From 9ccefbe2749edd84e53877cad5da32db9543acc9 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Mon, 13 Jun 2022 16:35:57 +0100 Subject: [PATCH 001/580] Add nightly 5.7 CI (#1438) --- .github/workflows/ci.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d0d7fd40c..2fd2dc9b3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,6 +24,9 @@ jobs: fail-fast: false matrix: include: + - image: swiftlang/swift:nightly-5.7-focal + # No TSAN because of: https://github.com/apple/swift/issues/59068 + # swift-test-flags: "--sanitize=thread" - image: swift:5.6-focal # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" @@ -50,6 +53,16 @@ jobs: fail-fast: false matrix: include: + - image: swiftlang/swift:nightly-5.7-focal + env: + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 428000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 174000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 181000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 - image: swift:5.6-focal env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 429000 @@ -95,6 +108,7 @@ jobs: fail-fast: false matrix: include: + - image: swiftlang/swift:nightly-5.7-focal - image: swift:5.6-focal - image: swift:5.5-focal - image: swift:5.4-focal From 703d3515458ea39ff32e05362f7eabff1600839c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 14 Jun 2022 08:49:15 +0100 Subject: [PATCH 002/580] Remove Cocoapods script and references (#1439) Motivation: We no longer support Cocoapods, so we should remove any references to them from the repo. Modifications: - Remove pod related tooling and documentation. Result: More accurate documentation. Cleaner repo. --- CGRPCZlib.podspec | 20 -- Examples/Google/SpeechToText/README.md | 2 - README.md | 35 +-- docs/plugin.md | 4 +- gRPC-Swift-Plugins.podspec | 20 -- gRPC-Swift.podspec | 29 --- scripts/build_podspecs.py | 302 ------------------------- scripts/bundle-plugins-for-release.sh | 3 - 8 files changed, 5 insertions(+), 410 deletions(-) delete mode 100644 CGRPCZlib.podspec delete mode 100644 gRPC-Swift-Plugins.podspec delete mode 100644 gRPC-Swift.podspec delete mode 100755 scripts/build_podspecs.py diff --git a/CGRPCZlib.podspec b/CGRPCZlib.podspec deleted file mode 100644 index 3c554749f..000000000 --- a/CGRPCZlib.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - - s.name = 'CGRPCZlib' - s.module_name = 'CGRPCZlib' - 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' - s.authors = { 'The gRPC contributors' => 'grpc-packages@google.com' } - - s.swift_version = '5.2' - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' - s.tvos.deployment_target = '10.0' - s.watchos.deployment_target = '6.0' - s.source = { :git => "https://github.com/grpc/grpc-swift.git", :tag => s.version } - - s.source_files = 'Sources/CGRPCZlib/**/*.{swift,c,h}' - -end \ No newline at end of file diff --git a/Examples/Google/SpeechToText/README.md b/Examples/Google/SpeechToText/README.md index faedb3749..6f219ee73 100644 --- a/Examples/Google/SpeechToText/README.md +++ b/Examples/Google/SpeechToText/README.md @@ -29,8 +29,6 @@ This project requires a Google Cloud API Key. Please [register](https://cloud.go ## Helpful Links * [Getting Started with Speech APIs](https://cloud.google.com/speech-to-text/docs/quickstart) -* [CocoaPods](https://cocoapods.org/) -* [gRPC-Swift CocoaPod](https://cocoapods.org/pods/gRPC-Swift) NOTE: Implementation of the AudioStreamManager is based off of [Google's Example Audio Streaming](https://github.com/GoogleCloudPlatform/ios-docs-samples/blob/master/speech/Swift/Speech-gRPC-Streaming/Speech/AudioController.swift) diff --git a/README.md b/README.md index 6fac045e6..040837469 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ insecure channels. gRPC Swift has recently been rewritten on top of [SwiftNIO][swift-nio] as opposed to the core library provided by the [gRPC project][grpc]. -Version | Implementation | Branch | `protoc` Plugin | CocoaPod | Support ---------|----------------|------------------------|-------------------------|-----------------------|----------------------------------------- -1.x | SwiftNIO | [`main`][branch-new] | `protoc-gen-grpc-swift` | [gRPC-Swift][pod-new] | Actively developed and supported -0.x | gRPC C library | [`cgrpc`][branch-old] | `protoc-gen-swiftgrpc` | [SwiftGRPC][pod-old] | No longer developed; security fixes only +Version | Implementation | Branch | `protoc` Plugin | Support +--------|----------------|------------------------|-------------------------|----------------------------------------- +1.x | SwiftNIO | [`main`][branch-new] | `protoc-gen-grpc-swift` | Actively developed and supported +0.x | gRPC C library | [`cgrpc`][branch-old] | `protoc-gen-swiftgrpc` | No longer developed; security fixes only The remainder of this README refers to the 1.x version of gRPC Swift. @@ -81,21 +81,6 @@ Alternatively, gRPC Swift can be manually integrated into a project: 1. Add the generated project to your own project, and 1. Add a build dependency on `GRPC`. -#### CocoaPods - -gRPC Swift is currently available [from CocoaPods][pod-new]. To integrate, add -the following line to your `Podfile`: - -```ruby - pod 'gRPC-Swift', '~> 1.0.0' -``` - -Then, run `pod install` from command line and use your project's generated -`.xcworkspace` file. You might also need to add `use_frameworks!` to your `Podfile`. - -*⚠️ If you have conficting modules as a result of having a transitive -dependency on '[gRPC-Core][grpc-core-pod]' see [grpc/grpc-swift#945][grpc-swift-945].* - ### Getting the `protoc` Plugins Binary releases of `protoc`, the Protocol Buffer Compiler, are available on @@ -111,14 +96,6 @@ To install these plugins, just copy the two executables (`protoc-gen-swift` and that is part of your `PATH` environment variable. Alternatively the full path to the plugins can be specified when using `protoc`. -Alternatively, you can get the latest precompiled version of the plugins by adding -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: @@ -178,15 +155,11 @@ Please get involved! See our [guidelines for contributing](CONTRIBUTING.md). [docs-tutorial]: ./docs/basic-tutorial.md [docs-interceptors-tutorial]: ./docs/interceptors-tutorial.md [grpc]: https://github.com/grpc/grpc -[grpc-core-pod]: https://cocoapods.org/pods/gRPC-Core -[grpc-swift-945]: https://github.com/grpc/grpc-swift/pull/945 [protobuf-releases]: https://github.com/protocolbuffers/protobuf/releases [swift-nio-platforms]: https://github.com/apple/swift-nio#supported-platforms [swift-nio]: https://github.com/apple/swift-nio [swift-protobuf]: https://github.com/apple/swift-protobuf [xcode-spm]: https://help.apple.com/xcode/mac/current/#/devb83d64851 -[pod-new]: https://cocoapods.org/pods/gRPC-Swift -[pod-old]: https://cocoapods.org/pods/SwiftGRPC [branch-new]: https://github.com/grpc/grpc-swift/tree/main [branch-old]: https://github.com/grpc/grpc-swift/tree/cgrpc [examples-out-of-source]: https://github.com/grpc/grpc-swift/tree/main/Examples diff --git a/docs/plugin.md b/docs/plugin.md index 923a318d4..ea1c7350b 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -96,9 +96,7 @@ desired, and setting `KeepMethodCasing=true` will yield stubs named `Foo`. The **GRPCModuleName** option allows the name of the gRPC Swift runtime module to be specified. The value, if not specified, defaults to "GRPC". -*Note: most users will not need to use this option. It is intended as a -workaround for CocoaPods users who may end up with a transitive dependency on -the gRPC core C library whose module name is also "GRPC".* +*Note: most users will not need to use this option.* ### SwiftProtobufModuleName diff --git a/gRPC-Swift-Plugins.podspec b/gRPC-Swift-Plugins.podspec deleted file mode 100644 index 562e741e3..000000000 --- a/gRPC-Swift-Plugins.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - - s.name = 'gRPC-Swift-Plugins' - 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' - s.authors = { 'The gRPC contributors' => 'grpc-packages@google.com' } - - s.swift_version = '5.2' - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' - s.tvos.deployment_target = '10.0' - s.watchos.deployment_target = '6.0' - s.source = { :http => "https://github.com/grpc/grpc-swift/releases/download/#{s.version}/protoc-grpc-swift-plugins-#{s.version}.zip"} - - s.preserve_paths = '*' - s.dependency 'gRPC-Swift', s.version.to_s - -end \ No newline at end of file diff --git a/gRPC-Swift.podspec b/gRPC-Swift.podspec deleted file mode 100644 index 82f8308c3..000000000 --- a/gRPC-Swift.podspec +++ /dev/null @@ -1,29 +0,0 @@ -Pod::Spec.new do |s| - - s.name = 'gRPC-Swift' - s.module_name = 'GRPC' - 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' - s.authors = { 'The gRPC contributors' => 'grpc-packages@google.com' } - - s.swift_version = '5.2' - s.ios.deployment_target = '10.0' - s.osx.deployment_target = '10.12' - s.tvos.deployment_target = '10.0' - s.watchos.deployment_target = '6.0' - s.source = { :git => "https://github.com/grpc/grpc-swift.git", :tag => s.version } - - s.source_files = 'Sources/GRPC/**/*.{swift,c,h}' - - s.dependency 'CGRPCZlib', s.version.to_s - s.dependency 'Logging', '>= 1.4.0', '< 2.0.0' - s.dependency 'SwiftNIO', '>= 2.32.0', '< 3.0.0' - s.dependency 'SwiftNIOExtras', '>= 1.4.0', '< 2.0.0' - s.dependency 'SwiftNIOHTTP2', '>= 1.18.2', '< 2.0.0' - s.dependency 'SwiftNIOSSL', '>= 2.14.0', '< 3.0.0' - s.dependency 'SwiftNIOTransportServices', '>= 1.11.1', '< 2.0.0' - s.dependency 'SwiftProtobuf', '>= 1.9.0', '< 2.0.0' - -end \ No newline at end of file diff --git a/scripts/build_podspecs.py b/scripts/build_podspecs.py deleted file mode 100755 index dac880b62..000000000 --- a/scripts/build_podspecs.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2020, 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 os -import json -import random -import string -import argparse -import subprocess -import sys - - -class TargetDependency(object): - def __init__(self, name): - self.name = name - - def __str__(self): - return "s.dependency '{name}', s.version.to_s".format(name=self.name) - - -class ProductDependency(object): - def __init__(self, name, lower, upper): - self.name = name - self.lower = lower - self.upper = upper - - def __str__(self): - return "s.dependency '{name}', '>= {lower}', '< {upper}'".format( - name=self.name, lower=self.lower, upper=self.upper) - - -class Pod: - def __init__(self, name, module_name, version, description, dependencies=None, is_plugins_pod=False): - self.name = name - self.module_name = module_name - self.version = version - self.is_plugins_pod = is_plugins_pod - self.dependencies = dependencies if dependencies is not None else [] - self.description = description - - def as_podspec(self): - print('\n') - print('Building Podspec for %s' % self.name) - print('-' * 80) - - indent=' ' * 4 - - podspec = "Pod::Spec.new do |s|\n\n" - podspec += indent + "s.name = '%s'\n" % self.name - if not self.is_plugins_pod: - podspec += indent + "s.module_name = '%s'\n" % self.module_name - podspec += indent + "s.version = '%s'\n" % self.version - podspec += indent + "s.license = { :type => 'Apache 2.0', :file => 'LICENSE' }\n" - podspec += indent + "s.summary = '%s'\n" % self.description - podspec += indent + "s.homepage = 'https://www.grpc.io'\n" - podspec += indent + "s.authors = { 'The gRPC contributors' => \'grpc-packages@google.com' }\n\n" - - podspec += indent + "s.swift_version = '5.4'\n" - podspec += indent + "s.ios.deployment_target = '10.0'\n" - podspec += indent + "s.osx.deployment_target = '10.12'\n" - podspec += indent + "s.tvos.deployment_target = '10.0'\n" - podspec += indent + "s.watchos.deployment_target = '6.0'\n" - - if self.is_plugins_pod: - podspec += indent + "s.source = { :http => \"https://github.com/grpc/grpc-swift/releases/download/#{s.version}/protoc-grpc-swift-plugins-#{s.version}.zip\"}\n\n" - podspec += indent + "s.preserve_paths = '*'\n" - else: - podspec += indent + "s.source = { :git => \"https://github.com/grpc/grpc-swift.git\", :tag => s.version }\n\n" - podspec += indent + "s.source_files = 'Sources/%s/**/*.{swift,c,h}'\n" % (self.module_name) - - podspec += "\n" if len(self.dependencies) > 0 else "" - - for dep in sorted(self.dependencies, key=lambda x: x.name): - podspec += indent + str(dep) + "\n" - - podspec += "\nend" - return podspec - -class PodManager: - def __init__(self, directory, version, should_publish, package_dump): - self.directory = directory - self.version = version - self.should_publish = should_publish - self.package_dump = package_dump - - def write(self, pod, contents): - print(' Writing to %s/%s.podspec ' % (self.directory, pod)) - with open('%s/%s.podspec' % (self.directory, pod), 'w') as podspec_file: - podspec_file.write(contents) - - def publish(self, pod_name): - subprocess.check_call(['pod', 'repo', 'update']) - print(' Publishing %s.podspec' % (pod_name)) - - args = ['pod', 'trunk', 'push', '--synchronous'] - - # The gRPC-Swift pod emits warnings about redundant availability - # guards on watchOS. These are redundant for the Cocoapods where we set - # the deployment target for watchOS to watchOS 6. However they are - # required for SPM where the deployment target is lower (and we can't - # raise it without breaking all of our consumers). We'll allow warnings - # to work around this. - if pod_name == "gRPC-Swift": - args.append("--allow-warnings") - - path_to_podspec = self.directory + '/' + pod_name + ".podspec" - args.append(path_to_podspec) - subprocess.check_call(args) - - def build_pods(self): - cgrpczlib_pod = Pod( - self.pod_name_for_grpc_target('CGRPCZlib'), - 'CGRPCZlib', - self.version, - 'Compression library that provides in-memory compression and decompression functions', - dependencies=self.build_dependency_list('CGRPCZlib') - ) - - grpc_pod = Pod( - self.pod_name_for_grpc_target('GRPC'), - 'GRPC', - self.version, - 'Swift gRPC code generator plugin and runtime library', - dependencies=self.build_dependency_list('GRPC') - ) - - grpc_plugins_pod = Pod( - 'gRPC-Swift-Plugins', - '', - self.version, - 'Swift gRPC code generator plugin binaries', - dependencies=[TargetDependency("gRPC-Swift")], - is_plugins_pod=True - ) - - return [cgrpczlib_pod, grpc_pod, grpc_plugins_pod] - - def go(self, start_from): - pods = self.build_pods() - - if start_from: - pods = pods[list(pod.name for pod in pods).index(start_from):] - - # Create .podspec files and publish - for target in pods: - self.write(target.name, target.as_podspec()) - if self.should_publish: - self.publish(target.name) - else: - print(' Skipping Publishing...') - - - def pod_name_for_package(self, name): - """Return the CocoaPod name for a given Swift package.""" - pod_mappings = { - 'swift-log': 'Logging', - 'swift-nio': 'SwiftNIO', - 'swift-nio-extras': 'SwiftNIOExtras', - 'swift-nio-http2': 'SwiftNIOHTTP2', - 'swift-nio-ssl': 'SwiftNIOSSL', - 'swift-nio-transport-services': 'SwiftNIOTransportServices', - 'SwiftProtobuf': 'SwiftProtobuf' - } - return pod_mappings[name] - - - def pod_name_for_grpc_target(self, name): - """Return the CocoaPod name for a given gRPC Swift target.""" - return { - 'GRPC': 'gRPC-Swift', - 'CGRPCZlib': 'CGRPCZlib' - }[name] - - - def get_package_requirements(self, package_name): - """ - Returns the lower and upper bound version requirements for a given - package dependency. - """ - for dependency in self.package_dump['dependencies']: - if dependency['name'] == package_name: - # There should only be 1 range. - requirement = dependency['requirement']['range'][0] - return (requirement['lowerBound'], requirement['upperBound']) - - # This shouldn't happen. - raise ValueError('Could not find package called', package_name) - - - def get_dependencies(self, target_name): - """ - Returns a tuple of dependency lists for a given target. - - The first entry is the list of product dependencies; dependencies on - products from other packages. The second entry is a list of target - dependencies, i.e. dependencies on other targets within the package. - """ - for target in self.package_dump['targets']: - if target['name'] == target_name: - product_dependencies = set() - target_dependencies = [] - - for dependency in target['dependencies']: - if 'product' in dependency: - product_dependencies.add(dependency['product'][1]) - elif 'target' in dependency: - target_dependencies.append(dependency['target'][0]) - else: - raise ValueError('Unexpected dependency type:', dependency) - - return (product_dependencies, target_dependencies) - - # This shouldn't happen. - raise ValueError('Could not find dependency called', target_name) - - - def build_dependency_list(self, target_name): - """ - Returns a list of dependencies for the given target. - - Dependencies may be either 'TargetDependency' or 'ProductDependency'. - """ - product, target = self.get_dependencies(target_name) - dependencies = [] - - for package_name in product: - (lower, upper) = self.get_package_requirements(package_name) - pod_name = self.pod_name_for_package(package_name) - dependencies.append(ProductDependency(pod_name, lower, upper)) - - for target_name in target: - pod_name = self.pod_name_for_grpc_target(target_name) - dependencies.append(TargetDependency(pod_name)) - - return dependencies - - -def dir_path(path): - if os.path.isdir(path): - return path - - raise NotADirectoryError(path) - -def main(): - parser = argparse.ArgumentParser(description='Build Podspec files for gRPC Swift') - - parser.add_argument( - '-p', - '--path', - type=dir_path, - help='The directory where generated podspec files will be saved. If not passed, defaults to the current working directory.' - ) - - parser.add_argument( - '-u', - '--upload', - action='store_true', - help='Determines if the newly built Podspec files should be pushed.' - ) - - parser.add_argument( - '-f', - '--start-from', - help='The name of the Podspec to start from.' - ) - - parser.add_argument('version') - args = parser.parse_args() - - should_publish = args.upload - version = args.version - path = args.path - start_from = args.start_from - - if not path: - path = os.getcwd() - - print("Reading package description...") - lines = subprocess.check_output(["swift", "package", "dump-package"]) - package_dump = json.loads(lines) - assert(package_dump["name"] == "grpc-swift") - - pod_manager = PodManager(path, version, should_publish, package_dump) - pod_manager.go(start_from) - - return 0 - -if __name__ == "__main__": - main() diff --git a/scripts/bundle-plugins-for-release.sh b/scripts/bundle-plugins-for-release.sh index 9f2c0e260..096d38834 100755 --- a/scripts/bundle-plugins-for-release.sh +++ b/scripts/bundle-plugins-for-release.sh @@ -25,9 +25,6 @@ set -eu # └── bin # ├── protoc-gen-grpc-swift # └── protoc-gen-swift -# -# Note: the name of the generated zip should match that expected by the -# buid_podspecs.py script. if [[ $# -lt 1 ]]; then echo "Usage: $0 RELEASE_VERSION" From f822ce48a50bdf282844cbdc75dd060ac4fdd838 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 15 Jun 2022 14:23:34 +0100 Subject: [PATCH 003/580] Build release plugins as universal binaries (#1441) Motivation: We upload protoc plugins for each release. As it stands, the architecture these plugins are built for is that of the machine building them. We should upload universal binaries instead. Modifications: - Modify the bundling script to build universal binaries Result: Better platform support for the uploaded macOS plugins. --- scripts/bundle-plugins-for-release.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/bundle-plugins-for-release.sh b/scripts/bundle-plugins-for-release.sh index 096d38834..bb49b2fe9 100755 --- a/scripts/bundle-plugins-for-release.sh +++ b/scripts/bundle-plugins-for-release.sh @@ -45,11 +45,13 @@ stage_bin="${stage}/bin" mkdir -p "${stage_bin}" # Make the plugins. -make -C "${root}" plugins +swift build -c release --arch arm64 --arch x86_64 --product protoc-gen-grpc-swift +swift build -c release --arch arm64 --arch x86_64 --product protoc-gen-swift +binpath=$(swift build -c release --arch arm64 --arch x86_64 --show-bin-path) # Copy them to the stage. -cp "${root}/protoc-gen-grpc-swift" "${stage_bin}" -cp "${root}/protoc-gen-swift" "${stage_bin}" +cp "${binpath}/protoc-gen-grpc-swift" "${stage_bin}" +cp "${binpath}/protoc-gen-swift" "${stage_bin}" # Copy the LICENSE to the stage. cp "${root}/LICENSE" "${stage}" From a98b41064bc891fb54a6cfc7da9d0089ed4dd3a4 Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Thu, 23 Jun 2022 09:16:03 +0100 Subject: [PATCH 004/580] Raise minimum NIO version to 2.36.0 (#1444) With the merge of the async/await support to main we now require NIO 2.36.0, which is the release that contains the new availability requirements for EventLoopFuture.get Resolves #1443 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 7dcac1125..322b56fc3 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let argumentParserMinimumVersion: Version = "1.0.0" let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.32.0" + from: "2.36.0" ), .package( url: "https://github.com/apple/swift-nio-http2.git", From d63f32c930e8d71bd2ac7e271c60fa9bacff123e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 Jun 2022 09:50:09 +0100 Subject: [PATCH 005/580] Bump version number to 1.8.1 (#1445) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.8.1 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 815624769..d9670425a 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 8 /// The patch version. - internal static let patch = 0 + internal static let patch = 1 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From d772b688d718c2eb29f99b535fc4d92ebba031c5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 Jun 2022 09:52:57 +0100 Subject: [PATCH 006/580] Update p12 bundle for tests (#1442) Motivation: Tests on macOS using TLS provided by Network.framework (rather than NIOSSL) started failing because the certificate in the pkcs12 bundle expired. Modifications: Update testing certs. Result: Tests pass. --- Sources/GRPCSampleData/bundle.p12 | Bin 2405 -> 2405 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Sources/GRPCSampleData/bundle.p12 b/Sources/GRPCSampleData/bundle.p12 index ec1cd27c2b36a430bd194fec5c32c4d4dd3ee77b..cb9a3e369376f563dbd985c90726fd9e256bfd08 100644 GIT binary patch delta 2271 zcmV<52q5?666F$*U4L1kWV*R@%bWrN2mpYB1Aq*I`QfA`$HnRZzDKK_4O&vlZIDa# zKMwR!|M}wUdMbT6AqB1Bd|8LKqlW)k-~nakStpu_lT{ipgltWaXUhJ^1j-#l5uCvO zn?rZ%0eOfTh|iOu1xAoN%aa+p{@zAR_BryciCa}w7fJ86t$)=KU;3lljtycr4Ff~$ z2pc+JY1Qg}Rb$i>CkFdk%p6zyCEj%Gs|0t?TC7rRTFCR=8QdNh{6Mbfpii__S6|!N0d6ZTsec**+0byz3{A^l7A_&z4zOFcwRH1i=-1~AjvY{iUAPxK3q2zM1pOBz za)`h1GaWc`%6|sVRk5_B)(6fv6*n`>=X)kdg_110PQ!OZXjrwhWlM~{KlG&8ic)2y z)FOxi2{BS2+d%o=ENtp3A%TDU&=4M%hVrS>PGE1z&wqpv;nv_s{86+ojzPUG5GRwb zPbPJC>81M$%1Rcmi&3TgVl$;`B2j!OV%%*@Da{ny=F4@2>wvZw-enKN6+sFpTja4* zso{;mNC=(hr?AZ@F?XsfBY%s#NfNnzaVQiB^fH+WJp`%uDRL9WCP7Jn8vl4_M z$oOk?`9F~?ique|mXyWps>c{$6v`gl4G>d%$jp;UA4ZWo25NRCbhpP$m*GIyBwfA! zdddYMF~wQOKA;YVT`}z6e&566W<>l-AGh0s?SKgHBX^l+Hmwn$m4zbrdS9@^?n8xsdD6^gL zBk=}O5TVnGLGot>E?ju4GOMz|iN1K|4}X04M||~T|sPnF=2LRkTShHF(*>+7C{o{ELYbns7Yh%D+4tE-S|r zg*1Oah#jSjzP=GXzJklB=6IWx#}NYlSrQ=h(((6OE{RdH>xt`HgL!8nvVUe&WsA+I zGkUH!>Zd0Z7)TK|knPZCg*FWSs|W#cZdHw)A&j%jcr6n+$ydm{Iby-p_HMP$GTMpa z88|U#GZpT7#DK(Mm#iBxk9kz8`Q*FODD3&M{Vs^H7b%zOd%ZT7{b=2g9Qs|Ck$xtm z7g|13hmc!_NShq+E+ZPE<$r6h_OzOq3i`<-KGRBZw!qbe+W>)H_E`;#nEUhu#P#lz zmn!q`Qk~UR^oJR@QNEJ#dc;MJqM|`nd#0?~sPBK34tXpMl!~lU<>tuA0m{5yW6?SZ zONp~o03liB^q`mY?s7j%IalT2Ma3)=)Je}(mscSa3x92ayl>z(Mt``5*sT7peR!n# z@CNBI&689W@7h0g<+pf?oSON}ND ztZ5YH4;OJ9PI1B35V*tHgEVP!3mB*COv<8dW)_>2T8erDb@WE9#

`N}^B>#lxsyRu%f$V+?I-@K>;qHR! zw?c7#GZ6xHS?DL63-mp3W8E3?{8vV z)tf zJ{3!Md+FS|b$EBaosQ?Vg)(VlFS^bzBQ89xdF=a2oUjcHn12HrtwT3|qKZoq>N_DT zI=40H(3Mt_o(=|;>>Yv6+Vr~DBCwnNc3q=fYNK|Umk*!;kV?IV(_cTcI$1}cU5r~;0po(sV4I;!G>U^(nVWTNr}W%M`O)|_D0G&5yV>sp7hR4rJi|@8V!ID)5(7-(ylRHt7&;UzDnFd*|pk}@FBxpIsi@>#5CTF{8YVbsT z0A)>^fPb>BTYN-?FE5dTM3Mpx440urS%ofJ?cq4N@*UhjIh(|MqDUncF!v=vS&Rk6 zs_2BwUtm6Az9!Ei8Cl!79H{K)M4NC289g>F6MseGF55|HTd2oWiCw~p%br*G9Tu%) zA=`vNMGOckt*)*v1B93;VFUBMQAq`moU6IkgMX2Q;K~1D2{!njqJ9@G#Z$T?oI@lj zr2(EIK!;)m!k6+75TBzbxRP?XY%C&3=2WL{Q|7PO7WlJ0gUZ5*qt-SW!XfD!Fo8TA zz<-ErK4?=N1@mySeLt~FH30uL(wbm0QMTXzKOgdeE^|lZ%Hs=|302utk%UP65HcrI zqipYMu2V|Z;Gvi0;D^K-cyJU?W{B2@HApM1C2#FPL_`wC-2I1LSD{RLr>MH$wHwg{ zIJ&>7JZP4RTuW}Utw$!y z`hFEo7JCi?!}H%h%Xd@FBC^6_$e0y?Lt!@&@3?7s?8d*^K+)+WsRwObI^$T8F+35HGX)6?jA2^yY`2VVz!Bea3dvwt+t z8#pHvZJyR|m82pNJVzj$8y9TQME}Id;`Wc$ll#;D&eJ-LAgV}lgSQ8gGYN!*b(dsU z?fRG!F4?uZi(214ryw4J)K|)jh~qy@({3bP;(3nnaeL3eMPhKau#H=RJ}l}z^>(;X z)E&9I;VK?kOh+PhT@XHnZ-37;Ixcwos=$)?6}=tZN-$gx3ll_5{zKVLI&G+8 z8?1$XKEk$c9Q?RoZq~JXBL(*7LRCMj$L4&~okB!bYGz}$To2xwpJoCYpSDpn7dLfq zm*bcbI3qa+>J+*_&n-isOmRCqEbjMD379=c9)L+jY;KvcBU=(&zc>ehK7S{kB7O-m zz4#reP~rFZhWy`%TN$uLZ0^m8Tsb3t2^d3ssF3Da&#zuE&W)`&zhGYV^)H3I zMUI>jwck3Nyu5onmYD5P7Ds`yxvPBzoC8eiuwZ%lSBGYBXoYXzJzbX8P3e4Tt2LvZ z$V`NNd)!n6ePPs4xqtBv9}M00v`6z&jHoU?61!q_`p6SIS+ zH1AIN#&P_`RVV6Xlw0slRv1W7KaJNmtXH<%?#(t>4@~Kjj(=6>a>^J$Mkwk5w}K$} z?kre_nSU;y00X;$=Wy+IFsa#>w(%oqN^U;={WEWmD~a8*=IpbuN*zE|!wTPHu1apE zsfuPpI#a^84zj+OE4>|uI}N|hepJY~Id~reynoyj^ER56ia2t{)xjqkI@1g}#g^Z@@RY^rJ5>wzt~2hp%f8l$ z;CQexB`_lf2`Yw2hW8Bt2^BFG1QhJ;K?)q7_vuQtdOR4#uYm4ItDn>`F)$%82?hl# t4g&%j1povTv01k46_5+}sX}9xluO1fYl-dN1PH3WfuyQ;e*6Lg2mnUTIz|8h From ac0a76d649244347086a3f606913e09ca4327f10 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 12 Jul 2022 10:00:14 +0100 Subject: [PATCH 007/580] Use swift-atomics (#1453) Motivation: NIO deprecated its atomics in apple/swift-nio#2204. Modifications: - Use swift-atomics in the `QPSBenchmark` sub-package - Use a lock in the one test which used `NIOAtomic` Result: Not using deprecated code. --- Performance/QPSBenchmark/Package.swift | 2 ++ .../QPSBenchmark/Runtime/AsyncClient.swift | 26 ++++++++++++------- ...treamResponseHandlerRetainCycleTests.swift | 15 ++++++++--- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Performance/QPSBenchmark/Package.swift b/Performance/QPSBenchmark/Package.swift index a498884ca..a96a368ca 100644 --- a/Performance/QPSBenchmark/Package.swift +++ b/Performance/QPSBenchmark/Package.swift @@ -27,6 +27,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", from: "2.32.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), .package( url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "1.0.0-alpha" @@ -42,6 +43,7 @@ let package = Package( name: "QPSBenchmark", dependencies: [ .product(name: "GRPC", package: "grpc-swift"), + .product(name: "Atomics", package: "swift-atomics"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncClient.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncClient.swift index 203f21da4..5cd4b1d60 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncClient.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncClient.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +import Atomics import BenchmarkUtils import Foundation import GRPC @@ -168,8 +169,8 @@ final class AsyncQPSClient: QPSClient { /// Succeeds after a stop has been requested and all outstanding requests have completed. private var stopComplete: EventLoopPromise - private let running: NIOAtomic = .makeAtomic(value: false) - private let outstanding: NIOAtomic = .makeAtomic(value: 0) + private let running = ManagedAtomic(false) + private let outstanding = ManagedAtomic(0) private var requestMaker: RequestMakerType @@ -211,15 +212,20 @@ final class AsyncQPSClient: QPSClient { // - when a request finishes it will either start a new request or decrement the // the atomic counter (if we've been told to stop) // - if the counter drops to zero we're finished. - let exchangedRunning = self.running.compareAndExchange(expected: false, desired: true) - precondition(exchangedRunning, "launchRequests should only be called once") + let exchangedRunning = self.running.compareExchange( + expected: false, + desired: true, + ordering: .relaxed + ) + precondition(exchangedRunning.exchanged, "launchRequests should only be called once") // We only decrement the outstanding count when running has been changed back to false. - let exchangedOutstanding = self.outstanding.compareAndExchange( + let exchangedOutstanding = self.outstanding.compareExchange( expected: 0, - desired: self.maxPermittedOutstandingRequests + desired: self.maxPermittedOutstandingRequests, + ordering: .relaxed ) - precondition(exchangedOutstanding, "launchRequests should only be called once") + precondition(exchangedOutstanding.exchanged, "launchRequests should only be called once") for _ in 0 ..< self.maxPermittedOutstandingRequests { self.requestMaker.makeRequest().whenComplete { _ in @@ -229,11 +235,11 @@ final class AsyncQPSClient: QPSClient { } private func makeRequest() { - if self.running.load() { + if self.running.load(ordering: .relaxed) { self.requestMaker.makeRequest().whenComplete { _ in self.makeRequest() } - } else if self.outstanding.sub(1) == 1 { + } else if self.outstanding.loadThenWrappingDecrement(ordering: .relaxed) == 1 { self.stopIsComplete() } // else we're no longer running but not all RPCs have finished. } @@ -260,7 +266,7 @@ final class AsyncQPSClient: QPSClient { /// - returns: A future which can be waited on to signal when all activity has ceased. func stop() -> EventLoopFuture { self.requestMaker.requestStop() - self.running.store(false) + self.running.store(false, ordering: .relaxed) return self.stopComplete.futureResult } } diff --git a/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift b/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift index bfda4bdec..7409ce99a 100644 --- a/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift +++ b/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift @@ -57,10 +57,19 @@ final class StreamResponseHandlerRetainCycleTests: GRPCTestCase { func testHandlerClosureIsReleasedOnceStreamEnds() { final class Counter { - private let atomic = NIOAtomic.makeAtomic(value: 0) - func increment() { self.atomic.add(1) } + private let lock = Lock() + private var _value = 0 + + func increment() { + self.lock.withLockVoid { + self._value += 1 + } + } + var value: Int { - self.atomic.load() + return self.lock.withLock { + self._value + } } } From babcff1f9d1ff209c75852428d0c94acfacf6890 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 12 Jul 2022 17:46:38 +0100 Subject: [PATCH 008/580] Provide an error when cancelling async writer (#1456) Motivation: The `AsyncWriter` fails any pending writes with a `CancellationError` if the writer is cancelled. This can be misleading if the writer is cancelled because a connection could not be established, for example. Modifications: - Pass an error through `AsyncWriter.cancel` Result: Pending writes which are failed because the writer has been cancelled have more relevant errors. --- .../GRPC/AsyncAwaitSupport/AsyncWriter.swift | 16 ++++++------- .../GRPCAsyncBidirectionalStreamingCall.swift | 8 +++---- .../GRPCAsyncClientStreamingCall.swift | 2 +- .../GRPCAsyncServerHandler.swift | 4 ++-- .../AsyncAwaitSupport/AsyncClientTests.swift | 23 ++++++++++++++++++- .../AsyncAwaitSupport/AsyncWriterTests.swift | 8 +++---- 6 files changed, 40 insertions(+), 21 deletions(-) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift index 844318d75..aba4c9de5 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift @@ -198,9 +198,9 @@ internal final actor AsyncWriter: Sendable { /// As ``cancel()`` but executed asynchronously. @usableFromInline - internal nonisolated func cancelAsynchronously() { + internal nonisolated func cancelAsynchronously(withError error: Error) { Task { - await self.cancel() + await self.cancel(withError: error) } } @@ -209,7 +209,7 @@ internal final actor AsyncWriter: Sendable { /// Any pending writes will be dropped and their continuations will be resumed with /// a `CancellationError`. Any writes after cancellation has completed will also fail. @usableFromInline - internal func cancel() { + internal func cancel(withError error: Error) { // If there's an end we should fail that last. let pendingEnd: PendingEnd? @@ -228,13 +228,11 @@ internal final actor AsyncWriter: Sendable { pendingEnd = nil } - let cancellationError = CancellationError() - while let pending = self._pendingElements.popFirst() { - pending.continuation.resume(throwing: cancellationError) + pending.continuation.resume(throwing: error) } - pendingEnd?.continuation.resume(throwing: cancellationError) + pendingEnd?.continuation.resume(throwing: error) } /// Write an `element`. @@ -263,7 +261,7 @@ internal final actor AsyncWriter: Sendable { throw GRPCAsyncWriterError.tooManyPendingWrites } } onCancel: { - self.cancelAsynchronously() + self.cancelAsynchronously(withError: CancellationError()) } } @@ -283,7 +281,7 @@ internal final actor AsyncWriter: Sendable { } } } onCancel: { - self.cancelAsynchronously() + self.cancelAsynchronously(withError: CancellationError()) } } } diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 26a9b9237..139beb937 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -92,7 +92,7 @@ public struct GRPCAsyncBidirectionalStreamingCall Echo_EchoAsyncClient { + private func makeClient( + port: Int, + configure: (inout GRPCChannelPool.Configuration) -> Void = { _ in } + ) throws -> Echo_EchoAsyncClient { precondition(self.pool == nil) self.pool = try GRPCChannelPool.with( @@ -73,6 +76,7 @@ final class AsyncClientCancellationTests: GRPCTestCase { eventLoopGroup: self.group ) { $0.backgroundActivityLogger = self.clientLogger + configure(&$0) } return Echo_EchoAsyncClient(channel: self.pool) @@ -394,6 +398,23 @@ final class AsyncClientCancellationTests: GRPCTestCase { return .bidirectionalStreaming(echo.makeUpdateCall()) } } + + func testConnectionFailureCancelsRequestStreamWithError() async throws { + let echo = try self.makeClient(port: 0) { + // Configure a short wait time; we will not start a server so fail quickly. + $0.connectionPool.maxWaitTime = .milliseconds(10) + } + + let update = echo.makeUpdateCall() + await XCTAssertThrowsError(try await update.requestStream.send(.init())) { error in + XCTAssertFalse(error is CancellationError) + } + + let collect = echo.makeCollectCall() + await XCTAssertThrowsError(try await collect.requestStream.send(.init())) { error in + XCTAssertFalse(error is CancellationError) + } + } } #endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift index 23d6c6ddd..b0123999d 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift @@ -170,7 +170,7 @@ internal class AsyncWriterTests: GRPCTestCase { async let pendingWrite: Void = writer.write("foo") - await writer.cancel() + await writer.cancel(withError: CancellationError()) do { try await pendingWrite @@ -202,7 +202,7 @@ internal class AsyncWriterTests: GRPCTestCase { async let pendingWrite: Void = writer.finish(42) - await writer.cancel() + await writer.cancel(withError: CancellationError()) do { try await pendingWrite @@ -229,13 +229,13 @@ internal class AsyncWriterTests: GRPCTestCase { let delegate = CollectingDelegate() let writer = AsyncWriter(delegate: delegate) - await writer.cancel() + await writer.cancel(withError: CancellationError()) await XCTAssertThrowsError(try await writer.write("1")) { error in XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) } // Fine, no need to throw. Nothing should change. - await writer.cancel() + await writer.cancel(withError: CancellationError()) await XCTAssertThrowsError(try await writer.write("2")) { error in XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) } From 4c63368b7462305903507e8acebd77264c0fb695 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 13 Jul 2022 11:14:04 +0100 Subject: [PATCH 009/580] Bump version number to 1.8.2 (#1457) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.8.2 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index d9670425a..7aaa183cb 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 8 /// The patch version. - internal static let patch = 1 + internal static let patch = 2 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 1cde9ee7273f1b314826c03db04c550935d0bd44 Mon Sep 17 00:00:00 2001 From: Pouya Yarandi <30620887+pouyayarandi@users.noreply.github.com> Date: Tue, 26 Jul 2022 14:26:36 +0430 Subject: [PATCH 010/580] Make path available in async calls (#1462) --- .../GRPCAsyncBidirectionalStreamingCall.swift | 5 +++++ .../AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift | 5 +++++ .../AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift | 5 +++++ Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 139beb937..7f2c3271f 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -35,6 +35,11 @@ public struct GRPCAsyncBidirectionalStreamingCall: Sendabl self.call.options } + /// The path used to make the RPC. + public var path: String { + self.call.path + } + /// Cancel this RPC if it hasn't already completed. public func cancel() { self.call.cancel(promise: nil) From d114c5ec34015bab663b3b7afaa7d6197656e47e Mon Sep 17 00:00:00 2001 From: David Nadoba Date: Mon, 1 Aug 2022 19:14:15 +0200 Subject: [PATCH 011/580] Bump version number to 1.9.0 (#1465) Motivation: We plan on tagging a release soon. Modifications: Bump the version to 1.9.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 7aaa183cb..c2d272475 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,10 +19,10 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 8 + internal static let minor = 9 /// The patch version. - internal static let patch = 2 + internal static let patch = 0 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From f624b0022ac73d505bb92b9e56f3b7851a2777c2 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 2 Aug 2022 16:23:40 +0100 Subject: [PATCH 012/580] Prepare for DocC (#1467) # Motivation We wanna use DocC for publishing docs. # Modification Setup our `Package.swift` manifests to enable proper doc generation. # Result We can now use DocC to generate docs. --- Package.swift | 6 +- Package@swift-5.4.swift | 478 +++++++++++++++++++++++++++++++++++++++ Package@swift-5.5.swift | 478 +++++++++++++++++++++++++++++++++++++++ scripts/license-check.sh | 5 + 4 files changed, 966 insertions(+), 1 deletion(-) create mode 100644 Package@swift-5.4.swift create mode 100644 Package@swift-5.5.swift diff --git a/Package.swift b/Package.swift index 322b56fc3..09fae334f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.6 /* * Copyright 2017, gRPC Authors All rights reserved. * @@ -67,6 +67,10 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-argument-parser.git", from: argumentParserMinimumVersion ), + .package( + url: "https://github.com/apple/swift-docc-plugin", + from: "1.0.0" + ), ].appending( .package( url: "https://github.com/apple/swift-nio-ssl.git", diff --git a/Package@swift-5.4.swift b/Package@swift-5.4.swift new file mode 100644 index 000000000..6122630d2 --- /dev/null +++ b/Package@swift-5.4.swift @@ -0,0 +1,478 @@ +// swift-tools-version:5.4 +/* + * Copyright 2022, 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 PackageDescription +// swiftformat puts the next import before the tools version. +// swiftformat:disable:next sortedImports +import class Foundation.ProcessInfo + +let grpcPackageName = "grpc-swift" +let grpcProductName = "GRPC" +let cgrpcZlibProductName = "CGRPCZlib" +let grpcTargetName = grpcProductName +let cgrpcZlibTargetName = cgrpcZlibProductName + +let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil + +#if swift(>=5.6) +// swift-argument-parser raised its minimum Swift version in 1.1.0 but +// also accidentally broke API. This was fixed in "1.1.1". +let argumentParserMinimumVersion: Version = "1.1.1" +#else +let argumentParserMinimumVersion: Version = "1.0.0" +#endif + +// MARK: - Package Dependencies + +let packageDependencies: [Package.Dependency] = [ + .package( + url: "https://github.com/apple/swift-nio.git", + from: "2.36.0" + ), + .package( + url: "https://github.com/apple/swift-nio-http2.git", + from: "1.22.0" + ), + .package( + url: "https://github.com/apple/swift-nio-transport-services.git", + from: "1.11.1" + ), + .package( + url: "https://github.com/apple/swift-nio-extras.git", + from: "1.4.0" + ), + .package( + name: "SwiftProtobuf", + url: "https://github.com/apple/swift-protobuf.git", + from: "1.19.0" + ), + .package( + url: "https://github.com/apple/swift-log.git", + from: "1.4.0" + ), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + from: argumentParserMinimumVersion + ), +].appending( + .package( + url: "https://github.com/apple/swift-nio-ssl.git", + from: "2.14.0" + ), + if: includeNIOSSL +) + +// MARK: - Target Dependencies + +extension Target.Dependency { + // Target dependencies; external + static let grpc: Self = .target(name: grpcTargetName) + static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) + static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") + + // Target dependencies; internal + static let grpcSampleData: Self = .target(name: "GRPCSampleData") + static let echoModel: Self = .target(name: "EchoModel") + static let echoImplementation: Self = .target(name: "EchoImplementation") + static let helloWorldModel: Self = .target(name: "HelloWorldModel") + static let routeGuideModel: Self = .target(name: "RouteGuideModel") + static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") + static let interopTestImplementation: Self = + .target(name: "GRPCInteroperabilityTestsImplementation") + + // Product dependencies + static let argumentParser: Self = .product( + name: "ArgumentParser", + package: "swift-argument-parser" + ) + static let nio: Self = .product(name: "NIO", package: "swift-nio") + static let nioConcurrencyHelpers: Self = .product( + name: "NIOConcurrencyHelpers", + package: "swift-nio" + ) + static let nioCore: Self = .product(name: "NIOCore", package: "swift-nio") + static let nioEmbedded: Self = .product(name: "NIOEmbedded", package: "swift-nio") + static let nioExtras: Self = .product(name: "NIOExtras", package: "swift-nio-extras") + static let nioFoundationCompat: Self = .product(name: "NIOFoundationCompat", package: "swift-nio") + static let nioHTTP1: Self = .product(name: "NIOHTTP1", package: "swift-nio") + static let nioHTTP2: Self = .product(name: "NIOHTTP2", package: "swift-nio-http2") + static let nioPosix: Self = .product(name: "NIOPosix", package: "swift-nio") + static let nioSSL: Self = .product(name: "NIOSSL", package: "swift-nio-ssl") + static let nioTLS: Self = .product(name: "NIOTLS", package: "swift-nio") + static let nioTransportServices: Self = .product( + name: "NIOTransportServices", + package: "swift-nio-transport-services" + ) + static let logging: Self = .product(name: "Logging", package: "swift-log") + static let protobuf: Self = .product(name: "SwiftProtobuf", package: "SwiftProtobuf") + static let protobufPluginLibrary: Self = .product( + name: "SwiftProtobufPluginLibrary", + package: "SwiftProtobuf" + ) +} + +// MARK: - Targets + +extension Target { + static let grpc: Target = .target( + name: grpcTargetName, + dependencies: [ + .cgrpcZlib, + .nio, + .nioCore, + .nioPosix, + .nioEmbedded, + .nioFoundationCompat, + .nioTLS, + .nioTransportServices, + .nioHTTP1, + .nioHTTP2, + .nioExtras, + .logging, + .protobuf, + ].appending( + .nioSSL, if: includeNIOSSL + ), + path: "Sources/GRPC" + ) + + static let cgrpcZlib: Target = .target( + name: cgrpcZlibTargetName, + path: "Sources/CGRPCZlib", + linkerSettings: [ + .linkedLibrary("z"), + ] + ) + + static let protocGenGRPCSwift: Target = .executableTarget( + name: "protoc-gen-grpc-swift", + dependencies: [ + .protobuf, + .protobufPluginLibrary, + ], + exclude: [ + "README.md", + ] + ) + + static let grpcTests: Target = .testTarget( + name: "GRPCTests", + dependencies: [ + .grpc, + .echoModel, + .echoImplementation, + .helloWorldModel, + .interopTestModels, + .interopTestImplementation, + .grpcSampleData, + .nioCore, + .nioConcurrencyHelpers, + .nioPosix, + .nioTLS, + .nioHTTP1, + .nioHTTP2, + .nioEmbedded, + .nioTransportServices, + .logging, + ].appending( + .nioSSL, if: includeNIOSSL + ), + exclude: [ + "Codegen/Normalization/normalization.proto", + ] + ) + + static let interopTestModels: Target = .target( + name: "GRPCInteroperabilityTestModels", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + exclude: [ + "README.md", + "generate.sh", + "src/proto/grpc/testing/empty.proto", + "src/proto/grpc/testing/empty_service.proto", + "src/proto/grpc/testing/messages.proto", + "src/proto/grpc/testing/test.proto", + "unimplemented_call.patch", + ] + ) + + static let interopTestImplementation: Target = .target( + name: "GRPCInteroperabilityTestsImplementation", + dependencies: [ + .grpc, + .interopTestModels, + .nioCore, + .nioPosix, + .nioHTTP1, + .logging, + ].appending( + .nioSSL, if: includeNIOSSL + ) + ) + + static let interopTests: Target = .executableTarget( + name: "GRPCInteroperabilityTests", + dependencies: [ + .grpc, + .interopTestImplementation, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ] + ) + + static let backoffInteropTest: Target = .executableTarget( + name: "GRPCConnectionBackoffInteropTest", + dependencies: [ + .grpc, + .interopTestModels, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ], + exclude: [ + "README.md", + ] + ) + + static let perfTests: Target = .executableTarget( + name: "GRPCPerformanceTests", + dependencies: [ + .grpc, + .grpcSampleData, + .nioCore, + .nioEmbedded, + .nioPosix, + .nioHTTP2, + .argumentParser, + ] + ) + + static let grpcSampleData: Target = .target( + name: "GRPCSampleData", + dependencies: includeNIOSSL ? [.nioSSL] : [], + exclude: [ + "bundle.p12", + ] + ) + + static let echoModel: Target = .target( + name: "EchoModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/Echo/Model", + exclude: [ + "echo.proto", + ] + ) + + static let echoImplementation: Target = .target( + name: "EchoImplementation", + dependencies: [ + .echoModel, + .grpc, + .nioCore, + .nioHTTP2, + .protobuf, + ], + path: "Sources/Examples/Echo/Implementation" + ) + + static let echo: Target = .executableTarget( + name: "Echo", + dependencies: [ + .grpc, + .echoModel, + .echoImplementation, + .grpcSampleData, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ].appending( + .nioSSL, if: includeNIOSSL + ), + path: "Sources/Examples/Echo/Runtime" + ) + + static let helloWorldModel: Target = .target( + name: "HelloWorldModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/HelloWorld/Model", + exclude: [ + "helloworld.proto", + ] + ) + + static let helloWorldClient: Target = .executableTarget( + name: "HelloWorldClient", + dependencies: [ + .grpc, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/HelloWorld/Client" + ) + + static let helloWorldServer: Target = .executableTarget( + name: "HelloWorldServer", + dependencies: [ + .grpc, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/HelloWorld/Server" + ) + + static let routeGuideModel: Target = .target( + name: "RouteGuideModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/RouteGuide/Model", + exclude: [ + "route_guide.proto", + ] + ) + + static let routeGuideClient: Target = .executableTarget( + name: "RouteGuideClient", + dependencies: [ + .grpc, + .routeGuideModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/RouteGuide/Client" + ) + + static let routeGuideServer: Target = .executableTarget( + name: "RouteGuideServer", + dependencies: [ + .grpc, + .routeGuideModel, + .nioCore, + .nioConcurrencyHelpers, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/RouteGuide/Server" + ) + + static let packetCapture: Target = .executableTarget( + name: "PacketCapture", + dependencies: [ + .grpc, + .echoModel, + .nioCore, + .nioPosix, + .nioExtras, + .argumentParser, + ], + path: "Sources/Examples/PacketCapture", + exclude: [ + "README.md", + ] + ) +} + +// MARK: - Products + +extension Product { + static let grpc: Product = .library( + name: grpcProductName, + targets: [grpcTargetName] + ) + + static let cgrpcZlib: Product = .library( + name: cgrpcZlibProductName, + targets: [cgrpcZlibTargetName] + ) + + static let protocGenGRPCSwift: Product = .executable( + name: "protoc-gen-grpc-swift", + targets: ["protoc-gen-grpc-swift"] + ) +} + +// MARK: - Package + +let package = Package( + name: grpcPackageName, + products: [ + .grpc, + .cgrpcZlib, + .protocGenGRPCSwift, + ], + dependencies: packageDependencies, + targets: [ + // Products + .grpc, + .cgrpcZlib, + .protocGenGRPCSwift, + + // Tests etc. + .grpcTests, + .interopTestModels, + .interopTestImplementation, + .interopTests, + .backoffInteropTest, + .perfTests, + .grpcSampleData, + + // Examples + .echoModel, + .echoImplementation, + .echo, + .helloWorldModel, + .helloWorldClient, + .helloWorldServer, + .routeGuideModel, + .routeGuideClient, + .routeGuideServer, + .packetCapture, + ] +) + +extension Array { + func appending(_ element: Element, if condition: Bool) -> [Element] { + if condition { + return self + [element] + } else { + return self + } + } +} diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift new file mode 100644 index 000000000..453a4c384 --- /dev/null +++ b/Package@swift-5.5.swift @@ -0,0 +1,478 @@ +// swift-tools-version:5.5 +/* + * Copyright 2022, 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 PackageDescription +// swiftformat puts the next import before the tools version. +// swiftformat:disable:next sortedImports +import class Foundation.ProcessInfo + +let grpcPackageName = "grpc-swift" +let grpcProductName = "GRPC" +let cgrpcZlibProductName = "CGRPCZlib" +let grpcTargetName = grpcProductName +let cgrpcZlibTargetName = cgrpcZlibProductName + +let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil + +#if swift(>=5.6) +// swift-argument-parser raised its minimum Swift version in 1.1.0 but +// also accidentally broke API. This was fixed in "1.1.1". +let argumentParserMinimumVersion: Version = "1.1.1" +#else +let argumentParserMinimumVersion: Version = "1.0.0" +#endif + +// MARK: - Package Dependencies + +let packageDependencies: [Package.Dependency] = [ + .package( + url: "https://github.com/apple/swift-nio.git", + from: "2.36.0" + ), + .package( + url: "https://github.com/apple/swift-nio-http2.git", + from: "1.22.0" + ), + .package( + url: "https://github.com/apple/swift-nio-transport-services.git", + from: "1.11.1" + ), + .package( + url: "https://github.com/apple/swift-nio-extras.git", + from: "1.4.0" + ), + .package( + name: "SwiftProtobuf", + url: "https://github.com/apple/swift-protobuf.git", + from: "1.19.0" + ), + .package( + url: "https://github.com/apple/swift-log.git", + from: "1.4.0" + ), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + from: argumentParserMinimumVersion + ), +].appending( + .package( + url: "https://github.com/apple/swift-nio-ssl.git", + from: "2.14.0" + ), + if: includeNIOSSL +) + +// MARK: - Target Dependencies + +extension Target.Dependency { + // Target dependencies; external + static let grpc: Self = .target(name: grpcTargetName) + static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) + static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") + + // Target dependencies; internal + static let grpcSampleData: Self = .target(name: "GRPCSampleData") + static let echoModel: Self = .target(name: "EchoModel") + static let echoImplementation: Self = .target(name: "EchoImplementation") + static let helloWorldModel: Self = .target(name: "HelloWorldModel") + static let routeGuideModel: Self = .target(name: "RouteGuideModel") + static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") + static let interopTestImplementation: Self = + .target(name: "GRPCInteroperabilityTestsImplementation") + + // Product dependencies + static let argumentParser: Self = .product( + name: "ArgumentParser", + package: "swift-argument-parser" + ) + static let nio: Self = .product(name: "NIO", package: "swift-nio") + static let nioConcurrencyHelpers: Self = .product( + name: "NIOConcurrencyHelpers", + package: "swift-nio" + ) + static let nioCore: Self = .product(name: "NIOCore", package: "swift-nio") + static let nioEmbedded: Self = .product(name: "NIOEmbedded", package: "swift-nio") + static let nioExtras: Self = .product(name: "NIOExtras", package: "swift-nio-extras") + static let nioFoundationCompat: Self = .product(name: "NIOFoundationCompat", package: "swift-nio") + static let nioHTTP1: Self = .product(name: "NIOHTTP1", package: "swift-nio") + static let nioHTTP2: Self = .product(name: "NIOHTTP2", package: "swift-nio-http2") + static let nioPosix: Self = .product(name: "NIOPosix", package: "swift-nio") + static let nioSSL: Self = .product(name: "NIOSSL", package: "swift-nio-ssl") + static let nioTLS: Self = .product(name: "NIOTLS", package: "swift-nio") + static let nioTransportServices: Self = .product( + name: "NIOTransportServices", + package: "swift-nio-transport-services" + ) + static let logging: Self = .product(name: "Logging", package: "swift-log") + static let protobuf: Self = .product(name: "SwiftProtobuf", package: "SwiftProtobuf") + static let protobufPluginLibrary: Self = .product( + name: "SwiftProtobufPluginLibrary", + package: "SwiftProtobuf" + ) +} + +// MARK: - Targets + +extension Target { + static let grpc: Target = .target( + name: grpcTargetName, + dependencies: [ + .cgrpcZlib, + .nio, + .nioCore, + .nioPosix, + .nioEmbedded, + .nioFoundationCompat, + .nioTLS, + .nioTransportServices, + .nioHTTP1, + .nioHTTP2, + .nioExtras, + .logging, + .protobuf, + ].appending( + .nioSSL, if: includeNIOSSL + ), + path: "Sources/GRPC" + ) + + static let cgrpcZlib: Target = .target( + name: cgrpcZlibTargetName, + path: "Sources/CGRPCZlib", + linkerSettings: [ + .linkedLibrary("z"), + ] + ) + + static let protocGenGRPCSwift: Target = .executableTarget( + name: "protoc-gen-grpc-swift", + dependencies: [ + .protobuf, + .protobufPluginLibrary, + ], + exclude: [ + "README.md", + ] + ) + + static let grpcTests: Target = .testTarget( + name: "GRPCTests", + dependencies: [ + .grpc, + .echoModel, + .echoImplementation, + .helloWorldModel, + .interopTestModels, + .interopTestImplementation, + .grpcSampleData, + .nioCore, + .nioConcurrencyHelpers, + .nioPosix, + .nioTLS, + .nioHTTP1, + .nioHTTP2, + .nioEmbedded, + .nioTransportServices, + .logging, + ].appending( + .nioSSL, if: includeNIOSSL + ), + exclude: [ + "Codegen/Normalization/normalization.proto", + ] + ) + + static let interopTestModels: Target = .target( + name: "GRPCInteroperabilityTestModels", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + exclude: [ + "README.md", + "generate.sh", + "src/proto/grpc/testing/empty.proto", + "src/proto/grpc/testing/empty_service.proto", + "src/proto/grpc/testing/messages.proto", + "src/proto/grpc/testing/test.proto", + "unimplemented_call.patch", + ] + ) + + static let interopTestImplementation: Target = .target( + name: "GRPCInteroperabilityTestsImplementation", + dependencies: [ + .grpc, + .interopTestModels, + .nioCore, + .nioPosix, + .nioHTTP1, + .logging, + ].appending( + .nioSSL, if: includeNIOSSL + ) + ) + + static let interopTests: Target = .executableTarget( + name: "GRPCInteroperabilityTests", + dependencies: [ + .grpc, + .interopTestImplementation, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ] + ) + + static let backoffInteropTest: Target = .executableTarget( + name: "GRPCConnectionBackoffInteropTest", + dependencies: [ + .grpc, + .interopTestModels, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ], + exclude: [ + "README.md", + ] + ) + + static let perfTests: Target = .executableTarget( + name: "GRPCPerformanceTests", + dependencies: [ + .grpc, + .grpcSampleData, + .nioCore, + .nioEmbedded, + .nioPosix, + .nioHTTP2, + .argumentParser, + ] + ) + + static let grpcSampleData: Target = .target( + name: "GRPCSampleData", + dependencies: includeNIOSSL ? [.nioSSL] : [], + exclude: [ + "bundle.p12", + ] + ) + + static let echoModel: Target = .target( + name: "EchoModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/Echo/Model", + exclude: [ + "echo.proto", + ] + ) + + static let echoImplementation: Target = .target( + name: "EchoImplementation", + dependencies: [ + .echoModel, + .grpc, + .nioCore, + .nioHTTP2, + .protobuf, + ], + path: "Sources/Examples/Echo/Implementation" + ) + + static let echo: Target = .executableTarget( + name: "Echo", + dependencies: [ + .grpc, + .echoModel, + .echoImplementation, + .grpcSampleData, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ].appending( + .nioSSL, if: includeNIOSSL + ), + path: "Sources/Examples/Echo/Runtime" + ) + + static let helloWorldModel: Target = .target( + name: "HelloWorldModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/HelloWorld/Model", + exclude: [ + "helloworld.proto", + ] + ) + + static let helloWorldClient: Target = .executableTarget( + name: "HelloWorldClient", + dependencies: [ + .grpc, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/HelloWorld/Client" + ) + + static let helloWorldServer: Target = .executableTarget( + name: "HelloWorldServer", + dependencies: [ + .grpc, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/HelloWorld/Server" + ) + + static let routeGuideModel: Target = .target( + name: "RouteGuideModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/RouteGuide/Model", + exclude: [ + "route_guide.proto", + ] + ) + + static let routeGuideClient: Target = .executableTarget( + name: "RouteGuideClient", + dependencies: [ + .grpc, + .routeGuideModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/RouteGuide/Client" + ) + + static let routeGuideServer: Target = .executableTarget( + name: "RouteGuideServer", + dependencies: [ + .grpc, + .routeGuideModel, + .nioCore, + .nioConcurrencyHelpers, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/RouteGuide/Server" + ) + + static let packetCapture: Target = .executableTarget( + name: "PacketCapture", + dependencies: [ + .grpc, + .echoModel, + .nioCore, + .nioPosix, + .nioExtras, + .argumentParser, + ], + path: "Sources/Examples/PacketCapture", + exclude: [ + "README.md", + ] + ) +} + +// MARK: - Products + +extension Product { + static let grpc: Product = .library( + name: grpcProductName, + targets: [grpcTargetName] + ) + + static let cgrpcZlib: Product = .library( + name: cgrpcZlibProductName, + targets: [cgrpcZlibTargetName] + ) + + static let protocGenGRPCSwift: Product = .executable( + name: "protoc-gen-grpc-swift", + targets: ["protoc-gen-grpc-swift"] + ) +} + +// MARK: - Package + +let package = Package( + name: grpcPackageName, + products: [ + .grpc, + .cgrpcZlib, + .protocGenGRPCSwift, + ], + dependencies: packageDependencies, + targets: [ + // Products + .grpc, + .cgrpcZlib, + .protocGenGRPCSwift, + + // Tests etc. + .grpcTests, + .interopTestModels, + .interopTestImplementation, + .interopTests, + .backoffInteropTest, + .perfTests, + .grpcSampleData, + + // Examples + .echoModel, + .echoImplementation, + .echo, + .helloWorldModel, + .helloWorldClient, + .helloWorldServer, + .routeGuideModel, + .routeGuideClient, + .routeGuideServer, + .packetCapture, + ] +) + +extension Array { + func appending(_ element: Element, if condition: Bool) -> [Element] { + if condition { + return self + [element] + } else { + return self + } + } +} diff --git a/scripts/license-check.sh b/scripts/license-check.sh index 7d15e3bf4..55fec544a 100755 --- a/scripts/license-check.sh +++ b/scripts/license-check.sh @@ -110,6 +110,11 @@ check_copyright_headers() { drop_first=1 expected_lines=15 ;; + */Package@swift-*.*.swift) + expected_sha="$SWIFT_SHA" + drop_first=1 + expected_lines=15 + ;; *) expected_sha="$SWIFT_SHA" drop_first=0 From 6639e45c1fc258e24742b20c3b1bec6da73998c8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 Aug 2022 10:51:49 +0100 Subject: [PATCH 013/580] Remove warnings from Package.swift (#1469) Motivation: In #1467 different Package.swift versions were added. Raising the tools version to 5.6 in 'Package.swift' highlighted use of deprecated SPM APIs. Modifications: - Stop using the deprecated API in Package.swift - Remove the code conditionalised on the Swift version Result: Fewer warnings --- Package.swift | 17 +++++------------ Package@swift-5.4.swift | 10 +--------- Package@swift-5.5.swift | 10 +--------- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/Package.swift b/Package.swift index 09fae334f..da2618013 100644 --- a/Package.swift +++ b/Package.swift @@ -27,14 +27,6 @@ let cgrpcZlibTargetName = cgrpcZlibProductName let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil -#if swift(>=5.6) -// swift-argument-parser raised its minimum Swift version in 1.1.0 but -// also accidentally broke API. This was fixed in "1.1.1". -let argumentParserMinimumVersion: Version = "1.1.1" -#else -let argumentParserMinimumVersion: Version = "1.0.0" -#endif - // MARK: - Package Dependencies let packageDependencies: [Package.Dependency] = [ @@ -55,7 +47,6 @@ let packageDependencies: [Package.Dependency] = [ from: "1.4.0" ), .package( - name: "SwiftProtobuf", url: "https://github.com/apple/swift-protobuf.git", from: "1.19.0" ), @@ -65,7 +56,9 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-argument-parser.git", - from: argumentParserMinimumVersion + // Version is higher than in other Package@swift manifests: 1.1.0 raised the minimum Swift + // version and indluded async support. + from: "1.1.1" ), .package( url: "https://github.com/apple/swift-docc-plugin", @@ -121,10 +114,10 @@ extension Target.Dependency { package: "swift-nio-transport-services" ) static let logging: Self = .product(name: "Logging", package: "swift-log") - static let protobuf: Self = .product(name: "SwiftProtobuf", package: "SwiftProtobuf") + static let protobuf: Self = .product(name: "SwiftProtobuf", package: "swift-protobuf") static let protobufPluginLibrary: Self = .product( name: "SwiftProtobufPluginLibrary", - package: "SwiftProtobuf" + package: "swift-protobuf" ) } diff --git a/Package@swift-5.4.swift b/Package@swift-5.4.swift index 6122630d2..069e519de 100644 --- a/Package@swift-5.4.swift +++ b/Package@swift-5.4.swift @@ -27,14 +27,6 @@ let cgrpcZlibTargetName = cgrpcZlibProductName let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil -#if swift(>=5.6) -// swift-argument-parser raised its minimum Swift version in 1.1.0 but -// also accidentally broke API. This was fixed in "1.1.1". -let argumentParserMinimumVersion: Version = "1.1.1" -#else -let argumentParserMinimumVersion: Version = "1.0.0" -#endif - // MARK: - Package Dependencies let packageDependencies: [Package.Dependency] = [ @@ -65,7 +57,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-argument-parser.git", - from: argumentParserMinimumVersion + from: "1.0.0" ), ].appending( .package( diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index 453a4c384..a6806791f 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -27,14 +27,6 @@ let cgrpcZlibTargetName = cgrpcZlibProductName let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil -#if swift(>=5.6) -// swift-argument-parser raised its minimum Swift version in 1.1.0 but -// also accidentally broke API. This was fixed in "1.1.1". -let argumentParserMinimumVersion: Version = "1.1.1" -#else -let argumentParserMinimumVersion: Version = "1.0.0" -#endif - // MARK: - Package Dependencies let packageDependencies: [Package.Dependency] = [ @@ -65,7 +57,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-argument-parser.git", - from: argumentParserMinimumVersion + from: "1.0.0" ), ].appending( .package( From 7e6d8fac42b8302d2d4ed8cab57445e73de83f3a Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 10 Aug 2022 16:53:39 +0100 Subject: [PATCH 014/580] Add async version of QPS Benchmark Service (#1470) Motivation: We want to provide an async/await version of the QPS Benchmark service, and be able to choose whether we want to use that or the ELF version when running the tests. Modifications: Added support for an async/await implementation of the Benchmark Service. This is currently not in use, but a follow-up CR will include changes to the Worker Service and other classes, and will enable choosing between the ELF and async/await implementations when running. Result: An async/await version of the Benchmark Service will be ready for use. --- Performance/QPSBenchmark/Package.swift | 3 +- .../Model/benchmark_service.grpc.swift | 6 +- .../Model/worker_service.grpc.swift | 4 +- .../Async/AsyncBenchmarkServiceImpl.swift | 121 ++++++++++++++++++ .../QPSBenchmark/Runtime/AsyncServer.swift | 2 +- 5 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift diff --git a/Performance/QPSBenchmark/Package.swift b/Performance/QPSBenchmark/Package.swift index a96a368ca..afefd1c69 100644 --- a/Performance/QPSBenchmark/Package.swift +++ b/Performance/QPSBenchmark/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.6 /* * Copyright 2020, gRPC Authors All rights reserved. * @@ -19,6 +19,7 @@ import PackageDescription let package = Package( name: "QPSBenchmark", + platforms: [.macOS(.v12)], products: [ .executable(name: "QPSBenchmark", targets: ["QPSBenchmark"]), ], diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift index 49d3bed34..2a6a61826 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift @@ -350,7 +350,7 @@ extension Grpc_Testing_BenchmarkServiceAsyncClientProtocol { public func streamingCall( _ requests: RequestStream, callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence, RequestStream.Element == Grpc_Testing_SimpleRequest { + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_SimpleRequest { return self.performAsyncBidirectionalStreamingCall( path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingCall.path, requests: requests, @@ -374,7 +374,7 @@ extension Grpc_Testing_BenchmarkServiceAsyncClientProtocol { public func streamingFromClient( _ requests: RequestStream, callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_SimpleResponse where RequestStream: AsyncSequence, RequestStream.Element == Grpc_Testing_SimpleRequest { + ) async throws -> Grpc_Testing_SimpleResponse where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_SimpleRequest { return try await self.performAsyncClientStreamingCall( path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromClient.path, requests: requests, @@ -410,7 +410,7 @@ extension Grpc_Testing_BenchmarkServiceAsyncClientProtocol { public func streamingBothWays( _ requests: RequestStream, callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence, RequestStream.Element == Grpc_Testing_SimpleRequest { + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_SimpleRequest { return self.performAsyncBidirectionalStreamingCall( path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingBothWays.path, requests: requests, diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift index d3bc75213..8147bd723 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift @@ -302,7 +302,7 @@ extension Grpc_Testing_WorkerServiceAsyncClientProtocol { public func runServer( _ requests: RequestStream, callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence, RequestStream.Element == Grpc_Testing_ServerArgs { + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_ServerArgs { return self.performAsyncBidirectionalStreamingCall( path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runServer.path, requests: requests, @@ -326,7 +326,7 @@ extension Grpc_Testing_WorkerServiceAsyncClientProtocol { public func runClient( _ requests: RequestStream, callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence, RequestStream.Element == Grpc_Testing_ClientArgs { + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_ClientArgs { return self.performAsyncBidirectionalStreamingCall( path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runClient.path, requests: requests, diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift new file mode 100644 index 000000000..6a41206b1 --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift @@ -0,0 +1,121 @@ +/* + * Copyright 2022, 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 Foundation +import GRPC +import NIOCore + +/// Implementation of asynchronous service for benchmarking. +final class AsyncBenchmarkServiceImpl: Grpc_Testing_BenchmarkServiceAsyncProvider { + let interceptors: Grpc_Testing_BenchmarkServiceServerInterceptorFactoryProtocol? = nil + + /// One request followed by one response. + /// The server returns the client payload as-is. + func unaryCall( + request: Grpc_Testing_SimpleRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Grpc_Testing_SimpleResponse { + return try AsyncBenchmarkServiceImpl.processSimpleRPC(request: request) + } + + /// Repeated sequence of one request followed by one response. + /// Should be called streaming ping-pong + /// The server returns the client payload as-is on each response + func streamingCall( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + for try await request in requestStream { + let response = try AsyncBenchmarkServiceImpl.processSimpleRPC(request: request) + try await responseStream.send(response) + } + } + + /// Single-sided unbounded streaming from client to server + /// The server returns the client payload as-is once the client does WritesDone + func streamingFromClient( + requestStream: GRPCAsyncRequestStream, + context: GRPCAsyncServerCallContext + ) async throws -> Grpc_Testing_SimpleResponse { + context.request.logger.warning("streamingFromClient not implemented yet") + throw GRPCStatus( + code: .unimplemented, + message: "Not implemented" + ) + } + + /// Single-sided unbounded streaming from server to client + /// The server repeatedly returns the client payload as-is + func streamingFromServer( + request: Grpc_Testing_SimpleRequest, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + context.request.logger.warning("streamingFromServer not implemented yet") + throw GRPCStatus( + code: GRPCStatus.Code.unimplemented, + message: "Not implemented" + ) + } + + /// Two-sided unbounded streaming between server to client + /// Both sides send the content of their own choice to the other + func streamingBothWays( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + context.request.logger.warning("streamingBothWays not implemented yet") + throw GRPCStatus( + code: GRPCStatus.Code.unimplemented, + message: "Not implemented" + ) + } + + /// Make a payload for sending back to the client. + private static func makePayload( + type: Grpc_Testing_PayloadType, + size: Int + ) throws -> Grpc_Testing_Payload { + if type != .compressable { + // Making a payload which is not compressable is hard - and not implemented in + // other implementations too. + throw GRPCStatus(code: .internalError, message: "Failed to make payload") + } + var payload = Grpc_Testing_Payload() + payload.body = Data(count: size) + payload.type = type + return payload + } + + /// Process a simple RPC. + /// - parameters: + /// - request: The request from the client. + /// - returns: A response to send back to the client. + private static func processSimpleRPC( + request: Grpc_Testing_SimpleRequest + ) throws -> Grpc_Testing_SimpleResponse { + var response = Grpc_Testing_SimpleResponse() + if request.responseSize > 0 { + response.payload = try self.makePayload( + type: request.responseType, + size: Int(request.responseSize) + ) + } + return response + } +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncServer.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncServer.swift index 6b58c6f01..8f7aa475b 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncServer.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncServer.swift @@ -51,7 +51,7 @@ final class AsyncQPSServer: QPSServer { let workerService = AsyncQPSServerImpl() // Start the server. - // TODO: Support TLS is requested. + // TODO: Support TLS if requested. self.server = Server.insecure(group: self.eventLoopGroup) .withServiceProviders([workerService]) .withLogger(self.logger) From 3e1521fd745c902041b254075b7ddf783e967f01 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 11 Aug 2022 11:49:45 +0100 Subject: [PATCH 015/580] Add async implementation of QPSBenchmark (#1471) Motivation: We want to provide an async/await version of the QPSBenchmark implementation, and be able to choose whether we want to use that or the ELF version when running the performance tests via the gRPC JSON driver. Modifications: - Added full async/await-based implementations of the worker, server, and client. - Created a new command line flag (`--use-async`) to choose between the EventLoopFuture and async/await versions when running the tests. Result: QPSBenchmark can optionally be executed using an async/await implementation. --- Performance/QPSBenchmark/Package.swift | 9 +- .../Runtime/Async/AsyncClientProtocol.swift | 36 ++ .../Async/AsyncPingPongRequestMaker.swift | 85 +++++ .../Runtime/Async/AsyncQPSClientImpl.swift | 293 +++++++++++++++ .../Runtime/Async/AsyncQPSServerImpl.swift | 102 ++++++ .../Runtime/Async/AsyncRequestMaker.swift | 42 +++ .../Runtime/Async/AsyncServerProtocol.swift | 36 ++ .../Async/AsyncUnaryRequestMaker.swift | 66 ++++ .../Async/AsyncWorkerServiceImpl.swift | 335 ++++++++++++++++++ .../NIOBenchmarkServiceImpl.swift} | 6 +- .../NIOClientProtocol.swift} | 2 +- .../NIOPingPongRequestMaker.swift} | 2 +- .../NIOQPSClientImpl.swift} | 14 +- .../NIOQPSServerImpl.swift} | 6 +- .../NIORequestMaker.swift} | 2 +- .../NIOServerProtocol.swift} | 4 +- .../NIOUnaryRequestMaker.swift} | 2 +- .../NIOWorkerServiceImpl.swift} | 16 +- .../QPSBenchmark/Runtime/QPSWorker.swift | 24 +- .../Sources/QPSBenchmark/Runtime/Stats.swift | 3 +- .../Sources/QPSBenchmark/Runtime/main.swift | 13 +- 21 files changed, 1051 insertions(+), 47 deletions(-) create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{BenchmarkServiceImpl.swift => NIO/NIOBenchmarkServiceImpl.swift} (94%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{ClientInterface.swift => NIO/NIOClientProtocol.swift} (98%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{AsyncPingPongRequestMaker.swift => NIO/NIOPingPongRequestMaker.swift} (98%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{AsyncClient.swift => NIO/NIOQPSClientImpl.swift} (96%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{AsyncServer.swift => NIO/NIOQPSServerImpl.swift} (96%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{RequestMaker.swift => NIO/NIORequestMaker.swift} (98%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{ServerInterface.swift => NIO/NIOServerProtocol.swift} (93%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{AsyncUnaryRequestMaker.swift => NIO/NIOUnaryRequestMaker.swift} (97%) rename Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/{WorkerServiceImpl.swift => NIO/NIOWorkerServiceImpl.swift} (96%) diff --git a/Performance/QPSBenchmark/Package.swift b/Performance/QPSBenchmark/Package.swift index afefd1c69..8a900b981 100644 --- a/Performance/QPSBenchmark/Package.swift +++ b/Performance/QPSBenchmark/Package.swift @@ -25,16 +25,15 @@ let package = Package( ], dependencies: [ .package(path: "../../"), - .package(url: "https://github.com/apple/swift-nio.git", from: "2.32.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.41.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.4.3"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.1.1"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), .package( url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "1.0.0-alpha" ), .package( - name: "SwiftProtobuf", url: "https://github.com/apple/swift-protobuf.git", from: "1.19.0" ), @@ -51,7 +50,7 @@ let package = Package( .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Logging", package: "swift-log"), .product(name: "Lifecycle", package: "swift-service-lifecycle"), - .product(name: "SwiftProtobuf", package: "SwiftProtobuf"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), .target(name: "BenchmarkUtils"), ], exclude: [ diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift new file mode 100644 index 000000000..3414e519a --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift @@ -0,0 +1,36 @@ +/* + * Copyright 2022, 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 GRPC +import NIOCore + +/// Protocol which async clients must implement. +protocol AsyncQPSClient { + /// Start the execution of the client. + func startClient() + + /// Send the status of the current test + /// - parameters: + /// - reset: Indicates if the stats collection should be reset after publication or not. + /// - responseStream: the response stream to write the response to. + func sendStatus( + reset: Bool, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws + + /// Shut down the client. + func shutdown() async throws +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift new file mode 100644 index 000000000..ae497d80c --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift @@ -0,0 +1,85 @@ +/* + * Copyright 2022, 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 Atomics +import Foundation +import GRPC +import Logging +import NIOCore + +/// Makes streaming requests and listens to responses ping-pong style. +/// Iterations can be limited by config. +/// Class is marked as `@unchecked Sendable` because `ManagedAtomic` doesn't conform +/// to `Sendable`, but we know it's safe. +final class AsyncPingPongRequestMaker: AsyncRequestMaker, @unchecked Sendable { + private let client: Grpc_Testing_BenchmarkServiceAsyncClient + private let requestMessage: Grpc_Testing_SimpleRequest + private let logger: Logger + private let stats: StatsWithLock + + /// If greater than zero gives a limit to how many messages are exchanged before termination. + private let messagesPerStream: Int + /// Stops more requests being made after stop is requested. + private let stopRequested = ManagedAtomic(false) + + /// Initialiser to gather requirements. + /// - Parameters: + /// - config: config from the driver describing what to do. + /// - client: client interface to the server. + /// - requestMessage: Pre-made request message to use possibly repeatedly. + /// - logger: Where to log useful diagnostics. + /// - stats: Where to record statistics on latency. + init( + config: Grpc_Testing_ClientConfig, + client: Grpc_Testing_BenchmarkServiceAsyncClient, + requestMessage: Grpc_Testing_SimpleRequest, + logger: Logger, + stats: StatsWithLock + ) { + self.client = client + self.requestMessage = requestMessage + self.logger = logger + self.stats = stats + + self.messagesPerStream = Int(config.messagesPerStream) + } + + /// Initiate a request sequence to the server - in this case the sequence is streaming requests to the server and waiting + /// to see responses before repeating ping-pong style. The number of iterations can be limited by config. + func makeRequest() async throws { + var startTime = grpcTimeNow() + var messagesSent = 0 + + let streamingCall = self.client.makeStreamingCallCall() + var responseStream = streamingCall.responseStream.makeAsyncIterator() + while !self.stopRequested.load(ordering: .relaxed), + self.messagesPerStream == 0 || messagesSent < self.messagesPerStream { + try await streamingCall.requestStream.send(self.requestMessage) + let _ = try await responseStream.next() + let endTime = grpcTimeNow() + self.stats.add(latency: endTime - startTime) + messagesSent += 1 + startTime = endTime + } + } + + /// Request termination of the request-response sequence. + func requestStop() { + self.logger.info("AsyncPingPongRequestMaker stop requested") + // Flag stop as requested - this will prevent any more requests being made. + self.stopRequested.store(true, ordering: .relaxed) + } +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift new file mode 100644 index 000000000..b764cc770 --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift @@ -0,0 +1,293 @@ +/* + * Copyright 2022, 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 Atomics +import BenchmarkUtils +import Foundation +import GRPC +import Logging +import NIOConcurrencyHelpers +import NIOCore +import NIOPosix + +/// Client to make a series of asynchronous calls. +final class AsyncQPSClientImpl: AsyncQPSClient { + private let logger = Logger(label: "AsyncQPSClientImpl") + + private let eventLoopGroup: MultiThreadedEventLoopGroup + private let threadCount: Int + private let channelRepeaters: [ChannelRepeater] + + private var statsPeriodStart: DispatchTime + private var cpuStatsPeriodStart: CPUTime + + /// Initialise a client to send requests. + /// - parameters: + /// - config: Config from the driver specifying how the client should behave. + init(config: Grpc_Testing_ClientConfig) throws { + // Setup threads + let threadCount = config.threadsToUse() + self.threadCount = threadCount + self.logger.info("Sizing AsyncQPSClientImpl", metadata: ["threads": "\(threadCount)"]) + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) + self.eventLoopGroup = eventLoopGroup + + // Parse possible invalid targets before code with side effects. + let serverTargets = try config.parsedServerTargets() + precondition(serverTargets.count > 0) + + // Start recording stats. + self.statsPeriodStart = grpcTimeNow() + self.cpuStatsPeriodStart = getResourceUsage() + + let requestMessage = try AsyncQPSClientImpl + .makeClientRequest(payloadConfig: config.payloadConfig) + + // Start the requested number of channels. + self.channelRepeaters = (0 ..< Int(config.clientChannels)).map { channelNumber in + ChannelRepeater( + target: serverTargets[channelNumber % serverTargets.count], + requestMessage: requestMessage, + config: config, + eventLoop: eventLoopGroup.next() + ) + } + } + + /// Start the execution of the client. + func startClient() { + Task { + try await withThrowingTaskGroup(of: Void.self) { group in + for repeater in self.channelRepeaters { + group.addTask { + try await repeater.start() + } + } + try await group.waitForAll() + } + } + } + + /// Send current status back to the driver process. + /// - parameters: + /// - reset: Should the stats reset after being sent. + /// - context: Calling context to allow results to be sent back to the driver. + func sendStatus( + reset: Bool, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + let currentTime = grpcTimeNow() + let currentResourceUsage = getResourceUsage() + var result = Grpc_Testing_ClientStatus() + result.stats.timeElapsed = (currentTime - self.statsPeriodStart).asSeconds() + result.stats.timeSystem = currentResourceUsage.systemTime - self.cpuStatsPeriodStart + .systemTime + result.stats.timeUser = currentResourceUsage.userTime - self.cpuStatsPeriodStart.userTime + result.stats.cqPollCount = 0 + + // Collect stats from each of the channels. + var latencyHistogram = Histogram() + var statusCounts = StatusCounts() + for channelRepeater in self.channelRepeaters { + let stats = channelRepeater.getStats(reset: reset) + try! latencyHistogram.merge(source: stats.latencies) + statusCounts.merge(source: stats.statuses) + } + result.stats.latencies = Grpc_Testing_HistogramData(from: latencyHistogram) + result.stats.requestResults = statusCounts.toRequestResultCounts() + self.logger.info("Sending client status") + try await responseStream.send(result) + + if reset { + self.statsPeriodStart = currentTime + self.cpuStatsPeriodStart = currentResourceUsage + } + } + + /// Shut down the service. + func shutdown() async throws { + await withThrowingTaskGroup(of: Void.self) { group in + for repeater in self.channelRepeaters { + group.addTask { + do { + try await repeater.stop() + } catch { + self.logger.warning( + "A channel repeater could not be stopped", + metadata: ["error": "\(error)"] + ) + } + } + } + } + } + + /// Make a request which can be sent to the server. + private static func makeClientRequest( + payloadConfig: Grpc_Testing_PayloadConfig + ) throws -> Grpc_Testing_SimpleRequest { + if let payload = payloadConfig.payload { + switch payload { + case .bytebufParams: + throw GRPCStatus(code: .invalidArgument, message: "Byte buffer not supported.") + case let .simpleParams(simpleParams): + var result = Grpc_Testing_SimpleRequest() + result.responseType = .compressable + result.responseSize = simpleParams.respSize + result.payload.type = .compressable + let size = Int(simpleParams.reqSize) + let body = Data(count: size) + result.payload.body = body + return result + case .complexParams: + throw GRPCStatus( + code: .invalidArgument, + message: "Complex params not supported." + ) + } + } else { + // Default - simple proto without payloads. + var result = Grpc_Testing_SimpleRequest() + result.responseType = .compressable + result.responseSize = 0 + result.payload.type = .compressable + return result + } + } + + /// Class to manage a channel. Repeatedly makes requests on that channel and records what happens. + /// /// Class is marked as `@unchecked Sendable` because `ManagedAtomic` doesn't conform + /// to `Sendable`, but we know it's safe. + private final class ChannelRepeater: @unchecked Sendable { + private let channel: GRPCChannel + private let eventLoop: EventLoop + private let maxPermittedOutstandingRequests: Int + + private let stats: StatsWithLock + + /// Succeeds after a stop has been requested and all outstanding requests have completed. + private let stopComplete: EventLoopPromise + + private let running = ManagedAtomic(false) + + private let requestMaker: RequestMakerType + + init( + target: ConnectionTarget, + requestMessage: Grpc_Testing_SimpleRequest, + config: Grpc_Testing_ClientConfig, + eventLoop: EventLoop + ) { + self.eventLoop = eventLoop + // 'try!' is fine; it'll only throw if we can't make an SSL context + // TODO: Support TLS if requested. + self.channel = try! GRPCChannelPool.with( + target: target, + transportSecurity: .plaintext, + eventLoopGroup: eventLoop + ) + + let logger = Logger(label: "ChannelRepeater") + let client = Grpc_Testing_BenchmarkServiceAsyncClient(channel: self.channel) + self.maxPermittedOutstandingRequests = Int(config.outstandingRpcsPerChannel) + self.stopComplete = eventLoop.makePromise() + self.stats = StatsWithLock() + + self.requestMaker = RequestMakerType( + config: config, + client: client, + requestMessage: requestMessage, + logger: logger, + stats: self.stats + ) + } + + /// Launch as many requests as allowed on the channel. Must only be called once. + private func launchRequests() async throws { + let exchangedRunning = self.running.compareExchange( + expected: false, + desired: true, + ordering: .relaxed + ) + precondition(exchangedRunning.exchanged, "launchRequests should only be called once") + + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0 ..< self.maxPermittedOutstandingRequests { + group.addTask { + try await self.requestMaker.makeRequest() + } + } + + /// While `running` is true, we'll keep launching new requests to + /// maintain `maxPermittedOutstandingRequests` running + /// at any given time. + for try await _ in group { + if self.running.load(ordering: .relaxed) { + group.addTask { + try await self.requestMaker.makeRequest() + } + } + } + self.stopIsComplete() + } + } + + /// Get stats for sending to the driver. + /// - parameters: + /// - reset: Should the stats reset after copying. + /// - returns: The statistics for this channel. + func getStats(reset: Bool) -> Stats { + return self.stats.copyData(reset: reset) + } + + /// Start sending requests to the server. + func start() async throws { + try await self.launchRequests() + } + + private func stopIsComplete() { + // Close the connection then signal done. + self.channel.close().cascade(to: self.stopComplete) + } + + /// Stop sending requests to the server. + /// - returns: A future which can be waited on to signal when all activity has ceased. + func stop() async throws { + self.requestMaker.requestStop() + self.running.store(false, ordering: .relaxed) + try await self.stopComplete.futureResult.get() + } + } +} + +/// Create an asynchronous client of the requested type. +/// - parameters: +/// - config: Description of the client required. +/// - returns: The client created. +func makeAsyncClient(config: Grpc_Testing_ClientConfig) throws -> AsyncQPSClient { + switch config.rpcType { + case .unary: + return try AsyncQPSClientImpl(config: config) + case .streaming: + return try AsyncQPSClientImpl(config: config) + case .streamingFromClient, + .streamingFromServer, + .streamingBothWays: + throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") + case .UNRECOGNIZED: + throw GRPCStatus(code: .invalidArgument, message: "Unrecognised client rpc type") + } +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift new file mode 100644 index 000000000..a5112aa8b --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift @@ -0,0 +1,102 @@ +/* + * Copyright 2022, 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 Foundation +import GRPC +import Logging +import NIOCore +import NIOPosix + +/// Server setup for asynchronous requests. +final class AsyncQPSServerImpl: AsyncQPSServer { + private let logger = Logger(label: "AsyncQPSServerImpl") + + private let eventLoopGroup: MultiThreadedEventLoopGroup + private let server: Server + private let threadCount: Int + + private var statsPeriodStart: DispatchTime + private var cpuStatsPeriodStart: CPUTime + + var serverInfo: ServerInfo { + let port = self.server.channel.localAddress?.port ?? 0 + return ServerInfo(threadCount: self.threadCount, port: port) + } + + /// Initialisation. + /// - parameters: + /// - config: Description of the type of server required. + init(config: Grpc_Testing_ServerConfig) async throws { + // Setup threads as requested. + let threadCount = config.asyncServerThreads > 0 + ? Int(config.asyncServerThreads) + : System.coreCount + self.threadCount = threadCount + self.logger.info("Sizing AsyncQPSServerImpl", metadata: ["threads": "\(threadCount)"]) + self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) + + // Start stats gathering. + self.statsPeriodStart = grpcTimeNow() + self.cpuStatsPeriodStart = getResourceUsage() + + let workerService = AsyncBenchmarkServiceImpl() + + // Start the server + self.server = try await Server.insecure(group: self.eventLoopGroup) + .withServiceProviders([workerService]) + .withLogger(self.logger) + .bind(host: "localhost", port: Int(config.port)) + .get() + } + + /// Send the status of the current test + /// - parameters: + /// - reset: Indicates if the stats collection should be reset after publication or not. + /// - responseStream: the response stream to which the status should be sent. + func sendStatus( + reset: Bool, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + let currentTime = grpcTimeNow() + let currentResourceUsage = getResourceUsage() + var result = Grpc_Testing_ServerStatus() + result.stats.timeElapsed = (currentTime - self.statsPeriodStart).asSeconds() + result.stats.timeSystem = currentResourceUsage.systemTime - self.cpuStatsPeriodStart + .systemTime + result.stats.timeUser = currentResourceUsage.userTime - self.cpuStatsPeriodStart.userTime + result.stats.totalCpuTime = 0 + result.stats.idleCpuTime = 0 + result.stats.cqPollCount = 0 + self.logger.info("Sending server status") + try await responseStream.send(result) + if reset { + self.statsPeriodStart = currentTime + self.cpuStatsPeriodStart = currentResourceUsage + } + } + + /// Shut down the service. + func shutdown() async throws { + do { + try await self.server.initiateGracefulShutdown().get() + } catch { + self.logger.error("Error closing server", metadata: ["error": "\(error)"]) + // May as well plough on anyway - + // we will hopefully sort outselves out shutting down the eventloops + } + try await self.eventLoopGroup.shutdownGracefully() + } +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift new file mode 100644 index 000000000..d0148999a --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift @@ -0,0 +1,42 @@ +/* + * Copyright 2022, 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 GRPC +import Logging + +/// Implement to provide a method of making requests to a server from a client. +protocol AsyncRequestMaker: Sendable { + /// Initialiser to gather requirements. + /// - Parameters: + /// - config: config from the driver describing what to do. + /// - client: client interface to the server. + /// - requestMessage: Pre-made request message to use possibly repeatedly. + /// - logger: Where to log useful diagnostics. + /// - stats: Where to record statistics on latency. + init( + config: Grpc_Testing_ClientConfig, + client: Grpc_Testing_BenchmarkServiceAsyncClient, + requestMessage: Grpc_Testing_SimpleRequest, + logger: Logger, + stats: StatsWithLock + ) + + /// Initiate a request sequence to the server. + func makeRequest() async throws + + /// Request termination of the request-response sequence. + func requestStop() +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift new file mode 100644 index 000000000..60e1c33b0 --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift @@ -0,0 +1,36 @@ +/* + * Copyright 2022, 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 GRPC +import NIOCore + +/// Interface server types must implement when using async APIs. +protocol AsyncQPSServer { + /// The server information for this server. + var serverInfo: ServerInfo { get } + + /// Send the status of the current test + /// - parameters: + /// - reset: Indicates if the stats collection should be reset after publication or not. + /// - responseStream: the response stream to write the response to. + func sendStatus( + reset: Bool, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws + + /// Shut down the service. + func shutdown() async throws +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift new file mode 100644 index 000000000..5921ef038 --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift @@ -0,0 +1,66 @@ +/* + * Copyright 2020, 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 GRPC +import Logging +import NIOCore + +/// Makes unary requests to the server and records performance statistics. +final class AsyncUnaryRequestMaker: AsyncRequestMaker { + private let client: Grpc_Testing_BenchmarkServiceAsyncClient + private let requestMessage: Grpc_Testing_SimpleRequest + private let logger: Logger + private let stats: StatsWithLock + + /// Initialiser to gather requirements. + /// - Parameters: + /// - config: config from the driver describing what to do. + /// - client: client interface to the server. + /// - requestMessage: Pre-made request message to use possibly repeatedly. + /// - logger: Where to log useful diagnostics. + /// - stats: Where to record statistics on latency. + init( + config: Grpc_Testing_ClientConfig, + client: Grpc_Testing_BenchmarkServiceAsyncClient, + requestMessage: Grpc_Testing_SimpleRequest, + logger: Logging.Logger, + stats: StatsWithLock + ) { + self.client = client + self.requestMessage = requestMessage + self.logger = logger + self.stats = stats + } + + /// Initiate a request sequence to the server - in this case a single unary requests and wait for a response. + /// - returns: A future which completes when the request-response sequence is complete. + func makeRequest() async throws { + let startTime = grpcTimeNow() + do { + _ = try await self.client.unaryCall(self.requestMessage) + let endTime = grpcTimeNow() + self.stats.add(latency: endTime - startTime) + } catch { + self.logger.error("Error from unary request", metadata: ["error": "\(error)"]) + throw error + } + } + + /// Request termination of the request-response sequence. + func requestStop() { + // No action here - we could potentially try and cancel the request easiest to just wait. + } +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift new file mode 100644 index 000000000..2b82d9d60 --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift @@ -0,0 +1,335 @@ +/* + * Copyright 2022, 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 GRPC +import NIOCore + +// Implementation of the control service for communication with the driver process. +actor AsyncWorkerServiceImpl: Grpc_Testing_WorkerServiceAsyncProvider { + let interceptors: Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol? = nil + + private let finishedPromise: EventLoopPromise + private let serverPortOverride: Int? + + private var runningServer: AsyncQPSServer? + private var runningClient: AsyncQPSClient? + + /// Initialise. + /// - parameters: + /// - finishedPromise: Promise to complete when the server has finished running. + /// - serverPortOverride: An override to port number requested by the driver process. + init(finishedPromise: EventLoopPromise, serverPortOverride: Int?) { + self.finishedPromise = finishedPromise + self.serverPortOverride = serverPortOverride + } + + /// Start server with specified workload. + /// First request sent specifies the ServerConfig followed by ServerStatus + /// response. After that, a "Mark" can be sent anytime to request the latest + /// stats. Closing the stream will initiate shutdown of the test server + /// and once the shutdown has finished, the OK status is sent to terminate + /// this RPC. + func runServer( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + context.request.logger.info("runServer stream started.") + for try await request in requestStream { + try await self.handleServerMessage( + context: context, + args: request, + responseStream: responseStream + ) + } + try await self.handleServerEnd(context: context) + } + + /// Start client with specified workload. + /// First request sent specifies the ClientConfig followed by ClientStatus + /// response. After that, a "Mark" can be sent anytime to request the latest + /// stats. Closing the stream will initiate shutdown of the test client + /// and once the shutdown has finished, the OK status is sent to terminate + /// this RPC. + func runClient( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + for try await request in requestStream { + try await self.handleClientMessage( + context: context, + args: request, + responseStream: responseStream + ) + } + try await self.handleClientEnd(context: context) + } + + /// Just return the core count - unary call + func coreCount( + request: Grpc_Testing_CoreRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Grpc_Testing_CoreResponse { + context.request.logger.notice("coreCount queried") + return Grpc_Testing_CoreResponse.with { $0.cores = Int32(System.coreCount) } + } + + /// Quit this worker + func quitWorker( + request: Grpc_Testing_Void, + context: GRPCAsyncServerCallContext + ) -> Grpc_Testing_Void { + context.request.logger.warning("quitWorker called") + self.finishedPromise.succeed(()) + return Grpc_Testing_Void() + } + + // MARK: Run Server + + /// Handle a message received from the driver about operating as a server. + private func handleServerMessage( + context: GRPCAsyncServerCallContext, + args: Grpc_Testing_ServerArgs, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + switch args.argtype { + case let .some(.setup(serverConfig)): + try await self.handleServerSetup( + context: context, + config: serverConfig, + responseStream: responseStream + ) + case let .some(.mark(mark)): + try await self.handleServerMarkRequested( + context: context, + mark: mark, + responseStream: responseStream + ) + case .none: + () + } + } + + /// Handle a request to setup a server. + /// Makes a new server and sets it running. + private func handleServerSetup( + context: GRPCAsyncServerCallContext, + config: Grpc_Testing_ServerConfig, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + context.request.logger.info("server setup requested") + guard self.runningServer == nil else { + context.request.logger.error("server already running") + throw GRPCStatus( + code: GRPCStatus.Code.resourceExhausted, + message: "Server worker busy" + ) + } + try await self.runServerBody( + context: context, + serverConfig: config, + responseStream: responseStream + ) + } + + /// Gathers stats and returns them to the driver process. + private func handleServerMarkRequested( + context: GRPCAsyncServerCallContext, + mark: Grpc_Testing_Mark, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + context.request.logger.info("server mark requested") + guard let runningServer = self.runningServer else { + context.request.logger.error("server not running") + throw GRPCStatus( + code: GRPCStatus.Code.failedPrecondition, + message: "Server not running" + ) + } + try await runningServer.sendStatus(reset: mark.reset, responseStream: responseStream) + } + + /// Handle a message from the driver asking this server function to stop running. + private func handleServerEnd(context: GRPCAsyncServerCallContext) async throws { + context.request.logger.info("runServer stream ended.") + if let runningServer = self.runningServer { + self.runningServer = nil + try await runningServer.shutdown() + } + } + + // MARK: Create Server + + /// Start a server running of the requested type. + private func runServerBody( + context: GRPCAsyncServerCallContext, + serverConfig: Grpc_Testing_ServerConfig, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + var serverConfig = serverConfig + self.serverPortOverride.map { serverConfig.port = Int32($0) } + + self.runningServer = try await AsyncWorkerServiceImpl.createServer( + context: context, + config: serverConfig, + responseStream: responseStream + ) + } + + private static func sendServerInfo( + _ serverInfo: ServerInfo, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + var response = Grpc_Testing_ServerStatus() + response.cores = Int32(serverInfo.threadCount) + response.port = Int32(serverInfo.port) + try await responseStream.send(response) + } + + /// Create a server of the requested type. + private static func createServer( + context: GRPCAsyncServerCallContext, + config: Grpc_Testing_ServerConfig, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws -> AsyncQPSServer { + context.request.logger.info( + "Starting server", + metadata: ["type": .stringConvertible(config.serverType)] + ) + + switch config.serverType { + case .asyncServer: + let asyncServer = try await AsyncQPSServerImpl(config: config) + let serverInfo = asyncServer.serverInfo + try await self.sendServerInfo(serverInfo, responseStream: responseStream) + return asyncServer + case .syncServer, + .asyncGenericServer, + .otherServer, + .callbackServer: + throw GRPCStatus(code: .unimplemented, message: "Server Type not implemented") + case .UNRECOGNIZED: + throw GRPCStatus(code: .invalidArgument, message: "Unrecognised server type") + } + } + + // MARK: Run Client + + /// Handle a message from the driver about operating as a client. + private func handleClientMessage( + context: GRPCAsyncServerCallContext, + args: Grpc_Testing_ClientArgs, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + switch args.argtype { + case let .some(.setup(clientConfig)): + try await self.handleClientSetup( + context: context, + config: clientConfig, + responseStream: responseStream + ) + self.runningClient!.startClient() + case let .some(.mark(mark)): + // Capture stats + try await self.handleClientMarkRequested( + context: context, + mark: mark, + responseStream: responseStream + ) + case .none: + () + } + } + + /// Setup a client as described by the message from the driver. + private func handleClientSetup( + context: GRPCAsyncServerCallContext, + config: Grpc_Testing_ClientConfig, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + context.request.logger.info("client setup requested") + guard self.runningClient == nil else { + context.request.logger.error("client already running") + throw GRPCStatus( + code: GRPCStatus.Code.resourceExhausted, + message: "Client worker busy" + ) + } + try self.runClientBody(context: context, clientConfig: config) + // Initial status is the default (in C++) + try await responseStream.send(Grpc_Testing_ClientStatus()) + } + + /// Captures stats and send back to driver process. + private func handleClientMarkRequested( + context: GRPCAsyncServerCallContext, + mark: Grpc_Testing_Mark, + responseStream: GRPCAsyncResponseStreamWriter + ) async throws { + context.request.logger.info("client mark requested") + guard let runningClient = self.runningClient else { + context.request.logger.error("client not running") + throw GRPCStatus( + code: GRPCStatus.Code.failedPrecondition, + message: "Client not running" + ) + } + try await runningClient.sendStatus(reset: mark.reset, responseStream: responseStream) + } + + /// Call when an end message has been received. + /// Causes the running client to shutdown. + private func handleClientEnd(context: GRPCAsyncServerCallContext) async throws { + context.request.logger.info("runClient ended") + if let runningClient = self.runningClient { + self.runningClient = nil + try await runningClient.shutdown() + } + } + + // MARK: Create Client + + /// Setup and run a client of the requested type. + private func runClientBody( + context: GRPCAsyncServerCallContext, + clientConfig: Grpc_Testing_ClientConfig + ) throws { + self.runningClient = try AsyncWorkerServiceImpl.makeClient( + context: context, + clientConfig: clientConfig + ) + } + + /// Create a client of the requested type. + private static func makeClient( + context: GRPCAsyncServerCallContext, + clientConfig: Grpc_Testing_ClientConfig + ) throws -> AsyncQPSClient { + switch clientConfig.clientType { + case .asyncClient: + if case .bytebufParams = clientConfig.payloadConfig.payload { + throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") + } + return try makeAsyncClient(config: clientConfig) + case .syncClient, + .otherClient, + .callbackClient: + throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") + case .UNRECOGNIZED: + throw GRPCStatus(code: .invalidArgument, message: "Unrecognised client type") + } + } +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/BenchmarkServiceImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOBenchmarkServiceImpl.swift similarity index 94% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/BenchmarkServiceImpl.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOBenchmarkServiceImpl.swift index 9a0988c0a..1ed385b86 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/BenchmarkServiceImpl.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOBenchmarkServiceImpl.swift @@ -19,7 +19,7 @@ import GRPC import NIOCore /// Implementation of asynchronous service for benchmarking. -final class AsyncQPSServerImpl: Grpc_Testing_BenchmarkServiceProvider { +final class NIOBenchmarkServiceImpl: Grpc_Testing_BenchmarkServiceProvider { let interceptors: Grpc_Testing_BenchmarkServiceServerInterceptorFactoryProtocol? = nil /// One request followed by one response. @@ -30,7 +30,7 @@ final class AsyncQPSServerImpl: Grpc_Testing_BenchmarkServiceProvider { ) -> EventLoopFuture { do { return context.eventLoop - .makeSucceededFuture(try AsyncQPSServerImpl.processSimpleRPC(request: request)) + .makeSucceededFuture(try NIOBenchmarkServiceImpl.processSimpleRPC(request: request)) } catch { return context.eventLoop.makeFailedFuture(error) } @@ -46,7 +46,7 @@ final class AsyncQPSServerImpl: Grpc_Testing_BenchmarkServiceProvider { switch event { case let .message(request): do { - let response = try AsyncQPSServerImpl.processSimpleRPC(request: request) + let response = try NIOBenchmarkServiceImpl.processSimpleRPC(request: request) context.sendResponse(response, promise: nil) } catch { context.statusPromise.fail(error) diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ClientInterface.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOClientProtocol.swift similarity index 98% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ClientInterface.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOClientProtocol.swift index df633acab..dea3f1fdb 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ClientInterface.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOClientProtocol.swift @@ -18,7 +18,7 @@ import GRPC import NIOCore /// Protocol which clients must implement. -protocol QPSClient { +protocol NIOQPSClient { /// Send the status of the current test /// - parameters: /// - reset: Indicates if the stats collection should be reset after publication or not. diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncPingPongRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOPingPongRequestMaker.swift similarity index 98% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncPingPongRequestMaker.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOPingPongRequestMaker.swift index f1c1ce91b..9a40c010c 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncPingPongRequestMaker.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOPingPongRequestMaker.swift @@ -21,7 +21,7 @@ import NIOCore /// Makes streaming requests and listens to responses ping-pong style. /// Iterations can be limited by config. -final class AsyncPingPongRequestMaker: RequestMaker { +final class NIOPingPongRequestMaker: NIORequestMaker { private let client: Grpc_Testing_BenchmarkServiceNIOClient private let requestMessage: Grpc_Testing_SimpleRequest private let logger: Logger diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncClient.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSClientImpl.swift similarity index 96% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncClient.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSClientImpl.swift index 5cd4b1d60..3e7a5f2f8 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncClient.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSClientImpl.swift @@ -24,11 +24,11 @@ import NIOCore import NIOPosix /// Client to make a series of asynchronous calls. -final class AsyncQPSClient: QPSClient { +final class NIOQPSClientImpl: NIOQPSClient { private let eventLoopGroup: MultiThreadedEventLoopGroup private let threadCount: Int - private let logger = Logger(label: "AsyncQPSClient") + private let logger = Logger(label: "NIOQPSClientImpl") private let channelRepeaters: [ChannelRepeater] @@ -46,7 +46,7 @@ final class AsyncQPSClient: QPSClient { // Setup threads let threadCount = config.threadsToUse() self.threadCount = threadCount - self.logger.info("Sizing AsyncQPSClient", metadata: ["threads": "\(threadCount)"]) + self.logger.info("Sizing NIOQPSClientImpl", metadata: ["threads": "\(threadCount)"]) let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) self.eventLoopGroup = eventLoopGroup @@ -54,7 +54,7 @@ final class AsyncQPSClient: QPSClient { self.statsPeriodStart = grpcTimeNow() self.cpuStatsPeriodStart = getResourceUsage() - let requestMessage = try AsyncQPSClient + let requestMessage = try NIOQPSClientImpl .makeClientRequest(payloadConfig: config.payloadConfig) // Start the requested number of channels. @@ -276,12 +276,12 @@ final class AsyncQPSClient: QPSClient { /// - parameters: /// - config: Description of the client required. /// - returns: The client created. -func makeAsyncClient(config: Grpc_Testing_ClientConfig) throws -> QPSClient { +func makeAsyncClient(config: Grpc_Testing_ClientConfig) throws -> NIOQPSClient { switch config.rpcType { case .unary: - return try AsyncQPSClient(config: config) + return try NIOQPSClientImpl(config: config) case .streaming: - return try AsyncQPSClient(config: config) + return try NIOQPSClientImpl(config: config) case .streamingFromClient: throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") case .streamingFromServer: diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncServer.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSServerImpl.swift similarity index 96% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncServer.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSServerImpl.swift index 8f7aa475b..234833ed7 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncServer.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSServerImpl.swift @@ -20,8 +20,8 @@ import Logging import NIOCore import NIOPosix -/// Server setup for asynchronous requests. -final class AsyncQPSServer: QPSServer { +/// Server setup for asynchronous requests (using EventLoopFutures). +final class NIOQPSServerImpl: NIOQPSServer { private let eventLoopGroup: MultiThreadedEventLoopGroup private let server: EventLoopFuture private let threadCount: Int @@ -48,7 +48,7 @@ final class AsyncQPSServer: QPSServer { self.statsPeriodStart = grpcTimeNow() self.cpuStatsPeriodStart = getResourceUsage() - let workerService = AsyncQPSServerImpl() + let workerService = NIOBenchmarkServiceImpl() // Start the server. // TODO: Support TLS if requested. diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/RequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIORequestMaker.swift similarity index 98% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/RequestMaker.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIORequestMaker.swift index 5fb47cd3b..3bbb74b87 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/RequestMaker.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIORequestMaker.swift @@ -19,7 +19,7 @@ import Logging import NIOCore /// Implement to provide a method of making requests to a server from a client. -protocol RequestMaker { +protocol NIORequestMaker { /// Initialiser to gather requirements. /// - Parameters: /// - config: config from the driver describing what to do. diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerInterface.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOServerProtocol.swift similarity index 93% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerInterface.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOServerProtocol.swift index 0235d188e..7a04207e6 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerInterface.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOServerProtocol.swift @@ -17,8 +17,8 @@ import GRPC import NIOCore -/// Interface server types must implement. -protocol QPSServer { +/// Interface server types must implement when using NIO. +protocol NIOQPSServer { /// Send the status of the current test /// - parameters: /// - reset: Indicates if the stats collection should be reset after publication or not. diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncUnaryRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOUnaryRequestMaker.swift similarity index 97% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncUnaryRequestMaker.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOUnaryRequestMaker.swift index 43260c2bd..ba2feb807 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/AsyncUnaryRequestMaker.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOUnaryRequestMaker.swift @@ -19,7 +19,7 @@ import Logging import NIOCore /// Makes unary requests to the server and records performance statistics. -final class AsyncUnaryRequestMaker: RequestMaker { +final class NIOUnaryRequestMaker: NIORequestMaker { private let client: Grpc_Testing_BenchmarkServiceNIOClient private let requestMessage: Grpc_Testing_SimpleRequest private let logger: Logger diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/WorkerServiceImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOWorkerServiceImpl.swift similarity index 96% rename from Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/WorkerServiceImpl.swift rename to Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOWorkerServiceImpl.swift index ec6e6f133..1466d5e17 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/WorkerServiceImpl.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOWorkerServiceImpl.swift @@ -18,14 +18,14 @@ import GRPC import NIOCore // Implementation of the control service for communication with the driver process. -class WorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { +class NIOWorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { let interceptors: Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol? = nil private let finishedPromise: EventLoopPromise private let serverPortOverride: Int? - private var runningServer: QPSServer? - private var runningClient: QPSClient? + private var runningServer: NIOQPSServer? + private var runningClient: NIOQPSClient? /// Initialise. /// - parameters: @@ -173,7 +173,7 @@ class WorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { self.serverPortOverride.map { serverConfig.port = Int32($0) } do { - self.runningServer = try WorkerServiceImpl.createServer( + self.runningServer = try NIOWorkerServiceImpl.createServer( context: context, config: serverConfig ) @@ -186,7 +186,7 @@ class WorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { private static func createServer( context: StreamingResponseCallContext, config: Grpc_Testing_ServerConfig - ) throws -> QPSServer { + ) throws -> NIOQPSServer { context.logger.info( "Starting server", metadata: ["type": .stringConvertible(config.serverType)] @@ -196,7 +196,7 @@ class WorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { case .syncServer: throw GRPCStatus(code: .unimplemented, message: "Server Type not implemented") case .asyncServer: - let asyncServer = AsyncQPSServer( + let asyncServer = NIOQPSServerImpl( config: config, whenBound: { serverInfo in var response = Grpc_Testing_ServerStatus() @@ -297,7 +297,7 @@ class WorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { clientConfig: Grpc_Testing_ClientConfig ) { do { - self.runningClient = try WorkerServiceImpl.makeClient( + self.runningClient = try NIOWorkerServiceImpl.makeClient( context: context, clientConfig: clientConfig ) @@ -310,7 +310,7 @@ class WorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { private static func makeClient( context: StreamingResponseCallContext, clientConfig: Grpc_Testing_ClientConfig - ) throws -> QPSClient { + ) throws -> NIOQPSClient { switch clientConfig.clientType { case .syncClient: throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift index 7fa496bbe..1dd44e86b 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift @@ -22,16 +22,18 @@ import NIOPosix /// Sets up and runs a worker service which listens for instructions on what tests to run. /// Currently doesn't understand TLS for communication with the driver. class QPSWorker { - private var driverPort: Int - private var serverPort: Int? + private let driverPort: Int + private let serverPort: Int? + private let useAsync: Bool /// Initialise. /// - parameters: /// - driverPort: Port to listen for instructions on. /// - serverPort: Possible override for the port the testing will actually occur on - usually supplied by the driver process. - init(driverPort: Int, serverPort: Int?) { + init(driverPort: Int, serverPort: Int?, useAsync: Bool) { self.driverPort = driverPort self.serverPort = serverPort + self.useAsync = useAsync } private let logger = Logger(label: "QPSWorker") @@ -49,12 +51,20 @@ class QPSWorker { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) self.eventLoopGroup = eventLoopGroup + let workerService: CallHandlerProvider let workEndPromise: EventLoopPromise = eventLoopGroup.next().makePromise() workEndPromise.futureResult.whenSuccess(onQuit) - let workerService = WorkerServiceImpl( - finishedPromise: workEndPromise, - serverPortOverride: self.serverPort - ) + if self.useAsync { + workerService = AsyncWorkerServiceImpl( + finishedPromise: workEndPromise, + serverPortOverride: self.serverPort + ) + } else { + workerService = NIOWorkerServiceImpl( + finishedPromise: workEndPromise, + serverPortOverride: self.serverPort + ) + } // Start the server. self.logger.info("Binding to localhost", metadata: ["driverPort": "\(self.driverPort)"]) diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift index bac7504b2..5e33f3924 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift @@ -28,7 +28,8 @@ struct Stats { /// Stats with access controlled by a lock - /// Needs locking rather than event loop hopping as the driver refuses to wait shutting /// the connection immediately after the request. -class StatsWithLock { +/// Marked `@unchecked Sendable` since we control access to `data` via a Lock. +final class StatsWithLock: @unchecked Sendable { private var data = Stats() private let lock = Lock() diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift index e20f6efba..cb9df44f3 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift @@ -26,11 +26,8 @@ final class QPSWorkerApp: ParsableCommand { @Option(name: .customLong("server_port"), help: "Port for operation as a server.") var serverPort: Int? - @Option( - name: .customLong("credential_type"), - help: "Credential type for communication with driver." - ) - var credentialType: String = "todo" // TODO: Default to kInsecureCredentialsType + @Flag + var useAsync: Bool = false /// Run the application and wait for completion to be signalled. func run() throws { @@ -47,11 +44,13 @@ final class QPSWorkerApp: ParsableCommand { // This installs backtrace. let lifecycle = ServiceLifecycle() + logger.info("Initializing QPSWorker - useAsync: \(self.useAsync)") let qpsWorker = QPSWorker( driverPort: self.driverPort, - serverPort: self.serverPort + serverPort: self.serverPort, + useAsync: self.useAsync ) - // credentialType: self.credentialType) + qpsWorker.start { lifecycle.shutdown() } From a0d57279afa61975b8ca0d263f8a19c86bc5c43b Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 25 Aug 2022 08:04:15 +0100 Subject: [PATCH 016/580] Add index docc page and fix comment links (#1473) # Motivation We want to be able to generate proper docc pages. # Modification This PR adds a new index page for the `GRPC` module which is adapting the current `README`. Furthermore, it fixes a bunch of links in the doc comments so that they resolve properly. # Result We can now build better docc pages. --- README.md | 2 +- .../GRPCAsyncBidirectionalStreamingCall.swift | 2 +- .../GRPCAsyncClientStreamingCall.swift | 2 +- .../GRPCAsyncRequestStreamWriter.swift | 2 +- .../GRPCAsyncServerCallContext.swift | 12 +- .../GRPCAsyncServerStreamingCall.swift | 2 +- .../GRPCAsyncUnaryCall.swift | 2 +- .../ClientStreamingServerHandler.swift | 2 +- .../CallHandlers/ServerHandlerProtocol.swift | 2 +- Sources/GRPC/CallOptions.swift | 18 +-- .../BidirectionalStreamingCall.swift | 14 +- Sources/GRPC/ClientCalls/Call.swift | 22 ++-- Sources/GRPC/ClientCalls/ClientCall.swift | 18 +-- .../ClientCalls/ClientStreamingCall.swift | 14 +- Sources/GRPC/ClientCalls/UnaryCall.swift | 2 +- Sources/GRPC/ClientConnection.swift | 26 ++-- Sources/GRPC/ClientErrorDelegate.swift | 8 +- .../GRPC/Compression/MessageEncoding.swift | 2 +- Sources/GRPC/ConnectionBackoff.swift | 4 +- Sources/GRPC/ConnectionKeepalive.swift | 4 +- Sources/GRPC/ConnectivityState.swift | 4 +- Sources/GRPC/Docs.docc/index.md | 120 ++++++++++++++++++ .../GRPCChannel/ClientConnection+NWTLS.swift | 2 +- Sources/GRPC/GRPCChannel/GRPCChannel.swift | 26 ++-- .../GRPC/GRPCChannel/GRPCChannelBuilder.swift | 6 +- Sources/GRPC/GRPCClient.swift | 8 +- Sources/GRPC/GRPCError.swift | 2 +- .../GRPCServerRequestRoutingHandler.swift | 2 +- Sources/GRPC/GRPCStatusAndMetadata.swift | 2 +- .../ClientInterceptorContext.swift | 2 +- .../GRPC/Interceptor/ClientInterceptors.swift | 16 +-- .../GRPC/Interceptor/ServerInterceptors.swift | 6 +- Sources/GRPC/Server+NIOSSL.swift | 2 +- Sources/GRPC/Server.swift | 2 +- .../ServerCallContext.swift | 6 +- .../StreamingResponseCallContext.swift | 2 +- .../UnaryResponseCallContext.swift | 6 +- Sources/GRPC/ServerErrorDelegate.swift | 18 +-- Sources/GRPC/TimeLimit.swift | 4 +- Sources/GRPC/UserInfo.swift | 12 +- 40 files changed, 266 insertions(+), 142 deletions(-) create mode 100644 Sources/GRPC/Docs.docc/index.md diff --git a/README.md b/README.md index 040837469..04a4886f1 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ package dependency to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.0.0") + .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.9.0") ] ``` diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 7f2c3271f..2e6ca4590 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -17,7 +17,7 @@ import NIOHPACK -/// Async-await variant of BidirectionalStreamingCall. +/// Async-await variant of ``BidirectionalStreamingCall``. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncBidirectionalStreamingCall: Sendable { private let call: Call diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift index b23baf694..82856bace 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift @@ -17,7 +17,7 @@ import NIOHPACK -/// Async-await variant of `ClientStreamingCall`. +/// Async-await variant of ``ClientStreamingCall``. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncClientStreamingCall: Sendable { private let call: Call diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift index 073fa9d28..53b0d150a 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift @@ -46,7 +46,7 @@ public struct GRPCAsyncRequestStreamWriter: Sendable { /// before sending another request. Callers who do not need this guarantee do not have to `await` /// the completion of this call and may send messages concurrently from multiple ``Task``s. /// However, it is important to note that no more than 16 writes may be pending at any one time - /// and attempting to exceed this will result in an ``GRPCAsyncWriterError.tooManyPendingWrites`` + /// and attempting to exceed this will result in an ``GRPCAsyncWriterError/tooManyPendingWrites`` /// error being thrown. /// /// Callers must call ``finish()`` when they have no more requests left to send. diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift index 7531355e5..41b57f28f 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift @@ -32,10 +32,10 @@ public struct GRPCAsyncServerCallContext: Sendable { Response(contextProvider: self.contextProvider) } - /// Access the `UserInfo` dictionary which is shared with the interceptor contexts for this RPC. + /// Access the ``UserInfo`` dictionary which is shared with the interceptor contexts for this RPC. /// - /// - Important: While `UserInfo` has value-semantics, this function accesses a reference - /// wrapped `UserInfo`. The contexts passed to interceptors provide the same reference. As such + /// - Important: While ``UserInfo`` has value-semantics, this function accesses a reference + /// wrapped ``UserInfo``. The contexts passed to interceptors provide the same reference. As such /// this may be used as a mechanism to pass information between interceptors and service /// providers. public func withUserInfo( @@ -44,10 +44,10 @@ public struct GRPCAsyncServerCallContext: Sendable { return try await self.contextProvider.withUserInfo(body) } - /// Modify the `UserInfo` dictionary which is shared with the interceptor contexts for this RPC. + /// Modify the ``UserInfo`` dictionary which is shared with the interceptor contexts for this RPC. /// - /// - Important: While `UserInfo` has value-semantics, this function accesses a reference - /// wrapped `UserInfo`. The contexts passed to interceptors provide the same reference. As such + /// - Important: While ``UserInfo`` has value-semantics, this function accesses a reference + /// wrapped ``UserInfo``. The contexts passed to interceptors provide the same reference. As such /// this may be used as a mechanism to pass information between interceptors and service /// providers. public func withMutableUserInfo( diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift index e0f67ac43..339dc583e 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift @@ -17,7 +17,7 @@ import NIOHPACK -/// Async-await variant of `ServerStreamingCall`. +/// Async-await variant of ``ServerStreamingCall``. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncServerStreamingCall { private let call: Call diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift index 0d71e0447..3908d2e16 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift @@ -19,7 +19,7 @@ import NIOHPACK /// A unary gRPC call. The request is sent on initialization. /// -/// Note: while this object is a `struct`, its implementation delegates to `Call`. It therefore +/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore /// has reference semantics. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncUnaryCall: Sendable { diff --git a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift index 9e2345ebc..f82c8e136 100644 --- a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift @@ -43,7 +43,7 @@ public final class ClientStreamingServerHandler< @usableFromInline internal let context: CallHandlerContext - /// A reference to a `UserInfo`. + /// A reference to a ``UserInfo``. @usableFromInline internal let userInfoRef: Ref diff --git a/Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift b/Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift index 631b2bdfb..8c3f68796 100644 --- a/Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift +++ b/Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift @@ -20,7 +20,7 @@ import NIOHPACK /// On receiving a new RPC, gRPC will ask all available service providers for an instance of this /// protocol in order to handle the RPC. /// -/// See also: `CallHandlerProvider.handle(method:context:)` +/// See also: ``CallHandlerProvider/handle(method:context:)``. public protocol GRPCServerHandlerProtocol { /// Called when request headers have been received at the start of an RPC. /// - Parameter metadata: The request headers. diff --git a/Sources/GRPC/CallOptions.swift b/Sources/GRPC/CallOptions.swift index 86d555018..d9f28e428 100644 --- a/Sources/GRPC/CallOptions.swift +++ b/Sources/GRPC/CallOptions.swift @@ -41,9 +41,11 @@ public struct CallOptions: GRPCSendable { /// for the remote peer to use for encoding responses. /// /// Compression may also be disabled at the message-level for streaming requests (i.e. client - /// streaming and bidirectional streaming RPCs) by setting `compression` to `.disabled` in - /// `sendMessage(_:compression)`, `sendMessage(_:compression:promise)`, - /// `sendMessages(_:compression)` or `sendMessages(_:compression:promise)`. + /// streaming and bidirectional streaming RPCs) by setting `compression` to ``Compression/disabled`` in + /// ``StreamingRequestClientCall/sendMessage(_:compression:)-uvtc``, + /// ``StreamingRequestClientCall/sendMessage(_:compression:promise:)`` , + /// ``StreamingRequestClientCall/sendMessages(_:compression:)-55vb3`` or + /// ``StreamingRequestClientCall/sendMessage(_:compression:promise:)`. /// /// Note that enabling `compression` via the `sendMessage` or `sendMessages` methods only applies /// if encoding has been specified in these options. @@ -52,12 +54,12 @@ public struct CallOptions: GRPCSendable { /// Whether the call is cacheable. public var cacheable: Bool - /// How IDs should be provided for requests. Defaults to `.autogenerated`. + /// How IDs should be provided for requests. Defaults to ``RequestIDProvider-swift.struct/autogenerated``. /// /// The request ID is used for logging and will be added to the headers of a call if /// `requestIDHeader` is specified. /// - /// - Important: When setting `CallOptions` at the client level, `.userDefined` should __not__ be + /// - Important: When setting ``CallOptions`` at the client level, ``RequestIDProvider-swift.struct/userDefined(_:)`` should __not__ be /// used otherwise each request will have the same ID. public var requestIDProvider: RequestIDProvider @@ -65,7 +67,7 @@ public struct CallOptions: GRPCSendable { /// value is `nil` (the default) then no additional header will be added. /// /// Setting this value will add a request ID to the headers of the call these options are used - /// with. The request ID will be provided by `requestIDProvider` and will also be used in log + /// with. The request ID will be provided by ``requestIDProvider-swift.property`` and will also be used in log /// messages associated with the call. public var requestIDHeader: String? @@ -81,7 +83,7 @@ public struct CallOptions: GRPCSendable { /// A logger used for the call. Defaults to a no-op logger. /// - /// If a `requestIDProvider` exists then a request ID will automatically attached to the logger's + /// If a ``requestIDProvider-swift.property`` exists then a request ID will automatically attached to the logger's /// metadata using the 'grpc-request-id' key. public var logger: Logger @@ -166,7 +168,7 @@ extension CallOptions { /// Specify an ID to be used. /// - /// - Important: this should only be used when `CallOptions` are passed directly to the call. + /// - Important: this should only be used when ``CallOptions`` are passed directly to the call. /// If it is used for the default options on a client then all calls with have the same ID. public static func userDefined(_ requestID: String) -> RequestIDProvider { return RequestIDProvider(.static(requestID)) diff --git a/Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift b/Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift index e14c8939a..dff23a5c2 100644 --- a/Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift +++ b/Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift @@ -20,10 +20,10 @@ import NIOHTTP2 /// A bidirectional-streaming gRPC call. Each response is passed to the provided observer block. /// -/// Messages should be sent via the `sendMessage` and `sendMessages` methods; the stream of messages -/// must be terminated by calling `sendEnd` to indicate the final message has been sent. +/// Messages should be sent via the ``sendMessage(_:compression:)`` and ``sendMessages(_:compression:)`` methods; the stream of messages +/// must be terminated by calling ``sendEnd()`` to indicate the final message has been sent. /// -/// Note: while this object is a `struct`, its implementation delegates to `Call`. It therefore +/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore /// has reference semantics. public struct BidirectionalStreamingCall< RequestPayload, @@ -94,8 +94,8 @@ public struct BidirectionalStreamingCall< /// Sends a message to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or - /// `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or + /// ``sendEnd(promise:)``. /// /// - Parameters: /// - message: The message to send. @@ -113,8 +113,8 @@ public struct BidirectionalStreamingCall< /// Sends a sequence of messages to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or - /// `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or + /// ``sendEnd(promise:)``. /// /// - Parameters: /// - messages: The sequence of messages to send. diff --git a/Sources/GRPC/ClientCalls/Call.swift b/Sources/GRPC/ClientCalls/Call.swift index 81897c2a2..3277681a4 100644 --- a/Sources/GRPC/ClientCalls/Call.swift +++ b/Sources/GRPC/ClientCalls/Call.swift @@ -22,21 +22,21 @@ import protocol SwiftProtobuf.Message /// An object representing a single RPC from the perspective of a client. It allows the caller to /// send request parts, request a cancellation, and receive response parts in a provided callback. /// -/// The call object sits atop an interceptor pipeline (see `ClientInterceptor`) which allows for +/// The call object sits atop an interceptor pipeline (see ``ClientInterceptor``) which allows for /// request and response streams to be arbitrarily transformed or observed. Requests sent via this /// call will traverse the pipeline before reaching the network, and responses received will /// traverse the pipeline having been received from the network. /// -/// This object is a lower-level API than the equivalent wrapped calls (such as `UnaryCall` and -/// `BidirectionalStreamingCall`). The caller is therefore required to do more in order to use this -/// object correctly. Callers must call `invoke(_:)` to start the call and ensure that the correct +/// This object is a lower-level API than the equivalent wrapped calls (such as ``UnaryCall`` and +/// ``BidirectionalStreamingCall``). The caller is therefore required to do more in order to use this +/// object correctly. Callers must call ``invoke(onError:onResponsePart:)`` to start the call and ensure that the correct /// number of request parts are sent in the correct order (exactly one `metadata`, followed /// by at most one `message` for unary and server streaming calls, and any number of `message` parts /// for client streaming and bidirectional streaming calls. All call types must terminate their /// request stream by sending one `end` message. /// -/// Callers are not able to create `Call` objects directly, rather they must be created via an -/// object conforming to `GRPCChannel` such as `ClientConnection`. +/// Callers are not able to create ``Call`` objects directly, rather they must be created via an +/// object conforming to ``GRPCChannel`` such as ``ClientConnection``. public final class Call { @usableFromInline internal enum State { @@ -109,7 +109,7 @@ public final class Call { /// Starts the call and provides a callback which is invoked on every response part received from /// the server. /// - /// This must be called prior to `send(_:promise:)` or `cancel(promise:)`. + /// This must be called prior to ``send(_:)`` or ``cancel()``. /// /// - Parameters: /// - onError: A callback invoked when an error is received. @@ -135,7 +135,7 @@ public final class Call { /// - Parameters: /// - part: The request part to send. /// - promise: A promise which will be completed when the request part has been handled. - /// - Note: Sending will always fail if `invoke(_:)` has not been called. + /// - Note: Sending will always fail if ``invoke(onError:onResponsePart:)`` has not been called. @inlinable public func send(_ part: GRPCClientRequestPart, promise: EventLoopPromise?) { if self.eventLoop.inEventLoop { @@ -150,7 +150,7 @@ public final class Call { /// Attempt to cancel the RPC. /// - Parameter promise: A promise which will be completed once the cancellation request has been /// dealt with. - /// - Note: Cancellation will always fail if `invoke(_:)` has not been called. + /// - Note: Cancellation will always fail if ``invoke(onError:onResponsePart:)`` has not been called. public func cancel(promise: EventLoopPromise?) { if self.eventLoop.inEventLoop { self._cancel(promise: promise) @@ -166,7 +166,7 @@ extension Call { /// Send a request part on the RPC. /// - Parameter part: The request part to send. /// - Returns: A future which will be resolved when the request has been handled. - /// - Note: Sending will always fail if `invoke(_:)` has not been called. + /// - Note: Sending will always fail if ``invoke(onError:onResponsePart:)`` has not been called. @inlinable public func send(_ part: GRPCClientRequestPart) -> EventLoopFuture { let promise = self.eventLoop.makePromise(of: Void.self) @@ -175,7 +175,7 @@ extension Call { } /// Attempt to cancel the RPC. - /// - Note: Cancellation will always fail if `invoke(_:)` has not been called. + /// - Note: Cancellation will always fail if ``invoke(onError:onResponsePart:)`` has not been called. /// - Returns: A future which will be resolved when the cancellation request has been cancelled. public func cancel() -> EventLoopFuture { let promise = self.eventLoop.makePromise(of: Void.self) diff --git a/Sources/GRPC/ClientCalls/ClientCall.swift b/Sources/GRPC/ClientCalls/ClientCall.swift index 2ec01b753..d2d386893 100644 --- a/Sources/GRPC/ClientCalls/ClientCall.swift +++ b/Sources/GRPC/ClientCalls/ClientCall.swift @@ -43,7 +43,7 @@ public protocol ClientCall { /// /// The client may populate the status if, for example, it was not possible to connect to the service. /// - /// Note: despite `GRPCStatus` conforming to `Error`, the value will be __always__ delivered as a __success__ + /// Note: despite ``GRPCStatus`` conforming to `Error`, the value will be __always__ delivered as a __success__ /// result even if the status represents a __negative__ outcome. This future will __never__ be fulfilled /// with an error. var status: EventLoopFuture { get } @@ -54,7 +54,7 @@ public protocol ClientCall { /// Cancel the current call. /// /// Closes the HTTP/2 stream once it becomes available. Additional writes to the channel will be ignored. - /// Any unfulfilled promises will be failed with a cancelled status (excepting `status` which will be + /// Any unfulfilled promises will be failed with a cancelled status (excepting ``status`` which will be /// succeeded, if not already succeeded). func cancel(promise: EventLoopPromise?) } @@ -67,11 +67,12 @@ extension ClientCall { } } -/// A `ClientCall` with request streaming; i.e. client-streaming and bidirectional-streaming. +/// A ``ClientCall`` with request streaming; i.e. client-streaming and bidirectional-streaming. public protocol StreamingRequestClientCall: ClientCall { /// Sends a message to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling + /// or ``sendEnd(promise:)``. /// /// - Parameters: /// - message: The message to send. @@ -82,7 +83,7 @@ public protocol StreamingRequestClientCall: ClientCall { /// Sends a message to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()-7bhdp`` or ``sendEnd(promise:)``. /// /// - Parameters: /// - message: The message to send. @@ -97,7 +98,8 @@ public protocol StreamingRequestClientCall: ClientCall { /// Sends a sequence of messages to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling + /// or ``sendEnd(promise:)``. /// /// - Parameters: /// - messages: The sequence of messages to send. @@ -108,7 +110,7 @@ public protocol StreamingRequestClientCall: ClientCall { /// Sends a sequence of messages to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()-7bhdp`` or ``sendEnd(promise:)``. /// /// - Parameters: /// - messages: The sequence of messages to send. @@ -161,7 +163,7 @@ extension StreamingRequestClientCall { } } -/// A `ClientCall` with a unary response; i.e. unary and client-streaming. +/// A ``ClientCall`` with a unary response; i.e. unary and client-streaming. public protocol UnaryResponseClientCall: ClientCall { /// The response message returned from the service if the call is successful. This may be failed /// if the call encounters an error. diff --git a/Sources/GRPC/ClientCalls/ClientStreamingCall.swift b/Sources/GRPC/ClientCalls/ClientStreamingCall.swift index 03bd534ca..7f8bcd581 100644 --- a/Sources/GRPC/ClientCalls/ClientStreamingCall.swift +++ b/Sources/GRPC/ClientCalls/ClientStreamingCall.swift @@ -20,10 +20,10 @@ import NIOHTTP2 /// A client-streaming gRPC call. /// -/// Messages should be sent via the `sendMessage` and `sendMessages` methods; the stream of messages -/// must be terminated by calling `sendEnd` to indicate the final message has been sent. +/// Messages should be sent via the ``sendMessage(_:compression:)`` and ``sendMessages(_:compression:)`` methods; the stream of messages +/// must be terminated by calling ``sendEnd()`` to indicate the final message has been sent. /// -/// Note: while this object is a `struct`, its implementation delegates to `Call`. It therefore +/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore /// has reference semantics. public struct ClientStreamingCall: StreamingRequestClientCall, UnaryResponseClientCall { @@ -94,8 +94,8 @@ public struct ClientStreamingCall: StreamingReq /// Sends a message to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or - /// `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or + /// ``sendEnd(promise:)``. /// /// - Parameters: /// - message: The message to send. @@ -113,8 +113,8 @@ public struct ClientStreamingCall: StreamingReq /// Sends a sequence of messages to the service. /// - /// - Important: Callers must terminate the stream of messages by calling `sendEnd()` or - /// `sendEnd(promise:)`. + /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or + /// ``sendEnd(promise:)``. /// /// - Parameters: /// - messages: The sequence of messages to send. diff --git a/Sources/GRPC/ClientCalls/UnaryCall.swift b/Sources/GRPC/ClientCalls/UnaryCall.swift index 5cb7a712a..5efd9f819 100644 --- a/Sources/GRPC/ClientCalls/UnaryCall.swift +++ b/Sources/GRPC/ClientCalls/UnaryCall.swift @@ -23,7 +23,7 @@ import SwiftProtobuf /// A unary gRPC call. The request is sent on initialization. /// -/// Note: while this object is a `struct`, its implementation delegates to `Call`. It therefore +/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore /// has reference semantics. public struct UnaryCall: UnaryResponseClientCall { private let call: Call diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index a376a46db..fbc2c58ea 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -41,15 +41,15 @@ import SwiftProtobuf /// passed (5 minutes, by default) -- the underlying channel will be closed. The client will not /// idle the connection if any RPC exists, even if there has been no activity on the RPC for the /// idle timeout. Long-lived, low activity RPCs may benefit from configuring keepalive (see -/// `ClientConnectionKeepalive`) which periodically pings the server to ensure that the connection +/// ``ClientConnectionKeepalive``) which periodically pings the server to ensure that the connection /// is not dropped. If the connection is idle a new channel will be created on-demand when the next /// RPC is made. /// -/// The state of the connection can be observed using a `ConnectivityStateDelegate`. +/// The state of the connection can be observed using a ``ConnectivityStateDelegate``. /// /// Since the connection is managed, and may potentially spend long periods of time waiting for a /// connection to come up (cellular connections, for example), different behaviors may be used when -/// starting a call. The different behaviors are detailed in the `CallStartBehavior` documentation. +/// starting a call. The different behaviors are detailed in the ``CallStartBehavior`` documentation. /// /// ### Channel Pipeline /// @@ -111,11 +111,11 @@ public final class ClientConnection: GRPCSendable { } /// Creates a new connection from the given configuration. Prefer using - /// `ClientConnection.secure(group:)` to build a connection secured with TLS or - /// `ClientConnection.insecure(group:)` to build a plaintext connection. + /// ``ClientConnection/secure(group:)`` to build a connection secured with TLS or + /// ``ClientConnection/insecure(group:)`` to build a plaintext connection. /// - /// - Important: Users should prefer using `ClientConnection.secure(group:)` to build a connection - /// with TLS, or `ClientConnection.insecure(group:)` to build a connection without TLS. + /// - Important: Users should prefer using ``ClientConnection/secure(group:)`` to build a connection + /// with TLS, or ``ClientConnection/insecure(group:)`` to build a connection without TLS. public init(configuration: Configuration) { self.configuration = configuration self.scheme = configuration.tlsConfiguration == nil ? "http" : "https" @@ -354,8 +354,8 @@ public struct CallStartBehavior: Hashable, GRPCSendable { } extension ClientConnection { - /// Configuration for a `ClientConnection`. Users should prefer using one of the - /// `ClientConnection` builders: `ClientConnection.secure(_:)` or `ClientConnection.insecure(_:)`. + /// Configuration for a ``ClientConnection``. Users should prefer using one of the + /// ``ClientConnection`` builders: ``ClientConnection/secure(group:)`` or ``ClientConnection/insecure(group:)``. public struct Configuration: GRPCSendable { /// The target to connect to. public var target: ConnectionTarget @@ -365,7 +365,7 @@ extension ClientConnection { /// An error delegate which is called when errors are caught. Provided delegates **must not /// maintain a strong reference to this `ClientConnection`**. Doing so will cause a retain - /// cycle. Defaults to `LoggingClientErrorDelegate`. + /// cycle. Defaults to ``LoggingClientErrorDelegate``. public var errorDelegate: ClientErrorDelegate? = LoggingClientErrorDelegate.shared /// A delegate which is called when the connectivity state is changed. Defaults to `nil`. @@ -378,8 +378,8 @@ extension ClientConnection { #if canImport(NIOSSL) /// TLS configuration for this connection. `nil` if TLS is not desired. /// - /// - Important: `tls` is deprecated; use `tlsConfiguration` or one of - /// the `ClientConnection.withTLS` builder functions. + /// - Important: `tls` is deprecated; use ``tlsConfiguration`` or one of + /// the ``ClientConnection/usingTLS(with:on:)`` builder functions. @available(*, deprecated, renamed: "tlsConfiguration") public var tls: TLS? { get { @@ -412,7 +412,7 @@ extension ClientConnection { /// The behavior used to determine when an RPC should start. That is, whether it should wait for /// an active connection or fail quickly if no connection is currently available. /// - /// Defaults to `waitsForConnectivity`. + /// Defaults to ``CallStartBehavior/waitsForConnectivity``. public var callStartBehavior: CallStartBehavior = .waitsForConnectivity /// The HTTP/2 flow control target window size. Defaults to 8MB. Values are clamped between diff --git a/Sources/GRPC/ClientErrorDelegate.swift b/Sources/GRPC/ClientErrorDelegate.swift index 4054c4474..ee3734ae2 100644 --- a/Sources/GRPC/ClientErrorDelegate.swift +++ b/Sources/GRPC/ClientErrorDelegate.swift @@ -19,9 +19,9 @@ import Logging /// Delegate called when errors are caught by the client on individual HTTP/2 streams and errors in /// the underlying HTTP/2 connection. /// -/// The intended use of this protocol is with `ClientConnection`. In order to avoid retain +/// The intended use of this protocol is with ``ClientConnection``. In order to avoid retain /// cycles, classes implementing this delegate **must not** maintain a strong reference to the -/// `ClientConnection`. +/// ``ClientConnection``. public protocol ClientErrorDelegate: AnyObject, GRPCPreconcurrencySendable { /// Called when the client catches an error. /// @@ -34,14 +34,14 @@ public protocol ClientErrorDelegate: AnyObject, GRPCPreconcurrencySendable { } extension ClientErrorDelegate { - /// Calls `didCatchError(_:logger:file:line:)` with appropriate context placeholders when no + /// Calls ``didCatchError(_:logger:file:line:)`` with appropriate context placeholders when no /// context is available. internal func didCatchErrorWithoutContext(_ error: Error, logger: Logger) { self.didCatchError(error, logger: logger, file: "", line: 0) } } -/// A `ClientErrorDelegate` which logs errors. +/// A ``ClientErrorDelegate`` which logs errors. public final class LoggingClientErrorDelegate: ClientErrorDelegate { /// A shared instance of this class. public static let shared = LoggingClientErrorDelegate() diff --git a/Sources/GRPC/Compression/MessageEncoding.swift b/Sources/GRPC/Compression/MessageEncoding.swift index b3fdf4d34..60605768e 100644 --- a/Sources/GRPC/Compression/MessageEncoding.swift +++ b/Sources/GRPC/Compression/MessageEncoding.swift @@ -37,7 +37,7 @@ public struct Compression: Hashable, GRPCSendable { /// Disable compression. public static let disabled = Compression(.disabled) - /// Defer to the call (the `CallOptions` for the client, and the context for the server) to + /// Defer to the call (the ``CallOptions`` for the client, and the context for the server) to /// determine whether compression should be used for the message. public static let deferToCallDefault = Compression(.deferToCallDefault) } diff --git a/Sources/GRPC/ConnectionBackoff.swift b/Sources/GRPC/ConnectionBackoff.swift index fda681036..c176757de 100644 --- a/Sources/GRPC/ConnectionBackoff.swift +++ b/Sources/GRPC/ConnectionBackoff.swift @@ -66,7 +66,7 @@ public struct ConnectionBackoff: Sequence, GRPCSendable { } } - /// Creates a `ConnectionBackoff`. + /// Creates a ``ConnectionBackoff``. /// /// - Parameters: /// - initialBackoff: Initial backoff in seconds, defaults to 1.0. @@ -97,7 +97,7 @@ public struct ConnectionBackoff: Sequence, GRPCSendable { } } -/// An iterator for `ConnectionBackoff`. +/// An iterator for ``ConnectionBackoff``. public class ConnectionBackoffIterator: IteratorProtocol { public typealias Element = (timeout: TimeInterval, backoff: TimeInterval) diff --git a/Sources/GRPC/ConnectionKeepalive.swift b/Sources/GRPC/ConnectionKeepalive.swift index 07a630f0f..e1e7a9783 100644 --- a/Sources/GRPC/ConnectionKeepalive.swift +++ b/Sources/GRPC/ConnectionKeepalive.swift @@ -29,7 +29,7 @@ public struct ClientConnectionKeepalive: Hashable, GRPCSendable { /// The amount of time to wait for an acknowledgment. /// If it does not receive an acknowledgment within this time, it will close the connection - /// This value must be less than `interval` + /// This value must be less than ``interval``. public var timeout: TimeAmount /// Send keepalive pings even if there are no calls in flight. @@ -64,7 +64,7 @@ public struct ServerConnectionKeepalive: Hashable { /// The amount of time to wait for an acknowledgment. /// If it does not receive an acknowledgment within this time, it will close the connection - /// This value must be less than `interval` + /// This value must be less than ``interval``. public var timeout: TimeAmount /// Send keepalive pings even if there are no calls in flight. diff --git a/Sources/GRPC/ConnectivityState.swift b/Sources/GRPC/ConnectivityState.swift index 2098a3693..7c6a4621d 100644 --- a/Sources/GRPC/ConnectivityState.swift +++ b/Sources/GRPC/ConnectivityState.swift @@ -33,7 +33,7 @@ public enum ConnectivityState: GRPCSendable { case ready /// There has been some transient failure (such as a TCP 3-way handshake timing out or a socket - /// error). Channels in this state will eventually switch to the `.connecting` state and try to + /// error). Channels in this state will eventually switch to the ``connecting`` state and try to /// establish a connection again. Since retries are done with exponential backoff, channels that /// fail to connect will start out spending very little time in this state but as the attempts /// fail repeatedly, the channel will spend increasingly large amounts of time in this state. @@ -48,7 +48,7 @@ public enum ConnectivityState: GRPCSendable { } public protocol ConnectivityStateDelegate: AnyObject, GRPCPreconcurrencySendable { - /// Called when a change in `ConnectivityState` has occurred. + /// Called when a change in ``ConnectivityState`` has occurred. /// /// - Parameter oldState: The old connectivity state. /// - Parameter newState: The new connectivity state. diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md new file mode 100644 index 000000000..ee324f570 --- /dev/null +++ b/Sources/GRPC/Docs.docc/index.md @@ -0,0 +1,120 @@ +# ``GRPC`` + +gRPC for Swift. + +grpc-swift is a Swift package that contains a gRPC Swift API and code generator. + +It is intended for use with Apple's [SwiftProtobuf][swift-protobuf] support for +Protocol Buffers. Both projects contain code generation plugins for `protoc`, +Google's Protocol Buffer compiler, and both contain libraries of supporting code +that is needed to build and run the generated code. + +APIs and generated code is provided for both gRPC clients and servers, and can +be built either with Xcode or the Swift Package Manager. Support is provided for +all four gRPC API styles (Unary, Server Streaming, Client Streaming, and +Bidirectional Streaming) and connections can be made either over secure (TLS) or +insecure channels. + +## Supported Platforms + +gRPC Swift's platform support is identical to the [platform support of Swift +NIO][swift-nio-platforms]. + +The earliest supported Swift version for gRPC Swift 1.8.x and newer is 5.4. +For 1.7.x and earlier the oldest supported Swift version is 5.2. + +Versions of clients and services which are use Swift's Concurrency support +are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. + +## Getting gRPC Swift + +There are two parts to gRPC Swift: the gRPC library and an API code generator. + +### Getting the gRPC library + +The Swift Package Manager is the preferred way to get gRPC Swift. Simply add the +package dependency to your `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.9.0") +] +``` + +...and depend on `"GRPC"` in the necessary targets: + +```swift +.target( + name: ..., + dependencies: [.product(name: "GRPC", package: "grpc-swift")] +] +``` + +### Getting the protoc Plugins + +Binary releases of `protoc`, the Protocol Buffer Compiler, are available on +[GitHub][protobuf-releases]. + +To build the plugins, run `make plugins` in the main directory. This uses the +Swift Package Manager to build both of the necessary plugins: +`protoc-gen-swift`, which generates Protocol Buffer support code and +`protoc-gen-grpc-swift`, which generates gRPC interface code. + +To install these plugins, just copy the two executables (`protoc-gen-swift` and +`protoc-gen-grpc-swift`) that show up in the main directory into a directory +that is part of your `PATH` environment variable. Alternatively the full path to +the plugins can be specified when using `protoc`. + +#### 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 +across two directories: + +- [`/Sources/Examples`][examples-in-source] contains examples which do not + require additional dependencies and may be built using the Swift Package + Manager. +- [`/Examples`][examples-out-of-source] contains examples which rely on + external dependencies or may not be built by the Swift Package Manager (such + as an iOS app). + +Some of the examples are accompanied by tutorials, including: +- A [quick start guide][docs-quickstart] for creating and running your first + gRPC service. +- A [basic tutorial][docs-tutorial] covering the creation and implementation of + a gRPC service using all four call types as well as the code required to setup + and run a server and make calls to it using a generated client. +- An [interceptors][docs-interceptors-tutorial] tutorial covering how to create + and use interceptors with gRPC Swift. + +## Additional documentation + +- Options for the `protoc` plugin in [`docs/plugin.md`][docs-plugin] +- How to configure TLS in [`docs/tls.md`][docs-tls] +- How to configure keepalive in [`docs/keepalive.md`][docs-keepalive] +- Support for Apple Platforms and NIO Transport Services in + [`docs/apple-platforms.md`][docs-apple] + +[docs-apple]: https://github.com/grpc/grpc-swift/tree/main/docs/apple-platforms.md +[docs-plugin]: https://github.com/grpc/grpc-swift/tree/main/docs/plugin.md +[docs-quickstart]: https://github.com/grpc/grpc-swift/tree/main/docs/quick-start.md +[docs-tls]: https://github.com/grpc/grpc-swift/tree/main/docs/tls.md +[docs-keepalive]: https://github.com/grpc/grpc-swift/tree/main/docs/keepalive.md +[docs-tutorial]: https://github.com/grpc/grpc-swift/tree/main/docs/basic-tutorial.md +[docs-interceptors-tutorial]: https://github.com/grpc/grpc-swift/tree/main/docs/interceptors-tutorial.md +[grpc]: https://github.com/grpc/grpc +[protobuf-releases]: https://github.com/protocolbuffers/protobuf/releases +[swift-nio-platforms]: https://github.com/apple/swift-nio#supported-platforms +[swift-nio]: https://github.com/apple/swift-nio +[swift-protobuf]: https://github.com/apple/swift-protobuf +[xcode-spm]: https://help.apple.com/xcode/mac/current/#/devb83d64851 +[branch-new]: https://github.com/grpc/grpc-swift/tree/main +[branch-old]: https://github.com/grpc/grpc-swift/tree/cgrpc +[examples-out-of-source]: https://github.com/grpc/grpc-swift/tree/main/Examples +[examples-in-source]: https://github.com/grpc/grpc-swift/tree/main/Sources/Examples diff --git a/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift b/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift index 6a7a7a399..132d8566f 100644 --- a/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift +++ b/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift @@ -19,7 +19,7 @@ import NIOCore import Security extension ClientConnection { - /// Returns a `ClientConnection` builder configured with the Network.framework TLS backend. + /// Returns a ``ClientConnection`` builder configured with the Network.framework TLS backend. /// /// This builder must use a `NIOTSEventLoopGroup` (or an `EventLoop` from a /// `NIOTSEventLoopGroup`). diff --git a/Sources/GRPC/GRPCChannel/GRPCChannel.swift b/Sources/GRPC/GRPCChannel/GRPCChannel.swift index 3760434bd..b34ef5c12 100644 --- a/Sources/GRPC/GRPCChannel/GRPCChannel.swift +++ b/Sources/GRPC/GRPCChannel/GRPCChannel.swift @@ -21,18 +21,18 @@ public protocol GRPCChannel: GRPCPreconcurrencySendable { /// Makes a gRPC call on the channel with requests and responses conforming to /// `SwiftProtobuf.Message`. /// - /// Note: this is a lower-level construct that any of `UnaryCall`, `ClientStreamingCall`, - /// `ServerStreamingCall` or `BidirectionalStreamingCall` and does not have an API to protect + /// Note: this is a lower-level construct that any of ``UnaryCall``, ``ClientStreamingCall``, + /// ``ServerStreamingCall`` or ``BidirectionalStreamingCall`` and does not have an API to protect /// users against protocol violations (such as sending to requests on a unary call). /// - /// After making the `Call`, users must `invoke` the call with a callback which is invoked - /// for each response part (or error) received. Any call to `send(_:promise:)` prior to calling - /// `invoke` will fail and not be sent. Users are also responsible for closing the request stream + /// After making the ``Call``, users must ``Call/invoke(onError:onResponsePart:)`` the call with a callback which is invoked + /// for each response part (or error) received. Any call to ``Call/send(_:)`` prior to calling + /// ``Call/invoke(onError:onResponsePart:)`` will fail and not be sent. Users are also responsible for closing the request stream /// by sending the `.end` request part. /// /// - Parameters: /// - path: The path of the RPC, e.g. "/echo.Echo/get". - /// - type: The type of the RPC, e.g. `.unary`. + /// - type: The type of the RPC, e.g. ``GRPCCallType/unary``. /// - callOptions: Options for the RPC. /// - interceptors: A list of interceptors to intercept the request and response stream with. func makeCall( @@ -42,20 +42,20 @@ public protocol GRPCChannel: GRPCPreconcurrencySendable { interceptors: [ClientInterceptor] ) -> Call - /// Makes a gRPC call on the channel with requests and responses conforming to `GRPCPayload`. + /// Makes a gRPC call on the channel with requests and responses conforming to ``GRPCPayload``. /// - /// Note: this is a lower-level construct that any of `UnaryCall`, `ClientStreamingCall`, - /// `ServerStreamingCall` or `BidirectionalStreamingCall` and does not have an API to protect + /// Note: this is a lower-level construct that any of ``UnaryCall``, ``ClientStreamingCall``, + /// ``ServerStreamingCall`` or ``BidirectionalStreamingCall`` and does not have an API to protect /// users against protocol violations (such as sending to requests on a unary call). /// - /// After making the `Call`, users must `invoke` the call with a callback which is invoked - /// for each response part (or error) received. Any call to `send(_:promise:)` prior to calling - /// `invoke` will fail and not be sent. Users are also responsible for closing the request stream + /// After making the ``Call``, users must ``Call/invoke(onError:onResponsePart:)`` the call with a callback which is invoked + /// for each response part (or error) received. Any call to ``Call/send(_:)`` prior to calling + /// ``Call/invoke(onError:onResponsePart:)`` will fail and not be sent. Users are also responsible for closing the request stream /// by sending the `.end` request part. /// /// - Parameters: /// - path: The path of the RPC, e.g. "/echo.Echo/get". - /// - type: The type of the RPC, e.g. `.unary`. + /// - type: The type of the RPC, e.g. ``GRPCCallType/unary``. /// - callOptions: Options for the RPC. /// - interceptors: A list of interceptors to intercept the request and response stream with. func makeCall( diff --git a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift index 714171e99..1dee4b921 100644 --- a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift +++ b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift @@ -18,12 +18,12 @@ import Logging import NIOCore extension ClientConnection { - /// Returns an insecure `ClientConnection` builder which is *not configured with TLS*. + /// Returns an insecure ``ClientConnection`` builder which is *not configured with TLS*. public static func insecure(group: EventLoopGroup) -> ClientConnection.Builder { return Builder(group: group) } - /// Returns a `ClientConnection` builder configured with a TLS backend appropriate for the + /// Returns a ``ClientConnection`` builder configured with a TLS backend appropriate for the /// given `EventLoopGroup`. /// /// gRPC Swift offers two TLS 'backends'. The 'NIOSSL' backend is available on Darwin and Linux @@ -50,7 +50,7 @@ extension ClientConnection { ) } - /// Returns a `ClientConnection` builder configured with the TLS backend appropriate for the + /// Returns a ``ClientConnection`` builder configured with the TLS backend appropriate for the /// provided configuration and `EventLoopGroup`. /// /// - Important: The caller is responsible for ensuring the provided `configuration` may be used diff --git a/Sources/GRPC/GRPCClient.swift b/Sources/GRPC/GRPCClient.swift index 726c67ff3..1a892c46e 100644 --- a/Sources/GRPC/GRPCClient.swift +++ b/Sources/GRPC/GRPCClient.swift @@ -166,7 +166,7 @@ extension GRPCClient { } /// A client which has no generated stubs and may be used to create gRPC calls manually. -/// See `GRPCClient` for details. +/// See ``GRPCClient`` for details. /// /// Example: /// @@ -194,7 +194,7 @@ public final class AnyServiceClient: GRPCClient { /// Creates a client which may be used to call any service. /// /// - Parameters: - /// - connection: `ClientConnection` to the service host. + /// - connection: ``ClientConnection`` to the service host. /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. public init(channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions()) { self.channel = channel @@ -209,7 +209,7 @@ extension AnyServiceClient: @unchecked GRPCSendable {} #endif // swift(>=5.6) /// A client which has no generated stubs and may be used to create gRPC calls manually. -/// See `GRPCClient` for details. +/// See ``GRPCClient`` for details. /// /// Example: /// @@ -229,7 +229,7 @@ public struct GRPCAnyServiceClient: GRPCClient { /// Creates a client which may be used to call any service. /// /// - Parameters: - /// - connection: `ClientConnection` to the service host. + /// - connection: ``ClientConnection`` to the service host. /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. public init(channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions()) { self.channel = channel diff --git a/Sources/GRPC/GRPCError.swift b/Sources/GRPC/GRPCError.swift index 5c247bfe8..051e2a6c0 100644 --- a/Sources/GRPC/GRPCError.swift +++ b/Sources/GRPC/GRPCError.swift @@ -325,7 +325,7 @@ extension GRPCError { } } -/// Requirements for `GRPCError` types. +/// Requirements for ``GRPCError`` types. public protocol GRPCErrorProtocol: GRPCStatusTransformable, Equatable, CustomStringConvertible {} extension GRPCErrorProtocol { diff --git a/Sources/GRPC/GRPCServerRequestRoutingHandler.swift b/Sources/GRPC/GRPCServerRequestRoutingHandler.swift index b3e65ad3b..fd0f36157 100644 --- a/Sources/GRPC/GRPCServerRequestRoutingHandler.swift +++ b/Sources/GRPC/GRPCServerRequestRoutingHandler.swift @@ -20,7 +20,7 @@ import NIOHTTP1 import NIOHTTP2 import SwiftProtobuf -/// Provides `GRPCServerHandlerProtocol` objects for the methods on a particular service name. +/// Provides ``GRPCServerHandlerProtocol`` objects for the methods on a particular service name. /// /// Implemented by the generated code. public protocol CallHandlerProvider: AnyObject { diff --git a/Sources/GRPC/GRPCStatusAndMetadata.swift b/Sources/GRPC/GRPCStatusAndMetadata.swift index ec0806c97..04b58c98a 100644 --- a/Sources/GRPC/GRPCStatusAndMetadata.swift +++ b/Sources/GRPC/GRPCStatusAndMetadata.swift @@ -16,7 +16,7 @@ import NIOHPACK import NIOHTTP1 -/// A simple struct holding a `GRPCStatus` and optionally trailers in the form of +/// A simple struct holding a ``GRPCStatus`` and optionally trailers in the form of /// `HPACKHeaders`. public struct GRPCStatusAndTrailers: Equatable { /// The status. diff --git a/Sources/GRPC/Interceptor/ClientInterceptorContext.swift b/Sources/GRPC/Interceptor/ClientInterceptorContext.swift index 9efc9e7d2..bfc192546 100644 --- a/Sources/GRPC/Interceptor/ClientInterceptorContext.swift +++ b/Sources/GRPC/Interceptor/ClientInterceptorContext.swift @@ -54,7 +54,7 @@ public struct ClientInterceptorContext { return self._pipeline.details.options } - /// Construct a `ClientInterceptorContext` for the interceptor at the given index within in + /// Construct a ``ClientInterceptorContext`` for the interceptor at the given index within in /// interceptor pipeline. @inlinable internal init( diff --git a/Sources/GRPC/Interceptor/ClientInterceptors.swift b/Sources/GRPC/Interceptor/ClientInterceptors.swift index b179528e8..282549d5a 100644 --- a/Sources/GRPC/Interceptor/ClientInterceptors.swift +++ b/Sources/GRPC/Interceptor/ClientInterceptors.swift @@ -21,24 +21,24 @@ import NIOCore /// The default behaviour for this base class is to forward any events to the next interceptor. /// /// Interceptors may observe a number of different events: -/// - receiving response parts with `receive(_:context:)`, -/// - receiving errors with `errorCaught(_:context:)`, -/// - sending request parts with `send(_:promise:context:)`, and -/// - RPC cancellation with `cancel(context:)`. +/// - receiving response parts with ``receive(_:context:)-5v1ih``, +/// - receiving errors with ``errorCaught(_:context:)-6pncp``, +/// - sending request parts with ``send(_:promise:context:)-4igtj``, and +/// - RPC cancellation with ``cancel(promise:context:)-5tkf5``. /// /// These events flow through a pipeline of interceptors for each RPC. Request parts sent from the -/// call object (e.g. `UnaryCall`, `BidirectionalStreamingCall`) will traverse the pipeline in the -/// outbound direction from its tail via `send(_:context:)` eventually reaching the head of the +/// call object (e.g. ``UnaryCall``, ``BidirectionalStreamingCall``) will traverse the pipeline in the +/// outbound direction from its tail via ``send(_:promise:context:)-4igtj`` eventually reaching the head of the /// pipeline where it will be sent sent to the server. /// /// Response parts, or errors, received from the transport fill be fired in the inbound direction -/// back through the interceptor pipeline via `receive(_:context:)` and `errorCaught(_:context:)`, +/// back through the interceptor pipeline via ``receive(_:context:)-5v1ih`` and ``errorCaught(_:context:)-6pncp``, /// respectively. Note that the `end` response part and any error received are terminal: the /// pipeline will be torn down once these parts reach the the tail and are a signal that the /// interceptor should free up any resources it may be using. /// /// Each of the interceptor functions is provided with a `context` which exposes analogous functions -/// (`receive(_:)`, `errorCaught(_:)`, `send(_:promise:)`, and `cancel(promise:)`) which may be +/// (``receive(_:context:)-5v1ih``, ``errorCaught(_:context:)-6pncp``, ``send(_:promise:context:)-4igtj``, and ``cancel(promise:context:)-5tkf5``) which may be /// called to forward events to the next interceptor in the appropriate direction. /// /// ### Thread Safety diff --git a/Sources/GRPC/Interceptor/ServerInterceptors.swift b/Sources/GRPC/Interceptor/ServerInterceptors.swift index 4090b0bde..48a3e2a06 100644 --- a/Sources/GRPC/Interceptor/ServerInterceptors.swift +++ b/Sources/GRPC/Interceptor/ServerInterceptors.swift @@ -22,8 +22,8 @@ import NIOCore /// interceptor. /// /// Interceptors may observe two different types of event: -/// - receiving request parts with `receive(_:context:)`, -/// - sending response parts with `send(_:promise:context:)`. +/// - receiving request parts with ``receive(_:context:)``, +/// - sending response parts with ``send(_:promise:context:)``. /// /// These events flow through a pipeline of interceptors for each RPC. Request parts will enter /// the head of the interceptor pipeline once the request router has determined that there is a @@ -32,7 +32,7 @@ import NIOCore /// traversing the pipeline through to the head. /// /// Each of the interceptor functions is provided with a `context` which exposes analogous functions -/// (`receive(_:)` and `send(_:promise:)`) which may be called to forward events to the next +/// (``receive(_:context:)`` and ``send(_:promise:context:)``) which may be called to forward events to the next /// interceptor. /// /// ### Thread Safety diff --git a/Sources/GRPC/Server+NIOSSL.swift b/Sources/GRPC/Server+NIOSSL.swift index 6c27900c8..5656ca744 100644 --- a/Sources/GRPC/Server+NIOSSL.swift +++ b/Sources/GRPC/Server+NIOSSL.swift @@ -17,7 +17,7 @@ import NIOSSL extension Server.Configuration { - /// TLS configuration for a `Server`. + /// TLS configuration for a ``Server``. /// /// Note that this configuration is a subset of `NIOSSL.TLSConfiguration` where certain options /// are removed from the users control to ensure the configuration complies with the gRPC diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 3a77509bf..77b5e8caf 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -315,7 +315,7 @@ extension Server { /// streaming and bidirectional streaming RPCs) by passing setting `compression` to `.disabled` /// in `sendResponse(_:compression)`. /// - /// Defaults to `.disabled`. + /// Defaults to ``ServerMessageEncoding/disabled``. public var messageEncoding: ServerMessageEncoding = .disabled /// The maximum size in bytes of a message which may be received from a client. Defaults to 4MB. diff --git a/Sources/GRPC/ServerCallContexts/ServerCallContext.swift b/Sources/GRPC/ServerCallContexts/ServerCallContext.swift index e229836d7..603442e1d 100644 --- a/Sources/GRPC/ServerCallContexts/ServerCallContext.swift +++ b/Sources/GRPC/ServerCallContexts/ServerCallContext.swift @@ -90,8 +90,8 @@ open class ServerCallContextBase: ServerCallContext { /// A `UserInfo` dictionary which is shared with the interceptor contexts for this RPC. /// - /// - Important: While `UserInfo` has value-semantics, this property retrieves from, and sets a - /// reference wrapped `UserInfo`. The contexts passed to interceptors provide the same + /// - Important: While ``UserInfo`` has value-semantics, this property retrieves from, and sets a + /// reference wrapped ``UserInfo``. The contexts passed to interceptors provide the same /// reference. As such this may be used as a mechanism to pass information between interceptors /// and service providers. /// - Important: This *must* be accessed from the context's `eventLoop` in order to ensure @@ -107,7 +107,7 @@ open class ServerCallContextBase: ServerCallContext { } } - /// A reference to an underlying `UserInfo`. We share this with the interceptors. + /// A reference to an underlying ``UserInfo``. We share this with the interceptors. @usableFromInline internal let userInfoRef: Ref diff --git a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift b/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift index 614cc2c34..1dcfa6aa4 100644 --- a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift +++ b/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift @@ -23,7 +23,7 @@ import SwiftProtobuf /// An abstract base class for a context provided to handlers for RPCs which may return multiple /// responses, i.e. server streaming and bidirectional streaming RPCs. open class StreamingResponseCallContext: ServerCallContextBase { - /// A promise for the `GRPCStatus`, the end of the response stream. This must be completed by + /// A promise for the ``GRPCStatus``, the end of the response stream. This must be completed by /// bidirectional streaming RPC handlers to end the RPC. /// /// Note that while this is also present for server streaming RPCs, it is not necessary to diff --git a/Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift b/Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift index 252f2c467..9e3a3a892 100644 --- a/Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift +++ b/Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift @@ -25,11 +25,11 @@ import SwiftProtobuf /// /// For client streaming RPCs the handler must complete the `responsePromise` to return the response /// to the client. Unary RPCs do complete the promise directly: they are provided an -/// `StatusOnlyCallContext` view of this context where the `responsePromise` is not exposed. Instead +/// ``StatusOnlyCallContext`` view of this context where the `responsePromise` is not exposed. Instead /// they must return an `EventLoopFuture` from the method they are implementing. open class UnaryResponseCallContext: ServerCallContextBase, StatusOnlyCallContext { /// A promise for a single response message. This must be completed to send a response back to the - /// client. If the promise is failed, the failure value will be converted to `GRPCStatus` and + /// client. If the promise is failed, the failure value will be converted to ``GRPCStatus`` and /// used as the final status for the RPC. public let responsePromise: EventLoopPromise @@ -102,7 +102,7 @@ open class UnaryResponseCallContext: ServerCallContextBase, StatusOnly } } -/// Protocol variant of `UnaryResponseCallContext` that only exposes the `responseStatus` and `trailingMetadata` +/// Protocol variant of ``UnaryResponseCallContext`` that only exposes the ``responseStatus`` and ``trailers`` /// fields, but not `responsePromise`. /// /// We can use a protocol (instead of an abstract base class) here because removing the generic diff --git a/Sources/GRPC/ServerErrorDelegate.swift b/Sources/GRPC/ServerErrorDelegate.swift index 69f188d01..c9c8e15a3 100644 --- a/Sources/GRPC/ServerErrorDelegate.swift +++ b/Sources/GRPC/ServerErrorDelegate.swift @@ -26,15 +26,15 @@ public protocol ServerErrorDelegate: AnyObject { /// Transforms the given error (thrown somewhere inside the gRPC library) into a new error. /// /// This allows library users to transform errors which may be out of their control - /// into more meaningful `GRPCStatus` errors before they are sent to the user. + /// into more meaningful ``GRPCStatus`` errors before they are sent to the user. /// /// - note: - /// Errors returned by this method are not passed to `observe` again. + /// Errors returned by this method are not passed to ``observeLibraryError(_:)-5wuhj`` again. /// /// - note: - /// This defaults to returning `nil`. In that case, if the original error conforms to `GRPCStatusTransformable`, - /// that error's `asGRPCStatus()` result will be sent to the user. If that's not the case, either, - /// `GRPCStatus.processingError` is returned. + /// This defaults to returning `nil`. In that case, if the original error conforms to ``GRPCStatusTransformable``, + /// that error's ``GRPCStatusTransformable/makeGRPCStatus()`` result will be sent to the user. If that's not the case, either, + /// ``GRPCStatus/processingError`` is returned. func transformLibraryError(_ error: Error) -> GRPCStatusAndTrailers? /// Called when a request's status or response promise is failed somewhere in the user-provided request handler code. @@ -46,15 +46,15 @@ public protocol ServerErrorDelegate: AnyObject { /// Transforms the given status or response promise failure into a new error. /// /// This allows library users to transform errors which happen during their handling of the request - /// into more meaningful `GRPCStatus` errors before they are sent to the user. + /// into more meaningful ``GRPCStatus`` errors before they are sent to the user. /// /// - note: /// Errors returned by this method are not passed to `observe` again. /// /// - note: - /// This defaults to returning `nil`. In that case, if the original error conforms to `GRPCStatusTransformable`, - /// that error's `asGRPCStatus()` result will be sent to the user. If that's not the case, either, - /// `GRPCStatus.processingError` is returned. + /// This defaults to returning `nil`. In that case, if the original error conforms to ``GRPCStatusTransformable``, + /// that error's ``GRPCStatusTransformable/makeGRPCStatus()`` result will be sent to the user. If that's not the case, either, + /// ``GRPCStatus/processingError`` is returned. /// /// - Parameters: /// - error: The original error the status/response promise was failed with. diff --git a/Sources/GRPC/TimeLimit.swift b/Sources/GRPC/TimeLimit.swift index a44d3ff6c..b8ebcb26e 100644 --- a/Sources/GRPC/TimeLimit.swift +++ b/Sources/GRPC/TimeLimit.swift @@ -24,10 +24,10 @@ import NIOCore /// /// RPCs may have a time limit imposed on them by a caller which may be timeout or deadline based. /// If the RPC has not completed before the limit is reached then the call will be cancelled and -/// completed with a `.deadlineExceeded` status code. +/// completed with a ``GRPCStatus/Code-swift.struct/deadlineExceeded`` status code. /// /// - Note: Servers may impose a time limit on an RPC independent of the client's time limit; RPCs -/// may therefore complete with `.deadlineExceeded` even if no time limit was set by the client. +/// may therefore complete with ``GRPCStatus/Code-swift.struct/deadlineExceeded`` even if no time limit was set by the client. public struct TimeLimit: Equatable, CustomStringConvertible, GRPCSendable { // private but for shimming. private enum Wrapped: Equatable, GRPCSendable { diff --git a/Sources/GRPC/UserInfo.swift b/Sources/GRPC/UserInfo.swift index b388c27ca..239ad542a 100644 --- a/Sources/GRPC/UserInfo.swift +++ b/Sources/GRPC/UserInfo.swift @@ -14,13 +14,13 @@ * limitations under the License. */ -/// `UserInfo` is a dictionary for heterogeneously typed values with type safe access to the stored +/// ``UserInfo`` is a dictionary for heterogeneously typed values with type safe access to the stored /// values. /// -/// `UserInfo` is shared between server interceptor contexts and server handlers, this is on a -/// per-RPC basis. `UserInfo` is *not* shared across a connection. +/// ``UserInfo`` is shared between server interceptor contexts and server handlers, this is on a +/// per-RPC basis. ``UserInfo`` is *not* shared across a connection. /// -/// Values are keyed by a type conforming to the `UserInfo.Key` protocol. The protocol requires an +/// Values are keyed by a type conforming to the ``UserInfo/Key`` protocol. The protocol requires an /// `associatedtype`: the type of the value the key is paired with. A key can be created using a /// caseless `enum`, for example: /// @@ -30,7 +30,7 @@ /// } /// ``` /// -/// Values can be set and retrieved from `UserInfo` by subscripting with the key: +/// Values can be set and retrieved from ``UserInfo`` by subscripting with the key: /// /// ``` /// userInfo[IDKey.self] = 42 @@ -39,7 +39,7 @@ /// userInfo[IDKey.self] = nil /// ``` /// -/// More convenient access can be provided with helper extensions on `UserInfo`: +/// More convenient access can be provided with helper extensions on ``UserInfo``: /// /// ``` /// extension UserInfo { From 964a8775f77391e3094e93ed55ccc14ef3048568 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 31 Aug 2022 13:53:37 +0100 Subject: [PATCH 017/580] Add SPM plugin for GRPC code generation (#1474) * Add SPM plugin for GRPC code generation ## Motivation After adding an SPM plugin for protobuf generation, we want to offer the same feature for GRPC code generation. ## Modifications * Added a GRPC codegen SPM plugin ## Result GRPC codegen SPM plugin is now available. * PR changes * Folder rename * Fixes typo in docs * Changes in docs --- Package.swift | 17 +- Plugins/GRPCSwiftPlugin/plugin.swift | 187 ++++++++++++++++++ .../Docs.docc/spm-plugin.md | 139 +++++++++++++ 3 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 Plugins/GRPCSwiftPlugin/plugin.swift create mode 100644 Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md diff --git a/Package.swift b/Package.swift index da2618013..16957ad2b 100644 --- a/Package.swift +++ b/Package.swift @@ -48,7 +48,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.19.0" + from: "1.20.1" ), .package( url: "https://github.com/apple/swift-log.git", @@ -165,6 +165,14 @@ extension Target { ] ) + static let grpcSwiftPlugin: Target = .plugin( + name: "GRPCSwiftPlugin", + capability: .buildTool(), + dependencies: [ + .protocGenGRPCSwift, + ] + ) + static let grpcTests: Target = .testTarget( name: "GRPCTests", dependencies: [ @@ -423,6 +431,11 @@ extension Product { name: "protoc-gen-grpc-swift", targets: ["protoc-gen-grpc-swift"] ) + + static let grpcSwiftPlugin: Product = .plugin( + name: "GRPCSwiftPlugin", + targets: ["GRPCSwiftPlugin"] + ) } // MARK: - Package @@ -433,6 +446,7 @@ let package = Package( .grpc, .cgrpcZlib, .protocGenGRPCSwift, + .grpcSwiftPlugin, ], dependencies: packageDependencies, targets: [ @@ -440,6 +454,7 @@ let package = Package( .grpc, .cgrpcZlib, .protocGenGRPCSwift, + .grpcSwiftPlugin, // Tests etc. .grpcTests, diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift new file mode 100644 index 000000000..d933bea55 --- /dev/null +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -0,0 +1,187 @@ +/* + * Copyright 2022, 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 Foundation +import PackagePlugin + +@main +struct GRPCSwiftPlugin: BuildToolPlugin { + /// Errors thrown by the `GRPCSwiftPlugin` + enum PluginError: Error { + /// Indicates that the target where the plugin was applied to was not `SourceModuleTarget`. + case invalidTarget + /// Indicates that the file extension of an input file was not `.proto`. + case invalidInputFileExtension + } + + /// The configuration of the plugin. + struct Configuration: Codable { + /// Encapsulates a single invocation of protoc. + struct Invocation: Codable { + /// The visibility of the generated files. + enum Visibility: String, Codable { + /// The generated files should have `internal` access level. + case `internal` + /// The generated files should have `public` access level. + case `public` + } + + /// An array of paths to `.proto` files for this invocation. + var protoFiles: [String] + /// The visibility of the generated files. + var visibility: Visibility? + /// Whether server code is generated. + var server: Bool? + /// Whether client code is generated. + var client: Bool? + /// Determines whether the casing of generated function names is kept. + var keepMethodCasing: Bool? + } + + /// The path to the `protoc` binary. + /// + /// If this is not set, SPM will try to find the tool itself. + var protocPath: String? + + /// A list of invocations of `protoc` with the `GRPCSwiftPlugin`. + var invocations: [Invocation] + } + + static let configurationFileName = "grpc-swift-config.json" + + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + // Let's check that this is a source target + guard let target = target as? SourceModuleTarget else { + throw PluginError.invalidTarget + } + + // We need to find the configuration file at the root of the target + let configurationFilePath = target.directory.appending(subpath: Self.configurationFileName) + let data = try Data(contentsOf: URL(fileURLWithPath: "\(configurationFilePath)")) + let configuration = try JSONDecoder().decode(Configuration.self, from: data) + + try self.validateConfiguration(configuration) + + // We need to find the path of protoc and protoc-gen-grpc-swift + let protocPath: Path + if let configuredProtocPath = configuration.protocPath { + protocPath = Path(configuredProtocPath) + } else if let environmentPath = ProcessInfo.processInfo.environment["PROTOC_PATH"] { + // The user set the env variable, so let's take that + protocPath = Path(environmentPath) + } else { + // The user didn't set anything so let's try see if SPM can find a binary for us + protocPath = try context.tool(named: "protoc").path + } + let protocGenGRPCSwiftPath = try context.tool(named: "protoc-gen-grpc-swift").path + + // This plugin generates its output into GeneratedSources + let outputDirectory = context.pluginWorkDirectory + + return configuration.invocations.map { invocation in + self.invokeProtoc( + target: target, + invocation: invocation, + protocPath: protocPath, + protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, + outputDirectory: outputDirectory + ) + } + } + + /// Invokes `protoc` with the given inputs + /// + /// - Parameters: + /// - target: The plugin's target. + /// - invocation: The `protoc` invocation. + /// - protocPath: The path to the `protoc` binary. + /// - protocGenSwiftPath: The path to the `protoc-gen-swift` binary. + /// - outputDirectory: The output directory for the generated files. + /// - Returns: The build command. + private func invokeProtoc( + target: Target, + invocation: Configuration.Invocation, + protocPath: Path, + protocGenGRPCSwiftPath: Path, + outputDirectory: Path + ) -> Command { + // Construct the `protoc` arguments. + var protocArgs = [ + "--plugin=protoc-gen-grpc-swift=\(protocGenGRPCSwiftPath)", + "--grpc-swift_out=\(outputDirectory)", + // We include the target directory as a proto search path + "-I", + "\(target.directory)", + ] + + if let visibility = invocation.visibility { + protocArgs.append("--grpc-swift_opt=Visibility=\(visibility.rawValue.capitalized)") + } + + if let generateServerCode = invocation.server { + protocArgs.append("--grpc-swift_opt=Server=\(generateServerCode)") + } + + if let generateClientCode = invocation.client { + protocArgs.append("--grpc-swift_opt=Client=\(generateClientCode)") + } + + if let keepMethodCasingOption = invocation.keepMethodCasing { + protocArgs.append("--grpc-swift_opt=KeepMethodCasing=\(keepMethodCasingOption)") + } + + var inputFiles = [Path]() + var outputFiles = [Path]() + + for var file in invocation.protoFiles { + // Append the file to the protoc args so that it is used for generating + protocArgs.append("\(file)") + inputFiles.append(target.directory.appending(file)) + + // The name of the output file is based on the name of the input file. + // We validated in the beginning that every file has the suffix of .proto + // This means we can just drop the last 5 elements and append the new suffix + file.removeLast(5) + file.append("grpc.swift") + let protobufOutputPath = outputDirectory.appending(file) + + // Add the outputPath as an output file + outputFiles.append(protobufOutputPath) + } + + // Construct the command. Specifying the input and output paths lets the build + // system know when to invoke the command. The output paths are passed on to + // the rule engine in the build system. + return Command.buildCommand( + displayName: "Generating gRPC Swift files from proto files", + executable: protocPath, + arguments: protocArgs, + inputFiles: inputFiles + [protocGenGRPCSwiftPath], + outputFiles: outputFiles + ) + } + + /// Validates the configuration file for various user errors. + private func validateConfiguration(_ configuration: Configuration) throws { + for invocation in configuration.invocations { + for protoFile in invocation.protoFiles { + if !protoFile.hasSuffix(".proto") { + throw PluginError.invalidInputFileExtension + } + } + } + } +} diff --git a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md new file mode 100644 index 000000000..879e661db --- /dev/null +++ b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md @@ -0,0 +1,139 @@ +# Using the Swift Package Manager plugin + +The Swift Package Manager introduced new plugin capabilities in Swift 5.6, enabling the extension of +the build process with custom build tools. Learn how to use the `GRPCSwiftPlugin` plugin for the +Swift Package Manager. + +## Overview + +> Warning: Due to limitations of binary executable discovery with Xcode we only recommend using the Swift Package Manager +plugin in leaf packages. For more information, read the `Defining the path to the protoc binary` section of +this article. + +The plugin works by running the system installed `protoc` compiler with the `protoc-gen-grpc-swift` plugin +for specified `.proto` files in your targets source folder. Furthermore, the plugin allows defining a +configuration file which will be used to customize the invocation of `protoc`. + +### Installing the protoc compiler + +First, you must ensure that you have the `protoc` compiler installed. +There are multiple ways to do this. Some of the easiest are: + +1. If you are on macOS, installing it via `brew install protoc` +2. Download the binary from [Google's github repository](https://github.com/protocolbuffers/protobuf). + +### Adding the proto files to your target + +Next, you need to add the `.proto` files for which you want to generate your Swift types to your target's +source directory. You should also commit these files to your git repository since the generated types +are now generated on demand. + +> Note: imports on your `.proto` files will have to include the relative path from the target source to the `.proto` file you wish to import. + +### Adding the plugin to your manifest + +After adding the `.proto` files you can now add the plugin to the target inside your `Package.swift` manifest. +First, you need to add a dependency on `grpc-swift`. Afterwards, you can declare the usage of the plugin +for your target. Here is an example snippet of a `Package.swift` manifest: + +```swift +let package = Package( + name: "YourPackage", + products: [...], + dependencies: [ + ... + .package(url: "https://github.com/grpc/grpc-swift", from: "1.10.0"), + ... + ], + targets: [ + ... + .executableTarget( + name: "YourTarget", + plugins: [ + .plugin(name: "GRPCSwiftPlugin", package: "grpc-swift") + ] + ), + ... +) + +``` + +### Configuring the plugin + +Lastly, after you have added the `.proto` files and modified your `Package.swift` manifest, you can now +configure the plugin to invoke the `protoc` compiler. This is done by adding a `grpc-swift-config.json` +to the root of your target's source folder. An example configuration file looks like this: + +```json +{ + "invocations": [ + { + "protoFiles": [ + "Path/To/Foo.proto", + ], + "visibility": "internal", + "server": false + }, + { + "protoFiles": [ + "Bar.proto" + ], + "visibility": "public", + "client": false, + "keepMethodCasing": false + } + ] +} +``` + +> Note: paths to your `.proto` files will have to include the relative path from the target source to the `.proto` file location. + +In the above configuration, you declared two invocations to the `protoc` compiler. The first invocation +is generating Swift types for the `Foo.proto` file with `internal` visibility. Notice the relative path to the `.proto` file. +We have also specified the `server` option and set it to false: this means that server code won't be generated for this proto. +The second invocation is generating Swift types for the `Bar.proto` file with the `public` visibility. +Notice the `client` option: it's been set to false, so no client code will be generated for this proto. We have also set +the `keepMethodCasing` option to false, which means that the casing of the autogenerated captions won't be kept. + +> Note: You can find more information about supported options in the protoc Swift plugin documentation. Be aware that +`server`, `client` and `keepMethodCasing` are currently the only three options supported in the Swift Package Manager plugin. + +### Defining the path to the protoc binary + +The plugin needs to be able to invoke the `protoc` binary to generate the Swift types. There are several ways to achieve this. + +First, by default, the package manager looks into the `$PATH` to find binaries named `protoc`. +This works immediately if you use `swift build` to build your package and `protoc` is installed +in the `$PATH` (`brew` is adding it to your `$PATH` automatically). +However, this doesn't work if you want to compile from Xcode since Xcode is not passed the `$PATH`. + +If compiling from Xcode, you have **three options** to set the path of `protoc` that the plugin is going to use: + +* Set an environment variable `PROTOC_PATH` that gets picked up by the plugin. Here are two examples of how you can achieve this: + +```shell +# swift build +env PROTOC_PATH=/opt/homebrew/bin/protoc swift build + +# To start Xcode (Xcode MUST NOT be running before invoking this) +env PROTOC_PATH=/opt/homebrew/bin/protoc xed . + +# xcodebuild +env PROTOC_PATH=/opt/homebrew/bin/protoc xcodebuild +``` + +* Point the plugin to the concrete location of the `protoc` compiler is by changing the configuration file like this: + +```json +{ + "protocPath": "/path/to/protoc", + "invocations": [...] +} +``` + +> Warning: The configuration file option only solves the problem for leaf packages that are using the Swift package manager +plugin since there you can point the package manager to the right binary. The environment variable +does solve the problem for transitive packages as well; however, it requires your users to set +the variable now. In general we advise against adopting the plugin as a non-leaf package! + +* You can start Xcode by running `$ xed .` from the command line from the directory your project is located - this should make `$PATH` visible to Xcode. From 20bba23f4aa0f0d48cef2f8e31741be6621f802b Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 31 Aug 2022 15:09:47 +0100 Subject: [PATCH 018/580] Add GRPC and protobuf SPM plugins to QPSBenchmark (#1475) Motivation We want to make use of both the protobuf and GRPC codegen SPM plugins in QPSBenchmark Modifications * Added support for both GRPC and protobuf plugin to QPSBenchmark Result QPSBenchmark now autogenerates both proto and GRPC code when building. --- Performance/QPSBenchmark/Makefile | 63 - Performance/QPSBenchmark/Package.swift | 13 +- Performance/QPSBenchmark/README.md | 2 - .../Model/benchmark_service.grpc.swift | 782 ------ .../Model/benchmark_service.pb.swift | 38 - .../Model/benchmark_service.proto | 2 +- .../QPSBenchmark/Model/control.pb.swift | 2381 ----------------- .../Sources/QPSBenchmark/Model/control.proto | 4 +- .../QPSBenchmark/Model/core_stats.pb.swift | 312 --- .../QPSBenchmark/Model/messages.pb.swift | 1225 --------- .../QPSBenchmark/Model/payloads.pb.swift | 335 --- .../Sources/QPSBenchmark/Model/stats.pb.swift | 470 ---- .../Sources/QPSBenchmark/Model/stats.proto | 2 +- .../Model/worker_service.grpc.swift | 681 ----- .../Model/worker_service.pb.swift | 38 - .../QPSBenchmark/Model/worker_service.proto | 2 +- .../QPSBenchmark/grpc-swift-config.json | 11 + .../QPSBenchmark/swift-protobuf-config.json | 14 + 18 files changed, 34 insertions(+), 6341 deletions(-) delete mode 100644 Performance/QPSBenchmark/Makefile delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.pb.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.pb.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.pb.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.pb.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.pb.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.pb.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.pb.swift create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json create mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json diff --git a/Performance/QPSBenchmark/Makefile b/Performance/QPSBenchmark/Makefile deleted file mode 100644 index b78694f4b..000000000 --- a/Performance/QPSBenchmark/Makefile +++ /dev/null @@ -1,63 +0,0 @@ -# Which Swift to use. -SWIFT:=swift -# Where products will be built; this is the SPM default. -GRPC_SWIFT_PATH:=../.. -SWIFT_BUILD_PATH:=./.build -SWIFT_BUILD_CONFIGURATION=release -SWIFT_FLAGS=--build-path=${SWIFT_BUILD_PATH} --configuration=${SWIFT_BUILD_CONFIGURATION} - -# protoc plugins. -PROTOC_GEN_SWIFT=${GRPC_SWIFT_PATH}/${SWIFT_BUILD_PATH}/release/protoc-gen-swift -PROTOC_GEN_GRPC_SWIFT=${GRPC_SWIFT_PATH}/${SWIFT_BUILD_PATH}/release/protoc-gen-grpc-swift - -SWIFT_BUILD:=${SWIFT} build ${SWIFT_FLAGS} -SWIFT_BUILD_RELEASE:=${SWIFT} build ${SWIFT_FLAGS_RELEASE} - -### Package and plugin build targets ########################################### - -all: - ${SWIFT_BUILD} - -${PROTOC_GEN_SWIFT}: ${GRPC_SWIFT_PATH}/Package.resolved - ${SWIFT_BUILD_RELEASE} --product protoc-gen-swift --package-path ${GRPC_SWIFT_PATH} - -${PROTOC_GEN_GRPC_SWIFT}: ${GRPC_SWIFT_PATH}/Sources/protoc-gen-grpc-swift/*.swift - ${SWIFT_BUILD_RELEASE} --product protoc-gen-grpc-swift --package-path ${GRPC_SWIFT_PATH} - -### Protobuf Generation ######################################################## - -%.pb.swift: %.proto ${PROTOC_GEN_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_SWIFT} \ - --swift_opt=Visibility=Public \ - --swift_out=$(dir $<) - -%.grpc.swift: %.proto ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Visibility=Public \ - --grpc-swift_out=$(dir $<) - -QBS_PROTO=Sources/QPSBenchmark/Model/benchmark_service.proto \ - Sources/QPSBenchmark/Model/payloads.proto \ - Sources/QPSBenchmark/Model/control.proto \ - Sources/QPSBenchmark/Model/stats.proto \ - Sources/QPSBenchmark/Model/core_stats.proto \ - Sources/QPSBenchmark/Model/worker_service.proto \ - Sources/QPSBenchmark/Model/messages.proto -QBS_PB=$(QBS_PROTO:.proto=.pb.swift) -QBS_GRPC=$(QBS_PROTO:.proto=.grpc.swift) - -# Generate the protobufs for the QPS benchmarking worker. -.PHONY: -generate-qps-worker: ${QBS_PB} ${QBS_GRPC} - -### Misc. ###################################################################### - -.PHONY: -clean: - -rm -rf Packages - -rm -rf ${SWIFT_BUILD_PATH} - -rm -f Package.resolved diff --git a/Performance/QPSBenchmark/Package.swift b/Performance/QPSBenchmark/Package.swift index 8a900b981..ba34e21a0 100644 --- a/Performance/QPSBenchmark/Package.swift +++ b/Performance/QPSBenchmark/Package.swift @@ -35,7 +35,7 @@ let package = Package( ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.19.0" + from: "1.20.1" ), ], targets: [ @@ -53,14 +53,9 @@ let package = Package( .product(name: "SwiftProtobuf", package: "swift-protobuf"), .target(name: "BenchmarkUtils"), ], - exclude: [ - "Model/benchmark_service.proto", - "Model/control.proto", - "Model/core_stats.proto", - "Model/messages.proto", - "Model/payloads.proto", - "Model/stats.proto", - "Model/worker_service.proto", + plugins: [ + .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf"), + .plugin(name: "GRPCSwiftPlugin", package: "grpc-swift"), ] ), .target( diff --git a/Performance/QPSBenchmark/README.md b/Performance/QPSBenchmark/README.md index fe93865cc..a8e26d1d2 100644 --- a/Performance/QPSBenchmark/README.md +++ b/Performance/QPSBenchmark/README.md @@ -5,8 +5,6 @@ An implementation of the QPS worker for benchmarking described in the ## Building -To rebuild the proto files run `make generate-qps-worker`. - The benchmarks can be built in the usual Swift Package Manager way but release mode is strongly recommended: `swift build -c release` diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift deleted file mode 100644 index 2a6a61826..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.grpc.swift +++ /dev/null @@ -1,782 +0,0 @@ -// -// DO NOT EDIT. -// -// Generated by the protocol buffer compiler. -// Source: benchmark_service.proto -// - -// -// Copyright 2018, 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 GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Usage: instantiate `Grpc_Testing_BenchmarkServiceClient`, then call methods of this protocol to make API calls. -public protocol Grpc_Testing_BenchmarkServiceClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? { get } - - func unaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func streamingCall( - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_SimpleResponse) -> Void - ) -> BidirectionalStreamingCall - - func streamingFromClient( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func streamingFromServer( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_SimpleResponse) -> Void - ) -> ServerStreamingCall - - func streamingBothWays( - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_SimpleResponse) -> Void - ) -> BidirectionalStreamingCall -} - -extension Grpc_Testing_BenchmarkServiceClientProtocol { - public var serviceName: String { - return "grpc.testing.BenchmarkService" - } - - /// One request followed by one response. - /// The server returns the client payload as-is. - /// - /// - Parameters: - /// - request: Request to send to UnaryCall. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func unaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.unaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [] - ) - } - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func streamingCall( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_SimpleResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingCallInterceptors() ?? [], - handler: handler - ) - } - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. - public func streamingFromClient( - callOptions: CallOptions? = nil - ) -> ClientStreamingCall { - return self.makeClientStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromClient.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingFromClientInterceptors() ?? [] - ) - } - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - /// - /// - Parameters: - /// - request: Request to send to StreamingFromServer. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - public func streamingFromServer( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_SimpleResponse) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromServer.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingFromServerInterceptors() ?? [], - handler: handler - ) - } - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func streamingBothWays( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_SimpleResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingBothWays.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingBothWaysInterceptors() ?? [], - handler: handler - ) - } -} - -#if compiler(>=5.6) -@available(*, deprecated) -extension Grpc_Testing_BenchmarkServiceClient: @unchecked Sendable {} -#endif // compiler(>=5.6) - -@available(*, deprecated, renamed: "Grpc_Testing_BenchmarkServiceNIOClient") -public final class Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServiceClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the grpc.testing.BenchmarkService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Grpc_Testing_BenchmarkServiceNIOClient: Grpc_Testing_BenchmarkServiceClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? - - /// Creates a client for the grpc.testing.BenchmarkService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -#if compiler(>=5.6) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_BenchmarkServiceAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? { get } - - func makeUnaryCallCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeStreamingCallCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall - - func makeStreamingFromClientCall( - callOptions: CallOptions? - ) -> GRPCAsyncClientStreamingCall - - func makeStreamingFromServerCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? - ) -> GRPCAsyncServerStreamingCall - - func makeStreamingBothWaysCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_BenchmarkServiceAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_BenchmarkServiceClientMetadata.serviceDescriptor - } - - public var interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? { - return nil - } - - public func makeUnaryCallCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.unaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [] - ) - } - - public func makeStreamingCallCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingCallInterceptors() ?? [] - ) - } - - public func makeStreamingFromClientCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncClientStreamingCall { - return self.makeAsyncClientStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromClient.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingFromClientInterceptors() ?? [] - ) - } - - public func makeStreamingFromServerCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncServerStreamingCall { - return self.makeAsyncServerStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromServer.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingFromServerInterceptors() ?? [] - ) - } - - public func makeStreamingBothWaysCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingBothWays.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingBothWaysInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_BenchmarkServiceAsyncClientProtocol { - public func unaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_SimpleResponse { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.unaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [] - ) - } - - public func streamingCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_SimpleRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingCallInterceptors() ?? [] - ) - } - - public func streamingCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_SimpleRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingCallInterceptors() ?? [] - ) - } - - public func streamingFromClient( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_SimpleResponse where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_SimpleRequest { - return try await self.performAsyncClientStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromClient.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingFromClientInterceptors() ?? [] - ) - } - - public func streamingFromClient( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_SimpleResponse where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_SimpleRequest { - return try await self.performAsyncClientStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromClient.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingFromClientInterceptors() ?? [] - ) - } - - public func streamingFromServer( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream { - return self.performAsyncServerStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromServer.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingFromServerInterceptors() ?? [] - ) - } - - public func streamingBothWays( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_SimpleRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingBothWays.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingBothWaysInterceptors() ?? [] - ) - } - - public func streamingBothWays( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_SimpleRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingBothWays.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingBothWaysInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Grpc_Testing_BenchmarkServiceAsyncClient: Grpc_Testing_BenchmarkServiceAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -#endif // compiler(>=5.6) - -public protocol Grpc_Testing_BenchmarkServiceClientInterceptorFactoryProtocol: GRPCSendable { - - /// - Returns: Interceptors to use when invoking 'unaryCall'. - func makeUnaryCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'streamingCall'. - func makeStreamingCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'streamingFromClient'. - func makeStreamingFromClientInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'streamingFromServer'. - func makeStreamingFromServerInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'streamingBothWays'. - func makeStreamingBothWaysInterceptors() -> [ClientInterceptor] -} - -public enum Grpc_Testing_BenchmarkServiceClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "BenchmarkService", - fullName: "grpc.testing.BenchmarkService", - methods: [ - Grpc_Testing_BenchmarkServiceClientMetadata.Methods.unaryCall, - Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingCall, - Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromClient, - Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingFromServer, - Grpc_Testing_BenchmarkServiceClientMetadata.Methods.streamingBothWays, - ] - ) - - public enum Methods { - public static let unaryCall = GRPCMethodDescriptor( - name: "UnaryCall", - path: "/grpc.testing.BenchmarkService/UnaryCall", - type: GRPCCallType.unary - ) - - public static let streamingCall = GRPCMethodDescriptor( - name: "StreamingCall", - path: "/grpc.testing.BenchmarkService/StreamingCall", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let streamingFromClient = GRPCMethodDescriptor( - name: "StreamingFromClient", - path: "/grpc.testing.BenchmarkService/StreamingFromClient", - type: GRPCCallType.clientStreaming - ) - - public static let streamingFromServer = GRPCMethodDescriptor( - name: "StreamingFromServer", - path: "/grpc.testing.BenchmarkService/StreamingFromServer", - type: GRPCCallType.serverStreaming - ) - - public static let streamingBothWays = GRPCMethodDescriptor( - name: "StreamingBothWays", - path: "/grpc.testing.BenchmarkService/StreamingBothWays", - type: GRPCCallType.bidirectionalStreaming - ) - } -} - -/// To build a server, implement a class that conforms to this protocol. -public protocol Grpc_Testing_BenchmarkServiceProvider: CallHandlerProvider { - var interceptors: Grpc_Testing_BenchmarkServiceServerInterceptorFactoryProtocol? { get } - - /// One request followed by one response. - /// The server returns the client payload as-is. - func unaryCall(request: Grpc_Testing_SimpleRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - func streamingCall(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - func streamingFromServer(request: Grpc_Testing_SimpleRequest, context: StreamingResponseCallContext) -> EventLoopFuture - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - func streamingBothWays(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Grpc_Testing_BenchmarkServiceProvider { - public var serviceName: Substring { - return Grpc_Testing_BenchmarkServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "UnaryCall": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [], - userFunction: self.unaryCall(request:context:) - ) - - case "StreamingCall": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingCallInterceptors() ?? [], - observerFactory: self.streamingCall(context:) - ) - - case "StreamingFromClient": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingFromClientInterceptors() ?? [], - observerFactory: self.streamingFromClient(context:) - ) - - case "StreamingFromServer": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingFromServerInterceptors() ?? [], - userFunction: self.streamingFromServer(request:context:) - ) - - case "StreamingBothWays": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingBothWaysInterceptors() ?? [], - observerFactory: self.streamingBothWays(context:) - ) - - default: - return nil - } - } -} - -#if compiler(>=5.6) - -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_BenchmarkServiceAsyncProvider: CallHandlerProvider { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_BenchmarkServiceServerInterceptorFactoryProtocol? { get } - - /// One request followed by one response. - /// The server returns the client payload as-is. - @Sendable func unaryCall( - request: Grpc_Testing_SimpleRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - @Sendable func streamingCall( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - @Sendable func streamingFromClient( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - @Sendable func streamingFromServer( - request: Grpc_Testing_SimpleRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - @Sendable func streamingBothWays( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_BenchmarkServiceAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_BenchmarkServiceServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Grpc_Testing_BenchmarkServiceServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Grpc_Testing_BenchmarkServiceServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "UnaryCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [], - wrapping: self.unaryCall(request:context:) - ) - - case "StreamingCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingCallInterceptors() ?? [], - wrapping: self.streamingCall(requestStream:responseStream:context:) - ) - - case "StreamingFromClient": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingFromClientInterceptors() ?? [], - wrapping: self.streamingFromClient(requestStream:context:) - ) - - case "StreamingFromServer": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingFromServerInterceptors() ?? [], - wrapping: self.streamingFromServer(request:responseStream:context:) - ) - - case "StreamingBothWays": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingBothWaysInterceptors() ?? [], - wrapping: self.streamingBothWays(requestStream:responseStream:context:) - ) - - default: - return nil - } - } -} - -#endif // compiler(>=5.6) - -public protocol Grpc_Testing_BenchmarkServiceServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'unaryCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeUnaryCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'streamingCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStreamingCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'streamingFromClient'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStreamingFromClientInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'streamingFromServer'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStreamingFromServerInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'streamingBothWays'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStreamingBothWaysInterceptors() -> [ServerInterceptor] -} - -public enum Grpc_Testing_BenchmarkServiceServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "BenchmarkService", - fullName: "grpc.testing.BenchmarkService", - methods: [ - Grpc_Testing_BenchmarkServiceServerMetadata.Methods.unaryCall, - Grpc_Testing_BenchmarkServiceServerMetadata.Methods.streamingCall, - Grpc_Testing_BenchmarkServiceServerMetadata.Methods.streamingFromClient, - Grpc_Testing_BenchmarkServiceServerMetadata.Methods.streamingFromServer, - Grpc_Testing_BenchmarkServiceServerMetadata.Methods.streamingBothWays, - ] - ) - - public enum Methods { - public static let unaryCall = GRPCMethodDescriptor( - name: "UnaryCall", - path: "/grpc.testing.BenchmarkService/UnaryCall", - type: GRPCCallType.unary - ) - - public static let streamingCall = GRPCMethodDescriptor( - name: "StreamingCall", - path: "/grpc.testing.BenchmarkService/StreamingCall", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let streamingFromClient = GRPCMethodDescriptor( - name: "StreamingFromClient", - path: "/grpc.testing.BenchmarkService/StreamingFromClient", - type: GRPCCallType.clientStreaming - ) - - public static let streamingFromServer = GRPCMethodDescriptor( - name: "StreamingFromServer", - path: "/grpc.testing.BenchmarkService/StreamingFromServer", - type: GRPCCallType.serverStreaming - ) - - public static let streamingBothWays = GRPCMethodDescriptor( - name: "StreamingBothWays", - path: "/grpc.testing.BenchmarkService/StreamingBothWays", - type: GRPCCallType.bidirectionalStreaming - ) - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.pb.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.pb.swift deleted file mode 100644 index f90bc7860..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.pb.swift +++ /dev/null @@ -1,38 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: benchmark_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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. - -/// An integration test service that covers all the method signature permutations -/// of unary/streaming requests/responses. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto index f844bc097..5308209a1 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto @@ -16,7 +16,7 @@ // of unary/streaming requests/responses. syntax = "proto3"; -import "messages.proto"; +import "Model/messages.proto"; package grpc.testing; diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.pb.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.pb.swift deleted file mode 100644 index 3f5b8faba..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.pb.swift +++ /dev/null @@ -1,2381 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: control.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -public enum Grpc_Testing_ClientType: SwiftProtobuf.Enum { - public typealias RawValue = Int - - /// Many languages support a basic distinction between using - /// sync or async client, and this allows the specification - case syncClient // = 0 - case asyncClient // = 1 - - /// used for some language-specific variants - case otherClient // = 2 - case callbackClient // = 3 - case UNRECOGNIZED(Int) - - public init() { - self = .syncClient - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .syncClient - case 1: self = .asyncClient - case 2: self = .otherClient - case 3: self = .callbackClient - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .syncClient: return 0 - case .asyncClient: return 1 - case .otherClient: return 2 - case .callbackClient: return 3 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension Grpc_Testing_ClientType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static var allCases: [Grpc_Testing_ClientType] = [ - .syncClient, - .asyncClient, - .otherClient, - .callbackClient, - ] -} - -#endif // swift(>=4.2) - -public enum Grpc_Testing_ServerType: SwiftProtobuf.Enum { - public typealias RawValue = Int - case syncServer // = 0 - case asyncServer // = 1 - case asyncGenericServer // = 2 - - /// used for some language-specific variants - case otherServer // = 3 - case callbackServer // = 4 - case UNRECOGNIZED(Int) - - public init() { - self = .syncServer - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .syncServer - case 1: self = .asyncServer - case 2: self = .asyncGenericServer - case 3: self = .otherServer - case 4: self = .callbackServer - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .syncServer: return 0 - case .asyncServer: return 1 - case .asyncGenericServer: return 2 - case .otherServer: return 3 - case .callbackServer: return 4 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension Grpc_Testing_ServerType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static var allCases: [Grpc_Testing_ServerType] = [ - .syncServer, - .asyncServer, - .asyncGenericServer, - .otherServer, - .callbackServer, - ] -} - -#endif // swift(>=4.2) - -public enum Grpc_Testing_RpcType: SwiftProtobuf.Enum { - public typealias RawValue = Int - case unary // = 0 - case streaming // = 1 - case streamingFromClient // = 2 - case streamingFromServer // = 3 - case streamingBothWays // = 4 - case UNRECOGNIZED(Int) - - public init() { - self = .unary - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .unary - case 1: self = .streaming - case 2: self = .streamingFromClient - case 3: self = .streamingFromServer - case 4: self = .streamingBothWays - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .unary: return 0 - case .streaming: return 1 - case .streamingFromClient: return 2 - case .streamingFromServer: return 3 - case .streamingBothWays: return 4 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension Grpc_Testing_RpcType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static var allCases: [Grpc_Testing_RpcType] = [ - .unary, - .streaming, - .streamingFromClient, - .streamingFromServer, - .streamingBothWays, - ] -} - -#endif // swift(>=4.2) - -/// Parameters of poisson process distribution, which is a good representation -/// of activity coming in from independent identical stationary sources. -public struct Grpc_Testing_PoissonParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The rate of arrivals (a.k.a. lambda parameter of the exp distribution). - public var offeredLoad: Double = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Once an RPC finishes, immediately start a new one. -/// No configuration parameters needed. -public struct Grpc_Testing_ClosedLoopParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_LoadParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var load: Grpc_Testing_LoadParams.OneOf_Load? = nil - - public var closedLoop: Grpc_Testing_ClosedLoopParams { - get { - if case .closedLoop(let v)? = load {return v} - return Grpc_Testing_ClosedLoopParams() - } - set {load = .closedLoop(newValue)} - } - - public var poisson: Grpc_Testing_PoissonParams { - get { - if case .poisson(let v)? = load {return v} - return Grpc_Testing_PoissonParams() - } - set {load = .poisson(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public enum OneOf_Load: Equatable { - case closedLoop(Grpc_Testing_ClosedLoopParams) - case poisson(Grpc_Testing_PoissonParams) - - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Testing_LoadParams.OneOf_Load, rhs: Grpc_Testing_LoadParams.OneOf_Load) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.closedLoop, .closedLoop): return { - guard case .closedLoop(let l) = lhs, case .closedLoop(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.poisson, .poisson): return { - guard case .poisson(let l) = lhs, case .poisson(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - public init() {} -} - -/// presence of SecurityParams implies use of TLS -public struct Grpc_Testing_SecurityParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var useTestCa: Bool = false - - public var serverHostOverride: String = String() - - public var credType: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_ChannelArg { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var name: String = String() - - public var value: Grpc_Testing_ChannelArg.OneOf_Value? = nil - - public var strValue: String { - get { - if case .strValue(let v)? = value {return v} - return String() - } - set {value = .strValue(newValue)} - } - - public var intValue: Int32 { - get { - if case .intValue(let v)? = value {return v} - return 0 - } - set {value = .intValue(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public enum OneOf_Value: Equatable { - case strValue(String) - case intValue(Int32) - - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Testing_ChannelArg.OneOf_Value, rhs: Grpc_Testing_ChannelArg.OneOf_Value) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.strValue, .strValue): return { - guard case .strValue(let l) = lhs, case .strValue(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.intValue, .intValue): return { - guard case .intValue(let l) = lhs, case .intValue(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - public init() {} -} - -public struct Grpc_Testing_ClientConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// List of targets to connect to. At least one target needs to be specified. - public var serverTargets: [String] { - get {return _storage._serverTargets} - set {_uniqueStorage()._serverTargets = newValue} - } - - public var clientType: Grpc_Testing_ClientType { - get {return _storage._clientType} - set {_uniqueStorage()._clientType = newValue} - } - - public var securityParams: Grpc_Testing_SecurityParams { - get {return _storage._securityParams ?? Grpc_Testing_SecurityParams()} - set {_uniqueStorage()._securityParams = newValue} - } - /// Returns true if `securityParams` has been explicitly set. - public var hasSecurityParams: Bool {return _storage._securityParams != nil} - /// Clears the value of `securityParams`. Subsequent reads from it will return its default value. - public mutating func clearSecurityParams() {_uniqueStorage()._securityParams = nil} - - /// How many concurrent RPCs to start for each channel. - /// For synchronous client, use a separate thread for each outstanding RPC. - public var outstandingRpcsPerChannel: Int32 { - get {return _storage._outstandingRpcsPerChannel} - set {_uniqueStorage()._outstandingRpcsPerChannel = newValue} - } - - /// Number of independent client channels to create. - /// i-th channel will connect to server_target[i % server_targets.size()] - public var clientChannels: Int32 { - get {return _storage._clientChannels} - set {_uniqueStorage()._clientChannels = newValue} - } - - /// Only for async client. Number of threads to use to start/manage RPCs. - public var asyncClientThreads: Int32 { - get {return _storage._asyncClientThreads} - set {_uniqueStorage()._asyncClientThreads = newValue} - } - - public var rpcType: Grpc_Testing_RpcType { - get {return _storage._rpcType} - set {_uniqueStorage()._rpcType = newValue} - } - - /// The requested load for the entire client (aggregated over all the threads). - public var loadParams: Grpc_Testing_LoadParams { - get {return _storage._loadParams ?? Grpc_Testing_LoadParams()} - set {_uniqueStorage()._loadParams = newValue} - } - /// Returns true if `loadParams` has been explicitly set. - public var hasLoadParams: Bool {return _storage._loadParams != nil} - /// Clears the value of `loadParams`. Subsequent reads from it will return its default value. - public mutating func clearLoadParams() {_uniqueStorage()._loadParams = nil} - - public var payloadConfig: Grpc_Testing_PayloadConfig { - get {return _storage._payloadConfig ?? Grpc_Testing_PayloadConfig()} - set {_uniqueStorage()._payloadConfig = newValue} - } - /// Returns true if `payloadConfig` has been explicitly set. - public var hasPayloadConfig: Bool {return _storage._payloadConfig != nil} - /// Clears the value of `payloadConfig`. Subsequent reads from it will return its default value. - public mutating func clearPayloadConfig() {_uniqueStorage()._payloadConfig = nil} - - public var histogramParams: Grpc_Testing_HistogramParams { - get {return _storage._histogramParams ?? Grpc_Testing_HistogramParams()} - set {_uniqueStorage()._histogramParams = newValue} - } - /// Returns true if `histogramParams` has been explicitly set. - public var hasHistogramParams: Bool {return _storage._histogramParams != nil} - /// Clears the value of `histogramParams`. Subsequent reads from it will return its default value. - public mutating func clearHistogramParams() {_uniqueStorage()._histogramParams = nil} - - /// Specify the cores we should run the client on, if desired - public var coreList: [Int32] { - get {return _storage._coreList} - set {_uniqueStorage()._coreList = newValue} - } - - public var coreLimit: Int32 { - get {return _storage._coreLimit} - set {_uniqueStorage()._coreLimit = newValue} - } - - /// If we use an OTHER_CLIENT client_type, this string gives more detail - public var otherClientApi: String { - get {return _storage._otherClientApi} - set {_uniqueStorage()._otherClientApi = newValue} - } - - public var channelArgs: [Grpc_Testing_ChannelArg] { - get {return _storage._channelArgs} - set {_uniqueStorage()._channelArgs = newValue} - } - - /// Number of threads that share each completion queue - public var threadsPerCq: Int32 { - get {return _storage._threadsPerCq} - set {_uniqueStorage()._threadsPerCq = newValue} - } - - /// Number of messages on a stream before it gets finished/restarted - public var messagesPerStream: Int32 { - get {return _storage._messagesPerStream} - set {_uniqueStorage()._messagesPerStream = newValue} - } - - /// Use coalescing API when possible. - public var useCoalesceApi: Bool { - get {return _storage._useCoalesceApi} - set {_uniqueStorage()._useCoalesceApi = newValue} - } - - /// If 0, disabled. Else, specifies the period between gathering latency - /// medians in milliseconds. - public var medianLatencyCollectionIntervalMillis: Int32 { - get {return _storage._medianLatencyCollectionIntervalMillis} - set {_uniqueStorage()._medianLatencyCollectionIntervalMillis = newValue} - } - - /// Number of client processes. 0 indicates no restriction. - public var clientProcesses: Int32 { - get {return _storage._clientProcesses} - set {_uniqueStorage()._clientProcesses = newValue} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -public struct Grpc_Testing_ClientStatus { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var stats: Grpc_Testing_ClientStats { - get {return _stats ?? Grpc_Testing_ClientStats()} - set {_stats = newValue} - } - /// Returns true if `stats` has been explicitly set. - public var hasStats: Bool {return self._stats != nil} - /// Clears the value of `stats`. Subsequent reads from it will return its default value. - public mutating func clearStats() {self._stats = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _stats: Grpc_Testing_ClientStats? = nil -} - -/// Request current stats -public struct Grpc_Testing_Mark { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// if true, the stats will be reset after taking their snapshot. - public var reset: Bool = false - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_ClientArgs { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var argtype: Grpc_Testing_ClientArgs.OneOf_Argtype? = nil - - public var setup: Grpc_Testing_ClientConfig { - get { - if case .setup(let v)? = argtype {return v} - return Grpc_Testing_ClientConfig() - } - set {argtype = .setup(newValue)} - } - - public var mark: Grpc_Testing_Mark { - get { - if case .mark(let v)? = argtype {return v} - return Grpc_Testing_Mark() - } - set {argtype = .mark(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public enum OneOf_Argtype: Equatable { - case setup(Grpc_Testing_ClientConfig) - case mark(Grpc_Testing_Mark) - - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Testing_ClientArgs.OneOf_Argtype, rhs: Grpc_Testing_ClientArgs.OneOf_Argtype) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.setup, .setup): return { - guard case .setup(let l) = lhs, case .setup(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mark, .mark): return { - guard case .mark(let l) = lhs, case .mark(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - public init() {} -} - -public struct Grpc_Testing_ServerConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var serverType: Grpc_Testing_ServerType = .syncServer - - public var securityParams: Grpc_Testing_SecurityParams { - get {return _securityParams ?? Grpc_Testing_SecurityParams()} - set {_securityParams = newValue} - } - /// Returns true if `securityParams` has been explicitly set. - public var hasSecurityParams: Bool {return self._securityParams != nil} - /// Clears the value of `securityParams`. Subsequent reads from it will return its default value. - public mutating func clearSecurityParams() {self._securityParams = nil} - - /// Port on which to listen. Zero means pick unused port. - public var port: Int32 = 0 - - /// Only for async server. Number of threads used to serve the requests. - public var asyncServerThreads: Int32 = 0 - - /// Specify the number of cores to limit server to, if desired - public var coreLimit: Int32 = 0 - - /// payload config, used in generic server. - /// Note this must NOT be used in proto (non-generic) servers. For proto servers, - /// 'response sizes' must be configured from the 'response_size' field of the - /// 'SimpleRequest' objects in RPC requests. - public var payloadConfig: Grpc_Testing_PayloadConfig { - get {return _payloadConfig ?? Grpc_Testing_PayloadConfig()} - set {_payloadConfig = newValue} - } - /// Returns true if `payloadConfig` has been explicitly set. - public var hasPayloadConfig: Bool {return self._payloadConfig != nil} - /// Clears the value of `payloadConfig`. Subsequent reads from it will return its default value. - public mutating func clearPayloadConfig() {self._payloadConfig = nil} - - /// Specify the cores we should run the server on, if desired - public var coreList: [Int32] = [] - - /// If we use an OTHER_SERVER client_type, this string gives more detail - public var otherServerApi: String = String() - - /// Number of threads that share each completion queue - public var threadsPerCq: Int32 = 0 - - /// Buffer pool size (no buffer pool specified if unset) - public var resourceQuotaSize: Int32 = 0 - - public var channelArgs: [Grpc_Testing_ChannelArg] = [] - - /// Number of server processes. 0 indicates no restriction. - public var serverProcesses: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _securityParams: Grpc_Testing_SecurityParams? = nil - fileprivate var _payloadConfig: Grpc_Testing_PayloadConfig? = nil -} - -public struct Grpc_Testing_ServerArgs { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var argtype: Grpc_Testing_ServerArgs.OneOf_Argtype? = nil - - public var setup: Grpc_Testing_ServerConfig { - get { - if case .setup(let v)? = argtype {return v} - return Grpc_Testing_ServerConfig() - } - set {argtype = .setup(newValue)} - } - - public var mark: Grpc_Testing_Mark { - get { - if case .mark(let v)? = argtype {return v} - return Grpc_Testing_Mark() - } - set {argtype = .mark(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public enum OneOf_Argtype: Equatable { - case setup(Grpc_Testing_ServerConfig) - case mark(Grpc_Testing_Mark) - - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Testing_ServerArgs.OneOf_Argtype, rhs: Grpc_Testing_ServerArgs.OneOf_Argtype) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.setup, .setup): return { - guard case .setup(let l) = lhs, case .setup(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mark, .mark): return { - guard case .mark(let l) = lhs, case .mark(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - public init() {} -} - -public struct Grpc_Testing_ServerStatus { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var stats: Grpc_Testing_ServerStats { - get {return _stats ?? Grpc_Testing_ServerStats()} - set {_stats = newValue} - } - /// Returns true if `stats` has been explicitly set. - public var hasStats: Bool {return self._stats != nil} - /// Clears the value of `stats`. Subsequent reads from it will return its default value. - public mutating func clearStats() {self._stats = nil} - - /// the port bound by the server - public var port: Int32 = 0 - - /// Number of cores available to the server - public var cores: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _stats: Grpc_Testing_ServerStats? = nil -} - -public struct Grpc_Testing_CoreRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_CoreResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Number of cores available on the server - public var cores: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_Void { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A single performance scenario: input to qps_json_driver -public struct Grpc_Testing_Scenario { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Human readable name for this scenario - public var name: String { - get {return _storage._name} - set {_uniqueStorage()._name = newValue} - } - - /// Client configuration - public var clientConfig: Grpc_Testing_ClientConfig { - get {return _storage._clientConfig ?? Grpc_Testing_ClientConfig()} - set {_uniqueStorage()._clientConfig = newValue} - } - /// Returns true if `clientConfig` has been explicitly set. - public var hasClientConfig: Bool {return _storage._clientConfig != nil} - /// Clears the value of `clientConfig`. Subsequent reads from it will return its default value. - public mutating func clearClientConfig() {_uniqueStorage()._clientConfig = nil} - - /// Number of clients to start for the test - public var numClients: Int32 { - get {return _storage._numClients} - set {_uniqueStorage()._numClients = newValue} - } - - /// Server configuration - public var serverConfig: Grpc_Testing_ServerConfig { - get {return _storage._serverConfig ?? Grpc_Testing_ServerConfig()} - set {_uniqueStorage()._serverConfig = newValue} - } - /// Returns true if `serverConfig` has been explicitly set. - public var hasServerConfig: Bool {return _storage._serverConfig != nil} - /// Clears the value of `serverConfig`. Subsequent reads from it will return its default value. - public mutating func clearServerConfig() {_uniqueStorage()._serverConfig = nil} - - /// Number of servers to start for the test - public var numServers: Int32 { - get {return _storage._numServers} - set {_uniqueStorage()._numServers = newValue} - } - - /// Warmup period, in seconds - public var warmupSeconds: Int32 { - get {return _storage._warmupSeconds} - set {_uniqueStorage()._warmupSeconds = newValue} - } - - /// Benchmark time, in seconds - public var benchmarkSeconds: Int32 { - get {return _storage._benchmarkSeconds} - set {_uniqueStorage()._benchmarkSeconds = newValue} - } - - /// Number of workers to spawn locally (usually zero) - public var spawnLocalWorkerCount: Int32 { - get {return _storage._spawnLocalWorkerCount} - set {_uniqueStorage()._spawnLocalWorkerCount = newValue} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -/// A set of scenarios to be run with qps_json_driver -public struct Grpc_Testing_Scenarios { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var scenarios: [Grpc_Testing_Scenario] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Basic summary that can be computed from ClientStats and ServerStats -/// once the scenario has finished. -public struct Grpc_Testing_ScenarioResultSummary { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Total number of operations per second over all clients. What is counted as 1 'operation' depends on the benchmark scenarios: - /// For unary benchmarks, an operation is processing of a single unary RPC. - /// For streaming benchmarks, an operation is processing of a single ping pong of request and response. - public var qps: Double { - get {return _storage._qps} - set {_uniqueStorage()._qps = newValue} - } - - /// QPS per server core. - public var qpsPerServerCore: Double { - get {return _storage._qpsPerServerCore} - set {_uniqueStorage()._qpsPerServerCore = newValue} - } - - /// The total server cpu load based on system time across all server processes, expressed as percentage of a single cpu core. - /// For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server - /// processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. - /// Same explanation for the total client cpu load below. - public var serverSystemTime: Double { - get {return _storage._serverSystemTime} - set {_uniqueStorage()._serverSystemTime = newValue} - } - - /// The total server cpu load based on user time across all server processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - public var serverUserTime: Double { - get {return _storage._serverUserTime} - set {_uniqueStorage()._serverUserTime = newValue} - } - - /// The total client cpu load based on system time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - public var clientSystemTime: Double { - get {return _storage._clientSystemTime} - set {_uniqueStorage()._clientSystemTime = newValue} - } - - /// The total client cpu load based on user time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - public var clientUserTime: Double { - get {return _storage._clientUserTime} - set {_uniqueStorage()._clientUserTime = newValue} - } - - /// X% latency percentiles (in nanoseconds) - public var latency50: Double { - get {return _storage._latency50} - set {_uniqueStorage()._latency50 = newValue} - } - - public var latency90: Double { - get {return _storage._latency90} - set {_uniqueStorage()._latency90 = newValue} - } - - public var latency95: Double { - get {return _storage._latency95} - set {_uniqueStorage()._latency95 = newValue} - } - - public var latency99: Double { - get {return _storage._latency99} - set {_uniqueStorage()._latency99 = newValue} - } - - public var latency999: Double { - get {return _storage._latency999} - set {_uniqueStorage()._latency999 = newValue} - } - - /// server cpu usage percentage - public var serverCpuUsage: Double { - get {return _storage._serverCpuUsage} - set {_uniqueStorage()._serverCpuUsage = newValue} - } - - /// Number of requests that succeeded/failed - public var successfulRequestsPerSecond: Double { - get {return _storage._successfulRequestsPerSecond} - set {_uniqueStorage()._successfulRequestsPerSecond = newValue} - } - - public var failedRequestsPerSecond: Double { - get {return _storage._failedRequestsPerSecond} - set {_uniqueStorage()._failedRequestsPerSecond = newValue} - } - - /// Number of polls called inside completion queue per request - public var clientPollsPerRequest: Double { - get {return _storage._clientPollsPerRequest} - set {_uniqueStorage()._clientPollsPerRequest = newValue} - } - - public var serverPollsPerRequest: Double { - get {return _storage._serverPollsPerRequest} - set {_uniqueStorage()._serverPollsPerRequest = newValue} - } - - /// Queries per CPU-sec over all servers or clients - public var serverQueriesPerCpuSec: Double { - get {return _storage._serverQueriesPerCpuSec} - set {_uniqueStorage()._serverQueriesPerCpuSec = newValue} - } - - public var clientQueriesPerCpuSec: Double { - get {return _storage._clientQueriesPerCpuSec} - set {_uniqueStorage()._clientQueriesPerCpuSec = newValue} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -/// Results of a single benchmark scenario. -public struct Grpc_Testing_ScenarioResult { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Inputs used to run the scenario. - public var scenario: Grpc_Testing_Scenario { - get {return _scenario ?? Grpc_Testing_Scenario()} - set {_scenario = newValue} - } - /// Returns true if `scenario` has been explicitly set. - public var hasScenario: Bool {return self._scenario != nil} - /// Clears the value of `scenario`. Subsequent reads from it will return its default value. - public mutating func clearScenario() {self._scenario = nil} - - /// Histograms from all clients merged into one histogram. - public var latencies: Grpc_Testing_HistogramData { - get {return _latencies ?? Grpc_Testing_HistogramData()} - set {_latencies = newValue} - } - /// Returns true if `latencies` has been explicitly set. - public var hasLatencies: Bool {return self._latencies != nil} - /// Clears the value of `latencies`. Subsequent reads from it will return its default value. - public mutating func clearLatencies() {self._latencies = nil} - - /// Client stats for each client - public var clientStats: [Grpc_Testing_ClientStats] = [] - - /// Server stats for each server - public var serverStats: [Grpc_Testing_ServerStats] = [] - - /// Number of cores available to each server - public var serverCores: [Int32] = [] - - /// An after-the-fact computed summary - public var summary: Grpc_Testing_ScenarioResultSummary { - get {return _summary ?? Grpc_Testing_ScenarioResultSummary()} - set {_summary = newValue} - } - /// Returns true if `summary` has been explicitly set. - public var hasSummary: Bool {return self._summary != nil} - /// Clears the value of `summary`. Subsequent reads from it will return its default value. - public mutating func clearSummary() {self._summary = nil} - - /// Information on success or failure of each worker - public var clientSuccess: [Bool] = [] - - public var serverSuccess: [Bool] = [] - - /// Number of failed requests (one row per status code seen) - public var requestResults: [Grpc_Testing_RequestResultCount] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _scenario: Grpc_Testing_Scenario? = nil - fileprivate var _latencies: Grpc_Testing_HistogramData? = nil - fileprivate var _summary: Grpc_Testing_ScenarioResultSummary? = nil -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_ClientType: @unchecked Sendable {} -extension Grpc_Testing_ServerType: @unchecked Sendable {} -extension Grpc_Testing_RpcType: @unchecked Sendable {} -extension Grpc_Testing_PoissonParams: @unchecked Sendable {} -extension Grpc_Testing_ClosedLoopParams: @unchecked Sendable {} -extension Grpc_Testing_LoadParams: @unchecked Sendable {} -extension Grpc_Testing_LoadParams.OneOf_Load: @unchecked Sendable {} -extension Grpc_Testing_SecurityParams: @unchecked Sendable {} -extension Grpc_Testing_ChannelArg: @unchecked Sendable {} -extension Grpc_Testing_ChannelArg.OneOf_Value: @unchecked Sendable {} -extension Grpc_Testing_ClientConfig: @unchecked Sendable {} -extension Grpc_Testing_ClientStatus: @unchecked Sendable {} -extension Grpc_Testing_Mark: @unchecked Sendable {} -extension Grpc_Testing_ClientArgs: @unchecked Sendable {} -extension Grpc_Testing_ClientArgs.OneOf_Argtype: @unchecked Sendable {} -extension Grpc_Testing_ServerConfig: @unchecked Sendable {} -extension Grpc_Testing_ServerArgs: @unchecked Sendable {} -extension Grpc_Testing_ServerArgs.OneOf_Argtype: @unchecked Sendable {} -extension Grpc_Testing_ServerStatus: @unchecked Sendable {} -extension Grpc_Testing_CoreRequest: @unchecked Sendable {} -extension Grpc_Testing_CoreResponse: @unchecked Sendable {} -extension Grpc_Testing_Void: @unchecked Sendable {} -extension Grpc_Testing_Scenario: @unchecked Sendable {} -extension Grpc_Testing_Scenarios: @unchecked Sendable {} -extension Grpc_Testing_ScenarioResultSummary: @unchecked Sendable {} -extension Grpc_Testing_ScenarioResult: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_ClientType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "SYNC_CLIENT"), - 1: .same(proto: "ASYNC_CLIENT"), - 2: .same(proto: "OTHER_CLIENT"), - 3: .same(proto: "CALLBACK_CLIENT"), - ] -} - -extension Grpc_Testing_ServerType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "SYNC_SERVER"), - 1: .same(proto: "ASYNC_SERVER"), - 2: .same(proto: "ASYNC_GENERIC_SERVER"), - 3: .same(proto: "OTHER_SERVER"), - 4: .same(proto: "CALLBACK_SERVER"), - ] -} - -extension Grpc_Testing_RpcType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNARY"), - 1: .same(proto: "STREAMING"), - 2: .same(proto: "STREAMING_FROM_CLIENT"), - 3: .same(proto: "STREAMING_FROM_SERVER"), - 4: .same(proto: "STREAMING_BOTH_WAYS"), - ] -} - -extension Grpc_Testing_PoissonParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".PoissonParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "offered_load"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.offeredLoad) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.offeredLoad != 0 { - try visitor.visitSingularDoubleField(value: self.offeredLoad, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_PoissonParams, rhs: Grpc_Testing_PoissonParams) -> Bool { - if lhs.offeredLoad != rhs.offeredLoad {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClosedLoopParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ClosedLoopParams" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - public func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ClosedLoopParams, rhs: Grpc_Testing_ClosedLoopParams) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".LoadParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "closed_loop"), - 2: .same(proto: "poisson"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ClosedLoopParams? - var hadOneofValue = false - if let current = self.load { - hadOneofValue = true - if case .closedLoop(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.load = .closedLoop(v) - } - }() - case 2: try { - var v: Grpc_Testing_PoissonParams? - var hadOneofValue = false - if let current = self.load { - hadOneofValue = true - if case .poisson(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.load = .poisson(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.load { - case .closedLoop?: try { - guard case .closedLoop(let v)? = self.load else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .poisson?: try { - guard case .poisson(let v)? = self.load else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_LoadParams, rhs: Grpc_Testing_LoadParams) -> Bool { - if lhs.load != rhs.load {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SecurityParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SecurityParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "use_test_ca"), - 2: .standard(proto: "server_host_override"), - 3: .standard(proto: "cred_type"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.useTestCa) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.serverHostOverride) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.credType) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.useTestCa != false { - try visitor.visitSingularBoolField(value: self.useTestCa, fieldNumber: 1) - } - if !self.serverHostOverride.isEmpty { - try visitor.visitSingularStringField(value: self.serverHostOverride, fieldNumber: 2) - } - if !self.credType.isEmpty { - try visitor.visitSingularStringField(value: self.credType, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SecurityParams, rhs: Grpc_Testing_SecurityParams) -> Bool { - if lhs.useTestCa != rhs.useTestCa {return false} - if lhs.serverHostOverride != rhs.serverHostOverride {return false} - if lhs.credType != rhs.credType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ChannelArg: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ChannelArg" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .standard(proto: "str_value"), - 3: .standard(proto: "int_value"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 2: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.value != nil {try decoder.handleConflictingOneOf()} - self.value = .strValue(v) - } - }() - case 3: try { - var v: Int32? - try decoder.decodeSingularInt32Field(value: &v) - if let v = v { - if self.value != nil {try decoder.handleConflictingOneOf()} - self.value = .intValue(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - switch self.value { - case .strValue?: try { - guard case .strValue(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - }() - case .intValue?: try { - guard case .intValue(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularInt32Field(value: v, fieldNumber: 3) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ChannelArg, rhs: Grpc_Testing_ChannelArg) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ClientConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "server_targets"), - 2: .standard(proto: "client_type"), - 3: .standard(proto: "security_params"), - 4: .standard(proto: "outstanding_rpcs_per_channel"), - 5: .standard(proto: "client_channels"), - 7: .standard(proto: "async_client_threads"), - 8: .standard(proto: "rpc_type"), - 10: .standard(proto: "load_params"), - 11: .standard(proto: "payload_config"), - 12: .standard(proto: "histogram_params"), - 13: .standard(proto: "core_list"), - 14: .standard(proto: "core_limit"), - 15: .standard(proto: "other_client_api"), - 16: .standard(proto: "channel_args"), - 17: .standard(proto: "threads_per_cq"), - 18: .standard(proto: "messages_per_stream"), - 19: .standard(proto: "use_coalesce_api"), - 20: .standard(proto: "median_latency_collection_interval_millis"), - 21: .standard(proto: "client_processes"), - ] - - fileprivate class _StorageClass { - var _serverTargets: [String] = [] - var _clientType: Grpc_Testing_ClientType = .syncClient - var _securityParams: Grpc_Testing_SecurityParams? = nil - var _outstandingRpcsPerChannel: Int32 = 0 - var _clientChannels: Int32 = 0 - var _asyncClientThreads: Int32 = 0 - var _rpcType: Grpc_Testing_RpcType = .unary - var _loadParams: Grpc_Testing_LoadParams? = nil - var _payloadConfig: Grpc_Testing_PayloadConfig? = nil - var _histogramParams: Grpc_Testing_HistogramParams? = nil - var _coreList: [Int32] = [] - var _coreLimit: Int32 = 0 - var _otherClientApi: String = String() - var _channelArgs: [Grpc_Testing_ChannelArg] = [] - var _threadsPerCq: Int32 = 0 - var _messagesPerStream: Int32 = 0 - var _useCoalesceApi: Bool = false - var _medianLatencyCollectionIntervalMillis: Int32 = 0 - var _clientProcesses: Int32 = 0 - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _serverTargets = source._serverTargets - _clientType = source._clientType - _securityParams = source._securityParams - _outstandingRpcsPerChannel = source._outstandingRpcsPerChannel - _clientChannels = source._clientChannels - _asyncClientThreads = source._asyncClientThreads - _rpcType = source._rpcType - _loadParams = source._loadParams - _payloadConfig = source._payloadConfig - _histogramParams = source._histogramParams - _coreList = source._coreList - _coreLimit = source._coreLimit - _otherClientApi = source._otherClientApi - _channelArgs = source._channelArgs - _threadsPerCq = source._threadsPerCq - _messagesPerStream = source._messagesPerStream - _useCoalesceApi = source._useCoalesceApi - _medianLatencyCollectionIntervalMillis = source._medianLatencyCollectionIntervalMillis - _clientProcesses = source._clientProcesses - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - public mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedStringField(value: &_storage._serverTargets) }() - case 2: try { try decoder.decodeSingularEnumField(value: &_storage._clientType) }() - case 3: try { try decoder.decodeSingularMessageField(value: &_storage._securityParams) }() - case 4: try { try decoder.decodeSingularInt32Field(value: &_storage._outstandingRpcsPerChannel) }() - case 5: try { try decoder.decodeSingularInt32Field(value: &_storage._clientChannels) }() - case 7: try { try decoder.decodeSingularInt32Field(value: &_storage._asyncClientThreads) }() - case 8: try { try decoder.decodeSingularEnumField(value: &_storage._rpcType) }() - case 10: try { try decoder.decodeSingularMessageField(value: &_storage._loadParams) }() - case 11: try { try decoder.decodeSingularMessageField(value: &_storage._payloadConfig) }() - case 12: try { try decoder.decodeSingularMessageField(value: &_storage._histogramParams) }() - case 13: try { try decoder.decodeRepeatedInt32Field(value: &_storage._coreList) }() - case 14: try { try decoder.decodeSingularInt32Field(value: &_storage._coreLimit) }() - case 15: try { try decoder.decodeSingularStringField(value: &_storage._otherClientApi) }() - case 16: try { try decoder.decodeRepeatedMessageField(value: &_storage._channelArgs) }() - case 17: try { try decoder.decodeSingularInt32Field(value: &_storage._threadsPerCq) }() - case 18: try { try decoder.decodeSingularInt32Field(value: &_storage._messagesPerStream) }() - case 19: try { try decoder.decodeSingularBoolField(value: &_storage._useCoalesceApi) }() - case 20: try { try decoder.decodeSingularInt32Field(value: &_storage._medianLatencyCollectionIntervalMillis) }() - case 21: try { try decoder.decodeSingularInt32Field(value: &_storage._clientProcesses) }() - default: break - } - } - } - } - - public func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !_storage._serverTargets.isEmpty { - try visitor.visitRepeatedStringField(value: _storage._serverTargets, fieldNumber: 1) - } - if _storage._clientType != .syncClient { - try visitor.visitSingularEnumField(value: _storage._clientType, fieldNumber: 2) - } - try { if let v = _storage._securityParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if _storage._outstandingRpcsPerChannel != 0 { - try visitor.visitSingularInt32Field(value: _storage._outstandingRpcsPerChannel, fieldNumber: 4) - } - if _storage._clientChannels != 0 { - try visitor.visitSingularInt32Field(value: _storage._clientChannels, fieldNumber: 5) - } - if _storage._asyncClientThreads != 0 { - try visitor.visitSingularInt32Field(value: _storage._asyncClientThreads, fieldNumber: 7) - } - if _storage._rpcType != .unary { - try visitor.visitSingularEnumField(value: _storage._rpcType, fieldNumber: 8) - } - try { if let v = _storage._loadParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) - } }() - try { if let v = _storage._payloadConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - } }() - try { if let v = _storage._histogramParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 12) - } }() - if !_storage._coreList.isEmpty { - try visitor.visitPackedInt32Field(value: _storage._coreList, fieldNumber: 13) - } - if _storage._coreLimit != 0 { - try visitor.visitSingularInt32Field(value: _storage._coreLimit, fieldNumber: 14) - } - if !_storage._otherClientApi.isEmpty { - try visitor.visitSingularStringField(value: _storage._otherClientApi, fieldNumber: 15) - } - if !_storage._channelArgs.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._channelArgs, fieldNumber: 16) - } - if _storage._threadsPerCq != 0 { - try visitor.visitSingularInt32Field(value: _storage._threadsPerCq, fieldNumber: 17) - } - if _storage._messagesPerStream != 0 { - try visitor.visitSingularInt32Field(value: _storage._messagesPerStream, fieldNumber: 18) - } - if _storage._useCoalesceApi != false { - try visitor.visitSingularBoolField(value: _storage._useCoalesceApi, fieldNumber: 19) - } - if _storage._medianLatencyCollectionIntervalMillis != 0 { - try visitor.visitSingularInt32Field(value: _storage._medianLatencyCollectionIntervalMillis, fieldNumber: 20) - } - if _storage._clientProcesses != 0 { - try visitor.visitSingularInt32Field(value: _storage._clientProcesses, fieldNumber: 21) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ClientConfig, rhs: Grpc_Testing_ClientConfig) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._serverTargets != rhs_storage._serverTargets {return false} - if _storage._clientType != rhs_storage._clientType {return false} - if _storage._securityParams != rhs_storage._securityParams {return false} - if _storage._outstandingRpcsPerChannel != rhs_storage._outstandingRpcsPerChannel {return false} - if _storage._clientChannels != rhs_storage._clientChannels {return false} - if _storage._asyncClientThreads != rhs_storage._asyncClientThreads {return false} - if _storage._rpcType != rhs_storage._rpcType {return false} - if _storage._loadParams != rhs_storage._loadParams {return false} - if _storage._payloadConfig != rhs_storage._payloadConfig {return false} - if _storage._histogramParams != rhs_storage._histogramParams {return false} - if _storage._coreList != rhs_storage._coreList {return false} - if _storage._coreLimit != rhs_storage._coreLimit {return false} - if _storage._otherClientApi != rhs_storage._otherClientApi {return false} - if _storage._channelArgs != rhs_storage._channelArgs {return false} - if _storage._threadsPerCq != rhs_storage._threadsPerCq {return false} - if _storage._messagesPerStream != rhs_storage._messagesPerStream {return false} - if _storage._useCoalesceApi != rhs_storage._useCoalesceApi {return false} - if _storage._medianLatencyCollectionIntervalMillis != rhs_storage._medianLatencyCollectionIntervalMillis {return false} - if _storage._clientProcesses != rhs_storage._clientProcesses {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ClientStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "stats"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._stats) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._stats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ClientStatus, rhs: Grpc_Testing_ClientStatus) -> Bool { - if lhs._stats != rhs._stats {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Mark: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Mark" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "reset"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.reset) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.reset != false { - try visitor.visitSingularBoolField(value: self.reset, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Mark, rhs: Grpc_Testing_Mark) -> Bool { - if lhs.reset != rhs.reset {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientArgs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ClientArgs" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "setup"), - 2: .same(proto: "mark"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ClientConfig? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .setup(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .setup(v) - } - }() - case 2: try { - var v: Grpc_Testing_Mark? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .mark(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .mark(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.argtype { - case .setup?: try { - guard case .setup(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .mark?: try { - guard case .mark(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ClientArgs, rhs: Grpc_Testing_ClientArgs) -> Bool { - if lhs.argtype != rhs.argtype {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ServerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "server_type"), - 2: .standard(proto: "security_params"), - 4: .same(proto: "port"), - 7: .standard(proto: "async_server_threads"), - 8: .standard(proto: "core_limit"), - 9: .standard(proto: "payload_config"), - 10: .standard(proto: "core_list"), - 11: .standard(proto: "other_server_api"), - 12: .standard(proto: "threads_per_cq"), - 1001: .standard(proto: "resource_quota_size"), - 1002: .standard(proto: "channel_args"), - 21: .standard(proto: "server_processes"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.serverType) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._securityParams) }() - case 4: try { try decoder.decodeSingularInt32Field(value: &self.port) }() - case 7: try { try decoder.decodeSingularInt32Field(value: &self.asyncServerThreads) }() - case 8: try { try decoder.decodeSingularInt32Field(value: &self.coreLimit) }() - case 9: try { try decoder.decodeSingularMessageField(value: &self._payloadConfig) }() - case 10: try { try decoder.decodeRepeatedInt32Field(value: &self.coreList) }() - case 11: try { try decoder.decodeSingularStringField(value: &self.otherServerApi) }() - case 12: try { try decoder.decodeSingularInt32Field(value: &self.threadsPerCq) }() - case 21: try { try decoder.decodeSingularInt32Field(value: &self.serverProcesses) }() - case 1001: try { try decoder.decodeSingularInt32Field(value: &self.resourceQuotaSize) }() - case 1002: try { try decoder.decodeRepeatedMessageField(value: &self.channelArgs) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.serverType != .syncServer { - try visitor.visitSingularEnumField(value: self.serverType, fieldNumber: 1) - } - try { if let v = self._securityParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if self.port != 0 { - try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 4) - } - if self.asyncServerThreads != 0 { - try visitor.visitSingularInt32Field(value: self.asyncServerThreads, fieldNumber: 7) - } - if self.coreLimit != 0 { - try visitor.visitSingularInt32Field(value: self.coreLimit, fieldNumber: 8) - } - try { if let v = self._payloadConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 9) - } }() - if !self.coreList.isEmpty { - try visitor.visitPackedInt32Field(value: self.coreList, fieldNumber: 10) - } - if !self.otherServerApi.isEmpty { - try visitor.visitSingularStringField(value: self.otherServerApi, fieldNumber: 11) - } - if self.threadsPerCq != 0 { - try visitor.visitSingularInt32Field(value: self.threadsPerCq, fieldNumber: 12) - } - if self.serverProcesses != 0 { - try visitor.visitSingularInt32Field(value: self.serverProcesses, fieldNumber: 21) - } - if self.resourceQuotaSize != 0 { - try visitor.visitSingularInt32Field(value: self.resourceQuotaSize, fieldNumber: 1001) - } - if !self.channelArgs.isEmpty { - try visitor.visitRepeatedMessageField(value: self.channelArgs, fieldNumber: 1002) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ServerConfig, rhs: Grpc_Testing_ServerConfig) -> Bool { - if lhs.serverType != rhs.serverType {return false} - if lhs._securityParams != rhs._securityParams {return false} - if lhs.port != rhs.port {return false} - if lhs.asyncServerThreads != rhs.asyncServerThreads {return false} - if lhs.coreLimit != rhs.coreLimit {return false} - if lhs._payloadConfig != rhs._payloadConfig {return false} - if lhs.coreList != rhs.coreList {return false} - if lhs.otherServerApi != rhs.otherServerApi {return false} - if lhs.threadsPerCq != rhs.threadsPerCq {return false} - if lhs.resourceQuotaSize != rhs.resourceQuotaSize {return false} - if lhs.channelArgs != rhs.channelArgs {return false} - if lhs.serverProcesses != rhs.serverProcesses {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ServerArgs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerArgs" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "setup"), - 2: .same(proto: "mark"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ServerConfig? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .setup(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .setup(v) - } - }() - case 2: try { - var v: Grpc_Testing_Mark? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .mark(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .mark(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.argtype { - case .setup?: try { - guard case .setup(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .mark?: try { - guard case .mark(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ServerArgs, rhs: Grpc_Testing_ServerArgs) -> Bool { - if lhs.argtype != rhs.argtype {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ServerStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "stats"), - 2: .same(proto: "port"), - 3: .same(proto: "cores"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._stats) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.port) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &self.cores) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._stats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.port != 0 { - try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 2) - } - if self.cores != 0 { - try visitor.visitSingularInt32Field(value: self.cores, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ServerStatus, rhs: Grpc_Testing_ServerStatus) -> Bool { - if lhs._stats != rhs._stats {return false} - if lhs.port != rhs.port {return false} - if lhs.cores != rhs.cores {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_CoreRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".CoreRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - public func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_CoreRequest, rhs: Grpc_Testing_CoreRequest) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_CoreResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".CoreResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "cores"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.cores) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.cores != 0 { - try visitor.visitSingularInt32Field(value: self.cores, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_CoreResponse, rhs: Grpc_Testing_CoreResponse) -> Bool { - if lhs.cores != rhs.cores {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Void: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Void" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - public func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Void, rhs: Grpc_Testing_Void) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Scenario: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Scenario" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .standard(proto: "client_config"), - 3: .standard(proto: "num_clients"), - 4: .standard(proto: "server_config"), - 5: .standard(proto: "num_servers"), - 6: .standard(proto: "warmup_seconds"), - 7: .standard(proto: "benchmark_seconds"), - 8: .standard(proto: "spawn_local_worker_count"), - ] - - fileprivate class _StorageClass { - var _name: String = String() - var _clientConfig: Grpc_Testing_ClientConfig? = nil - var _numClients: Int32 = 0 - var _serverConfig: Grpc_Testing_ServerConfig? = nil - var _numServers: Int32 = 0 - var _warmupSeconds: Int32 = 0 - var _benchmarkSeconds: Int32 = 0 - var _spawnLocalWorkerCount: Int32 = 0 - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _name = source._name - _clientConfig = source._clientConfig - _numClients = source._numClients - _serverConfig = source._serverConfig - _numServers = source._numServers - _warmupSeconds = source._warmupSeconds - _benchmarkSeconds = source._benchmarkSeconds - _spawnLocalWorkerCount = source._spawnLocalWorkerCount - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - public mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &_storage._name) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._clientConfig) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &_storage._numClients) }() - case 4: try { try decoder.decodeSingularMessageField(value: &_storage._serverConfig) }() - case 5: try { try decoder.decodeSingularInt32Field(value: &_storage._numServers) }() - case 6: try { try decoder.decodeSingularInt32Field(value: &_storage._warmupSeconds) }() - case 7: try { try decoder.decodeSingularInt32Field(value: &_storage._benchmarkSeconds) }() - case 8: try { try decoder.decodeSingularInt32Field(value: &_storage._spawnLocalWorkerCount) }() - default: break - } - } - } - } - - public func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !_storage._name.isEmpty { - try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 1) - } - try { if let v = _storage._clientConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if _storage._numClients != 0 { - try visitor.visitSingularInt32Field(value: _storage._numClients, fieldNumber: 3) - } - try { if let v = _storage._serverConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - if _storage._numServers != 0 { - try visitor.visitSingularInt32Field(value: _storage._numServers, fieldNumber: 5) - } - if _storage._warmupSeconds != 0 { - try visitor.visitSingularInt32Field(value: _storage._warmupSeconds, fieldNumber: 6) - } - if _storage._benchmarkSeconds != 0 { - try visitor.visitSingularInt32Field(value: _storage._benchmarkSeconds, fieldNumber: 7) - } - if _storage._spawnLocalWorkerCount != 0 { - try visitor.visitSingularInt32Field(value: _storage._spawnLocalWorkerCount, fieldNumber: 8) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Scenario, rhs: Grpc_Testing_Scenario) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._name != rhs_storage._name {return false} - if _storage._clientConfig != rhs_storage._clientConfig {return false} - if _storage._numClients != rhs_storage._numClients {return false} - if _storage._serverConfig != rhs_storage._serverConfig {return false} - if _storage._numServers != rhs_storage._numServers {return false} - if _storage._warmupSeconds != rhs_storage._warmupSeconds {return false} - if _storage._benchmarkSeconds != rhs_storage._benchmarkSeconds {return false} - if _storage._spawnLocalWorkerCount != rhs_storage._spawnLocalWorkerCount {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Scenarios: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Scenarios" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "scenarios"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.scenarios) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.scenarios.isEmpty { - try visitor.visitRepeatedMessageField(value: self.scenarios, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Scenarios, rhs: Grpc_Testing_Scenarios) -> Bool { - if lhs.scenarios != rhs.scenarios {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ScenarioResultSummary: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ScenarioResultSummary" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "qps"), - 2: .standard(proto: "qps_per_server_core"), - 3: .standard(proto: "server_system_time"), - 4: .standard(proto: "server_user_time"), - 5: .standard(proto: "client_system_time"), - 6: .standard(proto: "client_user_time"), - 7: .standard(proto: "latency_50"), - 8: .standard(proto: "latency_90"), - 9: .standard(proto: "latency_95"), - 10: .standard(proto: "latency_99"), - 11: .standard(proto: "latency_999"), - 12: .standard(proto: "server_cpu_usage"), - 13: .standard(proto: "successful_requests_per_second"), - 14: .standard(proto: "failed_requests_per_second"), - 15: .standard(proto: "client_polls_per_request"), - 16: .standard(proto: "server_polls_per_request"), - 17: .standard(proto: "server_queries_per_cpu_sec"), - 18: .standard(proto: "client_queries_per_cpu_sec"), - ] - - fileprivate class _StorageClass { - var _qps: Double = 0 - var _qpsPerServerCore: Double = 0 - var _serverSystemTime: Double = 0 - var _serverUserTime: Double = 0 - var _clientSystemTime: Double = 0 - var _clientUserTime: Double = 0 - var _latency50: Double = 0 - var _latency90: Double = 0 - var _latency95: Double = 0 - var _latency99: Double = 0 - var _latency999: Double = 0 - var _serverCpuUsage: Double = 0 - var _successfulRequestsPerSecond: Double = 0 - var _failedRequestsPerSecond: Double = 0 - var _clientPollsPerRequest: Double = 0 - var _serverPollsPerRequest: Double = 0 - var _serverQueriesPerCpuSec: Double = 0 - var _clientQueriesPerCpuSec: Double = 0 - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _qps = source._qps - _qpsPerServerCore = source._qpsPerServerCore - _serverSystemTime = source._serverSystemTime - _serverUserTime = source._serverUserTime - _clientSystemTime = source._clientSystemTime - _clientUserTime = source._clientUserTime - _latency50 = source._latency50 - _latency90 = source._latency90 - _latency95 = source._latency95 - _latency99 = source._latency99 - _latency999 = source._latency999 - _serverCpuUsage = source._serverCpuUsage - _successfulRequestsPerSecond = source._successfulRequestsPerSecond - _failedRequestsPerSecond = source._failedRequestsPerSecond - _clientPollsPerRequest = source._clientPollsPerRequest - _serverPollsPerRequest = source._serverPollsPerRequest - _serverQueriesPerCpuSec = source._serverQueriesPerCpuSec - _clientQueriesPerCpuSec = source._clientQueriesPerCpuSec - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - public mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &_storage._qps) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &_storage._qpsPerServerCore) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &_storage._serverSystemTime) }() - case 4: try { try decoder.decodeSingularDoubleField(value: &_storage._serverUserTime) }() - case 5: try { try decoder.decodeSingularDoubleField(value: &_storage._clientSystemTime) }() - case 6: try { try decoder.decodeSingularDoubleField(value: &_storage._clientUserTime) }() - case 7: try { try decoder.decodeSingularDoubleField(value: &_storage._latency50) }() - case 8: try { try decoder.decodeSingularDoubleField(value: &_storage._latency90) }() - case 9: try { try decoder.decodeSingularDoubleField(value: &_storage._latency95) }() - case 10: try { try decoder.decodeSingularDoubleField(value: &_storage._latency99) }() - case 11: try { try decoder.decodeSingularDoubleField(value: &_storage._latency999) }() - case 12: try { try decoder.decodeSingularDoubleField(value: &_storage._serverCpuUsage) }() - case 13: try { try decoder.decodeSingularDoubleField(value: &_storage._successfulRequestsPerSecond) }() - case 14: try { try decoder.decodeSingularDoubleField(value: &_storage._failedRequestsPerSecond) }() - case 15: try { try decoder.decodeSingularDoubleField(value: &_storage._clientPollsPerRequest) }() - case 16: try { try decoder.decodeSingularDoubleField(value: &_storage._serverPollsPerRequest) }() - case 17: try { try decoder.decodeSingularDoubleField(value: &_storage._serverQueriesPerCpuSec) }() - case 18: try { try decoder.decodeSingularDoubleField(value: &_storage._clientQueriesPerCpuSec) }() - default: break - } - } - } - } - - public func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if _storage._qps != 0 { - try visitor.visitSingularDoubleField(value: _storage._qps, fieldNumber: 1) - } - if _storage._qpsPerServerCore != 0 { - try visitor.visitSingularDoubleField(value: _storage._qpsPerServerCore, fieldNumber: 2) - } - if _storage._serverSystemTime != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverSystemTime, fieldNumber: 3) - } - if _storage._serverUserTime != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverUserTime, fieldNumber: 4) - } - if _storage._clientSystemTime != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientSystemTime, fieldNumber: 5) - } - if _storage._clientUserTime != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientUserTime, fieldNumber: 6) - } - if _storage._latency50 != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency50, fieldNumber: 7) - } - if _storage._latency90 != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency90, fieldNumber: 8) - } - if _storage._latency95 != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency95, fieldNumber: 9) - } - if _storage._latency99 != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency99, fieldNumber: 10) - } - if _storage._latency999 != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency999, fieldNumber: 11) - } - if _storage._serverCpuUsage != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverCpuUsage, fieldNumber: 12) - } - if _storage._successfulRequestsPerSecond != 0 { - try visitor.visitSingularDoubleField(value: _storage._successfulRequestsPerSecond, fieldNumber: 13) - } - if _storage._failedRequestsPerSecond != 0 { - try visitor.visitSingularDoubleField(value: _storage._failedRequestsPerSecond, fieldNumber: 14) - } - if _storage._clientPollsPerRequest != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientPollsPerRequest, fieldNumber: 15) - } - if _storage._serverPollsPerRequest != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverPollsPerRequest, fieldNumber: 16) - } - if _storage._serverQueriesPerCpuSec != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverQueriesPerCpuSec, fieldNumber: 17) - } - if _storage._clientQueriesPerCpuSec != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientQueriesPerCpuSec, fieldNumber: 18) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ScenarioResultSummary, rhs: Grpc_Testing_ScenarioResultSummary) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._qps != rhs_storage._qps {return false} - if _storage._qpsPerServerCore != rhs_storage._qpsPerServerCore {return false} - if _storage._serverSystemTime != rhs_storage._serverSystemTime {return false} - if _storage._serverUserTime != rhs_storage._serverUserTime {return false} - if _storage._clientSystemTime != rhs_storage._clientSystemTime {return false} - if _storage._clientUserTime != rhs_storage._clientUserTime {return false} - if _storage._latency50 != rhs_storage._latency50 {return false} - if _storage._latency90 != rhs_storage._latency90 {return false} - if _storage._latency95 != rhs_storage._latency95 {return false} - if _storage._latency99 != rhs_storage._latency99 {return false} - if _storage._latency999 != rhs_storage._latency999 {return false} - if _storage._serverCpuUsage != rhs_storage._serverCpuUsage {return false} - if _storage._successfulRequestsPerSecond != rhs_storage._successfulRequestsPerSecond {return false} - if _storage._failedRequestsPerSecond != rhs_storage._failedRequestsPerSecond {return false} - if _storage._clientPollsPerRequest != rhs_storage._clientPollsPerRequest {return false} - if _storage._serverPollsPerRequest != rhs_storage._serverPollsPerRequest {return false} - if _storage._serverQueriesPerCpuSec != rhs_storage._serverQueriesPerCpuSec {return false} - if _storage._clientQueriesPerCpuSec != rhs_storage._clientQueriesPerCpuSec {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ScenarioResult: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ScenarioResult" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "scenario"), - 2: .same(proto: "latencies"), - 3: .standard(proto: "client_stats"), - 4: .standard(proto: "server_stats"), - 5: .standard(proto: "server_cores"), - 6: .same(proto: "summary"), - 7: .standard(proto: "client_success"), - 8: .standard(proto: "server_success"), - 9: .standard(proto: "request_results"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._scenario) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._latencies) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &self.clientStats) }() - case 4: try { try decoder.decodeRepeatedMessageField(value: &self.serverStats) }() - case 5: try { try decoder.decodeRepeatedInt32Field(value: &self.serverCores) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._summary) }() - case 7: try { try decoder.decodeRepeatedBoolField(value: &self.clientSuccess) }() - case 8: try { try decoder.decodeRepeatedBoolField(value: &self.serverSuccess) }() - case 9: try { try decoder.decodeRepeatedMessageField(value: &self.requestResults) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._scenario { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._latencies { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if !self.clientStats.isEmpty { - try visitor.visitRepeatedMessageField(value: self.clientStats, fieldNumber: 3) - } - if !self.serverStats.isEmpty { - try visitor.visitRepeatedMessageField(value: self.serverStats, fieldNumber: 4) - } - if !self.serverCores.isEmpty { - try visitor.visitPackedInt32Field(value: self.serverCores, fieldNumber: 5) - } - try { if let v = self._summary { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - if !self.clientSuccess.isEmpty { - try visitor.visitPackedBoolField(value: self.clientSuccess, fieldNumber: 7) - } - if !self.serverSuccess.isEmpty { - try visitor.visitPackedBoolField(value: self.serverSuccess, fieldNumber: 8) - } - if !self.requestResults.isEmpty { - try visitor.visitRepeatedMessageField(value: self.requestResults, fieldNumber: 9) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ScenarioResult, rhs: Grpc_Testing_ScenarioResult) -> Bool { - if lhs._scenario != rhs._scenario {return false} - if lhs._latencies != rhs._latencies {return false} - if lhs.clientStats != rhs.clientStats {return false} - if lhs.serverStats != rhs.serverStats {return false} - if lhs.serverCores != rhs.serverCores {return false} - if lhs._summary != rhs._summary {return false} - if lhs.clientSuccess != rhs.clientSuccess {return false} - if lhs.serverSuccess != rhs.serverSuccess {return false} - if lhs.requestResults != rhs.requestResults {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto index 6be8c1c89..1522560c9 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto @@ -14,8 +14,8 @@ syntax = "proto3"; -import "payloads.proto"; -import "stats.proto"; +import "Model/payloads.proto"; +import "Model/stats.proto"; package grpc.testing; diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.pb.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.pb.swift deleted file mode 100644 index c978ce273..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.pb.swift +++ /dev/null @@ -1,312 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: core_stats.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2017 gRPC authors. -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -public struct Grpc_Core_Bucket { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var start: Double = 0 - - public var count: UInt64 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Core_Histogram { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var buckets: [Grpc_Core_Bucket] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Core_Metric { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var name: String = String() - - public var value: Grpc_Core_Metric.OneOf_Value? = nil - - public var count: UInt64 { - get { - if case .count(let v)? = value {return v} - return 0 - } - set {value = .count(newValue)} - } - - public var histogram: Grpc_Core_Histogram { - get { - if case .histogram(let v)? = value {return v} - return Grpc_Core_Histogram() - } - set {value = .histogram(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public enum OneOf_Value: Equatable { - case count(UInt64) - case histogram(Grpc_Core_Histogram) - - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Core_Metric.OneOf_Value, rhs: Grpc_Core_Metric.OneOf_Value) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.count, .count): return { - guard case .count(let l) = lhs, case .count(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.histogram, .histogram): return { - guard case .histogram(let l) = lhs, case .histogram(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - public init() {} -} - -public struct Grpc_Core_Stats { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var metrics: [Grpc_Core_Metric] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Core_Bucket: @unchecked Sendable {} -extension Grpc_Core_Histogram: @unchecked Sendable {} -extension Grpc_Core_Metric: @unchecked Sendable {} -extension Grpc_Core_Metric.OneOf_Value: @unchecked Sendable {} -extension Grpc_Core_Stats: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.core" - -extension Grpc_Core_Bucket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Bucket" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "start"), - 2: .same(proto: "count"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.start) }() - case 2: try { try decoder.decodeSingularUInt64Field(value: &self.count) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.start != 0 { - try visitor.visitSingularDoubleField(value: self.start, fieldNumber: 1) - } - if self.count != 0 { - try visitor.visitSingularUInt64Field(value: self.count, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Core_Bucket, rhs: Grpc_Core_Bucket) -> Bool { - if lhs.start != rhs.start {return false} - if lhs.count != rhs.count {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Core_Histogram: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Histogram" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "buckets"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.buckets) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.buckets.isEmpty { - try visitor.visitRepeatedMessageField(value: self.buckets, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Core_Histogram, rhs: Grpc_Core_Histogram) -> Bool { - if lhs.buckets != rhs.buckets {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Core_Metric: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Metric" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 10: .same(proto: "count"), - 11: .same(proto: "histogram"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 10: try { - var v: UInt64? - try decoder.decodeSingularUInt64Field(value: &v) - if let v = v { - if self.value != nil {try decoder.handleConflictingOneOf()} - self.value = .count(v) - } - }() - case 11: try { - var v: Grpc_Core_Histogram? - var hadOneofValue = false - if let current = self.value { - hadOneofValue = true - if case .histogram(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.value = .histogram(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - switch self.value { - case .count?: try { - guard case .count(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularUInt64Field(value: v, fieldNumber: 10) - }() - case .histogram?: try { - guard case .histogram(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Core_Metric, rhs: Grpc_Core_Metric) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Core_Stats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Stats" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "metrics"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.metrics) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.metrics.isEmpty { - try visitor.visitRepeatedMessageField(value: self.metrics, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Core_Stats, rhs: Grpc_Core_Stats) -> Bool { - if lhs.metrics != rhs.metrics {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.pb.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.pb.swift deleted file mode 100644 index f5a10d334..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.pb.swift +++ /dev/null @@ -1,1225 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: messages.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The type of payload that should be returned. -public enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { - public typealias RawValue = Int - - /// Compressable text format. - case compressable // = 0 - case UNRECOGNIZED(Int) - - public init() { - self = .compressable - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .compressable - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .compressable: return 0 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension Grpc_Testing_PayloadType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static var allCases: [Grpc_Testing_PayloadType] = [ - .compressable, - ] -} - -#endif // swift(>=4.2) - -/// The type of route that a client took to reach a server w.r.t. gRPCLB. -/// The server must fill in "fallback" if it detects that the RPC reached -/// the server via the "gRPCLB fallback" path, and "backend" if it detects -/// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got -/// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly -/// how this detection is done is context and server dependent. -public enum Grpc_Testing_GrpclbRouteType: SwiftProtobuf.Enum { - public typealias RawValue = Int - - /// Server didn't detect the route that a client took to reach it. - case unknown // = 0 - - /// Indicates that a client reached a server via gRPCLB fallback. - case fallback // = 1 - - /// Indicates that a client reached a server as a gRPCLB-given backend. - case backend // = 2 - case UNRECOGNIZED(Int) - - public init() { - self = .unknown - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .fallback - case 2: self = .backend - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .unknown: return 0 - case .fallback: return 1 - case .backend: return 2 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension Grpc_Testing_GrpclbRouteType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static var allCases: [Grpc_Testing_GrpclbRouteType] = [ - .unknown, - .fallback, - .backend, - ] -} - -#endif // swift(>=4.2) - -/// TODO(dgq): Go back to using well-known types once -/// https://github.com/grpc/grpc/issues/6980 has been fixed. -/// import "google/protobuf/wrappers.proto"; -public struct Grpc_Testing_BoolValue { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The bool value. - public var value: Bool = false - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A block of data, to simply increase gRPC message size. -public struct Grpc_Testing_Payload { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The type of data in body. - public var type: Grpc_Testing_PayloadType = .compressable - - /// Primary contents of payload. - public var body: Data = Data() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A protobuf representation for grpc status. This is used by test -/// clients to specify a status that the server should attempt to return. -public struct Grpc_Testing_EchoStatus { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var code: Int32 = 0 - - public var message: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Unary request. -public struct Grpc_Testing_SimpleRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, server randomly chooses one from other formats. - public var responseType: Grpc_Testing_PayloadType = .compressable - - /// Desired payload size in the response from the server. - public var responseSize: Int32 = 0 - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether SimpleResponse should include username. - public var fillUsername: Bool = false - - /// Whether SimpleResponse should include OAuth scope. - public var fillOauthScope: Bool = false - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - public var responseCompressed: Grpc_Testing_BoolValue { - get {return _responseCompressed ?? Grpc_Testing_BoolValue()} - set {_responseCompressed = newValue} - } - /// Returns true if `responseCompressed` has been explicitly set. - public var hasResponseCompressed: Bool {return self._responseCompressed != nil} - /// Clears the value of `responseCompressed`. Subsequent reads from it will return its default value. - public mutating func clearResponseCompressed() {self._responseCompressed = nil} - - /// Whether server should return a given status - public var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - public var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - public mutating func clearResponseStatus() {self._responseStatus = nil} - - /// Whether the server should expect this request to be compressed. - public var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - public var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - public mutating func clearExpectCompressed() {self._expectCompressed = nil} - - /// Whether SimpleResponse should include server_id. - public var fillServerID: Bool = false - - /// Whether SimpleResponse should include grpclb_route_type. - public var fillGrpclbRouteType: Bool = false - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseCompressed: Grpc_Testing_BoolValue? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil -} - -/// Unary response, as configured by the request. -public struct Grpc_Testing_SimpleResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase message size. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// The user the request came from, for verifying authentication was - /// successful when the client expected it. - public var username: String = String() - - /// OAuth scope. - public var oauthScope: String = String() - - /// Server ID. This must be unique among different server instances, - /// but the same across all RPC's made to a particular server instance. - public var serverID: String = String() - - /// gRPCLB Path. - public var grpclbRouteType: Grpc_Testing_GrpclbRouteType = .unknown - - /// Server hostname. - public var hostname: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// Client-streaming request. -public struct Grpc_Testing_StreamingInputCallRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether the server should expect this request to be compressed. This field - /// is "nullable" in order to interoperate seamlessly with servers not able to - /// implement the full compression tests by introspecting the call to verify - /// the request's compression status. - public var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - public var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - public mutating func clearExpectCompressed() {self._expectCompressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil -} - -/// Client-streaming response. -public struct Grpc_Testing_StreamingInputCallResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Aggregated size of payloads received from the client. - public var aggregatedPayloadSize: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Configuration for a particular response. -public struct Grpc_Testing_ResponseParameters { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload sizes in responses from the server. - public var size: Int32 = 0 - - /// Desired interval between consecutive responses in the response stream in - /// microseconds. - public var intervalUs: Int32 = 0 - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - public var compressed: Grpc_Testing_BoolValue { - get {return _compressed ?? Grpc_Testing_BoolValue()} - set {_compressed = newValue} - } - /// Returns true if `compressed` has been explicitly set. - public var hasCompressed: Bool {return self._compressed != nil} - /// Clears the value of `compressed`. Subsequent reads from it will return its default value. - public mutating func clearCompressed() {self._compressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _compressed: Grpc_Testing_BoolValue? = nil -} - -/// Server-streaming request. -public struct Grpc_Testing_StreamingOutputCallRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, the payload from each response in the stream - /// might be of different types. This is to simulate a mixed type of payload - /// stream. - public var responseType: Grpc_Testing_PayloadType = .compressable - - /// Configuration for each expected response message. - public var responseParameters: [Grpc_Testing_ResponseParameters] = [] - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether server should return a given status - public var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - public var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - public mutating func clearResponseStatus() {self._responseStatus = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil -} - -/// Server-streaming response, as configured by the request and parameters. -public struct Grpc_Testing_StreamingOutputCallResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase response size. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// For reconnect interop test only. -/// Client tells server what reconnection parameters it used. -public struct Grpc_Testing_ReconnectParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var maxReconnectBackoffMs: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// For reconnect interop test only. -/// Server tells client whether its reconnects are following the spec and the -/// reconnect backoffs it saw. -public struct Grpc_Testing_ReconnectInfo { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var passed: Bool = false - - public var backoffMs: [Int32] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_LoadBalancerStatsRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Request stats for the next num_rpcs sent by client. - public var numRpcs: Int32 = 0 - - /// If num_rpcs have not completed within timeout_sec, return partial results. - public var timeoutSec: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_LoadBalancerStatsResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of completed RPCs for each peer. - public var rpcsByPeer: Dictionary = [:] - - /// The number of RPCs that failed to record a remote peer. - public var numFailures: Int32 = 0 - - public var rpcsByMethod: Dictionary = [:] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public struct RpcsByPeer { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of completed RPCs for each peer. - public var rpcsByPeer: Dictionary = [:] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - } - - public init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_PayloadType: @unchecked Sendable {} -extension Grpc_Testing_GrpclbRouteType: @unchecked Sendable {} -extension Grpc_Testing_BoolValue: @unchecked Sendable {} -extension Grpc_Testing_Payload: @unchecked Sendable {} -extension Grpc_Testing_EchoStatus: @unchecked Sendable {} -extension Grpc_Testing_SimpleRequest: @unchecked Sendable {} -extension Grpc_Testing_SimpleResponse: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ResponseParameters: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ReconnectParams: @unchecked Sendable {} -extension Grpc_Testing_ReconnectInfo: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsRequest: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_PayloadType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "COMPRESSABLE"), - ] -} - -extension Grpc_Testing_GrpclbRouteType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "GRPCLB_ROUTE_TYPE_UNKNOWN"), - 1: .same(proto: "GRPCLB_ROUTE_TYPE_FALLBACK"), - 2: .same(proto: "GRPCLB_ROUTE_TYPE_BACKEND"), - ] -} - -extension Grpc_Testing_BoolValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".BoolValue" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "value"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.value) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.value != false { - try visitor.visitSingularBoolField(value: self.value, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_BoolValue, rhs: Grpc_Testing_BoolValue) -> Bool { - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Payload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Payload" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "body"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 2: try { try decoder.decodeSingularBytesField(value: &self.body) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.type != .compressable { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) - } - if !self.body.isEmpty { - try visitor.visitSingularBytesField(value: self.body, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Payload, rhs: Grpc_Testing_Payload) -> Bool { - if lhs.type != rhs.type {return false} - if lhs.body != rhs.body {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_EchoStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".EchoStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "code"), - 2: .same(proto: "message"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.code != 0 { - try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) - } - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_EchoStatus, rhs: Grpc_Testing_EchoStatus) -> Bool { - if lhs.code != rhs.code {return false} - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SimpleRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_size"), - 3: .same(proto: "payload"), - 4: .standard(proto: "fill_username"), - 5: .standard(proto: "fill_oauth_scope"), - 6: .standard(proto: "response_compressed"), - 7: .standard(proto: "response_status"), - 8: .standard(proto: "expect_compressed"), - 9: .standard(proto: "fill_server_id"), - 10: .standard(proto: "fill_grpclb_route_type"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.responseSize) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.fillUsername) }() - case 5: try { try decoder.decodeSingularBoolField(value: &self.fillOauthScope) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._responseCompressed) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - case 8: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - case 9: try { try decoder.decodeSingularBoolField(value: &self.fillServerID) }() - case 10: try { try decoder.decodeSingularBoolField(value: &self.fillGrpclbRouteType) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if self.responseSize != 0 { - try visitor.visitSingularInt32Field(value: self.responseSize, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if self.fillUsername != false { - try visitor.visitSingularBoolField(value: self.fillUsername, fieldNumber: 4) - } - if self.fillOauthScope != false { - try visitor.visitSingularBoolField(value: self.fillOauthScope, fieldNumber: 5) - } - try { if let v = self._responseCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() - if self.fillServerID != false { - try visitor.visitSingularBoolField(value: self.fillServerID, fieldNumber: 9) - } - if self.fillGrpclbRouteType != false { - try visitor.visitSingularBoolField(value: self.fillGrpclbRouteType, fieldNumber: 10) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SimpleRequest, rhs: Grpc_Testing_SimpleRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseSize != rhs.responseSize {return false} - if lhs._payload != rhs._payload {return false} - if lhs.fillUsername != rhs.fillUsername {return false} - if lhs.fillOauthScope != rhs.fillOauthScope {return false} - if lhs._responseCompressed != rhs._responseCompressed {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.fillServerID != rhs.fillServerID {return false} - if lhs.fillGrpclbRouteType != rhs.fillGrpclbRouteType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SimpleResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .same(proto: "username"), - 3: .standard(proto: "oauth_scope"), - 4: .standard(proto: "server_id"), - 5: .standard(proto: "grpclb_route_type"), - 6: .same(proto: "hostname"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.username) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.oauthScope) }() - case 4: try { try decoder.decodeSingularStringField(value: &self.serverID) }() - case 5: try { try decoder.decodeSingularEnumField(value: &self.grpclbRouteType) }() - case 6: try { try decoder.decodeSingularStringField(value: &self.hostname) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.username.isEmpty { - try visitor.visitSingularStringField(value: self.username, fieldNumber: 2) - } - if !self.oauthScope.isEmpty { - try visitor.visitSingularStringField(value: self.oauthScope, fieldNumber: 3) - } - if !self.serverID.isEmpty { - try visitor.visitSingularStringField(value: self.serverID, fieldNumber: 4) - } - if self.grpclbRouteType != .unknown { - try visitor.visitSingularEnumField(value: self.grpclbRouteType, fieldNumber: 5) - } - if !self.hostname.isEmpty { - try visitor.visitSingularStringField(value: self.hostname, fieldNumber: 6) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SimpleResponse, rhs: Grpc_Testing_SimpleResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.username != rhs.username {return false} - if lhs.oauthScope != rhs.oauthScope {return false} - if lhs.serverID != rhs.serverID {return false} - if lhs.grpclbRouteType != rhs.grpclbRouteType {return false} - if lhs.hostname != rhs.hostname {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .standard(proto: "expect_compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingInputCallRequest, rhs: Grpc_Testing_StreamingInputCallRequest) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "aggregated_payload_size"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.aggregatedPayloadSize) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.aggregatedPayloadSize != 0 { - try visitor.visitSingularInt32Field(value: self.aggregatedPayloadSize, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingInputCallResponse, rhs: Grpc_Testing_StreamingInputCallResponse) -> Bool { - if lhs.aggregatedPayloadSize != rhs.aggregatedPayloadSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ResponseParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ResponseParameters" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "size"), - 2: .standard(proto: "interval_us"), - 3: .same(proto: "compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.intervalUs) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._compressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.size != 0 { - try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) - } - if self.intervalUs != 0 { - try visitor.visitSingularInt32Field(value: self.intervalUs, fieldNumber: 2) - } - try { if let v = self._compressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ResponseParameters, rhs: Grpc_Testing_ResponseParameters) -> Bool { - if lhs.size != rhs.size {return false} - if lhs.intervalUs != rhs.intervalUs {return false} - if lhs._compressed != rhs._compressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_parameters"), - 3: .same(proto: "payload"), - 7: .standard(proto: "response_status"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.responseParameters) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if !self.responseParameters.isEmpty { - try visitor.visitRepeatedMessageField(value: self.responseParameters, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingOutputCallRequest, rhs: Grpc_Testing_StreamingOutputCallRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseParameters != rhs.responseParameters {return false} - if lhs._payload != rhs._payload {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingOutputCallResponse, rhs: Grpc_Testing_StreamingOutputCallResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ReconnectParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "max_reconnect_backoff_ms"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.maxReconnectBackoffMs) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.maxReconnectBackoffMs != 0 { - try visitor.visitSingularInt32Field(value: self.maxReconnectBackoffMs, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ReconnectParams, rhs: Grpc_Testing_ReconnectParams) -> Bool { - if lhs.maxReconnectBackoffMs != rhs.maxReconnectBackoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ReconnectInfo" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "passed"), - 2: .standard(proto: "backoff_ms"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.passed) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.backoffMs) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.passed != false { - try visitor.visitSingularBoolField(value: self.passed, fieldNumber: 1) - } - if !self.backoffMs.isEmpty { - try visitor.visitPackedInt32Field(value: self.backoffMs, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ReconnectInfo, rhs: Grpc_Testing_ReconnectInfo) -> Bool { - if lhs.passed != rhs.passed {return false} - if lhs.backoffMs != rhs.backoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".LoadBalancerStatsRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "num_rpcs"), - 2: .standard(proto: "timeout_sec"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.numRpcs) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.timeoutSec) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.numRpcs != 0 { - try visitor.visitSingularInt32Field(value: self.numRpcs, fieldNumber: 1) - } - if self.timeoutSec != 0 { - try visitor.visitSingularInt32Field(value: self.timeoutSec, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_LoadBalancerStatsRequest, rhs: Grpc_Testing_LoadBalancerStatsRequest) -> Bool { - if lhs.numRpcs != rhs.numRpcs {return false} - if lhs.timeoutSec != rhs.timeoutSec {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".LoadBalancerStatsResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "rpcs_by_peer"), - 2: .standard(proto: "num_failures"), - 3: .standard(proto: "rpcs_by_method"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.rpcsByPeer) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.numFailures) }() - case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.rpcsByMethod) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.rpcsByPeer.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.rpcsByPeer, fieldNumber: 1) - } - if self.numFailures != 0 { - try visitor.visitSingularInt32Field(value: self.numFailures, fieldNumber: 2) - } - if !self.rpcsByMethod.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.rpcsByMethod, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse, rhs: Grpc_Testing_LoadBalancerStatsResponse) -> Bool { - if lhs.rpcsByPeer != rhs.rpcsByPeer {return false} - if lhs.numFailures != rhs.numFailures {return false} - if lhs.rpcsByMethod != rhs.rpcsByMethod {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".RpcsByPeer" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "rpcs_by_peer"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.rpcsByPeer) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.rpcsByPeer.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.rpcsByPeer, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer, rhs: Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer) -> Bool { - if lhs.rpcsByPeer != rhs.rpcsByPeer {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.pb.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.pb.swift deleted file mode 100644 index 9ac715471..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.pb.swift +++ /dev/null @@ -1,335 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: payloads.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -public struct Grpc_Testing_ByteBufferParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var reqSize: Int32 = 0 - - public var respSize: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_SimpleProtoParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var reqSize: Int32 = 0 - - public var respSize: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// TODO (vpai): Fill this in once the details of complex, representative -/// protos are decided -public struct Grpc_Testing_ComplexProtoParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_PayloadConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var payload: Grpc_Testing_PayloadConfig.OneOf_Payload? = nil - - public var bytebufParams: Grpc_Testing_ByteBufferParams { - get { - if case .bytebufParams(let v)? = payload {return v} - return Grpc_Testing_ByteBufferParams() - } - set {payload = .bytebufParams(newValue)} - } - - public var simpleParams: Grpc_Testing_SimpleProtoParams { - get { - if case .simpleParams(let v)? = payload {return v} - return Grpc_Testing_SimpleProtoParams() - } - set {payload = .simpleParams(newValue)} - } - - public var complexParams: Grpc_Testing_ComplexProtoParams { - get { - if case .complexParams(let v)? = payload {return v} - return Grpc_Testing_ComplexProtoParams() - } - set {payload = .complexParams(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public enum OneOf_Payload: Equatable { - case bytebufParams(Grpc_Testing_ByteBufferParams) - case simpleParams(Grpc_Testing_SimpleProtoParams) - case complexParams(Grpc_Testing_ComplexProtoParams) - - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Testing_PayloadConfig.OneOf_Payload, rhs: Grpc_Testing_PayloadConfig.OneOf_Payload) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.bytebufParams, .bytebufParams): return { - guard case .bytebufParams(let l) = lhs, case .bytebufParams(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.simpleParams, .simpleParams): return { - guard case .simpleParams(let l) = lhs, case .simpleParams(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.complexParams, .complexParams): return { - guard case .complexParams(let l) = lhs, case .complexParams(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - public init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_ByteBufferParams: @unchecked Sendable {} -extension Grpc_Testing_SimpleProtoParams: @unchecked Sendable {} -extension Grpc_Testing_ComplexProtoParams: @unchecked Sendable {} -extension Grpc_Testing_PayloadConfig: @unchecked Sendable {} -extension Grpc_Testing_PayloadConfig.OneOf_Payload: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_ByteBufferParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ByteBufferParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "req_size"), - 2: .standard(proto: "resp_size"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.reqSize) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.respSize) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.reqSize != 0 { - try visitor.visitSingularInt32Field(value: self.reqSize, fieldNumber: 1) - } - if self.respSize != 0 { - try visitor.visitSingularInt32Field(value: self.respSize, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ByteBufferParams, rhs: Grpc_Testing_ByteBufferParams) -> Bool { - if lhs.reqSize != rhs.reqSize {return false} - if lhs.respSize != rhs.respSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleProtoParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SimpleProtoParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "req_size"), - 2: .standard(proto: "resp_size"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.reqSize) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.respSize) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.reqSize != 0 { - try visitor.visitSingularInt32Field(value: self.reqSize, fieldNumber: 1) - } - if self.respSize != 0 { - try visitor.visitSingularInt32Field(value: self.respSize, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SimpleProtoParams, rhs: Grpc_Testing_SimpleProtoParams) -> Bool { - if lhs.reqSize != rhs.reqSize {return false} - if lhs.respSize != rhs.respSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ComplexProtoParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ComplexProtoParams" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - public func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ComplexProtoParams, rhs: Grpc_Testing_ComplexProtoParams) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_PayloadConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".PayloadConfig" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "bytebuf_params"), - 2: .standard(proto: "simple_params"), - 3: .standard(proto: "complex_params"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ByteBufferParams? - var hadOneofValue = false - if let current = self.payload { - hadOneofValue = true - if case .bytebufParams(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payload = .bytebufParams(v) - } - }() - case 2: try { - var v: Grpc_Testing_SimpleProtoParams? - var hadOneofValue = false - if let current = self.payload { - hadOneofValue = true - if case .simpleParams(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payload = .simpleParams(v) - } - }() - case 3: try { - var v: Grpc_Testing_ComplexProtoParams? - var hadOneofValue = false - if let current = self.payload { - hadOneofValue = true - if case .complexParams(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payload = .complexParams(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.payload { - case .bytebufParams?: try { - guard case .bytebufParams(let v)? = self.payload else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .simpleParams?: try { - guard case .simpleParams(let v)? = self.payload else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case .complexParams?: try { - guard case .complexParams(let v)? = self.payload else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_PayloadConfig, rhs: Grpc_Testing_PayloadConfig) -> Bool { - if lhs.payload != rhs.payload {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.pb.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.pb.swift deleted file mode 100644 index 0952562bc..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.pb.swift +++ /dev/null @@ -1,470 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: stats.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -public struct Grpc_Testing_ServerStats { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// wall clock time change in seconds since last reset - public var timeElapsed: Double = 0 - - /// change in user time (in seconds) used by the server since last reset - public var timeUser: Double = 0 - - /// change in server time (in seconds) used by the server process and all - /// threads since last reset - public var timeSystem: Double = 0 - - /// change in total cpu time of the server (data from proc/stat) - public var totalCpuTime: UInt64 = 0 - - /// change in idle time of the server (data from proc/stat) - public var idleCpuTime: UInt64 = 0 - - /// Number of polls called inside completion queue - public var cqPollCount: UInt64 = 0 - - /// Core library stats - public var coreStats: Grpc_Core_Stats { - get {return _coreStats ?? Grpc_Core_Stats()} - set {_coreStats = newValue} - } - /// Returns true if `coreStats` has been explicitly set. - public var hasCoreStats: Bool {return self._coreStats != nil} - /// Clears the value of `coreStats`. Subsequent reads from it will return its default value. - public mutating func clearCoreStats() {self._coreStats = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _coreStats: Grpc_Core_Stats? = nil -} - -/// Histogram params based on grpc/support/histogram.c -public struct Grpc_Testing_HistogramParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// first bucket is [0, 1 + resolution) - public var resolution: Double = 0 - - /// use enough buckets to allow this value - public var maxPossible: Double = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Histogram data based on grpc/support/histogram.c -public struct Grpc_Testing_HistogramData { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var bucket: [UInt32] = [] - - public var minSeen: Double = 0 - - public var maxSeen: Double = 0 - - public var sum: Double = 0 - - public var sumOfSquares: Double = 0 - - public var count: Double = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_RequestResultCount { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var statusCode: Int32 = 0 - - public var count: Int64 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Grpc_Testing_ClientStats { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Latency histogram. Data points are in nanoseconds. - public var latencies: Grpc_Testing_HistogramData { - get {return _latencies ?? Grpc_Testing_HistogramData()} - set {_latencies = newValue} - } - /// Returns true if `latencies` has been explicitly set. - public var hasLatencies: Bool {return self._latencies != nil} - /// Clears the value of `latencies`. Subsequent reads from it will return its default value. - public mutating func clearLatencies() {self._latencies = nil} - - /// See ServerStats for details. - public var timeElapsed: Double = 0 - - public var timeUser: Double = 0 - - public var timeSystem: Double = 0 - - /// Number of failed requests (one row per status code seen) - public var requestResults: [Grpc_Testing_RequestResultCount] = [] - - /// Number of polls called inside completion queue - public var cqPollCount: UInt64 = 0 - - /// Core library stats - public var coreStats: Grpc_Core_Stats { - get {return _coreStats ?? Grpc_Core_Stats()} - set {_coreStats = newValue} - } - /// Returns true if `coreStats` has been explicitly set. - public var hasCoreStats: Bool {return self._coreStats != nil} - /// Clears the value of `coreStats`. Subsequent reads from it will return its default value. - public mutating func clearCoreStats() {self._coreStats = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _latencies: Grpc_Testing_HistogramData? = nil - fileprivate var _coreStats: Grpc_Core_Stats? = nil -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_ServerStats: @unchecked Sendable {} -extension Grpc_Testing_HistogramParams: @unchecked Sendable {} -extension Grpc_Testing_HistogramData: @unchecked Sendable {} -extension Grpc_Testing_RequestResultCount: @unchecked Sendable {} -extension Grpc_Testing_ClientStats: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_ServerStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerStats" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "time_elapsed"), - 2: .standard(proto: "time_user"), - 3: .standard(proto: "time_system"), - 4: .standard(proto: "total_cpu_time"), - 5: .standard(proto: "idle_cpu_time"), - 6: .standard(proto: "cq_poll_count"), - 7: .standard(proto: "core_stats"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.timeElapsed) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.timeUser) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &self.timeSystem) }() - case 4: try { try decoder.decodeSingularUInt64Field(value: &self.totalCpuTime) }() - case 5: try { try decoder.decodeSingularUInt64Field(value: &self.idleCpuTime) }() - case 6: try { try decoder.decodeSingularUInt64Field(value: &self.cqPollCount) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._coreStats) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.timeElapsed != 0 { - try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 1) - } - if self.timeUser != 0 { - try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 2) - } - if self.timeSystem != 0 { - try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 3) - } - if self.totalCpuTime != 0 { - try visitor.visitSingularUInt64Field(value: self.totalCpuTime, fieldNumber: 4) - } - if self.idleCpuTime != 0 { - try visitor.visitSingularUInt64Field(value: self.idleCpuTime, fieldNumber: 5) - } - if self.cqPollCount != 0 { - try visitor.visitSingularUInt64Field(value: self.cqPollCount, fieldNumber: 6) - } - try { if let v = self._coreStats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ServerStats, rhs: Grpc_Testing_ServerStats) -> Bool { - if lhs.timeElapsed != rhs.timeElapsed {return false} - if lhs.timeUser != rhs.timeUser {return false} - if lhs.timeSystem != rhs.timeSystem {return false} - if lhs.totalCpuTime != rhs.totalCpuTime {return false} - if lhs.idleCpuTime != rhs.idleCpuTime {return false} - if lhs.cqPollCount != rhs.cqPollCount {return false} - if lhs._coreStats != rhs._coreStats {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_HistogramParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".HistogramParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "resolution"), - 2: .standard(proto: "max_possible"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.resolution) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.maxPossible) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.resolution != 0 { - try visitor.visitSingularDoubleField(value: self.resolution, fieldNumber: 1) - } - if self.maxPossible != 0 { - try visitor.visitSingularDoubleField(value: self.maxPossible, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_HistogramParams, rhs: Grpc_Testing_HistogramParams) -> Bool { - if lhs.resolution != rhs.resolution {return false} - if lhs.maxPossible != rhs.maxPossible {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_HistogramData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".HistogramData" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "bucket"), - 2: .standard(proto: "min_seen"), - 3: .standard(proto: "max_seen"), - 4: .same(proto: "sum"), - 5: .standard(proto: "sum_of_squares"), - 6: .same(proto: "count"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedUInt32Field(value: &self.bucket) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.minSeen) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &self.maxSeen) }() - case 4: try { try decoder.decodeSingularDoubleField(value: &self.sum) }() - case 5: try { try decoder.decodeSingularDoubleField(value: &self.sumOfSquares) }() - case 6: try { try decoder.decodeSingularDoubleField(value: &self.count) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.bucket.isEmpty { - try visitor.visitPackedUInt32Field(value: self.bucket, fieldNumber: 1) - } - if self.minSeen != 0 { - try visitor.visitSingularDoubleField(value: self.minSeen, fieldNumber: 2) - } - if self.maxSeen != 0 { - try visitor.visitSingularDoubleField(value: self.maxSeen, fieldNumber: 3) - } - if self.sum != 0 { - try visitor.visitSingularDoubleField(value: self.sum, fieldNumber: 4) - } - if self.sumOfSquares != 0 { - try visitor.visitSingularDoubleField(value: self.sumOfSquares, fieldNumber: 5) - } - if self.count != 0 { - try visitor.visitSingularDoubleField(value: self.count, fieldNumber: 6) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_HistogramData, rhs: Grpc_Testing_HistogramData) -> Bool { - if lhs.bucket != rhs.bucket {return false} - if lhs.minSeen != rhs.minSeen {return false} - if lhs.maxSeen != rhs.maxSeen {return false} - if lhs.sum != rhs.sum {return false} - if lhs.sumOfSquares != rhs.sumOfSquares {return false} - if lhs.count != rhs.count {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_RequestResultCount: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".RequestResultCount" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "status_code"), - 2: .same(proto: "count"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.statusCode) }() - case 2: try { try decoder.decodeSingularInt64Field(value: &self.count) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.statusCode != 0 { - try visitor.visitSingularInt32Field(value: self.statusCode, fieldNumber: 1) - } - if self.count != 0 { - try visitor.visitSingularInt64Field(value: self.count, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_RequestResultCount, rhs: Grpc_Testing_RequestResultCount) -> Bool { - if lhs.statusCode != rhs.statusCode {return false} - if lhs.count != rhs.count {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ClientStats" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "latencies"), - 2: .standard(proto: "time_elapsed"), - 3: .standard(proto: "time_user"), - 4: .standard(proto: "time_system"), - 5: .standard(proto: "request_results"), - 6: .standard(proto: "cq_poll_count"), - 7: .standard(proto: "core_stats"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._latencies) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.timeElapsed) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &self.timeUser) }() - case 4: try { try decoder.decodeSingularDoubleField(value: &self.timeSystem) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &self.requestResults) }() - case 6: try { try decoder.decodeSingularUInt64Field(value: &self.cqPollCount) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._coreStats) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._latencies { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.timeElapsed != 0 { - try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 2) - } - if self.timeUser != 0 { - try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 3) - } - if self.timeSystem != 0 { - try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 4) - } - if !self.requestResults.isEmpty { - try visitor.visitRepeatedMessageField(value: self.requestResults, fieldNumber: 5) - } - if self.cqPollCount != 0 { - try visitor.visitSingularUInt64Field(value: self.cqPollCount, fieldNumber: 6) - } - try { if let v = self._coreStats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ClientStats, rhs: Grpc_Testing_ClientStats) -> Bool { - if lhs._latencies != rhs._latencies {return false} - if lhs.timeElapsed != rhs.timeElapsed {return false} - if lhs.timeUser != rhs.timeUser {return false} - if lhs.timeSystem != rhs.timeSystem {return false} - if lhs.requestResults != rhs.requestResults {return false} - if lhs.cqPollCount != rhs.cqPollCount {return false} - if lhs._coreStats != rhs._coreStats {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto index 568b774e8..e7af31944 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto @@ -16,7 +16,7 @@ syntax = "proto3"; package grpc.testing; -import "core_stats.proto"; +import "Model/core_stats.proto"; message ServerStats { // wall clock time change in seconds since last reset diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift deleted file mode 100644 index 8147bd723..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.grpc.swift +++ /dev/null @@ -1,681 +0,0 @@ -// -// DO NOT EDIT. -// -// Generated by the protocol buffer compiler. -// Source: worker_service.proto -// - -// -// Copyright 2018, 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 GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Usage: instantiate `Grpc_Testing_WorkerServiceClient`, then call methods of this protocol to make API calls. -public protocol Grpc_Testing_WorkerServiceClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? { get } - - func runServer( - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_ServerStatus) -> Void - ) -> BidirectionalStreamingCall - - func runClient( - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_ClientStatus) -> Void - ) -> BidirectionalStreamingCall - - func coreCount( - _ request: Grpc_Testing_CoreRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func quitWorker( - _ request: Grpc_Testing_Void, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Grpc_Testing_WorkerServiceClientProtocol { - public var serviceName: String { - return "grpc.testing.WorkerService" - } - - /// Start server with specified workload. - /// First request sent specifies the ServerConfig followed by ServerStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test server - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func runServer( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_ServerStatus) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runServer.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunServerInterceptors() ?? [], - handler: handler - ) - } - - /// Start client with specified workload. - /// First request sent specifies the ClientConfig followed by ClientStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test client - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func runClient( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_ClientStatus) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runClient.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunClientInterceptors() ?? [], - handler: handler - ) - } - - /// Just return the core count - unary call - /// - /// - Parameters: - /// - request: Request to send to CoreCount. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func coreCount( - _ request: Grpc_Testing_CoreRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.coreCount.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCoreCountInterceptors() ?? [] - ) - } - - /// Quit this worker - /// - /// - Parameters: - /// - request: Request to send to QuitWorker. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func quitWorker( - _ request: Grpc_Testing_Void, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.quitWorker.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeQuitWorkerInterceptors() ?? [] - ) - } -} - -#if compiler(>=5.6) -@available(*, deprecated) -extension Grpc_Testing_WorkerServiceClient: @unchecked Sendable {} -#endif // compiler(>=5.6) - -@available(*, deprecated, renamed: "Grpc_Testing_WorkerServiceNIOClient") -public final class Grpc_Testing_WorkerServiceClient: Grpc_Testing_WorkerServiceClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the grpc.testing.WorkerService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Grpc_Testing_WorkerServiceNIOClient: Grpc_Testing_WorkerServiceClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? - - /// Creates a client for the grpc.testing.WorkerService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -#if compiler(>=5.6) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_WorkerServiceAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? { get } - - func makeRunServerCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall - - func makeRunClientCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall - - func makeCoreCountCall( - _ request: Grpc_Testing_CoreRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeQuitWorkerCall( - _ request: Grpc_Testing_Void, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_WorkerServiceAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_WorkerServiceClientMetadata.serviceDescriptor - } - - public var interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? { - return nil - } - - public func makeRunServerCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runServer.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunServerInterceptors() ?? [] - ) - } - - public func makeRunClientCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runClient.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunClientInterceptors() ?? [] - ) - } - - public func makeCoreCountCall( - _ request: Grpc_Testing_CoreRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.coreCount.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCoreCountInterceptors() ?? [] - ) - } - - public func makeQuitWorkerCall( - _ request: Grpc_Testing_Void, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.quitWorker.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeQuitWorkerInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_WorkerServiceAsyncClientProtocol { - public func runServer( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_ServerArgs { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runServer.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunServerInterceptors() ?? [] - ) - } - - public func runServer( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_ServerArgs { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runServer.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunServerInterceptors() ?? [] - ) - } - - public func runClient( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_ClientArgs { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runClient.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunClientInterceptors() ?? [] - ) - } - - public func runClient( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_ClientArgs { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.runClient.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRunClientInterceptors() ?? [] - ) - } - - public func coreCount( - _ request: Grpc_Testing_CoreRequest, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_CoreResponse { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.coreCount.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCoreCountInterceptors() ?? [] - ) - } - - public func quitWorker( - _ request: Grpc_Testing_Void, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_Void { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_WorkerServiceClientMetadata.Methods.quitWorker.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeQuitWorkerInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Grpc_Testing_WorkerServiceAsyncClient: Grpc_Testing_WorkerServiceAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -#endif // compiler(>=5.6) - -public protocol Grpc_Testing_WorkerServiceClientInterceptorFactoryProtocol: GRPCSendable { - - /// - Returns: Interceptors to use when invoking 'runServer'. - func makeRunServerInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'runClient'. - func makeRunClientInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'coreCount'. - func makeCoreCountInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'quitWorker'. - func makeQuitWorkerInterceptors() -> [ClientInterceptor] -} - -public enum Grpc_Testing_WorkerServiceClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "WorkerService", - fullName: "grpc.testing.WorkerService", - methods: [ - Grpc_Testing_WorkerServiceClientMetadata.Methods.runServer, - Grpc_Testing_WorkerServiceClientMetadata.Methods.runClient, - Grpc_Testing_WorkerServiceClientMetadata.Methods.coreCount, - Grpc_Testing_WorkerServiceClientMetadata.Methods.quitWorker, - ] - ) - - public enum Methods { - public static let runServer = GRPCMethodDescriptor( - name: "RunServer", - path: "/grpc.testing.WorkerService/RunServer", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let runClient = GRPCMethodDescriptor( - name: "RunClient", - path: "/grpc.testing.WorkerService/RunClient", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let coreCount = GRPCMethodDescriptor( - name: "CoreCount", - path: "/grpc.testing.WorkerService/CoreCount", - type: GRPCCallType.unary - ) - - public static let quitWorker = GRPCMethodDescriptor( - name: "QuitWorker", - path: "/grpc.testing.WorkerService/QuitWorker", - type: GRPCCallType.unary - ) - } -} - -/// To build a server, implement a class that conforms to this protocol. -public protocol Grpc_Testing_WorkerServiceProvider: CallHandlerProvider { - var interceptors: Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol? { get } - - /// Start server with specified workload. - /// First request sent specifies the ServerConfig followed by ServerStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test server - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runServer(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// Start client with specified workload. - /// First request sent specifies the ClientConfig followed by ClientStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test client - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runClient(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// Just return the core count - unary call - func coreCount(request: Grpc_Testing_CoreRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Quit this worker - func quitWorker(request: Grpc_Testing_Void, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension Grpc_Testing_WorkerServiceProvider { - public var serviceName: Substring { - return Grpc_Testing_WorkerServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "RunServer": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRunServerInterceptors() ?? [], - observerFactory: self.runServer(context:) - ) - - case "RunClient": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRunClientInterceptors() ?? [], - observerFactory: self.runClient(context:) - ) - - case "CoreCount": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCoreCountInterceptors() ?? [], - userFunction: self.coreCount(request:context:) - ) - - case "QuitWorker": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeQuitWorkerInterceptors() ?? [], - userFunction: self.quitWorker(request:context:) - ) - - default: - return nil - } - } -} - -#if compiler(>=5.6) - -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_WorkerServiceAsyncProvider: CallHandlerProvider { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol? { get } - - /// Start server with specified workload. - /// First request sent specifies the ServerConfig followed by ServerStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test server - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - @Sendable func runServer( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// Start client with specified workload. - /// First request sent specifies the ClientConfig followed by ClientStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test client - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - @Sendable func runClient( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// Just return the core count - unary call - @Sendable func coreCount( - request: Grpc_Testing_CoreRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_CoreResponse - - /// Quit this worker - @Sendable func quitWorker( - request: Grpc_Testing_Void, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_Void -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_WorkerServiceAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_WorkerServiceServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Grpc_Testing_WorkerServiceServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "RunServer": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRunServerInterceptors() ?? [], - wrapping: self.runServer(requestStream:responseStream:context:) - ) - - case "RunClient": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRunClientInterceptors() ?? [], - wrapping: self.runClient(requestStream:responseStream:context:) - ) - - case "CoreCount": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCoreCountInterceptors() ?? [], - wrapping: self.coreCount(request:context:) - ) - - case "QuitWorker": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeQuitWorkerInterceptors() ?? [], - wrapping: self.quitWorker(request:context:) - ) - - default: - return nil - } - } -} - -#endif // compiler(>=5.6) - -public protocol Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'runServer'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRunServerInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'runClient'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRunClientInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'coreCount'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCoreCountInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'quitWorker'. - /// Defaults to calling `self.makeInterceptors()`. - func makeQuitWorkerInterceptors() -> [ServerInterceptor] -} - -public enum Grpc_Testing_WorkerServiceServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "WorkerService", - fullName: "grpc.testing.WorkerService", - methods: [ - Grpc_Testing_WorkerServiceServerMetadata.Methods.runServer, - Grpc_Testing_WorkerServiceServerMetadata.Methods.runClient, - Grpc_Testing_WorkerServiceServerMetadata.Methods.coreCount, - Grpc_Testing_WorkerServiceServerMetadata.Methods.quitWorker, - ] - ) - - public enum Methods { - public static let runServer = GRPCMethodDescriptor( - name: "RunServer", - path: "/grpc.testing.WorkerService/RunServer", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let runClient = GRPCMethodDescriptor( - name: "RunClient", - path: "/grpc.testing.WorkerService/RunClient", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let coreCount = GRPCMethodDescriptor( - name: "CoreCount", - path: "/grpc.testing.WorkerService/CoreCount", - type: GRPCCallType.unary - ) - - public static let quitWorker = GRPCMethodDescriptor( - name: "QuitWorker", - path: "/grpc.testing.WorkerService/QuitWorker", - type: GRPCCallType.unary - ) - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.pb.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.pb.swift deleted file mode 100644 index 2065ed1f5..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.pb.swift +++ /dev/null @@ -1,38 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: worker_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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. - -/// An integration test service that covers all the method signature permutations -/// of unary/streaming requests/responses. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto index 36202a29b..2c0901c36 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto @@ -16,7 +16,7 @@ // of unary/streaming requests/responses. syntax = "proto3"; -import "control.proto"; +import "Model/control.proto"; package grpc.testing; diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json b/Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json new file mode 100644 index 000000000..3019862f0 --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json @@ -0,0 +1,11 @@ +{ + "invocations": [ + { + "protoFiles": [ + "Model/benchmark_service.proto", + "Model/worker_service.proto", + ], + "visibility": "public" + } + ] +} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json b/Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json new file mode 100644 index 000000000..e7f07d3e0 --- /dev/null +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json @@ -0,0 +1,14 @@ +{ + "invocations": [ + { + "protoFiles": [ + "Model/messages.proto", + "Model/control.proto", + "Model/core_stats.proto", + "Model/payloads.proto", + "Model/stats.proto" + ], + "visibility": "public" + } + ] +} From 428c5c3058b51d8d71dc182daf73ea617139d194 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 7 Sep 2022 10:44:52 +0100 Subject: [PATCH 019/580] Log when receiving GOAWAY frames (#1476) --- Sources/GRPC/GRPCIdleHandler.swift | 6 +++++- Sources/GRPC/Logger.swift | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index adddb12eb..eed505a1c 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -287,7 +287,11 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { let frame = self.unwrapInboundIn(data) switch frame.payload { - case .goAway: + case let .goAway(lastStreamID, errorCode, _): + self.stateMachine.logger.debug("received GOAWAY frame", metadata: [ + MetadataKey.h2GoAwayLastStreamID: "\(Int(lastStreamID))", + MetadataKey.h2GoAwayError: "\(errorCode.networkCode)", + ]) self.perform(operations: self.stateMachine.receiveGoAway()) case let .settings(.settings(settings)): self.perform(operations: self.stateMachine.receiveSettings(settings)) diff --git a/Sources/GRPC/Logger.swift b/Sources/GRPC/Logger.swift index 4b1e97e3d..030a8af55 100644 --- a/Sources/GRPC/Logger.swift +++ b/Sources/GRPC/Logger.swift @@ -29,6 +29,8 @@ enum MetadataKey { static let h2Payload = "h2_payload" static let h2Headers = "h2_headers" static let h2DataBytes = "h2_data_bytes" + static let h2GoAwayError = "h2_goaway_error" + static let h2GoAwayLastStreamID = "h2_goaway_last_stream_id" static let error = "error" } From 1579a9c082e6d8679501c9b36cdf9efddd2d5e20 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 8 Sep 2022 10:57:31 +0100 Subject: [PATCH 020/580] Fix preconcurrency warnings (#1480) Motivation: NIO and Logging have now adopted Sendable so the `@preconcurrency` imports now emit warnings. Modifications: - Raise minimum required version to the versions with `Sendable` support - Remove `@preconcurrency` imports Result: No more warnings. --- Package.swift | 4 +-- .../GRPCAsyncServerCallContext.swift | 2 +- Sources/GRPC/CallOptions.swift | 7 +----- Sources/GRPC/ClientConnection.swift | 6 ++--- Sources/GRPC/ConnectionKeepalive.swift | 4 --- .../GRPC/ConnectionPool/GRPCChannelPool.swift | 5 ---- Sources/GRPC/PlatformSupport.swift | 25 +++++++++++++++++-- Sources/GRPC/TimeLimit.swift | 4 --- 8 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Package.swift b/Package.swift index 16957ad2b..0931529c5 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.36.0" + from: "2.41.1" ), .package( url: "https://github.com/apple/swift-nio-http2.git", @@ -52,7 +52,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-log.git", - from: "1.4.0" + from: "1.4.4" ), .package( url: "https://github.com/apple/swift-argument-parser.git", diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift index 41b57f28f..7b2ee6c3f 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift @@ -15,7 +15,7 @@ */ #if compiler(>=5.6) -@preconcurrency import Logging +import Logging import NIOConcurrencyHelpers import NIOHPACK diff --git a/Sources/GRPC/CallOptions.swift b/Sources/GRPC/CallOptions.swift index d9f28e428..51631abd6 100644 --- a/Sources/GRPC/CallOptions.swift +++ b/Sources/GRPC/CallOptions.swift @@ -15,14 +15,9 @@ */ import struct Foundation.UUID -#if swift(>=5.6) -@preconcurrency import Logging -@preconcurrency import NIOCore -#else -import Logging import NIOCore -#endif // swift(>=5.6) +import Logging import NIOHPACK import NIOHTTP1 import NIOHTTP2 diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index fbc2c58ea..6dfbd2f12 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -15,14 +15,12 @@ */ #if swift(>=5.6) @preconcurrency import Foundation -@preconcurrency import Logging -@preconcurrency import NIOCore #else import Foundation -import Logging -import NIOCore #endif // swift(>=5.6) +import Logging +import NIOCore import NIOHPACK import NIOHTTP2 #if canImport(NIOSSL) diff --git a/Sources/GRPC/ConnectionKeepalive.swift b/Sources/GRPC/ConnectionKeepalive.swift index e1e7a9783..8035cd5d6 100644 --- a/Sources/GRPC/ConnectionKeepalive.swift +++ b/Sources/GRPC/ConnectionKeepalive.swift @@ -13,11 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if swift(>=5.6) -@preconcurrency import NIOCore -#else import NIOCore -#endif // swift(>=5.6) /// Provides keepalive pings. /// diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift index 6338e6b27..366f1f4a0 100644 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift @@ -13,13 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if swift(>=5.6) -@preconcurrency import Logging -@preconcurrency import NIOCore -#else import Logging import NIOCore -#endif // swift(>=5.6) import NIOPosix public enum GRPCChannelPool { diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift index ed6635c80..cbe01cba2 100644 --- a/Sources/GRPC/PlatformSupport.swift +++ b/Sources/GRPC/PlatformSupport.swift @@ -121,7 +121,13 @@ public protocol ClientBootstrapProtocol { func connectTimeout(_ timeout: TimeAmount) -> Self func channelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption + + #if swift(>=5.7) + @preconcurrency + func channelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture) -> Self + #else func channelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture) -> Self + #endif } extension ClientBootstrapProtocol { @@ -149,10 +155,25 @@ public protocol ServerBootstrapProtocol { func bind(unixDomainSocketPath: String) -> EventLoopFuture func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture - func serverChannelInitializer(_ initializer: @escaping (Channel) -> EventLoopFuture) -> Self + #if swift(>=5.7) + @preconcurrency + func serverChannelInitializer( + _ handler: @escaping @Sendable (Channel) -> EventLoopFuture + ) -> Self + #else + func serverChannelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture) -> Self + #endif + func serverChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption - func childChannelInitializer(_ initializer: @escaping (Channel) -> EventLoopFuture) -> Self + #if swift(>=5.7) + @preconcurrency + func childChannelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture) + -> Self + #else + func childChannelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture) -> Self + #endif + func childChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption } diff --git a/Sources/GRPC/TimeLimit.swift b/Sources/GRPC/TimeLimit.swift index b8ebcb26e..ab370f240 100644 --- a/Sources/GRPC/TimeLimit.swift +++ b/Sources/GRPC/TimeLimit.swift @@ -14,11 +14,7 @@ * limitations under the License. */ import Dispatch -#if swift(>=5.6) -@preconcurrency import NIOCore -#else import NIOCore -#endif // swift(>=5.6) /// A time limit for an RPC. /// From 6cab35e471cd8e35df5ef913c749527369f67553 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 8 Sep 2022 13:15:25 +0100 Subject: [PATCH 021/580] Bump version number to 1.10.0 (#1481) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.10.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index c2d272475..797801a5e 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 9 + internal static let minor = 10 /// The patch version. internal static let patch = 0 From 04b35de0e2320833960b9705b8c352c2d18e5fa6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Sep 2022 08:41:07 +0100 Subject: [PATCH 022/580] Clarify relative paths for SPM plugin docs (#1488) Resolves #1487 --- Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md index 879e661db..9413a7de8 100644 --- a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md +++ b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md @@ -28,7 +28,8 @@ Next, you need to add the `.proto` files for which you want to generate your Swi source directory. You should also commit these files to your git repository since the generated types are now generated on demand. -> Note: imports on your `.proto` files will have to include the relative path from the target source to the `.proto` file you wish to import. +> Note: imports on your `.proto` files will have to include the relative path from the target source to the `.proto` file you wish to import. +> Files **must** be contained within the target source directory. ### Adding the plugin to your manifest @@ -87,6 +88,7 @@ to the root of your target's source folder. An example configuration file looks ``` > Note: paths to your `.proto` files will have to include the relative path from the target source to the `.proto` file location. +> Files **must** be contained within the target source directory. In the above configuration, you declared two invocations to the `protoc` compiler. The first invocation is generating Swift types for the `Foo.proto` file with `internal` visibility. Notice the relative path to the `.proto` file. From 75030dddf59c62db0f560dfe7f78965261ebfa22 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Sep 2022 09:05:56 +0100 Subject: [PATCH 023/580] Raise minimum Swift version to 5.5 (#1483) Motivation: Now that 5.7 has been released NIO is due to drop support for 5.4. gRPC Swift must do the same. Modifications: - Remove 5.4 from the CI - Add 5.7 release, move 5.7-nightly to main-nightly - Update Package.swift for the FuzzTesting subpackage to 5.7 - Update the README.md and Docc index page. Result: Minimum supported Swift version is 5.4 --- .github/workflows/ci.yaml | 33 +-- FuzzTesting/Package.swift | 2 +- Package@swift-5.4.swift | 470 -------------------------------- README.md | 9 +- Sources/GRPC/Docs.docc/index.md | 9 +- 5 files changed, 32 insertions(+), 491 deletions(-) delete mode 100644 Package@swift-5.4.swift diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2fd2dc9b3..bf808f490 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,10 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-5.7-focal + - image: swiftlang/swift:nightly-focal + # No TSAN because of: https://github.com/apple/swift/issues/59068 + # swift-test-flags: "--sanitize=thread" + - image: swift:5.7-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - image: swift:5.6-focal @@ -32,8 +35,6 @@ jobs: # swift-test-flags: "--sanitize=thread" - image: swift:5.5-focal swift-test-flags: "--sanitize=thread" - - image: swift:5.4-focal - swift-test-flags: "--sanitize=thread" name: Build and Test on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -53,7 +54,17 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-5.7-focal + - image: swiftlang/swift:nightly-focal + env: + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 428000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 174000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 181000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 + - image: swift:5.7-jammy env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 428000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 @@ -83,16 +94,6 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 186000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 193000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 193000 - - image: swift:5.4-focal - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 459000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 189000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 186000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 193000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 193000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -108,10 +109,10 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-5.7-focal + - image: swiftlang/swift:nightly-focal + - image: swift:5.7-jammy - image: swift:5.6-focal - image: swift:5.5-focal - - image: swift:5.4-focal name: Integration Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: diff --git a/FuzzTesting/Package.swift b/FuzzTesting/Package.swift index 8daed6018..03d3c49aa 100644 --- a/FuzzTesting/Package.swift +++ b/FuzzTesting/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.7 /* * Copyright 2021, gRPC Authors All rights reserved. * diff --git a/Package@swift-5.4.swift b/Package@swift-5.4.swift deleted file mode 100644 index 069e519de..000000000 --- a/Package@swift-5.4.swift +++ /dev/null @@ -1,470 +0,0 @@ -// swift-tools-version:5.4 -/* - * Copyright 2022, 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 PackageDescription -// swiftformat puts the next import before the tools version. -// swiftformat:disable:next sortedImports -import class Foundation.ProcessInfo - -let grpcPackageName = "grpc-swift" -let grpcProductName = "GRPC" -let cgrpcZlibProductName = "CGRPCZlib" -let grpcTargetName = grpcProductName -let cgrpcZlibTargetName = cgrpcZlibProductName - -let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil - -// MARK: - Package Dependencies - -let packageDependencies: [Package.Dependency] = [ - .package( - url: "https://github.com/apple/swift-nio.git", - from: "2.36.0" - ), - .package( - url: "https://github.com/apple/swift-nio-http2.git", - from: "1.22.0" - ), - .package( - url: "https://github.com/apple/swift-nio-transport-services.git", - from: "1.11.1" - ), - .package( - url: "https://github.com/apple/swift-nio-extras.git", - from: "1.4.0" - ), - .package( - name: "SwiftProtobuf", - url: "https://github.com/apple/swift-protobuf.git", - from: "1.19.0" - ), - .package( - url: "https://github.com/apple/swift-log.git", - from: "1.4.0" - ), - .package( - url: "https://github.com/apple/swift-argument-parser.git", - from: "1.0.0" - ), -].appending( - .package( - url: "https://github.com/apple/swift-nio-ssl.git", - from: "2.14.0" - ), - if: includeNIOSSL -) - -// MARK: - Target Dependencies - -extension Target.Dependency { - // Target dependencies; external - static let grpc: Self = .target(name: grpcTargetName) - static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) - static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") - - // Target dependencies; internal - static let grpcSampleData: Self = .target(name: "GRPCSampleData") - static let echoModel: Self = .target(name: "EchoModel") - static let echoImplementation: Self = .target(name: "EchoImplementation") - static let helloWorldModel: Self = .target(name: "HelloWorldModel") - static let routeGuideModel: Self = .target(name: "RouteGuideModel") - static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") - static let interopTestImplementation: Self = - .target(name: "GRPCInteroperabilityTestsImplementation") - - // Product dependencies - static let argumentParser: Self = .product( - name: "ArgumentParser", - package: "swift-argument-parser" - ) - static let nio: Self = .product(name: "NIO", package: "swift-nio") - static let nioConcurrencyHelpers: Self = .product( - name: "NIOConcurrencyHelpers", - package: "swift-nio" - ) - static let nioCore: Self = .product(name: "NIOCore", package: "swift-nio") - static let nioEmbedded: Self = .product(name: "NIOEmbedded", package: "swift-nio") - static let nioExtras: Self = .product(name: "NIOExtras", package: "swift-nio-extras") - static let nioFoundationCompat: Self = .product(name: "NIOFoundationCompat", package: "swift-nio") - static let nioHTTP1: Self = .product(name: "NIOHTTP1", package: "swift-nio") - static let nioHTTP2: Self = .product(name: "NIOHTTP2", package: "swift-nio-http2") - static let nioPosix: Self = .product(name: "NIOPosix", package: "swift-nio") - static let nioSSL: Self = .product(name: "NIOSSL", package: "swift-nio-ssl") - static let nioTLS: Self = .product(name: "NIOTLS", package: "swift-nio") - static let nioTransportServices: Self = .product( - name: "NIOTransportServices", - package: "swift-nio-transport-services" - ) - static let logging: Self = .product(name: "Logging", package: "swift-log") - static let protobuf: Self = .product(name: "SwiftProtobuf", package: "SwiftProtobuf") - static let protobufPluginLibrary: Self = .product( - name: "SwiftProtobufPluginLibrary", - package: "SwiftProtobuf" - ) -} - -// MARK: - Targets - -extension Target { - static let grpc: Target = .target( - name: grpcTargetName, - dependencies: [ - .cgrpcZlib, - .nio, - .nioCore, - .nioPosix, - .nioEmbedded, - .nioFoundationCompat, - .nioTLS, - .nioTransportServices, - .nioHTTP1, - .nioHTTP2, - .nioExtras, - .logging, - .protobuf, - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Sources/GRPC" - ) - - static let cgrpcZlib: Target = .target( - name: cgrpcZlibTargetName, - path: "Sources/CGRPCZlib", - linkerSettings: [ - .linkedLibrary("z"), - ] - ) - - static let protocGenGRPCSwift: Target = .executableTarget( - name: "protoc-gen-grpc-swift", - dependencies: [ - .protobuf, - .protobufPluginLibrary, - ], - exclude: [ - "README.md", - ] - ) - - static let grpcTests: Target = .testTarget( - name: "GRPCTests", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .helloWorldModel, - .interopTestModels, - .interopTestImplementation, - .grpcSampleData, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .nioTLS, - .nioHTTP1, - .nioHTTP2, - .nioEmbedded, - .nioTransportServices, - .logging, - ].appending( - .nioSSL, if: includeNIOSSL - ), - exclude: [ - "Codegen/Normalization/normalization.proto", - ] - ) - - static let interopTestModels: Target = .target( - name: "GRPCInteroperabilityTestModels", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - exclude: [ - "README.md", - "generate.sh", - "src/proto/grpc/testing/empty.proto", - "src/proto/grpc/testing/empty_service.proto", - "src/proto/grpc/testing/messages.proto", - "src/proto/grpc/testing/test.proto", - "unimplemented_call.patch", - ] - ) - - static let interopTestImplementation: Target = .target( - name: "GRPCInteroperabilityTestsImplementation", - dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .nioHTTP1, - .logging, - ].appending( - .nioSSL, if: includeNIOSSL - ) - ) - - static let interopTests: Target = .executableTarget( - name: "GRPCInteroperabilityTests", - dependencies: [ - .grpc, - .interopTestImplementation, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ] - ) - - static let backoffInteropTest: Target = .executableTarget( - name: "GRPCConnectionBackoffInteropTest", - dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ], - exclude: [ - "README.md", - ] - ) - - static let perfTests: Target = .executableTarget( - name: "GRPCPerformanceTests", - dependencies: [ - .grpc, - .grpcSampleData, - .nioCore, - .nioEmbedded, - .nioPosix, - .nioHTTP2, - .argumentParser, - ] - ) - - static let grpcSampleData: Target = .target( - name: "GRPCSampleData", - dependencies: includeNIOSSL ? [.nioSSL] : [], - exclude: [ - "bundle.p12", - ] - ) - - static let echoModel: Target = .target( - name: "EchoModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Sources/Examples/Echo/Model", - exclude: [ - "echo.proto", - ] - ) - - static let echoImplementation: Target = .target( - name: "EchoImplementation", - dependencies: [ - .echoModel, - .grpc, - .nioCore, - .nioHTTP2, - .protobuf, - ], - path: "Sources/Examples/Echo/Implementation" - ) - - static let echo: Target = .executableTarget( - name: "Echo", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .grpcSampleData, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Sources/Examples/Echo/Runtime" - ) - - static let helloWorldModel: Target = .target( - name: "HelloWorldModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Sources/Examples/HelloWorld/Model", - exclude: [ - "helloworld.proto", - ] - ) - - static let helloWorldClient: Target = .executableTarget( - name: "HelloWorldClient", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/HelloWorld/Client" - ) - - static let helloWorldServer: Target = .executableTarget( - name: "HelloWorldServer", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/HelloWorld/Server" - ) - - static let routeGuideModel: Target = .target( - name: "RouteGuideModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Sources/Examples/RouteGuide/Model", - exclude: [ - "route_guide.proto", - ] - ) - - static let routeGuideClient: Target = .executableTarget( - name: "RouteGuideClient", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/RouteGuide/Client" - ) - - static let routeGuideServer: Target = .executableTarget( - name: "RouteGuideServer", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/RouteGuide/Server" - ) - - static let packetCapture: Target = .executableTarget( - name: "PacketCapture", - dependencies: [ - .grpc, - .echoModel, - .nioCore, - .nioPosix, - .nioExtras, - .argumentParser, - ], - path: "Sources/Examples/PacketCapture", - exclude: [ - "README.md", - ] - ) -} - -// MARK: - Products - -extension Product { - static let grpc: Product = .library( - name: grpcProductName, - targets: [grpcTargetName] - ) - - static let cgrpcZlib: Product = .library( - name: cgrpcZlibProductName, - targets: [cgrpcZlibTargetName] - ) - - static let protocGenGRPCSwift: Product = .executable( - name: "protoc-gen-grpc-swift", - targets: ["protoc-gen-grpc-swift"] - ) -} - -// MARK: - Package - -let package = Package( - name: grpcPackageName, - products: [ - .grpc, - .cgrpcZlib, - .protocGenGRPCSwift, - ], - dependencies: packageDependencies, - targets: [ - // Products - .grpc, - .cgrpcZlib, - .protocGenGRPCSwift, - - // Tests etc. - .grpcTests, - .interopTestModels, - .interopTestImplementation, - .interopTests, - .backoffInteropTest, - .perfTests, - .grpcSampleData, - - // Examples - .echoModel, - .echoImplementation, - .echo, - .helloWorldModel, - .helloWorldClient, - .helloWorldServer, - .routeGuideModel, - .routeGuideClient, - .routeGuideServer, - .packetCapture, - ] -) - -extension Array { - func appending(_ element: Element, if condition: Bool) -> [Element] { - if condition { - return self + [element] - } else { - return self - } - } -} diff --git a/README.md b/README.md index 04a4886f1..cc337a324 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,13 @@ The remainder of this README refers to the 1.x version of gRPC Swift. gRPC Swift's platform support is identical to the [platform support of Swift NIO][swift-nio-platforms]. -The earliest supported Swift version for gRPC Swift 1.8.x and newer is 5.4. -For 1.7.x and earlier the oldest supported Swift version is 5.2. +The earliest supported version of Swift for gRPC Swift releases are as follows: + +gRPC Swift Version | Earliest Swift Version +-------------------|----------------------- +`1.0.0 ..< 1.8.0` | 5.2 +`1.8.0 ..< 1.11.0` | 5.4 +`1.11.0...` | 5.5 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md index ee324f570..1a58334c0 100644 --- a/Sources/GRPC/Docs.docc/index.md +++ b/Sources/GRPC/Docs.docc/index.md @@ -20,8 +20,13 @@ insecure channels. gRPC Swift's platform support is identical to the [platform support of Swift NIO][swift-nio-platforms]. -The earliest supported Swift version for gRPC Swift 1.8.x and newer is 5.4. -For 1.7.x and earlier the oldest supported Swift version is 5.2. +The earliest supported version of Swift for gRPC Swift releases are as follows: + +gRPC Swift Version | Earliest Swift Version +-------------------|----------------------- +`1.0.0 ..< 1.8.0` | 5.2 +`1.8.0 ..< 1.11.0` | 5.4 +`1.11.0...` | 5.5 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. From 4b0a1316736706569f2079d682e9c82b39ec937b Mon Sep 17 00:00:00 2001 From: Filip W Date: Wed, 21 Sep 2022 13:57:08 +0200 Subject: [PATCH 024/580] updated NaturalLanguage example to Xcode 14 and latest package versions (#1489) --- .../Google/NaturalLanguage/Package.resolved | 60 +- .../Sources/annotations.pb.swift | 18 +- .../NaturalLanguage/Sources/http.pb.swift | 330 ++-- .../Sources/language_service.grpc.swift | 853 +++++++- .../Sources/language_service.pb.swift | 1719 +++++++++-------- .../Google/NaturalLanguage/Sources/main.swift | 15 +- 6 files changed, 1889 insertions(+), 1106 deletions(-) diff --git a/Examples/Google/NaturalLanguage/Package.resolved b/Examples/Google/NaturalLanguage/Package.resolved index 358f3bfe5..61a38d594 100644 --- a/Examples/Google/NaturalLanguage/Package.resolved +++ b/Examples/Google/NaturalLanguage/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/attaswift/BigInt", "state": { "branch": null, - "revision": "19f5e8a48be155e34abb98a2bcf4a343316f0343", - "version": "5.0.0" + "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version": "5.3.0" } }, { @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", "state": { "branch": null, - "revision": "79ac632c85df81f2618dd838fc3869633e163370", - "version": "1.2.0" + "revision": "af1b58fc569bfde777462349b9f7314b61762be0", + "version": "1.3.2" } }, { @@ -24,26 +24,35 @@ "repositoryURL": "https://github.com/googleapis/google-auth-library-swift.git", "state": { "branch": null, - "revision": "f3c652646735e27885e81e710d4147f33eb6c26f", - "version": "0.5.1" + "revision": "4b510d91fc74f1415eae6dabc9836b8c3e1f44f6", + "version": "0.5.3" } }, { - "package": "GRPC", - "repositoryURL": "/Users/georgebarnett/workspace/grpc-swift", + "package": "grpc-swift", + "repositoryURL": "/Users/filipw/Documents/dev/grpc-swift", "state": { "branch": "HEAD", - "revision": "13a4fdedf9ba1286d8fb4a73ccb49fe8c77871e9", + "revision": "6cab35e471cd8e35df5ef913c749527369f67553", "version": null } }, + { + "package": "swift-atomics", + "repositoryURL": "https://github.com/apple/swift-atomics.git", + "state": { + "branch": null, + "revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", + "version": "1.0.2" + } + }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log", "state": { "branch": null, - "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", - "version": "1.2.0" + "revision": "6fe203dc33195667ce1759bf0182975e4653ba1c", + "version": "1.4.4" } }, { @@ -51,8 +60,17 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "ff01888051cd7efceb1bf8319c1dd3986c4bf6fc", - "version": "2.10.1" + "revision": "b4e0a274f7f34210e97e2f2c50ab02a10b549250", + "version": "2.41.1" + } + }, + { + "package": "swift-nio-extras", + "repositoryURL": "https://github.com/apple/swift-nio-extras.git", + "state": { + "branch": null, + "revision": "6c84d247754ad77487a6f0694273b89b83efd056", + "version": "1.14.0" } }, { @@ -60,8 +78,8 @@ "repositoryURL": "https://github.com/apple/swift-nio-http2.git", "state": { "branch": null, - "revision": "867259332c45d5405efed844f7b3997ebfa94167", - "version": "1.7.2" + "revision": "f9ab1c94c80d568efd762d2a638f25162691d766", + "version": "1.22.1" } }, { @@ -69,8 +87,8 @@ "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", "state": { "branch": null, - "revision": "ccf96bbe65ecc7c1558ab0dba7ffabdea5c1d31f", - "version": "2.4.4" + "revision": "ba7c0d7f82affc518147ea61d240330bf7f7ea9b", + "version": "2.22.1" } }, { @@ -78,8 +96,8 @@ "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", "state": { "branch": null, - "revision": "80b11dc13261e0e52f75ea3a0b2e04f24e925019", - "version": "1.2.1" + "revision": "4e02d9cf35cabfb538c96613272fb027dd0c8692", + "version": "1.13.1" } }, { @@ -87,8 +105,8 @@ "repositoryURL": "https://github.com/apple/swift-protobuf.git", "state": { "branch": null, - "revision": "da75a93ac017534e0028e83c0e4fc4610d2acf48", - "version": "1.7.0" + "revision": "b8230909dedc640294d7324d37f4c91ad3dcf177", + "version": "1.20.1" } } ] diff --git a/Examples/Google/NaturalLanguage/Sources/annotations.pb.swift b/Examples/Google/NaturalLanguage/Sources/annotations.pb.swift index d9abbc9c2..249cc2bdd 100644 --- a/Examples/Google/NaturalLanguage/Sources/annotations.pb.swift +++ b/Examples/Google/NaturalLanguage/Sources/annotations.pb.swift @@ -1,4 +1,5 @@ // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: google/api/annotations.proto @@ -6,7 +7,7 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -// Copyright (c) 2015, Google Inc. +// Copyright 2015 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,7 +27,7 @@ import SwiftProtobuf // If the compiler emits an error on this type, it is because this file // was generated by a version of the `protoc` Swift plug-in that is // incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that your are building against the same version of the API +// Please ensure that you are building against the same version of the API // that was used to generate this file. fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} @@ -35,6 +36,13 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP // MARK: - Extension support defined in annotations.proto. +// MARK: - Extension Properties + +// Swift Extensions on the exteneded Messages to add easy access to the declared +// extension fields. The names are based on the extension field name from the proto +// declaration. To avoid naming collisions, the names are prefixed with the name of +// the scope where the extend directive occurs. + extension SwiftProtobuf.Google_Protobuf_MethodOptions { /// See `HttpRule`. @@ -55,6 +63,8 @@ extension SwiftProtobuf.Google_Protobuf_MethodOptions { } +// MARK: - File's ExtensionMap: Google_Api_Annotations_Extensions + /// A `SwiftProtobuf.SimpleExtensionMap` that includes all of the extensions defined by /// this .proto file. It can be used any place an `SwiftProtobuf.ExtensionMap` is needed /// in parsing, or it can be combined with other `SwiftProtobuf.SimpleExtensionMap`s to create @@ -63,6 +73,10 @@ let Google_Api_Annotations_Extensions: SwiftProtobuf.SimpleExtensionMap = [ Google_Api_Extensions_http ] +// Extension Objects - The only reason these might be needed is when manually +// constructing a `SimpleExtensionMap`, otherwise, use the above _Extension Properties_ +// accessors for the extension fields on the messages directly. + /// See `HttpRule`. let Google_Api_Extensions_http = SwiftProtobuf.MessageExtension, SwiftProtobuf.Google_Protobuf_MethodOptions>( _protobuf_fieldNumber: 72295728, diff --git a/Examples/Google/NaturalLanguage/Sources/http.pb.swift b/Examples/Google/NaturalLanguage/Sources/http.pb.swift index 1b424adf2..62b494f53 100644 --- a/Examples/Google/NaturalLanguage/Sources/http.pb.swift +++ b/Examples/Google/NaturalLanguage/Sources/http.pb.swift @@ -1,4 +1,5 @@ // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: google/api/http.proto @@ -6,7 +7,7 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -// Copyright 2019 Google LLC. +// Copyright 2015 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,7 +27,7 @@ import SwiftProtobuf // If the compiler emits an error on this type, it is because this file // was generated by a version of the `protoc` Swift plug-in that is // incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that your are building against the same version of the API +// Please ensure that you are building against the same version of the API // that was used to generate this file. fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} @@ -336,63 +337,57 @@ struct Google_Api_HttpRule { /// Selects a method to which this rule applies. /// /// Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - var selector: String { - get {return _storage._selector} - set {_uniqueStorage()._selector = newValue} - } + var selector: String = String() /// Determines the URL pattern is matched by this rules. This pattern can be /// used with any of the {get|put|post|delete|patch} methods. A custom method /// can be defined using the 'custom' field. - var pattern: OneOf_Pattern? { - get {return _storage._pattern} - set {_uniqueStorage()._pattern = newValue} - } + var pattern: Google_Api_HttpRule.OneOf_Pattern? = nil /// Maps to HTTP GET. Used for listing and getting information about /// resources. var get: String { get { - if case .get(let v)? = _storage._pattern {return v} + if case .get(let v)? = pattern {return v} return String() } - set {_uniqueStorage()._pattern = .get(newValue)} + set {pattern = .get(newValue)} } /// Maps to HTTP PUT. Used for replacing a resource. var put: String { get { - if case .put(let v)? = _storage._pattern {return v} + if case .put(let v)? = pattern {return v} return String() } - set {_uniqueStorage()._pattern = .put(newValue)} + set {pattern = .put(newValue)} } /// Maps to HTTP POST. Used for creating a resource or performing an action. var post: String { get { - if case .post(let v)? = _storage._pattern {return v} + if case .post(let v)? = pattern {return v} return String() } - set {_uniqueStorage()._pattern = .post(newValue)} + set {pattern = .post(newValue)} } /// Maps to HTTP DELETE. Used for deleting a resource. var delete: String { get { - if case .delete(let v)? = _storage._pattern {return v} + if case .delete(let v)? = pattern {return v} return String() } - set {_uniqueStorage()._pattern = .delete(newValue)} + set {pattern = .delete(newValue)} } /// Maps to HTTP PATCH. Used for updating a resource. var patch: String { get { - if case .patch(let v)? = _storage._pattern {return v} + if case .patch(let v)? = pattern {return v} return String() } - set {_uniqueStorage()._pattern = .patch(newValue)} + set {pattern = .patch(newValue)} } /// The custom pattern is used for specifying an HTTP method that is not @@ -401,10 +396,10 @@ struct Google_Api_HttpRule { /// for services that provide content to Web (HTML) clients. var custom: Google_Api_CustomHttpPattern { get { - if case .custom(let v)? = _storage._pattern {return v} + if case .custom(let v)? = pattern {return v} return Google_Api_CustomHttpPattern() } - set {_uniqueStorage()._pattern = .custom(newValue)} + set {pattern = .custom(newValue)} } /// The name of the request field whose value is mapped to the HTTP request @@ -413,10 +408,7 @@ struct Google_Api_HttpRule { /// /// NOTE: the referred field must be present at the top-level of the request /// message type. - var body: String { - get {return _storage._body} - set {_uniqueStorage()._body = newValue} - } + var body: String = String() /// Optional. The name of the response field whose value is mapped to the HTTP /// response body. When omitted, the entire response message will be used @@ -424,18 +416,12 @@ struct Google_Api_HttpRule { /// /// NOTE: The referred field must be present at the top-level of the response /// message type. - var responseBody: String { - get {return _storage._responseBody} - set {_uniqueStorage()._responseBody = newValue} - } + var responseBody: String = String() /// Additional HTTP bindings for the selector. Nested bindings must /// not contain an `additional_bindings` field themselves (that is, /// the nesting may only be one level deep). - var additionalBindings: [Google_Api_HttpRule] { - get {return _storage._additionalBindings} - set {_uniqueStorage()._additionalBindings = newValue} - } + var additionalBindings: [Google_Api_HttpRule] = [] var unknownFields = SwiftProtobuf.UnknownStorage() @@ -462,13 +448,34 @@ struct Google_Api_HttpRule { #if !swift(>=4.1) static func ==(lhs: Google_Api_HttpRule.OneOf_Pattern, rhs: Google_Api_HttpRule.OneOf_Pattern) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch (lhs, rhs) { - case (.get(let l), .get(let r)): return l == r - case (.put(let l), .put(let r)): return l == r - case (.post(let l), .post(let r)): return l == r - case (.delete(let l), .delete(let r)): return l == r - case (.patch(let l), .patch(let r)): return l == r - case (.custom(let l), .custom(let r)): return l == r + case (.get, .get): return { + guard case .get(let l) = lhs, case .get(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.put, .put): return { + guard case .put(let l) = lhs, case .put(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.post, .post): return { + guard case .post(let l) = lhs, case .post(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.delete, .delete): return { + guard case .delete(let l) = lhs, case .delete(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.patch, .patch): return { + guard case .patch(let l) = lhs, case .patch(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.custom, .custom): return { + guard case .custom(let l) = lhs, case .custom(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -476,8 +483,6 @@ struct Google_Api_HttpRule { } init() {} - - fileprivate var _storage = _StorageClass.defaultInstance } /// A custom pattern is used for defining custom HTTP verb. @@ -497,6 +502,13 @@ struct Google_Api_CustomHttpPattern { init() {} } +#if swift(>=5.5) && canImport(_Concurrency) +extension Google_Api_Http: @unchecked Sendable {} +extension Google_Api_HttpRule: @unchecked Sendable {} +extension Google_Api_HttpRule.OneOf_Pattern: @unchecked Sendable {} +extension Google_Api_CustomHttpPattern: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "google.api" @@ -510,9 +522,12 @@ extension Google_Api_Http: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeRepeatedMessageField(value: &self.rules) - case 2: try decoder.decodeSingularBoolField(value: &self.fullyDecodeReservedExpansion) + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.rules) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.fullyDecodeReservedExpansion) }() default: break } } @@ -551,130 +566,126 @@ extension Google_Api_HttpRule: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 11: .standard(proto: "additional_bindings"), ] - fileprivate class _StorageClass { - var _selector: String = String() - var _pattern: Google_Api_HttpRule.OneOf_Pattern? - var _body: String = String() - var _responseBody: String = String() - var _additionalBindings: [Google_Api_HttpRule] = [] - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _selector = source._selector - _pattern = source._pattern - _body = source._body - _responseBody = source._responseBody - _additionalBindings = source._additionalBindings - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularStringField(value: &_storage._selector) - case 2: - if _storage._pattern != nil {try decoder.handleConflictingOneOf()} - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v {_storage._pattern = .get(v)} - case 3: - if _storage._pattern != nil {try decoder.handleConflictingOneOf()} - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v {_storage._pattern = .put(v)} - case 4: - if _storage._pattern != nil {try decoder.handleConflictingOneOf()} - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v {_storage._pattern = .post(v)} - case 5: - if _storage._pattern != nil {try decoder.handleConflictingOneOf()} - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v {_storage._pattern = .delete(v)} - case 6: - if _storage._pattern != nil {try decoder.handleConflictingOneOf()} - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v {_storage._pattern = .patch(v)} - case 7: try decoder.decodeSingularStringField(value: &_storage._body) - case 8: - var v: Google_Api_CustomHttpPattern? - if let current = _storage._pattern { - try decoder.handleConflictingOneOf() - if case .custom(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v {_storage._pattern = .custom(v)} - case 11: try decoder.decodeRepeatedMessageField(value: &_storage._additionalBindings) - case 12: try decoder.decodeSingularStringField(value: &_storage._responseBody) - default: break + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.selector) }() + case 2: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.pattern != nil {try decoder.handleConflictingOneOf()} + self.pattern = .get(v) + } + }() + case 3: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.pattern != nil {try decoder.handleConflictingOneOf()} + self.pattern = .put(v) } + }() + case 4: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.pattern != nil {try decoder.handleConflictingOneOf()} + self.pattern = .post(v) + } + }() + case 5: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.pattern != nil {try decoder.handleConflictingOneOf()} + self.pattern = .delete(v) + } + }() + case 6: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.pattern != nil {try decoder.handleConflictingOneOf()} + self.pattern = .patch(v) + } + }() + case 7: try { try decoder.decodeSingularStringField(value: &self.body) }() + case 8: try { + var v: Google_Api_CustomHttpPattern? + var hadOneofValue = false + if let current = self.pattern { + hadOneofValue = true + if case .custom(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.pattern = .custom(v) + } + }() + case 11: try { try decoder.decodeRepeatedMessageField(value: &self.additionalBindings) }() + case 12: try { try decoder.decodeSingularStringField(value: &self.responseBody) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if !_storage._selector.isEmpty { - try visitor.visitSingularStringField(value: _storage._selector, fieldNumber: 1) - } - switch _storage._pattern { - case .get(let v)?: - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - case .put(let v)?: - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - case .post(let v)?: - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - case .delete(let v)?: - try visitor.visitSingularStringField(value: v, fieldNumber: 5) - case .patch(let v)?: - try visitor.visitSingularStringField(value: v, fieldNumber: 6) - case nil: break - default: break - } - if !_storage._body.isEmpty { - try visitor.visitSingularStringField(value: _storage._body, fieldNumber: 7) - } - if case .custom(let v)? = _storage._pattern { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } - if !_storage._additionalBindings.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._additionalBindings, fieldNumber: 11) - } - if !_storage._responseBody.isEmpty { - try visitor.visitSingularStringField(value: _storage._responseBody, fieldNumber: 12) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.selector.isEmpty { + try visitor.visitSingularStringField(value: self.selector, fieldNumber: 1) + } + switch self.pattern { + case .get?: try { + guard case .get(let v)? = self.pattern else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + }() + case .put?: try { + guard case .put(let v)? = self.pattern else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + }() + case .post?: try { + guard case .post(let v)? = self.pattern else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + }() + case .delete?: try { + guard case .delete(let v)? = self.pattern else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + }() + case .patch?: try { + guard case .patch(let v)? = self.pattern else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + }() + default: break + } + if !self.body.isEmpty { + try visitor.visitSingularStringField(value: self.body, fieldNumber: 7) + } + try { if case .custom(let v)? = self.pattern { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() + if !self.additionalBindings.isEmpty { + try visitor.visitRepeatedMessageField(value: self.additionalBindings, fieldNumber: 11) + } + if !self.responseBody.isEmpty { + try visitor.visitSingularStringField(value: self.responseBody, fieldNumber: 12) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Api_HttpRule, rhs: Google_Api_HttpRule) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._selector != rhs_storage._selector {return false} - if _storage._pattern != rhs_storage._pattern {return false} - if _storage._body != rhs_storage._body {return false} - if _storage._responseBody != rhs_storage._responseBody {return false} - if _storage._additionalBindings != rhs_storage._additionalBindings {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs.selector != rhs.selector {return false} + if lhs.pattern != rhs.pattern {return false} + if lhs.body != rhs.body {return false} + if lhs.responseBody != rhs.responseBody {return false} + if lhs.additionalBindings != rhs.additionalBindings {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -689,9 +700,12 @@ extension Google_Api_CustomHttpPattern: SwiftProtobuf.Message, SwiftProtobuf._Me mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularStringField(value: &self.kind) - case 2: try decoder.decodeSingularStringField(value: &self.path) + case 1: try { try decoder.decodeSingularStringField(value: &self.kind) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.path) }() default: break } } diff --git a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift b/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift index 3bc2cd0d2..ab4920f52 100644 --- a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift +++ b/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift @@ -20,172 +20,841 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import Foundation import GRPC import NIO -import NIOHTTP1 +import NIOConcurrencyHelpers import SwiftProtobuf -/// Usage: instantiate Google_Cloud_Language_V1_LanguageServiceServiceClient, then call methods of this protocol to make API calls. -internal protocol Google_Cloud_Language_V1_LanguageServiceService { - func analyzeSentiment(_ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, callOptions: CallOptions?) -> UnaryCall - func analyzeEntities(_ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, callOptions: CallOptions?) -> UnaryCall - func analyzeEntitySentiment(_ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, callOptions: CallOptions?) -> UnaryCall - func analyzeSyntax(_ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, callOptions: CallOptions?) -> UnaryCall - func classifyText(_ request: Google_Cloud_Language_V1_ClassifyTextRequest, callOptions: CallOptions?) -> UnaryCall - func annotateText(_ request: Google_Cloud_Language_V1_AnnotateTextRequest, callOptions: CallOptions?) -> UnaryCall -} +/// Provides text analysis operations such as sentiment analysis and entity +/// recognition. +/// +/// Usage: instantiate `Google_Cloud_Language_V1_LanguageServiceClient`, then call methods of this protocol to make API calls. +internal protocol Google_Cloud_Language_V1_LanguageServiceClientProtocol: GRPCClient { + var serviceName: String { get } + var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { get } -internal final class Google_Cloud_Language_V1_LanguageServiceServiceClient: GRPCClient, Google_Cloud_Language_V1_LanguageServiceService { - internal let connection: ClientConnection - internal var defaultCallOptions: CallOptions + func analyzeSentiment( + _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, + callOptions: CallOptions? + ) -> UnaryCall - /// Creates a client for the google.cloud.language.v1.LanguageService service. - /// - /// - Parameters: - /// - connection: `ClientConnection` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - internal init(connection: ClientConnection, defaultCallOptions: CallOptions = CallOptions()) { - self.connection = connection - self.defaultCallOptions = defaultCallOptions + func analyzeEntities( + _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, + callOptions: CallOptions? + ) -> UnaryCall + + func analyzeEntitySentiment( + _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, + callOptions: CallOptions? + ) -> UnaryCall + + func analyzeSyntax( + _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, + callOptions: CallOptions? + ) -> UnaryCall + + func classifyText( + _ request: Google_Cloud_Language_V1_ClassifyTextRequest, + callOptions: CallOptions? + ) -> UnaryCall + + func annotateText( + _ request: Google_Cloud_Language_V1_AnnotateTextRequest, + callOptions: CallOptions? + ) -> UnaryCall +} + +extension Google_Cloud_Language_V1_LanguageServiceClientProtocol { + internal var serviceName: String { + return "google.cloud.language.v1.LanguageService" } - /// Asynchronous unary call to AnalyzeSentiment. + /// Analyzes the sentiment of the provided text. /// /// - Parameters: /// - request: Request to send to AnalyzeSentiment. - /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - callOptions: Call options. /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeSentiment(_ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, callOptions: CallOptions? = nil) -> UnaryCall { - return self.makeUnaryCall(path: "/google.cloud.language.v1.LanguageService/AnalyzeSentiment", - request: request, - callOptions: callOptions ?? self.defaultCallOptions) + internal func analyzeSentiment( + _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [] + ) } - /// Asynchronous unary call to AnalyzeEntities. + /// Finds named entities (currently proper names and common nouns) in the text + /// along with entity types, salience, mentions for each entity, and + /// other properties. /// /// - Parameters: /// - request: Request to send to AnalyzeEntities. - /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - callOptions: Call options. /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeEntities(_ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, callOptions: CallOptions? = nil) -> UnaryCall { - return self.makeUnaryCall(path: "/google.cloud.language.v1.LanguageService/AnalyzeEntities", - request: request, - callOptions: callOptions ?? self.defaultCallOptions) + internal func analyzeEntities( + _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [] + ) } - /// Asynchronous unary call to AnalyzeEntitySentiment. + /// Finds entities, similar to + /// [AnalyzeEntities][google.cloud.language.v1.LanguageService.AnalyzeEntities] + /// in the text and analyzes sentiment associated with each entity and its + /// mentions. /// /// - Parameters: /// - request: Request to send to AnalyzeEntitySentiment. - /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - callOptions: Call options. /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeEntitySentiment(_ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, callOptions: CallOptions? = nil) -> UnaryCall { - return self.makeUnaryCall(path: "/google.cloud.language.v1.LanguageService/AnalyzeEntitySentiment", - request: request, - callOptions: callOptions ?? self.defaultCallOptions) + internal func analyzeEntitySentiment( + _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [] + ) } - /// Asynchronous unary call to AnalyzeSyntax. + /// Analyzes the syntax of the text and provides sentence boundaries and + /// tokenization along with part of speech tags, dependency trees, and other + /// properties. /// /// - Parameters: /// - request: Request to send to AnalyzeSyntax. - /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - callOptions: Call options. /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeSyntax(_ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, callOptions: CallOptions? = nil) -> UnaryCall { - return self.makeUnaryCall(path: "/google.cloud.language.v1.LanguageService/AnalyzeSyntax", - request: request, - callOptions: callOptions ?? self.defaultCallOptions) + internal func analyzeSyntax( + _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [] + ) } - /// Asynchronous unary call to ClassifyText. + /// Classifies a document into categories. /// /// - Parameters: /// - request: Request to send to ClassifyText. - /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - callOptions: Call options. /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func classifyText(_ request: Google_Cloud_Language_V1_ClassifyTextRequest, callOptions: CallOptions? = nil) -> UnaryCall { - return self.makeUnaryCall(path: "/google.cloud.language.v1.LanguageService/ClassifyText", - request: request, - callOptions: callOptions ?? self.defaultCallOptions) + internal func classifyText( + _ request: Google_Cloud_Language_V1_ClassifyTextRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [] + ) } - /// Asynchronous unary call to AnnotateText. + /// A convenience method that provides all the features that analyzeSentiment, + /// analyzeEntities, and analyzeSyntax provide in one call. /// /// - Parameters: /// - request: Request to send to AnnotateText. - /// - callOptions: Call options; `self.defaultCallOptions` is used if `nil`. + /// - callOptions: Call options. /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func annotateText(_ request: Google_Cloud_Language_V1_AnnotateTextRequest, callOptions: CallOptions? = nil) -> UnaryCall { - return self.makeUnaryCall(path: "/google.cloud.language.v1.LanguageService/AnnotateText", - request: request, - callOptions: callOptions ?? self.defaultCallOptions) + internal func annotateText( + _ request: Google_Cloud_Language_V1_AnnotateTextRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [] + ) + } +} + +#if compiler(>=5.6) +@available(*, deprecated) +extension Google_Cloud_Language_V1_LanguageServiceClient: @unchecked Sendable {} +#endif // compiler(>=5.6) + +@available(*, deprecated, renamed: "Google_Cloud_Language_V1_LanguageServiceNIOClient") +internal final class Google_Cloud_Language_V1_LanguageServiceClient: Google_Cloud_Language_V1_LanguageServiceClientProtocol { + private let lock = Lock() + private var _defaultCallOptions: CallOptions + private var _interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? + internal let channel: GRPCChannel + internal var defaultCallOptions: CallOptions { + get { self.lock.withLock { return self._defaultCallOptions } } + set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } + } + internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { + get { self.lock.withLock { return self._interceptors } } + set { self.lock.withLockVoid { self._interceptors = newValue } } + } + + /// Creates a client for the google.cloud.language.v1.LanguageService service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self._defaultCallOptions = defaultCallOptions + self._interceptors = interceptors + } +} + +internal struct Google_Cloud_Language_V1_LanguageServiceNIOClient: Google_Cloud_Language_V1_LanguageServiceClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? + + /// Creates a client for the google.cloud.language.v1.LanguageService service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +#if compiler(>=5.6) +/// Provides text analysis operations such as sentiment analysis and entity +/// recognition. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol: GRPCClient { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { get } + + func makeAnalyzeSentimentCall( + _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeAnalyzeEntitiesCall( + _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeAnalyzeEntitySentimentCall( + _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeAnalyzeSyntaxCall( + _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeClassifyTextCall( + _ request: Google_Cloud_Language_V1_ClassifyTextRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeAnnotateTextCall( + _ request: Google_Cloud_Language_V1_AnnotateTextRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Google_Cloud_Language_V1_LanguageServiceClientMetadata.serviceDescriptor + } + + internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { + return nil + } + + internal func makeAnalyzeSentimentCall( + _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [] + ) + } + + internal func makeAnalyzeEntitiesCall( + _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [] + ) + } + + internal func makeAnalyzeEntitySentimentCall( + _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [] + ) + } + + internal func makeAnalyzeSyntaxCall( + _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [] + ) + } + + internal func makeClassifyTextCall( + _ request: Google_Cloud_Language_V1_ClassifyTextRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [] + ) + } + + internal func makeAnnotateTextCall( + _ request: Google_Cloud_Language_V1_AnnotateTextRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol { + internal func analyzeSentiment( + _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, + callOptions: CallOptions? = nil + ) async throws -> Google_Cloud_Language_V1_AnalyzeSentimentResponse { + return try await self.performAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [] + ) + } + + internal func analyzeEntities( + _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, + callOptions: CallOptions? = nil + ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitiesResponse { + return try await self.performAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [] + ) + } + + internal func analyzeEntitySentiment( + _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, + callOptions: CallOptions? = nil + ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse { + return try await self.performAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [] + ) + } + + internal func analyzeSyntax( + _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, + callOptions: CallOptions? = nil + ) async throws -> Google_Cloud_Language_V1_AnalyzeSyntaxResponse { + return try await self.performAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [] + ) + } + + internal func classifyText( + _ request: Google_Cloud_Language_V1_ClassifyTextRequest, + callOptions: CallOptions? = nil + ) async throws -> Google_Cloud_Language_V1_ClassifyTextResponse { + return try await self.performAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [] + ) + } + + internal func annotateText( + _ request: Google_Cloud_Language_V1_AnnotateTextRequest, + callOptions: CallOptions? = nil + ) async throws -> Google_Cloud_Language_V1_AnnotateTextResponse { + return try await self.performAsyncUnaryCall( + path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal struct Google_Cloud_Language_V1_LanguageServiceAsyncClient: Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? + + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors } +} + +#endif // compiler(>=5.6) + +internal protocol Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol: GRPCSendable { + + /// - Returns: Interceptors to use when invoking 'analyzeSentiment'. + func makeAnalyzeSentimentInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'analyzeEntities'. + func makeAnalyzeEntitiesInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'analyzeEntitySentiment'. + func makeAnalyzeEntitySentimentInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'analyzeSyntax'. + func makeAnalyzeSyntaxInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'classifyText'. + func makeClassifyTextInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'annotateText'. + func makeAnnotateTextInterceptors() -> [ClientInterceptor] +} + +internal enum Google_Cloud_Language_V1_LanguageServiceClientMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "LanguageService", + fullName: "google.cloud.language.v1.LanguageService", + methods: [ + Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment, + Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities, + Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment, + Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax, + Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText, + Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText, + ] + ) + + internal enum Methods { + internal static let analyzeSentiment = GRPCMethodDescriptor( + name: "AnalyzeSentiment", + path: "/google.cloud.language.v1.LanguageService/AnalyzeSentiment", + type: GRPCCallType.unary + ) + + internal static let analyzeEntities = GRPCMethodDescriptor( + name: "AnalyzeEntities", + path: "/google.cloud.language.v1.LanguageService/AnalyzeEntities", + type: GRPCCallType.unary + ) + + internal static let analyzeEntitySentiment = GRPCMethodDescriptor( + name: "AnalyzeEntitySentiment", + path: "/google.cloud.language.v1.LanguageService/AnalyzeEntitySentiment", + type: GRPCCallType.unary + ) + + internal static let analyzeSyntax = GRPCMethodDescriptor( + name: "AnalyzeSyntax", + path: "/google.cloud.language.v1.LanguageService/AnalyzeSyntax", + type: GRPCCallType.unary + ) + + internal static let classifyText = GRPCMethodDescriptor( + name: "ClassifyText", + path: "/google.cloud.language.v1.LanguageService/ClassifyText", + type: GRPCCallType.unary + ) + internal static let annotateText = GRPCMethodDescriptor( + name: "AnnotateText", + path: "/google.cloud.language.v1.LanguageService/AnnotateText", + type: GRPCCallType.unary + ) + } } +/// Provides text analysis operations such as sentiment analysis and entity +/// recognition. +/// /// To build a server, implement a class that conforms to this protocol. internal protocol Google_Cloud_Language_V1_LanguageServiceProvider: CallHandlerProvider { + var interceptors: Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol? { get } + + /// Analyzes the sentiment of the provided text. func analyzeSentiment(request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, context: StatusOnlyCallContext) -> EventLoopFuture + + /// Finds named entities (currently proper names and common nouns) in the text + /// along with entity types, salience, mentions for each entity, and + /// other properties. func analyzeEntities(request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, context: StatusOnlyCallContext) -> EventLoopFuture + + /// Finds entities, similar to + /// [AnalyzeEntities][google.cloud.language.v1.LanguageService.AnalyzeEntities] + /// in the text and analyzes sentiment associated with each entity and its + /// mentions. func analyzeEntitySentiment(request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, context: StatusOnlyCallContext) -> EventLoopFuture + + /// Analyzes the syntax of the text and provides sentence boundaries and + /// tokenization along with part of speech tags, dependency trees, and other + /// properties. func analyzeSyntax(request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, context: StatusOnlyCallContext) -> EventLoopFuture + + /// Classifies a document into categories. func classifyText(request: Google_Cloud_Language_V1_ClassifyTextRequest, context: StatusOnlyCallContext) -> EventLoopFuture + + /// A convenience method that provides all the features that analyzeSentiment, + /// analyzeEntities, and analyzeSyntax provide in one call. func annotateText(request: Google_Cloud_Language_V1_AnnotateTextRequest, context: StatusOnlyCallContext) -> EventLoopFuture } extension Google_Cloud_Language_V1_LanguageServiceProvider { - internal var serviceName: String { return "google.cloud.language.v1.LanguageService" } + internal var serviceName: Substring { + return Google_Cloud_Language_V1_LanguageServiceServerMetadata.serviceDescriptor.fullName[...] + } /// Determines, calls and returns the appropriate request handler, depending on the request's method. /// Returns nil for methods not handled by this service. - internal func handleMethod(_ methodName: String, callHandlerContext: CallHandlerContext) -> GRPCCallHandler? { - switch methodName { + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { case "AnalyzeSentiment": - return UnaryCallHandler(callHandlerContext: callHandlerContext) { context in - return { request in - self.analyzeSentiment(request: request, context: context) - } - } + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [], + userFunction: self.analyzeSentiment(request:context:) + ) case "AnalyzeEntities": - return UnaryCallHandler(callHandlerContext: callHandlerContext) { context in - return { request in - self.analyzeEntities(request: request, context: context) - } - } + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [], + userFunction: self.analyzeEntities(request:context:) + ) case "AnalyzeEntitySentiment": - return UnaryCallHandler(callHandlerContext: callHandlerContext) { context in - return { request in - self.analyzeEntitySentiment(request: request, context: context) - } - } + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [], + userFunction: self.analyzeEntitySentiment(request:context:) + ) case "AnalyzeSyntax": - return UnaryCallHandler(callHandlerContext: callHandlerContext) { context in - return { request in - self.analyzeSyntax(request: request, context: context) - } - } + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [], + userFunction: self.analyzeSyntax(request:context:) + ) case "ClassifyText": - return UnaryCallHandler(callHandlerContext: callHandlerContext) { context in - return { request in - self.classifyText(request: request, context: context) - } - } + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [], + userFunction: self.classifyText(request:context:) + ) case "AnnotateText": - return UnaryCallHandler(callHandlerContext: callHandlerContext) { context in - return { request in - self.annotateText(request: request, context: context) - } - } + return UnaryServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [], + userFunction: self.annotateText(request:context:) + ) - default: return nil + default: + return nil } } } +#if compiler(>=5.6) + +/// Provides text analysis operations such as sentiment analysis and entity +/// recognition. +/// +/// To implement a server, implement an object which conforms to this protocol. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Google_Cloud_Language_V1_LanguageServiceAsyncProvider: CallHandlerProvider { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol? { get } + + /// Analyzes the sentiment of the provided text. + @Sendable func analyzeSentiment( + request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Google_Cloud_Language_V1_AnalyzeSentimentResponse + + /// Finds named entities (currently proper names and common nouns) in the text + /// along with entity types, salience, mentions for each entity, and + /// other properties. + @Sendable func analyzeEntities( + request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitiesResponse + + /// Finds entities, similar to + /// [AnalyzeEntities][google.cloud.language.v1.LanguageService.AnalyzeEntities] + /// in the text and analyzes sentiment associated with each entity and its + /// mentions. + @Sendable func analyzeEntitySentiment( + request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse + + /// Analyzes the syntax of the text and provides sentence boundaries and + /// tokenization along with part of speech tags, dependency trees, and other + /// properties. + @Sendable func analyzeSyntax( + request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Google_Cloud_Language_V1_AnalyzeSyntaxResponse + + /// Classifies a document into categories. + @Sendable func classifyText( + request: Google_Cloud_Language_V1_ClassifyTextRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Google_Cloud_Language_V1_ClassifyTextResponse + + /// A convenience method that provides all the features that analyzeSentiment, + /// analyzeEntities, and analyzeSyntax provide in one call. + @Sendable func annotateText( + request: Google_Cloud_Language_V1_AnnotateTextRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Google_Cloud_Language_V1_AnnotateTextResponse +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Google_Cloud_Language_V1_LanguageServiceAsyncProvider { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Google_Cloud_Language_V1_LanguageServiceServerMetadata.serviceDescriptor + } + + internal var serviceName: Substring { + return Google_Cloud_Language_V1_LanguageServiceServerMetadata.serviceDescriptor.fullName[...] + } + + internal var interceptors: Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol? { + return nil + } + + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "AnalyzeSentiment": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [], + wrapping: self.analyzeSentiment(request:context:) + ) + + case "AnalyzeEntities": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [], + wrapping: self.analyzeEntities(request:context:) + ) + + case "AnalyzeEntitySentiment": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [], + wrapping: self.analyzeEntitySentiment(request:context:) + ) + + case "AnalyzeSyntax": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [], + wrapping: self.analyzeSyntax(request:context:) + ) + + case "ClassifyText": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [], + wrapping: self.classifyText(request:context:) + ) + + case "AnnotateText": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [], + wrapping: self.annotateText(request:context:) + ) + + default: + return nil + } + } +} + +#endif // compiler(>=5.6) + +internal protocol Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol { + + /// - Returns: Interceptors to use when handling 'analyzeSentiment'. + /// Defaults to calling `self.makeInterceptors()`. + func makeAnalyzeSentimentInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'analyzeEntities'. + /// Defaults to calling `self.makeInterceptors()`. + func makeAnalyzeEntitiesInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'analyzeEntitySentiment'. + /// Defaults to calling `self.makeInterceptors()`. + func makeAnalyzeEntitySentimentInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'analyzeSyntax'. + /// Defaults to calling `self.makeInterceptors()`. + func makeAnalyzeSyntaxInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'classifyText'. + /// Defaults to calling `self.makeInterceptors()`. + func makeClassifyTextInterceptors() -> [ServerInterceptor] + + /// - Returns: Interceptors to use when handling 'annotateText'. + /// Defaults to calling `self.makeInterceptors()`. + func makeAnnotateTextInterceptors() -> [ServerInterceptor] +} + +internal enum Google_Cloud_Language_V1_LanguageServiceServerMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "LanguageService", + fullName: "google.cloud.language.v1.LanguageService", + methods: [ + Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeSentiment, + Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeEntities, + Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeEntitySentiment, + Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeSyntax, + Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.classifyText, + Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.annotateText, + ] + ) + + internal enum Methods { + internal static let analyzeSentiment = GRPCMethodDescriptor( + name: "AnalyzeSentiment", + path: "/google.cloud.language.v1.LanguageService/AnalyzeSentiment", + type: GRPCCallType.unary + ) + + internal static let analyzeEntities = GRPCMethodDescriptor( + name: "AnalyzeEntities", + path: "/google.cloud.language.v1.LanguageService/AnalyzeEntities", + type: GRPCCallType.unary + ) + + internal static let analyzeEntitySentiment = GRPCMethodDescriptor( + name: "AnalyzeEntitySentiment", + path: "/google.cloud.language.v1.LanguageService/AnalyzeEntitySentiment", + type: GRPCCallType.unary + ) + + internal static let analyzeSyntax = GRPCMethodDescriptor( + name: "AnalyzeSyntax", + path: "/google.cloud.language.v1.LanguageService/AnalyzeSyntax", + type: GRPCCallType.unary + ) + + internal static let classifyText = GRPCMethodDescriptor( + name: "ClassifyText", + path: "/google.cloud.language.v1.LanguageService/ClassifyText", + type: GRPCCallType.unary + ) + + internal static let annotateText = GRPCMethodDescriptor( + name: "AnnotateText", + path: "/google.cloud.language.v1.LanguageService/AnnotateText", + type: GRPCCallType.unary + ) + } +} diff --git a/Examples/Google/NaturalLanguage/Sources/language_service.pb.swift b/Examples/Google/NaturalLanguage/Sources/language_service.pb.swift index 608ee5122..b4a8c2c70 100644 --- a/Examples/Google/NaturalLanguage/Sources/language_service.pb.swift +++ b/Examples/Google/NaturalLanguage/Sources/language_service.pb.swift @@ -1,4 +1,5 @@ // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: google/cloud/language/v1/language_service.proto @@ -26,7 +27,7 @@ import SwiftProtobuf // If the compiler emits an error on this type, it is because this file // was generated by a version of the `protoc` Swift plug-in that is // incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that your are building against the same version of the API +// Please ensure that you are building against the same version of the API // that was used to generate this file. fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} @@ -142,11 +143,11 @@ struct Google_Cloud_Language_V1_Document { /// The language of the document (if not specified, the language is /// automatically detected). Both ISO and BCP-47 language codes are /// accepted.
- /// [Language Support](/natural-language/docs/languages) - /// lists currently supported languages for each API method. - /// If the language (either specified by the caller or automatically detected) - /// is not supported by the called API method, an `INVALID_ARGUMENT` error - /// is returned. + /// [Language + /// Support](https://cloud.google.com/natural-language/docs/languages) lists + /// currently supported languages for each API method. If the language (either + /// specified by the caller or automatically detected) is not supported by the + /// called API method, an `INVALID_ARGUMENT` error is returned. var language: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() @@ -165,9 +166,18 @@ struct Google_Cloud_Language_V1_Document { #if !swift(>=4.1) static func ==(lhs: Google_Cloud_Language_V1_Document.OneOf_Source, rhs: Google_Cloud_Language_V1_Document.OneOf_Source) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch (lhs, rhs) { - case (.content(let l), .content(let r)): return l == r - case (.gcsContentUri(let l), .gcsContentUri(let r)): return l == r + case (.content, .content): return { + guard case .content(let l) = lhs, case .content(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.gcsContentUri, .gcsContentUri): return { + guard case .gcsContentUri(let l) = lhs, case .gcsContentUri(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -236,31 +246,32 @@ struct Google_Cloud_Language_V1_Sentence { /// The sentence text. var text: Google_Cloud_Language_V1_TextSpan { - get {return _storage._text ?? Google_Cloud_Language_V1_TextSpan()} - set {_uniqueStorage()._text = newValue} + get {return _text ?? Google_Cloud_Language_V1_TextSpan()} + set {_text = newValue} } /// Returns true if `text` has been explicitly set. - var hasText: Bool {return _storage._text != nil} + var hasText: Bool {return self._text != nil} /// Clears the value of `text`. Subsequent reads from it will return its default value. - mutating func clearText() {_uniqueStorage()._text = nil} + mutating func clearText() {self._text = nil} /// For calls to [AnalyzeSentiment][] or if - /// [AnnotateTextRequest.Features.extract_document_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_document_sentiment] is set to - /// true, this field will contain the sentiment for the sentence. + /// [AnnotateTextRequest.Features.extract_document_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_document_sentiment] + /// is set to true, this field will contain the sentiment for the sentence. var sentiment: Google_Cloud_Language_V1_Sentiment { - get {return _storage._sentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_uniqueStorage()._sentiment = newValue} + get {return _sentiment ?? Google_Cloud_Language_V1_Sentiment()} + set {_sentiment = newValue} } /// Returns true if `sentiment` has been explicitly set. - var hasSentiment: Bool {return _storage._sentiment != nil} + var hasSentiment: Bool {return self._sentiment != nil} /// Clears the value of `sentiment`. Subsequent reads from it will return its default value. - mutating func clearSentiment() {_uniqueStorage()._sentiment = nil} + mutating func clearSentiment() {self._sentiment = nil} var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _text: Google_Cloud_Language_V1_TextSpan? = nil + fileprivate var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil } /// Represents a phrase in the text that is a known entity, such as @@ -272,26 +283,17 @@ struct Google_Cloud_Language_V1_Entity { // methods supported on all messages. /// The representative name for the entity. - var name: String { - get {return _storage._name} - set {_uniqueStorage()._name = newValue} - } + var name: String = String() /// The entity type. - var type: Google_Cloud_Language_V1_Entity.TypeEnum { - get {return _storage._type} - set {_uniqueStorage()._type = newValue} - } + var type: Google_Cloud_Language_V1_Entity.TypeEnum = .unknown /// Metadata associated with the entity. /// /// For most entity types, the metadata is a Wikipedia URL (`wikipedia_url`) /// and Knowledge Graph MID (`mid`), if they are available. For the metadata /// associated with other entity types, see the Type table below. - var metadata: Dictionary { - get {return _storage._metadata} - set {_uniqueStorage()._metadata = newValue} - } + var metadata: Dictionary = [:] /// The salience score associated with the entity in the [0, 1.0] range. /// @@ -299,30 +301,24 @@ struct Google_Cloud_Language_V1_Entity { /// importance or centrality of that entity to the entire document text. /// Scores closer to 0 are less salient, while scores closer to 1.0 are highly /// salient. - var salience: Float { - get {return _storage._salience} - set {_uniqueStorage()._salience = newValue} - } + var salience: Float = 0 /// The mentions of this entity in the input document. The API currently /// supports proper noun mentions. - var mentions: [Google_Cloud_Language_V1_EntityMention] { - get {return _storage._mentions} - set {_uniqueStorage()._mentions = newValue} - } + var mentions: [Google_Cloud_Language_V1_EntityMention] = [] /// For calls to [AnalyzeEntitySentiment][] or if - /// [AnnotateTextRequest.Features.extract_entity_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entity_sentiment] is set to - /// true, this field will contain the aggregate sentiment expressed for this - /// entity in the provided document. + /// [AnnotateTextRequest.Features.extract_entity_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entity_sentiment] + /// is set to true, this field will contain the aggregate sentiment expressed + /// for this entity in the provided document. var sentiment: Google_Cloud_Language_V1_Sentiment { - get {return _storage._sentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_uniqueStorage()._sentiment = newValue} + get {return _sentiment ?? Google_Cloud_Language_V1_Sentiment()} + set {_sentiment = newValue} } /// Returns true if `sentiment` has been explicitly set. - var hasSentiment: Bool {return _storage._sentiment != nil} + var hasSentiment: Bool {return self._sentiment != nil} /// Clears the value of `sentiment`. Subsequent reads from it will return its default value. - mutating func clearSentiment() {_uniqueStorage()._sentiment = nil} + mutating func clearSentiment() {self._sentiment = nil} var unknownFields = SwiftProtobuf.UnknownStorage() @@ -357,44 +353,53 @@ struct Google_Cloud_Language_V1_Entity { /// Other types of entities case other // = 7 - /// Phone number

+ /// Phone number + /// /// The metadata lists the phone number, formatted according to local - /// convention, plus whichever additional elements appear in the text:

    - ///
  • number – the actual number, broken down into - /// sections as per local convention
  • national_prefix - /// – country code, if detected
  • area_code – - /// region or area code, if detected
  • extension – - /// phone extension (to be dialed after connection), if detected
+ /// convention, plus whichever additional elements appear in the text: + /// + /// * `number` - the actual number, broken down into sections as per local + /// convention + /// * `national_prefix` - country code, if detected + /// * `area_code` - region or area code, if detected + /// * `extension` - phone extension (to be dialed after connection), if + /// detected case phoneNumber // = 9 - /// Address

+ /// Address + /// /// The metadata identifies the street number and locality plus whichever - /// additional elements appear in the text:
    - ///
  • street_number – street number
  • - ///
  • locality – city or town
  • - ///
  • street_name – street/route name, if detected
  • - ///
  • postal_code – postal code, if detected
  • - ///
  • country – country, if detected
  • - ///
  • broad_region – administrative area, such as the - /// state, if detected
  • narrow_region – smaller - /// administrative area, such as county, if detected
  • - ///
  • sublocality – used in Asian addresses to demark a - /// district within a city, if detected
+ /// additional elements appear in the text: + /// + /// * `street_number` - street number + /// * `locality` - city or town + /// * `street_name` - street/route name, if detected + /// * `postal_code` - postal code, if detected + /// * `country` - country, if detected< + /// * `broad_region` - administrative area, such as the state, if detected + /// * `narrow_region` - smaller administrative area, such as county, if + /// detected + /// * `sublocality` - used in Asian addresses to demark a district within a + /// city, if detected case address // = 10 - /// Date

- /// The metadata identifies the components of the date:
    - ///
  • year – four digit year, if detected
  • - ///
  • month – two digit month number, if detected
  • - ///
  • day – two digit day number, if detected
+ /// Date + /// + /// The metadata identifies the components of the date: + /// + /// * `year` - four digit year, if detected + /// * `month` - two digit month number, if detected + /// * `day` - two digit day number, if detected case date // = 11 - /// Number

+ /// Number + /// /// The metadata is the number itself. case number // = 12 - /// Price

- /// The metadata identifies the value and currency. + /// Price + /// + /// The metadata identifies the `value` and `currency`. case price // = 13 case UNRECOGNIZED(Int) @@ -444,7 +449,7 @@ struct Google_Cloud_Language_V1_Entity { init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil } #if swift(>=4.2) @@ -2003,32 +2008,29 @@ struct Google_Cloud_Language_V1_EntityMention { /// The mention text. var text: Google_Cloud_Language_V1_TextSpan { - get {return _storage._text ?? Google_Cloud_Language_V1_TextSpan()} - set {_uniqueStorage()._text = newValue} + get {return _text ?? Google_Cloud_Language_V1_TextSpan()} + set {_text = newValue} } /// Returns true if `text` has been explicitly set. - var hasText: Bool {return _storage._text != nil} + var hasText: Bool {return self._text != nil} /// Clears the value of `text`. Subsequent reads from it will return its default value. - mutating func clearText() {_uniqueStorage()._text = nil} + mutating func clearText() {self._text = nil} /// The type of the entity mention. - var type: Google_Cloud_Language_V1_EntityMention.TypeEnum { - get {return _storage._type} - set {_uniqueStorage()._type = newValue} - } + var type: Google_Cloud_Language_V1_EntityMention.TypeEnum = .unknown /// For calls to [AnalyzeEntitySentiment][] or if - /// [AnnotateTextRequest.Features.extract_entity_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entity_sentiment] is set to - /// true, this field will contain the sentiment expressed for this mention of - /// the entity in the provided document. + /// [AnnotateTextRequest.Features.extract_entity_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entity_sentiment] + /// is set to true, this field will contain the sentiment expressed for this + /// mention of the entity in the provided document. var sentiment: Google_Cloud_Language_V1_Sentiment { - get {return _storage._sentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_uniqueStorage()._sentiment = newValue} + get {return _sentiment ?? Google_Cloud_Language_V1_Sentiment()} + set {_sentiment = newValue} } /// Returns true if `sentiment` has been explicitly set. - var hasSentiment: Bool {return _storage._sentiment != nil} + var hasSentiment: Bool {return self._sentiment != nil} /// Clears the value of `sentiment`. Subsequent reads from it will return its default value. - mutating func clearSentiment() {_uniqueStorage()._sentiment = nil} + mutating func clearSentiment() {self._sentiment = nil} var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2072,7 +2074,8 @@ struct Google_Cloud_Language_V1_EntityMention { init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _text: Google_Cloud_Language_V1_TextSpan? = nil + fileprivate var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil } #if swift(>=4.2) @@ -2098,7 +2101,9 @@ struct Google_Cloud_Language_V1_TextSpan { var content: String = String() /// The API calculates the beginning offset of the content in the original - /// document according to the [EncodingType][google.cloud.language.v1.EncodingType] specified in the API request. + /// document according to the + /// [EncodingType][google.cloud.language.v1.EncodingType] specified in the API + /// request. var beginOffset: Int32 = 0 var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2113,7 +2118,7 @@ struct Google_Cloud_Language_V1_ClassificationCategory { // methods supported on all messages. /// The name of the category representing the document, from the [predefined - /// taxonomy](/natural-language/docs/categories). + /// taxonomy](https://cloud.google.com/natural-language/docs/categories). var name: String = String() /// The classifier's confidence of the category. Number represents how certain @@ -2125,33 +2130,172 @@ struct Google_Cloud_Language_V1_ClassificationCategory { init() {} } +/// Model options available for classification requests. +struct Google_Cloud_Language_V1_ClassificationModelOptions { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// If this field is not set, then the `v1_model` will be used by default. + var modelType: Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType? = nil + + /// Setting this field will use the V1 model and V1 content categories + /// version. The V1 model is a legacy model; support for this will be + /// discontinued in the future. + var v1Model: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model { + get { + if case .v1Model(let v)? = modelType {return v} + return Google_Cloud_Language_V1_ClassificationModelOptions.V1Model() + } + set {modelType = .v1Model(newValue)} + } + + /// Setting this field will use the V2 model with the appropriate content + /// categories version. The V2 model is a better performing model. + var v2Model: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model { + get { + if case .v2Model(let v)? = modelType {return v} + return Google_Cloud_Language_V1_ClassificationModelOptions.V2Model() + } + set {modelType = .v2Model(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// If this field is not set, then the `v1_model` will be used by default. + enum OneOf_ModelType: Equatable { + /// Setting this field will use the V1 model and V1 content categories + /// version. The V1 model is a legacy model; support for this will be + /// discontinued in the future. + case v1Model(Google_Cloud_Language_V1_ClassificationModelOptions.V1Model) + /// Setting this field will use the V2 model with the appropriate content + /// categories version. The V2 model is a better performing model. + case v2Model(Google_Cloud_Language_V1_ClassificationModelOptions.V2Model) + + #if !swift(>=4.1) + static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType, rhs: Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.v1Model, .v1Model): return { + guard case .v1Model(let l) = lhs, case .v1Model(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.v2Model, .v2Model): return { + guard case .v2Model(let l) = lhs, case .v2Model(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + /// Options for the V1 model. + struct V1Model { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + /// Options for the V2 model. + struct V2Model { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The content categories used for classification. + var contentCategoriesVersion: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion = .unspecified + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The content categories used for classification. + enum ContentCategoriesVersion: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// If `ContentCategoriesVersion` is not specified, this option will + /// default to `V1`. + case unspecified // = 0 + + /// Legacy content categories of our initial launch in 2017. + case v1 // = 1 + + /// Updated content categories in 2022. + case v2 // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .unspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .v1 + case 2: self = .v2 + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unspecified: return 0 + case .v1: return 1 + case .v2: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} + } + + init() {} +} + +#if swift(>=4.2) + +extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion] = [ + .unspecified, + .v1, + .v2, + ] +} + +#endif // swift(>=4.2) + /// The sentiment analysis request message. struct Google_Cloud_Language_V1_AnalyzeSentimentRequest { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Input document. + /// Required. Input document. var document: Google_Cloud_Language_V1_Document { - get {return _storage._document ?? Google_Cloud_Language_V1_Document()} - set {_uniqueStorage()._document = newValue} + get {return _document ?? Google_Cloud_Language_V1_Document()} + set {_document = newValue} } /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return _storage._document != nil} + var hasDocument: Bool {return self._document != nil} /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {_uniqueStorage()._document = nil} + mutating func clearDocument() {self._document = nil} /// The encoding type used by the API to calculate sentence offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType { - get {return _storage._encodingType} - set {_uniqueStorage()._encodingType = newValue} - } + var encodingType: Google_Cloud_Language_V1_EncodingType = .none var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _document: Google_Cloud_Language_V1_Document? = nil } /// The sentiment analysis response message. @@ -2162,33 +2306,28 @@ struct Google_Cloud_Language_V1_AnalyzeSentimentResponse { /// The overall sentiment of the input document. var documentSentiment: Google_Cloud_Language_V1_Sentiment { - get {return _storage._documentSentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_uniqueStorage()._documentSentiment = newValue} + get {return _documentSentiment ?? Google_Cloud_Language_V1_Sentiment()} + set {_documentSentiment = newValue} } /// Returns true if `documentSentiment` has been explicitly set. - var hasDocumentSentiment: Bool {return _storage._documentSentiment != nil} + var hasDocumentSentiment: Bool {return self._documentSentiment != nil} /// Clears the value of `documentSentiment`. Subsequent reads from it will return its default value. - mutating func clearDocumentSentiment() {_uniqueStorage()._documentSentiment = nil} + mutating func clearDocumentSentiment() {self._documentSentiment = nil} /// The language of the text, which will be the same as the language specified /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field for more details. - var language: String { - get {return _storage._language} - set {_uniqueStorage()._language = newValue} - } + /// See [Document.language][google.cloud.language.v1.Document.language] field + /// for more details. + var language: String = String() /// The sentiment for all the sentences in the document. - var sentences: [Google_Cloud_Language_V1_Sentence] { - get {return _storage._sentences} - set {_uniqueStorage()._sentences = newValue} - } + var sentences: [Google_Cloud_Language_V1_Sentence] = [] var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _documentSentiment: Google_Cloud_Language_V1_Sentiment? = nil } /// The entity-level sentiment analysis request message. @@ -2197,27 +2336,24 @@ struct Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Input document. + /// Required. Input document. var document: Google_Cloud_Language_V1_Document { - get {return _storage._document ?? Google_Cloud_Language_V1_Document()} - set {_uniqueStorage()._document = newValue} + get {return _document ?? Google_Cloud_Language_V1_Document()} + set {_document = newValue} } /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return _storage._document != nil} + var hasDocument: Bool {return self._document != nil} /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {_uniqueStorage()._document = nil} + mutating func clearDocument() {self._document = nil} /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType { - get {return _storage._encodingType} - set {_uniqueStorage()._encodingType = newValue} - } + var encodingType: Google_Cloud_Language_V1_EncodingType = .none var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _document: Google_Cloud_Language_V1_Document? = nil } /// The entity-level sentiment analysis response message. @@ -2231,7 +2367,8 @@ struct Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse { /// The language of the text, which will be the same as the language specified /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field for more details. + /// See [Document.language][google.cloud.language.v1.Document.language] field + /// for more details. var language: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2245,27 +2382,24 @@ struct Google_Cloud_Language_V1_AnalyzeEntitiesRequest { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Input document. + /// Required. Input document. var document: Google_Cloud_Language_V1_Document { - get {return _storage._document ?? Google_Cloud_Language_V1_Document()} - set {_uniqueStorage()._document = newValue} + get {return _document ?? Google_Cloud_Language_V1_Document()} + set {_document = newValue} } /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return _storage._document != nil} + var hasDocument: Bool {return self._document != nil} /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {_uniqueStorage()._document = nil} + mutating func clearDocument() {self._document = nil} /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType { - get {return _storage._encodingType} - set {_uniqueStorage()._encodingType = newValue} - } + var encodingType: Google_Cloud_Language_V1_EncodingType = .none var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _document: Google_Cloud_Language_V1_Document? = nil } /// The entity analysis response message. @@ -2279,7 +2413,8 @@ struct Google_Cloud_Language_V1_AnalyzeEntitiesResponse { /// The language of the text, which will be the same as the language specified /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field for more details. + /// See [Document.language][google.cloud.language.v1.Document.language] field + /// for more details. var language: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2293,27 +2428,24 @@ struct Google_Cloud_Language_V1_AnalyzeSyntaxRequest { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Input document. + /// Required. Input document. var document: Google_Cloud_Language_V1_Document { - get {return _storage._document ?? Google_Cloud_Language_V1_Document()} - set {_uniqueStorage()._document = newValue} + get {return _document ?? Google_Cloud_Language_V1_Document()} + set {_document = newValue} } /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return _storage._document != nil} + var hasDocument: Bool {return self._document != nil} /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {_uniqueStorage()._document = nil} + mutating func clearDocument() {self._document = nil} /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType { - get {return _storage._encodingType} - set {_uniqueStorage()._encodingType = newValue} - } + var encodingType: Google_Cloud_Language_V1_EncodingType = .none var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _document: Google_Cloud_Language_V1_Document? = nil } /// The syntax analysis response message. @@ -2330,7 +2462,8 @@ struct Google_Cloud_Language_V1_AnalyzeSyntaxResponse { /// The language of the text, which will be the same as the language specified /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field for more details. + /// See [Document.language][google.cloud.language.v1.Document.language] field + /// for more details. var language: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2344,21 +2477,33 @@ struct Google_Cloud_Language_V1_ClassifyTextRequest { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Input document. + /// Required. Input document. var document: Google_Cloud_Language_V1_Document { - get {return _storage._document ?? Google_Cloud_Language_V1_Document()} - set {_uniqueStorage()._document = newValue} + get {return _document ?? Google_Cloud_Language_V1_Document()} + set {_document = newValue} } /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return _storage._document != nil} + var hasDocument: Bool {return self._document != nil} /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {_uniqueStorage()._document = nil} + mutating func clearDocument() {self._document = nil} + + /// Model options to use for classification. Defaults to v1 options if not + /// specified. + var classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions { + get {return _classificationModelOptions ?? Google_Cloud_Language_V1_ClassificationModelOptions()} + set {_classificationModelOptions = newValue} + } + /// Returns true if `classificationModelOptions` has been explicitly set. + var hasClassificationModelOptions: Bool {return self._classificationModelOptions != nil} + /// Clears the value of `classificationModelOptions`. Subsequent reads from it will return its default value. + mutating func clearClassificationModelOptions() {self._classificationModelOptions = nil} var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _document: Google_Cloud_Language_V1_Document? = nil + fileprivate var _classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions? = nil } /// The document classification response message. @@ -2382,31 +2527,28 @@ struct Google_Cloud_Language_V1_AnnotateTextRequest { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Input document. + /// Required. Input document. var document: Google_Cloud_Language_V1_Document { - get {return _storage._document ?? Google_Cloud_Language_V1_Document()} - set {_uniqueStorage()._document = newValue} + get {return _document ?? Google_Cloud_Language_V1_Document()} + set {_document = newValue} } /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return _storage._document != nil} + var hasDocument: Bool {return self._document != nil} /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {_uniqueStorage()._document = nil} + mutating func clearDocument() {self._document = nil} - /// The enabled features. + /// Required. The enabled features. var features: Google_Cloud_Language_V1_AnnotateTextRequest.Features { - get {return _storage._features ?? Google_Cloud_Language_V1_AnnotateTextRequest.Features()} - set {_uniqueStorage()._features = newValue} + get {return _features ?? Google_Cloud_Language_V1_AnnotateTextRequest.Features()} + set {_features = newValue} } /// Returns true if `features` has been explicitly set. - var hasFeatures: Bool {return _storage._features != nil} + var hasFeatures: Bool {return self._features != nil} /// Clears the value of `features`. Subsequent reads from it will return its default value. - mutating func clearFeatures() {_uniqueStorage()._features = nil} + mutating func clearFeatures() {self._features = nil} /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType { - get {return _storage._encodingType} - set {_uniqueStorage()._encodingType = newValue} - } + var encodingType: Google_Cloud_Language_V1_EncodingType = .none var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2432,14 +2574,28 @@ struct Google_Cloud_Language_V1_AnnotateTextRequest { /// Classify the full document into categories. var classifyText: Bool = false + /// The model options to use for classification. Defaults to v1 options + /// if not specified. Only used if `classify_text` is set to true. + var classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions { + get {return _classificationModelOptions ?? Google_Cloud_Language_V1_ClassificationModelOptions()} + set {_classificationModelOptions = newValue} + } + /// Returns true if `classificationModelOptions` has been explicitly set. + var hasClassificationModelOptions: Bool {return self._classificationModelOptions != nil} + /// Clears the value of `classificationModelOptions`. Subsequent reads from it will return its default value. + mutating func clearClassificationModelOptions() {self._classificationModelOptions = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} + + fileprivate var _classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions? = nil } init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _document: Google_Cloud_Language_V1_Document? = nil + fileprivate var _features: Google_Cloud_Language_V1_AnnotateTextRequest.Features? = nil } /// The text annotations response message. @@ -2450,59 +2606,94 @@ struct Google_Cloud_Language_V1_AnnotateTextResponse { /// Sentences in the input document. Populated if the user enables /// [AnnotateTextRequest.Features.extract_syntax][google.cloud.language.v1.AnnotateTextRequest.Features.extract_syntax]. - var sentences: [Google_Cloud_Language_V1_Sentence] { - get {return _storage._sentences} - set {_uniqueStorage()._sentences = newValue} - } + var sentences: [Google_Cloud_Language_V1_Sentence] = [] /// Tokens, along with their syntactic information, in the input document. /// Populated if the user enables /// [AnnotateTextRequest.Features.extract_syntax][google.cloud.language.v1.AnnotateTextRequest.Features.extract_syntax]. - var tokens: [Google_Cloud_Language_V1_Token] { - get {return _storage._tokens} - set {_uniqueStorage()._tokens = newValue} - } + var tokens: [Google_Cloud_Language_V1_Token] = [] /// Entities, along with their semantic information, in the input document. /// Populated if the user enables /// [AnnotateTextRequest.Features.extract_entities][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entities]. - var entities: [Google_Cloud_Language_V1_Entity] { - get {return _storage._entities} - set {_uniqueStorage()._entities = newValue} - } + var entities: [Google_Cloud_Language_V1_Entity] = [] /// The overall sentiment for the document. Populated if the user enables /// [AnnotateTextRequest.Features.extract_document_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_document_sentiment]. var documentSentiment: Google_Cloud_Language_V1_Sentiment { - get {return _storage._documentSentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_uniqueStorage()._documentSentiment = newValue} + get {return _documentSentiment ?? Google_Cloud_Language_V1_Sentiment()} + set {_documentSentiment = newValue} } /// Returns true if `documentSentiment` has been explicitly set. - var hasDocumentSentiment: Bool {return _storage._documentSentiment != nil} + var hasDocumentSentiment: Bool {return self._documentSentiment != nil} /// Clears the value of `documentSentiment`. Subsequent reads from it will return its default value. - mutating func clearDocumentSentiment() {_uniqueStorage()._documentSentiment = nil} + mutating func clearDocumentSentiment() {self._documentSentiment = nil} /// The language of the text, which will be the same as the language specified /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field for more details. - var language: String { - get {return _storage._language} - set {_uniqueStorage()._language = newValue} - } + /// See [Document.language][google.cloud.language.v1.Document.language] field + /// for more details. + var language: String = String() /// Categories identified in the input document. - var categories: [Google_Cloud_Language_V1_ClassificationCategory] { - get {return _storage._categories} - set {_uniqueStorage()._categories = newValue} - } + var categories: [Google_Cloud_Language_V1_ClassificationCategory] = [] var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _storage = _StorageClass.defaultInstance + fileprivate var _documentSentiment: Google_Cloud_Language_V1_Sentiment? = nil } +#if swift(>=5.5) && canImport(_Concurrency) +extension Google_Cloud_Language_V1_EncodingType: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Document: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Document.OneOf_Source: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Document.TypeEnum: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Sentence: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Entity: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Entity.TypeEnum: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Token: @unchecked Sendable {} +extension Google_Cloud_Language_V1_Sentiment: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Tag: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Aspect: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Case: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Form: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Gender: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Mood: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Number: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Person: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Proper: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Reciprocity: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Tense: @unchecked Sendable {} +extension Google_Cloud_Language_V1_PartOfSpeech.Voice: @unchecked Sendable {} +extension Google_Cloud_Language_V1_DependencyEdge: @unchecked Sendable {} +extension Google_Cloud_Language_V1_DependencyEdge.Label: @unchecked Sendable {} +extension Google_Cloud_Language_V1_EntityMention: @unchecked Sendable {} +extension Google_Cloud_Language_V1_EntityMention.TypeEnum: @unchecked Sendable {} +extension Google_Cloud_Language_V1_TextSpan: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassificationCategory: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassificationModelOptions: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassificationModelOptions.V1Model: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeSentimentRequest: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeSentimentResponse: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeEntitiesRequest: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeEntitiesResponse: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeSyntaxRequest: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnalyzeSyntaxResponse: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassifyTextRequest: @unchecked Sendable {} +extension Google_Cloud_Language_V1_ClassifyTextResponse: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnnotateTextRequest: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnnotateTextRequest.Features: @unchecked Sendable {} +extension Google_Cloud_Language_V1_AnnotateTextResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "google.cloud.language.v1" @@ -2527,33 +2718,50 @@ extension Google_Cloud_Language_V1_Document: SwiftProtobuf.Message, SwiftProtobu mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularEnumField(value: &self.type) - case 2: - if self.source != nil {try decoder.handleConflictingOneOf()} + case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() + case 2: try { var v: String? try decoder.decodeSingularStringField(value: &v) - if let v = v {self.source = .content(v)} - case 3: - if self.source != nil {try decoder.handleConflictingOneOf()} + if let v = v { + if self.source != nil {try decoder.handleConflictingOneOf()} + self.source = .content(v) + } + }() + case 3: try { var v: String? try decoder.decodeSingularStringField(value: &v) - if let v = v {self.source = .gcsContentUri(v)} - case 4: try decoder.decodeSingularStringField(value: &self.language) + if let v = v { + if self.source != nil {try decoder.handleConflictingOneOf()} + self.source = .gcsContentUri(v) + } + }() + case 4: try { try decoder.decodeSingularStringField(value: &self.language) }() default: break } } } func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if self.type != .unspecified { try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) } switch self.source { - case .content(let v)?: + case .content?: try { + guard case .content(let v)? = self.source else { preconditionFailure() } try visitor.visitSingularStringField(value: v, fieldNumber: 2) - case .gcsContentUri(let v)?: + }() + case .gcsContentUri?: try { + guard case .gcsContentUri(let v)? = self.source else { preconditionFailure() } try visitor.visitSingularStringField(value: v, fieldNumber: 3) + }() case nil: break } if !self.language.isEmpty { @@ -2586,63 +2794,36 @@ extension Google_Cloud_Language_V1_Sentence: SwiftProtobuf.Message, SwiftProtobu 2: .same(proto: "sentiment"), ] - fileprivate class _StorageClass { - var _text: Google_Cloud_Language_V1_TextSpan? = nil - var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _text = source._text - _sentiment = source._sentiment - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._text) - case 2: try decoder.decodeSingularMessageField(value: &_storage._sentiment) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._text) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._sentiment) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._text { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if let v = _storage._sentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._text { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._sentiment { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_Sentence, rhs: Google_Cloud_Language_V1_Sentence) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._text != rhs_storage._text {return false} - if _storage._sentiment != rhs_storage._sentiment {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._text != rhs._text {return false} + if lhs._sentiment != rhs._sentiment {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2659,91 +2840,56 @@ extension Google_Cloud_Language_V1_Entity: SwiftProtobuf.Message, SwiftProtobuf. 6: .same(proto: "sentiment"), ] - fileprivate class _StorageClass { - var _name: String = String() - var _type: Google_Cloud_Language_V1_Entity.TypeEnum = .unknown - var _metadata: Dictionary = [:] - var _salience: Float = 0 - var _mentions: [Google_Cloud_Language_V1_EntityMention] = [] - var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _name = source._name - _type = source._type - _metadata = source._metadata - _salience = source._salience - _mentions = source._mentions - _sentiment = source._sentiment - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularStringField(value: &_storage._name) - case 2: try decoder.decodeSingularEnumField(value: &_storage._type) - case 3: try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &_storage._metadata) - case 4: try decoder.decodeSingularFloatField(value: &_storage._salience) - case 5: try decoder.decodeRepeatedMessageField(value: &_storage._mentions) - case 6: try decoder.decodeSingularMessageField(value: &_storage._sentiment) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.type) }() + case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.metadata) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self.salience) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.mentions) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._sentiment) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if !_storage._name.isEmpty { - try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 1) - } - if _storage._type != .unknown { - try visitor.visitSingularEnumField(value: _storage._type, fieldNumber: 2) - } - if !_storage._metadata.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: _storage._metadata, fieldNumber: 3) - } - if _storage._salience != 0 { - try visitor.visitSingularFloatField(value: _storage._salience, fieldNumber: 4) - } - if !_storage._mentions.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._mentions, fieldNumber: 5) - } - if let v = _storage._sentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) } + if self.type != .unknown { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 2) + } + if !self.metadata.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.metadata, fieldNumber: 3) + } + if self.salience != 0 { + try visitor.visitSingularFloatField(value: self.salience, fieldNumber: 4) + } + if !self.mentions.isEmpty { + try visitor.visitRepeatedMessageField(value: self.mentions, fieldNumber: 5) + } + try { if let v = self._sentiment { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_Entity, rhs: Google_Cloud_Language_V1_Entity) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._name != rhs_storage._name {return false} - if _storage._type != rhs_storage._type {return false} - if _storage._metadata != rhs_storage._metadata {return false} - if _storage._salience != rhs_storage._salience {return false} - if _storage._mentions != rhs_storage._mentions {return false} - if _storage._sentiment != rhs_storage._sentiment {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs.name != rhs.name {return false} + if lhs.type != rhs.type {return false} + if lhs.metadata != rhs.metadata {return false} + if lhs.salience != rhs.salience {return false} + if lhs.mentions != rhs.mentions {return false} + if lhs._sentiment != rhs._sentiment {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -2805,11 +2951,14 @@ extension Google_Cloud_Language_V1_Token: SwiftProtobuf.Message, SwiftProtobuf._ _ = _uniqueStorage() try withExtendedLifetime(_storage) { (_storage: _StorageClass) in while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._text) - case 2: try decoder.decodeSingularMessageField(value: &_storage._partOfSpeech) - case 3: try decoder.decodeSingularMessageField(value: &_storage._dependencyEdge) - case 4: try decoder.decodeSingularStringField(value: &_storage._lemma) + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._text) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._partOfSpeech) }() + case 3: try { try decoder.decodeSingularMessageField(value: &_storage._dependencyEdge) }() + case 4: try { try decoder.decodeSingularStringField(value: &_storage._lemma) }() default: break } } @@ -2818,15 +2967,19 @@ extension Google_Cloud_Language_V1_Token: SwiftProtobuf.Message, SwiftProtobuf._ func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._text { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._text { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if let v = _storage._partOfSpeech { + } }() + try { if let v = _storage._partOfSpeech { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } - if let v = _storage._dependencyEdge { + } }() + try { if let v = _storage._dependencyEdge { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } + } }() if !_storage._lemma.isEmpty { try visitor.visitSingularStringField(value: _storage._lemma, fieldNumber: 4) } @@ -2861,9 +3014,12 @@ extension Google_Cloud_Language_V1_Sentiment: SwiftProtobuf.Message, SwiftProtob mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 2: try decoder.decodeSingularFloatField(value: &self.magnitude) - case 3: try decoder.decodeSingularFloatField(value: &self.score) + case 2: try { try decoder.decodeSingularFloatField(value: &self.magnitude) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self.score) }() default: break } } @@ -2906,19 +3062,22 @@ extension Google_Cloud_Language_V1_PartOfSpeech: SwiftProtobuf.Message, SwiftPro mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularEnumField(value: &self.tag) - case 2: try decoder.decodeSingularEnumField(value: &self.aspect) - case 3: try decoder.decodeSingularEnumField(value: &self.`case`) - case 4: try decoder.decodeSingularEnumField(value: &self.form) - case 5: try decoder.decodeSingularEnumField(value: &self.gender) - case 6: try decoder.decodeSingularEnumField(value: &self.mood) - case 7: try decoder.decodeSingularEnumField(value: &self.number) - case 8: try decoder.decodeSingularEnumField(value: &self.person) - case 9: try decoder.decodeSingularEnumField(value: &self.proper) - case 10: try decoder.decodeSingularEnumField(value: &self.reciprocity) - case 11: try decoder.decodeSingularEnumField(value: &self.tense) - case 12: try decoder.decodeSingularEnumField(value: &self.voice) + case 1: try { try decoder.decodeSingularEnumField(value: &self.tag) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.aspect) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.`case`) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.form) }() + case 5: try { try decoder.decodeSingularEnumField(value: &self.gender) }() + case 6: try { try decoder.decodeSingularEnumField(value: &self.mood) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.number) }() + case 8: try { try decoder.decodeSingularEnumField(value: &self.person) }() + case 9: try { try decoder.decodeSingularEnumField(value: &self.proper) }() + case 10: try { try decoder.decodeSingularEnumField(value: &self.reciprocity) }() + case 11: try { try decoder.decodeSingularEnumField(value: &self.tense) }() + case 12: try { try decoder.decodeSingularEnumField(value: &self.voice) }() default: break } } @@ -3133,9 +3292,12 @@ extension Google_Cloud_Language_V1_DependencyEdge: SwiftProtobuf.Message, SwiftP mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularInt32Field(value: &self.headTokenIndex) - case 2: try decoder.decodeSingularEnumField(value: &self.label) + case 1: try { try decoder.decodeSingularInt32Field(value: &self.headTokenIndex) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.label) }() default: break } } @@ -3255,70 +3417,41 @@ extension Google_Cloud_Language_V1_EntityMention: SwiftProtobuf.Message, SwiftPr 3: .same(proto: "sentiment"), ] - fileprivate class _StorageClass { - var _text: Google_Cloud_Language_V1_TextSpan? = nil - var _type: Google_Cloud_Language_V1_EntityMention.TypeEnum = .unknown - var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _text = source._text - _type = source._type - _sentiment = source._sentiment - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._text) - case 2: try decoder.decodeSingularEnumField(value: &_storage._type) - case 3: try decoder.decodeSingularMessageField(value: &_storage._sentiment) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._text) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.type) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._sentiment) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._text { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if _storage._type != .unknown { - try visitor.visitSingularEnumField(value: _storage._type, fieldNumber: 2) - } - if let v = _storage._sentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._text { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.type != .unknown { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 2) + } + try { if let v = self._sentiment { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_EntityMention, rhs: Google_Cloud_Language_V1_EntityMention) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._text != rhs_storage._text {return false} - if _storage._type != rhs_storage._type {return false} - if _storage._sentiment != rhs_storage._sentiment {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._text != rhs._text {return false} + if lhs.type != rhs.type {return false} + if lhs._sentiment != rhs._sentiment {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3341,9 +3474,12 @@ extension Google_Cloud_Language_V1_TextSpan: SwiftProtobuf.Message, SwiftProtobu mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularStringField(value: &self.content) - case 2: try decoder.decodeSingularInt32Field(value: &self.beginOffset) + case 1: try { try decoder.decodeSingularStringField(value: &self.content) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.beginOffset) }() default: break } } @@ -3376,9 +3512,12 @@ extension Google_Cloud_Language_V1_ClassificationCategory: SwiftProtobuf.Message mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularStringField(value: &self.name) - case 2: try decoder.decodeSingularFloatField(value: &self.confidence) + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.confidence) }() default: break } } @@ -3402,147 +3541,220 @@ extension Google_Cloud_Language_V1_ClassificationCategory: SwiftProtobuf.Message } } -extension Google_Cloud_Language_V1_AnalyzeSentimentRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeSentimentRequest" +extension Google_Cloud_Language_V1_ClassificationModelOptions: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClassificationModelOptions" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "document"), - 2: .standard(proto: "encoding_type"), + 1: .standard(proto: "v1_model"), + 2: .standard(proto: "v2_model"), ] - fileprivate class _StorageClass { - var _document: Google_Cloud_Language_V1_Document? = nil - var _encodingType: Google_Cloud_Language_V1_EncodingType = .none + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model? + var hadOneofValue = false + if let current = self.modelType { + hadOneofValue = true + if case .v1Model(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.modelType = .v1Model(v) + } + }() + case 2: try { + var v: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model? + var hadOneofValue = false + if let current = self.modelType { + hadOneofValue = true + if case .v2Model(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.modelType = .v2Model(v) + } + }() + default: break + } + } + } - static let defaultInstance = _StorageClass() + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.modelType { + case .v1Model?: try { + guard case .v1Model(let v)? = self.modelType else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .v2Model?: try { + guard case .v2Model(let v)? = self.modelType else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } - private init() {} + static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions, rhs: Google_Cloud_Language_V1_ClassificationModelOptions) -> Bool { + if lhs.modelType != rhs.modelType {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} - init(copying source: _StorageClass) { - _document = source._document - _encodingType = source._encodingType +extension Google_Cloud_Language_V1_ClassificationModelOptions.V1Model: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Google_Cloud_Language_V1_ClassificationModelOptions.protoMessageName + ".V1Model" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { } } - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model, rhs: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true } +} + +extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Google_Cloud_Language_V1_ClassificationModelOptions.protoMessageName + ".V2Model" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "content_categories_version"), + ] mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._document) - case 2: try decoder.decodeSingularEnumField(value: &_storage._encodingType) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.contentCategoriesVersion) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if _storage._encodingType != .none { - try visitor.visitSingularEnumField(value: _storage._encodingType, fieldNumber: 2) - } + if self.contentCategoriesVersion != .unspecified { + try visitor.visitSingularEnumField(value: self.contentCategoriesVersion, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSentimentRequest, rhs: Google_Cloud_Language_V1_AnalyzeSentimentRequest) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._document != rhs_storage._document {return false} - if _storage._encodingType != rhs_storage._encodingType {return false} - return true - } - if !storagesAreEqual {return false} - } + static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model, rhs: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model) -> Bool { + if lhs.contentCategoriesVersion != rhs.contentCategoriesVersion {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Google_Cloud_Language_V1_AnalyzeSentimentResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeSentimentResponse" +extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion: SwiftProtobuf._ProtoNameProviding { static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "document_sentiment"), - 2: .same(proto: "language"), - 3: .same(proto: "sentences"), + 0: .same(proto: "CONTENT_CATEGORIES_VERSION_UNSPECIFIED"), + 1: .same(proto: "V1"), + 2: .same(proto: "V2"), ] +} - fileprivate class _StorageClass { - var _documentSentiment: Google_Cloud_Language_V1_Sentiment? = nil - var _language: String = String() - var _sentences: [Google_Cloud_Language_V1_Sentence] = [] - - static let defaultInstance = _StorageClass() - - private init() {} +extension Google_Cloud_Language_V1_AnalyzeSentimentRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".AnalyzeSentimentRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "document"), + 2: .standard(proto: "encoding_type"), + ] - init(copying source: _StorageClass) { - _documentSentiment = source._documentSentiment - _language = source._language - _sentences = source._sentences + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() + default: break + } } } - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._document { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.encodingType != .none { + try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) } - return _storage + try unknownFields.traverse(visitor: &visitor) } + static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSentimentRequest, rhs: Google_Cloud_Language_V1_AnalyzeSentimentRequest) -> Bool { + if lhs._document != rhs._document {return false} + if lhs.encodingType != rhs.encodingType {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Google_Cloud_Language_V1_AnalyzeSentimentResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".AnalyzeSentimentResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "document_sentiment"), + 2: .same(proto: "language"), + 3: .same(proto: "sentences"), + ] + mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._documentSentiment) - case 2: try decoder.decodeSingularStringField(value: &_storage._language) - case 3: try decoder.decodeRepeatedMessageField(value: &_storage._sentences) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._documentSentiment) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.language) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.sentences) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._documentSentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if !_storage._language.isEmpty { - try visitor.visitSingularStringField(value: _storage._language, fieldNumber: 2) - } - if !_storage._sentences.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._sentences, fieldNumber: 3) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._documentSentiment { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.language.isEmpty { + try visitor.visitSingularStringField(value: self.language, fieldNumber: 2) + } + if !self.sentences.isEmpty { + try visitor.visitRepeatedMessageField(value: self.sentences, fieldNumber: 3) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSentimentResponse, rhs: Google_Cloud_Language_V1_AnalyzeSentimentResponse) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._documentSentiment != rhs_storage._documentSentiment {return false} - if _storage._language != rhs_storage._language {return false} - if _storage._sentences != rhs_storage._sentences {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._documentSentiment != rhs._documentSentiment {return false} + if lhs.language != rhs.language {return false} + if lhs.sentences != rhs.sentences {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3555,63 +3767,36 @@ extension Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest: SwiftProtobuf. 2: .standard(proto: "encoding_type"), ] - fileprivate class _StorageClass { - var _document: Google_Cloud_Language_V1_Document? = nil - var _encodingType: Google_Cloud_Language_V1_EncodingType = .none - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _document = source._document - _encodingType = source._encodingType - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._document) - case 2: try decoder.decodeSingularEnumField(value: &_storage._encodingType) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if _storage._encodingType != .none { - try visitor.visitSingularEnumField(value: _storage._encodingType, fieldNumber: 2) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._document { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.encodingType != .none { + try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, rhs: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._document != rhs_storage._document {return false} - if _storage._encodingType != rhs_storage._encodingType {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._document != rhs._document {return false} + if lhs.encodingType != rhs.encodingType {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3626,9 +3811,12 @@ extension Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse: SwiftProtobuf mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeRepeatedMessageField(value: &self.entities) - case 2: try decoder.decodeSingularStringField(value: &self.language) + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.entities) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.language) }() default: break } } @@ -3659,63 +3847,36 @@ extension Google_Cloud_Language_V1_AnalyzeEntitiesRequest: SwiftProtobuf.Message 2: .standard(proto: "encoding_type"), ] - fileprivate class _StorageClass { - var _document: Google_Cloud_Language_V1_Document? = nil - var _encodingType: Google_Cloud_Language_V1_EncodingType = .none - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _document = source._document - _encodingType = source._encodingType - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._document) - case 2: try decoder.decodeSingularEnumField(value: &_storage._encodingType) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if _storage._encodingType != .none { - try visitor.visitSingularEnumField(value: _storage._encodingType, fieldNumber: 2) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._document { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.encodingType != .none { + try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, rhs: Google_Cloud_Language_V1_AnalyzeEntitiesRequest) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._document != rhs_storage._document {return false} - if _storage._encodingType != rhs_storage._encodingType {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._document != rhs._document {return false} + if lhs.encodingType != rhs.encodingType {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3730,9 +3891,12 @@ extension Google_Cloud_Language_V1_AnalyzeEntitiesResponse: SwiftProtobuf.Messag mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeRepeatedMessageField(value: &self.entities) - case 2: try decoder.decodeSingularStringField(value: &self.language) + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.entities) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.language) }() default: break } } @@ -3763,63 +3927,36 @@ extension Google_Cloud_Language_V1_AnalyzeSyntaxRequest: SwiftProtobuf.Message, 2: .standard(proto: "encoding_type"), ] - fileprivate class _StorageClass { - var _document: Google_Cloud_Language_V1_Document? = nil - var _encodingType: Google_Cloud_Language_V1_EncodingType = .none - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _document = source._document - _encodingType = source._encodingType - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._document) - case 2: try decoder.decodeSingularEnumField(value: &_storage._encodingType) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if _storage._encodingType != .none { - try visitor.visitSingularEnumField(value: _storage._encodingType, fieldNumber: 2) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._document { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.encodingType != .none { + try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, rhs: Google_Cloud_Language_V1_AnalyzeSyntaxRequest) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._document != rhs_storage._document {return false} - if _storage._encodingType != rhs_storage._encodingType {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._document != rhs._document {return false} + if lhs.encodingType != rhs.encodingType {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3835,10 +3972,13 @@ extension Google_Cloud_Language_V1_AnalyzeSyntaxResponse: SwiftProtobuf.Message, mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeRepeatedMessageField(value: &self.sentences) - case 2: try decoder.decodeRepeatedMessageField(value: &self.tokens) - case 3: try decoder.decodeSingularStringField(value: &self.language) + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.sentences) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.tokens) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.language) }() default: break } } @@ -3870,58 +4010,39 @@ extension Google_Cloud_Language_V1_ClassifyTextRequest: SwiftProtobuf.Message, S static let protoMessageName: String = _protobuf_package + ".ClassifyTextRequest" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "document"), + 3: .standard(proto: "classification_model_options"), ] - fileprivate class _StorageClass { - var _document: Google_Cloud_Language_V1_Document? = nil - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _document = source._document - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._document) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._classificationModelOptions) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._document { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._classificationModelOptions { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_ClassifyTextRequest, rhs: Google_Cloud_Language_V1_ClassifyTextRequest) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._document != rhs_storage._document {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._document != rhs._document {return false} + if lhs._classificationModelOptions != rhs._classificationModelOptions {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3935,8 +4056,11 @@ extension Google_Cloud_Language_V1_ClassifyTextResponse: SwiftProtobuf.Message, mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeRepeatedMessageField(value: &self.categories) + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.categories) }() default: break } } @@ -3964,70 +4088,41 @@ extension Google_Cloud_Language_V1_AnnotateTextRequest: SwiftProtobuf.Message, S 3: .standard(proto: "encoding_type"), ] - fileprivate class _StorageClass { - var _document: Google_Cloud_Language_V1_Document? = nil - var _features: Google_Cloud_Language_V1_AnnotateTextRequest.Features? = nil - var _encodingType: Google_Cloud_Language_V1_EncodingType = .none - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _document = source._document - _features = source._features - _encodingType = source._encodingType - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularMessageField(value: &_storage._document) - case 2: try decoder.decodeSingularMessageField(value: &_storage._features) - case 3: try decoder.decodeSingularEnumField(value: &_storage._encodingType) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._features) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if let v = _storage._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } - if let v = _storage._features { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } - if _storage._encodingType != .none { - try visitor.visitSingularEnumField(value: _storage._encodingType, fieldNumber: 3) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._document { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._features { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if self.encodingType != .none { + try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 3) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_AnnotateTextRequest, rhs: Google_Cloud_Language_V1_AnnotateTextRequest) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._document != rhs_storage._document {return false} - if _storage._features != rhs_storage._features {return false} - if _storage._encodingType != rhs_storage._encodingType {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs._document != rhs._document {return false} + if lhs._features != rhs._features {return false} + if lhs.encodingType != rhs.encodingType {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4041,22 +4136,31 @@ extension Google_Cloud_Language_V1_AnnotateTextRequest.Features: SwiftProtobuf.M 3: .standard(proto: "extract_document_sentiment"), 4: .standard(proto: "extract_entity_sentiment"), 6: .standard(proto: "classify_text"), + 10: .standard(proto: "classification_model_options"), ] mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try decoder.decodeSingularBoolField(value: &self.extractSyntax) - case 2: try decoder.decodeSingularBoolField(value: &self.extractEntities) - case 3: try decoder.decodeSingularBoolField(value: &self.extractDocumentSentiment) - case 4: try decoder.decodeSingularBoolField(value: &self.extractEntitySentiment) - case 6: try decoder.decodeSingularBoolField(value: &self.classifyText) + case 1: try { try decoder.decodeSingularBoolField(value: &self.extractSyntax) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.extractEntities) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.extractDocumentSentiment) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.extractEntitySentiment) }() + case 6: try { try decoder.decodeSingularBoolField(value: &self.classifyText) }() + case 10: try { try decoder.decodeSingularMessageField(value: &self._classificationModelOptions) }() default: break } } } func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if self.extractSyntax != false { try visitor.visitSingularBoolField(value: self.extractSyntax, fieldNumber: 1) } @@ -4072,6 +4176,9 @@ extension Google_Cloud_Language_V1_AnnotateTextRequest.Features: SwiftProtobuf.M if self.classifyText != false { try visitor.visitSingularBoolField(value: self.classifyText, fieldNumber: 6) } + try { if let v = self._classificationModelOptions { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -4081,6 +4188,7 @@ extension Google_Cloud_Language_V1_AnnotateTextRequest.Features: SwiftProtobuf.M if lhs.extractDocumentSentiment != rhs.extractDocumentSentiment {return false} if lhs.extractEntitySentiment != rhs.extractEntitySentiment {return false} if lhs.classifyText != rhs.classifyText {return false} + if lhs._classificationModelOptions != rhs._classificationModelOptions {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -4097,91 +4205,56 @@ extension Google_Cloud_Language_V1_AnnotateTextResponse: SwiftProtobuf.Message, 6: .same(proto: "categories"), ] - fileprivate class _StorageClass { - var _sentences: [Google_Cloud_Language_V1_Sentence] = [] - var _tokens: [Google_Cloud_Language_V1_Token] = [] - var _entities: [Google_Cloud_Language_V1_Entity] = [] - var _documentSentiment: Google_Cloud_Language_V1_Sentiment? = nil - var _language: String = String() - var _categories: [Google_Cloud_Language_V1_ClassificationCategory] = [] - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _sentences = source._sentences - _tokens = source._tokens - _entities = source._entities - _documentSentiment = source._documentSentiment - _language = source._language - _categories = source._categories - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeRepeatedMessageField(value: &_storage._sentences) - case 2: try decoder.decodeRepeatedMessageField(value: &_storage._tokens) - case 3: try decoder.decodeRepeatedMessageField(value: &_storage._entities) - case 4: try decoder.decodeSingularMessageField(value: &_storage._documentSentiment) - case 5: try decoder.decodeSingularStringField(value: &_storage._language) - case 6: try decoder.decodeRepeatedMessageField(value: &_storage._categories) - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.sentences) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.tokens) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.entities) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._documentSentiment) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.language) }() + case 6: try { try decoder.decodeRepeatedMessageField(value: &self.categories) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - if !_storage._sentences.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._sentences, fieldNumber: 1) - } - if !_storage._tokens.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._tokens, fieldNumber: 2) - } - if !_storage._entities.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._entities, fieldNumber: 3) - } - if let v = _storage._documentSentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } - if !_storage._language.isEmpty { - try visitor.visitSingularStringField(value: _storage._language, fieldNumber: 5) - } - if !_storage._categories.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._categories, fieldNumber: 6) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.sentences.isEmpty { + try visitor.visitRepeatedMessageField(value: self.sentences, fieldNumber: 1) + } + if !self.tokens.isEmpty { + try visitor.visitRepeatedMessageField(value: self.tokens, fieldNumber: 2) + } + if !self.entities.isEmpty { + try visitor.visitRepeatedMessageField(value: self.entities, fieldNumber: 3) + } + try { if let v = self._documentSentiment { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + if !self.language.isEmpty { + try visitor.visitSingularStringField(value: self.language, fieldNumber: 5) + } + if !self.categories.isEmpty { + try visitor.visitRepeatedMessageField(value: self.categories, fieldNumber: 6) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Google_Cloud_Language_V1_AnnotateTextResponse, rhs: Google_Cloud_Language_V1_AnnotateTextResponse) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._sentences != rhs_storage._sentences {return false} - if _storage._tokens != rhs_storage._tokens {return false} - if _storage._entities != rhs_storage._entities {return false} - if _storage._documentSentiment != rhs_storage._documentSentiment {return false} - if _storage._language != rhs_storage._language {return false} - if _storage._categories != rhs_storage._categories {return false} - return true - } - if !storagesAreEqual {return false} - } + if lhs.sentences != rhs.sentences {return false} + if lhs.tokens != rhs.tokens {return false} + if lhs.entities != rhs.entities {return false} + if lhs._documentSentiment != rhs._documentSentiment {return false} + if lhs.language != rhs.language {return false} + if lhs.categories != rhs.categories {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Examples/Google/NaturalLanguage/Sources/main.swift b/Examples/Google/NaturalLanguage/Sources/main.swift index 6ed8b095b..a98529d17 100644 --- a/Examples/Google/NaturalLanguage/Sources/main.swift +++ b/Examples/Google/NaturalLanguage/Sources/main.swift @@ -27,15 +27,10 @@ func makeServiceClient( host: String, port: Int, eventLoopGroup: MultiThreadedEventLoopGroup -) -> Google_Cloud_Language_V1_LanguageServiceServiceClient { - let configuration = ClientConnection.Configuration( - target: .hostAndPort(host, port), - eventLoopGroup: eventLoopGroup, - tls: .init() - ) - - let connection = ClientConnection(configuration: configuration) - return Google_Cloud_Language_V1_LanguageServiceServiceClient(connection: connection) +) -> Google_Cloud_Language_V1_LanguageServiceNIOClient { + let connection = ClientConnection.usingPlatformAppropriateTLS(for: eventLoopGroup) + .connect(host: host, port: port) + return Google_Cloud_Language_V1_LanguageServiceNIOClient(channel: connection) } enum AuthError: Error { @@ -90,7 +85,7 @@ do { // Use CallOptions to send the auth token (necessary) and set a custom timeout (optional). let headers: HPACKHeaders = ["authorization": "Bearer \(authToken)"] - let callOptions = CallOptions(customMetadata: headers, timeout: .seconds(rounding: 30)) + let callOptions = CallOptions(customMetadata: headers, timeLimit: TimeLimit.timeout(.seconds(30))) print("CALL OPTIONS\n\(callOptions)\n") // Construct the API request. From b11a8562b3e9450f3c8058ce5380b54847e52adf Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 22 Sep 2022 12:28:27 +0200 Subject: [PATCH 025/580] Expose public creation methods for `GRPCAsyncRequestStream` and `GRPCAsyncResponseStreamWriter` (#1485) Motivation: It is highly desirable to be able to write tests against the generated method of a service. Currently, this is close to impossible since both the `GRPCAsyncRequestStream` and the `GRPCAsyncResponseStreamWriter` don't expose a public init so users cannot drive and observe the functions. Modification: Adds new public methods to drive and observe the request stream and the response writer. Result We can now test functions which use the request stream and response stream writer. --- .../GRPCAsyncRequestStream.swift | 120 ++++++++++++++++-- .../GRPCAsyncResponseStreamWriter.swift | 115 ++++++++++++++++- .../Interceptor/ClientTransportFactory.swift | 3 +- .../GRPCAsyncRequestStreamTests.swift | 34 +++++ .../GRPCAsyncResponseStreamWriterTests.swift | 34 +++++ 5 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift create mode 100644 Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift index 25df9899c..278b39c43 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift @@ -15,46 +15,140 @@ */ #if compiler(>=5.6) - -/// This is currently a wrapper around AsyncThrowingStream because we want to be +/// A type for the stream of request messages send to a gRPC server method. +/// +/// To enable testability this type provides a static ``GRPCAsyncRequestStream/makeTestingRequestStream()`` +/// method which allows you to create a stream that you can drive. +/// +/// - Note: This is currently a wrapper around AsyncThrowingStream because we want to be /// able to swap out the implementation for something else in the future. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncRequestStream: AsyncSequence { + /// A source used for driving a ``GRPCAsyncRequestStream`` during tests. + public struct Source { + @usableFromInline + internal let continuation: AsyncThrowingStream.Continuation + + @inlinable + init(continuation: AsyncThrowingStream.Continuation) { + self.continuation = continuation + } + + /// Yields the element to the request stream. + /// + /// - Parameter element: The element to yield to the request stream. + @inlinable + public func yield(_ element: Element) { + self.continuation.yield(element) + } + + /// Finished the request stream. + @inlinable + public func finish() { + self.continuation.finish() + } + + /// Finished the request stream. + /// + /// - Parameter error: An optional `Error` to finish the request stream with. + @inlinable + public func finish(throwing error: Error?) { + self.continuation.finish(throwing: error) + } + } + + /// Simple struct for the return type of ``GRPCAsyncRequestStream/makeTestingRequestStream()``. + /// + /// This struct contains two properties: + /// 1. The ``stream`` which is the actual ``GRPCAsyncRequestStream`` and should be passed to the method under testing. + /// 2. The ``source`` which can be used to drive the stream. + public struct TestingStream { + /// The actual stream. + public let stream: GRPCAsyncRequestStream + /// The source used to drive the stream. + public let source: Source + + @inlinable + init(stream: GRPCAsyncRequestStream, source: Source) { + self.stream = stream + self.source = source + } + } + @usableFromInline - internal typealias _WrappedStream = PassthroughMessageSequence + enum Backing: Sendable { + case passthroughMessageSequence(PassthroughMessageSequence) + case asyncStream(AsyncThrowingStream) + } @usableFromInline - internal let _stream: _WrappedStream + internal let backing: Backing + + @inlinable + internal init(_ sequence: PassthroughMessageSequence) { + self.backing = .passthroughMessageSequence(sequence) + } @inlinable - internal init(_ stream: _WrappedStream) { - self._stream = stream + internal init(_ stream: AsyncThrowingStream) { + self.backing = .asyncStream(stream) + } + + /// Creates a new testing stream. + /// + /// This is useful for writing unit tests for your gRPC method implementations since it allows you to drive the stream passed + /// to your method. + /// + /// - Returns: A new ``TestingStream`` containing the actual ``GRPCAsyncRequestStream`` and a ``Source``. + @inlinable + public static func makeTestingRequestStream() -> TestingStream { + var continuation: AsyncThrowingStream.Continuation! + let stream = AsyncThrowingStream { continuation = $0 } + let source = Source(continuation: continuation) + let requestStream = Self(stream) + return TestingStream(stream: requestStream, source: source) } @inlinable public func makeAsyncIterator() -> Iterator { - Self.AsyncIterator(self._stream) + switch self.backing { + case let .passthroughMessageSequence(sequence): + return Self.AsyncIterator(.passthroughMessageSequence(sequence.makeAsyncIterator())) + case let .asyncStream(stream): + return Self.AsyncIterator(.asyncStream(stream.makeAsyncIterator())) + } } public struct Iterator: AsyncIteratorProtocol { @usableFromInline - internal var iterator: _WrappedStream.AsyncIterator + enum BackingIterator { + case passthroughMessageSequence(PassthroughMessageSequence.Iterator) + case asyncStream(AsyncThrowingStream.Iterator) + } + + @usableFromInline + internal var iterator: BackingIterator @usableFromInline - internal init(_ stream: _WrappedStream) { - self.iterator = stream.makeAsyncIterator() + internal init(_ iterator: BackingIterator) { + self.iterator = iterator } @inlinable public mutating func next() async throws -> Element? { - try await self.iterator.next() + switch self.iterator { + case let .passthroughMessageSequence(iterator): + return try await iterator.next() + case var .asyncStream(iterator): + let element = try await iterator.next() + self.iterator = .asyncStream(iterator) + return element + } } } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension GRPCAsyncRequestStream: Sendable where Element: Sendable {} -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCAsyncRequestStream.Iterator: Sendable where Element: Sendable {} #endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift index 1c662e241..3fb1f8792 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift @@ -17,8 +17,84 @@ #if compiler(>=5.6) /// Writer for server-streaming RPC handlers to provide responses. +/// +/// To enable testability this type provides a static ``GRPCAsyncResponseStreamWriter/makeTestingResponseStreamWriter()`` +/// method which allows you to create a stream that you can drive. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncResponseStreamWriter: Sendable { + /// An `AsyncSequence` backing a ``GRPCAsyncResponseStreamWriter`` for testing purposes. + /// + /// - Important: This `AsyncSequence` is never finishing. + public struct ResponseStream: AsyncSequence { + public typealias Element = (Response, Compression) + + @usableFromInline + internal let stream: AsyncStream<(Response, Compression)> + + @usableFromInline + internal let continuation: AsyncStream<(Response, Compression)>.Continuation + + @inlinable + init( + stream: AsyncStream<(Response, Compression)>, + continuation: AsyncStream<(Response, Compression)>.Continuation + ) { + self.stream = stream + self.continuation = continuation + } + + public func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(iterator: self.stream.makeAsyncIterator()) + } + + /// Finishes the response stream. + /// + /// This is useful in tests to finish the stream after the async method finished and allows you to collect all written responses. + public func finish() { + self.continuation.finish() + } + + public struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline + internal var iterator: AsyncStream<(Response, Compression)>.AsyncIterator + + @inlinable + init(iterator: AsyncStream<(Response, Compression)>.AsyncIterator) { + self.iterator = iterator + } + + public mutating func next() async -> Element? { + await self.iterator.next() + } + } + } + + /// Simple struct for the return type of ``GRPCAsyncResponseStreamWriter/makeTestingResponseStreamWriter()``. + /// + /// This struct contains two properties: + /// 1. The ``writer`` which is the actual ``GRPCAsyncResponseStreamWriter`` and should be passed to the method under testing. + /// 2. The ``stream`` which can be used to observe the written responses. + public struct TestingStreamWriter { + /// The actual writer. + public let writer: GRPCAsyncResponseStreamWriter + /// The written responses in a stream. + /// + /// - Important: This `AsyncSequence` is never finishing. + public let stream: ResponseStream + + @inlinable + init(writer: GRPCAsyncResponseStreamWriter, stream: ResponseStream) { + self.writer = writer + self.stream = stream + } + } + + @usableFromInline + enum Backing: Sendable { + case asyncWriter(AsyncWriter) + case closure(@Sendable ((Response, Compression)) async -> Void) + } + @usableFromInline internal typealias Element = (Response, Compression) @@ -26,11 +102,16 @@ public struct GRPCAsyncResponseStreamWriter: Sendable { internal typealias Delegate = AsyncResponseStreamWriterDelegate @usableFromInline - internal let asyncWriter: AsyncWriter + internal let backing: Backing @inlinable internal init(wrapping asyncWriter: AsyncWriter) { - self.asyncWriter = asyncWriter + self.backing = .asyncWriter(asyncWriter) + } + + @inlinable + internal init(onWrite: @escaping @Sendable ((Response, Compression)) async -> Void) { + self.backing = .closure(onWrite) } @inlinable @@ -38,7 +119,35 @@ public struct GRPCAsyncResponseStreamWriter: Sendable { _ response: Response, compression: Compression = .deferToCallDefault ) async throws { - try await self.asyncWriter.write((response, compression)) + switch self.backing { + case let .asyncWriter(writer): + try await writer.write((response, compression)) + + case let .closure(closure): + await closure((response, compression)) + } + } + + /// Creates a new `GRPCAsyncResponseStreamWriter` backed by a ``ResponseStream``. + /// This is mostly useful for testing purposes where one wants to observe the written responses. + /// + /// - Note: For most tests it is useful to call ``ResponseStream/finish()`` after the async method under testing + /// resumed. This allows you to easily collect all written responses. + @inlinable + public static func makeTestingResponseStreamWriter() -> TestingStreamWriter { + var continuation: AsyncStream<(Response, Compression)>.Continuation! + let asyncStream = AsyncStream<(Response, Compression)> { cont in + continuation = cont + } + let writer = Self.init { [continuation] in + continuation!.yield($0) + } + let responseStream = ResponseStream( + stream: asyncStream, + continuation: continuation + ) + + return TestingStreamWriter(writer: writer, stream: responseStream) } } diff --git a/Sources/GRPC/Interceptor/ClientTransportFactory.swift b/Sources/GRPC/Interceptor/ClientTransportFactory.swift index 2010fbe9a..a6f1e9065 100644 --- a/Sources/GRPC/Interceptor/ClientTransportFactory.swift +++ b/Sources/GRPC/Interceptor/ClientTransportFactory.swift @@ -304,8 +304,7 @@ internal struct FakeClientTransportFactory { ) where RequestSerializer.Input == Request, RequestDeserializer.Output == Request, ResponseSerializer.Input == Response, - ResponseDeserializer.Output == Response - { + ResponseDeserializer.Output == Response { self.fakeResponseStream = fakeResponseStream self.requestSerializer = AnySerializer(wrapping: requestSerializer) self.responseDeserializer = AnyDeserializer(wrapping: responseDeserializer) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift new file mode 100644 index 000000000..b2b7871f5 --- /dev/null +++ b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift @@ -0,0 +1,34 @@ +/* + * Copyright 2022, 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. + */ +#if compiler(>=5.6) + +import GRPC +import XCTest + +@available(macOS 12, iOS 13, tvOS 13, watchOS 6, *) +final class GRPCAsyncRequestStreamTests: XCTestCase { + func testRecorder() async throws { + let testingStream = GRPCAsyncRequestStream.makeTestingRequestStream() + + testingStream.source.yield(1) + testingStream.source.finish(throwing: nil) + + let results = try await testingStream.stream.collect() + + XCTAssertEqual(results, [1]) + } +} +#endif diff --git a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift new file mode 100644 index 000000000..a299f2c2c --- /dev/null +++ b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift @@ -0,0 +1,34 @@ +/* + * Copyright 2022, 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. + */ +#if compiler(>=5.6) + +import GRPC +import XCTest + +@available(macOS 12, iOS 13, tvOS 13, watchOS 6, *) +final class GRPCAsyncResponseStreamWriterTests: XCTestCase { + func testRecorder() async throws { + let responseStreamWriter = GRPCAsyncResponseStreamWriter.makeTestingResponseStreamWriter() + + try await responseStreamWriter.writer.send(1, compression: .disabled) + responseStreamWriter.stream.finish() + + let results = try await responseStreamWriter.stream.collect() + XCTAssertEqual(results[0].0, 1) + XCTAssertEqual(results[0].1, .disabled) + } +} +#endif From 7fcc7bcef125801a750994657fb429898816121e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 26 Sep 2022 16:19:09 +0100 Subject: [PATCH 026/580] Use forward looking SwiftProtobuf API (#1482) --- Package.swift | 2 +- Package@swift-5.5.swift | 2 +- Sources/protoc-gen-grpc-swift/StreamingType.swift | 6 +++--- Sources/protoc-gen-grpc-swift/main.swift | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Package.swift b/Package.swift index 0931529c5..5ce8a2b29 100644 --- a/Package.swift +++ b/Package.swift @@ -48,7 +48,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.20.1" + from: "1.20.2" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index a6806791f..116070a64 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -49,7 +49,7 @@ let packageDependencies: [Package.Dependency] = [ .package( name: "SwiftProtobuf", url: "https://github.com/apple/swift-protobuf.git", - from: "1.19.0" + from: "1.20.2" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Sources/protoc-gen-grpc-swift/StreamingType.swift b/Sources/protoc-gen-grpc-swift/StreamingType.swift index c6f108d3f..bb22f4464 100644 --- a/Sources/protoc-gen-grpc-swift/StreamingType.swift +++ b/Sources/protoc-gen-grpc-swift/StreamingType.swift @@ -38,14 +38,14 @@ extension StreamingType { } internal func streamingType(_ method: MethodDescriptor) -> StreamingType { - if method.proto.clientStreaming { - if method.proto.serverStreaming { + if method.clientStreaming { + if method.serverStreaming { return .bidirectionalStreaming } else { return .clientStreaming } } else { - if method.proto.serverStreaming { + if method.serverStreaming { return .serverStreaming } else { return .unary diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 76387fba2..091428a97 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -125,8 +125,8 @@ func main() throws { // Only generate output for services. for name in request.fileToGenerate { - let fileDescriptor = descriptorSet.lookupFileDescriptor(protoName: name) - if !fileDescriptor.services.isEmpty { + if let fileDescriptor = descriptorSet.fileDescriptor(named: name), + !fileDescriptor.services.isEmpty { let grpcFileName = uniqueOutputFileName( component: "grpc", fileDescriptor: fileDescriptor, From dfc2ab0285f9be52f2a5cc9cd6b61b92d069e3c0 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 27 Sep 2022 14:52:41 +0100 Subject: [PATCH 027/580] Update swift-nio version, allocation test max values and alloc-limits.sh (#1490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation This change updates the allocation limits for `grpc-swift` allocation tests. This is necessary because we’ve increased the number of allocations in the new version of `swift-nio` for some of the code paths the tests hit. This change also fixes some warnings as a result of the NIO update. ## Modifications * Updated max allocation values for allocation tests * Updated the `alloc-limits.sh` script to work with the output of the GitHub CI logs, which include timestamps. Output from running the script locally doesn’t contain timestamps (and still works). * Modified the allocation tests to use `Echo_EchoNIOClient` instead of the now deprecated `Echo_EchoClient` * Fixed warnings related to NIO's `Lock` being deprecated. ## Result `swift-nio` has been updated to 2.42.0, allocation tests have updated allocation counts, and warnings related to the update fixed. --- .github/workflows/ci.yaml | 24 +++++++++---------- Package.swift | 2 +- Package@swift-5.5.swift | 2 +- .../allocations/tests/test_bidi_1k_rpcs.swift | 2 +- .../PassthroughMessageSource.swift | 4 ++-- Sources/GRPC/ConnectionManager.swift | 4 ++-- Sources/GRPC/ConnectionPool/PoolManager.swift | 12 +++++----- Sources/GRPC/ConnectivityState.swift | 6 ++--- .../AsyncAwaitSupport/AsyncWriterTests.swift | 6 ++--- Tests/GRPCTests/CapturingLogHandler.swift | 4 ++-- Tests/GRPCTests/ClientQuiescingTests.swift | 10 ++++---- .../ConnectionPool/GRPCChannelPoolTests.swift | 8 +++---- .../PoolManagerStateMachineTests.swift | 2 +- Tests/GRPCTests/ErrorRecordingDelegate.swift | 6 ++--- ...treamResponseHandlerRetainCycleTests.swift | 4 ++-- scripts/alloc-limits.sh | 1 + 16 files changed, 49 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf808f490..889ee8a54 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -58,9 +58,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 428000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 174000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 181000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 @@ -68,9 +68,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 428000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 174000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 181000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 @@ -78,9 +78,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 429000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 177000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 175000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 182000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 182000 @@ -88,9 +88,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 459000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 189000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 186000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 193000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 193000 diff --git a/Package.swift b/Package.swift index 5ce8a2b29..8c921631b 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.41.1" + from: "2.42.0" ), .package( url: "https://github.com/apple/swift-nio-http2.git", diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index 116070a64..64952921f 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.36.0" + from: "2.42.0" ), .package( url: "https://github.com/apple/swift-nio-http2.git", diff --git a/Performance/allocations/tests/test_bidi_1k_rpcs.swift b/Performance/allocations/tests/test_bidi_1k_rpcs.swift index 30e8c5617..589ef0d8e 100644 --- a/Performance/allocations/tests/test_bidi_1k_rpcs.swift +++ b/Performance/allocations/tests/test_bidi_1k_rpcs.swift @@ -48,7 +48,7 @@ class BidiPingPongBenchmark: Benchmark { } func run() throws -> Int { - let echo = Echo_EchoClient(channel: self.client) + let echo = Echo_EchoNIOClient(channel: self.client) var statusCodeSum = 0 // We'll use this semaphore to make sure we're ping-ponging request-response diff --git a/Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift b/Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift index 6a0773602..07473b564 100644 --- a/Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift +++ b/Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift @@ -38,7 +38,7 @@ internal final class PassthroughMessageSource /// - Important: We use a `class` with a lock rather than an `actor` as we must guarantee that /// calls to ``yield(_:)`` are not reordered. @usableFromInline - internal let _lock: Lock + internal let _lock: NIOLock /// A queue of elements which may be consumed as soon as there is demand. @usableFromInline @@ -56,7 +56,7 @@ internal final class PassthroughMessageSource @usableFromInline internal init(initialBufferCapacity: Int = 16) { - self._lock = Lock() + self._lock = NIOLock() self._continuationResults = CircularBuffer(initialCapacity: initialBufferCapacity) self._continuation = nil self._isTerminated = false diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 899321df5..bb7ef471a 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -279,7 +279,7 @@ internal final class ConnectionManager { private let connectionID: String private var channelNumber: UInt64 - private var channelNumberLock = Lock() + private var channelNumberLock = NIOLock() private var _connectionIDAndNumber: String { return "\(self.connectionID)/\(self.channelNumber)" @@ -292,7 +292,7 @@ internal final class ConnectionManager { } private func updateConnectionID() { - self.channelNumberLock.withLockVoid { + self.channelNumberLock.withLock { self.channelNumber &+= 1 self.logger[metadataKey: MetadataKey.connectionID] = "\(self._connectionIDAndNumber)" } diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index 961f377e5..8bfca4a07 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -96,7 +96,7 @@ internal final class PoolManager { internal var _pools: [ConnectionPool] @usableFromInline - internal let lock = Lock() + internal let lock = NIOLock() /// The `EventLoopGroup` providing `EventLoop`s for connection pools. Once initialized the manager /// will hold as many pools as there are loops in this `EventLoopGroup`. @@ -145,7 +145,7 @@ internal final class PoolManager { } deinit { - self.lock.withLockVoid { + self.lock.withLock { assert( self._state.isShutdownOrShuttingDown, "The pool manager (\(ObjectIdentifier(self))) must be shutdown before going out of scope." @@ -187,7 +187,7 @@ internal final class PoolManager { ) } - self.lock.withLockVoid { + self.lock.withLock { assert(self._pools.isEmpty) self._pools = pools @@ -334,7 +334,7 @@ internal final class PoolManager { } private func shutdownComplete() { - self.lock.withLockVoid { + self.lock.withLock { self._state.shutdownComplete() } } @@ -345,14 +345,14 @@ internal final class PoolManager { extension PoolManager: StreamLender { @usableFromInline internal func returnStreams(_ count: Int, to pool: ConnectionPool) { - self.lock.withLockVoid { + self.lock.withLock { self._state.returnStreams(count, toPoolOnEventLoopWithID: pool.eventLoop.id) } } @usableFromInline internal func changeStreamCapacity(by delta: Int, for pool: ConnectionPool) { - self.lock.withLockVoid { + self.lock.withLock { self._state.changeStreamCapacity(by: delta, forPoolOnEventLoopWithID: pool.eventLoop.id) } } diff --git a/Sources/GRPC/ConnectivityState.swift b/Sources/GRPC/ConnectivityState.swift index 7c6a4621d..9b46da131 100644 --- a/Sources/GRPC/ConnectivityState.swift +++ b/Sources/GRPC/ConnectivityState.swift @@ -74,10 +74,10 @@ extension ConnectivityStateMonitor: @unchecked Sendable {} #endif // compiler(>=5.6) public class ConnectivityStateMonitor { - private let stateLock = Lock() + private let stateLock = NIOLock() private var _state: ConnectivityState = .idle - private let delegateLock = Lock() + private let delegateLock = NIOLock() private var _delegate: ConnectivityStateDelegate? private let delegateCallbackQueue: DispatchQueue @@ -105,7 +105,7 @@ public class ConnectivityStateMonitor { } } set { - self.delegateLock.withLockVoid { + self.delegateLock.withLock { self._delegate = newValue } } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift index b0123999d..9ddee0818 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift @@ -277,7 +277,7 @@ fileprivate final class CollectingDelegate< Element: Sendable, End: Sendable >: AsyncWriterDelegate, @unchecked Sendable { - private let lock = Lock() + private let lock = NIOLock() private var _elements: [Element] = [] private var _end: End? @@ -290,13 +290,13 @@ fileprivate final class CollectingDelegate< } internal func write(_ element: Element) { - self.lock.withLockVoid { + self.lock.withLock { self._elements.append(element) } } internal func writeEnd(_ end: End) { - self.lock.withLockVoid { + self.lock.withLock { self._end = end } } diff --git a/Tests/GRPCTests/CapturingLogHandler.swift b/Tests/GRPCTests/CapturingLogHandler.swift index d4acd6c4c..a7eba1758 100644 --- a/Tests/GRPCTests/CapturingLogHandler.swift +++ b/Tests/GRPCTests/CapturingLogHandler.swift @@ -20,7 +20,7 @@ import NIOConcurrencyHelpers /// A `LogHandler` factory which captures all logs emitted by the handlers it makes. internal class CapturingLogHandlerFactory { - private var lock = Lock() + private var lock = NIOLock() private var _logs: [CapturedLog] = [] private var logFormatter: CapturedLogFormatter? @@ -45,7 +45,7 @@ internal class CapturingLogHandlerFactory { /// Make a `LogHandler` whose logs will be recorded by this factory. func make(_ label: String) -> LogHandler { return CapturingLogHandler(label: label) { log in - self.lock.withLockVoid { + self.lock.withLock { self._logs.append(log) } diff --git a/Tests/GRPCTests/ClientQuiescingTests.swift b/Tests/GRPCTests/ClientQuiescingTests.swift index eedfa475f..02b897365 100644 --- a/Tests/GRPCTests/ClientQuiescingTests.swift +++ b/Tests/GRPCTests/ClientQuiescingTests.swift @@ -425,10 +425,10 @@ extension ClientQuiescingTests { } private var state = _State.active(0) - private let lock = Lock() + private let lock = NIOLock() internal func assert(_ state: State, line: UInt = #line) { - self.lock.withLockVoid { + self.lock.withLock { switch (self.state, state) { case (.active, .active), (.shutdownRequested, .shutdownRequested), @@ -441,7 +441,7 @@ extension ClientQuiescingTests { } internal func willStartRPC() { - self.lock.withLockVoid { + self.lock.withLock { switch self.state { case let .active(outstandingRPCs): self.state = .active(outstandingRPCs + 1) @@ -458,7 +458,7 @@ extension ClientQuiescingTests { } internal func didFinishRPC() { - self.lock.withLockVoid { + self.lock.withLock { switch self.state { case let .active(outstandingRPCs): XCTAssertGreaterThan(outstandingRPCs, 0) @@ -475,7 +475,7 @@ extension ClientQuiescingTests { } internal func willRequestGracefulShutdown() { - self.lock.withLockVoid { + self.lock.withLock { switch self.state { case let .active(outstandingRPCs): self.state = .shutdownRequested(outstandingRPCs) diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index 6d380519a..581db9a34 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -312,7 +312,7 @@ final class GRPCChannelPoolTests: GRPCTestCase { $0.connectionPool.maxWaitTime = .hours(1) } - let lock = Lock() + let lock = NIOLock() var order = 0 // We need a connection to be up and running to avoid hitting the waiter limit when creating a @@ -341,15 +341,15 @@ final class GRPCChannelPoolTests: GRPCTestCase { } // Still zero: the first RPC is still active. - lock.withLockVoid { XCTAssertEqual(order, 0) } + lock.withLock { XCTAssertEqual(order, 0) } // End the first RPC. XCTAssertNoThrow(try rpcs.first!.sendEnd().wait()) XCTAssertNoThrow(try rpcs.first!.status.wait()) - lock.withLockVoid { XCTAssertEqual(order, 1) } + lock.withLock { XCTAssertEqual(order, 1) } // End the last RPC. XCTAssertNoThrow(try rpcs.last!.sendEnd().wait()) XCTAssertNoThrow(try rpcs.last!.status.wait()) - lock.withLockVoid { XCTAssertEqual(order, 2) } + lock.withLock { XCTAssertEqual(order, 2) } // End the rest. for rpc in rpcs.dropFirst().dropLast() { diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift index 77f8b2515..2a663d1a0 100644 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift @@ -330,7 +330,7 @@ extension PoolManagerStateMachine.ShutdownAction { private final class EmbeddedEventLoopGroup: EventLoopGroup { internal let loops: [EmbeddedEventLoop] - internal let lock = Lock() + internal let lock = NIOLock() internal var index = 0 internal init(loops: Int) { diff --git a/Tests/GRPCTests/ErrorRecordingDelegate.swift b/Tests/GRPCTests/ErrorRecordingDelegate.swift index 244d37531..477be9a16 100644 --- a/Tests/GRPCTests/ErrorRecordingDelegate.swift +++ b/Tests/GRPCTests/ErrorRecordingDelegate.swift @@ -24,7 +24,7 @@ extension ErrorRecordingDelegate: @unchecked Sendable {} #endif // compiler(>=5.6) final class ErrorRecordingDelegate: ClientErrorDelegate { - private let lock: Lock + private let lock: NIOLock private var _errors: [Error] = [] internal var errors: [Error] { @@ -37,11 +37,11 @@ final class ErrorRecordingDelegate: ClientErrorDelegate { init(expectation: XCTestExpectation) { self.expectation = expectation - self.lock = Lock() + self.lock = NIOLock() } func didCatchError(_ error: Error, logger: Logger, file: StaticString, line: Int) { - self.lock.withLockVoid { + self.lock.withLock { self._errors.append(error) } self.expectation.fulfill() diff --git a/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift b/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift index 7409ce99a..39fb2225c 100644 --- a/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift +++ b/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift @@ -57,11 +57,11 @@ final class StreamResponseHandlerRetainCycleTests: GRPCTestCase { func testHandlerClosureIsReleasedOnceStreamEnds() { final class Counter { - private let lock = Lock() + private let lock = NIOLock() private var _value = 0 func increment() { - self.lock.withLockVoid { + self.lock.withLock { self._value += 1 } } diff --git a/scripts/alloc-limits.sh b/scripts/alloc-limits.sh index dbbf2ffdd..16aa0063b 100755 --- a/scripts/alloc-limits.sh +++ b/scripts/alloc-limits.sh @@ -28,6 +28,7 @@ # MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request=64000 grep 'test_.*\.total_allocations: ' \ + | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}.[0-9]*Z //' \ | sed 's/^test_/MAX_ALLOCS_ALLOWED_/' \ | sed 's/.total_allocations://' \ | awk '{ print " " $1 ": " ((int($2 / 1000) + 1) * 1000) }' \ From 2bf2a164cea054a9bfbd08fd7ace18504327bca9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 27 Sep 2022 15:44:16 +0100 Subject: [PATCH 028/580] Use a C executable for fuzz testing (#1492) Motivation: Fuzz testing was accidentally broken in https://github.com/grpc/grpc-swift/pull/1483. Modifications: - The compiler was looking for a 'ServerFuzzer_main' symbol which didn't exist ('ServerFuzzer' is an executable target). - Add a 'ServerFuzzerLib' target containing the contents of the existing 'ServerFuzzer' target. - Change the 'ServerFuzzer' target to be a C module calling the API exposed by 'ServerFuzzerLib' Result: Fuzzing builds again. --- FuzzTesting/Package.swift | 6 ++++++ FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c | 9 +++++++++ .../main.swift => ServerFuzzerLib/ServerFuzzer.swift} | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c rename FuzzTesting/Sources/{ServerFuzzer/main.swift => ServerFuzzerLib/ServerFuzzer.swift} (97%) diff --git a/FuzzTesting/Package.swift b/FuzzTesting/Package.swift index 03d3c49aa..4d4da31e5 100644 --- a/FuzzTesting/Package.swift +++ b/FuzzTesting/Package.swift @@ -25,6 +25,12 @@ let package = Package( targets: [ .executableTarget( name: "ServerFuzzer", + dependencies: [ + .target(name: "ServerFuzzerLib"), + ] + ), + .target( + name: "ServerFuzzerLib", dependencies: [ .product(name: "GRPC", package: "grpc-swift"), .product(name: "NIO", package: "swift-nio"), diff --git a/FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c b/FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c new file mode 100644 index 000000000..d6ce95691 --- /dev/null +++ b/FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c @@ -0,0 +1,9 @@ +#include +#include + +// Provided by ServerFuzzerLib. +int ServerFuzzer(const uint8_t *Data, size_t Size); + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + return ServerFuzzer(data, size); +} diff --git a/FuzzTesting/Sources/ServerFuzzer/main.swift b/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift similarity index 97% rename from FuzzTesting/Sources/ServerFuzzer/main.swift rename to FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift index 6593d0486..31baa6070 100644 --- a/FuzzTesting/Sources/ServerFuzzer/main.swift +++ b/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift @@ -17,7 +17,7 @@ import EchoImplementation import GRPC import NIO -@_cdecl("LLVMFuzzerTestOneInput") +@_cdecl("ServerFuzzer") public func test(_ start: UnsafeRawPointer, _ count: Int) -> CInt { let bytes = UnsafeRawBufferPointer(start: start, count: count) From 07233c5289457e3b3602f59ee2e66872c35efd78 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 27 Sep 2022 16:22:40 +0100 Subject: [PATCH 029/580] Adopt NIOAsyncSequenceProducer in grpc-swift (#1477) We want to make use of `NIOAsyncSequenceProducer` (or its throwing counterpart) instead of using `PassthroughMessageSequence` and `PassthroughMessageSource`. * Replaced usages of `PassthroughMessageSequence` and `PassthroughMessageSource` with `NIOThrowingAsyncSequenceProducer` grpc-swift now uses `NIOAsyncSequenceProducer` --- .../GRPCAsyncBidirectionalStreamingCall.swift | 32 +++- .../GRPCAsyncRequestStream.swift | 33 +++- .../GRPCAsyncResponseStream.swift | 11 +- .../GRPCAsyncSequenceProducerDelegate.swift | 35 ++++ .../GRPCAsyncServerHandler.swift | 43 ++++- .../GRPCAsyncServerStreamingCall.swift | 23 ++- .../PassthroughMessageSequence.swift | 64 ------- .../PassthroughMessageSource.swift | 176 ------------------ .../PassthroughMessageSourceTests.swift | 160 ---------------- .../GRPCAsyncServerHandlerTests.swift | 118 ++++++------ 10 files changed, 211 insertions(+), 484 deletions(-) create mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSequence.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/PassthroughMessageSourceTests.swift diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 2e6ca4590..43afb23b5 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -15,6 +15,7 @@ */ #if compiler(>=5.6) +import NIOCore import NIOHPACK /// Async-await variant of ``BidirectionalStreamingCall``. @@ -22,7 +23,12 @@ import NIOHPACK public struct GRPCAsyncBidirectionalStreamingCall: Sendable { private let call: Call private let responseParts: StreamingResponseParts - private let responseSource: PassthroughMessageSource + private let responseSource: NIOThrowingAsyncSequenceProducer< + Response, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source /// A request stream writer for sending messages to the server. public let requestStream: GRPCAsyncRequestStreamWriter @@ -80,8 +86,17 @@ public struct GRPCAsyncBidirectionalStreamingCall) { self.call = call self.responseParts = StreamingResponseParts(on: call.eventLoop) { _ in } - self.responseSource = PassthroughMessageSource() - self.responseStream = .init(PassthroughMessageSequence(consuming: self.responseSource)) + let sequence = NIOThrowingAsyncSequenceProducer< + Response, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.makeSequence( + backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), + delegate: GRPCAsyncSequenceProducerDelegate() + ) + self.responseSource = sequence.source + self.responseStream = .init(sequence.sequence) self.requestStream = call.makeRequestStreamWriter() } @@ -96,7 +111,7 @@ public struct GRPCAsyncBidirectionalStreamingCall( responseParts: StreamingResponseParts, - responseSource: PassthroughMessageSource, + responseSource: NIOThrowingAsyncSequenceProducer< + Response, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source, requestStream: GRPCAsyncRequestStreamWriter?, requestType: Request.Type = Request.self ) -> (GRPCClientResponsePart) -> Void { @@ -135,7 +155,7 @@ internal enum AsyncCall { if status.isOk { responseSource.finish() } else { - responseSource.finish(throwing: status) + responseSource.finish(status) } requestStream?.asyncWriter.cancelAsynchronously(withError: status) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift index 278b39c43..0c7fac69a 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift @@ -15,6 +15,8 @@ */ #if compiler(>=5.6) +import NIOCore + /// A type for the stream of request messages send to a gRPC server method. /// /// To enable testability this type provides a static ``GRPCAsyncRequestStream/makeTestingRequestStream()`` @@ -77,16 +79,26 @@ public struct GRPCAsyncRequestStream: AsyncSequence { @usableFromInline enum Backing: Sendable { - case passthroughMessageSequence(PassthroughMessageSequence) case asyncStream(AsyncThrowingStream) + case nioThrowingAsyncSequence(NIOThrowingAsyncSequenceProducer< + Element, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >) } @usableFromInline internal let backing: Backing @inlinable - internal init(_ sequence: PassthroughMessageSequence) { - self.backing = .passthroughMessageSequence(sequence) + internal init(_ sequence: NIOThrowingAsyncSequenceProducer< + Element, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >) { + self.backing = .nioThrowingAsyncSequence(sequence) } @inlinable @@ -112,18 +124,23 @@ public struct GRPCAsyncRequestStream: AsyncSequence { @inlinable public func makeAsyncIterator() -> Iterator { switch self.backing { - case let .passthroughMessageSequence(sequence): - return Self.AsyncIterator(.passthroughMessageSequence(sequence.makeAsyncIterator())) case let .asyncStream(stream): return Self.AsyncIterator(.asyncStream(stream.makeAsyncIterator())) + case let .nioThrowingAsyncSequence(sequence): + return Self.AsyncIterator(.nioThrowingAsyncSequence(sequence.makeAsyncIterator())) } } public struct Iterator: AsyncIteratorProtocol { @usableFromInline enum BackingIterator { - case passthroughMessageSequence(PassthroughMessageSequence.Iterator) case asyncStream(AsyncThrowingStream.Iterator) + case nioThrowingAsyncSequence(NIOThrowingAsyncSequenceProducer< + Element, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.AsyncIterator) } @usableFromInline @@ -137,12 +154,12 @@ public struct GRPCAsyncRequestStream: AsyncSequence { @inlinable public mutating func next() async throws -> Element? { switch self.iterator { - case let .passthroughMessageSequence(iterator): - return try await iterator.next() case var .asyncStream(iterator): let element = try await iterator.next() self.iterator = .asyncStream(iterator) return element + case let .nioThrowingAsyncSequence(iterator): + return try await iterator.next() } } } diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift index b19189523..cf49befd4 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift @@ -15,12 +15,19 @@ */ #if compiler(>=5.6) +import NIOCore + /// This is currently a wrapper around AsyncThrowingStream because we want to be /// able to swap out the implementation for something else in the future. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncResponseStream: AsyncSequence { @usableFromInline - internal typealias WrappedStream = PassthroughMessageSequence + internal typealias WrappedStream = NIOThrowingAsyncSequenceProducer< + Element, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + > @usableFromInline internal let stream: WrappedStream @@ -52,7 +59,5 @@ public struct GRPCAsyncResponseStream: AsyncSequence { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension GRPCAsyncResponseStream: Sendable {} -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCAsyncResponseStream.Iterator: Sendable {} #endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift new file mode 100644 index 000000000..817a48e2a --- /dev/null +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift @@ -0,0 +1,35 @@ +/* + * Copyright 2022, 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. + */ + +#if compiler(>=5.6) + +import NIOCore + +@usableFromInline +internal struct GRPCAsyncSequenceProducerDelegate: NIOAsyncSequenceProducerDelegate { + @inlinable + internal init() {} + + // TODO: this method will have to be implemented when we add support for backpressure. + @inlinable + internal func produceMore() {} + + // TODO: this method will have to be implemented when we add support for backpressure. + @inlinable + internal func didTerminate() {} +} + +#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift index d5c82ee6a..548ca2d0b 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift @@ -235,6 +235,14 @@ internal final class AsyncServerHandler< GRPCAsyncServerCallContext ) async throws -> Void + @usableFromInline + internal typealias AsyncSequenceProducer = NIOThrowingAsyncSequenceProducer< + Request, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + > + @inlinable internal init( context: CallHandlerContext, @@ -422,7 +430,10 @@ internal final class AsyncServerHandler< contextProvider: self ) - let requestSource = PassthroughMessageSource() + let sequenceProducer = AsyncSequenceProducer.makeSequence( + backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), + delegate: GRPCAsyncSequenceProducerDelegate() + ) let writerDelegate = AsyncResponseStreamWriterDelegate( send: self.interceptResponseMessage(_:compression:), @@ -448,12 +459,13 @@ internal final class AsyncServerHandler< // Update our state before invoke the handler. self.handlerStateMachine.handlerInvoked(requestHeaders: headers) self.handlerComponents = ServerHandlerComponents( - requestSource: requestSource, + requestSource: sequenceProducer.source, responseWriter: writer, task: promise.completeWithTask { // We don't have a task cancellation handler here: we do it in `self.cancel()`. try await self.invokeUserHandler( - requestStreamSource: requestSource, + sequence: sequenceProducer.sequence, + sequenceSource: sequenceProducer.source, responseStreamWriter: writer, callContext: handlerContext ) @@ -468,18 +480,19 @@ internal final class AsyncServerHandler< @Sendable @usableFromInline internal func invokeUserHandler( - requestStreamSource: PassthroughMessageSource, + sequence: AsyncSequenceProducer, + sequenceSource: AsyncSequenceProducer.Source, responseStreamWriter: AsyncWriter>, callContext: GRPCAsyncServerCallContext ) async throws { defer { // It's possible the user handler completed before the end of the request stream. We // explicitly finish it to drop any unconsumed inbound messages. - requestStreamSource.finish() + sequenceSource.finish() } do { - let requestStream = GRPCAsyncRequestStream(.init(consuming: requestStreamSource)) + let requestStream = GRPCAsyncRequestStream(sequence) let responseStream = GRPCAsyncResponseStreamWriter(wrapping: responseStreamWriter) try await self.userHandler(requestStream, responseStream, callContext) @@ -530,7 +543,7 @@ internal final class AsyncServerHandler< case .forward: switch self.handlerStateMachine.handleMessage() { case .forward: - self.handlerComponents?.requestSource.yield(request) + _ = self.handlerComponents?.requestSource.yield(request) case .cancel: self.cancel(error: nil) } @@ -809,11 +822,21 @@ internal struct ServerHandlerComponents @usableFromInline - internal let requestSource: PassthroughMessageSource + internal let requestSource: NIOThrowingAsyncSequenceProducer< + Request, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source @inlinable init( - requestSource: PassthroughMessageSource, + requestSource: NIOThrowingAsyncSequenceProducer< + Request, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source, responseWriter: AsyncWriter, task: Task ) { @@ -830,7 +853,7 @@ internal struct ServerHandlerComponents=5.6) +import NIOCore import NIOHPACK /// Async-await variant of ``ServerStreamingCall``. @@ -22,7 +23,12 @@ import NIOHPACK public struct GRPCAsyncServerStreamingCall { private let call: Call private let responseParts: StreamingResponseParts - private let responseSource: PassthroughMessageSource + private let responseSource: NIOThrowingAsyncSequenceProducer< + Response, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source /// The stream of responses from the server. public let responseStream: GRPCAsyncResponseStream @@ -79,8 +85,17 @@ public struct GRPCAsyncServerStreamingCall() - self.responseStream = .init(PassthroughMessageSequence(consuming: self.responseSource)) + let sequence = NIOThrowingAsyncSequenceProducer< + Response, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.makeSequence( + backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), + delegate: GRPCAsyncSequenceProducerDelegate() + ) + self.responseSource = sequence.source + self.responseStream = .init(sequence.sequence) } /// We expose this as the only non-private initializer so that the caller @@ -96,7 +111,7 @@ public struct GRPCAsyncServerStreamingCall=5.6) - -/// An ``AsyncSequence`` adapter for a ``PassthroughMessageSource``.` -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal struct PassthroughMessageSequence: AsyncSequence { - @usableFromInline - internal typealias Element = Element - - @usableFromInline - internal typealias AsyncIterator = Iterator - - /// The source of messages in the sequence. - @usableFromInline - internal let _source: PassthroughMessageSource - - @usableFromInline - internal func makeAsyncIterator() -> Iterator { - return Iterator(storage: self._source) - } - - @usableFromInline - internal init(consuming source: PassthroughMessageSource) { - self._source = source - } - - @usableFromInline - internal struct Iterator: AsyncIteratorProtocol { - @usableFromInline - internal let _storage: PassthroughMessageSource - - fileprivate init(storage: PassthroughMessageSource) { - self._storage = storage - } - - @inlinable - internal func next() async throws -> Element? { - // The storage handles co-operative cancellation, so we don't bother checking here. - return try await self._storage.consumeNextElement() - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension PassthroughMessageSequence: Sendable {} -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension PassthroughMessageSequence.Iterator: Sendable {} - -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift b/Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift deleted file mode 100644 index 07473b564..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/PassthroughMessageSource.swift +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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. - */ -#if compiler(>=5.6) -import NIOConcurrencyHelpers -import NIOCore - -/// The source of messages for a ``PassthroughMessageSequence``.` -/// -/// Values may be provided to the source with calls to ``yield(_:)`` which returns whether the value -/// was accepted (and how many values are yet to be consumed) -- or dropped. -/// -/// The backing storage has an unbounded capacity and callers should use the number of unconsumed -/// values returned from ``yield(_:)`` as an indication of when to stop providing values. -/// -/// The source must be finished exactly once by calling ``finish()`` or ``finish(throwing:)`` to -/// indicate that the sequence should end with an error. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal final class PassthroughMessageSource { - @usableFromInline - internal typealias _ContinuationResult = Result - - /// All state in this class must be accessed via the lock. - /// - /// - Important: We use a `class` with a lock rather than an `actor` as we must guarantee that - /// calls to ``yield(_:)`` are not reordered. - @usableFromInline - internal let _lock: NIOLock - - /// A queue of elements which may be consumed as soon as there is demand. - @usableFromInline - internal var _continuationResults: CircularBuffer<_ContinuationResult> - - /// A continuation which will be resumed in the future. The continuation must be `nil` - /// if ``continuationResults`` is not empty. - @usableFromInline - internal var _continuation: Optional> - - /// True if a terminal continuation result (`.success(nil)` or `.failure()`) has been seen. - /// No more values may be enqueued to `continuationResults` if this is `true`. - @usableFromInline - internal var _isTerminated: Bool - - @usableFromInline - internal init(initialBufferCapacity: Int = 16) { - self._lock = NIOLock() - self._continuationResults = CircularBuffer(initialCapacity: initialBufferCapacity) - self._continuation = nil - self._isTerminated = false - } - - // MARK: - Append / Yield - - @usableFromInline - internal enum YieldResult: Hashable { - /// The value was accepted. The `queueDepth` indicates how many elements are waiting to be - /// consumed. - /// - /// If `queueDepth` is zero then the value was consumed immediately. - case accepted(queueDepth: Int) - - /// The value was dropped because the source has already been finished. - case dropped - } - - @inlinable - @discardableResult - internal func yield(_ element: Element) -> YieldResult { - let continuationResult: _ContinuationResult = .success(element) - return self._yield(continuationResult, isTerminator: false) - } - - @inlinable - @discardableResult - internal func finish(throwing error: Failure? = nil) -> YieldResult { - let continuationResult: _ContinuationResult = error.map { .failure($0) } ?? .success(nil) - return self._yield(continuationResult, isTerminator: true) - } - - @usableFromInline - internal enum _YieldResult { - /// The sequence has already been terminated; drop the element. - case alreadyTerminated - /// The element was added to the queue to be consumed later. - case queued(Int) - /// Demand for an element already existed: complete the continuation with the result being - /// yielded. - case resume(CheckedContinuation) - } - - @inlinable - internal func _yield( - _ continuationResult: _ContinuationResult, isTerminator: Bool - ) -> YieldResult { - let result: _YieldResult = self._lock.withLock { - if self._isTerminated { - return .alreadyTerminated - } else { - self._isTerminated = isTerminator - } - - if let continuation = self._continuation { - self._continuation = nil - return .resume(continuation) - } else { - self._continuationResults.append(continuationResult) - return .queued(self._continuationResults.count) - } - } - - let yieldResult: YieldResult - switch result { - case let .queued(size): - yieldResult = .accepted(queueDepth: size) - case let .resume(continuation): - // If we resume a continuation then the queue must be empty - yieldResult = .accepted(queueDepth: 0) - continuation.resume(with: continuationResult) - case .alreadyTerminated: - yieldResult = .dropped - } - - return yieldResult - } - - // MARK: - Next - - @inlinable - internal func consumeNextElement() async throws -> Element? { - self._lock.lock() - if let nextResult = self._continuationResults.popFirst() { - self._lock.unlock() - return try nextResult.get() - } else if self._isTerminated { - self._lock.unlock() - return nil - } - - // Slow path; we need a continuation. - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - // Nothing buffered and not terminated yet: save the continuation for later. - precondition(self._continuation == nil) - self._continuation = continuation - self._lock.unlock() - } - } onCancel: { - let continuation: CheckedContinuation? = self._lock.withLock { - let cont = self._continuation - self._continuation = nil - return cont - } - - continuation?.resume(throwing: CancellationError()) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -// @unchecked is ok: mutable state is accessed/modified via a lock. -extension PassthroughMessageSource: @unchecked Sendable {} - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/PassthroughMessageSourceTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/PassthroughMessageSourceTests.swift deleted file mode 100644 index c3a2e4431..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/PassthroughMessageSourceTests.swift +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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. - */ -#if compiler(>=5.6) -@testable import GRPC -import XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -class PassthroughMessageSourceTests: GRPCTestCase { - func testBasicUsage() async throws { - let source = PassthroughMessageSource() - let sequence = PassthroughMessageSequence(consuming: source) - - XCTAssertEqual(source.yield("foo"), .accepted(queueDepth: 1)) - XCTAssertEqual(source.yield("bar"), .accepted(queueDepth: 2)) - XCTAssertEqual(source.yield("baz"), .accepted(queueDepth: 3)) - - let firstTwo = try await sequence.prefix(2).collect() - XCTAssertEqual(firstTwo, ["foo", "bar"]) - - XCTAssertEqual(source.yield("bar"), .accepted(queueDepth: 2)) - XCTAssertEqual(source.yield("foo"), .accepted(queueDepth: 3)) - - XCTAssertEqual(source.finish(), .accepted(queueDepth: 4)) - - let theRest = try await sequence.collect() - XCTAssertEqual(theRest, ["baz", "bar", "foo"]) - } - - func testFinishWithError() async throws { - let source = PassthroughMessageSource() - - XCTAssertEqual(source.yield("one"), .accepted(queueDepth: 1)) - XCTAssertEqual(source.yield("two"), .accepted(queueDepth: 2)) - XCTAssertEqual(source.yield("three"), .accepted(queueDepth: 3)) - XCTAssertEqual(source.finish(throwing: TestError()), .accepted(queueDepth: 4)) - - // We should still be able to get the elements before the error. - let sequence = PassthroughMessageSequence(consuming: source) - let elements = try await sequence.prefix(3).collect() - XCTAssertEqual(elements, ["one", "two", "three"]) - - do { - for try await element in sequence { - XCTFail("Unexpected value '\(element)'") - } - XCTFail("AsyncSequence did not throw") - } catch { - XCTAssert(error is TestError) - } - } - - func testYieldAfterFinish() async throws { - let source = PassthroughMessageSource() - XCTAssertEqual(source.finish(), .accepted(queueDepth: 1)) - XCTAssertEqual(source.yield("foo"), .dropped) - - let sequence = PassthroughMessageSequence(consuming: source) - let elements = try await sequence.count() - XCTAssertEqual(elements, 0) - } - - func testMultipleFinishes() async throws { - let source = PassthroughMessageSource() - XCTAssertEqual(source.finish(), .accepted(queueDepth: 1)) - XCTAssertEqual(source.finish(), .dropped) - XCTAssertEqual(source.finish(throwing: TestError()), .dropped) - - let sequence = PassthroughMessageSequence(consuming: source) - let elements = try await sequence.count() - XCTAssertEqual(elements, 0) - } - - func testConsumeBeforeYield() async throws { - let source = PassthroughMessageSource() - let sequence = PassthroughMessageSequence(consuming: source) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask(priority: .high) { - let iterator = sequence.makeAsyncIterator() - if let next = try await iterator.next() { - XCTAssertEqual(next, "one") - } else { - XCTFail("No value produced") - } - } - - group.addTask(priority: .low) { - let result = source.yield("one") - // We can't guarantee that this task will run after the other so we *may* have a queue - // depth of one. - XCTAssert(result == .accepted(queueDepth: 0) || result == .accepted(queueDepth: 1)) - } - } - } - - func testConsumeBeforeFinish() async throws { - let source = PassthroughMessageSource() - let sequence = PassthroughMessageSequence(consuming: source) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask(priority: .high) { - let iterator = sequence.makeAsyncIterator() - await XCTAssertThrowsError(_ = try await iterator.next()) { error in - XCTAssert(error is TestError) - } - } - - group.addTask(priority: .low) { - let result = source.finish(throwing: TestError()) - // We can't guarantee that this task will run after the other so we *may* have a queue - // depth of one. - XCTAssert(result == .accepted(queueDepth: 0) || result == .accepted(queueDepth: 1)) - } - } - } - - func testCooperativeCancellationOfSourceOnNext() async throws { - let source = PassthroughMessageSource() - try await withTaskCancelledAfter(nanoseconds: 100_000) { - do { - _ = try await source.consumeNextElement() - XCTFail("consumeNextElement() should throw CancellationError") - } catch { - XCTAssert(error is CancellationError) - } - } - } - - func testCooperativeCancellationOfSequenceOnNext() async throws { - let source = PassthroughMessageSource() - let sequence = PassthroughMessageSequence(consuming: source) - try await withTaskCancelledAfter(nanoseconds: 100_000) { - do { - for try await _ in sequence { - XCTFail("consumeNextElement() should throw CancellationError") - } - XCTFail("consumeNextElement() should throw CancellationError") - } catch { - XCTAssert(error is CancellationError) - } - } - } -} - -fileprivate struct TestError: Error {} - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift index 0b340a6c9..589c36446 100644 --- a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift +++ b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift @@ -126,18 +126,18 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertMetadata() + await responseStream.next().assertMetadata() for expected in ["1", "2", "3"] { - try await responseStream.next().assertMessage { buffer, metadata in + await responseStream.next().assertMessage { buffer, metadata in XCTAssertEqual(buffer, .init(string: expected)) XCTAssertFalse(metadata.compress) } } - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .ok) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testHappyPathWithCompressionEnabled() async throws { @@ -158,15 +158,15 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertMetadata() + await responseStream.next().assertMetadata() for expected in ["1", "2", "3"] { - try await responseStream.next().assertMessage { buffer, metadata in + await responseStream.next().assertMessage { buffer, metadata in XCTAssertEqual(buffer, .init(string: expected)) XCTAssertTrue(metadata.compress) } } - try await responseStream.next().assertStatus() - try await responseStream.next().assertNil() + await responseStream.next().assertStatus() + await responseStream.next().assertNil() } func testHappyPathWithCompressionEnabledButDisabledByCaller() async throws { @@ -193,15 +193,15 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertMetadata() + await responseStream.next().assertMetadata() for expected in ["1", "2", "3"] { - try await responseStream.next().assertMessage { buffer, metadata in + await responseStream.next().assertMessage { buffer, metadata in XCTAssertEqual(buffer, .init(string: expected)) XCTAssertFalse(metadata.compress) } } - try await responseStream.next().assertStatus() - try await responseStream.next().assertNil() + await responseStream.next().assertStatus() + await responseStream.next().assertNil() } func testResponseHeadersAndTrailersSentFromContext() async throws { @@ -220,14 +220,14 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertMetadata { headers in + await responseStream.next().assertMetadata { headers in XCTAssertEqual(headers, ["pontiac": "bandit"]) } - try await responseStream.next().assertMessage() - try await responseStream.next().assertStatus { _, trailers in + await responseStream.next().assertMessage() + await responseStream.next().assertStatus { _, trailers in XCTAssertEqual(trailers, ["disco": "strangler"]) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testThrowingDeserializer() async throws { @@ -249,10 +249,10 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .internalError) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testThrowingSerializer() async throws { @@ -274,11 +274,11 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertMetadata() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertMetadata() + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .internalError) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testReceiveMessageBeforeHeaders() async throws { @@ -294,10 +294,10 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .internalError) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testReceiveMultipleHeaders() async throws { @@ -314,10 +314,10 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .internalError) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testFinishBeforeStarting() async throws { @@ -330,8 +330,8 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus() - try await responseStream.next().assertNil() + await responseStream.next().assertStatus() + await responseStream.next().assertNil() } func testFinishAfterHeaders() async throws { @@ -345,8 +345,8 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus() - try await responseStream.next().assertNil() + await responseStream.next().assertStatus() + await responseStream.next().assertNil() } func testFinishAfterMessage() async throws { @@ -359,18 +359,18 @@ class AsyncServerHandlerTests: GRPCTestCase { // Await the metadata and message so we know the user function is running. let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertMetadata() - try await responseStream.next().assertMessage() + await responseStream.next().assertMetadata() + await responseStream.next().assertMessage() // Finish, i.e. terminate early. self.loop.execute { handler.finish() } - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .internalError) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testErrorAfterHeaders() async throws { @@ -384,11 +384,11 @@ class AsyncServerHandlerTests: GRPCTestCase { // We don't send a message so we don't expect any responses. As metadata is sent lazily on the // first message we don't expect to get metadata back either. let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .unavailable) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testErrorAfterMessage() async throws { @@ -401,8 +401,8 @@ class AsyncServerHandlerTests: GRPCTestCase { // Wait the metadata and message; i.e. for function to have been invoked. let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertMetadata() - try await responseStream.next().assertMessage() + await responseStream.next().assertMetadata() + await responseStream.next().assertMessage() // Throw in an error. self.loop.execute { @@ -410,10 +410,10 @@ class AsyncServerHandlerTests: GRPCTestCase { } // The RPC should end. - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .unavailable) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testHandlerThrowsGRPCStatusOKResultsInUnknownStatus() async throws { @@ -428,10 +428,10 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .unknown) } - try await responseStream.next().assertNil() + await responseStream.next().assertNil() } func testUnaryHandlerReceivingMultipleMessages() async throws { @@ -460,7 +460,7 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .internalError) } } @@ -494,24 +494,36 @@ class AsyncServerHandlerTests: GRPCTestCase { } let responseStream = self.recorder.responseSequence.makeAsyncIterator() - try await responseStream.next().assertStatus { status, _ in + await responseStream.next().assertStatus { status, _ in XCTAssertEqual(status.code, .internalError) } } } internal final class AsyncResponseStream: GRPCServerResponseWriter { - private let source: PassthroughMessageSource, Never> + private let source: NIOAsyncSequenceProducer< + GRPCServerResponsePart, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source - internal var responseSequence: PassthroughMessageSequence< + internal var responseSequence: NIOAsyncSequenceProducer< GRPCServerResponsePart, - Never - > { - return .init(consuming: self.source) - } + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + > init() { - self.source = PassthroughMessageSource() + let sequenceProducer = NIOAsyncSequenceProducer< + GRPCServerResponsePart, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.makeSequence( + backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), + delegate: GRPCAsyncSequenceProducerDelegate() + ) + self.source = sequenceProducer.source + self.responseSequence = sequenceProducer.sequence } func sendMetadata( @@ -519,7 +531,7 @@ internal final class AsyncResponseStream: GRPCServerResponseWriter { flush: Bool, promise: EventLoopPromise? ) { - self.source.yield(.metadata(metadata)) + _ = self.source.yield(.metadata(metadata)) promise?.succeed(()) } @@ -528,7 +540,7 @@ internal final class AsyncResponseStream: GRPCServerResponseWriter { metadata: MessageMetadata, promise: EventLoopPromise? ) { - self.source.yield(.message(bytes, metadata)) + _ = self.source.yield(.message(bytes, metadata)) promise?.succeed(()) } @@ -537,7 +549,7 @@ internal final class AsyncResponseStream: GRPCServerResponseWriter { trailers: HPACKHeaders, promise: EventLoopPromise? ) { - self.source.yield(.end(status, trailers)) + _ = self.source.yield(.end(status, trailers)) self.source.finish() promise?.succeed(()) } From 16de20453cbba83b0a3ff1ac5790b1d6910c03f8 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 29 Sep 2022 16:36:34 +0100 Subject: [PATCH 030/580] Adopt NIOAsyncWriter (#1493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation This change adds support for using `swift-nio`’s new `NIOAsyncWriter` instead of the custom `AsyncWriter` we used to have. Modifications * Deleted `AsyncWriter` and replaced its usages with `NIOAsyncWriter` Result `grpc-swift` now uses `NIOAsyncWriter ` --- .../GRPC/AsyncAwaitSupport/AsyncWriter.swift | 332 ------------------ .../Call+AsyncRequestStreamWriter.swift | 33 +- .../GRPCAsyncBidirectionalStreamingCall.swift | 22 +- .../GRPCAsyncClientStreamingCall.swift | 10 +- .../GRPCAsyncRequestStream.swift | 41 +-- .../GRPCAsyncRequestStreamWriter.swift | 71 +--- .../GRPCAsyncResponseStream.swift | 1 + .../GRPCAsyncResponseStreamWriter.swift | 67 +--- .../GRPCAsyncServerHandler.swift | 201 +++++------ .../GRPCAsyncServerStreamingCall.swift | 22 +- .../GRPCAsyncWriterSinkDelegate.swift | 48 +++ .../AsyncAwaitSupport/NIOAsyncWrappers.swift | 51 +++ Sources/GRPC/ServerErrorProcessor.swift | 2 +- .../AsyncAwaitSupport/AsyncWriterTests.swift | 305 ---------------- .../GRPCAsyncServerHandlerTests.swift | 17 +- 15 files changed, 309 insertions(+), 914 deletions(-) delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift create mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift create mode 100644 Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift deleted file mode 100644 index aba4c9de5..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncWriter.swift +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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. - */ -#if compiler(>=5.6) -import NIOCore - -/// An asynchronous writer which forwards messages to a delegate. -/// -/// Forwarding of messages to the delegate may be paused and resumed by controlling the writability -/// of the writer. This may be controlled by calls to ``toggleWritability()``. When the writer is -/// paused (by becoming unwritable) calls to ``write(_:)`` may suspend. When the writer is resumed -/// (by becoming writable) any calls which are suspended may be resumed. -/// -/// The writer must also be "finished" with a final value: as for writing, calls to ``finish(_:)`` -/// may suspend if the writer has been paused. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal final actor AsyncWriter: Sendable { - @usableFromInline - internal typealias Element = Delegate.Element - - @usableFromInline - internal typealias End = Delegate.End - - /// A value pending a write. - @usableFromInline - internal struct _Pending: Sendable { - @usableFromInline - var value: Value - - @usableFromInline - var continuation: CheckedContinuation - - @inlinable - internal init(_ value: Value, continuation: CheckedContinuation) { - self.value = value - self.continuation = continuation - } - } - - @usableFromInline - typealias PendingElement = _Pending - - @usableFromInline - typealias PendingEnd = _Pending - - @usableFromInline - internal enum _CompletionState: Sendable { - /// Finish hasn't been called yet. May move to `pending` or `completed`. - case incomplete - /// Finish has been called but the writer is paused. May move to `completed`. - case pending(PendingEnd) - /// The completion message has been sent to the delegate. This is a terminal state. - case completed - - /// Move from `pending` to `completed` and return the `PendingCompletion`. Returns `nil` if - /// the state was not `pending`. - @inlinable - mutating func completeIfPending() -> PendingEnd? { - switch self { - case let .pending(pending): - self = .completed - return pending - case .incomplete, .completed: - return nil - } - } - - @usableFromInline - var isPendingOrCompleted: Bool { - switch self { - case .incomplete: - return false - case .pending, .completed: - return true - } - } - } - - /// The maximum number of pending elements. `pendingElements` must not grow beyond this limit. - @usableFromInline - internal let _maxPendingElements: Int - - /// The maximum number of writes to the delegate made in `resume` before yielding to allow other - /// values to be queued. - @usableFromInline - internal let _maxWritesBeforeYield: Int - - /// Elements and continuations which have been buffered but are awaiting consumption by the - /// delegate. - @usableFromInline - internal var _pendingElements: CircularBuffer - - /// The completion state of the writer. - @usableFromInline - internal var _completionState: _CompletionState - - /// Whether the writer is paused. - @usableFromInline - internal var _isPaused: Bool - - /// The delegate to process elements. By convention we call the delegate before resuming any - /// continuation. - @usableFromInline - internal let _delegate: Delegate - - @inlinable - internal init( - maxPendingElements: Int = 16, - maxWritesBeforeYield: Int = 5, - isWritable: Bool = true, - delegate: Delegate - ) { - self._maxPendingElements = maxPendingElements - self._maxWritesBeforeYield = maxWritesBeforeYield - self._pendingElements = CircularBuffer(initialCapacity: maxPendingElements) - self._completionState = .incomplete - self._isPaused = !isWritable - self._delegate = delegate - } - - deinit { - switch self._completionState { - case .completed: - () - case .incomplete, .pending: - assertionFailure("writer has not completed is pending completion") - } - } - - /// As ``toggleWritability()`` but executed asynchronously. - @usableFromInline - internal nonisolated func toggleWritabilityAsynchronously() { - Task { - await self.toggleWritability() - } - } - - /// Toggles whether the writer is writable or not. The writer is initially writable. - /// - /// If the writer becomes writable then it may resume writes to the delegate. If it becomes - /// unwritable then calls to `write` may suspend until the writability changes again. - /// - /// This API does not offer explicit control over the writability state so the caller must ensure - /// calls to this function correspond with changes in writability. The reason for this is that the - /// underlying type is an `actor` and updating its state is therefore asynchronous. However, - /// this functions is not called from an asynchronous context so it is not possible to `await` - /// state updates to complete. Instead, changing the state is via a `nonisolated` function on - /// the `actor` which spawns a new task. If this or a similar API allowed the writability to be - /// explicitly set then calls to that API are not guaranteed to be ordered which may lead to - /// deadlock. - @usableFromInline - internal func toggleWritability() async { - if self._isPaused { - self._isPaused = false - await self.resumeWriting() - } else { - self._isPaused = true - } - } - - private func resumeWriting() async { - var writes = 0 - - while !self._isPaused { - if let pendingElement = self._pendingElements.popFirst() { - self._delegate.write(pendingElement.value) - pendingElement.continuation.resume() - } else if let pendingCompletion = self._completionState.completeIfPending() { - self._delegate.writeEnd(pendingCompletion.value) - pendingCompletion.continuation.resume() - } else { - break - } - - // `writes` will never exceed `maxWritesBeforeYield` so unchecked arithmetic is okay here. - writes &+= 1 - if writes == self._maxWritesBeforeYield { - writes = 0 - // We yield every so often to let the delegate (i.e. 'NIO.Channel') catch up since it may - // decide it is no longer writable. - await Task.yield() - } - } - } - - /// As ``cancel()`` but executed asynchronously. - @usableFromInline - internal nonisolated func cancelAsynchronously(withError error: Error) { - Task { - await self.cancel(withError: error) - } - } - - /// Cancel all pending writes. - /// - /// Any pending writes will be dropped and their continuations will be resumed with - /// a `CancellationError`. Any writes after cancellation has completed will also fail. - @usableFromInline - internal func cancel(withError error: Error) { - // If there's an end we should fail that last. - let pendingEnd: PendingEnd? - - // Mark our state as completed before resuming any continuations (any future writes should fail - // immediately). - switch self._completionState { - case .incomplete: - pendingEnd = nil - self._completionState = .completed - - case let .pending(pending): - pendingEnd = pending - self._completionState = .completed - - case .completed: - pendingEnd = nil - } - - while let pending = self._pendingElements.popFirst() { - pending.continuation.resume(throwing: error) - } - - pendingEnd?.continuation.resume(throwing: error) - } - - /// Write an `element`. - /// - /// The call may be suspend if the writer is paused. - /// - /// Throws: ``GRPCAsyncWriterError`` if the writer has already been finished or too many write tasks - /// have been suspended. - @inlinable - internal func write(_ element: Element) async throws { - // There are three outcomes of writing: - // - write the element directly (if the writer isn't paused and no writes are pending) - // - queue the element (the writer is paused or there are writes already pending) - // - error (the writer is complete or the queue is full). - return try await withTaskCancellationHandler { - if self._completionState.isPendingOrCompleted { - throw GRPCAsyncWriterError.alreadyFinished - } else if !self._isPaused, self._pendingElements.isEmpty { - self._delegate.write(element) - } else if self._pendingElements.count < self._maxPendingElements { - // The continuation will be resumed later. - try await withCheckedThrowingContinuation { continuation in - self._pendingElements.append(PendingElement(element, continuation: continuation)) - } - } else { - throw GRPCAsyncWriterError.tooManyPendingWrites - } - } onCancel: { - self.cancelAsynchronously(withError: CancellationError()) - } - } - - /// Write the final element - @inlinable - internal func finish(_ end: End) async throws { - return try await withTaskCancellationHandler { - if self._completionState.isPendingOrCompleted { - throw GRPCAsyncWriterError.alreadyFinished - } else if !self._isPaused, self._pendingElements.isEmpty { - self._completionState = .completed - self._delegate.writeEnd(end) - } else { - try await withCheckedThrowingContinuation { continuation in - // Either we're paused or there are pending writes which must be consumed first. - self._completionState = .pending(PendingEnd(end, continuation: continuation)) - } - } - } onCancel: { - self.cancelAsynchronously(withError: CancellationError()) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncWriter where End == Void { - @inlinable - internal func finish() async throws { - try await self.finish(()) - } -} - -public struct GRPCAsyncWriterError: Error, Hashable { - private let wrapped: Wrapped - - @usableFromInline - internal enum Wrapped: Sendable { - case tooManyPendingWrites - case alreadyFinished - } - - @usableFromInline - internal init(_ wrapped: Wrapped) { - self.wrapped = wrapped - } - - /// There are too many writes pending. This may occur when too many Tasks are writing - /// concurrently. - public static let tooManyPendingWrites = Self(.tooManyPendingWrites) - - /// The writer has already finished. This may occur when the RPC completes prematurely, or when - /// a user calls finish more than once. - public static let alreadyFinished = Self(.alreadyFinished) -} - -@usableFromInline -internal protocol AsyncWriterDelegate: AnyObject, Sendable { - associatedtype Element: Sendable - associatedtype End: Sendable - - @inlinable - func write(_ element: Element) - - @inlinable - func writeEnd(_ end: End) -} - -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift index 98d791f1e..57480f356 100644 --- a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift @@ -14,20 +14,35 @@ * limitations under the License. */ #if compiler(>=5.6) +import NIOCore @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension Call where Request: Sendable, Response: Sendable { - internal func makeRequestStreamWriter() -> GRPCAsyncRequestStreamWriter { - let delegate = GRPCAsyncRequestStreamWriter.Delegate( - compressionEnabled: self.options.messageEncoding.enabledForRequests - ) { request, metadata in - self.send(.message(request, metadata), promise: nil) - } finish: { - self.send(.end, promise: nil) - } + typealias AsyncWriter = NIOAsyncWriter< + (Request, Compression), + GRPCAsyncWriterSinkDelegate<(Request, Compression)> + > + internal func makeRequestStreamWriter() + -> (GRPCAsyncRequestStreamWriter, AsyncWriter.Sink) { + let delegate = GRPCAsyncWriterSinkDelegate<(Request, Compression)>( + didYield: { requests in + for (request, compression) in requests { + let compress = compression + .isEnabled(callDefault: self.options.messageEncoding.enabledForRequests) + + // TODO: be smarter about inserting flushes. + // We currently always flush after every write which may trigger more syscalls than necessary. + let metadata = MessageMetadata(compress: compress, flush: true) + self.send(.message(request, metadata), promise: nil) + } + }, + didTerminate: { _ in self.send(.end, promise: nil) } + ) + + let writer = NIOAsyncWriter.makeWriter(isWritable: false, delegate: delegate) // Start as not-writable; writability will be toggled when the stream comes up. - return GRPCAsyncRequestStreamWriter(asyncWriter: .init(isWritable: false, delegate: delegate)) + return (GRPCAsyncRequestStreamWriter(asyncWriter: writer.writer), writer.sink) } } diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 43afb23b5..26c20e501 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -29,6 +29,7 @@ public struct GRPCAsyncBidirectionalStreamingCall.Source + private let requestSink: AsyncSink<(Request, Compression)> /// A request stream writer for sending messages to the server. public let requestStream: GRPCAsyncRequestStreamWriter @@ -86,7 +87,8 @@ public struct GRPCAsyncBidirectionalStreamingCall) { self.call = call self.responseParts = StreamingResponseParts(on: call.eventLoop) { _ in } - let sequence = NIOThrowingAsyncSequenceProducer< + + let sequenceProducer = NIOThrowingAsyncSequenceProducer< Response, Error, NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, @@ -95,9 +97,12 @@ public struct GRPCAsyncBidirectionalStreamingCall=5.6) +import NIOCore import NIOHPACK /// Async-await variant of ``ClientStreamingCall``. @@ -22,6 +23,7 @@ import NIOHPACK public struct GRPCAsyncClientStreamingCall: Sendable { private let call: Call private let responseParts: UnaryResponseParts + private let requestSink: AsyncSink<(Request, Compression)> /// A request stream writer for sending messages to the server. public let requestStream: GRPCAsyncRequestStreamWriter @@ -81,7 +83,9 @@ public struct GRPCAsyncClientStreamingCall) { self.call = call self.responseParts = UnaryResponseParts(on: call.eventLoop) - self.requestStream = call.makeRequestStreamWriter() + let (requestStream, requestSink) = call.makeRequestStreamWriter() + self.requestStream = requestStream + self.requestSink = AsyncSink(wrapping: requestSink) } /// We expose this as the only non-private initializer so that the caller @@ -91,11 +95,11 @@ public struct GRPCAsyncClientStreamingCall: AsyncSequence { + @usableFromInline + internal typealias _AsyncSequenceProducer = NIOThrowingAsyncSequenceProducer< + Element, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + > + /// A source used for driving a ``GRPCAsyncRequestStream`` during tests. public struct Source { @usableFromInline @@ -80,25 +85,15 @@ public struct GRPCAsyncRequestStream: AsyncSequence { @usableFromInline enum Backing: Sendable { case asyncStream(AsyncThrowingStream) - case nioThrowingAsyncSequence(NIOThrowingAsyncSequenceProducer< - Element, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >) + case throwingAsyncSequenceProducer(_AsyncSequenceProducer) } @usableFromInline internal let backing: Backing @inlinable - internal init(_ sequence: NIOThrowingAsyncSequenceProducer< - Element, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >) { - self.backing = .nioThrowingAsyncSequence(sequence) + internal init(_ sequence: _AsyncSequenceProducer) { + self.backing = .throwingAsyncSequenceProducer(sequence) } @inlinable @@ -126,8 +121,8 @@ public struct GRPCAsyncRequestStream: AsyncSequence { switch self.backing { case let .asyncStream(stream): return Self.AsyncIterator(.asyncStream(stream.makeAsyncIterator())) - case let .nioThrowingAsyncSequence(sequence): - return Self.AsyncIterator(.nioThrowingAsyncSequence(sequence.makeAsyncIterator())) + case let .throwingAsyncSequenceProducer(sequence): + return Self.AsyncIterator(.throwingAsyncSequenceProducer(sequence.makeAsyncIterator())) } } @@ -135,12 +130,7 @@ public struct GRPCAsyncRequestStream: AsyncSequence { @usableFromInline enum BackingIterator { case asyncStream(AsyncThrowingStream.Iterator) - case nioThrowingAsyncSequence(NIOThrowingAsyncSequenceProducer< - Element, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.AsyncIterator) + case throwingAsyncSequenceProducer(_AsyncSequenceProducer.AsyncIterator) } @usableFromInline @@ -153,12 +143,13 @@ public struct GRPCAsyncRequestStream: AsyncSequence { @inlinable public mutating func next() async throws -> Element? { + if Task.isCancelled { throw GRPCStatus(code: .cancelled) } switch self.iterator { case var .asyncStream(iterator): let element = try await iterator.next() self.iterator = .asyncStream(iterator) return element - case let .nioThrowingAsyncSequence(iterator): + case let .throwingAsyncSequenceProducer(iterator): return try await iterator.next() } } diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift index 53b0d150a..3a12f229b 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift @@ -14,6 +14,7 @@ * limitations under the License. */ #if compiler(>=5.6) +import NIOCore /// An object allowing the holder -- a client -- to send requests on an RPC. /// @@ -33,10 +34,16 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncRequestStreamWriter: Sendable { @usableFromInline - internal let asyncWriter: AsyncWriter> + typealias AsyncWriter = NIOAsyncWriter< + (Request, Compression), + GRPCAsyncWriterSinkDelegate<(Request, Compression)> + > + + @usableFromInline + /* private */ internal let asyncWriter: AsyncWriter @inlinable - internal init(asyncWriter: AsyncWriter>) { + internal init(asyncWriter: AsyncWriter) { self.asyncWriter = asyncWriter } @@ -62,65 +69,17 @@ public struct GRPCAsyncRequestStreamWriter: Sendable { _ request: Request, compression: Compression = .deferToCallDefault ) async throws { - try await self.asyncWriter.write((request, compression)) + try await self.asyncWriter.yield((request, compression)) } - /// Finish the request stream for the RPC. This must be called when there are no more requests to - /// be sent. - /// - /// - Throws: ``GRPCAsyncWriterError`` if the request stream has already been finished. + /// Finish the request stream for the RPC. This must be called when there are no more requests to be sent. public func finish() async throws { - try await self.asyncWriter.finish() + self.asyncWriter.finish() } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCAsyncRequestStreamWriter { - /// A delegate for the writer which writes messages to an underlying receiver.` - @usableFromInline - internal final class Delegate: AsyncWriterDelegate, Sendable { - @usableFromInline - internal typealias Element = (Request, Compression) - - @usableFromInline - internal typealias End = Void - - @usableFromInline - internal let _compressionEnabled: Bool - - @usableFromInline - internal let _send: @Sendable (Request, MessageMetadata) -> Void - - @usableFromInline - internal let _finish: @Sendable () -> Void - - @inlinable - internal init( - compressionEnabled: Bool, - send: @Sendable @escaping (Request, MessageMetadata) -> Void, - finish: @Sendable @escaping () -> Void - ) { - self._compressionEnabled = compressionEnabled - self._send = send - self._finish = finish - } - - @inlinable - internal func write(_ element: (Request, Compression)) { - let (request, compression) = element - let compress = compression.isEnabled(callDefault: self._compressionEnabled) - - // TODO: be smarter about inserting flushes. - // - // We currently always flush after every write which may trigger more syscalls than necessary. - let metadata = MessageMetadata(compress: compress, flush: true) - self._send(request, metadata) - } - @inlinable - internal func writeEnd(_ end: Void) { - self._finish() - } + /// Finish the request stream for the RPC with the given error. + internal func finish(_ error: Error) { + self.asyncWriter.finish(error: error) } } diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift index cf49befd4..dc4bb6747 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + #if compiler(>=5.6) import NIOCore diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift index 3fb1f8792..d7939e2d8 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift @@ -15,6 +15,7 @@ */ #if compiler(>=5.6) +import NIOCore /// Writer for server-streaming RPC handlers to provide responses. /// @@ -22,6 +23,12 @@ /// method which allows you to create a stream that you can drive. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct GRPCAsyncResponseStreamWriter: Sendable { + @usableFromInline + internal typealias AsyncWriter = NIOAsyncWriter< + (Response, Compression), + GRPCAsyncWriterSinkDelegate<(Response, Compression)> + > + /// An `AsyncSequence` backing a ``GRPCAsyncResponseStreamWriter`` for testing purposes. /// /// - Important: This `AsyncSequence` is never finishing. @@ -91,21 +98,15 @@ public struct GRPCAsyncResponseStreamWriter: Sendable { @usableFromInline enum Backing: Sendable { - case asyncWriter(AsyncWriter) + case asyncWriter(AsyncWriter) case closure(@Sendable ((Response, Compression)) async -> Void) } - @usableFromInline - internal typealias Element = (Response, Compression) - - @usableFromInline - internal typealias Delegate = AsyncResponseStreamWriterDelegate - @usableFromInline internal let backing: Backing @inlinable - internal init(wrapping asyncWriter: AsyncWriter) { + internal init(wrapping asyncWriter: AsyncWriter) { self.backing = .asyncWriter(asyncWriter) } @@ -121,7 +122,7 @@ public struct GRPCAsyncResponseStreamWriter: Sendable { ) async throws { switch self.backing { case let .asyncWriter(writer): - try await writer.write((response, compression)) + try await writer.yield((response, compression)) case let .closure(closure): await closure((response, compression)) @@ -151,52 +152,4 @@ public struct GRPCAsyncResponseStreamWriter: Sendable { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal final class AsyncResponseStreamWriterDelegate: AsyncWriterDelegate { - @usableFromInline - internal typealias Element = (Response, Compression) - - @usableFromInline - internal typealias End = GRPCStatus - - @usableFromInline - internal let _send: @Sendable (Response, Compression) -> Void - - @usableFromInline - internal let _finish: @Sendable (GRPCStatus) -> Void - - // Create a new AsyncResponseStreamWriterDelegate. - // - // - Important: the `send` and `finish` closures must be thread-safe. - @inlinable - internal init( - send: @escaping @Sendable (Response, Compression) -> Void, - finish: @escaping @Sendable (GRPCStatus) -> Void - ) { - self._send = send - self._finish = finish - } - - @inlinable - internal func _send( - _ response: Response, - compression: Compression = .deferToCallDefault - ) { - self._send(response, compression) - } - - // MARK: - AsyncWriterDelegate conformance. - - @inlinable - internal func write(_ element: (Response, Compression)) { - self._send(element.0, compression: element.1) - } - - @inlinable - internal func writeEnd(_ end: GRPCStatus) { - self._finish(end) - } -} - #endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift index 548ca2d0b..d7ba2b552 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift @@ -14,6 +14,7 @@ * limitations under the License. */ #if compiler(>=5.6) +import DequeModule import Logging import NIOCore import NIOHPACK @@ -224,7 +225,8 @@ internal final class AsyncServerHandler< @usableFromInline internal private(set) var handlerComponents: Optional + Response, + GRPCAsyncWriterSinkDelegate<(Response, Compression)> >> /// The user provided function to execute. @@ -430,43 +432,41 @@ internal final class AsyncServerHandler< contextProvider: self ) - let sequenceProducer = AsyncSequenceProducer.makeSequence( - backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), + let backpressureStrategy = NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark( + lowWatermark: 10, + highWatermark: 50 + ) + let requestSequenceProducer = NIOThrowingAsyncSequenceProducer.makeSequence( + elementType: Request.self, + failureType: Error.self, + backPressureStrategy: backpressureStrategy, delegate: GRPCAsyncSequenceProducerDelegate() ) - let writerDelegate = AsyncResponseStreamWriterDelegate( - send: self.interceptResponseMessage(_:compression:), - finish: self.interceptResponseStatus(_:) + let responseWriter = NIOAsyncWriter.makeWriter( + isWritable: true, + delegate: GRPCAsyncWriterSinkDelegate<(Response, Compression)>( + didYield: self.interceptResponseMessages, + didTerminate: { error in + self.interceptTermination(error) + } + ) ) - let writer = AsyncWriter(delegate: writerDelegate) - - // The user handler has two exit modes: - // 1. It completes successfully (the async user function completes without throwing), or - // 2. It throws an error. - // - // On the happy path the 'ok' status is queued up on the async writer. On the error path - // the writer queue is drained and promise below is completed. When the promise is failed - // it processes the error (possibly via a delegate) and sends back an appropriate status. - // We require separate paths as the failure path needs to execute on the event loop to process - // the error. - let promise = self.eventLoop.makePromise(of: Void.self) - // The success path is taken care of by the Task. - promise.futureResult.whenFailure { error in - self.userHandlerThrewError(error) - } // Update our state before invoke the handler. self.handlerStateMachine.handlerInvoked(requestHeaders: headers) - self.handlerComponents = ServerHandlerComponents( - requestSource: sequenceProducer.source, - responseWriter: writer, - task: promise.completeWithTask { + self.handlerComponents = ServerHandlerComponents< + Request, + Response, + GRPCAsyncWriterSinkDelegate<(Response, Compression)> + >( + requestSource: requestSequenceProducer.source, + responseWriterSink: responseWriter.sink, + task: Task { // We don't have a task cancellation handler here: we do it in `self.cancel()`. - try await self.invokeUserHandler( - sequence: sequenceProducer.sequence, - sequenceSource: sequenceProducer.source, - responseStreamWriter: writer, + await self.invokeUserHandler( + requestSequence: requestSequenceProducer, + responseWriter: responseWriter.writer, callContext: handlerContext ) } @@ -480,60 +480,27 @@ internal final class AsyncServerHandler< @Sendable @usableFromInline internal func invokeUserHandler( - sequence: AsyncSequenceProducer, - sequenceSource: AsyncSequenceProducer.Source, - responseStreamWriter: AsyncWriter>, + requestSequence: AsyncSequenceProducer.NewSequence, + responseWriter: NIOAsyncWriter< + (Response, Compression), + GRPCAsyncWriterSinkDelegate<(Response, Compression)> + >, callContext: GRPCAsyncServerCallContext - ) async throws { + ) async { defer { // It's possible the user handler completed before the end of the request stream. We // explicitly finish it to drop any unconsumed inbound messages. - sequenceSource.finish() + requestSequence.source.finish() } do { - let requestStream = GRPCAsyncRequestStream(sequence) - let responseStream = GRPCAsyncResponseStreamWriter(wrapping: responseStreamWriter) - try await self.userHandler(requestStream, responseStream, callContext) + let grpcRequestStream = GRPCAsyncRequestStream(requestSequence.sequence) + let grpcResponseStreamWriter = GRPCAsyncResponseStreamWriter(wrapping: responseWriter) + try await self.userHandler(grpcRequestStream, grpcResponseStreamWriter, callContext) - // Done successfully. Queue up and send back an 'ok' status. - try await responseStreamWriter.finish(.ok) + responseWriter.finish() } catch { - // Drop pending writes as we're on the error path. - await responseStreamWriter.cancel(withError: error) - - if let thrownStatus = error as? GRPCStatus, thrownStatus.isOk { - throw GRPCStatus(code: .unknown, message: "Handler threw error with status code 'ok'.") - } else { - throw error - } - } - } - - @usableFromInline - internal func userHandlerThrewError(_ error: Error) { - self.eventLoop.assertInEventLoop() - - switch self.handlerStateMachine.sendStatus() { - case let .intercept(requestHeaders, trailers): - let (status, processedTrailers) = ServerErrorProcessor.processObserverError( - error, - headers: requestHeaders, - trailers: trailers, - delegate: self.errorDelegate - ) - - switch self.interceptorStateMachine.interceptResponseStatus() { - case .intercept: - self.interceptors?.send(.end(status, processedTrailers), promise: nil) - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - - case .drop: - () + responseWriter.finish(error: error) } } @@ -616,25 +583,54 @@ internal final class AsyncServerHandler< @Sendable @inlinable - internal func interceptResponseMessage(_ response: Response, compression: Compression) { + internal func interceptResponseMessages(_ messages: Deque<(Response, Compression)>) { if self.eventLoop.inEventLoop { - self._interceptResponseMessage(response, compression: compression) + for message in messages { + self._interceptResponseMessage(message.0, compression: message.1) + } } else { self.eventLoop.execute { - self._interceptResponseMessage(response, compression: compression) + for message in messages { + self._interceptResponseMessage(message.0, compression: message.1) + } } } } @inlinable - internal func _interceptResponseStatus(_ status: GRPCStatus) { + internal func _interceptTermination(_ error: Error?) { self.eventLoop.assertInEventLoop() + let processedError: Error? + if let thrownStatus = error as? GRPCStatus, thrownStatus.isOk { + processedError = GRPCStatus( + code: .unknown, + message: "Handler threw error with status code 'ok'." + ) + } else { + processedError = error + } + switch self.handlerStateMachine.sendStatus() { - case let .intercept(_, trailers): + case let .intercept(requestHeaders, trailers): + let status: GRPCStatus + let processedTrailers: HPACKHeaders + + if let processedError = processedError { + (status, processedTrailers) = ServerErrorProcessor.processObserverError( + processedError, + headers: requestHeaders, + trailers: trailers, + delegate: self.errorDelegate + ) + } else { + status = GRPCStatus.ok + processedTrailers = trailers + } + switch self.interceptorStateMachine.interceptResponseStatus() { case .intercept: - self.interceptors?.send(.end(status, trailers), promise: nil) + self.interceptors?.send(.end(status, processedTrailers), promise: nil) case .cancel: return self.cancel(error: nil) case .drop: @@ -648,12 +644,12 @@ internal final class AsyncServerHandler< @Sendable @inlinable - internal func interceptResponseStatus(_ status: GRPCStatus) { + internal func interceptTermination(_ status: Error?) { if self.eventLoop.inEventLoop { - self._interceptResponseStatus(status) + self._interceptTermination(status) } else { self.eventLoop.execute { - self._interceptResponseStatus(status) + self._interceptTermination(status) } } } @@ -816,32 +812,37 @@ protocol AsyncServerCallContextProvider: Sendable { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline -internal struct ServerHandlerComponents { - @usableFromInline - internal let task: Task +internal struct ServerHandlerComponents< + Request: Sendable, + Response: Sendable, + Delegate: NIOAsyncWriterSinkDelegate +> where Delegate.Element == (Response, Compression) { @usableFromInline - internal let responseWriter: AsyncWriter + internal typealias AsyncWriterSink = NIOAsyncWriter<(Response, Compression), Delegate>.Sink + @usableFromInline - internal let requestSource: NIOThrowingAsyncSequenceProducer< + internal typealias AsyncSequenceSource = NIOThrowingAsyncSequenceProducer< Request, Error, NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, GRPCAsyncSequenceProducerDelegate >.Source + @usableFromInline + internal let task: Task + @usableFromInline + internal let responseWriterSink: AsyncWriterSink + @usableFromInline + internal let requestSource: AsyncSequenceSource + @inlinable init( - requestSource: NIOThrowingAsyncSequenceProducer< - Request, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source, - responseWriter: AsyncWriter, + requestSource: AsyncSequenceSource, + responseWriterSink: AsyncWriterSink, task: Task ) { self.task = task - self.responseWriter = responseWriter + self.responseWriterSink = responseWriterSink self.requestSource = requestSource } @@ -849,12 +850,12 @@ internal struct ServerHandlerComponents=5.6) import NIOCore @@ -85,17 +86,20 @@ public struct GRPCAsyncServerStreamingCall.makeSequence( - backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), + + let backpressureStrategy = NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark( + lowWatermark: 10, + highWatermark: 50 + ) + let sequenceProducer = NIOThrowingAsyncSequenceProducer.makeSequence( + elementType: Response.self, + failureType: Error.self, + backPressureStrategy: backpressureStrategy, delegate: GRPCAsyncSequenceProducerDelegate() ) - self.responseSource = sequence.source - self.responseStream = .init(sequence.sequence) + + self.responseSource = sequenceProducer.source + self.responseStream = .init(sequenceProducer.sequence) } /// We expose this as the only non-private initializer so that the caller diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift new file mode 100644 index 000000000..3cfa6f13c --- /dev/null +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift @@ -0,0 +1,48 @@ +/* + * Copyright 2022, 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. + */ + +#if compiler(>=5.6) +import DequeModule +import NIOCore + +@usableFromInline +internal struct GRPCAsyncWriterSinkDelegate: NIOAsyncWriterSinkDelegate { + @usableFromInline + let _didYield: (@Sendable (Deque) -> Void)? + + @usableFromInline + let _didTerminate: (@Sendable (Error?) -> Void)? + + @inlinable + init( + didYield: (@Sendable (Deque) -> Void)? = nil, + didTerminate: (@Sendable (Error?) -> Void)? = nil + ) { + self._didYield = didYield + self._didTerminate = didTerminate + } + + @inlinable + func didYield(contentsOf sequence: Deque) { + self._didYield?(sequence) + } + + @inlinable + func didTerminate(error: Error?) { + self._didTerminate?(error) + } +} +#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift b/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift new file mode 100644 index 000000000..40bffd123 --- /dev/null +++ b/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift @@ -0,0 +1,51 @@ +/* + * Copyright 2022, 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. + */ + +#if compiler(>=5.6) +import NIOCore + +/// Unchecked-sendable wrapper for ``NIOAsyncWriter/Sink``, to avoid getting sendability warnings. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal struct AsyncSink: @unchecked Sendable { + private let sink: NIOAsyncWriter< + Element, + GRPCAsyncWriterSinkDelegate + >.Sink + + @inlinable + init(wrapping sink: NIOAsyncWriter< + Element, + GRPCAsyncWriterSinkDelegate + >.Sink) { + self.sink = sink + } + + @inlinable + func setWritability(to writability: Bool) { + self.sink.setWritability(to: writability) + } + + @inlinable + func finish(error: Error) { + self.sink.finish(error: error) + } + + @inlinable + func finish() { + self.sink.finish() + } +} +#endif // compiler(>=5.6) diff --git a/Sources/GRPC/ServerErrorProcessor.swift b/Sources/GRPC/ServerErrorProcessor.swift index b0993ed60..34f98e846 100644 --- a/Sources/GRPC/ServerErrorProcessor.swift +++ b/Sources/GRPC/ServerErrorProcessor.swift @@ -43,7 +43,7 @@ internal enum ServerErrorProcessor { status = grpcStatusTransformable.makeGRPCStatus() trailers = [:] } else { - // Eh... well, we don't what status to use. Use a generic one. + // Eh... well, we don't know what status to use. Use a generic one. status = .processingError(cause: error) trailers = [:] } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift deleted file mode 100644 index 9ddee0818..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncWriterTests.swift +++ /dev/null @@ -1,305 +0,0 @@ -/* - * 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. - */ -#if compiler(>=5.6) -@testable import GRPC -import NIOConcurrencyHelpers -import XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -internal class AsyncWriterTests: GRPCTestCase { - func testSingleWriterHappyPath() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - try await writer.write("jimmy") - XCTAssertEqual(delegate.elements, ["jimmy"]) - - try await writer.write("jab") - XCTAssertEqual(delegate.elements, ["jimmy", "jab"]) - - try await writer.finish(99) - XCTAssertEqual(delegate.end, 99) - } - - func testPauseAndResumeWrites() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - // pause - await writer.toggleWritability() - - async let written1: Void = writer.write("wunch") - XCTAssert(delegate.elements.isEmpty) - - // resume - await writer.toggleWritability() - try await written1 - XCTAssertEqual(delegate.elements, ["wunch"]) - - try await writer.finish(0) - XCTAssertEqual(delegate.end, 0) - } - - func testTooManyWrites() async throws { - let delegate = CollectingDelegate() - // Zero pending elements means that any write when paused will trigger an error. - let writer = AsyncWriter(maxPendingElements: 0, delegate: delegate) - - // pause - await writer.toggleWritability() - - await XCTAssertThrowsError(try await writer.write("pontiac")) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .tooManyPendingWrites) - } - - // resume (we must finish the writer.) - await writer.toggleWritability() - try await writer.finish(0) - XCTAssertEqual(delegate.end, 0) - XCTAssertTrue(delegate.elements.isEmpty) - } - - func testWriteAfterFinish() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - try await writer.finish(0) - XCTAssertEqual(delegate.end, 0) - - await XCTAssertThrowsError(try await writer.write("cheddar")) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - } - - XCTAssertTrue(delegate.elements.isEmpty) - } - - func testTooManyCallsToFinish() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - try await writer.finish(0) - XCTAssertEqual(delegate.end, 0) - - await XCTAssertThrowsError(try await writer.finish(1)) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - } - - // Still 0. - XCTAssertEqual(delegate.end, 0) - } - - func testCallToFinishWhilePending() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - // Pause. - await writer.toggleWritability() - - async let finished: Void = writer.finish(42) - XCTAssertNil(delegate.end) - - // Resume. - await writer.toggleWritability() - try await finished - - XCTAssertEqual(delegate.end, 42) - } - - func testTooManyCallsToFinishWhilePending() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - // Pause. - await writer.toggleWritability() - - // We want to test that when a finish has suspended that another task calling finish results - // in an `AsyncWriterError.alreadyFinished` error. - // - // It's hard to achieve this reliably in an obvious way because we can't guarantee the - // ordering of `Task`s or when they will be suspended during `finish`. However, by pausing the - // writer and calling finish in two separate tasks we guarantee that one will run first and - // suspend (because the writer is paused) and the other will throw an error. When one throws - // an error it can resume the writer allowing the other task to resume successfully. - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - do { - try await writer.finish(1) - } catch { - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - // Resume. - await writer.toggleWritability() - } - } - - group.addTask { - do { - try await writer.finish(2) - } catch { - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - // Resume. - await writer.toggleWritability() - } - } - } - - // We should definitely be finished by this point. - await XCTAssertThrowsError(try await writer.finish(3)) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - } - } - - func testCancellationForPendingWrite() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - // Pause. - await writer.toggleWritability() - - async let pendingWrite: Void = writer.write("foo") - - await writer.cancel(withError: CancellationError()) - - do { - try await pendingWrite - XCTFail("Expected to throw an error.") - } catch is CancellationError { - // Cancellation is fine: we cancelled while the write was pending. - () - } catch let error as GRPCAsyncWriterError { - // Already finish is also fine: we cancelled before the write was enqueued. - XCTAssertEqual(error, .alreadyFinished) - } catch { - XCTFail("Unexpected error: \(error)") - } - - await XCTAssertThrowsError(try await writer.write("bar")) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - } - - XCTAssertTrue(delegate.elements.isEmpty) - XCTAssertNil(delegate.end) - } - - func testCancellationForPendingFinish() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - // Pause. - await writer.toggleWritability() - - async let pendingWrite: Void = writer.finish(42) - - await writer.cancel(withError: CancellationError()) - - do { - try await pendingWrite - XCTFail("Expected to throw an error.") - } catch is CancellationError { - // Cancellation is fine: we cancelled while the write was pending. - () - } catch let error as GRPCAsyncWriterError { - // Already finish is also fine: we cancelled before the write was enqueued. - XCTAssertEqual(error, .alreadyFinished) - } catch { - XCTFail("Unexpected error: \(error)") - } - - await XCTAssertThrowsError(try await writer.finish(42)) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - } - - XCTAssertTrue(delegate.elements.isEmpty) - XCTAssertNil(delegate.end) - } - - func testMultipleCancellations() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(delegate: delegate) - - await writer.cancel(withError: CancellationError()) - await XCTAssertThrowsError(try await writer.write("1")) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - } - - // Fine, no need to throw. Nothing should change. - await writer.cancel(withError: CancellationError()) - await XCTAssertThrowsError(try await writer.write("2")) { error in - XCTAssertEqual(error as? GRPCAsyncWriterError, .alreadyFinished) - } - - XCTAssertTrue(delegate.elements.isEmpty) - XCTAssertNil(delegate.end) - } - - func testCooperativeCancellationOnWrite() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(isWritable: false, delegate: delegate) - try await withTaskCancelledAfter(nanoseconds: 100_000) { - do { - // Without co-operative cancellation then this will suspend indefinitely. - try await writer.write("I should be cancelled") - XCTFail("write(_:) should throw CancellationError") - } catch { - XCTAssert(error is CancellationError) - } - } - } - - func testCooperativeCancellationOnFinish() async throws { - let delegate = CollectingDelegate() - let writer = AsyncWriter(isWritable: false, delegate: delegate) - try await withTaskCancelledAfter(nanoseconds: 100_000) { - do { - // Without co-operative cancellation then this will suspend indefinitely. - try await writer.finish() - XCTFail("finish() should throw CancellationError") - } catch { - XCTAssert(error is CancellationError) - } - } - } -} - -fileprivate final class CollectingDelegate< - Element: Sendable, - End: Sendable ->: AsyncWriterDelegate, @unchecked Sendable { - private let lock = NIOLock() - private var _elements: [Element] = [] - private var _end: End? - - internal var elements: [Element] { - return self.lock.withLock { self._elements } - } - - internal var end: End? { - return self.lock.withLock { self._end } - } - - internal func write(_ element: Element) { - self.lock.withLock { - self._elements.append(element) - } - } - - internal func writeEnd(_ end: End) { - self.lock.withLock { - self._end = end - } - } -} - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift index 589c36446..b775ecce6 100644 --- a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift +++ b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift @@ -514,16 +514,17 @@ internal final class AsyncResponseStream: GRPCServerResponseWriter { > init() { - let sequenceProducer = NIOAsyncSequenceProducer< - GRPCServerResponsePart, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.makeSequence( - backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), + let backpressureStrategy = NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark( + lowWatermark: 10, + highWatermark: 50 + ) + let sequence = NIOAsyncSequenceProducer.makeSequence( + elementType: GRPCServerResponsePart.self, + backPressureStrategy: backpressureStrategy, delegate: GRPCAsyncSequenceProducerDelegate() ) - self.source = sequenceProducer.source - self.responseSequence = sequenceProducer.sequence + self.source = sequence.source + self.responseSequence = sequence.sequence } func sendMetadata( From 87cecdeb2aae6b359b754d0dc7099e8237cf1824 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 29 Sep 2022 17:09:16 +0100 Subject: [PATCH 031/580] Bump version number to 1.11.0 (#1495) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.11.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 797801a5e..adea3d2cb 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 10 + internal static let minor = 11 /// The patch version. internal static let patch = 0 From 4a1fab11cc24ecbc419e359481101f514306a4af Mon Sep 17 00:00:00 2001 From: Photon cat Date: Tue, 4 Oct 2022 01:17:41 -0700 Subject: [PATCH 032/580] Remove invalid command line args from the example (#1496) These two args has been removed since 1.8 and is on by default. Passing in these two args will result in the grpc stub not being generated at all. --- docs/basic-tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/basic-tutorial.md b/docs/basic-tutorial.md index fb5794560..2c13c2a16 100644 --- a/docs/basic-tutorial.md +++ b/docs/basic-tutorial.md @@ -167,7 +167,7 @@ $ protoc Sources/Examples/RouteGuide/Model/route_guide.proto \ --swift_opt=Visibility=Public \ --swift_out=Sources/Examples/RouteGuide/Model \ --plugin=./.build/debug/protoc-gen-grpc-swift \ - --grpc-swift_opt=Visibility=Public,AsyncClient=True,AsyncServer=True \ + --grpc-swift_opt=Visibility=Public \ --grpc-swift_out=Sources/Examples/RouteGuide/Model ``` From cfa950fbe16b0ae3ffcb16e46928a93d0f2dc857 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 7 Oct 2022 16:45:00 +0100 Subject: [PATCH 033/580] Allow to write a sequence of requests/responses (#1499) * Allow to write a sequence of requests/responses # Motivation We recently adopted the `NIOAsyncWriter` to back the `GRPCAsyncRequestStreamWriter` and the `GRPCAsyncResponseStreamWriter`; however, we did not expose the functionality to write a sequence of the elements that the `NIOAsyncWriter` offers. This can be useful in cases where you want to write a batch of requests/responses since it reduces the amount of locks and thread hops. # Modification Expose new methods on the `GRPCAsyncRequestStreamWriter` and the `GRPCAsyncResponseStreamWriter` to enable to write a sequence. I also fixed up the comments for the `GRPCAsyncRequestStreamWriter` since they were outdated. # Result Users can write a batch of requests/responses. * Update tests --- .../GRPCAsyncRequestStreamWriter.swift | 29 ++++++++++++++----- .../GRPCAsyncResponseStreamWriter.swift | 17 +++++++++++ .../GRPCTests/GRPCAsyncClientCallTests.swift | 9 ++++-- .../GRPCAsyncServerHandlerTests.swift | 22 ++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift index 3a12f229b..499075e41 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift @@ -49,12 +49,7 @@ public struct GRPCAsyncRequestStreamWriter: Sendable { /// Send a single request. /// - /// To ensure requests are delivered in order callers should `await` the result of this call - /// before sending another request. Callers who do not need this guarantee do not have to `await` - /// the completion of this call and may send messages concurrently from multiple ``Task``s. - /// However, it is important to note that no more than 16 writes may be pending at any one time - /// and attempting to exceed this will result in an ``GRPCAsyncWriterError/tooManyPendingWrites`` - /// error being thrown. + /// It is safe to send multiple requests concurrently by sharing the ``GRPCAsyncRequestStreamWriter`` across tasks. /// /// Callers must call ``finish()`` when they have no more requests left to send. /// @@ -62,8 +57,7 @@ public struct GRPCAsyncRequestStreamWriter: Sendable { /// - request: The request to send. /// - compression: Whether the request should be compressed or not. Ignored if compression was /// not enabled for the RPC. - /// - Throws: ``GRPCAsyncWriterError`` if there are too many pending writes or the request stream - /// has already been finished. + /// - Throws: If the request stream has already been finished. @inlinable public func send( _ request: Request, @@ -72,6 +66,25 @@ public struct GRPCAsyncRequestStreamWriter: Sendable { try await self.asyncWriter.yield((request, compression)) } + /// Send a sequence of requests. + /// + /// It is safe to send multiple requests concurrently by sharing the ``GRPCAsyncRequestStreamWriter`` across tasks. + /// + /// Callers must call ``finish()`` when they have no more requests left to send. + /// + /// - Parameters: + /// - requests: The requests to send. + /// - compression: Whether the requests should be compressed or not. Ignored if compression was + /// not enabled for the RPC. + /// - Throws: If the request stream has already been finished. + @inlinable + public func send( + _ requests: S, + compression: Compression = .deferToCallDefault + ) async throws where S.Element == Request { + try await self.asyncWriter.yield(contentsOf: requests.lazy.map { ($0, compression) }) + } + /// Finish the request stream for the RPC. This must be called when there are no more requests to be sent. public func finish() async throws { self.asyncWriter.finish() diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift index d7939e2d8..3d06c007f 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift @@ -129,6 +129,23 @@ public struct GRPCAsyncResponseStreamWriter: Sendable { } } + @inlinable + public func send( + contentsOf responses: S, + compression: Compression = .deferToCallDefault + ) async throws where S.Element == Response { + let responsesWithCompression = responses.lazy.map { ($0, compression) } + switch self.backing { + case let .asyncWriter(writer): + try await writer.yield(contentsOf: responsesWithCompression) + + case let .closure(closure): + for response in responsesWithCompression { + await closure(response) + } + } + } + /// Creates a new `GRPCAsyncResponseStreamWriter` backed by a ``ResponseStream``. /// This is mostly useful for testing purposes where one wants to observe the written responses. /// diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift index 5dd382b00..d64b2032a 100644 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift @@ -132,14 +132,17 @@ class GRPCAsyncClientCallTests: GRPCTestCase { callOptions: .init() ) - for word in ["boyle", "jeffers", "holt"] { - try await update.requestStream.send(.with { $0.text = word }) + let requests = ["boyle", "jeffers", "holt"] + .map { word in Echo_EchoRequest.with { $0.text = word } } + for request in requests { + try await update.requestStream.send(request) } + try await update.requestStream.send(requests) try await update.requestStream.finish() let numResponses = try await update.responseStream.map { _ in 1 }.reduce(0, +) - await assertThat(numResponses, .is(.equalTo(3))) + await assertThat(numResponses, .is(.equalTo(6))) await assertThat(try await update.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) await assertThat(await update.status, .hasCode(.ok)) } diff --git a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift index b775ecce6..85460e9f6 100644 --- a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift +++ b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift @@ -230,6 +230,28 @@ class AsyncServerHandlerTests: GRPCTestCase { await responseStream.next().assertNil() } + func testResponseSequence() async throws { + let handler = self.makeHandler { _, responseStreamWriter, _ in + try await responseStreamWriter.send(contentsOf: ["1", "2", "3"]) + } + defer { + XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) + } + + self.loop.execute { + handler.receiveMetadata([:]) + handler.receiveEnd() + } + + let responseStream = self.recorder.responseSequence.makeAsyncIterator() + await responseStream.next().assertMetadata { _ in } + await responseStream.next().assertMessage() + await responseStream.next().assertMessage() + await responseStream.next().assertMessage() + await responseStream.next().assertStatus { _, _ in } + await responseStream.next().assertNil() + } + func testThrowingDeserializer() async throws { let handler = AsyncServerHandler( context: self.makeCallHandlerContext(), From 4915076adf40d907559b8464511769a66f85980b Mon Sep 17 00:00:00 2001 From: Sam Amin <113029880+Sam-Amin@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:22:18 +0100 Subject: [PATCH 034/580] Fix HelloWorld Samples (#1505) * Fixed HelloWorld samples - Samples were not starting up without a main function * Fixed code review comments --- .../HelloWorld/Client/{main.swift => helloworldclient.swift} | 1 + .../HelloWorld/Server/{main.swift => HelloWorldServer.swift} | 1 + 2 files changed, 2 insertions(+) rename Sources/Examples/HelloWorld/Client/{main.swift => helloworldclient.swift} (99%) rename Sources/Examples/HelloWorld/Server/{main.swift => HelloWorldServer.swift} (99%) diff --git a/Sources/Examples/HelloWorld/Client/main.swift b/Sources/Examples/HelloWorld/Client/helloworldclient.swift similarity index 99% rename from Sources/Examples/HelloWorld/Client/main.swift rename to Sources/Examples/HelloWorld/Client/helloworldclient.swift index 0849f7c66..62c0091f0 100644 --- a/Sources/Examples/HelloWorld/Client/main.swift +++ b/Sources/Examples/HelloWorld/Client/helloworldclient.swift @@ -20,6 +20,7 @@ import HelloWorldModel import NIOCore import NIOPosix +@main @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct HelloWorld: AsyncParsableCommand { @Option(help: "The port to connect to") diff --git a/Sources/Examples/HelloWorld/Server/main.swift b/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift similarity index 99% rename from Sources/Examples/HelloWorld/Server/main.swift rename to Sources/Examples/HelloWorld/Server/HelloWorldServer.swift index 9b22dfdc6..3c0e17c26 100644 --- a/Sources/Examples/HelloWorld/Server/main.swift +++ b/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift @@ -20,6 +20,7 @@ import HelloWorldModel import NIOCore import NIOPosix +@main @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct HelloWorld: AsyncParsableCommand { @Option(help: "The port to listen on for new connections") From 930667fc612e2b790fbfb00aebda6b9f67a8fdca Mon Sep 17 00:00:00 2001 From: Cory Benfield Date: Mon, 17 Oct 2022 21:06:12 +0100 Subject: [PATCH 035/580] Revert "Fix HelloWorld Samples (#1505)" (#1506) This reverts commit 4915076adf40d907559b8464511769a66f85980b. --- .../HelloWorld/Client/{helloworldclient.swift => main.swift} | 1 - .../HelloWorld/Server/{HelloWorldServer.swift => main.swift} | 1 - 2 files changed, 2 deletions(-) rename Sources/Examples/HelloWorld/Client/{helloworldclient.swift => main.swift} (99%) rename Sources/Examples/HelloWorld/Server/{HelloWorldServer.swift => main.swift} (99%) diff --git a/Sources/Examples/HelloWorld/Client/helloworldclient.swift b/Sources/Examples/HelloWorld/Client/main.swift similarity index 99% rename from Sources/Examples/HelloWorld/Client/helloworldclient.swift rename to Sources/Examples/HelloWorld/Client/main.swift index 62c0091f0..0849f7c66 100644 --- a/Sources/Examples/HelloWorld/Client/helloworldclient.swift +++ b/Sources/Examples/HelloWorld/Client/main.swift @@ -20,7 +20,6 @@ import HelloWorldModel import NIOCore import NIOPosix -@main @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct HelloWorld: AsyncParsableCommand { @Option(help: "The port to connect to") diff --git a/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift b/Sources/Examples/HelloWorld/Server/main.swift similarity index 99% rename from Sources/Examples/HelloWorld/Server/HelloWorldServer.swift rename to Sources/Examples/HelloWorld/Server/main.swift index 3c0e17c26..9b22dfdc6 100644 --- a/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift +++ b/Sources/Examples/HelloWorld/Server/main.swift @@ -20,7 +20,6 @@ import HelloWorldModel import NIOCore import NIOPosix -@main @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct HelloWorld: AsyncParsableCommand { @Option(help: "The port to listen on for new connections") From 95e6a8213fdff3d46bbd52a629c37a825667da3b Mon Sep 17 00:00:00 2001 From: Michael Schwarz Date: Wed, 19 Oct 2022 17:49:59 +0200 Subject: [PATCH 036/580] Allow to `finish()` GRPCAsyncRequestStreamWriter sync, non-throwing (#1504) * Add sync method variant for finishing RequestStreamWriter * Adjust call sites to use sync RequestStreamWriter finish * Remove async variant of GRPCAsyncRequestStreamWriter's .finish() Co-authored-by: George Barnett --- Sources/Examples/RouteGuide/Client/RouteGuideClient.swift | 4 ++-- .../AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift | 2 +- .../AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift | 4 ++-- Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift | 4 ++-- .../AsyncAwaitSupport/AsyncIntegrationTests.swift | 6 +++--- .../AsyncAwaitSupport/InterceptorsAsyncTests.swift | 4 ++-- Tests/GRPCTests/GRPCAsyncClientCallTests.swift | 8 ++++---- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift index 93252c238..7b34777d0 100644 --- a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift +++ b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift @@ -143,7 +143,7 @@ extension RouteGuideExample { } } - try await recordRoute.requestStream.finish() + recordRoute.requestStream.finish() let summary = try await recordRoute.response print( @@ -188,7 +188,7 @@ extension RouteGuideExample { try await Task.sleep(nanoseconds: UInt64.random(in: UInt64(2e8) ... UInt64(1e9))) } - try await routeChat.requestStream.finish() + routeChat.requestStream.finish() } // Add a task to print each message received on the response stream. diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift index 499075e41..7bfe524c0 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift @@ -86,7 +86,7 @@ public struct GRPCAsyncRequestStreamWriter: Sendable { } /// Finish the request stream for the RPC. This must be called when there are no more requests to be sent. - public func finish() async throws { + public func finish() { self.asyncWriter.finish() } diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift index 423174f17..8f675cf78 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift @@ -421,7 +421,7 @@ extension GRPCClient { for try await request in requests { try await call.requestStream.send(request) } - try await call.requestStream.finish() + call.requestStream.finish() } catch { // If we throw then cancel the call. We will rely on the response throwing an appropriate // error below. @@ -452,7 +452,7 @@ extension GRPCClient { for try await request in requests { try await call.requestStream.send(request) } - try await call.requestStream.finish() + call.requestStream.finish() } onCancel: { call.cancel() } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift index 5833e35e5..df009a6cf 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift @@ -222,7 +222,7 @@ final class AsyncClientCancellationTests: GRPCTestCase { let collect = echo.makeCollectCall() // Send and close. try await collect.requestStream.send(.with { $0.text = "foo" }) - try await collect.requestStream.finish() + collect.requestStream.finish() // Await the response and status. _ = try await collect.response @@ -294,7 +294,7 @@ final class AsyncClientCancellationTests: GRPCTestCase { let update = echo.makeUpdateCall() // Send and close. try await update.requestStream.send(.with { $0.text = "foo" }) - try await update.requestStream.finish() + update.requestStream.finish() // Await the response and status. let responseCount = try await update.responseStream.count() diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift index 34c960116..4f41ebef0 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift @@ -82,7 +82,7 @@ final class AsyncIntegrationTests: GRPCTestCase { try await collect.requestStream.send(.with { $0.text = "boyle" }) try await collect.requestStream.send(.with { $0.text = "jeffers" }) try await collect.requestStream.send(.with { $0.text = "holt" }) - try await collect.requestStream.finish() + collect.requestStream.finish() let initialMetadata = try await collect.initialMetadata initialMetadata.assertFirst("200", forName: ":status") @@ -149,7 +149,7 @@ final class AsyncIntegrationTests: GRPCTestCase { XCTAssertEqual(response, "Swift echo update (\(i)): \(name)") } - try await update.requestStream.finish() + update.requestStream.finish() // This isn't right after we make the call as servers are not guaranteed to send metadata back // immediately. Concretely, we don't send initial metadata back until the first response @@ -186,7 +186,7 @@ final class AsyncIntegrationTests: GRPCTestCase { _ = try await update.responseStream.first(where: { _ in true }) XCTAssertNoThrow(try self.server.close().wait()) self.server = nil // So that tearDown() does not call close() again. - try await update.requestStream.finish() + update.requestStream.finish() } } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift index 9375ee15e..8ff554ebc 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift @@ -105,7 +105,7 @@ class InterceptorsAsyncTests: GRPCTestCase { let call = self.echo.makeCollectCall(callOptions: .init()) try await call.requestStream.send(.with { $0.text = "1 2" }) try await call.requestStream.send(.with { $0.text = "3 4" }) - try await call.requestStream.finish() + call.requestStream.finish() await assertThat( try await call.response, @@ -153,7 +153,7 @@ class InterceptorsAsyncTests: GRPCTestCase { let call = self.echo.makeUpdateCall(callOptions: .init()) try await call.requestStream.send(.with { $0.text = "1 2" }) try await call.requestStream.send(.with { $0.text = "3 4" }) - try await call.requestStream.finish() + call.requestStream.finish() var count = 0 for try await response in call.responseStream { diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift index d64b2032a..067c41504 100644 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift @@ -98,7 +98,7 @@ class GRPCAsyncClientCallTests: GRPCTestCase { for word in ["boyle", "jeffers", "holt"] { try await collect.requestStream.send(.with { $0.text = word }) } - try await collect.requestStream.finish() + collect.requestStream.finish() await assertThat(try await collect.initialMetadata, .is(.equalTo(Self.OKInitialMetadata))) await assertThat(try await collect.response, .doesNotThrow()) @@ -138,7 +138,7 @@ class GRPCAsyncClientCallTests: GRPCTestCase { try await update.requestStream.send(request) } try await update.requestStream.send(requests) - try await update.requestStream.finish() + update.requestStream.finish() let numResponses = try await update.responseStream.map { _ in 1 }.reduce(0, +) @@ -163,7 +163,7 @@ class GRPCAsyncClientCallTests: GRPCTestCase { await assertThat(try await responseStreamIterator.next(), .is(.notNil())) } - try await update.requestStream.finish() + update.requestStream.finish() await assertThat(try await responseStreamIterator.next(), .is(.nil())) @@ -191,7 +191,7 @@ class GRPCAsyncClientCallTests: GRPCTestCase { try await update.requestStream.send(.with { $0.text = word }) await counter.incrementRequests() } - try await update.requestStream.finish() + update.requestStream.finish() } // Get responses in a separate task. taskGroup.addTask { From 3677a71c74175999d5dc9479a0e8aa07a0a54bee Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 21 Oct 2022 09:31:28 +0100 Subject: [PATCH 037/580] Log TLS version (#1509) * Log TLS version * Use released versions, reformat Co-authored-by: David Nadoba --- Package.swift | 2 +- Package@swift-5.5.swift | 2 +- Sources/GRPC/GRPCIdleHandler.swift | 8 ++ .../GRPC/GRPCServerPipelineConfigurator.swift | 2 + Sources/GRPC/TLSVersion.swift | 133 ++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 Sources/GRPC/TLSVersion.swift diff --git a/Package.swift b/Package.swift index 8c921631b..406a560ad 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-nio-transport-services.git", - from: "1.11.1" + from: "1.15.0" ), .package( url: "https://github.com/apple/swift-nio-extras.git", diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index 64952921f..ced9819cc 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -40,7 +40,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-nio-transport-services.git", - from: "1.11.1" + from: "1.15.0" ), .package( url: "https://github.com/apple/swift-nio-extras.git", diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index eed505a1c..25ed79d44 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -16,6 +16,7 @@ import Logging import NIOCore import NIOHTTP2 +import NIOTLS internal final class GRPCIdleHandler: ChannelInboundHandler { typealias InboundIn = HTTP2Frame @@ -247,6 +248,13 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { } else if event is ChannelShouldQuiesceEvent { self.perform(operations: self.stateMachine.initiateGracefulShutdown()) // Swallow this event. + } else if case let .handshakeCompleted(negotiatedProtocol) = event as? TLSUserEvent { + let tlsVersion = try? context.channel.getTLSVersionSync() + self.stateMachine.logger.debug("TLS handshake completed", metadata: [ + "alpn": "\(negotiatedProtocol ?? "nil")", + "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", + ]) + context.fireUserInboundEventTriggered(event) } else { context.fireUserInboundEventTriggered(event) } diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift index 47c231d73..5854faea9 100644 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ b/Sources/GRPC/GRPCServerPipelineConfigurator.swift @@ -231,8 +231,10 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan ) { switch event { case let .handshakeCompleted(negotiatedProtocol): + let tlsVersion = try? context.channel.getTLSVersionSync() self.configuration.logger.debug("TLS handshake completed", metadata: [ "alpn": "\(negotiatedProtocol ?? "nil")", + "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", ]) switch negotiatedProtocol { diff --git a/Sources/GRPC/TLSVersion.swift b/Sources/GRPC/TLSVersion.swift new file mode 100644 index 000000000..9be80c77f --- /dev/null +++ b/Sources/GRPC/TLSVersion.swift @@ -0,0 +1,133 @@ +/* + * Copyright 2022, 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 NIOCore +#if canImport(NIOSSL) +import NIOSSL +#endif +#if canImport(Network) +import Network +import NIOTransportServices +#endif + +// The same as 'TLSVersion' which is defined in NIOSSL which we don't always have. +enum GRPCTLSVersion: Hashable { + case tlsv1 + case tlsv11 + case tlsv12 + case tlsv13 +} + +#if canImport(NIOSSL) +extension GRPCTLSVersion { + init(_ tlsVersion: TLSVersion) { + switch tlsVersion { + case .tlsv1: + self = .tlsv1 + case .tlsv11: + self = .tlsv11 + case .tlsv12: + self = .tlsv12 + case .tlsv13: + self = .tlsv13 + } + } +} +#endif + +#if canImport(Network) +@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) +extension GRPCTLSVersion { + init?(_ metadata: NWProtocolTLS.Metadata) { + let protocolMetadata = metadata.securityProtocolMetadata + + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + let nwTLSVersion = sec_protocol_metadata_get_negotiated_tls_protocol_version(protocolMetadata) + switch nwTLSVersion { + case .TLSv10: + self = .tlsv1 + case .TLSv11: + self = .tlsv11 + case .TLSv12: + self = .tlsv12 + case .TLSv13: + self = .tlsv13 + case .DTLSv10, .DTLSv12: + return nil + @unknown default: + return nil + } + } else { + let sslVersion = sec_protocol_metadata_get_negotiated_protocol_version(protocolMetadata) + switch sslVersion { + case .sslProtocolUnknown: + return nil + case .tlsProtocol1, .tlsProtocol1Only: + self = .tlsv1 + case .tlsProtocol11: + self = .tlsv11 + case .tlsProtocol12: + self = .tlsv12 + case .tlsProtocol13: + self = .tlsv13 + case .dtlsProtocol1, + .dtlsProtocol12, + .sslProtocol2, + .sslProtocol3, + .sslProtocol3Only, + .sslProtocolAll, + .tlsProtocolMaxSupported: + return nil + @unknown default: + return nil + } + } + } +} +#endif + +extension Channel { + /// This method tries to get the TLS version from either the Network.framework or NIOSSL + /// - Precondition: Must be called on the `EventLoop` the `Channel` is running on. + func getTLSVersionSync( + file: StaticString = #fileID, + line: UInt = #line + ) throws -> GRPCTLSVersion? { + #if canImport(Network) + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + do { + // cast can never fail because we explicitly ask for the NWProtocolTLS Metadata. + // it may still be nil if Network.framework isn't used for TLS in which case we will + // fall through and try to get the TLS version from NIOSSL + if let metadata = try self.getMetadataSync( + definition: NWProtocolTLS.definition, + file: file, + line: line + ) as! NWProtocolTLS.Metadata? { + return GRPCTLSVersion(metadata) + } + } catch is NIOTSChannelIsNotANIOTSConnectionChannel { + // Not a NIOTS channel, we might be using NIOSSL so try that next. + } + } + #endif + #if canImport(NIOSSL) + return try self.pipeline.syncOperations.nioSSL_tlsVersion().map(GRPCTLSVersion.init) + #else + return nil + #endif + } +} From 0332b349908a564b6bbfe5ad948dcefcb0c90a1e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 24 Oct 2022 09:33:31 +0100 Subject: [PATCH 038/580] Fix HelloWorld examples (#1512) Resolves #1511 --- .../Client/{main.swift => HelloWorldClient.swift} | 8 ++++++++ .../Server/{main.swift => HelloWorldServer.swift} | 8 ++++++++ 2 files changed, 16 insertions(+) rename Sources/Examples/HelloWorld/Client/{main.swift => HelloWorldClient.swift} (94%) rename Sources/Examples/HelloWorld/Server/{main.swift => HelloWorldServer.swift} (92%) diff --git a/Sources/Examples/HelloWorld/Client/main.swift b/Sources/Examples/HelloWorld/Client/HelloWorldClient.swift similarity index 94% rename from Sources/Examples/HelloWorld/Client/main.swift rename to Sources/Examples/HelloWorld/Client/HelloWorldClient.swift index 0849f7c66..41de5df33 100644 --- a/Sources/Examples/HelloWorld/Client/main.swift +++ b/Sources/Examples/HelloWorld/Client/HelloWorldClient.swift @@ -21,6 +21,7 @@ import NIOCore import NIOPosix @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@main struct HelloWorld: AsyncParsableCommand { @Option(help: "The port to connect to") var port: Int = 1234 @@ -67,4 +68,11 @@ struct HelloWorld: AsyncParsableCommand { } } } +#else +@main +enum HelloWorld { + static func main() { + fatalError("This example requires swift >= 5.6") + } +} #endif // compiler(>=5.6) diff --git a/Sources/Examples/HelloWorld/Server/main.swift b/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift similarity index 92% rename from Sources/Examples/HelloWorld/Server/main.swift rename to Sources/Examples/HelloWorld/Server/HelloWorldServer.swift index 9b22dfdc6..aa78f86f0 100644 --- a/Sources/Examples/HelloWorld/Server/main.swift +++ b/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift @@ -21,6 +21,7 @@ import NIOCore import NIOPosix @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@main struct HelloWorld: AsyncParsableCommand { @Option(help: "The port to listen on for new connections") var port = 1234 @@ -43,4 +44,11 @@ struct HelloWorld: AsyncParsableCommand { try await server.onClose.get() } } +#else +@main +enum HelloWorld { + static func main() { + fatalError("This example requires swift >= 5.6") + } +} #endif // compiler(>=5.6) From a6e90574ec72aad0b9ebf7b3c0411565bd3b5558 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 24 Oct 2022 11:05:39 +0100 Subject: [PATCH 039/580] Fix sendable warnings (#1513) Motivation: Warnings were emitted for `debugChannelInitializer`s as they were not `Sendable` even though their definition via a typealias was. Modifications: Expand out typealiases and add appropriate `@Sendable` and `@preconcurrency` annotations. Results: No warnings. --- Sources/GRPC/ClientConnection.swift | 46 ++++++++++++++++++- .../GRPC/ConnectionPool/GRPCChannelPool.swift | 7 ++- .../GRPC/GRPCChannel/GRPCChannelBuilder.swift | 13 +++++- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index 6dfbd2f12..bd04081d0 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -454,9 +454,15 @@ extension ClientConnection { /// used to add additional handlers to the pipeline and is intended for debugging. /// /// - Warning: The initializer closure may be invoked *multiple times*. - public var debugChannelInitializer: GRPCChannelInitializer? + #if compiler(>=5.6) + @preconcurrency + public var debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? + #else + public var debugChannelInitializer: ((Channel) -> EventLoopFuture)? + #endif #if canImport(NIOSSL) + #if compiler(>=5.6) /// Create a `Configuration` with some pre-defined defaults. Prefer using /// `ClientConnection.secure(group:)` to build a connection secured with TLS or /// `ClientConnection.insecure(group:)` to build a plaintext connection. @@ -480,6 +486,7 @@ extension ClientConnection { /// - Parameter debugChannelInitializer: A channel initializer will be called after gRPC has /// initialized the channel. Defaults to `nil`. @available(*, deprecated, renamed: "default(target:eventLoopGroup:)") + @preconcurrency public init( target: ConnectionTarget, eventLoopGroup: EventLoopGroup, @@ -496,7 +503,7 @@ extension ClientConnection { label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() } ), - debugChannelInitializer: GRPCChannelInitializer? = nil + debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? = nil ) { self.target = target self.eventLoopGroup = eventLoopGroup @@ -512,6 +519,41 @@ extension ClientConnection { self.backgroundActivityLogger = backgroundActivityLogger self.debugChannelInitializer = debugChannelInitializer } + #else + @available(*, deprecated, renamed: "default(target:eventLoopGroup:)") + public init( + target: ConnectionTarget, + eventLoopGroup: EventLoopGroup, + errorDelegate: ClientErrorDelegate? = LoggingClientErrorDelegate(), + connectivityStateDelegate: ConnectivityStateDelegate? = nil, + connectivityStateDelegateQueue: DispatchQueue? = nil, + tls: Configuration.TLS? = nil, + connectionBackoff: ConnectionBackoff? = ConnectionBackoff(), + connectionKeepalive: ClientConnectionKeepalive = ClientConnectionKeepalive(), + connectionIdleTimeout: TimeAmount = .minutes(30), + callStartBehavior: CallStartBehavior = .waitsForConnectivity, + httpTargetWindowSize: Int = 8 * 1024 * 1024, + backgroundActivityLogger: Logger = Logger( + label: "io.grpc", + factory: { _ in SwiftLogNoOpLogHandler() } + ), + debugChannelInitializer: ((Channel) -> EventLoopFuture)? = nil + ) { + self.target = target + self.eventLoopGroup = eventLoopGroup + self.errorDelegate = errorDelegate + self.connectivityStateDelegate = connectivityStateDelegate + self.connectivityStateDelegateQueue = connectivityStateDelegateQueue + self.tlsConfiguration = tls.map { GRPCTLSConfiguration(transforming: $0) } + self.connectionBackoff = connectionBackoff + self.connectionKeepalive = connectionKeepalive + self.connectionIdleTimeout = connectionIdleTimeout + self.callStartBehavior = callStartBehavior + self.httpTargetWindowSize = httpTargetWindowSize + self.backgroundActivityLogger = backgroundActivityLogger + self.debugChannelInitializer = debugChannelInitializer + } + #endif // compiler(>=5.6) #endif // canImport(NIOSSL) private init(eventLoopGroup: EventLoopGroup, target: ConnectionTarget) { diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift index 366f1f4a0..32f5f9898 100644 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift @@ -167,7 +167,12 @@ extension GRPCChannelPool { /// This may be used to add additional handlers to the pipeline and is intended for debugging. /// /// - Warning: The initializer closure may be invoked *multiple times*. - public var debugChannelInitializer: GRPCChannelInitializer? + #if compiler(>=5.6) + @preconcurrency + public var debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? + #else + public var debugChannelInitializer: ((Channel) -> EventLoopFuture)? + #endif /// An error delegate which is called when errors are caught. public var errorDelegate: ClientErrorDelegate? diff --git a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift index 1dee4b921..a726e46e0 100644 --- a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift +++ b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift @@ -315,13 +315,24 @@ extension ClientConnection.Builder { /// used to add additional handlers to the pipeline and is intended for debugging. /// /// - Warning: The initializer closure may be invoked *multiple times*. + #if compiler(>=5.6) @discardableResult + @preconcurrency public func withDebugChannelInitializer( - _ debugChannelInitializer: @escaping GRPCChannelInitializer + _ debugChannelInitializer: @Sendable @escaping (Channel) -> EventLoopFuture ) -> Self { self.configuration.debugChannelInitializer = debugChannelInitializer return self } + #else + @discardableResult + public func withDebugChannelInitializer( + _ debugChannelInitializer: @escaping (Channel) -> EventLoopFuture + ) -> Self { + self.configuration.debugChannelInitializer = debugChannelInitializer + return self + } + #endif } extension Double { From 85416bc1fc66659572b579611c042dd6804ea514 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 28 Oct 2022 11:26:43 +0100 Subject: [PATCH 040/580] Bump version number to 1.12.0 (#1516) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.12.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index adea3d2cb..4bf7a57af 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 11 + internal static let minor = 12 /// The patch version. internal static let patch = 0 From 184230a938bd0c6ffbf68311de137aafe8ce759e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 31 Oct 2022 13:21:44 +0000 Subject: [PATCH 041/580] Add a connection pool delegate (#1515) Motivation: It can be useful to observe what the underlying connection pool is doing over time. This could, for example, be instrumenting it to understand how many streams are active at a given time or to understand when a connection is in the processing of being brought up. Modifications: Add new API: - `GRPCConnectionPoolDelegate` which users can implement and configure on the `GRPCChannelPool` to receive notifications about the connection pool state. - `GRPCConnectionID`, an opaque ID to distinguish between different connections in the pool. Modify the connection pool: - Per-EventLoop pools now hold on to quiescing connections and track additional state, including whether a connection is quiescing and how many streams are currently open on a connection. By contrast the pool manager (which manages a number of these per-EventLoop pools) tracks reserved streams (which are not necessarily open). The delegate tracks _opened_ streams rather than _reserved_ streams (as reserved streams are not allocated to a connection but to an any connection running on an appropriate event-loop). - Wire through the new delegate and call out to it in appropriate places. - Add a stream opened function to the internal H2 delegate - Expose various pieces of state from the connection manager Result: Users can instrument the underlying connection pool. --- .../GRPC/ConnectionManager+Delegates.swift | 6 + Sources/GRPC/ConnectionManager.swift | 46 ++++ .../ConnectionPool/ConnectionManagerID.swift | 2 +- .../ConnectionPool+PerConnectionState.swift | 52 ++++- .../GRPC/ConnectionPool/ConnectionPool.swift | 145 +++++++++++-- .../GRPC/ConnectionPool/GRPCChannelPool.swift | 59 +++++ Sources/GRPC/ConnectionPool/PoolManager.swift | 8 +- .../GRPC/ConnectionPool/PooledChannel.swift | 3 +- Sources/GRPC/GRPCIdleHandler.swift | 1 + Tests/GRPCTests/ConnectionManagerTests.swift | 6 + .../ConnectionPoolDelegates.swift | 205 ++++++++++++++++++ .../ConnectionPool/ConnectionPoolTests.swift | 144 +++++++++++- .../ConnectionPool/GRPCChannelPoolTests.swift | 162 +++++++++++++- .../PoolManagerStateMachineTests.swift | 1 + 14 files changed, 804 insertions(+), 36 deletions(-) create mode 100644 Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift diff --git a/Sources/GRPC/ConnectionManager+Delegates.swift b/Sources/GRPC/ConnectionManager+Delegates.swift index c05b51f3e..35d5a870e 100644 --- a/Sources/GRPC/ConnectionManager+Delegates.swift +++ b/Sources/GRPC/ConnectionManager+Delegates.swift @@ -35,6 +35,12 @@ internal protocol ConnectionManagerConnectivityDelegate { } internal protocol ConnectionManagerHTTP2Delegate { + /// An HTTP/2 stream was opened. + /// + /// - Parameters: + /// - connectionManager: The connection manager reporting the opened stream. + func streamOpened(_ connectionManager: ConnectionManager) + /// An HTTP/2 stream was closed. /// /// - Parameters: diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index bb7ef471a..7847da04f 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -242,6 +242,17 @@ internal final class ConnectionManager { } } + /// Returns whether the state is 'shutdown'. + private var isShutdown: Bool { + self.eventLoop.assertInEventLoop() + switch self.state { + case .shutdown: + return true + case .idle, .connecting, .transientFailure, .active, .ready: + return false + } + } + /// Returns the `HTTP2StreamMultiplexer` from the 'ready' state or `nil` if it is not available. private var multiplexer: HTTP2StreamMultiplexer? { self.eventLoop.assertInEventLoop() @@ -582,6 +593,31 @@ internal final class ConnectionManager { } } + /// Registers a callback which fires when the current active connection is closed. + /// + /// If there is a connection, the callback will be invoked with `true` when the connection is + /// closed. Otherwise the callback is invoked with `false`. + internal func onCurrentConnectionClose(_ onClose: @escaping (Bool) -> Void) { + if self.eventLoop.inEventLoop { + self._onCurrentConnectionClose(onClose) + } else { + self.eventLoop.execute { + self._onCurrentConnectionClose(onClose) + } + } + } + + private func _onCurrentConnectionClose(_ onClose: @escaping (Bool) -> Void) { + self.eventLoop.assertInEventLoop() + + switch self.state { + case let .ready(state): + state.channel.closeFuture.whenComplete { _ in onClose(true) } + case .idle, .connecting, .active, .transientFailure, .shutdown: + onClose(false) + } + } + // MARK: - State changes from the channel handler. /// The channel caught an error. Hold on to it until the channel becomes inactive, it may provide @@ -807,6 +843,11 @@ internal final class ConnectionManager { } } + internal func streamOpened() { + self.eventLoop.assertInEventLoop() + self.http2Delegate?.streamOpened(self) + } + internal func streamClosed() { self.eventLoop.assertInEventLoop() self.http2Delegate?.streamClosed(self) @@ -1001,6 +1042,11 @@ extension ConnectionManager { return self.manager.isIdle } + /// Returne `true` if the connection is in the shutdown state. + internal var isShutdown: Bool { + return self.manager.isShutdown + } + /// Returns the `multiplexer` from a connection in the `ready` state or `nil` if it is any /// other state. internal var multiplexer: HTTP2StreamMultiplexer? { diff --git a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift b/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift index f5c787000..15f87a0e9 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift @@ -15,7 +15,7 @@ */ @usableFromInline -internal struct ConnectionManagerID: Hashable, CustomStringConvertible { +internal struct ConnectionManagerID: Hashable, CustomStringConvertible, GRPCSendable { @usableFromInline internal let _id: ObjectIdentifier diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift index c9be9eddb..02d182919 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift @@ -26,8 +26,32 @@ extension ConnectionPool { @usableFromInline internal var _availability: StreamAvailability? + @usableFromInline + internal var isQuiescing: Bool { + get { + return self._availability?.isQuiescing ?? false + } + set { + self._availability?.isQuiescing = true + } + } + @usableFromInline internal struct StreamAvailability { + @usableFromInline + struct Utilization { + @usableFromInline + var used: Int + @usableFromInline + var capacity: Int + + @usableFromInline + init(used: Int, capacity: Int) { + self.used = used + self.capacity = capacity + } + } + @usableFromInline var multiplexer: HTTP2StreamMultiplexer /// Maximum number of available streams. @@ -36,24 +60,39 @@ extension ConnectionPool { /// Number of streams reserved. @usableFromInline var reserved: Int = 0 + /// Number of streams opened. + @usableFromInline + var open: Int = 0 + @usableFromInline + var isQuiescing = false /// Number of available streams. @usableFromInline var available: Int { - return self.maxAvailable - self.reserved + return self.isQuiescing ? 0 : self.maxAvailable - self.reserved } /// Increment the reserved streams and return the multiplexer. @usableFromInline mutating func reserve() -> HTTP2StreamMultiplexer { + assert(!self.isQuiescing) self.reserved += 1 return self.multiplexer } + @usableFromInline + mutating func opened() -> Utilization { + self.open += 1 + return .init(used: self.open, capacity: self.maxAvailable) + } + /// Decrement the reserved streams by one. @usableFromInline - mutating func `return`() { + mutating func `return`() -> Utilization { self.reserved -= 1 + self.open -= 1 assert(self.reserved >= 0) + assert(self.open >= 0) + return .init(used: self.open, capacity: self.maxAvailable) } } @@ -92,10 +131,15 @@ extension ConnectionPool { return self._availability?.reserve() } + @usableFromInline + internal mutating func openedStream() -> PerConnectionState.StreamAvailability.Utilization? { + return self._availability?.opened() + } + /// Return a reserved stream to the connection. @usableFromInline - internal mutating func returnStream() { - self._availability?.return() + internal mutating func returnStream() -> PerConnectionState.StreamAvailability.Utilization? { + return self._availability?.return() } /// Update the maximum concurrent streams available on the connection, marking it as available diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 0056500da..554d3b5bb 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -98,6 +98,9 @@ internal final class ConnectionPool { @usableFromInline internal let streamLender: StreamLender + @usableFromInline + internal var delegate: GRPCConnectionPoolDelegate? + /// A logger which always sets "GRPC" as its source. @usableFromInline internal let logger: GRPCLogger @@ -147,6 +150,7 @@ internal final class ConnectionPool { connectionBackoff: ConnectionBackoff, channelProvider: ConnectionManagerChannelProvider, streamLender: StreamLender, + delegate: GRPCConnectionPoolDelegate?, logger: GRPCLogger, now: @escaping () -> NIODeadline = NIODeadline.now ) { @@ -165,6 +169,7 @@ internal final class ConnectionPool { self.connectionBackoff = connectionBackoff self.channelProvider = channelProvider self.streamLender = streamLender + self.delegate = delegate self.logger = logger self.now = now } @@ -191,7 +196,9 @@ internal final class ConnectionPool { http2Delegate: self, logger: self.logger.unwrapped ) - self._connections[manager.id] = PerConnectionState(manager: manager) + let id = manager.id + self._connections[id] = PerConnectionState(manager: manager) + self.delegate?.connectionAdded(id: .init(id)) } // MARK: - Called from the pool manager @@ -397,8 +404,8 @@ internal final class ConnectionPool { // TODO: make this cheaper by storing and incrementally updating the number of idle connections let capacity = self._connections.values.reduce(0) { sum, state in - if state.manager.sync.isIdle { - // Idle connection, no capacity. + if state.manager.sync.isIdle || state.isQuiescing { + // Idle connection or quiescing (so the capacity should be ignored). return sum } else if let knownMaxAvailableStreams = state.maxAvailableStreams { // A known value of max concurrent streams, i.e. the connection is active. @@ -502,6 +509,7 @@ internal final class ConnectionPool { self._state = .shuttingDown(promise.futureResult) promise.futureResult.whenComplete { _ in self._state = .shutdown + self.delegate = nil self.logger.trace("finished shutting down connection pool") } @@ -509,8 +517,22 @@ internal final class ConnectionPool { let connections = self._connections self._connections.removeAll() - let allShutdown = connections.values.map { - $0.manager.shutdown(mode: mode) + let allShutdown: [EventLoopFuture] = connections.values.map { + let id = $0.manager.id + let manager = $0.manager + + return manager.eventLoop.flatSubmit { + // If the connection was idle/shutdown before calling shutdown then we shouldn't tell + // the delegate the connection closed (because it either never connected or was already + // informed about this). + let connectionIsInactive = manager.sync.isIdle || manager.sync.isShutdown + return manager.shutdown(mode: mode).always { _ in + if !connectionIsInactive { + self.delegate?.connectionClosed(id: .init(id), error: nil) + } + self.delegate?.connectionRemoved(id: .init(id)) + } + } } // Fail the outstanding waiters. @@ -564,27 +586,71 @@ extension ConnectionPool: ConnectionManagerConnectivityDelegate { default: () } + + guard let delegate = self.delegate else { return } + + switch (oldState, newState) { + case (.idle, .connecting), + (.transientFailure, .connecting): + delegate.startedConnecting(id: .init(manager.id)) + + case (.connecting, .ready): + // The connection becoming ready is handled by 'receivedSettingsMaxConcurrentStreams'. + () + + case (.ready, .idle): + delegate.connectionClosed(id: .init(manager.id), error: nil) + + case let (.ready, .transientFailure(error)): + delegate.connectionClosed(id: .init(manager.id), error: error) + + case let (.connecting, .transientFailure(error)): + delegate.connectFailed(id: .init(manager.id), error: error) + + default: + () + } } func connectionIsQuiescing(_ manager: ConnectionManager) { self.eventLoop.assertInEventLoop() - guard let removed = self._connections.removeValue(forKey: manager.id) else { + + // Find the relevant connection. + guard let index = self._connections.index(forKey: manager.id) else { return } - // Drop any delegates. We're no longer interested in these events. - removed.manager.sync.connectivityDelegate = nil - removed.manager.sync.http2Delegate = nil + // Drop the connectivity delegate, we're no longer interested in its events now. + manager.sync.connectivityDelegate = nil + + // Started quiescing; update our state and notify the pool delegate. + self._connections.values[index].isQuiescing = true + self.delegate?.connectionQuiescing(id: .init(manager.id)) + + // As the connection is quescing, we need to know when the current connection its managing has + // closed. When that happens drop the H2 delegate and update the pool delegate. + manager.onCurrentConnectionClose { hadActiveConnection in + assert(hadActiveConnection) + if let removed = self._connections.removeValue(forKey: manager.id) { + removed.manager.sync.http2Delegate = nil + self.delegate?.connectionClosed(id: .init(removed.manager.id), error: nil) + self.delegate?.connectionRemoved(id: .init(removed.manager.id)) + } + } + + // Grab the number of reserved streams (before invalidating the index by adding a connection). + let reservedStreams = self._connections.values[index].reservedStreams // Replace the connection with a new idle one. self.addConnectionToPool() - // Since we're removing this connection from the pool, the pool manager can ignore any streams - // reserved against this connection. + // Since we're removing this connection from the pool (and no new streams can be created on + // the connection), the pool manager can ignore any streams reserved against this connection. + // We do still care about the number of reserved streams for the connection though // - // Note: we don't need to adjust the number of available streams as the number of connections - // hasn't changed. - self.streamLender.returnStreams(removed.reservedStreams, to: self) + // Note: we don't need to adjust the number of available streams as the effective number of + // connections hasn't changed. + self.streamLender.returnStreams(reservedStreams, to: self) } private func updateMostRecentError(_ error: Error) { @@ -606,15 +672,43 @@ extension ConnectionPool: ConnectionManagerConnectivityDelegate { } extension ConnectionPool: ConnectionManagerHTTP2Delegate { + internal func streamOpened(_ manager: ConnectionManager) { + self.eventLoop.assertInEventLoop() + if let utilization = self._connections[manager.id]?.openedStream(), + let delegate = self.delegate { + delegate.connectionUtilizationChanged( + id: .init(manager.id), + streamsUsed: utilization.used, + streamCapacity: utilization.capacity + ) + } + } + internal func streamClosed(_ manager: ConnectionManager) { self.eventLoop.assertInEventLoop() + guard let index = self._connections.index(forKey: manager.id) else { + return + } + // Return the stream the connection and to the pool manager. - self._connections[manager.id]?.returnStream() - self.streamLender.returnStreams(1, to: self) + if let utilization = self._connections.values[index].returnStream(), + let delegate = self.delegate { + delegate.connectionUtilizationChanged( + id: .init(manager.id), + streamsUsed: utilization.used, + streamCapacity: utilization.capacity + ) + } - // A stream was returned: we may be able to service a waiter now. - self.tryServiceWaiters() + // Don't return the stream to the pool manager if the connection is quescing, they were returned + // when the connection started quiescing. + if !self._connections.values[index].isQuiescing { + self.streamLender.returnStreams(1, to: self) + + // A stream was returned: we may be able to service a waiter now. + self.tryServiceWaiters() + } } internal func receivedSettingsMaxConcurrentStreams( @@ -623,10 +717,21 @@ extension ConnectionPool: ConnectionManagerHTTP2Delegate { ) { self.eventLoop.assertInEventLoop() + // Find the relevant connection. + guard let index = self._connections.index(forKey: manager.id) else { + return + } + + // When the connection is quiescing, the pool manager is not interested in updates to the + // connection, bail out early. + if self._connections.values[index].isQuiescing { + return + } + // If we received a SETTINGS update then a connection is okay: drop the last known error. self._mostRecentError = nil - let previous = self._connections[manager.id]?.updateMaxConcurrentStreams(maxConcurrentStreams) + let previous = self._connections.values[index].updateMaxConcurrentStreams(maxConcurrentStreams) let delta: Int if let previousValue = previous { @@ -637,6 +742,8 @@ extension ConnectionPool: ConnectionManagerHTTP2Delegate { // There was no previous value so this must be a new connection. We'll compare against our // assumed default. delta = maxConcurrentStreams - self.assumedMaxConcurrentStreams + // Notify the delegate. + self.delegate?.connectSucceeded(id: .init(manager.id), streamCapacity: maxConcurrentStreams) } if delta != 0 { diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift index 32f5f9898..ea7a23c53 100644 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift @@ -177,6 +177,10 @@ extension GRPCChannelPool { /// An error delegate which is called when errors are caught. public var errorDelegate: ClientErrorDelegate? + /// A delegate which will be notified about changes to the state of connections managed by the + /// pool. + public var delegate: GRPCConnectionPoolDelegate? + /// A logger used for background activity, such as connection state changes. public var backgroundActivityLogger = Logger( label: "io.grpc", @@ -287,3 +291,58 @@ extension GRPCChannelPool.Configuration { public var reservationLoadThreshold: Double = 0.9 } } + +/// The ID of a connection in the connection pool. +public struct GRPCConnectionID: Hashable, GRPCSendable, CustomStringConvertible { + private let id: ConnectionManagerID + + public var description: String { + return String(describing: self.id) + } + + internal init(_ id: ConnectionManagerID) { + self.id = id + } +} + +/// A delegate for the connection pool which is notified of various lifecycle events. +/// +/// All functions must execute quickly and may be executed on arbitrary threads. The implementor is +/// responsible for ensuring thread safety. +public protocol GRPCConnectionPoolDelegate: GRPCSendable { + /// A new connection was created with the given ID and added to the pool. The connection is not + /// yet active (or connecting). + /// + /// In most cases ``startedConnecting(id:)`` will be the next function called for the given + /// connection but ``connectionRemoved(id:)`` may also be called. + func connectionAdded(id: GRPCConnectionID) + + /// The connection with the given ID was removed from the pool. + func connectionRemoved(id: GRPCConnectionID) + + /// The connection with the given ID has started trying to establish a connection. The outcome + /// of the connection will be reported as either ``connectSucceeded(id:streamCapacity:)`` or + /// ``connectFailed(id:error:)``. + func startedConnecting(id: GRPCConnectionID) + + /// A connection attempt failed with the given error. After some period of + /// time ``startedConnecting(id:)`` may be called again. + func connectFailed(id: GRPCConnectionID, error: Error) + + /// A connection was established on the connection with the given ID. `streamCapacity` streams are + /// available to use on the connection. The maximum number of available streams may change over + /// time and is reported via ``connectionUtilizationChanged(id:streamsUsed:streamCapacity:)``. The + func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) + + /// The utlization of the connection changed; a stream may have been used, returned or the + /// maximum number of concurrent streams available on the connection changed. + func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) + + /// The remote peer is quiescing the connection: no new streams will be created on it. The + /// connection will eventually be closed and removed from the pool. + func connectionQuiescing(id: GRPCConnectionID) + + /// The connection was closed. The connection may be established again in the future (notified + /// via ``startedConnecting(id:)``). + func connectionClosed(id: GRPCConnectionID, error: Error?) +} diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index 8bfca4a07..05bd33641 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -57,6 +57,9 @@ internal final class PoolManager { @usableFromInline var channelProvider: DefaultChannelProvider + @usableFromInline + var delegate: GRPCConnectionPoolDelegate? + @usableFromInline internal init( maxConnections: Int, @@ -64,7 +67,8 @@ internal final class PoolManager { loadThreshold: Double, assumedMaxConcurrentStreams: Int = 100, connectionBackoff: ConnectionBackoff, - channelProvider: DefaultChannelProvider + channelProvider: DefaultChannelProvider, + delegate: GRPCConnectionPoolDelegate? ) { self.maxConnections = maxConnections self.maxWaiters = maxWaiters @@ -72,6 +76,7 @@ internal final class PoolManager { self.assumedMaxConcurrentStreams = assumedMaxConcurrentStreams self.connectionBackoff = connectionBackoff self.channelProvider = channelProvider + self.delegate = delegate } } @@ -224,6 +229,7 @@ internal final class PoolManager { connectionBackoff: configuration.connectionBackoff, channelProvider: configuration.channelProvider, streamLender: self, + delegate: configuration.delegate, logger: logger ) } diff --git a/Sources/GRPC/ConnectionPool/PooledChannel.swift b/Sources/GRPC/ConnectionPool/PooledChannel.swift index d3a407178..999f87f6c 100644 --- a/Sources/GRPC/ConnectionPool/PooledChannel.swift +++ b/Sources/GRPC/ConnectionPool/PooledChannel.swift @@ -98,7 +98,8 @@ internal final class PooledChannel: GRPCChannel { loadThreshold: configuration.connectionPool.reservationLoadThreshold, assumedMaxConcurrentStreams: 100, connectionBackoff: configuration.connectionBackoff, - channelProvider: provider + channelProvider: provider, + delegate: configuration.delegate ), logger: configuration.backgroundActivityLogger.wrapped ) diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 25ed79d44..b2b87e7af 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -239,6 +239,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { if let created = event as? NIOHTTP2StreamCreatedEvent { self.perform(operations: self.stateMachine.streamCreated(withID: created.streamID)) self.handlePingAction(self.pingHandler.streamCreated()) + self.mode.connectionManager?.streamOpened() context.fireUserInboundEventTriggered(event) } else if let closed = event as? StreamClosedEvent { self.perform(operations: self.stateMachine.streamClosed(withID: closed.streamID)) diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 5b7ea1c03..ec1358c7e 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -1034,9 +1034,14 @@ extension ConnectionManagerTests { ) class HTTP2Delegate: ConnectionManagerHTTP2Delegate { + var streamsOpened = 0 var streamsClosed = 0 var maxConcurrentStreams = 0 + func streamOpened(_ connectionManager: ConnectionManager) { + self.streamsOpened += 1 + } + func streamClosed(_ connectionManager: ConnectionManager) { self.streamsClosed += 1 } @@ -1118,6 +1123,7 @@ extension ConnectionManagerTests { channel.pipeline.fireUserInboundEventTriggered(streamClosed) } + XCTAssertEqual(http2.streamsOpened, 4) XCTAssertEqual(http2.streamsClosed, 4) } diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift new file mode 100644 index 000000000..3691706ff --- /dev/null +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift @@ -0,0 +1,205 @@ +/* + * Copyright 2022, 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 GRPC +import NIOConcurrencyHelpers +import NIOCore + +final class IsConnectingDelegate: GRPCConnectionPoolDelegate { + private let lock = NIOLock() + private var connecting = Set() + private var active = Set() + + enum StateNotifacation: Hashable, GRPCSendable { + case connecting + case connected + } + + #if swift(>=5.6) + private let onStateChange: @Sendable (StateNotifacation) -> Void + #else + private let onStateChange: (StateNotifacation) -> Void + #endif + + #if swift(>=5.6) + init(onStateChange: @escaping @Sendable (StateNotifacation) -> Void) { + self.onStateChange = onStateChange + } + #else + init(onStateChange: @escaping (StateNotifacation) -> Void) { + self.onStateChange = onStateChange + } + #endif + + func startedConnecting(id: GRPCConnectionID) { + let didStartConnecting: Bool = self.lock.withLock { + let (inserted, _) = self.connecting.insert(id) + // Only intereseted new connection attempts when there are no active connections. + return inserted && self.connecting.count == 1 && self.active.isEmpty + } + + if didStartConnecting { + self.onStateChange(.connecting) + } + } + + func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) { + let didStopConnecting: Bool = self.lock.withLock { + let removed = self.connecting.remove(id) != nil + let (inserted, _) = self.active.insert(id) + return removed && inserted && self.active.count == 1 + } + + if didStopConnecting { + self.onStateChange(.connected) + } + } + + func connectionClosed(id: GRPCConnectionID, error: Error?) { + self.lock.withLock { + self.active.remove(id) + self.connecting.remove(id) + } + } + + func connectionQuiescing(id: GRPCConnectionID) { + self.lock.withLock { + _ = self.active.remove(id) + } + } + + // No-op. + func connectionAdded(id: GRPCConnectionID) {} + + // No-op. + func connectionRemoved(id: GRPCConnectionID) {} + + // Conection failures put the connection into a backing off state, we consider that to still + // be 'connecting' at this point. + func connectFailed(id: GRPCConnectionID, error: Error) {} + + // No-op. + func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) {} +} + +#if swift(>=5.6) +extension IsConnectingDelegate: @unchecked Sendable {} +#endif + +final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { + struct UnexpectedEvent: Error { + var event: Event + + init(_ event: Event) { + self.event = event + } + } + + enum Event: Equatable { + case connectionAdded(GRPCConnectionID) + case startedConnecting(GRPCConnectionID) + case connectFailed(GRPCConnectionID) + case connectSucceeded(GRPCConnectionID, Int) + case connectionClosed(GRPCConnectionID) + case connectionUtilizationChanged(GRPCConnectionID, Int, Int) + case connectionQuiescing(GRPCConnectionID) + case connectionRemoved(GRPCConnectionID) + + var id: GRPCConnectionID { + switch self { + case let .connectionAdded(id), + let .startedConnecting(id), + let .connectFailed(id), + let .connectSucceeded(id, _), + let .connectionClosed(id), + let .connectionUtilizationChanged(id, _, _), + let .connectionQuiescing(id), + let .connectionRemoved(id): + return id + } + } + } + + private var events: CircularBuffer = [] + private let lock = NIOLock() + + var first: Event? { + return self.lock.withLock { + self.events.first + } + } + + var isEmpty: Bool { + return self.lock.withLock { self.events.isEmpty } + } + + func popFirst() -> Event? { + return self.lock.withLock { + self.events.popFirst() + } + } + + func connectionAdded(id: GRPCConnectionID) { + self.lock.withLock { + self.events.append(.connectionAdded(id)) + } + } + + func startedConnecting(id: GRPCConnectionID) { + self.lock.withLock { + self.events.append(.startedConnecting(id)) + } + } + + func connectFailed(id: GRPCConnectionID, error: Error) { + self.lock.withLock { + self.events.append(.connectFailed(id)) + } + } + + func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) { + self.lock.withLock { + self.events.append(.connectSucceeded(id, streamCapacity)) + } + } + + func connectionClosed(id: GRPCConnectionID, error: Error?) { + self.lock.withLock { + self.events.append(.connectionClosed(id)) + } + } + + func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) { + self.lock.withLock { + self.events.append(.connectionUtilizationChanged(id, streamsUsed, streamCapacity)) + } + } + + func connectionQuiescing(id: GRPCConnectionID) { + self.lock.withLock { + self.events.append(.connectionQuiescing(id)) + } + } + + func connectionRemoved(id: GRPCConnectionID) { + self.lock.withLock { + self.events.append(.connectionRemoved(id)) + } + } +} + +#if swift(>=5.6) +extension EventRecordingConnectionPoolDelegate: @unchecked Sendable {} +#endif // swift(>=5.6) diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index a84bb2e36..df39c1899 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -53,6 +53,7 @@ final class ConnectionPoolTests: GRPCTestCase { reservationLoadThreshold: Double = 0.9, now: @escaping () -> NIODeadline = { .now() }, connectionBackoff: ConnectionBackoff = ConnectionBackoff(), + delegate: GRPCConnectionPoolDelegate? = nil, onReservationReturned: @escaping (Int) -> Void = { _ in }, onMaximumReservationsChange: @escaping (Int) -> Void = { _ in }, channelProvider: ConnectionManagerChannelProvider @@ -68,6 +69,7 @@ final class ConnectionPoolTests: GRPCTestCase { onReturnStreams: onReservationReturned, onUpdateMaxAvailableStreams: onMaximumReservationsChange ), + delegate: delegate, logger: self.logger.wrapped, now: now ) @@ -75,10 +77,12 @@ final class ConnectionPoolTests: GRPCTestCase { private func makePool( waiters: Int = 1000, + delegate: GRPCConnectionPoolDelegate? = nil, makeChannel: @escaping (ConnectionManager, EventLoop) -> EventLoopFuture ) -> ConnectionPool { return self.makePool( waiters: waiters, + delegate: delegate, channelProvider: HookedChannelProvider(makeChannel) ) } @@ -88,6 +92,7 @@ final class ConnectionPoolTests: GRPCTestCase { reservationLoadThreshold: Double = 0.9, now: @escaping () -> NIODeadline = { .now() }, connectionBackoff: ConnectionBackoff = ConnectionBackoff(), + delegate: GRPCConnectionPoolDelegate? = nil, onReservationReturned: @escaping (Int) -> Void = { _ in }, onMaximumReservationsChange: @escaping (Int) -> Void = { _ in } ) -> (ConnectionPool, ChannelController) { @@ -97,6 +102,7 @@ final class ConnectionPoolTests: GRPCTestCase { reservationLoadThreshold: reservationLoadThreshold, now: now, connectionBackoff: connectionBackoff, + delegate: delegate, onReservationReturned: onReservationReturned, onMaximumReservationsChange: onMaximumReservationsChange, channelProvider: controller @@ -127,7 +133,9 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertEqual(pool.sync.availableStreams, 0) XCTAssertEqual(pool.sync.reservedStreams, 0) - XCTAssertNoThrow(try pool.shutdown().wait()) + let shutdownFuture = pool.shutdown() + self.eventLoop.run() + XCTAssertNoThrow(try shutdownFuture.wait()) } func testShutdownEmptyPool() { @@ -600,7 +608,8 @@ final class ConnectionPoolTests: GRPCTestCase { // The quiescing connection had 1 stream reserved, it's now returned to the outer pool and we // have a new idle connection in place of the old one. XCTAssertEqual(reservationsReturned, [1]) - XCTAssertEqual(pool.sync.reservedStreams, 0) + // The inner pool still knows about the reserved stream. + XCTAssertEqual(pool.sync.reservedStreams, 1) XCTAssertEqual(pool.sync.availableStreams, 0) XCTAssertEqual(pool.sync.idleConnections, 1) @@ -619,7 +628,8 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertNoThrow(try w2.wait()) controller.openStreamInChannel(atIndex: 1) - XCTAssertEqual(pool.sync.reservedStreams, 1) + // The stream on the quiescing connection is still reserved. + XCTAssertEqual(pool.sync.reservedStreams, 2) XCTAssertEqual(pool.sync.availableStreams, 99) // Return a stream for the _quiescing_ connection: nothing should change in the pool. @@ -865,6 +875,130 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertThrowsError(try promise.futureResult.wait()) XCTAssertNil(waiter._scheduledTimeout) } + + func testConnectionPoolDelegate() throws { + let recorder = EventRecordingConnectionPoolDelegate() + let (pool, controller) = self.setUpPoolAndController(delegate: recorder) + pool.initialize(connections: 2) + + func assertConnectionAdded( + _ event: EventRecordingConnectionPoolDelegate.Event? + ) throws -> GRPCConnectionID { + let unwrappedEvent = try XCTUnwrap(event) + switch unwrappedEvent { + case let .connectionAdded(id): + return id + default: + throw EventRecordingConnectionPoolDelegate.UnexpectedEvent(unwrappedEvent) + } + } + + let connID1 = try assertConnectionAdded(recorder.popFirst()) + let connID2 = try assertConnectionAdded(recorder.popFirst()) + + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + $0.eventLoop.makeSucceededVoidFuture() + } + // Start creating the channel. + self.eventLoop.run() + + let startedConnecting = recorder.popFirst() + let firstConn: GRPCConnectionID + let secondConn: GRPCConnectionID + + if startedConnecting == .startedConnecting(connID1) { + firstConn = connID1 + secondConn = connID2 + } else if startedConnecting == .startedConnecting(connID2) { + firstConn = connID2 + secondConn = connID1 + } else { + return XCTFail("Unexpected event") + } + + // Connect the connection. + self.eventLoop.run() + controller.connectChannel(atIndex: 0) + controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) + XCTAssertEqual(recorder.popFirst(), .connectSucceeded(firstConn, 10)) + + // Open a stream for the waiter. + controller.openStreamInChannel(atIndex: 0) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, 1, 10)) + self.eventLoop.run() + XCTAssertNoThrow(try waiter.wait()) + + // Okay, more utilization! + for n in 2 ... 8 { + let w = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + $0.eventLoop.makeSucceededVoidFuture() + } + + controller.openStreamInChannel(atIndex: 0) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, n, 10)) + self.eventLoop.run() + XCTAssertNoThrow(try w.wait()) + } + + // The utilisation threshold before bringing up a new connection is 0.9; we have 8 open streams + // (out of 10) now so opening the next should trigger a connect on the other connection. + let w9 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + $0.eventLoop.makeSucceededVoidFuture() + } + XCTAssertEqual(recorder.popFirst(), .startedConnecting(secondConn)) + + // Deal with the 9th stream. + controller.openStreamInChannel(atIndex: 0) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, 9, 10)) + self.eventLoop.run() + XCTAssertNoThrow(try w9.wait()) + + // Bring up the next connection. + controller.connectChannel(atIndex: 1) + controller.sendSettingsToChannel(atIndex: 1, maxConcurrentStreams: 10) + XCTAssertEqual(recorder.popFirst(), .connectSucceeded(secondConn, 10)) + + // The next stream should be on the new connection. + let w10 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + $0.eventLoop.makeSucceededVoidFuture() + } + + // Deal with the 10th stream. + controller.openStreamInChannel(atIndex: 1) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(secondConn, 1, 10)) + self.eventLoop.run() + XCTAssertNoThrow(try w10.wait()) + + // Close the streams. + for i in 1 ... 9 { + controller.closeStreamInChannel(atIndex: 0) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, 9 - i, 10)) + } + + controller.closeStreamInChannel(atIndex: 1) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(secondConn, 0, 10)) + + // Close the connections. + controller.fireChannelInactiveForChannel(atIndex: 0) + XCTAssertEqual(recorder.popFirst(), .connectionClosed(firstConn)) + controller.fireChannelInactiveForChannel(atIndex: 1) + XCTAssertEqual(recorder.popFirst(), .connectionClosed(secondConn)) + + // All conns are already closed. + let shutdownFuture = pool.shutdown() + self.eventLoop.run() + XCTAssertNoThrow(try shutdownFuture.wait()) + + // Two connections must be removed. + for _ in 0 ..< 2 { + if let event = recorder.popFirst() { + let id = event.id + XCTAssertEqual(event, .connectionRemoved(id)) + } else { + XCTFail("Expected .connectionRemoved") + } + } + } } extension ConnectionPool { @@ -1031,8 +1165,8 @@ internal struct HookedStreamLender: StreamLender { self.onReturnStreams(count) } - internal func changeStreamCapacity(by max: Int, for pool: ConnectionPool) { - self.onUpdateMaxAvailableStreams(max) + internal func changeStreamCapacity(by delta: Int, for: ConnectionPool) { + self.onUpdateMaxAvailableStreams(delta) } } diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index 581db9a34..b8c560bbb 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -72,9 +72,9 @@ final class GRPCChannelPoolTests: GRPCTestCase { .withServiceProviders([EchoProvider()]) } - private func startServer(withTLS: Bool = false) { + private func startServer(withTLS: Bool = false, port: Int = 0) { self.server = try! self.makeServerBuilder(withTLS: withTLS) - .bind(host: "localhost", port: 0) + .bind(host: "localhost", port: port) .wait() } @@ -104,13 +104,18 @@ final class GRPCChannelPoolTests: GRPCTestCase { } } - private func setUpClientAndServer(withTLS tls: Bool) { - self.configureEventLoopGroup() + private func setUpClientAndServer( + withTLS tls: Bool, + threads: Int = System.coreCount, + _ configure: (inout GRPCChannelPool.Configuration) -> Void = { _ in } + ) { + self.configureEventLoopGroup(threads: threads) self.startServer(withTLS: tls) self.startChannel(withTLS: tls) { // We'll allow any number of waiters since we immediately fire off a bunch of RPCs and don't // want to bounce off the limit as we wait for a connection to come up. $0.connectionPool.maxWaitersPerEventLoop = .max + configure(&$0) } } @@ -435,6 +440,153 @@ final class GRPCChannelPoolTests: GRPCTestCase { XCTFail("Status message did not contain a possible cause: '\(status.message ?? "nil")'") } } -} + func testConnectionPoolDelegateSingleConnection() throws { + let recorder = EventRecordingConnectionPoolDelegate() + self.setUpClientAndServer(withTLS: false, threads: 1) { + $0.delegate = recorder + } + + let warmup = self.echo.get(.with { $0.text = "" }) + XCTAssertNoThrow(try warmup.status.wait()) + + let id = try XCTUnwrap(recorder.first?.id) + XCTAssertEqual(recorder.popFirst(), .connectionAdded(id)) + XCTAssertEqual(recorder.popFirst(), .startedConnecting(id)) + XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 0, 100)) + + let rpcs: [ClientStreamingCall] = try (1 ... 10).map { i in + let rpc = self.echo.collect() + XCTAssertNoThrow(try rpc.sendMessage(.with { $0.text = "foo" }).wait()) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, i, 100)) + return rpc + } + + for (i, rpc) in rpcs.enumerated() { + XCTAssertNoThrow(try rpc.sendEnd().wait()) + XCTAssertNoThrow(try rpc.status.wait()) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 10 - (i + 1), 100)) + } + + XCTAssertNoThrow(try self.channel?.close().wait()) + XCTAssertEqual(recorder.popFirst(), .connectionClosed(id)) + XCTAssertEqual(recorder.popFirst(), .connectionRemoved(id)) + XCTAssert(recorder.isEmpty) + } + + func testConnectionPoolDelegateQuiescing() throws { + let recorder = EventRecordingConnectionPoolDelegate() + self.setUpClientAndServer(withTLS: false, threads: 1) { + $0.delegate = recorder + } + + XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) + let id1 = try XCTUnwrap(recorder.first?.id) + XCTAssertEqual(recorder.popFirst(), .connectionAdded(id1)) + XCTAssertEqual(recorder.popFirst(), .startedConnecting(id1)) + XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 0, 100)) + + // Start an RPC. + let rpc = self.echo.collect() + XCTAssertNoThrow(try rpc.sendMessage(.with { $0.text = "foo" }).wait()) + // Complete another one to make sure the previous one is known by the server. + XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) + + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 2, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) + + // Start shutting the server down. + let didShutdown = self.server!.initiateGracefulShutdown() + self.server = nil // Avoid shutting down again in tearDown + + // Pause a moment so we know the client received the GOAWAY. + let sleep = self.group.any().scheduleTask(in: .milliseconds(50)) {} + XCTAssertNoThrow(try sleep.futureResult.wait()) + XCTAssertEqual(recorder.popFirst(), .connectionQuiescing(id1)) + + // Finish the RPC. + XCTAssertNoThrow(try rpc.sendEnd().wait()) + XCTAssertNoThrow(try rpc.status.wait()) + + // Server should shutdown now. + XCTAssertNoThrow(try didShutdown.wait()) + } + + func testDelegateCanTellWhenFirstConnectionIsBeingEstablished() { + final class State { + private enum _State { + case idle + case connecting + case connected + } + + private var state: _State = .idle + private let lock = NIOLock() + + var isConnected: Bool { + return self.lock.withLock { + switch self.state { + case .connected: + return true + case .idle, .connecting: + return false + } + } + } + + func startedConnecting() { + self.lock.withLock { + switch self.state { + case .idle: + self.state = .connecting + case .connecting, .connected: + XCTFail("Invalid state \(self.state) for \(#function)") + } + } + } + + func connected() { + self.lock.withLock { + switch self.state { + case .connecting: + self.state = .connected + case .idle, .connected: + XCTFail("Invalid state \(self.state) for \(#function)") + } + } + } + } + + let state = State() + + self.setUpClientAndServer(withTLS: false, threads: 1) { + $0.delegate = IsConnectingDelegate { stateChange in + switch stateChange { + case .connecting: + state.startedConnecting() + case .connected: + state.connected() + } + } + } + + XCTAssertFalse(state.isConnected) + let rpc = self.echo.get(.with { $0.text = "" }) + XCTAssertNoThrow(try rpc.status.wait()) + XCTAssertTrue(state.isConnected) + + // We should be able to do a bunch of other RPCs without the state changing (we'll XCTFail if + // a state change happens). + let rpcs: [EventLoopFuture] = (0 ..< 20).map { i in + let rpc = self.echo.get(.with { $0.text = "\(i)" }) + return rpc.status + } + XCTAssertNoThrow(try EventLoopFuture.andAllSucceed(rpcs, on: self.group.any()).wait()) + } +} #endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift index 2a663d1a0..5610b9655 100644 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift @@ -39,6 +39,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { onReturnStreams: { _ in }, onUpdateMaxAvailableStreams: { _ in } ), + delegate: nil, logger: self.logger.wrapped ) } From 3e17f2b847da39b50329b7a11c5ad23453190e8b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 3 Nov 2022 12:45:16 +0000 Subject: [PATCH 042/580] Bump version number to 1.13.0 (#1519) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.13.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 4bf7a57af..7307e842a 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 12 + internal static let minor = 13 /// The patch version. internal static let patch = 0 From d7f2a53b0adeb5bd848e749f2083adc8f2e4ca44 Mon Sep 17 00:00:00 2001 From: carolinacass <67160898+carolinacass@users.noreply.github.com> Date: Tue, 8 Nov 2022 16:11:53 +0000 Subject: [PATCH 043/580] Use #fileID/#filePath instead of #file (#1518) --- .../RouteGuide/Client/RouteGuideClient.swift | 2 +- .../RouteGuide/Server/RouteGuideServer.swift | 2 +- Sources/GRPC/ConnectionManager.swift | 2 +- Sources/GRPC/GRPCError.swift | 4 ++-- Sources/GRPC/GRPCLogger.swift | 8 +++---- Sources/GRPC/PlatformSupport.swift | 2 +- .../Assertions.swift | 8 +++---- .../AsyncAwaitSupport/XCTest+AsyncAwait.swift | 2 +- Tests/GRPCTests/ClientTransportTests.swift | 4 ++-- Tests/GRPCTests/ConnectionManagerTests.swift | 6 ++--- .../ConnectionPool/ConnectionPoolTests.swift | 16 ++++++------- .../PoolManagerStateMachineTests.swift | 10 ++++---- .../EventLoopFuture+Assertions.swift | 6 ++--- Tests/GRPCTests/FunctionalTests.swift | 6 ++--- .../GRPCTests/GRPCNetworkFrameworkTests.swift | 2 +- Tests/GRPCTests/GRPCTestCase.swift | 2 +- .../GRPCWebToHTTP2StateMachineTests.swift | 24 +++++++++---------- .../GRPCTests/HeaderNormalizationTests.swift | 2 +- .../SampleCertificate+Assertions.swift | 2 +- .../ServerFuzzingRegressionTests.swift | 2 +- Tests/GRPCTests/XCTestHelpers.swift | 12 +++++----- 21 files changed, 62 insertions(+), 62 deletions(-) diff --git a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift index 7b34777d0..c6d416a5a 100644 --- a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift +++ b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift @@ -23,7 +23,7 @@ import RouteGuideModel /// Loads the features from `route_guide_db.json`, assumed to be in the directory above this file. func loadFeatures() throws -> [Routeguide_Feature] { - let url = URL(fileURLWithPath: #file) + let url = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() // main.swift .deletingLastPathComponent() // Client/ .appendingPathComponent("route_guide_db.json") diff --git a/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift b/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift index 1b76ca4a2..192478bc6 100644 --- a/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift +++ b/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift @@ -24,7 +24,7 @@ import RouteGuideModel /// Loads the features from `route_guide_db.json`, assumed to be in the directory above this file. func loadFeatures() throws -> [Routeguide_Feature] { - let url = URL(fileURLWithPath: #file) + let url = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() // main.swift .deletingLastPathComponent() // Server/ .appendingPathComponent("route_guide_db.json") diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 7847da04f..32456efc9 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -1063,7 +1063,7 @@ extension ConnectionManager { extension ConnectionManager { private func invalidState( function: StaticString = #function, - file: StaticString = #file, + file: StaticString = #fileID, line: UInt = #line ) -> Never { preconditionFailure("Invalid state \(self.state) for \(function)", file: file, line: line) diff --git a/Sources/GRPC/GRPCError.swift b/Sources/GRPC/GRPCError.swift index 051e2a6c0..99594ec90 100644 --- a/Sources/GRPC/GRPCError.swift +++ b/Sources/GRPC/GRPCError.swift @@ -309,7 +309,7 @@ extension GRPCError { init( _ error: GRPCStatusTransformable, - file: StaticString = #file, + file: StaticString = #fileID, line: Int = #line, function: StaticString = #function ) { @@ -331,7 +331,7 @@ public protocol GRPCErrorProtocol: GRPCStatusTransformable, Equatable, CustomStr extension GRPCErrorProtocol { /// Creates a `GRPCError.WithContext` containing a `GRPCError` and the location of the call site. internal func captureContext( - file: StaticString = #file, + file: StaticString = #fileID, line: Int = #line, function: StaticString = #function ) -> GRPCError.WithContext { diff --git a/Sources/GRPC/GRPCLogger.swift b/Sources/GRPC/GRPCLogger.swift index 7d46109a5..2f81fac21 100644 --- a/Sources/GRPC/GRPCLogger.swift +++ b/Sources/GRPC/GRPCLogger.swift @@ -46,7 +46,7 @@ internal struct GRPCLogger { internal func trace( _ message: @autoclosure () -> Logger.Message, metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #file, + file: String = #fileID, function: String = #function, line: UInt = #line ) { @@ -64,7 +64,7 @@ internal struct GRPCLogger { internal func debug( _ message: @autoclosure () -> Logger.Message, metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #file, + file: String = #fileID, function: String = #function, line: UInt = #line ) { @@ -82,7 +82,7 @@ internal struct GRPCLogger { internal func notice( _ message: @autoclosure () -> Logger.Message, metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #file, + file: String = #fileID, function: String = #function, line: UInt = #line ) { @@ -100,7 +100,7 @@ internal struct GRPCLogger { internal func warning( _ message: @autoclosure () -> Logger.Message, metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #file, + file: String = #fileID, function: String = #function, line: UInt = #line ) { diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift index cbe01cba2..6f9b1a163 100644 --- a/Sources/GRPC/PlatformSupport.swift +++ b/Sources/GRPC/PlatformSupport.swift @@ -416,7 +416,7 @@ extension EventLoopGroup { internal func preconditionCompatible( with tlsConfiguration: GRPCTLSConfiguration, - file: StaticString = #file, + file: StaticString = #fileID, line: UInt = #line ) { precondition( diff --git a/Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift b/Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift index e1259750a..181f9b0f4 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift @@ -28,7 +28,7 @@ public struct AssertionError: Error { public func assertEqual( _ value1: T, _ value2: T, - file: StaticString = #file, + file: StaticString = #fileID, line: UInt = #line ) throws { guard value1 == value2 else { @@ -43,7 +43,7 @@ public func assertEqual( public func waitAndAssertEqual( _ future: EventLoopFuture, _ value: T, - file: StaticString = #file, + file: StaticString = #fileID, line: UInt = #line ) throws { try assertEqual(try future.wait(), value, file: file, line: line) @@ -56,7 +56,7 @@ public func waitAndAssertEqual( public func waitAndAssertEqual( _ future1: EventLoopFuture, _ future2: EventLoopFuture, - file: StaticString = #file, + file: StaticString = #fileID, line: UInt = #line ) throws { try assertEqual(try future1.wait(), try future2.wait(), file: file, line: line) @@ -64,7 +64,7 @@ public func waitAndAssertEqual( public func waitAndAssert( _ future: EventLoopFuture, - file: StaticString = #file, + file: StaticString = #fileID, line: UInt = #line, message: String = "", verify: (T) -> Bool diff --git a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift index 2b24265b0..5041830a6 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift @@ -20,7 +20,7 @@ import XCTest internal func XCTAssertThrowsError( _ expression: @autoclosure () async throws -> T, verify: (Error) -> Void = { _ in }, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async { do { diff --git a/Tests/GRPCTests/ClientTransportTests.swift b/Tests/GRPCTests/ClientTransportTests.swift index 856e69c30..504fac463 100644 --- a/Tests/GRPCTests/ClientTransportTests.swift +++ b/Tests/GRPCTests/ClientTransportTests.swift @@ -80,7 +80,7 @@ class ClientTransportTests: GRPCTestCase { self.transport.configure(body) } - private func connect(file: StaticString = #file, line: UInt = #line) throws { + private func connect(file: StaticString = #filePath, line: UInt = #line) throws { let address = try assertNoThrow(SocketAddress(unixDomainSocketPath: "/whatever")) assertThat( try self.channel.connect(to: address).wait(), @@ -103,7 +103,7 @@ class ClientTransportTests: GRPCTestCase { private func sendResponse( _ part: _GRPCClientResponsePart, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws { assertThat(try self.channel.writeInbound(part), .doesNotThrow(), file: file, line: line) diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index ec1358c7e..c671531ca 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -66,7 +66,7 @@ class ConnectionManagerTests: GRPCTestCase { from: ConnectivityState, to: ConnectivityState, timeout: DispatchTimeInterval = .seconds(1), - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, body: () throws -> Result ) rethrows -> Result { @@ -81,7 +81,7 @@ class ConnectionManagerTests: GRPCTestCase { private func waitForStateChanges( _ changes: [Change], timeout: DispatchTimeInterval = .seconds(1), - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, body: () throws -> Result ) rethrows -> Result { @@ -1248,7 +1248,7 @@ internal class RecordingConnectivityDelegate: ConnectivityStateDelegate { func waitForExpectedChanges( timeout: DispatchTimeInterval, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { let result = self.semaphore.wait(timeout: .now() + timeout) diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index df39c1899..08cf46a9b 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -1026,7 +1026,7 @@ internal final class ChannelController { private func isValidIndex( _ index: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) -> Bool { let isValid = self.channels.indices.contains(index) @@ -1036,7 +1036,7 @@ internal final class ChannelController { internal func connectChannel( atIndex index: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } @@ -1050,7 +1050,7 @@ internal final class ChannelController { internal func fireChannelInactiveForChannel( atIndex index: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } @@ -1060,7 +1060,7 @@ internal final class ChannelController { internal func throwError( _ error: Error, inChannelAtIndex index: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } @@ -1070,7 +1070,7 @@ internal final class ChannelController { internal func sendSettingsToChannel( atIndex index: Int, maxConcurrentStreams: Int = 100, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } @@ -1083,7 +1083,7 @@ internal final class ChannelController { internal func sendGoAwayToChannel( atIndex index: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } @@ -1098,7 +1098,7 @@ internal final class ChannelController { internal func openStreamInChannel( atIndex index: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } @@ -1115,7 +1115,7 @@ internal final class ChannelController { internal func closeStreamInChannel( atIndex index: Int, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift index 5610b9655..46fbaf337 100644 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift @@ -270,7 +270,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { extension Result { internal func assertSuccess( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (Success) -> Void = { _ in } ) { @@ -282,7 +282,7 @@ extension Result { } internal func assertFailure( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (Failure) -> Void = { _ in } ) { @@ -296,7 +296,7 @@ extension Result { extension PoolManagerStateMachine.ShutdownAction { internal func assertShutdownPools( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { if case .shutdownPools = self { @@ -307,7 +307,7 @@ extension PoolManagerStateMachine.ShutdownAction { } internal func assertAlreadyShuttingDown( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (EventLoopFuture) -> Void = { _ in } ) { @@ -318,7 +318,7 @@ extension PoolManagerStateMachine.ShutdownAction { } } - internal func assertAlreadyShutdown(file: StaticString = #file, line: UInt = #line) { + internal func assertAlreadyShutdown(file: StaticString = #filePath, line: UInt = #line) { if case .alreadyShutdown = self { () } else { diff --git a/Tests/GRPCTests/EventLoopFuture+Assertions.swift b/Tests/GRPCTests/EventLoopFuture+Assertions.swift index 2ead3a53c..4678b4ac8 100644 --- a/Tests/GRPCTests/EventLoopFuture+Assertions.swift +++ b/Tests/GRPCTests/EventLoopFuture+Assertions.swift @@ -27,7 +27,7 @@ extension EventLoopFuture where Value: Equatable { func assertEqual( _ expected: Value, fulfill expectation: XCTestExpectation, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { self.whenComplete { result in @@ -59,7 +59,7 @@ extension EventLoopFuture { /// - handler: A block to run additional verification on the error. Defaults to no-op. func assertError( fulfill expectation: XCTestExpectation, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, handler: @escaping (Error) -> Void = { _ in } ) { @@ -84,7 +84,7 @@ extension EventLoopFuture { /// - Parameter expectation: The expectation to fulfill. func assertSuccess( fulfill expectation: XCTestExpectation, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { self.whenSuccess { _ in diff --git a/Tests/GRPCTests/FunctionalTests.swift b/Tests/GRPCTests/FunctionalTests.swift index 219034629..29d313565 100644 --- a/Tests/GRPCTests/FunctionalTests.swift +++ b/Tests/GRPCTests/FunctionalTests.swift @@ -40,7 +40,7 @@ class FunctionalTestsInsecureTransport: EchoTestCaseBase { func doTestUnary( request: Echo_EchoRequest, expect response: Echo_EchoResponse, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { let responseExpectation = self.makeResponseExpectation() @@ -53,7 +53,7 @@ class FunctionalTestsInsecureTransport: EchoTestCaseBase { self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout) } - func doTestUnary(message: String, file: StaticString = #file, line: UInt = #line) { + func doTestUnary(message: String, file: StaticString = #filePath, line: UInt = #line) { self.doTestUnary( request: Echo_EchoRequest(text: message), expect: Echo_EchoResponse(text: "Swift echo get: \(message)"), @@ -130,7 +130,7 @@ class FunctionalTestsInsecureTransport: EchoTestCaseBase { func doTestClientStreaming( messages: [String], - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws { let responseExpectation = self.makeResponseExpectation() diff --git a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift index 98a2653e5..e75d55a2f 100644 --- a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift +++ b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift @@ -37,7 +37,7 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { private var group: MultiThreadedEventLoopGroup! private let queue = DispatchQueue(label: "io.grpc.verify-handshake") - private static let p12bundleURL = URL(fileURLWithPath: #file) + private static let p12bundleURL = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() // (this file) .deletingLastPathComponent() // GRPCTests .deletingLastPathComponent() // Tests diff --git a/Tests/GRPCTests/GRPCTestCase.swift b/Tests/GRPCTests/GRPCTestCase.swift index c409bd765..8ec6d320d 100644 --- a/Tests/GRPCTests/GRPCTestCase.swift +++ b/Tests/GRPCTests/GRPCTestCase.swift @@ -38,7 +38,7 @@ class GRPCTestCase: XCTestCase { override func tearDown() { let logs = self.capturedLogs() - // The default source emitted by swift-log is the directory containing the '#file' in which the + // The default source emitted by swift-log is the directory containing the '#filePath' in which the // log was emitted. It's meant to represent the system which emitted the log, typically the // module name. In most cases it's right but in a few places, i.e. where the source lives in // child directories below 'GRPC', it isn't. We'll use this as a sanity check. diff --git a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift index 5d4a7ade8..76747880a 100644 --- a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift +++ b/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift @@ -516,7 +516,7 @@ final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase { extension GRPCWebToHTTP2ServerCodec.StateMachine.Action { func assertRead( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (HTTP2Frame.FramePayload) -> Void = { _ in } ) { @@ -528,7 +528,7 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.Action { } func assertWrite( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (Write) -> Void = { _ in } ) { @@ -540,7 +540,7 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.Action { } func assertCompletePromise( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (Error?) -> Void = { _ in } ) { @@ -557,7 +557,7 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.Action { } func assertNone( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { if case .none = self { @@ -570,7 +570,7 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.Action { extension HTTP2Frame.FramePayload { func assertHeaders( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (Headers) -> Void = { _ in } ) { @@ -582,7 +582,7 @@ extension HTTP2Frame.FramePayload { } func assertData( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (Data) -> Void = { _ in } ) { @@ -594,7 +594,7 @@ extension HTTP2Frame.FramePayload { } func assertEmptyDataWithEndStream( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { self.assertData(file: file, line: line) { @@ -608,7 +608,7 @@ extension HTTP2Frame.FramePayload { extension HTTPServerResponsePart { func assertHead( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (HTTPResponseHead) -> Void = { _ in } ) { @@ -620,7 +620,7 @@ extension HTTPServerResponsePart { } func assertBody( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (ByteBuffer) -> Void = { _ in } ) { @@ -632,7 +632,7 @@ extension HTTPServerResponsePart { } func assertEnd( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (HTTPHeaders?) -> Void = { _ in } ) { @@ -646,7 +646,7 @@ extension HTTPServerResponsePart { extension IOData { func assertByteBuffer( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (ByteBuffer) -> Void = { _ in } ) { @@ -660,7 +660,7 @@ extension IOData { extension Optional { func assertSome( - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, verify: (Wrapped) -> Void = { _ in } ) { diff --git a/Tests/GRPCTests/HeaderNormalizationTests.swift b/Tests/GRPCTests/HeaderNormalizationTests.swift index 95ab64dee..42f2a8a10 100644 --- a/Tests/GRPCTests/HeaderNormalizationTests.swift +++ b/Tests/GRPCTests/HeaderNormalizationTests.swift @@ -119,7 +119,7 @@ class HeaderNormalizationTests: GRPCTestCase { private func assertCustomMetadataIsLowercased( _ headers: EventLoopFuture, expectation: XCTestExpectation, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { // Header lookup is case-insensitive so we need to pull out the values we know the server sent diff --git a/Tests/GRPCTests/SampleCertificate+Assertions.swift b/Tests/GRPCTests/SampleCertificate+Assertions.swift index 0cdca3520..86e7938cc 100644 --- a/Tests/GRPCTests/SampleCertificate+Assertions.swift +++ b/Tests/GRPCTests/SampleCertificate+Assertions.swift @@ -19,7 +19,7 @@ import GRPCSampleData import XCTest extension SampleCertificate { - func assertNotExpired(file: StaticString = #file, line: UInt = #line) { + func assertNotExpired(file: StaticString = #filePath, line: UInt = #line) { XCTAssertFalse( self.isExpired, "Certificate expired at \(self.notAfter)", diff --git a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift b/Tests/GRPCTests/ServerFuzzingRegressionTests.swift index 0bb852259..be97565eb 100644 --- a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift +++ b/Tests/GRPCTests/ServerFuzzingRegressionTests.swift @@ -22,7 +22,7 @@ import NIOEmbedded import XCTest final class ServerFuzzingRegressionTests: GRPCTestCase { - private static let failCasesURL = URL(fileURLWithPath: #file) + private static let failCasesURL = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() // ServerFuzzingRegressionTests.swift .deletingLastPathComponent() // GRPCTests .deletingLastPathComponent() // Tests diff --git a/Tests/GRPCTests/XCTestHelpers.swift b/Tests/GRPCTests/XCTestHelpers.swift index 156bfac33..d0ea66481 100644 --- a/Tests/GRPCTests/XCTestHelpers.swift +++ b/Tests/GRPCTests/XCTestHelpers.swift @@ -26,7 +26,7 @@ struct UnwrapError: Error {} func assertNotNil( _ expression: @autoclosure () throws -> Value?, message: @autoclosure () -> String = "Optional value was nil", - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> Value { guard let value = try expression() else { @@ -39,7 +39,7 @@ func assertNotNil( func assertNoThrow( _ expression: @autoclosure () throws -> Value, message: @autoclosure () -> String = "Unexpected error thrown", - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> Value { do { @@ -59,7 +59,7 @@ func assertNoThrow( func assertThat( _ expression: @autoclosure @escaping () throws -> Value, _ matcher: Matcher, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { // For value matchers we'll assert that we don't throw by default. @@ -69,7 +69,7 @@ func assertThat( func assertThat( _ expression: @autoclosure @escaping () throws -> Value, _ matcher: ExpressionMatcher, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { switch matcher.evaluate(expression) { @@ -666,7 +666,7 @@ struct ExpressionMatcher { func assertThat( _ expression: @autoclosure @escaping () async throws -> Value, _ matcher: Matcher, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async { // For value matchers we'll assert that we don't throw by default. @@ -677,7 +677,7 @@ func assertThat( func assertThat( _ expression: @autoclosure @escaping () async throws -> Value, _ matcher: ExpressionMatcher, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) async { // Create a shim here from async-await world... From 9df1f25b534d52b39e4a36bbdeef6688114c2563 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 14 Nov 2022 11:28:22 +0000 Subject: [PATCH 044/580] Update test certs (#1522) Motivation: Test certs expired; tests now fail. Modifications: - Add a script to generate a Swift source file which uses the output of the existing `makecert` script. - Fix a warning. Results: Tests pass. --- .swiftformat | 1 + Sources/GRPC/Serialization.swift | 1 + .../GRPCSampleData/GRPCSwiftCertificate.swift | 414 +++++++++--------- scripts/make-sample-certs.py | 233 ++++++++++ 4 files changed, 438 insertions(+), 211 deletions(-) create mode 100755 scripts/make-sample-certs.py diff --git a/.swiftformat b/.swiftformat index 94bb95c88..83b402ee9 100644 --- a/.swiftformat +++ b/.swiftformat @@ -3,6 +3,7 @@ # Ignore generated files. --exclude **/LinuxMain.swift,**/XCTestManifests.swift,**/*.grpc.swift,**/*.pb.swift +--exclude Sources/GRPCSampleData/GRPCSwiftCertificate.swift --indent 2 --maxwidth 100 diff --git a/Sources/GRPC/Serialization.swift b/Sources/GRPC/Serialization.swift index 4d851270c..641ac86f9 100644 --- a/Sources/GRPC/Serialization.swift +++ b/Sources/GRPC/Serialization.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import struct Foundation.Data import NIOCore import NIOFoundationCompat import SwiftProtobuf diff --git a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift index fe1dcc77e..3294ae96f 100644 --- a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift +++ b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift @@ -1,5 +1,5 @@ /* - * Copyright 2019, gRPC Authors All rights reserved. + * Copyright 2022, 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. @@ -13,6 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +//----------------------------------------------------------------------------- +// THIS FILE WAS GENERATED WITH make-sample-certs.py +// +// DO NOT UPDATE MANUALLY +//----------------------------------------------------------------------------- + #if canImport(NIOSSL) import struct Foundation.Date import NIOSSL @@ -26,57 +33,49 @@ public struct SampleCertificate { public static let ca = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), commonName: "some-ca", - // Not After : Nov 12 13:06:40 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_400.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) public static let otherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), commonName: "some-other-ca", - // Not After : Nov 12 13:06:41 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_401.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) public static let server = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), commonName: "localhost", - // Not After : Nov 12 13:06:41 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_401.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) public static let exampleServer = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), commonName: "example.com", - // Not After : Nov 12 13:06:41 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_401.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) public static let serverSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - // Not After : Nov 12 13:06:41 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_401.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) public static let client = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), commonName: "localhost", - // Not After : Nov 12 13:06:41 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_401.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) public static let clientSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - // Not After : Nov 12 13:06:41 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_401.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) public static let exampleServerWithExplicitCurve = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), commonName: "localhost", - // Not After : Nov 12 13:06:41 2022 GMT - notAfter: Date(timeIntervalSince1970: 1_668_258_401.0) + notAfter: Date(timeIntervalSince1970: 1699960773) ) } @@ -105,267 +104,260 @@ public struct SamplePrivateKey { // MARK: - Certificates and private keys -// NOTE: use the "makecert" script in the scripts directory to generate new -// certificates and private keys when these expire. - private let caCert = """ -----BEGIN CERTIFICATE----- -MIIC9zCCAd+gAwIBAgIJAMfc2gvFlVceMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV -BAMMB3NvbWUtY2EwHhcNMjExMTEyMTMwNjQwWhcNMjIxMTEyMTMwNjQwWjASMRAw -DgYDVQQDDAdzb21lLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -8Ko4QgNV2oiM2h/zQcgyQHF4eFpgU8h3XQuev7Hwa8ZkMFOgVDQANZJEhURNtLV0 -rFjft23uMet1tXrEnFBW2Lj05gIdkbsrZXVSyhRAZ+LKUyY5hCvXSy66JXQpkbQi -UhpKyqni3GhIMTyYb7HMo8dA/0XNWl+I+DrsivBXiE2hD2Fq/0b+G4gOxGHujYRJ -WGMm8uHA/B67sPej9V6yn7OKsqb9OEI/VE5IsfHumAR6HNV4KBHnlIqR+REzXcT1 -eqra8koM9A7un1SRkMoU4HyVdk4VAdXzAxZ6IiFtXzmQR4uwfVpQfiQ1bZEpVKFV -kZusdmmoTvgfU1MQNZLj9QIDAQABo1AwTjAdBgNVHQ4EFgQU0qP3HeQn4dJyjUwN -EKLs4fZY/8QwHwYDVR0jBBgwFoAU0qP3HeQn4dJyjUwNEKLs4fZY/8QwDAYDVR0T -BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAlPbE2MXzO06GcRuTj8z6P7FMRSEV -UrUvCkhHzSkJPYA2t3gsShbksNrj815LxQGu66QtuwqkL9Ey/K5pO/8XH00oR58H -QkDcPuAoVac/8ezEc2z1aJ6FzvAwiKBJDkS6q3EYllGmLHFRBFbg0oewtppHZuv5 -6dVdra/4XH3KNMdSsdv8rKc/mAG34eRsT5UPNTuW0CBm2whfob4nq3sVwedh4/IU -aWeKsutFYsrVC/ppA1H3ZUS/L9bEcpj3CmEdjRtX1wXN6yC2WesjwFOvYBZ9ENWL -6p6Dk6yoQWwmoM9Y72MoWC5PMHc/4zkHNl4g6Fcbhv82prR5e2AzOaWB5w== +MIICoDCCAYgCCQCf2sGvEIOvlDANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz +b21lLWNhMB4XDTIyMTExNDExMTkzM1oXDTIzMTExNDExMTkzM1owEjEQMA4GA1UE +AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOjCRRWS +ezh/izFLT4g8lAjVUGbjckCnJRPfwgWDAC1adTDi4QhzONVlmYUzUkx6VPCcVVou +vc/WM8hHC6cRDdf/ubGRZondGw6bzaO2yqc8K6BNSvqFnkuQHRpPoSc/RKHe+qTT +glhygm3GlAUaNl0hJpXWlLqOoIb0mn8emF7afbyyWariPPQyzY2rywPLPXipitmW +Jw7GxVC+Q2yx5GQxPvutCdtkAsrS1AsYxpvpW+kHmtj0Dj40N7yhTz1cw2QtCD2i +CQuk9oRwtIiJi54USy/r6oq5NOlwqHyq+DGDt5XZx1RKvGJTn3ujHPEJVipoTkdX +/K+RpqQxJNGhyO8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAjKgbr1hwRMUwEYLe +AAf0ODw0iRfch9QG6qYTOFjuC3KyDKdzf52OGm49Ep4gVmgMPLcA3YGtUXd8dJSf +WYZ0pSJYeLVJiEkkV/0gwpc+vK2OxZe1XBPbW3JN+wmRBxi3MiL7bbSvDlYIj7Dv +8c1vs8SxE4VuSkFRrcrVV0V95xs01X1/M9aa8z9Lf59ecKytaLvKysorDrCN8nC3 +zlMDehPCLH9y4UlBqp8ClUpKk/5/P8HXr40ZGq+5TFrR80YABPSVX7krRMcxIhfu +IFIT2yhjkxMQWj8SCDUZxamBElAXY9KHSlEv3y+teRQABBVNxslHXqLKfKTF3q4S +tUVJuA== -----END CERTIFICATE----- """ private let otherCACert = """ -----BEGIN CERTIFICATE----- -MIIDAzCCAeugAwIBAgIJAKXWyMK52taJMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV -BAMMDXNvbWUtb3RoZXItY2EwHhcNMjExMTEyMTMwNjQxWhcNMjIxMTEyMTMwNjQx -WjAYMRYwFAYDVQQDDA1zb21lLW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEAnwzOvLDX6wsyZRbX8LaVGgtmVEEft2BWF8V+/2gHo428g1ba -YyHvMS8CJTOneIKB6HYlBEamB+wjnCFvWMz0eynzaT0HVJwhK5qhRYNZDr4ZtGFx -ov0Xau+rS/YW7NMNKLgLDgHYLMBDWLnyfDy+VWBUlhwV1lzlk9xeZdHvFodNbjpC -Mrw0gukfoFXvdvOqQSP4J/9TITv3jCY4gs4wZMFY/+XzpxQNEP5deEJGsH13PdXV -hHxx11VP+ippkkcx7d11UP8UR9Y/RJM319tgOl91H7hTfLE/dn+N18yFGdW7fGW8 -+MGV9tnk8K9JavnGOL9pYfjCMuZav3dMd1ObaQIDAQABo1AwTjAdBgNVHQ4EFgQU -GJjjNxqJOPKpPAiKVIYT1TRhJYEwHwYDVR0jBBgwFoAUGJjjNxqJOPKpPAiKVIYT -1TRhJYEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAA+zbg+pxa3lI -W1lZcBb6fEiaK9sazyJfZ0vXBQmX3GuCQnWhTHmccvYQVAxqzQAwTfTEXfIu8nPr -76j60zr/rTjDHsc0i/xMoaCAsWX7h/UcMYOsfgbKbFR7kvbf5n/2RdKbIbd4A2Og -2sD8k7gqhBBsDgGbNsrIgzKoQYSrJOjTxTFlDAkG6gypRKqSzgiUvh+6wP1h4Jj7 -GpS82LHl1x3oXH/RJR3mWBy61VMbOHDc54lmbezs43WLOvnfimAvr7LfsfYSmp3O -AlNpK9RvHP1Xfjx43l+Bb2EEJWAf/eBjWGpCcWz3EAl8k9W+lousBh9/wacdeLCV -lNtTzgbeYA== +MIICrDCCAZQCCQDQxWAzi9Y9LTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z +b21lLW90aGVyLWNhMB4XDTIyMTExNDExMTkzM1oXDTIzMTExNDExMTkzM1owGDEW +MBQGA1UEAwwNc29tZS1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANOr3vaYLkfnqk0lREo0VJD/rnUGQ6BiVtKiou0uksb9gX4oHdKlnqyi +dvFuwaJHIzjBhdWD2EqgWwuBTB3y/UybD/ZvkojnLD+QNMnbgG5aCnO03gVlVBOf +JggEtAEM31C7Fi6X7Gr/QwRI721+kqNSB48Rj3BT93cDW73aSeL6IZ8jlvefWYR7 +1UI3bP+4WG58PSJOhUs2edaOn0G5wRZ5LyK6A77noll90cP+CVNlqLj8HRapqhf1 +XZhGwwaEYxNV1oDroxq9mcM+6E8LdWCsdE3N4Dx6pdL0lOjwvhevZ2ct/fb31NYE +fMstojwKf9Of5/J4kZaC1mp44IwPS00CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA +iUuX1YYdVwqamg13eji1I9/8eMP5demBnXjM7DYP3JqDhClTYNnN8aB+o1YW51ce +3V1FtN/f3g3YMgYB4YSOb241G9uXGCz5CwcYeBCJbUT/PdNZOrTW1EzA+gAy8GxS +yMbK+ZrXy+7mJr79sumIO2WGk//eznvgrmlKq+eZtVf/TDTYs5TdbqI4sqoN+qPQ +WyuBXEkU2D259VvZ+GLljVr7JCysciALKDk3QAb6cfjhFh3aOqb40m5i4Jg6g2G6 +iFS1kE3KjaWhYYn66BRVOYzfT25RkFBxxJh2Pg6DQOyVUWsWJ+VrstpQlcGMElmq +/LaIwNYfuUNcKb90L+M6vg== -----END CERTIFICATE----- """ private let serverCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMMCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZbykEoZj1mdTRW -ofPPmEmBNlPJcepamcKtBqsqjv4OBbWVFgMIlmNgRef7ZWlz/YHE3cu7dISYEZ2/ -C5zKgNGPiJFtiPC+GHKGPAbAHoxQa1kT204r5Hxpjr5iKAPVrHKilJO3wpF8Ins9 -G6AcR63lyppO/bpW6RxcF4fivcjiVvcq+TtBRrA3JROduQjjD6rYXuRHcrIq9Hc6 -CnUxQ1t1DCU7xqUGV8mdpJlI41NGqz0zoxFw/OKT2/3MR7mHGzAD/vHjIgKE0B5T -9xPa75ur7Gi70T5jT/3jwFaVKCt4bQCIAREuUEtzbwxGYoXDkSodUS/KLfbycCgy -u0wxOsECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAONUYolaHs2I3CVB9rCspr25I -jxQfW2xzGuLsm91l9GxvtDS89wEu15zhFDsQPR6u9br6qENfMVDiOa36wCmtvjxJ -2vYzWiWBVoSEBvwIkWMg/qlmdNB2lRxj/WgjcGNtUJwbe5Ex9ldquspoQNfEvwjz -KeKnDCJ8oit6IJvkdG0crowReX9w3fOFiARp87J/NoQlmm1hiY3FD0nQ3wvBhtby -0svSQSwG244B9TlzLBwEzRvC8+qLIP1LSjBXg3nXZCfkV6or/B2+gOl9uxB76O2h -KYJqo2GeZKYQHF5Lco7DYfzJcL7IeDNV2yeLk/3i7e6OoLcMr8G4aJ645em+WQ== +Fw0yMjExMTQxMTE5MzNaFw0yMzExMTQxMTE5MzNaMBQxEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANEQLRL2oOHPHN7K +ozmP8Ks8CcndChJrunNTJAWCiMA/HFEVVfylnQpPyc/t+W2yD+d+PsJknu2hI1O5 +o53SBsEDm1taHaCJt9ur5NKpEphzOI7VuwkgcoGqmY0Hz7GmBmbG06Z6ne8EZfg5 +a/rjxhW3GyOmIT3s9xWiU3MW7VX0PDlVmkZzVYtcSp9+AXQMDpvLK48INu1mUC6u +1nbEzj6KuFwpU5+V1cRLHer+I9HVA7qBcgsIDDEdUDG0/l0MivAyDbNHGHDZcsfj +jwTMsGRcd+IONItHyYb72+JBEKv3/qFAe4XIeR6iJQP4OxZ4CoxeUFgkVQwqBNd+ +1JYDuvECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEASoyiLe/ak0nH5Bl7RvwAsO+Y +J2kA/oIUCpFEmsqDxK9Nt4eGIvknGzWsfTsVM4jXQo1MYCE7XrjpG9H6fubeQvxG +b+eU3VaHztflxLouzBIM6LnzoXnt2/rjQWIMbWZri2Gwl4incvVDLOv3jm5VD1Uw +OePLd+DvD/NzQ4nWdqCqhZAjopEPUpOT7fP8OkJVjGddvAn/0KyXkg3tutmUMB9m +8KctofAp1fKmd056Lgj+j6DIFDxxWEiihTO1ae8FlS4X/teeGSEVGv5M4baWRrcD +29V9XNIbMiwCNa7DJlPpxkjHdT4KifwPDHJ92RfK54SU1k0i8LD9KByuV4av9w== -----END CERTIFICATE----- """ private let serverSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl -ci1jYTAeFw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMM -CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZbykEo -Zj1mdTRWofPPmEmBNlPJcepamcKtBqsqjv4OBbWVFgMIlmNgRef7ZWlz/YHE3cu7 -dISYEZ2/C5zKgNGPiJFtiPC+GHKGPAbAHoxQa1kT204r5Hxpjr5iKAPVrHKilJO3 -wpF8Ins9G6AcR63lyppO/bpW6RxcF4fivcjiVvcq+TtBRrA3JROduQjjD6rYXuRH -crIq9Hc6CnUxQ1t1DCU7xqUGV8mdpJlI41NGqz0zoxFw/OKT2/3MR7mHGzAD/vHj -IgKE0B5T9xPa75ur7Gi70T5jT/3jwFaVKCt4bQCIAREuUEtzbwxGYoXDkSodUS/K -LfbycCgyu0wxOsECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAb195ecB8D8P20X3j -Sc1dHF5s5845aTOB+wYeFVFKVWFbRJwx7x2qpUXv4KqYrHNNruQKukmFTIA1pBHC -Ejdr5PDudUoxLwZE43PrpjxqhdV8bgXogB13xTEJwCkpjj3b9BNsiL67n4B3BAzy -aXOiZJ7tPYmB9Fpxom4W6Iq0uc0n1UShbxZerAuBet0pYkmsoMPVupnIH8TqZbIL -6Jht6iodCjf+WP7hgK4nXEXVd0SFj9mjpWTaz/PPfv/hTd53K1kZE13VrEifi3C7 -HrCPXcACcUohrXZJW1764yuODuQKpleBjBt+QvlhO54pBBXdP3F+h/FirQIrymA7 -BBG4rg== +ci1jYTAeFw0yMjExMTQxMTE5MzNaFw0yMzExMTQxMTE5MzNaMBQxEjAQBgNVBAMM +CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANEQLRL2 +oOHPHN7KozmP8Ks8CcndChJrunNTJAWCiMA/HFEVVfylnQpPyc/t+W2yD+d+PsJk +nu2hI1O5o53SBsEDm1taHaCJt9ur5NKpEphzOI7VuwkgcoGqmY0Hz7GmBmbG06Z6 +ne8EZfg5a/rjxhW3GyOmIT3s9xWiU3MW7VX0PDlVmkZzVYtcSp9+AXQMDpvLK48I +Nu1mUC6u1nbEzj6KuFwpU5+V1cRLHer+I9HVA7qBcgsIDDEdUDG0/l0MivAyDbNH +GHDZcsfjjwTMsGRcd+IONItHyYb72+JBEKv3/qFAe4XIeR6iJQP4OxZ4CoxeUFgk +VQwqBNd+1JYDuvECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZN5RQsfPP09YIfYo +UGu9m5+lpzYhE0S2+szysTg2IpWug0ZK4xhnqQYd9cGRks+U6hiLPdiyHCwOykf6 +OplIp5fMxPWZipREb9nA33Ra1G9vpB/tZxQJxDTvUeCH88SQOszdZk79+zWyVkaF ++TCa3jDXb/vT20+wKxpPUjse5w2j0VOh21KaP82EMyOY/ZvhbMC60QyHnFDvJAEV +sle77vbdLjYELpYUpf9N+TxFDZ2B4dY/edprLZGt3LcUUFv/WB8FxZdWcjdZML2F +TMqicbP7H27+V1HF1rFUJWKzDNh4Wg6bY6lQNTZeHUyLwf/WlUraXTKYpqSH8FQ1 +703RGQ== -----END CERTIFICATE----- """ private let serverKey = """ -----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAplvKQShmPWZ1NFah88+YSYE2U8lx6lqZwq0GqyqO/g4FtZUW -AwiWY2BF5/tlaXP9gcTdy7t0hJgRnb8LnMqA0Y+IkW2I8L4YcoY8BsAejFBrWRPb -TivkfGmOvmIoA9WscqKUk7fCkXwiez0boBxHreXKmk79ulbpHFwXh+K9yOJW9yr5 -O0FGsDclE525COMPqthe5Edysir0dzoKdTFDW3UMJTvGpQZXyZ2kmUjjU0arPTOj -EXD84pPb/cxHuYcbMAP+8eMiAoTQHlP3E9rvm6vsaLvRPmNP/ePAVpUoK3htAIgB -ES5QS3NvDEZihcORKh1RL8ot9vJwKDK7TDE6wQIDAQABAoIBABWH17trISBdPFoT -xE4r1gfdY0ygy8+K/k+F2VEZ5vvWkMKZkwm9eMlP0nxduxhU3MCI3DPcBQ6MJ+uE -qFoYk2eL7h70UD7oO33HBcnR36JFXj9fJIkPgTjg6IqXZZppczI6/IPJyrLNoCDX -HdYxEs3c6cXi50/Qo8b53EnH/Mwc2bS8HSAkL7sYd5+AKjfe2XlIt395nA6Vbpex -SRnGmWnfkyo/PdHyNd4WNjhDn4zW3rlJtFO6z/Rf7DZgU+utETf4CEDH3Bfm0hqV -bAl8zyuytpGWv/2e0eyvQ6dEQZLNI2RAuid5M+FjT5u5fBOpggaqGe1L7SfI5KiO -E1SiQAECgYEA1d5g2if0ZOLg3dG+tzYXscss62o5dhW4ULa5VzVZ9B0AX+CPBJCj -2uSpVbhibrCljzgoYdL4+sZw6955pieXBFrPMgzfR4sjfK5z9O9GerMFecUr3+i4 -XLgYCzrmoSytNem64mV/H5WauR6Ob0UlDYrLDWfer1OMHClplDsVdoECgYEAxyFt -SYDCabN+4Yvh+TC/X80h65f2GNrwZqFrXjnfyOv4SyY9U2XvMy6YTjM9gTy5RJQ2 -XaG7Upk2auRU6n0VL311/GA2eL7iBRqJL/3NHibT5KfeXfoiAAYyjlbHrGN1QJWe -3o8XdPaUh9ccGT8fZhl+Eg9VeorUmSCwC4UUJEECgYA8/CyiCMKoAgodNrIrjEE1 -cbpdZuz7vzXPzksLkysTcTGqJV6i7pvKz2l6CBoJdlW/gUQCoSZeXDfXCpmlx6RI -mZx7qTACNqrn4tcuAQ0X7/SfxJm+P55S0iwJB8K8MwExXnTsGgUl/IMiRpRXJmBq -fClqqTPWyvwpC6YPnsmAAQKBgHLt3wa6Uvrwxz1kH9NUCFBBs98nALnNu0xww+hJ -XNi5IMA23NRCk/ElZnBT8J6jroZfSJV34AbHOPouuLfx44VaUvuLiETeXtL1QtK5 -GGbboBZrsNLqqC79ZLZ0baAYczcIY/4t9irimk1goO4NWZDzC6lewkYM1LFghVrQ -vxRBAoGAEDEh/IUmsYjyPVVDm+nV4/6ODtnhqBpBpAGo0JL3FJ7XEOR3byH4DXQG -Rdz6haHMfQQqBfiPOk9HZ+Y2WO+PHJ3OrM6wyzZUXKZr7r4im5hkisITVbU/cIdl -FqOsrd5SuS98XIo05JKlWbPFLgzgYtnx2zLiU8ATyJHIF6LrbwY= +MIIEpQIBAAKCAQEA0RAtEvag4c8c3sqjOY/wqzwJyd0KEmu6c1MkBYKIwD8cURVV +/KWdCk/Jz+35bbIP534+wmSe7aEjU7mjndIGwQObW1odoIm326vk0qkSmHM4jtW7 +CSBygaqZjQfPsaYGZsbTpnqd7wRl+Dlr+uPGFbcbI6YhPez3FaJTcxbtVfQ8OVWa +RnNVi1xKn34BdAwOm8srjwg27WZQLq7WdsTOPoq4XClTn5XVxEsd6v4j0dUDuoFy +CwgMMR1QMbT+XQyK8DINs0cYcNlyx+OPBMywZFx34g40i0fJhvvb4kEQq/f+oUB7 +hch5HqIlA/g7FngKjF5QWCRVDCoE137UlgO68QIDAQABAoIBAEumodjh2/e6PYU1 +KHl0567e69/bF4Dw8Kg4pqlDwf5nF/UTVmk0+K25j5qpT3/tVin7mfQ3+vacP69V +VqqOTJldl8Mnyd7E1v4rpoLAYZU+5HFzT9oOnsDjHetVr0dmf5yDSCVO64WJPuji +xnskHxLOjoiI3jCNZh+y/KWB32IhdofSwBccw852JM2qC5l8vgE+sfjOeWXDiPRI +YLlVRlxZFv7N2kn+EDnPEQ8m2OGYKvNzU0d9nz05NdkRXzMh9zegTTL4EQhTaMf0 +2AXy2ekKFVvWouV4y8QW1shz5Tun2y4ZQJnwiCyldED9sMxaziQxfdJ6N7f4+K5c +Sh4+Ct0CgYEA7bGvY02jQfHcDPOjZ/xkXb98lr1uLGTSvwK3zw+rI0+nrUJwH6fB +nSaXyWk059OqHTKPpa/d8DxFL2LP6vbvfTWCv9mnn61WWWDmP0Eo/k93XgWkmclb +vQGfXV2wtCTnhz+iUSSJA8f8jZhCtOD6xa8pLsaYrGD6oR5wzfs0nysCgYEA4SoC +/JWDMkw4mndI2vQ8GqDzBFtJCMr/dva7YGCGtbimDzxuOI68vW/y4X8Izg2i1hVz +iKRYCI9KzRdQrQ7masZF2d4DeaqPeA72JN4hoUOw0TZjP8yD4ECifUt786ZNvV1n +NlEhNb55zD73Cl6v0OJZkEfp1MC5ZQwLw7bMYFMCgYEAvNu5Z0WAuhzZotDSvQSl +GnfTHlJU/6D8chhOw47Hg77+k4N+Yyh/hcXsRHP7PVfIinpp+FPMG91He2cfnKmn +j+y8foMJ1K19NnbvesLjN20cgvAo4KhE4+AuJ5kRlZDdBXFiHubQltiHqlmYZu97 +USbjqe7Rz+UePnZZWtCF9xECgYEAjGODZTVbjdrUWAsT0+EAMKI1o3u/N8pKKkSA +ZAELPPaaI1nMZ1sn9v179HkeZks+QjkxxfqiIQQm4WUuGhj2NZDWMJcql4tu1K6P +bkFJuqDX+Dnu+/JqL0Jdjb2o1SvVwMIh/k3rZPUUP/LqWP7cpGLc8QbFlq9raMNv ++mFZYJ0CgYEA5IQzp7SymcKgQqwcq5no2YOr76AykSCjOnLYYrotFqbxGJ19Cnol +Z74Habxjv89Kc7bfIwbz/AolkhAS2y0CYwSJL4wZIUb8W2mroSmaHhsk3A4DMPBB +wgSsdiBpixQqNDUAvnHc3FIyAGdpA73TJQrGY2F6QyQ9re3a/R8Dc3k= -----END RSA PRIVATE KEY----- """ private let exampleServerCert = """ -----BEGIN CERTIFICATE----- MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBYxFDASBgNVBAMMC2V4YW1w -bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5eeBEXc8K/Tg -YkfueXOlX3tZU45ooudYeBMyHPLG/2j1bRxLEHTPQ/1SrSKaTZH+s+KdKTtD7i7t -6SEatnMZi01NzWMA4etARx3cOMtDtZIoA8w6pgyxFVY8iOlehrQrPoOvZrZc1LJl -7xK6xZQR3v1t/68fhQDU9uOU6dNJgvyjMsrhOuJ6DPHmOAe1pUUVu+/7SCwCd4of -Vsq7pfahQhMQK5w8IUtOlqd4xg5tgTGWIPFmppVACYSOpDVDQM30KH8I1W7idhH8 -EAyyTEbJx9B1wwi7PXd4u3cCeZRrSAgwEpRAYS/ZznCnXuZ5fafcvUO3X3gB7jAS -XRPrm7/EMQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA12dbCAmQBefUQ5PkjDu02 -pO+ajryeivexwM4mpn8u9IIANQKu7E1oJYfj3zflpIxly3DebuoHV7zdFzfDzdu4 -LIQIEqqqDLsBA2gvk25INMgXx9rtA1fmL3pB4CDUoBGHkprJHdni6Z397sZIe0Bl -vOca55RbpZ4kj8qEUxI2WrSVtotRuQLS6KuqGEFeZS4+31kP6RVD7H3ZlO0gevNM -TgIHZknzu5Lvhdz5jx6PO/3RRwC3V5UB/mHnY6f8QS0M2c2gpS8nZUNpuN6dkRHX -bcEVVDiWHZnLwA8Ugts2m3eNXyPgmXSgFW6Be9FGYwuTCh6mvXR2Zf5pS+wAndBg +Fw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBYxFDASBgNVBAMMC2V4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5UPdc3MERjIj +rKNMcsCJEPJtzFZG7T99rDaENxl5hF5TdlCuMfKQMyf4rmUk2KdQXduWDmP/9keO +Btc3Hw9xm7mHn7UPbK0kHjlncqTCnjZzVQ2j0stg4Q0WjGeS0aB8k1AHPiBaOnvJ +LYcBJrA8mZK+inEE0gWEJsODTM+bKb3+5I69qVoHAkU2tXTDV9g1YKfP4H3rufEg +622AR1yAo8UxaGjY3amWps6XF/9R2iaSDAPLH1dCBw/YWrIH51n75S+n/H3Rz0+H +/aT9Eze0M2F2Nj1cU8fVcbDNR0smssgXVmE2mvQ+OvbO0H7VTS1HK2q2aPOPkRWh +yhFnOvPnbQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAWk5KHkWVsbXqz6/MnwxD5 +fn7JrBR77Vz3mxQO9NDKN/vczNMJf5cli6thrB5VPprl5LFXWwQ+LUQhP+XprigQ +8owU1gMNqDzxVHn7By2lnAehLcWYxDoGc8xgTuf2aEjFAyW9xB67YP/kDx9uNFwY +z+zWc6eMVr6dtKcsHcrIEoxLPBO9kuC/wlNY+73q04mmy9XQny15iQLy4sQT0wk4 +xV4p86rqDZcGepdV2/bLk2coF9cUOPOGwUBqEIc7n5GekC2WTXSnjOEK5c+2Wkbw +Yt4jXnvsaQ8bwpchHfM1K3mLn+2rCEZ3F4E5Ug7DKebrwU4z/Nccf1DwM4pdI0EI -----END CERTIFICATE----- """ private let exampleServerKey = """ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA5eeBEXc8K/TgYkfueXOlX3tZU45ooudYeBMyHPLG/2j1bRxL -EHTPQ/1SrSKaTZH+s+KdKTtD7i7t6SEatnMZi01NzWMA4etARx3cOMtDtZIoA8w6 -pgyxFVY8iOlehrQrPoOvZrZc1LJl7xK6xZQR3v1t/68fhQDU9uOU6dNJgvyjMsrh -OuJ6DPHmOAe1pUUVu+/7SCwCd4ofVsq7pfahQhMQK5w8IUtOlqd4xg5tgTGWIPFm -ppVACYSOpDVDQM30KH8I1W7idhH8EAyyTEbJx9B1wwi7PXd4u3cCeZRrSAgwEpRA -YS/ZznCnXuZ5fafcvUO3X3gB7jASXRPrm7/EMQIDAQABAoIBAQDHu2A+NEBqT8vA -lo1vpjC9ywPHu6jcHfCWINcgnyqTKjROHo54NYL7plD1aWJ0kamdzfqLn5lcjBjU -uJXkfAptIzO8g454t1CYeDCihrTEQb3RztQE/nG5/7mHmHcuv8fx/6WarkPn5TT5 -hmQM0p7UA4hU4WeYvShHdWAh5BWxXPUy4DC8Za+nBQC2cjnJk8lQMqPbA7wwbSpt -rh8RXGWvXB2kh9LDJfCgOlyN0FJavVJ+RsrEpgINw8jplOMipq6KMqzpbJvO7RGq -nyWttlMheFagav2VmQ79A9/mcMxK2JP8amD842alCQopkBCUpiJp3CNoF38BNtj8 -7D3DMQahAoGBAPbbeMFX2PPkifDFlS9Cide1Vq9aCIkXFAVc+6Tcy0pk83U5XLs6 -PrIqmeZ5+GD2KKe/zMBLiuAjLXuyNtZkQi1XpC8pInk77szvLTVOLFsJYne2YSAo -vQBAGrKRFsHzRn76SZcuV5CUSen3fLKudGggHFoUKUVR8dMHVc/eDY7HAoGBAO5r -S8cOR85mnRaGLopKERcWjUOq6X5k1/atHjO+gawPcAxSTMebSvdmI/WcNLHiaHHM -ZorlFzjIfpx4o+cpjNB85RsVnMfrt+sS/B6DUaSgz4HXRIRPJm6+G8JtSi0Aa6gC -CI8yCKZsjAkcC6s8KRmuY9e3vqTnG1itTnsQin1HAoGAGmK5FIlsQh1ydQ7ZdFS7 -YRgb7OBFu0mBNVWL/EIxZIFH2IbKF6URIIAXNSBiYRLOo6eHniI09OItsWQKIn5S -6H/Op8/QxH6YdsU14tW5Pf3RzZPr68EO+qDfeaiycwaqyVW9WfB1IZoIEH8IkBy/ -ioWsIiC3jJZGr9S/4lkMv+8CgYEAlf/LXSEO7DyC+HjTLw4KUoxNtBUDchHgDcI9 -DjD9RFMyG45r3+lD8QLB/PSZ8pCPRYljul8HjSIXBjqgY/8wKLtrKO8gBGe4/pyj -Ik9cPkcuRnI5GUTy2RmiPWClGkr5cGpXGEBSUOJZ+CE89i6TbSTajA1+VCFSgygG -CEcP2mECgYAPgAJ7ukZCWMZwRNy1WDVmmZfUJIMHrt/QpdaUemORKefpsiwIm0JL -LL3fEaYXnvV6J4SNcKxFDNCF3hUQ2gleRJ34pMmA0EWlkPrYnNcobRVhA9E1N0tO -Xy5aj7bjNP3+fmgjbPwPT2BMwTEuOeEyrZdNNYcDnp0oMNbcyjIbYw== +MIIEpAIBAAKCAQEA5UPdc3MERjIjrKNMcsCJEPJtzFZG7T99rDaENxl5hF5TdlCu +MfKQMyf4rmUk2KdQXduWDmP/9keOBtc3Hw9xm7mHn7UPbK0kHjlncqTCnjZzVQ2j +0stg4Q0WjGeS0aB8k1AHPiBaOnvJLYcBJrA8mZK+inEE0gWEJsODTM+bKb3+5I69 +qVoHAkU2tXTDV9g1YKfP4H3rufEg622AR1yAo8UxaGjY3amWps6XF/9R2iaSDAPL +H1dCBw/YWrIH51n75S+n/H3Rz0+H/aT9Eze0M2F2Nj1cU8fVcbDNR0smssgXVmE2 +mvQ+OvbO0H7VTS1HK2q2aPOPkRWhyhFnOvPnbQIDAQABAoIBAQDk0+vAQ1hMx9ab +hRHUpx8nbxDwFl0Mh4Zj0LX+WMrUt2EOglCbQcNzi73GMuWn6LdqNrV6/4yGv7ye +T0iRE9UM3Qzk9s7CZb3a/OinoJMvXqGWjtqolp3HgkyzLt13pXsxfXr9I0Vrggm2 +Cz2248hYcAMGIu/wv9i66AGxNLVl3nzlo3K3J+6LwGYSrM5MMsN8p/RIc7RD30cg +Qer6uiGdYemD2hbOuqcqImzhMLvoYn683uLOoDhiFLmAPIU+VxtHs2pMpp4ebjrl +PpS8TtHnV85v/fhX6RE/jo5razdSU4LxW/p/fF5Zte+QR6FJgFFWaQvZd6/Vtuh6 +K0Hadt1xAoGBAP1fuBjUElQgWBtoXb422X2/LurnyHfNSqSDM3z8OiCSN08r5GmM +ylWqh7k1sQWBzQ4OAsZcbwvrpvxMGYEd1K99LtUcM235WcKTomz7QRRUKG74tyFk +VdCgcMF+q2DdBE+hlF08bTNk1dM6uPlinNiMklydhLFjjhLlfDkiteGTAoGBAOek +LXqKMK4H5I1VQBgNKAv25tVItDabX5MPqhJVxmsvbNNTh/pfaNW7ietZNkec8FXs +UtS2Hv2hwNMVSb8l9bk96b2x9wiww2exI4oWKjKkJrSVsIcWc4BgeZ2xUtnV6QR5 +XSNm7D4E11KhuHPbB01cAZeC0Cf/4rTZ5ERhULL/AoGAS/UpHJBfGkdEAptsFv0c +gH0TFKr9xySNLvqCMgLvbhpHaH2xEQ97DOl9nMGC2zLJhWAf5tWJGNrBibtKnhGS +VDXEF3FH3b018oYN2HwOS4jbQkFfrSwGKfAfPXK67+PySekXsEfQOOsOyy88is7M +VIL30boLMJ621eVkM0C7o+8CgYBLiiK6n24YksJZxL9OGJxCqpXEYB1E4Y5davJP +YGGAesrGb6scXxjU+n+TnFgzKl7F5ndsnqeklqdHLt4J09s6OZKMJgklcF+I5R9t +3KSONzHYGiijJRMtfkiqwDUAjN2cc+eHr/zCjNmbPNnmDjtnYuWx/xrasHvB9nyW +QBYNCQKBgQCDqdvchLcreSdbXKr6swvBz8XxzCaEParbm7iOvdLlv93svvCaHgI8 +6E+FlXk68Qc2Dj8de/xEnl/OonNQRgIQh7czBJmYP8+TCPECm8fvv2TsddNvjTmF +TTx8wf9gixHffRtXZ4ILrP1sX7c4if3bfaMxKz+0ZyfzWZry8qZtVQ== -----END RSA PRIVATE KEY----- """ private let clientCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMMCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+3lfFuI8LRc31C -qzBJw60JEJMohIXRdoNYVmNnTSd1LQHaCd/qM5J2QU7LtMpAfJ29DyT0Q3Z67VMG -Bgz2kcx5lKGJpNfT7BJcB2F+Em1wgPj/N/wrHzQS+hBrnRokt0buyrqAaEsRRU6O -8g6trnda9+h7/5ktQpHkNpitJXWYyykyAP97NjYS1wDsSORbAYIbFIBuGoPQeiDW -AQVja9VTTCqEdYsShsUjXRGUPgBEk9i+IIqZg3eLZ26dEfVx0f5vGrmWnVDKCSNK -TzHL7p6mO8NL0k8CrTZwHYvfgUjVgZu43L6GIYiG3PTZhsHCZZgXZ4hUuRWrwTeJ -6wx6u8sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJfnPkN+JURx7aYnyBisPFD0s -wdF9XC6/Oxxe9GZ3aaLcNsf6Cwshd6sI/7FKOdP3EVtixcrCMupH16cN1sVw4UC8 -o8iv7G8h9XeY3DrzpOaAm1xxnRAox4AjY9Hkz2FzKyywCFNfj7I/vuPivdi1xk4v -tWGPXQvMmVhl9bP5FVEbK4w0MFIMkV0XnXuKffh+6lKS2hZhVejQ56S4hRMYT7Qp -wLDLwn5b2fYUaPKwjeoj1RA6tuagPnzVbvwGMrViQiwSqDgv2X7BxMP/5IxsWxlr -Ar/Yo+UQbilfT7pJWKvuRomZ+h+lg63AW1YmoLMkGV0rQL3qTpVypIRiJTdPvQ== +Fw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBQxEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOMSDHjt5P3MOmtw +atYP0PnCfr2sxsMd1uxKeb+x5mpYuckcsnm1UR0LcBCizwBZ7yYAZQkFyK7vdJge +Hv9P3Rki6+6Jj/ngLdpOirtUcOfnfzbdlA2k6qJtY6G+ZKyczDICWaZHzNRycDkL +Yv4kzUT8PynIRIK/LPyXQa+tGty9+G2exVPdpzKpCgE8fKd8FeCOLW06Z0RsP0FS +ySPSJxdDq0BRbfurhplhawh7uJ+7IoVfdWV2wwDvLztCEXHn2iiNpyzIixYapnVB +PX1MXelsPRJaa9EKwOiqJB5ZcV9JWk9wa4W7mJrRfFTRh/9HRsoXIAaJPIqjTmqI +ffat/1cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAWpPkMzjKLAnxbfyasR6cPk3t +ZanuBQ9RqVJw2gPem+SmSpvyPZ3CJTi0ZeUbPRSJ2W8YHyogaA5XShZSm8JJQKye +TNYqUewVbU18OwVco0l7Wc8R1iiZgYEUcvMF2f/EWEMoCTmS3JlpbLl7LmdTy7iQ +gIrR+iQ649nLw1T4Q5kp7zxjI6WJ3eZVNUjlTrUzKapSY4Tm2/+GafD+WNVRRACh +Y9VNkaQ6qYy4SaLw6+bX2YdbDhIi275vAONHIZcAsMt6/aLJzKgfTxRqqmEvmJdQ +KSVRRaSKZ/qe9UBdl4oFn1wupFAoNDQWkXT/Q3kVhxVXZ8ZE+ylZnuWcj3CHvQ== -----END CERTIFICATE----- """ private let clientSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl -ci1jYTAeFw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMM -CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+3lfFu -I8LRc31CqzBJw60JEJMohIXRdoNYVmNnTSd1LQHaCd/qM5J2QU7LtMpAfJ29DyT0 -Q3Z67VMGBgz2kcx5lKGJpNfT7BJcB2F+Em1wgPj/N/wrHzQS+hBrnRokt0buyrqA -aEsRRU6O8g6trnda9+h7/5ktQpHkNpitJXWYyykyAP97NjYS1wDsSORbAYIbFIBu -GoPQeiDWAQVja9VTTCqEdYsShsUjXRGUPgBEk9i+IIqZg3eLZ26dEfVx0f5vGrmW -nVDKCSNKTzHL7p6mO8NL0k8CrTZwHYvfgUjVgZu43L6GIYiG3PTZhsHCZZgXZ4hU -uRWrwTeJ6wx6u8sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAlbFNTPHLbJABMLy2 -DwfznwxAwkCfyAdZWq7A5gLP3GhY5xafc3HdeJ6gOfeXH9v1y3DEI6jeAXxmXAAJ -4hOIQuVfLL76PIkAwdHRXYrzLdTiN+pGNGVBRiOZoXIzGcdC4k+2KgeHGyVu5bb4 -EDFeeiuHMaeNlBEeIrUH38AsTm8hruVPuqnm0WAZVJ5RvWPOJgv667P6B/le+U5O -z5F11zPr47l69Zlh5Ud+WlG3yymj2ZIibuqdcQC9iuiLFcig6PhBheJzxr7MCvcA -T8T/DZusQBdcHqVrMbBfSnL426Kunqd8AXWEz09o5oTkeyK/CNWFyMfPfJqEOTWs -tHgfiw== +ci1jYTAeFw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBQxEjAQBgNVBAMM +CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOMSDHjt +5P3MOmtwatYP0PnCfr2sxsMd1uxKeb+x5mpYuckcsnm1UR0LcBCizwBZ7yYAZQkF +yK7vdJgeHv9P3Rki6+6Jj/ngLdpOirtUcOfnfzbdlA2k6qJtY6G+ZKyczDICWaZH +zNRycDkLYv4kzUT8PynIRIK/LPyXQa+tGty9+G2exVPdpzKpCgE8fKd8FeCOLW06 +Z0RsP0FSySPSJxdDq0BRbfurhplhawh7uJ+7IoVfdWV2wwDvLztCEXHn2iiNpyzI +ixYapnVBPX1MXelsPRJaa9EKwOiqJB5ZcV9JWk9wa4W7mJrRfFTRh/9HRsoXIAaJ +PIqjTmqIffat/1cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAIqGPpw/m3oIb+Ok7 +eZCEph/IcEwvkJXAFYeYjQHDsK1EW/HNoyjKKU3CaLJuWtvNbW+8GpmiVdAcO0XS +RN+xwDOLmB+9Ob70tZRsR/4/695WkkCm/70Y89YqTq3ev86vZmPWBGZsdXB/rvfs +sJbEkNeDRAquEbVQ3K8qmG7w8oC+VdzdQfQHY6hdkzsb0Q99aPASwGjxPVDz12Tb +v9g9f9yVwI+vxxabHr4nvKJ/GfuHRzG2eSW2TNBY/Kxp10+lCdMfbPq2p0LsV4eZ +eHPCFqiBe6CK80Pdpy7CNCPBBvGkGb7nfxBi4/tNVDgMlOQy6pA3PLjib8NLMCIA +5iUEvw== -----END CERTIFICATE----- """ private let clientKey = """ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAn7eV8W4jwtFzfUKrMEnDrQkQkyiEhdF2g1hWY2dNJ3UtAdoJ -3+ozknZBTsu0ykB8nb0PJPRDdnrtUwYGDPaRzHmUoYmk19PsElwHYX4SbXCA+P83 -/CsfNBL6EGudGiS3Ru7KuoBoSxFFTo7yDq2ud1r36Hv/mS1CkeQ2mK0ldZjLKTIA -/3s2NhLXAOxI5FsBghsUgG4ag9B6INYBBWNr1VNMKoR1ixKGxSNdEZQ+AEST2L4g -ipmDd4tnbp0R9XHR/m8auZadUMoJI0pPMcvunqY7w0vSTwKtNnAdi9+BSNWBm7jc -voYhiIbc9NmGwcJlmBdniFS5FavBN4nrDHq7ywIDAQABAoIBAAKBWrTCyYTQzEL2 -vMCxJ4SbU8s7I3kF5BoDVLeScz9fMymIRgdhIRX3DOczgs55XHsM8CPgQP6mxvo6 -afXiGD9g2Nf/1Lod9OIE14jL9XYKAbvmJParpn2mno2LYpd6Y/WU4VEzmm8zAidN -Tra0OrxcjO70ovnAH/8x2Tlj3eaOTKl3HzOHGj7gl/DikQu1J2ZgdXwfwUPGYhL/ -TtlifD3+UWZbFaG/RjrqAmF3UJr1QI0EseFgQJ+/8VpCsc/FEL21VETCcHBSmYjO -AMXiJIN7MxMArYtVwdTgYwklCeK8zxucB9OvDkAbxghUTClWWPNIk/TBub6vG7C1 -JibtVqECgYEAzDlVYJSTDtk+RVm0yf0UZ2WzRIuxp7+ldq8Na3BNiwLQVDuQiMBN -dYg5lT5JOE2w10+oiZwNUZXy+hVA5Kd+HsD/t2zVV1opciYwe6/GTTiveo74PyD0 -SP9R+P9936xuFAPaYRKwYcmv0c/j6Vx1qAqG1mup6njBUh+n4dvM1YcCgYEAyDWk -rbQLknMIqTENLab6H7TYaDcqq6lcW8EgxzcecCSNFKccxL2YP4KUyfK/DRm1ufpU -nmKPrmTwzmtHPZ/pnKKFzlwEtdX+wfg8TBC8UU4g83UioR9J3hSylbkwETscQqgF -eMEIEaJx50JDMGmn0BKpqK+sLSQslP3Gbcc3+J0CgYEAjx9LF0Fogkp7WozQp5Im -f4QFi28/FOm5YyCxDe+JWHejWrTXyQ7D+i96833QQJYp7esUmUP1DY1B2EOW0+gR -+imVzI2IQgyc6TOcXMJF/g5Q5FpX3Z4RtSrB3vfm1h94kaxVmhxH4nA/OJIyDnRO -vHKMJq8TSJBSI2St+hpZRfcCgYEAxADAV84MBjPYJst+u1LdTG0f7+cSPzxuzuUj -0eSESAWAmNeBsppqksKkJ5EeuRSSdKA+d1DGmVT46xzbgdksO8xgcsZjViFKZ1s+ -rLk1o+N5Ht9uJ48aIfDhZPMHu9bCs/8KXE2eOKVwHZchcCP/xhR/REW3qfngK3zG -5nJCuYECgYBd2qcQ6oZAfnBTqrHbMy/cHRwkoxIQ9HqkmiGkFC4nNRUKULToaxGA -Er6QJWO3htk1GImKZdmDeJZIUN+aFCgYfzbNVgq8CUJp9vD28cQKTDTwKpN8jNZ1 -rQ44/3Dg9zUI2KWxWYafpawuQIVNchlt7PAlVhtgNxJXUHEQl+/Opw== +MIIEpQIBAAKCAQEA4xIMeO3k/cw6a3Bq1g/Q+cJ+vazGwx3W7Ep5v7Hmali5yRyy +ebVRHQtwEKLPAFnvJgBlCQXIru90mB4e/0/dGSLr7omP+eAt2k6Ku1Rw5+d/Nt2U +DaTqom1job5krJzMMgJZpkfM1HJwOQti/iTNRPw/KchEgr8s/JdBr60a3L34bZ7F +U92nMqkKATx8p3wV4I4tbTpnRGw/QVLJI9InF0OrQFFt+6uGmWFrCHu4n7sihV91 +ZXbDAO8vO0IRcefaKI2nLMiLFhqmdUE9fUxd6Ww9Elpr0QrA6KokHllxX0laT3Br +hbuYmtF8VNGH/0dGyhcgBok8iqNOaoh99q3/VwIDAQABAoIBAQC5gA4mYJoY6FW1 +XcI5m/QphcWKaHJ8BY2FvZXWj5vftxoXfNUk7oYURzrGrGqVK+Nd1Sa1Bz+aAc7r +UngaNQE3vrqlRUYUaRqsZEubm/Ec0pavmLaRqu9vwBOLmAGgrft2w0q/t5pS2CZr +w6ycWC5FNBjZplypv0oeE+c6gB0YxKJ2mjKEYHWOop+uBPql2G6TfCeu4mMekZPH +cHbMuMlBPN23HT7BmGCvk1YSaGbMt0iCTM0zThfe53AtLSVKn33szHm/XjLYJYGM +7N+SttwM+O88diFShWHUHmWsy5Lv0Mrkw3NRz37yQ8Uh1fJ0TMeLZEIzKSLrI8lv +XrBVE89ZAoGBAP8bBSytb6Aof6p1noIx8nu31d5/5mvRSUxoF1Lu/B/EeaNTGXxD +HvJEi4Lh/txm6/3IeC5gfJ0jExxoWyTD3wLqVvmf+FEMntXqnATC832uw3vkxZpd +E/MldDHE4UkWGBUvii7JN1fisyyZKLUu517crebJthpwk5Sf/1FgK1SbAoGBAOPd +3VTQ7uaD2zPn4QM9KZsFJ+7cS7l1qtupplj9e12T7r53tQLoFjcery2Ja7ZrF3aq +y07D2ww8y8v1ShxqTgSOdeqPCX1a4OS7Z93zsy58Jv3ZcXbfbSGiLbpoueQJbUZ0 +vKlDIf4uHn78fz8WIbe87UwKneKnaRrO64DtHQX1AoGBAIH11vYCySozV46UaxLy +tRB3//lg+RcWQJwvLyqt2z2nzzv4OrSGUT6k0tnzne3UdQcN2MPvnaxD0RmYxE3/ +hx4qGfMDnvJTVput8JuwYXE21hnI2y4fmuk0vHQaU5bzLYOle2UIVyxrrlHbGNTs +tywpimJXgnEHxvdhZyWis5BfAoGBAN45P2M6J+KzcRGb8DuiaHMAgkNWoJsMAEcd +mldrTeajINCsGeHtycyTpi/4tw0+P7HBO2ljZLr4h6AvZcl0ewXCkYjhWlXgTTeE +9PTmeDa7aaNjbl6J4vpMGeCTxcZ40xNFQcCo8fvbqm4ZfVdfFB8Gpz3jlLq4na5B +YjdoB0gJAoGAZCK3JbIN56KnmyENuZ6szWTZwkCMZq3kPVKBK8LAITVLVxg7Emjs +GyTU+JhMx9Hk2tU/tftM/dTZ2TRRMwmPbZNadtkQdDgsXDhfkrW9JmVewx4ECCcI +gBfWFOoABVTmVM9oNc74FeWu3nDjqGix5ZJ8+Zjjr8wUEcrU2TPZKn4= -----END RSA PRIVATE KEY----- """ private let serverExplicitCurveCert = """ -----BEGIN CERTIFICATE----- -MIICZzCCAg2gAwIBAgIJANBDrZOOttwWMAoGCCqGSM49BAMCMBYxFDASBgNVBAMM -C2V4YW1wbGUuY29tMB4XDTIxMTExMjEzMDY0MVoXDTIyMTExMjEzMDY0MVowFjEU -MBIGA1UEAwwLZXhhbXBsZS5jb20wggFLMIIBAwYHKoZIzj0CATCB9wIBATAsBgcq -hkjOPQEBAiEA/////wAAAAEAAAAAAAAAAAAAAAD///////////////8wWwQg//// -/wAAAAEAAAAAAAAAAAAAAAD///////////////wEIFrGNdiqOpPns+u9VXaYhrxl -HQawzFOw9jvOPD4n0mBLAxUAxJ02CIbnBJNqZnjhE50mt4GffpAEQQRrF9Hy4SxC -R/i85uVjpEDydwN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFe -zsu2QGg3v1H1AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQED -QgAE+YMhPzGDLJuuMxB+ICQKWvPjbTEsRXgbq3Hmrds6x/1QY14e/xh4TqpAEW6M -0R5xhqg7ZU7O8NHtuiCyxPzCoqNQME4wHQYDVR0OBBYEFD22/486iAai5jKBCttn -JstzA2u8MB8GA1UdIwQYMBaAFD22/486iAai5jKBCttnJstzA2u8MAwGA1UdEwQF -MAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgHwZi/Vk9odNqrae9LBxmt/ve4twT8JT0 -+CvvSVkNtnkCIQDcKZBIlsR1OVVxZDWGtcvz9oW+MMOrrbFYAIaf0akhWQ== +MIICEDCCAbYCCQDCeNe2vM7d6DAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt +cGxlLmNvbTAeFw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBYxFDASBgNV +BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B +AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB +AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT +sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl +Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo +N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABDmd +3Pzv6HbsUTmNd7RljKbkYP+36ljl6qVZKZ+8m3Exq4DvtIzLKho/4NluAhWCsRev +2pWTfEiqiYS/U40TnfQwCgYIKoZIzj0EAwIDSAAwRQIhAI+BpDBjiqZD7r5vhPrG +TT9Kq9s4ekIc1/a4AoTioT8CAiAluJHscXt+vBcqEI9sH0wudusCdPJyLbvNtMZd +wdduCw== -----END CERTIFICATE----- """ private let serverExplicitCurveKey = """ -----BEGIN EC PRIVATE KEY----- -MIIBaAIBAQQgxqYoMA6z4uJGgk/XIg1R2j8qF/Sv/g38YJG81dGGq4SggfowgfcC +MIIBaAIBAQQgBLTFlKchn4c+dQphsqJ2hWVpLPeRQ0opnSwvRsH+63iggfowgfcC AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 -YyVRAgEBoUQDQgAE+YMhPzGDLJuuMxB+ICQKWvPjbTEsRXgbq3Hmrds6x/1QY14e -/xh4TqpAEW6M0R5xhqg7ZU7O8NHtuiCyxPzCog== +YyVRAgEBoUQDQgAEOZ3c/O/oduxROY13tGWMpuRg/7fqWOXqpVkpn7ybcTGrgO+0 +jMsqGj/g2W4CFYKxF6/alZN8SKqJhL9TjROd9A== -----END EC PRIVATE KEY----- """ diff --git a/scripts/make-sample-certs.py b/scripts/make-sample-certs.py new file mode 100755 index 000000000..cc387515c --- /dev/null +++ b/scripts/make-sample-certs.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 + +# Copyright 2022, 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 os +import subprocess +import datetime + +TEMPLATE = """\ +/* + * Copyright {year}, 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. + */ + +//----------------------------------------------------------------------------- +// THIS FILE WAS GENERATED WITH make-sample-certs.py +// +// DO NOT UPDATE MANUALLY +//----------------------------------------------------------------------------- + +#if canImport(NIOSSL) +import struct Foundation.Date +import NIOSSL + +/// Wraps `NIOSSLCertificate` to provide the certificate common name and expiry date. +public struct SampleCertificate {{ + public var certificate: NIOSSLCertificate + public var commonName: String + public var notAfter: Date + + public static let ca = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), + commonName: "some-ca", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) + + public static let otherCA = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), + commonName: "some-other-ca", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) + + public static let server = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), + commonName: "localhost", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) + + public static let exampleServer = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), + commonName: "example.com", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) + + public static let serverSignedByOtherCA = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), + commonName: "localhost", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) + + public static let client = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), + commonName: "localhost", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) + + public static let clientSignedByOtherCA = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), + commonName: "localhost", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) + + public static let exampleServerWithExplicitCurve = SampleCertificate( + certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), + commonName: "localhost", + notAfter: Date(timeIntervalSince1970: {timestamp}) + ) +}} + +extension SampleCertificate {{ + /// Returns whether the certificate has expired. + public var isExpired: Bool {{ + return self.notAfter < Date() + }} +}} + +/// Provides convenience methods to make `NIOSSLPrivateKey`s for corresponding `GRPCSwiftCertificate`s. +public struct SamplePrivateKey {{ + private init() {{}} + + public static let server = try! NIOSSLPrivateKey(bytes: .init(serverKey.utf8), format: .pem) + public static let exampleServer = try! NIOSSLPrivateKey( + bytes: .init(exampleServerKey.utf8), + format: .pem + ) + public static let client = try! NIOSSLPrivateKey(bytes: .init(clientKey.utf8), format: .pem) + public static let exampleServerWithExplicitCurve = try! NIOSSLPrivateKey( + bytes: .init(serverExplicitCurveKey.utf8), + format: .pem + ) +}} + +// MARK: - Certificates and private keys + +private let caCert = \""" +{ca_cert} +\""" + +private let otherCACert = \""" +{other_ca_cert} +\""" + +private let serverCert = \""" +{server_cert} +\""" + +private let serverSignedByOtherCACert = \""" +{server_signed_by_other_ca_cert} +\""" + +private let serverKey = \""" +{server_key} +\""" + +private let exampleServerCert = \""" +{example_server_cert} +\""" + +private let exampleServerKey = \""" +{example_server_key} +\""" + +private let clientCert = \""" +{client_cert} +\""" + +private let clientSignedByOtherCACert = \""" +{client_signed_by_other_ca_cert} +\""" + +private let clientKey = \""" +{client_key} +\""" + +private let serverExplicitCurveCert = \""" +{server_explicit_curve_cert} +\""" + +private let serverExplicitCurveKey = \""" +{server_explicit_curve_key} +\""" + +#endif // canImport(NIOSSL) +""" + +def load_file(root, name): + with open(os.path.join(root, name)) as fh: + return fh.read().strip() + + +def extract_key(ec_key_and_params): + lines = [] + include_line = True + for line in ec_key_and_params.split("\n"): + if line == "-----BEGIN EC PARAMETERS-----": + include_line = False + elif line == "-----BEGIN EC PRIVATE KEY-----": + include_line = True + + if include_line: + lines.append(line) + return "\n".join(lines).strip() + + +if __name__ == "__main__": + now = datetime.datetime.now() + # makecert uses an expiry of 365 days. + delta = datetime.timedelta(days=365) + # Seconds since epoch + not_after = (now + delta).strftime("%s") + + # Expect to be called from the root of the checkout. + root = os.path.abspath(os.curdir) + executable = os.path.join(root, "scripts", "makecert") + try: + subprocess.check_call(executable) + except FileNotFoundError: + print("Please run the script from the root of the repository") + exit(1) + + kwargs = { + "year": now.year, + "timestamp": not_after, + "ca_cert": load_file(root, "ca.crt"), + "other_ca_cert": load_file(root, "other-ca.crt"), + "server_cert": load_file(root, "server-localhost.crt"), + "server_signed_by_other_ca_cert": load_file(root, "server-localhost-other-ca.crt"), + "server_key": load_file(root, "server-localhost.key"), + "example_server_cert": load_file(root, "server-example.com.crt"), + "example_server_key": load_file(root, "server-example.com.key"), + "client_cert": load_file(root, "client.crt"), + "client_signed_by_other_ca_cert": load_file(root, "client-other-ca.crt"), + "client_key": load_file(root, "client.key"), + "server_explicit_curve_cert": load_file(root, "server-explicit-ec.crt"), + "server_explicit_curve_key": extract_key(load_file(root, + "server-explicit-ec.key")) + } + + formatted = TEMPLATE.format(**kwargs) + with open("Sources/GRPCSampleData/GRPCSwiftCertificate.swift", "w") as fh: + fh.write(formatted) From b1a509364149be8897040940a7f3c9fed77f6086 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 14 Nov 2022 14:33:17 +0000 Subject: [PATCH 045/580] Make request/response parts conditionally sendable (#1523) Motivation: The request/response parts are conditionally sendable based on their request/response payload. They are not currently marked as such which causes warnings in tests. Modifications: - Add conditional sendable conformance to request/response parts Results: Fewer warnings. --- Sources/GRPC/Interceptor/MessageParts.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/GRPC/Interceptor/MessageParts.swift b/Sources/GRPC/Interceptor/MessageParts.swift index bc1403d89..9d81bc321 100644 --- a/Sources/GRPC/Interceptor/MessageParts.swift +++ b/Sources/GRPC/Interceptor/MessageParts.swift @@ -99,3 +99,10 @@ extension GRPCServerResponsePart { } } } + +#if swift(>=5.6) +extension GRPCClientRequestPart: Sendable where Request: Sendable {} +extension GRPCClientResponsePart: Sendable where Response: Sendable {} +extension GRPCServerRequestPart: Sendable where Request: Sendable {} +extension GRPCServerResponsePart: Sendable where Response: Sendable {} +#endif From 7b13c340cc202bd044b0bc334e19a46e441be4db Mon Sep 17 00:00:00 2001 From: Compound Radius <104736875+compoundradius@users.noreply.github.com> Date: Wed, 7 Dec 2022 00:22:38 -0800 Subject: [PATCH 046/580] update quick-start.md (#1527) --- Makefile | 2 +- docs/quick-start.md | 86 +++++++++++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/Makefile b/Makefile index 7ee086e7c..d0fe6930a 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SWIFT:=swift # Where products will be built; this is the SPM default. SWIFT_BUILD_PATH:=./.build SWIFT_BUILD_CONFIGURATION=debug -SWIFT_FLAGS=--build-path=${SWIFT_BUILD_PATH} --configuration=${SWIFT_BUILD_CONFIGURATION} --enable-test-discovery +SWIFT_FLAGS=--scratch-path=${SWIFT_BUILD_PATH} --configuration=${SWIFT_BUILD_CONFIGURATION} # Force release configuration (for plugins) SWIFT_FLAGS_RELEASE=$(patsubst --configuration=%,--configuration=release,$(SWIFT_FLAGS)) diff --git a/docs/quick-start.md b/docs/quick-start.md index addb8e1a1..5ba056c65 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -28,7 +28,7 @@ and other tutorials): ```sh $ # Clone the repository at the latest release to get the example code: -$ git clone -b 1.0.0 https://github.com/grpc/grpc-swift +$ git clone -b 1.13.0 https://github.com/grpc/grpc-swift $ # Navigate to the repository $ cd grpc-swift/ ``` @@ -131,27 +131,27 @@ In the same directory, open method like this: ```swift -class GreeterProvider: Helloworld_GreeterProvider { +final class GreeterProvider: Helloworld_GreeterAsyncProvider { + let interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? = nil + func sayHello( request: Helloworld_HelloRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { + context: GRPCAsyncServerCallContext + ) async throws -> Helloworld_HelloReply { let recipient = request.name.isEmpty ? "stranger" : request.name - let response = Helloworld_HelloReply.with { + return Helloworld_HelloReply.with { $0.message = "Hello \(recipient)!" } - return context.eventLoop.makeSucceededFuture(response) } func sayHelloAgain( request: Helloworld_HelloRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { + context: GRPCAsyncServerCallContext + ) async throws -> Helloworld_HelloReply { let recipient = request.name.isEmpty ? "stranger" : request.name - let response = Helloworld_HelloReply.with { + return Helloworld_HelloReply.with { $0.message = "Hello again \(recipient)!" } - return context.eventLoop.makeSucceededFuture(response) } } ``` @@ -159,36 +159,54 @@ class GreeterProvider: Helloworld_GreeterProvider { #### Update the client In the same directory, open -`Sources/Examples/HelloWorld/Client/main.swift`. Call the new method like this: +`Sources/Examples/HelloWorld/Client/HelloWorldClient.swift`. Call the new method like this: ```swift -func greet(name: String?, client greeter: Helloworld_GreeterClient) { - // Form the request with the name, if one was provided. - let request = Helloworld_HelloRequest.with { - $0.name = name ?? "" - } +func run() async throws { + // Setup an `EventLoopGroup` for the connection to run on. + // + // See: https://github.com/apple/swift-nio#eventloops-and-eventloopgroups + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + // Make sure the group is shutdown when we're done with it. + defer { + try! group.syncShutdownGracefully() + } - // Make the RPC call to the server. - let sayHello = greeter.sayHello(request) + // Configure the channel, we're not using TLS so the connection is `insecure`. + let channel = try GRPCChannelPool.with( + target: .host("localhost", port: self.port), + transportSecurity: .plaintext, + eventLoopGroup: group + ) - // wait() on the response to stop the program from exiting before the response is received. - do { - let response = try sayHello.response.wait() - print("Greeter received: \(response.message)") - } catch { - print("Greeter failed: \(error)") - return - } + // Close the connection when we're done with it. + defer { + try! channel.close().wait() + } + + // Provide the connection to the generated client. + let greeter = Helloworld_GreeterAsyncClient(channel: channel) - let sayHelloAgain = greeter.sayHelloAgain(request) - do { - let response = try sayHelloAgain.response.wait() - print("Greeter received: \(response.message)") - } catch { - print("Greeter failed: \(error)") - return + // Form the request with the name, if one was provided. + let request = Helloworld_HelloRequest.with { + $0.name = self.name ?? "" + } + + do { + let greeting = try await greeter.sayHello(request) + print("Greeter received: \(greeting.message)") + } catch { + print("Greeter failed: \(error)") + } + + do { + let greetingAgain = try await greeter.sayHelloAgain(request) + print("Greeter received: \(greetingAgain.message)") + } catch { + print("Greeter failed: \(error)") + } } -} ``` #### Run! From e49a33143c50eef21a83867f3346eb52322679c7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 7 Dec 2022 11:29:08 +0000 Subject: [PATCH 047/580] Pass through the right h2 configuration (#1528) Motivation: The client passed the target window size through max frame size. Modifications: Pass through the right configuration. --- Sources/GRPC/ConnectionPool/PooledChannel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/ConnectionPool/PooledChannel.swift b/Sources/GRPC/ConnectionPool/PooledChannel.swift index 999f87f6c..f7e3af01a 100644 --- a/Sources/GRPC/ConnectionPool/PooledChannel.swift +++ b/Sources/GRPC/ConnectionPool/PooledChannel.swift @@ -85,7 +85,7 @@ internal final class PooledChannel: GRPCChannel { tlsMode: tlsMode, tlsConfiguration: configuration.transportSecurity.tlsConfiguration, httpTargetWindowSize: configuration.http2.targetWindowSize, - httpMaxFrameSize: configuration.http2.targetWindowSize, + httpMaxFrameSize: configuration.http2.maxFrameSize, errorDelegate: configuration.errorDelegate, debugChannelInitializer: configuration.debugChannelInitializer ) From 67788d5dccf9a631e69010b2984666f300eca8ea Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 8 Dec 2022 11:28:49 +0000 Subject: [PATCH 048/580] Avoid copies of large payloads (#1529) Motivation: Messages are prefixed with a 5-byte header. Currently messages of all sizes are written into a new buffer with their header. For smaller payloads this is good: we avoid the extra allocations associated with creating HTTP/2 frames. For large payloads the cost of the copy outweighs the cost of extra allocations. Modifications: - For messages larger than 8KB emit an extra HTTP/2 DATA frame containing just the message header. Result: Better performance for large payloads. --- .github/workflows/ci.yaml | 8 +- Sources/GRPC/GRPCClientChannelHandler.swift | 27 +++- Sources/GRPC/GRPCClientStateMachine.swift | 31 ++-- Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift | 13 +- Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift | 64 ++++---- .../GRPC/LengthPrefixedMessageWriter.swift | 143 +++++------------- Sources/GRPC/ReadWriteStates.swift | 25 +-- .../GRPCClientStateMachineTests.swift | 141 +++++++++-------- .../HTTP2ToRawGRPCStateMachineTests.swift | 4 +- .../LengthPrefixedMessageWriterTests.swift | 50 +++++- 10 files changed, 250 insertions(+), 256 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 889ee8a54..218d3eaea 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,7 +56,7 @@ jobs: include: - image: swiftlang/swift:nightly-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 428000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 392000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 @@ -66,7 +66,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 - image: swift:5.7-jammy env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 428000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 392000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 @@ -76,7 +76,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 - image: swift:5.6-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 429000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 393000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 177000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 @@ -86,7 +86,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 182000 - image: swift:5.5-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 459000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 423000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 189000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index 483dce7de..052ec8e5b 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -502,7 +502,10 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { switch self.unwrapOutboundIn(data) { case let .head(requestHead): // Feed the request into the state machine: - switch self.stateMachine.sendRequestHeaders(requestHead: requestHead) { + switch self.stateMachine.sendRequestHeaders( + requestHead: requestHead, + allocator: context.channel.allocator + ) { case let .success(headers): // We're clear to write some headers. Create an appropriate frame and write it. let framePayload = HTTP2Frame.FramePayload.headers(.init(headers: headers)) @@ -526,19 +529,29 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { // Feed the request message into the state machine: let result = self.stateMachine.sendRequest( request.message, - compressed: request.compressed, - allocator: context.channel.allocator + compressed: request.compressed ) switch result { - case let .success(buffer): - // We're clear to send a message; wrap it up in an HTTP/2 frame. - let framePayload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) + case let .success((buffer, maybeBuffer)): + let frame1 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) self.logger.trace("writing HTTP2 frame", metadata: [ MetadataKey.h2Payload: "DATA", MetadataKey.h2DataBytes: "\(buffer.readableBytes)", MetadataKey.h2EndStream: "false", ]) - context.write(self.wrapOutboundOut(framePayload), promise: promise) + // If there's a second buffer, attach the promise to the second write. + let promise1 = maybeBuffer == nil ? promise : nil + context.write(self.wrapOutboundOut(frame1), promise: promise1) + + if let actuallyBuffer = maybeBuffer { + let frame2 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(actuallyBuffer))) + self.logger.trace("writing HTTP2 frame", metadata: [ + MetadataKey.h2Payload: "DATA", + MetadataKey.h2DataBytes: "\(actuallyBuffer.readableBytes)", + MetadataKey.h2EndStream: "false", + ]) + context.write(self.wrapOutboundOut(frame2), promise: promise) + } case let .failure(writeError): switch writeError { diff --git a/Sources/GRPC/GRPCClientStateMachine.swift b/Sources/GRPC/GRPCClientStateMachine.swift index c7f02865e..1a8031a27 100644 --- a/Sources/GRPC/GRPCClientStateMachine.swift +++ b/Sources/GRPC/GRPCClientStateMachine.swift @@ -165,10 +165,11 @@ struct GRPCClientStateMachine { /// /// - Parameter requestHead: The client request head for the RPC. mutating func sendRequestHeaders( - requestHead: _GRPCRequestHead + requestHead: _GRPCRequestHead, + allocator: ByteBufferAllocator ) -> Result { return self.withStateAvoidingCoWs { state in - state.sendRequestHeaders(requestHead: requestHead) + state.sendRequestHeaders(requestHead: requestHead, allocator: allocator) } } @@ -195,11 +196,10 @@ struct GRPCClientStateMachine { /// request will be written. mutating func sendRequest( _ message: ByteBuffer, - compressed: Bool, - allocator: ByteBufferAllocator - ) -> Result { + compressed: Bool + ) -> Result<(ByteBuffer, ByteBuffer?), MessageWriteError> { return self.withStateAvoidingCoWs { state in - state.sendRequest(message, compressed: compressed, allocator: allocator) + state.sendRequest(message, compressed: compressed) } } @@ -351,7 +351,8 @@ struct GRPCClientStateMachine { extension GRPCClientStateMachine.State { /// See `GRPCClientStateMachine.sendRequestHeaders(requestHead:)`. mutating func sendRequestHeaders( - requestHead: _GRPCRequestHead + requestHead: _GRPCRequestHead, + allocator: ByteBufferAllocator ) -> Result { let result: Result @@ -369,7 +370,10 @@ extension GRPCClientStateMachine.State { result = .success(headers) self = .clientActiveServerIdle( - writeState: pendingWriteState.makeWriteState(messageEncoding: requestHead.encoding), + writeState: pendingWriteState.makeWriteState( + messageEncoding: requestHead.encoding, + allocator: allocator + ), pendingReadState: .init(arity: responseArity, messageEncoding: requestHead.encoding) ) @@ -390,18 +394,17 @@ extension GRPCClientStateMachine.State { /// See `GRPCClientStateMachine.sendRequest(_:allocator:)`. mutating func sendRequest( _ message: ByteBuffer, - compressed: Bool, - allocator: ByteBufferAllocator - ) -> Result { - let result: Result + compressed: Bool + ) -> Result<(ByteBuffer, ByteBuffer?), MessageWriteError> { + let result: Result<(ByteBuffer, ByteBuffer?), MessageWriteError> switch self { case .clientActiveServerIdle(var writeState, let pendingReadState): - result = writeState.write(message, compressed: compressed, allocator: allocator) + result = writeState.write(message, compressed: compressed) self = .clientActiveServerIdle(writeState: writeState, pendingReadState: pendingReadState) case .clientActiveServerActive(var writeState, let readState): - result = writeState.write(message, compressed: compressed, allocator: allocator) + result = writeState.write(message, compressed: compressed) self = .clientActiveServerActive(writeState: writeState, readState: readState) case .clientClosedServerIdle, diff --git a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift b/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift index 3fc8cc866..2d6fa7483 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift @@ -295,9 +295,16 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe ) switch writeBuffer { - case let .success(buffer): - let payload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) - self.context.write(self.wrapOutboundOut(payload), promise: promise) + case let .success((buffer, maybeBuffer)): + if let actuallyBuffer = maybeBuffer { + let payload1 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) + self.context.write(self.wrapOutboundOut(payload1), promise: nil) + let payload2 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(actuallyBuffer))) + self.context.write(self.wrapOutboundOut(payload2), promise: promise) + } else { + let payload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) + self.context.write(self.wrapOutboundOut(payload), promise: promise) + } if metadata.flush { self.markFlushPoint() } diff --git a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift index 2d45e8502..235e285b4 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift @@ -303,7 +303,11 @@ extension HTTP2ToRawGRPCStateMachine.State { } // Figure out which encoding we should use for responses. - let (writer, responseEncoding) = self.extractResponseEncoding(from: headers, encoding: encoding) + let (writer, responseEncoding) = self.extractResponseEncoding( + from: headers, + encoding: encoding, + allocator: allocator + ) // Parse the path, and create a call handler. guard let path = headers.first(name: ":path") else { @@ -516,7 +520,8 @@ extension HTTP2ToRawGRPCStateMachine.State { /// - Returns: A message writer and the response encoding header to send back to the client. private func extractResponseEncoding( from headers: HPACKHeaders, - encoding: ServerMessageEncoding + encoding: ServerMessageEncoding, + allocator: ByteBufferAllocator ) -> (LengthPrefixedMessageWriter, String?) { let writer: LengthPrefixedMessageWriter let responseEncoding: String? @@ -534,12 +539,12 @@ extension HTTP2ToRawGRPCStateMachine.State { configuration.enabledAlgorithms.contains($0) } - writer = LengthPrefixedMessageWriter(compression: algorithm) + writer = LengthPrefixedMessageWriter(compression: algorithm, allocator: allocator) responseEncoding = algorithm?.name case .disabled: // The server doesn't have compression enabled. - writer = LengthPrefixedMessageWriter(compression: .none) + writer = LengthPrefixedMessageWriter(compression: .none, allocator: allocator) responseEncoding = nil } @@ -642,12 +647,11 @@ extension HTTP2ToRawGRPCStateMachine { static func writeGRPCFramedMessage( _ buffer: ByteBuffer, compress: Bool, - allocator: ByteBufferAllocator, - writer: LengthPrefixedMessageWriter - ) -> Result { + writer: inout LengthPrefixedMessageWriter + ) -> Result<(ByteBuffer, ByteBuffer?), Error> { do { - let prefixed = try writer.write(buffer: buffer, allocator: allocator, compressed: compress) - return .success(prefixed) + let buffers = try writer.write(buffer: buffer, compressed: compress) + return .success(buffers) } catch { return .failure(error) } @@ -655,31 +659,27 @@ extension HTTP2ToRawGRPCStateMachine { } extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseOpenState { - func send( + mutating func send( buffer: ByteBuffer, - allocator: ByteBufferAllocator, compress: Bool - ) -> Result { + ) -> Result<(ByteBuffer, ByteBuffer?), Error> { return HTTP2ToRawGRPCStateMachine.writeGRPCFramedMessage( buffer, compress: compress, - allocator: allocator, - writer: self.writer + writer: &self.writer ) } } extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseOpenState { - func send( + mutating func send( buffer: ByteBuffer, - allocator: ByteBufferAllocator, compress: Bool - ) -> Result { + ) -> Result<(ByteBuffer, ByteBuffer?), Error> { return HTTP2ToRawGRPCStateMachine.writeGRPCFramedMessage( buffer, compress: compress, - allocator: allocator, - writer: self.writer + writer: &self.writer ) } } @@ -903,12 +903,14 @@ extension HTTP2ToRawGRPCStateMachine { } /// Send a response buffer. - func send( + mutating func send( buffer: ByteBuffer, allocator: ByteBufferAllocator, compress: Bool - ) -> Result { - return self.state.send(buffer: buffer, allocator: allocator, compress: compress) + ) -> Result<(ByteBuffer, ByteBuffer?), Error> { + return self.withStateAvoidingCoWs { state in + state.send(buffer: buffer, allocator: allocator, compress: compress) + } } /// Send status and trailers. @@ -1115,11 +1117,11 @@ extension HTTP2ToRawGRPCStateMachine.State { } } - func send( + mutating func send( buffer: ByteBuffer, allocator: ByteBufferAllocator, compress: Bool - ) -> Result { + ) -> Result<(ByteBuffer, ByteBuffer?), Error> { switch self { case .requestIdleResponseIdle: preconditionFailure("Invalid state: the request stream is still closed") @@ -1129,19 +1131,21 @@ extension HTTP2ToRawGRPCStateMachine.State { let error = GRPCError.InvalidState("Response headers must be sent before response message") return .failure(error) - case let .requestOpenResponseOpen(state): - return state.send( + case var .requestOpenResponseOpen(state): + let result = state.send( buffer: buffer, - allocator: allocator, compress: compress ) + self = .requestOpenResponseOpen(state) + return result - case let .requestClosedResponseOpen(state): - return state.send( + case var .requestClosedResponseOpen(state): + let result = state.send( buffer: buffer, - allocator: allocator, compress: compress ) + self = .requestClosedResponseOpen(state) + return result case .requestOpenResponseClosed, .requestClosedResponseClosed: diff --git a/Sources/GRPC/LengthPrefixedMessageWriter.swift b/Sources/GRPC/LengthPrefixedMessageWriter.swift index 54efbf970..fc3e326a3 100644 --- a/Sources/GRPC/LengthPrefixedMessageWriter.swift +++ b/Sources/GRPC/LengthPrefixedMessageWriter.swift @@ -28,8 +28,13 @@ internal struct LengthPrefixedMessageWriter { return self.compression != nil } - init(compression: CompressionAlgorithm? = nil) { + /// A scratch buffer that we encode messages into: if the buffer isn't held elsewhere then we + /// can avoid having to allocate a new one. + private var scratch: ByteBuffer + + init(compression: CompressionAlgorithm? = nil, allocator: ByteBufferAllocator) { self.compression = compression + self.scratch = allocator.buffer(capacity: 0) switch self.compression?.algorithm { case .none, .some(.identity): @@ -41,144 +46,76 @@ internal struct LengthPrefixedMessageWriter { } } - private func compress( + private mutating func compress( buffer: ByteBuffer, - using compressor: Zlib.Deflate, - allocator: ByteBufferAllocator + using compressor: Zlib.Deflate ) throws -> ByteBuffer { // The compressor will allocate the correct size. For now the leading 5 bytes will do. - var output = allocator.buffer(capacity: 5) - + self.scratch.clear(minimumCapacity: 5) // Set the compression byte. - output.writeInteger(UInt8(1)) - + self.scratch.writeInteger(UInt8(1)) // Set the length to zero; we'll write the actual value in a moment. - let payloadSizeIndex = output.writerIndex - output.writeInteger(UInt32(0)) + let payloadSizeIndex = self.scratch.writerIndex + self.scratch.writeInteger(UInt32(0)) let bytesWritten: Int do { var buffer = buffer - bytesWritten = try compressor.deflate(&buffer, into: &output) + bytesWritten = try compressor.deflate(&buffer, into: &self.scratch) } catch { throw error } // Now fill in the message length. - output.writePayloadLength(UInt32(bytesWritten), at: payloadSizeIndex) + self.scratch.writePayloadLength(UInt32(bytesWritten), at: payloadSizeIndex) // Finally, the compression context should be reset between messages. compressor.reset() - return output + return self.scratch } /// Writes the readable bytes of `buffer` as a gRPC length-prefixed message. /// /// - Parameters: /// - buffer: The bytes to compress and length-prefix. - /// - allocator: A `ByteBufferAllocator`. /// - compressed: Whether the bytes should be compressed. This is ignored if not compression /// mechanism was configured on this writer. /// - Returns: A buffer containing the length prefixed bytes. - func write( + mutating func write( buffer: ByteBuffer, - allocator: ByteBufferAllocator, compressed: Bool = true - ) throws -> ByteBuffer { + ) throws -> (ByteBuffer, ByteBuffer?) { if compressed, let compressor = self.compressor { - return try self.compress(buffer: buffer, using: compressor, allocator: allocator) - } else if buffer.readerIndex >= 5 { - // We're not compressing and we have enough bytes before the reader index that we can write - // over with the compression byte and length. - var buffer = buffer - - // Get the size of the message. - let messageSize = buffer.readableBytes - - // Move the reader index back 5 bytes. This is okay: we validated the `readerIndex` above. - buffer.moveReaderIndex(to: buffer.readerIndex - 5) - - // Fill in the compression byte and message length. - buffer.setInteger(UInt8(0), at: buffer.readerIndex) - buffer.setInteger(UInt32(messageSize), at: buffer.readerIndex + 1) - - // The message bytes are already in place, we're done. - return buffer + let compressedAndFramedPayload = try self.compress(buffer: buffer, using: compressor) + return (compressedAndFramedPayload, nil) + } else if buffer.readableBytes > Self.singleBufferSizeLimit { + // Buffer is larger than the limit for emitting a single buffer: create a second buffer + // containing just the message header. + self.scratch.clear(minimumCapacity: 5) + self.scratch.writeMultipleIntegers(UInt8(0), UInt32(buffer.readableBytes)) + return (self.scratch, buffer) } else { - // We're not compressing and we don't have enough space before the message bytes passed in. - // We need a new buffer. - var lengthPrefixed = allocator.buffer(capacity: 5 + buffer.readableBytes) - - // Write the compression byte. - lengthPrefixed.writeInteger(UInt8(0)) - - // Write the message length. - lengthPrefixed.writeInteger(UInt32(buffer.readableBytes)) - - // Write the message. - var buffer = buffer - lengthPrefixed.writeBuffer(&buffer) - - return lengthPrefixed + // We're not compressing and the message is within our single buffer size limit. + self.scratch.clear(minimumCapacity: 5 &+ buffer.readableBytes) + self.scratch.writeMultipleIntegers(UInt8(0), UInt32(buffer.readableBytes)) + self.scratch.writeImmutableBuffer(buffer) + return (self.scratch, nil) } } - /// Writes the data into a `ByteBuffer` as a gRPC length-prefixed message. + /// Message size above which we emit two buffers: one containing the header and one with the + /// actual message bytes. At or below the limit we copy the message into a new buffer containing + /// both the header and the message. /// - /// - Parameters: - /// - payload: The payload to serialize and write. - /// - buffer: The buffer to write the message into. - /// - Returns: A `ByteBuffer` containing a gRPC length-prefixed message. - /// - Precondition: `compression.supported` is `true`. - /// - Note: See `LengthPrefixedMessageReader` for more details on the format. - func write( - _ payload: GRPCPayload, - into buffer: inout ByteBuffer, - compressed: Bool = true - ) throws { - buffer.reserveCapacity(buffer.writerIndex + LengthPrefixedMessageWriter.metadataLength) - - if compressed, let compressor = self.compressor { - // Set the compression byte. - buffer.writeInteger(UInt8(1)) - - // Leave a gap for the length, we'll set it in a moment. - let payloadSizeIndex = buffer.writerIndex - buffer.moveWriterIndex(forwardBy: MemoryLayout.size) - - var messageBuf = ByteBufferAllocator().buffer(capacity: 0) - try payload.serialize(into: &messageBuf) - - // Compress the message. - let bytesWritten = try compressor.deflate(&messageBuf, into: &buffer) - - // Now fill in the message length. - buffer.writePayloadLength(UInt32(bytesWritten), at: payloadSizeIndex) - - // Finally, the compression context should be reset between messages. - compressor.reset() - } else { - // We could be using 'identity' compression, but since the result is the same we'll just - // say it isn't compressed. - buffer.writeInteger(UInt8(0)) - - // Leave a gap for the length, we'll set it in a moment. - let payloadSizeIndex = buffer.writerIndex - buffer.moveWriterIndex(forwardBy: MemoryLayout.size) - - let payloadPrefixedBytes = buffer.readableBytes - // Writes the payload into the buffer - try payload.serialize(into: &buffer) - - // Calculates the Written bytes with respect to the prefixed ones - let bytesWritten = buffer.readableBytes - payloadPrefixedBytes - - // Write the message length. - buffer.writePayloadLength(UInt32(bytesWritten), at: payloadSizeIndex) - } - } + /// Using two buffers avoids expensive copies of large messages. For smaller messages the copy + /// is cheaper than the additional allocations and overhead required to send an extra HTTP/2 DATA + /// frame. + /// + /// The value of 8192 was chosen empirically. We subtract the length of the message header + /// as `ByteBuffer` reserve capacity in powers of two and want to avoid overallocating. + private static let singleBufferSizeLimit = 8192 - 5 } extension ByteBuffer { diff --git a/Sources/GRPC/ReadWriteStates.swift b/Sources/GRPC/ReadWriteStates.swift index 27df2d4c4..1a8034505 100644 --- a/Sources/GRPC/ReadWriteStates.swift +++ b/Sources/GRPC/ReadWriteStates.swift @@ -30,7 +30,10 @@ struct PendingWriteState { /// The 'content-type' being written. var contentType: ContentType - func makeWriteState(messageEncoding: ClientMessageEncoding) -> WriteState { + func makeWriteState( + messageEncoding: ClientMessageEncoding, + allocator: ByteBufferAllocator + ) -> WriteState { let compression: CompressionAlgorithm? switch messageEncoding { case let .enabled(configuration): @@ -39,7 +42,7 @@ struct PendingWriteState { compression = nil } - let writer = LengthPrefixedMessageWriter(compression: compression) + let writer = LengthPrefixedMessageWriter(compression: compression, allocator: allocator) return .writing(self.arity, self.contentType, writer) } } @@ -53,25 +56,23 @@ enum WriteState { /// more messages to be written. case notWriting - /// Writes a message into a buffer using the `writer` and `allocator`. + /// Writes a message into a buffer using the `writer`. /// /// - Parameter message: The `Message` to write. - /// - Parameter allocator: An allocator to provide a `ByteBuffer` into which the message will be - /// written. mutating func write( _ message: ByteBuffer, - compressed: Bool, - allocator: ByteBufferAllocator - ) -> Result { + compressed: Bool + ) -> Result<(ByteBuffer, ByteBuffer?), MessageWriteError> { switch self { case .notWriting: return .failure(.cardinalityViolation) - case let .writing(writeArity, contentType, writer): - let buffer: ByteBuffer + case .writing(let writeArity, let contentType, var writer): + self = .notWriting + let buffers: (ByteBuffer, ByteBuffer?) do { - buffer = try writer.write(buffer: message, allocator: allocator, compressed: compressed) + buffers = try writer.write(buffer: message, compressed: compressed) } catch { self = .notWriting return .failure(.serializationFailed) @@ -84,7 +85,7 @@ enum WriteState { self = .writing(writeArity, contentType, writer) } - return .success(buffer) + return .success(buffers) } } } diff --git a/Tests/GRPCTests/GRPCClientStateMachineTests.swift b/Tests/GRPCTests/GRPCClientStateMachineTests.swift index 58e451d06..dc31e25cf 100644 --- a/Tests/GRPCTests/GRPCClientStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCClientStateMachineTests.swift @@ -38,8 +38,16 @@ class GRPCClientStateMachineTests: GRPCTestCase { func writeMessage(_ message: String) throws -> ByteBuffer { let buffer = self.allocator.buffer(string: message) - let writer = LengthPrefixedMessageWriter(compression: .none) - return try writer.write(buffer: buffer, allocator: self.allocator, compressed: false) + var writer = LengthPrefixedMessageWriter(compression: .none, allocator: .init()) + var (buffer1, buffer2) = try writer.write( + buffer: buffer, + compressed: false + ) + + if var buffer2 = buffer2 { + buffer1.writeBuffer(&buffer2) + } + return buffer1 } /// Writes a message into the given `buffer`. @@ -73,7 +81,7 @@ extension GRPCClientStateMachineTests { deadline: .distantFuture, customMetadata: [:], encoding: .disabled - )).assertFailure { + ), allocator: .init()).assertFailure { XCTAssertEqual($0, .invalidState) } } @@ -89,7 +97,7 @@ extension GRPCClientStateMachineTests { deadline: .distantFuture, customMetadata: [:], encoding: .disabled - )).assertSuccess() + ), allocator: .init()).assertSuccess() } func testSendRequestHeadersFromClientActiveServerIdle() { @@ -133,8 +141,7 @@ extension GRPCClientStateMachineTests { var stateMachine = self.makeStateMachine(state) stateMachine.sendRequest( ByteBuffer(string: "Hello!"), - compressed: false, - allocator: self.allocator + compressed: false ).assertFailure { XCTAssertEqual($0, expected) } @@ -146,10 +153,10 @@ extension GRPCClientStateMachineTests { let request = "Hello!" stateMachine.sendRequest( ByteBuffer(string: request), - compressed: false, - allocator: self.allocator - ).assertSuccess { buffer in - var buffer = buffer + compressed: false + ).assertSuccess { buffers in + var buffer = buffers.0 + XCTAssertNil(buffers.1) // Remove the length and compression flag prefix. buffer.moveReaderIndex(forwardBy: 5) let data = buffer.readString(length: buffer.readableBytes)! @@ -537,7 +544,7 @@ extension GRPCClientStateMachineTests { deadline: .distantFuture, customMetadata: [:], encoding: .disabled - )).assertSuccess() + ), allocator: .init()).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() @@ -545,8 +552,7 @@ extension GRPCClientStateMachineTests { // Send a request. stateMachine.sendRequest( ByteBuffer(string: "Hello!"), - compressed: false, - allocator: self.allocator + compressed: false ).assertSuccess() // Close the request stream. @@ -573,18 +579,15 @@ extension GRPCClientStateMachineTests { deadline: .distantFuture, customMetadata: [:], encoding: .disabled - )).assertSuccess() + ), allocator: .init()).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() // Send some requests. - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false, allocator: self.allocator) - .assertSuccess() - stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false, allocator: self.allocator) - .assertSuccess() - stateMachine.sendRequest(ByteBuffer(string: "3"), compressed: false, allocator: self.allocator) - .assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "3"), compressed: false).assertSuccess() // Close the request stream. stateMachine.sendEndOfRequestStream().assertSuccess() @@ -610,14 +613,13 @@ extension GRPCClientStateMachineTests { deadline: .distantFuture, customMetadata: [:], encoding: .disabled - )).assertSuccess() + ), allocator: .init()).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() // Send a request. - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false, allocator: self.allocator) - .assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() // Close the request stream. stateMachine.sendEndOfRequestStream().assertSuccess() @@ -648,24 +650,21 @@ extension GRPCClientStateMachineTests { deadline: .distantFuture, customMetadata: [:], encoding: .disabled - )).assertSuccess() + ), allocator: .init()).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() // Interleave requests and responses: - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false, allocator: self.allocator) - .assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() // Receive a response. var firstBuffer = try self.writeMessage("1") stateMachine.receiveResponseBuffer(&firstBuffer, maxMessageLength: .max).assertSuccess() // Send two more requests. - stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false, allocator: self.allocator) - .assertSuccess() - stateMachine.sendRequest(ByteBuffer(string: "3"), compressed: false, allocator: self.allocator) - .assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "3"), compressed: false).assertSuccess() // Receive two responses in one buffer. var secondBuffer = try self.writeMessage("2") @@ -691,15 +690,11 @@ extension GRPCClientStateMachineTests { )) // One is fine. - stateMachine - .sendRequest(ByteBuffer(string: "1"), compressed: false, allocator: self.allocator) - .assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() // Two is not. - stateMachine - .sendRequest(ByteBuffer(string: "2"), compressed: false, allocator: self.allocator) - .assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } + stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertFailure { + XCTAssertEqual($0, .cardinalityViolation) + } } } @@ -709,15 +704,11 @@ extension GRPCClientStateMachineTests { .makeStateMachine(.clientActiveServerActive(writeState: .one(), readState: readState)) // One is fine. - stateMachine - .sendRequest(ByteBuffer(string: "1"), compressed: false, allocator: self.allocator) - .assertSuccess() + stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() // Two is not. - stateMachine - .sendRequest(ByteBuffer(string: "2"), compressed: false, allocator: self.allocator) - .assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } + stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertFailure { + XCTAssertEqual($0, .cardinalityViolation) + } } } @@ -725,10 +716,9 @@ extension GRPCClientStateMachineTests { var stateMachine = self.makeStateMachine(.clientClosedServerClosed) // No requests allowed! - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false, allocator: self.allocator) - .assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } + stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertFailure { + XCTAssertEqual($0, .cardinalityViolation) + } } func testReceiveTooManyRequests() throws { @@ -782,7 +772,7 @@ extension GRPCClientStateMachineTests { acceptableForResponses: [.identity], decompressionLimit: .ratio(10) )) - )).assertSuccess { headers in + ), allocator: .init()).assertSuccess { headers in XCTAssertEqual(headers[":method"], ["POST"]) XCTAssertEqual(headers[":path"], ["/echo/Get"]) XCTAssertEqual(headers[":authority"], ["localhost"]) @@ -818,7 +808,7 @@ extension GRPCClientStateMachineTests { deadline: .distantFuture, customMetadata: customMetadata, encoding: .disabled - )).assertSuccess { headers in + ), allocator: .init()).assertSuccess { headers in // Pull out the entries we care about by matching values let filtered = headers.filter { _, value, _ in value == filterKey @@ -855,7 +845,7 @@ extension GRPCClientStateMachineTests { acceptableForResponses: [], decompressionLimit: .ratio(10) )) - )).assertSuccess { headers in + ), allocator: .init()).assertSuccess { headers in XCTAssertEqual(headers["user-agent"], ["test-user-agent"]) } } @@ -875,7 +865,7 @@ extension GRPCClientStateMachineTests { acceptableForResponses: [], decompressionLimit: .ratio(10) )) - )).assertSuccess { headers in + ), allocator: .init()).assertSuccess { headers in XCTAssertFalse(headers.contains(name: "grpc-encoding")) XCTAssertFalse(headers.contains(name: "grpc-accept-encoding")) } @@ -896,7 +886,7 @@ extension GRPCClientStateMachineTests { acceptableForResponses: [.identity, .gzip], decompressionLimit: .ratio(10) )) - )).assertSuccess { headers in + ), allocator: .init()).assertSuccess { headers in XCTAssertFalse(headers.contains(name: "grpc-encoding")) XCTAssertTrue(headers.contains(name: "grpc-accept-encoding")) } @@ -917,7 +907,7 @@ extension GRPCClientStateMachineTests { acceptableForResponses: [], decompressionLimit: .ratio(10) )) - )).assertSuccess { headers in + ), allocator: .init()).assertSuccess { headers in XCTAssertEqual(headers["grpc-encoding"], ["gzip"]) // This asymmetry is strange but allowed: if a client does not advertise support of the // compression it is using, the server may still process the message so long as it too @@ -1148,14 +1138,15 @@ class ReadStateTests: GRPCTestCase { func testReadWithLeftOverBytesForOneExpectedMessage() throws { // Write a message into the buffer: let message = ByteBuffer(string: "Hello!") - let writer = LengthPrefixedMessageWriter(compression: .none) - var buffer = try writer.write(buffer: message, allocator: self.allocator) + var writer = LengthPrefixedMessageWriter(compression: .none) + var buffers = try writer.write(buffer: message) + XCTAssertNil(buffers.1) // And some extra junk bytes: let bytes: [UInt8] = [0x00] - buffer.writeBytes(bytes) + buffers.0.writeBytes(bytes) var state: ReadState = .one() - state.readMessages(&buffer, maxLength: .max).assertFailure { + state.readMessages(&buffers.0, maxLength: .max).assertFailure { XCTAssertEqual($0, .leftOverBytes) } state.assertNotReading() @@ -1164,13 +1155,15 @@ class ReadStateTests: GRPCTestCase { func testReadTooManyMessagesForOneExpectedMessages() throws { // Write a message into the buffer twice: let message = ByteBuffer(string: "Hello!") - let writer = LengthPrefixedMessageWriter(compression: .none) - var buffer = try writer.write(buffer: message, allocator: self.allocator) - var second = try writer.write(buffer: message, allocator: self.allocator) - buffer.writeBuffer(&second) + var writer = LengthPrefixedMessageWriter(compression: .none) + var buffers1 = try writer.write(buffer: message) + var buffers2 = try writer.write(buffer: message) + XCTAssertNil(buffers1.1) + XCTAssertNil(buffers2.1) + buffers1.0.writeBuffer(&buffers2.0) var state: ReadState = .one() - state.readMessages(&buffer, maxLength: .max).assertFailure { + state.readMessages(&buffers1.0, maxLength: .max).assertFailure { XCTAssertEqual($0, .cardinalityViolation) } state.assertNotReading() @@ -1179,8 +1172,9 @@ class ReadStateTests: GRPCTestCase { func testReadOneMessageForOneExpectedMessages() throws { // Write a message into the buffer twice: let message = ByteBuffer(string: "Hello!") - let writer = LengthPrefixedMessageWriter(compression: .none) - var buffer = try writer.write(buffer: message, allocator: self.allocator) + var writer = LengthPrefixedMessageWriter(compression: .none) + var (buffer, other) = try writer.write(buffer: message) + XCTAssertNil(other) var state: ReadState = .one() state.readMessages(&buffer, maxLength: .max).assertSuccess { @@ -1194,8 +1188,9 @@ class ReadStateTests: GRPCTestCase { func testReadOneMessageForManyExpectedMessages() throws { // Write a message into the buffer twice: let message = ByteBuffer(string: "Hello!") - let writer = LengthPrefixedMessageWriter(compression: .none) - var buffer = try writer.write(buffer: message, allocator: self.allocator) + var writer = LengthPrefixedMessageWriter(compression: .none) + var (buffer, other) = try writer.write(buffer: message) + XCTAssertNil(other) var state: ReadState = .many() state.readMessages(&buffer, maxLength: .max).assertSuccess { @@ -1209,11 +1204,11 @@ class ReadStateTests: GRPCTestCase { func testReadManyMessagesForManyExpectedMessages() throws { // Write a message into the buffer twice: let message = ByteBuffer(string: "Hello!") - let writer = LengthPrefixedMessageWriter(compression: .none) + var writer = LengthPrefixedMessageWriter(compression: .none) - var first = try writer.write(buffer: message, allocator: self.allocator) - var second = try writer.write(buffer: message, allocator: self.allocator) - var third = try writer.write(buffer: message, allocator: self.allocator) + var (first, _) = try writer.write(buffer: message) + var (second, _) = try writer.write(buffer: message) + var (third, _) = try writer.write(buffer: message) first.writeBuffer(&second) first.writeBuffer(&third) diff --git a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift b/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift index 3317d2701..982866445 100644 --- a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift +++ b/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift @@ -616,7 +616,7 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { func testSendData() { for startingState in [DesiredState.requestOpenResponseOpen, .requestClosedResponseOpen] { - let machine = self.makeStateMachine(state: startingState) + var machine = self.makeStateMachine(state: startingState) let buffer = ByteBuffer(repeating: 0, count: 1024) // We should be able to do this multiple times. @@ -656,7 +656,7 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { } func testSendDataBeforeMetadata() { - let machine = self.makeStateMachine(state: .requestClosedResponseIdle(pipelineConfigured: true)) + var machine = self.makeStateMachine(state: .requestClosedResponseIdle(pipelineConfigured: true)) // Response stream is still idle, so this should fail. let buffer = ByteBuffer(repeating: 0, count: 1024) diff --git a/Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift b/Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift index 24789bc30..b7269ae5f 100644 --- a/Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift +++ b/Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift @@ -19,11 +19,12 @@ import XCTest class LengthPrefixedMessageWriterTests: GRPCTestCase { func testWriteBytesWithNoLeadingSpaceOrCompression() throws { - let writer = LengthPrefixedMessageWriter() + var writer = LengthPrefixedMessageWriter() let allocator = ByteBufferAllocator() let buffer = allocator.buffer(bytes: [1, 2, 3]) - var prefixed = try writer.write(buffer: buffer, allocator: allocator) + var (prefixed, other) = try writer.write(buffer: buffer) + XCTAssertNil(other) XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 0) XCTAssertEqual(prefixed.readInteger(as: UInt32.self), 3) XCTAssertEqual(prefixed.readBytes(length: 3), [1, 2, 3]) @@ -31,13 +32,14 @@ class LengthPrefixedMessageWriterTests: GRPCTestCase { } func testWriteBytesWithLeadingSpaceAndNoCompression() throws { - let writer = LengthPrefixedMessageWriter() + var writer = LengthPrefixedMessageWriter() let allocator = ByteBufferAllocator() var buffer = allocator.buffer(bytes: Array(repeating: 0, count: 5) + [1, 2, 3]) buffer.moveReaderIndex(forwardBy: 5) - var prefixed = try writer.write(buffer: buffer, allocator: allocator) + var (prefixed, other) = try writer.write(buffer: buffer) + XCTAssertNil(other) XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 0) XCTAssertEqual(prefixed.readInteger(as: UInt32.self), 3) XCTAssertEqual(prefixed.readBytes(length: 3), [1, 2, 3]) @@ -45,11 +47,12 @@ class LengthPrefixedMessageWriterTests: GRPCTestCase { } func testWriteBytesWithNoLeadingSpaceAndCompression() throws { - let writer = LengthPrefixedMessageWriter(compression: .gzip) + var writer = LengthPrefixedMessageWriter(compression: .gzip) let allocator = ByteBufferAllocator() let buffer = allocator.buffer(bytes: [1, 2, 3]) - var prefixed = try writer.write(buffer: buffer, allocator: allocator) + var (prefixed, other) = try writer.write(buffer: buffer) + XCTAssertNil(other) XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 1) let size = prefixed.readInteger(as: UInt32.self)! @@ -59,12 +62,13 @@ class LengthPrefixedMessageWriterTests: GRPCTestCase { } func testWriteBytesWithLeadingSpaceAndCompression() throws { - let writer = LengthPrefixedMessageWriter(compression: .gzip) + var writer = LengthPrefixedMessageWriter(compression: .gzip) let allocator = ByteBufferAllocator() var buffer = allocator.buffer(bytes: Array(repeating: 0, count: 5) + [1, 2, 3]) buffer.moveReaderIndex(forwardBy: 5) - var prefixed = try writer.write(buffer: buffer, allocator: allocator) + var (prefixed, other) = try writer.write(buffer: buffer) + XCTAssertNil(other) XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 1) let size = prefixed.readInteger(as: UInt32.self)! @@ -72,4 +76,34 @@ class LengthPrefixedMessageWriterTests: GRPCTestCase { XCTAssertNotNil(prefixed.readBytes(length: Int(size))) XCTAssertEqual(prefixed.readableBytes, 0) } + + func testLargeCompressedPayloadEmitsOneBuffer() throws { + var writer = LengthPrefixedMessageWriter(compression: .gzip) + let allocator = ByteBufferAllocator() + let message = ByteBuffer(repeating: 0, count: 16 * 1024 * 1024) + + var (lengthPrefixed, other) = try writer.write(buffer: message) + XCTAssertNil(other) + XCTAssertEqual(lengthPrefixed.readInteger(as: UInt8.self), 1) + let length = lengthPrefixed.readInteger(as: UInt32.self) + XCTAssertEqual(length, UInt32(lengthPrefixed.readableBytes)) + } + + func testLargeUncompressedPayloadEmitsTwoBuffers() throws { + var writer = LengthPrefixedMessageWriter(compression: .none) + let allocator = ByteBufferAllocator() + let message = ByteBuffer(repeating: 0, count: 16 * 1024 * 1024) + + var (header, payload) = try writer.write(buffer: message) + XCTAssertEqual(header.readInteger(as: UInt8.self), 0) + XCTAssertEqual(header.readInteger(as: UInt32.self), UInt32(message.readableBytes)) + XCTAssertEqual(header.readableBytes, 0) + XCTAssertEqual(payload, message) + } +} + +extension LengthPrefixedMessageWriter { + init(compression: CompressionAlgorithm? = nil) { + self.init(compression: compression, allocator: .init()) + } } From 524f06b0db8e9049af1f95e8187d5982f227a630 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 12 Dec 2022 10:39:54 +0000 Subject: [PATCH 049/580] Avoid CoWs (#1530) We didn't actually avoid a CoW here. We also don't need the 'modifying' workaround; the compiler is smarter now. --- Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift | 93 ++++--------------- 1 file changed, 19 insertions(+), 74 deletions(-) diff --git a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift index 235e285b4..e3daf435d 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift @@ -21,23 +21,6 @@ import NIOHTTP2 struct HTTP2ToRawGRPCStateMachine { /// The current state. private var state: State = .requestIdleResponseIdle - - /// Temporarily sets `self.state` to `._modifying` before calling the provided block and setting - /// `self.state` to the `State` modified by the block. - /// - /// Since we hold state as associated data on our `State` enum, any modification to that state - /// will trigger a copy on write for its heap allocated data. Temporarily setting the `self.state` - /// to `._modifying` allows us to avoid an extra reference to any heap allocated data and - /// therefore avoid a copy on write. - @inlinable - internal mutating func withStateAvoidingCoWs(_ body: (inout State) -> Action) -> Action { - var state: State = ._modifying - swap(&self.state, &state) - defer { - swap(&self.state, &state) - } - return body(&state) - } } extension HTTP2ToRawGRPCStateMachine { @@ -63,9 +46,6 @@ extension HTTP2ToRawGRPCStateMachine { // Both streams are closed. This state is terminal. case requestClosedResponseClosed - - // Not a real state. See 'withStateAvoidingCoWs'. - case _modifying } struct RequestOpenResponseIdleState { @@ -867,21 +847,19 @@ extension HTTP2ToRawGRPCStateMachine { encoding: ServerMessageEncoding, normalizeHeaders: Bool ) -> ReceiveHeadersAction { - return self.withStateAvoidingCoWs { state in - state.receive( - headers: headers, - eventLoop: eventLoop, - errorDelegate: errorDelegate, - remoteAddress: remoteAddress, - logger: logger, - allocator: allocator, - responseWriter: responseWriter, - closeFuture: closeFuture, - services: services, - encoding: encoding, - normalizeHeaders: normalizeHeaders - ) - } + return self.state.receive( + headers: headers, + eventLoop: eventLoop, + errorDelegate: errorDelegate, + remoteAddress: remoteAddress, + logger: logger, + allocator: allocator, + responseWriter: responseWriter, + closeFuture: closeFuture, + services: services, + encoding: encoding, + normalizeHeaders: normalizeHeaders + ) } /// Receive request buffer. @@ -890,16 +868,12 @@ extension HTTP2ToRawGRPCStateMachine { /// - endStream: Whether end stream was set. /// - Returns: Returns whether the caller should try to read a message from the buffer. mutating func receive(buffer: inout ByteBuffer, endStream: Bool) -> ReceiveDataAction { - return self.withStateAvoidingCoWs { state in - state.receive(buffer: &buffer, endStream: endStream) - } + self.state.receive(buffer: &buffer, endStream: endStream) } /// Send response headers. mutating func send(headers: HPACKHeaders) -> Result { - return self.withStateAvoidingCoWs { state in - state.send(headers: headers) - } + self.state.send(headers: headers) } /// Send a response buffer. @@ -908,9 +882,7 @@ extension HTTP2ToRawGRPCStateMachine { allocator: ByteBufferAllocator, compress: Bool ) -> Result<(ByteBuffer, ByteBuffer?), Error> { - return self.withStateAvoidingCoWs { state in - state.send(buffer: buffer, allocator: allocator, compress: compress) - } + self.state.send(buffer: buffer, allocator: allocator, compress: compress) } /// Send status and trailers. @@ -918,23 +890,17 @@ extension HTTP2ToRawGRPCStateMachine { status: GRPCStatus, trailers: HPACKHeaders ) -> HTTP2ToRawGRPCStateMachine.SendEndAction { - return self.withStateAvoidingCoWs { state in - state.send(status: status, trailers: trailers) - } + self.state.send(status: status, trailers: trailers) } /// The pipeline has been configured with a service provider. mutating func pipelineConfigured() -> PipelineConfiguredAction { - return self.withStateAvoidingCoWs { state in - state.pipelineConfigured() - } + self.state.pipelineConfigured() } /// Try to read a request message. mutating func readNextRequest(maxLength: Int) -> ReadNextMessageAction { - return self.withStateAvoidingCoWs { state in - state.readNextRequest(maxLength: maxLength) - } + self.state.readNextRequest(maxLength: maxLength) } } @@ -959,9 +925,6 @@ extension HTTP2ToRawGRPCStateMachine.State { .requestClosedResponseOpen, .requestClosedResponseClosed: preconditionFailure("Invalid state: response stream opened before pipeline was configured") - - case ._modifying: - preconditionFailure("Left in modifying state") } } @@ -1005,9 +968,6 @@ extension HTTP2ToRawGRPCStateMachine.State { .requestClosedResponseIdle, .requestClosedResponseOpen: preconditionFailure("Invalid state: \(self)") - - case ._modifying: - preconditionFailure("Left in modifying state") } } @@ -1049,9 +1009,6 @@ extension HTTP2ToRawGRPCStateMachine.State { case .requestClosedResponseClosed: return .nothing - - case ._modifying: - preconditionFailure("Left in modifying state") } } @@ -1085,9 +1042,6 @@ extension HTTP2ToRawGRPCStateMachine.State { case .requestOpenResponseClosed, .requestClosedResponseClosed: return .none - - case ._modifying: - preconditionFailure("Left in modifying state") } } @@ -1111,9 +1065,6 @@ extension HTTP2ToRawGRPCStateMachine.State { .requestClosedResponseOpen, .requestClosedResponseClosed: return .failure(GRPCError.AlreadyComplete()) - - case ._modifying: - preconditionFailure("Left in modifying state") } } @@ -1150,9 +1101,6 @@ extension HTTP2ToRawGRPCStateMachine.State { case .requestOpenResponseClosed, .requestClosedResponseClosed: return .failure(GRPCError.AlreadyComplete()) - - case ._modifying: - preconditionFailure("Left in modifying state") } } @@ -1183,9 +1131,6 @@ extension HTTP2ToRawGRPCStateMachine.State { case .requestOpenResponseClosed, .requestClosedResponseClosed: return .failure(GRPCError.AlreadyComplete()) - - case ._modifying: - preconditionFailure("Left in modifying state") } } } From 4312579fc8020f658bce6d19b4b91a2406f09f63 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 13 Dec 2022 10:40:14 +0000 Subject: [PATCH 050/580] Delay client async writer starting (#1531) Motivation: It's possible for async streaming clients to fail and drop messages in some situations. The situation leading to this happens because streaming calls set up state and then write out headers. While setting up state the HTTP/2 stream channel is configured, when it becomes active gRPC calls outs to enable the async writer to start emitting writes. This can happen before the headers are written so if a write is already pending then it can race the headers being written. If the message is written first then the write promise is failed and the message is dropped. Modifications: Delay letting the async writer emit writes until the headers have been written. Result: Correct ordering is enforced. --- .../GRPC/Interceptor/ClientTransport.swift | 5 ++- Tests/GRPCTests/ClientCallTests.swift | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/Sources/GRPC/Interceptor/ClientTransport.swift b/Sources/GRPC/Interceptor/ClientTransport.swift index 7bef9f9ee..810e2a698 100644 --- a/Sources/GRPC/Interceptor/ClientTransport.swift +++ b/Sources/GRPC/Interceptor/ClientTransport.swift @@ -337,7 +337,6 @@ extension ClientTransport { self._pipeline?.logger = self.logger self.logger.debug("activated stream channel") self.channel = channel - self.onStart() self.unbuffer() case .close: @@ -943,6 +942,10 @@ extension ClientTransport { case let .metadata(headers): let head = self.makeRequestHead(with: headers) channel.write(self.wrapOutboundOut(.head(head)), promise: promise) + // Messages are buffered by this class and in the async writer for async calls. Initially the + // async writer is not allowed to emit messages; the call to 'onStart()' signals that messages + // may be emitted. We call it here to avoid races between writing headers and messages. + self.onStart() case let .message(request, metadata): do { diff --git a/Tests/GRPCTests/ClientCallTests.swift b/Tests/GRPCTests/ClientCallTests.swift index 0a560c3ce..f01f9378f 100644 --- a/Tests/GRPCTests/ClientCallTests.swift +++ b/Tests/GRPCTests/ClientCallTests.swift @@ -206,4 +206,43 @@ class ClientCallTests: GRPCTestCase { // Cancellation should now fail, we've already cancelled. assertThat(try get.cancel().wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self))) } + + func testWriteMessageOnStart() throws { + // This test isn't deterministic so run a bunch of iterations. + for _ in 0 ..< 100 { + let call = self.update() + let promise = call.eventLoop.makePromise(of: Void.self) + let finished = call.eventLoop.makePromise(of: Void.self) + + call.invokeStreamingRequests { + // Send in onStart. + call.send( + .message(.with { $0.text = "foo" }, .init(compress: false, flush: false)), + promise: promise + ) + } onError: { _ in // ignore errors + } onResponsePart: { + switch $0 { + case .metadata, .message: + () + case .end: + finished.succeed(()) + } + } + + // End the stream. + promise.futureResult.whenComplete { _ in + call.send(.end, promise: nil) + } + + do { + try promise.futureResult.wait() + try finished.futureResult.wait() + } catch { + // Stop on the first error. + XCTFail("Unexpected error: \(error)") + return + } + } + } } From 6bf35472cb8621481e59e6d1450b19627db81cea Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 13 Dec 2022 16:27:05 +0000 Subject: [PATCH 051/580] Bump version number to 1.13.1 (#1532) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.13.1 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 7307e842a..807a96120 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 13 /// The patch version. - internal static let patch = 0 + internal static let patch = 1 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From d4a58a318183961aa01b538fca8d5a43d88a8484 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 15 Dec 2022 17:32:46 +0000 Subject: [PATCH 052/580] Avoid a CoW in the client read state (#1533) Motivation: In some Swift versions the reader is CoW'd when appending in one of the client state machines substates. Modifications: - Temporarily switch state before appending to the reader Result: Fewer CoWs --- Sources/GRPC/ReadWriteStates.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/GRPC/ReadWriteStates.swift b/Sources/GRPC/ReadWriteStates.swift index 1a8034505..cdef94f75 100644 --- a/Sources/GRPC/ReadWriteStates.swift +++ b/Sources/GRPC/ReadWriteStates.swift @@ -150,6 +150,7 @@ enum ReadState { return .failure(.cardinalityViolation) case .reading(let readArity, var reader): + self = .notReading // Avoid CoWs reader.append(buffer: &buffer) var messages: [ByteBuffer] = [] From 9fc6ead754a30fa8c70113a034b82631d62d1ad0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Dec 2022 08:40:58 +0000 Subject: [PATCH 053/580] Don't ack pings twice (#1534) Motivation: gRPC Swift is emitting two acks per ping. NIOHTTP2 is emitting one and the keepalive handler is emitting the other. Modifications: - Don't emit ping acks from the keep alive handler; just let the H2 handler do it. Result: - No unnecessary ping acks are emitted. - Resolves #1520 --- Sources/GRPC/GRPCIdleHandler.swift | 4 + Sources/GRPC/GRPCKeepaliveHandlers.swift | 12 +- Tests/GRPCTests/GRPCPingHandlerTests.swift | 124 ++++++++++++++++----- 3 files changed, 106 insertions(+), 34 deletions(-) diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index b2b87e7af..11c82a075 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -184,6 +184,10 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { case .none: () + case .ack: + // NIO's HTTP2 handler acks for us so this is a no-op. + () + case .cancelScheduledTimeout: self.scheduledClose?.cancel() self.scheduledClose = nil diff --git a/Sources/GRPC/GRPCKeepaliveHandlers.swift b/Sources/GRPC/GRPCKeepaliveHandlers.swift index d22d182ab..3fa66ed75 100644 --- a/Sources/GRPC/GRPCKeepaliveHandlers.swift +++ b/Sources/GRPC/GRPCKeepaliveHandlers.swift @@ -90,6 +90,7 @@ struct PingHandler { enum Action { case none + case ack case schedulePing(delay: TimeAmount, timeout: TimeAmount) case cancelScheduledTimeout case reply(HTTP2Frame.FramePayload) @@ -170,14 +171,14 @@ struct PingHandler { // This is a valid ping, reset our strike count and reply with a pong. self.pingStrikes = 0 self.lastReceivedPingDate = self.now() - return .reply(self.generatePingFrame(data: pingData, ack: true)) + return .ack } } else { // We don't support ping strikes. We'll just reply with a pong. // // Note: we don't need to update `pingStrikes` or `lastReceivedPingDate` as we don't // support ping strikes. - return .reply(self.generatePingFrame(data: pingData, ack: true)) + return .ack } } @@ -185,20 +186,19 @@ struct PingHandler { if self.shouldBlockPing { return .none } else { - return .reply(self.generatePingFrame(data: self.pingData, ack: false)) + return .reply(self.generatePingFrame(data: self.pingData)) } } private mutating func generatePingFrame( - data: HTTP2PingData, - ack: Bool + data: HTTP2PingData ) -> HTTP2Frame.FramePayload { if self.activeStreams == 0 { self.sentPingsWithoutData += 1 } self.lastSentPingDate = self.now() - return HTTP2Frame.FramePayload.ping(data, ack: ack) + return HTTP2Frame.FramePayload.ping(data, ack: false) } /// Returns true if, on receipt of a ping, the ping should be regarded as a ping strike. diff --git a/Tests/GRPCTests/GRPCPingHandlerTests.swift b/Tests/GRPCTests/GRPCPingHandlerTests.swift index a959962d7..6d86ef0b2 100644 --- a/Tests/GRPCTests/GRPCPingHandlerTests.swift +++ b/Tests/GRPCTests/GRPCPingHandlerTests.swift @@ -15,6 +15,7 @@ */ @testable import GRPC import NIOCore +import NIOEmbedded import NIOHTTP2 import XCTest @@ -249,24 +250,15 @@ class GRPCPingHandlerTests: GRPCTestCase { pingData: HTTP2PingData(withInteger: 1), ack: false ) - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)) - ) + XCTAssertEqual(response, .ack) // Received another ping, response should be a pong (ping strikes not in effect) response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)) - ) + XCTAssertEqual(response, .ack) // Received another ping, response should be a pong (ping strikes not in effect) response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)) - ) + XCTAssertEqual(response, .ack) } func testPingWithoutDataResultsInPongForClient() { @@ -274,10 +266,7 @@ class GRPCPingHandlerTests: GRPCTestCase { self.setupPingHandler(permitWithoutCalls: false) let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual( - action, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)) - ) + XCTAssertEqual(action, .ack) } func testPingWithoutDataResultsInPongForServer() { @@ -291,10 +280,7 @@ class GRPCPingHandlerTests: GRPCTestCase { ) let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual( - action, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)) - ) + XCTAssertEqual(action, .ack) } func testPingStrikesOnServer() { @@ -312,10 +298,7 @@ class GRPCPingHandlerTests: GRPCTestCase { pingData: HTTP2PingData(withInteger: 1), ack: false ) - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)) - ) + XCTAssertEqual(response, .ack) // Received another ping, which is invalid (ping strike), response should be no action response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) @@ -326,10 +309,7 @@ class GRPCPingHandlerTests: GRPCTestCase { // Received another ping, which is valid now, response should be a pong response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: true)) - ) + XCTAssertEqual(response, .ack) // Received another ping, which is invalid (ping strike), response should be no action response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) @@ -381,6 +361,8 @@ extension PingHandler.Action: Equatable { switch (lhs, rhs) { case (.none, .none): return true + case (.ack, .ack): + return true case (let .schedulePing(lhsDelay, lhsTimeout), let .schedulePing(rhsDelay, rhsTimeout)): return lhsDelay == rhsDelay && lhsTimeout == rhsTimeout case (.cancelScheduledTimeout, .cancelScheduledTimeout): @@ -401,3 +383,89 @@ extension PingHandler.Action: Equatable { } } } + +extension GRPCPingHandlerTests { + func testSingleAckIsEmittedOnPing() throws { + let client = EmbeddedChannel() + let _ = try client.configureHTTP2Pipeline(mode: .client) { _ in + fatalError("Unexpected inbound stream") + }.wait() + + let server = EmbeddedChannel() + let serverMux = try server.configureHTTP2Pipeline(mode: .server) { _ in + fatalError("Unexpected inbound stream") + }.wait() + + let idleHandler = GRPCIdleHandler( + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.serverLogger + ) + try server.pipeline.syncOperations.addHandler(idleHandler, position: .before(serverMux)) + try server.connect(to: .init(unixDomainSocketPath: "/ignored")).wait() + try client.connect(to: .init(unixDomainSocketPath: "/ignored")).wait() + + func interact(client: EmbeddedChannel, server: EmbeddedChannel) throws { + var didRead = true + while didRead { + didRead = false + + if let data = try client.readOutbound(as: ByteBuffer.self) { + didRead = true + try server.writeInbound(data) + } + + if let data = try server.readOutbound(as: ByteBuffer.self) { + didRead = true + try client.writeInbound(data) + } + } + } + + try interact(client: client, server: server) + + // Settings. + let f1 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self)) + f1.payload.assertSettings(ack: false) + + // Settings ack. + let f2 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self)) + f2.payload.assertSettings(ack: true) + + // Send a ping. + let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(.init(withInteger: 42), ack: false)) + try client.writeOutbound(ping) + try interact(client: client, server: server) + + // Ping ack. + let f3 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self)) + f3.payload.assertPing(ack: true) + + XCTAssertNil(try client.readInbound(as: HTTP2Frame.self)) + } +} + +extension HTTP2Frame.FramePayload { + func assertSettings(ack: Bool, file: StaticString = #file, line: UInt = #line) { + switch self { + case let .settings(settings): + switch settings { + case .ack: + XCTAssertTrue(ack, file: file, line: line) + case .settings: + XCTAssertFalse(ack, file: file, line: line) + } + default: + XCTFail("Expected .settings got \(self)", file: file, line: line) + } + } + + func assertPing(ack: Bool, file: StaticString = #file, line: UInt = #line) { + switch self { + case let .ping(_, ack: pingAck): + XCTAssertEqual(pingAck, ack, file: file, line: line) + default: + XCTFail("Expected .ping got \(self)", file: file, line: line) + } + } +} From 2dc64986c60d96df6eb3b470616c3a6b16585698 Mon Sep 17 00:00:00 2001 From: Peter Adams <63288215+PeterAdams-A@users.noreply.github.com> Date: Mon, 19 Dec 2022 15:02:46 +0000 Subject: [PATCH 054/580] Allow Swift Package Index to generate documentation (#1535) Motivation: Having all swift documentation available in one place makes a better user experience. Modifications: Add a .spi.yml to describe what documentation to generate. Result: Swift Package Index will generate documentation. --- .spi.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .spi.yml diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 000000000..d0a8efb16 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [GRPC] From 829a936e81afd0e8c41c8f6fb105211bb00068c3 Mon Sep 17 00:00:00 2001 From: Peter Adams <63288215+PeterAdams-A@users.noreply.github.com> Date: Tue, 20 Dec 2022 09:41:12 +0000 Subject: [PATCH 055/580] Add protoc-gen-grpc-swift to Swift Package Index documentation (#1536) Motivation: This project also contains useful documentation which should be exposed to users. Modifications: Add the project to the list in .spi.yml Result: Swift Package Index will publish extra documentation. --- .spi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.spi.yml b/.spi.yml index d0a8efb16..b734eb1e6 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [GRPC] + - documentation_targets: [GRPC, protoc-gen-grpc-swift] From e71050720bd710b398cbeb9b203e1654db05dfd0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Dec 2022 11:34:48 +0000 Subject: [PATCH 056/580] Add a coalescing writer (#1537) Motivation: Creating frames in HTTP/2 isn't free: they incur one allocation for the frame payload and another for the data buffer. In addition to this HTTP/2 currently creates a promise per write, so we also have an additional write promise. On top of the allocations we have the repeated work for processing extra frames. We can recover much of this cost by coalescing writes before a flush. For streaming RPCs this can have a fairly significant impact. Modifications: - Add a coalescing length prefixed writer: serialized messages are appended and length prefixing (and compression) is done lazily on calls to `next()`. - Add an internal 'one-or-many' queue which is a FIFO queue backed by either a single element on the stack or a deque. This avoids allocating a queue on the heap for the length prefixed writer when there is only one message appended before flushing. Result: Nothing changes yet, but we have a writer in place which we can adopt later. --- ...oalescingLengthPrefixedMessageWriter.swift | 357 ++++++++++++++++++ ...cingLengthPrefixedMessageWriterTests.swift | 221 +++++++++++ Tests/GRPCTests/OneOrManyQueueTests.swift | 138 +++++++ 3 files changed, 716 insertions(+) create mode 100644 Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift create mode 100644 Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift create mode 100644 Tests/GRPCTests/OneOrManyQueueTests.swift diff --git a/Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift b/Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift new file mode 100644 index 000000000..f36bb5ad8 --- /dev/null +++ b/Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift @@ -0,0 +1,357 @@ +/* + * Copyright 2022, 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 DequeModule +import NIOCore + +internal struct CoalescingLengthPrefixedMessageWriter { + /// Length of the gRPC message header (1 compression byte, 4 bytes for the length). + static let metadataLength = 5 + + /// Message size above which we emit two buffers: one containing the header and one with the + /// actual message bytes. At or below the limit we copy the message into a new buffer containing + /// both the header and the message. + /// + /// Using two buffers avoids expensive copies of large messages. For smaller messages the copy + /// is cheaper than the additional allocations and overhead required to send an extra HTTP/2 DATA + /// frame. + /// + /// The value of 16k was chosen empirically. We subtract the length of the message header + /// as `ByteBuffer` reserve capacity in powers of two and want to avoid overallocating. + static let singleBufferSizeLimit = 16384 - Self.metadataLength + + /// The compression algorithm to use, if one should be used. + private let compression: CompressionAlgorithm? + /// Any compressor associated with the compression algorithm. + private let compressor: Zlib.Deflate? + + /// Whether the compression message flag should be set. + private var supportsCompression: Bool { + return self.compression != nil + } + + /// A scratch buffer that we encode messages into: if the buffer isn't held elsewhere then we + /// can avoid having to allocate a new one. + private var scratch: ByteBuffer + + /// Outbound buffers waiting to be written. + private var pending: OneOrManyQueue + + private struct Pending { + var buffer: ByteBuffer + var promise: EventLoopPromise? + var compress: Bool + + init(buffer: ByteBuffer, compress: Bool, promise: EventLoopPromise?) { + self.buffer = buffer + self.promise = promise + self.compress = compress + } + + var isSmallEnoughToCoalesce: Bool { + let limit = CoalescingLengthPrefixedMessageWriter.singleBufferSizeLimit + return self.buffer.readableBytes <= limit + } + + var shouldCoalesce: Bool { + return self.isSmallEnoughToCoalesce || self.compress + } + } + + private enum State { + // Coalescing pending messages. + case coalescing + // Emitting a non-coalesced message; the header has been written, the body + // needs to be written next. + case emittingLargeFrame(ByteBuffer, EventLoopPromise?) + } + + private var state: State + + init(compression: CompressionAlgorithm? = nil, allocator: ByteBufferAllocator) { + self.compression = compression + self.scratch = allocator.buffer(capacity: 0) + self.state = .coalescing + self.pending = .init() + + switch self.compression?.algorithm { + case .none, .some(.identity): + self.compressor = nil + case .some(.deflate): + self.compressor = Zlib.Deflate(format: .deflate) + case .some(.gzip): + self.compressor = Zlib.Deflate(format: .gzip) + } + } + + /// Append a serialized message buffer to the writer. + mutating func append(buffer: ByteBuffer, compress: Bool, promise: EventLoopPromise?) { + let pending = Pending( + buffer: buffer, + compress: compress && self.supportsCompression, + promise: promise + ) + + self.pending.append(pending) + } + + /// Return a tuple of the next buffer to write and its associated write promise. + mutating func next() -> (Result, EventLoopPromise?)? { + switch self.state { + case .coalescing: + // Nothing pending: exit early. + if self.pending.isEmpty { + return nil + } + + // First up we need to work out how many elements we're going to pop off the front + // and coalesce. + // + // At the same time we'll compute how much capacity we'll need in the buffer and cascade + // their promises. + var messagesToCoalesce = 0 + var requiredCapacity = 0 + var promise: EventLoopPromise? + + for element in self.pending { + if !element.shouldCoalesce { + break + } + + messagesToCoalesce &+= 1 + requiredCapacity += element.buffer.readableBytes + Self.metadataLength + if let existing = promise { + existing.futureResult.cascade(to: element.promise) + } else { + promise = element.promise + } + } + + if messagesToCoalesce == 0 { + // Nothing to coalesce; this means the first element should be emitted with its header in + // a separate buffer. Note: the force unwrap is okay here: we early exit if `self.pending` + // is empty. + let pending = self.pending.pop()! + + // Set the scratch buffer to just be a message header then store the message bytes. + self.scratch.clear(minimumCapacity: Self.metadataLength) + self.scratch.writeMultipleIntegers(UInt8(0), UInt32(pending.buffer.readableBytes)) + self.state = .emittingLargeFrame(pending.buffer, pending.promise) + return (.success(self.scratch), nil) + } else { + self.scratch.clear(minimumCapacity: requiredCapacity) + + // Drop and encode the messages. + while messagesToCoalesce > 0, let next = self.pending.pop() { + messagesToCoalesce &-= 1 + do { + try self.encode(next.buffer, compress: next.compress) + } catch { + return (.failure(error), promise) + } + } + + return (.success(self.scratch), promise) + } + + case let .emittingLargeFrame(buffer, promise): + // We just emitted the header, now emit the body. + self.state = .coalescing + return (.success(buffer), promise) + } + } + + private mutating func encode(_ buffer: ByteBuffer, compress: Bool) throws { + if let compressor = self.compressor, compress { + try self.encode(buffer, compressor: compressor) + } else { + try self.encode(buffer) + } + } + + private mutating func encode(_ buffer: ByteBuffer, compressor: Zlib.Deflate) throws { + // Set the compression byte. + self.scratch.writeInteger(UInt8(1)) + // Set the length to zero; we'll write the actual value in a moment. + let payloadSizeIndex = self.scratch.writerIndex + self.scratch.writeInteger(UInt32(0)) + + let bytesWritten: Int + do { + var buffer = buffer + bytesWritten = try compressor.deflate(&buffer, into: &self.scratch) + } catch { + throw error + } + + self.scratch.setInteger(UInt32(bytesWritten), at: payloadSizeIndex) + + // Finally, the compression context should be reset between messages. + compressor.reset() + } + + private mutating func encode(_ buffer: ByteBuffer) throws { + self.scratch.writeMultipleIntegers(UInt8(0), UInt32(buffer.readableBytes)) + self.scratch.writeImmutableBuffer(buffer) + } +} + +/// A FIFO-queue which allows for a single to be stored on the stack and defers to a +/// heap-implementation if further elements are added. +/// +/// This is useful when optimising for unary streams where avoiding the cost of a heap +/// allocation is desirable. +internal struct OneOrManyQueue: Collection { + private var backing: Backing + + private enum Backing: Collection { + case none + case one(Element) + case many(Deque) + + var startIndex: Int { + switch self { + case .none, .one: + return 0 + case let .many(elements): + return elements.startIndex + } + } + + var endIndex: Int { + switch self { + case .none: + return 0 + case .one: + return 1 + case let .many(elements): + return elements.endIndex + } + } + + subscript(index: Int) -> Element { + switch self { + case .none: + fatalError("Invalid index") + case let .one(element): + assert(index == 0) + return element + case let .many(elements): + return elements[index] + } + } + + func index(after index: Int) -> Int { + switch self { + case .none: + return 0 + case .one: + return 1 + case let .many(elements): + return elements.index(after: index) + } + } + + var count: Int { + switch self { + case .none: + return 0 + case .one: + return 1 + case let .many(elements): + return elements.count + } + } + + var isEmpty: Bool { + switch self { + case .none: + return true + case .one: + return false + case let .many(elements): + return elements.isEmpty + } + } + + mutating func append(_ element: Element) { + switch self { + case .none: + self = .one(element) + case let .one(one): + var elements = Deque() + elements.reserveCapacity(16) + elements.append(one) + elements.append(element) + self = .many(elements) + case var .many(elements): + self = .none + elements.append(element) + self = .many(elements) + } + } + + mutating func pop() -> Element? { + switch self { + case .none: + return nil + case let .one(element): + self = .none + return element + case var .many(many): + self = .none + let element = many.popFirst() + self = .many(many) + return element + } + } + } + + init() { + self.backing = .none + } + + var isEmpty: Bool { + return self.backing.isEmpty + } + + var count: Int { + return self.backing.count + } + + var startIndex: Int { + return self.backing.startIndex + } + + var endIndex: Int { + return self.backing.endIndex + } + + subscript(index: Int) -> Element { + return self.backing[index] + } + + func index(after index: Int) -> Int { + return self.backing.index(after: index) + } + + mutating func append(_ element: Element) { + self.backing.append(element) + } + + mutating func pop() -> Element? { + return self.backing.pop() + } +} diff --git a/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift b/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift new file mode 100644 index 000000000..878aed9c9 --- /dev/null +++ b/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift @@ -0,0 +1,221 @@ +/* + * Copyright 2022, 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. + */ +@testable import GRPC +import NIOCore +import NIOEmbedded +import XCTest + +internal final class CoalescingLengthPrefixedMessageWriterTests: GRPCTestCase { + private let loop = EmbeddedEventLoop() + + private func makeWriter( + compression: CompressionAlgorithm? = .none + ) -> CoalescingLengthPrefixedMessageWriter { + return .init(compression: compression, allocator: .init()) + } + + private func testSingleSmallWrite(withPromise: Bool) throws { + var writer = self.makeWriter() + + let promise = withPromise ? self.loop.makePromise(of: Void.self) : nil + writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: promise) + + let (result, maybePromise) = try XCTUnwrap(writer.next()) + try result.assertValue { buffer in + var buffer = buffer + let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compressed) + XCTAssertEqual(length, UInt32(ByteBuffer.smallEnoughToCoalesce.readableBytes)) + XCTAssertEqual(buffer.readSlice(length: Int(length)), .smallEnoughToCoalesce) + XCTAssertEqual(buffer.readableBytes, 0) + } + + // No more bufers. + XCTAssertNil(writer.next()) + + if withPromise { + XCTAssertNotNil(maybePromise) + } else { + XCTAssertNil(maybePromise) + } + + // Don't leak the promise. + maybePromise?.succeed(()) + } + + private func testMultipleSmallWrites(withPromise: Bool) throws { + var writer = self.makeWriter() + let messages = 100 + + for _ in 0 ..< messages { + let promise = withPromise ? self.loop.makePromise(of: Void.self) : nil + writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: promise) + } + + let (result, maybePromise) = try XCTUnwrap(writer.next()) + try result.assertValue { buffer in + var buffer = buffer + + // Read all the messages. + for _ in 0 ..< messages { + let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compressed) + XCTAssertEqual(length, UInt32(ByteBuffer.smallEnoughToCoalesce.readableBytes)) + XCTAssertEqual(buffer.readSlice(length: Int(length)), .smallEnoughToCoalesce) + } + + XCTAssertEqual(buffer.readableBytes, 0) + } + + // No more bufers. + XCTAssertNil(writer.next()) + + if withPromise { + XCTAssertNotNil(maybePromise) + } else { + XCTAssertNil(maybePromise) + } + + // Don't leak the promise. + maybePromise?.succeed(()) + } + + func testSingleSmallWriteWithPromise() throws { + try self.testSingleSmallWrite(withPromise: true) + } + + func testSingleSmallWriteWithoutPromise() throws { + try self.testSingleSmallWrite(withPromise: false) + } + + func testMultipleSmallWriteWithPromise() throws { + try self.testMultipleSmallWrites(withPromise: true) + } + + func testMultipleSmallWriteWithoutPromise() throws { + try self.testMultipleSmallWrites(withPromise: false) + } + + func testSingleLargeMessage() throws { + var writer = self.makeWriter() + writer.append(buffer: .tooBigToCoalesce, compress: false, promise: nil) + + let (result1, promise1) = try XCTUnwrap(writer.next()) + XCTAssertNil(promise1) + try result1.assertValue { buffer in + var buffer = buffer + let (compress, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compress) + XCTAssertEqual(Int(length), ByteBuffer.tooBigToCoalesce.readableBytes) + XCTAssertEqual(buffer.readableBytes, 0) + } + + let (result2, promise2) = try XCTUnwrap(writer.next()) + XCTAssertNil(promise2) + result2.assertValue { buffer in + XCTAssertEqual(buffer, .tooBigToCoalesce) + } + + XCTAssertNil(writer.next()) + } + + func testMessagesBeforeLargeAreCoalesced() throws { + var writer = self.makeWriter() + // First two should be coalesced. The third should be split as two buffers. + writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: nil) + writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: nil) + writer.append(buffer: .tooBigToCoalesce, compress: false, promise: nil) + + let (result1, _) = try XCTUnwrap(writer.next()) + try result1.assertValue { buffer in + var buffer = buffer + for _ in 0 ..< 2 { + let (compress, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compress) + XCTAssertEqual(Int(length), ByteBuffer.smallEnoughToCoalesce.readableBytes) + XCTAssertEqual(buffer.readSlice(length: Int(length)), .smallEnoughToCoalesce) + } + XCTAssertEqual(buffer.readableBytes, 0) + } + + let (result2, _) = try XCTUnwrap(writer.next()) + try result2.assertValue { buffer in + var buffer = buffer + let (compress, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compress) + XCTAssertEqual(Int(length), ByteBuffer.tooBigToCoalesce.readableBytes) + XCTAssertEqual(buffer.readableBytes, 0) + } + + let (result3, _) = try XCTUnwrap(writer.next()) + result3.assertValue { buffer in + XCTAssertEqual(buffer, .tooBigToCoalesce) + } + + XCTAssertNil(writer.next()) + } + + func testCompressedMessagesAreAlwaysCoalesced() throws { + var writer = self.makeWriter(compression: .gzip) + writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: nil) + writer.append(buffer: .tooBigToCoalesce, compress: true, promise: nil) + + let (result, _) = try XCTUnwrap(writer.next()) + try result.assertValue { buffer in + var buffer = buffer + + let (compress1, length1) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compress1) + XCTAssertEqual(Int(length1), ByteBuffer.smallEnoughToCoalesce.readableBytes) + XCTAssertEqual(buffer.readSlice(length: Int(length1)), .smallEnoughToCoalesce) + + let (compress2, length2) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertTrue(compress2) + // Can't assert the length or the content, only that the length must be equal + // to the number of remaining bytes. + XCTAssertEqual(Int(length2), buffer.readableBytes) + } + + XCTAssertNil(writer.next()) + } +} + +extension Result { + func assertValue(_ body: (Success) throws -> Void) rethrows { + switch self { + case let .success(success): + try body(success) + case let .failure(error): + XCTFail("Unexpected failure: \(error)") + } + } +} + +extension ByteBuffer { + fileprivate static let smallEnoughToCoalesce = Self(repeating: 42, count: 128) + fileprivate static let tooBigToCoalesce = Self( + repeating: 42, + count: CoalescingLengthPrefixedMessageWriter.singleBufferSizeLimit + 1 + ) + + mutating func readMessageHeader() -> (Bool, UInt32)? { + if let (compressed, length) = self.readMultipleIntegers(as: (UInt8, UInt32).self) { + return (compressed != 0, length) + } else { + return nil + } + } +} diff --git a/Tests/GRPCTests/OneOrManyQueueTests.swift b/Tests/GRPCTests/OneOrManyQueueTests.swift new file mode 100644 index 000000000..d68be6f71 --- /dev/null +++ b/Tests/GRPCTests/OneOrManyQueueTests.swift @@ -0,0 +1,138 @@ +/* + * Copyright 2022, 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. + */ +@testable import GRPC +import XCTest + +internal final class OneOrManyQueueTests: GRPCTestCase { + func testIsEmpty() { + XCTAssertTrue(OneOrManyQueue().isEmpty) + } + + func testIsEmptyManyBacked() { + XCTAssertTrue(OneOrManyQueue.manyBacked.isEmpty) + } + + func testCount() { + var queue = OneOrManyQueue() + XCTAssertEqual(queue.count, 0) + queue.append(1) + XCTAssertEqual(queue.count, 1) + } + + func testCountManyBacked() { + var manyBacked = OneOrManyQueue.manyBacked + XCTAssertEqual(manyBacked.count, 0) + for i in 1 ... 100 { + manyBacked.append(1) + XCTAssertEqual(manyBacked.count, i) + } + } + + func testAppendAndPop() { + var queue = OneOrManyQueue() + XCTAssertNil(queue.pop()) + + queue.append(1) + XCTAssertEqual(queue.count, 1) + XCTAssertEqual(queue.pop(), 1) + + XCTAssertNil(queue.pop()) + XCTAssertEqual(queue.count, 0) + XCTAssertTrue(queue.isEmpty) + } + + func testAppendAndPopManyBacked() { + var manyBacked = OneOrManyQueue.manyBacked + XCTAssertNil(manyBacked.pop()) + + manyBacked.append(1) + XCTAssertEqual(manyBacked.count, 1) + manyBacked.append(2) + XCTAssertEqual(manyBacked.count, 2) + + XCTAssertEqual(manyBacked.pop(), 1) + XCTAssertEqual(manyBacked.count, 1) + + XCTAssertEqual(manyBacked.pop(), 2) + XCTAssertEqual(manyBacked.count, 0) + + XCTAssertNil(manyBacked.pop()) + XCTAssertTrue(manyBacked.isEmpty) + } + + func testIndexes() { + var queue = OneOrManyQueue() + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, 0) + + // Non-empty. + queue.append(1) + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, 1) + } + + func testIndexesManyBacked() { + var queue = OneOrManyQueue.manyBacked + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, 0) + + for i in 1 ... 100 { + queue.append(i) + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, i) + } + } + + func testIndexAfter() { + var queue = OneOrManyQueue() + XCTAssertEqual(queue.startIndex, queue.endIndex) + XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) + + queue.append(1) + XCTAssertNotEqual(queue.startIndex, queue.endIndex) + XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) + } + + func testSubscript() throws { + var queue = OneOrManyQueue() + queue.append(42) + let index = try XCTUnwrap(queue.firstIndex(of: 42)) + XCTAssertEqual(queue[index], 42) + } + + func testSubscriptManyBacked() throws { + var queue = OneOrManyQueue.manyBacked + for i in 0 ... 100 { + queue.append(i) + } + + for i in 0 ... 100 { + XCTAssertEqual(queue[i], i) + } + } +} + +extension OneOrManyQueue where Element == Int { + static var manyBacked: Self { + var queue = OneOrManyQueue() + // Append and pop to move to the 'many' backing. + queue.append(1) + queue.append(2) + XCTAssertEqual(queue.pop(), 1) + XCTAssertEqual(queue.pop(), 2) + return queue + } +} From ff0b37462c6f5a0f14d3fbb6fb1c1f8f77d8ede7 Mon Sep 17 00:00:00 2001 From: Oliver Epper <45991777+oliverepper@users.noreply.github.com> Date: Tue, 3 Jan 2023 16:31:44 +0100 Subject: [PATCH 057/580] update swift-nio-ssl dependency (#1541) --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 406a560ad..8653027ac 100644 --- a/Package.swift +++ b/Package.swift @@ -67,7 +67,7 @@ let packageDependencies: [Package.Dependency] = [ ].appending( .package( url: "https://github.com/apple/swift-nio-ssl.git", - from: "2.14.0" + from: "2.23.0" ), if: includeNIOSSL ) From ff24d24aa3c8d9dbf9db1e58f44c22745fa8c8ff Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 3 Jan 2023 17:22:20 +0000 Subject: [PATCH 058/580] Update NIOSSL dependency for 5.5 manifest (#1543) --- Package@swift-5.5.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift index ced9819cc..ac90779eb 100644 --- a/Package@swift-5.5.swift +++ b/Package@swift-5.5.swift @@ -62,7 +62,7 @@ let packageDependencies: [Package.Dependency] = [ ].appending( .package( url: "https://github.com/apple/swift-nio-ssl.git", - from: "2.14.0" + from: "2.23.0" ), if: includeNIOSSL ) From 254ea135be1911bfbf988858750362aa066158a5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 4 Jan 2023 13:04:00 +0000 Subject: [PATCH 059/580] Adopt the coalescing writer for clients (#1539) Motivation: In #1357 we introduced a message frames which coalesces writes into a single buffer in a write-flush cycle to reduce the number of emitted DATA frames. This PR adopts those changes for the client. Modifications: - Adjust the client state machine to use the coalescing writer Results: Small messages are coalesced in a flush cycle within a stream. Co-authored-by: Cory Benfield --- Sources/GRPC/GRPCClientChannelHandler.swift | 80 +++++++++++++------ Sources/GRPC/GRPCClientStateMachine.swift | 49 ++++++++++-- .../GRPC/LengthPrefixedMessageWriter.swift | 1 + Sources/GRPC/ReadWriteStates.swift | 69 +++++++++------- .../GRPCClientStateMachineTests.swift | 21 ++--- 5 files changed, 150 insertions(+), 70 deletions(-) diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index 052ec8e5b..d8bb8d999 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -529,29 +529,13 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { // Feed the request message into the state machine: let result = self.stateMachine.sendRequest( request.message, - compressed: request.compressed + compressed: request.compressed, + promise: promise ) - switch result { - case let .success((buffer, maybeBuffer)): - let frame1 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) - self.logger.trace("writing HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(buffer.readableBytes)", - MetadataKey.h2EndStream: "false", - ]) - // If there's a second buffer, attach the promise to the second write. - let promise1 = maybeBuffer == nil ? promise : nil - context.write(self.wrapOutboundOut(frame1), promise: promise1) - if let actuallyBuffer = maybeBuffer { - let frame2 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(actuallyBuffer))) - self.logger.trace("writing HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(actuallyBuffer.readableBytes)", - MetadataKey.h2EndStream: "false", - ]) - context.write(self.wrapOutboundOut(frame2), promise: promise) - } + switch result { + case .success: + () case let .failure(writeError): switch writeError { @@ -572,13 +556,37 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { } case .end: + // About to send end: write any outbound messages first. + while let (result, promise) = self.stateMachine.nextRequest() { + switch result { + case let .success(buffer): + let framePayload: HTTP2Frame.FramePayload = .data( + .init(data: .byteBuffer(buffer), endStream: false) + ) + + self.logger.trace("writing HTTP2 frame", metadata: [ + MetadataKey.h2Payload: "DATA", + MetadataKey.h2DataBytes: "\(buffer.readableBytes)", + MetadataKey.h2EndStream: "false", + ]) + context.write(self.wrapOutboundOut(framePayload), promise: promise) + + case let .failure(error): + context.fireErrorCaught(error) + promise?.fail(error) + return + } + } + // Okay: can we close the request stream? switch self.stateMachine.sendEndOfRequestStream() { case .success: // We can. Send an empty DATA frame with end-stream set. let empty = context.channel.allocator.buffer(capacity: 0) - let framePayload = HTTP2Frame.FramePayload - .data(.init(data: .byteBuffer(empty), endStream: true)) + let framePayload: HTTP2Frame.FramePayload = .data( + .init(data: .byteBuffer(empty), endStream: true) + ) + self.logger.trace("writing HTTP2 frame", metadata: [ MetadataKey.h2Payload: "DATA", MetadataKey.h2DataBytes: "0", @@ -605,4 +613,30 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { } } } + + func flush(context: ChannelHandlerContext) { + // Drain any requests. + while let (result, promise) = self.stateMachine.nextRequest() { + switch result { + case let .success(buffer): + let framePayload: HTTP2Frame.FramePayload = .data( + .init(data: .byteBuffer(buffer), endStream: false) + ) + + self.logger.trace("writing HTTP2 frame", metadata: [ + MetadataKey.h2Payload: "DATA", + MetadataKey.h2DataBytes: "\(buffer.readableBytes)", + MetadataKey.h2EndStream: "false", + ]) + context.write(self.wrapOutboundOut(framePayload), promise: promise) + + case let .failure(error): + context.fireErrorCaught(error) + promise?.fail(error) + return + } + } + + context.flush() + } } diff --git a/Sources/GRPC/GRPCClientStateMachine.swift b/Sources/GRPC/GRPCClientStateMachine.swift index 1a8031a27..3d79ebd5a 100644 --- a/Sources/GRPC/GRPCClientStateMachine.swift +++ b/Sources/GRPC/GRPCClientStateMachine.swift @@ -196,13 +196,18 @@ struct GRPCClientStateMachine { /// request will be written. mutating func sendRequest( _ message: ByteBuffer, - compressed: Bool - ) -> Result<(ByteBuffer, ByteBuffer?), MessageWriteError> { + compressed: Bool, + promise: EventLoopPromise? = nil + ) -> Result { return self.withStateAvoidingCoWs { state in - state.sendRequest(message, compressed: compressed) + state.sendRequest(message, compressed: compressed, promise: promise) } } + mutating func nextRequest() -> (Result, EventLoopPromise?)? { + return self.state.nextRequest() + } + /// Closes the request stream. /// /// The client must be streaming requests in order to terminate the request stream. Valid @@ -394,18 +399,21 @@ extension GRPCClientStateMachine.State { /// See `GRPCClientStateMachine.sendRequest(_:allocator:)`. mutating func sendRequest( _ message: ByteBuffer, - compressed: Bool - ) -> Result<(ByteBuffer, ByteBuffer?), MessageWriteError> { - let result: Result<(ByteBuffer, ByteBuffer?), MessageWriteError> + compressed: Bool, + promise: EventLoopPromise? + ) -> Result { + let result: Result switch self { case .clientActiveServerIdle(var writeState, let pendingReadState): - result = writeState.write(message, compressed: compressed) + let result = writeState.write(message, compressed: compressed, promise: promise) self = .clientActiveServerIdle(writeState: writeState, pendingReadState: pendingReadState) + return result case .clientActiveServerActive(var writeState, let readState): - result = writeState.write(message, compressed: compressed) + let result = writeState.write(message, compressed: compressed, promise: promise) self = .clientActiveServerActive(writeState: writeState, readState: readState) + return result case .clientClosedServerIdle, .clientClosedServerActive, @@ -422,6 +430,31 @@ extension GRPCClientStateMachine.State { return result } + mutating func nextRequest() -> (Result, EventLoopPromise?)? { + switch self { + case .clientActiveServerIdle(var writeState, let pendingReadState): + self = .modifying + let result = writeState.next() + self = .clientActiveServerIdle(writeState: writeState, pendingReadState: pendingReadState) + return result + + case .clientActiveServerActive(var writeState, let readState): + self = .modifying + let result = writeState.next() + self = .clientActiveServerActive(writeState: writeState, readState: readState) + return result + + case .clientIdleServerIdle, + .clientClosedServerIdle, + .clientClosedServerActive, + .clientClosedServerClosed: + return nil + + case .modifying: + preconditionFailure("State left as 'modifying'") + } + } + /// See `GRPCClientStateMachine.sendEndOfRequestStream()`. mutating func sendEndOfRequestStream() -> Result { let result: Result diff --git a/Sources/GRPC/LengthPrefixedMessageWriter.swift b/Sources/GRPC/LengthPrefixedMessageWriter.swift index fc3e326a3..8fef6efd7 100644 --- a/Sources/GRPC/LengthPrefixedMessageWriter.swift +++ b/Sources/GRPC/LengthPrefixedMessageWriter.swift @@ -15,6 +15,7 @@ */ import Foundation import NIOCore +import NIOHPACK internal struct LengthPrefixedMessageWriter { static let metadataLength = 5 diff --git a/Sources/GRPC/ReadWriteStates.swift b/Sources/GRPC/ReadWriteStates.swift index cdef94f75..62456f7cf 100644 --- a/Sources/GRPC/ReadWriteStates.swift +++ b/Sources/GRPC/ReadWriteStates.swift @@ -42,50 +42,61 @@ struct PendingWriteState { compression = nil } - let writer = LengthPrefixedMessageWriter(compression: compression, allocator: allocator) - return .writing(self.arity, self.contentType, writer) + let writer = CoalescingLengthPrefixedMessageWriter( + compression: compression, + allocator: allocator + ) + return .init(arity: self.arity, contentType: self.contentType, writer: writer) } } /// The write state of a stream. -enum WriteState { - /// Writing may be attempted using the given writer. - case writing(MessageArity, ContentType, LengthPrefixedMessageWriter) - - /// Writing may not be attempted: either a write previously failed or it is not valid for any - /// more messages to be written. - case notWriting +struct WriteState { + private var arity: MessageArity + private var contentType: ContentType + private var writer: CoalescingLengthPrefixedMessageWriter + private var canWrite: Bool + + init( + arity: MessageArity, + contentType: ContentType, + writer: CoalescingLengthPrefixedMessageWriter + ) { + self.arity = arity + self.contentType = contentType + self.writer = writer + self.canWrite = true + } /// Writes a message into a buffer using the `writer`. /// /// - Parameter message: The `Message` to write. mutating func write( _ message: ByteBuffer, - compressed: Bool - ) -> Result<(ByteBuffer, ByteBuffer?), MessageWriteError> { - switch self { - case .notWriting: + compressed: Bool, + promise: EventLoopPromise? + ) -> Result { + guard self.canWrite else { return .failure(.cardinalityViolation) + } - case .writing(let writeArity, let contentType, var writer): - self = .notWriting - let buffers: (ByteBuffer, ByteBuffer?) + self.writer.append(buffer: message, compress: compressed, promise: promise) - do { - buffers = try writer.write(buffer: message, compressed: compressed) - } catch { - self = .notWriting - return .failure(.serializationFailed) - } + switch self.arity { + case .one: + self.canWrite = false + case .many: + () + } - // If we only expect to write one message then we're no longer writable. - if case .one = writeArity { - self = .notWriting - } else { - self = .writing(writeArity, contentType, writer) - } + return .success(()) + } - return .success(buffers) + mutating func next() -> (Result, EventLoopPromise?)? { + if let next = self.writer.next() { + return (next.0.mapError { _ in .serializationFailed }, next.1) + } else { + return nil } } } diff --git a/Tests/GRPCTests/GRPCClientStateMachineTests.swift b/Tests/GRPCTests/GRPCClientStateMachineTests.swift index dc31e25cf..2357ca1a8 100644 --- a/Tests/GRPCTests/GRPCClientStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCClientStateMachineTests.swift @@ -154,14 +154,7 @@ extension GRPCClientStateMachineTests { stateMachine.sendRequest( ByteBuffer(string: request), compressed: false - ).assertSuccess { buffers in - var buffer = buffers.0 - XCTAssertNil(buffers.1) - // Remove the length and compression flag prefix. - buffer.moveReaderIndex(forwardBy: 5) - let data = buffer.readString(length: buffer.readableBytes)! - XCTAssertEqual(request, data) - } + ).assertSuccess() } func testSendRequestFromIdle() { @@ -1299,10 +1292,18 @@ extension PendingWriteState { extension WriteState { static func one() -> WriteState { - return .writing(.one, .protobuf, LengthPrefixedMessageWriter(compression: .none)) + return .init( + arity: .one, + contentType: .protobuf, + writer: .init(compression: .none, allocator: .init()) + ) } static func many() -> WriteState { - return .writing(.many, .protobuf, LengthPrefixedMessageWriter(compression: .none)) + return .init( + arity: .many, + contentType: .protobuf, + writer: .init(compression: .none, allocator: .init()) + ) } } From e078a68731a306722dbb23f34d599fb63bf44d8d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 4 Jan 2023 15:04:04 +0000 Subject: [PATCH 060/580] Update perf test precondition (#1545) Motivation: A change made in #1529 allowed multiple DATA frames to be emitted for a single message. One of the perf tests preconditions on there being exactly one DATA frame per message which is no longer the case. Modifications: - Update the precondition Result: Perf test does not crash. Co-authored-by: Cory Benfield --- .../Benchmarks/EmbeddedClientThroughput.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift index 45d71cf34..e3369b9f8 100644 --- a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift @@ -108,7 +108,12 @@ class EmbeddedClientThroughput: Benchmark { while let _ = try channel.readOutbound(as: HTTP2Frame.FramePayload.self) { requestFrames += 1 } - precondition(requestFrames == 3) // headers, data, empty data (end-stream) + // headers, data, empty data (end-stream). If the request is large there may be + // two DATA frames. + precondition( + requestFrames == 3 || requestFrames == 4, + "Expected 3/4 HTTP/2 frames but got \(requestFrames)" + ) // Okay, let's build a response. From d04613e82c27a6bf5e022532847ef57a72db3443 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Jan 2023 09:32:31 +0000 Subject: [PATCH 061/580] Use 5.8-nightly in CI (#1547) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 218d3eaea..83cd28fd5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-focal + - image: swiftlang/swift:nightly-5.8-focal # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - image: swift:5.7-jammy @@ -54,7 +54,7 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-focal + - image: swiftlang/swift:nightly-5.8-focal env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 392000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 From 4715a1dfaf5aab46590c9e081e1756a4670ade8e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Jan 2023 09:43:30 +0000 Subject: [PATCH 062/580] Adopt the coalescing writer for servers (#1546) Motivation: In #1357 we introduced a message frames which coalesces writes into a single buffer in a write-flush cycle to reduce the number of emitted DATA frames. This PR adopts those changes for the server. Modifications: - Adjust the server state machine and handler to use the coalescing writer Results: Small messages are coalesced in a flush cycle within a stream. Co-authored-by: Cory Benfield --- Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift | 37 +++--- Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift | 112 +++++++++--------- Tests/GRPCTests/CompressionTests.swift | 24 ++-- .../HTTP2ToRawGRPCStateMachineTests.swift | 55 +++++++-- 4 files changed, 145 insertions(+), 83 deletions(-) diff --git a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift b/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift index 2d6fa7483..af424d0c3 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift @@ -181,6 +181,7 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe self.isReading = false if self.flushPending { + self.deliverPendingResponses() self.flushPending = false context.flush() } @@ -188,6 +189,18 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe context.fireChannelReadComplete() } + private func deliverPendingResponses() { + while let (result, promise) = self.state.nextResponse() { + switch result { + case let .success(buffer): + let payload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) + self.context.write(self.wrapOutboundOut(payload), promise: promise) + case let .failure(error): + promise?.fail(error) + } + } + } + /// Called when the pipeline has finished configuring. private func configured() { switch self.state.pipelineConfigured() { @@ -288,23 +301,14 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe metadata: MessageMetadata, promise: EventLoopPromise? ) { - let writeBuffer = self.state.send( + let result = self.state.send( buffer: buffer, - allocator: self.context.channel.allocator, - compress: metadata.compress + compress: metadata.compress, + promise: promise ) - switch writeBuffer { - case let .success((buffer, maybeBuffer)): - if let actuallyBuffer = maybeBuffer { - let payload1 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) - self.context.write(self.wrapOutboundOut(payload1), promise: nil) - let payload2 = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(actuallyBuffer))) - self.context.write(self.wrapOutboundOut(payload2), promise: promise) - } else { - let payload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) - self.context.write(self.wrapOutboundOut(payload), promise: promise) - } + switch result { + case .success: if metadata.flush { self.markFlushPoint() } @@ -319,6 +323,9 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe trailers: HPACKHeaders, promise: EventLoopPromise? ) { + // About to end the stream: send any pending responses. + self.deliverPendingResponses() + switch self.state.send(status: status, trailers: trailers) { case let .sendTrailers(trailers): self.sendTrailers(trailers, promise: promise) @@ -349,6 +356,8 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe if self.isReading { self.flushPending = true } else { + // About to flush: send any pending responses. + self.deliverPendingResponses() self.flushPending = false self.context.flush() } diff --git a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift index e3daf435d..a46d20aab 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift @@ -53,7 +53,7 @@ extension HTTP2ToRawGRPCStateMachine { var reader: LengthPrefixedMessageReader /// A length prefixed message writer for response messages. - var writer: LengthPrefixedMessageWriter + var writer: CoalescingLengthPrefixedMessageWriter /// The content type of the RPC. var contentType: ContentType @@ -78,7 +78,7 @@ extension HTTP2ToRawGRPCStateMachine { var reader: LengthPrefixedMessageReader /// A length prefixed message writer for response messages. - var writer: LengthPrefixedMessageWriter + var writer: CoalescingLengthPrefixedMessageWriter /// The content type of the RPC. var contentType: ContentType @@ -113,7 +113,7 @@ extension HTTP2ToRawGRPCStateMachine { var reader: LengthPrefixedMessageReader /// A length prefixed message writer for response messages. - var writer: LengthPrefixedMessageWriter + var writer: CoalescingLengthPrefixedMessageWriter /// Whether to normalize user-provided metadata. var normalizeHeaders: Bool @@ -130,7 +130,7 @@ extension HTTP2ToRawGRPCStateMachine { var reader: LengthPrefixedMessageReader /// A length prefixed message writer for response messages. - var writer: LengthPrefixedMessageWriter + var writer: CoalescingLengthPrefixedMessageWriter /// Whether to normalize user-provided metadata. var normalizeHeaders: Bool @@ -502,8 +502,8 @@ extension HTTP2ToRawGRPCStateMachine.State { from headers: HPACKHeaders, encoding: ServerMessageEncoding, allocator: ByteBufferAllocator - ) -> (LengthPrefixedMessageWriter, String?) { - let writer: LengthPrefixedMessageWriter + ) -> (CoalescingLengthPrefixedMessageWriter, String?) { + let writer: CoalescingLengthPrefixedMessageWriter let responseEncoding: String? switch encoding { @@ -519,12 +519,12 @@ extension HTTP2ToRawGRPCStateMachine.State { configuration.enabledAlgorithms.contains($0) } - writer = LengthPrefixedMessageWriter(compression: algorithm, allocator: allocator) + writer = .init(compression: algorithm, allocator: allocator) responseEncoding = algorithm?.name case .disabled: // The server doesn't have compression enabled. - writer = LengthPrefixedMessageWriter(compression: .none, allocator: allocator) + writer = .init(compression: .none, allocator: allocator) responseEncoding = nil } @@ -623,44 +623,23 @@ extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseIdleState { // MARK: - Send Data -extension HTTP2ToRawGRPCStateMachine { - static func writeGRPCFramedMessage( - _ buffer: ByteBuffer, - compress: Bool, - writer: inout LengthPrefixedMessageWriter - ) -> Result<(ByteBuffer, ByteBuffer?), Error> { - do { - let buffers = try writer.write(buffer: buffer, compressed: compress) - return .success(buffers) - } catch { - return .failure(error) - } - } -} - extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseOpenState { mutating func send( buffer: ByteBuffer, - compress: Bool - ) -> Result<(ByteBuffer, ByteBuffer?), Error> { - return HTTP2ToRawGRPCStateMachine.writeGRPCFramedMessage( - buffer, - compress: compress, - writer: &self.writer - ) + compress: Bool, + promise: EventLoopPromise? + ) { + self.writer.append(buffer: buffer, compress: compress, promise: promise) } } extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseOpenState { mutating func send( buffer: ByteBuffer, - compress: Bool - ) -> Result<(ByteBuffer, ByteBuffer?), Error> { - return HTTP2ToRawGRPCStateMachine.writeGRPCFramedMessage( - buffer, - compress: compress, - writer: &self.writer - ) + compress: Bool, + promise: EventLoopPromise? + ) { + self.writer.append(buffer: buffer, compress: compress, promise: promise) } } @@ -879,10 +858,14 @@ extension HTTP2ToRawGRPCStateMachine { /// Send a response buffer. mutating func send( buffer: ByteBuffer, - allocator: ByteBufferAllocator, - compress: Bool - ) -> Result<(ByteBuffer, ByteBuffer?), Error> { - self.state.send(buffer: buffer, allocator: allocator, compress: compress) + compress: Bool, + promise: EventLoopPromise? + ) -> Result { + self.state.send(buffer: buffer, compress: compress, promise: promise) + } + + mutating func nextResponse() -> (Result, EventLoopPromise?)? { + self.state.nextResponse() } /// Send status and trailers. @@ -1070,9 +1053,9 @@ extension HTTP2ToRawGRPCStateMachine.State { mutating func send( buffer: ByteBuffer, - allocator: ByteBufferAllocator, - compress: Bool - ) -> Result<(ByteBuffer, ByteBuffer?), Error> { + compress: Bool, + promise: EventLoopPromise? + ) -> Result { switch self { case .requestIdleResponseIdle: preconditionFailure("Invalid state: the request stream is still closed") @@ -1083,24 +1066,47 @@ extension HTTP2ToRawGRPCStateMachine.State { return .failure(error) case var .requestOpenResponseOpen(state): - let result = state.send( - buffer: buffer, - compress: compress - ) + self = .requestClosedResponseClosed + state.send(buffer: buffer, compress: compress, promise: promise) + self = .requestOpenResponseOpen(state) + return .success(()) + + case var .requestClosedResponseOpen(state): + self = .requestClosedResponseClosed + state.send(buffer: buffer, compress: compress, promise: promise) + self = .requestClosedResponseOpen(state) + return .success(()) + + case .requestOpenResponseClosed, + .requestClosedResponseClosed: + return .failure(GRPCError.AlreadyComplete()) + } + } + + mutating func nextResponse() -> (Result, EventLoopPromise?)? { + switch self { + case .requestIdleResponseIdle: + preconditionFailure("Invalid state: the request stream is still closed") + + case .requestOpenResponseIdle, + .requestClosedResponseIdle: + return nil + + case var .requestOpenResponseOpen(state): + self = .requestClosedResponseClosed + let result = state.writer.next() self = .requestOpenResponseOpen(state) return result case var .requestClosedResponseOpen(state): - let result = state.send( - buffer: buffer, - compress: compress - ) + self = .requestClosedResponseClosed + let result = state.writer.next() self = .requestClosedResponseOpen(state) return result case .requestOpenResponseClosed, .requestClosedResponseClosed: - return .failure(GRPCError.AlreadyComplete()) + return nil } } diff --git a/Tests/GRPCTests/CompressionTests.swift b/Tests/GRPCTests/CompressionTests.swift index b7875ce7c..efe138e1a 100644 --- a/Tests/GRPCTests/CompressionTests.swift +++ b/Tests/GRPCTests/CompressionTests.swift @@ -16,6 +16,7 @@ import EchoImplementation import EchoModel import GRPC +import NIOConcurrencyHelpers import NIOCore import NIOHPACK import NIOPosix @@ -268,21 +269,27 @@ class MessageCompressionTests: GRPCTestCase { func testDecompressionLimitIsRespectedByClientForStreamingCall() throws { try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(2048)))) - self - .setupClient(encoding: .enabled(.init( - forRequests: .gzip, - decompressionLimit: .absolute(1024) - ))) + self.setupClient( + encoding: .enabled(.init(forRequests: .gzip, decompressionLimit: .absolute(1024))) + ) + + let responsePromise = self.group.next().makePromise(of: Echo_EchoResponse.self) + let lock = NIOLock() + var responseCount = 0 - var responses: [Echo_EchoResponse] = [] let update = self.echo.update { - responses.append($0) + lock.withLock { + responseCount += 1 + } + responsePromise.succeed($0) } let status = self.expectation(description: "received status") // Smaller than limit. update.sendMessage(.with { $0.text = "foo" }, promise: nil) + XCTAssertNoThrow(try responsePromise.futureResult.wait()) + // Should be just over the limit. update.sendMessage(.with { $0.text = String(repeating: "x", count: 1024) }, promise: nil) update.sendEnd(promise: nil) @@ -292,7 +299,8 @@ class MessageCompressionTests: GRPCTestCase { }.assertEqual(.resourceExhausted, fulfill: status) self.wait(for: [status], timeout: self.defaultTimeout) - XCTAssertEqual(responses.count, 1) + let receivedResponses = lock.withLock { responseCount } + XCTAssertEqual(receivedResponses, 1) } func testIdentityCompressionIsntCompression() throws { diff --git a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift b/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift index 982866445..280e0f4a7 100644 --- a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift +++ b/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift @@ -623,8 +623,8 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { for _ in 0 ..< 5 { let action = machine.send( buffer: buffer, - allocator: self.allocator, - compress: false + compress: false, + promise: nil ) assertThat(action, .is(.success())) } @@ -633,8 +633,8 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { // write as normal. let action = machine.send( buffer: buffer, - allocator: self.allocator, - compress: true + compress: true, + promise: nil ) assertThat(action, .is(.success())) } @@ -649,8 +649,8 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { let buffer = ByteBuffer(repeating: 0, count: 1024) let action2 = machine.send( buffer: buffer, - allocator: self.allocator, - compress: false + compress: false, + promise: nil ) assertThat(action2, .is(.failure())) } @@ -662,12 +662,51 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { let buffer = ByteBuffer(repeating: 0, count: 1024) let action2 = machine.send( buffer: buffer, - allocator: self.allocator, - compress: false + compress: false, + promise: nil ) assertThat(action2, .is(.failure())) } + // MARK: Next Response + + func testNextResponseBeforeMetadata() { + var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: true)) + XCTAssertNil(machine.nextResponse()) + } + + func testNextResponseWhenOpen() throws { + for startingState in [DesiredState.requestOpenResponseOpen, .requestClosedResponseOpen] { + var machine = self.makeStateMachine(state: startingState) + + // No response buffered yet. + XCTAssertNil(machine.nextResponse()) + + let buffer = ByteBuffer(repeating: 0, count: 1024) + machine.send(buffer: buffer, compress: false, promise: nil).assertSuccess() + + let (framedBuffer, promise) = try XCTUnwrap(machine.nextResponse()) + XCTAssertNil(promise) // Didn't provide a promise. + framedBuffer.assertSuccess() + + // No more responses. + XCTAssertNil(machine.nextResponse()) + } + } + + func testNextResponseWhenClosed() throws { + var machine = self.makeStateMachine(state: .requestClosedResponseOpen) + let action = machine.send(status: .ok, trailers: [:]) + switch action { + case .sendTrailersAndFinish: + () + default: + XCTFail("Expected 'sendTrailersAndFinish' but got \(action)") + } + + XCTAssertNil(machine.nextResponse()) + } + // MARK: Send End func testSendEndWhenResponseStreamIsIdle() { From 71f87ccd8362840ea2036aa28bbeb7daff56f9dd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Jan 2023 16:20:52 +0000 Subject: [PATCH 063/580] Remove unused code (#1549) Motivation: We added a coalescing message framer in #1539 and #1546. The old framer is no longer used. Modification: - Remote the now unused message writer and tests Result: Less unused code. --- .../GRPC/LengthPrefixedMessageWriter.swift | 133 ------------------ .../GRPCClientStateMachineTests.swift | 89 +++++------- .../LengthPrefixedMessageWriterTests.swift | 109 -------------- 3 files changed, 39 insertions(+), 292 deletions(-) delete mode 100644 Sources/GRPC/LengthPrefixedMessageWriter.swift delete mode 100644 Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift diff --git a/Sources/GRPC/LengthPrefixedMessageWriter.swift b/Sources/GRPC/LengthPrefixedMessageWriter.swift deleted file mode 100644 index 8fef6efd7..000000000 --- a/Sources/GRPC/LengthPrefixedMessageWriter.swift +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 Foundation -import NIOCore -import NIOHPACK - -internal struct LengthPrefixedMessageWriter { - static let metadataLength = 5 - - /// The compression algorithm to use, if one should be used. - let compression: CompressionAlgorithm? - private let compressor: Zlib.Deflate? - - /// Whether the compression message flag should be set. - private var shouldSetCompressionFlag: Bool { - return self.compression != nil - } - - /// A scratch buffer that we encode messages into: if the buffer isn't held elsewhere then we - /// can avoid having to allocate a new one. - private var scratch: ByteBuffer - - init(compression: CompressionAlgorithm? = nil, allocator: ByteBufferAllocator) { - self.compression = compression - self.scratch = allocator.buffer(capacity: 0) - - switch self.compression?.algorithm { - case .none, .some(.identity): - self.compressor = nil - case .some(.deflate): - self.compressor = Zlib.Deflate(format: .deflate) - case .some(.gzip): - self.compressor = Zlib.Deflate(format: .gzip) - } - } - - private mutating func compress( - buffer: ByteBuffer, - using compressor: Zlib.Deflate - ) throws -> ByteBuffer { - // The compressor will allocate the correct size. For now the leading 5 bytes will do. - self.scratch.clear(minimumCapacity: 5) - // Set the compression byte. - self.scratch.writeInteger(UInt8(1)) - // Set the length to zero; we'll write the actual value in a moment. - let payloadSizeIndex = self.scratch.writerIndex - self.scratch.writeInteger(UInt32(0)) - - let bytesWritten: Int - - do { - var buffer = buffer - bytesWritten = try compressor.deflate(&buffer, into: &self.scratch) - } catch { - throw error - } - - // Now fill in the message length. - self.scratch.writePayloadLength(UInt32(bytesWritten), at: payloadSizeIndex) - - // Finally, the compression context should be reset between messages. - compressor.reset() - - return self.scratch - } - - /// Writes the readable bytes of `buffer` as a gRPC length-prefixed message. - /// - /// - Parameters: - /// - buffer: The bytes to compress and length-prefix. - /// - compressed: Whether the bytes should be compressed. This is ignored if not compression - /// mechanism was configured on this writer. - /// - Returns: A buffer containing the length prefixed bytes. - mutating func write( - buffer: ByteBuffer, - compressed: Bool = true - ) throws -> (ByteBuffer, ByteBuffer?) { - if compressed, let compressor = self.compressor { - let compressedAndFramedPayload = try self.compress(buffer: buffer, using: compressor) - return (compressedAndFramedPayload, nil) - } else if buffer.readableBytes > Self.singleBufferSizeLimit { - // Buffer is larger than the limit for emitting a single buffer: create a second buffer - // containing just the message header. - self.scratch.clear(minimumCapacity: 5) - self.scratch.writeMultipleIntegers(UInt8(0), UInt32(buffer.readableBytes)) - return (self.scratch, buffer) - } else { - // We're not compressing and the message is within our single buffer size limit. - self.scratch.clear(minimumCapacity: 5 &+ buffer.readableBytes) - self.scratch.writeMultipleIntegers(UInt8(0), UInt32(buffer.readableBytes)) - self.scratch.writeImmutableBuffer(buffer) - return (self.scratch, nil) - } - } - - /// Message size above which we emit two buffers: one containing the header and one with the - /// actual message bytes. At or below the limit we copy the message into a new buffer containing - /// both the header and the message. - /// - /// Using two buffers avoids expensive copies of large messages. For smaller messages the copy - /// is cheaper than the additional allocations and overhead required to send an extra HTTP/2 DATA - /// frame. - /// - /// The value of 8192 was chosen empirically. We subtract the length of the message header - /// as `ByteBuffer` reserve capacity in powers of two and want to avoid overallocating. - private static let singleBufferSizeLimit = 8192 - 5 -} - -extension ByteBuffer { - @discardableResult - mutating func writePayloadLength(_ length: UInt32, at index: Int) -> Int { - let writerIndex = self.writerIndex - defer { - self.moveWriterIndex(to: writerIndex) - } - - self.moveWriterIndex(to: index) - return self.writeInteger(length) - } -} diff --git a/Tests/GRPCTests/GRPCClientStateMachineTests.swift b/Tests/GRPCTests/GRPCClientStateMachineTests.swift index 2357ca1a8..04bd643ff 100644 --- a/Tests/GRPCTests/GRPCClientStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCClientStateMachineTests.swift @@ -38,16 +38,21 @@ class GRPCClientStateMachineTests: GRPCTestCase { func writeMessage(_ message: String) throws -> ByteBuffer { let buffer = self.allocator.buffer(string: message) - var writer = LengthPrefixedMessageWriter(compression: .none, allocator: .init()) - var (buffer1, buffer2) = try writer.write( - buffer: buffer, - compressed: false - ) - - if var buffer2 = buffer2 { - buffer1.writeBuffer(&buffer2) + var writer = CoalescingLengthPrefixedMessageWriter(compression: .none, allocator: .init()) + writer.append(buffer: buffer, compress: false, promise: nil) + + var result: ByteBuffer? + while let next = writer.next() { + switch next.0 { + case let .success(buffer): + result.setOrWriteImmutableBuffer(buffer) + case let .failure(error): + throw error + } } - return buffer1 + + // We wrote a message, we must get at least one buffer out (or throw). + return result! } /// Writes a message into the given `buffer`. @@ -1119,6 +1124,14 @@ extension GRPCClientStateMachineTests { class ReadStateTests: GRPCTestCase { var allocator = ByteBufferAllocator() + func writeMessage(_ message: String) -> ByteBuffer { + var buffer = self.allocator.buffer(capacity: 5 + message.utf8.count) + buffer.writeInteger(UInt8(0)) + buffer.writeInteger(UInt32(message.utf8.count)) + buffer.writeBytes(message.utf8) + return buffer + } + func testReadWhenNoExpectedMessages() { var state: ReadState = .notReading var buffer = self.allocator.buffer(capacity: 0) @@ -1129,17 +1142,13 @@ class ReadStateTests: GRPCTestCase { } func testReadWithLeftOverBytesForOneExpectedMessage() throws { - // Write a message into the buffer: - let message = ByteBuffer(string: "Hello!") - var writer = LengthPrefixedMessageWriter(compression: .none) - var buffers = try writer.write(buffer: message) - XCTAssertNil(buffers.1) + var buffer = self.writeMessage("Hello!") // And some extra junk bytes: let bytes: [UInt8] = [0x00] - buffers.0.writeBytes(bytes) + buffer.writeBytes(bytes) var state: ReadState = .one() - state.readMessages(&buffers.0, maxLength: .max).assertFailure { + state.readMessages(&buffer, maxLength: .max).assertFailure { XCTAssertEqual($0, .leftOverBytes) } state.assertNotReading() @@ -1147,31 +1156,22 @@ class ReadStateTests: GRPCTestCase { func testReadTooManyMessagesForOneExpectedMessages() throws { // Write a message into the buffer twice: - let message = ByteBuffer(string: "Hello!") - var writer = LengthPrefixedMessageWriter(compression: .none) - var buffers1 = try writer.write(buffer: message) - var buffers2 = try writer.write(buffer: message) - XCTAssertNil(buffers1.1) - XCTAssertNil(buffers2.1) - buffers1.0.writeBuffer(&buffers2.0) + var buffer1 = self.writeMessage("Hello!") + let buffer2 = buffer1 + buffer1.writeImmutableBuffer(buffer2) var state: ReadState = .one() - state.readMessages(&buffers1.0, maxLength: .max).assertFailure { + state.readMessages(&buffer1, maxLength: .max).assertFailure { XCTAssertEqual($0, .cardinalityViolation) } state.assertNotReading() } func testReadOneMessageForOneExpectedMessages() throws { - // Write a message into the buffer twice: - let message = ByteBuffer(string: "Hello!") - var writer = LengthPrefixedMessageWriter(compression: .none) - var (buffer, other) = try writer.write(buffer: message) - XCTAssertNil(other) - + var buffer = self.writeMessage("Hello!") var state: ReadState = .one() state.readMessages(&buffer, maxLength: .max).assertSuccess { - XCTAssertEqual($0, [message]) + XCTAssertEqual($0, [ByteBuffer(string: "Hello!")]) } // We shouldn't be able to read anymore. @@ -1179,15 +1179,10 @@ class ReadStateTests: GRPCTestCase { } func testReadOneMessageForManyExpectedMessages() throws { - // Write a message into the buffer twice: - let message = ByteBuffer(string: "Hello!") - var writer = LengthPrefixedMessageWriter(compression: .none) - var (buffer, other) = try writer.write(buffer: message) - XCTAssertNil(other) - + var buffer = self.writeMessage("Hello!") var state: ReadState = .many() state.readMessages(&buffer, maxLength: .max).assertSuccess { - XCTAssertEqual($0, [message]) + XCTAssertEqual($0, [ByteBuffer(string: "Hello!")]) } // We should still be able to read. @@ -1195,20 +1190,14 @@ class ReadStateTests: GRPCTestCase { } func testReadManyMessagesForManyExpectedMessages() throws { - // Write a message into the buffer twice: - let message = ByteBuffer(string: "Hello!") - var writer = LengthPrefixedMessageWriter(compression: .none) - - var (first, _) = try writer.write(buffer: message) - var (second, _) = try writer.write(buffer: message) - var (third, _) = try writer.write(buffer: message) - - first.writeBuffer(&second) - first.writeBuffer(&third) + let lengthPrefixed = self.writeMessage("Hello!") + var buffer = lengthPrefixed + buffer.writeImmutableBuffer(lengthPrefixed) + buffer.writeImmutableBuffer(lengthPrefixed) var state: ReadState = .many() - state.readMessages(&first, maxLength: .max).assertSuccess { - XCTAssertEqual($0, [message, message, message]) + state.readMessages(&buffer, maxLength: .max).assertSuccess { + XCTAssertEqual($0, Array(repeating: ByteBuffer(string: "Hello!"), count: 3)) } // We should still be able to read. diff --git a/Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift b/Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift deleted file mode 100644 index b7269ae5f..000000000 --- a/Tests/GRPCTests/LengthPrefixedMessageWriterTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020, 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. - */ -@testable import GRPC -import NIOCore -import XCTest - -class LengthPrefixedMessageWriterTests: GRPCTestCase { - func testWriteBytesWithNoLeadingSpaceOrCompression() throws { - var writer = LengthPrefixedMessageWriter() - let allocator = ByteBufferAllocator() - let buffer = allocator.buffer(bytes: [1, 2, 3]) - - var (prefixed, other) = try writer.write(buffer: buffer) - XCTAssertNil(other) - XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 0) - XCTAssertEqual(prefixed.readInteger(as: UInt32.self), 3) - XCTAssertEqual(prefixed.readBytes(length: 3), [1, 2, 3]) - XCTAssertEqual(prefixed.readableBytes, 0) - } - - func testWriteBytesWithLeadingSpaceAndNoCompression() throws { - var writer = LengthPrefixedMessageWriter() - let allocator = ByteBufferAllocator() - - var buffer = allocator.buffer(bytes: Array(repeating: 0, count: 5) + [1, 2, 3]) - buffer.moveReaderIndex(forwardBy: 5) - - var (prefixed, other) = try writer.write(buffer: buffer) - XCTAssertNil(other) - XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 0) - XCTAssertEqual(prefixed.readInteger(as: UInt32.self), 3) - XCTAssertEqual(prefixed.readBytes(length: 3), [1, 2, 3]) - XCTAssertEqual(prefixed.readableBytes, 0) - } - - func testWriteBytesWithNoLeadingSpaceAndCompression() throws { - var writer = LengthPrefixedMessageWriter(compression: .gzip) - let allocator = ByteBufferAllocator() - - let buffer = allocator.buffer(bytes: [1, 2, 3]) - var (prefixed, other) = try writer.write(buffer: buffer) - XCTAssertNil(other) - - XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 1) - let size = prefixed.readInteger(as: UInt32.self)! - XCTAssertGreaterThanOrEqual(size, 0) - XCTAssertNotNil(prefixed.readBytes(length: Int(size))) - XCTAssertEqual(prefixed.readableBytes, 0) - } - - func testWriteBytesWithLeadingSpaceAndCompression() throws { - var writer = LengthPrefixedMessageWriter(compression: .gzip) - let allocator = ByteBufferAllocator() - - var buffer = allocator.buffer(bytes: Array(repeating: 0, count: 5) + [1, 2, 3]) - buffer.moveReaderIndex(forwardBy: 5) - var (prefixed, other) = try writer.write(buffer: buffer) - XCTAssertNil(other) - - XCTAssertEqual(prefixed.readInteger(as: UInt8.self), 1) - let size = prefixed.readInteger(as: UInt32.self)! - XCTAssertGreaterThanOrEqual(size, 0) - XCTAssertNotNil(prefixed.readBytes(length: Int(size))) - XCTAssertEqual(prefixed.readableBytes, 0) - } - - func testLargeCompressedPayloadEmitsOneBuffer() throws { - var writer = LengthPrefixedMessageWriter(compression: .gzip) - let allocator = ByteBufferAllocator() - let message = ByteBuffer(repeating: 0, count: 16 * 1024 * 1024) - - var (lengthPrefixed, other) = try writer.write(buffer: message) - XCTAssertNil(other) - XCTAssertEqual(lengthPrefixed.readInteger(as: UInt8.self), 1) - let length = lengthPrefixed.readInteger(as: UInt32.self) - XCTAssertEqual(length, UInt32(lengthPrefixed.readableBytes)) - } - - func testLargeUncompressedPayloadEmitsTwoBuffers() throws { - var writer = LengthPrefixedMessageWriter(compression: .none) - let allocator = ByteBufferAllocator() - let message = ByteBuffer(repeating: 0, count: 16 * 1024 * 1024) - - var (header, payload) = try writer.write(buffer: message) - XCTAssertEqual(header.readInteger(as: UInt8.self), 0) - XCTAssertEqual(header.readInteger(as: UInt32.self), UInt32(message.readableBytes)) - XCTAssertEqual(header.readableBytes, 0) - XCTAssertEqual(payload, message) - } -} - -extension LengthPrefixedMessageWriter { - init(compression: CompressionAlgorithm? = nil) { - self.init(compression: compression, allocator: .init()) - } -} From 1629de188c25682adc4cb47a80adca1ac90b494d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Jan 2023 16:33:47 +0000 Subject: [PATCH 064/580] Bump version number to 1.13.2 (#1550) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.13.2 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 807a96120..467d298d8 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 13 /// The patch version. - internal static let patch = 1 + internal static let patch = 2 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From bcca31f3cfe1a20ca598e14b4e3663206a449813 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 11 Jan 2023 13:42:16 +0000 Subject: [PATCH 065/580] Expose closeFuture in server interceptor context (#1553) Motivation: Server calls expose a `closeFuture` where users can register callbacks to tear things down when the RPC ends. Interceptors don't have this capability and must rely on observing an `.end`. Modifications: Expose the `closeFuture` from `ServerCallContext` to the `ServerInterceptorContext`. Result: - Users can be notified in interceptors when the call ends. - Resolves #1552 --- .../GRPCAsyncServerHandler.swift | 1 + .../BidirectionalStreamingServerHandler.swift | 1 + .../ClientStreamingServerHandler.swift | 1 + .../ServerStreamingServerHandler.swift | 1 + .../CallHandlers/UnaryServerHandler.swift | 1 + .../ServerInterceptorContext.swift | 6 ++ .../ServerInterceptorPipeline.swift | 7 +++ Tests/GRPCTests/InterceptorsTests.swift | 60 ++++++++++++++++++- .../ServerInterceptorPipelineTests.swift | 1 + 9 files changed, 78 insertions(+), 1 deletion(-) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift index d7ba2b552..fb3bf311a 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift @@ -283,6 +283,7 @@ internal final class AsyncServerHandler< callType: callType, remoteAddress: context.remoteAddress, userInfoRef: self.userInfoRef, + closeFuture: context.closeFuture, interceptors: interceptors, onRequestPart: self.receiveInterceptedPart(_:), onResponsePart: self.sendInterceptedPart(_:promise:) diff --git a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift index 11081d7da..9fd5a434f 100644 --- a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift @@ -92,6 +92,7 @@ public final class BidirectionalStreamingServerHandler< callType: .bidirectionalStreaming, remoteAddress: context.remoteAddress, userInfoRef: userInfoRef, + closeFuture: context.closeFuture, interceptors: interceptors, onRequestPart: self.receiveInterceptedPart(_:), onResponsePart: self.sendInterceptedPart(_:promise:) diff --git a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift index f82c8e136..e7929aa2f 100644 --- a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift @@ -93,6 +93,7 @@ public final class ClientStreamingServerHandler< callType: .clientStreaming, remoteAddress: context.remoteAddress, userInfoRef: userInfoRef, + closeFuture: context.closeFuture, interceptors: interceptors, onRequestPart: self.receiveInterceptedPart(_:), onResponsePart: self.sendInterceptedPart(_:promise:) diff --git a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift index 8526e6388..6cb3b4bbf 100644 --- a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift @@ -89,6 +89,7 @@ public final class ServerStreamingServerHandler< callType: .serverStreaming, remoteAddress: context.remoteAddress, userInfoRef: userInfoRef, + closeFuture: context.closeFuture, interceptors: interceptors, onRequestPart: self.receiveInterceptedPart(_:), onResponsePart: self.sendInterceptedPart(_:promise:) diff --git a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift b/Sources/GRPC/CallHandlers/UnaryServerHandler.swift index 6da422eec..f90a2545f 100644 --- a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift +++ b/Sources/GRPC/CallHandlers/UnaryServerHandler.swift @@ -87,6 +87,7 @@ public final class UnaryServerHandler< callType: .unary, remoteAddress: context.remoteAddress, userInfoRef: userInfoRef, + closeFuture: context.closeFuture, interceptors: interceptors, onRequestPart: self.receiveInterceptedPart(_:), onResponsePart: self.sendInterceptedPart(_:promise:) diff --git a/Sources/GRPC/Interceptor/ServerInterceptorContext.swift b/Sources/GRPC/Interceptor/ServerInterceptorContext.swift index 632bee450..5543b417f 100644 --- a/Sources/GRPC/Interceptor/ServerInterceptorContext.swift +++ b/Sources/GRPC/Interceptor/ServerInterceptorContext.swift @@ -54,6 +54,12 @@ public struct ServerInterceptorContext { return self._pipeline.remoteAddress } + /// A future which completes when the call closes. This may be used to register callbacks which + /// free up resources used by the interceptor. + public var closeFuture: EventLoopFuture { + return self._pipeline.closeFuture + } + /// A 'UserInfo' dictionary. /// /// - Important: While `UserInfo` has value-semantics, this property retrieves from, and sets a diff --git a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift b/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift index 82acb3bf5..0000c3fc7 100644 --- a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift +++ b/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift @@ -42,6 +42,11 @@ internal final class ServerInterceptorPipeline { @usableFromInline internal let userInfoRef: Ref + /// A future which completes when the call closes. This may be used to register callbacks which + /// free up resources used by the interceptor. + @usableFromInline + internal let closeFuture: EventLoopFuture + /// Called when a response part has traversed the interceptor pipeline. @usableFromInline internal let _onResponsePart: (GRPCServerResponsePart, EventLoopPromise?) -> Void @@ -99,6 +104,7 @@ internal final class ServerInterceptorPipeline { callType: GRPCCallType, remoteAddress: SocketAddress?, userInfoRef: Ref, + closeFuture: EventLoopFuture, interceptors: [ServerInterceptor], onRequestPart: @escaping (GRPCServerRequestPart) -> Void, onResponsePart: @escaping (GRPCServerResponsePart, EventLoopPromise?) -> Void @@ -109,6 +115,7 @@ internal final class ServerInterceptorPipeline { self.type = callType self.remoteAddress = remoteAddress self.userInfoRef = userInfoRef + self.closeFuture = closeFuture self._onResponsePart = onResponsePart self._onRequestPart = onRequestPart diff --git a/Tests/GRPCTests/InterceptorsTests.swift b/Tests/GRPCTests/InterceptorsTests.swift index d1241e2f6..1d8176abb 100644 --- a/Tests/GRPCTests/InterceptorsTests.swift +++ b/Tests/GRPCTests/InterceptorsTests.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import Atomics import EchoImplementation import EchoModel import GRPC @@ -28,6 +29,7 @@ class InterceptorsTests: GRPCTestCase { private var server: Server! private var connection: ClientConnection! private var echo: Echo_EchoNIOClient! + private let onCloseCounter = ManagedAtomic(0) override func setUp() { super.setUp() @@ -35,7 +37,7 @@ class InterceptorsTests: GRPCTestCase { self.server = try! Server.insecure(group: self.group) .withServiceProviders([ - EchoProvider(), + EchoProvider(interceptors: CountOnCloseInterceptors(counter: self.onCloseCounter)), HelloWorldProvider(interceptors: HelloWorldServerInterceptorFactory()), ]) .withLogger(self.serverLogger) @@ -64,6 +66,8 @@ class InterceptorsTests: GRPCTestCase { let get = self.echo.get(.with { $0.text = "hello" }) assertThat(try get.response.wait(), .is(.with { $0.text = "hello :teg ohce tfiwS" })) assertThat(try get.status.wait(), .hasCode(.ok)) + + XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) } func testCollect() { @@ -73,6 +77,8 @@ class InterceptorsTests: GRPCTestCase { collect.sendEnd(promise: nil) assertThat(try collect.response.wait(), .is(.with { $0.text = "3 4 1 2 :tcelloc ohce tfiwS" })) assertThat(try collect.status.wait(), .hasCode(.ok)) + + XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) } func testExpand() { @@ -81,6 +87,8 @@ class InterceptorsTests: GRPCTestCase { assertThat(response, .is(.with { $0.text = "hello :)0( dnapxe ohce tfiwS" })) } assertThat(try expand.status.wait(), .hasCode(.ok)) + + XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) } func testUpdate() { @@ -91,6 +99,8 @@ class InterceptorsTests: GRPCTestCase { update.sendMessage(.with { $0.text = "hello" }, promise: nil) update.sendEnd(promise: nil) assertThat(try update.status.wait(), .hasCode(.ok)) + + XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) } func testSayHello() { @@ -360,6 +370,54 @@ final class ReversingInterceptors: Echo_EchoClientInterceptorFactoryProtocol { } } +final class CountOnCloseInterceptors: Echo_EchoServerInterceptorFactoryProtocol { + // This interceptor is stateless, let's just share it. + private let interceptors: [ServerInterceptor] + + init(counter: ManagedAtomic) { + self.interceptors = [CountOnCloseServerInterceptor(counter: counter)] + } + + func makeGetInterceptors() -> [ServerInterceptor] { + return self.interceptors + } + + func makeExpandInterceptors() -> [ServerInterceptor] { + return self.interceptors + } + + func makeCollectInterceptors() -> [ServerInterceptor] { + return self.interceptors + } + + func makeUpdateInterceptors() -> [ServerInterceptor] { + return self.interceptors + } +} + +final class CountOnCloseServerInterceptor: ServerInterceptor { + private let counter: ManagedAtomic + + init(counter: ManagedAtomic) { + self.counter = counter + } + + override func receive( + _ part: GRPCServerRequestPart, + context: ServerInterceptorContext + ) { + switch part { + case .metadata: + context.closeFuture.whenComplete { _ in + self.counter.wrappingIncrement(ordering: .sequentiallyConsistent) + } + default: + () + } + context.receive(part) + } +} + private enum MagicKey: UserInfo.Key { typealias Value = String } diff --git a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift index 0e0b75f4a..9143e0339 100644 --- a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift @@ -43,6 +43,7 @@ class ServerInterceptorPipelineTests: GRPCTestCase { callType: callType, remoteAddress: nil, userInfoRef: Ref(UserInfo()), + closeFuture: self.embeddedEventLoop.makeSucceededVoidFuture(), interceptors: interceptors, onRequestPart: onRequestPart, onResponsePart: onResponsePart From 93882cc81920725b2460f6cba2a4c892d855fb2e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 26 Jan 2023 11:37:10 +0000 Subject: [PATCH 066/580] Add "--version" to protoc-gen-grpc-swift (#1557) Motivation: It can be useful to know the version of the protoc plugin being used. Modifications: - Add a "--version" flag to protoc-gen-grpc-swift which prints the plugin name and version. Result: Resolves #1556 --- Sources/protoc-gen-grpc-swift/Version.swift | 1 + Sources/protoc-gen-grpc-swift/main.swift | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 120000 Sources/protoc-gen-grpc-swift/Version.swift diff --git a/Sources/protoc-gen-grpc-swift/Version.swift b/Sources/protoc-gen-grpc-swift/Version.swift new file mode 120000 index 000000000..aa26b19d3 --- /dev/null +++ b/Sources/protoc-gen-grpc-swift/Version.swift @@ -0,0 +1 @@ +../GRPC/Version.swift \ No newline at end of file diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 091428a97..1901f688f 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -104,7 +104,18 @@ func uniqueOutputFileName( } } -func main() throws { +func printVersion(args: [String]) { + // Stip off the file path + let program = args.first?.split(separator: "/").last ?? "protoc-gen-grpc-swift" + print("\(program) \(Version.versionString)") +} + +func main(args: [String]) throws { + if args.dropFirst().contains("--version") { + printVersion(args: args) + return + } + // initialize responses var response = Google_Protobuf_Compiler_CodeGeneratorResponse( files: [], @@ -147,7 +158,7 @@ func main() throws { } do { - try main() + try main(args: CommandLine.arguments) } catch { Log("ERROR: \(error)") } From b76ca2821ddf8ad89667f32f11694f6b4021c4ae Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 3 Feb 2023 11:31:47 +0000 Subject: [PATCH 067/580] Update allocation counts (#1558) --- .github/workflows/ci.yaml | 59 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 83cd28fd5..beb4bfd1a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,44 +56,45 @@ jobs: include: - image: swiftlang/swift:nightly-5.8-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 392000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 174000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 181000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 391000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 175000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 173000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 180000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 180000 - image: swift:5.7-jammy + env: + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 391000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 175000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 173000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 180000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 180000 + - image: swift:5.6-focal env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 392000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 174000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 181000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 - - image: swift:5.6-focal - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 393000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 177000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 175000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 182000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 182000 - image: swift:5.5-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 423000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 189000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 112000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 67000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 63000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 186000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 193000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 193000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 422000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 188000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 185000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 192000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 192000 + name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: From 3f7c71413af2961f3006df5220140826854b4874 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 3 Feb 2023 14:10:40 +0000 Subject: [PATCH 068/580] Server interceptors retain cycle (#1559) Motivation: There's a code path through the ELF-based server handlers where the interceptor pipeline does not get `nil`'d out to break the reference cycle between the handler and the interceptors leading to a leak. This can happen if the RPC ends unexpectedtly. Modifications: - Break the retain cycle on the next event-loop tick; this gives any clean up code a chance to run first. - Break the retain cycle from the interceptors when they send end. Results: Fewer leaks. --- .../BidirectionalStreamingServerHandler.swift | 3 +++ .../ClientStreamingServerHandler.swift | 3 +++ .../ServerStreamingServerHandler.swift | 3 +++ .../GRPC/CallHandlers/UnaryServerHandler.swift | 3 +++ .../Interceptor/ServerInterceptorPipeline.swift | 15 +++++++++++---- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift index 9fd5a434f..229cb1b5e 100644 --- a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift @@ -137,6 +137,9 @@ public final class BidirectionalStreamingServerHandler< case let .creatingObserver(context), let .observing(_, context): context.statusPromise.fail(GRPCStatus(code: .unavailable, message: nil)) + self.context.eventLoop.execute { + self.interceptors = nil + } case .completed: self.interceptors = nil diff --git a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift index e7929aa2f..2e1167998 100644 --- a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift @@ -138,6 +138,9 @@ public final class ClientStreamingServerHandler< case let .creatingObserver(context), let .observing(_, context): context.responsePromise.fail(GRPCStatus(code: .unavailable, message: nil)) + self.context.eventLoop.execute { + self.interceptors = nil + } case .completed: self.interceptors = nil diff --git a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift index 6cb3b4bbf..7028020c6 100644 --- a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift @@ -134,6 +134,9 @@ public final class ServerStreamingServerHandler< case let .createdContext(context), let .invokedFunction(context): context.statusPromise.fail(GRPCStatus(code: .unavailable, message: nil)) + self.context.eventLoop.execute { + self.interceptors = nil + } case .completed: self.interceptors = nil diff --git a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift b/Sources/GRPC/CallHandlers/UnaryServerHandler.swift index f90a2545f..c0a84abb6 100644 --- a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift +++ b/Sources/GRPC/CallHandlers/UnaryServerHandler.swift @@ -132,6 +132,9 @@ public final class UnaryServerHandler< case let .createdContext(context), let .invokedFunction(context): context.responsePromise.fail(GRPCStatus(code: .unavailable, message: nil)) + self.context.eventLoop.execute { + self.interceptors = nil + } case .completed: self.interceptors = nil diff --git a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift b/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift index 0000c3fc7..863c63941 100644 --- a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift +++ b/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift @@ -49,11 +49,14 @@ internal final class ServerInterceptorPipeline { /// Called when a response part has traversed the interceptor pipeline. @usableFromInline - internal let _onResponsePart: (GRPCServerResponsePart, EventLoopPromise?) -> Void + internal var _onResponsePart: Optional<( + GRPCServerResponsePart, + EventLoopPromise? + ) -> Void> /// Called when a request part has traversed the interceptor pipeline. @usableFromInline - internal let _onRequestPart: (GRPCServerRequestPart) -> Void + internal var _onRequestPart: Optional<(GRPCServerRequestPart) -> Void> /// The index before the first user interceptor context index. (always -1). @usableFromInline @@ -185,7 +188,7 @@ internal final class ServerInterceptorPipeline { ) case self._tailIndex: - self._onRequestPart(part) + self._onRequestPart?(part) default: self._userContexts[index].invokeReceive(part) @@ -245,10 +248,11 @@ internal final class ServerInterceptorPipeline { ) { switch index { case self._headIndex: + let onResponsePart = self._onResponsePart if part.isEnd { self.close() } - self._onResponsePart(part, promise) + onResponsePart?(part, promise) case self._tailIndex: // The next outbound index must exist: it will be the head or a user interceptor. @@ -269,6 +273,9 @@ internal final class ServerInterceptorPipeline { self._isOpen = false // Each context hold a ref to the pipeline; break the retain cycle. self._userContexts.removeAll() + // Drop the refs to the server handler. + self._onRequestPart = nil + self._onResponsePart = nil } } From a70e9ad7576245642015866c06a54fbc6c7805a9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 7 Feb 2023 17:04:39 +0000 Subject: [PATCH 069/580] Use iterator when checking encoding headers (#1563) Motivation: SwiftNIO HTTP/2 1.24.0 added a method for retrieving a sequence of header values rather than directly returning an array of values. We can use it when checking the encoding header as there should only be at most one value. Modifications: - Update HTTP/2 to be min 1.24.0 - Use the iterator API Result: Fewer allocations. --- .github/workflows/ci.yaml | 4 ++-- Package.swift | 2 +- Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift | 17 ++++++++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index beb4bfd1a..261ed2a68 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,8 +16,8 @@ jobs: run: | SWIFTFORMAT_VERSION=0.49.4 git clone --depth 1 --branch "$SWIFTFORMAT_VERSION" "https://github.com/nicklockwood/SwiftFormat" "$HOME/SwiftFormat" - swift build --package-path "$HOME/SwiftFormat" --product swiftformat - export PATH=$PATH:"$(swift build --package-path "$HOME/SwiftFormat" --show-bin-path)" + swift build -c release --package-path "$HOME/SwiftFormat" --product swiftformat + export PATH=$PATH:"$(swift build -c release --package-path "$HOME/SwiftFormat" --show-bin-path)" ./scripts/sanity.sh unit-tests: strategy: diff --git a/Package.swift b/Package.swift index 8653027ac..e7b387df6 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-nio-http2.git", - from: "1.22.0" + from: "1.24.1" ), .package( url: "https://github.com/apple/swift-nio-transport-services.git", diff --git a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift index a46d20aab..a134c786e 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift @@ -447,10 +447,19 @@ extension HTTP2ToRawGRPCStateMachine.State { from headers: HPACKHeaders, encoding: ServerMessageEncoding ) -> RequestEncodingValidation { - let encodings = headers[canonicalForm: GRPCHeaderName.encoding] + let encodingValues = headers.values(forHeader: GRPCHeaderName.encoding, canonicalForm: true) + var encodingIterator = encodingValues.makeIterator() + let encodingHeader = encodingIterator.next() // Fail if there's more than one encoding header. - if encodings.count > 1 { + if let first = encodingHeader, let second = encodingIterator.next() { + var encodings: [Substring] = [] + encodings.reserveCapacity(8) + encodings.append(first) + encodings.append(second) + while let next = encodingIterator.next() { + encodings.append(next) + } let status = GRPCStatus( code: .invalidArgument, message: "'\(GRPCHeaderName.encoding)' must contain no more than one value but was '\(encodings.joined(separator: ", "))'" @@ -458,12 +467,10 @@ extension HTTP2ToRawGRPCStateMachine.State { return .invalid(status: status, acceptEncoding: nil) } - let encodingHeader = encodings.first let result: RequestEncodingValidation - let validator = MessageEncodingHeaderValidator(encoding: encoding) - switch validator.validate(requestEncoding: encodingHeader) { + switch validator.validate(requestEncoding: encodingHeader.map { String($0) }) { case let .supported(algorithm, decompressionLimit, acceptEncoding): // Request message encoding is valid and supported. result = .valid( From c12f59f38e78fa783c237d30bf52c12b377fa009 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 8 Feb 2023 13:31:56 +0000 Subject: [PATCH 070/580] Buffer in the server pipeline configuration (#1564) Motivation: When not using TLS, the server pipeline configurator inspects the first bytes on a connection to determine whether HTTP1 or HTTP2 is being used and closes the connection if it is determined that neither are. It does this by only parsing the first packet, which may not have enough bytes to make a correct determination. Modifications: - Buffer bytes in the configurator. - Parse the buffered bytes and only close if enough bytes have been received. Result: Better version determination. --- .../GRPC/GRPCServerPipelineConfigurator.swift | 140 ++++++++++++++---- .../GRPCServerPipelineConfiguratorTests.swift | 51 +++++++ Tests/GRPCTests/HTTPVersionParserTests.swift | 28 ++-- 3 files changed, 178 insertions(+), 41 deletions(-) diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift index 5854faea9..61155426b 100644 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ b/Sources/GRPC/GRPCServerPipelineConfigurator.swift @@ -33,8 +33,8 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan /// The server configuration. private let configuration: Server.Configuration - /// Reads which we're holding on to before the pipeline is configured. - private var bufferedReads = CircularBuffer() + /// A buffer containing the buffered bytes. + private var buffer: ByteBuffer? /// The current state. private var state: State @@ -212,13 +212,17 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan buffer: ByteBuffer, context: ChannelHandlerContext ) { - if HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer) { + switch HTTPVersionParser.determineHTTPVersion(buffer) { + case .http2: self.configureHTTP2(context: context) - } else if HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer) { + case .http1: self.configureHTTP1(context: context) - } else { + case .unknown: + // Neither H2 nor H1 or the length limit has been exceeded. self.configuration.logger.error("Unable to determine http version, closing") context.close(mode: .all, promise: nil) + case .notEnoughBytes: + () // Try again with more bytes. } } @@ -268,13 +272,9 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan /// Try to parse the buffered data to determine whether or not HTTP/2 or HTTP/1 should be used. private func tryParsingBufferedData(context: ChannelHandlerContext) { - guard let first = self.bufferedReads.first else { - // No data buffered yet. We'll try when we read. - return + if let buffer = self.buffer { + self.determineHTTPVersionAndConfigurePipeline(buffer: buffer, context: context) } - - let buffer = self.unwrapInboundIn(first) - self.determineHTTPVersionAndConfigurePipeline(buffer: buffer, context: context) } // MARK: - Channel Handler @@ -312,7 +312,8 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan } internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - self.bufferedReads.append(data) + var buffer = self.unwrapInboundIn(data) + self.buffer.setOrWriteBuffer(&buffer) switch self.state { case .notConfigured(alpn: .notExpected), @@ -335,8 +336,9 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan removalToken: ChannelHandlerContext.RemovalToken ) { // Forward any buffered reads. - while let read = self.bufferedReads.popFirst() { - context.fireChannelRead(read) + if let buffer = self.buffer { + self.buffer = nil + context.fireChannelRead(self.wrapInboundOut(buffer)) } context.leavePipeline(removalToken: removalToken) } @@ -375,16 +377,64 @@ struct HTTPVersionParser { /// Determines whether the bytes in the `ByteBuffer` are prefixed with the HTTP/2 client /// connection preface. - static func prefixedWithHTTP2ConnectionPreface(_ buffer: ByteBuffer) -> Bool { + static func prefixedWithHTTP2ConnectionPreface(_ buffer: ByteBuffer) -> SubParseResult { let view = buffer.readableBytesView guard view.count >= HTTPVersionParser.http2ClientMagic.count else { // Not enough bytes. - return false + return .notEnoughBytes } let slice = view[view.startIndex ..< view.startIndex.advanced(by: self.http2ClientMagic.count)] - return slice.elementsEqual(HTTPVersionParser.http2ClientMagic) + return slice.elementsEqual(HTTPVersionParser.http2ClientMagic) ? .accepted : .rejected + } + + enum ParseResult: Hashable { + case http1 + case http2 + case unknown + case notEnoughBytes + } + + enum SubParseResult: Hashable { + case accepted + case rejected + case notEnoughBytes + } + + private static let maxLengthToCheck = 1024 + + static func determineHTTPVersion(_ buffer: ByteBuffer) -> ParseResult { + switch Self.prefixedWithHTTP2ConnectionPreface(buffer) { + case .accepted: + return .http2 + + case .notEnoughBytes: + switch Self.prefixedWithHTTP1RequestLine(buffer) { + case .accepted: + // Not enough bytes to check H2, but enough to confirm H1. + return .http1 + case .notEnoughBytes: + // Not enough bytes to check H2 or H1. + return .notEnoughBytes + case .rejected: + // Not enough bytes to check H2 and definitely not H1. + return .notEnoughBytes + } + + case .rejected: + switch Self.prefixedWithHTTP1RequestLine(buffer) { + case .accepted: + // Not H2, but H1 is confirmed. + return .http1 + case .notEnoughBytes: + // Not H2, but not enough bytes to reject H1 yet. + return .notEnoughBytes + case .rejected: + // Not H2 or H1. + return .unknown + } + } } private static let http1_1 = [ @@ -399,29 +449,59 @@ struct HTTPVersionParser { ] /// Determines whether the bytes in the `ByteBuffer` are prefixed with an HTTP/1.1 request line. - static func prefixedWithHTTP1RequestLine(_ buffer: ByteBuffer) -> Bool { + static func prefixedWithHTTP1RequestLine(_ buffer: ByteBuffer) -> SubParseResult { var readableBytesView = buffer.readableBytesView + // We don't need to validate the request line, only determine whether we think it's an HTTP1 + // request line. Another handler will parse it properly. + // From RFC 2616 § 5.1: // Request-Line = Method SP Request-URI SP HTTP-Version CRLF - // Read off the Method and Request-URI (and spaces). - guard readableBytesView.trimPrefix(to: UInt8(ascii: " ")) != nil, - readableBytesView.trimPrefix(to: UInt8(ascii: " ")) != nil else { - return false + // Get through the first space. + guard readableBytesView.dropPrefix(through: UInt8(ascii: " ")) != nil else { + let tooLong = buffer.readableBytes > Self.maxLengthToCheck + return tooLong ? .rejected : .notEnoughBytes + } + + // Get through the second space. + guard readableBytesView.dropPrefix(through: UInt8(ascii: " ")) != nil else { + let tooLong = buffer.readableBytes > Self.maxLengthToCheck + return tooLong ? .rejected : .notEnoughBytes + } + + // +2 for \r\n + guard readableBytesView.count >= (Self.http1_1.count + 2) else { + return .notEnoughBytes } - // Read off the HTTP-Version and CR. - guard let versionView = readableBytesView.trimPrefix(to: UInt8(ascii: "\r")) else { - return false + guard let version = readableBytesView.dropPrefix(through: UInt8(ascii: "\r")), + readableBytesView.first == UInt8(ascii: "\n") else { + // If we didn't drop the prefix OR we did and the next byte wasn't '\n', then we had enough + // bytes but the '\r\n' wasn't present: reject this as being HTTP1. + return .rejected + } + + return version.elementsEqual(Self.http1_1) ? .accepted : .rejected + } +} + +extension Collection where Self == Self.SubSequence, Self.Element: Equatable { + /// Drops the prefix off the collection up to and including the first `separator` + /// only if that separator appears in the collection. + /// + /// Returns the prefix up to but not including the separator if it was found, nil otherwise. + mutating func dropPrefix(through separator: Element) -> SubSequence? { + if self.isEmpty { + return nil } - // Check that the LF followed the CR. - guard readableBytesView.first == UInt8(ascii: "\n") else { - return false + guard let separatorIndex = self.firstIndex(of: separator) else { + return nil } - // Now check the HTTP version. - return versionView.elementsEqual(HTTPVersionParser.http1_1) + let prefix = self[.. Date: Wed, 8 Feb 2023 13:43:52 +0000 Subject: [PATCH 071/580] Bump version number to 1.14.0 (#1565) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.14.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 467d298d8..95006dbea 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,10 +19,10 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 13 + internal static let minor = 14 /// The patch version. - internal static let patch = 2 + internal static let patch = 0 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 83157d51b2c2f5690259e03f74b59aac99f652d7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 8 Feb 2023 15:55:56 +0000 Subject: [PATCH 072/580] Do not check source of logs in tests (#1566) Motivation: swift-log captures the 'source' of a log, which is usually the defining module. In older Swift versions this was done by parsing `#file` and was not always correct as it would just take the directory containing the file rather than the source module: we added a check after each test in gRPC to make sure the source was always "GRPC". This is no longer necessary as swift-log now does the right thing. Modifications: - Remove the check Result: Tests are slightly faster. --- Tests/GRPCTests/GRPCTestCase.swift | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Tests/GRPCTests/GRPCTestCase.swift b/Tests/GRPCTests/GRPCTestCase.swift index 8ec6d320d..a9d68e94d 100644 --- a/Tests/GRPCTests/GRPCTestCase.swift +++ b/Tests/GRPCTests/GRPCTestCase.swift @@ -36,21 +36,10 @@ class GRPCTestCase: XCTestCase { } override func tearDown() { - let logs = self.capturedLogs() - - // The default source emitted by swift-log is the directory containing the '#filePath' in which the - // log was emitted. It's meant to represent the system which emitted the log, typically the - // module name. In most cases it's right but in a few places, i.e. where the source lives in - // child directories below 'GRPC', it isn't. We'll use this as a sanity check. - // - // See also: https://github.com/apple/swift-log/issues/145 - for log in logs { - XCTAssertEqual(log.source, "GRPC", "Incorrect log source in \(log.file) on line \(log.line)") - } - // Only print logs when there's a failure and we're *not* always logging (when we are always // logging, logs will be printed as they're caught). if !GRPCTestCase.alwaysLog, (self.testRun.map { $0.totalFailureCount > 0 } ?? false) { + let logs = self.capturedLogs() self.printCapturedLogs(logs) } From 3e4c8e393cb8e3b5b6f4b21111eaf801b4c7bd54 Mon Sep 17 00:00:00 2001 From: Sami Suteria Date: Tue, 14 Feb 2023 01:12:22 -0800 Subject: [PATCH 073/580] Add import paths to SPM Plugin (#1568) --- Plugins/GRPCSwiftPlugin/plugin.swift | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index d933bea55..ae766dd89 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -51,6 +51,13 @@ struct GRPCSwiftPlugin: BuildToolPlugin { var keepMethodCasing: Bool? } + /// Specify the directory in which to search for + /// imports. May be specified multiple times; + /// directories will be searched in order. + /// The target source directory is always appended + /// to the import paths. + var importPaths: [String]? + /// The path to the `protoc` binary. /// /// If this is not set, SPM will try to find the tool itself. @@ -75,6 +82,11 @@ struct GRPCSwiftPlugin: BuildToolPlugin { try self.validateConfiguration(configuration) + var importPaths: [Path] = [target.directory] + if let configuredImportPaths = configuration.importPaths { + importPaths.append(contentsOf: configuredImportPaths.map { Path($0) }) + } + // We need to find the path of protoc and protoc-gen-grpc-swift let protocPath: Path if let configuredProtocPath = configuration.protocPath { @@ -97,7 +109,8 @@ struct GRPCSwiftPlugin: BuildToolPlugin { invocation: invocation, protocPath: protocPath, protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, - outputDirectory: outputDirectory + outputDirectory: outputDirectory, + importPaths: importPaths ) } } @@ -110,23 +123,27 @@ struct GRPCSwiftPlugin: BuildToolPlugin { /// - protocPath: The path to the `protoc` binary. /// - protocGenSwiftPath: The path to the `protoc-gen-swift` binary. /// - outputDirectory: The output directory for the generated files. + /// - importPaths: List of paths to pass with "-I " to `protoc` /// - Returns: The build command. private func invokeProtoc( target: Target, invocation: Configuration.Invocation, protocPath: Path, protocGenGRPCSwiftPath: Path, - outputDirectory: Path + outputDirectory: Path, + importPaths: [Path] ) -> Command { // Construct the `protoc` arguments. var protocArgs = [ "--plugin=protoc-gen-grpc-swift=\(protocGenGRPCSwiftPath)", "--grpc-swift_out=\(outputDirectory)", - // We include the target directory as a proto search path - "-I", - "\(target.directory)", ] + importPaths.forEach { path in + protocArgs.append("-I") + protocArgs.append("\(path)") + } + if let visibility = invocation.visibility { protocArgs.append("--grpc-swift_opt=Visibility=\(visibility.rawValue.capitalized)") } From c4682a9c5db05c169a3b4ba149af134417092f56 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 16 Feb 2023 17:07:00 +0000 Subject: [PATCH 074/580] Connect EmbeddedChannel when FuzzTesting (#1569) Motivation: FuzzTesting uses EmbeddedChannel but never called connect meaning that channel active was never fired. HTTP/2 recently added activity state checking and complains that we fire inactive without having activated. Modifications: - call connect on the EmbeddedChannel in fuzzing and in tests - if the connection manager is closed while connecting it waits for the connect to resolve before closing the channel, however the channel future is completed before channel active is fired so defer closing it until the next loop tick. Result: Channel active is more frequently fired at the right time. --- .../Sources/ServerFuzzerLib/ServerFuzzer.swift | 2 ++ Sources/GRPC/ConnectionManager.swift | 11 ++++++++--- Tests/GRPCTests/ServerFuzzingRegressionTests.swift | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift b/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift index 31baa6070..0cbbaafa3 100644 --- a/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift +++ b/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift @@ -22,6 +22,8 @@ public func test(_ start: UnsafeRawPointer, _ count: Int) -> CInt { let bytes = UnsafeRawBufferPointer(start: start, count: count) let channel = EmbeddedChannel() + try! channel.connect(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() + defer { _ = try? channel.finish() } diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 32456efc9..2414139c0 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -514,9 +514,14 @@ internal final class ConnectionManager { state.candidate.whenComplete { switch $0 { case let .success(channel): - // In case we do successfully connect, close immediately. - channel.close(mode: .all, promise: nil) - promise.completeWith(channel.closeFuture.recoveringFromUncleanShutdown()) + // In case we do successfully connect, close on the next loop tick. When connecting a + // channel NIO will complete the promise for the channel before firing channel active. + // That means we may close and fire inactive before active which HTTP/2 will be unhappy + // about. + self.eventLoop.execute { + channel.close(mode: .all, promise: nil) + promise.completeWith(channel.closeFuture.recoveringFromUncleanShutdown()) + } case .failure: // We failed to connect, that's fine we still shutdown successfully. diff --git a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift b/Tests/GRPCTests/ServerFuzzingRegressionTests.swift index be97565eb..444dca022 100644 --- a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift +++ b/Tests/GRPCTests/ServerFuzzingRegressionTests.swift @@ -31,6 +31,7 @@ final class ServerFuzzingRegressionTests: GRPCTestCase { private func runTest(withInput buffer: ByteBuffer) { let channel = EmbeddedChannel() + try! channel.connect(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() defer { _ = try? channel.finish() } From 1f87e8c5b9ab127019b63a1b9c202d4ec130b531 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 27 Feb 2023 14:56:12 +0000 Subject: [PATCH 075/580] Nil out the `onStart` callback after using it (#1570) Motivation: The `ClientTransport` has an `onStart` callback which is called once the stream has been established. For async calls this is used to notify the writability manager that it may now start writing. The callback references the call which holds the transport which holds the callback forming a strong retain cycle and a slow memory leak. The transport should break this cycle. Modifications: - nil out the `onStart` callback after it has been called. Result: Fixes a leak --- Sources/GRPC/Interceptor/ClientTransport.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/Interceptor/ClientTransport.swift b/Sources/GRPC/Interceptor/ClientTransport.swift index 810e2a698..0387d1ca0 100644 --- a/Sources/GRPC/Interceptor/ClientTransport.swift +++ b/Sources/GRPC/Interceptor/ClientTransport.swift @@ -94,7 +94,7 @@ internal final class ClientTransport { private var channel: Channel? /// A callback which is invoked once when the stream channel becomes active. - private let onStart: () -> Void + private var onStart: (() -> Void)? /// Our current state as logging metadata. private var stateForLogging: Logger.MetadataValue { @@ -945,7 +945,8 @@ extension ClientTransport { // Messages are buffered by this class and in the async writer for async calls. Initially the // async writer is not allowed to emit messages; the call to 'onStart()' signals that messages // may be emitted. We call it here to avoid races between writing headers and messages. - self.onStart() + self.onStart?() + self.onStart = nil case let .message(request, metadata): do { From a20cac0cad4e0da457de687c45cb55aee9a45e19 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 27 Feb 2023 15:10:52 +0000 Subject: [PATCH 076/580] Bump version number to 1.14.1 (#1572) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.14.1 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 95006dbea..2a93422be 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 14 /// The patch version. - internal static let patch = 0 + internal static let patch = 1 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 770629b04496ca82d10418371cde8d7293618cfd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 3 Mar 2023 13:27:08 +0000 Subject: [PATCH 077/580] Don't trap on invalid connection state transitions (#1573) Motivation: The connection manager is quite aggresive about trapping if an invalid state is hit. It's alsmost impossible to know when states are truly unreachable so in most coses we should handle them as best as we can. If we believe them to be unreachable but cannot easily prove it then we should crash in debug mode and handle it as best as possible in release mode. Modifications: - Handle various state transitions more gently in the connection manager. Result: Gentler state handling. --- Sources/GRPC/ConnectionManager.swift | 150 ++++++++++--------- Tests/GRPCTests/ConnectionManagerTests.swift | 113 ++++++++++++++ 2 files changed, 193 insertions(+), 70 deletions(-) diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 2414139c0..8b66f505e 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -77,11 +77,14 @@ internal final class ConnectionManager { var scheduled: Scheduled var reason: Error - init(from state: ConnectingState, scheduled: Scheduled, reason: Error) { + init(from state: ConnectingState, scheduled: Scheduled, reason: Error?) { self.backoffIterator = state.backoffIterator self.readyChannelMuxPromise = state.readyChannelMuxPromise self.scheduled = scheduled - self.reason = reason + self.reason = reason ?? GRPCStatus( + code: .unavailable, + message: "Unexpected connection drop" + ) } init(from state: ConnectedState, scheduled: Scheduled) { @@ -391,7 +394,7 @@ internal final class ConnectionManager { self.startConnecting() // We started connecting so we must transition to the `connecting` state. guard case let .connecting(connecting) = self.state else { - self.invalidState() + self.unreachableState() } multiplexer = connecting.readyChannelMuxPromise.futureResult @@ -432,7 +435,7 @@ internal final class ConnectionManager { self.startConnecting() // We started connecting so we must transition to the `connecting` state. guard case let .connecting(connecting) = self.state else { - self.invalidState() + self.unreachableState() } return connecting.candidateMuxPromise.futureResult case let .connecting(state): @@ -674,20 +677,13 @@ internal final class ConnectionManager { case .shutdown: channel.close(mode: .all, promise: nil) - // These cases are purposefully separated: some crash reporting services provide stack traces - // which don't include the precondition failure message (which contain the invalid state we were - // in). Keeping the cases separate allows us work out the state from the line number. - case .idle: - self.invalidState() - - case .active: - self.invalidState() - - case .ready: - self.invalidState() - - case .transientFailure: - self.invalidState() + case .idle, .transientFailure: + // Received a channelActive when not connecting. Can happen if channelActive and + // channelInactive are reordered. Ignore. + () + case .active, .ready: + // Received a second 'channelActive', already active so ignore. + () } } @@ -700,6 +696,43 @@ internal final class ConnectionManager { ]) switch self.state { + // We can hit inactive in connecting if we see channelInactive before channelActive; that's not + // common but we should tolerate it. + case let .connecting(connecting): + // Should we try connecting again? + switch connecting.reconnect { + // No, shutdown instead. + case .none: + self.logger.debug("shutting down connection") + + let error = GRPCStatus( + code: .unavailable, + message: "The connection was dropped and connection re-establishment is disabled" + ) + + let shutdownState = ShutdownState( + closeFuture: self.eventLoop.makeSucceededFuture(()), + reason: error + ) + + self.state = .shutdown(shutdownState) + // Shutting down, so fail the outstanding promises. + connecting.readyChannelMuxPromise.fail(error) + connecting.candidateMuxPromise.fail(error) + + // Yes, after some time. + case let .after(delay): + let error = GRPCStatus(code: .unavailable, message: "Connection closed while connecting") + // Fail the candidate mux promise. KEep the 'readyChannelMuxPromise' as we'll try again. + connecting.candidateMuxPromise.fail(error) + + let scheduled = self.eventLoop.scheduleTask(in: .seconds(timeInterval: delay)) { + self.startConnecting() + } + self.logger.debug("scheduling connection attempt", metadata: ["delay_secs": "\(delay)"]) + self.state = .transientFailure(.init(from: connecting, scheduled: scheduled, reason: nil)) + } + // The channel is `active` but not `ready`. Should we try again? case let .active(active): switch active.reconnect { @@ -766,14 +799,9 @@ internal final class ConnectionManager { case .shutdown: () - // These cases are purposefully separated: some crash reporting services provide stack traces - // which don't include the precondition failure message (which contain the invalid state we were - // in). Keeping the cases separate allows us work out the state from the line number. - case .connecting: - self.invalidState() - + // Received 'channelInactive' twice; fine, ignore. case .transientFailure: - self.invalidState() + () } } @@ -793,20 +821,20 @@ internal final class ConnectionManager { case .shutdown: () - // These cases are purposefully separated: some crash reporting services provide stack traces - // which don't include the precondition failure message (which contain the invalid state we were - // in). Keeping the cases separate allows us work out the state from the line number. - case .idle: - self.invalidState() - - case .transientFailure: - self.invalidState() + case .idle, .transientFailure: + // No connection or connection attempt exists but connection was marked as ready. This is + // strange. Ignore it in release mode as there's nothing to close and nowehere to fire an + // error to. + assertionFailure("received initial HTTP/2 SETTINGS frame in \(self.state.label) state") case .connecting: - self.invalidState() + // No channel exists to receive initial HTTP/2 SETTINGS frame on... weird. Ignore in release + // mode. + assertionFailure("received initial HTTP/2 SETTINGS frame in \(self.state.label) state") case .ready: - self.invalidState() + // Already received initial HTTP/2 SETTINGS frame; ignore in release mode. + assertionFailure("received initial HTTP/2 SETTINGS frame in \(self.state.label) state") } } @@ -834,17 +862,14 @@ internal final class ConnectionManager { // 'channelInactive()'. () - // These cases are purposefully separated: some crash reporting services provide stack traces - // which don't include the precondition failure message (which contain the invalid state we were - // in). Keeping the cases separate allows us work out the state from the line number. - case .idle: - self.invalidState() + case .idle, .transientFailure: + // There's no connection to idle; ignore. + () case .connecting: - self.invalidState() - - case .transientFailure: - self.invalidState() + // The idle watchdog is started when the connection is active, this shouldn't happen + // in the connecting state. Ignore it in release mode. + assertionFailure("tried to idle a connection in the \(self.state.label) state") } } @@ -908,22 +933,10 @@ extension ConnectionManager { case .shutdown: () - // We can't fail to connect if we aren't trying. - // - // These cases are purposefully separated: some crash reporting services provide stack traces - // which don't include the precondition failure message (which contain the invalid state we were - // in). Keeping the cases separate allows us work out the state from the line number. - case .idle: - self.invalidState() - - case .active: - self.invalidState() - - case .ready: - self.invalidState() - - case .transientFailure: - self.invalidState() + // Connection attempt failed, but no connection attempt is in progress. + case .idle, .active, .ready, .transientFailure: + // Nothing we can do other than ignore in release mode. + assertionFailure("connect promise failed in \(self.state.label) state") } } } @@ -951,17 +964,14 @@ extension ConnectionManager { case .shutdown: () - // These cases are purposefully separated: some crash reporting services provide stack traces - // which don't include the precondition failure message (which contain the invalid state we were - // in). Keeping the cases separate allows us work out the state from the line number. + // We only call startConnecting() if the connection does not exist and after checking what the + // current state is, so none of these states should be reachable. case .connecting: - self.invalidState() - + self.unreachableState() case .active: - self.invalidState() - + self.unreachableState() case .ready: - self.invalidState() + self.unreachableState() } } @@ -1066,11 +1076,11 @@ extension ConnectionManager { } extension ConnectionManager { - private func invalidState( + private func unreachableState( function: StaticString = #function, file: StaticString = #fileID, line: UInt = #line ) -> Never { - preconditionFailure("Invalid state \(self.state) for \(function)", file: file, line: line) + fatalError("Invalid state \(self.state) for \(function)", file: file, line: line) } } diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index c671531ca..174f73112 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -239,6 +239,119 @@ extension ConnectionManagerTests { } } + func testChannelInactiveBeforeActiveWithNoReconnect() throws { + let channel = EmbeddedChannel(loop: self.loop) + let channelPromise = self.loop.makePromise(of: Channel.self) + + let manager = self.makeConnectionManager { _, _ in + return channelPromise.futureResult + } + + // Start the connection. + self.waitForStateChange(from: .idle, to: .connecting) { + // Triggers the connect. + _ = manager.getHTTP2Multiplexer() + self.loop.run() + } + + try channel.pipeline.syncOperations.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: HTTP2StreamMultiplexer( + mode: .client, + channel: channel, + inboundStreamInitializer: nil + ), + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ) + channelPromise.succeed(channel) + // Oops: wrong way around. We should tolerate this. + self.waitForStateChange(from: .connecting, to: .shutdown) { + channel.pipeline.fireChannelInactive() + } + + // Should be ignored. + channel.pipeline.fireChannelActive() + } + + func testChannelInactiveBeforeActiveWillReconnect() throws { + var channels = [EmbeddedChannel(loop: self.loop), EmbeddedChannel(loop: self.loop)] + var channelPromises: [EventLoopPromise] = [self.loop.makePromise(), + self.loop.makePromise()] + var channelFutures = Array(channelPromises.map { $0.futureResult }) + + var configuration = self.defaultConfiguration + configuration.connectionBackoff = .oneSecondFixed + + let manager = self.makeConnectionManager(configuration: configuration) { _, _ in + return channelFutures.removeLast() + } + + // Start the connection. + self.waitForStateChange(from: .idle, to: .connecting) { + // Triggers the connect. + _ = manager.getHTTP2Multiplexer() + self.loop.run() + } + + // Setup the channel. + let channel1 = channels.removeLast() + let channel1Promise = channelPromises.removeLast() + + try channel1.pipeline.syncOperations.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: HTTP2StreamMultiplexer( + mode: .client, + channel: channel1, + inboundStreamInitializer: nil + ), + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ) + channel1Promise.succeed(channel1) + // Oops: wrong way around. We should tolerate this. + self.waitForStateChange(from: .connecting, to: .transientFailure) { + channel1.pipeline.fireChannelInactive() + } + + channel1.pipeline.fireChannelActive() + + // Start the next attempt. + self.waitForStateChange(from: .transientFailure, to: .connecting) { + self.loop.advanceTime(by: .seconds(1)) + } + + let channel2 = channels.removeLast() + let channel2Promise = channelPromises.removeLast() + try channel2.pipeline.syncOperations.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: HTTP2StreamMultiplexer( + mode: .client, + channel: channel1, + inboundStreamInitializer: nil + ), + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ) + + channel2Promise.succeed(channel2) + + try self.waitForStateChange(from: .connecting, to: .ready) { + channel2.pipeline.fireChannelActive() + let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) + XCTAssertNoThrow(try channel2.writeInbound(frame)) + } + } + func testIdleTimeoutWhenThereAreActiveStreams() throws { let channelPromise = self.loop.makePromise(of: Channel.self) let manager = self.makeConnectionManager { _, _ in From f09cfb4d36315e2b48dbba1359179abf3cb2e6ac Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 7 Mar 2023 17:05:10 +0000 Subject: [PATCH 078/580] Bump version number to 1.14.2 (#1574) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.14.2 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 2a93422be..1047b8f47 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 14 /// The patch version. - internal static let patch = 1 + internal static let patch = 2 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 45e2a4c1d975ef490fc7760ed4e7c82eb0071102 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Mar 2023 08:02:55 +0000 Subject: [PATCH 079/580] Update allocation limits (#1579) Motivation: A new NIO release reduced allocations in our allocation tests. Modifications: - Update baseline numbers. Result: CI is green. --- .github/workflows/ci.yaml | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 261ed2a68..54a2a2534 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,44 +56,44 @@ jobs: include: - image: swiftlang/swift:nightly-5.8-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 391000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 175000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 347000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 167000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 173000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 180000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 180000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 169000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 176000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 176000 - image: swift:5.7-jammy env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 391000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 175000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 347000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 167000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 173000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 180000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 180000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 169000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 176000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 176000 - image: swift:5.6-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 392000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 176000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 348000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 168000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 174000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 181000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 181000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 170000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 177000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 177000 - image: swift:5.5-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 422000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 188000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 378000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 180000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 185000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 192000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 192000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 181000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 188000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 188000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest From 03b3f1fc38dce26fe405fa894de04bb7aa92f1c3 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Mar 2023 09:47:57 +0000 Subject: [PATCH 080/580] Fix typo in `protoc-gen-grpc-swift` docs. (#1578) --- Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md index 9413a7de8..23de42967 100644 --- a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md +++ b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md @@ -19,7 +19,7 @@ configuration file which will be used to customize the invocation of `protoc`. First, you must ensure that you have the `protoc` compiler installed. There are multiple ways to do this. Some of the easiest are: -1. If you are on macOS, installing it via `brew install protoc` +1. If you are on macOS, installing it via `brew install protobuf` 2. Download the binary from [Google's github repository](https://github.com/protocolbuffers/protobuf). ### Adding the proto files to your target From b625430f5a12f8f63d3d67a3e4545aad7451551f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 3 Apr 2023 13:43:30 +0100 Subject: [PATCH 081/580] Fix gRPC Web trailers encoding (#1582) Motivation: In gRPC Web HTTP trailers are encoded as a 'regular' gRPC message; that is a length prefixed message. For grpc-web we sent trailers back as regular trailers. Modifications: - Send trailers back as a length prefixed body part. - Update tests. Result: Resolves #1580 --- Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift | 37 ++++++++++++++----- .../GRPCWebToHTTP2ServerCodecTests.swift | 19 +++++++--- .../GRPCWebToHTTP2StateMachineTests.swift | 26 +++++++++---- Tests/GRPCTests/ServerWebTests.swift | 4 +- 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift b/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift index 3c5136b8a..761c307ce 100644 --- a/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift +++ b/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift @@ -453,9 +453,17 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.State { ) ) } else { - // No response buffer; plain gRPC Web. - let trailers = HTTPHeaders(hpackHeaders: trailers) - return .write(.init(part: .end(trailers), promise: promise, closeChannel: closeChannel)) + // No response buffer; plain gRPC Web. Trailers are encoded into the body as a regular + // length-prefixed message. + let buffer = GRPCWebToHTTP2ServerCodec.formatTrailers(trailers, allocator: allocator) + return .write( + .init( + part: .body(.byteBuffer(buffer)), + additionalPart: .end(nil), + promise: promise, + closeChannel: closeChannel + ) + ) } } @@ -671,18 +679,27 @@ extension GRPCWebToHTTP2ServerCodec { allocator: ByteBufferAllocator ) -> ByteBuffer { // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md - let encodedTrailers = trailers.map { name, value, _ in - "\(name): \(value)" - }.joined(separator: "\r\n") + let length = trailers.reduce(0) { partial, trailer in + // +4 for: ":", " ", "\r", "\n" + return partial + trailer.name.utf8.count + trailer.value.utf8.count + 4 + } + var buffer = allocator.buffer(capacity: 5 + length) - var buffer = allocator.buffer(capacity: 5 + encodedTrailers.utf8.count) // Uncompressed trailer byte. buffer.writeInteger(UInt8(0x80)) // Length. - buffer.writeInteger(UInt32(encodedTrailers.utf8.count)) - // Uncompressed trailers. - buffer.writeString(encodedTrailers) + let lengthIndex = buffer.writerIndex + buffer.writeInteger(UInt32(0)) + + var bytesWritten = 0 + for (name, value, _) in trailers { + bytesWritten += buffer.writeString(name) + bytesWritten += buffer.writeString(": ") + bytesWritten += buffer.writeString(value) + bytesWritten += buffer.writeString("\r\n") + } + buffer.setInteger(UInt32(bytesWritten), at: lengthIndex) return buffer } diff --git a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift index 16453eda6..72f144ff6 100644 --- a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift +++ b/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift @@ -24,10 +24,14 @@ import XCTest class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { private func writeTrailers(_ trailers: HPACKHeaders, into buffer: inout ByteBuffer) { - let encoded = trailers.map { "\($0.name): \($0.value)" }.joined(separator: "\r\n") buffer.writeInteger(UInt8(0x80)) - buffer.writeInteger(UInt32(encoded.utf8.count)) - buffer.writeString(encoded) + try! buffer.writeLengthPrefixed(as: UInt32.self) { + var length = 0 + for (name, value, _) in trailers { + length += $0.writeString("\(name): \(value)\r\n") + } + return length + } } private func receiveHead( @@ -106,7 +110,7 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { on channel: EmbeddedChannel, expectedBytes: ByteBuffer? = nil ) throws { - let headers: HPACKHeaders = ["grpc-status": "\(status)"] + let headers: HPACKHeaders = ["grpc-status": "\(status.rawValue)"] let headersPayload: HTTP2Frame.FramePayload = .headers(.init(headers: headers, endStream: true)) assertThat(try channel.writeOutbound(headersPayload), .doesNotThrow()) @@ -128,7 +132,10 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { // Outbound try self.sendResponseHeaders(on: channel) try self.sendBytes([1, 2, 3], on: channel, expectedBytes: [1, 2, 3]) - try self.sendEnd(status: .ok, on: channel) + + var buffer = ByteBuffer() + self.writeTrailers(["grpc-status": "0"], into: &buffer) + try self.sendEnd(status: .ok, on: channel, expectedBytes: buffer) } func testWebTextHappyPath() throws { @@ -150,7 +157,7 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { // Build up the expected response, i.e. the response bytes and the trailers, base64 encoded. var expectedBodyBuffer = ByteBuffer(bytes: [1, 2, 3]) let status = GRPCStatus.Code.ok - self.writeTrailers(["grpc-status": "\(status)"], into: &expectedBodyBuffer) + self.writeTrailers(["grpc-status": "\(status.rawValue)"], into: &expectedBodyBuffer) try self.sendEnd(status: status, on: channel, expectedBytes: expectedBodyBuffer.base64Encoded()) } diff --git a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift index 76747880a..8d644b943 100644 --- a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift +++ b/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift @@ -223,12 +223,11 @@ final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase { promise: nil, allocator: self.allocator ).assertWrite { write in - write.part.assertEnd { - $0.assertSome { trailers in - XCTAssertEqual(trailers[canonicalForm: "grpc-status"], ["0"]) - } + write.part.assertBody { buffer in + var buffer = buffer + let trailers = buffer.readLengthPrefixedMessage().map { String(buffer: $0) } + XCTAssertEqual(trailers, "grpc-status: 0\r\n") } - XCTAssertEqual(write.closeChannel, expectChannelClose) } } @@ -330,15 +329,15 @@ final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase { write.part.assertBody { buffer in var buffer = buffer let base64Encoded = buffer.readString(length: buffer.readableBytes)! - XCTAssertEqual(base64Encoded, "aGVsbG8sIHdvcmxkIYAAAAAOZ3JwYy1zdGF0dXM6IDA=") + XCTAssertEqual(base64Encoded, "aGVsbG8sIHdvcmxkIYAAAAAQZ3JwYy1zdGF0dXM6IDANCg==") let data = Data(base64Encoded: base64Encoded)! buffer.writeData(data) XCTAssertEqual(buffer.readString(length: 13), "hello, world!") XCTAssertEqual(buffer.readInteger(), UInt8(0x80)) - XCTAssertEqual(buffer.readInteger(), UInt32(14)) - XCTAssertEqual(buffer.readString(length: 14), "grpc-status: 0") + XCTAssertEqual(buffer.readInteger(), UInt32(16)) + XCTAssertEqual(buffer.readString(length: 16), "grpc-status: 0\r\n") XCTAssertEqual(buffer.readableBytes, 0) } @@ -672,3 +671,14 @@ extension Optional { } } } + +extension ByteBuffer { + mutating func readLengthPrefixedMessage() -> ByteBuffer? { + // Read off and ignore the compression byte. + if self.readInteger(as: UInt8.self) == nil { + return nil + } + + return self.readLengthPrefixedSlice(as: UInt32.self) + } +} diff --git a/Tests/GRPCTests/ServerWebTests.swift b/Tests/GRPCTests/ServerWebTests.swift index 6d08cdd57..f5eeed860 100644 --- a/Tests/GRPCTests/ServerWebTests.swift +++ b/Tests/GRPCTests/ServerWebTests.swift @@ -41,9 +41,9 @@ class ServerWebTests: EchoTestCaseBase { private func gRPCWebTrailers(status: Int = 0, message: String? = nil) -> Data { var data: Data if let message = message { - data = "grpc-status: \(status)\r\ngrpc-message: \(message)".data(using: .utf8)! + data = "grpc-status: \(status)\r\ngrpc-message: \(message)\r\n".data(using: .utf8)! } else { - data = "grpc-status: \(status)".data(using: .utf8)! + data = "grpc-status: \(status)\r\n".data(using: .utf8)! } // Add the gRPC prefix with the compression byte and the 4 length bytes. From 4fd5a1054ff8ca2aab22bbfad5442686c01b5cf7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 5 Apr 2023 13:26:39 +0100 Subject: [PATCH 082/580] Allow CORS to be configured for gRPC Web (#1583) Motivation: The WebCORS handler unconditionally sets "Access-Control-Allow-Origin" to "*" in response headers regardless of whether the request is a CORS request or whether the client sends credentials. Moreover we don't expose any knobs to control how CORS is configured. Modifications: - Add CORS configuration to the server and server builder - Let the allowed origins be '.any' (i.e. '*") or '.only' (limited to the provided origins) - Let the user configure what headers are permitted in responses. - Let the user configure whether credentialed requests are accepted. Result: More control over CORS Co-authored-by: Cory Benfield --- .../GRPC/GRPCServerPipelineConfigurator.swift | 2 +- Sources/GRPC/Server.swift | 60 +++- Sources/GRPC/ServerBuilder.swift | 9 + Sources/GRPC/WebCORSHandler.swift | 213 ++++++++--- Tests/GRPCTests/WebCORSHandlerTests.swift | 334 ++++++++++++++++++ 5 files changed, 559 insertions(+), 59 deletions(-) create mode 100644 Tests/GRPCTests/WebCORSHandlerTests.swift diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift index 61155426b..eba4f9c5a 100644 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ b/Sources/GRPC/GRPCServerPipelineConfigurator.swift @@ -191,7 +191,7 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan // we'll be on the right event loop and sync operations are fine. let sync = context.pipeline.syncOperations try sync.configureHTTPServerPipeline(withErrorHandling: true) - try sync.addHandler(WebCORSHandler()) + try sync.addHandler(WebCORSHandler(configuration: self.configuration.webCORS)) let scheme = self.configuration.tlsConfiguration == nil ? "http" : "https" try sync.addHandler(GRPCWebToHTTP2ServerCodec(scheme: scheme)) // There's no need to normalize headers for HTTP/1. diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 77b5e8caf..d3c0ba034 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -367,6 +367,9 @@ extension Server { /// the need to recalculate this dictionary each time we receive an rpc. internal var serviceProvidersByName: [Substring: CallHandlerProvider] + /// CORS configuration for gRPC-Web support. + public var webCORS = Configuration.CORS() + #if canImport(NIOSSL) /// Create a `Configuration` with some pre-defined defaults. /// @@ -401,11 +404,9 @@ extension Server { ) { self.target = target self.eventLoopGroup = eventLoopGroup - self - .serviceProvidersByName = Dictionary( - uniqueKeysWithValues: serviceProviders - .map { ($0.serviceName, $0) } - ) + self.serviceProvidersByName = Dictionary( + uniqueKeysWithValues: serviceProviders.map { ($0.serviceName, $0) } + ) self.errorDelegate = errorDelegate self.tlsConfiguration = tls.map { GRPCTLSConfiguration(transforming: $0) } self.connectionKeepalive = connectionKeepalive @@ -451,6 +452,55 @@ extension Server { } } +extension Server.Configuration { + public struct CORS: Hashable, GRPCSendable { + /// Determines which 'origin' header field values are permitted in a CORS request. + public var allowedOrigins: AllowedOrigins + /// Sets the headers which are permitted in a response to a CORS request. + public var allowedHeaders: [String] + /// Enabling this value allows sets the "access-control-allow-credentials" header field + /// to "true" in respones to CORS requests. This must be enabled if the client intends to send + /// credentials. + public var allowCredentialedRequests: Bool + /// The maximum age in seconds which pre-flight CORS requests may be cached for. + public var preflightCacheExpiration: Int + + public init( + allowedOrigins: AllowedOrigins = .all, + allowedHeaders: [String] = ["content-type", "x-grpc-web", "x-user-agent"], + allowCredentialedRequests: Bool = false, + preflightCacheExpiration: Int = 86400 + ) { + self.allowedOrigins = allowedOrigins + self.allowedHeaders = allowedHeaders + self.allowCredentialedRequests = allowCredentialedRequests + self.preflightCacheExpiration = preflightCacheExpiration + } + } +} + +extension Server.Configuration.CORS { + public struct AllowedOrigins: Hashable, Sendable { + enum Wrapped: Hashable, Sendable { + case all + case only([String]) + } + + private(set) var wrapped: Wrapped + private init(_ wrapped: Wrapped) { + self.wrapped = wrapped + } + + /// Allow all origin values. + public static let all = Self(.all) + + /// Allow only the given origin values. + public static func only(_ allowed: [String]) -> Self { + return Self(.only(allowed)) + } + } +} + extension ServerBootstrapProtocol { fileprivate func bind(to target: BindTarget) -> EventLoopFuture { switch target.wrapped { diff --git a/Sources/GRPC/ServerBuilder.swift b/Sources/GRPC/ServerBuilder.swift index 3082d48c0..1600335c8 100644 --- a/Sources/GRPC/ServerBuilder.swift +++ b/Sources/GRPC/ServerBuilder.swift @@ -165,6 +165,15 @@ extension Server.Builder { } } +extension Server.Builder { + /// Set the CORS configuration for gRPC Web. + @discardableResult + public func withCORSConfiguration(_ configuration: Server.Configuration.CORS) -> Self { + self.configuration.webCORS = configuration + return self + } +} + extension Server.Builder { /// Sets the root server logger. Accepted connections will branch from this logger and RPCs on /// each connection will use a logger branched from the connections logger. This logger is made diff --git a/Sources/GRPC/WebCORSHandler.swift b/Sources/GRPC/WebCORSHandler.swift index d851a2959..0a2c838ac 100644 --- a/Sources/GRPC/WebCORSHandler.swift +++ b/Sources/GRPC/WebCORSHandler.swift @@ -17,54 +17,110 @@ import NIOCore import NIOHTTP1 /// Handler that manages the CORS protocol for requests incoming from the browser. -internal class WebCORSHandler { - var requestMethod: HTTPMethod? +internal final class WebCORSHandler { + let configuration: Server.Configuration.CORS + + private var state: State = .idle + private enum State: Equatable { + /// Starting state. + case idle + /// CORS preflight request is in progress. + case processingPreflightRequest + /// "Real" request is in progress. + case processingRequest(origin: String?) + } + + init(configuration: Server.Configuration.CORS) { + self.configuration = configuration + } } extension WebCORSHandler: ChannelInboundHandler { typealias InboundIn = HTTPServerRequestPart + typealias InboundOut = HTTPServerRequestPart typealias OutboundOut = HTTPServerResponsePart func channelRead(context: ChannelHandlerContext, data: NIOAny) { - // If the request is OPTIONS, the request is not propagated further. switch self.unwrapInboundIn(data) { - case let .head(requestHead): - self.requestMethod = requestHead.method - if self.requestMethod == .OPTIONS { - var headers = HTTPHeaders() - headers.add(name: "Access-Control-Allow-Origin", value: "*") - headers.add(name: "Access-Control-Allow-Methods", value: "POST") - headers.add( - name: "Access-Control-Allow-Headers", - value: "content-type,x-grpc-web,x-user-agent" - ) - headers.add(name: "Access-Control-Max-Age", value: "86400") - context.write( - self.wrapOutboundOut(.head(HTTPResponseHead( - version: requestHead.version, - status: .ok, - headers: headers - ))), - promise: nil - ) - return + case let .head(head): + self.receivedRequestHead(context: context, head) + + case let .body(body): + self.receivedRequestBody(context: context, body) + + case let .end(trailers): + self.receivedRequestEnd(context: context, trailers) + } + } + + private func receivedRequestHead(context: ChannelHandlerContext, _ head: HTTPRequestHead) { + if head.method == .OPTIONS, + head.headers.contains(.accessControlRequestMethod), + let origin = head.headers.first(name: "origin") { + // If the request is OPTIONS with a access-control-request-method header it's a CORS + // preflight request and is not propagated further. + self.state = .processingPreflightRequest + self.handlePreflightRequest(context: context, head: head, origin: origin) + } else { + self.state = .processingRequest(origin: head.headers.first(name: "origin")) + context.fireChannelRead(self.wrapInboundOut(.head(head))) + } + } + + private func receivedRequestBody(context: ChannelHandlerContext, _ body: ByteBuffer) { + // OPTIONS requests do not have a body, but still handle this case to be + // cautious. + if self.state == .processingPreflightRequest { + return + } + + context.fireChannelRead(self.wrapInboundOut(.body(body))) + } + + private func receivedRequestEnd(context: ChannelHandlerContext, _ trailers: HTTPHeaders?) { + if self.state == .processingPreflightRequest { + // End of OPTIONS request; reset state and finish the response. + self.state = .idle + context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) + } else { + context.fireChannelRead(self.wrapInboundOut(.end(trailers))) + } + } + + private func handlePreflightRequest( + context: ChannelHandlerContext, + head: HTTPRequestHead, + origin: String + ) { + let responseHead: HTTPResponseHead + + if let allowedOrigin = self.configuration.allowedOrigins.header(origin) { + var headers = HTTPHeaders() + headers.reserveCapacity(4 + self.configuration.allowedHeaders.count) + headers.add(name: .accessControlAllowOrigin, value: allowedOrigin) + headers.add(name: .accessControlAllowMethods, value: "POST") + + for value in self.configuration.allowedHeaders { + headers.add(name: .accessControlAllowHeaders, value: value) } - case .body: - if self.requestMethod == .OPTIONS { - // OPTIONS requests do not have a body, but still handle this case to be - // cautious. - return + + if self.configuration.allowCredentialedRequests { + headers.add(name: .accessControlAllowCredentials, value: "true") } - case .end: - if self.requestMethod == .OPTIONS { - context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) - self.requestMethod = nil - return + if self.configuration.preflightCacheExpiration > 0 { + headers.add( + name: .accessControlMaxAge, + value: "\(self.configuration.preflightCacheExpiration)" + ) } + responseHead = HTTPResponseHead(version: head.version, status: .ok, headers: headers) + } else { + // Not allowed; respond with 403. This is okay in a pre-flight request. + responseHead = HTTPResponseHead(version: head.version, status: .forbidden) } - // The OPTIONS request should be fully handled at this point. - context.fireChannelRead(data) + + context.write(self.wrapOutboundOut(.head(responseHead)), promise: nil) } } @@ -74,25 +130,76 @@ extension WebCORSHandler: ChannelOutboundHandler { func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { let responsePart = self.unwrapOutboundIn(data) switch responsePart { - case let .head(responseHead): - var headers = responseHead.headers - // CORS requires all requests to have an Allow-Origin header. - headers.add(name: "Access-Control-Allow-Origin", value: "*") - //! FIXME: Check whether we can let browsers keep connections alive. It's not possible - // now as the channel has a state that can't be reused since the pipeline is modified to - // inject the gRPC call handler. - headers.add(name: "Connection", value: "close") - - context.write( - self.wrapOutboundOut(.head(HTTPResponseHead( - version: responseHead.version, - status: responseHead.status, - headers: headers - ))), - promise: promise - ) - default: + case var .head(responseHead): + switch self.state { + case let .processingRequest(origin): + self.prepareCORSResponseHead(&responseHead, origin: origin) + context.write(self.wrapOutboundOut(.head(responseHead)), promise: promise) + + case .idle, .processingPreflightRequest: + assertionFailure("Writing response head when no request is in progress") + context.close(promise: nil) + } + + case .body: + context.write(data, promise: promise) + + case .end: + self.state = .idle context.write(data, promise: promise) } } + + private func prepareCORSResponseHead(_ head: inout HTTPResponseHead, origin: String?) { + guard let header = origin.flatMap({ self.configuration.allowedOrigins.header($0) }) else { + // No origin or the origin is not allowed; don't treat it as a CORS request. + return + } + + head.headers.replaceOrAdd(name: .accessControlAllowOrigin, value: header) + + if self.configuration.allowCredentialedRequests { + head.headers.add(name: .accessControlAllowCredentials, value: "true") + } + + //! FIXME: Check whether we can let browsers keep connections alive. It's not possible + // now as the channel has a state that can't be reused since the pipeline is modified to + // inject the gRPC call handler. + head.headers.replaceOrAdd(name: "Connection", value: "close") + } +} + +extension HTTPHeaders { + fileprivate enum CORSHeader: String { + case accessControlRequestMethod = "access-control-request-method" + case accessControlRequestHeaders = "access-control-request-headers" + case accessControlAllowOrigin = "access-control-allow-origin" + case accessControlAllowMethods = "access-control-allow-methods" + case accessControlAllowHeaders = "access-control-allow-headers" + case accessControlAllowCredentials = "access-control-allow-credentials" + case accessControlMaxAge = "access-control-max-age" + } + + fileprivate func contains(_ name: CORSHeader) -> Bool { + return self.contains(name: name.rawValue) + } + + fileprivate mutating func add(name: CORSHeader, value: String) { + self.add(name: name.rawValue, value: value) + } + + fileprivate mutating func replaceOrAdd(name: CORSHeader, value: String) { + self.replaceOrAdd(name: name.rawValue, value: value) + } +} + +extension Server.Configuration.CORS.AllowedOrigins { + internal func header(_ origin: String) -> String? { + switch self.wrapped { + case .all: + return "*" + case let .only(allowed): + return allowed.contains(origin) ? origin : nil + } + } } diff --git a/Tests/GRPCTests/WebCORSHandlerTests.swift b/Tests/GRPCTests/WebCORSHandlerTests.swift new file mode 100644 index 000000000..f9d8255c7 --- /dev/null +++ b/Tests/GRPCTests/WebCORSHandlerTests.swift @@ -0,0 +1,334 @@ +/* + * Copyright 2023, 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. + */ +@testable import GRPC +import NIOCore +import NIOEmbedded +import NIOHTTP1 +import XCTest + +internal final class WebCORSHandlerTests: XCTestCase { + struct PreflightRequestSpec { + var configuration: Server.Configuration.CORS + var requestOrigin: Optional + var expectOrigin: Optional + var expectAllowedHeaders: [String] + var expectAllowCredentials: Bool + var expectMaxAge: Optional + var expectStatus: HTTPResponseStatus = .ok + } + + func runPreflightRequestTest(spec: PreflightRequestSpec) throws { + let channel = EmbeddedChannel(handler: WebCORSHandler(configuration: spec.configuration)) + + var request = HTTPRequestHead(version: .http1_1, method: .OPTIONS, uri: "http://foo.example") + if let origin = spec.requestOrigin { + request.headers.add(name: "origin", value: origin) + } + request.headers.add(name: "access-control-request-method", value: "POST") + try channel.writeRequestPart(.head(request)) + try channel.writeRequestPart(.end(nil)) + + switch try channel.readResponsePart() { + case let .head(response): + XCTAssertEqual(response.version, request.version) + + if let expected = spec.expectOrigin { + XCTAssertEqual(response.headers["access-control-allow-origin"], [expected]) + } else { + XCTAssertFalse(response.headers.contains(name: "access-control-allow-origin")) + } + + if spec.expectAllowedHeaders.isEmpty { + XCTAssertFalse(response.headers.contains(name: "access-control-allow-headers")) + } else { + XCTAssertEqual(response.headers["access-control-allow-headers"], spec.expectAllowedHeaders) + } + + if spec.expectAllowCredentials { + XCTAssertEqual(response.headers["access-control-allow-credentials"], ["true"]) + } else { + XCTAssertFalse(response.headers.contains(name: "access-control-allow-credentials")) + } + + if let maxAge = spec.expectMaxAge { + XCTAssertEqual(response.headers["access-control-max-age"], [maxAge]) + } else { + XCTAssertFalse(response.headers.contains(name: "access-control-max-age")) + } + + XCTAssertEqual(response.status, spec.expectStatus) + + case .body, .end, .none: + XCTFail("Unexpected response part") + } + } + + func testOptionsPreflightAllowAllOrigins() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowedHeaders: ["x-grpc-web"], + allowCredentialedRequests: false, + preflightCacheExpiration: 60 + ), + requestOrigin: "foo", + expectOrigin: "*", + expectAllowedHeaders: ["x-grpc-web"], + expectAllowCredentials: false, + expectMaxAge: "60" + ) + try self.runPreflightRequestTest(spec: spec) + } + + func testOptionsPreflightAllowSomeOrigins() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .only(["bar", "foo"]), + allowedHeaders: ["x-grpc-web"], + allowCredentialedRequests: false, + preflightCacheExpiration: 60 + ), + requestOrigin: "foo", + expectOrigin: "foo", + expectAllowedHeaders: ["x-grpc-web"], + expectAllowCredentials: false, + expectMaxAge: "60" + ) + try self.runPreflightRequestTest(spec: spec) + } + + func testOptionsPreflightAllowNoHeaders() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowedHeaders: [], + allowCredentialedRequests: false, + preflightCacheExpiration: 60 + ), + requestOrigin: "foo", + expectOrigin: "*", + expectAllowedHeaders: [], + expectAllowCredentials: false, + expectMaxAge: "60" + ) + try self.runPreflightRequestTest(spec: spec) + } + + func testOptionsPreflightNoMaxAge() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowedHeaders: [], + allowCredentialedRequests: false, + preflightCacheExpiration: 0 + ), + requestOrigin: "foo", + expectOrigin: "*", + expectAllowedHeaders: [], + expectAllowCredentials: false, + expectMaxAge: nil + ) + try self.runPreflightRequestTest(spec: spec) + } + + func testOptionsPreflightNegativeMaxAge() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowedHeaders: [], + allowCredentialedRequests: false, + preflightCacheExpiration: -1 + ), + requestOrigin: "foo", + expectOrigin: "*", + expectAllowedHeaders: [], + expectAllowCredentials: false, + expectMaxAge: nil + ) + try self.runPreflightRequestTest(spec: spec) + } + + func testOptionsPreflightWithCredentials() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowedHeaders: [], + allowCredentialedRequests: true, + preflightCacheExpiration: 60 + ), + requestOrigin: "foo", + expectOrigin: "*", + expectAllowedHeaders: [], + expectAllowCredentials: true, + expectMaxAge: "60" + ) + try self.runPreflightRequestTest(spec: spec) + } + + func testOptionsPreflightWithDisallowedOrigin() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .only(["foo"]), + allowedHeaders: [], + allowCredentialedRequests: false, + preflightCacheExpiration: 60 + ), + requestOrigin: "bar", + expectOrigin: nil, + expectAllowedHeaders: [], + expectAllowCredentials: false, + expectMaxAge: nil, + expectStatus: .forbidden + ) + try self.runPreflightRequestTest(spec: spec) + } +} + +extension WebCORSHandlerTests { + struct RegularRequestSpec { + var configuration: Server.Configuration.CORS + var requestOrigin: Optional + var expectOrigin: Optional + var expectAllowCredentials: Bool + } + + func runRegularRequestTest( + spec: RegularRequestSpec + ) throws { + let channel = EmbeddedChannel(handler: WebCORSHandler(configuration: spec.configuration)) + + var request = HTTPRequestHead(version: .http1_1, method: .OPTIONS, uri: "http://foo.example") + if let origin = spec.requestOrigin { + request.headers.add(name: "origin", value: origin) + } + + try channel.writeRequestPart(.head(request)) + try channel.writeRequestPart(.end(nil)) + XCTAssertEqual(try channel.readRequestPart(), .head(request)) + XCTAssertEqual(try channel.readRequestPart(), .end(nil)) + + let response = HTTPResponseHead(version: request.version, status: .imATeapot) + try channel.writeResponsePart(.head(response)) + try channel.writeResponsePart(.end(nil)) + + switch try channel.readResponsePart() { + case let .head(head): + // Should not be modified. + XCTAssertEqual(head.version, response.version) + XCTAssertEqual(head.status, response.status) + + if let expected = spec.expectOrigin { + XCTAssertEqual(head.headers["access-control-allow-origin"], [expected]) + } else { + XCTAssertFalse(head.headers.contains(name: "access-control-allow-origin")) + } + + if spec.expectAllowCredentials { + XCTAssertEqual(head.headers["access-control-allow-credentials"], ["true"]) + } else { + XCTAssertFalse(head.headers.contains(name: "access-control-allow-credentials")) + } + + case .body, .end, .none: + XCTFail("Unexpected response part") + } + + XCTAssertEqual(try channel.readResponsePart(), .end(nil)) + } + + func testRegularRequestWithWildcardOrigin() throws { + let spec = RegularRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowCredentialedRequests: false + ), + requestOrigin: "foo", + expectOrigin: "*", + expectAllowCredentials: false + ) + try self.runRegularRequestTest(spec: spec) + } + + func testRegularRequestWithLimitedOrigin() throws { + let spec = RegularRequestSpec( + configuration: .init( + allowedOrigins: .only(["foo", "bar"]), + allowCredentialedRequests: false + ), + requestOrigin: "foo", + expectOrigin: "foo", + expectAllowCredentials: false + ) + try self.runRegularRequestTest(spec: spec) + } + + func testRegularRequestWithNoOrigin() throws { + let spec = RegularRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowCredentialedRequests: false + ), + requestOrigin: nil, + expectOrigin: nil, + expectAllowCredentials: false + ) + try self.runRegularRequestTest(spec: spec) + } + + func testRegularRequestWithCredentials() throws { + let spec = RegularRequestSpec( + configuration: .init( + allowedOrigins: .all, + allowCredentialedRequests: true + ), + requestOrigin: "foo", + expectOrigin: "*", + expectAllowCredentials: true + ) + try self.runRegularRequestTest(spec: spec) + } + + func testRegularRequestWithDisallowedOrigin() throws { + let spec = RegularRequestSpec( + configuration: .init( + allowedOrigins: .only(["foo"]), + allowCredentialedRequests: true + ), + requestOrigin: "bar", + expectOrigin: nil, + expectAllowCredentials: false + ) + try self.runRegularRequestTest(spec: spec) + } +} + +extension EmbeddedChannel { + fileprivate func writeRequestPart(_ part: HTTPServerRequestPart) throws { + try self.writeInbound(part) + } + + fileprivate func readRequestPart() throws -> HTTPServerRequestPart? { + try self.readInbound() + } + + fileprivate func writeResponsePart(_ part: HTTPServerResponsePart) throws { + try self.writeOutbound(part) + } + + fileprivate func readResponsePart() throws -> HTTPServerResponsePart? { + try self.readOutbound() + } +} From 130467153ff0acd642d2f098b69c1ac33373b24e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 11 Apr 2023 08:33:17 +0100 Subject: [PATCH 083/580] Bump version number to 1.15.0 (#1584) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.15.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 1047b8f47..1b8d031f9 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,10 +19,10 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 14 + internal static let minor = 15 /// The patch version. - internal static let patch = 2 + internal static let patch = 0 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From d2d60e6e796170d39bb6bbba10a3c9cfa7f91a61 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 12 Apr 2023 10:55:00 +0100 Subject: [PATCH 084/580] Drop support for Swift 5.5 (#1585) Motivation: Now that 5.8 has been released we can drop support for Swift 5.5. Modifications: - Update CI jobs - Remove conditional branches which are no longer supported - Update docs Result: Swift 5.6 is the lowest supported Swift version --- .github/workflows/ci.yaml | 31 +- .swiftformat | 2 +- Package@swift-5.5.swift | 470 ------------------ README.md | 5 +- .../Implementation/EchoAsyncProvider.swift | 3 - Sources/Examples/Echo/Model/echo.grpc.swift | 13 +- Sources/Examples/Echo/Runtime/Echo.swift | 12 - .../HelloWorld/Client/HelloWorldClient.swift | 9 - .../HelloWorld/Model/helloworld.grpc.swift | 11 +- .../HelloWorld/Server/GreeterProvider.swift | 2 - .../HelloWorld/Server/HelloWorldServer.swift | 9 - .../PacketCapture/PacketCapture.swift | 9 - .../RouteGuide/Client/RouteGuideClient.swift | 9 - .../RouteGuide/Model/route_guide.grpc.swift | 11 +- .../Server/RouteGuideProvider.swift | 4 - .../RouteGuide/Server/RouteGuideServer.swift | 9 - .../ServerHandlerStateMachine+Actions.swift | 2 - .../ServerHandlerStateMachine+Draining.swift | 2 - .../ServerHandlerStateMachine+Finished.swift | 2 - .../ServerHandlerStateMachine+Handling.swift | 2 - .../ServerHandlerStateMachine+Idle.swift | 2 - .../ServerHandlerStateMachine.swift | 2 - ...erverInterceptorStateMachine+Actions.swift | 2 - ...rverInterceptorStateMachine+Finished.swift | 2 - ...InterceptorStateMachine+Intercepting.swift | 2 - .../ServerInterceptorStateMachine.swift | 2 - .../StreamState.swift | 2 - .../Call+AsyncRequestStreamWriter.swift | 3 - ...llationError+GRPCStatusTransformable.swift | 4 - .../GRPCAsyncBidirectionalStreamingCall.swift | 4 - .../GRPCAsyncClientStreamingCall.swift | 4 - .../GRPCAsyncRequestStream.swift | 4 - .../GRPCAsyncRequestStreamWriter.swift | 3 - .../GRPCAsyncResponseStream.swift | 5 - .../GRPCAsyncResponseStreamWriter.swift | 4 - .../GRPCAsyncSequenceProducerDelegate.swift | 5 - .../GRPCAsyncServerCallContext.swift | 4 - .../GRPCAsyncServerHandler.swift | 3 - .../GRPCAsyncServerStreamingCall.swift | 4 - .../GRPCAsyncUnaryCall.swift | 4 - .../GRPCAsyncWriterSinkDelegate.swift | 3 - .../GRPCChannel+AsyncAwaitSupport.swift | 4 - .../GRPCClient+AsyncAwaitSupport.swift | 4 - .../GRPC/AsyncAwaitSupport/GRPCSendable.swift | 14 +- .../AsyncAwaitSupport/NIOAsyncWrappers.swift | 3 - Sources/GRPC/CallOptions.swift | 14 +- Sources/GRPC/ClientCalls/Call.swift | 2 - .../GRPC/ClientCalls/ResponseContainers.swift | 2 - Sources/GRPC/ClientConnection.swift | 54 +- .../Compression/CompressionAlgorithm.swift | 2 +- .../GRPC/Compression/DecompressionLimit.swift | 4 +- .../GRPC/Compression/MessageEncoding.swift | 8 +- Sources/GRPC/ConnectionBackoff.swift | 6 +- Sources/GRPC/ConnectionKeepalive.swift | 2 +- .../GRPC/ConnectionManager+Delegates.swift | 2 +- Sources/GRPC/ConnectionManager.swift | 6 +- .../ConnectionPool/ConnectionManagerID.swift | 2 +- .../GRPC/ConnectionPool/GRPCChannelPool.swift | 16 +- Sources/GRPC/ConnectionPool/PoolManager.swift | 2 - Sources/GRPC/ConnectivityState.swift | 8 +- Sources/GRPC/Docs.docc/index.md | 5 +- Sources/GRPC/FakeChannel.swift | 2 - .../GRPC/GRPCChannel/GRPCChannelBuilder.swift | 10 - Sources/GRPC/GRPCClient.swift | 4 +- Sources/GRPC/GRPCClientChannelHandler.swift | 2 +- Sources/GRPC/GRPCServiceDescription.swift | 4 +- Sources/GRPC/GRPCStatus.swift | 6 +- Sources/GRPC/GRPCTLSConfiguration.swift | 4 +- .../GRPC/Interceptor/ClientInterceptors.swift | 52 -- Sources/GRPC/Interceptor/MessageParts.swift | 4 +- Sources/GRPC/TimeLimit.swift | 4 +- .../Generated/test.grpc.swift | 33 +- .../TestServiceAsyncProvider.swift | 2 - .../Generator-Client.swift | 9 +- .../Generator-Server.swift | 4 - Sources/protoc-gen-grpc-swift/Generator.swift | 8 - .../AsyncAwaitSupport/AsyncClientTests.swift | 3 - .../AsyncIntegrationTests.swift | 3 - .../AsyncSequence+Helpers.swift | 4 - .../ServerHandlerStateMachineTests.swift | 2 - ...erceptorStateMachineStreamStateTests.swift | 3 - .../ServerInterceptorStateMachineTests.swift | 3 - .../GRPCAsyncRequestStreamTests.swift | 3 - .../GRPCAsyncResponseStreamWriterTests.swift | 2 - .../InterceptorsAsyncTests.swift | 3 - .../AsyncAwaitSupport/XCTest+AsyncAwait.swift | 5 +- Tests/GRPCTests/ClientTimeoutTests.swift | 2 - .../Normalization/normalization.grpc.swift | 11 +- Tests/GRPCTests/ConnectionManagerTests.swift | 2 - .../ConnectionPoolDelegates.swift | 16 +- .../DelegatingErrorHandlerTests.swift | 4 +- .../EchoInterceptorFactories.swift | 4 - Tests/GRPCTests/EchoMetadataTests.swift | 2 - Tests/GRPCTests/ErrorRecordingDelegate.swift | 2 - .../GRPCTests/GRPCAsyncClientCallTests.swift | 6 +- .../GRPCAsyncServerHandlerTests.swift | 3 - .../GRPCTests/GRPCInteroperabilityTests.swift | 2 - Tests/GRPCTests/InterceptorsTests.swift | 2 - Tests/GRPCTests/XCTestHelpers.swift | 4 - 99 files changed, 79 insertions(+), 1005 deletions(-) delete mode 100644 Package@swift-5.5.swift diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54a2a2534..a79d63662 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,7 +24,10 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-5.8-focal + - image: swiftlang/swift:nightly-focal + # No TSAN because of: https://github.com/apple/swift/issues/59068 + # swift-test-flags: "--sanitize=thread" + - image: swift:5.8-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - image: swift:5.7-jammy @@ -33,8 +36,6 @@ jobs: - image: swift:5.6-focal # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.5-focal - swift-test-flags: "--sanitize=thread" name: Build and Test on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -54,7 +55,17 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-5.8-focal + - image: swiftlang/swift:nightly-focal + env: + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 347000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 167000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 169000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 176000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 176000 + - image: swift:5.8-jammy env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 347000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 167000 @@ -84,16 +95,6 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 177000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 177000 - - image: swift:5.5-focal - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 378000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 180000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 181000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 188000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 188000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest @@ -111,9 +112,9 @@ jobs: matrix: include: - image: swiftlang/swift:nightly-focal + - image: swift:5.8-jammy - image: swift:5.7-jammy - image: swift:5.6-focal - - image: swift:5.5-focal name: Integration Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: diff --git a/.swiftformat b/.swiftformat index 83b402ee9..9410761e8 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,5 +1,5 @@ # Language version. ---swiftversion 5.2 +--swiftversion 5.6 # Ignore generated files. --exclude **/LinuxMain.swift,**/XCTestManifests.swift,**/*.grpc.swift,**/*.pb.swift diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift deleted file mode 100644 index ac90779eb..000000000 --- a/Package@swift-5.5.swift +++ /dev/null @@ -1,470 +0,0 @@ -// swift-tools-version:5.5 -/* - * Copyright 2022, 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 PackageDescription -// swiftformat puts the next import before the tools version. -// swiftformat:disable:next sortedImports -import class Foundation.ProcessInfo - -let grpcPackageName = "grpc-swift" -let grpcProductName = "GRPC" -let cgrpcZlibProductName = "CGRPCZlib" -let grpcTargetName = grpcProductName -let cgrpcZlibTargetName = cgrpcZlibProductName - -let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil - -// MARK: - Package Dependencies - -let packageDependencies: [Package.Dependency] = [ - .package( - url: "https://github.com/apple/swift-nio.git", - from: "2.42.0" - ), - .package( - url: "https://github.com/apple/swift-nio-http2.git", - from: "1.22.0" - ), - .package( - url: "https://github.com/apple/swift-nio-transport-services.git", - from: "1.15.0" - ), - .package( - url: "https://github.com/apple/swift-nio-extras.git", - from: "1.4.0" - ), - .package( - name: "SwiftProtobuf", - url: "https://github.com/apple/swift-protobuf.git", - from: "1.20.2" - ), - .package( - url: "https://github.com/apple/swift-log.git", - from: "1.4.0" - ), - .package( - url: "https://github.com/apple/swift-argument-parser.git", - from: "1.0.0" - ), -].appending( - .package( - url: "https://github.com/apple/swift-nio-ssl.git", - from: "2.23.0" - ), - if: includeNIOSSL -) - -// MARK: - Target Dependencies - -extension Target.Dependency { - // Target dependencies; external - static let grpc: Self = .target(name: grpcTargetName) - static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) - static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") - - // Target dependencies; internal - static let grpcSampleData: Self = .target(name: "GRPCSampleData") - static let echoModel: Self = .target(name: "EchoModel") - static let echoImplementation: Self = .target(name: "EchoImplementation") - static let helloWorldModel: Self = .target(name: "HelloWorldModel") - static let routeGuideModel: Self = .target(name: "RouteGuideModel") - static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") - static let interopTestImplementation: Self = - .target(name: "GRPCInteroperabilityTestsImplementation") - - // Product dependencies - static let argumentParser: Self = .product( - name: "ArgumentParser", - package: "swift-argument-parser" - ) - static let nio: Self = .product(name: "NIO", package: "swift-nio") - static let nioConcurrencyHelpers: Self = .product( - name: "NIOConcurrencyHelpers", - package: "swift-nio" - ) - static let nioCore: Self = .product(name: "NIOCore", package: "swift-nio") - static let nioEmbedded: Self = .product(name: "NIOEmbedded", package: "swift-nio") - static let nioExtras: Self = .product(name: "NIOExtras", package: "swift-nio-extras") - static let nioFoundationCompat: Self = .product(name: "NIOFoundationCompat", package: "swift-nio") - static let nioHTTP1: Self = .product(name: "NIOHTTP1", package: "swift-nio") - static let nioHTTP2: Self = .product(name: "NIOHTTP2", package: "swift-nio-http2") - static let nioPosix: Self = .product(name: "NIOPosix", package: "swift-nio") - static let nioSSL: Self = .product(name: "NIOSSL", package: "swift-nio-ssl") - static let nioTLS: Self = .product(name: "NIOTLS", package: "swift-nio") - static let nioTransportServices: Self = .product( - name: "NIOTransportServices", - package: "swift-nio-transport-services" - ) - static let logging: Self = .product(name: "Logging", package: "swift-log") - static let protobuf: Self = .product(name: "SwiftProtobuf", package: "SwiftProtobuf") - static let protobufPluginLibrary: Self = .product( - name: "SwiftProtobufPluginLibrary", - package: "SwiftProtobuf" - ) -} - -// MARK: - Targets - -extension Target { - static let grpc: Target = .target( - name: grpcTargetName, - dependencies: [ - .cgrpcZlib, - .nio, - .nioCore, - .nioPosix, - .nioEmbedded, - .nioFoundationCompat, - .nioTLS, - .nioTransportServices, - .nioHTTP1, - .nioHTTP2, - .nioExtras, - .logging, - .protobuf, - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Sources/GRPC" - ) - - static let cgrpcZlib: Target = .target( - name: cgrpcZlibTargetName, - path: "Sources/CGRPCZlib", - linkerSettings: [ - .linkedLibrary("z"), - ] - ) - - static let protocGenGRPCSwift: Target = .executableTarget( - name: "protoc-gen-grpc-swift", - dependencies: [ - .protobuf, - .protobufPluginLibrary, - ], - exclude: [ - "README.md", - ] - ) - - static let grpcTests: Target = .testTarget( - name: "GRPCTests", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .helloWorldModel, - .interopTestModels, - .interopTestImplementation, - .grpcSampleData, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .nioTLS, - .nioHTTP1, - .nioHTTP2, - .nioEmbedded, - .nioTransportServices, - .logging, - ].appending( - .nioSSL, if: includeNIOSSL - ), - exclude: [ - "Codegen/Normalization/normalization.proto", - ] - ) - - static let interopTestModels: Target = .target( - name: "GRPCInteroperabilityTestModels", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - exclude: [ - "README.md", - "generate.sh", - "src/proto/grpc/testing/empty.proto", - "src/proto/grpc/testing/empty_service.proto", - "src/proto/grpc/testing/messages.proto", - "src/proto/grpc/testing/test.proto", - "unimplemented_call.patch", - ] - ) - - static let interopTestImplementation: Target = .target( - name: "GRPCInteroperabilityTestsImplementation", - dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .nioHTTP1, - .logging, - ].appending( - .nioSSL, if: includeNIOSSL - ) - ) - - static let interopTests: Target = .executableTarget( - name: "GRPCInteroperabilityTests", - dependencies: [ - .grpc, - .interopTestImplementation, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ] - ) - - static let backoffInteropTest: Target = .executableTarget( - name: "GRPCConnectionBackoffInteropTest", - dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ], - exclude: [ - "README.md", - ] - ) - - static let perfTests: Target = .executableTarget( - name: "GRPCPerformanceTests", - dependencies: [ - .grpc, - .grpcSampleData, - .nioCore, - .nioEmbedded, - .nioPosix, - .nioHTTP2, - .argumentParser, - ] - ) - - static let grpcSampleData: Target = .target( - name: "GRPCSampleData", - dependencies: includeNIOSSL ? [.nioSSL] : [], - exclude: [ - "bundle.p12", - ] - ) - - static let echoModel: Target = .target( - name: "EchoModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Sources/Examples/Echo/Model", - exclude: [ - "echo.proto", - ] - ) - - static let echoImplementation: Target = .target( - name: "EchoImplementation", - dependencies: [ - .echoModel, - .grpc, - .nioCore, - .nioHTTP2, - .protobuf, - ], - path: "Sources/Examples/Echo/Implementation" - ) - - static let echo: Target = .executableTarget( - name: "Echo", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .grpcSampleData, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Sources/Examples/Echo/Runtime" - ) - - static let helloWorldModel: Target = .target( - name: "HelloWorldModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Sources/Examples/HelloWorld/Model", - exclude: [ - "helloworld.proto", - ] - ) - - static let helloWorldClient: Target = .executableTarget( - name: "HelloWorldClient", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/HelloWorld/Client" - ) - - static let helloWorldServer: Target = .executableTarget( - name: "HelloWorldServer", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/HelloWorld/Server" - ) - - static let routeGuideModel: Target = .target( - name: "RouteGuideModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Sources/Examples/RouteGuide/Model", - exclude: [ - "route_guide.proto", - ] - ) - - static let routeGuideClient: Target = .executableTarget( - name: "RouteGuideClient", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/RouteGuide/Client" - ) - - static let routeGuideServer: Target = .executableTarget( - name: "RouteGuideServer", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .argumentParser, - ], - path: "Sources/Examples/RouteGuide/Server" - ) - - static let packetCapture: Target = .executableTarget( - name: "PacketCapture", - dependencies: [ - .grpc, - .echoModel, - .nioCore, - .nioPosix, - .nioExtras, - .argumentParser, - ], - path: "Sources/Examples/PacketCapture", - exclude: [ - "README.md", - ] - ) -} - -// MARK: - Products - -extension Product { - static let grpc: Product = .library( - name: grpcProductName, - targets: [grpcTargetName] - ) - - static let cgrpcZlib: Product = .library( - name: cgrpcZlibProductName, - targets: [cgrpcZlibTargetName] - ) - - static let protocGenGRPCSwift: Product = .executable( - name: "protoc-gen-grpc-swift", - targets: ["protoc-gen-grpc-swift"] - ) -} - -// MARK: - Package - -let package = Package( - name: grpcPackageName, - products: [ - .grpc, - .cgrpcZlib, - .protocGenGRPCSwift, - ], - dependencies: packageDependencies, - targets: [ - // Products - .grpc, - .cgrpcZlib, - .protocGenGRPCSwift, - - // Tests etc. - .grpcTests, - .interopTestModels, - .interopTestImplementation, - .interopTests, - .backoffInteropTest, - .perfTests, - .grpcSampleData, - - // Examples - .echoModel, - .echoImplementation, - .echo, - .helloWorldModel, - .helloWorldClient, - .helloWorldServer, - .routeGuideModel, - .routeGuideClient, - .routeGuideServer, - .packetCapture, - ] -) - -extension Array { - func appending(_ element: Element, if condition: Bool) -> [Element] { - if condition { - return self + [element] - } else { - return self - } - } -} diff --git a/README.md b/README.md index cc337a324..f58727738 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ gRPC Swift Version | Earliest Swift Version -------------------|----------------------- `1.0.0 ..< 1.8.0` | 5.2 `1.8.0 ..< 1.11.0` | 5.4 -`1.11.0...` | 5.5 +`1.11.0..< 1.16.0`.| 5.5 +`1.16.0...` | 5.6 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. @@ -59,7 +60,7 @@ package dependency to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.9.0") + .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.15.0") ] ``` diff --git a/Sources/Examples/Echo/Implementation/EchoAsyncProvider.swift b/Sources/Examples/Echo/Implementation/EchoAsyncProvider.swift index 166392c58..0c06abeb5 100644 --- a/Sources/Examples/Echo/Implementation/EchoAsyncProvider.swift +++ b/Sources/Examples/Echo/Implementation/EchoAsyncProvider.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import EchoModel import GRPC @@ -68,5 +67,3 @@ public final class EchoAsyncProvider: Echo_EchoAsyncProvider { } } } - -#endif // compiler(>=5.6) diff --git a/Sources/Examples/Echo/Model/echo.grpc.swift b/Sources/Examples/Echo/Model/echo.grpc.swift index 900fbd2d5..f1bd21112 100644 --- a/Sources/Examples/Echo/Model/echo.grpc.swift +++ b/Sources/Examples/Echo/Model/echo.grpc.swift @@ -136,10 +136,8 @@ extension Echo_EchoClientProtocol { } } -#if compiler(>=5.6) @available(*, deprecated) extension Echo_EchoClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @available(*, deprecated, renamed: "Echo_EchoNIOClient") public final class Echo_EchoClient: Echo_EchoClientProtocol { @@ -195,7 +193,6 @@ public struct Echo_EchoNIOClient: Echo_EchoClientProtocol { } } -#if compiler(>=5.6) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol Echo_EchoAsyncClientProtocol: GRPCClient { static var serviceDescriptor: GRPCServiceDescriptor { get } @@ -367,9 +364,7 @@ public struct Echo_EchoAsyncClient: Echo_EchoAsyncClientProtocol { } } -#endif // compiler(>=5.6) - -public protocol Echo_EchoClientInterceptorFactoryProtocol: GRPCSendable { +public protocol Echo_EchoClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'get'. func makeGetInterceptors() -> [ClientInterceptor] @@ -423,10 +418,8 @@ public enum Echo_EchoClientMetadata { } } -#if compiler(>=5.6) @available(swift, deprecated: 5.6) extension Echo_EchoTestClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @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.") public final class Echo_EchoTestClient: Echo_EchoClientProtocol { @@ -618,8 +611,6 @@ extension Echo_EchoProvider { } } -#if compiler(>=5.6) - /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol Echo_EchoAsyncProvider: CallHandlerProvider { @@ -714,8 +705,6 @@ extension Echo_EchoAsyncProvider { } } -#endif // compiler(>=5.6) - public protocol Echo_EchoServerInterceptorFactoryProtocol { /// - Returns: Interceptors to use when handling 'get'. diff --git a/Sources/Examples/Echo/Runtime/Echo.swift b/Sources/Examples/Echo/Runtime/Echo.swift index b8ee8c40d..65abd883d 100644 --- a/Sources/Examples/Echo/Runtime/Echo.swift +++ b/Sources/Examples/Echo/Runtime/Echo.swift @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#if compiler(>=5.6) - import ArgumentParser import EchoImplementation import EchoModel @@ -246,12 +243,3 @@ func echoUpdate(client: Echo_EchoAsyncClient, message: String) async throws { print("update received: \(response.text)") } } - -#else -@main -enum Echo { - static func main() { - print("This example requires Swift >= 5.6") - } -} -#endif diff --git a/Sources/Examples/HelloWorld/Client/HelloWorldClient.swift b/Sources/Examples/HelloWorld/Client/HelloWorldClient.swift index 41de5df33..253e7c19c 100644 --- a/Sources/Examples/HelloWorld/Client/HelloWorldClient.swift +++ b/Sources/Examples/HelloWorld/Client/HelloWorldClient.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import ArgumentParser import GRPC import HelloWorldModel @@ -68,11 +67,3 @@ struct HelloWorld: AsyncParsableCommand { } } } -#else -@main -enum HelloWorld { - static func main() { - fatalError("This example requires swift >= 5.6") - } -} -#endif // compiler(>=5.6) diff --git a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift index b5883f387..cc229bcab 100644 --- a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift +++ b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift @@ -63,10 +63,8 @@ extension Helloworld_GreeterClientProtocol { } } -#if compiler(>=5.6) @available(*, deprecated) extension Helloworld_GreeterClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @available(*, deprecated, renamed: "Helloworld_GreeterNIOClient") public final class Helloworld_GreeterClient: Helloworld_GreeterClientProtocol { @@ -122,7 +120,6 @@ public struct Helloworld_GreeterNIOClient: Helloworld_GreeterClientProtocol { } } -#if compiler(>=5.6) /// The greeting service definition. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol Helloworld_GreeterAsyncClientProtocol: GRPCClient { @@ -190,9 +187,7 @@ public struct Helloworld_GreeterAsyncClient: Helloworld_GreeterAsyncClientProtoc } } -#endif // compiler(>=5.6) - -public protocol Helloworld_GreeterClientInterceptorFactoryProtocol: GRPCSendable { +public protocol Helloworld_GreeterClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'sayHello'. func makeSayHelloInterceptors() -> [ClientInterceptor] @@ -253,8 +248,6 @@ extension Helloworld_GreeterProvider { } } -#if compiler(>=5.6) - /// The greeting service definition. /// /// To implement a server, implement an object which conforms to this protocol. @@ -304,8 +297,6 @@ extension Helloworld_GreeterAsyncProvider { } } -#endif // compiler(>=5.6) - public protocol Helloworld_GreeterServerInterceptorFactoryProtocol { /// - Returns: Interceptors to use when handling 'sayHello'. diff --git a/Sources/Examples/HelloWorld/Server/GreeterProvider.swift b/Sources/Examples/HelloWorld/Server/GreeterProvider.swift index d4be35bef..bf6ea4449 100644 --- a/Sources/Examples/HelloWorld/Server/GreeterProvider.swift +++ b/Sources/Examples/HelloWorld/Server/GreeterProvider.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import GRPC import HelloWorldModel import NIOCore @@ -32,4 +31,3 @@ final class GreeterProvider: Helloworld_GreeterAsyncProvider { } } } -#endif // compiler(>=5.6) diff --git a/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift b/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift index aa78f86f0..038728a9f 100644 --- a/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift +++ b/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import ArgumentParser import GRPC import HelloWorldModel @@ -44,11 +43,3 @@ struct HelloWorld: AsyncParsableCommand { try await server.onClose.get() } } -#else -@main -enum HelloWorld { - static func main() { - fatalError("This example requires swift >= 5.6") - } -} -#endif // compiler(>=5.6) diff --git a/Sources/Examples/PacketCapture/PacketCapture.swift b/Sources/Examples/PacketCapture/PacketCapture.swift index 15178576e..8d1544b47 100644 --- a/Sources/Examples/PacketCapture/PacketCapture.swift +++ b/Sources/Examples/PacketCapture/PacketCapture.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import ArgumentParser import EchoModel import GRPC @@ -84,11 +83,3 @@ struct PCAP: AsyncParsableCommand { try await echo.channel.close().get() } } -#else -@main -enum PCAP { - static func main() { - print("This example requires Swift >= 5.6") - } -} -#endif // compiler(>=5.6) diff --git a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift index c6d416a5a..47710deef 100644 --- a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift +++ b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import ArgumentParser import Foundation import GRPC @@ -247,11 +246,3 @@ extension Routeguide_Feature: CustomStringConvertible { return "\(self.name) at \(self.location)" } } -#else -@main -enum NotAvailable { - static func main() { - print("This example requires Swift >= 5.6") - } -} -#endif // compiler(>=5.6) diff --git a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift index 641d470cf..e8ab16682 100644 --- a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift +++ b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift @@ -154,10 +154,8 @@ extension Routeguide_RouteGuideClientProtocol { } } -#if compiler(>=5.6) @available(*, deprecated) extension Routeguide_RouteGuideClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @available(*, deprecated, renamed: "Routeguide_RouteGuideNIOClient") public final class Routeguide_RouteGuideClient: Routeguide_RouteGuideClientProtocol { @@ -213,7 +211,6 @@ public struct Routeguide_RouteGuideNIOClient: Routeguide_RouteGuideClientProtoco } } -#if compiler(>=5.6) /// Interface exported by the server. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol Routeguide_RouteGuideAsyncClientProtocol: GRPCClient { @@ -386,9 +383,7 @@ public struct Routeguide_RouteGuideAsyncClient: Routeguide_RouteGuideAsyncClient } } -#endif // compiler(>=5.6) - -public protocol Routeguide_RouteGuideClientInterceptorFactoryProtocol: GRPCSendable { +public protocol Routeguide_RouteGuideClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'getFeature'. func makeGetFeatureInterceptors() -> [ClientInterceptor] @@ -531,8 +526,6 @@ extension Routeguide_RouteGuideProvider { } } -#if compiler(>=5.6) - /// Interface exported by the server. /// /// To implement a server, implement an object which conforms to this protocol. @@ -645,8 +638,6 @@ extension Routeguide_RouteGuideAsyncProvider { } } -#endif // compiler(>=5.6) - public protocol Routeguide_RouteGuideServerInterceptorFactoryProtocol { /// - Returns: Interceptors to use when handling 'getFeature'. diff --git a/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift b/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift index 301a56e02..63fa450b5 100644 --- a/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift +++ b/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift @@ -19,8 +19,6 @@ import NIOConcurrencyHelpers import NIOCore import RouteGuideModel -#if compiler(>=5.6) - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) internal final class RouteGuideProvider: Routeguide_RouteGuideAsyncProvider { private let features: [Routeguide_Feature] @@ -130,8 +128,6 @@ internal final actor Notes { } } -#endif // compiler(>=5.6) - private func degreesToRadians(_ degrees: Double) -> Double { return degrees * .pi / 180.0 } diff --git a/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift b/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift index 192478bc6..02af3b2b6 100644 --- a/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift +++ b/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import ArgumentParser import struct Foundation.Data import struct Foundation.URL @@ -64,11 +63,3 @@ struct RouteGuide: AsyncParsableCommand { try await server.onClose.get() } } -#else -@main -enum RouteGuide { - static func main() { - print("This example requires Swift >= 5.6") - } -} -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift index 8c75f0e5e..80b5cf67a 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOHPACK @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -94,4 +93,3 @@ extension ServerHandlerStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift index 857beda70..5aa1f58eb 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOHPACK @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -107,4 +106,3 @@ extension ServerHandlerStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift index c9637a1f0..5395775b9 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOHPACK @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -78,4 +77,3 @@ extension ServerHandlerStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift index a05787d86..7dd6340fb 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOHPACK @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -110,4 +109,3 @@ extension ServerHandlerStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift index fa51bcd4b..a017bddac 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOHPACK @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -78,4 +77,3 @@ extension ServerHandlerStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift index f0ab4fa77..3e7098a98 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOHPACK @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -359,4 +358,3 @@ extension ServerHandlerStateMachine.Finished { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift index dd3231059..c570e20b6 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) extension ServerInterceptorStateMachine { @usableFromInline enum InterceptAction: Hashable { @@ -65,4 +64,3 @@ extension ServerInterceptorStateMachine { case none } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift index 483240d50..3c1a2445f 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) extension ServerInterceptorStateMachine { /// The 'Finished' state is, as the name suggests, a terminal state. Nothing can happen in this /// state. @@ -91,4 +90,3 @@ extension ServerInterceptorStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift index 0d0a19e9b..2596b319e 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) extension ServerInterceptorStateMachine { /// The 'Intercepting' state is responsible for validating that appropriate message parts are /// forwarded to the interceptor pipeline and that messages parts which have been emitted from the @@ -138,4 +137,3 @@ extension ServerInterceptorStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift index 8863bc6b6..cbbecf8d0 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) @usableFromInline internal struct ServerInterceptorStateMachine { @usableFromInline @@ -283,4 +282,3 @@ extension ServerInterceptorStateMachine.Finished { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift index 2ba686209..5fddb3c39 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) extension ServerInterceptorStateMachine { @usableFromInline internal enum StreamFilter: Hashable { @@ -99,4 +98,3 @@ extension ServerInterceptorStateMachine { } } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift index 57480f356..720ced509 100644 --- a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOCore @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -45,5 +44,3 @@ extension Call where Request: Sendable, Response: Sendable { return (GRPCAsyncRequestStreamWriter(asyncWriter: writer.writer), writer.sink) } } - -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift b/Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift index 3539860f9..a64ba5955 100644 --- a/Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift +++ b/Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift @@ -14,13 +14,9 @@ * limitations under the License. */ -#if compiler(>=5.6) - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CancellationError: GRPCStatusTransformable { public func makeGRPCStatus() -> GRPCStatus { return GRPCStatus(code: .unavailable, message: nil) } } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 26c20e501..690e113f1 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - import NIOCore import NIOHPACK @@ -187,5 +185,3 @@ internal enum AsyncCall { } } } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift index 3c37f5691..cc006e5d2 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - import NIOCore import NIOHPACK @@ -110,5 +108,3 @@ public struct GRPCAsyncClientStreamingCall=5.6) import NIOCore /// A type for the stream of request messages send to a gRPC server method. @@ -158,5 +156,3 @@ public struct GRPCAsyncRequestStream: AsyncSequence { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension GRPCAsyncRequestStream: Sendable where Element: Sendable {} - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift index 7bfe524c0..9516d00a6 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import NIOCore /// An object allowing the holder -- a client -- to send requests on an RPC. @@ -95,5 +94,3 @@ public struct GRPCAsyncRequestStreamWriter: Sendable { self.asyncWriter.finish(error: error) } } - -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift index dc4bb6747..225fa31d8 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#if compiler(>=5.6) - import NIOCore /// This is currently a wrapper around AsyncThrowingStream because we want to be @@ -60,5 +57,3 @@ public struct GRPCAsyncResponseStream: AsyncSequence { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension GRPCAsyncResponseStream: Sendable {} - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift index 3d06c007f..c00490c8a 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#if compiler(>=5.6) import NIOCore /// Writer for server-streaming RPC handlers to provide responses. @@ -168,5 +166,3 @@ public struct GRPCAsyncResponseStreamWriter: Sendable { return TestingStreamWriter(writer: writer, stream: responseStream) } } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift index 817a48e2a..baca7ba91 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#if compiler(>=5.6) - import NIOCore @usableFromInline @@ -31,5 +28,3 @@ internal struct GRPCAsyncSequenceProducerDelegate: NIOAsyncSequenceProducerDeleg @inlinable internal func didTerminate() {} } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift index 7b2ee6c3f..73da1c527 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - import Logging import NIOConcurrencyHelpers import NIOHPACK @@ -115,5 +113,3 @@ extension GRPCAsyncServerCallContext { } } } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift index fb3bf311a..02a237694 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import DequeModule import Logging import NIOCore @@ -860,5 +859,3 @@ internal struct ServerHandlerComponents< self.task.cancel() } } - -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift index 03a7075c6..2fddbba4b 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift @@ -14,8 +14,6 @@ * limitations under the License. */ -#if compiler(>=5.6) - import NIOCore import NIOHPACK @@ -128,5 +126,3 @@ public struct GRPCAsyncServerStreamingCall=5.6) - import NIOHPACK /// A unary gRPC call. The request is sent on initialization. @@ -104,5 +102,3 @@ public struct GRPCAsyncUnaryCall: Sendabl Self(call: call, request) } } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift index 3cfa6f13c..9e9b088a2 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#if compiler(>=5.6) import DequeModule import NIOCore @@ -45,4 +43,3 @@ internal struct GRPCAsyncWriterSinkDelegate: NIOAsyncWriterSi self._didTerminate?(error) } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift index 8e781246b..fff10db18 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - import SwiftProtobuf @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -223,5 +221,3 @@ extension GRPCChannel { ) } } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift index 8f675cf78..91ba44eae 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - import SwiftProtobuf @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -481,5 +479,3 @@ extension AsyncStream { } } } - -#endif diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift index d0f0906dc..a94525a48 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift @@ -13,25 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import NIOCore -#if compiler(>=5.6) +@available(*, deprecated, renamed: "Swift.Sendable") public typealias GRPCSendable = Swift.Sendable -#else -public typealias GRPCSendable = Any -#endif // compiler(>=5.6) -#if compiler(>=5.6) @preconcurrency public protocol GRPCPreconcurrencySendable: Sendable {} -#else -public protocol GRPCPreconcurrencySendable {} -#endif // compiler(>=5.6) -#if compiler(>=5.6) @preconcurrency public typealias GRPCChannelInitializer = @Sendable (Channel) -> EventLoopFuture -#else -public typealias GRPCChannelInitializer = (Channel) -> EventLoopFuture -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift b/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift index 40bffd123..9269d9b2f 100644 --- a/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift +++ b/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#if compiler(>=5.6) import NIOCore /// Unchecked-sendable wrapper for ``NIOAsyncWriter/Sink``, to avoid getting sendability warnings. @@ -48,4 +46,3 @@ internal struct AsyncSink: @unchecked Sendable { self.sink.finish() } } -#endif // compiler(>=5.6) diff --git a/Sources/GRPC/CallOptions.swift b/Sources/GRPC/CallOptions.swift index 51631abd6..00bffb3cd 100644 --- a/Sources/GRPC/CallOptions.swift +++ b/Sources/GRPC/CallOptions.swift @@ -23,7 +23,7 @@ import NIOHTTP1 import NIOHTTP2 /// Options to use for GRPC calls. -public struct CallOptions: GRPCSendable { +public struct CallOptions: Sendable { /// Additional metadata to send to the service. public var customMetadata: HPACKHeaders @@ -125,14 +125,10 @@ public struct CallOptions: GRPCSendable { } extension CallOptions { - public struct RequestIDProvider: GRPCSendable { - #if swift(>=5.6) + public struct RequestIDProvider: Sendable { public typealias RequestIDGenerator = @Sendable () -> String - #else - public typealias RequestIDGenerator = () -> String - #endif // swift(>=5.6) - private enum RequestIDSource: GRPCSendable { + private enum RequestIDSource: Sendable { case none case `static`(String) case generated(RequestIDGenerator) @@ -179,7 +175,7 @@ extension CallOptions { } extension CallOptions { - public struct EventLoopPreference: GRPCSendable { + public struct EventLoopPreference: Sendable { /// No preference. The framework will assign an `EventLoop`. public static let indifferent = EventLoopPreference(.indifferent) @@ -189,7 +185,7 @@ extension CallOptions { } @usableFromInline - internal enum Preference: GRPCSendable { + internal enum Preference: Sendable { case indifferent case exact(EventLoop) } diff --git a/Sources/GRPC/ClientCalls/Call.swift b/Sources/GRPC/ClientCalls/Call.swift index 3277681a4..759798836 100644 --- a/Sources/GRPC/ClientCalls/Call.swift +++ b/Sources/GRPC/ClientCalls/Call.swift @@ -440,7 +440,5 @@ extension Call { } } -#if compiler(>=5.6) // @unchecked is ok: all mutable state is accessed/modified from the appropriate event loop. extension Call: @unchecked Sendable where Request: Sendable, Response: Sendable {} -#endif diff --git a/Sources/GRPC/ClientCalls/ResponseContainers.swift b/Sources/GRPC/ClientCalls/ResponseContainers.swift index 9169a8a5a..d8bf6411c 100644 --- a/Sources/GRPC/ClientCalls/ResponseContainers.swift +++ b/Sources/GRPC/ClientCalls/ResponseContainers.swift @@ -201,8 +201,6 @@ extension Error { } } -#if compiler(>=5.6) // @unchecked is ok: all mutable state is accessed/modified from an appropriate event loop. extension UnaryResponseParts: @unchecked Sendable where Response: Sendable {} extension StreamingResponseParts: @unchecked Sendable where Response: Sendable {} -#endif diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index bd04081d0..395c52978 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -13,11 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if swift(>=5.6) @preconcurrency import Foundation -#else -import Foundation -#endif // swift(>=5.6) import Logging import NIOCore @@ -83,7 +79,7 @@ import SwiftProtobuf /// The 'GRPCIdleHandler' intercepts HTTP/2 frames and various events and is responsible for /// informing and controlling the state of the connection (idling and keepalive). The HTTP/2 streams /// are used to handle individual RPCs. -public final class ClientConnection: GRPCSendable { +public final class ClientConnection: Sendable { private let connectionManager: ConnectionManager /// HTTP multiplexer from the underlying channel handling gRPC calls. @@ -266,7 +262,7 @@ extension ClientConnection: GRPCChannel { // MARK: - Configuration structures /// A target to connect to. -public struct ConnectionTarget: GRPCSendable { +public struct ConnectionTarget: Sendable { internal enum Wrapped { case hostAndPort(String, Int) case unixDomainSocket(String) @@ -320,8 +316,8 @@ public struct ConnectionTarget: GRPCSendable { } /// The connectivity behavior to use when starting an RPC. -public struct CallStartBehavior: Hashable, GRPCSendable { - internal enum Behavior: Hashable, GRPCSendable { +public struct CallStartBehavior: Hashable, Sendable { + internal enum Behavior: Hashable, Sendable { case waitsForConnectivity case fastFailure } @@ -354,7 +350,7 @@ public struct CallStartBehavior: Hashable, GRPCSendable { extension ClientConnection { /// Configuration for a ``ClientConnection``. Users should prefer using one of the /// ``ClientConnection`` builders: ``ClientConnection/secure(group:)`` or ``ClientConnection/insecure(group:)``. - public struct Configuration: GRPCSendable { + public struct Configuration: Sendable { /// The target to connect to. public var target: ConnectionTarget @@ -454,15 +450,10 @@ extension ClientConnection { /// used to add additional handlers to the pipeline and is intended for debugging. /// /// - Warning: The initializer closure may be invoked *multiple times*. - #if compiler(>=5.6) @preconcurrency public var debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? - #else - public var debugChannelInitializer: ((Channel) -> EventLoopFuture)? - #endif #if canImport(NIOSSL) - #if compiler(>=5.6) /// Create a `Configuration` with some pre-defined defaults. Prefer using /// `ClientConnection.secure(group:)` to build a connection secured with TLS or /// `ClientConnection.insecure(group:)` to build a plaintext connection. @@ -519,41 +510,6 @@ extension ClientConnection { self.backgroundActivityLogger = backgroundActivityLogger self.debugChannelInitializer = debugChannelInitializer } - #else - @available(*, deprecated, renamed: "default(target:eventLoopGroup:)") - public init( - target: ConnectionTarget, - eventLoopGroup: EventLoopGroup, - errorDelegate: ClientErrorDelegate? = LoggingClientErrorDelegate(), - connectivityStateDelegate: ConnectivityStateDelegate? = nil, - connectivityStateDelegateQueue: DispatchQueue? = nil, - tls: Configuration.TLS? = nil, - connectionBackoff: ConnectionBackoff? = ConnectionBackoff(), - connectionKeepalive: ClientConnectionKeepalive = ClientConnectionKeepalive(), - connectionIdleTimeout: TimeAmount = .minutes(30), - callStartBehavior: CallStartBehavior = .waitsForConnectivity, - httpTargetWindowSize: Int = 8 * 1024 * 1024, - backgroundActivityLogger: Logger = Logger( - label: "io.grpc", - factory: { _ in SwiftLogNoOpLogHandler() } - ), - debugChannelInitializer: ((Channel) -> EventLoopFuture)? = nil - ) { - self.target = target - self.eventLoopGroup = eventLoopGroup - self.errorDelegate = errorDelegate - self.connectivityStateDelegate = connectivityStateDelegate - self.connectivityStateDelegateQueue = connectivityStateDelegateQueue - self.tlsConfiguration = tls.map { GRPCTLSConfiguration(transforming: $0) } - self.connectionBackoff = connectionBackoff - self.connectionKeepalive = connectionKeepalive - self.connectionIdleTimeout = connectionIdleTimeout - self.callStartBehavior = callStartBehavior - self.httpTargetWindowSize = httpTargetWindowSize - self.backgroundActivityLogger = backgroundActivityLogger - self.debugChannelInitializer = debugChannelInitializer - } - #endif // compiler(>=5.6) #endif // canImport(NIOSSL) private init(eventLoopGroup: EventLoopGroup, target: ConnectionTarget) { diff --git a/Sources/GRPC/Compression/CompressionAlgorithm.swift b/Sources/GRPC/Compression/CompressionAlgorithm.swift index 2b7a56592..960cf7d9b 100644 --- a/Sources/GRPC/Compression/CompressionAlgorithm.swift +++ b/Sources/GRPC/Compression/CompressionAlgorithm.swift @@ -18,7 +18,7 @@ /// /// 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: Equatable, GRPCSendable { +public struct CompressionAlgorithm: Equatable, Sendable { /// Identity compression; "no" compression but indicated via the "grpc-encoding" header. public static let identity = CompressionAlgorithm(.identity) public static let deflate = CompressionAlgorithm(.deflate) diff --git a/Sources/GRPC/Compression/DecompressionLimit.swift b/Sources/GRPC/Compression/DecompressionLimit.swift index b206e7e1a..6f002ed6f 100644 --- a/Sources/GRPC/Compression/DecompressionLimit.swift +++ b/Sources/GRPC/Compression/DecompressionLimit.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -public struct DecompressionLimit: Equatable, GRPCSendable { - private enum Limit: Equatable, GRPCSendable { +public struct DecompressionLimit: Equatable, Sendable { + private enum Limit: Equatable, Sendable { case ratio(Int) case absolute(Int) } diff --git a/Sources/GRPC/Compression/MessageEncoding.swift b/Sources/GRPC/Compression/MessageEncoding.swift index 60605768e..6301c12bb 100644 --- a/Sources/GRPC/Compression/MessageEncoding.swift +++ b/Sources/GRPC/Compression/MessageEncoding.swift @@ -15,9 +15,9 @@ */ /// Whether compression should be enabled for the message. -public struct Compression: Hashable, GRPCSendable { +public struct Compression: Hashable, Sendable { @usableFromInline - internal enum _Wrapped: Hashable, GRPCSendable { + internal enum _Wrapped: Hashable, Sendable { case enabled case disabled case deferToCallDefault @@ -57,7 +57,7 @@ extension Compression { } /// Whether compression is enabled or disabled for a client. -public enum ClientMessageEncoding: GRPCSendable { +public enum ClientMessageEncoding: Sendable { /// Compression is enabled with the given configuration. case enabled(Configuration) /// Compression is disabled. @@ -76,7 +76,7 @@ extension ClientMessageEncoding { } extension ClientMessageEncoding { - public struct Configuration: GRPCSendable { + public struct Configuration: Sendable { public init( forRequests outbound: CompressionAlgorithm?, acceptableForResponses inbound: [CompressionAlgorithm] = CompressionAlgorithm.all, diff --git a/Sources/GRPC/ConnectionBackoff.swift b/Sources/GRPC/ConnectionBackoff.swift index c176757de..561fc23c6 100644 --- a/Sources/GRPC/ConnectionBackoff.swift +++ b/Sources/GRPC/ConnectionBackoff.swift @@ -19,7 +19,7 @@ import Foundation /// /// This algorithm and defaults are determined by the gRPC connection backoff /// [documentation](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md). -public struct ConnectionBackoff: Sequence, GRPCSendable { +public struct ConnectionBackoff: Sequence, Sendable { public typealias Iterator = ConnectionBackoffIterator /// The initial backoff in seconds. @@ -41,8 +41,8 @@ public struct ConnectionBackoff: Sequence, GRPCSendable { /// A limit on the number of times to attempt reconnection. public var retries: Retries - public struct Retries: Hashable, GRPCSendable { - fileprivate enum Limit: Hashable, GRPCSendable { + public struct Retries: Hashable, Sendable { + fileprivate enum Limit: Hashable, Sendable { case limited(Int) case unlimited } diff --git a/Sources/GRPC/ConnectionKeepalive.swift b/Sources/GRPC/ConnectionKeepalive.swift index 8035cd5d6..841081f81 100644 --- a/Sources/GRPC/ConnectionKeepalive.swift +++ b/Sources/GRPC/ConnectionKeepalive.swift @@ -19,7 +19,7 @@ import NIOCore /// /// The defaults are determined by the gRPC keepalive /// [documentation] (https://github.com/grpc/grpc/blob/master/doc/keepalive.md). -public struct ClientConnectionKeepalive: Hashable, GRPCSendable { +public struct ClientConnectionKeepalive: Hashable, Sendable { /// The amount of time to wait before sending a keepalive ping. public var interval: TimeAmount diff --git a/Sources/GRPC/ConnectionManager+Delegates.swift b/Sources/GRPC/ConnectionManager+Delegates.swift index 35d5a870e..5b214f57e 100644 --- a/Sources/GRPC/ConnectionManager+Delegates.swift +++ b/Sources/GRPC/ConnectionManager+Delegates.swift @@ -60,7 +60,7 @@ internal protocol ConnectionManagerHTTP2Delegate { // This mirrors `ConnectivityState` (which is public API) but adds `Error` as associated data // to a few cases. -internal enum _ConnectivityState: GRPCSendable { +internal enum _ConnectivityState: Sendable { case idle(Error?) case connecting case ready diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 8b66f505e..35ee41101 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -19,16 +19,12 @@ import NIOConcurrencyHelpers import NIOCore import NIOHTTP2 -#if compiler(>=5.6) // Unchecked because mutable state is always accessed and modified on a particular event loop. // APIs which _may_ be called from different threads execute onto the correct event loop first. // APIs which _must_ be called from an exact event loop have preconditions checking that the correct // event loop is being used. -extension ConnectionManager: @unchecked Sendable {} -#endif // compiler(>=5.6) - @usableFromInline -internal final class ConnectionManager { +internal final class ConnectionManager: @unchecked Sendable { internal enum Reconnect { case none case after(TimeInterval) diff --git a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift b/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift index 15f87a0e9..33a7220ed 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift @@ -15,7 +15,7 @@ */ @usableFromInline -internal struct ConnectionManagerID: Hashable, CustomStringConvertible, GRPCSendable { +internal struct ConnectionManagerID: Hashable, CustomStringConvertible, Sendable { @usableFromInline internal let _id: ObjectIdentifier diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift index ea7a23c53..6011d071d 100644 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift @@ -87,7 +87,7 @@ public enum GRPCChannelPool { } extension GRPCChannelPool { - public struct Configuration: GRPCSendable { + public struct Configuration: Sendable { @inlinable internal init( target: ConnectionTarget, @@ -167,12 +167,8 @@ extension GRPCChannelPool { /// This may be used to add additional handlers to the pipeline and is intended for debugging. /// /// - Warning: The initializer closure may be invoked *multiple times*. - #if compiler(>=5.6) @preconcurrency public var debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? - #else - public var debugChannelInitializer: ((Channel) -> EventLoopFuture)? - #endif /// An error delegate which is called when errors are caught. public var errorDelegate: ClientErrorDelegate? @@ -192,7 +188,7 @@ extension GRPCChannelPool { } extension GRPCChannelPool.Configuration { - public struct TransportSecurity: GRPCSendable { + public struct TransportSecurity: Sendable { private init(_ configuration: GRPCTLSConfiguration?) { self.tlsConfiguration = configuration } @@ -219,7 +215,7 @@ extension GRPCChannelPool.Configuration { } extension GRPCChannelPool.Configuration { - public struct HTTP2: Hashable, GRPCSendable { + public struct HTTP2: Hashable, Sendable { private static let allowedTargetWindowSizes = (1 ... Int(Int32.max)) private static let allowedMaxFrameSizes = (1 << 14) ... ((1 << 24) - 1) @@ -252,7 +248,7 @@ extension GRPCChannelPool.Configuration { } extension GRPCChannelPool.Configuration { - public struct ConnectionPool: Hashable, GRPCSendable { + public struct ConnectionPool: Hashable, Sendable { /// Default connection pool configuration. public static let defaults = ConnectionPool() @@ -293,7 +289,7 @@ extension GRPCChannelPool.Configuration { } /// The ID of a connection in the connection pool. -public struct GRPCConnectionID: Hashable, GRPCSendable, CustomStringConvertible { +public struct GRPCConnectionID: Hashable, Sendable, CustomStringConvertible { private let id: ConnectionManagerID public var description: String { @@ -309,7 +305,7 @@ public struct GRPCConnectionID: Hashable, GRPCSendable, CustomStringConvertible /// /// All functions must execute quickly and may be executed on arbitrary threads. The implementor is /// responsible for ensuring thread safety. -public protocol GRPCConnectionPoolDelegate: GRPCSendable { +public protocol GRPCConnectionPoolDelegate: Sendable { /// A new connection was created with the given ID and added to the pool. The connection is not /// yet active (or connecting). /// diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index 05bd33641..c9f745422 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -17,10 +17,8 @@ import Logging import NIOConcurrencyHelpers import NIOCore -#if compiler(>=5.6) // Unchecked because all mutable state is protected by a lock. extension PooledChannel: @unchecked Sendable {} -#endif // compiler(>=5.6) @usableFromInline internal final class PoolManager { diff --git a/Sources/GRPC/ConnectivityState.swift b/Sources/GRPC/ConnectivityState.swift index 9b46da131..ff238ce65 100644 --- a/Sources/GRPC/ConnectivityState.swift +++ b/Sources/GRPC/ConnectivityState.swift @@ -20,7 +20,7 @@ import NIOCore /// The connectivity state of a client connection. Note that this is heavily lifted from the gRPC /// documentation: https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md. -public enum ConnectivityState: GRPCSendable { +public enum ConnectivityState: Sendable { /// This is the state where the channel has not yet been created. case idle @@ -68,12 +68,8 @@ extension ConnectivityStateDelegate { public func connectionStartedQuiescing() {} } -#if compiler(>=5.6) // Unchecked because all mutable state is protected by locks. -extension ConnectivityStateMonitor: @unchecked Sendable {} -#endif // compiler(>=5.6) - -public class ConnectivityStateMonitor { +public class ConnectivityStateMonitor: @unchecked Sendable { private let stateLock = NIOLock() private var _state: ConnectivityState = .idle diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md index 1a58334c0..d5ef1a85f 100644 --- a/Sources/GRPC/Docs.docc/index.md +++ b/Sources/GRPC/Docs.docc/index.md @@ -26,7 +26,8 @@ gRPC Swift Version | Earliest Swift Version -------------------|----------------------- `1.0.0 ..< 1.8.0` | 5.2 `1.8.0 ..< 1.11.0` | 5.4 -`1.11.0...` | 5.5 +`1.11.0..< 1.16.0`.| 5.5 +`1.16.0...` | 5.6 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. @@ -42,7 +43,7 @@ package dependency to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.9.0") + .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.15.0") ] ``` diff --git a/Sources/GRPC/FakeChannel.swift b/Sources/GRPC/FakeChannel.swift index 46ce88be8..8348b215f 100644 --- a/Sources/GRPC/FakeChannel.swift +++ b/Sources/GRPC/FakeChannel.swift @@ -18,11 +18,9 @@ import NIOCore import NIOEmbedded import SwiftProtobuf -#if compiler(>=5.6) // This type is deprecated, but we need to '@unchecked Sendable' to avoid warnings in our own code. @available(swift, deprecated: 5.6) extension FakeChannel: @unchecked Sendable {} -#endif // compiler(>=5.6) /// A fake channel for use with generated test clients. /// diff --git a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift index a726e46e0..67ea2619b 100644 --- a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift +++ b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift @@ -315,7 +315,6 @@ extension ClientConnection.Builder { /// used to add additional handlers to the pipeline and is intended for debugging. /// /// - Warning: The initializer closure may be invoked *multiple times*. - #if compiler(>=5.6) @discardableResult @preconcurrency public func withDebugChannelInitializer( @@ -324,15 +323,6 @@ extension ClientConnection.Builder { self.configuration.debugChannelInitializer = debugChannelInitializer return self } - #else - @discardableResult - public func withDebugChannelInitializer( - _ debugChannelInitializer: @escaping (Channel) -> EventLoopFuture - ) -> Self { - self.configuration.debugChannelInitializer = debugChannelInitializer - return self - } - #endif } extension Double { diff --git a/Sources/GRPC/GRPCClient.swift b/Sources/GRPC/GRPCClient.swift index 1a892c46e..783c3f842 100644 --- a/Sources/GRPC/GRPCClient.swift +++ b/Sources/GRPC/GRPCClient.swift @@ -202,11 +202,9 @@ public final class AnyServiceClient: GRPCClient { } } -#if swift(>=5.6) // Unchecked because mutable state is protected by a lock. @available(*, deprecated, renamed: "GRPCAnyServiceClient") -extension AnyServiceClient: @unchecked GRPCSendable {} -#endif // swift(>=5.6) +extension AnyServiceClient: @unchecked Sendable {} /// A client which has no generated stubs and may be used to create gRPC calls manually. /// See ``GRPCClient`` for details. diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index d8bb8d999..3464ef1c2 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -226,7 +226,7 @@ extension _GRPCRequestHead { } /// The type of gRPC call. -public enum GRPCCallType: Hashable, GRPCSendable { +public enum GRPCCallType: Hashable, Sendable { /// Unary: a single request and a single response. case unary diff --git a/Sources/GRPC/GRPCServiceDescription.swift b/Sources/GRPC/GRPCServiceDescription.swift index cfa5cc427..7b88905c3 100644 --- a/Sources/GRPC/GRPCServiceDescription.swift +++ b/Sources/GRPC/GRPCServiceDescription.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -public struct GRPCServiceDescriptor: Hashable, GRPCSendable { +public struct GRPCServiceDescriptor: Hashable, Sendable { /// The name of the service excluding the package, e.g. 'Echo'. public var name: String @@ -31,7 +31,7 @@ public struct GRPCServiceDescriptor: Hashable, GRPCSendable { } } -public struct GRPCMethodDescriptor: Hashable, GRPCSendable { +public struct GRPCMethodDescriptor: Hashable, Sendable { /// The name of the method, e.g. 'Get'. public var name: String diff --git a/Sources/GRPC/GRPCStatus.swift b/Sources/GRPC/GRPCStatus.swift index 16290041f..3087dfd0e 100644 --- a/Sources/GRPC/GRPCStatus.swift +++ b/Sources/GRPC/GRPCStatus.swift @@ -18,7 +18,7 @@ import NIOHTTP1 import NIOHTTP2 /// Encapsulates the result of a gRPC call. -public struct GRPCStatus: Error, GRPCSendable { +public struct GRPCStatus: Error, Sendable { /// Storage for message/cause. In the happy case ('ok') there will not be a message or cause /// and this will reference a static storage containing nil values. Making it optional makes the /// setters for message and cause a little messy. @@ -145,7 +145,7 @@ extension GRPCStatus { extension GRPCStatus { /// Status codes for gRPC operations (replicated from `status_code_enum.h` in the /// [gRPC core library](https://github.com/grpc/grpc)). - public struct Code: Hashable, CustomStringConvertible, GRPCSendable { + public struct Code: Hashable, CustomStringConvertible, Sendable { // `rawValue` must be an `Int` for API reasons and we don't need (or want) to store anything so // wide, a `UInt8` is fine. private let _rawValue: UInt8 @@ -316,11 +316,9 @@ extension GRPCStatus { } } -#if compiler(>=5.6) // `GRPCStatus` has CoW semantics so it is inherently `Sendable`. Rather than marking `GRPCStatus` // as `@unchecked Sendable` we only mark `Storage` as such. extension GRPCStatus.Storage: @unchecked Sendable {} -#endif // compiler(>=5.6) /// This protocol serves as a customisation point for error types so that gRPC calls may be /// terminated with an appropriate status. diff --git a/Sources/GRPC/GRPCTLSConfiguration.swift b/Sources/GRPC/GRPCTLSConfiguration.swift index cf3a9f2dd..8adc7ad50 100644 --- a/Sources/GRPC/GRPCTLSConfiguration.swift +++ b/Sources/GRPC/GRPCTLSConfiguration.swift @@ -28,8 +28,8 @@ import Security /// This structure allow configuring TLS for a wide range of TLS implementations. Some /// options are removed from the user's control to ensure the configuration complies with /// the gRPC specification. -public struct GRPCTLSConfiguration: GRPCSendable { - fileprivate enum Backend: GRPCSendable { +public struct GRPCTLSConfiguration: Sendable { + fileprivate enum Backend: Sendable { #if canImport(NIOSSL) /// Configuration for NIOSSSL. case nio(NIOConfiguration) diff --git a/Sources/GRPC/Interceptor/ClientInterceptors.swift b/Sources/GRPC/Interceptor/ClientInterceptors.swift index 282549d5a..74d567567 100644 --- a/Sources/GRPC/Interceptor/ClientInterceptors.swift +++ b/Sources/GRPC/Interceptor/ClientInterceptors.swift @@ -48,7 +48,6 @@ import NIOCore /// require any extra attention. However, if work is done on a `DispatchQueue` or _other_ /// `EventLoop` then implementers should ensure that they use `context` from the correct /// `EventLoop`. -#if swift(>=5.6) @preconcurrency open class ClientInterceptor: @unchecked Sendable { public init() {} @@ -98,54 +97,3 @@ import NIOCore context.cancel(promise: promise) } } -#else -open class ClientInterceptor { - public init() {} - - /// Called when the interceptor has received a response part to handle. - /// - Parameters: - /// - part: The response part which has been received from the server. - /// - context: An interceptor context which may be used to forward the response part. - open func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - context.receive(part) - } - - /// Called when the interceptor has received an error. - /// - Parameters: - /// - error: The error. - /// - context: An interceptor context which may be used to forward the error. - open func errorCaught( - _ error: Error, - context: ClientInterceptorContext - ) { - context.errorCaught(error) - } - - /// Called when the interceptor has received a request part to handle. - /// - Parameters: - /// - part: The request part which should be sent to the server. - /// - promise: A promise which should be completed when the response part has been handled. - /// - context: An interceptor context which may be used to forward the request part. - open func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - context.send(part, promise: promise) - } - - /// Called when the interceptor has received a request to cancel the RPC. - /// - Parameters: - /// - promise: A promise which should be cancellation request has been handled. - /// - context: An interceptor context which may be used to forward the cancellation request. - open func cancel( - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - context.cancel(promise: promise) - } -} -#endif // swift(>=5.6) diff --git a/Sources/GRPC/Interceptor/MessageParts.swift b/Sources/GRPC/Interceptor/MessageParts.swift index 9d81bc321..1e2495884 100644 --- a/Sources/GRPC/Interceptor/MessageParts.swift +++ b/Sources/GRPC/Interceptor/MessageParts.swift @@ -60,7 +60,7 @@ public enum GRPCServerResponsePart { } /// Metadata associated with a request or response message. -public struct MessageMetadata: Equatable, GRPCSendable { +public struct MessageMetadata: Equatable, Sendable { /// Whether the message should be compressed. If compression has not been enabled on the RPC /// then this setting is ignored. public var compress: Bool @@ -100,9 +100,7 @@ extension GRPCServerResponsePart { } } -#if swift(>=5.6) extension GRPCClientRequestPart: Sendable where Request: Sendable {} extension GRPCClientResponsePart: Sendable where Response: Sendable {} extension GRPCServerRequestPart: Sendable where Request: Sendable {} extension GRPCServerResponsePart: Sendable where Response: Sendable {} -#endif diff --git a/Sources/GRPC/TimeLimit.swift b/Sources/GRPC/TimeLimit.swift index ab370f240..7ea6e1de1 100644 --- a/Sources/GRPC/TimeLimit.swift +++ b/Sources/GRPC/TimeLimit.swift @@ -24,9 +24,9 @@ import NIOCore /// /// - Note: Servers may impose a time limit on an RPC independent of the client's time limit; RPCs /// may therefore complete with ``GRPCStatus/Code-swift.struct/deadlineExceeded`` even if no time limit was set by the client. -public struct TimeLimit: Equatable, CustomStringConvertible, GRPCSendable { +public struct TimeLimit: Equatable, CustomStringConvertible, Sendable { // private but for shimming. - private enum Wrapped: Equatable, GRPCSendable { + private enum Wrapped: Equatable, Sendable { case none case timeout(TimeAmount) case deadline(NIODeadline) diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift index 90ebf84d9..230bd1692 100644 --- a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift +++ b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift @@ -244,10 +244,8 @@ extension Grpc_Testing_TestServiceClientProtocol { } } -#if compiler(>=5.6) @available(*, deprecated) extension Grpc_Testing_TestServiceClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @available(*, deprecated, renamed: "Grpc_Testing_TestServiceNIOClient") public final class Grpc_Testing_TestServiceClient: Grpc_Testing_TestServiceClientProtocol { @@ -303,7 +301,6 @@ public struct Grpc_Testing_TestServiceNIOClient: Grpc_Testing_TestServiceClientP } } -#if compiler(>=5.6) /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -602,9 +599,7 @@ public struct Grpc_Testing_TestServiceAsyncClient: Grpc_Testing_TestServiceAsync } } -#endif // compiler(>=5.6) - -public protocol Grpc_Testing_TestServiceClientInterceptorFactoryProtocol: GRPCSendable { +public protocol Grpc_Testing_TestServiceClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'emptyCall'. func makeEmptyCallInterceptors() -> [ClientInterceptor] @@ -736,10 +731,8 @@ extension Grpc_Testing_UnimplementedServiceClientProtocol { } } -#if compiler(>=5.6) @available(*, deprecated) extension Grpc_Testing_UnimplementedServiceClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @available(*, deprecated, renamed: "Grpc_Testing_UnimplementedServiceNIOClient") public final class Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_UnimplementedServiceClientProtocol { @@ -795,7 +788,6 @@ public struct Grpc_Testing_UnimplementedServiceNIOClient: Grpc_Testing_Unimpleme } } -#if compiler(>=5.6) /// A simple service NOT implemented at servers so clients can test for /// that case. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -864,9 +856,7 @@ public struct Grpc_Testing_UnimplementedServiceAsyncClient: Grpc_Testing_Unimple } } -#endif // compiler(>=5.6) - -public protocol Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol: GRPCSendable { +public protocol Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'unimplementedCall'. func makeUnimplementedCallInterceptors() -> [ClientInterceptor] @@ -950,10 +940,8 @@ extension Grpc_Testing_ReconnectServiceClientProtocol { } } -#if compiler(>=5.6) @available(*, deprecated) extension Grpc_Testing_ReconnectServiceClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @available(*, deprecated, renamed: "Grpc_Testing_ReconnectServiceNIOClient") public final class Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectServiceClientProtocol { @@ -1009,7 +997,6 @@ public struct Grpc_Testing_ReconnectServiceNIOClient: Grpc_Testing_ReconnectServ } } -#if compiler(>=5.6) /// A service used to control reconnect server. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol Grpc_Testing_ReconnectServiceAsyncClientProtocol: GRPCClient { @@ -1106,9 +1093,7 @@ public struct Grpc_Testing_ReconnectServiceAsyncClient: Grpc_Testing_ReconnectSe } } -#endif // compiler(>=5.6) - -public protocol Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol: GRPCSendable { +public protocol Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'start'. func makeStartInterceptors() -> [ClientInterceptor] @@ -1261,8 +1246,6 @@ extension Grpc_Testing_TestServiceProvider { } } -#if compiler(>=5.6) - /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. /// @@ -1415,8 +1398,6 @@ extension Grpc_Testing_TestServiceAsyncProvider { } } -#endif // compiler(>=5.6) - public protocol Grpc_Testing_TestServiceServerInterceptorFactoryProtocol { /// - Returns: Interceptors to use when handling 'emptyCall'. @@ -1556,8 +1537,6 @@ extension Grpc_Testing_UnimplementedServiceProvider { } } -#if compiler(>=5.6) - /// A simple service NOT implemented at servers so clients can test for /// that case. /// @@ -1608,8 +1587,6 @@ extension Grpc_Testing_UnimplementedServiceAsyncProvider { } } -#endif // compiler(>=5.6) - public protocol Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol { /// - Returns: Interceptors to use when handling 'unimplementedCall'. @@ -1681,8 +1658,6 @@ extension Grpc_Testing_ReconnectServiceProvider { } } -#if compiler(>=5.6) - /// A service used to control reconnect server. /// /// To implement a server, implement an object which conforms to this protocol. @@ -1745,8 +1720,6 @@ extension Grpc_Testing_ReconnectServiceAsyncProvider { } } -#endif // compiler(>=5.6) - public protocol Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol { /// - Returns: Interceptors to use when handling 'start'. diff --git a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift index 689287dc4..9427b6e58 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import Foundation import GRPC import GRPCInteroperabilityTestModels @@ -217,4 +216,3 @@ public class TestServiceAsyncProvider: Grpc_Testing_TestServiceAsyncProvider { ) } } -#endif // compiler(>=5.6) diff --git a/Sources/protoc-gen-grpc-swift/Generator-Client.swift b/Sources/protoc-gen-grpc-swift/Generator-Client.swift index be2157549..c093cc9ae 100644 --- a/Sources/protoc-gen-grpc-swift/Generator-Client.swift +++ b/Sources/protoc-gen-grpc-swift/Generator-Client.swift @@ -29,7 +29,6 @@ extension Generator { self.println() self.printStructBackedServiceClientImplementation() self.println() - self.printIfCompilerGuardForAsyncAwait() self.printAsyncServiceClientProtocol() self.println() self.printAsyncClientProtocolExtension() @@ -38,8 +37,6 @@ extension Generator { self.println() self.printAsyncServiceClientImplementation() self.println() - self.printEndCompilerGuardForAsyncAwait() - self.println() // Both implementations share definitions for interceptors and metadata. self.printServiceClientInterceptorFactoryProtocol() self.println() @@ -159,7 +156,7 @@ extension Generator { } private func printServiceClientInterceptorFactoryProtocol() { - self.println("\(self.access) protocol \(self.clientInterceptorProtocolName): GRPCSendable {") + self.println("\(self.access) protocol \(self.clientInterceptorProtocolName): Sendable {") self.withIndentation { // Method specific interceptors. for method in service.methods { @@ -189,10 +186,8 @@ extension Generator { } 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) {") @@ -534,10 +529,8 @@ extension Generator { } private func printTestClient() { - 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 " diff --git a/Sources/protoc-gen-grpc-swift/Generator-Server.swift b/Sources/protoc-gen-grpc-swift/Generator-Server.swift index 780e6c98a..4a483fb01 100644 --- a/Sources/protoc-gen-grpc-swift/Generator-Server.swift +++ b/Sources/protoc-gen-grpc-swift/Generator-Server.swift @@ -24,14 +24,10 @@ extension Generator { self.println() self.printServerProtocolExtension() self.println() - self.printIfCompilerGuardForAsyncAwait() - self.println() self.printServerProtocolAsyncAwait() self.println() self.printServerProtocolExtensionAsyncAwait() self.println() - self.printEndCompilerGuardForAsyncAwait() - self.println() // Both implementations share definitions for interceptors and metadata. self.printServerInterceptorFactoryProtocol() self.println() diff --git a/Sources/protoc-gen-grpc-swift/Generator.swift b/Sources/protoc-gen-grpc-swift/Generator.swift index a85101899..76589a2f6 100644 --- a/Sources/protoc-gen-grpc-swift/Generator.swift +++ b/Sources/protoc-gen-grpc-swift/Generator.swift @@ -169,12 +169,4 @@ class Generator { func printAvailabilityForAsyncAwait() { self.println("@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)") } - - func printIfCompilerGuardForAsyncAwait() { - self.println("#if compiler(>=5.6)") - } - - func printEndCompilerGuardForAsyncAwait() { - self.println("#endif // compiler(>=5.6)") - } } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift index df009a6cf..1d8697b7e 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import EchoImplementation import EchoModel import GRPC @@ -416,5 +415,3 @@ final class AsyncClientCancellationTests: GRPCTestCase { } } } - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift index 4f41ebef0..de8e8670c 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import EchoImplementation import EchoModel import GRPC @@ -195,5 +194,3 @@ extension HPACKHeaders { XCTAssertEqual(self.first(name: name), value) } } - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift index 89e16169c..cadad82ce 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension AsyncSequence { internal func collect() async throws -> [Element] { @@ -27,5 +25,3 @@ extension AsyncSequence { return try await self.reduce(0) { count, _ in count + 1 } } } - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift index 6951f58ff..b1742e01f 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) @testable import GRPC import NIOCore import NIOEmbedded @@ -328,4 +327,3 @@ extension ServerHandlerStateMachine.CancelAction { XCTAssertEqual(self, .cancelAndNilOutHandlerComponents) } } -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift index 383ec41e1..f42b3c648 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) @testable import GRPC import XCTest @@ -126,5 +125,3 @@ internal final class ServerInterceptorStateMachineStreamStateTests: GRPCTestCase XCTAssertEqual(state, .done) } } - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift index 6f6b9a9b7..ce7464fde 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) @testable import GRPC import NIOEmbedded import XCTest @@ -185,5 +184,3 @@ extension ServerInterceptorStateMachine.CancelAction { XCTAssertEqual(self, .nilOutInterceptorPipeline) } } - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift index b2b7871f5..708b5da55 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - import GRPC import XCTest @@ -31,4 +29,3 @@ final class GRPCAsyncRequestStreamTests: XCTestCase { XCTAssertEqual(results, [1]) } } -#endif diff --git a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift index a299f2c2c..2c903d15b 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import GRPC import XCTest @@ -31,4 +30,3 @@ final class GRPCAsyncResponseStreamWriterTests: XCTestCase { XCTAssertEqual(results[0].1, .disabled) } } -#endif diff --git a/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift index 8ff554ebc..dca269d66 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import EchoImplementation import EchoModel import GRPC @@ -169,5 +168,3 @@ class InterceptorsAsyncTests: GRPCTestCase { } } } - -#endif diff --git a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift index 5041830a6..7e8ff10ca 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) import XCTest @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) @@ -31,7 +30,7 @@ internal func XCTAssertThrowsError( } } -fileprivate enum TaskResult { +private enum TaskResult { case operation(Result) case cancellation } @@ -70,5 +69,3 @@ func withTaskCancelledAfter( try await group.waitForAll() } } - -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/ClientTimeoutTests.swift b/Tests/GRPCTests/ClientTimeoutTests.swift index 9930df17d..0bf157101 100644 --- a/Tests/GRPCTests/ClientTimeoutTests.swift +++ b/Tests/GRPCTests/ClientTimeoutTests.swift @@ -164,10 +164,8 @@ class ClientTimeoutTests: GRPCTestCase { } } -#if compiler(>=5.6) // Unchecked as it uses an 'EmbeddedChannel'. extension EmbeddedGRPCChannel: @unchecked Sendable {} -#endif // compiler(>=5.6) private final class EmbeddedGRPCChannel: GRPCChannel { let embeddedChannel: EmbeddedChannel diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift index 112d29900..98be1ca17 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift @@ -234,10 +234,8 @@ extension Normalization_NormalizationClientProtocol { } } -#if compiler(>=5.6) @available(*, deprecated) extension Normalization_NormalizationClient: @unchecked Sendable {} -#endif // compiler(>=5.6) @available(*, deprecated, renamed: "Normalization_NormalizationNIOClient") internal final class Normalization_NormalizationClient: Normalization_NormalizationClientProtocol { @@ -293,7 +291,6 @@ internal struct Normalization_NormalizationNIOClient: Normalization_Normalizatio } } -#if compiler(>=5.6) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) internal protocol Normalization_NormalizationAsyncClientProtocol: GRPCClient { static var serviceDescriptor: GRPCServiceDescriptor { get } @@ -599,9 +596,7 @@ internal struct Normalization_NormalizationAsyncClient: Normalization_Normalizat } } -#endif // compiler(>=5.6) - -internal protocol Normalization_NormalizationClientInterceptorFactoryProtocol: GRPCSendable { +internal protocol Normalization_NormalizationClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'Unary'. func makeUnaryInterceptors() -> [ClientInterceptor] @@ -806,8 +801,6 @@ extension Normalization_NormalizationProvider { } } -#if compiler(>=5.6) - /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) internal protocol Normalization_NormalizationAsyncProvider: CallHandlerProvider { @@ -956,8 +949,6 @@ extension Normalization_NormalizationAsyncProvider { } } -#endif // compiler(>=5.6) - internal protocol Normalization_NormalizationServerInterceptorFactoryProtocol { /// - Returns: Interceptors to use when handling 'Unary'. diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 174f73112..92f2333f3 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -1272,10 +1272,8 @@ internal struct Change: Hashable, CustomStringConvertible { } } -#if compiler(>=5.6) // Unchecked as all mutable state is modified from a serial queue. extension RecordingConnectivityDelegate: @unchecked Sendable {} -#endif // compiler(>=5.6) internal class RecordingConnectivityDelegate: ConnectivityStateDelegate { private let serialQueue = DispatchQueue(label: "io.grpc.testing") diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift index 3691706ff..4a063bfa3 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift @@ -22,26 +22,16 @@ final class IsConnectingDelegate: GRPCConnectionPoolDelegate { private var connecting = Set() private var active = Set() - enum StateNotifacation: Hashable, GRPCSendable { + enum StateNotifacation: Hashable, Sendable { case connecting case connected } - #if swift(>=5.6) private let onStateChange: @Sendable (StateNotifacation) -> Void - #else - private let onStateChange: (StateNotifacation) -> Void - #endif - #if swift(>=5.6) init(onStateChange: @escaping @Sendable (StateNotifacation) -> Void) { self.onStateChange = onStateChange } - #else - init(onStateChange: @escaping (StateNotifacation) -> Void) { - self.onStateChange = onStateChange - } - #endif func startedConnecting(id: GRPCConnectionID) { let didStartConnecting: Bool = self.lock.withLock { @@ -94,9 +84,7 @@ final class IsConnectingDelegate: GRPCConnectionPoolDelegate { func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) {} } -#if swift(>=5.6) extension IsConnectingDelegate: @unchecked Sendable {} -#endif final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { struct UnexpectedEvent: Error { @@ -200,6 +188,4 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { } } -#if swift(>=5.6) extension EventRecordingConnectionPoolDelegate: @unchecked Sendable {} -#endif // swift(>=5.6) diff --git a/Tests/GRPCTests/DelegatingErrorHandlerTests.swift b/Tests/GRPCTests/DelegatingErrorHandlerTests.swift index b3cf17ddc..5b5cfc413 100644 --- a/Tests/GRPCTests/DelegatingErrorHandlerTests.swift +++ b/Tests/GRPCTests/DelegatingErrorHandlerTests.swift @@ -44,9 +44,7 @@ class DelegatingErrorHandlerTests: GRPCTestCase { XCTAssertEqual(delegate.errors[0] as? NIOSSLError, .writeDuringTLSShutdown) } } -#endif // canImport(NIOSSL) -#if canImport(NIOSSL) && compiler(>=5.6) // Unchecked because the error recorder is only ever used in the context of an EmbeddedChannel. extension DelegatingErrorHandlerTests.ErrorRecorder: @unchecked Sendable {} -#endif // canImport(NIOSSL) && compiler(>=5.6) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift b/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift index 10e140699..01c00d57e 100644 --- a/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift +++ b/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift @@ -19,12 +19,8 @@ import GRPC // MARK: - Client internal final class EchoClientInterceptors: Echo_EchoClientInterceptorFactoryProtocol { - #if swift(>=5.6) internal typealias Factory = @Sendable () -> ClientInterceptor - #else - internal typealias Factory = () -> ClientInterceptor - #endif // swift(>=5.6) private let factories: [Factory] internal init(_ factories: Factory...) { diff --git a/Tests/GRPCTests/EchoMetadataTests.swift b/Tests/GRPCTests/EchoMetadataTests.swift index 74c17b063..567450d5e 100644 --- a/Tests/GRPCTests/EchoMetadataTests.swift +++ b/Tests/GRPCTests/EchoMetadataTests.swift @@ -81,11 +81,9 @@ internal final class EchoMetadataTests: GRPCTestCase { self.testServiceDescriptor(Echo_EchoClientMetadata.serviceDescriptor) self.testServiceDescriptor(Echo_EchoServerMetadata.serviceDescriptor) - #if swift(>=5.6) if #available(macOS 12, *) { self.testServiceDescriptor(Echo_EchoAsyncClient.serviceDescriptor) } - #endif } func testGet() { diff --git a/Tests/GRPCTests/ErrorRecordingDelegate.swift b/Tests/GRPCTests/ErrorRecordingDelegate.swift index 477be9a16..2991ad26a 100644 --- a/Tests/GRPCTests/ErrorRecordingDelegate.swift +++ b/Tests/GRPCTests/ErrorRecordingDelegate.swift @@ -18,10 +18,8 @@ import Logging import NIOConcurrencyHelpers import XCTest -#if compiler(>=5.6) // Unchecked as all mutable state is accessed and modified behind a lock. extension ErrorRecordingDelegate: @unchecked Sendable {} -#endif // compiler(>=5.6) final class ErrorRecordingDelegate: ClientErrorDelegate { private let lock: NIOLock diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift index 067c41504..7f2fb3502 100644 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - import EchoImplementation import EchoModel @testable import GRPC @@ -211,7 +209,7 @@ class GRPCAsyncClientCallTests: GRPCTestCase { // Workaround https://bugs.swift.org/browse/SR-15070 (compiler crashes when defining a class/actor // in an async context). @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -fileprivate actor RequestResponseCounter { +private actor RequestResponseCounter { var numResponses = 0 var numRequests = 0 @@ -223,5 +221,3 @@ fileprivate actor RequestResponseCounter { self.numRequests += 1 } } - -#endif diff --git a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift index 85460e9f6..057d37713 100644 --- a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift +++ b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift @@ -13,8 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if compiler(>=5.6) - @testable import GRPC import NIOCore import NIOEmbedded @@ -614,4 +612,3 @@ extension Optional where Wrapped == GRPCServerResponsePart { } } } -#endif diff --git a/Tests/GRPCTests/GRPCInteroperabilityTests.swift b/Tests/GRPCTests/GRPCInteroperabilityTests.swift index 9f82d005d..14efae0fe 100644 --- a/Tests/GRPCTests/GRPCInteroperabilityTests.swift +++ b/Tests/GRPCTests/GRPCInteroperabilityTests.swift @@ -252,7 +252,6 @@ class GRPCSecureInteroperabilityTests: GRPCInsecureInteroperabilityTests { } #endif // canImport(NIOSSL) -#if compiler(>=5.6) @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) class GRPCInsecureInteroperabilityAsyncTests: GRPCInsecureInteroperabilityTests { override func makeProvider() -> CallHandlerProvider { @@ -418,4 +417,3 @@ class GRPCSecureInteroperabilityAsyncTests: GRPCInsecureInteroperabilityAsyncTes } } #endif // canImport(NIOSSL) -#endif // compiler(>=5.6) diff --git a/Tests/GRPCTests/InterceptorsTests.swift b/Tests/GRPCTests/InterceptorsTests.swift index 1d8176abb..a57b9cd36 100644 --- a/Tests/GRPCTests/InterceptorsTests.swift +++ b/Tests/GRPCTests/InterceptorsTests.swift @@ -158,9 +158,7 @@ class HelloWorldProvider: Helloworld_GreeterProvider { } } -#if swift(>=5.6) extension HelloWorldClientInterceptorFactory: @unchecked Sendable {} -#endif // swift(>=5.6) private class HelloWorldClientInterceptorFactory: Helloworld_GreeterClientInterceptorFactoryProtocol { diff --git a/Tests/GRPCTests/XCTestHelpers.swift b/Tests/GRPCTests/XCTestHelpers.swift index d0ea66481..6f23221c6 100644 --- a/Tests/GRPCTests/XCTestHelpers.swift +++ b/Tests/GRPCTests/XCTestHelpers.swift @@ -660,8 +660,6 @@ struct ExpressionMatcher { } } -#if compiler(>=5.6) - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) func assertThat( _ expression: @autoclosure @escaping () async throws -> Value, @@ -695,5 +693,3 @@ func assertThat( XCTFail("ACTUAL: \(actual), EXPECTED: \(expected)", file: file, line: line) } } - -#endif From 87fa9edb5ab3b36bf466c957f57de2f78811ed7f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 14 Apr 2023 15:58:11 +0100 Subject: [PATCH 085/580] Change a stray GRPCSendable to Sendable (#1588) Motivation: Occurrences of GRPCSendable were replaced with Sendable in #1584 as we dropped support for 5.5. However, one was missed. Modifications: - Change last `GRPCSendable` to `Sendable` Result: Fewer warnings. --- Sources/GRPC/Server.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index d3c0ba034..9ccef7faf 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -453,7 +453,7 @@ extension Server { } extension Server.Configuration { - public struct CORS: Hashable, GRPCSendable { + public struct CORS: Hashable, Sendable { /// Determines which 'origin' header field values are permitted in a CORS request. public var allowedOrigins: AllowedOrigins /// Sets the headers which are permitted in a response to a CORS request. From 75b390e901c7d70af9b4c5ca2677e035f00cd1ab Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 18 Apr 2023 08:09:56 +0100 Subject: [PATCH 086/580] Adopt h2handler multiplexer (#1587) Motivation: Switch from `HTTP2StreamMultiplexer` to `HTTP2Handler.StreamMultiplexer` to benefit from improved performance due to reduced allocations. Modifications: - Replace all references to `HTTP2StreamMultiplexer` with `HTTP2Handler.StreamMultiplexer`. - `GRPCIdleHandler` is now a `NIOHTTP2StreamDelegate` to allow it to receive notifications when a stream is created or closed rather than using `UserInboundEvent`s. - `GRPCIdleHandler` now has two states of configuration in the client case. This is required to break a cycle of dependencies which would otherwise exist at `init` because the `GRPCIdleHandler` holds a reference to the `HTTP2Handler.StreamMultiplexer` (which is a member of the `HTTP2Handler` itself) and the `HTTP2Handler` holds a reference back to the `GRPCIdleHandler` as its stream delegate. - Several tests now insert `HTTP2Handler` into the pipeline where they used to use `HTTP2StreamMultiplexer`. This causes a few changes in behavior often around different assertions. This required some small supporting changes. - Introduce test infrastructure `AsyncEventStreamConnectionPoolDelegate` to eliminate observed racey behavior in `EventRecordingConnectionPoolDelegate`. Result: Performance increase without Overall behavior changes. --- .github/workflows/ci.yaml | 40 +- NOTICES.txt | 8 + Package.swift | 2 +- Sources/GRPC/ClientConnection.swift | 91 +-- Sources/GRPC/ConnectionManager.swift | 41 +- .../ConnectionPool+PerConnectionState.swift | 6 +- .../ConnectionPool+Waiter.swift | 6 +- .../GRPC/ConnectionPool/ConnectionPool.swift | 16 +- Sources/GRPC/ConnectionPool/PoolManager.swift | 2 +- Sources/GRPC/GRPCClientChannelHandler.swift | 4 +- Sources/GRPC/GRPCIdleHandler.swift | 102 ++- .../GRPC/GRPCServerPipelineConfigurator.swift | 67 +- Sources/GRPC/Server.swift | 6 +- Tests/GRPCTests/ClientTimeoutTests.swift | 7 +- Tests/GRPCTests/ConnectionManagerTests.swift | 588 ++++++++++-------- .../ConnectionPoolDelegates.swift | 52 ++ .../ConnectionPool/ConnectionPoolTests.swift | 230 ++++++- .../ConnectionPool/GRPCChannelPoolTests.swift | 83 ++- 18 files changed, 867 insertions(+), 484 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a79d63662..2d31e40b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,44 +57,44 @@ jobs: include: - image: swiftlang/swift:nightly-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 347000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 167000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 169000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 176000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 176000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 - image: swift:5.8-jammy env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 347000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 167000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 169000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 176000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 176000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 - image: swift:5.7-jammy env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 347000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 167000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 169000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 176000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 176000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 - image: swift:5.6-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 348000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 168000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 247000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 139000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 170000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 177000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 177000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 130000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 137000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 137000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest diff --git a/NOTICES.txt b/NOTICES.txt index f1ff7bbab..a0898dd80 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -25,3 +25,11 @@ framework: 'test_01_allocation_counts.sh', 'run-nio-alloc-counter-tests.sh' and * https://github.com/apple/swift-nio/blob/main/LICENSE.txt * HOMEPAGE: * https://github.com/apple/swift-nio + +This product contains a simplified derivation of SwiftNIO HTTP/2's +'HTTP2FrameEncoder' for testing purposes. + + * LICENSE (Apache License 2.0): + * https://github.com/apple/swift-nio-http2/blob/main/LICENSE.txt + * HOMEPAGE: + * https://github.com/apple/swift-nio-http2 diff --git a/Package.swift b/Package.swift index e7b387df6..473622c25 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-nio-http2.git", - from: "1.24.1" + from: "1.26.0" ), .package( url: "https://github.com/apple/swift-nio-transport-services.git", diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index 395c52978..e44092d6e 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -53,28 +53,25 @@ import SwiftProtobuf /// │ DelegatingErrorHandler │ /// └──────────▲───────────────┘ /// HTTP2Frame│ -/// │ ⠇ ⠇ ⠇ ⠇ -/// │ ┌┴─▼┐ ┌┴─▼┐ -/// │ │ | │ | HTTP/2 streams -/// │ └▲─┬┘ └▲─┬┘ -/// │ │ │ │ │ HTTP2Frame -/// ┌─┴────────────────┴─▼───┴─▼┐ -/// │ HTTP2StreamMultiplexer | -/// └─▲───────────────────────┬─┘ -/// HTTP2Frame│ │HTTP2Frame -/// ┌─┴───────────────────────▼─┐ -/// │ GRPCIdleHandler │ -/// └─▲───────────────────────┬─┘ -/// HTTP2Frame│ │HTTP2Frame -/// ┌─┴───────────────────────▼─┐ -/// │ NIOHTTP2Handler │ -/// └─▲───────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// ┌─┴───────────────────────▼─┐ -/// │ NIOSSLHandler │ -/// └─▲───────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// │ ▼ +/// │ +/// │ +/// │ +/// │ +/// │ +/// HTTP2Frame│ ⠇ ⠇ ⠇ ⠇ ⠇ +/// ┌─┴──────────────────▼─┐ ┌┴─▼┐ ┌┴─▼┐ +/// │ GRPCIdleHandler │ │ | │ | HTTP/2 streams +/// └─▲──────────────────┬─┘ └▲─┬┘ └▲─┬┘ +/// HTTP2Frame│ │ │ │ │ │ HTTP2Frame +/// ┌─┴──────────────────▼────────┴─▼───┴─▼┐ +/// │ NIOHTTP2Handler │ +/// └─▲──────────────────────────────────┬─┘ +/// ByteBuffer│ │ByteBuffer +/// ┌─┴──────────────────────────────────▼─┐ +/// │ NIOSSLHandler │ +/// └─▲──────────────────────────────────┬─┘ +/// ByteBuffer│ │ByteBuffer +/// │ ▼ /// /// The 'GRPCIdleHandler' intercepts HTTP/2 frames and various events and is responsible for /// informing and controlling the state of the connection (idling and keepalive). The HTTP/2 streams @@ -83,7 +80,7 @@ public final class ClientConnection: Sendable { private let connectionManager: ConnectionManager /// HTTP multiplexer from the underlying channel handling gRPC calls. - internal func getMultiplexer() -> EventLoopFuture { + internal func getMultiplexer() -> EventLoopFuture { return self.connectionManager.getHTTP2Multiplexer() } @@ -245,7 +242,7 @@ extension ClientConnection: GRPCChannel { } private static func makeStreamChannel( - using result: Result, + using result: Result, promise: EventLoopPromise ) { switch result { @@ -606,29 +603,31 @@ extension ChannelPipeline.SynchronousOperations { HTTP2Setting(parameter: .initialWindowSize, value: httpTargetWindowSize), ] - // We could use 'configureHTTP2Pipeline' here, but we need to add a few handlers between the - // two HTTP/2 handlers so we'll do it manually instead. - try self.addHandler(NIOHTTP2Handler(mode: .client, initialSettings: initialSettings)) - - let h2Multiplexer = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - targetWindowSize: httpTargetWindowSize, - inboundStreamInitializer: nil - ) - - // The multiplexer is passed through the idle handler so it is only reported on - // successful channel activation - with happy eyeballs multiple pipelines can - // be constructed so it's not safe to report just yet. - try self.addHandler(GRPCIdleHandler( + let grpcIdleHandler = GRPCIdleHandler( connectionManager: connectionManager, - multiplexer: h2Multiplexer, idleTimeout: connectionIdleTimeout, keepalive: connectionKeepalive, logger: logger - )) + ) + + var connectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() + connectionConfiguration.initialSettings = initialSettings + var streamConfiguration = NIOHTTP2Handler.StreamConfiguration() + streamConfiguration.targetWindowSize = httpTargetWindowSize + let h2Handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + connectionConfiguration: connectionConfiguration, + streamConfiguration: streamConfiguration, + streamDelegate: grpcIdleHandler + ) { channel in + channel.close() + } + try self.addHandler(h2Handler) + + grpcIdleHandler.setMultiplexer(try h2Handler.syncMultiplexer()) + try self.addHandler(grpcIdleHandler) - try self.addHandler(h2Multiplexer) try self.addHandler(DelegatingErrorHandler(logger: logger, delegate: errorDelegate)) } } @@ -638,7 +637,13 @@ extension Channel { errorDelegate: ClientErrorDelegate?, logger: Logger ) -> EventLoopFuture { - return self.configureHTTP2Pipeline(mode: .client, inboundStreamInitializer: nil).flatMap { _ in + return self.configureHTTP2Pipeline( + mode: .client, + connectionConfiguration: .init(), + streamConfiguration: .init() + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + }.flatMap { _ in self.pipeline.addHandler(DelegatingErrorHandler(logger: logger, delegate: errorDelegate)) } } diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 35ee41101..efb801e48 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -35,19 +35,23 @@ internal final class ConnectionManager: @unchecked Sendable { var reconnect: Reconnect var candidate: EventLoopFuture - var readyChannelMuxPromise: EventLoopPromise - var candidateMuxPromise: EventLoopPromise + var readyChannelMuxPromise: EventLoopPromise + var candidateMuxPromise: EventLoopPromise } internal struct ConnectedState { var backoffIterator: ConnectionBackoffIterator? var reconnect: Reconnect var candidate: Channel - var readyChannelMuxPromise: EventLoopPromise - var multiplexer: HTTP2StreamMultiplexer + var readyChannelMuxPromise: EventLoopPromise + var multiplexer: NIOHTTP2Handler.StreamMultiplexer var error: Error? - init(from state: ConnectingState, candidate: Channel, multiplexer: HTTP2StreamMultiplexer) { + init( + from state: ConnectingState, + candidate: Channel, + multiplexer: NIOHTTP2Handler.StreamMultiplexer + ) { self.backoffIterator = state.backoffIterator self.reconnect = state.reconnect self.candidate = candidate @@ -58,7 +62,7 @@ internal final class ConnectionManager: @unchecked Sendable { internal struct ReadyState { var channel: Channel - var multiplexer: HTTP2StreamMultiplexer + var multiplexer: NIOHTTP2Handler.StreamMultiplexer var error: Error? init(from state: ConnectedState) { @@ -69,7 +73,7 @@ internal final class ConnectionManager: @unchecked Sendable { internal struct TransientFailureState { var backoffIterator: ConnectionBackoffIterator? - var readyChannelMuxPromise: EventLoopPromise + var readyChannelMuxPromise: EventLoopPromise var scheduled: Scheduled var reason: Error @@ -252,8 +256,8 @@ internal final class ConnectionManager: @unchecked Sendable { } } - /// Returns the `HTTP2StreamMultiplexer` from the 'ready' state or `nil` if it is not available. - private var multiplexer: HTTP2StreamMultiplexer? { + /// Returns the `NIOHTTP2Handler.StreamMultiplexer` from the 'ready' state or `nil` if it is not available. + private var multiplexer: NIOHTTP2Handler.StreamMultiplexer? { self.eventLoop.assertInEventLoop() switch self.state { case let .ready(state): @@ -361,8 +365,8 @@ internal final class ConnectionManager: @unchecked Sendable { /// Get the multiplexer from the underlying channel handling gRPC calls. /// if the `ConnectionManager` was configured to be `fastFailure` this will have /// one chance to connect - if not reconnections are managed here. - internal func getHTTP2Multiplexer() -> EventLoopFuture { - func getHTTP2Multiplexer0() -> EventLoopFuture { + internal func getHTTP2Multiplexer() -> EventLoopFuture { + func getHTTP2Multiplexer0() -> EventLoopFuture { switch self.callStartBehavior { case .waitsForConnectivity: return self.getHTTP2MultiplexerPatient() @@ -382,8 +386,8 @@ internal final class ConnectionManager: @unchecked Sendable { /// Returns a future for the multiplexer which succeeded when the channel is connected. /// Reconnects are handled if necessary. - private func getHTTP2MultiplexerPatient() -> EventLoopFuture { - let multiplexer: EventLoopFuture + private func getHTTP2MultiplexerPatient() -> EventLoopFuture { + let multiplexer: EventLoopFuture switch self.state { case .idle: @@ -421,11 +425,12 @@ internal final class ConnectionManager: @unchecked Sendable { /// attempt, or if the state is 'idle' returns the future for the next connection attempt. /// /// Note: if the state is 'transientFailure' or 'shutdown' then a failed future will be returned. - private func getHTTP2MultiplexerOptimistic() -> EventLoopFuture { + private func getHTTP2MultiplexerOptimistic() + -> EventLoopFuture { // `getHTTP2Multiplexer` makes sure we're on the event loop but let's just be sure. self.eventLoop.preconditionInEventLoop() - let muxFuture: EventLoopFuture = { () in + let muxFuture: EventLoopFuture = { () in switch self.state { case .idle: self.startConnecting() @@ -656,7 +661,7 @@ internal final class ConnectionManager: @unchecked Sendable { } /// The connecting channel became `active`. Must be called on the `EventLoop`. - internal func channelActive(channel: Channel, multiplexer: HTTP2StreamMultiplexer) { + internal func channelActive(channel: Channel, multiplexer: NIOHTTP2Handler.StreamMultiplexer) { self.eventLoop.preconditionInEventLoop() self.logger.debug("activating connection", metadata: [ "connectivity_state": "\(self.state.label)", @@ -973,7 +978,7 @@ extension ConnectionManager { private func startConnecting( backoffIterator: ConnectionBackoffIterator?, - muxPromise: EventLoopPromise + muxPromise: EventLoopPromise ) { let timeoutAndBackoff = backoffIterator?.next() @@ -1060,7 +1065,7 @@ extension ConnectionManager { /// Returns the `multiplexer` from a connection in the `ready` state or `nil` if it is any /// other state. - internal var multiplexer: HTTP2StreamMultiplexer? { + internal var multiplexer: NIOHTTP2Handler.StreamMultiplexer? { return self.manager.multiplexer } diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift index 02d182919..1ae2dc9f3 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift @@ -53,7 +53,7 @@ extension ConnectionPool { } @usableFromInline - var multiplexer: HTTP2StreamMultiplexer + var multiplexer: NIOHTTP2Handler.StreamMultiplexer /// Maximum number of available streams. @usableFromInline var maxAvailable: Int @@ -73,7 +73,7 @@ extension ConnectionPool { /// Increment the reserved streams and return the multiplexer. @usableFromInline - mutating func reserve() -> HTTP2StreamMultiplexer { + mutating func reserve() -> NIOHTTP2Handler.StreamMultiplexer { assert(!self.isQuiescing) self.reserved += 1 return self.multiplexer @@ -127,7 +127,7 @@ extension ConnectionPool { /// /// The result may be safely unwrapped if `self.availableStreams > 0` when reserving a stream. @usableFromInline - internal mutating func reserveStream() -> HTTP2StreamMultiplexer? { + internal mutating func reserveStream() -> NIOHTTP2Handler.StreamMultiplexer? { return self._availability?.reserve() } diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift index b4b386f8b..ac9b6d810 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift @@ -30,7 +30,7 @@ extension ConnectionPool { /// The channel initializer. @usableFromInline - internal let _channelInitializer: (Channel) -> EventLoopFuture + internal let _channelInitializer: @Sendable (Channel) -> EventLoopFuture /// The deadline at which the timeout is scheduled. @usableFromInline @@ -51,7 +51,7 @@ extension ConnectionPool { internal init( deadline: NIODeadline, promise: EventLoopPromise, - channelInitializer: @escaping (Channel) -> EventLoopFuture + channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { self._deadline = deadline self._promise = promise @@ -83,7 +83,7 @@ extension ConnectionPool { /// Succeed the waiter with the given multiplexer. @usableFromInline - internal func succeed(with multiplexer: HTTP2StreamMultiplexer) { + internal func succeed(with multiplexer: NIOHTTP2Handler.StreamMultiplexer) { self._scheduledTimeout?.cancel() self._scheduledTimeout = nil multiplexer.createStreamChannel(promise: self._promise, self._channelInitializer) diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 554d3b5bb..6cda6364c 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -215,7 +215,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { if self.eventLoop.inEventLoop { self._makeStream( @@ -241,7 +241,7 @@ internal final class ConnectionPool { internal func makeStream( deadline: NIODeadline, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> EventLoopFuture { let promise = self.eventLoop.makePromise(of: Channel.self) self.makeStream(deadline: deadline, promise: promise, logger: logger, initializer: initializer) @@ -277,7 +277,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { self.eventLoop.assertInEventLoop() @@ -310,7 +310,7 @@ internal final class ConnectionPool { @inlinable internal func _tryMakeStream( promise: EventLoopPromise, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> Bool { // We shouldn't jump the queue. guard self.waiters.isEmpty else { @@ -344,7 +344,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { // Don't overwhelm the pool with too many waiters. guard self.waiters.count < self.maxWaiters else { @@ -479,10 +479,10 @@ internal final class ConnectionPool { /// Reserves a stream from the connection with the most available streams, if one exists. /// - /// - Returns: The `HTTP2StreamMultiplexer` from the connection the stream was reserved from, + /// - Returns: The `NIOHTTP2Handler.StreamMultiplexer` from the connection the stream was reserved from, /// or `nil` if no stream could be reserved. @usableFromInline - internal func _reserveStreamFromMostAvailableConnection() -> HTTP2StreamMultiplexer? { + internal func _reserveStreamFromMostAvailableConnection() -> NIOHTTP2Handler.StreamMultiplexer? { let index = self._mostAvailableConnectionIndex() if index != self._connections.endIndex { @@ -701,7 +701,7 @@ extension ConnectionPool: ConnectionManagerHTTP2Delegate { ) } - // Don't return the stream to the pool manager if the connection is quescing, they were returned + // Don't return the stream to the pool manager if the connection is quiescing, they were returned // when the connection started quiescing. if !self._connections.values[index].isQuiescing { self.streamLender.returnStreams(1, to: self) diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index c9f745422..a6e53d59f 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -278,7 +278,7 @@ internal final class PoolManager { preferredEventLoop: EventLoop?, deadline: NIODeadline, logger: GRPCLogger, - streamInitializer initializer: @escaping (Channel) -> EventLoopFuture + streamInitializer initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> PooledStreamChannel { let preferredEventLoopID = preferredEventLoop.map { EventLoopID($0) } let reservedPool = self.lock.withLock { diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index 3464ef1c2..3ebe041d2 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -270,10 +270,10 @@ public enum GRPCCallType: Hashable, Sendable { /// This handler relies heavily on the `GRPCClientStateMachine` to manage the state of the request /// and response streams, which share a single HTTP/2 stream for transport. /// -/// Typical usage of this handler is with a `HTTP2StreamMultiplexer` from SwiftNIO HTTP2: +/// Typical usage of this handler is with a `NIOHTTP2Handler.StreamMultiplexer` from SwiftNIO HTTP2: /// /// ``` -/// let multiplexer: HTTP2StreamMultiplexer = // ... +/// let multiplexer: NIOHTTP2Handler.StreamMultiplexer = // ... /// multiplexer.createStreamChannel(promise: nil) { (channel, streamID) in /// let clientChannelHandler = GRPCClientChannelHandler( /// streamID: streamID, diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 11c82a075..2690cdcd5 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -35,24 +35,73 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { private var scheduledPing: RepeatedTask? /// The mode we're operating in. - private let mode: Mode + /// + /// This is a `var` to allow the client configuration state to be updated. + private var mode: Mode private var context: ChannelHandlerContext? + /// Keeps track of the client configuration state. + /// We need two levels of configuration to break the dependency cycle with the stream multiplexer. + internal enum ClientConfigurationState { + case partial(ConnectionManager) + case complete(ConnectionManager, NIOHTTP2Handler.StreamMultiplexer) + case deinitialized + + mutating func setMultiplexer(_ multiplexer: NIOHTTP2Handler.StreamMultiplexer) { + switch self { + case let .partial(connectionManager): + self = .complete(connectionManager, multiplexer) + case .complete: + preconditionFailure("Setting the multiplexer twice is not supported.") + case .deinitialized: + preconditionFailure( + "Setting the multiplexer after removing from a channel is not supported." + ) + } + } + } + /// The mode of operation: the client tracks additional connection state in the connection /// manager. internal enum Mode { - case client(ConnectionManager, HTTP2StreamMultiplexer) + case client(ClientConfigurationState) case server + mutating func setMultiplexer(_ multiplexer: NIOHTTP2Handler.StreamMultiplexer) { + switch self { + case var .client(clientConfigurationState): + clientConfigurationState.setMultiplexer(multiplexer) + self = .client(clientConfigurationState) + case .server: + preconditionFailure("Setting the multiplexer in server mode is not supported.") + } + } + var connectionManager: ConnectionManager? { switch self { - case let .client(manager, _): - return manager + case let .client(configurationState): + switch configurationState { + case let .complete(connectionManager, _): + return connectionManager + case let .partial(connectionManager): + return connectionManager + case .deinitialized: + return nil + } case .server: return nil } } + + mutating func deinitialize() { + switch self { + case .client: + self = .client(.deinitialized) + case .server: + break // nothing to drop + } + } } /// The current state. @@ -60,12 +109,11 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { init( connectionManager: ConnectionManager, - multiplexer: HTTP2StreamMultiplexer, idleTimeout: TimeAmount, keepalive configuration: ClientConnectionKeepalive, logger: Logger ) { - self.mode = .client(connectionManager, multiplexer) + self.mode = .client(.partial(connectionManager)) self.idleTimeout = idleTimeout self.stateMachine = .init(role: .client, logger: logger) self.pingHandler = PingHandler( @@ -98,6 +146,10 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { ) } + internal func setMultiplexer(_ multiplexer: NIOHTTP2Handler.StreamMultiplexer) { + self.mode.setMultiplexer(multiplexer) + } + private func sendGoAway(lastStreamID streamID: HTTP2StreamID) { guard let context = self.context else { return @@ -237,20 +289,11 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { func handlerRemoved(context: ChannelHandlerContext) { self.context = nil + self.mode.deinitialize() } func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - if let created = event as? NIOHTTP2StreamCreatedEvent { - self.perform(operations: self.stateMachine.streamCreated(withID: created.streamID)) - self.handlePingAction(self.pingHandler.streamCreated()) - self.mode.connectionManager?.streamOpened() - context.fireUserInboundEventTriggered(event) - } else if let closed = event as? StreamClosedEvent { - self.perform(operations: self.stateMachine.streamClosed(withID: closed.streamID)) - self.handlePingAction(self.pingHandler.streamClosed()) - self.mode.connectionManager?.streamClosed() - context.fireUserInboundEventTriggered(event) - } else if event is ChannelShouldQuiesceEvent { + if event is ChannelShouldQuiesceEvent { self.perform(operations: self.stateMachine.initiateGracefulShutdown()) // Swallow this event. } else if case let .handshakeCompleted(negotiatedProtocol) = event as? TLSUserEvent { @@ -279,8 +322,15 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { // No state machine action here. switch self.mode { - case let .client(connectionManager, multiplexer): - connectionManager.channelActive(channel: context.channel, multiplexer: multiplexer) + case let .client(configurationState): + switch configurationState { + case let .complete(connectionManager, multiplexer): + connectionManager.channelActive(channel: context.channel, multiplexer: multiplexer) + case .partial: + preconditionFailure("not yet initialised") + case .deinitialized: + preconditionFailure("removed from channel") + } case .server: () } @@ -319,6 +369,20 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { } } +extension GRPCIdleHandler: NIOHTTP2StreamDelegate { + func streamCreated(_ id: NIOHTTP2.HTTP2StreamID, channel: NIOCore.Channel) { + self.perform(operations: self.stateMachine.streamCreated(withID: id)) + self.handlePingAction(self.pingHandler.streamCreated()) + self.mode.connectionManager?.streamOpened() + } + + func streamClosed(_ id: NIOHTTP2.HTTP2StreamID, channel: NIOCore.Channel) { + self.perform(operations: self.stateMachine.streamClosed(withID: id)) + self.handlePingAction(self.pingHandler.streamClosed()) + self.mode.connectionManager?.streamClosed() + } +} + extension HTTP2SettingsParameter { internal var loggingMetadataKey: String { switch self { diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift index eba4f9c5a..a095c36d5 100644 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ b/Sources/GRPC/GRPCServerPipelineConfigurator.swift @@ -78,38 +78,39 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan } /// Makes an HTTP/2 handler. - private func makeHTTP2Handler() -> NIOHTTP2Handler { - return .init( - mode: .server, - initialSettings: [ - HTTP2Setting( - parameter: .maxConcurrentStreams, - value: self.configuration.httpMaxConcurrentStreams - ), - HTTP2Setting( - parameter: .maxHeaderListSize, - value: HPACKDecoder.defaultMaxHeaderListSize - ), - HTTP2Setting( - parameter: .maxFrameSize, - value: self.configuration.httpMaxFrameSize - ), - HTTP2Setting( - parameter: .initialWindowSize, - value: self.configuration.httpTargetWindowSize - ), - ] - ) - } - - /// Makes an HTTP/2 multiplexer suitable handling gRPC requests. - private func makeHTTP2Multiplexer(for channel: Channel) -> HTTP2StreamMultiplexer { - var logger = self.configuration.logger + private func makeHTTP2Handler( + for channel: Channel, + streamDelegate: NIOHTTP2StreamDelegate? + ) -> NIOHTTP2Handler { + var connectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() + connectionConfiguration.initialSettings = [ + HTTP2Setting( + parameter: .maxConcurrentStreams, + value: self.configuration.httpMaxConcurrentStreams + ), + HTTP2Setting( + parameter: .maxHeaderListSize, + value: HPACKDecoder.defaultMaxHeaderListSize + ), + HTTP2Setting( + parameter: .maxFrameSize, + value: self.configuration.httpMaxFrameSize + ), + HTTP2Setting( + parameter: .initialWindowSize, + value: self.configuration.httpTargetWindowSize + ), + ] + + var streamConfiguration = NIOHTTP2Handler.StreamConfiguration() + streamConfiguration.targetWindowSize = self.configuration.httpTargetWindowSize return .init( mode: .server, - channel: channel, - targetWindowSize: self.configuration.httpTargetWindowSize + eventLoop: channel.eventLoop, + connectionConfiguration: connectionConfiguration, + streamConfiguration: streamConfiguration, + streamDelegate: streamDelegate ) { stream in // Sync options were added to the HTTP/2 stream channel in 1.17.0 (we require at least this) // so this shouldn't be `nil`, but it's not a problem if it is. @@ -118,6 +119,7 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan return String(Int(streamID)) } ?? "" + var logger = self.configuration.logger logger[metadataKey: MetadataKey.h2StreamID] = "\(streamID)" do { @@ -165,13 +167,14 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan // to then insert our keepalive and idle handlers between. We can just add everything together. let result: Result + let idleHandler = self.makeIdleHandler() do { // This is only ever called as a result of reading a user inbound event or reading inbound so // we'll be on the right event loop and sync operations are fine. let sync = context.pipeline.syncOperations - try sync.addHandler(self.makeHTTP2Handler()) - try sync.addHandler(self.makeIdleHandler()) - try sync.addHandler(self.makeHTTP2Multiplexer(for: context.channel)) + try sync.addHandler(self.makeHTTP2Handler(for: context.channel, streamDelegate: idleHandler)) + // Here we intentionally don't associate the multiplexer with the idleHandler in the server case + try sync.addHandler(idleHandler) result = .success(()) } catch { result = .failure(error) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 9ccef7faf..4a7083ef6 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -54,10 +54,6 @@ import Network /// `GRPCServerPipelineConfigurator`. In the case of HTTP/2: /// /// ┌─────────────────────────────────┐ -/// │ HTTP2StreamMultiplexer │ -/// └─▲─────────────────────────────┬─┘ -/// HTTP2Frame│ │HTTP2Frame -/// ┌─┴─────────────────────────────▼─┐ /// │ HTTP2Handler │ /// └─▲─────────────────────────────┬─┘ /// ByteBuffer│ │ByteBuffer @@ -67,7 +63,7 @@ import Network /// ByteBuffer│ │ByteBuffer /// │ ▼ /// -/// The `HTTP2StreamMultiplexer` provides one `Channel` for each HTTP/2 stream (and thus each +/// The `NIOHTTP2Handler.StreamMultiplexer` provides one `Channel` for each HTTP/2 stream (and thus each /// RPC). /// /// 3. The frames for each stream channel are routed by the `HTTP2ToRawGRPCServerCodec` handler to diff --git a/Tests/GRPCTests/ClientTimeoutTests.swift b/Tests/GRPCTests/ClientTimeoutTests.swift index 0bf157101..ff1cef9f5 100644 --- a/Tests/GRPCTests/ClientTimeoutTests.swift +++ b/Tests/GRPCTests/ClientTimeoutTests.swift @@ -169,7 +169,7 @@ extension EmbeddedGRPCChannel: @unchecked Sendable {} private final class EmbeddedGRPCChannel: GRPCChannel { let embeddedChannel: EmbeddedChannel - let multiplexer: EventLoopFuture + let multiplexer: EventLoopFuture let logger: Logger let scheme: String @@ -195,8 +195,11 @@ private final class EmbeddedGRPCChannel: GRPCChannel { errorDelegate: errorDelegate, logger: logger ).flatMap { - embeddedChannel.pipeline.handler(type: HTTP2StreamMultiplexer.self) + embeddedChannel.pipeline.handler(type: NIOHTTP2Handler.self) + }.flatMap { h2Handler in + h2Handler.multiplexer } + self.scheme = "http" self.authority = "localhost" self.errorDelegate = errorDelegate diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 92f2333f3..b41daf4c7 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -116,7 +116,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let multiplexer: EventLoopFuture = self + let multiplexer: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let channel = manager.getHTTP2Multiplexer() self.loop.run() @@ -146,20 +146,22 @@ extension ConnectionManagerTests { // Setup the real channel and activate it. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -169,7 +171,7 @@ extension ConnectionManagerTests { // Write a settings frame on the root stream; this'll make the channel 'ready'. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) + XCTAssertNoThrow(try channel.writeInbound(frame.encode())) } // Close the channel. @@ -188,7 +190,7 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -197,20 +199,23 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -220,7 +225,7 @@ extension ConnectionManagerTests { // Write a settings frame on the root stream; this'll make the channel 'ready'. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) + XCTAssertNoThrow(try channel.writeInbound(frame.encode())) // Wait for the multiplexer, it _must_ be ready now. XCTAssertNoThrow(try readyChannelMux.wait()) } @@ -239,6 +244,24 @@ extension ConnectionManagerTests { } } + /// Forwards only the first `channelInactive` call + /// + /// This is useful in tests where we intentionally mis-use the channels + /// and call `fireChannelInactive` manually during the test but don't want + /// teardown to cause precondition failures due to this unexpected behavior. + class SwallowSecondInactiveHandler: ChannelInboundHandler { + typealias InboundIn = HTTP2Frame + typealias OutboundOut = HTTP2Frame + + private var seenAnInactive = false + func channelInactive(context: ChannelHandlerContext) { + if !self.seenAnInactive { + self.seenAnInactive = true + context.fireChannelInactive() + } + } + } + func testChannelInactiveBeforeActiveWithNoReconnect() throws { let channel = EmbeddedChannel(loop: self.loop) let channelPromise = self.loop.makePromise(of: Channel.self) @@ -253,28 +276,33 @@ extension ConnectionManagerTests { _ = manager.getHTTP2Multiplexer() self.loop.run() } - - try channel.pipeline.syncOperations.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ), - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.syncOperations.addHandler(SwallowSecondInactiveHandler()) + try channel.pipeline.syncOperations.addHandler(h2handler) + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.syncOperations.addHandler(idleHandler) + try channel.pipeline.syncOperations.addHandler(NIOCloseOnErrorHandler()) channelPromise.succeed(channel) - // Oops: wrong way around. We should tolerate this. - self.waitForStateChange(from: .connecting, to: .shutdown) { - channel.pipeline.fireChannelInactive() - } - // Should be ignored. + // Oops: wrong way around. We should tolerate this - just don't crash. + channel.pipeline.fireChannelInactive() channel.pipeline.fireChannelActive() + + channel.embeddedEventLoop.run() + try manager.shutdown(mode: .forceful).wait() } func testChannelInactiveBeforeActiveWillReconnect() throws { @@ -300,55 +328,59 @@ extension ConnectionManagerTests { // Setup the channel. let channel1 = channels.removeLast() let channel1Promise = channelPromises.removeLast() - - try channel1.pipeline.syncOperations.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: HTTP2StreamMultiplexer( - mode: .client, - channel: channel1, - inboundStreamInitializer: nil - ), - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) + let idleHandler1 = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) + let h2handler1 = NIOHTTP2Handler( + mode: .client, + eventLoop: channel1.eventLoop, + streamDelegate: idleHandler1 + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel1.pipeline.syncOperations.addHandler(SwallowSecondInactiveHandler()) + try channel1.pipeline.syncOperations.addHandler(h2handler1) + idleHandler1.setMultiplexer(try h2handler1.syncMultiplexer()) + try channel1.pipeline.syncOperations.addHandler(idleHandler1) + try channel1.pipeline.syncOperations.addHandler(NIOCloseOnErrorHandler()) channel1Promise.succeed(channel1) // Oops: wrong way around. We should tolerate this. - self.waitForStateChange(from: .connecting, to: .transientFailure) { - channel1.pipeline.fireChannelInactive() - } - + channel1.pipeline.fireChannelInactive() channel1.pipeline.fireChannelActive() // Start the next attempt. - self.waitForStateChange(from: .transientFailure, to: .connecting) { - self.loop.advanceTime(by: .seconds(1)) - } + self.loop.advanceTime(by: .seconds(1)) let channel2 = channels.removeLast() let channel2Promise = channelPromises.removeLast() - try channel2.pipeline.syncOperations.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: HTTP2StreamMultiplexer( - mode: .client, - channel: channel1, - inboundStreamInitializer: nil - ), - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) + let idleHandler2 = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) + let h2handler2 = NIOHTTP2Handler( + mode: .client, + eventLoop: channel2.eventLoop, + streamDelegate: idleHandler2 + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel2.pipeline.syncOperations.addHandler(SwallowSecondInactiveHandler()) + try channel2.pipeline.syncOperations.addHandler(h2handler2) + idleHandler2.setMultiplexer(try h2handler2.syncMultiplexer()) + try channel2.pipeline.syncOperations.addHandler(idleHandler2) + try channel2.pipeline.syncOperations.addHandler(NIOCloseOnErrorHandler()) channel2Promise.succeed(channel2) try self.waitForStateChange(from: .connecting, to: .ready) { channel2.pipeline.fireChannelActive() let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel2.writeInbound(frame)) + XCTAssertNoThrow(try channel2.writeInbound(frame.encode())) } } @@ -359,7 +391,7 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -368,20 +400,23 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(channel) XCTAssertNoThrow( @@ -392,18 +427,13 @@ extension ConnectionManagerTests { // Write a settings frame on the root stream; this'll make the channel 'ready'. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) + XCTAssertNoThrow(try channel.writeInbound(frame.encode())) // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. XCTAssertNoThrow(try readyChannelMux.wait()) } // "create" a stream; the details don't matter here. - let streamCreated = NIOHTTP2StreamCreatedEvent( - streamID: 1, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - channel.pipeline.fireUserInboundEventTriggered(streamCreated) + idleHandler.streamCreated(1, channel: channel) // Wait for the idle timeout: this should _not_ cause the channel to idle. self.loop.advanceTime(by: .minutes(5)) @@ -411,8 +441,7 @@ extension ConnectionManagerTests { // Now we're going to close the stream and wait for an idle timeout and then shutdown. self.waitForStateChange(from: .ready, to: .idle) { // Close the stream. - let streamClosed = StreamClosedEvent(streamID: 1, reason: nil) - channel.pipeline.fireUserInboundEventTriggered(streamClosed) + idleHandler.streamClosed(1, channel: channel) // ... wait for the idle timeout, self.loop.advanceTime(by: .minutes(5)) } @@ -431,7 +460,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -440,20 +469,23 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -491,10 +523,10 @@ extension ConnectionManagerTests { return next } - let readyChannelMux: EventLoopFuture = self.waitForStateChanges([ + let readyChannelMux = self.waitForStateChanges([ Change(from: .idle, to: .connecting), Change(from: .connecting, to: .transientFailure), - ]) { + ]) { () -> EventLoopFuture in // Get a HTTP/2 stream multiplexer. let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -512,20 +544,23 @@ extension ConnectionManagerTests { // Setup the actual channel and complete the promise. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -535,7 +570,7 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) + XCTAssertNoThrow(try channel.writeInbound(frame.encode())) } // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. @@ -556,7 +591,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -592,10 +627,10 @@ extension ConnectionManagerTests { self.loop.makeFailedFuture(DoomedChannelError()) } - let readyChannelMux: EventLoopFuture = self.waitForStateChanges([ + let readyChannelMux = self.waitForStateChanges([ Change(from: .idle, to: .connecting), Change(from: .connecting, to: .transientFailure), - ]) { + ]) { () -> EventLoopFuture in // Get a HTTP/2 stream multiplexer. let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -619,7 +654,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -628,20 +663,23 @@ extension ConnectionManagerTests { // Prepare the channel let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -694,7 +732,7 @@ extension ConnectionManagerTests { return next } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -703,20 +741,23 @@ extension ConnectionManagerTests { // Prepare the channel let firstChannel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: firstChannel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try firstChannel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: firstChannel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try firstChannel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try firstChannel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(firstChannel) XCTAssertNoThrow( @@ -770,7 +811,7 @@ extension ConnectionManagerTests { return next } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -779,20 +820,22 @@ extension ConnectionManagerTests { // Prepare the first channel let firstChannel = EmbeddedChannel(loop: self.loop) - let firstH2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: firstChannel, - inboundStreamInitializer: nil + let firstIdleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try firstChannel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: firstH2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + let firstH2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: firstChannel.eventLoop, + streamDelegate: firstIdleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try firstChannel.pipeline.addHandler(firstH2handler).wait() + firstIdleHandler.setMultiplexer(try firstH2handler.syncMultiplexer()) + try firstChannel.pipeline.addHandler(firstIdleHandler).wait() firstChannelPromise.succeed(firstChannel) XCTAssertNoThrow( try firstChannel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -802,19 +845,14 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try firstChannel.writeInbound(frame)) + XCTAssertNoThrow(try firstChannel.writeInbound(frame.encode())) } // Channel should now be ready. XCTAssertNoThrow(try readyChannelMux.wait()) // Kill the first channel. But first ensure there's an active RPC, otherwise we'll idle. - let streamCreated = NIOHTTP2StreamCreatedEvent( - streamID: 1, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - firstChannel.pipeline.fireUserInboundEventTriggered(streamCreated) + firstIdleHandler.streamCreated(1, channel: firstChannel) try self.waitForStateChange(from: .ready, to: .transientFailure) { XCTAssertNoThrow(try firstChannel.close().wait()) @@ -827,20 +865,22 @@ extension ConnectionManagerTests { // Prepare the second channel let secondChannel = EmbeddedChannel(loop: self.loop) - let secondH2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: secondChannel, - inboundStreamInitializer: nil + let secondIdleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try secondChannel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: secondH2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + let secondH2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: secondChannel.eventLoop, + streamDelegate: secondIdleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try secondChannel.pipeline.addHandler(secondH2handler).wait() + secondIdleHandler.setMultiplexer(try secondH2handler.syncMultiplexer()) + try secondChannel.pipeline.addHandler(secondIdleHandler).wait() secondChannelPromise.succeed(secondChannel) XCTAssertNoThrow( try secondChannel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -850,7 +890,7 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try secondChannel.writeInbound(frame)) + XCTAssertNoThrow(try secondChannel.writeInbound(frame.encode())) } // Now shutdown @@ -867,7 +907,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -876,20 +916,23 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() + + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + try channel.pipeline.addHandler(idleHandler).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -899,7 +942,7 @@ extension ConnectionManagerTests { try self.waitForStateChange(from: .connecting, to: .ready) { // Write a SETTINGS frame on the root stream. let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) + XCTAssertNoThrow(try channel.writeInbound(frame.encode())) } // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. @@ -912,7 +955,7 @@ extension ConnectionManagerTests { streamID: .rootStream, payload: .goAway(lastStreamID: 1, errorCode: .noError, opaqueData: nil) ) - XCTAssertNoThrow(try channel.writeInbound(goAway)) + XCTAssertNoThrow(try channel.writeInbound(goAway.encode())) self.loop.run() } @@ -1018,31 +1061,34 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self.waitForStateChange( - from: .idle, - to: .connecting - ) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } + let readyChannelMux: EventLoopFuture = self + .waitForStateChange( + from: .idle, + to: .connecting + ) { + let readyChannelMux = manager.getHTTP2Multiplexer() + self.loop.run() + return readyChannelMux + } // Setup the real channel and activate it. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - XCTAssertNoThrow(try channel.pipeline.addHandlers([ - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ), - ]).wait()) + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + XCTAssertNoThrow(try channel.pipeline.addHandler(idleHandler).wait()) channelPromise.succeed(channel) self.loop.run() @@ -1052,7 +1098,7 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) + XCTAssertNoThrow(try channel.writeInbound(frame.encode())) } // The channel should now be ready. @@ -1076,7 +1122,7 @@ extension ConnectionManagerTests { let readyChannelMux = self.waitForStateChange( from: .idle, to: .connecting - ) { () -> EventLoopFuture in + ) { () -> EventLoopFuture in let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() return readyChannelMux @@ -1084,20 +1130,22 @@ extension ConnectionManagerTests { // Setup the actual channel and activate it. let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil + let idleHandler = GRPCIdleHandler( + connectionManager: manager, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger ) - XCTAssertNoThrow(try channel.pipeline.addHandlers([ - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ), - ]).wait()) + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try channel.pipeline.addHandler(h2handler).wait() + idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) + XCTAssertNoThrow(try channel.pipeline.addHandler(idleHandler).wait()) channelPromise.succeed(channel) self.loop.run() @@ -1107,7 +1155,7 @@ extension ConnectionManagerTests { // "ready" the connection. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) + XCTAssertNoThrow(try channel.writeInbound(frame.encode())) } // The HTTP/2 stream multiplexer should now be ready. @@ -1140,12 +1188,6 @@ extension ConnectionManagerTests { XCTAssertNoThrow(try channel.finish()) } - let multiplexer = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - class HTTP2Delegate: ConnectionManagerHTTP2Delegate { var streamsOpened = 0 var streamsClosed = 0 @@ -1174,11 +1216,19 @@ extension ConnectionManagerTests { channelProvider: HookedChannelProvider { manager, eventLoop -> EventLoopFuture in let idleHandler = GRPCIdleHandler( connectionManager: manager, - multiplexer: multiplexer, idleTimeout: .minutes(5), keepalive: ClientConnectionKeepalive(), logger: self.logger ) + let h2Handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + try! channel.pipeline.syncOperations.addHandler(h2Handler) + idleHandler.setMultiplexer(try! h2Handler.syncMultiplexer()) // We're going to cheat a bit by not putting the multiplexer in the channel. This allows // us to just fire stream created/closed events into the channel. @@ -1210,30 +1260,26 @@ extension ConnectionManagerTests { let settings = [HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams)] return HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) } - XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 42))) + XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 42).encode())) // We're ready now so the future multiplexer will resolve and we'll have seen an update to // max concurrent streams. XCTAssertNoThrow(try futureMultiplexer.wait()) XCTAssertEqual(http2.maxConcurrentStreams, 42) - XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 13))) + XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 13).encode())) XCTAssertEqual(http2.maxConcurrentStreams, 13) + let streamDelegate = try channel.pipeline.handler(type: GRPCIdleHandler.self).wait() + // Open some streams. for streamID in stride(from: HTTP2StreamID(1), to: HTTP2StreamID(9), by: 2) { - let streamCreated = NIOHTTP2StreamCreatedEvent( - streamID: streamID, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - channel.pipeline.fireUserInboundEventTriggered(streamCreated) + streamDelegate.streamCreated(streamID, channel: channel) } // ... and then close them. for streamID in stride(from: HTTP2StreamID(1), to: HTTP2StreamID(9), by: 2) { - let streamClosed = StreamClosedEvent(streamID: streamID, reason: nil) - channel.pipeline.fireUserInboundEventTriggered(streamClosed) + streamDelegate.streamClosed(streamID, channel: channel) } XCTAssertEqual(http2.streamsOpened, 4) @@ -1246,7 +1292,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let multiplexer: EventLoopFuture = self.waitForStateChange( + let multiplexer: EventLoopFuture = self.waitForStateChange( from: .idle, to: .connecting ) { diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift index 4a063bfa3..924b38d3d 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift @@ -189,3 +189,55 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { } extension EventRecordingConnectionPoolDelegate: @unchecked Sendable {} + +final class AsyncEventStreamConnectionPoolDelegate: GRPCConnectionPoolDelegate { + private let continuation: AsyncStream.Continuation + + static func makeDelegateAndAsyncStream() + -> ( + AsyncEventStreamConnectionPoolDelegate, + AsyncStream + ) { + var continuation: AsyncStream.Continuation! + let asyncStream = AsyncStream(EventRecordingConnectionPoolDelegate.Event.self) { + continuation = $0 + } + return (Self(continuation: continuation), asyncStream) + } + + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + func connectionAdded(id: GRPCConnectionID) { + self.continuation.yield(.connectionAdded(id)) + } + + func startedConnecting(id: GRPCConnectionID) { + self.continuation.yield(.startedConnecting(id)) + } + + func connectFailed(id: GRPCConnectionID, error: Error) { + self.continuation.yield(.connectFailed(id)) + } + + func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) { + self.continuation.yield(.connectSucceeded(id, streamCapacity)) + } + + func connectionClosed(id: GRPCConnectionID, error: Error?) { + self.continuation.yield(.connectionClosed(id)) + } + + func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) { + self.continuation.yield(.connectionUtilizationChanged(id, streamsUsed, streamCapacity)) + } + + func connectionQuiescing(id: GRPCConnectionID) { + self.continuation.yield(.connectionQuiescing(id)) + } + + func connectionRemoved(id: GRPCConnectionID) { + self.continuation.yield(.connectionRemoved(id)) + } +} diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 08cf46a9b..8300f8a9d 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -1010,17 +1010,24 @@ extension ConnectionPool { // MARK: - Helpers +struct ChannelAndState { + let channel: EmbeddedChannel + let streamDelegate: NIOHTTP2StreamDelegate + var isActive: Bool +} + internal final class ChannelController { - private var channels: [EmbeddedChannel] = [] + private var channels: [ChannelAndState] = [] internal var count: Int { return self.channels.count } internal func finish() { - while let channel = self.channels.popLast() { - // We're okay with this throwing: some channels are left in a bad state (i.e. with errors). - _ = try? channel.finish() + while let state = self.channels.popLast() { + if state.isActive { + _ = try? state.channel.finish() + } } } @@ -1040,9 +1047,10 @@ internal final class ChannelController { line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } + self.channels[index].isActive = true XCTAssertNoThrow( - try self.channels[index].connect(to: .init(unixDomainSocketPath: "/")), + try self.channels[index].channel.connect(to: .init(unixDomainSocketPath: "/")), file: file, line: line ) @@ -1054,7 +1062,8 @@ internal final class ChannelController { line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } - self.channels[index].pipeline.fireChannelInactive() + self.channels[index].channel.pipeline.fireChannelInactive() + self.channels[index].isActive = false } internal func throwError( @@ -1064,7 +1073,7 @@ internal final class ChannelController { line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } - self.channels[index].pipeline.fireErrorCaught(error) + self.channels[index].channel.pipeline.fireErrorCaught(error) } internal func sendSettingsToChannel( @@ -1078,7 +1087,11 @@ internal final class ChannelController { let settings = [HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) - XCTAssertNoThrow(try self.channels[index].writeInbound(settingsFrame), file: file, line: line) + XCTAssertNoThrow( + try self.channels[index].channel.writeInbound(settingsFrame.encode()), + file: file, + line: line + ) } internal func sendGoAwayToChannel( @@ -1093,7 +1106,11 @@ internal final class ChannelController { payload: .goAway(lastStreamID: .maxID, errorCode: .noError, opaqueData: nil) ) - XCTAssertNoThrow(try self.channels[index].writeInbound(goAwayFrame), file: file, line: line) + XCTAssertNoThrow( + try self.channels[index].channel.writeInbound(goAwayFrame.encode()), + file: file, + line: line + ) } internal func openStreamInChannel( @@ -1104,13 +1121,8 @@ internal final class ChannelController { guard self.isValidIndex(index, file: file, line: line) else { return } // The details don't matter here. - let event = NIOHTTP2StreamCreatedEvent( - streamID: .rootStream, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - - self.channels[index].pipeline.fireUserInboundEventTriggered(event) + let channel = self.channels[index] + channel.streamDelegate.streamCreated(.rootStream, channel: channel.channel) } internal func closeStreamInChannel( @@ -1121,8 +1133,8 @@ internal final class ChannelController { guard self.isValidIndex(index, file: file, line: line) else { return } // The details don't matter here. - let event = StreamClosedEvent(streamID: .rootStream, reason: nil) - self.channels[index].pipeline.fireUserInboundEventTriggered(event) + let channel = self.channels[index] + channel.streamDelegate.streamClosed(.rootStream, channel: channel.channel) } } @@ -1134,24 +1146,27 @@ extension ChannelController: ConnectionManagerChannelProvider { logger: Logger ) -> EventLoopFuture { let channel = EmbeddedChannel(loop: eventLoop as! EmbeddedEventLoop) - self.channels.append(channel) - - let multiplexer = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) let idleHandler = GRPCIdleHandler( connectionManager: connectionManager, - multiplexer: multiplexer, idleTimeout: .minutes(5), keepalive: ClientConnectionKeepalive(), logger: logger ) + let h2handler = NIOHTTP2Handler( + mode: .client, + eventLoop: channel.eventLoop, + streamDelegate: idleHandler + ) { channel in + channel.eventLoop.makeSucceededVoidFuture() + } + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(h2handler)) + + idleHandler.setMultiplexer(try! h2handler.syncMultiplexer()) + self.channels.append(.init(channel: channel, streamDelegate: idleHandler, isActive: false)) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(idleHandler)) - XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(multiplexer)) return eventLoop.makeSucceededFuture(channel) } @@ -1198,3 +1213,164 @@ extension Optional where Wrapped == ConnectionPoolError { } } } + +// Simplified version of the frame encoder found in SwiftNIO HTTP/2 +struct HTTP2FrameEncoder { + mutating func encode(frame: HTTP2Frame, to buf: inout ByteBuffer) throws -> IOData? { + // note our starting point + let start = buf.writerIndex + + // +-----------------------------------------------+ + // | Length (24) | + // +---------------+---------------+---------------+ + // | Type (8) | Flags (8) | + // +-+-------------+---------------+-------------------------------+ + // |R| Stream Identifier (31) | + // +=+=============================================================+ + // | Frame Payload (0...) ... + // +---------------------------------------------------------------+ + + // skip 24-bit length for now, we'll fill that in later + buf.moveWriterIndex(forwardBy: 3) + + // 8-bit type + buf.writeInteger(frame.code()) + + // skip the 8 bit flags for now, we'll fill it in later as well. + let flagsIndex = buf.writerIndex + var flags = FrameFlags() + buf.moveWriterIndex(forwardBy: 1) + + // 32-bit stream identifier -- ensuring the top bit is empty + buf.writeInteger(Int32(frame.streamID)) + + // frame payload follows, which depends on the frame type itself + let payloadStart = buf.writerIndex + let extraFrameData: IOData? + let payloadSize: Int + + switch frame.payload { + case let .settings(.settings(settings)): + for setting in settings { + buf.writeInteger(setting.parameter.networkRepresentation()) + buf.writeInteger(UInt32(setting.value)) + } + + payloadSize = settings.count * 6 + extraFrameData = nil + + case .settings(.ack): + payloadSize = 0 + extraFrameData = nil + flags.insert(.ack) + + case let .goAway(lastStreamID, errorCode, opaqueData): + let streamVal = UInt32(Int(lastStreamID)) & ~0x8000_0000 + buf.writeInteger(streamVal) + buf.writeInteger(UInt32(errorCode.networkCode)) + + if let data = opaqueData { + payloadSize = data.readableBytes + 8 + extraFrameData = .byteBuffer(data) + } else { + payloadSize = 8 + extraFrameData = nil + } + + case .data, .headers, .priority, + .rstStream, .pushPromise, .ping, + .windowUpdate, .alternativeService, .origin: + preconditionFailure("Frame type not supported: \(frame.payload)") + } + + // Write the frame data. This is the payload size and the flags byte. + buf.writePayloadSize(payloadSize, at: start) + buf.setInteger(flags.rawValue, at: flagsIndex) + + // all bytes to write are in the provided buffer now + return extraFrameData + } + + struct FrameFlags: OptionSet { + internal private(set) var rawValue: UInt8 + + internal init(rawValue: UInt8) { + self.rawValue = rawValue + } + + /// ACK flag. Valid on SETTINGS and PING frames. + internal static let ack = FrameFlags(rawValue: 0x01) + } +} + +extension HTTP2SettingsParameter { + internal func networkRepresentation() -> UInt16 { + switch self { + case HTTP2SettingsParameter.headerTableSize: + return UInt16(1) + case HTTP2SettingsParameter.enablePush: + return UInt16(2) + case HTTP2SettingsParameter.maxConcurrentStreams: + return UInt16(3) + case HTTP2SettingsParameter.initialWindowSize: + return UInt16(4) + case HTTP2SettingsParameter.maxFrameSize: + return UInt16(5) + case HTTP2SettingsParameter.maxHeaderListSize: + return UInt16(6) + case HTTP2SettingsParameter.enableConnectProtocol: + return UInt16(8) + default: + preconditionFailure("Unknown settings parameter.") + } + } +} + +extension ByteBuffer { + fileprivate mutating func writePayloadSize(_ size: Int, at location: Int) { + // Yes, this performs better than running a UInt8 through the generic write(integer:) three times. + var bytes: (UInt8, UInt8, UInt8) + bytes.0 = UInt8((size & 0xFF0000) >> 16) + bytes.1 = UInt8((size & 0x00FF00) >> 8) + bytes.2 = UInt8(size & 0x0000FF) + withUnsafeBytes(of: bytes) { ptr in + _ = self.setBytes(ptr, at: location) + } + } +} + +extension HTTP2Frame { + internal func encode() throws -> ByteBuffer { + let allocator = ByteBufferAllocator() + var buffer = allocator.buffer(capacity: 1024) + + var frameEncoder = HTTP2FrameEncoder() + let extraData = try frameEncoder.encode(frame: self, to: &buffer) + if let extraData = extraData { + switch extraData { + case let .byteBuffer(extraBuffer): + buffer.writeImmutableBuffer(extraBuffer) + default: + preconditionFailure() + } + } + return buffer + } + + /// The one-byte identifier used to indicate the type of a frame on the wire. + internal func code() -> UInt8 { + switch self.payload { + case .data: return 0x0 + case .headers: return 0x1 + case .priority: return 0x2 + case .rstStream: return 0x3 + case .settings: return 0x4 + case .pushPromise: return 0x5 + case .ping: return 0x6 + case .goAway: return 0x7 + case .windowUpdate: return 0x8 + case .alternativeService: return 0xA + case .origin: return 0xC + } + } +} diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index b8c560bbb..8fbb6b2b9 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -441,54 +441,75 @@ final class GRPCChannelPoolTests: GRPCTestCase { } } - func testConnectionPoolDelegateSingleConnection() throws { - let recorder = EventRecordingConnectionPoolDelegate() + func testConnectionPoolDelegateSingleConnection() async throws { + let (delegate, stream) = AsyncEventStreamConnectionPoolDelegate.makeDelegateAndAsyncStream() self.setUpClientAndServer(withTLS: false, threads: 1) { - $0.delegate = recorder + $0.delegate = delegate } let warmup = self.echo.get(.with { $0.text = "" }) XCTAssertNoThrow(try warmup.status.wait()) - let id = try XCTUnwrap(recorder.first?.id) - XCTAssertEqual(recorder.popFirst(), .connectionAdded(id)) - XCTAssertEqual(recorder.popFirst(), .startedConnecting(id)) - XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 0, 100)) - - let rpcs: [ClientStreamingCall] = try (1 ... 10).map { i in + var iterator = stream.makeAsyncIterator() + + var event = await iterator.next() + let id = try XCTUnwrap(event?.id) + XCTAssertEqual(event, .connectionAdded(id)) + event = await iterator.next() + XCTAssertEqual(event, .startedConnecting(id)) + event = await iterator.next() + XCTAssertEqual(event, .connectSucceeded(id, 100)) + event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id, 1, 100)) + event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id, 0, 100)) + + let rpcs: [ClientStreamingCall] = try (1 ... 10).map { _ in let rpc = self.echo.collect() XCTAssertNoThrow(try rpc.sendMessage(.with { $0.text = "foo" }).wait()) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, i, 100)) return rpc } + for (i, _) in rpcs.enumerated() { + let event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id, i + 1, 100)) + } + for (i, rpc) in rpcs.enumerated() { XCTAssertNoThrow(try rpc.sendEnd().wait()) XCTAssertNoThrow(try rpc.status.wait()) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 10 - (i + 1), 100)) + let event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id, 10 - (i + 1), 100)) } XCTAssertNoThrow(try self.channel?.close().wait()) - XCTAssertEqual(recorder.popFirst(), .connectionClosed(id)) - XCTAssertEqual(recorder.popFirst(), .connectionRemoved(id)) - XCTAssert(recorder.isEmpty) + event = await iterator.next() + XCTAssertEqual(event, .connectionClosed(id)) + event = await iterator.next() + XCTAssertEqual(event, .connectionRemoved(id)) } - func testConnectionPoolDelegateQuiescing() throws { - let recorder = EventRecordingConnectionPoolDelegate() + func testConnectionPoolDelegateQuiescing() async throws { + let (delegate, stream) = AsyncEventStreamConnectionPoolDelegate.makeDelegateAndAsyncStream() self.setUpClientAndServer(withTLS: false, threads: 1) { - $0.delegate = recorder + $0.delegate = delegate } XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) - let id1 = try XCTUnwrap(recorder.first?.id) - XCTAssertEqual(recorder.popFirst(), .connectionAdded(id1)) - XCTAssertEqual(recorder.popFirst(), .startedConnecting(id1)) - XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 0, 100)) + + var iterator = stream.makeAsyncIterator() + + var event = await iterator.next() + let id1 = try XCTUnwrap(event?.id) + XCTAssertEqual(event, .connectionAdded(id1)) + event = await iterator.next() + XCTAssertEqual(event, .startedConnecting(id1)) + event = await iterator.next() + XCTAssertEqual(event, .connectSucceeded(id1, 100)) + event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id1, 1, 100)) + event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id1, 0, 100)) // Start an RPC. let rpc = self.echo.collect() @@ -496,9 +517,12 @@ final class GRPCChannelPoolTests: GRPCTestCase { // Complete another one to make sure the previous one is known by the server. XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 2, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) + event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id1, 1, 100)) + event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id1, 2, 100)) + event = await iterator.next() + XCTAssertEqual(event, .connectionUtilizationChanged(id1, 1, 100)) // Start shutting the server down. let didShutdown = self.server!.initiateGracefulShutdown() @@ -507,7 +531,8 @@ final class GRPCChannelPoolTests: GRPCTestCase { // Pause a moment so we know the client received the GOAWAY. let sleep = self.group.any().scheduleTask(in: .milliseconds(50)) {} XCTAssertNoThrow(try sleep.futureResult.wait()) - XCTAssertEqual(recorder.popFirst(), .connectionQuiescing(id1)) + event = await iterator.next() + XCTAssertEqual(event, .connectionQuiescing(id1)) // Finish the RPC. XCTAssertNoThrow(try rpc.sendEnd().wait()) From 2d5795d2ae8be00338abf1d3263ff1e9121b0a93 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 May 2023 11:52:11 +0100 Subject: [PATCH 087/580] Allow custom verification callback to be configured for servers (#1595) Motivation: NIOSSL allows users to override its certificate verification logic by setting a verification callback. gRPC allows clients to configure this but not servers. Modifications: - Add extra API to `GRPCTLSConfiguration` to allow custom verification callbacks to be set for servers. Result: Users can override the certificate verification logic on the server. --- Sources/GRPC/GRPCTLSConfiguration.swift | 60 ++++++++++++++- Sources/GRPC/Server.swift | 12 ++- .../AsyncAwaitSupport/AsyncClientTests.swift | 4 +- .../ConnectionPool/ConnectionPoolTests.swift | 1 - Tests/GRPCTests/ServerTLSErrorTests.swift | 77 +++++++++++++++++++ 5 files changed, 148 insertions(+), 6 deletions(-) diff --git a/Sources/GRPC/GRPCTLSConfiguration.swift b/Sources/GRPC/GRPCTLSConfiguration.swift index 8adc7ad50..99aebfd74 100644 --- a/Sources/GRPC/GRPCTLSConfiguration.swift +++ b/Sources/GRPC/GRPCTLSConfiguration.swift @@ -14,6 +14,7 @@ * limitations under the License. */ #if canImport(NIOSSL) +import NIOCore import NIOSSL #endif @@ -310,6 +311,38 @@ extension GRPCTLSConfiguration { trustRoots: NIOSSLTrustRoots = .default, certificateVerification: CertificateVerification = .none, requireALPN: Bool = true + ) -> GRPCTLSConfiguration { + return Self.makeServerConfigurationBackedByNIOSSL( + certificateChain: certificateChain, + privateKey: privateKey, + trustRoots: trustRoots, + certificateVerification: certificateVerification, + requireALPN: requireALPN, + customVerificationCallback: nil + ) + } + + /// TLS Configuration with suitable defaults for servers. + /// + /// This is a wrapper around `NIOSSL.TLSConfiguration` to restrict input to values which comply + /// with the gRPC protocol. + /// + /// - Parameter certificateChain: The certificate to offer during negotiation. + /// - Parameter privateKey: The private key associated with the leaf certificate. + /// - Parameter trustRoots: The trust roots to validate certificates, this defaults to using a + /// root provided by the platform. + /// - Parameter certificateVerification: Whether to verify the remote certificate. Defaults to + /// `.none`. + /// - Parameter requireALPN: Whether ALPN is required or not. + /// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic, + /// defaults to `nil`. + public static func makeServerConfigurationBackedByNIOSSL( + certificateChain: [NIOSSLCertificateSource], + privateKey: NIOSSLPrivateKeySource, + trustRoots: NIOSSLTrustRoots = .default, + certificateVerification: CertificateVerification = .none, + requireALPN: Bool = true, + customVerificationCallback: NIOSSLCustomVerificationCallback? = nil ) -> GRPCTLSConfiguration { var configuration = TLSConfiguration.makeServerConfiguration( certificateChain: certificateChain, @@ -323,7 +356,8 @@ extension GRPCTLSConfiguration { return GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( configuration: configuration, - requireALPN: requireALPN + requireALPN: requireALPN, + customVerificationCallback: customVerificationCallback ) } @@ -338,6 +372,28 @@ extension GRPCTLSConfiguration { public static func makeServerConfigurationBackedByNIOSSL( configuration: TLSConfiguration, requireALPN: Bool = true + ) -> GRPCTLSConfiguration { + return Self.makeServerConfigurationBackedByNIOSSL( + configuration: configuration, + requireALPN: requireALPN, + customVerificationCallback: nil + ) + } + + /// Creates a gRPC TLS Configuration suitable for servers using the given + /// `NIOSSL.TLSConfiguration`. + /// + /// - Note: If no ALPN tokens are set in `configuration.applicationProtocols` then "grpc-exp", + /// "h2", and "http/1.1" will be used. + /// - Parameters: + /// - configuration: The `NIOSSL.TLSConfiguration` to base this configuration on. + /// - requiresALPN: Whether the server enforces ALPN. Defaults to `true`. + /// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic, + /// defaults to `nil`. + public static func makeServerConfigurationBackedByNIOSSL( + configuration: TLSConfiguration, + requireALPN: Bool = true, + customVerificationCallback: NIOSSLCustomVerificationCallback? = nil ) -> GRPCTLSConfiguration { var configuration = configuration @@ -348,7 +404,7 @@ extension GRPCTLSConfiguration { let nioConfiguration = NIOConfiguration( configuration: configuration, - customVerificationCallback: nil, + customVerificationCallback: customVerificationCallback, hostnameOverride: nil, requireALPN: requireALPN ) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 4a7083ef6..b859591be 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -140,7 +140,17 @@ public final class Server { let sync = channel.pipeline.syncOperations #if canImport(NIOSSL) if let sslContext = try sslContext?.get() { - try sync.addHandler(NIOSSLServerHandler(context: sslContext)) + let sslHandler: NIOSSLServerHandler + if let verify = configuration.tlsConfiguration?.nioSSLCustomVerificationCallback { + sslHandler = NIOSSLServerHandler( + context: sslContext, + customVerificationCallback: verify + ) + } else { + sslHandler = NIOSSLServerHandler(context: sslContext) + } + + try sync.addHandler(sslHandler) } #endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift index 1d8697b7e..0a75efac7 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift @@ -33,12 +33,12 @@ final class AsyncClientCancellationTests: GRPCTestCase { override func tearDown() async throws { if self.pool != nil { - try self.pool.close().wait() + try await self.pool.close().get() self.pool = nil } if self.server != nil { - try self.server.close().wait() + try await self.server.close().get() self.server = nil } diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 8300f8a9d..d2dfb739c 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -1245,7 +1245,6 @@ struct HTTP2FrameEncoder { buf.writeInteger(Int32(frame.streamID)) // frame payload follows, which depends on the frame type itself - let payloadStart = buf.writerIndex let extraFrameData: IOData? let payloadSize: Int diff --git a/Tests/GRPCTests/ServerTLSErrorTests.swift b/Tests/GRPCTests/ServerTLSErrorTests.swift index 5914eeb9d..5e85c2cb2 100644 --- a/Tests/GRPCTests/ServerTLSErrorTests.swift +++ b/Tests/GRPCTests/ServerTLSErrorTests.swift @@ -124,6 +124,83 @@ class ServerTLSErrorTests: GRPCTestCase { XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)") } } + + func testServerCustomVerificationCallback() async throws { + let verificationCallbackInvoked = self.serverEventLoopGroup.next().makePromise(of: Void.self) + let configuration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( + certificateChain: [.certificate(SampleCertificate.server.certificate)], + privateKey: .privateKey(SamplePrivateKey.server), + certificateVerification: .fullVerification, + customVerificationCallback: { _, promise in + verificationCallbackInvoked.succeed() + promise.succeed(.failed) + } + ) + + let server = try await Server.usingTLS(with: configuration, on: self.serverEventLoopGroup) + .withServiceProviders([EchoProvider()]) + .bind(host: "localhost", port: 0) + .get() + defer { + XCTAssertNoThrow(try server.close().wait()) + } + + let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( + certificateChain: [.certificate(SampleCertificate.client.certificate)], + privateKey: .privateKey(SamplePrivateKey.client), + trustRoots: .certificates([SampleCertificate.ca.certificate]), + certificateVerification: .noHostnameVerification, + hostnameOverride: SampleCertificate.server.commonName + ) + + let client = try GRPCChannelPool.with( + target: .hostAndPort("localhost", server.channel.localAddress!.port!), + transportSecurity: .tls(clientTLSConfiguration), + eventLoopGroup: self.clientEventLoopGroup + ) + defer { + XCTAssertNoThrow(try client.close().wait()) + } + + let echo = Echo_EchoAsyncClient(channel: client) + + enum TaskResult { + case rpcFailed + case rpcSucceeded + case verificationCallbackInvoked + } + + await withTaskGroup(of: TaskResult.self, returning: Void.self) { group in + group.addTask { + // Call the service to start an RPC. + do { + _ = try await echo.get(.with { $0.text = "foo" }) + return .rpcSucceeded + } catch { + return .rpcFailed + } + } + + group.addTask { + // '!' is okay, the promise is only ever succeeded. + try! await verificationCallbackInvoked.futureResult.get() + return .verificationCallbackInvoked + } + + while let next = await group.next() { + switch next { + case .verificationCallbackInvoked: + // Expected. + group.cancelAll() + case .rpcFailed: + // Expected, carry on. + continue + case .rpcSucceeded: + XCTFail("RPC succeeded but shouldn't have") + } + } + } + } } #endif // canImport(NIOSSL) From ef8ffb937bad3bc3bc9136eca723b698b8b00d7c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 May 2023 13:41:25 +0100 Subject: [PATCH 088/580] Allow for more CORS configuration (#1594) Motivation: We added some level of CORS configuration support in #1583. This change adds further flexibility. Modifications: - Add an 'originBased' mode where the value of the origin header is returned in the response head. - Add a custom fallback where the user can specify a callback which is passed the value of the origin header and returns the value to return in the 'access-control-allow-origin' response header (or nil, if the origin is not allowed). Result: More flexibility for CORS. --- Sources/GRPC/Server.swift | 58 +++++++++++++++++++++++ Sources/GRPC/WebCORSHandler.swift | 4 ++ Tests/GRPCTests/WebCORSHandlerTests.swift | 44 +++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index b859591be..1d1c89c7c 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -489,7 +489,9 @@ extension Server.Configuration.CORS { public struct AllowedOrigins: Hashable, Sendable { enum Wrapped: Hashable, Sendable { case all + case originBased case only([String]) + case custom(AnyCustomCORSAllowedOrigin) } private(set) var wrapped: Wrapped @@ -500,10 +502,23 @@ extension Server.Configuration.CORS { /// Allow all origin values. public static let all = Self(.all) + /// Allow all origin values; similar to `all` but returns the value of the origin header field + /// in the 'access-control-allow-origin' response header (rather than "*"). + public static let originBased = Self(.originBased) + /// Allow only the given origin values. public static func only(_ allowed: [String]) -> Self { return Self(.only(allowed)) } + + /// Provide a custom CORS origin check. + /// + /// - Parameter checkOrigin: A closure which is called with the value of the 'origin' header + /// and returns the value to use in the 'access-control-allow-origin' response header, + /// or `nil` if the origin is not allowed. + public static func custom(_ custom: C) -> Self { + return Self(.custom(AnyCustomCORSAllowedOrigin(custom))) + } } } @@ -530,3 +545,46 @@ extension Comparable { return min(max(self, range.lowerBound), range.upperBound) } } + +public protocol GRPCCustomCORSAllowedOrigin: Sendable, Hashable { + /// Returns the value to use for the 'access-control-allow-origin' response header for the given + /// value of the 'origin' request header. + /// + /// - Parameter origin: The value of the 'origin' request header field. + /// - Returns: The value to use for the 'access-control-allow-origin' header field or `nil` if no + /// CORS related headers should be returned. + func check(origin: String) -> String? +} + +extension Server.Configuration.CORS.AllowedOrigins { + struct AnyCustomCORSAllowedOrigin: GRPCCustomCORSAllowedOrigin { + private var checkOrigin: @Sendable (String) -> String? + private let hashInto: @Sendable (inout Hasher) -> Void + #if swift(>=5.7) + private let isEqualTo: @Sendable (any GRPCCustomCORSAllowedOrigin) -> Bool + #else + private let isEqualTo: @Sendable (Any) -> Bool + #endif + + init(_ wrap: W) { + self.checkOrigin = { wrap.check(origin: $0) } + self.hashInto = { wrap.hash(into: &$0) } + self.isEqualTo = { wrap == ($0 as? W) } + } + + func check(origin: String) -> String? { + return self.checkOrigin(origin) + } + + func hash(into hasher: inout Hasher) { + self.hashInto(&hasher) + } + + static func == ( + lhs: Server.Configuration.CORS.AllowedOrigins.AnyCustomCORSAllowedOrigin, + rhs: Server.Configuration.CORS.AllowedOrigins.AnyCustomCORSAllowedOrigin + ) -> Bool { + return lhs.isEqualTo(rhs) + } + } +} diff --git a/Sources/GRPC/WebCORSHandler.swift b/Sources/GRPC/WebCORSHandler.swift index 0a2c838ac..7f586ed24 100644 --- a/Sources/GRPC/WebCORSHandler.swift +++ b/Sources/GRPC/WebCORSHandler.swift @@ -198,8 +198,12 @@ extension Server.Configuration.CORS.AllowedOrigins { switch self.wrapped { case .all: return "*" + case .originBased: + return origin case let .only(allowed): return allowed.contains(origin) ? origin : nil + case let .custom(custom): + return custom.check(origin: origin) } } } diff --git a/Tests/GRPCTests/WebCORSHandlerTests.swift b/Tests/GRPCTests/WebCORSHandlerTests.swift index f9d8255c7..e64d06b6f 100644 --- a/Tests/GRPCTests/WebCORSHandlerTests.swift +++ b/Tests/GRPCTests/WebCORSHandlerTests.swift @@ -93,6 +93,50 @@ internal final class WebCORSHandlerTests: XCTestCase { try self.runPreflightRequestTest(spec: spec) } + func testOptionsPreflightOriginBased() throws { + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .originBased, + allowedHeaders: ["x-grpc-web"], + allowCredentialedRequests: false, + preflightCacheExpiration: 60 + ), + requestOrigin: "foo", + expectOrigin: "foo", + expectAllowedHeaders: ["x-grpc-web"], + expectAllowCredentials: false, + expectMaxAge: "60" + ) + try self.runPreflightRequestTest(spec: spec) + } + + func testOptionsPreflightCustom() throws { + struct Wrapper: GRPCCustomCORSAllowedOrigin { + func check(origin: String) -> String? { + if origin == "foo" { + return "bar" + } else { + return nil + } + } + } + + let spec = PreflightRequestSpec( + configuration: .init( + allowedOrigins: .custom(Wrapper()), + allowedHeaders: ["x-grpc-web"], + allowCredentialedRequests: false, + preflightCacheExpiration: 60 + ), + requestOrigin: "foo", + expectOrigin: "bar", + expectAllowedHeaders: ["x-grpc-web"], + expectAllowCredentials: false, + expectMaxAge: "60" + ) + try self.runPreflightRequestTest(spec: spec) + } + func testOptionsPreflightAllowSomeOrigins() throws { let spec = PreflightRequestSpec( configuration: .init( From 4ab02e1ae5b4dfdd723773e955b62f35ccbaa7c7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 May 2023 14:22:54 +0100 Subject: [PATCH 089/580] Bump version number to 1.16.0 (#1596) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.16.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 1b8d031f9..b0207a555 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 15 + internal static let minor = 16 /// The patch version. internal static let patch = 0 From 76ae1e4bdde6c0dbedae5ad79ef51e78afe640d5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 25 May 2023 15:01:20 +0100 Subject: [PATCH 090/580] Fix warnings from nightly Swift builds (#1600) Motivation: Nightly swift builds produce a few warnings: mostly around shadowing generics from an outer scope. Modifications: - Fix warnings. Result: Fewer warnings. --- Sources/GRPC/ClientConnection.swift | 4 +++ .../Interceptor/ClientTransportFactory.swift | 26 ++++++++++--------- .../GRPCTestingConvenienceMethods.swift | 6 +++-- .../InteroperabilityTestCases.swift | 4 +-- Tests/GRPCTests/XCTestHelpers.swift | 22 ++++++++-------- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index e44092d6e..e3ac756e0 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#if os(Linux) @preconcurrency import Foundation +#else +import Foundation +#endif import Logging import NIOCore diff --git a/Sources/GRPC/Interceptor/ClientTransportFactory.swift b/Sources/GRPC/Interceptor/ClientTransportFactory.swift index a6f1e9065..85feb8d72 100644 --- a/Sources/GRPC/Interceptor/ClientTransportFactory.swift +++ b/Sources/GRPC/Interceptor/ClientTransportFactory.swift @@ -21,10 +21,10 @@ import protocol SwiftProtobuf.Message @usableFromInline internal struct ClientTransportFactory { /// The underlying transport factory. - private var factory: Factory + private var factory: Factory @usableFromInline - internal enum Factory { + internal enum Factory { case http2(HTTP2ClientTransportFactory) case fake(FakeClientTransportFactory) } @@ -45,13 +45,14 @@ internal struct ClientTransportFactory { /// - errorDelegate: A client error delegate. /// - Returns: A factory for making and configuring HTTP/2 based transport. @usableFromInline - internal static func http2( + internal static func http2( channel: EventLoopFuture, authority: String, scheme: String, maximumReceiveMessageLength: Int, errorDelegate: ClientErrorDelegate? - ) -> ClientTransportFactory { + ) -> ClientTransportFactory where Request: SwiftProtobuf.Message, + Response: SwiftProtobuf.Message { let http2 = HTTP2ClientTransportFactory( streamChannel: channel, scheme: scheme, @@ -72,13 +73,13 @@ internal struct ClientTransportFactory { /// - errorDelegate: A client error delegate. /// - Returns: A factory for making and configuring HTTP/2 based transport. @usableFromInline - internal static func http2( + internal static func http2( channel: EventLoopFuture, authority: String, scheme: String, maximumReceiveMessageLength: Int, errorDelegate: ClientErrorDelegate? - ) -> ClientTransportFactory { + ) -> ClientTransportFactory where Request: GRPCPayload, Response: GRPCPayload { let http2 = HTTP2ClientTransportFactory( streamChannel: channel, scheme: scheme, @@ -95,9 +96,10 @@ internal struct ClientTransportFactory { /// - Parameter fakeResponse: The fake response stream. /// - Returns: A factory for making and configuring fake transport. @usableFromInline - internal static func fake( + internal static func fake( _ fakeResponse: _FakeResponseStream? - ) -> ClientTransportFactory { + ) -> ClientTransportFactory where Request: SwiftProtobuf.Message, + Response: SwiftProtobuf.Message { let factory = FakeClientTransportFactory( fakeResponse, requestSerializer: ProtobufSerializer(), @@ -112,9 +114,9 @@ internal struct ClientTransportFactory { /// - Parameter fakeResponse: The fake response stream. /// - Returns: A factory for making and configuring fake transport. @usableFromInline - internal static func fake( + internal static func fake( _ fakeResponse: _FakeResponseStream? - ) -> ClientTransportFactory { + ) -> ClientTransportFactory where Request: GRPCPayload, Response: GRPCPayload { let factory = FakeClientTransportFactory( fakeResponse, requestSerializer: GRPCPayloadSerializer(), @@ -239,7 +241,7 @@ internal struct HTTP2ClientTransportFactory { ) } - fileprivate func configure(_ transport: ClientTransport) { + fileprivate func configure(_ transport: ClientTransport) { transport.configure { _ in self.streamChannel.flatMapThrowing { channel in // This initializer will always occur on the appropriate event loop, sync operations are @@ -342,7 +344,7 @@ internal struct FakeClientTransportFactory { ) } - fileprivate func configure(_ transport: ClientTransport) { + fileprivate func configure(_ transport: ClientTransport) { transport.configure { handler in if let fakeResponse = self.fakeResponseStream { return fakeResponse.channel.pipeline.addHandlers(self.codec, handler).always { result in diff --git a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift b/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift index 9d1d3b221..1c7dbae1c 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift @@ -21,9 +21,11 @@ import SwiftProtobuf // MARK: - Payload creation extension Grpc_Testing_Payload { - static func bytes(of body: inout T) -> Grpc_Testing_Payload { + static func bytes(of value: UInt64) -> Grpc_Testing_Payload { return Grpc_Testing_Payload.with { payload in - payload.body = Data(bytes: &body, count: MemoryLayout.size(ofValue: body)) + withUnsafeBytes(of: value) { bytes in + payload.body = Data(bytes) + } } } diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift index 1d0f647fc..eab6be6fd 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift @@ -67,8 +67,8 @@ class CacheableUnary: InteroperabilityTest { func run(using connection: ClientConnection) throws { let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - var timestamp = DispatchTime.now().rawValue - let request = Grpc_Testing_SimpleRequest.withPayload(of: .bytes(of: ×tamp)) + let timestamp = DispatchTime.now().uptimeNanoseconds + let request = Grpc_Testing_SimpleRequest.withPayload(of: .bytes(of: timestamp)) let headers: HPACKHeaders = ["x-user-ip": "1.2.3.4"] let callOptions = CallOptions(customMetadata: headers, cacheable: true) diff --git a/Tests/GRPCTests/XCTestHelpers.swift b/Tests/GRPCTests/XCTestHelpers.swift index 6f23221c6..5da278d0e 100644 --- a/Tests/GRPCTests/XCTestHelpers.swift +++ b/Tests/GRPCTests/XCTestHelpers.swift @@ -100,24 +100,24 @@ struct Matcher { // MARK: Sugar /// Just returns the provided matcher. - static func `is`(_ matcher: Matcher) -> Matcher { + static func `is`(_ matcher: Matcher) -> Matcher { return matcher } /// Just returns the provided matcher. - static func and(_ matcher: Matcher) -> Matcher { + static func and(_ matcher: Matcher) -> Matcher { return matcher } // MARK: Equality /// Checks the equality of the actual value against the provided value. See `equalTo(_:)`. - static func `is`(_ value: Value) -> Matcher { + static func `is`(_ value: V) -> Matcher { return .equalTo(value) } /// Checks the equality of the actual value against the provided value. - static func equalTo(_ expected: Value) -> Matcher { + static func equalTo(_ expected: V) -> Matcher { return .init { actual in actual == expected ? .match @@ -133,7 +133,7 @@ struct Matcher { } /// Matches if the value is `nil`. - static func `nil`() -> Matcher { + static func `nil`() -> Matcher { return .init { actual in actual == nil ? .match @@ -142,7 +142,7 @@ struct Matcher { } /// Matches if the value is not `nil`. - static func notNil(_ matcher: Matcher? = nil) -> Matcher { + static func notNil(_ matcher: Matcher? = nil) -> Matcher { return .init { actual in if let actual = actual { return matcher?.evaluate(actual) ?? .match @@ -154,7 +154,7 @@ struct Matcher { // MARK: Result - static func success(_ matcher: Matcher? = nil) -> Matcher> { + static func success(_ matcher: Matcher? = nil) -> Matcher> { return .init { actual in switch actual { case let .success(value): @@ -191,7 +191,7 @@ struct Matcher { // MARK: Utility - static func all(_ matchers: Matcher...) -> Matcher { + static func all(_ matchers: Matcher...) -> Matcher { return .init { actual in for matcher in matchers { let result = matcher.evaluate(actual) @@ -209,7 +209,7 @@ struct Matcher { // MARK: Type /// Checks that the actual value is an instance of the given type. - static func instanceOf(_: Expected.Type) -> Matcher { + static func instanceOf(_: Expected.Type) -> Matcher { return .init { actual in if actual is Expected { return .match @@ -635,7 +635,7 @@ struct ExpressionMatcher { /// Asserts that the expression does not throw and error. Returns the result of any provided /// matcher on the result of the expression. - static func doesNotThrow(_ matcher: Matcher? = nil) -> ExpressionMatcher { + static func doesNotThrow(_ matcher: Matcher? = nil) -> ExpressionMatcher { return .init { expression in do { let value = try expression() @@ -648,7 +648,7 @@ struct ExpressionMatcher { /// Asserts that the expression throws and error. Returns the result of any provided matcher /// on the error thrown by the expression. - static func `throws`(_ matcher: Matcher? = nil) -> ExpressionMatcher { + static func `throws`(_ matcher: Matcher? = nil) -> ExpressionMatcher { return .init { expression in do { let value = try expression() From 345dd6e5eb77d0001f1a7b979de84bd400a5f185 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 26 May 2023 11:43:28 +0100 Subject: [PATCH 091/580] Allow network framework tests to be skipped when no user interaction (#1602) Motivation: The network framework tests require user interaction on their first run. This is not possible in CI so skip the tests in those situations. Modifications: - Allow network framework tests to be skipped if import the pkcs12 bundle fails with 'errSecInteractionNotAllowed'. Result: Fewer test failures. --- .../AsyncAwaitSupport/AsyncClientTests.swift | 2 +- .../GRPCTests/GRPCNetworkFrameworkTests.swift | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift index 0a75efac7..06ff6ff84 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift @@ -42,7 +42,7 @@ final class AsyncClientCancellationTests: GRPCTestCase { self.server = nil } - try self.group.syncShutdownGracefully() + try await self.group.shutdownGracefully() self.group = nil try await super.tearDown() diff --git a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift index e75d55a2f..e3a765219 100644 --- a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift +++ b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift @@ -46,19 +46,20 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { .appendingPathComponent("bundle") .appendingPathExtension("p12") - override func setUp() { - super.setUp() + // Not really 'async' but there is no 'func setUp() throws' to override. + override func setUp() async throws { + try await super.setUp() self.tsGroup = NIOTSEventLoopGroup(loopCount: 1) self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.identity = try? self.loadIdentity() + self.identity = try self.loadIdentity() XCTAssertNotNil( self.identity, "Unable to load identity from '\(GRPCNetworkFrameworkTests.p12bundleURL)'" ) - self.pkcs12Bundle = try? NIOSSLPKCS12Bundle( + self.pkcs12Bundle = try NIOSSLPKCS12Bundle( file: GRPCNetworkFrameworkTests.p12bundleURL.path, passphrase: "password".utf8 ) @@ -70,10 +71,10 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { } override func tearDown() { - XCTAssertNoThrow(try self.client.close().wait()) - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - XCTAssertNoThrow(try self.tsGroup.syncShutdownGracefully()) + XCTAssertNoThrow(try self.client?.close().wait()) + XCTAssertNoThrow(try self.server?.close().wait()) + XCTAssertNoThrow(try self.group?.syncShutdownGracefully()) + XCTAssertNoThrow(try self.tsGroup?.syncShutdownGracefully()) super.tearDown() } @@ -84,8 +85,13 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { var rawItems: CFArray? let status = SecPKCS12Import(data as CFData, options as CFDictionary, &rawItems) - guard status == errSecSuccess else { - XCTFail("SecPKCS12Import failed with status \(status)") + switch status { + case errSecSuccess: + () + case errSecInteractionNotAllowed: + throw XCTSkip("Unable to import PKCS12 bundle: no interaction allowed") + default: + XCTFail("SecPKCS12Import: failed with status \(status)") return nil } From c7d65217d8a5a3cfbb4cb19865daf7ed5a93f25c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 26 May 2023 14:54:41 +0100 Subject: [PATCH 092/580] Fix stream accounting bug when stream close leads to connection close (#1603) Motivation: The connection pool manager manages a pool of connections per event-loop. It spreads load across these pools by tracking how many streams a pool has capacity for and how many streams are in use. To facilitate this each pool reports back to the pool manager when streams have been reserved and when they have been returned. If connections are closed unexpectedly (due to an error, for example) then the pool reports this in bulk. However when the streams are closed they are also reported back to the pool manager. This means the manager can end up thinking a pool has a negative number of reserved streams which results in an assertion failure. Modifications: - Check if the connection a stream is being returned to is available before reporting stream closures to the pool manager. Result: - Better stream accounting. - Resolved #1598 --- .../ConnectionPool+PerConnectionState.swift | 5 +++ .../GRPC/ConnectionPool/ConnectionPool.swift | 6 +-- .../ConnectionPool/ConnectionPoolTests.swift | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift index 1ae2dc9f3..0d4211924 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift @@ -26,6 +26,11 @@ extension ConnectionPool { @usableFromInline internal var _availability: StreamAvailability? + @usableFromInline + internal var isAvailable: Bool { + return self._availability != nil + } + @usableFromInline internal var isQuiescing: Bool { get { diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 6cda6364c..40c633764 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -701,9 +701,9 @@ extension ConnectionPool: ConnectionManagerHTTP2Delegate { ) } - // Don't return the stream to the pool manager if the connection is quiescing, they were returned - // when the connection started quiescing. - if !self._connections.values[index].isQuiescing { + // Return the stream to the pool manager if the connection is available and not quiescing. For + // quiescing connections streams were returned when the connection started quiescing. + if self._connections.values[index].isAvailable, !self._connections.values[index].isQuiescing { self.streamLender.returnStreams(1, to: self) // A stream was returned: we may be able to service a waiter now. diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index d2dfb739c..4c381c5f9 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -876,6 +876,50 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertNil(waiter._scheduledTimeout) } + func testReturnStreamAfterConnectionCloses() throws { + var returnedStreams = 0 + let (pool, controller) = self.setUpPoolAndController(onReservationReturned: { returned in + returnedStreams += returned + }) + pool.initialize(connections: 1) + + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + $0.eventLoop.makeSucceededVoidFuture() + } + // Start creating the channel. + self.eventLoop.run() + XCTAssertEqual(controller.count, 1) + + // Fire up the connection. + controller.connectChannel(atIndex: 0) + controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) + + // Run the loop to create the stream, we need to fire the stream creation event too. + self.eventLoop.run() + XCTAssertNoThrow(try waiter.wait()) + controller.openStreamInChannel(atIndex: 0) + + XCTAssertEqual(pool.sync.waiters, 0) + XCTAssertEqual(pool.sync.availableStreams, 9) + XCTAssertEqual(pool.sync.reservedStreams, 1) + XCTAssertEqual(pool.sync.connections, 1) + + // Close all streams on connection 0. + let error = GRPCStatus(code: .internalError, message: nil) + controller.throwError(error, inChannelAtIndex: 0) + controller.fireChannelInactiveForChannel(atIndex: 0) + XCTAssertEqual(returnedStreams, 1) + + XCTAssertEqual(pool.sync.waiters, 0) + XCTAssertEqual(pool.sync.availableStreams, 0) + XCTAssertEqual(pool.sync.reservedStreams, 0) + XCTAssertEqual(pool.sync.connections, 1) + + // The connection is closed so the stream shouldn't be returned again. + controller.closeStreamInChannel(atIndex: 0) + XCTAssertEqual(returnedStreams, 1) + } + func testConnectionPoolDelegate() throws { let recorder = EventRecordingConnectionPoolDelegate() let (pool, controller) = self.setUpPoolAndController(delegate: recorder) From a48b5dd5c0e82a640a2b26610a42e7c865f59ddf Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 31 May 2023 11:28:47 +0100 Subject: [PATCH 093/580] Improve Sendable checking for server code (#1605) Motivation: The server handler protocol must be Sendable so that any methods on the handler are also Sendable as annotating methods as `@Sendable` does not work as expected. It follows from this that generated server interceptor factories must also be Sendable (they already are for clients). This puts the `ServerInterceptor` class in an awkward position: it must be Sendable but is not inherently thread-safe (these restrictions are documented and existed before Sendable checking was introduced to Swift). The `ClientInterceptor` has the same restrictions and is `@unchecked Sendable` so we elect to do the same for the `ServerInterceptor`. Modifications: - Make generated server handlers and server interceptor factories Sendable - Make `ServerInterceptor` `@unchecked Sendable` - Regenerate - Fix some warnings in test code Result: Better Sendable checking --- Sources/Examples/Echo/Model/echo.grpc.swift | 20 +++---- .../HelloWorld/Model/helloworld.grpc.swift | 8 +-- .../RouteGuide/Model/route_guide.grpc.swift | 20 +++---- .../GRPC/Interceptor/ServerInterceptors.swift | 2 +- .../Generated/test.grpc.swift | 52 +++++++++---------- .../TestServiceAsyncProvider.swift | 4 +- .../Generator-Server+AsyncAwait.swift | 15 +++--- .../Generator-Server.swift | 2 +- .../Normalization/normalization.grpc.swift | 36 ++++++------- .../EchoInterceptorFactories.swift | 11 ++-- Tests/GRPCTests/InterceptorsTests.swift | 2 +- Tests/GRPCTests/ServerInterceptorTests.swift | 4 +- 12 files changed, 85 insertions(+), 91 deletions(-) diff --git a/Sources/Examples/Echo/Model/echo.grpc.swift b/Sources/Examples/Echo/Model/echo.grpc.swift index f1bd21112..033d639e4 100644 --- a/Sources/Examples/Echo/Model/echo.grpc.swift +++ b/Sources/Examples/Echo/Model/echo.grpc.swift @@ -613,31 +613,31 @@ extension Echo_EchoProvider { /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Echo_EchoAsyncProvider: CallHandlerProvider { +public protocol Echo_EchoAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get } /// Immediately returns an echo of a request. - @Sendable func get( + func get( request: Echo_EchoRequest, context: GRPCAsyncServerCallContext ) async throws -> Echo_EchoResponse /// Splits a request into words and returns each word in a stream of messages. - @Sendable func expand( + func expand( request: Echo_EchoRequest, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext ) async throws /// Collects a stream of messages and returns them concatenated when the caller closes. - @Sendable func collect( + func collect( requestStream: GRPCAsyncRequestStream, context: GRPCAsyncServerCallContext ) async throws -> Echo_EchoResponse /// Streams back messages as they are received in an input stream. - @Sendable func update( + func update( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext @@ -669,7 +669,7 @@ extension Echo_EchoAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeGetInterceptors() ?? [], - wrapping: self.get(request:context:) + wrapping: { try await self.get(request: $0, context: $1) } ) case "Expand": @@ -678,7 +678,7 @@ extension Echo_EchoAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeExpandInterceptors() ?? [], - wrapping: self.expand(request:responseStream:context:) + wrapping: { try await self.expand(request: $0, responseStream: $1, context: $2) } ) case "Collect": @@ -687,7 +687,7 @@ extension Echo_EchoAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeCollectInterceptors() ?? [], - wrapping: self.collect(requestStream:context:) + wrapping: { try await self.collect(requestStream: $0, context: $1) } ) case "Update": @@ -696,7 +696,7 @@ extension Echo_EchoAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeUpdateInterceptors() ?? [], - wrapping: self.update(requestStream:responseStream:context:) + wrapping: { try await self.update(requestStream: $0, responseStream: $1, context: $2) } ) default: @@ -705,7 +705,7 @@ extension Echo_EchoAsyncProvider { } } -public protocol Echo_EchoServerInterceptorFactoryProtocol { +public protocol Echo_EchoServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'get'. /// Defaults to calling `self.makeInterceptors()`. diff --git a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift index cc229bcab..08c826be9 100644 --- a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift +++ b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift @@ -252,12 +252,12 @@ extension Helloworld_GreeterProvider { /// /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Helloworld_GreeterAsyncProvider: CallHandlerProvider { +public protocol Helloworld_GreeterAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? { get } /// Sends a greeting. - @Sendable func sayHello( + func sayHello( request: Helloworld_HelloRequest, context: GRPCAsyncServerCallContext ) async throws -> Helloworld_HelloReply @@ -288,7 +288,7 @@ extension Helloworld_GreeterAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeSayHelloInterceptors() ?? [], - wrapping: self.sayHello(request:context:) + wrapping: { try await self.sayHello(request: $0, context: $1) } ) default: @@ -297,7 +297,7 @@ extension Helloworld_GreeterAsyncProvider { } } -public protocol Helloworld_GreeterServerInterceptorFactoryProtocol { +public protocol Helloworld_GreeterServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'sayHello'. /// Defaults to calling `self.makeInterceptors()`. diff --git a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift index e8ab16682..807beb875 100644 --- a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift +++ b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift @@ -530,7 +530,7 @@ extension Routeguide_RouteGuideProvider { /// /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Routeguide_RouteGuideAsyncProvider: CallHandlerProvider { +public protocol Routeguide_RouteGuideAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Routeguide_RouteGuideServerInterceptorFactoryProtocol? { get } @@ -540,7 +540,7 @@ public protocol Routeguide_RouteGuideAsyncProvider: CallHandlerProvider { /// /// A feature with an empty name is returned if there's no feature at the given /// position. - @Sendable func getFeature( + func getFeature( request: Routeguide_Point, context: GRPCAsyncServerCallContext ) async throws -> Routeguide_Feature @@ -551,7 +551,7 @@ public protocol Routeguide_RouteGuideAsyncProvider: CallHandlerProvider { /// streamed rather than returned at once (e.g. in a response message with a /// repeated field), as the rectangle may cover a large area and contain a /// huge number of features. - @Sendable func listFeatures( + func listFeatures( request: Routeguide_Rectangle, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext @@ -561,7 +561,7 @@ public protocol Routeguide_RouteGuideAsyncProvider: CallHandlerProvider { /// /// Accepts a stream of Points on a route being traversed, returning a /// RouteSummary when traversal is completed. - @Sendable func recordRoute( + func recordRoute( requestStream: GRPCAsyncRequestStream, context: GRPCAsyncServerCallContext ) async throws -> Routeguide_RouteSummary @@ -570,7 +570,7 @@ public protocol Routeguide_RouteGuideAsyncProvider: CallHandlerProvider { /// /// Accepts a stream of RouteNotes sent while a route is being traversed, /// while receiving other RouteNotes (e.g. from other users). - @Sendable func routeChat( + func routeChat( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext @@ -602,7 +602,7 @@ extension Routeguide_RouteGuideAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeGetFeatureInterceptors() ?? [], - wrapping: self.getFeature(request:context:) + wrapping: { try await self.getFeature(request: $0, context: $1) } ) case "ListFeatures": @@ -611,7 +611,7 @@ extension Routeguide_RouteGuideAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeListFeaturesInterceptors() ?? [], - wrapping: self.listFeatures(request:responseStream:context:) + wrapping: { try await self.listFeatures(request: $0, responseStream: $1, context: $2) } ) case "RecordRoute": @@ -620,7 +620,7 @@ extension Routeguide_RouteGuideAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeRecordRouteInterceptors() ?? [], - wrapping: self.recordRoute(requestStream:context:) + wrapping: { try await self.recordRoute(requestStream: $0, context: $1) } ) case "RouteChat": @@ -629,7 +629,7 @@ extension Routeguide_RouteGuideAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeRouteChatInterceptors() ?? [], - wrapping: self.routeChat(requestStream:responseStream:context:) + wrapping: { try await self.routeChat(requestStream: $0, responseStream: $1, context: $2) } ) default: @@ -638,7 +638,7 @@ extension Routeguide_RouteGuideAsyncProvider { } } -public protocol Routeguide_RouteGuideServerInterceptorFactoryProtocol { +public protocol Routeguide_RouteGuideServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'getFeature'. /// Defaults to calling `self.makeInterceptors()`. diff --git a/Sources/GRPC/Interceptor/ServerInterceptors.swift b/Sources/GRPC/Interceptor/ServerInterceptors.swift index 48a3e2a06..835f34ded 100644 --- a/Sources/GRPC/Interceptor/ServerInterceptors.swift +++ b/Sources/GRPC/Interceptor/ServerInterceptors.swift @@ -42,7 +42,7 @@ import NIOCore /// require any extra attention. However, if work is done on a `DispatchQueue` or _other_ /// `EventLoop` then implementers should ensure that they use `context` from the correct /// `EventLoop`. -open class ServerInterceptor { +open class ServerInterceptor: @unchecked Sendable { public init() {} /// Called when the interceptor has received a request part to handle. diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift index 230bd1692..34dc74462 100644 --- a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift +++ b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift @@ -1251,18 +1251,18 @@ extension Grpc_Testing_TestServiceProvider { /// /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_TestServiceAsyncProvider: CallHandlerProvider { +public protocol Grpc_Testing_TestServiceAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? { get } /// One empty request followed by one empty response. - @Sendable func emptyCall( + func emptyCall( request: Grpc_Testing_Empty, context: GRPCAsyncServerCallContext ) async throws -> Grpc_Testing_Empty /// One request followed by one response. - @Sendable func unaryCall( + func unaryCall( request: Grpc_Testing_SimpleRequest, context: GRPCAsyncServerCallContext ) async throws -> Grpc_Testing_SimpleResponse @@ -1270,14 +1270,14 @@ public protocol Grpc_Testing_TestServiceAsyncProvider: CallHandlerProvider { /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. - @Sendable func cacheableUnaryCall( + func cacheableUnaryCall( request: Grpc_Testing_SimpleRequest, context: GRPCAsyncServerCallContext ) async throws -> Grpc_Testing_SimpleResponse /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. - @Sendable func streamingOutputCall( + func streamingOutputCall( request: Grpc_Testing_StreamingOutputCallRequest, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext @@ -1285,7 +1285,7 @@ public protocol Grpc_Testing_TestServiceAsyncProvider: CallHandlerProvider { /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. - @Sendable func streamingInputCall( + func streamingInputCall( requestStream: GRPCAsyncRequestStream, context: GRPCAsyncServerCallContext ) async throws -> Grpc_Testing_StreamingInputCallResponse @@ -1293,7 +1293,7 @@ public protocol Grpc_Testing_TestServiceAsyncProvider: CallHandlerProvider { /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. - @Sendable func fullDuplexCall( + func fullDuplexCall( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext @@ -1303,7 +1303,7 @@ public protocol Grpc_Testing_TestServiceAsyncProvider: CallHandlerProvider { /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. - @Sendable func halfDuplexCall( + func halfDuplexCall( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext @@ -1335,7 +1335,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeEmptyCallInterceptors() ?? [], - wrapping: self.emptyCall(request:context:) + wrapping: { try await self.emptyCall(request: $0, context: $1) } ) case "UnaryCall": @@ -1344,7 +1344,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [], - wrapping: self.unaryCall(request:context:) + wrapping: { try await self.unaryCall(request: $0, context: $1) } ) case "CacheableUnaryCall": @@ -1353,7 +1353,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeCacheableUnaryCallInterceptors() ?? [], - wrapping: self.cacheableUnaryCall(request:context:) + wrapping: { try await self.cacheableUnaryCall(request: $0, context: $1) } ) case "StreamingOutputCall": @@ -1362,7 +1362,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeStreamingOutputCallInterceptors() ?? [], - wrapping: self.streamingOutputCall(request:responseStream:context:) + wrapping: { try await self.streamingOutputCall(request: $0, responseStream: $1, context: $2) } ) case "StreamingInputCall": @@ -1371,7 +1371,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeStreamingInputCallInterceptors() ?? [], - wrapping: self.streamingInputCall(requestStream:context:) + wrapping: { try await self.streamingInputCall(requestStream: $0, context: $1) } ) case "FullDuplexCall": @@ -1380,7 +1380,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeFullDuplexCallInterceptors() ?? [], - wrapping: self.fullDuplexCall(requestStream:responseStream:context:) + wrapping: { try await self.fullDuplexCall(requestStream: $0, responseStream: $1, context: $2) } ) case "HalfDuplexCall": @@ -1389,7 +1389,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeHalfDuplexCallInterceptors() ?? [], - wrapping: self.halfDuplexCall(requestStream:responseStream:context:) + wrapping: { try await self.halfDuplexCall(requestStream: $0, responseStream: $1, context: $2) } ) default: @@ -1398,7 +1398,7 @@ extension Grpc_Testing_TestServiceAsyncProvider { } } -public protocol Grpc_Testing_TestServiceServerInterceptorFactoryProtocol { +public protocol Grpc_Testing_TestServiceServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'emptyCall'. /// Defaults to calling `self.makeInterceptors()`. @@ -1542,12 +1542,12 @@ extension Grpc_Testing_UnimplementedServiceProvider { /// /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_UnimplementedServiceAsyncProvider: CallHandlerProvider { +public protocol Grpc_Testing_UnimplementedServiceAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol? { get } /// A call that no server should implement - @Sendable func unimplementedCall( + func unimplementedCall( request: Grpc_Testing_Empty, context: GRPCAsyncServerCallContext ) async throws -> Grpc_Testing_Empty @@ -1578,7 +1578,7 @@ extension Grpc_Testing_UnimplementedServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [], - wrapping: self.unimplementedCall(request:context:) + wrapping: { try await self.unimplementedCall(request: $0, context: $1) } ) default: @@ -1587,7 +1587,7 @@ extension Grpc_Testing_UnimplementedServiceAsyncProvider { } } -public protocol Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol { +public protocol Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'unimplementedCall'. /// Defaults to calling `self.makeInterceptors()`. @@ -1662,16 +1662,16 @@ extension Grpc_Testing_ReconnectServiceProvider { /// /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_ReconnectServiceAsyncProvider: CallHandlerProvider { +public protocol Grpc_Testing_ReconnectServiceAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol? { get } - @Sendable func start( + func start( request: Grpc_Testing_ReconnectParams, context: GRPCAsyncServerCallContext ) async throws -> Grpc_Testing_Empty - @Sendable func stop( + func stop( request: Grpc_Testing_Empty, context: GRPCAsyncServerCallContext ) async throws -> Grpc_Testing_ReconnectInfo @@ -1702,7 +1702,7 @@ extension Grpc_Testing_ReconnectServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeStartInterceptors() ?? [], - wrapping: self.start(request:context:) + wrapping: { try await self.start(request: $0, context: $1) } ) case "Stop": @@ -1711,7 +1711,7 @@ extension Grpc_Testing_ReconnectServiceAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeStopInterceptors() ?? [], - wrapping: self.stop(request:context:) + wrapping: { try await self.stop(request: $0, context: $1) } ) default: @@ -1720,7 +1720,7 @@ extension Grpc_Testing_ReconnectServiceAsyncProvider { } } -public protocol Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol { +public protocol Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'start'. /// Defaults to calling `self.makeInterceptors()`. diff --git a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift index 9427b6e58..1df7f6f7a 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift @@ -22,8 +22,8 @@ import NIOCore /// /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md#server @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public class TestServiceAsyncProvider: Grpc_Testing_TestServiceAsyncProvider { - public var interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? +public final class TestServiceAsyncProvider: Grpc_Testing_TestServiceAsyncProvider { + public let interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? = nil public init() {} diff --git a/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift b/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift index ed4584335..d124a47a1 100644 --- a/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift +++ b/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift @@ -29,7 +29,7 @@ extension Generator { self.println("/// To implement a server, implement an object which conforms to this protocol.") self.printAvailabilityForAsyncAwait() self.withIndentation( - "\(self.access) protocol \(self.asyncProviderName): CallHandlerProvider", + "\(self.access) protocol \(self.asyncProviderName): CallHandlerProvider, Sendable", braces: .curly ) { self.println("static var serviceDescriptor: GRPCServiceDescriptor { get }") @@ -86,7 +86,7 @@ extension Generator { name: self.methodFunctionName, arguments: arguments, returnType: returnType, - sendable: true, + sendable: false, async: true, throws: true, bodyBuilder: nil @@ -152,20 +152,19 @@ extension Generator { self.println("requestDeserializer: \(Types.deserializer(for: requestType))(),") self.println("responseSerializer: \(Types.serializer(for: responseType))(),") self.println("interceptors: self.interceptors?.\(interceptorFactory)() ?? [],") + let prefix = "wrapping: { try await self.\(functionName)" switch streamingType(self.method) { case .unary: - self.println("wrapping: self.\(functionName)(request:context:)") + self.println("\(prefix)(request: $0, context: $1) }") case .clientStreaming: - self.println("wrapping: self.\(functionName)(requestStream:context:)") + self.println("\(prefix)(requestStream: $0, context: $1) }") case .serverStreaming: - self.println("wrapping: self.\(functionName)(request:responseStream:context:)") + self.println("\(prefix)(request: $0, responseStream: $1, context: $2) }") case .bidirectionalStreaming: - self.println( - "wrapping: self.\(functionName)(requestStream:responseStream:context:)" - ) + self.println("\(prefix)(requestStream: $0, responseStream: $1, context: $2) }") } } } diff --git a/Sources/protoc-gen-grpc-swift/Generator-Server.swift b/Sources/protoc-gen-grpc-swift/Generator-Server.swift index 4a483fb01..a99249f9e 100644 --- a/Sources/protoc-gen-grpc-swift/Generator-Server.swift +++ b/Sources/protoc-gen-grpc-swift/Generator-Server.swift @@ -148,7 +148,7 @@ extension Generator { } private func printServerInterceptorFactoryProtocol() { - self.println("\(self.access) protocol \(self.serverInterceptorProtocolName) {") + self.println("\(self.access) protocol \(self.serverInterceptorProtocolName): Sendable {") self.withIndentation { // Method specific interceptors. for method in service.methods { diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift index 98be1ca17..79658cb9e 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift @@ -803,49 +803,49 @@ extension Normalization_NormalizationProvider { /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Normalization_NormalizationAsyncProvider: CallHandlerProvider { +internal protocol Normalization_NormalizationAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Normalization_NormalizationServerInterceptorFactoryProtocol? { get } - @Sendable func Unary( + func Unary( request: SwiftProtobuf.Google_Protobuf_Empty, context: GRPCAsyncServerCallContext ) async throws -> Normalization_FunctionName - @Sendable func unary( + func unary( request: SwiftProtobuf.Google_Protobuf_Empty, context: GRPCAsyncServerCallContext ) async throws -> Normalization_FunctionName - @Sendable func ServerStreaming( + func ServerStreaming( request: SwiftProtobuf.Google_Protobuf_Empty, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext ) async throws - @Sendable func serverStreaming( + func serverStreaming( request: SwiftProtobuf.Google_Protobuf_Empty, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext ) async throws - @Sendable func ClientStreaming( + func ClientStreaming( requestStream: GRPCAsyncRequestStream, context: GRPCAsyncServerCallContext ) async throws -> Normalization_FunctionName - @Sendable func clientStreaming( + func clientStreaming( requestStream: GRPCAsyncRequestStream, context: GRPCAsyncServerCallContext ) async throws -> Normalization_FunctionName - @Sendable func BidirectionalStreaming( + func BidirectionalStreaming( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext ) async throws - @Sendable func bidirectionalStreaming( + func bidirectionalStreaming( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext @@ -877,7 +877,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeUnaryInterceptors() ?? [], - wrapping: self.Unary(request:context:) + wrapping: { try await self.Unary(request: $0, context: $1) } ) case "unary": @@ -886,7 +886,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeunaryInterceptors() ?? [], - wrapping: self.unary(request:context:) + wrapping: { try await self.unary(request: $0, context: $1) } ) case "ServerStreaming": @@ -895,7 +895,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeServerStreamingInterceptors() ?? [], - wrapping: self.ServerStreaming(request:responseStream:context:) + wrapping: { try await self.ServerStreaming(request: $0, responseStream: $1, context: $2) } ) case "serverStreaming": @@ -904,7 +904,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeserverStreamingInterceptors() ?? [], - wrapping: self.serverStreaming(request:responseStream:context:) + wrapping: { try await self.serverStreaming(request: $0, responseStream: $1, context: $2) } ) case "ClientStreaming": @@ -913,7 +913,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeClientStreamingInterceptors() ?? [], - wrapping: self.ClientStreaming(requestStream:context:) + wrapping: { try await self.ClientStreaming(requestStream: $0, context: $1) } ) case "clientStreaming": @@ -922,7 +922,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeclientStreamingInterceptors() ?? [], - wrapping: self.clientStreaming(requestStream:context:) + wrapping: { try await self.clientStreaming(requestStream: $0, context: $1) } ) case "BidirectionalStreaming": @@ -931,7 +931,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeBidirectionalStreamingInterceptors() ?? [], - wrapping: self.BidirectionalStreaming(requestStream:responseStream:context:) + wrapping: { try await self.BidirectionalStreaming(requestStream: $0, responseStream: $1, context: $2) } ) case "bidirectionalStreaming": @@ -940,7 +940,7 @@ extension Normalization_NormalizationAsyncProvider { requestDeserializer: ProtobufDeserializer(), responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makebidirectionalStreamingInterceptors() ?? [], - wrapping: self.bidirectionalStreaming(requestStream:responseStream:context:) + wrapping: { try await self.bidirectionalStreaming(requestStream: $0, responseStream: $1, context: $2) } ) default: @@ -949,7 +949,7 @@ extension Normalization_NormalizationAsyncProvider { } } -internal protocol Normalization_NormalizationServerInterceptorFactoryProtocol { +internal protocol Normalization_NormalizationServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'Unary'. /// Defaults to calling `self.makeInterceptors()`. diff --git a/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift b/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift index 01c00d57e..907ea783a 100644 --- a/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift +++ b/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift @@ -19,8 +19,7 @@ import GRPC // MARK: - Client internal final class EchoClientInterceptors: Echo_EchoClientInterceptorFactoryProtocol { - internal typealias Factory = @Sendable () - -> ClientInterceptor + typealias Factory = @Sendable () -> ClientInterceptor private let factories: [Factory] internal init(_ factories: Factory...) { @@ -51,17 +50,13 @@ internal final class EchoClientInterceptors: Echo_EchoClientInterceptorFactoryPr // MARK: - Server internal final class EchoServerInterceptors: Echo_EchoServerInterceptorFactoryProtocol { - internal typealias Factory = () -> ServerInterceptor - private var factories: [Factory] = [] + typealias Factory = @Sendable () -> ServerInterceptor + private let factories: [Factory] internal init(_ factories: Factory...) { self.factories = factories } - internal func register(_ factory: @escaping Factory) { - self.factories.append(factory) - } - private func makeInterceptors() -> [ServerInterceptor] { return self.factories.map { $0() } } diff --git a/Tests/GRPCTests/InterceptorsTests.swift b/Tests/GRPCTests/InterceptorsTests.swift index a57b9cd36..4f53bd084 100644 --- a/Tests/GRPCTests/InterceptorsTests.swift +++ b/Tests/GRPCTests/InterceptorsTests.swift @@ -208,7 +208,7 @@ class NotReallyAuthServerInterceptor: } } -class HelloWorldServerInterceptorFactory: Helloworld_GreeterServerInterceptorFactoryProtocol { +final class HelloWorldServerInterceptorFactory: Helloworld_GreeterServerInterceptorFactoryProtocol { func makeSayHelloInterceptors( ) -> [ServerInterceptor] { return [RemoteAddressExistsInterceptor(), NotReallyAuthServerInterceptor()] diff --git a/Tests/GRPCTests/ServerInterceptorTests.swift b/Tests/GRPCTests/ServerInterceptorTests.swift index 063857fc7..9cc090446 100644 --- a/Tests/GRPCTests/ServerInterceptorTests.swift +++ b/Tests/GRPCTests/ServerInterceptorTests.swift @@ -178,7 +178,7 @@ class ServerInterceptorTests: GRPCTestCase { } } -class EchoInterceptorFactory: Echo_EchoServerInterceptorFactoryProtocol { +final class EchoInterceptorFactory: Echo_EchoServerInterceptorFactoryProtocol { private let interceptor: ServerInterceptor init(interceptor: ServerInterceptor) { @@ -271,7 +271,7 @@ class EchoFromInterceptor: Echo_EchoProvider { return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) } - class Interceptors: Echo_EchoServerInterceptorFactoryProtocol { + final class Interceptors: Echo_EchoServerInterceptorFactoryProtocol { func makeGetInterceptors() -> [ServerInterceptor] { return [Interceptor()] } From 6b55ce088ba842522e2719d8d5404c354fd7bedf Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 31 May 2023 13:03:13 +0100 Subject: [PATCH 094/580] Allow server handlers to send response headers directly (#1599) Motivation: The async server call context allows users to set headers which are sent when the first message is sent. In many cases this is fine, however, some use cases require the headers to be sent immediately. Modifications: - Add `sendHeaders(_:)` to the `GRPCAsyncServerCallContext` which sends headers to the client and throws if headers have already been written or it's too late to send them. Result: Headers can be sent directly from a server call handler. --- .../ServerHandlerStateMachine+Actions.swift | 5 +- .../ServerHandlerStateMachine+Draining.swift | 8 +- .../ServerHandlerStateMachine+Finished.swift | 4 +- .../ServerHandlerStateMachine+Handling.swift | 8 +- .../ServerHandlerStateMachine.swift | 2 +- .../GRPCAsyncServerCallContext.swift | 19 +- .../GRPCAsyncServerHandler.swift | 51 +++++- .../ServerHandlerStateMachineTests.swift | 9 +- .../GRPCTests/GRPCAsyncClientCallTests.swift | 170 +++++++++++++++++- 9 files changed, 252 insertions(+), 24 deletions(-) diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift index 80b5cf67a..c1aae5c00 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift @@ -70,12 +70,13 @@ extension ServerHandlerStateMachine { /// Update the metadata. It must not have been written yet. @inlinable - mutating func update(_ metadata: HPACKHeaders) { + mutating func update(_ metadata: HPACKHeaders) -> Bool { switch self { case .notWritten: self = .notWritten(metadata) + return true case .written: - assertionFailure("Metadata must not be set after it has been sent") + return false } } diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift index 5aa1f58eb..ffe572ef2 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift @@ -50,16 +50,16 @@ extension ServerHandlerStateMachine { @inlinable mutating func setResponseHeaders( _ metadata: HPACKHeaders - ) -> Self.NextStateAndOutput { - self.responseHeaders.update(metadata) - return .init(nextState: .draining(self)) + ) -> Self.NextStateAndOutput { + let output = self.responseHeaders.update(metadata) + return .init(nextState: .draining(self), output: output) } @inlinable mutating func setResponseTrailers( _ metadata: HPACKHeaders ) -> Self.NextStateAndOutput { - self.responseTrailers.update(metadata) + _ = self.responseTrailers.update(metadata) return .init(nextState: .draining(self)) } diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift index 5395775b9..8c978cbf3 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift @@ -35,8 +35,8 @@ extension ServerHandlerStateMachine { @inlinable mutating func setResponseHeaders( _ headers: HPACKHeaders - ) -> Self.NextStateAndOutput { - return .init(nextState: .finished(self)) + ) -> Self.NextStateAndOutput { + return .init(nextState: .finished(self), output: false) } @inlinable diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift index 7dd6340fb..424f5afb8 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift @@ -50,16 +50,16 @@ extension ServerHandlerStateMachine { @inlinable mutating func setResponseHeaders( _ metadata: HPACKHeaders - ) -> Self.NextStateAndOutput { - self.responseHeaders.update(metadata) - return .init(nextState: .handling(self)) + ) -> Self.NextStateAndOutput { + let output = self.responseHeaders.update(metadata) + return .init(nextState: .handling(self), output: output) } @inlinable mutating func setResponseTrailers( _ metadata: HPACKHeaders ) -> Self.NextStateAndOutput { - self.responseTrailers.update(metadata) + _ = self.responseTrailers.update(metadata) return .init(nextState: .handling(self)) } diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift index 3e7098a98..23c3728de 100644 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift +++ b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift @@ -27,7 +27,7 @@ internal struct ServerHandlerStateMachine { } @inlinable - mutating func setResponseHeaders(_ headers: HPACKHeaders) { + mutating func setResponseHeaders(_ headers: HPACKHeaders) -> Bool { switch self.state { case var .handling(handling): let nextStateAndOutput = handling.setResponseHeaders(headers) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift index 73da1c527..d12515962 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift @@ -30,6 +30,21 @@ public struct GRPCAsyncServerCallContext: Sendable { Response(contextProvider: self.contextProvider) } + /// Notifies the client that the RPC has been accepted for processing by the server. + /// + /// On accepting the RPC the server will send the given headers (which may be empty) along with + /// any transport specific headers (such the ":status" pseudo header) to the client. + /// + /// It is not necessary to call this function: the RPC is implicitly accepted when the first + /// response message is sent, however this may be useful when clients require an early indication + /// that the RPC has been accepted. + /// + /// If the RPC has already been accepted (either implicitly or explicitly) then this function is + /// a no-op. + public func acceptRPC(headers: HPACKHeaders) async { + await self.contextProvider.acceptRPC(headers) + } + /// Access the ``UserInfo`` dictionary which is shared with the interceptor contexts for this RPC. /// /// - Important: While ``UserInfo`` has value-semantics, this function accesses a reference @@ -87,8 +102,8 @@ extension GRPCAsyncServerCallContext { /// Set the metadata to return at the start of the RPC. /// /// - Important: If this is required it should be updated _before_ the first response is sent - /// via the response stream writer. Updates must not be made after the first response has - /// been sent. + /// via the response stream writer. Updates must not be made after the RPC has been accepted + /// or the first response has been sent otherwise this method will throw an error. public func setHeaders(_ headers: HPACKHeaders) async throws { try await self.contextProvider.setResponseHeaders(headers) } diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift index 02a237694..6da95584b 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift @@ -207,6 +207,16 @@ internal final class AsyncServerHandler< @usableFromInline internal var compressResponsesIfPossible: Bool + /// The interceptor pipeline does not track flushing as a separate event. The flush decision is + /// included with metadata alongside each message. For the status and trailers the flush is + /// implicit. For headers we track whether to flush here. + /// + /// In most cases the flush will be delayed until the first message is flushed and this will + /// remain unset. However, this may be set when the server handler + /// uses ``GRPCAsyncServerCallContext/sendHeaders(_:)``. + @usableFromInline + internal var flushNextHeaders: Bool + /// A state machine for the interceptor pipeline. @usableFromInline internal private(set) var interceptorStateMachine: ServerInterceptorStateMachine @@ -265,6 +275,7 @@ internal final class AsyncServerHandler< self.errorDelegate = context.errorDelegate self.compressionEnabledOnRPC = context.encoding.isEnabled self.compressResponsesIfPossible = true + self.flushNextHeaders = false self.logger = context.logger self.userInfoRef = Ref(UserInfo()) @@ -685,7 +696,9 @@ internal final class AsyncServerHandler< switch self.interceptorStateMachine.interceptedResponseMetadata() { case .forward: if let responseWriter = self.responseWriter { - responseWriter.sendMetadata(metadata, flush: false, promise: promise) + let flush = self.flushNextHeaders + self.flushNextHeaders = false + responseWriter.sendMetadata(metadata, flush: flush, promise: promise) } else if let promise = promise { promise.fail(GRPCStatus.processingError) } @@ -747,11 +760,44 @@ extension AsyncServerHandler: AsyncServerCallContextProvider { @usableFromInline internal func setResponseHeaders(_ headers: HPACKHeaders) async throws { let completed = self.eventLoop.submit { - self.handlerStateMachine.setResponseHeaders(headers) + if !self.handlerStateMachine.setResponseHeaders(headers) { + throw GRPCStatus( + code: .failedPrecondition, + message: "Tried to send response headers in an invalid state" + ) + } } try await completed.get() } + @usableFromInline + internal func acceptRPC(_ headers: HPACKHeaders) async { + let completed = self.eventLoop.submit { + guard self.handlerStateMachine.setResponseHeaders(headers) else { return } + + // Shh,it's a lie! We don't really have a message to send but the state machine doesn't know + // (or care) about that. It will, however, tell us if we can send the headers or not. + switch self.handlerStateMachine.sendMessage() { + case let .intercept(.some(headers)): + switch self.interceptorStateMachine.interceptResponseMetadata() { + case .intercept: + self.flushNextHeaders = true + self.interceptors?.send(.metadata(headers), promise: nil) + case .cancel: + return self.cancel(error: nil) + case .drop: + () + } + + case .intercept(.none), .drop: + // intercept(.none) means headers have already been sent; we should never hit this because + // we guard on setting the response headers above. + () + } + } + try? await completed.get() + } + @usableFromInline internal func setResponseTrailers(_ headers: HPACKHeaders) async throws { let completed = self.eventLoop.submit { @@ -798,6 +844,7 @@ extension AsyncServerHandler: AsyncServerCallContextProvider { @usableFromInline protocol AsyncServerCallContextProvider: Sendable { func setResponseHeaders(_ headers: HPACKHeaders) async throws + func acceptRPC(_ headers: HPACKHeaders) async func setResponseTrailers(_ trailers: HPACKHeaders) async throws func setResponseCompression(_ enabled: Bool) async throws diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift index b1742e01f..22d133180 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift @@ -210,7 +210,7 @@ internal final class ServerHandlerStateMachineTests: GRPCTestCase { func testSetResponseHeadersWhenHandling() { var stateMachine = self.makeStateMachine(inState: .handling) - stateMachine.setResponseHeaders(["foo": "bar"]) + XCTAssertTrue(stateMachine.setResponseHeaders(["foo": "bar"])) stateMachine.sendMessage().assertInterceptHeadersThenMessage { headers in XCTAssertEqual(headers, ["foo": "bar"]) } @@ -218,7 +218,7 @@ internal final class ServerHandlerStateMachineTests: GRPCTestCase { func testSetResponseHeadersWhenHandlingAreMovedToDraining() { var stateMachine = self.makeStateMachine(inState: .handling) - stateMachine.setResponseHeaders(["foo": "bar"]) + XCTAssertTrue(stateMachine.setResponseHeaders(["foo": "bar"])) stateMachine.handleEnd().assertForward() stateMachine.sendMessage().assertInterceptHeadersThenMessage { headers in XCTAssertEqual(headers, ["foo": "bar"]) @@ -227,7 +227,7 @@ internal final class ServerHandlerStateMachineTests: GRPCTestCase { func testSetResponseHeadersWhenDraining() { var stateMachine = self.makeStateMachine(inState: .draining) - stateMachine.setResponseHeaders(["foo": "bar"]) + XCTAssertTrue(stateMachine.setResponseHeaders(["foo": "bar"])) stateMachine.sendMessage().assertInterceptHeadersThenMessage { headers in XCTAssertEqual(headers, ["foo": "bar"]) } @@ -235,8 +235,7 @@ internal final class ServerHandlerStateMachineTests: GRPCTestCase { func testSetResponseHeadersWhenFinished() { var stateMachine = self.makeStateMachine(inState: .finished) - stateMachine.setResponseHeaders(["foo": "bar"]) - // Nothing we can assert on, only that we don't crash. + XCTAssertFalse(stateMachine.setResponseHeaders(["foo": "bar"])) } func testSetResponseTrailersWhenHandling() { diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift index 7f2fb3502..647db7370 100644 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift @@ -35,12 +35,14 @@ class GRPCAsyncClientCallTests: GRPCTestCase { ("grpc-status", "0"), ]) - private func setUpServerAndChannel() throws -> ClientConnection { + private func setUpServerAndChannel( + service: CallHandlerProvider = EchoProvider() + ) throws -> ClientConnection { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) self.group = group let server = try Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) + .withServiceProviders([service]) .withLogger(self.serverLogger) .bind(host: "127.0.0.1", port: 0) .wait() @@ -204,6 +206,110 @@ class GRPCAsyncClientCallTests: GRPCTestCase { await assertThat(try await update.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) await assertThat(await update.status, .hasCode(.ok)) } + + func testExplicitAcceptUnary(twice: Bool, function: String = #function) async throws { + let headers: HPACKHeaders = ["fn": function] + let channel = try self.setUpServerAndChannel( + service: AsyncEchoProvider(headers: headers, sendTwice: twice) + ) + let echo = Echo_EchoAsyncClient(channel: channel) + let call = echo.makeGetCall(.with { $0.text = "" }) + let responseHeaders = try await call.initialMetadata + XCTAssertEqual(responseHeaders.first(name: "fn"), function) + let status = await call.status + XCTAssertEqual(status.code, .ok) + } + + func testExplicitAcceptUnary() async throws { + try await self.testExplicitAcceptUnary(twice: false) + } + + func testExplicitAcceptTwiceUnary() async throws { + try await self.testExplicitAcceptUnary(twice: true) + } + + func testExplicitAcceptClientStreaming(twice: Bool, function: String = #function) async throws { + let headers: HPACKHeaders = ["fn": function] + let channel = try self.setUpServerAndChannel( + service: AsyncEchoProvider(headers: headers, sendTwice: twice) + ) + let echo = Echo_EchoAsyncClient(channel: channel) + let call = echo.makeCollectCall() + let responseHeaders = try await call.initialMetadata + XCTAssertEqual(responseHeaders.first(name: "fn"), function) + + // Close request stream; the response should be empty. + call.requestStream.finish() + let response = try await call.response + XCTAssertEqual(response.text, "") + + let status = await call.status + XCTAssertEqual(status.code, .ok) + } + + func testExplicitAcceptClientStreaming() async throws { + try await self.testExplicitAcceptClientStreaming(twice: false) + } + + func testExplicitAcceptTwiceClientStreaming() async throws { + try await self.testExplicitAcceptClientStreaming(twice: true) + } + + func testExplicitAcceptServerStreaming(twice: Bool, function: String = #function) async throws { + let headers: HPACKHeaders = ["fn": #function] + let channel = try self.setUpServerAndChannel( + service: AsyncEchoProvider(headers: headers, sendTwice: twice) + ) + let echo = Echo_EchoAsyncClient(channel: channel) + let call = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) + let responseHeaders = try await call.initialMetadata + XCTAssertEqual(responseHeaders.first(name: "fn"), #function) + + // Close request stream; the response should be empty. + let responses = try await call.responseStream.collect() + XCTAssertEqual(responses.count, 3) + + let status = await call.status + XCTAssertEqual(status.code, .ok) + } + + func testExplicitAcceptServerStreaming() async throws { + try await self.testExplicitAcceptServerStreaming(twice: false) + } + + func testExplicitAcceptTwiceServerStreaming() async throws { + try await self.testExplicitAcceptServerStreaming(twice: true) + } + + func testExplicitAcceptBidirectionalStreaming( + twice: Bool, + function: String = #function + ) async throws { + let headers: HPACKHeaders = ["fn": function] + let channel = try self.setUpServerAndChannel( + service: AsyncEchoProvider(headers: headers, sendTwice: twice) + ) + let echo = Echo_EchoAsyncClient(channel: channel) + let call = echo.makeUpdateCall() + let responseHeaders = try await call.initialMetadata + XCTAssertEqual(responseHeaders.first(name: "fn"), function) + + // Close request stream; there should be no responses. + call.requestStream.finish() + let responses = try await call.responseStream.collect() + XCTAssertEqual(responses.count, 0) + + let status = await call.status + XCTAssertEqual(status.code, .ok) + } + + func testExplicitAcceptBidirectionalStreaming() async throws { + try await self.testExplicitAcceptBidirectionalStreaming(twice: false) + } + + func testExplicitAcceptTwiceBidirectionalStreaming() async throws { + try await self.testExplicitAcceptBidirectionalStreaming(twice: true) + } } // Workaround https://bugs.swift.org/browse/SR-15070 (compiler crashes when defining a class/actor @@ -221,3 +327,63 @@ private actor RequestResponseCounter { self.numRequests += 1 } } + +private final class AsyncEchoProvider: Echo_EchoAsyncProvider { + let headers: HPACKHeaders + let sendTwice: Bool + + init(headers: HPACKHeaders, sendTwice: Bool = false) { + self.headers = headers + self.sendTwice = sendTwice + } + + private func accept(context: GRPCAsyncServerCallContext) async { + await context.acceptRPC(headers: self.headers) + if self.sendTwice { + await context.acceptRPC(headers: self.headers) // Should be a no-op. + } + } + + func get( + request: Echo_EchoRequest, + context: GRPCAsyncServerCallContext + ) async throws -> Echo_EchoResponse { + await self.accept(context: context) + return Echo_EchoResponse.with { $0.text = request.text } + } + + func expand( + request: Echo_EchoRequest, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + await self.accept(context: context) + for part in request.text.components(separatedBy: " ") { + let response = Echo_EchoResponse.with { + $0.text = part + } + try await responseStream.send(response) + } + } + + func collect( + requestStream: GRPCAsyncRequestStream, + context: GRPCAsyncServerCallContext + ) async throws -> Echo_EchoResponse { + await self.accept(context: context) + let collected = try await requestStream.map { $0.text }.collect().joined(separator: " ") + return Echo_EchoResponse.with { $0.text = collected } + } + + func update( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + await self.accept(context: context) + for try await request in requestStream { + let response = Echo_EchoResponse.with { $0.text = request.text } + try await responseStream.send(response) + } + } +} From fcef85f9b8bc9d17706fcae1cb552dbaa36beaf8 Mon Sep 17 00:00:00 2001 From: Sergey Mikhanov <135028735+smikhanov@users.noreply.github.com> Date: Wed, 31 May 2023 14:26:48 +0100 Subject: [PATCH 095/580] Emit 'swift-format-ignore-file' in generated files to avoid having them reformatted by swift-format (#1606) --- .../Google/NaturalLanguage/Sources/language_service.grpc.swift | 1 + Sources/Examples/Echo/Model/echo.grpc.swift | 1 + Sources/Examples/HelloWorld/Model/helloworld.grpc.swift | 1 + Sources/Examples/RouteGuide/Model/route_guide.grpc.swift | 1 + .../GRPCInteroperabilityTestModels/Generated/test.grpc.swift | 1 + Sources/protoc-gen-grpc-swift/Generator.swift | 1 + Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift | 1 + dev/codegen-tests/01-echo/golden/echo.grpc.swift | 1 + dev/codegen-tests/02-multifile/golden/a.grpc.swift | 1 + dev/codegen-tests/02-multifile/golden/b.grpc.swift | 1 + .../03-multifile-with-module-map/golden/a.grpc.swift | 1 + .../03-multifile-with-module-map/golden/b.grpc.swift | 1 + .../04-service-with-message-import/golden/service.grpc.swift | 1 + dev/codegen-tests/05-service-only/golden/test.grpc.swift | 1 + dev/codegen-tests/06-test-client-only/golden/test.grpc.swift | 1 + scripts/license-check.sh | 2 +- 16 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift b/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift index ab4920f52..94f40ce63 100644 --- a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift +++ b/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: google/cloud/language/v1/language_service.proto diff --git a/Sources/Examples/Echo/Model/echo.grpc.swift b/Sources/Examples/Echo/Model/echo.grpc.swift index 033d639e4..5eca37d34 100644 --- a/Sources/Examples/Echo/Model/echo.grpc.swift +++ b/Sources/Examples/Echo/Model/echo.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: echo.proto diff --git a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift index 08c826be9..ca8d34c46 100644 --- a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift +++ b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: helloworld.proto diff --git a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift index 807beb875..e7c5a579f 100644 --- a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift +++ b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: route_guide.proto diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift index 34dc74462..837255066 100644 --- a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift +++ b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: src/proto/grpc/testing/test.proto diff --git a/Sources/protoc-gen-grpc-swift/Generator.swift b/Sources/protoc-gen-grpc-swift/Generator.swift index 76589a2f6..db5941d5f 100644 --- a/Sources/protoc-gen-grpc-swift/Generator.swift +++ b/Sources/protoc-gen-grpc-swift/Generator.swift @@ -111,6 +111,7 @@ class Generator { self.printer.print(""" // // DO NOT EDIT. + // swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: \(self.file.name) diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift index 79658cb9e..daeca7a26 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: normalization.proto diff --git a/dev/codegen-tests/01-echo/golden/echo.grpc.swift b/dev/codegen-tests/01-echo/golden/echo.grpc.swift index 82a564601..4b97c4075 100644 --- a/dev/codegen-tests/01-echo/golden/echo.grpc.swift +++ b/dev/codegen-tests/01-echo/golden/echo.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: echo.proto diff --git a/dev/codegen-tests/02-multifile/golden/a.grpc.swift b/dev/codegen-tests/02-multifile/golden/a.grpc.swift index 299a646d8..1e8d3ee51 100644 --- a/dev/codegen-tests/02-multifile/golden/a.grpc.swift +++ b/dev/codegen-tests/02-multifile/golden/a.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: a.proto diff --git a/dev/codegen-tests/02-multifile/golden/b.grpc.swift b/dev/codegen-tests/02-multifile/golden/b.grpc.swift index 6b1bd0022..b790d7dd6 100644 --- a/dev/codegen-tests/02-multifile/golden/b.grpc.swift +++ b/dev/codegen-tests/02-multifile/golden/b.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: b.proto diff --git a/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift b/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift index 7c2735564..292f990c1 100644 --- a/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift +++ b/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: a.proto diff --git a/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift b/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift index 6b1bd0022..b790d7dd6 100644 --- a/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift +++ b/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: b.proto diff --git a/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift b/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift index 881d057c3..622f845a2 100644 --- a/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift +++ b/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: service.proto diff --git a/dev/codegen-tests/05-service-only/golden/test.grpc.swift b/dev/codegen-tests/05-service-only/golden/test.grpc.swift index 98943be8a..91c625e28 100644 --- a/dev/codegen-tests/05-service-only/golden/test.grpc.swift +++ b/dev/codegen-tests/05-service-only/golden/test.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: test.proto diff --git a/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift b/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift index 5721459a1..3e79b9163 100644 --- a/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift +++ b/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift @@ -1,5 +1,6 @@ // // DO NOT EDIT. +// swift-format-ignore-file // // Generated by the protocol buffer compiler. // Source: test.proto diff --git a/scripts/license-check.sh b/scripts/license-check.sh index 55fec544a..b329de481 100755 --- a/scripts/license-check.sh +++ b/scripts/license-check.sh @@ -97,7 +97,7 @@ check_copyright_headers() { # Package.swift is preceeded by a "swift-tools-version" line. *.grpc.swift) expected_sha="$SWIFT_GRPC_SHA" - drop_first=8 + drop_first=9 expected_lines=13 ;; *.pb.swift) From dbd94fad62e6409a1f225dc07623cf9eaa2e262b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 2 Jun 2023 13:33:12 +0100 Subject: [PATCH 096/580] Update allocation counts (#1609) Motivation: NIO 2.54.0 added allocations in EmbeddedChannel; our limits our now out of whack. Modifications: Update allocation limits. Result: CI passes. --- .github/workflows/ci.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2d31e40b2..7edf580e9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -59,9 +59,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 @@ -69,9 +69,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 @@ -79,9 +79,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 @@ -89,9 +89,9 @@ jobs: env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 247000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 139000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 109000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 64000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 60000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 130000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 137000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 137000 From 76d4ec1dff8cfbd5f7363b98a4a9cdb26fb6c6c3 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 2 Jun 2023 13:46:53 +0100 Subject: [PATCH 097/580] Increase cancellation support for async calls (#1608) Motivation: The async client calls have limiteed support for cancellation: they support it for the "wrapped" calls and request/response streams but not for metadata/status on the lower level call objects. Modifications: - Add support for Task cancellation on the async call types Result: Better cancellation support --- .../GRPCAsyncBidirectionalStreamingCall.swift | 18 ++- .../GRPCAsyncClientStreamingCall.swift | 22 +++- .../GRPCAsyncServerStreamingCall.swift | 18 ++- .../GRPCAsyncUnaryCall.swift | 22 +++- .../AsyncAwaitSupport/AsyncClientTests.swift | 122 ++++++++++++++++++ .../AsyncAwaitSupport/XCTest+AsyncAwait.swift | 13 ++ .../InterceptedRPCCancellationTests.swift | 2 +- 7 files changed, 202 insertions(+), 15 deletions(-) diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 690e113f1..271da849e 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -52,6 +52,12 @@ public struct GRPCAsyncBidirectionalStreamingCall(_ fn: () async throws -> R) async rethrows -> R { + return try await withTaskCancellationHandler(operation: fn) { + self.cancel() + } + } + /// The initial metadata returned from the server. /// /// - Important: The initial metadata will only be available when the first response has been @@ -59,7 +65,9 @@ public struct GRPCAsyncBidirectionalStreamingCall(_ fn: () async throws -> R) async rethrows -> R { + return try await withTaskCancellationHandler(operation: fn) { + self.cancel() + } + } + /// The initial metadata returned from the server. /// /// - Important: The initial metadata will only be available when the response has been received. public var initialMetadata: HPACKHeaders { get async throws { - try await self.responseParts.initialMetadata.get() + return try await self.withRPCCancellation { + try await self.responseParts.initialMetadata.get() + } } } /// The response returned by the server. public var response: Response { get async throws { - try await self.responseParts.response.get() + return try await self.withRPCCancellation { + try await self.responseParts.response.get() + } } } @@ -64,7 +74,9 @@ public struct GRPCAsyncClientStreamingCall(_ fn: () async throws -> R) async rethrows -> R { + return try await withTaskCancellationHandler(operation: fn) { + self.cancel() + } + } + /// The initial metadata returned from the server. /// /// - Important: The initial metadata will only be available when the first response has been @@ -56,7 +62,9 @@ public struct GRPCAsyncServerStreamingCall: Sendabl // MARK: - Response Parts + private func withRPCCancellation(_ fn: () async throws -> R) async rethrows -> R { + return try await withTaskCancellationHandler(operation: fn) { + self.cancel() + } + } + /// The initial metadata returned from the server. /// /// - Important: The initial metadata will only be available when the response has been received. public var initialMetadata: HPACKHeaders { get async throws { - try await self.responseParts.initialMetadata.get() + try await self.withRPCCancellation { + try await self.responseParts.initialMetadata.get() + } } } @@ -56,7 +64,9 @@ public struct GRPCAsyncUnaryCall: Sendabl /// Callers should rely on the `status` of the call for the canonical outcome. public var response: Response { get async throws { - try await self.responseParts.response.get() + try await self.withRPCCancellation { + try await self.responseParts.response.get() + } } } @@ -65,7 +75,9 @@ public struct GRPCAsyncUnaryCall: Sendabl /// - Important: Awaiting this property will suspend until the responses have been consumed. public var trailingMetadata: HPACKHeaders { get async throws { - try await self.responseParts.trailingMetadata.get() + try await self.withRPCCancellation { + try await self.responseParts.trailingMetadata.get() + } } } @@ -75,7 +87,9 @@ public struct GRPCAsyncUnaryCall: Sendabl public var status: GRPCStatus { get async { // force-try acceptable because any error is encapsulated in a successful GRPCStatus future. - try! await self.responseParts.status.get() + await self.withRPCCancellation { + try! await self.responseParts.status.get() + } } } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift index 06ff6ff84..0a6378030 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift @@ -414,4 +414,126 @@ final class AsyncClientCancellationTests: GRPCTestCase { XCTAssertFalse(error is CancellationError) } } + + func testCancelUnary() async throws { + // We don't want the RPC to complete before we cancel it so use the never resolving service. + let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) + + do { + let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) + let task = Task { try await get.initialMetadata } + task.cancel() + await XCTAssertThrowsError(try await task.value) + } + + do { + let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) + let task = Task { try await get.response } + task.cancel() + await XCTAssertThrowsError(try await task.value) + } + + do { + let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) + let task = Task { try await get.trailingMetadata } + task.cancel() + await XCTAssertNoThrowAsync(try await task.value) + } + + do { + let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) + let task = Task { await get.status } + task.cancel() + let status = await task.value + XCTAssertEqual(status.code, .cancelled) + } + } + + func testCancelClientStreaming() async throws { + // We don't want the RPC to complete before we cancel it so use the never resolving service. + let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) + + do { + let collect = echo.makeCollectCall() + let task = Task { try await collect.initialMetadata } + task.cancel() + await XCTAssertThrowsError(try await task.value) + } + + do { + let collect = echo.makeCollectCall() + let task = Task { try await collect.response } + task.cancel() + await XCTAssertThrowsError(try await task.value) + } + + do { + let collect = echo.makeCollectCall() + let task = Task { try await collect.trailingMetadata } + task.cancel() + await XCTAssertNoThrowAsync(try await task.value) + } + + do { + let collect = echo.makeCollectCall() + let task = Task { await collect.status } + task.cancel() + let status = await task.value + XCTAssertEqual(status.code, .cancelled) + } + } + + func testCancelServerStreaming() async throws { + // We don't want the RPC to complete before we cancel it so use the never resolving service. + let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) + + do { + let expand = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) + let task = Task { try await expand.initialMetadata } + task.cancel() + await XCTAssertThrowsError(try await task.value) + } + + do { + let expand = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) + let task = Task { try await expand.trailingMetadata } + task.cancel() + await XCTAssertNoThrowAsync(try await task.value) + } + + do { + let expand = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) + let task = Task { await expand.status } + task.cancel() + let status = await task.value + XCTAssertEqual(status.code, .cancelled) + } + } + + func testCancelBidirectionalStreaming() async throws { + // We don't want the RPC to complete before we cancel it so use the never resolving service. + let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) + + do { + let update = echo.makeUpdateCall() + let task = Task { try await update.initialMetadata } + task.cancel() + await XCTAssertThrowsError(try await task.value) + } + + do { + let update = echo.makeUpdateCall() + let task = Task { try await update.trailingMetadata } + task.cancel() + await XCTAssertNoThrowAsync(try await task.value) + } + + do { + let update = echo.makeUpdateCall() + let task = Task { await update.status } + task.cancel() + let status = await task.value + XCTAssertEqual(status.code, .cancelled) + } + } } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift index 7e8ff10ca..fb56aa32b 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift @@ -30,6 +30,19 @@ internal func XCTAssertThrowsError( } } +@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) +internal func XCTAssertNoThrowAsync( + _ expression: @autoclosure () async throws -> T, + file: StaticString = #filePath, + line: UInt = #line +) async { + do { + _ = try await expression() + } catch { + XCTFail("Expression throw error '\(error)'", file: file, line: line) + } +} + private enum TaskResult { case operation(Result) case cancellation diff --git a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift index 4edd204d7..9676366a1 100644 --- a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift +++ b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift @@ -33,7 +33,7 @@ final class InterceptedRPCCancellationTests: GRPCTestCase { } // Interceptor checks that a "magic" header is present. - let serverInterceptors = EchoServerInterceptors(MagicRequiredServerInterceptor.init) + let serverInterceptors = EchoServerInterceptors({ MagicRequiredServerInterceptor() }) let server = try Server.insecure(group: group) .withLogger(self.serverLogger) .withServiceProviders([EchoProvider(interceptors: serverInterceptors)]) From 038c22f024c7723696f9f8e98bd734fab2d2963d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 5 Jun 2023 13:23:14 +0100 Subject: [PATCH 098/580] Handle non-200 status codes more gracefully (#1613) Motivation: Accepted RPCs should have a 200 HTTP response status code. gRPC knows how to handle some non-200 status codes and can synthesize a gRPC status from them. At the moment they are treated as errors and later converted to a gRPC status. However this means that only the gRPC status sees the error as a status: other response parts (e.g. the response stream) see an error relating to invalid HTTP response codes rather than the synthesized status. Modifications: - Handle non-200 HTTP status codes more gracefully by turning them into a gRPC status where possible. Results: - Non-200 status codes are more readily converted to a gRPC status. --- Sources/GRPC/GRPCClientChannelHandler.swift | 2 - Sources/GRPC/GRPCClientStateMachine.swift | 28 +++---- Sources/GRPC/GRPCError.swift | 24 ++++-- .../GRPCClientStateMachineTests.swift | 14 +--- Tests/GRPCTests/GRPCStatusCodeTests.swift | 74 +++++++++++++++++-- 5 files changed, 101 insertions(+), 41 deletions(-) diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index 3ebe041d2..15b2c3d17 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -374,8 +374,6 @@ extension GRPCClientChannelHandler: ChannelInboundHandler { return GRPCError.InvalidContentType(contentType).captureContext() case let .invalidHTTPStatus(status): return GRPCError.InvalidHTTPStatus(status).captureContext() - case let .invalidHTTPStatusWithGRPCStatus(status): - return GRPCError.InvalidHTTPStatusWithGRPCStatus(status).captureContext() case .invalidState: return GRPCError.InvalidState("parsing end-of-stream trailers").captureContext() } diff --git a/Sources/GRPC/GRPCClientStateMachine.swift b/Sources/GRPC/GRPCClientStateMachine.swift index 3d79ebd5a..b8a4c0a7b 100644 --- a/Sources/GRPC/GRPCClientStateMachine.swift +++ b/Sources/GRPC/GRPCClientStateMachine.swift @@ -41,10 +41,6 @@ enum ReceiveEndOfResponseStreamError: Error, Equatable { /// The HTTP response status from the server was not 200 OK. case invalidHTTPStatus(String?) - /// The HTTP response status from the server was not 200 OK but the "grpc-status" header contained - /// a valid value. - case invalidHTTPStatusWithGRPCStatus(GRPCStatus) - /// An invalid state was encountered. This is a serious implementation error. case invalidState } @@ -742,7 +738,7 @@ extension GRPCClientStateMachine.State { private func readStatusCode(from trailers: HPACKHeaders) -> GRPCStatus.Code? { return trailers.first(name: GRPCHeaderName.statusCode) .flatMap(Int.init) - .flatMap(GRPCStatus.Code.init) + .flatMap({ GRPCStatus.Code(rawValue: $0) }) } private func readStatusMessage(from trailers: HPACKHeaders) -> String? { @@ -764,18 +760,22 @@ extension GRPCClientStateMachine.State { // // See: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md let statusHeader = trailers.first(name: ":status") - guard let status = statusHeader.flatMap(Int.init).map({ HTTPResponseStatus(statusCode: $0) }) - else { + let httpResponseStatus = statusHeader.flatMap(Int.init).map { + HTTPResponseStatus(statusCode: $0) + } + + guard let httpResponseStatus = httpResponseStatus else { return .failure(.invalidHTTPStatus(statusHeader)) } - guard status == .ok else { - if let code = self.readStatusCode(from: trailers) { - let message = self.readStatusMessage(from: trailers) - return .failure(.invalidHTTPStatusWithGRPCStatus(.init(code: code, message: message))) - } else { - return .failure(.invalidHTTPStatus(statusHeader)) - } + guard httpResponseStatus == .ok else { + // Non-200 response. If there's a 'grpc-status' message we should use that otherwise try + // to create one from the HTTP status code. + let grpcStatusCode = self.readStatusCode(from: trailers) + ?? GRPCStatus.Code(httpStatus: Int(httpResponseStatus.code)) + ?? .unknown + let message = self.readStatusMessage(from: trailers) + return .success(GRPCStatus(code: grpcStatusCode, message: message)) } // Only validate the content-type header if it's present. This is a small deviation from the diff --git a/Sources/GRPC/GRPCError.swift b/Sources/GRPC/GRPCError.swift index 99594ec90..5daec8962 100644 --- a/Sources/GRPC/GRPCError.swift +++ b/Sources/GRPC/GRPCError.swift @@ -229,7 +229,7 @@ public enum GRPCError { public func makeGRPCStatus() -> GRPCStatus { return GRPCStatus( - code: .init(httpStatus: self.status), + code: .init(httpStatus: self.status) ?? .unknown, message: self.description, cause: self ) @@ -342,21 +342,29 @@ extension GRPCErrorProtocol { extension GRPCStatus.Code { /// The gRPC status code associated with the given HTTP status code. This should only be used if /// the RPC did not return a 'grpc-status' trailer. - internal init(httpStatus: String?) { + internal init?(httpStatus codeString: String?) { + if let code = codeString.flatMap(Int.init) { + self.init(httpStatus: code) + } else { + return nil + } + } + + internal init?(httpStatus: Int) { /// See: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md switch httpStatus { - case "400": + case 400: self = .internalError - case "401": + case 401: self = .unauthenticated - case "403": + case 403: self = .permissionDenied - case "404": + case 404: self = .unimplemented - case "429", "502", "503", "504": + case 429, 502, 503, 504: self = .unavailable default: - self = .unknown + return nil } } } diff --git a/Tests/GRPCTests/GRPCClientStateMachineTests.swift b/Tests/GRPCTests/GRPCClientStateMachineTests.swift index 04bd643ff..cb03c23d1 100644 --- a/Tests/GRPCTests/GRPCClientStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCClientStateMachineTests.swift @@ -1097,14 +1097,8 @@ extension GRPCClientStateMachineTests { ":status": "418", "grpc-status": "5", ] - stateMachine.receiveEndOfResponseStream(trailers).assertFailure { error in - XCTAssertEqual( - error, - .invalidHTTPStatusWithGRPCStatus(GRPCStatus( - code: GRPCStatus.Code(rawValue: 5)!, - message: nil - )) - ) + stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in + XCTAssertEqual(status.code.rawValue, 5) } } @@ -1115,8 +1109,8 @@ extension GRPCClientStateMachineTests { )) let trailers: HPACKHeaders = [":status": "418"] - stateMachine.receiveEndOfResponseStream(trailers).assertFailure { error in - XCTAssertEqual(error, .invalidHTTPStatus("418")) + stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in + XCTAssertEqual(status.code, .unknown) } } } diff --git a/Tests/GRPCTests/GRPCStatusCodeTests.swift b/Tests/GRPCTests/GRPCStatusCodeTests.swift index 1eabe16ba..b2d97fd33 100644 --- a/Tests/GRPCTests/GRPCStatusCodeTests.swift +++ b/Tests/GRPCTests/GRPCStatusCodeTests.swift @@ -117,14 +117,74 @@ class GRPCStatusCodeTests: GRPCTestCase { self.sendRequestHead() let headerFramePayload = HTTP2Frame.FramePayload.headers(.init(headers: headers)) - XCTAssertThrowsError(try self.channel.writeInbound(headerFramePayload)) { error in - guard let withContext = error as? GRPCError.WithContext, - let invalidHTTPStatus = withContext.error as? GRPCError.InvalidHTTPStatusWithGRPCStatus - else { - XCTFail("Unexpected error: \(error)") - return + try self.channel.writeInbound(headerFramePayload) + + let responsePart1 = try XCTUnwrap( + self.channel.readInbound(as: _GRPCClientResponsePart.self) + ) + + switch responsePart1 { + case .trailingMetadata: + () + case .initialMetadata, .message, .status: + XCTFail("Unexpected response part \(responsePart1)") + } + + let responsePart2 = try XCTUnwrap( + self.channel.readInbound(as: _GRPCClientResponsePart.self) + ) + + switch responsePart2 { + case .initialMetadata, .message, .trailingMetadata: + XCTFail("Unexpected response part \(responsePart2)") + case let .status(actual): + XCTAssertEqual(actual.code, status.code) + XCTAssertEqual(actual.message, status.message) + } + } + + func testNon200StatusCodesAreConverted() throws { + let tests: [(Int, GRPCStatus.Code)] = [ + (400, .internalError), + (401, .unauthenticated), + (403, .permissionDenied), + (404, .unimplemented), + (429, .unavailable), + (502, .unavailable), + (503, .unavailable), + (504, .unavailable), + ] + + for (httpStatusCode, grpcStatusCode) in tests { + let headers: HPACKHeaders = [":status": "\(httpStatusCode)"] + + self.setUp() + self.sendRequestHead() + let headerFramePayload = HTTP2Frame.FramePayload + .headers(.init(headers: headers, endStream: true)) + try self.channel.writeInbound(headerFramePayload) + + let responsePart1 = try XCTUnwrap( + self.channel.readInbound(as: _GRPCClientResponsePart.self) + ) + + switch responsePart1 { + case .trailingMetadata: + () + case .initialMetadata, .message, .status: + XCTFail("Unexpected response part \(responsePart1)") + } + + let responsePart2 = try XCTUnwrap( + self.channel.readInbound(as: _GRPCClientResponsePart.self) + ) + + switch responsePart2 { + case .initialMetadata, .message, .trailingMetadata: + XCTFail("Unexpected response part \(responsePart2)") + case let .status(actual): + XCTAssertEqual(actual.code, grpcStatusCode) } - XCTAssertEqual(invalidHTTPStatus.makeGRPCStatus(), status) } } From 9f4e2e4fd0294fe83372a2b80426a883d5617d2b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 6 Jun 2023 14:40:32 +0100 Subject: [PATCH 099/580] Bump version number to 1.17.0 (#1614) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.17.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index b0207a555..9430db5df 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 16 + internal static let minor = 17 /// The patch version. internal static let patch = 0 From 8a70336d129ee7f6d982fd43aafb27885c654c4b Mon Sep 17 00:00:00 2001 From: Kristopher Wuollett Date: Wed, 7 Jun 2023 12:30:41 -0500 Subject: [PATCH 100/580] Remove the gRPC Authors copyright header from generated swift files (#1612) Motivation: The common practice of code generators is not to apply their own copyright and license headers to generated files. Instead the output is owned by the owner of the input files and the executor of the code generator. Fixes #1611. Modifications: Removed license header from file generator, test case files, and checks for generated file license headers in the CI sanity license-check. Results: End users of protoc-gen-grpc-swift will not have a Copyright by gRPC Authors added to the generated swift files from their own proto files. Related: The copyright headers could be added back via #1610. --- .../Sources/language_service.grpc.swift | 16 ------- Sources/Examples/Echo/Model/echo.grpc.swift | 16 ------- .../HelloWorld/Model/helloworld.grpc.swift | 16 ------- .../RouteGuide/Model/route_guide.grpc.swift | 16 ------- .../Generated/test.grpc.swift | 16 ------- Sources/protoc-gen-grpc-swift/Generator.swift | 16 ------- .../Normalization/normalization.grpc.swift | 16 ------- .../01-echo/golden/echo.grpc.swift | 16 ------- .../02-multifile/golden/a.grpc.swift | 16 ------- .../02-multifile/golden/b.grpc.swift | 16 ------- .../golden/a.grpc.swift | 16 ------- .../golden/b.grpc.swift | 16 ------- .../golden/service.grpc.swift | 16 ------- .../05-service-only/golden/test.grpc.swift | 16 ------- .../golden/test.grpc.swift | 16 ------- scripts/license-check.sh | 42 +------------------ 16 files changed, 2 insertions(+), 280 deletions(-) diff --git a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift b/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift index 94f40ce63..75eb20105 100644 --- a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift +++ b/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: google/cloud/language/v1/language_service.proto // - -// -// Copyright 2018, 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 GRPC import NIO import NIOConcurrencyHelpers diff --git a/Sources/Examples/Echo/Model/echo.grpc.swift b/Sources/Examples/Echo/Model/echo.grpc.swift index 5eca37d34..6763b31cc 100644 --- a/Sources/Examples/Echo/Model/echo.grpc.swift +++ b/Sources/Examples/Echo/Model/echo.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: echo.proto // - -// -// Copyright 2018, 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 GRPC import NIO import NIOConcurrencyHelpers diff --git a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift index ca8d34c46..014226e95 100644 --- a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift +++ b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: helloworld.proto // - -// -// Copyright 2018, 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 GRPC import NIO import NIOConcurrencyHelpers diff --git a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift index e7c5a579f..71b372d35 100644 --- a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift +++ b/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: route_guide.proto // - -// -// Copyright 2018, 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 GRPC import NIO import NIOConcurrencyHelpers diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift index 837255066..6f0522adc 100644 --- a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift +++ b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: src/proto/grpc/testing/test.proto // - -// -// Copyright 2018, 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 GRPC import NIO import NIOConcurrencyHelpers diff --git a/Sources/protoc-gen-grpc-swift/Generator.swift b/Sources/protoc-gen-grpc-swift/Generator.swift index db5941d5f..9c0eed253 100644 --- a/Sources/protoc-gen-grpc-swift/Generator.swift +++ b/Sources/protoc-gen-grpc-swift/Generator.swift @@ -115,22 +115,6 @@ class Generator { // // Generated by the protocol buffer compiler. // Source: \(self.file.name) - // - - // - // Copyright 2018, 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. //\n """) diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift index daeca7a26..48cd11100 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: normalization.proto // - -// -// Copyright 2018, 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 GRPC import NIO import NIOConcurrencyHelpers diff --git a/dev/codegen-tests/01-echo/golden/echo.grpc.swift b/dev/codegen-tests/01-echo/golden/echo.grpc.swift index 4b97c4075..8b509dcb9 100644 --- a/dev/codegen-tests/01-echo/golden/echo.grpc.swift +++ b/dev/codegen-tests/01-echo/golden/echo.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: echo.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/dev/codegen-tests/02-multifile/golden/a.grpc.swift b/dev/codegen-tests/02-multifile/golden/a.grpc.swift index 1e8d3ee51..9856ae1b7 100644 --- a/dev/codegen-tests/02-multifile/golden/a.grpc.swift +++ b/dev/codegen-tests/02-multifile/golden/a.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: a.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/dev/codegen-tests/02-multifile/golden/b.grpc.swift b/dev/codegen-tests/02-multifile/golden/b.grpc.swift index b790d7dd6..b9f8590f5 100644 --- a/dev/codegen-tests/02-multifile/golden/b.grpc.swift +++ b/dev/codegen-tests/02-multifile/golden/b.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: b.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift b/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift index 292f990c1..be25e087f 100644 --- a/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift +++ b/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: a.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift b/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift index b790d7dd6..b9f8590f5 100644 --- a/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift +++ b/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: b.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift b/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift index 622f845a2..fda36f0ac 100644 --- a/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift +++ b/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: service.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/dev/codegen-tests/05-service-only/golden/test.grpc.swift b/dev/codegen-tests/05-service-only/golden/test.grpc.swift index 91c625e28..2d4a36484 100644 --- a/dev/codegen-tests/05-service-only/golden/test.grpc.swift +++ b/dev/codegen-tests/05-service-only/golden/test.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: test.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift b/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift index 3e79b9163..196bb33a4 100644 --- a/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift +++ b/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift @@ -5,22 +5,6 @@ // Generated by the protocol buffer compiler. // Source: test.proto // - -// -// Copyright 2018, 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 GRPC import NIO import SwiftProtobuf diff --git a/scripts/license-check.sh b/scripts/license-check.sh index b329de481..496197ce9 100755 --- a/scripts/license-check.sh +++ b/scripts/license-check.sh @@ -38,42 +38,6 @@ read -r -d '' COPYRIGHT_HEADER_SWIFT << 'EOF' EOF SWIFT_SHA=$(echo "$COPYRIGHT_HEADER_SWIFT" | shasum | awk '{print $1}') -# Copyright header text and SHA for *.grpc.swift files -read -r -d '' COPYRIGHT_HEADER_SWIFT_GRPC << 'EOF' -// Copyright YEARS, 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. -EOF -SWIFT_GRPC_SHA=$(echo "$COPYRIGHT_HEADER_SWIFT_GRPC" | shasum | awk '{print $1}') - -# Copyright header text and SHA for *.pb.swift files -read -r -d '' COPYRIGHT_HEADER_SWIFT_PB << 'EOF' -// Copyright YEARS gRPC authors. -// -// 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. -EOF -SWIFT_GRPC_PB=$(echo "$COPYRIGHT_HEADER_SWIFT_PB" | shasum | awk '{print $1}') - replace_years() { sed -e 's/201[56789]-20[12][0-9]/YEARS/' -e 's/201[56789]/YEARS/' } @@ -134,10 +98,8 @@ check_copyright_headers() { fi done < <(find . -name '*.swift' \ - ! -name 'echo.pb.swift' \ - ! -name 'annotations.pb.swift' \ - ! -name 'language_service.pb.swift' \ - ! -name 'http.pb.swift' \ + ! -name '*.pb.swift' \ + ! -name '*.grpc.swift' \ ! -name 'LinuxMain.swift' \ ! -name 'XCTestManifests.swift' \ ! -path './.build/*') From 2453b9067662ed5ad884984b62f3d0726d8b3684 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 16 Jun 2023 10:53:26 +0100 Subject: [PATCH 101/580] Relax AsyncSequence.collect availability check (#1616) Motivation: Tests currently fail with: > grpc-swift/Tests/GRPCTests/GRPCAsyncClientCallTests.swift:374:61: error: 'collect()' is only available in macOS 12 or newer Modifications: Relax the availability check on the `AsyncSequence` extension. It seems to arise from the constraint on `AsyncSequence.reduce(into: Result)` which only requires 10.15, not 12. Result: Tests should pass. --- Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift index cadad82ce..6e596d758 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension AsyncSequence { internal func collect() async throws -> [Element] { return try await self.reduce(into: []) { accumulated, next in From 5eb37db61ccc32f2f08b89045f0d584cfb058b31 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 29 Jun 2023 17:58:10 +0100 Subject: [PATCH 102/580] Make Server Sendable (#1623) --- Sources/GRPC/Server.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 1d1c89c7c..d54c3b414 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -79,7 +79,9 @@ import Network /// HTTP2Frame.FramePayload│ │HTTP2Frame.FramePayload /// │ ▼ /// -public final class Server { +///- Note: This class is thread safe. It's marked as `@unchecked Sendable` because the non-Sendable +/// `errorDelegate` property is mutated, but it's done thread-safely, as it only happens inside the `EventLoop`. +public final class Server: @unchecked Sendable { /// Makes and configures a `ServerBootstrap` using the provided configuration. public class func makeBootstrap(configuration: Configuration) -> ServerBootstrapProtocol { let bootstrap = PlatformSupport.makeServerBootstrap(group: configuration.eventLoopGroup) From bbcc0d93611fa906eb61feef0b0dc1a8d4bfe77c Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 30 Jun 2023 09:27:25 +0100 Subject: [PATCH 103/580] Fix comment formatting issue (#1624) --- Sources/GRPC/Server.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index d54c3b414..7b16c9232 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -79,7 +79,7 @@ import Network /// HTTP2Frame.FramePayload│ │HTTP2Frame.FramePayload /// │ ▼ /// -///- Note: This class is thread safe. It's marked as `@unchecked Sendable` because the non-Sendable +/// - Note: This class is thread safe. It's marked as `@unchecked Sendable` because the non-Sendable /// `errorDelegate` property is mutated, but it's done thread-safely, as it only happens inside the `EventLoop`. public final class Server: @unchecked Sendable { /// Makes and configures a `ServerBootstrap` using the provided configuration. From 473f4668d7ae602d30229c2e88ad334a08f9cef4 Mon Sep 17 00:00:00 2001 From: Denil Chungath <95201442+denil-ct@users.noreply.github.com> Date: Tue, 4 Jul 2023 18:22:09 +0530 Subject: [PATCH 104/580] Add support for `XcodeProjectPlugin` (#1621) --- Plugins/GRPCSwiftPlugin/plugin.swift | 83 ++++++++++++++----- .../Docs.docc/spm-plugin.md | 39 ++++----- 2 files changed, 84 insertions(+), 38 deletions(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index ae766dd89..2a07ea21b 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -18,13 +18,15 @@ import Foundation import PackagePlugin @main -struct GRPCSwiftPlugin: BuildToolPlugin { +struct GRPCSwiftPlugin { /// Errors thrown by the `GRPCSwiftPlugin` enum PluginError: Error { /// Indicates that the target where the plugin was applied to was not `SourceModuleTarget`. case invalidTarget /// Indicates that the file extension of an input file was not `.proto`. case invalidInputFileExtension + /// Indicates that there was no configuration file at the required location. + case noConfigFound } /// The configuration of the plugin. @@ -69,20 +71,33 @@ struct GRPCSwiftPlugin: BuildToolPlugin { static let configurationFileName = "grpc-swift-config.json" - func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { - // Let's check that this is a source target - guard let target = target as? SourceModuleTarget else { - throw PluginError.invalidTarget + /// Create build commands for the given arguments + /// - Parameters: + /// - pluginWorkDirectory: The path of a writable directory into which the plugin or the build + /// commands it constructs can write anything it wants. + /// - sourceFiles: The input files that are associated with the target. + /// - tool: The tool method from the context. + /// - Returns: The build commands configured based on the arguments. + func createBuildCommands( + pluginWorkDirectory: PackagePlugin.Path, + sourceFiles: FileList, + tool: (String) throws -> PackagePlugin.PluginContext.Tool + ) throws -> [Command] { + guard let configurationFilePath = sourceFiles.first( + where: { + $0.path.lastComponent == Self.configurationFileName + } + )?.path else { + throw PluginError.noConfigFound } - // We need to find the configuration file at the root of the target - let configurationFilePath = target.directory.appending(subpath: Self.configurationFileName) let data = try Data(contentsOf: URL(fileURLWithPath: "\(configurationFilePath)")) let configuration = try JSONDecoder().decode(Configuration.self, from: data) try self.validateConfiguration(configuration) - var importPaths: [Path] = [target.directory] + let targetDirectory = configurationFilePath.removingLastComponent() + var importPaths: [Path] = [targetDirectory] if let configuredImportPaths = configuration.importPaths { importPaths.append(contentsOf: configuredImportPaths.map { Path($0) }) } @@ -96,20 +111,17 @@ struct GRPCSwiftPlugin: BuildToolPlugin { protocPath = Path(environmentPath) } else { // The user didn't set anything so let's try see if SPM can find a binary for us - protocPath = try context.tool(named: "protoc").path + protocPath = try tool("protoc").path } - let protocGenGRPCSwiftPath = try context.tool(named: "protoc-gen-grpc-swift").path - - // This plugin generates its output into GeneratedSources - let outputDirectory = context.pluginWorkDirectory + let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").path return configuration.invocations.map { invocation in self.invokeProtoc( - target: target, + directory: targetDirectory, invocation: invocation, protocPath: protocPath, protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, - outputDirectory: outputDirectory, + outputDirectory: pluginWorkDirectory, importPaths: importPaths ) } @@ -118,15 +130,15 @@ struct GRPCSwiftPlugin: BuildToolPlugin { /// Invokes `protoc` with the given inputs /// /// - Parameters: - /// - target: The plugin's target. + /// - directory: The plugin's target directory. /// - invocation: The `protoc` invocation. /// - protocPath: The path to the `protoc` binary. /// - protocGenSwiftPath: The path to the `protoc-gen-swift` binary. /// - outputDirectory: The output directory for the generated files. /// - importPaths: List of paths to pass with "-I " to `protoc` - /// - Returns: The build command. + /// - Returns: The build command configured based on the arguments private func invokeProtoc( - target: Target, + directory: Path, invocation: Configuration.Invocation, protocPath: Path, protocGenGRPCSwiftPath: Path, @@ -166,7 +178,7 @@ struct GRPCSwiftPlugin: BuildToolPlugin { for var file in invocation.protoFiles { // Append the file to the protoc args so that it is used for generating protocArgs.append("\(file)") - inputFiles.append(target.directory.appending(file)) + inputFiles.append(directory.appending(file)) // The name of the output file is based on the name of the input file. // We validated in the beginning that every file has the suffix of .proto @@ -202,3 +214,36 @@ struct GRPCSwiftPlugin: BuildToolPlugin { } } } + +extension GRPCSwiftPlugin: BuildToolPlugin { + func createBuildCommands( + context: PluginContext, + target: Target + ) async throws -> [Command] { + guard let swiftTarget = target as? SwiftSourceModuleTarget else { + throw PluginError.invalidTarget + } + return try self.createBuildCommands( + pluginWorkDirectory: context.pluginWorkDirectory, + sourceFiles: swiftTarget.sourceFiles, + tool: context.tool + ) + } +} + +#if canImport(XcodeProjectPlugin) +import XcodeProjectPlugin + +extension GRPCSwiftPlugin: XcodeBuildToolPlugin { + func createBuildCommands( + context: XcodePluginContext, + target: XcodeTarget + ) throws -> [Command] { + return try self.createBuildCommands( + pluginWorkDirectory: context.pluginWorkDirectory, + sourceFiles: target.inputFiles, + tool: context.tool + ) + } +} +#endif diff --git a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md index 23de42967..fb994630c 100644 --- a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md +++ b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md @@ -22,18 +22,8 @@ There are multiple ways to do this. Some of the easiest are: 1. If you are on macOS, installing it via `brew install protobuf` 2. Download the binary from [Google's github repository](https://github.com/protocolbuffers/protobuf). -### Adding the proto files to your target - -Next, you need to add the `.proto` files for which you want to generate your Swift types to your target's -source directory. You should also commit these files to your git repository since the generated types -are now generated on demand. - -> Note: imports on your `.proto` files will have to include the relative path from the target source to the `.proto` file you wish to import. -> Files **must** be contained within the target source directory. - ### Adding the plugin to your manifest -After adding the `.proto` files you can now add the plugin to the target inside your `Package.swift` manifest. First, you need to add a dependency on `grpc-swift`. Afterwards, you can declare the usage of the plugin for your target. Here is an example snippet of a `Package.swift` manifest: @@ -61,23 +51,33 @@ let package = Package( ### Configuring the plugin -Lastly, after you have added the `.proto` files and modified your `Package.swift` manifest, you can now -configure the plugin to invoke the `protoc` compiler. This is done by adding a `grpc-swift-config.json` -to the root of your target's source folder. An example configuration file looks like this: +Configuring the plugin is done by adding a `grpc-swift-config.json` file anywhere in your target's sources. Before you start configuring the plugin, you need to add the `.proto` files to your sources. You should also commit these files to your git repository since the generated types are now generated on demand. It's also important to note that the proto files in your configuration should be in the same directory as the config file. Let's see an example to have a better understanding. + +Here's an example file structure that looks like this: + +```text +Sources +├── main.swift +├── ProtoBuf + ├── grpc-swift-config.json + ├── foo.proto + └── Bar + └── Bar.proto +``` ```json { "invocations": [ { "protoFiles": [ - "Path/To/Foo.proto", + "Foo.proto", ], "visibility": "internal", "server": false }, { "protoFiles": [ - "Bar.proto" + "Bar/Bar.proto" ], "visibility": "public", "client": false, @@ -87,11 +87,12 @@ to the root of your target's source folder. An example configuration file looks } ``` -> Note: paths to your `.proto` files will have to include the relative path from the target source to the `.proto` file location. -> Files **must** be contained within the target source directory. +> Note: paths to your `.proto` files will have to include the relative path from the config file directory to the `.proto` file location. +> Files **must** be contained within the same directory as the config file. -In the above configuration, you declared two invocations to the `protoc` compiler. The first invocation -is generating Swift types for the `Foo.proto` file with `internal` visibility. Notice the relative path to the `.proto` file. +In the above configuration, notice the relative path of the `.proto` file with respect to the configuration file. If you add a file in the `Sources` folder, the plugin would be unable to access it as the path is computed relative to the `grpc-swift-config.json` file. So the `.proto` files have to be added within the `ProtoBuf` folder, with relative paths taken into consideration. +Here, you declared two invocations to the `protoc` compiler. The first invocation +is generating Swift types for the `Foo.proto` file with `internal` visibility. We have also specified the `server` option and set it to false: this means that server code won't be generated for this proto. The second invocation is generating Swift types for the `Bar.proto` file with the `public` visibility. Notice the `client` option: it's been set to false, so no client code will be generated for this proto. We have also set From 0970e57cd2c196cf31eec55417b76b30caca1f35 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 4 Jul 2023 14:00:41 +0100 Subject: [PATCH 105/580] Bump version to 1.18.0 (#1625) Co-authored-by: George Barnett --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 9430db5df..2973797d5 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 17 + internal static let minor = 18 /// The patch version. internal static let patch = 0 From eefe83d17a3fbcc0487ef6a98d50032508e718d0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 5 Jul 2023 14:03:08 +0100 Subject: [PATCH 106/580] Update expired test certs (#1627) --- .../GRPCSampleData/GRPCSwiftCertificate.swift | 380 +++++++++--------- Sources/GRPCSampleData/bundle.p12 | Bin 2405 -> 2405 bytes 2 files changed, 190 insertions(+), 190 deletions(-) diff --git a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift index 3294ae96f..2abcc8f8f 100644 --- a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift +++ b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift @@ -1,5 +1,5 @@ /* - * Copyright 2022, gRPC Authors All rights reserved. + * Copyright 2023, 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. @@ -33,49 +33,49 @@ public struct SampleCertificate { public static let ca = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), commonName: "some-ca", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) public static let otherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), commonName: "some-other-ca", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) public static let server = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) public static let exampleServer = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), commonName: "example.com", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) public static let serverSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) public static let client = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) public static let clientSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) public static let exampleServerWithExplicitCurve = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1699960773) + notAfter: Date(timeIntervalSince1970: 1720088924) ) } @@ -106,258 +106,258 @@ public struct SamplePrivateKey { private let caCert = """ -----BEGIN CERTIFICATE----- -MIICoDCCAYgCCQCf2sGvEIOvlDANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz -b21lLWNhMB4XDTIyMTExNDExMTkzM1oXDTIzMTExNDExMTkzM1owEjEQMA4GA1UE -AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOjCRRWS -ezh/izFLT4g8lAjVUGbjckCnJRPfwgWDAC1adTDi4QhzONVlmYUzUkx6VPCcVVou -vc/WM8hHC6cRDdf/ubGRZondGw6bzaO2yqc8K6BNSvqFnkuQHRpPoSc/RKHe+qTT -glhygm3GlAUaNl0hJpXWlLqOoIb0mn8emF7afbyyWariPPQyzY2rywPLPXipitmW -Jw7GxVC+Q2yx5GQxPvutCdtkAsrS1AsYxpvpW+kHmtj0Dj40N7yhTz1cw2QtCD2i -CQuk9oRwtIiJi54USy/r6oq5NOlwqHyq+DGDt5XZx1RKvGJTn3ujHPEJVipoTkdX -/K+RpqQxJNGhyO8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAjKgbr1hwRMUwEYLe -AAf0ODw0iRfch9QG6qYTOFjuC3KyDKdzf52OGm49Ep4gVmgMPLcA3YGtUXd8dJSf -WYZ0pSJYeLVJiEkkV/0gwpc+vK2OxZe1XBPbW3JN+wmRBxi3MiL7bbSvDlYIj7Dv -8c1vs8SxE4VuSkFRrcrVV0V95xs01X1/M9aa8z9Lf59ecKytaLvKysorDrCN8nC3 -zlMDehPCLH9y4UlBqp8ClUpKk/5/P8HXr40ZGq+5TFrR80YABPSVX7krRMcxIhfu -IFIT2yhjkxMQWj8SCDUZxamBElAXY9KHSlEv3y+teRQABBVNxslHXqLKfKTF3q4S -tUVJuA== +MIICoDCCAYgCCQCgCA1/0dKfFjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz +b21lLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowEjEQMA4GA1UE +AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTi2aJy +Vw3E0OQwNIm9GZOG4E/Rc0atKoJes9yWaMrMPGwoenLEc2JNIvJSdBGZHKO7HKAG +OnffpqVIXtRBIU7l8HEhX97Q+knI6wPz8O7JaGVf6KznLa2eFO6xGM1pogO7m+/M +0mw8LSftn2IEiJk9v00qj+WgfwJJqL/TUZRoT5M2+u99uiaW7bnI1+1vawo5i7A1 +zfN6SBud5K/BaEYcAjxX1JMWCJLWSuOFZArWX7Je2MP+LqZkjh8kQO+d8ZZaLSIs +ujd6x6/r365Sl24l4auNfWy/5V1Ctfxl4avupAm7CpmEFpswe/ucNHkD0drUCzvt +hBeR3coLXWgbQs0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJm1Yntrrl6WxPbsA +s1DrI9YHdQjUNkouX0PtGp4yKrP7hwTclIhHjlGaQRJ2p1I7hllCMCPDa2YZa714 +XhtvEmpWOeLXMFolpKEn83kccvkQviZ3yd2lKH64jDX1/g2Rf6dXhDZMKrMAkEdx +X3JwZwPxwb8VDtac7TkVgOcQFHRzdX2g6pQXz3eNsjckGNJgzzl/ln6DrHHDbruI +M7bfnc2ZCBcHUCLWts8LnX2ekUq9KOxMe4e3sD27sKPizklNfGH4Rdg4LByhkx3S +GGR3ziWyixfcs4BNhA5mbsvb8vpPdtOh1oFt+TtPxlQ2FQOnSHk6wF285XggYYgv +p8pG5Q== -----END CERTIFICATE----- """ private let otherCACert = """ -----BEGIN CERTIFICATE----- -MIICrDCCAZQCCQDQxWAzi9Y9LTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z -b21lLW90aGVyLWNhMB4XDTIyMTExNDExMTkzM1oXDTIzMTExNDExMTkzM1owGDEW +MIICrDCCAZQCCQC3Iplq4Q/2+jANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z +b21lLW90aGVyLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowGDEW MBQGA1UEAwwNc29tZS1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBANOr3vaYLkfnqk0lREo0VJD/rnUGQ6BiVtKiou0uksb9gX4oHdKlnqyi -dvFuwaJHIzjBhdWD2EqgWwuBTB3y/UybD/ZvkojnLD+QNMnbgG5aCnO03gVlVBOf -JggEtAEM31C7Fi6X7Gr/QwRI721+kqNSB48Rj3BT93cDW73aSeL6IZ8jlvefWYR7 -1UI3bP+4WG58PSJOhUs2edaOn0G5wRZ5LyK6A77noll90cP+CVNlqLj8HRapqhf1 -XZhGwwaEYxNV1oDroxq9mcM+6E8LdWCsdE3N4Dx6pdL0lOjwvhevZ2ct/fb31NYE -fMstojwKf9Of5/J4kZaC1mp44IwPS00CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA -iUuX1YYdVwqamg13eji1I9/8eMP5demBnXjM7DYP3JqDhClTYNnN8aB+o1YW51ce -3V1FtN/f3g3YMgYB4YSOb241G9uXGCz5CwcYeBCJbUT/PdNZOrTW1EzA+gAy8GxS -yMbK+ZrXy+7mJr79sumIO2WGk//eznvgrmlKq+eZtVf/TDTYs5TdbqI4sqoN+qPQ -WyuBXEkU2D259VvZ+GLljVr7JCysciALKDk3QAb6cfjhFh3aOqb40m5i4Jg6g2G6 -iFS1kE3KjaWhYYn66BRVOYzfT25RkFBxxJh2Pg6DQOyVUWsWJ+VrstpQlcGMElmq -/LaIwNYfuUNcKb90L+M6vg== +AQoCggEBAOko0E/i9WlJS5eBAfJsQ1Xz5cAse959qRz4LSE2PsRXYIqGD+CHSlxf +K549WPYfCTEJxT4+MCwU9MfyHSTmhYo/MEA6K1jMznZULhYFriLLiGBCB238W0Xo +bEf3EN9xrHlmHaYrN9EwI6Qiq/AYkpAmbrlgbLW5Ig03YWTODS8k4R1nrkB609BC +DBEyzBiCjgzo0xVduTgf6iiEfUg+dlvkeH+4qjLU0DRJq0g7YIM/kEX/zL2YUad5 +9aytkDjO30IhcjQC+wvhCLBn6FDyYOpthaGM1cbMLG3efMpGAtyny2qATo63yVmf +kd8ftmV86BidAm+tCnFwBzxfXd4CB00CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA +Qxth0x5noVZrWZs67kBpjhiNI5Zg4/IMFukL4qv4XqC4AkwJJ4XaMAVTgtZ+mGmr +yOJ6pEzw0C7nWmTtlUjQu32Z+YNLSnE6wcIEx8ed1fwI0kezcyBBrg+Rs1vDNi1c +Tshq0by3RBuuSLclYrR64pmzYj4XJjABYIPurmtBCh4iwVhEe3tYs8I5vlKhmvA3 +ZTnqs21wD0v7FA4aM4EguFfLTMlBuD7U4G+agXvtcV4tXzQSh7RaXB06Mt4mNJ1k +LfqH39ZEnzeqUVm0vn283hvH9RzTYuHZu8J9wtmDrSTb6EcA4kpnILOgjhyLNL5G +EZi+HPA+wJ2bsRVlAxmuMA== -----END CERTIFICATE----- """ private let serverCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMjExMTQxMTE5MzNaFw0yMzExMTQxMTE5MzNaMBQxEjAQBgNVBAMMCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANEQLRL2oOHPHN7K -ozmP8Ks8CcndChJrunNTJAWCiMA/HFEVVfylnQpPyc/t+W2yD+d+PsJknu2hI1O5 -o53SBsEDm1taHaCJt9ur5NKpEphzOI7VuwkgcoGqmY0Hz7GmBmbG06Z6ne8EZfg5 -a/rjxhW3GyOmIT3s9xWiU3MW7VX0PDlVmkZzVYtcSp9+AXQMDpvLK48INu1mUC6u -1nbEzj6KuFwpU5+V1cRLHer+I9HVA7qBcgsIDDEdUDG0/l0MivAyDbNHGHDZcsfj -jwTMsGRcd+IONItHyYb72+JBEKv3/qFAe4XIeR6iJQP4OxZ4CoxeUFgkVQwqBNd+ -1JYDuvECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEASoyiLe/ak0nH5Bl7RvwAsO+Y -J2kA/oIUCpFEmsqDxK9Nt4eGIvknGzWsfTsVM4jXQo1MYCE7XrjpG9H6fubeQvxG -b+eU3VaHztflxLouzBIM6LnzoXnt2/rjQWIMbWZri2Gwl4incvVDLOv3jm5VD1Uw -OePLd+DvD/NzQ4nWdqCqhZAjopEPUpOT7fP8OkJVjGddvAn/0KyXkg3tutmUMB9m -8KctofAp1fKmd056Lgj+j6DIFDxxWEiihTO1ae8FlS4X/teeGSEVGv5M4baWRrcD -29V9XNIbMiwCNa7DJlPpxkjHdT4KifwPDHJ92RfK54SU1k0i8LD9KByuV4av9w== +Fw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuWLHyxPM/F +sviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv3x6sdRdT +0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAkbWL6X4RG +1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb0GM1yvBa +j88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6ZQ2x3H6cS +cTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU4P+ZzHEw +r/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGpBsuzx72mOBa9o7m1eNh2cY +H6MrNi1b6vTaA3SOH68RDxg2qx6UrKxI34/No7FaOzRrfs9vUaKXHwwBnDxMskH5 +iTmVAGegumDQE3Bd11j+v1tKxXWS/bvWH7tfK6taoex76ktR3L8qO+Hp8n4YKuSb +qJScIhMPIg7fWPonLvcszGFPdBIxU3YkAZJZFeom/s1WhWCYXsJZSYOXv4YRlaU5 +ozeV3v9icDptaxNY7n4U6C32eykMjowJJ9dcOD+ib3PF88S+utmZnSEGYu+5bnXy +6MGWZcYH1wQ0RpNC+YzjQcGsKwHfaoBS4WFEK2fJdRfX4owZOu6HO1zhyoLpqw== -----END CERTIFICATE----- """ private let serverSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl -ci1jYTAeFw0yMjExMTQxMTE5MzNaFw0yMzExMTQxMTE5MzNaMBQxEjAQBgNVBAMM -CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANEQLRL2 -oOHPHN7KozmP8Ks8CcndChJrunNTJAWCiMA/HFEVVfylnQpPyc/t+W2yD+d+PsJk -nu2hI1O5o53SBsEDm1taHaCJt9ur5NKpEphzOI7VuwkgcoGqmY0Hz7GmBmbG06Z6 -ne8EZfg5a/rjxhW3GyOmIT3s9xWiU3MW7VX0PDlVmkZzVYtcSp9+AXQMDpvLK48I -Nu1mUC6u1nbEzj6KuFwpU5+V1cRLHer+I9HVA7qBcgsIDDEdUDG0/l0MivAyDbNH -GHDZcsfjjwTMsGRcd+IONItHyYb72+JBEKv3/qFAe4XIeR6iJQP4OxZ4CoxeUFgk -VQwqBNd+1JYDuvECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAZN5RQsfPP09YIfYo -UGu9m5+lpzYhE0S2+szysTg2IpWug0ZK4xhnqQYd9cGRks+U6hiLPdiyHCwOykf6 -OplIp5fMxPWZipREb9nA33Ra1G9vpB/tZxQJxDTvUeCH88SQOszdZk79+zWyVkaF -+TCa3jDXb/vT20+wKxpPUjse5w2j0VOh21KaP82EMyOY/ZvhbMC60QyHnFDvJAEV -sle77vbdLjYELpYUpf9N+TxFDZ2B4dY/edprLZGt3LcUUFv/WB8FxZdWcjdZML2F -TMqicbP7H27+V1HF1rFUJWKzDNh4Wg6bY6lQNTZeHUyLwf/WlUraXTKYpqSH8FQ1 -703RGQ== +ci1jYTAeFw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMM +CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuW +LHyxPM/FsviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv +3x6sdRdT0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAk +bWL6X4RG1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb +0GM1yvBaj88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6Z +Q2x3H6cScTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU +4P+ZzHEwr/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY3vY+hng2gLh9t8q +/fvZewBiLAjsePbgRGT/xO4zCi3JwbHt07oGyQfo63ok5IJIrj3MPVy7N/oGJF0Y +niQrIhXs0NCKEZ/P9amh6wZJKAOtfD9t3oiNWTx56shm1vFQTTUdpykK0b37jGiK +y0N0p8M27ym/gQGTixfHNBtA0p+rmdDErOHqfU5Px3iQfmMmf4hxXPOSkGMixyre +3AR6wURMGLUCLVxi0sQYNd4fGo/GwbswTSJI7+sypZHMwpXbaN7KjorkSmI8UuoY +aGEewReM008rQWGWf3ybmNCChhru82lPQGMp6y9fN0s591iIzjpCXixzd1j1V4oY +yXRecw== -----END CERTIFICATE----- """ private let serverKey = """ -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA0RAtEvag4c8c3sqjOY/wqzwJyd0KEmu6c1MkBYKIwD8cURVV -/KWdCk/Jz+35bbIP534+wmSe7aEjU7mjndIGwQObW1odoIm326vk0qkSmHM4jtW7 -CSBygaqZjQfPsaYGZsbTpnqd7wRl+Dlr+uPGFbcbI6YhPez3FaJTcxbtVfQ8OVWa -RnNVi1xKn34BdAwOm8srjwg27WZQLq7WdsTOPoq4XClTn5XVxEsd6v4j0dUDuoFy -CwgMMR1QMbT+XQyK8DINs0cYcNlyx+OPBMywZFx34g40i0fJhvvb4kEQq/f+oUB7 -hch5HqIlA/g7FngKjF5QWCRVDCoE137UlgO68QIDAQABAoIBAEumodjh2/e6PYU1 -KHl0567e69/bF4Dw8Kg4pqlDwf5nF/UTVmk0+K25j5qpT3/tVin7mfQ3+vacP69V -VqqOTJldl8Mnyd7E1v4rpoLAYZU+5HFzT9oOnsDjHetVr0dmf5yDSCVO64WJPuji -xnskHxLOjoiI3jCNZh+y/KWB32IhdofSwBccw852JM2qC5l8vgE+sfjOeWXDiPRI -YLlVRlxZFv7N2kn+EDnPEQ8m2OGYKvNzU0d9nz05NdkRXzMh9zegTTL4EQhTaMf0 -2AXy2ekKFVvWouV4y8QW1shz5Tun2y4ZQJnwiCyldED9sMxaziQxfdJ6N7f4+K5c -Sh4+Ct0CgYEA7bGvY02jQfHcDPOjZ/xkXb98lr1uLGTSvwK3zw+rI0+nrUJwH6fB -nSaXyWk059OqHTKPpa/d8DxFL2LP6vbvfTWCv9mnn61WWWDmP0Eo/k93XgWkmclb -vQGfXV2wtCTnhz+iUSSJA8f8jZhCtOD6xa8pLsaYrGD6oR5wzfs0nysCgYEA4SoC -/JWDMkw4mndI2vQ8GqDzBFtJCMr/dva7YGCGtbimDzxuOI68vW/y4X8Izg2i1hVz -iKRYCI9KzRdQrQ7masZF2d4DeaqPeA72JN4hoUOw0TZjP8yD4ECifUt786ZNvV1n -NlEhNb55zD73Cl6v0OJZkEfp1MC5ZQwLw7bMYFMCgYEAvNu5Z0WAuhzZotDSvQSl -GnfTHlJU/6D8chhOw47Hg77+k4N+Yyh/hcXsRHP7PVfIinpp+FPMG91He2cfnKmn -j+y8foMJ1K19NnbvesLjN20cgvAo4KhE4+AuJ5kRlZDdBXFiHubQltiHqlmYZu97 -USbjqe7Rz+UePnZZWtCF9xECgYEAjGODZTVbjdrUWAsT0+EAMKI1o3u/N8pKKkSA -ZAELPPaaI1nMZ1sn9v179HkeZks+QjkxxfqiIQQm4WUuGhj2NZDWMJcql4tu1K6P -bkFJuqDX+Dnu+/JqL0Jdjb2o1SvVwMIh/k3rZPUUP/LqWP7cpGLc8QbFlq9raMNv -+mFZYJ0CgYEA5IQzp7SymcKgQqwcq5no2YOr76AykSCjOnLYYrotFqbxGJ19Cnol -Z74Habxjv89Kc7bfIwbz/AolkhAS2y0CYwSJL4wZIUb8W2mroSmaHhsk3A4DMPBB -wgSsdiBpixQqNDUAvnHc3FIyAGdpA73TJQrGY2F6QyQ9re3a/R8Dc3k= +MIIEogIBAAKCAQEApNl3O5YsfLE8z8Wy+IEFelLOXYzEnEOSaKvwNJ1c7kTWwcID +KZNrHL1s52LVdRltFPIp9O/fHqx1F1PQazZaOEvTIVP0gRWsZkAO3cYhigdhlTnp +if+cuCUZBmoh6jffDI8QgCRtYvpfhEbUdzqclDGHDVppW2xg0ChaMBXM/vvdNmDJ +FywaWKoe1ikPSrRv4R+Y+RvQYzXK8FqPzx1ZYUWUbTRpkVBq1KQis9uRcbQy7j/V +TFgMS7E5isgWQUx9HxXyHplDbHcfpxJxMnZOkiXDes7oTQn8dhpWGa8ml68g9CG8 +f9D68zVuS9oiRyQtyFbtWBTg/5nMcTCv/ExyowIDAQABAoIBAAq5FpdqqlQmF0WQ +n5aoldmiH0hYisV7Y7+pR4O0pMHe+nU6EIiYzUPeUoIunKH0WHMfWXlUTRgqsacl +zY3byDyXOhGV63amGUPBcPYeGDppRoC1dqqCVQhpaVpQdwpMPhcMC0+6jt78WFA7 +Z0CmMF83ZYiJ1AadYyLHLS6pjF8dmkj/Rd6yeLIVkKr4xHxou7au/6WKorop5XLM +fEyWC2iotha2dkXw3i324n0qrbR2v/EYLnAn75uA9FF/pJWe6iPc6H5tfBSnzmO6 +fkZ2rCrDt4ANabg6WMmRdrZXFHSR/JlPPyh4T4iJGenkLltKZG+wWSm2nVXE0DYt +JQdmhiECgYEAz3EclGIrk63Hp/2mAHAOIOUGh6+Tk4JA+ibHSzziVZZqJsGQ9jcK +eOn6TX5674+aNzo2ROnHCT6u2tCQEl5/lrB8YpYh3F6aaNqPvFwqRhDViOw2l8KL +Ic40x19o5ur3Ss914htwTxiEzQVB/n+5zhE7W4N/RaDIT2hedWR19PECgYEAy3AF +CiHa6P+pbhskoSETtxbWkhDENpXat2dlFRDrNN9T2NZNVmIxCjAE52arduCxaLTP +hazyq4d7FZ4OkxfbJY9D2HnBS6mF0RHB0gZXZ7iB/uEr0KcTex5saqX9TF3YA5Wj +PNVtOM37IIaLJ1qOmfXf4yL3EVlI30eNwfoMkNMCgYAv0VAYOET5Rs7GP6b7ZNks +5f5KWsO29giKYVQBWOiHeCPCCU6kIu3sD2teX7Bw9nZDEs0dt5Hk5Kkj0X3UbioV +D1us0hS+GqSXVQJbFhe8jPbcGC9BblvqEAGEj867pCAbA5WV6GNMKEe8huC+jKzE +/p3jK320DCsAevuDLgQu0QKBgHvE60v+zPB0muAiI2bkeNorSuAS001iXm62uQjY +AkFondqOhv7HPo60KEegbzEkAstxNdBeKEWzZ27/el6DZRC02NIbQT6HJKLN6t2c +fhDccDphRAbtnyyIle1Mj46miYWkxGt+bbThnKdtM7v9nESPEmdeHnKvn2Y4YkZh +msOBAoGAaarkv8JjjmIgjRZrJ7r4dkzZwZa/msm+/NHr3nlXK227ExMeFRPmzYls +zIofM+DoEk1sDXRfnv+8EU8Dn1DYSq6M6W8xrm7Ulpzj0kXE4f9TD+MUwSNCQ6Gg +zLRkHQBKblIa0lEvlulLtJT2UN9AnCmvTH2R11wD87DWjFDZKD8= -----END RSA PRIVATE KEY----- """ private let exampleServerCert = """ -----BEGIN CERTIFICATE----- MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBYxFDASBgNVBAMMC2V4YW1w -bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5UPdc3MERjIj -rKNMcsCJEPJtzFZG7T99rDaENxl5hF5TdlCuMfKQMyf4rmUk2KdQXduWDmP/9keO -Btc3Hw9xm7mHn7UPbK0kHjlncqTCnjZzVQ2j0stg4Q0WjGeS0aB8k1AHPiBaOnvJ -LYcBJrA8mZK+inEE0gWEJsODTM+bKb3+5I69qVoHAkU2tXTDV9g1YKfP4H3rufEg -622AR1yAo8UxaGjY3amWps6XF/9R2iaSDAPLH1dCBw/YWrIH51n75S+n/H3Rz0+H -/aT9Eze0M2F2Nj1cU8fVcbDNR0smssgXVmE2mvQ+OvbO0H7VTS1HK2q2aPOPkRWh -yhFnOvPnbQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAWk5KHkWVsbXqz6/MnwxD5 -fn7JrBR77Vz3mxQO9NDKN/vczNMJf5cli6thrB5VPprl5LFXWwQ+LUQhP+XprigQ -8owU1gMNqDzxVHn7By2lnAehLcWYxDoGc8xgTuf2aEjFAyW9xB67YP/kDx9uNFwY -z+zWc6eMVr6dtKcsHcrIEoxLPBO9kuC/wlNY+73q04mmy9XQny15iQLy4sQT0wk4 -xV4p86rqDZcGepdV2/bLk2coF9cUOPOGwUBqEIc7n5GekC2WTXSnjOEK5c+2Wkbw -Yt4jXnvsaQ8bwpchHfM1K3mLn+2rCEZ3F4E5Ug7DKebrwU4z/Nccf1DwM4pdI0EI +Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNVBAMMC2V4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0yKJAjr3evjW +69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+95XROgElY3DsuARH5wuksTi+mek5 +J5MObbUhHtGIaoqVaDew6TokawwQyBwngKudssu9/6rq38m33OSEv/5oc6xtTdKJ +Lrmqqf664+QajxCeec9CPMGJExQ25c1A5QOkkyC5xR+TcRRIcKPaDZ9aj6JlcD58 +QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+qNV2Pl5DZL8ujEX8XCf8EyHc5GNYN +5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9JyM+wu/a0iMVBr4Yk73VVhOaH+aUI +eNHaD7fhlQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB1++0U/7RduytrWh2bu6uz +sFk4XK+5eIhKn4DMq6vKXFQYF94Tkz8K2RDGzZ3Cl8qRU7dLwlHrUgqFI89XMFAM +LjumIWoMnfik8A6cBmp/HqURzXPNv6Wgn4MtU7aDs8WAEsGYAo5TTtqVJUGc2Mlf +NkW3MQ/RTfUncamx2wNFjwLmGTuERgHA/OA8WQVnMDI5JLXH5sigdOMTkqgkGzhg +8NVWnqubG4b4a7W3xl4s2FjqglqXP3vu+c1F6cWJfKgOXIqd8NduJ+p2FJZ1rW2c +3jkHoqBLqA4/zua+HUn5ICcUZrZid7HgmlUoR/4n+dbjT3Jdpp4BpNn3q8JuWE4Q -----END CERTIFICATE----- """ private let exampleServerKey = """ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA5UPdc3MERjIjrKNMcsCJEPJtzFZG7T99rDaENxl5hF5TdlCu -MfKQMyf4rmUk2KdQXduWDmP/9keOBtc3Hw9xm7mHn7UPbK0kHjlncqTCnjZzVQ2j -0stg4Q0WjGeS0aB8k1AHPiBaOnvJLYcBJrA8mZK+inEE0gWEJsODTM+bKb3+5I69 -qVoHAkU2tXTDV9g1YKfP4H3rufEg622AR1yAo8UxaGjY3amWps6XF/9R2iaSDAPL -H1dCBw/YWrIH51n75S+n/H3Rz0+H/aT9Eze0M2F2Nj1cU8fVcbDNR0smssgXVmE2 -mvQ+OvbO0H7VTS1HK2q2aPOPkRWhyhFnOvPnbQIDAQABAoIBAQDk0+vAQ1hMx9ab -hRHUpx8nbxDwFl0Mh4Zj0LX+WMrUt2EOglCbQcNzi73GMuWn6LdqNrV6/4yGv7ye -T0iRE9UM3Qzk9s7CZb3a/OinoJMvXqGWjtqolp3HgkyzLt13pXsxfXr9I0Vrggm2 -Cz2248hYcAMGIu/wv9i66AGxNLVl3nzlo3K3J+6LwGYSrM5MMsN8p/RIc7RD30cg -Qer6uiGdYemD2hbOuqcqImzhMLvoYn683uLOoDhiFLmAPIU+VxtHs2pMpp4ebjrl -PpS8TtHnV85v/fhX6RE/jo5razdSU4LxW/p/fF5Zte+QR6FJgFFWaQvZd6/Vtuh6 -K0Hadt1xAoGBAP1fuBjUElQgWBtoXb422X2/LurnyHfNSqSDM3z8OiCSN08r5GmM -ylWqh7k1sQWBzQ4OAsZcbwvrpvxMGYEd1K99LtUcM235WcKTomz7QRRUKG74tyFk -VdCgcMF+q2DdBE+hlF08bTNk1dM6uPlinNiMklydhLFjjhLlfDkiteGTAoGBAOek -LXqKMK4H5I1VQBgNKAv25tVItDabX5MPqhJVxmsvbNNTh/pfaNW7ietZNkec8FXs -UtS2Hv2hwNMVSb8l9bk96b2x9wiww2exI4oWKjKkJrSVsIcWc4BgeZ2xUtnV6QR5 -XSNm7D4E11KhuHPbB01cAZeC0Cf/4rTZ5ERhULL/AoGAS/UpHJBfGkdEAptsFv0c -gH0TFKr9xySNLvqCMgLvbhpHaH2xEQ97DOl9nMGC2zLJhWAf5tWJGNrBibtKnhGS -VDXEF3FH3b018oYN2HwOS4jbQkFfrSwGKfAfPXK67+PySekXsEfQOOsOyy88is7M -VIL30boLMJ621eVkM0C7o+8CgYBLiiK6n24YksJZxL9OGJxCqpXEYB1E4Y5davJP -YGGAesrGb6scXxjU+n+TnFgzKl7F5ndsnqeklqdHLt4J09s6OZKMJgklcF+I5R9t -3KSONzHYGiijJRMtfkiqwDUAjN2cc+eHr/zCjNmbPNnmDjtnYuWx/xrasHvB9nyW -QBYNCQKBgQCDqdvchLcreSdbXKr6swvBz8XxzCaEParbm7iOvdLlv93svvCaHgI8 -6E+FlXk68Qc2Dj8de/xEnl/OonNQRgIQh7czBJmYP8+TCPECm8fvv2TsddNvjTmF -TTx8wf9gixHffRtXZ4ILrP1sX7c4if3bfaMxKz+0ZyfzWZry8qZtVQ== +MIIEpAIBAAKCAQEA0yKJAjr3evjW69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+ +95XROgElY3DsuARH5wuksTi+mek5J5MObbUhHtGIaoqVaDew6TokawwQyBwngKud +ssu9/6rq38m33OSEv/5oc6xtTdKJLrmqqf664+QajxCeec9CPMGJExQ25c1A5QOk +kyC5xR+TcRRIcKPaDZ9aj6JlcD58QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+q +NV2Pl5DZL8ujEX8XCf8EyHc5GNYN5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9J +yM+wu/a0iMVBr4Yk73VVhOaH+aUIeNHaD7fhlQIDAQABAoIBAElHYToO8ToTB7US +HjHTLRvGupna8n+9CL3Hs/X9eG2nCAcZ84tGyjlRkIJ0/RPZGIOOPPjtcunEUnvz +xDw7c2VY3/nqY3Sqb5JjBaTJqUFq1CMKbU9S+3yy+5X0UwYtog1o5SPQopcyDT7U +XfFmYcMatkUVRYuNbbXcjhC7IVqcQpPzPrBaGJ/cm8ZCdTJIWCrJfsI2mgDA9d+B +k5c5uQxhohPWFdZsGGTdJgRCJww1mlAHJXxR06hkBJEnG/vmRaCvsjBgrOtP0/iE +y8NBETAYMl1Ms+w9Pv+pcE8eHgAvicNkKiOlvLTeval1ZIV236IiGlvls808SaYb +RCTlN7kCgYEA+/sFtAMj/ZPacAcuv1mTFKaYUFwbeZAZGHVyniwfgRRvPDBtqQ9i +vGWOLL2fLqbp00sy9LS0FNo8OSnJSGux2IEmoHdNWDNzyORkXLhB9Lov5VqKj6V3 +PO6JswgPtNrb+3fQulAq7YkU6qPdrtHGdU+to/lHQrD6FkMLdKz/zdMCgYEA1oC3 +nOv/PsUwNWlUzkxfgQ0Pnv32NKfoFu2cWL9C0FlCgQxakm03JrbcfjEQafPR64jE +7uhe7aueiQz339jlMxyJk5BFNn/nIOBmLUFD3wV7xcsU1mRnbl/a4R1Wxk7vwmW1 +s4LRu35iiWb/UkblsTA/qdMcigRlRPkOg3TYqfcCgYBWgWgE06swa+jq2txmnrbK +uSLDO8vG4PxslC2ENbufEcfaTvnmtzx7VxYHMBYM6wqNGlzk+4BzRDS2nyzV6vsE +S9pZ7nskE43lYts9pZgnDyBQSdQV2oVj6rRlPRg/S3+IBisnO0xxfcUrhJQfZy8N +qQwAphybvawtplixdo7fNwKBgQCEvxfipzJJOGNDSrJPEXixNtIKBQUPRTIermHp +kkPZCMRddLXAlJJjBRujhN2xlFC/QN8PMwM8ds8f5cSo5WPCo9CIX+pVdgYllHnn +W9KS/KPCnpGAtJZF+lBMrIl9JHDAj41JUJZXQDne6rzrwDB53XAouxuYVmwNqUxQ +EknbtQKBgQCylI9b7Syz5pBzZZNBSn4eqzlsdYgG3WUMGyEyvVzTjfbovWK98xeE +A5F8BNesnoopCW9MtW2QJ9iSLn24sNpj0Uvw9pobB3uDj5Jn0oIndGe7N/7beYhE +HjkJ4liJkM5Q1oUOvVFFPJWEm99cP8urojakeMO3la3tGaElwHVWWQ== -----END RSA PRIVATE KEY----- """ private let clientCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBQxEjAQBgNVBAMMCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOMSDHjt5P3MOmtw -atYP0PnCfr2sxsMd1uxKeb+x5mpYuckcsnm1UR0LcBCizwBZ7yYAZQkFyK7vdJge -Hv9P3Rki6+6Jj/ngLdpOirtUcOfnfzbdlA2k6qJtY6G+ZKyczDICWaZHzNRycDkL -Yv4kzUT8PynIRIK/LPyXQa+tGty9+G2exVPdpzKpCgE8fKd8FeCOLW06Z0RsP0FS -ySPSJxdDq0BRbfurhplhawh7uJ+7IoVfdWV2wwDvLztCEXHn2iiNpyzIixYapnVB -PX1MXelsPRJaa9EKwOiqJB5ZcV9JWk9wa4W7mJrRfFTRh/9HRsoXIAaJPIqjTmqI -ffat/1cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAWpPkMzjKLAnxbfyasR6cPk3t -ZanuBQ9RqVJw2gPem+SmSpvyPZ3CJTi0ZeUbPRSJ2W8YHyogaA5XShZSm8JJQKye -TNYqUewVbU18OwVco0l7Wc8R1iiZgYEUcvMF2f/EWEMoCTmS3JlpbLl7LmdTy7iQ -gIrR+iQ649nLw1T4Q5kp7zxjI6WJ3eZVNUjlTrUzKapSY4Tm2/+GafD+WNVRRACh -Y9VNkaQ6qYy4SaLw6+bX2YdbDhIi275vAONHIZcAsMt6/aLJzKgfTxRqqmEvmJdQ -KSVRRaSKZ/qe9UBdl4oFn1wupFAoNDQWkXT/Q3kVhxVXZ8ZE+ylZnuWcj3CHvQ== +Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr8dF1Nzg/ +gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjPtPJxRSgj +qUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o9qVe7uQr +bSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK8hUfMq5K +PGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+B64HeKX9 +xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p4q/NHRvO +ADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsjJ+nFYDH31PmM9YpKGuytOw +DQYVLFYWIGybZC7FSESzlGx1GOZ5nY1AWj7gCnAvc7/Ct3efI/7qA0tSB+rbd2tA +/qqU0/0FJLZDyiXrpNzhFoYkg2VzZRSmFdsJbhzjNJM7iJRsWEhXn/7qydLyp1vj +i7DlYQVI2QgEQQz7BMJ6D3zPRxlyDzlVjr7l54M8RX9Dj8Oj9Sajd0RlkLiGW7YR +TC2nNebpRGN57Hi8dCM3xLWQcJ0N7BK0A67MnQaRbUQ0DMvxXO0+HUHxpfN39P/H +6Y81QkFAeeCMCsSWTGHspIJ8teKk+KmIe3xZ72taWNge1Cu3xas7Zsl1lbI2mw== -----END CERTIFICATE----- """ private let clientSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl -ci1jYTAeFw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBQxEjAQBgNVBAMM -CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOMSDHjt -5P3MOmtwatYP0PnCfr2sxsMd1uxKeb+x5mpYuckcsnm1UR0LcBCizwBZ7yYAZQkF -yK7vdJgeHv9P3Rki6+6Jj/ngLdpOirtUcOfnfzbdlA2k6qJtY6G+ZKyczDICWaZH -zNRycDkLYv4kzUT8PynIRIK/LPyXQa+tGty9+G2exVPdpzKpCgE8fKd8FeCOLW06 -Z0RsP0FSySPSJxdDq0BRbfurhplhawh7uJ+7IoVfdWV2wwDvLztCEXHn2iiNpyzI -ixYapnVBPX1MXelsPRJaa9EKwOiqJB5ZcV9JWk9wa4W7mJrRfFTRh/9HRsoXIAaJ -PIqjTmqIffat/1cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAIqGPpw/m3oIb+Ok7 -eZCEph/IcEwvkJXAFYeYjQHDsK1EW/HNoyjKKU3CaLJuWtvNbW+8GpmiVdAcO0XS -RN+xwDOLmB+9Ob70tZRsR/4/695WkkCm/70Y89YqTq3ev86vZmPWBGZsdXB/rvfs -sJbEkNeDRAquEbVQ3K8qmG7w8oC+VdzdQfQHY6hdkzsb0Q99aPASwGjxPVDz12Tb -v9g9f9yVwI+vxxabHr4nvKJ/GfuHRzG2eSW2TNBY/Kxp10+lCdMfbPq2p0LsV4eZ -eHPCFqiBe6CK80Pdpy7CNCPBBvGkGb7nfxBi4/tNVDgMlOQy6pA3PLjib8NLMCIA -5iUEvw== +ci1jYTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMM +CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr +8dF1Nzg/gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjP +tPJxRSgjqUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o +9qVe7uQrbSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK +8hUfMq5KPGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+ +B64HeKX9xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p +4q/NHRvOADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAiEKKo8JIG29d16ZT +6d4ERj/o/3B2rwpTvSxmVaon3Zzz0gQ+HhuEH9D3XzzZ//P7qe8PxpcZ75veuv7X +ZcIPK+L7QqLAR/RrbWSbhI8CpQ0WX2MitKkz+cdRCey8/4JF4g8PXMMuFrP6fGEm +79l4aJoAiTNJ98qufUzD63kqU+kpPGjML6rnJFfwTVAWu/7Sy92u052IsoZfiKx0 +yN1vYr9jLD48n26YsyVjuuqqMW+OKxzRGA3xCa02W3cILQb0NVv4hM0+yGd1laKe +1zGHzuaeCIL9bFGBtxRXTWyyEG9z5nohEz/waHpUHg5VcbrkLOIIAhsolLuDKQyl +JanCRA== -----END CERTIFICATE----- """ private let clientKey = """ -----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA4xIMeO3k/cw6a3Bq1g/Q+cJ+vazGwx3W7Ep5v7Hmali5yRyy -ebVRHQtwEKLPAFnvJgBlCQXIru90mB4e/0/dGSLr7omP+eAt2k6Ku1Rw5+d/Nt2U -DaTqom1job5krJzMMgJZpkfM1HJwOQti/iTNRPw/KchEgr8s/JdBr60a3L34bZ7F -U92nMqkKATx8p3wV4I4tbTpnRGw/QVLJI9InF0OrQFFt+6uGmWFrCHu4n7sihV91 -ZXbDAO8vO0IRcefaKI2nLMiLFhqmdUE9fUxd6Ww9Elpr0QrA6KokHllxX0laT3Br -hbuYmtF8VNGH/0dGyhcgBok8iqNOaoh99q3/VwIDAQABAoIBAQC5gA4mYJoY6FW1 -XcI5m/QphcWKaHJ8BY2FvZXWj5vftxoXfNUk7oYURzrGrGqVK+Nd1Sa1Bz+aAc7r -UngaNQE3vrqlRUYUaRqsZEubm/Ec0pavmLaRqu9vwBOLmAGgrft2w0q/t5pS2CZr -w6ycWC5FNBjZplypv0oeE+c6gB0YxKJ2mjKEYHWOop+uBPql2G6TfCeu4mMekZPH -cHbMuMlBPN23HT7BmGCvk1YSaGbMt0iCTM0zThfe53AtLSVKn33szHm/XjLYJYGM -7N+SttwM+O88diFShWHUHmWsy5Lv0Mrkw3NRz37yQ8Uh1fJ0TMeLZEIzKSLrI8lv -XrBVE89ZAoGBAP8bBSytb6Aof6p1noIx8nu31d5/5mvRSUxoF1Lu/B/EeaNTGXxD -HvJEi4Lh/txm6/3IeC5gfJ0jExxoWyTD3wLqVvmf+FEMntXqnATC832uw3vkxZpd -E/MldDHE4UkWGBUvii7JN1fisyyZKLUu517crebJthpwk5Sf/1FgK1SbAoGBAOPd -3VTQ7uaD2zPn4QM9KZsFJ+7cS7l1qtupplj9e12T7r53tQLoFjcery2Ja7ZrF3aq -y07D2ww8y8v1ShxqTgSOdeqPCX1a4OS7Z93zsy58Jv3ZcXbfbSGiLbpoueQJbUZ0 -vKlDIf4uHn78fz8WIbe87UwKneKnaRrO64DtHQX1AoGBAIH11vYCySozV46UaxLy -tRB3//lg+RcWQJwvLyqt2z2nzzv4OrSGUT6k0tnzne3UdQcN2MPvnaxD0RmYxE3/ -hx4qGfMDnvJTVput8JuwYXE21hnI2y4fmuk0vHQaU5bzLYOle2UIVyxrrlHbGNTs -tywpimJXgnEHxvdhZyWis5BfAoGBAN45P2M6J+KzcRGb8DuiaHMAgkNWoJsMAEcd -mldrTeajINCsGeHtycyTpi/4tw0+P7HBO2ljZLr4h6AvZcl0ewXCkYjhWlXgTTeE -9PTmeDa7aaNjbl6J4vpMGeCTxcZ40xNFQcCo8fvbqm4ZfVdfFB8Gpz3jlLq4na5B -YjdoB0gJAoGAZCK3JbIN56KnmyENuZ6szWTZwkCMZq3kPVKBK8LAITVLVxg7Emjs -GyTU+JhMx9Hk2tU/tftM/dTZ2TRRMwmPbZNadtkQdDgsXDhfkrW9JmVewx4ECCcI -gBfWFOoABVTmVM9oNc74FeWu3nDjqGix5ZJ8+Zjjr8wUEcrU2TPZKn4= +MIIEowIBAAKCAQEA4/wm1Wvx0XU3OD+B6xTlTTpiSy467nFKmG/cYS2whj2Lj2up +OTq3jPVo0mAEb7Z88qoIyM+08nFFKCOpSGjFJLXkCnrJ4M31UkYZvZMfWMBgUeZV +vqcHqAN1h2f2Kxg76Vohnaj2pV7u5CttJYfGvhmo6VUYzmSIFMgijJAlicTERnqf +PqeoFWPpmRnc3DD3NFDwCMryFR8yrko8aiFOoC8i7c1TBTXYB4+2d5vfUWlzUMpY +zdl4LyQ0eF5m64iUGGsGYn4Hrgd4pf3F+XXnMj3OIMtLMxr0gvplX7dzogGJN4Zq +9/lyMc7Vq2RAhf6MJFDBv6nir80dG84AOEBd6wIDAQABAoIBAGOV2hSxoSCAVg2Q +2BwqtXrFfPggCofrHs11V0tvnMMWkSalvXaNKm49KHt0i5uMmAmbslidOgoI5k+B +PEmv0iWV+jWFqzcyX+1/R3Eimbe32JsNxPiRl2uRjz4FcGckn87vmu12R762uB0c +xwF0zKBvLvQ1Qq+tBDAnt8e0k2EYqgl7LEIb/1vDsxyVLNLzpSBfENYbE24YD0rI +/PQwot0UJhWVFnYxYczbQSLxhep1tRzhLaUY3k1SrvGvG/TM389TK5upJoV1/emc +EgRCnBPM2geOq59Bul0ri9bvXyVh19VegNM8MLwdf9/UtDFEhApYyoePve96LspM +aOQSe2ECgYEA+NkCXElSy+TdPLH4944PGdXNm6GtuhlRjMc3pz+r/5gQ9J68rIRt +OJewx6bQx8lOFYxzhiTbN2wY7ylm9/Cn6jk7zfUXrqq73tbWgS+YSWajoPZM4sDB +QdQuOkArdLGkHMM5+2U81d/D7o5nb4/ACCY1XtvrNUqgBe6tyHe/INECgYEA6omj +GVY/yYK1RsRNrTmYipZbnR0UPpTsVevdAoX9TrHG5AHTgmh/ONGGCx3/dgmLqZO+ +D4MZSGyK1appxJjpsKzkM5T7X+bznHWzXe6kGnzDzkgBzx4N4s3LHPZ4uUvNnZwi +h281KEBsblKOu4khDk8jL3LXjRxGYwOjqdisYfsCgYEA9ho4OWjSl486NYKVhM5b +pONLunUFSR0tB5smMSPJSLftXN94HO3CzstGK82QgWVW8fy7a5kbrA4eArjheqfo +iL4dpSyVRUrZDiNOdOjLJRx7Cv9LPp3/AsmDBlzcHUZp1YBF4ZhXt/Ta4xy2syBp +fCW9dpjsXwH0jKll+PJkdWECgYAjgDHv495D4kUOMSiQz+cHEztKzNwDnQco+kq5 +1w5Amyg/2wbo9mhLcWuYwzGn7En3oSVjs7RgAg4ByYm4+GxnEcR5ClQCcDLvu+Eq +lrTATaJV1xBvCV2QtxXHjIc5hP/am4eeeHbTYO0IxfZU7KzUPaZVyExYT69XzXU4 +gFOXgQKBgFbrLumJh27/nHtvUM0xh7RZ61NplRXDLez8DinSlNI9ZKl197LKdBzB +6cHi59SojJFJY6QAdqdtenj33KHKjgdc3rH1VvipytPBJRO2qohBpYuSZiY2y+Df +dW493Y3+mwD6VsGFFvBPSC3jhDBeIYxajEJChzkbClVDRS0muLQv -----END RSA PRIVATE KEY----- """ private let serverExplicitCurveCert = """ -----BEGIN CERTIFICATE----- -MIICEDCCAbYCCQDCeNe2vM7d6DAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt -cGxlLmNvbTAeFw0yMjExMTQxMTE5MzRaFw0yMzExMTQxMTE5MzRaMBYxFDASBgNV +MIICEDCCAbYCCQC7a34VXIF7+DAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt +cGxlLmNvbTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNV BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo -N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABDmd -3Pzv6HbsUTmNd7RljKbkYP+36ljl6qVZKZ+8m3Exq4DvtIzLKho/4NluAhWCsRev -2pWTfEiqiYS/U40TnfQwCgYIKoZIzj0EAwIDSAAwRQIhAI+BpDBjiqZD7r5vhPrG -TT9Kq9s4ekIc1/a4AoTioT8CAiAluJHscXt+vBcqEI9sH0wudusCdPJyLbvNtMZd -wdduCw== +N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABN5Q +sDW36YI12PFC/kRnACzCt8a5lqjaFu6QNl0Y0ZYaiE9MdR+EOGcCfoSGf9r8n1Yl +peOOLlvsXQ0UO8WJbsYwCgYIKoZIzj0EAwIDSAAwRQIgGd0bh4HWEd3ytsCEGaw0 +m567URfCk1u6sY4I77U64zQCIQD5hOn0PDS4eYR+kBB5MadQtcBtz8gjtW/OJcfV +D1NSHw== -----END CERTIFICATE----- """ private let serverExplicitCurveKey = """ -----BEGIN EC PRIVATE KEY----- -MIIBaAIBAQQgBLTFlKchn4c+dQphsqJ2hWVpLPeRQ0opnSwvRsH+63iggfowgfcC +MIIBaAIBAQQgHqp+i/1N/Iq8DUruPu0ep9WiB9I+n1Ox6qFucixKbr6ggfowgfcC AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 -YyVRAgEBoUQDQgAEOZ3c/O/oduxROY13tGWMpuRg/7fqWOXqpVkpn7ybcTGrgO+0 -jMsqGj/g2W4CFYKxF6/alZN8SKqJhL9TjROd9A== +YyVRAgEBoUQDQgAE3lCwNbfpgjXY8UL+RGcALMK3xrmWqNoW7pA2XRjRlhqIT0x1 +H4Q4ZwJ+hIZ/2vyfViWl444uW+xdDRQ7xYluxg== -----END EC PRIVATE KEY----- """ diff --git a/Sources/GRPCSampleData/bundle.p12 b/Sources/GRPCSampleData/bundle.p12 index cb9a3e369376f563dbd985c90726fd9e256bfd08..c32d3287272daa1445a9b6ca45bbfc60a71d9db4 100644 GIT binary patch delta 2271 zcmV<52q5?666F$*U4N|tY{QM`)wcoy2mpYB1AurN)Z4YdVs2O85`tTaW5 zvhx$M;J%`qTs%YewlQ~KKb~@DF7<%bh*^p4d@yfHhjclFb$b;bN1&);N?5Oo(+{sz zdjrUavM>{w^Kk9Sqdi`*Re(L~U%qY);=4JU?4H(}&qTUx5Pz823KYjqGgK?3u;+D! zTPRd%9Ce_{v8L!o)xD;szIwGuGYu=7^D%=0WEarQ&VP}GxnEx&pVSbu2U*&E0KU%8 zau-)N!qZpEqp0t3fYC>fv(4LEB#tLThUa9Gg zlJ&d#q)FYXd3v%ofFHFGcrLhm{>=&Az4;=nsN9g}9e>0@hB`p_ad@y4ukF61tGd}3 zbSkfi4-ZoMHNNwz8^VF*mAkM6(D>s&7_NK}^E@SEm*lZ$kS?Sv-k4;ybk_Fiw zOlYC1eJHw;y_Yi8cVX6Y=2sn#pmKMCe(k}vyzbnrj?zT-R+{rtr7*G{4Ev&l>Fh25 zxp8=?=YN6?WwX9IdU=j8Y*t5S&#IgcE2-X@Ck*vNM%^9SMt)z^^zv#vl!}*I04Srbh3+bw7xAn z54Qoo$AMaOVik~kG|O4^Xx0x+-D@0Kv0dEB zCug+&$#=a0q7tGn8vwDiP@k2r8A{VctFm2l`@(@TL*AqV%adevs=s*hj+*C|l3dw;hWgB*fx-v@fL-2U6*PBjTmnqVrgvFE+C zr;MuQ<;7GaXOhr6G3pf(@tUpAB6;=VJ~$k;K=sriDr~C8C#E|TV0PPFzb=4*JGS6# z43F)ChYNit5r88A$V6p2AvP?Qh?w)U*VHv$iX}^(<(MDrSia;|G3Ic`Ec70K9e?c; zE0}`2vzPMk7+)mGWAR1&LMwh$Rc^R*^v<+KS?1R7SZy|gZiJIY z3KZsJ;GYRHw#L>2X5<%`dhxaf6HrY4Li|HM+xr!KSGOd&!Op(ho-|eop#V+9;2bWU znh6MzNwKRl#-8JEF;P=u4KCV`_}I~%C@Mx-tIjmQdt$fzn?cXXFsn1 z5Z{+MK@IY+m4(tjkAg>cNU#q#yW)XtoR}Y zY}#1xuVZT6_f5IX59jKfGjc8XVawUey0pF4y5GeYj!V|tPNFr$Wvds;p??*Mp!fgJ zZ4e4_GtEF=S@L*8vNVQ^61oqxee*9bpeclHT%aft>t=_w*+#6P+o;23i3s|#!Vm$) zuVb+<;Cuw0MMyxCuw6yu>`zN2$>tOa3DO>IagGdZk z@a9n1ZN&*gnF`i5swxXb3xDjKTD#lFnE{?v@nf;Mb|oYU231S;MfJBMCw3d>|LZNR zQsx-`5ZVP3^-4nKWBO%nPmt^7rou~JW+V-hnh(PdP&mDXg`2py;hlI&C@fMr!H~Kz z#e}^;^xZ4l+3c3yiiAz!Q$U%MBA~;*Xy}i}>=OgQkK-y1q$bCAoqrV4gxfF;fumaGwi@9?^+YhN-xaCAy>-^z?N@sktv14UF1-G}MddRCTL?*P_B^mJEHz?sV8D zxX$9-C15VuN74SI$7WFcq48U0$E@=p zS47vhbSOwcTu}Gg8sQ&fpZu3txUY)vf$I)qf8huloM_)d^M5!k)m?f$N)9`ioLoM# zCO+*O_q!kCm72-HY@8Va$OuiSZkL-z*AbXgheDa3fC1fMbD+R-a)v4;b`U=(jJ04E z^x^o#n)jk%GutjR{}tB#Um!w;q=pTa0X*0IaQT-ac!-=h#sMu1bo9XApc~{otri-+ zKT7K}d$yTZ7k_ANvY9!ELuD1+4n2hUCYLxeBep1ka4W#ijr!ZTvwHfX{@Cy7nK80> zSC(+(PdY5Kt>uP0mFRbPeEDmz12N#Yt~;mB-D>HS=CYbY(sRtG-=jg|N|84UcdG{^ zg%$)cB`_lf2`Yw2hW8Bt2^BFG1Qe}fvb`arhn7L*!aSz+UiwLab-c$gF)$%82?hl# t4g&%j1povTwF!~lvMg%fdo=O`D_)qpy6AwR1PI1vV&|}<-mC%w2mnyhZqWb$ delta 2271 zcmV<52q5?666F$*U4L1kWV*R@%bWrN2mpYB1Aq*I`QfA`$HnRZzDKK_4O&vlZIDa# zKMwR!|M}wUdMbT6AqB1Bd|8LKqlW)k-~nakStpu_lT{ipgltWaXUhJ^1j-#l5uCvO zn?rZ%0eOfTh|iOu1xAoN%aa+p{@zAR_BryciCa}w7fJ86t$)=KU;3lljtycr4Ff~$ z2pc+JY1Qg}Rb$i>CkFdk%p6zyCEj%Gs|0t?TC7rRTFCR=8QdNh{6Mbfpii__S6|!N0d6ZTsec**+0byz3{A^l7A_&z4zOFcwRH1i=-1~AjvY{iUAPxK3q2zM1pOBz za)`h1GaWc`%6|sVRk5_B)(6fv6*n`>=X)kdg_110PQ!OZXjrwhWlM~{KlG&8ic)2y z)FOxi2{BS2+d%o=ENtp3A%TDU&=4M%hVrS>PGE1z&wqpv;nv_s{86+ojzPUG5GRwb zPbPJC>81M$%1Rcmi&3TgVl$;`B2j!OV%%*@Da{ny=F4@2>wvZw-enKN6+sFpTja4* zso{;mNC=(hr?AZ@F?XsfBY%s#NfNnzaVQiB^fH+WJp`%uDRL9WCP7Jn8vl4_M z$oOk?`9F~?ique|mXyWps>c{$6v`gl4G>d%$jp;UA4ZWo25NRCbhpP$m*GIyBwfA! zdddYMF~wQOKA;YVT`}z6e&566W<>l-AGh0s?SKgHBX^l+Hmwn$m4zbrdS9@^?n8xsdD6^gL zBk=}O5TVnGLGot>E?ju4GOMz|iN1K|4}X04M||~T|sPnF=2LRkTShHF(*>+7C{o{ELYbns7Yh%D+4tE-S|r zg*1Oah#jSjzP=GXzJklB=6IWx#}NYlSrQ=h(((6OE{RdH>xt`HgL!8nvVUe&WsA+I zGkUH!>Zd0Z7)TK|knPZCg*FWSs|W#cZdHw)A&j%jcr6n+$ydm{Iby-p_HMP$GTMpa z88|U#GZpT7#DK(Mm#iBxk9kz8`Q*FODD3&M{Vs^H7b%zOd%ZT7{b=2g9Qs|Ck$xtm z7g|13hmc!_NShq+E+ZPE<$r6h_OzOq3i`<-KGRBZw!qbe+W>)H_E`;#nEUhu#P#lz zmn!q`Qk~UR^oJR@QNEJ#dc;MJqM|`nd#0?~sPBK34tXpMl!~lU<>tuA0m{5yW6?SZ zONp~o03liB^q`mY?s7j%IalT2Ma3)=)Je}(mscSa3x92ayl>z(Mt``5*sT7peR!n# z@CNBI&689W@7h0g<+pf?oSON}ND ztZ5YH4;OJ9PI1B35V*tHgEVP!3mB*COv<8dW)_>2T8erDb@WE9#

`N}^B>#lxsyRu%f$V+?I-@K>;qHR! zw?c7#GZ6xHS?DL63-mp3W8E3?{8vV z) Date: Wed, 5 Jul 2023 15:23:14 +0200 Subject: [PATCH 107/580] Only schedule a close if the ping was actually sent (#1626) Co-authored-by: George Barnett --- Sources/GRPC/GRPCIdleHandler.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 2690cdcd5..535304e53 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -265,7 +265,9 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { initialDelay: delay, delay: delay ) { _ in - self.handlePingAction(self.pingHandler.pingFired()) + let action = self.pingHandler.pingFired() + if case .none = action { return } + self.handlePingAction(action) // `timeout` is less than `interval`, guaranteeing that the close task // will be fired before a new ping is triggered. assert(timeout < delay, "`timeout` must be less than `interval`") From bf6065fc97c2ead57073d6d556cbb83daa60aa66 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 13 Jul 2023 14:23:03 +0100 Subject: [PATCH 108/580] Add note about SPM plugin known issues, provide better errors (#1629) --- Plugins/GRPCSwiftPlugin/plugin.swift | 28 ++++++++++++---- .../Docs.docc/spm-plugin.md | 33 ++++++++++++------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index 2a07ea21b..a2fda772f 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -20,13 +20,27 @@ import PackagePlugin @main struct GRPCSwiftPlugin { /// Errors thrown by the `GRPCSwiftPlugin` - enum PluginError: Error { + enum PluginError: Error, CustomStringConvertible { /// Indicates that the target where the plugin was applied to was not `SourceModuleTarget`. - case invalidTarget + case invalidTarget(Target) /// Indicates that the file extension of an input file was not `.proto`. - case invalidInputFileExtension + case invalidInputFileExtension(String) /// Indicates that there was no configuration file at the required location. - case noConfigFound + case noConfigFound(String) + + var description: String { + switch self { + case let .invalidTarget(target): + return "Expected a SwiftSourceModuleTarget but got '\(type(of: target))'." + case let .invalidInputFileExtension(path): + return "The input file '\(path)' does not have a '.proto' extension." + case let .noConfigFound(path): + return """ + No configuration file found named '\(path)'. The file must not be listed in the \ + 'exclude:' argument for the target in Package.swift. + """ + } + } } /// The configuration of the plugin. @@ -88,7 +102,7 @@ struct GRPCSwiftPlugin { $0.path.lastComponent == Self.configurationFileName } )?.path else { - throw PluginError.noConfigFound + throw PluginError.noConfigFound(Self.configurationFileName) } let data = try Data(contentsOf: URL(fileURLWithPath: "\(configurationFilePath)")) @@ -208,7 +222,7 @@ struct GRPCSwiftPlugin { for invocation in configuration.invocations { for protoFile in invocation.protoFiles { if !protoFile.hasSuffix(".proto") { - throw PluginError.invalidInputFileExtension + throw PluginError.invalidInputFileExtension(protoFile) } } } @@ -221,7 +235,7 @@ extension GRPCSwiftPlugin: BuildToolPlugin { target: Target ) async throws -> [Command] { guard let swiftTarget = target as? SwiftSourceModuleTarget else { - throw PluginError.invalidTarget + throw PluginError.invalidTarget(target) } return try self.createBuildCommands( pluginWorkDirectory: context.pluginWorkDirectory, diff --git a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md index fb994630c..807b766fe 100644 --- a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md +++ b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md @@ -58,12 +58,12 @@ Here's an example file structure that looks like this: ```text Sources ├── main.swift -├── ProtoBuf +└── ProtoBuf ├── grpc-swift-config.json ├── foo.proto └── Bar └── Bar.proto -``` +``` ```json { @@ -103,19 +103,19 @@ the `keepMethodCasing` option to false, which means that the casing of the autog ### Defining the path to the protoc binary -The plugin needs to be able to invoke the `protoc` binary to generate the Swift types. There are several ways to achieve this. +The plugin needs to be able to invoke the `protoc` binary to generate the Swift types. There are several ways to achieve this. -First, by default, the package manager looks into the `$PATH` to find binaries named `protoc`. -This works immediately if you use `swift build` to build your package and `protoc` is installed +First, by default, the package manager looks into the `$PATH` to find binaries named `protoc`. +This works immediately if you use `swift build` to build your package and `protoc` is installed in the `$PATH` (`brew` is adding it to your `$PATH` automatically). However, this doesn't work if you want to compile from Xcode since Xcode is not passed the `$PATH`. -If compiling from Xcode, you have **three options** to set the path of `protoc` that the plugin is going to use: +If compiling from Xcode, you have **three options** to set the path of `protoc` that the plugin is going to use: * Set an environment variable `PROTOC_PATH` that gets picked up by the plugin. Here are two examples of how you can achieve this: ```shell -# swift build +# swift build env PROTOC_PATH=/opt/homebrew/bin/protoc swift build # To start Xcode (Xcode MUST NOT be running before invoking this) @@ -134,9 +134,18 @@ env PROTOC_PATH=/opt/homebrew/bin/protoc xcodebuild } ``` -> Warning: The configuration file option only solves the problem for leaf packages that are using the Swift package manager -plugin since there you can point the package manager to the right binary. The environment variable -does solve the problem for transitive packages as well; however, it requires your users to set -the variable now. In general we advise against adopting the plugin as a non-leaf package! +* You can start Xcode by running `$ xed .` from the command line from the directory your project is located - this should make `$PATH` visible to Xcode. + +### Known Issues + +- The configuration file _must not_ be excluded from the list of sources for the + target in the package manifest (that is, it should not be present in the + `exclude` argument for the target). The build system does not have access to + the file if it is excluded, however, `swift build` will result in a warning + that the file should be excluded. +- The plugin should only be used for leaf package. The configuration file option + only solves the problem for leaf packages that are using the Swift package + manager plugin since there you can point the package manager to the right + binary. The environment variable does solve the problem for transitive + packages as well; however, it requires your users to set the variable now. -* You can start Xcode by running `$ xed .` from the command line from the directory your project is located - this should make `$PATH` visible to Xcode. From 5e3e3f577fbbe46f9a1c05233e7a378ee53187ca Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Tue, 8 Aug 2023 12:12:03 +0100 Subject: [PATCH 109/580] Add bind and connect APIs for VSOCK sockets (#1636) Motivation: The VSOCK address family facilitates communication between virtual machines and the host they are running that need a communications channel that is independent of virtual machine network configuration. While both GRPC has support for building channels using sockets that were created out of band (`withConnectedSocket(_:)` and `withBoundSocket(_:)`, there are no APIs that facilitate the creation of VSOCK-based channels. https://github.com/apple/swift-nio/pull/2479 adds a `VsockAddress` type and associated bootstrap APIs, but these need corresponding APIs here to be used in GRPC clients and servers. Modifications: - Add `ConnectionTarget.vsockAddress` and `BindTarget.vsockAddress `. - Add `ClientBootstrapProtocol connect(to vsockAddress:)` and `ServerBootstrapProtocol.bind(to vsockAddress:)`. - Add `Server.bind(vsockAddress: VsockAddress)`. Signed-off-by: Si Beaumont --- Package.swift | 2 +- Sources/GRPC/ClientConnection.swift | 11 +++++ Sources/GRPC/PlatformSupport.swift | 12 ++++- Sources/GRPC/Server.swift | 3 ++ Sources/GRPC/ServerBuilder.swift | 7 +++ Tests/GRPCTests/VsockSocketTests.swift | 67 ++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 Tests/GRPCTests/VsockSocketTests.swift diff --git a/Package.swift b/Package.swift index 473622c25..6e3f2f007 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.42.0" + from: "2.58.0" ), .package( url: "https://github.com/apple/swift-nio-http2.git", diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index e3ac756e0..ef997ad30 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -23,6 +23,7 @@ import Logging import NIOCore import NIOHPACK import NIOHTTP2 +import NIOPosix #if canImport(NIOSSL) import NIOSSL #endif @@ -269,6 +270,7 @@ public struct ConnectionTarget: Sendable { case unixDomainSocket(String) case socketAddress(SocketAddress) case connectedSocket(NIOBSDSocket.Handle) + case vsockAddress(VsockAddress) } internal var wrapped: Wrapped @@ -301,6 +303,11 @@ public struct ConnectionTarget: Sendable { return ConnectionTarget(.connectedSocket(socket)) } + /// A vsock socket. + public static func vsockAddress(_ vsockAddress: VsockAddress) -> ConnectionTarget { + return ConnectionTarget(.vsockAddress(vsockAddress)) + } + @usableFromInline var host: String { switch self.wrapped { @@ -312,6 +319,8 @@ public struct ConnectionTarget: Sendable { return address.host case .unixDomainSocket, .socketAddress(.unixDomainSocket), .connectedSocket: return "localhost" + case let .vsockAddress(address): + return "vsock://\(address.cid)" } } } @@ -552,6 +561,8 @@ extension ClientBootstrapProtocol { return self.connect(to: address) case let .connectedSocket(socket): return self.withConnectedSocket(socket) + case let .vsockAddress(address): + return self.connect(to: address) } } } diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift index 6f9b1a163..0ecfcd97d 100644 --- a/Sources/GRPC/PlatformSupport.swift +++ b/Sources/GRPC/PlatformSupport.swift @@ -118,6 +118,7 @@ public protocol ClientBootstrapProtocol { func connect(host: String, port: Int) -> EventLoopFuture func connect(unixDomainSocketPath: String) -> EventLoopFuture func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture + func connect(to vsockAddress: VsockAddress) -> EventLoopFuture func connectTimeout(_ timeout: TimeAmount) -> Self func channelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption @@ -144,6 +145,10 @@ extension NIOTSConnectionBootstrap: ClientBootstrapProtocol { public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture { preconditionFailure("NIOTSConnectionBootstrap does not support withConnectedSocket(_:)") } + + public func connect(to vsockAddress: VsockAddress) -> EventLoopFuture { + preconditionFailure("NIOTSConnectionBootstrap does not support connect(to vsockAddress:)") + } } #endif @@ -154,6 +159,7 @@ public protocol ServerBootstrapProtocol { func bind(host: String, port: Int) -> EventLoopFuture func bind(unixDomainSocketPath: String) -> EventLoopFuture func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture + func bind(to vsockAddress: VsockAddress) -> EventLoopFuture #if swift(>=5.7) @preconcurrency @@ -189,7 +195,11 @@ extension ServerBootstrap: ServerBootstrapProtocol {} @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) extension NIOTSListenerBootstrap: ServerBootstrapProtocol { public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture { - preconditionFailure("NIOTSListenerBootstrap does not support withConnectedSocket(_:)") + preconditionFailure("NIOTSListenerBootstrap does not support withBoundSocket(_:)") + } + + public func bind(to vsockAddress: VsockAddress) -> EventLoopFuture { + preconditionFailure("NIOTSListenerBootstrap does not support bind(to vsockAddress:)") } } #endif diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 7b16c9232..5ba505954 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -538,6 +538,9 @@ extension ServerBootstrapProtocol { case let .connectedSocket(socket): return self.withBoundSocket(socket) + + case let .vsockAddress(address): + return self.bind(to: address) } } } diff --git a/Sources/GRPC/ServerBuilder.swift b/Sources/GRPC/ServerBuilder.swift index 1600335c8..8f98520ee 100644 --- a/Sources/GRPC/ServerBuilder.swift +++ b/Sources/GRPC/ServerBuilder.swift @@ -15,6 +15,7 @@ */ import Logging import NIOCore +import NIOPosix #if canImport(Network) import Security @@ -60,6 +61,12 @@ extension Server { self.configuration.tlsConfiguration = self.maybeTLS return Server.start(configuration: self.configuration) } + + public func bind(vsockAddress: VsockAddress) -> EventLoopFuture { + self.configuration.target = .vsockAddress(vsockAddress) + self.configuration.tlsConfiguration = self.maybeTLS + return Server.start(configuration: self.configuration) + } } } diff --git a/Tests/GRPCTests/VsockSocketTests.swift b/Tests/GRPCTests/VsockSocketTests.swift new file mode 100644 index 000000000..f9bc09c30 --- /dev/null +++ b/Tests/GRPCTests/VsockSocketTests.swift @@ -0,0 +1,67 @@ +/* + * Copyright 2022, 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 EchoImplementation +import EchoModel +import GRPC +import NIOPosix +import XCTest + +class VsockSocketTests: GRPCTestCase { + func testVsockSocket() throws { + try XCTSkipUnless(self.vsockAvailable(), "Vsock unavailable") + let group = NIOPosix.MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + // Setup a server. + let server = try Server.insecure(group: group) + .withServiceProviders([EchoProvider()]) + .withLogger(self.serverLogger) + .bind(vsockAddress: .init(cid: .any, port: 31234)) + .wait() + defer { + XCTAssertNoThrow(try server.close().wait()) + } + + let channel = try GRPCChannelPool.with( + target: .vsockAddress(.init(cid: .local, port: 31234)), + transportSecurity: .plaintext, + eventLoopGroup: group + ) + defer { + XCTAssertNoThrow(try channel.close().wait()) + } + + let client = Echo_EchoNIOClient(channel: channel) + let resp = try client.get(Echo_EchoRequest(text: "Hello")).response.wait() + XCTAssertEqual(resp.text, "Swift echo get: Hello") + } + + private func vsockAvailable() -> Bool { + let fd: CInt + #if os(Linux) + fd = socket(AF_VSOCK, CInt(SOCK_STREAM.rawValue), 0) + #elseif canImport(Darwin) + fd = socket(AF_VSOCK, SOCK_STREAM, 0) + #else + fd = -1 + #endif + if fd == -1 { return false } + precondition(close(fd) == 0) + return true + } +} From 735d88f8196467f24189896f29c4625af7cbd072 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 8 Aug 2023 12:42:15 +0100 Subject: [PATCH 110/580] Bump version number to 1.19.0 (#1637) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.19.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 2973797d5..03264a81a 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 18 + internal static let minor = 19 /// The patch version. internal static let patch = 0 From 666e30dff378971940a7c3a19005db1a1bffe726 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 14 Aug 2023 18:18:31 +0100 Subject: [PATCH 111/580] Update SwiftFormat version (#1639) Motivation: Some changes cause the formatter to throw an error while formatting. This particular bug was fixed in 0.52.0. Modifications: - Update the formatter version to 0.52.0 - Update the rules applied to minimise the diff from updating - Run the formatter Results: - Formatter is up-to-date. --- .github/workflows/ci.yaml | 2 +- .swiftformat | 17 +++ .../Sources/Launch/AppDelegate.swift | 2 +- .../SpeechToText/Sources/ViewController.swift | 4 +- Package.swift | 2 +- .../Async/AsyncPingPongRequestMaker.swift | 2 +- .../StreamingResponseCallContext.swift | 4 +- .../Benchmarks/UnaryThroughput.swift | 2 +- .../ClientInterceptorPipelineTests.swift | 6 +- .../ConnectionPool/GRPCChannelPoolTests.swift | 2 +- .../GRPCTests/GRPCAsyncClientCallTests.swift | 4 +- Tests/GRPCTests/GRPCPingHandlerTests.swift | 2 +- .../GRPCWebToHTTP2ServerCodecTests.swift | 20 ++-- .../ServerInterceptorPipelineTests.swift | 14 +-- Tests/GRPCTests/ServerInterceptorTests.swift | 20 ++-- Tests/GRPCTests/ServerThrowingTests.swift | 4 +- Tests/GRPCTests/ServerWebTests.swift | 2 +- Tests/GRPCTests/UnaryServerHandlerTests.swift | 108 +++++++++--------- Tests/GRPCTests/UserInfoTests.swift | 8 +- Tests/GRPCTests/XCTestHelpers.swift | 4 +- scripts/format.sh | 2 +- 21 files changed, 125 insertions(+), 106 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7edf580e9..5c8e754de 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v3 - name: "Formatting and License Headers check" run: | - SWIFTFORMAT_VERSION=0.49.4 + SWIFTFORMAT_VERSION=0.52.0 git clone --depth 1 --branch "$SWIFTFORMAT_VERSION" "https://github.com/nicklockwood/SwiftFormat" "$HOME/SwiftFormat" swift build -c release --package-path "$HOME/SwiftFormat" --product swiftformat export PATH=$PATH:"$(swift build -c release --package-path "$HOME/SwiftFormat" --show-bin-path)" diff --git a/.swiftformat b/.swiftformat index 9410761e8..f18a823ae 100644 --- a/.swiftformat +++ b/.swiftformat @@ -41,3 +41,20 @@ # Put ACLs on declarations within an extension rather than the extension itself. --extensionacl on-declarations + +# Don't remove internal ACLs +--disable redundantInternal + +# Don't remove redundant parenstheses, because no all of them are redundant. +--disable redundantParens + +# Don't remove static Self +--disable redundantStaticSelf + +# Hoisting try and await causes a bunch of issues (and churn) in 0.52.0. Disable +# them for the time being. +--disable hoistTry +--disable hoistAwait + +# Disabled as enabling causes a lot of churn. +--disable wrapSingleLineComments diff --git a/Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift b/Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift index 0ec34cc1b..4d4a6a931 100644 --- a/Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift +++ b/Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift @@ -16,7 +16,7 @@ import UIKit -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, diff --git a/Examples/Google/SpeechToText/Sources/ViewController.swift b/Examples/Google/SpeechToText/Sources/ViewController.swift index 6b4d283f3..f1c4451ef 100644 --- a/Examples/Google/SpeechToText/Sources/ViewController.swift +++ b/Examples/Google/SpeechToText/Sources/ViewController.swift @@ -27,7 +27,7 @@ final class ViewController: UIViewController { button.backgroundColor = .darkGray button.layer.cornerRadius = 15 button.clipsToBounds = true - button.addTarget(self, action: #selector(recordTapped), for: .touchUpInside) + button.addTarget(self, action: #selector(self.recordTapped), for: .touchUpInside) return button }() @@ -110,7 +110,7 @@ final class ViewController: UIViewController { self.textView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide.snp.topMargin) make.left.right.equalToSuperview() - make.bottom.equalTo(recordButton.snp.top) + make.bottom.equalTo(self.recordButton.snp.top) } self.recordButton.snp.makeConstraints { make in diff --git a/Package.swift b/Package.swift index 6e3f2f007..393599af8 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ */ import PackageDescription // swiftformat puts the next import before the tools version. -// swiftformat:disable:next sortedImports +// swiftformat:disable:next sortImports import class Foundation.ProcessInfo let grpcPackageName = "grpc-swift" diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift index ae497d80c..c5df08cd5 100644 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift +++ b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift @@ -68,7 +68,7 @@ final class AsyncPingPongRequestMaker: AsyncRequestMaker, @unchecked Sendable { while !self.stopRequested.load(ordering: .relaxed), self.messagesPerStream == 0 || messagesSent < self.messagesPerStream { try await streamingCall.requestStream.send(self.requestMessage) - let _ = try await responseStream.next() + _ = try await responseStream.next() let endTime = grpcTimeNow() self.stats.add(latency: endTime - startTime) messagesSent += 1 diff --git a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift b/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift index 1dcfa6aa4..a8ededb5e 100644 --- a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift +++ b/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift @@ -237,7 +237,9 @@ internal final class _StreamingResponseCallContext: /// Concrete implementation of `StreamingResponseCallContext` used for testing. /// /// Simply records all sent messages. -open class StreamingResponseCallContextTestStub: StreamingResponseCallContext { +open class StreamingResponseCallContextTestStub: StreamingResponseCallContext< + ResponsePayload +> { open var recordedResponses: [ResponsePayload] = [] override open func sendResponse( diff --git a/Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift b/Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift index 8ee6b46ad..1fdf58fc0 100644 --- a/Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift @@ -79,7 +79,7 @@ class Unary: ServerProvidingBenchmark { let upperBound = min(lowerBound + batchSize, self.requestCount) let requests = (lowerBound ..< upperBound).map { _ in - client.get(Echo_EchoRequest.with { $0.text = self.requestText }).response + self.client.get(Echo_EchoRequest.with { $0.text = self.requestText }).response } messages += requests.count diff --git a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift index 3e2e08283..0804d22c8 100644 --- a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift @@ -159,7 +159,7 @@ class ClientInterceptorPipelineTests: GRPCTestCase { assertThat(cancelled, .is(false)) cancelled = true // We don't expect a promise: this cancellation is fired by the pipeline. - assertThat(promise, .is(.nil())) + assertThat(promise, .is(.none())) }, onRequestPart: { _, _ in XCTFail("Unexpected request part") @@ -202,14 +202,14 @@ class ClientInterceptorPipelineTests: GRPCTestCase { assertThat(cancellations, .is(0)) cancellations += 1 // We don't expect a promise: this cancellation is fired by the pipeline. - assertThat(promise, .is(.nil())) + assertThat(promise, .is(.none())) }, onRequestPart: { _, _ in XCTFail("Unexpected request part") }, onResponsePart: { part in // We only expect the end. - assertThat(part.end, .is(.notNil())) + assertThat(part.end, .is(.some())) } ) diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index 8fbb6b2b9..7edc6e564 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -303,7 +303,7 @@ final class GRPCChannelPoolTests: GRPCTestCase { // If we express no event loop preference then we should not get the loaded loop. let indifferentLoopRPCs = (1 ... 10).map { - _ in echo.get(.with { $0.text = "" }) + _ in self.echo.get(.with { $0.text = "" }) } XCTAssert(indifferentLoopRPCs.map { $0.eventLoop }.allSatisfy { $0 !== loop }) diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift index 647db7370..2754ba32d 100644 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift @@ -160,12 +160,12 @@ class GRPCAsyncClientCallTests: GRPCTestCase { var responseStreamIterator = update.responseStream.makeAsyncIterator() for word in ["boyle", "jeffers", "holt"] { try await update.requestStream.send(.with { $0.text = word }) - await assertThat(try await responseStreamIterator.next(), .is(.notNil())) + await assertThat(try await responseStreamIterator.next(), .is(.some())) } update.requestStream.finish() - await assertThat(try await responseStreamIterator.next(), .is(.nil())) + await assertThat(try await responseStreamIterator.next(), .is(.none())) await assertThat(try await update.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) await assertThat(await update.status, .hasCode(.ok)) diff --git a/Tests/GRPCTests/GRPCPingHandlerTests.swift b/Tests/GRPCTests/GRPCPingHandlerTests.swift index 6d86ef0b2..1434c4273 100644 --- a/Tests/GRPCTests/GRPCPingHandlerTests.swift +++ b/Tests/GRPCTests/GRPCPingHandlerTests.swift @@ -387,7 +387,7 @@ extension PingHandler.Action: Equatable { extension GRPCPingHandlerTests { func testSingleAckIsEmittedOnPing() throws { let client = EmbeddedChannel() - let _ = try client.configureHTTP2Pipeline(mode: .client) { _ in + _ = try client.configureHTTP2Pipeline(mode: .client) { _ in fatalError("Unexpected inbound stream") }.wait() diff --git a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift index 72f144ff6..1e1b81a82 100644 --- a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift +++ b/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift @@ -47,7 +47,7 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { ) assertThat(try channel.writeInbound(HTTPServerRequestPart.head(head)), .doesNotThrow()) let headersPayload = try channel.readInbound(as: HTTP2Frame.FramePayload.self) - assertThat(headersPayload, .notNil(.headers(.contains(":path", [path])))) + assertThat(headersPayload, .some(.headers(.contains(":path", [path])))) } private func receiveBytes( @@ -59,14 +59,14 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { if let expectedBytes = expectedBytes { let dataPayload = try channel.readInbound(as: HTTP2Frame.FramePayload.self) - assertThat(dataPayload, .notNil(.data(buffer: ByteBuffer(bytes: expectedBytes)))) + assertThat(dataPayload, .some(.data(buffer: ByteBuffer(bytes: expectedBytes)))) } } private func receiveEnd(on channel: EmbeddedChannel) throws { assertThat(try channel.writeInbound(HTTPServerRequestPart.end(nil)), .doesNotThrow()) let dataEndPayload = try channel.readInbound(as: HTTP2Frame.FramePayload.self) - assertThat(dataEndPayload, .notNil(.data(buffer: ByteBuffer(), endStream: true))) + assertThat(dataEndPayload, .some(.data(buffer: ByteBuffer(), endStream: true))) } private func sendResponseHeaders(on channel: EmbeddedChannel) throws { @@ -74,7 +74,7 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { let headerPayload: HTTP2Frame.FramePayload = .headers(.init(headers: responseHeaders)) assertThat(try channel.writeOutbound(headerPayload), .doesNotThrow()) let responseHead = try channel.readOutbound(as: HTTPServerResponsePart.self) - assertThat(responseHead, .notNil(.head(status: .ok))) + assertThat(responseHead, .some(.head(status: .ok))) } private func sendTrailersOnlyResponse(on channel: EmbeddedChannel) throws { @@ -83,9 +83,9 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { assertThat(try channel.writeOutbound(headerPayload), .doesNotThrow()) let responseHead = try channel.readOutbound(as: HTTPServerResponsePart.self) - assertThat(responseHead, .notNil(.head(status: .ok))) + assertThat(responseHead, .some(.head(status: .ok))) let end = try channel.readOutbound(as: HTTPServerResponsePart.self) - assertThat(end, .notNil(.end())) + assertThat(end, .some(.end())) } private func sendBytes( @@ -99,9 +99,9 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { if let expectedBytes = expectedBytes { let expectedBuffer = ByteBuffer(bytes: expectedBytes) - assertThat(try channel.readOutbound(), .notNil(.body(.is(expectedBuffer)))) + assertThat(try channel.readOutbound(), .some(.body(.is(expectedBuffer)))) } else { - assertThat(try channel.readOutbound(as: HTTPServerResponsePart.self), .doesNotThrow(.nil())) + assertThat(try channel.readOutbound(as: HTTPServerResponsePart.self), .doesNotThrow(.none())) } } @@ -115,10 +115,10 @@ class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { assertThat(try channel.writeOutbound(headersPayload), .doesNotThrow()) if let expectedBytes = expectedBytes { - assertThat(try channel.readOutbound(), .notNil(.body(.is(expectedBytes)))) + assertThat(try channel.readOutbound(), .some(.body(.is(expectedBytes)))) } - assertThat(try channel.readOutbound(), .notNil(.end())) + assertThat(try channel.readOutbound(), .some(.end())) } func testWebBinaryHappyPath() throws { diff --git a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift index 9143e0339..1f594278e 100644 --- a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift @@ -60,7 +60,7 @@ class ServerInterceptorPipelineTests: GRPCTestCase { onRequestPart: { requestParts.append($0) }, onResponsePart: { part, promise in responseParts.append(part) - assertThat(promise, .is(.nil())) + assertThat(promise, .is(.none())) } ) @@ -80,7 +80,7 @@ class ServerInterceptorPipelineTests: GRPCTestCase { assertThat(responseParts, .hasCount(3)) assertThat(responseParts[0].metadata, .is([:])) assertThat(responseParts[1].message, .is("bar")) - assertThat(responseParts[2].end, .is(.notNil())) + assertThat(responseParts[2].end, .is(.some())) // Pipelines should now be closed. We can't send or receive. let p = self.embeddedEventLoop.makePromise(of: Void.self) @@ -110,15 +110,15 @@ class ServerInterceptorPipelineTests: GRPCTestCase { // Check the request parts are there. assertThat(recorder.requestParts, .hasCount(3)) - assertThat(recorder.requestParts[0].metadata, .is(.notNil())) - assertThat(recorder.requestParts[1].message, .is(.notNil())) + assertThat(recorder.requestParts[0].metadata, .is(.some())) + assertThat(recorder.requestParts[1].message, .is(.some())) assertThat(recorder.requestParts[2].isEnd, .is(true)) // Check the response parts are there. assertThat(recorder.responseParts, .hasCount(3)) - assertThat(recorder.responseParts[0].metadata, .is(.notNil())) - assertThat(recorder.responseParts[1].message, .is(.notNil())) - assertThat(recorder.responseParts[2].end, .is(.notNil())) + assertThat(recorder.responseParts[0].metadata, .is(.some())) + assertThat(recorder.responseParts[1].message, .is(.some())) + assertThat(recorder.responseParts[2].end, .is(.some())) } } diff --git a/Tests/GRPCTests/ServerInterceptorTests.swift b/Tests/GRPCTests/ServerInterceptorTests.swift index 9cc090446..df77d82cf 100644 --- a/Tests/GRPCTests/ServerInterceptorTests.swift +++ b/Tests/GRPCTests/ServerInterceptorTests.swift @@ -98,9 +98,9 @@ class ServerInterceptorTests: GRPCTestCase { handler.receiveEnd() // Expect responses. - assertThat(self.recorder.metadata, .is(.notNil())) + assertThat(self.recorder.metadata, .is(.some())) assertThat(self.recorder.messages.count, .is(1)) - assertThat(self.recorder.status, .is(.notNil())) + assertThat(self.recorder.status, .is(.some())) // We expect 2 request parts: the provider responds before it sees end, that's fine. assertThat(recordingInterceptor.requestParts, .hasCount(2)) @@ -123,9 +123,9 @@ class ServerInterceptorTests: GRPCTestCase { handler.receiveEnd() // Get the responses. - assertThat(self.recorder.metadata, .is(.notNil())) + assertThat(self.recorder.metadata, .is(.some())) assertThat(self.recorder.messages.count, .is(1)) - assertThat(self.recorder.status, .is(.notNil())) + assertThat(self.recorder.status, .is(.some())) } func testClientStreamingFromInterceptor() throws { @@ -140,9 +140,9 @@ class ServerInterceptorTests: GRPCTestCase { handler.receiveEnd() // Get the responses. - assertThat(self.recorder.metadata, .is(.notNil())) + assertThat(self.recorder.metadata, .is(.some())) assertThat(self.recorder.messages.count, .is(1)) - assertThat(self.recorder.status, .is(.notNil())) + assertThat(self.recorder.status, .is(.some())) } func testServerStreamingFromInterceptor() throws { @@ -155,9 +155,9 @@ class ServerInterceptorTests: GRPCTestCase { handler.receiveEnd() // Get the responses. - assertThat(self.recorder.metadata, .is(.notNil())) + assertThat(self.recorder.metadata, .is(.some())) assertThat(self.recorder.messages.count, .is(3)) - assertThat(self.recorder.status, .is(.notNil())) + assertThat(self.recorder.status, .is(.some())) } func testBidirectionalStreamingFromInterceptor() throws { @@ -172,9 +172,9 @@ class ServerInterceptorTests: GRPCTestCase { handler.receiveEnd() // Get the responses. - assertThat(self.recorder.metadata, .is(.notNil())) + assertThat(self.recorder.metadata, .is(.some())) assertThat(self.recorder.messages.count, .is(3)) - assertThat(self.recorder.status, .is(.notNil())) + assertThat(self.recorder.status, .is(.some())) } } diff --git a/Tests/GRPCTests/ServerThrowingTests.swift b/Tests/GRPCTests/ServerThrowingTests.swift index 55ccf6939..2d943810d 100644 --- a/Tests/GRPCTests/ServerThrowingTests.swift +++ b/Tests/GRPCTests/ServerThrowingTests.swift @@ -150,7 +150,7 @@ class ServerThrowingTests: EchoTestCaseBase { } } XCTAssertThrowsError(try call.response.wait()) { - XCTAssertEqual(expectedError, $0 as? GRPCStatus) + XCTAssertEqual(self.expectedError, $0 as? GRPCStatus) } } @@ -170,7 +170,7 @@ class ServerThrowingTests: EchoTestCaseBase { // With `ErrorReturningEchoProvider` we actually _return_ a response, which means that the `response` future // will _not_ fail, so in that case this test doesn't apply. XCTAssertThrowsError(try call.response.wait()) { - XCTAssertEqual(expectedError, $0 as? GRPCStatus) + XCTAssertEqual(self.expectedError, $0 as? GRPCStatus) } } } diff --git a/Tests/GRPCTests/ServerWebTests.swift b/Tests/GRPCTests/ServerWebTests.swift index f5eeed860..a2ebef8b3 100644 --- a/Tests/GRPCTests/ServerWebTests.swift +++ b/Tests/GRPCTests/ServerWebTests.swift @@ -157,7 +157,7 @@ extension ServerWebTests { var expectedData = Data() var index = 0 message.split(separator: " ").forEach { component in - expectedData.append(gRPCEncodedEchoRequest("Swift echo expand (\(index)): \(component)")) + expectedData.append(self.gRPCEncodedEchoRequest("Swift echo expand (\(index)): \(component)")) index += 1 } expectedData.append(self.gRPCWebTrailers()) diff --git a/Tests/GRPCTests/UnaryServerHandlerTests.swift b/Tests/GRPCTests/UnaryServerHandlerTests.swift index e6df95817..2dcd06641 100644 --- a/Tests/GRPCTests/UnaryServerHandlerTests.swift +++ b/Tests/GRPCTests/UnaryServerHandlerTests.swift @@ -149,7 +149,7 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { assertThat(self.recorder.messages.first, .is(buffer)) assertThat(self.recorder.messageMetadata.first?.compress, .is(false)) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) assertThat(self.recorder.trailers, .is([:])) } @@ -199,7 +199,7 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testThrowingSerializer() { @@ -219,7 +219,7 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveEnd() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testUserFunctionReturnsFailedFuture() { @@ -234,7 +234,7 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.status?.message, .is(":(")) } @@ -242,9 +242,9 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { let handler = self.makeHandler(function: self.neverCalled(_:context:)) handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testReceiveMultipleHeaders() { @@ -255,7 +255,7 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMetadata([:]) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testReceiveMultipleMessages() { @@ -271,17 +271,17 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testFinishBeforeStarting() { let handler = self.makeHandler(function: self.neverCalled(_:context:)) handler.finish() - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.nil())) - assertThat(self.recorder.trailers, .is(.nil())) + assertThat(self.recorder.status, .is(.none())) + assertThat(self.recorder.trailers, .is(.none())) } func testFinishAfterHeaders() { @@ -292,7 +292,7 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } @@ -304,7 +304,7 @@ class UnaryServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } } @@ -376,7 +376,7 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3"))) assertThat(self.recorder.messageMetadata.first?.compress, .is(false)) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) assertThat(self.recorder.trailers, .is([:])) } @@ -430,7 +430,7 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testThrowingSerializer() { @@ -450,7 +450,7 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveEnd() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testObserverFactoryReturnsFailedFuture() { @@ -460,7 +460,7 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMetadata([:]) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.status?.message, .is(":(")) } @@ -485,7 +485,7 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveEnd() assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3 4 5"))) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) } func testDelayedObserverFactoryAllMessagesBeforeSucceeding() { @@ -506,16 +506,16 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { promise.succeed(()) assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3"))) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) } func testReceiveMessageBeforeHeaders() { let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testReceiveMultipleHeaders() { @@ -526,17 +526,17 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMetadata([:]) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testFinishBeforeStarting() { let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) handler.finish() - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.nil())) - assertThat(self.recorder.trailers, .is(.nil())) + assertThat(self.recorder.status, .is(.none())) + assertThat(self.recorder.trailers, .is(.none())) } func testFinishAfterHeaders() { @@ -547,7 +547,7 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } @@ -559,7 +559,7 @@ class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } } @@ -620,7 +620,7 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { .is([ByteBuffer(string: "a"), ByteBuffer(string: "b")]) ) assertThat(self.recorder.messageMetadata.map { $0.compress }, .is([false, false])) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) assertThat(self.recorder.trailers, .is([:])) } @@ -670,7 +670,7 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testThrowingSerializer() { @@ -690,7 +690,7 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveEnd() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testUserFunctionReturnsFailedFuture() { @@ -705,7 +705,7 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.status?.message, .is(":(")) } @@ -713,9 +713,9 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { let handler = self.makeHandler(userFunction: self.neverCalled(_:context:)) handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testReceiveMultipleHeaders() { @@ -726,7 +726,7 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMetadata([:]) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testReceiveMultipleMessages() { @@ -742,17 +742,17 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testFinishBeforeStarting() { let handler = self.makeHandler(userFunction: self.neverCalled(_:context:)) handler.finish() - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.nil())) - assertThat(self.recorder.trailers, .is(.nil())) + assertThat(self.recorder.status, .is(.none())) + assertThat(self.recorder.trailers, .is(.none())) } func testFinishAfterHeaders() { @@ -763,7 +763,7 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } @@ -775,7 +775,7 @@ class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } } @@ -849,7 +849,7 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { .is([ByteBuffer(string: "1"), ByteBuffer(string: "2"), ByteBuffer(string: "3")]) ) assertThat(self.recorder.messageMetadata.map { $0.compress }, .is([false, false, false])) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) assertThat(self.recorder.trailers, .is([:])) } @@ -909,7 +909,7 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMessage(buffer) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testThrowingSerializer() { @@ -929,7 +929,7 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveEnd() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testObserverFactoryReturnsFailedFuture() { @@ -939,7 +939,7 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMetadata([:]) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.status?.message, .is(":(")) } @@ -964,7 +964,7 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { self.recorder.messages, .is([ByteBuffer(string: "1"), ByteBuffer(string: "2")]) ) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) } func testDelayedObserverFactoryAllMessagesBeforeSucceeding() { @@ -987,16 +987,16 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { self.recorder.messages, .is([ByteBuffer(string: "1"), ByteBuffer(string: "2")]) ) - assertThat(self.recorder.status, .notNil(.hasCode(.ok))) + assertThat(self.recorder.status, .some(.hasCode(.ok))) } func testReceiveMessageBeforeHeaders() { let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testReceiveMultipleHeaders() { @@ -1007,17 +1007,17 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.receiveMetadata([:]) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.internalError))) + assertThat(self.recorder.status, .some(.hasCode(.internalError))) } func testFinishBeforeStarting() { let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) handler.finish() - assertThat(self.recorder.metadata, .is(.nil())) + assertThat(self.recorder.metadata, .is(.none())) assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.nil())) - assertThat(self.recorder.trailers, .is(.nil())) + assertThat(self.recorder.status, .is(.none())) + assertThat(self.recorder.trailers, .is(.none())) } func testFinishAfterHeaders() { @@ -1028,7 +1028,7 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } @@ -1040,7 +1040,7 @@ class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { handler.finish() assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "hello"))) - assertThat(self.recorder.status, .notNil(.hasCode(.unavailable))) + assertThat(self.recorder.status, .some(.hasCode(.unavailable))) assertThat(self.recorder.trailers, .is([:])) } } diff --git a/Tests/GRPCTests/UserInfoTests.swift b/Tests/GRPCTests/UserInfoTests.swift index 8e1bc14dd..e9546ecd9 100644 --- a/Tests/GRPCTests/UserInfoTests.swift +++ b/Tests/GRPCTests/UserInfoTests.swift @@ -26,10 +26,10 @@ class UserInfoTests: GRPCTestCase { assertThat(userInfo[BarKey.self], .is(42)) userInfo[FooKey.self] = nil - assertThat(userInfo[FooKey.self], .is(.nil())) + assertThat(userInfo[FooKey.self], .is(.none())) userInfo[BarKey.self] = nil - assertThat(userInfo[BarKey.self], .is(.nil())) + assertThat(userInfo[BarKey.self], .is(.none())) } func testWithExtensions() { @@ -42,10 +42,10 @@ class UserInfoTests: GRPCTestCase { assertThat(userInfo.bar, .is(42)) userInfo.foo = nil - assertThat(userInfo.foo, .is(.nil())) + assertThat(userInfo.foo, .is(.none())) userInfo.bar = nil - assertThat(userInfo.bar, .is(.nil())) + assertThat(userInfo.bar, .is(.none())) } func testDescription() { diff --git a/Tests/GRPCTests/XCTestHelpers.swift b/Tests/GRPCTests/XCTestHelpers.swift index 5da278d0e..a760b3d2e 100644 --- a/Tests/GRPCTests/XCTestHelpers.swift +++ b/Tests/GRPCTests/XCTestHelpers.swift @@ -133,7 +133,7 @@ struct Matcher { } /// Matches if the value is `nil`. - static func `nil`() -> Matcher { + static func none() -> Matcher { return .init { actual in actual == nil ? .match @@ -142,7 +142,7 @@ struct Matcher { } /// Matches if the value is not `nil`. - static func notNil(_ matcher: Matcher? = nil) -> Matcher { + static func some(_ matcher: Matcher? = nil) -> Matcher { return .init { actual in if let actual = actual { return matcher?.evaluate(actual) ?? .match diff --git a/scripts/format.sh b/scripts/format.sh index 2384068fd..f73f366b6 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -22,7 +22,7 @@ SWIFTFORMAT_DIR="$HERE/.swiftformat-source" # Important: if this is changed then make sure to update the version # in the .github/workflows/ci.yaml as well! -SWIFTFORMAT_VERSION=0.49.4 +SWIFTFORMAT_VERSION=0.52.0 # Clone SwiftFormat if we don't already have it. if [ ! -d "$SWIFTFORMAT_DIR" ]; then From 2828ee77a74e7d22007c0632f8eb2c5a4b96503e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 22 Aug 2023 08:53:54 +0100 Subject: [PATCH 112/580] Revert "Adopt h2handler multiplexer (#1587)" (#1641) This reverts commit 75b390e901c7d70af9b4c5ca2677e035f00cd1ab. --- .github/workflows/ci.yaml | 41 +- NOTICES.txt | 8 - Package.swift | 2 +- Sources/GRPC/ClientConnection.swift | 91 ++- Sources/GRPC/ConnectionManager.swift | 41 +- .../ConnectionPool+PerConnectionState.swift | 6 +- .../ConnectionPool+Waiter.swift | 6 +- .../GRPC/ConnectionPool/ConnectionPool.swift | 14 +- Sources/GRPC/ConnectionPool/PoolManager.swift | 2 +- Sources/GRPC/GRPCClientChannelHandler.swift | 4 +- Sources/GRPC/GRPCIdleHandler.swift | 102 +-- .../GRPC/GRPCServerPipelineConfigurator.swift | 67 +- Sources/GRPC/Server.swift | 6 +- Tests/GRPCTests/ClientTimeoutTests.swift | 7 +- Tests/GRPCTests/ConnectionManagerTests.swift | 588 ++++++++---------- .../ConnectionPoolDelegates.swift | 52 -- .../ConnectionPool/ConnectionPoolTests.swift | 229 +------ .../ConnectionPool/GRPCChannelPoolTests.swift | 83 +-- 18 files changed, 483 insertions(+), 866 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5c8e754de..f91e7d497 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,45 +57,44 @@ jobs: include: - image: swiftlang/swift:nightly-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swift:5.8-jammy env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swift:5.7-jammy env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 246000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 138000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 129000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 136000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 136000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swift:5.6-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 247000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 139000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 324000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 162000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 130000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 137000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 137000 - + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 164000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 171000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 171000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: diff --git a/NOTICES.txt b/NOTICES.txt index a0898dd80..f1ff7bbab 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -25,11 +25,3 @@ framework: 'test_01_allocation_counts.sh', 'run-nio-alloc-counter-tests.sh' and * https://github.com/apple/swift-nio/blob/main/LICENSE.txt * HOMEPAGE: * https://github.com/apple/swift-nio - -This product contains a simplified derivation of SwiftNIO HTTP/2's -'HTTP2FrameEncoder' for testing purposes. - - * LICENSE (Apache License 2.0): - * https://github.com/apple/swift-nio-http2/blob/main/LICENSE.txt - * HOMEPAGE: - * https://github.com/apple/swift-nio-http2 diff --git a/Package.swift b/Package.swift index 393599af8..2c89b5742 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-nio-http2.git", - from: "1.26.0" + from: "1.24.1" ), .package( url: "https://github.com/apple/swift-nio-transport-services.git", diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index ef997ad30..9b399d36f 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -58,25 +58,28 @@ import SwiftProtobuf /// │ DelegatingErrorHandler │ /// └──────────▲───────────────┘ /// HTTP2Frame│ -/// │ -/// │ -/// │ -/// │ -/// │ -/// HTTP2Frame│ ⠇ ⠇ ⠇ ⠇ ⠇ -/// ┌─┴──────────────────▼─┐ ┌┴─▼┐ ┌┴─▼┐ -/// │ GRPCIdleHandler │ │ | │ | HTTP/2 streams -/// └─▲──────────────────┬─┘ └▲─┬┘ └▲─┬┘ -/// HTTP2Frame│ │ │ │ │ │ HTTP2Frame -/// ┌─┴──────────────────▼────────┴─▼───┴─▼┐ -/// │ NIOHTTP2Handler │ -/// └─▲──────────────────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// ┌─┴──────────────────────────────────▼─┐ -/// │ NIOSSLHandler │ -/// └─▲──────────────────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// │ ▼ +/// │ ⠇ ⠇ ⠇ ⠇ +/// │ ┌┴─▼┐ ┌┴─▼┐ +/// │ │ | │ | HTTP/2 streams +/// │ └▲─┬┘ └▲─┬┘ +/// │ │ │ │ │ HTTP2Frame +/// ┌─┴────────────────┴─▼───┴─▼┐ +/// │ HTTP2StreamMultiplexer | +/// └─▲───────────────────────┬─┘ +/// HTTP2Frame│ │HTTP2Frame +/// ┌─┴───────────────────────▼─┐ +/// │ GRPCIdleHandler │ +/// └─▲───────────────────────┬─┘ +/// HTTP2Frame│ │HTTP2Frame +/// ┌─┴───────────────────────▼─┐ +/// │ NIOHTTP2Handler │ +/// └─▲───────────────────────┬─┘ +/// ByteBuffer│ │ByteBuffer +/// ┌─┴───────────────────────▼─┐ +/// │ NIOSSLHandler │ +/// └─▲───────────────────────┬─┘ +/// ByteBuffer│ │ByteBuffer +/// │ ▼ /// /// The 'GRPCIdleHandler' intercepts HTTP/2 frames and various events and is responsible for /// informing and controlling the state of the connection (idling and keepalive). The HTTP/2 streams @@ -85,7 +88,7 @@ public final class ClientConnection: Sendable { private let connectionManager: ConnectionManager /// HTTP multiplexer from the underlying channel handling gRPC calls. - internal func getMultiplexer() -> EventLoopFuture { + internal func getMultiplexer() -> EventLoopFuture { return self.connectionManager.getHTTP2Multiplexer() } @@ -247,7 +250,7 @@ extension ClientConnection: GRPCChannel { } private static func makeStreamChannel( - using result: Result, + using result: Result, promise: EventLoopPromise ) { switch result { @@ -618,31 +621,29 @@ extension ChannelPipeline.SynchronousOperations { HTTP2Setting(parameter: .initialWindowSize, value: httpTargetWindowSize), ] - let grpcIdleHandler = GRPCIdleHandler( + // We could use 'configureHTTP2Pipeline' here, but we need to add a few handlers between the + // two HTTP/2 handlers so we'll do it manually instead. + try self.addHandler(NIOHTTP2Handler(mode: .client, initialSettings: initialSettings)) + + let h2Multiplexer = HTTP2StreamMultiplexer( + mode: .client, + channel: channel, + targetWindowSize: httpTargetWindowSize, + inboundStreamInitializer: nil + ) + + // The multiplexer is passed through the idle handler so it is only reported on + // successful channel activation - with happy eyeballs multiple pipelines can + // be constructed so it's not safe to report just yet. + try self.addHandler(GRPCIdleHandler( connectionManager: connectionManager, + multiplexer: h2Multiplexer, idleTimeout: connectionIdleTimeout, keepalive: connectionKeepalive, logger: logger - ) - - var connectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() - connectionConfiguration.initialSettings = initialSettings - var streamConfiguration = NIOHTTP2Handler.StreamConfiguration() - streamConfiguration.targetWindowSize = httpTargetWindowSize - let h2Handler = NIOHTTP2Handler( - mode: .client, - eventLoop: channel.eventLoop, - connectionConfiguration: connectionConfiguration, - streamConfiguration: streamConfiguration, - streamDelegate: grpcIdleHandler - ) { channel in - channel.close() - } - try self.addHandler(h2Handler) - - grpcIdleHandler.setMultiplexer(try h2Handler.syncMultiplexer()) - try self.addHandler(grpcIdleHandler) + )) + try self.addHandler(h2Multiplexer) try self.addHandler(DelegatingErrorHandler(logger: logger, delegate: errorDelegate)) } } @@ -652,13 +653,7 @@ extension Channel { errorDelegate: ClientErrorDelegate?, logger: Logger ) -> EventLoopFuture { - return self.configureHTTP2Pipeline( - mode: .client, - connectionConfiguration: .init(), - streamConfiguration: .init() - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - }.flatMap { _ in + return self.configureHTTP2Pipeline(mode: .client, inboundStreamInitializer: nil).flatMap { _ in self.pipeline.addHandler(DelegatingErrorHandler(logger: logger, delegate: errorDelegate)) } } diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index efb801e48..35ee41101 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -35,23 +35,19 @@ internal final class ConnectionManager: @unchecked Sendable { var reconnect: Reconnect var candidate: EventLoopFuture - var readyChannelMuxPromise: EventLoopPromise - var candidateMuxPromise: EventLoopPromise + var readyChannelMuxPromise: EventLoopPromise + var candidateMuxPromise: EventLoopPromise } internal struct ConnectedState { var backoffIterator: ConnectionBackoffIterator? var reconnect: Reconnect var candidate: Channel - var readyChannelMuxPromise: EventLoopPromise - var multiplexer: NIOHTTP2Handler.StreamMultiplexer + var readyChannelMuxPromise: EventLoopPromise + var multiplexer: HTTP2StreamMultiplexer var error: Error? - init( - from state: ConnectingState, - candidate: Channel, - multiplexer: NIOHTTP2Handler.StreamMultiplexer - ) { + init(from state: ConnectingState, candidate: Channel, multiplexer: HTTP2StreamMultiplexer) { self.backoffIterator = state.backoffIterator self.reconnect = state.reconnect self.candidate = candidate @@ -62,7 +58,7 @@ internal final class ConnectionManager: @unchecked Sendable { internal struct ReadyState { var channel: Channel - var multiplexer: NIOHTTP2Handler.StreamMultiplexer + var multiplexer: HTTP2StreamMultiplexer var error: Error? init(from state: ConnectedState) { @@ -73,7 +69,7 @@ internal final class ConnectionManager: @unchecked Sendable { internal struct TransientFailureState { var backoffIterator: ConnectionBackoffIterator? - var readyChannelMuxPromise: EventLoopPromise + var readyChannelMuxPromise: EventLoopPromise var scheduled: Scheduled var reason: Error @@ -256,8 +252,8 @@ internal final class ConnectionManager: @unchecked Sendable { } } - /// Returns the `NIOHTTP2Handler.StreamMultiplexer` from the 'ready' state or `nil` if it is not available. - private var multiplexer: NIOHTTP2Handler.StreamMultiplexer? { + /// Returns the `HTTP2StreamMultiplexer` from the 'ready' state or `nil` if it is not available. + private var multiplexer: HTTP2StreamMultiplexer? { self.eventLoop.assertInEventLoop() switch self.state { case let .ready(state): @@ -365,8 +361,8 @@ internal final class ConnectionManager: @unchecked Sendable { /// Get the multiplexer from the underlying channel handling gRPC calls. /// if the `ConnectionManager` was configured to be `fastFailure` this will have /// one chance to connect - if not reconnections are managed here. - internal func getHTTP2Multiplexer() -> EventLoopFuture { - func getHTTP2Multiplexer0() -> EventLoopFuture { + internal func getHTTP2Multiplexer() -> EventLoopFuture { + func getHTTP2Multiplexer0() -> EventLoopFuture { switch self.callStartBehavior { case .waitsForConnectivity: return self.getHTTP2MultiplexerPatient() @@ -386,8 +382,8 @@ internal final class ConnectionManager: @unchecked Sendable { /// Returns a future for the multiplexer which succeeded when the channel is connected. /// Reconnects are handled if necessary. - private func getHTTP2MultiplexerPatient() -> EventLoopFuture { - let multiplexer: EventLoopFuture + private func getHTTP2MultiplexerPatient() -> EventLoopFuture { + let multiplexer: EventLoopFuture switch self.state { case .idle: @@ -425,12 +421,11 @@ internal final class ConnectionManager: @unchecked Sendable { /// attempt, or if the state is 'idle' returns the future for the next connection attempt. /// /// Note: if the state is 'transientFailure' or 'shutdown' then a failed future will be returned. - private func getHTTP2MultiplexerOptimistic() - -> EventLoopFuture { + private func getHTTP2MultiplexerOptimistic() -> EventLoopFuture { // `getHTTP2Multiplexer` makes sure we're on the event loop but let's just be sure. self.eventLoop.preconditionInEventLoop() - let muxFuture: EventLoopFuture = { () in + let muxFuture: EventLoopFuture = { () in switch self.state { case .idle: self.startConnecting() @@ -661,7 +656,7 @@ internal final class ConnectionManager: @unchecked Sendable { } /// The connecting channel became `active`. Must be called on the `EventLoop`. - internal func channelActive(channel: Channel, multiplexer: NIOHTTP2Handler.StreamMultiplexer) { + internal func channelActive(channel: Channel, multiplexer: HTTP2StreamMultiplexer) { self.eventLoop.preconditionInEventLoop() self.logger.debug("activating connection", metadata: [ "connectivity_state": "\(self.state.label)", @@ -978,7 +973,7 @@ extension ConnectionManager { private func startConnecting( backoffIterator: ConnectionBackoffIterator?, - muxPromise: EventLoopPromise + muxPromise: EventLoopPromise ) { let timeoutAndBackoff = backoffIterator?.next() @@ -1065,7 +1060,7 @@ extension ConnectionManager { /// Returns the `multiplexer` from a connection in the `ready` state or `nil` if it is any /// other state. - internal var multiplexer: NIOHTTP2Handler.StreamMultiplexer? { + internal var multiplexer: HTTP2StreamMultiplexer? { return self.manager.multiplexer } diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift index 0d4211924..3ebd6fbd4 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift @@ -58,7 +58,7 @@ extension ConnectionPool { } @usableFromInline - var multiplexer: NIOHTTP2Handler.StreamMultiplexer + var multiplexer: HTTP2StreamMultiplexer /// Maximum number of available streams. @usableFromInline var maxAvailable: Int @@ -78,7 +78,7 @@ extension ConnectionPool { /// Increment the reserved streams and return the multiplexer. @usableFromInline - mutating func reserve() -> NIOHTTP2Handler.StreamMultiplexer { + mutating func reserve() -> HTTP2StreamMultiplexer { assert(!self.isQuiescing) self.reserved += 1 return self.multiplexer @@ -132,7 +132,7 @@ extension ConnectionPool { /// /// The result may be safely unwrapped if `self.availableStreams > 0` when reserving a stream. @usableFromInline - internal mutating func reserveStream() -> NIOHTTP2Handler.StreamMultiplexer? { + internal mutating func reserveStream() -> HTTP2StreamMultiplexer? { return self._availability?.reserve() } diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift index ac9b6d810..b4b386f8b 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift @@ -30,7 +30,7 @@ extension ConnectionPool { /// The channel initializer. @usableFromInline - internal let _channelInitializer: @Sendable (Channel) -> EventLoopFuture + internal let _channelInitializer: (Channel) -> EventLoopFuture /// The deadline at which the timeout is scheduled. @usableFromInline @@ -51,7 +51,7 @@ extension ConnectionPool { internal init( deadline: NIODeadline, promise: EventLoopPromise, - channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture + channelInitializer: @escaping (Channel) -> EventLoopFuture ) { self._deadline = deadline self._promise = promise @@ -83,7 +83,7 @@ extension ConnectionPool { /// Succeed the waiter with the given multiplexer. @usableFromInline - internal func succeed(with multiplexer: NIOHTTP2Handler.StreamMultiplexer) { + internal func succeed(with multiplexer: HTTP2StreamMultiplexer) { self._scheduledTimeout?.cancel() self._scheduledTimeout = nil multiplexer.createStreamChannel(promise: self._promise, self._channelInitializer) diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 40c633764..132b3130d 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -215,7 +215,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture + initializer: @escaping (Channel) -> EventLoopFuture ) { if self.eventLoop.inEventLoop { self._makeStream( @@ -241,7 +241,7 @@ internal final class ConnectionPool { internal func makeStream( deadline: NIODeadline, logger: GRPCLogger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture + initializer: @escaping (Channel) -> EventLoopFuture ) -> EventLoopFuture { let promise = self.eventLoop.makePromise(of: Channel.self) self.makeStream(deadline: deadline, promise: promise, logger: logger, initializer: initializer) @@ -277,7 +277,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture + initializer: @escaping (Channel) -> EventLoopFuture ) { self.eventLoop.assertInEventLoop() @@ -310,7 +310,7 @@ internal final class ConnectionPool { @inlinable internal func _tryMakeStream( promise: EventLoopPromise, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture + initializer: @escaping (Channel) -> EventLoopFuture ) -> Bool { // We shouldn't jump the queue. guard self.waiters.isEmpty else { @@ -344,7 +344,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture + initializer: @escaping (Channel) -> EventLoopFuture ) { // Don't overwhelm the pool with too many waiters. guard self.waiters.count < self.maxWaiters else { @@ -479,10 +479,10 @@ internal final class ConnectionPool { /// Reserves a stream from the connection with the most available streams, if one exists. /// - /// - Returns: The `NIOHTTP2Handler.StreamMultiplexer` from the connection the stream was reserved from, + /// - Returns: The `HTTP2StreamMultiplexer` from the connection the stream was reserved from, /// or `nil` if no stream could be reserved. @usableFromInline - internal func _reserveStreamFromMostAvailableConnection() -> NIOHTTP2Handler.StreamMultiplexer? { + internal func _reserveStreamFromMostAvailableConnection() -> HTTP2StreamMultiplexer? { let index = self._mostAvailableConnectionIndex() if index != self._connections.endIndex { diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index a6e53d59f..c9f745422 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -278,7 +278,7 @@ internal final class PoolManager { preferredEventLoop: EventLoop?, deadline: NIODeadline, logger: GRPCLogger, - streamInitializer initializer: @escaping @Sendable (Channel) -> EventLoopFuture + streamInitializer initializer: @escaping (Channel) -> EventLoopFuture ) -> PooledStreamChannel { let preferredEventLoopID = preferredEventLoop.map { EventLoopID($0) } let reservedPool = self.lock.withLock { diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index 15b2c3d17..1e2695b90 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -270,10 +270,10 @@ public enum GRPCCallType: Hashable, Sendable { /// This handler relies heavily on the `GRPCClientStateMachine` to manage the state of the request /// and response streams, which share a single HTTP/2 stream for transport. /// -/// Typical usage of this handler is with a `NIOHTTP2Handler.StreamMultiplexer` from SwiftNIO HTTP2: +/// Typical usage of this handler is with a `HTTP2StreamMultiplexer` from SwiftNIO HTTP2: /// /// ``` -/// let multiplexer: NIOHTTP2Handler.StreamMultiplexer = // ... +/// let multiplexer: HTTP2StreamMultiplexer = // ... /// multiplexer.createStreamChannel(promise: nil) { (channel, streamID) in /// let clientChannelHandler = GRPCClientChannelHandler( /// streamID: streamID, diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 535304e53..4e5c1eb90 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -35,73 +35,24 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { private var scheduledPing: RepeatedTask? /// The mode we're operating in. - /// - /// This is a `var` to allow the client configuration state to be updated. - private var mode: Mode + private let mode: Mode private var context: ChannelHandlerContext? - /// Keeps track of the client configuration state. - /// We need two levels of configuration to break the dependency cycle with the stream multiplexer. - internal enum ClientConfigurationState { - case partial(ConnectionManager) - case complete(ConnectionManager, NIOHTTP2Handler.StreamMultiplexer) - case deinitialized - - mutating func setMultiplexer(_ multiplexer: NIOHTTP2Handler.StreamMultiplexer) { - switch self { - case let .partial(connectionManager): - self = .complete(connectionManager, multiplexer) - case .complete: - preconditionFailure("Setting the multiplexer twice is not supported.") - case .deinitialized: - preconditionFailure( - "Setting the multiplexer after removing from a channel is not supported." - ) - } - } - } - /// The mode of operation: the client tracks additional connection state in the connection /// manager. internal enum Mode { - case client(ClientConfigurationState) + case client(ConnectionManager, HTTP2StreamMultiplexer) case server - mutating func setMultiplexer(_ multiplexer: NIOHTTP2Handler.StreamMultiplexer) { - switch self { - case var .client(clientConfigurationState): - clientConfigurationState.setMultiplexer(multiplexer) - self = .client(clientConfigurationState) - case .server: - preconditionFailure("Setting the multiplexer in server mode is not supported.") - } - } - var connectionManager: ConnectionManager? { switch self { - case let .client(configurationState): - switch configurationState { - case let .complete(connectionManager, _): - return connectionManager - case let .partial(connectionManager): - return connectionManager - case .deinitialized: - return nil - } + case let .client(manager, _): + return manager case .server: return nil } } - - mutating func deinitialize() { - switch self { - case .client: - self = .client(.deinitialized) - case .server: - break // nothing to drop - } - } } /// The current state. @@ -109,11 +60,12 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { init( connectionManager: ConnectionManager, + multiplexer: HTTP2StreamMultiplexer, idleTimeout: TimeAmount, keepalive configuration: ClientConnectionKeepalive, logger: Logger ) { - self.mode = .client(.partial(connectionManager)) + self.mode = .client(connectionManager, multiplexer) self.idleTimeout = idleTimeout self.stateMachine = .init(role: .client, logger: logger) self.pingHandler = PingHandler( @@ -146,10 +98,6 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { ) } - internal func setMultiplexer(_ multiplexer: NIOHTTP2Handler.StreamMultiplexer) { - self.mode.setMultiplexer(multiplexer) - } - private func sendGoAway(lastStreamID streamID: HTTP2StreamID) { guard let context = self.context else { return @@ -291,11 +239,20 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { func handlerRemoved(context: ChannelHandlerContext) { self.context = nil - self.mode.deinitialize() } func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - if event is ChannelShouldQuiesceEvent { + if let created = event as? NIOHTTP2StreamCreatedEvent { + self.perform(operations: self.stateMachine.streamCreated(withID: created.streamID)) + self.handlePingAction(self.pingHandler.streamCreated()) + self.mode.connectionManager?.streamOpened() + context.fireUserInboundEventTriggered(event) + } else if let closed = event as? StreamClosedEvent { + self.perform(operations: self.stateMachine.streamClosed(withID: closed.streamID)) + self.handlePingAction(self.pingHandler.streamClosed()) + self.mode.connectionManager?.streamClosed() + context.fireUserInboundEventTriggered(event) + } else if event is ChannelShouldQuiesceEvent { self.perform(operations: self.stateMachine.initiateGracefulShutdown()) // Swallow this event. } else if case let .handshakeCompleted(negotiatedProtocol) = event as? TLSUserEvent { @@ -324,15 +281,8 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { // No state machine action here. switch self.mode { - case let .client(configurationState): - switch configurationState { - case let .complete(connectionManager, multiplexer): - connectionManager.channelActive(channel: context.channel, multiplexer: multiplexer) - case .partial: - preconditionFailure("not yet initialised") - case .deinitialized: - preconditionFailure("removed from channel") - } + case let .client(connectionManager, multiplexer): + connectionManager.channelActive(channel: context.channel, multiplexer: multiplexer) case .server: () } @@ -371,20 +321,6 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { } } -extension GRPCIdleHandler: NIOHTTP2StreamDelegate { - func streamCreated(_ id: NIOHTTP2.HTTP2StreamID, channel: NIOCore.Channel) { - self.perform(operations: self.stateMachine.streamCreated(withID: id)) - self.handlePingAction(self.pingHandler.streamCreated()) - self.mode.connectionManager?.streamOpened() - } - - func streamClosed(_ id: NIOHTTP2.HTTP2StreamID, channel: NIOCore.Channel) { - self.perform(operations: self.stateMachine.streamClosed(withID: id)) - self.handlePingAction(self.pingHandler.streamClosed()) - self.mode.connectionManager?.streamClosed() - } -} - extension HTTP2SettingsParameter { internal var loggingMetadataKey: String { switch self { diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift index a095c36d5..eba4f9c5a 100644 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ b/Sources/GRPC/GRPCServerPipelineConfigurator.swift @@ -78,39 +78,38 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan } /// Makes an HTTP/2 handler. - private func makeHTTP2Handler( - for channel: Channel, - streamDelegate: NIOHTTP2StreamDelegate? - ) -> NIOHTTP2Handler { - var connectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() - connectionConfiguration.initialSettings = [ - HTTP2Setting( - parameter: .maxConcurrentStreams, - value: self.configuration.httpMaxConcurrentStreams - ), - HTTP2Setting( - parameter: .maxHeaderListSize, - value: HPACKDecoder.defaultMaxHeaderListSize - ), - HTTP2Setting( - parameter: .maxFrameSize, - value: self.configuration.httpMaxFrameSize - ), - HTTP2Setting( - parameter: .initialWindowSize, - value: self.configuration.httpTargetWindowSize - ), - ] - - var streamConfiguration = NIOHTTP2Handler.StreamConfiguration() - streamConfiguration.targetWindowSize = self.configuration.httpTargetWindowSize + private func makeHTTP2Handler() -> NIOHTTP2Handler { + return .init( + mode: .server, + initialSettings: [ + HTTP2Setting( + parameter: .maxConcurrentStreams, + value: self.configuration.httpMaxConcurrentStreams + ), + HTTP2Setting( + parameter: .maxHeaderListSize, + value: HPACKDecoder.defaultMaxHeaderListSize + ), + HTTP2Setting( + parameter: .maxFrameSize, + value: self.configuration.httpMaxFrameSize + ), + HTTP2Setting( + parameter: .initialWindowSize, + value: self.configuration.httpTargetWindowSize + ), + ] + ) + } + + /// Makes an HTTP/2 multiplexer suitable handling gRPC requests. + private func makeHTTP2Multiplexer(for channel: Channel) -> HTTP2StreamMultiplexer { + var logger = self.configuration.logger return .init( mode: .server, - eventLoop: channel.eventLoop, - connectionConfiguration: connectionConfiguration, - streamConfiguration: streamConfiguration, - streamDelegate: streamDelegate + channel: channel, + targetWindowSize: self.configuration.httpTargetWindowSize ) { stream in // Sync options were added to the HTTP/2 stream channel in 1.17.0 (we require at least this) // so this shouldn't be `nil`, but it's not a problem if it is. @@ -119,7 +118,6 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan return String(Int(streamID)) } ?? "" - var logger = self.configuration.logger logger[metadataKey: MetadataKey.h2StreamID] = "\(streamID)" do { @@ -167,14 +165,13 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan // to then insert our keepalive and idle handlers between. We can just add everything together. let result: Result - let idleHandler = self.makeIdleHandler() do { // This is only ever called as a result of reading a user inbound event or reading inbound so // we'll be on the right event loop and sync operations are fine. let sync = context.pipeline.syncOperations - try sync.addHandler(self.makeHTTP2Handler(for: context.channel, streamDelegate: idleHandler)) - // Here we intentionally don't associate the multiplexer with the idleHandler in the server case - try sync.addHandler(idleHandler) + try sync.addHandler(self.makeHTTP2Handler()) + try sync.addHandler(self.makeIdleHandler()) + try sync.addHandler(self.makeHTTP2Multiplexer(for: context.channel)) result = .success(()) } catch { result = .failure(error) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 5ba505954..e6adf5299 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -54,6 +54,10 @@ import Network /// `GRPCServerPipelineConfigurator`. In the case of HTTP/2: /// /// ┌─────────────────────────────────┐ +/// │ HTTP2StreamMultiplexer │ +/// └─▲─────────────────────────────┬─┘ +/// HTTP2Frame│ │HTTP2Frame +/// ┌─┴─────────────────────────────▼─┐ /// │ HTTP2Handler │ /// └─▲─────────────────────────────┬─┘ /// ByteBuffer│ │ByteBuffer @@ -63,7 +67,7 @@ import Network /// ByteBuffer│ │ByteBuffer /// │ ▼ /// -/// The `NIOHTTP2Handler.StreamMultiplexer` provides one `Channel` for each HTTP/2 stream (and thus each +/// The `HTTP2StreamMultiplexer` provides one `Channel` for each HTTP/2 stream (and thus each /// RPC). /// /// 3. The frames for each stream channel are routed by the `HTTP2ToRawGRPCServerCodec` handler to diff --git a/Tests/GRPCTests/ClientTimeoutTests.swift b/Tests/GRPCTests/ClientTimeoutTests.swift index ff1cef9f5..0bf157101 100644 --- a/Tests/GRPCTests/ClientTimeoutTests.swift +++ b/Tests/GRPCTests/ClientTimeoutTests.swift @@ -169,7 +169,7 @@ extension EmbeddedGRPCChannel: @unchecked Sendable {} private final class EmbeddedGRPCChannel: GRPCChannel { let embeddedChannel: EmbeddedChannel - let multiplexer: EventLoopFuture + let multiplexer: EventLoopFuture let logger: Logger let scheme: String @@ -195,11 +195,8 @@ private final class EmbeddedGRPCChannel: GRPCChannel { errorDelegate: errorDelegate, logger: logger ).flatMap { - embeddedChannel.pipeline.handler(type: NIOHTTP2Handler.self) - }.flatMap { h2Handler in - h2Handler.multiplexer + embeddedChannel.pipeline.handler(type: HTTP2StreamMultiplexer.self) } - self.scheme = "http" self.authority = "localhost" self.errorDelegate = errorDelegate diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index b41daf4c7..92f2333f3 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -116,7 +116,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let multiplexer: EventLoopFuture = self + let multiplexer: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let channel = manager.getHTTP2Multiplexer() self.loop.run() @@ -146,22 +146,20 @@ extension ConnectionManagerTests { // Setup the real channel and activate it. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.addHandler(idleHandler).wait() + channel: channel, + inboundStreamInitializer: nil + ) + try channel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -171,7 +169,7 @@ extension ConnectionManagerTests { // Write a settings frame on the root stream; this'll make the channel 'ready'. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel.writeInbound(frame)) } // Close the channel. @@ -190,7 +188,7 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -199,23 +197,20 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.addHandler(idleHandler).wait() + channel: channel, + inboundStreamInitializer: nil + ) + try channel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -225,7 +220,7 @@ extension ConnectionManagerTests { // Write a settings frame on the root stream; this'll make the channel 'ready'. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel.writeInbound(frame)) // Wait for the multiplexer, it _must_ be ready now. XCTAssertNoThrow(try readyChannelMux.wait()) } @@ -244,24 +239,6 @@ extension ConnectionManagerTests { } } - /// Forwards only the first `channelInactive` call - /// - /// This is useful in tests where we intentionally mis-use the channels - /// and call `fireChannelInactive` manually during the test but don't want - /// teardown to cause precondition failures due to this unexpected behavior. - class SwallowSecondInactiveHandler: ChannelInboundHandler { - typealias InboundIn = HTTP2Frame - typealias OutboundOut = HTTP2Frame - - private var seenAnInactive = false - func channelInactive(context: ChannelHandlerContext) { - if !self.seenAnInactive { - self.seenAnInactive = true - context.fireChannelInactive() - } - } - } - func testChannelInactiveBeforeActiveWithNoReconnect() throws { let channel = EmbeddedChannel(loop: self.loop) let channelPromise = self.loop.makePromise(of: Channel.self) @@ -276,33 +253,28 @@ extension ConnectionManagerTests { _ = manager.getHTTP2Multiplexer() self.loop.run() } - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - let h2handler = NIOHTTP2Handler( - mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.syncOperations.addHandler(SwallowSecondInactiveHandler()) - try channel.pipeline.syncOperations.addHandler(h2handler) - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.syncOperations.addHandler(idleHandler) - try channel.pipeline.syncOperations.addHandler(NIOCloseOnErrorHandler()) + try channel.pipeline.syncOperations.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: HTTP2StreamMultiplexer( + mode: .client, + channel: channel, + inboundStreamInitializer: nil + ), + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ) channelPromise.succeed(channel) + // Oops: wrong way around. We should tolerate this. + self.waitForStateChange(from: .connecting, to: .shutdown) { + channel.pipeline.fireChannelInactive() + } - // Oops: wrong way around. We should tolerate this - just don't crash. - channel.pipeline.fireChannelInactive() + // Should be ignored. channel.pipeline.fireChannelActive() - - channel.embeddedEventLoop.run() - try manager.shutdown(mode: .forceful).wait() } func testChannelInactiveBeforeActiveWillReconnect() throws { @@ -328,59 +300,55 @@ extension ConnectionManagerTests { // Setup the channel. let channel1 = channels.removeLast() let channel1Promise = channelPromises.removeLast() - let idleHandler1 = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger + + try channel1.pipeline.syncOperations.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: HTTP2StreamMultiplexer( + mode: .client, + channel: channel1, + inboundStreamInitializer: nil + ), + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) ) - let h2handler1 = NIOHTTP2Handler( - mode: .client, - eventLoop: channel1.eventLoop, - streamDelegate: idleHandler1 - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel1.pipeline.syncOperations.addHandler(SwallowSecondInactiveHandler()) - try channel1.pipeline.syncOperations.addHandler(h2handler1) - idleHandler1.setMultiplexer(try h2handler1.syncMultiplexer()) - try channel1.pipeline.syncOperations.addHandler(idleHandler1) - try channel1.pipeline.syncOperations.addHandler(NIOCloseOnErrorHandler()) channel1Promise.succeed(channel1) // Oops: wrong way around. We should tolerate this. - channel1.pipeline.fireChannelInactive() + self.waitForStateChange(from: .connecting, to: .transientFailure) { + channel1.pipeline.fireChannelInactive() + } + channel1.pipeline.fireChannelActive() // Start the next attempt. - self.loop.advanceTime(by: .seconds(1)) + self.waitForStateChange(from: .transientFailure, to: .connecting) { + self.loop.advanceTime(by: .seconds(1)) + } let channel2 = channels.removeLast() let channel2Promise = channelPromises.removeLast() - let idleHandler2 = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger + try channel2.pipeline.syncOperations.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: HTTP2StreamMultiplexer( + mode: .client, + channel: channel1, + inboundStreamInitializer: nil + ), + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) ) - let h2handler2 = NIOHTTP2Handler( - mode: .client, - eventLoop: channel2.eventLoop, - streamDelegate: idleHandler2 - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel2.pipeline.syncOperations.addHandler(SwallowSecondInactiveHandler()) - try channel2.pipeline.syncOperations.addHandler(h2handler2) - idleHandler2.setMultiplexer(try h2handler2.syncMultiplexer()) - try channel2.pipeline.syncOperations.addHandler(idleHandler2) - try channel2.pipeline.syncOperations.addHandler(NIOCloseOnErrorHandler()) channel2Promise.succeed(channel2) try self.waitForStateChange(from: .connecting, to: .ready) { channel2.pipeline.fireChannelActive() let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel2.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel2.writeInbound(frame)) } } @@ -391,7 +359,7 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -400,23 +368,20 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.addHandler(idleHandler).wait() + channel: channel, + inboundStreamInitializer: nil + ) + try channel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(channel) XCTAssertNoThrow( @@ -427,13 +392,18 @@ extension ConnectionManagerTests { // Write a settings frame on the root stream; this'll make the channel 'ready'. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel.writeInbound(frame)) // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. XCTAssertNoThrow(try readyChannelMux.wait()) } // "create" a stream; the details don't matter here. - idleHandler.streamCreated(1, channel: channel) + let streamCreated = NIOHTTP2StreamCreatedEvent( + streamID: 1, + localInitialWindowSize: nil, + remoteInitialWindowSize: nil + ) + channel.pipeline.fireUserInboundEventTriggered(streamCreated) // Wait for the idle timeout: this should _not_ cause the channel to idle. self.loop.advanceTime(by: .minutes(5)) @@ -441,7 +411,8 @@ extension ConnectionManagerTests { // Now we're going to close the stream and wait for an idle timeout and then shutdown. self.waitForStateChange(from: .ready, to: .idle) { // Close the stream. - idleHandler.streamClosed(1, channel: channel) + let streamClosed = StreamClosedEvent(streamID: 1, reason: nil) + channel.pipeline.fireUserInboundEventTriggered(streamClosed) // ... wait for the idle timeout, self.loop.advanceTime(by: .minutes(5)) } @@ -460,7 +431,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -469,23 +440,20 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.addHandler(idleHandler).wait() + channel: channel, + inboundStreamInitializer: nil + ) + try channel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -523,10 +491,10 @@ extension ConnectionManagerTests { return next } - let readyChannelMux = self.waitForStateChanges([ + let readyChannelMux: EventLoopFuture = self.waitForStateChanges([ Change(from: .idle, to: .connecting), Change(from: .connecting, to: .transientFailure), - ]) { () -> EventLoopFuture in + ]) { // Get a HTTP/2 stream multiplexer. let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -544,23 +512,20 @@ extension ConnectionManagerTests { // Setup the actual channel and complete the promise. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.addHandler(idleHandler).wait() + channel: channel, + inboundStreamInitializer: nil + ) + try channel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -570,7 +535,7 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel.writeInbound(frame)) } // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. @@ -591,7 +556,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -627,10 +592,10 @@ extension ConnectionManagerTests { self.loop.makeFailedFuture(DoomedChannelError()) } - let readyChannelMux = self.waitForStateChanges([ + let readyChannelMux: EventLoopFuture = self.waitForStateChanges([ Change(from: .idle, to: .connecting), Change(from: .connecting, to: .transientFailure), - ]) { () -> EventLoopFuture in + ]) { // Get a HTTP/2 stream multiplexer. let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -654,7 +619,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -663,23 +628,20 @@ extension ConnectionManagerTests { // Prepare the channel let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.addHandler(idleHandler).wait() + channel: channel, + inboundStreamInitializer: nil + ) + try channel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -732,7 +694,7 @@ extension ConnectionManagerTests { return next } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -741,23 +703,20 @@ extension ConnectionManagerTests { // Prepare the channel let firstChannel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: firstChannel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try firstChannel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try firstChannel.pipeline.addHandler(idleHandler).wait() + channel: firstChannel, + inboundStreamInitializer: nil + ) + try firstChannel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(firstChannel) XCTAssertNoThrow( @@ -811,7 +770,7 @@ extension ConnectionManagerTests { return next } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -820,22 +779,20 @@ extension ConnectionManagerTests { // Prepare the first channel let firstChannel = EmbeddedChannel(loop: self.loop) - let firstIdleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - let firstH2handler = NIOHTTP2Handler( + let firstH2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: firstChannel.eventLoop, - streamDelegate: firstIdleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try firstChannel.pipeline.addHandler(firstH2handler).wait() - firstIdleHandler.setMultiplexer(try firstH2handler.syncMultiplexer()) - try firstChannel.pipeline.addHandler(firstIdleHandler).wait() + channel: firstChannel, + inboundStreamInitializer: nil + ) + try firstChannel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: firstH2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() firstChannelPromise.succeed(firstChannel) XCTAssertNoThrow( try firstChannel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -845,14 +802,19 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try firstChannel.writeInbound(frame.encode())) + XCTAssertNoThrow(try firstChannel.writeInbound(frame)) } // Channel should now be ready. XCTAssertNoThrow(try readyChannelMux.wait()) // Kill the first channel. But first ensure there's an active RPC, otherwise we'll idle. - firstIdleHandler.streamCreated(1, channel: firstChannel) + let streamCreated = NIOHTTP2StreamCreatedEvent( + streamID: 1, + localInitialWindowSize: nil, + remoteInitialWindowSize: nil + ) + firstChannel.pipeline.fireUserInboundEventTriggered(streamCreated) try self.waitForStateChange(from: .ready, to: .transientFailure) { XCTAssertNoThrow(try firstChannel.close().wait()) @@ -865,22 +827,20 @@ extension ConnectionManagerTests { // Prepare the second channel let secondChannel = EmbeddedChannel(loop: self.loop) - let secondIdleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - let secondH2handler = NIOHTTP2Handler( + let secondH2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: secondChannel.eventLoop, - streamDelegate: secondIdleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try secondChannel.pipeline.addHandler(secondH2handler).wait() - secondIdleHandler.setMultiplexer(try secondH2handler.syncMultiplexer()) - try secondChannel.pipeline.addHandler(secondIdleHandler).wait() + channel: secondChannel, + inboundStreamInitializer: nil + ) + try secondChannel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: secondH2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() secondChannelPromise.succeed(secondChannel) XCTAssertNoThrow( try secondChannel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -890,7 +850,7 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try secondChannel.writeInbound(frame.encode())) + XCTAssertNoThrow(try secondChannel.writeInbound(frame)) } // Now shutdown @@ -907,7 +867,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -916,23 +876,20 @@ extension ConnectionManagerTests { // Setup the channel. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - try channel.pipeline.addHandler(idleHandler).wait() + channel: channel, + inboundStreamInitializer: nil + ) + try channel.pipeline.addHandler( + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ).wait() channelPromise.succeed(channel) XCTAssertNoThrow( try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) @@ -942,7 +899,7 @@ extension ConnectionManagerTests { try self.waitForStateChange(from: .connecting, to: .ready) { // Write a SETTINGS frame on the root stream. let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel.writeInbound(frame)) } // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. @@ -955,7 +912,7 @@ extension ConnectionManagerTests { streamID: .rootStream, payload: .goAway(lastStreamID: 1, errorCode: .noError, opaqueData: nil) ) - XCTAssertNoThrow(try channel.writeInbound(goAway.encode())) + XCTAssertNoThrow(try channel.writeInbound(goAway)) self.loop.run() } @@ -1061,34 +1018,31 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self - .waitForStateChange( - from: .idle, - to: .connecting - ) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } + let readyChannelMux: EventLoopFuture = self.waitForStateChange( + from: .idle, + to: .connecting + ) { + let readyChannelMux = manager.getHTTP2Multiplexer() + self.loop.run() + return readyChannelMux + } // Setup the real channel and activate it. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - XCTAssertNoThrow(try channel.pipeline.addHandler(idleHandler).wait()) + channel: channel, + inboundStreamInitializer: nil + ) + XCTAssertNoThrow(try channel.pipeline.addHandlers([ + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ), + ]).wait()) channelPromise.succeed(channel) self.loop.run() @@ -1098,7 +1052,7 @@ extension ConnectionManagerTests { // Write a SETTINGS frame on the root stream. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel.writeInbound(frame)) } // The channel should now be ready. @@ -1122,7 +1076,7 @@ extension ConnectionManagerTests { let readyChannelMux = self.waitForStateChange( from: .idle, to: .connecting - ) { () -> EventLoopFuture in + ) { () -> EventLoopFuture in let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() return readyChannelMux @@ -1130,22 +1084,20 @@ extension ConnectionManagerTests { // Setup the actual channel and activate it. let channel = EmbeddedChannel(loop: self.loop) - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - let h2handler = NIOHTTP2Handler( + let h2mux = HTTP2StreamMultiplexer( mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try channel.pipeline.addHandler(h2handler).wait() - idleHandler.setMultiplexer(try h2handler.syncMultiplexer()) - XCTAssertNoThrow(try channel.pipeline.addHandler(idleHandler).wait()) + channel: channel, + inboundStreamInitializer: nil + ) + XCTAssertNoThrow(try channel.pipeline.addHandlers([ + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ), + ]).wait()) channelPromise.succeed(channel) self.loop.run() @@ -1155,7 +1107,7 @@ extension ConnectionManagerTests { // "ready" the connection. try self.waitForStateChange(from: .connecting, to: .ready) { let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame.encode())) + XCTAssertNoThrow(try channel.writeInbound(frame)) } // The HTTP/2 stream multiplexer should now be ready. @@ -1188,6 +1140,12 @@ extension ConnectionManagerTests { XCTAssertNoThrow(try channel.finish()) } + let multiplexer = HTTP2StreamMultiplexer( + mode: .client, + channel: channel, + inboundStreamInitializer: nil + ) + class HTTP2Delegate: ConnectionManagerHTTP2Delegate { var streamsOpened = 0 var streamsClosed = 0 @@ -1216,19 +1174,11 @@ extension ConnectionManagerTests { channelProvider: HookedChannelProvider { manager, eventLoop -> EventLoopFuture in let idleHandler = GRPCIdleHandler( connectionManager: manager, + multiplexer: multiplexer, idleTimeout: .minutes(5), keepalive: ClientConnectionKeepalive(), logger: self.logger ) - let h2Handler = NIOHTTP2Handler( - mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - try! channel.pipeline.syncOperations.addHandler(h2Handler) - idleHandler.setMultiplexer(try! h2Handler.syncMultiplexer()) // We're going to cheat a bit by not putting the multiplexer in the channel. This allows // us to just fire stream created/closed events into the channel. @@ -1260,26 +1210,30 @@ extension ConnectionManagerTests { let settings = [HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams)] return HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) } - XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 42).encode())) + XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 42))) // We're ready now so the future multiplexer will resolve and we'll have seen an update to // max concurrent streams. XCTAssertNoThrow(try futureMultiplexer.wait()) XCTAssertEqual(http2.maxConcurrentStreams, 42) - XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 13).encode())) + XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 13))) XCTAssertEqual(http2.maxConcurrentStreams, 13) - let streamDelegate = try channel.pipeline.handler(type: GRPCIdleHandler.self).wait() - // Open some streams. for streamID in stride(from: HTTP2StreamID(1), to: HTTP2StreamID(9), by: 2) { - streamDelegate.streamCreated(streamID, channel: channel) + let streamCreated = NIOHTTP2StreamCreatedEvent( + streamID: streamID, + localInitialWindowSize: nil, + remoteInitialWindowSize: nil + ) + channel.pipeline.fireUserInboundEventTriggered(streamCreated) } // ... and then close them. for streamID in stride(from: HTTP2StreamID(1), to: HTTP2StreamID(9), by: 2) { - streamDelegate.streamClosed(streamID, channel: channel) + let streamClosed = StreamClosedEvent(streamID: streamID, reason: nil) + channel.pipeline.fireUserInboundEventTriggered(streamClosed) } XCTAssertEqual(http2.streamsOpened, 4) @@ -1292,7 +1246,7 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let multiplexer: EventLoopFuture = self.waitForStateChange( + let multiplexer: EventLoopFuture = self.waitForStateChange( from: .idle, to: .connecting ) { diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift index 924b38d3d..4a063bfa3 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift @@ -189,55 +189,3 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { } extension EventRecordingConnectionPoolDelegate: @unchecked Sendable {} - -final class AsyncEventStreamConnectionPoolDelegate: GRPCConnectionPoolDelegate { - private let continuation: AsyncStream.Continuation - - static func makeDelegateAndAsyncStream() - -> ( - AsyncEventStreamConnectionPoolDelegate, - AsyncStream - ) { - var continuation: AsyncStream.Continuation! - let asyncStream = AsyncStream(EventRecordingConnectionPoolDelegate.Event.self) { - continuation = $0 - } - return (Self(continuation: continuation), asyncStream) - } - - init(continuation: AsyncStream.Continuation) { - self.continuation = continuation - } - - func connectionAdded(id: GRPCConnectionID) { - self.continuation.yield(.connectionAdded(id)) - } - - func startedConnecting(id: GRPCConnectionID) { - self.continuation.yield(.startedConnecting(id)) - } - - func connectFailed(id: GRPCConnectionID, error: Error) { - self.continuation.yield(.connectFailed(id)) - } - - func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) { - self.continuation.yield(.connectSucceeded(id, streamCapacity)) - } - - func connectionClosed(id: GRPCConnectionID, error: Error?) { - self.continuation.yield(.connectionClosed(id)) - } - - func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) { - self.continuation.yield(.connectionUtilizationChanged(id, streamsUsed, streamCapacity)) - } - - func connectionQuiescing(id: GRPCConnectionID) { - self.continuation.yield(.connectionQuiescing(id)) - } - - func connectionRemoved(id: GRPCConnectionID) { - self.continuation.yield(.connectionRemoved(id)) - } -} diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 4c381c5f9..1859bf675 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -1054,24 +1054,17 @@ extension ConnectionPool { // MARK: - Helpers -struct ChannelAndState { - let channel: EmbeddedChannel - let streamDelegate: NIOHTTP2StreamDelegate - var isActive: Bool -} - internal final class ChannelController { - private var channels: [ChannelAndState] = [] + private var channels: [EmbeddedChannel] = [] internal var count: Int { return self.channels.count } internal func finish() { - while let state = self.channels.popLast() { - if state.isActive { - _ = try? state.channel.finish() - } + while let channel = self.channels.popLast() { + // We're okay with this throwing: some channels are left in a bad state (i.e. with errors). + _ = try? channel.finish() } } @@ -1091,10 +1084,9 @@ internal final class ChannelController { line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } - self.channels[index].isActive = true XCTAssertNoThrow( - try self.channels[index].channel.connect(to: .init(unixDomainSocketPath: "/")), + try self.channels[index].connect(to: .init(unixDomainSocketPath: "/")), file: file, line: line ) @@ -1106,8 +1098,7 @@ internal final class ChannelController { line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } - self.channels[index].channel.pipeline.fireChannelInactive() - self.channels[index].isActive = false + self.channels[index].pipeline.fireChannelInactive() } internal func throwError( @@ -1117,7 +1108,7 @@ internal final class ChannelController { line: UInt = #line ) { guard self.isValidIndex(index, file: file, line: line) else { return } - self.channels[index].channel.pipeline.fireErrorCaught(error) + self.channels[index].pipeline.fireErrorCaught(error) } internal func sendSettingsToChannel( @@ -1131,11 +1122,7 @@ internal final class ChannelController { let settings = [HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams)] let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) - XCTAssertNoThrow( - try self.channels[index].channel.writeInbound(settingsFrame.encode()), - file: file, - line: line - ) + XCTAssertNoThrow(try self.channels[index].writeInbound(settingsFrame), file: file, line: line) } internal func sendGoAwayToChannel( @@ -1150,11 +1137,7 @@ internal final class ChannelController { payload: .goAway(lastStreamID: .maxID, errorCode: .noError, opaqueData: nil) ) - XCTAssertNoThrow( - try self.channels[index].channel.writeInbound(goAwayFrame.encode()), - file: file, - line: line - ) + XCTAssertNoThrow(try self.channels[index].writeInbound(goAwayFrame), file: file, line: line) } internal func openStreamInChannel( @@ -1165,8 +1148,13 @@ internal final class ChannelController { guard self.isValidIndex(index, file: file, line: line) else { return } // The details don't matter here. - let channel = self.channels[index] - channel.streamDelegate.streamCreated(.rootStream, channel: channel.channel) + let event = NIOHTTP2StreamCreatedEvent( + streamID: .rootStream, + localInitialWindowSize: nil, + remoteInitialWindowSize: nil + ) + + self.channels[index].pipeline.fireUserInboundEventTriggered(event) } internal func closeStreamInChannel( @@ -1177,8 +1165,8 @@ internal final class ChannelController { guard self.isValidIndex(index, file: file, line: line) else { return } // The details don't matter here. - let channel = self.channels[index] - channel.streamDelegate.streamClosed(.rootStream, channel: channel.channel) + let event = StreamClosedEvent(streamID: .rootStream, reason: nil) + self.channels[index].pipeline.fireUserInboundEventTriggered(event) } } @@ -1190,27 +1178,24 @@ extension ChannelController: ConnectionManagerChannelProvider { logger: Logger ) -> EventLoopFuture { let channel = EmbeddedChannel(loop: eventLoop as! EmbeddedEventLoop) + self.channels.append(channel) + + let multiplexer = HTTP2StreamMultiplexer( + mode: .client, + channel: channel, + inboundStreamInitializer: nil + ) let idleHandler = GRPCIdleHandler( connectionManager: connectionManager, + multiplexer: multiplexer, idleTimeout: .minutes(5), keepalive: ClientConnectionKeepalive(), logger: logger ) - let h2handler = NIOHTTP2Handler( - mode: .client, - eventLoop: channel.eventLoop, - streamDelegate: idleHandler - ) { channel in - channel.eventLoop.makeSucceededVoidFuture() - } - XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(h2handler)) - - idleHandler.setMultiplexer(try! h2handler.syncMultiplexer()) - self.channels.append(.init(channel: channel, streamDelegate: idleHandler, isActive: false)) - XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(idleHandler)) + XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(multiplexer)) return eventLoop.makeSucceededFuture(channel) } @@ -1257,163 +1242,3 @@ extension Optional where Wrapped == ConnectionPoolError { } } } - -// Simplified version of the frame encoder found in SwiftNIO HTTP/2 -struct HTTP2FrameEncoder { - mutating func encode(frame: HTTP2Frame, to buf: inout ByteBuffer) throws -> IOData? { - // note our starting point - let start = buf.writerIndex - - // +-----------------------------------------------+ - // | Length (24) | - // +---------------+---------------+---------------+ - // | Type (8) | Flags (8) | - // +-+-------------+---------------+-------------------------------+ - // |R| Stream Identifier (31) | - // +=+=============================================================+ - // | Frame Payload (0...) ... - // +---------------------------------------------------------------+ - - // skip 24-bit length for now, we'll fill that in later - buf.moveWriterIndex(forwardBy: 3) - - // 8-bit type - buf.writeInteger(frame.code()) - - // skip the 8 bit flags for now, we'll fill it in later as well. - let flagsIndex = buf.writerIndex - var flags = FrameFlags() - buf.moveWriterIndex(forwardBy: 1) - - // 32-bit stream identifier -- ensuring the top bit is empty - buf.writeInteger(Int32(frame.streamID)) - - // frame payload follows, which depends on the frame type itself - let extraFrameData: IOData? - let payloadSize: Int - - switch frame.payload { - case let .settings(.settings(settings)): - for setting in settings { - buf.writeInteger(setting.parameter.networkRepresentation()) - buf.writeInteger(UInt32(setting.value)) - } - - payloadSize = settings.count * 6 - extraFrameData = nil - - case .settings(.ack): - payloadSize = 0 - extraFrameData = nil - flags.insert(.ack) - - case let .goAway(lastStreamID, errorCode, opaqueData): - let streamVal = UInt32(Int(lastStreamID)) & ~0x8000_0000 - buf.writeInteger(streamVal) - buf.writeInteger(UInt32(errorCode.networkCode)) - - if let data = opaqueData { - payloadSize = data.readableBytes + 8 - extraFrameData = .byteBuffer(data) - } else { - payloadSize = 8 - extraFrameData = nil - } - - case .data, .headers, .priority, - .rstStream, .pushPromise, .ping, - .windowUpdate, .alternativeService, .origin: - preconditionFailure("Frame type not supported: \(frame.payload)") - } - - // Write the frame data. This is the payload size and the flags byte. - buf.writePayloadSize(payloadSize, at: start) - buf.setInteger(flags.rawValue, at: flagsIndex) - - // all bytes to write are in the provided buffer now - return extraFrameData - } - - struct FrameFlags: OptionSet { - internal private(set) var rawValue: UInt8 - - internal init(rawValue: UInt8) { - self.rawValue = rawValue - } - - /// ACK flag. Valid on SETTINGS and PING frames. - internal static let ack = FrameFlags(rawValue: 0x01) - } -} - -extension HTTP2SettingsParameter { - internal func networkRepresentation() -> UInt16 { - switch self { - case HTTP2SettingsParameter.headerTableSize: - return UInt16(1) - case HTTP2SettingsParameter.enablePush: - return UInt16(2) - case HTTP2SettingsParameter.maxConcurrentStreams: - return UInt16(3) - case HTTP2SettingsParameter.initialWindowSize: - return UInt16(4) - case HTTP2SettingsParameter.maxFrameSize: - return UInt16(5) - case HTTP2SettingsParameter.maxHeaderListSize: - return UInt16(6) - case HTTP2SettingsParameter.enableConnectProtocol: - return UInt16(8) - default: - preconditionFailure("Unknown settings parameter.") - } - } -} - -extension ByteBuffer { - fileprivate mutating func writePayloadSize(_ size: Int, at location: Int) { - // Yes, this performs better than running a UInt8 through the generic write(integer:) three times. - var bytes: (UInt8, UInt8, UInt8) - bytes.0 = UInt8((size & 0xFF0000) >> 16) - bytes.1 = UInt8((size & 0x00FF00) >> 8) - bytes.2 = UInt8(size & 0x0000FF) - withUnsafeBytes(of: bytes) { ptr in - _ = self.setBytes(ptr, at: location) - } - } -} - -extension HTTP2Frame { - internal func encode() throws -> ByteBuffer { - let allocator = ByteBufferAllocator() - var buffer = allocator.buffer(capacity: 1024) - - var frameEncoder = HTTP2FrameEncoder() - let extraData = try frameEncoder.encode(frame: self, to: &buffer) - if let extraData = extraData { - switch extraData { - case let .byteBuffer(extraBuffer): - buffer.writeImmutableBuffer(extraBuffer) - default: - preconditionFailure() - } - } - return buffer - } - - /// The one-byte identifier used to indicate the type of a frame on the wire. - internal func code() -> UInt8 { - switch self.payload { - case .data: return 0x0 - case .headers: return 0x1 - case .priority: return 0x2 - case .rstStream: return 0x3 - case .settings: return 0x4 - case .pushPromise: return 0x5 - case .ping: return 0x6 - case .goAway: return 0x7 - case .windowUpdate: return 0x8 - case .alternativeService: return 0xA - case .origin: return 0xC - } - } -} diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index 7edc6e564..a118d813e 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -441,75 +441,54 @@ final class GRPCChannelPoolTests: GRPCTestCase { } } - func testConnectionPoolDelegateSingleConnection() async throws { - let (delegate, stream) = AsyncEventStreamConnectionPoolDelegate.makeDelegateAndAsyncStream() + func testConnectionPoolDelegateSingleConnection() throws { + let recorder = EventRecordingConnectionPoolDelegate() self.setUpClientAndServer(withTLS: false, threads: 1) { - $0.delegate = delegate + $0.delegate = recorder } let warmup = self.echo.get(.with { $0.text = "" }) XCTAssertNoThrow(try warmup.status.wait()) - var iterator = stream.makeAsyncIterator() - - var event = await iterator.next() - let id = try XCTUnwrap(event?.id) - XCTAssertEqual(event, .connectionAdded(id)) - event = await iterator.next() - XCTAssertEqual(event, .startedConnecting(id)) - event = await iterator.next() - XCTAssertEqual(event, .connectSucceeded(id, 100)) - event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id, 1, 100)) - event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id, 0, 100)) - - let rpcs: [ClientStreamingCall] = try (1 ... 10).map { _ in + let id = try XCTUnwrap(recorder.first?.id) + XCTAssertEqual(recorder.popFirst(), .connectionAdded(id)) + XCTAssertEqual(recorder.popFirst(), .startedConnecting(id)) + XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 0, 100)) + + let rpcs: [ClientStreamingCall] = try (1 ... 10).map { i in let rpc = self.echo.collect() XCTAssertNoThrow(try rpc.sendMessage(.with { $0.text = "foo" }).wait()) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, i, 100)) return rpc } - for (i, _) in rpcs.enumerated() { - let event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id, i + 1, 100)) - } - for (i, rpc) in rpcs.enumerated() { XCTAssertNoThrow(try rpc.sendEnd().wait()) XCTAssertNoThrow(try rpc.status.wait()) - let event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id, 10 - (i + 1), 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 10 - (i + 1), 100)) } XCTAssertNoThrow(try self.channel?.close().wait()) - event = await iterator.next() - XCTAssertEqual(event, .connectionClosed(id)) - event = await iterator.next() - XCTAssertEqual(event, .connectionRemoved(id)) + XCTAssertEqual(recorder.popFirst(), .connectionClosed(id)) + XCTAssertEqual(recorder.popFirst(), .connectionRemoved(id)) + XCTAssert(recorder.isEmpty) } - func testConnectionPoolDelegateQuiescing() async throws { - let (delegate, stream) = AsyncEventStreamConnectionPoolDelegate.makeDelegateAndAsyncStream() + func testConnectionPoolDelegateQuiescing() throws { + let recorder = EventRecordingConnectionPoolDelegate() self.setUpClientAndServer(withTLS: false, threads: 1) { - $0.delegate = delegate + $0.delegate = recorder } XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) - - var iterator = stream.makeAsyncIterator() - - var event = await iterator.next() - let id1 = try XCTUnwrap(event?.id) - XCTAssertEqual(event, .connectionAdded(id1)) - event = await iterator.next() - XCTAssertEqual(event, .startedConnecting(id1)) - event = await iterator.next() - XCTAssertEqual(event, .connectSucceeded(id1, 100)) - event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id1, 1, 100)) - event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id1, 0, 100)) + let id1 = try XCTUnwrap(recorder.first?.id) + XCTAssertEqual(recorder.popFirst(), .connectionAdded(id1)) + XCTAssertEqual(recorder.popFirst(), .startedConnecting(id1)) + XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 0, 100)) // Start an RPC. let rpc = self.echo.collect() @@ -517,12 +496,9 @@ final class GRPCChannelPoolTests: GRPCTestCase { // Complete another one to make sure the previous one is known by the server. XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) - event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id1, 1, 100)) - event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id1, 2, 100)) - event = await iterator.next() - XCTAssertEqual(event, .connectionUtilizationChanged(id1, 1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 2, 100)) + XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) // Start shutting the server down. let didShutdown = self.server!.initiateGracefulShutdown() @@ -531,8 +507,7 @@ final class GRPCChannelPoolTests: GRPCTestCase { // Pause a moment so we know the client received the GOAWAY. let sleep = self.group.any().scheduleTask(in: .milliseconds(50)) {} XCTAssertNoThrow(try sleep.futureResult.wait()) - event = await iterator.next() - XCTAssertEqual(event, .connectionQuiescing(id1)) + XCTAssertEqual(recorder.popFirst(), .connectionQuiescing(id1)) // Finish the RPC. XCTAssertNoThrow(try rpc.sendEnd().wait()) From 84bac657e9930d26e9124bac082f26586dc2d209 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 22 Aug 2023 10:04:25 +0100 Subject: [PATCH 113/580] Bump version number to 1.19.1 (#1642) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.19.1 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 03264a81a..1763b2205 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 19 /// The patch version. - internal static let patch = 0 + internal static let patch = 1 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 5ea1768801a5cc4ede77b277b5fb60422e88f363 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Oct 2023 12:33:45 +0100 Subject: [PATCH 114/580] Add proposal for v2 stub design (#1652) --- .../GRPC/Docs.docc/Proposals/0001-stub-api.md | 1135 +++++++++++++++++ 1 file changed, 1135 insertions(+) create mode 100644 Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md diff --git a/Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md b/Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md new file mode 100644 index 000000000..4beb5f245 --- /dev/null +++ b/Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md @@ -0,0 +1,1135 @@ +# gRPC-0001: stub layer and interceptor API + +## Overview + +- Proposal: gRPC-0001 +- Author(s): [George Barnett](https://github.com/glbrntt) +- Revisions: + - v1 (25/09/23): + - Adds type-erased wrappers for `AsyncSequence` and `Writer`. + - Renames `BindableService` to `RPCService` + - Add `AsyncSequence` conveneince API to `Writer` + - Add note about possible workaround for clients returning responses + +## Introduction + +This proposal lays out the API design for the stub layer and interceptors for +gRPC Swift v2. + +See also https://forums.swift.org/t/grpc-swift-plans-for-v2/67361. + +## Motivation + +The stub layer and interceptors are the highest touch point API for users of +gRPC. It's important that the API: + +- Uses natural Swift idioms. +- Feels consistent between the client and server. +- Extends naturally to the interceptor API. +- Enforces the gRPC protocol by design. In other words, it should + be impossible or difficult to construct an invalid request or response + stream. +- Allows the gRPC protocol to fully expressed. For example, server RPC handlers + should be able to send initial and trailing metadata when they + choose to. + +## Detailed design + +This design uses the canonical "echo" service to illustrate the various call +types. The service has four methods each of which map to the four gRPC call +types: + +- Get: a unary RPC, +- Collect: a client streaming RPC, and +- Expand: a server streaming RPC, +- Update: a bidirectional RPC. + +The main focus of this design is the broad shape of the generated code. While +most users generate code from a service definition written in the Protocol +Buffers IDL, this design is agnostic to the source IDL. + +### Request and response objects + +When enumerating options and experimenting with different API, using request and +response objects emerged as the best fit. The request and response objects are +distinct between the client and the server and varied by the number of messages +they accept: + +Type | Used by | Messages +---------------------------|---------|---------- +`ClientRequest.Single` | Client | One +`ClientRequest.Stream` | Client | Many +`ServerRequest.Single` | Server | One +`ServerRequest.Stream` | Server | Many +`ClientResponse.Single` | Client | One +`ClientResponse.Stream` | Client | Many +`ServerResponse.Single` | Server | One +`ServerResponse.Stream` | Server | Many + +Objects to be consumed by users are "pull based" and use `AsyncSequence`s to +represent streams of messages. Objects where messages are produced by users +(`ClientRequest.Stream` and `ServerResponse.Stream`) are "push based" and use a +"producer function" to provide messages. These types are detailed below. + +```swift +public enum ClientRequest { + /// A request created by the client containing a single message. + public struct Single: Sendable { + /// Metadata sent at the begining of the RPC. + public var metadata: Metadata + + /// The message to send to the server. + public var message: Message + + /// Create a new single client request. + public init(message: Message, metadata: Metadata = [:]) { + // ... + } + } + + /// A request created by the client containing a message producer. + public struct Stream: Sendable { + public typealias Producer = @Sendable (RPCWriter) async throws -> Void + + /// Metadata sent at the begining of the RPC. + public var metadata: Metadata + + /// A closure which produces and writes messages into a writer destined for + /// the server. + /// + /// The producer will only be consumed once by gRPC and therefore isn't + /// required to be idempotent. If the producer throws an error then the RPC + /// will be cancelled. + public var producer: Producer + + /// Create a new streaming client request. + public init(metadata: Metadata = [:], producer: @escaping Producer) { + // ... + } + } +} + +public enum ServerRequest { + /// A request received at the server containing a single message. + public struct Single: Sendable { + /// Metadata received from the client at the begining of the RPC. + public var metadata: Metadata + + /// The message received from the client. + public var message: Message + + /// Create a new single server request. + public init(metadata: Metadata, message: Message) { + // ... + } + } + + /// A request received at the server containing a stream of messages. + public struct Stream: Sendable { + /// Metadata received from the client at the begining of the RPC. + public var metadata: Metadata + + /// An `AsyncSequence` of messages received from the client. + public var messages: RPCAsyncSequence + + /// Create a new streaming server request. + public init(metadata: Metadata, messages: RPCAsyncSequence) { + // ... + } + } +} + +public enum ServerResponse { + /// A response returned by a service for a single message. + public struct Single: Sendable { + /// The outcome of the RPC. + /// + /// The `success` indicates the server accepted the RPC for processing and + /// the RPC completed successfully. The `failure` case indicates that the + /// server either rejected the RPC or threw an error while processing the + /// request. In the `failure` case only a status and trailing metadata will + /// be returned to the client. + public var result: Result + + /// An accepted RPC with a successful outcome. + public struct Accepted { + /// Metadata to send to the client at the beginning of the response stream. + public var metadata: Metadata + + /// The single message to send back to the client. + public var message: Message + + /// Metadata to send to the client at the end of the response stream. + public var trailingMetadata: Metadata + } + + public init(result: Result) { + // ... + } + + /// Conveneince API to create an successful response. + public init(message: Message, metadata: Metadata = [:], trailingMetadata: Metadata = [:]) { + // ... + } + + /// Conveneince API to create an unsuccessful response. + public init(error: RPCError) { + // ... + } + } + + /// A response returned by a service producing a stream of messages. + public struct Stream: Sendable { + /// The initial outcome of the RPC; a `success` result indicates that the + /// services has accepted the RPC for processing. The RPC may still result + /// in failure by later throwing an error. + /// + /// The `failure` case indicates that the server rejected the RPC and will + /// not process it. Only status and trailing metadata will be sent to the + /// client. + public var result: Result + + /// A closure which, when called, writes values into the provided writer and + /// returns trailing metadata indicating the end of the response stream. + public typealias Producer = @Sendable (RPCWriter) async throws -> Metadata + + /// An accepted RPC. + public struct Accepted: Sendable { + /// Metadata to send to the client at the beginning of the response stream. + public var metadata: Metadata + + /// A closure which, when called, writes values into the provided writer and + /// returns trailing metadata indicating the end of the response stream. + /// + /// Returning metadata indicates a successful response and gRPC will + /// terminate the RPC with an 'ok' status code. Throwing an error will + /// terminate the RPC with an appropriate status code. You can control the + /// status code, message and metadata returned to the client by throwing an + /// `RPCError`. If the error thrown is not an `RPCError` then the `unknown` + /// status code is used. + /// + /// gRPC will invoke this function at most once therefore it isn't required + /// to be idempotent. + public var producer: Producer + } + + public init(result: Result) { + // ... + } + + /// Conveneince API to create an accepted response. + public init(metadata: Metadata = [:], producer: @escaping Producer) { + // ... + } + + /// Conveneince API to create an unsuccessful response. + public init(error: RPCError) { + // ... + } + } +} + +public enum ClientResponse { + public struct Single { + /// The body of an accepted single response. + public struct Body { + /// Metadata received from the server at the start of the RPC. + public var metadata: Metadata + + /// The message received from the server. + public var message: Message + + /// Metadata received from the server at the end of the RPC. + public var trailingMetadata: Metadata + } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` case indicates the RPC completed successfully with an 'ok' + /// status code. The `failure` case indicates that the RPC was rejected or + /// couldn't be completed successfully. + public var result: Result + + public init(result: Result) { + // ... + } + + // Note: it's possible to provide a number of conveneince APIs on top: + + /// The metadata received from server at the start of the RPC. + /// + /// The metadata will be empty if `result` is `failure`. + public var metadata: Metadata { + get { + // ... + } + } + + /// The message returned from the server. + /// + /// Throws if the RPC was rejected or failed. + public var message: Message { + get throws { + // ... + } + } + + /// The metadata received from server at the end of the RPC. + public var trailingMetadata: Metadata { + get { + // ... + } + } + } + + public struct Stream: Sendable { + public struct Body { + /// Metadata received from the server at the start of the RPC. + public var metadata: Metadata + + /// A sequence of messages received from the server ending with metadata + /// if the RPC succeeded. + /// + /// If the RPC fails then the sequence will throw an error. + public var bodyParts: RPCAsyncSequence + + public enum BodyPart: Sendable { + case message(Message) + case trailers(Metadata) + } + } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` case indicates the RPC was accepted by the server for + /// processing, however, the RPC may still fail by throwing an error from its + /// `messages` sequence. The `failure` case indicates that the RPC was + /// rejected by the server. + public var result: Result + + public init(result: Result) { + // ... + } + + // Note: it's possible to provide a number of conveneince APIs on top: + + /// The metadata received from server at the start of the RPC. + /// + /// The metadata will be empty if `result` is `failure`. + public var metadata: Metadata { + get { + // ... + } + } + + /// The stream of messages received from the server. + public var messages: RPCAsyncSequence { + get { + // ... + } + } + + /// The metadata received from server at the end of the RPC. + public var trailingMetadata: Metadata { + get { + // ... + } + } + } +} + +// MARK: - Supporting types + +/// A sink for values which are produced over time. +public protocol Writer: Sendable { + /// Write a sequence of elements. + /// + /// Writes may suspend if the sink is unable to accept writes. + func write(contentsOf elements: some Sequence) async throws +} + +extension Writer { + /// Write a single element. + public func write(_ element: Element) async throws { + try await self.write(contentsOf: CollectionOfOne(element)) + } + + /// Write an `AsyncSequence` of elements. + public func write( + contentsOf elements: Source + ) async throws where Source.Element == Element { + for try await element in elements { + try await self.write(element) + } + } +} + +/// A type-erasing `Writer`. +public struct RPCWriter: Writer { + public init(wrapping other: Other) where Other.Element == Element { + // ... + } +} + +/// A type-erasing `AsyncSequence`. +public struct RPCAsyncSequence: AsyncSequence, Sendable { + public init(wrapping other: Other) where Other.Element == Element { + // ... + } +} + +/// An RPC error. +/// +/// Every RPC is terminated with a status which includes a code, and optionally, +/// a message. The status describes the ultimate outcome of the RPC. +/// +/// This type is like a status but only represents negative outcomes, that is, +/// all status codes except for `ok`. This type can also carry ``Metadata``. +/// This can be used by service authors to transmit additional information to +/// clients if an RPC throws an error. +public struct RPCError: Error, Hashable, Sendable { + public struct Code: Hashable, Sendable { + public var code: UInt8 + + private init(_ code: UInt8) { + // ... + } + + // All non-zero status codes from: + // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md + + /// The operation was cancelled (typically by the caller). + public static let cancelled = Self(code: 1) + + /// Unknown error. An example of where this error may be returned is if a + /// status value received from another address space belongs to an error-space + /// that is not known in this address space. Also errors raised by APIs that + /// do not return enough error information may be converted to this error. + public static let unknown = Self(code: 2) + + // etc. + } + + /// The error code. + public var code: Code + + /// A message describing the error. + public var message: String + + /// Metadata associated with the error. + public var metadata: Metadata + + public init(code: Code, message: String, metadata: Metadata = [:]) { + // ... + } +} +``` + +### Generated server code + +Code generated for each service includes two protocols. The first, higher-level +protocol which most users interact with, includes one function per defined +method. The shape of the function matches the method definition. For example +unary methods accept a `ServerRequest.Single` and return a +`ServerResponse.Single`, bidirectional streaming methods accept a +`ServerRequest.Stream` and return a `ServerResponse.Stream`. + +The second, base protocol, defines each method in terms of streaming requests +and responses. The higher-level protocol refines the base protocol and provides +default implementations of methods in the base protocol in terms of their +higher-level counterpart. + +The base protocol is an escape hatch allowing advanced users to have further +control over their RPCs. As an example, if a service owner needs to respond to +initial metadata in a client streaming RPC before processing the complete stream +of messages from the request they could implement their RPC in terms of the +fully streaming version provided by the base protocol. + +Users can throw any error from each method. Since gRPC has a well defined error +model, gRPC Swift catches errors of type `RPCError` and extracts the code and +message. The code and message are propagated back to the client as the status of +the RPC. The library discards all other errors and returns a status with code +`unknown` to the client. + +The following code demonstrates how these protocols would look for the Echo +service. Some details are elided as they aren't relevant. + +```swift +// (Defined elsewhere.) +public typealias EchoRequest = ... +public typealias EchoResponse = ... + +/// The generated base protocol for the "Echo" service providing each method +/// in a fully streamed form. +/// +/// This protocol should typically not be implemented, instead you should +/// implement ``EchoServiceProtocol`` which refines this protocol. However, if +/// you require more granular control over your RPCs then they may implement +/// this protocol, or methods from this protocol, instead. +public protocol EchoServiceStreamingProtocol: RPCService, Sendable { + func get( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream + + func collect( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream + + func expand( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream + + func update( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream +} + +// Generated conformance to `RPCService`. +extension EchoServiceStreamingProtocol { + public func registerRPCs(with router: inout RPCRouter) { + // Implementation elided. + } +} + +/// The generated protocol for the "Echo" service. +/// +/// You must implement an instance of this protocol with your business logic and +/// register it with a server in order to use it. See also +/// ``EchoServiceStreamingProtocol``. +public protocol EchoServiceProtocol: EchoServiceStreamingProtocol { + func get( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single + + func collect( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single + + func expand( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream + + func update( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream +} + +// Generated partial conformance to `EchoServiceStreamingProtocol`. +extension EchoServiceProtocol { + public func get( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + // Implementation elided. Calls corresponding function on `EchoServiceStreamingProtocol`. + } + + public func collect( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + // Implementation elided. Calls corresponding function on `EchoServiceStreamingProtocol`. + } + + public func expand( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + // Implementation elided. Calls corresponding function on `EchoServiceStreamingProtocol`. + } + + // Note: 'update' has the same definition in `EchoServiceProtocol` and + // `EchoServiceStreamingProtocol` and is not required here. +} +``` + +#### Example: Echo service implementation + +One could implement the Echo service as: + +```swift +struct EchoService: EchoServiceProtocol { + func get( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + // Echo back the original message. + return ServerResponse.Single( + message: EchoResponse(text: "echo: \(request.message.text)") + ) + } + + func collect( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + // Gather all request messages and join them + let joined = try await request.messages.map { + $0.text + }.reduce(into: []) { + $0.append($1) + }.join(separator: " ") + + // Responsd with the joined message. Unlike 'get', we also echo back the + // request metadata as the leading and trailing metadata. + return ServerResponse.Single( + message: EchoResponse(text: "echo: \(joined)") + metadata: request.metadata, + trailingMetadata: request.metadata + ) + } + + func expand( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + // Echo back each part of the single request + for part in request.message.text.split(separator: " ") { + try await writer.write(EchoResponse(text: "echo: \(part)")) + } + + return [:] + } + } + + func update( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + // Echo back the request metadata as the initial metadata. + return ServerResponse.Stream(metadata: request.metadata) { writer in + // Echo back each request message + for try await message in request.messages { + try await writer.write(EchoResponse(text: "echo: \(message.text)")) + } + + // Echo back the request metadata as trailing metadata. + return request.metadata + } + } +} +``` + +### Generated client code + +The generated client code follows a similar pattern to the server code. Each +method has the same shape: it accepts a request and a closure which handles a +response from the server. The closure is generic over its return type and the +method returns that value to the caller once the closure exits. Having a +response handler provides a signal to the caller that once the closure exits the +RPC has finished and gRPC can free any related resources. + +Each method also has additional parameters which the generated code would +provide defaults for, including the request encoder and response decoder. +In most cases users would not need to specify the encoder and decoder. + +Code generated for the client includes a single protocol and a concrete +implementation of that protocol. The following code demonstrates how the +generated client protocol for the Echo service would look. + +```swift +// (Defined elsewhere.) +public typealias EchoRequest = ... +public typealias EchoResponse = ... + +/// The generated protocol for a client of the "Echo" service. +public protocol EchoClientProtocol: Sendable { + func get( + request: ClientRequest.Single, + encoder: some MessageEncoder, + decoder: some MessageDecoder, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async rethrows -> R + + func collect( + request: ClientRequest.Stream, + encoder: some MessageEncoder, + decoder: some MessageDecoder, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async rethrows -> R + + func expand( + request: ClientRequest.Single, + encoder: some MessageEncoder, + decoder: some MessageDecoder, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async rethrows -> R + + func update( + request: ClientRequest.Stream, + encoder: some MessageEncoder, + decoder: some MessageDecoder, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async rethrows -> R +} + +extension EchoClientProtocol { + public func get( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async rethrows -> R { + // Implementation elided. Calls corresponding function on + // `EchoClientProtocol` specifying the encoder and decoder. + } + + func collect( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async rethrows -> R { + // Implementation elided. Calls corresponding function on + // `EchoClientProtocol` specifying the encoder and decoder. + } + + func expand( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async rethrows -> R { + // Implementation elided. Calls corresponding function on + // `EchoClientProtocol` specifying the encoder and decoder. + } + + func update( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async rethrows -> R { + // Implementation elided. Calls corresponding function on + // `EchoClientProtocol` specifying the encoder and decoder. + } +} + +// Note: a concerete client implementation would also be generated. The details +// aren't interesting here. +``` + +#### Example: Echo client usage + +An example of using the Echo client follows. Some functions highlight the +"sugared" API built on top of the more verbose lower-level API: + +```swift +func get(echo: some EchoClientProtocol) async throws { + // Make the request: + let request = ClientRequest.Single(message: Echo.Request(text: "foo")) + + // Execute the RPC (most verbose API): + await echo.get(request: request) { response in + switch response.result { + case .success(let body): + print( + """ + 'Get' succeeded. + metadata: \(body.metadata) + message: \(body.message.text) + trailing metadata: \(body.trailingMetadata) + """ + ) + case .failure(let error): + print("'Get' failed with error code '\(error.code)' and metadata '\(error.metadata)'") + } + } + + // Execute the RPC (sugared API): + await echo.get(request: request) { response in + print("'Get' received metadata '\(response.metadata)'") + do { + let message = try response.message + print("'Get' received '\(message.text)'") + } catch { + print("'Get' caught error '\(error)'") + } + } + + // The generated code _could_ default the closure to return the response + // message which would make the common case straighforward: + let message = try await echo.get(request: request) + print("'Get' received '\(message.text)'") +} + +func clientStreaming(echo: some EchoClientProtocol) async throws { + // Make the request: + let request = ClientRequest.Stream { writer in + for text in ["foo", "bar", "baz"] { + try await writer.write(Echo.Request(text: text)) + } + } + + // Execute the RPC: + try await echo.collect(request: request) { response in + // (Same as for the unary "Get") + } +} + +func serverStreaming(echo: some EchoClientProtocol) async throws { + // Make the request, adding metadata: + let request = ClientRequest.Single( + message: Echo.Request(text: "foo bar baz"), + metadata: ["foo": "bar"], + ) + + // Execute the RPC (most verbose API): + try await echo.expand(request: request) { response in + switch response.result { + case .success(let body): + print("'Expand' accepted with metadata '\(body.metadata)'") + do { + for try await part in body.bodyParts { + switch part { + case .message(let message): + print("'Expand' received message '\(message.text)'") + case .trailers(let metadata): + print("'Expand' received trailers '\(metadata)'") + } + } + } catch let error as RPCError { + print("'Expand' failed with error '\(error.code)' and metadata '\(error.metadata)'") + } catch { + print("'Expand' failed with error '\(error)'") + } + case .failure(let error): + print("'Expand' rejected with error '\(error.code)' and metadata '\(error.metadata)'") + } + } + + // Execute the RPC (sugared API): + await echo.expand(request: request) { response in + print("'Expand' received metadata '\(response.metadata)'") + do { + for try await message in response.messages { + print("'Expand' received '\(message.text)'") + } + } catch let error as RPCError { + print("'Expand' failed with error '\(error.code)' and metadata '\(error.metadata)'") + } catch { + print("'Expand' failed with error '\(error)'") + } + } + + // Note: there is no generated 'default' handler, the function body defines + // the lifetime of the RPC and the RPC is cancelled once the closure exits. + // Therefore escaping the message sequence would result in the sequence + // throwing an error. It must be consumed from within the handler. It may + // be possible for the compiler to enforce this in the future with + // `~Escapable`. + // + // See: https://github.com/atrick/swift-evolution/blob/bufferview-roadmap/visions/language-support-for-BufferView.md +} + +func bidirectional(echo: some EchoClient) async throws { + // Make the request, adding metadata: + let request = ClientRequest.Stream(metadata: ["foo": "bar"]) { writer in + for text in ["foo", "bar", "baz"] { + try await writer.write(Echo.Request(text: text)) + } + } + + // Execute the RPC: + try await echo.collect(request: request) { response in + // (Same as for the server streaming "Expand") + } +} +``` + +### Interceptors + +Using the preceding patterns allows for interceptors to follow the shape of +bidirectional streaming calls. This is advantageous: once users are comfortable +with the bidirectional RPC interface the step to writing interceptors is +straighforward. + +The `protocol` for client and server interceptors also have the same shape: they +require a single function `intercept` which accept a `request`, `context`, and +`next` parameters. The `request` parameter is the request object which is +_always_ the streaming variant. The `context` provides additional information +about the intercepted RPC, and `next` is a closure that the interceptor may call +to forward the request and context to the next interceptor. + +```swift +/// A type that intercepts requests and response for clients. +/// +/// Interceptors allow users to inspect and modify requests and responses. +/// Requests are intercepted before they are handed to a transport. Responses +/// are intercepted after they have been received from the transport and before +/// they are returned to the client. +/// +/// They are typically used for cross-cutting concerns like injecting metadata, +/// validating messages, logging additional data, and tracing. +/// +/// Interceptors are registered with a client and apply to all RPCs. Use the +/// ``ClientContext/descriptor`` if you need to configure behaviour on a per-RPC +/// basis. +public protocol ClientInterceptor: Sendable { + /// Intercept a request object. + /// + /// - Parameters: + /// - request: The request object. + /// - context: Additional context about the request, including a descriptor + /// of the method being called. + /// - next: A closure to invoke to hand off the request and context to the next + /// interceptor in the chain. + /// - Returns: A response object. + func intercept( + request: ClientRequest.Stream, + context: ClientContext, + next: @Sendable ( + _ request: ClientRequest.Stream, + _ context: ClientContext + ) async throws -> ClientResponse.Stream + ) async throws -> ClientResponse.Stream +} + +/// A context passed to client interceptors containing additional information +/// about the RPC. +public struct ClientContext: Sendable { + /// A description of the method being called including the method and service + /// name. + public var descriptor: MethodDescriptor +} + +/// A type that intercepts requests and response for servers. +/// +/// Interceptors allow users to inspect and modify requests and responses. +/// Requests are intercepted after they have been received from the transport but +/// before they have been handed off to a service. Responses are intercepted +/// after they have been returned from a service and before they are written to +/// the transport. +/// +/// They are typically used for cross-cutting concerns like validating metadata +/// and messages, logging additional data, and tracing. +/// +/// Interceptors are registered with the server and apply to all RPCs. Use the +/// ``ClientContext/descriptor`` if you need to configure behaviour on a per-RPC +/// basis. +public protocol ServerInterceptor: Sendable { + /// Intercept a request object. + /// + /// - Parameters: + /// - request: The request object. + /// - context: Additional context about the request, including a descriptor + /// of the method being called. + /// - next: A closure to invoke to hand off the request and context to the next + /// interceptor in the chain. + /// - Returns: A response object. + func intercept( + request: ServerRequest.Stream, + context: ServerContext, + next: @Sendable ( + _ request: ServerRequest.Stream + _ context: ServerContext + ) async throws -> ServerResponse.Stream + ) async throws -> ServerResponse.Stream +} + +/// A context passed to server interceptors containing additional information +/// about the RPC. +public struct ServerContext: Sendable { + /// A description of the method being called including the method and service + /// name. + public var descriptor: MethodDescriptor +} +``` + +Importantly with this pattern, the API is a natural extension of both client and +server API for bidirectional streaming RPCs so users don't need to learn a new +paradigm, the same concepts apply. + +Some examples of interceptors include: + +```swift +struct AuthenticatingServerInterceptor: ServerInterceptor { + func intercept( + request: ServerRequest.Stream, + context: ServerContext, + next: @Sendable ( + _ request: ServerRequest.Stream + _ context: ServerContext + ) async throws -> ServerResponse.Stream + ) async throws -> ServerResponse.Stream { + guard let token = metadata["auth"], self.validate(token) else { + // Token is missing or not valid, reject the request and respond + // appropriately. + return ServerResponse.Stream( + error: RPCError(code: .unauthenticated, message: "...") + ) + } + + // Valid token is present, forward the request. + return try await next(request, context) + } +} + +struct LoggingClientInterceptor: ClientInterceptor { + struct LoggingWriter: Writer { + let base: RPCWriter + + init(wrapping base: RPCWriter) { + self.base = base + } + + func write(_ value: Value) async throws { + try await self.base.write(value) + print("Sent message: '\(value)'") + } + } + + func intercept( + request: ClientRequest.Stream, + context: ClientContext, + next: @Sendable ( + _ request: ClientRequest.Stream, + _ context: ClientContext + ) async throws -> ClientResponse.Stream + ) async throws -> ClientResponse.Stream { + // Construct a request which wraps the original and uses a logging writer + // to print a message every time a message is written. + let interceptedRequest = ClientRequest.Stream( + metadata: request.metadata + ) { writer in + let loggingWriter = LoggingWriter(wrapping: writer) + try await request.producer(loggingWriter) + print("Send end") + } + + print("Making request to '\(context.descriptor)', metadata: \(request.metadata)") + + let response = try await(interceptedRequest, context) + let interceptedResponse: ClientResponse.Stream + + // Inspect the response. On success re-map the body to print each part. On + // failure print the error. + switch response.result { + case .success(let body): + print("Call accepted, metadata: '\(body.metadata)'") + interceptedResponse = ClientResponse.Stream( + result = .success( + ClientResponse.Stream.Body( + metadata: body.metadata, + bodyParts: body.bodyParts.map { + switch $0 { + case .message(let message): + print("Received message: '\(message)'") + case .metadata(let metadata): + print("Received metadata: '\(metadata)'") + } + + return $0 + } + ) + ) + ) + + case .failure(let error): + print("Call failed with error code: '\(error.code)', metadata: '\(error.metadata)'") + interceptedResponse = response + } + + return interceptedResponse + } +} +``` + +## Alternative approaches + +When enumerating designs there were a number of alternatives considered which +were ultimately dismissed. These are briefly described in the following +sections. + +### Strong error typing + +As gRPC has well defined error codes, having API which enforce `RPCError` as +the thrown error type is appealing as it ensures service authors propagate +appropriate information to clients and clients know they can only observe +`RPCError`s. + +However, Swift doesn't currently have typed throws so would have to resort to +using `Result` types. While the API could be heavily sugared it doesn't result in +idiomatic code. There are a few places where using `Result` types simply doesn't +work in an ergonomic way. Each `AsyncSequence`, for example, would have to be +non-throwing and use `Result` types as their `Element`. + +> Note: there has been recent active work in this area for Embedded Swift so +> this might be worth revisiting in the future. +> +> https://forums.swift.org/t/status-check-typed-throws/66637 + +### Using `~Copyable` writers + +One appealing aspect of `~Copyable` types and ownership modifiers is that it's +easy to represent an writer as a kind of state machine. Consider a writer passed +to a server handler, for example. Initially it may either send metadata or it +may return a status as its first and only value. If it writes metadata it may +then write messages. After any number of messages it may then write a final +status. This composes naturally with `~Copyable` types: + +```swift +struct Writer: ~Copyable { + consuming func writeMetadata(_ metadata: Metadata) async throws -> Body { + // ... + } + + consuming func writeEnd(_ metadata: Metadata) async throws { + // ... + } + + struct Body: ~Copyable { + func writeMessage(_ message: Message) async throws { + // ... + } + + consuming func writeEnd(_ metadata: Metadata) async throws { + // ... + } + } +} +``` + +This neatly encapsulates the stream semantics of gRPC and uses the compiler to +ensure that writing an invalid stream is impossible. However, in practice it isn't +ergonomic to use as it requires dealing with multiple writer types and users can +still reach for writer functions after consuming the type, they just result in +an error. It also doesn't obviously allow for "default" values, the API forces users +to send initial metadata to get the `Body` writer. In practice most users don't +need to set metadata and the framework sends empty metadata on their behalf. + +### Using `AsyncSequence` for outbound message streams + +The `ClientRequest.Stream` and `ServerResponse.Stream` each have a `producer` +closure provided by callers. This is a "push" based system: callers must emit +messages into the writer when they wish to send messages to the other peer. + +An alternative to this would be to use a "pull" based API like `AsyncSequence`. +There are, however, some downsides to this. + +The first is that many `AsyncSequence` implementations don't appropriately exert +backpressure to the producer: `AsyncStream`, for example has an unbounded buffer +by default, this is problematic as the underlying transport may not be able to +consume messages as fast as the sequence is producing them. `AsyncChannel` has a +maximum buffer size of 1, while this wouldn't overwhelm the transport, it has +undesirable performance characteristics requiring expensive suspension points +for each message. The `Writer` approach allows the transport to directly exert +backpressure on the writer. + +Finally, on the server, to allow users to send trailing metadata, the +caller would have to deal with an `enum` of messages and metadata. The onus +would fall on the implementer of the service to ensure that metadata only +appears once as the final element in the stream. The proposed +`ServerResponse.Stream` avoids this by requiring the user to specify a closure +which accepts a writer and returns `Metadata`. This ensures implementers can +send any number of messages followed by exactly one `Metadata`. + +### Clients returning responses + +The proposed client API requires that the caller consumes responses within a +closure. A more obvious spelling is for the client methods to return a response +object to the caller. + +However, this has a number of issues. For example it doesn't make the lifetime +of the RPC obvious to the caller which can result in users accidentally keeping +expensive network resources alive for longer than necessary. Another issue is +that RPCs typically have deadlines associated with them which are naturally +modelled as separate `Task`s. These `Task`s need to run somewhere, which isn't +possible to do without resorting to unstructured concurrency. Using a response +handler allows the client to run the RPC within a `TaskGroup` and have the +response handler run as a child task next to any tasks which require running +concurrently. + +One workaround is for clients to have a long running `TaskGroup` for executing +such tasks required by RPCs. This would allow a model whereby the request is +intercepted in the callers task (and would therefore also have access to task +local values) and then be transferred to the clients long running task for +execution. In this model the lifetime of the RPC would be bounded by the +lifetime of the response object. One major downside to this approach is that +task locals are only reachable from the interceptors: they wouldn't be reachable +from the transport layer which may have its own transport-specific interceptors. From d090cf4fa275b7aef3c921bdc25eece3ffefa3d9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Oct 2023 16:09:04 +0100 Subject: [PATCH 115/580] Drop Swift 5.6 (#1657) Motivation: Now that Swift 5.9 has been released we can drop support for Swift 5.6. Modifications: - Update CI - Update minimum tools version - Remove code conditioned on Swift version - Update README Result: Swift 5.7 is the minimum supported Swift version --- .github/workflows/ci.yaml | 36 ++++++++++++-------------- Package.swift | 2 +- Performance/QPSBenchmark/Package.swift | 2 +- README.md | 3 ++- Sources/GRPC/Docs.docc/index.md | 3 ++- Sources/GRPC/PlatformSupport.swift | 12 --------- Sources/GRPC/Server.swift | 4 --- 7 files changed, 23 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f91e7d497..e1d92f9be 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,16 +24,16 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-focal + - image: swiftlang/swift:nightly-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.8-jammy + - image: swift:5.9-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.7-jammy + - image: swift:5.8-focal # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.6-focal + - image: swift:5.7-focal # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" name: Build and Test on ${{ matrix.image }} @@ -46,8 +46,6 @@ jobs: run: swift build ${{ matrix.swift-build-flags }} timeout-minutes: 20 - name: 🧪 Test - # Skip tests on 5.6: https://bugs.swift.org/browse/SR-15955 - if: ${{ matrix.image != 'swift:5.6-focal' }} run: swift test ${{ matrix.swift-test-flags }} timeout-minutes: 20 performance-tests: @@ -55,7 +53,7 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-focal + - image: swiftlang/swift:nightly-jammy env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -65,7 +63,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.8-jammy + - image: swift:5.9-jammy env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -75,7 +73,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.7-jammy + - image: swift:5.8-focal env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -85,16 +83,16 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.6-focal + - image: swift:5.7-focal env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 324000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 162000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 164000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 171000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 171000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -110,10 +108,10 @@ jobs: fail-fast: false matrix: include: - - image: swiftlang/swift:nightly-focal - - image: swift:5.8-jammy - - image: swift:5.7-jammy - - image: swift:5.6-focal + - image: swiftlang/swift:nightly-jammy + - image: swift:5.9-jammy + - image: swift:5.8-focal + - image: swift:5.7-focal name: Integration Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: diff --git a/Package.swift b/Package.swift index 2c89b5742..3f7b9a8b7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 /* * Copyright 2017, gRPC Authors All rights reserved. * diff --git a/Performance/QPSBenchmark/Package.swift b/Performance/QPSBenchmark/Package.swift index ba34e21a0..76bb18d92 100644 --- a/Performance/QPSBenchmark/Package.swift +++ b/Performance/QPSBenchmark/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 /* * Copyright 2020, gRPC Authors All rights reserved. * diff --git a/README.md b/README.md index f58727738..47fa297e8 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ gRPC Swift Version | Earliest Swift Version `1.0.0 ..< 1.8.0` | 5.2 `1.8.0 ..< 1.11.0` | 5.4 `1.11.0..< 1.16.0`.| 5.5 -`1.16.0...` | 5.6 +`1.16.0..< 1.20.0` | 5.6 +`1.20.0...` | 5.7 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md index d5ef1a85f..c93880bc7 100644 --- a/Sources/GRPC/Docs.docc/index.md +++ b/Sources/GRPC/Docs.docc/index.md @@ -27,7 +27,8 @@ gRPC Swift Version | Earliest Swift Version `1.0.0 ..< 1.8.0` | 5.2 `1.8.0 ..< 1.11.0` | 5.4 `1.11.0..< 1.16.0`.| 5.5 -`1.16.0...` | 5.6 +`1.16.0..< 1.20.0` | 5.6 +`1.20.0...` | 5.7 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift index 0ecfcd97d..32bce8671 100644 --- a/Sources/GRPC/PlatformSupport.swift +++ b/Sources/GRPC/PlatformSupport.swift @@ -123,12 +123,8 @@ public protocol ClientBootstrapProtocol { func connectTimeout(_ timeout: TimeAmount) -> Self func channelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption - #if swift(>=5.7) @preconcurrency func channelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture) -> Self - #else - func channelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture) -> Self - #endif } extension ClientBootstrapProtocol { @@ -161,24 +157,16 @@ public protocol ServerBootstrapProtocol { func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture func bind(to vsockAddress: VsockAddress) -> EventLoopFuture - #if swift(>=5.7) @preconcurrency func serverChannelInitializer( _ handler: @escaping @Sendable (Channel) -> EventLoopFuture ) -> Self - #else - func serverChannelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture) -> Self - #endif func serverChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption - #if swift(>=5.7) @preconcurrency func childChannelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture) -> Self - #else - func childChannelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture) -> Self - #endif func childChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption } diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index e6adf5299..3ff6331a1 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -569,11 +569,7 @@ extension Server.Configuration.CORS.AllowedOrigins { struct AnyCustomCORSAllowedOrigin: GRPCCustomCORSAllowedOrigin { private var checkOrigin: @Sendable (String) -> String? private let hashInto: @Sendable (inout Hasher) -> Void - #if swift(>=5.7) private let isEqualTo: @Sendable (any GRPCCustomCORSAllowedOrigin) -> Bool - #else - private let isEqualTo: @Sendable (Any) -> Bool - #endif init(_ wrap: W) { self.checkOrigin = { wrap.check(origin: $0) } From d576a74069417d5add87c889ad416180763a783c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Oct 2023 16:14:42 +0100 Subject: [PATCH 116/580] Add RPCError and Status (#1656) RPCError and Status Motivation: One mistake we made in v1 was having the status also be an error. This leads to interesting situations where a status with code 'ok' can be treated as an error. Here, we split them into two objects. Modifications: - Add a new GRPCCore target - Add RPCError and Status types and tests Result: RPCError and status are separate types. --- Package.swift | 21 ++ Sources/GRPCCore/Metadata.swift | 18 ++ Sources/GRPCCore/RPCError.swift | 244 ++++++++++++++++ Sources/GRPCCore/Status.swift | 264 ++++++++++++++++++ Tests/GRPCCoreTests/RPCErrorTests.swift | 101 +++++++ Tests/GRPCCoreTests/StatusTests.swift | 98 +++++++ .../Test Utilities/XCTest+Utilities.swift | 25 ++ 7 files changed, 771 insertions(+) create mode 100644 Sources/GRPCCore/Metadata.swift create mode 100644 Sources/GRPCCore/RPCError.swift create mode 100644 Sources/GRPCCore/Status.swift create mode 100644 Tests/GRPCCoreTests/RPCErrorTests.swift create mode 100644 Tests/GRPCCoreTests/StatusTests.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift diff --git a/Package.swift b/Package.swift index 3f7b9a8b7..16aea5d15 100644 --- a/Package.swift +++ b/Package.swift @@ -119,6 +119,8 @@ extension Target.Dependency { name: "SwiftProtobufPluginLibrary", package: "swift-protobuf" ) + + static let grpcCore: Self = .target(name: "GRPCCore") } // MARK: - Targets @@ -146,6 +148,12 @@ extension Target { path: "Sources/GRPC" ) + static let grpcCore: Target = .target( + name: "GRPCCore", + dependencies: [], + path: "Sources/GRPCCore" + ) + static let cgrpcZlib: Target = .target( name: cgrpcZlibTargetName, path: "Sources/CGRPCZlib", @@ -200,6 +208,13 @@ extension Target { ] ) + static let grpcCoreTests: Target = .testTarget( + name: "GRPCCoreTests", + dependencies: [ + .grpcCore, + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -476,6 +491,12 @@ let package = Package( .routeGuideClient, .routeGuideServer, .packetCapture, + + // v2 + .grpcCore, + + // v2 tests + .grpcCoreTests, ] ) diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift new file mode 100644 index 000000000..5a72fd161 --- /dev/null +++ b/Sources/GRPCCore/Metadata.swift @@ -0,0 +1,18 @@ +/* + * Copyright 2023, 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. + */ + +// FIXME: placeholder. +public typealias Metadata = [String: String] diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift new file mode 100644 index 000000000..46c27a4ce --- /dev/null +++ b/Sources/GRPCCore/RPCError.swift @@ -0,0 +1,244 @@ +/* + * Copyright 2023, 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. + */ + +/// An error representing the outcome of an RPC. +/// +/// See also ``Status``. +public struct RPCError: @unchecked Sendable, Hashable, Error { + // @unchecked because it relies on heap allocated storage and 'isKnownUniquelyReferenced' + + private var storage: Storage + private mutating func ensureStorageIsUnique() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + /// A code representing the high-level domain of the error. + public var code: Code { + get { self.storage.code } + set { + self.ensureStorageIsUnique() + self.storage.code = newValue + } + } + + /// A message providing additional context about the error. + public var message: String { + get { self.storage.message } + set { + self.ensureStorageIsUnique() + self.storage.message = newValue + } + } + + /// Metadata associated with the error. + /// + /// Any metadata included in the error thrown from a service will be sent back to the client and + /// conversely any ``RPCError`` received by the client may include metadata sent by a service. + /// + /// Note that clients and servers may synthesise errors which may not include metadata. + public var metadata: Metadata { + get { self.storage.metadata } + set { + self.ensureStorageIsUnique() + self.storage.metadata = newValue + } + } + + /// Create a new RPC error. + /// + /// - Parameters: + /// - code: The status code. + /// - message: A message providing additional context about the code. + /// - metadata: Any metadata to attach to the error. + public init(code: Code, message: String, metadata: Metadata = [:]) { + self.storage = Storage(code: code, message: message, metadata: metadata) + } + + /// Create a new RPC error from the provided ``Status``. + /// + /// Returns `nil` if the provided ``Status`` has code ``Status/Code-swift.struct/ok``. + /// + /// - Parameter status: The status to convert. + public init?(status: Status) { + guard let code = Code(statusCode: status.code) else { return nil } + self.init(code: code, message: status.message, metadata: [:]) + } +} + +extension RPCError: CustomStringConvertible { + public var description: String { + "\(self.code): \"\(self.message)\"" + } +} + +extension RPCError { + private final class Storage: Hashable { + var code: RPCError.Code + var message: String + var metadata: Metadata + + init(code: RPCError.Code, message: String, metadata: Metadata) { + self.code = code + self.message = message + self.metadata = metadata + } + + func copy() -> Self { + Self(code: self.code, message: self.message, metadata: self.metadata) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + hasher.combine(self.metadata) + } + + static func == (lhs: RPCError.Storage, rhs: RPCError.Storage) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message && lhs.metadata == rhs.metadata + } + } +} + +extension RPCError { + public struct Code: Hashable, Sendable, CustomStringConvertible { + /// The numeric value of the error code. + public var rawValue: Int { Int(self.wrapped.rawValue) } + + private var wrapped: Status.Code.Wrapped + private init(_ wrapped: Status.Code.Wrapped) { + self.wrapped = wrapped + } + + internal init?(statusCode: Status.Code) { + if statusCode == .ok { + return nil + } else { + self.wrapped = statusCode.wrapped + } + } + + public var description: String { + String(describing: self.wrapped) + } + } +} + +extension RPCError.Code { + /// The operation was cancelled (typically by the caller). + public static let cancelled = Self(.cancelled) + + /// Unknown error. An example of where this error may be returned is if a + /// Status value received from another address space belongs to an error-space + /// that is not known in this address space. Also errors raised by APIs that + /// do not return enough error information may be converted to this error. + public static let unknown = Self(.unknown) + + /// Client specified an invalid argument. Note that this differs from + /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are + /// problematic regardless of the state of the system (e.g., a malformed file + /// name). + public static let invalidArgument = Self(.invalidArgument) + + /// Deadline expired before operation could complete. For operations that + /// change the state of the system, this error may be returned even if the + /// operation has completed successfully. For example, a successful response + /// from a server could have been delayed long enough for the deadline to + /// expire. + public static let deadlineExceeded = Self(.deadlineExceeded) + + /// Some requested entity (e.g., file or directory) was not found. + public static let notFound = Self(.notFound) + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + public static let alreadyExists = Self(.alreadyExists) + + /// The caller does not have permission to execute the specified operation. + /// ``permissionDenied`` must not be used for rejections caused by exhausting + /// some resource (use ``resourceExhausted`` instead for those errors). + /// ``permissionDenied`` must not be used if the caller can not be identified + /// (use ``unauthenticated`` instead for those errors). + public static let permissionDenied = Self(.permissionDenied) + + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the + /// entire file system is out of space. + public static let resourceExhausted = Self(.resourceExhausted) + + /// Operation was rejected because the system is not in a state required for + /// the operation's execution. For example, directory to be deleted may be + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding + /// between ``failedPrecondition``, ``aborted``, and ``unavailable``: + /// - Use ``unavailable`` if the client can retry just the failing call. + /// - Use ``aborted`` if the client should retry at a higher-level + /// (e.g., restarting a read-modify-write sequence). + /// - Use ``failedPrecondition`` if the client should not retry until + /// the system state has been explicitly fixed. E.g., if an "rmdir" + /// fails because the directory is non-empty, ``failedPrecondition`` + /// should be returned since the client should not retry unless + /// they have first fixed up the directory by deleting files from it. + /// - Use ``failedPrecondition`` if the client performs conditional + /// REST Get/Update/Delete on a resource and the resource on the + /// server does not match the condition. E.g., conflicting + /// read-modify-write on the same resource. + public static let failedPrecondition = Self(.failedPrecondition) + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let aborted = Self(.aborted) + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike ``invalidArgument``, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate ``invalidArgument`` if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate ``outOfRange`` if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between ``failedPrecondition`` and + /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error) + /// when it applies so that callers who are iterating through a space can + /// easily look for an ``outOfRange`` error to detect when they are done. + public static let outOfRange = Self(.outOfRange) + + /// Operation is not implemented or not supported/enabled in this service. + public static let unimplemented = Self(.unimplemented) + + /// Internal errors. Means some invariants expected by underlying System has + /// been broken. If you see one of these errors, Something is very broken. + public static let internalError = Self(.internalError) + + /// The service is currently unavailable. This is a most likely a transient + /// condition and may be corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let unavailable = Self(.unavailable) + + /// Unrecoverable data loss or corruption. + public static let dataLoss = Self(.dataLoss) + + /// The request does not have valid authentication credentials for the + /// operation. + public static let unauthenticated = Self(.unauthenticated) +} diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift new file mode 100644 index 000000000..69e30bdb2 --- /dev/null +++ b/Sources/GRPCCore/Status.swift @@ -0,0 +1,264 @@ +/* + * Copyright 2023, 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. + */ + +/// A status object represents the outcome of an RPC. +/// +/// Each ``Status`` is composed of a ``Status/code-swift.property`` and ``Status/message``. Each +/// service implementation chooses the code and message returned to the client for each RPC +/// it implements. However, client and server implementations may also generate status objects +/// on their own if an error happens. +/// +/// ``Status`` represents the raw outcome of an RPC whether it was successful or not; ``RPCError`` +/// is similar to ``Status`` but only represents error cases, in other words represents all status +/// codes apart from ``Code-swift.struct/ok``. +public struct Status: @unchecked Sendable, Hashable { + // @unchecked because it relies on heap allocated storage and 'isKnownUniquelyReferenced' + + private var storage: Storage + private mutating func ensureStorageIsUnique() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + /// A code representing the high-level domain of the status. + public var code: Code { + get { self.storage.code } + set { + self.ensureStorageIsUnique() + self.storage.code = newValue + } + } + + /// A message providing additional context about the status. + public var message: String { + get { self.storage.message } + set { + self.ensureStorageIsUnique() + self.storage.message = newValue + } + } + + /// Create a new status. + /// + /// - Parameters: + /// - code: The status code. + /// - message: A message providing additional context about the code. + public init(code: Code, message: String) { + if code == .ok, message.isEmpty { + // Avoid a heap allocation for the common case. + self.storage = Storage.okWithNoMessage + } else { + self.storage = Storage(code: code, message: message) + } + } +} + +extension Status: CustomStringConvertible { + public var description: String { + "\(self.code): \"\(self.message)\"" + } +} + +extension Status { + private final class Storage: Hashable { + static let okWithNoMessage = Storage(code: .ok, message: "") + + var code: Status.Code + var message: String + + init(code: Status.Code, message: String) { + self.code = code + self.message = message + } + + func copy() -> Self { + Self(code: self.code, message: self.message) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + } + + static func == (lhs: Status.Storage, rhs: Status.Storage) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message + } + } +} + +extension Status { + /// Status codes for gRPC operations. + /// + /// The outcome of every RPC is indicated by a status code. + public struct Code: Hashable, CustomStringConvertible, Sendable { + // Source: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md + enum Wrapped: UInt8, Hashable, Sendable { + case ok = 0 + case cancelled = 1 + case unknown = 2 + case invalidArgument = 3 + case deadlineExceeded = 4 + case notFound = 5 + case alreadyExists = 6 + case permissionDenied = 7 + case resourceExhausted = 8 + case failedPrecondition = 9 + case aborted = 10 + case outOfRange = 11 + case unimplemented = 12 + case internalError = 13 + case unavailable = 14 + case dataLoss = 15 + case unauthenticated = 16 + } + + /// The underlying value. + let wrapped: Wrapped + + /// The numeric value of the error code. + public var rawValue: Int { Int(self.wrapped.rawValue) } + + /// Creates a status codes from its raw value. + /// + /// - Parameters: + /// - rawValue: The numeric value to create the code from. + /// Returns `nil` if the `rawValue` isn't a valid error code. + public init?(rawValue: Int) { + if let value = UInt8(exactly: rawValue), let wrapped = Wrapped(rawValue: value) { + self.wrapped = wrapped + } else { + return nil + } + } + + private init(_ wrapped: Wrapped) { + self.wrapped = wrapped + } + + public var description: String { + String(describing: self.wrapped) + } + } +} + +extension Status.Code { + /// The operation completed successfully. + public static let ok = Self(.ok) + + /// The operation was cancelled (typically by the caller). + public static let cancelled = Self(.cancelled) + + /// Unknown error. An example of where this error may be returned is if a + /// Status value received from another address space belongs to an error-space + /// that is not known in this address space. Also errors raised by APIs that + /// do not return enough error information may be converted to this error. + public static let unknown = Self(.unknown) + + /// Client specified an invalid argument. Note that this differs from + /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are + /// problematic regardless of the state of the system (e.g., a malformed file + /// name). + public static let invalidArgument = Self(.invalidArgument) + + /// Deadline expired before operation could complete. For operations that + /// change the state of the system, this error may be returned even if the + /// operation has completed successfully. For example, a successful response + /// from a server could have been delayed long enough for the deadline to + /// expire. + public static let deadlineExceeded = Self(.deadlineExceeded) + + /// Some requested entity (e.g., file or directory) was not found. + public static let notFound = Self(.notFound) + + /// Some entity that we attempted to create (e.g., file or directory) already + /// exists. + public static let alreadyExists = Self(.alreadyExists) + + /// The caller does not have permission to execute the specified operation. + /// ``permissionDenied`` must not be used for rejections caused by exhausting + /// some resource (use ``resourceExhausted`` instead for those errors). + /// ``permissionDenied`` must not be used if the caller can not be identified + /// (use ``unauthenticated`` instead for those errors). + public static let permissionDenied = Self(.permissionDenied) + + /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the + /// entire file system is out of space. + public static let resourceExhausted = Self(.resourceExhausted) + + /// Operation was rejected because the system is not in a state required for + /// the operation's execution. For example, directory to be deleted may be + /// non-empty, an rmdir operation is applied to a non-directory, etc. + /// + /// A litmus test that may help a service implementor in deciding + /// between ``failedPrecondition``, ``aborted``, and ``unavailable``: + /// - Use ``unavailable`` if the client can retry just the failing call. + /// - Use ``aborted`` if the client should retry at a higher-level + /// (e.g., restarting a read-modify-write sequence). + /// - Use ``failedPrecondition`` if the client should not retry until + /// the system state has been explicitly fixed. E.g., if an "rmdir" + /// fails because the directory is non-empty, ``failedPrecondition`` + /// should be returned since the client should not retry unless + /// they have first fixed up the directory by deleting files from it. + /// - Use ``failedPrecondition`` if the client performs conditional + /// REST Get/Update/Delete on a resource and the resource on the + /// server does not match the condition. E.g., conflicting + /// read-modify-write on the same resource. + public static let failedPrecondition = Self(.failedPrecondition) + + /// The operation was aborted, typically due to a concurrency issue like + /// sequencer check failures, transaction aborts, etc. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let aborted = Self(.aborted) + + /// Operation was attempted past the valid range. E.g., seeking or reading + /// past end of file. + /// + /// Unlike ``invalidArgument``, this error indicates a problem that may be fixed + /// if the system state changes. For example, a 32-bit file system will + /// generate ``invalidArgument`` if asked to read at an offset that is not in the + /// range [0,2^32-1], but it will generate ``outOfRange`` if asked to read from + /// an offset past the current file size. + /// + /// There is a fair bit of overlap between ``failedPrecondition`` and + /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error) + /// when it applies so that callers who are iterating through a space can + /// easily look for an ``outOfRange`` error to detect when they are done. + public static let outOfRange = Self(.outOfRange) + + /// Operation is not implemented or not supported/enabled in this service. + public static let unimplemented = Self(.unimplemented) + + /// Internal errors. Means some invariants expected by underlying System has + /// been broken. If you see one of these errors, Something is very broken. + public static let internalError = Self(.internalError) + + /// The service is currently unavailable. This is a most likely a transient + /// condition and may be corrected by retrying with a backoff. + /// + /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, + /// and ``unavailable``. + public static let unavailable = Self(.unavailable) + + /// Unrecoverable data loss or corruption. + public static let dataLoss = Self(.dataLoss) + + /// The request does not have valid authentication credentials for the + /// operation. + public static let unauthenticated = Self(.unauthenticated) +} diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift new file mode 100644 index 000000000..877af2aee --- /dev/null +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -0,0 +1,101 @@ +/* + * Copyright 2023, 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 RPCErrorTests: XCTestCase { + private static let statusCodeRawValue: [(RPCError.Code, Int)] = [ + (.cancelled, 1), + (.unknown, 2), + (.invalidArgument, 3), + (.deadlineExceeded, 4), + (.notFound, 5), + (.alreadyExists, 6), + (.permissionDenied, 7), + (.resourceExhausted, 8), + (.failedPrecondition, 9), + (.aborted, 10), + (.outOfRange, 11), + (.unimplemented, 12), + (.internalError, 13), + (.unavailable, 14), + (.dataLoss, 15), + (.unauthenticated, 16), + ] + + func testCustomStringConvertible() { + XCTAssertDescription(RPCError(code: .dataLoss, message: ""), #"dataLoss: """#) + XCTAssertDescription(RPCError(code: .unknown, message: "message"), #"unknown: "message""#) + XCTAssertDescription(RPCError(code: .aborted, message: "message"), #"aborted: "message""#) + } + + func testErrorFromStatus() throws { + var status = Status(code: .ok, message: "") + // ok isn't an error + XCTAssertNil(RPCError(status: status)) + + status.code = .invalidArgument + var error = try XCTUnwrap(RPCError(status: status)) + XCTAssertEqual(error.code, .invalidArgument) + XCTAssertEqual(error.message, "") + XCTAssertEqual(error.metadata, [:]) + + status.code = .cancelled + status.message = "an error message" + error = try XCTUnwrap(RPCError(status: status)) + XCTAssertEqual(error.code, .cancelled) + XCTAssertEqual(error.message, "an error message") + XCTAssertEqual(error.metadata, [:]) + } + + func testEquatableConformance() { + XCTAssertEqual( + RPCError(code: .cancelled, message: ""), + RPCError(code: .cancelled, message: "") + ) + + XCTAssertEqual( + RPCError(code: .cancelled, message: "message"), + RPCError(code: .cancelled, message: "message") + ) + + XCTAssertEqual( + RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]), + RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]) + ) + + XCTAssertNotEqual( + RPCError(code: .cancelled, message: ""), + RPCError(code: .cancelled, message: "message") + ) + + XCTAssertNotEqual( + RPCError(code: .cancelled, message: "message"), + RPCError(code: .unknown, message: "message") + ) + + XCTAssertNotEqual( + RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]), + RPCError(code: .cancelled, message: "message", metadata: ["foo": "baz"]) + ) + } + + func testStatusCodeRawValues() { + for (code, expected) in Self.statusCodeRawValue { + XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value") + } + } +} diff --git a/Tests/GRPCCoreTests/StatusTests.swift b/Tests/GRPCCoreTests/StatusTests.swift new file mode 100644 index 000000000..29c48b287 --- /dev/null +++ b/Tests/GRPCCoreTests/StatusTests.swift @@ -0,0 +1,98 @@ +/* + * Copyright 2023, 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 StatusTests: XCTestCase { + private static let statusCodeRawValue: [(Status.Code, Int)] = [ + (.ok, 0), + (.cancelled, 1), + (.unknown, 2), + (.invalidArgument, 3), + (.deadlineExceeded, 4), + (.notFound, 5), + (.alreadyExists, 6), + (.permissionDenied, 7), + (.resourceExhausted, 8), + (.failedPrecondition, 9), + (.aborted, 10), + (.outOfRange, 11), + (.unimplemented, 12), + (.internalError, 13), + (.unavailable, 14), + (.dataLoss, 15), + (.unauthenticated, 16), + ] + + func testCustomStringConvertible() { + XCTAssertDescription(Status(code: .ok, message: ""), #"ok: """#) + XCTAssertDescription(Status(code: .dataLoss, message: "message"), #"dataLoss: "message""#) + XCTAssertDescription(Status(code: .unknown, message: "message"), #"unknown: "message""#) + XCTAssertDescription(Status(code: .aborted, message: "message"), #"aborted: "message""#) + } + + func testStatusCodeRawValues() { + for (code, expected) in Self.statusCodeRawValue { + XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value") + } + } + + func testStatusCodeFromValidRawValue() { + for (expected, rawValue) in Self.statusCodeRawValue { + XCTAssertEqual( + Status.Code(rawValue: rawValue), + expected, + "\(rawValue) didn't convert to expected code \(expected)" + ) + } + } + + func testStatusCodeFromInvalidRawValue() { + // Internally represented as a `UInt8`; try all other values. + for rawValue in UInt8(17) ... UInt8.max { + XCTAssertNil(Status.Code(rawValue: Int(rawValue))) + } + + // API accepts `Int` so try invalid `Int` values too. + XCTAssertNil(Status.Code(rawValue: -1)) + XCTAssertNil(Status.Code(rawValue: 1000)) + XCTAssertNil(Status.Code(rawValue: .max)) + } + + func testEquatableConformance() { + XCTAssertEqual(Status(code: .ok, message: ""), Status(code: .ok, message: "")) + XCTAssertEqual(Status(code: .ok, message: "message"), Status(code: .ok, message: "message")) + + XCTAssertNotEqual( + Status(code: .ok, message: ""), + Status(code: .ok, message: "message") + ) + + XCTAssertNotEqual( + Status(code: .ok, message: "message"), + Status(code: .internalError, message: "message") + ) + + XCTAssertNotEqual( + Status(code: .ok, message: "message"), + Status(code: .ok, message: "different message") + ) + } + + func testFitsInExistentialContainer() { + XCTAssertLessThanOrEqual(MemoryLayout.size, 24) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift new file mode 100644 index 000000000..275396d4f --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -0,0 +1,25 @@ +/* + * Copyright 2023, 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 XCTest + +func XCTAssertDescription( + _ subject: some CustomStringConvertible, + _ expected: String, + file: StaticString = #filePath, + line: UInt = #line +) { + XCTAssertEqual(String(describing: subject), expected, file: file, line: line) +} From 09c46b0a6582652ebddb9bd6967ca2eb8fc67073 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:49:50 +0100 Subject: [PATCH 117/580] Tool that generates serialised file descriptor protos (#1654) Motivation: In order to implement the reflection service we need to be able to generate serialised file descriptor protos of the services and messages that a server offers. Modifications: Added an option for protoc-gen-grpc-swift that enables generating a binary file containing a base64 encoded representation of the serialized file descriptor proto of the specified proto file. Result: This change enables the generation of serialised file descriptor protos that will be useful in the implementation of the reflection service. --- Makefile | 14 +++ Package.swift | 1 + Sources/protoc-gen-grpc-swift/main.swift | 59 +++++++++---- Sources/protoc-gen-grpc-swift/options.swift | 8 ++ .../Serialization/SerializationTests.swift | 85 +++++++++++++++++++ .../Serialization/echo.grpc.reflection.txt | 1 + 6 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift create mode 100644 Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt diff --git a/Makefile b/Makefile index d0fe6930a..d9e10b0cf 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,20 @@ ${NORMALIZATION_GRPC}: ${NORMALIZATION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} .PHONY: generate-normalization: ${NORMALIZATION_PB} ${NORMALIZATION_GRPC} +SERIALIZATION_GRPC_REFLECTION=Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt + +# For serialization we'll set the ReflectionData option to true. +${SERIALIZATION_GRPC_REFLECTION}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ + --grpc-swift_out=$(dir ${SERIALIZATION_GRPC_REFLECTION}) + +# Generates binary file containing the serialized file descriptor proto for the Serialization test +.PHONY: +generate-reflection-data: ${SERIALIZATION_GRPC_REFLECTION} + ### Testing #################################################################### # Normal test suite. diff --git a/Package.swift b/Package.swift index 16aea5d15..120f8df6a 100644 --- a/Package.swift +++ b/Package.swift @@ -205,6 +205,7 @@ extension Target { ), exclude: [ "Codegen/Normalization/normalization.proto", + "Codegen/Serialization/echo.grpc.reflection.txt", ] ) diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 1901f688f..0f32bd53e 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -64,9 +64,10 @@ enum FileNaming: String { func outputFileName( component: String, fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming + fileNamingOption: FileNaming, + extension: String ) -> String { - let ext = "." + component + ".swift" + let ext = "." + component + "." + `extension` let pathParts = splitPath(pathname: fileDescriptor.name) switch fileNamingOption { case .FullPath: @@ -84,19 +85,22 @@ func uniqueOutputFileName( component: String, fileDescriptor: FileDescriptor, fileNamingOption: FileNaming, - generatedFiles: inout [String: Int] + generatedFiles: inout [String: Int], + extension: String = "swift" ) -> String { let defaultName = outputFileName( component: component, fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption + fileNamingOption: fileNamingOption, + extension: `extension` ) if let count = generatedFiles[defaultName] { generatedFiles[defaultName] = count + 1 return outputFileName( component: "\(count)." + component, fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption + fileNamingOption: fileNamingOption, + extension: `extension` ) } else { generatedFiles[defaultName] = 1 @@ -136,19 +140,38 @@ func main(args: [String]) throws { // Only generate output for services. for name in request.fileToGenerate { - if let fileDescriptor = descriptorSet.fileDescriptor(named: name), - !fileDescriptor.services.isEmpty { - let grpcFileName = uniqueOutputFileName( - component: "grpc", - fileDescriptor: fileDescriptor, - fileNamingOption: options.fileNaming, - generatedFiles: &generatedFiles - ) - let grpcGenerator = Generator(fileDescriptor, options: options) - var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() - grpcFile.name = grpcFileName - grpcFile.content = grpcGenerator.code - response.file.append(grpcFile) + if let fileDescriptor = descriptorSet.fileDescriptor(named: name) { + if (options.generateReflectionData) { + var binaryFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() + let binaryFileName = uniqueOutputFileName( + component: "grpc.reflection", + fileDescriptor: fileDescriptor, + fileNamingOption: options.fileNaming, + generatedFiles: &generatedFiles, + extension: "txt" + ) + let serializedFileDescriptorProto = try fileDescriptor.proto.serializedData() + .base64EncodedString() + binaryFile.name = binaryFileName + binaryFile.content = serializedFileDescriptorProto + response.file.append(binaryFile) + } + if ( + !fileDescriptor.services + .isEmpty && (options.generateClient || options.generateServer) + ) { + var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() + let grpcFileName = uniqueOutputFileName( + component: "grpc", + fileDescriptor: fileDescriptor, + fileNamingOption: options.fileNaming, + generatedFiles: &generatedFiles + ) + let grpcGenerator = Generator(fileDescriptor, options: options) + grpcFile.name = grpcFileName + grpcFile.content = grpcGenerator.code + response.file.append(grpcFile) + } } } diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/options.swift index 15d141def..bdef17338 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/options.swift @@ -64,6 +64,7 @@ final class GeneratorOptions { private(set) var extraModuleImports: [String] = [] private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" + private(set) var generateReflectionData = false init(parameter: String?) throws { for pair in GeneratorOptions.parseParameter(string: parameter) { @@ -143,6 +144,13 @@ final class GeneratorOptions { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } + case "ReflectionData": + if let value = Bool(pair.value) { + self.generateReflectionData = value + } else { + throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) + } + default: throw GenerationError.unknownParameter(name: pair.key) } diff --git a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift new file mode 100644 index 000000000..f8f9174e9 --- /dev/null +++ b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift @@ -0,0 +1,85 @@ +/* + * Copyright 2023, 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 Foundation +import GRPC +import SwiftProtobuf +import XCTest + +final class SerializationTests: GRPCTestCase { + var fileDescriptorProto: Google_Protobuf_FileDescriptorProto! + + override func setUp() { + super.setUp() + let binaryFileURL = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent().appendingPathComponent("echo.grpc.reflection.txt") + let base64EncodedData = try! Data(contentsOf: binaryFileURL) + let binaryData = Data(base64Encoded: base64EncodedData)! + self + .fileDescriptorProto = + try! Google_Protobuf_FileDescriptorProto(serializedData: binaryData) + } + + func testFileDescriptorMetadata() throws { + let name = self.fileDescriptorProto.name + XCTAssertEqual(name, "echo.proto") + + let syntax = self.fileDescriptorProto.syntax + XCTAssertEqual(syntax, "proto3") + + let package = self.fileDescriptorProto.package + XCTAssertEqual(package, "echo") + } + + func testFileDescriptorMessages() { + let messages = self.fileDescriptorProto.messageType + XCTAssertEqual(messages.count, 2) + for message in messages { + XCTAssert((message.name == "EchoRequest") || (message.name == "EchoResponse")) + XCTAssertEqual(message.field.count, 1) + XCTAssertEqual(message.field.first!.name, "text") + XCTAssert(message.field.first!.hasNumber) + } + } + + func testFileDescriptorServices() { + let services = self.fileDescriptorProto.service + XCTAssertEqual(services.count, 1) + XCTAssertEqual(self.fileDescriptorProto.service.first!.method.count, 4) + for method in self.fileDescriptorProto.service.first!.method { + switch method.name { + case "Get": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + case "Expand": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + XCTAssert(method.serverStreaming) + case "Collect": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + XCTAssert(method.clientStreaming) + case "Update": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + XCTAssert(method.clientStreaming) + XCTAssert(method.serverStreaming) + default: + XCTFail("The method name is incorrect.") + } + } + } +} diff --git a/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt b/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt new file mode 100644 index 000000000..af26ef4a7 --- /dev/null +++ b/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt @@ -0,0 +1 @@ +CgplY2hvLnByb3RvEgRlY2hvIiEKC0VjaG9SZXF1ZXN0EhIKBHRleHQYASABKAlSBHRleHQiIgoMRWNob1Jlc3BvbnNlEhIKBHRleHQYASABKAlSBHRleHQy2AEKBEVjaG8SLgoDR2V0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgASMwoGRXhwYW5kEhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAwARI0CgdDb2xsZWN0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAoARI1CgZVcGRhdGUSES5lY2hvLkVjaG9SZXF1ZXN0GhIuZWNoby5FY2hvUmVzcG9uc2UiACgBMAFK/QoKBhIEDgAoAQrCBAoBDBIDDgASMrcEIENvcHlyaWdodCAoYykgMjAxNSwgR29vZ2xlIEluYy4KCiBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCggKAQISAxAADQoKCgIGABIEEgAeAQoKCgMGAAESAxIIDAo4CgQGAAIAEgMUAjAaKyBJbW1lZGlhdGVseSByZXR1cm5zIGFuIGVjaG8gb2YgYSByZXF1ZXN0LgoKDAoFBgACAAESAxQGCQoMCgUGAAIAAhIDFAoVCgwKBQYAAgADEgMUICwKWQoEBgACARIDFwI6GkwgU3BsaXRzIGEgcmVxdWVzdCBpbnRvIHdvcmRzIGFuZCByZXR1cm5zIGVhY2ggd29yZCBpbiBhIHN0cmVhbSBvZiBtZXNzYWdlcy4KCgwKBQYAAgEBEgMXBgwKDAoFBgACAQISAxcNGAoMCgUGAAIBBhIDFyMpCgwKBQYAAgEDEgMXKjYKYgoEBgACAhIDGgI7GlUgQ29sbGVjdHMgYSBzdHJlYW0gb2YgbWVzc2FnZXMgYW5kIHJldHVybnMgdGhlbSBjb25jYXRlbmF0ZWQgd2hlbiB0aGUgY2FsbGVyIGNsb3Nlcy4KCgwKBQYAAgIBEgMaBg0KDAoFBgACAgUSAxoOFAoMCgUGAAICAhIDGhUgCgwKBQYAAgIDEgMaKzcKTQoEBgACAxIDHQJBGkAgU3RyZWFtcyBiYWNrIG1lc3NhZ2VzIGFzIHRoZXkgYXJlIHJlY2VpdmVkIGluIGFuIGlucHV0IHN0cmVhbS4KCgwKBQYAAgMBEgMdBgwKDAoFBgACAwUSAx0NEwoMCgUGAAIDAhIDHRQfCgwKBQYAAgMGEgMdKjAKDAoFBgACAwMSAx0xPQoKCgIEABIEIAAjAQoKCgMEAAESAyAIEwoyCgQEAAIAEgMiAhIaJSBUaGUgdGV4dCBvZiBhIG1lc3NhZ2UgdG8gYmUgZWNob2VkLgoKDAoFBAACAAUSAyICCAoMCgUEAAIAARIDIgkNCgwKBQQAAgADEgMiEBEKCgoCBAESBCUAKAEKCgoDBAEBEgMlCBQKLAoEBAECABIDJwISGh8gVGhlIHRleHQgb2YgYW4gZWNobyByZXNwb25zZS4KCgwKBQQBAgAFEgMnAggKDAoFBAECAAESAycJDQoMCgUEAQIAAxIDJxARYgZwcm90bzM= \ No newline at end of file From 62882cccb33a4387ae60112287b1a72c35a4ef08 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:45:17 +0100 Subject: [PATCH 118/580] Generated reflection service code (#1659) Motivation: In order to develop the Reflection Service, the specific proto file should be included in the module and the service code generated from it. Modifications: Created the module for the Reflection Service, added the proto file, updated the Package.swift to include the module, updated the Makefile to inlude targets for generating the server code, created the ReflectionProvider. Result: The Reflection Service can now be implemented. --- Makefile | 16 + Package.swift | 15 + .../Model/reflection.grpc.swift | 122 +++ .../Model/reflection.pb.swift | 843 ++++++++++++++++++ .../Model/reflection.proto | 141 +++ .../Server/ReflectionProvider.swift | 29 + 6 files changed, 1166 insertions(+) create mode 100644 Sources/GRPCReflectionService/Model/reflection.grpc.swift create mode 100644 Sources/GRPCReflectionService/Model/reflection.pb.swift create mode 100644 Sources/GRPCReflectionService/Model/reflection.proto create mode 100644 Sources/GRPCReflectionService/Server/ReflectionProvider.swift diff --git a/Makefile b/Makefile index d9e10b0cf..c908ea67d 100644 --- a/Makefile +++ b/Makefile @@ -131,6 +131,22 @@ ${SERIALIZATION_GRPC_REFLECTION}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} .PHONY: generate-reflection-data: ${SERIALIZATION_GRPC_REFLECTION} +REFLECTION_PROTO=Sources/GRPCReflectionService/Model/reflection.proto +REFLECTION_PB=$(REFLECTION_PROTO:.proto=.pb.swift) +REFLECTION_GRPC=$(REFLECTION_PROTO:.proto=.grpc.swift) + +# For Reflection we'll generate only the Server code. +${REFLECTION_GRPC}: ${REFLECTION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=false \ + --grpc-swift_out=$(dir $<) + +# Generates protobufs and gRPC server for the Reflection Service +.PHONY: +generate-reflection: ${REFLECTION_PB} ${REFLECTION_GRPC} + ### Testing #################################################################### # Normal test suite. diff --git a/Package.swift b/Package.swift index 120f8df6a..9475ee8c0 100644 --- a/Package.swift +++ b/Package.swift @@ -89,6 +89,7 @@ extension Target.Dependency { static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") static let interopTestImplementation: Self = .target(name: "GRPCInteroperabilityTestsImplementation") + static let reflectionService: Self = .target(name: "GRPCReflectionService") // Product dependencies static let argumentParser: Self = .product( @@ -428,6 +429,19 @@ extension Target { "README.md", ] ) + + static let reflectionService: Target = .target( + name: "GRPCReflectionService", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/GRPCReflectionService", + exclude: [ + "Model/reflection.proto", + ] + ) } // MARK: - Products @@ -471,6 +485,7 @@ let package = Package( .cgrpcZlib, .protocGenGRPCSwift, .grpcSwiftPlugin, + .reflectionService, // Tests etc. .grpcTests, diff --git a/Sources/GRPCReflectionService/Model/reflection.grpc.swift b/Sources/GRPCReflectionService/Model/reflection.grpc.swift new file mode 100644 index 000000000..2239744da --- /dev/null +++ b/Sources/GRPCReflectionService/Model/reflection.grpc.swift @@ -0,0 +1,122 @@ +// +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the protocol buffer compiler. +// Source: reflection.proto +// +import GRPC +import NIO +import NIOConcurrencyHelpers +import SwiftProtobuf + + +/// To build a server, implement a class that conforms to this protocol. +internal protocol Reflection_ServerReflectionProvider: CallHandlerProvider { + var interceptors: Reflection_ServerReflectionServerInterceptorFactoryProtocol? { get } + + /// The reflection service is structured as a bidirectional stream, ensuring + /// all related requests go to a single server. + func serverReflectionInfo(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> +} + +extension Reflection_ServerReflectionProvider { + internal var serviceName: Substring { + return Reflection_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] + } + + /// Determines, calls and returns the appropriate request handler, depending on the request's method. + /// Returns nil for methods not handled by this service. + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "ServerReflectionInfo": + return BidirectionalStreamingServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], + observerFactory: self.serverReflectionInfo(context:) + ) + + default: + return nil + } + } +} + +/// To implement a server, implement an object which conforms to this protocol. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Reflection_ServerReflectionAsyncProvider: CallHandlerProvider, Sendable { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Reflection_ServerReflectionServerInterceptorFactoryProtocol? { get } + + /// The reflection service is structured as a bidirectional stream, ensuring + /// all related requests go to a single server. + func serverReflectionInfo( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Reflection_ServerReflectionAsyncProvider { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Reflection_ServerReflectionServerMetadata.serviceDescriptor + } + + internal var serviceName: Substring { + return Reflection_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] + } + + internal var interceptors: Reflection_ServerReflectionServerInterceptorFactoryProtocol? { + return nil + } + + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "ServerReflectionInfo": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], + wrapping: { try await self.serverReflectionInfo(requestStream: $0, responseStream: $1, context: $2) } + ) + + default: + return nil + } + } +} + +internal protocol Reflection_ServerReflectionServerInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when handling 'serverReflectionInfo'. + /// Defaults to calling `self.makeInterceptors()`. + func makeServerReflectionInfoInterceptors() -> [ServerInterceptor] +} + +internal enum Reflection_ServerReflectionServerMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "ServerReflection", + fullName: "reflection.ServerReflection", + methods: [ + Reflection_ServerReflectionServerMetadata.Methods.serverReflectionInfo, + ] + ) + + internal enum Methods { + internal static let serverReflectionInfo = GRPCMethodDescriptor( + name: "ServerReflectionInfo", + path: "/reflection.ServerReflection/ServerReflectionInfo", + type: GRPCCallType.bidirectionalStreaming + ) + } +} diff --git a/Sources/GRPCReflectionService/Model/reflection.pb.swift b/Sources/GRPCReflectionService/Model/reflection.pb.swift new file mode 100644 index 000000000..81bb72e3f --- /dev/null +++ b/Sources/GRPCReflectionService/Model/reflection.pb.swift @@ -0,0 +1,843 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: reflection.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2023 The gRPC Authors +// +// 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. + +// Service exported by server reflection. A more complete description of how +// server reflection works can be found at +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +// +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The message sent by the client when calling ServerReflectionInfo method. +public struct Reflection_ServerReflectionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var host: String = String() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + public var messageRequest: Reflection_ServerReflectionRequest.OneOf_MessageRequest? = nil + + /// Find a proto file by the file name. + public var fileByFilename: String { + get { + if case .fileByFilename(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileByFilename(newValue)} + } + + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + public var fileContainingSymbol: String { + get { + if case .fileContainingSymbol(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileContainingSymbol(newValue)} + } + + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + public var fileContainingExtension: Reflection_ExtensionRequest { + get { + if case .fileContainingExtension(let v)? = messageRequest {return v} + return Reflection_ExtensionRequest() + } + set {messageRequest = .fileContainingExtension(newValue)} + } + + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + public var allExtensionNumbersOfType: String { + get { + if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .allExtensionNumbersOfType(newValue)} + } + + /// List the full names of registered services. The content will not be + /// checked. + public var listServices: String { + get { + if case .listServices(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .listServices(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + public enum OneOf_MessageRequest: Equatable { + /// Find a proto file by the file name. + case fileByFilename(String) + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + case fileContainingSymbol(String) + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + case fileContainingExtension(Reflection_ExtensionRequest) + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + case allExtensionNumbersOfType(String) + /// List the full names of registered services. The content will not be + /// checked. + case listServices(String) + + #if !swift(>=4.1) + public static func ==(lhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest, rhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileByFilename, .fileByFilename): return { + guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingSymbol, .fileContainingSymbol): return { + guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingExtension, .fileContainingExtension): return { + guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { + guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServices, .listServices): return { + guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + public init() {} +} + +/// The type name and extension number sent by the client when requesting +/// file_containing_extension. +public struct Reflection_ExtensionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Fully-qualified type name. The format should be . + public var containingType: String = String() + + public var extensionNumber: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// The message sent by the server to answer ServerReflectionInfo method. +public struct Reflection_ServerReflectionResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var validHost: String = String() + + public var originalRequest: Reflection_ServerReflectionRequest { + get {return _originalRequest ?? Reflection_ServerReflectionRequest()} + set {_originalRequest = newValue} + } + /// Returns true if `originalRequest` has been explicitly set. + public var hasOriginalRequest: Bool {return self._originalRequest != nil} + /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. + public mutating func clearOriginalRequest() {self._originalRequest = nil} + + /// The server sets one of the following fields according to the message_request + /// in the request. + public var messageResponse: Reflection_ServerReflectionResponse.OneOf_MessageResponse? = nil + + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. + /// As the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + public var fileDescriptorResponse: Reflection_FileDescriptorResponse { + get { + if case .fileDescriptorResponse(let v)? = messageResponse {return v} + return Reflection_FileDescriptorResponse() + } + set {messageResponse = .fileDescriptorResponse(newValue)} + } + + /// This message is used to answer all_extension_numbers_of_type requests. + public var allExtensionNumbersResponse: Reflection_ExtensionNumberResponse { + get { + if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} + return Reflection_ExtensionNumberResponse() + } + set {messageResponse = .allExtensionNumbersResponse(newValue)} + } + + /// This message is used to answer list_services requests. + public var listServicesResponse: Reflection_ListServiceResponse { + get { + if case .listServicesResponse(let v)? = messageResponse {return v} + return Reflection_ListServiceResponse() + } + set {messageResponse = .listServicesResponse(newValue)} + } + + /// This message is used when an error occurs. + public var errorResponse: Reflection_ErrorResponse { + get { + if case .errorResponse(let v)? = messageResponse {return v} + return Reflection_ErrorResponse() + } + set {messageResponse = .errorResponse(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The server sets one of the following fields according to the message_request + /// in the request. + public enum OneOf_MessageResponse: Equatable { + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. + /// As the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + case fileDescriptorResponse(Reflection_FileDescriptorResponse) + /// This message is used to answer all_extension_numbers_of_type requests. + case allExtensionNumbersResponse(Reflection_ExtensionNumberResponse) + /// This message is used to answer list_services requests. + case listServicesResponse(Reflection_ListServiceResponse) + /// This message is used when an error occurs. + case errorResponse(Reflection_ErrorResponse) + + #if !swift(>=4.1) + public static func ==(lhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse, rhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileDescriptorResponse, .fileDescriptorResponse): return { + guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { + guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServicesResponse, .listServicesResponse): return { + guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.errorResponse, .errorResponse): return { + guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + public init() {} + + fileprivate var _originalRequest: Reflection_ServerReflectionRequest? = nil +} + +/// Serialized FileDescriptorProto messages sent by the server answering +/// a file_by_filename, file_containing_symbol, or file_containing_extension +/// request. +public struct Reflection_FileDescriptorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Serialized FileDescriptorProto messages. We avoid taking a dependency on + /// descriptor.proto, which uses proto2 only features, by making them opaque + /// bytes instead. + public var fileDescriptorProto: [Data] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// A list of extension numbers sent by the server answering +/// all_extension_numbers_of_type request. +public struct Reflection_ExtensionNumberResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of the base type, including the package name. The format + /// is . + public var baseTypeName: String = String() + + public var extensionNumber: [Int32] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// A list of ServiceResponse sent by the server answering list_services request. +public struct Reflection_ListServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The information of each service may be expanded in the future, so we use + /// ServiceResponse message to encapsulate it. + public var service: [Reflection_ServiceResponse] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// The information of a single service used by ListServiceResponse to answer +/// list_services request. +public struct Reflection_ServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of a registered service, including its package name. The format + /// is . + public var name: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// The error code and error message sent by the server when an error occurs. +public struct Reflection_ErrorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// This field uses the error codes defined in grpc::StatusCode. + public var errorCode: Int32 = 0 + + public var errorMessage: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Reflection_ServerReflectionRequest: @unchecked Sendable {} +extension Reflection_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} +extension Reflection_ExtensionRequest: @unchecked Sendable {} +extension Reflection_ServerReflectionResponse: @unchecked Sendable {} +extension Reflection_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} +extension Reflection_FileDescriptorResponse: @unchecked Sendable {} +extension Reflection_ExtensionNumberResponse: @unchecked Sendable {} +extension Reflection_ListServiceResponse: @unchecked Sendable {} +extension Reflection_ServiceResponse: @unchecked Sendable {} +extension Reflection_ErrorResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "reflection" + +extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "host"), + 3: .standard(proto: "file_by_filename"), + 4: .standard(proto: "file_containing_symbol"), + 5: .standard(proto: "file_containing_extension"), + 6: .standard(proto: "all_extension_numbers_of_type"), + 7: .standard(proto: "list_services"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() + case 3: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileByFilename(v) + } + }() + case 4: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingSymbol(v) + } + }() + case 5: try { + var v: Reflection_ExtensionRequest? + var hadOneofValue = false + if let current = self.messageRequest { + hadOneofValue = true + if case .fileContainingExtension(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingExtension(v) + } + }() + case 6: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .allExtensionNumbersOfType(v) + } + }() + case 7: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .listServices(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.host.isEmpty { + try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) + } + switch self.messageRequest { + case .fileByFilename?: try { + guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + }() + case .fileContainingSymbol?: try { + guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + }() + case .fileContainingExtension?: try { + guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .allExtensionNumbersOfType?: try { + guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + }() + case .listServices?: try { + guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_ServerReflectionRequest, rhs: Reflection_ServerReflectionRequest) -> Bool { + if lhs.host != rhs.host {return false} + if lhs.messageRequest != rhs.messageRequest {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "containing_type"), + 2: .standard(proto: "extension_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.containingType.isEmpty { + try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) + } + if self.extensionNumber != 0 { + try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_ExtensionRequest, rhs: Reflection_ExtensionRequest) -> Bool { + if lhs.containingType != rhs.containingType {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "valid_host"), + 2: .standard(proto: "original_request"), + 4: .standard(proto: "file_descriptor_response"), + 5: .standard(proto: "all_extension_numbers_response"), + 6: .standard(proto: "list_services_response"), + 7: .standard(proto: "error_response"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() + case 4: try { + var v: Reflection_FileDescriptorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .fileDescriptorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .fileDescriptorResponse(v) + } + }() + case 5: try { + var v: Reflection_ExtensionNumberResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .allExtensionNumbersResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .allExtensionNumbersResponse(v) + } + }() + case 6: try { + var v: Reflection_ListServiceResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .listServicesResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .listServicesResponse(v) + } + }() + case 7: try { + var v: Reflection_ErrorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .errorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .errorResponse(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.validHost.isEmpty { + try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) + } + try { if let v = self._originalRequest { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + switch self.messageResponse { + case .fileDescriptorResponse?: try { + guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .allExtensionNumbersResponse?: try { + guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .listServicesResponse?: try { + guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .errorResponse?: try { + guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_ServerReflectionResponse, rhs: Reflection_ServerReflectionResponse) -> Bool { + if lhs.validHost != rhs.validHost {return false} + if lhs._originalRequest != rhs._originalRequest {return false} + if lhs.messageResponse != rhs.messageResponse {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "file_descriptor_proto"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.fileDescriptorProto.isEmpty { + try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_FileDescriptorResponse, rhs: Reflection_FileDescriptorResponse) -> Bool { + if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "base_type_name"), + 2: .standard(proto: "extension_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.baseTypeName.isEmpty { + try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) + } + if !self.extensionNumber.isEmpty { + try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_ExtensionNumberResponse, rhs: Reflection_ExtensionNumberResponse) -> Bool { + if lhs.baseTypeName != rhs.baseTypeName {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "service"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.service.isEmpty { + try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_ListServiceResponse, rhs: Reflection_ListServiceResponse) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ServiceResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_ServiceResponse, rhs: Reflection_ServiceResponse) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ErrorResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "error_code"), + 2: .standard(proto: "error_message"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.errorCode != 0 { + try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) + } + if !self.errorMessage.isEmpty { + try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Reflection_ErrorResponse, rhs: Reflection_ErrorResponse) -> Bool { + if lhs.errorCode != rhs.errorCode {return false} + if lhs.errorMessage != rhs.errorMessage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/GRPCReflectionService/Model/reflection.proto b/Sources/GRPCReflectionService/Model/reflection.proto new file mode 100644 index 000000000..839a0e3af --- /dev/null +++ b/Sources/GRPCReflectionService/Model/reflection.proto @@ -0,0 +1,141 @@ +// Copyright 2016 The gRPC Authors +// +// 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. + +// Service exported by server reflection. A more complete description of how +// server reflection works can be found at +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +// +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + +syntax = "proto3"; + +package reflection; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server sets one of the following fields according to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. + // As the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requests. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services requests. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} diff --git a/Sources/GRPCReflectionService/Server/ReflectionProvider.swift b/Sources/GRPCReflectionService/Server/ReflectionProvider.swift new file mode 100644 index 000000000..7b49df005 --- /dev/null +++ b/Sources/GRPCReflectionService/Server/ReflectionProvider.swift @@ -0,0 +1,29 @@ +/* + * Copyright 2023, 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 Foundation +import GRPC + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +final class ReflectionService: Reflection_ServerReflectionAsyncProvider { + func serverReflectionInfo( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + throw GRPCStatus(code: .unimplemented) + } +} From 9cdcd2d9de68b8a5d2b75698412f0311728287d4 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 26 Sep 2023 14:19:58 +0100 Subject: [PATCH 119/580] Switch to swift-format --- .github/workflows/ci.yaml | 4 -- .gitignore | 1 + .swift-format | 58 +++++++++++++++++++++++++++ .swiftformat | 60 ---------------------------- scripts/format.sh | 83 ++++++++++++++++++++++++++++++++------- scripts/sanity.sh | 3 +- 6 files changed, 129 insertions(+), 80 deletions(-) create mode 100644 .swift-format delete mode 100644 .swiftformat diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e1d92f9be..c8562f805 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,10 +14,6 @@ jobs: - uses: actions/checkout@v3 - name: "Formatting and License Headers check" run: | - SWIFTFORMAT_VERSION=0.52.0 - git clone --depth 1 --branch "$SWIFTFORMAT_VERSION" "https://github.com/nicklockwood/SwiftFormat" "$HOME/SwiftFormat" - swift build -c release --package-path "$HOME/SwiftFormat" --product swiftformat - export PATH=$PATH:"$(swift build -c release --package-path "$HOME/SwiftFormat" --show-bin-path)" ./scripts/sanity.sh unit-tests: strategy: diff --git a/.gitignore b/.gitignore index ae6e5af6e..b0c63f998 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,6 @@ Examples/EchoWeb/node_modules Examples/EchoWeb/package-lock.json dev/codegen-tests/**/generated/* /scripts/.swiftformat-source/ +/scripts/.swift-format-source/ Package.resolved *.out.* diff --git a/.swift-format b/.swift-format new file mode 100644 index 000000000..139f9650c --- /dev/null +++ b/.swift-format @@ -0,0 +1,58 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 100, + "maximumBlankLines" : 1, + "prioritizeKeepingFunctionOutputTogether" : true, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLowerCamelCase" : false, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : false, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : false, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : false, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : false, + "UseSynthesizedInitializer" : false, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : true, + "tabWidth" : 2, + "version" : 1 +} diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index f18a823ae..000000000 --- a/.swiftformat +++ /dev/null @@ -1,60 +0,0 @@ -# Language version. ---swiftversion 5.6 - -# Ignore generated files. ---exclude **/LinuxMain.swift,**/XCTestManifests.swift,**/*.grpc.swift,**/*.pb.swift ---exclude Sources/GRPCSampleData/GRPCSwiftCertificate.swift - ---indent 2 ---maxwidth 100 - -# Require explicit self ---self insert - -# Only remove unused args in closures. ---stripunusedargs closure-only - -# Wrap before the first argument (if wrapping is necessary). ---wraparguments before-first - -# Don't indent #if blocks ---ifdef no-indent - -# Don't turn Optional into Foo? ---shortoptionals except-properties - -# This rule doesn't always work as we'd expect: specifically when we return a -# succeeded future whose type is a closure then that closure is incorrectly -# treated as a trailing closure. This is relevant because the service provider -# API for client streaming RPCs has this exact shape. ---disable trailingClosures - -# Don't wrap the opening brace of multiline statements. ---disable wrapMultilineStatementBraces - -# We used to support 5.0 and return is redundant in more places in 5.1: enabling -# this rule creates a large (and unnecessary) diff. ---disable redundantReturn - -# Don't prefer using key paths for trivial closures. ---disable preferKeyPath - -# Put ACLs on declarations within an extension rather than the extension itself. ---extensionacl on-declarations - -# Don't remove internal ACLs ---disable redundantInternal - -# Don't remove redundant parenstheses, because no all of them are redundant. ---disable redundantParens - -# Don't remove static Self ---disable redundantStaticSelf - -# Hoisting try and await causes a bunch of issues (and churn) in 0.52.0. Disable -# them for the time being. ---disable hoistTry ---disable hoistAwait - -# Disabled as enabling causes a lot of churn. ---disable wrapSingleLineComments diff --git a/scripts/format.sh b/scripts/format.sh index f73f366b6..748a023aa 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -16,43 +16,98 @@ set -eu +function log() { printf -- "** %s\n" "$*" >&2; } +function error() { printf -- "** ERROR: %s\n" "$*" >&2; } +function fatal() { error "$*"; exit 1; } + +function usage() { + echo >&2 "Usage:" + echo >&2 " $0 -[f|l]" + echo >&2 "" + echo >&2 "Options:" + echo >&2 " -f Format source code in place" + echo >&2 " -l Lint check without formatting the source code" +} + +lint=false +while getopts ":lh" opt; do + case "$opt" in + l) + lint=true + ;; + h) + usage + exit 1 + ;; + \?) + usage + exit 1 + ;; + esac +done + +THIS_SCRIPT=$0 HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" REPO="$HERE/.." -SWIFTFORMAT_DIR="$HERE/.swiftformat-source" - -# Important: if this is changed then make sure to update the version -# in the .github/workflows/ci.yaml as well! -SWIFTFORMAT_VERSION=0.52.0 +SWIFTFORMAT_DIR="$HERE/.swift-format-source" +SWIFTFORMAT_VERSION=509.0.0 # Clone SwiftFormat if we don't already have it. if [ ! -d "$SWIFTFORMAT_DIR" ]; then - echo "- Cloning SwiftFormat @ $SWIFTFORMAT_VERSION" + echo "- Cloning swift-format @ $SWIFTFORMAT_VERSION" git clone \ --depth 1 \ --branch "$SWIFTFORMAT_VERSION" \ - https://github.com/nicklockwood/SwiftFormat.git \ + https://github.com/apple/swift-format.git \ "$SWIFTFORMAT_DIR" fi cd "$SWIFTFORMAT_DIR" # Figure out the path for the binary. -SWIFTFORMAT_BIN="$(swift build --show-bin-path -c release)/swiftformat-$SWIFTFORMAT_VERSION" +SWIFTFORMAT_BIN="$(swift build --show-bin-path -c release)/swift-format-$SWIFTFORMAT_VERSION" # Build it if we don't already have it. if [ ! -f "$SWIFTFORMAT_BIN" ]; then # We're not on the right tag, fetch and checkout the right one. - echo "- Fetching SwiftFormat @ $SWIFTFORMAT_VERSION" + echo "- Fetching swift-format @ $SWIFTFORMAT_VERSION" git fetch --depth 1 origin "refs/tags/$SWIFTFORMAT_VERSION:refs/tags/$SWIFTFORMAT_VERSION" git checkout "$SWIFTFORMAT_VERSION" # Now build and name the bin appropriately. - echo "- Building SwiftFormat @ $SWIFTFORMAT_VERSION" - swift build -c release --product swiftformat - mv "$(swift build --show-bin-path -c release)/swiftformat" "$SWIFTFORMAT_BIN" + echo "- Building swift-format @ $SWIFTFORMAT_VERSION" + swift build -c release --product swift-format + mv "$(swift build --show-bin-path -c release)/swift-format" "$SWIFTFORMAT_BIN" echo "- OK" fi -# Now run it. -$SWIFTFORMAT_BIN "$REPO" +if "$lint"; then + "${SWIFTFORMAT_BIN}" lint \ + --parallel --recursive --strict \ + "${REPO}/Sources" "${REPO}/Tests" \ + && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? + + if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then + fatal "Running swift-format produced errors. + + To fix, run the following command: + + % $THIS_SCRIPT -f + " + exit "${SWIFT_FORMAT_RC}" + fi + + log "Ran swift-format lint with no errors." +else + "${SWIFTFORMAT_BIN}" format \ + --parallel --recursive --in-place \ + "${REPO}/Sources" "${REPO}/Tests" \ + && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? + + if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then + fatal "Running swift-format produced errors." "${SWIFT_FORMAT_RC}" + fi + + log "Ran swift-format with no errors." +fi diff --git a/scripts/sanity.sh b/scripts/sanity.sh index fbc6df07e..a317b3987 100755 --- a/scripts/sanity.sh +++ b/scripts/sanity.sh @@ -42,8 +42,7 @@ function check_license_headers() { } function check_formatting() { - hash swiftformat 2> /dev/null || { printf "\033[0;31mERROR\033[0m swiftformat must be installed (see: https://github.com/nicklockwood/SwiftFormat)\n"; exit 1; } - run_logged "Checking formatting (swiftformat $(swiftformat --version))" "swiftformat --lint --verbose $HERE/.." + run_logged "Checking formatting" "$HERE/format.sh lint" } errors=0 From b76f4b4d69c9335c55ab3a9528db73c96627db17 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 27 Sep 2023 16:50:24 +0100 Subject: [PATCH 120/580] Re-format code --- .../HPACKHeaders+Prettify.swift | 7 +- .../Echo/Implementation/Interceptors.swift | 3 +- Sources/Examples/Echo/Runtime/Echo.swift | 5 +- .../PacketCapture/PacketCapture.swift | 8 +- .../RouteGuide/Client/RouteGuideClient.swift | 8 +- .../Server/RouteGuideProvider.swift | 3 +- .../RouteGuide/Server/RouteGuideServer.swift | 9 +- .../Call+AsyncRequestStreamWriter.swift | 6 +- .../GRPCAsyncBidirectionalStreamingCall.swift | 13 +- .../GRPCAsyncServerHandler.swift | 26 +- .../GRPCAsyncServerStreamingCall.swift | 13 +- .../GRPCClient+AsyncAwaitSupport.swift | 4 +- .../AsyncAwaitSupport/NIOAsyncWrappers.swift | 19 +- .../BidirectionalStreamingServerHandler.swift | 9 +- .../ClientStreamingServerHandler.swift | 11 +- .../ServerStreamingServerHandler.swift | 11 +- .../CallHandlers/UnaryServerHandler.swift | 6 +- Sources/GRPC/CallOptions.swift | 7 +- Sources/GRPC/ClientCalls/Call.swift | 3 +- Sources/GRPC/ClientCalls/ClientCall.swift | 4 +- .../ClientCalls/ClientStreamingCall.swift | 3 +- Sources/GRPC/ClientConnection.swift | 43 +- ...ClientConnectionConfiguration+NIOSSL.swift | 2 +- Sources/GRPC/Compression/Zlib.swift | 27 +- .../GRPC/ConnectionManager+Delegates.swift | 8 +- Sources/GRPC/ConnectionManager.swift | 119 ++-- .../ConnectionManagerChannelProvider.swift | 14 +- .../GRPC/ConnectionPool/ConnectionPool.swift | 79 ++- Sources/GRPC/ConnectionPool/PoolManager.swift | 17 +- .../PoolManagerStateMachine.swift | 16 +- .../GRPC/ConnectionPool/PooledChannel.swift | 5 +- Sources/GRPC/ConnectivityState.swift | 11 +- Sources/GRPC/DebugOnly.swift | 7 +- Sources/GRPC/FakeChannel.swift | 14 +- .../GRPCChannel/ClientConnection+NIOSSL.swift | 8 +- .../GRPCChannel/ClientConnection+NWTLS.swift | 4 +- Sources/GRPC/GRPCClientChannelHandler.swift | 78 ++- Sources/GRPC/GRPCClientStateMachine.swift | 63 ++- Sources/GRPC/GRPCContentType.swift | 6 +- Sources/GRPC/GRPCIdleHandler.swift | 25 +- .../GRPC/GRPCIdleHandlerStateMachine.swift | 40 +- Sources/GRPC/GRPCKeepaliveHandlers.swift | 7 +- .../GRPC/GRPCServerPipelineConfigurator.swift | 31 +- .../GRPC/GRPCStatusMessageMarshaller.swift | 8 +- Sources/GRPC/GRPCTLSConfiguration.swift | 17 +- Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift | 27 +- Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift | 16 +- Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift | 56 +- .../GRPC/Interceptor/ClientTransport.swift | 31 +- .../Interceptor/ClientTransportFactory.swift | 22 +- .../ServerInterceptorPipeline.swift | 11 +- .../GRPC/LengthPrefixedMessageReader.swift | 2 +- Sources/GRPC/PlatformSupport.swift | 7 +- Sources/GRPC/ReadWriteStates.swift | 6 +- Sources/GRPC/Serialization.swift | 4 +- Sources/GRPC/Server+NIOSSL.swift | 2 +- Sources/GRPC/Server.swift | 33 +- Sources/GRPC/ServerBuilder+NIOSSL.swift | 8 +- .../StreamingResponseCallContext.swift | 6 +- Sources/GRPC/TLSVersion.swift | 13 +- Sources/GRPC/UserInfo.swift | 7 +- Sources/GRPC/WebCORSHandler.swift | 5 +- Sources/GRPC/_EmbeddedThroughput.swift | 1 + Sources/GRPC/_FakeResponseStream.swift | 22 +- .../main.swift | 2 +- .../GRPCTestingConvenienceMethods.swift | 9 +- .../InteroperabilityTestCases.swift | 57 +- ...InteroperabilityTestClientConnection.swift | 3 +- .../InteroperabilityTestCredentials.swift | 102 ++-- .../InteroperabilityTestServer.swift | 6 +- .../TestServiceProvider.swift | 15 +- .../Benchmarks/EmbeddedClientThroughput.swift | 13 +- .../Benchmarks/EmbeddedServer.swift | 16 +- Sources/GRPCPerformanceTests/main.swift | 12 +- .../GRPCSampleData/GRPCSwiftCertificate.swift | 484 ++++++++-------- .../Generator-Client+AsyncAwait.swift | 13 +- .../Generator-Client.swift | 21 +- .../Generator-Server+AsyncAwait.swift | 2 +- Sources/protoc-gen-grpc-swift/Generator.swift | 24 +- Sources/protoc-gen-grpc-swift/main.swift | 7 +- Tests/GRPCTests/ALPNConfigurationTests.swift | 2 +- .../GRPCTests/Array+BoundsCheckingTests.swift | 3 +- .../AsyncIntegrationTests.swift | 41 +- .../ServerHandlerStateMachineTests.swift | 4 +- ...erceptorStateMachineStreamStateTests.swift | 4 +- .../ServerInterceptorStateMachineTests.swift | 12 +- .../AsyncAwaitSupport/XCTest+AsyncAwait.swift | 2 +- Tests/GRPCTests/BasicEchoTestCase.swift | 3 +- Tests/GRPCTests/CallPathTests.swift | 4 +- Tests/GRPCTests/CapturingLogHandler.swift | 6 +- Tests/GRPCTests/ClientCallTests.swift | 5 +- .../ClientConnectionBackoffTests.swift | 83 +-- .../ClientInterceptorPipelineTests.swift | 6 +- Tests/GRPCTests/ClientQuiescingTests.swift | 4 +- Tests/GRPCTests/ClientTLSFailureTests.swift | 41 +- Tests/GRPCTests/ClientTLSTests.swift | 10 +- Tests/GRPCTests/ClientTimeoutTests.swift | 3 +- Tests/GRPCTests/ClientTransportTests.swift | 4 +- ...cingLengthPrefixedMessageWriterTests.swift | 4 +- Tests/GRPCTests/CompressionTests.swift | 156 ++++-- Tests/GRPCTests/ConnectionManagerTests.swift | 79 +-- .../ConnectionPoolDelegates.swift | 14 +- .../ConnectionPool/ConnectionPoolTests.swift | 4 +- .../ConnectionPool/GRPCChannelPoolTests.swift | 9 +- .../PoolManagerStateMachineTests.swift | 4 +- .../ConnectivityStateMonitorTests.swift | 17 +- .../DelegatingErrorHandlerTests.swift | 2 +- Tests/GRPCTests/EchoTestClientTests.swift | 36 +- Tests/GRPCTests/FakeResponseStreamTests.swift | 3 +- Tests/GRPCTests/FunctionalTests.swift | 10 +- .../GRPCTests/GRPCAsyncClientCallTests.swift | 22 +- .../GRPCAsyncServerHandlerTests.swift | 28 +- .../GRPCClientChannelHandlerTests.swift | 12 +- .../GRPCClientStateMachineTests.swift | 521 +++++++++++------- Tests/GRPCTests/GRPCCustomPayloadTests.swift | 5 +- .../GRPCIdleHandlerStateMachineTests.swift | 4 +- Tests/GRPCTests/GRPCIdleTests.swift | 16 +- .../GRPCTests/GRPCInteroperabilityTests.swift | 4 +- Tests/GRPCTests/GRPCKeepaliveTests.swift | 3 +- Tests/GRPCTests/GRPCLoggerTests.swift | 4 +- .../GRPCTests/GRPCNetworkFrameworkTests.swift | 16 +- Tests/GRPCTests/GRPCPingHandlerTests.swift | 16 +- .../GRPCServerPipelineConfiguratorTests.swift | 16 +- Tests/GRPCTests/GRPCStatusCodeTests.swift | 6 +- Tests/GRPCTests/GRPCStatusTests.swift | 28 +- Tests/GRPCTests/GRPCTestCase.swift | 2 +- Tests/GRPCTests/GRPCTimeoutTests.swift | 3 +- .../GRPCWebToHTTP2ServerCodecTests.swift | 7 +- .../GRPCWebToHTTP2StateMachineTests.swift | 19 +- .../HTTP2MaxConcurrentStreamsTests.swift | 8 +- .../HTTP2ToRawGRPCStateMachineTests.swift | 40 +- Tests/GRPCTests/HTTPVersionParserTests.swift | 4 +- .../GRPCTests/HeaderNormalizationTests.swift | 15 +- .../InterceptedRPCCancellationTests.swift | 6 +- Tests/GRPCTests/InterceptorsTests.swift | 19 +- .../GRPCTests/LazyEventLoopPromiseTests.swift | 4 +- .../LengthPrefixedMessageReaderTests.swift | 76 +-- .../MessageEncodingHeaderValidatorTests.swift | 38 +- Tests/GRPCTests/MutualTLSTests.swift | 6 +- Tests/GRPCTests/OneOrManyQueueTests.swift | 4 +- Tests/GRPCTests/PlatformSupportTests.swift | 3 +- Tests/GRPCTests/RequestIDProviderTests.swift | 4 +- .../SampleCertificate+Assertions.swift | 2 +- .../GRPCTests/ServerErrorDelegateTests.swift | 8 +- .../ServerFuzzingRegressionTests.swift | 11 +- .../ServerInterceptorPipelineTests.swift | 7 +- Tests/GRPCTests/ServerInterceptorTests.swift | 10 +- Tests/GRPCTests/ServerTLSErrorTests.swift | 16 +- Tests/GRPCTests/ServerThrowingTests.swift | 46 +- Tests/GRPCTests/ServerWebTests.swift | 19 +- Tests/GRPCTests/StopwatchTests.swift | 3 +- Tests/GRPCTests/TestClientExample.swift | 23 +- Tests/GRPCTests/TimeLimitTests.swift | 4 +- Tests/GRPCTests/UnaryServerHandlerTests.swift | 4 +- Tests/GRPCTests/WebCORSHandlerTests.swift | 4 +- .../GRPCTests/WithConnectedSocketTests.swift | 3 +- Tests/GRPCTests/XCTestHelpers.swift | 14 +- Tests/GRPCTests/ZeroLengthWriteTests.swift | 2 +- Tests/GRPCTests/ZlibTests.swift | 4 +- 159 files changed, 2119 insertions(+), 1487 deletions(-) diff --git a/Sources/Examples/Echo/Implementation/HPACKHeaders+Prettify.swift b/Sources/Examples/Echo/Implementation/HPACKHeaders+Prettify.swift index bf4e9c63b..9b87d8c1c 100644 --- a/Sources/Examples/Echo/Implementation/HPACKHeaders+Prettify.swift +++ b/Sources/Examples/Echo/Implementation/HPACKHeaders+Prettify.swift @@ -16,7 +16,8 @@ import NIOHPACK func prettify(_ headers: HPACKHeaders) -> String { - return "[" + headers.map { name, value, _ in - "'\(name)': '\(value)'" - }.joined(separator: ", ") + "]" + return "[" + + headers.map { name, value, _ in + "'\(name)': '\(value)'" + }.joined(separator: ", ") + "]" } diff --git a/Sources/Examples/Echo/Implementation/Interceptors.swift b/Sources/Examples/Echo/Implementation/Interceptors.swift index 28abb0a37..9a248b7f9 100644 --- a/Sources/Examples/Echo/Implementation/Interceptors.swift +++ b/Sources/Examples/Echo/Implementation/Interceptors.swift @@ -107,7 +107,8 @@ public final class ExampleClientInterceptorFactory: Echo_EchoClientInterceptorFa // Returns an array of interceptors to use for the 'Collect' RPC. public func makeCollectInterceptors() - -> [ClientInterceptor] { + -> [ClientInterceptor] + { return [LoggingEchoClientInterceptor()] } diff --git a/Sources/Examples/Echo/Runtime/Echo.swift b/Sources/Examples/Echo/Runtime/Echo.swift index 65abd883d..c9bdee818 100644 --- a/Sources/Examples/Echo/Runtime/Echo.swift +++ b/Sources/Examples/Echo/Runtime/Echo.swift @@ -20,6 +20,7 @@ import GRPC import GRPCSampleData import NIOCore import NIOPosix + #if canImport(NIOSSL) import NIOSSL #endif @@ -136,7 +137,7 @@ func startEchoServer(group: EventLoopGroup, port: Int, useTLS: Bool) async throw print("starting secure server") #else fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } else { print("starting insecure server") builder = Server.insecure(group: group) @@ -180,7 +181,7 @@ func makeClient( .withTLS(trustRoots: .certificates([caCert.certificate])) #else fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } else { builder = ClientConnection.insecure(group: group) } diff --git a/Sources/Examples/PacketCapture/PacketCapture.swift b/Sources/Examples/PacketCapture/PacketCapture.swift index 8d1544b47..468add246 100644 --- a/Sources/Examples/PacketCapture/PacketCapture.swift +++ b/Sources/Examples/PacketCapture/PacketCapture.swift @@ -56,9 +56,11 @@ struct PCAP: AsyncParsableCommand { // used TLS we would likely want to place the handler in a different position in the // pipeline so that the captured packets in the trace would not be encrypted. let writePCAPHandler = NIOWritePCAPHandler(mode: .client, fileSink: fileSink.write(buffer:)) - return channel.eventLoop.makeCompletedFuture(Result { - try channel.pipeline.syncOperations.addHandler(writePCAPHandler, position: .first) - }) + return channel.eventLoop.makeCompletedFuture( + Result { + try channel.pipeline.syncOperations.addHandler(writePCAPHandler, position: .first) + } + ) } } diff --git a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift index 47710deef..4f9c63d97 100644 --- a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift +++ b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift @@ -23,8 +23,8 @@ import RouteGuideModel /// Loads the features from `route_guide_db.json`, assumed to be in the directory above this file. func loadFeatures() throws -> [Routeguide_Feature] { let url = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // main.swift - .deletingLastPathComponent() // Client/ + .deletingLastPathComponent() // main.swift + .deletingLastPathComponent() // Client/ .appendingPathComponent("route_guide_db.json") let data = try Data(contentsOf: url) @@ -146,8 +146,8 @@ extension RouteGuideExample { let summary = try await recordRoute.response print( - "Finished trip with \(summary.pointCount) points. Passed \(summary.featureCount) features. " + - "Travelled \(summary.distance) meters. It took \(summary.elapsedTime) seconds." + "Finished trip with \(summary.pointCount) points. Passed \(summary.featureCount) features. " + + "Travelled \(summary.distance) meters. It took \(summary.elapsedTime) seconds." ) } catch { print("RecordRoute Failed: \(error)") diff --git a/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift b/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift index 63fa450b5..8b4402515 100644 --- a/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift +++ b/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift @@ -148,7 +148,8 @@ extension Routeguide_Point { let deltaLat = lat2 - lat1 let deltaLon = lon2 - lon1 - let a = sin(deltaLat / 2) * sin(deltaLat / 2) + let a = + sin(deltaLat / 2) * sin(deltaLat / 2) + cos(lat1) * cos(lat2) * sin(deltaLon / 2) * sin(deltaLon / 2) let c = 2 * atan2(sqrt(a), sqrt(1 - a)) diff --git a/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift b/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift index 02af3b2b6..1551b4fce 100644 --- a/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift +++ b/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift @@ -14,18 +14,19 @@ * limitations under the License. */ import ArgumentParser -import struct Foundation.Data -import struct Foundation.URL import GRPC import NIOCore import NIOPosix import RouteGuideModel +import struct Foundation.Data +import struct Foundation.URL + /// Loads the features from `route_guide_db.json`, assumed to be in the directory above this file. func loadFeatures() throws -> [Routeguide_Feature] { let url = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // main.swift - .deletingLastPathComponent() // Server/ + .deletingLastPathComponent() // main.swift + .deletingLastPathComponent() // Server/ .appendingPathComponent("route_guide_db.json") let data = try Data(contentsOf: url) diff --git a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift index 720ced509..5381bef32 100644 --- a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift +++ b/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift @@ -22,11 +22,13 @@ extension Call where Request: Sendable, Response: Sendable { GRPCAsyncWriterSinkDelegate<(Request, Compression)> > internal func makeRequestStreamWriter() - -> (GRPCAsyncRequestStreamWriter, AsyncWriter.Sink) { + -> (GRPCAsyncRequestStreamWriter, AsyncWriter.Sink) + { let delegate = GRPCAsyncWriterSinkDelegate<(Request, Compression)>( didYield: { requests in for (request, compression) in requests { - let compress = compression + let compress = + compression .isEnabled(callDefault: self.options.messageEncoding.enabledForRequests) // TODO: be smarter about inserting flushes. diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift index 271da849e..339c99731 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift @@ -21,12 +21,13 @@ import NIOHPACK public struct GRPCAsyncBidirectionalStreamingCall: Sendable { private let call: Call private let responseParts: StreamingResponseParts - private let responseSource: NIOThrowingAsyncSequenceProducer< - Response, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source + private let responseSource: + NIOThrowingAsyncSequenceProducer< + Response, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source private let requestSink: AsyncSink<(Request, Compression)> /// A request stream writer for sending messages to the server. diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift index 6da95584b..bea30ae6a 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift @@ -232,19 +232,23 @@ internal final class AsyncServerHandler< internal private(set) var handlerStateMachine: ServerHandlerStateMachine /// A bag of components used by the user handler. @usableFromInline - internal private(set) var handlerComponents: Optional - >> + internal private(set) var handlerComponents: + Optional< + ServerHandlerComponents< + Request, + Response, + GRPCAsyncWriterSinkDelegate<(Response, Compression)> + > + > /// The user provided function to execute. @usableFromInline - internal let userHandler: @Sendable ( - GRPCAsyncRequestStream, - GRPCAsyncResponseStreamWriter, - GRPCAsyncServerCallContext - ) async throws -> Void + internal let userHandler: + @Sendable ( + GRPCAsyncRequestStream, + GRPCAsyncResponseStreamWriter, + GRPCAsyncServerCallContext + ) async throws -> Void @usableFromInline internal typealias AsyncSequenceProducer = NIOThrowingAsyncSequenceProducer< @@ -415,7 +419,7 @@ internal final class AsyncServerHandler< internal func receiveInterceptedMetadata(_ headers: HPACKHeaders) { switch self.interceptorStateMachine.interceptedRequestMetadata() { case .forward: - () // continue + () // continue case .cancel: return self.cancel(error: nil) case .drop: diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift index 26b7506e7..e2879783e 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift @@ -22,12 +22,13 @@ import NIOHPACK public struct GRPCAsyncServerStreamingCall { private let call: Call private let responseParts: StreamingResponseParts - private let responseSource: NIOThrowingAsyncSequenceProducer< - Response, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source + private let responseSource: + NIOThrowingAsyncSequenceProducer< + Response, + Error, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source /// The stream of responses from the server. public let responseStream: GRPCAsyncResponseStream diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift index 91ba44eae..b433d6323 100644 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift +++ b/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift @@ -329,7 +329,7 @@ extension GRPCClient { requestType: Request.Type = Request.self, responseType: Response.Type = Response.self ) -> GRPCAsyncResponseStream - where RequestStream.Element == Request { + where RequestStream.Element == Request { let call = self.channel.makeAsyncBidirectionalStreamingCall( path: path, callOptions: callOptions ?? self.defaultCallOptions, @@ -350,7 +350,7 @@ extension GRPCClient { requestType: Request.Type = Request.self, responseType: Response.Type = Response.self ) -> GRPCAsyncResponseStream - where RequestStream.Element == Request { + where RequestStream.Element == Request { let call = self.channel.makeAsyncBidirectionalStreamingCall( path: path, callOptions: callOptions ?? self.defaultCallOptions, diff --git a/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift b/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift index 9269d9b2f..651a16151 100644 --- a/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift +++ b/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift @@ -18,16 +18,19 @@ import NIOCore /// Unchecked-sendable wrapper for ``NIOAsyncWriter/Sink``, to avoid getting sendability warnings. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) internal struct AsyncSink: @unchecked Sendable { - private let sink: NIOAsyncWriter< - Element, - GRPCAsyncWriterSinkDelegate - >.Sink + private let sink: + NIOAsyncWriter< + Element, + GRPCAsyncWriterSinkDelegate + >.Sink @inlinable - init(wrapping sink: NIOAsyncWriter< - Element, - GRPCAsyncWriterSinkDelegate - >.Sink) { + init( + wrapping sink: NIOAsyncWriter< + Element, + GRPCAsyncWriterSinkDelegate + >.Sink + ) { self.sink = sink } diff --git a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift index 229cb1b5e..6a9736cdc 100644 --- a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift @@ -49,8 +49,9 @@ public final class BidirectionalStreamingServerHandler< /// The user provided function to execute. @usableFromInline - internal let observerFactory: (_StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> + internal let observerFactory: + (_StreamingResponseCallContext) + -> EventLoopFuture<(StreamEvent) -> Void> /// The state of the handler. @usableFromInline @@ -135,7 +136,7 @@ public final class BidirectionalStreamingServerHandler< self.state = .completed case let .creatingObserver(context), - let .observing(_, context): + let .observing(_, context): context.statusPromise.fail(GRPCStatus(code: .unavailable, message: nil)) self.context.eventLoop.execute { self.interceptors = nil @@ -320,7 +321,7 @@ public final class BidirectionalStreamingServerHandler< self.interceptors.send(.end(status, trailers), promise: nil) case let .creatingObserver(context), - let .observing(_, context): + let .observing(_, context): // We don't have a promise to fail. Just send back end. self.state = .completed diff --git a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift index 2e1167998..009b690f3 100644 --- a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift @@ -49,8 +49,9 @@ public final class ClientStreamingServerHandler< /// The user provided function to execute. @usableFromInline - internal let handlerFactory: (UnaryResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> + internal let handlerFactory: + (UnaryResponseCallContext) + -> EventLoopFuture<(StreamEvent) -> Void> /// The state of the handler. @usableFromInline @@ -136,7 +137,7 @@ public final class ClientStreamingServerHandler< self.state = .completed case let .creatingObserver(context), - let .observing(_, context): + let .observing(_, context): context.responsePromise.fail(GRPCStatus(code: .unavailable, message: nil)) self.context.eventLoop.execute { self.interceptors = nil @@ -266,7 +267,7 @@ public final class ClientStreamingServerHandler< preconditionFailure() case let .creatingObserver(context), - let .observing(_, context): + let .observing(_, context): switch result { case let .success(response): // Complete when we send end. @@ -303,7 +304,7 @@ public final class ClientStreamingServerHandler< self.interceptors.send(.end(status, trailers), promise: nil) case let .creatingObserver(context), - let .observing(_, context): + let .observing(_, context): // We don't have a promise to fail. Just send back end. self.state = .completed diff --git a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift index 7028020c6..3c930b8d4 100644 --- a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift +++ b/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift @@ -45,8 +45,9 @@ public final class ServerStreamingServerHandler< /// The user provided function to execute. @usableFromInline - internal let userFunction: (Request, StreamingResponseCallContext) - -> EventLoopFuture + internal let userFunction: + (Request, StreamingResponseCallContext) + -> EventLoopFuture /// The state of the handler. @usableFromInline @@ -132,7 +133,7 @@ public final class ServerStreamingServerHandler< self.state = .completed case let .createdContext(context), - let .invokedFunction(context): + let .invokedFunction(context): context.statusPromise.fail(GRPCStatus(code: .unavailable, message: nil)) self.context.eventLoop.execute { self.interceptors = nil @@ -259,7 +260,7 @@ public final class ServerStreamingServerHandler< preconditionFailure() case let .createdContext(context), - let .invokedFunction(context): + let .invokedFunction(context): switch result { case let .success(status): @@ -320,7 +321,7 @@ public final class ServerStreamingServerHandler< self.interceptors.send(.end(status, trailers), promise: nil) case let .createdContext(context), - let .invokedFunction(context): + let .invokedFunction(context): // We don't have a promise to fail. Just send back end. self.state = .completed diff --git a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift b/Sources/GRPC/CallHandlers/UnaryServerHandler.swift index c0a84abb6..5624b601d 100644 --- a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift +++ b/Sources/GRPC/CallHandlers/UnaryServerHandler.swift @@ -130,7 +130,7 @@ public final class UnaryServerHandler< self.state = .completed case let .createdContext(context), - let .invokedFunction(context): + let .invokedFunction(context): context.responsePromise.fail(GRPCStatus(code: .unavailable, message: nil)) self.context.eventLoop.execute { self.interceptors = nil @@ -236,7 +236,7 @@ public final class UnaryServerHandler< // 'created' is allowed here: we may have to (and tear down) after receiving headers // but before receiving a message. case let .createdContext(context), - let .invokedFunction(context): + let .invokedFunction(context): switch result { case let .success(response): @@ -304,7 +304,7 @@ public final class UnaryServerHandler< self.interceptors.send(.end(status, trailers), promise: nil) case let .createdContext(context), - let .invokedFunction(context): + let .invokedFunction(context): // We don't have a promise to fail. Just send back end. self.state = .completed diff --git a/Sources/GRPC/CallOptions.swift b/Sources/GRPC/CallOptions.swift index 00bffb3cd..990b52a53 100644 --- a/Sources/GRPC/CallOptions.swift +++ b/Sources/GRPC/CallOptions.swift @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.UUID - -import NIOCore - import Logging +import NIOCore import NIOHPACK import NIOHTTP1 import NIOHTTP2 +import struct Foundation.UUID + /// Options to use for GRPC calls. public struct CallOptions: Sendable { /// Additional metadata to send to the service. diff --git a/Sources/GRPC/ClientCalls/Call.swift b/Sources/GRPC/ClientCalls/Call.swift index 759798836..46777ed20 100644 --- a/Sources/GRPC/ClientCalls/Call.swift +++ b/Sources/GRPC/ClientCalls/Call.swift @@ -17,6 +17,7 @@ import Logging import NIOCore import NIOHPACK import NIOHTTP2 + import protocol SwiftProtobuf.Message /// An object representing a single RPC from the perspective of a client. It allows the caller to @@ -325,7 +326,7 @@ extension Call { switch (self.channelPromise, self._state) { case let (.some(promise), .idle), - let (.some(promise), .invoked): + let (.some(promise), .invoked): // We already have a promise, just use that. return promise.futureResult diff --git a/Sources/GRPC/ClientCalls/ClientCall.swift b/Sources/GRPC/ClientCalls/ClientCall.swift index d2d386893..ef131159c 100644 --- a/Sources/GRPC/ClientCalls/ClientCall.swift +++ b/Sources/GRPC/ClientCalls/ClientCall.swift @@ -106,7 +106,7 @@ public protocol StreamingRequestClientCall: ClientCall { /// - compression: Whether compression should be used for this message. Ignored if compression /// was not enabled for the RPC. func sendMessages(_ messages: S, compression: Compression) -> EventLoopFuture - where S.Element == RequestPayload + where S.Element == RequestPayload /// Sends a sequence of messages to the service. /// @@ -150,7 +150,7 @@ extension StreamingRequestClientCall { _ messages: S, compression: Compression = .deferToCallDefault ) -> EventLoopFuture - where S.Element == RequestPayload { + where S.Element == RequestPayload { let promise = self.eventLoop.makePromise(of: Void.self) self.sendMessages(messages, compression: compression, promise: promise) return promise.futureResult diff --git a/Sources/GRPC/ClientCalls/ClientStreamingCall.swift b/Sources/GRPC/ClientCalls/ClientStreamingCall.swift index 7f8bcd581..abf9d0612 100644 --- a/Sources/GRPC/ClientCalls/ClientStreamingCall.swift +++ b/Sources/GRPC/ClientCalls/ClientStreamingCall.swift @@ -26,7 +26,8 @@ import NIOHTTP2 /// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore /// has reference semantics. public struct ClientStreamingCall: StreamingRequestClientCall, - UnaryResponseClientCall { + UnaryResponseClientCall +{ private let call: Call private let responseParts: UnaryResponseParts diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index 9b399d36f..d238b7def 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -13,24 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if os(Linux) -@preconcurrency import Foundation -#else -import Foundation -#endif import Logging import NIOCore import NIOHPACK import NIOHTTP2 import NIOPosix -#if canImport(NIOSSL) -import NIOSSL -#endif import NIOTLS import NIOTransportServices import SwiftProtobuf +#if os(Linux) +@preconcurrency import Foundation +#else +import Foundation +#endif + +#if canImport(NIOSSL) +import NIOSSL +#endif + /// Provides a single, managed connection to a server which is guaranteed to always use the same /// `EventLoop`. /// @@ -396,7 +398,7 @@ extension ClientConnection { self.tlsConfiguration = newValue.map { .init(transforming: $0) } } } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) /// TLS configuration for this connection. `nil` if TLS is not desired. public var tlsConfiguration: GRPCTLSConfiguration? @@ -523,7 +525,7 @@ extension ClientConnection { self.backgroundActivityLogger = backgroundActivityLogger self.debugChannelInitializer = debugChannelInitializer } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) private init(eventLoopGroup: EventLoopGroup, target: ConnectionTarget) { self.eventLoopGroup = eventLoopGroup @@ -598,7 +600,7 @@ extension ChannelPipeline.SynchronousOperations { try self.addHandler(TLSVerificationHandler(logger: logger)) } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) extension ChannelPipeline.SynchronousOperations { internal func configureHTTP2AndGRPCHandlersForGRPCClient( @@ -635,13 +637,15 @@ extension ChannelPipeline.SynchronousOperations { // The multiplexer is passed through the idle handler so it is only reported on // successful channel activation - with happy eyeballs multiple pipelines can // be constructed so it's not safe to report just yet. - try self.addHandler(GRPCIdleHandler( - connectionManager: connectionManager, - multiplexer: h2Multiplexer, - idleTimeout: connectionIdleTimeout, - keepalive: connectionKeepalive, - logger: logger - )) + try self.addHandler( + GRPCIdleHandler( + connectionManager: connectionManager, + multiplexer: h2Multiplexer, + idleTimeout: connectionIdleTimeout, + keepalive: connectionKeepalive, + logger: logger + ) + ) try self.addHandler(h2Multiplexer) try self.addHandler(DelegatingErrorHandler(logger: logger, delegate: errorDelegate)) @@ -675,8 +679,7 @@ extension String { var ipv6Addr = in6_addr() return self.withCString { ptr in - inet_pton(AF_INET, ptr, &ipv4Addr) == 1 || - inet_pton(AF_INET6, ptr, &ipv6Addr) == 1 + inet_pton(AF_INET, ptr, &ipv4Addr) == 1 || inet_pton(AF_INET6, ptr, &ipv6Addr) == 1 } } } diff --git a/Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift b/Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift index 6de8b4e83..d10550db1 100644 --- a/Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift +++ b/Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift @@ -130,4 +130,4 @@ extension ClientConnection.Configuration { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/Compression/Zlib.swift b/Sources/GRPC/Compression/Zlib.swift index d5d1dc8c9..4186f1f9e 100644 --- a/Sources/GRPC/Compression/Zlib.swift +++ b/Sources/GRPC/Compression/Zlib.swift @@ -14,9 +14,10 @@ * limitations under the License. */ import CGRPCZlib -import struct Foundation.Data import NIOCore +import struct Foundation.Data + /// Provides minimally configurable wrappers around zlib's compression and decompression /// functionality. /// @@ -67,7 +68,8 @@ enum Zlib { self.stream.availableInputBytes = 0 } - let writtenBytes = try output + let writtenBytes = + try output .writeWithUnsafeMutableBytes(minimumWritableBytes: Int(upperBound)) { outputPointer in try self.stream.deflate( outputBuffer: CGRPCZlib_castVoidToBytefPointer(outputPointer.baseAddress!), @@ -108,11 +110,11 @@ enum Zlib { private func initialize() { let rc = CGRPCZlib_deflateInit2( &self.stream.zstream, - Z_DEFAULT_COMPRESSION, // compression level - Z_DEFLATED, // compression method (this must be Z_DEFLATED) - self.format.windowBits, // window size, i.e. deflate/gzip - 8, // memory level (this is the default value in the docs) - Z_DEFAULT_STRATEGY // compression strategy + Z_DEFAULT_COMPRESSION, // compression level + Z_DEFLATED, // compression method (this must be Z_DEFLATED) + self.format.windowBits, // window size, i.e. deflate/gzip + 8, // memory level (this is the default value in the docs) + Z_DEFAULT_STRATEGY // compression strategy ) // Possible return codes: @@ -180,7 +182,7 @@ enum Zlib { self = .inflated case (.complete, .inflated), - (.outputBufferTooSmall, .inflated): + (.outputBufferTooSmall, .inflated): preconditionFailure("invalid outcome '\(result.outcome)'; inflation is already complete") } } @@ -280,8 +282,10 @@ enum Zlib { // account here. let writerIndex = output.writerIndex let minimumWritableBytes = inflationState.outputBufferSize - writerIndex - bytesWritten = try output - .writeWithUnsafeMutableBytes(minimumWritableBytes: minimumWritableBytes) { outputPointer in + bytesWritten = + try output + .writeWithUnsafeMutableBytes(minimumWritableBytes: minimumWritableBytes) { + outputPointer in let inflateResult = try self.stream.inflate( outputBuffer: CGRPCZlib_castVoidToBytefPointer(outputPointer.baseAddress!), outputBufferSize: outputPointer.count @@ -366,7 +370,8 @@ enum Zlib { return Int(self.zstream.avail_out) } set { - return self.zstream.avail_out = UInt32(newValue) + self.zstream.avail_out = UInt32(newValue) + return } } diff --git a/Sources/GRPC/ConnectionManager+Delegates.swift b/Sources/GRPC/ConnectionManager+Delegates.swift index 5b214f57e..5687dc52b 100644 --- a/Sources/GRPC/ConnectionManager+Delegates.swift +++ b/Sources/GRPC/ConnectionManager+Delegates.swift @@ -71,10 +71,10 @@ internal enum _ConnectivityState: Sendable { internal func isSameState(as other: _ConnectivityState) -> Bool { switch (self, other) { case (.idle, .idle), - (.connecting, .connecting), - (.ready, .ready), - (.transientFailure, .transientFailure), - (.shutdown, .shutdown): + (.connecting, .connecting), + (.ready, .ready), + (.transientFailure, .transientFailure), + (.shutdown, .shutdown): return true default: return false diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 35ee41101..a0081fe7a 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -77,20 +77,24 @@ internal final class ConnectionManager: @unchecked Sendable { self.backoffIterator = state.backoffIterator self.readyChannelMuxPromise = state.readyChannelMuxPromise self.scheduled = scheduled - self.reason = reason ?? GRPCStatus( - code: .unavailable, - message: "Unexpected connection drop" - ) + self.reason = + reason + ?? GRPCStatus( + code: .unavailable, + message: "Unexpected connection drop" + ) } init(from state: ConnectedState, scheduled: Scheduled) { self.backoffIterator = state.backoffIterator self.readyChannelMuxPromise = state.readyChannelMuxPromise self.scheduled = scheduled - self.reason = state.error ?? GRPCStatus( - code: .unavailable, - message: "Unexpected connection drop" - ) + self.reason = + state.error + ?? GRPCStatus( + code: .unavailable, + message: "Unexpected connection drop" + ) } init( @@ -101,10 +105,12 @@ internal final class ConnectionManager: @unchecked Sendable { self.backoffIterator = backoffIterator self.readyChannelMuxPromise = state.channel.eventLoop.makePromise() self.scheduled = scheduled - self.reason = state.error ?? GRPCStatus( - code: .unavailable, - message: "Unexpected connection drop" - ) + self.reason = + state.error + ?? GRPCStatus( + code: .unavailable, + message: "Unexpected connection drop" + ) } } @@ -410,9 +416,12 @@ internal final class ConnectionManager: @unchecked Sendable { multiplexer = self.eventLoop.makeFailedFuture(state.reason) } - self.logger.debug("vending multiplexer future", metadata: [ - "connectivity_state": "\(self.state.label)", - ]) + self.logger.debug( + "vending multiplexer future", + metadata: [ + "connectivity_state": "\(self.state.label)" + ] + ) return multiplexer } @@ -447,9 +456,12 @@ internal final class ConnectionManager: @unchecked Sendable { } }() - self.logger.debug("vending fast-failing multiplexer future", metadata: [ - "connectivity_state": "\(self.state.label)", - ]) + self.logger.debug( + "vending fast-failing multiplexer future", + metadata: [ + "connectivity_state": "\(self.state.label)" + ] + ) return muxFuture } @@ -485,10 +497,13 @@ internal final class ConnectionManager: @unchecked Sendable { } private func _shutdown(mode: ShutdownMode, promise: EventLoopPromise) { - self.logger.debug("shutting down connection", metadata: [ - "connectivity_state": "\(self.state.label)", - "shutdown.mode": "\(mode)", - ]) + self.logger.debug( + "shutting down connection", + metadata: [ + "connectivity_state": "\(self.state.label)", + "shutdown.mode": "\(mode)", + ] + ) switch self.state { // We don't have a channel and we don't want one, easy! @@ -634,9 +649,12 @@ internal final class ConnectionManager: @unchecked Sendable { // error is channel fatal, in which case we'll see channelInactive soon (acceptable), or it's not, // and future I/O will either fail fast or work. In either case, all we do is log this and move on. case .idle: - self.logger.warning("ignoring unexpected error in idle", metadata: [ - MetadataKey.error: "\(error)", - ]) + self.logger.warning( + "ignoring unexpected error in idle", + metadata: [ + MetadataKey.error: "\(error)" + ] + ) case .connecting: self.connectionFailed(withError: error) @@ -658,9 +676,12 @@ internal final class ConnectionManager: @unchecked Sendable { /// The connecting channel became `active`. Must be called on the `EventLoop`. internal func channelActive(channel: Channel, multiplexer: HTTP2StreamMultiplexer) { self.eventLoop.preconditionInEventLoop() - self.logger.debug("activating connection", metadata: [ - "connectivity_state": "\(self.state.label)", - ]) + self.logger.debug( + "activating connection", + metadata: [ + "connectivity_state": "\(self.state.label)" + ] + ) switch self.state { case let .connecting(connecting): @@ -687,9 +708,12 @@ internal final class ConnectionManager: @unchecked Sendable { /// Must be called on the `EventLoop`. internal func channelInactive() { self.eventLoop.preconditionInEventLoop() - self.logger.debug("deactivating connection", metadata: [ - "connectivity_state": "\(self.state.label)", - ]) + self.logger.debug( + "deactivating connection", + metadata: [ + "connectivity_state": "\(self.state.label)" + ] + ) switch self.state { // We can hit inactive in connecting if we see channelInactive before channelActive; that's not @@ -780,11 +804,13 @@ internal final class ConnectionManager: @unchecked Sendable { } self.logger.debug("scheduling connection attempt", metadata: ["delay": "0"]) let backoffIterator = self.connectionBackoff?.makeIterator() - self.state = .transientFailure(TransientFailureState( - from: ready, - scheduled: scheduled, - backoffIterator: backoffIterator - )) + self.state = .transientFailure( + TransientFailureState( + from: ready, + scheduled: scheduled, + backoffIterator: backoffIterator + ) + ) } // This is fine: we expect the channel to become inactive after becoming idle. @@ -805,9 +831,12 @@ internal final class ConnectionManager: @unchecked Sendable { /// called on the `EventLoop`. internal func ready() { self.eventLoop.preconditionInEventLoop() - self.logger.debug("connection ready", metadata: [ - "connectivity_state": "\(self.state.label)", - ]) + self.logger.debug( + "connection ready", + metadata: [ + "connectivity_state": "\(self.state.label)" + ] + ) switch self.state { case let .active(connected): @@ -838,9 +867,12 @@ internal final class ConnectionManager: @unchecked Sendable { /// the `EventLoop`. internal func idle() { self.eventLoop.preconditionInEventLoop() - self.logger.debug("idling connection", metadata: [ - "connectivity_state": "\(self.state.label)", - ]) + self.logger.debug( + "idling connection", + metadata: [ + "connectivity_state": "\(self.state.label)" + ] + ) switch self.state { case let .active(state): @@ -882,7 +914,8 @@ internal final class ConnectionManager: @unchecked Sendable { internal func maxConcurrentStreamsChanged(_ maxConcurrentStreams: Int) { self.eventLoop.assertInEventLoop() self.http2Delegate?.receivedSettingsMaxConcurrentStreams( - self, maxConcurrentStreams: maxConcurrentStreams + self, + maxConcurrentStreams: maxConcurrentStreams ) } diff --git a/Sources/GRPC/ConnectionManagerChannelProvider.swift b/Sources/GRPC/ConnectionManagerChannelProvider.swift index aaa6cf560..5a2210e73 100644 --- a/Sources/GRPC/ConnectionManagerChannelProvider.swift +++ b/Sources/GRPC/ConnectionManagerChannelProvider.swift @@ -16,10 +16,11 @@ import Logging import NIOCore import NIOPosix +import NIOTransportServices + #if canImport(NIOSSL) import NIOSSL #endif -import NIOTransportServices @usableFromInline internal protocol ConnectionManagerChannelProvider { @@ -44,7 +45,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider { enum TLSMode { #if canImport(NIOSSL) case configureWithNIOSSL(Result) - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) case configureWithNetworkFramework case disabled } @@ -115,7 +116,7 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider { // TLS is configured, and we aren't using a Network.framework TLS backend, so we must be // using NIOSSL, so we must be able to import it. fatalError() - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } } else { tlsMode = .disabled @@ -163,7 +164,8 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider { logger: logger ) - bootstrap = bootstrap + bootstrap = + bootstrap .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) .channelInitializer { channel in @@ -185,12 +187,12 @@ internal struct DefaultChannelProvider: ConnectionManagerChannelProvider { customVerificationCallback: self.tlsConfiguration?.nioSSLCustomVerificationCallback, logger: logger ) - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) // Network.framework TLS configuration is applied when creating the bootstrap so is a // no-op here. case .configureWithNetworkFramework, - .disabled: + .disabled: () } diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 132b3130d..d0d9508cc 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -348,9 +348,12 @@ internal final class ConnectionPool { ) { // Don't overwhelm the pool with too many waiters. guard self.waiters.count < self.maxWaiters else { - logger.trace("connection pool has too many waiters", metadata: [ - Metadata.waitersMax: "\(self.maxWaiters)", - ]) + logger.trace( + "connection pool has too many waiters", + metadata: [ + Metadata.waitersMax: "\(self.maxWaiters)" + ] + ) promise.fail(ConnectionPoolError.tooManyWaiters(connectionError: self._mostRecentError)) return } @@ -366,25 +369,34 @@ internal final class ConnectionPool { if let index = self.waiters.firstIndex(where: { $0.id == waiter.id }) { self.waiters.remove(at: index) - logger.trace("timed out waiting for a connection", metadata: [ - Metadata.waiterID: "\(waiter.id)", - Metadata.waitersCount: "\(self.waiters.count)", - ]) + logger.trace( + "timed out waiting for a connection", + metadata: [ + Metadata.waiterID: "\(waiter.id)", + Metadata.waitersCount: "\(self.waiters.count)", + ] + ) } } // request logger - logger.debug("waiting for a connection to become available", metadata: [ - Metadata.waiterID: "\(waiter.id)", - Metadata.waitersCount: "\(self.waiters.count)", - ]) + logger.debug( + "waiting for a connection to become available", + metadata: [ + Metadata.waiterID: "\(waiter.id)", + Metadata.waitersCount: "\(self.waiters.count)", + ] + ) self.waiters.append(waiter) // pool logger - self.logger.trace("enqueued connection waiter", metadata: [ - Metadata.waitersCount: "\(self.waiters.count)", - ]) + self.logger.trace( + "enqueued connection waiter", + metadata: [ + Metadata.waitersCount: "\(self.waiters.count)" + ] + ) if self._shouldBringUpAnotherConnection() { self._startConnectingIdleConnection() @@ -458,8 +470,9 @@ internal final class ConnectionPool { /// /// - Note: this is linear in the number of connections. @usableFromInline - internal func _mostAvailableConnectionIndex( - ) -> Dictionary.Index { + internal func _mostAvailableConnectionIndex() + -> Dictionary.Index + { var index = self._connections.values.startIndex var selectedIndex = self._connections.values.endIndex var mostAvailableStreams = 0 @@ -571,16 +584,16 @@ extension ConnectionPool: ConnectionManagerConnectivityDelegate { ) { switch (oldState, newState) { case let (.ready, .transientFailure(error)), - let (.ready, .idle(.some(error))): + let (.ready, .idle(.some(error))): self.updateMostRecentError(error) self.connectionUnavailable(manager.id) case (.ready, .idle(.none)), - (.ready, .shutdown): + (.ready, .shutdown): self.connectionUnavailable(manager.id) case let (_, .transientFailure(error)), - let (_, .idle(.some(error))): + let (_, .idle(.some(error))): self.updateMostRecentError(error) default: @@ -591,7 +604,7 @@ extension ConnectionPool: ConnectionManagerConnectivityDelegate { switch (oldState, newState) { case (.idle, .connecting), - (.transientFailure, .connecting): + (.transientFailure, .connecting): delegate.startedConnecting(id: .init(manager.id)) case (.connecting, .ready): @@ -675,7 +688,8 @@ extension ConnectionPool: ConnectionManagerHTTP2Delegate { internal func streamOpened(_ manager: ConnectionManager) { self.eventLoop.assertInEventLoop() if let utilization = self._connections[manager.id]?.openedStream(), - let delegate = self.delegate { + let delegate = self.delegate + { delegate.connectionUtilizationChanged( id: .init(manager.id), streamsUsed: utilization.used, @@ -693,7 +707,8 @@ extension ConnectionPool: ConnectionManagerHTTP2Delegate { // Return the stream the connection and to the pool manager. if let utilization = self._connections.values[index].returnStream(), - let delegate = self.delegate { + let delegate = self.delegate + { delegate.connectionUtilizationChanged( id: .init(manager.id), streamsUsed: utilization.used, @@ -765,9 +780,12 @@ extension ConnectionPool { private func tryServiceWaiters() { if self.waiters.isEmpty { return } - self.logger.trace("servicing waiters", metadata: [ - Metadata.waitersCount: "\(self.waiters.count)", - ]) + self.logger.trace( + "servicing waiters", + metadata: [ + Metadata.waitersCount: "\(self.waiters.count)" + ] + ) let now = self.now() var serviced = 0 @@ -791,10 +809,13 @@ extension ConnectionPool { } } - self.logger.trace("done servicing waiters", metadata: [ - Metadata.waitersCount: "\(self.waiters.count)", - Metadata.waitersServiced: "\(serviced)", - ]) + self.logger.trace( + "done servicing waiters", + metadata: [ + Metadata.waitersCount: "\(self.waiters.count)", + Metadata.waitersServiced: "\(serviced)", + ] + ) } } diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index c9f745422..76ab818d7 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -172,11 +172,14 @@ internal final class PoolManager { let pools = self.makePools(perPoolConfiguration: configuration, logger: logger) - logger.debug("initializing connection pool manager", metadata: [ - Metadata.poolCount: "\(pools.count)", - Metadata.connectionsPerPool: "\(configuration.maxConnections)", - Metadata.waitersPerPool: "\(configuration.maxWaiters)", - ]) + logger.debug( + "initializing connection pool manager", + metadata: [ + Metadata.poolCount: "\(pools.count)", + Metadata.connectionsPerPool: "\(configuration.maxConnections)", + Metadata.waitersPerPool: "\(configuration.maxWaiters)", + ] + ) // The assumed maximum number of streams concurrently available in each pool. let assumedCapacity = configuration.assumedStreamCapacity @@ -331,8 +334,8 @@ internal final class PoolManager { promise.succeed(()) case (.shutdownPools, .none), - (.alreadyShuttingDown, .some), - (.alreadyShutdown, .some): + (.alreadyShuttingDown, .some), + (.alreadyShutdown, .some): preconditionFailure() } } diff --git a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift b/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift index 62a2c7e56..5f0484a33 100644 --- a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift +++ b/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift @@ -45,13 +45,15 @@ internal struct PoolManagerStateMachine { poolKeys: [PoolManager.ConnectionPoolKey], assumedMaxAvailableStreamsPerPool: Int ) { - self.pools = Dictionary(uniqueKeysWithValues: poolKeys.map { key in - let value = PerPoolState( - poolIndex: key.index, - assumedMaxAvailableStreams: assumedMaxAvailableStreamsPerPool - ) - return (key.eventLoopID, value) - }) + self.pools = Dictionary( + uniqueKeysWithValues: poolKeys.map { key in + let value = PerPoolState( + poolIndex: key.index, + assumedMaxAvailableStreams: assumedMaxAvailableStreamsPerPool + ) + return (key.eventLoopID, value) + } + ) } } diff --git a/Sources/GRPC/ConnectionPool/PooledChannel.swift b/Sources/GRPC/ConnectionPool/PooledChannel.swift index f7e3af01a..1b706451f 100644 --- a/Sources/GRPC/ConnectionPool/PooledChannel.swift +++ b/Sources/GRPC/ConnectionPool/PooledChannel.swift @@ -16,10 +16,11 @@ import Logging import NIOCore import NIOHTTP2 +import SwiftProtobuf + #if canImport(NIOSSL) import NIOSSL #endif -import SwiftProtobuf @usableFromInline internal final class PooledChannel: GRPCChannel { @@ -70,7 +71,7 @@ internal final class PooledChannel: GRPCChannel { // - Network.framework is not available // NIOSSL or Network.framework must be available as TLS is configured. fatalError() - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } else { scheme = "http" tlsMode = .disabled diff --git a/Sources/GRPC/ConnectivityState.swift b/Sources/GRPC/ConnectivityState.swift index ff238ce65..bf9092b9b 100644 --- a/Sources/GRPC/ConnectivityState.swift +++ b/Sources/GRPC/ConnectivityState.swift @@ -120,10 +120,13 @@ public class ConnectivityStateMonitor: @unchecked Sendable { } if let (oldState, newState) = change { - logger.debug("connectivity state change", metadata: [ - "old_state": "\(oldState)", - "new_state": "\(newState)", - ]) + logger.debug( + "connectivity state change", + metadata: [ + "old_state": "\(oldState)", + "new_state": "\(newState)", + ] + ) self.delegateCallbackQueue.async { if let delegate = self.delegate { diff --git a/Sources/GRPC/DebugOnly.swift b/Sources/GRPC/DebugOnly.swift index 22a631fef..c30546970 100644 --- a/Sources/GRPC/DebugOnly.swift +++ b/Sources/GRPC/DebugOnly.swift @@ -15,5 +15,10 @@ */ internal func debugOnly(_ body: () -> Void) { - assert({ body(); return true }()) + assert( + { + body() + return true + }() + ) } diff --git a/Sources/GRPC/FakeChannel.swift b/Sources/GRPC/FakeChannel.swift index 8348b215f..56ed52c86 100644 --- a/Sources/GRPC/FakeChannel.swift +++ b/Sources/GRPC/FakeChannel.swift @@ -34,7 +34,8 @@ extension FakeChannel: @unchecked Sendable {} @available( swift, deprecated: 5.6, - message: "GRPCChannel implementations must be Sendable but this implementation is not. Using a client and server on localhost is the recommended alternative." + message: + "GRPCChannel implementations must be Sendable but this implementation is not. Using a client and server on localhost is the recommended alternative." ) public class FakeChannel: GRPCChannel { /// Fake response streams keyed by their path. @@ -43,9 +44,14 @@ public class FakeChannel: GRPCChannel { /// A logger. public let logger: Logger - public init(logger: Logger = Logger(label: "io.grpc", factory: { _ in - SwiftLogNoOpLogHandler() - })) { + public init( + logger: Logger = Logger( + label: "io.grpc", + factory: { _ in + SwiftLogNoOpLogHandler() + } + ) + ) { self.responseStreams = [:] self.logger = logger } diff --git a/Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift b/Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift index ca53cc1f6..b2e81e4b0 100644 --- a/Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift +++ b/Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift @@ -20,8 +20,10 @@ import NIOSSL extension ClientConnection { /// Returns a `ClientConnection` builder configured with TLS. @available( - *, deprecated, - message: "Use one of 'usingPlatformAppropriateTLS(for:)', 'usingTLSBackedByNIOSSL(on:)' or 'usingTLSBackedByNetworkFramework(on:)' or 'usingTLS(on:with:)'" + *, + deprecated, + message: + "Use one of 'usingPlatformAppropriateTLS(for:)', 'usingTLSBackedByNIOSSL(on:)' or 'usingTLSBackedByNetworkFramework(on:)' or 'usingTLS(on:with:)'" ) public static func secure(group: EventLoopGroup) -> ClientConnection.Builder.Secure { return ClientConnection.usingTLSBackedByNIOSSL(on: group) @@ -96,4 +98,4 @@ extension ClientConnection.Builder.Secure { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift b/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift index 132d8566f..be35c43e6 100644 --- a/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift +++ b/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift @@ -66,5 +66,5 @@ extension ClientConnection.Builder.Secure { } } -#endif // canImport(Network) -#endif // canImport(Security) +#endif // canImport(Network) +#endif // canImport(Security) diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index 1e2695b90..fe29c56a2 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -352,11 +352,14 @@ extension GRPCClientChannelHandler: ChannelInboundHandler { content: HTTP2Frame.FramePayload.Headers, context: ChannelHandlerContext ) { - self.logger.trace("received HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "HEADERS", - MetadataKey.h2Headers: "\(content.headers)", - MetadataKey.h2EndStream: "\(content.endStream)", - ]) + self.logger.trace( + "received HTTP2 frame", + metadata: [ + MetadataKey.h2Payload: "HEADERS", + MetadataKey.h2Headers: "\(content.headers)", + MetadataKey.h2EndStream: "\(content.endStream)", + ] + ) // In the case of a "Trailers-Only" response there's no guarantee that end-of-stream will be set // on the headers frame: end stream may be sent on an empty data frame as well. If the headers @@ -424,11 +427,14 @@ extension GRPCClientChannelHandler: ChannelInboundHandler { preconditionFailure("Received DATA frame with non-ByteBuffer IOData") } - self.logger.trace("received HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(content.data.readableBytes)", - MetadataKey.h2EndStream: "\(content.endStream)", - ]) + self.logger.trace( + "received HTTP2 frame", + metadata: [ + MetadataKey.h2Payload: "DATA", + MetadataKey.h2DataBytes: "\(content.data.readableBytes)", + MetadataKey.h2EndStream: "\(content.endStream)", + ] + ) self.consumeBytes(from: &buffer, context: context) @@ -507,11 +513,14 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { case let .success(headers): // We're clear to write some headers. Create an appropriate frame and write it. let framePayload = HTTP2Frame.FramePayload.headers(.init(headers: headers)) - self.logger.trace("writing HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "HEADERS", - MetadataKey.h2Headers: "\(headers)", - MetadataKey.h2EndStream: "false", - ]) + self.logger.trace( + "writing HTTP2 frame", + metadata: [ + MetadataKey.h2Payload: "HEADERS", + MetadataKey.h2Headers: "\(headers)", + MetadataKey.h2EndStream: "false", + ] + ) context.write(self.wrapOutboundOut(framePayload), promise: promise) case let .failure(sendRequestHeadersError): @@ -562,11 +571,14 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { .init(data: .byteBuffer(buffer), endStream: false) ) - self.logger.trace("writing HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(buffer.readableBytes)", - MetadataKey.h2EndStream: "false", - ]) + self.logger.trace( + "writing HTTP2 frame", + metadata: [ + MetadataKey.h2Payload: "DATA", + MetadataKey.h2DataBytes: "\(buffer.readableBytes)", + MetadataKey.h2EndStream: "false", + ] + ) context.write(self.wrapOutboundOut(framePayload), promise: promise) case let .failure(error): @@ -585,11 +597,14 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { .init(data: .byteBuffer(empty), endStream: true) ) - self.logger.trace("writing HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "0", - MetadataKey.h2EndStream: "true", - ]) + self.logger.trace( + "writing HTTP2 frame", + metadata: [ + MetadataKey.h2Payload: "DATA", + MetadataKey.h2DataBytes: "0", + MetadataKey.h2EndStream: "true", + ] + ) context.write(self.wrapOutboundOut(framePayload), promise: promise) case let .failure(error): @@ -621,11 +636,14 @@ extension GRPCClientChannelHandler: ChannelOutboundHandler { .init(data: .byteBuffer(buffer), endStream: false) ) - self.logger.trace("writing HTTP2 frame", metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(buffer.readableBytes)", - MetadataKey.h2EndStream: "false", - ]) + self.logger.trace( + "writing HTTP2 frame", + metadata: [ + MetadataKey.h2Payload: "DATA", + MetadataKey.h2DataBytes: "\(buffer.readableBytes)", + MetadataKey.h2EndStream: "false", + ] + ) context.write(self.wrapOutboundOut(framePayload), promise: promise) case let .failure(error): diff --git a/Sources/GRPC/GRPCClientStateMachine.swift b/Sources/GRPC/GRPCClientStateMachine.swift index b8a4c0a7b..a3d87ec52 100644 --- a/Sources/GRPC/GRPCClientStateMachine.swift +++ b/Sources/GRPC/GRPCClientStateMachine.swift @@ -379,10 +379,10 @@ extension GRPCClientStateMachine.State { ) case .clientActiveServerIdle, - .clientClosedServerIdle, - .clientClosedServerActive, - .clientActiveServerActive, - .clientClosedServerClosed: + .clientClosedServerIdle, + .clientClosedServerActive, + .clientActiveServerActive, + .clientClosedServerClosed: result = .failure(.invalidState) case .modifying: @@ -412,8 +412,8 @@ extension GRPCClientStateMachine.State { return result case .clientClosedServerIdle, - .clientClosedServerActive, - .clientClosedServerClosed: + .clientClosedServerActive, + .clientClosedServerClosed: result = .failure(.cardinalityViolation) case .clientIdleServerIdle: @@ -441,9 +441,9 @@ extension GRPCClientStateMachine.State { return result case .clientIdleServerIdle, - .clientClosedServerIdle, - .clientClosedServerActive, - .clientClosedServerClosed: + .clientClosedServerIdle, + .clientClosedServerActive, + .clientClosedServerClosed: return nil case .modifying: @@ -465,8 +465,8 @@ extension GRPCClientStateMachine.State { self = .clientClosedServerActive(readState: readState) case .clientClosedServerIdle, - .clientClosedServerActive, - .clientClosedServerClosed: + .clientClosedServerActive, + .clientClosedServerClosed: result = .failure(.alreadyClosed) case .clientIdleServerIdle: @@ -499,9 +499,9 @@ extension GRPCClientStateMachine.State { } case .clientIdleServerIdle, - .clientClosedServerActive, - .clientActiveServerActive, - .clientClosedServerClosed: + .clientClosedServerActive, + .clientActiveServerActive, + .clientClosedServerClosed: result = .failure(.invalidState) case .modifying: @@ -528,9 +528,9 @@ extension GRPCClientStateMachine.State { self = .clientActiveServerActive(writeState: writeState, readState: readState) case .clientIdleServerIdle, - .clientActiveServerIdle, - .clientClosedServerIdle, - .clientClosedServerClosed: + .clientActiveServerIdle, + .clientClosedServerIdle, + .clientClosedServerClosed: result = .failure(.invalidState) case .modifying: @@ -548,19 +548,19 @@ extension GRPCClientStateMachine.State { switch self { case .clientActiveServerIdle, - .clientClosedServerIdle: + .clientClosedServerIdle: result = self.parseTrailersOnly(trailers).map { status in self = .clientClosedServerClosed return status } case .clientActiveServerActive, - .clientClosedServerActive: + .clientClosedServerActive: result = .success(self.parseTrailers(trailers)) self = .clientClosedServerClosed case .clientIdleServerIdle, - .clientClosedServerClosed: + .clientClosedServerClosed: result = .failure(.invalidState) case .modifying: @@ -580,9 +580,9 @@ extension GRPCClientStateMachine.State { preconditionFailure() case .clientActiveServerIdle, - .clientActiveServerActive, - .clientClosedServerIdle, - .clientClosedServerActive: + .clientActiveServerActive, + .clientClosedServerIdle, + .clientClosedServerActive: self = .clientClosedServerClosed status = .init( code: .internalError, @@ -656,9 +656,11 @@ extension GRPCClientStateMachine.State { // Add user-defined custom metadata: this should come after the call definition headers. // TODO: make header normalization user-configurable. - headers.add(contentsOf: customMetadata.lazy.map { name, value, indexing in - (name.lowercased(), value, indexing) - }) + headers.add( + contentsOf: customMetadata.lazy.map { name, value, indexing in + (name.lowercased(), value, indexing) + } + ) // Add default user-agent value, if `customMetadata` didn't contain user-agent if !customMetadata.contains(name: "user-agent") { @@ -685,7 +687,8 @@ extension GRPCClientStateMachine.State { // Implementations must synthesize a Status & Status-Message to propagate to the application // layer when this occurs." let statusHeader = headers.first(name: ":status") - let responseStatus = statusHeader + let responseStatus = + statusHeader .flatMap(Int.init) .map { code in HTTPResponseStatus(statusCode: code) @@ -771,7 +774,8 @@ extension GRPCClientStateMachine.State { guard httpResponseStatus == .ok else { // Non-200 response. If there's a 'grpc-status' message we should use that otherwise try // to create one from the HTTP status code. - let grpcStatusCode = self.readStatusCode(from: trailers) + let grpcStatusCode = + self.readStatusCode(from: trailers) ?? GRPCStatus.Code(httpStatus: Int(httpResponseStatus.code)) ?? .unknown let message = self.readStatusMessage(from: trailers) @@ -783,7 +787,8 @@ extension GRPCClientStateMachine.State { // missing then we should avoid the error and propagate the status code and message sent by // the server instead. if let contentTypeHeader = trailers.first(name: "content-type"), - ContentType(value: contentTypeHeader) == nil { + ContentType(value: contentTypeHeader) == nil + { return .failure(.invalidContentType(contentTypeHeader)) } diff --git a/Sources/GRPC/GRPCContentType.swift b/Sources/GRPC/GRPCContentType.swift index 2f522c1a5..e8d706963 100644 --- a/Sources/GRPC/GRPCContentType.swift +++ b/Sources/GRPC/GRPCContentType.swift @@ -25,15 +25,15 @@ internal enum ContentType { init?(value: String) { switch value { case "application/grpc", - "application/grpc+proto": + "application/grpc+proto": self = .protobuf case "application/grpc-web", - "application/grpc-web+proto": + "application/grpc-web+proto": self = .webProtobuf case "application/grpc-web-text", - "application/grpc-web-text+proto": + "application/grpc-web-text+proto": self = .webTextProtobuf default: diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 4e5c1eb90..fc62ec3bd 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -128,7 +128,8 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { // Max concurrent streams changed. if let manager = self.mode.connectionManager, - let maxConcurrentStreams = operations.maxConcurrentStreamsChange { + let maxConcurrentStreams = operations.maxConcurrentStreamsChange + { manager.maxConcurrentStreamsChanged(maxConcurrentStreams) } @@ -257,10 +258,13 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { // Swallow this event. } else if case let .handshakeCompleted(negotiatedProtocol) = event as? TLSUserEvent { let tlsVersion = try? context.channel.getTLSVersionSync() - self.stateMachine.logger.debug("TLS handshake completed", metadata: [ - "alpn": "\(negotiatedProtocol ?? "nil")", - "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", - ]) + self.stateMachine.logger.debug( + "TLS handshake completed", + metadata: [ + "alpn": "\(negotiatedProtocol ?? "nil")", + "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", + ] + ) context.fireUserInboundEventTriggered(event) } else { context.fireUserInboundEventTriggered(event) @@ -303,10 +307,13 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { switch frame.payload { case let .goAway(lastStreamID, errorCode, _): - self.stateMachine.logger.debug("received GOAWAY frame", metadata: [ - MetadataKey.h2GoAwayLastStreamID: "\(Int(lastStreamID))", - MetadataKey.h2GoAwayError: "\(errorCode.networkCode)", - ]) + self.stateMachine.logger.debug( + "received GOAWAY frame", + metadata: [ + MetadataKey.h2GoAwayLastStreamID: "\(Int(lastStreamID))", + MetadataKey.h2GoAwayError: "\(errorCode.networkCode)", + ] + ) self.perform(operations: self.stateMachine.receiveGoAway()) case let .settings(.settings(settings)): self.perform(operations: self.stateMachine.receiveSettings(settings)) diff --git a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift b/Sources/GRPC/GRPCIdleHandlerStateMachine.swift index 5a52c14a8..f7e0b7581 100644 --- a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift +++ b/Sources/GRPC/GRPCIdleHandlerStateMachine.swift @@ -528,9 +528,12 @@ struct GRPCIdleHandlerStateMachine { // Log the change in settings. self.logger.debug( "HTTP2 settings update", - metadata: Dictionary(settings.map { - ("\($0.parameter.loggingMetadataKey)", "\($0.value)") - }, uniquingKeysWith: { a, _ in a }) + metadata: Dictionary( + settings.map { + ("\($0.parameter.loggingMetadataKey)", "\($0.value)") + }, + uniquingKeysWith: { a, _ in a } + ) ) var operations: Operations = .none @@ -673,15 +676,21 @@ extension CanOpenStreams { () } - logger.debug("HTTP2 stream created", metadata: [ - MetadataKey.h2StreamID: "\(streamID)", - MetadataKey.h2ActiveStreams: "\(self.openStreams)", - ]) + logger.debug( + "HTTP2 stream created", + metadata: [ + MetadataKey.h2StreamID: "\(streamID)", + MetadataKey.h2ActiveStreams: "\(self.openStreams)", + ] + ) if self.openStreams == self.maxConcurrentStreams { - logger.warning("HTTP2 max concurrent stream limit reached", metadata: [ - MetadataKey.h2ActiveStreams: "\(self.openStreams)", - ]) + logger.warning( + "HTTP2 max concurrent stream limit reached", + metadata: [ + MetadataKey.h2ActiveStreams: "\(self.openStreams)" + ] + ) } } } @@ -695,9 +704,12 @@ extension CanCloseStreams { fileprivate mutating func streamClosed(_ streamID: HTTP2StreamID, logger: Logger) { self.openStreams -= 1 - logger.debug("HTTP2 stream closed", metadata: [ - MetadataKey.h2StreamID: "\(streamID)", - MetadataKey.h2ActiveStreams: "\(self.openStreams)", - ]) + logger.debug( + "HTTP2 stream closed", + metadata: [ + MetadataKey.h2StreamID: "\(streamID)", + MetadataKey.h2ActiveStreams: "\(self.openStreams)", + ] + ) } } diff --git a/Sources/GRPC/GRPCKeepaliveHandlers.swift b/Sources/GRPC/GRPCKeepaliveHandlers.swift index 3fa66ed75..38f88a97e 100644 --- a/Sources/GRPC/GRPCKeepaliveHandlers.swift +++ b/Sources/GRPC/GRPCKeepaliveHandlers.swift @@ -215,8 +215,8 @@ struct PingHandler { "Ping strikes are not supported but we're checking for one" ) guard self.activeStreams == 0, self.permitWithoutCalls, - let lastReceivedPingDate = self.lastReceivedPingDate, - let minimumReceivedPingIntervalWithoutData = self.minimumReceivedPingIntervalWithoutData + let lastReceivedPingDate = self.lastReceivedPingDate, + let minimumReceivedPingIntervalWithoutData = self.minimumReceivedPingIntervalWithoutData else { return false } @@ -239,7 +239,8 @@ struct PingHandler { // The time elapsed since the previous ping is less than the minimum required if let lastSentPingDate = self.lastSentPingDate, - self.now() - lastSentPingDate < self.minimumSentPingIntervalWithoutData { + self.now() - lastSentPingDate < self.minimumSentPingIntervalWithoutData + { return true } diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift index eba4f9c5a..b40b78e56 100644 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ b/Sources/GRPC/GRPCServerPipelineConfigurator.swift @@ -114,9 +114,10 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan // Sync options were added to the HTTP/2 stream channel in 1.17.0 (we require at least this) // so this shouldn't be `nil`, but it's not a problem if it is. let http2StreamID = try? stream.syncOptions?.getOption(HTTP2StreamChannelOptions.streamID) - let streamID = http2StreamID.map { streamID in - return String(Int(streamID)) - } ?? "" + let streamID = + http2StreamID.map { streamID in + return String(Int(streamID)) + } ?? "" logger[metadataKey: MetadataKey.h2StreamID] = "\(streamID)" @@ -222,7 +223,7 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan self.configuration.logger.error("Unable to determine http version, closing") context.close(mode: .all, promise: nil) case .notEnoughBytes: - () // Try again with more bytes. + () // Try again with more bytes. } } @@ -236,10 +237,13 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan switch event { case let .handshakeCompleted(negotiatedProtocol): let tlsVersion = try? context.channel.getTLSVersionSync() - self.configuration.logger.debug("TLS handshake completed", metadata: [ - "alpn": "\(negotiatedProtocol ?? "nil")", - "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", - ]) + self.configuration.logger.debug( + "TLS handshake completed", + metadata: [ + "alpn": "\(negotiatedProtocol ?? "nil")", + "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", + ] + ) switch negotiatedProtocol { case let .some(negotiated): @@ -303,8 +307,8 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan } case .notConfigured(alpn: .expectedButFallingBack), - .notConfigured(alpn: .notExpected), - .configuring: + .notConfigured(alpn: .notExpected), + .configuring: () } @@ -317,13 +321,13 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan switch self.state { case .notConfigured(alpn: .notExpected), - .notConfigured(alpn: .expectedButFallingBack): + .notConfigured(alpn: .expectedButFallingBack): // If ALPN isn't expected, or we didn't negotiate via ALPN and we don't require it then we // can try parsing the data we just buffered. self.tryParsingBufferedData(context: context) case .notConfigured(alpn: .expected), - .configuring: + .configuring: // We expect ALPN or we're being configured, just buffer the data, we'll forward it later. () } @@ -476,7 +480,8 @@ struct HTTPVersionParser { } guard let version = readableBytesView.dropPrefix(through: UInt8(ascii: "\r")), - readableBytesView.first == UInt8(ascii: "\n") else { + readableBytesView.first == UInt8(ascii: "\n") + else { // If we didn't drop the prefix OR we did and the next byte wasn't '\n', then we had enough // bytes but the '\r\n' wasn't present: reject this as being HTTP1. return .rejected diff --git a/Sources/GRPC/GRPCStatusMessageMarshaller.swift b/Sources/GRPC/GRPCStatusMessageMarshaller.swift index c9df18d72..827933054 100644 --- a/Sources/GRPC/GRPCStatusMessageMarshaller.swift +++ b/Sources/GRPC/GRPCStatusMessageMarshaller.swift @@ -59,7 +59,7 @@ extension GRPCStatusMessageMarshaller { switch char { // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses case 0x20 ... 0x24, - 0x26 ... 0x7E: + 0x26 ... 0x7E: bytes.append(char) default: @@ -78,7 +78,7 @@ extension GRPCStatusMessageMarshaller { for byte in view { switch byte { case 0x20 ... 0x24, - 0x26 ... 0x7E: + 0x26 ... 0x7E: () default: @@ -131,8 +131,8 @@ extension GRPCStatusMessageMarshaller { switch byte { case UInt8(ascii: "%"): guard let (nextIndex, nextNextIndex) = utf8.nextTwoIndices(after: currentIndex), - let nextHex = fromHex(utf8[nextIndex]), - let nextNextHex = fromHex(utf8[nextNextIndex]) + let nextHex = fromHex(utf8[nextIndex]), + let nextNextHex = fromHex(utf8[nextNextIndex]) else { // If we can't decode the message, aborting and returning the encoded message is fine // according to the spec. diff --git a/Sources/GRPC/GRPCTLSConfiguration.swift b/Sources/GRPC/GRPCTLSConfiguration.swift index 99aebfd74..902b4fc97 100644 --- a/Sources/GRPC/GRPCTLSConfiguration.swift +++ b/Sources/GRPC/GRPCTLSConfiguration.swift @@ -69,7 +69,7 @@ public struct GRPCTLSConfiguration: Sendable { #endif } } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) internal var isNetworkFrameworkTLSBackend: Bool { switch self.backend { @@ -175,7 +175,7 @@ public struct GRPCTLSConfiguration: Sendable { configuration: deprecated.configuration, customVerificationCallback: deprecated.customVerificationCallback, hostnameOverride: deprecated.hostnameOverride, - requireALPN: false // Not currently supported. + requireALPN: false // Not currently supported. ) ) } @@ -209,11 +209,12 @@ public struct GRPCTLSConfiguration: Sendable { } return nil } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } // MARK: - NIO Backend +// canImport(NIOSSL) #if canImport(NIOSSL) extension GRPCTLSConfiguration { internal struct NIOConfiguration { @@ -287,7 +288,7 @@ extension GRPCTLSConfiguration { configuration: configuration, customVerificationCallback: customVerificationCallback, hostnameOverride: hostnameOverride, - requireALPN: false // We don't currently support this. + requireALPN: false // We don't currently support this. ) return GRPCTLSConfiguration(nio: nioConfiguration) @@ -481,7 +482,7 @@ extension GRPCTLSConfiguration { } } } -#endif // canImport(NIOSSL) +#endif // MARK: - Network Backend @@ -652,7 +653,7 @@ extension GRPCTLSConfiguration { #if canImport(NIOSSL) case .nio: preconditionFailure() - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } } } @@ -673,7 +674,7 @@ extension GRPCTLSConfiguration { // We're using NIOSSL with Network.framework; that's okay and permitted for backwards // compatibility. return bootstrap - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } } @@ -690,7 +691,7 @@ extension GRPCTLSConfiguration { // We're using NIOSSL with Network.framework; that's okay and permitted for backwards // compatibility. return bootstrap - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } } } diff --git a/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift b/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift index 761c307ce..976be10c1 100644 --- a/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift +++ b/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.Data + import NIOCore import NIOHPACK import NIOHTTP1 import NIOHTTP2 +import struct Foundation.Data + /// A codec for translating between gRPC Web (as HTTP/1) and HTTP/2 frame payloads. internal final class GRPCWebToHTTP2ServerCodec: ChannelDuplexHandler { internal typealias InboundIn = HTTPServerRequestPart @@ -242,14 +244,14 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.State { return self.processResponseData(payload, promise: promise) case .priority, - .rstStream, - .settings, - .pushPromise, - .ping, - .goAway, - .windowUpdate, - .alternativeService, - .origin: + .rstStream, + .settings, + .pushPromise, + .ping, + .goAway, + .windowUpdate, + .alternativeService, + .origin: preconditionFailure("Unsupported frame payload") } } @@ -573,7 +575,7 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.State { preconditionFailure("Invalid state: haven't received request head") case let .fullyOpen(_, outbound), - let .clientClosedServerOpen(outbound): + let .clientClosedServerOpen(outbound): if outbound.responseHeadersSent { // Headers have been sent, these must be trailers, so end stream must be set. assert(payload.endStream) @@ -754,8 +756,9 @@ extension GRPCWebToHTTP2ServerCodec.StateMachine.InboundState { let action: GRPCWebToHTTP2ServerCodec.StateMachine.Action if bytesToRead > 0, - let base64Encoded = self.requestBuffer!.readString(length: bytesToRead), - let base64Decoded = Data(base64Encoded: base64Encoded) { + let base64Encoded = self.requestBuffer!.readString(length: bytesToRead), + let base64Decoded = Data(base64Encoded: base64Encoded) + { // Recycle the input buffer and restore the request buffer. buffer.clear() buffer.writeContiguousBytes(base64Decoded) diff --git a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift b/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift index af424d0c3..49c13597f 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift @@ -165,14 +165,14 @@ internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServe // Ignored. case .alternativeService, - .goAway, - .origin, - .ping, - .priority, - .pushPromise, - .rstStream, - .settings, - .windowUpdate: + .goAway, + .origin, + .ping, + .priority, + .pushPromise, + .rstStream, + .settings, + .windowUpdate: () } } diff --git a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift index a134c786e..de5456d8e 100644 --- a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift +++ b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift @@ -295,7 +295,8 @@ extension HTTP2ToRawGRPCStateMachine.State { } guard let callPath = CallPath(requestURI: path), - let service = services[Substring(callPath.service)] else { + let service = services[Substring(callPath.service)] + else { return self.methodNotImplemented(path, contentType: contentType) } @@ -415,8 +416,8 @@ extension HTTP2ToRawGRPCStateMachine.State { acceptEncoding = advertisedEncoding.joined(separator: ",") status = GRPCStatus( code: .unimplemented, - message: "\(encoding) compression is not supported, supported algorithms are " + - "listed in '\(GRPCHeaderName.acceptEncoding)'" + message: "\(encoding) compression is not supported, supported algorithms are " + + "listed in '\(GRPCHeaderName.acceptEncoding)'" ) } @@ -462,7 +463,8 @@ extension HTTP2ToRawGRPCStateMachine.State { } let status = GRPCStatus( code: .invalidArgument, - message: "'\(GRPCHeaderName.encoding)' must contain no more than one value but was '\(encodings.joined(separator: ", "))'" + message: + "'\(GRPCHeaderName.encoding)' must contain no more than one value but was '\(encodings.joined(separator: ", "))'" ) return .invalid(status: status, acceptEncoding: nil) } @@ -911,9 +913,9 @@ extension HTTP2ToRawGRPCStateMachine.State { return action case .requestOpenResponseOpen, - .requestOpenResponseClosed, - .requestClosedResponseOpen, - .requestClosedResponseClosed: + .requestOpenResponseClosed, + .requestClosedResponseOpen, + .requestClosedResponseClosed: preconditionFailure("Invalid state: response stream opened before pipeline was configured") } } @@ -934,7 +936,7 @@ extension HTTP2ToRawGRPCStateMachine.State { switch self { // These are the only states in which we can receive headers. Everything else is invalid. case .requestIdleResponseIdle, - .requestClosedResponseClosed: + .requestClosedResponseClosed: let stateAndAction = self._receive( headers: headers, eventLoop: eventLoop, @@ -953,10 +955,10 @@ extension HTTP2ToRawGRPCStateMachine.State { // We can't receive headers in any of these states. case .requestOpenResponseIdle, - .requestOpenResponseOpen, - .requestOpenResponseClosed, - .requestClosedResponseIdle, - .requestClosedResponseOpen: + .requestOpenResponseOpen, + .requestOpenResponseClosed, + .requestClosedResponseIdle, + .requestClosedResponseOpen: preconditionFailure("Invalid state: \(self)") } } @@ -982,7 +984,7 @@ extension HTTP2ToRawGRPCStateMachine.State { return stateAndAction.action case .requestClosedResponseIdle, - .requestClosedResponseOpen: + .requestClosedResponseOpen: preconditionFailure("Invalid state: the request stream is already closed") case .requestOpenResponseClosed: @@ -1030,7 +1032,7 @@ extension HTTP2ToRawGRPCStateMachine.State { return action case .requestOpenResponseClosed, - .requestClosedResponseClosed: + .requestClosedResponseClosed: return .none } } @@ -1051,9 +1053,9 @@ extension HTTP2ToRawGRPCStateMachine.State { return .success(headers) case .requestOpenResponseOpen, - .requestOpenResponseClosed, - .requestClosedResponseOpen, - .requestClosedResponseClosed: + .requestOpenResponseClosed, + .requestClosedResponseOpen, + .requestClosedResponseClosed: return .failure(GRPCError.AlreadyComplete()) } } @@ -1068,7 +1070,7 @@ extension HTTP2ToRawGRPCStateMachine.State { preconditionFailure("Invalid state: the request stream is still closed") case .requestOpenResponseIdle, - .requestClosedResponseIdle: + .requestClosedResponseIdle: let error = GRPCError.InvalidState("Response headers must be sent before response message") return .failure(error) @@ -1085,7 +1087,7 @@ extension HTTP2ToRawGRPCStateMachine.State { return .success(()) case .requestOpenResponseClosed, - .requestClosedResponseClosed: + .requestClosedResponseClosed: return .failure(GRPCError.AlreadyComplete()) } } @@ -1096,7 +1098,7 @@ extension HTTP2ToRawGRPCStateMachine.State { preconditionFailure("Invalid state: the request stream is still closed") case .requestOpenResponseIdle, - .requestClosedResponseIdle: + .requestClosedResponseIdle: return nil case var .requestOpenResponseOpen(state): @@ -1112,7 +1114,7 @@ extension HTTP2ToRawGRPCStateMachine.State { return result case .requestOpenResponseClosed, - .requestClosedResponseClosed: + .requestClosedResponseClosed: return nil } } @@ -1142,7 +1144,7 @@ extension HTTP2ToRawGRPCStateMachine.State { return .sendTrailersAndFinish(state.send(status: status, trailers: trailers)) case .requestOpenResponseClosed, - .requestClosedResponseClosed: + .requestClosedResponseClosed: return .failure(GRPCError.AlreadyComplete()) } } @@ -1250,16 +1252,18 @@ extension HTTP2ToRawGRPCStateMachine { } private static let gRPCStatusOkTrailers: HPACKHeaders = [ - GRPCHeaderName.statusCode: String(describing: GRPCStatus.Code.ok.rawValue), + GRPCHeaderName.statusCode: String(describing: GRPCStatus.Code.ok.rawValue) ] } extension HPACKHeaders { fileprivate mutating func add(contentsOf other: HPACKHeaders, normalize: Bool) { if normalize { - self.add(contentsOf: other.lazy.map { name, value, indexable in - (name: name.lowercased(), value: value, indexable: indexable) - }) + self.add( + contentsOf: other.lazy.map { name, value, indexable in + (name: name.lowercased(), value: value, indexable: indexable) + } + ) } else { self.add(contentsOf: other) } diff --git a/Sources/GRPC/Interceptor/ClientTransport.swift b/Sources/GRPC/Interceptor/ClientTransport.swift index 0387d1ca0..f2ed7b86a 100644 --- a/Sources/GRPC/Interceptor/ClientTransport.swift +++ b/Sources/GRPC/Interceptor/ClientTransport.swift @@ -785,7 +785,7 @@ extension ClientTransportState { return .propagateError case .activatingTransport, - .active: + .active: // We're either fully active or unbuffering. Forward an error, fail any writes and then close. self = .closing return .propagateErrorAndClose @@ -855,10 +855,13 @@ extension ClientTransport { promise: EventLoopPromise? ) { self.callEventLoop.assertInEventLoop() - self.logger.trace("buffering request part", metadata: [ - "request_part": "\(part.name)", - "call_state": self.stateForLogging, - ]) + self.logger.trace( + "buffering request part", + metadata: [ + "request_part": "\(part.name)", + "call_state": self.stateForLogging, + ] + ) self.writeBuffer.append(.init(request: part, promise: promise)) } @@ -873,9 +876,12 @@ extension ClientTransport { // Save any flushing until we're done writing. var shouldFlush = false - self.logger.trace("unbuffering request parts", metadata: [ - "request_parts": "\(self.writeBuffer.count)", - ]) + self.logger.trace( + "unbuffering request parts", + metadata: [ + "request_parts": "\(self.writeBuffer.count)" + ] + ) // Why the double loop? A promise completed as a result of the flush may enqueue more writes, // or causes us to change state (i.e. we may have to close). If we didn't loop around then we @@ -883,9 +889,12 @@ extension ClientTransport { while self.state.isUnbuffering, !self.writeBuffer.isEmpty { // Pull out as many writes as possible. while let write = self.writeBuffer.popFirst() { - self.logger.trace("unbuffering request part", metadata: [ - "request_part": "\(write.request.name)", - ]) + self.logger.trace( + "unbuffering request part", + metadata: [ + "request_part": "\(write.request.name)" + ] + ) if !shouldFlush { shouldFlush = self.shouldFlush(after: write.request) diff --git a/Sources/GRPC/Interceptor/ClientTransportFactory.swift b/Sources/GRPC/Interceptor/ClientTransportFactory.swift index 85feb8d72..5023ebfa7 100644 --- a/Sources/GRPC/Interceptor/ClientTransportFactory.swift +++ b/Sources/GRPC/Interceptor/ClientTransportFactory.swift @@ -15,6 +15,7 @@ */ import NIOCore import NIOHTTP2 + import protocol SwiftProtobuf.Message /// A `ClientTransport` factory for an RPC. @@ -51,8 +52,11 @@ internal struct ClientTransportFactory { scheme: String, maximumReceiveMessageLength: Int, errorDelegate: ClientErrorDelegate? - ) -> ClientTransportFactory where Request: SwiftProtobuf.Message, - Response: SwiftProtobuf.Message { + ) -> ClientTransportFactory + where + Request: SwiftProtobuf.Message, + Response: SwiftProtobuf.Message + { let http2 = HTTP2ClientTransportFactory( streamChannel: channel, scheme: scheme, @@ -98,8 +102,11 @@ internal struct ClientTransportFactory { @usableFromInline internal static func fake( _ fakeResponse: _FakeResponseStream? - ) -> ClientTransportFactory where Request: SwiftProtobuf.Message, - Response: SwiftProtobuf.Message { + ) -> ClientTransportFactory + where + Request: SwiftProtobuf.Message, + Response: SwiftProtobuf.Message + { let factory = FakeClientTransportFactory( fakeResponse, requestSerializer: ProtobufSerializer(), @@ -303,10 +310,13 @@ internal struct FakeClientTransportFactory { requestDeserializer: RequestDeserializer, responseSerializer: ResponseSerializer, responseDeserializer: ResponseDeserializer - ) where RequestSerializer.Input == Request, + ) + where + RequestSerializer.Input == Request, RequestDeserializer.Output == Request, ResponseSerializer.Input == Response, - ResponseDeserializer.Output == Response { + ResponseDeserializer.Output == Response + { self.fakeResponseStream = fakeResponseStream self.requestSerializer = AnySerializer(wrapping: requestSerializer) self.responseDeserializer = AnyDeserializer(wrapping: responseDeserializer) diff --git a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift b/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift index 863c63941..15c6442c9 100644 --- a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift +++ b/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift @@ -49,10 +49,13 @@ internal final class ServerInterceptorPipeline { /// Called when a response part has traversed the interceptor pipeline. @usableFromInline - internal var _onResponsePart: Optional<( - GRPCServerResponsePart, - EventLoopPromise? - ) -> Void> + internal var _onResponsePart: + Optional< + ( + GRPCServerResponsePart, + EventLoopPromise? + ) -> Void + > /// Called when a request part has traversed the interceptor pipeline. @usableFromInline diff --git a/Sources/GRPC/LengthPrefixedMessageReader.swift b/Sources/GRPC/LengthPrefixedMessageReader.swift index c3f49c47e..e1700963f 100644 --- a/Sources/GRPC/LengthPrefixedMessageReader.swift +++ b/Sources/GRPC/LengthPrefixedMessageReader.swift @@ -113,7 +113,7 @@ internal struct LengthPrefixedMessageReader { .reserveCapacity(minimumWritableBytes: max(remainingMessageBytes, buffer.readableBytes)) case .expectingCompressedFlag, - .expectingMessageLength: + .expectingMessageLength: // Just append the buffer; these parts are too small to make a meaningful difference. () } diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift index 32bce8671..efa6009f5 100644 --- a/Sources/GRPC/PlatformSupport.swift +++ b/Sources/GRPC/PlatformSupport.swift @@ -165,7 +165,9 @@ public protocol ServerBootstrapProtocol { func serverChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption @preconcurrency - func childChannelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture) + func childChannelInitializer( + _ handler: @escaping @Sendable (Channel) -> EventLoopFuture + ) -> Self func childChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption @@ -278,7 +280,8 @@ public enum PlatformSupport { #if canImport(Network) if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), - let transportServicesBootstrap = bootstrap as? NIOTSConnectionBootstrap { + let transportServicesBootstrap = bootstrap as? NIOTSConnectionBootstrap + { return transportServicesBootstrap.tlsOptions(from: tlsConfigruation) } #endif diff --git a/Sources/GRPC/ReadWriteStates.swift b/Sources/GRPC/ReadWriteStates.swift index 62456f7cf..9aede44cf 100644 --- a/Sources/GRPC/ReadWriteStates.swift +++ b/Sources/GRPC/ReadWriteStates.swift @@ -130,7 +130,7 @@ struct PendingReadState { ) case (.enabled, .none), - (.disabled, _): + (.disabled, _): reader = LengthPrefixedMessageReader() } return .reading(self.arity, reader) @@ -161,7 +161,7 @@ enum ReadState { return .failure(.cardinalityViolation) case .reading(let readArity, var reader): - self = .notReading // Avoid CoWs + self = .notReading // Avoid CoWs reader.append(buffer: &buffer) var messages: [ByteBuffer] = [] @@ -187,7 +187,7 @@ enum ReadState { switch (readArity, messages.count) { // Always allowed: case (.one, 0), - (.many, 0...): + (.many, 0...): self = .reading(readArity, reader) return .success(messages) diff --git a/Sources/GRPC/Serialization.swift b/Sources/GRPC/Serialization.swift index 641ac86f9..a6c590d59 100644 --- a/Sources/GRPC/Serialization.swift +++ b/Sources/GRPC/Serialization.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.Data + import NIOCore import NIOFoundationCompat import SwiftProtobuf +import struct Foundation.Data + public protocol MessageSerializer { associatedtype Input diff --git a/Sources/GRPC/Server+NIOSSL.swift b/Sources/GRPC/Server+NIOSSL.swift index 5656ca744..31a1d9e6d 100644 --- a/Sources/GRPC/Server+NIOSSL.swift +++ b/Sources/GRPC/Server+NIOSSL.swift @@ -122,4 +122,4 @@ extension Server.Configuration { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 3ff6331a1..1f0ce6e40 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -20,10 +20,11 @@ import NIOExtras import NIOHTTP1 import NIOHTTP2 import NIOPosix +import NIOTransportServices + #if canImport(NIOSSL) import NIOSSL #endif -import NIOTransportServices #if canImport(Network) import Network #endif @@ -116,18 +117,20 @@ public final class Server: @unchecked Sendable { // No TLS configuration, no SSL context. sslContext = nil } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) #if canImport(Network) if let tlsConfiguration = configuration.tlsConfiguration { if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), - let transportServicesBootstrap = bootstrap as? NIOTSListenerBootstrap { + let transportServicesBootstrap = bootstrap as? NIOTSListenerBootstrap + { _ = transportServicesBootstrap.tlsOptions(from: tlsConfiguration) } } - #endif // canImport(Network) + #endif // canImport(Network) - return bootstrap + return + bootstrap // Enable `SO_REUSEADDR` to avoid "address already in use" error. .serverChannelOption( ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), @@ -158,7 +161,7 @@ public final class Server: @unchecked Sendable { try sync.addHandler(sslHandler) } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) // Configures the pipeline based on whether the connection uses TLS or not. try sync.addHandler(GRPCServerPipelineConfigurator(configuration: configuration)) @@ -169,7 +172,8 @@ public final class Server: @unchecked Sendable { hasTLS: configuration.tlsConfiguration != nil ) if requiresZeroLengthWorkaround, - #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) + { try sync.addHandler(NIOFilterEmptyWritesHandler()) } } catch { @@ -286,7 +290,8 @@ extension Server { set { self .serviceProvidersByName = Dictionary( - uniqueKeysWithValues: newValue + uniqueKeysWithValues: + newValue .map { ($0.serviceName, $0) } ) } @@ -307,7 +312,7 @@ extension Server { self.tlsConfiguration = newValue.map { GRPCTLSConfiguration(transforming: $0) } } } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) public var tlsConfiguration: GRPCTLSConfiguration? @@ -428,7 +433,7 @@ extension Server { self.logger = logger self.debugChannelInitializer = debugChannelInitializer } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) private init( eventLoopGroup: EventLoopGroup, @@ -437,9 +442,11 @@ extension Server { ) { self.eventLoopGroup = eventLoopGroup self.target = target - self.serviceProvidersByName = Dictionary(uniqueKeysWithValues: serviceProviders.map { - ($0.serviceName, $0) - }) + self.serviceProvidersByName = Dictionary( + uniqueKeysWithValues: serviceProviders.map { + ($0.serviceName, $0) + } + ) } /// Make a new configuration using default values. diff --git a/Sources/GRPC/ServerBuilder+NIOSSL.swift b/Sources/GRPC/ServerBuilder+NIOSSL.swift index c71badbde..93dd36da4 100644 --- a/Sources/GRPC/ServerBuilder+NIOSSL.swift +++ b/Sources/GRPC/ServerBuilder+NIOSSL.swift @@ -20,8 +20,10 @@ import NIOSSL extension Server { /// Returns a `Server` builder configured with TLS. @available( - *, deprecated, - message: "Use one of 'usingTLSBackedByNIOSSL(on:certificateChain:privateKey:)', 'usingTLSBackedByNetworkFramework(on:with:)' or 'usingTLS(with:on:)'" + *, + deprecated, + message: + "Use one of 'usingTLSBackedByNIOSSL(on:certificateChain:privateKey:)', 'usingTLSBackedByNetworkFramework(on:with:)' or 'usingTLS(with:on:)'" ) public static func secure( group: EventLoopGroup, @@ -76,4 +78,4 @@ extension Server.Builder.Secure { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift b/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift index a8ededb5e..99a266e2e 100644 --- a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift +++ b/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift @@ -147,7 +147,8 @@ open class StreamingResponseCallContext: ServerCallContextBase /// A concrete implementation of `StreamingResponseCallContext` used internally. @usableFromInline internal final class _StreamingResponseCallContext: - StreamingResponseCallContext { + StreamingResponseCallContext +{ @usableFromInline internal let _sendResponse: (Response, MessageMetadata, EventLoopPromise?) -> Void @@ -239,7 +240,8 @@ internal final class _StreamingResponseCallContext: /// Simply records all sent messages. open class StreamingResponseCallContextTestStub: StreamingResponseCallContext< ResponsePayload -> { +> +{ open var recordedResponses: [ResponsePayload] = [] override open func sendResponse( diff --git a/Sources/GRPC/TLSVersion.swift b/Sources/GRPC/TLSVersion.swift index 9be80c77f..1c1c0c73f 100644 --- a/Sources/GRPC/TLSVersion.swift +++ b/Sources/GRPC/TLSVersion.swift @@ -15,6 +15,7 @@ */ import NIOCore + #if canImport(NIOSSL) import NIOSSL #endif @@ -84,12 +85,12 @@ extension GRPCTLSVersion { case .tlsProtocol13: self = .tlsv13 case .dtlsProtocol1, - .dtlsProtocol12, - .sslProtocol2, - .sslProtocol3, - .sslProtocol3Only, - .sslProtocolAll, - .tlsProtocolMaxSupported: + .dtlsProtocol12, + .sslProtocol2, + .sslProtocol3, + .sslProtocol3Only, + .sslProtocolAll, + .tlsProtocolMaxSupported: return nil @unknown default: return nil diff --git a/Sources/GRPC/UserInfo.swift b/Sources/GRPC/UserInfo.swift index 239ad542a..8917a78ba 100644 --- a/Sources/GRPC/UserInfo.swift +++ b/Sources/GRPC/UserInfo.swift @@ -76,9 +76,10 @@ public struct UserInfo: CustomStringConvertible { } public var description: String { - return "[" + self.storage.map { key, value in - "\(key): \(value)" - }.joined(separator: ", ") + "]" + return "[" + + self.storage.map { key, value in + "\(key): \(value)" + }.joined(separator: ", ") + "]" } /// A `UserInfoKey` wrapper. diff --git a/Sources/GRPC/WebCORSHandler.swift b/Sources/GRPC/WebCORSHandler.swift index 7f586ed24..6b2f546aa 100644 --- a/Sources/GRPC/WebCORSHandler.swift +++ b/Sources/GRPC/WebCORSHandler.swift @@ -55,8 +55,9 @@ extension WebCORSHandler: ChannelInboundHandler { private func receivedRequestHead(context: ChannelHandlerContext, _ head: HTTPRequestHead) { if head.method == .OPTIONS, - head.headers.contains(.accessControlRequestMethod), - let origin = head.headers.first(name: "origin") { + head.headers.contains(.accessControlRequestMethod), + let origin = head.headers.first(name: "origin") + { // If the request is OPTIONS with a access-control-request-method header it's a CORS // preflight request and is not propagated further. self.state = .processingPreflightRequest diff --git a/Sources/GRPC/_EmbeddedThroughput.swift b/Sources/GRPC/_EmbeddedThroughput.swift index f6e4ed827..0efcad3c0 100644 --- a/Sources/GRPC/_EmbeddedThroughput.swift +++ b/Sources/GRPC/_EmbeddedThroughput.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import Logging import NIOCore import NIOEmbedded diff --git a/Sources/GRPC/_FakeResponseStream.swift b/Sources/GRPC/_FakeResponseStream.swift index 5a2f5b282..7e23ba8e4 100644 --- a/Sources/GRPC/_FakeResponseStream.swift +++ b/Sources/GRPC/_FakeResponseStream.swift @@ -137,8 +137,8 @@ public class _FakeResponseStream { return .valid case (.responsePart(.initialMetadata), .sending), - (.responsePart(.initialMetadata), .closing), - (.responsePart(.initialMetadata), .closed): + (.responsePart(.initialMetadata), .closing), + (.responsePart(.initialMetadata), .closed): // We can only send initial metadata from '.idle'. return .invalid(reason: "Initial metadata has already been sent") @@ -152,32 +152,32 @@ public class _FakeResponseStream { return .valid case (.responsePart(.message), .closing), - (.responsePart(.message), .closed): + (.responsePart(.message), .closed): // We can't send messages once we're closing or closed. return .invalid(reason: "Messages can't be sent after the stream has been closed") case (.responsePart(.trailingMetadata), .idle), - (.responsePart(.trailingMetadata), .sending): + (.responsePart(.trailingMetadata), .sending): self.sendState = .closing return .valid case (.responsePart(.trailingMetadata), .closing), - (.responsePart(.trailingMetadata), .closed): + (.responsePart(.trailingMetadata), .closed): // We're already closing or closed. return .invalid(reason: "Trailing metadata can't be sent after the stream has been closed") case (.responsePart(.status), .idle), - (.error, .idle), - (.responsePart(.status), .sending), - (.error, .sending), - (.responsePart(.status), .closed), - (.error, .closed): + (.error, .idle), + (.responsePart(.status), .sending), + (.error, .sending), + (.responsePart(.status), .closed), + (.error, .closed): // We can only error/close if we're closing (i.e. have already sent trailers which we enforce // from the API in the subclasses). return .invalid(reason: "Status/error can only be sent after trailing metadata has been sent") case (.responsePart(.status), .closing), - (.error, .closing): + (.error, .closing): self.sendState = .closed return .valid } diff --git a/Sources/GRPCConnectionBackoffInteropTest/main.swift b/Sources/GRPCConnectionBackoffInteropTest/main.swift index e719dd699..c8124a6bc 100644 --- a/Sources/GRPCConnectionBackoffInteropTest/main.swift +++ b/Sources/GRPCConnectionBackoffInteropTest/main.swift @@ -121,4 +121,4 @@ struct ConnectionBackoffInteropTest: ParsableCommand { } ConnectionBackoffInteropTest.main() -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift b/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift index 1c7dbae1c..18de4e07b 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.Data + import GRPCInteroperabilityTestModels import NIOHPACK import SwiftProtobuf +import struct Foundation.Data + // MARK: - Payload creation extension Grpc_Testing_Payload { @@ -115,7 +117,8 @@ extension Grpc_Testing_StreamingInputCallRequest: PayloadRequest {} extension HPACKHeaders { /// See `ServerFeatures.echoMetadata`. var shouldEchoMetadata: Bool { - return self.contains(name: "x-grpc-test-echo-initial") || self - .contains(name: "x-grpc-test-echo-trailing-bin") + return self.contains(name: "x-grpc-test-echo-initial") + || self + .contains(name: "x-grpc-test-echo-trailing-bin") } } diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift index eab6be6fd..9a33664fd 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift @@ -14,11 +14,12 @@ * limitations under the License. */ import Dispatch -import struct Foundation.Data import GRPC import GRPCInteroperabilityTestModels import NIOHPACK +import struct Foundation.Data + /// This test verifies that implementations support zero-size messages. Ideally, client /// implementations would verify that the request and response were zero bytes serialized, but /// this is generally prohibitive to perform, so is not required. @@ -195,10 +196,14 @@ class ClientCompressedUnary: InteroperabilityTest { // With compression expected and enabled. let options = - CallOptions(messageEncoding: .enabled(.init( - forRequests: .gzip, - decompressionLimit: .absolute(1024 * 1024) - ))) + CallOptions( + messageEncoding: .enabled( + .init( + forRequests: .gzip, + decompressionLimit: .absolute(1024 * 1024) + ) + ) + ) let compressed = client.unaryCall(compressedRequest, callOptions: options) try waitAndAssertEqual(compressed.response.map { $0.payload }, .zeros(count: 314_159)) try waitAndAssertEqual(compressed.status.map { $0.code }, .ok) @@ -269,10 +274,15 @@ class ServerCompressedUnary: InteroperabilityTest { } let options = - CallOptions(messageEncoding: .enabled(.responsesOnly(decompressionLimit: .absolute( - 1024 * - 1024 - )))) + CallOptions( + messageEncoding: .enabled( + .responsesOnly( + decompressionLimit: .absolute( + 1024 * 1024 + ) + ) + ) + ) let compressed = client.unaryCall(compressedRequest, callOptions: options) // We can't verify that the compression bit was set, instead we verify that the encoding header // was sent by the server. This isn't quite the same since as it can still be set but the @@ -438,10 +448,14 @@ class ClientCompressedStreaming: InteroperabilityTest { } let options = - CallOptions(messageEncoding: .enabled(.init( - forRequests: .gzip, - decompressionLimit: .ratio(10) - ))) + CallOptions( + messageEncoding: .enabled( + .init( + forRequests: .gzip, + decompressionLimit: .ratio(10) + ) + ) + ) let streaming = client.streamingInputCall(callOptions: options) streaming.sendMessage(probeRequest, compression: .enabled, promise: nil) streaming.sendMessage(secondMessage, compression: .disabled, promise: nil) @@ -562,10 +576,15 @@ class ServerCompressedStreaming: InteroperabilityTest { } let options = - CallOptions(messageEncoding: .enabled(.responsesOnly(decompressionLimit: .absolute( - 1024 * - 1024 - )))) + CallOptions( + messageEncoding: .enabled( + .responsesOnly( + decompressionLimit: .absolute( + 1024 * 1024 + ) + ) + ) + ) var payloads: [Grpc_Testing_Payload] = [] let rpc = client.streamingOutputCall(request, callOptions: options) { response in payloads.append(response.payload) @@ -752,7 +771,7 @@ class CustomMetadata: InteroperabilityTest { let trailingMetadataValue = Data([0xAB, 0xAB, 0xAB]).base64EncodedString() func checkMetadata(call: SpecificClientCall) throws - where SpecificClientCall: ClientCall { + where SpecificClientCall: ClientCall { let initialName = call.initialMetadata.map { $0[self.initialMetadataName] } try waitAndAssertEqual(initialName, [self.initialMetadataValue]) @@ -830,7 +849,7 @@ class StatusCodeAndMessage: InteroperabilityTest { let expectedMessage = "test status message" func checkStatus(call: SpecificClientCall) throws - where SpecificClientCall: ClientCall { + where SpecificClientCall: ClientCall { try waitAndAssertEqual(call.status.map { $0.code.rawValue }, self.expectedCode) try waitAndAssertEqual(call.status.map { $0.message }, self.expectedMessage) } diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift index 5d5a2cfff..40be07038 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift @@ -15,6 +15,7 @@ */ import GRPC import NIOCore + #if canImport(NIOSSL) import NIOSSL #endif @@ -34,7 +35,7 @@ public func makeInteroperabilityTestClientBuilder( .withTLS(serverHostnameOverride: "foo.test.google.fr") #else fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } else { builder = ClientConnection.insecure(group: group) } diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift index 9afa3dc5c..ddcd24453 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift @@ -51,59 +51,59 @@ public struct InteroperabilityTestCredentials { ) private static let caCertificatePem = """ - -----BEGIN CERTIFICATE----- - MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV - BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX - aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla - Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 - YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT - BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 - +L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu - g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd - Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV - HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau - sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m - oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG - Dfcog5wrJytaQ6UA0wE= - -----END CERTIFICATE----- - """ + -----BEGIN CERTIFICATE----- + MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV + BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX + aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla + Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 + YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT + BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 + +L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu + g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd + Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV + HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau + sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m + oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG + Dfcog5wrJytaQ6UA0wE= + -----END CERTIFICATE----- + """ private static let server1CertificatePem = """ - -----BEGIN CERTIFICATE----- - MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET - MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ - dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx - MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV - BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 - ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco - LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg - zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd - 9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw - CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy - em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G - CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 - hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh - y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 - -----END CERTIFICATE----- - """ + -----BEGIN CERTIFICATE----- + MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET + MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ + dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx + MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV + BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 + ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco + LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg + zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd + 9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw + CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy + em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G + CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 + hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh + y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 + -----END CERTIFICATE----- + """ private static let server1KeyPem = """ - -----BEGIN PRIVATE KEY----- - MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD - M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf - 3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY - AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm - V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY - tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p - dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q - K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR - 81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff - DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd - aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 - ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 - XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe - F98XJ7tIFfJq - -----END PRIVATE KEY----- - """ + -----BEGIN PRIVATE KEY----- + MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD + M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf + 3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY + AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm + V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY + tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p + dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q + K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR + 81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff + DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd + aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 + ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 + XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe + F98XJ7tIFfJq + -----END PRIVATE KEY----- + """ } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift index 63fd6836d..61cc5865c 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift @@ -16,6 +16,7 @@ import GRPC import Logging import NIOCore + #if canImport(NIOSSL) import NIOSSL #endif @@ -56,7 +57,7 @@ public func makeInteroperabilityTestServer( .withTLS(trustRoots: .certificates([caCert])) #else fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } else { builder = Server.insecure(group: eventLoopGroup) } @@ -65,7 +66,8 @@ public func makeInteroperabilityTestServer( builder.withLogger(logger) } - return builder + return + builder .withMessageCompression(.enabled(.init(decompressionLimit: .absolute(1024 * 1024)))) .withServiceProviders(serviceProviders) .bind(host: host, port: port) diff --git a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift index 7b98a29fb..e4a11ab48 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.Data + import GRPC import GRPCInteroperabilityTestModels import NIOCore +import struct Foundation.Data + /// A service provider for the gRPC interoperability test suite. /// /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md#server @@ -168,7 +170,8 @@ public class TestServiceProvider: Grpc_Testing_TestServiceProvider { switch event { case let .message(request): if request.expectCompressed.value, - !context.headers.contains(name: "grpc-encoding") { + !context.headers.contains(name: "grpc-encoding") + { context.responseStatus = GRPCStatus( code: .invalidArgument, message: "Expected compressed request, but 'grpc-encoding' was missing" @@ -179,9 +182,11 @@ public class TestServiceProvider: Grpc_Testing_TestServiceProvider { } case .end: - context.responsePromise.succeed(Grpc_Testing_StreamingInputCallResponse.with { response in - response.aggregatedPayloadSize = numericCast(aggregatePayloadSize) - }) + context.responsePromise.succeed( + Grpc_Testing_StreamingInputCallResponse.with { response in + response.aggregatedPayloadSize = numericCast(aggregatePayloadSize) + } + ) } }) } diff --git a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift index e3369b9f8..5fdf714db 100644 --- a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.Data + import GRPC import Logging import NIOCore @@ -21,6 +21,8 @@ import NIOEmbedded import NIOHPACK import NIOHTTP2 +import struct Foundation.Data + /// Tests the throughput on the client side by firing a unary request through an embedded channel /// and writing back enough gRPC as HTTP/2 frames to get through the state machine. /// @@ -64,15 +66,16 @@ class EmbeddedClientThroughput: Benchmark { let serializedResponse = try response.serializedData() var buffer = ByteBufferAllocator().buffer(capacity: serializedResponse.count + 5) - buffer.writeInteger(UInt8(0)) // compression byte + buffer.writeInteger(UInt8(0)) // compression byte buffer.writeInteger(UInt32(serializedResponse.count)) buffer.writeContiguousBytes(serializedResponse) self.responseDataChunks = [] while buffer.readableBytes > 0, - let slice = buffer.readSlice( - length: min(maximumResponseFrameSize, buffer.readableBytes) - ) { + let slice = buffer.readSlice( + length: min(maximumResponseFrameSize, buffer.readableBytes) + ) + { self.responseDataChunks.append(slice) } } diff --git a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift index fd4748667..17f6bf1bc 100644 --- a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift @@ -47,11 +47,13 @@ final class EmbeddedServerChildChannelBenchmark: Benchmark { } static func makeHeadersPayload(method: String) -> HTTP2Frame.FramePayload { - return .headers(.init(headers: [ - ":path": "/echo.Echo/\(method)", - ":method": "POST", - "content-type": "application/grpc", - ])) + return .headers( + .init(headers: [ + ":path": "/echo.Echo/\(method)", + ":method": "POST", + "content-type": "application/grpc", + ]) + ) } private var headersPayload: HTTP2Frame.FramePayload! @@ -101,8 +103,8 @@ final class EmbeddedServerChildChannelBenchmark: Benchmark { let serialized = try Echo_EchoRequest.with { $0.text = requestText }.serializedData() buffer.reserveCapacity(5 + serialized.count) - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(serialized.count)) // length + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(serialized.count)) // length buffer.writeData(serialized) self.requestPayload = .data(.init(data: .byteBuffer(buffer), endStream: false)) diff --git a/Sources/GRPCPerformanceTests/main.swift b/Sources/GRPCPerformanceTests/main.swift index 76f0fa856..6210245e5 100644 --- a/Sources/GRPCPerformanceTests/main.swift +++ b/Sources/GRPCPerformanceTests/main.swift @@ -18,7 +18,7 @@ import GRPC import Logging let smallRequest = String(repeating: "x", count: 8) -let largeRequest = String(repeating: "x", count: 1 << 16) // 65k +let largeRequest = String(repeating: "x", count: 1 << 16) // 65k // Add benchmarks here! func runBenchmarks(spec: TestSpec) { @@ -264,9 +264,11 @@ struct PerformanceTests: ParsableCommand { } } -assert({ - print("⚠️ WARNING: YOU ARE RUNNING IN DEBUG MODE ⚠️") - return true -}()) +assert( + { + print("⚠️ WARNING: YOU ARE RUNNING IN DEBUG MODE ⚠️") + return true + }() +) PerformanceTests.main() diff --git a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift index 2abcc8f8f..042ffc063 100644 --- a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift +++ b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift @@ -33,49 +33,49 @@ public struct SampleCertificate { public static let ca = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), commonName: "some-ca", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) public static let otherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), commonName: "some-other-ca", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) public static let server = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) public static let exampleServer = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), commonName: "example.com", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) public static let serverSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) public static let client = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) public static let clientSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) public static let exampleServerWithExplicitCurve = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1720088924) + notAfter: Date(timeIntervalSince1970: 1_720_088_924) ) } @@ -105,260 +105,260 @@ public struct SamplePrivateKey { // MARK: - Certificates and private keys private let caCert = """ ------BEGIN CERTIFICATE----- -MIICoDCCAYgCCQCgCA1/0dKfFjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz -b21lLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowEjEQMA4GA1UE -AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTi2aJy -Vw3E0OQwNIm9GZOG4E/Rc0atKoJes9yWaMrMPGwoenLEc2JNIvJSdBGZHKO7HKAG -OnffpqVIXtRBIU7l8HEhX97Q+knI6wPz8O7JaGVf6KznLa2eFO6xGM1pogO7m+/M -0mw8LSftn2IEiJk9v00qj+WgfwJJqL/TUZRoT5M2+u99uiaW7bnI1+1vawo5i7A1 -zfN6SBud5K/BaEYcAjxX1JMWCJLWSuOFZArWX7Je2MP+LqZkjh8kQO+d8ZZaLSIs -ujd6x6/r365Sl24l4auNfWy/5V1Ctfxl4avupAm7CpmEFpswe/ucNHkD0drUCzvt -hBeR3coLXWgbQs0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJm1Yntrrl6WxPbsA -s1DrI9YHdQjUNkouX0PtGp4yKrP7hwTclIhHjlGaQRJ2p1I7hllCMCPDa2YZa714 -XhtvEmpWOeLXMFolpKEn83kccvkQviZ3yd2lKH64jDX1/g2Rf6dXhDZMKrMAkEdx -X3JwZwPxwb8VDtac7TkVgOcQFHRzdX2g6pQXz3eNsjckGNJgzzl/ln6DrHHDbruI -M7bfnc2ZCBcHUCLWts8LnX2ekUq9KOxMe4e3sD27sKPizklNfGH4Rdg4LByhkx3S -GGR3ziWyixfcs4BNhA5mbsvb8vpPdtOh1oFt+TtPxlQ2FQOnSHk6wF285XggYYgv -p8pG5Q== ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICoDCCAYgCCQCgCA1/0dKfFjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz + b21lLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowEjEQMA4GA1UE + AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTi2aJy + Vw3E0OQwNIm9GZOG4E/Rc0atKoJes9yWaMrMPGwoenLEc2JNIvJSdBGZHKO7HKAG + OnffpqVIXtRBIU7l8HEhX97Q+knI6wPz8O7JaGVf6KznLa2eFO6xGM1pogO7m+/M + 0mw8LSftn2IEiJk9v00qj+WgfwJJqL/TUZRoT5M2+u99uiaW7bnI1+1vawo5i7A1 + zfN6SBud5K/BaEYcAjxX1JMWCJLWSuOFZArWX7Je2MP+LqZkjh8kQO+d8ZZaLSIs + ujd6x6/r365Sl24l4auNfWy/5V1Ctfxl4avupAm7CpmEFpswe/ucNHkD0drUCzvt + hBeR3coLXWgbQs0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJm1Yntrrl6WxPbsA + s1DrI9YHdQjUNkouX0PtGp4yKrP7hwTclIhHjlGaQRJ2p1I7hllCMCPDa2YZa714 + XhtvEmpWOeLXMFolpKEn83kccvkQviZ3yd2lKH64jDX1/g2Rf6dXhDZMKrMAkEdx + X3JwZwPxwb8VDtac7TkVgOcQFHRzdX2g6pQXz3eNsjckGNJgzzl/ln6DrHHDbruI + M7bfnc2ZCBcHUCLWts8LnX2ekUq9KOxMe4e3sD27sKPizklNfGH4Rdg4LByhkx3S + GGR3ziWyixfcs4BNhA5mbsvb8vpPdtOh1oFt+TtPxlQ2FQOnSHk6wF285XggYYgv + p8pG5Q== + -----END CERTIFICATE----- + """ private let otherCACert = """ ------BEGIN CERTIFICATE----- -MIICrDCCAZQCCQC3Iplq4Q/2+jANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z -b21lLW90aGVyLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowGDEW -MBQGA1UEAwwNc29tZS1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAOko0E/i9WlJS5eBAfJsQ1Xz5cAse959qRz4LSE2PsRXYIqGD+CHSlxf -K549WPYfCTEJxT4+MCwU9MfyHSTmhYo/MEA6K1jMznZULhYFriLLiGBCB238W0Xo -bEf3EN9xrHlmHaYrN9EwI6Qiq/AYkpAmbrlgbLW5Ig03YWTODS8k4R1nrkB609BC -DBEyzBiCjgzo0xVduTgf6iiEfUg+dlvkeH+4qjLU0DRJq0g7YIM/kEX/zL2YUad5 -9aytkDjO30IhcjQC+wvhCLBn6FDyYOpthaGM1cbMLG3efMpGAtyny2qATo63yVmf -kd8ftmV86BidAm+tCnFwBzxfXd4CB00CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA -Qxth0x5noVZrWZs67kBpjhiNI5Zg4/IMFukL4qv4XqC4AkwJJ4XaMAVTgtZ+mGmr -yOJ6pEzw0C7nWmTtlUjQu32Z+YNLSnE6wcIEx8ed1fwI0kezcyBBrg+Rs1vDNi1c -Tshq0by3RBuuSLclYrR64pmzYj4XJjABYIPurmtBCh4iwVhEe3tYs8I5vlKhmvA3 -ZTnqs21wD0v7FA4aM4EguFfLTMlBuD7U4G+agXvtcV4tXzQSh7RaXB06Mt4mNJ1k -LfqH39ZEnzeqUVm0vn283hvH9RzTYuHZu8J9wtmDrSTb6EcA4kpnILOgjhyLNL5G -EZi+HPA+wJ2bsRVlAxmuMA== ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICrDCCAZQCCQC3Iplq4Q/2+jANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z + b21lLW90aGVyLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowGDEW + MBQGA1UEAwwNc29tZS1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC + AQoCggEBAOko0E/i9WlJS5eBAfJsQ1Xz5cAse959qRz4LSE2PsRXYIqGD+CHSlxf + K549WPYfCTEJxT4+MCwU9MfyHSTmhYo/MEA6K1jMznZULhYFriLLiGBCB238W0Xo + bEf3EN9xrHlmHaYrN9EwI6Qiq/AYkpAmbrlgbLW5Ig03YWTODS8k4R1nrkB609BC + DBEyzBiCjgzo0xVduTgf6iiEfUg+dlvkeH+4qjLU0DRJq0g7YIM/kEX/zL2YUad5 + 9aytkDjO30IhcjQC+wvhCLBn6FDyYOpthaGM1cbMLG3efMpGAtyny2qATo63yVmf + kd8ftmV86BidAm+tCnFwBzxfXd4CB00CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA + Qxth0x5noVZrWZs67kBpjhiNI5Zg4/IMFukL4qv4XqC4AkwJJ4XaMAVTgtZ+mGmr + yOJ6pEzw0C7nWmTtlUjQu32Z+YNLSnE6wcIEx8ed1fwI0kezcyBBrg+Rs1vDNi1c + Tshq0by3RBuuSLclYrR64pmzYj4XJjABYIPurmtBCh4iwVhEe3tYs8I5vlKhmvA3 + ZTnqs21wD0v7FA4aM4EguFfLTMlBuD7U4G+agXvtcV4tXzQSh7RaXB06Mt4mNJ1k + LfqH39ZEnzeqUVm0vn283hvH9RzTYuHZu8J9wtmDrSTb6EcA4kpnILOgjhyLNL5G + EZi+HPA+wJ2bsRVlAxmuMA== + -----END CERTIFICATE----- + """ private let serverCert = """ ------BEGIN CERTIFICATE----- -MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMMCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuWLHyxPM/F -sviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv3x6sdRdT -0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAkbWL6X4RG -1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb0GM1yvBa -j88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6ZQ2x3H6cS -cTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU4P+ZzHEw -r/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGpBsuzx72mOBa9o7m1eNh2cY -H6MrNi1b6vTaA3SOH68RDxg2qx6UrKxI34/No7FaOzRrfs9vUaKXHwwBnDxMskH5 -iTmVAGegumDQE3Bd11j+v1tKxXWS/bvWH7tfK6taoex76ktR3L8qO+Hp8n4YKuSb -qJScIhMPIg7fWPonLvcszGFPdBIxU3YkAZJZFeom/s1WhWCYXsJZSYOXv4YRlaU5 -ozeV3v9icDptaxNY7n4U6C32eykMjowJJ9dcOD+ib3PF88S+utmZnSEGYu+5bnXy -6MGWZcYH1wQ0RpNC+YzjQcGsKwHfaoBS4WFEK2fJdRfX4owZOu6HO1zhyoLpqw== ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe + Fw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMMCWxvY2Fs + aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuWLHyxPM/F + sviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv3x6sdRdT + 0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAkbWL6X4RG + 1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb0GM1yvBa + j88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6ZQ2x3H6cS + cTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU4P+ZzHEw + r/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGpBsuzx72mOBa9o7m1eNh2cY + H6MrNi1b6vTaA3SOH68RDxg2qx6UrKxI34/No7FaOzRrfs9vUaKXHwwBnDxMskH5 + iTmVAGegumDQE3Bd11j+v1tKxXWS/bvWH7tfK6taoex76ktR3L8qO+Hp8n4YKuSb + qJScIhMPIg7fWPonLvcszGFPdBIxU3YkAZJZFeom/s1WhWCYXsJZSYOXv4YRlaU5 + ozeV3v9icDptaxNY7n4U6C32eykMjowJJ9dcOD+ib3PF88S+utmZnSEGYu+5bnXy + 6MGWZcYH1wQ0RpNC+YzjQcGsKwHfaoBS4WFEK2fJdRfX4owZOu6HO1zhyoLpqw== + -----END CERTIFICATE----- + """ private let serverSignedByOtherCACert = """ ------BEGIN CERTIFICATE----- -MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl -ci1jYTAeFw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMM -CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuW -LHyxPM/FsviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv -3x6sdRdT0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAk -bWL6X4RG1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb -0GM1yvBaj88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6Z -Q2x3H6cScTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU -4P+ZzHEwr/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY3vY+hng2gLh9t8q -/fvZewBiLAjsePbgRGT/xO4zCi3JwbHt07oGyQfo63ok5IJIrj3MPVy7N/oGJF0Y -niQrIhXs0NCKEZ/P9amh6wZJKAOtfD9t3oiNWTx56shm1vFQTTUdpykK0b37jGiK -y0N0p8M27ym/gQGTixfHNBtA0p+rmdDErOHqfU5Px3iQfmMmf4hxXPOSkGMixyre -3AR6wURMGLUCLVxi0sQYNd4fGo/GwbswTSJI7+sypZHMwpXbaN7KjorkSmI8UuoY -aGEewReM008rQWGWf3ybmNCChhru82lPQGMp6y9fN0s591iIzjpCXixzd1j1V4oY -yXRecw== ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl + ci1jYTAeFw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMM + CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuW + LHyxPM/FsviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv + 3x6sdRdT0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAk + bWL6X4RG1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb + 0GM1yvBaj88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6Z + Q2x3H6cScTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU + 4P+ZzHEwr/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY3vY+hng2gLh9t8q + /fvZewBiLAjsePbgRGT/xO4zCi3JwbHt07oGyQfo63ok5IJIrj3MPVy7N/oGJF0Y + niQrIhXs0NCKEZ/P9amh6wZJKAOtfD9t3oiNWTx56shm1vFQTTUdpykK0b37jGiK + y0N0p8M27ym/gQGTixfHNBtA0p+rmdDErOHqfU5Px3iQfmMmf4hxXPOSkGMixyre + 3AR6wURMGLUCLVxi0sQYNd4fGo/GwbswTSJI7+sypZHMwpXbaN7KjorkSmI8UuoY + aGEewReM008rQWGWf3ybmNCChhru82lPQGMp6y9fN0s591iIzjpCXixzd1j1V4oY + yXRecw== + -----END CERTIFICATE----- + """ private let serverKey = """ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEApNl3O5YsfLE8z8Wy+IEFelLOXYzEnEOSaKvwNJ1c7kTWwcID -KZNrHL1s52LVdRltFPIp9O/fHqx1F1PQazZaOEvTIVP0gRWsZkAO3cYhigdhlTnp -if+cuCUZBmoh6jffDI8QgCRtYvpfhEbUdzqclDGHDVppW2xg0ChaMBXM/vvdNmDJ -FywaWKoe1ikPSrRv4R+Y+RvQYzXK8FqPzx1ZYUWUbTRpkVBq1KQis9uRcbQy7j/V -TFgMS7E5isgWQUx9HxXyHplDbHcfpxJxMnZOkiXDes7oTQn8dhpWGa8ml68g9CG8 -f9D68zVuS9oiRyQtyFbtWBTg/5nMcTCv/ExyowIDAQABAoIBAAq5FpdqqlQmF0WQ -n5aoldmiH0hYisV7Y7+pR4O0pMHe+nU6EIiYzUPeUoIunKH0WHMfWXlUTRgqsacl -zY3byDyXOhGV63amGUPBcPYeGDppRoC1dqqCVQhpaVpQdwpMPhcMC0+6jt78WFA7 -Z0CmMF83ZYiJ1AadYyLHLS6pjF8dmkj/Rd6yeLIVkKr4xHxou7au/6WKorop5XLM -fEyWC2iotha2dkXw3i324n0qrbR2v/EYLnAn75uA9FF/pJWe6iPc6H5tfBSnzmO6 -fkZ2rCrDt4ANabg6WMmRdrZXFHSR/JlPPyh4T4iJGenkLltKZG+wWSm2nVXE0DYt -JQdmhiECgYEAz3EclGIrk63Hp/2mAHAOIOUGh6+Tk4JA+ibHSzziVZZqJsGQ9jcK -eOn6TX5674+aNzo2ROnHCT6u2tCQEl5/lrB8YpYh3F6aaNqPvFwqRhDViOw2l8KL -Ic40x19o5ur3Ss914htwTxiEzQVB/n+5zhE7W4N/RaDIT2hedWR19PECgYEAy3AF -CiHa6P+pbhskoSETtxbWkhDENpXat2dlFRDrNN9T2NZNVmIxCjAE52arduCxaLTP -hazyq4d7FZ4OkxfbJY9D2HnBS6mF0RHB0gZXZ7iB/uEr0KcTex5saqX9TF3YA5Wj -PNVtOM37IIaLJ1qOmfXf4yL3EVlI30eNwfoMkNMCgYAv0VAYOET5Rs7GP6b7ZNks -5f5KWsO29giKYVQBWOiHeCPCCU6kIu3sD2teX7Bw9nZDEs0dt5Hk5Kkj0X3UbioV -D1us0hS+GqSXVQJbFhe8jPbcGC9BblvqEAGEj867pCAbA5WV6GNMKEe8huC+jKzE -/p3jK320DCsAevuDLgQu0QKBgHvE60v+zPB0muAiI2bkeNorSuAS001iXm62uQjY -AkFondqOhv7HPo60KEegbzEkAstxNdBeKEWzZ27/el6DZRC02NIbQT6HJKLN6t2c -fhDccDphRAbtnyyIle1Mj46miYWkxGt+bbThnKdtM7v9nESPEmdeHnKvn2Y4YkZh -msOBAoGAaarkv8JjjmIgjRZrJ7r4dkzZwZa/msm+/NHr3nlXK227ExMeFRPmzYls -zIofM+DoEk1sDXRfnv+8EU8Dn1DYSq6M6W8xrm7Ulpzj0kXE4f9TD+MUwSNCQ6Gg -zLRkHQBKblIa0lEvlulLtJT2UN9AnCmvTH2R11wD87DWjFDZKD8= ------END RSA PRIVATE KEY----- -""" + -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEApNl3O5YsfLE8z8Wy+IEFelLOXYzEnEOSaKvwNJ1c7kTWwcID + KZNrHL1s52LVdRltFPIp9O/fHqx1F1PQazZaOEvTIVP0gRWsZkAO3cYhigdhlTnp + if+cuCUZBmoh6jffDI8QgCRtYvpfhEbUdzqclDGHDVppW2xg0ChaMBXM/vvdNmDJ + FywaWKoe1ikPSrRv4R+Y+RvQYzXK8FqPzx1ZYUWUbTRpkVBq1KQis9uRcbQy7j/V + TFgMS7E5isgWQUx9HxXyHplDbHcfpxJxMnZOkiXDes7oTQn8dhpWGa8ml68g9CG8 + f9D68zVuS9oiRyQtyFbtWBTg/5nMcTCv/ExyowIDAQABAoIBAAq5FpdqqlQmF0WQ + n5aoldmiH0hYisV7Y7+pR4O0pMHe+nU6EIiYzUPeUoIunKH0WHMfWXlUTRgqsacl + zY3byDyXOhGV63amGUPBcPYeGDppRoC1dqqCVQhpaVpQdwpMPhcMC0+6jt78WFA7 + Z0CmMF83ZYiJ1AadYyLHLS6pjF8dmkj/Rd6yeLIVkKr4xHxou7au/6WKorop5XLM + fEyWC2iotha2dkXw3i324n0qrbR2v/EYLnAn75uA9FF/pJWe6iPc6H5tfBSnzmO6 + fkZ2rCrDt4ANabg6WMmRdrZXFHSR/JlPPyh4T4iJGenkLltKZG+wWSm2nVXE0DYt + JQdmhiECgYEAz3EclGIrk63Hp/2mAHAOIOUGh6+Tk4JA+ibHSzziVZZqJsGQ9jcK + eOn6TX5674+aNzo2ROnHCT6u2tCQEl5/lrB8YpYh3F6aaNqPvFwqRhDViOw2l8KL + Ic40x19o5ur3Ss914htwTxiEzQVB/n+5zhE7W4N/RaDIT2hedWR19PECgYEAy3AF + CiHa6P+pbhskoSETtxbWkhDENpXat2dlFRDrNN9T2NZNVmIxCjAE52arduCxaLTP + hazyq4d7FZ4OkxfbJY9D2HnBS6mF0RHB0gZXZ7iB/uEr0KcTex5saqX9TF3YA5Wj + PNVtOM37IIaLJ1qOmfXf4yL3EVlI30eNwfoMkNMCgYAv0VAYOET5Rs7GP6b7ZNks + 5f5KWsO29giKYVQBWOiHeCPCCU6kIu3sD2teX7Bw9nZDEs0dt5Hk5Kkj0X3UbioV + D1us0hS+GqSXVQJbFhe8jPbcGC9BblvqEAGEj867pCAbA5WV6GNMKEe8huC+jKzE + /p3jK320DCsAevuDLgQu0QKBgHvE60v+zPB0muAiI2bkeNorSuAS001iXm62uQjY + AkFondqOhv7HPo60KEegbzEkAstxNdBeKEWzZ27/el6DZRC02NIbQT6HJKLN6t2c + fhDccDphRAbtnyyIle1Mj46miYWkxGt+bbThnKdtM7v9nESPEmdeHnKvn2Y4YkZh + msOBAoGAaarkv8JjjmIgjRZrJ7r4dkzZwZa/msm+/NHr3nlXK227ExMeFRPmzYls + zIofM+DoEk1sDXRfnv+8EU8Dn1DYSq6M6W8xrm7Ulpzj0kXE4f9TD+MUwSNCQ6Gg + zLRkHQBKblIa0lEvlulLtJT2UN9AnCmvTH2R11wD87DWjFDZKD8= + -----END RSA PRIVATE KEY----- + """ private let exampleServerCert = """ ------BEGIN CERTIFICATE----- -MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNVBAMMC2V4YW1w -bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0yKJAjr3evjW -69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+95XROgElY3DsuARH5wuksTi+mek5 -J5MObbUhHtGIaoqVaDew6TokawwQyBwngKudssu9/6rq38m33OSEv/5oc6xtTdKJ -Lrmqqf664+QajxCeec9CPMGJExQ25c1A5QOkkyC5xR+TcRRIcKPaDZ9aj6JlcD58 -QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+qNV2Pl5DZL8ujEX8XCf8EyHc5GNYN -5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9JyM+wu/a0iMVBr4Yk73VVhOaH+aUI -eNHaD7fhlQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB1++0U/7RduytrWh2bu6uz -sFk4XK+5eIhKn4DMq6vKXFQYF94Tkz8K2RDGzZ3Cl8qRU7dLwlHrUgqFI89XMFAM -LjumIWoMnfik8A6cBmp/HqURzXPNv6Wgn4MtU7aDs8WAEsGYAo5TTtqVJUGc2Mlf -NkW3MQ/RTfUncamx2wNFjwLmGTuERgHA/OA8WQVnMDI5JLXH5sigdOMTkqgkGzhg -8NVWnqubG4b4a7W3xl4s2FjqglqXP3vu+c1F6cWJfKgOXIqd8NduJ+p2FJZ1rW2c -3jkHoqBLqA4/zua+HUn5ICcUZrZid7HgmlUoR/4n+dbjT3Jdpp4BpNn3q8JuWE4Q ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe + Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNVBAMMC2V4YW1w + bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0yKJAjr3evjW + 69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+95XROgElY3DsuARH5wuksTi+mek5 + J5MObbUhHtGIaoqVaDew6TokawwQyBwngKudssu9/6rq38m33OSEv/5oc6xtTdKJ + Lrmqqf664+QajxCeec9CPMGJExQ25c1A5QOkkyC5xR+TcRRIcKPaDZ9aj6JlcD58 + QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+qNV2Pl5DZL8ujEX8XCf8EyHc5GNYN + 5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9JyM+wu/a0iMVBr4Yk73VVhOaH+aUI + eNHaD7fhlQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB1++0U/7RduytrWh2bu6uz + sFk4XK+5eIhKn4DMq6vKXFQYF94Tkz8K2RDGzZ3Cl8qRU7dLwlHrUgqFI89XMFAM + LjumIWoMnfik8A6cBmp/HqURzXPNv6Wgn4MtU7aDs8WAEsGYAo5TTtqVJUGc2Mlf + NkW3MQ/RTfUncamx2wNFjwLmGTuERgHA/OA8WQVnMDI5JLXH5sigdOMTkqgkGzhg + 8NVWnqubG4b4a7W3xl4s2FjqglqXP3vu+c1F6cWJfKgOXIqd8NduJ+p2FJZ1rW2c + 3jkHoqBLqA4/zua+HUn5ICcUZrZid7HgmlUoR/4n+dbjT3Jdpp4BpNn3q8JuWE4Q + -----END CERTIFICATE----- + """ private let exampleServerKey = """ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA0yKJAjr3evjW69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+ -95XROgElY3DsuARH5wuksTi+mek5J5MObbUhHtGIaoqVaDew6TokawwQyBwngKud -ssu9/6rq38m33OSEv/5oc6xtTdKJLrmqqf664+QajxCeec9CPMGJExQ25c1A5QOk -kyC5xR+TcRRIcKPaDZ9aj6JlcD58QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+q -NV2Pl5DZL8ujEX8XCf8EyHc5GNYN5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9J -yM+wu/a0iMVBr4Yk73VVhOaH+aUIeNHaD7fhlQIDAQABAoIBAElHYToO8ToTB7US -HjHTLRvGupna8n+9CL3Hs/X9eG2nCAcZ84tGyjlRkIJ0/RPZGIOOPPjtcunEUnvz -xDw7c2VY3/nqY3Sqb5JjBaTJqUFq1CMKbU9S+3yy+5X0UwYtog1o5SPQopcyDT7U -XfFmYcMatkUVRYuNbbXcjhC7IVqcQpPzPrBaGJ/cm8ZCdTJIWCrJfsI2mgDA9d+B -k5c5uQxhohPWFdZsGGTdJgRCJww1mlAHJXxR06hkBJEnG/vmRaCvsjBgrOtP0/iE -y8NBETAYMl1Ms+w9Pv+pcE8eHgAvicNkKiOlvLTeval1ZIV236IiGlvls808SaYb -RCTlN7kCgYEA+/sFtAMj/ZPacAcuv1mTFKaYUFwbeZAZGHVyniwfgRRvPDBtqQ9i -vGWOLL2fLqbp00sy9LS0FNo8OSnJSGux2IEmoHdNWDNzyORkXLhB9Lov5VqKj6V3 -PO6JswgPtNrb+3fQulAq7YkU6qPdrtHGdU+to/lHQrD6FkMLdKz/zdMCgYEA1oC3 -nOv/PsUwNWlUzkxfgQ0Pnv32NKfoFu2cWL9C0FlCgQxakm03JrbcfjEQafPR64jE -7uhe7aueiQz339jlMxyJk5BFNn/nIOBmLUFD3wV7xcsU1mRnbl/a4R1Wxk7vwmW1 -s4LRu35iiWb/UkblsTA/qdMcigRlRPkOg3TYqfcCgYBWgWgE06swa+jq2txmnrbK -uSLDO8vG4PxslC2ENbufEcfaTvnmtzx7VxYHMBYM6wqNGlzk+4BzRDS2nyzV6vsE -S9pZ7nskE43lYts9pZgnDyBQSdQV2oVj6rRlPRg/S3+IBisnO0xxfcUrhJQfZy8N -qQwAphybvawtplixdo7fNwKBgQCEvxfipzJJOGNDSrJPEXixNtIKBQUPRTIermHp -kkPZCMRddLXAlJJjBRujhN2xlFC/QN8PMwM8ds8f5cSo5WPCo9CIX+pVdgYllHnn -W9KS/KPCnpGAtJZF+lBMrIl9JHDAj41JUJZXQDne6rzrwDB53XAouxuYVmwNqUxQ -EknbtQKBgQCylI9b7Syz5pBzZZNBSn4eqzlsdYgG3WUMGyEyvVzTjfbovWK98xeE -A5F8BNesnoopCW9MtW2QJ9iSLn24sNpj0Uvw9pobB3uDj5Jn0oIndGe7N/7beYhE -HjkJ4liJkM5Q1oUOvVFFPJWEm99cP8urojakeMO3la3tGaElwHVWWQ== ------END RSA PRIVATE KEY----- -""" + -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEA0yKJAjr3evjW69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+ + 95XROgElY3DsuARH5wuksTi+mek5J5MObbUhHtGIaoqVaDew6TokawwQyBwngKud + ssu9/6rq38m33OSEv/5oc6xtTdKJLrmqqf664+QajxCeec9CPMGJExQ25c1A5QOk + kyC5xR+TcRRIcKPaDZ9aj6JlcD58QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+q + NV2Pl5DZL8ujEX8XCf8EyHc5GNYN5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9J + yM+wu/a0iMVBr4Yk73VVhOaH+aUIeNHaD7fhlQIDAQABAoIBAElHYToO8ToTB7US + HjHTLRvGupna8n+9CL3Hs/X9eG2nCAcZ84tGyjlRkIJ0/RPZGIOOPPjtcunEUnvz + xDw7c2VY3/nqY3Sqb5JjBaTJqUFq1CMKbU9S+3yy+5X0UwYtog1o5SPQopcyDT7U + XfFmYcMatkUVRYuNbbXcjhC7IVqcQpPzPrBaGJ/cm8ZCdTJIWCrJfsI2mgDA9d+B + k5c5uQxhohPWFdZsGGTdJgRCJww1mlAHJXxR06hkBJEnG/vmRaCvsjBgrOtP0/iE + y8NBETAYMl1Ms+w9Pv+pcE8eHgAvicNkKiOlvLTeval1ZIV236IiGlvls808SaYb + RCTlN7kCgYEA+/sFtAMj/ZPacAcuv1mTFKaYUFwbeZAZGHVyniwfgRRvPDBtqQ9i + vGWOLL2fLqbp00sy9LS0FNo8OSnJSGux2IEmoHdNWDNzyORkXLhB9Lov5VqKj6V3 + PO6JswgPtNrb+3fQulAq7YkU6qPdrtHGdU+to/lHQrD6FkMLdKz/zdMCgYEA1oC3 + nOv/PsUwNWlUzkxfgQ0Pnv32NKfoFu2cWL9C0FlCgQxakm03JrbcfjEQafPR64jE + 7uhe7aueiQz339jlMxyJk5BFNn/nIOBmLUFD3wV7xcsU1mRnbl/a4R1Wxk7vwmW1 + s4LRu35iiWb/UkblsTA/qdMcigRlRPkOg3TYqfcCgYBWgWgE06swa+jq2txmnrbK + uSLDO8vG4PxslC2ENbufEcfaTvnmtzx7VxYHMBYM6wqNGlzk+4BzRDS2nyzV6vsE + S9pZ7nskE43lYts9pZgnDyBQSdQV2oVj6rRlPRg/S3+IBisnO0xxfcUrhJQfZy8N + qQwAphybvawtplixdo7fNwKBgQCEvxfipzJJOGNDSrJPEXixNtIKBQUPRTIermHp + kkPZCMRddLXAlJJjBRujhN2xlFC/QN8PMwM8ds8f5cSo5WPCo9CIX+pVdgYllHnn + W9KS/KPCnpGAtJZF+lBMrIl9JHDAj41JUJZXQDne6rzrwDB53XAouxuYVmwNqUxQ + EknbtQKBgQCylI9b7Syz5pBzZZNBSn4eqzlsdYgG3WUMGyEyvVzTjfbovWK98xeE + A5F8BNesnoopCW9MtW2QJ9iSLn24sNpj0Uvw9pobB3uDj5Jn0oIndGe7N/7beYhE + HjkJ4liJkM5Q1oUOvVFFPJWEm99cP8urojakeMO3la3tGaElwHVWWQ== + -----END RSA PRIVATE KEY----- + """ private let clientCert = """ ------BEGIN CERTIFICATE----- -MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe -Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMMCWxvY2Fs -aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr8dF1Nzg/ -gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjPtPJxRSgj -qUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o9qVe7uQr -bSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK8hUfMq5K -PGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+B64HeKX9 -xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p4q/NHRvO -ADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsjJ+nFYDH31PmM9YpKGuytOw -DQYVLFYWIGybZC7FSESzlGx1GOZ5nY1AWj7gCnAvc7/Ct3efI/7qA0tSB+rbd2tA -/qqU0/0FJLZDyiXrpNzhFoYkg2VzZRSmFdsJbhzjNJM7iJRsWEhXn/7qydLyp1vj -i7DlYQVI2QgEQQz7BMJ6D3zPRxlyDzlVjr7l54M8RX9Dj8Oj9Sajd0RlkLiGW7YR -TC2nNebpRGN57Hi8dCM3xLWQcJ0N7BK0A67MnQaRbUQ0DMvxXO0+HUHxpfN39P/H -6Y81QkFAeeCMCsSWTGHspIJ8teKk+KmIe3xZ72taWNge1Cu3xas7Zsl1lbI2mw== ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe + Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMMCWxvY2Fs + aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr8dF1Nzg/ + gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjPtPJxRSgj + qUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o9qVe7uQr + bSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK8hUfMq5K + PGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+B64HeKX9 + xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p4q/NHRvO + ADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsjJ+nFYDH31PmM9YpKGuytOw + DQYVLFYWIGybZC7FSESzlGx1GOZ5nY1AWj7gCnAvc7/Ct3efI/7qA0tSB+rbd2tA + /qqU0/0FJLZDyiXrpNzhFoYkg2VzZRSmFdsJbhzjNJM7iJRsWEhXn/7qydLyp1vj + i7DlYQVI2QgEQQz7BMJ6D3zPRxlyDzlVjr7l54M8RX9Dj8Oj9Sajd0RlkLiGW7YR + TC2nNebpRGN57Hi8dCM3xLWQcJ0N7BK0A67MnQaRbUQ0DMvxXO0+HUHxpfN39P/H + 6Y81QkFAeeCMCsSWTGHspIJ8teKk+KmIe3xZ72taWNge1Cu3xas7Zsl1lbI2mw== + -----END CERTIFICATE----- + """ private let clientSignedByOtherCACert = """ ------BEGIN CERTIFICATE----- -MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl -ci1jYTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMM -CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr -8dF1Nzg/gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjP -tPJxRSgjqUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o -9qVe7uQrbSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK -8hUfMq5KPGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+ -B64HeKX9xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p -4q/NHRvOADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAiEKKo8JIG29d16ZT -6d4ERj/o/3B2rwpTvSxmVaon3Zzz0gQ+HhuEH9D3XzzZ//P7qe8PxpcZ75veuv7X -ZcIPK+L7QqLAR/RrbWSbhI8CpQ0WX2MitKkz+cdRCey8/4JF4g8PXMMuFrP6fGEm -79l4aJoAiTNJ98qufUzD63kqU+kpPGjML6rnJFfwTVAWu/7Sy92u052IsoZfiKx0 -yN1vYr9jLD48n26YsyVjuuqqMW+OKxzRGA3xCa02W3cILQb0NVv4hM0+yGd1laKe -1zGHzuaeCIL9bFGBtxRXTWyyEG9z5nohEz/waHpUHg5VcbrkLOIIAhsolLuDKQyl -JanCRA== ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl + ci1jYTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMM + CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr + 8dF1Nzg/gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjP + tPJxRSgjqUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o + 9qVe7uQrbSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK + 8hUfMq5KPGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+ + B64HeKX9xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p + 4q/NHRvOADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAiEKKo8JIG29d16ZT + 6d4ERj/o/3B2rwpTvSxmVaon3Zzz0gQ+HhuEH9D3XzzZ//P7qe8PxpcZ75veuv7X + ZcIPK+L7QqLAR/RrbWSbhI8CpQ0WX2MitKkz+cdRCey8/4JF4g8PXMMuFrP6fGEm + 79l4aJoAiTNJ98qufUzD63kqU+kpPGjML6rnJFfwTVAWu/7Sy92u052IsoZfiKx0 + yN1vYr9jLD48n26YsyVjuuqqMW+OKxzRGA3xCa02W3cILQb0NVv4hM0+yGd1laKe + 1zGHzuaeCIL9bFGBtxRXTWyyEG9z5nohEz/waHpUHg5VcbrkLOIIAhsolLuDKQyl + JanCRA== + -----END CERTIFICATE----- + """ private let clientKey = """ ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA4/wm1Wvx0XU3OD+B6xTlTTpiSy467nFKmG/cYS2whj2Lj2up -OTq3jPVo0mAEb7Z88qoIyM+08nFFKCOpSGjFJLXkCnrJ4M31UkYZvZMfWMBgUeZV -vqcHqAN1h2f2Kxg76Vohnaj2pV7u5CttJYfGvhmo6VUYzmSIFMgijJAlicTERnqf -PqeoFWPpmRnc3DD3NFDwCMryFR8yrko8aiFOoC8i7c1TBTXYB4+2d5vfUWlzUMpY -zdl4LyQ0eF5m64iUGGsGYn4Hrgd4pf3F+XXnMj3OIMtLMxr0gvplX7dzogGJN4Zq -9/lyMc7Vq2RAhf6MJFDBv6nir80dG84AOEBd6wIDAQABAoIBAGOV2hSxoSCAVg2Q -2BwqtXrFfPggCofrHs11V0tvnMMWkSalvXaNKm49KHt0i5uMmAmbslidOgoI5k+B -PEmv0iWV+jWFqzcyX+1/R3Eimbe32JsNxPiRl2uRjz4FcGckn87vmu12R762uB0c -xwF0zKBvLvQ1Qq+tBDAnt8e0k2EYqgl7LEIb/1vDsxyVLNLzpSBfENYbE24YD0rI -/PQwot0UJhWVFnYxYczbQSLxhep1tRzhLaUY3k1SrvGvG/TM389TK5upJoV1/emc -EgRCnBPM2geOq59Bul0ri9bvXyVh19VegNM8MLwdf9/UtDFEhApYyoePve96LspM -aOQSe2ECgYEA+NkCXElSy+TdPLH4944PGdXNm6GtuhlRjMc3pz+r/5gQ9J68rIRt -OJewx6bQx8lOFYxzhiTbN2wY7ylm9/Cn6jk7zfUXrqq73tbWgS+YSWajoPZM4sDB -QdQuOkArdLGkHMM5+2U81d/D7o5nb4/ACCY1XtvrNUqgBe6tyHe/INECgYEA6omj -GVY/yYK1RsRNrTmYipZbnR0UPpTsVevdAoX9TrHG5AHTgmh/ONGGCx3/dgmLqZO+ -D4MZSGyK1appxJjpsKzkM5T7X+bznHWzXe6kGnzDzkgBzx4N4s3LHPZ4uUvNnZwi -h281KEBsblKOu4khDk8jL3LXjRxGYwOjqdisYfsCgYEA9ho4OWjSl486NYKVhM5b -pONLunUFSR0tB5smMSPJSLftXN94HO3CzstGK82QgWVW8fy7a5kbrA4eArjheqfo -iL4dpSyVRUrZDiNOdOjLJRx7Cv9LPp3/AsmDBlzcHUZp1YBF4ZhXt/Ta4xy2syBp -fCW9dpjsXwH0jKll+PJkdWECgYAjgDHv495D4kUOMSiQz+cHEztKzNwDnQco+kq5 -1w5Amyg/2wbo9mhLcWuYwzGn7En3oSVjs7RgAg4ByYm4+GxnEcR5ClQCcDLvu+Eq -lrTATaJV1xBvCV2QtxXHjIc5hP/am4eeeHbTYO0IxfZU7KzUPaZVyExYT69XzXU4 -gFOXgQKBgFbrLumJh27/nHtvUM0xh7RZ61NplRXDLez8DinSlNI9ZKl197LKdBzB -6cHi59SojJFJY6QAdqdtenj33KHKjgdc3rH1VvipytPBJRO2qohBpYuSZiY2y+Df -dW493Y3+mwD6VsGFFvBPSC3jhDBeIYxajEJChzkbClVDRS0muLQv ------END RSA PRIVATE KEY----- -""" + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA4/wm1Wvx0XU3OD+B6xTlTTpiSy467nFKmG/cYS2whj2Lj2up + OTq3jPVo0mAEb7Z88qoIyM+08nFFKCOpSGjFJLXkCnrJ4M31UkYZvZMfWMBgUeZV + vqcHqAN1h2f2Kxg76Vohnaj2pV7u5CttJYfGvhmo6VUYzmSIFMgijJAlicTERnqf + PqeoFWPpmRnc3DD3NFDwCMryFR8yrko8aiFOoC8i7c1TBTXYB4+2d5vfUWlzUMpY + zdl4LyQ0eF5m64iUGGsGYn4Hrgd4pf3F+XXnMj3OIMtLMxr0gvplX7dzogGJN4Zq + 9/lyMc7Vq2RAhf6MJFDBv6nir80dG84AOEBd6wIDAQABAoIBAGOV2hSxoSCAVg2Q + 2BwqtXrFfPggCofrHs11V0tvnMMWkSalvXaNKm49KHt0i5uMmAmbslidOgoI5k+B + PEmv0iWV+jWFqzcyX+1/R3Eimbe32JsNxPiRl2uRjz4FcGckn87vmu12R762uB0c + xwF0zKBvLvQ1Qq+tBDAnt8e0k2EYqgl7LEIb/1vDsxyVLNLzpSBfENYbE24YD0rI + /PQwot0UJhWVFnYxYczbQSLxhep1tRzhLaUY3k1SrvGvG/TM389TK5upJoV1/emc + EgRCnBPM2geOq59Bul0ri9bvXyVh19VegNM8MLwdf9/UtDFEhApYyoePve96LspM + aOQSe2ECgYEA+NkCXElSy+TdPLH4944PGdXNm6GtuhlRjMc3pz+r/5gQ9J68rIRt + OJewx6bQx8lOFYxzhiTbN2wY7ylm9/Cn6jk7zfUXrqq73tbWgS+YSWajoPZM4sDB + QdQuOkArdLGkHMM5+2U81d/D7o5nb4/ACCY1XtvrNUqgBe6tyHe/INECgYEA6omj + GVY/yYK1RsRNrTmYipZbnR0UPpTsVevdAoX9TrHG5AHTgmh/ONGGCx3/dgmLqZO+ + D4MZSGyK1appxJjpsKzkM5T7X+bznHWzXe6kGnzDzkgBzx4N4s3LHPZ4uUvNnZwi + h281KEBsblKOu4khDk8jL3LXjRxGYwOjqdisYfsCgYEA9ho4OWjSl486NYKVhM5b + pONLunUFSR0tB5smMSPJSLftXN94HO3CzstGK82QgWVW8fy7a5kbrA4eArjheqfo + iL4dpSyVRUrZDiNOdOjLJRx7Cv9LPp3/AsmDBlzcHUZp1YBF4ZhXt/Ta4xy2syBp + fCW9dpjsXwH0jKll+PJkdWECgYAjgDHv495D4kUOMSiQz+cHEztKzNwDnQco+kq5 + 1w5Amyg/2wbo9mhLcWuYwzGn7En3oSVjs7RgAg4ByYm4+GxnEcR5ClQCcDLvu+Eq + lrTATaJV1xBvCV2QtxXHjIc5hP/am4eeeHbTYO0IxfZU7KzUPaZVyExYT69XzXU4 + gFOXgQKBgFbrLumJh27/nHtvUM0xh7RZ61NplRXDLez8DinSlNI9ZKl197LKdBzB + 6cHi59SojJFJY6QAdqdtenj33KHKjgdc3rH1VvipytPBJRO2qohBpYuSZiY2y+Df + dW493Y3+mwD6VsGFFvBPSC3jhDBeIYxajEJChzkbClVDRS0muLQv + -----END RSA PRIVATE KEY----- + """ private let serverExplicitCurveCert = """ ------BEGIN CERTIFICATE----- -MIICEDCCAbYCCQC7a34VXIF7+DAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt -cGxlLmNvbTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNV -BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B -AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB -AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT -sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl -Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo -N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABN5Q -sDW36YI12PFC/kRnACzCt8a5lqjaFu6QNl0Y0ZYaiE9MdR+EOGcCfoSGf9r8n1Yl -peOOLlvsXQ0UO8WJbsYwCgYIKoZIzj0EAwIDSAAwRQIgGd0bh4HWEd3ytsCEGaw0 -m567URfCk1u6sY4I77U64zQCIQD5hOn0PDS4eYR+kBB5MadQtcBtz8gjtW/OJcfV -D1NSHw== ------END CERTIFICATE----- -""" + -----BEGIN CERTIFICATE----- + MIICEDCCAbYCCQC7a34VXIF7+DAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt + cGxlLmNvbTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNV + BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B + AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB + AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT + sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl + Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo + N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABN5Q + sDW36YI12PFC/kRnACzCt8a5lqjaFu6QNl0Y0ZYaiE9MdR+EOGcCfoSGf9r8n1Yl + peOOLlvsXQ0UO8WJbsYwCgYIKoZIzj0EAwIDSAAwRQIgGd0bh4HWEd3ytsCEGaw0 + m567URfCk1u6sY4I77U64zQCIQD5hOn0PDS4eYR+kBB5MadQtcBtz8gjtW/OJcfV + D1NSHw== + -----END CERTIFICATE----- + """ private let serverExplicitCurveKey = """ ------BEGIN EC PRIVATE KEY----- -MIIBaAIBAQQgHqp+i/1N/Iq8DUruPu0ep9WiB9I+n1Ox6qFucixKbr6ggfowgfcC -AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// -MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr -vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE -axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W -K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 -YyVRAgEBoUQDQgAE3lCwNbfpgjXY8UL+RGcALMK3xrmWqNoW7pA2XRjRlhqIT0x1 -H4Q4ZwJ+hIZ/2vyfViWl444uW+xdDRQ7xYluxg== ------END EC PRIVATE KEY----- -""" + -----BEGIN EC PRIVATE KEY----- + MIIBaAIBAQQgHqp+i/1N/Iq8DUruPu0ep9WiB9I+n1Ox6qFucixKbr6ggfowgfcC + AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// + MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr + vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE + axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W + K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 + YyVRAgEBoUQDQgAE3lCwNbfpgjXY8UL+RGcALMK3xrmWqNoW7pA2XRjRlhqIT0x1 + H4Q4ZwJ+hIZ/2vyfViWl444uW+xdDRQ7xYluxg== + -----END EC PRIVATE KEY----- + """ -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift b/Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift index 967b81344..9788c5def 100644 --- a/Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift +++ b/Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift @@ -49,7 +49,7 @@ extension Generator { case .clientStreaming, .bidirectionalStreaming: arguments = [ - "callOptions: \(Types.clientCallOptions)?", + "callOptions: \(Types.clientCallOptions)?" ] } @@ -61,7 +61,7 @@ extension Generator { ) } } - self.println("}") // protocol + self.println("}") // protocol } } @@ -156,7 +156,8 @@ extension Generator { let streamsRequests = [.clientStreaming, .bidirectionalStreaming].contains(rpcType) // (protocol, requires sendable) - let sequenceProtocols: [(String, Bool)?] = streamsRequests + let sequenceProtocols: [(String, Bool)?] = + streamsRequests ? [("Sequence", false), ("AsyncSequence", true)] : [nil] @@ -165,12 +166,14 @@ extension Generator { if i > 0 || j > 0 { self.println() } - let functionName = streamsRequests + let functionName = + streamsRequests ? "\(self.methodFunctionName)" : self.methodFunctionName let requestParamName = streamsRequests ? "requests" : "request" let requestParamType = streamsRequests ? "RequestStream" : self.methodInputName - let returnType = streamsResponses + let returnType = + streamsResponses ? Types.responseStream(of: self.methodOutputName) : self.methodOutputName let maybeWhereClause = sequenceProtocol.map { protocolName, mustBeSendable -> String in diff --git a/Sources/protoc-gen-grpc-swift/Generator-Client.swift b/Sources/protoc-gen-grpc-swift/Generator-Client.swift index c093cc9ae..d18082132 100644 --- a/Sources/protoc-gen-grpc-swift/Generator-Client.swift +++ b/Sources/protoc-gen-grpc-swift/Generator-Client.swift @@ -89,11 +89,14 @@ extension Generator { self.println("\(functionHead)func \(name)(") self.withIndentation { // Add a comma after each argument except the last. - arguments.forEach(beforeLast: { - self.println($0 + ",") - }, onLast: { - self.println($0) - }) + arguments.forEach( + beforeLast: { + self.println($0 + ",") + }, + onLast: { + self.println($0) + } + ) } self.println(")\(asyncThrows)\(`return`)\(genericWhere)", newline: !hasBody) } @@ -504,7 +507,7 @@ extension Generator { self.printFunction( name: "make\(self.method.name)ResponseStream", arguments: [ - "_ requestHandler: @escaping (FakeRequestPart<\(self.methodInputName)>) -> () = { _ in }", + "_ requestHandler: @escaping (FakeRequestPart<\(self.methodInputName)>) -> () = { _ in }" ], returnType: "\(type)<\(self.methodInputName), \(self.methodOutputName)>", access: self.access @@ -569,7 +572,7 @@ extension Generator { self.printFakeResponseStreams() } - self.println("}") // end class + self.println("}") // end class } } @@ -664,9 +667,9 @@ extension MethodDescriptor { let sourceComments = self.protoSourceComments() if sourceComments.isEmpty { - return "/// \(streamingType.name) call to \(self.name)\n" // comments end with "\n" already. + return "/// \(streamingType.name) call to \(self.name)\n" // comments end with "\n" already. } else { - return sourceComments // already prefixed with "///" + return sourceComments // already prefixed with "///" } } } diff --git a/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift b/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift index d124a47a1..71e921eb4 100644 --- a/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift +++ b/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift @@ -176,7 +176,7 @@ extension Generator { self.println("return nil") } - self.println("}") // switch + self.println("}") // switch } } } diff --git a/Sources/protoc-gen-grpc-swift/Generator.swift b/Sources/protoc-gen-grpc-swift/Generator.swift index 9c0eed253..7ed0c4aad 100644 --- a/Sources/protoc-gen-grpc-swift/Generator.swift +++ b/Sources/protoc-gen-grpc-swift/Generator.swift @@ -20,8 +20,8 @@ class Generator { private var printer: CodePrinter internal var file: FileDescriptor - internal var service: ServiceDescriptor! // context during generation - internal var method: MethodDescriptor! // context during generation + internal var service: ServiceDescriptor! // context during generation + internal var method: MethodDescriptor! // context during generation internal let protobufNamer: SwiftProtobufNamer @@ -108,15 +108,17 @@ class Generator { } private func printMain() { - self.printer.print(""" - // - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the protocol buffer compiler. - // Source: \(self.file.name) - //\n - """) + self.printer.print( + """ + // + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the protocol buffer compiler. + // Source: \(self.file.name) + //\n + """ + ) let moduleNames = [ self.options.gRPCModuleName, diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 0f32bd53e..4a1b47b8a 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -141,7 +141,7 @@ func main(args: [String]) throws { // Only generate output for services. for name in request.fileToGenerate { if let fileDescriptor = descriptorSet.fileDescriptor(named: name) { - if (options.generateReflectionData) { + if options.generateReflectionData { var binaryFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() let binaryFileName = uniqueOutputFileName( component: "grpc.reflection", @@ -156,10 +156,7 @@ func main(args: [String]) throws { binaryFile.content = serializedFileDescriptorProto response.file.append(binaryFile) } - if ( - !fileDescriptor.services - .isEmpty && (options.generateClient || options.generateServer) - ) { + if !fileDescriptor.services.isEmpty && (options.generateClient || options.generateServer) { var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() let grpcFileName = uniqueOutputFileName( component: "grpc", diff --git a/Tests/GRPCTests/ALPNConfigurationTests.swift b/Tests/GRPCTests/ALPNConfigurationTests.swift index 25419a95a..b106f9c0e 100644 --- a/Tests/GRPCTests/ALPNConfigurationTests.swift +++ b/Tests/GRPCTests/ALPNConfigurationTests.swift @@ -101,4 +101,4 @@ class ALPNConfigurationTests: GRPCTestCase { XCTAssertEqual(config.nioConfiguration!.configuration.applicationProtocols, ["foo"]) } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/Array+BoundsCheckingTests.swift b/Tests/GRPCTests/Array+BoundsCheckingTests.swift index 477db084f..80c83c531 100644 --- a/Tests/GRPCTests/Array+BoundsCheckingTests.swift +++ b/Tests/GRPCTests/Array+BoundsCheckingTests.swift @@ -14,9 +14,10 @@ * limitations under the License. */ -@testable import GRPC import XCTest +@testable import GRPC + class ArrayBoundsCheckingTests: GRPCTestCase { func testBoundsCheckEmpty() { let array: [Int] = [] diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift index de8e8670c..fe3d0f3db 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift @@ -114,11 +114,14 @@ final class AsyncIntegrationTests: GRPCTestCase { initialMetadata.assertFirst("200", forName: ":status") let responses = try await expand.responseStream.map { $0.text }.collect() - XCTAssertEqual(responses, [ - "Swift echo expand (0): boyle", - "Swift echo expand (1): jeffers", - "Swift echo expand (2): holt", - ]) + XCTAssertEqual( + responses, + [ + "Swift echo expand (0): boyle", + "Swift echo expand (1): jeffers", + "Swift echo expand (2): holt", + ] + ) let trailingMetadata = try await expand.trailingMetadata trailingMetadata.assertFirst("0", forName: "grpc-status") @@ -130,11 +133,14 @@ final class AsyncIntegrationTests: GRPCTestCase { func testServerStreamingWrapper() async throws { let responseStream = self.echo.expand(.with { $0.text = "boyle jeffers holt" }) let responses = try await responseStream.map { $0.text }.collect() - XCTAssertEqual(responses, [ - "Swift echo expand (0): boyle", - "Swift echo expand (1): jeffers", - "Swift echo expand (2): holt", - ]) + XCTAssertEqual( + responses, + [ + "Swift echo expand (0): boyle", + "Swift echo expand (1): jeffers", + "Swift echo expand (2): holt", + ] + ) } func testBidirectionalStreaming() async throws { @@ -172,11 +178,14 @@ final class AsyncIntegrationTests: GRPCTestCase { let responseStream = self.echo.update(requests) let responses = try await responseStream.map { $0.text }.collect() - XCTAssertEqual(responses, [ - "Swift echo update (0): boyle", - "Swift echo update (1): jeffers", - "Swift echo update (2): holt", - ]) + XCTAssertEqual( + responses, + [ + "Swift echo update (0): boyle", + "Swift echo update (1): jeffers", + "Swift echo update (2): holt", + ] + ) } func testServerCloseAfterMessage() async throws { @@ -184,7 +193,7 @@ final class AsyncIntegrationTests: GRPCTestCase { try await update.requestStream.send(.with { $0.text = "hello" }) _ = try await update.responseStream.first(where: { _ in true }) XCTAssertNoThrow(try self.server.close().wait()) - self.server = nil // So that tearDown() does not call close() again. + self.server = nil // So that tearDown() does not call close() again. update.requestStream.finish() } } diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift index 22d133180..e0f1827b9 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHPACK import XCTest +@testable import GRPC + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) internal final class ServerHandlerStateMachineTests: GRPCTestCase { private enum InitialState { diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift index f42b3c648..789a126a4 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import XCTest +@testable import GRPC + internal final class ServerInterceptorStateMachineStreamStateTests: GRPCTestCase { func testInboundStreamState_receiveMetadataWhileIdle() { var state = ServerInterceptorStateMachine.InboundStreamState.idle diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift index ce7464fde..98bd6d7c5 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift @@ -13,15 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOEmbedded import XCTest +@testable import GRPC + final class ServerInterceptorStateMachineTests: GRPCTestCase { func testInterceptRequestMetadataWhenIntercepting() { var stateMachine = ServerInterceptorStateMachine() stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptRequestMetadata().assertCancel() // Can't receive metadata twice. + stateMachine.interceptRequestMetadata().assertCancel() // Can't receive metadata twice. } func testInterceptRequestMessageWhenIntercepting() { @@ -32,14 +34,14 @@ final class ServerInterceptorStateMachineTests: GRPCTestCase { func testInterceptRequestEndWhenIntercepting() { var stateMachine = ServerInterceptorStateMachine() stateMachine.interceptRequestEnd().assertIntercept() - stateMachine.interceptRequestEnd().assertCancel() // Can't receive end twice. + stateMachine.interceptRequestEnd().assertCancel() // Can't receive end twice. } func testInterceptedRequestMetadataWhenIntercepting() { var stateMachine = ServerInterceptorStateMachine() stateMachine.interceptRequestMetadata().assertIntercept() stateMachine.interceptedRequestMetadata().assertForward() - stateMachine.interceptedRequestMetadata().assertCancel() // Can't intercept metadata twice. + stateMachine.interceptedRequestMetadata().assertCancel() // Can't intercept metadata twice. } func testInterceptedRequestMessageWhenIntercepting() { @@ -58,7 +60,7 @@ final class ServerInterceptorStateMachineTests: GRPCTestCase { stateMachine.interceptedRequestMetadata().assertForward() stateMachine.interceptRequestEnd().assertIntercept() stateMachine.interceptedRequestEnd().assertForward() - stateMachine.interceptedRequestEnd().assertCancel() // Can't intercept end twice. + stateMachine.interceptedRequestEnd().assertCancel() // Can't intercept end twice. } func testInterceptResponseMetadataWhenIntercepting() { diff --git a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift index fb56aa32b..95c415f30 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift @@ -71,7 +71,7 @@ func withTaskCancelledAfter( // Check which task completed. switch firstResult { case .cancellation: - () // Fine, what we expect. + () // Fine, what we expect. case .operation: XCTFail("Operation completed before cancellation") case .none: diff --git a/Tests/GRPCTests/BasicEchoTestCase.swift b/Tests/GRPCTests/BasicEchoTestCase.swift index 4e45878f9..f8ccf2f58 100644 --- a/Tests/GRPCTests/BasicEchoTestCase.swift +++ b/Tests/GRPCTests/BasicEchoTestCase.swift @@ -20,10 +20,11 @@ import Foundation import GRPC import GRPCSampleData import NIOCore +import XCTest + #if canImport(NIOSSL) import NIOSSL #endif -import XCTest extension Echo_EchoRequest { init(text: String) { diff --git a/Tests/GRPCTests/CallPathTests.swift b/Tests/GRPCTests/CallPathTests.swift index 6e90b7354..304a9598f 100644 --- a/Tests/GRPCTests/CallPathTests.swift +++ b/Tests/GRPCTests/CallPathTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import XCTest +@testable import GRPC + class CallPathTests: GRPCTestCase { func testSplitPathNormal() { let path = "/server/method" diff --git a/Tests/GRPCTests/CapturingLogHandler.swift b/Tests/GRPCTests/CapturingLogHandler.swift index a7eba1758..6791ff958 100644 --- a/Tests/GRPCTests/CapturingLogHandler.swift +++ b/Tests/GRPCTests/CapturingLogHandler.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.Date -import class Foundation.DateFormatter + import Logging import NIOConcurrencyHelpers +import struct Foundation.Date +import class Foundation.DateFormatter + /// A `LogHandler` factory which captures all logs emitted by the handlers it makes. internal class CapturingLogHandlerFactory { private var lock = NIOLock() diff --git a/Tests/GRPCTests/ClientCallTests.swift b/Tests/GRPCTests/ClientCallTests.swift index f01f9378f..fc65fead1 100644 --- a/Tests/GRPCTests/ClientCallTests.swift +++ b/Tests/GRPCTests/ClientCallTests.swift @@ -15,11 +15,12 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOCore import NIOPosix import XCTest +@testable import GRPC + class ClientCallTests: GRPCTestCase { private var group: MultiThreadedEventLoopGroup! private var server: Server! @@ -220,7 +221,7 @@ class ClientCallTests: GRPCTestCase { .message(.with { $0.text = "foo" }, .init(compress: false, flush: false)), promise: promise ) - } onError: { _ in // ignore errors + } onError: { _ in // ignore errors } onResponsePart: { switch $0 { case .metadata, .message: diff --git a/Tests/GRPCTests/ClientConnectionBackoffTests.swift b/Tests/GRPCTests/ClientConnectionBackoffTests.swift index 440a23b27..3d6fd9a34 100644 --- a/Tests/GRPCTests/ClientConnectionBackoffTests.swift +++ b/Tests/GRPCTests/ClientConnectionBackoffTests.swift @@ -78,10 +78,13 @@ class ClientConnectionBackoffTests: GRPCTestCase { func testClientConnectionFailsWithNoBackoff() throws { self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .shutdown), + ] + ) } self.client = self.connectionBuilder() @@ -100,12 +103,15 @@ class ClientConnectionBackoffTests: GRPCTestCase { func testClientConnectionFailureIsLimited() throws { self.connectionStateRecorder.expectChanges(4) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .transientFailure), - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .shutdown), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .transientFailure), + Change(from: .transientFailure, to: .connecting), + Change(from: .connecting, to: .shutdown), + ] + ) } self.client = self.connectionBuilder() @@ -124,10 +130,13 @@ class ClientConnectionBackoffTests: GRPCTestCase { func testClientEventuallyConnects() throws { self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .transientFailure), + ] + ) } // Start the client first. @@ -144,10 +153,13 @@ class ClientConnectionBackoffTests: GRPCTestCase { self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .ready), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .transientFailure, to: .connecting), + Change(from: .connecting, to: .ready), + ] + ) } self.server = self.makeServer() @@ -165,10 +177,13 @@ class ClientConnectionBackoffTests: GRPCTestCase { // Prepare the delegate so it expects the connection to hit `.ready`. self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .ready), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .ready), + ] + ) } // Configure the client backoff to have a short backoff. @@ -192,11 +207,14 @@ class ClientConnectionBackoffTests: GRPCTestCase { // 1. when the server has been killed, and // 2. when the client attempts to reconnect. self.connectionStateRecorder.expectChanges(3) { changes in - XCTAssertEqual(changes, [ - Change(from: .ready, to: .transientFailure), - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .ready, to: .transientFailure), + Change(from: .transientFailure, to: .connecting), + Change(from: .connecting, to: .transientFailure), + ] + ) } // Okay, kill the server! @@ -210,10 +228,13 @@ class ClientConnectionBackoffTests: GRPCTestCase { // Get ready for the new healthy connection. self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .ready), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .transientFailure, to: .connecting), + Change(from: .connecting, to: .ready), + ] + ) } // This should succeed once we get a connection again. diff --git a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift index 0804d22c8..ee6ca9bb3 100644 --- a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import Logging import NIOCore import NIOEmbedded import NIOHPACK import XCTest +@testable import GRPC + class ClientInterceptorPipelineTests: GRPCTestCase { override func setUp() { super.setUp() @@ -262,7 +264,7 @@ class ClientInterceptorPipelineTests: GRPCTestCase { // Check the file and line, if expected. if let expectedFile = self.file, let expectedLine = self.line { - XCTAssertEqual("\(file)", "\(expectedFile)") // StaticString isn't Equatable + XCTAssertEqual("\(file)", "\(expectedFile)") // StaticString isn't Equatable XCTAssertEqual(line, expectedLine) } } diff --git a/Tests/GRPCTests/ClientQuiescingTests.swift b/Tests/GRPCTests/ClientQuiescingTests.swift index 02b897365..77c69c203 100644 --- a/Tests/GRPCTests/ClientQuiescingTests.swift +++ b/Tests/GRPCTests/ClientQuiescingTests.swift @@ -431,8 +431,8 @@ extension ClientQuiescingTests { self.lock.withLock { switch (self.state, state) { case (.active, .active), - (.shutdownRequested, .shutdownRequested), - (.shutdown, .shutdown): + (.shutdownRequested, .shutdownRequested), + (.shutdown, .shutdown): () default: XCTFail("Expected \(state) but state is \(self.state)", line: line) diff --git a/Tests/GRPCTests/ClientTLSFailureTests.swift b/Tests/GRPCTests/ClientTLSFailureTests.swift index d3ab689e6..b14e8fe9c 100644 --- a/Tests/GRPCTests/ClientTLSFailureTests.swift +++ b/Tests/GRPCTests/ClientTLSFailureTests.swift @@ -112,10 +112,13 @@ class ClientTLSFailureTests: GRPCTestCase { let stateChangeDelegate = RecordingConnectivityDelegate() stateChangeDelegate.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .shutdown), + ] + ) } configuration.connectivityStateDelegate = stateChangeDelegate @@ -127,7 +130,8 @@ class ClientTLSFailureTests: GRPCTestCase { stateChangeDelegate.waitForExpectedChanges(timeout: .seconds(5)) if let nioSSLError = errorRecorder.errors.first as? NIOSSLError, - case .handshakeFailed(.sslError) = nioSSLError { + case .handshakeFailed(.sslError) = nioSSLError + { // Expected case. } else { XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)") @@ -149,10 +153,13 @@ class ClientTLSFailureTests: GRPCTestCase { let stateChangeDelegate = RecordingConnectivityDelegate() stateChangeDelegate.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .shutdown), + ] + ) } configuration.connectivityStateDelegate = stateChangeDelegate @@ -194,10 +201,13 @@ class ClientTLSFailureTests: GRPCTestCase { let stateChangeDelegate = RecordingConnectivityDelegate() stateChangeDelegate.expectChanges(2) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .shutdown), + ] + ) } configuration.connectivityStateDelegate = stateChangeDelegate @@ -209,7 +219,8 @@ class ClientTLSFailureTests: GRPCTestCase { stateChangeDelegate.waitForExpectedChanges(timeout: .seconds(5)) if let nioSSLError = errorRecorder.errors.first as? NIOSSLError, - case .handshakeFailed(.sslError) = nioSSLError { + case .handshakeFailed(.sslError) = nioSSLError + { // Expected case. } else { XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)") @@ -217,4 +228,4 @@ class ClientTLSFailureTests: GRPCTestCase { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ClientTLSTests.swift b/Tests/GRPCTests/ClientTLSTests.swift index 7b3669191..4bcea05a7 100644 --- a/Tests/GRPCTests/ClientTLSTests.swift +++ b/Tests/GRPCTests/ClientTLSTests.swift @@ -187,9 +187,11 @@ private class AuthorityCheckingEcho: Echo_EchoProvider { XCTAssertEqual(authority, SampleCertificate.exampleServer.commonName) XCTAssertNotEqual(authority, "localhost") - return context.eventLoop.makeSucceededFuture(.with { - $0.text = "Swift echo get: \(request.text)" - }) + return context.eventLoop.makeSucceededFuture( + .with { + $0.text = "Swift echo get: \(request.text)" + } + ) } func expand( @@ -212,4 +214,4 @@ private class AuthorityCheckingEcho: Echo_EchoProvider { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ClientTimeoutTests.swift b/Tests/GRPCTests/ClientTimeoutTests.swift index 0bf157101..4dc9abac7 100644 --- a/Tests/GRPCTests/ClientTimeoutTests.swift +++ b/Tests/GRPCTests/ClientTimeoutTests.swift @@ -15,7 +15,6 @@ */ import EchoModel import Foundation -@testable import GRPC import Logging import NIOCore import NIOEmbedded @@ -23,6 +22,8 @@ import NIOHTTP2 import SwiftProtobuf import XCTest +@testable import GRPC + class ClientTimeoutTests: GRPCTestCase { var channel: EmbeddedChannel! var client: Echo_EchoNIOClient! diff --git a/Tests/GRPCTests/ClientTransportTests.swift b/Tests/GRPCTests/ClientTransportTests.swift index 504fac463..500d799dd 100644 --- a/Tests/GRPCTests/ClientTransportTests.swift +++ b/Tests/GRPCTests/ClientTransportTests.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import XCTest +@testable import GRPC + class ClientTransportTests: GRPCTestCase { override func setUp() { super.setUp() diff --git a/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift b/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift index 878aed9c9..f61963e9f 100644 --- a/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift +++ b/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import XCTest +@testable import GRPC + internal final class CoalescingLengthPrefixedMessageWriterTests: GRPCTestCase { private let loop = EmbeddedEventLoop() diff --git a/Tests/GRPCTests/CompressionTests.swift b/Tests/GRPCTests/CompressionTests.swift index efe138e1a..99f3693d0 100644 --- a/Tests/GRPCTests/CompressionTests.swift +++ b/Tests/GRPCTests/CompressionTests.swift @@ -67,11 +67,15 @@ class MessageCompressionTests: GRPCTestCase { // The spec says that servers should handle compression they support but don't advertise. try self .setupServer(encoding: .enabled(.init(enabledAlgorithms: [], decompressionLimit: .ratio(10)))) - self.setupClient(encoding: .enabled(.init( - forRequests: .gzip, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ))) + self.setupClient( + encoding: .enabled( + .init( + forRequests: .gzip, + acceptableForResponses: [.deflate, .gzip], + decompressionLimit: .ratio(10) + ) + ) + ) let get = self.echo.get(.with { $0.text = "foo" }) @@ -90,11 +94,15 @@ class MessageCompressionTests: GRPCTestCase { func testUncompressedRequestsCompressedResponses() throws { try self.setupServer(encoding: .enabled(.init(decompressionLimit: .ratio(10)))) - self.setupClient(encoding: .enabled(.init( - forRequests: .none, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ))) + self.setupClient( + encoding: .enabled( + .init( + forRequests: .none, + acceptableForResponses: [.deflate, .gzip], + decompressionLimit: .ratio(10) + ) + ) + ) let get = self.echo.get(.with { $0.text = "foo" }) @@ -116,16 +124,24 @@ class MessageCompressionTests: GRPCTestCase { // so it must also return a "grpc-accept-encoding" header which includes the value it did not // advertise. try self - .setupServer(encoding: .enabled(.init( - enabledAlgorithms: [.gzip], - decompressionLimit: .ratio(10) - ))) + .setupServer( + encoding: .enabled( + .init( + enabledAlgorithms: [.gzip], + decompressionLimit: .ratio(10) + ) + ) + ) self - .setupClient(encoding: .enabled(.init( - forRequests: .deflate, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - ))) + .setupClient( + encoding: .enabled( + .init( + forRequests: .deflate, + acceptableForResponses: [], + decompressionLimit: .ratio(10) + ) + ) + ) let get = self.echo.get(.with { $0.text = "foo" }) @@ -146,15 +162,23 @@ class MessageCompressionTests: GRPCTestCase { // Server should be able to compress responses with a different method to the client, providing // the client supports it. try self - .setupServer(encoding: .enabled(.init( - enabledAlgorithms: [.gzip], - decompressionLimit: .ratio(10) - ))) - self.setupClient(encoding: .enabled(.init( - forRequests: .deflate, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ))) + .setupServer( + encoding: .enabled( + .init( + enabledAlgorithms: [.gzip], + decompressionLimit: .ratio(10) + ) + ) + ) + self.setupClient( + encoding: .enabled( + .init( + forRequests: .deflate, + acceptableForResponses: [.deflate, .gzip], + decompressionLimit: .ratio(10) + ) + ) + ) let get = self.echo.get(.with { $0.text = "foo" }) @@ -173,17 +197,25 @@ class MessageCompressionTests: GRPCTestCase { func testCompressedRequestWithCompressionNotSupportedOnServer() throws { try self - .setupServer(encoding: .enabled(.init( - enabledAlgorithms: [.gzip, .deflate], - decompressionLimit: .ratio(10) - ))) + .setupServer( + encoding: .enabled( + .init( + enabledAlgorithms: [.gzip, .deflate], + decompressionLimit: .ratio(10) + ) + ) + ) // We can't specify a compression we don't support, so we'll specify no compression and then // send a 'grpc-encoding' with our initial metadata. - self.setupClient(encoding: .enabled(.init( - forRequests: .none, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ))) + self.setupClient( + encoding: .enabled( + .init( + forRequests: .none, + acceptableForResponses: [.deflate, .gzip], + decompressionLimit: .ratio(10) + ) + ) + ) let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"] let get = self.echo.get( @@ -210,10 +242,14 @@ class MessageCompressionTests: GRPCTestCase { func testDecompressionLimitIsRespectedByServerForUnaryCall() throws { try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1)))) self - .setupClient(encoding: .enabled(.init( - forRequests: .gzip, - decompressionLimit: .absolute(1024) - ))) + .setupClient( + encoding: .enabled( + .init( + forRequests: .gzip, + decompressionLimit: .absolute(1024) + ) + ) + ) let get = self.echo.get(.with { $0.text = "foo" }) let status = self.expectation(description: "received status") @@ -228,10 +264,14 @@ class MessageCompressionTests: GRPCTestCase { func testDecompressionLimitIsRespectedByServerForStreamingCall() throws { try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1024)))) self - .setupClient(encoding: .enabled(.init( - forRequests: .gzip, - decompressionLimit: .absolute(2048) - ))) + .setupClient( + encoding: .enabled( + .init( + forRequests: .gzip, + decompressionLimit: .absolute(2048) + ) + ) + ) let collect = self.echo.collect() let status = self.expectation(description: "received status") @@ -251,10 +291,14 @@ class MessageCompressionTests: GRPCTestCase { func testDecompressionLimitIsRespectedByClientForUnaryCall() throws { try self - .setupServer(encoding: .enabled(.init( - enabledAlgorithms: [.gzip], - decompressionLimit: .absolute(1024) - ))) + .setupServer( + encoding: .enabled( + .init( + enabledAlgorithms: [.gzip], + decompressionLimit: .absolute(1024) + ) + ) + ) self.setupClient(encoding: .enabled(.responsesOnly(decompressionLimit: .absolute(1)))) let get = self.echo.get(.with { $0.text = "foo" }) @@ -335,11 +379,15 @@ class MessageCompressionTests: GRPCTestCase { try self.setupServer(encoding: .disabled) // We can't specify a compression we don't support, so we'll specify no compression and then // send a 'grpc-encoding' with our initial metadata. - self.setupClient(encoding: .enabled(.init( - forRequests: .none, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ))) + self.setupClient( + encoding: .enabled( + .init( + forRequests: .none, + acceptableForResponses: [.deflate, .gzip], + decompressionLimit: .ratio(10) + ) + ) + ) let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"] let get = self.echo.get( diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 92f2333f3..1b45c3191 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -14,13 +14,14 @@ * limitations under the License. */ import EchoModel -@testable import GRPC import Logging import NIOCore import NIOEmbedded import NIOHTTP2 import XCTest +@testable import GRPC + class ConnectionManagerTests: GRPCTestCase { private let loop = EmbeddedEventLoop() private let recorder = RecordingConnectivityDelegate() @@ -116,7 +117,8 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let multiplexer: EventLoopFuture = self + let multiplexer: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let channel = manager.getHTTP2Multiplexer() self.loop.run() @@ -188,7 +190,8 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -279,8 +282,10 @@ extension ConnectionManagerTests { func testChannelInactiveBeforeActiveWillReconnect() throws { var channels = [EmbeddedChannel(loop: self.loop), EmbeddedChannel(loop: self.loop)] - var channelPromises: [EventLoopPromise] = [self.loop.makePromise(), - self.loop.makePromise()] + var channelPromises: [EventLoopPromise] = [ + self.loop.makePromise(), + self.loop.makePromise(), + ] var channelFutures = Array(channelPromises.map { $0.futureResult }) var configuration = self.defaultConfiguration @@ -359,7 +364,8 @@ extension ConnectionManagerTests { } // Start the connection. - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -431,7 +437,8 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -556,7 +563,8 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -619,7 +627,8 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -694,7 +703,8 @@ extension ConnectionManagerTests { return next } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -770,7 +780,8 @@ extension ConnectionManagerTests { return next } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -867,7 +878,8 @@ extension ConnectionManagerTests { return channelPromise.futureResult } - let readyChannelMux: EventLoopFuture = self + let readyChannelMux: EventLoopFuture = + self .waitForStateChange(from: .idle, to: .connecting) { let readyChannelMux = manager.getHTTP2Multiplexer() self.loop.run() @@ -1034,15 +1046,17 @@ extension ConnectionManagerTests { channel: channel, inboundStreamInitializer: nil ) - XCTAssertNoThrow(try channel.pipeline.addHandlers([ - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ), - ]).wait()) + XCTAssertNoThrow( + try channel.pipeline.addHandlers([ + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ]).wait() + ) channelPromise.succeed(channel) self.loop.run() @@ -1089,15 +1103,17 @@ extension ConnectionManagerTests { channel: channel, inboundStreamInitializer: nil ) - XCTAssertNoThrow(try channel.pipeline.addHandlers([ - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ), - ]).wait()) + XCTAssertNoThrow( + try channel.pipeline.addHandlers([ + GRPCIdleHandler( + connectionManager: manager, + multiplexer: h2mux, + idleTimeout: .minutes(5), + keepalive: .init(), + logger: self.logger + ) + ]).wait() + ) channelPromise.succeed(channel) self.loop.run() @@ -1369,7 +1385,8 @@ internal class RecordingConnectivityDelegate: ConnectivityStateDelegate { case .timedOut: XCTFail( "Timed out before verifying \(self.expectation.count) change(s)", - file: file, line: line + file: file, + line: line ) } } diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift index 4a063bfa3..0de3de52d 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift @@ -108,13 +108,13 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { var id: GRPCConnectionID { switch self { case let .connectionAdded(id), - let .startedConnecting(id), - let .connectFailed(id), - let .connectSucceeded(id, _), - let .connectionClosed(id), - let .connectionUtilizationChanged(id, _, _), - let .connectionQuiescing(id), - let .connectionRemoved(id): + let .startedConnecting(id), + let .connectFailed(id), + let .connectSucceeded(id, _), + let .connectionClosed(id), + let .connectionUtilizationChanged(id, _, _), + let .connectionQuiescing(id), + let .connectionRemoved(id): return id } } diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 1859bf675..2e701fee8 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import Logging import NIOCore import NIOEmbedded import NIOHTTP2 import XCTest +@testable import GRPC + final class ConnectionPoolTests: GRPCTestCase { private enum TestError: Error { case noChannelExpected diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index a118d813e..dd41402d2 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -67,7 +67,8 @@ final class GRPCChannelPoolTests: GRPCTestCase { builder = Server.insecure(group: self.group) } - return builder + return + builder .withLogger(self.serverLogger) .withServiceProviders([EchoProvider()]) } @@ -327,7 +328,7 @@ final class GRPCChannelPoolTests: GRPCTestCase { // MAX_CONCURRENT_STREAMS should be 100, we'll create 101 RPCs, 100 of which should not have to // wait because there's already an active connection. - let rpcs = (0 ..< 101).map { _ in self.echo.update { _ in }} + let rpcs = (0 ..< 101).map { _ in self.echo.update { _ in } } // The first RPC should (obviously) complete first. rpcs.first!.status.whenComplete { _ in lock.withLock { @@ -502,7 +503,7 @@ final class GRPCChannelPoolTests: GRPCTestCase { // Start shutting the server down. let didShutdown = self.server!.initiateGracefulShutdown() - self.server = nil // Avoid shutting down again in tearDown + self.server = nil // Avoid shutting down again in tearDown // Pause a moment so we know the client received the GOAWAY. let sleep = self.group.any().scheduleTask(in: .milliseconds(50)) {} @@ -589,4 +590,4 @@ final class GRPCChannelPoolTests: GRPCTestCase { XCTAssertNoThrow(try EventLoopFuture.andAllSucceed(rpcs, on: self.group.any()).wait()) } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift index 46fbaf337..438bb461d 100644 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOConcurrencyHelpers import NIOCore import NIOEmbedded import XCTest +@testable import GRPC + class PoolManagerStateMachineTests: GRPCTestCase { private func makeConnectionPool( on eventLoop: EventLoop, diff --git a/Tests/GRPCTests/ConnectivityStateMonitorTests.swift b/Tests/GRPCTests/ConnectivityStateMonitorTests.swift index 7ba11ef21..3ecdb794c 100644 --- a/Tests/GRPCTests/ConnectivityStateMonitorTests.swift +++ b/Tests/GRPCTests/ConnectivityStateMonitorTests.swift @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import Logging import XCTest +@testable import GRPC + class ConnectivityStateMonitorTests: GRPCTestCase { // Ensure `.idle` isn't first since it is the initial state and we only trigger callbacks // when the state changes, not when the state is set. @@ -26,11 +28,14 @@ class ConnectivityStateMonitorTests: GRPCTestCase { let recorder = RecordingConnectivityDelegate() recorder.expectChanges(3) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .ready), - Change(from: .ready, to: .shutdown), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .ready), + Change(from: .ready, to: .shutdown), + ] + ) } let monitor = ConnectivityStateMonitor(delegate: recorder, queue: nil) diff --git a/Tests/GRPCTests/DelegatingErrorHandlerTests.swift b/Tests/GRPCTests/DelegatingErrorHandlerTests.swift index 5b5cfc413..71fa4b7b3 100644 --- a/Tests/GRPCTests/DelegatingErrorHandlerTests.swift +++ b/Tests/GRPCTests/DelegatingErrorHandlerTests.swift @@ -47,4 +47,4 @@ class DelegatingErrorHandlerTests: GRPCTestCase { // Unchecked because the error recorder is only ever used in the context of an EmbeddedChannel. extension DelegatingErrorHandlerTests.ErrorRecorder: @unchecked Sendable {} -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/EchoTestClientTests.swift b/Tests/GRPCTests/EchoTestClientTests.swift index 1db312c3b..d8373ebc7 100644 --- a/Tests/GRPCTests/EchoTestClientTests.swift +++ b/Tests/GRPCTests/EchoTestClientTests.swift @@ -164,13 +164,17 @@ class EchoTestClientTests: GRPCTestCase { // Create a response stream for 'Update'. let stream = client.makeUpdateResponseStream() - model.updateWords(["foo", "bar", "baz"], onResponse: { response in - XCTAssertEqual(response, "Expected response") - responses.fulfill() - }, onEnd: { status in - XCTAssertEqual(status.code, .ok) - completed.fulfill() - }) + model.updateWords( + ["foo", "bar", "baz"], + onResponse: { response in + XCTAssertEqual(response, "Expected response") + responses.fulfill() + }, + onEnd: { status in + XCTAssertEqual(status.code, .ok) + completed.fulfill() + } + ) // Send some responses: XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = "Expected response" })) @@ -193,13 +197,17 @@ class EchoTestClientTests: GRPCTestCase { let responses = self.expectation(description: "Received responses") responses.expectedFulfillmentCount = 3 - model.updateWords(["foo", "bar", "baz"], onResponse: { response in - XCTAssertTrue(response.hasPrefix("Swift echo update")) - responses.fulfill() - }, onEnd: { status in - XCTAssertEqual(status.code, .ok) - completed.fulfill() - }) + model.updateWords( + ["foo", "bar", "baz"], + onResponse: { response in + XCTAssertTrue(response.hasPrefix("Swift echo update")) + responses.fulfill() + }, + onEnd: { status in + XCTAssertEqual(status.code, .ok) + completed.fulfill() + } + ) self.wait(for: [responses, completed], timeout: 10.0) } diff --git a/Tests/GRPCTests/FakeResponseStreamTests.swift b/Tests/GRPCTests/FakeResponseStreamTests.swift index ba5afe6fa..64e615136 100644 --- a/Tests/GRPCTests/FakeResponseStreamTests.swift +++ b/Tests/GRPCTests/FakeResponseStreamTests.swift @@ -14,12 +14,13 @@ * limitations under the License. */ import EchoModel -@testable import GRPC import NIOCore import NIOEmbedded import NIOHPACK import XCTest +@testable import GRPC + class FakeResponseStreamTests: GRPCTestCase { private typealias Request = Echo_EchoRequest private typealias Response = Echo_EchoResponse diff --git a/Tests/GRPCTests/FunctionalTests.swift b/Tests/GRPCTests/FunctionalTests.swift index 29d313565..1ce33561b 100644 --- a/Tests/GRPCTests/FunctionalTests.swift +++ b/Tests/GRPCTests/FunctionalTests.swift @@ -16,12 +16,13 @@ import Dispatch import EchoModel import Foundation -@testable import GRPC import NIOCore import NIOHTTP1 import NIOHTTP2 import XCTest +@testable import GRPC + class FunctionalTestsInsecureTransport: EchoTestCaseBase { override var transportSecurity: TransportSecurity { return .none @@ -88,7 +89,8 @@ class FunctionalTestsInsecureTransport: EchoTestCaseBase { for lowerBound in stride(from: 0, to: numberOfRequests, by: batchSize) { let upperBound = min(lowerBound + batchSize, numberOfRequests) let numberOfCalls = upperBound - lowerBound - let responseExpectation = self + let responseExpectation = + self .makeResponseExpectation(expectedFulfillmentCount: numberOfCalls) let statusExpectation = self.makeStatusExpectation(expectedFulfillmentCount: numberOfCalls) @@ -362,7 +364,7 @@ class FunctionalTestsMutualAuthentication: FunctionalTestsInsecureTransport { try super.testBidirectionalStreamingLotsOfMessagesPingPong() } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) // MARK: - Variants using NIO TS and Network.framework @@ -564,4 +566,4 @@ class FunctionalTestsMutualAuthenticationNIOTS: FunctionalTestsInsecureTransport try super.testBidirectionalStreamingLotsOfMessagesPingPong() } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift index 2754ba32d..bc3f8e3d4 100644 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift @@ -15,11 +15,12 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOHPACK import NIOPosix import XCTest +@testable import GRPC + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) class GRPCAsyncClientCallTests: GRPCTestCase { private var group: MultiThreadedEventLoopGroup? @@ -32,7 +33,7 @@ class GRPCAsyncClientCallTests: GRPCTestCase { ]) private static let OKTrailingMetadata = HPACKHeaders([ - ("grpc-status", "0"), + ("grpc-status", "0") ]) private func setUpServerAndChannel( @@ -89,7 +90,8 @@ class GRPCAsyncClientCallTests: GRPCTestCase { func testAsyncClientStreamingCall() async throws { let channel = try self.setUpServerAndChannel() - let collect: GRPCAsyncClientStreamingCall = channel + let collect: GRPCAsyncClientStreamingCall = + channel .makeAsyncClientStreamingCall( path: "/echo.Echo/Collect", callOptions: .init() @@ -108,7 +110,8 @@ class GRPCAsyncClientCallTests: GRPCTestCase { func testAsyncServerStreamingCall() async throws { let channel = try self.setUpServerAndChannel() - let expand: GRPCAsyncServerStreamingCall = channel + let expand: GRPCAsyncServerStreamingCall = + channel .makeAsyncServerStreamingCall( path: "/echo.Echo/Expand", request: .with { $0.text = "boyle jeffers holt" }, @@ -126,7 +129,8 @@ class GRPCAsyncClientCallTests: GRPCTestCase { func testAsyncBidirectionalStreamingCall() async throws { let channel = try self.setUpServerAndChannel() - let update: GRPCAsyncBidirectionalStreamingCall = channel + let update: GRPCAsyncBidirectionalStreamingCall = + channel .makeAsyncBidirectionalStreamingCall( path: "/echo.Echo/Update", callOptions: .init() @@ -149,7 +153,8 @@ class GRPCAsyncClientCallTests: GRPCTestCase { func testAsyncBidirectionalStreamingCall_InterleavedRequestsAndResponses() async throws { let channel = try self.setUpServerAndChannel() - let update: GRPCAsyncBidirectionalStreamingCall = channel + let update: GRPCAsyncBidirectionalStreamingCall = + channel .makeAsyncBidirectionalStreamingCall( path: "/echo.Echo/Update", callOptions: .init() @@ -173,7 +178,8 @@ class GRPCAsyncClientCallTests: GRPCTestCase { func testAsyncBidirectionalStreamingCall_ConcurrentTasks() async throws { let channel = try self.setUpServerAndChannel() - let update: GRPCAsyncBidirectionalStreamingCall = channel + let update: GRPCAsyncBidirectionalStreamingCall = + channel .makeAsyncBidirectionalStreamingCall( path: "/echo.Echo/Update", callOptions: .init() @@ -340,7 +346,7 @@ private final class AsyncEchoProvider: Echo_EchoAsyncProvider { private func accept(context: GRPCAsyncServerCallContext) async { await context.acceptRPC(headers: self.headers) if self.sendTwice { - await context.acceptRPC(headers: self.headers) // Should be a no-op. + await context.acceptRPC(headers: self.headers) // Should be a no-op. } } diff --git a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift index 057d37713..efc6e1fa8 100644 --- a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift +++ b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHPACK import NIOPosix import XCTest +@testable import GRPC + // MARK: - Tests @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) @@ -521,17 +523,19 @@ class AsyncServerHandlerTests: GRPCTestCase { } internal final class AsyncResponseStream: GRPCServerResponseWriter { - private let source: NIOAsyncSequenceProducer< - GRPCServerResponsePart, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source - - internal var responseSequence: NIOAsyncSequenceProducer< - GRPCServerResponsePart, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - > + private let source: + NIOAsyncSequenceProducer< + GRPCServerResponsePart, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + >.Source + + internal var responseSequence: + NIOAsyncSequenceProducer< + GRPCServerResponsePart, + NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, + GRPCAsyncSequenceProducerDelegate + > init() { let backpressureStrategy = NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark( diff --git a/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift b/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift index 4c98bc259..1f0757177 100644 --- a/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift +++ b/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHPACK import NIOHTTP2 import XCTest +@testable import GRPC + class GRPCClientChannelHandlerTests: GRPCTestCase { private func makeRequestHead() -> _GRPCRequestHead { return _GRPCRequestHead( @@ -60,9 +62,9 @@ class GRPCClientChannelHandlerTests: GRPCTestCase { // Write a message, if we need to. if dataContainsMessage { - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message } let dataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) @@ -79,7 +81,7 @@ class GRPCClientChannelHandlerTests: GRPCTestCase { case .initialMetadata, .message, .trailingMetadata: XCTFail("Unexpected response part") case .status: - () // Expected + () // Expected } } else { XCTFail("Expected to read another response part") diff --git a/Tests/GRPCTests/GRPCClientStateMachineTests.swift b/Tests/GRPCTests/GRPCClientStateMachineTests.swift index cb03c23d1..5cef96594 100644 --- a/Tests/GRPCTests/GRPCClientStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCClientStateMachineTests.swift @@ -15,7 +15,6 @@ */ import EchoModel import Foundation -@testable import GRPC import Logging import NIOCore import NIOHPACK @@ -23,6 +22,8 @@ import NIOHTTP1 import SwiftProtobuf import XCTest +@testable import GRPC + class GRPCClientStateMachineTests: GRPCTestCase { typealias Request = Echo_EchoRequest typealias Response = Echo_EchoResponse @@ -78,56 +79,69 @@ class GRPCClientStateMachineTests: GRPCTestCase { extension GRPCClientStateMachineTests { func doTestSendRequestHeadersFromInvalidState(_ state: StateMachine.State) { var stateMachine = self.makeStateMachine(state) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "host", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), allocator: .init()).assertFailure { + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "host", + deadline: .distantFuture, + customMetadata: [:], + encoding: .disabled + ), + allocator: .init() + ).assertFailure { XCTAssertEqual($0, .invalidState) } } func testSendRequestHeadersFromIdle() { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "host", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), allocator: .init()).assertSuccess() + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "host", + deadline: .distantFuture, + customMetadata: [:], + encoding: .disabled + ), + allocator: .init() + ).assertSuccess() } func testSendRequestHeadersFromClientActiveServerIdle() { - self.doTestSendRequestHeadersFromInvalidState(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + self.doTestSendRequestHeadersFromInvalidState( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) } func testSendRequestHeadersFromClientClosedServerIdle() { self .doTestSendRequestHeadersFromInvalidState( - .clientClosedServerIdle(pendingReadState: .init( - arity: .one, - messageEncoding: .disabled - )) + .clientClosedServerIdle( + pendingReadState: .init( + arity: .one, + messageEncoding: .disabled + ) + ) ) } func testSendRequestHeadersFromActive() { self - .doTestSendRequestHeadersFromInvalidState(.clientActiveServerActive( - writeState: .one(), - readState: .one() - )) + .doTestSendRequestHeadersFromInvalidState( + .clientActiveServerActive( + writeState: .one(), + readState: .one() + ) + ) } func testSendRequestHeadersFromClientClosedServerActive() { @@ -170,10 +184,12 @@ extension GRPCClientStateMachineTests { } func testSendRequestFromClientActiveServerIdle() { - self.doTestSendRequestFromValidState(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + self.doTestSendRequestFromValidState( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) } func testSendRequestFromClientClosedServerIdle() { @@ -185,10 +201,12 @@ extension GRPCClientStateMachineTests { func testSendRequestFromActive() { self - .doTestSendRequestFromValidState(.clientActiveServerActive( - writeState: .one(), - readState: .one() - )) + .doTestSendRequestFromValidState( + .clientActiveServerActive( + writeState: .one(), + readState: .one() + ) + ) } func testSendRequestFromClientClosedServerActive() { @@ -423,7 +441,7 @@ extension GRPCClientStateMachineTests { // content-type to make a valid set of trailers. switch state { case .clientActiveServerIdle, - .clientClosedServerIdle: + .clientClosedServerIdle: trailers.add(name: ":status", value: "200") trailers.add(name: "content-type", value: "application/grpc+proto") default: @@ -530,19 +548,23 @@ extension GRPCClientStateMachineTests { } func testSimpleUnaryFlow() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) // Initiate the RPC - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), allocator: .init()).assertSuccess() + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "https", + path: "/echo/Get", + host: "foo", + deadline: .distantFuture, + customMetadata: [:], + encoding: .disabled + ), + allocator: .init() + ).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() @@ -565,19 +587,23 @@ extension GRPCClientStateMachineTests { } func testSimpleClientActiveFlow() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .many(), readArity: .one)) // Initiate the RPC - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), allocator: .init()).assertSuccess() + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "https", + path: "/echo/Get", + host: "foo", + deadline: .distantFuture, + customMetadata: [:], + encoding: .disabled + ), + allocator: .init() + ).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() @@ -599,19 +625,23 @@ extension GRPCClientStateMachineTests { } func testSimpleServerActiveFlow() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .many)) // Initiate the RPC - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), allocator: .init()).assertSuccess() + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "https", + path: "/echo/Get", + host: "foo", + deadline: .distantFuture, + customMetadata: [:], + encoding: .disabled + ), + allocator: .init() + ).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() @@ -636,19 +666,23 @@ extension GRPCClientStateMachineTests { } func testSimpleBidirectionalActiveFlow() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .many(), readArity: .many)) // Initiate the RPC - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), allocator: .init()).assertSuccess() + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "https", + path: "/echo/Get", + host: "foo", + deadline: .distantFuture, + customMetadata: [:], + encoding: .disabled + ), + allocator: .init() + ).assertSuccess() // Receive acknowledgement. stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() @@ -682,10 +716,12 @@ extension GRPCClientStateMachineTests { extension GRPCClientStateMachineTests { func testSendTooManyRequestsFromClientActiveServerIdle() { for messageCount in [MessageArity.one, MessageArity.many] { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: messageCount, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: messageCount, messageEncoding: .disabled) + ) + ) // One is fine. stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() @@ -698,7 +734,8 @@ extension GRPCClientStateMachineTests { func testSendTooManyRequestsFromActive() { for readState in [ReadState.one(), ReadState.many()] { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientActiveServerActive(writeState: .one(), readState: readState)) // One is fine. @@ -721,7 +758,8 @@ extension GRPCClientStateMachineTests { func testReceiveTooManyRequests() throws { for writeState in [WriteState.one(), WriteState.many()] { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientActiveServerActive(writeState: writeState, readState: .one())) // One response is fine. @@ -737,7 +775,8 @@ extension GRPCClientStateMachineTests { func testReceiveTooManyRequestsInOneBuffer() throws { for writeState in [WriteState.one(), WriteState.many()] { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientActiveServerActive(writeState: writeState, readState: .one())) // Write two responses into a single buffer. @@ -756,21 +795,27 @@ extension GRPCClientStateMachineTests { extension GRPCClientStateMachineTests { func testSendRequestHeaders() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .now() + .hours(1), - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled(.init( - forRequests: .identity, - acceptableForResponses: [.identity], - decompressionLimit: .ratio(10) - )) - ), allocator: .init()).assertSuccess { headers in + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "localhost", + deadline: .now() + .hours(1), + customMetadata: ["x-grpc-id": "request-id"], + encoding: .enabled( + .init( + forRequests: .identity, + acceptableForResponses: [.identity], + decompressionLimit: .ratio(10) + ) + ) + ), + allocator: .init() + ).assertSuccess { headers in XCTAssertEqual(headers[":method"], ["POST"]) XCTAssertEqual(headers[":path"], ["/echo/Get"]) XCTAssertEqual(headers[":authority"], ["localhost"]) @@ -796,17 +841,21 @@ extension GRPCClientStateMachineTests { "ALLUPPER": filterKey, ] - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: customMetadata, - encoding: .disabled - ), allocator: .init()).assertSuccess { headers in + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "localhost", + deadline: .distantFuture, + customMetadata: customMetadata, + encoding: .disabled + ), + allocator: .init() + ).assertSuccess { headers in // Pull out the entries we care about by matching values let filtered = headers.filter { _, value, _ in value == filterKey @@ -826,86 +875,110 @@ extension GRPCClientStateMachineTests { func testSendRequestHeadersWithCustomUserAgent() throws { let customMetadata: HPACKHeaders = [ - "user-agent": "test-user-agent", + "user-agent": "test-user-agent" ] - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: customMetadata, - encoding: .enabled(.init( - forRequests: nil, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - )) - ), allocator: .init()).assertSuccess { headers in + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "localhost", + deadline: .distantFuture, + customMetadata: customMetadata, + encoding: .enabled( + .init( + forRequests: nil, + acceptableForResponses: [], + decompressionLimit: .ratio(10) + ) + ) + ), + allocator: .init() + ).assertSuccess { headers in XCTAssertEqual(headers["user-agent"], ["test-user-agent"]) } } func testSendRequestHeadersWithNoCompressionInEitherDirection() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled(.init( - forRequests: nil, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - )) - ), allocator: .init()).assertSuccess { headers in + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "localhost", + deadline: .distantFuture, + customMetadata: ["x-grpc-id": "request-id"], + encoding: .enabled( + .init( + forRequests: nil, + acceptableForResponses: [], + decompressionLimit: .ratio(10) + ) + ) + ), + allocator: .init() + ).assertSuccess { headers in XCTAssertFalse(headers.contains(name: "grpc-encoding")) XCTAssertFalse(headers.contains(name: "grpc-accept-encoding")) } } func testSendRequestHeadersWithNoCompressionForRequests() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled(.init( - forRequests: nil, - acceptableForResponses: [.identity, .gzip], - decompressionLimit: .ratio(10) - )) - ), allocator: .init()).assertSuccess { headers in + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "localhost", + deadline: .distantFuture, + customMetadata: ["x-grpc-id": "request-id"], + encoding: .enabled( + .init( + forRequests: nil, + acceptableForResponses: [.identity, .gzip], + decompressionLimit: .ratio(10) + ) + ) + ), + allocator: .init() + ).assertSuccess { headers in XCTAssertFalse(headers.contains(name: "grpc-encoding")) XCTAssertTrue(headers.contains(name: "grpc-accept-encoding")) } } func testSendRequestHeadersWithNoCompressionForResponses() throws { - var stateMachine = self + var stateMachine = + self .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders(requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled(.init( - forRequests: .gzip, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - )) - ), allocator: .init()).assertSuccess { headers in + stateMachine.sendRequestHeaders( + requestHead: .init( + method: "POST", + scheme: "http", + path: "/echo/Get", + host: "localhost", + deadline: .distantFuture, + customMetadata: ["x-grpc-id": "request-id"], + encoding: .enabled( + .init( + forRequests: .gzip, + acceptableForResponses: [], + decompressionLimit: .ratio(10) + ) + ) + ), + allocator: .init() + ).assertSuccess { headers in XCTAssertEqual(headers["grpc-encoding"], ["gzip"]) // This asymmetry is strange but allowed: if a client does not advertise support of the // compression it is using, the server may still process the message so long as it too @@ -915,18 +988,22 @@ extension GRPCClientStateMachineTests { } func testReceiveResponseHeadersWithOkStatus() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() } func testReceiveResponseHeadersWithNotOkStatus() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) let code = "\(HTTPResponseStatus.paymentRequired.code)" let headers = self.makeResponseHeaders(status: code) @@ -936,10 +1013,12 @@ extension GRPCClientStateMachineTests { } func testReceiveResponseHeadersWithoutContentType() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) let headers = self.makeResponseHeaders(contentType: nil) stateMachine.receiveResponseHeaders(headers).assertFailure { @@ -948,10 +1027,12 @@ extension GRPCClientStateMachineTests { } func testReceiveResponseHeadersWithInvalidContentType() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) let headers = self.makeResponseHeaders(contentType: "video/mpeg") stateMachine.receiveResponseHeaders(headers).assertFailure { @@ -965,10 +1046,12 @@ extension GRPCClientStateMachineTests { acceptableForResponses: [.identity], decompressionLimit: .ratio(1) ) - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .enabled(configuration)) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .enabled(configuration)) + ) + ) var headers = self.makeResponseHeaders() // Identity should always be supported. @@ -985,10 +1068,12 @@ extension GRPCClientStateMachineTests { } func testReceiveResponseHeadersWithUnsupportedCompressionMechanism() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) var headers = self.makeResponseHeaders() headers.add(name: "grpc-encoding", value: "snappy") @@ -999,10 +1084,12 @@ extension GRPCClientStateMachineTests { } func testReceiveResponseHeadersWithUnknownCompressionMechanism() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) var headers = self.makeResponseHeaders() headers.add(name: "grpc-encoding", value: "not-a-known-compression-(probably)") @@ -1054,10 +1141,12 @@ extension GRPCClientStateMachineTests { } func testReceiveTrailersOnlyEndOfResponseStreamWithoutContentType() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) let trailers: HPACKHeaders = [ ":status": "200", @@ -1071,10 +1160,12 @@ extension GRPCClientStateMachineTests { } func testReceiveTrailersOnlyEndOfResponseStreamWithInvalidContentType() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) let trailers: HPACKHeaders = [ ":status": "200", @@ -1088,10 +1179,12 @@ extension GRPCClientStateMachineTests { } func testReceiveTrailersOnlyEndOfResponseStreamWithInvalidHTTPStatusAndValidGRPCStatus() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) let trailers: HPACKHeaders = [ ":status": "418", @@ -1103,10 +1196,12 @@ extension GRPCClientStateMachineTests { } func testReceiveTrailersOnlyEndOfResponseStreamWithInvalidHTTPStatusAndNoGRPCStatus() throws { - var stateMachine = self.makeStateMachine(.clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - )) + var stateMachine = self.makeStateMachine( + .clientActiveServerIdle( + writeState: .one(), + pendingReadState: .init(arity: .one, messageEncoding: .disabled) + ) + ) let trailers: HPACKHeaders = [":status": "418"] stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in diff --git a/Tests/GRPCTests/GRPCCustomPayloadTests.swift b/Tests/GRPCTests/GRPCCustomPayloadTests.swift index f9d876298..92cbfa656 100644 --- a/Tests/GRPCTests/GRPCCustomPayloadTests.swift +++ b/Tests/GRPCTests/GRPCCustomPayloadTests.swift @@ -300,8 +300,9 @@ private struct CustomPayload: GRPCPayload, Equatable { init(serializedByteBuffer: inout ByteBuffer) throws { guard let messageLength = serializedByteBuffer.readInteger(as: UInt32.self), - let message = serializedByteBuffer.readString(length: Int(messageLength)), - let number = serializedByteBuffer.readInteger(as: Int64.self) else { + let message = serializedByteBuffer.readString(length: Int(messageLength)), + let number = serializedByteBuffer.readInteger(as: Int64.self) + else { throw GRPCError.DeserializationFailure() } diff --git a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift b/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift index 4e1b7b4b2..7a4e568f3 100644 --- a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHTTP2 import XCTest +@testable import GRPC + class GRPCIdleHandlerStateMachineTests: GRPCTestCase { private func makeClientStateMachine() -> GRPCIdleHandlerStateMachine { return GRPCIdleHandlerStateMachine(role: .client, logger: self.clientLogger) diff --git a/Tests/GRPCTests/GRPCIdleTests.swift b/Tests/GRPCTests/GRPCIdleTests.swift index 1c2013f81..aa0635be6 100644 --- a/Tests/GRPCTests/GRPCIdleTests.swift +++ b/Tests/GRPCTests/GRPCIdleTests.swift @@ -15,11 +15,12 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOCore import NIOPosix import XCTest +@testable import GRPC + class GRPCIdleTests: GRPCTestCase { func testClientIdleTimeout() { XCTAssertNoThrow( @@ -55,11 +56,14 @@ class GRPCIdleTests: GRPCTestCase { // Setup a state change recorder for the client. let stateRecorder = RecordingConnectivityDelegate() stateRecorder.expectChanges(3) { changes in - XCTAssertEqual(changes, [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .ready), - Change(from: .ready, to: .idle), - ]) + XCTAssertEqual( + changes, + [ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .ready), + Change(from: .ready, to: .idle), + ] + ) } // Setup a connection. diff --git a/Tests/GRPCTests/GRPCInteroperabilityTests.swift b/Tests/GRPCTests/GRPCInteroperabilityTests.swift index 14efae0fe..e86c9b452 100644 --- a/Tests/GRPCTests/GRPCInteroperabilityTests.swift +++ b/Tests/GRPCTests/GRPCInteroperabilityTests.swift @@ -250,7 +250,7 @@ class GRPCSecureInteroperabilityTests: GRPCInsecureInteroperabilityTests { super.testTimeoutOnSleepingServer() } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) class GRPCInsecureInteroperabilityAsyncTests: GRPCInsecureInteroperabilityTests { @@ -416,4 +416,4 @@ class GRPCSecureInteroperabilityAsyncTests: GRPCInsecureInteroperabilityAsyncTes super.testClientCompressedStreaming() } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/GRPCKeepaliveTests.swift b/Tests/GRPCTests/GRPCKeepaliveTests.swift index 78dbea65b..359e70c5b 100644 --- a/Tests/GRPCTests/GRPCKeepaliveTests.swift +++ b/Tests/GRPCTests/GRPCKeepaliveTests.swift @@ -15,11 +15,12 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOCore import NIOPosix import XCTest +@testable import GRPC + class GRPCClientKeepaliveTests: GRPCTestCase { func testKeepaliveTimeoutFiresBeforeConnectionIsReady() throws { // This test relates to https://github.com/grpc/grpc-swift/issues/949 diff --git a/Tests/GRPCTests/GRPCLoggerTests.swift b/Tests/GRPCTests/GRPCLoggerTests.swift index 2f390fb06..73f7ac0f8 100644 --- a/Tests/GRPCTests/GRPCLoggerTests.swift +++ b/Tests/GRPCTests/GRPCLoggerTests.swift @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import Logging import XCTest +@testable import GRPC + final class GRPCLoggerTests: GRPCTestCase { func testLogSourceIsGRPC() { let recorder = CapturingLogHandlerFactory(printWhenCaptured: false) diff --git a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift index e3a765219..8f97602f0 100644 --- a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift +++ b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift @@ -38,9 +38,9 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { private let queue = DispatchQueue(label: "io.grpc.verify-handshake") private static let p12bundleURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // (this file) - .deletingLastPathComponent() // GRPCTests - .deletingLastPathComponent() // Tests + .deletingLastPathComponent() // (this file) + .deletingLastPathComponent() // GRPCTests + .deletingLastPathComponent() // Tests .appendingPathComponent("Sources") .appendingPathComponent("GRPCSampleData") .appendingPathComponent("bundle") @@ -106,7 +106,8 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { } private func startServer(_ builder: Server.Builder) throws { - self.server = try builder + self.server = + try builder .withServiceProviders([EchoProvider()]) .withLogger(self.serverLogger) .bind(host: "127.0.0.1", port: 0) @@ -114,7 +115,8 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { } private func startClient(_ builder: ClientConnection.Builder) { - self.client = builder + self.client = + builder .withBackgroundActivityLogger(self.clientLogger) .withConnectionReestablishment(enabled: false) .connect(host: "127.0.0.1", port: self.server.channel.localAddress!.port!) @@ -208,5 +210,5 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { } } -#endif // canImport(Network) -#endif // canImport(NIOSSL) +#endif // canImport(Network) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/GRPCPingHandlerTests.swift b/Tests/GRPCTests/GRPCPingHandlerTests.swift index 1434c4273..caab4face 100644 --- a/Tests/GRPCTests/GRPCPingHandlerTests.swift +++ b/Tests/GRPCTests/GRPCPingHandlerTests.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHTTP2 import XCTest +@testable import GRPC + class GRPCPingHandlerTests: GRPCTestCase { var pingHandler: PingHandler! @@ -319,11 +321,13 @@ class GRPCPingHandlerTests: GRPCTestCase { response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) XCTAssertEqual( response, - .reply(HTTP2Frame.FramePayload.goAway( - lastStreamID: .rootStream, - errorCode: .enhanceYourCalm, - opaqueData: nil - )) + .reply( + HTTP2Frame.FramePayload.goAway( + lastStreamID: .rootStream, + errorCode: .enhanceYourCalm, + opaqueData: nil + ) + ) ) } diff --git a/Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift b/Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift index 59da4837f..e5dbd836a 100644 --- a/Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift +++ b/Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHTTP2 import NIOTLS import XCTest +@testable import GRPC + class GRPCServerPipelineConfiguratorTests: GRPCTestCase { private var channel: EmbeddedChannel! @@ -125,7 +127,7 @@ class GRPCServerPipelineConfiguratorTests: GRPCTestCase { self.assertConfigurator(isPresent: false) self.assertHTTP2Handler(isPresent: true) } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) func testHTTP2SetupViaBytes() { self.setUp(tls: false) @@ -201,10 +203,10 @@ class GRPCServerPipelineConfiguratorTests: GRPCTestCase { // A SETTINGS frame MUST follow the connection preface. Append one so that the HTTP/2 handler // responds with its initial settings (and we validate that we forward frames once configuring). let emptySettingsFrameBytes: [UInt8] = [ - 0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) - 0x04, // 1-byte frame type (SETTINGS) - 0x00, // 1-byte flags (none) - 0x00, 0x00, 0x00, 0x00, // 4-byte stream identifier + 0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) + 0x04, // 1-byte frame type (SETTINGS) + 0x00, // 1-byte flags (none) + 0x00, 0x00, 0x00, 0x00, // 4-byte stream identifier ] bytes.writeBytes(emptySettingsFrameBytes) @@ -267,5 +269,5 @@ class GRPCServerPipelineConfiguratorTests: GRPCTestCase { self.assertConfigurator(isPresent: false) self.assertHTTP2Handler(isPresent: true) } - #endif // canImport(NIOSSL) + #endif // canImport(NIOSSL) } diff --git a/Tests/GRPCTests/GRPCStatusCodeTests.swift b/Tests/GRPCTests/GRPCStatusCodeTests.swift index b2d97fd33..58d2eec79 100644 --- a/Tests/GRPCTests/GRPCStatusCodeTests.swift +++ b/Tests/GRPCTests/GRPCStatusCodeTests.swift @@ -15,7 +15,6 @@ */ import EchoModel import Foundation -@testable import GRPC import Logging import NIOCore import NIOEmbedded @@ -24,6 +23,8 @@ import NIOHTTP1 import NIOHTTP2 import XCTest +@testable import GRPC + class GRPCStatusCodeTests: GRPCTestCase { var channel: EmbeddedChannel! @@ -65,7 +66,8 @@ class GRPCStatusCodeTests: GRPCTestCase { .writeInbound(self.headersFramePayload(status: status)) ) { error in guard let withContext = error as? GRPCError.WithContext, - let invalidHTTPStatus = withContext.error as? GRPCError.InvalidHTTPStatus else { + let invalidHTTPStatus = withContext.error as? GRPCError.InvalidHTTPStatus + else { XCTFail("Unexpected error: \(error)") return } diff --git a/Tests/GRPCTests/GRPCStatusTests.swift b/Tests/GRPCTests/GRPCStatusTests.swift index 29dd767f1..f32ea8050 100644 --- a/Tests/GRPCTests/GRPCStatusTests.swift +++ b/Tests/GRPCTests/GRPCStatusTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import XCTest +@testable import GRPC + class GRPCStatusTests: GRPCTestCase { func testStatusDescriptionWithoutMessage() { XCTAssertEqual( @@ -58,11 +60,13 @@ class GRPCStatusTests: GRPCTestCase { 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 - )) + String( + describing: GRPCStatus( + code: .internalError, + message: "unknown error processing request", + cause: cause + ) + ) ) } @@ -73,11 +77,13 @@ class GRPCStatusTests: GRPCTestCase { let cause = UnderlyingError() XCTAssertEqual( "internal error (13), cause: \(cause.description)", - String(describing: GRPCStatus( - code: .internalError, - message: nil, - cause: cause - )) + String( + describing: GRPCStatus( + code: .internalError, + message: nil, + cause: cause + ) + ) ) } diff --git a/Tests/GRPCTests/GRPCTestCase.swift b/Tests/GRPCTests/GRPCTestCase.swift index a9d68e94d..0adba1cd3 100644 --- a/Tests/GRPCTests/GRPCTestCase.swift +++ b/Tests/GRPCTests/GRPCTestCase.swift @@ -38,7 +38,7 @@ class GRPCTestCase: XCTestCase { override func tearDown() { // Only print logs when there's a failure and we're *not* always logging (when we are always // logging, logs will be printed as they're caught). - if !GRPCTestCase.alwaysLog, (self.testRun.map { $0.totalFailureCount > 0 } ?? false) { + if !GRPCTestCase.alwaysLog, self.testRun.map { $0.totalFailureCount > 0 } ?? false { let logs = self.capturedLogs() self.printCapturedLogs(logs) } diff --git a/Tests/GRPCTests/GRPCTimeoutTests.swift b/Tests/GRPCTests/GRPCTimeoutTests.swift index 8ff2bdcba..a6f260f48 100644 --- a/Tests/GRPCTests/GRPCTimeoutTests.swift +++ b/Tests/GRPCTests/GRPCTimeoutTests.swift @@ -15,10 +15,11 @@ */ import Dispatch import Foundation -@testable import GRPC import NIOCore import XCTest +@testable import GRPC + class GRPCTimeoutTests: GRPCTestCase { func testRoundingNegativeTimeout() { let timeout = GRPCTimeout(rounding: -10, unit: .seconds) diff --git a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift index 1e1b81a82..ab73141d2 100644 --- a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift +++ b/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import struct Foundation.Data -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHPACK @@ -22,6 +21,10 @@ import NIOHTTP1 import NIOHTTP2 import XCTest +import struct Foundation.Data + +@testable import GRPC + class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { private func writeTrailers(_ trailers: HPACKHeaders, into buffer: inout ByteBuffer) { buffer.writeInteger(UInt8(0x80)) diff --git a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift index 8d644b943..f35163888 100644 --- a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift +++ b/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift @@ -14,13 +14,14 @@ * limitations under the License. */ -@testable import GRPC import NIOCore import NIOHPACK import NIOHTTP1 import NIOHTTP2 import XCTest +@testable import GRPC + final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase { fileprivate typealias StateMachine = GRPCWebToHTTP2ServerCodec.StateMachine @@ -408,7 +409,9 @@ final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase { // More writes should be told to fail their promise. state.processOutbound( - framePayload: .headers(.init(headers: .init())), promise: nil, allocator: self.allocator + framePayload: .headers(.init(headers: .init())), + promise: nil, + allocator: self.allocator ).assertCompletePromise { error in XCTAssertNotNil(error) } @@ -423,10 +426,16 @@ final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase { } func test_handleMultipleRequests() { - func sendRequestHead(_ state: inout StateMachine, contentType: ContentType) -> StateMachine - .Action { + func sendRequestHead( + _ state: inout StateMachine, + contentType: ContentType + ) + -> StateMachine + .Action + { let requestHead = self.makeRequestHead( - uri: "/echo", headers: ["content-type": contentType.canonicalValue] + uri: "/echo", + headers: ["content-type": contentType.canonicalValue] ) return state.processInbound(serverRequestPart: requestHead, allocator: self.allocator) } diff --git a/Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift b/Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift index d397c525f..50a5bd637 100644 --- a/Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift +++ b/Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift @@ -15,12 +15,13 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOCore import NIOHTTP2 import NIOPosix import XCTest +@testable import GRPC + class HTTP2MaxConcurrentStreamsTests: GRPCTestCase { enum Constants { static let testTimeout: TimeInterval = 10 @@ -57,11 +58,12 @@ class HTTP2MaxConcurrentStreamsTests: GRPCTestCase { var clientStreamingCalls = (0 ..< Constants.testNumberOfConcurrentStreams) - .map { _ in echoClient.collect() } + .map { _ in echoClient.collect() } let allMessagesSentExpectation = self.expectation(description: "all messages sent") - let sendMessageFutures = clientStreamingCalls + let sendMessageFutures = + clientStreamingCalls .map { $0.sendMessage(.with { $0.text = "Hi!" }) } EventLoopFuture diff --git a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift b/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift index 280e0f4a7..a0d7a8b32 100644 --- a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift +++ b/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift @@ -14,7 +14,6 @@ * limitations under the License. */ import EchoImplementation -@testable import GRPC import NIOCore import NIOEmbedded import NIOHPACK @@ -22,6 +21,8 @@ import NIOHTTP2 import NIOPosix import XCTest +@testable import GRPC + class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { typealias StateMachine = HTTP2ToRawGRPCStateMachine typealias State = StateMachine.State @@ -344,10 +345,15 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { // metadata. Send back headers to test this. assertThat(action, .is(.configure())) let sendAction = machine.send(headers: [:]) - assertThat(sendAction, .success(.contains( - "grpc-accept-encoding", - ["deflate", "identity", "gzip"] - ))) + assertThat( + sendAction, + .success( + .contains( + "grpc-accept-encoding", + ["deflate", "identity", "gzip"] + ) + ) + ) } func testReceiveHeadersWithIdentityCompressionWhenCompressionIsDisabled() { @@ -686,7 +692,7 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { machine.send(buffer: buffer, compress: false, promise: nil).assertSuccess() let (framedBuffer, promise) = try XCTUnwrap(machine.nextResponse()) - XCTAssertNil(promise) // Didn't provide a promise. + XCTAssertNil(promise) // Didn't provide a promise. framedBuffer.assertSuccess() // No more responses. @@ -710,10 +716,13 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { // MARK: Send End func testSendEndWhenResponseStreamIsIdle() { - for (state, closed) in zip([ - DesiredState.requestOpenResponseIdle(pipelineConfigured: true), - DesiredState.requestClosedResponseIdle(pipelineConfigured: true), - ], [false, true]) { + for (state, closed) in zip( + [ + DesiredState.requestOpenResponseIdle(pipelineConfigured: true), + DesiredState.requestClosedResponseIdle(pipelineConfigured: true), + ], + [false, true] + ) { var machine = self.makeStateMachine(state: state) let action1 = machine.send(status: .ok, trailers: [:]) // This'll be a trailers-only response. @@ -730,10 +739,13 @@ class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { } func testSendEndWhenResponseStreamIsOpen() { - for (state, closed) in zip([ - DesiredState.requestOpenResponseOpen, - DesiredState.requestClosedResponseOpen, - ], [false, true]) { + for (state, closed) in zip( + [ + DesiredState.requestOpenResponseOpen, + DesiredState.requestClosedResponseOpen, + ], + [false, true] + ) { var machine = self.makeStateMachine(state: state) let action = machine.send( status: GRPCStatus(code: .ok, message: "ok"), diff --git a/Tests/GRPCTests/HTTPVersionParserTests.swift b/Tests/GRPCTests/HTTPVersionParserTests.swift index 2dc36979d..a282ef08c 100644 --- a/Tests/GRPCTests/HTTPVersionParserTests.swift +++ b/Tests/GRPCTests/HTTPVersionParserTests.swift @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import XCTest +@testable import GRPC + class HTTPVersionParserTests: GRPCTestCase { private let preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" diff --git a/Tests/GRPCTests/HeaderNormalizationTests.swift b/Tests/GRPCTests/HeaderNormalizationTests.swift index 42f2a8a10..cf7e079e5 100644 --- a/Tests/GRPCTests/HeaderNormalizationTests.swift +++ b/Tests/GRPCTests/HeaderNormalizationTests.swift @@ -15,13 +15,14 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOCore import NIOHPACK import NIOHTTP1 import NIOPosix import XCTest +@testable import GRPC + class EchoMetadataValidator: Echo_EchoProvider { let interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil @@ -31,11 +32,13 @@ class EchoMetadataValidator: Echo_EchoProvider { ) { // Header lookup is case-insensitive so we need to pull out the values we know the client sent // as custom-metadata and then compare a new set of headers. - let customMetadata = HPACKHeaders(headers.filter { _, value, _ in - value == "client" - }.map { - ($0.name, $0.value) - }) + let customMetadata = HPACKHeaders( + headers.filter { _, value, _ in + value == "client" + }.map { + ($0.name, $0.value) + } + ) XCTAssertEqual(customMetadata, ["client": "client"], line: line) } diff --git a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift index 9676366a1..a041fc1f4 100644 --- a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift +++ b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift @@ -15,12 +15,14 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOCore import NIOPosix -import protocol SwiftProtobuf.Message import XCTest +import protocol SwiftProtobuf.Message + +@testable import GRPC + final class InterceptedRPCCancellationTests: GRPCTestCase { func testCancellationWithinInterceptedRPC() throws { // This test validates that when using interceptors to replay an RPC that the lifecycle of diff --git a/Tests/GRPCTests/InterceptorsTests.swift b/Tests/GRPCTests/InterceptorsTests.swift index 4f53bd084..e411fdec5 100644 --- a/Tests/GRPCTests/InterceptorsTests.swift +++ b/Tests/GRPCTests/InterceptorsTests.swift @@ -161,15 +161,17 @@ class HelloWorldProvider: Helloworld_GreeterProvider { extension HelloWorldClientInterceptorFactory: @unchecked Sendable {} private class HelloWorldClientInterceptorFactory: - Helloworld_GreeterClientInterceptorFactoryProtocol { + Helloworld_GreeterClientInterceptorFactoryProtocol +{ var client: Helloworld_GreeterNIOClient init(client: Helloworld_GreeterNIOClient) { self.client = client } - func makeSayHelloInterceptors( - ) -> [ClientInterceptor] { + func makeSayHelloInterceptors() -> [ClientInterceptor< + Helloworld_HelloRequest, Helloworld_HelloReply + >] { return [NotReallyAuthClientInterceptor(client: self.client)] } } @@ -185,7 +187,8 @@ class RemoteAddressExistsInterceptor: ServerInterceptor: - ServerInterceptor { + ServerInterceptor +{ override func receive( _ part: GRPCServerRequestPart, context: ServerInterceptorContext @@ -209,14 +212,16 @@ class NotReallyAuthServerInterceptor: } final class HelloWorldServerInterceptorFactory: Helloworld_GreeterServerInterceptorFactoryProtocol { - func makeSayHelloInterceptors( - ) -> [ServerInterceptor] { + func makeSayHelloInterceptors() -> [ServerInterceptor< + Helloworld_HelloRequest, Helloworld_HelloReply + >] { return [RemoteAddressExistsInterceptor(), NotReallyAuthServerInterceptor()] } } class NotReallyAuthClientInterceptor: - ClientInterceptor { + ClientInterceptor +{ private let client: Helloworld_GreeterNIOClient private enum State { diff --git a/Tests/GRPCTests/LazyEventLoopPromiseTests.swift b/Tests/GRPCTests/LazyEventLoopPromiseTests.swift index a042e1fa9..6e771631b 100644 --- a/Tests/GRPCTests/LazyEventLoopPromiseTests.swift +++ b/Tests/GRPCTests/LazyEventLoopPromiseTests.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import XCTest +@testable import GRPC + class LazyEventLoopPromiseTests: GRPCTestCase { func testGetFutureAfterSuccess() { let loop = EmbeddedEventLoop() diff --git a/Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift b/Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift index c46200451..99b3f8096 100644 --- a/Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift +++ b/Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import Logging import NIOCore import XCTest +@testable import GRPC + class LengthPrefixedMessageReaderTests: GRPCTestCase { var reader: LengthPrefixedMessageReader! @@ -37,8 +39,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { final let twoByteMessage: [UInt8] = [0x01, 0x02] func lengthPrefixedTwoByteMessage(withCompression compression: Bool = false) -> [UInt8] { return [ - compression ? 0x01 : 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) + compression ? 0x01 : 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) ] + self.twoByteMessage } @@ -76,8 +78,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testNextMessageReturnsMessageForZeroLengthMessage() throws { let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x00, // 4-byte message length (0) + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, 0x00, // 4-byte message length (0) // 0-byte message ] @@ -89,13 +91,13 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testNextMessageDeliveredAcrossMultipleByteBuffers() throws { let firstBytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, // first 3 bytes of 4-byte message length + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, // first 3 bytes of 4-byte message length ] let secondBytes: [UInt8] = [ - 0x02, // fourth byte of 4-byte message length (2) - 0xF0, 0xBA, // 2-byte message + 0x02, // fourth byte of 4-byte message length (2) + 0xF0, 0xBA, // 2-byte message ] var firstBuffer = self.byteBuffer(withBytes: firstBytes) @@ -109,17 +111,17 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testNextMessageWhenMultipleMessagesAreBuffered() throws { let bytes: [UInt8] = [ // 1st message - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) - 0x0F, 0x00, // 2-byte message + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) + 0x0F, 0x00, // 2-byte message // 2nd message - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x04, // 4-byte message length (4) - 0xDE, 0xAD, 0xBE, 0xEF, // 4-byte message + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, 0x04, // 4-byte message length (4) + 0xDE, 0xAD, 0xBE, 0xEF, // 4-byte message // 3rd message - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x01, // 4-byte message length (1) - 0x01, // 1-byte message + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, 0x01, // 4-byte message length (1) + 0x01, // 1-byte message ] var buffer = self.byteBuffer(withBytes: bytes) @@ -135,7 +137,7 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testNextMessageReturnsNilWhenNoMessageLengthIsAvailable() throws { let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag + 0x00 // 1-byte compression flag ] var buffer = self.byteBuffer(withBytes: bytes) @@ -145,8 +147,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { // Ensure we can read a message when the rest of the bytes are delivered let restOfBytes: [UInt8] = [ - 0x00, 0x00, 0x00, 0x01, // 4-byte message length (1) - 0x00, // 1-byte message + 0x00, 0x00, 0x00, 0x01, // 4-byte message length (1) + 0x00, // 1-byte message ] var secondBuffer = self.byteBuffer(withBytes: restOfBytes) @@ -156,8 +158,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testNextMessageReturnsNilWhenNotAllMessageLengthIsAvailable() throws { let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, // 2-bytes of message length (should be 4) + 0x00, // 1-byte compression flag + 0x00, 0x00, // 2-bytes of message length (should be 4) ] var buffer = self.byteBuffer(withBytes: bytes) @@ -167,8 +169,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { // Ensure we can read a message when the rest of the bytes are delivered let restOfBytes: [UInt8] = [ - 0x00, 0x01, // 4-byte message length (1) - 0x00, // 1-byte message + 0x00, 0x01, // 4-byte message length (1) + 0x00, // 1-byte message ] var secondBuffer = self.byteBuffer(withBytes: restOfBytes) @@ -178,8 +180,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testNextMessageReturnsNilWhenNoMessageBytesAreAvailable() throws { let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) ] var buffer = self.byteBuffer(withBytes: bytes) @@ -195,9 +197,9 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testNextMessageReturnsNilWhenNotAllMessageBytesAreAvailable() throws { let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) - 0x00, // 1-byte of message + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) + 0x00, // 1-byte of message ] var buffer = self.byteBuffer(withBytes: bytes) @@ -207,7 +209,7 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { // Ensure we can read a message when the rest of the bytes are delivered let restOfBytes: [UInt8] = [ - 0x01, // final byte of message + 0x01 // final byte of message ] var secondBuffer = self.byteBuffer(withBytes: restOfBytes) @@ -220,7 +222,8 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { // compression flag is set as it indicates a lack of message encoding header. XCTAssertNil(self.reader.compression) - var buffer = self + var buffer = + self .byteBuffer(withBytes: self.lengthPrefixedTwoByteMessage(withCompression: true)) self.reader.append(buffer: &buffer) @@ -250,10 +253,11 @@ class LengthPrefixedMessageReaderTests: GRPCTestCase { func testExcessiveBytesAreDiscarded() throws { // We're going to use a 1kB message here for ease of testing. let message = Array(repeating: UInt8(0), count: 1024) - let largeMessage: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x04, 0x00, // 4-byte message length (1024) - ] + message + let largeMessage: [UInt8] = + [ + 0x00, // 1-byte compression flag + 0x00, 0x00, 0x04, 0x00, // 4-byte message length (1024) + ] + message var buffer = self.byteBuffer(withBytes: largeMessage) buffer.writeBytes(largeMessage) buffer.writeBytes(largeMessage) diff --git a/Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift b/Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift index ca0ebb844..d21c39f67 100644 --- a/Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift +++ b/Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift @@ -13,22 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import XCTest +@testable import GRPC + class MessageEncodingHeaderValidatorTests: GRPCTestCase { func testSupportedAlgorithm() throws { let validator = MessageEncodingHeaderValidator( - encoding: .enabled(.init( - enabledAlgorithms: [.deflate, .gzip], - decompressionLimit: .absolute(10) - )) + encoding: .enabled( + .init( + enabledAlgorithms: [.deflate, .gzip], + decompressionLimit: .absolute(10) + ) + ) ) let validation = validator.validate(requestEncoding: "gzip") switch validation { case .supported(.gzip, .absolute(10), acceptEncoding: []): - () // Expected + () // Expected default: XCTFail("Expected .supported but was \(validation)") } @@ -42,7 +46,7 @@ class MessageEncodingHeaderValidatorTests: GRPCTestCase { let validation = validator.validate(requestEncoding: "gzip") switch validation { case .supported(.gzip, .absolute(10), acceptEncoding: ["deflate", "gzip"]): - () // Expected + () // Expected default: XCTFail("Expected .supported but was \(validation)") } @@ -54,7 +58,7 @@ class MessageEncodingHeaderValidatorTests: GRPCTestCase { let validation = validator.validate(requestEncoding: "gzip") switch validation { case .unsupported(requestEncoding: "gzip", acceptEncoding: []): - () // Expected + () // Expected default: XCTFail("Expected .unsupported but was \(validation)") } @@ -63,13 +67,13 @@ class MessageEncodingHeaderValidatorTests: GRPCTestCase { func testUnsupportedButEnabled() throws { let validator = MessageEncodingHeaderValidator( encoding: - .enabled(.init(enabledAlgorithms: [.gzip], decompressionLimit: .absolute(10))) + .enabled(.init(enabledAlgorithms: [.gzip], decompressionLimit: .absolute(10))) ) let validation = validator.validate(requestEncoding: "not-supported") switch validation { case .unsupported(requestEncoding: "not-supported", acceptEncoding: ["gzip"]): - () // Expected + () // Expected default: XCTFail("Expected .unsupported but was \(validation)") } @@ -81,7 +85,7 @@ class MessageEncodingHeaderValidatorTests: GRPCTestCase { let validation = validator.validate(requestEncoding: nil) switch validation { case .noCompression: - () // Expected + () // Expected default: XCTFail("Expected .noCompression but was \(validation)") } @@ -90,16 +94,18 @@ class MessageEncodingHeaderValidatorTests: GRPCTestCase { func testNoCompressionWhenEnabled() throws { let validator = MessageEncodingHeaderValidator( encoding: - .enabled(.init( - enabledAlgorithms: CompressionAlgorithm.all, - decompressionLimit: .absolute(10) - )) + .enabled( + .init( + enabledAlgorithms: CompressionAlgorithm.all, + decompressionLimit: .absolute(10) + ) + ) ) let validation = validator.validate(requestEncoding: nil) switch validation { case .noCompression: - () // Expected + () // Expected default: XCTFail("Expected .noCompression but was \(validation)") } diff --git a/Tests/GRPCTests/MutualTLSTests.swift b/Tests/GRPCTests/MutualTLSTests.swift index 4a9346cfb..4282bba65 100644 --- a/Tests/GRPCTests/MutualTLSTests.swift +++ b/Tests/GRPCTests/MutualTLSTests.swift @@ -194,7 +194,7 @@ class MutualTLSTests: GRPCTestCase { certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)], privateKey: .privateKey(SamplePrivateKey.client), trustRoots: .certificates([ - SampleCertificate.otherCA.certificate, + SampleCertificate.otherCA.certificate ]), certificateVerification: .fullVerification ) @@ -211,7 +211,7 @@ class MutualTLSTests: GRPCTestCase { certificateChain: [.certificate(SampleCertificate.server.certificate)], privateKey: .privateKey(SamplePrivateKey.server), trustRoots: .certificates([ - SampleCertificate.ca.certificate, + SampleCertificate.ca.certificate ]), certificateVerification: .noHostnameVerification ) @@ -269,4 +269,4 @@ class MutualTLSTests: GRPCTestCase { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/OneOrManyQueueTests.swift b/Tests/GRPCTests/OneOrManyQueueTests.swift index d68be6f71..63c8acd0a 100644 --- a/Tests/GRPCTests/OneOrManyQueueTests.swift +++ b/Tests/GRPCTests/OneOrManyQueueTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import XCTest +@testable import GRPC + internal final class OneOrManyQueueTests: GRPCTestCase { func testIsEmpty() { XCTAssertTrue(OneOrManyQueue().isEmpty) diff --git a/Tests/GRPCTests/PlatformSupportTests.swift b/Tests/GRPCTests/PlatformSupportTests.swift index d5ca7eca5..02f24dc54 100644 --- a/Tests/GRPCTests/PlatformSupportTests.swift +++ b/Tests/GRPCTests/PlatformSupportTests.swift @@ -14,12 +14,13 @@ * limitations under the License. */ import Foundation -@testable import GRPC import NIOCore import NIOPosix import NIOTransportServices import XCTest +@testable import GRPC + #if canImport(Network) import Network #endif diff --git a/Tests/GRPCTests/RequestIDProviderTests.swift b/Tests/GRPCTests/RequestIDProviderTests.swift index 88dbfaa65..2a4c8dc10 100644 --- a/Tests/GRPCTests/RequestIDProviderTests.swift +++ b/Tests/GRPCTests/RequestIDProviderTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import XCTest +@testable import GRPC + class RequestIDProviderTests: GRPCTestCase { func testUserDefined() { let provider = CallOptions.RequestIDProvider.userDefined("foo") diff --git a/Tests/GRPCTests/SampleCertificate+Assertions.swift b/Tests/GRPCTests/SampleCertificate+Assertions.swift index 86e7938cc..b1a03e0d1 100644 --- a/Tests/GRPCTests/SampleCertificate+Assertions.swift +++ b/Tests/GRPCTests/SampleCertificate+Assertions.swift @@ -29,4 +29,4 @@ extension SampleCertificate { ) } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ServerErrorDelegateTests.swift b/Tests/GRPCTests/ServerErrorDelegateTests.swift index 8b69ad5f3..05d72ae68 100644 --- a/Tests/GRPCTests/ServerErrorDelegateTests.swift +++ b/Tests/GRPCTests/ServerErrorDelegateTests.swift @@ -16,7 +16,6 @@ import EchoImplementation import EchoModel import Foundation -@testable import GRPC import Logging import NIOCore import NIOEmbedded @@ -24,6 +23,8 @@ import NIOHPACK import NIOHTTP2 import XCTest +@testable import GRPC + private class ServerErrorDelegateMock: ServerErrorDelegate { private let transformLibraryErrorHandler: (Error) -> (GRPCStatusAndTrailers?) @@ -104,8 +105,9 @@ class ServerErrorDelegateTests: GRPCTestCase { .testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Expand") } - func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_bidirectionalStreaming( - ) throws { + func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_bidirectionalStreaming() + throws + { try self .testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Update") } diff --git a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift b/Tests/GRPCTests/ServerFuzzingRegressionTests.swift index 444dca022..38b0e2bee 100644 --- a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift +++ b/Tests/GRPCTests/ServerFuzzingRegressionTests.swift @@ -14,18 +14,19 @@ * limitations under the License. */ import EchoImplementation -import struct Foundation.Data -import struct Foundation.URL import GRPC import NIOCore import NIOEmbedded import XCTest +import struct Foundation.Data +import struct Foundation.URL + final class ServerFuzzingRegressionTests: GRPCTestCase { private static let failCasesURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // ServerFuzzingRegressionTests.swift - .deletingLastPathComponent() // GRPCTests - .deletingLastPathComponent() // Tests + .deletingLastPathComponent() // ServerFuzzingRegressionTests.swift + .deletingLastPathComponent() // GRPCTests + .deletingLastPathComponent() // Tests .appendingPathComponent("FuzzTesting") .appendingPathComponent("FailCases") diff --git a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift index 1f594278e..d5f10a71a 100644 --- a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHPACK import XCTest +@testable import GRPC + class ServerInterceptorPipelineTests: GRPCTestCase { override func setUp() { super.setUp() @@ -123,7 +125,8 @@ class ServerInterceptorPipelineTests: GRPCTestCase { } internal class RecordingServerInterceptor: - ServerInterceptor { + ServerInterceptor +{ var requestParts: [GRPCServerRequestPart] = [] var responseParts: [GRPCServerResponsePart] = [] diff --git a/Tests/GRPCTests/ServerInterceptorTests.swift b/Tests/GRPCTests/ServerInterceptorTests.swift index df77d82cf..34a8cc760 100644 --- a/Tests/GRPCTests/ServerInterceptorTests.swift +++ b/Tests/GRPCTests/ServerInterceptorTests.swift @@ -15,7 +15,6 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import HelloWorldModel import NIOCore import NIOEmbedded @@ -23,6 +22,8 @@ import NIOHTTP1 import SwiftProtobuf import XCTest +@testable import GRPC + extension GRPCServerHandlerProtocol { fileprivate func receiveRequest(_ request: Echo_EchoRequest) { let serializer = ProtobufSerializer() @@ -45,7 +46,8 @@ class ServerInterceptorTests: GRPCTestCase { } private func makeRecordingInterceptor() - -> RecordingServerInterceptor { + -> RecordingServerInterceptor + { return .init() } @@ -225,8 +227,8 @@ class ExtraRequestPartEmitter: ServerInterceptor) - -> EventLoopFuture<(StreamEvent) -> Void> { + func collect( + context: UnaryResponseCallContext + ) -> EventLoopFuture<(StreamEvent) -> Void> { return context.eventLoop.makeFailedFuture(thrownError) } - func update(context: StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> { + func update( + context: StreamingResponseCallContext + ) -> EventLoopFuture<(StreamEvent) -> Void> { return context.eventLoop.makeFailedFuture(thrownError) } } @@ -85,13 +88,15 @@ class DelayedThrowingEchoProvider: Echo_EchoProvider { return context.eventLoop.makeFailedFuture(thrownError, delay: 0.01) } - func collect(context: UnaryResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> { + func collect( + context: UnaryResponseCallContext + ) -> EventLoopFuture<(StreamEvent) -> Void> { return context.eventLoop.makeFailedFuture(thrownError, delay: 0.01) } - func update(context: StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> { + func update( + context: StreamingResponseCallContext + ) -> EventLoopFuture<(StreamEvent) -> Void> { return context.eventLoop.makeFailedFuture(thrownError, delay: 0.01) } } @@ -107,16 +112,18 @@ class ErrorReturningEchoProvider: ImmediateThrowingEchoProvider { return context.eventLoop.makeSucceededFuture(thrownError) } - override func collect(context: UnaryResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> { + override func collect( + context: UnaryResponseCallContext + ) -> EventLoopFuture<(StreamEvent) -> Void> { return context.eventLoop.makeSucceededFuture({ _ in context.responseStatus = thrownError context.responsePromise.succeed(Echo_EchoResponse()) }) } - override func update(context: StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> { + override func update( + context: StreamingResponseCallContext + ) -> EventLoopFuture<(StreamEvent) -> Void> { return context.eventLoop.makeSucceededFuture({ _ in context.statusPromise.succeed(thrownError) }) @@ -176,8 +183,11 @@ class ServerThrowingTests: EchoTestCaseBase { } func testServerStreaming() throws { - let call = client - .expand(Echo_EchoRequest(text: "foo")) { XCTFail("no message expected, got \($0)") } + let call = client.expand( + Echo_EchoRequest(text: "foo") + ) { + XCTFail("no message expected, got \($0)") + } // Nothing to throw here, but the `status` should be the expected error. XCTAssertEqual(self.expectedError, try call.status.wait()) let trailers = try call.trailingMetadata.wait() @@ -246,8 +256,10 @@ class ClientThrowingWhenServerReturningErrorTests: ServerThrowingTests { class ServerErrorTransformingTests: ServerThrowingTests { override var expectedError: GRPCStatus { return transformedError } override var expectedMetadata: HPACKHeaders? { - return HPACKHeaders([("grpc-status", "10"), ("grpc-message", "transformed error"), - ("transformed", "header")]) + return HPACKHeaders([ + ("grpc-status", "10"), ("grpc-message", "transformed error"), + ("transformed", "header"), + ]) } override func makeErrorDelegate() -> ServerErrorDelegate? { return ErrorTransformingDelegate() } diff --git a/Tests/GRPCTests/ServerWebTests.swift b/Tests/GRPCTests/ServerWebTests.swift index a2ebef8b3..254783357 100644 --- a/Tests/GRPCTests/ServerWebTests.swift +++ b/Tests/GRPCTests/ServerWebTests.swift @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import EchoModel import Foundation +import NIOCore +import XCTest + +@testable import GRPC + #if canImport(FoundationNetworking) import FoundationNetworking #endif -import EchoModel -@testable import GRPC -import NIOCore -import XCTest // Only test Unary and ServerStreaming, as ClientStreaming is not // supported in HTTP1. @@ -80,8 +83,8 @@ class ServerWebTests: EchoTestCaseBase { extension ServerWebTests { func testUnary() { let message = "hello, world!" - let expectedData = self.gRPCEncodedEchoRequest("Swift echo get: \(message)") + self - .gRPCWebTrailers() + let expectedData = + self.gRPCEncodedEchoRequest("Swift echo get: \(message)") + self.gRPCWebTrailers() let expectedResponse = expectedData.base64EncodedString() let completionHandlerExpectation = expectation(description: "completion handler called") @@ -134,8 +137,8 @@ extension ServerWebTests { for i in 0 ..< numberOfRequests { let message = "foo \(i)" - let expectedData = self.gRPCEncodedEchoRequest("Swift echo get: \(message)") + self - .gRPCWebTrailers() + let expectedData = + self.gRPCEncodedEchoRequest("Swift echo get: \(message)") + self.gRPCWebTrailers() let expectedResponse = expectedData.base64EncodedString() self.sendOverHTTP1(rpcMethod: "Get", message: message) { data, error in XCTAssertNil(error) diff --git a/Tests/GRPCTests/StopwatchTests.swift b/Tests/GRPCTests/StopwatchTests.swift index 7b9b90eb5..4a5e46709 100644 --- a/Tests/GRPCTests/StopwatchTests.swift +++ b/Tests/GRPCTests/StopwatchTests.swift @@ -14,9 +14,10 @@ * limitations under the License. */ import Foundation -@testable import GRPC import XCTest +@testable import GRPC + class StopwatchTests: GRPCTestCase { func testElapsed() { var time: TimeInterval = 0.0 diff --git a/Tests/GRPCTests/TestClientExample.swift b/Tests/GRPCTests/TestClientExample.swift index 7a96bd992..861152505 100644 --- a/Tests/GRPCTests/TestClientExample.swift +++ b/Tests/GRPCTests/TestClientExample.swift @@ -218,10 +218,12 @@ extension FakeResponseStreamExampleTests { let get = self.client.get(.with { $0.text = "Hello!" }) // Send the response as well as some trailing metadata. - XCTAssertNoThrow(try getResponseStream.sendMessage( - .with { $0.text = "Goodbye!" }, - trailingMetadata: ["bar": "baz"] - )) + XCTAssertNoThrow( + try getResponseStream.sendMessage( + .with { $0.text = "Goodbye!" }, + trailingMetadata: ["bar": "baz"] + ) + ) // Check the response values: XCTAssertEqual(try get.response.wait(), .with { $0.text = "Goodbye!" }) @@ -406,11 +408,14 @@ extension FakeResponseStreamExampleTests { update.sendEnd(promise: nil) // Check the expected request parts. - XCTAssertEqual(requestParts, [ - .metadata(["foo": "bar"]), - .message(.with { $0.text = "foo" }), - .end, - ]) + XCTAssertEqual( + requestParts, + [ + .metadata(["foo": "bar"]), + .message(.with { $0.text = "foo" }), + .end, + ] + ) // Send close from the server. XCTAssertNoThrow(try updateResponseStream.sendEnd()) diff --git a/Tests/GRPCTests/TimeLimitTests.swift b/Tests/GRPCTests/TimeLimitTests.swift index 2cf7ad0b2..003de9272 100644 --- a/Tests/GRPCTests/TimeLimitTests.swift +++ b/Tests/GRPCTests/TimeLimitTests.swift @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import XCTest +@testable import GRPC + class TimeLimitTests: GRPCTestCase { func testTimeout() { XCTAssertEqual(TimeLimit.timeout(.seconds(42)).timeout, .seconds(42)) diff --git a/Tests/GRPCTests/UnaryServerHandlerTests.swift b/Tests/GRPCTests/UnaryServerHandlerTests.swift index 2dcd06641..40648e9b5 100644 --- a/Tests/GRPCTests/UnaryServerHandlerTests.swift +++ b/Tests/GRPCTests/UnaryServerHandlerTests.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHPACK import XCTest +@testable import GRPC + // MARK: - Utils final class ResponseRecorder: GRPCServerResponseWriter { diff --git a/Tests/GRPCTests/WebCORSHandlerTests.swift b/Tests/GRPCTests/WebCORSHandlerTests.swift index e64d06b6f..7a347b51c 100644 --- a/Tests/GRPCTests/WebCORSHandlerTests.swift +++ b/Tests/GRPCTests/WebCORSHandlerTests.swift @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOEmbedded import NIOHTTP1 import XCTest +@testable import GRPC + internal final class WebCORSHandlerTests: XCTestCase { struct PreflightRequestSpec { var configuration: Server.Configuration.CORS diff --git a/Tests/GRPCTests/WithConnectedSocketTests.swift b/Tests/GRPCTests/WithConnectedSocketTests.swift index 04b80bb37..a4af27c77 100644 --- a/Tests/GRPCTests/WithConnectedSocketTests.swift +++ b/Tests/GRPCTests/WithConnectedSocketTests.swift @@ -15,11 +15,12 @@ */ import EchoImplementation import EchoModel -@testable import GRPC import NIOCore import NIOPosix import XCTest +@testable import GRPC + class WithConnectedSockettests: GRPCTestCase { func testWithConnectedSocket() throws { let group = NIOPosix.MultiThreadedEventLoopGroup(numberOfThreads: 1) diff --git a/Tests/GRPCTests/XCTestHelpers.swift b/Tests/GRPCTests/XCTestHelpers.swift index a760b3d2e..0fcdd9747 100644 --- a/Tests/GRPCTests/XCTestHelpers.swift +++ b/Tests/GRPCTests/XCTestHelpers.swift @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import NIOHPACK import NIOHTTP1 import NIOHTTP2 import XCTest +@testable import GRPC + struct UnwrapError: Error {} // We support Swift versions before 'XCTUnwrap' was introduced. @@ -458,7 +460,7 @@ struct Matcher { switch headersMatch { case .none, - .some(.match): + .some(.match): return endStream.map { Matcher.is($0).evaluate(payload.endStream) } ?? .match case .some(.noMatch): return headersMatch! @@ -480,7 +482,7 @@ struct Matcher { switch (endStreamMatches, payload.data) { case let (.none, .byteBuffer(b)), - let (.some(.match), .byteBuffer(b)): + let (.some(.match), .byteBuffer(b)): return buffer.map { Matcher.is($0).evaluate(b) } ?? .match case (.some(.noMatch), .byteBuffer): @@ -596,7 +598,8 @@ struct Matcher { } static func forwardHeadersThenRead() - -> Matcher { + -> Matcher + { return .init { actual in switch actual { case .forwardHeadersAndRead: @@ -608,7 +611,8 @@ struct Matcher { } static func forwardMessageThenRead() - -> Matcher { + -> Matcher + { return .init { actual in switch actual { case .forwardMessageThenReadNextMessage: diff --git a/Tests/GRPCTests/ZeroLengthWriteTests.swift b/Tests/GRPCTests/ZeroLengthWriteTests.swift index 1e2a36301..9b98a3520 100644 --- a/Tests/GRPCTests/ZeroLengthWriteTests.swift +++ b/Tests/GRPCTests/ZeroLengthWriteTests.swift @@ -265,4 +265,4 @@ final class ZeroLengthWriteTests: GRPCTestCase { } } -#endif // canImport(NIOSSL) +#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ZlibTests.swift b/Tests/GRPCTests/ZlibTests.swift index 66dc814b1..5ee37be52 100644 --- a/Tests/GRPCTests/ZlibTests.swift +++ b/Tests/GRPCTests/ZlibTests.swift @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@testable import GRPC + import NIOCore import XCTest +@testable import GRPC + class ZlibTests: GRPCTestCase { var allocator = ByteBufferAllocator() var inputSize = 4096 From 2abc8bd2b1669b6a230adfc8de70106464a95c3a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 6 Oct 2023 15:46:47 +0100 Subject: [PATCH 121/580] Add transport protocols and surrounding types (#1660) Motivation: The transport protocols provide a lower-level abstraction for different conection protocols. For example, there will be an HTTP/2 transport and in the future, when an implementation arises, a separate HTTP/3 transport. This change adds these interfaces and a handful of related types which surround the transport protocols. Modifications: - Add RPC parts - Add RPC stream composed of streams inbound and outbound RPC parts - Add async sequence wrapper - Add writer protocol and wrapper - Add various currency types - Tests Results: Transport protocols and related types are in place --- .../ClientRPCExecutionConfiguration.swift | 290 ++++++++++++++++++ Sources/GRPCCore/MethodDescriptor.swift | 46 +++ .../GRPCCore/Stream/RPCAsyncSequence.swift | 49 +++ Sources/GRPCCore/Stream/RPCWriter.swift | 37 +++ .../GRPCCore/Stream/RPCWriterProtocol.swift | 58 ++++ .../GRPCCore/Transport/ClientTransport.swift | 69 +++++ Sources/GRPCCore/Transport/RPCParts.swift | 45 +++ Sources/GRPCCore/Transport/RPCStream.swift | 37 +++ .../GRPCCore/Transport/ServerTransport.swift | 39 +++ ...ClientRPCExecutionConfigurationTests.swift | 49 +++ .../GRPCCoreTests/MethodDescriptorTests.swift | 26 ++ Tests/GRPCCoreTests/RPCPartsTests.swift | 24 ++ 12 files changed, 769 insertions(+) create mode 100644 Sources/GRPCCore/Call/ClientRPCExecutionConfiguration.swift create mode 100644 Sources/GRPCCore/MethodDescriptor.swift create mode 100644 Sources/GRPCCore/Stream/RPCAsyncSequence.swift create mode 100644 Sources/GRPCCore/Stream/RPCWriter.swift create mode 100644 Sources/GRPCCore/Stream/RPCWriterProtocol.swift create mode 100644 Sources/GRPCCore/Transport/ClientTransport.swift create mode 100644 Sources/GRPCCore/Transport/RPCParts.swift create mode 100644 Sources/GRPCCore/Transport/RPCStream.swift create mode 100644 Sources/GRPCCore/Transport/ServerTransport.swift create mode 100644 Tests/GRPCCoreTests/Call/ClientRPCExecutionConfigurationTests.swift create mode 100644 Tests/GRPCCoreTests/MethodDescriptorTests.swift create mode 100644 Tests/GRPCCoreTests/RPCPartsTests.swift diff --git a/Sources/GRPCCore/Call/ClientRPCExecutionConfiguration.swift b/Sources/GRPCCore/Call/ClientRPCExecutionConfiguration.swift new file mode 100644 index 000000000..b771e0b76 --- /dev/null +++ b/Sources/GRPCCore/Call/ClientRPCExecutionConfiguration.swift @@ -0,0 +1,290 @@ +/* + * Copyright 2023, 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. + */ + +/// Configuration values for executing an RPC. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct ClientRPCExecutionConfiguration: Hashable, Sendable { + /// The default timeout for the RPC. + /// + /// If no reply is received in the specified amount of time the request is aborted + /// with an ``RPCError`` with code ``RPCError/Code/deadlineExceeded``. + /// + /// The actual deadline used will be the minimum of the value specified here + /// and the value set by the application by the client API. If either one isn't set + /// then the other value is used. If neither is set then the request has no deadline. + /// + /// The timeout applies to the overall execution of an RPC. If, for example, a retry + /// policy is set then the timeout begins when the first attempt is started and _isn't_ reset + /// when subsequent attempts start. + public var timeout: Duration? + + /// The policy determining how many times, and when, the RPC is executed. + /// + /// There are two policy types: + /// 1. Retry + /// 2. Hedging + /// + /// The retry policy allows an RPC to be retried a limited number of times if the RPC + /// fails with one of the configured set of status codes. RPCs are only retried if they + /// fail immediately, that is, the first response part received from the server is a + /// status code. + /// + /// The hedging policy allows an RPC to be executed multiple times concurrently. Typically + /// each execution will be staggered by some delay. The first successful response will be + /// reported to the client. Hedging is only suitable for idempotent RPCs. + public var executionPolicy: ExecutionPolicy? + + /// Create an execution configuration. + /// + /// - Parameters: + /// - executionPolicy: The execution policy to use for the RPC. + /// - timeout: The default timeout for the RPC. + public init( + executionPolicy: ExecutionPolicy?, + timeout: Duration? + ) { + self.executionPolicy = executionPolicy + self.timeout = timeout + } + + /// Create an execution configuration with a retry policy. + /// + /// - Parameters: + /// - retryPolicy: The policy for retrying the RPC. + /// - timeout: The default timeout for the RPC. + public init( + retryPolicy: RetryPolicy, + timeout: Duration? = nil + ) { + self.executionPolicy = .retry(retryPolicy) + self.timeout = timeout + } + + /// Create an execution configuration with a hedging policy. + /// + /// - Parameters: + /// - hedgingPolicy: The policy for hedging the RPC. + /// - timeout: The default timeout for the RPC. + public init( + hedgingPolicy: HedgingPolicy, + timeout: Duration? = nil + ) { + self.executionPolicy = .hedge(hedgingPolicy) + self.timeout = timeout + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutionConfiguration { + /// The execution policy for an RPC. + public enum ExecutionPolicy: Hashable, Sendable { + /// Policy for retrying an RPC. + /// + /// See ``RetryPolicy`` for more details. + case retry(RetryPolicy) + + /// Policy for hedging an RPC. + /// + /// See ``HedgingPolicy`` for more details. + case hedge(HedgingPolicy) + } +} + +/// Policy for retrying an RPC. +/// +/// gRPC retries RPCs when the first response from the server is a status code which matches +/// one of the configured retryable status codes. If the server begins processing the RPC and +/// first responds with metadata and later responds with a retryable status code then the RPC +/// won't be retried. +/// +/// Execution attempts are limited by ``maximumAttempts`` which includes the original attempt. The +/// maximum number of attempts is limited to five. +/// +/// Subsequent attempts are executed after some delay. The first _retry_, or second attempt, will +/// be started after a randomly chosen delay between zero and ``initialBackoff``. More generally, +/// the nth retry will happen after a randomly chosen delay between zero +/// and `min(initialBackoff * backoffMultiplier^(n-1), maximumBackoff)`. +/// +/// For more information see [gRFC A6 Client +/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct RetryPolicy: Hashable, Sendable { + /// The maximum number of RPC attempts, including the original attempt. + /// + /// Must be greater than one, values greater than five are treated as five. + public var maximumAttempts: Int { + didSet { self.maximumAttempts = validateMaxAttempts(self.maximumAttempts) } + } + + /// The initial backoff duration. + /// + /// The initial retry will occur after a random amount of time up to this value. + /// + /// - Precondition: Must be greater than zero. + public var initialBackoff: Duration { + willSet { Self.validateInitialBackoff(newValue) } + } + + /// The maximum amount of time to backoff for. + /// + /// - Precondition: Must be greater than zero. + public var maximumBackoff: Duration { + willSet { Self.validateMaxBackoff(newValue) } + } + + /// The multiplier to apply to backoff. + /// + /// - Precondition: Must be greater than zero. + public var backoffMultiplier: Double { + willSet { Self.validateBackoffMultiplier(newValue) } + } + + /// The set of status codes which may be retried. + /// + /// - Precondition: Must not be empty. + public var retryableStatusCodes: Set { + willSet { Self.validateRetryableStatusCodes(newValue) } + } + + /// Create a new retry policy. + /// + /// - Parameters: + /// - maximumAttempts: The maximum number of attempts allowed for the RPC. + /// - initialBackoff: The initial backoff period for the first retry attempt. Must be + /// greater than zero. + /// - maximumBackoff: The maximum period of time to wait between attempts. Must be greater than + /// zero. + /// - backoffMultiplier: The exponential backoff multiplier. Must be greater than zero. + /// - retryableStatusCodes: The set of status codes which may be retried. Must not be empty. + /// - Precondition: `maximumAttempts`, `initialBackoff`, `maximumBackoff` and `backoffMultiplier` + /// must be greater than zero. + /// - Precondition: `retryableStatusCodes` must not be empty. + public init( + maximumAttempts: Int, + initialBackoff: Duration, + maximumBackoff: Duration, + backoffMultiplier: Double, + retryableStatusCodes: Set + ) { + self.maximumAttempts = validateMaxAttempts(maximumAttempts) + + Self.validateInitialBackoff(initialBackoff) + self.initialBackoff = initialBackoff + + Self.validateMaxBackoff(maximumBackoff) + self.maximumBackoff = maximumBackoff + + Self.validateBackoffMultiplier(backoffMultiplier) + self.backoffMultiplier = backoffMultiplier + + Self.validateRetryableStatusCodes(retryableStatusCodes) + self.retryableStatusCodes = retryableStatusCodes + } + + private static func validateInitialBackoff(_ value: Duration) { + precondition(value.isGreaterThanZero, "initialBackoff must be greater than zero") + } + + private static func validateMaxBackoff(_ value: Duration) { + precondition(value.isGreaterThanZero, "maximumBackoff must be greater than zero") + } + + private static func validateBackoffMultiplier(_ value: Double) { + precondition(value > 0, "backoffMultiplier must be greater than zero") + } + + private static func validateRetryableStatusCodes(_ value: Set) { + precondition(!value.isEmpty, "retryableStatusCodes mustn't be empty") + } +} + +/// Policy for hedging an RPC. +/// +/// Hedged RPCs may execute more than once on a server so only idempotent methods should +/// be hedged. +/// +/// gRPC executes the RPC at most ``maximumAttempts`` times, staggering each attempt +/// by ``hedgingDelay``. +/// +/// For more information see [gRFC A6 Client +/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct HedgingPolicy: Hashable, Sendable { + /// The maximum number of RPC attempts, including the original attempt. + /// + /// Values greater than five are treated as five. + /// + /// - Precondition: Must be greater than one. + public var maximumAttempts: Int { + didSet { self.maximumAttempts = validateMaxAttempts(self.maximumAttempts) } + } + + /// The first RPC will be sent immediately, but each subsequent RPC will be sent at intervals + /// of `hedgingDelay`. Set this to zero to immediately send all RPCs. + public var hedgingDelay: Duration { + willSet { Self.validateHedgingDelay(newValue) } + } + + /// The set of status codes which indicate other hedged RPCs may still succeed. + /// + /// If a non-fatal status code is returned by the server, hedged RPCs will continue. + /// Otherwise, outstanding requests will be cancelled and the error returned to the + /// application layer. + public var nonFatalStatusCodes: Set + + /// Create a new hedging policy. + /// + /// - Parameters: + /// - maximumAttempts: The maximum number of attempts allowed for the RPC. + /// - hedgingDelay: The delay between each hedged RPC. + /// - nonFatalStatusCodes: The set of status codes which indicated other hedged RPCs may still + /// succeed. + /// - Precondition: `maximumAttempts` must be greater than zero. + public init( + maximumAttempts: Int, + hedgingDelay: Duration, + nonFatalStatusCodes: Set + ) { + self.maximumAttempts = validateMaxAttempts(maximumAttempts) + + Self.validateHedgingDelay(hedgingDelay) + self.hedgingDelay = hedgingDelay + self.nonFatalStatusCodes = nonFatalStatusCodes + } + + private static func validateHedgingDelay(_ value: Duration) { + precondition( + value.isGreaterThanOrEqualToZero, + "hedgingDelay must be greater than or equal to zero" + ) + } +} + +private func validateMaxAttempts(_ value: Int) -> Int { + precondition(value > 0, "maximumAttempts must be greater than zero") + return min(value, 5) +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Duration { + fileprivate var isGreaterThanZero: Bool { + self.components.seconds > 0 || self.components.attoseconds > 0 + } + + fileprivate var isGreaterThanOrEqualToZero: Bool { + self.components.seconds >= 0 || self.components.attoseconds >= 0 + } +} diff --git a/Sources/GRPCCore/MethodDescriptor.swift b/Sources/GRPCCore/MethodDescriptor.swift new file mode 100644 index 000000000..8d2795ac1 --- /dev/null +++ b/Sources/GRPCCore/MethodDescriptor.swift @@ -0,0 +1,46 @@ +/* + * Copyright 2023, 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. + */ + +/// A description of a method on a service. +public struct MethodDescriptor: Sendable, Hashable { + /// The name of the service, including the package name. + /// + /// For example, the name of the "Greeter" service in "helloworld" package + /// is "helloworld.Greeter". + public var service: String + + /// The name of the method in the service, excluding the service name. + public var method: String + + /// The fully qualified method name in the format "package.service/method". + /// + /// For example, the fully qualified name of the "SayHello" method of the "Greeter" service in + /// "helloworld" package is "helloworld.Greeter/SayHelllo". + public var fullyQualifiedMethod: String { + "\(self.service)/\(self.method)" + } + + /// Creates a new method descriptor. + /// + /// - Parameters: + /// - service: The name of the service, including the package name. For example, + /// "helloworld.Greeter". + /// - method: The name of the method. For example, "SayHello". + public init(service: String, method: String) { + self.service = service + self.method = method + } +} diff --git a/Sources/GRPCCore/Stream/RPCAsyncSequence.swift b/Sources/GRPCCore/Stream/RPCAsyncSequence.swift new file mode 100644 index 000000000..3e397089e --- /dev/null +++ b/Sources/GRPCCore/Stream/RPCAsyncSequence.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2023, 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. + */ + +/// A type-erasing `AsyncSequence`. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct RPCAsyncSequence: AsyncSequence, Sendable { + private let _makeAsyncIterator: @Sendable () -> AsyncIterator + + /// Creates an ``RPCAsyncSequence`` by wrapping another `AsyncSequence`. + public init(wrapping other: S) where S.Element == Element { + self._makeAsyncIterator = { + AsyncIterator(wrapping: other.makeAsyncIterator()) + } + } + + public func makeAsyncIterator() -> AsyncIterator { + self._makeAsyncIterator() + } + + public struct AsyncIterator: AsyncIteratorProtocol { + private var iterator: any AsyncIteratorProtocol + + fileprivate init( + wrapping other: Iterator + ) where Iterator: AsyncIteratorProtocol, Iterator.Element == Element { + self.iterator = other + } + + public mutating func next() async throws -> Element? { + return try await self.iterator.next() as? Element + } + } +} + +@available(*, unavailable) +extension RPCAsyncSequence.AsyncIterator: Sendable {} diff --git a/Sources/GRPCCore/Stream/RPCWriter.swift b/Sources/GRPCCore/Stream/RPCWriter.swift new file mode 100644 index 000000000..1b835c822 --- /dev/null +++ b/Sources/GRPCCore/Stream/RPCWriter.swift @@ -0,0 +1,37 @@ +/* + * Copyright 2023, 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. + */ + +/// A type-erasing ``RPCWriterProtocol``. +public struct RPCWriter: Sendable, RPCWriterProtocol { + private let writer: any RPCWriterProtocol + + /// Creates an ``RPCWriter`` by wrapping the `other` writer. + /// + /// - Parameter other: The writer to wrap. + public init(wrapping other: some RPCWriterProtocol) { + self.writer = other + } + + /// Writes a sequence of elements. + /// + /// This function suspends until the elements have been accepted. Implements can use this + /// to exert backpressure on callers. + /// + /// - Parameter elements: The elements to write. + public func write(contentsOf elements: some Sequence) async throws { + try await self.writer.write(contentsOf: elements) + } +} diff --git a/Sources/GRPCCore/Stream/RPCWriterProtocol.swift b/Sources/GRPCCore/Stream/RPCWriterProtocol.swift new file mode 100644 index 000000000..48491e63e --- /dev/null +++ b/Sources/GRPCCore/Stream/RPCWriterProtocol.swift @@ -0,0 +1,58 @@ +/* + * Copyright 2023, 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. + */ + +/// A sink for values which are produced over time. +public protocol RPCWriterProtocol: Sendable { + /// The type of value written. + associatedtype Element + + /// Writes a sequence of elements. + /// + /// This function suspends until the elements have been accepted. Implements can use this + /// to exert backpressure on callers. + /// + /// - Parameter elements: The elements to write. + func write(contentsOf elements: some Sequence) async throws +} + +extension RPCWriterProtocol { + /// Writes a single element into the sink. + /// + /// - Parameter element: The element to write. + public func write(_ element: Element) async throws { + try await self.write(contentsOf: CollectionOfOne(element)) + } + + /// Writes an `AsyncSequence` of values into the sink. + /// + /// - Parameter elements: The elements to write. + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public func write( + _ elements: Elements + ) async throws where Elements.Element == Element { + for try await element in elements { + try await self.write(element) + } + } +} + +public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { + /// Indicate to the writer that no more writes are to be accepted. + /// + /// All writes after ``finish()`` has been called should result in an error + /// being thrown. + func finish() +} diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift new file mode 100644 index 000000000..8a012205d --- /dev/null +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -0,0 +1,69 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol ClientTransport: Sendable { + associatedtype Inbound: (AsyncSequence & Sendable) where Inbound.Element == RPCResponsePart + associatedtype Outbound: ClosableRPCWriterProtocol + + /// Establish and maintain a connection to the remote destination. + /// + /// Maintains a long-lived connection, or set of connections, to a remote destination. + /// Connections may be added or removed over time as required by the implementation and the + /// demand for streams by the client. + /// + /// Implementations of this function will typically create a long-lived task group which + /// maintains connections. The function exits when connections are no longer required by + /// the caller who signals this by calling ``close()`` to indicate that no new streams are + /// required or by cancelling the task this function runs in. + /// + /// - Parameter lazily: Whether the transport should establish connections lazily, that is, + /// when the first stream is opened or eagerly, when this function is called. If `false` + /// then the transport should attempt to establish a connection immediately. Note that + /// this is a _hint_: transports aren't required to respect this value and you should + /// refer to the documentation of the transport you're using to check whether it's supported. + func connect(lazily: Bool) async throws + + /// Signal to the transport that no new streams may be created. + /// + /// Existing streams may run to completion naturally but calling ``openStream(descriptor:)`` + /// should result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. + /// + /// If you want to forcefully cancel all active streams then cancel the task + /// running ``connect(lazily:)``. + func close() + + /// Open a stream using the transport. + /// + /// Transport implementations should throw an ``RPCError`` with the following error codes: + /// - ``RPCError/Code/failedPrecondition`` if the transport is closing or has been closed. + /// - ``RPCError/Code/unavailable`` if it's temporarily not possible to create a stream and it + /// may be possible after some backoff period. + /// + /// - Parameter descriptor: A description of the method to open a stream for. + /// - Returns: A stream. + func openStream( + descriptor: MethodDescriptor + ) async throws -> RPCStream + + /// Returns the execution configuration for a given method. + /// + /// - Parameter descriptor: The method to lookup configuration for. + /// - Returns: Execution configuration for the method, if it exists. + func executionConfiguration( + forMethod descriptor: MethodDescriptor + ) -> ClientRPCExecutionConfiguration? +} diff --git a/Sources/GRPCCore/Transport/RPCParts.swift b/Sources/GRPCCore/Transport/RPCParts.swift new file mode 100644 index 000000000..cd72f7efb --- /dev/null +++ b/Sources/GRPCCore/Transport/RPCParts.swift @@ -0,0 +1,45 @@ +/* + * Copyright 2023, 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. + */ + +/// Part of a request sent from a client to a server in a stream. +public enum RPCRequestPart: Hashable, Sendable { + /// Key-value pairs sent at the start of a request stream. Only one ``metadata(_:)`` value may + /// be sent to the server. + case metadata(Metadata) + + /// The bytes of a serialized message to send to the server. A stream may have any number of + /// messages sent on it. Restrictions for unary request or response streams are imposed at a + /// higher level. + case message([UInt8]) +} + +/// Part of a response sent from a server to a client in a stream. +public enum RPCResponsePart: Hashable, Sendable { + /// Key-value pairs sent at the start of the response stream. At most one ``metadata(_:)`` value + /// may be sent to the client. If the server sends ``metadata(_:)`` it must be the first part in + /// the response stream. + case metadata(Metadata) + + /// The bytes of a serialized message to send to the client. A stream may have any number of + /// messages sent on it. Restrictions for unary request or response streams are imposed at a + /// higher level. + case message([UInt8]) + + /// A status and key-value pairs sent to the client at the end of the response stream. Every + /// response stream must have exactly one ``status(_:_:)`` as the final part of the request + /// stream. + case status(Status, Metadata) +} diff --git a/Sources/GRPCCore/Transport/RPCStream.swift b/Sources/GRPCCore/Transport/RPCStream.swift new file mode 100644 index 000000000..eca345c2a --- /dev/null +++ b/Sources/GRPCCore/Transport/RPCStream.swift @@ -0,0 +1,37 @@ +/* + * Copyright 2023, 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. + */ + +/// A bidirectional communication channel between a client and server for a given method. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct RPCStream< + Inbound: AsyncSequence & Sendable, + Outbound: ClosableRPCWriterProtocol & Sendable +>: Sendable { + /// Information about the method this stream is for. + public var descriptor: MethodDescriptor + + /// A sequence of messages received from the network. + public var inbound: Inbound + + /// A writer for messages sent across the network. + public var outbound: Outbound + + public init(descriptor: MethodDescriptor, inbound: Inbound, outbound: Outbound) { + self.descriptor = descriptor + self.inbound = inbound + self.outbound = outbound + } +} diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift new file mode 100644 index 000000000..e538c68f7 --- /dev/null +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol ServerTransport { + associatedtype Inbound: (AsyncSequence & Sendable) where Inbound.Element == RPCRequestPart + associatedtype Outbound: ClosableRPCWriterProtocol + + /// Starts the transport and returns a sequence of accepted streams to handle. + /// + /// Implementations will typically bind to a listening port when this function is called + /// and start accepting new connections. Each accepted inbound RPC stream should be published + /// to the async sequence returned by the function. + /// + /// You can call ``stopListening()`` to stop the transport from accepting new streams. Existing + /// streams must be allowed to complete naturally. However, transports may also enforce a grace + /// period after which any open streams may be cancelled. You can also cancel the task running + /// ``listen()`` to abruptly close connections and streams. + func listen() async throws -> RPCAsyncSequence> + + /// Indicates to the transport that no new streams should be accepted. + /// + /// Existing streams are permitted to run to completion. However, the transport may also enforce + /// a grace period, after which remaining streams are cancelled. + func stopListening() +} diff --git a/Tests/GRPCCoreTests/Call/ClientRPCExecutionConfigurationTests.swift b/Tests/GRPCCoreTests/Call/ClientRPCExecutionConfigurationTests.swift new file mode 100644 index 000000000..6021d1a24 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/ClientRPCExecutionConfigurationTests.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2023, 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 ClientRPCExecutionConfigurationTests: XCTestCase { + func testRetryPolicyClampsMaxAttempts() { + var policy = RetryPolicy( + maximumAttempts: 10, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(1), + backoffMultiplier: 1.0, + retryableStatusCodes: [.unavailable] + ) + + // Should be clamped on init + XCTAssertEqual(policy.maximumAttempts, 5) + // and when modifying + policy.maximumAttempts = 10 + XCTAssertEqual(policy.maximumAttempts, 5) + } + + func testHedgingPolicyClampsMaxAttempts() { + var policy = HedgingPolicy( + maximumAttempts: 10, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [] + ) + + // Should be clamped on init + XCTAssertEqual(policy.maximumAttempts, 5) + // and when modifying + policy.maximumAttempts = 10 + XCTAssertEqual(policy.maximumAttempts, 5) + } +} diff --git a/Tests/GRPCCoreTests/MethodDescriptorTests.swift b/Tests/GRPCCoreTests/MethodDescriptorTests.swift new file mode 100644 index 000000000..cf4568898 --- /dev/null +++ b/Tests/GRPCCoreTests/MethodDescriptorTests.swift @@ -0,0 +1,26 @@ +/* + * Copyright 2023, 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 MethodDescriptorTests: XCTestCase { + func testFullyQualifiedName() { + let descriptor = MethodDescriptor(service: "foo.bar", method: "Baz") + XCTAssertEqual(descriptor.service, "foo.bar") + XCTAssertEqual(descriptor.method, "Baz") + XCTAssertEqual(descriptor.fullyQualifiedMethod, "foo.bar/Baz") + } +} diff --git a/Tests/GRPCCoreTests/RPCPartsTests.swift b/Tests/GRPCCoreTests/RPCPartsTests.swift new file mode 100644 index 000000000..e950a8e97 --- /dev/null +++ b/Tests/GRPCCoreTests/RPCPartsTests.swift @@ -0,0 +1,24 @@ +/* + * Copyright 2023, 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 RPCPartsTests: XCTestCase { + func testPartsFitInExistentialContainer() { + XCTAssertLessThanOrEqual(MemoryLayout.size, 24) + XCTAssertLessThanOrEqual(MemoryLayout.size, 24) + } +} From 9af35a0c8e962812179dd728bf5c9b5d4632b502 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 9 Oct 2023 10:42:58 +0100 Subject: [PATCH 122/580] Add missing availability guards (#1666) --- Sources/GRPCCore/Stream/RPCWriter.swift | 1 + Sources/GRPCCore/Stream/RPCWriterProtocol.swift | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCCore/Stream/RPCWriter.swift b/Sources/GRPCCore/Stream/RPCWriter.swift index 1b835c822..8eed40265 100644 --- a/Sources/GRPCCore/Stream/RPCWriter.swift +++ b/Sources/GRPCCore/Stream/RPCWriter.swift @@ -15,6 +15,7 @@ */ /// A type-erasing ``RPCWriterProtocol``. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct RPCWriter: Sendable, RPCWriterProtocol { private let writer: any RPCWriterProtocol diff --git a/Sources/GRPCCore/Stream/RPCWriterProtocol.swift b/Sources/GRPCCore/Stream/RPCWriterProtocol.swift index 48491e63e..fecb5df04 100644 --- a/Sources/GRPCCore/Stream/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Stream/RPCWriterProtocol.swift @@ -15,6 +15,7 @@ */ /// A sink for values which are produced over time. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol RPCWriterProtocol: Sendable { /// The type of value written. associatedtype Element @@ -28,6 +29,7 @@ public protocol RPCWriterProtocol: Sendable { func write(contentsOf elements: some Sequence) async throws } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriterProtocol { /// Writes a single element into the sink. /// @@ -39,7 +41,6 @@ extension RPCWriterProtocol { /// Writes an `AsyncSequence` of values into the sink. /// /// - Parameter elements: The elements to write. - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public func write( _ elements: Elements ) async throws where Elements.Element == Element { @@ -49,6 +50,7 @@ extension RPCWriterProtocol { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// Indicate to the writer that no more writes are to be accepted. /// From 9f012021931bc8b1401ed8b2936d87ca7aa94766 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 9 Oct 2023 10:49:23 +0100 Subject: [PATCH 123/580] Add client request and response objects (#1665) --- Sources/GRPCCore/Call/ClientRequest.swift | 116 +++++ Sources/GRPCCore/Call/ClientResponse.swift | 436 ++++++++++++++++++ .../GRPCCore/Stream/AsyncSequenceOfOne.swift | 58 +++ .../Call/ClientRequestTests.swift | 31 ++ .../Call/ClientResponseTests.swift | 171 +++++++ .../Stream/AsyncSequenceOfOne.swift | 39 ++ .../AsyncSequence+Utilities.swift | 36 ++ .../RPCAsyncSequence+Utilities.swift | 32 ++ .../Test Utilities/RPCWriter+Utilities.swift | 49 ++ .../Test Utilities/XCTest+Utilities.swift | 28 ++ 10 files changed, 996 insertions(+) create mode 100644 Sources/GRPCCore/Call/ClientRequest.swift create mode 100644 Sources/GRPCCore/Call/ClientResponse.swift create mode 100644 Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift create mode 100644 Tests/GRPCCoreTests/Call/ClientRequestTests.swift create mode 100644 Tests/GRPCCoreTests/Call/ClientResponseTests.swift create mode 100644 Tests/GRPCCoreTests/Stream/AsyncSequenceOfOne.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift diff --git a/Sources/GRPCCore/Call/ClientRequest.swift b/Sources/GRPCCore/Call/ClientRequest.swift new file mode 100644 index 000000000..bc03d5a8f --- /dev/null +++ b/Sources/GRPCCore/Call/ClientRequest.swift @@ -0,0 +1,116 @@ +/* + * Copyright 2023, 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. + */ + +/// A namespace for request message types used by clients. +public enum ClientRequest {} + +extension ClientRequest { + /// A request created by the client for a single message. + /// + /// This is used for unary and server-streaming RPCs. + /// + /// See ``ClientRequest/Stream`` for streaming requests and ``ServerRequest/Single`` for the + /// servers representation of a single-message request. + /// + /// ## Creating ``Single`` requests + /// + /// ```swift + /// let request = ClientRequest.Single(message: "Hello, gRPC!") + /// print(request.metadata) // prints '[:]' + /// print(request.message) // prints 'Hello, gRPC!' + /// ``` + public struct Single: Sendable { + /// Caller-specified metadata to send to the server at the start of the RPC. + /// + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport specific + /// metadata. Note that transports may also impose limits in the amount of metadata which may + /// be sent. + public var metadata: Metadata + + /// The message to send to the server. + public var message: Message + + /// Create a new single client request. + /// + /// - Parameters: + /// - message: The message to send to the server. + /// - metadata: Metadata to send to the server at the start of the request. Defaults to empty. + public init( + message: Message, + metadata: Metadata = [:] + ) { + self.metadata = metadata + self.message = message + } + } +} + +extension ClientRequest { + /// A request created by the client for a stream of messages. + /// + /// This is used for client-streaming and bidirectional-streaming RPCs. + /// + /// See ``ClientRequest/Single`` for single-message requests and ``ServerRequest/Stream`` for the + /// servers representation of a streaming-message request. + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public struct Stream: Sendable { + /// Caller-specified metadata sent to the server at the start of the RPC. + /// + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport specific + /// metadata. Note that transports may also impose limits in the amount of metadata which may + /// be sent. + public var metadata: Metadata + + /// A closure which, when called, writes messages in the writer. + /// + /// The producer will only be consumed once by gRPC and therefore isn't required to be + /// idempotent. If the producer throws an error then the RPC will be cancelled. Once the + /// producer returns the request stream is closed. + public var producer: @Sendable (RPCWriter) async throws -> Void + + /// Create a new streaming client request. + /// + /// - Parameters: + /// - messageType: The type of message contained in this request, defaults to `Message.self`. + /// - metadata: Metadata to send to the server at the start of the request. Defaults to empty. + /// - producer: A closure which writes messages to send to the server. The closure is called + /// at most once and may not be called. + public init( + of messageType: Message.Type = Message.self, + metadata: Metadata = [:], + producer: @escaping @Sendable (RPCWriter) async throws -> Void + ) { + self.metadata = metadata + self.producer = producer + } + } +} + +// MARK: - Conversion + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ClientRequest.Stream { + @_spi(Testing) + public init(single request: ClientRequest.Single) { + self.init(metadata: request.metadata) { + try await $0.write(request.message) + } + } +} diff --git a/Sources/GRPCCore/Call/ClientResponse.swift b/Sources/GRPCCore/Call/ClientResponse.swift new file mode 100644 index 000000000..f384ac80a --- /dev/null +++ b/Sources/GRPCCore/Call/ClientResponse.swift @@ -0,0 +1,436 @@ +/* + * Copyright 2023, 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. + */ + +/// A namespace for response message types used by clients. +public enum ClientResponse {} + +extension ClientResponse { + /// A response for a single message received by a client. + /// + /// Single responses are used for unary and client-streaming RPCs. For streaming responses + /// see ``ClientResponse/Stream``. + /// + /// A single response captures every part of the response stream and distinguishes successful + /// and unsuccessful responses via the ``accepted`` property. The value for the `success` case + /// contains the initial metadata, response message, and the trailing metadata and implicitly + /// has an ``Status/Code-swift.struct/ok`` status code. + /// + /// The `failure` case indicates that the server chose not to process the RPC, or the processing + /// of the RPC failed, or the client failed to execute the request. The failure case contains + /// an ``RPCError`` describing why the RPC failed, including an error code, error message and any + /// metadata sent by the server. + /// + /// ### Using ``Single`` responses + /// + /// Each response has a ``accepted`` property which contains all RPC information. You can create + /// one by calling ``init(accepted:)`` or one of the two convenience initializers: + /// - ``init(message:metadata:trailingMetadata:)`` to create a successful response, or + /// - ``init(of:error:)`` to create a failed response. + /// + /// You can interrogate a response by inspecting the ``accepted`` property directly or by using + /// its convenience properties: + /// - ``metadata`` extracts the initial metadata, + /// - ``message`` extracts the message, or throws if the response failed, and + /// - ``trailingMetadata`` extracts the trailing metadata. + /// + /// The following example demonstrates how you can use the API: + /// + /// ```swift + /// // Create a successful response + /// let response = ClientResponse.Single( + /// message: "Hello, World!", + /// metadata: ["hello": "initial metadata"], + /// trailingMetadata: ["goodbye": "trailing metadata"] + /// ) + /// + /// // The explicit API: + /// switch response { + /// case .success(let contents): + /// print("Received response with message '\(contents.message)'") + /// case .failure(let error): + /// print("RPC failed with code '\(error.code)'") + /// } + /// + /// // The convenience API: + /// do { + /// print("Received response with message '\(try response.message)'") + /// } catch let error as RPCError { + /// print("RPC failed with code '\(error.code)'") + /// } + /// ``` + public struct Single: Sendable { + /// The contents of an accepted response with a single message. + public struct Contents: Sendable { + /// Metadata received from the server at the beginning of the response. + /// + /// The metadata may contain transport-specific information in addition to any application + /// level metadata provided by the service. + public var metadata: Metadata + + /// The response message received from the server. + public var message: Message + + /// Metadata received from the server at the end of the response. + /// + /// The metadata may contain transport-specific information in addition to any application + /// level metadata provided by the service. + public var trailingMetadata: Metadata + + /// Creates a `Contents`. + /// + /// - Parameters: + /// - metadata: Metadata received from the server at the beginning of the response. + /// - message: The response message received from the server. + /// - trailingMetadata: Metadata received from the server at the end of the response. + public init( + metadata: Metadata, + message: Message, + trailingMetadata: Metadata + ) { + self.metadata = metadata + self.message = message + self.trailingMetadata = trailingMetadata + } + } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` case indicates the RPC completed successfully with an + /// ``Status/Code-swift.struct/ok`` status code. The `failure` case indicates that the RPC was + /// rejected by the server and wasn't processed or couldn't be processed successfully. + public var accepted: Result + + /// Creates a new response. + /// + /// - Parameter accepted: The result of the RPC. + public init(accepted: Result) { + self.accepted = accepted + } + } +} + +extension ClientResponse { + /// A response for a stream of messages received by a client. + /// + /// Stream responses are used for server-streaming and bidirectional-streaming RPCs. For single + /// responses see ``ClientResponse/Single``. + /// + /// A stream response captures every part of the response stream over time and distinguishes + /// accepted and rejected requests via the ``accepted`` property. An "accepted" request is one + /// where the the server responds with initial metadata and attempts to process the request. A + /// "rejected" request is one where the server responds with a status as the first and only + /// response part and doesn't process the request body. + /// + /// The value for the `success` case contains the initial metadata and a ``RPCAsyncSequence`` of + /// message parts (messages followed by a single status). If the sequence completes without + /// throwing then the response implicitly has an ``Status/Code-swift.struct/ok`` status code. + /// However, the response sequence may also throw an ``RPCError`` if the server fails to complete + /// processing the request. + /// + /// The `failure` case indicates that the server chose not to process the RPC or the client failed + /// to execute the request. The failure case contains an ``RPCError`` describing why the RPC + /// failed, including an error code, error message and any metadata sent by the server. + /// + /// ### Using ``Stream`` responses + /// + /// Each response has a ``accepted`` property which contains RPC information. You can create + /// one by calling ``init(accepted:)`` or one of the two convenience initializers: + /// - ``init(of:metadata:bodyParts:)`` to create an accepted response, or + /// - ``init(of:error:)`` to create a failed response. + /// + /// You can interrogate a response by inspecting the ``accepted`` property directly or by using + /// its convenience properties: + /// - ``metadata`` extracts the initial metadata, + /// - ``messages`` extracts the sequence of response message, or throws if the response failed. + /// + /// The following example demonstrates how you can use the API: + /// + /// ```swift + /// // Create a failed response + /// let response = ClientResponse.Stream( + /// of: String.self, + /// error: RPCError(code: .notFound, message: "The requested resource couldn't be located") + /// ) + /// + /// // The explicit API: + /// switch response { + /// case .success(let contents): + /// for try await part in contents.bodyParts { + /// switch part { + /// case .message(let message): + /// print("Received message '\(message)'") + /// case .trailingMetadata(let metadata): + /// print("Received trailing metadata '\(metadata)'") + /// } + /// } + /// case .failure(let error): + /// print("RPC failed with code '\(error.code)'") + /// } + /// + /// // The convenience API: + /// do { + /// for try await message in response.messages { + /// print("Received message '\(message)'") + /// } + /// } catch let error as RPCError { + /// print("RPC failed with code '\(error.code)'") + /// } + /// ``` + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public struct Stream: Sendable { + public struct Contents: Sendable { + /// Metadata received from the server at the beginning of the response. + /// + /// The metadata may contain transport-specific information in addition to any application + /// level metadata provided by the service. + public var metadata: Metadata + + /// A sequence of stream parts received from the server ending with metadata if the RPC + /// succeeded. + /// + /// If the RPC fails then the sequence will throw an ``RPCError``. + /// + /// The sequence may only be iterated once. + public var bodyParts: RPCAsyncSequence + + /// Parts received from the server. + public enum BodyPart: Sendable { + /// A response message. + case message(Message) + /// Metadata. Must be the final value of the sequence unless the stream throws an error. + case trailingMetadata(Metadata) + } + + /// Creates a ``Contents``. + /// + /// - Parameters: + /// - metadata: Metadata received from the server at the beginning of the response. + /// - bodyParts: An `AsyncSequence` of parts received from the server. + public init( + metadata: Metadata, + bodyParts: RPCAsyncSequence + ) { + self.metadata = metadata + self.bodyParts = bodyParts + } + } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` case indicates the RPC was accepted by the server for + /// processing, however, the RPC may still fail by throwing an error from its + /// `messages` sequence. The `failure` case indicates that the RPC was + /// rejected by the server. + public var accepted: Result + + /// Creates a new response. + /// + /// - Parameter accepted: The result of the RPC. + public init(accepted: Result) { + self.accepted = accepted + } + } +} + +// MARK: - Convenience API + +extension ClientResponse.Single { + /// Creates a new accepted response. + /// + /// - Parameters: + /// - metadata: Metadata received from the server at the beginning of the response. + /// - message: The response message received from the server. + /// - trailingMetadata: Metadata received from the server at the end of the response. + public init(message: Message, metadata: Metadata = [:], trailingMetadata: Metadata = [:]) { + let contents = Contents( + metadata: metadata, + message: message, + trailingMetadata: trailingMetadata + ) + self.accepted = .success(contents) + } + + /// Creates a new failed response. + /// + /// - Parameters: + /// - messageType: The type of message. + /// - error: An error describing why the RPC failed. + public init(of messageType: Message.Type = Message.self, error: RPCError) { + self.accepted = .failure(error) + } + + /// Returns metadata received from the server at the start of the response. + /// + /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty. + public var metadata: Metadata { + switch self.accepted { + case let .success(contents): + return contents.metadata + case .failure: + return [:] + } + } + + /// Returns the message received from the server. + /// + /// - Throws: ``RPCError`` if the request failed. + public var message: Message { + get throws { + try self.accepted.map { $0.message }.get() + } + } + + /// Returns metadata received from the server at the end of the response. + /// + /// Unlike ``metadata``, for rejected RPCs the metadata returned may contain values. + public var trailingMetadata: Metadata { + switch self.accepted { + case let .success(contents): + return contents.trailingMetadata + case let .failure(error): + return error.metadata + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ClientResponse.Stream { + /// Creates a new accepted response. + /// + /// - Parameters: + /// - messageType: The type of message. + /// - metadata: Metadata received from the server at the beginning of the response. + /// - bodyParts: An ``RPCAsyncSequence`` of response parts received from the server. + public init( + of messageType: Message.Type = Message.self, + metadata: Metadata, + bodyParts: RPCAsyncSequence + ) { + let contents = Contents(metadata: metadata, bodyParts: bodyParts) + self.accepted = .success(contents) + } + + /// Creates a new failed response. + /// + /// - Parameters: + /// - messageType: The type of message. + /// - error: An error describing why the RPC failed. + public init(of messageType: Message.Type = Message.self, error: RPCError) { + self.accepted = .failure(error) + } + + /// Returns metadata received from the server at the start of the response. + /// + /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty. + public var metadata: Metadata { + switch self.accepted { + case let .success(contents): + return contents.metadata + case .failure: + return [:] + } + } + + /// Returns metadata received from the server at the end of the response. + /// + /// Unlike ``metadata``, for rejected RPCs the metadata returned may contain values. + public var messages: RPCAsyncSequence { + switch self.accepted { + case let .success(contents): + let filtered = contents.bodyParts.compactMap { + switch $0 { + case let .message(message): + return message + case .trailingMetadata: + return nil + } + } + + return RPCAsyncSequence(wrapping: filtered) + + case let .failure(error): + return RPCAsyncSequence.throwing(error) + } + } +} + +// MARK: - Conversion + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ClientResponse.Single { + @_spi(Testing) + public init(stream response: ClientResponse.Stream) async { + switch response.accepted { + case .success(let contents): + do { + let metadata = contents.metadata + var iterator = contents.bodyParts.makeAsyncIterator() + + // Happy path: message, trailing metadata, nil. + let part1 = try await iterator.next() + let part2 = try await iterator.next() + let part3 = try await iterator.next() + + switch (part1, part2, part3) { + case (.some(.message(let message)), .some(.trailingMetadata(let trailingMetadata)), .none): + let contents = Contents( + metadata: metadata, + message: message, + trailingMetadata: trailingMetadata + ) + self.accepted = .success(contents) + + case (.some(.message), .some(.message), _): + let error = RPCError( + code: .unimplemented, + message: """ + Multiple messages received, but only one is expected. The server may have \ + incorrectly implemented the RPC or the client and server may have a different \ + opinion on whether this RPC streams responses. + """ + ) + self.accepted = .failure(error) + + case (.some(.trailingMetadata), .none, .none): + let error = RPCError( + code: .unimplemented, + message: "No messages received, exactly one was expected." + ) + self.accepted = .failure(error) + + case (_, _, _): + let error = RPCError( + code: .internalError, + message: """ + The stream from the client transport is invalid. This is likely to be an incorrectly \ + implemented transport. Received parts: \([part1, part2, part3])." + """ + ) + self.accepted = .failure(error) + } + } catch let error as RPCError { + // Known error type. + self.accepted = .failure(error) + } catch { + // Unexpected, but should be handled nonetheless. + self.accepted = .failure(RPCError(code: .unknown, message: String(describing: error))) + } + + case .failure(let error): + self.accepted = .failure(error) + } + } +} diff --git a/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift b/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift new file mode 100644 index 000000000..b0b794e9a --- /dev/null +++ b/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift @@ -0,0 +1,58 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension RPCAsyncSequence { + /// Returns an ``RPCAsyncSequence`` containing just the given element. + @_spi(Testing) + public static func one(_ element: Element) -> Self { + return Self(wrapping: AsyncSequenceOfOne(result: .success(element))) + } + + /// Returns an ``RPCAsyncSequence`` throwing the given error. + @_spi(Testing) + public static func throwing(_ error: E) -> Self { + return Self(wrapping: AsyncSequenceOfOne(result: .failure(error))) + } +} + +/// An `AsyncSequence` of a single value. +private struct AsyncSequenceOfOne: AsyncSequence { + private let result: Result + + init(result: Result) { + self.result = result + } + + func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(result: self.result) + } + + struct AsyncIterator: AsyncIteratorProtocol { + private var result: Result? + + fileprivate init(result: Result) { + self.result = result + } + + mutating func next() async throws -> Element? { + guard let result = self.result else { return nil } + + self.result = nil + return try result.get() + } + } +} diff --git a/Tests/GRPCCoreTests/Call/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/ClientRequestTests.swift new file mode 100644 index 000000000..a45f11621 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/ClientRequestTests.swift @@ -0,0 +1,31 @@ +/* + * Copyright 2023, 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. + */ +@_spi(Testing) import GRPCCore +import XCTest + +final class ClientRequestTests: XCTestCase { + func testSingleToStreamConversion() async throws { + let (messages, continuation) = AsyncStream.makeStream(of: String.self) + let single = ClientRequest.Single(message: "foo", metadata: ["bar": "baz"]) + let stream = ClientRequest.Stream(single: single) + + XCTAssertEqual(stream.metadata, ["bar": "baz"]) + try await stream.producer(.gathering(into: continuation)) + continuation.finish() + let collected = try await messages.collect() + XCTAssertEqual(collected, ["foo"]) + } +} diff --git a/Tests/GRPCCoreTests/Call/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/ClientResponseTests.swift new file mode 100644 index 000000000..5444075fe --- /dev/null +++ b/Tests/GRPCCoreTests/Call/ClientResponseTests.swift @@ -0,0 +1,171 @@ +/* + * Copyright 2023, 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. + */ +@_spi(Testing) import GRPCCore +import XCTest + +final class ClientResponseTests: XCTestCase { + func testAcceptedSingleResponseConvenienceMethods() { + let response = ClientResponse.Single( + message: "message", + metadata: ["foo": "bar"], + trailingMetadata: ["bar": "baz"] + ) + + XCTAssertEqual(response.metadata, ["foo": "bar"]) + XCTAssertEqual(try response.message, "message") + XCTAssertEqual(response.trailingMetadata, ["bar": "baz"]) + } + + func testRejectedSingleResponseConvenienceMethods() { + let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"]) + let response = ClientResponse.Single(of: String.self, error: error) + + XCTAssertEqual(response.metadata, [:]) + XCTAssertThrowsRPCError(try response.message) { + XCTAssertEqual($0, error) + } + XCTAssertEqual(response.trailingMetadata, ["bar": "baz"]) + } + + func testAcceptedStreamResponseConvenienceMethods() async throws { + let response = ClientResponse.Stream( + of: String.self, + metadata: ["foo": "bar"], + bodyParts: RPCAsyncSequence( + wrapping: AsyncStream { + $0.yield(.message("foo")) + $0.yield(.message("bar")) + $0.yield(.message("baz")) + $0.yield(.trailingMetadata(["baz": "baz"])) + $0.finish() + } + ) + ) + + XCTAssertEqual(response.metadata, ["foo": "bar"]) + let messages = try await response.messages.collect() + XCTAssertEqual(messages, ["foo", "bar", "baz"]) + } + + func testRejectedStreamResponseConvenienceMethods() async throws { + let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"]) + let response = ClientResponse.Stream(of: String.self, error: error) + + XCTAssertEqual(response.metadata, [:]) + await XCTAssertThrowsRPCErrorAsync { + try await response.messages.collect() + } errorHandler: { + XCTAssertEqual($0, error) + } + } + + func testStreamToSingleConversionForValidStream() async throws { + let stream = ClientResponse.Stream( + of: String.self, + metadata: ["foo": "bar"], + bodyParts: .elements(.message("foo"), .trailingMetadata(["bar": "baz"])) + ) + + let single = await ClientResponse.Single(stream: stream) + XCTAssertEqual(single.metadata, ["foo": "bar"]) + XCTAssertEqual(try single.message, "foo") + XCTAssertEqual(single.trailingMetadata, ["bar": "baz"]) + } + + func testStreamToSingleConversionForFailedStream() async throws { + let error = RPCError(code: .aborted, message: "aborted", metadata: ["bar": "baz"]) + let stream = ClientResponse.Stream(of: String.self, error: error) + + let single = await ClientResponse.Single(stream: stream) + XCTAssertEqual(single.metadata, [:]) + XCTAssertThrowsRPCError(try single.message) { + XCTAssertEqual($0, error) + } + XCTAssertEqual(single.trailingMetadata, ["bar": "baz"]) + } + + func testStreamToSingleConversionForInvalidSingleStream() async throws { + let bodies: [[ClientResponse.Stream.Contents.BodyPart]] = [ + [.message("1"), .message("2")], // Too many messages. + [.trailingMetadata([:])], // Too few messages + ] + + for body in bodies { + let stream = ClientResponse.Stream( + of: String.self, + metadata: ["foo": "bar"], + bodyParts: .elements(body) + ) + + let single = await ClientResponse.Single(stream: stream) + XCTAssertEqual(single.metadata, [:]) + XCTAssertThrowsRPCError(try single.message) { error in + XCTAssertEqual(error.code, .unimplemented) + } + XCTAssertEqual(single.trailingMetadata, [:]) + } + } + + func testStreamToSingleConversionForInvalidStream() async throws { + let bodies: [[ClientResponse.Stream.Contents.BodyPart]] = [ + [], // Empty stream + [.trailingMetadata([:]), .trailingMetadata([:])], // Multiple metadatas + [.trailingMetadata([:]), .message("")], // Metadata then message + ] + + for body in bodies { + let stream = ClientResponse.Stream( + of: String.self, + metadata: ["foo": "bar"], + bodyParts: .elements(body) + ) + + let single = await ClientResponse.Single(stream: stream) + XCTAssertEqual(single.metadata, [:]) + XCTAssertThrowsRPCError(try single.message) { error in + XCTAssertEqual(error.code, .internalError) + } + XCTAssertEqual(single.trailingMetadata, [:]) + } + } + + func testStreamToSingleConversionForStreamThrowingRPCError() async throws { + let error = RPCError(code: .dataLoss, message: "oops") + let stream = ClientResponse.Stream( + of: String.self, + metadata: [:], + bodyParts: .throwing(error) + ) + + let single = await ClientResponse.Single(stream: stream) + XCTAssertThrowsRPCError(try single.message) { + XCTAssertEqual($0, error) + } + } + + func testStreamToSingleConversionForStreamThrowingUnknownError() async throws { + let stream = ClientResponse.Stream( + of: String.self, + metadata: [:], + bodyParts: .throwing(CancellationError()) + ) + + let single = await ClientResponse.Single(stream: stream) + XCTAssertThrowsRPCError(try single.message) { error in + XCTAssertEqual(error.code, .unknown) + } + } +} diff --git a/Tests/GRPCCoreTests/Stream/AsyncSequenceOfOne.swift b/Tests/GRPCCoreTests/Stream/AsyncSequenceOfOne.swift new file mode 100644 index 000000000..00f9d4410 --- /dev/null +++ b/Tests/GRPCCoreTests/Stream/AsyncSequenceOfOne.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2023, 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. + */ +@_spi(Testing) import GRPCCore +import XCTest + +internal final class AsyncSequenceOfOneTests: XCTestCase { + func testSuccessPath() async throws { + let sequence = RPCAsyncSequence.one("foo") + let contents = try await sequence.collect() + XCTAssertEqual(contents, ["foo"]) + } + + func testFailurePath() async throws { + let sequence = RPCAsyncSequence.throwing(RPCError(code: .cancelled, message: "foo")) + + do { + let _ = try await sequence.collect() + XCTFail("Expected an error to be thrown") + } catch let error as RPCError { + XCTAssertEqual(error.code, .cancelled) + XCTAssertEqual(error.message, "foo") + } catch { + XCTFail("Expected error of type RPCError to be thrown") + } + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift new file mode 100644 index 000000000..2acd24bb6 --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift @@ -0,0 +1,36 @@ +/* + * Copyright 2023, 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. + */ + +extension AsyncSequence { + func collect() async throws -> [Element] { + return try await self.reduce(into: []) { $0.append($1) } + } +} + +#if swift(<5.9) +extension AsyncStream { + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift new file mode 100644 index 000000000..26bef603a --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift @@ -0,0 +1,32 @@ +/* + * Copyright 2023, 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 + +extension RPCAsyncSequence { + static func elements(_ elements: Element...) -> Self { + return .elements(elements) + } + + static func elements(_ elements: [Element]) -> Self { + let stream = AsyncStream { + for element in elements { + $0.yield(element) + } + $0.finish() + } + return RPCAsyncSequence(wrapping: stream) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift new file mode 100644 index 000000000..e334dceb2 --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2023, 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 + +extension RPCWriter { + /// Returns a writer which calls `XCTFail(_:)` on every write. + static func failTestOnWrite(elementType: Element.Type = Element.self) -> Self { + return RPCWriter(wrapping: FailOnWrite()) + } + + /// Returns a writer which gathers writes into an `AsyncStream`. + static func gathering(into continuation: AsyncStream.Continuation) -> Self { + return RPCWriter(wrapping: AsyncStreamGatheringWriter(continuation: continuation)) + } +} + +private struct FailOnWrite: RPCWriterProtocol { + func write(contentsOf elements: some Sequence) async throws { + XCTFail("Unexpected write") + } +} + +private struct AsyncStreamGatheringWriter: RPCWriterProtocol { + let continuation: AsyncStream.Continuation + + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + func write(contentsOf elements: some Sequence) async throws { + for element in elements { + self.continuation.yield(element) + } + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 275396d4f..aa51a4b98 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import GRPCCore import XCTest func XCTAssertDescription( @@ -23,3 +24,30 @@ func XCTAssertDescription( ) { XCTAssertEqual(String(describing: subject), expected, file: file, line: line) } + +func XCTAssertThrowsRPCError( + _ expression: @autoclosure () throws -> T, + _ errorHandler: (RPCError) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? RPCError else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + + errorHandler(error) + } +} + +func XCTAssertThrowsRPCErrorAsync( + _ expression: () async throws -> T, + errorHandler: (RPCError) -> Void +) async { + do { + _ = try await expression() + XCTFail("Expression didn't throw") + } catch let error as RPCError { + errorHandler(error) + } catch { + XCTFail("Error had unexpected type '\(type(of: error))'") + } +} From b92a24c308c0bba6c8fda9330fe3805a64073dce Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 9 Oct 2023 13:45:38 +0100 Subject: [PATCH 124/580] Add server request and response objects (#1667) --- Sources/GRPCCore/Call/ServerRequest.swift | 79 ++++ Sources/GRPCCore/Call/ServerResponse.swift | 344 ++++++++++++++++++ .../Call/ServerRequestTests.swift | 28 ++ .../Call/ServerResponseTests.swift | 105 ++++++ 4 files changed, 556 insertions(+) create mode 100644 Sources/GRPCCore/Call/ServerRequest.swift create mode 100644 Sources/GRPCCore/Call/ServerResponse.swift create mode 100644 Tests/GRPCCoreTests/Call/ServerRequestTests.swift create mode 100644 Tests/GRPCCoreTests/Call/ServerResponseTests.swift diff --git a/Sources/GRPCCore/Call/ServerRequest.swift b/Sources/GRPCCore/Call/ServerRequest.swift new file mode 100644 index 000000000..31bf0e1cf --- /dev/null +++ b/Sources/GRPCCore/Call/ServerRequest.swift @@ -0,0 +1,79 @@ +/* + * Copyright 2023, 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. + */ + +/// A namespace for request message types used by servers. +public enum ServerRequest {} + +extension ServerRequest { + /// A request received at the server containing a single message. + public struct Single: Sendable { + /// Metadata received from the client at the start of the RPC. + /// + /// The metadata contains gRPC and transport specific entries in addition to user-specified + /// metadata. + public var metadata: Metadata + + /// The message received from the client. + public var message: Message + + /// Create a new single server request. + /// + /// - Parameters: + /// - metadata: Metadata received from the client. + /// - messages: The message received from the client. + public init(metadata: Metadata, message: Message) { + self.metadata = metadata + self.message = message + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ServerRequest { + /// A request received at the server containing a stream of messages. + public struct Stream: Sendable { + /// Metadata received from the client at the start of the RPC. + /// + /// The metadata contains gRPC and transport specific entries in addition to user-specified + /// metadata. + public var metadata: Metadata + + /// A sequence of messages received from the client. + /// + /// The sequence may be iterated at most once. + public var messages: RPCAsyncSequence + + /// Create a new streaming request. + /// + /// - Parameters: + /// - metadata: Metadata received from the client. + /// - messages: A sequence of messages received from the client. + public init(metadata: Metadata, messages: RPCAsyncSequence) { + self.metadata = metadata + self.messages = messages + } + } +} + +// MARK: - Conversion + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ServerRequest.Stream { + @_spi(Testing) + public init(single request: ServerRequest.Single) { + self.init(metadata: request.metadata, messages: .one(request.message)) + } +} diff --git a/Sources/GRPCCore/Call/ServerResponse.swift b/Sources/GRPCCore/Call/ServerResponse.swift new file mode 100644 index 000000000..5fbdb43ec --- /dev/null +++ b/Sources/GRPCCore/Call/ServerResponse.swift @@ -0,0 +1,344 @@ +/* + * Copyright 2023, 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. + */ + +/// A namespace for response message types used by servers. +public enum ServerResponse {} + +extension ServerResponse { + /// A response for a single message sent by a server. + /// + /// Single responses are used for unary and client-streaming RPCs. For streaming responses + /// see ``ServerResponse/Stream``. + /// + /// A single response captures every part of the response stream and distinguishes successful + /// and unsuccessful responses via the ``accepted`` property. The value for the `success` case + /// contains the initial metadata, response message, and the trailing metadata and implicitly + /// has an ``Status/Code-swift.struct/ok`` status code. + /// + /// The `failure` case indicates that the server chose not to process the RPC, or the processing + /// of the RPC failed. The failure case contains an ``RPCError`` describing why the RPC failed, + /// including an error code, error message and any metadata sent by the server. + /// + /// ### Using ``Single`` responses + /// + /// Each response has an ``accepted`` property which contains all RPC information. You can create + /// one by calling ``init(accepted:)`` or one of the two convenience initializers: + /// - ``init(message:metadata:trailingMetadata:)`` to create a successful response, or + /// - ``init(of:error:)`` to create a failed response. + /// + /// You can interrogate a response by inspecting the ``accepted`` property directly or by using + /// its convenience properties: + /// - ``metadata`` extracts the initial metadata, + /// - ``message`` extracts the message, or throws if the response failed, and + /// - ``trailingMetadata`` extracts the trailing metadata. + /// + /// The following example demonstrates how you can use the API: + /// + /// ```swift + /// // Create a successful response + /// let response = ServerResponse.Single( + /// message: "Hello, World!", + /// metadata: ["hello": "initial metadata"], + /// trailingMetadata: ["goodbye": "trailing metadata"] + /// ) + /// + /// // The explicit API: + /// switch response { + /// case .success(let contents): + /// print("Received response with message '\(contents.message)'") + /// case .failure(let error): + /// print("RPC failed with code '\(error.code)'") + /// } + /// + /// // The convenience API: + /// do { + /// print("Received response with message '\(try response.message)'") + /// } catch let error as RPCError { + /// print("RPC failed with code '\(error.code)'") + /// } + /// ``` + public struct Single: Sendable { + /// An accepted RPC with a successful outcome. + public struct Contents: Sendable { + /// Caller-specified metadata to send to the client at the start of the response. + /// + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport + /// specific metadata. Note that transports may also impose limits in the amount of metadata + /// which may be sent. + public var metadata: Metadata + + /// The message to send to the client. + public var message: Message + + /// Caller-specified metadata to send to the client at the end of the response. + /// + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport + /// specific metadata. Note that transports may also impose limits in the amount of metadata + /// which may be sent. + public var trailingMetadata: Metadata + + /// Create a new single client request. + /// + /// - Parameters: + /// - message: The message to send to the server. + /// - metadata: Metadata to send to the client at the start of the response. Defaults to + /// empty. + /// - trailingMetadata: Metadata to send to the client at the end of the response. Defaults + /// to empty. + public init( + message: Message, + metadata: Metadata = [:], + trailingMetadata: Metadata = [:] + ) { + self.metadata = metadata + self.message = message + self.trailingMetadata = trailingMetadata + } + } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` indicates the server accepted the RPC for processing and the RPC completed + /// successfully and implies the RPC succeeded with the ``Status/Code-swift.struct/ok`` status + /// code. The `failure` case indicates that the service rejected the RPC without processing it + /// or could not process it successfully. + public var accepted: Result + + /// Creates a response. + /// + /// - Parameter accepted: Whether the RPC was accepted or rejected. + public init(accepted: Result) { + self.accepted = accepted + } + } +} + +extension ServerResponse { + /// A response for a stream of messages sent by a server. + /// + /// Stream responses are used for server-streaming and bidirectional-streaming RPCs. For single + /// responses see ``ServerResponse/Single``. + /// + /// A stream response captures every part of the response stream and distinguishes whether the + /// request was processed by the server via the ``accepted`` property. The value for the `success` + /// case contains the initial metadata and a closure which is provided with a message write and + /// returns trailing metadata. If the closure returns without error then the response implicitly + /// has an ``Status/Code-swift.struct/ok`` status code. You can throw an error from the producer + /// to indicate that the request couldn't be handled successfully. If an ``RPCError`` is thrown + /// then the client will receive an equivalent error populated with the same code and message. If + /// an error of any other type is thrown then the client will receive an error with the + /// ``Status/Code-swift.struct/unknown`` status code. + /// + /// The `failure` case indicates that the server chose not to process the RPC. The failure case + /// contains an ``RPCError`` describing why the RPC failed, including an error code, error + /// message and any metadata to send to the client. + /// + /// ### Using ``Stream`` responses + /// + /// Each response has an ``accepted`` property which contains all RPC information. You can create + /// one by calling ``init(accepted:)`` or one of the two convenience initializers: + /// - ``init(of:metadata:producer:)`` to create a successful response, or + /// - ``init(of:error:)`` to create a failed response. + /// + /// You can interrogate a response by inspecting the ``accepted`` property directly. The following + /// example demonstrates how you can use the API: + /// + /// ```swift + /// // Create a successful response + /// let response = ServerResponse.Stream( + /// of: String.self, + /// metadata: ["hello": "initial metadata"] + /// ) { writer in + /// // Write a few messages. + /// try await writer.write("Hello") + /// try await writer.write("World") + /// + /// // Send trailing metadata to the client. + /// return ["goodbye": "trailing metadata"] + /// } + /// ``` + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + public struct Stream: Sendable { + /// The contents of a response to a request which has been accepted for processing. + public struct Contents: Sendable { + /// Metadata to send to the client at the beginning of the response stream. + public var metadata: Metadata + + /// A closure which, when called, writes values into the provided writer and returns trailing + /// metadata indicating the end of the response stream. + /// + /// Returning metadata indicates a successful response and gRPC will terminate the RPC with + /// an ``Status/Code-swift.struct/ok`` status code. Throwing an error will terminate the RPC + /// with an appropriate status code. You can control the status code, message and metadata + /// returned to the client by throwing an ``RPCError``. If the error thrown is a type other + /// than ``RPCError`` then a status with code ``Status/Code-swift.struct/unknown`` will + /// be returned to the client. + /// + /// gRPC will invoke this function at most once therefore it isn't required to be idempotent. + public var producer: @Sendable (RPCWriter) async throws -> Metadata + + /// Create a ``Contents``. + /// + /// - Parameters: + /// - metadata: Metadata to send to the client at the start of the response. + /// - producer: A function which produces values + public init( + metadata: Metadata, + producer: @escaping @Sendable (RPCWriter) async throws -> Metadata + ) { + self.metadata = metadata + self.producer = producer + } + } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` case indicates that the service accepted the RPC for processing and will + /// send initial metadata back to the client before producing response messages. The RPC may + /// still result in failure by later throwing an error. + /// + /// The `failure` case indicates that the server rejected the RPC and will not process it. Only + /// the status and trailing metadata will be sent to the client. + public var accepted: Result + + /// Creates a response. + /// + /// - Parameter accepted: Whether the RPC was accepted or rejected. + public init(accepted: Result) { + self.accepted = accepted + } + } +} + +extension ServerResponse.Single { + /// Creates a new accepted response. + /// + /// - Parameters: + /// - metadata: Metadata to send to the client at the beginning of the response. + /// - message: The response message to send to the client. + /// - trailingMetadata: Metadata to send to the client at the end of the response. + public init(message: Message, metadata: Metadata = [:], trailingMetadata: Metadata = [:]) { + let contents = Contents( + message: message, + metadata: metadata, + trailingMetadata: trailingMetadata + ) + self.accepted = .success(contents) + } + + /// Creates a new failed response. + /// + /// - Parameters: + /// - messageType: The type of message. + /// - error: An error describing why the RPC failed. + public init(of messageType: Message.Type = Message.self, error: RPCError) { + self.accepted = .failure(error) + } + + /// Returns the metadata to be sent to the client at the start of the response. + /// + /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty. + public var metadata: Metadata { + switch self.accepted { + case let .success(contents): + return contents.metadata + case .failure: + return [:] + } + } + + /// Returns the message to send to the client. + /// + /// - Throws: ``RPCError`` if the request failed. + public var message: Message { + get throws { + try self.accepted.map { $0.message }.get() + } + } + + /// Returns metadata to be sent to the client at the end of the response. + /// + /// Unlike ``metadata``, for rejected RPCs the metadata returned may contain values. + public var trailingMetadata: Metadata { + switch self.accepted { + case let .success(contents): + return contents.trailingMetadata + case let .failure(error): + return error.metadata + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ServerResponse.Stream { + /// Creates a new accepted response. + /// + /// - Parameters: + /// - messageType: The type of message. + /// - metadata: Metadata to send to the client at the beginning of the response. + /// - producer: A closure which, when called, writes messages to the client. + public init( + of messageType: Message.Type = Message.self, + metadata: Metadata = [:], + producer: @escaping @Sendable (RPCWriter) async throws -> Metadata + ) { + let contents = Contents(metadata: metadata, producer: producer) + self.accepted = .success(contents) + } + + /// Creates a new failed response. + /// + /// - Parameters: + /// - messageType: The type of message. + /// - error: An error describing why the RPC failed. + public init(of messageType: Message.Type = Message.self, error: RPCError) { + self.accepted = .failure(error) + } + + /// Returns metadata received from the server at the start of the response. + /// + /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty. + public var metadata: Metadata { + switch self.accepted { + case let .success(contents): + return contents.metadata + case .failure: + return [:] + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ServerResponse.Stream { + @_spi(Testing) + public init(single response: ServerResponse.Single) { + switch response.accepted { + case .success(let contents): + let contents = Contents(metadata: contents.metadata) { + try await $0.write(contents.message) + return contents.trailingMetadata + } + self.accepted = .success(contents) + + case .failure(let error): + self.accepted = .failure(error) + } + } +} diff --git a/Tests/GRPCCoreTests/Call/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/ServerRequestTests.swift new file mode 100644 index 000000000..b83c242e8 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/ServerRequestTests.swift @@ -0,0 +1,28 @@ +/* + * Copyright 2023, 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. + */ +@_spi(Testing) import GRPCCore +import XCTest + +final class ServerRequestTests: XCTestCase { + func testSingleToStreamConversion() async throws { + let single = ServerRequest.Single(metadata: ["bar": "baz"], message: "foo") + let stream = ServerRequest.Stream(single: single) + + XCTAssertEqual(stream.metadata, ["bar": "baz"]) + let collected = try await stream.messages.collect() + XCTAssertEqual(collected, ["foo"]) + } +} diff --git a/Tests/GRPCCoreTests/Call/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/ServerResponseTests.swift new file mode 100644 index 000000000..7f7fdb288 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/ServerResponseTests.swift @@ -0,0 +1,105 @@ +/* + * Copyright 2023, 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. + */ +@_spi(Testing) import GRPCCore +import XCTest + +final class ServerResponseTests: XCTestCase { + func testSingleConvenienceInit() { + var response = ServerResponse.Single( + message: "message", + metadata: ["metadata": "initial"], + trailingMetadata: ["metadata": "trailing"] + ) + + switch response.accepted { + case .success(let contents): + XCTAssertEqual(contents.message, "message") + XCTAssertEqual(contents.metadata, ["metadata": "initial"]) + XCTAssertEqual(contents.trailingMetadata, ["metadata": "trailing"]) + case .failure: + XCTFail("Unexpected error") + } + + let error = RPCError(code: .aborted, message: "Aborted") + response = ServerResponse.Single(of: String.self, error: error) + switch response.accepted { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, error) + } + } + + func testStreamConvenienceInit() async throws { + var response = ServerResponse.Stream(of: String.self, metadata: ["metadata": "initial"]) { _ in + // Empty body. + return ["metadata": "trailing"] + } + + switch response.accepted { + case .success(let contents): + XCTAssertEqual(contents.metadata, ["metadata": "initial"]) + let trailingMetadata = try await contents.producer(.failTestOnWrite()) + XCTAssertEqual(trailingMetadata, ["metadata": "trailing"]) + case .failure: + XCTFail("Unexpected error") + } + + let error = RPCError(code: .aborted, message: "Aborted") + response = ServerResponse.Stream(of: String.self, error: error) + switch response.accepted { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, error) + } + } + + func testSingleToStreamConversionForSuccessfulResponse() async throws { + let single = ServerResponse.Single( + message: "foo", + metadata: ["metadata": "initial"], + trailingMetadata: ["metadata": "trailing"] + ) + + let stream = ServerResponse.Stream(single: single) + let (messages, continuation) = AsyncStream.makeStream(of: String.self) + let trailingMetadata: Metadata + + switch stream.accepted { + case .success(let contents): + trailingMetadata = try await contents.producer(.gathering(into: continuation)) + continuation.finish() + case .failure(let error): + throw error + } + + XCTAssertEqual(stream.metadata, ["metadata": "initial"]) + let collected = try await messages.collect() + XCTAssertEqual(collected, ["foo"]) + XCTAssertEqual(trailingMetadata, ["metadata": "trailing"]) + } + + func testSingleToStreamConversionForFailedResponse() async throws { + let error = RPCError(code: .aborted, message: "aborted") + let single = ServerResponse.Single(of: String.self, error: error) + let stream = ServerResponse.Stream(single: single) + + XCTAssertThrowsRPCError(try stream.accepted.get()) { + XCTAssertEqual($0, error) + } + } +} From 6d8108c7ed8707dc87a1753d0e29687a78ca3aee Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 9 Oct 2023 16:09:35 +0100 Subject: [PATCH 125/580] Interceptor APIs for client and server (#1668) --- .../Call/Client/ClientInterceptor.swift | 121 ++++++++++++++++++ .../ClientRPCExecutionConfiguration.swift | 0 .../Call/{ => Client}/ClientRequest.swift | 0 .../Call/{ => Client}/ClientResponse.swift | 0 .../Call/Server/ServerInterceptor.swift | 94 ++++++++++++++ .../Call/{ => Server}/ServerRequest.swift | 0 .../Call/{ => Server}/ServerResponse.swift | 0 ...ClientRPCExecutionConfigurationTests.swift | 0 .../{ => Client}/ClientRequestTests.swift | 0 .../{ => Client}/ClientResponseTests.swift | 0 .../{ => Server}/ServerRequestTests.swift | 0 .../{ => Server}/ServerResponseTests.swift | 0 12 files changed, 215 insertions(+) create mode 100644 Sources/GRPCCore/Call/Client/ClientInterceptor.swift rename Sources/GRPCCore/Call/{ => Client}/ClientRPCExecutionConfiguration.swift (100%) rename Sources/GRPCCore/Call/{ => Client}/ClientRequest.swift (100%) rename Sources/GRPCCore/Call/{ => Client}/ClientResponse.swift (100%) create mode 100644 Sources/GRPCCore/Call/Server/ServerInterceptor.swift rename Sources/GRPCCore/Call/{ => Server}/ServerRequest.swift (100%) rename Sources/GRPCCore/Call/{ => Server}/ServerResponse.swift (100%) rename Tests/GRPCCoreTests/Call/{ => Client}/ClientRPCExecutionConfigurationTests.swift (100%) rename Tests/GRPCCoreTests/Call/{ => Client}/ClientRequestTests.swift (100%) rename Tests/GRPCCoreTests/Call/{ => Client}/ClientResponseTests.swift (100%) rename Tests/GRPCCoreTests/Call/{ => Server}/ServerRequestTests.swift (100%) rename Tests/GRPCCoreTests/Call/{ => Server}/ServerResponseTests.swift (100%) diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift new file mode 100644 index 000000000..bbdaf9b30 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -0,0 +1,121 @@ +/* + * Copyright 2023, 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. + */ + +/// A type that intercepts requests and response for clients. +/// +/// Interceptors allow you to inspect and modify requests and responses. Requests are intercepted +/// before they are handed to a transport and responses are intercepted after they have been +/// received from the transport. They are typically used for cross-cutting concerns like injecting +/// metadata, validating messages, logging additional data, and tracing. +/// +/// Interceptors are registered with a client and apply to all RPCs. If you need to modify the +/// behavior of an interceptor on a per-RPC basis then you can use the +/// ``ClientInterceptorContext/descriptor`` to determine which RPC is being called and +/// conditionalise behavior accordingly. +/// +/// - TODO: Update example and documentation to show how to register an interceptor. +/// +/// Some examples of simple interceptors follow. +/// +/// ## Metadata injection +/// +/// A common use-case for client interceptors is injecting metadata into a request. +/// +/// ```swift +/// struct MetadataInjectingClientInterceptor: ClientInterceptor { +/// let key: String +/// let fetchMetadata: @Sendable () async -> String +/// +/// func intercept( +/// request: ClientRequest.Stream, +/// context: ClientInterceptorContext, +/// next: @Sendable ( +/// _ request: ClientRequest.Stream, +/// _ context: ClientInterceptorContext +/// ) async throws -> ClientResponse.Stream +/// ) async throws -> ClientResponse.Stream { +/// // Fetch the metadata value and attach it. +/// let value = await self.fetchMetadata() +/// var request = request +/// request.metadata[self.key] = value +/// +/// // Forward the request to the next interceptor. +/// return try await next(request, context) +/// } +/// } +/// ``` +/// +/// Interceptors can also be used to print information about RPCs. +/// +/// ## Logging interceptor +/// +/// ```swift +/// struct LoggingClientInterceptor: ClientInterceptor { +/// func intercept( +/// request: ClientRequest.Stream, +/// context: ClientInterceptorContext, +/// next: @Sendable ( +/// _ request: ClientRequest.Stream, +/// _ context: ClientInterceptorContext +/// ) async throws -> ClientResponse.Stream +/// ) async throws -> ClientResponse.Stream { +/// print("Invoking method '\(context.descriptor)'") +/// let response = try await next(request, context) +/// +/// switch response.accepted { +/// case .success: +/// print("Server accepted RPC for processing") +/// case .failure(let error): +/// print("Server rejected RPC with error '\(error)'") +/// } +/// +/// return response +/// } +/// } +/// ``` +/// +/// For server-side interceptors see ``ServerInterceptor``. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol ClientInterceptor: Sendable { + /// Intercept a request object. + /// + /// - Parameters: + /// - request: The request object. + /// - context: Additional context about the request, including a descriptor + /// of the method being called. + /// - next: A closure to invoke to hand off the request and context to the next + /// interceptor in the chain. + /// - Returns: A response object. + func intercept( + request: ClientRequest.Stream, + context: ClientInterceptorContext, + next: @Sendable ( + _ request: ClientRequest.Stream, + _ context: ClientInterceptorContext + ) async throws -> ClientResponse.Stream + ) async throws -> ClientResponse.Stream +} + +/// A context passed to client interceptors containing additional information about the RPC. +public struct ClientInterceptorContext: Sendable { + /// A description of the method being called. + public var descriptor: MethodDescriptor + + /// Create a new client interceptor context. + public init(descriptor: MethodDescriptor) { + self.descriptor = descriptor + } +} diff --git a/Sources/GRPCCore/Call/ClientRPCExecutionConfiguration.swift b/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfiguration.swift similarity index 100% rename from Sources/GRPCCore/Call/ClientRPCExecutionConfiguration.swift rename to Sources/GRPCCore/Call/Client/ClientRPCExecutionConfiguration.swift diff --git a/Sources/GRPCCore/Call/ClientRequest.swift b/Sources/GRPCCore/Call/Client/ClientRequest.swift similarity index 100% rename from Sources/GRPCCore/Call/ClientRequest.swift rename to Sources/GRPCCore/Call/Client/ClientRequest.swift diff --git a/Sources/GRPCCore/Call/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift similarity index 100% rename from Sources/GRPCCore/Call/ClientResponse.swift rename to Sources/GRPCCore/Call/Client/ClientResponse.swift diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift new file mode 100644 index 000000000..ca115ae43 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -0,0 +1,94 @@ +/* + * Copyright 2023, 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. + */ + +/// A type that intercepts requests and response for server. +/// +/// Interceptors allow you to inspect and modify requests and responses. Requests are intercepted +/// after they have been received by the transport and responses are intercepted after they have +/// been returned from a service. They are typically used for cross-cutting concerns like filtering +/// requests, validating messages, logging additional data, and tracing. +/// +/// Interceptors are registered with the server apply to all RPCs. If you need to modify the +/// behavior of an interceptor on a per-RPC basis then you can use the +/// ``ServerInterceptorContext/descriptor`` to determine which RPC is being called and +/// conditionalise behavior accordingly. +/// +/// - TODO: Update example and documentation to show how to register an interceptor. +/// +/// ## RPC filtering +/// +/// A common use of server-side interceptors is to filter requests from clients. Interceptors can +/// reject requests which are invalid without service code being called. The following example +/// demonstrates this. +/// +/// ```swift +/// struct AuthServerInterceptor: Sendable { +/// let isAuthorized: @Sendable (String, MethodDescriptor) async throws -> Void +/// +/// func intercept( +/// request: ServerRequest.Stream, +/// context: ServerInterceptorContext, +/// next: @Sendable ( +/// _ request: ServerRequest.Stream, +/// _ context: ServerInterceptorContext +/// ) async throws -> ServerResponse.Stream +/// ) async throws -> ServerResponse.Stream { +/// // Extract the auth token. +/// guard let token = request.metadata["authorization"] else { +/// throw RPCError(code: .unauthenticated, message: "Not authenticated") +/// } +/// +/// // Check whether it's valid. +/// try await self.isAuthorized(token, context.descriptor) +/// +/// // Forward the request. +/// return try await next(request, context) +/// } +/// } +/// ``` +/// +/// For server-side interceptors see ``ClientInterceptor``. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol ServerInterceptor: Sendable { + /// Intercept a request object. + /// + /// - Parameters: + /// - request: The request object. + /// - context: Additional context about the request, including a descriptor + /// of the method being called. + /// - next: A closure to invoke to hand off the request and context to the next + /// interceptor in the chain. + /// - Returns: A response object. + func intercept( + request: ServerRequest.Stream, + context: ServerInterceptorContext, + next: @Sendable ( + _ request: ServerRequest.Stream, + _ context: ServerInterceptorContext + ) async throws -> ServerResponse.Stream + ) async throws -> ServerResponse.Stream +} + +/// A context passed to client interceptors containing additional information about the RPC. +public struct ServerInterceptorContext: Sendable { + /// A description of the method being called. + public var descriptor: MethodDescriptor + + /// Create a new client interceptor context. + public init(descriptor: MethodDescriptor) { + self.descriptor = descriptor + } +} diff --git a/Sources/GRPCCore/Call/ServerRequest.swift b/Sources/GRPCCore/Call/Server/ServerRequest.swift similarity index 100% rename from Sources/GRPCCore/Call/ServerRequest.swift rename to Sources/GRPCCore/Call/Server/ServerRequest.swift diff --git a/Sources/GRPCCore/Call/ServerResponse.swift b/Sources/GRPCCore/Call/Server/ServerResponse.swift similarity index 100% rename from Sources/GRPCCore/Call/ServerResponse.swift rename to Sources/GRPCCore/Call/Server/ServerResponse.swift diff --git a/Tests/GRPCCoreTests/Call/ClientRPCExecutionConfigurationTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift similarity index 100% rename from Tests/GRPCCoreTests/Call/ClientRPCExecutionConfigurationTests.swift rename to Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift diff --git a/Tests/GRPCCoreTests/Call/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift similarity index 100% rename from Tests/GRPCCoreTests/Call/ClientRequestTests.swift rename to Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift diff --git a/Tests/GRPCCoreTests/Call/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift similarity index 100% rename from Tests/GRPCCoreTests/Call/ClientResponseTests.swift rename to Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift diff --git a/Tests/GRPCCoreTests/Call/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift similarity index 100% rename from Tests/GRPCCoreTests/Call/ServerRequestTests.swift rename to Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift diff --git a/Tests/GRPCCoreTests/Call/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift similarity index 100% rename from Tests/GRPCCoreTests/Call/ServerResponseTests.swift rename to Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift From 178e102033bd27e6bb61d22584bf707ff1a70155 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 9 Oct 2023 16:59:50 +0100 Subject: [PATCH 126/580] Message encoder/decoder (#1664) --- Sources/GRPCCore/Coding/Coding.swift | 53 +++++++++++++++++++ Tests/GRPCCoreTests/Coding/CodingTests.swift | 42 +++++++++++++++ .../Test Utilities/Coding+JSON.swift | 43 +++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 Sources/GRPCCore/Coding/Coding.swift create mode 100644 Tests/GRPCCoreTests/Coding/CodingTests.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift diff --git a/Sources/GRPCCore/Coding/Coding.swift b/Sources/GRPCCore/Coding/Coding.swift new file mode 100644 index 000000000..29569d3c0 --- /dev/null +++ b/Sources/GRPCCore/Coding/Coding.swift @@ -0,0 +1,53 @@ +/* + * Copyright 2023, 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. + */ + +/// Serializes a message into a sequence of bytes. +/// +/// Message serializers convert an input message to a sequence of bytes. Serializers are used to +/// convert messages into a form which is suitable for sending over a network. The reverse +/// operation, deserialization, is performed by a ``MessageDeserializer``. +/// +/// Serializers are used frequently and implementations should take care to ensure that +/// serialization is as cheap as possible. +public protocol MessageSerializer: Sendable { + /// The type of message this serializer can serialize. + associatedtype Message + + /// Serializes a ``Message`` into a sequence of bytes. + /// + /// - Parameter message: The message to serialize. + /// - Returns: The serialized bytes of a message. + func serialize(_ message: Message) throws -> [UInt8] +} + +/// Deserializes a sequence of bytes into a message. +/// +/// Message deserializers convert a sequence of bytes into a message. Deserializers are used to +/// convert bytes received from the network into an application specific message. The reverse +/// operation, serialization, is performed by a ``MessageSerializer``. +/// +/// Deserializers are used frequently and implementations should take care to ensure that +/// deserialization is as cheap as possible. +public protocol MessageDeserializer: Sendable { + /// The type of message this deserializer can deserialize. + associatedtype Message + + /// Deserializes a sequence of bytes into a ``Message``. + /// + /// - Parameter serializedMessageBytes: The bytes to deserialize. + /// - Returns: The deserialized message. + func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message +} diff --git a/Tests/GRPCCoreTests/Coding/CodingTests.swift b/Tests/GRPCCoreTests/Coding/CodingTests.swift new file mode 100644 index 000000000..efb57f94f --- /dev/null +++ b/Tests/GRPCCoreTests/Coding/CodingTests.swift @@ -0,0 +1,42 @@ +/* + * Copyright 2023, 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 CodingTests: XCTestCase { + func testJSONRoundtrip() throws { + // This test just demonstrates that the API is suitable. + + struct Message: Codable, Hashable { + var foo: String + var bar: Int + var baz: Baz + + struct Baz: Codable, Hashable { + var bazzy: Double + } + } + + let message = Message(foo: "foo", bar: 42, baz: .init(bazzy: 3.1415)) + + let serializer = JSONSerializer() + let deserializer = JSONDeserializer() + + let bytes = try serializer.serialize(message) + let roundTrip = try deserializer.deserialize(bytes) + XCTAssertEqual(roundTrip, message) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift new file mode 100644 index 000000000..da49f7bd2 --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift @@ -0,0 +1,43 @@ +/* + * Copyright 2023, 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 struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder + +private let jsonEncoder = JSONEncoder() +private let jsonDecoder = JSONDecoder() + +struct JSONSerializer: MessageSerializer { + func serialize(_ message: Message) throws -> [UInt8] { + do { + return try Array(jsonEncoder.encode(message)) + } catch { + throw RPCError(code: .internalError, message: "Can't serialize message to JSON. \(error)") + } + } +} + +struct JSONDeserializer: MessageDeserializer { + func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message { + do { + return try jsonDecoder.decode(Message.self, from: Data(serializedMessageBytes)) + } catch { + throw RPCError(code: .internalError, message: "Can't deserialze message from JSON. \(error)") + } + } +} From 69dafd8fec2fb3dfcd5b651d5a36b10cc5284e1f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 10 Oct 2023 13:02:02 +0100 Subject: [PATCH 127/580] Add missing deque import (#1670) Motivation: GRPC imports DequeModule but doesn't explicitly depend on it in the package manifest. Modifications: - Add missing dependcy Result: Package manifest more closely represents actual dependencies --- Package.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Package.swift b/Package.swift index 9475ee8c0..61447f772 100644 --- a/Package.swift +++ b/Package.swift @@ -46,6 +46,10 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-nio-extras.git", from: "1.4.0" ), + .package( + url: "https://github.com/apple/swift-collections.git", + from: "1.0.5" + ), .package( url: "https://github.com/apple/swift-protobuf.git", from: "1.20.2" @@ -120,6 +124,7 @@ extension Target.Dependency { name: "SwiftProtobufPluginLibrary", package: "swift-protobuf" ) + static let dequeModule: Self = .product(name: "DequeModule", package: "swift-collections") static let grpcCore: Self = .target(name: "GRPCCore") } @@ -143,6 +148,7 @@ extension Target { .nioExtras, .logging, .protobuf, + .dequeModule, ].appending( .nioSSL, if: includeNIOSSL ), From ac2749584ce047a320b45f7e782d0f9ceb8a8fe9 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 10 Oct 2023 13:51:35 +0100 Subject: [PATCH 128/580] add concurrency avail guard to AsyncSequenceOfOne (#1671) Motivation: Building fails with ``` error: concurrency is only available in macOS 10.15.0 or newer ``` Modifications: Add an availability guard to `AsyncSequenceOfOne` Result: Code will build. --- Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift b/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift index b0b794e9a..bc58be2f3 100644 --- a/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift +++ b/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift @@ -30,6 +30,7 @@ extension RPCAsyncSequence { } /// An `AsyncSequence` of a single value. +@available(macOS 10.15, iOS 13.0, tvOS 13, watchOS 6, *) private struct AsyncSequenceOfOne: AsyncSequence { private let result: Result From 0fde772d8bd74f3d35f435ca55ec12b7fbcbd4e8 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Wed, 11 Oct 2023 10:31:46 +0100 Subject: [PATCH 129/580] ClientRPCExecutionConfigurationTests only available on macOS 13... (#1672) --- .../Call/Client/ClientRPCExecutionConfigurationTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift index 6021d1a24..768b808b2 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class ClientRPCExecutionConfigurationTests: XCTestCase { func testRetryPolicyClampsMaxAttempts() { var policy = RetryPolicy( From 8f03192fd35770580a33baa08b894cb4f5c980c9 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:01:06 +0100 Subject: [PATCH 130/580] Implemented file-by-filename and list-services requests (#1669) Motivation: The two requests are necessary for implementing the Reflection Service. Modifications: Implemented the ReflectionServiceData struct that stores the useful information from the file descriptor protos received when initiaizing the service and provides the data used for constructing the server response for each type of request. Also imlemented the functions that create the server responses for the two requests. The requests and the functionality of the ReflectionServiceData struct's methods are tested in the "Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift" file. Result: The Reflection Service will support the file-by-filename and list-services requests. --- Makefile | 21 + Package.swift | 1 + .../Model/reflection.pb.swift | 2 +- .../Server/ReflectionProvider.swift | 29 - .../Server/ReflectionService.swift | 189 ++++ .../GRPCReflectionServiceTests.swift | 422 +++++++++ .../Generated/reflection.grpc.swift | 208 +++++ .../Generated/reflection.pb.swift | 843 ++++++++++++++++++ 8 files changed, 1685 insertions(+), 30 deletions(-) delete mode 100644 Sources/GRPCReflectionService/Server/ReflectionProvider.swift create mode 100644 Sources/GRPCReflectionService/Server/ReflectionService.swift create mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift create mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift create mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift diff --git a/Makefile b/Makefile index c908ea67d..23d253ca8 100644 --- a/Makefile +++ b/Makefile @@ -147,6 +147,27 @@ ${REFLECTION_GRPC}: ${REFLECTION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} .PHONY: generate-reflection: ${REFLECTION_PB} ${REFLECTION_GRPC} +TEST_REFLECTION_GRPC=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift +TEST_REFLECTION_PB=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift + +# For Reflection we'll generate only the Server code. +${TEST_REFLECTION_GRPC}: ${REFLECTION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=true,Server=false \ + --grpc-swift_out=$(dir ${TEST_REFLECTION_GRPC}) + +${TEST_REFLECTION_PB}: ${REFLECTION_PROTO} ${PROTOC_GEN_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_SWIFT} \ + --swift_out=$(dir ${TEST_REFLECTION_PB}) + +# Generates protobufs and gRPC client for the Reflection Service Tests +.PHONY: +generate-reflection-client: ${TEST_REFLECTION_PB} ${TEST_REFLECTION_GRPC} + ### Testing #################################################################### # Normal test suite. diff --git a/Package.swift b/Package.swift index 61447f772..99effbe88 100644 --- a/Package.swift +++ b/Package.swift @@ -207,6 +207,7 @@ extension Target { .nioEmbedded, .nioTransportServices, .logging, + .reflectionService, ].appending( .nioSSL, if: includeNIOSSL ), diff --git a/Sources/GRPCReflectionService/Model/reflection.pb.swift b/Sources/GRPCReflectionService/Model/reflection.pb.swift index 81bb72e3f..f86e9577c 100644 --- a/Sources/GRPCReflectionService/Model/reflection.pb.swift +++ b/Sources/GRPCReflectionService/Model/reflection.pb.swift @@ -7,7 +7,7 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -// Copyright 2023 The gRPC Authors +// Copyright 2016 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/GRPCReflectionService/Server/ReflectionProvider.swift b/Sources/GRPCReflectionService/Server/ReflectionProvider.swift deleted file mode 100644 index 7b49df005..000000000 --- a/Sources/GRPCReflectionService/Server/ReflectionProvider.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2023, 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 Foundation -import GRPC - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -final class ReflectionService: Reflection_ServerReflectionAsyncProvider { - func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - throw GRPCStatus(code: .unimplemented) - } -} diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift new file mode 100644 index 000000000..d28c1301e --- /dev/null +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -0,0 +1,189 @@ +/* + * Copyright 2023, 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 DequeModule +import Foundation +import GRPC +import SwiftProtobuf + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public final class ReflectionService: CallHandlerProvider, Sendable { + private let reflectionService: ReflectionServiceProvider + public var serviceName: Substring { + self.reflectionService.serviceName + } + + public init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { + self.reflectionService = try ReflectionServiceProvider(fileDescriptorProtos: fileDescriptors) + } + + public func handle( + method name: Substring, + context: GRPC.CallHandlerContext + ) -> GRPC.GRPCServerHandlerProtocol? { + self.reflectionService.handle(method: name, context: context) + } +} + +internal struct ReflectionServiceData: Sendable { + internal struct FileDescriptorProtoData: Sendable { + internal var serializedFileDescriptorProto: Data + internal var dependencyFileNames: [String] + } + + internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData] + internal var serviceNames: [String] + + internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { + self.serviceNames = [] + self.fileDescriptorDataByFilename = [:] + for fileDescriptorProto in fileDescriptors { + let serializedFileDescriptorProto: Data + do { + serializedFileDescriptorProto = try fileDescriptorProto.serializedData() + } catch { + throw GRPCStatus( + code: .invalidArgument, + message: + "The \(fileDescriptorProto.name) could not be serialized." + ) + } + let protoData = FileDescriptorProtoData( + serializedFileDescriptorProto: serializedFileDescriptorProto, + dependencyFileNames: fileDescriptorProto.dependency + ) + self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData + self.serviceNames.append(contentsOf: fileDescriptorProto.service.map { $0.name }) + } + } + + internal func serialisedFileDescriptorProtosForDependenciesOfFile( + named fileName: String + ) throws -> [Data] { + var toVisit = Deque() + var visited = Set() + var serializedFileDescriptorProtos: [Data] = [] + toVisit.append(fileName) + + while let currentFileName = toVisit.popFirst() { + if let protoData = self.fileDescriptorDataByFilename[currentFileName] { + toVisit.append( + contentsOf: protoData.dependencyFileNames + .filter { name in + return !visited.contains(name) + } + ) + + let serializedFileDescriptorProto = protoData.serializedFileDescriptorProto + serializedFileDescriptorProtos.append(serializedFileDescriptorProto) + } else { + throw GRPCStatus( + code: .notFound, + message: "The provided file or a dependency of the provided file could not be found." + ) + } + visited.insert(currentFileName) + } + return serializedFileDescriptorProtos + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsyncProvider { + private let protoRegistry: ReflectionServiceData + + internal init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto]) throws { + self.protoRegistry = try ReflectionServiceData( + fileDescriptors: fileDescriptorProtos + ) + } + + internal func findFileByFileName( + _ fileName: String, + request: Reflection_ServerReflectionRequest + ) throws -> Reflection_ServerReflectionResponse { + return Reflection_ServerReflectionResponse( + request: request, + fileDescriptorResponse: try .with { + $0.fileDescriptorProto = try self.protoRegistry + .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) + } + ) + } + + internal func getServicesNames( + request: Reflection_ServerReflectionRequest + ) throws -> Reflection_ServerReflectionResponse { + var listServicesResponse = Reflection_ListServiceResponse() + listServicesResponse.service = self.protoRegistry.serviceNames.map { serviceName in + Reflection_ServiceResponse.with { + $0.name = serviceName + } + } + return Reflection_ServerReflectionResponse( + request: request, + listServicesResponse: listServicesResponse + ) + } + + internal func serverReflectionInfo( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + for try await request in requestStream { + switch request.messageRequest { + case let .fileByFilename(fileName): + let response = try self.findFileByFileName( + fileName, + request: request + ) + try await responseStream.send(response) + + case .listServices: + let response = try self.getServicesNames(request: request) + try await responseStream.send(response) + + default: + throw GRPCStatus(code: .unimplemented) + } + } + } +} + +extension Reflection_ServerReflectionResponse { + init( + request: Reflection_ServerReflectionRequest, + fileDescriptorResponse: Reflection_FileDescriptorResponse + ) { + self = .with { + $0.validHost = request.host + $0.originalRequest = request + $0.fileDescriptorResponse = fileDescriptorResponse + } + } + + init( + request: Reflection_ServerReflectionRequest, + listServicesResponse: Reflection_ListServiceResponse + ) { + self = .with { + $0.validHost = request.host + $0.originalRequest = request + $0.listServicesResponse = listServicesResponse + } + } +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift new file mode 100644 index 000000000..85060b0e8 --- /dev/null +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift @@ -0,0 +1,422 @@ +/* + * Copyright 2023, 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 Foundation +import GRPC +import GRPCReflectionService +import NIOPosix +import SwiftProtobuf +import XCTest + +@testable import GRPCReflectionService + +final class GRPCReflectionServiceTests: GRPCTestCase { + private var group: MultiThreadedEventLoopGroup? + private var server: Server? + private var channel: GRPCChannel? + + private func generateProto(name: String, id: Int) -> Google_Protobuf_FileDescriptorProto { + let inputMessage = Google_Protobuf_DescriptorProto.with { + $0.name = "inputMessage" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "inputField" + $0.type = .bool + } + ] + } + + let outputMessage = Google_Protobuf_DescriptorProto.with { + $0.name = "outputMessage" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "outputField" + $0.type = .int32 + } + ] + } + + let method = Google_Protobuf_MethodDescriptorProto.with { + $0.name = "testMethod" + String(id) + $0.inputType = inputMessage.name + $0.outputType = outputMessage.name + } + + let serviceDescriptor = Google_Protobuf_ServiceDescriptorProto.with { + $0.method = [method] + $0.name = "service" + String(id) + } + + let fileDescriptorProto = Google_Protobuf_FileDescriptorProto.with { + $0.service = [serviceDescriptor] + $0.name = name + String(id) + ".proto" + $0.messageType = [inputMessage, outputMessage] + } + + return fileDescriptorProto + } + + /// Creates the dependencies of the proto used in the testing context. + private func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorProto] { + var fileDependencies: [Google_Protobuf_FileDescriptorProto] = [] + for id in 1 ... 4 { + let fileDescriptorProto = self.generateProto(name: "bar", id: id) + if id != 1 { + // Dependency of the first dependency. + fileDependencies[0].dependency.append(fileDescriptorProto.name) + } + fileDependencies.append(fileDescriptorProto) + } + return fileDependencies + } + + private func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescriptorProto] { + var protos: [Google_Protobuf_FileDescriptorProto] = [] + protos.append(self.generateProto(name: "foo", id: 0)) + for id in 1 ... 10 { + let fileDescriptorProtoA = self.generateProto(name: "fooA", id: id) + let fileDescriptorProtoB = self.generateProto(name: "fooB", id: id) + let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1 + protos[parent].dependency.append(fileDescriptorProtoA.name) + protos[parent].dependency.append(fileDescriptorProtoB.name) + protos.append(fileDescriptorProtoA) + protos.append(fileDescriptorProtoB) + } + return protos + } + + private func getServicesNamesFromProtos( + protos: [Google_Protobuf_FileDescriptorProto] + ) -> [String] { + return protos.serviceNames + } + + private func setUpServerAndChannel() throws { + let reflectionServiceProvider = try ReflectionService( + fileDescriptors: self.makeProtosWithDependencies() + ) + + let server = try Server.insecure(group: MultiThreadedEventLoopGroup.singleton) + .withServiceProviders([reflectionServiceProvider]) + .withLogger(self.serverLogger) + .bind(host: "127.0.0.1", port: 0) + .wait() + self.server = server + + let channel = try GRPCChannelPool.with( + target: .hostAndPort("127.0.0.1", server.channel.localAddress!.port!), + transportSecurity: .plaintext, + eventLoopGroup: MultiThreadedEventLoopGroup.singleton + ) { + $0.backgroundActivityLogger = self.clientLogger + } + + self.channel = channel + } + + override func tearDown() { + if let channel = self.channel { + XCTAssertNoThrow(try channel.close().wait()) + } + if let server = self.server { + XCTAssertNoThrow(try server.close().wait()) + } + + super.tearDown() + } + + func testFileByFileName() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileByFilename = "bar1.proto" + } + ) + serviceReflectionInfo.requestStream.finish() + + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + let receivedFileDescriptorProto = + try Google_Protobuf_FileDescriptorProto( + serializedData: (message.fileDescriptorResponse + .fileDescriptorProto[0]) + ) + + XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto") + XCTAssertEqual(receivedFileDescriptorProto.service.count, 1) + + guard let service = receivedFileDescriptorProto.service.first else { + return XCTFail("The received file descriptor proto doesn't have any services.") + } + guard let method = service.method.first else { + return XCTFail("The service of the received file descriptor proto doesn't have any methods.") + } + XCTAssertEqual(method.name, "testMethod1") + XCTAssertEqual(message.fileDescriptorResponse.fileDescriptorProto.count, 4) + } + + func testListServices() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.listServices = "services" + } + ) + + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted() + let servicesNames = self.getServicesNamesFromProtos( + protos: self.makeProtosWithDependencies() + ).sorted() + + XCTAssertEqual(receivedServices, servicesNames) + } + + func testReflectionServiceDataFileDescriptorDataByFilename() throws { + var protos = self.makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + + let registryFileDescriptorData = registry.fileDescriptorDataByFilename + + for (fileName, protoData) in registryFileDescriptorData { + let serializedFiledescriptorData = protoData.serializedFileDescriptorProto + let dependencyFileNames = protoData.dependencyFileNames + + guard let index = protos.firstIndex(where: { $0.name == fileName }) else { + return XCTFail( + """ + Could not find the file descriptor proto of \(fileName) \ + in the original file descriptor protos list. + """ + ) + } + + let originalProto = protos[index] + XCTAssertEqual(originalProto.name, fileName) + XCTAssertEqual(try originalProto.serializedData(), serializedFiledescriptorData) + XCTAssertEqual(originalProto.dependency, dependencyFileNames) + + protos.remove(at: index) + } + XCTAssert(protos.isEmpty) + } + + func testReflectionServiceServicesNames() throws { + let protos = self.makeProtosWithDependencies() + let servicesNames = self.getServicesNamesFromProtos(protos: protos).sorted() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let registryServices = registry.serviceNames.sorted() + XCTAssertEqual(registryServices, servicesNames) + } + + func testSerialisedFileDescriptorProtosForDependenciesOfFile() throws { + var protos = self.makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let serializedFileDescriptorProtos = + try registry + .serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") + let fileDescriptorProtos = try serializedFileDescriptorProtos.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + // Tests that the functions returns all the transitive dependencies, with their services and + // methods, together with the initial proto, as serialized data. + XCTAssertEqual(fileDescriptorProtos.count, 4) + for fileDescriptorProto in fileDescriptorProtos { + guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { + return XCTFail( + """ + Could not find the file descriptor proto of \(fileDescriptorProto.name) \ + in the original file descriptor protos list. + """ + ) + } + + for service in fileDescriptorProto.service { + guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + return XCTFail( + """ + Could not find the \(service.name) in the service \ + list of the \(fileDescriptorProto.name) file descriptor proto. + """ + ) + } + + let originalMethods = protos[protoIndex].service[serviceIndex].method + for method in service.method { + XCTAssert(originalMethods.contains(method)) + } + + for messageType in fileDescriptorProto.messageType { + XCTAssert(protos[protoIndex].messageType.contains(messageType)) + } + } + + protos.removeAll { $0 == fileDescriptorProto } + } + XCTAssert(protos.isEmpty) + } + + func testSerialisedFileDescriptorProtosForDependenciesOfFileComplexDependencyGraph() throws { + var protos = self.makeProtosWithComplexDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let serializedFileDescriptorProtos = + try registry + .serialisedFileDescriptorProtosForDependenciesOfFile(named: "foo0.proto") + let fileDescriptorProtos = try serializedFileDescriptorProtos.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + // Tests that the functions returns all the tranzitive dependencies, with their services and + // methods, together with the initial proto, as serialized data. + XCTAssertEqual(fileDescriptorProtos.count, 21) + for fileDescriptorProto in fileDescriptorProtos { + guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { + return XCTFail( + """ + Could not find the file descriptor proto of \(fileDescriptorProto.name) \ + in the original file descriptor protos list. + """ + ) + } + + for service in fileDescriptorProto.service { + guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + return XCTFail( + """ + Could not find the \(service.name) in the service \ + list of the \(fileDescriptorProto.name) file descriptor proto. + """ + ) + } + + let originalMethods = protos[protoIndex].service[serviceIndex].method + for method in service.method { + XCTAssert(originalMethods.contains(method)) + } + + for messageType in fileDescriptorProto.messageType { + XCTAssert(protos[protoIndex].messageType.contains(messageType)) + } + } + + protos.removeAll { $0 == fileDescriptorProto } + } + XCTAssert(protos.isEmpty) + } + + func testSerialisedFileDescriptorProtosForDependenciesOfFileDependencyLoops() throws { + var protos = self.makeProtosWithDependencies() + // Making dependencies of the "bar1.proto" to depend on "bar1.proto". + protos[1].dependency.append("bar1.proto") + protos[2].dependency.append("bar1.proto") + protos[3].dependency.append("bar1.proto") + let registry = try ReflectionServiceData(fileDescriptors: protos) + let serializedFileDescriptorProtos = + try registry + .serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") + let fileDescriptorProtos = try serializedFileDescriptorProtos.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + // Test that we get only 4 serialized File Descriptor Protos as response. + XCTAssertEqual(fileDescriptorProtos.count, 4) + for fileDescriptorProto in fileDescriptorProtos { + guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { + return XCTFail( + """ + Could not find the file descriptor proto of \(fileDescriptorProto.name) \ + in the original file descriptor protos list. + """ + ) + } + + for service in fileDescriptorProto.service { + guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + return XCTFail( + """ + Could not find the \(service.name) in the service \ + list of the \(fileDescriptorProto.name) file descriptor proto. + """ + ) + } + + let originalMethods = protos[protoIndex].service[serviceIndex].method + for method in service.method { + XCTAssert(originalMethods.contains(method)) + } + + for messageType in fileDescriptorProto.messageType { + XCTAssert(protos[protoIndex].messageType.contains(messageType)) + } + } + + protos.removeAll { $0 == fileDescriptorProto } + } + XCTAssert(protos.isEmpty) + } + + func testSerialisedFileDescriptorProtosForDependenciesOfFileInvalidFile() throws { + let protos = self.makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + XCTAssertThrowsError( + try registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "invalid.proto") + ) { error in + XCTAssertEqual( + error as? GRPCStatus, + GRPCStatus( + code: .notFound, + message: "The provided file or a dependency of the provided file could not be found." + ) + ) + } + } + + func testSerialisedFileDescriptorProtosForDependenciesOfFileDependencyNotProto() throws { + var protos = self.makeProtosWithDependencies() + protos[0].dependency.append("invalidDependency") + let registry = try ReflectionServiceData(fileDescriptors: protos) + XCTAssertThrowsError( + try registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") + ) { error in + XCTAssertEqual( + error as? GRPCStatus, + GRPCStatus( + code: .notFound, + message: "The provided file or a dependency of the provided file could not be found." + ) + ) + } + } +} + +extension Sequence where Element == Google_Protobuf_FileDescriptorProto { + var serviceNames: [String] { + self.flatMap { $0.service.map { $0.name } } + } +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift new file mode 100644 index 000000000..ade12d796 --- /dev/null +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift @@ -0,0 +1,208 @@ +// +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the protocol buffer compiler. +// Source: reflection.proto +// +import GRPC +import NIO +import NIOConcurrencyHelpers +import SwiftProtobuf + + +/// Usage: instantiate `Reflection_ServerReflectionClient`, then call methods of this protocol to make API calls. +internal protocol Reflection_ServerReflectionClientProtocol: GRPCClient { + var serviceName: String { get } + var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { get } + + func serverReflectionInfo( + callOptions: CallOptions?, + handler: @escaping (Reflection_ServerReflectionResponse) -> Void + ) -> BidirectionalStreamingCall +} + +extension Reflection_ServerReflectionClientProtocol { + internal var serviceName: String { + return "reflection.ServerReflection" + } + + /// The reflection service is structured as a bidirectional stream, ensuring + /// all related requests go to a single server. + /// + /// Callers should use the `send` method on the returned object to send messages + /// to the server. The caller should send an `.end` after the final message has been sent. + /// + /// - Parameters: + /// - callOptions: Call options. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. + internal func serverReflectionInfo( + callOptions: CallOptions? = nil, + handler: @escaping (Reflection_ServerReflectionResponse) -> Void + ) -> BidirectionalStreamingCall { + return self.makeBidirectionalStreamingCall( + path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], + handler: handler + ) + } +} + +@available(*, deprecated) +extension Reflection_ServerReflectionClient: @unchecked Sendable {} + +@available(*, deprecated, renamed: "Reflection_ServerReflectionNIOClient") +internal final class Reflection_ServerReflectionClient: Reflection_ServerReflectionClientProtocol { + private let lock = Lock() + private var _defaultCallOptions: CallOptions + private var _interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? + internal let channel: GRPCChannel + internal var defaultCallOptions: CallOptions { + get { self.lock.withLock { return self._defaultCallOptions } } + set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } + } + internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { + get { self.lock.withLock { return self._interceptors } } + set { self.lock.withLockVoid { self._interceptors = newValue } } + } + + /// Creates a client for the reflection.ServerReflection service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self._defaultCallOptions = defaultCallOptions + self._interceptors = interceptors + } +} + +internal struct Reflection_ServerReflectionNIOClient: Reflection_ServerReflectionClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? + + /// Creates a client for the reflection.ServerReflection service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Reflection_ServerReflectionAsyncClientProtocol: GRPCClient { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { get } + + func makeServerReflectionInfoCall( + callOptions: CallOptions? + ) -> GRPCAsyncBidirectionalStreamingCall +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Reflection_ServerReflectionAsyncClientProtocol { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Reflection_ServerReflectionClientMetadata.serviceDescriptor + } + + internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { + return nil + } + + internal func makeServerReflectionInfoCall( + callOptions: CallOptions? = nil + ) -> GRPCAsyncBidirectionalStreamingCall { + return self.makeAsyncBidirectionalStreamingCall( + path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Reflection_ServerReflectionAsyncClientProtocol { + internal func serverReflectionInfo( + _ requests: RequestStream, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Reflection_ServerReflectionRequest { + return self.performAsyncBidirectionalStreamingCall( + path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + requests: requests, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] + ) + } + + internal func serverReflectionInfo( + _ requests: RequestStream, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Reflection_ServerReflectionRequest { + return self.performAsyncBidirectionalStreamingCall( + path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + requests: requests, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal struct Reflection_ServerReflectionAsyncClient: Reflection_ServerReflectionAsyncClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? + + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +internal protocol Reflection_ServerReflectionClientInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when invoking 'serverReflectionInfo'. + func makeServerReflectionInfoInterceptors() -> [ClientInterceptor] +} + +internal enum Reflection_ServerReflectionClientMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "ServerReflection", + fullName: "reflection.ServerReflection", + methods: [ + Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo, + ] + ) + + internal enum Methods { + internal static let serverReflectionInfo = GRPCMethodDescriptor( + name: "ServerReflectionInfo", + path: "/reflection.ServerReflection/ServerReflectionInfo", + type: GRPCCallType.bidirectionalStreaming + ) + } +} + diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift new file mode 100644 index 000000000..d3cabe420 --- /dev/null +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift @@ -0,0 +1,843 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: reflection.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2016 The gRPC Authors +// +// 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. + +// Service exported by server reflection. A more complete description of how +// server reflection works can be found at +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +// +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The message sent by the client when calling ServerReflectionInfo method. +struct Reflection_ServerReflectionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var host: String = String() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + var messageRequest: Reflection_ServerReflectionRequest.OneOf_MessageRequest? = nil + + /// Find a proto file by the file name. + var fileByFilename: String { + get { + if case .fileByFilename(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileByFilename(newValue)} + } + + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + var fileContainingSymbol: String { + get { + if case .fileContainingSymbol(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileContainingSymbol(newValue)} + } + + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + var fileContainingExtension: Reflection_ExtensionRequest { + get { + if case .fileContainingExtension(let v)? = messageRequest {return v} + return Reflection_ExtensionRequest() + } + set {messageRequest = .fileContainingExtension(newValue)} + } + + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + var allExtensionNumbersOfType: String { + get { + if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .allExtensionNumbersOfType(newValue)} + } + + /// List the full names of registered services. The content will not be + /// checked. + var listServices: String { + get { + if case .listServices(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .listServices(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + enum OneOf_MessageRequest: Equatable { + /// Find a proto file by the file name. + case fileByFilename(String) + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + case fileContainingSymbol(String) + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + case fileContainingExtension(Reflection_ExtensionRequest) + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + case allExtensionNumbersOfType(String) + /// List the full names of registered services. The content will not be + /// checked. + case listServices(String) + + #if !swift(>=4.1) + static func ==(lhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest, rhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileByFilename, .fileByFilename): return { + guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingSymbol, .fileContainingSymbol): return { + guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingExtension, .fileContainingExtension): return { + guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { + guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServices, .listServices): return { + guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +/// The type name and extension number sent by the client when requesting +/// file_containing_extension. +struct Reflection_ExtensionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Fully-qualified type name. The format should be . + var containingType: String = String() + + var extensionNumber: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The message sent by the server to answer ServerReflectionInfo method. +struct Reflection_ServerReflectionResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var validHost: String = String() + + var originalRequest: Reflection_ServerReflectionRequest { + get {return _originalRequest ?? Reflection_ServerReflectionRequest()} + set {_originalRequest = newValue} + } + /// Returns true if `originalRequest` has been explicitly set. + var hasOriginalRequest: Bool {return self._originalRequest != nil} + /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. + mutating func clearOriginalRequest() {self._originalRequest = nil} + + /// The server sets one of the following fields according to the message_request + /// in the request. + var messageResponse: Reflection_ServerReflectionResponse.OneOf_MessageResponse? = nil + + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. + /// As the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + var fileDescriptorResponse: Reflection_FileDescriptorResponse { + get { + if case .fileDescriptorResponse(let v)? = messageResponse {return v} + return Reflection_FileDescriptorResponse() + } + set {messageResponse = .fileDescriptorResponse(newValue)} + } + + /// This message is used to answer all_extension_numbers_of_type requests. + var allExtensionNumbersResponse: Reflection_ExtensionNumberResponse { + get { + if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} + return Reflection_ExtensionNumberResponse() + } + set {messageResponse = .allExtensionNumbersResponse(newValue)} + } + + /// This message is used to answer list_services requests. + var listServicesResponse: Reflection_ListServiceResponse { + get { + if case .listServicesResponse(let v)? = messageResponse {return v} + return Reflection_ListServiceResponse() + } + set {messageResponse = .listServicesResponse(newValue)} + } + + /// This message is used when an error occurs. + var errorResponse: Reflection_ErrorResponse { + get { + if case .errorResponse(let v)? = messageResponse {return v} + return Reflection_ErrorResponse() + } + set {messageResponse = .errorResponse(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The server sets one of the following fields according to the message_request + /// in the request. + enum OneOf_MessageResponse: Equatable { + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. + /// As the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + case fileDescriptorResponse(Reflection_FileDescriptorResponse) + /// This message is used to answer all_extension_numbers_of_type requests. + case allExtensionNumbersResponse(Reflection_ExtensionNumberResponse) + /// This message is used to answer list_services requests. + case listServicesResponse(Reflection_ListServiceResponse) + /// This message is used when an error occurs. + case errorResponse(Reflection_ErrorResponse) + + #if !swift(>=4.1) + static func ==(lhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse, rhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileDescriptorResponse, .fileDescriptorResponse): return { + guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { + guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServicesResponse, .listServicesResponse): return { + guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.errorResponse, .errorResponse): return { + guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} + + fileprivate var _originalRequest: Reflection_ServerReflectionRequest? = nil +} + +/// Serialized FileDescriptorProto messages sent by the server answering +/// a file_by_filename, file_containing_symbol, or file_containing_extension +/// request. +struct Reflection_FileDescriptorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Serialized FileDescriptorProto messages. We avoid taking a dependency on + /// descriptor.proto, which uses proto2 only features, by making them opaque + /// bytes instead. + var fileDescriptorProto: [Data] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A list of extension numbers sent by the server answering +/// all_extension_numbers_of_type request. +struct Reflection_ExtensionNumberResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of the base type, including the package name. The format + /// is . + var baseTypeName: String = String() + + var extensionNumber: [Int32] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A list of ServiceResponse sent by the server answering list_services request. +struct Reflection_ListServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The information of each service may be expanded in the future, so we use + /// ServiceResponse message to encapsulate it. + var service: [Reflection_ServiceResponse] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The information of a single service used by ListServiceResponse to answer +/// list_services request. +struct Reflection_ServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of a registered service, including its package name. The format + /// is . + var name: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The error code and error message sent by the server when an error occurs. +struct Reflection_ErrorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// This field uses the error codes defined in grpc::StatusCode. + var errorCode: Int32 = 0 + + var errorMessage: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Reflection_ServerReflectionRequest: @unchecked Sendable {} +extension Reflection_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} +extension Reflection_ExtensionRequest: @unchecked Sendable {} +extension Reflection_ServerReflectionResponse: @unchecked Sendable {} +extension Reflection_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} +extension Reflection_FileDescriptorResponse: @unchecked Sendable {} +extension Reflection_ExtensionNumberResponse: @unchecked Sendable {} +extension Reflection_ListServiceResponse: @unchecked Sendable {} +extension Reflection_ServiceResponse: @unchecked Sendable {} +extension Reflection_ErrorResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "reflection" + +extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "host"), + 3: .standard(proto: "file_by_filename"), + 4: .standard(proto: "file_containing_symbol"), + 5: .standard(proto: "file_containing_extension"), + 6: .standard(proto: "all_extension_numbers_of_type"), + 7: .standard(proto: "list_services"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() + case 3: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileByFilename(v) + } + }() + case 4: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingSymbol(v) + } + }() + case 5: try { + var v: Reflection_ExtensionRequest? + var hadOneofValue = false + if let current = self.messageRequest { + hadOneofValue = true + if case .fileContainingExtension(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingExtension(v) + } + }() + case 6: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .allExtensionNumbersOfType(v) + } + }() + case 7: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .listServices(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.host.isEmpty { + try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) + } + switch self.messageRequest { + case .fileByFilename?: try { + guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + }() + case .fileContainingSymbol?: try { + guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + }() + case .fileContainingExtension?: try { + guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .allExtensionNumbersOfType?: try { + guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + }() + case .listServices?: try { + guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_ServerReflectionRequest, rhs: Reflection_ServerReflectionRequest) -> Bool { + if lhs.host != rhs.host {return false} + if lhs.messageRequest != rhs.messageRequest {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "containing_type"), + 2: .standard(proto: "extension_number"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.containingType.isEmpty { + try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) + } + if self.extensionNumber != 0 { + try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_ExtensionRequest, rhs: Reflection_ExtensionRequest) -> Bool { + if lhs.containingType != rhs.containingType {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "valid_host"), + 2: .standard(proto: "original_request"), + 4: .standard(proto: "file_descriptor_response"), + 5: .standard(proto: "all_extension_numbers_response"), + 6: .standard(proto: "list_services_response"), + 7: .standard(proto: "error_response"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() + case 4: try { + var v: Reflection_FileDescriptorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .fileDescriptorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .fileDescriptorResponse(v) + } + }() + case 5: try { + var v: Reflection_ExtensionNumberResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .allExtensionNumbersResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .allExtensionNumbersResponse(v) + } + }() + case 6: try { + var v: Reflection_ListServiceResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .listServicesResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .listServicesResponse(v) + } + }() + case 7: try { + var v: Reflection_ErrorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .errorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .errorResponse(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.validHost.isEmpty { + try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) + } + try { if let v = self._originalRequest { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + switch self.messageResponse { + case .fileDescriptorResponse?: try { + guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .allExtensionNumbersResponse?: try { + guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .listServicesResponse?: try { + guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .errorResponse?: try { + guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_ServerReflectionResponse, rhs: Reflection_ServerReflectionResponse) -> Bool { + if lhs.validHost != rhs.validHost {return false} + if lhs._originalRequest != rhs._originalRequest {return false} + if lhs.messageResponse != rhs.messageResponse {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "file_descriptor_proto"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.fileDescriptorProto.isEmpty { + try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_FileDescriptorResponse, rhs: Reflection_FileDescriptorResponse) -> Bool { + if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "base_type_name"), + 2: .standard(proto: "extension_number"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.baseTypeName.isEmpty { + try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) + } + if !self.extensionNumber.isEmpty { + try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_ExtensionNumberResponse, rhs: Reflection_ExtensionNumberResponse) -> Bool { + if lhs.baseTypeName != rhs.baseTypeName {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "service"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.service.isEmpty { + try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_ListServiceResponse, rhs: Reflection_ListServiceResponse) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServiceResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_ServiceResponse, rhs: Reflection_ServiceResponse) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Reflection_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ErrorResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "error_code"), + 2: .standard(proto: "error_message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.errorCode != 0 { + try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) + } + if !self.errorMessage.isEmpty { + try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Reflection_ErrorResponse, rhs: Reflection_ErrorResponse) -> Bool { + if lhs.errorCode != rhs.errorCode {return false} + if lhs.errorMessage != rhs.errorMessage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} From 5e8afe90f68b4fc8d5e071b5f80806b5c37ea942 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Mon, 16 Oct 2023 18:14:00 +0100 Subject: [PATCH 131/580] Add `package` visibility modifier (#1674) --- Plugins/GRPCSwiftPlugin/plugin.swift | 2 ++ Sources/protoc-gen-grpc-swift/options.swift | 3 +++ docs/plugin.md | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index a2fda772f..4f4d6e6e6 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -53,6 +53,8 @@ struct GRPCSwiftPlugin { case `internal` /// The generated files should have `public` access level. case `public` + /// The generated files should have `package` access level. + case `package` } /// An array of paths to `.proto` files for this invocation. diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/options.swift index bdef17338..155dd7377 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/options.swift @@ -40,6 +40,7 @@ final class GeneratorOptions { enum Visibility: String { case `internal` = "Internal" case `public` = "Public" + case `package` = "Package" var sourceSnippet: String { switch self { @@ -47,6 +48,8 @@ final class GeneratorOptions { return "internal" case .public: return "public" + case .package: + return "package" } } } diff --git a/docs/plugin.md b/docs/plugin.md index ea1c7350b..d09800c89 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -27,7 +27,7 @@ when invoking `protoc`. The **Visibility** option determines the access control for generated code. -- **Possible values:** Public, Internal +- **Possible values:** Public, Internal, Package - **Default value:** Internal ### Server From a313fcfead98cfc53c806468b510b9fe6d2cf5f4 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 17 Oct 2023 14:30:26 +0100 Subject: [PATCH 132/580] Fix soundness script (#1676) Motivation: The formatting script expects the '-l' flag to run the lint checker without running the formatter. The soundness script pass 'lint' which was ignored, so the script ran the formatter instead of the lint checker which means the formatting checker will never fail in CI. Modifications: - Fix the incorrect flag - Fix a formatting issue which crept in Result: CI checks formatting correctly... --- Tests/GRPCTests/InterceptedRPCCancellationTests.swift | 4 ++-- scripts/sanity.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift index a041fc1f4..3d7917ac9 100644 --- a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift +++ b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift @@ -170,10 +170,10 @@ final class MagicAddingClientInterceptor< interceptors: [] ) - self.retry!.invoke(onError: { + self.retry!.invoke { context.log.debug("intercepting error from retried rpc") context.errorCaught($0) - }) { responsePart in + } onResponsePart: { responsePart in context.log.debug("intercepting response part from retried rpc") context.receive(responsePart) } diff --git a/scripts/sanity.sh b/scripts/sanity.sh index a317b3987..a290eb9f7 100755 --- a/scripts/sanity.sh +++ b/scripts/sanity.sh @@ -42,7 +42,7 @@ function check_license_headers() { } function check_formatting() { - run_logged "Checking formatting" "$HERE/format.sh lint" + run_logged "Checking formatting" "$HERE/format.sh -l" } errors=0 From 4df985f80960fce888c02a83f7b84d69eac97a3f Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 17 Oct 2023 16:04:52 +0100 Subject: [PATCH 133/580] Add support for file-containing-symbol reflection request. (#1675) Motivation: The file-containing-symbol request is part of the Reflection Service and enables users to find the proto file containing a symbol they specify and its transitive dependencies. Modifications: Added a dictionary of the fully qualified names of symbols and their corresponding file names, in the ReflectionServiceData struct. Added the function that creates the server response, after getting the file name corresponding to the symbol name and getting its tranaitive dependencies. Also, split the tests into Integration and Unit tests, and added tests for the new request. Result: Users of the Reflection Service will be able to get from the server the proto file that contains the symbols they are specifying in the request and its transitive dependencies. --- .../Server/ReflectionService.swift | 74 +++++ .../ReflectionServiceIntegrationTests.swift | 177 +++++++++++ ...swift => ReflectionServiceUnitTests.swift} | 294 ++++++------------ .../GRPCReflectionServiceTests/Utils.swift | 118 +++++++ 4 files changed, 471 insertions(+), 192 deletions(-) create mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift rename Tests/GRPCTests/GRPCReflectionServiceTests/{GRPCReflectionServiceTests.swift => ReflectionServiceUnitTests.swift} (56%) create mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index d28c1301e..81cf8282c 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -46,10 +46,13 @@ internal struct ReflectionServiceData: Sendable { internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData] internal var serviceNames: [String] + internal var fileNameBySymbol: [String: String] internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { self.serviceNames = [] self.fileDescriptorDataByFilename = [:] + self.fileNameBySymbol = [:] + for fileDescriptorProto in fileDescriptors { let serializedFileDescriptorProto: Data do { @@ -67,6 +70,19 @@ internal struct ReflectionServiceData: Sendable { ) self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData self.serviceNames.append(contentsOf: fileDescriptorProto.service.map { $0.name }) + for qualifiedSybolName in fileDescriptorProto.qualifiedSymbolNames { + let oldValue = self.fileNameBySymbol.updateValue( + fileDescriptorProto.name, + forKey: qualifiedSybolName + ) + if let oldValue = oldValue { + throw GRPCStatus( + code: .alreadyExists, + message: + "The \(qualifiedSybolName) symbol from \(fileDescriptorProto.name) already exists in \(oldValue)." + ) + } + } } } @@ -99,6 +115,10 @@ internal struct ReflectionServiceData: Sendable { } return serializedFileDescriptorProtos } + + internal func nameOfFileContainingSymbol(named symbolName: String) -> String? { + return self.fileNameBySymbol[symbolName] + } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -139,6 +159,19 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync ) } + internal func findFileBySymbol( + _ symbolName: String, + request: Reflection_ServerReflectionRequest + ) throws -> Reflection_ServerReflectionResponse { + guard let fileName = self.protoRegistry.nameOfFileContainingSymbol(named: symbolName) else { + throw GRPCStatus( + code: .notFound, + message: "The provided symbol could not be found." + ) + } + return try self.findFileByFileName(fileName, request: request) + } + internal func serverReflectionInfo( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, @@ -157,6 +190,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync let response = try self.getServicesNames(request: request) try await responseStream.send(response) + case let .fileContainingSymbol(symbolName): + let response = try self.findFileBySymbol( + symbolName, + request: request + ) + try await responseStream.send(response) + default: throw GRPCStatus(code: .unimplemented) } @@ -187,3 +227,37 @@ extension Reflection_ServerReflectionResponse { } } } + +extension Google_Protobuf_FileDescriptorProto { + var qualifiedServiceAndMethodNames: [String] { + var names: [String] = [] + + for service in self.service { + names.append(self.package + "." + service.name) + names.append( + contentsOf: service.method + .map { self.package + "." + service.name + "." + $0.name } + ) + } + return names + } + + var qualifiedMessageTypes: [String] { + return self.messageType.map { + self.package + "." + $0.name + } + } + + var qualifiedEnumTypes: [String] { + return self.enumType.map { + self.package + "." + $0.name + } + } + + var qualifiedSymbolNames: [String] { + var names = self.qualifiedServiceAndMethodNames + names.append(contentsOf: self.qualifiedMessageTypes) + names.append(contentsOf: self.qualifiedEnumTypes) + return names + } +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift new file mode 100644 index 000000000..c6bd1a339 --- /dev/null +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -0,0 +1,177 @@ +/* + * Copyright 2023, 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 Foundation +import GRPC +import GRPCReflectionService +import NIOPosix +import SwiftProtobuf +import XCTest + +@testable import GRPCReflectionService + +final class ReflectionServiceIntegrationTests: GRPCTestCase { + private var server: Server? + private var channel: GRPCChannel? + private let protos: [Google_Protobuf_FileDescriptorProto] = makeProtosWithDependencies() + private let independentProto: Google_Protobuf_FileDescriptorProto = generateFileDescriptorProto( + fileName: "independentBar", + suffix: 5 + ) + + private func setUpServerAndChannel() throws { + let reflectionServiceProvider = try ReflectionService( + fileDescriptors: self.protos + [self.independentProto] + ) + + let server = try Server.insecure(group: MultiThreadedEventLoopGroup.singleton) + .withServiceProviders([reflectionServiceProvider]) + .withLogger(self.serverLogger) + .bind(host: "127.0.0.1", port: 0) + .wait() + self.server = server + + let channel = try GRPCChannelPool.with( + target: .hostAndPort("127.0.0.1", server.channel.localAddress!.port!), + transportSecurity: .plaintext, + eventLoopGroup: MultiThreadedEventLoopGroup.singleton + ) { + $0.backgroundActivityLogger = self.clientLogger + } + + self.channel = channel + } + + override func tearDown() { + if let channel = self.channel { + XCTAssertNoThrow(try channel.close().wait()) + } + if let server = self.server { + XCTAssertNoThrow(try server.close().wait()) + } + + super.tearDown() + } + + func testFileByFileName() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileByFilename = "bar1.proto" + } + ) + serviceReflectionInfo.requestStream.finish() + + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + let receivedFileDescriptorProto = + try Google_Protobuf_FileDescriptorProto( + serializedData: (message.fileDescriptorResponse + .fileDescriptorProto[0]) + ) + + XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto") + XCTAssertEqual(receivedFileDescriptorProto.service.count, 1) + + guard let service = receivedFileDescriptorProto.service.first else { + return XCTFail("The received file descriptor proto doesn't have any services.") + } + guard let method = service.method.first else { + return XCTFail("The service of the received file descriptor proto doesn't have any methods.") + } + XCTAssertEqual(method.name, "testMethod1") + XCTAssertEqual(message.fileDescriptorResponse.fileDescriptorProto.count, 4) + } + + func testListServices() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.listServices = "services" + } + ) + + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted() + let servicesNames = (self.protos + [self.independentProto]).serviceNames.sorted() + + XCTAssertEqual(receivedServices, servicesNames) + } + + func testFileBySymbol() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileContainingSymbol = "packagebar1.enumType1" + } + ) + + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + let receivedData: [Google_Protobuf_FileDescriptorProto] + do { + receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + } catch { + return XCTFail("Could not serialize data received as a message.") + } + + let fileToFind = self.protos[0] + let dependentProtos = self.protos[1...] + for fileDescriptorProto in receivedData { + if fileDescriptorProto == fileToFind { + XCTAssert( + fileDescriptorProto.enumType.names.contains("enumType1"), + """ + The response doesn't contain the serialized file descriptor proto \ + containing the \"packagebar1.enumType1\" symbol. + """ + ) + } else { + XCTAssert( + dependentProtos.contains(fileDescriptorProto), + """ + The \(fileDescriptorProto.name) is not a dependency of the \ + proto file containing the \"packagebar1.enumType1\" symbol. + """ + ) + } + } + } +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift similarity index 56% rename from Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift rename to Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index 85060b0e8..e3913c1d0 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/GRPCReflectionServiceTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -17,228 +17,144 @@ import Foundation import GRPC import GRPCReflectionService -import NIOPosix import SwiftProtobuf import XCTest @testable import GRPCReflectionService -final class GRPCReflectionServiceTests: GRPCTestCase { - private var group: MultiThreadedEventLoopGroup? - private var server: Server? - private var channel: GRPCChannel? - - private func generateProto(name: String, id: Int) -> Google_Protobuf_FileDescriptorProto { - let inputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "inputMessage" - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "inputField" - $0.type = .bool - } - ] - } - - let outputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "outputMessage" - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "outputField" - $0.type = .int32 - } - ] - } - - let method = Google_Protobuf_MethodDescriptorProto.with { - $0.name = "testMethod" + String(id) - $0.inputType = inputMessage.name - $0.outputType = outputMessage.name - } - - let serviceDescriptor = Google_Protobuf_ServiceDescriptorProto.with { - $0.method = [method] - $0.name = "service" + String(id) - } +final class ReflectionServiceUnitTests: GRPCTestCase { + /// Testing the fileDescriptorDataByFilename dictionary of the ReflectionServiceData object. + func testFileDescriptorDataByFilename() throws { + var protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileDescriptorProto = Google_Protobuf_FileDescriptorProto.with { - $0.service = [serviceDescriptor] - $0.name = name + String(id) + ".proto" - $0.messageType = [inputMessage, outputMessage] - } + let registryFileDescriptorData = registry.fileDescriptorDataByFilename - return fileDescriptorProto - } + for (fileName, protoData) in registryFileDescriptorData { + let serializedFiledescriptorData = protoData.serializedFileDescriptorProto + let dependencyFileNames = protoData.dependencyFileNames - /// Creates the dependencies of the proto used in the testing context. - private func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorProto] { - var fileDependencies: [Google_Protobuf_FileDescriptorProto] = [] - for id in 1 ... 4 { - let fileDescriptorProto = self.generateProto(name: "bar", id: id) - if id != 1 { - // Dependency of the first dependency. - fileDependencies[0].dependency.append(fileDescriptorProto.name) + guard let index = protos.firstIndex(where: { $0.name == fileName }) else { + return XCTFail( + """ + Could not find the file descriptor proto of \(fileName) \ + in the original file descriptor protos list. + """ + ) } - fileDependencies.append(fileDescriptorProto) - } - return fileDependencies - } - private func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescriptorProto] { - var protos: [Google_Protobuf_FileDescriptorProto] = [] - protos.append(self.generateProto(name: "foo", id: 0)) - for id in 1 ... 10 { - let fileDescriptorProtoA = self.generateProto(name: "fooA", id: id) - let fileDescriptorProtoB = self.generateProto(name: "fooB", id: id) - let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1 - protos[parent].dependency.append(fileDescriptorProtoA.name) - protos[parent].dependency.append(fileDescriptorProtoB.name) - protos.append(fileDescriptorProtoA) - protos.append(fileDescriptorProtoB) + let originalProto = protos[index] + XCTAssertEqual(originalProto.name, fileName) + XCTAssertEqual(try originalProto.serializedData(), serializedFiledescriptorData) + XCTAssertEqual(originalProto.dependency, dependencyFileNames) + + protos.remove(at: index) } - return protos + XCTAssert(protos.isEmpty) } - private func getServicesNamesFromProtos( - protos: [Google_Protobuf_FileDescriptorProto] - ) -> [String] { - return protos.serviceNames + /// Testing the serviceNames array of the ReflectionServiceData object. + func testServiceNames() throws { + let protos = makeProtosWithDependencies() + let servicesNames = protos.serviceNames.sorted() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let registryServices = registry.serviceNames.sorted() + XCTAssertEqual(registryServices, servicesNames) } - private func setUpServerAndChannel() throws { - let reflectionServiceProvider = try ReflectionService( - fileDescriptors: self.makeProtosWithDependencies() - ) - - let server = try Server.insecure(group: MultiThreadedEventLoopGroup.singleton) - .withServiceProviders([reflectionServiceProvider]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - self.server = server - - let channel = try GRPCChannelPool.with( - target: .hostAndPort("127.0.0.1", server.channel.localAddress!.port!), - transportSecurity: .plaintext, - eventLoopGroup: MultiThreadedEventLoopGroup.singleton - ) { - $0.backgroundActivityLogger = self.clientLogger - } + /// Testing the fileNameBySymbol array of the ReflectionServiceData object. + func testFileNameBySymbol() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let registryFileNameBySymbol = registry.fileNameBySymbol - self.channel = channel - } + var symbolsCount = 0 - override func tearDown() { - if let channel = self.channel { - XCTAssertNoThrow(try channel.close().wait()) - } - if let server = self.server { - XCTAssertNoThrow(try server.close().wait()) + for proto in protos { + let qualifiedSymbolNames = proto.qualifiedSymbolNames + symbolsCount += qualifiedSymbolNames.count + for qualifiedSymbolName in qualifiedSymbolNames { + XCTAssertEqual(registryFileNameBySymbol[qualifiedSymbolName], proto.name) + } } - super.tearDown() + XCTAssertEqual(symbolsCount, registryFileNameBySymbol.count) } - func testFileByFileName() async throws { - try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send( - .with { - $0.host = "127.0.0.1" - $0.fileByFilename = "bar1.proto" + func testFileNameBySymbolDuplicatedSymbol() throws { + var protos = makeProtosWithDependencies() + protos[1].messageType.append( + Google_Protobuf_DescriptorProto.with { + $0.name = "inputMessage" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "inputField" + $0.type = .bool + } + ] } ) - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") - } - - let receivedFileDescriptorProto = - try Google_Protobuf_FileDescriptorProto( - serializedData: (message.fileDescriptorResponse - .fileDescriptorProto[0]) + XCTAssertThrowsError( + try ReflectionServiceData(fileDescriptors: protos) + ) { error in + XCTAssertEqual( + error as? GRPCStatus, + GRPCStatus( + code: .alreadyExists, + message: + """ + The packagebar2.inputMessage symbol from bar2.proto \ + already exists in bar2.proto. + """ + ) ) - - XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto") - XCTAssertEqual(receivedFileDescriptorProto.service.count, 1) - - guard let service = receivedFileDescriptorProto.service.first else { - return XCTFail("The received file descriptor proto doesn't have any services.") } - guard let method = service.method.first else { - return XCTFail("The service of the received file descriptor proto doesn't have any methods.") - } - XCTAssertEqual(method.name, "testMethod1") - XCTAssertEqual(message.fileDescriptorResponse.fileDescriptorProto.count, 4) } - func testListServices() async throws { - try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - - try await serviceReflectionInfo.requestStream.send( - .with { - $0.host = "127.0.0.1" - $0.listServices = "services" - } - ) - - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") - } - - let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted() - let servicesNames = self.getServicesNamesFromProtos( - protos: self.makeProtosWithDependencies() - ).sorted() + // Testing the nameOfFileContainingSymbol method for different types of symbols. - XCTAssertEqual(receivedServices, servicesNames) + func testNameOfFileContainingSymbolEnum() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let fileName = registry.nameOfFileContainingSymbol(named: "packagebar2.enumType2") + XCTAssertEqual(fileName, "bar2.proto") } - func testReflectionServiceDataFileDescriptorDataByFilename() throws { - var protos = self.makeProtosWithDependencies() + func testNameOfFileContainingSymbolMessage() throws { + let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) + let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage") + XCTAssertEqual(fileName, "bar1.proto") + } - let registryFileDescriptorData = registry.fileDescriptorDataByFilename - - for (fileName, protoData) in registryFileDescriptorData { - let serializedFiledescriptorData = protoData.serializedFileDescriptorProto - let dependencyFileNames = protoData.dependencyFileNames - - guard let index = protos.firstIndex(where: { $0.name == fileName }) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileName) \ - in the original file descriptor protos list. - """ - ) - } - - let originalProto = protos[index] - XCTAssertEqual(originalProto.name, fileName) - XCTAssertEqual(try originalProto.serializedData(), serializedFiledescriptorData) - XCTAssertEqual(originalProto.dependency, dependencyFileNames) + func testNameOfFileContainingSymbolService() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let fileName = registry.nameOfFileContainingSymbol(named: "packagebar3.service3") + XCTAssertEqual(fileName, "bar3.proto") + } - protos.remove(at: index) - } - XCTAssert(protos.isEmpty) + func testNameOfFileContainingSymbolMethod() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let fileName = registry.nameOfFileContainingSymbol( + named: "packagebar4.service4.testMethod4" + ) + XCTAssertEqual(fileName, "bar4.proto") } - func testReflectionServiceServicesNames() throws { - let protos = self.makeProtosWithDependencies() - let servicesNames = self.getServicesNamesFromProtos(protos: protos).sorted() + func testNameOfFileContainingSymbolNonExistentSymbol() throws { + let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let registryServices = registry.serviceNames.sorted() - XCTAssertEqual(registryServices, servicesNames) + let fileName = registry.nameOfFileContainingSymbol(named: "packagebar2.enumType3") + XCTAssertEqual(fileName, nil) } + // Testing the serializedFileDescriptorProto method in different cases. + func testSerialisedFileDescriptorProtosForDependenciesOfFile() throws { - var protos = self.makeProtosWithDependencies() + var protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) let serializedFileDescriptorProtos = try registry @@ -285,7 +201,7 @@ final class GRPCReflectionServiceTests: GRPCTestCase { } func testSerialisedFileDescriptorProtosForDependenciesOfFileComplexDependencyGraph() throws { - var protos = self.makeProtosWithComplexDependencies() + var protos = makeProtosWithComplexDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) let serializedFileDescriptorProtos = try registry @@ -332,7 +248,7 @@ final class GRPCReflectionServiceTests: GRPCTestCase { } func testSerialisedFileDescriptorProtosForDependenciesOfFileDependencyLoops() throws { - var protos = self.makeProtosWithDependencies() + var protos = makeProtosWithDependencies() // Making dependencies of the "bar1.proto" to depend on "bar1.proto". protos[1].dependency.append("bar1.proto") protos[2].dependency.append("bar1.proto") @@ -382,7 +298,7 @@ final class GRPCReflectionServiceTests: GRPCTestCase { } func testSerialisedFileDescriptorProtosForDependenciesOfFileInvalidFile() throws { - let protos = self.makeProtosWithDependencies() + let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) XCTAssertThrowsError( try registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "invalid.proto") @@ -398,7 +314,7 @@ final class GRPCReflectionServiceTests: GRPCTestCase { } func testSerialisedFileDescriptorProtosForDependenciesOfFileDependencyNotProto() throws { - var protos = self.makeProtosWithDependencies() + var protos = makeProtosWithDependencies() protos[0].dependency.append("invalidDependency") let registry = try ReflectionServiceData(fileDescriptors: protos) XCTAssertThrowsError( @@ -414,9 +330,3 @@ final class GRPCReflectionServiceTests: GRPCTestCase { } } } - -extension Sequence where Element == Google_Protobuf_FileDescriptorProto { - var serviceNames: [String] { - self.flatMap { $0.service.map { $0.name } } - } -} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift new file mode 100644 index 000000000..95f68f131 --- /dev/null +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift @@ -0,0 +1,118 @@ +/* + * Copyright 2023, 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 Foundation +import GRPC +import SwiftProtobuf + +internal func generateFileDescriptorProto( + fileName name: String, + suffix id: Int +) -> Google_Protobuf_FileDescriptorProto { + let inputMessage = Google_Protobuf_DescriptorProto.with { + $0.name = "inputMessage" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "inputField" + $0.type = .bool + } + ] + } + + let outputMessage = Google_Protobuf_DescriptorProto.with { + $0.name = "outputMessage" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "outputField" + $0.type = .int32 + } + ] + } + + let enumType = Google_Protobuf_EnumDescriptorProto.with { + $0.name = "enumType" + String(id) + $0.value = [ + Google_Protobuf_EnumValueDescriptorProto.with { + $0.name = "value1" + }, + Google_Protobuf_EnumValueDescriptorProto.with { + $0.name = "value2" + }, + ] + } + + let method = Google_Protobuf_MethodDescriptorProto.with { + $0.name = "testMethod" + String(id) + $0.inputType = inputMessage.name + $0.outputType = outputMessage.name + } + + let serviceDescriptor = Google_Protobuf_ServiceDescriptorProto.with { + $0.method = [method] + $0.name = "service" + String(id) + } + + let fileDescriptorProto = Google_Protobuf_FileDescriptorProto.with { + $0.service = [serviceDescriptor] + $0.name = name + String(id) + ".proto" + $0.package = "package" + name + String(id) + $0.messageType = [inputMessage, outputMessage] + $0.enumType = [enumType] + } + + return fileDescriptorProto +} + +/// Creates the dependencies of the proto used in the testing context. +internal func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorProto] { + var fileDependencies: [Google_Protobuf_FileDescriptorProto] = [] + for id in 1 ... 4 { + let fileDescriptorProto = generateFileDescriptorProto(fileName: "bar", suffix: id) + if id != 1 { + // Dependency of the first dependency. + fileDependencies[0].dependency.append(fileDescriptorProto.name) + } + fileDependencies.append(fileDescriptorProto) + } + return fileDependencies +} + +internal func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescriptorProto] { + var protos: [Google_Protobuf_FileDescriptorProto] = [] + protos.append(generateFileDescriptorProto(fileName: "foo", suffix: 0)) + for id in 1 ... 10 { + let fileDescriptorProtoA = generateFileDescriptorProto(fileName: "fooA", suffix: id) + let fileDescriptorProtoB = generateFileDescriptorProto(fileName: "fooB", suffix: id) + let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1 + protos[parent].dependency.append(fileDescriptorProtoA.name) + protos[parent].dependency.append(fileDescriptorProtoB.name) + protos.append(fileDescriptorProtoA) + protos.append(fileDescriptorProtoB) + } + return protos +} + +extension Sequence where Element == Google_Protobuf_FileDescriptorProto { + var serviceNames: [String] { + self.flatMap { $0.service.map { $0.name } } + } +} + +extension Sequence where Element == Google_Protobuf_EnumDescriptorProto { + var names: [String] { + self.map { $0.name } + } +} From 99e99561589550c658c8b27f75675acc6435e958 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 19 Oct 2023 13:34:36 +0100 Subject: [PATCH 134/580] Add support for ordo-one/package-benchmark (#1678) --- .github/workflows/ci.yaml | 11 +++++ .gitignore | 1 + .../GRPCSwiftBenchmark/Benchmarks.swift | 37 +++++++++++++++++ Performance/Benchmarks/Package.swift | 41 +++++++++++++++++++ README.md | 19 +++++++++ 5 files changed, 109 insertions(+) create mode 100644 Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift create mode 100644 Performance/Benchmarks/Package.swift diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c8562f805..1a707761c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -50,6 +50,7 @@ jobs: matrix: include: - image: swiftlang/swift:nightly-jammy + swift-version: main env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -60,6 +61,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swift:5.9-jammy + swift-version: 5.9 env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -70,6 +72,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swift:5.8-focal + swift-version: 5.8 env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -80,6 +83,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swift:5.7-focal + swift-version: 5.7 env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -99,6 +103,13 @@ jobs: run: ./Performance/allocations/test-allocation-counts.sh env: ${{ matrix.env }} timeout-minutes: 20 + - name: Install jemalloc for benchmarking + run: apt update && apt-get install -y libjemalloc-dev + timeout-minutes: 20 + - name: Run Benchmarks + working-directory: ./Performance/Benchmarks + run: swift package benchmark baseline check --check-absolute-path Thresholds/${{ matrix.swift-version }}/ + timeout-minutes: 20 integration-tests: strategy: fail-fast: false diff --git a/.gitignore b/.gitignore index b0c63f998..4c81ad261 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ dev/codegen-tests/**/generated/* /scripts/.swift-format-source/ Package.resolved *.out.* +/Performance/Benchmarks/.benchmarkBaselines/ \ No newline at end of file diff --git a/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift b/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift new file mode 100644 index 000000000..59e30ad49 --- /dev/null +++ b/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift @@ -0,0 +1,37 @@ +/* + * Copyright 2023, 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 Benchmark +import Foundation + +let benchmarks = { + Benchmark.defaultConfiguration = .init( + metrics: [ + .mallocCountTotal, + .syscalls, + .readSyscalls, + .writeSyscalls, + .memoryLeaked, + .retainCount, + .releaseCount, + ] + ) + + // async code is currently still quite flaky in the number of retain/release it does so we don't measure them today + var configWithoutRetainRelease = Benchmark.defaultConfiguration + configWithoutRetainRelease.metrics.removeAll(where: { $0 == .retainCount || $0 == .releaseCount }) + + // Add Benchmarks here +} diff --git a/Performance/Benchmarks/Package.swift b/Performance/Benchmarks/Package.swift new file mode 100644 index 000000000..74d324150 --- /dev/null +++ b/Performance/Benchmarks/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version: 5.7 +/* + * Copyright 2023, 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 PackageDescription + +let package = Package( + name: "benchmarks", + platforms: [ + .macOS(.v13), + ], + dependencies: [ + .package(path: "../../"), + .package(url: "https://github.com/ordo-one/package-benchmark", from: "1.11.2") + ], + targets: [ + .executableTarget( + name: "GRPCSwiftBenchmark", + dependencies: [ + .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "GRPC", package: "grpc-swift") + ], + path: "Benchmarks/GRPCSwiftBenchmark", + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "package-benchmark") + ] + ), + ] +) diff --git a/README.md b/README.md index 47fa297e8..91155b7e8 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,25 @@ The `docs` directory contains documentation, including: - How to configure keepalive in [`docs/keepalive.md`][docs-keepalive] - Support for Apple Platforms and NIO Transport Services in [`docs/apple-platforms.md`][docs-apple] + +## Benchmarks + +Benchmarks for `grpc-swift` are in a separate Swift Package in the `Performance/Benchmarks` subfolder of this repository. +They use the [`package-benchmark`](https://github.com/ordo-one/package-benchmark) plugin. +Benchmarks depends on the [`jemalloc`](https://jemalloc.net) memory allocation library, which is used by `package-benchmark` to capture memory allocation statistics. +An installation guide can be found in the [Getting Started article](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted#Installing-Prerequisites-and-Platform-Support) of `package-benchmark`. +Afterwards you can run the benchmarks from CLI by going to the `Performance/Benchmarks` subfolder (e.g. `cd Performance/Benchmarks`) and invoking: +``` +swift package benchmark +``` + +Profiling benchmarks or building the benchmarks in release mode in Xcode with `jemalloc` is currently not supported and requires disabling `jemalloc`. +Make sure Xcode is closed and then open it from the CLI with the `BENCHMARK_DISABLE_JEMALLOC=true` environment variable set e.g.: +``` +BENCHMARK_DISABLE_JEMALLOC=true xed . +``` + +For more information please refer to `swift package benchmark --help` or the [documentation of `package-benchmark`](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark). ## Security From cd104a0aab8e5d298a85ace0037d7457f01c4674 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:33:00 +0100 Subject: [PATCH 135/580] Implemented file-containing-extension request for Reflection Service (#1677) Motivation: The file-containing-extension request enables users to get the file descriptor protos of the proto file containing the extension they are looking for and its transitive dependencies. Modifications: - Created a new struct (ExtensionDescriptor) to represent the type that is extended and the field number of each extension that exists inside the protos passed to the Reflection Service. - Added a dictionary inside the ReflectionServiceData registry to store the extensions, avoid duplicates and retrieve nicely the file name of the proto that contains the requested extension. The file name is then used to get the serialized file descriptor protos of the proto containing the extension and its transitive dependencies. - Added integration and unit tests. Result: The Reflection Service can now be used to get the serialized file descriptor protos of the proto containing the provided extension and its transitive dependencies. --- .../Server/ReflectionService.swift | 63 +++++++++++++ .../ReflectionServiceIntegrationTests.swift | 64 ++++++++++++- .../ReflectionServiceUnitTests.swift | 92 ++++++++++++++++++- .../GRPCReflectionServiceTests/Utils.swift | 37 +++++--- 4 files changed, 238 insertions(+), 18 deletions(-) diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 81cf8282c..fe388a0ec 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -43,15 +43,21 @@ internal struct ReflectionServiceData: Sendable { internal var serializedFileDescriptorProto: Data internal var dependencyFileNames: [String] } + private struct ExtensionDescriptor: Sendable, Hashable { + internal let extendeeTypeName: String + internal let fieldNumber: Int32 + } internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData] internal var serviceNames: [String] internal var fileNameBySymbol: [String: String] + private var fileNameByExtensionDescriptor: [ExtensionDescriptor: String] internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { self.serviceNames = [] self.fileDescriptorDataByFilename = [:] self.fileNameBySymbol = [:] + self.fileNameByExtensionDescriptor = [:] for fileDescriptorProto in fileDescriptors { let serializedFileDescriptorProto: Data @@ -70,6 +76,8 @@ internal struct ReflectionServiceData: Sendable { ) self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData self.serviceNames.append(contentsOf: fileDescriptorProto.service.map { $0.name }) + + // Populating the dictionary. for qualifiedSybolName in fileDescriptorProto.qualifiedSymbolNames { let oldValue = self.fileNameBySymbol.updateValue( fileDescriptorProto.name, @@ -83,6 +91,28 @@ internal struct ReflectionServiceData: Sendable { ) } } + + // Populating the dictionary. + for `extension` in fileDescriptorProto.extension { + let extensionDescriptor = ExtensionDescriptor( + extendeeTypeName: `extension`.extendee, + fieldNumber: `extension`.number + ) + let oldFileName = self.fileNameByExtensionDescriptor.updateValue( + fileDescriptorProto.name, + forKey: extensionDescriptor + ) + if let oldFileName = oldFileName { + throw GRPCStatus( + code: .alreadyExists, + message: + """ + The extension of the \(extensionDescriptor.extendeeTypeName) type with the field number equal to \ + \(extensionDescriptor.fieldNumber) from \(fileDescriptorProto.name) already exists in \(oldFileName). + """ + ) + } + } } } @@ -119,6 +149,14 @@ internal struct ReflectionServiceData: Sendable { internal func nameOfFileContainingSymbol(named symbolName: String) -> String? { return self.fileNameBySymbol[symbolName] } + + internal func nameOfFileContainingExtension( + named extendeeName: String, + fieldNumber number: Int32 + ) -> String? { + let key = ExtensionDescriptor(extendeeTypeName: extendeeName, fieldNumber: number) + return self.fileNameByExtensionDescriptor[key] + } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -172,6 +210,24 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync return try self.findFileByFileName(fileName, request: request) } + internal func findFileByExtension( + extensionRequest: Reflection_ExtensionRequest, + request: Reflection_ServerReflectionRequest + ) throws -> Reflection_ServerReflectionResponse { + guard + let fileName = self.protoRegistry.nameOfFileContainingExtension( + named: extensionRequest.containingType, + fieldNumber: extensionRequest.extensionNumber + ) + else { + throw GRPCStatus( + code: .notFound, + message: "The provided extension could not be found." + ) + } + return try self.findFileByFileName(fileName, request: request) + } + internal func serverReflectionInfo( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, @@ -197,6 +253,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync ) try await responseStream.send(response) + case let .fileContainingExtension(extensionRequest): + let response = try self.findFileByExtension( + extensionRequest: extensionRequest, + request: request + ) + try await responseStream.send(response) + default: throw GRPCStatus(code: .unimplemented) } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index c6bd1a339..651ac0c3c 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -29,7 +29,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { private let protos: [Google_Protobuf_FileDescriptorProto] = makeProtosWithDependencies() private let independentProto: Google_Protobuf_FileDescriptorProto = generateFileDescriptorProto( fileName: "independentBar", - suffix: 5 + suffix: "5" ) private func setUpServerAndChannel() throws { @@ -174,4 +174,66 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { } } } + + func testFileByExtension() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileContainingExtension = .with { + $0.containingType = "inputMessage1" + $0.extensionNumber = 2 + } + } + ) + + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + let receivedData: [Google_Protobuf_FileDescriptorProto] + do { + receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + } catch { + return XCTFail("Could not serialize data received as a message.") + } + + let fileToFind = self.protos[0] + let dependentProtos = self.protos[1...] + var receivedProtoContainingExtension = 0 + var dependenciesCount = 0 + for fileDescriptorProto in receivedData { + if fileDescriptorProto == fileToFind { + receivedProtoContainingExtension += 1 + XCTAssert( + fileDescriptorProto.extension.map { $0.name }.contains("extensionInputMessage1"), + """ + The response doesn't contain the serialized file descriptor proto \ + containing the \"extensionInputMessage1\" extension. + """ + ) + } else { + dependenciesCount += 1 + XCTAssert( + dependentProtos.contains(fileDescriptorProto), + """ + The \(fileDescriptorProto.name) is not a dependency of the \ + proto file containing the \"extensionInputMessage1\" symbol. + """ + ) + } + } + XCTAssertEqual( + receivedProtoContainingExtension, + 1, + "The file descriptor proto of the proto containing the extension was not received." + ) + XCTAssertEqual(dependenciesCount, 3) + } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index e3913c1d0..7b7f60b14 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -62,7 +62,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { XCTAssertEqual(registryServices, servicesNames) } - /// Testing the fileNameBySymbol array of the ReflectionServiceData object. + /// Testing the fileNameBySymbol dictionary of the ReflectionServiceData object. func testFileNameBySymbol() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) @@ -85,7 +85,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { var protos = makeProtosWithDependencies() protos[1].messageType.append( Google_Protobuf_DescriptorProto.with { - $0.name = "inputMessage" + $0.name = "inputMessage2" $0.field = [ Google_Protobuf_FieldDescriptorProto.with { $0.name = "inputField" @@ -104,7 +104,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { code: .alreadyExists, message: """ - The packagebar2.inputMessage symbol from bar2.proto \ + The packagebar2.inputMessage2 symbol from bar2.proto \ already exists in bar2.proto. """ ) @@ -124,7 +124,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { func testNameOfFileContainingSymbolMessage() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage") + let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage1") XCTAssertEqual(fileName, "bar1.proto") } @@ -148,7 +148,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) let fileName = registry.nameOfFileContainingSymbol(named: "packagebar2.enumType3") - XCTAssertEqual(fileName, nil) + XCTAssertNil(fileName) } // Testing the serializedFileDescriptorProto method in different cases. @@ -329,4 +329,86 @@ final class ReflectionServiceUnitTests: GRPCTestCase { ) } } + + // Testing the nameOfFileContainingExtension() method. + + func testNameOfFileContainingExtensions() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + for proto in protos { + for `extension` in proto.extension { + let registryFileName = registry.nameOfFileContainingExtension( + named: `extension`.extendee, + fieldNumber: `extension`.number + ) + XCTAssertEqual(registryFileName, proto.name) + } + } + } + + func testNameOfFileContainingExtensionsSameTypeExtensionsDifferentNumbers() throws { + var protos = makeProtosWithDependencies() + protos[0].extension.append( + .with { + $0.extendee = "inputMessage1" + $0.number = 3 + } + ) + let registry = try ReflectionServiceData(fileDescriptors: protos) + + for proto in protos { + for `extension` in proto.extension { + let registryFileName = registry.nameOfFileContainingExtension( + named: `extension`.extendee, + fieldNumber: `extension`.number + ) + XCTAssertEqual(registryFileName, proto.name) + } + } + } + + func testNameOfFileContainingExtensionsInvalidTypeName() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let registryFileName = registry.nameOfFileContainingExtension( + named: "InvalidType", + fieldNumber: 2 + ) + XCTAssertNil(registryFileName) + } + + func testNameOfFileContainingExtensionsInvalidFieldNumber() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let registryFileName = registry.nameOfFileContainingExtension( + named: protos[0].extension[0].extendee, + fieldNumber: 4 + ) + XCTAssertNil(registryFileName) + } + + func testNameOfFileContainingExtensionsDuplicatedExtensions() throws { + var protos = makeProtosWithDependencies() + protos[0].extension.append( + .with { + $0.extendee = "inputMessage1" + $0.number = 2 + } + ) + XCTAssertThrowsError( + try ReflectionServiceData(fileDescriptors: protos) + ) { error in + XCTAssertEqual( + error as? GRPCStatus, + GRPCStatus( + code: .alreadyExists, + message: + """ + The extension of the inputMessage1 type with the field number equal to \ + 2 from \(protos[0].name) already exists in \(protos[0].name). + """ + ) + ) + } + } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift index 95f68f131..faea453cb 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift @@ -20,10 +20,10 @@ import SwiftProtobuf internal func generateFileDescriptorProto( fileName name: String, - suffix id: Int + suffix: String ) -> Google_Protobuf_FileDescriptorProto { let inputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "inputMessage" + $0.name = "inputMessage" + suffix $0.field = [ Google_Protobuf_FieldDescriptorProto.with { $0.name = "inputField" @@ -32,8 +32,14 @@ internal func generateFileDescriptorProto( ] } + let inputMessageExtension = Google_Protobuf_FieldDescriptorProto.with { + $0.name = "extensionInputMessage" + suffix + $0.extendee = "inputMessage" + suffix + $0.number = 2 + } + let outputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "outputMessage" + $0.name = "outputMessage" + suffix $0.field = [ Google_Protobuf_FieldDescriptorProto.with { $0.name = "outputField" @@ -43,7 +49,7 @@ internal func generateFileDescriptorProto( } let enumType = Google_Protobuf_EnumDescriptorProto.with { - $0.name = "enumType" + String(id) + $0.name = "enumType" + suffix $0.value = [ Google_Protobuf_EnumValueDescriptorProto.with { $0.name = "value1" @@ -55,22 +61,23 @@ internal func generateFileDescriptorProto( } let method = Google_Protobuf_MethodDescriptorProto.with { - $0.name = "testMethod" + String(id) + $0.name = "testMethod" + suffix $0.inputType = inputMessage.name $0.outputType = outputMessage.name } let serviceDescriptor = Google_Protobuf_ServiceDescriptorProto.with { $0.method = [method] - $0.name = "service" + String(id) + $0.name = "service" + suffix } let fileDescriptorProto = Google_Protobuf_FileDescriptorProto.with { $0.service = [serviceDescriptor] - $0.name = name + String(id) + ".proto" - $0.package = "package" + name + String(id) + $0.name = name + suffix + ".proto" + $0.package = "package" + name + suffix $0.messageType = [inputMessage, outputMessage] $0.enumType = [enumType] + $0.extension = [inputMessageExtension] } return fileDescriptorProto @@ -80,7 +87,7 @@ internal func generateFileDescriptorProto( internal func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorProto] { var fileDependencies: [Google_Protobuf_FileDescriptorProto] = [] for id in 1 ... 4 { - let fileDescriptorProto = generateFileDescriptorProto(fileName: "bar", suffix: id) + let fileDescriptorProto = generateFileDescriptorProto(fileName: "bar", suffix: String(id)) if id != 1 { // Dependency of the first dependency. fileDependencies[0].dependency.append(fileDescriptorProto.name) @@ -92,10 +99,16 @@ internal func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorPro internal func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescriptorProto] { var protos: [Google_Protobuf_FileDescriptorProto] = [] - protos.append(generateFileDescriptorProto(fileName: "foo", suffix: 0)) + protos.append(generateFileDescriptorProto(fileName: "foo", suffix: "0")) for id in 1 ... 10 { - let fileDescriptorProtoA = generateFileDescriptorProto(fileName: "fooA", suffix: id) - let fileDescriptorProtoB = generateFileDescriptorProto(fileName: "fooB", suffix: id) + let fileDescriptorProtoA = generateFileDescriptorProto( + fileName: "fooA", + suffix: String(id) + "A" + ) + let fileDescriptorProtoB = generateFileDescriptorProto( + fileName: "fooB", + suffix: String(id) + "B" + ) let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1 protos[parent].dependency.append(fileDescriptorProtoA.name) protos[parent].dependency.append(fileDescriptorProtoB.name) From e97206ce77a2a132be4531c84ef655dd321bf847 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 23 Oct 2023 14:33:45 +0100 Subject: [PATCH 136/580] Vendor in NIOs lock (#1681) Motivation: For v2 we need a lock, and since we want to avoid taking on a NIO dependency in the core library we can vendor in NIOs lock type instead. Motivation: - Vendor in NIOs lock type and locked value box, make them internal - Update NOTICES.txt Result: We have a lock type. --- NOTICES.txt | 3 + .../Internal/Concurrency Primities/Lock.swift | 255 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 Sources/GRPCCore/Internal/Concurrency Primities/Lock.swift diff --git a/NOTICES.txt b/NOTICES.txt index f1ff7bbab..9f6a63d5d 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -21,6 +21,9 @@ This product uses scripts derived from SwiftNIO's integration testing framework: 'test_01_allocation_counts.sh', 'run-nio-alloc-counter-tests.sh' and 'test_functions.sh'. +It also uses derivations of SwiftNIO's lock 'NIOLock.swift' and locked value box +'NIOLockedValueBox.swift'. + * LICENSE (Apache License 2.0): * https://github.com/apple/swift-nio/blob/main/LICENSE.txt * HOMEPAGE: diff --git a/Sources/GRPCCore/Internal/Concurrency Primities/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primities/Lock.swift new file mode 100644 index 000000000..0aa5c40d4 --- /dev/null +++ b/Sources/GRPCCore/Internal/Concurrency Primities/Lock.swift @@ -0,0 +1,255 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#endif + +@usableFromInline +typealias LockPrimitive = pthread_mutex_t + +@usableFromInline +enum LockOperations {} + +extension LockOperations { + @inlinable + static func create(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + var attr = pthread_mutexattr_t() + pthread_mutexattr_init(&attr) + + let err = pthread_mutex_init(mutex, &attr) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + } + + @inlinable + static func destroy(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + let err = pthread_mutex_destroy(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + } + + @inlinable + static func lock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + let err = pthread_mutex_lock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + } + + @inlinable + static func unlock(_ mutex: UnsafeMutablePointer) { + mutex.assertValidAlignment() + + let err = pthread_mutex_unlock(mutex) + precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") + } +} + +// Tail allocate both the mutex and a generic value using ManagedBuffer. +// Both the header pointer and the elements pointer are stable for +// the class's entire lifetime. +// +// However, for safety reasons, we elect to place the lock in the "elements" +// section of the buffer instead of the head. The reasoning here is subtle, +// so buckle in. +// +// _As a practical matter_, the implementation of ManagedBuffer ensures that +// the pointer to the header is stable across the lifetime of the class, and so +// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` +// the value of the header pointer will be the same. This is because ManagedBuffer uses +// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure +// that it does not invoke any weird Swift accessors that might copy the value. +// +// _However_, the header is also available via the `.header` field on the ManagedBuffer. +// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends +// do not interact with Swift's exclusivity model. That is, the various `with` functions do not +// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because +// there's literally no other way to perform the access, but for `.header` it's entirely possible +// to accidentally recursively read it. +// +// Our implementation is free from these issues, so we don't _really_ need to worry about it. +// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive +// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, +// and future maintainers will be happier that we were cautious. +// +// See also: https://github.com/apple/swift/pull/40000 +@usableFromInline +final class LockStorage: ManagedBuffer { + + @inlinable + static func create(value: Value) -> Self { + let buffer = Self.create(minimumCapacity: 1) { _ in + return value + } + let storage = unsafeDowncast(buffer, to: Self.self) + + storage.withUnsafeMutablePointers { _, lockPtr in + LockOperations.create(lockPtr) + } + + return storage + } + + @inlinable + func lock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.lock(lockPtr) + } + } + + @inlinable + func unlock() { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.unlock(lockPtr) + } + } + + @inlinable + deinit { + self.withUnsafeMutablePointerToElements { lockPtr in + LockOperations.destroy(lockPtr) + } + } + + @inlinable + func withLockPrimitive( + _ body: (UnsafeMutablePointer) throws -> T + ) rethrows -> T { + try self.withUnsafeMutablePointerToElements { lockPtr in + return try body(lockPtr) + } + } + + @inlinable + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + try self.withUnsafeMutablePointers { valuePtr, lockPtr in + LockOperations.lock(lockPtr) + defer { LockOperations.unlock(lockPtr) } + return try mutate(&valuePtr.pointee) + } + } +} + +extension LockStorage: @unchecked Sendable {} + +/// A threading lock based on `libpthread` instead of `libdispatch`. +/// +/// - note: ``Lock`` has reference semantics. +/// +/// This object provides a lock on top of a single `pthread_mutex_t`. This kind +/// of lock is safe to use with `libpthread`-based threading models, such as the +/// one used by NIO. On Windows, the lock is based on the substantially similar +/// `SRWLOCK` type. +@usableFromInline +struct Lock { + @usableFromInline + internal let _storage: LockStorage + + /// Create a new lock. + @inlinable + init() { + self._storage = .create(value: ()) + } + + /// Acquire the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `unlock`, to simplify lock handling. + @inlinable + func lock() { + self._storage.lock() + } + + /// Release the lock. + /// + /// Whenever possible, consider using `withLock` instead of this method and + /// `lock`, to simplify lock handling. + @inlinable + func unlock() { + self._storage.unlock() + } + + @inlinable + internal func withLockPrimitive( + _ body: (UnsafeMutablePointer) throws -> T + ) rethrows -> T { + return try self._storage.withLockPrimitive(body) + } +} + +extension Lock { + /// Acquire the lock for the duration of the given block. + /// + /// This convenience method should be preferred to `lock` and `unlock` in + /// most situations, as it ensures that the lock will be released regardless + /// of how `body` exits. + /// + /// - Parameter body: The block to execute while holding the lock. + /// - Returns: The value returned by the block. + @inlinable + func withLock(_ body: () throws -> T) rethrows -> T { + self.lock() + defer { + self.unlock() + } + return try body() + } +} + +extension Lock: Sendable {} + +extension UnsafeMutablePointer { + @inlinable + func assertValidAlignment() { + assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) + } +} + +@usableFromInline +struct LockedValueBox { + @usableFromInline + let storage: LockStorage + + @inlinable + init(_ value: Value) { + self.storage = .create(value: value) + } + + @inlinable + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + return try self.storage.withLockedValue(mutate) + } +} + +extension LockedValueBox: Sendable where Value: Sendable {} From ef5a0fe38a8a5506d5d73ba53a9cfe2e90e7758a Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:27:37 +0100 Subject: [PATCH 137/580] Support all_extension_numbers_of_type reflection requests (#1680) Motivation: The reflection service should provide the possibility for users to request the list with all the field numbers of the extensions of a type. Modifications: - Implemented the dictionary that stores the arrays of integers representing the field numbers of the extensions for each type that has extensions. - Implemented the methods of the Reflection Service that create the specific response with the array of integers representing the field numbers or an empty array for the case that the type doesn't have any extensions (but is a valid type). - Implemented integration and Unit tests. Result: The Reflection Service will enable users to find all the extension field numbers for a specific type they requested. --- .../Server/ReflectionService.swift | 62 ++++++++++- .../ReflectionServiceIntegrationTests.swift | 31 +++++- .../ReflectionServiceUnitTests.swift | 104 +++++++++++++----- .../GRPCReflectionServiceTests/Utils.swift | 30 ++++- 4 files changed, 186 insertions(+), 41 deletions(-) diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index fe388a0ec..35a3afc1f 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -51,13 +51,18 @@ internal struct ReflectionServiceData: Sendable { internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData] internal var serviceNames: [String] internal var fileNameBySymbol: [String: String] + + // Stores the file names for each extension identified by an ExtensionDescriptor object. private var fileNameByExtensionDescriptor: [ExtensionDescriptor: String] + // Stores the field numbers for each type that has extensions. + private var fieldNumbersByType: [String: [Int32]] internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { self.serviceNames = [] self.fileDescriptorDataByFilename = [:] self.fileNameBySymbol = [:] self.fileNameByExtensionDescriptor = [:] + self.fieldNumbersByType = [:] for fileDescriptorProto in fileDescriptors { let serializedFileDescriptorProto: Data @@ -92,10 +97,15 @@ internal struct ReflectionServiceData: Sendable { } } - // Populating the dictionary. + for typeName in fileDescriptorProto.qualifiedMessageTypes { + self.fieldNumbersByType[typeName] = [] + } + + // Populating the dictionary and the one. for `extension` in fileDescriptorProto.extension { + let typeName = String(`extension`.extendee.drop(while: { $0 == "." })) let extensionDescriptor = ExtensionDescriptor( - extendeeTypeName: `extension`.extendee, + extendeeTypeName: typeName, fieldNumber: `extension`.number ) let oldFileName = self.fileNameByExtensionDescriptor.updateValue( @@ -112,6 +122,7 @@ internal struct ReflectionServiceData: Sendable { """ ) } + self.fieldNumbersByType[typeName, default: []].append(`extension`.number) } } } @@ -151,12 +162,23 @@ internal struct ReflectionServiceData: Sendable { } internal func nameOfFileContainingExtension( - named extendeeName: String, + extendeeName: String, fieldNumber number: Int32 ) -> String? { let key = ExtensionDescriptor(extendeeTypeName: extendeeName, fieldNumber: number) return self.fileNameByExtensionDescriptor[key] } + + // Returns an empty array if the type has no extensions. + internal func extensionsFieldNumbersOfType(named typeName: String) throws -> [Int32] { + guard let fieldNumbers = self.fieldNumbersByType[typeName] else { + throw GRPCStatus( + code: .invalidArgument, + message: "The provided type is invalid." + ) + } + return fieldNumbers + } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -216,7 +238,7 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync ) throws -> Reflection_ServerReflectionResponse { guard let fileName = self.protoRegistry.nameOfFileContainingExtension( - named: extensionRequest.containingType, + extendeeName: extensionRequest.containingType, fieldNumber: extensionRequest.extensionNumber ) else { @@ -228,6 +250,20 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync return try self.findFileByFileName(fileName, request: request) } + internal func findExtensionsFieldNumbersOfType( + named typeName: String, + request: Reflection_ServerReflectionRequest + ) throws -> Reflection_ServerReflectionResponse { + let fieldNumbers = try self.protoRegistry.extensionsFieldNumbersOfType(named: typeName) + return Reflection_ServerReflectionResponse( + request: request, + extensionNumberResponse: .with { + $0.baseTypeName = typeName + $0.extensionNumber = fieldNumbers + } + ) + } + internal func serverReflectionInfo( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, @@ -260,6 +296,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync ) try await responseStream.send(response) + case let .allExtensionNumbersOfType(typeName): + let response = try self.findExtensionsFieldNumbersOfType( + named: typeName, + request: request + ) + try await responseStream.send(response) + default: throw GRPCStatus(code: .unimplemented) } @@ -289,6 +332,17 @@ extension Reflection_ServerReflectionResponse { $0.listServicesResponse = listServicesResponse } } + + init( + request: Reflection_ServerReflectionRequest, + extensionNumberResponse: Reflection_ExtensionNumberResponse + ) { + self = .with { + $0.validHost = request.host + $0.originalRequest = request + $0.allExtensionNumbersResponse = extensionNumberResponse + } + } } extension Google_Protobuf_FileDescriptorProto { diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index 651ac0c3c..790c2c9f8 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -184,7 +184,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { .with { $0.host = "127.0.0.1" $0.fileContainingExtension = .with { - $0.containingType = "inputMessage1" + $0.containingType = "packagebar1.inputMessage1" $0.extensionNumber = 2 } } @@ -212,10 +212,12 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { if fileDescriptorProto == fileToFind { receivedProtoContainingExtension += 1 XCTAssert( - fileDescriptorProto.extension.map { $0.name }.contains("extensionInputMessage1"), + fileDescriptorProto.extension.map { $0.name }.contains( + "extension.packagebar1.inputMessage1-2" + ), """ The response doesn't contain the serialized file descriptor proto \ - containing the \"extensionInputMessage1\" extension. + containing the \"extensioninputMessage1-2\" extension. """ ) } else { @@ -224,7 +226,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { dependentProtos.contains(fileDescriptorProto), """ The \(fileDescriptorProto.name) is not a dependency of the \ - proto file containing the \"extensionInputMessage1\" symbol. + proto file containing the \"extensioninputMessage1-2\" extension. """ ) } @@ -236,4 +238,25 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { ) XCTAssertEqual(dependenciesCount, 3) } + + func testAllExtensionNumbersOfType() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.allExtensionNumbersOfType = "packagebar2.inputMessage2" + } + ) + + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + XCTAssertEqual(message.allExtensionNumbersResponse.baseTypeName, "packagebar2.inputMessage2") + XCTAssertEqual(message.allExtensionNumbersResponse.extensionNumber, [1, 2, 3, 4, 5]) + } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index 7b7f60b14..8ede0e472 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -337,29 +337,9 @@ final class ReflectionServiceUnitTests: GRPCTestCase { let registry = try ReflectionServiceData(fileDescriptors: protos) for proto in protos { for `extension` in proto.extension { + let typeName = String(`extension`.extendee.drop(while: { $0 == "." })) let registryFileName = registry.nameOfFileContainingExtension( - named: `extension`.extendee, - fieldNumber: `extension`.number - ) - XCTAssertEqual(registryFileName, proto.name) - } - } - } - - func testNameOfFileContainingExtensionsSameTypeExtensionsDifferentNumbers() throws { - var protos = makeProtosWithDependencies() - protos[0].extension.append( - .with { - $0.extendee = "inputMessage1" - $0.number = 3 - } - ) - let registry = try ReflectionServiceData(fileDescriptors: protos) - - for proto in protos { - for `extension` in proto.extension { - let registryFileName = registry.nameOfFileContainingExtension( - named: `extension`.extendee, + extendeeName: typeName, fieldNumber: `extension`.number ) XCTAssertEqual(registryFileName, proto.name) @@ -371,7 +351,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) let registryFileName = registry.nameOfFileContainingExtension( - named: "InvalidType", + extendeeName: "InvalidType", fieldNumber: 2 ) XCTAssertNil(registryFileName) @@ -381,8 +361,8 @@ final class ReflectionServiceUnitTests: GRPCTestCase { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) let registryFileName = registry.nameOfFileContainingExtension( - named: protos[0].extension[0].extendee, - fieldNumber: 4 + extendeeName: protos[0].extension[0].extendee, + fieldNumber: 9 ) XCTAssertNil(registryFileName) } @@ -391,7 +371,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { var protos = makeProtosWithDependencies() protos[0].extension.append( .with { - $0.extendee = "inputMessage1" + $0.extendee = ".packagebar1.inputMessage1" $0.number = 2 } ) @@ -404,11 +384,81 @@ final class ReflectionServiceUnitTests: GRPCTestCase { code: .alreadyExists, message: """ - The extension of the inputMessage1 type with the field number equal to \ + The extension of the packagebar1.inputMessage1 type with the field number equal to \ 2 from \(protos[0].name) already exists in \(protos[0].name). """ ) ) } } + + // Testing the extensionsFieldNumbersOfType() method. + + func testExtensionsFieldNumbersOfType() throws { + var protos = makeProtosWithDependencies() + protos[0].extension.append( + .with { + $0.extendee = ".packagebar1.inputMessage1" + $0.number = 120 + } + ) + let registry = try ReflectionServiceData(fileDescriptors: protos) + let extensionNumbers = try registry.extensionsFieldNumbersOfType( + named: "packagebar1.inputMessage1" + ) + XCTAssertEqual(extensionNumbers, [1, 2, 3, 4, 5, 120]) + } + + func testExtensionsFieldNumbersOfTypeNoExtensionsType() throws { + var protos = makeProtosWithDependencies() + protos[0].messageType.append( + Google_Protobuf_DescriptorProto.with { + $0.name = "noExtensionMessage" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "noExtensionField" + $0.type = .bool + } + ] + } + ) + let registry = try ReflectionServiceData(fileDescriptors: protos) + let extensionNumbers = try registry.extensionsFieldNumbersOfType( + named: "packagebar1.noExtensionMessage" + ) + XCTAssertEqual(extensionNumbers, []) + } + + func testExtensionsFieldNumbersOfTypeInvalidTypeName() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + XCTAssertThrowsError( + try registry.extensionsFieldNumbersOfType( + named: "packagebar1.invalidTypeMessage" + ) + ) { error in + XCTAssertEqual( + error as? GRPCStatus, + GRPCStatus( + code: .invalidArgument, + message: "The provided type is invalid." + ) + ) + } + } + + func testExtensionsFieldNumbersOfTypeExtensionsInDifferentProtoFiles() throws { + var protos = makeProtosWithDependencies() + protos[2].extension.append( + .with { + $0.extendee = ".packagebar1.inputMessage1" + $0.number = 130 + } + ) + let registry = try ReflectionServiceData(fileDescriptors: protos) + let extensionNumbers = try registry.extensionsFieldNumbersOfType( + named: "packagebar1.inputMessage1" + ) + XCTAssertEqual(extensionNumbers, [1, 2, 3, 4, 5, 130]) + } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift index faea453cb..9008aad9e 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift @@ -18,6 +18,23 @@ import Foundation import GRPC import SwiftProtobuf +internal func makeExtensions( + forType typeName: String, + number: Int +) -> [Google_Protobuf_FieldDescriptorProto] { + var extensions: [Google_Protobuf_FieldDescriptorProto] = [] + for id in 1 ... number { + extensions.append( + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "extension" + typeName + "-" + String(id) + $0.extendee = typeName + $0.number = Int32(id) + } + ) + } + return extensions +} + internal func generateFileDescriptorProto( fileName name: String, suffix: String @@ -32,11 +49,11 @@ internal func generateFileDescriptorProto( ] } - let inputMessageExtension = Google_Protobuf_FieldDescriptorProto.with { - $0.name = "extensionInputMessage" + suffix - $0.extendee = "inputMessage" + suffix - $0.number = 2 - } + let packageName = "package" + name + suffix + let inputMessageExtensions = makeExtensions( + forType: "." + packageName + "." + "inputMessage" + suffix, + number: 5 + ) let outputMessage = Google_Protobuf_DescriptorProto.with { $0.name = "outputMessage" + suffix @@ -77,7 +94,7 @@ internal func generateFileDescriptorProto( $0.package = "package" + name + suffix $0.messageType = [inputMessage, outputMessage] $0.enumType = [enumType] - $0.extension = [inputMessageExtension] + $0.extension = inputMessageExtensions } return fileDescriptorProto @@ -109,6 +126,7 @@ internal func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescri fileName: "fooB", suffix: String(id) + "B" ) + let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1 protos[parent].dependency.append(fileDescriptorProtoA.name) protos[parent].dependency.append(fileDescriptorProtoB.name) From fdf0a8175ec3b2f1c8ae75fa6fb8a7dbddb1033e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 25 Oct 2023 10:31:53 +0100 Subject: [PATCH 138/580] Allow binding to a socket address via server builder (#1686) Motivation: Users can bind to a socket address if they use the server config but not the builder. Modifications: - Add API to connect to a socket address or bind target via the server builder API Result: More convenient APIs --- Sources/GRPC/ServerBuilder.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/GRPC/ServerBuilder.swift b/Sources/GRPC/ServerBuilder.swift index 8f98520ee..c783e0d24 100644 --- a/Sources/GRPC/ServerBuilder.swift +++ b/Sources/GRPC/ServerBuilder.swift @@ -62,11 +62,23 @@ extension Server { return Server.start(configuration: self.configuration) } + public func bind(to socketAddress: SocketAddress) -> EventLoopFuture { + self.configuration.target = .socketAddress(socketAddress) + self.configuration.tlsConfiguration = self.maybeTLS + return Server.start(configuration: self.configuration) + } + public func bind(vsockAddress: VsockAddress) -> EventLoopFuture { self.configuration.target = .vsockAddress(vsockAddress) self.configuration.tlsConfiguration = self.maybeTLS return Server.start(configuration: self.configuration) } + + public func bind(to target: BindTarget) -> EventLoopFuture { + self.configuration.target = target + self.configuration.tlsConfiguration = self.maybeTLS + return Server.start(configuration: self.configuration) + } } } From f1669c8ba9b658a6c64444004a4e3d3b97a2e217 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 25 Oct 2023 11:07:03 +0100 Subject: [PATCH 139/580] Add backpressure aware async sequence (#1687) Motivation: Swift doesn't have a general purpose backpressure aware async sequence yet. However, one has been pitched in SE-0406. While we can't use it yet, we can vendor it in. Modifications: - Vendor in the code from SE-0406 with some minor modifications, such as changing the name and making it internal Result: We have a backpressure aware async sequence. --- NOTICES.txt | 10 + Package.swift | 5 +- .../Streaming/Internal/BufferedStream.swift | 1936 +++++++++++++++++ .../Internal/BufferedStreamTests.swift | 1103 ++++++++++ .../AsyncSequence+Utilities.swift | 16 + 5 files changed, 3069 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCCore/Streaming/Internal/BufferedStream.swift create mode 100644 Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift diff --git a/NOTICES.txt b/NOTICES.txt index 9f6a63d5d..cdd00a5ec 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -28,3 +28,13 @@ It also uses derivations of SwiftNIO's lock 'NIOLock.swift' and locked value box * https://github.com/apple/swift-nio/blob/main/LICENSE.txt * HOMEPAGE: * https://github.com/apple/swift-nio + +--- + +This product contains a derivation of the backpressure aware async stream from +the Swift project. + + * LICENSE (Apache License 2.0): + * https://github.com/apple/swift/blob/main/LICENSE.txt + * HOMEPAGE: + * https://github.com/apple/swift diff --git a/Package.swift b/Package.swift index 99effbe88..10472303b 100644 --- a/Package.swift +++ b/Package.swift @@ -157,7 +157,9 @@ extension Target { static let grpcCore: Target = .target( name: "GRPCCore", - dependencies: [], + dependencies: [ + .dequeModule + ], path: "Sources/GRPCCore" ) @@ -221,6 +223,7 @@ extension Target { name: "GRPCCoreTests", dependencies: [ .grpcCore, + .dequeModule, ] ) diff --git a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift new file mode 100644 index 000000000..79da6833f --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift @@ -0,0 +1,1936 @@ +/* + * 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2020-2021 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import DequeModule + +/// An asynchronous sequence generated from an error-throwing closure that +/// calls a continuation to produce new elements. +/// +/// `BufferedStream` conforms to `AsyncSequence`, providing a convenient +/// way to create an asynchronous sequence without manually implementing an +/// asynchronous iterator. In particular, an asynchronous stream is well-suited +/// to adapt callback- or delegation-based APIs to participate with +/// `async`-`await`. +/// +/// In contrast to `AsyncStream`, this type can throw an error from the awaited +/// `next()`, which terminates the stream with the thrown error. +/// +/// You initialize an `BufferedStream` with a closure that receives an +/// `BufferedStream.Continuation`. Produce elements in this closure, then +/// provide them to the stream by calling the continuation's `yield(_:)` method. +/// When there are no further elements to produce, call the continuation's +/// `finish()` method. This causes the sequence iterator to produce a `nil`, +/// which terminates the sequence. If an error occurs, call the continuation's +/// `finish(throwing:)` method, which causes the iterator's `next()` method to +/// throw the error to the awaiting call point. The continuation is `Sendable`, +/// which permits calling it from concurrent contexts external to the iteration +/// of the `BufferedStream`. +/// +/// An arbitrary source of elements can produce elements faster than they are +/// consumed by a caller iterating over them. Because of this, `BufferedStream` +/// defines a buffering behavior, allowing the stream to buffer a specific +/// number of oldest or newest elements. By default, the buffer limit is +/// `Int.max`, which means it's unbounded. +/// +/// ### Adapting Existing Code to Use Streams +/// +/// To adapt existing callback code to use `async`-`await`, use the callbacks +/// to provide values to the stream, by using the continuation's `yield(_:)` +/// method. +/// +/// Consider a hypothetical `QuakeMonitor` type that provides callers with +/// `Quake` instances every time it detects an earthquake. To receive callbacks, +/// callers set a custom closure as the value of the monitor's +/// `quakeHandler` property, which the monitor calls back as necessary. Callers +/// can also set an `errorHandler` to receive asynchronous error notifications, +/// such as the monitor service suddenly becoming unavailable. +/// +/// class QuakeMonitor { +/// var quakeHandler: ((Quake) -> Void)? +/// var errorHandler: ((Error) -> Void)? +/// +/// func startMonitoring() {…} +/// func stopMonitoring() {…} +/// } +/// +/// To adapt this to use `async`-`await`, extend the `QuakeMonitor` to add a +/// `quakes` property, of type `BufferedStream`. In the getter for +/// this property, return an `BufferedStream`, whose `build` closure -- +/// called at runtime to create the stream -- uses the continuation to +/// perform the following steps: +/// +/// 1. Creates a `QuakeMonitor` instance. +/// 2. Sets the monitor's `quakeHandler` property to a closure that receives +/// each `Quake` instance and forwards it to the stream by calling the +/// continuation's `yield(_:)` method. +/// 3. Sets the monitor's `errorHandler` property to a closure that receives +/// any error from the monitor and forwards it to the stream by calling the +/// continuation's `finish(throwing:)` method. This causes the stream's +/// iterator to throw the error and terminate the stream. +/// 4. Sets the continuation's `onTermination` property to a closure that +/// calls `stopMonitoring()` on the monitor. +/// 5. Calls `startMonitoring` on the `QuakeMonitor`. +/// +/// ``` +/// extension QuakeMonitor { +/// +/// static var throwingQuakes: BufferedStream { +/// BufferedStream { continuation in +/// let monitor = QuakeMonitor() +/// monitor.quakeHandler = { quake in +/// continuation.yield(quake) +/// } +/// monitor.errorHandler = { error in +/// continuation.finish(throwing: error) +/// } +/// continuation.onTermination = { @Sendable _ in +/// monitor.stopMonitoring() +/// } +/// monitor.startMonitoring() +/// } +/// } +/// } +/// ``` +/// +/// +/// Because the stream is an `AsyncSequence`, the call point uses the +/// `for`-`await`-`in` syntax to process each `Quake` instance as produced by the stream: +/// +/// do { +/// for try await quake in quakeStream { +/// print("Quake: \(quake.date)") +/// } +/// print("Stream done.") +/// } catch { +/// print("Error: \(error)") +/// } +/// +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@usableFromInline +internal struct BufferedStream { + @usableFromInline + final class _Backing: Sendable { + @usableFromInline + let storage: _BackPressuredStorage + + @inlinable + init(storage: _BackPressuredStorage) { + self.storage = storage + } + + deinit { + self.storage.sequenceDeinitialized() + } + } + + @usableFromInline + enum _Implementation: Sendable { + /// This is the implementation with backpressure based on the Source + case backpressured(_Backing) + } + + @usableFromInline + let implementation: _Implementation +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BufferedStream: AsyncSequence { + /// The asynchronous iterator for iterating an asynchronous stream. + /// + /// This type is not `Sendable`. Don't use it from multiple + /// concurrent contexts. It is a programmer error to invoke `next()` from a + /// concurrent context that contends with another such call, which + /// results in a call to `fatalError()`. + @usableFromInline + internal struct Iterator: AsyncIteratorProtocol { + @usableFromInline + final class _Backing { + @usableFromInline + let storage: _BackPressuredStorage + + @inlinable + init(storage: _BackPressuredStorage) { + self.storage = storage + self.storage.iteratorInitialized() + } + + deinit { + self.storage.iteratorDeinitialized() + } + } + + @usableFromInline + enum _Implementation { + /// This is the implementation with backpressure based on the Source + case backpressured(_Backing) + } + + @usableFromInline + var implementation: _Implementation + + @inlinable + init(implementation: _Implementation) { + self.implementation = implementation + } + + /// The next value from the asynchronous stream. + /// + /// When `next()` returns `nil`, this signifies the end of the + /// `BufferedStream`. + /// + /// It is a programmer error to invoke `next()` from a concurrent context + /// that contends with another such call, which results in a call to + /// `fatalError()`. + /// + /// If you cancel the task this iterator is running in while `next()` is + /// awaiting a value, the `BufferedStream` terminates. In this case, + /// `next()` may return `nil` immediately, or else return `nil` on + /// subsequent calls. + @inlinable + internal mutating func next() async throws -> Element? { + switch self.implementation { + case .backpressured(let backing): + return try await backing.storage.next() + } + } + } + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + @inlinable + internal func makeAsyncIterator() -> Iterator { + switch self.implementation { + case .backpressured(let backing): + return Iterator(implementation: .backpressured(.init(storage: backing.storage))) + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BufferedStream: Sendable where Element: Sendable {} + +@usableFromInline +internal struct _ManagedCriticalState: @unchecked Sendable { + @usableFromInline + let lock: LockedValueBox + + @inlinable + internal init(_ initial: State) { + self.lock = .init(initial) + } + + @inlinable + internal func withCriticalRegion( + _ critical: (inout State) throws -> R + ) rethrows -> R { + try self.lock.withLockedValue(critical) + } +} + +@usableFromInline +internal struct AlreadyFinishedError: Error { + @inlinable + init() {} +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BufferedStream { + /// A mechanism to interface between producer code and an asynchronous stream. + /// + /// Use this source to provide elements to the stream by calling one of the `write` methods, then terminate the stream normally + /// by calling the `finish()` method. You can also use the source's `finish(throwing:)` method to terminate the stream by + /// throwing an error. + @usableFromInline + internal struct Source: Sendable { + /// A strategy that handles the backpressure of the asynchronous stream. + @usableFromInline + internal struct BackPressureStrategy: Sendable { + /// When the high watermark is reached producers will be suspended. All producers will be resumed again once + /// the low watermark is reached. + @inlinable + internal static func watermark(low: Int, high: Int) -> BackPressureStrategy { + BackPressureStrategy( + internalBackPressureStrategy: .watermark(.init(low: low, high: high)) + ) + } + + @inlinable + init(internalBackPressureStrategy: _InternalBackPressureStrategy) { + self._internalBackPressureStrategy = internalBackPressureStrategy + } + + @usableFromInline + let _internalBackPressureStrategy: _InternalBackPressureStrategy + } + + /// A type that indicates the result of writing elements to the source. + @frozen + @usableFromInline + internal enum WriteResult: Sendable { + /// A token that is returned when the asynchronous stream's backpressure strategy indicated that production should + /// be suspended. Use this token to enqueue a callback by calling the ``enqueueCallback(_:)`` method. + @usableFromInline + internal struct CallbackToken: Sendable { + @usableFromInline + let id: UInt + @usableFromInline + init(id: UInt) { + self.id = id + } + } + + /// Indicates that more elements should be produced and written to the source. + case produceMore + + /// Indicates that a callback should be enqueued. + /// + /// The associated token should be passed to the ``enqueueCallback(_:)`` method. + case enqueueCallback(CallbackToken) + } + + /// Backing class for the source used to hook a deinit. + @usableFromInline + final class _Backing: Sendable { + @usableFromInline + let storage: _BackPressuredStorage + + @inlinable + init(storage: _BackPressuredStorage) { + self.storage = storage + } + + deinit { + self.storage.sourceDeinitialized() + } + } + + /// A callback to invoke when the stream finished. + /// + /// The stream finishes and calls this closure in the following cases: + /// - No iterator was created and the sequence was deinited + /// - An iterator was created and deinited + /// - After ``finish(throwing:)`` was called and all elements have been consumed + /// - The consuming task got cancelled + @inlinable + internal var onTermination: (@Sendable () -> Void)? { + set { + self._backing.storage.onTermination = newValue + } + get { + self._backing.storage.onTermination + } + } + + @usableFromInline + var _backing: _Backing + + @inlinable + internal init(storage: _BackPressuredStorage) { + self._backing = .init(storage: storage) + } + + /// Writes new elements to the asynchronous stream. + /// + /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the asynchronous stream already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter sequence: The elements to write to the asynchronous stream. + /// - Returns: The result that indicates if more elements should be produced at this time. + @inlinable + internal func write(contentsOf sequence: S) throws -> WriteResult + where Element == S.Element, S: Sequence { + try self._backing.storage.write(contentsOf: sequence) + } + + /// Write the element to the asynchronous stream. + /// + /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the + /// provided element. If the asynchronous stream already terminated then this method will throw an error + /// indicating the failure. + /// + /// - Parameter element: The element to write to the asynchronous stream. + /// - Returns: The result that indicates if more elements should be produced at this time. + @inlinable + internal func write(_ element: Element) throws -> WriteResult { + try self._backing.storage.write(contentsOf: CollectionOfOne(element)) + } + + /// Enqueues a callback that will be invoked once more elements should be produced. + /// + /// Call this method after ``write(contentsOf:)`` or ``write(:)`` returned ``WriteResult/enqueueCallback(_:)``. + /// + /// - Important: Enqueueing the same token multiple times is not allowed. + /// + /// - Parameters: + /// - callbackToken: The callback token. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. + @inlinable + internal func enqueueCallback( + callbackToken: WriteResult.CallbackToken, + onProduceMore: @escaping @Sendable (Result) -> Void + ) { + self._backing.storage.enqueueProducer( + callbackToken: callbackToken, + onProduceMore: onProduceMore + ) + } + + /// Cancel an enqueued callback. + /// + /// Call this method to cancel a callback enqueued by the ``enqueueCallback(callbackToken:onProduceMore:)`` method. + /// + /// - Note: This methods supports being called before ``enqueueCallback(callbackToken:onProduceMore:)`` is called and + /// will mark the passed `callbackToken` as cancelled. + /// + /// - Parameter callbackToken: The callback token. + @inlinable + internal func cancelCallback(callbackToken: WriteResult.CallbackToken) { + self._backing.storage.cancelProducer(callbackToken: callbackToken) + } + + /// Write new elements to the asynchronous stream and provide a callback which will be invoked once more elements should be produced. + /// + /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the asynchronous stream already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - sequence: The elements to write to the asynchronous stream. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``write(contentsOf:onProduceMore:)``. + @inlinable + internal func write( + contentsOf sequence: S, + onProduceMore: @escaping @Sendable (Result) -> Void + ) where Element == S.Element, S: Sequence { + do { + let writeResult = try self.write(contentsOf: sequence) + + switch writeResult { + case .produceMore: + onProduceMore(Result.success(())) + + case .enqueueCallback(let callbackToken): + self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) + } + } catch { + onProduceMore(.failure(error)) + } + } + + /// Writes the element to the asynchronous stream. + /// + /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the + /// provided element. If the asynchronous stream already terminated then `onProduceMore` will be invoked with + /// a `Result.failure`. + /// + /// - Parameters: + /// - sequence: The element to write to the asynchronous stream. + /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be + /// invoked during the call to ``write(_:onProduceMore:)``. + @inlinable + internal func write( + _ element: Element, + onProduceMore: @escaping @Sendable (Result) -> Void + ) { + self.write(contentsOf: CollectionOfOne(element), onProduceMore: onProduceMore) + } + + /// Write new elements to the asynchronous stream. + /// + /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the + /// first element of the provided sequence. If the asynchronous stream already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The elements to write to the asynchronous stream. + @inlinable + internal func write(contentsOf sequence: S) async throws + where Element == S.Element, S: Sequence { + let writeResult = try { try self.write(contentsOf: sequence) }() + + switch writeResult { + case .produceMore: + return + + case .enqueueCallback(let callbackToken): + try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + self.enqueueCallback( + callbackToken: callbackToken, + onProduceMore: { result in + switch result { + case .success(): + continuation.resume(returning: ()) + case .failure(let error): + continuation.resume(throwing: error) + } + } + ) + } + } onCancel: { + self.cancelCallback(callbackToken: callbackToken) + } + } + } + + /// Write new element to the asynchronous stream. + /// + /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the + /// provided element. If the asynchronous stream already terminated then this method will throw an error + /// indicating the failure. + /// + /// This method returns once more elements should be produced. + /// + /// - Parameters: + /// - sequence: The element to write to the asynchronous stream. + @inlinable + internal func write(_ element: Element) async throws { + try await self.write(contentsOf: CollectionOfOne(element)) + } + + /// Write the elements of the asynchronous sequence to the asynchronous stream. + /// + /// This method returns once the provided asynchronous sequence or the the asynchronous stream finished. + /// + /// - Important: This method does not finish the source if consuming the upstream sequence terminated. + /// + /// - Parameters: + /// - sequence: The elements to write to the asynchronous stream. + @inlinable + internal func write(contentsOf sequence: S) async throws + where Element == S.Element, S: AsyncSequence { + for try await element in sequence { + try await self.write(contentsOf: CollectionOfOne(element)) + } + } + + /// Indicates that the production terminated. + /// + /// After all buffered elements are consumed the next iteration point will return `nil` or throw an error. + /// + /// Calling this function more than once has no effect. After calling finish, the stream enters a terminal state and doesn't accept + /// new elements. + /// + /// - Parameters: + /// - error: The error to throw, or `nil`, to finish normally. + @inlinable + internal func finish(throwing error: Error?) { + self._backing.storage.finish(error) + } + } + + /// Initializes a new ``BufferedStream`` and an ``BufferedStream/Source``. + /// + /// - Parameters: + /// - elementType: The element type of the stream. + /// - failureType: The failure type of the stream. + /// - backPressureStrategy: The backpressure strategy that the stream should use. + /// - Returns: A tuple containing the stream and its source. The source should be passed to the + /// producer while the stream should be passed to the consumer. + @inlinable + internal static func makeStream( + of elementType: Element.Type = Element.self, + throwing failureType: Error.Type = Error.self, + backPressureStrategy: Source.BackPressureStrategy + ) -> (`Self`, Source) where Error == Error { + let storage = _BackPressuredStorage( + backPressureStrategy: backPressureStrategy._internalBackPressureStrategy + ) + let source = Source(storage: storage) + + return (.init(storage: storage), source) + } + + @inlinable + init(storage: _BackPressuredStorage) { + self.implementation = .backpressured(.init(storage: storage)) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BufferedStream { + @usableFromInline + struct _WatermarkBackPressureStrategy: Sendable { + /// The low watermark where demand should start. + @usableFromInline + let _low: Int + /// The high watermark where demand should be stopped. + @usableFromInline + let _high: Int + + /// Initializes a new ``_WatermarkBackPressureStrategy``. + /// + /// - Parameters: + /// - low: The low watermark where demand should start. + /// - high: The high watermark where demand should be stopped. + @inlinable + init(low: Int, high: Int) { + precondition(low <= high) + self._low = low + self._high = high + } + + @inlinable + func didYield(bufferDepth: Int) -> Bool { + // We are demanding more until we reach the high watermark + return bufferDepth < self._high + } + + @inlinable + func didConsume(bufferDepth: Int) -> Bool { + // We start demanding again once we are below the low watermark + return bufferDepth < self._low + } + } + + @usableFromInline + enum _InternalBackPressureStrategy: Sendable { + case watermark(_WatermarkBackPressureStrategy) + + @inlinable + mutating func didYield(bufferDepth: Int) -> Bool { + switch self { + case .watermark(let strategy): + return strategy.didYield(bufferDepth: bufferDepth) + } + } + + @inlinable + mutating func didConsume(bufferDepth: Int) -> Bool { + switch self { + case .watermark(let strategy): + return strategy.didConsume(bufferDepth: bufferDepth) + } + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BufferedStream { + // We are unchecked Sendable since we are protecting our state with a lock. + @usableFromInline + final class _BackPressuredStorage: Sendable { + /// The state machine + @usableFromInline + let _stateMachine: _ManagedCriticalState<_StateMachine> + + @usableFromInline + var onTermination: (@Sendable () -> Void)? { + set { + self._stateMachine.withCriticalRegion { + $0._onTermination = newValue + } + } + get { + self._stateMachine.withCriticalRegion { + $0._onTermination + } + } + } + + @inlinable + init( + backPressureStrategy: _InternalBackPressureStrategy + ) { + self._stateMachine = .init(.init(backPressureStrategy: backPressureStrategy)) + } + + @inlinable + func sequenceDeinitialized() { + let action = self._stateMachine.withCriticalRegion { + $0.sequenceDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + producerContinuation(.failure(AlreadyFinishedError())) + } + onTermination?() + + case .none: + break + } + } + + @inlinable + func iteratorInitialized() { + self._stateMachine.withCriticalRegion { + $0.iteratorInitialized() + } + } + + @inlinable + func iteratorDeinitialized() { + let action = self._stateMachine.withCriticalRegion { + $0.iteratorDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): + for producerContinuation in producerContinuations { + producerContinuation(.failure(AlreadyFinishedError())) + } + onTermination?() + + case .none: + break + } + } + + @inlinable + func sourceDeinitialized() { + let action = self._stateMachine.withCriticalRegion { + $0.sourceDeinitialized() + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .failProducersAndCallOnTermination( + let consumer, + let producerContinuations, + let onTermination + ): + consumer?.resume(returning: nil) + for producerContinuation in producerContinuations { + producerContinuation(.failure(AlreadyFinishedError())) + } + onTermination?() + + case .failProducers(let producerContinuations): + for producerContinuation in producerContinuations { + producerContinuation(.failure(AlreadyFinishedError())) + } + + case .none: + break + } + } + + @inlinable + func write( + contentsOf sequence: some Sequence + ) throws -> Source.WriteResult { + let action = self._stateMachine.withCriticalRegion { + return $0.write(sequence) + } + + switch action { + case .returnProduceMore: + return .produceMore + + case .returnEnqueue(let callbackToken): + return .enqueueCallback(callbackToken) + + case .resumeConsumerAndReturnProduceMore(let continuation, let element): + continuation.resume(returning: element) + return .produceMore + + case .resumeConsumerAndReturnEnqueue(let continuation, let element, let callbackToken): + continuation.resume(returning: element) + return .enqueueCallback(callbackToken) + + case .throwFinishedError: + throw AlreadyFinishedError() + } + } + + @inlinable + func enqueueProducer( + callbackToken: Source.WriteResult.CallbackToken, + onProduceMore: @escaping @Sendable (Result) -> Void + ) { + let action = self._stateMachine.withCriticalRegion { + $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) + } + + switch action { + case .resumeProducer(let onProduceMore): + onProduceMore(Result.success(())) + + case .resumeProducerWithError(let onProduceMore, let error): + onProduceMore(Result.failure(error)) + + case .none: + break + } + } + + @inlinable + func cancelProducer(callbackToken: Source.WriteResult.CallbackToken) { + let action = self._stateMachine.withCriticalRegion { + $0.cancelProducer(callbackToken: callbackToken) + } + + switch action { + case .resumeProducerWithCancellationError(let onProduceMore): + onProduceMore(Result.failure(CancellationError())) + + case .none: + break + } + } + + @inlinable + func finish(_ failure: Error?) { + let action = self._stateMachine.withCriticalRegion { + $0.finish(failure) + } + + switch action { + case .callOnTermination(let onTermination): + onTermination?() + + case .resumeConsumerAndCallOnTermination( + let consumerContinuation, + let failure, + let onTermination + ): + switch failure { + case .some(let error): + consumerContinuation.resume(throwing: error) + case .none: + consumerContinuation.resume(returning: nil) + } + + onTermination?() + + case .resumeProducers(let producerContinuations): + for producerContinuation in producerContinuations { + producerContinuation(.failure(AlreadyFinishedError())) + } + + case .none: + break + } + } + + @inlinable + func next() async throws -> Element? { + let action = self._stateMachine.withCriticalRegion { + $0.next() + } + + switch action { + case .returnElement(let element): + return element + + case .returnElementAndResumeProducers(let element, let producerContinuations): + for producerContinuation in producerContinuations { + producerContinuation(Result.success(())) + } + + return element + + case .returnErrorAndCallOnTermination(let failure, let onTermination): + onTermination?() + switch failure { + case .some(let error): + throw error + + case .none: + return nil + } + + case .returnNil: + return nil + + case .suspendTask: + return try await self.suspendNext() + } + } + + @inlinable + func suspendNext() async throws -> Element? { + return try await withTaskCancellationHandler { + return try await withCheckedThrowingContinuation { continuation in + let action = self._stateMachine.withCriticalRegion { + $0.suspendNext(continuation: continuation) + } + + switch action { + case .resumeConsumerWithElement(let continuation, let element): + continuation.resume(returning: element) + + case .resumeConsumerWithElementAndProducers( + let continuation, + let element, + let producerContinuations + ): + continuation.resume(returning: element) + for producerContinuation in producerContinuations { + producerContinuation(Result.success(())) + } + + case .resumeConsumerWithErrorAndCallOnTermination( + let continuation, + let failure, + let onTermination + ): + switch failure { + case .some(let error): + continuation.resume(throwing: error) + + case .none: + continuation.resume(returning: nil) + } + onTermination?() + + case .resumeConsumerWithNil(let continuation): + continuation.resume(returning: nil) + + case .none: + break + } + } + } onCancel: { + let action = self._stateMachine.withCriticalRegion { + $0.cancelNext() + } + + switch action { + case .resumeConsumerWithCancellationErrorAndCallOnTermination( + let continuation, + let onTermination + ): + continuation.resume(throwing: CancellationError()) + onTermination?() + + case .failProducersAndCallOnTermination( + let producerContinuations, + let onTermination + ): + for producerContinuation in producerContinuations { + producerContinuation(.failure(AlreadyFinishedError())) + } + onTermination?() + + case .none: + break + } + } + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BufferedStream { + /// The state machine of the backpressured async stream. + @usableFromInline + struct _StateMachine { + @usableFromInline + enum _State { + @usableFromInline + struct Initial { + /// The backpressure strategy. + @usableFromInline + var backPressureStrategy: _InternalBackPressureStrategy + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + /// The onTermination callback. + @usableFromInline + var onTermination: (@Sendable () -> Void)? + + @inlinable + init( + backPressureStrategy: _InternalBackPressureStrategy, + iteratorInitialized: Bool, + onTermination: (@Sendable () -> Void)? = nil + ) { + self.backPressureStrategy = backPressureStrategy + self.iteratorInitialized = iteratorInitialized + self.onTermination = onTermination + } + } + + @usableFromInline + struct Streaming { + /// The backpressure strategy. + @usableFromInline + var backPressureStrategy: _InternalBackPressureStrategy + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + /// The onTermination callback. + @usableFromInline + var onTermination: (@Sendable () -> Void)? + /// The buffer of elements. + @usableFromInline + var buffer: Deque + /// The optional consumer continuation. + @usableFromInline + var consumerContinuation: CheckedContinuation? + /// The producer continuations. + @usableFromInline + var producerContinuations: Deque<(UInt, (Result) -> Void)> + /// The producers that have been cancelled. + @usableFromInline + var cancelledAsyncProducers: Deque + /// Indicates if we currently have outstanding demand. + @usableFromInline + var hasOutstandingDemand: Bool + + @inlinable + init( + backPressureStrategy: _InternalBackPressureStrategy, + iteratorInitialized: Bool, + onTermination: (@Sendable () -> Void)? = nil, + buffer: Deque, + consumerContinuation: CheckedContinuation? = nil, + producerContinuations: Deque<(UInt, (Result) -> Void)>, + cancelledAsyncProducers: Deque, + hasOutstandingDemand: Bool + ) { + self.backPressureStrategy = backPressureStrategy + self.iteratorInitialized = iteratorInitialized + self.onTermination = onTermination + self.buffer = buffer + self.consumerContinuation = consumerContinuation + self.producerContinuations = producerContinuations + self.cancelledAsyncProducers = cancelledAsyncProducers + self.hasOutstandingDemand = hasOutstandingDemand + } + } + + @usableFromInline + struct SourceFinished { + /// Indicates if the iterator was initialized. + @usableFromInline + var iteratorInitialized: Bool + /// The buffer of elements. + @usableFromInline + var buffer: Deque + /// The failure that should be thrown after the last element has been consumed. + @usableFromInline + var failure: Error? + /// The onTermination callback. + @usableFromInline + var onTermination: (@Sendable () -> Void)? + + @inlinable + init( + iteratorInitialized: Bool, + buffer: Deque, + failure: Error? = nil, + onTermination: (@Sendable () -> Void)? + ) { + self.iteratorInitialized = iteratorInitialized + self.buffer = buffer + self.failure = failure + self.onTermination = onTermination + } + } + + case initial(Initial) + /// The state once either any element was yielded or `next()` was called. + case streaming(Streaming) + /// The state once the underlying source signalled that it is finished. + case sourceFinished(SourceFinished) + + /// The state once there can be no outstanding demand. This can happen if: + /// 1. The iterator was deinited + /// 2. The underlying source finished and all buffered elements have been consumed + case finished(iteratorInitialized: Bool) + + /// An intermediate state to avoid CoWs. + case modify + } + + /// The state machine's current state. + @usableFromInline + var _state: _State + + // The ID used for the next CallbackToken. + @usableFromInline + var nextCallbackTokenID: UInt = 0 + + @inlinable + var _onTermination: (@Sendable () -> Void)? { + set { + switch self._state { + case .initial(var initial): + initial.onTermination = newValue + self._state = .initial(initial) + + case .streaming(var streaming): + streaming.onTermination = newValue + self._state = .streaming(streaming) + + case .sourceFinished(var sourceFinished): + sourceFinished.onTermination = newValue + self._state = .sourceFinished(sourceFinished) + + case .finished: + break + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + get { + switch self._state { + case .initial(let initial): + return initial.onTermination + + case .streaming(let streaming): + return streaming.onTermination + + case .sourceFinished(let sourceFinished): + return sourceFinished.onTermination + + case .finished: + return nil + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + } + + /// Initializes a new `StateMachine`. + /// + /// We are passing and holding the back-pressure strategy here because + /// it is a customizable extension of the state machine. + /// + /// - Parameter backPressureStrategy: The back-pressure strategy. + @inlinable + init( + backPressureStrategy: _InternalBackPressureStrategy + ) { + self._state = .initial( + .init( + backPressureStrategy: backPressureStrategy, + iteratorInitialized: false + ) + ) + } + + /// Generates the next callback token. + @inlinable + mutating func nextCallbackToken() -> Source.WriteResult.CallbackToken { + let id = self.nextCallbackTokenID + self.nextCallbackTokenID += 1 + return .init(id: id) + } + + /// Actions returned by `sequenceDeinitialized()`. + @usableFromInline + enum SequenceDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((@Sendable () -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + [(Result) -> Void], + (@Sendable () -> Void)? + ) + } + + @inlinable + mutating func sequenceDeinitialized() -> SequenceDeinitializedAction? { + switch self._state { + case .initial(let initial): + if initial.iteratorInitialized { + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + return .none + } else { + // No iterator was created so we can transition to finished right away. + self._state = .finished(iteratorInitialized: false) + + return .callOnTermination(initial.onTermination) + } + + case .streaming(let streaming): + if streaming.iteratorInitialized { + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + return .none + } else { + // No iterator was created so we can transition to finished right away. + self._state = .finished(iteratorInitialized: false) + + return .failProducersAndCallOnTermination( + Array(streaming.producerContinuations.map { $0.1 }), + streaming.onTermination + ) + } + + case .sourceFinished(let sourceFinished): + if sourceFinished.iteratorInitialized { + // An iterator was created and we deinited the sequence. + // This is an expected pattern and we just continue on normal. + return .none + } else { + // No iterator was created so we can transition to finished right away. + self._state = .finished(iteratorInitialized: false) + + return .callOnTermination(sourceFinished.onTermination) + } + + case .finished: + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + return .none + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + @inlinable + mutating func iteratorInitialized() { + switch self._state { + case .initial(var initial): + if initial.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + initial.iteratorInitialized = true + self._state = .initial(initial) + } + + case .streaming(var streaming): + if streaming.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + streaming.iteratorInitialized = true + self._state = .streaming(streaming) + } + + case .sourceFinished(var sourceFinished): + if sourceFinished.iteratorInitialized { + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + } else { + // The first and only iterator was initialized. + sourceFinished.iteratorInitialized = true + self._state = .sourceFinished(sourceFinished) + } + + case .finished(iteratorInitialized: true): + // Our sequence is a unicast sequence and does not support multiple AsyncIterator's + fatalError("Only a single AsyncIterator can be created") + + case .finished(iteratorInitialized: false): + // It is strange that an iterator is created after we are finished + // but it can definitely happen, e.g. + // Sequence.init -> source.finish -> sequence.makeAsyncIterator + self._state = .finished(iteratorInitialized: true) + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `iteratorDeinitialized()`. + @usableFromInline + enum IteratorDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((@Sendable () -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + [(Result) -> Void], + (@Sendable () -> Void)? + ) + } + + @inlinable + mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { + switch self._state { + case .initial(let initial): + if initial.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self._state = .finished(iteratorInitialized: true) + return .callOnTermination(initial.onTermination) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("AsyncStream internal inconsistency") + } + + case .streaming(let streaming): + if streaming.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self._state = .finished(iteratorInitialized: true) + + return .failProducersAndCallOnTermination( + Array(streaming.producerContinuations.map { $0.1 }), + streaming.onTermination + ) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("AsyncStream internal inconsistency") + } + + case .sourceFinished(let sourceFinished): + if sourceFinished.iteratorInitialized { + // An iterator was created and deinited. Since we only support + // a single iterator we can now transition to finish. + self._state = .finished(iteratorInitialized: true) + return .callOnTermination(sourceFinished.onTermination) + } else { + // An iterator needs to be initialized before it can be deinitialized. + fatalError("AsyncStream internal inconsistency") + } + + case .finished: + // We are already finished so there is nothing left to clean up. + // This is just the references dropping afterwards. + return .none + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `sourceDeinitialized()`. + @usableFromInline + enum SourceDeinitializedAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((() -> Void)?) + /// Indicates that all producers should be failed and `onTermination` should be called. + case failProducersAndCallOnTermination( + CheckedContinuation?, + [(Result) -> Void], + (@Sendable () -> Void)? + ) + /// Indicates that all producers should be failed. + case failProducers([(Result) -> Void]) + } + + @inlinable + mutating func sourceDeinitialized() -> SourceDeinitializedAction? { + switch self._state { + case .initial(let initial): + // The source got deinited before anything was written + self._state = .finished(iteratorInitialized: initial.iteratorInitialized) + return .callOnTermination(initial.onTermination) + + case .streaming(let streaming): + if streaming.buffer.isEmpty { + // We can transition to finished right away since the buffer is empty now + self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) + + return .failProducersAndCallOnTermination( + streaming.consumerContinuation, + Array(streaming.producerContinuations.map { $0.1 }), + streaming.onTermination + ) + } else { + // The continuation must be `nil` if the buffer has elements + precondition(streaming.consumerContinuation == nil) + + self._state = .sourceFinished( + .init( + iteratorInitialized: streaming.iteratorInitialized, + buffer: streaming.buffer, + failure: nil, + onTermination: streaming.onTermination + ) + ) + + return .failProducers( + Array(streaming.producerContinuations.map { $0.1 }) + ) + } + + case .sourceFinished, .finished: + // This is normal and we just have to tolerate it + return .none + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `write()`. + @usableFromInline + enum WriteAction { + /// Indicates that the producer should be notified to produce more. + case returnProduceMore + /// Indicates that the producer should be suspended to stop producing. + case returnEnqueue( + callbackToken: Source.WriteResult.CallbackToken + ) + /// Indicates that the consumer should be resumed and the producer should be notified to produce more. + case resumeConsumerAndReturnProduceMore( + continuation: CheckedContinuation, + element: Element + ) + /// Indicates that the consumer should be resumed and the producer should be suspended. + case resumeConsumerAndReturnEnqueue( + continuation: CheckedContinuation, + element: Element, + callbackToken: Source.WriteResult.CallbackToken + ) + /// Indicates that the producer has been finished. + case throwFinishedError + + @inlinable + init( + callbackToken: Source.WriteResult.CallbackToken?, + continuationAndElement: (CheckedContinuation, Element)? = nil + ) { + switch (callbackToken, continuationAndElement) { + case (.none, .none): + self = .returnProduceMore + + case (.some(let callbackToken), .none): + self = .returnEnqueue(callbackToken: callbackToken) + + case (.none, .some((let continuation, let element))): + self = .resumeConsumerAndReturnProduceMore( + continuation: continuation, + element: element + ) + + case (.some(let callbackToken), .some((let continuation, let element))): + self = .resumeConsumerAndReturnEnqueue( + continuation: continuation, + element: element, + callbackToken: callbackToken + ) + } + } + } + + @inlinable + mutating func write(_ sequence: some Sequence) -> WriteAction { + switch self._state { + case .initial(var initial): + var buffer = Deque() + buffer.append(contentsOf: sequence) + + let shouldProduceMore = initial.backPressureStrategy.didYield( + bufferDepth: buffer.count + ) + let callbackToken = shouldProduceMore ? nil : self.nextCallbackToken() + + self._state = .streaming( + .init( + backPressureStrategy: initial.backPressureStrategy, + iteratorInitialized: initial.iteratorInitialized, + onTermination: initial.onTermination, + buffer: buffer, + consumerContinuation: nil, + producerContinuations: .init(), + cancelledAsyncProducers: .init(), + hasOutstandingDemand: shouldProduceMore + ) + ) + + return .init(callbackToken: callbackToken) + + case .streaming(var streaming): + self._state = .modify + + streaming.buffer.append(contentsOf: sequence) + + // We have an element and can resume the continuation + let shouldProduceMore = streaming.backPressureStrategy.didYield( + bufferDepth: streaming.buffer.count + ) + streaming.hasOutstandingDemand = shouldProduceMore + let callbackToken = shouldProduceMore ? nil : self.nextCallbackToken() + + if let consumerContinuation = streaming.consumerContinuation { + guard let element = streaming.buffer.popFirst() else { + // We got a yield of an empty sequence. We just tolerate this. + self._state = .streaming(streaming) + + return .init(callbackToken: callbackToken) + } + + // We got a consumer continuation and an element. We can resume the consumer now + streaming.consumerContinuation = nil + self._state = .streaming(streaming) + return .init( + callbackToken: callbackToken, + continuationAndElement: (consumerContinuation, element) + ) + } else { + // We don't have a suspended consumer so we just buffer the elements + self._state = .streaming(streaming) + return .init( + callbackToken: callbackToken + ) + } + + case .sourceFinished, .finished: + // If the source has finished we are dropping the elements. + return .throwFinishedError + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `enqueueProducer()`. + @usableFromInline + enum EnqueueProducerAction { + /// Indicates that the producer should be notified to produce more. + case resumeProducer((Result) -> Void) + /// Indicates that the producer should be notified about an error. + case resumeProducerWithError((Result) -> Void, Error) + } + + @inlinable + mutating func enqueueProducer( + callbackToken: Source.WriteResult.CallbackToken, + onProduceMore: @Sendable @escaping (Result) -> Void + ) -> EnqueueProducerAction? { + switch self._state { + case .initial: + // We need to transition to streaming before we can suspend + // This is enforced because the CallbackToken has no internal init so + // one must create it by calling `write` first. + fatalError("AsyncStream internal inconsistency") + + case .streaming(var streaming): + if let index = streaming.cancelledAsyncProducers.firstIndex(of: callbackToken.id) { + // Our producer got marked as cancelled. + self._state = .modify + streaming.cancelledAsyncProducers.remove(at: index) + self._state = .streaming(streaming) + + return .resumeProducerWithError(onProduceMore, CancellationError()) + } else if streaming.hasOutstandingDemand { + // We hit an edge case here where we wrote but the consuming thread got interleaved + return .resumeProducer(onProduceMore) + } else { + self._state = .modify + streaming.producerContinuations.append((callbackToken.id, onProduceMore)) + + self._state = .streaming(streaming) + return .none + } + + case .sourceFinished, .finished: + // Since we are unlocking between yielding and suspending the yield + // It can happen that the source got finished or the consumption fully finishes. + return .resumeProducerWithError(onProduceMore, AlreadyFinishedError()) + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `cancelProducer()`. + @usableFromInline + enum CancelProducerAction { + /// Indicates that the producer should be notified about cancellation. + case resumeProducerWithCancellationError((Result) -> Void) + } + + @inlinable + mutating func cancelProducer( + callbackToken: Source.WriteResult.CallbackToken + ) -> CancelProducerAction? { + switch self._state { + case .initial: + // We need to transition to streaming before we can suspend + fatalError("AsyncStream internal inconsistency") + + case .streaming(var streaming): + if let index = streaming.producerContinuations.firstIndex(where: { + $0.0 == callbackToken.id + }) { + // We have an enqueued producer that we need to resume now + self._state = .modify + let continuation = streaming.producerContinuations.remove(at: index).1 + self._state = .streaming(streaming) + + return .resumeProducerWithCancellationError(continuation) + } else { + // The task that yields was cancelled before yielding so the cancellation handler + // got invoked right away + self._state = .modify + streaming.cancelledAsyncProducers.append(callbackToken.id) + self._state = .streaming(streaming) + + return .none + } + + case .sourceFinished, .finished: + // Since we are unlocking between yielding and suspending the yield + // It can happen that the source got finished or the consumption fully finishes. + return .none + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `finish()`. + @usableFromInline + enum FinishAction { + /// Indicates that `onTermination` should be called. + case callOnTermination((() -> Void)?) + /// Indicates that the consumer should be resumed with the failure, the producers + /// should be resumed with an error and `onTermination` should be called. + case resumeConsumerAndCallOnTermination( + consumerContinuation: CheckedContinuation, + failure: Error?, + onTermination: (() -> Void)? + ) + /// Indicates that the producers should be resumed with an error. + case resumeProducers( + producerContinuations: [(Result) -> Void] + ) + } + + @inlinable + mutating func finish(_ failure: Error?) -> FinishAction? { + switch self._state { + case .initial(let initial): + // Nothing was yielded nor did anybody call next + // This means we can transition to sourceFinished and store the failure + self._state = .sourceFinished( + .init( + iteratorInitialized: initial.iteratorInitialized, + buffer: .init(), + failure: failure, + onTermination: initial.onTermination + ) + ) + + return .callOnTermination(initial.onTermination) + + case .streaming(let streaming): + if let consumerContinuation = streaming.consumerContinuation { + // We have a continuation, this means our buffer must be empty + // Furthermore, we can now transition to finished + // and resume the continuation with the failure + precondition(streaming.buffer.isEmpty, "Expected an empty buffer") + precondition( + streaming.producerContinuations.isEmpty, + "Expected no suspended producers" + ) + + self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) + + return .resumeConsumerAndCallOnTermination( + consumerContinuation: consumerContinuation, + failure: failure, + onTermination: streaming.onTermination + ) + } else { + self._state = .sourceFinished( + .init( + iteratorInitialized: streaming.iteratorInitialized, + buffer: streaming.buffer, + failure: failure, + onTermination: streaming.onTermination + ) + ) + + return .resumeProducers( + producerContinuations: Array(streaming.producerContinuations.map { $0.1 }) + ) + } + + case .sourceFinished, .finished: + // If the source has finished, finishing again has no effect. + return .none + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `next()`. + @usableFromInline + enum NextAction { + /// Indicates that the element should be returned to the caller. + case returnElement(Element) + /// Indicates that the element should be returned to the caller and that all producers should be called. + case returnElementAndResumeProducers(Element, [(Result) -> Void]) + /// Indicates that the `Error` should be returned to the caller and that `onTermination` should be called. + case returnErrorAndCallOnTermination(Error?, (() -> Void)?) + /// Indicates that the `nil` should be returned to the caller. + case returnNil + /// Indicates that the `Task` of the caller should be suspended. + case suspendTask + } + + @inlinable + mutating func next() -> NextAction { + switch self._state { + case .initial(let initial): + // We are not interacting with the back-pressure strategy here because + // we are doing this inside `next(:)` + self._state = .streaming( + .init( + backPressureStrategy: initial.backPressureStrategy, + iteratorInitialized: initial.iteratorInitialized, + onTermination: initial.onTermination, + buffer: Deque(), + consumerContinuation: nil, + producerContinuations: .init(), + cancelledAsyncProducers: .init(), + hasOutstandingDemand: false + ) + ) + + return .suspendTask + case .streaming(var streaming): + guard streaming.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError("AsyncStream internal inconsistency") + } + + self._state = .modify + + if let element = streaming.buffer.popFirst() { + // We have an element to fulfil the demand right away. + let shouldProduceMore = streaming.backPressureStrategy.didConsume( + bufferDepth: streaming.buffer.count + ) + streaming.hasOutstandingDemand = shouldProduceMore + + if shouldProduceMore { + // There is demand and we have to resume our producers + let producers = Array(streaming.producerContinuations.map { $0.1 }) + streaming.producerContinuations.removeAll() + self._state = .streaming(streaming) + return .returnElementAndResumeProducers(element, producers) + } else { + // We don't have any new demand, so we can just return the element. + self._state = .streaming(streaming) + return .returnElement(element) + } + } else { + // There is nothing in the buffer to fulfil the demand so we need to suspend. + // We are not interacting with the back-pressure strategy here because + // we are doing this inside `suspendNext` + self._state = .streaming(streaming) + + return .suspendTask + } + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + self._state = .modify + + if let element = sourceFinished.buffer.popFirst() { + self._state = .sourceFinished(sourceFinished) + + return .returnElement(element) + } else { + // We are returning the queued failure now and can transition to finished + self._state = .finished(iteratorInitialized: sourceFinished.iteratorInitialized) + + return .returnErrorAndCallOnTermination( + sourceFinished.failure, + sourceFinished.onTermination + ) + } + + case .finished: + return .returnNil + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `suspendNext()`. + @usableFromInline + enum SuspendNextAction { + /// Indicates that the consumer should be resumed. + case resumeConsumerWithElement(CheckedContinuation, Element) + /// Indicates that the consumer and all producers should be resumed. + case resumeConsumerWithElementAndProducers( + CheckedContinuation, + Element, + [(Result) -> Void] + ) + /// Indicates that the consumer should be resumed with the failure and that `onTermination` should be called. + case resumeConsumerWithErrorAndCallOnTermination( + CheckedContinuation, + Error?, + (() -> Void)? + ) + /// Indicates that the consumer should be resumed with `nil`. + case resumeConsumerWithNil(CheckedContinuation) + } + + @inlinable + mutating func suspendNext( + continuation: CheckedContinuation + ) -> SuspendNextAction? { + switch self._state { + case .initial: + // We need to transition to streaming before we can suspend + preconditionFailure("AsyncStream internal inconsistency") + + case .streaming(var streaming): + guard streaming.consumerContinuation == nil else { + // We have multiple AsyncIterators iterating the sequence + fatalError( + "This should never happen since we only allow a single Iterator to be created" + ) + } + + self._state = .modify + + // We have to check here again since we might have a producer interleave next and suspendNext + if let element = streaming.buffer.popFirst() { + // We have an element to fulfil the demand right away. + + let shouldProduceMore = streaming.backPressureStrategy.didConsume( + bufferDepth: streaming.buffer.count + ) + streaming.hasOutstandingDemand = shouldProduceMore + + if shouldProduceMore { + // There is demand and we have to resume our producers + let producers = Array(streaming.producerContinuations.map { $0.1 }) + streaming.producerContinuations.removeAll() + self._state = .streaming(streaming) + return .resumeConsumerWithElementAndProducers( + continuation, + element, + producers + ) + } else { + // We don't have any new demand, so we can just return the element. + self._state = .streaming(streaming) + return .resumeConsumerWithElement(continuation, element) + } + } else { + // There is nothing in the buffer to fulfil the demand so we to store the continuation. + streaming.consumerContinuation = continuation + self._state = .streaming(streaming) + + return .none + } + + case .sourceFinished(var sourceFinished): + // Check if we have an element left in the buffer and return it + self._state = .modify + + if let element = sourceFinished.buffer.popFirst() { + self._state = .sourceFinished(sourceFinished) + + return .resumeConsumerWithElement(continuation, element) + } else { + // We are returning the queued failure now and can transition to finished + self._state = .finished(iteratorInitialized: sourceFinished.iteratorInitialized) + + return .resumeConsumerWithErrorAndCallOnTermination( + continuation, + sourceFinished.failure, + sourceFinished.onTermination + ) + } + + case .finished: + return .resumeConsumerWithNil(continuation) + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + + /// Actions returned by `cancelNext()`. + @usableFromInline + enum CancelNextAction { + /// Indicates that the continuation should be resumed with a cancellation error, the producers should be finished and call onTermination. + case resumeConsumerWithCancellationErrorAndCallOnTermination( + CheckedContinuation, + (() -> Void)? + ) + /// Indicates that the producers should be finished and call onTermination. + case failProducersAndCallOnTermination([(Result) -> Void], (() -> Void)?) + } + + @inlinable + mutating func cancelNext() -> CancelNextAction? { + switch self._state { + case .initial: + // We need to transition to streaming before we can suspend + fatalError("AsyncStream internal inconsistency") + + case .streaming(let streaming): + self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) + + if let consumerContinuation = streaming.consumerContinuation { + precondition( + streaming.producerContinuations.isEmpty, + "Internal inconsistency. Unexpected producer continuations." + ) + return .resumeConsumerWithCancellationErrorAndCallOnTermination( + consumerContinuation, + streaming.onTermination + ) + } else { + return .failProducersAndCallOnTermination( + Array(streaming.producerContinuations.map { $0.1 }), + streaming.onTermination + ) + } + + case .sourceFinished, .finished: + return .none + + case .modify: + fatalError("AsyncStream internal inconsistency") + } + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BufferedStream.Source: ClosableRPCWriterProtocol { + @inlinable + func finish() { + self.finish(throwing: nil) + } + + @inlinable + func finish(throwing error: Error) { + self.finish(throwing: error as Error?) + } +} diff --git a/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift b/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift new file mode 100644 index 000000000..e34dd9bc2 --- /dev/null +++ b/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift @@ -0,0 +1,1103 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +final class BufferedStreamTests: XCTestCase { + // MARK: - sequenceDeinitialized + + func testSequenceDeinitialized_whenNoIterator() async throws { + var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + withExtendedLifetime(stream) {} + stream = nil + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + do { + _ = try { try source.write(2) }() + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is AlreadyFinishedError) + } + + group.cancelAll() + } + } + + func testSequenceDeinitialized_whenIterator() async throws { + var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + var iterator = stream?.makeAsyncIterator() + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + try withExtendedLifetime(stream) { + let writeResult = try source.write(1) + writeResult.assertIsProducerMore() + } + + stream = nil + + do { + let writeResult = try { try source.write(2) }() + writeResult.assertIsProducerMore() + } catch { + XCTFail("Expected no error to be thrown") + } + + let element1 = try await iterator?.next() + XCTAssertEqual(element1, 1) + let element2 = try await iterator?.next() + XCTAssertEqual(element2, 2) + + group.cancelAll() + } + } + + func testSequenceDeinitialized_whenFinished() async throws { + var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + withExtendedLifetime(stream) { + source.finish(throwing: nil) + } + + stream = nil + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + do { + _ = try { try source.write(1) }() + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is AlreadyFinishedError) + } + + group.cancelAll() + } + } + + func testSequenceDeinitialized_whenStreaming_andSuspendedProducer() async throws { + var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 2) + ) + + _ = try { try source.write(1) }() + + do { + try await withCheckedThrowingContinuation { continuation in + source.write(1) { result in + continuation.resume(with: result) + } + + stream = nil + _ = stream?.makeAsyncIterator() + } + } catch { + XCTAssertTrue(error is AlreadyFinishedError) + } + } + + // MARK: - iteratorInitialized + + func testIteratorInitialized_whenInitial() async throws { + let (stream, _) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + _ = stream.makeAsyncIterator() + } + + func testIteratorInitialized_whenStreaming() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + try await source.write(1) + + var iterator = stream.makeAsyncIterator() + let element = try await iterator.next() + XCTAssertEqual(element, 1) + } + + func testIteratorInitialized_whenSourceFinished() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + try await source.write(1) + source.finish(throwing: nil) + + var iterator = stream.makeAsyncIterator() + let element1 = try await iterator.next() + XCTAssertEqual(element1, 1) + let element2 = try await iterator.next() + XCTAssertNil(element2) + } + + func testIteratorInitialized_whenFinished() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + source.finish(throwing: nil) + + var iterator = stream.makeAsyncIterator() + let element = try await iterator.next() + XCTAssertNil(element) + } + + // MARK: - iteratorDeinitialized + + func testIteratorDeinitialized_whenInitial() async throws { + var (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() + iterator = nil + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenStreaming() async throws { + var (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + try await source.write(1) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() + iterator = nil + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenSourceFinished() async throws { + var (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + try await source.write(1) + source.finish(throwing: nil) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() + iterator = nil + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenFinished() async throws { + var (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source.onTermination = { + onTerminationContinuation.finish() + } + + source.finish(throwing: nil) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() + iterator = nil + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testIteratorDeinitialized_whenStreaming_andSuspendedProducer() async throws { + var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 2) + ) + + var iterator: BufferedStream.AsyncIterator? = stream?.makeAsyncIterator() + stream = nil + + _ = try { try source.write(1) }() + + do { + try await withCheckedThrowingContinuation { continuation in + source.write(1) { result in + continuation.resume(with: result) + } + + iterator = nil + } + } catch { + XCTAssertTrue(error is AlreadyFinishedError) + } + + _ = try await iterator?.next() + } + + // MARK: - sourceDeinitialized + + func testSourceDeinitialized_whenInitial() async throws { + var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.onTermination = { + onTerminationContinuation.finish() + } + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + source = nil + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + + withExtendedLifetime(stream) {} + } + + func testSourceDeinitialized_whenStreaming_andEmptyBuffer() async throws { + var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.onTermination = { + onTerminationContinuation.finish() + } + + try await source?.write(1) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() + _ = try await iterator?.next() + + source = nil + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testSourceDeinitialized_whenStreaming_andNotEmptyBuffer() async throws { + var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.onTermination = { + onTerminationContinuation.finish() + } + + try await source?.write(1) + try await source?.write(2) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() + _ = try await iterator?.next() + + source = nil + + _ = await onTerminationIterator.next() + + _ = try await iterator?.next() + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testSourceDeinitialized_whenSourceFinished() async throws { + var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.onTermination = { + onTerminationContinuation.finish() + } + + try await source?.write(1) + try await source?.write(2) + source?.finish(throwing: nil) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() + _ = try await iterator?.next() + + source = nil + + _ = await onTerminationIterator.next() + + _ = try await iterator?.next() + _ = try await iterator?.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testSourceDeinitialized_whenFinished() async throws { + var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 5, high: 10) + ) + + let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() + source?.onTermination = { + onTerminationContinuation.finish() + } + + source?.finish(throwing: nil) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while !Task.isCancelled { + onTerminationContinuation.yield() + try await Task.sleep(nanoseconds: 200_000_000) + } + } + + var onTerminationIterator = onTerminationStream.makeAsyncIterator() + _ = await onTerminationIterator.next() + + _ = stream.makeAsyncIterator() + + source = nil + + _ = await onTerminationIterator.next() + + let terminationResult: Void? = await onTerminationIterator.next() + XCTAssertNil(terminationResult) + + group.cancelAll() + } + } + + func testSourceDeinitialized_whenStreaming_andSuspendedProducer() async throws { + var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 0, high: 0) + ) + let (producerStream, producerContinuation) = AsyncThrowingStream.makeStream() + var iterator = stream.makeAsyncIterator() + + source?.write(1) { + producerContinuation.yield(with: $0) + } + + _ = try await iterator.next() + source = nil + + do { + try await producerStream.first { _ in true } + XCTFail("We expected to throw here") + } catch { + XCTAssertTrue(error is AlreadyFinishedError) + } + } + + // MARK: - write + + func testWrite_whenInitial() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 5) + ) + + try await source.write(1) + + var iterator = stream.makeAsyncIterator() + let element = try await iterator.next() + XCTAssertEqual(element, 1) + } + + func testWrite_whenStreaming_andNoConsumer() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 5) + ) + + try await source.write(1) + try await source.write(2) + + var iterator = stream.makeAsyncIterator() + let element1 = try await iterator.next() + XCTAssertEqual(element1, 1) + let element2 = try await iterator.next() + XCTAssertEqual(element2, 2) + } + + func testWrite_whenStreaming_andSuspendedConsumer() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 5) + ) + + try await withThrowingTaskGroup(of: Int?.self) { group in + group.addTask { + return try await stream.first { _ in true } + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(nanoseconds: 500_000_000) + + try await source.write(1) + let element = try await group.next() + XCTAssertEqual(element, 1) + } + } + + func testWrite_whenStreaming_andSuspendedConsumer_andEmptySequence() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 5) + ) + + try await withThrowingTaskGroup(of: Int?.self) { group in + group.addTask { + return try await stream.first { _ in true } + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(nanoseconds: 500_000_000) + + try await source.write(contentsOf: []) + try await source.write(contentsOf: [1]) + let element = try await group.next() + XCTAssertEqual(element, 1) + } + } + + // MARK: - enqueueProducer + + func testEnqueueProducer_whenStreaming_andAndCancelled() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 2) + ) + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + try await source.write(1) + + let writeResult = try { try source.write(2) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.cancelCallback(callbackToken: callbackToken) + + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + } + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = try await stream.first { _ in true } + XCTAssertEqual(element, 1) + } + + func testEnqueueProducer_whenStreaming_andAndCancelled_andAsync() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 2) + ) + + try await source.write(1) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await source.write(2) + } + + group.cancelAll() + do { + try await group.next() + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + } + + let element = try await stream.first { _ in true } + XCTAssertEqual(element, 1) + } + + func testEnqueueProducer_whenStreaming_andInterleaving() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 1) + ) + var iterator = stream.makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.write(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + let element = try await iterator.next() + XCTAssertEqual(element, 1) + + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + } + + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } + + func testEnqueueProducer_whenStreaming_andSuspending() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 1) + ) + var iterator = stream.makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.write(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + } + + let element = try await iterator.next() + XCTAssertEqual(element, 1) + + do { + _ = try await producerStream.first { _ in true } + } catch { + XCTFail("Expected no error to be thrown") + } + } + + func testEnqueueProducer_whenFinished() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 1) + ) + var iterator = stream.makeAsyncIterator() + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + let writeResult = try { try source.write(1) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.finish(throwing: nil) + + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + } + + let element = try await iterator.next() + XCTAssertEqual(element, 1) + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is AlreadyFinishedError) + } + } + + // MARK: - cancelProducer + + func testCancelProducer_whenStreaming() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 2) + ) + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + try await source.write(1) + + let writeResult = try { try source.write(2) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + + source.cancelCallback(callbackToken: callbackToken) + } + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + let element = try await stream.first { _ in true } + XCTAssertEqual(element, 1) + } + + func testCancelProducer_whenSourceFinished() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 2) + ) + + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + + try await source.write(1) + + let writeResult = try { try source.write(2) }() + + switch writeResult { + case .produceMore: + preconditionFailure() + case .enqueueCallback(let callbackToken): + source.enqueueCallback(callbackToken: callbackToken) { result in + producerSource.yield(with: result) + } + + source.finish(throwing: nil) + + source.cancelCallback(callbackToken: callbackToken) + } + + do { + _ = try await producerStream.first { _ in true } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is AlreadyFinishedError) + } + + let element = try await stream.first { _ in true } + XCTAssertEqual(element, 1) + } + + // MARK: - finish + + func testFinish_whenStreaming_andConsumerSuspended() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 1) + ) + + try await withThrowingTaskGroup(of: Int?.self) { group in + group.addTask { + return try await stream.first { $0 == 2 } + } + + // This is always going to be a bit racy since we need the call to next() suspend + try await Task.sleep(nanoseconds: 500_000_000) + + source.finish(throwing: nil) + let element = try await group.next() + XCTAssertEqual(element, .some(nil)) + } + } + + func testFinish_whenInitial() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 1, high: 1) + ) + + source.finish(throwing: CancellationError()) + + do { + for try await _ in stream {} + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + } + + } + + // MARK: - Backpressure + + func testBackPressure() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 4) + ) + + let (backPressureEventStream, backPressureEventContinuation) = AsyncStream.makeStream( + of: Void.self + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while true { + backPressureEventContinuation.yield(()) + try await source.write(contentsOf: [1]) + } + } + + var backPressureEventIterator = backPressureEventStream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() + + await backPressureEventIterator.next() + await backPressureEventIterator.next() + await backPressureEventIterator.next() + await backPressureEventIterator.next() + + _ = try await iterator.next() + _ = try await iterator.next() + _ = try await iterator.next() + + await backPressureEventIterator.next() + await backPressureEventIterator.next() + await backPressureEventIterator.next() + + group.cancelAll() + } + } + + func testBackPressureSync() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 4) + ) + + let (backPressureEventStream, backPressureEventContinuation) = AsyncStream.makeStream( + of: Void.self + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + @Sendable func yield() { + backPressureEventContinuation.yield(()) + source.write(contentsOf: [1]) { result in + switch result { + case .success: + yield() + + case .failure: + break + } + } + } + + yield() + } + + var backPressureEventIterator = backPressureEventStream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() + + await backPressureEventIterator.next() + await backPressureEventIterator.next() + await backPressureEventIterator.next() + await backPressureEventIterator.next() + + _ = try await iterator.next() + _ = try await iterator.next() + _ = try await iterator.next() + + await backPressureEventIterator.next() + await backPressureEventIterator.next() + await backPressureEventIterator.next() + + group.cancelAll() + } + } + + func testThrowsError() async throws { + let (stream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 4) + ) + + try await source.write(1) + try await source.write(2) + source.finish(throwing: CancellationError()) + + var elements = [Int]() + var iterator = stream.makeAsyncIterator() + + do { + while let element = try await iterator.next() { + elements.append(element) + } + XCTFail("Expected an error to be thrown") + } catch { + XCTAssertTrue(error is CancellationError) + XCTAssertEqual(elements, [1, 2]) + } + + let element = try await iterator.next() + XCTAssertNil(element) + } + + func testAsyncSequenceWrite() async throws { + let (stream, continuation) = AsyncStream.makeStream() + let (backpressuredStream, source) = BufferedStream.makeStream( + of: Int.self, + backPressureStrategy: .watermark(low: 2, high: 4) + ) + + continuation.yield(1) + continuation.yield(2) + continuation.finish() + + try await source.write(contentsOf: stream) + source.finish(throwing: nil) + + let elements = try await backpressuredStream.collect() + XCTAssertEqual(elements, [1, 2]) + } +} + +extension BufferedStream.Source.WriteResult { + func assertIsProducerMore() { + switch self { + case .produceMore: + return + + case .enqueueCallback: + XCTFail("Expected produceMore") + } + } + + func assertIsEnqueueCallback() { + switch self { + case .produceMore: + XCTFail("Expected enqueueCallback") + + case .enqueueCallback: + return + } + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift index 2acd24bb6..c5d43db4a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift @@ -33,4 +33,20 @@ extension AsyncStream { return (stream, continuation) } } + +extension AsyncThrowingStream { + static func makeStream( + of elementType: Element.Type = Element.self, + throwing failureType: Failure.Type = Failure.self, + bufferingPolicy limit: AsyncThrowingStream.Continuation.BufferingPolicy = + .unbounded + ) -> ( + stream: AsyncThrowingStream, + continuation: AsyncThrowingStream.Continuation + ) where Failure == Error { + var continuation: AsyncThrowingStream.Continuation! + let stream = AsyncThrowingStream(bufferingPolicy: limit) { continuation = $0 } + return (stream, continuation!) + } +} #endif From aa69fcb6928bf736865b026acbb489c0b1b33fb3 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 25 Oct 2023 11:19:08 +0100 Subject: [PATCH 140/580] Make the connection pool error public (#1685) Motivation: Sometimes users end up with connection pool errors but they can't inspect them because the type is internal. Modifications: - Make the connection pool error extensible and public Result: Users can inspect connection pool errors. --- .../GRPC/ConnectionPool/ConnectionPool.swift | 97 +++++++++++++++---- .../ConnectionPool/ConnectionPoolTests.swift | 85 ++++++++-------- Tests/GRPCTests/GRPCStatusTests.swift | 4 +- 3 files changed, 126 insertions(+), 60 deletions(-) diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index d0d9508cc..e9b1fa1b9 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -283,7 +283,7 @@ internal final class ConnectionPool { guard case .active = self._state else { // Fail the promise right away if we're shutting down or already shut down. - promise.fail(ConnectionPoolError.shutdown) + promise.fail(GRPCConnectionPoolError.shutdown) return } @@ -354,7 +354,7 @@ internal final class ConnectionPool { Metadata.waitersMax: "\(self.maxWaiters)" ] ) - promise.fail(ConnectionPoolError.tooManyWaiters(connectionError: self._mostRecentError)) + promise.fail(GRPCConnectionPoolError.tooManyWaiters(connectionError: self._mostRecentError)) return } @@ -364,7 +364,7 @@ internal final class ConnectionPool { // timeout before appending it to the waiters, it wont run until the next event loop tick at the // earliest (even if the deadline has already passed). waiter.scheduleTimeout(on: self.eventLoop) { - waiter.fail(ConnectionPoolError.deadlineExceeded(connectionError: self._mostRecentError)) + waiter.fail(GRPCConnectionPoolError.deadlineExceeded(connectionError: self._mostRecentError)) if let index = self.waiters.firstIndex(where: { $0.id == waiter.id }) { self.waiters.remove(at: index) @@ -550,7 +550,7 @@ internal final class ConnectionPool { // Fail the outstanding waiters. while let waiter = self.waiters.popFirst() { - waiter.fail(ConnectionPoolError.shutdown) + waiter.fail(GRPCConnectionPoolError.shutdown) } // Cascade the result of the shutdown into the promise. @@ -864,40 +864,97 @@ extension ConnectionPool { } } -@usableFromInline -internal enum ConnectionPoolError: Error { - /// The pool is shutdown or shutting down. - case shutdown +/// An error thrown from the ``GRPCChannelPool``. +public struct GRPCConnectionPoolError: Error, CustomStringConvertible { + public struct Code: Hashable, Sendable, CustomStringConvertible { + enum Code { + case shutdown + case tooManyWaiters + case deadlineExceeded + } + + fileprivate var code: Code + + private init(_ code: Code) { + self.code = code + } + + public var description: String { + String(describing: self.code) + } - /// There are too many waiters in the pool. - case tooManyWaiters(connectionError: Error?) + /// The pool is shutdown or shutting down. + public static var shutdown: Self { Self(.shutdown) } - /// The deadline for creating a stream has passed. - case deadlineExceeded(connectionError: Error?) + /// There are too many waiters in the pool. + public static var tooManyWaiters: Self { Self(.tooManyWaiters) } + + /// The deadline for creating a stream has passed. + public static var deadlineExceeded: Self { Self(.deadlineExceeded) } + } + + /// The error code. + public var code: Code + + /// An underlying error which caused this error to be thrown. + public var underlyingError: Error? + + public var description: String { + if let underlyingError = self.underlyingError { + return "\(self.code) (\(underlyingError))" + } else { + return String(describing: self.code) + } + } + + /// Create a new connection pool error with the given code and underlying error. + /// + /// - Parameters: + /// - code: The error code. + /// - underlyingError: The underlying error which led to this error being thrown. + public init(code: Code, underlyingError: Error? = nil) { + self.code = code + self.underlyingError = underlyingError + } } -extension ConnectionPoolError: GRPCStatusTransformable { +extension GRPCConnectionPoolError { @usableFromInline - internal func makeGRPCStatus() -> GRPCStatus { - switch self { + static let shutdown = Self(code: .shutdown) + + @inlinable + static func tooManyWaiters(connectionError: Error?) -> Self { + Self(code: .tooManyWaiters, underlyingError: connectionError) + } + + @inlinable + static func deadlineExceeded(connectionError: Error?) -> Self { + Self(code: .deadlineExceeded, underlyingError: connectionError) + } +} + +extension GRPCConnectionPoolError: GRPCStatusTransformable { + public func makeGRPCStatus() -> GRPCStatus { + switch self.code.code { case .shutdown: return GRPCStatus( code: .unavailable, - message: "The connection pool is shutdown" + message: "The connection pool is shutdown", + cause: self.underlyingError ) - case let .tooManyWaiters(error): + case .tooManyWaiters: return GRPCStatus( code: .resourceExhausted, message: "The connection pool has no capacity for new RPCs or RPC waiters", - cause: error + cause: self.underlyingError ) - case let .deadlineExceeded(error): + case .deadlineExceeded: return GRPCStatus( code: .deadlineExceeded, message: "Timed out waiting for an HTTP/2 stream from the connection pool", - cause: error + cause: self.underlyingError ) } } diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 2e701fee8..5c2738774 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -160,7 +160,7 @@ final class ConnectionPoolTests: GRPCTestCase { } XCTAssertThrowsError(try stream.wait()) { error in - XCTAssert((error as? ConnectionPoolError).isShutdown) + XCTAssert((error as? GRPCConnectionPoolError).isShutdown) } } @@ -181,14 +181,14 @@ final class ConnectionPoolTests: GRPCTestCase { } XCTAssertThrowsError(try tooManyWaiters.wait()) { error in - XCTAssert((error as? ConnectionPoolError).isTooManyWaiters) + XCTAssert((error as? GRPCConnectionPoolError).isTooManyWaiters) } XCTAssertNoThrow(try pool.shutdown().wait()) // All 'waiting' futures will be failed by the shutdown promise. for waiter in waiting { XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? ConnectionPoolError).isShutdown) + XCTAssert((error as? GRPCConnectionPoolError).isShutdown) } } } @@ -205,7 +205,7 @@ final class ConnectionPoolTests: GRPCTestCase { self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? ConnectionPoolError).isDeadlineExceeded) + XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded) } XCTAssertEqual(pool.sync.waiters, 0) @@ -225,7 +225,7 @@ final class ConnectionPoolTests: GRPCTestCase { self.eventLoop.run() XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? ConnectionPoolError).isDeadlineExceeded) + XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded) } XCTAssertEqual(pool.sync.waiters, 0) @@ -358,7 +358,7 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertNoThrow(try shutdown.wait()) for waiter in others { XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? ConnectionPoolError).isShutdown) + XCTAssert((error as? GRPCConnectionPoolError).isShutdown) } } } @@ -503,7 +503,7 @@ final class ConnectionPoolTests: GRPCTestCase { // We need to advance the time to fire the timeout to fail the waiter. self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) XCTAssertThrowsError(try waiter1.wait()) { error in - XCTAssert((error as? ConnectionPoolError).isDeadlineExceeded) + XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded) } self.eventLoop.run() @@ -758,8 +758,10 @@ final class ConnectionPoolTests: GRPCTestCase { self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) XCTAssertThrowsError(try w1.wait()) { error in - switch error as? ConnectionPoolError { - case .some(.deadlineExceeded(.none)): + switch error as? GRPCConnectionPoolError { + case .some(let error): + XCTAssertEqual(error.code, .deadlineExceeded) + XCTAssertNil(error.underlyingError) // Deadline exceeded but no underlying error, as expected. () default: @@ -774,10 +776,11 @@ final class ConnectionPoolTests: GRPCTestCase { self.eventLoop.advanceTime(to: .uptimeNanoseconds(20)) XCTAssertThrowsError(try w2.wait()) { error in - switch error as? ConnectionPoolError { - case let .some(.deadlineExceeded(.some(wrappedError))): + switch error as? GRPCConnectionPoolError { + case let .some(error): + XCTAssertEqual(error.code, .deadlineExceeded) // Deadline exceeded and we have the underlying error. - XCTAssert(wrappedError is DummyError) + XCTAssert(error.underlyingError is DummyError) default: XCTFail("Expected ConnectionPoolError.deadlineExceeded(.some) but got \(error)") } @@ -837,9 +840,10 @@ final class ConnectionPoolTests: GRPCTestCase { $0.eventLoop.makeSucceededVoidFuture() } XCTAssertThrowsError(try tooManyWaiters.wait()) { error in - switch error as? ConnectionPoolError { - case .some(.tooManyWaiters(.none)): - () + switch error as? GRPCConnectionPoolError { + case .some(let error): + XCTAssertEqual(error.code, .tooManyWaiters) + XCTAssertNil(error.underlyingError) default: XCTFail("Expected ConnectionPoolError.tooManyWaiters(.none) but got \(error)") } @@ -849,9 +853,10 @@ final class ConnectionPoolTests: GRPCTestCase { self.eventLoop.advanceTime(by: .seconds(1)) for waiter in waiters { XCTAssertThrowsError(try waiter.wait()) { error in - switch error as? ConnectionPoolError { - case .some(.deadlineExceeded(.none)): - () + switch error as? GRPCConnectionPoolError { + case .some(let error): + XCTAssertEqual(error.code, .deadlineExceeded) + XCTAssertNil(error.underlyingError) default: XCTFail("Expected ConnectionPoolError.deadlineExceeded(.none) but got \(error)") } @@ -869,7 +874,7 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertNil(waiter._scheduledTimeout) waiter.scheduleTimeout(on: self.eventLoop) { - waiter.fail(ConnectionPoolError.deadlineExceeded(connectionError: nil)) + waiter.fail(GRPCConnectionPoolError.deadlineExceeded(connectionError: nil)) } XCTAssertNotNil(waiter._scheduledTimeout) @@ -1045,6 +1050,25 @@ final class ConnectionPoolTests: GRPCTestCase { } } } + + func testConnectionPoolErrorDescription() { + var error = GRPCConnectionPoolError(code: .deadlineExceeded) + XCTAssertEqual(String(describing: error), "deadlineExceeded") + error.code = .shutdown + XCTAssertEqual(String(describing: error), "shutdown") + error.code = .tooManyWaiters + XCTAssertEqual(String(describing: error), "tooManyWaiters") + + struct DummyError: Error {} + error.underlyingError = DummyError() + XCTAssertEqual(String(describing: error), "tooManyWaiters (DummyError())") + } + + func testConnectionPoolErrorCodeEquality() { + let error = GRPCConnectionPoolError(code: .deadlineExceeded) + XCTAssertEqual(error.code, .deadlineExceeded) + XCTAssertNotEqual(error.code, .shutdown) + } } extension ConnectionPool { @@ -1216,31 +1240,16 @@ internal struct HookedStreamLender: StreamLender { } } -extension Optional where Wrapped == ConnectionPoolError { +extension Optional where Wrapped == GRPCConnectionPoolError { internal var isTooManyWaiters: Bool { - switch self { - case .some(.tooManyWaiters): - return true - case .some(.deadlineExceeded), .some(.shutdown), .none: - return false - } + self?.code == .tooManyWaiters } internal var isDeadlineExceeded: Bool { - switch self { - case .some(.deadlineExceeded): - return true - case .some(.tooManyWaiters), .some(.shutdown), .none: - return false - } + self?.code == .deadlineExceeded } internal var isShutdown: Bool { - switch self { - case .some(.shutdown): - return true - case .some(.tooManyWaiters), .some(.deadlineExceeded), .none: - return false - } + self?.code == .shutdown } } diff --git a/Tests/GRPCTests/GRPCStatusTests.swift b/Tests/GRPCTests/GRPCStatusTests.swift index f32ea8050..8dde8ed39 100644 --- a/Tests/GRPCTests/GRPCStatusTests.swift +++ b/Tests/GRPCTests/GRPCStatusTests.swift @@ -113,10 +113,10 @@ class GRPCStatusTests: GRPCTestCase { // No message/cause, so uses the nil backing storage. XCTAssertEqual(status.testingOnly_storageObjectIdentifier, nilStorageID) - status.cause = ConnectionPoolError.tooManyWaiters(connectionError: nil) + status.cause = GRPCConnectionPoolError.tooManyWaiters(connectionError: nil) let storageID = status.testingOnly_storageObjectIdentifier XCTAssertNotEqual(storageID, nilStorageID) - XCTAssert(status.cause is ConnectionPoolError) + XCTAssert(status.cause is GRPCConnectionPoolError) // The storage of status should be uniquely ref'd, so setting cause to nil should not change // the backing storage (even if the nil storage could now be used). From 6ccafcc6c44b159302d5054e6e58e457cf0e4b69 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 25 Oct 2023 15:12:07 +0100 Subject: [PATCH 141/580] Add helpers to conver between Status.Code and RPCError.Code (#1688) Motivation: It's often useful to conver between RPCError and Status codes but we don't currently expose any API to do this; users have to go via the `rawValue` APIs. It should be easier than that. Modifications: - Add convenience APIs to `RPCError.Code` and `Status.Code` to create one from the other. Result: - Easier to convert between status and error codes --- Sources/GRPCCore/RPCError.swift | 50 +++++++++++++------------ Sources/GRPCCore/Status.swift | 46 +++++++++++++---------- Tests/GRPCCoreTests/RPCErrorTests.swift | 20 ++++++++++ Tests/GRPCCoreTests/StatusTests.swift | 19 ++++++++++ 4 files changed, 93 insertions(+), 42 deletions(-) diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 46c27a4ce..3a26269d5 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -75,7 +75,7 @@ public struct RPCError: @unchecked Sendable, Hashable, Error { /// /// - Parameter status: The status to convert. public init?(status: Status) { - guard let code = Code(statusCode: status.code) else { return nil } + guard let code = Code(status.code) else { return nil } self.init(code: code, message: status.message, metadata: [:]) } } @@ -119,16 +119,20 @@ extension RPCError { /// The numeric value of the error code. public var rawValue: Int { Int(self.wrapped.rawValue) } - private var wrapped: Status.Code.Wrapped - private init(_ wrapped: Status.Code.Wrapped) { - self.wrapped = wrapped + internal var wrapped: Status.Code.Wrapped + private init(code: Status.Code.Wrapped) { + self.wrapped = code } - internal init?(statusCode: Status.Code) { - if statusCode == .ok { + /// Creates an error code from the given ``Status/Code-swift.struct``; returns `nil` if the + /// code is ``Status/Code-swift.struct/ok``. + /// + /// - Parameter code: The status code to create this ``RPCError/Code-swift.struct`` from. + public init?(_ code: Status.Code) { + if code == .ok { return nil } else { - self.wrapped = statusCode.wrapped + self.wrapped = code.wrapped } } @@ -140,44 +144,44 @@ extension RPCError { extension RPCError.Code { /// The operation was cancelled (typically by the caller). - public static let cancelled = Self(.cancelled) + public static let cancelled = Self(code: .cancelled) /// Unknown error. An example of where this error may be returned is if a /// Status value received from another address space belongs to an error-space /// that is not known in this address space. Also errors raised by APIs that /// do not return enough error information may be converted to this error. - public static let unknown = Self(.unknown) + public static let unknown = Self(code: .unknown) /// Client specified an invalid argument. Note that this differs from /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are /// problematic regardless of the state of the system (e.g., a malformed file /// name). - public static let invalidArgument = Self(.invalidArgument) + public static let invalidArgument = Self(code: .invalidArgument) /// Deadline expired before operation could complete. For operations that /// change the state of the system, this error may be returned even if the /// operation has completed successfully. For example, a successful response /// from a server could have been delayed long enough for the deadline to /// expire. - public static let deadlineExceeded = Self(.deadlineExceeded) + public static let deadlineExceeded = Self(code: .deadlineExceeded) /// Some requested entity (e.g., file or directory) was not found. - public static let notFound = Self(.notFound) + public static let notFound = Self(code: .notFound) /// Some entity that we attempted to create (e.g., file or directory) already /// exists. - public static let alreadyExists = Self(.alreadyExists) + public static let alreadyExists = Self(code: .alreadyExists) /// The caller does not have permission to execute the specified operation. /// ``permissionDenied`` must not be used for rejections caused by exhausting /// some resource (use ``resourceExhausted`` instead for those errors). /// ``permissionDenied`` must not be used if the caller can not be identified /// (use ``unauthenticated`` instead for those errors). - public static let permissionDenied = Self(.permissionDenied) + public static let permissionDenied = Self(code: .permissionDenied) /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the /// entire file system is out of space. - public static let resourceExhausted = Self(.resourceExhausted) + public static let resourceExhausted = Self(code: .resourceExhausted) /// Operation was rejected because the system is not in a state required for /// the operation's execution. For example, directory to be deleted may be @@ -197,14 +201,14 @@ extension RPCError.Code { /// REST Get/Update/Delete on a resource and the resource on the /// server does not match the condition. E.g., conflicting /// read-modify-write on the same resource. - public static let failedPrecondition = Self(.failedPrecondition) + public static let failedPrecondition = Self(code: .failedPrecondition) /// The operation was aborted, typically due to a concurrency issue like /// sequencer check failures, transaction aborts, etc. /// /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, /// and ``unavailable``. - public static let aborted = Self(.aborted) + public static let aborted = Self(code: .aborted) /// Operation was attempted past the valid range. E.g., seeking or reading /// past end of file. @@ -219,26 +223,26 @@ extension RPCError.Code { /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error) /// when it applies so that callers who are iterating through a space can /// easily look for an ``outOfRange`` error to detect when they are done. - public static let outOfRange = Self(.outOfRange) + public static let outOfRange = Self(code: .outOfRange) /// Operation is not implemented or not supported/enabled in this service. - public static let unimplemented = Self(.unimplemented) + public static let unimplemented = Self(code: .unimplemented) /// Internal errors. Means some invariants expected by underlying System has /// been broken. If you see one of these errors, Something is very broken. - public static let internalError = Self(.internalError) + public static let internalError = Self(code: .internalError) /// The service is currently unavailable. This is a most likely a transient /// condition and may be corrected by retrying with a backoff. /// /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, /// and ``unavailable``. - public static let unavailable = Self(.unavailable) + public static let unavailable = Self(code: .unavailable) /// Unrecoverable data loss or corruption. - public static let dataLoss = Self(.dataLoss) + public static let dataLoss = Self(code: .dataLoss) /// The request does not have valid authentication credentials for the /// operation. - public static let unauthenticated = Self(.unauthenticated) + public static let unauthenticated = Self(code: .unauthenticated) } diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift index 69e30bdb2..cd5535bff 100644 --- a/Sources/GRPCCore/Status.swift +++ b/Sources/GRPCCore/Status.swift @@ -145,8 +145,16 @@ extension Status { } } - private init(_ wrapped: Wrapped) { - self.wrapped = wrapped + /// Creates a status code from an ``RPCError/Code-swift.struct``. + /// + /// - Parameters: + /// - code: The error code to create this ``Status/Code-swift.struct`` from. + public init(_ code: RPCError.Code) { + self.wrapped = code.wrapped + } + + private init(code: Wrapped) { + self.wrapped = code } public var description: String { @@ -157,47 +165,47 @@ extension Status { extension Status.Code { /// The operation completed successfully. - public static let ok = Self(.ok) + public static let ok = Self(code: .ok) /// The operation was cancelled (typically by the caller). - public static let cancelled = Self(.cancelled) + public static let cancelled = Self(code: .cancelled) /// Unknown error. An example of where this error may be returned is if a /// Status value received from another address space belongs to an error-space /// that is not known in this address space. Also errors raised by APIs that /// do not return enough error information may be converted to this error. - public static let unknown = Self(.unknown) + public static let unknown = Self(code: .unknown) /// Client specified an invalid argument. Note that this differs from /// ``failedPrecondition``. ``invalidArgument`` indicates arguments that are /// problematic regardless of the state of the system (e.g., a malformed file /// name). - public static let invalidArgument = Self(.invalidArgument) + public static let invalidArgument = Self(code: .invalidArgument) /// Deadline expired before operation could complete. For operations that /// change the state of the system, this error may be returned even if the /// operation has completed successfully. For example, a successful response /// from a server could have been delayed long enough for the deadline to /// expire. - public static let deadlineExceeded = Self(.deadlineExceeded) + public static let deadlineExceeded = Self(code: .deadlineExceeded) /// Some requested entity (e.g., file or directory) was not found. - public static let notFound = Self(.notFound) + public static let notFound = Self(code: .notFound) /// Some entity that we attempted to create (e.g., file or directory) already /// exists. - public static let alreadyExists = Self(.alreadyExists) + public static let alreadyExists = Self(code: .alreadyExists) /// The caller does not have permission to execute the specified operation. /// ``permissionDenied`` must not be used for rejections caused by exhausting /// some resource (use ``resourceExhausted`` instead for those errors). /// ``permissionDenied`` must not be used if the caller can not be identified /// (use ``unauthenticated`` instead for those errors). - public static let permissionDenied = Self(.permissionDenied) + public static let permissionDenied = Self(code: .permissionDenied) /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the /// entire file system is out of space. - public static let resourceExhausted = Self(.resourceExhausted) + public static let resourceExhausted = Self(code: .resourceExhausted) /// Operation was rejected because the system is not in a state required for /// the operation's execution. For example, directory to be deleted may be @@ -217,14 +225,14 @@ extension Status.Code { /// REST Get/Update/Delete on a resource and the resource on the /// server does not match the condition. E.g., conflicting /// read-modify-write on the same resource. - public static let failedPrecondition = Self(.failedPrecondition) + public static let failedPrecondition = Self(code: .failedPrecondition) /// The operation was aborted, typically due to a concurrency issue like /// sequencer check failures, transaction aborts, etc. /// /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, /// and ``unavailable``. - public static let aborted = Self(.aborted) + public static let aborted = Self(code: .aborted) /// Operation was attempted past the valid range. E.g., seeking or reading /// past end of file. @@ -239,26 +247,26 @@ extension Status.Code { /// ``outOfRange``. We recommend using ``outOfRange`` (the more specific error) /// when it applies so that callers who are iterating through a space can /// easily look for an ``outOfRange`` error to detect when they are done. - public static let outOfRange = Self(.outOfRange) + public static let outOfRange = Self(code: .outOfRange) /// Operation is not implemented or not supported/enabled in this service. - public static let unimplemented = Self(.unimplemented) + public static let unimplemented = Self(code: .unimplemented) /// Internal errors. Means some invariants expected by underlying System has /// been broken. If you see one of these errors, Something is very broken. - public static let internalError = Self(.internalError) + public static let internalError = Self(code: .internalError) /// The service is currently unavailable. This is a most likely a transient /// condition and may be corrected by retrying with a backoff. /// /// See litmus test above for deciding between ``failedPrecondition``, ``aborted``, /// and ``unavailable``. - public static let unavailable = Self(.unavailable) + public static let unavailable = Self(code: .unavailable) /// Unrecoverable data loss or corruption. - public static let dataLoss = Self(.dataLoss) + public static let dataLoss = Self(code: .dataLoss) /// The request does not have valid authentication credentials for the /// operation. - public static let unauthenticated = Self(.unauthenticated) + public static let unauthenticated = Self(code: .unauthenticated) } diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift index 877af2aee..87949ccdd 100644 --- a/Tests/GRPCCoreTests/RPCErrorTests.swift +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -61,6 +61,26 @@ final class RPCErrorTests: XCTestCase { XCTAssertEqual(error.metadata, [:]) } + func testErrorCodeFromStatusCode() throws { + XCTAssertNil(RPCError.Code(Status.Code.ok)) + XCTAssertEqual(RPCError.Code(Status.Code.cancelled), .cancelled) + XCTAssertEqual(RPCError.Code(Status.Code.unknown), .unknown) + XCTAssertEqual(RPCError.Code(Status.Code.invalidArgument), .invalidArgument) + XCTAssertEqual(RPCError.Code(Status.Code.deadlineExceeded), .deadlineExceeded) + XCTAssertEqual(RPCError.Code(Status.Code.notFound), .notFound) + XCTAssertEqual(RPCError.Code(Status.Code.alreadyExists), .alreadyExists) + XCTAssertEqual(RPCError.Code(Status.Code.permissionDenied), .permissionDenied) + XCTAssertEqual(RPCError.Code(Status.Code.resourceExhausted), .resourceExhausted) + XCTAssertEqual(RPCError.Code(Status.Code.failedPrecondition), .failedPrecondition) + XCTAssertEqual(RPCError.Code(Status.Code.aborted), .aborted) + XCTAssertEqual(RPCError.Code(Status.Code.outOfRange), .outOfRange) + XCTAssertEqual(RPCError.Code(Status.Code.unimplemented), .unimplemented) + XCTAssertEqual(RPCError.Code(Status.Code.internalError), .internalError) + XCTAssertEqual(RPCError.Code(Status.Code.unavailable), .unavailable) + XCTAssertEqual(RPCError.Code(Status.Code.dataLoss), .dataLoss) + XCTAssertEqual(RPCError.Code(Status.Code.unauthenticated), .unauthenticated) + } + func testEquatableConformance() { XCTAssertEqual( RPCError(code: .cancelled, message: ""), diff --git a/Tests/GRPCCoreTests/StatusTests.swift b/Tests/GRPCCoreTests/StatusTests.swift index 29c48b287..98d114934 100644 --- a/Tests/GRPCCoreTests/StatusTests.swift +++ b/Tests/GRPCCoreTests/StatusTests.swift @@ -50,6 +50,25 @@ final class StatusTests: XCTestCase { } } + func testStatusCodeFromErrorCode() throws { + XCTAssertEqual(Status.Code(RPCError.Code.cancelled), .cancelled) + XCTAssertEqual(Status.Code(RPCError.Code.unknown), .unknown) + XCTAssertEqual(Status.Code(RPCError.Code.invalidArgument), .invalidArgument) + XCTAssertEqual(Status.Code(RPCError.Code.deadlineExceeded), .deadlineExceeded) + XCTAssertEqual(Status.Code(RPCError.Code.notFound), .notFound) + XCTAssertEqual(Status.Code(RPCError.Code.alreadyExists), .alreadyExists) + XCTAssertEqual(Status.Code(RPCError.Code.permissionDenied), .permissionDenied) + XCTAssertEqual(Status.Code(RPCError.Code.resourceExhausted), .resourceExhausted) + XCTAssertEqual(Status.Code(RPCError.Code.failedPrecondition), .failedPrecondition) + XCTAssertEqual(Status.Code(RPCError.Code.aborted), .aborted) + XCTAssertEqual(Status.Code(RPCError.Code.outOfRange), .outOfRange) + XCTAssertEqual(Status.Code(RPCError.Code.unimplemented), .unimplemented) + XCTAssertEqual(Status.Code(RPCError.Code.internalError), .internalError) + XCTAssertEqual(Status.Code(RPCError.Code.unavailable), .unavailable) + XCTAssertEqual(Status.Code(RPCError.Code.dataLoss), .dataLoss) + XCTAssertEqual(Status.Code(RPCError.Code.unauthenticated), .unauthenticated) + } + func testStatusCodeFromValidRawValue() { for (expected, rawValue) in Self.statusCodeRawValue { XCTAssertEqual( From e595df4dfa1ca184d20f502f433637fff55153e0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 26 Oct 2023 13:58:01 +0100 Subject: [PATCH 142/580] Add a broadcast async sequence (#1684) Motivation: To support retries and hedging we need a way to buffer elements over time that can support multiple consumers concurrently and allows for consumers to start consuming after some elements have been produced. An `AsyncSequence` fits this quite naturally but we don't yet have a general purpose implementat that fits this requirement. This change adds `BroadcastAsyncSequence` which isn't a general purpose async sequence but instead is tailored to the needs of grpc for hedging and retries. This means it supports a low number of concurrent iterators and maintains a limited size internal buffer and drops the slowest consumers when the buffer becomes full. Modifications: - Add a `BroadcastAsyncSequence` and tests - Made a bunch of things inlinable/usableFromInline which necessitated a switch from `@_spi(Testing)` to `@testable` imports. - Rename the 'Stream' directory to 'Streaming' Result: - `BroadcastAsyncSequence` can be used to implement retries and hedging. --- Package.swift | 2 +- .../GRPCCore/Call/Client/ClientResponse.swift | 3 +- .../Internal}/AsyncSequenceOfOne.swift | 24 +- .../Internal/BroadcastAsyncSequence.swift | 1734 +++++++++++++++++ .../RPCAsyncSequence.swift | 0 .../{Stream => Streaming}/RPCWriter.swift | 0 .../RPCWriterProtocol.swift | 8 +- .../Call/Client/ClientResponseTests.swift | 4 +- .../Internal}/AsyncSequenceOfOne.swift | 4 +- .../BroadcastAsyncSequenceTests.swift | 257 +++ .../Test Utilities/XCTest+Utilities.swift | 12 + 11 files changed, 2034 insertions(+), 14 deletions(-) rename Sources/GRPCCore/{Stream => Streaming/Internal}/AsyncSequenceOfOne.swift (76%) create mode 100644 Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift rename Sources/GRPCCore/{Stream => Streaming}/RPCAsyncSequence.swift (100%) rename Sources/GRPCCore/{Stream => Streaming}/RPCWriter.swift (100%) rename Sources/GRPCCore/{Stream => Streaming}/RPCWriterProtocol.swift (88%) rename Tests/GRPCCoreTests/{Stream => Streaming/Internal}/AsyncSequenceOfOne.swift (97%) create mode 100644 Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift diff --git a/Package.swift b/Package.swift index 10472303b..2b3ed64cb 100644 --- a/Package.swift +++ b/Package.swift @@ -158,7 +158,7 @@ extension Target { static let grpcCore: Target = .target( name: "GRPCCore", dependencies: [ - .dequeModule + .dequeModule, ], path: "Sources/GRPCCore" ) diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index f384ac80a..642e9e0f3 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -371,8 +371,7 @@ extension ClientResponse.Stream { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ClientResponse.Single { - @_spi(Testing) - public init(stream response: ClientResponse.Stream) async { + init(stream response: ClientResponse.Stream) async { switch response.accepted { case .success(let contents): do { diff --git a/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift similarity index 76% rename from Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift rename to Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift index bc58be2f3..2e79773b5 100644 --- a/Sources/GRPCCore/Stream/AsyncSequenceOfOne.swift +++ b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift @@ -17,38 +17,46 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCAsyncSequence { /// Returns an ``RPCAsyncSequence`` containing just the given element. - @_spi(Testing) - public static func one(_ element: Element) -> Self { + @inlinable + static func one(_ element: Element) -> Self { return Self(wrapping: AsyncSequenceOfOne(result: .success(element))) } /// Returns an ``RPCAsyncSequence`` throwing the given error. - @_spi(Testing) - public static func throwing(_ error: E) -> Self { + @inlinable + static func throwing(_ error: E) -> Self { return Self(wrapping: AsyncSequenceOfOne(result: .failure(error))) } } /// An `AsyncSequence` of a single value. +@usableFromInline @available(macOS 10.15, iOS 13.0, tvOS 13, watchOS 6, *) -private struct AsyncSequenceOfOne: AsyncSequence { - private let result: Result +struct AsyncSequenceOfOne: AsyncSequence { + @usableFromInline + let result: Result + @inlinable init(result: Result) { self.result = result } + @inlinable func makeAsyncIterator() -> AsyncIterator { AsyncIterator(result: self.result) } + @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { - private var result: Result? + @usableFromInline + private(set) var result: Result? - fileprivate init(result: Result) { + @inlinable + init(result: Result) { self.result = result } + @inlinable mutating func next() async throws -> Element? { guard let result = self.result else { return nil } diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift new file mode 100644 index 000000000..5e8e08365 --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -0,0 +1,1734 @@ +/* + * Copyright 2023, 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 DequeModule + +/// An `AsyncSequence` which can broadcast its values to multiple consumers concurrently. +/// +/// The sequence is not a general-purpose broadcast sequence; it is tailored specifically for the +/// requirements of gRPC Swift, in particular it is used to support retrying and hedging requests. +/// +/// In order to achieve this it maintains on an internal buffer of elements which is limited in +/// size. Each iterator ("subscriber") maintains an offset into the elements which the sequence has +/// produced over time. If a subscriber is consuming too slowly (and the buffer is full) then the +/// sequence will cancel the subscriber's subscription to the stream, dropping the oldest element +/// in the buffer to make space for more elements. If the buffer is full and all subscribers are +/// equally slow then all producers are suspended until the buffer drops to a reasonable size. +/// +/// The expectation is that the number of subscribers will be low; for retries there will be at most +/// one subscriber at a time, for hedging there may be at most five subscribers at a time. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@usableFromInline +struct BroadcastAsyncSequence: Sendable, AsyncSequence { + @usableFromInline + let _storage: _BroadcastSequenceStorage + + @inlinable + init(_storage: _BroadcastSequenceStorage) { + self._storage = _storage + } + + /// Make a new stream and continuation. + /// + /// - Parameters: + /// - elementType: The type of element this sequence produces. + /// - bufferSize: The number of elements this sequence may store. + /// - Returns: A stream and continuation. + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferSize: Int + ) -> (stream: Self, continuation: Self.Source) { + let storage = _BroadcastSequenceStorage(bufferSize: bufferSize) + let stream = Self(_storage: storage) + let continuation = Self.Source(_storage: storage) + return (stream, continuation) + } + + @inlinable + func makeAsyncIterator() -> AsyncIterator { + let id = self._storage.subscribe() + return AsyncIterator(_storage: _storage, id: id) + } + + /// Returns true if it is known to be safe for the next subscriber to subscribe and successfully + /// consume elements. + /// + /// This function can return `false` if there are active subscribers or the internal buffer no + /// longer contains the first element in the sequence. + @inlinable + var isKnownSafeForNextSubscriber: Bool { + self._storage.isKnownSafeForNextSubscriber + } + + /// Invalidates all active subscribers. + /// + /// Any active subscriber will receive an error the next time they attempt to consume an element. + @inlinable + func invalidateAllSubscriptions() { + self._storage.invalidateAllSubscriptions() + } +} + +// MARK: - AsyncIterator + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BroadcastAsyncSequence { + @usableFromInline + struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline + let _storage: _BroadcastSequenceStorage + @usableFromInline + let _subscriberID: _BroadcastSequenceStateMachine.Subscriptions.ID + + @inlinable + init( + _storage: _BroadcastSequenceStorage, + id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) { + self._storage = _storage + self._subscriberID = id + } + + @inlinable + mutating func next() async throws -> Element? { + try await self._storage.nextElement(forSubscriber: self._subscriberID) + } + } +} + +// MARK: - Continuation + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BroadcastAsyncSequence { + @usableFromInline + struct Source: Sendable { + @usableFromInline + let _storage: _BroadcastSequenceStorage + + @usableFromInline + init(_storage: _BroadcastSequenceStorage) { + self._storage = _storage + } + + @inlinable + func write(_ element: Element) async throws { + try await self._storage.yield(element) + } + + @inlinable + func finish(with result: Result) { + self._storage.finish(result) + } + + @inlinable + func finish() { + self.finish(with: .success(())) + } + + @inlinable + func finish(throwing error: Error) { + self.finish(with: .failure(error)) + } + } +} + +@usableFromInline +enum BroadcastAsyncSequenceError: Error { + /// The consumer was too slow. + case consumingTooSlow + /// The producer has already finished. + case productionAlreadyFinished +} + +// MARK: - Storage + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@usableFromInline +final class _BroadcastSequenceStorage: Sendable { + @usableFromInline + let _state: LockedValueBox<_BroadcastSequenceStateMachine> + + @inlinable + init(bufferSize: Int) { + self._state = LockedValueBox(_BroadcastSequenceStateMachine(bufferSize: bufferSize)) + } + + deinit { + let onDrop = self._state.withLockedValue { state in + state.dropResources() + } + + switch onDrop { + case .none: + () + case .resume(let consumers, let producers): + consumers.resume() + producers.resume() + } + } + + // MARK - Producer + + /// Yield a single element to the stream. Suspends if the stream's buffer is full. + /// + /// - Parameter element: The element to write. + @inlinable + func yield(_ element: Element) async throws { + let onYield = self._state.withLockedValue { state in state.yield(element) } + + switch onYield { + case .none: + () + + case .resume(let continuations): + continuations.resume() + + case .suspend(let token): + try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let onProduceMore = self._state.withLockedValue { state in + state.waitToProduceMore(continuation: continuation, token: token) + } + + switch onProduceMore { + case .resume(let continuation, let result): + continuation.resume(with: result) + case .none: + () + } + } + } onCancel: { + let onCancel = self._state.withLockedValue { state in + state.cancelProducer(withToken: token) + } + + switch onCancel { + case .resume(let continuation, let result): + continuation.resume(with: result) + case .none: + () + } + } + + case .throwAlreadyFinished: + throw BroadcastAsyncSequenceError.productionAlreadyFinished + } + } + + /// Indicate that no more values will be produced. + /// + /// - Parameter result: Whether the stream is finishing cleanly or because of an error. + @inlinable + func finish(_ result: Result) { + let action = self._state.withLockedValue { state in state.finish(result: result) } + switch action { + case .none: + () + case .resume(let subscribers, let producers): + subscribers.resume() + producers.resume() + } + } + + // MARK: - Consumer + + /// Create a subscription to the stream. + /// + /// - Returns: Returns a unique subscription ID. + @inlinable + func subscribe() -> _BroadcastSequenceStateMachine.Subscriptions.ID { + self._state.withLockedValue { $0.subscribe() } + } + + /// Returns the next element for the given subscriber, if it is available. + /// + /// - Parameter id: The ID of the subscriber requesting the element. + /// - Returns: The next element or `nil` if the stream has been terminated. + @inlinable + func nextElement( + forSubscriber id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) async throws -> Element? { + let onNext = self._state.withLockedValue { $0.nextElement(forSubscriber: id) } + + switch onNext { + case .return(let returnAndProduceMore): + returnAndProduceMore.producers.resume() + return try returnAndProduceMore.nextResult.get() + + case .suspend: + return try await withTaskCancellationHandler { + return try await withCheckedThrowingContinuation { continuation in + let onSetContinuation = self._state.withLockedValue { state in + state.setContinuation(continuation, forSubscription: id) + } + + switch onSetContinuation { + case .resume(let continuation, let result): + continuation.resume(with: result) + case .none: + () + } + } + } onCancel: { + let onCancel = self._state.withLockedValue { state in + state.cancelSubscription(withID: id) + } + + switch onCancel { + case .resume(let continuation, let result): + continuation.resume(with: result) + case .none: + () + } + } + } + } + + /// Returns true if it's guaranteed that the next subscriber may join and safely begin consuming + /// elements. + @inlinable + var isKnownSafeForNextSubscriber: Bool { + self._state.withLockedValue { state in + state.nextSubscriptionIsValid + } + } + + /// Invalidates all active subscriptions. + @inlinable + func invalidateAllSubscriptions() { + let action = self._state.withLockedValue { state in + state.invalidateAllSubscriptions() + } + + switch action { + case .resume(let continuations): + continuations.resume() + case .none: + () + } + } +} + +// MARK: - State machine + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@usableFromInline +struct _BroadcastSequenceStateMachine: Sendable { + @usableFromInline + typealias ConsumerContinuation = CheckedContinuation + @usableFromInline + typealias ProducerContinuation = CheckedContinuation + + @usableFromInline + struct ConsumerContinuations { + @usableFromInline + var continuations: _OneOrMany + @usableFromInline + var result: Result + + @inlinable + init(continuations: _OneOrMany, result: Result) { + self.continuations = continuations + self.result = result + } + + @inlinable + func resume() { + switch self.continuations { + case .one(let continuation): + continuation.resume(with: self.result) + case .many(let continuations): + for continuation in continuations { + continuation.resume(with: self.result) + } + } + } + } + + @usableFromInline + struct ProducerContinuations { + @usableFromInline + var continuations: [ProducerContinuation] + @usableFromInline + var result: Result + + @inlinable + init(continuations: [ProducerContinuation], result: Result) { + self.continuations = continuations + self.result = result + } + + @inlinable + func resume() { + for continuation in self.continuations { + continuation.resume(with: self.result) + } + } + } + + @usableFromInline + enum State: Sendable { + /// No subscribers and no elements have been produced. + case initial(Initial) + /// Subscribers exist but no elements have been produced. + case subscribed(Subscribed) + /// Elements have been produced, there may or may not be subscribers. + case streaming(Streaming) + /// No more elements will be produced. There may or may not been subscribers. + case finished(Finished) + /// Temporary state to avoid CoWs. + case _modifying + + @inlinable + init(bufferSize: Int) { + self = .initial(Initial(bufferSize: bufferSize)) + } + + @usableFromInline + struct Initial: Sendable { + @usableFromInline + let bufferSize: Int + + @inlinable + init(bufferSize: Int) { + self.bufferSize = bufferSize + } + } + + @usableFromInline + struct Subscribed: Sendable { + /// Active subscriptions. + @usableFromInline + var subscriptions: _BroadcastSequenceStateMachine.Subscriptions + /// Subscriptions to fail and remove when they next request an element. + @usableFromInline + var subscriptionsToDrop: [_BroadcastSequenceStateMachine.Subscriptions.ID] + + /// The maximum size of the element buffer. + @usableFromInline + let bufferSize: Int + + @inlinable + init(from state: Initial) { + self.subscriptions = Subscriptions() + self.subscriptionsToDrop = [] + self.bufferSize = state.bufferSize + } + + @inlinable + mutating func finish(result: Result) -> OnFinish { + guard let continuations = self.subscriptions.removeSubscribersWithContinuations() else { + return .none + } + + return .resume( + .init(continuations: continuations, result: result.map { nil }), + .init(continuations: [], result: .success(())) + ) + } + + @inlinable + mutating func next(_ id: _BroadcastSequenceStateMachine.Subscriptions.ID) -> OnNext { + // Not streaming, so suspend or remove if the subscription should be dropped. + guard let index = self.subscriptionsToDrop.firstIndex(of: id) else { + return .suspend + } + + self.subscriptionsToDrop.remove(at: index) + return .return(.init(nextResult: .failure(BroadcastAsyncSequenceError.consumingTooSlow))) + } + + @inlinable + mutating func cancel( + _ id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnCancelSubscription { + let (removed, continuation) = self.subscriptions.removeSubscriber(withID: id) + assert(removed) + if let continuation = continuation { + return .resume(continuation, .failure(CancellationError())) + } else { + return .none + } + } + + @inlinable + mutating func setContinuation( + _ continuation: ConsumerContinuation, + forSubscription id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnSetContinuation { + let didSet = self.subscriptions.setContinuation(continuation, forSubscriber: id) + return didSet ? .none : .resume(continuation, .failure(CancellationError())) + } + + @inlinable + mutating func subscribe() -> _BroadcastSequenceStateMachine.Subscriptions.ID { + self.subscriptions.subscribe() + } + + @inlinable + mutating func invalidateAllSubscriptions() -> OnInvalidateAllSubscriptions { + let ids = self.subscriptions.removeAllSubscribers() + self.subscriptionsToDrop.append(contentsOf: ids) + return .none + } + + @inlinable + mutating func dropResources(error: BroadcastAsyncSequenceError) -> OnDropResources { + if let continuations = self.subscriptions.removeSubscribersWithContinuations() { + let consumerContinuations = ConsumerContinuations( + continuations: continuations, + result: .failure(error) + ) + let producerContinuations = ProducerContinuations(continuations: [], result: .success(())) + return .resume(consumerContinuations, producerContinuations) + } else { + return .none + } + } + } + + @usableFromInline + struct Streaming: Sendable { + /// A deque of elements tagged with IDs. + @usableFromInline + var elements: Elements + /// The maximum size of the element buffer. + @usableFromInline + let bufferSize: Int + + // TODO: (optimisation) one-or-many Deque to avoid allocations in the case of a single writer + /// Producers which have been suspended. + @usableFromInline + var producers: [(ProducerContinuation, Int)] + /// The IDs of producers which have been cancelled. + @usableFromInline + var cancelledProducers: [Int] + /// The next token for a producer. + @usableFromInline + var producerToken: Int + + /// Active subscriptions. + @usableFromInline + var subscriptions: _BroadcastSequenceStateMachine.Subscriptions + /// Subscriptions to fail and remove when they next request an element. + @usableFromInline + var subscriptionsToDrop: [_BroadcastSequenceStateMachine.Subscriptions.ID] + @inlinable + init(from state: Initial) { + self.elements = Elements() + self.producers = [] + self.producerToken = 0 + self.cancelledProducers = [] + self.subscriptions = Subscriptions() + self.subscriptionsToDrop = [] + self.bufferSize = state.bufferSize + } + + @inlinable + init(from state: Subscribed) { + self.elements = Elements() + self.producers = [] + self.producerToken = 0 + self.cancelledProducers = [] + self.subscriptions = state.subscriptions + self.subscriptionsToDrop = state.subscriptionsToDrop + self.bufferSize = state.bufferSize + } + + @inlinable + mutating func append(_ element: Element) -> OnYield { + let onYield: OnYield + self.elements.append(element) + + if self.elements.count >= self.bufferSize, let lowestID = self.elements.lowestID { + // If the buffer is too large then: + // - if all subscribers are equally slow suspend the producer + // - if some subscribers are slow then remove them and the oldest value + // - if no subscribers are slow then remove the oldest value + let slowConsumers = self.subscriptions.subscribers(withElementID: lowestID) + + switch slowConsumers.count { + case 0: + if self.subscriptions.isEmpty { + // No consumers. + let token = self.producerToken + self.producerToken += 1 + onYield = .suspend(token) + } else { + // No consumers are slow. Remove the oldest value. + self.elements.removeFirst() + onYield = .none + } + + case self.subscriptions.count: + // All consumers are slow; stop the production of new value. + let token = self.producerToken + self.producerToken += 1 + onYield = .suspend(token) + + default: + // Some consumers are slow, but not all. Remove the slow consumers and drop the + // oldest value. + self.elements.removeFirst() + self.subscriptions.removeAllSubscribers(in: slowConsumers) + self.subscriptionsToDrop.append(contentsOf: slowConsumers) + onYield = .none + } + } else { + // The buffer isn't full. Take the continuations of subscriptions which have them; they + // must be waiting for the value we just appended. + let continuations = self.subscriptions.takeContinuations().map { + ConsumerContinuations(continuations: $0, result: .success(element)) + } + + if let continuations = continuations { + onYield = .resume(continuations) + } else { + onYield = .none + } + } + + return onYield + } + + @inlinable + mutating func next(_ id: _BroadcastSequenceStateMachine.Subscriptions.ID) -> OnNext { + let onNext: OnNext + + // 1. Lookup the subscriber by ID to get their next offset + // 2. If the element exists, update the element pointer and return the element + // 3. Else if the ID is in the future, wait + // 4. Else the ID is in the past, fail and remove the subscription. + + // Lookup the subscriber with the given ID. + let onNextForSubscription = self.subscriptions.withMutableElementID( + forSubscriber: id + ) { elementID -> (OnNext, Bool) in + let onNext: OnNext + let removeSubscription: Bool + + // Subscriber exists; do we have the element it requires next? + switch self.elements.lookupElement(withID: elementID) { + case .found(let element): + // Element exists in the buffer. Advance our element ID. + elementID.formNext() + onNext = .return(.init(nextResult: .success(element))) + removeSubscription = false + case .maybeAvailableLater: + // Element may exist in the future. + onNext = .suspend + removeSubscription = false + case .noLongerAvailable: + // Element existed in the past but was dropped from the buffer. + onNext = .return( + .init(nextResult: .failure(BroadcastAsyncSequenceError.consumingTooSlow)) + ) + removeSubscription = true + } + + return (onNext, removeSubscription) + } + + switch onNextForSubscription { + case .return(var resultAndResume): + // The producer only suspends when all consumers are equally slow or there are no + // consumers at all. The latter can't be true: this function can only be called by a + // consumer. The former can't be true anymore because consumption isn't concurrent + // so this consumer must be faster than the others so let the producer resume. + // + // Note that this doesn't mean that all other consumers will be dropped: they can continue + // to produce until the producer provides more values. + resultAndResume.producers = ProducerContinuations( + continuations: self.producers.map { $0.0 }, + result: .success(()) + ) + self.producers.removeAll() + onNext = .return(resultAndResume) + + case .suspend: + onNext = .suspend + + case .none: + // No subscription found, must have been dropped or already finished. + if let index = self.subscriptionsToDrop.firstIndex(where: { $0 == id }) { + self.subscriptionsToDrop.remove(at: index) + onNext = .return( + .init(nextResult: .failure(BroadcastAsyncSequenceError.consumingTooSlow)) + ) + } else { + // Unknown subscriber, i.e. already finished. + onNext = .return(.init(nextResult: .success(nil))) + } + } + + return onNext + } + + @inlinable + mutating func setContinuation( + _ continuation: ConsumerContinuation, + forSubscription id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnSetContinuation { + let didSet = self.subscriptions.setContinuation(continuation, forSubscriber: id) + return didSet ? .none : .resume(continuation, .failure(CancellationError())) + } + + @inlinable + mutating func cancel( + _ id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnCancelSubscription { + let (removed, continuation) = self.subscriptions.removeSubscriber(withID: id) + assert(removed) + if let continuation = continuation { + return .resume(continuation, .failure(CancellationError())) + } else { + return .none + } + } + + @inlinable + mutating func waitToProduceMore( + _ continuation: ProducerContinuation, + token: Int + ) -> OnWaitToProduceMore { + let onWaitToProduceMore: OnWaitToProduceMore + + if self.elements.count < self.bufferSize { + // Buffer has free space, no need to suspend. + onWaitToProduceMore = .resume(continuation, .success(())) + } else if let index = self.cancelledProducers.firstIndex(of: token) { + // Producer was cancelled before suspending. + self.cancelledProducers.remove(at: index) + onWaitToProduceMore = .resume(continuation, .failure(CancellationError())) + } else { + // Store the continuation to resume later. + self.producers.append((continuation, token)) + onWaitToProduceMore = .none + } + + return onWaitToProduceMore + } + + @inlinable + mutating func cancelProducer(withToken token: Int) -> OnCancelProducer { + guard let index = self.producers.firstIndex(where: { $0.1 == token }) else { + self.cancelledProducers.append(token) + return .none + } + + let (continuation, _) = self.producers.remove(at: index) + return .resume(continuation, .failure(CancellationError())) + } + + @inlinable + mutating func finish(result: Result) -> OnFinish { + let continuations = self.subscriptions.removeSubscribersWithContinuations() + let producers = self.producers.map { $0.0 } + self.producers.removeAll() + return .resume( + .init(continuations: continuations ?? .many([]), result: result.map { nil }), + .init(continuations: producers, result: .success(())) + ) + } + + @inlinable + mutating func subscribe() -> _BroadcastSequenceStateMachine.Subscriptions.ID { + self.subscriptions.subscribe() + } + + @inlinable + mutating func invalidateAllSubscriptions() -> OnInvalidateAllSubscriptions { + let onCancel: OnInvalidateAllSubscriptions + + // Remove subscriptions with continuations, they need to be failed. + switch self.subscriptions.removeSubscribersWithContinuations() { + case .some(let oneOrMany): + let continuations = ConsumerContinuations( + continuations: oneOrMany, + result: .failure( + BroadcastAsyncSequenceError.consumingTooSlow + ) + ) + onCancel = .resume(continuations) + case .none: + onCancel = .none + } + + // Remove any others to be failed when they next call 'next'. + let ids = self.subscriptions.removeAllSubscribers() + self.subscriptionsToDrop.append(contentsOf: ids) + return onCancel + } + + @inlinable + mutating func dropResources(error: BroadcastAsyncSequenceError) -> OnDropResources { + let consumers = self.subscriptions.removeSubscribersWithContinuations().map { + ConsumerContinuations(continuations: $0, result: .failure(error)) + } + + let producers = ProducerContinuations( + continuations: self.producers.map { $0.0 }, + result: .failure(error) + ) + + self.producers.removeAll() + + return .resume( + consumers ?? ConsumerContinuations(continuations: .many([]), result: .failure(error)), + producers + ) + } + + @inlinable + func nextSubscriptionIsValid() -> Bool { + return self.subscriptions.isEmpty && self.elements.lowestID == .initial + } + } + + @usableFromInline + struct Finished: Sendable { + /// A deque of elements tagged with IDs. + @usableFromInline + var elements: Elements + + /// Active subscriptions. + @usableFromInline + var subscriptions: _BroadcastSequenceStateMachine.Subscriptions + /// Subscriptions to fail and remove when they next request an element. + @usableFromInline + var subscriptionsToDrop: [_BroadcastSequenceStateMachine.Subscriptions.ID] + + /// The terminating result of the sequence. + @usableFromInline + let result: Result + + @inlinable + init(from state: Initial, result: Result) { + self.elements = Elements() + self.subscriptions = Subscriptions() + self.subscriptionsToDrop = [] + self.result = result + } + + @inlinable + init(from state: Subscribed, result: Result) { + self.elements = Elements() + self.subscriptions = state.subscriptions + self.subscriptionsToDrop = [] + self.result = result + } + + @inlinable + init(from state: Streaming, result: Result) { + self.elements = state.elements + self.subscriptions = state.subscriptions + self.subscriptionsToDrop = state.subscriptionsToDrop + self.result = result + } + + @inlinable + mutating func next(_ id: _BroadcastSequenceStateMachine.Subscriptions.ID) -> OnNext { + let onNext: OnNext + let onNextForSubscription = self.subscriptions.withMutableElementID( + forSubscriber: id + ) { elementID -> (OnNext, Bool) in + let onNext: OnNext + let removeSubscription: Bool + + switch self.elements.lookupElement(withID: elementID) { + case .found(let element): + elementID.formNext() + onNext = .return(.init(nextResult: .success(element))) + removeSubscription = false + case .maybeAvailableLater: + onNext = .return(.init(nextResult: self.result.map { nil })) + removeSubscription = true + case .noLongerAvailable: + onNext = .return( + .init(nextResult: .failure(BroadcastAsyncSequenceError.consumingTooSlow)) + ) + removeSubscription = true + } + + return (onNext, removeSubscription) + } + + switch onNextForSubscription { + case .return(let result): + onNext = .return(result) + + case .none: + // No subscriber with the given ID, it was likely dropped previously. + if let index = self.subscriptionsToDrop.firstIndex(where: { $0 == id }) { + self.subscriptionsToDrop.remove(at: index) + onNext = .return( + .init(nextResult: .failure(BroadcastAsyncSequenceError.consumingTooSlow)) + ) + } else { + // Unknown subscriber, i.e. already finished. + onNext = .return(.init(nextResult: .success(nil))) + } + + case .suspend: + fatalError("Internal inconsistency") + } + + return onNext + } + + @inlinable + mutating func subscribe() -> _BroadcastSequenceStateMachine.Subscriptions.ID { + self.subscriptions.subscribe() + } + + @inlinable + func nextSubscriptionIsValid() -> Bool { + self.elements.lowestID == .initial + } + } + } + + @usableFromInline + var _state: State + + @inlinable + init(bufferSize: Int) { + self._state = State(bufferSize: bufferSize) + } + + @inlinable + var nextSubscriptionIsValid: Bool { + let isValid: Bool + + switch self._state { + case .initial: + isValid = true + case .subscribed: + isValid = true + case .streaming(let state): + isValid = state.nextSubscriptionIsValid() + case .finished(let state): + isValid = state.nextSubscriptionIsValid() + case ._modifying: + fatalError("Internal inconsistency") + } + + return isValid + } + + @usableFromInline + enum OnInvalidateAllSubscriptions { + case resume(ConsumerContinuations) + case none + } + + @inlinable + mutating func invalidateAllSubscriptions() -> OnInvalidateAllSubscriptions { + let onCancel: OnInvalidateAllSubscriptions + + switch self._state { + case .initial: + onCancel = .none + + case .subscribed(var state): + onCancel = state.invalidateAllSubscriptions() + self._state = .subscribed(state) + + case .streaming(var state): + onCancel = state.invalidateAllSubscriptions() + self._state = .streaming(state) + + case .finished: + onCancel = .none + + case ._modifying: + fatalError("Internal inconsistency") + } + + return onCancel + } + + @usableFromInline + enum OnYield { + case none + case suspend(Int) + case resume(ConsumerContinuations) + case throwAlreadyFinished + } + + @inlinable + mutating func yield(_ element: Element) -> OnYield { + let onYield: OnYield + + switch self._state { + case .initial(let state): + self._state = ._modifying + // Move to streaming. + var state = State.Streaming(from: state) + onYield = state.append(element) + self._state = .streaming(state) + + case .subscribed(let state): + self._state = ._modifying + var state = State.Streaming(from: state) + onYield = state.append(element) + self._state = .streaming(state) + + case .streaming(var state): + self._state = ._modifying + onYield = state.append(element) + self._state = .streaming(state) + + case .finished: + onYield = .throwAlreadyFinished + + case ._modifying: + fatalError("Internal inconsistency") + } + + return onYield + } + + @usableFromInline + enum OnFinish { + case none + case resume(ConsumerContinuations, ProducerContinuations) + } + + @inlinable + mutating func finish(result: Result) -> OnFinish { + let onFinish: OnFinish + + switch self._state { + case .initial(let state): + self._state = ._modifying + let state = State.Finished(from: state, result: result) + self._state = .finished(state) + onFinish = .none + + case .subscribed(var state): + self._state = ._modifying + onFinish = state.finish(result: result) + self._state = .finished(State.Finished(from: state, result: result)) + + case .streaming(var state): + self._state = ._modifying + onFinish = state.finish(result: result) + self._state = .finished(State.Finished(from: state, result: result)) + + case .finished: + onFinish = .none + + case ._modifying: + fatalError("Internal inconsistency") + } + + return onFinish + } + + @usableFromInline + enum OnNext { + @usableFromInline + struct ReturnAndResumeProducers { + @usableFromInline + var nextResult: Result + @usableFromInline + var producers: ProducerContinuations + + @inlinable + init( + nextResult: Result, + producers: [ProducerContinuation] = [], + producerResult: Result = .success(()) + ) { + self.nextResult = nextResult + self.producers = ProducerContinuations(continuations: producers, result: producerResult) + } + } + + case `return`(ReturnAndResumeProducers) + case suspend + } + + @inlinable + mutating func nextElement( + forSubscriber id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnNext { + let onNext: OnNext + + switch self._state { + case .initial: + // No subscribers so demand isn't possible. + fatalError("Internal inconsistency") + + case .subscribed(var state): + self._state = ._modifying + onNext = state.next(id) + self._state = .subscribed(state) + + case .streaming(var state): + self._state = ._modifying + onNext = state.next(id) + self._state = .streaming(state) + + case .finished(var state): + self._state = ._modifying + onNext = state.next(id) + self._state = .finished(state) + + case ._modifying: + fatalError("Internal inconsistency") + } + + return onNext + } + + @usableFromInline + enum OnSetContinuation { + case none + case resume(ConsumerContinuation, Result) + } + + @inlinable + mutating func setContinuation( + _ continuation: ConsumerContinuation, + forSubscription id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnSetContinuation { + let onSetContinuation: OnSetContinuation + + switch self._state { + case .initial: + // No subscribers so demand isn't possible. + fatalError("Internal inconsistency") + + case .subscribed(var state): + self._state = ._modifying + onSetContinuation = state.setContinuation(continuation, forSubscription: id) + self._state = .subscribed(state) + + case .streaming(var state): + self._state = ._modifying + onSetContinuation = state.setContinuation(continuation, forSubscription: id) + self._state = .streaming(state) + + case .finished, ._modifying: + // All values must have been produced, nothing to wait for. + fatalError("Internal inconsistency") + } + + return onSetContinuation + } + + @usableFromInline + enum OnCancelSubscription { + case none + case resume(ConsumerContinuation, Result) + } + + @inlinable + mutating func cancelSubscription( + withID id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnCancelSubscription { + let onCancel: OnCancelSubscription + + switch self._state { + case .initial: + // No subscribers so demand isn't possible. + fatalError("Internal inconsistency") + + case .subscribed(var state): + self._state = ._modifying + onCancel = state.cancel(id) + self._state = .subscribed(state) + + case .streaming(var state): + self._state = ._modifying + onCancel = state.cancel(id) + self._state = .streaming(state) + + case .finished, ._modifying: + // All values must have been produced, nothing to wait for. + fatalError("Internal inconsistency") + } + + return onCancel + } + + @usableFromInline + enum OnSubscribe { + case subscribed(_BroadcastSequenceStateMachine.Subscriptions.ID) + } + + @inlinable + mutating func subscribe() -> _BroadcastSequenceStateMachine.Subscriptions.ID { + let id: _BroadcastSequenceStateMachine.Subscriptions.ID + + switch self._state { + case .initial(let state): + self._state = ._modifying + var state = State.Subscribed(from: state) + id = state.subscribe() + self._state = .subscribed(state) + + case .subscribed(var state): + self._state = ._modifying + id = state.subscribe() + self._state = .subscribed(state) + + case .streaming(var state): + self._state = ._modifying + id = state.subscribe() + self._state = .streaming(state) + + case .finished(var state): + self._state = ._modifying + id = state.subscribe() + self._state = .finished(state) + + case ._modifying: + fatalError("Internal inconsistency") + } + + return id + } + + @usableFromInline + enum OnWaitToProduceMore { + case none + case resume(ProducerContinuation, Result) + } + + @inlinable + mutating func waitToProduceMore( + continuation: ProducerContinuation, + token: Int + ) -> OnWaitToProduceMore { + let onWaitToProduceMore: OnWaitToProduceMore + + switch self._state { + case .initial, .subscribed: + // Nothing produced yet, so no reason have to wait to produce. + fatalError("Internal inconsistency") + + case .streaming(var state): + self._state = ._modifying + onWaitToProduceMore = state.waitToProduceMore(continuation, token: token) + self._state = .streaming(state) + + case .finished: + onWaitToProduceMore = .resume(continuation, .success(())) + + case ._modifying: + fatalError("Internal inconsistency") + } + + return onWaitToProduceMore + } + + @usableFromInline + typealias OnCancelProducer = OnWaitToProduceMore + + @inlinable + mutating func cancelProducer(withToken token: Int) -> OnCancelProducer { + let onCancelProducer: OnCancelProducer + + switch self._state { + case .initial, .subscribed: + // Nothing produced yet, so no reason have to wait to produce. + fatalError("Internal inconsistency") + + case .streaming(var state): + self._state = ._modifying + onCancelProducer = state.cancelProducer(withToken: token) + self._state = .streaming(state) + + case .finished: + // No producers to cancel; do nothing. + onCancelProducer = .none + + case ._modifying: + fatalError("Internal inconsistency") + } + + return onCancelProducer + } + + @usableFromInline + enum OnDropResources { + case none + case resume(ConsumerContinuations, ProducerContinuations) + } + + @inlinable + mutating func dropResources() -> OnDropResources { + let error = BroadcastAsyncSequenceError.productionAlreadyFinished + let onDrop: OnDropResources + + switch self._state { + case .initial(let state): + self._state = ._modifying + onDrop = .none + self._state = .finished(State.Finished(from: state, result: .failure(error))) + + case .subscribed(var state): + self._state = ._modifying + onDrop = state.dropResources(error: error) + self._state = .finished(State.Finished(from: state, result: .failure(error))) + + case .streaming(var state): + self._state = ._modifying + onDrop = state.dropResources(error: error) + self._state = .finished(State.Finished(from: state, result: .failure(error))) + + case .finished: + onDrop = .none + + case ._modifying: + fatalError("Internal inconsistency") + } + + return onDrop + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension _BroadcastSequenceStateMachine { + /// A collection of elements tagged with an identifier. + /// + /// Identifiers are assigned when elements are added to the collection and are monotonically + /// increasing. If element 'A' is added before element 'B' then 'A' will have a lower ID than 'B'. + @usableFromInline + struct Elements: Sendable { + /// The ID of an element + @usableFromInline + struct ID: Hashable, Sendable, Comparable, Strideable { + @usableFromInline + private(set) var rawValue: Int + + @usableFromInline + static var initial: Self { + ID(id: 0) + } + + private init(id: Int) { + self.rawValue = id + } + + @inlinable + mutating func formNext() { + self.rawValue += 1 + } + + @inlinable + func next() -> Self { + var copy = self + copy.formNext() + return copy + } + + @inlinable + func distance(to other: Self) -> Int { + other.rawValue - self.rawValue + } + + @inlinable + func advanced(by n: Int) -> Self { + var copy = self + copy.rawValue += n + return copy + } + + @inlinable + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue < rhs.rawValue + } + } + + @usableFromInline + struct _IdentifiableElement: Sendable { + @usableFromInline + var element: Element + @usableFromInline + var id: ID + + @inlinable + init(element: Element, id: ID) { + self.element = element + self.id = id + } + } + + @usableFromInline + var _elements: Deque<_IdentifiableElement> + @usableFromInline + var _nextID: ID + + @inlinable + init() { + self._nextID = .initial + self._elements = [] + } + + @inlinable + mutating func nextElementID() -> ID { + let id = self._nextID + self._nextID.formNext() + return id + } + + /// The highest ID of the stored elements; `nil` if there are no elements. + @inlinable + var highestID: ID? { self._elements.last?.id } + + /// The lowest ID of the stored elements; `nil` if there are no elements. + @inlinable + var lowestID: ID? { self._elements.first?.id } + + /// The number of stored elements. + @inlinable + var count: Int { self._elements.count } + + /// Whether there are no stored elements. + @inlinable + var isEmpty: Bool { self._elements.isEmpty } + + /// Appends an element to the collection. + @inlinable + mutating func append(_ element: Element) { + self._elements.append(_IdentifiableElement(element: element, id: self.nextElementID())) + } + + /// Removes the first element from the collection. + @discardableResult + @inlinable + mutating func removeFirst() -> Element { + let removed = self._elements.removeFirst() + return removed.element + } + + @usableFromInline + enum ElementLookup { + /// The element was found in the collection. + case found(Element) + /// The element isn't in the collection, but it could be in the future. + case maybeAvailableLater + /// The element was in the collection, but is no longer available. + case noLongerAvailable + } + + /// Lookup the element with the given ID. + /// + /// - Parameter id: The ID of the element to lookup. + @inlinable + mutating func lookupElement(withID id: ID) -> ElementLookup { + guard let low = self.lowestID, let high = self.highestID else { + // Must be empty. + return id >= self._nextID ? .maybeAvailableLater : .noLongerAvailable + } + assert(low <= high) + + let lookup: ElementLookup + + if id < low { + lookup = .noLongerAvailable + } else if id > high { + lookup = .maybeAvailableLater + } else { + // IDs are monotonically increasing. If the buffer contains the tag we can use it to index + // into the deque by looking at the offsets. + let offset = low.distance(to: id) + let index = self._elements.startIndex.advanced(by: offset) + lookup = .found(self._elements[index].element) + } + + return lookup + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension _BroadcastSequenceStateMachine { + /// A collection of subcriptions. + @usableFromInline + struct Subscriptions: Sendable { + @usableFromInline + struct ID: Hashable, Sendable { + @usableFromInline + private(set) var rawValue: Int + + @inlinable + init() { + self.rawValue = 0 + } + + @inlinable + mutating func formNext() { + self.rawValue += 1 + } + + @inlinable + func next() -> Self { + var copy = self + copy.formNext() + return copy + } + } + + @usableFromInline + struct _Subscriber: Sendable { + /// The ID of the subscriber. + @usableFromInline + var id: ID + + /// The ID of the next element the subscriber will consume. + @usableFromInline + var nextElementID: _BroadcastSequenceStateMachine.Elements.ID + + /// A continuation which which will be resumed when the next element becomes available. + @usableFromInline + var continuation: ConsumerContinuation? + + @inlinable + init( + id: ID, + nextElementID: _BroadcastSequenceStateMachine.Elements.ID, + continuation: ConsumerContinuation? = nil + ) { + self.id = id + self.nextElementID = nextElementID + self.continuation = continuation + } + + /// Returns and sets the continuation to `nil` if one exists. + /// + /// The next element ID is advanced if a contination exists. + /// + /// - Returns: The continuation, if one existed. + @inlinable + mutating func takeContinuation() -> ConsumerContinuation? { + guard let continuation = self.continuation else { return nil } + self.continuation = nil + self.nextElementID.formNext() + return continuation + } + } + + @usableFromInline + var _subscribers: [_Subscriber] + @usableFromInline + var _nextSubscriberID: ID + + @inlinable + init() { + self._subscribers = [] + self._nextSubscriberID = ID() + } + + /// Returns the number of subscribers. + @inlinable + var count: Int { self._subscribers.count } + + /// Returns whether the collection is empty. + @inlinable + var isEmpty: Bool { self._subscribers.isEmpty } + + /// Adds a new subscriber and returns its unique ID. + /// + /// - Returns: The ID of the new subscriber. + @inlinable + mutating func subscribe() -> ID { + let id = self._nextSubscriberID + self._nextSubscriberID.formNext() + self._subscribers.append(_Subscriber(id: id, nextElementID: .initial)) + return id + } + + /// Provides mutable access to the element ID of the given subscriber, if it exists. + /// + /// - Parameters: + /// - id: The ID of the subscriber. + /// - body: A closure to mutate the element ID of the subscriber which returns the result and + /// a boolean indicating whether the subscriber should be removed. + /// - Returns: The result returned from the closure or `nil` if no subscriber exists with the + /// given ID. + @inlinable + mutating func withMutableElementID( + forSubscriber id: ID, + _ body: ( + inout _BroadcastSequenceStateMachine.Elements.ID + ) -> (result: R, removeSubscription: Bool) + ) -> R? { + guard let index = self._subscribers.firstIndex(where: { $0.id == id }) else { return nil } + let (result, removeSubscription) = body(&self._subscribers[index].nextElementID) + if removeSubscription { + self._subscribers.remove(at: index) + } + return result + } + + /// Sets the continuation for the subscription with the given ID. + /// - Parameters: + /// - continuation: The continuation to set. + /// - id: The ID of the subscriber. + /// - Returns: A boolean indicating whether the continuation was set or not. + @inlinable + mutating func setContinuation( + _ continuation: ConsumerContinuation, + forSubscriber id: ID + ) -> Bool { + guard let index = self._subscribers.firstIndex(where: { $0.id == id }) else { + return false + } + + assert(self._subscribers[index].continuation == nil) + self._subscribers[index].continuation = continuation + return true + } + + /// Returns an array of subscriber IDs which are whose next element ID is `id`. + @inlinable + func subscribers( + withElementID id: _BroadcastSequenceStateMachine.Elements.ID + ) -> [ID] { + return self._subscribers.filter { + $0.nextElementID == id + }.map { + $0.id + } + } + + /// Removes the subscriber with the given ID. + /// - Parameter id: The ID of the subscriber to remove. + /// - Returns: A tuple indicating whether a subscriber was removed and any continuation + /// associated with the subscriber. + @inlinable + mutating func removeSubscriber(withID id: ID) -> (Bool, ConsumerContinuation?) { + guard let index = self._subscribers.firstIndex(where: { $0.id == id }) else { + return (false, nil) + } + + let continuation = self._subscribers[index].continuation + self._subscribers.remove(at: index) + return (true, continuation) + } + + /// Remove all subscribers in the given array of IDs. + @inlinable + mutating func removeAllSubscribers(in idsToRemove: [ID]) { + self._subscribers.removeAll { + idsToRemove.contains($0.id) + } + } + + /// Remove all subscribers and return their IDs. + @inlinable + mutating func removeAllSubscribers() -> [ID] { + let subscribers = self._subscribers.map { $0.id } + self._subscribers.removeAll() + return subscribers + } + + /// Returns any continuations set on subscribers, unsetting at the same time. + @inlinable + mutating func takeContinuations() -> _OneOrMany? { + // Avoid allocs if there's only one subscriber. + let count = self._countPendingContinuations() + let result: _OneOrMany? + + switch count { + case 0: + result = nil + + case 1: + let index = self._subscribers.firstIndex(where: { $0.continuation != nil })! + let continuation = self._subscribers[index].takeContinuation()! + result = .one(continuation) + + default: + var continuations = [ConsumerContinuation]() + continuations.reserveCapacity(count) + + for index in self._subscribers.indices { + if let continuation = self._subscribers[index].takeContinuation() { + continuations.append(continuation) + } + } + + result = .many(continuations) + } + + return result + } + + /// Removes all subscribers which have continuations and return their continuations. + @inlinable + mutating func removeSubscribersWithContinuations() -> _OneOrMany? { + // Avoid allocs if there's only one subscriber. + let count = self._countPendingContinuations() + let result: _OneOrMany? + + switch count { + case 0: + result = nil + + case 1: + let index = self._subscribers.firstIndex(where: { $0.continuation != nil })! + let subscription = self._subscribers.remove(at: index) + result = .one(subscription.continuation!) + + default: + var continuations = [ConsumerContinuation]() + continuations.reserveCapacity(count) + var removable = [ID]() + removable.reserveCapacity(count) + + for subscription in self._subscribers { + if let continuation = subscription.continuation { + continuations.append(continuation) + removable.append(subscription.id) + } + } + + self._subscribers.removeAll { + removable.contains($0.id) + } + + result = .many(continuations) + } + + return result + } + + @inlinable + func _countPendingContinuations() -> Int { + return self._subscribers.reduce(into: 0) { count, subscription in + if subscription.continuation != nil { + count += 1 + } + } + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension _BroadcastSequenceStateMachine { + // TODO: tiny array + @usableFromInline + enum _OneOrMany { + case one(Value) + case many([Value]) + } +} diff --git a/Sources/GRPCCore/Stream/RPCAsyncSequence.swift b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift similarity index 100% rename from Sources/GRPCCore/Stream/RPCAsyncSequence.swift rename to Sources/GRPCCore/Streaming/RPCAsyncSequence.swift diff --git a/Sources/GRPCCore/Stream/RPCWriter.swift b/Sources/GRPCCore/Streaming/RPCWriter.swift similarity index 100% rename from Sources/GRPCCore/Stream/RPCWriter.swift rename to Sources/GRPCCore/Streaming/RPCWriter.swift diff --git a/Sources/GRPCCore/Stream/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift similarity index 88% rename from Sources/GRPCCore/Stream/RPCWriterProtocol.swift rename to Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index fecb5df04..6de7722c5 100644 --- a/Sources/GRPCCore/Stream/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -42,7 +42,7 @@ extension RPCWriterProtocol { /// /// - Parameter elements: The elements to write. public func write( - _ elements: Elements + contentsOf elements: Elements ) async throws where Elements.Element == Element { for try await element in elements { try await self.write(element) @@ -57,4 +57,10 @@ public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// All writes after ``finish()`` has been called should result in an error /// being thrown. func finish() + + /// Indicate to the writer that no more writes are to be accepted because an error occurred. + /// + /// All writes after ``finish(throwing:)`` has been called should result in an error + /// being thrown. + func finish(throwing error: Error) } diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index 5444075fe..ff668f9f2 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@_spi(Testing) import GRPCCore + import XCTest +@testable import GRPCCore + final class ClientResponseTests: XCTestCase { func testAcceptedSingleResponseConvenienceMethods() { let response = ClientResponse.Single( diff --git a/Tests/GRPCCoreTests/Stream/AsyncSequenceOfOne.swift b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift similarity index 97% rename from Tests/GRPCCoreTests/Stream/AsyncSequenceOfOne.swift rename to Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift index 00f9d4410..6b29ca4a2 100644 --- a/Tests/GRPCCoreTests/Stream/AsyncSequenceOfOne.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@_spi(Testing) import GRPCCore + import XCTest +@testable import GRPCCore + internal final class AsyncSequenceOfOneTests: XCTestCase { func testSuccessPath() async throws { let sequence = RPCAsyncSequence.one("foo") diff --git a/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift b/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift new file mode 100644 index 000000000..6195977c6 --- /dev/null +++ b/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift @@ -0,0 +1,257 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +final class BroadcastAsyncSequenceTests: XCTestCase { + func testSingleSubscriberToEmptyStream() async throws { + let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + source.finish() + let elements = try await stream.collect() + XCTAssertEqual(elements, []) + } + + func testMultipleSubscribersToEmptyStream() async throws { + let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + source.finish() + do { + let elements = try await stream.collect() + XCTAssertEqual(elements, []) + } + do { + let elements = try await stream.collect() + XCTAssertEqual(elements, []) + } + } + + func testSubscribeToEmptyStreamBeforeFinish() async throws { + let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + var iterator = stream.makeAsyncIterator() + source.finish() + let element = try await iterator.next() + XCTAssertNil(element) + } + + func testSlowConsumerIsLeftBehind() async throws { + let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + var consumer1 = stream.makeAsyncIterator() + var consumer2 = stream.makeAsyncIterator() + + for element in 0 ..< 15 { + try await source.write(element) + } + + // Buffer should now be full. Consume with one consumer so that the other is dropped on + // the next yield. + let element = try await consumer1.next() + XCTAssertEqual(element, 0) + + // Will invalidate consumer2 as the slowest consumer. + try await source.write(15) + + await XCTAssertThrowsErrorAsync { + try await consumer2.next() + } errorHandler: { error in + XCTAssertEqual(error as? BroadcastAsyncSequenceError, .consumingTooSlow) + } + + // consumer1 should be free to continue. + for expected in 1 ... 15 { + let element = try await consumer1.next() + XCTAssertEqual(element, expected) + } + + // consumer1 should end as expected. + source.finish() + let end = try await consumer1.next() + XCTAssertNil(end) + } + + func testConsumerJoiningAfterSomeElements() async throws { + let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + for element in 0 ..< 10 { + try await source.write(element) + } + + var consumer1 = stream.makeAsyncIterator() + do { + for expected in 0 ..< 8 { + let element = try await consumer1.next() + XCTAssertEqual(element, expected) + } + } + + // Add a second consumer, consume the first four elements. + var consumer2 = stream.makeAsyncIterator() + do { + for expected in 0 ..< 4 { + let element = try await consumer2.next() + XCTAssertEqual(element, expected) + } + } + + // Add another consumer, consume the first two elements. + var consumer3 = stream.makeAsyncIterator() + do { + for expected in 0 ..< 2 { + let element = try await consumer3.next() + XCTAssertEqual(element, expected) + } + } + + // Advance each consumer in lock-step. + for offset in 0 ..< 10 { + try await source.write(10 + offset) + let element1 = try await consumer1.next() + XCTAssertEqual(element1, 8 + offset) + let element2 = try await consumer2.next() + XCTAssertEqual(element2, 4 + offset) + let element3 = try await consumer3.next() + XCTAssertEqual(element3, 2 + offset) + } + + // Subscribing isn't possible. + await XCTAssertThrowsErrorAsync { + try await stream.collect() + } errorHandler: { error in + XCTAssertEqual(error as? BroadcastAsyncSequenceError, .consumingTooSlow) + } + + source.finish() + + // All elements are present. The existing consumers can finish however they choose. + do { + for expected in 18 ..< 20 { + let element = try await consumer1.next() + XCTAssertEqual(element, expected) + } + let end = try await consumer1.next() + XCTAssertNil(end) + } + + do { + for expected in 14 ..< 20 { + let element = try await consumer2.next() + XCTAssertEqual(element, expected) + } + let end = try await consumer2.next() + XCTAssertNil(end) + } + + do { + for expected in 12 ..< 20 { + let element = try await consumer3.next() + XCTAssertEqual(element, expected) + } + let end = try await consumer3.next() + XCTAssertNil(end) + } + } + + func testInvalidateAllConsumersForSingleConcurrentConsumer() async throws { + let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + for element in 0 ..< 10 { + try await source.write(element) + } + + var consumer1 = stream.makeAsyncIterator() + stream.invalidateAllSubscriptions() + await XCTAssertThrowsErrorAsync { + try await consumer1.next() + } errorHandler: { error in + XCTAssertEqual(error as? BroadcastAsyncSequenceError, .consumingTooSlow) + } + + // Subscribe, consume one, then cancel. + var consumer2 = stream.makeAsyncIterator() + do { + let value = try await consumer2.next() + XCTAssertEqual(value, 0) + } + stream.invalidateAllSubscriptions() + await XCTAssertThrowsErrorAsync { + try await consumer2.next() + } errorHandler: { error in + XCTAssertEqual(error as? BroadcastAsyncSequenceError, .consumingTooSlow) + } + } + + func testInvalidateAllConsumersForMultipleConcurrentConsumer() async throws { + let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + for element in 0 ..< 10 { + try await source.write(element) + } + + let consumers: [BroadcastAsyncSequence.AsyncIterator] = (0 ..< 5).map { _ in + stream.makeAsyncIterator() + } + + for var consumer in consumers { + let value = try await consumer.next() + XCTAssertEqual(value, 0) + } + + stream.invalidateAllSubscriptions() + + for var consumer in consumers { + await XCTAssertThrowsErrorAsync { + try await consumer.next() + } errorHandler: { error in + XCTAssertEqual(error as? BroadcastAsyncSequenceError, .consumingTooSlow) + } + } + } + + func testCancelSubscriber() async throws { + let (stream, _) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + await withTaskGroup(of: Void.self) { group in + group.cancelAll() + group.addTask { + do { + _ = try await stream.collect() + XCTFail() + } catch { + XCTAssert(error is CancellationError) + } + } + } + } + + func testCancelProducer() async throws { + let (_, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) + for i in 0 ..< 15 { + try await source.write(i) + } + + try await withThrowingTaskGroup(of: Void.self) { group in + group.cancelAll() + for _ in 0 ..< 10 { + group.addTask { + try await source.write(42) + } + } + + while let result = await group.nextResult() { + XCTAssertThrowsError(try result.get()) { error in + XCTAssert(error is CancellationError) + } + } + } + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index aa51a4b98..3c1cbaf70 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -25,6 +25,18 @@ func XCTAssertDescription( XCTAssertEqual(String(describing: subject), expected, file: file, line: line) } +func XCTAssertThrowsErrorAsync( + _ expression: () async throws -> T, + errorHandler: (Error) -> Void +) async { + do { + _ = try await expression() + XCTFail("Expression didn't throw") + } catch { + errorHandler(error) + } +} + func XCTAssertThrowsRPCError( _ expression: @autoclosure () throws -> T, _ errorHandler: (RPCError) -> Void From f37ce30f21b5b86201b39eb2c7d44cc965fb90af Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 26 Oct 2023 14:57:38 +0100 Subject: [PATCH 143/580] Add a retry throttle (#1689) Motivation: To implement retries and hedging, transports need to be able to throttle attempts to svoid overloading servers. The uses a token based system where successful requests, ones which end with non-retryable status code, add tokens and those which fail remove tokens from the system. Successful requests add 1 token, failed requests remove `tokenRatio` tokens (typically less than 1). Modification: - Implement a retry throttle - Add a requirement to the `ClientTransport` Result: Retries can be throttled. --- .../GRPCCore/Transport/ClientTransport.swift | 7 + .../GRPCCore/Transport/RetryThrottle.swift | 124 ++++++++++++++++++ .../Transport/RetryThrottleTests.swift | 98 ++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 Sources/GRPCCore/Transport/RetryThrottle.swift create mode 100644 Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 8a012205d..968d949b6 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -19,6 +19,13 @@ public protocol ClientTransport: Sendable { associatedtype Inbound: (AsyncSequence & Sendable) where Inbound.Element == RPCResponsePart associatedtype Outbound: ClosableRPCWriterProtocol + /// Returns a throttle which gRPC uses to determine whether retries can be executed. + /// + /// Client transports don't need to implement the throttle or interact with it beyond its + /// creation. gRPC will record the results of requests to determine whether retries can be + /// performed. + var retryThrottle: RetryThrottle { get } + /// Establish and maintain a connection to the remote destination. /// /// Maintains a long-lived connection, or set of connections, to a remote destination. diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift new file mode 100644 index 000000000..c57b614a9 --- /dev/null +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -0,0 +1,124 @@ +/* + * Copyright 2023, 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. + */ + +/// A throttle used to rate-limit retries and hedging attempts. +/// +/// gRPC prevents servers from being overloaded by retries and hedging by using a token-based +/// throttling mechanism at the transport level. +/// +/// Each client transport maintains a throttle for the server it is connected to and gRPC records +/// successful and failed RPC attempts. Successful attempts increment the number of tokens +/// by ``tokenRatio`` and failed attempts decrement the available tokens by one. In the context +/// of throttling, a failed attempt is one where the server terminates the RPC with a status code +/// which is retryable or non fatal (as defined by ``RetryPolicy/retryableStatusCodes`` and +/// ``HedgingPolicy/nonFatalStatusCodes``) or when the client receives a pushback response from +/// the server. +/// +/// See also [gRFC A6: client retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +public struct RetryThrottle: Sendable { + // Note: only three figures after the decimal point from the original token ratio are used so + // all computation is done a scaled number of tokens (tokens * 1000). This allows us to do all + // computation in integer space. + + /// The number of tokens available, multiplied by 1000. + private let scaledTokensAvailable: LockedValueBox + /// The number of tokens, multiplied by 1000. + private let scaledTokenRatio: Int + /// The maximum number of tokens, multiplied by 1000. + private let scaledMaximumTokens: Int + /// The retry threshold, multiplied by 1000. If ``scaledTokensAvailable`` is above this then + /// retries are permitted. + private let scaledRetryThreshold: Int + + /// Returns the throttling token ratio. + /// + /// The number of tokens held by the throttle is incremented by this value for each successful + /// response. In the context of throttling, a successful response is one which: + /// - receives metadata from the server, or + /// - is terminated with a non-retryable or fatal status code. + /// + /// If the response is a pushback response then it is not considered to be successful, even if + /// either of the preceding conditions are met. + public var tokenRatio: Double { + Double(self.scaledTokenRatio) / 1000 + } + + /// The maximum number of tokens the throttle may hold. + public var maximumTokens: Int { + self.scaledMaximumTokens / 1000 + } + + /// The number of tokens the throttle currently has. + /// + /// If this value is less than or equal to the retry threshold (defined as `maximumTokens / 2`) + /// then RPCs will not be retried and hedging will be disabled. + public var tokens: Double { + self.scaledTokensAvailable.withLockedValue { + Double($0) / 1000 + } + } + + /// Returns whether retries and hedging are permitted at this time. + public var isRetryPermitted: Bool { + self.scaledTokensAvailable.withLockedValue { + $0 > self.scaledRetryThreshold + } + } + + /// Create a new throttle. + /// + /// - Parameters: + /// - maximumTokens: The maximum number of tokens available. Must be in the range `1...1000`. + /// - tokenRatio: The number of tokens to increment the available tokens by for successful + /// responses. See the documentation on this type for a description of what counts as a + /// successful response. Note that only three decimal places are used from this value. + /// - Precondition: `maximumTokens` must be in the range `1...1000`. + /// - Precondition: `tokenRatio` must be `>= 0.001`. + public init(maximumTokens: Int, tokenRatio: Double) { + precondition( + (1 ... 1000).contains(maximumTokens), + "maximumTokens must be in the range 1...1000 (is \(maximumTokens))" + ) + + let scaledTokenRatio = Int(tokenRatio * 1000) + precondition(scaledTokenRatio > 0, "tokenRatio must be >= 0.001 (is \(tokenRatio))") + + let scaledTokens = maximumTokens * 1000 + self.scaledMaximumTokens = scaledTokens + self.scaledRetryThreshold = scaledTokens / 2 + self.scaledTokenRatio = scaledTokenRatio + self.scaledTokensAvailable = LockedValueBox(scaledTokens) + } + + /// Records a success, adding a token to the throttle. + @usableFromInline + func recordSuccess() { + self.scaledTokensAvailable.withLockedValue { value in + value = min(self.scaledMaximumTokens, value &+ self.scaledTokenRatio) + } + } + + /// Records a failure, removing tokens from the throttle. + /// - Returns: Whether retries will now be throttled. + @usableFromInline + @discardableResult + func recordFailure() -> Bool { + self.scaledTokensAvailable.withLockedValue { value in + value = max(0, value &- 1000) + return value <= self.scaledRetryThreshold + } + } +} diff --git a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift new file mode 100644 index 000000000..2d6ea9f19 --- /dev/null +++ b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift @@ -0,0 +1,98 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +final class RetryThrottleTests: XCTestCase { + func testThrottleOnInit() { + let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + // Start with max tokens, so permitted. + XCTAssertTrue(throttle.isRetryPermitted) + XCTAssertEqual(throttle.maximumTokens, 10) + XCTAssertEqual(throttle.tokens, 10) + XCTAssertEqual(throttle.tokenRatio, 0.1) + } + + func testThrottleIgnoresMoreThanThreeDecimals() { + let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1239) + XCTAssertEqual(throttle.tokenRatio, 0.123) + } + + func testFailureReducesTokens() { + let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + XCTAssertEqual(throttle.tokens, 10) + XCTAssert(throttle.isRetryPermitted) + + throttle.recordFailure() + XCTAssertEqual(throttle.tokens, 9) + XCTAssert(throttle.isRetryPermitted) + + throttle.recordFailure() + XCTAssertEqual(throttle.tokens, 8) + XCTAssert(throttle.isRetryPermitted) + + throttle.recordFailure() + XCTAssertEqual(throttle.tokens, 7) + XCTAssert(throttle.isRetryPermitted) + + throttle.recordFailure() + XCTAssertEqual(throttle.tokens, 6) + XCTAssert(throttle.isRetryPermitted) + + // Drop to threshold, retries no longer allowed. + throttle.recordFailure() + XCTAssertEqual(throttle.tokens, 5) + XCTAssertFalse(throttle.isRetryPermitted) + } + + func testTokensCantDropBelowZero() { + let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + for _ in 0 ..< 1000 { + throttle.recordFailure() + XCTAssertGreaterThanOrEqual(throttle.tokens, 0) + } + XCTAssertEqual(throttle.tokens, 0) + } + + func testSuccessIncreasesTokens() { + let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + + // Drop to zero. + for _ in 0 ..< 10 { + throttle.recordFailure() + } + XCTAssertEqual(throttle.tokens, 0) + + // Start recording successes. + throttle.recordSuccess() + XCTAssertEqual(throttle.tokens, 0.1) + + throttle.recordSuccess() + XCTAssertEqual(throttle.tokens, 0.2) + + throttle.recordSuccess() + XCTAssertEqual(throttle.tokens, 0.3) + } + + func testTokensCantRiseAboveMax() { + let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + XCTAssertEqual(throttle.tokens, 10) + throttle.recordSuccess() + XCTAssertEqual(throttle.tokens, 10) + } +} From b58291e034371763d8fec42ad917c29f064b0cee Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 26 Oct 2023 17:37:24 +0100 Subject: [PATCH 144/580] Add Metadata collection (#1683) --- NOTICES.txt | 19 + Sources/GRPCCore/Internal/Base64.swift | 605 ++++++++++++++++++ .../Lock.swift | 0 .../GRPCCore/Internal/String+Extensions.swift | 80 +++ Sources/GRPCCore/Metadata.swift | 454 ++++++++++++- Tests/GRPCCoreTests/MetadataTests.swift | 208 ++++++ Tests/GRPCTests/GRPCTestCase.swift | 2 +- 7 files changed, 1365 insertions(+), 3 deletions(-) create mode 100644 Sources/GRPCCore/Internal/Base64.swift rename Sources/GRPCCore/Internal/{Concurrency Primities => Concurrency Primitives}/Lock.swift (100%) create mode 100644 Sources/GRPCCore/Internal/String+Extensions.swift create mode 100644 Tests/GRPCCoreTests/MetadataTests.swift diff --git a/NOTICES.txt b/NOTICES.txt index cdd00a5ec..63039c3e3 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -31,6 +31,16 @@ It also uses derivations of SwiftNIO's lock 'NIOLock.swift' and locked value box --- +This product uses derivations of SwiftNIOHTTP2's implementation of case +insensitive comparison of strings, found in 'HPACKHeader.swift'. + + * LICENSE (Apache License 2.0): + * https://github.com/apple/swift-nio-http2/blob/main/LICENSE.txt + * HOMEPAGE: + * https://github.com/apple/swift-nio-http2 + +--- + This product contains a derivation of the backpressure aware async stream from the Swift project. @@ -38,3 +48,12 @@ the Swift project. * https://github.com/apple/swift/blob/main/LICENSE.txt * HOMEPAGE: * https://github.com/apple/swift + +--- + +This product uses derivations of swift-extras/swift-extras-base64 'Base64.swift'. + + * LICENSE (Apache License 2.0): + * https://github.com/swift-extras/swift-extras-base64/blob/main/LICENSE + * HOMEPAGE: + * https://github.com/swift-extras/swift-extras-base64 diff --git a/Sources/GRPCCore/Internal/Base64.swift b/Sources/GRPCCore/Internal/Base64.swift new file mode 100644 index 000000000..6265b996b --- /dev/null +++ b/Sources/GRPCCore/Internal/Base64.swift @@ -0,0 +1,605 @@ +/* + * Copyright 2023, 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. + */ +// This base64 implementation is heavily inspired by: + +// https://github.com/lemire/fastbase64/blob/master/src/chromiumbase64.c +/* + Copyright (c) 2015-2016, Wojciech Muła, Alfred Klomp, Daniel Lemire + (Unless otherwise stated in the source code) + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// https://github.com/client9/stringencoders/blob/master/src/modp_b64.c +/* + The MIT License (MIT) + + Copyright (c) 2016 Nick Galbreath + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +internal enum Base64 {} + +extension Base64 { + internal struct DecodingOptions: OptionSet { + internal let rawValue: UInt + internal init(rawValue: UInt) { self.rawValue = rawValue } + + internal static let base64UrlAlphabet = DecodingOptions(rawValue: UInt(1 << 0)) + internal static let omitPaddingCharacter = DecodingOptions(rawValue: UInt(1 << 1)) + } + + internal enum DecodingError: Error, Equatable { + case invalidLength + case invalidCharacter(UInt8) + case unexpectedPaddingCharacter + case unexpectedEnd + } + + internal static func decode( + string encoded: String, + options: DecodingOptions = [] + ) throws -> [UInt8] { + let decoded = try encoded.utf8.withContiguousStorageIfAvailable { + (characterPointer) -> [UInt8] in + guard characterPointer.count > 0 else { + return [] + } + + let outputLength = ((characterPointer.count + 3) / 4) * 3 + + return try characterPointer.withMemoryRebound(to: UInt8.self) { (input) -> [UInt8] in + try [UInt8](unsafeUninitializedCapacity: outputLength) { output, length in + try Self._decodeChromium(from: input, into: output, length: &length, options: options) + } + } + } + + if decoded != nil { + return decoded! + } + + var encoded = encoded + encoded.makeContiguousUTF8() + return try Self.decode(string: encoded, options: options) + } + + private static func _decodeChromium( + from inBuffer: UnsafeBufferPointer, + into outBuffer: UnsafeMutableBufferPointer, + length: inout Int, + options: DecodingOptions = [] + ) throws { + let remaining = inBuffer.count % 4 + switch (options.contains(.omitPaddingCharacter), remaining) { + case (false, 1...): + throw DecodingError.invalidLength + case (true, 1): + throw DecodingError.invalidLength + default: + // everythin alright so far + break + } + + let outputLength = ((inBuffer.count + 3) / 4) * 3 + let fullchunks = remaining == 0 ? inBuffer.count / 4 - 1 : inBuffer.count / 4 + guard outBuffer.count >= outputLength else { + preconditionFailure("Expected the out buffer to be at least as long as outputLength") + } + + try Self.withUnsafeDecodingTablesAsBufferPointers(options: options) { d0, d1, d2, d3 in + var outIndex = 0 + if fullchunks > 0 { + for chunk in 0 ..< fullchunks { + let inIndex = chunk * 4 + let a0 = inBuffer[inIndex] + let a1 = inBuffer[inIndex + 1] + let a2 = inBuffer[inIndex + 2] + let a3 = inBuffer[inIndex + 3] + var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2)] | d3[Int(a3)] + + if x >= Self.badCharacter { + // TODO: Inspect characters here better + throw DecodingError.invalidCharacter(inBuffer[inIndex]) + } + + withUnsafePointer(to: &x) { ptr in + ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in + outBuffer[outIndex] = newPtr[0] + outBuffer[outIndex + 1] = newPtr[1] + outBuffer[outIndex + 2] = newPtr[2] + outIndex += 3 + } + } + } + } + + // inIndex is the first index in the last chunk + let inIndex = fullchunks * 4 + let a0 = inBuffer[inIndex] + let a1 = inBuffer[inIndex + 1] + var a2: UInt8? + var a3: UInt8? + if inIndex + 2 < inBuffer.count, inBuffer[inIndex + 2] != Self.encodePaddingCharacter { + a2 = inBuffer[inIndex + 2] + } + if inIndex + 3 < inBuffer.count, inBuffer[inIndex + 3] != Self.encodePaddingCharacter { + a3 = inBuffer[inIndex + 3] + } + + var x: UInt32 = d0[Int(a0)] | d1[Int(a1)] | d2[Int(a2 ?? 65)] | d3[Int(a3 ?? 65)] + if x >= Self.badCharacter { + // TODO: Inspect characters here better + throw DecodingError.invalidCharacter(inBuffer[inIndex]) + } + + withUnsafePointer(to: &x) { ptr in + ptr.withMemoryRebound(to: UInt8.self, capacity: 4) { newPtr in + outBuffer[outIndex] = newPtr[0] + outIndex += 1 + if a2 != nil { + outBuffer[outIndex] = newPtr[1] + outIndex += 1 + } + if a3 != nil { + outBuffer[outIndex] = newPtr[2] + outIndex += 1 + } + } + } + + length = outIndex + } + } + + static func withUnsafeDecodingTablesAsBufferPointers( + options: Base64.DecodingOptions, + _ body: ( + UnsafeBufferPointer, UnsafeBufferPointer, UnsafeBufferPointer, + UnsafeBufferPointer + ) throws -> R + ) rethrows -> R { + let decoding0 = options.contains(.base64UrlAlphabet) ? Self.decoding0url : Self.decoding0 + let decoding1 = options.contains(.base64UrlAlphabet) ? Self.decoding1url : Self.decoding1 + let decoding2 = options.contains(.base64UrlAlphabet) ? Self.decoding2url : Self.decoding2 + let decoding3 = options.contains(.base64UrlAlphabet) ? Self.decoding3url : Self.decoding3 + + assert(decoding0.count == 256) + assert(decoding1.count == 256) + assert(decoding2.count == 256) + assert(decoding3.count == 256) + + return try decoding0.withUnsafeBufferPointer { (d0) -> R in + try decoding1.withUnsafeBufferPointer { (d1) -> R in + try decoding2.withUnsafeBufferPointer { (d2) -> R in + try decoding3.withUnsafeBufferPointer { (d3) -> R in + try body(d0, d1, d2, d3) + } + } + } + } + } + + internal static let encodePaddingCharacter: UInt8 = 61 + static let badCharacter: UInt32 = 0x01FF_FFFF + + static let decoding0: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x0000_00F8, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_00FC, + 0x0000_00D0, 0x0000_00D4, 0x0000_00D8, 0x0000_00DC, 0x0000_00E0, 0x0000_00E4, + 0x0000_00E8, 0x0000_00EC, 0x0000_00F0, 0x0000_00F4, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, + 0x0000_0004, 0x0000_0008, 0x0000_000C, 0x0000_0010, 0x0000_0014, 0x0000_0018, + 0x0000_001C, 0x0000_0020, 0x0000_0024, 0x0000_0028, 0x0000_002C, 0x0000_0030, + 0x0000_0034, 0x0000_0038, 0x0000_003C, 0x0000_0040, 0x0000_0044, 0x0000_0048, + 0x0000_004C, 0x0000_0050, 0x0000_0054, 0x0000_0058, 0x0000_005C, 0x0000_0060, + 0x0000_0064, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x0000_0068, 0x0000_006C, 0x0000_0070, 0x0000_0074, 0x0000_0078, + 0x0000_007C, 0x0000_0080, 0x0000_0084, 0x0000_0088, 0x0000_008C, 0x0000_0090, + 0x0000_0094, 0x0000_0098, 0x0000_009C, 0x0000_00A0, 0x0000_00A4, 0x0000_00A8, + 0x0000_00AC, 0x0000_00B0, 0x0000_00B4, 0x0000_00B8, 0x0000_00BC, 0x0000_00C0, + 0x0000_00C4, 0x0000_00C8, 0x0000_00CC, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] + + static let decoding1: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x0000_E003, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_F003, + 0x0000_4003, 0x0000_5003, 0x0000_6003, 0x0000_7003, 0x0000_8003, 0x0000_9003, + 0x0000_A003, 0x0000_B003, 0x0000_C003, 0x0000_D003, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, + 0x0000_1000, 0x0000_2000, 0x0000_3000, 0x0000_4000, 0x0000_5000, 0x0000_6000, + 0x0000_7000, 0x0000_8000, 0x0000_9000, 0x0000_A000, 0x0000_B000, 0x0000_C000, + 0x0000_D000, 0x0000_E000, 0x0000_F000, 0x0000_0001, 0x0000_1001, 0x0000_2001, + 0x0000_3001, 0x0000_4001, 0x0000_5001, 0x0000_6001, 0x0000_7001, 0x0000_8001, + 0x0000_9001, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x0000_A001, 0x0000_B001, 0x0000_C001, 0x0000_D001, 0x0000_E001, + 0x0000_F001, 0x0000_0002, 0x0000_1002, 0x0000_2002, 0x0000_3002, 0x0000_4002, + 0x0000_5002, 0x0000_6002, 0x0000_7002, 0x0000_8002, 0x0000_9002, 0x0000_A002, + 0x0000_B002, 0x0000_C002, 0x0000_D002, 0x0000_E002, 0x0000_F002, 0x0000_0003, + 0x0000_1003, 0x0000_2003, 0x0000_3003, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] + + static let decoding2: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x0080_0F00, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x00C0_0F00, + 0x0000_0D00, 0x0040_0D00, 0x0080_0D00, 0x00C0_0D00, 0x0000_0E00, 0x0040_0E00, + 0x0080_0E00, 0x00C0_0E00, 0x0000_0F00, 0x0040_0F00, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, + 0x0040_0000, 0x0080_0000, 0x00C0_0000, 0x0000_0100, 0x0040_0100, 0x0080_0100, + 0x00C0_0100, 0x0000_0200, 0x0040_0200, 0x0080_0200, 0x00C0_0200, 0x0000_0300, + 0x0040_0300, 0x0080_0300, 0x00C0_0300, 0x0000_0400, 0x0040_0400, 0x0080_0400, + 0x00C0_0400, 0x0000_0500, 0x0040_0500, 0x0080_0500, 0x00C0_0500, 0x0000_0600, + 0x0040_0600, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x0080_0600, 0x00C0_0600, 0x0000_0700, 0x0040_0700, 0x0080_0700, + 0x00C0_0700, 0x0000_0800, 0x0040_0800, 0x0080_0800, 0x00C0_0800, 0x0000_0900, + 0x0040_0900, 0x0080_0900, 0x00C0_0900, 0x0000_0A00, 0x0040_0A00, 0x0080_0A00, + 0x00C0_0A00, 0x0000_0B00, 0x0040_0B00, 0x0080_0B00, 0x00C0_0B00, 0x0000_0C00, + 0x0040_0C00, 0x0080_0C00, 0x00C0_0C00, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] + + static let decoding3: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x003E_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x003F_0000, + 0x0034_0000, 0x0035_0000, 0x0036_0000, 0x0037_0000, 0x0038_0000, 0x0039_0000, + 0x003A_0000, 0x003B_0000, 0x003C_0000, 0x003D_0000, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, + 0x0001_0000, 0x0002_0000, 0x0003_0000, 0x0004_0000, 0x0005_0000, 0x0006_0000, + 0x0007_0000, 0x0008_0000, 0x0009_0000, 0x000A_0000, 0x000B_0000, 0x000C_0000, + 0x000D_0000, 0x000E_0000, 0x000F_0000, 0x0010_0000, 0x0011_0000, 0x0012_0000, + 0x0013_0000, 0x0014_0000, 0x0015_0000, 0x0016_0000, 0x0017_0000, 0x0018_0000, + 0x0019_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x001A_0000, 0x001B_0000, 0x001C_0000, 0x001D_0000, 0x001E_0000, + 0x001F_0000, 0x0020_0000, 0x0021_0000, 0x0022_0000, 0x0023_0000, 0x0024_0000, + 0x0025_0000, 0x0026_0000, 0x0027_0000, 0x0028_0000, 0x0029_0000, 0x002A_0000, + 0x002B_0000, 0x002C_0000, 0x002D_0000, 0x002E_0000, 0x002F_0000, 0x0030_0000, + 0x0031_0000, 0x0032_0000, 0x0033_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] + + static let decoding0url: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 18 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 24 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 30 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 36 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_00F8, 0x01FF_FFFF, 0x01FF_FFFF, // 42 + 0x0000_00D0, 0x0000_00D4, 0x0000_00D8, 0x0000_00DC, 0x0000_00E0, 0x0000_00E4, // 48 + 0x0000_00E8, 0x0000_00EC, 0x0000_00F0, 0x0000_00F4, 0x01FF_FFFF, 0x01FF_FFFF, // 54 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, // 60 + 0x0000_0004, 0x0000_0008, 0x0000_000C, 0x0000_0010, 0x0000_0014, 0x0000_0018, // 66 + 0x0000_001C, 0x0000_0020, 0x0000_0024, 0x0000_0028, 0x0000_002C, 0x0000_0030, // 72 + 0x0000_0034, 0x0000_0038, 0x0000_003C, 0x0000_0040, 0x0000_0044, 0x0000_0048, // 78 + 0x0000_004C, 0x0000_0050, 0x0000_0054, 0x0000_0058, 0x0000_005C, 0x0000_0060, // 84 + 0x0000_0064, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_00FC, // 90 + 0x01FF_FFFF, 0x0000_0068, 0x0000_006C, 0x0000_0070, 0x0000_0074, 0x0000_0078, + 0x0000_007C, 0x0000_0080, 0x0000_0084, 0x0000_0088, 0x0000_008C, 0x0000_0090, + 0x0000_0094, 0x0000_0098, 0x0000_009C, 0x0000_00A0, 0x0000_00A4, 0x0000_00A8, + 0x0000_00AC, 0x0000_00B0, 0x0000_00B4, 0x0000_00B8, 0x0000_00BC, 0x0000_00C0, + 0x0000_00C4, 0x0000_00C8, 0x0000_00CC, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] + + static let decoding1url: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 18 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 24 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 30 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 36 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_E003, 0x01FF_FFFF, 0x01FF_FFFF, // 42 + 0x0000_4003, 0x0000_5003, 0x0000_6003, 0x0000_7003, 0x0000_8003, 0x0000_9003, // 48 + 0x0000_A003, 0x0000_B003, 0x0000_C003, 0x0000_D003, 0x01FF_FFFF, 0x01FF_FFFF, // 54 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, // 60 + 0x0000_1000, 0x0000_2000, 0x0000_3000, 0x0000_4000, 0x0000_5000, 0x0000_6000, // 66 + 0x0000_7000, 0x0000_8000, 0x0000_9000, 0x0000_A000, 0x0000_B000, 0x0000_C000, // 72 + 0x0000_D000, 0x0000_E000, 0x0000_F000, 0x0000_0001, 0x0000_1001, 0x0000_2001, // 78 + 0x0000_3001, 0x0000_4001, 0x0000_5001, 0x0000_6001, 0x0000_7001, 0x0000_8001, // 84 + 0x0000_9001, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_F003, // 90 + 0x01FF_FFFF, 0x0000_A001, 0x0000_B001, 0x0000_C001, 0x0000_D001, 0x0000_E001, + 0x0000_F001, 0x0000_0002, 0x0000_1002, 0x0000_2002, 0x0000_3002, 0x0000_4002, + 0x0000_5002, 0x0000_6002, 0x0000_7002, 0x0000_8002, 0x0000_9002, 0x0000_A002, + 0x0000_B002, 0x0000_C002, 0x0000_D002, 0x0000_E002, 0x0000_F002, 0x0000_0003, + 0x0000_1003, 0x0000_2003, 0x0000_3003, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] + + static let decoding2url: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 18 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 24 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 30 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 36 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0080_0F00, 0x01FF_FFFF, 0x01FF_FFFF, // 42 + 0x0000_0D00, 0x0040_0D00, 0x0080_0D00, 0x00C0_0D00, 0x0000_0E00, 0x0040_0E00, // 48 + 0x0080_0E00, 0x00C0_0E00, 0x0000_0F00, 0x0040_0F00, 0x01FF_FFFF, 0x01FF_FFFF, // 54 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, // 60 + 0x0040_0000, 0x0080_0000, 0x00C0_0000, 0x0000_0100, 0x0040_0100, 0x0080_0100, // 66 + 0x00C0_0100, 0x0000_0200, 0x0040_0200, 0x0080_0200, 0x00C0_0200, 0x0000_0300, // 72 + 0x0040_0300, 0x0080_0300, 0x00C0_0300, 0x0000_0400, 0x0040_0400, 0x0080_0400, // 78 + 0x00C0_0400, 0x0000_0500, 0x0040_0500, 0x0080_0500, 0x00C0_0500, 0x0000_0600, // 84 + 0x0040_0600, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x00C0_0F00, // 90 + 0x01FF_FFFF, 0x0080_0600, 0x00C0_0600, 0x0000_0700, 0x0040_0700, 0x0080_0700, + 0x00C0_0700, 0x0000_0800, 0x0040_0800, 0x0080_0800, 0x00C0_0800, 0x0000_0900, + 0x0040_0900, 0x0080_0900, 0x00C0_0900, 0x0000_0A00, 0x0040_0A00, 0x0080_0A00, + 0x00C0_0A00, 0x0000_0B00, 0x0040_0B00, 0x0080_0B00, 0x00C0_0B00, 0x0000_0C00, + 0x0040_0C00, 0x0080_0C00, 0x00C0_0C00, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] + + static let decoding3url: [UInt32] = [ + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 18 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 24 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 30 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 36 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x003E_0000, 0x01FF_FFFF, 0x01FF_FFFF, // 42 + 0x0034_0000, 0x0035_0000, 0x0036_0000, 0x0037_0000, 0x0038_0000, 0x0039_0000, // 48 + 0x003A_0000, 0x003B_0000, 0x003C_0000, 0x003D_0000, 0x01FF_FFFF, 0x01FF_FFFF, // 54 + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x0000_0000, // 60 + 0x0001_0000, 0x0002_0000, 0x0003_0000, 0x0004_0000, 0x0005_0000, 0x0006_0000, // 66 + 0x0007_0000, 0x0008_0000, 0x0009_0000, 0x000A_0000, 0x000B_0000, 0x000C_0000, // 72 + 0x000D_0000, 0x000E_0000, 0x000F_0000, 0x0010_0000, 0x0011_0000, 0x0012_0000, // 78 + 0x0013_0000, 0x0014_0000, 0x0015_0000, 0x0016_0000, 0x0017_0000, 0x0018_0000, // 84 + 0x0019_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x003F_0000, // 90 + 0x01FF_FFFF, 0x001A_0000, 0x001B_0000, 0x001C_0000, 0x001D_0000, 0x001E_0000, + 0x001F_0000, 0x0020_0000, 0x0021_0000, 0x0022_0000, 0x0023_0000, 0x0024_0000, + 0x0025_0000, 0x0026_0000, 0x0027_0000, 0x0028_0000, 0x0029_0000, 0x002A_0000, + 0x002B_0000, 0x002C_0000, 0x002D_0000, 0x002E_0000, 0x002F_0000, 0x0030_0000, + 0x0031_0000, 0x0032_0000, 0x0033_0000, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, + ] +} diff --git a/Sources/GRPCCore/Internal/Concurrency Primities/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift similarity index 100% rename from Sources/GRPCCore/Internal/Concurrency Primities/Lock.swift rename to Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift diff --git a/Sources/GRPCCore/Internal/String+Extensions.swift b/Sources/GRPCCore/Internal/String+Extensions.swift new file mode 100644 index 000000000..f230c1ffe --- /dev/null +++ b/Sources/GRPCCore/Internal/String+Extensions.swift @@ -0,0 +1,80 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension UInt8 { + @inlinable + var isASCII: Bool { + return self <= 127 + } +} + +extension String.UTF8View { + /// Compares two UTF8 strings as case insensitive ASCII bytes. + /// + /// - Parameter bytes: The string constant in the form of a collection of `UInt8` + /// - Returns: Whether the collection contains **EXACTLY** this array or no, but by ignoring case. + @inlinable + func compareCaseInsensitiveASCIIBytes(to other: String.UTF8View) -> Bool { + // fast path: we can get the underlying bytes of both + let maybeMaybeResult = self.withContiguousStorageIfAvailable { lhsBuffer -> Bool? in + other.withContiguousStorageIfAvailable { rhsBuffer in + if lhsBuffer.count != rhsBuffer.count { + return false + } + + for idx in 0 ..< lhsBuffer.count { + // let's hope this gets vectorised ;) + if lhsBuffer[idx] & 0xdf != rhsBuffer[idx] & 0xdf && lhsBuffer[idx].isASCII { + return false + } + } + return true + } + } + + if let maybeResult = maybeMaybeResult, let result = maybeResult { + return result + } else { + return self._compareCaseInsensitiveASCIIBytesSlowPath(to: other) + } + } + + @inlinable + @inline(never) + func _compareCaseInsensitiveASCIIBytesSlowPath(to other: String.UTF8View) -> Bool { + return self.elementsEqual(other, by: { return (($0 & 0xdf) == ($1 & 0xdf) && $0.isASCII) }) + } +} + +extension String { + @inlinable + func isEqualCaseInsensitiveASCIIBytes(to: String) -> Bool { + return self.utf8.compareCaseInsensitiveASCIIBytes(to: to.utf8) + } +} diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 5a72fd161..0b2e560f8 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -14,5 +14,455 @@ * limitations under the License. */ -// FIXME: placeholder. -public typealias Metadata = [String: String] +/// A collection of metadata key-value pairs, found in RPC streams. +/// +/// Metadata is a side channel associated with an RPC, that allows you to send information between clients +/// and servers. Metadata is stored as a list of key-value pairs where keys aren't required to be unique; +/// a single key may have multiple values associated with it. +/// +/// Keys are case-insensitive ASCII strings. Values may be ASCII strings or binary data. The keys +/// for binary data should end with "-bin": this will be asserted when adding a new binary value. +/// Keys must not be prefixed with "grpc-" as these are reserved for gRPC. +/// +/// # Using Metadata +/// +/// You can add values to ``Metadata`` using the ``addString(_:forKey:)`` and +/// ``addBinary(_:forKey:)`` methods: +/// +/// ```swift +/// var metadata = Metadata() +/// metadata.addString("value", forKey: "key") +/// metadata.addBinary([118, 97, 108, 117, 101], forKey: "key-bin") +/// ``` +/// +/// As ``Metadata`` conforms to `RandomAccessCollection` you can iterate over its values. +/// Because metadata can store strings and binary values, its `Element` type is an `enum` representing +/// both possibilities: +/// +/// ```swift +/// for (key, value) in metadata { +/// switch value { +/// case .string(let value): +/// print("'\(key)' has a string value: '\(value)'") +/// case .binary(let value): +/// print("'\(key)' has a binary value: '\(value)'") +/// } +/// } +/// ``` +/// +/// You can also iterate over the values for a specific key: +/// +/// ```swift +/// for value in metadata["key"] { +/// switch value { +/// case .string(let value): +/// print("'key' has a string value: '\(value)'") +/// case .binary(let value): +/// print("'key' has a binary value: '\(value)'") +/// } +/// } +/// ``` +/// +/// You can get only string or binary values for a key using ``subscript(stringValues:)`` and +/// ``subscript(binaryValues:)``: +/// +/// ```swift +/// for value in metadata[stringValues: "key"] { +/// print("'key' has a string value: '\(value)'") +/// } +/// +/// for value in metadata[binaryValues: "key"] { +/// print("'key' has a binary value: '\(value)'") +/// } +/// ``` +/// +/// - Note: Binary values are encoded as base64 strings when they are sent over the wire, so keys with +/// the "-bin" suffix may have string values (rather than binary). These are deserialized automatically when +/// using ``subscript(binaryValues:)``. +public struct Metadata: Sendable, Hashable { + + /// A metadata value. It can either be a simple string, or binary data. + public enum Value: Sendable, Hashable { + case string(String) + case binary([UInt8]) + } + + /// A metadata key-value pair. + internal struct KeyValuePair: Sendable, Hashable { + internal let key: String + internal let value: Value + + /// Constructor for a metadata key-value pair. + /// + /// - Parameters: + /// - key: The key for the key-value pair. + /// - value: The value to be associated to the given key. If it's a binary value, then the associated + /// key must end in "-bin", otherwise, this method will produce an assertion failure. + init(key: String, value: Value) { + if case .binary = value { + assert(key.hasSuffix("-bin"), "Keys for binary values must end in -bin") + } + self.key = key + self.value = value + } + } + + private var elements: [KeyValuePair] + + /// The Metadata collection's capacity. + public var capacity: Int { + self.elements.capacity + } + + /// Initialize an empty Metadata collection. + public init() { + self.elements = [] + } + + /// Reserve the specified minimum capacity in the collection. + /// + /// - Parameter minimumCapacity: The minimum capacity to reserve in the collection. + public mutating func reserveCapacity(_ minimumCapacity: Int) { + self.elements.reserveCapacity(minimumCapacity) + } + + /// Add a new key-value pair, where the value is a string. + /// + /// - Parameters: + /// - stringValue: The string value to be associated with the given key. + /// - key: The key to be associated with the given value. + public mutating func addString(_ stringValue: String, forKey key: String) { + self.addValue(.string(stringValue), forKey: key) + } + + /// Add a new key-value pair, where the value is binary data, in the form of `[UInt8]`. + /// + /// - Parameters: + /// - binaryValue: The binary data (i.e., `[UInt8]`) to be associated with the given key. + /// - key: The key to be associated with the given value. Must end in "-bin". + public mutating func addBinary(_ binaryValue: [UInt8], forKey key: String) { + self.addValue(.binary(binaryValue), forKey: key) + } + + /// Add a new key-value pair. + /// + /// - Parameters: + /// - value: The ``Value`` to be associated with the given key. + /// - key: The key to be associated with the given value. If value is binary, it must end in "-bin". + internal mutating func addValue(_ value: Value, forKey key: String) { + self.elements.append(.init(key: key, value: value)) + } + + /// Removes all values associated with the given key. + /// + /// - Parameter key: The key for which all values should be removed. + /// + /// - Complexity: O(*n*), where *n* is the number of entries in the metadata instance. + public mutating func removeAllValues(forKey key: String) { + elements.removeAll { metadataKeyValue in + metadataKeyValue.key.isEqualCaseInsensitiveASCIIBytes(to: key) + } + } + + /// Adds a key-value pair to the collection, where the value is a string. + /// + /// If there are pairs already associated to the given key, they will all be removed first, and the new pair + /// will be added. If no pairs are present with the given key, a new one will be added. + /// + /// - Parameters: + /// - stringValue: The string value to be associated with the given key. + /// - key: The key to be associated with the given value. + /// + /// - Complexity: O(*n*), where *n* is the number of entries in the metadata instance. + public mutating func replaceOrAddString(_ stringValue: String, forKey key: String) { + self.replaceOrAddValue(.string(stringValue), forKey: key) + } + + /// Adds a key-value pair to the collection, where the value is `[UInt8]`. + /// + /// If there are pairs already associated to the given key, they will all be removed first, and the new pair + /// will be added. If no pairs are present with the given key, a new one will be added. + /// + /// - Parameters: + /// - binaryValue: The `[UInt8]` to be associated with the given key. + /// - key: The key to be associated with the given value. Must end in "-bin". + /// + /// - Complexity: O(*n*), where *n* is the number of entries in the metadata instance. + public mutating func replaceOrAddBinary(_ binaryValue: [UInt8], forKey key: String) { + self.replaceOrAddValue(.binary(binaryValue), forKey: key) + } + + /// Adds a key-value pair to the collection. + /// + /// If there are pairs already associated to the given key, they will all be removed first, and the new pair + /// will be added. If no pairs are present with the given key, a new one will be added. + /// + /// - Parameters: + /// - value: The ``Value`` to be associated with the given key. + /// - key: The key to be associated with the given value. If value is binary, it must end in "-bin". + /// + /// - Complexity: O(*n*), where *n* is the number of entries in the metadata instance. + internal mutating func replaceOrAddValue(_ value: Value, forKey key: String) { + self.removeAllValues(forKey: key) + self.elements.append(.init(key: key, value: value)) + } + + /// Removes all key-value pairs from this metadata instance. + /// + /// - Parameter keepingCapacity: Whether the current capacity should be kept or reset. + /// + /// - Complexity: O(*n*), where *n* is the number of entries in the metadata instance. + public mutating func removeAll(keepingCapacity: Bool) { + self.elements.removeAll(keepingCapacity: keepingCapacity) + } +} + +extension Metadata: RandomAccessCollection { + public typealias Element = (key: String, value: Value) + + public struct Index: Comparable, Sendable { + @usableFromInline + let _base: Array.Index + + @inlinable + init(_base: Array.Index) { + self._base = _base + } + + @inlinable + public static func < (lhs: Index, rhs: Index) -> Bool { + return lhs._base < rhs._base + } + } + + public var startIndex: Index { + return .init(_base: self.elements.startIndex) + } + + public var endIndex: Index { + return .init(_base: self.elements.endIndex) + } + + public func index(before i: Index) -> Index { + return .init(_base: self.elements.index(before: i._base)) + } + + public func index(after i: Index) -> Index { + return .init(_base: self.elements.index(after: i._base)) + } + + public subscript(position: Index) -> Element { + let keyValuePair = self.elements[position._base] + return (key: keyValuePair.key, value: keyValuePair.value) + } +} + +extension Metadata { + + /// A sequence of metadata values for a given key. + public struct Values: Sequence { + + /// An iterator for all metadata ``Value``s associated with a given key. + public struct Iterator: IteratorProtocol { + private var metadataIterator: Metadata.Iterator + private let key: String + + init(forKey key: String, metadata: Metadata) { + self.metadataIterator = metadata.makeIterator() + self.key = key + } + + public mutating func next() -> Value? { + while let nextKeyValue = self.metadataIterator.next() { + if nextKeyValue.key.isEqualCaseInsensitiveASCIIBytes(to: self.key) { + return nextKeyValue.value + } + } + return nil + } + } + + private let key: String + private let metadata: Metadata + + internal init(key: String, metadata: Metadata) { + self.key = key + self.metadata = metadata + } + + public func makeIterator() -> Iterator { + Iterator(forKey: self.key, metadata: self.metadata) + } + } + + /// Get a ``Values`` sequence for a given key. + /// + /// - Parameter key: The returned sequence will only return values for this key. + /// + /// - Returns: A sequence containing all values for the given key. + public subscript(_ key: String) -> Values { + Values(key: key, metadata: self) + } +} + +extension Metadata { + + /// A sequence of metadata string values for a given key. + public struct StringValues: Sequence { + + /// An iterator for all string values associated with a given key. + /// + /// This iterator will only return values originally stored as strings for a given key. + public struct Iterator: IteratorProtocol { + private var values: Values.Iterator + + init(values: Values) { + self.values = values.makeIterator() + } + + public mutating func next() -> String? { + while let value = self.values.next() { + switch value { + case .string(let stringValue): + return stringValue + case .binary: + continue + } + } + return nil + } + } + + private let key: String + private let metadata: Metadata + + internal init(key: String, metadata: Metadata) { + self.key = key + self.metadata = metadata + } + + public func makeIterator() -> Iterator { + Iterator(values: Values(key: self.key, metadata: self.metadata)) + } + } + + /// Get a ``StringValues`` sequence for a given key. + /// + /// - Parameter key: The returned sequence will only return string values for this key. + /// + /// - Returns: A sequence containing string values for the given key. + public subscript(stringValues key: String) -> StringValues { + StringValues(key: key, metadata: self) + } +} + +extension Metadata { + + /// A sequence of metadata binary values for a given key. + public struct BinaryValues: Sequence { + + /// An iterator for all binary data values associated with a given key. + /// + /// This iterator will return values originally stored as binary data for a given key, and will also try to + /// decode values stored as strings as if they were base64-encoded strings. + public struct Iterator: IteratorProtocol { + private var values: Values.Iterator + + init(values: Values) { + self.values = values.makeIterator() + } + + public mutating func next() -> [UInt8]? { + while let value = self.values.next() { + switch value { + case .string(let stringValue): + do { + return try Base64.decode(string: stringValue) + } catch { + continue + } + case .binary(let binaryValue): + return binaryValue + } + } + return nil + } + } + + private let key: String + private let metadata: Metadata + + internal init(key: String, metadata: Metadata) { + self.key = key + self.metadata = metadata + } + + public func makeIterator() -> Iterator { + Iterator(values: Values(key: self.key, metadata: self.metadata)) + } + } + + /// A subscript to get a ``BinaryValues`` sequence for a given key. + /// + /// As it's iterated, this sequence will return values originally stored as binary data for a given key, and will + /// also try to decode values stored as strings as if they were base64-encoded strings; only strings that + /// are successfully decoded will be returned. + /// + /// - Parameter key: The returned sequence will only return binary (i.e. `[UInt8]`) values for this key. + /// + /// - Returns: A sequence containing binary (i.e. `[UInt8]`) values for the given key. + /// + /// - SeeAlso: ``BinaryValues/Iterator``. + public subscript(binaryValues key: String) -> BinaryValues { + BinaryValues(key: key, metadata: self) + } +} + +extension Metadata: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, Value)...) { + self.elements = elements.map { KeyValuePair(key: $0, value: $1) } + } +} + +extension Metadata: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: (String, Value)...) { + self.elements = elements.map { KeyValuePair(key: $0, value: $1) } + } +} + +extension Metadata.Value: ExpressibleByStringLiteral { + public init(stringLiteral value: StringLiteralType) { + self = .string(value) + } +} + +extension Metadata.Value: ExpressibleByStringInterpolation { + public init(stringInterpolation: DefaultStringInterpolation) { + self = .string(String(stringInterpolation: stringInterpolation)) + } +} + +extension Metadata.Value: ExpressibleByArrayLiteral { + public typealias ArrayLiteralElement = UInt8 + + public init(arrayLiteral elements: ArrayLiteralElement...) { + self = .binary(elements) + } +} + +extension Metadata: CustomStringConvertible { + public var description: String { + String(describing: self.map({ ($0.key, $0.value) })) + } +} + +extension Metadata.Value: CustomStringConvertible { + public var description: String { + switch self { + case .string(let stringValue): + return String(describing: stringValue) + case .binary(let binaryValue): + return String(describing: binaryValue) + } + } +} diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift new file mode 100644 index 000000000..fca7125b0 --- /dev/null +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -0,0 +1,208 @@ +/* + * Copyright 2023, 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 MetadataTests: XCTestCase { + func testAddStringValue() { + var metadata = Metadata() + XCTAssertTrue(metadata.isEmpty) + + metadata.addString("testValue", forKey: "testString") + XCTAssertEqual(metadata.count, 1) + + let sequence = metadata[stringValues: "testString"] + var iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), "testValue") + XCTAssertNil(iterator.next()) + } + + func testAddBinaryValue() { + var metadata = Metadata() + XCTAssertTrue(metadata.isEmpty) + + metadata.addBinary(Array("base64encodedString".utf8), forKey: "testBinary-bin") + XCTAssertEqual(metadata.count, 1) + + let sequence = metadata[binaryValues: "testBinary-bin"] + var iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), Array("base64encodedString".utf8)) + XCTAssertNil(iterator.next()) + } + + func testCreateFromDictionaryLiteral() { + let metadata: Metadata = [ + "testKey": "stringValue", + "testKey-bin": .binary(Array("base64encodedString".utf8)), + ] + XCTAssertEqual(metadata.count, 2) + + let stringSequence = metadata[stringValues: "testKey"] + var stringIterator = stringSequence.makeIterator() + XCTAssertEqual(stringIterator.next(), "stringValue") + XCTAssertNil(stringIterator.next()) + + let binarySequence = metadata[binaryValues: "testKey-bin"] + var binaryIterator = binarySequence.makeIterator() + XCTAssertEqual(binaryIterator.next(), Array("base64encodedString".utf8)) + XCTAssertNil(binaryIterator.next()) + } + + func testReplaceOrAddValue() { + var metadata: Metadata = [ + "testKey": "value1", + "testKey": "value2", + ] + XCTAssertEqual(metadata.count, 2) + + var sequence = metadata[stringValues: "testKey"] + var iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), "value1") + XCTAssertEqual(iterator.next(), "value2") + XCTAssertNil(iterator.next()) + + metadata.replaceOrAddString("anotherValue", forKey: "testKey2") + XCTAssertEqual(metadata.count, 3) + sequence = metadata[stringValues: "testKey"] + iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), "value1") + XCTAssertEqual(iterator.next(), "value2") + XCTAssertNil(iterator.next()) + sequence = metadata[stringValues: "testKey2"] + iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), "anotherValue") + XCTAssertNil(iterator.next()) + + metadata.replaceOrAddString("newValue", forKey: "testKey") + XCTAssertEqual(metadata.count, 2) + sequence = metadata[stringValues: "testKey"] + iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), "newValue") + XCTAssertNil(iterator.next()) + sequence = metadata[stringValues: "testKey2"] + iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), "anotherValue") + XCTAssertNil(iterator.next()) + } + + func testReserveCapacity() { + var metadata = Metadata() + XCTAssertEqual(metadata.capacity, 0) + + metadata.reserveCapacity(10) + XCTAssertEqual(metadata.capacity, 10) + } + + func testValuesIteration() { + let metadata: Metadata = [ + "testKey-bin": "string1", + "testKey-bin": .binary(.init("data1".utf8)), + "testKey-bin": "string2", + "testKey-bin": .binary(.init("data2".utf8)), + "testKey-bin": "string3", + "testKey-bin": .binary(.init("data3".utf8)), + ] + XCTAssertEqual(metadata.count, 6) + + let sequence = metadata["testKey-bin"] + var iterator = sequence.makeIterator() + XCTAssertEqual(iterator.next(), .string("string1")) + XCTAssertEqual(iterator.next(), .binary(.init("data1".utf8))) + XCTAssertEqual(iterator.next(), .string("string2")) + XCTAssertEqual(iterator.next(), .binary(.init("data2".utf8))) + XCTAssertEqual(iterator.next(), .string("string3")) + XCTAssertEqual(iterator.next(), .binary(.init("data3".utf8))) + XCTAssertNil(iterator.next()) + } + + func testStringValuesIteration() { + let metadata: Metadata = [ + "testKey-bin": "string1", + "testKey-bin": .binary(.init("data1".utf8)), + "testKey-bin": "string2", + "testKey-bin": .binary(.init("data2".utf8)), + "testKey-bin": "string3", + "testKey-bin": .binary(.init("data3".utf8)), + ] + XCTAssertEqual(metadata.count, 6) + + let stringSequence = metadata[stringValues: "testKey-bin"] + var stringIterator = stringSequence.makeIterator() + XCTAssertEqual(stringIterator.next(), "string1") + XCTAssertEqual(stringIterator.next(), "string2") + XCTAssertEqual(stringIterator.next(), "string3") + XCTAssertNil(stringIterator.next()) + } + + func testBinaryValuesIteration_InvalidBase64EncodedStrings() { + let metadata: Metadata = [ + "testKey-bin": "invalidBase64-1", + "testKey-bin": .binary(.init("data1".utf8)), + "testKey-bin": "invalidBase64-2", + "testKey-bin": .binary(.init("data2".utf8)), + "testKey-bin": "invalidBase64-3", + "testKey-bin": .binary(.init("data3".utf8)), + ] + XCTAssertEqual(metadata.count, 6) + + let binarySequence = metadata[binaryValues: "testKey-bin"] + var binaryIterator = binarySequence.makeIterator() + XCTAssertEqual(binaryIterator.next(), Array("data1".utf8)) + XCTAssertEqual(binaryIterator.next(), Array("data2".utf8)) + XCTAssertEqual(binaryIterator.next(), Array("data3".utf8)) + XCTAssertNil(binaryIterator.next()) + } + + func testBinaryValuesIteration_ValidBase64EncodedStrings() { + let metadata: Metadata = [ + "testKey-bin": "c3RyaW5nMQ==", + "testKey-bin": .binary(.init("data1".utf8)), + "testKey-bin": "c3RyaW5nMg==", + "testKey-bin": .binary(.init("data2".utf8)), + "testKey-bin": "c3RyaW5nMw==", + "testKey-bin": .binary(.init("data3".utf8)), + ] + XCTAssertEqual(metadata.count, 6) + + let binarySequence = metadata[binaryValues: "testKey-bin"] + var binaryIterator = binarySequence.makeIterator() + XCTAssertEqual(binaryIterator.next(), Array("string1".utf8)) + XCTAssertEqual(binaryIterator.next(), Array("data1".utf8)) + XCTAssertEqual(binaryIterator.next(), Array("string2".utf8)) + XCTAssertEqual(binaryIterator.next(), Array("data2".utf8)) + XCTAssertEqual(binaryIterator.next(), Array("string3".utf8)) + XCTAssertEqual(binaryIterator.next(), Array("data3".utf8)) + XCTAssertNil(binaryIterator.next()) + } + + func testKeysAreCaseInsensitive() { + let metadata: Metadata = [ + "testkey1": "value1", + "TESTKEY2": "value2", + ] + XCTAssertEqual(metadata.count, 2) + + var stringSequence = metadata[stringValues: "TESTKEY1"] + var stringIterator = stringSequence.makeIterator() + XCTAssertEqual(stringIterator.next(), "value1") + XCTAssertNil(stringIterator.next()) + + stringSequence = metadata[stringValues: "testkey2"] + stringIterator = stringSequence.makeIterator() + XCTAssertEqual(stringIterator.next(), "value2") + XCTAssertNil(stringIterator.next()) + } +} diff --git a/Tests/GRPCTests/GRPCTestCase.swift b/Tests/GRPCTests/GRPCTestCase.swift index 0adba1cd3..3c9747265 100644 --- a/Tests/GRPCTests/GRPCTestCase.swift +++ b/Tests/GRPCTests/GRPCTestCase.swift @@ -38,7 +38,7 @@ class GRPCTestCase: XCTestCase { override func tearDown() { // Only print logs when there's a failure and we're *not* always logging (when we are always // logging, logs will be printed as they're caught). - if !GRPCTestCase.alwaysLog, self.testRun.map { $0.totalFailureCount > 0 } ?? false { + if !GRPCTestCase.alwaysLog, self.testRun.map({ $0.totalFailureCount > 0 }) ?? false { let logs = self.capturedLogs() self.printCapturedLogs(logs) } From cdb9fc616bfd6d72604190dbdce2af7801a283e6 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Fri, 27 Oct 2023 13:46:18 +0100 Subject: [PATCH 145/580] Added support for Error Responses from the Reflection Service (#1682) Motivation: Whenever an error occurs during the exchange between the server implementing the Reflection Service and a client, the client should receive an Error Message. Modifications: Replaced throwing errors with creating an Error Reponse and sending it back to the client. Also, changed the APIs to use the Result type and added new extensions for the Reflection_ServerReflectionResponse initialisation. Also modified the tests. Result: When an error occurs within the Reflection Service, the clients will be sent an Error Response. --- .../Server/ReflectionService.swift | 205 +++++++----- .../ReflectionServiceIntegrationTests.swift | 86 +++++ .../ReflectionServiceUnitTests.swift | 316 +++++++++++------- .../GRPCReflectionServiceTests/Utils.swift | 14 + 4 files changed, 412 insertions(+), 209 deletions(-) diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 35a3afc1f..190117e76 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -129,7 +129,7 @@ internal struct ReflectionServiceData: Sendable { internal func serialisedFileDescriptorProtosForDependenciesOfFile( named fileName: String - ) throws -> [Data] { + ) -> Result<[Data], GRPCStatus> { var toVisit = Deque() var visited = Set() var serializedFileDescriptorProtos: [Data] = [] @@ -147,37 +147,59 @@ internal struct ReflectionServiceData: Sendable { let serializedFileDescriptorProto = protoData.serializedFileDescriptorProto serializedFileDescriptorProtos.append(serializedFileDescriptorProto) } else { - throw GRPCStatus( - code: .notFound, - message: "The provided file or a dependency of the provided file could not be found." + return .failure( + GRPCStatus( + code: .notFound, + message: "The provided file or a dependency of the provided file could not be found." + ) ) } visited.insert(currentFileName) } - return serializedFileDescriptorProtos + return .success(serializedFileDescriptorProtos) } - internal func nameOfFileContainingSymbol(named symbolName: String) -> String? { - return self.fileNameBySymbol[symbolName] + internal func nameOfFileContainingSymbol(named symbolName: String) -> Result { + guard let fileName = self.fileNameBySymbol[symbolName] else { + return .failure( + GRPCStatus( + code: .notFound, + message: "The provided symbol could not be found." + ) + ) + } + return .success(fileName) } internal func nameOfFileContainingExtension( extendeeName: String, fieldNumber number: Int32 - ) -> String? { + ) -> Result { let key = ExtensionDescriptor(extendeeTypeName: extendeeName, fieldNumber: number) - return self.fileNameByExtensionDescriptor[key] + guard let fileName = self.fileNameByExtensionDescriptor[key] else { + return .failure( + GRPCStatus( + code: .notFound, + message: "The provided extension could not be found." + ) + ) + } + return .success(fileName) } // Returns an empty array if the type has no extensions. - internal func extensionsFieldNumbersOfType(named typeName: String) throws -> [Int32] { + internal func extensionsFieldNumbersOfType( + named typeName: String + ) -> Result<[Int32], GRPCStatus> { guard let fieldNumbers = self.fieldNumbersByType[typeName] else { - throw GRPCStatus( - code: .invalidArgument, - message: "The provided type is invalid." + return .failure( + GRPCStatus( + code: .invalidArgument, + message: "The provided type is invalid." + ) ) } - return fieldNumbers + return .success(fieldNumbers) } } @@ -191,17 +213,26 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync ) } + internal func _findFileByFileName( + _ fileName: String + ) -> Result { + return self.protoRegistry + .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) + .map { fileDescriptorProtos in + Reflection_ServerReflectionResponse.OneOf_MessageResponse.fileDescriptorResponse( + .with { + $0.fileDescriptorProto = fileDescriptorProtos + } + ) + } + } + internal func findFileByFileName( _ fileName: String, request: Reflection_ServerReflectionRequest - ) throws -> Reflection_ServerReflectionResponse { - return Reflection_ServerReflectionResponse( - request: request, - fileDescriptorResponse: try .with { - $0.fileDescriptorProto = try self.protoRegistry - .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) - } - ) + ) -> Reflection_ServerReflectionResponse { + let result = self._findFileByFileName(fileName) + return result.makeResponse(request: request) } internal func getServicesNames( @@ -215,53 +246,50 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync } return Reflection_ServerReflectionResponse( request: request, - listServicesResponse: listServicesResponse + messageResponse: .listServicesResponse(listServicesResponse) ) } internal func findFileBySymbol( _ symbolName: String, request: Reflection_ServerReflectionRequest - ) throws -> Reflection_ServerReflectionResponse { - guard let fileName = self.protoRegistry.nameOfFileContainingSymbol(named: symbolName) else { - throw GRPCStatus( - code: .notFound, - message: "The provided symbol could not be found." - ) + ) -> Reflection_ServerReflectionResponse { + let result = self.protoRegistry.nameOfFileContainingSymbol( + named: symbolName + ).flatMap { + self._findFileByFileName($0) } - return try self.findFileByFileName(fileName, request: request) + return result.makeResponse(request: request) } internal func findFileByExtension( extensionRequest: Reflection_ExtensionRequest, request: Reflection_ServerReflectionRequest - ) throws -> Reflection_ServerReflectionResponse { - guard - let fileName = self.protoRegistry.nameOfFileContainingExtension( - extendeeName: extensionRequest.containingType, - fieldNumber: extensionRequest.extensionNumber - ) - else { - throw GRPCStatus( - code: .notFound, - message: "The provided extension could not be found." - ) + ) -> Reflection_ServerReflectionResponse { + let result = self.protoRegistry.nameOfFileContainingExtension( + extendeeName: extensionRequest.containingType, + fieldNumber: extensionRequest.extensionNumber + ).flatMap { + self._findFileByFileName($0) } - return try self.findFileByFileName(fileName, request: request) + return result.makeResponse(request: request) } internal func findExtensionsFieldNumbersOfType( named typeName: String, request: Reflection_ServerReflectionRequest - ) throws -> Reflection_ServerReflectionResponse { - let fieldNumbers = try self.protoRegistry.extensionsFieldNumbersOfType(named: typeName) - return Reflection_ServerReflectionResponse( - request: request, - extensionNumberResponse: .with { - $0.baseTypeName = typeName - $0.extensionNumber = fieldNumbers - } - ) + ) -> Reflection_ServerReflectionResponse { + let result = self.protoRegistry.extensionsFieldNumbersOfType( + named: typeName + ).map { fieldNumbers in + Reflection_ServerReflectionResponse.OneOf_MessageResponse.allExtensionNumbersResponse( + Reflection_ExtensionNumberResponse.with { + $0.baseTypeName = typeName + $0.extensionNumber = fieldNumbers + } + ) + } + return result.makeResponse(request: request) } internal func serverReflectionInfo( @@ -272,7 +300,7 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync for try await request in requestStream { switch request.messageRequest { case let .fileByFilename(fileName): - let response = try self.findFileByFileName( + let response = self.findFileByFileName( fileName, request: request ) @@ -283,28 +311,37 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync try await responseStream.send(response) case let .fileContainingSymbol(symbolName): - let response = try self.findFileBySymbol( + let response = self.findFileBySymbol( symbolName, request: request ) try await responseStream.send(response) case let .fileContainingExtension(extensionRequest): - let response = try self.findFileByExtension( + let response = self.findFileByExtension( extensionRequest: extensionRequest, request: request ) try await responseStream.send(response) case let .allExtensionNumbersOfType(typeName): - let response = try self.findExtensionsFieldNumbersOfType( + let response = self.findExtensionsFieldNumbersOfType( named: typeName, request: request ) try await responseStream.send(response) default: - throw GRPCStatus(code: .unimplemented) + let response = Reflection_ServerReflectionResponse( + request: request, + messageResponse: .errorResponse( + Reflection_ErrorResponse.with { + $0.errorCode = Int32(GRPCStatus.Code.unimplemented.rawValue) + $0.errorMessage = "The request is not implemented." + } + ) + ) + try await responseStream.send(response) } } } @@ -313,34 +350,12 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync extension Reflection_ServerReflectionResponse { init( request: Reflection_ServerReflectionRequest, - fileDescriptorResponse: Reflection_FileDescriptorResponse - ) { - self = .with { - $0.validHost = request.host - $0.originalRequest = request - $0.fileDescriptorResponse = fileDescriptorResponse - } - } - - init( - request: Reflection_ServerReflectionRequest, - listServicesResponse: Reflection_ListServiceResponse - ) { - self = .with { - $0.validHost = request.host - $0.originalRequest = request - $0.listServicesResponse = listServicesResponse - } - } - - init( - request: Reflection_ServerReflectionRequest, - extensionNumberResponse: Reflection_ExtensionNumberResponse + messageResponse: Reflection_ServerReflectionResponse.OneOf_MessageResponse ) { self = .with { $0.validHost = request.host $0.originalRequest = request - $0.allExtensionNumbersResponse = extensionNumberResponse + $0.messageResponse = messageResponse } } } @@ -378,3 +393,33 @@ extension Google_Protobuf_FileDescriptorProto { return names } } + +extension Result { + func recover() -> Result { + self.flatMapError { status in + let error = Reflection_ErrorResponse.with { + $0.errorCode = Int32(status.code.rawValue) + $0.errorMessage = status.message ?? "" + } + return .success(.errorResponse(error)) + } + } + + func makeResponse( + request: Reflection_ServerReflectionRequest + ) -> Reflection_ServerReflectionResponse { + let result = self.recover().attachRequest(request) + // Safe to '!' as the failure type is 'Never'. + return try! result.get() + } +} + +extension Result where Success == Reflection_ServerReflectionResponse.OneOf_MessageResponse { + func attachRequest( + _ request: Reflection_ServerReflectionRequest + ) -> Result { + self.map { message in + Reflection_ServerReflectionResponse(request: request, messageResponse: message) + } + } +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index 790c2c9f8..b3b685813 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -259,4 +259,90 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { XCTAssertEqual(message.allExtensionNumbersResponse.baseTypeName, "packagebar2.inputMessage2") XCTAssertEqual(message.allExtensionNumbersResponse.extensionNumber, [1, 2, 3, 4, 5]) } + + func testErrorResponseFileByFileNameRequest() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileByFilename = "invalidFileName.proto" + } + ) + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) + XCTAssertEqual( + message.errorResponse.errorMessage, + "The provided file or a dependency of the provided file could not be found." + ) + } + + func testErrorResponseFileBySymbolRequest() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileContainingSymbol = "packagebar1.invalidEnumType1" + } + ) + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) + XCTAssertEqual(message.errorResponse.errorMessage, "The provided symbol could not be found.") + } + + func testErrorResponseFileByExtensionRequest() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileContainingExtension = .with { + $0.containingType = "packagebar1.invalidInputMessage1" + $0.extensionNumber = 2 + } + } + ) + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) + XCTAssertEqual(message.errorResponse.errorMessage, "The provided extension could not be found.") + } + + func testErrorResponseAllExtensionNumbersOfTypeRequest() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.allExtensionNumbersOfType = "packagebar2.invalidInputMessage2" + } + ) + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + + XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.invalidArgument.rawValue)) + XCTAssertEqual(message.errorResponse.errorMessage, "The provided type is invalid.") + } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index 8ede0e472..453edaa9a 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -117,38 +117,52 @@ final class ReflectionServiceUnitTests: GRPCTestCase { func testNameOfFileContainingSymbolEnum() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileName = registry.nameOfFileContainingSymbol(named: "packagebar2.enumType2") - XCTAssertEqual(fileName, "bar2.proto") + let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( + named: "packagebar2.enumType2" + ) + XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar2.proto") } func testNameOfFileContainingSymbolMessage() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage1") - XCTAssertEqual(fileName, "bar1.proto") + let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( + named: "packagebar1.inputMessage1" + ) + XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar1.proto") } func testNameOfFileContainingSymbolService() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileName = registry.nameOfFileContainingSymbol(named: "packagebar3.service3") - XCTAssertEqual(fileName, "bar3.proto") + let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( + named: "packagebar3.service3" + ) + XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar3.proto") } func testNameOfFileContainingSymbolMethod() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileName = registry.nameOfFileContainingSymbol( + let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( named: "packagebar4.service4.testMethod4" ) - XCTAssertEqual(fileName, "bar4.proto") + XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar4.proto") } func testNameOfFileContainingSymbolNonExistentSymbol() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileName = registry.nameOfFileContainingSymbol(named: "packagebar2.enumType3") - XCTAssertNil(fileName) + let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( + named: "packagebar2.enumType3" + ) + XCTAssertThrowsGRPCStatus(try nameOfFileContainingSymbolResult.get()) { + status in + XCTAssertEqual( + status, + GRPCStatus(code: .notFound, message: "The provided symbol could not be found.") + ) + } } // Testing the serializedFileDescriptorProto method in different cases. @@ -156,95 +170,112 @@ final class ReflectionServiceUnitTests: GRPCTestCase { func testSerialisedFileDescriptorProtosForDependenciesOfFile() throws { var protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtos = - try registry + let serializedFileDescriptorProtosResult = + registry .serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") - let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) - } - // Tests that the functions returns all the transitive dependencies, with their services and - // methods, together with the initial proto, as serialized data. - XCTAssertEqual(fileDescriptorProtos.count, 4) - for fileDescriptorProto in fileDescriptorProtos { - guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileDescriptorProto.name) \ - in the original file descriptor protos list. - """ - ) - } - for service in fileDescriptorProto.service { - guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + switch serializedFileDescriptorProtosResult { + case .success(let serializedFileDescriptorProtos): + let fileDescriptorProtos = try serializedFileDescriptorProtos.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + // Tests that the functions returns all the transitive dependencies, with their services and + // methods, together with the initial proto, as serialized data. + XCTAssertEqual(fileDescriptorProtos.count, 4) + for fileDescriptorProto in fileDescriptorProtos { + guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { return XCTFail( """ - Could not find the \(service.name) in the service \ - list of the \(fileDescriptorProto.name) file descriptor proto. + Could not find the file descriptor proto of \(fileDescriptorProto.name) \ + in the original file descriptor protos list. """ ) } - let originalMethods = protos[protoIndex].service[serviceIndex].method - for method in service.method { - XCTAssert(originalMethods.contains(method)) - } + for service in fileDescriptorProto.service { + guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + return XCTFail( + """ + Could not find the \(service.name) in the service \ + list of the \(fileDescriptorProto.name) file descriptor proto. + """ + ) + } - for messageType in fileDescriptorProto.messageType { - XCTAssert(protos[protoIndex].messageType.contains(messageType)) + let originalMethods = protos[protoIndex].service[serviceIndex].method + for method in service.method { + XCTAssert(originalMethods.contains(method)) + } + + for messageType in fileDescriptorProto.messageType { + XCTAssert(protos[protoIndex].messageType.contains(messageType)) + } } - } - protos.removeAll { $0 == fileDescriptorProto } + protos.removeAll { $0 == fileDescriptorProto } + } + XCTAssert(protos.isEmpty) + case .failure(let status): + XCTFail( + "Faild with GRPCStatus code: " + String(status.code.rawValue) + " and message: " + + (status.message ?? "empty") + "." + ) } - XCTAssert(protos.isEmpty) } func testSerialisedFileDescriptorProtosForDependenciesOfFileComplexDependencyGraph() throws { var protos = makeProtosWithComplexDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtos = - try registry + let serializedFileDescriptorProtosResult = + registry .serialisedFileDescriptorProtosForDependenciesOfFile(named: "foo0.proto") - let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) - } - // Tests that the functions returns all the tranzitive dependencies, with their services and - // methods, together with the initial proto, as serialized data. - XCTAssertEqual(fileDescriptorProtos.count, 21) - for fileDescriptorProto in fileDescriptorProtos { - guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileDescriptorProto.name) \ - in the original file descriptor protos list. - """ - ) + switch serializedFileDescriptorProtosResult { + case .success(let serializedFileDescriptorProtos): + let fileDescriptorProtos = try serializedFileDescriptorProtos.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) } - - for service in fileDescriptorProto.service { - guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + // Tests that the functions returns all the tranzitive dependencies, with their services and + // methods, together with the initial proto, as serialized data. + XCTAssertEqual(fileDescriptorProtos.count, 21) + for fileDescriptorProto in fileDescriptorProtos { + guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { return XCTFail( """ - Could not find the \(service.name) in the service \ - list of the \(fileDescriptorProto.name) file descriptor proto. + Could not find the file descriptor proto of \(fileDescriptorProto.name) \ + in the original file descriptor protos list. """ ) } - let originalMethods = protos[protoIndex].service[serviceIndex].method - for method in service.method { - XCTAssert(originalMethods.contains(method)) - } + for service in fileDescriptorProto.service { + guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + return XCTFail( + """ + Could not find the \(service.name) in the service \ + list of the \(fileDescriptorProto.name) file descriptor proto. + """ + ) + } - for messageType in fileDescriptorProto.messageType { - XCTAssert(protos[protoIndex].messageType.contains(messageType)) + let originalMethods = protos[protoIndex].service[serviceIndex].method + for method in service.method { + XCTAssert(originalMethods.contains(method)) + } + + for messageType in fileDescriptorProto.messageType { + XCTAssert(protos[protoIndex].messageType.contains(messageType)) + } } - } - protos.removeAll { $0 == fileDescriptorProto } + protos.removeAll { $0 == fileDescriptorProto } + } + XCTAssert(protos.isEmpty) + case .failure(let status): + XCTFail( + "Faild with GRPCStatus code: " + String(status.code.rawValue) + " and message: " + + (status.message ?? "empty") + "." + ) } - XCTAssert(protos.isEmpty) } func testSerialisedFileDescriptorProtosForDependenciesOfFileDependencyLoops() throws { @@ -254,57 +285,67 @@ final class ReflectionServiceUnitTests: GRPCTestCase { protos[2].dependency.append("bar1.proto") protos[3].dependency.append("bar1.proto") let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtos = - try registry + let serializedFileDescriptorProtosResult = + registry .serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") - let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) - } - // Test that we get only 4 serialized File Descriptor Protos as response. - XCTAssertEqual(fileDescriptorProtos.count, 4) - for fileDescriptorProto in fileDescriptorProtos { - guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileDescriptorProto.name) \ - in the original file descriptor protos list. - """ - ) + switch serializedFileDescriptorProtosResult { + case .success(let serializedFileDescriptorProtos): + let fileDescriptorProtos = try serializedFileDescriptorProtos.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) } - - for service in fileDescriptorProto.service { - guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + // Test that we get only 4 serialized File Descriptor Protos as response. + XCTAssertEqual(fileDescriptorProtos.count, 4) + for fileDescriptorProto in fileDescriptorProtos { + guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { return XCTFail( """ - Could not find the \(service.name) in the service \ - list of the \(fileDescriptorProto.name) file descriptor proto. + Could not find the file descriptor proto of \(fileDescriptorProto.name) \ + in the original file descriptor protos list. """ ) } - let originalMethods = protos[protoIndex].service[serviceIndex].method - for method in service.method { - XCTAssert(originalMethods.contains(method)) - } + for service in fileDescriptorProto.service { + guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { + return XCTFail( + """ + Could not find the \(service.name) in the service \ + list of the \(fileDescriptorProto.name) file descriptor proto. + """ + ) + } - for messageType in fileDescriptorProto.messageType { - XCTAssert(protos[protoIndex].messageType.contains(messageType)) + let originalMethods = protos[protoIndex].service[serviceIndex].method + for method in service.method { + XCTAssert(originalMethods.contains(method)) + } + + for messageType in fileDescriptorProto.messageType { + XCTAssert(protos[protoIndex].messageType.contains(messageType)) + } } - } - protos.removeAll { $0 == fileDescriptorProto } + protos.removeAll { $0 == fileDescriptorProto } + } + XCTAssert(protos.isEmpty) + case .failure(let status): + XCTFail( + "Faild with GRPCStatus code: " + String(status.code.rawValue) + " and message: " + + (status.message ?? "empty") + "." + ) } - XCTAssert(protos.isEmpty) } func testSerialisedFileDescriptorProtosForDependenciesOfFileInvalidFile() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - XCTAssertThrowsError( - try registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "invalid.proto") - ) { error in + let serializedFileDescriptorProtosForDependenciesOfFileResult = + registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "invalid.proto") + + XCTAssertThrowsGRPCStatus(try serializedFileDescriptorProtosForDependenciesOfFileResult.get()) { + status in XCTAssertEqual( - error as? GRPCStatus, + status, GRPCStatus( code: .notFound, message: "The provided file or a dependency of the provided file could not be found." @@ -317,11 +358,13 @@ final class ReflectionServiceUnitTests: GRPCTestCase { var protos = makeProtosWithDependencies() protos[0].dependency.append("invalidDependency") let registry = try ReflectionServiceData(fileDescriptors: protos) - XCTAssertThrowsError( - try registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") - ) { error in + let serializedFileDescriptorProtosForDependenciesOfFileResult = + registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") + + XCTAssertThrowsGRPCStatus(try serializedFileDescriptorProtosForDependenciesOfFileResult.get()) { + status in XCTAssertEqual( - error as? GRPCStatus, + status, GRPCStatus( code: .notFound, message: "The provided file or a dependency of the provided file could not be found." @@ -338,11 +381,11 @@ final class ReflectionServiceUnitTests: GRPCTestCase { for proto in protos { for `extension` in proto.extension { let typeName = String(`extension`.extendee.drop(while: { $0 == "." })) - let registryFileName = registry.nameOfFileContainingExtension( + let registryFileNameResult = registry.nameOfFileContainingExtension( extendeeName: typeName, fieldNumber: `extension`.number ) - XCTAssertEqual(registryFileName, proto.name) + XCTAssertEqual(try registryFileNameResult.get(), proto.name) } } } @@ -350,21 +393,35 @@ final class ReflectionServiceUnitTests: GRPCTestCase { func testNameOfFileContainingExtensionsInvalidTypeName() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let registryFileName = registry.nameOfFileContainingExtension( + let registryFileNameResult = registry.nameOfFileContainingExtension( extendeeName: "InvalidType", fieldNumber: 2 ) - XCTAssertNil(registryFileName) + + XCTAssertThrowsGRPCStatus(try registryFileNameResult.get()) { + status in + XCTAssertEqual( + status, + GRPCStatus(code: .notFound, message: "The provided extension could not be found.") + ) + } } func testNameOfFileContainingExtensionsInvalidFieldNumber() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let registryFileName = registry.nameOfFileContainingExtension( + let registryFileNameResult = registry.nameOfFileContainingExtension( extendeeName: protos[0].extension[0].extendee, fieldNumber: 9 ) - XCTAssertNil(registryFileName) + + XCTAssertThrowsGRPCStatus(try registryFileNameResult.get()) { + status in + XCTAssertEqual( + status, + GRPCStatus(code: .notFound, message: "The provided extension could not be found.") + ) + } } func testNameOfFileContainingExtensionsDuplicatedExtensions() throws { @@ -403,10 +460,11 @@ final class ReflectionServiceUnitTests: GRPCTestCase { } ) let registry = try ReflectionServiceData(fileDescriptors: protos) - let extensionNumbers = try registry.extensionsFieldNumbersOfType( + let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( named: "packagebar1.inputMessage1" ) - XCTAssertEqual(extensionNumbers, [1, 2, 3, 4, 5, 120]) + + XCTAssertEqual(try extensionsFieldNumbersOfTypeResult.get(), [1, 2, 3, 4, 5, 120]) } func testExtensionsFieldNumbersOfTypeNoExtensionsType() throws { @@ -423,26 +481,25 @@ final class ReflectionServiceUnitTests: GRPCTestCase { } ) let registry = try ReflectionServiceData(fileDescriptors: protos) - let extensionNumbers = try registry.extensionsFieldNumbersOfType( + let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( named: "packagebar1.noExtensionMessage" ) - XCTAssertEqual(extensionNumbers, []) + + XCTAssertEqual(try extensionsFieldNumbersOfTypeResult.get(), []) } func testExtensionsFieldNumbersOfTypeInvalidTypeName() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - XCTAssertThrowsError( - try registry.extensionsFieldNumbersOfType( - named: "packagebar1.invalidTypeMessage" - ) - ) { error in + let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( + named: "packagebar1.invalidTypeMessage" + ) + + XCTAssertThrowsGRPCStatus(try extensionsFieldNumbersOfTypeResult.get()) { + status in XCTAssertEqual( - error as? GRPCStatus, - GRPCStatus( - code: .invalidArgument, - message: "The provided type is invalid." - ) + status, + GRPCStatus(code: .invalidArgument, message: "The provided type is invalid.") ) } } @@ -456,9 +513,10 @@ final class ReflectionServiceUnitTests: GRPCTestCase { } ) let registry = try ReflectionServiceData(fileDescriptors: protos) - let extensionNumbers = try registry.extensionsFieldNumbersOfType( + let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( named: "packagebar1.inputMessage1" ) - XCTAssertEqual(extensionNumbers, [1, 2, 3, 4, 5, 130]) + + XCTAssertEqual(try extensionsFieldNumbersOfTypeResult.get(), [1, 2, 3, 4, 5, 130]) } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift index 9008aad9e..fcd141b1a 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift @@ -17,6 +17,7 @@ import Foundation import GRPC import SwiftProtobuf +import XCTest internal func makeExtensions( forType typeName: String, @@ -136,6 +137,19 @@ internal func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescri return protos } +func XCTAssertThrowsGRPCStatus( + _ expression: @autoclosure () throws -> T, + _ errorHandler: (GRPCStatus) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? GRPCStatus else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + + errorHandler(error) + } +} + extension Sequence where Element == Google_Protobuf_FileDescriptorProto { var serviceNames: [String] { self.flatMap { $0.service.map { $0.name } } From 959cbd3f82efd282ebed74f2cc0010d9d702030a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 30 Oct 2023 11:17:43 +0000 Subject: [PATCH 146/580] Add a handful of utilities (#1690) Motivation: The client rpc executor makes use of a bunch of utilities. Since it will be a reasonably large PR, in order to make it slightly less large, I'd like to get some of the utilities reviewed separately. Since most are too small to be worth reviewing individually this change includes a few unrelated utilities. Modifications: - Add `UnsafeTransfer` - Adds an optional `cause` error to `RPCError` - Add extensions to `Metadata` for setting/parsing a few gRPC specific metadata fields - Add extensions to `Result` for working with `async` closures and casting errors to a known type - Add a type-erased closable writer similar to the type-erased writer Result: A few handy helpers are in place and the rpc executor PR will be a little smaller. --- .../UnsafeTransfer.swift | 28 ++++++ Sources/GRPCCore/Internal/Metadata+GRPC.swift | 86 +++++++++++++++++++ .../GRPCCore/Internal/Result+Catching.swift | 46 ++++++++++ Sources/GRPCCore/RPCError.swift | 28 ++++-- .../Internal/RPCWriter+Closable.swift | 61 +++++++++++++ .../Internal/Metadata+GRPCTests.swift | 86 +++++++++++++++++++ .../Internal/Result+CatchingTests.swift | 65 ++++++++++++++ 7 files changed, 393 insertions(+), 7 deletions(-) create mode 100644 Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift create mode 100644 Sources/GRPCCore/Internal/Metadata+GRPC.swift create mode 100644 Sources/GRPCCore/Internal/Result+Catching.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/RPCWriter+Closable.swift create mode 100644 Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift create mode 100644 Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift new file mode 100644 index 000000000..bc82d0255 --- /dev/null +++ b/Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift @@ -0,0 +1,28 @@ +/* + * Copyright 2023, 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. + */ + +@usableFromInline +struct UnsafeTransfer { + @usableFromInline + var wrappedValue: Wrapped + + @inlinable + init(_ wrappedValue: Wrapped) { + self.wrappedValue = wrappedValue + } +} + +extension UnsafeTransfer: @unchecked Sendable {} diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift new file mode 100644 index 000000000..cd58a0b74 --- /dev/null +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -0,0 +1,86 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Metadata { + @inlinable + var previousRPCAttempts: Int? { + get { + self.firstString(forKey: .previousRPCAttempts).flatMap { Int($0) } + } + set { + if let newValue = newValue { + self.replaceOrAddString(String(describing: newValue), forKey: .previousRPCAttempts) + } else { + self.removeAllValues(forKey: .previousRPCAttempts) + } + } + } + + @inlinable + var retryPushback: RetryPushback? { + return self.firstString(forKey: .retryPushbackMs).map { + RetryPushback(milliseconds: $0) + } + } +} + +extension Metadata { + @usableFromInline + enum GRPCKey: String, Sendable, Hashable { + case retryPushbackMs = "grpc-retry-pushback-ms" + case previousRPCAttempts = "grpc-previous-rpc-attempts" + } + + @inlinable + func firstString(forKey key: GRPCKey) -> String? { + self[stringValues: key.rawValue].first(where: { _ in true }) + } + + @inlinable + mutating func replaceOrAddString(_ value: String, forKey key: GRPCKey) { + self.replaceOrAddString(value, forKey: key.rawValue) + } + + @inlinable + mutating func removeAllValues(forKey key: GRPCKey) { + self.removeAllValues(forKey: key.rawValue) + } +} + +extension Metadata { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @usableFromInline + enum RetryPushback: Hashable, Sendable { + case retryAfter(Duration) + case stopRetrying + + @inlinable + init(milliseconds value: String) { + if let milliseconds = Int64(value), milliseconds >= 0 { + let (seconds, remainingMilliseconds) = milliseconds.quotientAndRemainder(dividingBy: 1000) + // 1e18 attoseconds per second + // 1e15 attoseconds per millisecond. + let attoseconds = Int64(remainingMilliseconds) * 1_000_000_000_000_000 + self = .retryAfter(Duration(secondsComponent: seconds, attosecondsComponent: attoseconds)) + } else { + // Negative or not parseable means stop trying. + // Source: https://github.com/grpc/proposal/blob/master/A6-client-retries.md + self = .stopRetrying + } + } + } +} diff --git a/Sources/GRPCCore/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift new file mode 100644 index 000000000..bf2393752 --- /dev/null +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -0,0 +1,46 @@ +/* + * Copyright 2023, 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. + */ + +extension Result where Failure == any Error { + /// Like `Result(catching:)`, but `async`. + /// + /// - Parameter body: An `async` closure to catch the result of. + @inlinable + init(catching body: () async throws -> Success) async { + do { + self = .success(try await body()) + } catch { + self = .failure(error) + } + } + + /// Attempts to map the error to the given error type. + /// + /// If the cast fails then the provided closure is used to create an error of the given type. + /// + /// - Parameters: + /// - errorType: The type of error to cast to. + /// - buildError: A closure which constructs the desired error if the cast fails. + @inlinable + func castError( + to errorType: NewError.Type = NewError.self, + or buildError: (any Error) -> NewError + ) -> Result { + return self.mapError { error in + return (error as? NewError) ?? buildError(error) + } + } +} diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 3a26269d5..2eb5fd210 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -59,24 +59,36 @@ public struct RPCError: @unchecked Sendable, Hashable, Error { } } + /// The original error which led to this error being thrown. + public var cause: Error? { + get { self.storage.cause } + set { + self.ensureStorageIsUnique() + self.storage.cause = newValue + } + } + /// Create a new RPC error. /// /// - Parameters: /// - code: The status code. /// - message: A message providing additional context about the code. /// - metadata: Any metadata to attach to the error. - public init(code: Code, message: String, metadata: Metadata = [:]) { - self.storage = Storage(code: code, message: message, metadata: metadata) + /// - cause: An underlying error which led to this error being thrown. + public init(code: Code, message: String, metadata: Metadata = [:], cause: Error? = nil) { + self.storage = Storage(code: code, message: message, metadata: metadata, cause: cause) } /// Create a new RPC error from the provided ``Status``. /// /// Returns `nil` if the provided ``Status`` has code ``Status/Code-swift.struct/ok``. /// - /// - Parameter status: The status to convert. - public init?(status: Status) { + /// - Parameters: + /// - status: The status to convert. + /// - metadata: Any metadata to attach to the error. + public init?(status: Status, metadata: Metadata = [:]) { guard let code = Code(status.code) else { return nil } - self.init(code: code, message: status.message, metadata: [:]) + self.init(code: code, message: status.message, metadata: metadata) } } @@ -91,15 +103,17 @@ extension RPCError { var code: RPCError.Code var message: String var metadata: Metadata + var cause: Error? - init(code: RPCError.Code, message: String, metadata: Metadata) { + init(code: RPCError.Code, message: String, metadata: Metadata, cause: Error?) { self.code = code self.message = message self.metadata = metadata + self.cause = cause } func copy() -> Self { - Self(code: self.code, message: self.message, metadata: self.metadata) + Self(code: self.code, message: self.message, metadata: self.metadata, cause: self.cause) } func hash(into hasher: inout Hasher) { diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Closable.swift new file mode 100644 index 000000000..c376a3f7a --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Closable.swift @@ -0,0 +1,61 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension RPCWriter { + @usableFromInline + struct Closable: ClosableRPCWriterProtocol { + @usableFromInline + let writer: any ClosableRPCWriterProtocol + + /// Creates an ``RPCWriter`` by wrapping the `other` writer. + /// + /// - Parameter other: The writer to wrap. + @inlinable + init(wrapping other: some ClosableRPCWriterProtocol) { + self.writer = other + } + + /// Writes a sequence of elements. + /// + /// This function suspends until the elements have been accepted. Implements can use this + /// to exert backpressure on callers. + /// + /// - Parameter elements: The elements to write. + @inlinable + func write(contentsOf elements: some Sequence) async throws { + try await self.writer.write(contentsOf: elements) + } + + /// Indicate to the writer that no more writes are to be accepted. + /// + /// All writes after ``finish()`` has been called should result in an error + /// being thrown. + @inlinable + func finish() { + self.writer.finish() + } + + /// Indicate to the writer that no more writes are to be accepted because an error occurred. + /// + /// All writes after ``finish(throwing:)`` has been called should result in an error + /// being thrown. + @inlinable + func finish(throwing error: Error) { + self.writer.finish(throwing: error) + } + } +} diff --git a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift new file mode 100644 index 000000000..a6bb11a84 --- /dev/null +++ b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift @@ -0,0 +1,86 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +final class MetadataGRPCTests: XCTestCase { + func testPreviousRPCAttemptsValidValues() { + let testData = [("0", 0), ("1", 1), ("-1", -1)] + for (value, expected) in testData { + let metadata: Metadata = ["grpc-previous-rpc-attempts": "\(value)"] + XCTAssertEqual(metadata.previousRPCAttempts, expected) + } + } + + func testPreviousRPCAttemptsInvalidValues() { + let values = ["foo", "42.0"] + for value in values { + let metadata: Metadata = ["grpc-previous-rpc-attempts": "\(value)"] + XCTAssertNil(metadata.previousRPCAttempts) + } + } + + func testSetPreviousRPCAttemptsToValue() { + var metadata: Metadata = [:] + + metadata.previousRPCAttempts = 42 + XCTAssertEqual(metadata, ["grpc-previous-rpc-attempts": "42"]) + + metadata.previousRPCAttempts = nil + XCTAssertEqual(metadata, [:]) + + for i in 0 ..< 5 { + metadata.addString("\(i)", forKey: "grpc-previous-rpc-attempts") + } + XCTAssertEqual(metadata.count, 5) + + // Should remove old values. + metadata.previousRPCAttempts = 42 + XCTAssertEqual(metadata, ["grpc-previous-rpc-attempts": "42"]) + } + + func testRetryPushbackValidDelay() { + let testData: [(String, Duration)] = [ + ("0", .zero), + ("1", Duration(secondsComponent: 0, attosecondsComponent: 1_000_000_000_000_000)), + ("999", Duration(secondsComponent: 0, attosecondsComponent: 999_000_000_000_000_000)), + ("1000", Duration(secondsComponent: 1, attosecondsComponent: 0)), + ("1001", Duration(secondsComponent: 1, attosecondsComponent: 1_000_000_000_000_000)), + ("1999", Duration(secondsComponent: 1, attosecondsComponent: 999_000_000_000_000_000)), + ] + + for (value, expectedDuration) in testData { + let metadata: Metadata = ["grpc-retry-pushback-ms": "\(value)"] + XCTAssertEqual(metadata.retryPushback, .retryAfter(expectedDuration)) + } + } + + func testRetryPushbackInvalidDelay() { + let testData: [String] = ["-1", "-inf", "not-a-number", "42.0"] + + for value in testData { + let metadata: Metadata = ["grpc-retry-pushback-ms": "\(value)"] + XCTAssertEqual(metadata.retryPushback, .stopRetrying) + } + } + + func testRetryPushbackNoValuePresent() { + let metadata: Metadata = [:] + XCTAssertNil(metadata.retryPushback) + } +} diff --git a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift new file mode 100644 index 000000000..cbe5ac742 --- /dev/null +++ b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift @@ -0,0 +1,65 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +final class ResultCatchingTests: XCTestCase { + func testResultCatching() async { + let result = await Result { + try? await Task.sleep(nanoseconds: 1) + throw RPCError(code: .unknown, message: "foo") + } + + switch result { + case .success: + XCTFail() + case .failure(let error): + XCTAssertEqual(error as? RPCError, RPCError(code: .unknown, message: "foo")) + } + } + + func testCastToErrorOfCorrectType() async { + let result = Result.failure(RPCError(code: .unknown, message: "foo")) + let typedFailure = result.castError(to: RPCError.self) { _ in + XCTFail("buildError(_:) was called") + return RPCError(code: .failedPrecondition, message: "shouldn't happen") + } + + switch typedFailure { + case .success: + XCTFail() + case .failure(let error): + XCTAssertEqual(error, RPCError(code: .unknown, message: "foo")) + } + } + + func testCastToErrorOfIncorrectType() async { + struct WrongError: Error {} + let result = Result.failure(WrongError()) + let typedFailure = result.castError(to: RPCError.self) { _ in + return RPCError(code: .invalidArgument, message: "fallback") + } + + switch typedFailure { + case .success: + XCTFail() + case .failure(let error): + XCTAssertEqual(error, RPCError(code: .invalidArgument, message: "fallback")) + } + } +} From 10f3576ea0ea5aad9206da42163aab73d3cb4989 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 31 Oct 2023 07:49:08 +0000 Subject: [PATCH 147/580] Changed the name of the package of the reflection.proto (#1695) Motivation: The name was not complete and GRPCurl's requests for the Reflection Service resulted in error. Modifications: Changed the name of the package, regenerated the Service code and changed the type of the variables accordingly, in the Service implementation and in the tests. Result: GRPCurl will be able to make requests to Servers implementing the Reflection Service. --- .../Model/reflection.grpc.swift | 46 +++---- .../Model/reflection.pb.swift | 126 +++++++++--------- .../Model/reflection.proto | 2 +- .../Server/ReflectionService.swift | 72 +++++----- .../Generated/reflection.grpc.swift | 86 ++++++------ .../Generated/reflection.pb.swift | 126 +++++++++--------- .../ReflectionServiceIntegrationTests.swift | 18 +-- 7 files changed, 239 insertions(+), 237 deletions(-) diff --git a/Sources/GRPCReflectionService/Model/reflection.grpc.swift b/Sources/GRPCReflectionService/Model/reflection.grpc.swift index 2239744da..0cd46f660 100644 --- a/Sources/GRPCReflectionService/Model/reflection.grpc.swift +++ b/Sources/GRPCReflectionService/Model/reflection.grpc.swift @@ -12,17 +12,17 @@ import SwiftProtobuf /// To build a server, implement a class that conforms to this protocol. -internal protocol Reflection_ServerReflectionProvider: CallHandlerProvider { - var interceptors: Reflection_ServerReflectionServerInterceptorFactoryProtocol? { get } +internal protocol Grpc_Reflection_V1_ServerReflectionProvider: CallHandlerProvider { + var interceptors: Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol? { get } /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. - func serverReflectionInfo(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> + func serverReflectionInfo(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> } -extension Reflection_ServerReflectionProvider { +extension Grpc_Reflection_V1_ServerReflectionProvider { internal var serviceName: Substring { - return Reflection_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] + return Grpc_Reflection_V1_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] } /// Determines, calls and returns the appropriate request handler, depending on the request's method. @@ -35,8 +35,8 @@ extension Reflection_ServerReflectionProvider { case "ServerReflectionInfo": return BidirectionalStreamingServerHandler( context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], observerFactory: self.serverReflectionInfo(context:) ) @@ -49,30 +49,30 @@ extension Reflection_ServerReflectionProvider { /// To implement a server, implement an object which conforms to this protocol. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Reflection_ServerReflectionAsyncProvider: CallHandlerProvider, Sendable { +internal protocol Grpc_Reflection_V1_ServerReflectionAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Reflection_ServerReflectionServerInterceptorFactoryProtocol? { get } + var interceptors: Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol? { get } /// The reflection service is structured as a bidirectional stream, ensuring /// all related requests go to a single server. func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext ) async throws } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Reflection_ServerReflectionAsyncProvider { +extension Grpc_Reflection_V1_ServerReflectionAsyncProvider { internal static var serviceDescriptor: GRPCServiceDescriptor { - return Reflection_ServerReflectionServerMetadata.serviceDescriptor + return Grpc_Reflection_V1_ServerReflectionServerMetadata.serviceDescriptor } internal var serviceName: Substring { - return Reflection_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] + return Grpc_Reflection_V1_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] } - internal var interceptors: Reflection_ServerReflectionServerInterceptorFactoryProtocol? { + internal var interceptors: Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol? { return nil } @@ -84,8 +84,8 @@ extension Reflection_ServerReflectionAsyncProvider { case "ServerReflectionInfo": return GRPCAsyncServerHandler( context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], wrapping: { try await self.serverReflectionInfo(requestStream: $0, responseStream: $1, context: $2) } ) @@ -96,26 +96,26 @@ extension Reflection_ServerReflectionAsyncProvider { } } -internal protocol Reflection_ServerReflectionServerInterceptorFactoryProtocol: Sendable { +internal protocol Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when handling 'serverReflectionInfo'. /// Defaults to calling `self.makeInterceptors()`. - func makeServerReflectionInfoInterceptors() -> [ServerInterceptor] + func makeServerReflectionInfoInterceptors() -> [ServerInterceptor] } -internal enum Reflection_ServerReflectionServerMetadata { +internal enum Grpc_Reflection_V1_ServerReflectionServerMetadata { internal static let serviceDescriptor = GRPCServiceDescriptor( name: "ServerReflection", - fullName: "reflection.ServerReflection", + fullName: "grpc.reflection.v1.ServerReflection", methods: [ - Reflection_ServerReflectionServerMetadata.Methods.serverReflectionInfo, + Grpc_Reflection_V1_ServerReflectionServerMetadata.Methods.serverReflectionInfo, ] ) internal enum Methods { internal static let serverReflectionInfo = GRPCMethodDescriptor( name: "ServerReflectionInfo", - path: "/reflection.ServerReflection/ServerReflectionInfo", + path: "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", type: GRPCCallType.bidirectionalStreaming ) } diff --git a/Sources/GRPCReflectionService/Model/reflection.pb.swift b/Sources/GRPCReflectionService/Model/reflection.pb.swift index f86e9577c..8e0d557c4 100644 --- a/Sources/GRPCReflectionService/Model/reflection.pb.swift +++ b/Sources/GRPCReflectionService/Model/reflection.pb.swift @@ -42,7 +42,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The message sent by the client when calling ServerReflectionInfo method. -public struct Reflection_ServerReflectionRequest { +public struct Grpc_Reflection_V1_ServerReflectionRequest { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -52,7 +52,7 @@ public struct Reflection_ServerReflectionRequest { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. - public var messageRequest: Reflection_ServerReflectionRequest.OneOf_MessageRequest? = nil + public var messageRequest: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest? = nil /// Find a proto file by the file name. public var fileByFilename: String { @@ -76,10 +76,10 @@ public struct Reflection_ServerReflectionRequest { /// Find the proto file which defines an extension extending the given /// message type with the given field number. - public var fileContainingExtension: Reflection_ExtensionRequest { + public var fileContainingExtension: Grpc_Reflection_V1_ExtensionRequest { get { if case .fileContainingExtension(let v)? = messageRequest {return v} - return Reflection_ExtensionRequest() + return Grpc_Reflection_V1_ExtensionRequest() } set {messageRequest = .fileContainingExtension(newValue)} } @@ -124,7 +124,7 @@ public struct Reflection_ServerReflectionRequest { case fileContainingSymbol(String) /// Find the proto file which defines an extension extending the given /// message type with the given field number. - case fileContainingExtension(Reflection_ExtensionRequest) + case fileContainingExtension(Grpc_Reflection_V1_ExtensionRequest) /// Finds the tag numbers used by all known extensions of the given message /// type, and appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the @@ -139,7 +139,7 @@ public struct Reflection_ServerReflectionRequest { case listServices(String) #if !swift(>=4.1) - public static func ==(lhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest, rhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 @@ -175,7 +175,7 @@ public struct Reflection_ServerReflectionRequest { /// The type name and extension number sent by the client when requesting /// file_containing_extension. -public struct Reflection_ExtensionRequest { +public struct Grpc_Reflection_V1_ExtensionRequest { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -191,15 +191,15 @@ public struct Reflection_ExtensionRequest { } /// The message sent by the server to answer ServerReflectionInfo method. -public struct Reflection_ServerReflectionResponse { +public struct Grpc_Reflection_V1_ServerReflectionResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. public var validHost: String = String() - public var originalRequest: Reflection_ServerReflectionRequest { - get {return _originalRequest ?? Reflection_ServerReflectionRequest()} + public var originalRequest: Grpc_Reflection_V1_ServerReflectionRequest { + get {return _originalRequest ?? Grpc_Reflection_V1_ServerReflectionRequest()} set {_originalRequest = newValue} } /// Returns true if `originalRequest` has been explicitly set. @@ -209,7 +209,7 @@ public struct Reflection_ServerReflectionResponse { /// The server sets one of the following fields according to the message_request /// in the request. - public var messageResponse: Reflection_ServerReflectionResponse.OneOf_MessageResponse? = nil + public var messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse? = nil /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. @@ -217,37 +217,37 @@ public struct Reflection_ServerReflectionResponse { /// FileDescriptorResponse message to encapsulate the repeated fields. /// The reflection service is allowed to avoid sending FileDescriptorProtos /// that were previously sent in response to earlier requests in the stream. - public var fileDescriptorResponse: Reflection_FileDescriptorResponse { + public var fileDescriptorResponse: Grpc_Reflection_V1_FileDescriptorResponse { get { if case .fileDescriptorResponse(let v)? = messageResponse {return v} - return Reflection_FileDescriptorResponse() + return Grpc_Reflection_V1_FileDescriptorResponse() } set {messageResponse = .fileDescriptorResponse(newValue)} } /// This message is used to answer all_extension_numbers_of_type requests. - public var allExtensionNumbersResponse: Reflection_ExtensionNumberResponse { + public var allExtensionNumbersResponse: Grpc_Reflection_V1_ExtensionNumberResponse { get { if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} - return Reflection_ExtensionNumberResponse() + return Grpc_Reflection_V1_ExtensionNumberResponse() } set {messageResponse = .allExtensionNumbersResponse(newValue)} } /// This message is used to answer list_services requests. - public var listServicesResponse: Reflection_ListServiceResponse { + public var listServicesResponse: Grpc_Reflection_V1_ListServiceResponse { get { if case .listServicesResponse(let v)? = messageResponse {return v} - return Reflection_ListServiceResponse() + return Grpc_Reflection_V1_ListServiceResponse() } set {messageResponse = .listServicesResponse(newValue)} } /// This message is used when an error occurs. - public var errorResponse: Reflection_ErrorResponse { + public var errorResponse: Grpc_Reflection_V1_ErrorResponse { get { if case .errorResponse(let v)? = messageResponse {return v} - return Reflection_ErrorResponse() + return Grpc_Reflection_V1_ErrorResponse() } set {messageResponse = .errorResponse(newValue)} } @@ -263,16 +263,16 @@ public struct Reflection_ServerReflectionResponse { /// FileDescriptorResponse message to encapsulate the repeated fields. /// The reflection service is allowed to avoid sending FileDescriptorProtos /// that were previously sent in response to earlier requests in the stream. - case fileDescriptorResponse(Reflection_FileDescriptorResponse) + case fileDescriptorResponse(Grpc_Reflection_V1_FileDescriptorResponse) /// This message is used to answer all_extension_numbers_of_type requests. - case allExtensionNumbersResponse(Reflection_ExtensionNumberResponse) + case allExtensionNumbersResponse(Grpc_Reflection_V1_ExtensionNumberResponse) /// This message is used to answer list_services requests. - case listServicesResponse(Reflection_ListServiceResponse) + case listServicesResponse(Grpc_Reflection_V1_ListServiceResponse) /// This message is used when an error occurs. - case errorResponse(Reflection_ErrorResponse) + case errorResponse(Grpc_Reflection_V1_ErrorResponse) #if !swift(>=4.1) - public static func ==(lhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse, rhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 @@ -301,13 +301,13 @@ public struct Reflection_ServerReflectionResponse { public init() {} - fileprivate var _originalRequest: Reflection_ServerReflectionRequest? = nil + fileprivate var _originalRequest: Grpc_Reflection_V1_ServerReflectionRequest? = nil } /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. -public struct Reflection_FileDescriptorResponse { +public struct Grpc_Reflection_V1_FileDescriptorResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -324,7 +324,7 @@ public struct Reflection_FileDescriptorResponse { /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. -public struct Reflection_ExtensionNumberResponse { +public struct Grpc_Reflection_V1_ExtensionNumberResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -341,14 +341,14 @@ public struct Reflection_ExtensionNumberResponse { } /// A list of ServiceResponse sent by the server answering list_services request. -public struct Reflection_ListServiceResponse { +public struct Grpc_Reflection_V1_ListServiceResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// The information of each service may be expanded in the future, so we use /// ServiceResponse message to encapsulate it. - public var service: [Reflection_ServiceResponse] = [] + public var service: [Grpc_Reflection_V1_ServiceResponse] = [] public var unknownFields = SwiftProtobuf.UnknownStorage() @@ -357,7 +357,7 @@ public struct Reflection_ListServiceResponse { /// The information of a single service used by ListServiceResponse to answer /// list_services request. -public struct Reflection_ServiceResponse { +public struct Grpc_Reflection_V1_ServiceResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -372,7 +372,7 @@ public struct Reflection_ServiceResponse { } /// The error code and error message sent by the server when an error occurs. -public struct Reflection_ErrorResponse { +public struct Grpc_Reflection_V1_ErrorResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -388,23 +388,23 @@ public struct Reflection_ErrorResponse { } #if swift(>=5.5) && canImport(_Concurrency) -extension Reflection_ServerReflectionRequest: @unchecked Sendable {} -extension Reflection_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} -extension Reflection_ExtensionRequest: @unchecked Sendable {} -extension Reflection_ServerReflectionResponse: @unchecked Sendable {} -extension Reflection_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} -extension Reflection_FileDescriptorResponse: @unchecked Sendable {} -extension Reflection_ExtensionNumberResponse: @unchecked Sendable {} -extension Reflection_ListServiceResponse: @unchecked Sendable {} -extension Reflection_ServiceResponse: @unchecked Sendable {} -extension Reflection_ErrorResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1_ExtensionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ExtensionNumberResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ListServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ErrorResponse: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. -fileprivate let _protobuf_package = "reflection" +fileprivate let _protobuf_package = "grpc.reflection.v1" -extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "host"), @@ -439,7 +439,7 @@ extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtob } }() case 5: try { - var v: Reflection_ExtensionRequest? + var v: Grpc_Reflection_V1_ExtensionRequest? var hadOneofValue = false if let current = self.messageRequest { hadOneofValue = true @@ -506,7 +506,7 @@ extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtob try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_ServerReflectionRequest, rhs: Reflection_ServerReflectionRequest) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest) -> Bool { if lhs.host != rhs.host {return false} if lhs.messageRequest != rhs.messageRequest {return false} if lhs.unknownFields != rhs.unknownFields {return false} @@ -514,7 +514,7 @@ extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtob } } -extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "containing_type"), @@ -544,7 +544,7 @@ extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_ExtensionRequest, rhs: Reflection_ExtensionRequest) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ExtensionRequest, rhs: Grpc_Reflection_V1_ExtensionRequest) -> Bool { if lhs.containingType != rhs.containingType {return false} if lhs.extensionNumber != rhs.extensionNumber {return false} if lhs.unknownFields != rhs.unknownFields {return false} @@ -552,7 +552,7 @@ extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes } } -extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "valid_host"), @@ -572,7 +572,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() case 4: try { - var v: Reflection_FileDescriptorResponse? + var v: Grpc_Reflection_V1_FileDescriptorResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -585,7 +585,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } }() case 5: try { - var v: Reflection_ExtensionNumberResponse? + var v: Grpc_Reflection_V1_ExtensionNumberResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -598,7 +598,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } }() case 6: try { - var v: Reflection_ListServiceResponse? + var v: Grpc_Reflection_V1_ListServiceResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -611,7 +611,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } }() case 7: try { - var v: Reflection_ErrorResponse? + var v: Grpc_Reflection_V1_ErrorResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -661,7 +661,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_ServerReflectionResponse, rhs: Reflection_ServerReflectionResponse) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse) -> Bool { if lhs.validHost != rhs.validHost {return false} if lhs._originalRequest != rhs._originalRequest {return false} if lhs.messageResponse != rhs.messageResponse {return false} @@ -670,7 +670,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } } -extension Reflection_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "file_descriptor_proto"), @@ -695,14 +695,14 @@ extension Reflection_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobu try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_FileDescriptorResponse, rhs: Reflection_FileDescriptorResponse) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_FileDescriptorResponse, rhs: Grpc_Reflection_V1_FileDescriptorResponse) -> Bool { if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "base_type_name"), @@ -732,7 +732,7 @@ extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtob try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_ExtensionNumberResponse, rhs: Reflection_ExtensionNumberResponse) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ExtensionNumberResponse, rhs: Grpc_Reflection_V1_ExtensionNumberResponse) -> Bool { if lhs.baseTypeName != rhs.baseTypeName {return false} if lhs.extensionNumber != rhs.extensionNumber {return false} if lhs.unknownFields != rhs.unknownFields {return false} @@ -740,7 +740,7 @@ extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtob } } -extension Reflection_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "service"), @@ -765,14 +765,14 @@ extension Reflection_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._ try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_ListServiceResponse, rhs: Reflection_ListServiceResponse) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ListServiceResponse, rhs: Grpc_Reflection_V1_ListServiceResponse) -> Bool { if lhs.service != rhs.service {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Reflection_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ServiceResponse" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "name"), @@ -797,14 +797,14 @@ extension Reflection_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._Mess try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_ServiceResponse, rhs: Reflection_ServiceResponse) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ServiceResponse, rhs: Grpc_Reflection_V1_ServiceResponse) -> Bool { if lhs.name != rhs.name {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Reflection_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".ErrorResponse" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "error_code"), @@ -834,7 +834,7 @@ extension Reflection_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._Messag try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Reflection_ErrorResponse, rhs: Reflection_ErrorResponse) -> Bool { + public static func ==(lhs: Grpc_Reflection_V1_ErrorResponse, rhs: Grpc_Reflection_V1_ErrorResponse) -> Bool { if lhs.errorCode != rhs.errorCode {return false} if lhs.errorMessage != rhs.errorMessage {return false} if lhs.unknownFields != rhs.unknownFields {return false} diff --git a/Sources/GRPCReflectionService/Model/reflection.proto b/Sources/GRPCReflectionService/Model/reflection.proto index 839a0e3af..c540fe365 100644 --- a/Sources/GRPCReflectionService/Model/reflection.proto +++ b/Sources/GRPCReflectionService/Model/reflection.proto @@ -21,7 +21,7 @@ syntax = "proto3"; -package reflection; +package grpc.reflection.v1; service ServerReflection { // The reflection service is structured as a bidirectional stream, ensuring diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 190117e76..40450c767 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -204,7 +204,7 @@ internal struct ReflectionServiceData: Sendable { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsyncProvider { +internal final class ReflectionServiceProvider: Grpc_Reflection_V1_ServerReflectionAsyncProvider { private let protoRegistry: ReflectionServiceData internal init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto]) throws { @@ -215,11 +215,11 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync internal func _findFileByFileName( _ fileName: String - ) -> Result { + ) -> Result { return self.protoRegistry .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) .map { fileDescriptorProtos in - Reflection_ServerReflectionResponse.OneOf_MessageResponse.fileDescriptorResponse( + Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.fileDescriptorResponse( .with { $0.fileDescriptorProto = fileDescriptorProtos } @@ -229,22 +229,22 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync internal func findFileByFileName( _ fileName: String, - request: Reflection_ServerReflectionRequest - ) -> Reflection_ServerReflectionResponse { + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { let result = self._findFileByFileName(fileName) return result.makeResponse(request: request) } internal func getServicesNames( - request: Reflection_ServerReflectionRequest - ) throws -> Reflection_ServerReflectionResponse { - var listServicesResponse = Reflection_ListServiceResponse() + request: Grpc_Reflection_V1_ServerReflectionRequest + ) throws -> Grpc_Reflection_V1_ServerReflectionResponse { + var listServicesResponse = Grpc_Reflection_V1_ListServiceResponse() listServicesResponse.service = self.protoRegistry.serviceNames.map { serviceName in - Reflection_ServiceResponse.with { + Grpc_Reflection_V1_ServiceResponse.with { $0.name = serviceName } } - return Reflection_ServerReflectionResponse( + return Grpc_Reflection_V1_ServerReflectionResponse( request: request, messageResponse: .listServicesResponse(listServicesResponse) ) @@ -252,8 +252,8 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync internal func findFileBySymbol( _ symbolName: String, - request: Reflection_ServerReflectionRequest - ) -> Reflection_ServerReflectionResponse { + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { let result = self.protoRegistry.nameOfFileContainingSymbol( named: symbolName ).flatMap { @@ -263,9 +263,9 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync } internal func findFileByExtension( - extensionRequest: Reflection_ExtensionRequest, - request: Reflection_ServerReflectionRequest - ) -> Reflection_ServerReflectionResponse { + extensionRequest: Grpc_Reflection_V1_ExtensionRequest, + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { let result = self.protoRegistry.nameOfFileContainingExtension( extendeeName: extensionRequest.containingType, fieldNumber: extensionRequest.extensionNumber @@ -277,13 +277,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync internal func findExtensionsFieldNumbersOfType( named typeName: String, - request: Reflection_ServerReflectionRequest - ) -> Reflection_ServerReflectionResponse { + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { let result = self.protoRegistry.extensionsFieldNumbersOfType( named: typeName ).map { fieldNumbers in - Reflection_ServerReflectionResponse.OneOf_MessageResponse.allExtensionNumbersResponse( - Reflection_ExtensionNumberResponse.with { + Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.allExtensionNumbersResponse( + Grpc_Reflection_V1_ExtensionNumberResponse.with { $0.baseTypeName = typeName $0.extensionNumber = fieldNumbers } @@ -293,8 +293,8 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync } internal func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, context: GRPCAsyncServerCallContext ) async throws { for try await request in requestStream { @@ -332,10 +332,10 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync try await responseStream.send(response) default: - let response = Reflection_ServerReflectionResponse( + let response = Grpc_Reflection_V1_ServerReflectionResponse( request: request, messageResponse: .errorResponse( - Reflection_ErrorResponse.with { + Grpc_Reflection_V1_ErrorResponse.with { $0.errorCode = Int32(GRPCStatus.Code.unimplemented.rawValue) $0.errorMessage = "The request is not implemented." } @@ -347,10 +347,10 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync } } -extension Reflection_ServerReflectionResponse { +extension Grpc_Reflection_V1_ServerReflectionResponse { init( - request: Reflection_ServerReflectionRequest, - messageResponse: Reflection_ServerReflectionResponse.OneOf_MessageResponse + request: Grpc_Reflection_V1_ServerReflectionRequest, + messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse ) { self = .with { $0.validHost = request.host @@ -394,10 +394,11 @@ extension Google_Protobuf_FileDescriptorProto { } } -extension Result { - func recover() -> Result { +extension Result { + func recover() -> Result + { self.flatMapError { status in - let error = Reflection_ErrorResponse.with { + let error = Grpc_Reflection_V1_ErrorResponse.with { $0.errorCode = Int32(status.code.rawValue) $0.errorMessage = status.message ?? "" } @@ -406,20 +407,21 @@ extension Result Reflection_ServerReflectionResponse { + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { let result = self.recover().attachRequest(request) // Safe to '!' as the failure type is 'Never'. return try! result.get() } } -extension Result where Success == Reflection_ServerReflectionResponse.OneOf_MessageResponse { +extension Result +where Success == Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse { func attachRequest( - _ request: Reflection_ServerReflectionRequest - ) -> Result { + _ request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Result { self.map { message in - Reflection_ServerReflectionResponse(request: request, messageResponse: message) + Grpc_Reflection_V1_ServerReflectionResponse(request: request, messageResponse: message) } } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift index ade12d796..57a605e77 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift @@ -11,20 +11,20 @@ import NIOConcurrencyHelpers import SwiftProtobuf -/// Usage: instantiate `Reflection_ServerReflectionClient`, then call methods of this protocol to make API calls. -internal protocol Reflection_ServerReflectionClientProtocol: GRPCClient { +/// Usage: instantiate `Grpc_Reflection_V1_ServerReflectionClient`, then call methods of this protocol to make API calls. +internal protocol Grpc_Reflection_V1_ServerReflectionClientProtocol: GRPCClient { var serviceName: String { get } - var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { get } + var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { get } func serverReflectionInfo( callOptions: CallOptions?, - handler: @escaping (Reflection_ServerReflectionResponse) -> Void - ) -> BidirectionalStreamingCall + handler: @escaping (Grpc_Reflection_V1_ServerReflectionResponse) -> Void + ) -> BidirectionalStreamingCall } -extension Reflection_ServerReflectionClientProtocol { +extension Grpc_Reflection_V1_ServerReflectionClientProtocol { internal var serviceName: String { - return "reflection.ServerReflection" + return "grpc.reflection.v1.ServerReflection" } /// The reflection service is structured as a bidirectional stream, ensuring @@ -39,10 +39,10 @@ extension Reflection_ServerReflectionClientProtocol { /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. internal func serverReflectionInfo( callOptions: CallOptions? = nil, - handler: @escaping (Reflection_ServerReflectionResponse) -> Void - ) -> BidirectionalStreamingCall { + handler: @escaping (Grpc_Reflection_V1_ServerReflectionResponse) -> Void + ) -> BidirectionalStreamingCall { return self.makeBidirectionalStreamingCall( - path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, callOptions: callOptions ?? self.defaultCallOptions, interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], handler: handler @@ -51,24 +51,24 @@ extension Reflection_ServerReflectionClientProtocol { } @available(*, deprecated) -extension Reflection_ServerReflectionClient: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionClient: @unchecked Sendable {} -@available(*, deprecated, renamed: "Reflection_ServerReflectionNIOClient") -internal final class Reflection_ServerReflectionClient: Reflection_ServerReflectionClientProtocol { +@available(*, deprecated, renamed: "Grpc_Reflection_V1_ServerReflectionNIOClient") +internal final class Grpc_Reflection_V1_ServerReflectionClient: Grpc_Reflection_V1_ServerReflectionClientProtocol { private let lock = Lock() private var _defaultCallOptions: CallOptions - private var _interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? + private var _interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? internal let channel: GRPCChannel internal var defaultCallOptions: CallOptions { get { self.lock.withLock { return self._defaultCallOptions } } set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } } - internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { + internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { get { self.lock.withLock { return self._interceptors } } set { self.lock.withLockVoid { self._interceptors = newValue } } } - /// Creates a client for the reflection.ServerReflection service. + /// Creates a client for the grpc.reflection.v1.ServerReflection service. /// /// - Parameters: /// - channel: `GRPCChannel` to the service host. @@ -77,7 +77,7 @@ internal final class Reflection_ServerReflectionClient: Reflection_ServerReflect internal init( channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions(), - interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? = nil + interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? = nil ) { self.channel = channel self._defaultCallOptions = defaultCallOptions @@ -85,12 +85,12 @@ internal final class Reflection_ServerReflectionClient: Reflection_ServerReflect } } -internal struct Reflection_ServerReflectionNIOClient: Reflection_ServerReflectionClientProtocol { +internal struct Grpc_Reflection_V1_ServerReflectionNIOClient: Grpc_Reflection_V1_ServerReflectionClientProtocol { internal var channel: GRPCChannel internal var defaultCallOptions: CallOptions - internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? + internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? - /// Creates a client for the reflection.ServerReflection service. + /// Creates a client for the grpc.reflection.v1.ServerReflection service. /// /// - Parameters: /// - channel: `GRPCChannel` to the service host. @@ -99,7 +99,7 @@ internal struct Reflection_ServerReflectionNIOClient: Reflection_ServerReflectio internal init( channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions(), - interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? = nil + interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? = nil ) { self.channel = channel self.defaultCallOptions = defaultCallOptions @@ -108,30 +108,30 @@ internal struct Reflection_ServerReflectionNIOClient: Reflection_ServerReflectio } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Reflection_ServerReflectionAsyncClientProtocol: GRPCClient { +internal protocol Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol: GRPCClient { static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { get } + var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { get } func makeServerReflectionInfoCall( callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall + ) -> GRPCAsyncBidirectionalStreamingCall } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Reflection_ServerReflectionAsyncClientProtocol { +extension Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol { internal static var serviceDescriptor: GRPCServiceDescriptor { - return Reflection_ServerReflectionClientMetadata.serviceDescriptor + return Grpc_Reflection_V1_ServerReflectionClientMetadata.serviceDescriptor } - internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? { + internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { return nil } internal func makeServerReflectionInfoCall( callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { + ) -> GRPCAsyncBidirectionalStreamingCall { return self.makeAsyncBidirectionalStreamingCall( - path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, callOptions: callOptions ?? self.defaultCallOptions, interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] ) @@ -139,13 +139,13 @@ extension Reflection_ServerReflectionAsyncClientProtocol { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Reflection_ServerReflectionAsyncClientProtocol { +extension Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol { internal func serverReflectionInfo( _ requests: RequestStream, callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Reflection_ServerReflectionRequest { + ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Reflection_V1_ServerReflectionRequest { return self.performAsyncBidirectionalStreamingCall( - path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, requests: requests, callOptions: callOptions ?? self.defaultCallOptions, interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] @@ -155,9 +155,9 @@ extension Reflection_ServerReflectionAsyncClientProtocol { internal func serverReflectionInfo( _ requests: RequestStream, callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Reflection_ServerReflectionRequest { + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Reflection_V1_ServerReflectionRequest { return self.performAsyncBidirectionalStreamingCall( - path: Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, requests: requests, callOptions: callOptions ?? self.defaultCallOptions, interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] @@ -166,15 +166,15 @@ extension Reflection_ServerReflectionAsyncClientProtocol { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct Reflection_ServerReflectionAsyncClient: Reflection_ServerReflectionAsyncClientProtocol { +internal struct Grpc_Reflection_V1_ServerReflectionAsyncClient: Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol { internal var channel: GRPCChannel internal var defaultCallOptions: CallOptions - internal var interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? + internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? internal init( channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions(), - interceptors: Reflection_ServerReflectionClientInterceptorFactoryProtocol? = nil + interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? = nil ) { self.channel = channel self.defaultCallOptions = defaultCallOptions @@ -182,25 +182,25 @@ internal struct Reflection_ServerReflectionAsyncClient: Reflection_ServerReflect } } -internal protocol Reflection_ServerReflectionClientInterceptorFactoryProtocol: Sendable { +internal protocol Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol: Sendable { /// - Returns: Interceptors to use when invoking 'serverReflectionInfo'. - func makeServerReflectionInfoInterceptors() -> [ClientInterceptor] + func makeServerReflectionInfoInterceptors() -> [ClientInterceptor] } -internal enum Reflection_ServerReflectionClientMetadata { +internal enum Grpc_Reflection_V1_ServerReflectionClientMetadata { internal static let serviceDescriptor = GRPCServiceDescriptor( name: "ServerReflection", - fullName: "reflection.ServerReflection", + fullName: "grpc.reflection.v1.ServerReflection", methods: [ - Reflection_ServerReflectionClientMetadata.Methods.serverReflectionInfo, + Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo, ] ) internal enum Methods { internal static let serverReflectionInfo = GRPCMethodDescriptor( name: "ServerReflectionInfo", - path: "/reflection.ServerReflection/ServerReflectionInfo", + path: "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", type: GRPCCallType.bidirectionalStreaming ) } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift index d3cabe420..281330e5f 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift @@ -42,7 +42,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The message sent by the client when calling ServerReflectionInfo method. -struct Reflection_ServerReflectionRequest { +struct Grpc_Reflection_V1_ServerReflectionRequest { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -52,7 +52,7 @@ struct Reflection_ServerReflectionRequest { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. - var messageRequest: Reflection_ServerReflectionRequest.OneOf_MessageRequest? = nil + var messageRequest: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest? = nil /// Find a proto file by the file name. var fileByFilename: String { @@ -76,10 +76,10 @@ struct Reflection_ServerReflectionRequest { /// Find the proto file which defines an extension extending the given /// message type with the given field number. - var fileContainingExtension: Reflection_ExtensionRequest { + var fileContainingExtension: Grpc_Reflection_V1_ExtensionRequest { get { if case .fileContainingExtension(let v)? = messageRequest {return v} - return Reflection_ExtensionRequest() + return Grpc_Reflection_V1_ExtensionRequest() } set {messageRequest = .fileContainingExtension(newValue)} } @@ -124,7 +124,7 @@ struct Reflection_ServerReflectionRequest { case fileContainingSymbol(String) /// Find the proto file which defines an extension extending the given /// message type with the given field number. - case fileContainingExtension(Reflection_ExtensionRequest) + case fileContainingExtension(Grpc_Reflection_V1_ExtensionRequest) /// Finds the tag numbers used by all known extensions of the given message /// type, and appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the @@ -139,7 +139,7 @@ struct Reflection_ServerReflectionRequest { case listServices(String) #if !swift(>=4.1) - static func ==(lhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest, rhs: Reflection_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 @@ -175,7 +175,7 @@ struct Reflection_ServerReflectionRequest { /// The type name and extension number sent by the client when requesting /// file_containing_extension. -struct Reflection_ExtensionRequest { +struct Grpc_Reflection_V1_ExtensionRequest { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -191,15 +191,15 @@ struct Reflection_ExtensionRequest { } /// The message sent by the server to answer ServerReflectionInfo method. -struct Reflection_ServerReflectionResponse { +struct Grpc_Reflection_V1_ServerReflectionResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. var validHost: String = String() - var originalRequest: Reflection_ServerReflectionRequest { - get {return _originalRequest ?? Reflection_ServerReflectionRequest()} + var originalRequest: Grpc_Reflection_V1_ServerReflectionRequest { + get {return _originalRequest ?? Grpc_Reflection_V1_ServerReflectionRequest()} set {_originalRequest = newValue} } /// Returns true if `originalRequest` has been explicitly set. @@ -209,7 +209,7 @@ struct Reflection_ServerReflectionResponse { /// The server sets one of the following fields according to the message_request /// in the request. - var messageResponse: Reflection_ServerReflectionResponse.OneOf_MessageResponse? = nil + var messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse? = nil /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. @@ -217,37 +217,37 @@ struct Reflection_ServerReflectionResponse { /// FileDescriptorResponse message to encapsulate the repeated fields. /// The reflection service is allowed to avoid sending FileDescriptorProtos /// that were previously sent in response to earlier requests in the stream. - var fileDescriptorResponse: Reflection_FileDescriptorResponse { + var fileDescriptorResponse: Grpc_Reflection_V1_FileDescriptorResponse { get { if case .fileDescriptorResponse(let v)? = messageResponse {return v} - return Reflection_FileDescriptorResponse() + return Grpc_Reflection_V1_FileDescriptorResponse() } set {messageResponse = .fileDescriptorResponse(newValue)} } /// This message is used to answer all_extension_numbers_of_type requests. - var allExtensionNumbersResponse: Reflection_ExtensionNumberResponse { + var allExtensionNumbersResponse: Grpc_Reflection_V1_ExtensionNumberResponse { get { if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} - return Reflection_ExtensionNumberResponse() + return Grpc_Reflection_V1_ExtensionNumberResponse() } set {messageResponse = .allExtensionNumbersResponse(newValue)} } /// This message is used to answer list_services requests. - var listServicesResponse: Reflection_ListServiceResponse { + var listServicesResponse: Grpc_Reflection_V1_ListServiceResponse { get { if case .listServicesResponse(let v)? = messageResponse {return v} - return Reflection_ListServiceResponse() + return Grpc_Reflection_V1_ListServiceResponse() } set {messageResponse = .listServicesResponse(newValue)} } /// This message is used when an error occurs. - var errorResponse: Reflection_ErrorResponse { + var errorResponse: Grpc_Reflection_V1_ErrorResponse { get { if case .errorResponse(let v)? = messageResponse {return v} - return Reflection_ErrorResponse() + return Grpc_Reflection_V1_ErrorResponse() } set {messageResponse = .errorResponse(newValue)} } @@ -263,16 +263,16 @@ struct Reflection_ServerReflectionResponse { /// FileDescriptorResponse message to encapsulate the repeated fields. /// The reflection service is allowed to avoid sending FileDescriptorProtos /// that were previously sent in response to earlier requests in the stream. - case fileDescriptorResponse(Reflection_FileDescriptorResponse) + case fileDescriptorResponse(Grpc_Reflection_V1_FileDescriptorResponse) /// This message is used to answer all_extension_numbers_of_type requests. - case allExtensionNumbersResponse(Reflection_ExtensionNumberResponse) + case allExtensionNumbersResponse(Grpc_Reflection_V1_ExtensionNumberResponse) /// This message is used to answer list_services requests. - case listServicesResponse(Reflection_ListServiceResponse) + case listServicesResponse(Grpc_Reflection_V1_ListServiceResponse) /// This message is used when an error occurs. - case errorResponse(Reflection_ErrorResponse) + case errorResponse(Grpc_Reflection_V1_ErrorResponse) #if !swift(>=4.1) - static func ==(lhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse, rhs: Reflection_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 @@ -301,13 +301,13 @@ struct Reflection_ServerReflectionResponse { init() {} - fileprivate var _originalRequest: Reflection_ServerReflectionRequest? = nil + fileprivate var _originalRequest: Grpc_Reflection_V1_ServerReflectionRequest? = nil } /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. -struct Reflection_FileDescriptorResponse { +struct Grpc_Reflection_V1_FileDescriptorResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -324,7 +324,7 @@ struct Reflection_FileDescriptorResponse { /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. -struct Reflection_ExtensionNumberResponse { +struct Grpc_Reflection_V1_ExtensionNumberResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -341,14 +341,14 @@ struct Reflection_ExtensionNumberResponse { } /// A list of ServiceResponse sent by the server answering list_services request. -struct Reflection_ListServiceResponse { +struct Grpc_Reflection_V1_ListServiceResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// The information of each service may be expanded in the future, so we use /// ServiceResponse message to encapsulate it. - var service: [Reflection_ServiceResponse] = [] + var service: [Grpc_Reflection_V1_ServiceResponse] = [] var unknownFields = SwiftProtobuf.UnknownStorage() @@ -357,7 +357,7 @@ struct Reflection_ListServiceResponse { /// The information of a single service used by ListServiceResponse to answer /// list_services request. -struct Reflection_ServiceResponse { +struct Grpc_Reflection_V1_ServiceResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -372,7 +372,7 @@ struct Reflection_ServiceResponse { } /// The error code and error message sent by the server when an error occurs. -struct Reflection_ErrorResponse { +struct Grpc_Reflection_V1_ErrorResponse { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -388,23 +388,23 @@ struct Reflection_ErrorResponse { } #if swift(>=5.5) && canImport(_Concurrency) -extension Reflection_ServerReflectionRequest: @unchecked Sendable {} -extension Reflection_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} -extension Reflection_ExtensionRequest: @unchecked Sendable {} -extension Reflection_ServerReflectionResponse: @unchecked Sendable {} -extension Reflection_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} -extension Reflection_FileDescriptorResponse: @unchecked Sendable {} -extension Reflection_ExtensionNumberResponse: @unchecked Sendable {} -extension Reflection_ListServiceResponse: @unchecked Sendable {} -extension Reflection_ServiceResponse: @unchecked Sendable {} -extension Reflection_ErrorResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1_ExtensionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ExtensionNumberResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ListServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1_ErrorResponse: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. -fileprivate let _protobuf_package = "reflection" +fileprivate let _protobuf_package = "grpc.reflection.v1" -extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "host"), @@ -439,7 +439,7 @@ extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtob } }() case 5: try { - var v: Reflection_ExtensionRequest? + var v: Grpc_Reflection_V1_ExtensionRequest? var hadOneofValue = false if let current = self.messageRequest { hadOneofValue = true @@ -506,7 +506,7 @@ extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtob try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_ServerReflectionRequest, rhs: Reflection_ServerReflectionRequest) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest) -> Bool { if lhs.host != rhs.host {return false} if lhs.messageRequest != rhs.messageRequest {return false} if lhs.unknownFields != rhs.unknownFields {return false} @@ -514,7 +514,7 @@ extension Reflection_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtob } } -extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "containing_type"), @@ -544,7 +544,7 @@ extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_ExtensionRequest, rhs: Reflection_ExtensionRequest) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ExtensionRequest, rhs: Grpc_Reflection_V1_ExtensionRequest) -> Bool { if lhs.containingType != rhs.containingType {return false} if lhs.extensionNumber != rhs.extensionNumber {return false} if lhs.unknownFields != rhs.unknownFields {return false} @@ -552,7 +552,7 @@ extension Reflection_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._Mes } } -extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "valid_host"), @@ -572,7 +572,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() case 4: try { - var v: Reflection_FileDescriptorResponse? + var v: Grpc_Reflection_V1_FileDescriptorResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -585,7 +585,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } }() case 5: try { - var v: Reflection_ExtensionNumberResponse? + var v: Grpc_Reflection_V1_ExtensionNumberResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -598,7 +598,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } }() case 6: try { - var v: Reflection_ListServiceResponse? + var v: Grpc_Reflection_V1_ListServiceResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -611,7 +611,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } }() case 7: try { - var v: Reflection_ErrorResponse? + var v: Grpc_Reflection_V1_ErrorResponse? var hadOneofValue = false if let current = self.messageResponse { hadOneofValue = true @@ -661,7 +661,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_ServerReflectionResponse, rhs: Reflection_ServerReflectionResponse) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse) -> Bool { if lhs.validHost != rhs.validHost {return false} if lhs._originalRequest != rhs._originalRequest {return false} if lhs.messageResponse != rhs.messageResponse {return false} @@ -670,7 +670,7 @@ extension Reflection_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProto } } -extension Reflection_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "file_descriptor_proto"), @@ -695,14 +695,14 @@ extension Reflection_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobu try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_FileDescriptorResponse, rhs: Reflection_FileDescriptorResponse) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_FileDescriptorResponse, rhs: Grpc_Reflection_V1_FileDescriptorResponse) -> Bool { if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "base_type_name"), @@ -732,7 +732,7 @@ extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtob try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_ExtensionNumberResponse, rhs: Reflection_ExtensionNumberResponse) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ExtensionNumberResponse, rhs: Grpc_Reflection_V1_ExtensionNumberResponse) -> Bool { if lhs.baseTypeName != rhs.baseTypeName {return false} if lhs.extensionNumber != rhs.extensionNumber {return false} if lhs.unknownFields != rhs.unknownFields {return false} @@ -740,7 +740,7 @@ extension Reflection_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtob } } -extension Reflection_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "service"), @@ -765,14 +765,14 @@ extension Reflection_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._ try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_ListServiceResponse, rhs: Reflection_ListServiceResponse) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ListServiceResponse, rhs: Grpc_Reflection_V1_ListServiceResponse) -> Bool { if lhs.service != rhs.service {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Reflection_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ServiceResponse" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "name"), @@ -797,14 +797,14 @@ extension Reflection_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._Mess try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_ServiceResponse, rhs: Reflection_ServiceResponse) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ServiceResponse, rhs: Grpc_Reflection_V1_ServiceResponse) -> Bool { if lhs.name != rhs.name {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Reflection_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { +extension Grpc_Reflection_V1_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ErrorResponse" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "error_code"), @@ -834,7 +834,7 @@ extension Reflection_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._Messag try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Reflection_ErrorResponse, rhs: Reflection_ErrorResponse) -> Bool { + static func ==(lhs: Grpc_Reflection_V1_ErrorResponse, rhs: Grpc_Reflection_V1_ErrorResponse) -> Bool { if lhs.errorCode != rhs.errorCode {return false} if lhs.errorMessage != rhs.errorMessage {return false} if lhs.unknownFields != rhs.unknownFields {return false} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index b3b685813..29b3c6aa4 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -68,7 +68,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testFileByFileName() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( .with { @@ -104,7 +104,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testListServices() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( @@ -128,7 +128,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testFileBySymbol() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( @@ -177,7 +177,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testFileByExtension() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( @@ -241,7 +241,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testAllExtensionNumbersOfType() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( @@ -262,7 +262,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testErrorResponseFileByFileNameRequest() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( .with { @@ -285,7 +285,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testErrorResponseFileBySymbolRequest() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( .with { @@ -305,7 +305,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testErrorResponseFileByExtensionRequest() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( .with { @@ -328,7 +328,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { func testErrorResponseAllExtensionNumbersOfTypeRequest() async throws { try self.setUpServerAndChannel() - let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) let serviceReflectionInfo = client.makeServerReflectionInfoCall() try await serviceReflectionInfo.requestStream.send( .with { From 4d1dc6bd19695cc08aa4e42009213686ad2322df Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 31 Oct 2023 10:54:20 +0000 Subject: [PATCH 148/580] Add missing availability annotations (#1696) --- Sources/GRPCCore/Internal/Result+Catching.swift | 1 + Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Sources/GRPCCore/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift index bf2393752..68bbbebd7 100644 --- a/Sources/GRPCCore/Internal/Result+Catching.swift +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension Result where Failure == any Error { /// Like `Result(catching:)`, but `async`. /// diff --git a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift index cbe5ac742..aee39daf4 100644 --- a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift +++ b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ResultCatchingTests: XCTestCase { func testResultCatching() async { let result = await Result { From c841cbb6acb8dbb118325088976c070d068e5e5c Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Wed, 1 Nov 2023 10:57:06 +0000 Subject: [PATCH 149/580] guard testPreviousRPCAttemptsValidValues (#1698) It relies on `metadata.previousRPCAttempts`, `retryPushback` which are only available on macOS 13.0 and newer. --- Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift index a6bb11a84..25ded0048 100644 --- a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift +++ b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class MetadataGRPCTests: XCTestCase { func testPreviousRPCAttemptsValidValues() { let testData = [("0", 0), ("1", 1), ("-1", -1)] From fe75fb1d0b0728f9612cb323118b73823247fd1e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 1 Nov 2023 12:56:48 +0000 Subject: [PATCH 150/580] Basic client RPC executor (#1693) Motivation: Clients can execute RPCs in a few different ways: straight up as a single attempt, with retries, or with hedging. This is done by the `ClientRPCExecutor`, the bones of which are added in this PR. At a high level, the executor takes a transport, request, serializer, deserializer and response handler and executes the request against a transport and executes the response handler if a response is received or synthesized locally. The executor only returns once the response has been handled. Modifications: - Add the `ClientRPCExecutor` and the one-shot implementation (hedging and retries will follow later) - The `ClientRPCExecutor` uses a `ClientStreamExecutor` which deals in serialized streams - Add a testing harness, which includes type erased transports (and a basic in process transport for testing) and different server behaviours. Result: Can execute one-shot requests. --- Package.swift | 6 + .../GRPCCore/Call/Client/ClientRequest.swift | 12 - .../GRPCCore/Call/Client/ClientResponse.swift | 67 ----- .../ClientRPCExecutor+OneShotExecutor.swift | 143 +++++++++ .../Client/Internal/ClientRPCExecutor.swift | 181 ++++++++++++ .../Internal/ClientRequest+Convenience.swift | 24 ++ .../Internal/ClientResponse+Convenience.swift | 136 +++++++++ .../Internal/ClientStreamExecutor.swift | 271 ++++++++++++++++++ .../Internal/AsyncStream+MakeStream.swift | 31 ++ .../Internal/RPCAsyncSequence+Buffered.swift | 31 ++ .../Streaming/Internal/RPCWriter+Map.swift | 51 ++++ .../Internal/RPCWriter+Serialize.swift | 53 ++++ .../Call/Client/ClientRequestTests.swift | 4 +- ...ientRPCExecutorTestHarness+Transport.swift | 162 +++++++++++ .../ClientRPCExecutorTestHarness.swift | 166 +++++++++++ ...PCExecutorTestHasness+ServerBehavior.swift | 109 +++++++ .../Internal/ClientRPCExecutorTests.swift | 229 +++++++++++++++ .../Test Utilities/Coding+Identity.swift | 28 ++ .../Transport/AnyTransport.swift | 109 +++++++ .../Transport/StreamCountingTransport.swift | 99 +++++++ .../Transport/ThrowingTransport.swift | 49 ++++ 21 files changed, 1881 insertions(+), 80 deletions(-) create mode 100644 Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift create mode 100644 Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift create mode 100644 Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift create mode 100644 Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift create mode 100644 Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift diff --git a/Package.swift b/Package.swift index 2b3ed64cb..ed717cb5e 100644 --- a/Package.swift +++ b/Package.swift @@ -50,6 +50,10 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-collections.git", from: "1.0.5" ), + .package( + url: "https://github.com/apple/swift-atomics.git", + from: "1.2.0" + ), .package( url: "https://github.com/apple/swift-protobuf.git", from: "1.20.2" @@ -125,6 +129,7 @@ extension Target.Dependency { package: "swift-protobuf" ) static let dequeModule: Self = .product(name: "DequeModule", package: "swift-collections") + static let atomics: Self = .product(name: "Atomics", package: "swift-atomics") static let grpcCore: Self = .target(name: "GRPCCore") } @@ -224,6 +229,7 @@ extension Target { dependencies: [ .grpcCore, .dequeModule, + .atomics ] ) diff --git a/Sources/GRPCCore/Call/Client/ClientRequest.swift b/Sources/GRPCCore/Call/Client/ClientRequest.swift index bc03d5a8f..17e5e1077 100644 --- a/Sources/GRPCCore/Call/Client/ClientRequest.swift +++ b/Sources/GRPCCore/Call/Client/ClientRequest.swift @@ -102,15 +102,3 @@ extension ClientRequest { } } } - -// MARK: - Conversion - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ClientRequest.Stream { - @_spi(Testing) - public init(single request: ClientRequest.Single) { - self.init(metadata: request.metadata) { - try await $0.write(request.message) - } - } -} diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index 642e9e0f3..1ccdcfd00 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -366,70 +366,3 @@ extension ClientResponse.Stream { } } } - -// MARK: - Conversion - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ClientResponse.Single { - init(stream response: ClientResponse.Stream) async { - switch response.accepted { - case .success(let contents): - do { - let metadata = contents.metadata - var iterator = contents.bodyParts.makeAsyncIterator() - - // Happy path: message, trailing metadata, nil. - let part1 = try await iterator.next() - let part2 = try await iterator.next() - let part3 = try await iterator.next() - - switch (part1, part2, part3) { - case (.some(.message(let message)), .some(.trailingMetadata(let trailingMetadata)), .none): - let contents = Contents( - metadata: metadata, - message: message, - trailingMetadata: trailingMetadata - ) - self.accepted = .success(contents) - - case (.some(.message), .some(.message), _): - let error = RPCError( - code: .unimplemented, - message: """ - Multiple messages received, but only one is expected. The server may have \ - incorrectly implemented the RPC or the client and server may have a different \ - opinion on whether this RPC streams responses. - """ - ) - self.accepted = .failure(error) - - case (.some(.trailingMetadata), .none, .none): - let error = RPCError( - code: .unimplemented, - message: "No messages received, exactly one was expected." - ) - self.accepted = .failure(error) - - case (_, _, _): - let error = RPCError( - code: .internalError, - message: """ - The stream from the client transport is invalid. This is likely to be an incorrectly \ - implemented transport. Received parts: \([part1, part2, part3])." - """ - ) - self.accepted = .failure(error) - } - } catch let error as RPCError { - // Known error type. - self.accepted = .failure(error) - } catch { - // Unexpected, but should be handled nonetheless. - self.accepted = .failure(RPCError(code: .unknown, message: String(describing: error))) - } - - case .failure(let error): - self.accepted = .failure(error) - } - } -} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift new file mode 100644 index 000000000..5dbc7b510 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -0,0 +1,143 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutor { + /// An executor for requests which doesn't apply retries or hedging. The request has just one + /// attempt at execution. + @usableFromInline + struct OneShotExecutor< + Transport: ClientTransport, + Serializer: MessageSerializer, + Deserializer: MessageDeserializer + > { + @usableFromInline + typealias Input = Serializer.Message + @usableFromInline + typealias Output = Deserializer.Message + + @usableFromInline + let transport: Transport + @usableFromInline + let timeout: Duration? + @usableFromInline + let interceptors: [any ClientInterceptor] + @usableFromInline + let serializer: Serializer + @usableFromInline + let deserializer: Deserializer + + @inlinable + init( + transport: Transport, + timeout: Duration?, + interceptors: [any ClientInterceptor], + serializer: Serializer, + deserializer: Deserializer + ) { + self.transport = transport + self.timeout = timeout + self.interceptors = interceptors + self.serializer = serializer + self.deserializer = deserializer + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutor.OneShotExecutor { + @inlinable + func execute( + request: ClientRequest.Stream, + method: MethodDescriptor, + responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R { + let result = await withTaskGroup( + of: _OneShotExecutorTask.self, + returning: Result.self + ) { group in + if let timeout = self.timeout { + group.addTask { + let result = await Result { + try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + } + return .timedOut(result) + } + } + + let streamExecutor = ClientStreamExecutor(transport: self.transport) + group.addTask { + return .streamExecutorCompleted(await streamExecutor.run()) + } + + group.addTask { + let response = await ClientRPCExecutor.unsafeExecute( + request: request, + method: method, + attempt: 1, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + streamProcessor: streamExecutor + ) + + let result = await Result { + try await responseHandler(response) + } + + return .responseHandled(result) + } + + while let result = await group.next() { + switch result { + case .streamExecutorCompleted(.success): + // Stream finished; wait for the response to be handled. + () + + case .streamExecutorCompleted(.failure): + // Stream execution threw: cancel and wait. + group.cancelAll() + + case .timedOut(.success): + // The deadline passed; cancel the ongoing work group. + group.cancelAll() + + case .timedOut(.failure): + // The deadline task failed (because the task was cancelled). Wait for the response + // to be handled. + () + + case .responseHandled(let result): + // Response handled: cancel any other remaining tasks. + group.cancelAll() + return result + } + } + + // Unreachable: exactly one task returns `responseHandled` and we return when it completes. + fatalError("Internal inconsistency") + } + + return try result.get() + } +} + +@usableFromInline +enum _OneShotExecutorTask { + case streamExecutorCompleted(Result) + case timedOut(Result) + case responseHandled(Result) +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift new file mode 100644 index 000000000..4d460527b --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -0,0 +1,181 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +enum ClientRPCExecutor { + /// Execute the request and handle its response. + /// + /// - Parameters: + /// - request: The request to execute. + /// - method: A description of the method to execute the request against. + /// - configuration: The execution configuration. + /// - serializer: A serializer to convert input messages to bytes. + /// - deserializer: A deserializer to convert bytes to output messages. + /// - transport: The transport to execute the request on. + /// - interceptors: An array of interceptors which the request and response pass through. The + /// interceptors will be called in the order of the array. + /// - handler: A closure for handling the response. Once the closure returns, any resources from + /// the RPC will be torn down. + /// - Returns: The result returns from the `handler`. + @inlinable + static func execute( + request: ClientRequest.Stream, + method: MethodDescriptor, + configuration: ClientRPCExecutionConfiguration, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + transport: some ClientTransport, + interceptors: [any ClientInterceptor], + handler: @Sendable @escaping (ClientResponse.Stream) async throws -> Result + ) async throws -> Result { + switch configuration.executionPolicy { + case .none: + let oneShotExecutor = OneShotExecutor( + transport: transport, + timeout: configuration.timeout, + interceptors: interceptors, + serializer: serializer, + deserializer: deserializer + ) + + return try await oneShotExecutor.execute( + request: request, + method: method, + responseHandler: handler + ) + + case .retry, .hedge: + fatalError() + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutor { + /// Executes a request on a given stream processor. + /// + /// - Warning: This method is "unsafe" because the `streamProcessor` must be running in a task + /// while this function is executing. + /// + /// - Parameters: + /// - request: The request to execute. + /// - method: A description of the method to execute the request against. + /// - attempt: The attempt number of the request. + /// - serializer: A serializer to convert input messages to bytes. + /// - deserializer: A deserializer to convert bytes to output messages. + /// - interceptors: An array of interceptors which the request and response pass through. The + /// interceptors will be called in the order of the array. + /// - streamProcessor: A processor which executes the serialized request. + /// - Returns: The deserialized response. + @inlinable + static func unsafeExecute( + request: ClientRequest.Stream, + method: MethodDescriptor, + attempt: Int, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + interceptors: [any ClientInterceptor], + streamProcessor: ClientStreamExecutor + ) async -> ClientResponse.Stream { + let context = ClientInterceptorContext(descriptor: method) + + return await Self._intercept( + request: request, + context: context, + interceptors: interceptors + ) { request, context in + // Let the server know this is a retry. + var metadata = request.metadata + if attempt > 1 { + metadata.previousRPCAttempts = attempt &- 1 + } + + var response = await streamProcessor.execute( + request: ClientRequest.Stream<[UInt8]>(metadata: metadata) { writer in + try await request.producer(.serializing(into: writer, with: serializer)) + }, + method: context.descriptor + ) + + // Attach the number of previous attempts, it can be useful information for callers. + if attempt > 1 { + switch response.accepted { + case .success(var contents): + contents.metadata.previousRPCAttempts = attempt &- 1 + response.accepted = .success(contents) + + case .failure(var error): + error.metadata.previousRPCAttempts = attempt &- 1 + response.accepted = .failure(error) + } + } + + return response.map { bytes in + try deserializer.deserialize(bytes) + } + } + } + + @inlinable + static func _intercept( + request: ClientRequest.Stream, + context: ClientInterceptorContext, + interceptors: [any ClientInterceptor], + finally: @escaping @Sendable ( + _ request: ClientRequest.Stream, + _ context: ClientInterceptorContext + ) async -> ClientResponse.Stream + ) async -> ClientResponse.Stream { + return await self._intercept( + request: request, + context: context, + iterator: interceptors.makeIterator(), + finally: finally + ) + } + + @inlinable + static func _intercept( + request: ClientRequest.Stream, + context: ClientInterceptorContext, + iterator: Array.Iterator, + finally: @escaping @Sendable ( + _ request: ClientRequest.Stream, + _ context: ClientInterceptorContext + ) async -> ClientResponse.Stream + ) async -> ClientResponse.Stream { + var iterator = iterator + + switch iterator.next() { + case .some(let interceptor): + let iter = iterator + do { + return try await interceptor.intercept(request: request, context: context) { + await self._intercept(request: $0, context: $1, iterator: iter, finally: finally) + } + } catch let error as RPCError { + return ClientResponse.Stream(error: error) + } catch let other { + let error = RPCError(code: .unknown, message: "", cause: other) + return ClientResponse.Stream(error: error) + } + + case .none: + return await finally(request, context) + } + } +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift new file mode 100644 index 000000000..74beb368b --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift @@ -0,0 +1,24 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ClientRequest.Stream { + internal init(single request: ClientRequest.Single) { + self.init(metadata: request.metadata) { + try await $0.write(request.message) + } + } +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift new file mode 100644 index 000000000..505a43af4 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -0,0 +1,136 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ClientResponse.Single { + /// Converts a streaming response into a single response. + /// + /// - Parameter response: The streaming response to convert. + init(stream response: ClientResponse.Stream) async { + switch response.accepted { + case .success(let contents): + do { + let metadata = contents.metadata + var iterator = contents.bodyParts.makeAsyncIterator() + + // Happy path: message, trailing metadata, nil. + let part1 = try await iterator.next() + let part2 = try await iterator.next() + let part3 = try await iterator.next() + + switch (part1, part2, part3) { + case (.some(.message(let message)), .some(.trailingMetadata(let trailingMetadata)), .none): + let contents = Contents( + metadata: metadata, + message: message, + trailingMetadata: trailingMetadata + ) + self.accepted = .success(contents) + + case (.some(.message), .some(.message), _): + let error = RPCError( + code: .unimplemented, + message: """ + Multiple messages received, but only one is expected. The server may have \ + incorrectly implemented the RPC or the client and server may have a different \ + opinion on whether this RPC streams responses. + """ + ) + self.accepted = .failure(error) + + case (.some(.trailingMetadata), .none, .none): + let error = RPCError( + code: .unimplemented, + message: "No messages received, exactly one was expected." + ) + self.accepted = .failure(error) + + case (_, _, _): + let error = RPCError( + code: .internalError, + message: """ + The stream from the client transport is invalid. This is likely to be an incorrectly \ + implemented transport. Received parts: \([part1, part2, part3])." + """ + ) + self.accepted = .failure(error) + } + } catch let error as RPCError { + // Known error type. + self.accepted = .failure(error) + } catch { + // Unexpected, but should be handled nonetheless. + self.accepted = .failure(RPCError(code: .unknown, message: String(describing: error))) + } + + case .failure(let error): + self.accepted = .failure(error) + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ClientResponse.Stream { + /// Creates a streaming response from the given status and metadata. + /// + /// If the ``Status`` has code ``Status/Code-swift.struct/ok`` then an accepted stream is created + /// containing only the provided metadata. Otherwise a failed response is returned with an error + /// created from the status and metadata. + /// + /// - Parameters: + /// - status: The status received from the server. + /// - metadata: The metadata received from the server. + @inlinable + init(status: Status, metadata: Metadata) { + if let error = RPCError(status: status, metadata: metadata) { + self.accepted = .failure(error) + } else { + self.accepted = .success(.init(metadata: [:], bodyParts: .one(.trailingMetadata(metadata)))) + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ClientResponse.Stream { + /// Returns a new response which maps the messages of this response. + /// + /// - Parameter transform: The function to transform each message with. + /// - Returns: The new response. + @inlinable + func map( + _ transform: @escaping @Sendable (Message) throws -> Mapped + ) -> ClientResponse.Stream { + switch self.accepted { + case .success(let contents): + return ClientResponse.Stream( + metadata: self.metadata, + bodyParts: RPCAsyncSequence( + wrapping: contents.bodyParts.map { + switch $0 { + case .message(let message): + return .message(try transform(message)) + case .trailingMetadata(let metadata): + return .trailingMetadata(metadata) + } + } + ) + ) + + case .failure(let error): + return ClientResponse.Stream(accepted: .failure(error)) + } + } +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift new file mode 100644 index 000000000..3a4714025 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -0,0 +1,271 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +internal struct ClientStreamExecutor { + /// The client transport to execute the stream on. + @usableFromInline + let _transport: Transport + + /// An `AsyncStream` and continuation to send and receive processing events on. + @usableFromInline + let _work: (stream: AsyncStream<_Event>, continuation: AsyncStream<_Event>.Continuation) + + @usableFromInline + let _watermarks: (low: Int, high: Int) + + @usableFromInline + enum _Event: Sendable { + /// Send the request on the outbound stream. + case request(ClientRequest.Stream<[UInt8]>, Transport.Outbound) + /// Receive the response from the inbound stream. + case response( + RPCWriter.Contents.BodyPart>.Closable, + UnsafeTransfer + ) + } + + @inlinable + init(transport: Transport, responseStreamWatermarks: (low: Int, high: Int) = (16, 32)) { + self._transport = transport + self._work = AsyncStream.makeStream() + self._watermarks = responseStreamWatermarks + } + + /// Run the stream executor. + /// + /// This is required to be running until the response returned from ``execute(request:method:)`` + /// has been processed. + @inlinable + func run() async -> Result { + await withTaskGroup(of: Result.self) { group in + for await event in self._work.stream { + switch event { + case .request(let request, let outboundStream): + group.addTask { + await self._processRequest(request, on: outboundStream) + } + + case .response(let writer, let iterator): + group.addTask { + await self._processResponse(writer: writer, iterator: iterator) + } + } + } + + while let result = await group.next() { + switch result { + case .success: + () + case .failure: + group.cancelAll() + return result + } + } + + return .success(()) + } + } + + /// Execute a request on the stream executor. + /// + /// The ``run()`` method must be running at the same time as this method. + /// + /// - Parameters: + /// - request: A streaming request. + /// - method: A description of the method to call. + /// - Returns: A streamed response. + @inlinable + func execute( + request: ClientRequest.Stream<[UInt8]>, + method: MethodDescriptor + ) async -> ClientResponse.Stream<[UInt8]> { + // Each execution method can add work to process in the 'run' method. They must not add + // new work once they return. + defer { self._work.continuation.finish() } + + // Open a stream. Return a failed response if we can't open one. + let stream: RPCStream + + do { + stream = try await self._transport.openStream(descriptor: method) + } catch let error as RPCError { + return ClientResponse.Stream(error: error) + } catch let other { + let error = RPCError( + code: .unknown, + message: "Transport failed to create stream.", + cause: other + ) + return ClientResponse.Stream(error: error) + } + + // Start processing the request. + self._work.continuation.yield(.request(request, stream.outbound)) + + // Wait for the first response to determine how to handle the response. + switch await self._waitForFirstResponsePart(on: stream.inbound) { + case .metadata(let metadata, let iterator): + // Expected happy case: the server is processing the request. + + // TODO: (optimisation) use a hint about whether the response is streamed. Use a specialised + // sequence to avoid allocations if it isn't + let responses = RPCAsyncSequence.makeBackpressuredStream( + of: ClientResponse.Stream<[UInt8]>.Contents.BodyPart.self, + watermarks: self._watermarks + ) + + self._work.continuation.yield(.response(responses.writer, iterator)) + return ClientResponse.Stream(metadata: metadata, bodyParts: responses.stream) + + case .status(let status, let metadata): + // Expected unhappy (but okay) case; the server rejected the request. + return ClientResponse.Stream(status: status, metadata: metadata) + + case .failed(let error): + // Very unhappy case: the server did something unexpected. + return ClientResponse.Stream(error: error) + } + } + + @inlinable + func _processRequest>( + _ request: ClientRequest.Stream<[UInt8]>, + on stream: Stream + ) async -> Result { + let result = await Result { + try await stream.write(.metadata(request.metadata)) + try await request.producer(.map(into: stream) { .message($0) }) + }.castError(to: RPCError.self) { other in + RPCError(code: .unknown, message: "Write failed.", cause: other) + } + + switch result { + case .success: + stream.finish() + case .failure(let error): + stream.finish(throwing: error) + } + + return result + } + + @usableFromInline + enum OnFirstResponsePart: Sendable { + case metadata(Metadata, UnsafeTransfer) + case status(Status, Metadata) + case failed(RPCError) + } + + @inlinable + func _waitForFirstResponsePart( + on stream: Transport.Inbound + ) async -> OnFirstResponsePart { + var iterator = stream.makeAsyncIterator() + let result = await Result { + switch try await iterator.next() { + case .metadata(let metadata): + return .metadata(metadata, UnsafeTransfer(iterator)) + + case .status(let status, let metadata): + return .status(status, metadata) + + case .message: + let error = RPCError( + code: .internalError, + message: """ + Invalid stream. The transport returned a message as the first element in the \ + stream, expected metadata. This is likely to be a transport-specific bug. + """ + ) + return .failed(error) + + case .none: + let error = RPCError( + code: .internalError, + message: """ + Invalid stream. The transport returned an empty stream. This is likely to be \ + a transport-specific bug. + """ + ) + return .failed(error) + } + }.castError(to: RPCError.self) { error in + RPCError( + code: .unknown, + message: "The transport threw an unexpected error.", + cause: error + ) + } + + switch result { + case .success(let firstPart): + return firstPart + case .failure(let error): + return .failed(error) + } + } + + @inlinable + func _processResponse( + writer: RPCWriter.Contents.BodyPart>.Closable, + iterator: UnsafeTransfer + ) async -> Result { + var iterator = iterator.wrappedValue + let result = await Result { + while let next = try await iterator.next() { + switch next { + case .metadata(let metadata): + let error = RPCError( + code: .internalError, + message: """ + Received multiple sets of metadata from the transport. This is likely to be a \ + transport specific bug. Metadata received: '\(metadata)'. + """ + ) + throw error + + case .message(let bytes): + try await writer.write(.message(bytes)) + + case .status(let status, let metadata): + if let error = RPCError(status: status, metadata: metadata) { + throw error + } else { + try await writer.write(.trailingMetadata(metadata)) + } + } + } + }.castError(to: RPCError.self) { error in + RPCError( + code: .unknown, + message: "Can't write to output stream, cancelling RPC.", + cause: error + ) + } + + // Make sure the writer is finished. + switch result { + case .success: + writer.finish() + case .failure(let error): + writer.finish(throwing: error) + } + + return result + } +} diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift b/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..ab1f365ac --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift @@ -0,0 +1,31 @@ +/* + * Copyright 2023, 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. + */ + +#if swift(<5.9) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift new file mode 100644 index 000000000..7dfc275b7 --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift @@ -0,0 +1,31 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension RPCAsyncSequence { + @inlinable + static func makeBackpressuredStream( + of elementType: Element.Type = Element.self, + watermarks: (low: Int, high: Int) + ) -> (stream: Self, writer: RPCWriter.Closable) { + let (stream, continuation) = BufferedStream.makeStream( + of: Element.self, + backPressureStrategy: .watermark(low: watermarks.low, high: watermarks.high) + ) + + return (RPCAsyncSequence(wrapping: stream), RPCWriter.Closable(wrapping: continuation)) + } +} diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift new file mode 100644 index 000000000..f99e4852b --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -0,0 +1,51 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@usableFromInline +struct MapRPCWriter: RPCWriterProtocol { + @usableFromInline + typealias Element = Value + + @usableFromInline + let base: RPCWriter + @usableFromInline + let transform: @Sendable (Value) -> Mapped + + @inlinable + init(base: some RPCWriterProtocol, transform: @escaping @Sendable (Value) -> Mapped) { + self.base = RPCWriter(wrapping: base) + self.transform = transform + } + + @inlinable + func write(contentsOf elements: some Sequence) async throws { + let transformed = elements.lazy.map { self.transform($0) } + try await self.base.write(contentsOf: transformed) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension RPCWriter { + @inlinable + static func map( + into writer: some RPCWriterProtocol, + transform: @Sendable @escaping (Element) -> Mapped + ) -> Self { + let mapper = MapRPCWriter(base: writer, transform: transform) + return RPCWriter(wrapping: mapper) + } +} diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift new file mode 100644 index 000000000..5bc2a0e41 --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -0,0 +1,53 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@usableFromInline +struct SerializingRPCWriter: RPCWriterProtocol { + @usableFromInline + typealias Element = Serializer.Message + + @usableFromInline + let base: RPCWriter<[UInt8]> + @usableFromInline + let serializer: Serializer + + @inlinable + init(serializer: Serializer, base: some RPCWriterProtocol<[UInt8]>) { + self.serializer = serializer + self.base = RPCWriter(wrapping: base) + } + + @inlinable + func write(contentsOf elements: some Sequence) async throws { + let requestParts = try elements.map { message in + try self.serializer.serialize(message) + } + + try await self.base.write(contentsOf: requestParts) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension RPCWriter { + @inlinable + static func serializing( + into writer: some RPCWriterProtocol<[UInt8]>, + with serializer: some MessageSerializer + ) -> Self { + return RPCWriter(wrapping: SerializingRPCWriter(serializer: serializer, base: writer)) + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift index a45f11621..f37a06ee5 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@_spi(Testing) import GRPCCore + import XCTest +@testable import GRPCCore + final class ClientRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let (messages, continuation) = AsyncStream.makeStream(of: String.self) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift new file mode 100644 index 000000000..d425aae05 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -0,0 +1,162 @@ +/* + * Copyright 2023, 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 Atomics + +@testable import GRPCCore + +// TODO: replace with real in-process transport + +final class TestingClientTransport: ClientTransport, Sendable { + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable + + let retryThrottle: RetryThrottle + + private let state: LockedValueBox + private enum State { + case unconnected(TestingServerTransport) + case connected(TestingServerTransport) + case closed + } + + fileprivate init(server: TestingServerTransport, throttle: RetryThrottle) { + self.state = LockedValueBox(.unconnected(server)) + self.retryThrottle = throttle + } + + deinit { + self.state.withLockedValue { state in + switch state { + case .unconnected(let server), .connected(let server): + server.stopListening() + case .closed: + () + } + } + } + + func connect(lazily: Bool) async throws { + try self.state.withLockedValue { state in + switch state { + case let .unconnected(server): + state = .connected(server) + + case .connected: + () + + case .closed: + throw RPCError( + code: .failedPrecondition, + message: "Can't connect to server, transport is closed." + ) + } + } + } + + func close() { + self.state.withLockedValue { state in + switch state { + case .unconnected(let server), .connected(let server): + state = .closed + server.stopListening() + + case .closed: + () + } + } + } + + func executionConfiguration( + forMethod descriptor: MethodDescriptor + ) -> ClientRPCExecutionConfiguration? { + nil + } + + func openStream( + descriptor: MethodDescriptor + ) async throws -> RPCStream { + let request = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) + let response = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) + + let clientStream = RPCStream( + descriptor: descriptor, + inbound: response.stream, + outbound: request.writer + ) + + let serverStream = RPCStream( + descriptor: descriptor, + inbound: request.stream, + outbound: response.writer + ) + + let error: RPCError? = self.state.withLockedValue { state in + switch state { + case .connected(let transport): + transport.acceptStream(serverStream) + return nil + + case .unconnected: + return RPCError( + code: .failedPrecondition, + message: "The client transport must be connected before streams can be created." + ) + + case .closed: + return RPCError(code: .failedPrecondition, message: "The client transport is closed.") + } + } + + if let error = error { + serverStream.outbound.finish() + clientStream.outbound.finish() + throw error + } else { + return clientStream + } + } +} + +final class TestingServerTransport: ServerTransport, Sendable { + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable + + typealias Stream = RPCStream + private let accepted: + (stream: AsyncStream, continuation: AsyncStream.Continuation) + + init() { + self.accepted = AsyncStream.makeStream() + } + + fileprivate func acceptStream(_ stream: RPCStream) { + self.accepted.continuation.yield(stream) + } + + func listen() async throws -> RPCAsyncSequence> { + return RPCAsyncSequence(wrapping: self.accepted.stream) + } + + func stopListening() { + self.accepted.continuation.finish() + } + + func spawnClientTransport( + throttle: RetryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + ) -> TestingClientTransport { + return TestingClientTransport(server: self, throttle: throttle) + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift new file mode 100644 index 000000000..41a8998ad --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -0,0 +1,166 @@ +/* + * Copyright 2023, 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 Atomics +import XCTest + +@testable import GRPCCore + +/// A test harness for the ``ClientRPCExecutor``. +/// +/// It provides different hooks for controlling the transport implementation and the behaviour +/// of the server to allow for flexible testing scenarios with minimal boilerplate. The harness +/// also tracks how many streams the client has opened, how many streams the server accepted, and +/// how many streams the client failed to open. +struct ClientRPCExecutorTestHarness { + private let server: ServerStreamHandler + private let clientTransport: StreamCountingClientTransport + private let serverTransport: StreamCountingServerTransport + + var clientStreamsOpened: Int { + self.clientTransport.streamsOpened + } + + var clientStreamOpenFailures: Int { + self.clientTransport.streamFailures + } + + var serverStreamsAccepted: Int { + self.serverTransport.acceptedStreams + } + + init(transport: Transport = .inProcess, server: ServerStreamHandler) { + self.server = server + + switch transport { + case .inProcess: + let server = TestingServerTransport() + let client = server.spawnClientTransport() + self.serverTransport = StreamCountingServerTransport(wrapping: server) + self.clientTransport = StreamCountingClientTransport(wrapping: client) + + case .throwsOnStreamCreation(let code): + let server = TestingServerTransport() // Will never be called. + let client = ThrowOnStreamCreationTransport(code: code) + self.serverTransport = StreamCountingServerTransport(wrapping: server) + self.clientTransport = StreamCountingClientTransport(wrapping: client) + } + } + + enum Transport { + case inProcess + case throwsOnStreamCreation(code: RPCError.Code) + } + + func unary( + request: ClientRequest.Single<[UInt8]>, + configuration: ClientRPCExecutionConfiguration? = nil, + handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void + ) async throws { + try await self.bidirectional( + request: ClientRequest.Stream(single: request), + configuration: configuration + ) { response in + try await handler(ClientResponse.Single(stream: response)) + } + } + + func clientStreaming( + request: ClientRequest.Stream<[UInt8]>, + configuration: ClientRPCExecutionConfiguration? = nil, + handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void + ) async throws { + try await self.bidirectional( + request: request, + configuration: configuration + ) { response in + try await handler(ClientResponse.Single(stream: response)) + } + } + + func serverStreaming( + request: ClientRequest.Single<[UInt8]>, + configuration: ClientRPCExecutionConfiguration? = nil, + handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void + ) async throws { + try await self.bidirectional( + request: ClientRequest.Stream(single: request), + configuration: configuration + ) { response in + try await handler(response) + } + } + + func bidirectional( + request: ClientRequest.Stream<[UInt8]>, + configuration: ClientRPCExecutionConfiguration? = nil, + handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void + ) async throws { + try await self.execute( + request: request, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer(), + configuration: configuration, + handler: handler + ) + } + + private func execute( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + configuration: ClientRPCExecutionConfiguration?, + handler: @escaping @Sendable (ClientResponse.Stream) async throws -> Void + ) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await withThrowingTaskGroup(of: Void.self) { serverGroup in + let streams = try await self.serverTransport.listen() + for try await stream in streams { + serverGroup.addTask { + try await self.server.handle(stream: stream) + } + } + } + } + + try await self.clientTransport.connect(lazily: false) + + let executionConfiguration: ClientRPCExecutionConfiguration + if let configuration = configuration { + executionConfiguration = configuration + } else { + executionConfiguration = ClientRPCExecutionConfiguration(executionPolicy: nil, timeout: nil) + } + + // Execute the request. + try await ClientRPCExecutor.execute( + request: request, + method: MethodDescriptor(service: "foo", method: "bar"), + configuration: executionConfiguration, + serializer: serializer, + deserializer: deserializer, + transport: self.clientTransport, + interceptors: [], + handler: handler + ) + + // Close the client so the server can finish. + self.clientTransport.close() + self.serverTransport.stopListening() + group.cancelAll() + } + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift new file mode 100644 index 000000000..75524ae82 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift @@ -0,0 +1,109 @@ +/* + * Copyright 2023, 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 Atomics +import XCTest + +@testable import GRPCCore + +extension ClientRPCExecutorTestHarness { + struct ServerStreamHandler: Sendable { + private let handler: + @Sendable ( + _ stream: RPCStream, RPCWriter.Closable> + ) async throws -> Void + + init( + _ handler: @escaping @Sendable ( + RPCStream, RPCWriter.Closable> + ) async throws -> Void + ) { + self.handler = handler + } + + func handle( + stream: RPCStream + ) async throws where Inbound.Element == RPCRequestPart, Outbound.Element == RPCResponsePart { + let erased = RPCStream( + descriptor: stream.descriptor, + inbound: RPCAsyncSequence(wrapping: stream.inbound), + outbound: RPCWriter.Closable(wrapping: stream.outbound) + ) + + try await self.handler(erased) + } + } +} + +extension ClientRPCExecutorTestHarness.ServerStreamHandler { + static var echo: Self { + return Self { + stream in + let response = stream.inbound.map { part -> RPCResponsePart in + switch part { + case .metadata(let metadata): + return .metadata(metadata) + case .message(let bytes): + return .message(bytes) + } + } + + try await stream.outbound.write(contentsOf: response) + try await stream.outbound.write(.status(Status(code: .ok, message: ""), [:])) + stream.outbound.finish() + } + } + + static func reject( + withError error: RPCError, + consumeInbound: Bool = false + ) -> Self { + return Self { stream in + if consumeInbound { + for try await _ in stream.inbound {} + } + + // All error codes are valid status codes, '!' is safe. + let status = Status(code: Status.Code(error.code), message: error.message) + try await stream.outbound.write(.status(status, error.metadata)) + stream.outbound.finish() + } + } + + static var failTest: Self { + return Self { stream in + XCTFail("Server accepted unexpected stream") + let status = Status(code: .unknown, message: "Unexpected stream") + try await stream.outbound.write(.status(status, [:])) + stream.outbound.finish() + } + } + + static func attemptBased(_ onAttempt: @Sendable @escaping (_ attempt: Int) -> Self) -> Self { + let attempts = ManagedAtomic(1) + return Self { stream in + let attempt = attempts.loadThenWrappingIncrement(ordering: .sequentiallyConsistent) + let handler = onAttempt(attempt) + try await handler.handle(stream: stream) + } + } + + static func sleepFor(duration: Duration, then handler: Self) -> Self { + return Self { stream in + try await Task.sleep(until: .now.advanced(by: duration), clock: .continuous) + try await handler.handle(stream: stream) + } + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift new file mode 100644 index 000000000..c25fc3d83 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -0,0 +1,229 @@ +/* + * Copyright 2023, 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 ClientRPCExecutorTests: XCTestCase { + func testUnaryEcho() async throws { + let tester = ClientRPCExecutorTestHarness(server: .echo) + try await tester.unary( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { response in + XCTAssertEqual(response.metadata, ["foo": "bar"]) + XCTAssertEqual(try response.message, [1, 2, 3]) + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testClientStreamingEcho() async throws { + let tester = ClientRPCExecutorTestHarness(server: .echo) + try await tester.clientStreaming( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { response in + XCTAssertEqual(response.metadata, ["foo": "bar"]) + XCTAssertEqual(try response.message, [1, 2, 3]) + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testServerStreamingEcho() async throws { + let tester = ClientRPCExecutorTestHarness(server: .echo) + try await tester.serverStreaming( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { response in + XCTAssertEqual(response.metadata, ["foo": "bar"]) + let messages = try await response.messages.collect() + XCTAssertEqual(messages, [[1, 2, 3]]) + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testBidirectionalStreamingEcho() async throws { + let tester = ClientRPCExecutorTestHarness(server: .echo) + try await tester.bidirectional( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { response in + XCTAssertEqual(response.metadata, ["foo": "bar"]) + let messages = try await response.messages.collect() + XCTAssertEqual(messages, [[1, 2, 3]]) + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testUnaryRejectedByServer() async throws { + let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) + let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) + try await tester.unary( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { response in + XCTAssertThrowsRPCError(try response.message) { + XCTAssertEqual($0, error) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testClientStreamingRejectedByServer() async throws { + let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) + let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) + try await tester.clientStreaming( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { response in + XCTAssertThrowsRPCError(try response.message) { + XCTAssertEqual($0, error) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testServerStreamingRejectedByServer() async throws { + let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) + let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) + try await tester.serverStreaming( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { response in + await XCTAssertThrowsRPCErrorAsync { + try await response.messages.collect() + } errorHandler: { + XCTAssertEqual($0, error) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testBidirectionalRejectedByServer() async throws { + let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) + let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) + try await tester.bidirectional( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { response in + await XCTAssertThrowsRPCErrorAsync { + try await response.messages.collect() + } errorHandler: { + XCTAssertEqual($0, error) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 1) + } + + func testUnaryUnableToOpenStream() async throws { + let tester = ClientRPCExecutorTestHarness( + transport: .throwsOnStreamCreation(code: .aborted), + server: .failTest + ) + + try await tester.unary( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { response in + XCTAssertThrowsRPCError(try response.message) { + XCTAssertEqual($0.code, .aborted) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 0) + XCTAssertEqual(tester.clientStreamOpenFailures, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 0) + } + + func testClientStreamingUnableToOpenStream() async throws { + let tester = ClientRPCExecutorTestHarness( + transport: .throwsOnStreamCreation(code: .aborted), + server: .failTest + ) + + try await tester.clientStreaming( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { response in + XCTAssertThrowsRPCError(try response.message) { + XCTAssertEqual($0.code, .aborted) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 0) + XCTAssertEqual(tester.clientStreamOpenFailures, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 0) + } + + func testServerStreamingUnableToOpenStream() async throws { + let tester = ClientRPCExecutorTestHarness( + transport: .throwsOnStreamCreation(code: .aborted), + server: .failTest + ) + + try await tester.serverStreaming( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { response in + await XCTAssertThrowsRPCErrorAsync { + try await response.messages.collect() + } errorHandler: { + XCTAssertEqual($0.code, .aborted) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 0) + XCTAssertEqual(tester.clientStreamOpenFailures, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 0) + } + + func testBidirectionalUnableToOpenStream() async throws { + let tester = ClientRPCExecutorTestHarness( + transport: .throwsOnStreamCreation(code: .aborted), + server: .failTest + ) + + try await tester.bidirectional( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { response in + await XCTAssertThrowsRPCErrorAsync { + try await response.messages.collect() + } errorHandler: { + XCTAssertEqual($0.code, .aborted) + } + } + + XCTAssertEqual(tester.clientStreamsOpened, 0) + XCTAssertEqual(tester.clientStreamOpenFailures, 1) + XCTAssertEqual(tester.serverStreamsAccepted, 0) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift new file mode 100644 index 000000000..335426fad --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift @@ -0,0 +1,28 @@ +/* + * Copyright 2023, 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 + +struct IdentitySerializer: MessageSerializer { + func serialize(_ message: [UInt8]) throws -> [UInt8] { + return message + } +} + +struct IdentityDeserializer: MessageDeserializer { + func deserialize(_ serializedMessageBytes: [UInt8]) throws -> [UInt8] { + return serializedMessageBytes + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift new file mode 100644 index 000000000..7a439f48d --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -0,0 +1,109 @@ +/* + * Copyright 2023, 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. + */ +@testable import GRPCCore + +struct AnyClientTransport: ClientTransport, Sendable { + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable + + private let _retryThrottle: @Sendable () -> RetryThrottle + private let _openStream: @Sendable (MethodDescriptor) async throws -> RPCStream + private let _connect: @Sendable (Bool) async throws -> Void + private let _close: @Sendable () -> Void + private let _configuration: @Sendable (MethodDescriptor) -> ClientRPCExecutionConfiguration? + + init(wrapping transport: Transport) { + self._retryThrottle = { transport.retryThrottle } + self._openStream = { descriptor in + let stream = try await transport.openStream(descriptor: descriptor) + return RPCStream( + descriptor: stream.descriptor, + inbound: RPCAsyncSequence(wrapping: stream.inbound), + outbound: RPCWriter.Closable(wrapping: stream.outbound) + ) + } + + self._connect = { lazily in + try await transport.connect(lazily: lazily) + } + + self._close = { + transport.close() + } + + self._configuration = { descriptor in + transport.executionConfiguration(forMethod: descriptor) + } + } + + var retryThrottle: RetryThrottle { + self._retryThrottle() + } + + func connect(lazily: Bool) async throws { + try await self._connect(lazily) + } + + func close() { + self._close() + } + + func openStream( + descriptor: MethodDescriptor + ) async throws -> RPCStream { + try await self._openStream(descriptor) + } + + func executionConfiguration( + forMethod descriptor: MethodDescriptor + ) -> ClientRPCExecutionConfiguration? { + self._configuration(descriptor) + } +} + +struct AnyServerTransport: ServerTransport, Sendable { + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable + + private let _listen: @Sendable () async throws -> RPCAsyncSequence> + private let _stopListening: @Sendable () -> Void + + init(wrapping transport: Transport) { + self._listen = { + let mapped = try await transport.listen().map { stream in + return RPCStream( + descriptor: stream.descriptor, + inbound: RPCAsyncSequence(wrapping: stream.inbound), + outbound: RPCWriter.Closable(wrapping: stream.outbound) + ) + } + + return RPCAsyncSequence(wrapping: mapped) + } + + self._stopListening = { + transport.stopListening() + } + } + + func listen() async throws -> RPCAsyncSequence> { + try await self._listen() + } + + func stopListening() { + self._stopListening() + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift new file mode 100644 index 000000000..c73d2e442 --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -0,0 +1,99 @@ +/* + * Copyright 2023, 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 Atomics + +@testable import GRPCCore + +struct StreamCountingClientTransport: ClientTransport, Sendable { + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable + + private let transport: AnyClientTransport + private let _streamsOpened = ManagedAtomic(0) + private let _streamFailures = ManagedAtomic(0) + + var streamsOpened: Int { + self._streamsOpened.load(ordering: .sequentiallyConsistent) + } + + var streamFailures: Int { + self._streamFailures.load(ordering: .sequentiallyConsistent) + } + + init(wrapping transport: Transport) { + self.transport = AnyClientTransport(wrapping: transport) + } + + var retryThrottle: RetryThrottle { + self.transport.retryThrottle + } + + func connect(lazily: Bool) async throws { + try await self.transport.connect(lazily: lazily) + } + + func close() { + self.transport.close() + } + + func openStream( + descriptor: MethodDescriptor + ) async throws -> RPCStream { + do { + let stream = try await self.transport.openStream(descriptor: descriptor) + self._streamsOpened.wrappingIncrement(ordering: .sequentiallyConsistent) + return stream + } catch { + self._streamFailures.wrappingIncrement(ordering: .sequentiallyConsistent) + throw error + } + } + + func executionConfiguration( + forMethod descriptor: MethodDescriptor + ) -> ClientRPCExecutionConfiguration? { + self.transport.executionConfiguration(forMethod: descriptor) + } +} + +struct StreamCountingServerTransport: ServerTransport, Sendable { + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable + + private let transport: AnyServerTransport + private let _acceptedStreams = ManagedAtomic(0) + + var acceptedStreams: Int { + self._acceptedStreams.load(ordering: .sequentiallyConsistent) + } + + init(wrapping transport: Transport) { + self.transport = AnyServerTransport(wrapping: transport) + } + + func listen() async throws -> RPCAsyncSequence> { + let mapped = try await self.transport.listen().map { stream in + self._acceptedStreams.wrappingIncrement(ordering: .sequentiallyConsistent) + return stream + } + + return RPCAsyncSequence(wrapping: mapped) + } + + func stopListening() { + self.transport.stopListening() + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift new file mode 100644 index 000000000..7126d970e --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2023, 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. + */ +@testable import GRPCCore + +struct ThrowOnStreamCreationTransport: ClientTransport { + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable + + private let code: RPCError.Code + + init(code: RPCError.Code) { + self.code = code + } + + let retryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + + func connect(lazily: Bool) async throws { + // no-op + } + + func close() { + // no-op + } + + func executionConfiguration( + forMethod descriptor: MethodDescriptor + ) -> ClientRPCExecutionConfiguration? { + return nil + } + + func openStream( + descriptor: MethodDescriptor + ) async throws -> RPCStream { + throw RPCError(code: self.code, message: "") + } +} From 1c19d3f00bd6c42a106e81646a28d72625949a9c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 1 Nov 2023 13:03:50 +0000 Subject: [PATCH 151/580] Convenience method to jitter keepalive interval (#1697) Motivation: If a large number of clients use keepalive and are brought up at the same time, their ping intervals could align resulting in servers receiving large numbers of pings simultaneously. Modifications: - Add a convenience method to the client keepalive configuration which allows the interval to be jittered while maintaining the invariant that the interval must be greater than the timeout. - Add additional checks on the keepalive invariants Result: Users can easily apply jitter to their keepalive configuration. --- Sources/GRPC/ConnectionKeepalive.swift | 92 ++++++++++++++++++-- Tests/GRPCTests/ConnectionManagerTests.swift | 34 ++++++++ 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/Sources/GRPC/ConnectionKeepalive.swift b/Sources/GRPC/ConnectionKeepalive.swift index 841081f81..f3aadf07a 100644 --- a/Sources/GRPC/ConnectionKeepalive.swift +++ b/Sources/GRPC/ConnectionKeepalive.swift @@ -20,13 +20,21 @@ import NIOCore /// The defaults are determined by the gRPC keepalive /// [documentation] (https://github.com/grpc/grpc/blob/master/doc/keepalive.md). public struct ClientConnectionKeepalive: Hashable, Sendable { + private func checkInvariants(line: UInt = #line) { + precondition(self.timeout < self.interval, "'timeout' must be less than 'interval'", line: line) + } + /// The amount of time to wait before sending a keepalive ping. - public var interval: TimeAmount + public var interval: TimeAmount { + didSet { self.checkInvariants() } + } /// The amount of time to wait for an acknowledgment. /// If it does not receive an acknowledgment within this time, it will close the connection /// This value must be less than ``interval``. - public var timeout: TimeAmount + public var timeout: TimeAmount { + didSet { self.checkInvariants() } + } /// Send keepalive pings even if there are no calls in flight. public var permitWithoutCalls: Bool @@ -45,23 +53,63 @@ public struct ClientConnectionKeepalive: Hashable, Sendable { maximumPingsWithoutData: UInt = 2, minimumSentPingIntervalWithoutData: TimeAmount = .minutes(5) ) { - precondition(timeout < interval, "`timeout` must be less than `interval`") self.interval = interval self.timeout = timeout self.permitWithoutCalls = permitWithoutCalls self.maximumPingsWithoutData = maximumPingsWithoutData self.minimumSentPingIntervalWithoutData = minimumSentPingIntervalWithoutData + self.checkInvariants() + } +} + +extension ClientConnectionKeepalive { + /// Applies jitter to the ``interval``. + /// + /// The current ``interval`` will be adjusted by no more than `maxJitter` in either direction, + /// that is the ``interval`` may increase or decrease by no more than `maxJitter`. As + /// the ``timeout`` must be strictly less than the ``interval``, the lower range of the jittered + /// interval is clamped to `max(interval - maxJitter, timeout + .nanoseconds(1)))`. + /// + /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may + /// be applied in either direction. + public mutating func jitterInterval(byAtMost maxJitter: TimeAmount) { + // The interval must be larger than the timeout so clamp the lower bound to be greater than + // the timeout. + let lowerBound = max(self.interval - maxJitter, self.timeout + .nanoseconds(1)) + let upperBound = self.interval + maxJitter + self.interval = .nanoseconds(.random(in: lowerBound.nanoseconds ... upperBound.nanoseconds)) + } + + /// Returns a new ``ClientConnectionKeepalive`` with a jittered ``interval``. + /// + /// See also ``jitterInterval(byAtMost:)``. + /// + /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may + /// be applied in either direction. + /// - Returns: A new ``ClientConnectionKeepalive``. + public func jitteringInterval(byAtMost maxJitter: TimeAmount) -> Self { + var copy = self + copy.jitterInterval(byAtMost: maxJitter) + return copy } } public struct ServerConnectionKeepalive: Hashable { + private func checkInvariants(line: UInt = #line) { + precondition(self.timeout < self.interval, "'timeout' must be less than 'interval'", line: line) + } + /// The amount of time to wait before sending a keepalive ping. - public var interval: TimeAmount + public var interval: TimeAmount { + didSet { self.checkInvariants() } + } /// The amount of time to wait for an acknowledgment. /// If it does not receive an acknowledgment within this time, it will close the connection /// This value must be less than ``interval``. - public var timeout: TimeAmount + public var timeout: TimeAmount { + didSet { self.checkInvariants() } + } /// Send keepalive pings even if there are no calls in flight. public var permitWithoutCalls: Bool @@ -92,7 +140,6 @@ public struct ServerConnectionKeepalive: Hashable { minimumReceivedPingIntervalWithoutData: TimeAmount = .minutes(5), maximumPingStrikes: UInt = 2 ) { - precondition(timeout < interval, "`timeout` must be less than `interval`") self.interval = interval self.timeout = timeout self.permitWithoutCalls = permitWithoutCalls @@ -100,5 +147,38 @@ public struct ServerConnectionKeepalive: Hashable { self.minimumSentPingIntervalWithoutData = minimumSentPingIntervalWithoutData self.minimumReceivedPingIntervalWithoutData = minimumReceivedPingIntervalWithoutData self.maximumPingStrikes = maximumPingStrikes + self.checkInvariants() + } +} + +extension ServerConnectionKeepalive { + /// Applies jitter to the ``interval``. + /// + /// The current ``interval`` will be adjusted by no more than `maxJitter` in either direction, + /// that is the ``interval`` may increase or decrease by no more than `maxJitter`. As + /// the ``timeout`` must be strictly less than the ``interval``, the lower range of the jittered + /// interval is clamped to `max(interval - maxJitter, timeout + .nanoseconds(1)))`. + /// + /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may + /// be applied in either direction. + public mutating func jitterInterval(byAtMost maxJitter: TimeAmount) { + // The interval must be larger than the timeout so clamp the lower bound to be greater than + // the timeout. + let lowerBound = max(self.interval - maxJitter, self.timeout + .nanoseconds(1)) + let upperBound = self.interval + maxJitter + self.interval = .nanoseconds(.random(in: lowerBound.nanoseconds ... upperBound.nanoseconds)) + } + + /// Returns a new ``ClientConnectionKeepalive`` with a jittered ``interval``. + /// + /// See also ``jitterInterval(byAtMost:)``. + /// + /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may + /// be applied in either direction. + /// - Returns: A new ``ClientConnectionKeepalive``. + public func jitteringInterval(byAtMost maxJitter: TimeAmount) -> Self { + var copy = self + copy.jitterInterval(byAtMost: maxJitter) + return copy } } diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 1b45c3191..270e69835 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -1277,6 +1277,40 @@ extension ConnectionManagerTests { XCTAssertThrowsError(try multiplexer.wait()) } + + func testClientKeepaliveJitterWithoutClamping() { + let original = ClientConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) + let keepalive = original.jitteringInterval(byAtMost: .milliseconds(500)) + + XCTAssertGreaterThanOrEqual(keepalive.interval, .milliseconds(1500)) + XCTAssertLessThanOrEqual(keepalive.interval, .milliseconds(2500)) + } + + func testClientKeepaliveJitterClampedToTimeout() { + let original = ClientConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) + let keepalive = original.jitteringInterval(byAtMost: .seconds(2)) + + // Strictly greater than the timeout of 1 seconds. + XCTAssertGreaterThan(keepalive.interval, .seconds(1)) + XCTAssertLessThanOrEqual(keepalive.interval, .seconds(4)) + } + + func testServerKeepaliveJitterWithoutClamping() { + let original = ServerConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) + let keepalive = original.jitteringInterval(byAtMost: .milliseconds(500)) + + XCTAssertGreaterThanOrEqual(keepalive.interval, .milliseconds(1500)) + XCTAssertLessThanOrEqual(keepalive.interval, .milliseconds(2500)) + } + + func testServerKeepaliveJitterClampedToTimeout() { + let original = ServerConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) + let keepalive = original.jitteringInterval(byAtMost: .seconds(2)) + + // Strictly greater than the timeout of 1 seconds. + XCTAssertGreaterThan(keepalive.interval, .seconds(1)) + XCTAssertLessThanOrEqual(keepalive.interval, .seconds(4)) + } } internal struct Change: Hashable, CustomStringConvertible { From 765ff328e16b533649d4748f9829a1ef8c5f1121 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 1 Nov 2023 13:54:44 +0000 Subject: [PATCH 152/580] Add benchmarks for Metadata collection (#1691) --- .github/workflows/ci.yaml | 2 +- Package.swift | 6 + .../GRPCSwiftBenchmark/Benchmarks.swift | 131 +++++++++++++++--- Performance/Benchmarks/Package.swift | 2 +- ...wiftBenchmark.Metadata_Add_binary.p90.json | 7 + ...wiftBenchmark.Metadata_Add_string.p90.json | 7 + ...hmark.Metadata_Iterate_all_values.p90.json | 7 + ...es_when_only_binary_values_stored.p90.json | 7 + ...y_values_when_only_strings_stored.p90.json | 7 + ...rk.Metadata_Iterate_string_values.p90.json | 7 + ...rk.Metadata_Remove_values_for_key.p90.json | 7 + ...wiftBenchmark.Metadata_Add_binary.p90.json | 7 + ...wiftBenchmark.Metadata_Add_string.p90.json | 7 + ...hmark.Metadata_Iterate_all_values.p90.json | 7 + ...es_when_only_binary_values_stored.p90.json | 7 + ...y_values_when_only_strings_stored.p90.json | 7 + ...rk.Metadata_Iterate_string_values.p90.json | 7 + ...rk.Metadata_Remove_values_for_key.p90.json | 7 + ...wiftBenchmark.Metadata_Add_binary.p90.json | 7 + ...wiftBenchmark.Metadata_Add_string.p90.json | 7 + ...hmark.Metadata_Iterate_all_values.p90.json | 7 + ...es_when_only_binary_values_stored.p90.json | 7 + ...y_values_when_only_strings_stored.p90.json | 7 + ...rk.Metadata_Iterate_string_values.p90.json | 7 + ...rk.Metadata_Remove_values_for_key.p90.json | 7 + 25 files changed, 268 insertions(+), 20 deletions(-) create mode 100644 Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_string.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1a707761c..d793ca32d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -108,7 +108,7 @@ jobs: timeout-minutes: 20 - name: Run Benchmarks working-directory: ./Performance/Benchmarks - run: swift package benchmark baseline check --check-absolute-path Thresholds/${{ matrix.swift-version }}/ + run: swift package benchmark baseline check --no-progress --check-absolute-path Thresholds/${{ matrix.swift-version }}/ timeout-minutes: 20 integration-tests: strategy: diff --git a/Package.swift b/Package.swift index ed717cb5e..09cee9608 100644 --- a/Package.swift +++ b/Package.swift @@ -467,6 +467,11 @@ extension Product { name: grpcProductName, targets: [grpcTargetName] ) + + static let grpcCore: Product = .library( + name: "_GRPCCore", + targets: ["GRPCCore"] + ) static let cgrpcZlib: Product = .library( name: cgrpcZlibProductName, @@ -490,6 +495,7 @@ let package = Package( name: grpcPackageName, products: [ .grpc, + .grpcCore, .cgrpcZlib, .protocGenGRPCSwift, .grpcSwiftPlugin, diff --git a/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift b/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift index 59e30ad49..ddd481f4d 100644 --- a/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift +++ b/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift @@ -14,24 +14,119 @@ * limitations under the License. */ import Benchmark -import Foundation +import GRPCCore let benchmarks = { - Benchmark.defaultConfiguration = .init( - metrics: [ - .mallocCountTotal, - .syscalls, - .readSyscalls, - .writeSyscalls, - .memoryLeaked, - .retainCount, - .releaseCount, - ] - ) - - // async code is currently still quite flaky in the number of retain/release it does so we don't measure them today - var configWithoutRetainRelease = Benchmark.defaultConfiguration - configWithoutRetainRelease.metrics.removeAll(where: { $0 == .retainCount || $0 == .releaseCount }) - - // Add Benchmarks here + Benchmark.defaultConfiguration = .init( + metrics: [ + .mallocCountTotal, + .syscalls, + .readSyscalls, + .writeSyscalls, + .memoryLeaked, + .retainCount, + .releaseCount, + ] + ) + + // async code is currently still quite flaky in the number of retain/release it does so we don't measure them today + var configWithoutRetainRelease = Benchmark.defaultConfiguration + configWithoutRetainRelease.metrics.removeAll(where: { $0 == .retainCount || $0 == .releaseCount }) + + Benchmark("Metadata_Add_string") { benchmark in + for _ in benchmark.scaledIterations { + var metadata = Metadata() + for i in 0..<1000 { + metadata.addString("\(i)", forKey: "\(i)") + } + } + } + + Benchmark("Metadata_Add_binary") { benchmark in + let value: [UInt8] = [1, 2, 3] + for _ in benchmark.scaledIterations { + var metadata = Metadata() + + benchmark.startMeasurement() + for i in 0..<1000 { + metadata.addBinary(value, forKey: "\(i)") + } + benchmark.stopMeasurement() + } + } + + Benchmark("Metadata_Remove_values_for_key") { benchmark in + for _ in benchmark.scaledIterations { + var metadata = Metadata() + for i in 0..<1000 { + metadata.addString("value", forKey: "\(i)") + } + + benchmark.startMeasurement() + for i in 0..<1000 { + metadata.removeAllValues(forKey: "\(i)") + } + benchmark.stopMeasurement() + } + } + + Benchmark("Metadata_Iterate_all_values") { benchmark in + for _ in benchmark.scaledIterations { + var metadata = Metadata() + for i in 0..<1000 { + metadata.addString("value", forKey: "key") + } + + benchmark.startMeasurement() + for value in metadata["key"] { + blackHole(value) + } + benchmark.stopMeasurement() + } + } + + Benchmark("Metadata_Iterate_string_values") { benchmark in + for _ in benchmark.scaledIterations { + var metadata = Metadata() + for i in 0..<1000 { + metadata.addString("\(i)", forKey: "key") + } + + benchmark.startMeasurement() + for value in metadata[stringValues: "key"] { + blackHole(value) + } + benchmark.stopMeasurement() + } + } + + Benchmark("Metadata_Iterate_binary_values_when_only_binary_values_stored") { benchmark in + for _ in benchmark.scaledIterations { + var metadata = Metadata() + for i in 0..<1000 { + metadata.addBinary([1], forKey: "key") + } + + benchmark.startMeasurement() + for value in metadata[binaryValues: "key"] { + blackHole(value) + } + benchmark.stopMeasurement() + } + } + + Benchmark("Metadata_Iterate_binary_values_when_only_strings_stored") { benchmark in + for _ in benchmark.scaledIterations { + var metadata = Metadata() + for i in 0..<1000 { + metadata.addString("\(i)", forKey: "key") + } + + benchmark.startMeasurement() + for value in metadata[binaryValues: "key"] { + blackHole(value) + } + benchmark.stopMeasurement() + } + } } diff --git a/Performance/Benchmarks/Package.swift b/Performance/Benchmarks/Package.swift index 74d324150..1fb63864b 100644 --- a/Performance/Benchmarks/Package.swift +++ b/Performance/Benchmarks/Package.swift @@ -30,7 +30,7 @@ let package = Package( name: "GRPCSwiftBenchmark", dependencies: [ .product(name: "Benchmark", package: "package-benchmark"), - .product(name: "GRPC", package: "grpc-swift") + .product(name: "_GRPCCore", package: "grpc-swift") ], path: "Benchmarks/GRPCSwiftBenchmark", plugins: [ diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json new file mode 100644 index 000000000..b20317867 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 1011, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_string.p90.json new file mode 100644 index 000000000..7fde30a69 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_string.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 4012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json new file mode 100644 index 000000000..5605d8671 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1005, + "retainCount" : 1005, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json new file mode 100644 index 000000000..5605d8671 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1005, + "retainCount" : 1005, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json new file mode 100644 index 000000000..451ede0a1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 2000, + "memoryLeaked" : 0, + "releaseCount" : 4005, + "retainCount" : 2005, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json new file mode 100644 index 000000000..5605d8671 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1005, + "retainCount" : 1005, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json new file mode 100644 index 000000000..295dd81c5 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 2002000, + "retainCount" : 1999000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json new file mode 100644 index 000000000..b20317867 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 1011, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json new file mode 100644 index 000000000..7fde30a69 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 4012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json new file mode 100644 index 000000000..ff39fd3c1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1002, + "retainCount" : 1001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json new file mode 100644 index 000000000..ff39fd3c1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1002, + "retainCount" : 1001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json new file mode 100644 index 000000000..9b82ead83 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 2000, + "memoryLeaked" : 0, + "releaseCount" : 4002, + "retainCount" : 2001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json new file mode 100644 index 000000000..ff39fd3c1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1002, + "retainCount" : 1001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json new file mode 100644 index 000000000..295dd81c5 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 2002000, + "retainCount" : 1999000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json new file mode 100644 index 000000000..b20317867 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 1011, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json new file mode 100644 index 000000000..7fde30a69 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 4012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json new file mode 100644 index 000000000..ff39fd3c1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1002, + "retainCount" : 1001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json new file mode 100644 index 000000000..ff39fd3c1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1002, + "retainCount" : 1001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json new file mode 100644 index 000000000..9b82ead83 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 2000, + "memoryLeaked" : 0, + "releaseCount" : 4002, + "retainCount" : 2001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json new file mode 100644 index 000000000..ff39fd3c1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 1002, + "retainCount" : 1001, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json new file mode 100644 index 000000000..295dd81c5 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 2002000, + "retainCount" : 1999000, + "syscalls" : 0 +} From aeff23a753b58ef831e7a52c8f1874d0a8794aee Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 1 Nov 2023 14:11:03 +0000 Subject: [PATCH 153/580] Fix last peer initiated stream ID when quiescing (#1700) Motivation: The idle handler records the ID of the last stream created by the remote peer and uses it when sending GOAWAY frames. In most paths this was correctly updated according to the role the handler played in the connection and the stream ID. However, in the quiescing state it was unconditionally updated. This can lead to cases where the client attempts to send a GOAWAY with a client initiated stream ID: this is invalid and NIO HTTP/2 treats it as a connection level error, closing all open streams. Modification: - Conditionally update the last peer initiated stream ID when quiescing Result: Fewer connection errors --- .../GRPC/GRPCIdleHandlerStateMachine.swift | 10 ++++++- .../GRPCIdleHandlerStateMachineTests.swift | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift b/Sources/GRPC/GRPCIdleHandlerStateMachine.swift index f7e0b7581..9da96068d 100644 --- a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift +++ b/Sources/GRPC/GRPCIdleHandlerStateMachine.swift @@ -275,7 +275,15 @@ struct GRPCIdleHandlerStateMachine { operations.cancelIdleTask(state.idleTask) case var .quiescing(state): - state.lastPeerInitiatedStreamID = streamID + switch state.role { + case .client where streamID.isServerInitiated: + state.lastPeerInitiatedStreamID = streamID + case .server where streamID.isClientInitiated: + state.lastPeerInitiatedStreamID = streamID + default: + () + } + state.openStreams += 1 self.state = .quiescing(state) diff --git a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift b/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift index 7a4e568f3..c17da7f99 100644 --- a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift @@ -535,6 +535,34 @@ class GRPCIdleHandlerStateMachineTests: GRPCTestCase { _ = stateMachine.channelInactive() stateMachine.ratchetDownGoAwayStreamID().assertDoNothing() } + + func testStreamIDWhenQuiescing() { + var stateMachine = self.makeClientStateMachine() + let op1 = stateMachine.receiveSettings([]) + op1.assertConnectionManager(.ready) + + // Open a stream so we enter quiescing when receiving the GOAWAY. + let op2 = stateMachine.streamCreated(withID: 1) + op2.assertDoNothing() + + let op3 = stateMachine.receiveGoAway() + op3.assertConnectionManager(.quiescing) + + // Create a new stream. This can happen if the GOAWAY races with opening the stream; HTTP2 will + // open and then close the stream with an error. + let op4 = stateMachine.streamCreated(withID: 3) + op4.assertDoNothing() + + // Close the newly opened stream. + let op5 = stateMachine.streamClosed(withID: 3) + op5.assertDoNothing() + + // Close the original stream. + let op6 = stateMachine.streamClosed(withID: 1) + // Now we can send a GOAWAY with stream ID zero (we're the client and the server didn't open + // any streams). + XCTAssertEqual(op6.sendGoAwayWithLastPeerInitiatedStreamID, 0) + } } extension GRPCIdleHandlerStateMachine.Operations { From 02ff057bc03a2c5af20d04e4bcd31244e40fe8c5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 1 Nov 2023 14:47:47 +0000 Subject: [PATCH 154/580] Bump version number to 1.20.0 (#1701) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.20.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 1763b2205..57665669a 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,10 +19,10 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 19 + internal static let minor = 20 /// The patch version. - internal static let patch = 1 + internal static let patch = 0 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 663a85221ecf93e4b8fb1f0fdd34d4b27ae78665 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 1 Nov 2023 14:52:47 +0000 Subject: [PATCH 155/580] Additional keepalive diagnostics (#1692) Motivation: It's useful to know the state of keepalive when debugging connection issues. gRPC doesn't emit any logs around this at the moment which makes debugging difficult. Modifications: Add additional logs to the idle handler when: - the idle timeout task is scheduled, cancelled, and fires - the keepalive timer is scheduled - the scheduled close timer is fired - the connection is closed - GOAWAY frames are sent (already logs on receive) - PING frames are sent and received Result: Better visibility into connection lifecycle --- Sources/GRPC/GRPCIdleHandler.swift | 60 ++++++++++++++++++++++-------- Sources/GRPC/Logger.swift | 4 ++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index fc62ec3bd..614d6488e 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -98,19 +98,6 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { ) } - private func sendGoAway(lastStreamID streamID: HTTP2StreamID) { - guard let context = self.context else { - return - } - - let frame = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: streamID, errorCode: .noError, opaqueData: nil) - ) - - context.writeAndFlush(self.wrapOutboundOut(frame), promise: nil) - } - private func perform(operations: GRPCIdleHandlerStateMachine.Operations) { // Prod the connection manager. if let event = operations.connectionManagerEvent, let manager = self.mode.connectionManager { @@ -137,11 +124,17 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { if let idleTask = operations.idleTask { switch idleTask { case let .cancel(task): + self.stateMachine.logger.debug("idle timeout task cancelled") task.cancel() case .schedule: if self.idleTimeout != .nanoseconds(.max), let context = self.context { + self.stateMachine.logger.debug( + "scheduling idle timeout task", + metadata: [MetadataKey.delayMs: "\(self.idleTimeout.milliseconds)"] + ) let task = context.eventLoop.scheduleTask(in: self.idleTimeout) { + self.stateMachine.logger.debug("idle timeout task fired") self.idleTimeoutFired() } self.perform(operations: self.stateMachine.scheduledIdleTimeoutTask(task)) @@ -151,6 +144,13 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { // Send a GOAWAY frame. if let streamID = operations.sendGoAwayWithLastPeerInitiatedStreamID { + self.stateMachine.logger.debug( + "sending GOAWAY frame", + metadata: [ + MetadataKey.h2GoAwayLastStreamID: "\(Int(streamID))" + ] + ) + let goAwayFrame = HTTP2Frame( streamID: .rootStream, payload: .goAway(lastStreamID: streamID, errorCode: .noError, opaqueData: nil) @@ -175,6 +175,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { // Close on the next event-loop tick so we don't drop any events which are // currently being processed. context.eventLoop.execute { + self.stateMachine.logger.debug("closing connection") context.close(mode: .all, promise: nil) } } @@ -186,8 +187,12 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { () case .ack: - // NIO's HTTP2 handler acks for us so this is a no-op. - () + // NIO's HTTP2 handler acks for us so this is a no-op. Log so it doesn't appear that we are + // ignoring pings. + self.stateMachine.logger.debug( + "sending PING frame", + metadata: [MetadataKey.h2PingAck: "true"] + ) case .cancelScheduledTimeout: self.scheduledClose?.cancel() @@ -197,6 +202,15 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { self.schedulePing(in: delay, timeout: timeout) case let .reply(framePayload): + switch framePayload { + case .ping(_, let ack): + self.stateMachine.logger.debug( + "sending PING frame", + metadata: [MetadataKey.h2PingAck: "\(ack)"] + ) + default: + () + } let frame = HTTP2Frame(streamID: .rootStream, payload: framePayload) self.context?.writeAndFlush(self.wrapOutboundOut(frame), promise: nil) @@ -210,6 +224,11 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { return } + self.stateMachine.logger.debug( + "scheduled keepalive pings", + metadata: [MetadataKey.intervalMs: "\(delay.milliseconds)"] + ) + self.scheduledPing = self.context?.eventLoop.scheduleRepeatedTask( initialDelay: delay, delay: delay @@ -226,6 +245,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { private func scheduleClose(in timeout: TimeAmount) { self.scheduledClose = self.context?.eventLoop.scheduleTask(in: timeout) { + self.stateMachine.logger.debug("keepalive timer expired") self.perform(operations: self.stateMachine.shutdownNow()) } } @@ -318,6 +338,10 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { case let .settings(.settings(settings)): self.perform(operations: self.stateMachine.receiveSettings(settings)) case let .ping(data, ack): + self.stateMachine.logger.debug( + "received PING frame", + metadata: [MetadataKey.h2PingAck: "\(ack)"] + ) self.handlePingAction(self.pingHandler.read(pingData: data, ack: ack)) default: // We're not interested in other events. @@ -350,3 +374,9 @@ extension HTTP2SettingsParameter { } } } + +extension TimeAmount { + fileprivate var milliseconds: Int64 { + self.nanoseconds / 1_000_000 + } +} diff --git a/Sources/GRPC/Logger.swift b/Sources/GRPC/Logger.swift index 030a8af55..ce5991aa5 100644 --- a/Sources/GRPC/Logger.swift +++ b/Sources/GRPC/Logger.swift @@ -31,6 +31,10 @@ enum MetadataKey { static let h2DataBytes = "h2_data_bytes" static let h2GoAwayError = "h2_goaway_error" static let h2GoAwayLastStreamID = "h2_goaway_last_stream_id" + static let h2PingAck = "h2_ping_ack" + + static let delayMs = "delay_ms" + static let intervalMs = "interval_ms" static let error = "error" } From b28658fb83d841e9fcf0f2d4b41ddd9e4158647a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 1 Nov 2023 16:33:54 +0000 Subject: [PATCH 156/580] Add some missing Sendable annotations (#1702) Motivation: The latest HTTP2 release added a number of senability annotations which now cause warnings in our internal code. Modifications: - Add missing `Sendable` annotations Result: Fewer warnings --- .../GRPC/ConnectionPool/ConnectionPool+Waiter.swift | 4 ++-- Sources/GRPC/ConnectionPool/ConnectionPool.swift | 10 +++++----- Sources/GRPC/ConnectionPool/PoolManager.swift | 2 +- Sources/GRPC/GRPCServerPipelineConfigurator.swift | 5 ++--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift index b4b386f8b..8a5cd5ad1 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift @@ -30,7 +30,7 @@ extension ConnectionPool { /// The channel initializer. @usableFromInline - internal let _channelInitializer: (Channel) -> EventLoopFuture + internal let _channelInitializer: @Sendable (Channel) -> EventLoopFuture /// The deadline at which the timeout is scheduled. @usableFromInline @@ -51,7 +51,7 @@ extension ConnectionPool { internal init( deadline: NIODeadline, promise: EventLoopPromise, - channelInitializer: @escaping (Channel) -> EventLoopFuture + channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { self._deadline = deadline self._promise = promise diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index e9b1fa1b9..34b4bec80 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -215,7 +215,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { if self.eventLoop.inEventLoop { self._makeStream( @@ -241,7 +241,7 @@ internal final class ConnectionPool { internal func makeStream( deadline: NIODeadline, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> EventLoopFuture { let promise = self.eventLoop.makePromise(of: Channel.self) self.makeStream(deadline: deadline, promise: promise, logger: logger, initializer: initializer) @@ -277,7 +277,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { self.eventLoop.assertInEventLoop() @@ -310,7 +310,7 @@ internal final class ConnectionPool { @inlinable internal func _tryMakeStream( promise: EventLoopPromise, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> Bool { // We shouldn't jump the queue. guard self.waiters.isEmpty else { @@ -344,7 +344,7 @@ internal final class ConnectionPool { deadline: NIODeadline, promise: EventLoopPromise, logger: GRPCLogger, - initializer: @escaping (Channel) -> EventLoopFuture + initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { // Don't overwhelm the pool with too many waiters. guard self.waiters.count < self.maxWaiters else { diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index 76ab818d7..49d184b7e 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -281,7 +281,7 @@ internal final class PoolManager { preferredEventLoop: EventLoop?, deadline: NIODeadline, logger: GRPCLogger, - streamInitializer initializer: @escaping (Channel) -> EventLoopFuture + streamInitializer initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> PooledStreamChannel { let preferredEventLoopID = preferredEventLoop.map { EventLoopID($0) } let reservedPool = self.lock.withLock { diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift index b40b78e56..c1b208e3a 100644 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ b/Sources/GRPC/GRPCServerPipelineConfigurator.swift @@ -104,13 +104,11 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan /// Makes an HTTP/2 multiplexer suitable handling gRPC requests. private func makeHTTP2Multiplexer(for channel: Channel) -> HTTP2StreamMultiplexer { - var logger = self.configuration.logger - return .init( mode: .server, channel: channel, targetWindowSize: self.configuration.httpTargetWindowSize - ) { stream in + ) { [logger = self.configuration.logger] stream in // Sync options were added to the HTTP/2 stream channel in 1.17.0 (we require at least this) // so this shouldn't be `nil`, but it's not a problem if it is. let http2StreamID = try? stream.syncOptions?.getOption(HTTP2StreamChannelOptions.streamID) @@ -119,6 +117,7 @@ final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChan return String(Int(streamID)) } ?? "" + var logger = logger logger[metadataKey: MetadataKey.h2StreamID] = "\(streamID)" do { From d5ad41e7d729f639394e2cef4d2aaffe56de8ad0 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 2 Nov 2023 11:32:43 +0000 Subject: [PATCH 157/580] Add in-process server transport (#1704) Motivation: We want to have a basic in-process transport implementation, to be used for example for testing purposes. Modification: Added a new `InProcessServerTransport`. Result: We now have an in-process implementation of `ServerTransport`. --- .../{Internal => }/RPCWriter+Closable.swift | 9 +- .../Transport/InProcessServerTransport.swift | 62 +++++++++++++ .../InProcessServerTransportTest.swift | 88 +++++++++++++++++++ 3 files changed, 154 insertions(+), 5 deletions(-) rename Sources/GRPCCore/Streaming/{Internal => }/RPCWriter+Closable.swift (89%) create mode 100644 Sources/GRPCCore/Transport/InProcessServerTransport.swift create mode 100644 Tests/GRPCCoreTests/Transport/InProcessServerTransportTest.swift diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift similarity index 89% rename from Sources/GRPCCore/Streaming/Internal/RPCWriter+Closable.swift rename to Sources/GRPCCore/Streaming/RPCWriter+Closable.swift index c376a3f7a..8746a0b0e 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -16,8 +16,7 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriter { - @usableFromInline - struct Closable: ClosableRPCWriterProtocol { + public struct Closable: ClosableRPCWriterProtocol { @usableFromInline let writer: any ClosableRPCWriterProtocol @@ -36,7 +35,7 @@ extension RPCWriter { /// /// - Parameter elements: The elements to write. @inlinable - func write(contentsOf elements: some Sequence) async throws { + public func write(contentsOf elements: some Sequence) async throws { try await self.writer.write(contentsOf: elements) } @@ -45,7 +44,7 @@ extension RPCWriter { /// All writes after ``finish()`` has been called should result in an error /// being thrown. @inlinable - func finish() { + public func finish() { self.writer.finish() } @@ -54,7 +53,7 @@ extension RPCWriter { /// All writes after ``finish(throwing:)`` has been called should result in an error /// being thrown. @inlinable - func finish(throwing error: Error) { + public func finish(throwing error: Error) { self.writer.finish(throwing: error) } } diff --git a/Sources/GRPCCore/Transport/InProcessServerTransport.swift b/Sources/GRPCCore/Transport/InProcessServerTransport.swift new file mode 100644 index 000000000..09409aa6f --- /dev/null +++ b/Sources/GRPCCore/Transport/InProcessServerTransport.swift @@ -0,0 +1,62 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +/// An in-process implementation of a ``ServerTransport``. +public struct InProcessServerTransport: ServerTransport { + public typealias Inbound = RPCAsyncSequence + public typealias Outbound = RPCWriter.Closable + + private let newStreams: AsyncStream> + private let newStreamsContinuation: AsyncStream>.Continuation + + /// Creates a new instance of ``InProcessServerTransport``. + public init() { + (self.newStreams, self.newStreamsContinuation) = AsyncStream.makeStream() + } + + /// Publish a new ``RPCStream``, which will be returned by the transport's ``RPCAsyncSequence``, + /// returned when calling ``listen()``. + /// + /// - Parameter stream: The new ``RPCStream`` to publish. + /// - Throws: ``RPCError`` with code ``RPCError/Code-swift.struct/failedPrecondition`` + /// if the server transport stopped listening to new streams (i.e., if ``stopListening()`` has been called). + internal func acceptStream(_ stream: RPCStream) throws { + let yieldResult = self.newStreamsContinuation.yield(stream) + if case .terminated = yieldResult { + throw RPCError( + code: .failedPrecondition, + message: "The server transport is closed." + ) + } + } + + /// Return a new ``RPCAsyncSequence`` that will contain all published ``RPCStream``s published + /// to this transport using the ``acceptStream(_:)`` method. + /// + /// - Returns: An ``RPCAsyncSequence`` of all published ``RPCStream``s. + public func listen() -> RPCAsyncSequence> { + RPCAsyncSequence(wrapping: self.newStreams) + } + + /// Stop listening to any new ``RPCStream`` publications. + /// + /// All further calls to ``acceptStream(_:)`` will not produce any new elements on the + /// ``RPCAsyncSequence`` returned by ``listen()``. + public func stopListening() { + self.newStreamsContinuation.finish() + } +} diff --git a/Tests/GRPCCoreTests/Transport/InProcessServerTransportTest.swift b/Tests/GRPCCoreTests/Transport/InProcessServerTransportTest.swift new file mode 100644 index 000000000..68306965d --- /dev/null +++ b/Tests/GRPCCoreTests/Transport/InProcessServerTransportTest.swift @@ -0,0 +1,88 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +final class InProcessServerTransportTest: XCTestCase { + func testStartListening() async throws { + let transport = InProcessServerTransport() + let stream = RPCStream, RPCWriter.Closable>( + descriptor: .init(service: "testService", method: "testMethod"), + inbound: .elements([.message([42])]), + outbound: .init( + wrapping: BufferedStream.Source( + storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) + ) + ) + ) + + let streamSequence = transport.listen() + var streamSequenceInterator = streamSequence.makeAsyncIterator() + + try transport.acceptStream(stream) + + let testStream = try await streamSequenceInterator.next() + let messages = try await testStream?.inbound.collect() + XCTAssertEqual(messages, [.message([42])]) + } + + func testStopListening() async throws { + let transport = InProcessServerTransport() + let firstStream = RPCStream< + RPCAsyncSequence, RPCWriter.Closable + >( + descriptor: .init(service: "testService1", method: "testMethod1"), + inbound: .elements([.message([42])]), + outbound: .init( + wrapping: BufferedStream.Source( + storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) + ) + ) + ) + + let streamSequence = transport.listen() + var streamSequenceInterator = streamSequence.makeAsyncIterator() + + try transport.acceptStream(firstStream) + + let firstTestStream = try await streamSequenceInterator.next() + let firstStreamMessages = try await firstTestStream?.inbound.collect() + XCTAssertEqual(firstStreamMessages, [.message([42])]) + + transport.stopListening() + + let secondStream = RPCStream< + RPCAsyncSequence, RPCWriter.Closable + >( + descriptor: .init(service: "testService1", method: "testMethod1"), + inbound: .elements([.message([42])]), + outbound: .init( + wrapping: BufferedStream.Source( + storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) + ) + ) + ) + + XCTAssertThrowsRPCError(try transport.acceptStream(secondStream)) { error in + XCTAssertEqual(error.code, .failedPrecondition) + } + + let secondTestStream = try await streamSequenceInterator.next() + XCTAssertNil(secondTestStream) + } +} From 8a5cb56a041c7a2c00d0e14de17f47a7bcc884a0 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 2 Nov 2023 13:19:38 +0000 Subject: [PATCH 158/580] availability guard usages of async stream (#1705) --- .../GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift | 1 + Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift | 1 + .../ClientRPCExecutorTestHarness+Transport.swift | 2 ++ .../ClientRPCExecutorTestHarness.swift | 1 + .../ClientRPCExecutorTestHasness+ServerBehavior.swift | 2 ++ .../Call/Client/Internal/ClientRPCExecutorTests.swift | 1 + 6 files changed, 8 insertions(+) diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift b/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift index ab1f365ac..68b492869 100644 --- a/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift +++ b/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift @@ -15,6 +15,7 @@ */ #if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension AsyncStream { @inlinable static func makeStream( diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift index f37a06ee5..7d0304260 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ClientRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let (messages, continuation) = AsyncStream.makeStream(of: String.self) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index d425aae05..0309757c3 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -19,6 +19,7 @@ import Atomics // TODO: replace with real in-process transport +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class TestingClientTransport: ClientTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable @@ -130,6 +131,7 @@ final class TestingClientTransport: ClientTransport, Sendable { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class TestingServerTransport: ServerTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 41a8998ad..a0cedf0a4 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -24,6 +24,7 @@ import XCTest /// of the server to allow for flexible testing scenarios with minimal boilerplate. The harness /// also tracks how many streams the client has opened, how many streams the server accepted, and /// how many streams the client failed to open. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ClientRPCExecutorTestHarness { private let server: ServerStreamHandler private let clientTransport: StreamCountingClientTransport diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift index 75524ae82..cf5c2b701 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ClientRPCExecutorTestHarness { struct ServerStreamHandler: Sendable { private let handler: @@ -47,6 +48,7 @@ extension ClientRPCExecutorTestHarness { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { return Self { diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index c25fc3d83..bb7f3be85 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) From ad926b51a9fecf1760d4c1497be2ed79380f77cf Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 3 Nov 2023 09:20:44 +0000 Subject: [PATCH 159/580] propagate availability guards (#1706) --- .../ClientRPCExecutorTestHarness+Transport.swift | 4 ++-- .../ClientRPCExecutorTestHarness.swift | 2 +- .../ClientRPCExecutorTestHasness+ServerBehavior.swift | 4 ++-- .../Call/Client/Internal/ClientRPCExecutorTests.swift | 2 +- .../GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift | 2 ++ .../Test Utilities/Transport/StreamCountingTransport.swift | 2 ++ .../Test Utilities/Transport/ThrowingTransport.swift | 1 + 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 0309757c3..d93a34862 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -19,7 +19,7 @@ import Atomics // TODO: replace with real in-process transport -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class TestingClientTransport: ClientTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable @@ -131,7 +131,7 @@ final class TestingClientTransport: ClientTransport, Sendable { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class TestingServerTransport: ServerTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index a0cedf0a4..7d46ff9c2 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -24,7 +24,7 @@ import XCTest /// of the server to allow for flexible testing scenarios with minimal boilerplate. The harness /// also tracks how many streams the client has opened, how many streams the server accepted, and /// how many streams the client failed to open. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct ClientRPCExecutorTestHarness { private let server: ServerStreamHandler private let clientTransport: StreamCountingClientTransport diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift index cf5c2b701..41537e83f 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClientRPCExecutorTestHarness { struct ServerStreamHandler: Sendable { private let handler: @@ -48,7 +48,7 @@ extension ClientRPCExecutorTestHarness { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { return Self { diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index bb7f3be85..505745c86 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index 7a439f48d..b4967955a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -15,6 +15,7 @@ */ @testable import GRPCCore +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct AnyClientTransport: ClientTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable @@ -74,6 +75,7 @@ struct AnyClientTransport: ClientTransport, Sendable { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct AnyServerTransport: ServerTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index c73d2e442..ec5e2e5fb 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -17,6 +17,7 @@ import Atomics @testable import GRPCCore +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct StreamCountingClientTransport: ClientTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable @@ -69,6 +70,7 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct StreamCountingServerTransport: ServerTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 7126d970e..c340021c6 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -15,6 +15,7 @@ */ @testable import GRPCCore +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct ThrowOnStreamCreationTransport: ClientTransport { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable From 55196026c0afe089a35a60f0253ab30a709a89a0 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:53:52 +0000 Subject: [PATCH 160/580] Added ReflectionService initialiser using file paths (#1699) Motivation: Initializing the Reflection Service using file descriptor protos is too laborious for users, so simply passing the file paths of the binary files containing the serialized file descriptor protos and letting the Service extract the data from those paths is more convenient. Modifications: Added a new initializer that takes in file paths and extracts and converts to file descriptor proto format the Data from the files. Also modified the integration tests to create temporary files containing the serialized file descriptor protos. Result: GRPCUsers can add the ReflectionService to their Server in a more convenient way. --- .../Server/ReflectionService.swift | 59 ++++++++++++++ .../ReflectionServiceUnitTests.swift | 76 +++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 40450c767..9214b69a3 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -26,6 +26,24 @@ public final class ReflectionService: CallHandlerProvider, Sendable { self.reflectionService.serviceName } + /// Creates a `ReflectionService` by loading serialized reflection data created by `protoc-gen-grpc-swift`. + /// + /// You can generate serialized reflection data using the `protoc-gen-grpc-swift` plugin for `protoc` by + /// setting the `ReflectionData` option to `True`. The paths provided should be absolute or relative to the + /// current working directory. + /// + /// - Parameter filePaths: The paths to files containing serialized reflection data. + /// + /// - Throws: When a file can't be read from disk or parsed. + public init(serializedFileDescriptorProtoFilePaths filePaths: [String]) throws { + let fileDescriptorProtos = try ReflectionService.readSerializedFileDescriptorProtos( + atPaths: filePaths + ) + self.reflectionService = try ReflectionServiceProvider( + fileDescriptorProtos: fileDescriptorProtos + ) + } + public init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { self.reflectionService = try ReflectionServiceProvider(fileDescriptorProtos: fileDescriptors) } @@ -425,3 +443,44 @@ where Success == Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageRespon } } } + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ReflectionService { + static func readSerializedFileDescriptorProto( + atPath path: String + ) throws -> Google_Protobuf_FileDescriptorProto { + let fileURL: URL + #if os(Linux) + fileURL = URL(fileURLWithPath: path) + #else + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + fileURL = URL(filePath: path, directoryHint: .notDirectory) + } else { + fileURL = URL(fileURLWithPath: path) + } + #endif + let binaryData = try Data(contentsOf: fileURL) + guard let serializedData = Data(base64Encoded: binaryData) else { + throw GRPCStatus( + code: .invalidArgument, + message: + """ + The \(path) file contents could not be transformed \ + into serialized data representing a file descriptor proto. + """ + ) + } + return try Google_Protobuf_FileDescriptorProto(serializedData: serializedData) + } + + static func readSerializedFileDescriptorProtos( + atPaths paths: [String] + ) throws -> [Google_Protobuf_FileDescriptorProto] { + var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]() + fileDescriptorProtos.reserveCapacity(paths.count) + for path in paths { + try fileDescriptorProtos.append(readSerializedFileDescriptorProto(atPath: path)) + } + return fileDescriptorProtos + } +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index 453edaa9a..570748eb7 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -519,4 +519,80 @@ final class ReflectionServiceUnitTests: GRPCTestCase { XCTAssertEqual(try extensionsFieldNumbersOfTypeResult.get(), [1, 2, 3, 4, 5, 130]) } + + func testReadSerializedFileDescriptorProto() throws { + let initialFileDescriptorProto = generateFileDescriptorProto(fileName: "test", suffix: "1") + let data = try initialFileDescriptorProto.serializedData().base64EncodedData() + #if os(Linux) + let temporaryDirectory = "/tmp/" + #else + let temporaryDirectory = FileManager.default.temporaryDirectory.path() + #endif + let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" + FileManager.default.createFile(atPath: filePath, contents: data) + defer { + XCTAssertNoThrow(try FileManager.default.removeItem(atPath: filePath)) + } + let reflectionServiceFileDescriptorProto = + try ReflectionService.readSerializedFileDescriptorProto(atPath: filePath) + XCTAssertEqual(reflectionServiceFileDescriptorProto, initialFileDescriptorProto) + } + + func testReadSerializedFileDescriptorProtoInvalidFileContents() throws { + let invalidData = "%%%%%££££".data(using: .utf8) + #if os(Linux) + let temporaryDirectory = "/tmp/" + #else + let temporaryDirectory = FileManager.default.temporaryDirectory.path() + #endif + let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" + FileManager.default.createFile(atPath: filePath, contents: invalidData) + defer { + XCTAssertNoThrow(try FileManager.default.removeItem(atPath: filePath)) + } + + XCTAssertThrowsGRPCStatus( + try ReflectionService.readSerializedFileDescriptorProto(atPath: filePath) + ) { + status in + XCTAssertEqual( + status, + GRPCStatus( + code: .invalidArgument, + message: + """ + The \(filePath) file contents could not be transformed \ + into serialized data representing a file descriptor proto. + """ + ) + ) + } + } + + func testReadSerializedFileDescriptorProtos() throws { + let initialFileDescriptorProtos = makeProtosWithDependencies() + var filePaths: [String] = [] + + for initialFileDescriptorProto in initialFileDescriptorProtos { + let data = try initialFileDescriptorProto.serializedData() + .base64EncodedData() + #if os(Linux) + let temporaryDirectory = "/tmp/" + #else + let temporaryDirectory = FileManager.default.temporaryDirectory.path() + #endif + let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" + FileManager.default.createFile(atPath: filePath, contents: data) + filePaths.append(filePath) + } + defer { + for filePath in filePaths { + XCTAssertNoThrow(try FileManager.default.removeItem(atPath: filePath)) + } + } + + let reflectionServiceFileDescriptorProtos = + try ReflectionService.readSerializedFileDescriptorProtos(atPaths: filePaths) + XCTAssertEqual(reflectionServiceFileDescriptorProtos, initialFileDescriptorProtos) + } } From b52f944f0e5ee1d91ae94b5a42dae2b8e354526f Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:23:44 +0000 Subject: [PATCH 161/580] Added availability check for OS version in unit tests for getting the path of the temporary directory (#1709) Motivation: The .path() version is available only on macOS 13.0 or newer, hence the Skywagon errors. Modifications: Checked availability for macOS 13.0 or newer for calling the .path() function and used the "/tmp" directory for the older versions. Result: After your change, what will change. --- .../ReflectionServiceUnitTests.swift | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index 570748eb7..a21dfe2e8 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -523,10 +523,15 @@ final class ReflectionServiceUnitTests: GRPCTestCase { func testReadSerializedFileDescriptorProto() throws { let initialFileDescriptorProto = generateFileDescriptorProto(fileName: "test", suffix: "1") let data = try initialFileDescriptorProto.serializedData().base64EncodedData() + let temporaryDirectory: String #if os(Linux) - let temporaryDirectory = "/tmp/" + temporaryDirectory = "/tmp/" #else - let temporaryDirectory = FileManager.default.temporaryDirectory.path() + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + temporaryDirectory = FileManager.default.temporaryDirectory.path() + } else { + temporaryDirectory = "/tmp/" + } #endif let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" FileManager.default.createFile(atPath: filePath, contents: data) @@ -540,10 +545,15 @@ final class ReflectionServiceUnitTests: GRPCTestCase { func testReadSerializedFileDescriptorProtoInvalidFileContents() throws { let invalidData = "%%%%%££££".data(using: .utf8) + let temporaryDirectory: String #if os(Linux) - let temporaryDirectory = "/tmp/" + temporaryDirectory = "/tmp/" #else - let temporaryDirectory = FileManager.default.temporaryDirectory.path() + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + temporaryDirectory = FileManager.default.temporaryDirectory.path() + } else { + temporaryDirectory = "/tmp/" + } #endif let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" FileManager.default.createFile(atPath: filePath, contents: invalidData) @@ -576,10 +586,15 @@ final class ReflectionServiceUnitTests: GRPCTestCase { for initialFileDescriptorProto in initialFileDescriptorProtos { let data = try initialFileDescriptorProto.serializedData() .base64EncodedData() + let temporaryDirectory: String #if os(Linux) - let temporaryDirectory = "/tmp/" + temporaryDirectory = "/tmp/" #else - let temporaryDirectory = FileManager.default.temporaryDirectory.path() + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + temporaryDirectory = FileManager.default.temporaryDirectory.path() + } else { + temporaryDirectory = "/tmp/" + } #endif let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" FileManager.default.createFile(atPath: filePath, contents: data) From 017dc09f11fa8856e3b28b021fd66617bcc0ae37 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 8 Nov 2023 13:17:28 +0000 Subject: [PATCH 162/580] Add support for retries (#1708) Motivation: The `ClientRPCExecutor` currently ignores retry and hedging policies. This change adds support for retries. Modifications: - Add a retry executor and wire it up to the client rpc executor - Add a few missing state transitions to the broadcasts sequence Result: RPC can be retried under certain conditions --- .../ClientRPCExecutor+OneShotExecutor.swift | 11 +- .../ClientRPCExecutor+RetryExecutor.swift | 306 ++++++++++++++++++ .../Client/Internal/ClientRPCExecutor.swift | 19 +- .../Internal/ClientStreamExecutor.swift | 28 +- .../Client/Internal/RetryDelaySequence.swift | 95 ++++++ .../BroadcastAsyncSequence+RPCWriter.swift | 25 ++ .../Internal/BroadcastAsyncSequence.swift | 161 +++++---- .../ClientRPCExecutorTests+Retries.swift | 303 +++++++++++++++++ .../Call/Client/RetryDelaySequenceTests.swift | 92 ++++++ 9 files changed, 955 insertions(+), 85 deletions(-) create mode 100644 Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift create mode 100644 Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index 5dbc7b510..1800f9ea2 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -80,7 +80,8 @@ extension ClientRPCExecutor.OneShotExecutor { let streamExecutor = ClientStreamExecutor(transport: self.transport) group.addTask { - return .streamExecutorCompleted(await streamExecutor.run()) + await streamExecutor.run() + return .streamExecutorCompleted } group.addTask { @@ -103,14 +104,10 @@ extension ClientRPCExecutor.OneShotExecutor { while let result = await group.next() { switch result { - case .streamExecutorCompleted(.success): + case .streamExecutorCompleted: // Stream finished; wait for the response to be handled. () - case .streamExecutorCompleted(.failure): - // Stream execution threw: cancel and wait. - group.cancelAll() - case .timedOut(.success): // The deadline passed; cancel the ongoing work group. group.cancelAll() @@ -137,7 +134,7 @@ extension ClientRPCExecutor.OneShotExecutor { @usableFromInline enum _OneShotExecutorTask { - case streamExecutorCompleted(Result) + case streamExecutorCompleted case timedOut(Result) case responseHandled(Result) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift new file mode 100644 index 000000000..083f22b1e --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -0,0 +1,306 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutor { + @usableFromInline + struct RetryExecutor< + Transport: ClientTransport, + Serializer: MessageSerializer, + Deserializer: MessageDeserializer + > { + @usableFromInline + typealias Input = Serializer.Message + @usableFromInline + typealias Output = Deserializer.Message + + @usableFromInline + let transport: Transport + @usableFromInline + let policy: RetryPolicy + @usableFromInline + let timeout: Duration? + @usableFromInline + let interceptors: [any ClientInterceptor] + @usableFromInline + let serializer: Serializer + @usableFromInline + let deserializer: Deserializer + @usableFromInline + let bufferSize: Int + + @inlinable + init( + transport: Transport, + policy: RetryPolicy, + timeout: Duration?, + interceptors: [any ClientInterceptor], + serializer: Serializer, + deserializer: Deserializer, + bufferSize: Int + ) { + self.transport = transport + self.policy = policy + self.timeout = timeout + self.interceptors = interceptors + self.serializer = serializer + self.deserializer = deserializer + self.bufferSize = bufferSize + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutor.RetryExecutor { + @inlinable + func execute( + request: ClientRequest.Stream, + method: MethodDescriptor, + responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R { + // There's quite a lot going on here... + // + // The high level approach is to have two levels of task group. In the outer level tasks are + // run to: + // - run a timeout task (if necessary), + // - run the request producer so that it writes into a broadcast sequence (in this instance we + // don't care about broadcasting but the sequence's ability to replay) + // - run the inner task group. + // + // An inner task group is run for each RPC attempt. We might also pause between attempts. The + // inner group runs two tasks: + // - a stream executor, and + // - the unsafe RPC executor which inspects the response, either passing it to the handler or + // deciding a retry should be undertaken. + // + // It is also worth noting that the server can override the retry delay using "pushback" and + // retries may be skipped if the throttle is applied. + let result = await withTaskGroup( + of: _RetryExecutorTask.self, + returning: Result.self + ) { group in + // Add a task to limit the overall execution time of the RPC. + if let timeout = self.timeout { + group.addTask { + let result = await Result { + try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + } + return .timedOut(result) + } + } + + // Play the original request into the broadcast sequence and construct a replayable request. + let retry = BroadcastAsyncSequence.makeStream(bufferSize: self.bufferSize) + group.addTask { + let result = await Result { + try await request.producer(RPCWriter(wrapping: retry.continuation)) + } + retry.continuation.finish(with: result) + return .outboundFinished(result) + } + + // The sequence isn't limited by the number of attempts as the iterator is reset when the + // server applies pushback. + let delaySequence = RetryDelaySequence(policy: self.policy) + var delayIterator = delaySequence.makeIterator() + + for attempt in 1 ... self.policy.maximumAttempts { + group.addTask { + await withTaskGroup( + of: _RetryExecutorSubTask.self, + returning: _RetryExecutorTask.self + ) { thisAttemptGroup in + let streamExecutor = ClientStreamExecutor(transport: self.transport) + thisAttemptGroup.addTask { + await streamExecutor.run() + return .streamProcessed + } + + thisAttemptGroup.addTask { + let response = await ClientRPCExecutor.unsafeExecute( + request: ClientRequest.Stream(metadata: request.metadata) { + try await $0.write(contentsOf: retry.stream) + }, + method: method, + attempt: attempt, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + streamProcessor: streamExecutor + ) + + let shouldRetry: Bool + let retryDelayOverride: Duration? + + switch response.accepted { + case .success: + // Request was accepted. This counts as success to the throttle and there's no need + // to retry. + self.transport.retryThrottle.recordSuccess() + retryDelayOverride = nil + shouldRetry = false + + case .failure(let error): + // The request was rejected. Determine whether a retry should be carried out. The + // following conditions must be checked: + // + // - Whether the status code is retryable. + // - Whether more attempts are permitted by the config. + // - Whether the throttle permits another retry to be carried out. + // - Whether the server pushed back to either stop further retries or to override + // the delay before the next retry. + let code = Status.Code(error.code) + let isRetryableStatusCode = self.policy.retryableStatusCodes.contains(code) + + if isRetryableStatusCode { + // Counted as failure for throttling. + let throttled = self.transport.retryThrottle.recordFailure() + + // Status code can be retried, Did the server send pushback? + switch error.metadata.retryPushback { + case .retryAfter(let delay): + // Pushback: only retry if our config permits it. + shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + retryDelayOverride = delay + case .stopRetrying: + // Server told us to stop trying. + shouldRetry = false + retryDelayOverride = nil + case .none: + // No pushback: only retry if our config permits it. + shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + retryDelayOverride = nil + break + } + } else { + // Not-retryable; this is considered a success. + self.transport.retryThrottle.recordSuccess() + shouldRetry = false + retryDelayOverride = nil + } + } + + if shouldRetry { + // Cancel subscribers of the broadcast sequence. This is safe as we are the only + // subscriber and maximises the chances that 'isKnownSafeForNextSubscriber' will + // return true. + // + // Note: this must only be called if we should retry, otherwise we may cancel a + // subscriber for an accepted request. + retry.stream.invalidateAllSubscriptions() + + // Only retry if we know it's safe for the next subscriber, that is, the first + // element is still in the buffer. It's safe to call this because there's only + // ever one attempt at a time and the existing subscribers have been invalidated. + if retry.stream.isKnownSafeForNextSubscriber { + return .retry(retryDelayOverride) + } + } + + // Not retrying or not safe to retry. + let result = await Result { + // Check for cancellation; the RPC may have timed out in which case we should skip + // the response handler. + try Task.checkCancellation() + return try await responseHandler(response) + } + return .handledResponse(result) + } + + while let result = await thisAttemptGroup.next() { + switch result { + case .streamProcessed: + () // Continue processing; wait for the response to be handled. + + case .retry(let delayOverride): + thisAttemptGroup.cancelAll() + return .retry(delayOverride) + + case .handledResponse(let result): + thisAttemptGroup.cancelAll() + return .handledResponse(result) + } + } + + fatalError("Internal inconsistency") + } + } + + loop: while let next = await group.next() { + switch next { + case .handledResponse(let result): + // A usable response; cancel the remaining work and return the result. + group.cancelAll() + return result + + case .retry(let delayOverride): + // The attempt failed, wait a bit and then retry. The server might have overridden the + // delay via pushback so preferentially use that value. + // + // Any error will come from cancellation: if it happens while we're sleeping we can + // just loop around, the next attempt will be cancelled immediately and we will return + // its response to the client. + if let delayOverride = delayOverride { + // If the delay is overridden with server pushback then reset the iterator for the + // next retry. + delayIterator = delaySequence.makeIterator() + try? await Task.sleep(until: .now.advanced(by: delayOverride), clock: .continuous) + } else { + // The delay iterator never terminates. + try? await Task.sleep( + until: .now.advanced(by: delayIterator.next()!), + clock: .continuous + ) + } + + break loop // from the while loop so another attempt can be started. + + case .timedOut(.success), .outboundFinished(.failure): + // Timeout task fired successfully or failed to process the outbound stream. Cancel and + // wait for a usable response (which is likely to be an error). + group.cancelAll() + + case .timedOut(.failure), .outboundFinished(.success): + // Timeout task failed which means it was cancelled (so no need to cancel again) or the + // outbound stream was successfully processed (so don't need to do anything). + () + } + } + } + + fatalError("Internal inconsistency") + } + + return try result.get() + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +enum _RetryExecutorTask { + case timedOut(Result) + case handledResponse(Result) + case retry(Duration?) + case outboundFinished(Result) +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +enum _RetryExecutorSubTask { + case streamProcessed + case handledResponse(Result) + case retry(Duration?) +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 4d460527b..6896dae59 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -58,7 +58,24 @@ enum ClientRPCExecutor { responseHandler: handler ) - case .retry, .hedge: + case .retry(let policy): + let retryExecutor = RetryExecutor( + transport: transport, + policy: policy, + timeout: configuration.timeout, + interceptors: interceptors, + serializer: serializer, + deserializer: deserializer, + bufferSize: 64 // TODO: the client should have some control over this. + ) + + return try await retryExecutor.execute( + request: request, + method: method, + responseHandler: handler + ) + + case .hedge: fatalError() } } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 3a4714025..32f6389c5 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -51,8 +51,8 @@ internal struct ClientStreamExecutor { /// This is required to be running until the response returned from ``execute(request:method:)`` /// has been processed. @inlinable - func run() async -> Result { - await withTaskGroup(of: Result.self) { group in + func run() async { + await withTaskGroup(of: Void.self) { group in for await event in self._work.stream { switch event { case .request(let request, let outboundStream): @@ -66,18 +66,6 @@ internal struct ClientStreamExecutor { } } } - - while let result = await group.next() { - switch result { - case .success: - () - case .failure: - group.cancelAll() - return result - } - } - - return .success(()) } } @@ -117,8 +105,10 @@ internal struct ClientStreamExecutor { // Start processing the request. self._work.continuation.yield(.request(request, stream.outbound)) + let part = await self._waitForFirstResponsePart(on: stream.inbound) + // Wait for the first response to determine how to handle the response. - switch await self._waitForFirstResponsePart(on: stream.inbound) { + switch part { case .metadata(let metadata, let iterator): // Expected happy case: the server is processing the request. @@ -146,7 +136,7 @@ internal struct ClientStreamExecutor { func _processRequest>( _ request: ClientRequest.Stream<[UInt8]>, on stream: Stream - ) async -> Result { + ) async { let result = await Result { try await stream.write(.metadata(request.metadata)) try await request.producer(.map(into: stream) { .message($0) }) @@ -160,8 +150,6 @@ internal struct ClientStreamExecutor { case .failure(let error): stream.finish(throwing: error) } - - return result } @usableFromInline @@ -224,7 +212,7 @@ internal struct ClientStreamExecutor { func _processResponse( writer: RPCWriter.Contents.BodyPart>.Closable, iterator: UnsafeTransfer - ) async -> Result { + ) async { var iterator = iterator.wrappedValue let result = await Result { while let next = try await iterator.next() { @@ -265,7 +253,5 @@ internal struct ClientStreamExecutor { case .failure(let error): writer.finish(throwing: error) } - - return result } } diff --git a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift new file mode 100644 index 000000000..b07691e4e --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -0,0 +1,95 @@ +/* + * Copyright 2023, 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. + */ +#if canImport(Darwin) +import Darwin +#else +import Glibc +#endif + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +struct RetryDelaySequence: Sequence { + @usableFromInline + typealias Element = Duration + + @usableFromInline + let policy: RetryPolicy + + @inlinable + init(policy: RetryPolicy) { + self.policy = policy + } + + @inlinable + func makeIterator() -> Iterator { + Iterator(policy: self.policy) + } + + @usableFromInline + struct Iterator: IteratorProtocol { + @usableFromInline + let policy: RetryPolicy + @usableFromInline + private(set) var n = 1 + + @inlinable + init(policy: RetryPolicy) { + self.policy = policy + } + + @inlinable + var _initialBackoffSeconds: Double { + Self._durationToTimeInterval(self.policy.initialBackoff) + } + + @inlinable + var _maximumBackoffSeconds: Double { + Self._durationToTimeInterval(self.policy.maximumBackoff) + } + + @inlinable + mutating func next() -> Duration? { + defer { self.n += 1 } + + /// The nth retry will happen after a randomly chosen delay between zero and + /// `min(initialBackoff * backoffMultiplier^(n-1), maximumBackoff)`. + let factor = pow(self.policy.backoffMultiplier, Double(self.n - 1)) + let computedBackoff = self._initialBackoffSeconds * factor + let clampedBackoff = Swift.min(computedBackoff, self._maximumBackoffSeconds) + let randomisedBackoff = Double.random(in: 0.0 ... clampedBackoff) + + return Self._timeIntervalToDuration(randomisedBackoff) + } + + @inlinable + static func _timeIntervalToDuration(_ seconds: Double) -> Duration { + let secondsComponent = Int64(seconds) + let attoseconds = (seconds - Double(secondsComponent)) * 1e18 + let attosecondsComponent = Int64(attoseconds) + return Duration( + secondsComponent: secondsComponent, + attosecondsComponent: attosecondsComponent + ) + } + + @inlinable + static func _durationToTimeInterval(_ duration: Duration) -> Double { + var seconds = Double(duration.components.seconds) + seconds += (Double(duration.components.attoseconds) / 1e18) + return seconds + } + } +} diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift new file mode 100644 index 000000000..ff54d9814 --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift @@ -0,0 +1,25 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension BroadcastAsyncSequence.Source: ClosableRPCWriterProtocol { + @inlinable + func write(contentsOf elements: some Sequence) async throws { + for element in elements { + try await self.write(element) + } + } +} diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift index 5e8e08365..738cfb86d 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -430,10 +430,7 @@ struct _BroadcastSequenceStateMachine: Sendable { @inlinable mutating func finish(result: Result) -> OnFinish { - guard let continuations = self.subscriptions.removeSubscribersWithContinuations() else { - return .none - } - + let continuations = self.subscriptions.removeSubscribersWithContinuations() return .resume( .init(continuations: continuations, result: result.map { nil }), .init(continuations: [], result: .success(())) @@ -455,8 +452,7 @@ struct _BroadcastSequenceStateMachine: Sendable { mutating func cancel( _ id: _BroadcastSequenceStateMachine.Subscriptions.ID ) -> OnCancelSubscription { - let (removed, continuation) = self.subscriptions.removeSubscriber(withID: id) - assert(removed) + let (_, continuation) = self.subscriptions.removeSubscriber(withID: id) if let continuation = continuation { return .resume(continuation, .failure(CancellationError())) } else { @@ -469,8 +465,11 @@ struct _BroadcastSequenceStateMachine: Sendable { _ continuation: ConsumerContinuation, forSubscription id: _BroadcastSequenceStateMachine.Subscriptions.ID ) -> OnSetContinuation { - let didSet = self.subscriptions.setContinuation(continuation, forSubscriber: id) - return didSet ? .none : .resume(continuation, .failure(CancellationError())) + if self.subscriptions.setContinuation(continuation, forSubscriber: id) { + return .none + } else { + return .resume(continuation, .failure(CancellationError())) + } } @inlinable @@ -480,23 +479,28 @@ struct _BroadcastSequenceStateMachine: Sendable { @inlinable mutating func invalidateAllSubscriptions() -> OnInvalidateAllSubscriptions { + // Remove subscriptions with continuations, they need to be failed. + let continuations = self.subscriptions.removeSubscribersWithContinuations() + let consumerContinuations = ConsumerContinuations( + continuations: continuations, + result: .failure(BroadcastAsyncSequenceError.consumingTooSlow) + ) + + // Remove any others to be failed when they next call 'next'. let ids = self.subscriptions.removeAllSubscribers() self.subscriptionsToDrop.append(contentsOf: ids) - return .none + return .resume(consumerContinuations) } @inlinable mutating func dropResources(error: BroadcastAsyncSequenceError) -> OnDropResources { - if let continuations = self.subscriptions.removeSubscribersWithContinuations() { - let consumerContinuations = ConsumerContinuations( - continuations: continuations, - result: .failure(error) - ) - let producerContinuations = ProducerContinuations(continuations: [], result: .success(())) - return .resume(consumerContinuations, producerContinuations) - } else { - return .none - } + let continuations = self.subscriptions.removeSubscribersWithContinuations() + let consumerContinuations = ConsumerContinuations( + continuations: continuations, + result: .failure(error) + ) + let producerContinuations = ProducerContinuations(continuations: [], result: .success(())) + return .resume(consumerContinuations, producerContinuations) } } @@ -682,16 +686,18 @@ struct _BroadcastSequenceStateMachine: Sendable { _ continuation: ConsumerContinuation, forSubscription id: _BroadcastSequenceStateMachine.Subscriptions.ID ) -> OnSetContinuation { - let didSet = self.subscriptions.setContinuation(continuation, forSubscriber: id) - return didSet ? .none : .resume(continuation, .failure(CancellationError())) + if self.subscriptions.setContinuation(continuation, forSubscriber: id) { + return .none + } else { + return .resume(continuation, .failure(CancellationError())) + } } @inlinable mutating func cancel( _ id: _BroadcastSequenceStateMachine.Subscriptions.ID ) -> OnCancelSubscription { - let (removed, continuation) = self.subscriptions.removeSubscriber(withID: id) - assert(removed) + let (_, continuation) = self.subscriptions.removeSubscriber(withID: id) if let continuation = continuation { return .resume(continuation, .failure(CancellationError())) } else { @@ -739,7 +745,7 @@ struct _BroadcastSequenceStateMachine: Sendable { let producers = self.producers.map { $0.0 } self.producers.removeAll() return .resume( - .init(continuations: continuations ?? .many([]), result: result.map { nil }), + .init(continuations: continuations, result: result.map { nil }), .init(continuations: producers, result: .success(())) ) } @@ -751,33 +757,26 @@ struct _BroadcastSequenceStateMachine: Sendable { @inlinable mutating func invalidateAllSubscriptions() -> OnInvalidateAllSubscriptions { - let onCancel: OnInvalidateAllSubscriptions - // Remove subscriptions with continuations, they need to be failed. - switch self.subscriptions.removeSubscribersWithContinuations() { - case .some(let oneOrMany): - let continuations = ConsumerContinuations( - continuations: oneOrMany, - result: .failure( - BroadcastAsyncSequenceError.consumingTooSlow - ) - ) - onCancel = .resume(continuations) - case .none: - onCancel = .none - } + let continuations = self.subscriptions.removeSubscribersWithContinuations() + let consumerContinuations = ConsumerContinuations( + continuations: continuations, + result: .failure(BroadcastAsyncSequenceError.consumingTooSlow) + ) // Remove any others to be failed when they next call 'next'. let ids = self.subscriptions.removeAllSubscribers() self.subscriptionsToDrop.append(contentsOf: ids) - return onCancel + return .resume(consumerContinuations) } @inlinable mutating func dropResources(error: BroadcastAsyncSequenceError) -> OnDropResources { - let consumers = self.subscriptions.removeSubscribersWithContinuations().map { - ConsumerContinuations(continuations: $0, result: .failure(error)) - } + let continuations = self.subscriptions.removeSubscribersWithContinuations() + let consumerContinuations = ConsumerContinuations( + continuations: continuations, + result: .failure(error) + ) let producers = ProducerContinuations( continuations: self.producers.map { $0.0 }, @@ -786,10 +785,7 @@ struct _BroadcastSequenceStateMachine: Sendable { self.producers.removeAll() - return .resume( - consumers ?? ConsumerContinuations(continuations: .many([]), result: .failure(error)), - producers - ) + return .resume(consumerContinuations, producers) } @inlinable @@ -894,10 +890,49 @@ struct _BroadcastSequenceStateMachine: Sendable { self.subscriptions.subscribe() } + @inlinable + mutating func invalidateAllSubscriptions() -> OnInvalidateAllSubscriptions { + // Remove subscriptions with continuations, they need to be failed. + let continuations = self.subscriptions.removeSubscribersWithContinuations() + let consumerContinuations = ConsumerContinuations( + continuations: continuations, + result: .failure(BroadcastAsyncSequenceError.consumingTooSlow) + ) + + // Remove any others to be failed when they next call 'next'. + let ids = self.subscriptions.removeAllSubscribers() + self.subscriptionsToDrop.append(contentsOf: ids) + return .resume(consumerContinuations) + } + + @inlinable + mutating func dropResources(error: BroadcastAsyncSequenceError) -> OnDropResources { + let continuations = self.subscriptions.removeSubscribersWithContinuations() + let consumerContinuations = ConsumerContinuations( + continuations: continuations, + result: .failure(error) + ) + + let producers = ProducerContinuations(continuations: [], result: .failure(error)) + return .resume(consumerContinuations, producers) + } + @inlinable func nextSubscriptionIsValid() -> Bool { self.elements.lowestID == .initial } + + @inlinable + mutating func cancel( + _ id: _BroadcastSequenceStateMachine.Subscriptions.ID + ) -> OnCancelSubscription { + let (_, continuation) = self.subscriptions.removeSubscriber(withID: id) + if let continuation = continuation { + return .resume(continuation, .failure(CancellationError())) + } else { + return .none + } + } } } @@ -944,15 +979,19 @@ struct _BroadcastSequenceStateMachine: Sendable { onCancel = .none case .subscribed(var state): + self._state = ._modifying onCancel = state.invalidateAllSubscriptions() self._state = .subscribed(state) case .streaming(var state): + self._state = ._modifying onCancel = state.invalidateAllSubscriptions() self._state = .streaming(state) - case .finished: - onCancel = .none + case .finished(var state): + self._state = ._modifying + onCancel = state.invalidateAllSubscriptions() + self._state = .finished(state) case ._modifying: fatalError("Internal inconsistency") @@ -1124,7 +1163,10 @@ struct _BroadcastSequenceStateMachine: Sendable { onSetContinuation = state.setContinuation(continuation, forSubscription: id) self._state = .streaming(state) - case .finished, ._modifying: + case .finished(let state): + onSetContinuation = .resume(continuation, state.result.map { _ in nil }) + + case ._modifying: // All values must have been produced, nothing to wait for. fatalError("Internal inconsistency") } @@ -1159,7 +1201,12 @@ struct _BroadcastSequenceStateMachine: Sendable { onCancel = state.cancel(id) self._state = .streaming(state) - case .finished, ._modifying: + case .finished(var state): + self._state = ._modifying + onCancel = state.cancel(id) + self._state = .finished(state) + + case ._modifying: // All values must have been produced, nothing to wait for. fatalError("Internal inconsistency") } @@ -1293,8 +1340,10 @@ struct _BroadcastSequenceStateMachine: Sendable { onDrop = state.dropResources(error: error) self._state = .finished(State.Finished(from: state, result: .failure(error))) - case .finished: - onDrop = .none + case .finished(var state): + self._state = ._modifying + onDrop = state.dropResources(error: error) + self._state = .finished(state) case ._modifying: fatalError("Internal inconsistency") @@ -1461,7 +1510,7 @@ extension _BroadcastSequenceStateMachine { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension _BroadcastSequenceStateMachine { - /// A collection of subcriptions. + /// A collection of subscriptions. @usableFromInline struct Subscriptions: Sendable { @usableFromInline @@ -1675,14 +1724,14 @@ extension _BroadcastSequenceStateMachine { /// Removes all subscribers which have continuations and return their continuations. @inlinable - mutating func removeSubscribersWithContinuations() -> _OneOrMany? { + mutating func removeSubscribersWithContinuations() -> _OneOrMany { // Avoid allocs if there's only one subscriber. let count = self._countPendingContinuations() - let result: _OneOrMany? + let result: _OneOrMany switch count { case 0: - result = nil + result = .many([]) case 1: let index = self._subscribers.firstIndex(where: { $0.continuation != nil })! diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift new file mode 100644 index 000000000..4fac76702 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -0,0 +1,303 @@ +/* + * Copyright 2023, 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 + +extension ClientRPCExecutorTests { + fileprivate func makeHarnessForRetries( + rejectUntilAttempt firstSuccessfulAttempt: Int, + withCode code: RPCError.Code, + consumeInboundStream: Bool = false + ) -> ClientRPCExecutorTestHarness { + return ClientRPCExecutorTestHarness( + server: .attemptBased { attempt in + guard attempt < firstSuccessfulAttempt else { + return .echo + } + + return .reject( + withError: RPCError(code: code, message: ""), + consumeInbound: consumeInboundStream + ) + } + ) + } + + func testRetriesEventuallySucceed() async throws { + let harness = self.makeHarnessForRetries(rejectUntilAttempt: 3, withCode: .unavailable) + try await harness.bidirectional( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + configuration: .retry(codes: [.unavailable]) + ) { response in + XCTAssertEqual( + response.metadata, + [ + "foo": "bar", + "grpc-previous-rpc-attempts": "2", + ] + ) + let messages = try await response.messages.collect() + XCTAssertEqual(messages, [[0], [1], [2]]) + } + + // Success on the third attempt. + XCTAssertEqual(harness.clientStreamsOpened, 3) + XCTAssertEqual(harness.serverStreamsAccepted, 3) + } + + func testRetriesRespectRetryableCodes() async throws { + let harness = self.makeHarnessForRetries(rejectUntilAttempt: 3, withCode: .unavailable) + try await harness.bidirectional( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([0, 1, 2]) + }, + configuration: .retry(codes: [.aborted]) + ) { response in + switch response.accepted { + case .success: + XCTFail("Expected response to be rejected") + case .failure(let error): + XCTAssertEqual(error.code, .unavailable) + } + } + + // Error code wasn't retryable, only one stream. + XCTAssertEqual(harness.clientStreamsOpened, 1) + XCTAssertEqual(harness.serverStreamsAccepted, 1) + } + + func testRetriesRespectRetryLimit() async throws { + let harness = self.makeHarnessForRetries(rejectUntilAttempt: 5, withCode: .unavailable) + try await harness.bidirectional( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([0, 1, 2]) + }, + configuration: .retry(maximumAttempts: 2, codes: [.unavailable]) + ) { response in + switch response.accepted { + case .success: + XCTFail("Expected response to be rejected") + case .failure(let error): + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(Array(error.metadata[stringValues: "grpc-previous-rpc-attempts"]), ["1"]) + } + } + + // Only two attempts permitted. + XCTAssertEqual(harness.clientStreamsOpened, 2) + XCTAssertEqual(harness.serverStreamsAccepted, 2) + } + + func testRetriesCantBeExecutedForTooManyRequestMessages() async throws { + let harness = self.makeHarnessForRetries( + rejectUntilAttempt: 3, + withCode: .unavailable, + consumeInboundStream: true + ) + + try await harness.bidirectional( + request: ClientRequest.Stream { + for _ in 0 ..< 1000 { + try await $0.write([]) + } + }, + configuration: .retry(codes: [.unavailable]) + ) { response in + switch response.accepted { + case .success: + XCTFail("Expected response to be rejected") + case .failure(let error): + XCTAssertEqual(error.code, .unavailable) + XCTAssertFalse(error.metadata.contains { $0.key == "grpc-previous-rpc-attempts" }) + } + } + + // The request stream can't be buffered as it's a) large, and b) the server consumes it before + // responding. Even though the server responded with a retryable status code, the request buffer + // was dropped so only one attempt was made. + XCTAssertEqual(harness.clientStreamsOpened, 1) + XCTAssertEqual(harness.serverStreamsAccepted, 1) + } + + func testRetriesWithImmediateTimeout() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .sleepFor(duration: .milliseconds(250), then: .echo) + ) + + await XCTAssertThrowsErrorAsync { + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + configuration: .retry(codes: [.unavailable], timeout: .zero) + ) { response in + XCTFail("Response not expected to be handled") + } + } errorHandler: { error in + XCTAssert(error is CancellationError) + } + } + + func testRetriesWithTimeoutDuringFirstAttempt() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .sleepFor(duration: .milliseconds(250), then: .echo) + ) + + await XCTAssertThrowsErrorAsync { + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + configuration: .retry(codes: [.unavailable], timeout: .milliseconds(50)) + ) { response in + XCTFail("Response not expected to be handled") + } + } errorHandler: { error in + XCTAssert(error is CancellationError) + } + } + + func testRetriesWithTimeoutDuringSecondAttempt() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .sleepFor( + duration: .milliseconds(100), + then: .reject(withError: RPCError(code: .unavailable, message: "")) + ) + ) + + await XCTAssertThrowsErrorAsync { + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + configuration: .retry(codes: [.unavailable], timeout: .milliseconds(150)) + ) { response in + XCTFail("Response not expected to be handled") + } + } errorHandler: { error in + XCTAssert(error is CancellationError) + } + } + + func testRetriesWithServerPushback() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .attemptBased { attempt in + if attempt == 2 { + return .echo + } else { + return .init { stream in + // Use a short pushback to override the long configured retry delay. + let status = Status(code: .unavailable, message: "") + let metadata: Metadata = ["grpc-retry-pushback-ms": "10"] + try await stream.outbound.write(.status(status, metadata)) + } + } + } + ) + + let retryPolicy = RetryPolicy( + maximumAttempts: 5, + initialBackoff: .seconds(60), + maximumBackoff: .seconds(50), + backoffMultiplier: 1, + retryableStatusCodes: [.unavailable] + ) + + let start = ContinuousClock.now + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + }, + configuration: .init(retryPolicy: retryPolicy) + ) { response in + let end = ContinuousClock.now + let duration = end - start + // Loosely check whether the RPC completed in less than 60 seconds (i.e. the configured retry + // delay). Allow lots of headroom to avoid false negatives; CI systems can be slow. + XCTAssertLessThanOrEqual(duration, .seconds(5)) + XCTAssertEqual(Array(response.metadata[stringValues: "grpc-previous-rpc-attempts"]), ["1"]) + } + } + + func testRetriesWithNegativeServerPushback() async throws { + // Negative and values which can't be parsed should halt retries. + for pushback in ["-1", "not-an-int"] { + let harness = ClientRPCExecutorTestHarness( + server: .reject( + withError: RPCError( + code: .unavailable, + message: "", + metadata: ["grpc-retry-pushback-ms": "\(pushback)"] + ) + ) + ) + + let retryPolicy = RetryPolicy( + maximumAttempts: 5, + initialBackoff: .seconds(60), + maximumBackoff: .seconds(50), + backoffMultiplier: 1, + retryableStatusCodes: [.unavailable] + ) + + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + }, + configuration: .init(retryPolicy: retryPolicy) + ) { response in + switch response.accepted { + case .success: + XCTFail("Expected RPC to fail") + case .failure(let error): + XCTAssertEqual(error.code, .unavailable) + } + } + + // Only one attempt should be made. + XCTAssertEqual(harness.clientStreamsOpened, 1) + XCTAssertEqual(harness.serverStreamsAccepted, 1) + } + } +} + +extension ClientRPCExecutionConfiguration { + fileprivate static func retry( + maximumAttempts: Int = 5, + codes: Set, + timeout: Duration? = nil + ) -> Self { + let policy = RetryPolicy( + maximumAttempts: maximumAttempts, + initialBackoff: .milliseconds(10), + maximumBackoff: .milliseconds(100), + backoffMultiplier: 1.6, + retryableStatusCodes: codes + ) + + return ClientRPCExecutionConfiguration(retryPolicy: policy, timeout: timeout) + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift new file mode 100644 index 000000000..1f421a9eb --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -0,0 +1,92 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +final class RetryDelaySequenceTests: XCTestCase { + func testSequence() { + let policy = RetryPolicy( + maximumAttempts: 1, // ignored here + initialBackoff: .seconds(1), + maximumBackoff: .seconds(8), + backoffMultiplier: 2.0, + retryableStatusCodes: [.aborted] // ignored here + ) + + let sequence = RetryDelaySequence(policy: policy) + var iterator = sequence.makeIterator() + + // The iterator will never return 'nil', '!' is safe. + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(1)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(2)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(4)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(8)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(8)) // Clamped + } + + func testSequenceSupportsMultipleIteration() { + let policy = RetryPolicy( + maximumAttempts: 1, // ignored here + initialBackoff: .seconds(1), + maximumBackoff: .seconds(8), + backoffMultiplier: 2.0, + retryableStatusCodes: [.aborted] // ignored here + ) + + let sequence = RetryDelaySequence(policy: policy) + for _ in 0 ..< 10 { + var iterator = sequence.makeIterator() + // The iterator will never return 'nil', '!' is safe. + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(1)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(2)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(4)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(8)) + XCTAssertLessThanOrEqual(iterator.next()!, .seconds(8)) // Clamped + } + } + + func testDurationToDouble() { + let testData: [(Duration, Double)] = [ + (.zero, 0.0), + (.seconds(1), 1.0), + (.milliseconds(1500), 1.5), + (.nanoseconds(1_000_000_000), 1.0), + (.nanoseconds(3_141_592_653), 3.141592653), + ] + + for (duration, expected) in testData { + XCTAssertEqual(RetryDelaySequence.Iterator._durationToTimeInterval(duration), expected) + } + } + + func testDoubleToDuration() { + let testData: [(Double, Duration)] = [ + (0.0, .zero), + (1.0, .seconds(1)), + (1.5, .milliseconds(1500)), + (1.0, .nanoseconds(1_000_000_000)), + (3.141592653, .nanoseconds(3_141_592_653)), + ] + + for (seconds, expected) in testData { + let actual = RetryDelaySequence.Iterator._timeIntervalToDuration(seconds) + XCTAssertEqual(actual.components.seconds, expected.components.seconds) + // We lose some precision in the conversion, that's fine. + XCTAssertEqual(actual.components.attoseconds / 1_000, expected.components.attoseconds / 1_000) + } + } +} From abf2e5e76b9b3ea3285dc26d36fd42e6d646f418 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 9 Nov 2023 10:21:06 +0000 Subject: [PATCH 163/580] availability guards (#1711) --- .../Call/Client/Internal/ClientRPCExecutorTests+Retries.swift | 2 ++ Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index 4fac76702..a313148ea 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClientRPCExecutorTests { fileprivate func makeHarnessForRetries( rejectUntilAttempt firstSuccessfulAttempt: Int, @@ -284,6 +285,7 @@ extension ClientRPCExecutorTests { } } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClientRPCExecutionConfiguration { fileprivate static func retry( maximumAttempts: Int = 5, diff --git a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift index 1f421a9eb..09bc182cd 100644 --- a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -17,6 +17,7 @@ import XCTest @testable import GRPCCore +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class RetryDelaySequenceTests: XCTestCase { func testSequence() { let policy = RetryPolicy( From cddc12dc4e3e5ceb4e9b69564684984fd07253f6 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:01:43 +0000 Subject: [PATCH 164/580] Fixed the list services response of reflection service (#1712) Motivation: The response should contain the fully qualified names of the services, not just the services' names Modifications: - added the package name for each service i the serviceNames array of the proto registry - modified unit test and integration test accordingly Result: When requesting listServices, we will receive the fully qualified names of the services. --- .../GRPCReflectionService/Server/ReflectionService.swift | 5 +++-- .../ReflectionServiceIntegrationTests.swift | 3 ++- .../ReflectionServiceUnitTests.swift | 2 +- Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift | 6 +++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 9214b69a3..048748269 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -98,8 +98,9 @@ internal struct ReflectionServiceData: Sendable { dependencyFileNames: fileDescriptorProto.dependency ) self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData - self.serviceNames.append(contentsOf: fileDescriptorProto.service.map { $0.name }) - + self.serviceNames.append( + contentsOf: fileDescriptorProto.service.map { fileDescriptorProto.package + "." + $0.name } + ) // Populating the dictionary. for qualifiedSybolName in fileDescriptorProto.qualifiedSymbolNames { let oldValue = self.fileNameBySymbol.updateValue( diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index 29b3c6aa4..1dad7194c 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -121,7 +121,8 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { } let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted() - let servicesNames = (self.protos + [self.independentProto]).serviceNames.sorted() + let servicesNames = (self.protos + [self.independentProto]).flatMap { $0.qualifiedServiceNames } + .sorted() XCTAssertEqual(receivedServices, servicesNames) } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index a21dfe2e8..f9071208a 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -56,7 +56,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { /// Testing the serviceNames array of the ReflectionServiceData object. func testServiceNames() throws { let protos = makeProtosWithDependencies() - let servicesNames = protos.serviceNames.sorted() + let servicesNames = protos.flatMap { $0.qualifiedServiceNames }.sorted() let registry = try ReflectionServiceData(fileDescriptors: protos) let registryServices = registry.serviceNames.sorted() XCTAssertEqual(registryServices, servicesNames) diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift index fcd141b1a..7d5af0850 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift @@ -150,9 +150,9 @@ func XCTAssertThrowsGRPCStatus( } } -extension Sequence where Element == Google_Protobuf_FileDescriptorProto { - var serviceNames: [String] { - self.flatMap { $0.service.map { $0.name } } +extension Google_Protobuf_FileDescriptorProto { + var qualifiedServiceNames: [String] { + self.service.map { self.package + "." + $0.name } } } From 187b6090a69fbcf54cc372c0b587597c60da0c89 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 9 Nov 2023 16:14:21 +0000 Subject: [PATCH 165/580] Adding support for v1alpha reflection (#1703) Motivation: v1alpha reflection is also used by GRPC users, so we should provide support for it. Modifications: - Generated the .grpc and .pb files for the v1alpha reflection. - Added an extension for the ReflectionService that represents the version, that is set by the users. - Created an enum to represent the 2 possible Reflection Service Providers and changed the methods that were calling provider's methods to switch between the possible cases. Result: Users will be able to set the version for the Reflection Service that they use, v1alpha becoming a possibility. --- Makefile | 56 +- Package.swift | 3 +- .../Server/ReflectionService.swift | 248 ++---- .../Server/ReflectionServiceV1.swift | 208 +++++ .../Server/ReflectionServiceV1Alpha.swift | 214 +++++ .../reflection-v1.grpc.swift} | 2 +- .../reflection-v1.pb.swift} | 2 +- .../reflection-v1.proto} | 0 .../v1Alpha/reflection-v1alpha.grpc.swift | 122 +++ .../v1Alpha/reflection-v1alpha.pb.swift | 838 ++++++++++++++++++ .../v1Alpha/reflection-v1alpha.proto | 136 +++ .../reflection-v1.grpc.swift} | 2 +- .../reflection-v1.pb.swift} | 2 +- .../v1Alpha/reflection-v1alpha.grpc.swift | 208 +++++ .../v1Alpha/reflection-v1alpha.pb.swift | 838 ++++++++++++++++++ .../ReflectionServiceIntegrationTests.swift | 400 ++++----- .../GRPCReflectionServiceTests/Utils.swift | 184 ++++ 17 files changed, 3049 insertions(+), 414 deletions(-) create mode 100644 Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift create mode 100644 Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift rename Sources/GRPCReflectionService/{Model/reflection.grpc.swift => v1/reflection-v1.grpc.swift} (99%) rename Sources/GRPCReflectionService/{Model/reflection.pb.swift => v1/reflection-v1.pb.swift} (99%) rename Sources/GRPCReflectionService/{Model/reflection.proto => v1/reflection-v1.proto} (100%) create mode 100644 Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift create mode 100644 Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift create mode 100644 Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto rename Tests/GRPCTests/GRPCReflectionServiceTests/Generated/{reflection.grpc.swift => v1/reflection-v1.grpc.swift} (99%) rename Tests/GRPCTests/GRPCReflectionServiceTests/Generated/{reflection.pb.swift => v1/reflection-v1.pb.swift} (99%) create mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift create mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift diff --git a/Makefile b/Makefile index 23d253ca8..210795dea 100644 --- a/Makefile +++ b/Makefile @@ -131,42 +131,70 @@ ${SERIALIZATION_GRPC_REFLECTION}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} .PHONY: generate-reflection-data: ${SERIALIZATION_GRPC_REFLECTION} -REFLECTION_PROTO=Sources/GRPCReflectionService/Model/reflection.proto -REFLECTION_PB=$(REFLECTION_PROTO:.proto=.pb.swift) -REFLECTION_GRPC=$(REFLECTION_PROTO:.proto=.grpc.swift) +REFLECTION_V1_PROTO=Sources/GRPCReflectionService/v1/reflection-v1.proto +REFLECTION_V1_PB=$(REFLECTION_V1_PROTO:.proto=.pb.swift) +REFLECTION_V1_GRPC=$(REFLECTION_V1_PROTO:.proto=.grpc.swift) + +REFLECTION_V1ALPHA_PROTO=Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto +REFLECTION_V1ALPHA_PB=$(REFLECTION_V1ALPHA_PROTO:.proto=.pb.swift) +REFLECTION_V1ALPHA_GRPC=$(REFLECTION_V1ALPHA_PROTO:.proto=.grpc.swift) # For Reflection we'll generate only the Server code. -${REFLECTION_GRPC}: ${REFLECTION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} +${REFLECTION_V1_GRPC}: ${REFLECTION_V1_PROTO} ${PROTOC_GEN_GRPC_SWIFT} protoc $< \ --proto_path=$(dir $<) \ --plugin=${PROTOC_GEN_GRPC_SWIFT} \ --grpc-swift_opt=Client=false \ --grpc-swift_out=$(dir $<) +# For Reflection we'll generate only the Server code. +${REFLECTION_V1ALPHA_GRPC}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=false \ + --grpc-swift_out=$(dir $<) + # Generates protobufs and gRPC server for the Reflection Service .PHONY: -generate-reflection: ${REFLECTION_PB} ${REFLECTION_GRPC} +generate-reflection: ${REFLECTION_V1_PB} ${REFLECTION_V1_GRPC} ${REFLECTION_V1ALPHA_PB} ${REFLECTION_V1ALPHA_GRPC} -TEST_REFLECTION_GRPC=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift -TEST_REFLECTION_PB=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift +TEST_REFLECTION_V1_GRPC=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift +TEST_REFLECTION_V1_PB=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift +TEST_REFLECTION_V1ALPHA_GRPC=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift +TEST_REFLECTION_V1ALPHA_PB=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift -# For Reflection we'll generate only the Server code. -${TEST_REFLECTION_GRPC}: ${REFLECTION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} +# For Testing the Reflection we'll generate only the Client code. +${TEST_REFLECTION_V1_GRPC}: ${REFLECTION_V1_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=true,Server=false \ + --grpc-swift_out=$(dir ${TEST_REFLECTION_V1_GRPC}) + +${TEST_REFLECTION_V1_PB}: ${REFLECTION_V1_PROTO} ${PROTOC_GEN_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_SWIFT} \ + --swift_out=$(dir ${TEST_REFLECTION_V1_PB}) + +# For Testing the Reflection we'll generate only the Client code. +${TEST_REFLECTION_V1ALPHA_GRPC}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_GRPC_SWIFT} protoc $< \ --proto_path=$(dir $<) \ --plugin=${PROTOC_GEN_GRPC_SWIFT} \ --grpc-swift_opt=Client=true,Server=false \ - --grpc-swift_out=$(dir ${TEST_REFLECTION_GRPC}) + --grpc-swift_out=$(dir ${TEST_REFLECTION_V1ALPHA_GRPC}) -${TEST_REFLECTION_PB}: ${REFLECTION_PROTO} ${PROTOC_GEN_SWIFT} +${TEST_REFLECTION_V1ALPHA_PB}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_SWIFT} protoc $< \ --proto_path=$(dir $<) \ --plugin=${PROTOC_GEN_SWIFT} \ - --swift_out=$(dir ${TEST_REFLECTION_PB}) + --swift_out=$(dir ${TEST_REFLECTION_V1ALPHA_PB}) -# Generates protobufs and gRPC client for the Reflection Service Tests +# Generates protobufs and gRPC clients for the Reflection Service Tests .PHONY: -generate-reflection-client: ${TEST_REFLECTION_PB} ${TEST_REFLECTION_GRPC} +generate-reflection-test-clients: ${TEST_REFLECTION_V1_PB} ${TEST_REFLECTION_V1_GRPC} ${TEST_REFLECTION_V1ALPHA_PB} ${TEST_REFLECTION_V1ALPHA_GRPC} ### Testing #################################################################### diff --git a/Package.swift b/Package.swift index 09cee9608..b8732828a 100644 --- a/Package.swift +++ b/Package.swift @@ -455,7 +455,8 @@ extension Target { ], path: "Sources/GRPCReflectionService", exclude: [ - "Model/reflection.proto", + "v1/reflection-v1.proto", + "v1Alpha/reflection-v1alpha.proto" ] ) } diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 048748269..39d6ffdb3 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -21,9 +21,15 @@ import SwiftProtobuf @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public final class ReflectionService: CallHandlerProvider, Sendable { - private let reflectionService: ReflectionServiceProvider + private let provider: Provider + public var serviceName: Substring { - self.reflectionService.serviceName + switch self.provider { + case .v1(let provider): + return provider.serviceName + case .v1Alpha(let provider): + return provider.serviceName + } } /// Creates a `ReflectionService` by loading serialized reflection data created by `protoc-gen-grpc-swift`. @@ -33,26 +39,49 @@ public final class ReflectionService: CallHandlerProvider, Sendable { /// current working directory. /// /// - Parameter filePaths: The paths to files containing serialized reflection data. + /// - Parameter version: The version of the reflection service to create. /// /// - Throws: When a file can't be read from disk or parsed. - public init(serializedFileDescriptorProtoFilePaths filePaths: [String]) throws { + public init(serializedFileDescriptorProtoFilePaths filePaths: [String], version: Version) throws { let fileDescriptorProtos = try ReflectionService.readSerializedFileDescriptorProtos( atPaths: filePaths ) - self.reflectionService = try ReflectionServiceProvider( - fileDescriptorProtos: fileDescriptorProtos - ) + switch version.wrapped { + case .v1: + self.provider = .v1( + try ReflectionServiceProviderV1(fileDescriptorProtos: fileDescriptorProtos) + ) + case .v1Alpha: + self.provider = .v1Alpha( + try ReflectionServiceProviderV1Alpha(fileDescriptorProtos: fileDescriptorProtos) + ) + } } - public init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { - self.reflectionService = try ReflectionServiceProvider(fileDescriptorProtos: fileDescriptors) + public init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto], version: Version) throws + { + switch version.wrapped { + case .v1: + self.provider = .v1( + try ReflectionServiceProviderV1(fileDescriptorProtos: fileDescriptorProtos) + ) + case .v1Alpha: + self.provider = .v1Alpha( + try ReflectionServiceProviderV1Alpha(fileDescriptorProtos: fileDescriptorProtos) + ) + } } public func handle( method name: Substring, context: GRPC.CallHandlerContext ) -> GRPC.GRPCServerHandlerProtocol? { - self.reflectionService.handle(method: name, context: context) + switch self.provider { + case .v1(let reflectionV1Provider): + return reflectionV1Provider.handle(method: name, context: context) + case .v1Alpha(let reflectionV1AlphaProvider): + return reflectionV1AlphaProvider.handle(method: name, context: context) + } } } @@ -222,163 +251,6 @@ internal struct ReflectionServiceData: Sendable { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final class ReflectionServiceProvider: Grpc_Reflection_V1_ServerReflectionAsyncProvider { - private let protoRegistry: ReflectionServiceData - - internal init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto]) throws { - self.protoRegistry = try ReflectionServiceData( - fileDescriptors: fileDescriptorProtos - ) - } - - internal func _findFileByFileName( - _ fileName: String - ) -> Result { - return self.protoRegistry - .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) - .map { fileDescriptorProtos in - Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.fileDescriptorResponse( - .with { - $0.fileDescriptorProto = fileDescriptorProtos - } - ) - } - } - - internal func findFileByFileName( - _ fileName: String, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self._findFileByFileName(fileName) - return result.makeResponse(request: request) - } - - internal func getServicesNames( - request: Grpc_Reflection_V1_ServerReflectionRequest - ) throws -> Grpc_Reflection_V1_ServerReflectionResponse { - var listServicesResponse = Grpc_Reflection_V1_ListServiceResponse() - listServicesResponse.service = self.protoRegistry.serviceNames.map { serviceName in - Grpc_Reflection_V1_ServiceResponse.with { - $0.name = serviceName - } - } - return Grpc_Reflection_V1_ServerReflectionResponse( - request: request, - messageResponse: .listServicesResponse(listServicesResponse) - ) - } - - internal func findFileBySymbol( - _ symbolName: String, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.protoRegistry.nameOfFileContainingSymbol( - named: symbolName - ).flatMap { - self._findFileByFileName($0) - } - return result.makeResponse(request: request) - } - - internal func findFileByExtension( - extensionRequest: Grpc_Reflection_V1_ExtensionRequest, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.protoRegistry.nameOfFileContainingExtension( - extendeeName: extensionRequest.containingType, - fieldNumber: extensionRequest.extensionNumber - ).flatMap { - self._findFileByFileName($0) - } - return result.makeResponse(request: request) - } - - internal func findExtensionsFieldNumbersOfType( - named typeName: String, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.protoRegistry.extensionsFieldNumbersOfType( - named: typeName - ).map { fieldNumbers in - Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.allExtensionNumbersResponse( - Grpc_Reflection_V1_ExtensionNumberResponse.with { - $0.baseTypeName = typeName - $0.extensionNumber = fieldNumbers - } - ) - } - return result.makeResponse(request: request) - } - - internal func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await request in requestStream { - switch request.messageRequest { - case let .fileByFilename(fileName): - let response = self.findFileByFileName( - fileName, - request: request - ) - try await responseStream.send(response) - - case .listServices: - let response = try self.getServicesNames(request: request) - try await responseStream.send(response) - - case let .fileContainingSymbol(symbolName): - let response = self.findFileBySymbol( - symbolName, - request: request - ) - try await responseStream.send(response) - - case let .fileContainingExtension(extensionRequest): - let response = self.findFileByExtension( - extensionRequest: extensionRequest, - request: request - ) - try await responseStream.send(response) - - case let .allExtensionNumbersOfType(typeName): - let response = self.findExtensionsFieldNumbersOfType( - named: typeName, - request: request - ) - try await responseStream.send(response) - - default: - let response = Grpc_Reflection_V1_ServerReflectionResponse( - request: request, - messageResponse: .errorResponse( - Grpc_Reflection_V1_ErrorResponse.with { - $0.errorCode = Int32(GRPCStatus.Code.unimplemented.rawValue) - $0.errorMessage = "The request is not implemented." - } - ) - ) - try await responseStream.send(response) - } - } - } -} - -extension Grpc_Reflection_V1_ServerReflectionResponse { - init( - request: Grpc_Reflection_V1_ServerReflectionRequest, - messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse - ) { - self = .with { - $0.validHost = request.host - $0.originalRequest = request - $0.messageResponse = messageResponse - } - } -} - extension Google_Protobuf_FileDescriptorProto { var qualifiedServiceAndMethodNames: [String] { var names: [String] = [] @@ -413,35 +285,29 @@ extension Google_Protobuf_FileDescriptorProto { } } -extension Result { - func recover() -> Result - { - self.flatMapError { status in - let error = Grpc_Reflection_V1_ErrorResponse.with { - $0.errorCode = Int32(status.code.rawValue) - $0.errorMessage = status.message ?? "" - } - return .success(.errorResponse(error)) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ReflectionService { + /// The version of the reflection service. + /// + /// Depending in the version you are using, when creating the ReflectionService + /// provide the corresponding `Version` variable (`v1` or `v1Alpha`). + public struct Version: Sendable, Hashable { + internal enum Wrapped { + case v1 + case v1Alpha } - } + var wrapped: Wrapped + private init(_ wrapped: Wrapped) { self.wrapped = wrapped } - func makeResponse( - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.recover().attachRequest(request) - // Safe to '!' as the failure type is 'Never'. - return try! result.get() + /// The v1 version of reflection service: https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1/reflection.proto. + public static var v1: Self { Self(.v1) } + /// The v1alpha version of reflection service: https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto. + public static var v1Alpha: Self { Self(.v1Alpha) } } -} -extension Result -where Success == Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse { - func attachRequest( - _ request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Result { - self.map { message in - Grpc_Reflection_V1_ServerReflectionResponse(request: request, messageResponse: message) - } + private enum Provider { + case v1(ReflectionServiceProviderV1) + case v1Alpha(ReflectionServiceProviderV1Alpha) } } diff --git a/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift b/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift new file mode 100644 index 000000000..c8109ee88 --- /dev/null +++ b/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift @@ -0,0 +1,208 @@ +/* + * Copyright 2023, 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 Foundation +import GRPC +import SwiftProtobuf + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal final class ReflectionServiceProviderV1: Grpc_Reflection_V1_ServerReflectionAsyncProvider { + private let protoRegistry: ReflectionServiceData + + internal init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto]) throws { + self.protoRegistry = try ReflectionServiceData( + fileDescriptors: fileDescriptorProtos + ) + } + + internal func _findFileByFileName( + _ fileName: String + ) -> Result { + return self.protoRegistry + .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) + .map { fileDescriptorProtos in + Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.fileDescriptorResponse( + .with { + $0.fileDescriptorProto = fileDescriptorProtos + } + ) + } + } + + internal func findFileByFileName( + _ fileName: String, + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { + let result = self._findFileByFileName(fileName) + return result.makeResponse(request: request) + } + + internal func getServicesNames( + request: Grpc_Reflection_V1_ServerReflectionRequest + ) throws -> Grpc_Reflection_V1_ServerReflectionResponse { + var listServicesResponse = Grpc_Reflection_V1_ListServiceResponse() + listServicesResponse.service = self.protoRegistry.serviceNames.map { serviceName in + Grpc_Reflection_V1_ServiceResponse.with { + $0.name = serviceName + } + } + return Grpc_Reflection_V1_ServerReflectionResponse( + request: request, + messageResponse: .listServicesResponse(listServicesResponse) + ) + } + + internal func findFileBySymbol( + _ symbolName: String, + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { + let result = self.protoRegistry.nameOfFileContainingSymbol( + named: symbolName + ).flatMap { + self._findFileByFileName($0) + } + return result.makeResponse(request: request) + } + + internal func findFileByExtension( + extensionRequest: Grpc_Reflection_V1_ExtensionRequest, + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { + let result = self.protoRegistry.nameOfFileContainingExtension( + extendeeName: extensionRequest.containingType, + fieldNumber: extensionRequest.extensionNumber + ).flatMap { + self._findFileByFileName($0) + } + return result.makeResponse(request: request) + } + + internal func findExtensionsFieldNumbersOfType( + named typeName: String, + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { + let result = self.protoRegistry.extensionsFieldNumbersOfType( + named: typeName + ).map { fieldNumbers in + Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.allExtensionNumbersResponse( + Grpc_Reflection_V1_ExtensionNumberResponse.with { + $0.baseTypeName = typeName + $0.extensionNumber = fieldNumbers + } + ) + } + return result.makeResponse(request: request) + } + + internal func serverReflectionInfo( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + for try await request in requestStream { + switch request.messageRequest { + case let .fileByFilename(fileName): + let response = self.findFileByFileName( + fileName, + request: request + ) + try await responseStream.send(response) + + case .listServices: + let response = try self.getServicesNames(request: request) + try await responseStream.send(response) + + case let .fileContainingSymbol(symbolName): + let response = self.findFileBySymbol( + symbolName, + request: request + ) + try await responseStream.send(response) + + case let .fileContainingExtension(extensionRequest): + let response = self.findFileByExtension( + extensionRequest: extensionRequest, + request: request + ) + try await responseStream.send(response) + + case let .allExtensionNumbersOfType(typeName): + let response = self.findExtensionsFieldNumbersOfType( + named: typeName, + request: request + ) + try await responseStream.send(response) + + default: + let response = Grpc_Reflection_V1_ServerReflectionResponse( + request: request, + messageResponse: .errorResponse( + Grpc_Reflection_V1_ErrorResponse.with { + $0.errorCode = Int32(GRPCStatus.Code.unimplemented.rawValue) + $0.errorMessage = "The request is not implemented." + } + ) + ) + try await responseStream.send(response) + } + } + } +} + +extension Grpc_Reflection_V1_ServerReflectionResponse { + init( + request: Grpc_Reflection_V1_ServerReflectionRequest, + messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse + ) { + self = .with { + $0.validHost = request.host + $0.originalRequest = request + $0.messageResponse = messageResponse + } + } +} + +extension Result { + func recover() -> Result + { + self.flatMapError { status in + let error = Grpc_Reflection_V1_ErrorResponse.with { + $0.errorCode = Int32(status.code.rawValue) + $0.errorMessage = status.message ?? "" + } + return .success(.errorResponse(error)) + } + } + + func makeResponse( + request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Grpc_Reflection_V1_ServerReflectionResponse { + let result = self.recover().attachRequest(request) + // Safe to '!' as the failure type is 'Never'. + return try! result.get() + } +} + +extension Result +where Success == Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse { + func attachRequest( + _ request: Grpc_Reflection_V1_ServerReflectionRequest + ) -> Result { + self.map { message in + Grpc_Reflection_V1_ServerReflectionResponse(request: request, messageResponse: message) + } + } +} diff --git a/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift b/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift new file mode 100644 index 000000000..6686c78cd --- /dev/null +++ b/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift @@ -0,0 +1,214 @@ +/* + * Copyright 2023, 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 Foundation +import GRPC +import SwiftProtobuf + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal final class ReflectionServiceProviderV1Alpha: + Grpc_Reflection_V1alpha_ServerReflectionAsyncProvider +{ + private let protoRegistry: ReflectionServiceData + + internal init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto]) throws { + self.protoRegistry = try ReflectionServiceData( + fileDescriptors: fileDescriptorProtos + ) + } + + internal func _findFileByFileName( + _ fileName: String + ) -> Result { + return self.protoRegistry + .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) + .map { fileDescriptorProtos in + Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse + .fileDescriptorResponse( + .with { + $0.fileDescriptorProto = fileDescriptorProtos + } + ) + } + } + + internal func findFileByFileName( + _ fileName: String, + request: Grpc_Reflection_V1alpha_ServerReflectionRequest + ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { + let result = self._findFileByFileName(fileName) + return result.makeResponse(request: request) + } + + internal func getServicesNames( + request: Grpc_Reflection_V1alpha_ServerReflectionRequest + ) throws -> Grpc_Reflection_V1alpha_ServerReflectionResponse { + var listServicesResponse = Grpc_Reflection_V1alpha_ListServiceResponse() + listServicesResponse.service = self.protoRegistry.serviceNames.map { serviceName in + Grpc_Reflection_V1alpha_ServiceResponse.with { + $0.name = serviceName + } + } + return Grpc_Reflection_V1alpha_ServerReflectionResponse( + request: request, + messageResponse: .listServicesResponse(listServicesResponse) + ) + } + + internal func findFileBySymbol( + _ symbolName: String, + request: Grpc_Reflection_V1alpha_ServerReflectionRequest + ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { + let result = self.protoRegistry.nameOfFileContainingSymbol( + named: symbolName + ).flatMap { + self._findFileByFileName($0) + } + return result.makeResponse(request: request) + } + + internal func findFileByExtension( + extensionRequest: Grpc_Reflection_V1alpha_ExtensionRequest, + request: Grpc_Reflection_V1alpha_ServerReflectionRequest + ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { + let result = self.protoRegistry.nameOfFileContainingExtension( + extendeeName: extensionRequest.containingType, + fieldNumber: extensionRequest.extensionNumber + ).flatMap { + self._findFileByFileName($0) + } + return result.makeResponse(request: request) + } + + internal func findExtensionsFieldNumbersOfType( + named typeName: String, + request: Grpc_Reflection_V1alpha_ServerReflectionRequest + ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { + let result = self.protoRegistry.extensionsFieldNumbersOfType( + named: typeName + ).map { fieldNumbers in + Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse + .allExtensionNumbersResponse( + Grpc_Reflection_V1alpha_ExtensionNumberResponse.with { + $0.baseTypeName = typeName + $0.extensionNumber = fieldNumbers + } + ) + } + return result.makeResponse(request: request) + } + + internal func serverReflectionInfo( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws { + for try await request in requestStream { + switch request.messageRequest { + case let .fileByFilename(fileName): + let response = self.findFileByFileName( + fileName, + request: request + ) + try await responseStream.send(response) + + case .listServices: + let response = try self.getServicesNames(request: request) + try await responseStream.send(response) + + case let .fileContainingSymbol(symbolName): + let response = self.findFileBySymbol( + symbolName, + request: request + ) + try await responseStream.send(response) + + case let .fileContainingExtension(extensionRequest): + let response = self.findFileByExtension( + extensionRequest: extensionRequest, + request: request + ) + try await responseStream.send(response) + + case let .allExtensionNumbersOfType(typeName): + let response = self.findExtensionsFieldNumbersOfType( + named: typeName, + request: request + ) + try await responseStream.send(response) + + default: + let response = Grpc_Reflection_V1alpha_ServerReflectionResponse( + request: request, + messageResponse: .errorResponse( + Grpc_Reflection_V1alpha_ErrorResponse.with { + $0.errorCode = Int32(GRPCStatus.Code.unimplemented.rawValue) + $0.errorMessage = "The request is not implemented." + } + ) + ) + try await responseStream.send(response) + } + } + } +} + +extension Grpc_Reflection_V1alpha_ServerReflectionResponse { + init( + request: Grpc_Reflection_V1alpha_ServerReflectionRequest, + messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse + ) { + self = .with { + $0.validHost = request.host + $0.originalRequest = request + $0.messageResponse = messageResponse + } + } +} + +extension Result +{ + func recover() -> Result< + Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse, Never + > { + self.flatMapError { status in + let error = Grpc_Reflection_V1alpha_ErrorResponse.with { + $0.errorCode = Int32(status.code.rawValue) + $0.errorMessage = status.message ?? "" + } + return .success(.errorResponse(error)) + } + } + + func makeResponse( + request: Grpc_Reflection_V1alpha_ServerReflectionRequest + ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { + let result = self.recover().attachRequest(request) + // Safe to '!' as the failure type is 'Never'. + return try! result.get() + } +} + +extension Result +where Success == Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse { + func attachRequest( + _ request: Grpc_Reflection_V1alpha_ServerReflectionRequest + ) -> Result { + self.map { message in + Grpc_Reflection_V1alpha_ServerReflectionResponse(request: request, messageResponse: message) + } + } +} diff --git a/Sources/GRPCReflectionService/Model/reflection.grpc.swift b/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift similarity index 99% rename from Sources/GRPCReflectionService/Model/reflection.grpc.swift rename to Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift index 0cd46f660..620845aac 100644 --- a/Sources/GRPCReflectionService/Model/reflection.grpc.swift +++ b/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift @@ -3,7 +3,7 @@ // swift-format-ignore-file // // Generated by the protocol buffer compiler. -// Source: reflection.proto +// Source: reflection-v1.proto // import GRPC import NIO diff --git a/Sources/GRPCReflectionService/Model/reflection.pb.swift b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift similarity index 99% rename from Sources/GRPCReflectionService/Model/reflection.pb.swift rename to Sources/GRPCReflectionService/v1/reflection-v1.pb.swift index 8e0d557c4..49162fb05 100644 --- a/Sources/GRPCReflectionService/Model/reflection.pb.swift +++ b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift @@ -2,7 +2,7 @@ // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection.proto +// Source: reflection-v1.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ diff --git a/Sources/GRPCReflectionService/Model/reflection.proto b/Sources/GRPCReflectionService/v1/reflection-v1.proto similarity index 100% rename from Sources/GRPCReflectionService/Model/reflection.proto rename to Sources/GRPCReflectionService/v1/reflection-v1.proto diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift new file mode 100644 index 000000000..eec65dda6 --- /dev/null +++ b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift @@ -0,0 +1,122 @@ +// +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the protocol buffer compiler. +// Source: reflection-v1alpha.proto +// +import GRPC +import NIO +import NIOConcurrencyHelpers +import SwiftProtobuf + + +/// To build a server, implement a class that conforms to this protocol. +internal protocol Grpc_Reflection_V1alpha_ServerReflectionProvider: CallHandlerProvider { + var interceptors: Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol? { get } + + /// The reflection service is structured as a bidirectional stream, ensuring + /// all related requests go to a single server. + func serverReflectionInfo(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> +} + +extension Grpc_Reflection_V1alpha_ServerReflectionProvider { + internal var serviceName: Substring { + return Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] + } + + /// Determines, calls and returns the appropriate request handler, depending on the request's method. + /// Returns nil for methods not handled by this service. + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "ServerReflectionInfo": + return BidirectionalStreamingServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], + observerFactory: self.serverReflectionInfo(context:) + ) + + default: + return nil + } + } +} + +/// To implement a server, implement an object which conforms to this protocol. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Grpc_Reflection_V1alpha_ServerReflectionAsyncProvider: CallHandlerProvider, Sendable { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol? { get } + + /// The reflection service is structured as a bidirectional stream, ensuring + /// all related requests go to a single server. + func serverReflectionInfo( + requestStream: GRPCAsyncRequestStream, + responseStream: GRPCAsyncResponseStreamWriter, + context: GRPCAsyncServerCallContext + ) async throws +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Grpc_Reflection_V1alpha_ServerReflectionAsyncProvider { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.serviceDescriptor + } + + internal var serviceName: Substring { + return Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] + } + + internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol? { + return nil + } + + internal func handle( + method name: Substring, + context: CallHandlerContext + ) -> GRPCServerHandlerProtocol? { + switch name { + case "ServerReflectionInfo": + return GRPCAsyncServerHandler( + context: context, + requestDeserializer: ProtobufDeserializer(), + responseSerializer: ProtobufSerializer(), + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], + wrapping: { try await self.serverReflectionInfo(requestStream: $0, responseStream: $1, context: $2) } + ) + + default: + return nil + } + } +} + +internal protocol Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when handling 'serverReflectionInfo'. + /// Defaults to calling `self.makeInterceptors()`. + func makeServerReflectionInfoInterceptors() -> [ServerInterceptor] +} + +internal enum Grpc_Reflection_V1alpha_ServerReflectionServerMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "ServerReflection", + fullName: "grpc.reflection.v1alpha.ServerReflection", + methods: [ + Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.Methods.serverReflectionInfo, + ] + ) + + internal enum Methods { + internal static let serverReflectionInfo = GRPCMethodDescriptor( + name: "ServerReflectionInfo", + path: "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", + type: GRPCCallType.bidirectionalStreaming + ) + } +} diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift new file mode 100644 index 000000000..84e739577 --- /dev/null +++ b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift @@ -0,0 +1,838 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: reflection-v1alpha.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2016 gRPC authors. +// +// 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. + +// Service exported by server reflection + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The message sent by the client when calling ServerReflectionInfo method. +public struct Grpc_Reflection_V1alpha_ServerReflectionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var host: String = String() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + public var messageRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest? = nil + + /// Find a proto file by the file name. + public var fileByFilename: String { + get { + if case .fileByFilename(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileByFilename(newValue)} + } + + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + public var fileContainingSymbol: String { + get { + if case .fileContainingSymbol(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileContainingSymbol(newValue)} + } + + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + public var fileContainingExtension: Grpc_Reflection_V1alpha_ExtensionRequest { + get { + if case .fileContainingExtension(let v)? = messageRequest {return v} + return Grpc_Reflection_V1alpha_ExtensionRequest() + } + set {messageRequest = .fileContainingExtension(newValue)} + } + + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + public var allExtensionNumbersOfType: String { + get { + if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .allExtensionNumbersOfType(newValue)} + } + + /// List the full names of registered services. The content will not be + /// checked. + public var listServices: String { + get { + if case .listServices(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .listServices(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + public enum OneOf_MessageRequest: Equatable { + /// Find a proto file by the file name. + case fileByFilename(String) + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + case fileContainingSymbol(String) + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + case fileContainingExtension(Grpc_Reflection_V1alpha_ExtensionRequest) + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + case allExtensionNumbersOfType(String) + /// List the full names of registered services. The content will not be + /// checked. + case listServices(String) + + #if !swift(>=4.1) + public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileByFilename, .fileByFilename): return { + guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingSymbol, .fileContainingSymbol): return { + guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingExtension, .fileContainingExtension): return { + guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { + guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServices, .listServices): return { + guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + public init() {} +} + +/// The type name and extension number sent by the client when requesting +/// file_containing_extension. +public struct Grpc_Reflection_V1alpha_ExtensionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Fully-qualified type name. The format should be . + public var containingType: String = String() + + public var extensionNumber: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// The message sent by the server to answer ServerReflectionInfo method. +public struct Grpc_Reflection_V1alpha_ServerReflectionResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var validHost: String = String() + + public var originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest { + get {return _originalRequest ?? Grpc_Reflection_V1alpha_ServerReflectionRequest()} + set {_originalRequest = newValue} + } + /// Returns true if `originalRequest` has been explicitly set. + public var hasOriginalRequest: Bool {return self._originalRequest != nil} + /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. + public mutating func clearOriginalRequest() {self._originalRequest = nil} + + /// The server set one of the following fields accroding to the message_request + /// in the request. + public var messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse? = nil + + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. As + /// the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + public var fileDescriptorResponse: Grpc_Reflection_V1alpha_FileDescriptorResponse { + get { + if case .fileDescriptorResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_FileDescriptorResponse() + } + set {messageResponse = .fileDescriptorResponse(newValue)} + } + + /// This message is used to answer all_extension_numbers_of_type requst. + public var allExtensionNumbersResponse: Grpc_Reflection_V1alpha_ExtensionNumberResponse { + get { + if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_ExtensionNumberResponse() + } + set {messageResponse = .allExtensionNumbersResponse(newValue)} + } + + /// This message is used to answer list_services request. + public var listServicesResponse: Grpc_Reflection_V1alpha_ListServiceResponse { + get { + if case .listServicesResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_ListServiceResponse() + } + set {messageResponse = .listServicesResponse(newValue)} + } + + /// This message is used when an error occurs. + public var errorResponse: Grpc_Reflection_V1alpha_ErrorResponse { + get { + if case .errorResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_ErrorResponse() + } + set {messageResponse = .errorResponse(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The server set one of the following fields accroding to the message_request + /// in the request. + public enum OneOf_MessageResponse: Equatable { + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. As + /// the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + case fileDescriptorResponse(Grpc_Reflection_V1alpha_FileDescriptorResponse) + /// This message is used to answer all_extension_numbers_of_type requst. + case allExtensionNumbersResponse(Grpc_Reflection_V1alpha_ExtensionNumberResponse) + /// This message is used to answer list_services request. + case listServicesResponse(Grpc_Reflection_V1alpha_ListServiceResponse) + /// This message is used when an error occurs. + case errorResponse(Grpc_Reflection_V1alpha_ErrorResponse) + + #if !swift(>=4.1) + public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileDescriptorResponse, .fileDescriptorResponse): return { + guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { + guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServicesResponse, .listServicesResponse): return { + guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.errorResponse, .errorResponse): return { + guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + public init() {} + + fileprivate var _originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest? = nil +} + +/// Serialized FileDescriptorProto messages sent by the server answering +/// a file_by_filename, file_containing_symbol, or file_containing_extension +/// request. +public struct Grpc_Reflection_V1alpha_FileDescriptorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Serialized FileDescriptorProto messages. We avoid taking a dependency on + /// descriptor.proto, which uses proto2 only features, by making them opaque + /// bytes instead. + public var fileDescriptorProto: [Data] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// A list of extension numbers sent by the server answering +/// all_extension_numbers_of_type request. +public struct Grpc_Reflection_V1alpha_ExtensionNumberResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of the base type, including the package name. The format + /// is . + public var baseTypeName: String = String() + + public var extensionNumber: [Int32] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// A list of ServiceResponse sent by the server answering list_services request. +public struct Grpc_Reflection_V1alpha_ListServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The information of each service may be expanded in the future, so we use + /// ServiceResponse message to encapsulate it. + public var service: [Grpc_Reflection_V1alpha_ServiceResponse] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// The information of a single service used by ListServiceResponse to answer +/// list_services request. +public struct Grpc_Reflection_V1alpha_ServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of a registered service, including its package name. The format + /// is . + public var name: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// The error code and error message sent by the server when an error occurs. +public struct Grpc_Reflection_V1alpha_ErrorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// This field uses the error codes defined in grpc::StatusCode. + public var errorCode: Int32 = 0 + + public var errorMessage: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Reflection_V1alpha_ServerReflectionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ExtensionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServerReflectionResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ListServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ErrorResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.reflection.v1alpha" + +extension Grpc_Reflection_V1alpha_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "host"), + 3: .standard(proto: "file_by_filename"), + 4: .standard(proto: "file_containing_symbol"), + 5: .standard(proto: "file_containing_extension"), + 6: .standard(proto: "all_extension_numbers_of_type"), + 7: .standard(proto: "list_services"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() + case 3: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileByFilename(v) + } + }() + case 4: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingSymbol(v) + } + }() + case 5: try { + var v: Grpc_Reflection_V1alpha_ExtensionRequest? + var hadOneofValue = false + if let current = self.messageRequest { + hadOneofValue = true + if case .fileContainingExtension(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingExtension(v) + } + }() + case 6: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .allExtensionNumbersOfType(v) + } + }() + case 7: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .listServices(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.host.isEmpty { + try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) + } + switch self.messageRequest { + case .fileByFilename?: try { + guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + }() + case .fileContainingSymbol?: try { + guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + }() + case .fileContainingExtension?: try { + guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .allExtensionNumbersOfType?: try { + guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + }() + case .listServices?: try { + guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest) -> Bool { + if lhs.host != rhs.host {return false} + if lhs.messageRequest != rhs.messageRequest {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "containing_type"), + 2: .standard(proto: "extension_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.containingType.isEmpty { + try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) + } + if self.extensionNumber != 0 { + try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionRequest, rhs: Grpc_Reflection_V1alpha_ExtensionRequest) -> Bool { + if lhs.containingType != rhs.containingType {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "valid_host"), + 2: .standard(proto: "original_request"), + 4: .standard(proto: "file_descriptor_response"), + 5: .standard(proto: "all_extension_numbers_response"), + 6: .standard(proto: "list_services_response"), + 7: .standard(proto: "error_response"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() + case 4: try { + var v: Grpc_Reflection_V1alpha_FileDescriptorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .fileDescriptorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .fileDescriptorResponse(v) + } + }() + case 5: try { + var v: Grpc_Reflection_V1alpha_ExtensionNumberResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .allExtensionNumbersResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .allExtensionNumbersResponse(v) + } + }() + case 6: try { + var v: Grpc_Reflection_V1alpha_ListServiceResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .listServicesResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .listServicesResponse(v) + } + }() + case 7: try { + var v: Grpc_Reflection_V1alpha_ErrorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .errorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .errorResponse(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.validHost.isEmpty { + try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) + } + try { if let v = self._originalRequest { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + switch self.messageResponse { + case .fileDescriptorResponse?: try { + guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .allExtensionNumbersResponse?: try { + guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .listServicesResponse?: try { + guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .errorResponse?: try { + guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Bool { + if lhs.validHost != rhs.validHost {return false} + if lhs._originalRequest != rhs._originalRequest {return false} + if lhs.messageResponse != rhs.messageResponse {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "file_descriptor_proto"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.fileDescriptorProto.isEmpty { + try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_FileDescriptorResponse, rhs: Grpc_Reflection_V1alpha_FileDescriptorResponse) -> Bool { + if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "base_type_name"), + 2: .standard(proto: "extension_number"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.baseTypeName.isEmpty { + try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) + } + if !self.extensionNumber.isEmpty { + try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse, rhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse) -> Bool { + if lhs.baseTypeName != rhs.baseTypeName {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "service"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.service.isEmpty { + try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_ListServiceResponse, rhs: Grpc_Reflection_V1alpha_ListServiceResponse) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ServiceResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_ServiceResponse, rhs: Grpc_Reflection_V1alpha_ServiceResponse) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ErrorResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "error_code"), + 2: .standard(proto: "error_message"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.errorCode != 0 { + try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) + } + if !self.errorMessage.isEmpty { + try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Reflection_V1alpha_ErrorResponse, rhs: Grpc_Reflection_V1alpha_ErrorResponse) -> Bool { + if lhs.errorCode != rhs.errorCode {return false} + if lhs.errorMessage != rhs.errorMessage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto new file mode 100644 index 000000000..816852f82 --- /dev/null +++ b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto @@ -0,0 +1,136 @@ +// Copyright 2016 gRPC authors. +// +// 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. + +// Service exported by server reflection + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server set one of the following fields accroding to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. As + // the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requst. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services request. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift similarity index 99% rename from Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift rename to Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift index 57a605e77..6f33bf6a9 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.grpc.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift @@ -3,7 +3,7 @@ // swift-format-ignore-file // // Generated by the protocol buffer compiler. -// Source: reflection.proto +// Source: reflection-v1.proto // import GRPC import NIO diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift similarity index 99% rename from Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift rename to Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift index 281330e5f..093d1ee2c 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/reflection.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift @@ -2,7 +2,7 @@ // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection.proto +// Source: reflection-v1.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift new file mode 100644 index 000000000..48b9116aa --- /dev/null +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift @@ -0,0 +1,208 @@ +// +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the protocol buffer compiler. +// Source: reflection-v1alpha.proto +// +import GRPC +import NIO +import NIOConcurrencyHelpers +import SwiftProtobuf + + +/// Usage: instantiate `Grpc_Reflection_V1alpha_ServerReflectionClient`, then call methods of this protocol to make API calls. +internal protocol Grpc_Reflection_V1alpha_ServerReflectionClientProtocol: GRPCClient { + var serviceName: String { get } + var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { get } + + func serverReflectionInfo( + callOptions: CallOptions?, + handler: @escaping (Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Void + ) -> BidirectionalStreamingCall +} + +extension Grpc_Reflection_V1alpha_ServerReflectionClientProtocol { + internal var serviceName: String { + return "grpc.reflection.v1alpha.ServerReflection" + } + + /// The reflection service is structured as a bidirectional stream, ensuring + /// all related requests go to a single server. + /// + /// Callers should use the `send` method on the returned object to send messages + /// to the server. The caller should send an `.end` after the final message has been sent. + /// + /// - Parameters: + /// - callOptions: Call options. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. + internal func serverReflectionInfo( + callOptions: CallOptions? = nil, + handler: @escaping (Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Void + ) -> BidirectionalStreamingCall { + return self.makeBidirectionalStreamingCall( + path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], + handler: handler + ) + } +} + +@available(*, deprecated) +extension Grpc_Reflection_V1alpha_ServerReflectionClient: @unchecked Sendable {} + +@available(*, deprecated, renamed: "Grpc_Reflection_V1alpha_ServerReflectionNIOClient") +internal final class Grpc_Reflection_V1alpha_ServerReflectionClient: Grpc_Reflection_V1alpha_ServerReflectionClientProtocol { + private let lock = Lock() + private var _defaultCallOptions: CallOptions + private var _interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? + internal let channel: GRPCChannel + internal var defaultCallOptions: CallOptions { + get { self.lock.withLock { return self._defaultCallOptions } } + set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } + } + internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { + get { self.lock.withLock { return self._interceptors } } + set { self.lock.withLockVoid { self._interceptors = newValue } } + } + + /// Creates a client for the grpc.reflection.v1alpha.ServerReflection service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self._defaultCallOptions = defaultCallOptions + self._interceptors = interceptors + } +} + +internal struct Grpc_Reflection_V1alpha_ServerReflectionNIOClient: Grpc_Reflection_V1alpha_ServerReflectionClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? + + /// Creates a client for the grpc.reflection.v1alpha.ServerReflection service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal protocol Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol: GRPCClient { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { get } + + func makeServerReflectionInfoCall( + callOptions: CallOptions? + ) -> GRPCAsyncBidirectionalStreamingCall +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol { + internal static var serviceDescriptor: GRPCServiceDescriptor { + return Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.serviceDescriptor + } + + internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { + return nil + } + + internal func makeServerReflectionInfoCall( + callOptions: CallOptions? = nil + ) -> GRPCAsyncBidirectionalStreamingCall { + return self.makeAsyncBidirectionalStreamingCall( + path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol { + internal func serverReflectionInfo( + _ requests: RequestStream, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Reflection_V1alpha_ServerReflectionRequest { + return self.performAsyncBidirectionalStreamingCall( + path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + requests: requests, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] + ) + } + + internal func serverReflectionInfo( + _ requests: RequestStream, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Reflection_V1alpha_ServerReflectionRequest { + return self.performAsyncBidirectionalStreamingCall( + path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, + requests: requests, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +internal struct Grpc_Reflection_V1alpha_ServerReflectionAsyncClient: Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol { + internal var channel: GRPCChannel + internal var defaultCallOptions: CallOptions + internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? + + internal init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +internal protocol Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when invoking 'serverReflectionInfo'. + func makeServerReflectionInfoInterceptors() -> [ClientInterceptor] +} + +internal enum Grpc_Reflection_V1alpha_ServerReflectionClientMetadata { + internal static let serviceDescriptor = GRPCServiceDescriptor( + name: "ServerReflection", + fullName: "grpc.reflection.v1alpha.ServerReflection", + methods: [ + Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo, + ] + ) + + internal enum Methods { + internal static let serverReflectionInfo = GRPCMethodDescriptor( + name: "ServerReflectionInfo", + path: "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", + type: GRPCCallType.bidirectionalStreaming + ) + } +} + diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift new file mode 100644 index 000000000..6d45fdf41 --- /dev/null +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift @@ -0,0 +1,838 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: reflection-v1alpha.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2016 gRPC authors. +// +// 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. + +// Service exported by server reflection + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The message sent by the client when calling ServerReflectionInfo method. +struct Grpc_Reflection_V1alpha_ServerReflectionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var host: String = String() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + var messageRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest? = nil + + /// Find a proto file by the file name. + var fileByFilename: String { + get { + if case .fileByFilename(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileByFilename(newValue)} + } + + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + var fileContainingSymbol: String { + get { + if case .fileContainingSymbol(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .fileContainingSymbol(newValue)} + } + + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + var fileContainingExtension: Grpc_Reflection_V1alpha_ExtensionRequest { + get { + if case .fileContainingExtension(let v)? = messageRequest {return v} + return Grpc_Reflection_V1alpha_ExtensionRequest() + } + set {messageRequest = .fileContainingExtension(newValue)} + } + + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + var allExtensionNumbersOfType: String { + get { + if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .allExtensionNumbersOfType(newValue)} + } + + /// List the full names of registered services. The content will not be + /// checked. + var listServices: String { + get { + if case .listServices(let v)? = messageRequest {return v} + return String() + } + set {messageRequest = .listServices(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// To use reflection service, the client should set one of the following + /// fields in message_request. The server distinguishes requests by their + /// defined field and then handles them using corresponding methods. + enum OneOf_MessageRequest: Equatable { + /// Find a proto file by the file name. + case fileByFilename(String) + /// Find the proto file that declares the given fully-qualified symbol name. + /// This field should be a fully-qualified symbol name + /// (e.g. .[.] or .). + case fileContainingSymbol(String) + /// Find the proto file which defines an extension extending the given + /// message type with the given field number. + case fileContainingExtension(Grpc_Reflection_V1alpha_ExtensionRequest) + /// Finds the tag numbers used by all known extensions of the given message + /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Its corresponding method is best-effort: it's not guaranteed that the + /// reflection service will implement this method, and it's not guaranteed + /// that this method will provide all extensions. Returns + /// StatusCode::UNIMPLEMENTED if it's not implemented. + /// This field should be a fully-qualified type name. The format is + /// . + case allExtensionNumbersOfType(String) + /// List the full names of registered services. The content will not be + /// checked. + case listServices(String) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileByFilename, .fileByFilename): return { + guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingSymbol, .fileContainingSymbol): return { + guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.fileContainingExtension, .fileContainingExtension): return { + guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { + guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServices, .listServices): return { + guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +/// The type name and extension number sent by the client when requesting +/// file_containing_extension. +struct Grpc_Reflection_V1alpha_ExtensionRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Fully-qualified type name. The format should be . + var containingType: String = String() + + var extensionNumber: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The message sent by the server to answer ServerReflectionInfo method. +struct Grpc_Reflection_V1alpha_ServerReflectionResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var validHost: String = String() + + var originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest { + get {return _originalRequest ?? Grpc_Reflection_V1alpha_ServerReflectionRequest()} + set {_originalRequest = newValue} + } + /// Returns true if `originalRequest` has been explicitly set. + var hasOriginalRequest: Bool {return self._originalRequest != nil} + /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. + mutating func clearOriginalRequest() {self._originalRequest = nil} + + /// The server set one of the following fields accroding to the message_request + /// in the request. + var messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse? = nil + + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. As + /// the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + var fileDescriptorResponse: Grpc_Reflection_V1alpha_FileDescriptorResponse { + get { + if case .fileDescriptorResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_FileDescriptorResponse() + } + set {messageResponse = .fileDescriptorResponse(newValue)} + } + + /// This message is used to answer all_extension_numbers_of_type requst. + var allExtensionNumbersResponse: Grpc_Reflection_V1alpha_ExtensionNumberResponse { + get { + if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_ExtensionNumberResponse() + } + set {messageResponse = .allExtensionNumbersResponse(newValue)} + } + + /// This message is used to answer list_services request. + var listServicesResponse: Grpc_Reflection_V1alpha_ListServiceResponse { + get { + if case .listServicesResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_ListServiceResponse() + } + set {messageResponse = .listServicesResponse(newValue)} + } + + /// This message is used when an error occurs. + var errorResponse: Grpc_Reflection_V1alpha_ErrorResponse { + get { + if case .errorResponse(let v)? = messageResponse {return v} + return Grpc_Reflection_V1alpha_ErrorResponse() + } + set {messageResponse = .errorResponse(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The server set one of the following fields accroding to the message_request + /// in the request. + enum OneOf_MessageResponse: Equatable { + /// This message is used to answer file_by_filename, file_containing_symbol, + /// file_containing_extension requests with transitive dependencies. As + /// the repeated label is not allowed in oneof fields, we use a + /// FileDescriptorResponse message to encapsulate the repeated fields. + /// The reflection service is allowed to avoid sending FileDescriptorProtos + /// that were previously sent in response to earlier requests in the stream. + case fileDescriptorResponse(Grpc_Reflection_V1alpha_FileDescriptorResponse) + /// This message is used to answer all_extension_numbers_of_type requst. + case allExtensionNumbersResponse(Grpc_Reflection_V1alpha_ExtensionNumberResponse) + /// This message is used to answer list_services request. + case listServicesResponse(Grpc_Reflection_V1alpha_ListServiceResponse) + /// This message is used when an error occurs. + case errorResponse(Grpc_Reflection_V1alpha_ErrorResponse) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.fileDescriptorResponse, .fileDescriptorResponse): return { + guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { + guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.listServicesResponse, .listServicesResponse): return { + guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.errorResponse, .errorResponse): return { + guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} + + fileprivate var _originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest? = nil +} + +/// Serialized FileDescriptorProto messages sent by the server answering +/// a file_by_filename, file_containing_symbol, or file_containing_extension +/// request. +struct Grpc_Reflection_V1alpha_FileDescriptorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Serialized FileDescriptorProto messages. We avoid taking a dependency on + /// descriptor.proto, which uses proto2 only features, by making them opaque + /// bytes instead. + var fileDescriptorProto: [Data] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A list of extension numbers sent by the server answering +/// all_extension_numbers_of_type request. +struct Grpc_Reflection_V1alpha_ExtensionNumberResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of the base type, including the package name. The format + /// is . + var baseTypeName: String = String() + + var extensionNumber: [Int32] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A list of ServiceResponse sent by the server answering list_services request. +struct Grpc_Reflection_V1alpha_ListServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The information of each service may be expanded in the future, so we use + /// ServiceResponse message to encapsulate it. + var service: [Grpc_Reflection_V1alpha_ServiceResponse] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The information of a single service used by ListServiceResponse to answer +/// list_services request. +struct Grpc_Reflection_V1alpha_ServiceResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Full name of a registered service, including its package name. The format + /// is . + var name: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The error code and error message sent by the server when an error occurs. +struct Grpc_Reflection_V1alpha_ErrorResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// This field uses the error codes defined in grpc::StatusCode. + var errorCode: Int32 = 0 + + var errorMessage: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Reflection_V1alpha_ServerReflectionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ExtensionRequest: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServerReflectionResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ListServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ServiceResponse: @unchecked Sendable {} +extension Grpc_Reflection_V1alpha_ErrorResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.reflection.v1alpha" + +extension Grpc_Reflection_V1alpha_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "host"), + 3: .standard(proto: "file_by_filename"), + 4: .standard(proto: "file_containing_symbol"), + 5: .standard(proto: "file_containing_extension"), + 6: .standard(proto: "all_extension_numbers_of_type"), + 7: .standard(proto: "list_services"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() + case 3: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileByFilename(v) + } + }() + case 4: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingSymbol(v) + } + }() + case 5: try { + var v: Grpc_Reflection_V1alpha_ExtensionRequest? + var hadOneofValue = false + if let current = self.messageRequest { + hadOneofValue = true + if case .fileContainingExtension(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageRequest = .fileContainingExtension(v) + } + }() + case 6: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .allExtensionNumbersOfType(v) + } + }() + case 7: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.messageRequest != nil {try decoder.handleConflictingOneOf()} + self.messageRequest = .listServices(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.host.isEmpty { + try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) + } + switch self.messageRequest { + case .fileByFilename?: try { + guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + }() + case .fileContainingSymbol?: try { + guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + }() + case .fileContainingExtension?: try { + guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .allExtensionNumbersOfType?: try { + guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 6) + }() + case .listServices?: try { + guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest) -> Bool { + if lhs.host != rhs.host {return false} + if lhs.messageRequest != rhs.messageRequest {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "containing_type"), + 2: .standard(proto: "extension_number"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.containingType.isEmpty { + try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) + } + if self.extensionNumber != 0 { + try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionRequest, rhs: Grpc_Reflection_V1alpha_ExtensionRequest) -> Bool { + if lhs.containingType != rhs.containingType {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "valid_host"), + 2: .standard(proto: "original_request"), + 4: .standard(proto: "file_descriptor_response"), + 5: .standard(proto: "all_extension_numbers_response"), + 6: .standard(proto: "list_services_response"), + 7: .standard(proto: "error_response"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() + case 4: try { + var v: Grpc_Reflection_V1alpha_FileDescriptorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .fileDescriptorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .fileDescriptorResponse(v) + } + }() + case 5: try { + var v: Grpc_Reflection_V1alpha_ExtensionNumberResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .allExtensionNumbersResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .allExtensionNumbersResponse(v) + } + }() + case 6: try { + var v: Grpc_Reflection_V1alpha_ListServiceResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .listServicesResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .listServicesResponse(v) + } + }() + case 7: try { + var v: Grpc_Reflection_V1alpha_ErrorResponse? + var hadOneofValue = false + if let current = self.messageResponse { + hadOneofValue = true + if case .errorResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.messageResponse = .errorResponse(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.validHost.isEmpty { + try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) + } + try { if let v = self._originalRequest { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + switch self.messageResponse { + case .fileDescriptorResponse?: try { + guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .allExtensionNumbersResponse?: try { + guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .listServicesResponse?: try { + guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .errorResponse?: try { + guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Bool { + if lhs.validHost != rhs.validHost {return false} + if lhs._originalRequest != rhs._originalRequest {return false} + if lhs.messageResponse != rhs.messageResponse {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "file_descriptor_proto"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.fileDescriptorProto.isEmpty { + try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_FileDescriptorResponse, rhs: Grpc_Reflection_V1alpha_FileDescriptorResponse) -> Bool { + if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "base_type_name"), + 2: .standard(proto: "extension_number"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.baseTypeName.isEmpty { + try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) + } + if !self.extensionNumber.isEmpty { + try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse, rhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse) -> Bool { + if lhs.baseTypeName != rhs.baseTypeName {return false} + if lhs.extensionNumber != rhs.extensionNumber {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "service"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.service.isEmpty { + try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_ListServiceResponse, rhs: Grpc_Reflection_V1alpha_ListServiceResponse) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServiceResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_ServiceResponse, rhs: Grpc_Reflection_V1alpha_ServiceResponse) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Reflection_V1alpha_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ErrorResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "error_code"), + 2: .standard(proto: "error_message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.errorCode != 0 { + try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) + } + if !self.errorMessage.isEmpty { + try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Reflection_V1alpha_ErrorResponse, rhs: Grpc_Reflection_V1alpha_ErrorResponse) -> Bool { + if lhs.errorCode != rhs.errorCode {return false} + if lhs.errorMessage != rhs.errorMessage {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index 1dad7194c..f47aa12ec 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -31,10 +31,12 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { fileName: "independentBar", suffix: "5" ) + private let versions: [ReflectionService.Version] = [.v1, .v1Alpha] - private func setUpServerAndChannel() throws { + private func setUpServerAndChannel(version: ReflectionService.Version) throws { let reflectionServiceProvider = try ReflectionService( - fileDescriptors: self.protos + [self.independentProto] + fileDescriptorProtos: self.protos + [self.independentProto], + version: version ) let server = try Server.insecure(group: MultiThreadedEventLoopGroup.singleton) @@ -66,284 +68,274 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { super.tearDown() } + private func getServerReflectionResponse( + for request: Grpc_Reflection_V1_ServerReflectionRequest, + version: ReflectionService.Version + ) async throws -> Grpc_Reflection_V1_ServerReflectionResponse? { + let response: Grpc_Reflection_V1_ServerReflectionResponse? + switch version { + case .v1: + let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send(request) + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + response = try await iterator.next() + case .v1Alpha: + let client = Grpc_Reflection_V1alpha_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + try await serviceReflectionInfo.requestStream.send( + Grpc_Reflection_V1alpha_ServerReflectionRequest(request) + ) + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + response = try await iterator.next().map { + Grpc_Reflection_V1_ServerReflectionResponse($0) + } + default: + return nil + } + return response + } + + private func forEachVersion( + _ body: (GRPCChannel?, ReflectionService.Version) async throws -> Void + ) async throws { + for version in self.versions { + try setUpServerAndChannel(version: version) + let result: Result + do { + try await body(self.channel, version) + result = .success(()) + } catch { + result = .failure(error) + } + try result.get() + try await self.tearDown() + } + } + func testFileByFileName() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.fileByFilename = "bar1.proto" } - ) - serviceReflectionInfo.requestStream.finish() + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + + // response can't be nil as we just checked it. + let receivedFileDescriptorProto = + try Google_Protobuf_FileDescriptorProto( + serializedData: (message.fileDescriptorResponse + .fileDescriptorProto[0]) + ) - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") - } + XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto") + XCTAssertEqual(receivedFileDescriptorProto.service.count, 1) - let receivedFileDescriptorProto = - try Google_Protobuf_FileDescriptorProto( - serializedData: (message.fileDescriptorResponse - .fileDescriptorProto[0]) + let service = try XCTUnwrap( + receivedFileDescriptorProto.service.first, + "The received file descriptor proto doesn't have any services." ) - - XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto") - XCTAssertEqual(receivedFileDescriptorProto.service.count, 1) - - guard let service = receivedFileDescriptorProto.service.first else { - return XCTFail("The received file descriptor proto doesn't have any services.") - } - guard let method = service.method.first else { - return XCTFail("The service of the received file descriptor proto doesn't have any methods.") + let method = try XCTUnwrap( + service.method.first, + "The service of the received file descriptor proto doesn't have any methods." + ) + XCTAssertEqual(method.name, "testMethod1") + XCTAssertEqual(message.fileDescriptorResponse.fileDescriptorProto.count, 4) } - XCTAssertEqual(method.name, "testMethod1") - XCTAssertEqual(message.fileDescriptorResponse.fileDescriptorProto.count, 4) } func testListServices() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.listServices = "services" } - ) - - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") - } - - let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted() - let servicesNames = (self.protos + [self.independentProto]).flatMap { $0.qualifiedServiceNames } + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted() + let servicesNames = (self.protos + [self.independentProto]).flatMap { + $0.qualifiedServiceNames + } .sorted() - XCTAssertEqual(receivedServices, servicesNames) + XCTAssertEqual(receivedServices, servicesNames) + } } func testFileBySymbol() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.fileContainingSymbol = "packagebar1.enumType1" } - ) - - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") - } - let receivedData: [Google_Protobuf_FileDescriptorProto] - do { - receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + let receivedData: [Google_Protobuf_FileDescriptorProto] + do { + receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + } catch { + return XCTFail("Could not serialize data received as a message.") } - } catch { - return XCTFail("Could not serialize data received as a message.") - } - let fileToFind = self.protos[0] - let dependentProtos = self.protos[1...] - for fileDescriptorProto in receivedData { - if fileDescriptorProto == fileToFind { - XCTAssert( - fileDescriptorProto.enumType.names.contains("enumType1"), - """ - The response doesn't contain the serialized file descriptor proto \ - containing the \"packagebar1.enumType1\" symbol. - """ - ) - } else { - XCTAssert( - dependentProtos.contains(fileDescriptorProto), - """ - The \(fileDescriptorProto.name) is not a dependency of the \ - proto file containing the \"packagebar1.enumType1\" symbol. - """ - ) + let fileToFind = self.protos[0] + let dependentProtos = self.protos[1...] + for fileDescriptorProto in receivedData { + if fileDescriptorProto == fileToFind { + XCTAssert( + fileDescriptorProto.enumType.names.contains("enumType1"), + """ + The response doesn't contain the serialized file descriptor proto \ + containing the \"packagebar1.enumType1\" symbol. + """ + ) + } else { + XCTAssert( + dependentProtos.contains(fileDescriptorProto), + """ + The \(fileDescriptorProto.name) is not a dependency of the \ + proto file containing the \"packagebar1.enumType1\" symbol. + """ + ) + } } } } func testFileByExtension() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.fileContainingExtension = .with { $0.containingType = "packagebar1.inputMessage1" $0.extensionNumber = 2 } } - ) - - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") - } - let receivedData: [Google_Protobuf_FileDescriptorProto] - do { - receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + let receivedData: [Google_Protobuf_FileDescriptorProto] + do { + receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + } catch { + return XCTFail("Could not serialize data received as a message.") } - } catch { - return XCTFail("Could not serialize data received as a message.") - } - let fileToFind = self.protos[0] - let dependentProtos = self.protos[1...] - var receivedProtoContainingExtension = 0 - var dependenciesCount = 0 - for fileDescriptorProto in receivedData { - if fileDescriptorProto == fileToFind { - receivedProtoContainingExtension += 1 - XCTAssert( - fileDescriptorProto.extension.map { $0.name }.contains( - "extension.packagebar1.inputMessage1-2" - ), - """ - The response doesn't contain the serialized file descriptor proto \ - containing the \"extensioninputMessage1-2\" extension. - """ - ) - } else { - dependenciesCount += 1 - XCTAssert( - dependentProtos.contains(fileDescriptorProto), - """ - The \(fileDescriptorProto.name) is not a dependency of the \ - proto file containing the \"extensioninputMessage1-2\" extension. - """ - ) + let fileToFind = self.protos[0] + let dependentProtos = self.protos[1...] + var receivedProtoContainingExtension = 0 + var dependenciesCount = 0 + for fileDescriptorProto in receivedData { + if fileDescriptorProto == fileToFind { + receivedProtoContainingExtension += 1 + XCTAssert( + fileDescriptorProto.extension.map { $0.name }.contains( + "extension.packagebar1.inputMessage1-2" + ), + """ + The response doesn't contain the serialized file descriptor proto \ + containing the \"extensioninputMessage1-2\" extension. + """ + ) + } else { + dependenciesCount += 1 + XCTAssert( + dependentProtos.contains(fileDescriptorProto), + """ + The \(fileDescriptorProto.name) is not a dependency of the \ + proto file containing the \"extensioninputMessage1-2\" extension. + """ + ) + } } + XCTAssertEqual( + receivedProtoContainingExtension, + 1, + "The file descriptor proto of the proto containing the extension was not received." + ) + XCTAssertEqual(dependenciesCount, 3) } - XCTAssertEqual( - receivedProtoContainingExtension, - 1, - "The file descriptor proto of the proto containing the extension was not received." - ) - XCTAssertEqual(dependenciesCount, 3) } func testAllExtensionNumbersOfType() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.allExtensionNumbersOfType = "packagebar2.inputMessage2" } - ) - - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + XCTAssertEqual(message.allExtensionNumbersResponse.baseTypeName, "packagebar2.inputMessage2") + XCTAssertEqual(message.allExtensionNumbersResponse.extensionNumber, [1, 2, 3, 4, 5]) } - XCTAssertEqual(message.allExtensionNumbersResponse.baseTypeName, "packagebar2.inputMessage2") - XCTAssertEqual(message.allExtensionNumbersResponse.extensionNumber, [1, 2, 3, 4, 5]) } func testErrorResponseFileByFileNameRequest() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.fileByFilename = "invalidFileName.proto" } - ) - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) + XCTAssertEqual( + message.errorResponse.errorMessage, + "The provided file or a dependency of the provided file could not be found." + ) } - - XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) - XCTAssertEqual( - message.errorResponse.errorMessage, - "The provided file or a dependency of the provided file could not be found." - ) } func testErrorResponseFileBySymbolRequest() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.fileContainingSymbol = "packagebar1.invalidEnumType1" } - ) - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) + XCTAssertEqual(message.errorResponse.errorMessage, "The provided symbol could not be found.") } - - XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) - XCTAssertEqual(message.errorResponse.errorMessage, "The provided symbol could not be found.") } func testErrorResponseFileByExtensionRequest() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.fileContainingExtension = .with { $0.containingType = "packagebar1.invalidInputMessage1" $0.extensionNumber = 2 } } - ) - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) + XCTAssertEqual( + message.errorResponse.errorMessage, + "The provided extension could not be found." + ) } - - XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) - XCTAssertEqual(message.errorResponse.errorMessage, "The provided extension could not be found.") } func testErrorResponseAllExtensionNumbersOfTypeRequest() async throws { - try self.setUpServerAndChannel() - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send( - .with { + try await self.forEachVersion { channel, version in + let request = Grpc_Reflection_V1_ServerReflectionRequest.with { $0.host = "127.0.0.1" $0.allExtensionNumbersOfType = "packagebar2.invalidInputMessage2" } - ) - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - guard let message = try await iterator.next() else { - return XCTFail("Could not get a response message.") + let response = try await self.getServerReflectionResponse(for: request, version: version) + let message = try XCTUnwrap(response, "Could not get a response message.") + XCTAssertEqual( + message.errorResponse.errorCode, + Int32(GRPCStatus.Code.invalidArgument.rawValue) + ) + XCTAssertEqual(message.errorResponse.errorMessage, "The provided type is invalid.") } - - XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.invalidArgument.rawValue)) - XCTAssertEqual(message.errorResponse.errorMessage, "The provided type is invalid.") } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift index 7d5af0850..75a1e2fed 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift @@ -161,3 +161,187 @@ extension Sequence where Element == Google_Protobuf_EnumDescriptorProto { self.map { $0.name } } } + +extension Grpc_Reflection_V1_ExtensionRequest { + init(_ v1AlphaExtensionRequest: Grpc_Reflection_V1alpha_ExtensionRequest) { + self = .with { + $0.containingType = v1AlphaExtensionRequest.containingType + $0.extensionNumber = v1AlphaExtensionRequest.extensionNumber + $0.unknownFields = v1AlphaExtensionRequest.unknownFields + } + } +} + +extension Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest? { + init(_ v1AlphaRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest) { + guard let messageRequest = v1AlphaRequest.messageRequest else { + self = nil + return + } + switch messageRequest { + case .allExtensionNumbersOfType(let typeName): + self = .allExtensionNumbersOfType(typeName) + case .fileByFilename(let fileName): + self = .fileByFilename(fileName) + case .fileContainingSymbol(let symbol): + self = .fileContainingSymbol(symbol) + case .fileContainingExtension(let v1AlphaExtensionRequest): + self = .fileContainingExtension( + Grpc_Reflection_V1_ExtensionRequest(v1AlphaExtensionRequest) + ) + case .listServices(let parameter): + self = .listServices(parameter) + } + } +} + +extension Grpc_Reflection_V1_ServerReflectionRequest { + init(_ v1AlphaRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest) { + self = .with { + $0.host = v1AlphaRequest.host + $0.messageRequest = Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest?( + v1AlphaRequest + ) + } + } +} + +extension Grpc_Reflection_V1_FileDescriptorResponse { + init(_ v1AlphaFileDescriptorResponse: Grpc_Reflection_V1alpha_FileDescriptorResponse) { + self = .with { + $0.fileDescriptorProto = v1AlphaFileDescriptorResponse.fileDescriptorProto + $0.unknownFields = v1AlphaFileDescriptorResponse.unknownFields + } + } +} + +extension Grpc_Reflection_V1_ExtensionNumberResponse { + init(_ v1AlphaExtensionNumberResponse: Grpc_Reflection_V1alpha_ExtensionNumberResponse) { + self = .with { + $0.baseTypeName = v1AlphaExtensionNumberResponse.baseTypeName + $0.extensionNumber = v1AlphaExtensionNumberResponse.extensionNumber + $0.unknownFields = v1AlphaExtensionNumberResponse.unknownFields + } + } +} + +extension Grpc_Reflection_V1_ServiceResponse { + init(_ v1AlphaServiceResponse: Grpc_Reflection_V1alpha_ServiceResponse) { + self = .with { + $0.name = v1AlphaServiceResponse.name + $0.unknownFields = v1AlphaServiceResponse.unknownFields + } + } +} + +extension Grpc_Reflection_V1_ListServiceResponse { + init(_ v1AlphaListServicesResponse: Grpc_Reflection_V1alpha_ListServiceResponse) { + self = .with { + $0.service = v1AlphaListServicesResponse.service.map { + Grpc_Reflection_V1_ServiceResponse($0) + } + $0.unknownFields = v1AlphaListServicesResponse.unknownFields + } + } +} + +extension Grpc_Reflection_V1_ErrorResponse { + init(_ v1AlphaErrorResponse: Grpc_Reflection_V1alpha_ErrorResponse) { + self = .with { + $0.errorCode = v1AlphaErrorResponse.errorCode + $0.errorMessage = v1AlphaErrorResponse.errorMessage + $0.unknownFields = v1AlphaErrorResponse.unknownFields + } + } +} + +extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse? { + init(_ v1AlphaResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse) { + guard let messageRequest = v1AlphaResponse.messageResponse else { + self = nil + return + } + switch messageRequest { + case .fileDescriptorResponse(let v1AlphaFileDescriptorResponse): + self = .fileDescriptorResponse( + Grpc_Reflection_V1_FileDescriptorResponse( + v1AlphaFileDescriptorResponse + ) + ) + case .allExtensionNumbersResponse(let v1AlphaAllExtensionNumbersResponse): + self = .allExtensionNumbersResponse( + Grpc_Reflection_V1_ExtensionNumberResponse( + v1AlphaAllExtensionNumbersResponse + ) + ) + case .listServicesResponse(let v1AlphaListServicesResponse): + self = .listServicesResponse( + Grpc_Reflection_V1_ListServiceResponse( + v1AlphaListServicesResponse + ) + ) + case .errorResponse(let v1AlphaErrorResponse): + self = .errorResponse( + Grpc_Reflection_V1_ErrorResponse(v1AlphaErrorResponse) + ) + } + } +} + +extension Grpc_Reflection_V1_ServerReflectionResponse { + init(_ v1AlphaResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse) { + self = .with { + $0.validHost = v1AlphaResponse.validHost + $0.originalRequest = Grpc_Reflection_V1_ServerReflectionRequest( + v1AlphaResponse.originalRequest + ) + $0.messageResponse = Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse?( + v1AlphaResponse + ) + } + } +} + +extension Grpc_Reflection_V1alpha_ExtensionRequest { + init(_ v1ExtensionRequest: Grpc_Reflection_V1_ExtensionRequest) { + self = .with { + $0.containingType = v1ExtensionRequest.containingType + $0.extensionNumber = v1ExtensionRequest.extensionNumber + $0.unknownFields = v1ExtensionRequest.unknownFields + } + } +} + +extension Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest? { + init(_ v1Request: Grpc_Reflection_V1_ServerReflectionRequest) { + guard let messageRequest = v1Request.messageRequest else { + self = nil + return + } + switch messageRequest { + case .allExtensionNumbersOfType(let typeName): + self = .allExtensionNumbersOfType(typeName) + case .fileByFilename(let fileName): + self = .fileByFilename(fileName) + case .fileContainingSymbol(let symbol): + self = .fileContainingSymbol(symbol) + case .fileContainingExtension(let v1ExtensionRequest): + self = .fileContainingExtension( + Grpc_Reflection_V1alpha_ExtensionRequest(v1ExtensionRequest) + ) + case .listServices(let parameter): + self = .listServices(parameter) + } + } +} + +extension Grpc_Reflection_V1alpha_ServerReflectionRequest { + init(_ v1Request: Grpc_Reflection_V1_ServerReflectionRequest) { + self = .with { + $0.host = v1Request.host + $0.messageRequest = Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest?( + v1Request + ) + } + } +} From 7ea3260e29449424e9aa84bff92d4fe9921d879f Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:03:51 +0000 Subject: [PATCH 166/580] Created README for the Reflection Service (#1694) Motivation: To help users set up a Server that is implementing the Reflection Service. Modifications: Added a step by step tutorial on how to set up the example server and run GRPCurl in order to test the Reflection Service. Result: Users will get information on how to set up the Reflection Service for a Server. --- Makefile | 24 +++ Package.swift | 19 ++ .../Generated/echo.grpc.reflection.txt | 1 + .../Generated/helloworld.grpc.reflection.txt | 1 + .../ReflectionService/GreeterProvider.swift | 1 + .../ReflectionService/ReflectionServer.swift | 57 ++++++ .../ReflectionServiceTutorial.md | 189 ++++++++++++++++++ .../Server/ReflectionService.swift | 25 ++- 8 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt create mode 100644 Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt create mode 120000 Sources/Examples/ReflectionService/GreeterProvider.swift create mode 100644 Sources/Examples/ReflectionService/ReflectionServer.swift create mode 100644 Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md diff --git a/Makefile b/Makefile index 210795dea..381fe0ae1 100644 --- a/Makefile +++ b/Makefile @@ -196,6 +196,30 @@ ${TEST_REFLECTION_V1ALPHA_PB}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_SWIFT} .PHONY: generate-reflection-test-clients: ${TEST_REFLECTION_V1_PB} ${TEST_REFLECTION_V1_GRPC} ${TEST_REFLECTION_V1ALPHA_PB} ${TEST_REFLECTION_V1ALPHA_GRPC} +HELLOWORLD_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt + +${HELLOWORLD_SERIALIZED_PROTO_GRPC}: ${HELLOWORLD_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ + --grpc-swift_out=$(dir ${HELLOWORLD_SERIALIZED_PROTO_GRPC}) + +.PHONY: +generate-helloworld-reflection-data: ${HELLOWORLD_SERIALIZED_PROTO_GRPC} + +ECHO_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt + +${ECHO_SERIALIZED_PROTO_GRPC}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ + --grpc-swift_out=$(dir ${ECHO_SERIALIZED_PROTO_GRPC}) + +.PHONY: +generate-echo-reflection-data: ${ECHO_SERIALIZED_PROTO_GRPC} + ### Testing #################################################################### # Normal test suite. diff --git a/Package.swift b/Package.swift index b8732828a..77a95d6fa 100644 --- a/Package.swift +++ b/Package.swift @@ -459,6 +459,24 @@ extension Target { "v1Alpha/reflection-v1alpha.proto" ] ) + + static let reflectionServer: Target = .executableTarget( + name: "ReflectionServer", + dependencies: [ + .grpc, + .reflectionService, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + .echoModel, + .echoImplementation + ], + path: "Sources/Examples/ReflectionService", + resources: [ + .copy("Generated") + ] + ) } // MARK: - Products @@ -530,6 +548,7 @@ let package = Package( .routeGuideClient, .routeGuideServer, .packetCapture, + .reflectionServer, // v2 .grpcCore, diff --git a/Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt b/Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt new file mode 100644 index 000000000..af26ef4a7 --- /dev/null +++ b/Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt @@ -0,0 +1 @@ +CgplY2hvLnByb3RvEgRlY2hvIiEKC0VjaG9SZXF1ZXN0EhIKBHRleHQYASABKAlSBHRleHQiIgoMRWNob1Jlc3BvbnNlEhIKBHRleHQYASABKAlSBHRleHQy2AEKBEVjaG8SLgoDR2V0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgASMwoGRXhwYW5kEhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAwARI0CgdDb2xsZWN0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAoARI1CgZVcGRhdGUSES5lY2hvLkVjaG9SZXF1ZXN0GhIuZWNoby5FY2hvUmVzcG9uc2UiACgBMAFK/QoKBhIEDgAoAQrCBAoBDBIDDgASMrcEIENvcHlyaWdodCAoYykgMjAxNSwgR29vZ2xlIEluYy4KCiBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCggKAQISAxAADQoKCgIGABIEEgAeAQoKCgMGAAESAxIIDAo4CgQGAAIAEgMUAjAaKyBJbW1lZGlhdGVseSByZXR1cm5zIGFuIGVjaG8gb2YgYSByZXF1ZXN0LgoKDAoFBgACAAESAxQGCQoMCgUGAAIAAhIDFAoVCgwKBQYAAgADEgMUICwKWQoEBgACARIDFwI6GkwgU3BsaXRzIGEgcmVxdWVzdCBpbnRvIHdvcmRzIGFuZCByZXR1cm5zIGVhY2ggd29yZCBpbiBhIHN0cmVhbSBvZiBtZXNzYWdlcy4KCgwKBQYAAgEBEgMXBgwKDAoFBgACAQISAxcNGAoMCgUGAAIBBhIDFyMpCgwKBQYAAgEDEgMXKjYKYgoEBgACAhIDGgI7GlUgQ29sbGVjdHMgYSBzdHJlYW0gb2YgbWVzc2FnZXMgYW5kIHJldHVybnMgdGhlbSBjb25jYXRlbmF0ZWQgd2hlbiB0aGUgY2FsbGVyIGNsb3Nlcy4KCgwKBQYAAgIBEgMaBg0KDAoFBgACAgUSAxoOFAoMCgUGAAICAhIDGhUgCgwKBQYAAgIDEgMaKzcKTQoEBgACAxIDHQJBGkAgU3RyZWFtcyBiYWNrIG1lc3NhZ2VzIGFzIHRoZXkgYXJlIHJlY2VpdmVkIGluIGFuIGlucHV0IHN0cmVhbS4KCgwKBQYAAgMBEgMdBgwKDAoFBgACAwUSAx0NEwoMCgUGAAIDAhIDHRQfCgwKBQYAAgMGEgMdKjAKDAoFBgACAwMSAx0xPQoKCgIEABIEIAAjAQoKCgMEAAESAyAIEwoyCgQEAAIAEgMiAhIaJSBUaGUgdGV4dCBvZiBhIG1lc3NhZ2UgdG8gYmUgZWNob2VkLgoKDAoFBAACAAUSAyICCAoMCgUEAAIAARIDIgkNCgwKBQQAAgADEgMiEBEKCgoCBAESBCUAKAEKCgoDBAEBEgMlCBQKLAoEBAECABIDJwISGh8gVGhlIHRleHQgb2YgYW4gZWNobyByZXNwb25zZS4KCgwKBQQBAgAFEgMnAggKDAoFBAECAAESAycJDQoMCgUEAQIAAxIDJxARYgZwcm90bzM= \ No newline at end of file diff --git a/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt b/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt new file mode 100644 index 000000000..39da0fc07 --- /dev/null +++ b/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt @@ -0,0 +1 @@ +ChBoZWxsb3dvcmxkLnByb3RvEgpoZWxsb3dvcmxkIiIKDEhlbGxvUmVxdWVzdBISCgRuYW1lGAEgASgJUgRuYW1lIiYKCkhlbGxvUmVwbHkSGAoHbWVzc2FnZRgBIAEoCVIHbWVzc2FnZTJJCgdHcmVldGVyEj4KCFNheUhlbGxvEhguaGVsbG93b3JsZC5IZWxsb1JlcXVlc3QaFi5oZWxsb3dvcmxkLkhlbGxvUmVwbHkiAEI2Chtpby5ncnBjLmV4YW1wbGVzLmhlbGxvd29ybGRCD0hlbGxvV29ybGRQcm90b1ABogIDSExXSrMICgYSBA4AJQEKvwQKAQwSAw4AEjK0BCBDb3B5cmlnaHQgMjAxNSBnUlBDIGF1dGhvcnMuCgogTGljZW5zZWQgdW5kZXIgdGhlIEFwYWNoZSBMaWNlbnNlLCBWZXJzaW9uIDIuMCAodGhlICJMaWNlbnNlIik7CiB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiBZb3UgbWF5IG9idGFpbiBhIGNvcHkgb2YgdGhlIExpY2Vuc2UgYXQKCiAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzL0xJQ0VOU0UtMi4wCgogVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogZGlzdHJpYnV0ZWQgdW5kZXIgdGhlIExpY2Vuc2UgaXMgZGlzdHJpYnV0ZWQgb24gYW4gIkFTIElTIiBCQVNJUywKIFdJVEhPVVQgV0FSUkFOVElFUyBPUiBDT05ESVRJT05TIE9GIEFOWSBLSU5ELCBlaXRoZXIgZXhwcmVzcyBvciBpbXBsaWVkLgogU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogbGltaXRhdGlvbnMgdW5kZXIgdGhlIExpY2Vuc2UuCgoICgEIEgMQACIKCQoCCAoSAxAAIgoICgEIEgMRADQKCQoCCAESAxEANAoICgEIEgMSADAKCQoCCAgSAxIAMAoICgEIEgMTACEKCQoCCCQSAxMAIQoICgECEgMVABMKLgoCBgASBBgAGwEaIiBUaGUgZ3JlZXRpbmcgc2VydmljZSBkZWZpbml0aW9uLgoKCgoDBgABEgMYCA8KIAoEBgACABIDGgI1GhMgU2VuZHMgYSBncmVldGluZy4KCgwKBQYAAgABEgMaBg4KDAoFBgACAAISAxoQHAoMCgUGAAIAAxIDGicxCj0KAgQAEgQeACABGjEgVGhlIHJlcXVlc3QgbWVzc2FnZSBjb250YWluaW5nIHRoZSB1c2VyJ3MgbmFtZS4KCgoKAwQAARIDHggUCgsKBAQAAgASAx8CEgoMCgUEAAIABRIDHwIICgwKBQQAAgABEgMfCQ0KDAoFBAACAAMSAx8QEQo8CgIEARIEIwAlARowIFRoZSByZXNwb25zZSBtZXNzYWdlIGNvbnRhaW5pbmcgdGhlIGdyZWV0aW5ncy4KCgoKAwQBARIDIwgSCgsKBAQBAgASAyQCFQoMCgUEAQIABRIDJAIICgwKBQQBAgABEgMkCRAKDAoFBAECAAMSAyQTFGIGcHJvdG8z \ No newline at end of file diff --git a/Sources/Examples/ReflectionService/GreeterProvider.swift b/Sources/Examples/ReflectionService/GreeterProvider.swift new file mode 120000 index 000000000..6cd24dda7 --- /dev/null +++ b/Sources/Examples/ReflectionService/GreeterProvider.swift @@ -0,0 +1 @@ +../HelloWorld/Server/GreeterProvider.swift \ No newline at end of file diff --git a/Sources/Examples/ReflectionService/ReflectionServer.swift b/Sources/Examples/ReflectionService/ReflectionServer.swift new file mode 100644 index 000000000..ddad1f32d --- /dev/null +++ b/Sources/Examples/ReflectionService/ReflectionServer.swift @@ -0,0 +1,57 @@ +/* + * Copyright 2023, 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 ArgumentParser +import EchoImplementation +import EchoModel +import Foundation +import GRPC +import GRPCReflectionService +import NIOPosix +import SwiftProtobuf + +@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) +@main +struct ReflectionServer: AsyncParsableCommand { + func run() async throws { + // Getting the URLs of the files containing the reflection data. + guard + let greeterURL = Bundle.module.url( + forResource: "helloworld", + withExtension: "grpc.reflection.txt" + ), + let echoURL = Bundle.module.url(forResource: "echo", withExtension: "grpc.reflection.txt") + else { + print("The resource could not be loaded.") + throw ExitCode.failure + } + + let reflectionService = try ReflectionService( + reflectionDataFileURLs: [greeterURL, echoURL], + version: .v1 + ) + + // Start the server and print its address once it has started. + let server = try await Server.insecure(group: MultiThreadedEventLoopGroup.singleton) + .withServiceProviders([reflectionService, GreeterProvider(), EchoProvider()]) + .bind(host: "localhost", port: 1234) + .get() + + print("server started on port \(server.channel.localAddress!.port!)") + // Wait on the server's `onClose` future to stop the program from exiting. + try await server.onClose.get() + } +} diff --git a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md new file mode 100644 index 000000000..98b03f475 --- /dev/null +++ b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md @@ -0,0 +1,189 @@ +# Reflection service + +This tutorial goes through the steps of adding Reflection service to a +server, running it and testing it using gRPCurl. + + The server used in this example is implemented at + [Sources/Examples/ReflectionService/ReflectionServer.swift][reflection-server] + and it supports the "Greeter", "Echo", and "Reflection" services. + + +## Overview + +The Reflection service provides information about the public RPCs served by a server. +It is specific to services defined using the Protocol Buffers IDL. +By calling the Reflection service, clients can construct and send requests to services +without needing to generate code and types for them. + +You can also use CLI clients such as [gRPCurl][grpcurl-setup] and the [gRPC command line tool][grpc-cli] to: +- list services, +- describe services and their methods, +- describe symbols, +- describe extensions, +- construct and invoke RPCs. + +gRPC Swift supports both [v1][v1] and [v1alpha][v1alpha] of the reflection service. + +## Adding the Reflection service to a server + +You can use the Reflection service by adding it as a provider when constructing your server. + +To initialise the Reflection service we will use +``GRPCReflectionService/ReflectionService/init(reflectionDataFileURLs:version:)``. +It receives the URLs of the files containing the reflection data of the proto files +describing the services of the server and the version of the reflection service. + +### Generating the reflection data + +The server from this example uses the `GreeterProvider` and the `EchoProvider`, +besides the `ReflectionService`. + +The associated proto files are located at `Sources/Examples/HelloWorld/Model/helloworld.proto`, and +`Sources/Examples/Echo/Model/echo.proto` respectively. + +In order to generate the reflection data for the `helloworld.proto`, you can run the following command: + +```sh +$ protoc Sources/Examples/HelloWorld/Model/helloworld.proto \ + --proto_path=Sources/Examples/HelloWorld/Model \ + --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ + --grpc-swift_out=Sources/Examples/ReflectionService/Generated +``` + +Let's break the command down: +- The first argument passed to `protoc` is the path + to the `.proto` file to generate reflection data + for: [`Sources/Examples/HelloWorld/Model/helloworld.proto`][helloworld-proto]. +- The `proto_path` flag is the path to search for imports: `Sources/Examples/HelloWorld/Model`. +- The 'grpc-swift_opt' flag allows us to list options for the Swift generator. + To generate only the reflection data set: `Client=false,Server=false,ReflectionData=true`. +- The `grpc-swift_out` flag is used to set the path of the directory + where the generated file will be located: `Sources/Examples/ReflectionService/Generated`. + +This command assumes that the `protoc-gen-grpc-swift` plugin is in your `$PATH` environment variable. +You can learn how to get the plugin from this section of the `grpc-swift` README: +https://github.com/grpc/grpc-swift#getting-the-protoc-plugins. + +The command for generating the reflection data for the `Echo` service is similar. + +You can use Swift Package Manager [resources][swiftpm-resources] to add the generated reflection data to your target. +In our example the reflection data is written into the "Generated" directory within the target +so we include the `.copy("Generated")` rule in our target's resource list. + +### Instantiating the Reflection service + +To instantiate the `ReflectionService` you need to pass the URLs of the files containing +the generated reflection data and the version to use, in our case `.v1`. + +Depending on the version of [gRPCurl][grpcurl] you are using you might need to use the `.v1alpha` instead. +Beginning with [gRPCurl v1.8.8][grpcurl-v188] it uses the [v1][v1] reflection. Earlier versions use [v1alpha][v1alpha] +reflection. + +```swift +// Getting the URLs of the files containing the reflection data. +guard + let greeterURL = Bundle.module.url( + forResource: "helloworld", + withExtension: "grpc.reflection.txt" + ), + let echoURL = Bundle.module.url(forResource: "echo", withExtension: "grpc.reflection.txt") +else { + print("The resource could not be loaded.") + throw ExitCode.failure +} +let reflectionService = try ReflectionService( + reflectionDataFileURLs: [greeterURL, echoURL], + version: .v1 +) +``` + +### Running the server + +In our example the server isn't configured with TLS and listens on localhost port 1234. +The following code configures and starts the server: + +```swift +let server = try await Server.insecure(group: group) + .withServiceProviders([reflectionService, GreeterProvider(), EchoProvider()]) + .bind(host: "localhost", port: self.port) + .get() + +``` + +To run the server, from the root of the package run: + +```sh +$ swift run ReflectionServer +``` + +## Calling the Reflection service with gRPCurl + +Please follow the instructions from the [gRPCurl README][grpcurl-setup] to set up gRPCurl. + +From a different terminal than the one used for running the server, we will call gRPCurl commands, +following the format: `grpcurl [flags] [address] [list|describe] [symbol]`. + +We use the `-plaintext` flag, because the server isn't configured with TLS, and +the address is set to `localhost:1234`. + + +To see the available services use `list`: + +```sh +$ grpcurl -plaintext localhost:1234 list +echo.Echo +helloworld.Greeter +``` + +To see what methods are available for a service: + +```sh +$ grpcurl -plaintext localhost:1234 list echo.Echo +echo.Echo.Collect +echo.Echo.Expand +echo.Echo.Get +echo.Echo.Update +``` + +You can also get descriptions of objects like services, methods, and messages. The following +command fetches a description of the Echo service: + +```sh +$ grpcurl -plaintext localhost:1234 describe echo.Echo +echo.Echo is a service: +service Echo { + // Collects a stream of messages and returns them concatenated when the caller closes. + rpc Collect ( stream .echo.EchoRequest ) returns ( .echo.EchoResponse ); + // Splits a request into words and returns each word in a stream of messages. + rpc Expand ( .echo.EchoRequest ) returns ( stream .echo.EchoResponse ); + // Immediately returns an echo of a request. + rpc Get ( .echo.EchoRequest ) returns ( .echo.EchoResponse ); + // Streams back messages as they are received in an input stream. + rpc Update ( stream .echo.EchoRequest ) returns ( stream .echo.EchoResponse ); +} +``` + +You can send requests to the services with gRPCurl: + +```sh +$ grpcurl -d '{ "text": "test" }' -plaintext localhost:1234 echo.Echo.Get +{ + "text": "Swift echo get: test" +} +``` + +Note that when specifying a service, a method or a symbol, we have to use the fully qualified names: +- service: \.\ +- method: \.\.\ +- type: \.\ + +[grpcurl-setup]: https://github.com/fullstorydev/grpcurl#grpcurl +[grpcurl]: https://github.com/fullstorydev/grpcurl +[grpc-cli]: https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md +[v1]: ../v1/reflection-v1.proto +[v1alpha]: ../v1Alpha/reflection-v1alpha.proto +[reflection-server]: ../../Examples/ReflectionService/ReflectionServer.swift +[helloworld-proto]: ../../Examples/HelloWorld/Model/helloworld.proto +[echo-proto]: ../../Examples/Echo/Model/echo.proto +[grpcurl-v188]: https://github.com/fullstorydev/grpcurl/releases/tag/v1.8.8 +[swiftpm-resources]: https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageDescription.md#resource diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 39d6ffdb3..b452d59b4 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -32,6 +32,29 @@ public final class ReflectionService: CallHandlerProvider, Sendable { } } + /// Creates a `ReflectionService` by loading serialized reflection data created by `protoc-gen-grpc-swift`. + /// + /// You can generate serialized reflection data using the `protoc-gen-grpc-swift` plugin for `protoc` by + /// setting the `ReflectionData` option to `True`. + /// + /// - Parameter fileURLs: The URLs of the files containing serialized reflection data. + /// - Parameter version: The version of the reflection service to create. + /// + /// - Throws: When a file can't be read from disk or parsed. + public convenience init(reflectionDataFileURLs fileURLs: [URL], version: Version) throws { + let filePaths: [String] + #if os(Linux) + filePaths = fileURLs.map { $0.path } + #else + if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { + filePaths = fileURLs.map { $0.path() } + } else { + filePaths = fileURLs.map { $0.path } + } + #endif + try self.init(reflectionDataFilePaths: filePaths, version: version) + } + /// Creates a `ReflectionService` by loading serialized reflection data created by `protoc-gen-grpc-swift`. /// /// You can generate serialized reflection data using the `protoc-gen-grpc-swift` plugin for `protoc` by @@ -42,7 +65,7 @@ public final class ReflectionService: CallHandlerProvider, Sendable { /// - Parameter version: The version of the reflection service to create. /// /// - Throws: When a file can't be read from disk or parsed. - public init(serializedFileDescriptorProtoFilePaths filePaths: [String], version: Version) throws { + public init(reflectionDataFilePaths filePaths: [String], version: Version) throws { let fileDescriptorProtos = try ReflectionService.readSerializedFileDescriptorProtos( atPaths: filePaths ) From a31547e178385c35397b9fb03fc6b56ff3ddbdde Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 21 Nov 2023 16:18:39 +0000 Subject: [PATCH 167/580] Add in-process client transport (#1713) Motivation: We want to have a basic in-process transport implementation, to be used for example for testing purposes. Modification: - Added a new `InProcessClientTransport`. - To allow for multiple configurations to be used for each method, a new `ClientRPCExecutionConfigurationCollection` was also added. - Finally, `ClientTransport/openStream` was replaced by `ClientTransport/withStream`. Result: We now have an in-process implementation of `ClientTransport`. --- ...tRPCExecutionConfigurationCollection.swift | 76 ++++ .../ClientRPCExecutor+OneShotExecutor.swift | 99 +++--- .../ClientRPCExecutor+RetryExecutor.swift | 287 +++++++-------- .../Client/Internal/ClientRPCExecutor.swift | 8 +- .../Internal/ClientStreamExecutor.swift | 19 +- .../GRPCCore/Streaming/RPCAsyncSequence.swift | 3 +- .../GRPCCore/Transport/ClientTransport.swift | 25 +- .../Transport/InProcessClientTransport.swift | 335 ++++++++++++++++++ .../Transport/InProcessServerTransport.swift | 13 +- ...xecutionConfigurationCollectionTests.swift | 94 +++++ ...ientRPCExecutorTestHarness+Transport.swift | 155 +------- .../ClientRPCExecutorTestHarness.swift | 8 +- .../Internal/ClientRPCExecutorTests.swift | 60 ++-- .../Transport/AnyTransport.swift | 30 +- .../Transport/StreamCountingTransport.swift | 17 +- .../Transport/ThrowingTransport.swift | 7 +- .../InProcessClientTransportTests.swift | 263 ++++++++++++++ ...ft => InProcessServerTransportTests.swift} | 2 +- 18 files changed, 1085 insertions(+), 416 deletions(-) create mode 100644 Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift create mode 100644 Sources/GRPCCore/Transport/InProcessClientTransport.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift create mode 100644 Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift rename Tests/GRPCCoreTests/Transport/{InProcessServerTransportTest.swift => InProcessServerTransportTests.swift} (98%) diff --git a/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift b/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift new file mode 100644 index 000000000..9207fd757 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift @@ -0,0 +1,76 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + +/// A collection of ``ClientRPCExecutionConfiguration``s, mapped to specific methods or services. +/// +/// When creating a new instance, you must provide a default configuration to be used when getting +/// a configuration for a method that has not been given a specific override. +/// Use ``setDefaultConfiguration(_:forService:)`` to set a specific override for a whole +/// service. +/// +/// Use the subscript to get and set configurations for methods. +public struct ClientRPCExecutionConfigurationCollection: Sendable, Hashable { + private var elements: [MethodDescriptor: ClientRPCExecutionConfiguration] + private let defaultConfiguration: ClientRPCExecutionConfiguration + + public init( + defaultConfiguration: ClientRPCExecutionConfiguration = ClientRPCExecutionConfiguration( + executionPolicy: nil, + timeout: nil + ) + ) { + self.elements = [:] + self.defaultConfiguration = defaultConfiguration + } + + public subscript(_ descriptor: MethodDescriptor) -> ClientRPCExecutionConfiguration { + get { + if let methodLevelOverride = self.elements[descriptor] { + return methodLevelOverride + } + var serviceLevelDescriptor = descriptor + serviceLevelDescriptor.method = "" + return self.elements[serviceLevelDescriptor, default: self.defaultConfiguration] + } + + set { + precondition( + !descriptor.service.isEmpty, + "Method descriptor's service cannot be empty." + ) + + self.elements[descriptor] = newValue + } + } + + /// Set a default configuration for a service. + /// + /// If getting a configuration for a method that's part of a service, and the method itself doesn't have an + /// override, then this configuration will be used instead of the default configuration passed when creating + /// this instance of ``ClientRPCExecutionConfigurationCollection``. + /// + /// - Parameters: + /// - configuration: The default configuration for the service. + /// - service: The name of the service for which this override applies. + public mutating func setDefaultConfiguration( + _ configuration: ClientRPCExecutionConfiguration, + forService service: String + ) { + self[MethodDescriptor(service: service, method: "")] = configuration + } +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index 1800f9ea2..291f52da9 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -69,63 +69,70 @@ extension ClientRPCExecutor.OneShotExecutor { of: _OneShotExecutorTask.self, returning: Result.self ) { group in - if let timeout = self.timeout { - group.addTask { - let result = await Result { - try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + do { + return try await self.transport.withStream(descriptor: method) { stream in + if let timeout = self.timeout { + group.addTask { + let result = await Result { + try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + } + return .timedOut(result) + } } - return .timedOut(result) - } - } - let streamExecutor = ClientStreamExecutor(transport: self.transport) - group.addTask { - await streamExecutor.run() - return .streamExecutorCompleted - } + let streamExecutor = ClientStreamExecutor(transport: self.transport) + group.addTask { + await streamExecutor.run() + return .streamExecutorCompleted + } - group.addTask { - let response = await ClientRPCExecutor.unsafeExecute( - request: request, - method: method, - attempt: 1, - serializer: self.serializer, - deserializer: self.deserializer, - interceptors: self.interceptors, - streamProcessor: streamExecutor - ) + group.addTask { + let response = await ClientRPCExecutor.unsafeExecute( + request: request, + method: method, + attempt: 1, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + streamProcessor: streamExecutor, + stream: stream + ) - let result = await Result { - try await responseHandler(response) - } + let result = await Result { + try await responseHandler(response) + } - return .responseHandled(result) - } + return .responseHandled(result) + } - while let result = await group.next() { - switch result { - case .streamExecutorCompleted: - // Stream finished; wait for the response to be handled. - () + while let result = await group.next() { + switch result { + case .streamExecutorCompleted: + // Stream finished; wait for the response to be handled. + () - case .timedOut(.success): - // The deadline passed; cancel the ongoing work group. - group.cancelAll() + case .timedOut(.success): + // The deadline passed; cancel the ongoing work group. + group.cancelAll() - case .timedOut(.failure): - // The deadline task failed (because the task was cancelled). Wait for the response - // to be handled. - () + case .timedOut(.failure): + // The deadline task failed (because the task was cancelled). Wait for the response + // to be handled. + () - case .responseHandled(let result): - // Response handled: cancel any other remaining tasks. - group.cancelAll() - return result + case .responseHandled(let result): + // Response handled: cancel any other remaining tasks. + group.cancelAll() + return result + } + } + + // Unreachable: exactly one task returns `responseHandled` and we return when it completes. + fatalError("Internal inconsistency") } + } catch { + return .failure(error) } - - // Unreachable: exactly one task returns `responseHandled` and we return when it completes. - fatalError("Internal inconsistency") } return try result.get() diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 083f22b1e..619352723 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -118,169 +118,180 @@ extension ClientRPCExecutor.RetryExecutor { var delayIterator = delaySequence.makeIterator() for attempt in 1 ... self.policy.maximumAttempts { - group.addTask { - await withTaskGroup( - of: _RetryExecutorSubTask.self, - returning: _RetryExecutorTask.self - ) { thisAttemptGroup in - let streamExecutor = ClientStreamExecutor(transport: self.transport) - thisAttemptGroup.addTask { - await streamExecutor.run() - return .streamProcessed - } + do { + let attemptResult = try await self.transport.withStream(descriptor: method) { stream in + group.addTask { + await withTaskGroup( + of: _RetryExecutorSubTask.self, + returning: _RetryExecutorTask.self + ) { thisAttemptGroup in + let streamExecutor = ClientStreamExecutor(transport: self.transport) + thisAttemptGroup.addTask { + await streamExecutor.run() + return .streamProcessed + } - thisAttemptGroup.addTask { - let response = await ClientRPCExecutor.unsafeExecute( - request: ClientRequest.Stream(metadata: request.metadata) { - try await $0.write(contentsOf: retry.stream) - }, - method: method, - attempt: attempt, - serializer: self.serializer, - deserializer: self.deserializer, - interceptors: self.interceptors, - streamProcessor: streamExecutor - ) + thisAttemptGroup.addTask { + let response = await ClientRPCExecutor.unsafeExecute( + request: ClientRequest.Stream(metadata: request.metadata) { + try await $0.write(contentsOf: retry.stream) + }, + method: method, + attempt: attempt, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + streamProcessor: streamExecutor, + stream: stream + ) - let shouldRetry: Bool - let retryDelayOverride: Duration? + let shouldRetry: Bool + let retryDelayOverride: Duration? - switch response.accepted { - case .success: - // Request was accepted. This counts as success to the throttle and there's no need - // to retry. - self.transport.retryThrottle.recordSuccess() - retryDelayOverride = nil - shouldRetry = false + switch response.accepted { + case .success: + // Request was accepted. This counts as success to the throttle and there's no need + // to retry. + self.transport.retryThrottle.recordSuccess() + retryDelayOverride = nil + shouldRetry = false - case .failure(let error): - // The request was rejected. Determine whether a retry should be carried out. The - // following conditions must be checked: - // - // - Whether the status code is retryable. - // - Whether more attempts are permitted by the config. - // - Whether the throttle permits another retry to be carried out. - // - Whether the server pushed back to either stop further retries or to override - // the delay before the next retry. - let code = Status.Code(error.code) - let isRetryableStatusCode = self.policy.retryableStatusCodes.contains(code) + case .failure(let error): + // The request was rejected. Determine whether a retry should be carried out. The + // following conditions must be checked: + // + // - Whether the status code is retryable. + // - Whether more attempts are permitted by the config. + // - Whether the throttle permits another retry to be carried out. + // - Whether the server pushed back to either stop further retries or to override + // the delay before the next retry. + let code = Status.Code(error.code) + let isRetryableStatusCode = self.policy.retryableStatusCodes.contains(code) - if isRetryableStatusCode { - // Counted as failure for throttling. - let throttled = self.transport.retryThrottle.recordFailure() + if isRetryableStatusCode { + // Counted as failure for throttling. + let throttled = self.transport.retryThrottle.recordFailure() - // Status code can be retried, Did the server send pushback? - switch error.metadata.retryPushback { - case .retryAfter(let delay): - // Pushback: only retry if our config permits it. - shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled - retryDelayOverride = delay - case .stopRetrying: - // Server told us to stop trying. - shouldRetry = false - retryDelayOverride = nil - case .none: - // No pushback: only retry if our config permits it. - shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled - retryDelayOverride = nil - break + // Status code can be retried, Did the server send pushback? + switch error.metadata.retryPushback { + case .retryAfter(let delay): + // Pushback: only retry if our config permits it. + shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + retryDelayOverride = delay + case .stopRetrying: + // Server told us to stop trying. + shouldRetry = false + retryDelayOverride = nil + case .none: + // No pushback: only retry if our config permits it. + shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + retryDelayOverride = nil + break + } + } else { + // Not-retryable; this is considered a success. + self.transport.retryThrottle.recordSuccess() + shouldRetry = false + retryDelayOverride = nil + } } - } else { - // Not-retryable; this is considered a success. - self.transport.retryThrottle.recordSuccess() - shouldRetry = false - retryDelayOverride = nil - } - } - if shouldRetry { - // Cancel subscribers of the broadcast sequence. This is safe as we are the only - // subscriber and maximises the chances that 'isKnownSafeForNextSubscriber' will - // return true. - // - // Note: this must only be called if we should retry, otherwise we may cancel a - // subscriber for an accepted request. - retry.stream.invalidateAllSubscriptions() + if shouldRetry { + // Cancel subscribers of the broadcast sequence. This is safe as we are the only + // subscriber and maximises the chances that 'isKnownSafeForNextSubscriber' will + // return true. + // + // Note: this must only be called if we should retry, otherwise we may cancel a + // subscriber for an accepted request. + retry.stream.invalidateAllSubscriptions() - // Only retry if we know it's safe for the next subscriber, that is, the first - // element is still in the buffer. It's safe to call this because there's only - // ever one attempt at a time and the existing subscribers have been invalidated. - if retry.stream.isKnownSafeForNextSubscriber { - return .retry(retryDelayOverride) + // Only retry if we know it's safe for the next subscriber, that is, the first + // element is still in the buffer. It's safe to call this because there's only + // ever one attempt at a time and the existing subscribers have been invalidated. + if retry.stream.isKnownSafeForNextSubscriber { + return .retry(retryDelayOverride) + } + } + + // Not retrying or not safe to retry. + let result = await Result { + // Check for cancellation; the RPC may have timed out in which case we should skip + // the response handler. + try Task.checkCancellation() + return try await responseHandler(response) + } + return .handledResponse(result) } - } - // Not retrying or not safe to retry. - let result = await Result { - // Check for cancellation; the RPC may have timed out in which case we should skip - // the response handler. - try Task.checkCancellation() - return try await responseHandler(response) - } - return .handledResponse(result) - } + while let result = await thisAttemptGroup.next() { + switch result { + case .streamProcessed: + () // Continue processing; wait for the response to be handled. - while let result = await thisAttemptGroup.next() { - switch result { - case .streamProcessed: - () // Continue processing; wait for the response to be handled. + case .retry(let delayOverride): + thisAttemptGroup.cancelAll() + return .retry(delayOverride) - case .retry(let delayOverride): - thisAttemptGroup.cancelAll() - return .retry(delayOverride) + case .handledResponse(let result): + thisAttemptGroup.cancelAll() + return .handledResponse(result) + } + } - case .handledResponse(let result): - thisAttemptGroup.cancelAll() - return .handledResponse(result) + fatalError("Internal inconsistency") } } - fatalError("Internal inconsistency") - } - } + loop: while let next = await group.next() { + switch next { + case .handledResponse(let result): + // A usable response; cancel the remaining work and return the result. + group.cancelAll() + return Optional.some(result) - loop: while let next = await group.next() { - switch next { - case .handledResponse(let result): - // A usable response; cancel the remaining work and return the result. - group.cancelAll() - return result + case .retry(let delayOverride): + // The attempt failed, wait a bit and then retry. The server might have overridden the + // delay via pushback so preferentially use that value. + // + // Any error will come from cancellation: if it happens while we're sleeping we can + // just loop around, the next attempt will be cancelled immediately and we will return + // its response to the client. + if let delayOverride = delayOverride { + // If the delay is overridden with server pushback then reset the iterator for the + // next retry. + delayIterator = delaySequence.makeIterator() + try? await Task.sleep(until: .now.advanced(by: delayOverride), clock: .continuous) + } else { + // The delay iterator never terminates. + try? await Task.sleep( + until: .now.advanced(by: delayIterator.next()!), + clock: .continuous + ) + } - case .retry(let delayOverride): - // The attempt failed, wait a bit and then retry. The server might have overridden the - // delay via pushback so preferentially use that value. - // - // Any error will come from cancellation: if it happens while we're sleeping we can - // just loop around, the next attempt will be cancelled immediately and we will return - // its response to the client. - if let delayOverride = delayOverride { - // If the delay is overridden with server pushback then reset the iterator for the - // next retry. - delayIterator = delaySequence.makeIterator() - try? await Task.sleep(until: .now.advanced(by: delayOverride), clock: .continuous) - } else { - // The delay iterator never terminates. - try? await Task.sleep( - until: .now.advanced(by: delayIterator.next()!), - clock: .continuous - ) - } + break loop // from the while loop so another attempt can be started. - break loop // from the while loop so another attempt can be started. + case .timedOut(.success), .outboundFinished(.failure): + // Timeout task fired successfully or failed to process the outbound stream. Cancel and + // wait for a usable response (which is likely to be an error). + group.cancelAll() - case .timedOut(.success), .outboundFinished(.failure): - // Timeout task fired successfully or failed to process the outbound stream. Cancel and - // wait for a usable response (which is likely to be an error). - group.cancelAll() + case .timedOut(.failure), .outboundFinished(.success): + // Timeout task failed which means it was cancelled (so no need to cancel again) or the + // outbound stream was successfully processed (so don't need to do anything). + () + } + } + return nil + } - case .timedOut(.failure), .outboundFinished(.success): - // Timeout task failed which means it was cancelled (so no need to cancel again) or the - // outbound stream was successfully processed (so don't need to do anything). - () + if let attemptResult { + return attemptResult } + } catch { + return .failure(error) } } - fatalError("Internal inconsistency") } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 6896dae59..565df7ec3 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -99,14 +99,15 @@ extension ClientRPCExecutor { /// - streamProcessor: A processor which executes the serialized request. /// - Returns: The deserialized response. @inlinable - static func unsafeExecute( + static func unsafeExecute( request: ClientRequest.Stream, method: MethodDescriptor, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, interceptors: [any ClientInterceptor], - streamProcessor: ClientStreamExecutor + streamProcessor: ClientStreamExecutor, + stream: RPCStream ) async -> ClientResponse.Stream { let context = ClientInterceptorContext(descriptor: method) @@ -125,7 +126,8 @@ extension ClientRPCExecutor { request: ClientRequest.Stream<[UInt8]>(metadata: metadata) { writer in try await request.producer(.serializing(into: writer, with: serializer)) }, - method: context.descriptor + method: context.descriptor, + stream: stream ) // Attach the number of previous attempts, it can be useful information for callers. diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 32f6389c5..285f8abeb 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -80,28 +80,13 @@ internal struct ClientStreamExecutor { @inlinable func execute( request: ClientRequest.Stream<[UInt8]>, - method: MethodDescriptor + method: MethodDescriptor, + stream: RPCStream ) async -> ClientResponse.Stream<[UInt8]> { // Each execution method can add work to process in the 'run' method. They must not add // new work once they return. defer { self._work.continuation.finish() } - // Open a stream. Return a failed response if we can't open one. - let stream: RPCStream - - do { - stream = try await self._transport.openStream(descriptor: method) - } catch let error as RPCError { - return ClientResponse.Stream(error: error) - } catch let other { - let error = RPCError( - code: .unknown, - message: "Transport failed to create stream.", - cause: other - ) - return ClientResponse.Stream(error: error) - } - // Start processing the request. self._work.continuation.yield(.request(request, stream.outbound)) diff --git a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift index 3e397089e..6e2e89350 100644 --- a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift @@ -40,7 +40,8 @@ public struct RPCAsyncSequence: AsyncSequence, Sendable { } public mutating func next() async throws -> Element? { - return try await self.iterator.next() as? Element + let elem = try await self.iterator.next() + return elem as? Element } } } diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 968d949b6..6e7d20bcb 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -33,9 +33,9 @@ public protocol ClientTransport: Sendable { /// demand for streams by the client. /// /// Implementations of this function will typically create a long-lived task group which - /// maintains connections. The function exits when connections are no longer required by - /// the caller who signals this by calling ``close()`` to indicate that no new streams are - /// required or by cancelling the task this function runs in. + /// maintains connections. The function exits when all open streams have been closed and new connections + /// are no longer required by the caller who signals this by calling ``close()``, or by cancelling the + /// task this function runs in. /// /// - Parameter lazily: Whether the transport should establish connections lazily, that is, /// when the first stream is opened or eagerly, when this function is called. If `false` @@ -46,25 +46,30 @@ public protocol ClientTransport: Sendable { /// Signal to the transport that no new streams may be created. /// - /// Existing streams may run to completion naturally but calling ``openStream(descriptor:)`` + /// Existing streams may run to completion naturally but calling ``withStream(descriptor:_:)`` /// should result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. /// /// If you want to forcefully cancel all active streams then cancel the task /// running ``connect(lazily:)``. func close() - /// Open a stream using the transport. + /// Opens a stream using the transport, and uses it as input into a user-provided closure. + /// + /// - Important: The opened stream is closed after the closure is finished. /// /// Transport implementations should throw an ``RPCError`` with the following error codes: /// - ``RPCError/Code/failedPrecondition`` if the transport is closing or has been closed. /// - ``RPCError/Code/unavailable`` if it's temporarily not possible to create a stream and it /// may be possible after some backoff period. /// - /// - Parameter descriptor: A description of the method to open a stream for. - /// - Returns: A stream. - func openStream( - descriptor: MethodDescriptor - ) async throws -> RPCStream + /// - Parameters: + /// - descriptor: A description of the method to open a stream for. + /// - closure: A closure that takes the opened stream as parameter. + /// - Returns: Whatever value was returned from `closure`. + func withStream( + descriptor: MethodDescriptor, + _ closure: (_ stream: RPCStream) async throws -> T + ) async throws -> T /// Returns the execution configuration for a given method. /// diff --git a/Sources/GRPCCore/Transport/InProcessClientTransport.swift b/Sources/GRPCCore/Transport/InProcessClientTransport.swift new file mode 100644 index 000000000..47b1bbf73 --- /dev/null +++ b/Sources/GRPCCore/Transport/InProcessClientTransport.swift @@ -0,0 +1,335 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +/// An in-process implementation of a ``ClientTransport``. +/// +/// This is useful when you're interested in testing your application without any actual networking layers +/// involved, as the client and server will communicate directly with each other via in-process streams. +/// +/// To use this client, you'll have to provide an ``InProcessServerTransport`` upon creation, as well +/// as a ``ClientRPCExecutionConfigurationCollection``, containing a set of +/// ``ClientRPCExecutionConfiguration``s which are specific, per-method configurations for your +/// transport. +/// +/// Once you have a client, you must keep a long-running task executing ``connect(lazily:)``, which +/// will return only once all streams have been finished and ``close()`` has been called on this client; or +/// when the containing task is cancelled. +/// +/// To execute requests using this client, use ``withStream(descriptor:_:)``. If this function is +/// called before ``connect(lazily:)`` is called, then any streams will remain pending and the call will +/// block until ``connect(lazily:)`` is called or the task is cancelled. +/// +/// - SeeAlso: ``ClientTransport`` +public struct InProcessClientTransport: ClientTransport { + private enum State: Sendable { + struct UnconnectedState { + var serverTransport: InProcessServerTransport + var pendingStreams: [AsyncStream.Continuation] + + init(serverTransport: InProcessServerTransport) { + self.serverTransport = serverTransport + self.pendingStreams = [] + } + } + + struct ConnectedState { + var serverTransport: InProcessServerTransport + var nextStreamID: Int + var openStreams: + [Int: ( + RPCStream, + RPCStream, RPCWriter.Closable> + )] + var signalEndContinuation: AsyncStream.Continuation + + init( + fromUnconnected state: UnconnectedState, + signalEndContinuation: AsyncStream.Continuation + ) { + self.serverTransport = state.serverTransport + self.nextStreamID = 0 + self.openStreams = [:] + self.signalEndContinuation = signalEndContinuation + } + } + + struct ClosedState { + var openStreams: + [Int: ( + RPCStream, + RPCStream, RPCWriter.Closable> + )] + var signalEndContinuation: AsyncStream.Continuation? + + init() { + self.openStreams = [:] + self.signalEndContinuation = nil + } + + init(fromConnected state: ConnectedState) { + self.openStreams = state.openStreams + self.signalEndContinuation = state.signalEndContinuation + } + } + + case unconnected(UnconnectedState) + case connected(ConnectedState) + case closed(ClosedState) + } + + public typealias Inbound = RPCAsyncSequence + public typealias Outbound = RPCWriter.Closable + + public let retryThrottle: RetryThrottle + + private let executionConfigurations: ClientRPCExecutionConfigurationCollection + private let state: LockedValueBox + + public init( + server: InProcessServerTransport, + executionConfigurations: ClientRPCExecutionConfigurationCollection + ) { + self.retryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + self.executionConfigurations = executionConfigurations + self.state = LockedValueBox(.unconnected(.init(serverTransport: server))) + } + + /// Establish and maintain a connection to the remote destination. + /// + /// Maintains a long-lived connection, or set of connections, to a remote destination. + /// Connections may be added or removed over time as required by the implementation and the + /// demand for streams by the client. + /// + /// Implementations of this function will typically create a long-lived task group which + /// maintains connections. The function exits when all open streams have been closed and new connections + /// are no longer required by the caller who signals this by calling ``close()``, or by cancelling the + /// task this function runs in. + /// + /// - Parameter lazily: This parameter is ignored in this implementation. + public func connect(lazily: Bool) async throws { + let (stream, continuation) = AsyncStream.makeStream() + try self.state.withLockedValue { state in + switch state { + case .unconnected(let unconnectedState): + state = .connected( + .init( + fromUnconnected: unconnectedState, + signalEndContinuation: continuation + ) + ) + for pendingStream in unconnectedState.pendingStreams { + pendingStream.finish() + } + case .connected: + throw RPCError( + code: .failedPrecondition, + message: "Already connected to server." + ) + case .closed: + throw RPCError( + code: .failedPrecondition, + message: "Can't connect to server, transport is closed." + ) + } + } + + for await _ in stream { + // This for-await loop will exit (and thus `connect(lazily:)` will return) + // only when the task is cancelled, or when the stream's continuation is + // finished - whichever happens first. + // The continuation will be finished when `close()` is called and there + // are no more open streams. + } + + // If at this point there are any open streams, it's because Cancellation + // occurred and all open streams must now be closed. + let openStreams = self.state.withLockedValue { state in + switch state { + case .unconnected: + // We have transitioned to connected, and we can't transition back. + fatalError("Invalid state") + case .connected(let connectedState): + state = .closed(.init()) + return connectedState.openStreams.values + case .closed(let closedState): + return closedState.openStreams.values + } + } + + for (clientStream, serverStream) in openStreams { + clientStream.outbound.finish(throwing: CancellationError()) + serverStream.outbound.finish(throwing: CancellationError()) + } + } + + /// Signal to the transport that no new streams may be created. + /// + /// Existing streams may run to completion naturally but calling ``withStream(descriptor:_:)`` + /// will result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. + /// + /// If you want to forcefully cancel all active streams then cancel the task running ``connect(lazily:)``. + public func close() { + let maybeContinuation: AsyncStream.Continuation? = self.state.withLockedValue { state in + switch state { + case .unconnected: + state = .closed(.init()) + return nil + case .connected(let connectedState): + if connectedState.openStreams.count == 0 { + state = .closed(.init()) + return connectedState.signalEndContinuation + } else { + state = .closed(.init(fromConnected: connectedState)) + return nil + } + case .closed: + return nil + } + } + maybeContinuation?.finish() + } + + /// Opens a stream using the transport, and uses it as input into a user-provided closure. + /// + /// - Important: The opened stream is closed after the closure is finished. + /// + /// This transport implementation throws ``RPCError/Code/failedPrecondition`` if the transport + /// is closing or has been closed. + /// + /// This implementation will queue any streams (and thus block this call) if this function is called before + /// ``connect(lazily:)``, until a connection is established - at which point all streams will be + /// created. + /// + /// - Parameters: + /// - descriptor: A description of the method to open a stream for. + /// - closure: A closure that takes the opened stream as parameter. + /// - Returns: Whatever value was returned from `closure`. + public func withStream( + descriptor: MethodDescriptor, + _ closure: (RPCStream) async throws -> T + ) async throws -> T { + let request = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) + let response = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) + + let clientStream = RPCStream( + descriptor: descriptor, + inbound: response.stream, + outbound: request.writer + ) + + let serverStream = RPCStream( + descriptor: descriptor, + inbound: request.stream, + outbound: response.writer + ) + + let waitForConnectionStream: AsyncStream? = self.state.withLockedValue { state in + if case .unconnected(var unconnectedState) = state { + let (stream, continuation) = AsyncStream.makeStream() + unconnectedState.pendingStreams.append(continuation) + state = .unconnected(unconnectedState) + return stream + } + return nil + } + + if let waitForConnectionStream { + for await _ in waitForConnectionStream { + // This loop will exit either when the task is cancelled or when the + // client connects and this stream can be opened. + } + try Task.checkCancellation() + } + + let streamID = try self.state.withLockedValue { state in + switch state { + case .unconnected: + // The state cannot be unconnected because if it was, then the above + // for-await loop on `pendingStream` would have not returned. + // The only other option is for the task to have been cancelled, + // and that's why we check for cancellation right after the loop. + fatalError("Invalid state.") + + case .connected(var connectedState): + let streamID = connectedState.nextStreamID + do { + try connectedState.serverTransport.acceptStream(serverStream) + connectedState.openStreams[streamID] = (clientStream, serverStream) + connectedState.nextStreamID += 1 + state = .connected(connectedState) + } catch let acceptStreamError as RPCError { + serverStream.outbound.finish(throwing: acceptStreamError) + clientStream.outbound.finish(throwing: acceptStreamError) + throw acceptStreamError + } catch { + serverStream.outbound.finish(throwing: error) + clientStream.outbound.finish(throwing: error) + throw RPCError(code: .unknown, message: "Unknown error: \(error).") + } + return streamID + + case .closed: + let error = RPCError( + code: .failedPrecondition, + message: "The client transport is closed." + ) + serverStream.outbound.finish(throwing: error) + clientStream.outbound.finish(throwing: error) + throw error + } + } + + defer { + clientStream.outbound.finish() + + let maybeEndContinuation = self.state.withLockedValue { state in + switch state { + case .unconnected: + // The state cannot be unconnected at this point, because if we made + // it this far, it's because the transport was connected. + // Once connected, it's impossible to transition back to unconnected, + // so this is an invalid state. + fatalError("Invalid state") + case .connected(var connectedState): + connectedState.openStreams.removeValue(forKey: streamID) + state = .connected(connectedState) + case .closed(var closedState): + closedState.openStreams.removeValue(forKey: streamID) + state = .closed(closedState) + if closedState.openStreams.isEmpty { + // This was the last open stream: signal the closure of the client. + return closedState.signalEndContinuation + } + } + return nil + } + maybeEndContinuation?.finish() + } + + return try await closure(clientStream) + } + + /// Returns the execution configuration for a given method. + /// + /// - Parameter descriptor: The method to lookup configuration for. + /// - Returns: Execution configuration for the method, if it exists. + public func executionConfiguration( + forMethod descriptor: MethodDescriptor + ) -> ClientRPCExecutionConfiguration? { + self.executionConfigurations[descriptor] + } +} diff --git a/Sources/GRPCCore/Transport/InProcessServerTransport.swift b/Sources/GRPCCore/Transport/InProcessServerTransport.swift index 09409aa6f..47762e07f 100644 --- a/Sources/GRPCCore/Transport/InProcessServerTransport.swift +++ b/Sources/GRPCCore/Transport/InProcessServerTransport.swift @@ -16,7 +16,16 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) /// An in-process implementation of a ``ServerTransport``. -public struct InProcessServerTransport: ServerTransport { +/// +/// This is useful when you're interested in testing your application without any actual networking layers +/// involved, as the client and server will communicate directly with each other via in-process streams. +/// +/// To use this server, you call ``listen()`` and iterate over the returned `AsyncSequence` to get all +/// RPC requests made from clients (as ``RPCStream``s). +/// To stop listening to new requests, call ``stopListening()``. +/// +/// - SeeAlso: ``ClientTransport`` +public struct InProcessServerTransport: ServerTransport, Sendable { public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable @@ -56,6 +65,8 @@ public struct InProcessServerTransport: ServerTransport { /// /// All further calls to ``acceptStream(_:)`` will not produce any new elements on the /// ``RPCAsyncSequence`` returned by ``listen()``. + /// + /// - SeeAlso: ``ServerTransport`` public func stopListening() { self.newStreamsContinuation.finish() } diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift new file mode 100644 index 000000000..38f964652 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift @@ -0,0 +1,94 @@ +/* + * Copyright 2023, 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 + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class ClientRPCExecutionConfigurationCollectionTests: XCTestCase { + func testGetConfigurationForKnownMethod() { + let policy = HedgingPolicy( + maximumAttempts: 10, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [] + ) + let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) + var configurations = ClientRPCExecutionConfigurationCollection( + defaultConfiguration: defaultConfiguration + ) + let descriptor = MethodDescriptor(service: "test", method: "first") + let retryPolicy = RetryPolicy( + maximumAttempts: 10, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(1), + backoffMultiplier: 1.0, + retryableStatusCodes: [.unavailable] + ) + let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + configurations[descriptor] = overrideConfiguration + + XCTAssertEqual(configurations[descriptor], overrideConfiguration) + } + + func testGetConfigurationForUnknownMethodButServiceOverride() { + let policy = HedgingPolicy( + maximumAttempts: 10, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [] + ) + let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) + var configurations = ClientRPCExecutionConfigurationCollection( + defaultConfiguration: defaultConfiguration + ) + let firstDescriptor = MethodDescriptor(service: "test", method: "") + let retryPolicy = RetryPolicy( + maximumAttempts: 10, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(1), + backoffMultiplier: 1.0, + retryableStatusCodes: [.unavailable] + ) + let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + configurations[firstDescriptor] = overrideConfiguration + + let secondDescriptor = MethodDescriptor(service: "test", method: "second") + XCTAssertEqual(configurations[secondDescriptor], overrideConfiguration) + } + + func testGetConfigurationForUnknownMethodDefaultValue() { + let policy = HedgingPolicy( + maximumAttempts: 10, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [] + ) + let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) + var configurations = ClientRPCExecutionConfigurationCollection( + defaultConfiguration: defaultConfiguration + ) + let firstDescriptor = MethodDescriptor(service: "test1", method: "first") + let retryPolicy = RetryPolicy( + maximumAttempts: 10, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(1), + backoffMultiplier: 1.0, + retryableStatusCodes: [.unavailable] + ) + let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + configurations[firstDescriptor] = overrideConfiguration + + let secondDescriptor = MethodDescriptor(service: "test2", method: "second") + XCTAssertEqual(configurations[secondDescriptor], defaultConfiguration) + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index d93a34862..c787867d1 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -17,148 +17,21 @@ import Atomics @testable import GRPCCore -// TODO: replace with real in-process transport - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class TestingClientTransport: ClientTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable - - let retryThrottle: RetryThrottle - - private let state: LockedValueBox - private enum State { - case unconnected(TestingServerTransport) - case connected(TestingServerTransport) - case closed - } - - fileprivate init(server: TestingServerTransport, throttle: RetryThrottle) { - self.state = LockedValueBox(.unconnected(server)) - self.retryThrottle = throttle - } - - deinit { - self.state.withLockedValue { state in - switch state { - case .unconnected(let server), .connected(let server): - server.stopListening() - case .closed: - () - } - } - } - - func connect(lazily: Bool) async throws { - try self.state.withLockedValue { state in - switch state { - case let .unconnected(server): - state = .connected(server) - - case .connected: - () - - case .closed: - throw RPCError( - code: .failedPrecondition, - message: "Can't connect to server, transport is closed." - ) - } - } - } - - func close() { - self.state.withLockedValue { state in - switch state { - case .unconnected(let server), .connected(let server): - state = .closed - server.stopListening() - - case .closed: - () - } - } - } - - func executionConfiguration( - forMethod descriptor: MethodDescriptor - ) -> ClientRPCExecutionConfiguration? { - nil - } - - func openStream( - descriptor: MethodDescriptor - ) async throws -> RPCStream { - let request = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) - let response = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) - - let clientStream = RPCStream( - descriptor: descriptor, - inbound: response.stream, - outbound: request.writer - ) - - let serverStream = RPCStream( - descriptor: descriptor, - inbound: request.stream, - outbound: response.writer - ) - - let error: RPCError? = self.state.withLockedValue { state in - switch state { - case .connected(let transport): - transport.acceptStream(serverStream) - return nil - - case .unconnected: - return RPCError( - code: .failedPrecondition, - message: "The client transport must be connected before streams can be created." - ) - - case .closed: - return RPCError(code: .failedPrecondition, message: "The client transport is closed.") - } - } - - if let error = error { - serverStream.outbound.finish() - clientStream.outbound.finish() - throw error - } else { - return clientStream - } - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class TestingServerTransport: ServerTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable - - typealias Stream = RPCStream - private let accepted: - (stream: AsyncStream, continuation: AsyncStream.Continuation) - - init() { - self.accepted = AsyncStream.makeStream() - } - - fileprivate func acceptStream(_ stream: RPCStream) { - self.accepted.continuation.yield(stream) - } - - func listen() async throws -> RPCAsyncSequence> { - return RPCAsyncSequence(wrapping: self.accepted.stream) - } - - func stopListening() { - self.accepted.continuation.finish() - } - +extension InProcessServerTransport { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) - ) -> TestingClientTransport { - return TestingClientTransport(server: self, throttle: throttle) + ) -> InProcessClientTransport { + return InProcessClientTransport( + server: self, + executionConfigurations: .init( + defaultConfiguration: .init( + hedgingPolicy: .init( + maximumAttempts: 2, + hedgingDelay: .milliseconds(100), + nonFatalStatusCodes: [] + ) + ) + ) + ) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 7d46ff9c2..3b546d1e0 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -47,13 +47,13 @@ struct ClientRPCExecutorTestHarness { switch transport { case .inProcess: - let server = TestingServerTransport() + let server = InProcessServerTransport() let client = server.spawnClientTransport() self.serverTransport = StreamCountingServerTransport(wrapping: server) self.clientTransport = StreamCountingClientTransport(wrapping: client) case .throwsOnStreamCreation(let code): - let server = TestingServerTransport() // Will never be called. + let server = InProcessServerTransport() // Will never be called. let client = ThrowOnStreamCreationTransport(code: code) self.serverTransport = StreamCountingServerTransport(wrapping: server) self.clientTransport = StreamCountingClientTransport(wrapping: client) @@ -137,7 +137,9 @@ struct ClientRPCExecutorTestHarness { } } - try await self.clientTransport.connect(lazily: false) + group.addTask { + try await self.clientTransport.connect(lazily: false) + } let executionConfiguration: ClientRPCExecutionConfiguration if let configuration = configuration { diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 505745c86..65da3de7a 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -150,12 +150,12 @@ final class ClientRPCExecutorTests: XCTestCase { server: .failTest ) - try await tester.unary( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) - ) { response in - XCTAssertThrowsRPCError(try response.message) { - XCTAssertEqual($0.code, .aborted) - } + await XCTAssertThrowsRPCErrorAsync { + try await tester.unary( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { _ in } + } errorHandler: { error in + XCTAssertEqual(error.code, .aborted) } XCTAssertEqual(tester.clientStreamsOpened, 0) @@ -169,14 +169,14 @@ final class ClientRPCExecutorTests: XCTestCase { server: .failTest ) - try await tester.clientStreaming( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { - try await $0.write([1, 2, 3]) - } - ) { response in - XCTAssertThrowsRPCError(try response.message) { - XCTAssertEqual($0.code, .aborted) - } + await XCTAssertThrowsRPCErrorAsync { + try await tester.clientStreaming( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { _ in } + } errorHandler: { error in + XCTAssertEqual(error.code, .aborted) } XCTAssertEqual(tester.clientStreamsOpened, 0) @@ -190,14 +190,12 @@ final class ClientRPCExecutorTests: XCTestCase { server: .failTest ) - try await tester.serverStreaming( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) - ) { response in - await XCTAssertThrowsRPCErrorAsync { - try await response.messages.collect() - } errorHandler: { - XCTAssertEqual($0.code, .aborted) - } + await XCTAssertThrowsRPCErrorAsync { + try await tester.serverStreaming( + request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + ) { _ in } + } errorHandler: { + XCTAssertEqual($0.code, .aborted) } XCTAssertEqual(tester.clientStreamsOpened, 0) @@ -211,16 +209,14 @@ final class ClientRPCExecutorTests: XCTestCase { server: .failTest ) - try await tester.bidirectional( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { - try await $0.write([1, 2, 3]) - } - ) { response in - await XCTAssertThrowsRPCErrorAsync { - try await response.messages.collect() - } errorHandler: { - XCTAssertEqual($0.code, .aborted) - } + await XCTAssertThrowsRPCErrorAsync { + try await tester.bidirectional( + request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + try await $0.write([1, 2, 3]) + } + ) { _ in } + } errorHandler: { + XCTAssertEqual($0.code, .aborted) } XCTAssertEqual(tester.clientStreamsOpened, 0) diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index b4967955a..fa64d76fd 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -21,20 +21,22 @@ struct AnyClientTransport: ClientTransport, Sendable { typealias Outbound = RPCWriter.Closable private let _retryThrottle: @Sendable () -> RetryThrottle - private let _openStream: @Sendable (MethodDescriptor) async throws -> RPCStream + private let _withStream: + @Sendable ( + _ method: MethodDescriptor, + _ body: (RPCStream) async throws -> Any + ) async throws -> Any private let _connect: @Sendable (Bool) async throws -> Void private let _close: @Sendable () -> Void private let _configuration: @Sendable (MethodDescriptor) -> ClientRPCExecutionConfiguration? - init(wrapping transport: Transport) { + init(wrapping transport: Transport) + where Transport.Inbound == Inbound, Transport.Outbound == Outbound { self._retryThrottle = { transport.retryThrottle } - self._openStream = { descriptor in - let stream = try await transport.openStream(descriptor: descriptor) - return RPCStream( - descriptor: stream.descriptor, - inbound: RPCAsyncSequence(wrapping: stream.inbound), - outbound: RPCWriter.Closable(wrapping: stream.outbound) - ) + self._withStream = { descriptor, closure in + try await transport.withStream(descriptor: descriptor) { stream in + try await closure(stream) as Any + } } self._connect = { lazily in @@ -62,10 +64,12 @@ struct AnyClientTransport: ClientTransport, Sendable { self._close() } - func openStream( - descriptor: MethodDescriptor - ) async throws -> RPCStream { - try await self._openStream(descriptor) + func withStream( + descriptor: MethodDescriptor, + _ closure: (RPCStream) async throws -> T + ) async throws -> T { + let result = try await self._withStream(descriptor, closure) + return result as! T } func executionConfiguration( diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index ec5e2e5fb..11c294e03 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -34,7 +34,8 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { self._streamFailures.load(ordering: .sequentiallyConsistent) } - init(wrapping transport: Transport) { + init(wrapping transport: Transport) + where Transport.Inbound == Inbound, Transport.Outbound == Outbound { self.transport = AnyClientTransport(wrapping: transport) } @@ -50,13 +51,15 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { self.transport.close() } - func openStream( - descriptor: MethodDescriptor - ) async throws -> RPCStream { + func withStream( + descriptor: MethodDescriptor, + _ closure: (RPCStream) async throws -> T + ) async throws -> T { do { - let stream = try await self.transport.openStream(descriptor: descriptor) - self._streamsOpened.wrappingIncrement(ordering: .sequentiallyConsistent) - return stream + return try await self.transport.withStream(descriptor: descriptor) { stream in + self._streamsOpened.wrappingIncrement(ordering: .sequentiallyConsistent) + return try await closure(stream) + } } catch { self._streamFailures.wrappingIncrement(ordering: .sequentiallyConsistent) throw error diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index c340021c6..a80dc023d 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -42,9 +42,10 @@ struct ThrowOnStreamCreationTransport: ClientTransport { return nil } - func openStream( - descriptor: MethodDescriptor - ) async throws -> RPCStream { + func withStream( + descriptor: MethodDescriptor, + _ closure: (RPCStream) async throws -> T + ) async throws -> T { throw RPCError(code: self.code, message: "") } } diff --git a/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift b/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift new file mode 100644 index 000000000..5319c82d8 --- /dev/null +++ b/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift @@ -0,0 +1,263 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +final class InProcessClientTransportTests: XCTestCase { + struct FailTest: Error {} + + func testConnectWhenConnected() async { + let client = makeClient() + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await client.connect(lazily: false) + } + + group.addTask { + try await client.connect(lazily: false) + } + + await XCTAssertThrowsRPCErrorAsync { + try await group.next() + } errorHandler: { error in + XCTAssertEqual(error.code, .failedPrecondition) + } + group.cancelAll() + } + } + + func testConnectWhenClosed() async { + let client = makeClient() + + client.close() + + await XCTAssertThrowsRPCErrorAsync { + try await client.connect(lazily: false) + } errorHandler: { error in + XCTAssertEqual(error.code, .failedPrecondition) + } + } + + func testConnectWhenClosedAfterCancellation() async throws { + let client = makeClient() + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await client.connect(lazily: false) + } + group.addTask { + try await Task.sleep(for: .milliseconds(100)) + } + + try await group.next() + group.cancelAll() + + await XCTAssertThrowsRPCErrorAsync { + try await client.connect(lazily: false) + } errorHandler: { error in + XCTAssertEqual(error.code, .failedPrecondition) + } + } + } + + func testCloseWhenUnconnected() { + let client = makeClient() + + XCTAssertNoThrow(client.close()) + } + + func testCloseWhenClosed() { + let client = makeClient() + client.close() + + XCTAssertNoThrow(client.close()) + } + + func testConnectSuccessfullyAndThenClose() async throws { + let client = makeClient() + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await client.connect(lazily: false) + } + group.addTask { + try await Task.sleep(for: .milliseconds(100)) + } + + try await group.next() + client.close() + } + } + + func testOpenStreamWhenUnconnected() async throws { + let client = makeClient() + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await client.withStream(descriptor: .init(service: "test", method: "test")) { _ in + // Once the pending stream is opened, close the client to new connections, + // so that, once this closure is executed and this stream is closed, + // the client will return from `connect(lazily:)`. + client.close() + } + } + + group.addTask { + // Add a sleep to make sure connection happens after `withStream` has been called, + // to test pending streams are handled correctly. + try await Task.sleep(for: .milliseconds(100)) + try await client.connect(lazily: false) + } + + try await group.waitForAll() + } + } + + func testOpenStreamWhenClosed() async { + let client = makeClient() + + client.close() + + await XCTAssertThrowsRPCErrorAsync { + try await client.withStream(descriptor: .init(service: "test", method: "test")) { _ in } + } errorHandler: { error in + XCTAssertEqual(error.code, .failedPrecondition) + } + } + + func testOpenStreamSuccessfullyAndThenClose() async throws { + let server = InProcessServerTransport() + let client = makeClient(server: server) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await client.connect(lazily: false) + } + + group.addTask { + try await client.withStream(descriptor: .init(service: "test", method: "test")) { stream in + try await stream.outbound.write(.message([1])) + stream.outbound.finish() + let receivedMessages = try await stream.inbound.collect() + + XCTAssertEqual(receivedMessages, [.message([42])]) + } + } + + group.addTask { + for try await stream in server.listen() { + let receivedMessages = try await stream.inbound.collect() + try await stream.outbound.write(RPCResponsePart.message([42])) + stream.outbound.finish() + + XCTAssertEqual(receivedMessages, [.message([1])]) + } + } + + group.addTask { + try await Task.sleep(for: .milliseconds(100)) + client.close() + } + + try await group.next() + group.cancelAll() + } + } + + func testExecutionConfiguration() { + let policy = HedgingPolicy( + maximumAttempts: 10, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [] + ) + let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) + var configurations = ClientRPCExecutionConfigurationCollection( + defaultConfiguration: defaultConfiguration + ) + + var client = InProcessClientTransport(server: .init(), executionConfigurations: configurations) + + let firstDescriptor = MethodDescriptor(service: "test", method: "first") + XCTAssertEqual(client.executionConfiguration(forMethod: firstDescriptor), defaultConfiguration) + + let retryPolicy = RetryPolicy( + maximumAttempts: 10, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(1), + backoffMultiplier: 1.0, + retryableStatusCodes: [.unavailable] + ) + let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + configurations[firstDescriptor] = overrideConfiguration + client = InProcessClientTransport(server: .init(), executionConfigurations: configurations) + let secondDescriptor = MethodDescriptor(service: "test", method: "second") + XCTAssertEqual(client.executionConfiguration(forMethod: firstDescriptor), overrideConfiguration) + XCTAssertEqual(client.executionConfiguration(forMethod: secondDescriptor), defaultConfiguration) + } + + func testOpenMultipleStreamsThenClose() async throws { + let server = InProcessServerTransport() + let client = makeClient(server: server) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await client.connect(lazily: false) + } + + group.addTask { + try await client.withStream(descriptor: .init(service: "test", method: "test")) { stream in + try await Task.sleep(for: .milliseconds(100)) + } + } + + group.addTask { + try await client.withStream(descriptor: .init(service: "test", method: "test")) { stream in + try await Task.sleep(for: .milliseconds(100)) + } + } + + group.addTask { + try await Task.sleep(for: .milliseconds(50)) + client.close() + } + + try await group.next() + } + } + + func makeClient( + configuration: ClientRPCExecutionConfiguration? = nil, + server: InProcessServerTransport = InProcessServerTransport() + ) -> InProcessClientTransport { + let defaultPolicy = RetryPolicy( + maximumAttempts: 10, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(1), + backoffMultiplier: 1.0, + retryableStatusCodes: [.unavailable] + ) + + return InProcessClientTransport( + server: server, + executionConfigurations: .init( + defaultConfiguration: configuration ?? .init(retryPolicy: defaultPolicy) + ) + ) + } +} diff --git a/Tests/GRPCCoreTests/Transport/InProcessServerTransportTest.swift b/Tests/GRPCCoreTests/Transport/InProcessServerTransportTests.swift similarity index 98% rename from Tests/GRPCCoreTests/Transport/InProcessServerTransportTest.swift rename to Tests/GRPCCoreTests/Transport/InProcessServerTransportTests.swift index 68306965d..83cfeb6b1 100644 --- a/Tests/GRPCCoreTests/Transport/InProcessServerTransportTest.swift +++ b/Tests/GRPCCoreTests/Transport/InProcessServerTransportTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -final class InProcessServerTransportTest: XCTestCase { +final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessServerTransport() let stream = RPCStream, RPCWriter.Closable>( From d5a05a2591ad8a8f56a265e0427f2c12cf31dcf7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 22 Nov 2023 14:26:07 +0000 Subject: [PATCH 168/580] Add the server RPC executor. (#1715) Motivation: The server needs to handle accepted RPC streams by turning them into requests and letting a user provided handler handle the request, turning it into the response. The server executor is then responsible for handling the response and writing it back to the client. Modifications: - Add the server RPC executor - Add testing Utilities and tests for the server executor Result: Server RPCs can be handled --- Package.swift | 1 + .../Server/Internal/ServerRPCExecutor.swift | 299 +++++++++++++++ Sources/GRPCCore/Internal/Metadata+GRPC.swift | 13 + Sources/GRPCCore/Status.swift | 6 + .../Internal/AsyncIteratorSequence.swift | 69 ++++ .../RPCWriter+MessageToRPCResponsePart.swift | 53 +++ .../ServerRPCExecutorTestHarness.swift | 137 +++++++ .../Internal/ServerRPCExecutorTests.swift | 355 ++++++++++++++++++ .../Call/Server/ServerInterceptors.swift | 84 +++++ .../Test Utilities/XCTest+Utilities.swift | 13 + 10 files changed, 1030 insertions(+) create mode 100644 Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift create mode 100644 Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift create mode 100644 Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift create mode 100644 Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift diff --git a/Package.swift b/Package.swift index 77a95d6fa..8a33e274c 100644 --- a/Package.swift +++ b/Package.swift @@ -164,6 +164,7 @@ extension Target { name: "GRPCCore", dependencies: [ .dequeModule, + .atomics ], path: "Sources/GRPCCore" ) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift new file mode 100644 index 000000000..54b775195 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -0,0 +1,299 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +struct ServerRPCExecutor { + /// Executes an RPC using the provided handler. + /// + /// - Parameters: + /// - stream: The accepted stream to execute the RPC on. + /// - deserializer: A deserializer for messages received from the client. + /// - serializer: A serializer for messages to send to the client. + /// - interceptors: Server interceptors to apply to this RPC. + /// - handler: A handler which turns the request into a response. + @inlinable + static func execute( + stream: RPCStream, RPCWriter.Closable>, + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + interceptors: [any ServerInterceptor], + handler: @Sendable @escaping ( + _ request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream + ) async { + // Wait for the first request part from the transport. + let firstPart = await Self._waitForFirstRequestPart(inbound: stream.inbound) + + switch firstPart { + case .process(let metadata, let inbound): + await Self._execute( + method: stream.descriptor, + metadata: metadata, + inbound: inbound, + outbound: stream.outbound, + deserializer: deserializer, + serializer: serializer, + interceptors: interceptors, + handler: handler + ) + + case .reject(let error): + // Stream can't be handled; write an error status and close. + let status = Status(code: Status.Code(error.code), message: error.message) + try? await stream.outbound.write(.status(status, error.metadata)) + stream.outbound.finish() + } + } + + @inlinable + static func _execute( + method: MethodDescriptor, + metadata: Metadata, + inbound: UnsafeTransfer.AsyncIterator>, + outbound: RPCWriter.Closable, + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + interceptors: [any ServerInterceptor], + handler: @escaping @Sendable ( + _ request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream + ) async { + await withTaskGroup(of: ServerExecutorTask.self) { group in + if let timeout = metadata.timeout { + group.addTask { + let result = await Result { + try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + } + return .timedOut(result) + } + } + + group.addTask { + await Self._processRPC( + method: method, + metadata: metadata, + inbound: inbound, + outbound: outbound, + deserializer: deserializer, + serializer: serializer, + interceptors: interceptors, + handler: handler + ) + return .executed + } + + while let next = await group.next() { + switch next { + case .timedOut(.success): + // Timeout expired; cancel the work. + group.cancelAll() + + case .timedOut(.failure): + // Timeout failed (because it was cancelled). Wait for more tasks to finish. + () + + case .executed: + // The work finished. Cancel any remaining tasks. + group.cancelAll() + } + } + } + } + + @inlinable + static func _processRPC( + method: MethodDescriptor, + metadata: Metadata, + inbound: UnsafeTransfer.AsyncIterator>, + outbound: RPCWriter.Closable, + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + interceptors: [any ServerInterceptor], + handler: @escaping @Sendable ( + ServerRequest.Stream + ) async throws -> ServerResponse.Stream + ) async { + let messages = AsyncIteratorSequence(inbound.wrappedValue).map { part throws -> Input in + switch part { + case .message(let bytes): + return try deserializer.deserialize(bytes) + case .metadata: + throw RPCError( + code: .internalError, + message: """ + Server received an extra set of metadata. Only one set of metadata may be received \ + at the start of the RPC. This is likely to be caused by a misbehaving client. + """ + ) + } + } + + let response = await Result { + // Run the request through the interceptors, finally passing it to the handler. + return try await Self._intercept( + request: ServerRequest.Stream( + metadata: metadata, + messages: RPCAsyncSequence(wrapping: messages) + ), + context: ServerInterceptorContext(descriptor: method), + interceptors: interceptors + ) { request, _ in + try await handler(request) + } + }.castError(to: RPCError.self) { error in + RPCError(code: .unknown, message: "Service method threw an unknown error.", cause: error) + }.flatMap { response in + response.accepted + } + + let status: Status + let metadata: Metadata + + switch response { + case .success(let contents): + let result = await Result { + // Write the metadata and run the producer. + try await outbound.write(.metadata(contents.metadata)) + return try await contents.producer( + .serializingToRPCResponsePart(into: outbound, with: serializer) + ) + }.castError(to: RPCError.self) { error in + RPCError(code: .unknown, message: "", cause: error) + } + + switch result { + case .success(let trailingMetadata): + status = .ok + metadata = trailingMetadata + case .failure(let error): + status = Status(code: Status.Code(error.code), message: error.message) + metadata = error.metadata + } + + case .failure(let error): + status = Status(code: Status.Code(error.code), message: error.message) + metadata = error.metadata + } + + try? await outbound.write(.status(status, metadata)) + outbound.finish() + } + + @inlinable + static func _waitForFirstRequestPart( + inbound: RPCAsyncSequence + ) async -> OnFirstRequestPart { + var iterator = inbound.makeAsyncIterator() + let part = await Result { try await iterator.next() } + let onFirstRequestPart: OnFirstRequestPart + + switch part { + case .success(.metadata(let metadata)): + // The only valid first part. + onFirstRequestPart = .process(metadata, UnsafeTransfer(iterator)) + + case .success(.none): + // Empty stream; reject. + let error = RPCError(code: .internalError, message: "Empty inbound server stream.") + onFirstRequestPart = .reject(error) + + case .success(.message): + let error = RPCError( + code: .internalError, + message: """ + Invalid inbound server stream; received message bytes at start of stream. This is \ + likely to be a transport specific bug. + """ + ) + onFirstRequestPart = .reject(error) + + case .failure(let error): + let error = RPCError( + code: .unknown, + message: "Inbound server stream threw error when reading metadata.", + cause: error + ) + onFirstRequestPart = .reject(error) + } + + return onFirstRequestPart + } + + @usableFromInline + enum OnFirstRequestPart { + case process(Metadata, UnsafeTransfer.AsyncIterator>) + case reject(RPCError) + } + + @usableFromInline + enum ServerExecutorTask { + case timedOut(Result) + case executed + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ServerRPCExecutor { + @inlinable + static func _intercept( + request: ServerRequest.Stream, + context: ServerInterceptorContext, + interceptors: [any ServerInterceptor], + finally: @escaping @Sendable ( + _ request: ServerRequest.Stream, + _ context: ServerInterceptorContext + ) async throws -> ServerResponse.Stream + ) async throws -> ServerResponse.Stream { + return try await self._intercept( + request: request, + context: context, + iterator: interceptors.makeIterator(), + finally: finally + ) + } + + @inlinable + static func _intercept( + request: ServerRequest.Stream, + context: ServerInterceptorContext, + iterator: Array.Iterator, + finally: @escaping @Sendable ( + _ request: ServerRequest.Stream, + _ context: ServerInterceptorContext + ) async throws -> ServerResponse.Stream + ) async throws -> ServerResponse.Stream { + var iterator = iterator + + switch iterator.next() { + case .some(let interceptor): + let iter = iterator + do { + return try await interceptor.intercept(request: request, context: context) { + try await self._intercept(request: $0, context: $1, iterator: iter, finally: finally) + } + } catch let error as RPCError { + return ServerResponse.Stream(error: error) + } catch let other { + let error = RPCError(code: .unknown, message: "", cause: other) + return ServerResponse.Stream(error: error) + } + + case .none: + return try await finally(request, context) + } + } +} diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index cd58a0b74..f28bfc966 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -36,11 +36,24 @@ extension Metadata { RetryPushback(milliseconds: $0) } } + + @inlinable + var timeout: Duration? { + // Temporary hack to support tests; only supports nanoseconds. + guard let value = self.firstString(forKey: .timeout) else { return nil } + guard value.utf8.last == UTF8.CodeUnit(ascii: "n") else { return nil } + var index = value.utf8.endIndex + value.utf8.formIndex(before: &index) + guard let digits = String(value.utf8[..: AsyncSequence { + @usableFromInline + typealias Element = Base.Element + + /// The base iterator. + @usableFromInline + private(set) var base: Base + + /// Set to `true` when an iterator has been made. + @usableFromInline + let _hasMadeIterator = ManagedAtomic(false) + + @inlinable + init(_ base: Base) { + self.base = base + } + + @usableFromInline + struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline + private(set) var base: Base + + @inlinable + init(base: Base) { + self.base = base + } + + @inlinable + mutating func next() async throws -> Element? { + try await self.base.next() + } + } + + @inlinable + func makeAsyncIterator() -> AsyncIterator { + let (exchanged, original) = self._hasMadeIterator.compareExchange( + expected: false, + desired: true, + ordering: .relaxed + ) + + guard exchanged else { + fatalError("Only one iterator can be made") + } + + assert(!original) + return AsyncIterator(base: self.base) + } +} diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift new file mode 100644 index 000000000..23a7999b8 --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift @@ -0,0 +1,53 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@usableFromInline +struct MessageToRPCResponsePartWriter: RPCWriterProtocol { + @usableFromInline + typealias Element = Serializer.Message + + @usableFromInline + let base: RPCWriter + @usableFromInline + let serializer: Serializer + + @inlinable + init(serializer: Serializer, base: some RPCWriterProtocol) { + self.serializer = serializer + self.base = RPCWriter(wrapping: base) + } + + @inlinable + func write(contentsOf elements: some Sequence) async throws { + let requestParts = try elements.map { message -> RPCResponsePart in + .message(try self.serializer.serialize(message)) + } + + try await self.base.write(contentsOf: requestParts) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension RPCWriter { + @inlinable + static func serializingToRPCResponsePart( + into writer: some RPCWriterProtocol, + with serializer: some MessageSerializer + ) -> Self { + return RPCWriter(wrapping: MessageToRPCResponsePartWriter(serializer: serializer, base: writer)) + } +} diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift new file mode 100644 index 000000000..9febd2269 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -0,0 +1,137 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct ServerRPCExecutorTestHarness { + struct ServerHandler: Sendable { + let fn: @Sendable (ServerRequest.Stream) async throws -> ServerResponse.Stream + + init( + _ fn: @escaping @Sendable ( + ServerRequest.Stream + ) async throws -> ServerResponse.Stream + ) { + self.fn = fn + } + + func handle( + _ request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + try await self.fn(request) + } + + static func throwing(_ error: any Error) -> Self { + return Self { _ in throw error } + } + } + + let interceptors: [any ServerInterceptor] + + init(interceptors: [any ServerInterceptor] = []) { + self.interceptors = interceptors + } + + func execute( + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + handler: @escaping @Sendable ( + ServerRequest.Stream + ) async throws -> ServerResponse.Stream, + producer: @escaping (RPCWriter.Closable) async throws -> Void, + consumer: @escaping (RPCAsyncSequence) async throws -> Void + ) async throws { + try await self.execute( + deserializer: deserializer, + serializer: serializer, + handler: .init(handler), + producer: producer, + consumer: consumer + ) + } + + func execute( + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + handler: ServerHandler, + producer: @escaping (RPCWriter.Closable) async throws -> Void, + consumer: @escaping (RPCAsyncSequence) async throws -> Void + ) async throws { + let input = RPCAsyncSequence.makeBackpressuredStream( + of: RPCRequestPart.self, + watermarks: (16, 32) + ) + + let output = RPCAsyncSequence.makeBackpressuredStream( + of: RPCResponsePart.self, + watermarks: (16, 32) + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await producer(input.writer) + } + + group.addTask { + try await consumer(output.stream) + } + + group.addTask { + await ServerRPCExecutor.execute( + stream: RPCStream( + descriptor: MethodDescriptor(service: "foo", method: "bar"), + inbound: input.stream, + outbound: output.writer + ), + deserializer: deserializer, + serializer: serializer, + interceptors: self.interceptors, + handler: { try await handler.handle($0) } + ) + } + + try await group.waitForAll() + } + } + + func execute( + handler: ServerHandler<[UInt8], [UInt8]> = .echo, + producer: @escaping (RPCWriter.Closable) async throws -> Void, + consumer: @escaping (RPCAsyncSequence) async throws -> Void + ) async throws { + try await self.execute( + deserializer: IdentityDeserializer(), + serializer: IdentitySerializer(), + handler: handler, + producer: producer, + consumer: consumer + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ServerRPCExecutorTestHarness.ServerHandler where Input == Output { + static var echo: Self { + return Self { request in + return ServerResponse.Stream(metadata: request.metadata) { writer in + try await writer.write(contentsOf: request.messages) + return [:] + } + } + } +} diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift new file mode 100644 index 000000000..4f3c3e731 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -0,0 +1,355 @@ +/* + * Copyright 2023, 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 Atomics +import XCTest + +@testable import GRPCCore + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class ServerRPCExecutorTests: XCTestCase { + func testEchoNoMessages() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual( + parts, + [ + .metadata(["foo": "bar"]), + .status(.ok, [:]), + ] + ) + } + } + + func testEchoSingleMessage() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + try await inbound.write(.message([0])) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual( + parts, + [ + .metadata(["foo": "bar"]), + .message([0]), + .status(.ok, [:]), + ] + ) + } + } + + func testEchoMultipleMessages() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + try await inbound.write(.message([0])) + try await inbound.write(.message([1])) + try await inbound.write(.message([2])) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual( + parts, + [ + .metadata(["foo": "bar"]), + .message([0]), + .message([1]), + .message([2]), + .status(.ok, [:]), + ] + ) + } + } + + func testEchoSingleJSONMessage() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute( + deserializer: JSONDeserializer(), + serializer: JSONSerializer() + ) { request in + let messages = try await request.messages.collect() + XCTAssertEqual(messages, ["hello"]) + return ServerResponse.Stream(metadata: request.metadata) { writer in + try await writer.write("hello") + return [:] + } + } producer: { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + try await inbound.write(.message(Array("\"hello\"".utf8))) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual( + parts, + [ + .metadata(["foo": "bar"]), + .message(Array("\"hello\"".utf8)), + .status(.ok, [:]), + ] + ) + } + } + + func testEchoMultipleJSONMessages() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute( + deserializer: JSONDeserializer(), + serializer: JSONSerializer() + ) { request in + let messages = try await request.messages.collect() + XCTAssertEqual(messages, ["hello", "world"]) + return ServerResponse.Stream(metadata: request.metadata) { writer in + try await writer.write("hello") + try await writer.write("world") + return [:] + } + } producer: { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + try await inbound.write(.message(Array("\"hello\"".utf8))) + try await inbound.write(.message(Array("\"world\"".utf8))) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual( + parts, + [ + .metadata(["foo": "bar"]), + .message(Array("\"hello\"".utf8)), + .message(Array("\"world\"".utf8)), + .status(.ok, [:]), + ] + ) + } + } + + func testReturnTrailingMetadata() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute( + deserializer: IdentityDeserializer(), + serializer: IdentitySerializer() + ) { request in + return ServerResponse.Stream(metadata: request.metadata) { _ in + return ["bar": "baz"] + } + } producer: { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual( + parts, + [ + .metadata(["foo": "bar"]), + .status(.ok, ["bar": "baz"]), + ] + ) + } + } + + func testEmptyInbound() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .echo) { inbound in + inbound.finish() + } consumer: { outbound in + let part = try await outbound.collect().first + XCTAssertStatus(part) { status, _ in + XCTAssertEqual(status.code, .internalError) + } + } + } + + func testInboundStreamMissingMetadata() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.message([0])) + inbound.finish() + } consumer: { outbound in + let part = try await outbound.collect().first + XCTAssertStatus(part) { status, _ in + XCTAssertEqual(status.code, .internalError) + } + } + } + + func testInboundStreamThrows() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .echo) { inbound in + inbound.finish(throwing: RPCError(code: .aborted, message: "")) + } consumer: { outbound in + let part = try await outbound.collect().first + XCTAssertStatus(part) { status, _ in + XCTAssertEqual(status.code, .unknown) + } + } + } + + func testHandlerThrowsAnyError() async throws { + struct SomeError: Error {} + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .throwing(SomeError())) { inbound in + try await inbound.write(.metadata([:])) + inbound.finish() + } consumer: { outbound in + let part = try await outbound.collect().first + XCTAssertStatus(part) { status, _ in + XCTAssertEqual(status.code, .unknown) + } + } + } + + func testHandlerThrowsRPCError() async throws { + let error = RPCError(code: .aborted, message: "RPC aborted", metadata: ["foo": "bar"]) + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .throwing(error)) { inbound in + try await inbound.write(.metadata([:])) + inbound.finish() + } consumer: { outbound in + let part = try await outbound.collect().first + XCTAssertStatus(part) { status, metadata in + XCTAssertEqual(status.code, .aborted) + XCTAssertEqual(status.message, "RPC aborted") + XCTAssertEqual(metadata, ["foo": "bar"]) + } + } + } + + func testHandlerRespectsTimeout() async throws { + let harness = ServerRPCExecutorTestHarness() + try await harness.execute( + deserializer: IdentityDeserializer(), + serializer: IdentitySerializer() + ) { request in + do { + try await Task.sleep(until: .now.advanced(by: .seconds(180)), clock: .continuous) + } catch is CancellationError { + throw RPCError(code: .cancelled, message: "Sleep was cancelled") + } + + XCTFail("Server handler should've been cancelled by timeout.") + return ServerResponse.Stream(error: RPCError(code: .failedPrecondition, message: "")) + } producer: { inbound in + try await inbound.write(.metadata(["grpc-timeout": "1000n"])) + inbound.finish() + } consumer: { outbound in + let part = try await outbound.collect().first + XCTAssertStatus(part) { status, _ in + XCTAssertEqual(status.code, .cancelled) + XCTAssertEqual(status.message, "Sleep was cancelled") + } + } + } + + func testShortCircuitInterceptor() async throws { + let error = RPCError( + code: .unauthenticated, + message: "Unauthenticated", + metadata: ["foo": "bar"] + ) + + // The interceptor skips the handler altogether. + let harness = ServerRPCExecutorTestHarness(interceptors: [.rejectAll(with: error)]) + try await harness.execute( + deserializer: IdentityDeserializer(), + serializer: IdentitySerializer() + ) { request in + XCTFail("Unexpected request") + return ServerResponse.Stream( + of: [UInt8].self, + error: RPCError(code: .failedPrecondition, message: "") + ) + } producer: { inbound in + try await inbound.write(.metadata([:])) + inbound.finish() + } consumer: { outbound in + let part = try await outbound.collect().first + XCTAssertStatus(part) { status, metadata in + XCTAssertEqual(status.code, .unauthenticated) + XCTAssertEqual(status.message, "Unauthenticated") + XCTAssertEqual(metadata, ["foo": "bar"]) + } + } + } + + func testMultipleInterceptorsAreCalled() async throws { + let counter1 = ManagedAtomic(0) + let counter2 = ManagedAtomic(0) + + // The interceptor skips the handler altogether. + let harness = ServerRPCExecutorTestHarness( + interceptors: [ + .requestCounter(counter1), + .requestCounter(counter2), + ] + ) + + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata([:])) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual(parts, [.metadata([:]), .status(.ok, [:])]) + } + + XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) + XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 1) + } + + func testInterceptorsAreCalledInOrder() async throws { + let counter1 = ManagedAtomic(0) + let counter2 = ManagedAtomic(0) + + // The interceptor skips the handler altogether. + let harness = ServerRPCExecutorTestHarness( + interceptors: [ + .requestCounter(counter1), + .rejectAll(with: RPCError(code: .unavailable, message: "")), + .requestCounter(counter2), + ] + ) + + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata([:])) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual(parts, [.status(Status(code: .unavailable, message: ""), [:])]) + } + + XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) + // Zero because the RPC should've been rejected by the second interceptor. + XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 0) + } + + func testThrowingInterceptor() async throws { + let harness = ServerRPCExecutorTestHarness( + interceptors: [.throwError(RPCError(code: .unavailable, message: "Unavailable"))] + ) + + try await harness.execute(handler: .echo) { inbound in + try await inbound.write(.metadata([:])) + inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual(parts, [.status(Status(code: .unavailable, message: "Unavailable"), [:])]) + } + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift new file mode 100644 index 000000000..266648768 --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -0,0 +1,84 @@ +/* + * Copyright 2023, 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 Atomics +import GRPCCore + +extension ServerInterceptor where Self == RejectAllServerInterceptor { + static func rejectAll(with error: RPCError) -> Self { + return RejectAllServerInterceptor(error: error, throw: false) + } + + static func throwError(_ error: RPCError) -> Self { + return RejectAllServerInterceptor(error: error, throw: true) + } + +} + +extension ServerInterceptor where Self == RequestCountingServerInterceptor { + static func requestCounter(_ counter: ManagedAtomic) -> Self { + return RequestCountingServerInterceptor(counter: counter) + } +} + +/// Rejects all RPCs with the provided error. +struct RejectAllServerInterceptor: ServerInterceptor { + /// The error to reject all RPCs with. + let error: RPCError + /// Whether the error should be thrown. If `false` then the request is rejected with the error + /// instead. + let `throw`: Bool + + init(error: RPCError, throw: Bool = false) { + self.error = error + self.`throw` = `throw` + } + + func intercept( + request: ServerRequest.Stream, + context: ServerInterceptorContext, + next: @Sendable ( + ServerRequest.Stream, + ServerInterceptorContext + ) async throws -> ServerResponse.Stream + ) async throws -> ServerResponse.Stream { + if self.throw { + throw self.error + } else { + return ServerResponse.Stream(error: self.error) + } + } +} + +struct RequestCountingServerInterceptor: ServerInterceptor { + /// The error to reject all RPCs with. + let counter: ManagedAtomic + + init(counter: ManagedAtomic) { + self.counter = counter + } + + func intercept( + request: ServerRequest.Stream, + context: ServerInterceptorContext, + next: @Sendable ( + ServerRequest.Stream, + ServerInterceptorContext + ) async throws -> ServerResponse.Stream + ) async throws -> ServerResponse.Stream { + self.counter.wrappingIncrement(ordering: .sequentiallyConsistent) + return try await next(request, context) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 3c1cbaf70..78290bdc0 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -63,3 +63,16 @@ func XCTAssertThrowsRPCErrorAsync( XCTFail("Error had unexpected type '\(type(of: error))'") } } + +func XCTAssertStatus( + _ part: RPCResponsePart?, + statusHandler: (Status, Metadata) -> Void = { _, _ in } +) { + switch part { + case .some(.status(let status, let metadata)): + statusHandler(status, metadata) + default: + XCTFail("Expected '.status' but found '\(String(describing: part))'") + } + +} From 6f78513e5a9b1637f7f1b06d48f78c7724b17d4f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 22 Nov 2023 16:22:25 +0000 Subject: [PATCH 169/580] Add support for hedging (#1714) Motivation: The `ClientRPCExecutor` currently ignores hedging policies. This change adds support for them. Modifications: - Add a hedging executor and wire it up to the client rpc executor Result: RPC can be hedged under certain conditions --- .../ClientRPCExecutor+HedgingExecutor.swift | 530 ++++++++++++++++++ .../Client/Internal/ClientRPCExecutor.swift | 18 +- .../Internal/Task+SleepBackport.swift | 25 + .../Internal/TaskGroup+CancellableTask.swift | 82 +++ .../ClientRPCExecutorTests+Hedging.swift | 203 +++++++ .../Test Utilities/XCTest+Utilities.swift | 13 +- 6 files changed, 868 insertions(+), 3 deletions(-) create mode 100644 Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift create mode 100644 Sources/GRPCCore/Internal/Task+SleepBackport.swift create mode 100644 Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift new file mode 100644 index 000000000..63f46db74 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -0,0 +1,530 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutor { + @usableFromInline + struct HedgingExecutor< + Transport: ClientTransport, + Serializer: MessageSerializer, + Deserializer: MessageDeserializer + > { + @usableFromInline + typealias Input = Serializer.Message + @usableFromInline + typealias Output = Deserializer.Message + + @usableFromInline + let transport: Transport + @usableFromInline + let policy: HedgingPolicy + @usableFromInline + let timeout: Duration? + @usableFromInline + let interceptors: [any ClientInterceptor] + @usableFromInline + let serializer: Serializer + @usableFromInline + let deserializer: Deserializer + @usableFromInline + let bufferSize: Int + + @inlinable + init( + transport: Transport, + policy: HedgingPolicy, + timeout: Duration?, + interceptors: [any ClientInterceptor], + serializer: Serializer, + deserializer: Deserializer, + bufferSize: Int + ) { + self.transport = transport + self.policy = policy + self.timeout = timeout + self.interceptors = interceptors + self.serializer = serializer + self.deserializer = deserializer + self.bufferSize = bufferSize + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ClientRPCExecutor.HedgingExecutor { + @inlinable + func execute( + request: ClientRequest.Stream, + method: MethodDescriptor, + responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R { + // The high level approach is to have two levels of task group. In the outer level tasks are + // run to: + // - run a timeout task (if necessary), + // - run the request producer so that it writes into a broadcast sequence + // - run the inner task group. + // + // An inner task group runs a number of RPC attempts which may run concurrently. It's + // responsible for tracking the responses from the server, potentially using one and cancelling + // all other in flight attempts. Each attempt is started at a fixed interval unless the server + // explicitly overrides the period using "pushback". + let result = await withTaskGroup(of: _HedgingTaskResult.self) { group in + if let timeout = self.timeout { + group.addTask { + let result = await Result { + try await Task.sleep(for: timeout, clock: .continuous) + } + return .timedOut(result) + } + } + + // Play the original request into the broadcast sequence and construct a replayable request. + let broadcast = BroadcastAsyncSequence.makeStream(bufferSize: self.bufferSize) + group.addTask { + let result = await Result { + try await request.producer(RPCWriter(wrapping: broadcast.continuation)) + } + broadcast.continuation.finish(with: result) + return .finishedRequest(result) + } + + group.addTask { + let replayableRequest = ClientRequest.Stream(metadata: request.metadata) { writer in + try await writer.write(contentsOf: broadcast.stream) + } + + let result = await self.executeAttempt( + request: replayableRequest, + method: method, + responseHandler: responseHandler + ) + + return .rpcHandled(result) + } + + for await event in group { + switch event { + case .timedOut(let result): + switch result { + case .success: + group.cancelAll() + case .failure: + () // Cancelled, ignore and keep looping. + } + + case .finishedRequest(let result): + switch result { + case .success: + () + case .failure: + group.cancelAll() + } + + case .rpcHandled(let result): + group.cancelAll() + return result + } + } + + fatalError("Internal inconsistency") + } + + return try result.get() + } + + @inlinable + func executeAttempt( + request: ClientRequest.Stream, + method: MethodDescriptor, + responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async -> Result { + await withTaskGroup( + of: _HedgingAttemptTaskResult.self, + returning: Result.self + ) { group in + // The strategy here is to have two types of task running in the group: + // - To execute an RPC attempt. + // - To wait some time before starting the next attempt. + // + // As multiple attempts run concurrently, each attempt shares a broadcast sequence. + // When an attempt receives a usable response it will yield its attempt number into the + // sequence. Each attempt subgroup will also consume the sequence. If an attempt reads a + // value which is different to its attempt number then it will cancel itself. Each attempt + // returns back a handled response or the failed response (in case no attempts are + // successful). Failed responses may also impact when the next attempt is executed via + // server pushback. + let picker = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 2) + + // There's a potential race with attempts identifying that they are 'chosen'. Two attempts + // could succeed at the same time but, only one can yield first, the second wouldn't be aware + // of this. To avoid this each attempt goes via a state check before yielding to the sequence + // ensuring that only one response is used. (If this wasn't the case the response handler + // could be invoked more than once.) + let state = LockedValueBox(State(policy: self.policy)) + + // There's always a first attempt, safe to '!'. + let (attempt, scheduleNext) = state.withLockedValue({ $0.nextAttemptNumber() })! + + group.addTask { + let result = await self._startAttempt( + request: request, + method: method, + attempt: attempt, + state: state, + picker: picker, + responseHandler: responseHandler + ) + + return .attemptCompleted(result) + } + + // Schedule the second attempt. + var nextScheduledAttempt = ScheduledState() + if scheduleNext { + nextScheduledAttempt.schedule(in: &group, pushback: false, delay: self.policy.hedgingDelay) + } + + // Stop the most recent unusable response in case no response succeeds. + var unusableResponse: ClientResponse.Stream? + + while let next = await group.next() { + switch next { + case .scheduledAttemptFired(let outcome): + switch outcome { + case .ran: + // Start a new attempt and possibly schedule the next. + if let (attempt, scheduleNext) = state.withLockedValue({ $0.nextAttemptNumber() }) { + group.addTask { + let result = await self._startAttempt( + request: request, + method: method, + attempt: attempt, + state: state, + picker: picker, + responseHandler: responseHandler + ) + return .attemptCompleted(result) + } + + // Schedule the next attempt. + if scheduleNext { + nextScheduledAttempt.schedule( + in: &group, + pushback: false, + delay: self.policy.hedgingDelay + ) + } + } + + case .cancelled: + // Cancelling also resets the state. + nextScheduledAttempt.cancel() + } + + case .attemptCompleted(let outcome): + switch outcome { + case .usableResponse(let response): + // Note: we don't need to cancel other in-flight requests; they will communicate + // between themselves when one of them is chosen. + nextScheduledAttempt.cancel() + return response + + case .unusableResponse(let response, let pushback): + // Stash the unusable response. + unusableResponse = response + + switch pushback { + case .none: + // If the handle is for a pushback then don't cancel it or schedule a new timer. + if nextScheduledAttempt.hasPushbackHandle { + continue + } + + nextScheduledAttempt.cancel() + + if let (attempt, scheduleNext) = state.withLockedValue({ $0.nextAttemptNumber() }) { + group.addTask { + let result = await self._startAttempt( + request: request, + method: method, + attempt: attempt, + state: state, + picker: picker, + responseHandler: responseHandler + ) + return .attemptCompleted(result) + } + + // Schedule the next retry. + if scheduleNext { + nextScheduledAttempt.schedule( + in: &group, + pushback: true, + delay: self.policy.hedgingDelay + ) + } + } + + case .retryAfter(let delay): + nextScheduledAttempt.schedule(in: &group, pushback: true, delay: delay) + + case .stopRetrying: + // Stop any new attempts from happening. Let any existing attempts play out. + nextScheduledAttempt.cancel() + } + + case .noStreamAvailable(let error): + group.cancelAll() + return .failure(error) + } + } + } + + // The group always has a task which returns a response. If it's an acceptable response it + // will be processed and returned in the preceding while loop, this path is therefore only + // reachable if there was an unusable response so the force unwrap is safe. + return await Result { + try await responseHandler(unusableResponse!) + } + } + } + + @inlinable + func _startAttempt( + request: ClientRequest.Stream, + method: MethodDescriptor, + attempt: Int, + state: LockedValueBox, + picker: (stream: BroadcastAsyncSequence, continuation: BroadcastAsyncSequence.Source), + responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async -> _HedgingAttemptTaskResult.AttemptResult { + do { + return try await self.transport.withStream( + descriptor: method + ) { stream -> _HedgingAttemptTaskResult.AttemptResult in + return await withTaskGroup(of: _HedgingAttemptSubtaskResult.self) { group in + group.addTask { + do { + // The picker stream will have at most one element. + for try await selectedAttempt in picker.stream { + return .attemptPicked(selectedAttempt == attempt) + } + return .attemptPicked(false) + } catch { + return .attemptPicked(false) + } + } + + let processor = ClientStreamExecutor(transport: self.transport) + + group.addTask { + await processor.run() + return .processorFinished + } + + group.addTask { + let response = await ClientRPCExecutor.unsafeExecute( + request: request, + method: method, + attempt: attempt, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + streamProcessor: processor, + stream: stream + ) + return .response(response) + } + + for await next in group { + switch next { + case .attemptPicked(let wasPicked): + if !wasPicked { + group.cancelAll() + } + + case .response(let response): + switch response.accepted { + case .success: + self.transport.retryThrottle.recordSuccess() + + if state.withLockedValue({ $0.receivedUsableResponse() }) { + try? await picker.continuation.write(attempt) + picker.continuation.finish() + let result = await Result { try await responseHandler(response) } + return .usableResponse(result) + } else { + // A different attempt succeeded before we were cancelled. Report this as unusable. + return .unusableResponse(response, .none) + } + + case .failure(let error): + group.cancelAll() + + if self.policy.nonFatalStatusCodes.contains(Status.Code(error.code)) { + // The response failed and the status code is non-fatal, we can make another attempt. + self.transport.retryThrottle.recordFailure() + return .unusableResponse(response, error.metadata.retryPushback) + } else { + // A fatal error code counts as a success to the throttle. + self.transport.retryThrottle.recordSuccess() + + if state.withLockedValue({ $0.receivedUsableResponse() }) { + try! await picker.continuation.write(attempt) + picker.continuation.finish() + let result = await Result { try await responseHandler(response) } + return .usableResponse(result) + } else { + // A different attempt succeeded before we were cancelled. Report this as unusable. + return .unusableResponse(response, .none) + } + } + } + + case .processorFinished: + // Processor finished, wait for the response outcome. + () + } + } + + // There's always a task to return a `.response` which we use as a signal to return from + // the task group in the preceding code. This is therefore unreachable. + fatalError("Internal inconsistency") + } + } + } catch { + return .noStreamAvailable(error) + } + } + + @usableFromInline + struct State { + @usableFromInline + let _maximumAttempts: Int + @usableFromInline + private(set) var attempt: Int + @usableFromInline + private(set) var hasUsableResponse: Bool + + @inlinable + init(policy: HedgingPolicy) { + self._maximumAttempts = policy.maximumAttempts + self.attempt = 1 + self.hasUsableResponse = false + } + + @inlinable + mutating func receivedUsableResponse() -> Bool { + if self.hasUsableResponse { + return false + } else { + self.hasUsableResponse = true + return true + } + } + + @inlinable + mutating func nextAttemptNumber() -> (Int, Bool)? { + if self.hasUsableResponse || self.attempt > self._maximumAttempts { + return nil + } else { + let attempt = self.attempt + self.attempt += 1 + return (attempt, self.attempt <= self._maximumAttempts) + } + } + } + + @usableFromInline + struct ScheduledState { + @usableFromInline + var _handle: CancellableTaskHandle? + @usableFromInline + var _isPushback: Bool + + @inlinable + var hasPushbackHandle: Bool { + self._handle != nil && self._isPushback + } + + @inlinable + init() { + self._handle = nil + self._isPushback = false + } + + @inlinable + mutating func cancel() { + self._handle?.cancel() + self._handle = nil + self._isPushback = false + } + + @inlinable + mutating func schedule( + in group: inout TaskGroup<_HedgingAttemptTaskResult>, + pushback: Bool, + delay: Duration + ) { + self._handle?.cancel() + self._isPushback = pushback + self._handle = group.addCancellableTask { + do { + try await Task.sleep(for: delay, clock: .continuous) + return .scheduledAttemptFired(.ran) + } catch { + return .scheduledAttemptFired(.cancelled) + } + } + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +enum _HedgingTaskResult { + case rpcHandled(Result) + case finishedRequest(Result) + case timedOut(Result) +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +enum _HedgingAttemptTaskResult { + case attemptCompleted(AttemptResult) + case scheduledAttemptFired(ScheduleEvent) + + @usableFromInline + enum AttemptResult { + case unusableResponse(ClientResponse.Stream, Metadata.RetryPushback?) + case usableResponse(Result) + case noStreamAvailable(Error) + } + + @usableFromInline + enum ScheduleEvent { + case ran + case cancelled + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@usableFromInline +enum _HedgingAttemptSubtaskResult { + case attemptPicked(Bool) + case processorFinished + case response(ClientResponse.Stream) +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 565df7ec3..03e12c87a 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -75,8 +75,22 @@ enum ClientRPCExecutor { responseHandler: handler ) - case .hedge: - fatalError() + case .hedge(let policy): + let hedging = HedgingExecutor( + transport: transport, + policy: policy, + timeout: configuration.timeout, + interceptors: interceptors, + serializer: serializer, + deserializer: deserializer, + bufferSize: 64 // TODO: the client should have some control over this. + ) + + return try await hedging.execute( + request: request, + method: method, + responseHandler: handler + ) } } } diff --git a/Sources/GRPCCore/Internal/Task+SleepBackport.swift b/Sources/GRPCCore/Internal/Task+SleepBackport.swift new file mode 100644 index 000000000..68ed60e8d --- /dev/null +++ b/Sources/GRPCCore/Internal/Task+SleepBackport.swift @@ -0,0 +1,25 @@ +/* + * Copyright 2023, 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. + */ + +#if swift(<5.8) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Task where Success == Never, Failure == Never { + @inlinable + static func sleep(for duration: Duration, clock: ContinuousClock) async throws { + try await Self.sleep(for: duration) + } +} +#endif diff --git a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift new file mode 100644 index 000000000..1800f32dc --- /dev/null +++ b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift @@ -0,0 +1,82 @@ +/* + * Copyright 2023, 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. + */ + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension TaskGroup { + /// Adds a child task to the group which is individually cancellable. + /// + /// - Parameter operation: The task to add to the group. + /// - Returns: A handle which can be used to cancel the task without cancelling the rest of + /// the group. + @inlinable + mutating func addCancellableTask( + _ operation: @Sendable @escaping () async -> ChildTaskResult + ) -> CancellableTaskHandle { + let signal = AsyncStream.makeStream(of: Void.self) + self.addTask { + return await withTaskGroup( + of: _ResultOrCancelled.self, + returning: ChildTaskResult.self + ) { group in + group.addTask { + let childTaskResult = await operation() + return .result(childTaskResult) + } + + group.addTask { + for await _ in signal.stream {} + return .cancelled + } + + let first = await group.next()! + group.cancelAll() + let second = await group.next()! + + switch (first, second) { + case (.result(let result), .cancelled), (.cancelled, .result(let result)): + return result + default: + fatalError("Internal inconsistency") + } + } + } + + return CancellableTaskHandle(continuation: signal.continuation) + } + + @usableFromInline + enum _ResultOrCancelled { + case result(ChildTaskResult) + case cancelled + } +} + +@usableFromInline +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct CancellableTaskHandle: Sendable { + @usableFromInline + private(set) var continuation: AsyncStream.Continuation + + @inlinable + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + @inlinable + func cancel() { + self.continuation.finish() + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift new file mode 100644 index 000000000..9721c4aab --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -0,0 +1,203 @@ +/* + * Copyright 2023, 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 + +extension ClientRPCExecutorTests { + func testHedgingWhenAllAttemptsResultInNonFatalCodes() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .reject(withError: RPCError(code: .unavailable, message: "")) + ) + + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + configuration: .hedge(nonFatalCodes: [.unavailable]) + ) { response in + XCTAssertRejected(response) { error in + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(Array(error.metadata[stringValues: "grpc-previous-rpc-attempts"]), ["4"]) + } + } + + // All five attempts fail. + XCTAssertEqual(harness.clientStreamsOpened, 5) + XCTAssertEqual(harness.serverStreamsAccepted, 5) + } + + func testHedgingRespectsFatalStatusCodes() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .reject(withError: RPCError(code: .aborted, message: "")) + ) + + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + // Set a long delay to reduce the risk of racing the second attempt and checking the number + // of streams being opened. + configuration: .hedge(delay: .seconds(5), nonFatalCodes: []) + ) { response in + XCTAssertRejected(response) { error in + XCTAssertEqual(error.code, .aborted) + } + } + + // The first response is fatal. + XCTAssertEqual(harness.clientStreamsOpened, 1) + XCTAssertEqual(harness.serverStreamsAccepted, 1) + + } + + func testHedgingWhenServerIsSlowToRespond() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .attemptBased { attempt in + if attempt == 5 { + return .echo + } else { + return .sleepFor( + duration: .seconds(60), + then: .reject(withError: RPCError(code: .unavailable, message: "")) + ) + } + } + ) + + let start = ContinuousClock.now + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + configuration: .hedge( + maximumAttempts: 5, + delay: .milliseconds(10), + nonFatalCodes: [.unavailable] + ) + ) { response in + let duration = ContinuousClock.now - start + // Should take significantly less than the 60 seconds of the slow responders to get a + // response from the fast responder. Use a large amount of leeway to avoid false positives + // in slow CI systems. + XCTAssertLessThanOrEqual(duration, .milliseconds(500)) + + let messages = try await response.messages.collect() + XCTAssertEqual(messages, [[0], [1], [2]]) + XCTAssertEqual(Array(response.metadata[stringValues: "grpc-previous-rpc-attempts"]), ["4"]) + } + + // Only the 5th attempt succeeds. + XCTAssertEqual(harness.clientStreamsOpened, 5) + XCTAssertEqual(harness.serverStreamsAccepted, 5) + } + + func testHedgingWithServerPushback() async throws { + let harness = ClientRPCExecutorTestHarness( + server: .attemptBased { attempt in + if attempt == 2 { + return .echo + } else { + return .init { stream in + let status = Status(code: .unavailable, message: "") + let metadata: Metadata = ["grpc-retry-delay-ms": "10"] + try await stream.outbound.write(.status(status, metadata)) + } + } + } + ) + + let start = ContinuousClock.now + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + try await $0.write([1]) + try await $0.write([2]) + }, + configuration: .hedge( + maximumAttempts: 5, + delay: .seconds(60), // High delay, server pushback will override this. + nonFatalCodes: [.unavailable] + ) + ) { response in + let duration = ContinuousClock.now - start + // Should take significantly less than the 60 seconds. The server pushback is only 10 ms which + // should override the configured delay. Use a large amount of leeway to avoid false positives + // in slow CI systems. + XCTAssertLessThanOrEqual(duration, .milliseconds(500)) + + let messages = try await response.messages.collect() + XCTAssertEqual(messages, [[0], [1], [2]]) + XCTAssertEqual(Array(response.metadata[stringValues: "grpc-previous-rpc-attempts"]), ["1"]) + } + + // Only the 2nd attempt succeeds. + XCTAssertEqual(harness.clientStreamsOpened, 2) + XCTAssertEqual(harness.serverStreamsAccepted, 2) + } + + func testHedgingWithNegativeServerPushback() async throws { + // Negative and values which can't be parsed should halt retries. + for pushback in ["-1", "not-an-int"] { + let harness = ClientRPCExecutorTestHarness( + server: .reject( + withError: RPCError( + code: .unavailable, + message: "", + metadata: ["grpc-retry-pushback-ms": "\(pushback)"] + ) + ) + ) + + try await harness.bidirectional( + request: ClientRequest.Stream { + try await $0.write([0]) + }, + configuration: .hedge(delay: .seconds(60), nonFatalCodes: [.unavailable]) + ) { response in + XCTAssertRejected(response) { error in + XCTAssertEqual(error.code, .unavailable) + } + } + + // Only one attempt should be made. + XCTAssertEqual(harness.clientStreamsOpened, 1) + XCTAssertEqual(harness.serverStreamsAccepted, 1) + } + } +} + +extension ClientRPCExecutionConfiguration { + fileprivate static func hedge( + maximumAttempts: Int = 5, + delay: Duration = .milliseconds(25), + nonFatalCodes: Set, + timeout: Duration? = nil + ) -> Self { + let policy = HedgingPolicy( + maximumAttempts: maximumAttempts, + hedgingDelay: delay, + nonFatalStatusCodes: nonFatalCodes + ) + + return ClientRPCExecutionConfiguration(hedgingPolicy: policy, timeout: timeout) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 78290bdc0..b85889188 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -64,6 +64,18 @@ func XCTAssertThrowsRPCErrorAsync( } } +func XCTAssertRejected( + _ response: ClientResponse.Stream, + errorHandler: (RPCError) -> Void +) { + switch response.accepted { + case .success: + XCTFail("Expected RPC to be rejected") + case .failure(let error): + errorHandler(error) + } +} + func XCTAssertStatus( _ part: RPCResponsePart?, statusHandler: (Status, Metadata) -> Void = { _, _ in } @@ -74,5 +86,4 @@ func XCTAssertStatus( default: XCTFail("Expected '.status' but found '\(String(describing: part))'") } - } From c1fbfb48de457863e05374e93ae18d250dabe1a1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 Nov 2023 10:37:50 +0000 Subject: [PATCH 170/580] Add the router and service protocol (#1717) Motivation: The transport layer accepts streams from the client and transforms them into requests which are handled by services. Services, written by users, need a way to tell the server which streams should be handled by which methods. Modifications: - Add the RPC router which routes streams to "handlers" - Add the RegistrableRPCService protocol which services can conform to to register routes with a router. The `RegistrableRPCService` is typically not implemented by users and instead will be implemented in the generated code. However, it builds on public API offered by the router. Result: We have interfaces which allow streams to be routed to a service handler. --- Sources/GRPCCore/Call/Server/RPCRouter.swift | 135 ++++++++++++++++++ .../Call/Server/RegistrableRPCService.swift | 31 ++++ .../Call/Server/RPCRouterTests.swift | 62 ++++++++ 3 files changed, 228 insertions(+) create mode 100644 Sources/GRPCCore/Call/Server/RPCRouter.swift create mode 100644 Sources/GRPCCore/Call/Server/RegistrableRPCService.swift create mode 100644 Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift new file mode 100644 index 000000000..ab0b615a8 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -0,0 +1,135 @@ +/* + * Copyright 2023, 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. + */ + +/// Stores and provides handlers for RPCs. +/// +/// The router stores a handler for each RPC it knows about. Each handler encapsulate the business +/// logic for the RPC which is typically implemented by service owners. To register a handler you +/// can call ``registerHandler(forMethod:deserializer:serializer:handler:)``. You can check whether +/// the router has a handler for a method with ``hasHandler(forMethod:)`` or get a list of all +/// methods with handlers registered by calling ``methods``. You can also remove the handler for a +/// given method by calling ``removeHandler(forMethod:)``. +/// +/// In most cases you won't need to interact with the router directly. Instead you should register +/// your services with ``Server/Services-swift.struct/register(_:)`` which will in turn register +/// each method with the router. +/// +/// You may wish to not serve all methods from your service in which case you can either: +/// +/// 1. Remove individual methods by calling ``removeHandler(forMethod:)``, or +/// 2. Implement ``RegistrableRPCService/registerMethods(with:)`` to register only the methods you +/// want to be served. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct RPCRouter: Sendable { + @usableFromInline + struct RPCHandler: Sendable { + @usableFromInline + let _fn: + @Sendable ( + _ stream: RPCStream, RPCWriter.Closable>, + _ interceptors: [any ServerInterceptor] + ) async -> Void + + @inlinable + init( + method: MethodDescriptor, + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + handler: @Sendable @escaping ( + _ request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream + ) { + self._fn = { stream, interceptors in + await ServerRPCExecutor.execute( + stream: stream, + deserializer: deserializer, + serializer: serializer, + interceptors: interceptors, + handler: handler + ) + } + } + + @inlinable + func handle( + stream: RPCStream, RPCWriter.Closable>, + interceptors: [any ServerInterceptor] + ) async { + await self._fn(stream, interceptors) + } + } + + @usableFromInline + private(set) var handlers: [MethodDescriptor: RPCHandler] + + /// Creates a new router with no methods registered. + public init() { + self.handlers = [:] + } + + /// Returns all descriptors known to the router in an undefined order. + public var methods: [MethodDescriptor] { + Array(self.handlers.keys) + } + + /// Returns the number of methods registered with the router. + public var count: Int { + self.handlers.count + } + + /// Returns whether a handler exists for a given method. + /// + /// - Parameter descriptor: A descriptor of the method. + /// - Returns: Whether a handler exists for the method. + public func hasHandler(forMethod descriptor: MethodDescriptor) -> Bool { + return self.handlers.keys.contains(descriptor) + } + + /// Registers a handler with the router. + /// + /// - Note: if a handler already exists for a given method then it will be replaced. + /// + /// - Parameters: + /// - descriptor: A descriptor for the method to register a handler for. + /// - deserializer: A deserializer to deserialize input messages received from the client. + /// - serializer: A serializer to serialize output messages to send to the client. + /// - handler: The function which handles the request and returns a response. + @inlinable + public mutating func registerHandler( + forMethod descriptor: MethodDescriptor, + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + handler: @Sendable @escaping ( + _ request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream + ) { + self.handlers[descriptor] = RPCHandler( + method: descriptor, + deserializer: deserializer, + serializer: serializer, + handler: handler + ) + } + + /// Removes any handler registered for the specified method. + /// + /// - Parameter descriptor: A descriptor of the method to remove a handler for. + /// - Returns: Whether a handler was removed. + @discardableResult + public mutating func removeHandler(forMethod descriptor: MethodDescriptor) -> Bool { + return self.handlers.removeValue(forKey: descriptor) != nil + } +} diff --git a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift new file mode 100644 index 000000000..c19004522 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift @@ -0,0 +1,31 @@ +/* + * Copyright 2023, 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. + */ + +/// An RPC service which can register its methods with an ``RPCRouter``. +/// +/// You typically won't have to implement this protocol yourself as the generated service code +/// provides conformance for your generated service type. However, if you need to customise which +/// methods your service offers or how the methods are registered then you can override the +/// generated conformance by implementing ``registerMethods(with:)`` manually by calling +/// ``RPCRouter/registerHandler(forMethod:deserializer:serializer:handler:)`` for each method +/// you want to register with the router. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol RegistrableRPCService: Sendable { + /// Registers methods to server with the provided ``RPCRouter``. + /// + /// - Parameter router: The router to register methods with. + func registerMethods(with router: inout RPCRouter) +} diff --git a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift new file mode 100644 index 000000000..8fd0c1a98 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift @@ -0,0 +1,62 @@ +/* + * Copyright 2023, 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 + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class RPCRouterTests: XCTestCase { + func testEmptyRouter() async throws { + var router = RPCRouter() + XCTAssertEqual(router.count, 0) + XCTAssertEqual(router.methods, []) + XCTAssertFalse(router.hasHandler(forMethod: MethodDescriptor(service: "foo", method: "bar"))) + XCTAssertFalse(router.removeHandler(forMethod: MethodDescriptor(service: "foo", method: "bar"))) + } + + func testRegisterMethod() async throws { + var router = RPCRouter() + let method = MethodDescriptor(service: "foo", method: "bar") + router.registerHandler( + forMethod: method, + deserializer: IdentityDeserializer(), + serializer: IdentitySerializer() + ) { _ in + throw RPCError(code: .failedPrecondition, message: "Shouldn't be called") + } + + XCTAssertEqual(router.count, 1) + XCTAssertEqual(router.methods, [method]) + XCTAssertTrue(router.hasHandler(forMethod: method)) + } + + func testRemoveMethod() async throws { + var router = RPCRouter() + let method = MethodDescriptor(service: "foo", method: "bar") + router.registerHandler( + forMethod: method, + deserializer: IdentityDeserializer(), + serializer: IdentitySerializer() + ) { _ in + throw RPCError(code: .failedPrecondition, message: "Shouldn't be called") + } + + XCTAssertTrue(router.removeHandler(forMethod: method)) + XCTAssertFalse(router.hasHandler(forMethod: method)) + XCTAssertEqual(router.count, 0) + XCTAssertEqual(router.methods, []) + } +} From 060ecea96bdf90ac30a8dc1c10aaf150d3541a53 Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Fri, 24 Nov 2023 10:17:42 +0000 Subject: [PATCH 171/580] Add missing avaliable annotations to test code (#1720) Signed-off-by: Si Beaumont --- .../ClientRPCExecutorTestHarness+Transport.swift | 1 + .../Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift | 2 ++ .../GRPCCoreTests/Transport/InProcessClientTransportTests.swift | 1 + 3 files changed, 4 insertions(+) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index c787867d1..42529470e 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -17,6 +17,7 @@ import Atomics @testable import GRPCCore +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension InProcessServerTransport { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index 9721c4aab..ab6a6fca5 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClientRPCExecutorTests { func testHedgingWhenAllAttemptsResultInNonFatalCodes() async throws { let harness = ClientRPCExecutorTestHarness( @@ -185,6 +186,7 @@ extension ClientRPCExecutorTests { } } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ClientRPCExecutionConfiguration { fileprivate static func hedge( maximumAttempts: Int = 5, diff --git a/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift b/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift index 5319c82d8..c7bbdef80 100644 --- a/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift +++ b/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class InProcessClientTransportTests: XCTestCase { struct FailTest: Error {} From 0acdb9b46de9cf7ccad2224d30fd482e62f0d651 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Nov 2023 10:32:13 +0000 Subject: [PATCH 172/580] Fix Task sleep backport (#1719) Motivation: The `Task.sleep(for:tolerance:clock:)` backport was added behind a `swift(<5.8)` guard, this should be 5.9. Moreover, the backport just dropped the clock type rather than using the `Task.sleep(until:)` API. Modifications: - Raise Swift version - Use `Task.sleep(until:)` so the correct clock type is used. Result: Fewer build failures --- Sources/GRPCCore/Internal/Task+SleepBackport.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCCore/Internal/Task+SleepBackport.swift b/Sources/GRPCCore/Internal/Task+SleepBackport.swift index 68ed60e8d..cd8620da8 100644 --- a/Sources/GRPCCore/Internal/Task+SleepBackport.swift +++ b/Sources/GRPCCore/Internal/Task+SleepBackport.swift @@ -14,12 +14,16 @@ * limitations under the License. */ -#if swift(<5.8) +#if swift(<5.9) @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Task where Success == Never, Failure == Never { @inlinable - static func sleep(for duration: Duration, clock: ContinuousClock) async throws { - try await Self.sleep(for: duration) + static func sleep( + for duration: C.Instant.Duration, + tolerance: C.Instant.Duration? = nil, + clock: C = ContinuousClock() + ) async throws { + try await clock.sleep(until: clock.now.advanced(by: duration), tolerance: tolerance) } } #endif From 9bf9d2867f57c49250bffb0831ec6b7f88cd36ba Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Fri, 24 Nov 2023 14:03:27 +0000 Subject: [PATCH 173/580] Created the CodeGenerationRequest (#1716) Motivation: The CodeGenerationRequest is part of the new code generator library for grpc-swift. It represents services described by IDL and is the first stage in the process of code generation. Modifications: Implemented the CodeGenerationRequest struct and added the new target and library in manifest. Result: The code generators will be able to use a general representation for the services described in different IDLs. --- Package.swift | 6 + .../GRPCCodeGen/CodeGenerationRequest.swift | 239 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 Sources/GRPCCodeGen/CodeGenerationRequest.swift diff --git a/Package.swift b/Package.swift index 8a33e274c..00a655337 100644 --- a/Package.swift +++ b/Package.swift @@ -87,6 +87,7 @@ extension Target.Dependency { static let grpc: Self = .target(name: grpcTargetName) static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") + static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") // Target dependencies; internal static let grpcSampleData: Self = .target(name: "GRPCSampleData") @@ -478,6 +479,11 @@ extension Target { .copy("Generated") ] ) + + static let grpcCodeGen: Target = .target( + name: "GRPCCodeGen", + path: "Sources/GRPCCodeGen" + ) } // MARK: - Products diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift new file mode 100644 index 000000000..1876bf5de --- /dev/null +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -0,0 +1,239 @@ +/* + * Copyright 2023, 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. + */ + +/// Describes the services, dependencies and trivia from an IDL file, +/// and the IDL itself through its specific serializer and deserializer. +public struct CodeGenerationRequest { + /// The name of the source file containing the IDL, including the extension if applicable. + public var fileName: String + + /// Any comments at the top of the file such as documentation and copyright headers. + /// They will be placed at the top of the generated file. + public var leadingTrivia: String + + /// The Swift imports that the generated file depends on. The gRPC specific imports aren't required + /// as they will be added by default in the generated file. + /// + /// - SeeAlso: ``Dependency``. + public var dependencies: [Dependency] + + /// A description of each service to generate. + /// + /// - SeeAlso: ``ServiceDescriptor``. + public var services: [ServiceDescriptor] + + /// Closure that receives a message type as a `String` and returns a code snippet to + /// initialize a `MessageSerializer` for that type as a `String`. + /// + /// The result is inserted in the generated code, where clients serialize RPC inputs and + /// servers serialize RPC outputs. + /// + /// For example, to serialize Protobuf messages you could specify a serializer as: + /// ```swift + /// request.lookupSerializer = { messageType in + /// "ProtobufSerializer<\(messageType)>()" + /// } + /// ``` + public var lookupSerializer: (_ messageType: String) -> String + + /// Closure that receives a message type as a `String` and returns a code snippet to + /// initialize a `MessageDeserializer` for that type as a `String`. + /// + /// The result is inserted in the generated code, where clients deserialize RPC outputs and + /// servers deserialize RPC inputs. + /// + /// For example, to serialize Protobuf messages you could specify a serializer as: + /// ```swift + /// request.lookupDeserializer = { messageType in + /// "ProtobufDeserializer<\(messageType)>()" + /// } + /// ``` + public var lookupDeserializer: (_ messageType: String) -> String + + public init( + fileName: String, + leadingTrivia: String, + dependencies: [Dependency], + services: [ServiceDescriptor], + lookupSerializer: @escaping (String) -> String, + lookupDeserializer: @escaping (String) -> String + ) { + self.fileName = fileName + self.leadingTrivia = leadingTrivia + self.dependencies = dependencies + self.services = services + self.lookupSerializer = lookupSerializer + self.lookupDeserializer = lookupDeserializer + } + + /// Represents an import: a module or a specific item from a module. + public struct Dependency { + /// If the dependency is an item, the property's value is the item representation. + /// If the dependency is a module, this property is nil. + public var item: Item? = nil + + /// The name of the imported module or of the module an item is imported from. + public var module: String + + public init(item: Item? = nil, module: String) { + self.item = item + self.module = module + } + + /// Represents an item imported from a module. + public struct Item { + /// The keyword that specifies the item's kind (e.g. `func`, `struct`). + public var kind: Kind + + /// The name of the imported item. + public var name: String + + public init(kind: Kind, name: String) { + self.kind = kind + self.name = name + } + + /// Represents the imported item's kind. + public struct Kind { + /// Describes the keyword associated with the imported item. + internal enum Value { + case `typealias` + case `struct` + case `class` + case `enum` + case `protocol` + case `let` + case `var` + case `func` + } + + internal var value: Value + + internal init(_ value: Value) { + self.value = value + } + + /// The imported item is a typealias. + public static var `typealias`: Self { + Self(.`typealias`) + } + + /// The imported item is a struct. + public static var `struct`: Self { + Self(.`struct`) + } + + /// The imported item is a class. + public static var `class`: Self { + Self(.`class`) + } + + /// The imported item is an enum. + public static var `enum`: Self { + Self(.`enum`) + } + + /// The imported item is a protocol. + public static var `protocol`: Self { + Self(.`protocol`) + } + + /// The imported item is a let. + public static var `let`: Self { + Self(.`let`) + } + + /// The imported item is a var. + public static var `var`: Self { + Self(.`var`) + } + + /// The imported item is a function. + public static var `func`: Self { + Self(.`func`) + } + } + } + } + + /// Represents a service described in an IDL file. + public struct ServiceDescriptor { + /// Documentation from comments above the IDL service description. + public var documentation: String + + /// Service name. + public var name: String + + /// The service namespace. + /// + /// For `.proto` files it is the package name. + public var namespace: String + + /// A description of each method of a service. + /// + /// - SeeAlso: ``MethodDescriptor``. + public var methods: [MethodDescriptor] + + public init( + documentation: String, + name: String, + namespace: String, + methods: [MethodDescriptor] + ) { + self.documentation = documentation + self.name = name + self.namespace = namespace + self.methods = methods + } + + /// Represents a method described in an IDL file. + public struct MethodDescriptor { + /// Documentation from comments above the IDL method description. + public var documentation: String + + /// Method name. + public var name: String + + /// Identifies if the method is input streaming. + public var isInputStreaming: Bool + + /// Identifies if the method is output streaming. + public var isOutputStreaming: Bool + + /// The generated input type for the described method. + public var inputType: String + + /// The generated output type for the described method. + public var ouputType: String + + public init( + documentation: String, + name: String, + isInputStreaming: Bool, + isOutputStreaming: Bool, + inputType: String, + ouputType: String + ) { + self.documentation = documentation + self.name = name + self.isInputStreaming = isInputStreaming + self.isOutputStreaming = isOutputStreaming + self.inputType = inputType + self.ouputType = ouputType + } + } + } +} From 688287cdacd55e1c70eb72970c60ea851518501f Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 27 Nov 2023 11:38:50 +0200 Subject: [PATCH 174/580] Bring over structured Swift syntax from OpenAPI (#1722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: We’ll need to be able to represent code to generate as structured Swift types for the new Codegen library. This is effectively what swift-syntax provides but we can’t (at time of writing) take it as a dependency because of its unstable API. Swift OpenAPI Generator has already provided a number of these types as its StructuedSwiftRepresentation. Modifications: - Copied StructuredSwiftRepresentation.swift inside the Codegen library module - Mentioned the usage in NOTICES.txt Result: Codegen library will have a representation of the code to generate as structured Swift types. --- NOTICES.txt | 9 + .../StructuredSwiftRepresentation.swift | 1725 +++++++++++++++++ 2 files changed, 1734 insertions(+) create mode 100644 Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift diff --git a/NOTICES.txt b/NOTICES.txt index 63039c3e3..f089e7948 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -57,3 +57,12 @@ This product uses derivations of swift-extras/swift-extras-base64 'Base64.swift' * https://github.com/swift-extras/swift-extras-base64/blob/main/LICENSE * HOMEPAGE: * https://github.com/swift-extras/swift-extras-base64 + +--- + +This product uses derivations of apple/swift-openapi-generator 'StructuredSwiftRepresentation.swift'. + + * LICENSE (Apache License 2.0): + * https://github.com/apple/swift-openapi-generator/blob/main/LICENSE.txt + * HOMEPAGE: + * https://github.com/apple/swift-openapi-generator diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift new file mode 100644 index 000000000..74b531366 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -0,0 +1,1725 @@ +/* + * Copyright 2023, 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. + */ + +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A description of an import declaration. +/// +/// For example: `import Foo`. +struct ImportDescription: Equatable, Codable { + + /// The name of the imported module. + /// + /// For example, the `Foo` in `import Foo`. + var moduleName: String + + /// An array of module types imported from the module, if applicable. + /// + /// For example, if there are type imports like `import Foo.Bar`, they would be listed here. + var moduleTypes: [String]? + + /// The name of the private interface for an `@_spi` import. + /// + /// For example, if `spi` was "Secret" and the module name was "Foo" then the import + /// would be `@_spi(Secret) import Foo`. + var spi: String? = nil + + /// Requirements for the `@preconcurrency` attribute. + var preconcurrency: PreconcurrencyRequirement = .never + + /// Describes any requirement for the `@preconcurrency` attribute. + enum PreconcurrencyRequirement: Equatable, Codable { + /// The attribute is always required. + case always + /// The attribute is not required. + case never + /// The attribute is required only on the named operating systems. + case onOS([String]) + } +} + +/// A description of an access modifier. +/// +/// For example: `public`. +enum AccessModifier: String, Equatable, Codable { + + /// A declaration accessible outside of the module. + case `public` + + /// A declaration only accessible inside of the module. + case `internal` + + /// A declaration only accessible inside the same Swift file. + case `fileprivate` + + /// A declaration only accessible inside the same type or scope. + case `private` +} + +/// A description of a comment. +/// +/// For example `/// Hello`. +enum Comment: Equatable, Codable { + + /// An inline comment. + /// + /// For example: `// Great code below`. + case inline(String) + + /// A documentation comment. + /// + /// For example: `/// Important type`. + case doc(String) + + /// A mark comment. + /// + /// For example: `// MARK: - Public methods`, with the optional + /// section break (`-`). + case mark(String, sectionBreak: Bool) +} + +/// A description of a literal. +/// +/// For example `"hello"` or `42`. +enum LiteralDescription: Equatable, Codable { + + /// A string literal. + /// + /// For example `"hello"`. + case string(String) + + /// An integer literal. + /// + /// For example `42`. + case int(Int) + + /// A Boolean literal. + /// + /// For example `true`. + case bool(Bool) + + /// The nil literal: `nil`. + case `nil` + + /// An array literal. + /// + /// For example `["hello", 42]`. + case array([Expression]) +} + +/// A description of an identifier, such as a variable name. +/// +/// For example, in `let foo = 42`, `foo` is an identifier. +enum IdentifierDescription: Equatable, Codable { + + /// A pattern identifier. + /// + /// For example, `foo` in `let foo = 42`. + case pattern(String) + + /// A type identifier. + /// + /// For example, `Swift.String` in `let foo: Swift.String = "hi"`. + case type(ExistingTypeDescription) +} + +/// A description of a member access expression. +/// +/// For example `foo.bar`. +struct MemberAccessDescription: Equatable, Codable { + + /// The expression of which a member `right` is accessed. + /// + /// For example, in `foo.bar`, `left` represents `foo`. + var left: Expression? + + /// The member name to access. + /// + /// For example, in `foo.bar`, `right` is `bar`. + var right: String +} + +/// A description of a function argument. +/// +/// For example in `foo(bar: 42)`, the function argument is `bar: 42`. +struct FunctionArgumentDescription: Equatable, Codable { + + /// An optional label of the function argument. + /// + /// For example, in `foo(bar: 42)`, the `label` is `bar`. + var label: String? + + /// The expression passed as the function argument value. + /// + /// For example, in `foo(bar: 42)`, `expression` represents `42`. + var expression: Expression +} + +/// A description of a function call. +/// +/// For example `foo(bar: 42)`. +struct FunctionCallDescription: Equatable, Codable { + + /// The expression that returns the function to be called. + /// + /// For example, in `foo(bar: 42)`, `calledExpression` represents `foo`. + var calledExpression: Expression + + /// The arguments to be passed to the function. + var arguments: [FunctionArgumentDescription] + + /// A trailing closure. + var trailingClosure: ClosureInvocationDescription? + + /// Creates a new function call description. + /// - Parameters: + /// - calledExpression: An expression that returns the function to be called. + /// - arguments: Arguments to be passed to the function. + /// - trailingClosure: A trailing closure. + init( + calledExpression: Expression, + arguments: [FunctionArgumentDescription] = [], + trailingClosure: ClosureInvocationDescription? = nil + ) { + self.calledExpression = calledExpression + self.arguments = arguments + self.trailingClosure = trailingClosure + } + + /// Creates a new function call description. + /// - Parameters: + /// - calledExpression: An expression that returns the function to be called. + /// - arguments: Arguments to be passed to the function. + /// - trailingClosure: A trailing closure. + init( + calledExpression: Expression, + arguments: [Expression], + trailingClosure: ClosureInvocationDescription? = nil + ) { + self.init( + calledExpression: calledExpression, + arguments: arguments.map { .init(label: nil, expression: $0) }, + trailingClosure: trailingClosure + ) + } +} + +/// A type of a variable binding: `let` or `var`. +enum BindingKind: Equatable, Codable { + + /// A mutable variable. + case `var` + + /// An immutable variable. + case `let` +} + +/// A description of a variable declaration. +/// +/// For example `let foo = 42`. +struct VariableDescription: Equatable, Codable { + + /// An access modifier. + var accessModifier: AccessModifier? + + /// A Boolean value that indicates whether the variable is static. + var isStatic: Bool = false + + /// The variable binding kind. + var kind: BindingKind + + /// The name of the variable. + /// + /// For example, in `let foo = 42`, `left` is `foo`. + var left: String + + /// The type of the variable. + /// + /// For example, in `let foo: Int = 42`, `type` is `Int`. + var type: ExistingTypeDescription? + + /// The expression to be assigned to the variable. + /// + /// For example, in `let foo = 42`, `right` represents `42`. + var right: Expression? = nil + + /// Body code for the getter. + /// + /// For example, in `var foo: Int { 42 }`, `body` represents `{ 42 }`. + var getter: [CodeBlock]? = nil + + /// Effects for the getter. + /// + /// For example, in `var foo: Int { get throws { 42 } }`, effects are `[.throws]`. + var getterEffects: [FunctionKeyword] = [] + + /// Body code for the setter. + /// + /// For example, in `var foo: Int { set { _foo = newValue } }`, `body` + /// represents `{ _foo = newValue }`. + var setter: [CodeBlock]? = nil + + /// Body code for the `_modify` accessor. + /// + /// For example, in `var foo: Int { _modify { yield &_foo } }`, `body` + /// represents `{ yield &_foo }`. + var modify: [CodeBlock]? = nil +} + +/// A requirement of a where clause. +enum WhereClauseRequirement: Equatable, Codable { + + /// A conformance requirement. + /// + /// For example, in `extension Array where Element: Foo {`, the first tuple value is `Element` and the second `Foo`. + case conformance(String, String) +} + +/// A description of a where clause. +/// +/// For example: `extension Array where Element: Foo {`. +struct WhereClause: Equatable, Codable { + + /// One or more requirements to be added after the `where` keyword. + var requirements: [WhereClauseRequirement] +} + +/// A description of an extension declaration. +/// +/// For example `extension Foo {`. +struct ExtensionDescription: Equatable, Codable { + + /// An access modifier. + var accessModifier: AccessModifier? + + /// The name of the extended type. + /// + /// For example, in `extension Foo {`, `onType` is `Foo`. + var onType: String + + /// Additional type names that the extension conforms to. + /// + /// For example: `["Sendable", "Codable"]`. + var conformances: [String] = [] + + /// A where clause constraining the extension declaration. + var whereClause: WhereClause? = nil + + /// The declarations that the extension adds on the extended type. + var declarations: [Declaration] +} + +/// A description of a struct declaration. +/// +/// For example `struct Foo {`. +struct StructDescription: Equatable, Codable { + + /// An access modifier. + var accessModifier: AccessModifier? = nil + + /// The name of the struct. + /// + /// For example, in `struct Foo {`, `name` is `Foo`. + var name: String + + /// The type names that the struct conforms to. + /// + /// For example: `["Sendable", "Codable"]`. + var conformances: [String] = [] + + /// The declarations that make up the main struct body. + var members: [Declaration] = [] +} + +/// A description of an enum declaration. +/// +/// For example `enum Bar {`. +struct EnumDescription: Equatable, Codable { + + /// A Boolean value that indicates whether the enum has a `@frozen` + /// attribute. + var isFrozen: Bool = false + + /// A Boolean value that indicates whether the enum has the `indirect` + /// keyword. + var isIndirect: Bool = false + + /// An access modifier. + var accessModifier: AccessModifier? = nil + + /// The name of the enum. + /// + /// For example, in `enum Bar {`, `name` is `Bar`. + var name: String + + /// The type names that the enum conforms to. + /// + /// For example: `["Sendable", "Codable"]`. + var conformances: [String] = [] + + /// The declarations that make up the enum body. + var members: [Declaration] = [] +} + +/// A description of a type reference. +indirect enum ExistingTypeDescription: Equatable, Codable { + + /// A type with the `any` keyword in front of it. + /// + /// For example, `any Foo`. + case any(ExistingTypeDescription) + + /// An optional type. + /// + /// For example, `Foo?`. + case optional(ExistingTypeDescription) + + /// A wrapper type generic over a wrapped type. + /// + /// For example, `Wrapper`. + case generic(wrapper: ExistingTypeDescription, wrapped: ExistingTypeDescription) + + /// A type reference represented by the components. + /// + /// For example, `MyModule.Foo`. + case member([String]) + + /// An array with an element type. + /// + /// For example, `[Foo]`. + case array(ExistingTypeDescription) + + /// A dictionary where the key is `Swift.String` and the value is + /// the provided type. + /// + /// For example, `[String: Foo]`. + case dictionaryValue(ExistingTypeDescription) +} + +/// A description of a typealias declaration. +/// +/// For example `typealias Foo = Int`. +struct TypealiasDescription: Equatable, Codable { + + /// An access modifier. + var accessModifier: AccessModifier? + + /// The name of the typealias. + /// + /// For example, in `typealias Foo = Int`, `name` is `Foo`. + var name: String + + /// The existing type that serves as the underlying type of the alias. + /// + /// For example, in `typealias Foo = Int`, `existingType` is `Int`. + var existingType: ExistingTypeDescription +} + +/// A description of a protocol declaration. +/// +/// For example `protocol Foo {`. +struct ProtocolDescription: Equatable, Codable { + + /// An access modifier. + var accessModifier: AccessModifier? = nil + + /// The name of the protocol. + /// + /// For example, in `protocol Foo {`, `name` is `Foo`. + var name: String + + /// The type names that the protocol conforms to. + /// + /// For example: `["Sendable", "Codable"]`. + var conformances: [String] = [] + + /// The function and property declarations that make up the protocol + /// requirements. + var members: [Declaration] = [] +} + +/// A description of a function parameter declaration. +/// +/// For example, in `func foo(bar baz: String = "hi")`, the parameter +/// description represents `bar baz: String = "hi"` +struct ParameterDescription: Equatable, Codable { + + /// An external parameter label. + /// + /// For example, in `bar baz: String = "hi"`, `label` is `bar`. + var label: String? = nil + + /// An internal parameter name. + /// + /// For example, in `bar baz: String = "hi"`, `name` is `baz`. + var name: String? = nil + + /// The type name of the parameter. + /// + /// For example, in `bar baz: String = "hi"`, `type` is `String`. + var type: ExistingTypeDescription + + /// A default value of the parameter. + /// + /// For example, in `bar baz: String = "hi"`, `defaultValue` + /// represents `"hi"`. + var defaultValue: Expression? = nil +} + +/// A function kind: `func` or `init`. +enum FunctionKind: Equatable, Codable { + + /// An initializer. + /// + /// For example: `init()`, or `init?()` when `failable` is `true`. + case initializer(failable: Bool) + + /// A function or a method. Can be static. + /// + /// For example `foo()`, where `name` is `foo`. + case function(name: String, isStatic: Bool) +} + +/// A function keyword, such as `async` and `throws`. +enum FunctionKeyword: Equatable, Codable { + + /// An asynchronous function. + case `async` + + /// A function that can throw an error. + case `throws` +} + +/// A description of a function signature. +/// +/// For example: `func foo(bar: String) async throws -> Int`. +struct FunctionSignatureDescription: Equatable, Codable { + + /// An access modifier. + var accessModifier: AccessModifier? = nil + + /// The kind of the function. + var kind: FunctionKind + + /// The parameters of the function. + var parameters: [ParameterDescription] = [] + + /// The keywords of the function, such as `async` and `throws.` + var keywords: [FunctionKeyword] = [] + + /// The return type name of the function, such as `Int`. + var returnType: Expression? = nil +} + +/// A description of a function definition. +/// +/// For example: `func foo() { }`. +struct FunctionDescription: Equatable, Codable { + + /// The signature of the function. + var signature: FunctionSignatureDescription + + /// The body definition of the function. + /// + /// If nil, does not generate `{` and `}` at all for the body scope. + var body: [CodeBlock]? = nil + + /// Creates a new function description. + /// - Parameters: + /// - signature: The signature of the function. + /// - body: The body definition of the function. + init(signature: FunctionSignatureDescription, body: [CodeBlock]? = nil) { + self.signature = signature + self.body = body + } + + /// Creates a new function description. + /// - Parameters: + /// - accessModifier: An access modifier. + /// - kind: The kind of the function. + /// - parameters: The parameters of the function. + /// - keywords: The keywords of the function, such as `async`. + /// - returnType: The return type name of the function, such as `Int`. + /// - body: The body definition of the function. + init( + accessModifier: AccessModifier? = nil, + kind: FunctionKind, + parameters: [ParameterDescription] = [], + keywords: [FunctionKeyword] = [], + returnType: Expression? = nil, + body: [CodeBlock]? = nil + ) { + self.signature = .init( + accessModifier: accessModifier, + kind: kind, + parameters: parameters, + keywords: keywords, + returnType: returnType + ) + self.body = body + } + + /// Creates a new function description. + /// - Parameters: + /// - accessModifier: An access modifier. + /// - kind: The kind of the function. + /// - parameters: The parameters of the function. + /// - keywords: The keywords of the function, such as `async`. + /// - returnType: The return type name of the function, such as `Int`. + /// - body: The body definition of the function. + init( + accessModifier: AccessModifier? = nil, + kind: FunctionKind, + parameters: [ParameterDescription] = [], + keywords: [FunctionKeyword] = [], + returnType: Expression? = nil, + body: [Expression] + ) { + self.init( + accessModifier: accessModifier, + kind: kind, + parameters: parameters, + keywords: keywords, + returnType: returnType, + body: body.map { .expression($0) } + ) + } +} + +/// A description of the associated value of an enum case. +/// +/// For example, in `case foo(bar: String)`, the associated value +/// represents `bar: String`. +struct EnumCaseAssociatedValueDescription: Equatable, Codable { + + /// A variable label. + /// + /// For example, in `bar: String`, `label` is `bar`. + var label: String? + + /// A variable type name. + /// + /// For example, in `bar: String`, `type` is `String`. + var type: ExistingTypeDescription +} + +/// An enum case kind. +/// +/// For example: `case foo` versus `case foo(String)`, and so on. +enum EnumCaseKind: Equatable, Codable { + + /// A case with only a name. + /// + /// For example: `case foo`. + case nameOnly + + /// A case with a name and a raw value. + /// + /// For example: `case foo = "Foo"`. + case nameWithRawValue(LiteralDescription) + + /// A case with a name and associated values. + /// + /// For example: `case foo(String)`. + case nameWithAssociatedValues([EnumCaseAssociatedValueDescription]) +} + +/// A description of an enum case. +/// +/// For example: `case foo(String)`. +struct EnumCaseDescription: Equatable, Codable { + + /// The name of the enum case. + /// + /// For example, in `case foo`, `name` is `foo`. + var name: String + + /// The kind of the enum case. + var kind: EnumCaseKind = .nameOnly +} + +/// A declaration of a Swift entity. +indirect enum Declaration: Equatable, Codable { + + /// A declaration that adds a comment on top of the provided declaration. + case commentable(Comment?, Declaration) + + /// A declaration that adds a comment on top of the provided declaration. + case deprecated(DeprecationDescription, Declaration) + + /// A variable declaration. + case variable(VariableDescription) + + /// An extension declaration. + case `extension`(ExtensionDescription) + + /// A struct declaration. + case `struct`(StructDescription) + + /// An enum declaration. + case `enum`(EnumDescription) + + /// A typealias declaration. + case `typealias`(TypealiasDescription) + + /// A protocol declaration. + case `protocol`(ProtocolDescription) + + /// A function declaration. + case function(FunctionDescription) + + /// An enum case declaration. + case enumCase(EnumCaseDescription) +} + +/// A description of a deprecation notice. +/// +/// For example: `@available(*, deprecated, message: "This is going away", renamed: "other(param:)")` +struct DeprecationDescription: Equatable, Codable { + + /// A message used by the deprecation attribute. + var message: String? + + /// A new name of the symbol, allowing the user to get a fix-it. + var renamed: String? +} + +/// A description of an assignment expression. +/// +/// For example: `foo = 42`. +struct AssignmentDescription: Equatable, Codable { + + /// The left-hand side expression, the variable to assign to. + /// + /// For example, in `foo = 42`, `left` is `foo`. + var left: Expression + + /// The right-hand side expression, the value to assign. + /// + /// For example, in `foo = 42`, `right` is `42`. + var right: Expression +} + +/// A switch case kind, either a `case` or a `default`. +enum SwitchCaseKind: Equatable, Codable { + + /// A case. + /// + /// For example: `case let foo(bar):`. + case `case`(Expression, [String]) + + /// A case with multiple comma-separated expressions. + /// + /// For example: `case "foo", "bar":`. + case multiCase([Expression]) + + /// A default. Spelled as `default:`. + case `default` +} + +/// A description of a switch case definition. +/// +/// For example: `case foo: print("foo")`. +struct SwitchCaseDescription: Equatable, Codable { + + /// The kind of the switch case. + var kind: SwitchCaseKind + + /// The body of the switch case. + /// + /// For example, in `case foo: print("foo")`, `body` + /// represents `print("foo")`. + var body: [CodeBlock] +} + +/// A description of a switch statement expression. +/// +/// For example: `switch foo {`. +struct SwitchDescription: Equatable, Codable { + + /// The expression evaluated by the switch statement. + /// + /// For example, in `switch foo {`, `switchedExpression` is `foo`. + var switchedExpression: Expression + + /// The cases defined in the switch statement. + var cases: [SwitchCaseDescription] +} + +/// A description of an if branch and the corresponding code block. +/// +/// For example: in `if foo { bar }`, the condition pair represents +/// `foo` + `bar`. +struct IfBranch: Equatable, Codable { + + /// The expressions evaluated by the if statement and their corresponding + /// body blocks. If more than one is provided, an `else if` branch is added. + /// + /// For example, in `if foo { bar }`, `condition` is `foo`. + var condition: Expression + + /// The body executed if the `condition` evaluates to true. + /// + /// For example, in `if foo { bar }`, `body` is `bar`. + var body: [CodeBlock] +} + +/// A description of an if[[/elseif]/else] statement expression. +/// +/// For example: `if foo { } else if bar { } else { }`. +struct IfStatementDescription: Equatable, Codable { + + /// The primary `if` branch. + var ifBranch: IfBranch + + /// Additional `else if` branches. + var elseIfBranches: [IfBranch] + + /// The body of an else block. + /// + /// No `else` statement is added when `elseBody` is nil. + var elseBody: [CodeBlock]? +} + +/// A description of a do statement. +/// +/// For example: `do { try foo() } catch { return bar }`. +struct DoStatementDescription: Equatable, Codable { + + /// The code blocks in the `do` statement body. + /// + /// For example, in `do { try foo() } catch { return bar }`, + /// `doBody` is `try foo()`. + var doStatement: [CodeBlock] + + /// The code blocks in the `catch` statement. + /// + /// If nil, no `catch` statement is added. + /// + /// For example, in `do { try foo() } catch { return bar }`, + /// `catchBody` is `return bar`. + var catchBody: [CodeBlock]? +} + +/// A description of a value binding used in enums with associated values. +/// +/// For example: `let foo(bar)`. +struct ValueBindingDescription: Equatable, Codable { + + /// The binding kind: `let` or `var`. + var kind: BindingKind + + /// The bound values in a function call expression syntax. + /// + /// For example, in `let foo(bar)`, `value` represents `foo(bar)`. + var value: FunctionCallDescription +} + +/// A kind of a keyword. +enum KeywordKind: Equatable, Codable { + + /// The return keyword. + case `return` + + /// The try keyword. + case `try`(hasPostfixQuestionMark: Bool) + + /// The await keyword. + case `await` + + /// The throw keyword. + case `throw` + + /// The yield keyword. + case `yield` +} + +/// A description of an expression that places a keyword before an expression. +struct UnaryKeywordDescription: Equatable, Codable { + + /// The keyword to place before the expression. + /// + /// For example, in `return foo`, `kind` represents `return`. + var kind: KeywordKind + + /// The expression prefixed by the keyword. + /// + /// For example, in `return foo`, `expression` represents `foo`. + var expression: Expression? = nil +} + +/// A description of a closure invocation. +/// +/// For example: `{ foo in return foo + "bar" }`. +struct ClosureInvocationDescription: Equatable, Codable { + + /// The names of the arguments taken by the closure. + /// + /// For example, in `{ foo in return foo + "bar" }`, `argumentNames` + /// is `["foo"]`. + var argumentNames: [String] = [] + + /// The code blocks of the closure body. + /// + /// For example, in `{ foo in return foo + "bar" }`, `body` + /// represents `return foo + "bar"`. + var body: [CodeBlock]? = nil +} + +/// A binary operator. +/// +/// For example: `+=` in `a += b`. +enum BinaryOperator: String, Equatable, Codable { + + /// The += operator, adds and then assigns another value. + case plusEquals = "+=" + + /// The == operator, checks equality between two values. + case equals = "==" + + /// The ... operator, creates an end-inclusive range between two numbers. + case rangeInclusive = "..." + + /// The || operator, used between two Boolean values. + case booleanOr = "||" +} + +/// A description of a binary operation expression. +/// +/// For example: `foo += 1`. +struct BinaryOperationDescription: Equatable, Codable { + + /// The left-hand side expression of the operation. + /// + /// For example, in `foo += 1`, `left` is `foo`. + var left: Expression + + /// The binary operator tying the two expressions together. + /// + /// For example, in `foo += 1`, `operation` represents `+=`. + var operation: BinaryOperator + + /// The right-hand side expression of the operation. + /// + /// For example, in `foo += 1`, `right` is `1`. + var right: Expression +} + +/// A description of an inout expression, which provides a read-write +/// reference to a variable. +/// +/// For example, `&foo` passes a reference to the `foo` variable. +struct InOutDescription: Equatable, Codable { + + /// The referenced expression. + /// + /// For example, in `&foo`, `referencedExpr` is `foo`. + var referencedExpr: Expression +} + +/// A description of an optional chaining expression. +/// +/// For example, in `foo?`, `referencedExpr` is `foo`. +struct OptionalChainingDescription: Equatable, Codable { + + /// The referenced expression. + /// + /// For example, in `foo?`, `referencedExpr` is `foo`. + var referencedExpr: Expression +} + +/// A description of a tuple. +/// +/// For example: `(foo, bar)`. +struct TupleDescription: Equatable, Codable { + + /// The member expressions. + /// + /// For example, in `(foo, bar)`, `members` is `[foo, bar]`. + var members: [Expression] +} + +/// A Swift expression. +indirect enum Expression: Equatable, Codable { + + /// A literal. + /// + /// For example `"hello"` or `42`. + case literal(LiteralDescription) + + /// An identifier, such as a variable name. + /// + /// For example, in `let foo = 42`, `foo` is an identifier. + case identifier(IdentifierDescription) + + /// A member access expression. + /// + /// For example: `foo.bar`. + case memberAccess(MemberAccessDescription) + + /// A function call. + /// + /// For example: `foo(bar: 42)`. + case functionCall(FunctionCallDescription) + + /// An assignment expression. + /// + /// For example `foo = 42`. + case assignment(AssignmentDescription) + + /// A switch statement expression. + /// + /// For example: `switch foo {`. + case `switch`(SwitchDescription) + + /// An if statement, with optional else if's and an else statement attached. + /// + /// For example: `if foo { bar } else if baz { boo } else { bam }`. + case ifStatement(IfStatementDescription) + + /// A do statement. + /// + /// For example: `do { try foo() } catch { return bar }`. + case doStatement(DoStatementDescription) + + /// A value binding used in enums with associated values. + /// + /// For example: `let foo(bar)`. + case valueBinding(ValueBindingDescription) + + /// An expression that places a keyword before an expression. + case unaryKeyword(UnaryKeywordDescription) + + /// A closure invocation. + /// + /// For example: `{ foo in return foo + "bar" }`. + case closureInvocation(ClosureInvocationDescription) + + /// A binary operation expression. + /// + /// For example: `foo += 1`. + case binaryOperation(BinaryOperationDescription) + + /// An inout expression, which provides a reference to a variable. + /// + /// For example, `&foo` passes a reference to the `foo` variable. + case inOut(InOutDescription) + + /// An optional chaining expression. + /// + /// For example, in `foo?`, `referencedExpr` is `foo`. + case optionalChaining(OptionalChainingDescription) + + /// A tuple expression. + /// + /// For example: `(foo, bar)`. + case tuple(TupleDescription) +} + +/// A code block item, either a declaration or an expression. +enum CodeBlockItem: Equatable, Codable { + + /// A declaration, such as of a new type or function. + case declaration(Declaration) + + /// An expression, such as a call of a declared function. + case expression(Expression) +} + +/// A code block, with an optional comment. +struct CodeBlock: Equatable, Codable { + + /// The comment to prepend to the code block item. + var comment: Comment? + + /// The code block item that appears below the comment. + var item: CodeBlockItem +} + +/// A description of a Swift file. +struct FileDescription: Equatable, Codable { + + /// A comment placed at the top of the file. + var topComment: Comment? + + /// Import statements placed below the top comment, but before the code + /// blocks. + var imports: [ImportDescription]? + + /// The code blocks that represent the main contents of the file. + var codeBlocks: [CodeBlock] +} + +/// A description of a named Swift file. +struct NamedFileDescription: Equatable, Codable { + + /// A file name, including the file extension. + /// + /// For example: `Foo.swift`. + var name: String + + /// The contents of the file. + var contents: FileDescription +} + +/// A file with contents made up of structured Swift code. +struct StructuredSwiftRepresentation: Equatable, Codable { + + /// The contents of the file. + var file: NamedFileDescription +} + +// MARK: - Conveniences + +extension Declaration { + + /// A variable declaration. + /// + /// For example: `let foo = 42`. + /// - Parameters: + /// - accessModifier: An access modifier. + /// - isStatic: A Boolean value that indicates whether the variable + /// is static. + /// - kind: The variable binding kind. + /// - left: The name of the variable. + /// - type: The type of the variable. + /// - right: The expression to be assigned to the variable. + /// - getter: Body code for the getter of the variable. + /// - getterEffects: Effects of the getter. + /// - setter: Body code for the setter of the variable. + /// - modify: Body code for the `_modify` accessor. + /// - Returns: Variable declaration. + static func variable( + accessModifier: AccessModifier? = nil, + isStatic: Bool = false, + kind: BindingKind, + left: String, + type: ExistingTypeDescription? = nil, + right: Expression? = nil, + getter: [CodeBlock]? = nil, + getterEffects: [FunctionKeyword] = [], + setter: [CodeBlock]? = nil, + modify: [CodeBlock]? = nil + + ) -> Self { + .variable( + .init( + accessModifier: accessModifier, + isStatic: isStatic, + kind: kind, + left: left, + type: type, + right: right, + getter: getter, + getterEffects: getterEffects, + setter: setter, + modify: modify + ) + ) + } + + /// A description of an enum case. + /// + /// For example: `case foo(String)`. + /// - Parameters: + /// - name: The name of the enum case. + /// - kind: The kind of the enum case. + /// - Returns: An enum case declaration. + static func enumCase(name: String, kind: EnumCaseKind = .nameOnly) -> Self { + .enumCase(.init(name: name, kind: kind)) + } + + /// A description of a typealias declaration. + /// + /// For example `typealias Foo = Int`. + /// - Parameters: + /// - accessModifier: An access modifier. + /// - name: The name of the typealias. + /// - existingType: The existing type that serves as the + /// underlying type of the alias. + /// - Returns: A typealias declaration. + static func `typealias`( + accessModifier: AccessModifier? = nil, + name: String, + existingType: ExistingTypeDescription + ) + -> Self + { .typealias(.init(accessModifier: accessModifier, name: name, existingType: existingType)) } + + /// A description of a function definition. + /// + /// For example: `func foo() { }`. + /// - Parameters: + /// - accessModifier: An access modifier. + /// - kind: The kind of the function. + /// - parameters: The parameters of the function. + /// - keywords: The keywords of the function, such as `async` and + /// `throws.` + /// - returnType: The return type name of the function, such as `Int`. + /// - body: The body definition of the function. + /// - Returns: A function declaration. + static func function( + accessModifier: AccessModifier? = nil, + kind: FunctionKind, + parameters: [ParameterDescription], + keywords: [FunctionKeyword] = [], + returnType: Expression? = nil, + body: [CodeBlock]? = nil + ) -> Self { + .function( + .init( + accessModifier: accessModifier, + kind: kind, + parameters: parameters, + keywords: keywords, + returnType: returnType, + body: body + ) + ) + } + + /// A description of a function definition. + /// + /// For example: `func foo() { }`. + /// - Parameters: + /// - signature: The signature of the function. + /// - body: The body definition of the function. + /// - Returns: A function declaration. + static func function(signature: FunctionSignatureDescription, body: [CodeBlock]? = nil) -> Self { + .function(.init(signature: signature, body: body)) + } + + /// A description of an enum declaration. + /// + /// For example `enum Bar {`. + /// - Parameters: + /// - isFrozen: A Boolean value that indicates whether the enum has + /// a `@frozen` attribute. + /// - accessModifier: An access modifier. + /// - name: The name of the enum. + /// - conformances: The type names that the enum conforms to. + /// - members: The declarations that make up the enum body. + /// - Returns: An enum declaration. + static func `enum`( + isFrozen: Bool = false, + accessModifier: AccessModifier? = nil, + name: String, + conformances: [String] = [], + members: [Declaration] = [] + ) -> Self { + .enum( + .init( + isFrozen: isFrozen, + accessModifier: accessModifier, + name: name, + conformances: conformances, + members: members + ) + ) + } + + /// A description of an extension declaration. + /// + /// For example `extension Foo {`. + /// - Parameters: + /// - accessModifier: An access modifier. + /// - onType: The name of the extended type. + /// - conformances: Additional type names that the extension conforms to. + /// - whereClause: A where clause constraining the extension declaration. + /// - declarations: The declarations that the extension adds on the + /// extended type. + /// - Returns: An extension declaration. + static func `extension`( + accessModifier: AccessModifier?, + onType: String, + conformances: [String] = [], + whereClause: WhereClause? = nil, + declarations: [Declaration] + ) -> Self { + .extension( + .init( + accessModifier: accessModifier, + onType: onType, + conformances: conformances, + whereClause: whereClause, + declarations: declarations + ) + ) + } +} + +extension FunctionKind { + /// Returns a non-failable initializer, for example `init()`. + static var initializer: Self { .initializer(failable: false) } + + /// Returns a non-static function kind. + static func function(name: String) -> Self { .function(name: name, isStatic: false) } +} + +extension CodeBlock { + + /// Returns a new declaration code block wrapping the provided declaration. + /// - Parameter declaration: The declaration to wrap. + /// - Returns: A new `CodeBlock` instance containing the provided declaration. + static func declaration(_ declaration: Declaration) -> Self { + CodeBlock(item: .declaration(declaration)) + } + + /// Returns a new expression code block wrapping the provided expression. + /// - Parameter expression: The expression to wrap. + /// - Returns: A new `CodeBlock` instance containing the provided declaration. + static func expression(_ expression: Expression) -> Self { + CodeBlock(item: .expression(expression)) + } +} + +extension Expression { + + /// A string literal. + /// + /// For example: `"hello"`. + /// - Parameter value: The string value of the literal. + /// - Returns: A new `Expression` representing a string literal. + static func literal(_ value: String) -> Self { .literal(.string(value)) } + + /// An integer literal. + /// + /// For example `42`. + /// - Parameter value: The integer value of the literal. + /// - Returns: A new `Expression` representing an integer literal. + static func literal(_ value: Int) -> Self { .literal(.int(value)) } + + /// Returns a new expression that accesses the member on the current + /// expression. + /// - Parameter member: The name of the member to access on the expression. + /// - Returns: A new expression representing member access. + func dot(_ member: String) -> Expression { .memberAccess(.init(left: self, right: member)) } + + /// Returns a new expression that calls the current expression as a function + /// with the specified arguments. + /// - Parameter arguments: The arguments used to call the expression. + /// - Returns: A new expression representing a function call. + func call(_ arguments: [FunctionArgumentDescription]) -> Expression { + .functionCall(.init(calledExpression: self, arguments: arguments)) + } + + /// Returns a new member access expression without a receiver, + /// starting with dot. + /// + /// For example: `.foo`, where `member` is `foo`. + /// - Parameter member: The name of the member to access. + /// - Returns: A new expression representing member access with a dot prefix. + static func dot(_ member: String) -> Self { Self.memberAccess(.init(right: member)) } + + /// Returns a new identifier expression for the provided pattern, such + /// as a variable or function name. + /// - Parameter name: The name of the identifier. + /// - Returns: A new expression representing an identifier with + /// the specified name. + static func identifierPattern(_ name: String) -> Self { .identifier(.pattern(name)) } + + /// Returns a new identifier expression for the provided type name. + /// - Parameter type: The description of the type. + /// - Returns: A new expression representing an identifier with + /// the specified name. + static func identifierType(_ type: ExistingTypeDescription) -> Self { .identifier(.type(type)) } + + /// Returns a new identifier expression for the provided type name. + /// - Parameter type: The name of the type. + /// - Returns: A new expression representing an identifier with + /// the specified name. + static func identifierType(_ type: TypeName) -> Self { .identifier(.type(.init(type))) } + + /// Returns a new identifier expression for the provided type name. + /// - Parameter type: The usage of the type. + /// - Returns: A new expression representing an identifier with + /// the specified name. + static func identifierType(_ type: TypeUsage) -> Self { .identifier(.type(.init(type))) } + + /// Returns a new switch statement expression. + /// - Parameters: + /// - switchedExpression: The expression evaluated by the switch + /// statement. + /// - cases: The cases defined in the switch statement. + /// - Returns: A new expression representing a switch statement with the specified switched expression and cases + static func `switch`(switchedExpression: Expression, cases: [SwitchCaseDescription]) -> Self { + .`switch`(.init(switchedExpression: switchedExpression, cases: cases)) + } + + /// Returns an if statement, with optional else if's and an else + /// statement attached. + /// - Parameters: + /// - ifBranch: The primary `if` branch. + /// - elseIfBranches: Additional `else if` branches. + /// - elseBody: The body of an else block. + /// - Returns: A new expression representing an `if` statement with the specified branches and blocks. + static func ifStatement( + ifBranch: IfBranch, + elseIfBranches: [IfBranch] = [], + elseBody: [CodeBlock]? = nil + ) -> Self { + .ifStatement(.init(ifBranch: ifBranch, elseIfBranches: elseIfBranches, elseBody: elseBody)) + } + + /// Returns a new function call expression. + /// + /// For example `foo(bar: 42)`. + /// - Parameters: + /// - calledExpression: The expression to be called as a function. + /// - arguments: The arguments to be passed to the function call. + /// - trailingClosure: A trailing closure. + /// - Returns: A new expression representing a function call with the specified called expression and arguments. + static func functionCall( + calledExpression: Expression, + arguments: [FunctionArgumentDescription] = [], + trailingClosure: ClosureInvocationDescription? = nil + ) -> Self { + .functionCall( + .init( + calledExpression: calledExpression, + arguments: arguments, + trailingClosure: trailingClosure + ) + ) + } + + /// Returns a new function call expression. + /// + /// For example: `foo(bar: 42)`. + /// - Parameters: + /// - calledExpression: The expression called as a function. + /// - arguments: The arguments passed to the function call. + /// - trailingClosure: A trailing closure. + /// - Returns: A new expression representing a function call with the specified called expression and arguments. + static func functionCall( + calledExpression: Expression, + arguments: [Expression], + trailingClosure: ClosureInvocationDescription? = nil + ) -> Self { + .functionCall( + .init( + calledExpression: calledExpression, + arguments: arguments.map { .init(label: nil, expression: $0) }, + trailingClosure: trailingClosure + ) + ) + } + + /// Returns a new expression that places a keyword before an expression. + /// - Parameters: + /// - kind: The keyword to place before the expression. + /// - expression: The expression prefixed by the keyword. + /// - Returns: A new expression with the specified keyword placed before the expression. + static func unaryKeyword(kind: KeywordKind, expression: Expression? = nil) -> Self { + .unaryKeyword(.init(kind: kind, expression: expression)) + } + + /// Returns a new expression that puts the return keyword before + /// an expression. + /// - Parameter expression: The expression to prepend. + /// - Returns: A new expression with the `return` keyword placed before the expression. + static func `return`(_ expression: Expression? = nil) -> Self { + .unaryKeyword(kind: .return, expression: expression) + } + + /// Returns a new expression that puts the try keyword before + /// an expression. + /// - Parameter expression: The expression to prepend. + /// - Returns: A new expression with the `try` keyword placed before the expression. + static func `try`(_ expression: Expression) -> Self { + .unaryKeyword(kind: .try, expression: expression) + } + + /// Returns a new expression that puts the try? keyword before + /// an expression. + /// - Parameter expression: The expression to prepend. + /// - Returns: A new expression with the `try?` keyword placed before the expression. + static func optionalTry(_ expression: Expression) -> Self { + .unaryKeyword(kind: .try(hasPostfixQuestionMark: true), expression: expression) + } + + /// Returns a new expression that puts the await keyword before + /// an expression. + /// - Parameter expression: The expression to prepend. + /// - Returns: A new expression with the `await` keyword placed before the expression. + static func `await`(_ expression: Expression) -> Self { + .unaryKeyword(kind: .await, expression: expression) + } + + /// Returns a new expression that puts the yield keyword before + /// an expression. + /// - Parameter expression: The expression to prepend. + /// - Returns: A new expression with the `yield` keyword placed before the expression. + static func `yield`(_ expression: Expression) -> Self { + .unaryKeyword(kind: .yield, expression: expression) + } + + /// Returns a new expression that puts the provided code blocks into + /// a do/catch block. + /// - Parameter: + /// - doStatement: The code blocks in the `do` statement body. + /// - catchBody: The code blocks in the `catch` statement. + /// - Returns: The expression. + static func `do`(_ doStatement: [CodeBlock], catchBody: [CodeBlock]? = nil) -> Self { + .doStatement(.init(doStatement: doStatement, catchBody: catchBody)) + } + + /// Returns a new value binding used in enums with associated values. + /// + /// For example: `let foo(bar)`. + /// - Parameters: + /// - kind: The binding kind: `let` or `var`. + /// - value: The bound values in a function call expression syntax. + /// - Returns: A new expression representing the value binding. + static func valueBinding(kind: BindingKind, value: FunctionCallDescription) -> Self { + .valueBinding(.init(kind: kind, value: value)) + } + + /// Returns a new closure invocation expression. + /// + /// For example: such as `{ foo in return foo + "bar" }`. + /// - Parameters: + /// - argumentNames: The names of the arguments taken by the closure. + /// - body: The code blocks of the closure body. + /// - Returns: A new expression representing the closure invocation + static func `closureInvocation`(argumentNames: [String] = [], body: [CodeBlock]? = nil) -> Self { + .closureInvocation(.init(argumentNames: argumentNames, body: body)) + } + + /// Creates a new binary operation expression. + /// + /// For example: `foo += 1`. + /// - Parameters: + /// - left: The left-hand side expression of the operation. + /// - operation: The binary operator tying the two expressions together. + /// - right: The right-hand side expression of the operation. + /// - Returns: A new expression representing the binary operation. + static func `binaryOperation`( + left: Expression, + operation: BinaryOperator, + right: Expression + ) -> Self { + .binaryOperation(.init(left: left, operation: operation, right: right)) + } + + /// Creates a new inout expression, which provides a read-write + /// reference to a variable. + /// + /// For example, `&foo` passes a reference to the `foo` variable. + /// - Parameter referencedExpr: The referenced expression. + /// - Returns: A new expression representing the inout expression. + static func inOut(_ referencedExpr: Expression) -> Self { + .inOut(.init(referencedExpr: referencedExpr)) + } + + /// Creates a new assignment expression. + /// + /// For example: `foo = 42`. + /// - Parameters: + /// - left: The left-hand side expression, the variable to assign to. + /// - right: The right-hand side expression, the value to assign. + /// - Returns: Assignment expression. + static func assignment(left: Expression, right: Expression) -> Self { + .assignment(.init(left: left, right: right)) + } + + /// Returns a new optional chaining expression wrapping the current + /// expression. + /// + /// For example, for the current expression `foo`, returns `foo?`. + /// - Returns: A new expression representing the optional chaining operation. + func optionallyChained() -> Self { .optionalChaining(.init(referencedExpr: self)) } + + /// Returns a new tuple expression. + /// + /// For example, in `(foo, bar)`, `members` is `[foo, bar]`. + /// - Parameter expressions: The member expressions. + /// - Returns: A tuple expression. + static func tuple(_ expressions: [Expression]) -> Self { .tuple(.init(members: expressions)) } +} + +extension MemberAccessDescription { + /// Creates a new member access expression without a receiver, starting + /// with dot. + /// + /// For example, `.foo`, where `member` is `foo`. + /// - Parameter member: The name of the member to access. + /// - Returns: A new member access expression. + static func dot(_ member: String) -> Self { .init(right: member) } +} + +extension Expression: ExpressibleByStringLiteral, ExpressibleByNilLiteral, ExpressibleByArrayLiteral +{ + init(arrayLiteral elements: Expression...) { self = .literal(.array(elements)) } + + init(stringLiteral value: String) { self = .literal(.string(value)) } + + init(nilLiteral: ()) { self = .literal(.nil) } +} + +extension LiteralDescription: ExpressibleByStringLiteral, ExpressibleByNilLiteral, + ExpressibleByArrayLiteral +{ + init(arrayLiteral elements: Expression...) { self = .array(elements) } + + init(stringLiteral value: String) { self = .string(value) } + + init(nilLiteral: ()) { self = .nil } +} + +extension VariableDescription { + + /// Returns a new mutable variable declaration. + /// + /// For example `var foo = 42`. + /// - Parameter name: The name of the variable. + /// - Returns: A new mutable variable declaration. + static func `var`(_ name: String) -> Self { Self.init(kind: .var, left: name) } + + /// Returns a new immutable variable declaration. + /// + /// For example `let foo = 42`. + /// - Parameter name: The name of the variable. + /// - Returns: A new immutable variable declaration. + static func `let`(_ name: String) -> Self { Self.init(kind: .let, left: name) } +} + +extension Expression { + + /// Creates a new assignment description where the called expression is + /// assigned the value of the specified expression. + /// - Parameter rhs: The right-hand side of the assignment expression. + /// - Returns: An assignment description representing the assignment. + func equals(_ rhs: Expression) -> AssignmentDescription { .init(left: self, right: rhs) } +} + +extension FunctionArgumentDescription: ExpressibleByStringLiteral { + init(stringLiteral value: String) { self = .init(expression: .literal(.string(value))) } +} + +extension FunctionSignatureDescription { + /// Returns a new function signature description that has the access + /// modifier updated to the specified one. + /// - Parameter accessModifier: The access modifier to use. + /// - Returns: A function signature description with the specified access modifier. + func withAccessModifier(_ accessModifier: AccessModifier?) -> Self { + var value = self + value.accessModifier = accessModifier + return value + } +} + +extension SwitchCaseKind { + /// Returns a new switch case kind with no argument names, only the + /// specified expression as the name. + /// - Parameter expression: The expression for the switch case label. + /// - Returns: A switch case kind with the specified expression as the label. + static func `case`(_ expression: Expression) -> Self { .case(expression, []) } +} + +extension KeywordKind { + + /// Returns the try keyword without the postfix question mark. + static var `try`: Self { .try(hasPostfixQuestionMark: false) } +} + +extension Declaration { + /// Returns a new deprecated variant of the declaration if `shouldDeprecate` is true. + func deprecate(if shouldDeprecate: Bool) -> Self { + if shouldDeprecate { return .deprecated(.init(), self) } + return self + } + + /// Returns the declaration one level deeper, nested inside the commentable + /// declaration, if present. + var strippingTopComment: Self { + guard case let .commentable(_, underlyingDecl) = self else { return self } + return underlyingDecl + } +} + +extension Declaration { + + /// An access modifier. + var accessModifier: AccessModifier? { + get { + switch self { + case .commentable(_, let declaration): return declaration.accessModifier + case .deprecated(_, let declaration): return declaration.accessModifier + case .variable(let variableDescription): return variableDescription.accessModifier + case .extension(let extensionDescription): return extensionDescription.accessModifier + case .struct(let structDescription): return structDescription.accessModifier + case .enum(let enumDescription): return enumDescription.accessModifier + case .typealias(let typealiasDescription): return typealiasDescription.accessModifier + case .protocol(let protocolDescription): return protocolDescription.accessModifier + case .function(let functionDescription): return functionDescription.signature.accessModifier + case .enumCase: return nil + } + } + set { + switch self { + case .commentable(let comment, var declaration): + declaration.accessModifier = newValue + self = .commentable(comment, declaration) + case .deprecated(let deprecationDescription, var declaration): + declaration.accessModifier = newValue + self = .deprecated(deprecationDescription, declaration) + case .variable(var variableDescription): + variableDescription.accessModifier = newValue + self = .variable(variableDescription) + case .extension(var extensionDescription): + extensionDescription.accessModifier = newValue + self = .extension(extensionDescription) + case .struct(var structDescription): + structDescription.accessModifier = newValue + self = .struct(structDescription) + case .enum(var enumDescription): + enumDescription.accessModifier = newValue + self = .enum(enumDescription) + case .typealias(var typealiasDescription): + typealiasDescription.accessModifier = newValue + self = .typealias(typealiasDescription) + case .protocol(var protocolDescription): + protocolDescription.accessModifier = newValue + self = .protocol(protocolDescription) + case .function(var functionDescription): + functionDescription.signature.accessModifier = newValue + self = .function(functionDescription) + case .enumCase: break + } + } + } +} + +extension ExistingTypeDescription { + + /// Creates a member type description with the provided single component. + /// - Parameter singleComponent: A single component of the name. + /// - Returns: The new type description. + static func member(_ singleComponent: String) -> Self { .member([singleComponent]) } +} From 3835e14ff9f9fa490b598c263eab879f2f9936d0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 27 Nov 2023 14:45:58 +0000 Subject: [PATCH 175/580] Add new Server object (#1721) Motivation: The server is the main entry point for running gRPC services. It combines a set of transports, services, and interceptors. Modifications: - Add the 'Server' and 'ServerError' Result: Can piece together transports, interceptors and services --- Sources/GRPCCore/Call/Server/RPCRouter.swift | 24 + .../GRPCCore/Call/Server/ServerRequest.swift | 18 +- .../GRPCCore/Call/Server/ServerResponse.swift | 1 - Sources/GRPCCore/Server.swift | 479 ++++++++++++++++++ Sources/GRPCCore/ServerError.swift | 148 ++++++ .../GRPCCore/Transport/ServerTransport.swift | 4 +- Tests/GRPCCoreTests/ServerErrorTests.swift | 53 ++ Tests/GRPCCoreTests/ServerTests.swift | 455 +++++++++++++++++ .../Test Utilities/Services/BinaryEcho.swift | 104 ++++ .../Transport/ThrowingTransport.swift | 33 ++ .../Test Utilities/XCTest+Utilities.swift | 52 ++ .../ConnectionPool/GRPCChannelPoolTests.swift | 4 +- 12 files changed, 1369 insertions(+), 6 deletions(-) create mode 100644 Sources/GRPCCore/Server.swift create mode 100644 Sources/GRPCCore/ServerError.swift create mode 100644 Tests/GRPCCoreTests/ServerErrorTests.swift create mode 100644 Tests/GRPCCoreTests/ServerTests.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index ab0b615a8..4bfe57c3c 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -133,3 +133,27 @@ public struct RPCRouter: Sendable { return self.handlers.removeValue(forKey: descriptor) != nil } } + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension RPCRouter { + internal func handle( + stream: RPCStream, RPCWriter.Closable>, + interceptors: [any ServerInterceptor] + ) async { + if let handler = self.handlers[stream.descriptor] { + await handler.handle(stream: stream, interceptors: interceptors) + } else { + // If this throws then the stream must be closed which we can't do anything about, so ignore + // any error. + try? await stream.outbound.write(.status(.rpcNotImplemented, [:])) + stream.outbound.finish() + } + } +} + +extension Status { + fileprivate static let rpcNotImplemented = Status( + code: .unimplemented, + message: "Requested RPC isn't implemented by this server." + ) +} diff --git a/Sources/GRPCCore/Call/Server/ServerRequest.swift b/Sources/GRPCCore/Call/Server/ServerRequest.swift index 31bf0e1cf..dafbee3d7 100644 --- a/Sources/GRPCCore/Call/Server/ServerRequest.swift +++ b/Sources/GRPCCore/Call/Server/ServerRequest.swift @@ -72,8 +72,24 @@ extension ServerRequest { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerRequest.Stream { - @_spi(Testing) public init(single request: ServerRequest.Single) { self.init(metadata: request.metadata, messages: .one(request.message)) } } + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension ServerRequest.Single { + public init(stream request: ServerRequest.Stream) async throws { + var iterator = request.messages.makeAsyncIterator() + + guard let message = try await iterator.next() else { + throw RPCError(code: .internalError, message: "Empty stream.") + } + + guard try await iterator.next() == nil else { + throw RPCError(code: .internalError, message: "Too many messages.") + } + + self = ServerRequest.Single(metadata: request.metadata, message: message) + } +} diff --git a/Sources/GRPCCore/Call/Server/ServerResponse.swift b/Sources/GRPCCore/Call/Server/ServerResponse.swift index 5fbdb43ec..a0b516815 100644 --- a/Sources/GRPCCore/Call/Server/ServerResponse.swift +++ b/Sources/GRPCCore/Call/Server/ServerResponse.swift @@ -327,7 +327,6 @@ extension ServerResponse.Stream { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerResponse.Stream { - @_spi(Testing) public init(single response: ServerResponse.Single) { switch response.accepted { case .success(let contents): diff --git a/Sources/GRPCCore/Server.swift b/Sources/GRPCCore/Server.swift new file mode 100644 index 000000000..43a48041f --- /dev/null +++ b/Sources/GRPCCore/Server.swift @@ -0,0 +1,479 @@ +/* + * Copyright 2023, 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 Atomics + +/// A gRPC server. +/// +/// The server accepts connections from clients and listens on each connection for new streams +/// which are initiated by the client. Each stream maps to a single RPC. The server routes accepted +/// streams to a service to handle the RPC or rejects them with an appropriate error if no service +/// can handle the RPC. +/// +/// A ``Server`` may listen with multiple transports (for example, HTTP/2 and in-process) and route +/// requests from each transport to the same service instance. You can also use "interceptors", +/// to implement cross-cutting logic which apply to all accepted RPCs. Example uses of interceptors +/// include request filtering, authentication, and logging. Once requests have been intercepted +/// they are passed to a handler which in turn returns a response to send back to the client. +/// +/// ## Creating and configuring a server +/// +/// The following example demonstrates how to create and configure a server. +/// +/// ```swift +/// let server = Server() +/// +/// // Create and add an in-process transport. +/// let inProcessTransport = InProcessServerTransport() +/// server.transports.add(inProcessTransport) +/// +/// // Create and register the 'Greeter' and 'Echo' services. +/// server.services.register(GreeterService()) +/// server.services.register(EchoService()) +/// +/// // Create and add some interceptors. +/// server.interceptors.add(StatsRecordingServerInterceptors()) +/// ``` +/// +/// ## Starting and stopping the server +/// +/// Once you have configured the server call ``run()`` to start it. Calling ``run()`` starts each +/// of the server's transports. A ``ServerError`` is thrown if any of the transports can't be +/// started. +/// +/// ```swift +/// // Start running the server. +/// try await server.run() +/// ``` +/// +/// The ``run()`` method won't return until the server has finished handling all requests. You can +/// signal to the server that it should stop accepting new requests by calling ``stopListening()``. +/// This allows the server to drain existing requests gracefully. To stop the server more abruptly +/// you can cancel the task running your server. If your application requires additional resources +/// that need their lifecycles managed you should consider using [Swift Service +/// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public final class Server: Sendable { + typealias Stream = RPCStream + + /// A collection of ``ServerTransport`` implementations that the server uses to listen + /// for new requests. + public var transports: Transports { + get { + self.storage.withLockedValue { $0.transports } + } + set { + self.storage.withLockedValue { $0.transports = newValue } + } + } + + /// The services registered which the server is serving. + public var services: Services { + get { + self.storage.withLockedValue { $0.services } + } + set { + self.storage.withLockedValue { $0.services = newValue } + } + } + + /// A collection of ``ServerInterceptor`` implementations which are applied to all accepted + /// RPCs. + /// + /// RPCs are intercepted in the order that interceptors are added. That is, a request received + /// from the client will first be intercepted by the first added interceptor followed by the + /// second, and so on. + public var interceptors: Interceptors { + get { + self.storage.withLockedValue { $0.interceptors } + } + set { + self.storage.withLockedValue { $0.interceptors = newValue } + } + } + + /// Underlying storage for the server. + private struct Storage { + var transports: Transports + var services: Services + var interceptors: Interceptors + var state: State + + init() { + self.transports = Transports() + self.services = Services() + self.interceptors = Interceptors() + self.state = .notStarted + } + } + + private let storage: LockedValueBox + + /// The state of the server. + private enum State { + /// The server hasn't been started yet. Can transition to `starting` or `stopped`. + case notStarted + /// The server is starting but isn't accepting requests yet. Can transition to `running` + /// and `stopping`. + case starting + /// The server is running and accepting RPCs. Can transition to `stopping`. + case running + /// The server is stopping and no new RPCs will be accepted. Existing RPCs may run to + /// completion. May transition to `stopped`. + case stopping + /// The server has stopped, no RPCs are in flight and no more will be accepted. This state + /// is terminal. + case stopped + } + + /// Creates a new server with no resources. + /// + /// You can add resources to the server via ``transports-swift.property``, + /// ``services-swift.property``, and ``interceptors-swift.property`` and start the server by + /// calling ``run()``. Any changes to resources after ``run()`` has been called will be ignored. + public init() { + self.storage = LockedValueBox(Storage()) + } + + /// Starts the server and runs until all registered transports have closed. + /// + /// No RPCs are processed until all transports are listening. If a transport fails to start + /// listening then all open transports are closed and a ``ServerError`` is thrown. + /// + /// This function returns when all transports have stopped listening and all requests have been + /// handled. You can signal to transports that they should stop listening by calling + /// ``stopListening()``. The server will continue to process existing requests. + /// + /// To stop the server more abruptly you can cancel the task that this function is running in. + /// + /// You must register all resources you wish to use with the server before calling this function + /// as changes made after calling ``run()`` won't be reflected. + /// + /// - Note: You can only call this function once, repeated calls will result in a + /// ``ServerError`` being thrown. + /// - Important: You must register at least one transport by calling + /// ``Transports-swift.struct/add(_:)`` before calling this method. + public func run() async throws { + let (transports, router, interceptors) = try self.storage.withLockedValue { storage in + switch storage.state { + case .notStarted: + storage.state = .starting + return (storage.transports, storage.services.router, storage.interceptors) + + case .starting, .running: + throw ServerError( + code: .serverIsAlreadyRunning, + message: "The server is already running and can only be started once." + ) + + case .stopping, .stopped: + throw ServerError( + code: .serverIsStopped, + message: "The server has stopped and can only be started once." + ) + } + } + + // When we exit this function we must have stopped. + defer { + self.storage.withLockedValue { $0.state = .stopped } + } + + if transports.values.isEmpty { + throw ServerError( + code: .noTransportsConfigured, + message: """ + Can't start server, no transports are configured. You must add at least one transport \ + to the server using 'transports.add(_:)' before calling 'run()'. + """ + ) + } + + var listeners: [RPCAsyncSequence] = [] + listeners.reserveCapacity(transports.values.count) + + for transport in transports.values { + do { + let listener = try await transport.listen() + listeners.append(listener) + } catch let cause { + // Failed to start, so start stopping. + self.storage.withLockedValue { $0.state = .stopping } + // Some listeners may have started and have streams which need closing. + await Self.rejectRequests(listeners, transports: transports) + + throw ServerError( + code: .failedToStartTransport, + message: """ + Server didn't start because the '\(type(of: transport))' transport threw an error \ + while starting. + """, + cause: cause + ) + } + } + + // May have been told to stop listening while starting the transports. + let isStopping = self.storage.withLockedValue { storage in + switch storage.state { + case .notStarted, .running, .stopped: + fatalError("Invalid state") + + case .starting: + storage.state = .running + return false + + case .stopping: + return true + } + } + + // If the server is stopping then notify the transport and then consume them: there may be + // streams opened at a lower level (e.g. HTTP/2) which are already open and need to be consumed. + if isStopping { + await Self.rejectRequests(listeners, transports: transports) + } else { + await Self.handleRequests(listeners, router: router, interceptors: interceptors) + } + } + + private static func rejectRequests( + _ listeners: [RPCAsyncSequence], + transports: Transports + ) async { + // Tell the active listeners to stop listening. + for transport in transports.values.prefix(listeners.count) { + transport.stopListening() + } + + // Drain any open streams on active listeners. + await withTaskGroup(of: Void.self) { group in + let unavailable = Status( + code: .unavailable, + message: "The server isn't ready to accept requests." + ) + + for listener in listeners { + do { + for try await stream in listener { + group.addTask { + try? await stream.outbound.write(.status(unavailable, [:])) + stream.outbound.finish() + } + } + } catch { + // Suppress any errors, the original error from the transport which failed to start + // should be thrown. + } + } + } + } + + private static func handleRequests( + _ listeners: [RPCAsyncSequence], + router: RPCRouter, + interceptors: Interceptors + ) async { + #if swift(>=5.9) + if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { + await Self.handleRequestsInDiscardingTaskGroup( + listeners, + router: router, + interceptors: interceptors + ) + } else { + await Self.handleRequestsInTaskGroup(listeners, router: router, interceptors: interceptors) + } + #else + await Self.handleRequestsInTaskGroup(listeners, router: router, interceptors: interceptors) + #endif + } + + #if swift(>=5.9) + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + private static func handleRequestsInDiscardingTaskGroup( + _ listeners: [RPCAsyncSequence], + router: RPCRouter, + interceptors: Interceptors + ) async { + await withDiscardingTaskGroup { group in + for listener in listeners { + group.addTask { + await withDiscardingTaskGroup { subGroup in + do { + for try await stream in listener { + subGroup.addTask { + await router.handle(stream: stream, interceptors: interceptors.values) + } + } + } catch { + // If the listener threw then the connection must be broken, cancel all work. + subGroup.cancelAll() + } + } + } + } + } + } + #endif + + private static func handleRequestsInTaskGroup( + _ listeners: [RPCAsyncSequence], + router: RPCRouter, + interceptors: Interceptors + ) async { + // If the discarding task group isn't available then fall back to using a regular task group + // with a limit on subtasks. Most servers will use an HTTP/2 based transport, most + // implementations limit connections to 100 concurrent streams. A limit of 4096 gives the server + // scope to handle nearly 41 completely saturated connections. + let maxConcurrentSubTasks = 4096 + let tasks = ManagedAtomic(0) + + await withTaskGroup(of: Void.self) { group in + for listener in listeners { + group.addTask { + await withTaskGroup(of: Void.self) { subGroup in + do { + for try await stream in listener { + let taskCount = tasks.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) + if taskCount >= maxConcurrentSubTasks { + _ = await subGroup.next() + tasks.wrappingDecrement(ordering: .sequentiallyConsistent) + } + + subGroup.addTask { + await router.handle(stream: stream, interceptors: interceptors.values) + } + } + } catch { + // If the listener threw then the connection must be broken, cancel all work. + subGroup.cancelAll() + } + } + } + } + } + } + + /// Signal to the server that it should stop listening for new requests. + /// + /// By calling this function you indicate to clients that they mustn't start new requests + /// against this server. Once the server has processed all requests the ``run()`` method returns. + /// + /// Calling this on a server which is already stopping or has stopped has no effect. + public func stopListening() { + let transports = self.storage.withLockedValue { storage in + let transports: Transports? + + switch storage.state { + case .notStarted: + storage.state = .stopped + transports = nil + case .starting: + storage.state = .stopping + transports = nil + case .running: + storage.state = .stopping + transports = storage.transports + case .stopping: + transports = nil + case .stopped: + transports = nil + } + + return transports + } + + if let transports = transports?.values { + for transport in transports { + transport.stopListening() + } + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Server { + /// The transports which provide a bidirectional communication channel with clients. + /// + /// You can add a new transport by calling ``add(_:)``. + public struct Transports: Sendable { + private(set) var values: [any (ServerTransport & Sendable)] = [] + + /// Add a transport to the server. + /// + /// - Parameter transport: The transport to add. + public mutating func add(_ transport: some (ServerTransport & Sendable)) { + self.values.append(transport) + } + } + + /// The services registered with this server. + /// + /// You can register services by calling ``register(_:)`` or by manually adding handlers for + /// methods to the ``router``. + public struct Services: Sendable { + /// The router storing handlers for known methods. + public var router = RPCRouter() + + /// Registers service methods with the ``router``. + /// + /// - Parameter service: The service to register with the ``router``. + public mutating func register(_ service: some RegistrableRPCService) { + service.registerMethods(with: &self.router) + } + } + + /// A collection of interceptors providing cross-cutting functionality to each accepted RPC. + public struct Interceptors: Sendable { + private(set) var values: [any ServerInterceptor] = [] + + /// Add an interceptor to the server. + /// + /// The order in which interceptors are added reflects the order in which they are called. The + /// first interceptor added will be the first interceptor to intercept each request. The last + /// interceptor added will be the final interceptor to intercept each request before calling + /// the appropriate handler. + /// + /// - Parameter interceptor: The interceptor to add. + public mutating func add(_ interceptor: some ServerInterceptor) { + self.values.append(interceptor) + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Server.Transports: CustomStringConvertible { + public var description: String { + return String(describing: self.values) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Server.Services: CustomStringConvertible { + public var description: String { + // List the fully qualified all methods ordered by service and then method + let rpcs = self.router.methods.map { $0.fullyQualifiedMethod }.sorted() + return String(describing: rpcs) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Server.Interceptors: CustomStringConvertible { + public var description: String { + return String(describing: self.values.map { String(describing: type(of: $0)) }) + } +} diff --git a/Sources/GRPCCore/ServerError.swift b/Sources/GRPCCore/ServerError.swift new file mode 100644 index 000000000..45e4f4e95 --- /dev/null +++ b/Sources/GRPCCore/ServerError.swift @@ -0,0 +1,148 @@ +/* + * Copyright 2023, 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. + */ + +/// A runtime error thrown by the server. +/// +/// In contrast to ``RPCError``, the ``ServerError`` represents errors which happen at a scope +/// wider than an individual RPC. For example, attempting to start a server which is already +/// stopped would result in a ``ServerError``. +public struct ServerError: Error, Hashable, @unchecked Sendable { + private var storage: Storage + + // Ensures the underlying storage is unique. + private mutating func ensureUniqueStorage() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + /// The code indicating the domain of the error. + public var code: Code { + get { self.storage.code } + set { + self.ensureUniqueStorage() + self.storage.code = newValue + } + } + + /// A message providing more details about the error which may include details specific to this + /// instance of the error. + public var message: String { + get { self.storage.message } + set { + self.ensureUniqueStorage() + self.storage.message = newValue + } + } + + /// The original error which led to this error being thrown. + public var cause: Error? { + get { self.storage.cause } + set { + self.ensureUniqueStorage() + self.storage.cause = newValue + } + } + + /// Creates a new error. + /// + /// - Parameters: + /// - code: The error code. + /// - message: A description of the error. + /// - cause: The original error which led to this error being thrown. + public init(code: Code, message: String, cause: Error? = nil) { + self.storage = Storage(code: code, message: message, cause: cause) + } +} + +extension ServerError: CustomStringConvertible { + public var description: String { + if let cause = self.cause { + return "\(self.code): \"\(self.message)\" (cause: \"\(cause)\")" + } else { + return "\(self.code): \"\(self.message)\"" + } + } +} + +extension ServerError { + private final class Storage: Hashable { + var code: Code + var message: String + var cause: Error? + + init(code: Code, message: String, cause: Error?) { + self.code = code + self.message = message + self.cause = cause + } + + func copy() -> Storage { + return Storage(code: self.code, message: self.message, cause: self.cause) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + } + + static func == (lhs: Storage, rhs: Storage) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message + } + } +} + +extension ServerError { + public struct Code: Hashable, Sendable { + private enum Value { + case serverIsAlreadyRunning + case serverIsStopped + case failedToStartTransport + case noTransportsConfigured + } + + private var value: Value + private init(_ value: Value) { + self.value = value + } + + /// At attempt to start the server was made but it is already running. + public static var serverIsAlreadyRunning: Self { + Self(.serverIsAlreadyRunning) + } + + /// At attempt to start the server was made but it has already stopped. + public static var serverIsStopped: Self { + Self(.serverIsStopped) + } + + /// The server couldn't be started because a transport failed to start. + public static var failedToStartTransport: Self { + Self(.failedToStartTransport) + } + + /// The server couldn't be started because no transports were configured. + public static var noTransportsConfigured: Self { + Self(.noTransportsConfigured) + } + } +} + +extension ServerError.Code: CustomStringConvertible { + public var description: String { + String(describing: self.value) + } +} diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index e538c68f7..3c3dbc45c 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -16,8 +16,8 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol ServerTransport { - associatedtype Inbound: (AsyncSequence & Sendable) where Inbound.Element == RPCRequestPart - associatedtype Outbound: ClosableRPCWriterProtocol + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable /// Starts the transport and returns a sequence of accepted streams to handle. /// diff --git a/Tests/GRPCCoreTests/ServerErrorTests.swift b/Tests/GRPCCoreTests/ServerErrorTests.swift new file mode 100644 index 000000000..afe2b8e2a --- /dev/null +++ b/Tests/GRPCCoreTests/ServerErrorTests.swift @@ -0,0 +1,53 @@ +/* + * Copyright 2023, 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 ServerErrorTests: XCTestCase { + func testCopyOnWrite() { + // ServerError has a heap based storage, so check CoW semantics are correctly implemented. + let error1 = ServerError(code: .failedToStartTransport, message: "Failed to start transport") + var error2 = error1 + error2.code = .serverIsAlreadyRunning + XCTAssertEqual(error1.code, .failedToStartTransport) + XCTAssertEqual(error2.code, .serverIsAlreadyRunning) + + var error3 = error1 + error3.message = "foo" + XCTAssertEqual(error1.message, "Failed to start transport") + XCTAssertEqual(error3.message, "foo") + + var error4 = error1 + error4.cause = CancellationError() + XCTAssertNil(error1.cause) + XCTAssert(error4.cause is CancellationError) + } + + func testCustomStringConvertible() { + let error1 = ServerError(code: .failedToStartTransport, message: "Failed to start transport") + XCTAssertDescription(error1, #"failedToStartTransport: "Failed to start transport""#) + + let error2 = ServerError( + code: .failedToStartTransport, + message: "Failed to start transport", + cause: CancellationError() + ) + XCTAssertDescription( + error2, + #"failedToStartTransport: "Failed to start transport" (cause: "CancellationError()")"# + ) + } +} diff --git a/Tests/GRPCCoreTests/ServerTests.swift b/Tests/GRPCCoreTests/ServerTests.swift new file mode 100644 index 000000000..2fbc2f9be --- /dev/null +++ b/Tests/GRPCCoreTests/ServerTests.swift @@ -0,0 +1,455 @@ +/* + * Copyright 2023, 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 Atomics +import GRPCCore +import XCTest + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class ServerTests: XCTestCase { + func makeInProcessPair() -> (client: InProcessClientTransport, server: InProcessServerTransport) { + let server = InProcessServerTransport() + let client = InProcessClientTransport( + server: server, + executionConfigurations: ClientRPCExecutionConfigurationCollection() + ) + + return (client, server) + } + + func withInProcessClientConnectedToServer( + services: [any RegistrableRPCService], + interceptors: [any ServerInterceptor] = [], + _ body: (InProcessClientTransport, Server) async throws -> Void + ) async throws { + let inProcess = self.makeInProcessPair() + let server = Server() + server.transports.add(inProcess.server) + + for service in services { + server.services.register(service) + } + + for interceptor in interceptors { + server.interceptors.add(interceptor) + } + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.run() + } + + group.addTask { + try await inProcess.client.connect(lazily: true) + } + + try await body(inProcess.client, server) + inProcess.client.close() + server.stopListening() + } + + } + + func testServerHandlesUnary() async throws { + try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in + try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message([3, 1, 4, 1, 5])) + stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + XCTAssertMetadata(metadata) + + let message = try await responseParts.next() + XCTAssertMessage(message) { + XCTAssertEqual($0, [3, 1, 4, 1, 5]) + } + + let status = try await responseParts.next() + XCTAssertStatus(status) { status, _ in + XCTAssertEqual(status.code, .ok) + } + } + } + } + + func testServerHandlesClientStreaming() async throws { + try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in + try await client.withStream(descriptor: BinaryEcho.Methods.collect) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message([3])) + try await stream.outbound.write(.message([1])) + try await stream.outbound.write(.message([4])) + try await stream.outbound.write(.message([1])) + try await stream.outbound.write(.message([5])) + stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + XCTAssertMetadata(metadata) + + let message = try await responseParts.next() + XCTAssertMessage(message) { + XCTAssertEqual($0, [3, 1, 4, 1, 5]) + } + + let status = try await responseParts.next() + XCTAssertStatus(status) { status, _ in + XCTAssertEqual(status.code, .ok) + } + } + } + } + + func testServerHandlesServerStreaming() async throws { + try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in + try await client.withStream(descriptor: BinaryEcho.Methods.expand) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message([3, 1, 4, 1, 5])) + stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + XCTAssertMetadata(metadata) + + for byte in [3, 1, 4, 1, 5] as [UInt8] { + let message = try await responseParts.next() + XCTAssertMessage(message) { + XCTAssertEqual($0, [byte]) + } + } + + let status = try await responseParts.next() + XCTAssertStatus(status) { status, _ in + XCTAssertEqual(status.code, .ok) + } + } + } + } + + func testServerHandlesBidirectionalStreaming() async throws { + try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in + try await client.withStream(descriptor: BinaryEcho.Methods.update) { stream in + try await stream.outbound.write(.metadata([:])) + for byte in [3, 1, 4, 1, 5] as [UInt8] { + try await stream.outbound.write(.message([byte])) + } + stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + XCTAssertMetadata(metadata) + + for byte in [3, 1, 4, 1, 5] as [UInt8] { + let message = try await responseParts.next() + XCTAssertMessage(message) { + XCTAssertEqual($0, [byte]) + } + } + + let status = try await responseParts.next() + XCTAssertStatus(status) { status, _ in + XCTAssertEqual(status.code, .ok) + } + } + } + } + + func testUnimplementedMethod() async throws { + try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in + try await client.withStream( + descriptor: MethodDescriptor(service: "not", method: "implemented") + ) { stream in + try await stream.outbound.write(.metadata([:])) + stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let status = try await responseParts.next() + XCTAssertStatus(status) { status, _ in + XCTAssertEqual(status.code, .unimplemented) + } + } + } + } + + func testMultipleConcurrentRequests() async throws { + try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in + await withThrowingTaskGroup(of: Void.self) { group in + for i in UInt8.min ..< UInt8.max { + group.addTask { + try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message([i])) + stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + XCTAssertMetadata(metadata) + + let message = try await responseParts.next() + XCTAssertMessage(message) { XCTAssertEqual($0, [i]) } + + let status = try await responseParts.next() + XCTAssertStatus(status) { status, _ in + XCTAssertEqual(status.code, .ok) + } + } + } + } + } + } + } + + func testInterceptorsAreAppliedInOrder() async throws { + let counter1 = ManagedAtomic(0) + let counter2 = ManagedAtomic(0) + + try await self.withInProcessClientConnectedToServer( + services: [BinaryEcho()], + interceptors: [ + .requestCounter(counter1), + .rejectAll(with: RPCError(code: .unavailable, message: "")), + .requestCounter(counter2), + ] + ) { client, _ in + try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await stream.outbound.write(.metadata([:])) + stream.outbound.finish() + + let parts = try await stream.inbound.collect() + XCTAssertStatus(parts.first) { status, _ in + XCTAssertEqual(status.code, .unavailable) + } + } + } + + XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) + XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 0) + } + + func testInterceptorsAreNotAppliedToUnimplementedMethods() async throws { + let counter = ManagedAtomic(0) + + try await self.withInProcessClientConnectedToServer( + services: [BinaryEcho()], + interceptors: [.requestCounter(counter)] + ) { client, _ in + try await client.withStream( + descriptor: MethodDescriptor(service: "not", method: "implemented") + ) { stream in + try await stream.outbound.write(.metadata([:])) + stream.outbound.finish() + + let parts = try await stream.inbound.collect() + XCTAssertStatus(parts.first) { status, _ in + XCTAssertEqual(status.code, .unimplemented) + } + } + } + + XCTAssertEqual(counter.load(ordering: .sequentiallyConsistent), 0) + } + + func testNoNewRPCsAfterServerStopListening() async throws { + try await withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, server in + // Run an RPC so we know the server is up. + try await self.doEchoGet(using: client) + + // New streams should fail immediately after this. + server.stopListening() + + // RPC should fail now. + await XCTAssertThrowsRPCErrorAsync { + try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + XCTFail("Stream shouldn't be opened") + } + } errorHandler: { error in + XCTAssertEqual(error.code, .failedPrecondition) + } + } + } + + func testInFlightRPCsCanContinueAfterServerStopListening() async throws { + try await withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, server in + try await client.withStream(descriptor: BinaryEcho.Methods.update) { stream in + try await stream.outbound.write(.metadata([:])) + var iterator = stream.inbound.makeAsyncIterator() + // Don't need to validate the response, just that the server is running. + let metadata = try await iterator.next() + XCTAssertMetadata(metadata) + + // New streams should fail immediately after this. + server.stopListening() + + try await stream.outbound.write(.message([0])) + stream.outbound.finish() + + let message = try await iterator.next() + XCTAssertMessage(message) { XCTAssertEqual($0, [0]) } + let status = try await iterator.next() + XCTAssertStatus(status) + } + } + } + + func testCancelRunningServer() async throws { + let inProcess = self.makeInProcessPair() + let task = Task { + let server = Server() + server.services.register(BinaryEcho()) + server.transports.add(inProcess.server) + try await server.run() + } + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try? await inProcess.client.connect(lazily: true) + } + + try await self.doEchoGet(using: inProcess.client) + // The server must be running at this point as an RPC has completed. + task.cancel() + try await task.value + + group.cancelAll() + } + } + + func testTestRunServerWithNoTransport() async throws { + let server = Server() + await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + try await server.run() + } errorHandler: { error in + XCTAssertEqual(error.code, .noTransportsConfigured) + } + } + + func testTestRunStoppedServer() async throws { + let server = Server() + server.transports.add(InProcessServerTransport()) + // Run the server. + let task = Task { try await server.run() } + task.cancel() + try await task.value + + // Server is stopped, should throw an error. + await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + try await server.run() + } errorHandler: { error in + XCTAssertEqual(error.code, .serverIsStopped) + } + } + + func testRunServerWhenTransportThrows() async throws { + let server = Server() + server.transports.add(ThrowOnRunServerTransport()) + await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + try await server.run() + } errorHandler: { error in + XCTAssertEqual(error.code, .failedToStartTransport) + } + } + + func testRunServerDrainsRunningTransportsWhenOneFailsToStart() async throws { + let server = Server() + + // Register the in process transport first and allow it to come up. + let inProcess = self.makeInProcessPair() + server.transports.add(inProcess.server) + + // Register a transport waits for a signal before throwing. + let signal = AsyncStream.makeStream(of: Void.self) + server.transports.add(ThrowOnSignalServerTransport(signal: signal.stream)) + + // Connect the in process client and start an RPC. When the stream is opened signal the + // other transport to throw. This stream should be failed by the server. + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await inProcess.client.connect(lazily: true) + } + + group.addTask { + try await inProcess.client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + // The stream is open to the in-process transport. Let the other transport start. + signal.continuation.finish() + try await stream.outbound.write(.metadata([:])) + stream.outbound.finish() + + let parts = try await stream.inbound.collect() + XCTAssertStatus(parts.first) { status, _ in + XCTAssertEqual(status.code, .unavailable) + } + } + } + + await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + try await server.run() + } errorHandler: { error in + XCTAssertEqual(error.code, .failedToStartTransport) + } + + group.cancelAll() + } + } + + func testInterceptorsDescription() async throws { + let server = Server() + server.interceptors.add(.rejectAll(with: .init(code: .aborted, message: ""))) + server.interceptors.add(.requestCounter(.init(0))) + let description = String(describing: server.interceptors) + let expected = #"["RejectAllServerInterceptor", "RequestCountingServerInterceptor"]"# + XCTAssertEqual(description, expected) + } + + func testServicesDescription() async throws { + let server = Server() + let methods: [(String, String)] = [ + ("helloworld.Greeter", "SayHello"), + ("echo.Echo", "Foo"), + ("echo.Echo", "Bar"), + ("echo.Echo", "Baz"), + ] + + for (service, method) in methods { + let descriptor = MethodDescriptor(service: service, method: method) + server.services.router.registerHandler( + forMethod: descriptor, + deserializer: IdentityDeserializer(), + serializer: IdentitySerializer() + ) { _ in + fatalError("Unreachable") + } + } + + let description = String(describing: server.services) + let expected = """ + ["echo.Echo/Bar", "echo.Echo/Baz", "echo.Echo/Foo", "helloworld.Greeter/SayHello"] + """ + + XCTAssertEqual(description, expected) + } + + private func doEchoGet(using transport: some ClientTransport) async throws { + try await transport.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message([0])) + stream.outbound.finish() + // Don't need to validate the response, just that the server is running. + let parts = try await stream.inbound.collect() + XCTAssertEqual(parts.count, 3) + } + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift new file mode 100644 index 000000000..6a4ceb07e --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -0,0 +1,104 @@ +/* + * Copyright 2023, 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 + +struct BinaryEcho: RegistrableRPCService { + func get( + _ request: ServerRequest.Single<[UInt8]> + ) async throws -> ServerResponse.Single<[UInt8]> { + ServerResponse.Single(message: request.message, metadata: request.metadata) + } + + func collect( + _ request: ServerRequest.Stream<[UInt8]> + ) async throws -> ServerResponse.Single<[UInt8]> { + let collected = try await request.messages.reduce(into: []) { $0.append(contentsOf: $1) } + return ServerResponse.Single(message: collected, metadata: request.metadata) + } + + func expand( + _ request: ServerRequest.Single<[UInt8]> + ) async throws -> ServerResponse.Stream<[UInt8]> { + return ServerResponse.Stream(metadata: request.metadata) { + for byte in request.message { + try await $0.write([byte]) + } + return [:] + } + } + + func update( + _ request: ServerRequest.Stream<[UInt8]> + ) async throws -> ServerResponse.Stream<[UInt8]> { + return ServerResponse.Stream(metadata: request.metadata) { + for try await message in request.messages { + try await $0.write(message) + } + return [:] + } + } + + func registerMethods(with router: inout RPCRouter) { + let serializer = IdentitySerializer() + let deserializer = IdentityDeserializer() + + router.registerHandler( + forMethod: Methods.get, + deserializer: deserializer, + serializer: serializer + ) { streamRequest in + let singleRequest = try await ServerRequest.Single(stream: streamRequest) + let singleResponse = try await self.get(singleRequest) + return ServerResponse.Stream(single: singleResponse) + } + + router.registerHandler( + forMethod: Methods.collect, + deserializer: deserializer, + serializer: serializer + ) { streamRequest in + let singleResponse = try await self.collect(streamRequest) + return ServerResponse.Stream(single: singleResponse) + } + + router.registerHandler( + forMethod: Methods.expand, + deserializer: deserializer, + serializer: serializer + ) { streamRequest in + let singleRequest = try await ServerRequest.Single(stream: streamRequest) + let streamResponse = try await self.expand(singleRequest) + return streamResponse + } + + router.registerHandler( + forMethod: Methods.update, + deserializer: deserializer, + serializer: serializer + ) { streamRequest in + let streamResponse = try await self.update(streamRequest) + return streamResponse + } + } + + enum Methods { + static let get = MethodDescriptor(service: "echo.Echo", method: "Get") + static let collect = MethodDescriptor(service: "echo.Echo", method: "Collect") + static let expand = MethodDescriptor(service: "echo.Echo", method: "Expand") + static let update = MethodDescriptor(service: "echo.Echo", method: "Update") + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index a80dc023d..0dd1cee9b 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -49,3 +49,36 @@ struct ThrowOnStreamCreationTransport: ClientTransport { throw RPCError(code: self.code, message: "") } } + +struct ThrowOnRunServerTransport: ServerTransport { + func listen() async throws -> RPCAsyncSequence> { + throw RPCError( + code: .unavailable, + message: "The '\(type(of: self))' transport is never available." + ) + } + + func stopListening() { + // no-op + } +} + +struct ThrowOnSignalServerTransport: ServerTransport { + let signal: AsyncStream + + init(signal: AsyncStream) { + self.signal = signal + } + + func listen() async throws -> RPCAsyncSequence> { + for await _ in self.signal {} + throw RPCError( + code: .unavailable, + message: "The '\(type(of: self))' transport is never available." + ) + } + + func stopListening() { + // no-op + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index b85889188..7bc88edef 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -37,6 +37,34 @@ func XCTAssertThrowsErrorAsync( } } +func XCTAssertThrowsError( + ofType: E.Type, + _ expression: @autoclosure () throws -> T, + _ errorHandler: (E) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? E else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + errorHandler(error) + } +} + +func XCTAssertThrowsErrorAsync( + ofType: E.Type = E.self, + _ expression: () async throws -> T, + errorHandler: (E) -> Void +) async { + do { + _ = try await expression() + XCTFail("Expression didn't throw") + } catch let error as E { + errorHandler(error) + } catch { + XCTFail("Error had unexpected type '\(type(of: error))'") + } +} + func XCTAssertThrowsRPCError( _ expression: @autoclosure () throws -> T, _ errorHandler: (RPCError) -> Void @@ -76,6 +104,30 @@ func XCTAssertRejected( } } +func XCTAssertMetadata( + _ part: RPCResponsePart?, + metadataHandler: (Metadata) -> Void = { _ in } +) { + switch part { + case .some(.metadata(let metadata)): + metadataHandler(metadata) + default: + XCTFail("Expected '.metadata' but found '\(String(describing: part))'") + } +} + +func XCTAssertMessage( + _ part: RPCResponsePart?, + messageHandler: ([UInt8]) -> Void = { _ in } +) { + switch part { + case .some(.message(let message)): + messageHandler(message) + default: + XCTFail("Expected '.metadata' but found '\(String(describing: part))'") + } +} + func XCTAssertStatus( _ part: RPCResponsePart?, statusHandler: (Status, Metadata) -> Void = { _, _ in } diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index dd41402d2..52b3f9377 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -520,13 +520,13 @@ final class GRPCChannelPoolTests: GRPCTestCase { func testDelegateCanTellWhenFirstConnectionIsBeingEstablished() { final class State { - private enum _State { + private enum Storage { case idle case connecting case connected } - private var state: _State = .idle + private var state: Storage = .idle private let lock = NIOLock() var isConnected: Bool { From 2971c0ad3415430e4baf59c8bcaea40c521814cb Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Mon, 27 Nov 2023 14:54:36 +0000 Subject: [PATCH 176/580] Add new Timeout type (#1718) --- .../Server/Internal/ServerRPCExecutor.swift | 2 +- Sources/GRPCCore/Internal/Metadata+GRPC.swift | 18 +- Sources/GRPCCore/Timeout.swift | 220 ++++++++++++++++++ Tests/GRPCCoreTests/TimeoutTests.swift | 204 ++++++++++++++++ 4 files changed, 435 insertions(+), 9 deletions(-) create mode 100644 Sources/GRPCCore/Timeout.swift create mode 100644 Tests/GRPCCoreTests/TimeoutTests.swift diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 54b775195..9bf0e75a5 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -76,7 +76,7 @@ struct ServerRPCExecutor { if let timeout = metadata.timeout { group.addTask { let result = await Result { - try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + try await Task.sleep(for: timeout, clock: .continuous) } return .timedOut(result) } diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index f28bfc966..9bff423e3 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -39,14 +39,16 @@ extension Metadata { @inlinable var timeout: Duration? { - // Temporary hack to support tests; only supports nanoseconds. - guard let value = self.firstString(forKey: .timeout) else { return nil } - guard value.utf8.last == UTF8.CodeUnit(ascii: "n") else { return nil } - var index = value.utf8.endIndex - value.utf8.formIndex(before: &index) - guard let digits = String(value.utf8[.. Timeout.maxAmount { + switch roundedUnit { + case .nanoseconds: + roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000) + roundedUnit = .microseconds + case .microseconds: + roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000) + roundedUnit = .milliseconds + case .milliseconds: + roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000) + roundedUnit = .seconds + case .seconds: + roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60) + roundedUnit = .minutes + case .minutes: + roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60) + roundedUnit = .hours + case .hours: + roundedAmount = Timeout.maxAmount + roundedUnit = .hours + } + } + } + + self.init(amount: roundedAmount, unit: roundedUnit) + } + + private static func exceedsDigitLimit(_ value: Int64) -> Bool { + value > Timeout.maxAmount + } + + /// Creates a `GRPCTimeout`. + /// + /// - Precondition: The amount should be greater than or equal to zero and less than or equal + /// to `GRPCTimeout.maxAmount`. + internal init(amount: Int64, unit: Unit) { + precondition((0 ... Timeout.maxAmount).contains(amount)) + + self.amount = amount + self.unit = unit + } +} + +extension Int64 { + /// Returns the quotient of this value when divided by `divisor` rounded up to the nearest + /// multiple of `divisor` if the remainder is non-zero. + /// + /// - Parameter divisor: The value to divide this value by. + fileprivate func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 { + let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor) + return quotient + (remainder != 0 ? 1 : 0) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Duration { + /// Construct a `Duration` given a number of minutes represented as an `Int64`. + /// + /// let d: Duration = .minutes(5) + /// + /// - Returns: A `Duration` representing a given number of minutes. + internal static func minutes(_ minutes: Int64) -> Duration { + return Self.init(secondsComponent: 60 * minutes, attosecondsComponent: 0) + } + + /// Construct a `Duration` given a number of hours represented as an `Int64`. + /// + /// let d: Duration = .hours(3) + /// + /// - Returns: A `Duration` representing a given number of hours. + internal static func hours(_ hours: Int64) -> Duration { + return Self.init(secondsComponent: 60 * 60 * hours, attosecondsComponent: 0) + } + + internal init(amount: Int64, unit: Timeout.Unit) { + switch unit { + case .hours: + self = Self.hours(amount) + case .minutes: + self = Self.minutes(amount) + case .seconds: + self = Self.seconds(amount) + case .milliseconds: + self = Self.milliseconds(amount) + case .microseconds: + self = Self.microseconds(amount) + case .nanoseconds: + self = Self.nanoseconds(amount) + } + } +} diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift new file mode 100644 index 000000000..ddb664bf2 --- /dev/null +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -0,0 +1,204 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCore + +final class TimeoutTests: XCTestCase { + func testDecodeInvalidTimeout_Empty() { + let timeoutHeader = "" + XCTAssertNil(Timeout(decoding: timeoutHeader)) + } + + func testDecodeInvalidTimeout_NoAmount() { + let timeoutHeader = "H" + XCTAssertNil(Timeout(decoding: timeoutHeader)) + } + + func testDecodeInvalidTimeout_NoUnit() { + let timeoutHeader = "123" + XCTAssertNil(Timeout(decoding: timeoutHeader)) + } + + func testDecodeInvalidTimeout_TooLongAmount() { + let timeoutHeader = "100000000S" + XCTAssertNil(Timeout(decoding: timeoutHeader)) + } + + func testDecodeInvalidTimeout_InvalidUnit() { + let timeoutHeader = "123j" + XCTAssertNil(Timeout(decoding: timeoutHeader)) + } + + func testDecodeValidTimeout_Hours() throws { + let timeoutHeader = "123H" + let timeout = Timeout(decoding: timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.hours(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Minutes() throws { + let timeoutHeader = "123M" + let timeout = Timeout(decoding: timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.minutes(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Seconds() throws { + let timeoutHeader = "123S" + let timeout = Timeout(decoding: timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.seconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Milliseconds() throws { + let timeoutHeader = "123m" + let timeout = Timeout(decoding: timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.milliseconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Microseconds() throws { + let timeoutHeader = "123u" + let timeout = Timeout(decoding: timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.microseconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Nanoseconds() throws { + let timeoutHeader = "123n" + let timeout = Timeout(decoding: timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.nanoseconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) + } + + func testEncodeValidTimeout_Hours() { + let duration = Duration.hours(123) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Minutes() { + let duration = Duration.minutes(43) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Seconds() { + let duration = Duration.seconds(12345) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Seconds_TooLong_Minutes() { + let duration = Duration.seconds(111_111_111) + let timeout = Timeout(duration: duration) + // The conversion from seconds to minutes results in a loss of precision. + // 111,111,111 seconds / 60 = 1,851,851.85 minutes -rounding up-> 1,851,852 minutes * 60 = 111,111,120 seconds + let expectedRoundedDuration = Duration.minutes(1_851_852) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) + } + + func testEncodeValidTimeout_Seconds_TooLong_Hours() { + let duration = Duration.seconds(9_999_999_999) + let timeout = Timeout(duration: duration) + // The conversion from seconds to hours results in a loss of precision. + // 9,999,999,999 seconds / 60 = 166,666,666.65 minutes -rounding up-> + // 166,666,667 minutes / 60 = 2,777,777.78 hours -rounding up-> + // 2,777,778 hours * 60 -> 166,666,680 minutes * 60 = 10,000,000,800 seconds + let expectedRoundedDuration = Duration.hours(2_777_778) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) + } + + func testEncodeValidTimeout_Seconds_TooLong_MaxAmount() { + let duration = Duration.seconds(999_999_999_999) + let timeout = Timeout(duration: duration) + // The conversion from seconds to hours results in a number that still has + // more than the maximum allowed 8 digits, so we must clamp it. + // Make sure that `Timeout.maxAmount` is the amount used for the resulting timeout. + let expectedRoundedDuration = Duration.hours(Timeout.maxAmount) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) + } + + func testEncodeValidTimeout_SecondsAndMilliseconds() { + let duration = Duration(secondsComponent: 100, attosecondsComponent: Int64(1e+17)) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_SecondsAndMicroseconds() { + let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+14)) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_SecondsAndNanoseconds() { + let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+11)) + let timeout = Timeout(duration: duration) + // We can't convert seconds to nanoseconds because that would require at least + // 9 digits, and the maximum allowed is 8: we expect to simply drop the nanoseconds. + let expectedRoundedDuration = Duration.seconds(1) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) + } + + func testEncodeValidTimeout_Milliseconds() { + let duration = Duration.milliseconds(100) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Microseconds() { + let duration = Duration.microseconds(100) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Nanoseconds() { + let duration = Duration.nanoseconds(100) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } +} From f88a9c0eb3f0288576b08f04bafe5128bdd42f80 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 27 Nov 2023 18:28:22 +0200 Subject: [PATCH 177/580] Add SourceFile type for CodeGenLib (#1723) Motivation: The CodeRenderer needs to create objects of some type representing the files containing the generated code. This type is SourceFile. Modifications: Implemented the SourceFile struct, containing a name as a String and the generated code as Data. Result: The Code Renderer can now be brought from the OpenAPI generator, as it will have an output type. The code generator will use objects of this type to create the files containing the generated Swift code. --- Sources/GRPCCodeGen/SourceFile.swift | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Sources/GRPCCodeGen/SourceFile.swift diff --git a/Sources/GRPCCodeGen/SourceFile.swift b/Sources/GRPCCodeGen/SourceFile.swift new file mode 100644 index 000000000..c435fb100 --- /dev/null +++ b/Sources/GRPCCodeGen/SourceFile.swift @@ -0,0 +1,32 @@ +/* + * Copyright 2023, 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. + */ + +/// Representation of the file to be created by the code generator, that contains the +/// generated Swift source code. +public struct SourceFile: Sendable, Hashable { + /// The base name of the file. + public var name: String + + /// The generated code as a String. + public var contents: String + + /// Creates a representation of a file containing Swift code with the specified name + /// and contents. + public init(name: String, contents: String) { + self.name = name + self.contents = contents + } +} From 319cdde19af78b4193c67d7392fdc0efc275f538 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 27 Nov 2023 17:42:09 +0000 Subject: [PATCH 178/580] Rename server (#1724) --- .../{Server.swift => GRPCServer.swift} | 12 +++++------ ...erverTests.swift => GRPCServerTests.swift} | 20 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) rename Sources/GRPCCore/{Server.swift => GRPCServer.swift} (98%) rename Tests/GRPCCoreTests/{ServerTests.swift => GRPCServerTests.swift} (97%) diff --git a/Sources/GRPCCore/Server.swift b/Sources/GRPCCore/GRPCServer.swift similarity index 98% rename from Sources/GRPCCore/Server.swift rename to Sources/GRPCCore/GRPCServer.swift index 43a48041f..07c92715e 100644 --- a/Sources/GRPCCore/Server.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -34,7 +34,7 @@ import Atomics /// The following example demonstrates how to create and configure a server. /// /// ```swift -/// let server = Server() +/// let server = GRPCServer() /// /// // Create and add an in-process transport. /// let inProcessTransport = InProcessServerTransport() @@ -66,7 +66,7 @@ import Atomics /// that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public final class Server: Sendable { +public final class GRPCServer: Sendable { typealias Stream = RPCStream /// A collection of ``ServerTransport`` implementations that the server uses to listen @@ -406,7 +406,7 @@ public final class Server: Sendable { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Server { +extension GRPCServer { /// The transports which provide a bidirectional communication channel with clients. /// /// You can add a new transport by calling ``add(_:)``. @@ -456,14 +456,14 @@ extension Server { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Server.Transports: CustomStringConvertible { +extension GRPCServer.Transports: CustomStringConvertible { public var description: String { return String(describing: self.values) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Server.Services: CustomStringConvertible { +extension GRPCServer.Services: CustomStringConvertible { public var description: String { // List the fully qualified all methods ordered by service and then method let rpcs = self.router.methods.map { $0.fullyQualifiedMethod }.sorted() @@ -472,7 +472,7 @@ extension Server.Services: CustomStringConvertible { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Server.Interceptors: CustomStringConvertible { +extension GRPCServer.Interceptors: CustomStringConvertible { public var description: String { return String(describing: self.values.map { String(describing: type(of: $0)) }) } diff --git a/Tests/GRPCCoreTests/ServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift similarity index 97% rename from Tests/GRPCCoreTests/ServerTests.swift rename to Tests/GRPCCoreTests/GRPCServerTests.swift index 2fbc2f9be..e99a46914 100644 --- a/Tests/GRPCCoreTests/ServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -18,7 +18,7 @@ import GRPCCore import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class ServerTests: XCTestCase { +final class GRPCServerTests: XCTestCase { func makeInProcessPair() -> (client: InProcessClientTransport, server: InProcessServerTransport) { let server = InProcessServerTransport() let client = InProcessClientTransport( @@ -32,10 +32,10 @@ final class ServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [], - _ body: (InProcessClientTransport, Server) async throws -> Void + _ body: (InProcessClientTransport, GRPCServer) async throws -> Void ) async throws { let inProcess = self.makeInProcessPair() - let server = Server() + let server = GRPCServer() server.transports.add(inProcess.server) for service in services { @@ -308,7 +308,7 @@ final class ServerTests: XCTestCase { func testCancelRunningServer() async throws { let inProcess = self.makeInProcessPair() let task = Task { - let server = Server() + let server = GRPCServer() server.services.register(BinaryEcho()) server.transports.add(inProcess.server) try await server.run() @@ -329,7 +329,7 @@ final class ServerTests: XCTestCase { } func testTestRunServerWithNoTransport() async throws { - let server = Server() + let server = GRPCServer() await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { try await server.run() } errorHandler: { error in @@ -338,7 +338,7 @@ final class ServerTests: XCTestCase { } func testTestRunStoppedServer() async throws { - let server = Server() + let server = GRPCServer() server.transports.add(InProcessServerTransport()) // Run the server. let task = Task { try await server.run() } @@ -354,7 +354,7 @@ final class ServerTests: XCTestCase { } func testRunServerWhenTransportThrows() async throws { - let server = Server() + let server = GRPCServer() server.transports.add(ThrowOnRunServerTransport()) await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { try await server.run() @@ -364,7 +364,7 @@ final class ServerTests: XCTestCase { } func testRunServerDrainsRunningTransportsWhenOneFailsToStart() async throws { - let server = Server() + let server = GRPCServer() // Register the in process transport first and allow it to come up. let inProcess = self.makeInProcessPair() @@ -406,7 +406,7 @@ final class ServerTests: XCTestCase { } func testInterceptorsDescription() async throws { - let server = Server() + let server = GRPCServer() server.interceptors.add(.rejectAll(with: .init(code: .aborted, message: ""))) server.interceptors.add(.requestCounter(.init(0))) let description = String(describing: server.interceptors) @@ -415,7 +415,7 @@ final class ServerTests: XCTestCase { } func testServicesDescription() async throws { - let server = Server() + let server = GRPCServer() let methods: [(String, String)] = [ ("helloworld.Greeter", "SayHello"), ("echo.Echo", "Foo"), From bd0d0fdbce8ed02965816f913b22b7c26214c339 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:05:59 +0200 Subject: [PATCH 179/580] Added TypeName and TypeUsage from OpenAPI Generator. (#1726) Motivation: The types are used by the code renderer that will be brought over from OpenAPI Generator to the new gRPC Swift CodeGenLib. Modifications: Copied the TypeName, TypeUsage and some TypeName extensions that will be used in the context of gRPC, and modified TypeName so it represents only the Swift path of a type. Result: We will be able to bring over the Code Renderer and we will have types representations that can be used by the service translator. --- NOTICES.txt | 4 +- Sources/GRPCCodeGen/Internal/TypeName.swift | 105 +++++++++ Sources/GRPCCodeGen/Internal/TypeUsage.swift | 232 +++++++++++++++++++ 3 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCCodeGen/Internal/TypeName.swift create mode 100644 Sources/GRPCCodeGen/Internal/TypeUsage.swift diff --git a/NOTICES.txt b/NOTICES.txt index f089e7948..17394ee7c 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -60,9 +60,11 @@ This product uses derivations of swift-extras/swift-extras-base64 'Base64.swift' --- -This product uses derivations of apple/swift-openapi-generator 'StructuredSwiftRepresentation.swift'. +This product uses derivations of apple/swift-openapi-generator 'StructuredSwiftRepresentation.swift', +'TypeName.swift', 'TypeUsage.swift' and 'Builtins.swift'. * LICENSE (Apache License 2.0): * https://github.com/apple/swift-openapi-generator/blob/main/LICENSE.txt * HOMEPAGE: * https://github.com/apple/swift-openapi-generator + diff --git a/Sources/GRPCCodeGen/Internal/TypeName.swift b/Sources/GRPCCodeGen/Internal/TypeName.swift new file mode 100644 index 000000000..4340baa23 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/TypeName.swift @@ -0,0 +1,105 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Foundation + +/// A fully-qualified type name that contains the components of the Swift +/// type name. +/// +/// Use the type name to define a type, see also `TypeUsage` when referring +/// to a type. +struct TypeName: Hashable { + /// A list of components that make up the type name. + private let components: [String] + + /// Creates a new type name with the specified list of components. + /// - Parameter components: A list of components for the type. + init(components: [String]) { + precondition(!components.isEmpty, "TypeName path cannot be empty") + self.components = components + } + + /// A string representation of the fully qualified Swift type name. + /// + /// For example: `Swift.Int`. + var fullyQualifiedName: String { components.joined(separator: ".") } + + /// A string representation of the last path component of the Swift + /// type name. + /// + /// For example: `Int`. + var shortName: String { components.last! } + + /// Returns a type name by appending the specified component to the + /// current type name. + /// + /// In other words, returns a type name for a child type. + /// - Parameters: + /// - component: The name of the Swift type component. + /// - Returns: A new type name. + func appending(component: String) -> Self { + return .init(components: components + [component]) + } + + /// Returns a type name by removing the last component from the current + /// type name. + /// + /// In other words, returns a type name for the parent type. + var parent: TypeName { + precondition(components.count >= 1, "Cannot get the parent of a root type") + return .init(components: components.dropLast()) + } +} + +extension TypeName: CustomStringConvertible { + var description: String { + return fullyQualifiedSwiftName + } +} + +extension TypeName { + /// Returns the type name for the String type. + static var string: Self { .swift("String") } + + /// Returns the type name for the Int type. + static var int: Self { .swift("Int") } + + /// Returns a type name for a type with the specified name in the + /// Swift module. + /// - Parameter name: The name of the type. + /// - Returns: A TypeName representing the specified type within the Swift module. + static func swift(_ name: String) -> TypeName { TypeName(components: ["Swift", name]) } + + /// Returns a type name for a type with the specified name in the + /// Foundation module. + /// - Parameter name: The name of the type. + /// - Returns: A TypeName representing the specified type within the Foundation module. + static func foundation(_ name: String) -> TypeName { + TypeName(components: ["Foundation", name]) + } +} diff --git a/Sources/GRPCCodeGen/Internal/TypeUsage.swift b/Sources/GRPCCodeGen/Internal/TypeUsage.swift new file mode 100644 index 000000000..2fdf0a8fa --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/TypeUsage.swift @@ -0,0 +1,232 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// A reference to a Swift type, including modifiers such as whether the +/// type is wrapped in an optional, an array, or a dictionary. +/// +/// Whenever unsure whether to use `TypeUsage` or `TypeName` in a new API, +/// consider whether you need to define a type or refer to a type. +/// +/// To define a type, use `TypeName`, and to refer to a type, use `TypeUsage`. +/// +/// This type is not meant to represent all the various ways types can be +/// wrapped in Swift, only the ways we wrap things in this project. For example, +/// double optionals (`String??`) are automatically collapsed into a single +/// optional, and so on. +struct TypeUsage { + + /// Describes either a type name or a type usage. + fileprivate indirect enum Wrapped { + + /// A type name, used to define a type. + case name(TypeName) + + /// A type usage, used to refer to a type. + case usage(TypeUsage) + } + + /// The underlying type. + fileprivate var wrapped: Wrapped + + /// Describes the usage of the wrapped type. + fileprivate enum Usage { + + /// An unchanged underlying type. + /// + /// For example: `Wrapped` stays `Wrapped`. + case identity + + /// An optional wrapper for the underlying type. + /// + /// For example: `Wrapped` becomes `Wrapped?`. + case optional + + /// An array wrapped for the underlying type. + /// + /// For example: `Wrapped` becomes `[Wrapped]`. + case array + + /// A dictionary value wrapper for the underlying type. + /// + /// For example: `Wrapped` becomes `[String: Wrapped]`. + case dictionaryValue + + /// A generic type wrapper for the underlying type. + /// + /// For example, `Wrapped` becomes `Wrapper`. + case generic(wrapper: TypeName) + } + + /// The type usage applied to the underlying type. + fileprivate var usage: Usage +} + +extension TypeUsage: CustomStringConvertible { var description: String { fullyQualifiedName } } + +extension TypeUsage { + + /// A Boolean value that indicates whether the type is optional. + var isOptional: Bool { + guard case .optional = usage else { return false } + return true + } + + /// A string representation of the last component of the Swift type name. + /// + /// For example: `Int`. + var shortName: String { + let component: String + switch wrapped { + case let .name(typeName): component = typeName.shortName + case let .usage(usage): component = usage.shortName + } + return applied(to: component) + } + + /// A string representation of the fully qualified type name. + /// + /// For example: `Swift.Int`. + var fullyQualifiedName: String { + let component: String + switch wrapped { + case let .name(typeName): component = typeName.fullyQualifiedName + case let .usage(usage): component = usage.fullyQualifiedName + } + return applied(to: component) + } + + /// A string representation of the fully qualified Swift type name, with + /// any optional wrapping removed. + /// + /// For example: `Swift.Int`. + var fullyQualifiedNonOptionalName: String { withOptional(false).fullyQualifiedName } + + /// Returns a string representation of the type usage applied to the + /// specified Swift path component. + /// - Parameter component: A Swift path component. + /// - Returns: A string representation of the specified Swift path component with the applied type usage. + private func applied(to component: String) -> String { + switch usage { + case .identity: return component + case .optional: return component + "?" + case .array: return "[" + component + "]" + case .dictionaryValue: return "[String: " + component + "]" + case .generic(wrapper: let wrapper): + return "\(wrapper.fullyQualifiedName)<" + component + ">" + } + } + + /// The type name wrapped by the current type usage. + var typeName: TypeName { + switch wrapped { + case .name(let typeName): return typeName + case .usage(let typeUsage): return typeUsage.typeName + } + } + + /// A type usage created by treating the current type usage as an optional + /// type. + var asOptional: Self { + // Don't double wrap optionals + guard !isOptional else { return self } + return TypeUsage(wrapped: .usage(self), usage: .optional) + } + + /// A type usage created by removing the outer type usage wrapper. + private var unwrappedOneLevel: Self { + switch wrapped { + case let .usage(usage): return usage + case let .name(typeName): return typeName.asUsage + } + } + + /// Returns a type usage created by adding or removing an optional wrapper, + /// controlled by the specified parameter. + /// - Parameter isOptional: If `true`, wraps the current type usage in + /// an optional. If `false`, removes a potential optional wrapper from the + /// top level. + /// - Returns: A type usage with the adjusted optionality based on the `isOptional` parameter. + func withOptional(_ isOptional: Bool) -> Self { + if (isOptional && self.isOptional) || (!isOptional && !self.isOptional) { return self } + guard isOptional else { return unwrappedOneLevel } + return asOptional + } + + /// A type usage created by treating the current type usage as the element + /// type of an array. + /// - Returns: A type usage for the array. + var asArray: Self { TypeUsage(wrapped: .usage(self), usage: .array) } + + /// A type usage created by treating the current type usage as the value + /// type of a dictionary. + /// - Returns: A type usage for the dictionary. + var asDictionaryValue: Self { TypeUsage(wrapped: .usage(self), usage: .dictionaryValue) } + + /// A type usage created by wrapping the current type usage inside the + /// wrapper type, where the wrapper type is generic over the current type. + func asWrapped(in wrapper: TypeName) -> Self { + TypeUsage(wrapped: .usage(self), usage: .generic(wrapper: wrapper)) + } +} + +extension TypeName { + + /// A type usage that wraps the current type name without changing it. + var asUsage: TypeUsage { TypeUsage(wrapped: .name(self), usage: .identity) } +} + +extension ExistingTypeDescription { + + /// Creates a new type description from the provided type usage's wrapped + /// value. + /// - Parameter wrapped: The wrapped value. + private init(_ wrapped: TypeUsage.Wrapped) { + switch wrapped { + case .name(let typeName): self = .init(typeName) + case .usage(let typeUsage): self = .init(typeUsage) + } + } + + /// Creates a new type description from the provided type name. + /// - Parameter typeName: A type name. + init(_ typeName: TypeName) { self = .member(typeName.components) } + + /// Creates a new type description from the provided type usage. + /// - Parameter typeUsage: A type usage. + init(_ typeUsage: TypeUsage) { + switch typeUsage.usage { + case .generic(wrapper: let wrapper): + self = .generic(wrapper: .init(wrapper), wrapped: .init(typeUsage.wrapped)) + case .optional: self = .optional(.init(typeUsage.wrapped)) + case .identity: self = .init(typeUsage.wrapped) + case .array: self = .array(.init(typeUsage.wrapped)) + case .dictionaryValue: self = .dictionaryValue(.init(typeUsage.wrapped)) + } + } +} From bd5af62c5049ff11da0900cdddc1476f3a4cef4a Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:10:28 +0200 Subject: [PATCH 180/580] Update StructuredSwiftRepresentation (#1725) Motivation: The latest version of the Code Renderer uses the Expression type for the `left` property of a VariableDescription, so we need to also update the StructuredSwiftRepresentation accordingly. Result: We will have the latest version of StructuredSwiftRepresentation for the new CodeGenLib. --- .../StructuredSwiftRepresentation.swift | 72 ++++++++++++++----- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 74b531366..a328908f2 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project @@ -66,11 +65,14 @@ struct ImportDescription: Equatable, Codable { /// A description of an access modifier. /// /// For example: `public`. -enum AccessModifier: String, Equatable, Codable { +internal enum AccessModifier: String, Sendable, Equatable, Codable { /// A declaration accessible outside of the module. case `public` + /// A declaration accessible outside of the module but only inside the containing package or project. + case `package` + /// A declaration only accessible inside of the module. case `internal` @@ -256,7 +258,7 @@ struct VariableDescription: Equatable, Codable { /// The name of the variable. /// /// For example, in `let foo = 42`, `left` is `foo`. - var left: String + var left: Expression /// The type of the variable. /// @@ -1126,6 +1128,49 @@ extension Declaration { setter: [CodeBlock]? = nil, modify: [CodeBlock]? = nil + ) -> Self { + .variable( + accessModifier: accessModifier, + isStatic: isStatic, + kind: kind, + left: .identifierPattern(left), + type: type, + right: right, + getter: getter, + getterEffects: getterEffects, + setter: setter, + modify: modify + ) + } + + /// A variable declaration. + /// + /// For example: `let foo = 42`. + /// - Parameters: + /// - accessModifier: An access modifier. + /// - isStatic: A Boolean value that indicates whether the variable + /// is static. + /// - kind: The variable binding kind. + /// - left: The name of the variable. + /// - type: The type of the variable. + /// - right: The expression to be assigned to the variable. + /// - getter: Body code for the getter of the variable. + /// - getterEffects: Effects of the getter. + /// - setter: Body code for the setter of the variable. + /// - modify: Body code for the `_modify` accessor. + /// - Returns: Variable declaration. + static func variable( + accessModifier: AccessModifier? = nil, + isStatic: Bool = false, + kind: BindingKind, + left: Expression, + type: ExistingTypeDescription? = nil, + right: Expression? = nil, + getter: [CodeBlock]? = nil, + getterEffects: [FunctionKeyword] = [], + setter: [CodeBlock]? = nil, + modify: [CodeBlock]? = nil + ) -> Self { .variable( .init( @@ -1573,15 +1618,6 @@ extension MemberAccessDescription { static func dot(_ member: String) -> Self { .init(right: member) } } -extension Expression: ExpressibleByStringLiteral, ExpressibleByNilLiteral, ExpressibleByArrayLiteral -{ - init(arrayLiteral elements: Expression...) { self = .literal(.array(elements)) } - - init(stringLiteral value: String) { self = .literal(.string(value)) } - - init(nilLiteral: ()) { self = .literal(.nil) } -} - extension LiteralDescription: ExpressibleByStringLiteral, ExpressibleByNilLiteral, ExpressibleByArrayLiteral { @@ -1599,14 +1635,18 @@ extension VariableDescription { /// For example `var foo = 42`. /// - Parameter name: The name of the variable. /// - Returns: A new mutable variable declaration. - static func `var`(_ name: String) -> Self { Self.init(kind: .var, left: name) } + static func `var`(_ name: String) -> Self { + Self.init(kind: .var, left: .identifierPattern(name)) + } /// Returns a new immutable variable declaration. /// /// For example `let foo = 42`. /// - Parameter name: The name of the variable. /// - Returns: A new immutable variable declaration. - static func `let`(_ name: String) -> Self { Self.init(kind: .let, left: name) } + static func `let`(_ name: String) -> Self { + Self.init(kind: .let, left: .identifierPattern(name)) + } } extension Expression { @@ -1618,10 +1658,6 @@ extension Expression { func equals(_ rhs: Expression) -> AssignmentDescription { .init(left: self, right: rhs) } } -extension FunctionArgumentDescription: ExpressibleByStringLiteral { - init(stringLiteral value: String) { self = .init(expression: .literal(.string(value))) } -} - extension FunctionSignatureDescription { /// Returns a new function signature description that has the access /// modifier updated to the specified one. From 74794ce4c7aa56fc65c6a38e98acd582b4b1038d Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:09:42 +0200 Subject: [PATCH 181/580] Brought CodeRenderer from OpenAPI Generator. (#1728) Motivation: Provides an API which takes Structured Swift Representation of a file and generates an object containing the generated code as a String. Modifications: - Brought over RendererProtocol and TextBasedRenderer. - Brought over unit tests in Test_TextBasedRenderer. - Fixed TypeName bugs that produced errors while building. Result: The CodeGenLib will have a code renderer that transforms `StructuredSwiftRepresentation` into `SourceFile`. --- NOTICES.txt | 3 +- Package.swift | 11 +- .../Internal/Renderer/RendererProtocol.swift | 44 + .../Internal/Renderer/TextBasedRenderer.swift | 937 ++++++++++++++++++ Sources/GRPCCodeGen/Internal/TypeName.swift | 4 +- .../Renderer/TextBasedRendererTests.swift | 781 +++++++++++++++ 6 files changed, 1776 insertions(+), 4 deletions(-) create mode 100644 Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift create mode 100644 Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift diff --git a/NOTICES.txt b/NOTICES.txt index 17394ee7c..f4807b7f9 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -61,7 +61,8 @@ This product uses derivations of swift-extras/swift-extras-base64 'Base64.swift' --- This product uses derivations of apple/swift-openapi-generator 'StructuredSwiftRepresentation.swift', -'TypeName.swift', 'TypeUsage.swift' and 'Builtins.swift'. +'TypeName.swift', 'TypeUsage.swift', 'Builtins.swift', 'RendererProtocol.swift', 'TextBasedProtocol', +and 'Test_TextBasedRenderer'. * LICENSE (Apache License 2.0): * https://github.com/apple/swift-openapi-generator/blob/main/LICENSE.txt diff --git a/Package.swift b/Package.swift index 00a655337..30b1d8782 100644 --- a/Package.swift +++ b/Package.swift @@ -216,7 +216,7 @@ extension Target { .nioEmbedded, .nioTransportServices, .logging, - .reflectionService, + .reflectionService ].appending( .nioSSL, if: includeNIOSSL ), @@ -235,6 +235,13 @@ extension Target { ] ) + static let grpcCodeGenTests: Target = .testTarget( + name: "GRPCCodeGenTests", + dependencies: [ + .grpcCodeGen + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -559,9 +566,11 @@ let package = Package( // v2 .grpcCore, + .grpcCodeGen, // v2 tests .grpcCoreTests, + .grpcCodeGenTests ] ) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift b/Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift new file mode 100644 index 000000000..a08700e65 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift @@ -0,0 +1,44 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// An object that renders structured Swift representations +/// into Swift files. +/// +/// Rendering is the last phase of the generator pipeline. +protocol RendererProtocol { + + /// Renders the specified structured code into a raw Swift file. + /// - Parameters: + /// - code: A structured representation of the Swift code. + /// - config: The configuration of the generator. + /// - diagnostics: The collector to which to emit diagnostics. + /// - Returns: A raw file with Swift contents. + /// - Throws: An error if an issue occurs during rendering. + func render(structured code: StructuredSwiftRepresentation) throws -> SourceFile +} diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift new file mode 100644 index 000000000..0f58e0390 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -0,0 +1,937 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Foundation + +/// An object for building up a generated file line-by-line. +/// +/// After creation, make calls such as `writeLine` to build up the file, +/// and call `rendered` at the end to get the full file contents. +final class StringCodeWriter { + + /// The stored lines of code. + private var lines: [String] + + /// The current nesting level. + private var level: Int + + /// Whether the next call to `writeLine` will continue writing to the last + /// stored line. Otherwise a new line is appended. + private var nextWriteAppendsToLastLine: Bool = false + + /// Creates a new empty writer. + init() { + self.level = 0 + self.lines = [] + } + + /// Concatenates the stored lines of code into a single string. + /// - Returns: The contents of the full file in a single string. + func rendered() -> String { lines.joined(separator: "\n") } + + /// Writes a line of code. + /// + /// By default, a new line is appended to the file. + /// + /// To continue the last line, make a call to `nextLineAppendsToLastLine` + /// before calling `writeLine`. + /// - Parameter line: The contents of the line to write. + func writeLine(_ line: String) { + let newLine: String + if nextWriteAppendsToLastLine && !lines.isEmpty { + let existingLine = lines.removeLast() + newLine = existingLine + line + } else { + let indentation = Array(repeating: " ", count: 4 * level).joined() + newLine = indentation + line + } + lines.append(newLine) + nextWriteAppendsToLastLine = false + } + + /// Increases the indentation level by 1. + func push() { level += 1 } + + /// Decreases the indentation level by 1. + /// - Precondition: Current level must be greater than 0. + func pop() { + precondition(level > 0, "Cannot pop below 0") + level -= 1 + } + + /// Executes the provided closure with one level deeper indentation. + /// - Parameter work: The closure to execute. + /// - Returns: The result of the closure execution. + func withNestedLevel(_ work: () -> R) -> R { + push() + defer { pop() } + return work() + } + + /// Sets a flag on the writer so that the next call to `writeLine` continues + /// the last stored line instead of starting a new line. + /// + /// Safe to call repeatedly, it gets reset by `writeLine`. + func nextLineAppendsToLastLine() { nextWriteAppendsToLastLine = true } +} + +/// A renderer that uses string interpolation and concatenation +/// to convert the provided structure code into raw string form. +struct TextBasedRenderer: RendererProtocol { + + func render( + structured: StructuredSwiftRepresentation + ) throws + -> SourceFile + { + let namedFile = structured.file + renderFile(namedFile.contents) + let string = writer.rendered() + return SourceFile(name: namedFile.name, contents: string) + } + + /// The underlying writer. + private let writer: StringCodeWriter + + /// Creates a new empty renderer. + static var `default`: TextBasedRenderer { .init(writer: StringCodeWriter()) } + + // MARK: - Internals + + /// Returns the current contents of the writer as a string. + func renderedContents() -> String { writer.rendered() } + + /// Renders the specified Swift file. + func renderFile(_ description: FileDescription) { + if let topComment = description.topComment { renderComment(topComment) } + if let imports = description.imports { renderImports(imports) } + for codeBlock in description.codeBlocks { + renderCodeBlock(codeBlock) + writer.writeLine("") + } + } + + /// Renders the specified comment. + func renderComment(_ comment: Comment) { + let prefix: String + let commentString: String + switch comment { + case .inline(let string): + prefix = "//" + commentString = string + case .doc(let string): + prefix = "///" + commentString = string + case .mark(let string, sectionBreak: true): + prefix = "// MARK: -" + commentString = string + case .mark(let string, sectionBreak: false): + prefix = "// MARK:" + commentString = string + } + let lines = commentString.transformingLines { line in + if line.isEmpty { return prefix } + return "\(prefix) \(line)" + } + lines.forEach(writer.writeLine) + } + + /// Renders the specified import statements. + func renderImports(_ imports: [ImportDescription]?) { (imports ?? []).forEach(renderImport) } + + /// Renders a single import statement. + func renderImport(_ description: ImportDescription) { + func render(preconcurrency: Bool) { + let spiPrefix = description.spi.map { "@_spi(\($0)) " } ?? "" + let preconcurrencyPrefix = preconcurrency ? "@preconcurrency " : "" + if let moduleTypes = description.moduleTypes { + for type in moduleTypes { + writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(type)") + } + } else { + writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(description.moduleName)") + } + } + + switch description.preconcurrency { + case .always: render(preconcurrency: true) + case .never: render(preconcurrency: false) + case .onOS(let operatingSystems): + writer.writeLine("#if \(operatingSystems.map { "os(\($0))" }.joined(separator: " || "))") + render(preconcurrency: true) + writer.writeLine("#else") + render(preconcurrency: false) + writer.writeLine("#endif") + } + } + + /// Renders the specified access modifier. + func renderedAccessModifier(_ accessModifier: AccessModifier) -> String { + switch accessModifier { + case .public: return "public" + case .package: return "package" + case .internal: return "internal" + case .fileprivate: return "fileprivate" + case .private: return "private" + } + } + + /// Renders the specified identifier. + func renderedIdentifier(_ identifier: IdentifierDescription) -> String { + switch identifier { + case .pattern(let string): return string + case .type(let existingTypeDescription): + return renderedExistingTypeDescription(existingTypeDescription) + } + } + + /// Renders the specified member access expression. + func renderMemberAccess(_ memberAccess: MemberAccessDescription) { + if let left = memberAccess.left { + renderExpression(left) + writer.nextLineAppendsToLastLine() + } + writer.writeLine(".\(memberAccess.right)") + } + + /// Renders the specified function call argument. + func renderFunctionCallArgument(_ arg: FunctionArgumentDescription) { + if let left = arg.label { + writer.writeLine("\(left): ") + writer.nextLineAppendsToLastLine() + } + renderExpression(arg.expression) + } + + /// Renders the specified function call. + func renderFunctionCall(_ functionCall: FunctionCallDescription) { + renderExpression(functionCall.calledExpression) + writer.nextLineAppendsToLastLine() + writer.writeLine("(") + let arguments = functionCall.arguments + if arguments.count > 1 { + writer.withNestedLevel { + for (argument, isLast) in arguments.enumeratedWithLastMarker() { + renderFunctionCallArgument(argument) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(",") + } + } + } + } else { + writer.nextLineAppendsToLastLine() + if let argument = arguments.first { renderFunctionCallArgument(argument) } + writer.nextLineAppendsToLastLine() + } + writer.writeLine(")") + if let trailingClosure = functionCall.trailingClosure { + writer.nextLineAppendsToLastLine() + writer.writeLine(" ") + renderClosureInvocation(trailingClosure) + } + } + + /// Renders the specified assignment expression. + func renderAssignment(_ assignment: AssignmentDescription) { + renderExpression(assignment.left) + writer.nextLineAppendsToLastLine() + writer.writeLine(" = ") + writer.nextLineAppendsToLastLine() + renderExpression(assignment.right) + } + + /// Renders the specified switch case kind. + func renderSwitchCaseKind(_ kind: SwitchCaseKind) { + switch kind { + case let .`case`(expression, associatedValueNames): + let associatedValues: String + let maybeLet: String + if !associatedValueNames.isEmpty { + associatedValues = "(" + associatedValueNames.joined(separator: ", ") + ")" + maybeLet = "let " + } else { + associatedValues = "" + maybeLet = "" + } + writer.writeLine("case \(maybeLet)") + writer.nextLineAppendsToLastLine() + renderExpression(expression) + writer.nextLineAppendsToLastLine() + writer.writeLine(associatedValues) + case .multiCase(let expressions): + writer.writeLine("case ") + writer.nextLineAppendsToLastLine() + for (expression, isLast) in expressions.enumeratedWithLastMarker() { + renderExpression(expression) + writer.nextLineAppendsToLastLine() + if !isLast { writer.writeLine(", ") } + writer.nextLineAppendsToLastLine() + } + case .`default`: writer.writeLine("default") + } + } + + /// Renders the specified switch case. + func renderSwitchCase(_ switchCase: SwitchCaseDescription) { + renderSwitchCaseKind(switchCase.kind) + writer.nextLineAppendsToLastLine() + writer.writeLine(":") + writer.withNestedLevel { renderCodeBlocks(switchCase.body) } + } + + /// Renders the specified switch expression. + func renderSwitch(_ switchDesc: SwitchDescription) { + writer.writeLine("switch ") + writer.nextLineAppendsToLastLine() + renderExpression(switchDesc.switchedExpression) + writer.nextLineAppendsToLastLine() + writer.writeLine(" {") + for caseDesc in switchDesc.cases { renderSwitchCase(caseDesc) } + writer.writeLine("}") + } + + /// Renders the specified if statement. + func renderIf(_ ifDesc: IfStatementDescription) { + let ifBranch = ifDesc.ifBranch + writer.writeLine("if ") + writer.nextLineAppendsToLastLine() + renderExpression(ifBranch.condition) + writer.nextLineAppendsToLastLine() + writer.writeLine(" {") + writer.withNestedLevel { renderCodeBlocks(ifBranch.body) } + writer.writeLine("}") + for branch in ifDesc.elseIfBranches { + writer.nextLineAppendsToLastLine() + writer.writeLine(" else if ") + writer.nextLineAppendsToLastLine() + renderExpression(branch.condition) + writer.nextLineAppendsToLastLine() + writer.writeLine(" {") + writer.withNestedLevel { renderCodeBlocks(branch.body) } + writer.writeLine("}") + } + if let elseBody = ifDesc.elseBody { + writer.nextLineAppendsToLastLine() + writer.writeLine(" else {") + writer.withNestedLevel { renderCodeBlocks(elseBody) } + writer.writeLine("}") + } + } + + /// Renders the specified switch expression. + func renderDoStatement(_ description: DoStatementDescription) { + writer.writeLine("do {") + writer.withNestedLevel { renderCodeBlocks(description.doStatement) } + if let catchBody = description.catchBody { + writer.writeLine("} catch {") + if !catchBody.isEmpty { + writer.withNestedLevel { renderCodeBlocks(catchBody) } + } else { + writer.nextLineAppendsToLastLine() + } + } + writer.writeLine("}") + } + + /// Renders the specified value binding expression. + func renderValueBinding(_ valueBinding: ValueBindingDescription) { + writer.writeLine("\(renderedBindingKind(valueBinding.kind)) ") + writer.nextLineAppendsToLastLine() + renderFunctionCall(valueBinding.value) + } + + /// Renders the specified keyword. + func renderedKeywordKind(_ kind: KeywordKind) -> String { + switch kind { + case .return: return "return" + case .try(hasPostfixQuestionMark: let hasPostfixQuestionMark): + return "try\(hasPostfixQuestionMark ? "?" : "")" + case .await: return "await" + case .throw: return "throw" + case .yield: return "yield" + } + } + + /// Renders the specified unary keyword expression. + func renderUnaryKeywordExpression(_ expression: UnaryKeywordDescription) { + writer.writeLine(renderedKeywordKind(expression.kind)) + guard let expr = expression.expression else { return } + writer.nextLineAppendsToLastLine() + writer.writeLine(" ") + writer.nextLineAppendsToLastLine() + renderExpression(expr) + } + + /// Renders the specified closure invocation. + func renderClosureInvocation(_ invocation: ClosureInvocationDescription) { + writer.writeLine("{") + if !invocation.argumentNames.isEmpty { + writer.nextLineAppendsToLastLine() + writer.writeLine(" \(invocation.argumentNames.joined(separator: ", ")) in") + } + if let body = invocation.body { writer.withNestedLevel { renderCodeBlocks(body) } } + writer.writeLine("}") + } + + /// Renders the specified binary operator. + func renderedBinaryOperator(_ op: BinaryOperator) -> String { op.rawValue } + + /// Renders the specified binary operation. + func renderBinaryOperation(_ operation: BinaryOperationDescription) { + renderExpression(operation.left) + writer.nextLineAppendsToLastLine() + writer.writeLine(" \(renderedBinaryOperator(operation.operation)) ") + writer.nextLineAppendsToLastLine() + renderExpression(operation.right) + } + + /// Renders the specified inout expression. + func renderInOutDescription(_ description: InOutDescription) { + writer.writeLine("&") + writer.nextLineAppendsToLastLine() + renderExpression(description.referencedExpr) + } + + /// Renders the specified optional chaining expression. + func renderOptionalChainingDescription(_ description: OptionalChainingDescription) { + renderExpression(description.referencedExpr) + writer.nextLineAppendsToLastLine() + writer.writeLine("?") + } + + /// Renders the specified tuple expression. + func renderTupleDescription(_ description: TupleDescription) { + writer.writeLine("(") + writer.nextLineAppendsToLastLine() + let members = description.members + for (member, isLast) in members.enumeratedWithLastMarker() { + renderExpression(member) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(", ") + } + writer.nextLineAppendsToLastLine() + } + writer.writeLine(")") + } + + /// Renders the specified expression. + func renderExpression(_ expression: Expression) { + switch expression { + case .literal(let literalDescription): renderLiteral(literalDescription) + case .identifier(let identifierDescription): + writer.writeLine(renderedIdentifier(identifierDescription)) + case .memberAccess(let memberAccessDescription): renderMemberAccess(memberAccessDescription) + case .functionCall(let functionCallDescription): renderFunctionCall(functionCallDescription) + case .assignment(let assignment): renderAssignment(assignment) + case .switch(let switchDesc): renderSwitch(switchDesc) + case .ifStatement(let ifDesc): renderIf(ifDesc) + case .doStatement(let doStmt): renderDoStatement(doStmt) + case .valueBinding(let valueBinding): renderValueBinding(valueBinding) + case .unaryKeyword(let unaryKeyword): renderUnaryKeywordExpression(unaryKeyword) + case .closureInvocation(let closureInvocation): renderClosureInvocation(closureInvocation) + case .binaryOperation(let binaryOperation): renderBinaryOperation(binaryOperation) + case .inOut(let inOut): renderInOutDescription(inOut) + case .optionalChaining(let optionalChaining): + renderOptionalChainingDescription(optionalChaining) + case .tuple(let tuple): renderTupleDescription(tuple) + } + } + + /// Renders the specified literal expression. + func renderLiteral(_ literal: LiteralDescription) { + func write(_ string: String) { writer.writeLine(string) } + switch literal { + case let .string(string): + // Use a raw literal if the string contains a quote/backslash. + if string.contains("\"") || string.contains("\\") { + write("#\"\(string)\"#") + } else { + write("\"\(string)\"") + } + case let .int(int): write("\(int)") + case let .bool(bool): write(bool ? "true" : "false") + case .nil: write("nil") + case .array(let items): + writer.writeLine("[") + if !items.isEmpty { + writer.withNestedLevel { + for (item, isLast) in items.enumeratedWithLastMarker() { + renderExpression(item) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(",") + } + } + } + } else { + writer.nextLineAppendsToLastLine() + } + writer.writeLine("]") + } + } + + /// Renders the specified where clause requirement. + func renderedWhereClauseRequirement(_ requirement: WhereClauseRequirement) -> String { + switch requirement { + case .conformance(let left, let right): return "\(left): \(right)" + } + } + + /// Renders the specified where clause. + func renderedWhereClause(_ clause: WhereClause) -> String { + let renderedRequirements = clause.requirements.map(renderedWhereClauseRequirement) + return "where \(renderedRequirements.joined(separator: ", "))" + } + + /// Renders the specified extension declaration. + func renderExtension(_ extensionDescription: ExtensionDescription) { + if let accessModifier = extensionDescription.accessModifier { + writer.writeLine(renderedAccessModifier(accessModifier) + " ") + writer.nextLineAppendsToLastLine() + } + writer.writeLine("extension \(extensionDescription.onType)") + writer.nextLineAppendsToLastLine() + if !extensionDescription.conformances.isEmpty { + writer.writeLine(": \(extensionDescription.conformances.joined(separator: ", "))") + writer.nextLineAppendsToLastLine() + } + if let whereClause = extensionDescription.whereClause { + writer.writeLine(" " + renderedWhereClause(whereClause)) + writer.nextLineAppendsToLastLine() + } + writer.writeLine(" {") + for declaration in extensionDescription.declarations { + writer.withNestedLevel { renderDeclaration(declaration) } + } + writer.writeLine("}") + } + + /// Renders the specified type reference to an existing type. + func renderedExistingTypeDescription(_ type: ExistingTypeDescription) -> String { + switch type { + case .any(let existingTypeDescription): + return "any \(renderedExistingTypeDescription(existingTypeDescription))" + case .generic(let wrapper, let wrapped): + return + "\(renderedExistingTypeDescription(wrapper))<\(renderedExistingTypeDescription(wrapped))>" + case .optional(let existingTypeDescription): + return "\(renderedExistingTypeDescription(existingTypeDescription))?" + case .member(let components): return components.joined(separator: ".") + case .array(let existingTypeDescription): + return "[\(renderedExistingTypeDescription(existingTypeDescription))]" + case .dictionaryValue(let existingTypeDescription): + return "[String: \(renderedExistingTypeDescription(existingTypeDescription))]" + } + } + + /// Renders the specified typealias declaration. + func renderTypealias(_ alias: TypealiasDescription) { + var words: [String] = [] + if let accessModifier = alias.accessModifier { + words.append(renderedAccessModifier(accessModifier)) + } + words.append(contentsOf: [ + "typealias", alias.name, "=", renderedExistingTypeDescription(alias.existingType), + ]) + writer.writeLine(words.joinedWords()) + } + + /// Renders the specified binding kind. + func renderedBindingKind(_ kind: BindingKind) -> String { + switch kind { + case .var: return "var" + case .let: return "let" + } + } + + /// Renders the specified variable declaration. + func renderVariable(_ variable: VariableDescription) { + do { + if let accessModifier = variable.accessModifier { + writer.writeLine(renderedAccessModifier(accessModifier) + " ") + writer.nextLineAppendsToLastLine() + } + if variable.isStatic { + writer.writeLine("static ") + writer.nextLineAppendsToLastLine() + } + writer.writeLine(renderedBindingKind(variable.kind) + " ") + writer.nextLineAppendsToLastLine() + renderExpression(variable.left) + if let type = variable.type { + writer.nextLineAppendsToLastLine() + writer.writeLine(": \(renderedExistingTypeDescription(type))") + } + } + + if let right = variable.right { + writer.nextLineAppendsToLastLine() + writer.writeLine(" = ") + writer.nextLineAppendsToLastLine() + renderExpression(right) + } + + if let body = variable.getter { + writer.nextLineAppendsToLastLine() + writer.writeLine(" {") + writer.withNestedLevel { + let hasExplicitGetter = + !variable.getterEffects.isEmpty || variable.setter != nil || variable.modify != nil + if hasExplicitGetter { + let keywords = variable.getterEffects.map(renderedFunctionKeyword).joined(separator: " ") + let line = "get \(keywords) {" + writer.writeLine(line) + writer.push() + } + renderCodeBlocks(body) + if hasExplicitGetter { + writer.pop() + writer.writeLine("}") + } + if let modify = variable.modify { + writer.writeLine("_modify {") + writer.withNestedLevel { renderCodeBlocks(modify) } + writer.writeLine("}") + } + if let setter = variable.setter { + writer.writeLine("set {") + writer.withNestedLevel { renderCodeBlocks(setter) } + writer.writeLine("}") + } + } + writer.writeLine("}") + } + } + + /// Renders the specified struct declaration. + func renderStruct(_ structDesc: StructDescription) { + if let accessModifier = structDesc.accessModifier { + writer.writeLine(renderedAccessModifier(accessModifier) + " ") + writer.nextLineAppendsToLastLine() + } + writer.writeLine("struct \(structDesc.name)") + writer.nextLineAppendsToLastLine() + if !structDesc.conformances.isEmpty { + writer.writeLine(": \(structDesc.conformances.joined(separator: ", "))") + writer.nextLineAppendsToLastLine() + } + writer.writeLine(" {") + if !structDesc.members.isEmpty { + writer.withNestedLevel { for member in structDesc.members { renderDeclaration(member) } } + } else { + writer.nextLineAppendsToLastLine() + } + writer.writeLine("}") + } + + /// Renders the specified protocol declaration. + func renderProtocol(_ protocolDesc: ProtocolDescription) { + if let accessModifier = protocolDesc.accessModifier { + writer.writeLine("\(renderedAccessModifier(accessModifier)) ") + writer.nextLineAppendsToLastLine() + } + writer.writeLine("protocol \(protocolDesc.name)") + writer.nextLineAppendsToLastLine() + if !protocolDesc.conformances.isEmpty { + let conformances = protocolDesc.conformances.joined(separator: ", ") + writer.writeLine(": \(conformances)") + writer.nextLineAppendsToLastLine() + } + writer.writeLine(" {") + if !protocolDesc.members.isEmpty { + writer.withNestedLevel { for member in protocolDesc.members { renderDeclaration(member) } } + } else { + writer.nextLineAppendsToLastLine() + } + writer.writeLine("}") + } + + /// Renders the specified enum declaration. + func renderEnum(_ enumDesc: EnumDescription) { + if enumDesc.isFrozen { + writer.writeLine("@frozen ") + writer.nextLineAppendsToLastLine() + } + if let accessModifier = enumDesc.accessModifier { + writer.writeLine("\(renderedAccessModifier(accessModifier)) ") + writer.nextLineAppendsToLastLine() + } + if enumDesc.isIndirect { + writer.writeLine("indirect ") + writer.nextLineAppendsToLastLine() + } + writer.writeLine("enum \(enumDesc.name)") + writer.nextLineAppendsToLastLine() + if !enumDesc.conformances.isEmpty { + writer.writeLine(": \(enumDesc.conformances.joined(separator: ", "))") + writer.nextLineAppendsToLastLine() + } + writer.writeLine(" {") + if !enumDesc.members.isEmpty { + writer.withNestedLevel { for member in enumDesc.members { renderDeclaration(member) } } + } else { + writer.nextLineAppendsToLastLine() + } + writer.writeLine("}") + } + + /// Renders the specified enum case associated value. + func renderedEnumCaseAssociatedValue(_ value: EnumCaseAssociatedValueDescription) -> String { + var words: [String] = [] + if let label = value.label { words.append(label + ":") } + words.append(renderedExistingTypeDescription(value.type)) + return words.joinedWords() + } + + /// Renders the specified enum case declaration. + func renderEnumCase(_ enumCase: EnumCaseDescription) { + writer.writeLine("case \(enumCase.name)") + switch enumCase.kind { + case .nameOnly: break + case .nameWithRawValue(let rawValue): + writer.nextLineAppendsToLastLine() + writer.writeLine(" = ") + writer.nextLineAppendsToLastLine() + renderLiteral(rawValue) + case .nameWithAssociatedValues(let values): + if values.isEmpty { break } + let associatedValues = values.map(renderedEnumCaseAssociatedValue).joined(separator: ", ") + writer.nextLineAppendsToLastLine() + writer.writeLine("(\(associatedValues))") + } + } + + /// Renders the specified declaration. + func renderDeclaration(_ declaration: Declaration) { + switch declaration { + case let .commentable(comment, nestedDeclaration): + renderCommentableDeclaration(comment: comment, declaration: nestedDeclaration) + case let .deprecated(deprecation, nestedDeclaration): + renderDeprecatedDeclaration(deprecation: deprecation, declaration: nestedDeclaration) + case .variable(let variableDescription): renderVariable(variableDescription) + case .extension(let extensionDescription): renderExtension(extensionDescription) + case .struct(let structDescription): renderStruct(structDescription) + case .protocol(let protocolDescription): renderProtocol(protocolDescription) + case .enum(let enumDescription): renderEnum(enumDescription) + case .typealias(let typealiasDescription): renderTypealias(typealiasDescription) + case .function(let functionDescription): renderFunction(functionDescription) + case .enumCase(let enumCase): renderEnumCase(enumCase) + } + } + + /// Renders the specified function kind. + func renderedFunctionKind(_ functionKind: FunctionKind) -> String { + switch functionKind { + case .initializer(let isFailable): return "init\(isFailable ? "?" : "")" + case .function(let name, let isStatic): return (isStatic ? "static " : "") + "func \(name)" + } + } + + /// Renders the specified function keyword. + func renderedFunctionKeyword(_ keyword: FunctionKeyword) -> String { + switch keyword { + case .throws: return "throws" + case .async: return "async" + } + } + + /// Renders the specified function signature. + func renderFunctionSignature(_ signature: FunctionSignatureDescription) { + do { + if let accessModifier = signature.accessModifier { + writer.writeLine(renderedAccessModifier(accessModifier) + " ") + writer.nextLineAppendsToLastLine() + } + writer.writeLine(renderedFunctionKind(signature.kind) + "(") + let parameters = signature.parameters + let separateLines = parameters.count > 1 + if separateLines { + writer.withNestedLevel { + for (parameter, isLast) in signature.parameters.enumeratedWithLastMarker() { + renderParameter(parameter) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(",") + } + } + } + } else { + writer.nextLineAppendsToLastLine() + if let parameter = parameters.first { renderParameter(parameter) } + writer.nextLineAppendsToLastLine() + } + writer.writeLine(")") + } + + do { + let keywords = signature.keywords + if !keywords.isEmpty { + for keyword in keywords { + writer.nextLineAppendsToLastLine() + writer.writeLine(" " + renderedFunctionKeyword(keyword)) + } + } + } + + if let returnType = signature.returnType { + writer.nextLineAppendsToLastLine() + writer.writeLine(" -> ") + writer.nextLineAppendsToLastLine() + renderExpression(returnType) + } + } + + /// Renders the specified function declaration. + func renderFunction(_ functionDescription: FunctionDescription) { + renderFunctionSignature(functionDescription.signature) + guard let body = functionDescription.body else { return } + writer.nextLineAppendsToLastLine() + writer.writeLine(" {") + if !body.isEmpty { + writer.withNestedLevel { renderCodeBlocks(body) } + } else { + writer.nextLineAppendsToLastLine() + } + writer.writeLine("}") + } + + /// Renders the specified parameter declaration. + func renderParameter(_ parameterDescription: ParameterDescription) { + if let label = parameterDescription.label { + writer.writeLine(label) + } else { + writer.writeLine("_") + } + writer.nextLineAppendsToLastLine() + if let name = parameterDescription.name, name != parameterDescription.label { + // If the label and name are the same value, don't repeat it. + writer.writeLine(" ") + writer.nextLineAppendsToLastLine() + writer.writeLine(name) + writer.nextLineAppendsToLastLine() + } + writer.writeLine(": ") + writer.nextLineAppendsToLastLine() + writer.writeLine(renderedExistingTypeDescription(parameterDescription.type)) + if let defaultValue = parameterDescription.defaultValue { + writer.nextLineAppendsToLastLine() + writer.writeLine(" = ") + writer.nextLineAppendsToLastLine() + renderExpression(defaultValue) + } + } + + /// Renders the specified declaration with a comment. + func renderCommentableDeclaration(comment: Comment?, declaration: Declaration) { + if let comment { renderComment(comment) } + renderDeclaration(declaration) + } + + /// Renders the specified declaration with a deprecation annotation. + func renderDeprecatedDeclaration(deprecation: DeprecationDescription, declaration: Declaration) { + renderDeprecation(deprecation) + renderDeclaration(declaration) + } + + func renderDeprecation(_ deprecation: DeprecationDescription) { + let things: [String] = [ + "*", "deprecated", deprecation.message.map { "message: \"\($0)\"" }, + deprecation.renamed.map { "renamed: \"\($0)\"" }, + ] + .compactMap({ $0 }) + let line = "@available(\(things.joined(separator: ", ")))" + writer.writeLine(line) + } + + /// Renders the specified code block item. + func renderCodeBlockItem(_ description: CodeBlockItem) { + switch description { + case .declaration(let declaration): renderDeclaration(declaration) + case .expression(let expression): renderExpression(expression) + } + } + + /// Renders the specified code block. + func renderCodeBlock(_ description: CodeBlock) { + if let comment = description.comment { renderComment(comment) } + let item = description.item + renderCodeBlockItem(item) + } + + /// Renders the specified code blocks. + func renderCodeBlocks(_ blocks: [CodeBlock]) { blocks.forEach(renderCodeBlock) } +} + +extension Array { + + /// Returns a collection of tuples, where the first element is + /// the collection element and the second is a Boolean value indicating + /// whether it is the last element in the collection. + /// - Returns: A collection of tuples. + fileprivate func enumeratedWithLastMarker() -> [(Element, isLast: Bool)] { + let count = count + return enumerated().map { index, element in (element, index == count - 1) } + } +} + +extension Array where Element == String { + /// Returns a string where the elements of the array are joined + /// by a space character. + /// - Returns: A string with the elements of the array joined by space characters. + fileprivate func joinedWords() -> String { joined(separator: " ") } +} + +extension String { + + /// Returns an array of strings, where each string represents one line + /// in the current string. + /// - Returns: An array of strings, each representing one line in the original string. + fileprivate func asLines() -> [String] { + split(omittingEmptySubsequences: false, whereSeparator: \.isNewline).map(String.init) + } + + /// Returns a new string where the provided closure transforms each line. + /// The closure takes a string representing one line as a parameter. + /// - Parameter work: The closure that transforms each line. + /// - Returns: A new string where each line has been transformed using the given closure. + fileprivate func transformingLines(_ work: (String) -> String) -> [String] { asLines().map(work) } +} + +extension TextBasedRenderer { + + /// Returns the provided expression rendered as a string. + /// - Parameter expression: The expression. + /// - Returns: The string representation of the expression. + static func renderedExpressionAsString(_ expression: Expression) -> String { + let renderer = TextBasedRenderer.default + renderer.renderExpression(expression) + return renderer.renderedContents() + } +} diff --git a/Sources/GRPCCodeGen/Internal/TypeName.swift b/Sources/GRPCCodeGen/Internal/TypeName.swift index 4340baa23..0152de6a0 100644 --- a/Sources/GRPCCodeGen/Internal/TypeName.swift +++ b/Sources/GRPCCodeGen/Internal/TypeName.swift @@ -35,7 +35,7 @@ import Foundation /// to a type. struct TypeName: Hashable { /// A list of components that make up the type name. - private let components: [String] + internal let components: [String] /// Creates a new type name with the specified list of components. /// - Parameter components: A list of components for the type. @@ -78,7 +78,7 @@ struct TypeName: Hashable { extension TypeName: CustomStringConvertible { var description: String { - return fullyQualifiedSwiftName + return fullyQualifiedName } } diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift new file mode 100644 index 000000000..bbe99de1c --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -0,0 +1,781 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest + +@testable import GRPCCodeGen + +final class Test_TextBasedRenderer: XCTestCase { + + func testComment() throws { + try _test( + .inline( + #""" + Generated by foo + + Also, bar + """# + ), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: #""" + // Generated by foo + // + // Also, bar + """# + ) + try _test( + .doc( + #""" + Generated by foo + + Also, bar + """# + ), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: #""" + /// Generated by foo + /// + /// Also, bar + """# + ) + try _test( + .mark("Lorem ipsum", sectionBreak: false), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: #""" + // MARK: Lorem ipsum + """# + ) + try _test( + .mark("Lorem ipsum", sectionBreak: true), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: #""" + // MARK: - Lorem ipsum + """# + ) + try _test( + .inline( + """ + Generated by foo\r\nAlso, bar + """ + ), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: #""" + // Generated by foo + // Also, bar + """# + ) + } + + func testImports() throws { + try _test(nil, renderedBy: TextBasedRenderer.renderImports, rendersAs: "") + try _test( + [ImportDescription(moduleName: "Foo"), ImportDescription(moduleName: "Bar")], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + import Foo + import Bar + """# + ) + try _test( + [ImportDescription(moduleName: "Foo", spi: "Secret")], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + @_spi(Secret) import Foo + """# + ) + try _test( + [ImportDescription(moduleName: "Foo", preconcurrency: .onOS(["Bar", "Baz"]))], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + #if os(Bar) || os(Baz) + @preconcurrency import Foo + #else + import Foo + #endif + """# + ) + try _test( + [ + ImportDescription(moduleName: "Foo", preconcurrency: .always), + ImportDescription(moduleName: "Bar", spi: "Secret", preconcurrency: .always), + ], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + @preconcurrency import Foo + @preconcurrency @_spi(Secret) import Bar + """# + ) + } + + func testAccessModifiers() throws { + try _test( + .public, + renderedBy: TextBasedRenderer.renderedAccessModifier, + rendersAs: #""" + public + """# + ) + try _test( + .internal, + renderedBy: TextBasedRenderer.renderedAccessModifier, + rendersAs: #""" + internal + """# + ) + try _test( + .fileprivate, + renderedBy: TextBasedRenderer.renderedAccessModifier, + rendersAs: #""" + fileprivate + """# + ) + try _test( + .private, + renderedBy: TextBasedRenderer.renderedAccessModifier, + rendersAs: #""" + private + """# + ) + } + + func testLiterals() throws { + try _test( + .string("hi"), + renderedBy: TextBasedRenderer.renderLiteral, + rendersAs: #""" + "hi" + """# + ) + try _test( + .string("this string: \"foo\""), + renderedBy: TextBasedRenderer.renderLiteral, + rendersAs: #""" + #"this string: "foo""# + """# + ) + try _test( + .nil, + renderedBy: TextBasedRenderer.renderLiteral, + rendersAs: #""" + nil + """# + ) + try _test( + .array([]), + renderedBy: TextBasedRenderer.renderLiteral, + rendersAs: #""" + [] + """# + ) + try _test( + .array([.literal(.nil)]), + renderedBy: TextBasedRenderer.renderLiteral, + rendersAs: #""" + [ + nil + ] + """# + ) + try _test( + .array([.literal(.nil), .literal(.nil)]), + renderedBy: TextBasedRenderer.renderLiteral, + rendersAs: #""" + [ + nil, + nil + ] + """# + ) + } + + func testExpression() throws { + try _test( + .literal(.nil), + renderedBy: TextBasedRenderer.renderExpression, + rendersAs: #""" + nil + """# + ) + try _test( + .identifierPattern("foo"), + renderedBy: TextBasedRenderer.renderExpression, + rendersAs: #""" + foo + """# + ) + try _test( + .memberAccess(.init(left: .identifierPattern("foo"), right: "bar")), + renderedBy: TextBasedRenderer.renderExpression, + rendersAs: #""" + foo.bar + """# + ) + try _test( + .functionCall( + .init( + calledExpression: .identifierPattern("callee"), + arguments: [.init(label: nil, expression: .identifierPattern("foo"))] + ) + ), + renderedBy: TextBasedRenderer.renderExpression, + rendersAs: #""" + callee(foo) + """# + ) + } + + func testDeclaration() throws { + try _test( + .variable(kind: .let, left: "foo"), + renderedBy: TextBasedRenderer.renderDeclaration, + rendersAs: #""" + let foo + """# + ) + try _test( + .extension(.init(onType: "String", declarations: [])), + renderedBy: TextBasedRenderer.renderDeclaration, + rendersAs: #""" + extension String { + } + """# + ) + try _test( + .struct(.init(name: "Foo")), + renderedBy: TextBasedRenderer.renderDeclaration, + rendersAs: #""" + struct Foo {} + """# + ) + try _test( + .protocol(.init(name: "Foo")), + renderedBy: TextBasedRenderer.renderDeclaration, + rendersAs: #""" + protocol Foo {} + """# + ) + try _test( + .enum(.init(name: "Foo")), + renderedBy: TextBasedRenderer.renderDeclaration, + rendersAs: #""" + enum Foo {} + """# + ) + try _test( + .typealias(.init(name: "foo", existingType: .member(["Foo", "Bar"]))), + renderedBy: TextBasedRenderer.renderDeclaration, + rendersAs: #""" + typealias foo = Foo.Bar + """# + ) + try _test( + .function(FunctionDescription.init(kind: .function(name: "foo"), body: [])), + renderedBy: TextBasedRenderer.renderDeclaration, + rendersAs: #""" + func foo() {} + """# + ) + } + + func testFunctionKind() throws { + try _test( + .initializer, + renderedBy: TextBasedRenderer.renderedFunctionKind, + rendersAs: #""" + init + """# + ) + try _test( + .function(name: "funky"), + renderedBy: TextBasedRenderer.renderedFunctionKind, + rendersAs: #""" + func funky + """# + ) + try _test( + .function(name: "funky", isStatic: true), + renderedBy: TextBasedRenderer.renderedFunctionKind, + rendersAs: #""" + static func funky + """# + ) + } + + func testFunctionKeyword() throws { + try _test( + .throws, + renderedBy: TextBasedRenderer.renderedFunctionKeyword, + rendersAs: #""" + throws + """# + ) + try _test( + .async, + renderedBy: TextBasedRenderer.renderedFunctionKeyword, + rendersAs: #""" + async + """# + ) + } + + func testParameter() throws { + try _test( + .init(label: "l", name: "n", type: .member("T"), defaultValue: .literal(.nil)), + renderedBy: TextBasedRenderer.renderParameter, + rendersAs: #""" + l n: T = nil + """# + ) + try _test( + .init(label: nil, name: "n", type: .member("T"), defaultValue: .literal(.nil)), + renderedBy: TextBasedRenderer.renderParameter, + rendersAs: #""" + _ n: T = nil + """# + ) + try _test( + .init(label: "l", name: nil, type: .member("T"), defaultValue: .literal(.nil)), + renderedBy: TextBasedRenderer.renderParameter, + rendersAs: #""" + l: T = nil + """# + ) + try _test( + .init(label: nil, name: nil, type: .member("T"), defaultValue: .literal(.nil)), + renderedBy: TextBasedRenderer.renderParameter, + rendersAs: #""" + _: T = nil + """# + ) + try _test( + .init(label: nil, name: nil, type: .member("T"), defaultValue: nil), + renderedBy: TextBasedRenderer.renderParameter, + rendersAs: #""" + _: T + """# + ) + } + + func testFunction() throws { + try _test( + .init(accessModifier: .public, kind: .function(name: "f"), parameters: [], body: []), + renderedBy: TextBasedRenderer.renderFunction, + rendersAs: #""" + public func f() {} + """# + ) + try _test( + .init( + accessModifier: .public, + kind: .function(name: "f"), + parameters: [.init(label: "a", name: "b", type: .member("C"), defaultValue: nil)], + body: [] + ), + renderedBy: TextBasedRenderer.renderFunction, + rendersAs: #""" + public func f(a b: C) {} + """# + ) + try _test( + .init( + accessModifier: .public, + kind: .function(name: "f"), + parameters: [ + .init(label: "a", name: "b", type: .member("C"), defaultValue: nil), + .init(label: nil, name: "d", type: .member("E"), defaultValue: .literal(.string("f"))), + ], + body: [] + ), + renderedBy: TextBasedRenderer.renderFunction, + rendersAs: #""" + public func f( + a b: C, + _ d: E = "f" + ) {} + """# + ) + try _test( + .init( + kind: .function(name: "f"), + parameters: [], + keywords: [.async, .throws], + returnType: .identifierType(TypeName.string) + ), + renderedBy: TextBasedRenderer.renderFunction, + rendersAs: #""" + func f() async throws -> Swift.String + """# + ) + } + + func testIdentifiers() throws { + try _test( + .pattern("foo"), + renderedBy: TextBasedRenderer.renderedIdentifier, + rendersAs: #""" + foo + """# + ) + } + + func testMemberAccess() throws { + try _test( + .init(left: .identifierPattern("foo"), right: "bar"), + renderedBy: TextBasedRenderer.renderMemberAccess, + rendersAs: #""" + foo.bar + """# + ) + try _test( + .init(left: nil, right: "bar"), + renderedBy: TextBasedRenderer.renderMemberAccess, + rendersAs: #""" + .bar + """# + ) + } + + func testFunctionCallArgument() throws { + try _test( + .init(label: "foo", expression: .identifierPattern("bar")), + renderedBy: TextBasedRenderer.renderFunctionCallArgument, + rendersAs: #""" + foo: bar + """# + ) + try _test( + .init(label: nil, expression: .identifierPattern("bar")), + renderedBy: TextBasedRenderer.renderFunctionCallArgument, + rendersAs: #""" + bar + """# + ) + } + + func testFunctionCall() throws { + try _test( + .functionCall(.init(calledExpression: .identifierPattern("callee"))), + renderedBy: TextBasedRenderer.renderExpression, + rendersAs: #""" + callee() + """# + ) + try _test( + .functionCall( + .init( + calledExpression: .identifierPattern("callee"), + arguments: [.init(label: "foo", expression: .identifierPattern("bar"))] + ) + ), + renderedBy: TextBasedRenderer.renderExpression, + rendersAs: #""" + callee(foo: bar) + """# + ) + try _test( + .functionCall( + .init( + calledExpression: .identifierPattern("callee"), + arguments: [ + .init(label: "foo", expression: .identifierPattern("bar")), + .init(label: "baz", expression: .identifierPattern("boo")), + ] + ) + ), + renderedBy: TextBasedRenderer.renderExpression, + rendersAs: #""" + callee( + foo: bar, + baz: boo + ) + """# + ) + } + + func testExtension() throws { + try _test( + .init( + accessModifier: .public, + onType: "Info", + declarations: [.variable(kind: .let, left: "foo", type: .member("Int"))] + ), + renderedBy: TextBasedRenderer.renderExtension, + rendersAs: #""" + public extension Info { + let foo: Int + } + """# + ) + } + + func testDeprecation() throws { + try _test( + .init(), + renderedBy: TextBasedRenderer.renderDeprecation, + rendersAs: #""" + @available(*, deprecated) + """# + ) + try _test( + .init(message: "some message"), + renderedBy: TextBasedRenderer.renderDeprecation, + rendersAs: #""" + @available(*, deprecated, message: "some message") + """# + ) + try _test( + .init(renamed: "newSymbol(param:)"), + renderedBy: TextBasedRenderer.renderDeprecation, + rendersAs: #""" + @available(*, deprecated, renamed: "newSymbol(param:)") + """# + ) + try _test( + .init(message: "some message", renamed: "newSymbol(param:)"), + renderedBy: TextBasedRenderer.renderDeprecation, + rendersAs: #""" + @available(*, deprecated, message: "some message", renamed: "newSymbol(param:)") + """# + ) + } + + func testBindingKind() throws { + try _test( + .var, + renderedBy: TextBasedRenderer.renderedBindingKind, + rendersAs: #""" + var + """# + ) + try _test( + .let, + renderedBy: TextBasedRenderer.renderedBindingKind, + rendersAs: #""" + let + """# + ) + } + + func testVariable() throws { + try _test( + .init( + accessModifier: .public, + isStatic: true, + kind: .let, + left: .identifierPattern("foo"), + type: .init(TypeName.string), + right: .literal(.string("bar")) + ), + renderedBy: TextBasedRenderer.renderVariable, + rendersAs: #""" + public static let foo: Swift.String = "bar" + """# + ) + try _test( + .init( + accessModifier: .internal, + isStatic: false, + kind: .var, + left: .identifierPattern("foo"), + type: nil, + right: nil + ), + renderedBy: TextBasedRenderer.renderVariable, + rendersAs: #""" + internal var foo + """# + ) + try _test( + .init( + kind: .var, + left: .identifierPattern("foo"), + type: .init(TypeName.int), + getter: [CodeBlock.expression(.literal(.int(42)))] + ), + renderedBy: TextBasedRenderer.renderVariable, + rendersAs: #""" + var foo: Swift.Int { + 42 + } + """# + ) + try _test( + .init( + kind: .var, + left: .identifierPattern("foo"), + type: .init(TypeName.int), + getter: [CodeBlock.expression(.literal(.int(42)))], + getterEffects: [.throws] + ), + renderedBy: TextBasedRenderer.renderVariable, + rendersAs: #""" + var foo: Swift.Int { + get throws { + 42 + } + } + """# + ) + } + + func testStruct() throws { + try _test( + .init(name: "Structy"), + renderedBy: TextBasedRenderer.renderStruct, + rendersAs: #""" + struct Structy {} + """# + ) + } + + func testProtocol() throws { + try _test( + .init(name: "Protocoly"), + renderedBy: TextBasedRenderer.renderProtocol, + rendersAs: #""" + protocol Protocoly {} + """# + ) + } + + func testEnum() throws { + try _test( + .init(name: "Enumy"), + renderedBy: TextBasedRenderer.renderEnum, + rendersAs: #""" + enum Enumy {} + """# + ) + } + + func testCodeBlockItem() throws { + try _test( + .declaration(.variable(kind: .let, left: "foo")), + renderedBy: TextBasedRenderer.renderCodeBlockItem, + rendersAs: #""" + let foo + """# + ) + try _test( + .expression(.literal(.nil)), + renderedBy: TextBasedRenderer.renderCodeBlockItem, + rendersAs: #""" + nil + """# + ) + } + + func testCodeBlock() throws { + try _test( + .init( + comment: .inline("- MARK: Section"), + item: .declaration(.variable(kind: .let, left: "foo")) + ), + renderedBy: TextBasedRenderer.renderCodeBlock, + rendersAs: #""" + // - MARK: Section + let foo + """# + ) + try _test( + .init(comment: nil, item: .declaration(.variable(kind: .let, left: "foo"))), + renderedBy: TextBasedRenderer.renderCodeBlock, + rendersAs: #""" + let foo + """# + ) + } + + func testTypealias() throws { + try _test( + .init(name: "inty", existingType: .member("Int")), + renderedBy: TextBasedRenderer.renderTypealias, + rendersAs: #""" + typealias inty = Int + """# + ) + try _test( + .init(accessModifier: .private, name: "inty", existingType: .member("Int")), + renderedBy: TextBasedRenderer.renderTypealias, + rendersAs: #""" + private typealias inty = Int + """# + ) + } + + func testFile() throws { + try _test( + .init( + topComment: .inline("hi"), + imports: [.init(moduleName: "Foo")], + codeBlocks: [.init(comment: nil, item: .declaration(.struct(.init(name: "Bar"))))] + ), + renderedBy: TextBasedRenderer.renderFile, + rendersAs: #""" + // hi + import Foo + struct Bar {} + + """# + ) + } +} + +extension Test_TextBasedRenderer { + + func _test( + _ input: Input, + renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> String), + rendersAs output: String, + file: StaticString = #file, + line: UInt = #line + ) throws { + let renderer = TextBasedRenderer.default + XCTAssertEqual(renderClosure(renderer)(input), output, file: file, line: line) + } + + func _test( + _ input: Input, + renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> Void), + rendersAs output: String, + file: StaticString = #file, + line: UInt = #line + ) throws { + try _test( + input, + renderedBy: { renderer in + let closure = renderClosure(renderer) + return { input in + closure(input) + return renderer.renderedContents() + } + }, + rendersAs: output + ) + } +} From 39c2f4f378afdbc1b88d00359bae6a537c7af072 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 4 Dec 2023 10:44:07 +0000 Subject: [PATCH 182/580] Add Client object (#1729) Co-authored-by: Gus Cairo --- ...tRPCExecutionConfigurationCollection.swift | 76 --- .../Client/Internal/ClientRPCExecutor.swift | 2 +- Sources/GRPCCore/Call/Server/RPCRouter.swift | 2 +- Sources/GRPCCore/ClientError.swift | 148 ++++++ Sources/GRPCCore/GRPCClient.swift | 473 ++++++++++++++++++ Sources/GRPCCore/GRPCServer.swift | 2 +- ...ration.swift => MethodConfiguration.swift} | 4 +- Sources/GRPCCore/MethodConfigurations.swift | 92 ++++ .../GRPCCore/Transport/ClientTransport.swift | 6 +- .../Transport/InProcessClientTransport.swift | 6 +- ...ientRPCExecutorTestHarness+Transport.swift | 10 +- .../ClientRPCExecutorTestHarness.swift | 17 +- .../ClientRPCExecutorTests+Hedging.swift | 4 +- .../ClientRPCExecutorTests+Retries.swift | 4 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 448 +++++++++++++++++ Tests/GRPCCoreTests/GRPCServerTests.swift | 3 +- ...s.swift => MethodConfigurationTests.swift} | 2 +- ....swift => MethodConfigurationsTests.swift} | 31 +- .../Call/Client/ClientInterceptors.swift | 84 ++++ .../Call/Server/ServerInterceptors.swift | 3 +- .../Transport/AnyTransport.swift | 4 +- .../Transport/StreamCountingTransport.swift | 2 +- .../Transport/ThrowingTransport.swift | 2 +- .../Test Utilities/XCTest+Utilities.swift | 38 +- .../InProcessClientTransportTests.swift | 19 +- 25 files changed, 1339 insertions(+), 143 deletions(-) delete mode 100644 Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift create mode 100644 Sources/GRPCCore/ClientError.swift create mode 100644 Sources/GRPCCore/GRPCClient.swift rename Sources/GRPCCore/{Call/Client/ClientRPCExecutionConfiguration.swift => MethodConfiguration.swift} (98%) create mode 100644 Sources/GRPCCore/MethodConfigurations.swift create mode 100644 Tests/GRPCCoreTests/GRPCClientTests.swift rename Tests/GRPCCoreTests/{Call/Client/ClientRPCExecutionConfigurationTests.swift => MethodConfigurationTests.swift} (96%) rename Tests/GRPCCoreTests/{Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift => MethodConfigurationsTests.swift} (72%) create mode 100644 Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift diff --git a/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift b/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift deleted file mode 100644 index 9207fd757..000000000 --- a/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfigurationCollection.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - -/// A collection of ``ClientRPCExecutionConfiguration``s, mapped to specific methods or services. -/// -/// When creating a new instance, you must provide a default configuration to be used when getting -/// a configuration for a method that has not been given a specific override. -/// Use ``setDefaultConfiguration(_:forService:)`` to set a specific override for a whole -/// service. -/// -/// Use the subscript to get and set configurations for methods. -public struct ClientRPCExecutionConfigurationCollection: Sendable, Hashable { - private var elements: [MethodDescriptor: ClientRPCExecutionConfiguration] - private let defaultConfiguration: ClientRPCExecutionConfiguration - - public init( - defaultConfiguration: ClientRPCExecutionConfiguration = ClientRPCExecutionConfiguration( - executionPolicy: nil, - timeout: nil - ) - ) { - self.elements = [:] - self.defaultConfiguration = defaultConfiguration - } - - public subscript(_ descriptor: MethodDescriptor) -> ClientRPCExecutionConfiguration { - get { - if let methodLevelOverride = self.elements[descriptor] { - return methodLevelOverride - } - var serviceLevelDescriptor = descriptor - serviceLevelDescriptor.method = "" - return self.elements[serviceLevelDescriptor, default: self.defaultConfiguration] - } - - set { - precondition( - !descriptor.service.isEmpty, - "Method descriptor's service cannot be empty." - ) - - self.elements[descriptor] = newValue - } - } - - /// Set a default configuration for a service. - /// - /// If getting a configuration for a method that's part of a service, and the method itself doesn't have an - /// override, then this configuration will be used instead of the default configuration passed when creating - /// this instance of ``ClientRPCExecutionConfigurationCollection``. - /// - /// - Parameters: - /// - configuration: The default configuration for the service. - /// - service: The name of the service for which this override applies. - public mutating func setDefaultConfiguration( - _ configuration: ClientRPCExecutionConfiguration, - forService service: String - ) { - self[MethodDescriptor(service: service, method: "")] = configuration - } -} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 03e12c87a..d91ec3d46 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -35,7 +35,7 @@ enum ClientRPCExecutor { static func execute( request: ClientRequest.Stream, method: MethodDescriptor, - configuration: ClientRPCExecutionConfiguration, + configuration: MethodConfiguration, serializer: some MessageSerializer, deserializer: some MessageDeserializer, transport: some ClientTransport, diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index 4bfe57c3c..f4a041c41 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -24,7 +24,7 @@ /// given method by calling ``removeHandler(forMethod:)``. /// /// In most cases you won't need to interact with the router directly. Instead you should register -/// your services with ``Server/Services-swift.struct/register(_:)`` which will in turn register +/// your services with ``GRPCServer/Services-swift.struct/register(_:)`` which will in turn register /// each method with the router. /// /// You may wish to not serve all methods from your service in which case you can either: diff --git a/Sources/GRPCCore/ClientError.swift b/Sources/GRPCCore/ClientError.swift new file mode 100644 index 000000000..77ad48719 --- /dev/null +++ b/Sources/GRPCCore/ClientError.swift @@ -0,0 +1,148 @@ +/* + * Copyright 2023, 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. + */ + +/// A runtime error thrown by the client. +/// +/// In contrast to ``RPCError``, the ``ClientError`` represents errors which happen at a scope +/// wider than an individual RPC. For example, attempting to start a client which is already +/// stopped would result in a ``ClientError``. +public struct ClientError: Error, Hashable, @unchecked Sendable { + private var storage: Storage + + // Ensures the underlying storage is unique. + private mutating func ensureUniqueStorage() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + /// The code indicating the domain of the error. + public var code: Code { + get { self.storage.code } + set { + self.ensureUniqueStorage() + self.storage.code = newValue + } + } + + /// A message providing more details about the error which may include details specific to this + /// instance of the error. + public var message: String { + get { self.storage.message } + set { + self.ensureUniqueStorage() + self.storage.message = newValue + } + } + + /// The original error which led to this error being thrown. + public var cause: Error? { + get { self.storage.cause } + set { + self.ensureUniqueStorage() + self.storage.cause = newValue + } + } + + /// Creates a new error. + /// + /// - Parameters: + /// - code: The error code. + /// - message: A description of the error. + /// - cause: The original error which led to this error being thrown. + public init(code: Code, message: String, cause: Error? = nil) { + self.storage = Storage(code: code, message: message, cause: cause) + } +} + +extension ClientError: CustomStringConvertible { + public var description: String { + if let cause = self.cause { + return "\(self.code): \"\(self.message)\" (cause: \"\(cause)\")" + } else { + return "\(self.code): \"\(self.message)\"" + } + } +} + +extension ClientError { + private final class Storage: Hashable { + var code: Code + var message: String + var cause: Error? + + init(code: Code, message: String, cause: Error?) { + self.code = code + self.message = message + self.cause = cause + } + + func copy() -> Storage { + return Storage(code: self.code, message: self.message, cause: self.cause) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + } + + static func == (lhs: Storage, rhs: Storage) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message + } + } +} + +extension ClientError { + public struct Code: Hashable, Sendable { + private enum Value { + case clientIsAlreadyRunning + case clientIsNotRunning + case clientIsStopped + case transportError + } + + private var value: Value + private init(_ value: Value) { + self.value = value + } + + /// At attempt to start the client was made but it is already running. + public static var clientIsAlreadyRunning: Self { + Self(.clientIsAlreadyRunning) + } + + /// An attempt to start an RPC was made but the client is not running. + public static var clientIsNotRunning: Self { + Self(.clientIsNotRunning) + } + + /// At attempt to start the client was made but it has already stopped. + public static var clientIsStopped: Self { + Self(.clientIsStopped) + } + + /// The transport threw an error whilst connected. + public static var transportError: Self { + Self(.transportError) + } + } +} + +extension ClientError.Code: CustomStringConvertible { + public var description: String { + String(describing: self.value) + } +} diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift new file mode 100644 index 000000000..848e01297 --- /dev/null +++ b/Sources/GRPCCore/GRPCClient.swift @@ -0,0 +1,473 @@ +/* + * Copyright 2023, 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 Atomics + +/// A gRPC client. +/// +/// A ``GRPCClient`` communicates to a server via a ``ClientTransport``. +/// +/// You can start RPCs to the server by calling the corresponding method: +/// - ``unary(request:descriptor:serializer:deserializer:handler:)`` +/// - ``clientStreaming(request:descriptor:serializer:deserializer:handler:)`` +/// - ``serverStreaming(request:descriptor:serializer:deserializer:handler:)`` +/// - ``bidirectionalStreaming(request:descriptor:serializer:deserializer:handler:)`` +/// +/// However, in most cases you should prefer wrapping the ``GRPCClient`` with a generated stub. +/// +/// You can set ``MethodConfiguration``s on this client to override whatever configurations have been +/// set on the given transport. You can also use ``ClientInterceptor``s to implement cross-cutting +/// logic which apply to all RPCs. Example uses of interceptors include authentication and logging. +/// +/// ## Creating and configuring a client +/// +/// The following example demonstrates how to create and configure a client. +/// +/// ```swift +/// // Create a configuration object for the client. +/// var configuration = GRPCClient.Configuration() +/// +/// // Create and add an interceptor. +/// configuration.interceptors.add(StatsRecordingClientInterceptor()) +/// +/// // Override the timeout for the 'Get' method on the 'echo.Echo' service. This configuration +/// // takes precedence over any set by the transport. +/// let echoGet = MethodDescriptor(service: "echo.Echo", method: "Get") +/// configuration.method.overrides[echoGet] = MethodConfiguration( +/// executionPolicy: nil, +/// timeout: .seconds(5) +/// ) +/// +/// // Configure a fallback timeout for all RPCs if no configuration is provided in the overrides +/// // or by the transport. +/// let defaultMethodConfiguration = MethodConfiguration(executionPolicy: nil, timeout: seconds(10)) +/// configuration.method.defaults.setDefaultConfiguration(defaultMethodConfiguration) +/// +/// // Finally create a transport and instantiate the client. +/// let inProcessServerTransport = InProcessServerTransport() +/// let inProcessClientTransport = InProcessClientTransport(serverTransport: inProcessServerTransport) +/// let client = GRPCClient(transport: inProcessClientTransport, configuration: configuration) +/// ``` +/// +/// ## Starting and stopping the client +/// +/// Once you have configured the client, call ``run()`` to start it. Calling ``run()`` instructs the +/// transport to start connecting to the server. +/// +/// ```swift +/// // Start running the client. 'run()' must be running while RPCs are execute so it's executed in +/// // a task group. +/// try await withThrowingTaskGroup(of: Void.self) { group in +/// group.addTask { +/// try await client.run() +/// } +/// +/// // Execute a request against the "echo.Echo" service. +/// try await client.unary( +/// request: ClientRequest.Single<[UInt8]>(message: [72, 101, 108, 108, 111, 33]), +/// descriptor: MethodDescriptor(service: "echo.Echo", method: "Get"), +/// serializer: IdentitySerializer(), +/// deserializer: IdentityDeserializer(), +/// ) { response in +/// print(response.message) +/// } +/// +/// // The RPC has completed, close the client. +/// client.close() +/// } +/// ``` +/// +/// The ``run()`` method won't return until the client has finished handling all requests. You can +/// signal to the client that it should stop creating new request streams by calling ``close()``. +/// This gives the client enough time to drain any requests already in flight. To stop the client +/// more abruptly you can cancel the task running your client. If your application requires +/// additional resources that need their lifecycles managed you should consider using [Swift Service +/// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct GRPCClient: Sendable { + /// The transport which provides a bidirectional communication channel with the server. + private let transport: any ClientTransport + + /// The configuration used by the client. + public let configuration: Configuration + + /// The current state of the client. + private let state: ManagedAtomic + + /// The state of the client. + private enum State: UInt8, AtomicValue { + /// The client hasn't been started yet. Can transition to `running` or `stopped`. + case notStarted + /// The client is running and can send RPCs. Can transition to `stopping`. + case running + /// The client is stopping and no new RPCs will be sent. Existing RPCs may run to + /// completion. May transition to `stopped`. + case stopping + /// The client has stopped, no RPCs are in flight and no more will be accepted. This state + /// is terminal. + case stopped + } + + /// Creates a new client with the given transport and configuration. + /// + /// - Parameters: + /// - transport: The transport used to establish a communication channel with a server. + /// - configuration: Configuration for the client. + public init(transport: some ClientTransport, configuration: Configuration = Configuration()) { + self.transport = transport + self.configuration = configuration + self.state = ManagedAtomic(.notStarted) + } + + /// Start the client. + /// + /// This returns once ``close()`` has been called and all in-flight RPCs have finished executing. + /// If you need to abruptly stop all work you should cancel the task executing this method. + /// + /// The client, and by extension this function, can only be run once. If the client is already + /// running or has already been closed then a ``ClientError`` is thrown. + public func run() async throws { + let (wasNotStarted, original) = self.state.compareExchange( + expected: .notStarted, + desired: .running, + ordering: .sequentiallyConsistent + ) + + guard wasNotStarted else { + switch original { + case .notStarted: + // The value wasn't exchanged so the original value can't be 'notStarted'. + fatalError() + case .running: + throw ClientError( + code: .clientIsAlreadyRunning, + message: "The client is already running and can only be started once." + ) + case .stopping, .stopped: + throw ClientError( + code: .clientIsStopped, + message: "The client has stopped and can only be started once." + ) + } + } + + // When we exit this function we must have stopped. + defer { + self.state.store(.stopped, ordering: .sequentiallyConsistent) + } + + do { + try await self.transport.connect(lazily: false) + } catch { + throw ClientError( + code: .transportError, + message: "The transport threw an error while connected.", + cause: error + ) + } + } + + /// Close the client. + /// + /// The transport will be closed: this means that it will be given enough time to wait for + /// in-flight RPCs to finish executing, but no new RPCs will be accepted. You can cancel the task + /// executing ``run()`` if you want to abruptly stop in-flight RPCs. + public func close() { + while true { + let (wasRunning, actualState) = self.state.compareExchange( + expected: .running, + desired: .stopping, + ordering: .sequentiallyConsistent + ) + + // Transition from running to stopping: close the transport. + if wasRunning { + self.transport.close() + return + } + + // The expected state wasn't 'running'. There are two options: + // 1. The client isn't running yet. + // 2. The client is already stopping or stopped. + switch actualState { + case .notStarted: + // Not started: try going straight to stopped. + let (wasNotStarted, _) = self.state.compareExchange( + expected: .notStarted, + desired: .stopped, + ordering: .sequentiallyConsistent + ) + + // If the exchange happened then just return: the client wasn't started so there's no + // transport to start. + // + // If the exchange didn't happen then continue looping: the client must've been started by + // another thread. + if wasNotStarted { + return + } else { + continue + } + + case .running: + // Unreachable: the value was exchanged and this was the expected value. + fatalError() + + case .stopping, .stopped: + // No exchange happened but the client is already stopping. + return + } + } + } + + /// Executes a unary RPC. + /// + /// - Parameters: + /// - request: The unary request. + /// - descriptor: The method descriptor for which to execute this request. + /// - serializer: A request serializer. + /// - deserializer: A response deserializer. + /// - handler: A unary response handler. + /// + /// - Returns: The return value from the `handler`. + public func unary( + request: ClientRequest.Single, + descriptor: MethodDescriptor, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue + ) async throws -> ReturnValue { + try await bidirectionalStreaming( + request: ClientRequest.Stream(single: request), + descriptor: descriptor, + serializer: serializer, + deserializer: deserializer + ) { stream in + let singleResponse = await ClientResponse.Single(stream: stream) + return try await handler(singleResponse) + } + } + + /// Start a client-streaming RPC. + /// + /// - Parameters: + /// - request: The request stream. + /// - descriptor: The method descriptor for which to execute this request. + /// - serializer: A request serializer. + /// - deserializer: A response deserializer. + /// - handler: A unary response handler. + /// + /// - Returns: The return value from the `handler`. + public func clientStreaming( + request: ClientRequest.Stream, + descriptor: MethodDescriptor, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue + ) async throws -> ReturnValue { + try await bidirectionalStreaming( + request: request, + descriptor: descriptor, + serializer: serializer, + deserializer: deserializer + ) { stream in + let singleResponse = await ClientResponse.Single(stream: stream) + return try await handler(singleResponse) + } + } + + /// Start a server-streaming RPC. + /// + /// - Parameters: + /// - request: The unary request. + /// - descriptor: The method descriptor for which to execute this request. + /// - serializer: A request serializer. + /// - deserializer: A response deserializer. + /// - handler: A response stream handler. + /// + /// - Returns: The return value from the `handler`. + public func serverStreaming( + request: ClientRequest.Single, + descriptor: MethodDescriptor, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue + ) async throws -> ReturnValue { + try await bidirectionalStreaming( + request: ClientRequest.Stream(single: request), + descriptor: descriptor, + serializer: serializer, + deserializer: deserializer, + handler: handler + ) + } + + /// Start a bidirectional streaming RPC. + /// + /// - Note: ``run()`` must have been called and still executing, and ``close()`` mustn't + /// have been called. + /// + /// - Parameters: + /// - request: The streaming request. + /// - descriptor: The method descriptor for which to execute this request. + /// - serializer: A request serializer. + /// - deserializer: A response deserializer. + /// - handler: A response stream handler. + /// + /// - Returns: The return value from the `handler`. + public func bidirectionalStreaming( + request: ClientRequest.Stream, + descriptor: MethodDescriptor, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue + ) async throws -> ReturnValue { + switch self.state.load(ordering: .sequentiallyConsistent) { + case .running: + () + case .notStarted: + throw ClientError( + code: .clientIsNotRunning, + message: "Client must be running to make an RPC: call run() first." + ) + case .stopping, .stopped: + throw ClientError( + code: .clientIsStopped, + message: "Client has been stopped. Can't make any more RPCs." + ) + } + + return try await ClientRPCExecutor.execute( + request: request, + method: descriptor, + configuration: self.resolveMethodConfiguration(for: descriptor), + serializer: serializer, + deserializer: deserializer, + transport: self.transport, + interceptors: self.configuration.interceptors.values, + handler: handler + ) + } + + private func resolveMethodConfiguration(for descriptor: MethodDescriptor) -> MethodConfiguration { + if let configuration = self.configuration.method.overrides[descriptor] { + return configuration + } + + if let configuration = self.transport.executionConfiguration(forMethod: descriptor) { + return configuration + } + + if let configuration = self.configuration.method.defaults[descriptor] { + return configuration + } + + // No configuration found, return the "vanilla" configuration. + return MethodConfiguration(executionPolicy: nil, timeout: nil) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCClient { + public struct Configuration: Sendable { + /// A collection of interceptors providing cross-cutting functionality to each accepted RPC. + /// + /// The order in which interceptors are added reflects the order in which they are called. The + /// first interceptor added will be the first interceptor to intercept each request. The last + /// interceptor added will be the final interceptor to intercept each request before calling + /// the appropriate handler. + public var interceptors: Interceptors + + /// Configuration for how methods are executed. + /// + /// Method configuration determines how each RPC is executed by the client. Some services and + /// transports provide this information to the client when the server name is resolved. However, + /// you override this configuration and set default values should no override be set and the + /// transport doesn't provide a value. + public var method: Method + + /// Creates a new default configuration. + public init() { + self.interceptors = Interceptors() + self.method = Method() + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCClient.Configuration { + /// A collection of ``ClientInterceptor`` implementations which are applied to all accepted + /// RPCs. + /// + /// RPCs are intercepted in the order that interceptors are added. That is, a request sent from the client to + /// the server will first be intercepted by the first added interceptor followed by the second, and so on. + /// For responses from the server, they'll be applied in the opposite order. + public struct Interceptors: Sendable { + private(set) var values: [any ClientInterceptor] = [] + + /// Add an interceptor to the client. + /// + /// The order in which interceptors are added reflects the order in which they are called. The + /// first interceptor added will be the first interceptor to intercept each request. The last + /// interceptor added will be the final interceptor to intercept each request before calling + /// the appropriate handler. + /// + /// - Parameter interceptor: The interceptor to add. + public mutating func add(_ interceptor: some ClientInterceptor) { + self.values.append(interceptor) + } + + /// Adds a sequence of interceptor to the client. + /// + /// The order in which interceptors are added reflects the order in which they are called. The + /// first interceptor added will be the first interceptor to intercept each request. The last + /// interceptor added will be the final interceptor to intercept each request before calling + /// the appropriate handler. + /// + /// - Parameter interceptors: The interceptors to add. + public mutating func add(contentsOf interceptors: some Sequence) { + self.values.append(contentsOf: interceptors) + } + } + + /// Configuration for how methods should be executed. + /// + /// In most cases the client should defer to the configuration provided by the transport as this + /// can be provided to the transport as part of name resolution when establishing a connection. + /// + /// The client first checks ``overrides`` for a configuration, followed by the transport, followed + /// by ``defaults``. + public struct Method: Sendable, Hashable { + /// Configuration to use in precedence to that provided by the transport. + public var overrides: MethodConfigurations + + /// Configuration to use only if there are no overrides and the transport doesn't specify + /// any configuration. + public var defaults: MethodConfigurations + + public init() { + self.overrides = MethodConfigurations() + self.defaults = MethodConfigurations() + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCClient.Configuration.Interceptors: CustomStringConvertible { + public var description: String { + return String(describing: self.values.map { String(describing: type(of: $0)) }) + } +} diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 07c92715e..626816f9b 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -23,7 +23,7 @@ import Atomics /// streams to a service to handle the RPC or rejects them with an appropriate error if no service /// can handle the RPC. /// -/// A ``Server`` may listen with multiple transports (for example, HTTP/2 and in-process) and route +/// A ``GRPCServer`` may listen with multiple transports (for example, HTTP/2 and in-process) and route /// requests from each transport to the same service instance. You can also use "interceptors", /// to implement cross-cutting logic which apply to all accepted RPCs. Example uses of interceptors /// include request filtering, authentication, and logging. Once requests have been intercepted diff --git a/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfiguration.swift b/Sources/GRPCCore/MethodConfiguration.swift similarity index 98% rename from Sources/GRPCCore/Call/Client/ClientRPCExecutionConfiguration.swift rename to Sources/GRPCCore/MethodConfiguration.swift index b771e0b76..14d2f717f 100644 --- a/Sources/GRPCCore/Call/Client/ClientRPCExecutionConfiguration.swift +++ b/Sources/GRPCCore/MethodConfiguration.swift @@ -16,7 +16,7 @@ /// Configuration values for executing an RPC. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct ClientRPCExecutionConfiguration: Hashable, Sendable { +public struct MethodConfiguration: Hashable, Sendable { /// The default timeout for the RPC. /// /// If no reply is received in the specified amount of time the request is aborted @@ -88,7 +88,7 @@ public struct ClientRPCExecutionConfiguration: Hashable, Sendable { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ClientRPCExecutionConfiguration { +extension MethodConfiguration { /// The execution policy for an RPC. public enum ExecutionPolicy: Hashable, Sendable { /// Policy for retrying an RPC. diff --git a/Sources/GRPCCore/MethodConfigurations.swift b/Sources/GRPCCore/MethodConfigurations.swift new file mode 100644 index 000000000..c7e741222 --- /dev/null +++ b/Sources/GRPCCore/MethodConfigurations.swift @@ -0,0 +1,92 @@ +/* + * Copyright 2023, 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. + */ + +/// A collection of ``MethodConfiguration``s, mapped to specific methods or services. +/// +/// When creating a new instance, no overrides and no default will be set for using when getting +/// a configuration for a method that has not been given a specific override. +/// Use ``setDefaultConfiguration(_:forService:)`` to set a specific override for a whole +/// service, or set a default configuration for all methods by calling ``setDefaultConfiguration(_:)``. +/// +/// Use the subscript to get and set configurations for specific methods. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct MethodConfigurations: Sendable, Hashable { + private var elements: [MethodDescriptor: MethodConfiguration] + + /// Create a new ``MethodConfigurations`` with no overrides and no default configuration. + public init() { + self.elements = [:] + } + + /// Get or set the corresponding ``MethodConfiguration`` for the given ``MethodDescriptor``. + /// + /// Configuration is hierarchical and can be set per-method, per-service + /// (``setDefaultConfiguration(_:forService:)``) and globally (``setDefaultConfiguration(_:)``). + /// This subscript sets the per-method configuration but retrieves a configuration respecting + /// the hierarchy. If no per-method configuration is present, the per-service configuration is + /// checked and returned if present. If the per-service configuration isn't present then the + /// global configuration is returned, if present. + /// + /// - Parameters: + /// - descriptor: The ``MethodDescriptor`` for which to get or set a ``MethodConfiguration``. + public subscript(_ descriptor: MethodDescriptor) -> MethodConfiguration? { + get { + if let configuration = self.elements[descriptor] { + return configuration + } + + // Check if the config is set at the service level by clearing the method. + var descriptor = descriptor + descriptor.method = "" + + if let configuration = self.elements[descriptor] { + return configuration + } + + // Check if the config is set at the global level by clearing the service and method. + descriptor.service = "" + return self.elements[descriptor] + } + + set { + self.elements[descriptor] = newValue + } + } + + /// Set a default configuration for all methods that have no overrides. + /// + /// - Parameter configuration: The default configuration. + public mutating func setDefaultConfiguration(_ configuration: MethodConfiguration?) { + let descriptor = MethodDescriptor(service: "", method: "") + self.elements[descriptor] = configuration + } + + /// Set a default configuration for a service. + /// + /// If getting a configuration for a method that's part of a service, and the method itself doesn't have an + /// override, then this configuration will be used instead of the default configuration passed when creating + /// this instance of ``MethodConfigurations``. + /// + /// - Parameters: + /// - configuration: The default configuration for the service. + /// - service: The name of the service for which this override applies. + public mutating func setDefaultConfiguration( + _ configuration: MethodConfiguration?, + forService service: String + ) { + self.elements[MethodDescriptor(service: service, method: "")] = configuration + } +} diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 6e7d20bcb..913712ea7 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -16,8 +16,8 @@ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol ClientTransport: Sendable { - associatedtype Inbound: (AsyncSequence & Sendable) where Inbound.Element == RPCResponsePart - associatedtype Outbound: ClosableRPCWriterProtocol + typealias Inbound = RPCAsyncSequence + typealias Outbound = RPCWriter.Closable /// Returns a throttle which gRPC uses to determine whether retries can be executed. /// @@ -77,5 +77,5 @@ public protocol ClientTransport: Sendable { /// - Returns: Execution configuration for the method, if it exists. func executionConfiguration( forMethod descriptor: MethodDescriptor - ) -> ClientRPCExecutionConfiguration? + ) -> MethodConfiguration? } diff --git a/Sources/GRPCCore/Transport/InProcessClientTransport.swift b/Sources/GRPCCore/Transport/InProcessClientTransport.swift index 47b1bbf73..d3cf123c5 100644 --- a/Sources/GRPCCore/Transport/InProcessClientTransport.swift +++ b/Sources/GRPCCore/Transport/InProcessClientTransport.swift @@ -96,12 +96,12 @@ public struct InProcessClientTransport: ClientTransport { public let retryThrottle: RetryThrottle - private let executionConfigurations: ClientRPCExecutionConfigurationCollection + private let executionConfigurations: MethodConfigurations private let state: LockedValueBox public init( server: InProcessServerTransport, - executionConfigurations: ClientRPCExecutionConfigurationCollection + executionConfigurations: MethodConfigurations ) { self.retryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) self.executionConfigurations = executionConfigurations @@ -329,7 +329,7 @@ public struct InProcessClientTransport: ClientTransport { /// - Returns: Execution configuration for the method, if it exists. public func executionConfiguration( forMethod descriptor: MethodDescriptor - ) -> ClientRPCExecutionConfiguration? { + ) -> MethodConfiguration? { self.executionConfigurations[descriptor] } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 42529470e..abaa8e6ef 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -24,15 +24,7 @@ extension InProcessServerTransport { ) -> InProcessClientTransport { return InProcessClientTransport( server: self, - executionConfigurations: .init( - defaultConfiguration: .init( - hedgingPolicy: .init( - maximumAttempts: 2, - hedgingDelay: .milliseconds(100), - nonFatalStatusCodes: [] - ) - ) - ) + executionConfigurations: .init() ) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 3b546d1e0..3d22f454b 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -67,7 +67,7 @@ struct ClientRPCExecutorTestHarness { func unary( request: ClientRequest.Single<[UInt8]>, - configuration: ClientRPCExecutionConfiguration? = nil, + configuration: MethodConfiguration? = nil, handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void ) async throws { try await self.bidirectional( @@ -80,7 +80,7 @@ struct ClientRPCExecutorTestHarness { func clientStreaming( request: ClientRequest.Stream<[UInt8]>, - configuration: ClientRPCExecutionConfiguration? = nil, + configuration: MethodConfiguration? = nil, handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void ) async throws { try await self.bidirectional( @@ -93,7 +93,7 @@ struct ClientRPCExecutorTestHarness { func serverStreaming( request: ClientRequest.Single<[UInt8]>, - configuration: ClientRPCExecutionConfiguration? = nil, + configuration: MethodConfiguration? = nil, handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void ) async throws { try await self.bidirectional( @@ -106,7 +106,7 @@ struct ClientRPCExecutorTestHarness { func bidirectional( request: ClientRequest.Stream<[UInt8]>, - configuration: ClientRPCExecutionConfiguration? = nil, + configuration: MethodConfiguration? = nil, handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void ) async throws { try await self.execute( @@ -122,7 +122,7 @@ struct ClientRPCExecutorTestHarness { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - configuration: ClientRPCExecutionConfiguration?, + configuration: MethodConfiguration?, handler: @escaping @Sendable (ClientResponse.Stream) async throws -> Void ) async throws { try await withThrowingTaskGroup(of: Void.self) { group in @@ -141,11 +141,14 @@ struct ClientRPCExecutorTestHarness { try await self.clientTransport.connect(lazily: false) } - let executionConfiguration: ClientRPCExecutionConfiguration + let executionConfiguration: MethodConfiguration if let configuration = configuration { executionConfiguration = configuration } else { - executionConfiguration = ClientRPCExecutionConfiguration(executionPolicy: nil, timeout: nil) + executionConfiguration = MethodConfiguration( + executionPolicy: nil, + timeout: nil + ) } // Execute the request. diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index ab6a6fca5..79663bfee 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -187,7 +187,7 @@ extension ClientRPCExecutorTests { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ClientRPCExecutionConfiguration { +extension MethodConfiguration { fileprivate static func hedge( maximumAttempts: Int = 5, delay: Duration = .milliseconds(25), @@ -200,6 +200,6 @@ extension ClientRPCExecutionConfiguration { nonFatalStatusCodes: nonFatalCodes ) - return ClientRPCExecutionConfiguration(hedgingPolicy: policy, timeout: timeout) + return Self(hedgingPolicy: policy, timeout: timeout) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index a313148ea..335044e26 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -286,7 +286,7 @@ extension ClientRPCExecutorTests { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ClientRPCExecutionConfiguration { +extension MethodConfiguration { fileprivate static func retry( maximumAttempts: Int = 5, codes: Set, @@ -300,6 +300,6 @@ extension ClientRPCExecutionConfiguration { retryableStatusCodes: codes ) - return ClientRPCExecutionConfiguration(retryPolicy: policy, timeout: timeout) + return Self(retryPolicy: policy, timeout: timeout) } } diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift new file mode 100644 index 000000000..204c6579e --- /dev/null +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -0,0 +1,448 @@ +/* + * Copyright 2023, 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 Atomics +import GRPCCore +import XCTest + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class GRPCClientTests: XCTestCase { + func makeInProcessPair() -> (client: InProcessClientTransport, server: InProcessServerTransport) { + let server = InProcessServerTransport() + let client = InProcessClientTransport( + server: server, + executionConfigurations: MethodConfigurations() + ) + + return (client, server) + } + + func withInProcessConnectedClient( + services: [any RegistrableRPCService], + interceptors: [any ClientInterceptor] = [], + _ body: (GRPCClient, GRPCServer) async throws -> Void + ) async throws { + let inProcess = self.makeInProcessPair() + var configuration = GRPCClient.Configuration() + configuration.interceptors.add(contentsOf: interceptors) + let client = GRPCClient(transport: inProcess.client, configuration: configuration) + + let server = GRPCServer() + server.transports.add(inProcess.server) + + for service in services { + server.services.register(service) + } + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.run() + } + + group.addTask { + try await client.run() + } + + // Make sure both server and client are running + try await Task.sleep(for: .milliseconds(100)) + try await body(client, server) + client.close() + server.stopListening() + } + } + + struct IdentitySerializer: MessageSerializer { + typealias Message = [UInt8] + + func serialize(_ message: [UInt8]) throws -> [UInt8] { + return message + } + } + + struct IdentityDeserializer: MessageDeserializer { + typealias Message = [UInt8] + + func deserialize(_ serializedMessageBytes: [UInt8]) throws -> [UInt8] { + return serializedMessageBytes + } + } + + func testUnary() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + let message = try response.message + XCTAssertEqual(message, [3, 1, 4, 1, 5]) + } + } + } + + func testClientStreaming() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.clientStreaming( + request: .init(producer: { writer in + for byte in [3, 1, 4, 1, 5] as [UInt8] { + try await writer.write([byte]) + } + }), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + let message = try response.message + XCTAssertEqual(message, [3, 1, 4, 1, 5]) + } + } + } + + func testServerStreaming() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.serverStreaming( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.expand, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + var responseParts = response.messages.makeAsyncIterator() + for byte in [3, 1, 4, 1, 5] as [UInt8] { + let message = try await responseParts.next() + XCTAssertEqual(message, [byte]) + } + } + } + } + + func testBidirectionalStreaming() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.bidirectionalStreaming( + request: .init(producer: { writer in + for byte in [3, 1, 4, 1, 5] as [UInt8] { + try await writer.write([byte]) + } + }), + descriptor: BinaryEcho.Methods.update, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + var responseParts = response.messages.makeAsyncIterator() + for byte in [3, 1, 4, 1, 5] as [UInt8] { + let message = try await responseParts.next() + XCTAssertEqual(message, [byte]) + } + } + } + } + + func testUnimplementedMethod_Unary() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: MethodDescriptor(service: "not", method: "implemented"), + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + XCTAssertThrowsRPCError(try response.accepted.get()) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + func testUnimplementedMethod_ClientStreaming() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.clientStreaming( + request: .init(producer: { writer in + for byte in [3, 1, 4, 1, 5] as [UInt8] { + try await writer.write([byte]) + } + }), + descriptor: MethodDescriptor(service: "not", method: "implemented"), + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + XCTAssertThrowsRPCError(try response.accepted.get()) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + func testUnimplementedMethod_ServerStreaming() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.serverStreaming( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: MethodDescriptor(service: "not", method: "implemented"), + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + XCTAssertThrowsRPCError(try response.accepted.get()) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + func testUnimplementedMethod_BidirectionalStreaming() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + try await client.bidirectionalStreaming( + request: .init(producer: { writer in + for byte in [3, 1, 4, 1, 5] as [UInt8] { + try await writer.write([byte]) + } + }), + descriptor: MethodDescriptor(service: "not", method: "implemented"), + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + XCTAssertThrowsRPCError(try response.accepted.get()) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + func testMultipleConcurrentRequests() async throws { + try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + await withThrowingTaskGroup(of: Void.self) { group in + for i in UInt8.min ..< UInt8.max { + group.addTask { + try await client.unary( + request: .init(message: [i]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + let message = try response.message + XCTAssertEqual(message, [i]) + } + } + } + } + } + } + + func testInterceptorsAreAppliedInOrder() async throws { + let counter1 = ManagedAtomic(0) + let counter2 = ManagedAtomic(0) + + try await self.withInProcessConnectedClient( + services: [BinaryEcho()], + interceptors: [ + .requestCounter(counter1), + .rejectAll(with: RPCError(code: .unavailable, message: "")), + .requestCounter(counter2), + ] + ) { client, _ in + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + XCTAssertRejected(response) { error in + XCTAssertEqual(error.code, .unavailable) + } + } + } + + XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) + XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 0) + } + + func testNoNewRPCsAfterClientClose() async throws { + try await withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in + // Run an RPC so we know the client is running properly. + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + let message = try response.message + XCTAssertEqual(message, [3, 1, 4, 1, 5]) + } + + // New RPCs should fail immediately after this. + client.close() + + // RPC should fail now. + await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { _ in } + } errorHandler: { error in + XCTAssertEqual(error.code, .clientIsStopped) + } + } + } + + func testInFlightRPCsCanContinueAfterClientIsClosed() async throws { + try await withInProcessConnectedClient(services: [BinaryEcho()]) { client, server in + try await client.clientStreaming( + request: .init(producer: { writer in + + // Close the client once this RCP has been started. + client.close() + + // Attempts to start a new RPC should fail. + await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { _ in } + } errorHandler: { error in + XCTAssertEqual(error.code, .clientIsStopped) + } + + // Now write to the already opened stream to confirm that opened streams + // can successfully run to completion. + for byte in [3, 1, 4, 1, 5] as [UInt8] { + try await writer.write([byte]) + } + }), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + let message = try response.message + XCTAssertEqual(message, [3, 1, 4, 1, 5]) + } + } + } + + func testCancelRunningClient() async throws { + let inProcess = self.makeInProcessPair() + let client = GRPCClient(transport: inProcess.client) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let server = GRPCServer() + server.services.register(BinaryEcho()) + server.transports.add(inProcess.server) + try await server.run() + } + + group.addTask { + try await client.run() + } + + // Wait for client and server to be running. + try await Task.sleep(for: .milliseconds(10)) + + let task = Task { + try await client.clientStreaming( + request: .init(producer: { writer in + try await Task.sleep(for: .seconds(5)) + }), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + XCTAssertRejected(response) { error in + XCTAssertEqual(error.code, .unknown) + } + } + } + + // Check requests are getting through. + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + let message = try response.message + XCTAssertEqual(message, [3, 1, 4, 1, 5]) + } + + task.cancel() + try await task.value + group.cancelAll() + } + } + + func testRunStoppedClient() async throws { + let (clientTransport, _) = self.makeInProcessPair() + let client = GRPCClient(transport: clientTransport) + // Run the client. + let task = Task { try await client.run() } + task.cancel() + try await task.value + + // Client is stopped, should throw an error. + await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + try await client.run() + } errorHandler: { error in + XCTAssertEqual(error.code, .clientIsStopped) + } + } + + func testRunAlreadyRunningClient() async throws { + let (clientTransport, _) = self.makeInProcessPair() + let client = GRPCClient(transport: clientTransport) + // Run the client. + let task = Task { try await client.run() } + // Make sure the client is run for the first time here. + try await Task.sleep(for: .milliseconds(10)) + + // Client is already running, should throw an error. + await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + try await client.run() + } errorHandler: { error in + XCTAssertEqual(error.code, .clientIsAlreadyRunning) + } + + task.cancel() + } + + func testRunClientNotRunning() async throws { + let (clientTransport, _) = self.makeInProcessPair() + let client = GRPCClient(transport: clientTransport) + + // Client is not running, should throw an error. + await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer() + ) { response in + let message = try response.message + XCTAssertEqual(message, [3, 1, 4, 1, 5]) + } + } errorHandler: { error in + XCTAssertEqual(error.code, .clientIsNotRunning) + } + } + + func testInterceptorsDescription() async throws { + var config = GRPCClient.Configuration() + config.interceptors.add(.rejectAll(with: .init(code: .aborted, message: ""))) + config.interceptors.add(.requestCounter(.init(0))) + + let description = String(describing: config.interceptors) + let expected = #"["RejectAllClientInterceptor", "RequestCountingClientInterceptor"]"# + XCTAssertEqual(description, expected) + } +} diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index e99a46914..3bc4a6a8c 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -23,7 +23,7 @@ final class GRPCServerTests: XCTestCase { let server = InProcessServerTransport() let client = InProcessClientTransport( server: server, - executionConfigurations: ClientRPCExecutionConfigurationCollection() + executionConfigurations: MethodConfigurations() ) return (client, server) @@ -59,7 +59,6 @@ final class GRPCServerTests: XCTestCase { inProcess.client.close() server.stopListening() } - } func testServerHandlesUnary() async throws { diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift b/Tests/GRPCCoreTests/MethodConfigurationTests.swift similarity index 96% rename from Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift rename to Tests/GRPCCoreTests/MethodConfigurationTests.swift index 768b808b2..8104b9fd6 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationTests.swift +++ b/Tests/GRPCCoreTests/MethodConfigurationTests.swift @@ -17,7 +17,7 @@ import GRPCCore import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class ClientRPCExecutionConfigurationTests: XCTestCase { +final class MethodConfigurationTests: XCTestCase { func testRetryPolicyClampsMaxAttempts() { var policy = RetryPolicy( maximumAttempts: 10, diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift b/Tests/GRPCCoreTests/MethodConfigurationsTests.swift similarity index 72% rename from Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift rename to Tests/GRPCCoreTests/MethodConfigurationsTests.swift index 38f964652..0d8e88829 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRPCExecutionConfigurationCollectionTests.swift +++ b/Tests/GRPCCoreTests/MethodConfigurationsTests.swift @@ -17,17 +17,16 @@ import GRPCCore import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class ClientRPCExecutionConfigurationCollectionTests: XCTestCase { - func testGetConfigurationForKnownMethod() { +final class MethodConfigurationsTests: XCTestCase { + func testGetConfigurationForKnownMethod() async throws { let policy = HedgingPolicy( maximumAttempts: 10, hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) - var configurations = ClientRPCExecutionConfigurationCollection( - defaultConfiguration: defaultConfiguration - ) + let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + var configurations = MethodConfigurations() + configurations.setDefaultConfiguration(defaultConfiguration) let descriptor = MethodDescriptor(service: "test", method: "first") let retryPolicy = RetryPolicy( maximumAttempts: 10, @@ -36,7 +35,7 @@ final class ClientRPCExecutionConfigurationCollectionTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) configurations[descriptor] = overrideConfiguration XCTAssertEqual(configurations[descriptor], overrideConfiguration) @@ -48,10 +47,9 @@ final class ClientRPCExecutionConfigurationCollectionTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) - var configurations = ClientRPCExecutionConfigurationCollection( - defaultConfiguration: defaultConfiguration - ) + let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + var configurations = MethodConfigurations() + configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test", method: "") let retryPolicy = RetryPolicy( maximumAttempts: 10, @@ -60,7 +58,7 @@ final class ClientRPCExecutionConfigurationCollectionTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) configurations[firstDescriptor] = overrideConfiguration let secondDescriptor = MethodDescriptor(service: "test", method: "second") @@ -73,10 +71,9 @@ final class ClientRPCExecutionConfigurationCollectionTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) - var configurations = ClientRPCExecutionConfigurationCollection( - defaultConfiguration: defaultConfiguration - ) + let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + var configurations = MethodConfigurations() + configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test1", method: "first") let retryPolicy = RetryPolicy( maximumAttempts: 10, @@ -85,7 +82,7 @@ final class ClientRPCExecutionConfigurationCollectionTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) configurations[firstDescriptor] = overrideConfiguration let secondDescriptor = MethodDescriptor(service: "test2", method: "second") diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift new file mode 100644 index 000000000..e89681d58 --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -0,0 +1,84 @@ +/* + * Copyright 2023, 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 Atomics +import GRPCCore + +extension ClientInterceptor where Self == RejectAllClientInterceptor { + static func rejectAll(with error: RPCError) -> Self { + return RejectAllClientInterceptor(error: error, throw: false) + } + + static func throwError(_ error: RPCError) -> Self { + return RejectAllClientInterceptor(error: error, throw: true) + } + +} + +extension ClientInterceptor where Self == RequestCountingClientInterceptor { + static func requestCounter(_ counter: ManagedAtomic) -> Self { + return RequestCountingClientInterceptor(counter: counter) + } +} + +/// Rejects all RPCs with the provided error. +struct RejectAllClientInterceptor: ClientInterceptor { + /// The error to reject all RPCs with. + let error: RPCError + /// Whether the error should be thrown. If `false` then the request is rejected with the error + /// instead. + let `throw`: Bool + + init(error: RPCError, throw: Bool = false) { + self.error = error + self.`throw` = `throw` + } + + func intercept( + request: ClientRequest.Stream, + context: ClientInterceptorContext, + next: @Sendable ( + ClientRequest.Stream, + ClientInterceptorContext + ) async throws -> ClientResponse.Stream + ) async throws -> ClientResponse.Stream { + if self.throw { + throw self.error + } else { + return ClientResponse.Stream(error: self.error) + } + } +} + +struct RequestCountingClientInterceptor: ClientInterceptor { + /// The number of requests made. + let counter: ManagedAtomic + + init(counter: ManagedAtomic) { + self.counter = counter + } + + func intercept( + request: ClientRequest.Stream, + context: ClientInterceptorContext, + next: @Sendable ( + ClientRequest.Stream, + ClientInterceptorContext + ) async throws -> ClientResponse.Stream + ) async throws -> ClientResponse.Stream { + self.counter.wrappingIncrement(ordering: .sequentiallyConsistent) + return try await next(request, context) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 266648768..02d4061a6 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -24,7 +24,6 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { static func throwError(_ error: RPCError) -> Self { return RejectAllServerInterceptor(error: error, throw: true) } - } extension ServerInterceptor where Self == RequestCountingServerInterceptor { @@ -63,7 +62,7 @@ struct RejectAllServerInterceptor: ServerInterceptor { } struct RequestCountingServerInterceptor: ServerInterceptor { - /// The error to reject all RPCs with. + /// The number of requests made. let counter: ManagedAtomic init(counter: ManagedAtomic) { diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index fa64d76fd..5fb153bee 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -28,7 +28,7 @@ struct AnyClientTransport: ClientTransport, Sendable { ) async throws -> Any private let _connect: @Sendable (Bool) async throws -> Void private let _close: @Sendable () -> Void - private let _configuration: @Sendable (MethodDescriptor) -> ClientRPCExecutionConfiguration? + private let _configuration: @Sendable (MethodDescriptor) -> MethodConfiguration? init(wrapping transport: Transport) where Transport.Inbound == Inbound, Transport.Outbound == Outbound { @@ -74,7 +74,7 @@ struct AnyClientTransport: ClientTransport, Sendable { func executionConfiguration( forMethod descriptor: MethodDescriptor - ) -> ClientRPCExecutionConfiguration? { + ) -> MethodConfiguration? { self._configuration(descriptor) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 11c294e03..0cdb2d1fd 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -68,7 +68,7 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { func executionConfiguration( forMethod descriptor: MethodDescriptor - ) -> ClientRPCExecutionConfiguration? { + ) -> MethodConfiguration? { self.transport.executionConfiguration(forMethod: descriptor) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 0dd1cee9b..def987fac 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -38,7 +38,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { func executionConfiguration( forMethod descriptor: MethodDescriptor - ) -> ClientRPCExecutionConfiguration? { + ) -> MethodConfiguration? { return nil } diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 7bc88edef..71ca6dd7a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -104,6 +104,18 @@ func XCTAssertRejected( } } +func XCTAssertRejected( + _ response: ClientResponse.Single, + errorHandler: (RPCError) -> Void +) { + switch response.accepted { + case .success: + XCTFail("Expected RPC to be rejected") + case .failure(let error): + errorHandler(error) + } +} + func XCTAssertMetadata( _ part: RPCResponsePart?, metadataHandler: (Metadata) -> Void = { _ in } @@ -116,6 +128,18 @@ func XCTAssertMetadata( } } +func XCTAssertMetadata( + _ part: RPCRequestPart?, + metadataHandler: (Metadata) async throws -> Void = { _ in } +) async throws { + switch part { + case .some(.metadata(let metadata)): + try await metadataHandler(metadata) + default: + XCTFail("Expected '.metadata' but found '\(String(describing: part))'") + } +} + func XCTAssertMessage( _ part: RPCResponsePart?, messageHandler: ([UInt8]) -> Void = { _ in } @@ -124,7 +148,19 @@ func XCTAssertMessage( case .some(.message(let message)): messageHandler(message) default: - XCTFail("Expected '.metadata' but found '\(String(describing: part))'") + XCTFail("Expected '.message' but found '\(String(describing: part))'") + } +} + +func XCTAssertMessage( + _ part: RPCRequestPart?, + messageHandler: ([UInt8]) async throws -> Void = { _ in } +) async throws { + switch part { + case .some(.message(let message)): + try await messageHandler(message) + default: + XCTFail("Expected '.message' but found '\(String(describing: part))'") } } diff --git a/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift b/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift index c7bbdef80..4ff0557de 100644 --- a/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift +++ b/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift @@ -187,10 +187,9 @@ final class InProcessClientTransportTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = ClientRPCExecutionConfiguration(hedgingPolicy: policy) - var configurations = ClientRPCExecutionConfigurationCollection( - defaultConfiguration: defaultConfiguration - ) + let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + var configurations = MethodConfigurations() + configurations.setDefaultConfiguration(defaultConfiguration) var client = InProcessClientTransport(server: .init(), executionConfigurations: configurations) @@ -204,7 +203,7 @@ final class InProcessClientTransportTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = ClientRPCExecutionConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) configurations[firstDescriptor] = overrideConfiguration client = InProcessClientTransport(server: .init(), executionConfigurations: configurations) let secondDescriptor = MethodDescriptor(service: "test", method: "second") @@ -243,7 +242,7 @@ final class InProcessClientTransportTests: XCTestCase { } func makeClient( - configuration: ClientRPCExecutionConfiguration? = nil, + configuration: MethodConfiguration? = nil, server: InProcessServerTransport = InProcessServerTransport() ) -> InProcessClientTransport { let defaultPolicy = RetryPolicy( @@ -254,11 +253,13 @@ final class InProcessClientTransportTests: XCTestCase { retryableStatusCodes: [.unavailable] ) + var methodConfiguration = MethodConfigurations() + methodConfiguration.setDefaultConfiguration( + configuration ?? .init(retryPolicy: defaultPolicy) + ) return InProcessClientTransport( server: server, - executionConfigurations: .init( - defaultConfiguration: configuration ?? .init(retryPolicy: defaultPolicy) - ) + executionConfigurations: methodConfiguration ) } } From 1c33b78518ea010869ca1e3573b2fc37683491f9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 4 Dec 2023 11:06:10 +0000 Subject: [PATCH 183/580] Move in-process transport to own module (#1730) Motivation: The in-process transport is mostly helpful for testing, it should therefore not be built unnecessarily and should live in its own module. Modifications: - Move the in-process transport and its tests to their own modules Results: The in-process transport lives in its own module. --- Package.swift | 27 ++++++++++-- .../Concurrency Primitives/Lock.swift | 11 +++-- .../Internal/RPCAsyncSequence+Buffered.swift | 8 ++++ .../InProcessClientTransport.swift | 20 +++++---- .../InProcessServerTransport.swift | 4 +- .../Internal/AsyncStream+MakeStream.swift | 32 ++++++++++++++ ...ientRPCExecutorTestHarness+Transport.swift | 9 ++-- .../ClientRPCExecutorTestHarness.swift | 1 + Tests/GRPCCoreTests/GRPCClientTests.swift | 3 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 6 +-- .../InProcessClientTransportTests.swift | 22 +++++----- .../InProcessServerTransportTests.swift | 30 ++++++++++--- .../Test Utilities/XCTest+Utilities.swift | 44 +++++++++++++++++++ 13 files changed, 171 insertions(+), 46 deletions(-) rename Sources/{GRPCCore/Transport => GRPCInProcessTransport}/InProcessClientTransport.swift (95%) rename Sources/{GRPCCore/Transport => GRPCInProcessTransport}/InProcessServerTransport.swift (99%) create mode 100644 Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift rename Tests/{GRPCCoreTests/Transport => GRPCInProcessTransportTests}/InProcessClientTransportTests.swift (91%) rename Tests/{GRPCCoreTests/Transport => GRPCInProcessTransportTests}/InProcessServerTransportTests.swift (79%) create mode 100644 Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift diff --git a/Package.swift b/Package.swift index 30b1d8782..757b29cc9 100644 --- a/Package.swift +++ b/Package.swift @@ -133,6 +133,7 @@ extension Target.Dependency { static let atomics: Self = .product(name: "Atomics", package: "swift-atomics") static let grpcCore: Self = .target(name: "GRPCCore") + static let grpcInProcessTransport: Self = .target(name: "GRPCInProcessTransport") } // MARK: - Targets @@ -170,6 +171,13 @@ extension Target { path: "Sources/GRPCCore" ) + static let grpcInProcessTransport: Target = .target( + name: "GRPCInProcessTransport", + dependencies: [ + .grpcCore + ] + ) + static let cgrpcZlib: Target = .target( name: cgrpcZlibTargetName, path: "Sources/CGRPCZlib", @@ -230,18 +238,27 @@ extension Target { name: "GRPCCoreTests", dependencies: [ .grpcCore, + .grpcInProcessTransport, .dequeModule, .atomics ] ) + static let grpcInProcessTransportTests: Target = .testTarget( + name: "GRPCInProcessTransportTests", + dependencies: [ + .grpcCore, + .grpcInProcessTransport, + ] + ) + static let grpcCodeGenTests: Target = .testTarget( name: "GRPCCodeGenTests", dependencies: [ .grpcCodeGen ] ) - + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -468,7 +485,7 @@ extension Target { "v1Alpha/reflection-v1alpha.proto" ] ) - + static let reflectionServer: Target = .executableTarget( name: "ReflectionServer", dependencies: [ @@ -486,7 +503,7 @@ extension Target { .copy("Generated") ] ) - + static let grpcCodeGen: Target = .target( name: "GRPCCodeGen", path: "Sources/GRPCCodeGen" @@ -500,7 +517,7 @@ extension Product { name: grpcProductName, targets: [grpcTargetName] ) - + static let grpcCore: Product = .library( name: "_GRPCCore", targets: ["GRPCCore"] @@ -566,10 +583,12 @@ let package = Package( // v2 .grpcCore, + .grpcInProcessTransport, .grpcCodeGen, // v2 tests .grpcCoreTests, + .grpcInProcessTransportTests, .grpcCodeGenTests ] ) diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift index 0aa5c40d4..0cadb250e 100644 --- a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift +++ b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift @@ -237,19 +237,22 @@ extension UnsafeMutablePointer { } @usableFromInline -struct LockedValueBox { +internal typealias LockedValueBox = _LockedValueBox + +// TODO: Use 'package' ACL when 5.9 is the minimum Swift version. +public struct _LockedValueBox { @usableFromInline let storage: LockStorage @inlinable - init(_ value: Value) { + public init(_ value: Value) { self.storage = .create(value: value) } @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + public func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { return try self.storage.withLockedValue(mutate) } } -extension LockedValueBox: Sendable where Value: Sendable {} +extension _LockedValueBox: Sendable where Value: Sendable {} diff --git a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift index 7dfc275b7..ff59ea2bb 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift @@ -28,4 +28,12 @@ extension RPCAsyncSequence { return (RPCAsyncSequence(wrapping: stream), RPCWriter.Closable(wrapping: continuation)) } + + @inlinable + public static func _makeBackpressuredStream( + of elementType: Element.Type = Element.self, + watermarks: (low: Int, high: Int) + ) -> (stream: Self, writer: RPCWriter.Closable) { + return Self.makeBackpressuredStream(of: elementType, watermarks: watermarks) + } } diff --git a/Sources/GRPCCore/Transport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift similarity index 95% rename from Sources/GRPCCore/Transport/InProcessClientTransport.swift rename to Sources/GRPCInProcessTransport/InProcessClientTransport.swift index d3cf123c5..b42792146 100644 --- a/Sources/GRPCCore/Transport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -14,7 +14,8 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +import GRPCCore + /// An in-process implementation of a ``ClientTransport``. /// /// This is useful when you're interested in testing your application without any actual networking layers @@ -34,6 +35,7 @@ /// block until ``connect(lazily:)`` is called or the task is cancelled. /// /// - SeeAlso: ``ClientTransport`` +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct InProcessClientTransport: ClientTransport { private enum State: Sendable { struct UnconnectedState { @@ -96,16 +98,16 @@ public struct InProcessClientTransport: ClientTransport { public let retryThrottle: RetryThrottle - private let executionConfigurations: MethodConfigurations - private let state: LockedValueBox + private let methodConfiguration: MethodConfigurations + private let state: _LockedValueBox public init( server: InProcessServerTransport, - executionConfigurations: MethodConfigurations + methodConfiguration: MethodConfigurations = MethodConfigurations() ) { self.retryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) - self.executionConfigurations = executionConfigurations - self.state = LockedValueBox(.unconnected(.init(serverTransport: server))) + self.methodConfiguration = methodConfiguration + self.state = _LockedValueBox(.unconnected(.init(serverTransport: server))) } /// Establish and maintain a connection to the remote destination. @@ -222,8 +224,8 @@ public struct InProcessClientTransport: ClientTransport { descriptor: MethodDescriptor, _ closure: (RPCStream) async throws -> T ) async throws -> T { - let request = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) - let response = RPCAsyncSequence.makeBackpressuredStream(watermarks: (16, 32)) + let request = RPCAsyncSequence._makeBackpressuredStream(watermarks: (16, 32)) + let response = RPCAsyncSequence._makeBackpressuredStream(watermarks: (16, 32)) let clientStream = RPCStream( descriptor: descriptor, @@ -330,6 +332,6 @@ public struct InProcessClientTransport: ClientTransport { public func executionConfiguration( forMethod descriptor: MethodDescriptor ) -> MethodConfiguration? { - self.executionConfigurations[descriptor] + self.methodConfiguration[descriptor] } } diff --git a/Sources/GRPCCore/Transport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift similarity index 99% rename from Sources/GRPCCore/Transport/InProcessServerTransport.swift rename to Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 47762e07f..95181b00c 100644 --- a/Sources/GRPCCore/Transport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -14,7 +14,8 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +import GRPCCore + /// An in-process implementation of a ``ServerTransport``. /// /// This is useful when you're interested in testing your application without any actual networking layers @@ -25,6 +26,7 @@ /// To stop listening to new requests, call ``stopListening()``. /// /// - SeeAlso: ``ClientTransport`` +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct InProcessServerTransport: ServerTransport, Sendable { public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable diff --git a/Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift b/Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..68b492869 --- /dev/null +++ b/Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift @@ -0,0 +1,32 @@ +/* + * Copyright 2023, 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. + */ + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index abaa8e6ef..e02cb4ce5 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -14,17 +14,14 @@ * limitations under the License. */ import Atomics - -@testable import GRPCCore +import GRPCCore +import GRPCInProcessTransport @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension InProcessServerTransport { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) ) -> InProcessClientTransport { - return InProcessClientTransport( - server: self, - executionConfigurations: .init() - ) + return InProcessClientTransport(server: self) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 3d22f454b..9aacff2d7 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -14,6 +14,7 @@ * limitations under the License. */ import Atomics +import GRPCInProcessTransport import XCTest @testable import GRPCCore diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 204c6579e..d36f78dee 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -15,6 +15,7 @@ */ import Atomics import GRPCCore +import GRPCInProcessTransport import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -23,7 +24,7 @@ final class GRPCClientTests: XCTestCase { let server = InProcessServerTransport() let client = InProcessClientTransport( server: server, - executionConfigurations: MethodConfigurations() + methodConfiguration: MethodConfigurations() ) return (client, server) diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 3bc4a6a8c..e694e80cd 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -15,16 +15,14 @@ */ import Atomics import GRPCCore +import GRPCInProcessTransport import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCServerTests: XCTestCase { func makeInProcessPair() -> (client: InProcessClientTransport, server: InProcessServerTransport) { let server = InProcessServerTransport() - let client = InProcessClientTransport( - server: server, - executionConfigurations: MethodConfigurations() - ) + let client = InProcessClientTransport(server: server) return (client, server) } diff --git a/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift similarity index 91% rename from Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift rename to Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 4ff0557de..e751e364a 100644 --- a/Tests/GRPCCoreTests/Transport/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -14,10 +14,10 @@ * limitations under the License. */ +import GRPCCore +import GRPCInProcessTransport import XCTest -@testable import GRPCCore - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class InProcessClientTransportTests: XCTestCase { struct FailTest: Error {} @@ -34,7 +34,7 @@ final class InProcessClientTransportTests: XCTestCase { try await client.connect(lazily: false) } - await XCTAssertThrowsRPCErrorAsync { + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { try await group.next() } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) @@ -48,7 +48,7 @@ final class InProcessClientTransportTests: XCTestCase { client.close() - await XCTAssertThrowsRPCErrorAsync { + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { try await client.connect(lazily: false) } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) @@ -69,7 +69,7 @@ final class InProcessClientTransportTests: XCTestCase { try await group.next() group.cancelAll() - await XCTAssertThrowsRPCErrorAsync { + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { try await client.connect(lazily: false) } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) @@ -135,7 +135,7 @@ final class InProcessClientTransportTests: XCTestCase { client.close() - await XCTAssertThrowsRPCErrorAsync { + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { try await client.withStream(descriptor: .init(service: "test", method: "test")) { _ in } } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) @@ -155,7 +155,7 @@ final class InProcessClientTransportTests: XCTestCase { try await client.withStream(descriptor: .init(service: "test", method: "test")) { stream in try await stream.outbound.write(.message([1])) stream.outbound.finish() - let receivedMessages = try await stream.inbound.collect() + let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } XCTAssertEqual(receivedMessages, [.message([42])]) } @@ -163,7 +163,7 @@ final class InProcessClientTransportTests: XCTestCase { group.addTask { for try await stream in server.listen() { - let receivedMessages = try await stream.inbound.collect() + let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } try await stream.outbound.write(RPCResponsePart.message([42])) stream.outbound.finish() @@ -191,7 +191,7 @@ final class InProcessClientTransportTests: XCTestCase { var configurations = MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) - var client = InProcessClientTransport(server: .init(), executionConfigurations: configurations) + var client = InProcessClientTransport(server: .init(), methodConfiguration: configurations) let firstDescriptor = MethodDescriptor(service: "test", method: "first") XCTAssertEqual(client.executionConfiguration(forMethod: firstDescriptor), defaultConfiguration) @@ -205,7 +205,7 @@ final class InProcessClientTransportTests: XCTestCase { ) let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) configurations[firstDescriptor] = overrideConfiguration - client = InProcessClientTransport(server: .init(), executionConfigurations: configurations) + client = InProcessClientTransport(server: .init(), methodConfiguration: configurations) let secondDescriptor = MethodDescriptor(service: "test", method: "second") XCTAssertEqual(client.executionConfiguration(forMethod: firstDescriptor), overrideConfiguration) XCTAssertEqual(client.executionConfiguration(forMethod: secondDescriptor), defaultConfiguration) @@ -259,7 +259,7 @@ final class InProcessClientTransportTests: XCTestCase { ) return InProcessClientTransport( server: server, - executionConfigurations: methodConfiguration + methodConfiguration: methodConfiguration ) } } diff --git a/Tests/GRPCCoreTests/Transport/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift similarity index 79% rename from Tests/GRPCCoreTests/Transport/InProcessServerTransportTests.swift rename to Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 83cfeb6b1..febdeb977 100644 --- a/Tests/GRPCCoreTests/Transport/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -17,13 +17,19 @@ import XCTest @testable import GRPCCore +@testable import GRPCInProcessTransport final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessServerTransport() let stream = RPCStream, RPCWriter.Closable>( descriptor: .init(service: "testService", method: "testMethod"), - inbound: .elements([.message([42])]), + inbound: RPCAsyncSequence( + wrapping: AsyncStream { + $0.yield(.message([42])) + $0.finish() + } + ), outbound: .init( wrapping: BufferedStream.Source( storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) @@ -37,7 +43,7 @@ final class InProcessServerTransportTests: XCTestCase { try transport.acceptStream(stream) let testStream = try await streamSequenceInterator.next() - let messages = try await testStream?.inbound.collect() + let messages = try await testStream?.inbound.reduce(into: []) { $0.append($1) } XCTAssertEqual(messages, [.message([42])]) } @@ -47,7 +53,12 @@ final class InProcessServerTransportTests: XCTestCase { RPCAsyncSequence, RPCWriter.Closable >( descriptor: .init(service: "testService1", method: "testMethod1"), - inbound: .elements([.message([42])]), + inbound: RPCAsyncSequence( + wrapping: AsyncStream { + $0.yield(.message([42])) + $0.finish() + } + ), outbound: .init( wrapping: BufferedStream.Source( storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) @@ -61,7 +72,7 @@ final class InProcessServerTransportTests: XCTestCase { try transport.acceptStream(firstStream) let firstTestStream = try await streamSequenceInterator.next() - let firstStreamMessages = try await firstTestStream?.inbound.collect() + let firstStreamMessages = try await firstTestStream?.inbound.reduce(into: []) { $0.append($1) } XCTAssertEqual(firstStreamMessages, [.message([42])]) transport.stopListening() @@ -70,7 +81,12 @@ final class InProcessServerTransportTests: XCTestCase { RPCAsyncSequence, RPCWriter.Closable >( descriptor: .init(service: "testService1", method: "testMethod1"), - inbound: .elements([.message([42])]), + inbound: RPCAsyncSequence( + wrapping: AsyncStream { + $0.yield(.message([42])) + $0.finish() + } + ), outbound: .init( wrapping: BufferedStream.Source( storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) @@ -78,7 +94,9 @@ final class InProcessServerTransportTests: XCTestCase { ) ) - XCTAssertThrowsRPCError(try transport.acceptStream(secondStream)) { error in + XCTAssertThrowsError(ofType: RPCError.self) { + try transport.acceptStream(secondStream) + } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) } diff --git a/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift new file mode 100644 index 000000000..b49af0ec9 --- /dev/null +++ b/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift @@ -0,0 +1,44 @@ +/* + * Copyright 2023, 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 XCTest + +func XCTAssertThrowsError( + ofType: E.Type, + _ expression: () throws -> T, + errorHandler: (E) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? E else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + errorHandler(error) + } +} + +func XCTAssertThrowsErrorAsync( + ofType: E.Type = E.self, + _ expression: () async throws -> T, + errorHandler: (E) -> Void +) async { + do { + _ = try await expression() + XCTFail("Expression didn't throw") + } catch let error as E { + errorHandler(error) + } catch { + XCTFail("Error had unexpected type '\(type(of: error))'") + } +} From 2e1114a54a0f1c0563bf2cdd9cb4bd834f0feb7d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 4 Dec 2023 11:12:29 +0000 Subject: [PATCH 184/580] Make the reflection service a product (#1732) Motivation: The reflection service isn't usable outside of the package as it isn't exposed as a product. Modifications: - Add a "GRPCReflectionService" product Result: Reflection service is available --- Package.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 757b29cc9..df29fbf25 100644 --- a/Package.swift +++ b/Package.swift @@ -87,6 +87,7 @@ extension Target.Dependency { static let grpc: Self = .target(name: grpcTargetName) static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") + static let reflectionService: Self = .target(name: "GRPCReflectionService") static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") // Target dependencies; internal @@ -98,7 +99,6 @@ extension Target.Dependency { static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") static let interopTestImplementation: Self = .target(name: "GRPCInteroperabilityTestsImplementation") - static let reflectionService: Self = .target(name: "GRPCReflectionService") // Product dependencies static let argumentParser: Self = .product( @@ -528,6 +528,11 @@ extension Product { targets: [cgrpcZlibTargetName] ) + static let grpcReflectionService: Product = .library( + name: "GRPCReflectionService", + targets: ["GRPCReflectionService"] + ) + static let protocGenGRPCSwift: Product = .executable( name: "protoc-gen-grpc-swift", targets: ["protoc-gen-grpc-swift"] @@ -547,6 +552,7 @@ let package = Package( .grpc, .grpcCore, .cgrpcZlib, + .grpcReflectionService, .protocGenGRPCSwift, .grpcSwiftPlugin, ], From a7b4240192c31f18676fab5872e3fc5fa9fe1fe5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 4 Dec 2023 11:17:06 +0000 Subject: [PATCH 185/580] Drop the '.txt' extension from reflection data (#1731) Motivation: Generated reflection data has a ".txt" extension. This isn't quite right as the contents is base64 encdoded data. Moreover the generated reflection data is intended as an implementation detail for the reflection service. Modifications: - Change the generated reflection data extension from ".grpc.reflection.txt" to ".grpc.reflection". - Fix a bug in the reflection server example where resources couldn't be loaded because no subdirectory was provided Result: Generated reflection data files don't have a ".txt" extension --- Makefile | 14 +++--- Package.swift | 2 +- ...pc.reflection.txt => echo.grpc.reflection} | 0 ...lection.txt => helloworld.grpc.reflection} | 0 .../ReflectionService/ReflectionServer.swift | 9 +++- .../ReflectionServiceTutorial.md | 45 ++++++++++--------- Sources/protoc-gen-grpc-swift/main.swift | 4 +- .../Serialization/SerializationTests.swift | 2 +- ...pc.reflection.txt => echo.grpc.reflection} | 0 9 files changed, 43 insertions(+), 33 deletions(-) rename Sources/Examples/ReflectionService/Generated/{echo.grpc.reflection.txt => echo.grpc.reflection} (100%) rename Sources/Examples/ReflectionService/Generated/{helloworld.grpc.reflection.txt => helloworld.grpc.reflection} (100%) rename Tests/GRPCTests/Codegen/Serialization/{echo.grpc.reflection.txt => echo.grpc.reflection} (100%) diff --git a/Makefile b/Makefile index 381fe0ae1..3a540a8f1 100644 --- a/Makefile +++ b/Makefile @@ -117,7 +117,7 @@ ${NORMALIZATION_GRPC}: ${NORMALIZATION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} .PHONY: generate-normalization: ${NORMALIZATION_PB} ${NORMALIZATION_GRPC} -SERIALIZATION_GRPC_REFLECTION=Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt +SERIALIZATION_GRPC_REFLECTION=Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection # For serialization we'll set the ReflectionData option to true. ${SERIALIZATION_GRPC_REFLECTION}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} @@ -154,7 +154,7 @@ ${REFLECTION_V1ALPHA_GRPC}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_GRPC_SWIFT} --plugin=${PROTOC_GEN_GRPC_SWIFT} \ --grpc-swift_opt=Client=false \ --grpc-swift_out=$(dir $<) - + # Generates protobufs and gRPC server for the Reflection Service .PHONY: generate-reflection: ${REFLECTION_V1_PB} ${REFLECTION_V1_GRPC} ${REFLECTION_V1ALPHA_PB} ${REFLECTION_V1ALPHA_GRPC} @@ -191,12 +191,12 @@ ${TEST_REFLECTION_V1ALPHA_PB}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_SWIFT} --proto_path=$(dir $<) \ --plugin=${PROTOC_GEN_SWIFT} \ --swift_out=$(dir ${TEST_REFLECTION_V1ALPHA_PB}) - + # Generates protobufs and gRPC clients for the Reflection Service Tests .PHONY: generate-reflection-test-clients: ${TEST_REFLECTION_V1_PB} ${TEST_REFLECTION_V1_GRPC} ${TEST_REFLECTION_V1ALPHA_PB} ${TEST_REFLECTION_V1ALPHA_GRPC} -HELLOWORLD_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt +HELLOWORLD_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection ${HELLOWORLD_SERIALIZED_PROTO_GRPC}: ${HELLOWORLD_PROTO} ${PROTOC_GEN_GRPC_SWIFT} protoc $< \ @@ -204,11 +204,11 @@ ${HELLOWORLD_SERIALIZED_PROTO_GRPC}: ${HELLOWORLD_PROTO} ${PROTOC_GEN_GRPC_SWIFT --plugin=${PROTOC_GEN_GRPC_SWIFT} \ --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ --grpc-swift_out=$(dir ${HELLOWORLD_SERIALIZED_PROTO_GRPC}) - + .PHONY: generate-helloworld-reflection-data: ${HELLOWORLD_SERIALIZED_PROTO_GRPC} -ECHO_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt +ECHO_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/echo.grpc.reflection ${ECHO_SERIALIZED_PROTO_GRPC}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} protoc $< \ @@ -216,7 +216,7 @@ ${ECHO_SERIALIZED_PROTO_GRPC}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} --plugin=${PROTOC_GEN_GRPC_SWIFT} \ --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ --grpc-swift_out=$(dir ${ECHO_SERIALIZED_PROTO_GRPC}) - + .PHONY: generate-echo-reflection-data: ${ECHO_SERIALIZED_PROTO_GRPC} diff --git a/Package.swift b/Package.swift index df29fbf25..27316039d 100644 --- a/Package.swift +++ b/Package.swift @@ -230,7 +230,7 @@ extension Target { ), exclude: [ "Codegen/Normalization/normalization.proto", - "Codegen/Serialization/echo.grpc.reflection.txt", + "Codegen/Serialization/echo.grpc.reflection", ] ) diff --git a/Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt b/Sources/Examples/ReflectionService/Generated/echo.grpc.reflection similarity index 100% rename from Sources/Examples/ReflectionService/Generated/echo.grpc.reflection.txt rename to Sources/Examples/ReflectionService/Generated/echo.grpc.reflection diff --git a/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt b/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection similarity index 100% rename from Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection.txt rename to Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection diff --git a/Sources/Examples/ReflectionService/ReflectionServer.swift b/Sources/Examples/ReflectionService/ReflectionServer.swift index ddad1f32d..ba07e2b96 100644 --- a/Sources/Examples/ReflectionService/ReflectionServer.swift +++ b/Sources/Examples/ReflectionService/ReflectionServer.swift @@ -31,9 +31,14 @@ struct ReflectionServer: AsyncParsableCommand { guard let greeterURL = Bundle.module.url( forResource: "helloworld", - withExtension: "grpc.reflection.txt" + withExtension: "grpc.reflection", + subdirectory: "Generated" ), - let echoURL = Bundle.module.url(forResource: "echo", withExtension: "grpc.reflection.txt") + let echoURL = Bundle.module.url( + forResource: "echo", + withExtension: "grpc.reflection", + subdirectory: "Generated" + ) else { print("The resource could not be loaded.") throw ExitCode.failure diff --git a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md index 98b03f475..68db44286 100644 --- a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md +++ b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md @@ -1,21 +1,21 @@ # Reflection service -This tutorial goes through the steps of adding Reflection service to a -server, running it and testing it using gRPCurl. +This tutorial goes through the steps of adding Reflection service to a +server, running it and testing it using gRPCurl. - The server used in this example is implemented at + The server used in this example is implemented at [Sources/Examples/ReflectionService/ReflectionServer.swift][reflection-server] - and it supports the "Greeter", "Echo", and "Reflection" services. + and it supports the "Greeter", "Echo", and "Reflection" services. ## Overview -The Reflection service provides information about the public RPCs served by a server. +The Reflection service provides information about the public RPCs served by a server. It is specific to services defined using the Protocol Buffers IDL. By calling the Reflection service, clients can construct and send requests to services -without needing to generate code and types for them. +without needing to generate code and types for them. -You can also use CLI clients such as [gRPCurl][grpcurl-setup] and the [gRPC command line tool][grpc-cli] to: +You can also use CLI clients such as [gRPCurl][grpcurl-setup] and the [gRPC command line tool][grpc-cli] to: - list services, - describe services and their methods, - describe symbols, @@ -28,9 +28,9 @@ gRPC Swift supports both [v1][v1] and [v1alpha][v1alpha] of the reflection servi You can use the Reflection service by adding it as a provider when constructing your server. -To initialise the Reflection service we will use +To initialise the Reflection service we will use ``GRPCReflectionService/ReflectionService/init(reflectionDataFileURLs:version:)``. -It receives the URLs of the files containing the reflection data of the proto files +It receives the URLs of the files containing the reflection data of the proto files describing the services of the server and the version of the reflection service. ### Generating the reflection data @@ -38,7 +38,7 @@ describing the services of the server and the version of the reflection service. The server from this example uses the `GreeterProvider` and the `EchoProvider`, besides the `ReflectionService`. -The associated proto files are located at `Sources/Examples/HelloWorld/Model/helloworld.proto`, and +The associated proto files are located at `Sources/Examples/HelloWorld/Model/helloworld.proto`, and `Sources/Examples/Echo/Model/echo.proto` respectively. In order to generate the reflection data for the `helloworld.proto`, you can run the following command: @@ -51,8 +51,8 @@ $ protoc Sources/Examples/HelloWorld/Model/helloworld.proto \ ``` Let's break the command down: -- The first argument passed to `protoc` is the path - to the `.proto` file to generate reflection data +- The first argument passed to `protoc` is the path + to the `.proto` file to generate reflection data for: [`Sources/Examples/HelloWorld/Model/helloworld.proto`][helloworld-proto]. - The `proto_path` flag is the path to search for imports: `Sources/Examples/HelloWorld/Model`. - The 'grpc-swift_opt' flag allows us to list options for the Swift generator. @@ -61,18 +61,18 @@ Let's break the command down: where the generated file will be located: `Sources/Examples/ReflectionService/Generated`. This command assumes that the `protoc-gen-grpc-swift` plugin is in your `$PATH` environment variable. -You can learn how to get the plugin from this section of the `grpc-swift` README: +You can learn how to get the plugin from this section of the `grpc-swift` README: https://github.com/grpc/grpc-swift#getting-the-protoc-plugins. The command for generating the reflection data for the `Echo` service is similar. -You can use Swift Package Manager [resources][swiftpm-resources] to add the generated reflection data to your target. -In our example the reflection data is written into the "Generated" directory within the target +You can use Swift Package Manager [resources][swiftpm-resources] to add the generated reflection data to your target. +In our example the reflection data is written into the "Generated" directory within the target so we include the `.copy("Generated")` rule in our target's resource list. -### Instantiating the Reflection service +### Instantiating the Reflection service -To instantiate the `ReflectionService` you need to pass the URLs of the files containing +To instantiate the `ReflectionService` you need to pass the URLs of the files containing the generated reflection data and the version to use, in our case `.v1`. Depending on the version of [gRPCurl][grpcurl] you are using you might need to use the `.v1alpha` instead. @@ -84,9 +84,14 @@ reflection. guard let greeterURL = Bundle.module.url( forResource: "helloworld", - withExtension: "grpc.reflection.txt" + withExtension: "grpc.reflection", + subdirectory: "Generated" ), - let echoURL = Bundle.module.url(forResource: "echo", withExtension: "grpc.reflection.txt") + let echoURL = Bundle.module.url( + forResource: "echo", + withExtension: "grpc.reflection", + subdirectory: "Generated" + ) else { print("The resource could not be loaded.") throw ExitCode.failure @@ -123,7 +128,7 @@ Please follow the instructions from the [gRPCurl README][grpcurl-setup] to set u From a different terminal than the one used for running the server, we will call gRPCurl commands, following the format: `grpcurl [flags] [address] [list|describe] [symbol]`. -We use the `-plaintext` flag, because the server isn't configured with TLS, and +We use the `-plaintext` flag, because the server isn't configured with TLS, and the address is set to `localhost:1234`. diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 4a1b47b8a..d4b159cb2 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -144,11 +144,11 @@ func main(args: [String]) throws { if options.generateReflectionData { var binaryFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() let binaryFileName = uniqueOutputFileName( - component: "grpc.reflection", + component: "grpc", fileDescriptor: fileDescriptor, fileNamingOption: options.fileNaming, generatedFiles: &generatedFiles, - extension: "txt" + extension: "reflection" ) let serializedFileDescriptorProto = try fileDescriptor.proto.serializedData() .base64EncodedString() diff --git a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift index f8f9174e9..e9584273c 100644 --- a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift +++ b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift @@ -25,7 +25,7 @@ final class SerializationTests: GRPCTestCase { override func setUp() { super.setUp() let binaryFileURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent().appendingPathComponent("echo.grpc.reflection.txt") + .deletingLastPathComponent().appendingPathComponent("echo.grpc.reflection") let base64EncodedData = try! Data(contentsOf: binaryFileURL) let binaryData = Data(base64Encoded: base64EncodedData)! self diff --git a/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt b/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection similarity index 100% rename from Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt rename to Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection From ab82da88e9e5b641efad59482b9cbc794cf0e029 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 6 Dec 2023 15:07:03 +0000 Subject: [PATCH 186/580] Make the retry throttle optional (#1734) Motivation: The retry throttle should be optional, it's currently required by the client transport API. The in-process transport also defaults the throttle without allowing callers to configure it. Modifications: - Make the retry throttle an optional requirement on client transport - Allow callers to configure the retry throttle for the in-process client transport - Add `async throws` to one of the in-process server transport methods, while not necessary, the protocol allows it and we may want to change the implementation in the future. Result: Retry throttle is optional and configurable for the in-process transport --- .../ClientRPCExecutor+HedgingExecutor.swift | 6 +++--- .../Internal/ClientRPCExecutor+RetryExecutor.swift | 6 +++--- Sources/GRPCCore/Transport/ClientTransport.swift | 2 +- .../InProcessClientTransport.swift | 13 ++++++++++--- .../InProcessServerTransport.swift | 2 +- .../Test Utilities/Transport/AnyTransport.swift | 4 ++-- .../Transport/StreamCountingTransport.swift | 2 +- .../Transport/ThrowingTransport.swift | 2 +- .../InProcessClientTransportTests.swift | 2 +- .../InProcessServerTransportTests.swift | 4 ++-- 10 files changed, 25 insertions(+), 18 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 63f46db74..aba9d9423 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -359,7 +359,7 @@ extension ClientRPCExecutor.HedgingExecutor { case .response(let response): switch response.accepted { case .success: - self.transport.retryThrottle.recordSuccess() + self.transport.retryThrottle?.recordSuccess() if state.withLockedValue({ $0.receivedUsableResponse() }) { try? await picker.continuation.write(attempt) @@ -376,11 +376,11 @@ extension ClientRPCExecutor.HedgingExecutor { if self.policy.nonFatalStatusCodes.contains(Status.Code(error.code)) { // The response failed and the status code is non-fatal, we can make another attempt. - self.transport.retryThrottle.recordFailure() + self.transport.retryThrottle?.recordFailure() return .unusableResponse(response, error.metadata.retryPushback) } else { // A fatal error code counts as a success to the throttle. - self.transport.retryThrottle.recordSuccess() + self.transport.retryThrottle?.recordSuccess() if state.withLockedValue({ $0.receivedUsableResponse() }) { try! await picker.continuation.write(attempt) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 619352723..1f9fd549a 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -152,7 +152,7 @@ extension ClientRPCExecutor.RetryExecutor { case .success: // Request was accepted. This counts as success to the throttle and there's no need // to retry. - self.transport.retryThrottle.recordSuccess() + self.transport.retryThrottle?.recordSuccess() retryDelayOverride = nil shouldRetry = false @@ -170,7 +170,7 @@ extension ClientRPCExecutor.RetryExecutor { if isRetryableStatusCode { // Counted as failure for throttling. - let throttled = self.transport.retryThrottle.recordFailure() + let throttled = self.transport.retryThrottle?.recordFailure() ?? false // Status code can be retried, Did the server send pushback? switch error.metadata.retryPushback { @@ -190,7 +190,7 @@ extension ClientRPCExecutor.RetryExecutor { } } else { // Not-retryable; this is considered a success. - self.transport.retryThrottle.recordSuccess() + self.transport.retryThrottle?.recordSuccess() shouldRetry = false retryDelayOverride = nil } diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 913712ea7..989e31801 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -24,7 +24,7 @@ public protocol ClientTransport: Sendable { /// Client transports don't need to implement the throttle or interact with it beyond its /// creation. gRPC will record the results of requests to determine whether retries can be /// performed. - var retryThrottle: RetryThrottle { get } + var retryThrottle: RetryThrottle? { get } /// Establish and maintain a connection to the remote destination. /// diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index b42792146..9a9b5ec95 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -96,16 +96,23 @@ public struct InProcessClientTransport: ClientTransport { public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable - public let retryThrottle: RetryThrottle + public let retryThrottle: RetryThrottle? private let methodConfiguration: MethodConfigurations private let state: _LockedValueBox + /// Creates a new in-process client transport. + /// + /// - Parameters: + /// - server: The in-process server transport to connect to. + /// - methodConfiguration: Method specific configuration. + /// - retryThrottle: A throttle to apply to RPCs which are hedged or retried. public init( server: InProcessServerTransport, - methodConfiguration: MethodConfigurations = MethodConfigurations() + methodConfiguration: MethodConfigurations = MethodConfigurations(), + retryThrottle: RetryThrottle? = nil ) { - self.retryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + self.retryThrottle = retryThrottle self.methodConfiguration = methodConfiguration self.state = _LockedValueBox(.unconnected(.init(serverTransport: server))) } diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 95181b00c..845d38c19 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -59,7 +59,7 @@ public struct InProcessServerTransport: ServerTransport, Sendable { /// to this transport using the ``acceptStream(_:)`` method. /// /// - Returns: An ``RPCAsyncSequence`` of all published ``RPCStream``s. - public func listen() -> RPCAsyncSequence> { + public func listen() async throws -> RPCAsyncSequence> { RPCAsyncSequence(wrapping: self.newStreams) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index 5fb153bee..e211d214c 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -20,7 +20,7 @@ struct AnyClientTransport: ClientTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable - private let _retryThrottle: @Sendable () -> RetryThrottle + private let _retryThrottle: @Sendable () -> RetryThrottle? private let _withStream: @Sendable ( _ method: MethodDescriptor, @@ -52,7 +52,7 @@ struct AnyClientTransport: ClientTransport, Sendable { } } - var retryThrottle: RetryThrottle { + var retryThrottle: RetryThrottle? { self._retryThrottle() } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 0cdb2d1fd..9c6ebb276 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -39,7 +39,7 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { self.transport = AnyClientTransport(wrapping: transport) } - var retryThrottle: RetryThrottle { + var retryThrottle: RetryThrottle? { self.transport.retryThrottle } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index def987fac..39cdbaceb 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -26,7 +26,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { self.code = code } - let retryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + let retryThrottle: RetryThrottle? = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) func connect(lazily: Bool) async throws { // no-op diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index e751e364a..7aa64d05c 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -162,7 +162,7 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - for try await stream in server.listen() { + for try await stream in try await server.listen() { let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } try await stream.outbound.write(RPCResponsePart.message([42])) stream.outbound.finish() diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index febdeb977..3e57d6ac4 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -37,7 +37,7 @@ final class InProcessServerTransportTests: XCTestCase { ) ) - let streamSequence = transport.listen() + let streamSequence = try await transport.listen() var streamSequenceInterator = streamSequence.makeAsyncIterator() try transport.acceptStream(stream) @@ -66,7 +66,7 @@ final class InProcessServerTransportTests: XCTestCase { ) ) - let streamSequence = transport.listen() + let streamSequence = try await transport.listen() var streamSequenceInterator = streamSequence.makeAsyncIterator() try transport.acceptStream(firstStream) From 39c567aab39703081308ee65ec5305499d2415c6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Dec 2023 10:32:28 +0000 Subject: [PATCH 187/580] Add public API to create connection IDs (#1737) Motivation: Each connection pool delegate method relies on a connection ID. IDs are opaque to users and are created by gRPC from the underlying connection manager. However, this makes testing a connection pool delegate difficult as users must create real connections. Modifications: - Add a public init to the connection ID Result: Users can test connection pool delegate implementations without having a connection pool. --- .../GRPC/ConnectionPool/GRPCChannelPool.swift | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift index 6011d071d..8076342a0 100644 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift @@ -17,6 +17,8 @@ import Logging import NIOCore import NIOPosix +import struct Foundation.UUID + public enum GRPCChannelPool { /// Make a new ``GRPCChannel`` on which calls may be made to gRPC services. /// @@ -290,14 +292,32 @@ extension GRPCChannelPool.Configuration { /// The ID of a connection in the connection pool. public struct GRPCConnectionID: Hashable, Sendable, CustomStringConvertible { - private let id: ConnectionManagerID + private enum Value: Sendable, Hashable { + case managerID(ConnectionManagerID) + case uuid(UUID) + } + + private let id: Value public var description: String { - return String(describing: self.id) + switch self.id { + case .managerID(let id): + return String(describing: id) + case .uuid(let uuid): + return String(describing: uuid) + } } internal init(_ id: ConnectionManagerID) { - self.id = id + self.id = .managerID(id) + } + + /// Create a new unique connection ID. + /// + /// Normally you don't have to create connection IDs, gRPC will create them on your behalf. + /// However creating them manually is useful when testing the ``GRPCConnectionPoolDelegate``. + public init() { + self.id = .uuid(UUID()) } } From 79865359b894e8334e0d3b520294ceb2dfc7bcfd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Dec 2023 14:08:13 +0000 Subject: [PATCH 188/580] Simplify client and server init (#1735) Motivation: When configuring a client and server must users will specify only the transport and services (for a server) and potentially interceptors too. This should be the "easy" path. Moreover these are all reference types so it makes sense to separate them from "raw" configuration. Creating a client also diverged somewhat from the server. Having both follow a similar pattern simplifies things for users. Modifications: - Move interceptors from the client config to the init. - Modify the server to follow a similar pattern to the server. Results: Server and client are configured similarly. --- Sources/GRPCCore/ClientError.swift | 6 - Sources/GRPCCore/GRPCClient.swift | 101 ++--- Sources/GRPCCore/GRPCServer.swift | 344 +++++++----------- .../GRPCCore/Transport/ServerTransport.swift | 2 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 46 +-- Tests/GRPCCoreTests/GRPCServerTests.swift | 76 +--- 6 files changed, 186 insertions(+), 389 deletions(-) diff --git a/Sources/GRPCCore/ClientError.swift b/Sources/GRPCCore/ClientError.swift index 77ad48719..ddf4937dc 100644 --- a/Sources/GRPCCore/ClientError.swift +++ b/Sources/GRPCCore/ClientError.swift @@ -109,7 +109,6 @@ extension ClientError { public struct Code: Hashable, Sendable { private enum Value { case clientIsAlreadyRunning - case clientIsNotRunning case clientIsStopped case transportError } @@ -124,11 +123,6 @@ extension ClientError { Self(.clientIsAlreadyRunning) } - /// An attempt to start an RPC was made but the client is not running. - public static var clientIsNotRunning: Self { - Self(.clientIsNotRunning) - } - /// At attempt to start the client was made but it has already stopped. public static var clientIsStopped: Self { Self(.clientIsStopped) diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 848e01297..22f054f59 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -40,9 +40,6 @@ import Atomics /// // Create a configuration object for the client. /// var configuration = GRPCClient.Configuration() /// -/// // Create and add an interceptor. -/// configuration.interceptors.add(StatsRecordingClientInterceptor()) -/// /// // Override the timeout for the 'Get' method on the 'echo.Echo' service. This configuration /// // takes precedence over any set by the transport. /// let echoGet = MethodDescriptor(service: "echo.Echo", method: "Get") @@ -56,10 +53,15 @@ import Atomics /// let defaultMethodConfiguration = MethodConfiguration(executionPolicy: nil, timeout: seconds(10)) /// configuration.method.defaults.setDefaultConfiguration(defaultMethodConfiguration) /// -/// // Finally create a transport and instantiate the client. +/// // Finally create a transport and instantiate the client, adding an interceptor. /// let inProcessServerTransport = InProcessServerTransport() /// let inProcessClientTransport = InProcessClientTransport(serverTransport: inProcessServerTransport) -/// let client = GRPCClient(transport: inProcessClientTransport, configuration: configuration) +/// +/// let client = GRPCClient( +/// transport: inProcessClientTransport, +/// interceptors: [StatsRecordingClientInterceptor()], +/// configuration: configuration +/// ) /// ``` /// /// ## Starting and stopping the client @@ -101,6 +103,14 @@ public struct GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. private let transport: any ClientTransport + /// A collection of interceptors providing cross-cutting functionality to each accepted RPC. + /// + /// The order in which interceptors are added reflects the order in which they are called. The + /// first interceptor added will be the first interceptor to intercept each request. The last + /// interceptor added will be the final interceptor to intercept each request before calling + /// the appropriate handler. + private let interceptors: [any ClientInterceptor] + /// The configuration used by the client. public let configuration: Configuration @@ -121,13 +131,23 @@ public struct GRPCClient: Sendable { case stopped } - /// Creates a new client with the given transport and configuration. + /// Creates a new client with the given transport, interceptors and configuration. /// /// - Parameters: /// - transport: The transport used to establish a communication channel with a server. + /// - interceptors: A collection of interceptors providing cross-cutting functionality to each + /// accepted RPC. The order in which interceptors are added reflects the order in which they + /// are called. The first interceptor added will be the first interceptor to intercept each + /// request. The last interceptor added will be the final interceptor to intercept each + /// request before calling the appropriate handler. /// - configuration: Configuration for the client. - public init(transport: some ClientTransport, configuration: Configuration = Configuration()) { + public init( + transport: some ClientTransport, + interceptors: [any ClientInterceptor] = [], + configuration: Configuration = Configuration() + ) { self.transport = transport + self.interceptors = interceptors self.configuration = configuration self.state = ManagedAtomic(.notStarted) } @@ -250,7 +270,7 @@ public struct GRPCClient: Sendable { deserializer: some MessageDeserializer, handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue ) async throws -> ReturnValue { - try await bidirectionalStreaming( + try await self.bidirectionalStreaming( request: ClientRequest.Stream(single: request), descriptor: descriptor, serializer: serializer, @@ -278,7 +298,7 @@ public struct GRPCClient: Sendable { deserializer: some MessageDeserializer, handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue ) async throws -> ReturnValue { - try await bidirectionalStreaming( + try await self.bidirectionalStreaming( request: request, descriptor: descriptor, serializer: serializer, @@ -306,7 +326,7 @@ public struct GRPCClient: Sendable { deserializer: some MessageDeserializer, handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { - try await bidirectionalStreaming( + try await self.bidirectionalStreaming( request: ClientRequest.Stream(single: request), descriptor: descriptor, serializer: serializer, @@ -336,13 +356,10 @@ public struct GRPCClient: Sendable { handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { switch self.state.load(ordering: .sequentiallyConsistent) { - case .running: + case .notStarted, .running: + // Allow .notStarted as making a request can race with 'run()'. Transports should tolerate + // queuing the request if not yet started. () - case .notStarted: - throw ClientError( - code: .clientIsNotRunning, - message: "Client must be running to make an RPC: call run() first." - ) case .stopping, .stopped: throw ClientError( code: .clientIsStopped, @@ -357,7 +374,7 @@ public struct GRPCClient: Sendable { serializer: serializer, deserializer: deserializer, transport: self.transport, - interceptors: self.configuration.interceptors.values, + interceptors: self.interceptors, handler: handler ) } @@ -383,14 +400,6 @@ public struct GRPCClient: Sendable { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCClient { public struct Configuration: Sendable { - /// A collection of interceptors providing cross-cutting functionality to each accepted RPC. - /// - /// The order in which interceptors are added reflects the order in which they are called. The - /// first interceptor added will be the first interceptor to intercept each request. The last - /// interceptor added will be the final interceptor to intercept each request before calling - /// the appropriate handler. - public var interceptors: Interceptors - /// Configuration for how methods are executed. /// /// Method configuration determines how each RPC is executed by the client. Some services and @@ -401,7 +410,6 @@ extension GRPCClient { /// Creates a new default configuration. public init() { - self.interceptors = Interceptors() self.method = Method() } } @@ -409,40 +417,6 @@ extension GRPCClient { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCClient.Configuration { - /// A collection of ``ClientInterceptor`` implementations which are applied to all accepted - /// RPCs. - /// - /// RPCs are intercepted in the order that interceptors are added. That is, a request sent from the client to - /// the server will first be intercepted by the first added interceptor followed by the second, and so on. - /// For responses from the server, they'll be applied in the opposite order. - public struct Interceptors: Sendable { - private(set) var values: [any ClientInterceptor] = [] - - /// Add an interceptor to the client. - /// - /// The order in which interceptors are added reflects the order in which they are called. The - /// first interceptor added will be the first interceptor to intercept each request. The last - /// interceptor added will be the final interceptor to intercept each request before calling - /// the appropriate handler. - /// - /// - Parameter interceptor: The interceptor to add. - public mutating func add(_ interceptor: some ClientInterceptor) { - self.values.append(interceptor) - } - - /// Adds a sequence of interceptor to the client. - /// - /// The order in which interceptors are added reflects the order in which they are called. The - /// first interceptor added will be the first interceptor to intercept each request. The last - /// interceptor added will be the final interceptor to intercept each request before calling - /// the appropriate handler. - /// - /// - Parameter interceptors: The interceptors to add. - public mutating func add(contentsOf interceptors: some Sequence) { - self.values.append(contentsOf: interceptors) - } - } - /// Configuration for how methods should be executed. /// /// In most cases the client should defer to the configuration provided by the transport as this @@ -464,10 +438,3 @@ extension GRPCClient.Configuration { } } } - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCClient.Configuration.Interceptors: CustomStringConvertible { - public var description: String { - return String(describing: self.values.map { String(describing: type(of: $0)) }) - } -} diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 626816f9b..7f953fb6e 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -34,18 +34,22 @@ import Atomics /// The following example demonstrates how to create and configure a server. /// /// ```swift -/// let server = GRPCServer() -/// -/// // Create and add an in-process transport. +/// // Create and an in-process transport. /// let inProcessTransport = InProcessServerTransport() -/// server.transports.add(inProcessTransport) /// -/// // Create and register the 'Greeter' and 'Echo' services. -/// server.services.register(GreeterService()) -/// server.services.register(EchoService()) +/// // Create the 'Greeter' and 'Echo' services. +/// let greeter = GreeterService() +/// let echo = EchoService() +/// +/// // Create an interceptor. +/// let statsRecorder = StatsRecordingServerInterceptors() /// -/// // Create and add some interceptors. -/// server.interceptors.add(StatsRecordingServerInterceptors()) +/// // Finally create the server. +/// let server = GRPCServer( +/// transports: [inProcessTransport], +/// services: [greeter, echo], +/// interceptors: [statsRecorder] +/// ) /// ``` /// /// ## Starting and stopping the server @@ -66,29 +70,15 @@ import Atomics /// that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public final class GRPCServer: Sendable { +public struct GRPCServer: Sendable { typealias Stream = RPCStream /// A collection of ``ServerTransport`` implementations that the server uses to listen /// for new requests. - public var transports: Transports { - get { - self.storage.withLockedValue { $0.transports } - } - set { - self.storage.withLockedValue { $0.transports = newValue } - } - } + private let transports: [any ServerTransport] /// The services registered which the server is serving. - public var services: Services { - get { - self.storage.withLockedValue { $0.services } - } - set { - self.storage.withLockedValue { $0.services = newValue } - } - } + private let router: RPCRouter /// A collection of ``ServerInterceptor`` implementations which are applied to all accepted /// RPCs. @@ -96,34 +86,12 @@ public final class GRPCServer: Sendable { /// RPCs are intercepted in the order that interceptors are added. That is, a request received /// from the client will first be intercepted by the first added interceptor followed by the /// second, and so on. - public var interceptors: Interceptors { - get { - self.storage.withLockedValue { $0.interceptors } - } - set { - self.storage.withLockedValue { $0.interceptors = newValue } - } - } - - /// Underlying storage for the server. - private struct Storage { - var transports: Transports - var services: Services - var interceptors: Interceptors - var state: State - - init() { - self.transports = Transports() - self.services = Services() - self.interceptors = Interceptors() - self.state = .notStarted - } - } - - private let storage: LockedValueBox + private let interceptors: [any ServerInterceptor] /// The state of the server. - private enum State { + private let state: ManagedAtomic + + private enum State: UInt8, AtomicValue { /// The server hasn't been started yet. Can transition to `starting` or `stopped`. case notStarted /// The server is starting but isn't accepting requests yet. Can transition to `running` @@ -141,11 +109,46 @@ public final class GRPCServer: Sendable { /// Creates a new server with no resources. /// - /// You can add resources to the server via ``transports-swift.property``, - /// ``services-swift.property``, and ``interceptors-swift.property`` and start the server by - /// calling ``run()``. Any changes to resources after ``run()`` has been called will be ignored. - public init() { - self.storage = LockedValueBox(Storage()) + /// - Parameters: + /// - transports: The transports the server should listen on. + /// - services: Services offered by the server. + /// - interceptors: A collection of interceptors providing cross-cutting functionality to each + /// accepted RPC. The order in which interceptors are added reflects the order in which they + /// are called. The first interceptor added will be the first interceptor to intercept each + /// request. The last interceptor added will be the final interceptor to intercept each + /// request before calling the appropriate handler. + public init( + transports: [any ServerTransport], + services: [any RegistrableRPCService], + interceptors: [any ServerInterceptor] = [] + ) { + var router = RPCRouter() + for service in services { + service.registerMethods(with: &router) + } + + self.init(transports: transports, router: router, interceptors: interceptors) + } + + /// Creates a new server with no resources. + /// + /// - Parameters: + /// - transports: The transports the server should listen on. + /// - router: A ``RPCRouter`` used by the server to route accepted streams to method handlers. + /// - interceptors: A collection of interceptors providing cross-cutting functionality to each + /// accepted RPC. The order in which interceptors are added reflects the order in which they + /// are called. The first interceptor added will be the first interceptor to intercept each + /// request. The last interceptor added will be the final interceptor to intercept each + /// request before calling the appropriate handler. + public init( + transports: [any ServerTransport], + router: RPCRouter, + interceptors: [any ServerInterceptor] = [] + ) { + self.state = ManagedAtomic(.notStarted) + self.transports = transports + self.router = router + self.interceptors = interceptors } /// Starts the server and runs until all registered transports have closed. @@ -159,20 +162,19 @@ public final class GRPCServer: Sendable { /// /// To stop the server more abruptly you can cancel the task that this function is running in. /// - /// You must register all resources you wish to use with the server before calling this function - /// as changes made after calling ``run()`` won't be reflected. - /// /// - Note: You can only call this function once, repeated calls will result in a /// ``ServerError`` being thrown. - /// - Important: You must register at least one transport by calling - /// ``Transports-swift.struct/add(_:)`` before calling this method. public func run() async throws { - let (transports, router, interceptors) = try self.storage.withLockedValue { storage in - switch storage.state { + let (wasNotStarted, actualState) = self.state.compareExchange( + expected: .notStarted, + desired: .starting, + ordering: .sequentiallyConsistent + ) + + guard wasNotStarted else { + switch actualState { case .notStarted: - storage.state = .starting - return (storage.transports, storage.services.router, storage.interceptors) - + fatalError() case .starting, .running: throw ServerError( code: .serverIsAlreadyRunning, @@ -189,31 +191,31 @@ public final class GRPCServer: Sendable { // When we exit this function we must have stopped. defer { - self.storage.withLockedValue { $0.state = .stopped } + self.state.store(.stopped, ordering: .sequentiallyConsistent) } - if transports.values.isEmpty { + if self.transports.isEmpty { throw ServerError( code: .noTransportsConfigured, message: """ Can't start server, no transports are configured. You must add at least one transport \ - to the server using 'transports.add(_:)' before calling 'run()'. + to the server before calling 'run()'. """ ) } var listeners: [RPCAsyncSequence] = [] - listeners.reserveCapacity(transports.values.count) + listeners.reserveCapacity(self.transports.count) - for transport in transports.values { + for transport in self.transports { do { let listener = try await transport.listen() listeners.append(listener) } catch let cause { // Failed to start, so start stopping. - self.storage.withLockedValue { $0.state = .stopping } + self.state.store(.stopping, ordering: .sequentiallyConsistent) // Some listeners may have started and have streams which need closing. - await Self.rejectRequests(listeners, transports: transports) + await self.rejectRequests(listeners) throw ServerError( code: .failedToStartTransport, @@ -227,35 +229,24 @@ public final class GRPCServer: Sendable { } // May have been told to stop listening while starting the transports. - let isStopping = self.storage.withLockedValue { storage in - switch storage.state { - case .notStarted, .running, .stopped: - fatalError("Invalid state") - - case .starting: - storage.state = .running - return false - - case .stopping: - return true - } - } + let (wasStarting, _) = self.state.compareExchange( + expected: .starting, + desired: .running, + ordering: .sequentiallyConsistent + ) // If the server is stopping then notify the transport and then consume them: there may be // streams opened at a lower level (e.g. HTTP/2) which are already open and need to be consumed. - if isStopping { - await Self.rejectRequests(listeners, transports: transports) + if wasStarting { + await self.handleRequests(listeners) } else { - await Self.handleRequests(listeners, router: router, interceptors: interceptors) + await self.rejectRequests(listeners) } } - private static func rejectRequests( - _ listeners: [RPCAsyncSequence], - transports: Transports - ) async { + private func rejectRequests(_ listeners: [RPCAsyncSequence]) async { // Tell the active listeners to stop listening. - for transport in transports.values.prefix(listeners.count) { + for transport in self.transports.prefix(listeners.count) { transport.stopListening() } @@ -282,33 +273,21 @@ public final class GRPCServer: Sendable { } } - private static func handleRequests( - _ listeners: [RPCAsyncSequence], - router: RPCRouter, - interceptors: Interceptors - ) async { + private func handleRequests(_ listeners: [RPCAsyncSequence]) async { #if swift(>=5.9) if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - await Self.handleRequestsInDiscardingTaskGroup( - listeners, - router: router, - interceptors: interceptors - ) + await self.handleRequestsInDiscardingTaskGroup(listeners) } else { - await Self.handleRequestsInTaskGroup(listeners, router: router, interceptors: interceptors) + await self.handleRequestsInTaskGroup(listeners) } #else - await Self.handleRequestsInTaskGroup(listeners, router: router, interceptors: interceptors) + await self.handleRequestsInTaskGroup(listeners) #endif } #if swift(>=5.9) @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) - private static func handleRequestsInDiscardingTaskGroup( - _ listeners: [RPCAsyncSequence], - router: RPCRouter, - interceptors: Interceptors - ) async { + private func handleRequestsInDiscardingTaskGroup(_ listeners: [RPCAsyncSequence]) async { await withDiscardingTaskGroup { group in for listener in listeners { group.addTask { @@ -316,7 +295,7 @@ public final class GRPCServer: Sendable { do { for try await stream in listener { subGroup.addTask { - await router.handle(stream: stream, interceptors: interceptors.values) + await self.router.handle(stream: stream, interceptors: self.interceptors) } } } catch { @@ -330,11 +309,7 @@ public final class GRPCServer: Sendable { } #endif - private static func handleRequestsInTaskGroup( - _ listeners: [RPCAsyncSequence], - router: RPCRouter, - interceptors: Interceptors - ) async { + private func handleRequestsInTaskGroup(_ listeners: [RPCAsyncSequence]) async { // If the discarding task group isn't available then fall back to using a regular task group // with a limit on subtasks. Most servers will use an HTTP/2 based transport, most // implementations limit connections to 100 concurrent streams. A limit of 4096 gives the server @@ -355,7 +330,7 @@ public final class GRPCServer: Sendable { } subGroup.addTask { - await router.handle(stream: stream, interceptors: interceptors.values) + await self.router.handle(stream: stream, interceptors: self.interceptors) } } } catch { @@ -375,105 +350,50 @@ public final class GRPCServer: Sendable { /// /// Calling this on a server which is already stopping or has stopped has no effect. public func stopListening() { - let transports = self.storage.withLockedValue { storage in - let transports: Transports? - - switch storage.state { - case .notStarted: - storage.state = .stopped - transports = nil - case .starting: - storage.state = .stopping - transports = nil - case .running: - storage.state = .stopping - transports = storage.transports - case .stopping: - transports = nil - case .stopped: - transports = nil - } - - return transports - } - - if let transports = transports?.values { - for transport in transports { + let (wasRunning, actual) = self.state.compareExchange( + expected: .running, + desired: .stopping, + ordering: .sequentiallyConsistent + ) + + if wasRunning { + for transport in self.transports { transport.stopListening() } - } - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCServer { - /// The transports which provide a bidirectional communication channel with clients. - /// - /// You can add a new transport by calling ``add(_:)``. - public struct Transports: Sendable { - private(set) var values: [any (ServerTransport & Sendable)] = [] - - /// Add a transport to the server. - /// - /// - Parameter transport: The transport to add. - public mutating func add(_ transport: some (ServerTransport & Sendable)) { - self.values.append(transport) - } - } + } else { + switch actual { + case .notStarted: + let (exchanged, _) = self.state.compareExchange( + expected: .notStarted, + desired: .stopped, + ordering: .sequentiallyConsistent + ) - /// The services registered with this server. - /// - /// You can register services by calling ``register(_:)`` or by manually adding handlers for - /// methods to the ``router``. - public struct Services: Sendable { - /// The router storing handlers for known methods. - public var router = RPCRouter() - - /// Registers service methods with the ``router``. - /// - /// - Parameter service: The service to register with the ``router``. - public mutating func register(_ service: some RegistrableRPCService) { - service.registerMethods(with: &self.router) - } - } + // Lost a race with 'run()', try again. + if !exchanged { + self.stopListening() + } - /// A collection of interceptors providing cross-cutting functionality to each accepted RPC. - public struct Interceptors: Sendable { - private(set) var values: [any ServerInterceptor] = [] - - /// Add an interceptor to the server. - /// - /// The order in which interceptors are added reflects the order in which they are called. The - /// first interceptor added will be the first interceptor to intercept each request. The last - /// interceptor added will be the final interceptor to intercept each request before calling - /// the appropriate handler. - /// - /// - Parameter interceptor: The interceptor to add. - public mutating func add(_ interceptor: some ServerInterceptor) { - self.values.append(interceptor) - } - } -} + case .starting: + let (exchanged, _) = self.state.compareExchange( + expected: .starting, + desired: .stopping, + ordering: .sequentiallyConsistent + ) -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCServer.Transports: CustomStringConvertible { - public var description: String { - return String(describing: self.values) - } -} + // Lost a race with 'run()', try again. + if !exchanged { + self.stopListening() + } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCServer.Services: CustomStringConvertible { - public var description: String { - // List the fully qualified all methods ordered by service and then method - let rpcs = self.router.methods.map { $0.fullyQualifiedMethod }.sorted() - return String(describing: rpcs) - } -} + case .running: + // Unreachable, this branch only happens when the initial exchange didn't take place. + fatalError() -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCServer.Interceptors: CustomStringConvertible { - public var description: String { - return String(describing: self.values.map { String(describing: type(of: $0)) }) + case .stopping, .stopped: + // Already stopping/stopped, ignore. + () + } + } } } diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index 3c3dbc45c..05ca4e9ef 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -15,7 +15,7 @@ */ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol ServerTransport { +public protocol ServerTransport: Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index d36f78dee..bf8d5c050 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -36,16 +36,8 @@ final class GRPCClientTests: XCTestCase { _ body: (GRPCClient, GRPCServer) async throws -> Void ) async throws { let inProcess = self.makeInProcessPair() - var configuration = GRPCClient.Configuration() - configuration.interceptors.add(contentsOf: interceptors) - let client = GRPCClient(transport: inProcess.client, configuration: configuration) - - let server = GRPCServer() - server.transports.add(inProcess.server) - - for service in services { - server.services.register(service) - } + let client = GRPCClient(transport: inProcess.client, interceptors: interceptors) + let server = GRPCServer(transports: [inProcess.server], services: services) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -338,9 +330,7 @@ final class GRPCClientTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let server = GRPCServer() - server.services.register(BinaryEcho()) - server.transports.add(inProcess.server) + let server = GRPCServer(transports: [inProcess.server], services: [BinaryEcho()]) try await server.run() } @@ -416,34 +406,4 @@ final class GRPCClientTests: XCTestCase { task.cancel() } - - func testRunClientNotRunning() async throws { - let (clientTransport, _) = self.makeInProcessPair() - let client = GRPCClient(transport: clientTransport) - - // Client is not running, should throw an error. - await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { - try await client.unary( - request: .init(message: [3, 1, 4, 1, 5]), - descriptor: BinaryEcho.Methods.collect, - serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() - ) { response in - let message = try response.message - XCTAssertEqual(message, [3, 1, 4, 1, 5]) - } - } errorHandler: { error in - XCTAssertEqual(error.code, .clientIsNotRunning) - } - } - - func testInterceptorsDescription() async throws { - var config = GRPCClient.Configuration() - config.interceptors.add(.rejectAll(with: .init(code: .aborted, message: ""))) - config.interceptors.add(.requestCounter(.init(0))) - - let description = String(describing: config.interceptors) - let expected = #"["RejectAllClientInterceptor", "RequestCountingClientInterceptor"]"# - XCTAssertEqual(description, expected) - } } diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index e694e80cd..cc3169d50 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -33,16 +33,11 @@ final class GRPCServerTests: XCTestCase { _ body: (InProcessClientTransport, GRPCServer) async throws -> Void ) async throws { let inProcess = self.makeInProcessPair() - let server = GRPCServer() - server.transports.add(inProcess.server) - - for service in services { - server.services.register(service) - } - - for interceptor in interceptors { - server.interceptors.add(interceptor) - } + let server = GRPCServer( + transports: [inProcess.server], + services: services, + interceptors: interceptors + ) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -305,9 +300,7 @@ final class GRPCServerTests: XCTestCase { func testCancelRunningServer() async throws { let inProcess = self.makeInProcessPair() let task = Task { - let server = GRPCServer() - server.services.register(BinaryEcho()) - server.transports.add(inProcess.server) + let server = GRPCServer(transports: [inProcess.server], services: [BinaryEcho()]) try await server.run() } @@ -326,7 +319,7 @@ final class GRPCServerTests: XCTestCase { } func testTestRunServerWithNoTransport() async throws { - let server = GRPCServer() + let server = GRPCServer(transports: [], services: []) await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { try await server.run() } errorHandler: { error in @@ -335,8 +328,7 @@ final class GRPCServerTests: XCTestCase { } func testTestRunStoppedServer() async throws { - let server = GRPCServer() - server.transports.add(InProcessServerTransport()) + let server = GRPCServer(transports: [InProcessServerTransport()], services: []) // Run the server. let task = Task { try await server.run() } task.cancel() @@ -351,8 +343,7 @@ final class GRPCServerTests: XCTestCase { } func testRunServerWhenTransportThrows() async throws { - let server = GRPCServer() - server.transports.add(ThrowOnRunServerTransport()) + let server = GRPCServer(transports: [ThrowOnRunServerTransport()], services: []) await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { try await server.run() } errorHandler: { error in @@ -361,15 +352,17 @@ final class GRPCServerTests: XCTestCase { } func testRunServerDrainsRunningTransportsWhenOneFailsToStart() async throws { - let server = GRPCServer() - // Register the in process transport first and allow it to come up. let inProcess = self.makeInProcessPair() - server.transports.add(inProcess.server) - // Register a transport waits for a signal before throwing. let signal = AsyncStream.makeStream(of: Void.self) - server.transports.add(ThrowOnSignalServerTransport(signal: signal.stream)) + let server = GRPCServer( + transports: [ + inProcess.server, + ThrowOnSignalServerTransport(signal: signal.stream), + ], + services: [] + ) // Connect the in process client and start an RPC. When the stream is opened signal the // other transport to throw. This stream should be failed by the server. @@ -402,43 +395,6 @@ final class GRPCServerTests: XCTestCase { } } - func testInterceptorsDescription() async throws { - let server = GRPCServer() - server.interceptors.add(.rejectAll(with: .init(code: .aborted, message: ""))) - server.interceptors.add(.requestCounter(.init(0))) - let description = String(describing: server.interceptors) - let expected = #"["RejectAllServerInterceptor", "RequestCountingServerInterceptor"]"# - XCTAssertEqual(description, expected) - } - - func testServicesDescription() async throws { - let server = GRPCServer() - let methods: [(String, String)] = [ - ("helloworld.Greeter", "SayHello"), - ("echo.Echo", "Foo"), - ("echo.Echo", "Bar"), - ("echo.Echo", "Baz"), - ] - - for (service, method) in methods { - let descriptor = MethodDescriptor(service: service, method: method) - server.services.router.registerHandler( - forMethod: descriptor, - deserializer: IdentityDeserializer(), - serializer: IdentitySerializer() - ) { _ in - fatalError("Unreachable") - } - } - - let description = String(describing: server.services) - let expected = """ - ["echo.Echo/Bar", "echo.Echo/Baz", "echo.Echo/Foo", "helloworld.Greeter/SayHello"] - """ - - XCTAssertEqual(description, expected) - } - private func doEchoGet(using transport: some ClientTransport) async throws { try await transport.withStream(descriptor: BinaryEcho.Methods.get) { stream in try await stream.outbound.write(.metadata([:])) From 471fe8bba25a31295fef7c4d7e5da0beaaffebb8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Dec 2023 15:54:45 +0000 Subject: [PATCH 189/580] Add a convenience method for creating in-process client/server (#1738) Motivation: To use the in-process transport users must create a server and then a client depending on that server. It rarely makes sense to create one without the other and I have written more or less the same extension in three places to do this for me and return a pair. This should just be made into API. Modifications: - Add a `InProcessTransport.makePair()` convenience method - Replace use of old helpers Result: It's easier to create in-process transport. --- .../InProcessTransport.swift | 43 +++++++++++++++++++ Tests/GRPCCoreTests/GRPCClientTests.swift | 18 ++------ Tests/GRPCCoreTests/GRPCServerTests.swift | 14 ++---- 3 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 Sources/GRPCInProcessTransport/InProcessTransport.swift diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift new file mode 100644 index 000000000..e0db45f7c --- /dev/null +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -0,0 +1,43 @@ +/* + * Copyright 2023, 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 + +public enum InProcessTransport { + /// Returns a pair containing an ``InProcessServerTransport`` and an ``InProcessClientTransport``. + /// + /// This function is purely for convenience and does no more than constructing a server transport + /// and a client using that server transport. + /// + /// - Parameters: + /// - methodConfiguration: Method specific configuration used by the client transport to + /// determine how RPCs should be executed. + /// - retryThrottle: The retry throttle the client transport uses to determine whether a call + /// should be retried. + /// - Returns: A tuple containing the connected server and client in-process transports. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public static func makePair( + methodConfiguration: MethodConfigurations = MethodConfigurations(), + retryThrottle: RetryThrottle? = nil + ) -> (server: InProcessServerTransport, client: InProcessClientTransport) { + let server = InProcessServerTransport() + let client = InProcessClientTransport( + server: server, + methodConfiguration: methodConfiguration, + retryThrottle: retryThrottle + ) + return (server, client) + } +} diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index bf8d5c050..b3149b62b 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -20,22 +20,12 @@ import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCClientTests: XCTestCase { - func makeInProcessPair() -> (client: InProcessClientTransport, server: InProcessServerTransport) { - let server = InProcessServerTransport() - let client = InProcessClientTransport( - server: server, - methodConfiguration: MethodConfigurations() - ) - - return (client, server) - } - func withInProcessConnectedClient( services: [any RegistrableRPCService], interceptors: [any ClientInterceptor] = [], _ body: (GRPCClient, GRPCServer) async throws -> Void ) async throws { - let inProcess = self.makeInProcessPair() + let inProcess = InProcessTransport.makePair() let client = GRPCClient(transport: inProcess.client, interceptors: interceptors) let server = GRPCServer(transports: [inProcess.server], services: services) @@ -325,7 +315,7 @@ final class GRPCClientTests: XCTestCase { } func testCancelRunningClient() async throws { - let inProcess = self.makeInProcessPair() + let inProcess = InProcessTransport.makePair() let client = GRPCClient(transport: inProcess.client) try await withThrowingTaskGroup(of: Void.self) { group in @@ -374,7 +364,7 @@ final class GRPCClientTests: XCTestCase { } func testRunStoppedClient() async throws { - let (clientTransport, _) = self.makeInProcessPair() + let (_, clientTransport) = InProcessTransport.makePair() let client = GRPCClient(transport: clientTransport) // Run the client. let task = Task { try await client.run() } @@ -390,7 +380,7 @@ final class GRPCClientTests: XCTestCase { } func testRunAlreadyRunningClient() async throws { - let (clientTransport, _) = self.makeInProcessPair() + let (_, clientTransport) = InProcessTransport.makePair() let client = GRPCClient(transport: clientTransport) // Run the client. let task = Task { try await client.run() } diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index cc3169d50..936461a2c 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -20,19 +20,12 @@ import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCServerTests: XCTestCase { - func makeInProcessPair() -> (client: InProcessClientTransport, server: InProcessServerTransport) { - let server = InProcessServerTransport() - let client = InProcessClientTransport(server: server) - - return (client, server) - } - func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [], _ body: (InProcessClientTransport, GRPCServer) async throws -> Void ) async throws { - let inProcess = self.makeInProcessPair() + let inProcess = InProcessTransport.makePair() let server = GRPCServer( transports: [inProcess.server], services: services, @@ -298,7 +291,7 @@ final class GRPCServerTests: XCTestCase { } func testCancelRunningServer() async throws { - let inProcess = self.makeInProcessPair() + let inProcess = InProcessTransport.makePair() let task = Task { let server = GRPCServer(transports: [inProcess.server], services: [BinaryEcho()]) try await server.run() @@ -353,7 +346,8 @@ final class GRPCServerTests: XCTestCase { func testRunServerDrainsRunningTransportsWhenOneFailsToStart() async throws { // Register the in process transport first and allow it to come up. - let inProcess = self.makeInProcessPair() + let inProcess = InProcessTransport.makePair() + // Register a transport waits for a signal before throwing. let signal = AsyncStream.makeStream(of: Void.self) let server = GRPCServer( From 4f8cbe0132a4550560fdeff2e190903e09c43b26 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Dec 2023 16:11:52 +0000 Subject: [PATCH 190/580] Avoid invalid state when a connect failed (#1739) Motivation: The connection manager gets notified when a connect attempt fails and expected to be in the connecting state. Any other state is invalid and will lead to an assertion failure. The manager is notified of connection failures via the channel promise and via the handler (i.e. on `errorCaught(context:error:)`). However, connection failures lead to a state change and will result in crashes in debug builds if a notification is received via both paths. Modifications: - Ignore errors from the channel while in the connecting state and only listen errors from the channel promise callback. - Add test Result: Fewer crashes. --- Sources/GRPC/ConnectionManager.swift | 4 +- Tests/GRPCTests/ConnectionManagerTests.swift | 48 +++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index a0081fe7a..157be809f 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -657,7 +657,9 @@ internal final class ConnectionManager: @unchecked Sendable { ) case .connecting: - self.connectionFailed(withError: error) + // Ignore the error, the channel promise will notify the manager of any error which occurs + // while connecting. + () case var .active(state): state.error = error diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 270e69835..9fb0722af 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -1272,12 +1272,58 @@ extension ConnectionManagerTests { } self.waitForStateChange(from: .connecting, to: .shutdown) { - manager.channelError(EventLoopError.shutdown) + channelPromise.fail(EventLoopError.shutdown) } XCTAssertThrowsError(try multiplexer.wait()) } + func testChannelErrorAndConnectFailWhenConnecting() throws { + // This test checks a path through the connection manager which previously led to an invalid + // state (a connect failure in a state other than connecting). To trigger these we need to + // fire an error down the pipeline containing the idle handler and fail the connect promise. + let escapedChannelPromise = self.loop.makePromise(of: Channel.self) + let channelPromise = self.loop.makePromise(of: Channel.self) + + var configuration = self.defaultConfiguration + configuration.connectionBackoff = ConnectionBackoff() + let manager = self.makeConnectionManager( + configuration: configuration + ) { connectionManager, loop in + let channel = EmbeddedChannel(loop: loop as! EmbeddedEventLoop) + let multiplexer = HTTP2StreamMultiplexer(mode: .client, channel: channel) { + $0.eventLoop.makeSucceededVoidFuture() + } + + let idleHandler = GRPCIdleHandler( + connectionManager: connectionManager, + multiplexer: multiplexer, + idleTimeout: .minutes(60), + keepalive: .init(), + logger: self.clientLogger + ) + + channel.pipeline.addHandler(idleHandler).whenSuccess { + escapedChannelPromise.succeed(channel) + } + + return channelPromise.futureResult + } + + // Ask for the multiplexer to trigger channel creation. + self.waitForStateChange(from: .idle, to: .connecting) { + _ = manager.getHTTP2Multiplexer() + self.loop.run() + } + + // Fire an error down the pipeline. + let channel = try escapedChannelPromise.futureResult.wait() + channel.pipeline.fireErrorCaught(GRPCStatus(code: .unavailable)) + + // Fail the channel promise. + channelPromise.fail(GRPCStatus(code: .unavailable)) + } + func testClientKeepaliveJitterWithoutClamping() { let original = ClientConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) let keepalive = original.jitteringInterval(byAtMost: .milliseconds(500)) From 6ade19f0b57f5fc436dfecfced83f3c84d1095b9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 11 Dec 2023 09:40:13 +0000 Subject: [PATCH 191/580] Bump version number to 1.21.0 (#1741) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.21.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 57665669a..b225e798d 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 20 + internal static let minor = 21 /// The patch version. internal static let patch = 0 From 81873c0cedd70d26d59ad599ba770298a5e7b9e8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 11 Dec 2023 16:25:52 +0000 Subject: [PATCH 192/580] Add reflection service to .spi.yml (#1742) Motivation: No docs are generated on Swift Package Index for the reflection service as it isn't included in the configuration. Modifications: Add reflection service to .spi.yml Result: Docs generate for the reflection service --- .spi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.spi.yml b/.spi.yml index b734eb1e6..260eb791f 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [GRPC, protoc-gen-grpc-swift] + - documentation_targets: [GRPC, GRPCReflectionService, protoc-gen-grpc-swift] From e9273f1d548ddfc2851abc7a84b886aad461b2b0 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:31:36 +0200 Subject: [PATCH 193/580] [CodeGenLib] Translator for enums containing type aliases and static properties (#1733) Motivation: In the generated code we want to have: - enums for each namespace containing enums for all their services - enums for each service (inside the corresponding namespace enum) containing enums for all their methods, an array of all corresponding MethodDescriptors and type aliases for streaming and non-streaming protocols - enums for each method (inside the corresponding service enum) containing type aliases for the input and output types and a method descriptor (GRPCCore) Modifications: - Created the Translator protocol defining the 'translate()' function. - Implemented the IDLToStructuredSwiftTranslator struct which conforms to the Translator protocol and creates the StructuredSwiftRepresentation for a CodeGenerationRequest calling the Specialized translators' functions. - Created the SpecializedTranslator protocol. - Implemented the TypeAliasTranslator, the first of the 3 SpecializedTranslators that takes in a CodeGenerationRequest object and creates all the enums, type aliases and properties mentioned in the Motivation, in StructuredSwiftRepresentation format. - Created CodeGenError. - Wrote SnippetTests. Result: The generated code will contain useful type aliases for message types and protocol names, organized in namespace, service and method specific enums. --- NOTICES.txt | 2 +- Sources/GRPCCodeGen/CodeGenError.swift | 64 +++ .../GRPCCodeGen/CodeGenerationRequest.swift | 8 +- .../StructuredSwiftRepresentation.swift | 1 - .../IDLToStructuredSwiftTranslator.swift | 43 ++ .../Translator/SpecializedTranslator.swift | 29 + .../Internal/Translator/Translator.swift | 33 ++ .../Translator/TypealiasTranslator.swift | 310 +++++++++++ .../SnippetBasedTranslatorTests.swift | 519 ++++++++++++++++++ .../Internal/Translator/TestFunctions.swift | 67 +++ 10 files changed, 1070 insertions(+), 6 deletions(-) create mode 100644 Sources/GRPCCodeGen/CodeGenError.swift create mode 100644 Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift create mode 100644 Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift create mode 100644 Sources/GRPCCodeGen/Internal/Translator/Translator.swift create mode 100644 Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift diff --git a/NOTICES.txt b/NOTICES.txt index f4807b7f9..3a8509e83 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -62,7 +62,7 @@ This product uses derivations of swift-extras/swift-extras-base64 'Base64.swift' This product uses derivations of apple/swift-openapi-generator 'StructuredSwiftRepresentation.swift', 'TypeName.swift', 'TypeUsage.swift', 'Builtins.swift', 'RendererProtocol.swift', 'TextBasedProtocol', -and 'Test_TextBasedRenderer'. +'Test_TextBasedRenderer', and 'SnippetBasedReferenceTests.swift'. * LICENSE (Apache License 2.0): * https://github.com/apple/swift-openapi-generator/blob/main/LICENSE.txt diff --git a/Sources/GRPCCodeGen/CodeGenError.swift b/Sources/GRPCCodeGen/CodeGenError.swift new file mode 100644 index 000000000..041fae807 --- /dev/null +++ b/Sources/GRPCCodeGen/CodeGenError.swift @@ -0,0 +1,64 @@ +/* + * Copyright 2023, 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. + */ + +/// A error thrown by the ``SourceGenerator`` to signal errors in the ``CodeGenerationRequest`` object. +public struct CodeGenError: Error, Hashable, Sendable { + /// The code indicating the domain of the error. + public var code: Code + /// A message providing more details about the error which may include details specific to this + /// instance of the error. + public var message: String + + /// Creates a new error. + /// + /// - Parameters: + /// - code: The error code. + /// - message: A description of the error. + public init(code: Code, message: String) { + self.code = code + self.message = message + } +} + +extension CodeGenError { + public struct Code: Hashable, Sendable { + private enum Value { + case nonUniqueServiceName + case nonUniqueMethodName + } + + private var value: Value + private init(_ value: Value) { + self.value = value + } + + /// The same name is used for two services that are either in the same namespace or don't have a namespace. + public static var nonUniqueServiceName: Self { + Self(.nonUniqueServiceName) + } + + /// The same name is used for two methods of the same service. + public static var nonUniqueMethodName: Self { + Self(.nonUniqueMethodName) + } + } +} + +extension CodeGenError: CustomStringConvertible { + public var description: String { + return "\(self.code): \"\(self.message)\"" + } +} diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 1876bf5de..9a811647c 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -36,7 +36,7 @@ public struct CodeGenerationRequest { public var services: [ServiceDescriptor] /// Closure that receives a message type as a `String` and returns a code snippet to - /// initialize a `MessageSerializer` for that type as a `String`. + /// initialise a `MessageSerializer` for that type as a `String`. /// /// The result is inserted in the generated code, where clients serialize RPC inputs and /// servers serialize RPC outputs. @@ -217,7 +217,7 @@ public struct CodeGenerationRequest { public var inputType: String /// The generated output type for the described method. - public var ouputType: String + public var outputType: String public init( documentation: String, @@ -225,14 +225,14 @@ public struct CodeGenerationRequest { isInputStreaming: Bool, isOutputStreaming: Bool, inputType: String, - ouputType: String + outputType: String ) { self.documentation = documentation self.name = name self.isInputStreaming = isInputStreaming self.isOutputStreaming = isOutputStreaming self.inputType = inputType - self.ouputType = ouputType + self.outputType = outputType } } } diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index a328908f2..c78d459b7 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -66,7 +66,6 @@ struct ImportDescription: Equatable, Codable { /// /// For example: `public`. internal enum AccessModifier: String, Sendable, Equatable, Codable { - /// A declaration accessible outside of the module. case `public` diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift new file mode 100644 index 000000000..3870c530b --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -0,0 +1,43 @@ +/* + * Copyright 2023, 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. + */ + +struct IDLToStructuredSwiftTranslator: Translator { + private let typealiasTranslator = TypealiasTranslator() + + func translate( + codeGenerationRequest: CodeGenerationRequest, + client: Bool, + server: Bool + ) throws -> StructuredSwiftRepresentation { + let topComment = Comment.doc(codeGenerationRequest.leadingTrivia) + let imports: [ImportDescription] = [ + ImportDescription(moduleName: "GRPCCore") + ] + var codeBlocks: [CodeBlock] = [] + codeBlocks.append( + contentsOf: try self.typealiasTranslator.translate(from: codeGenerationRequest) + ) + + let fileDescription = FileDescription( + topComment: topComment, + imports: imports, + codeBlocks: codeBlocks + ) + let fileName = String(codeGenerationRequest.fileName.split(separator: ".")[0]) + let file = NamedFileDescription(name: fileName, contents: fileDescription) + return StructuredSwiftRepresentation(file: file) + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift new file mode 100644 index 000000000..e19876bf0 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift @@ -0,0 +1,29 @@ +/* + * Copyright 2023, 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. + */ + +/// Represents one responsibility of the ``Translator``: either the type aliases translation, +/// the server code translation or the client code translation. +protocol SpecializedTranslator { + /// Generates an array of ``CodeBlock`` elements that will be part of the ``StructuredSwiftRepresentation`` object + /// created by the ``Translator``. + /// + /// - Parameters: + /// - codeGenerationRequest: The ``CodeGenerationRequest`` object used to represent a Source IDL description of RPCs. + /// - Returns: An array of ``CodeBlock`` elements. + /// + /// - SeeAlso: ``CodeGenerationRequest``, ``Translator``, ``CodeBlock``. + func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift new file mode 100644 index 000000000..2ea3ab3f5 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift @@ -0,0 +1,33 @@ +/* + * Copyright 2023, 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. + */ + +/// Transforms ``CodeGenerationRequest`` objects into ``StructuredSwiftRepresentation`` objects. +/// +/// It represents the first step of the code generation process for IDL described RPCs. +protocol Translator { + /// Translates the provided ``CodeGenerationRequest`` object, into Swift code representation. + /// - Parameters: + /// - codeGenerationRequest: The IDL described RPCs representation. + /// - client: Whether or not client code should be generated from the IDL described RPCs representation. + /// - server: Whether or not server code should be generated from the IDL described RPCs representation. + /// - Returns: A structured Swift representation of the generated code. + /// - Throws: An error if there are issues translating the codeGenerationRequest. + func translate( + codeGenerationRequest: CodeGenerationRequest, + client: Bool, + server: Bool + ) throws -> StructuredSwiftRepresentation +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift new file mode 100644 index 000000000..1737b38ae --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -0,0 +1,310 @@ +/* + * Copyright 2023, 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. + */ + +/// Creates enums containing useful type aliases and static properties for the methods, services and +/// namespaces described in a ``CodeGenerationRequest`` object, using types from +/// ``StructuredSwiftRepresentation``. +/// +/// For example, in the case of the ``Echo`` service, the ``TypealiasTranslator`` will create +/// a representation for the following generated code: +/// ```swift +/// public enum echo { +/// public enum Echo { +/// public enum Methods { +/// public enum Get { +/// public typealias Input = Echo_EchoRequest +/// public typealias Output = Echo_EchoResponse +/// public static let descriptor = MethodDescriptor(service: "echo.Echo", method: "Get") +/// } +/// +/// public enum Collect { +/// public typealias Input = Echo_EchoRequest +/// public typealias Output = Echo_EchoResponse +/// public static let descriptor = MethodDescriptor(service: "echo.Echo", method: "Collect") +/// } +/// // ... +/// } +/// public static let methods: [MethodDescriptor] = [ +/// echo.Echo.Get.descriptor, +/// echo.Echo.Collect.descriptor, +/// // ... +/// ] +/// +/// public typealias StreamingServiceProtocol = echo_EchoServiceStreamingProtocol +/// public typealias ServiceProtocol = echo_EchoServiceProtocol +/// +/// } +/// } +/// ``` +/// +/// A ``CodeGenerationRequest`` can contain multiple namespaces, so the TypealiasTranslator will create a ``CodeBlock`` +/// for each namespace. +struct TypealiasTranslator: SpecializedTranslator { + func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { + var codeBlocks: [CodeBlock] = [] + let services = codeGenerationRequest.services + let servicesByNamespace = Dictionary(grouping: services, by: { $0.namespace }) + + // Verify service names are unique within each namespace and that services with no namespace + // don't have the same names as any of the namespaces. + try self.checkServiceNamesAreUnique(for: servicesByNamespace) + + // Sorting the keys and the services in each list of the dictionary is necessary + // so that the generated enums are deterministically ordered. + for (namespace, services) in servicesByNamespace.sorted(by: { $0.key < $1.key }) { + let namespaceCodeBlocks = try self.makeNamespaceEnum( + for: namespace, + containing: services.sorted(by: { $0.name < $1.name }) + ) + codeBlocks.append(contentsOf: namespaceCodeBlocks) + } + + return codeBlocks + } +} + +extension TypealiasTranslator { + private func checkServiceNamesAreUnique( + for servicesByNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]] + ) throws { + // Check that if there are services in an empty namespace, none have names which match other namespaces + let noNamespaceServices = servicesByNamespace["", default: []] + let namespaces = servicesByNamespace.keys + for service in noNamespaceServices { + if namespaces.contains(service.name) { + throw CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services with no namespace must not have the same names as the namespaces. \ + \(service.name) is used as a name for a service with no namespace and a namespace. + """ + ) + } + } + + // Check that service names are unique within each namespace. + for (namespace, services) in servicesByNamespace { + var serviceNames: Set = [] + for service in services { + if serviceNames.contains(service.name) { + let errorMessage: String + if namespace.isEmpty { + errorMessage = """ + Services in an empty namespace must have unique names. \ + \(service.name) is used as a name for multiple services without namespaces. + """ + } else { + errorMessage = """ + Services within the same namespace must have unique names. \ + \(service.name) is used as a name for multiple services in the \(service.namespace) namespace. + """ + } + throw CodeGenError( + code: .nonUniqueServiceName, + message: errorMessage + ) + } + serviceNames.insert(service.name) + } + } + } + + private func makeNamespaceEnum( + for namespace: String, + containing services: [CodeGenerationRequest.ServiceDescriptor] + ) throws -> [CodeBlock] { + var serviceDeclarations = [Declaration]() + + // Create the service specific enums. + for service in services { + let serviceEnum = try self.makeServiceEnum(from: service) + serviceDeclarations.append(serviceEnum) + } + + // If there is no namespace, the service enums are independent CodeBlocks. + // If there is a namespace, the associated enum will contain the service enums and will + // be represented as a single CodeBlock element. + if namespace.isEmpty { + return serviceDeclarations.map { + CodeBlock(item: .declaration($0)) + } + } else { + var namespaceEnum = EnumDescription(name: namespace) + namespaceEnum.members = serviceDeclarations + return [CodeBlock(item: .declaration(.enum(namespaceEnum)))] + } + } + + private func makeServiceEnum( + from service: CodeGenerationRequest.ServiceDescriptor + ) throws -> Declaration { + var serviceEnum = EnumDescription(name: service.name) + var methodsEnum = EnumDescription(name: "Methods") + let methods = service.methods + + // Verify method names are unique for the service. + try self.checkMethodNamesAreUnique(in: service) + + // Create the method specific enums. + for method in methods { + let methodEnum = self.makeMethodEnum(from: method, in: service) + methodsEnum.members.append(methodEnum) + } + serviceEnum.members.append(.enum(methodsEnum)) + + // Create the method descriptor array. + let methodDescriptorsDeclaration = self.makeMethodDescriptors(for: service) + serviceEnum.members.append(methodDescriptorsDeclaration) + + // Create the streaming and non-streaming service protocol type aliases. + let serviceProtocols = self.makeServiceProtocolsTypealiases(for: service) + serviceEnum.members.append(contentsOf: serviceProtocols) + + return .enum(serviceEnum) + } + + private func checkMethodNamesAreUnique( + in service: CodeGenerationRequest.ServiceDescriptor + ) throws { + let methodNames = service.methods.map { $0.name } + var seenNames = Set() + + for methodName in methodNames { + if seenNames.contains(methodName) { + throw CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique names. \ + \(methodName) is used as a name for multiple methods of the \(service.name) service. + """ + ) + } + seenNames.insert(methodName) + } + } + + private func makeMethodEnum( + from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + var methodEnum = EnumDescription(name: method.name) + + let inputTypealias = Declaration.typealias( + name: "Input", + existingType: .member([method.inputType]) + ) + let outputTypealias = Declaration.typealias( + name: "Output", + existingType: .member([method.outputType]) + ) + let descriptorVariable = self.makeMethodDescriptor( + from: method, + in: service + ) + methodEnum.members.append(inputTypealias) + methodEnum.members.append(outputTypealias) + methodEnum.members.append(descriptorVariable) + + return .enum(methodEnum) + } + + private func makeMethodDescriptor( + from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + let descriptorDeclarationLeft = Expression.identifier(.pattern("descriptor")) + + let fullyQualifiedServiceName: String + if service.namespace.isEmpty { + fullyQualifiedServiceName = service.name + } else { + fullyQualifiedServiceName = "\(service.namespace).\(service.name)" + } + + let descriptorDeclarationRight = Expression.functionCall( + FunctionCallDescription( + calledExpression: .identifierType(.member(["MethodDescriptor"])), + arguments: [ + FunctionArgumentDescription( + label: "service", + expression: .literal(fullyQualifiedServiceName) + ), + FunctionArgumentDescription( + label: "method", + expression: .literal(method.name) + ), + ] + ) + ) + return .variable( + isStatic: true, + kind: .let, + left: descriptorDeclarationLeft, + right: descriptorDeclarationRight + ) + } + + private func makeMethodDescriptors( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + var methodDescriptors = [Expression]() + let methodNames = service.methods.map { $0.name } + + for methodName in methodNames { + let methodDescriptorPath = Expression.memberAccess( + MemberAccessDescription( + left: .identifierType(.member(["Methods", methodName])), + right: "descriptor" + ) + ) + methodDescriptors.append(methodDescriptorPath) + } + + return .variable( + isStatic: true, + kind: .let, + left: .identifier(.pattern("methods")), + type: .array(.member(["MethodDescriptor"])), + right: .literal(.array(methodDescriptors)) + ) + } + + private func makeServiceProtocolsTypealiases( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> [Declaration] { + let namespacedPrefix: String + + if service.namespace.isEmpty { + namespacedPrefix = service.name + } else { + namespacedPrefix = "\(service.namespace)_\(service.name)" + } + + let streamingServiceProtocolName = "\(namespacedPrefix)ServiceStreamingProtocol" + let streamingServiceProtocolTypealias = Declaration.typealias( + name: "StreamingServiceProtocol", + existingType: .member([streamingServiceProtocolName]) + ) + + let serviceProtocolName = "\(namespacedPrefix)ServiceProtocol" + let serviceProtocolTypealias = Declaration.typealias( + name: "ServiceProtocol", + existingType: .member([serviceProtocolName]) + ) + + return [streamingServiceProtocolTypealias, serviceProtocolTypealias] + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift new file mode 100644 index 000000000..70ed1f948 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift @@ -0,0 +1,519 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCodeGen + +final class SnippetBasedTranslatorTests: XCTestCase { + typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + + func testTypealiasTranslator() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "MethodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + enum namespaceA { + enum ServiceA { + enum Methods { + enum MethodA { + typealias Input = NamespaceA_ServiceARequest + typealias Output = NamespaceA_ServiceAResponse + static let descriptor = MethodDescriptor( + service: "namespaceA.ServiceA", + method: "MethodA" + ) + } + } + static let methods: [MethodDescriptor] = [ + Methods.MethodA.descriptor + ] + typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorEmptyNamespace() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "MethodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "ServiceARequest", + outputType: "ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "", + methods: [method] + ) + let expectedSwift = + """ + enum ServiceA { + enum Methods { + enum MethodA { + typealias Input = ServiceARequest + typealias Output = ServiceAResponse + static let descriptor = MethodDescriptor( + service: "ServiceA", + method: "MethodA" + ) + } + } + static let methods: [MethodDescriptor] = [ + Methods.MethodA.descriptor + ] + typealias StreamingServiceProtocol = ServiceAServiceStreamingProtocol + typealias ServiceProtocol = ServiceAServiceProtocol + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorCheckMethodsOrder() throws { + let methodA = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "MethodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let methodB = MethodDescriptor( + documentation: "Documentation for MethodB", + name: "MethodB", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [methodA, methodB] + ) + let expectedSwift = + """ + enum namespaceA { + enum ServiceA { + enum Methods { + enum MethodA { + typealias Input = NamespaceA_ServiceARequest + typealias Output = NamespaceA_ServiceAResponse + static let descriptor = MethodDescriptor( + service: "namespaceA.ServiceA", + method: "MethodA" + ) + } + enum MethodB { + typealias Input = NamespaceA_ServiceARequest + typealias Output = NamespaceA_ServiceAResponse + static let descriptor = MethodDescriptor( + service: "namespaceA.ServiceA", + method: "MethodB" + ) + } + } + static let methods: [MethodDescriptor] = [ + Methods.MethodA.descriptor, + Methods.MethodB.descriptor + ] + typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorNoMethodsService() throws { + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [] + ) + let expectedSwift = + """ + enum namespaceA { + enum ServiceA { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorServiceAlphabeticalOrder() throws { + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: "BService", + namespace: "namespacea", + methods: [] + ) + + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "namespacea", + methods: [] + ) + + let expectedSwift = + """ + enum namespacea { + enum AService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = namespacea_AServiceServiceStreamingProtocol + typealias ServiceProtocol = namespacea_AServiceServiceProtocol + } + enum BService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = namespacea_BServiceServiceStreamingProtocol + typealias ServiceProtocol = namespacea_BServiceServiceProtocol + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorServiceAlphabeticalOrderNoNamespace() throws { + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: "BService", + namespace: "", + methods: [] + ) + + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "", + methods: [] + ) + + let expectedSwift = + """ + enum AService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = AServiceServiceStreamingProtocol + typealias ServiceProtocol = AServiceServiceProtocol + } + enum BService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol + typealias ServiceProtocol = BServiceServiceProtocol + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorNamespaceAlphabeticalOrder() throws { + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: "BService", + namespace: "bnamespace", + methods: [] + ) + + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "anamespace", + methods: [] + ) + + let expectedSwift = + """ + enum anamespace { + enum AService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol + typealias ServiceProtocol = anamespace_AServiceServiceProtocol + } + } + enum bnamespace { + enum BService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = bnamespace_BServiceServiceStreamingProtocol + typealias ServiceProtocol = bnamespace_BServiceServiceProtocol + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorNamespaceNoNamespaceOrder() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "anamespace", + methods: [] + ) + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: "BService", + namespace: "", + methods: [] + ) + let expectedSwift = + """ + enum BService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol + typealias ServiceProtocol = BServiceServiceProtocol + } + enum anamespace { + enum AService { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol + typealias ServiceProtocol = anamespace_AServiceServiceProtocol + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceA, serviceB]), + expectedSwift: expectedSwift + ) + } + + func testTypealiasTranslatorSameNameServicesNoNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "", + methods: [] + ) + + let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceA]) + let translator = TypealiasTranslator() + self.assertThrowsError( + ofType: CodeGenError.self, + try translator.translate(from: codeGenerationRequest) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services in an empty namespace must have unique names. \ + AService is used as a name for multiple services without namespaces. + """ + ) + ) + } + } + + func testTypealiasTranslatorSameNameServicesSameNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "namespacea", + methods: [] + ) + + let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceA]) + let translator = TypealiasTranslator() + self.assertThrowsError( + ofType: CodeGenError.self, + try translator.translate(from: codeGenerationRequest) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services within the same namespace must have unique names. \ + AService is used as a name for multiple services in the namespacea namespace. + """ + ) + ) + } + } + + func testTypealiasTranslatorSameNameMethodsSameServiceError() throws { + let methodA = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "MethodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "namespacea", + methods: [methodA, methodA] + ) + + let codeGenerationRequest = self.makeCodeGenerationRequest(services: [service]) + let translator = TypealiasTranslator() + self.assertThrowsError( + ofType: CodeGenError.self, + try translator.translate(from: codeGenerationRequest) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique names. \ + MethodA is used as a name for multiple methods of the AService service. + """ + ) + ) + } + } + + func testTypealiasTranslatorSameNameNoNamespaceServiceAndNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for SameName service with no namespace", + name: "SameName", + namespace: "", + methods: [] + ) + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: "BService", + namespace: "SameName", + methods: [] + ) + let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceB]) + let translator = TypealiasTranslator() + self.assertThrowsError( + ofType: CodeGenError.self, + try translator.translate(from: codeGenerationRequest) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services with no namespace must not have the same names as the namespaces. \ + SameName is used as a name for a service with no namespace and a namespace. + """ + ) + ) + } + } +} + +extension SnippetBasedTranslatorTests { + private func assertTypealiasTranslation( + codeGenerationRequest: CodeGenerationRequest, + expectedSwift: String + ) throws { + let translator = TypealiasTranslator() + let codeBlocks = try translator.translate(from: codeGenerationRequest) + let renderer = TextBasedRenderer.default + renderer.renderCodeBlocks(codeBlocks) + let contents = renderer.renderedContents() + try XCTAssertEqualWithDiff(contents, expectedSwift) + } + + private func assertThrowsError( + ofType: E.Type, + _ expression: @autoclosure () throws -> T, + _ errorHandler: (E) -> Void + ) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? E else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + errorHandler(error) + } + } +} + +extension SnippetBasedTranslatorTests { + private func makeCodeGenerationRequest(services: [ServiceDescriptor]) -> CodeGenerationRequest { + return CodeGenerationRequest( + fileName: "test.grpc", + leadingTrivia: "Some really exciting license header 2023.", + dependencies: [], + services: services, + lookupSerializer: { + "ProtobufSerializer<\($0)>()" + }, + lookupDeserializer: { + "ProtobufDeserializer<\($0)>()" + } + ) + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift new file mode 100644 index 000000000..4fcac62a8 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -0,0 +1,67 @@ +/* + * Copyright 2023, 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. + */ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest + +private func diff(expected: String, actual: String) throws -> String { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = [ + "bash", "-c", + "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')", + ] + let pipe = Pipe() + process.standardOutput = pipe + try process.run() + process.waitUntilExit() + let pipeData = try XCTUnwrap( + pipe.fileHandleForReading.readToEnd(), + """ + No output from command: + \(process.executableURL!.path) \(process.arguments!.joined(separator: " ")) + """ + ) + return String(decoding: pipeData, as: UTF8.self) +} + +internal func XCTAssertEqualWithDiff( + _ actual: String, + _ expected: String, + file: StaticString = #filePath, + line: UInt = #line +) throws { + if actual == expected { return } + XCTFail( + """ + XCTAssertEqualWithDiff failed (click for diff) + \(try diff(expected: expected, actual: actual)) + """, + file: file, + line: line + ) +} From 4570d0f97575d8465e6fa96ca27bcdff997857e6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 11 Dec 2023 17:06:46 +0000 Subject: [PATCH 194/580] Add benchmark thresholds for nightly (#1743) Motivation: The benchmarks are run on nightly builds but there are no thresholds. This is causing nightly CI to fail as the benchmark package is now more stringent about missing thresholds. Modifications: - Add thresholds for nightly Result: CI passes --- .../main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json | 7 +++++++ .../main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json | 7 +++++++ ...GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json | 7 +++++++ ...e_binary_values_when_only_binary_values_stored.p90.json | 7 +++++++ ...Iterate_binary_values_when_only_strings_stored.p90.json | 7 +++++++ ...CSwiftBenchmark.Metadata_Iterate_string_values.p90.json | 7 +++++++ ...CSwiftBenchmark.Metadata_Remove_values_for_key.p90.json | 7 +++++++ 7 files changed, 49 insertions(+) create mode 100644 Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json create mode 100644 Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json create mode 100644 Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json create mode 100644 Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json create mode 100644 Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json new file mode 100644 index 000000000..b642696c1 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 3012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json new file mode 100644 index 000000000..7fde30a69 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 4012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json new file mode 100644 index 000000000..b59f05063 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 2000, + "memoryLeaked" : 0, + "releaseCount" : 6001, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json new file mode 100644 index 000000000..5750750bc --- /dev/null +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 2002001, + "retainCount" : 1999000, + "syscalls" : 0 +} From 4bd354e14ab992d83d2d4429f33428b080e22fe7 Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Tue, 19 Dec 2023 08:51:56 +0000 Subject: [PATCH 195/580] Update available to fix build for non-macOS Apple platforms (#1746) ## Motivation The package currently doesn't build for non-macOS Apple platforms, e.g. iOS, because of missing `@available` annotations, mostly in tests. ## Modifications - Add missing `@available` annotations. - Use `#if os(macOS) || os(Linux)` in test utils that require `Foundation.Process`. Ideally we'd use `#if canImport(Foundation.Process)`, but the version of `swift-format` used by this project doesn't understand it. ## Result Code and tests can build for, and run on, other platforms, e.g. iOS. --- Sources/Examples/PacketCapture/PacketCapture.swift | 2 +- .../Internal/Translator/SnippetBasedTranslatorTests.swift | 4 ++++ .../Internal/Translator/TestFunctions.swift | 5 +++++ Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift | 1 + Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift | 1 + Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift | 1 + Tests/GRPCCoreTests/ServerErrorTests.swift | 1 + .../Streaming/Internal/AsyncSequenceOfOne.swift | 1 + .../Streaming/Internal/BufferedStreamTests.swift | 1 + .../Test Utilities/AsyncSequence+Utilities.swift | 3 +++ .../Test Utilities/Call/Client/ClientInterceptors.swift | 4 ++++ .../Test Utilities/Call/Server/ServerInterceptors.swift | 4 ++++ .../Test Utilities/RPCAsyncSequence+Utilities.swift | 1 + .../GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift | 3 +++ .../GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift | 1 + .../Test Utilities/Transport/ThrowingTransport.swift | 2 ++ Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift | 6 ++++++ Tests/GRPCCoreTests/TimeoutTests.swift | 1 + .../InProcessServerTransportTests.swift | 1 + .../Test Utilities/XCTest+Utilities.swift | 1 + .../ServerHandlerStateMachineTests.swift | 5 +++++ Tests/GRPCTests/EchoMetadataTests.swift | 2 +- Tests/GRPCTests/GRPCAsyncClientCallTests.swift | 1 + Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift | 1 + Tests/GRPCTests/GRPCNetworkFrameworkTests.swift | 2 +- .../ReflectionServiceIntegrationTests.swift | 1 + .../ReflectionServiceUnitTests.swift | 1 + Tests/GRPCTests/ServerTLSErrorTests.swift | 1 + 28 files changed, 55 insertions(+), 3 deletions(-) diff --git a/Sources/Examples/PacketCapture/PacketCapture.swift b/Sources/Examples/PacketCapture/PacketCapture.swift index 468add246..83d6de958 100644 --- a/Sources/Examples/PacketCapture/PacketCapture.swift +++ b/Sources/Examples/PacketCapture/PacketCapture.swift @@ -21,7 +21,7 @@ import NIOExtras import NIOPosix @main -@available(macOS 10.15, *) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct PCAP: AsyncParsableCommand { @Option(help: "The port to connect to") var port = 1234 diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift index 70ed1f948..87954c8eb 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) + import XCTest @testable import GRPCCodeGen @@ -517,3 +519,5 @@ extension SnippetBasedTranslatorTests { ) } } + +#endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 4fcac62a8..18fc4b564 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -26,6 +26,9 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) + import XCTest private func diff(expected: String, actual: String) throws -> String { @@ -65,3 +68,5 @@ internal func XCTAssertEqualWithDiff( line: line ) } + +#endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index ff668f9f2..72f1e1c1f 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ClientResponseTests: XCTestCase { func testAcceptedSingleResponseConvenienceMethods() { let response = ClientResponse.Single( diff --git a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift index b83c242e8..50446f224 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift @@ -16,6 +16,7 @@ @_spi(Testing) import GRPCCore import XCTest +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ServerRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let single = ServerRequest.Single(metadata: ["bar": "baz"], message: "foo") diff --git a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift index 7f7fdb288..d5614e906 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift @@ -16,6 +16,7 @@ @_spi(Testing) import GRPCCore import XCTest +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ServerResponseTests: XCTestCase { func testSingleConvenienceInit() { var response = ServerResponse.Single( diff --git a/Tests/GRPCCoreTests/ServerErrorTests.swift b/Tests/GRPCCoreTests/ServerErrorTests.swift index afe2b8e2a..4413fe038 100644 --- a/Tests/GRPCCoreTests/ServerErrorTests.swift +++ b/Tests/GRPCCoreTests/ServerErrorTests.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ServerErrorTests: XCTestCase { func testCopyOnWrite() { // ServerError has a heap based storage, so check CoW semantics are correctly implemented. diff --git a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift index 6b29ca4a2..472bda341 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) internal final class AsyncSequenceOfOneTests: XCTestCase { func testSuccessPath() async throws { let sequence = RPCAsyncSequence.one("foo") diff --git a/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift b/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift index e34dd9bc2..b2e9f173a 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift @@ -1080,6 +1080,7 @@ final class BufferedStreamTests: XCTestCase { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream.Source.WriteResult { func assertIsProducerMore() { switch self { diff --git a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift index c5d43db4a..87bc7f75d 100644 --- a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension AsyncSequence { func collect() async throws -> [Element] { return try await self.reduce(into: []) { $0.append($1) } @@ -21,6 +22,7 @@ extension AsyncSequence { } #if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension AsyncStream { static func makeStream( of elementType: Element.Type = Element.self, @@ -34,6 +36,7 @@ extension AsyncStream { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension AsyncThrowingStream { static func makeStream( of elementType: Element.Type = Element.self, diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index e89681d58..46264437f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -16,6 +16,7 @@ import Atomics import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ClientInterceptor where Self == RejectAllClientInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllClientInterceptor(error: error, throw: false) @@ -27,6 +28,7 @@ extension ClientInterceptor where Self == RejectAllClientInterceptor { } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ClientInterceptor where Self == RequestCountingClientInterceptor { static func requestCounter(_ counter: ManagedAtomic) -> Self { return RequestCountingClientInterceptor(counter: counter) @@ -34,6 +36,7 @@ extension ClientInterceptor where Self == RequestCountingClientInterceptor { } /// Rejects all RPCs with the provided error. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct RejectAllClientInterceptor: ClientInterceptor { /// The error to reject all RPCs with. let error: RPCError @@ -62,6 +65,7 @@ struct RejectAllClientInterceptor: ClientInterceptor { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct RequestCountingClientInterceptor: ClientInterceptor { /// The number of requests made. let counter: ManagedAtomic diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 02d4061a6..169b90969 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -16,6 +16,7 @@ import Atomics import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerInterceptor where Self == RejectAllServerInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllServerInterceptor(error: error, throw: false) @@ -26,6 +27,7 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerInterceptor where Self == RequestCountingServerInterceptor { static func requestCounter(_ counter: ManagedAtomic) -> Self { return RequestCountingServerInterceptor(counter: counter) @@ -33,6 +35,7 @@ extension ServerInterceptor where Self == RequestCountingServerInterceptor { } /// Rejects all RPCs with the provided error. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct RejectAllServerInterceptor: ServerInterceptor { /// The error to reject all RPCs with. let error: RPCError @@ -61,6 +64,7 @@ struct RejectAllServerInterceptor: ServerInterceptor { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct RequestCountingServerInterceptor: ServerInterceptor { /// The number of requests made. let counter: ManagedAtomic diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift index 26bef603a..23a87d58b 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift @@ -15,6 +15,7 @@ */ import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCAsyncSequence { static func elements(_ elements: Element...) -> Self { return .elements(elements) diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift index e334dceb2..e58bd7084 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriter { /// Returns a writer which calls `XCTFail(_:)` on every write. static func failTestOnWrite(elementType: Element.Type = Element.self) -> Self { @@ -28,12 +29,14 @@ extension RPCWriter { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private struct FailOnWrite: RPCWriterProtocol { func write(contentsOf elements: some Sequence) async throws { XCTFail("Unexpected write") } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private struct AsyncStreamGatheringWriter: RPCWriterProtocol { let continuation: AsyncStream.Continuation diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index 6a4ceb07e..edcc358fc 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) struct BinaryEcho: RegistrableRPCService { func get( _ request: ServerRequest.Single<[UInt8]> diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 39cdbaceb..25dba3cf2 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -50,6 +50,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ThrowOnRunServerTransport: ServerTransport { func listen() async throws -> RPCAsyncSequence> { throw RPCError( @@ -63,6 +64,7 @@ struct ThrowOnRunServerTransport: ServerTransport { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ThrowOnSignalServerTransport: ServerTransport { let signal: AsyncStream diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 71ca6dd7a..88aec4a1f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -25,6 +25,7 @@ func XCTAssertDescription( XCTAssertEqual(String(describing: subject), expected, file: file, line: line) } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsErrorAsync( _ expression: () async throws -> T, errorHandler: (Error) -> Void @@ -50,6 +51,7 @@ func XCTAssertThrowsError( } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsErrorAsync( ofType: E.Type = E.self, _ expression: () async throws -> T, @@ -78,6 +80,7 @@ func XCTAssertThrowsRPCError( } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsRPCErrorAsync( _ expression: () async throws -> T, errorHandler: (RPCError) -> Void @@ -92,6 +95,7 @@ func XCTAssertThrowsRPCErrorAsync( } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertRejected( _ response: ClientResponse.Stream, errorHandler: (RPCError) -> Void @@ -128,6 +132,7 @@ func XCTAssertMetadata( } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertMetadata( _ part: RPCRequestPart?, metadataHandler: (Metadata) async throws -> Void = { _ in } @@ -152,6 +157,7 @@ func XCTAssertMessage( } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertMessage( _ part: RPCRequestPart?, messageHandler: ([UInt8]) async throws -> Void = { _ in } diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index ddb664bf2..aeb98d154 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -17,6 +17,7 @@ import XCTest @testable import GRPCCore +@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) final class TimeoutTests: XCTestCase { func testDecodeInvalidTimeout_Empty() { let timeoutHeader = "" diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 3e57d6ac4..78466ae8a 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -19,6 +19,7 @@ import XCTest @testable import GRPCCore @testable import GRPCInProcessTransport +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessServerTransport() diff --git a/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift index b49af0ec9..67d381073 100644 --- a/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift @@ -28,6 +28,7 @@ func XCTAssertThrowsError( } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsErrorAsync( ofType: E.Type = E.self, _ expression: () async throws -> T, diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift index e0f1827b9..32f9497cd 100644 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift +++ b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift @@ -265,6 +265,7 @@ internal final class ServerHandlerStateMachineTests: GRPCTestCase { // MARK: - Action Assertions +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerHandlerStateMachine.HandleMetadataAction { func assertInvokeHandler() { XCTAssertEqual(self, .invokeHandler) @@ -275,6 +276,7 @@ extension ServerHandlerStateMachine.HandleMetadataAction { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerHandlerStateMachine.HandleMessageAction { func assertForward() { XCTAssertEqual(self, .forward) @@ -285,6 +287,7 @@ extension ServerHandlerStateMachine.HandleMessageAction { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerHandlerStateMachine.SendMessageAction { func assertInterceptHeadersThenMessage(_ verify: (HPACKHeaders) -> Void = { _ in }) { switch self { @@ -304,6 +307,7 @@ extension ServerHandlerStateMachine.SendMessageAction { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerHandlerStateMachine.SendStatusAction { func assertIntercept(_ verify: (HPACKHeaders) -> Void = { _ in }) { switch self { @@ -319,6 +323,7 @@ extension ServerHandlerStateMachine.SendStatusAction { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension ServerHandlerStateMachine.CancelAction { func assertNone() { XCTAssertEqual(self, .none) diff --git a/Tests/GRPCTests/EchoMetadataTests.swift b/Tests/GRPCTests/EchoMetadataTests.swift index 567450d5e..510222ab9 100644 --- a/Tests/GRPCTests/EchoMetadataTests.swift +++ b/Tests/GRPCTests/EchoMetadataTests.swift @@ -81,7 +81,7 @@ internal final class EchoMetadataTests: GRPCTestCase { self.testServiceDescriptor(Echo_EchoClientMetadata.serviceDescriptor) self.testServiceDescriptor(Echo_EchoServerMetadata.serviceDescriptor) - if #available(macOS 12, *) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { self.testServiceDescriptor(Echo_EchoAsyncClient.serviceDescriptor) } } diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift index bc3f8e3d4..e74fa2c8d 100644 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift @@ -334,6 +334,7 @@ private actor RequestResponseCounter { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private final class AsyncEchoProvider: Echo_EchoAsyncProvider { let headers: HPACKHeaders let sendTwice: Bool diff --git a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift index efc6e1fa8..fda2aac07 100644 --- a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift +++ b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift @@ -522,6 +522,7 @@ class AsyncServerHandlerTests: GRPCTestCase { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) internal final class AsyncResponseStream: GRPCServerResponseWriter { private let source: NIOAsyncSequenceProducer< diff --git a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift index 8f97602f0..ecc81bf36 100644 --- a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift +++ b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift @@ -27,7 +27,7 @@ import NIOTransportServices import Security import XCTest -@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class GRPCNetworkFrameworkTests: GRPCTestCase { private var server: Server! private var client: ClientConnection! diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index f47aa12ec..f0eb4bab4 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -23,6 +23,7 @@ import XCTest @testable import GRPCReflectionService +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ReflectionServiceIntegrationTests: GRPCTestCase { private var server: Server? private var channel: GRPCChannel? diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index f9071208a..519c3dc1a 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -22,6 +22,7 @@ import XCTest @testable import GRPCReflectionService +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ReflectionServiceUnitTests: GRPCTestCase { /// Testing the fileDescriptorDataByFilename dictionary of the ReflectionServiceData object. func testFileDescriptorDataByFilename() throws { diff --git a/Tests/GRPCTests/ServerTLSErrorTests.swift b/Tests/GRPCTests/ServerTLSErrorTests.swift index 86305762c..cdaa7fad8 100644 --- a/Tests/GRPCTests/ServerTLSErrorTests.swift +++ b/Tests/GRPCTests/ServerTLSErrorTests.swift @@ -129,6 +129,7 @@ class ServerTLSErrorTests: GRPCTestCase { } } + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func testServerCustomVerificationCallback() async throws { let verificationCallbackInvoked = self.serverEventLoopGroup.next().makePromise(of: Void.self) let configuration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( From 717e3f878867e83ae156854c521c75bbe4e5e08d Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 21 Dec 2023 12:40:00 +0200 Subject: [PATCH 196/580] [CodeGenLib] Translator for server code (#1745) Motivation: In the generated server code for a service described in a Source IDL file we want to have: - a protocol defining all the methods as bidirectional streaming, conforming to `RegistrableRPCService` - an extension for the streaming protocol that implements the `registerRPCs` function that is required by the `RegistrableRPCService` conformance - a service protocol that defines the methods of the service, as they are descibed in the Source IDL (unary/input-streaming/output-streaming/bidirectional-streaming) that conforms to the streaming protocol. - the extension of the service protocol that implements the bidirectional streaming methods required by the conformance to the streaming protocol, using calls to the service methods (in case a service method is bidirectional streaming), it doesn't need to be reimplemented. Modifications: - Created the ServerCodeTranslator that conforms to SpecializedTranslator and contains all the methods for creating the representation of the generated code that we want using StructuredSwiftRepresentation format. - Implemented snippet tests that test the ServerCodeTranslator. - added the inout parameter type as a case of ExistingTypeDescription in StructuredSwiftRepresentation, as it is needed by the `registerRPCs` function. Result: Server code can now be generated for a CodeGenerationRequest object. --- .../Internal/Renderer/TextBasedRenderer.swift | 4 + .../StructuredSwiftRepresentation.swift | 5 + .../IDLToStructuredSwiftTranslator.swift | 25 + .../Translator/ServerCodeTranslator.swift | 474 ++++++++++++++++++ ...erverCodeTranslatorSnippetBasedTests.swift | 431 ++++++++++++++++ .../Internal/Translator/TestFunctions.swift | 32 ++ ...ypealiasTranslatorSnippetBasedTests.swift} | 66 +-- 7 files changed, 989 insertions(+), 48 deletions(-) create mode 100644 Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift rename Tests/GRPCCodeGenTests/Internal/Translator/{SnippetBasedTranslatorTests.swift => TypealiasTranslatorSnippetBasedTests.swift} (87%) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 0f58e0390..85a2cc264 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -839,6 +839,10 @@ struct TextBasedRenderer: RendererProtocol { } writer.writeLine(": ") writer.nextLineAppendsToLastLine() + if parameterDescription.inout { + writer.writeLine("inout ") + writer.nextLineAppendsToLastLine() + } writer.writeLine(renderedExistingTypeDescription(parameterDescription.type)) if let defaultValue = parameterDescription.defaultValue { writer.nextLineAppendsToLastLine() diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index c78d459b7..fc7ce726e 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -490,6 +490,11 @@ struct ParameterDescription: Equatable, Codable { /// For example, in `bar baz: String = "hi"`, `defaultValue` /// represents `"hi"`. var defaultValue: Expression? = nil + + /// An inout parameter type. + /// + /// For example, `bar baz: inout String`. + var `inout`: Bool = false } /// A function kind: `func` or `init`. diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 3870c530b..ab1da8ee7 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -16,6 +16,7 @@ struct IDLToStructuredSwiftTranslator: Translator { private let typealiasTranslator = TypealiasTranslator() + private let serverCodeTranslator = ServerCodeTranslator() func translate( codeGenerationRequest: CodeGenerationRequest, @@ -31,6 +32,12 @@ struct IDLToStructuredSwiftTranslator: Translator { contentsOf: try self.typealiasTranslator.translate(from: codeGenerationRequest) ) + if server { + codeBlocks.append( + contentsOf: try self.serverCodeTranslator.translate(from: codeGenerationRequest) + ) + } + let fileDescription = FileDescription( topComment: topComment, imports: imports, @@ -41,3 +48,21 @@ struct IDLToStructuredSwiftTranslator: Translator { return StructuredSwiftRepresentation(file: file) } } + +extension CodeGenerationRequest.ServiceDescriptor { + var namespacedTypealiasPrefix: String { + if self.namespace.isEmpty { + return self.name + } else { + return "\(self.namespace).\(self.name)" + } + } + + var namespacedPrefix: String { + if self.namespace.isEmpty { + return self.name + } else { + return "\(self.namespace)_\(self.name)" + } + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift new file mode 100644 index 000000000..661389492 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -0,0 +1,474 @@ +/* + * Copyright 2023, 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. + */ + +/// Creates a representation for the server code that will be generated based on the ``CodeGenerationRequest`` object +/// specifications, using types from ``StructuredSwiftRepresentation``. +/// +/// For example, in the case of a service called "Bar", in the "foo" namespace which has +/// one method "baz", the ``ServerCodeTranslator`` will create +/// a representation for the following generated code: +/// +/// ```swift +/// public protocol foo_BarServiceStreamingProtocol: GRPCCore.RegistrableRPCService { +/// func baz( +/// request: ServerRequest.Stream +/// ) async throws -> ServerResponse.Stream +/// } +/// // Generated conformance to `RegistrableRPCService`. +/// extension foo.Bar.StreamingServiceProtocol { +/// public func registerRPCs(with router: inout RPCRouter) { +/// router.registerHandler( +/// for: foo.Method.baz.descriptor, +/// deserializer: ProtobufDeserializer(), +/// serializer: ProtobufSerializer(), +/// handler: { request in try await self.baz(request: request) } +/// ) +/// } +/// } +/// public protocol foo_BarServiceProtocol: foo.Bar.StreamingServiceProtocol { +/// func baz( +/// request: ServerRequest.Single +/// ) async throws -> ServerResponse.Single +/// } +/// // Generated partial conformance to `foo_BarStreamingServiceProtocol`. +/// extension foo.Bar.ServiceProtocol { +/// public func baz( +/// request: ServerRequest.Stream +/// ) async throws -> ServerResponse.Stream { +/// let response = try await self.baz(request: ServerRequest.Single(stream: request) +/// return ServerResponse.Stream(single: response) +/// } +/// } +///``` +struct ServerCodeTranslator: SpecializedTranslator { + func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { + var codeBlocks = [CodeBlock]() + for service in codeGenerationRequest.services { + // Create the streaming protocol that declares the service methods as bidirectional streaming. + let streamingProtocol = CodeBlockItem.declaration(self.makeStreamingProtocol(for: service)) + codeBlocks.append(CodeBlock(item: streamingProtocol)) + + // Create extension for implementing the 'registerRPCs' function which is a 'RegistrableRPCService' requirement. + let conformanceToRPCServiceExtension = CodeBlockItem.declaration( + self.makeConformanceToRPCServiceExtension(for: service, in: codeGenerationRequest) + ) + codeBlocks.append( + CodeBlock( + comment: .doc("Conformance to `GRPCCore.RegistrableRPCService`."), + item: conformanceToRPCServiceExtension + ) + ) + + // Create the service protocol that declares the service methods as they are described in the Source IDL (unary, + // client/server streaming or bidirectional streaming). + let serviceProtocol = CodeBlockItem.declaration(self.makeServiceProtocol(for: service)) + codeBlocks.append(CodeBlock(item: serviceProtocol)) + + // Create extension for partial conformance to the streaming protocol. + let extensionServiceProtocol = CodeBlockItem.declaration( + self.makeExtensionServiceProtocol(for: service) + ) + codeBlocks.append( + CodeBlock( + comment: .doc( + "Partial conformance to `\(self.protocolName(service: service, streaming: true))`." + ), + item: extensionServiceProtocol + ) + ) + } + + return codeBlocks + } +} + +extension ServerCodeTranslator { + private func makeStreamingProtocol( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + let methods = service.methods.compactMap { + Declaration.commentable( + .doc($0.documentation), + .function( + FunctionDescription( + signature: self.makeStreamingMethodSignature(for: $0, in: service) + ) + ) + ) + } + + let streamingProtocol = Declaration.protocol( + .init( + name: self.protocolName(service: service, streaming: true), + conformances: ["GRPCCore.RegistrableRPCService"], + members: methods + ) + ) + + return .commentable(.doc(service.documentation), streamingProtocol) + } + + private func makeStreamingMethodSignature( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> FunctionSignatureDescription { + return FunctionSignatureDescription( + kind: .function(name: method.name), + parameters: [ + .init( + label: "request", + type: .generic( + wrapper: .member(["ServerRequest", "Stream"]), + wrapped: .member( + self.methodInputOutputTypealias(for: method, service: service, type: .input) + ) + ) + ) + ], + keywords: [.async, .throws], + returnType: .identifierType( + .generic( + wrapper: .member(["ServerResponse", "Stream"]), + wrapped: .member( + self.methodInputOutputTypealias(for: method, service: service, type: .output) + ) + ) + ) + ) + } + + private func makeConformanceToRPCServiceExtension( + for service: CodeGenerationRequest.ServiceDescriptor, + in codeGenerationRequest: CodeGenerationRequest + ) -> Declaration { + let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) + let registerRPCMethod = self.makeRegisterRPCsMethod(for: service, in: codeGenerationRequest) + return .extension( + accessModifier: .public, + onType: streamingProtocol, + declarations: [registerRPCMethod] + ) + } + + private func makeRegisterRPCsMethod( + for service: CodeGenerationRequest.ServiceDescriptor, + in codeGenerationRequest: CodeGenerationRequest + ) -> Declaration { + let registerRPCsSignature = FunctionSignatureDescription( + kind: .function(name: "registerRPCs"), + parameters: [ + .init( + label: "with", + name: "router", + type: .member(["GRPCCore", "RPCRouter"]), + `inout`: true + ) + ] + ) + let registerRPCsBody = self.makeRegisterRPCsMethodBody(for: service, in: codeGenerationRequest) + return .function(signature: registerRPCsSignature, body: registerRPCsBody) + } + + private func makeRegisterRPCsMethodBody( + for service: CodeGenerationRequest.ServiceDescriptor, + in codeGenerationRequest: CodeGenerationRequest + ) -> [CodeBlock] { + let registerHandlerCalls = service.methods.compactMap { + CodeBlock.expression( + Expression.functionCall( + calledExpression: .memberAccess( + MemberAccessDescription(left: .identifierPattern("router"), right: "registerHandler") + ), + arguments: self.makeArgumentsForRegisterHandler( + for: $0, + in: service, + from: codeGenerationRequest + ) + ) + ) + } + + return registerHandlerCalls + } + + private func makeArgumentsForRegisterHandler( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor, + from codeGenerationRequest: CodeGenerationRequest + ) -> [FunctionArgumentDescription] { + var arguments = [FunctionArgumentDescription]() + arguments.append( + .init( + label: "for", + expression: .identifierPattern( + self.methodDescriptorPath(for: method, service: service) + ) + ) + ) + + arguments.append( + .init( + label: "deserializer", + expression: .identifierPattern( + codeGenerationRequest.lookupDeserializer( + self.methodInputOutputTypealias(for: method, service: service, type: .input) + ) + ) + ) + ) + + arguments.append( + .init( + label: "serializer", + expression: + .identifierPattern( + codeGenerationRequest.lookupSerializer( + self.methodInputOutputTypealias(for: method, service: service, type: .output) + ) + ) + ) + ) + + let getFunctionCall = Expression.functionCall( + calledExpression: .memberAccess( + MemberAccessDescription(left: .identifierPattern("self"), right: method.name) + ), + arguments: [ + FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")) + ] + ) + + let handlerClosureBody = Expression.unaryKeyword( + kind: .try, + expression: .unaryKeyword(kind: .await, expression: getFunctionCall) + ) + + arguments.append( + .init( + label: "handler", + expression: .closureInvocation( + .init(argumentNames: ["request"], body: [.expression(handlerClosureBody)]) + ) + ) + ) + + return arguments + } + + private func makeServiceProtocol( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + let methods = service.methods.compactMap { + self.makeServiceProtocolMethod(for: $0, in: service) + } + let protocolName = self.protocolName(service: service, streaming: false) + let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) + + return .commentable( + .doc(service.documentation), + .protocol( + ProtocolDescription(name: protocolName, conformances: [streamingProtocol], members: methods) + ) + ) + } + + private func makeServiceProtocolMethod( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + let inputStreaming = method.isInputStreaming ? "Stream" : "Single" + let outputStreaming = method.isOutputStreaming ? "Stream" : "Single" + + let inputTypealiasComponents = self.methodInputOutputTypealias( + for: method, + service: service, + type: .input + ) + let outputTypealiasComponents = self.methodInputOutputTypealias( + for: method, + service: service, + type: .output + ) + + let functionSignature = FunctionSignatureDescription( + kind: .function(name: method.name), + parameters: [ + .init( + label: "request", + type: + .generic( + wrapper: .member(["ServerRequest", inputStreaming]), + wrapped: .member(inputTypealiasComponents) + ) + ) + ], + keywords: [.async, .throws], + returnType: .identifierType( + .generic( + wrapper: .member(["ServerResponse", outputStreaming]), + wrapped: .member(outputTypealiasComponents) + ) + ) + ) + + return .commentable( + .doc(method.documentation), + .function(FunctionDescription(signature: functionSignature)) + ) + } + + private func makeExtensionServiceProtocol( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + let methods = service.methods.compactMap { + self.makeServiceProtocolExtensionMethod(for: $0, in: service) + } + + let protocolName = self.protocolNameTypealias(service: service, streaming: false) + return .extension(accessModifier: .public, onType: protocolName, declarations: methods) + } + + private func makeServiceProtocolExtensionMethod( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration? { + // The method has the same definition in StreamingServiceProtocol and ServiceProtocol. + if method.isInputStreaming && method.isOutputStreaming { + return nil + } + + let response = CodeBlock(item: .declaration(self.makeResponse(for: method))) + let returnStatement = CodeBlock(item: .expression(self.makeReturnStatement(for: method))) + + return .function( + signature: self.makeStreamingMethodSignature(for: method, in: service), + body: [response, returnStatement] + ) + } + + private func makeResponse( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + ) -> Declaration { + let serverRequest: Expression + if !method.isInputStreaming { + // Transform the streaming request into a unary request. + serverRequest = Expression.functionCall( + calledExpression: .memberAccess( + MemberAccessDescription( + left: .identifierPattern("ServerRequest"), + right: "Single" + ) + ), + arguments: [ + FunctionArgumentDescription(label: "stream", expression: .identifierPattern("request")) + ] + ) + } else { + serverRequest = Expression.identifierPattern("request") + } + // Call to the corresponding ServiceProtocol method. + let serviceProtocolMethod = Expression.functionCall( + calledExpression: .memberAccess( + MemberAccessDescription(left: .identifierPattern("self"), right: method.name) + ), + arguments: [FunctionArgumentDescription(label: "request", expression: serverRequest)] + ) + + let responseValue = Expression.unaryKeyword( + kind: .try, + expression: .unaryKeyword(kind: .await, expression: serviceProtocolMethod) + ) + + return .variable(kind: .let, left: "response", right: responseValue) + } + + private func makeReturnStatement( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + ) -> Expression { + let returnValue: Expression + // Transforming the unary response into a streaming one. + if !method.isOutputStreaming { + returnValue = .functionCall( + calledExpression: .memberAccess( + MemberAccessDescription( + left: .identifierType(.member(["ServerResponse"])), + right: "Stream" + ) + ), + arguments: [ + (FunctionArgumentDescription(label: "single", expression: .identifierPattern("response"))) + ] + ) + } else { + returnValue = .identifierPattern("response") + } + + return .unaryKeyword(kind: .return, expression: returnValue) + } + + fileprivate enum InputOutputType { + case input + case output + } + + /// Generates the fully qualified name of the typealias for the input or output type of a method. + private func methodInputOutputTypealias( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + service: CodeGenerationRequest.ServiceDescriptor, + type: InputOutputType + ) -> String { + var components: String = "\(service.namespacedTypealiasPrefix).Methods.\(method.name)" + + switch type { + case .input: + components.append(".Input") + case .output: + components.append(".Output") + } + + return components + } + + /// Generates the fully qualified name of a method descriptor. + private func methodDescriptorPath( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + service: CodeGenerationRequest.ServiceDescriptor + ) -> String { + return "\(service.namespacedTypealiasPrefix).Methods.\(method.name).descriptor" + } + + /// Generates the fully qualified name of the type alias for a service protocol. + internal func protocolNameTypealias( + service: CodeGenerationRequest.ServiceDescriptor, + streaming: Bool + ) -> String { + if streaming { + return "\(service.namespacedTypealiasPrefix).StreamingServiceProtocol" + } + return "\(service.namespacedTypealiasPrefix).ServiceProtocol" + } + + /// Generates the name of a service protocol. + internal func protocolName( + service: CodeGenerationRequest.ServiceDescriptor, + streaming: Bool + ) -> String { + if streaming { + return "\(service.namespacedPrefix)StreamingServiceProtocol" + } + return "\(service.namespacedPrefix)ServiceProtocol" + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift new file mode 100644 index 000000000..bb12826c9 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -0,0 +1,431 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCodeGen + +final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { + typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + + func testServerCodeTranslatorUnaryMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for unaryMethod", + name: "unaryMethod", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Documentation for unaryMethod + func unaryMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension namespaceA.ServiceA.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: namespaceA.ServiceA.Methods.unaryMethod.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.unaryMethod(request: request) + } + ) + } + } + /// Documentation for ServiceA + protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + /// Documentation for unaryMethod + func unaryMethod(request: ServerRequest.Single) async throws -> ServerResponse.Single + } + /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. + public extension namespaceA.ServiceA.ServiceProtocol { + func unaryMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.unaryMethod(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + } + """ + + try self.assertServerCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testServerCodeTranslatorInputStreamingMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for inputStreamingMethod", + name: "inputStreamingMethod", + isInputStreaming: true, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Documentation for inputStreamingMethod + func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension namespaceA.ServiceA.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.inputStreamingMethod(request: request) + } + ) + } + } + /// Documentation for ServiceA + protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + /// Documentation for inputStreamingMethod + func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Single + } + /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. + public extension namespaceA.ServiceA.ServiceProtocol { + func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.inputStreamingMethod(request: request) + return ServerResponse.Stream(single: response) + } + } + """ + + try self.assertServerCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testServerCodeTranslatorOutputStreamingMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for outputStreamingMethod", + name: "outputStreamingMethod", + isInputStreaming: false, + isOutputStreaming: true, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Documentation for outputStreamingMethod + func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension namespaceA.ServiceA.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: namespaceA.ServiceA.Methods.outputStreamingMethod.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.outputStreamingMethod(request: request) + } + ) + } + } + /// Documentation for ServiceA + protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + /// Documentation for outputStreamingMethod + func outputStreamingMethod(request: ServerRequest.Single) async throws -> ServerResponse.Stream + } + /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. + public extension namespaceA.ServiceA.ServiceProtocol { + func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request)) + return response + } + } + """ + + try self.assertServerCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testServerCodeTranslatorBidirectionalStreamingMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for bidirectionalStreamingMethod", + name: "bidirectionalStreamingMethod", + isInputStreaming: true, + isOutputStreaming: true, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Documentation for bidirectionalStreamingMethod + func bidirectionalStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension namespaceA.ServiceA.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.bidirectionalStreamingMethod(request: request) + } + ) + } + } + /// Documentation for ServiceA + protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + /// Documentation for bidirectionalStreamingMethod + func bidirectionalStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. + public extension namespaceA.ServiceA.ServiceProtocol { + } + """ + + try self.assertServerCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testServerCodeTranslatorMultipleMethods() throws { + let inputStreamingMethod = MethodDescriptor( + documentation: "Documentation for inputStreamingMethod", + name: "inputStreamingMethod", + isInputStreaming: true, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let outputStreamingMethod = MethodDescriptor( + documentation: "Documentation for outputStreamingMethod", + name: "outputStreamingMethod", + isInputStreaming: false, + isOutputStreaming: true, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [inputStreamingMethod, outputStreamingMethod] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Documentation for inputStreamingMethod + func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + /// Documentation for outputStreamingMethod + func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension namespaceA.ServiceA.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.inputStreamingMethod(request: request) + } + ) + router.registerHandler( + for: namespaceA.ServiceA.Methods.outputStreamingMethod.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.outputStreamingMethod(request: request) + } + ) + } + } + /// Documentation for ServiceA + protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + /// Documentation for inputStreamingMethod + func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Single + /// Documentation for outputStreamingMethod + func outputStreamingMethod(request: ServerRequest.Single) async throws -> ServerResponse.Stream + } + /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. + public extension namespaceA.ServiceA.ServiceProtocol { + func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.inputStreamingMethod(request: request) + return ServerResponse.Stream(single: response) + } + func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request)) + return response + } + } + """ + + try assertServerCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testServerCodeTranslatorNoNamespaceService() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "methodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Documentation for MethodA + func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension ServiceA.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: ServiceA.Methods.methodA.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.methodA(request: request) + } + ) + } + } + /// Documentation for ServiceA + protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { + /// Documentation for MethodA + func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single + } + /// Partial conformance to `ServiceAStreamingServiceProtocol`. + public extension ServiceA.ServiceProtocol { + func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.methodA(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + } + """ + + try self.assertServerCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testServerCodeTranslatorMoreServicesOrder() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [] + ) + let serviceB = ServiceDescriptor( + documentation: "Documentation for ServiceB", + name: "ServiceB", + namespace: "namespaceA", + methods: [] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension namespaceA.ServiceA.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) {} + } + /// Documentation for ServiceA + protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {} + /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. + public extension namespaceA.ServiceA.ServiceProtocol { + } + /// Documentation for ServiceB + protocol namespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + /// Conformance to `GRPCCore.RegistrableRPCService`. + public extension namespaceA.ServiceB.StreamingServiceProtocol { + func registerRPCs(with router: inout GRPCCore.RPCRouter) {} + } + /// Documentation for ServiceB + protocol namespaceA_ServiceBServiceProtocol: namespaceA.ServiceB.StreamingServiceProtocol {} + /// Partial conformance to `namespaceA_ServiceBStreamingServiceProtocol`. + public extension namespaceA.ServiceB.ServiceProtocol { + } + """ + + try self.assertServerCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), + expectedSwift: expectedSwift + ) + } + + private func assertServerCodeTranslation( + codeGenerationRequest: CodeGenerationRequest, + expectedSwift: String + ) throws { + let translator = ServerCodeTranslator() + let codeBlocks = try translator.translate(from: codeGenerationRequest) + let renderer = TextBasedRenderer.default + renderer.renderCodeBlocks(codeBlocks) + let contents = renderer.renderedContents() + try XCTAssertEqualWithDiff(contents, expectedSwift) + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 18fc4b564..4c2d781cd 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -31,6 +31,8 @@ import XCTest +import GRPCCodeGen + private func diff(expected: String, actual: String) throws -> String { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/env") @@ -69,4 +71,34 @@ internal func XCTAssertEqualWithDiff( ) } +internal func makeCodeGenerationRequest( + services: [CodeGenerationRequest.ServiceDescriptor] +) -> CodeGenerationRequest { + return CodeGenerationRequest( + fileName: "test.grpc", + leadingTrivia: "Some really exciting license header 2023.", + dependencies: [], + services: services, + lookupSerializer: { + "ProtobufSerializer<\($0)>()" + }, + lookupDeserializer: { + "ProtobufDeserializer<\($0)>()" + } + ) +} + +internal func XCTAssertThrowsError( + ofType: E.Type, + _ expression: @autoclosure () throws -> T, + _ errorHandler: (E) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? E else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + errorHandler(error) + } +} + #endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift similarity index 87% rename from Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift rename to Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 87954c8eb..f888faab3 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/SnippetBasedTranslatorTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -20,7 +20,7 @@ import XCTest @testable import GRPCCodeGen -final class SnippetBasedTranslatorTests: XCTestCase { +final class TypealiasTranslatorSnippetBasedTests: XCTestCase { typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor @@ -63,7 +63,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift ) } @@ -105,7 +105,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift ) } @@ -166,7 +166,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift ) } @@ -191,7 +191,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [service]), + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift ) } @@ -230,7 +230,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]), + codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), expectedSwift: expectedSwift ) } @@ -267,7 +267,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]), + codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), expectedSwift: expectedSwift ) } @@ -308,7 +308,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceB, serviceA]), + codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), expectedSwift: expectedSwift ) } @@ -345,7 +345,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { """ try self.assertTypealiasTranslation( - codeGenerationRequest: self.makeCodeGenerationRequest(services: [serviceA, serviceB]), + codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), expectedSwift: expectedSwift ) } @@ -358,9 +358,9 @@ final class SnippetBasedTranslatorTests: XCTestCase { methods: [] ) - let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceA]) + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) let translator = TypealiasTranslator() - self.assertThrowsError( + XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) ) { @@ -386,9 +386,9 @@ final class SnippetBasedTranslatorTests: XCTestCase { methods: [] ) - let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceA]) + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) let translator = TypealiasTranslator() - self.assertThrowsError( + XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) ) { @@ -422,9 +422,9 @@ final class SnippetBasedTranslatorTests: XCTestCase { methods: [methodA, methodA] ) - let codeGenerationRequest = self.makeCodeGenerationRequest(services: [service]) + let codeGenerationRequest = makeCodeGenerationRequest(services: [service]) let translator = TypealiasTranslator() - self.assertThrowsError( + XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) ) { @@ -455,9 +455,9 @@ final class SnippetBasedTranslatorTests: XCTestCase { namespace: "SameName", methods: [] ) - let codeGenerationRequest = self.makeCodeGenerationRequest(services: [serviceA, serviceB]) + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB]) let translator = TypealiasTranslator() - self.assertThrowsError( + XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) ) { @@ -476,7 +476,7 @@ final class SnippetBasedTranslatorTests: XCTestCase { } } -extension SnippetBasedTranslatorTests { +extension TypealiasTranslatorSnippetBasedTests { private func assertTypealiasTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String @@ -488,36 +488,6 @@ extension SnippetBasedTranslatorTests { let contents = renderer.renderedContents() try XCTAssertEqualWithDiff(contents, expectedSwift) } - - private func assertThrowsError( - ofType: E.Type, - _ expression: @autoclosure () throws -> T, - _ errorHandler: (E) -> Void - ) { - XCTAssertThrowsError(try expression()) { error in - guard let error = error as? E else { - return XCTFail("Error had unexpected type '\(type(of: error))'") - } - errorHandler(error) - } - } -} - -extension SnippetBasedTranslatorTests { - private func makeCodeGenerationRequest(services: [ServiceDescriptor]) -> CodeGenerationRequest { - return CodeGenerationRequest( - fileName: "test.grpc", - leadingTrivia: "Some really exciting license header 2023.", - dependencies: [], - services: services, - lookupSerializer: { - "ProtobufSerializer<\($0)>()" - }, - lookupDeserializer: { - "ProtobufDeserializer<\($0)>()" - } - ) - } } #endif // os(macOS) || os(Linux) From 61456310e10d2ec2b0db2befa26df035834d9489 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:06:02 +0200 Subject: [PATCH 197/580] [CodeGenLib] Typealias translator configuration for client and server (#1750) Motivation: The typealias translator should generate typealias for the client and server protocols only if the associated code (client/server code) will be generated as well. Also, at the moment the Typealias translator doesn't generate the ClientProtocol typealias. Modifications: - added a new initialiser for the TypealiasTranslator that sets the client and server property. - Based on the client and server properties' values the typealias translator generates or not the protocol typealiases. - created method gor client protocol typalias generation. - modified tests, so they include the client code. - added new tests for generating only client or server code. Result: The typealias translator will generate typealiases for client and server protocols only when the associated code is also generated, getting rid of the possibility of an error (when the protocol doesn't exist). --- .../IDLToStructuredSwiftTranslator.swift | 4 +- .../Translator/TypealiasTranslator.swift | 63 +++++-- ...TypealiasTranslatorSnippetBasedTests.swift | 178 ++++++++++++++++-- 3 files changed, 211 insertions(+), 34 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index ab1da8ee7..c90bbffbf 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -15,7 +15,6 @@ */ struct IDLToStructuredSwiftTranslator: Translator { - private let typealiasTranslator = TypealiasTranslator() private let serverCodeTranslator = ServerCodeTranslator() func translate( @@ -23,13 +22,14 @@ struct IDLToStructuredSwiftTranslator: Translator { client: Bool, server: Bool ) throws -> StructuredSwiftRepresentation { + let typealiasTranslator = TypealiasTranslator(client: client, server: server) let topComment = Comment.doc(codeGenerationRequest.leadingTrivia) let imports: [ImportDescription] = [ ImportDescription(moduleName: "GRPCCore") ] var codeBlocks: [CodeBlock] = [] codeBlocks.append( - contentsOf: try self.typealiasTranslator.translate(from: codeGenerationRequest) + contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest) ) if server { diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 1737b38ae..a502675df 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -53,6 +53,14 @@ /// A ``CodeGenerationRequest`` can contain multiple namespaces, so the TypealiasTranslator will create a ``CodeBlock`` /// for each namespace. struct TypealiasTranslator: SpecializedTranslator { + let client: Bool + let server: Bool + + init(client: Bool, server: Bool) { + self.client = client + self.server = server + } + func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { var codeBlocks: [CodeBlock] = [] let services = codeGenerationRequest.services @@ -169,9 +177,21 @@ extension TypealiasTranslator { let methodDescriptorsDeclaration = self.makeMethodDescriptors(for: service) serviceEnum.members.append(methodDescriptorsDeclaration) - // Create the streaming and non-streaming service protocol type aliases. - let serviceProtocols = self.makeServiceProtocolsTypealiases(for: service) - serviceEnum.members.append(contentsOf: serviceProtocols) + if self.server { + // Create the streaming and non-streaming service protocol type aliases. + let serviceProtocols = self.makeServiceProtocolsTypealiases(for: service) + serviceEnum.members.append(contentsOf: serviceProtocols) + } + + if self.client { + // Create the client protocol type alias. + let clientProtocol = self.makeClientProtocolTypealias(for: service) + serviceEnum.members.append(clientProtocol) + + // Create type alias for Client struct. + let clientStruct = self.makeClientStructTypealias(for: service) + serviceEnum.members.append(clientStruct) + } return .enum(serviceEnum) } @@ -236,7 +256,7 @@ extension TypealiasTranslator { let descriptorDeclarationRight = Expression.functionCall( FunctionCallDescription( - calledExpression: .identifierType(.member(["MethodDescriptor"])), + calledExpression: .identifierType(.member("MethodDescriptor")), arguments: [ FunctionArgumentDescription( label: "service", @@ -277,7 +297,7 @@ extension TypealiasTranslator { isStatic: true, kind: .let, left: .identifier(.pattern("methods")), - type: .array(.member(["MethodDescriptor"])), + type: .array(.member("MethodDescriptor")), right: .literal(.array(methodDescriptors)) ) } @@ -285,26 +305,33 @@ extension TypealiasTranslator { private func makeServiceProtocolsTypealiases( for service: CodeGenerationRequest.ServiceDescriptor ) -> [Declaration] { - let namespacedPrefix: String - - if service.namespace.isEmpty { - namespacedPrefix = service.name - } else { - namespacedPrefix = "\(service.namespace)_\(service.name)" - } - - let streamingServiceProtocolName = "\(namespacedPrefix)ServiceStreamingProtocol" let streamingServiceProtocolTypealias = Declaration.typealias( name: "StreamingServiceProtocol", - existingType: .member([streamingServiceProtocolName]) + existingType: .member("\(service.namespacedPrefix)ServiceStreamingProtocol") ) - - let serviceProtocolName = "\(namespacedPrefix)ServiceProtocol" let serviceProtocolTypealias = Declaration.typealias( name: "ServiceProtocol", - existingType: .member([serviceProtocolName]) + existingType: .member("\(service.namespacedPrefix)ServiceProtocol") ) return [streamingServiceProtocolTypealias, serviceProtocolTypealias] } + + private func makeClientProtocolTypealias( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + return .typealias( + name: "ClientProtocol", + existingType: .member("\(service.namespacedPrefix)ClientProtocol") + ) + } + + private func makeClientStructTypealias( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + return .typealias( + name: "Client", + existingType: .member("\(service.namespacedPrefix)Client") + ) + } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index f888faab3..b677217cd 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -58,13 +58,125 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ] typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + typealias ClientProtocol = namespaceA_ServiceAClientProtocol + typealias Client = namespaceA_ServiceAClient } } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true + ) + } + + func testTypealiasTranslatorNoMethodsServiceClientAndServer() throws { + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [] + ) + let expectedSwift = + """ + enum namespaceA { + enum ServiceA { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + typealias ClientProtocol = namespaceA_ServiceAClientProtocol + typealias Client = namespaceA_ServiceAClient + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift, + client: true, + server: true + ) + } + + func testTypealiasTranslatorServer() throws { + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [] + ) + let expectedSwift = + """ + enum namespaceA { + enum ServiceA { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift, + client: false, + server: true + ) + } + + func testTypealiasTranslatorClient() throws { + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [] + ) + let expectedSwift = + """ + enum namespaceA { + enum ServiceA { + enum Methods {} + static let methods: [MethodDescriptor] = [] + typealias ClientProtocol = namespaceA_ServiceAClientProtocol + typealias Client = namespaceA_ServiceAClient + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift, + client: true, + server: false + ) + } + + func testTypealiasTranslatorNoClientNoServer() throws { + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [] + ) + let expectedSwift = + """ + enum namespaceA { + enum ServiceA { + enum Methods {} + static let methods: [MethodDescriptor] = [] + } + } + """ + + try self.assertTypealiasTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift, + client: false, + server: false ) } @@ -101,12 +213,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ] typealias StreamingServiceProtocol = ServiceAServiceStreamingProtocol typealias ServiceProtocol = ServiceAServiceProtocol + typealias ClientProtocol = ServiceAClientProtocol + typealias Client = ServiceAClient } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true ) } @@ -161,13 +277,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ] typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + typealias ClientProtocol = namespaceA_ServiceAClientProtocol + typealias Client = namespaceA_ServiceAClient } } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true ) } @@ -186,13 +306,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + typealias ClientProtocol = namespaceA_ServiceAClientProtocol + typealias Client = namespaceA_ServiceAClient } } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true ) } @@ -219,19 +343,25 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = namespacea_AServiceServiceStreamingProtocol typealias ServiceProtocol = namespacea_AServiceServiceProtocol + typealias ClientProtocol = namespacea_AServiceClientProtocol + typealias Client = namespacea_AServiceClient } enum BService { enum Methods {} static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = namespacea_BServiceServiceStreamingProtocol typealias ServiceProtocol = namespacea_BServiceServiceProtocol + typealias ClientProtocol = namespacea_BServiceClientProtocol + typealias Client = namespacea_BServiceClient } } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true ) } @@ -257,18 +387,24 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = AServiceServiceStreamingProtocol typealias ServiceProtocol = AServiceServiceProtocol + typealias ClientProtocol = AServiceClientProtocol + typealias Client = AServiceClient } enum BService { enum Methods {} static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol typealias ServiceProtocol = BServiceServiceProtocol + typealias ClientProtocol = BServiceClientProtocol + typealias Client = BServiceClient } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true ) } @@ -295,6 +431,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol typealias ServiceProtocol = anamespace_AServiceServiceProtocol + typealias ClientProtocol = anamespace_AServiceClientProtocol + typealias Client = anamespace_AServiceClient } } enum bnamespace { @@ -303,13 +441,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = bnamespace_BServiceServiceStreamingProtocol typealias ServiceProtocol = bnamespace_BServiceServiceProtocol + typealias ClientProtocol = bnamespace_BServiceClientProtocol + typealias Client = bnamespace_BServiceClient } } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true ) } @@ -333,6 +475,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol typealias ServiceProtocol = BServiceServiceProtocol + typealias ClientProtocol = BServiceClientProtocol + typealias Client = BServiceClient } enum anamespace { enum AService { @@ -340,13 +484,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { static let methods: [MethodDescriptor] = [] typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol typealias ServiceProtocol = anamespace_AServiceServiceProtocol + typealias ClientProtocol = anamespace_AServiceClientProtocol + typealias Client = anamespace_AServiceClient } } """ try self.assertTypealiasTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + client: true, + server: true ) } @@ -359,7 +507,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) - let translator = TypealiasTranslator() + let translator = TypealiasTranslator(client: true, server: true) XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) @@ -387,7 +535,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) - let translator = TypealiasTranslator() + let translator = TypealiasTranslator(client: true, server: true) XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) @@ -423,7 +571,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let codeGenerationRequest = makeCodeGenerationRequest(services: [service]) - let translator = TypealiasTranslator() + let translator = TypealiasTranslator(client: true, server: true) XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) @@ -456,7 +604,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { methods: [] ) let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB]) - let translator = TypealiasTranslator() + let translator = TypealiasTranslator(client: true, server: true) XCTAssertThrowsError( ofType: CodeGenError.self, try translator.translate(from: codeGenerationRequest) @@ -479,9 +627,11 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { extension TypealiasTranslatorSnippetBasedTests { private func assertTypealiasTranslation( codeGenerationRequest: CodeGenerationRequest, - expectedSwift: String + expectedSwift: String, + client: Bool, + server: Bool ) throws { - let translator = TypealiasTranslator() + let translator = TypealiasTranslator(client: client, server: server) let codeBlocks = try translator.translate(from: codeGenerationRequest) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) From 279d55a437b1b1bcb00229e7c1f3535c28bf0e9c Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 4 Jan 2024 16:51:22 +0000 Subject: [PATCH 198/580] Add missing compiler directive to ServerCodeTranslatorSnippetBasedTests.swift (#1751) --- .../Translator/ServerCodeTranslatorSnippetBasedTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index bb12826c9..89df4bc52 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) + import XCTest @testable import GRPCCodeGen @@ -429,3 +431,5 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { try XCTAssertEqualWithDiff(contents, expectedSwift) } } + +#endif // os(macOS) || os(Linux) From ced0d70dd3ccf70fa8820aaff085ad998f379a24 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:11:48 +0000 Subject: [PATCH 199/580] [CodeGenLib] Add validation step for input (#1753) Add validation step for input in IDLToStructuredSwiftTranslator Motivation: The IDLToStructuredSwiftTranslator should discover errors in the CodeGenerationRequest object, before the specialized translators start their work. This way, if the input is not correct no translator will be started. Modifications: Moved the validation functions into a IDLToStructuredSwiftTranslator extension and the associated tests in a separate test file. Result: The input validation is not done by each individual specialzed translator, but by the main translator, before transformations take place. --- .../IDLToStructuredSwiftTranslator.swift | 83 +++++++++ .../Translator/TypealiasTranslator.swift | 72 -------- ...uredSwiftTranslatorSnippetBasedTests.swift | 169 ++++++++++++++++++ ...TypealiasTranslatorSnippetBasedTests.swift | 125 ------------- 4 files changed, 252 insertions(+), 197 deletions(-) create mode 100644 Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index c90bbffbf..85fadaeda 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -22,6 +22,7 @@ struct IDLToStructuredSwiftTranslator: Translator { client: Bool, server: Bool ) throws -> StructuredSwiftRepresentation { + try self.validateInput(codeGenerationRequest) let typealiasTranslator = TypealiasTranslator(client: client, server: server) let topComment = Comment.doc(codeGenerationRequest.leadingTrivia) let imports: [ImportDescription] = [ @@ -49,6 +50,88 @@ struct IDLToStructuredSwiftTranslator: Translator { } } +extension IDLToStructuredSwiftTranslator { + private func validateInput(_ codeGenerationRequest: CodeGenerationRequest) throws { + let servicesByNamespace = Dictionary( + grouping: codeGenerationRequest.services, + by: { $0.namespace } + ) + try self.checkServiceNamesAreUnique(for: servicesByNamespace) + for service in codeGenerationRequest.services { + try self.checkMethodNamesAreUnique(in: service) + } + } + + // Verify service names are unique within each namespace and that services with no namespace + // don't have the same names as any of the namespaces. + private func checkServiceNamesAreUnique( + for servicesByNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]] + ) throws { + // Check that if there are services in an empty namespace, none have names which match other namespaces + if let noNamespaceServices = servicesByNamespace[""] { + let namespaces = servicesByNamespace.keys + for service in noNamespaceServices { + if namespaces.contains(service.name) { + throw CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services with no namespace must not have the same names as the namespaces. \ + \(service.name) is used as a name for a service with no namespace and a namespace. + """ + ) + } + } + } + + // Check that service names are unique within each namespace. + for (namespace, services) in servicesByNamespace { + var serviceNames: Set = [] + for service in services { + if serviceNames.contains(service.name) { + let errorMessage: String + if namespace.isEmpty { + errorMessage = """ + Services in an empty namespace must have unique names. \ + \(service.name) is used as a name for multiple services without namespaces. + """ + } else { + errorMessage = """ + Services within the same namespace must have unique names. \ + \(service.name) is used as a name for multiple services in the \(service.namespace) namespace. + """ + } + throw CodeGenError( + code: .nonUniqueServiceName, + message: errorMessage + ) + } + serviceNames.insert(service.name) + } + } + } + + // Verify method names are unique for the service. + private func checkMethodNamesAreUnique( + in service: CodeGenerationRequest.ServiceDescriptor + ) throws { + let methodNames = service.methods.map { $0.name } + var seenNames = Set() + + for methodName in methodNames { + if seenNames.contains(methodName) { + throw CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique names. \ + \(methodName) is used as a name for multiple methods of the \(service.name) service. + """ + ) + } + seenNames.insert(methodName) + } + } +} + extension CodeGenerationRequest.ServiceDescriptor { var namespacedTypealiasPrefix: String { if self.namespace.isEmpty { diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index a502675df..56c43942e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -66,10 +66,6 @@ struct TypealiasTranslator: SpecializedTranslator { let services = codeGenerationRequest.services let servicesByNamespace = Dictionary(grouping: services, by: { $0.namespace }) - // Verify service names are unique within each namespace and that services with no namespace - // don't have the same names as any of the namespaces. - try self.checkServiceNamesAreUnique(for: servicesByNamespace) - // Sorting the keys and the services in each list of the dictionary is necessary // so that the generated enums are deterministically ordered. for (namespace, services) in servicesByNamespace.sorted(by: { $0.key < $1.key }) { @@ -85,51 +81,6 @@ struct TypealiasTranslator: SpecializedTranslator { } extension TypealiasTranslator { - private func checkServiceNamesAreUnique( - for servicesByNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]] - ) throws { - // Check that if there are services in an empty namespace, none have names which match other namespaces - let noNamespaceServices = servicesByNamespace["", default: []] - let namespaces = servicesByNamespace.keys - for service in noNamespaceServices { - if namespaces.contains(service.name) { - throw CodeGenError( - code: .nonUniqueServiceName, - message: """ - Services with no namespace must not have the same names as the namespaces. \ - \(service.name) is used as a name for a service with no namespace and a namespace. - """ - ) - } - } - - // Check that service names are unique within each namespace. - for (namespace, services) in servicesByNamespace { - var serviceNames: Set = [] - for service in services { - if serviceNames.contains(service.name) { - let errorMessage: String - if namespace.isEmpty { - errorMessage = """ - Services in an empty namespace must have unique names. \ - \(service.name) is used as a name for multiple services without namespaces. - """ - } else { - errorMessage = """ - Services within the same namespace must have unique names. \ - \(service.name) is used as a name for multiple services in the \(service.namespace) namespace. - """ - } - throw CodeGenError( - code: .nonUniqueServiceName, - message: errorMessage - ) - } - serviceNames.insert(service.name) - } - } - } - private func makeNamespaceEnum( for namespace: String, containing services: [CodeGenerationRequest.ServiceDescriptor] @@ -163,9 +114,6 @@ extension TypealiasTranslator { var methodsEnum = EnumDescription(name: "Methods") let methods = service.methods - // Verify method names are unique for the service. - try self.checkMethodNamesAreUnique(in: service) - // Create the method specific enums. for method in methods { let methodEnum = self.makeMethodEnum(from: method, in: service) @@ -196,26 +144,6 @@ extension TypealiasTranslator { return .enum(serviceEnum) } - private func checkMethodNamesAreUnique( - in service: CodeGenerationRequest.ServiceDescriptor - ) throws { - let methodNames = service.methods.map { $0.name } - var seenNames = Set() - - for methodName in methodNames { - if seenNames.contains(methodName) { - throw CodeGenError( - code: .nonUniqueMethodName, - message: """ - Methods of a service must have unique names. \ - \(methodName) is used as a name for multiple methods of the \(service.name) service. - """ - ) - } - seenNames.insert(methodName) - } - } - private func makeMethodEnum( from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift new file mode 100644 index 000000000..fca3f3ee2 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -0,0 +1,169 @@ +/* + * 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. + */ + +#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) + +import XCTest + +@testable import GRPCCodeGen + +final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { + typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + + func testSameNameServicesNoNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "", + methods: [] + ) + + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services in an empty namespace must have unique names. \ + AService is used as a name for multiple services without namespaces. + """ + ) + ) + } + } + + func testSameNameServicesSameNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "namespacea", + methods: [] + ) + + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services within the same namespace must have unique names. \ + AService is used as a name for multiple services in the namespacea namespace. + """ + ) + ) + } + } + + func testSameNameMethodsSameServiceError() throws { + let methodA = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "MethodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for AService", + name: "AService", + namespace: "namespacea", + methods: [methodA, methodA] + ) + + let codeGenerationRequest = makeCodeGenerationRequest(services: [service]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique names. \ + MethodA is used as a name for multiple methods of the AService service. + """ + ) + ) + } + } + + func testSameNameNoNamespaceServiceAndNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for SameName service with no namespace", + name: "SameName", + namespace: "", + methods: [] + ) + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: "BService", + namespace: "SameName", + methods: [] + ) + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services with no namespace must not have the same names as the namespaces. \ + SameName is used as a name for a service with no namespace and a namespace. + """ + ) + ) + } + } +} + +#endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index b677217cd..7bca92243 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -497,131 +497,6 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { server: true ) } - - func testTypealiasTranslatorSameNameServicesNoNamespaceError() throws { - let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", - name: "AService", - namespace: "", - methods: [] - ) - - let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) - let translator = TypealiasTranslator(client: true, server: true) - XCTAssertThrowsError( - ofType: CodeGenError.self, - try translator.translate(from: codeGenerationRequest) - ) { - error in - XCTAssertEqual( - error as CodeGenError, - CodeGenError( - code: .nonUniqueServiceName, - message: """ - Services in an empty namespace must have unique names. \ - AService is used as a name for multiple services without namespaces. - """ - ) - ) - } - } - - func testTypealiasTranslatorSameNameServicesSameNamespaceError() throws { - let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", - name: "AService", - namespace: "namespacea", - methods: [] - ) - - let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceA]) - let translator = TypealiasTranslator(client: true, server: true) - XCTAssertThrowsError( - ofType: CodeGenError.self, - try translator.translate(from: codeGenerationRequest) - ) { - error in - XCTAssertEqual( - error as CodeGenError, - CodeGenError( - code: .nonUniqueServiceName, - message: """ - Services within the same namespace must have unique names. \ - AService is used as a name for multiple services in the namespacea namespace. - """ - ) - ) - } - } - - func testTypealiasTranslatorSameNameMethodsSameServiceError() throws { - let methodA = MethodDescriptor( - documentation: "Documentation for MethodA", - name: "MethodA", - isInputStreaming: false, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "Documentation for AService", - name: "AService", - namespace: "namespacea", - methods: [methodA, methodA] - ) - - let codeGenerationRequest = makeCodeGenerationRequest(services: [service]) - let translator = TypealiasTranslator(client: true, server: true) - XCTAssertThrowsError( - ofType: CodeGenError.self, - try translator.translate(from: codeGenerationRequest) - ) { - error in - XCTAssertEqual( - error as CodeGenError, - CodeGenError( - code: .nonUniqueMethodName, - message: """ - Methods of a service must have unique names. \ - MethodA is used as a name for multiple methods of the AService service. - """ - ) - ) - } - } - - func testTypealiasTranslatorSameNameNoNamespaceServiceAndNamespaceError() throws { - let serviceA = ServiceDescriptor( - documentation: "Documentation for SameName service with no namespace", - name: "SameName", - namespace: "", - methods: [] - ) - let serviceB = ServiceDescriptor( - documentation: "Documentation for BService", - name: "BService", - namespace: "SameName", - methods: [] - ) - let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB]) - let translator = TypealiasTranslator(client: true, server: true) - XCTAssertThrowsError( - ofType: CodeGenError.self, - try translator.translate(from: codeGenerationRequest) - ) { - error in - XCTAssertEqual( - error as CodeGenError, - CodeGenError( - code: .nonUniqueServiceName, - message: """ - Services with no namespace must not have the same names as the namespaces. \ - SameName is used as a name for a service with no namespace and a namespace. - """ - ) - ) - } - } } extension TypealiasTranslatorSnippetBasedTests { From 53855b12ad2eaefd58469f17a78216ced3ad10a4 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:22:41 +0000 Subject: [PATCH 200/580] [CodeGenLib] Translator for Client Code (#1748) Motivation: In the generated client code for a service described in a Source IDL file we want to have: - a protocol declaring the methods for a client, with the serializer and deserializer as parameters. - an extension for the protocol defining the methods of the client, without the serializer and deserializer as parameters, that are calling the already declared methods, providing the default Protobuf serializer and deserializer. - a struct representing the generated client that has a GRPCClient as a property and conforms to the Client Protocol. Modifications: - Implemented the ClientCodeTranslator struct that contains all the methods for creating the `StructuredSwiftRepresentation` for the client code. - Added a new struct in the `StructuredSwiftRepresentation` that represents closure signatures, and a new `ParameterDescription` property that is set only when the parameter is a closure signature. - Added and modified render functions for the `StructuredSwiftRepresentation` additions. - Implemented snippet tests. Result: Client code can now be generated for a CodeGenerationRequest object. --- .../Internal/Renderer/TextBasedRenderer.swift | 198 ++++++- .../StructuredSwiftRepresentation.swift | 63 +- .../Translator/ClientCodeTranslator.swift | 432 ++++++++++++++ .../IDLToStructuredSwiftTranslator.swift | 13 +- .../Renderer/TextBasedRendererTests.swift | 35 +- ...lientCodeTranslatorSnippetBasedTests.swift | 542 ++++++++++++++++++ 6 files changed, 1251 insertions(+), 32 deletions(-) create mode 100644 Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 85a2cc264..54e642545 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -202,11 +202,11 @@ struct TextBasedRenderer: RendererProtocol { } /// Renders the specified identifier. - func renderedIdentifier(_ identifier: IdentifierDescription) -> String { + func renderIdentifier(_ identifier: IdentifierDescription) { switch identifier { - case .pattern(let string): return string + case .pattern(let string): writer.writeLine(string) case .type(let existingTypeDescription): - return renderedExistingTypeDescription(existingTypeDescription) + renderExistingTypeDescription(existingTypeDescription) } } @@ -446,7 +446,7 @@ struct TextBasedRenderer: RendererProtocol { switch expression { case .literal(let literalDescription): renderLiteral(literalDescription) case .identifier(let identifierDescription): - writer.writeLine(renderedIdentifier(identifierDescription)) + renderIdentifier(identifierDescription) case .memberAccess(let memberAccessDescription): renderMemberAccess(memberAccessDescription) case .functionCall(let functionCallDescription): renderFunctionCall(functionCallDescription) case .assignment(let assignment): renderAssignment(assignment) @@ -534,20 +534,44 @@ struct TextBasedRenderer: RendererProtocol { } /// Renders the specified type reference to an existing type. - func renderedExistingTypeDescription(_ type: ExistingTypeDescription) -> String { + func renderExistingTypeDescription(_ type: ExistingTypeDescription) { switch type { case .any(let existingTypeDescription): - return "any \(renderedExistingTypeDescription(existingTypeDescription))" + writer.writeLine("any ") + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(existingTypeDescription) case .generic(let wrapper, let wrapped): - return - "\(renderedExistingTypeDescription(wrapper))<\(renderedExistingTypeDescription(wrapped))>" + renderExistingTypeDescription(wrapper) + writer.nextLineAppendsToLastLine() + writer.writeLine("<") + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(wrapped) + writer.nextLineAppendsToLastLine() + writer.writeLine(">") case .optional(let existingTypeDescription): - return "\(renderedExistingTypeDescription(existingTypeDescription))?" - case .member(let components): return components.joined(separator: ".") + renderExistingTypeDescription(existingTypeDescription) + writer.nextLineAppendsToLastLine() + writer.writeLine("?") + case .member(let components): + writer.writeLine(components.joined(separator: ".")) case .array(let existingTypeDescription): - return "[\(renderedExistingTypeDescription(existingTypeDescription))]" + writer.writeLine("[") + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(existingTypeDescription) + writer.nextLineAppendsToLastLine() + writer.writeLine("]") case .dictionaryValue(let existingTypeDescription): - return "[String: \(renderedExistingTypeDescription(existingTypeDescription))]" + writer.writeLine("[String: ") + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(existingTypeDescription) + writer.nextLineAppendsToLastLine() + writer.writeLine("]") + case .some(let existingTypeDescription): + writer.writeLine("some ") + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(existingTypeDescription) + case .closure(let closureSignatureDescription): + renderClosureSignature(closureSignatureDescription) } } @@ -558,9 +582,11 @@ struct TextBasedRenderer: RendererProtocol { words.append(renderedAccessModifier(accessModifier)) } words.append(contentsOf: [ - "typealias", alias.name, "=", renderedExistingTypeDescription(alias.existingType), + "typealias", alias.name, "=", ]) - writer.writeLine(words.joinedWords()) + writer.writeLine(words.joinedWords() + " ") + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(alias.existingType) } /// Renders the specified binding kind. @@ -587,7 +613,9 @@ struct TextBasedRenderer: RendererProtocol { renderExpression(variable.left) if let type = variable.type { writer.nextLineAppendsToLastLine() - writer.writeLine(": \(renderedExistingTypeDescription(type))") + writer.writeLine(": ") + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(type) } } @@ -703,11 +731,12 @@ struct TextBasedRenderer: RendererProtocol { } /// Renders the specified enum case associated value. - func renderedEnumCaseAssociatedValue(_ value: EnumCaseAssociatedValueDescription) -> String { + func renderEnumCaseAssociatedValue(_ value: EnumCaseAssociatedValueDescription) { var words: [String] = [] if let label = value.label { words.append(label + ":") } - words.append(renderedExistingTypeDescription(value.type)) - return words.joinedWords() + writer.writeLine(words.joinedWords()) + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(value.type) } /// Renders the specified enum case declaration. @@ -722,9 +751,13 @@ struct TextBasedRenderer: RendererProtocol { renderLiteral(rawValue) case .nameWithAssociatedValues(let values): if values.isEmpty { break } - let associatedValues = values.map(renderedEnumCaseAssociatedValue).joined(separator: ", ") - writer.nextLineAppendsToLastLine() - writer.writeLine("(\(associatedValues))") + for (value, isLast) in values.enumeratedWithLastMarker() { + renderEnumCaseAssociatedValue(value) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(", ") + } + } } } @@ -750,7 +783,9 @@ struct TextBasedRenderer: RendererProtocol { func renderedFunctionKind(_ functionKind: FunctionKind) -> String { switch functionKind { case .initializer(let isFailable): return "init\(isFailable ? "?" : "")" - case .function(let name, let isStatic): return (isStatic ? "static " : "") + "func \(name)" + case .function(let name, let isStatic): + return (isStatic ? "static " : "") + "func \(name)" + } } @@ -759,6 +794,54 @@ struct TextBasedRenderer: RendererProtocol { switch keyword { case .throws: return "throws" case .async: return "async" + case .rethrows: return "rethrows" + } + } + + /// Renders the specified function signature. + func renderClosureSignature(_ signature: ClosureSignatureDescription) { + if signature.sendable { + writer.writeLine("@Sendable ") + writer.nextLineAppendsToLastLine() + } + if signature.escaping { + writer.writeLine("@escaping ") + writer.nextLineAppendsToLastLine() + } + + writer.writeLine("(") + let parameters = signature.parameters + let separateLines = parameters.count > 1 + if separateLines { + writer.withNestedLevel { + for (parameter, isLast) in signature.parameters.enumeratedWithLastMarker() { + renderClosureParameter(parameter) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(",") + } + } + } + } else { + writer.nextLineAppendsToLastLine() + if let parameter = parameters.first { + renderClosureParameter(parameter) + writer.nextLineAppendsToLastLine() + } + } + writer.writeLine(")") + + let keywords = signature.keywords + for keyword in keywords { + writer.nextLineAppendsToLastLine() + writer.writeLine(" " + renderedFunctionKeyword(keyword)) + } + + if let returnType = signature.returnType { + writer.nextLineAppendsToLastLine() + writer.writeLine(" -> ") + writer.nextLineAppendsToLastLine() + renderExpression(returnType) } } @@ -769,7 +852,26 @@ struct TextBasedRenderer: RendererProtocol { writer.writeLine(renderedAccessModifier(accessModifier) + " ") writer.nextLineAppendsToLastLine() } - writer.writeLine(renderedFunctionKind(signature.kind) + "(") + let generics = signature.generics + writer.writeLine( + renderedFunctionKind(signature.kind) + ) + if !generics.isEmpty { + writer.nextLineAppendsToLastLine() + writer.writeLine("<") + for (genericType, isLast) in generics.enumeratedWithLastMarker() { + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(genericType) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(", ") + } + } + writer.nextLineAppendsToLastLine() + writer.writeLine(">") + } + writer.nextLineAppendsToLastLine() + writer.writeLine("(") let parameters = signature.parameters let separateLines = parameters.count > 1 if separateLines { @@ -806,6 +908,11 @@ struct TextBasedRenderer: RendererProtocol { writer.nextLineAppendsToLastLine() renderExpression(returnType) } + + if let whereClause = signature.whereClause { + writer.nextLineAppendsToLastLine() + writer.writeLine(" " + renderedWhereClause(whereClause)) + } } /// Renders the specified function declaration. @@ -839,11 +946,54 @@ struct TextBasedRenderer: RendererProtocol { } writer.writeLine(": ") writer.nextLineAppendsToLastLine() + + if parameterDescription.inout { + writer.writeLine("inout ") + writer.nextLineAppendsToLastLine() + } + + if let type = parameterDescription.type { + renderExistingTypeDescription(type) + } + + if let defaultValue = parameterDescription.defaultValue { + writer.nextLineAppendsToLastLine() + writer.writeLine(" = ") + writer.nextLineAppendsToLastLine() + renderExpression(defaultValue) + } + } + + /// Renders the specified parameter declaration for a closure. + func renderClosureParameter(_ parameterDescription: ParameterDescription) { + let name = parameterDescription.name + let label: String + if let declaredLabel = parameterDescription.label { + label = declaredLabel + } else { + label = "_" + } + + if let name = name { + writer.writeLine(label) + if name != parameterDescription.label { + // If the label and name are the same value, don't repeat it. + writer.writeLine(" ") + writer.nextLineAppendsToLastLine() + writer.writeLine(name) + writer.nextLineAppendsToLastLine() + } + } + if parameterDescription.inout { writer.writeLine("inout ") writer.nextLineAppendsToLastLine() } - writer.writeLine(renderedExistingTypeDescription(parameterDescription.type)) + + if let type = parameterDescription.type { + renderExistingTypeDescription(type) + } + if let defaultValue = parameterDescription.defaultValue { writer.nextLineAppendsToLastLine() writer.writeLine(" = ") diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index fc7ce726e..556c3d909 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -420,6 +420,16 @@ indirect enum ExistingTypeDescription: Equatable, Codable { /// /// For example, `[String: Foo]`. case dictionaryValue(ExistingTypeDescription) + + /// A type with the `some` keyword in front of it. + /// + /// For example, `some Foo`. + case some(ExistingTypeDescription) + + /// A closure signature as a type. + /// + /// For example: `(String) async throws -> Int`. + case closure(ClosureSignatureDescription) } /// A description of a typealias declaration. @@ -483,7 +493,7 @@ struct ParameterDescription: Equatable, Codable { /// The type name of the parameter. /// /// For example, in `bar baz: String = "hi"`, `type` is `String`. - var type: ExistingTypeDescription + var type: ExistingTypeDescription? = nil /// A default value of the parameter. /// @@ -508,7 +518,10 @@ enum FunctionKind: Equatable, Codable { /// A function or a method. Can be static. /// /// For example `foo()`, where `name` is `foo`. - case function(name: String, isStatic: Bool) + case function( + name: String, + isStatic: Bool + ) } /// A function keyword, such as `async` and `throws`. @@ -519,6 +532,9 @@ enum FunctionKeyword: Equatable, Codable { /// A function that can throw an error. case `throws` + + /// A function that can rethrow an error. + case `rethrows` } /// A description of a function signature. @@ -532,6 +548,9 @@ struct FunctionSignatureDescription: Equatable, Codable { /// The kind of the function. var kind: FunctionKind + /// The generic types of the function. + var generics: [ExistingTypeDescription] = [] + /// The parameters of the function. var parameters: [ParameterDescription] = [] @@ -540,6 +559,9 @@ struct FunctionSignatureDescription: Equatable, Codable { /// The return type name of the function, such as `Int`. var returnType: Expression? = nil + + /// The where clause for a generic function. + var whereClause: WhereClause? } /// A description of a function definition. @@ -575,17 +597,21 @@ struct FunctionDescription: Equatable, Codable { init( accessModifier: AccessModifier? = nil, kind: FunctionKind, + generics: [ExistingTypeDescription] = [], parameters: [ParameterDescription] = [], keywords: [FunctionKeyword] = [], returnType: Expression? = nil, + whereClause: WhereClause? = nil, body: [CodeBlock]? = nil ) { self.signature = .init( accessModifier: accessModifier, kind: kind, + generics: generics, parameters: parameters, keywords: keywords, - returnType: returnType + returnType: returnType, + whereClause: whereClause ) self.body = body } @@ -601,22 +627,45 @@ struct FunctionDescription: Equatable, Codable { init( accessModifier: AccessModifier? = nil, kind: FunctionKind, + generics: [ExistingTypeDescription] = [], parameters: [ParameterDescription] = [], keywords: [FunctionKeyword] = [], returnType: Expression? = nil, + whereClause: WhereClause? = nil, body: [Expression] ) { self.init( accessModifier: accessModifier, kind: kind, + generics: generics, parameters: parameters, keywords: keywords, returnType: returnType, + whereClause: whereClause, body: body.map { .expression($0) } ) } } +/// A description of a closure signature. +/// +/// For example: `(String) async throws -> Int`. +struct ClosureSignatureDescription: Equatable, Codable { + /// The parameters of the function. + var parameters: [ParameterDescription] = [] + + /// The keywords of the function, such as `async` and `throws.` + var keywords: [FunctionKeyword] = [] + + /// The return type name of the function, such as `Int`. + var returnType: Expression? = nil + + /// The ``@Sendable`` attribute. + var sendable: Bool = false + + /// The ``@escaping`` attribute. + var escaping: Bool = false +} /// A description of the associated value of an enum case. /// /// For example, in `case foo(bar: String)`, the associated value @@ -1235,18 +1284,22 @@ extension Declaration { static func function( accessModifier: AccessModifier? = nil, kind: FunctionKind, + generics: [ExistingTypeDescription] = [], parameters: [ParameterDescription], keywords: [FunctionKeyword] = [], returnType: Expression? = nil, + whereClause: WhereClause?, body: [CodeBlock]? = nil ) -> Self { .function( .init( accessModifier: accessModifier, kind: kind, + generics: generics, parameters: parameters, keywords: keywords, returnType: returnType, + whereClause: whereClause, body: body ) ) @@ -1327,7 +1380,9 @@ extension FunctionKind { static var initializer: Self { .initializer(failable: false) } /// Returns a non-static function kind. - static func function(name: String) -> Self { .function(name: name, isStatic: false) } + static func function(name: String) -> Self { + .function(name: name, isStatic: false) + } } extension CodeBlock { diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift new file mode 100644 index 000000000..385f513f7 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -0,0 +1,432 @@ +/* + * Copyright 2023, 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. + */ + +/// Creates a representation for the client code that will be generated based on the ``CodeGenerationRequest`` object +/// specifications, using types from ``StructuredSwiftRepresentation``. +/// +/// For example, in the case of a service called "Bar", in the "foo" namespace which has +/// one method "baz", the ``ClientCodeTranslator`` will create +/// a representation for the following generated code: +/// +/// ```swift +/// public protocol foo_BarClientProtocol: Sendable { +/// func baz( +/// request: ClientRequest.Single, +/// serializer: some MessageSerializer, +/// deserializer: some MessageDeserializer, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async throws -> ServerResponse.Stream +/// } +/// extension foo.Bar.ClientProtocol { +/// public func get( +/// request: ClientRequest.Single, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async rethrows -> R { +/// try await self.baz( +/// request: request, +/// serializer: ProtobufSerializer(), +/// deserializer: ProtobufDeserializer(), +/// body +/// ) +/// } +/// struct foo_BarClient: foo.Bar.ClientProtocol { +/// let client: GRPCCore.GRPCClient +/// init(client: GRPCCore.GRPCClient) { +/// self.client = client +/// } +/// func methodA( +/// request: ClientRequest.Stream, +/// serializer: some MessageSerializer, +/// deserializer: some MessageDeserializer, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async rethrows -> R { +/// try await self.client.clientStreaming( +/// request: request, +/// descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, +/// serializer: serializer, +/// deserializer: deserializer, +/// handler: body +/// ) +/// } +/// } +///``` +struct ClientCodeTranslator: SpecializedTranslator { + func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { + var codeBlocks = [CodeBlock]() + + for service in codeGenerationRequest.services { + codeBlocks.append( + .declaration( + .commentable( + .doc(service.documentation), + self.makeClientProtocol(for: service, in: codeGenerationRequest) + ) + ) + ) + codeBlocks.append( + .declaration(self.makeExtensionProtocol(for: service, in: codeGenerationRequest)) + ) + codeBlocks.append( + .declaration( + .commentable( + .doc(service.documentation), + self.makeClientStruct(for: service, in: codeGenerationRequest) + ) + ) + ) + } + return codeBlocks + } +} + +extension ClientCodeTranslator { + private func makeClientProtocol( + for service: CodeGenerationRequest.ServiceDescriptor, + in codeGenerationRequest: CodeGenerationRequest + ) -> Declaration { + let methods = service.methods.map { + self.makeClientProtocolMethod( + for: $0, + in: service, + from: codeGenerationRequest, + generateSerializerDeserializer: false + ) + } + + let clientProtocol = Declaration.protocol( + ProtocolDescription( + name: "\(service.namespacedPrefix)ClientProtocol", + conformances: ["Sendable"], + members: methods + ) + ) + return clientProtocol + } + + private func makeExtensionProtocol( + for service: CodeGenerationRequest.ServiceDescriptor, + in codeGenerationRequest: CodeGenerationRequest + ) -> Declaration { + let methods = service.methods.map { + self.makeClientProtocolMethod( + for: $0, + in: service, + from: codeGenerationRequest, + generateSerializerDeserializer: true + ) + } + let clientProtocolExtension = Declaration.extension( + ExtensionDescription( + onType: "\(service.namespacedTypealiasPrefix).ClientProtocol", + declarations: methods + ) + ) + return clientProtocolExtension + } + + private func makeClientProtocolMethod( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor, + from codeGenerationRequest: CodeGenerationRequest, + generateSerializerDeserializer: Bool + ) -> Declaration { + let methodParameters = self.makeParameters( + for: method, + in: service, + from: codeGenerationRequest, + generateSerializerDeserializer: generateSerializerDeserializer + ) + let functionSignature = FunctionSignatureDescription( + kind: .function( + name: method.name, + isStatic: false + ), + generics: [.member("R")], + parameters: methodParameters, + keywords: [.async, .throws], + returnType: .identifierType(.member("R")), + whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]) + ) + + if generateSerializerDeserializer { + let body = self.makeSerializerDeserializerCall( + for: method, + in: service, + from: codeGenerationRequest + ) + return .function(signature: functionSignature, body: body) + } else { + return .commentable(.doc(method.documentation), .function(signature: functionSignature)) + } + } + + private func makeSerializerDeserializerCall( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor, + from codeGenerationRequest: CodeGenerationRequest + ) -> [CodeBlock] { + let functionCall = Expression.functionCall( + calledExpression: .memberAccess( + MemberAccessDescription(left: .identifierPattern("self"), right: method.name) + ), + arguments: [ + FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), + FunctionArgumentDescription( + label: "serializer", + expression: .identifierPattern( + codeGenerationRequest.lookupSerializer( + self.methodInputOutputTypealias(for: method, service: service, type: .input) + ) + ) + ), + FunctionArgumentDescription( + label: "deserializer", + expression: .identifierPattern( + codeGenerationRequest.lookupDeserializer( + self.methodInputOutputTypealias(for: method, service: service, type: .output) + ) + ) + ), + FunctionArgumentDescription(expression: .identifierPattern("body")), + ] + ) + let awaitFunctionCall = Expression.unaryKeyword(kind: .await, expression: functionCall) + let tryAwaitFunctionCall = Expression.unaryKeyword(kind: .try, expression: awaitFunctionCall) + + return [CodeBlock(item: .expression(tryAwaitFunctionCall))] + } + + private func makeParameters( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor, + from codeGenerationRequest: CodeGenerationRequest, + generateSerializerDeserializer: Bool + ) -> [ParameterDescription] { + var parameters = [ParameterDescription]() + + parameters.append(self.clientRequestParameter(for: method, in: service)) + if !generateSerializerDeserializer { + parameters.append(self.serializerParameter(for: method, in: service)) + parameters.append(self.deserializerParameter(for: method, in: service)) + } + parameters.append(self.bodyParameter(for: method, in: service)) + return parameters + } + private func clientRequestParameter( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> ParameterDescription { + let requestType = method.isInputStreaming ? "Stream" : "Single" + let clientRequestType = ExistingTypeDescription.member(["ClientRequest", requestType]) + return ParameterDescription( + label: "request", + type: .generic( + wrapper: clientRequestType, + wrapped: .member( + self.methodInputOutputTypealias(for: method, service: service, type: .input) + ) + ) + ) + } + + private func serializerParameter( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> ParameterDescription { + return ParameterDescription( + label: "serializer", + type: ExistingTypeDescription.some( + .generic( + wrapper: .member("MessageSerializer"), + wrapped: .member( + self.methodInputOutputTypealias(for: method, service: service, type: .input) + ) + ) + ) + ) + } + + private func deserializerParameter( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> ParameterDescription { + return ParameterDescription( + label: "deserializer", + type: ExistingTypeDescription.some( + .generic( + wrapper: .member("MessageDeserializer"), + wrapped: .member( + self.methodInputOutputTypealias(for: method, service: service, type: .output) + ) + ) + ) + ) + } + + private func bodyParameter( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor + ) -> ParameterDescription { + let clientStreaming = method.isOutputStreaming ? "Stream" : "Single" + let closureParameterType = ExistingTypeDescription.generic( + wrapper: .member(["ClientResponse", clientStreaming]), + wrapped: .member( + self.methodInputOutputTypealias(for: method, service: service, type: .output) + ) + ) + + let bodyClosure = ClosureSignatureDescription( + parameters: [.init(type: closureParameterType)], + keywords: [.async, .throws], + returnType: .identifierType(.member("R")), + sendable: true, + escaping: true + ) + return ParameterDescription(name: "body", type: .closure(bodyClosure)) + } + + private func makeClientStruct( + for service: CodeGenerationRequest.ServiceDescriptor, + in codeGenerationRequest: CodeGenerationRequest + ) -> Declaration { + let clientProperty = Declaration.variable( + kind: .let, + left: "client", + type: .member(["GRPCCore", "GRPCClient"]) + ) + let initializer = self.makeClientVariable() + let methods = service.methods.map { + Declaration.commentable( + .doc($0.documentation), + self.makeClientMethod(for: $0, in: service, from: codeGenerationRequest) + ) + } + + return .struct( + StructDescription( + name: "\(service.namespacedPrefix)Client", + conformances: ["\(service.namespacedTypealiasPrefix).ClientProtocol"], + members: [clientProperty, initializer] + methods + ) + ) + } + + private func makeClientVariable() -> Declaration { + let initializerBody = Expression.assignment( + left: .memberAccess( + MemberAccessDescription(left: .identifierPattern("self"), right: "client") + ), + right: .identifierPattern("client") + ) + return .function( + signature: .init( + kind: .initializer, + parameters: [.init(label: "client", type: .member(["GRPCCore", "GRPCClient"]))] + ), + body: [CodeBlock(item: .expression(initializerBody))] + ) + } + + private func clientMethod( + isInputStreaming: Bool, + isOutputStreaming: Bool + ) -> String { + switch (isInputStreaming, isOutputStreaming) { + case (true, true): + return "bidirectionalStreaming" + case (true, false): + return "clientStreaming" + case (false, true): + return "serverStreaming" + case (false, false): + return "unary" + } + } + + private func makeClientMethod( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + in service: CodeGenerationRequest.ServiceDescriptor, + from codeGenerationRequest: CodeGenerationRequest + ) -> Declaration { + let parameters = self.makeParameters( + for: method, + in: service, + from: codeGenerationRequest, + generateSerializerDeserializer: false + ) + let grpcMethodName = self.clientMethod( + isInputStreaming: method.isInputStreaming, + isOutputStreaming: method.isOutputStreaming + ) + let functionCall = Expression.functionCall( + calledExpression: .memberAccess( + MemberAccessDescription(left: .identifierPattern("self.client"), right: "\(grpcMethodName)") + ), + arguments: [ + .init(label: "request", expression: .identifierPattern("request")), + .init( + label: "descriptor", + expression: .identifierPattern( + "\(service.namespacedTypealiasPrefix).Methods.\(method.name).descriptor" + ) + ), + .init(label: "serializer", expression: .identifierPattern("serializer")), + .init(label: "deserializer", expression: .identifierPattern("deserializer")), + .init(label: "handler", expression: .identifierPattern("body")), + ] + ) + let body = UnaryKeywordDescription( + kind: .try, + expression: .unaryKeyword(kind: .await, expression: functionCall) + ) + + return .function( + kind: .function( + name: "\(method.name)", + isStatic: false + ), + generics: [.member("R")], + parameters: parameters, + keywords: [.async, .throws], + returnType: .identifierType(.member("R")), + whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]), + body: [.expression(.unaryKeyword(body))] + ) + } + + fileprivate enum InputOutputType { + case input + case output + } + + /// Generates the fully qualified name of the typealias for the input or output type of a method. + private func methodInputOutputTypealias( + for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + service: CodeGenerationRequest.ServiceDescriptor, + type: InputOutputType + ) -> String { + var components: String = "\(service.namespacedTypealiasPrefix).Methods.\(method.name)" + + switch type { + case .input: + components.append(".Input") + case .output: + components.append(".Output") + } + + return components + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 85fadaeda..0a707fd31 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -15,8 +15,6 @@ */ struct IDLToStructuredSwiftTranslator: Translator { - private let serverCodeTranslator = ServerCodeTranslator() - func translate( codeGenerationRequest: CodeGenerationRequest, client: Bool, @@ -28,14 +26,23 @@ struct IDLToStructuredSwiftTranslator: Translator { let imports: [ImportDescription] = [ ImportDescription(moduleName: "GRPCCore") ] + var codeBlocks: [CodeBlock] = [] codeBlocks.append( contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest) ) if server { + let serverCodeTranslator = ServerCodeTranslator() + codeBlocks.append( + contentsOf: try serverCodeTranslator.translate(from: codeGenerationRequest) + ) + } + + if client { + let clientCodeTranslator = ClientCodeTranslator() codeBlocks.append( - contentsOf: try self.serverCodeTranslator.translate(from: codeGenerationRequest) + contentsOf: try clientCodeTranslator.translate(from: codeGenerationRequest) ) } diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index bbe99de1c..0b822be07 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -381,6 +381,39 @@ final class Test_TextBasedRenderer: XCTestCase { ) } + func testGenericFunction() throws { + try _test( + .init( + accessModifier: .public, + kind: .function(name: "f"), + generics: [.member("R")], + parameters: [], + whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]), + body: [] + ), + renderedBy: TextBasedRenderer.renderFunction, + rendersAs: #""" + public func f() where R: Sendable {} + """# + ) + try _test( + .init( + accessModifier: .public, + kind: .function(name: "f"), + generics: [.member("R"), .member("T")], + parameters: [], + whereClause: WhereClause(requirements: [ + .conformance("R", "Sendable"), .conformance("T", "Encodable"), + ]), + body: [] + ), + renderedBy: TextBasedRenderer.renderFunction, + rendersAs: #""" + public func f() where R: Sendable, T: Encodable {} + """# + ) + } + func testFunction() throws { try _test( .init(accessModifier: .public, kind: .function(name: "f"), parameters: [], body: []), @@ -436,7 +469,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testIdentifiers() throws { try _test( .pattern("foo"), - renderedBy: TextBasedRenderer.renderedIdentifier, + renderedBy: TextBasedRenderer.renderIdentifier, rendersAs: #""" foo """# diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift new file mode 100644 index 000000000..144749390 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -0,0 +1,542 @@ +/* + * Copyright 2023, 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 XCTest + +@testable import GRPCCodeGen + +final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { + typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + + func testClientCodeTranslatorUnaryMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "methodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAClientProtocol: Sendable { + /// Documentation for MethodA + func methodA( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + } + extension namespaceA.ServiceA.ClientProtocol { + func methodA( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.methodA( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + /// Documentation for ServiceA + struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Documentation for MethodA + func methodA( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + """ + + try self.assertClientCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testClientCodeTranslatorClientStreamingMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "methodA", + isInputStreaming: true, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAClientProtocol: Sendable { + /// Documentation for MethodA + func methodA( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + } + extension namespaceA.ServiceA.ClientProtocol { + func methodA( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.methodA( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + /// Documentation for ServiceA + struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Documentation for MethodA + func methodA( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + """ + + try self.assertClientCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testClientCodeTranslatorServerStreamingMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "methodA", + isInputStreaming: false, + isOutputStreaming: true, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAClientProtocol: Sendable { + /// Documentation for MethodA + func methodA( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + } + extension namespaceA.ServiceA.ClientProtocol { + func methodA( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.methodA( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + /// Documentation for ServiceA + struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Documentation for MethodA + func methodA( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + """ + + try self.assertClientCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testClientCodeTranslatorBidirectionalStreamingMethod() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "methodA", + isInputStreaming: true, + isOutputStreaming: true, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAClientProtocol: Sendable { + /// Documentation for MethodA + func methodA( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + } + extension namespaceA.ServiceA.ClientProtocol { + func methodA( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.methodA( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + /// Documentation for ServiceA + struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Documentation for MethodA + func methodA( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + """ + + try self.assertClientCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testClientCodeTranslatorMultipleMethods() throws { + let methodA = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "methodA", + isInputStreaming: true, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let methodB = MethodDescriptor( + documentation: "Documentation for MethodB", + name: "methodB", + isInputStreaming: false, + isOutputStreaming: true, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [methodA, methodB] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAClientProtocol: Sendable { + /// Documentation for MethodA + func methodA( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + /// Documentation for MethodB + func methodB( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + } + extension namespaceA.ServiceA.ClientProtocol { + func methodA( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.methodA( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + func methodB( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.methodB( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + /// Documentation for ServiceA + struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Documentation for MethodA + func methodA( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + /// Documentation for MethodB + func methodB( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: namespaceA.ServiceA.Methods.methodB.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + """ + + try self.assertClientCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testClientCodeTranslatorNoNamespaceService() throws { + let method = MethodDescriptor( + documentation: "Documentation for MethodA", + name: "methodA", + isInputStreaming: false, + isOutputStreaming: false, + inputType: "ServiceARequest", + outputType: "ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "", + methods: [method] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol ServiceAClientProtocol: Sendable { + /// Documentation for MethodA + func methodA( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + } + extension ServiceA.ClientProtocol { + func methodA( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.methodA( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + /// Documentation for ServiceA + struct ServiceAClient: ServiceA.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Documentation for MethodA + func methodA( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: ServiceA.Methods.methodA.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + """ + + try self.assertClientCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [service]), + expectedSwift: expectedSwift + ) + } + + func testClientCodeTranslatorMultipleServices() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for ServiceA", + name: "ServiceA", + namespace: "namespaceA", + methods: [] + ) + let serviceB = ServiceDescriptor( + documentation: "Documentation for ServiceB", + name: "ServiceB", + namespace: "", + methods: [] + ) + let expectedSwift = + """ + /// Documentation for ServiceA + protocol namespaceA_ServiceAClientProtocol: Sendable {} + extension namespaceA.ServiceA.ClientProtocol { + } + /// Documentation for ServiceA + struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + } + /// Documentation for ServiceB + protocol ServiceBClientProtocol: Sendable {} + extension ServiceB.ClientProtocol { + } + /// Documentation for ServiceB + struct ServiceBClient: ServiceB.ClientProtocol { + let client: GRPCCore.GRPCClient + init(client: GRPCCore.GRPCClient) { + self.client = client + } + } + """ + + try self.assertClientCodeTranslation( + codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), + expectedSwift: expectedSwift + ) + } + + private func assertClientCodeTranslation( + codeGenerationRequest: CodeGenerationRequest, + expectedSwift: String + ) throws { + let translator = ClientCodeTranslator() + let codeBlocks = try translator.translate(from: codeGenerationRequest) + let renderer = TextBasedRenderer.default + renderer.renderCodeBlocks(codeBlocks) + let contents = renderer.renderedContents() + try XCTAssertEqualWithDiff(contents, expectedSwift) + } +} From f7641a0be51aca01883c74c0d3829935f1287006 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:54:41 +0000 Subject: [PATCH 201/580] [CodeGenLib] Translating dependencies into StructuredSwiftRepresentation (#1752) Translating dependencies into StructuredSwiftRepresentation Motivation: Dependency representations within CodeGeneratorRequest and StructuredSwiftRepresentation don't match (each contains fields that the other one doesn't have). The CodeGenerationRequest representation of a dependency doesn't contain preconcurrency requirements and spi, while the StructuredSwiftRepresentation doesn't allow items imports (func, class etc.). We need to add the missing fields to both representation in order to translate the imports, without losing information. Modifications: - Added the preconcurrency ans spi parameters to the CodeGenerationRequest Dependency struct (and the PreconcurrencyRequirement struct to represent that encapsulates the possible requirement types) - Added the item property to the StructuredSwiftRepresentation ImportDescription struct (and the Item struct backed up by an item representing the possible cases). - Created the function that translates a CodeGenerationRequest.Dependency into a StructuredSwiftRepresentation.ImportDescription. - Added the item rendering in the TextBasedRenderer. - Added tests for the TextRenderer and the IDLToStructuredSwiftRepresentationTranslator that check the imports. Result: Imports can now be translated and added in the generated code. --- Sources/GRPCCodeGen/CodeGenError.swift | 6 + .../GRPCCodeGen/CodeGenerationRequest.swift | 50 ++++++- .../Internal/Renderer/TextBasedRenderer.swift | 7 +- .../StructuredSwiftRepresentation.swift | 30 ++++- .../IDLToStructuredSwiftTranslator.swift | 37 +++++- .../Renderer/TextBasedRendererTests.swift | 54 ++++++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 122 ++++++++++++++++++ .../Internal/Translator/TestFunctions.swift | 17 +++ 8 files changed, 316 insertions(+), 7 deletions(-) diff --git a/Sources/GRPCCodeGen/CodeGenError.swift b/Sources/GRPCCodeGen/CodeGenError.swift index 041fae807..6cfffa32c 100644 --- a/Sources/GRPCCodeGen/CodeGenError.swift +++ b/Sources/GRPCCodeGen/CodeGenError.swift @@ -38,6 +38,7 @@ extension CodeGenError { private enum Value { case nonUniqueServiceName case nonUniqueMethodName + case invalidKind } private var value: Value @@ -54,6 +55,11 @@ extension CodeGenError { public static var nonUniqueMethodName: Self { Self(.nonUniqueMethodName) } + + /// An invalid kind name is used for an import. + public static var invalidKind: Self { + Self(.invalidKind) + } } } diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 9a811647c..1efb81636 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -88,9 +88,25 @@ public struct CodeGenerationRequest { /// The name of the imported module or of the module an item is imported from. public var module: String - public init(item: Item? = nil, module: String) { + /// The name of the private interface for an `@_spi` import. + /// + /// For example, if `spi` was "Secret" and the module name was "Foo" then the import + /// would be `@_spi(Secret) import Foo`. + public var spi: String? + + /// Requirements for the `@preconcurrency` attribute. + public var preconcurrency: PreconcurrencyRequirement + + public init( + item: Item? = nil, + module: String, + spi: String? = nil, + preconcurrency: PreconcurrencyRequirement = .notRequired + ) { self.item = item self.module = module + self.spi = spi + self.preconcurrency = preconcurrency } /// Represents an item imported from a module. @@ -109,7 +125,7 @@ public struct CodeGenerationRequest { /// Represents the imported item's kind. public struct Kind { /// Describes the keyword associated with the imported item. - internal enum Value { + internal enum Value: String { case `typealias` case `struct` case `class` @@ -167,6 +183,36 @@ public struct CodeGenerationRequest { } } } + + /// Describes any requirement for the `@preconcurrency` attribute. + public struct PreconcurrencyRequirement { + internal enum Value { + case required + case notRequired + case requiredOnOS([String]) + } + + internal var value: Value + + internal init(_ value: Value) { + self.value = value + } + + /// The attribute is always required. + public static var required: Self { + Self(.required) + } + + /// The attribute is not required. + public static var notRequired: Self { + Self(.notRequired) + } + + /// The attribute is required only on the named operating systems. + public static func requiredOnOS(_ OSs: [String]) -> PreconcurrencyRequirement { + return Self(.requiredOnOS(OSs)) + } + } } /// Represents a service described in an IDL file. diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 54e642545..d8005bdc6 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -169,7 +169,12 @@ struct TextBasedRenderer: RendererProtocol { func render(preconcurrency: Bool) { let spiPrefix = description.spi.map { "@_spi(\($0)) " } ?? "" let preconcurrencyPrefix = preconcurrency ? "@preconcurrency " : "" - if let moduleTypes = description.moduleTypes { + + if let item = description.item { + writer.writeLine( + "\(preconcurrencyPrefix)\(spiPrefix)import \(item.kind) \(description.moduleName).\(item.name)" + ) + } else if let moduleTypes = description.moduleTypes { for type in moduleTypes { writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(type)") } diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 556c3d909..e423ec1f3 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -31,7 +31,6 @@ /// /// For example: `import Foo`. struct ImportDescription: Equatable, Codable { - /// The name of the imported module. /// /// For example, the `Foo` in `import Foo`. @@ -51,6 +50,10 @@ struct ImportDescription: Equatable, Codable { /// Requirements for the `@preconcurrency` attribute. var preconcurrency: PreconcurrencyRequirement = .never + /// If the dependency is an item, the property's value is the item representation. + /// If the dependency is a module, this property is nil. + var item: Item? = nil + /// Describes any requirement for the `@preconcurrency` attribute. enum PreconcurrencyRequirement: Equatable, Codable { /// The attribute is always required. @@ -60,6 +63,31 @@ struct ImportDescription: Equatable, Codable { /// The attribute is required only on the named operating systems. case onOS([String]) } + + /// Represents an item imported from a module. + struct Item: Equatable, Codable { + /// The keyword that specifies the item's kind (e.g. `func`, `struct`). + var kind: Kind + + /// The name of the imported item. + var name: String + + init(kind: Kind, name: String) { + self.kind = kind + self.name = name + } + } + + enum Kind: String, Equatable, Codable { + case `typealias` + case `struct` + case `class` + case `enum` + case `protocol` + case `let` + case `var` + case `func` + } } /// A description of an access modifier. diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 0a707fd31..947a1add1 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -23,9 +23,11 @@ struct IDLToStructuredSwiftTranslator: Translator { try self.validateInput(codeGenerationRequest) let typealiasTranslator = TypealiasTranslator(client: client, server: server) let topComment = Comment.doc(codeGenerationRequest.leadingTrivia) - let imports: [ImportDescription] = [ - ImportDescription(moduleName: "GRPCCore") - ] + let imports = try codeGenerationRequest.dependencies.reduce( + into: [ImportDescription(moduleName: "GRPCCore")] + ) { partialResult, newDependency in + try partialResult.append(translateImport(dependency: newDependency)) + } var codeBlocks: [CodeBlock] = [] codeBlocks.append( @@ -58,6 +60,35 @@ struct IDLToStructuredSwiftTranslator: Translator { } extension IDLToStructuredSwiftTranslator { + private func translateImport( + dependency: CodeGenerationRequest.Dependency + ) throws -> ImportDescription { + var importDescription = ImportDescription(moduleName: dependency.module) + if let item = dependency.item { + if let matchedKind = ImportDescription.Kind(rawValue: item.kind.value.rawValue) { + importDescription.item = ImportDescription.Item(kind: matchedKind, name: item.name) + } else { + throw CodeGenError( + code: .invalidKind, + message: "Invalid kind name for import: \(item.kind.value.rawValue)" + ) + } + } + if let spi = dependency.spi { + importDescription.spi = spi + } + + switch dependency.preconcurrency.value { + case .required: + importDescription.preconcurrency = .always + case .notRequired: + importDescription.preconcurrency = .never + case .requiredOnOS(let OSs): + importDescription.preconcurrency = .onOS(OSs) + } + return importDescription + } + private func validateInput(_ codeGenerationRequest: CodeGenerationRequest) throws { let servicesByNamespace = Dictionary( grouping: codeGenerationRequest.services, diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 0b822be07..36da567f8 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -130,6 +130,60 @@ final class Test_TextBasedRenderer: XCTestCase { @preconcurrency @_spi(Secret) import Bar """# ) + + try _test( + [ + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .typealias, name: "Bar") + ), + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .struct, name: "Baz") + ), + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .class, name: "Bac") + ), + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .enum, name: "Bap") + ), + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .protocol, name: "Bat") + ), + ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .let, name: "Bam")), + ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .var, name: "Bag")), + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .func, name: "Bak") + ), + ImportDescription( + moduleName: "Foo", + spi: "Secret", + item: ImportDescription.Item(kind: .func, name: "SecretBar") + ), + ImportDescription( + moduleName: "Foo", + preconcurrency: .always, + item: ImportDescription.Item(kind: .func, name: "PreconcurrencyBar") + ), + ], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + import typealias Foo.Bar + import struct Foo.Baz + import class Foo.Bac + import enum Foo.Bap + import protocol Foo.Bat + import let Foo.Bam + import var Foo.Bag + import func Foo.Bak + @_spi(Secret) import func Foo.SecretBar + @preconcurrency import func Foo.PreconcurrencyBar + """# + ) } func testAccessModifiers() throws { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index fca3f3ee2..b7a32934b 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -24,6 +24,128 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + func testImports() throws { + var dependencies = [CodeGenerationRequest.Dependency]() + dependencies.append(CodeGenerationRequest.Dependency(module: "Foo")) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .typealias, name: "Bar"), module: "Foo") + ) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .struct, name: "Baz"), module: "Foo") + ) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .class, name: "Bac"), module: "Foo") + ) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .enum, name: "Bap"), module: "Foo") + ) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .protocol, name: "Bat"), module: "Foo") + ) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .let, name: "Baq"), module: "Foo") + ) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .var, name: "Bag"), module: "Foo") + ) + dependencies.append( + CodeGenerationRequest.Dependency(item: .init(kind: .func, name: "Bak"), module: "Foo") + ) + + let expectedSwift = + """ + /// Some really exciting license header 2023. + import GRPCCore + import Foo + import typealias Foo.Bar + import struct Foo.Baz + import class Foo.Bac + import enum Foo.Bap + import protocol Foo.Bat + import let Foo.Baq + import var Foo.Bag + import func Foo.Bak + """ + try self.assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), + expectedSwift: expectedSwift + ) + } + + func testPreconcurrencyImports() throws { + var dependencies = [CodeGenerationRequest.Dependency]() + dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", preconcurrency: .required)) + dependencies.append( + CodeGenerationRequest.Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + preconcurrency: .required + ) + ) + dependencies.append( + CodeGenerationRequest.Dependency( + module: "Baz", + preconcurrency: .requiredOnOS(["Deq", "Der"]) + ) + ) + let expectedSwift = + """ + /// Some really exciting license header 2023. + import GRPCCore + @preconcurrency import Foo + @preconcurrency import enum Foo.Bar + #if os(Deq) || os(Der) + @preconcurrency import Baz + #else + import Baz + #endif + """ + try self.assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), + expectedSwift: expectedSwift + ) + } + + func testSPIImports() throws { + var dependencies = [CodeGenerationRequest.Dependency]() + dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret")) + dependencies.append( + CodeGenerationRequest.Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + spi: "Secret" + ) + ) + + let expectedSwift = + """ + /// Some really exciting license header 2023. + import GRPCCore + @_spi(Secret) import Foo + @_spi(Secret) import enum Foo.Bar + """ + try self.assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), + expectedSwift: expectedSwift + ) + } + + private func assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: CodeGenerationRequest, + expectedSwift: String + ) throws { + let translator = IDLToStructuredSwiftTranslator() + let structuredSwift = try translator.translate( + codeGenerationRequest: codeGenerationRequest, + client: false, + server: false + ) + let renderer = TextBasedRenderer.default + let sourceFile = try renderer.render(structured: structuredSwift) + let contents = sourceFile.contents + try XCTAssertEqualWithDiff(contents, expectedSwift) + } + func testSameNameServicesNoNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for AService", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 4c2d781cd..1250b92d9 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -88,6 +88,23 @@ internal func makeCodeGenerationRequest( ) } +internal func makeCodeGenerationRequest( + dependencies: [CodeGenerationRequest.Dependency] +) -> CodeGenerationRequest { + return CodeGenerationRequest( + fileName: "test.grpc", + leadingTrivia: "Some really exciting license header 2023.", + dependencies: dependencies, + services: [], + lookupSerializer: { + "ProtobufSerializer<\($0)>()" + }, + lookupDeserializer: { + "ProtobufDeserializer<\($0)>()" + } + ) +} + internal func XCTAssertThrowsError( ofType: E.Type, _ expression: @autoclosure () throws -> T, From da5a236bb3d1d683a90db1b03fce6b807b7b973f Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 12 Jan 2024 13:47:26 +0000 Subject: [PATCH 202/580] Create new HTTP2 transport modules (#1757) --- Package.swift | 55 ++++++++++++++++++- Sources/GRPCHTTP2Core/README.md | 1 + Sources/GRPCHTTP2TransportNIOPosix/README.md | 1 + .../README.md | 1 + .../GRPCHTTP2CoreTests.swift | 17 ++++++ .../GRPCHTTP2TransportNIOPosixTests.swift | 17 ++++++ ...P2TransportNIOTransportServicesTests.swift | 17 ++++++ 7 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCHTTP2Core/README.md create mode 100644 Sources/GRPCHTTP2TransportNIOPosix/README.md create mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/README.md create mode 100644 Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift create mode 100644 Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift create mode 100644 Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift diff --git a/Package.swift b/Package.swift index 27316039d..201ba8e56 100644 --- a/Package.swift +++ b/Package.swift @@ -134,6 +134,9 @@ extension Target.Dependency { static let grpcCore: Self = .target(name: "GRPCCore") static let grpcInProcessTransport: Self = .target(name: "GRPCInProcessTransport") + static let grpcHTTP2Core: Self = .target(name: "GRPCHTTP2Core") + static let grpcHTTP2TransportNIOPosix: Self = .target(name: "GRPCHTTP2TransportNIOPosix") + static let grpcHTTP2TransportNIOTransportServices: Self = .target(name: "GRPCHTTP2TransportNIOTransportServices") } // MARK: - Targets @@ -177,6 +180,29 @@ extension Target { .grpcCore ] ) + + static let grpcHTTP2Core: Target = .target( + name: "GRPCHTTP2Core", + dependencies: [ + .grpcCore, + .nioCore, + .nioHTTP2 + ] + ) + + static let grpcHTTP2TransportNIOPosix: Target = .target( + name: "GRPCHTTP2TransportNIOPosix", + dependencies: [ + .grpcHTTP2Core + ] + ) + + static let grpcHTTP2TransportNIOTransportServices: Target = .target( + name: "GRPCHTTP2TransportNIOTransportServices", + dependencies: [ + .grpcHTTP2Core + ] + ) static let cgrpcZlib: Target = .target( name: cgrpcZlibTargetName, @@ -251,6 +277,27 @@ extension Target { .grpcInProcessTransport, ] ) + + static let grpcHTTP2CoreTests: Target = .testTarget( + name: "GRPCHTTP2CoreTests", + dependencies: [ + .grpcHTTP2Core + ] + ) + + static let grpcHTTP2TransportNIOPosixTests: Target = .testTarget( + name: "GRPCHTTP2TransportNIOPosixTests", + dependencies: [ + .grpcHTTP2TransportNIOPosix + ] + ) + + static let grpcHTTP2TransportNIOTransportServicesTests: Target = .testTarget( + name: "GRPCHTTP2TransportNIOTransportServicesTests", + dependencies: [ + .grpcHTTP2TransportNIOTransportServices + ] + ) static let grpcCodeGenTests: Target = .testTarget( name: "GRPCCodeGenTests", @@ -591,11 +638,17 @@ let package = Package( .grpcCore, .grpcInProcessTransport, .grpcCodeGen, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .grpcHTTP2TransportNIOTransportServices, // v2 tests .grpcCoreTests, .grpcInProcessTransportTests, - .grpcCodeGenTests + .grpcCodeGenTests, + .grpcHTTP2CoreTests, + .grpcHTTP2TransportNIOPosixTests, + .grpcHTTP2TransportNIOTransportServicesTests ] ) diff --git a/Sources/GRPCHTTP2Core/README.md b/Sources/GRPCHTTP2Core/README.md new file mode 100644 index 000000000..a0f051cc7 --- /dev/null +++ b/Sources/GRPCHTTP2Core/README.md @@ -0,0 +1 @@ +# gRPC HTTP2 Core diff --git a/Sources/GRPCHTTP2TransportNIOPosix/README.md b/Sources/GRPCHTTP2TransportNIOPosix/README.md new file mode 100644 index 000000000..635ca3410 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOPosix/README.md @@ -0,0 +1 @@ +# gRPC HTTP2 Transport NIO POSIX diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/README.md b/Sources/GRPCHTTP2TransportNIOTransportServices/README.md new file mode 100644 index 000000000..46303d13a --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/README.md @@ -0,0 +1 @@ +# gRPC HTTP2 Transport NIO Transport Services diff --git a/Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift new file mode 100644 index 000000000..3630df68b --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// Add tests to this package. diff --git a/Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift new file mode 100644 index 000000000..3630df68b --- /dev/null +++ b/Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// Add tests to this package. diff --git a/Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift new file mode 100644 index 000000000..3630df68b --- /dev/null +++ b/Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// Add tests to this package. From b8ccbf3799dc3e1dca351b1bddf769c4d4a4457e Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 15 Jan 2024 10:36:14 +0000 Subject: [PATCH 203/580] Add missing compiler directive to ClientCodeTranslatorSnippetBasedTests (#1758) Motivation: A missing compiler directive to avoid compiling the snippet-based tests in non-macOS/Linux platforms was causing the build to fail on iOS/tvOS/watchOS. Modifications: Add the compiler directive. Result: Builds won't fail again in iOS/tvOS/watchOS. --- .../Translator/ClientCodeTranslatorSnippetBasedTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 144749390..6e4daa1a6 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) + import XCTest @testable import GRPCCodeGen @@ -540,3 +542,5 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try XCTAssertEqualWithDiff(contents, expectedSwift) } } + +#endif // os(macOS) || os(Linux) From bba613b35ba7f21f45f81c86ec3f5064576fba13 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:30:05 +0000 Subject: [PATCH 204/580] [CodeGenLib] SourceGenerator public API Motivation: The SourceGenerator is responsible for transforming the CodeGenerationRequest input into a SourceFile object. Modifications: - created the public SourceGenerator struct that contains a Configuration struct (visibility level + indentation) and the `generate()` function. - the `generate()` function translates the input object into a StructuredSwiftRepresentation oject, then renders the latter into a SourceFile object. - modified tests to include Visibility. Result: The user can now generate a SourceFile that contains generated code, using a CodeGenerationRequest object and the SourceGenerator API. --- .../Internal/Renderer/TextBasedRenderer.swift | 14 +- .../StructuredSwiftRepresentation.swift | 5 +- .../Translator/ClientCodeTranslator.swift | 26 +- .../IDLToStructuredSwiftTranslator.swift | 15 +- .../Translator/ServerCodeTranslator.swift | 37 ++- .../Translator/SpecializedTranslator.swift | 20 ++ .../Internal/Translator/Translator.swift | 2 + .../Translator/TypealiasTranslator.swift | 20 +- Sources/GRPCCodeGen/SourceGenerator.swift | 76 +++++ .../Renderer/TextBasedRendererTests.swift | 67 +++- ...lientCodeTranslatorSnippetBasedTests.swift | 118 +++---- ...uredSwiftTranslatorSnippetBasedTests.swift | 17 +- ...erverCodeTranslatorSnippetBasedTests.swift | 118 +++---- ...TypealiasTranslatorSnippetBasedTests.swift | 303 +++++++++--------- 14 files changed, 550 insertions(+), 288 deletions(-) create mode 100644 Sources/GRPCCodeGen/SourceGenerator.swift diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index d8005bdc6..f9f8eeba5 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -40,14 +40,18 @@ final class StringCodeWriter { /// The current nesting level. private var level: Int + /// The indentation for each level as the number of spaces. + internal let indentation: Int + /// Whether the next call to `writeLine` will continue writing to the last /// stored line. Otherwise a new line is appended. private var nextWriteAppendsToLastLine: Bool = false /// Creates a new empty writer. - init() { + init(indentation: Int) { self.level = 0 self.lines = [] + self.indentation = indentation } /// Concatenates the stored lines of code into a single string. @@ -67,7 +71,7 @@ final class StringCodeWriter { let existingLine = lines.removeLast() newLine = existingLine + line } else { - let indentation = Array(repeating: " ", count: 4 * level).joined() + let indentation = Array(repeating: " ", count: self.indentation * level).joined() newLine = indentation + line } lines.append(newLine) @@ -119,7 +123,11 @@ struct TextBasedRenderer: RendererProtocol { private let writer: StringCodeWriter /// Creates a new empty renderer. - static var `default`: TextBasedRenderer { .init(writer: StringCodeWriter()) } + static var `default`: TextBasedRenderer { .init(indentation: 4) } + + init(indentation: Int) { + self.writer = StringCodeWriter(indentation: indentation) + } // MARK: - Internals diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index e423ec1f3..e5b513a51 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -344,7 +344,7 @@ struct WhereClause: Equatable, Codable { struct ExtensionDescription: Equatable, Codable { /// An access modifier. - var accessModifier: AccessModifier? + var accessModifier: AccessModifier? = nil /// The name of the extended type. /// @@ -694,6 +694,7 @@ struct ClosureSignatureDescription: Equatable, Codable { /// The ``@escaping`` attribute. var escaping: Bool = false } + /// A description of the associated value of an enum case. /// /// For example, in `case foo(bar: String)`, the associated value @@ -1385,7 +1386,7 @@ extension Declaration { /// extended type. /// - Returns: An extension declaration. static func `extension`( - accessModifier: AccessModifier?, + accessModifier: AccessModifier? = nil, onType: String, conformances: [String] = [], whereClause: WhereClause? = nil, diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 385f513f7..567fd365e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -42,12 +42,12 @@ /// body /// ) /// } -/// struct foo_BarClient: foo.Bar.ClientProtocol { -/// let client: GRPCCore.GRPCClient -/// init(client: GRPCCore.GRPCClient) { +/// public struct foo_BarClient: foo.Bar.ClientProtocol { +/// private let client: GRPCCore.GRPCClient +/// public init(client: GRPCCore.GRPCClient) { /// self.client = client /// } -/// func methodA( +/// public func methodA( /// request: ClientRequest.Stream, /// serializer: some MessageSerializer, /// deserializer: some MessageDeserializer, @@ -64,6 +64,12 @@ /// } ///``` struct ClientCodeTranslator: SpecializedTranslator { + var accessLevel: SourceGenerator.Configuration.AccessLevel + + init(accessLevel: SourceGenerator.Configuration.AccessLevel) { + self.accessLevel = accessLevel + } + func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { var codeBlocks = [CodeBlock]() @@ -108,6 +114,7 @@ extension ClientCodeTranslator { let clientProtocol = Declaration.protocol( ProtocolDescription( + accessModifier: self.accessModifier, name: "\(service.namespacedPrefix)ClientProtocol", conformances: ["Sendable"], members: methods @@ -125,7 +132,8 @@ extension ClientCodeTranslator { for: $0, in: service, from: codeGenerationRequest, - generateSerializerDeserializer: true + generateSerializerDeserializer: true, + accessModifier: self.accessModifier ) } let clientProtocolExtension = Declaration.extension( @@ -141,7 +149,8 @@ extension ClientCodeTranslator { for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest, - generateSerializerDeserializer: Bool + generateSerializerDeserializer: Bool, + accessModifier: AccessModifier? = nil ) -> Declaration { let methodParameters = self.makeParameters( for: method, @@ -150,6 +159,7 @@ extension ClientCodeTranslator { generateSerializerDeserializer: generateSerializerDeserializer ) let functionSignature = FunctionSignatureDescription( + accessModifier: accessModifier, kind: .function( name: method.name, isStatic: false @@ -303,6 +313,7 @@ extension ClientCodeTranslator { in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let clientProperty = Declaration.variable( + accessModifier: .private, kind: .let, left: "client", type: .member(["GRPCCore", "GRPCClient"]) @@ -317,6 +328,7 @@ extension ClientCodeTranslator { return .struct( StructDescription( + accessModifier: self.accessModifier, name: "\(service.namespacedPrefix)Client", conformances: ["\(service.namespacedTypealiasPrefix).ClientProtocol"], members: [clientProperty, initializer] + methods @@ -333,6 +345,7 @@ extension ClientCodeTranslator { ) return .function( signature: .init( + accessModifier: self.accessModifier, kind: .initializer, parameters: [.init(label: "client", type: .member(["GRPCCore", "GRPCClient"]))] ), @@ -394,6 +407,7 @@ extension ClientCodeTranslator { ) return .function( + accessModifier: self.accessModifier, kind: .function( name: "\(method.name)", isStatic: false diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 947a1add1..7e62a60bf 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -14,14 +14,23 @@ * limitations under the License. */ +/// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties. +/// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications, +/// using types from ``StructuredSwiftRepresentation``. struct IDLToStructuredSwiftTranslator: Translator { func translate( codeGenerationRequest: CodeGenerationRequest, + accessLevel: SourceGenerator.Configuration.AccessLevel, client: Bool, server: Bool ) throws -> StructuredSwiftRepresentation { try self.validateInput(codeGenerationRequest) - let typealiasTranslator = TypealiasTranslator(client: client, server: server) + let typealiasTranslator = TypealiasTranslator( + client: client, + server: server, + accessLevel: accessLevel + ) + let topComment = Comment.doc(codeGenerationRequest.leadingTrivia) let imports = try codeGenerationRequest.dependencies.reduce( into: [ImportDescription(moduleName: "GRPCCore")] @@ -35,14 +44,14 @@ struct IDLToStructuredSwiftTranslator: Translator { ) if server { - let serverCodeTranslator = ServerCodeTranslator() + let serverCodeTranslator = ServerCodeTranslator(accessLevel: accessLevel) codeBlocks.append( contentsOf: try serverCodeTranslator.translate(from: codeGenerationRequest) ) } if client { - let clientCodeTranslator = ClientCodeTranslator() + let clientCodeTranslator = ClientCodeTranslator(accessLevel: accessLevel) codeBlocks.append( contentsOf: try clientCodeTranslator.translate(from: codeGenerationRequest) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 661389492..dc9ba10dc 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -54,6 +54,12 @@ /// } ///``` struct ServerCodeTranslator: SpecializedTranslator { + var accessLevel: SourceGenerator.Configuration.AccessLevel + + init(accessLevel: SourceGenerator.Configuration.AccessLevel) { + self.accessLevel = accessLevel + } + func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { var codeBlocks = [CodeBlock]() for service in codeGenerationRequest.services { @@ -112,6 +118,7 @@ extension ServerCodeTranslator { let streamingProtocol = Declaration.protocol( .init( + accessModifier: self.accessModifier, name: self.protocolName(service: service, streaming: true), conformances: ["GRPCCore.RegistrableRPCService"], members: methods @@ -123,9 +130,11 @@ extension ServerCodeTranslator { private func makeStreamingMethodSignature( for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + in service: CodeGenerationRequest.ServiceDescriptor, + accessModifier: AccessModifier? = nil ) -> FunctionSignatureDescription { return FunctionSignatureDescription( + accessModifier: accessModifier, kind: .function(name: method.name), parameters: [ .init( @@ -157,7 +166,6 @@ extension ServerCodeTranslator { let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) let registerRPCMethod = self.makeRegisterRPCsMethod(for: service, in: codeGenerationRequest) return .extension( - accessModifier: .public, onType: streamingProtocol, declarations: [registerRPCMethod] ) @@ -168,7 +176,8 @@ extension ServerCodeTranslator { in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let registerRPCsSignature = FunctionSignatureDescription( - kind: .function(name: "registerRPCs"), + accessModifier: self.accessModifier, + kind: .function(name: "registerMethods"), parameters: [ .init( label: "with", @@ -280,14 +289,20 @@ extension ServerCodeTranslator { return .commentable( .doc(service.documentation), .protocol( - ProtocolDescription(name: protocolName, conformances: [streamingProtocol], members: methods) + ProtocolDescription( + accessModifier: self.accessModifier, + name: protocolName, + conformances: [streamingProtocol], + members: methods + ) ) ) } private func makeServiceProtocolMethod( for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + in service: CodeGenerationRequest.ServiceDescriptor, + accessModifier: AccessModifier? = nil ) -> Declaration { let inputStreaming = method.isInputStreaming ? "Stream" : "Single" let outputStreaming = method.isOutputStreaming ? "Stream" : "Single" @@ -304,6 +319,7 @@ extension ServerCodeTranslator { ) let functionSignature = FunctionSignatureDescription( + accessModifier: accessModifier, kind: .function(name: method.name), parameters: [ .init( @@ -338,7 +354,10 @@ extension ServerCodeTranslator { } let protocolName = self.protocolNameTypealias(service: service, streaming: false) - return .extension(accessModifier: .public, onType: protocolName, declarations: methods) + return .extension( + onType: protocolName, + declarations: methods + ) } private func makeServiceProtocolExtensionMethod( @@ -354,7 +373,11 @@ extension ServerCodeTranslator { let returnStatement = CodeBlock(item: .expression(self.makeReturnStatement(for: method))) return .function( - signature: self.makeStreamingMethodSignature(for: method, in: service), + signature: self.makeStreamingMethodSignature( + for: method, + in: service, + accessModifier: self.accessModifier + ), body: [response, returnStatement] ) } diff --git a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift index e19876bf0..ff73a9481 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift @@ -17,6 +17,10 @@ /// Represents one responsibility of the ``Translator``: either the type aliases translation, /// the server code translation or the client code translation. protocol SpecializedTranslator { + + /// The ``SourceGenerator.Configuration.AccessLevel`` object used to represent the visibility level used in the generated code. + var accessLevel: SourceGenerator.Configuration.AccessLevel { get } + /// Generates an array of ``CodeBlock`` elements that will be part of the ``StructuredSwiftRepresentation`` object /// created by the ``Translator``. /// @@ -27,3 +31,19 @@ protocol SpecializedTranslator { /// - SeeAlso: ``CodeGenerationRequest``, ``Translator``, ``CodeBlock``. func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] } + +extension SpecializedTranslator { + /// The access modifier that corresponds with the access level from ``SourceGenerator.Configuration``. + internal var accessModifier: AccessModifier { + get { + switch accessLevel.level { + case .internal: + return AccessModifier.internal + case .package: + return AccessModifier.package + case .public: + return AccessModifier.public + } + } + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift index 2ea3ab3f5..14c0c7042 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift @@ -21,12 +21,14 @@ protocol Translator { /// Translates the provided ``CodeGenerationRequest`` object, into Swift code representation. /// - Parameters: /// - codeGenerationRequest: The IDL described RPCs representation. + /// - accessLevel: The access level that will restrict the protocols, extensions and methods in the generated code. /// - client: Whether or not client code should be generated from the IDL described RPCs representation. /// - server: Whether or not server code should be generated from the IDL described RPCs representation. /// - Returns: A structured Swift representation of the generated code. /// - Throws: An error if there are issues translating the codeGenerationRequest. func translate( codeGenerationRequest: CodeGenerationRequest, + accessLevel: SourceGenerator.Configuration.AccessLevel, client: Bool, server: Bool ) throws -> StructuredSwiftRepresentation diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 56c43942e..29efdffc3 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -55,10 +55,12 @@ struct TypealiasTranslator: SpecializedTranslator { let client: Bool let server: Bool + let accessLevel: SourceGenerator.Configuration.AccessLevel - init(client: Bool, server: Bool) { + init(client: Bool, server: Bool, accessLevel: SourceGenerator.Configuration.AccessLevel) { self.client = client self.server = server + self.accessLevel = accessLevel } func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { @@ -101,7 +103,7 @@ extension TypealiasTranslator { CodeBlock(item: .declaration($0)) } } else { - var namespaceEnum = EnumDescription(name: namespace) + var namespaceEnum = EnumDescription(accessModifier: self.accessModifier, name: namespace) namespaceEnum.members = serviceDeclarations return [CodeBlock(item: .declaration(.enum(namespaceEnum)))] } @@ -110,8 +112,8 @@ extension TypealiasTranslator { private func makeServiceEnum( from service: CodeGenerationRequest.ServiceDescriptor ) throws -> Declaration { - var serviceEnum = EnumDescription(name: service.name) - var methodsEnum = EnumDescription(name: "Methods") + var serviceEnum = EnumDescription(accessModifier: self.accessModifier, name: service.name) + var methodsEnum = EnumDescription(accessModifier: self.accessModifier, name: "Methods") let methods = service.methods // Create the method specific enums. @@ -151,10 +153,12 @@ extension TypealiasTranslator { var methodEnum = EnumDescription(name: method.name) let inputTypealias = Declaration.typealias( + accessModifier: self.accessModifier, name: "Input", existingType: .member([method.inputType]) ) let outputTypealias = Declaration.typealias( + accessModifier: self.accessModifier, name: "Output", existingType: .member([method.outputType]) ) @@ -166,6 +170,8 @@ extension TypealiasTranslator { methodEnum.members.append(outputTypealias) methodEnum.members.append(descriptorVariable) + methodEnum.accessModifier = self.accessModifier + return .enum(methodEnum) } @@ -198,6 +204,7 @@ extension TypealiasTranslator { ) ) return .variable( + accessModifier: self.accessModifier, isStatic: true, kind: .let, left: descriptorDeclarationLeft, @@ -222,6 +229,7 @@ extension TypealiasTranslator { } return .variable( + accessModifier: self.accessModifier, isStatic: true, kind: .let, left: .identifier(.pattern("methods")), @@ -234,10 +242,12 @@ extension TypealiasTranslator { for service: CodeGenerationRequest.ServiceDescriptor ) -> [Declaration] { let streamingServiceProtocolTypealias = Declaration.typealias( + accessModifier: self.accessModifier, name: "StreamingServiceProtocol", existingType: .member("\(service.namespacedPrefix)ServiceStreamingProtocol") ) let serviceProtocolTypealias = Declaration.typealias( + accessModifier: self.accessModifier, name: "ServiceProtocol", existingType: .member("\(service.namespacedPrefix)ServiceProtocol") ) @@ -249,6 +259,7 @@ extension TypealiasTranslator { for service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { return .typealias( + accessModifier: self.accessModifier, name: "ClientProtocol", existingType: .member("\(service.namespacedPrefix)ClientProtocol") ) @@ -258,6 +269,7 @@ extension TypealiasTranslator { for service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { return .typealias( + accessModifier: self.accessModifier, name: "Client", existingType: .member("\(service.namespacedPrefix)Client") ) diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift new file mode 100644 index 000000000..98c934e5b --- /dev/null +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -0,0 +1,76 @@ +/* + * 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. + */ + +/// Creates a ``SourceFile`` containing the generated code for the RPCs represented in a ``CodeGenerationRequest`` object. +public struct SourceGenerator: Sendable { + /// The options regarding the access level, indentation for the generated code + /// and whether to generate server and client code. + public var configuration: Configuration + + public init(configuration: Configuration) { + self.configuration = configuration + } + + /// User options for the CodeGeneration. + public struct Configuration: Sendable { + /// The access level the generated code will have. + public var accessLevel: AccessLevel + /// The indentation of the generated code as the number of spaces. + public var indentation: Int + /// Whether or not client code should be generated. + public var client: Bool + /// Whether or not server code should be generated. + public var server: Bool + + /// The possible access levels for the generated code. + public struct AccessLevel: Sendable, Hashable { + internal var level: Level + internal enum Level { + case `internal` + case `public` + case `package` + } + + /// The generated code will have `internal` access level. + public static var `internal`: Self { Self(level: .`internal`) } + + /// The generated code will have `public` access level. + public static var `public`: Self { Self(level: .`public`) } + + /// The generated code will have `package` access level. + public static var `package`: Self { Self(level: .`package`) } + } + } + + /// The function that transforms a ``CodeGenerationRequest`` object into a ``SourceFile`` object containing + /// the generated code, in accordance to the configurations set by the user for the ``SourceGenerator``. + public func generate( + _ serviceRepresentation: CodeGenerationRequest + ) throws -> SourceFile { + let translator = IDLToStructuredSwiftTranslator() + let textRenderer = TextBasedRenderer(indentation: self.configuration.indentation) + + let structuredSwiftRepresentation = try translator.translate( + codeGenerationRequest: serviceRepresentation, + accessLevel: self.configuration.accessLevel, + client: configuration.client, + server: configuration.server + ) + let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) + + return sourceFile + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 36da567f8..bae3b2632 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -831,6 +831,62 @@ final class Test_TextBasedRenderer: XCTestCase { """# ) } + + func testIndentation() throws { + try _test( + .init( + topComment: .inline("hi"), + imports: [.init(moduleName: "Foo")], + codeBlocks: [ + .init( + comment: nil, + item: .declaration(.struct(.init(name: "Bar", members: [.struct(.init(name: "Baz"))]))) + ) + ] + ), + renderedBy: TextBasedRenderer.renderFile, + rendersAs: #""" + // hi + import Foo + struct Bar { + struct Baz {} + } + + """#, + indentation: 2 + ) + + try _test( + .array([.literal(.nil), .literal(.nil)]), + renderedBy: TextBasedRenderer.renderLiteral, + rendersAs: #""" + [ + nil, + nil + ] + """#, + indentation: 3 + ) + + try _test( + .init( + kind: .var, + left: .identifierPattern("foo"), + type: .init(TypeName.int), + getter: [CodeBlock.expression(.literal(.int(42)))], + getterEffects: [.throws] + ), + renderedBy: TextBasedRenderer.renderVariable, + rendersAs: #""" + var foo: Swift.Int { + get throws { + 42 + } + } + """#, + indentation: 5 + ) + } } extension Test_TextBasedRenderer { @@ -840,9 +896,10 @@ extension Test_TextBasedRenderer { renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> String), rendersAs output: String, file: StaticString = #file, - line: UInt = #line + line: UInt = #line, + indentation: Int = 4 ) throws { - let renderer = TextBasedRenderer.default + let renderer = TextBasedRenderer(indentation: indentation) XCTAssertEqual(renderClosure(renderer)(input), output, file: file, line: line) } @@ -851,7 +908,8 @@ extension Test_TextBasedRenderer { renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> Void), rendersAs output: String, file: StaticString = #file, - line: UInt = #line + line: UInt = #line, + indentation: Int = 4 ) throws { try _test( input, @@ -862,7 +920,8 @@ extension Test_TextBasedRenderer { return renderer.renderedContents() } }, - rendersAs: output + rendersAs: output, + indentation: indentation ) } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 6e4daa1a6..08c800bb3 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -42,7 +42,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol namespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: ClientRequest.Single, @@ -52,7 +52,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) async throws -> R where R: Sendable } extension namespaceA.ServiceA.ClientProtocol { - func methodA( + public func methodA( request: ClientRequest.Single, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { @@ -65,13 +65,13 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA - func methodA( + public func methodA( request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, @@ -90,7 +90,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertClientCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -112,7 +113,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol namespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: ClientRequest.Stream, @@ -122,7 +123,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) async throws -> R where R: Sendable } extension namespaceA.ServiceA.ClientProtocol { - func methodA( + public func methodA( request: ClientRequest.Stream, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { @@ -135,13 +136,13 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA - func methodA( + public func methodA( request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, @@ -160,7 +161,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertClientCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -182,7 +184,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol namespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: ClientRequest.Single, @@ -192,7 +194,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) async throws -> R where R: Sendable } extension namespaceA.ServiceA.ClientProtocol { - func methodA( + public func methodA( request: ClientRequest.Single, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { @@ -205,13 +207,13 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA - func methodA( + public func methodA( request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, @@ -230,7 +232,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertClientCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -252,7 +255,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol namespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: ClientRequest.Stream, @@ -262,7 +265,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) async throws -> R where R: Sendable } extension namespaceA.ServiceA.ClientProtocol { - func methodA( + public func methodA( request: ClientRequest.Stream, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { @@ -275,13 +278,13 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA - func methodA( + public func methodA( request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, @@ -300,7 +303,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertClientCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -330,7 +334,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAClientProtocol: Sendable { + package protocol namespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: ClientRequest.Stream, @@ -347,7 +351,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) async throws -> R where R: Sendable } extension namespaceA.ServiceA.ClientProtocol { - func methodA( + package func methodA( request: ClientRequest.Stream, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { @@ -358,7 +362,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { body ) } - func methodB( + package func methodB( request: ClientRequest.Single, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { @@ -371,13 +375,13 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + package struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + private let client: GRPCCore.GRPCClient + package init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA - func methodA( + package func methodA( request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, @@ -392,7 +396,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } /// Documentation for MethodB - func methodB( + package func methodB( request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, @@ -411,7 +415,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertClientCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .package ) } @@ -433,7 +438,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol ServiceAClientProtocol: Sendable { + internal protocol ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: ClientRequest.Single, @@ -443,7 +448,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) async throws -> R where R: Sendable } extension ServiceA.ClientProtocol { - func methodA( + internal func methodA( request: ClientRequest.Single, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { @@ -456,13 +461,13 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - struct ServiceAClient: ServiceA.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + internal struct ServiceAClient: ServiceA.ClientProtocol { + private let client: GRPCCore.GRPCClient + internal init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA - func methodA( + internal func methodA( request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, @@ -481,7 +486,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertClientCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .internal ) } @@ -501,24 +507,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAClientProtocol: Sendable {} + public protocol namespaceA_ServiceAClientProtocol: Sendable {} extension namespaceA.ServiceA.ClientProtocol { } /// Documentation for ServiceA - struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } } /// Documentation for ServiceB - protocol ServiceBClientProtocol: Sendable {} + public protocol ServiceBClientProtocol: Sendable {} extension ServiceB.ClientProtocol { } /// Documentation for ServiceB - struct ServiceBClient: ServiceB.ClientProtocol { - let client: GRPCCore.GRPCClient - init(client: GRPCCore.GRPCClient) { + public struct ServiceBClient: ServiceB.ClientProtocol { + private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } } @@ -526,15 +532,17 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertClientCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } private func assertClientCodeTranslation( codeGenerationRequest: CodeGenerationRequest, - expectedSwift: String + expectedSwift: String, + accessLevel: SourceGenerator.Configuration.AccessLevel ) throws { - let translator = ClientCodeTranslator() + let translator = ClientCodeTranslator(accessLevel: accessLevel) let codeBlocks = try translator.translate(from: codeGenerationRequest) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index b7a32934b..7d50aa37b 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -68,7 +68,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -102,7 +103,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -126,17 +128,20 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } private func assertIDLToStructuredSwiftTranslation( codeGenerationRequest: CodeGenerationRequest, - expectedSwift: String + expectedSwift: String, + accessLevel: SourceGenerator.Configuration.AccessLevel ) throws { let translator = IDLToStructuredSwiftTranslator() let structuredSwift = try translator.translate( codeGenerationRequest: codeGenerationRequest, + accessLevel: accessLevel, client: false, server: false ) @@ -160,6 +165,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ofType: CodeGenError.self, try translator.translate( codeGenerationRequest: codeGenerationRequest, + accessLevel: .public, client: true, server: true ) @@ -192,6 +198,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ofType: CodeGenError.self, try translator.translate( codeGenerationRequest: codeGenerationRequest, + accessLevel: .public, client: true, server: true ) @@ -232,6 +239,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ofType: CodeGenError.self, try translator.translate( codeGenerationRequest: codeGenerationRequest, + accessLevel: .public, client: true, server: true ) @@ -269,6 +277,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ofType: CodeGenError.self, try translator.translate( codeGenerationRequest: codeGenerationRequest, + accessLevel: .public, client: true, server: true ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 89df4bc52..ae29611b2 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -42,13 +42,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + public protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod func unaryMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension namespaceA.ServiceA.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) { + extension namespaceA.ServiceA.StreamingServiceProtocol { + public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( for: namespaceA.ServiceA.Methods.unaryMethod.descriptor, deserializer: ProtobufDeserializer(), @@ -60,13 +60,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + public protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod func unaryMethod(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - public extension namespaceA.ServiceA.ServiceProtocol { - func unaryMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension namespaceA.ServiceA.ServiceProtocol { + public func unaryMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unaryMethod(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -75,7 +75,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertServerCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -97,13 +98,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + package protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension namespaceA.ServiceA.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) { + extension namespaceA.ServiceA.StreamingServiceProtocol { + package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor, deserializer: ProtobufDeserializer(), @@ -115,13 +116,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + package protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Single } /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - public extension namespaceA.ServiceA.ServiceProtocol { - func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension namespaceA.ServiceA.ServiceProtocol { + package func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreamingMethod(request: request) return ServerResponse.Stream(single: response) } @@ -130,7 +131,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertServerCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .package ) } @@ -152,13 +154,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + public protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension namespaceA.ServiceA.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) { + extension namespaceA.ServiceA.StreamingServiceProtocol { + public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( for: namespaceA.ServiceA.Methods.outputStreamingMethod.descriptor, deserializer: ProtobufDeserializer(), @@ -170,13 +172,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + public protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod func outputStreamingMethod(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - public extension namespaceA.ServiceA.ServiceProtocol { - func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension namespaceA.ServiceA.ServiceProtocol { + public func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request)) return response } @@ -185,7 +187,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertServerCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } @@ -207,13 +210,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + package protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension namespaceA.ServiceA.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) { + extension namespaceA.ServiceA.StreamingServiceProtocol { + package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( for: namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.descriptor, deserializer: ProtobufDeserializer(), @@ -225,18 +228,19 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + package protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - public extension namespaceA.ServiceA.ServiceProtocol { + extension namespaceA.ServiceA.ServiceProtocol { } """ try self.assertServerCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .package ) } @@ -266,15 +270,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + internal protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Documentation for outputStreamingMethod func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension namespaceA.ServiceA.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) { + extension namespaceA.ServiceA.StreamingServiceProtocol { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor, deserializer: ProtobufDeserializer(), @@ -294,19 +298,19 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + internal protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Single /// Documentation for outputStreamingMethod func outputStreamingMethod(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - public extension namespaceA.ServiceA.ServiceProtocol { - func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension namespaceA.ServiceA.ServiceProtocol { + internal func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreamingMethod(request: request) return ServerResponse.Stream(single: response) } - func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request)) return response } @@ -315,7 +319,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { try assertServerCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .internal ) } @@ -337,13 +342,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension ServiceA.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) { + extension ServiceA.StreamingServiceProtocol { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( for: ServiceA.Methods.methodA.descriptor, deserializer: ProtobufDeserializer(), @@ -355,13 +360,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { + internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. - public extension ServiceA.ServiceProtocol { - func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension ServiceA.ServiceProtocol { + internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.methodA(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -370,7 +375,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { try self.assertServerCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .internal ) } @@ -390,40 +396,42 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + public protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension namespaceA.ServiceA.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) {} + extension namespaceA.ServiceA.StreamingServiceProtocol { + public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceA - protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {} + public protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {} /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - public extension namespaceA.ServiceA.ServiceProtocol { + extension namespaceA.ServiceA.ServiceProtocol { } /// Documentation for ServiceB - protocol namespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + public protocol namespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - public extension namespaceA.ServiceB.StreamingServiceProtocol { - func registerRPCs(with router: inout GRPCCore.RPCRouter) {} + extension namespaceA.ServiceB.StreamingServiceProtocol { + public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceB - protocol namespaceA_ServiceBServiceProtocol: namespaceA.ServiceB.StreamingServiceProtocol {} + public protocol namespaceA_ServiceBServiceProtocol: namespaceA.ServiceB.StreamingServiceProtocol {} /// Partial conformance to `namespaceA_ServiceBStreamingServiceProtocol`. - public extension namespaceA.ServiceB.ServiceProtocol { + extension namespaceA.ServiceB.ServiceProtocol { } """ try self.assertServerCodeTranslation( codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), - expectedSwift: expectedSwift + expectedSwift: expectedSwift, + accessLevel: .public ) } private func assertServerCodeTranslation( codeGenerationRequest: CodeGenerationRequest, - expectedSwift: String + expectedSwift: String, + accessLevel: SourceGenerator.Configuration.AccessLevel ) throws { - let translator = ServerCodeTranslator() + let translator = ServerCodeTranslator(accessLevel: accessLevel) let codeBlocks = try translator.translate(from: codeGenerationRequest) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 7bca92243..e21b2d646 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -41,25 +41,25 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum namespaceA { - enum ServiceA { - enum Methods { - enum MethodA { - typealias Input = NamespaceA_ServiceARequest - typealias Output = NamespaceA_ServiceAResponse - static let descriptor = MethodDescriptor( + public enum namespaceA { + public enum ServiceA { + public enum Methods { + public enum MethodA { + public typealias Input = NamespaceA_ServiceARequest + public typealias Output = NamespaceA_ServiceAResponse + public static let descriptor = MethodDescriptor( service: "namespaceA.ServiceA", method: "MethodA" ) } } - static let methods: [MethodDescriptor] = [ + public static let methods: [MethodDescriptor] = [ Methods.MethodA.descriptor ] - typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - typealias ClientProtocol = namespaceA_ServiceAClientProtocol - typealias Client = namespaceA_ServiceAClient + public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + public typealias ClientProtocol = namespaceA_ServiceAClientProtocol + public typealias Client = namespaceA_ServiceAClient } } """ @@ -68,7 +68,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .public ) } @@ -81,14 +82,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum namespaceA { - enum ServiceA { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - typealias ClientProtocol = namespaceA_ServiceAClientProtocol - typealias Client = namespaceA_ServiceAClient + public enum namespaceA { + public enum ServiceA { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + public typealias ClientProtocol = namespaceA_ServiceAClientProtocol + public typealias Client = namespaceA_ServiceAClient } } """ @@ -97,7 +98,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .public ) } @@ -110,12 +112,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum namespaceA { - enum ServiceA { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + public enum namespaceA { + public enum ServiceA { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol } } """ @@ -124,7 +126,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: false, - server: true + server: true, + accessLevel: .public ) } @@ -137,12 +140,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum namespaceA { - enum ServiceA { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias ClientProtocol = namespaceA_ServiceAClientProtocol - typealias Client = namespaceA_ServiceAClient + public enum namespaceA { + public enum ServiceA { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias ClientProtocol = namespaceA_ServiceAClientProtocol + public typealias Client = namespaceA_ServiceAClient } } """ @@ -151,7 +154,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: true, - server: false + server: false, + accessLevel: .public ) } @@ -164,10 +168,10 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum namespaceA { - enum ServiceA { - enum Methods {} - static let methods: [MethodDescriptor] = [] + public enum namespaceA { + public enum ServiceA { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] } } """ @@ -176,7 +180,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: false, - server: false + server: false, + accessLevel: .public ) } @@ -197,24 +202,24 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum ServiceA { - enum Methods { - enum MethodA { - typealias Input = ServiceARequest - typealias Output = ServiceAResponse - static let descriptor = MethodDescriptor( + public enum ServiceA { + public enum Methods { + public enum MethodA { + public typealias Input = ServiceARequest + public typealias Output = ServiceAResponse + public static let descriptor = MethodDescriptor( service: "ServiceA", method: "MethodA" ) } } - static let methods: [MethodDescriptor] = [ + public static let methods: [MethodDescriptor] = [ Methods.MethodA.descriptor ] - typealias StreamingServiceProtocol = ServiceAServiceStreamingProtocol - typealias ServiceProtocol = ServiceAServiceProtocol - typealias ClientProtocol = ServiceAClientProtocol - typealias Client = ServiceAClient + public typealias StreamingServiceProtocol = ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = ServiceAServiceProtocol + public typealias ClientProtocol = ServiceAClientProtocol + public typealias Client = ServiceAClient } """ @@ -222,7 +227,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .public ) } @@ -251,34 +257,34 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum namespaceA { - enum ServiceA { - enum Methods { - enum MethodA { - typealias Input = NamespaceA_ServiceARequest - typealias Output = NamespaceA_ServiceAResponse - static let descriptor = MethodDescriptor( + public enum namespaceA { + public enum ServiceA { + public enum Methods { + public enum MethodA { + public typealias Input = NamespaceA_ServiceARequest + public typealias Output = NamespaceA_ServiceAResponse + public static let descriptor = MethodDescriptor( service: "namespaceA.ServiceA", method: "MethodA" ) } - enum MethodB { - typealias Input = NamespaceA_ServiceARequest - typealias Output = NamespaceA_ServiceAResponse - static let descriptor = MethodDescriptor( + public enum MethodB { + public typealias Input = NamespaceA_ServiceARequest + public typealias Output = NamespaceA_ServiceAResponse + public static let descriptor = MethodDescriptor( service: "namespaceA.ServiceA", method: "MethodB" ) } } - static let methods: [MethodDescriptor] = [ + public static let methods: [MethodDescriptor] = [ Methods.MethodA.descriptor, Methods.MethodB.descriptor ] - typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - typealias ClientProtocol = namespaceA_ServiceAClientProtocol - typealias Client = namespaceA_ServiceAClient + public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + public typealias ClientProtocol = namespaceA_ServiceAClientProtocol + public typealias Client = namespaceA_ServiceAClient } } """ @@ -287,7 +293,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .public ) } @@ -300,14 +307,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum namespaceA { - enum ServiceA { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - typealias ClientProtocol = namespaceA_ServiceAClientProtocol - typealias Client = namespaceA_ServiceAClient + package enum namespaceA { + package enum ServiceA { + package enum Methods {} + package static let methods: [MethodDescriptor] = [] + package typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol + package typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + package typealias ClientProtocol = namespaceA_ServiceAClientProtocol + package typealias Client = namespaceA_ServiceAClient } } """ @@ -316,7 +323,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [service]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .package ) } @@ -337,22 +345,22 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ - enum namespacea { - enum AService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = namespacea_AServiceServiceStreamingProtocol - typealias ServiceProtocol = namespacea_AServiceServiceProtocol - typealias ClientProtocol = namespacea_AServiceClientProtocol - typealias Client = namespacea_AServiceClient + public enum namespacea { + public enum AService { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = namespacea_AServiceServiceStreamingProtocol + public typealias ServiceProtocol = namespacea_AServiceServiceProtocol + public typealias ClientProtocol = namespacea_AServiceClientProtocol + public typealias Client = namespacea_AServiceClient } - enum BService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = namespacea_BServiceServiceStreamingProtocol - typealias ServiceProtocol = namespacea_BServiceServiceProtocol - typealias ClientProtocol = namespacea_BServiceClientProtocol - typealias Client = namespacea_BServiceClient + public enum BService { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = namespacea_BServiceServiceStreamingProtocol + public typealias ServiceProtocol = namespacea_BServiceServiceProtocol + public typealias ClientProtocol = namespacea_BServiceClientProtocol + public typealias Client = namespacea_BServiceClient } } """ @@ -361,7 +369,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .public ) } @@ -382,21 +391,21 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ - enum AService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = AServiceServiceStreamingProtocol - typealias ServiceProtocol = AServiceServiceProtocol - typealias ClientProtocol = AServiceClientProtocol - typealias Client = AServiceClient + package enum AService { + package enum Methods {} + package static let methods: [MethodDescriptor] = [] + package typealias StreamingServiceProtocol = AServiceServiceStreamingProtocol + package typealias ServiceProtocol = AServiceServiceProtocol + package typealias ClientProtocol = AServiceClientProtocol + package typealias Client = AServiceClient } - enum BService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol - typealias ServiceProtocol = BServiceServiceProtocol - typealias ClientProtocol = BServiceClientProtocol - typealias Client = BServiceClient + package enum BService { + package enum Methods {} + package static let methods: [MethodDescriptor] = [] + package typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol + package typealias ServiceProtocol = BServiceServiceProtocol + package typealias ClientProtocol = BServiceClientProtocol + package typealias Client = BServiceClient } """ @@ -404,7 +413,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .package ) } @@ -425,24 +435,24 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ - enum anamespace { - enum AService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol - typealias ServiceProtocol = anamespace_AServiceServiceProtocol - typealias ClientProtocol = anamespace_AServiceClientProtocol - typealias Client = anamespace_AServiceClient + internal enum anamespace { + internal enum AService { + internal enum Methods {} + internal static let methods: [MethodDescriptor] = [] + internal typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol + internal typealias ServiceProtocol = anamespace_AServiceServiceProtocol + internal typealias ClientProtocol = anamespace_AServiceClientProtocol + internal typealias Client = anamespace_AServiceClient } } - enum bnamespace { - enum BService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = bnamespace_BServiceServiceStreamingProtocol - typealias ServiceProtocol = bnamespace_BServiceServiceProtocol - typealias ClientProtocol = bnamespace_BServiceClientProtocol - typealias Client = bnamespace_BServiceClient + internal enum bnamespace { + internal enum BService { + internal enum Methods {} + internal static let methods: [MethodDescriptor] = [] + internal typealias StreamingServiceProtocol = bnamespace_BServiceServiceStreamingProtocol + internal typealias ServiceProtocol = bnamespace_BServiceServiceProtocol + internal typealias ClientProtocol = bnamespace_BServiceClientProtocol + internal typealias Client = bnamespace_BServiceClient } } """ @@ -451,7 +461,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .internal ) } @@ -470,22 +481,22 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - enum BService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol - typealias ServiceProtocol = BServiceServiceProtocol - typealias ClientProtocol = BServiceClientProtocol - typealias Client = BServiceClient + public enum BService { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol + public typealias ServiceProtocol = BServiceServiceProtocol + public typealias ClientProtocol = BServiceClientProtocol + public typealias Client = BServiceClient } - enum anamespace { - enum AService { - enum Methods {} - static let methods: [MethodDescriptor] = [] - typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol - typealias ServiceProtocol = anamespace_AServiceServiceProtocol - typealias ClientProtocol = anamespace_AServiceClientProtocol - typealias Client = anamespace_AServiceClient + public enum anamespace { + public enum AService { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol + public typealias ServiceProtocol = anamespace_AServiceServiceProtocol + public typealias ClientProtocol = anamespace_AServiceClientProtocol + public typealias Client = anamespace_AServiceClient } } """ @@ -494,7 +505,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), expectedSwift: expectedSwift, client: true, - server: true + server: true, + accessLevel: .public ) } } @@ -504,9 +516,10 @@ extension TypealiasTranslatorSnippetBasedTests { codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, client: Bool, - server: Bool + server: Bool, + accessLevel: SourceGenerator.Configuration.AccessLevel ) throws { - let translator = TypealiasTranslator(client: client, server: server) + let translator = TypealiasTranslator(client: client, server: server, accessLevel: accessLevel) let codeBlocks = try translator.translate(from: codeGenerationRequest) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) From 917f37de0bf6de79ff05410db5d47b7c6b891843 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 18 Jan 2024 08:01:14 +0000 Subject: [PATCH 205/580] Add server connection state machine (#1760) Motivation: The server pipeline needs a channel handler which manages the lifecycle of TCP connections. This includes: policing keepalive pings sent by the client, managing graceful shutdown, shutting down idle connections, and shutting down connections which have lived for too long. Much of this logic can be abstracted into a state machine. This change add that state machine. Modifications: - Add a server connection handler state machine which tracks the graceful shutdown state and polices keepalive pings sent by the client. - Replace a few READMEs with empty Swift files to suppress a few build warnings. Result: State machine for managing server connections. --- ...ServerConnectionHandler+StateMachine.swift | 365 ++++++++++++++++++ .../Connection/ServerConnectionHandler.swift | 19 + .../GRPCHTTP2TransportNIOPosix/Empty.swift | 2 - Sources/GRPCHTTP2TransportNIOPosix/README.md | 1 - .../Empty.swift | 15 + .../README.md | 1 - ...rConnectionHandler+StateMachineTests.swift | 240 ++++++++++++ 7 files changed, 639 insertions(+), 4 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler+StateMachine.swift create mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift rename Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift => Sources/GRPCHTTP2TransportNIOPosix/Empty.swift (95%) delete mode 100644 Sources/GRPCHTTP2TransportNIOPosix/README.md create mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/Empty.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/README.md create mode 100644 Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionHandler+StateMachineTests.swift diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler+StateMachine.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler+StateMachine.swift new file mode 100644 index 000000000..5a10e41e0 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler+StateMachine.swift @@ -0,0 +1,365 @@ +/* + * 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 NIOCore +import NIOHTTP2 + +extension ServerConnectionHandler { + /// Tracks the state of TCP connections at the server. + /// + /// The state machine manages the state for the graceful shutdown procedure as well as policing + /// client-side keep alive. + struct StateMachine { + /// Current state. + private var state: State + + /// Opaque data sent to the client in a PING frame after emitting the first GOAWAY frame + /// as part of graceful shutdown. + private let goAwayPingData: HTTP2PingData + + /// Create a new state machine. + /// + /// - Parameters: + /// - allowKeepAliveWithoutCalls: Whether the client is permitted to send keep alive pings + /// when there are no active calls. + /// - minPingReceiveIntervalWithoutCalls: The minimum time interval required between keep + /// alive pings when there are no active calls. + /// - goAwayPingData: Opaque data sent to the client in a PING frame when the server + /// initiates graceful shutdown. + init( + allowKeepAliveWithoutCalls: Bool, + minPingReceiveIntervalWithoutCalls: TimeAmount, + goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: .random(in: .min ... .max)) + ) { + let keepAlive = KeepAlive( + allowWithoutCalls: allowKeepAliveWithoutCalls, + minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls + ) + + self.state = .active(State.Active(keepAlive: keepAlive)) + self.goAwayPingData = goAwayPingData + } + + /// Record that the stream with the given ID has been opened. + mutating func streamOpened(_ id: HTTP2StreamID) { + switch self.state { + case .active(var state): + state.lastStreamID = id + let (inserted, _) = state.openStreams.insert(id) + assert(inserted, "Can't open stream \(Int(id)), it's already open") + self.state = .active(state) + + case .closing(var state): + state.lastStreamID = id + let (inserted, _) = state.openStreams.insert(id) + assert(inserted, "Can't open stream \(Int(id)), it's already open") + self.state = .closing(state) + + case .closed: + () + } + } + + enum OnStreamClosed: Equatable { + /// Start the idle timer, after which the connection should be closed gracefully. + case startIdleTimer + /// Close the connection. + case close + /// Do nothing. + case none + } + + /// Record that the stream with the given ID has been closed. + mutating func streamClosed(_ id: HTTP2StreamID) -> OnStreamClosed { + let onStreamClosed: OnStreamClosed + + switch self.state { + case .active(var state): + let removedID = state.openStreams.remove(id) + assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") + onStreamClosed = state.openStreams.isEmpty ? .startIdleTimer : .none + self.state = .active(state) + + case .closing(var state): + let removedID = state.openStreams.remove(id) + assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") + // If the second GOAWAY hasn't been sent it isn't safe to close if there are no open + // streams: the client may have opened a stream which the server doesn't know about yet. + let canClose = state.sentSecondGoAway && state.openStreams.isEmpty + onStreamClosed = canClose ? .close : .none + self.state = .closing(state) + + case .closed: + onStreamClosed = .none + } + + return onStreamClosed + } + + enum OnPing: Equatable { + /// Send a GOAWAY frame with the code "enhance your calm" and immediately close the connection. + case enhanceYourCalmThenClose(HTTP2StreamID) + /// Acknowledge the ping. + case sendAck + /// Ignore the ping. + case none + } + + /// Received a ping with the given data. + /// + /// - Parameters: + /// - time: The time at which the ping was received. + /// - data: The data sent with the ping. + mutating func receivedPing(atTime time: NIODeadline, data: HTTP2PingData) -> OnPing { + let onPing: OnPing + + switch self.state { + case .active(var state): + let tooManyPings = state.keepAlive.receivedPing( + atTime: time, + hasOpenStreams: !state.openStreams.isEmpty + ) + + if tooManyPings { + onPing = .enhanceYourCalmThenClose(state.lastStreamID) + self.state = .closed + } else { + onPing = .sendAck + self.state = .active(state) + } + + case .closing(var state): + let tooManyPings = state.keepAlive.receivedPing( + atTime: time, + hasOpenStreams: !state.openStreams.isEmpty + ) + + if tooManyPings { + onPing = .enhanceYourCalmThenClose(state.lastStreamID) + self.state = .closed + } else { + onPing = .sendAck + self.state = .closing(state) + } + + case .closed: + onPing = .none + } + + return onPing + } + + enum OnPingAck: Equatable { + /// Send a GOAWAY frame with no error and the given last stream ID, optionally closing the + /// connection immediately afterwards. + case sendGoAway(lastStreamID: HTTP2StreamID, close: Bool) + /// Ignore the ack. + case none + } + + /// Received a PING frame with the 'ack' flag set. + mutating func receivedPingAck(data: HTTP2PingData) -> OnPingAck { + let onPingAck: OnPingAck + + switch self.state { + case .closing(var state): + // If only one GOAWAY has been sent and the data matches the data from the GOAWAY ping then + // the server should send another GOAWAY ratcheting down the last stream ID. If no streams + // are open then the server can close the connection immediately after, otherwise it must + // wait until all streams are closed. + if !state.sentSecondGoAway, data == self.goAwayPingData { + state.sentSecondGoAway = true + + if state.openStreams.isEmpty { + self.state = .closed + onPingAck = .sendGoAway(lastStreamID: state.lastStreamID, close: true) + } else { + self.state = .closing(state) + onPingAck = .sendGoAway(lastStreamID: state.lastStreamID, close: false) + } + } else { + onPingAck = .none + } + + case .active, .closed: + onPingAck = .none + } + + return onPingAck + } + + enum OnStartGracefulShutdown: Equatable { + /// Initiate graceful shutdown by sending a GOAWAY frame with the last stream ID set as the max + /// stream ID and no error. Follow it immediately with a PING frame with the given data. + case sendGoAwayAndPing(HTTP2PingData) + /// Ignore the request to start graceful shutdown. + case none + } + + /// Request that the connection begins graceful shutdown. + mutating func startGracefulShutdown() -> OnStartGracefulShutdown { + let onStartGracefulShutdown: OnStartGracefulShutdown + + switch self.state { + case .active(let state): + self.state = .closing(State.Closing(from: state)) + onStartGracefulShutdown = .sendGoAwayAndPing(self.goAwayPingData) + + case .closing, .closed: + onStartGracefulShutdown = .none + } + + return onStartGracefulShutdown + } + + /// Reset the state of keep-alive policing. + mutating func resetKeepAliveState() { + switch self.state { + case .active(var state): + state.keepAlive.reset() + self.state = .active(state) + + case .closing(var state): + state.keepAlive.reset() + self.state = .closing(state) + + case .closed: + () + } + } + + /// Marks the state as closed. + mutating func markClosed() { + self.state = .closed + } + } +} + +extension ServerConnectionHandler.StateMachine { + fileprivate struct KeepAlive { + /// Allow the client to send keep alive pings when there are no active calls. + private let allowWithoutCalls: Bool + + /// The minimum time interval which pings may be received at when there are no active calls. + private let minPingReceiveIntervalWithoutCalls: TimeAmount + + /// The maximum number of "bad" pings sent by the client the server tolerates before closing + /// the connection. + private let maxPingStrikes: Int + + /// The number of "bad" pings sent by the client. This can be reset when the server sends + /// DATA or HEADERS frames. + /// + /// Ping strikes account for pings being occasionally being used for purposes other than keep + /// alive (a low number of strikes is therefore expected and okay). + private var pingStrikes: Int + + /// The last time a valid ping happened. This may be in the distant past if there is no such + /// time (for example the connection is new and there are no active calls). + /// + /// Note: `distantPast` isn't used to indicate no previous valid ping as `NIODeadline` uses + /// the monotonic clock on Linux which uses an undefined starting point and in some cases isn't + /// always that distant. + private var lastValidPingTime: NIODeadline? + + init(allowWithoutCalls: Bool, minPingReceiveIntervalWithoutCalls: TimeAmount) { + self.allowWithoutCalls = allowWithoutCalls + self.minPingReceiveIntervalWithoutCalls = minPingReceiveIntervalWithoutCalls + self.maxPingStrikes = 2 + self.pingStrikes = 0 + self.lastValidPingTime = nil + } + + /// Reset ping strikes and the time of the last valid ping. + mutating func reset() { + self.lastValidPingTime = nil + self.pingStrikes = 0 + } + + /// Returns whether the client has sent too many pings. + mutating func receivedPing(atTime time: NIODeadline, hasOpenStreams: Bool) -> Bool { + let interval: TimeAmount + + if hasOpenStreams || self.allowWithoutCalls { + interval = self.minPingReceiveIntervalWithoutCalls + } else { + // If there are no open streams and keep alive pings aren't allowed without calls then + // use an interval of two hours. + // + // This comes from gRFC A8: https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md + interval = .hours(2) + } + + // If there's no last ping time then the first is acceptable. + let isAcceptablePing = self.lastValidPingTime.map { $0 + interval <= time } ?? true + let tooManyPings: Bool + + if isAcceptablePing { + self.lastValidPingTime = time + tooManyPings = false + } else { + self.pingStrikes += 1 + tooManyPings = self.pingStrikes > self.maxPingStrikes + } + + return tooManyPings + } + } +} + +extension ServerConnectionHandler.StateMachine { + fileprivate enum State { + /// The connection is active. + struct Active { + /// The number of open streams. + var openStreams: Set + /// The ID of the most recently opened stream (zero indicates no streams have been opened yet). + var lastStreamID: HTTP2StreamID + /// The state of keep alive. + var keepAlive: KeepAlive + + init(keepAlive: KeepAlive) { + self.openStreams = [] + self.lastStreamID = .rootStream + self.keepAlive = keepAlive + } + } + + /// The connection is closing gracefully, an initial GOAWAY frame has been sent (with the + /// last stream ID set to max). + struct Closing { + /// The number of open streams. + var openStreams: Set + /// The ID of the most recently opened stream (zero indicates no streams have been opened yet). + var lastStreamID: HTTP2StreamID + /// The state of keep alive. + var keepAlive: KeepAlive + /// Whether the second GOAWAY frame has been sent with a lower stream ID. + var sentSecondGoAway: Bool + + init(from state: Active) { + self.openStreams = state.openStreams + self.lastStreamID = state.lastStreamID + self.keepAlive = state.keepAlive + self.sentSecondGoAway = false + } + } + + case active(Active) + case closing(Closing) + case closed + } +} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift new file mode 100644 index 000000000..52dada671 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift @@ -0,0 +1,19 @@ +/* + * 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. + */ + +// Temporary namespace. Will be replaced with a channel handler. +enum ServerConnectionHandler { +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift b/Sources/GRPCHTTP2TransportNIOPosix/Empty.swift similarity index 95% rename from Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift rename to Sources/GRPCHTTP2TransportNIOPosix/Empty.swift index 3630df68b..43d5caa47 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCHTTP2CoreTests.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/Empty.swift @@ -13,5 +13,3 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -// Add tests to this package. diff --git a/Sources/GRPCHTTP2TransportNIOPosix/README.md b/Sources/GRPCHTTP2TransportNIOPosix/README.md deleted file mode 100644 index 635ca3410..000000000 --- a/Sources/GRPCHTTP2TransportNIOPosix/README.md +++ /dev/null @@ -1 +0,0 @@ -# gRPC HTTP2 Transport NIO POSIX diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/Empty.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/Empty.swift new file mode 100644 index 000000000..43d5caa47 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/Empty.swift @@ -0,0 +1,15 @@ +/* + * 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. + */ diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/README.md b/Sources/GRPCHTTP2TransportNIOTransportServices/README.md deleted file mode 100644 index 46303d13a..000000000 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/README.md +++ /dev/null @@ -1 +0,0 @@ -# gRPC HTTP2 Transport NIO Transport Services diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionHandler+StateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionHandler+StateMachineTests.swift new file mode 100644 index 000000000..ec4671c8a --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionHandler+StateMachineTests.swift @@ -0,0 +1,240 @@ +/* + * 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 NIOCore +import NIOHTTP2 +import XCTest + +@testable import GRPCHTTP2Core + +final class ServerConnectionHandlerStateMachineTests: XCTestCase { + private func makeStateMachine( + allowKeepAliveWithoutCalls: Bool = false, + minPingReceiveIntervalWithoutCalls: TimeAmount = .minutes(5), + goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: 42) + ) -> ServerConnectionHandler.StateMachine { + return .init( + allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls, + minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls, + goAwayPingData: goAwayPingData + ) + } + + func testCloseAllStreamsWhenActive() { + var state = self.makeStateMachine() + state.streamOpened(1) + XCTAssertEqual(state.streamClosed(1), .startIdleTimer) + } + + func testCloseSomeStreamsWhenActive() { + var state = self.makeStateMachine() + state.streamOpened(1) + state.streamOpened(2) + XCTAssertEqual(state.streamClosed(2), .none) + } + + func testOpenAndCloseStreamWhenClosed() { + var state = self.makeStateMachine() + state.markClosed() + state.streamOpened(1) + XCTAssertEqual(state.streamClosed(1), .none) + } + + func testGracefulShutdownWhenNoOpenStreams() { + let pingData = HTTP2PingData(withInteger: 42) + var state = self.makeStateMachine(goAwayPingData: pingData) + XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) + } + + func testGracefulShutdownWhenClosing() { + let pingData = HTTP2PingData(withInteger: 42) + var state = self.makeStateMachine(goAwayPingData: pingData) + XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) + XCTAssertEqual(state.startGracefulShutdown(), .none) + } + + func testGracefulShutdownWhenClosed() { + let pingData = HTTP2PingData(withInteger: 42) + var state = self.makeStateMachine(goAwayPingData: pingData) + state.markClosed() + XCTAssertEqual(state.startGracefulShutdown(), .none) + } + + func testReceiveAckForGoAwayPingWhenStreamsOpenedBeforeShutdownOnly() { + let pingData = HTTP2PingData(withInteger: 42) + var state = self.makeStateMachine(goAwayPingData: pingData) + state.streamOpened(1) + XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) + XCTAssertEqual( + state.receivedPingAck(data: pingData), + .sendGoAway(lastStreamID: 1, close: false) + ) + } + + func testReceiveAckForGoAwayPingWhenStreamsOpenedBeforeAck() { + let pingData = HTTP2PingData(withInteger: 42) + var state = self.makeStateMachine(goAwayPingData: pingData) + XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) + state.streamOpened(1) + XCTAssertEqual( + state.receivedPingAck(data: pingData), + .sendGoAway(lastStreamID: 1, close: false) + ) + } + + func testReceiveAckForGoAwayPingWhenNoOpenStreams() { + let pingData = HTTP2PingData(withInteger: 42) + var state = self.makeStateMachine(goAwayPingData: pingData) + XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) + XCTAssertEqual( + state.receivedPingAck(data: pingData), + .sendGoAway(lastStreamID: .rootStream, close: true) + ) + } + + func testReceiveAckNotForGoAwayPing() { + let pingData = HTTP2PingData(withInteger: 42) + var state = self.makeStateMachine(goAwayPingData: pingData) + XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) + + let otherPingData = HTTP2PingData(withInteger: 0) + XCTAssertEqual(state.receivedPingAck(data: otherPingData), .none) + } + + func testReceivePingAckWhenActive() { + var state = self.makeStateMachine() + XCTAssertEqual(state.receivedPingAck(data: HTTP2PingData()), .none) + } + + func testReceivePingAckWhenClosed() { + var state = self.makeStateMachine() + state.markClosed() + XCTAssertEqual(state.receivedPingAck(data: HTTP2PingData()), .none) + } + + func testGracefulShutdownFlow() { + var state = self.makeStateMachine() + // Open a few streams. + state.streamOpened(1) + state.streamOpened(2) + + switch state.startGracefulShutdown() { + case .sendGoAwayAndPing(let pingData): + // Open another stream and then receive the ping ack. + state.streamOpened(3) + XCTAssertEqual( + state.receivedPingAck(data: pingData), + .sendGoAway(lastStreamID: 3, close: false) + ) + case .none: + XCTFail("Expected '.sendGoAwayAndPing'") + } + + // Both GOAWAY frames have been sent. Start closing streams. + XCTAssertEqual(state.streamClosed(1), .none) + XCTAssertEqual(state.streamClosed(2), .none) + XCTAssertEqual(state.streamClosed(3), .close) + } + + func testGracefulShutdownWhenNoOpenStreamsBeforeSecondGoAway() { + var state = self.makeStateMachine() + // Open a stream. + state.streamOpened(1) + + switch state.startGracefulShutdown() { + case .sendGoAwayAndPing(let pingData): + // Close the stream. This shouldn't lead to a close. + XCTAssertEqual(state.streamClosed(1), .none) + // Only on receiving the ack do we send a GOAWAY and close. + XCTAssertEqual( + state.receivedPingAck(data: pingData), + .sendGoAway(lastStreamID: 1, close: true) + ) + case .none: + XCTFail("Expected '.sendGoAwayAndPing'") + } + } + + func testPingStrikeUsingMinReceiveInterval( + state: inout ServerConnectionHandler.StateMachine, + interval: TimeAmount, + expectedID id: HTTP2StreamID + ) { + var time = NIODeadline.now() + let data = HTTP2PingData() + + // The first ping is never a strike. + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) + + // Advance time by just less than the interval and get two strikes. + time = time + interval - .nanoseconds(1) + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) + + // Advance time so that we're at one interval since the last valid ping. This isn't a + // strike (but doesn't reset strikes) and updates the last valid ping time. + time = time + .nanoseconds(1) + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) + + // Now get a third and final strike. + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .enhanceYourCalmThenClose(id)) + } + + func testPingStrikesWhenKeepAliveIsNotPermittedWithoutCalls() { + let initialState = self.makeStateMachine( + allowKeepAliveWithoutCalls: false, + minPingReceiveIntervalWithoutCalls: .minutes(5) + ) + + var state = initialState + state.streamOpened(1) + self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 1) + + state = initialState + self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .hours(2), expectedID: 0) + } + + func testPingStrikesWhenKeepAliveIsPermittedWithoutCalls() { + var state = self.makeStateMachine( + allowKeepAliveWithoutCalls: true, + minPingReceiveIntervalWithoutCalls: .minutes(5) + ) + + self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 0) + } + + func testResetPingStrikeState() { + var state = self.makeStateMachine( + allowKeepAliveWithoutCalls: true, + minPingReceiveIntervalWithoutCalls: .minutes(5) + ) + + var time = NIODeadline.now() + let data = HTTP2PingData() + + // The first ping is never a strike. + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) + + // Advance time by less than the interval and get two strikes. + time = time + .minutes(1) + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) + XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) + + // Reset the ping strike state and test ping strikes as normal. + state.resetKeepAliveState() + self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 0) + } +} From 378b6e89933e1118a58a611f3c78b55db7e0f9a4 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 18 Jan 2024 13:23:33 +0000 Subject: [PATCH 206/580] Add client and server tracing interceptors (#1756) --- Package.swift | 28 +- .../ClientTracingInterceptor.swift | 140 ++++++++ Sources/GRPCInterceptors/HookedWriter.swift | 40 +++ .../OnFinishAsyncSequence.swift | 57 +++ .../ServerTracingInterceptor.swift | 148 ++++++++ .../TracingInterceptorTests.swift | 333 ++++++++++++++++++ .../TracingTestsUtilities.swift | 182 ++++++++++ 7 files changed, 927 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCInterceptors/ClientTracingInterceptor.swift create mode 100644 Sources/GRPCInterceptors/HookedWriter.swift create mode 100644 Sources/GRPCInterceptors/OnFinishAsyncSequence.swift create mode 100644 Sources/GRPCInterceptors/ServerTracingInterceptor.swift create mode 100644 Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift create mode 100644 Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift diff --git a/Package.swift b/Package.swift index 201ba8e56..cd9be155f 100644 --- a/Package.swift +++ b/Package.swift @@ -72,6 +72,10 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0" ), + .package( + url: "https://github.com/apple/swift-distributed-tracing.git", + from: "1.0.0" + ), ].appending( .package( url: "https://github.com/apple/swift-nio-ssl.git", @@ -131,9 +135,11 @@ extension Target.Dependency { ) static let dequeModule: Self = .product(name: "DequeModule", package: "swift-collections") static let atomics: Self = .product(name: "Atomics", package: "swift-atomics") + static let tracing: Self = .product(name: "Tracing", package: "swift-distributed-tracing") static let grpcCore: Self = .target(name: "GRPCCore") static let grpcInProcessTransport: Self = .target(name: "GRPCInProcessTransport") + static let grpcInterceptors: Self = .target(name: "GRPCInterceptors") static let grpcHTTP2Core: Self = .target(name: "GRPCHTTP2Core") static let grpcHTTP2TransportNIOPosix: Self = .target(name: "GRPCHTTP2TransportNIOPosix") static let grpcHTTP2TransportNIOTransportServices: Self = .target(name: "GRPCHTTP2TransportNIOTransportServices") @@ -181,6 +187,14 @@ extension Target { ] ) + static let grpcInterceptors: Target = .target( + name: "GRPCInterceptors", + dependencies: [ + .grpcCore, + .tracing + ] + ) + static let grpcHTTP2Core: Target = .target( name: "GRPCHTTP2Core", dependencies: [ @@ -274,10 +288,20 @@ extension Target { name: "GRPCInProcessTransportTests", dependencies: [ .grpcCore, - .grpcInProcessTransport, + .grpcInProcessTransport ] ) + static let grpcInterceptorsTests: Target = .testTarget( + name: "GRPCInterceptorsTests", + dependencies: [ + .grpcCore, + .tracing, + .nioCore, + .grpcInterceptors + ] + ) + static let grpcHTTP2CoreTests: Target = .testTarget( name: "GRPCHTTP2CoreTests", dependencies: [ @@ -638,6 +662,7 @@ let package = Package( .grpcCore, .grpcInProcessTransport, .grpcCodeGen, + .grpcInterceptors, .grpcHTTP2Core, .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, @@ -646,6 +671,7 @@ let package = Package( .grpcCoreTests, .grpcInProcessTransportTests, .grpcCodeGenTests, + .grpcInterceptorsTests, .grpcHTTP2CoreTests, .grpcHTTP2TransportNIOPosixTests, .grpcHTTP2TransportNIOTransportServicesTests diff --git a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift new file mode 100644 index 000000000..2bb9395c5 --- /dev/null +++ b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift @@ -0,0 +1,140 @@ +/* + * 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 Tracing + +/// A client interceptor that injects tracing information into the request. +/// +/// The tracing information is taken from the current `ServiceContext`, and injected into the request's +/// metadata. It will then be picked up by the server-side ``ServerTracingInterceptor``. +/// +/// For more information, refer to the documentation for `swift-distributed-tracing`. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct ClientTracingInterceptor: ClientInterceptor { + private let injector: ClientRequestInjector + private let emitEventOnEachWrite: Bool + + /// Create a new instance of a ``ClientTracingInterceptor``. + /// + /// - Parameter emitEventOnEachWrite: If `true`, each request part sent and response part + /// received will be recorded as a separate event in a tracing span. Otherwise, only the request/response + /// start and end will be recorded as events. + public init(emitEventOnEachWrite: Bool = false) { + self.injector = ClientRequestInjector() + self.emitEventOnEachWrite = emitEventOnEachWrite + } + + /// This interceptor will inject as the request's metadata whatever `ServiceContext` key-value pairs + /// have been made available by the tracing implementation bootstrapped in your application. + /// + /// Which key-value pairs are injected will depend on the specific tracing implementation + /// that has been configured when bootstrapping `swift-distributed-tracing` in your application. + public func intercept( + request: ClientRequest.Stream, + context: ClientInterceptorContext, + next: @Sendable (ClientRequest.Stream, ClientInterceptorContext) async throws -> + ClientResponse.Stream + ) async throws -> ClientResponse.Stream where Input: Sendable, Output: Sendable { + var request = request + let tracer = InstrumentationSystem.tracer + let serviceContext = ServiceContext.current ?? .topLevel + + tracer.inject( + serviceContext, + into: &request.metadata, + using: self.injector + ) + + return try await tracer.withSpan( + context.descriptor.fullyQualifiedMethod, + context: serviceContext, + ofKind: .client + ) { span in + span.addEvent("Request started") + + if self.emitEventOnEachWrite { + let wrappedProducer = request.producer + request.producer = { writer in + let eventEmittingWriter = HookedWriter( + wrapping: writer, + beforeEachWrite: { + span.addEvent("Sending request part") + }, + afterEachWrite: { + span.addEvent("Sent request part") + } + ) + + do { + try await wrappedProducer(RPCWriter(wrapping: eventEmittingWriter)) + } catch { + span.addEvent("Error encountered") + throw error + } + + span.addEvent("Request end") + } + } + + var response: ClientResponse.Stream + do { + response = try await next(request, context) + } catch { + span.addEvent("Error encountered") + throw error + } + + switch response.accepted { + case .success(var success): + if self.emitEventOnEachWrite { + let onEachPartRecordingSequence = success.bodyParts.map { element in + span.addEvent("Received response part") + return element + } + let onFinishRecordingSequence = OnFinishAsyncSequence( + wrapping: onEachPartRecordingSequence + ) { + span.addEvent("Received response end") + } + success.bodyParts = RPCAsyncSequence(wrapping: onFinishRecordingSequence) + response.accepted = .success(success) + } else { + let onFinishRecordingSequence = OnFinishAsyncSequence(wrapping: success.bodyParts) { + span.addEvent("Received response end") + } + success.bodyParts = RPCAsyncSequence(wrapping: onFinishRecordingSequence) + response.accepted = .success(success) + } + case .failure: + span.addEvent("Received error response") + } + + return response + } + } +} + +/// An injector responsible for injecting the required instrumentation keys from the `ServiceContext` into +/// the request metadata. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +struct ClientRequestInjector: Instrumentation.Injector { + typealias Carrier = Metadata + + func inject(_ value: String, forKey key: String, into carrier: inout Carrier) { + carrier.addString(value, forKey: key) + } +} diff --git a/Sources/GRPCInterceptors/HookedWriter.swift b/Sources/GRPCInterceptors/HookedWriter.swift new file mode 100644 index 000000000..b4bb52eed --- /dev/null +++ b/Sources/GRPCInterceptors/HookedWriter.swift @@ -0,0 +1,40 @@ +/* + * 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 Tracing + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +struct HookedWriter: RPCWriterProtocol { + private let writer: any RPCWriterProtocol + private let beforeEachWrite: @Sendable () -> Void + private let afterEachWrite: @Sendable () -> Void + + init( + wrapping other: some RPCWriterProtocol, + beforeEachWrite: @Sendable @escaping () -> Void, + afterEachWrite: @Sendable @escaping () -> Void + ) { + self.writer = other + self.beforeEachWrite = beforeEachWrite + self.afterEachWrite = afterEachWrite + } + + func write(contentsOf elements: some Sequence) async throws { + self.beforeEachWrite() + try await self.writer.write(contentsOf: elements) + self.afterEachWrite() + } +} diff --git a/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift b/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift new file mode 100644 index 000000000..311e1fcd8 --- /dev/null +++ b/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift @@ -0,0 +1,57 @@ +/* + * 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +struct OnFinishAsyncSequence: AsyncSequence, Sendable { + private let _makeAsyncIterator: @Sendable () -> AsyncIterator + + init( + wrapping other: S, + onFinish: @escaping () -> Void + ) where S.Element == Element { + self._makeAsyncIterator = { + AsyncIterator(wrapping: other.makeAsyncIterator(), onFinish: onFinish) + } + } + + func makeAsyncIterator() -> AsyncIterator { + self._makeAsyncIterator() + } + + struct AsyncIterator: AsyncIteratorProtocol { + private var iterator: any AsyncIteratorProtocol + private var onFinish: (() -> Void)? + + fileprivate init( + wrapping other: Iterator, + onFinish: @escaping () -> Void + ) where Iterator: AsyncIteratorProtocol, Iterator.Element == Element { + self.iterator = other + self.onFinish = onFinish + } + + mutating func next() async throws -> Element? { + let elem = try await self.iterator.next() + + if elem == nil { + self.onFinish?() + self.onFinish = nil + } + + return elem as? Element + } + } +} diff --git a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift new file mode 100644 index 000000000..d91da7051 --- /dev/null +++ b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift @@ -0,0 +1,148 @@ +/* + * 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 Tracing + +/// A server interceptor that extracts tracing information from the request. +/// +/// The extracted tracing information is made available to user code via the current `ServiceContext`. +/// For more information, refer to the documentation for `swift-distributed-tracing`. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct ServerTracingInterceptor: ServerInterceptor { + private let extractor: ServerRequestExtractor + private let emitEventOnEachWrite: Bool + + /// Create a new instance of a ``ServerTracingInterceptor``. + /// + /// - Parameter emitEventOnEachWrite: If `true`, each response part sent and request part + /// received will be recorded as a separate event in a tracing span. Otherwise, only the request/response + /// start and end will be recorded as events. + public init(emitEventOnEachWrite: Bool = false) { + self.extractor = ServerRequestExtractor() + self.emitEventOnEachWrite = emitEventOnEachWrite + } + + /// This interceptor will extract whatever `ServiceContext` key-value pairs have been inserted into the + /// request's metadata, and will make them available to user code via the `ServiceContext/current` + /// context. + /// + /// Which key-value pairs are extracted and made available will depend on the specific tracing implementation + /// that has been configured when bootstrapping `swift-distributed-tracing` in your application. + public func intercept( + request: ServerRequest.Stream, + context: ServerInterceptorContext, + next: @Sendable (ServerRequest.Stream, ServerInterceptorContext) async throws -> + ServerResponse.Stream + ) async throws -> ServerResponse.Stream where Input: Sendable, Output: Sendable { + var serviceContext = ServiceContext.topLevel + let tracer = InstrumentationSystem.tracer + + tracer.extract( + request.metadata, + into: &serviceContext, + using: self.extractor + ) + + return try await ServiceContext.withValue(serviceContext) { + try await tracer.withSpan( + context.descriptor.fullyQualifiedMethod, + context: serviceContext, + ofKind: .server + ) { span in + span.addEvent("Received request start") + + var request = request + + if self.emitEventOnEachWrite { + request.messages = RPCAsyncSequence( + wrapping: request.messages.map { element in + span.addEvent("Received request part") + return element + } + ) + } + + var response = try await next(request, context) + + span.addEvent("Received request end") + + switch response.accepted { + case .success(var success): + let wrappedProducer = success.producer + + if self.emitEventOnEachWrite { + success.producer = { writer in + let eventEmittingWriter = HookedWriter( + wrapping: writer, + beforeEachWrite: { + span.addEvent("Sending response part") + }, + afterEachWrite: { + span.addEvent("Sent response part") + } + ) + + let wrappedResult: Metadata + do { + wrappedResult = try await wrappedProducer( + RPCWriter(wrapping: eventEmittingWriter) + ) + } catch { + span.addEvent("Error encountered") + throw error + } + + span.addEvent("Sent response end") + return wrappedResult + } + } else { + success.producer = { writer in + let wrappedResult: Metadata + do { + wrappedResult = try await wrappedProducer(writer) + } catch { + span.addEvent("Error encountered") + throw error + } + + span.addEvent("Sent response end") + return wrappedResult + } + } + + response = .init(accepted: .success(success)) + case .failure: + span.addEvent("Sent error response") + } + + return response + } + } + } +} + +/// An extractor responsible for extracting the required instrumentation keys from request metadata. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +struct ServerRequestExtractor: Instrumentation.Extractor { + typealias Carrier = Metadata + + func extract(key: String, from carrier: Carrier) -> String? { + var values = carrier[stringValues: key].makeIterator() + // There should only be one value for each key. If more, pick just one. + return values.next() + } +} diff --git a/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift b/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift new file mode 100644 index 000000000..98fbb2558 --- /dev/null +++ b/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift @@ -0,0 +1,333 @@ +/* + * 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 Tracing +import XCTest + +@testable import GRPCInterceptors + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class TracingInterceptorTests: XCTestCase { + override class func setUp() { + InstrumentationSystem.bootstrap(TestTracer()) + } + + #if swift(>=5.8) // Compiling these tests fails in 5.7 + func testClientInterceptor() async throws { + var serviceContext = ServiceContext.topLevel + let traceIDString = UUID().uuidString + let interceptor = ClientTracingInterceptor(emitEventOnEachWrite: false) + let (stream, continuation) = AsyncStream.makeStream() + serviceContext.traceID = traceIDString + + try await ServiceContext.withValue(serviceContext) { + let methodDescriptor = MethodDescriptor( + service: "TracingInterceptorTests", + method: "testClientInterceptor" + ) + let response = try await interceptor.intercept( + request: .init(producer: { writer in + try await writer.write(contentsOf: ["request1"]) + try await writer.write(contentsOf: ["request2"]) + }), + context: .init(descriptor: methodDescriptor) + ) { stream, _ in + // Assert the metadata contains the injected context key-value. + XCTAssertEqual(stream.metadata, ["trace-id": "\(traceIDString)"]) + + // Write into the response stream to make sure the `producer` closure's called. + let writer = RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) + try await stream.producer(writer) + continuation.finish() + + return .init( + metadata: [], + bodyParts: .init( + wrapping: AsyncStream { cont in + cont.yield(.message(["response"])) + cont.finish() + } + ) + ) + } + + var streamIterator = stream.makeAsyncIterator() + var element = await streamIterator.next() + XCTAssertEqual(element, "request1") + element = await streamIterator.next() + XCTAssertEqual(element, "request2") + element = await streamIterator.next() + XCTAssertNil(element) + + var messages = response.messages.makeAsyncIterator() + var message = try await messages.next() + XCTAssertEqual(message, ["response"]) + message = try await messages.next() + XCTAssertNil(message) + + let tracer = InstrumentationSystem.tracer as! TestTracer + XCTAssertEqual( + tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { + $0.name + }, + [ + "Request started", + "Received response end", + ] + ) + } + } + + func testClientInterceptorAllEventsRecorded() async throws { + let methodDescriptor = MethodDescriptor( + service: "TracingInterceptorTests", + method: "testClientInterceptorAllEventsRecorded" + ) + var serviceContext = ServiceContext.topLevel + let traceIDString = UUID().uuidString + let interceptor = ClientTracingInterceptor(emitEventOnEachWrite: true) + let (stream, continuation) = AsyncStream.makeStream() + serviceContext.traceID = traceIDString + + try await ServiceContext.withValue(serviceContext) { + let response = try await interceptor.intercept( + request: .init(producer: { writer in + try await writer.write(contentsOf: ["request1"]) + try await writer.write(contentsOf: ["request2"]) + }), + context: .init(descriptor: methodDescriptor) + ) { stream, _ in + // Assert the metadata contains the injected context key-value. + XCTAssertEqual(stream.metadata, ["trace-id": "\(traceIDString)"]) + + // Write into the response stream to make sure the `producer` closure's called. + let writer = RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) + try await stream.producer(writer) + continuation.finish() + + return .init( + metadata: [], + bodyParts: .init( + wrapping: AsyncStream { cont in + cont.yield(.message(["response"])) + cont.finish() + } + ) + ) + } + + var streamIterator = stream.makeAsyncIterator() + var element = await streamIterator.next() + XCTAssertEqual(element, "request1") + element = await streamIterator.next() + XCTAssertEqual(element, "request2") + element = await streamIterator.next() + XCTAssertNil(element) + + var messages = response.messages.makeAsyncIterator() + var message = try await messages.next() + XCTAssertEqual(message, ["response"]) + message = try await messages.next() + XCTAssertNil(message) + + let tracer = InstrumentationSystem.tracer as! TestTracer + XCTAssertEqual( + tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { + $0.name + }, + [ + "Request started", + // Recorded when `request1` is sent + "Sending request part", + "Sent request part", + // Recorded when `request2` is sent + "Sending request part", + "Sent request part", + // Recorded after all request parts have been sent + "Request end", + // Recorded when receiving response part + "Received response part", + // Recorded at end of response + "Received response end", + ] + ) + } + } + #endif // swift >= 5.7 + + func testServerInterceptorErrorResponse() async throws { + let methodDescriptor = MethodDescriptor( + service: "TracingInterceptorTests", + method: "testServerInterceptorErrorResponse" + ) + let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: false) + let response = try await interceptor.intercept( + request: .init(single: .init(metadata: ["trace-id": "some-trace-id"], message: [])), + context: .init(descriptor: methodDescriptor) + ) { _, _ in + ServerResponse.Stream(error: .init(code: .unknown, message: "Test error")) + } + XCTAssertThrowsError(try response.accepted.get()) + + let tracer = InstrumentationSystem.tracer as! TestTracer + XCTAssertEqual( + tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { + $0.name + }, + [ + "Received request start", + "Received request end", + "Sent error response", + ] + ) + } + + func testServerInterceptor() async throws { + let methodDescriptor = MethodDescriptor( + service: "TracingInterceptorTests", + method: "testServerInterceptor" + ) + let (stream, continuation) = AsyncStream.makeStream() + let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: false) + let response = try await interceptor.intercept( + request: .init(single: .init(metadata: ["trace-id": "some-trace-id"], message: [])), + context: .init(descriptor: methodDescriptor) + ) { _, _ in + { [serviceContext = ServiceContext.current] in + return ServerResponse.Stream( + accepted: .success( + .init( + metadata: [], + producer: { writer in + guard let serviceContext else { + XCTFail("There should be a service context present.") + return ["Result": "Test failed"] + } + + let traceID = serviceContext.traceID + XCTAssertEqual("some-trace-id", traceID) + + try await writer.write("response1") + try await writer.write("response2") + + return ["Result": "Trailing metadata"] + } + ) + ) + ) + }() + } + + let responseContents = try response.accepted.get() + let trailingMetadata = try await responseContents.producer( + RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) + ) + continuation.finish() + XCTAssertEqual(trailingMetadata, ["Result": "Trailing metadata"]) + + var streamIterator = stream.makeAsyncIterator() + var element = await streamIterator.next() + XCTAssertEqual(element, "response1") + element = await streamIterator.next() + XCTAssertEqual(element, "response2") + element = await streamIterator.next() + XCTAssertNil(element) + + let tracer = InstrumentationSystem.tracer as! TestTracer + XCTAssertEqual( + tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { + $0.name + }, + [ + "Received request start", + "Received request end", + "Sent response end", + ] + ) + } + + func testServerInterceptorAllEventsRecorded() async throws { + let methodDescriptor = MethodDescriptor( + service: "TracingInterceptorTests", + method: "testServerInterceptorAllEventsRecorded" + ) + let (stream, continuation) = AsyncStream.makeStream() + let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: true) + let response = try await interceptor.intercept( + request: .init(single: .init(metadata: ["trace-id": "some-trace-id"], message: [])), + context: .init(descriptor: methodDescriptor) + ) { _, _ in + { [serviceContext = ServiceContext.current] in + return ServerResponse.Stream( + accepted: .success( + .init( + metadata: [], + producer: { writer in + guard let serviceContext else { + XCTFail("There should be a service context present.") + return ["Result": "Test failed"] + } + + let traceID = serviceContext.traceID + XCTAssertEqual("some-trace-id", traceID) + + try await writer.write("response1") + try await writer.write("response2") + + return ["Result": "Trailing metadata"] + } + ) + ) + ) + }() + } + + let responseContents = try response.accepted.get() + let trailingMetadata = try await responseContents.producer( + RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) + ) + continuation.finish() + XCTAssertEqual(trailingMetadata, ["Result": "Trailing metadata"]) + + var streamIterator = stream.makeAsyncIterator() + var element = await streamIterator.next() + XCTAssertEqual(element, "response1") + element = await streamIterator.next() + XCTAssertEqual(element, "response2") + element = await streamIterator.next() + XCTAssertNil(element) + + let tracer = InstrumentationSystem.tracer as! TestTracer + XCTAssertEqual( + tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { + $0.name + }, + [ + "Received request start", + "Received request end", + // Recorded when `response1` is sent + "Sending response part", + "Sent response part", + // Recorded when `response2` is sent + "Sending response part", + "Sent response part", + // Recorded when we're done sending response + "Sent response end", + ] + ) + } +} diff --git a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift new file mode 100644 index 000000000..ffed5213a --- /dev/null +++ b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift @@ -0,0 +1,182 @@ +/* + * 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 NIOConcurrencyHelpers +import Tracing + +final class TestTracer: Tracer { + typealias Span = TestSpan + + private var testSpans: NIOLockedValueBox<[String: TestSpan]> = .init([:]) + + func getEventsForTestSpan(ofOperationName operationName: String) -> [SpanEvent] { + self.testSpans.withLockedValue({ $0[operationName] })?.events ?? [] + } + + func extract( + _ carrier: Carrier, + into context: inout ServiceContextModule.ServiceContext, + using extractor: Extract + ) where Carrier == Extract.Carrier, Extract: Instrumentation.Extractor { + let traceID = extractor.extract(key: TraceID.keyName, from: carrier) + context[TraceID.self] = traceID + } + + func inject( + _ context: ServiceContextModule.ServiceContext, + into carrier: inout Carrier, + using injector: Inject + ) where Carrier == Inject.Carrier, Inject: Instrumentation.Injector { + if let traceID = context.traceID { + injector.inject(traceID, forKey: TraceID.keyName, into: &carrier) + } + } + + func forceFlush() { + // no-op + } + + func startSpan( + _ operationName: String, + context: @autoclosure () -> ServiceContext, + ofKind kind: SpanKind, + at instant: @autoclosure () -> Instant, + function: String, + file fileID: String, + line: UInt + ) -> TestSpan where Instant: TracerInstant { + return self.testSpans.withLockedValue { testSpans in + let span = TestSpan(context: context(), operationName: operationName) + testSpans[operationName] = span + return span + } + } +} + +class TestSpan: Span { + var context: ServiceContextModule.ServiceContext + var operationName: String + var attributes: Tracing.SpanAttributes + var isRecording: Bool + private(set) var status: Tracing.SpanStatus? + private(set) var events: [Tracing.SpanEvent] = [] + + init( + context: ServiceContextModule.ServiceContext, + operationName: String, + attributes: Tracing.SpanAttributes = [:], + isRecording: Bool = true + ) { + self.context = context + self.operationName = operationName + self.attributes = attributes + self.isRecording = isRecording + } + + func setStatus(_ status: Tracing.SpanStatus) { + self.status = status + } + + func addEvent(_ event: Tracing.SpanEvent) { + self.events.append(event) + } + + func recordError( + _ error: any Error, + attributes: Tracing.SpanAttributes, + at instant: @autoclosure () -> Instant + ) where Instant: Tracing.TracerInstant { + self.setStatus( + .init( + code: .error, + message: "Error: \(error), attributes: \(attributes), at instant: \(instant())" + ) + ) + } + + func addLink(_ link: Tracing.SpanLink) { + self.context.spanLinks?.append(link) + } + + func end(at instant: @autoclosure () -> Instant) where Instant: Tracing.TracerInstant { + self.setStatus(.init(code: .ok, message: "Ended at instant: \(instant())")) + } +} + +enum TraceID: ServiceContextModule.ServiceContextKey { + typealias Value = String + + static let keyName = "trace-id" +} + +enum ServiceContextSpanLinksKey: ServiceContextModule.ServiceContextKey { + typealias Value = [SpanLink] + + static let keyName = "span-links" +} + +extension ServiceContext { + var traceID: String? { + get { + self[TraceID.self] + } + set { + self[TraceID.self] = newValue + } + } + + var spanLinks: [SpanLink]? { + get { + self[ServiceContextSpanLinksKey.self] + } + set { + self[ServiceContextSpanLinksKey.self] = newValue + } + } +} + +struct TestWriter: RPCWriterProtocol { + typealias Element = WriterElement + + private let streamContinuation: AsyncStream.Continuation + + init(streamContinuation: AsyncStream.Continuation) { + self.streamContinuation = streamContinuation + } + + func write(contentsOf elements: some Sequence) async throws { + elements.forEach { element in + self.streamContinuation.yield(element) + } + } +} + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif From 9f56b78a5160624f1af12de4b24b0f9cccafc80a Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:32:18 +0000 Subject: [PATCH 207/580] [CodeGenLib] Differentiate type names from identifying names (#1759) Motivation: The names of the namespaces/service/methods used in the type names in the generated code can be different from the names identifying the namespaces/service/methods in the IDL. This is why we need to have different properties for them. Methods also have a third name - for the function signatures. Modifications: - Created the Name struct which contains the name in three formats - Added the name properties in the ServiceDescriptor and MethodDescriptor from CodeGenerationRequest - Modified the existing tests accordingly - Added new validation methods verifying the generated names - Added tests for the new checks Result: Users will be able to set an identifying name, a generated upper case name and a generated upper case name for the namespaces, services and methods which are used in the generated code. --------- Co-authored-by: George Barnett --- .../GRPCCodeGen/CodeGenerationRequest.swift | 59 +++- .../Translator/ClientCodeTranslator.swift | 40 +-- .../IDLToStructuredSwiftTranslator.swift | 143 ++++++--- .../Translator/ServerCodeTranslator.swift | 28 +- .../Translator/TypealiasTranslator.swift | 43 ++- ...lientCodeTranslatorSnippetBasedTests.swift | 269 ++++++++-------- ...uredSwiftTranslatorSnippetBasedTests.swift | 248 +++++++++++++-- ...erverCodeTranslatorSnippetBasedTests.swift | 289 +++++++++++------- ...TypealiasTranslatorSnippetBasedTests.swift | 227 ++++++++------ 9 files changed, 895 insertions(+), 451 deletions(-) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 1efb81636..fab71b977 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -216,17 +216,21 @@ public struct CodeGenerationRequest { } /// Represents a service described in an IDL file. - public struct ServiceDescriptor { + public struct ServiceDescriptor: Hashable { /// Documentation from comments above the IDL service description. public var documentation: String - /// Service name. - public var name: String + /// The service name in different formats. + /// + /// All properties of this object must be unique for each service from within a namespace. + public var name: Name - /// The service namespace. + /// The service namespace in different formats. /// - /// For `.proto` files it is the package name. - public var namespace: String + /// All different services from within the same namespace must have + /// the same ``Name`` object as this property. + /// For `.proto` files the base name of this object is the package name. + public var namespace: Name /// A description of each method of a service. /// @@ -235,8 +239,8 @@ public struct CodeGenerationRequest { public init( documentation: String, - name: String, - namespace: String, + name: Name, + namespace: Name, methods: [MethodDescriptor] ) { self.documentation = documentation @@ -246,12 +250,15 @@ public struct CodeGenerationRequest { } /// Represents a method described in an IDL file. - public struct MethodDescriptor { + public struct MethodDescriptor: Hashable { /// Documentation from comments above the IDL method description. public var documentation: String - /// Method name. - public var name: String + /// Method name in different formats. + /// + /// All properties of this object must be unique for each method + /// from within a service. + public var name: Name /// Identifies if the method is input streaming. public var isInputStreaming: Bool @@ -267,7 +274,7 @@ public struct CodeGenerationRequest { public init( documentation: String, - name: String, + name: Name, isInputStreaming: Bool, isOutputStreaming: Bool, inputType: String, @@ -282,4 +289,32 @@ public struct CodeGenerationRequest { } } } + + /// Represents the name associated with a namespace, service or a method, in three different formats. + public struct Name: Hashable { + /// The base name is the name used for the namespace/service/method in the IDL file, so it should follow + /// the specific casing of the IDL. + /// + /// The base name is also used in the descriptors that identify a specific method or service : + /// `..`. + public var base: String + + /// The `generatedUpperCase` name is used in the generated code. It is expected + /// to be the UpperCamelCase version of the base name + /// + /// For example, if `base` is "fooBar", then `generatedUpperCase` is "FooBar". + public var generatedUpperCase: String + + /// The `generatedLowerCase` name is used in the generated code. It is expected + /// to be the lowerCamelCase version of the base name + /// + /// For example, if `base` is "FooBar", then `generatedLowerCase` is "fooBar". + public var generatedLowerCase: String + + public init(base: String, generatedUpperCase: String, generatedLowerCase: String) { + self.base = base + self.generatedUpperCase = generatedUpperCase + self.generatedLowerCase = generatedLowerCase + } + } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 567fd365e..1b83f991c 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -22,23 +22,23 @@ /// a representation for the following generated code: /// /// ```swift -/// public protocol foo_BarClientProtocol: Sendable { +/// public protocol Foo_BarClientProtocol: Sendable { /// func baz( /// request: ClientRequest.Single, /// serializer: some MessageSerializer, /// deserializer: some MessageDeserializer, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R -/// ) async throws -> ServerResponse.Stream +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async throws -> ServerResponse.Stream /// } -/// extension foo.Bar.ClientProtocol { +/// extension Foo.Bar.ClientProtocol { /// public func get( -/// request: ClientRequest.Single, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// request: ClientRequest.Single, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R /// ) async rethrows -> R { /// try await self.baz( /// request: request, -/// serializer: ProtobufSerializer(), -/// deserializer: ProtobufDeserializer(), +/// serializer: ProtobufSerializer(), +/// deserializer: ProtobufDeserializer(), /// body /// ) /// } @@ -55,7 +55,7 @@ /// ) async rethrows -> R { /// try await self.client.clientStreaming( /// request: request, -/// descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, +/// descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, /// serializer: serializer, /// deserializer: deserializer, /// handler: body @@ -115,7 +115,7 @@ extension ClientCodeTranslator { let clientProtocol = Declaration.protocol( ProtocolDescription( accessModifier: self.accessModifier, - name: "\(service.namespacedPrefix)ClientProtocol", + name: "\(service.namespacedGeneratedName)ClientProtocol", conformances: ["Sendable"], members: methods ) @@ -138,7 +138,7 @@ extension ClientCodeTranslator { } let clientProtocolExtension = Declaration.extension( ExtensionDescription( - onType: "\(service.namespacedTypealiasPrefix).ClientProtocol", + onType: "\(service.namespacedTypealiasGeneratedName).ClientProtocol", declarations: methods ) ) @@ -161,7 +161,7 @@ extension ClientCodeTranslator { let functionSignature = FunctionSignatureDescription( accessModifier: accessModifier, kind: .function( - name: method.name, + name: method.name.generatedLowerCase, isStatic: false ), generics: [.member("R")], @@ -190,7 +190,10 @@ extension ClientCodeTranslator { ) -> [CodeBlock] { let functionCall = Expression.functionCall( calledExpression: .memberAccess( - MemberAccessDescription(left: .identifierPattern("self"), right: method.name) + MemberAccessDescription( + left: .identifierPattern("self"), + right: method.name.generatedLowerCase + ) ), arguments: [ FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), @@ -329,8 +332,8 @@ extension ClientCodeTranslator { return .struct( StructDescription( accessModifier: self.accessModifier, - name: "\(service.namespacedPrefix)Client", - conformances: ["\(service.namespacedTypealiasPrefix).ClientProtocol"], + name: "\(service.namespacedGeneratedName)Client", + conformances: ["\(service.namespacedTypealiasGeneratedName).ClientProtocol"], members: [clientProperty, initializer] + methods ) ) @@ -393,7 +396,7 @@ extension ClientCodeTranslator { .init( label: "descriptor", expression: .identifierPattern( - "\(service.namespacedTypealiasPrefix).Methods.\(method.name).descriptor" + "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase).descriptor" ) ), .init(label: "serializer", expression: .identifierPattern("serializer")), @@ -409,7 +412,7 @@ extension ClientCodeTranslator { return .function( accessModifier: self.accessModifier, kind: .function( - name: "\(method.name)", + name: "\(method.name.generatedLowerCase)", isStatic: false ), generics: [.member("R")], @@ -432,7 +435,8 @@ extension ClientCodeTranslator { service: CodeGenerationRequest.ServiceDescriptor, type: InputOutputType ) -> String { - var components: String = "\(service.namespacedTypealiasPrefix).Methods.\(method.name)" + var components: String = + "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase)" switch type { case .input: diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 7e62a60bf..2f8a81707 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -99,11 +99,14 @@ extension IDLToStructuredSwiftTranslator { } private func validateInput(_ codeGenerationRequest: CodeGenerationRequest) throws { - let servicesByNamespace = Dictionary( + try self.checkServiceDescriptorsAreUnique(codeGenerationRequest.services) + + let servicesByUpperCaseNamespace = Dictionary( grouping: codeGenerationRequest.services, - by: { $0.namespace } + by: { $0.namespace.generatedUpperCase } ) - try self.checkServiceNamesAreUnique(for: servicesByNamespace) + try self.checkServiceNamesAreUnique(for: servicesByUpperCaseNamespace) + for service in codeGenerationRequest.services { try self.checkMethodNamesAreUnique(in: service) } @@ -112,39 +115,42 @@ extension IDLToStructuredSwiftTranslator { // Verify service names are unique within each namespace and that services with no namespace // don't have the same names as any of the namespaces. private func checkServiceNamesAreUnique( - for servicesByNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]] + for servicesByUpperCaseNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]] ) throws { - // Check that if there are services in an empty namespace, none have names which match other namespaces - if let noNamespaceServices = servicesByNamespace[""] { - let namespaces = servicesByNamespace.keys + // Check that if there are services in an empty namespace, none have names which match other namespaces, + // to ensure that there are no enums with the same name in the type aliases part of the generated code. + if let noNamespaceServices = servicesByUpperCaseNamespace[""] { + let upperCaseNamespaces = servicesByUpperCaseNamespace.keys for service in noNamespaceServices { - if namespaces.contains(service.name) { + if upperCaseNamespaces.contains(service.name.generatedUpperCase) { throw CodeGenError( code: .nonUniqueServiceName, message: """ - Services with no namespace must not have the same names as the namespaces. \ - \(service.name) is used as a name for a service with no namespace and a namespace. + Services with no namespace must not have the same generated upper case names as the namespaces. \ + \(service.name.generatedUpperCase) is used as a generated upper case name for a service with no namespace and a namespace. """ ) } } } - // Check that service names are unique within each namespace. - for (namespace, services) in servicesByNamespace { - var serviceNames: Set = [] + // Check that the generated upper case names for services are unique within each namespace, to ensure that + // the service enums from each namespace enum have unique names. + for (namespace, services) in servicesByUpperCaseNamespace { + var upperCaseNames: Set = [] + for service in services { - if serviceNames.contains(service.name) { + if upperCaseNames.contains(service.name.generatedUpperCase) { let errorMessage: String if namespace.isEmpty { errorMessage = """ - Services in an empty namespace must have unique names. \ - \(service.name) is used as a name for multiple services without namespaces. + Services in an empty namespace must have unique generated upper case names. \ + \(service.name.generatedUpperCase) is used as a generated upper case name for multiple services without namespaces. """ } else { errorMessage = """ - Services within the same namespace must have unique names. \ - \(service.name) is used as a name for multiple services in the \(service.namespace) namespace. + Services within the same namespace must have unique generated upper case names. \ + \(service.name.generatedUpperCase) is used as a generated upper case name for multiple services in the \(service.namespace.base) namespace. """ } throw CodeGenError( @@ -152,47 +158,112 @@ extension IDLToStructuredSwiftTranslator { message: errorMessage ) } - serviceNames.insert(service.name) + upperCaseNames.insert(service.name.generatedUpperCase) } } } - // Verify method names are unique for the service. + // Verify method names are unique within a service. private func checkMethodNamesAreUnique( in service: CodeGenerationRequest.ServiceDescriptor ) throws { - let methodNames = service.methods.map { $0.name } - var seenNames = Set() + // Check that the method descriptors are unique, by checking that the base names + // of the methods of a specific service are unique. + let baseNames = service.methods.map { $0.name.base } + if let duplicatedBase = baseNames.getFirstDuplicate() { + throw CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique base names. \ + \(duplicatedBase) is used as a base name for multiple methods of the \(service.name.base) service. + """ + ) + } + + // Check that generated upper case names for methods are unique within a service, to ensure that + // the enums containing type aliases for each method of a service. + let upperCaseNames = service.methods.map { $0.name.generatedUpperCase } + if let duplicatedGeneratedUpperCase = upperCaseNames.getFirstDuplicate() { + throw CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique generated upper case names. \ + \(duplicatedGeneratedUpperCase) is used as a generated upper case name for multiple methods of the \(service.name.base) service. + """ + ) + } + + // Check that generated lower case names for methods are unique within a service, to ensure that + // the function declarations and definitions from the same protocols and extensions have unique names. + let lowerCaseNames = service.methods.map { $0.name.generatedLowerCase } + if let duplicatedLowerCase = lowerCaseNames.getFirstDuplicate() { + throw CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique lower case names. \ + \(duplicatedLowerCase) is used as a signature name for multiple methods of the \(service.name.base) service. + """ + ) + } + } - for methodName in methodNames { - if seenNames.contains(methodName) { + private func checkServiceDescriptorsAreUnique( + _ services: [CodeGenerationRequest.ServiceDescriptor] + ) throws { + var descriptors: Set = [] + for service in services { + let name = + service.namespace.base.isEmpty + ? service.name.base : "\(service.namespace.base).\(service.name.base)" + let (inserted, _) = descriptors.insert(name) + if !inserted { throw CodeGenError( - code: .nonUniqueMethodName, + code: .nonUniqueServiceName, message: """ - Methods of a service must have unique names. \ - \(methodName) is used as a name for multiple methods of the \(service.name) service. + Services must have unique descriptors. \ + \(name) is the descriptor of at least two different services. """ ) } - seenNames.insert(methodName) } } } extension CodeGenerationRequest.ServiceDescriptor { - var namespacedTypealiasPrefix: String { - if self.namespace.isEmpty { - return self.name + var namespacedTypealiasGeneratedName: String { + if self.namespace.generatedUpperCase.isEmpty { + return self.name.generatedUpperCase + } else { + return "\(self.namespace.generatedUpperCase).\(self.name.generatedUpperCase)" + } + } + + var namespacedGeneratedName: String { + if self.namespace.generatedUpperCase.isEmpty { + return self.name.generatedUpperCase } else { - return "\(self.namespace).\(self.name)" + return "\(self.namespace.generatedUpperCase)_\(self.name.generatedUpperCase)" } } - var namespacedPrefix: String { - if self.namespace.isEmpty { - return self.name + var fullyQualifiedName: String { + if self.namespace.base.isEmpty { + return self.name.base } else { - return "\(self.namespace)_\(self.name)" + return "\(self.namespace.base).\(self.name.base)" + } + } +} + +extension [String] { + internal func getFirstDuplicate() -> String? { + var seen = Set() + for element in self { + if seen.contains(element) { + return element + } + seen.insert(element) } + return nil } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index dc9ba10dc..dbf4914d6 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -135,7 +135,7 @@ extension ServerCodeTranslator { ) -> FunctionSignatureDescription { return FunctionSignatureDescription( accessModifier: accessModifier, - kind: .function(name: method.name), + kind: .function(name: method.name.generatedLowerCase), parameters: [ .init( label: "request", @@ -253,7 +253,10 @@ extension ServerCodeTranslator { let getFunctionCall = Expression.functionCall( calledExpression: .memberAccess( - MemberAccessDescription(left: .identifierPattern("self"), right: method.name) + MemberAccessDescription( + left: .identifierPattern("self"), + right: method.name.generatedLowerCase + ) ), arguments: [ FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")) @@ -320,7 +323,7 @@ extension ServerCodeTranslator { let functionSignature = FunctionSignatureDescription( accessModifier: accessModifier, - kind: .function(name: method.name), + kind: .function(name: method.name.generatedLowerCase), parameters: [ .init( label: "request", @@ -405,7 +408,10 @@ extension ServerCodeTranslator { // Call to the corresponding ServiceProtocol method. let serviceProtocolMethod = Expression.functionCall( calledExpression: .memberAccess( - MemberAccessDescription(left: .identifierPattern("self"), right: method.name) + MemberAccessDescription( + left: .identifierPattern("self"), + right: method.name.generatedLowerCase + ) ), arguments: [FunctionArgumentDescription(label: "request", expression: serverRequest)] ) @@ -453,7 +459,8 @@ extension ServerCodeTranslator { service: CodeGenerationRequest.ServiceDescriptor, type: InputOutputType ) -> String { - var components: String = "\(service.namespacedTypealiasPrefix).Methods.\(method.name)" + var components: String = + "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase)" switch type { case .input: @@ -470,7 +477,8 @@ extension ServerCodeTranslator { for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, service: CodeGenerationRequest.ServiceDescriptor ) -> String { - return "\(service.namespacedTypealiasPrefix).Methods.\(method.name).descriptor" + return + "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase).descriptor" } /// Generates the fully qualified name of the type alias for a service protocol. @@ -479,9 +487,9 @@ extension ServerCodeTranslator { streaming: Bool ) -> String { if streaming { - return "\(service.namespacedTypealiasPrefix).StreamingServiceProtocol" + return "\(service.namespacedTypealiasGeneratedName).StreamingServiceProtocol" } - return "\(service.namespacedTypealiasPrefix).ServiceProtocol" + return "\(service.namespacedTypealiasGeneratedName).ServiceProtocol" } /// Generates the name of a service protocol. @@ -490,8 +498,8 @@ extension ServerCodeTranslator { streaming: Bool ) -> String { if streaming { - return "\(service.namespacedPrefix)StreamingServiceProtocol" + return "\(service.namespacedGeneratedName)StreamingServiceProtocol" } - return "\(service.namespacedPrefix)ServiceProtocol" + return "\(service.namespacedGeneratedName)ServiceProtocol" } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 29efdffc3..308aa83a9 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -21,7 +21,7 @@ /// For example, in the case of the ``Echo`` service, the ``TypealiasTranslator`` will create /// a representation for the following generated code: /// ```swift -/// public enum echo { +/// public enum Echo { /// public enum Echo { /// public enum Methods { /// public enum Get { @@ -66,14 +66,17 @@ struct TypealiasTranslator: SpecializedTranslator { func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { var codeBlocks: [CodeBlock] = [] let services = codeGenerationRequest.services - let servicesByNamespace = Dictionary(grouping: services, by: { $0.namespace }) + let servicesByNamespace = Dictionary( + grouping: services, + by: { $0.namespace.generatedUpperCase } + ) // Sorting the keys and the services in each list of the dictionary is necessary // so that the generated enums are deterministically ordered. - for (namespace, services) in servicesByNamespace.sorted(by: { $0.key < $1.key }) { + for (generatedNamespace, services) in servicesByNamespace.sorted(by: { $0.key < $1.key }) { let namespaceCodeBlocks = try self.makeNamespaceEnum( - for: namespace, - containing: services.sorted(by: { $0.name < $1.name }) + for: generatedNamespace, + containing: services.sorted(by: { $0.name.generatedUpperCase < $1.name.generatedUpperCase }) ) codeBlocks.append(contentsOf: namespaceCodeBlocks) } @@ -112,7 +115,10 @@ extension TypealiasTranslator { private func makeServiceEnum( from service: CodeGenerationRequest.ServiceDescriptor ) throws -> Declaration { - var serviceEnum = EnumDescription(accessModifier: self.accessModifier, name: service.name) + var serviceEnum = EnumDescription( + accessModifier: self.accessModifier, + name: service.name.generatedUpperCase + ) var methodsEnum = EnumDescription(accessModifier: self.accessModifier, name: "Methods") let methods = service.methods @@ -150,7 +156,7 @@ extension TypealiasTranslator { from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { - var methodEnum = EnumDescription(name: method.name) + var methodEnum = EnumDescription(name: method.name.generatedUpperCase) let inputTypealias = Declaration.typealias( accessModifier: self.accessModifier, @@ -180,29 +186,22 @@ extension TypealiasTranslator { in service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { let descriptorDeclarationLeft = Expression.identifier(.pattern("descriptor")) - - let fullyQualifiedServiceName: String - if service.namespace.isEmpty { - fullyQualifiedServiceName = service.name - } else { - fullyQualifiedServiceName = "\(service.namespace).\(service.name)" - } - let descriptorDeclarationRight = Expression.functionCall( FunctionCallDescription( calledExpression: .identifierType(.member("MethodDescriptor")), arguments: [ FunctionArgumentDescription( label: "service", - expression: .literal(fullyQualifiedServiceName) + expression: .literal(service.fullyQualifiedName) ), FunctionArgumentDescription( label: "method", - expression: .literal(method.name) + expression: .literal(method.name.base) ), ] ) ) + return .variable( accessModifier: self.accessModifier, isStatic: true, @@ -216,7 +215,7 @@ extension TypealiasTranslator { for service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { var methodDescriptors = [Expression]() - let methodNames = service.methods.map { $0.name } + let methodNames = service.methods.map { $0.name.generatedUpperCase } for methodName in methodNames { let methodDescriptorPath = Expression.memberAccess( @@ -244,12 +243,12 @@ extension TypealiasTranslator { let streamingServiceProtocolTypealias = Declaration.typealias( accessModifier: self.accessModifier, name: "StreamingServiceProtocol", - existingType: .member("\(service.namespacedPrefix)ServiceStreamingProtocol") + existingType: .member("\(service.namespacedGeneratedName)ServiceStreamingProtocol") ) let serviceProtocolTypealias = Declaration.typealias( accessModifier: self.accessModifier, name: "ServiceProtocol", - existingType: .member("\(service.namespacedPrefix)ServiceProtocol") + existingType: .member("\(service.namespacedGeneratedName)ServiceProtocol") ) return [streamingServiceProtocolTypealias, serviceProtocolTypealias] @@ -261,7 +260,7 @@ extension TypealiasTranslator { return .typealias( accessModifier: self.accessModifier, name: "ClientProtocol", - existingType: .member("\(service.namespacedPrefix)ClientProtocol") + existingType: .member("\(service.namespacedGeneratedName)ClientProtocol") ) } @@ -271,7 +270,7 @@ extension TypealiasTranslator { return .typealias( accessModifier: self.accessModifier, name: "Client", - existingType: .member("\(service.namespacedPrefix)Client") + existingType: .member("\(service.namespacedGeneratedName)Client") ) } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 08c800bb3..b97d3ca2f 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -23,11 +23,12 @@ import XCTest final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + typealias Name = GRPCCodeGen.CodeGenerationRequest.Name func testClientCodeTranslatorUnaryMethod() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "methodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -35,51 +36,51 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), + namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } - extension namespaceA.ServiceA.ClientProtocol { + extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA - public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -98,7 +99,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorClientStreamingMethod() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "methodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: true, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -106,51 +107,51 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), + namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } - extension namespaceA.ServiceA.ClientProtocol { + extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA - public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, - descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -169,7 +170,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorServerStreamingMethod() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "methodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: true, inputType: "NamespaceA_ServiceARequest", @@ -177,51 +178,51 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), + namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } - extension namespaceA.ServiceA.ClientProtocol { + extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA - public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, - descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -240,7 +241,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorBidirectionalStreamingMethod() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "methodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: true, isOutputStreaming: true, inputType: "NamespaceA_ServiceARequest", @@ -248,51 +249,51 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), + namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } - extension namespaceA.ServiceA.ClientProtocol { + extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA - public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, - descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -311,7 +312,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorMultipleMethods() throws { let methodA = MethodDescriptor( documentation: "Documentation for MethodA", - name: "methodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: true, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -319,7 +320,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) let methodB = MethodDescriptor( documentation: "Documentation for MethodB", - name: "methodB", + name: Name(base: "MethodB", generatedUpperCase: "MethodB", generatedLowerCase: "methodB"), isInputStreaming: false, isOutputStreaming: true, inputType: "NamespaceA_ServiceARequest", @@ -327,69 +328,69 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), + namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [methodA, methodB] ) let expectedSwift = """ /// Documentation for ServiceA - package protocol namespaceA_ServiceAClientProtocol: Sendable { + package protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Documentation for MethodB func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } - extension namespaceA.ServiceA.ClientProtocol { + extension NamespaceA.ServiceA.ClientProtocol { package func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } package func methodB( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodB( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA - package struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + package struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient package init(client: GRPCCore.GRPCClient) { self.client = client } /// Documentation for MethodA package func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, - descriptor: namespaceA.ServiceA.Methods.methodA.descriptor, + descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -397,14 +398,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodB package func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, - descriptor: namespaceA.ServiceA.Methods.methodB.descriptor, + descriptor: NamespaceA.ServiceA.Methods.MethodB.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -423,7 +424,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorNoNamespaceService() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "methodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "ServiceARequest", @@ -431,8 +432,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [method] ) let expectedSwift = @@ -441,21 +442,21 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } extension ServiceA.ClientProtocol { internal func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -468,14 +469,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodA internal func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: ServiceA.Methods.methodA.descriptor, + descriptor: ServiceA.Methods.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -494,24 +495,28 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorMultipleServices() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), + namespace: Name( + base: "nammespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "" + ), methods: [] ) let serviceB = ServiceDescriptor( documentation: "Documentation for ServiceB", - name: "ServiceB", - namespace: "", + name: Name(base: "ServiceB", generatedUpperCase: "ServiceB", generatedLowerCase: ""), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAClientProtocol: Sendable {} - extension namespaceA.ServiceA.ClientProtocol { + public protocol NamespaceA_ServiceAClientProtocol: Sendable {} + extension NamespaceA.ServiceA.ClientProtocol { } /// Documentation for ServiceA - public struct namespaceA_ServiceAClient: namespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { self.client = client diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 7d50aa37b..3dfec1358 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -23,6 +23,7 @@ import XCTest final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + typealias Name = GRPCCodeGen.CodeGenerationRequest.Name func testImports() throws { var dependencies = [CodeGenerationRequest.Dependency]() @@ -154,8 +155,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameNameServicesNoNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: "AService", - namespace: "", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) @@ -176,19 +177,61 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { CodeGenError( code: .nonUniqueServiceName, message: """ - Services in an empty namespace must have unique names. \ - AService is used as a name for multiple services without namespaces. + Services must have unique descriptors. \ + AService is the descriptor of at least two different services. """ ) ) } } - func testSameNameServicesSameNamespaceError() throws { + func testSameDescriptorsServicesNoNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: "AService", - namespace: "namespacea", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), + methods: [] + ) + + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), + methods: [] + ) + + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + accessLevel: .public, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services must have unique descriptors. AService is the descriptor of at least two different services. + """ + ) + ) + } + } + func testSameDescriptorsSameNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name( + base: "namespacea", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespacea" + ), methods: [] ) @@ -209,18 +252,65 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { CodeGenError( code: .nonUniqueServiceName, message: """ - Services within the same namespace must have unique names. \ - AService is used as a name for multiple services in the namespacea namespace. + Services must have unique descriptors. \ + namespacea.AService is the descriptor of at least two different services. + """ + ) + ) + } + } + + func testSameGeneratedNameServicesSameNamespaceError() throws { + let serviceA = ServiceDescriptor( + documentation: "Documentation for AService", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name( + base: "namespacea", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespacea" + ), + methods: [] + ) + let serviceB = ServiceDescriptor( + documentation: "Documentation for BService", + name: Name(base: "BService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name( + base: "namespacea", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespacea" + ), + methods: [] + ) + + let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + accessLevel: .internal, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueServiceName, + message: """ + Services within the same namespace must have unique generated upper case names. \ + AService is used as a generated upper case name for multiple services in the namespacea namespace. """ ) ) } } - func testSameNameMethodsSameServiceError() throws { + func testSameBaseNameMethodsSameServiceError() throws { let methodA = MethodDescriptor( documentation: "Documentation for MethodA", - name: "MethodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -228,8 +318,12 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for AService", - name: "AService", - namespace: "namespacea", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name( + base: "namespacea", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespacea" + ), methods: [methodA, methodA] ) @@ -250,25 +344,135 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { CodeGenError( code: .nonUniqueMethodName, message: """ - Methods of a service must have unique names. \ - MethodA is used as a name for multiple methods of the AService service. + Methods of a service must have unique base names. \ + MethodA is used as a base name for multiple methods of the AService service. + """ + ) + ) + } + } + + func testSameGeneratedUpperCaseNameMethodsSameServiceError() throws { + let methodA = MethodDescriptor( + documentation: "Documentation for MethodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let methodB = MethodDescriptor( + documentation: "Documentation for MethodA", + name: Name(base: "MethodB", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for AService", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name( + base: "namespacea", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespacea" + ), + methods: [methodA, methodB] + ) + + let codeGenerationRequest = makeCodeGenerationRequest(services: [service]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + accessLevel: .public, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique generated upper case names. \ + MethodA is used as a generated upper case name for multiple methods of the AService service. + """ + ) + ) + } + } + + func testSameLowerCaseNameMethodsSameServiceError() throws { + let methodA = MethodDescriptor( + documentation: "Documentation for MethodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let methodB = MethodDescriptor( + documentation: "Documentation for MethodA", + name: Name(base: "MethodB", generatedUpperCase: "MethodB", generatedLowerCase: "methodA"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "NamespaceA_ServiceARequest", + outputType: "NamespaceA_ServiceAResponse" + ) + let service = ServiceDescriptor( + documentation: "Documentation for AService", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name( + base: "namespacea", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespacea" + ), + methods: [methodA, methodB] + ) + + let codeGenerationRequest = makeCodeGenerationRequest(services: [service]) + let translator = IDLToStructuredSwiftTranslator() + XCTAssertThrowsError( + ofType: CodeGenError.self, + try translator.translate( + codeGenerationRequest: codeGenerationRequest, + accessLevel: .public, + client: true, + server: true + ) + ) { + error in + XCTAssertEqual( + error as CodeGenError, + CodeGenError( + code: .nonUniqueMethodName, + message: """ + Methods of a service must have unique lower case names. \ + methodA is used as a signature name for multiple methods of the AService service. """ ) ) } } - func testSameNameNoNamespaceServiceAndNamespaceError() throws { + func testSameGeneratedNameNoNamespaceServiceAndNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for SameName service with no namespace", - name: "SameName", - namespace: "", + name: Name(base: "SameName", generatedUpperCase: "SameName", generatedLowerCase: "sameName"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) let serviceB = ServiceDescriptor( documentation: "Documentation for BService", - name: "BService", - namespace: "SameName", + name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bService"), + namespace: Name( + base: "sameName", + generatedUpperCase: "SameName", + generatedLowerCase: "sameName" + ), methods: [] ) let codeGenerationRequest = makeCodeGenerationRequest(services: [serviceA, serviceB]) @@ -288,8 +492,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { CodeGenError( code: .nonUniqueServiceName, message: """ - Services with no namespace must not have the same names as the namespaces. \ - SameName is used as a name for a service with no namespace and a namespace. + Services with no namespace must not have the same generated upper case names as the namespaces. \ + SameName is used as a generated upper case name for a service with no namespace and a namespace. """ ) ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index ae29611b2..d0a4a5975 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -23,11 +23,12 @@ import XCTest final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + typealias Name = GRPCCodeGen.CodeGenerationRequest.Name func testServerCodeTranslatorUnaryMethod() throws { let method = MethodDescriptor( documentation: "Documentation for unaryMethod", - name: "unaryMethod", + name: Name(base: "UnaryMethod", generatedUpperCase: "Unary", generatedLowerCase: "unary"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -35,39 +36,47 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name( + base: "AlongNameForServiceA", + generatedUpperCase: "ServiceA", + generatedLowerCase: "serviceA" + ), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod - func unaryMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - extension namespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA.ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: namespaceA.ServiceA.Methods.unaryMethod.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + for: NamespaceA.ServiceA.Methods.Unary.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in - try await self.unaryMethod(request: request) + try await self.unary(request: request) } ) } } /// Documentation for ServiceA - public protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod - func unaryMethod(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single } - /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - extension namespaceA.ServiceA.ServiceProtocol { - public func unaryMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.unaryMethod(request: ServerRequest.Single(stream: request)) + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { + public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.unary(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } } @@ -83,7 +92,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorInputStreamingMethod() throws { let method = MethodDescriptor( documentation: "Documentation for inputStreamingMethod", - name: "inputStreamingMethod", + name: Name( + base: "InputStreamingMethod", + generatedUpperCase: "InputStreaming", + generatedLowerCase: "inputStreaming" + ), isInputStreaming: true, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -91,39 +104,43 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - package protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - extension namespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA.ServiceA.StreamingServiceProtocol { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + for: NamespaceA.ServiceA.Methods.InputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in - try await self.inputStreamingMethod(request: request) + try await self.inputStreaming(request: request) } ) } } /// Documentation for ServiceA - package protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single } - /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - extension namespaceA.ServiceA.ServiceProtocol { - package func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.inputStreamingMethod(request: request) + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { + package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } } @@ -139,7 +156,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorOutputStreamingMethod() throws { let method = MethodDescriptor( documentation: "Documentation for outputStreamingMethod", - name: "outputStreamingMethod", + name: Name( + base: "OutputStreamingMethod", + generatedUpperCase: "OutputStreaming", + generatedLowerCase: "outputStreaming" + ), isInputStreaming: false, isOutputStreaming: true, inputType: "NamespaceA_ServiceARequest", @@ -147,39 +168,47 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name( + base: "ServiceATest", + generatedUpperCase: "ServiceA", + generatedLowerCase: "serviceA" + ), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod - func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - extension namespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA.ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: namespaceA.ServiceA.Methods.outputStreamingMethod.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + for: NamespaceA.ServiceA.Methods.OutputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in - try await self.outputStreamingMethod(request: request) + try await self.outputStreaming(request: request) } ) } } /// Documentation for ServiceA - public protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod - func outputStreamingMethod(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } - /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - extension namespaceA.ServiceA.ServiceProtocol { - public func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request)) + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { + public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } } @@ -195,7 +224,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorBidirectionalStreamingMethod() throws { let method = MethodDescriptor( documentation: "Documentation for bidirectionalStreamingMethod", - name: "bidirectionalStreamingMethod", + name: Name( + base: "BidirectionalStreamingMethod", + generatedUpperCase: "BidirectionalStreaming", + generatedLowerCase: "bidirectionalStreaming" + ), isInputStreaming: true, isOutputStreaming: true, inputType: "NamespaceA_ServiceARequest", @@ -203,37 +236,45 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name( + base: "ServiceATest", + generatedUpperCase: "ServiceA", + generatedLowerCase: "serviceA" + ), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [method] ) let expectedSwift = """ /// Documentation for ServiceA - package protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - extension namespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA.ServiceA.StreamingServiceProtocol { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: namespaceA.ServiceA.Methods.bidirectionalStreamingMethod.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + for: NamespaceA.ServiceA.Methods.BidirectionalStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in - try await self.bidirectionalStreamingMethod(request: request) + try await self.bidirectionalStreaming(request: request) } ) } } /// Documentation for ServiceA - package protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } - /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - extension namespaceA.ServiceA.ServiceProtocol { + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { } """ @@ -247,7 +288,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorMultipleMethods() throws { let inputStreamingMethod = MethodDescriptor( documentation: "Documentation for inputStreamingMethod", - name: "inputStreamingMethod", + name: Name( + base: "InputStreamingMethod", + generatedUpperCase: "InputStreaming", + generatedLowerCase: "inputStreaming" + ), isInputStreaming: true, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -255,7 +300,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) let outputStreamingMethod = MethodDescriptor( documentation: "Documentation for outputStreamingMethod", - name: "outputStreamingMethod", + name: Name( + base: "outputStreamingMethod", + generatedUpperCase: "OutputStreaming", + generatedLowerCase: "outputStreaming" + ), isInputStreaming: false, isOutputStreaming: true, inputType: "NamespaceA_ServiceARequest", @@ -263,55 +312,63 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name( + base: "ServiceATest", + generatedUpperCase: "ServiceA", + generatedLowerCase: "serviceA" + ), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [inputStreamingMethod, outputStreamingMethod] ) let expectedSwift = """ /// Documentation for ServiceA - internal protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Documentation for outputStreamingMethod - func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - extension namespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA.ServiceA.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: namespaceA.ServiceA.Methods.inputStreamingMethod.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + for: NamespaceA.ServiceA.Methods.InputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in - try await self.inputStreamingMethod(request: request) + try await self.inputStreaming(request: request) } ) router.registerHandler( - for: namespaceA.ServiceA.Methods.outputStreamingMethod.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + for: NamespaceA.ServiceA.Methods.OutputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in - try await self.outputStreamingMethod(request: request) + try await self.outputStreaming(request: request) } ) } } /// Documentation for ServiceA - internal protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol { + internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single /// Documentation for outputStreamingMethod - func outputStreamingMethod(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } - /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - extension namespaceA.ServiceA.ServiceProtocol { - internal func inputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.inputStreamingMethod(request: request) + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { + internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } - internal func outputStreamingMethod(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.outputStreamingMethod(request: ServerRequest.Single(stream: request)) + internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } } @@ -327,7 +384,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorNoNamespaceService() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "methodA", + name: Name(base: "methodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -335,8 +392,12 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "", + name: Name( + base: "ServiceATest", + generatedUpperCase: "ServiceA", + generatedLowerCase: "serviceA" + ), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [method] ) let expectedSwift = @@ -344,15 +405,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA - func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension ServiceA.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: ServiceA.Methods.methodA.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + for: ServiceA.Methods.MethodA.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.methodA(request: request) } @@ -362,11 +423,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA - func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single + func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. extension ServiceA.ServiceProtocol { - internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.methodA(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -383,39 +444,47 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorMoreServicesOrder() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let serviceB = ServiceDescriptor( documentation: "Documentation for ServiceB", - name: "ServiceB", - namespace: "namespaceA", + name: Name(base: "ServiceB", generatedUpperCase: "ServiceB", generatedLowerCase: "serviceB"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let expectedSwift = """ /// Documentation for ServiceA - public protocol namespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - extension namespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA.ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceA - public protocol namespaceA_ServiceAServiceProtocol: namespaceA.ServiceA.StreamingServiceProtocol {} - /// Partial conformance to `namespaceA_ServiceAStreamingServiceProtocol`. - extension namespaceA.ServiceA.ServiceProtocol { + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {} + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { } /// Documentation for ServiceB - public protocol namespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + public protocol NamespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - extension namespaceA.ServiceB.StreamingServiceProtocol { + extension NamespaceA.ServiceB.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceB - public protocol namespaceA_ServiceBServiceProtocol: namespaceA.ServiceB.StreamingServiceProtocol {} - /// Partial conformance to `namespaceA_ServiceBStreamingServiceProtocol`. - extension namespaceA.ServiceB.ServiceProtocol { + public protocol NamespaceA_ServiceBServiceProtocol: NamespaceA.ServiceB.StreamingServiceProtocol {} + /// Partial conformance to `NamespaceA_ServiceBStreamingServiceProtocol`. + extension NamespaceA.ServiceB.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index e21b2d646..1fcf39d61 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -23,11 +23,12 @@ import XCTest final class TypealiasTranslatorSnippetBasedTests: XCTestCase { typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor + typealias Name = GRPCCodeGen.CodeGenerationRequest.Name func testTypealiasTranslator() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "MethodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -35,13 +36,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [method] ) let expectedSwift = """ - public enum namespaceA { + public enum NamespaceA { public enum ServiceA { public enum Methods { public enum MethodA { @@ -56,10 +61,10 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let methods: [MethodDescriptor] = [ Methods.MethodA.descriptor ] - public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - public typealias ClientProtocol = namespaceA_ServiceAClientProtocol - public typealias Client = namespaceA_ServiceAClient + public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias Client = NamespaceA_ServiceAClient } } """ @@ -76,20 +81,24 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorNoMethodsServiceClientAndServer() throws { let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let expectedSwift = """ - public enum namespaceA { + public enum NamespaceA { public enum ServiceA { public enum Methods {} public static let methods: [MethodDescriptor] = [] - public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - public typealias ClientProtocol = namespaceA_ServiceAClientProtocol - public typealias Client = namespaceA_ServiceAClient + public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias Client = NamespaceA_ServiceAClient } } """ @@ -106,18 +115,22 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorServer() throws { let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let expectedSwift = """ - public enum namespaceA { + public enum NamespaceA { public enum ServiceA { public enum Methods {} public static let methods: [MethodDescriptor] = [] - public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } } """ @@ -134,18 +147,22 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorClient() throws { let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let expectedSwift = """ - public enum namespaceA { + public enum NamespaceA { public enum ServiceA { public enum Methods {} public static let methods: [MethodDescriptor] = [] - public typealias ClientProtocol = namespaceA_ServiceAClientProtocol - public typealias Client = namespaceA_ServiceAClient + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias Client = NamespaceA_ServiceAClient } } """ @@ -162,13 +179,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorNoClientNoServer() throws { let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let expectedSwift = """ - public enum namespaceA { + public enum NamespaceA { public enum ServiceA { public enum Methods {} public static let methods: [MethodDescriptor] = [] @@ -188,7 +209,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorEmptyNamespace() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: "MethodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "ServiceARequest", @@ -196,8 +217,8 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [method] ) let expectedSwift = @@ -235,7 +256,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorCheckMethodsOrder() throws { let methodA = MethodDescriptor( documentation: "Documentation for MethodA", - name: "MethodA", + name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -243,7 +264,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let methodB = MethodDescriptor( documentation: "Documentation for MethodB", - name: "MethodB", + name: Name(base: "MethodB", generatedUpperCase: "MethodB", generatedLowerCase: "methodB"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -251,13 +272,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [methodA, methodB] ) let expectedSwift = """ - public enum namespaceA { + public enum NamespaceA { public enum ServiceA { public enum Methods { public enum MethodA { @@ -281,10 +306,10 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { Methods.MethodA.descriptor, Methods.MethodB.descriptor ] - public typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - public typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - public typealias ClientProtocol = namespaceA_ServiceAClientProtocol - public typealias Client = namespaceA_ServiceAClient + public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias Client = NamespaceA_ServiceAClient } } """ @@ -301,20 +326,24 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorNoMethodsService() throws { let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: "ServiceA", - namespace: "namespaceA", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let expectedSwift = """ - package enum namespaceA { + package enum NamespaceA { package enum ServiceA { package enum Methods {} package static let methods: [MethodDescriptor] = [] - package typealias StreamingServiceProtocol = namespaceA_ServiceAServiceStreamingProtocol - package typealias ServiceProtocol = namespaceA_ServiceAServiceProtocol - package typealias ClientProtocol = namespaceA_ServiceAClientProtocol - package typealias Client = namespaceA_ServiceAClient + package typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + package typealias Client = NamespaceA_ServiceAClient } } """ @@ -331,36 +360,44 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorServiceAlphabeticalOrder() throws { let serviceB = ServiceDescriptor( documentation: "Documentation for BService", - name: "BService", - namespace: "namespacea", + name: Name(base: "BService", generatedUpperCase: "Bservice", generatedLowerCase: "bservice"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: "AService", - namespace: "namespacea", + name: Name(base: "AService", generatedUpperCase: "Aservice", generatedLowerCase: "aservice"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), methods: [] ) let expectedSwift = """ - public enum namespacea { - public enum AService { + public enum NamespaceA { + public enum Aservice { public enum Methods {} public static let methods: [MethodDescriptor] = [] - public typealias StreamingServiceProtocol = namespacea_AServiceServiceStreamingProtocol - public typealias ServiceProtocol = namespacea_AServiceServiceProtocol - public typealias ClientProtocol = namespacea_AServiceClientProtocol - public typealias Client = namespacea_AServiceClient + public typealias StreamingServiceProtocol = NamespaceA_AserviceServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol + public typealias ClientProtocol = NamespaceA_AserviceClientProtocol + public typealias Client = NamespaceA_AserviceClient } - public enum BService { + public enum Bservice { public enum Methods {} public static let methods: [MethodDescriptor] = [] - public typealias StreamingServiceProtocol = namespacea_BServiceServiceStreamingProtocol - public typealias ServiceProtocol = namespacea_BServiceServiceProtocol - public typealias ClientProtocol = namespacea_BServiceClientProtocol - public typealias Client = namespacea_BServiceClient + public typealias StreamingServiceProtocol = NamespaceA_BserviceServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol + public typealias ClientProtocol = NamespaceA_BserviceClientProtocol + public typealias Client = NamespaceA_BserviceClient } } """ @@ -377,15 +414,15 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorServiceAlphabeticalOrderNoNamespace() throws { let serviceB = ServiceDescriptor( documentation: "Documentation for BService", - name: "BService", - namespace: "", + name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bservice"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: "AService", - namespace: "", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aservice"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) @@ -421,38 +458,46 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorNamespaceAlphabeticalOrder() throws { let serviceB = ServiceDescriptor( documentation: "Documentation for BService", - name: "BService", - namespace: "bnamespace", + name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bservice"), + namespace: Name( + base: "bnamespace", + generatedUpperCase: "Bnamespace", + generatedLowerCase: "bnamespace" + ), methods: [] ) let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: "AService", - namespace: "anamespace", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aservice"), + namespace: Name( + base: "anamespace", + generatedUpperCase: "Anamespace", + generatedLowerCase: "anamespace" + ), methods: [] ) let expectedSwift = """ - internal enum anamespace { + internal enum Anamespace { internal enum AService { internal enum Methods {} internal static let methods: [MethodDescriptor] = [] - internal typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol - internal typealias ServiceProtocol = anamespace_AServiceServiceProtocol - internal typealias ClientProtocol = anamespace_AServiceClientProtocol - internal typealias Client = anamespace_AServiceClient + internal typealias StreamingServiceProtocol = Anamespace_AServiceServiceStreamingProtocol + internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + internal typealias ClientProtocol = Anamespace_AServiceClientProtocol + internal typealias Client = Anamespace_AServiceClient } } - internal enum bnamespace { + internal enum Bnamespace { internal enum BService { internal enum Methods {} internal static let methods: [MethodDescriptor] = [] - internal typealias StreamingServiceProtocol = bnamespace_BServiceServiceStreamingProtocol - internal typealias ServiceProtocol = bnamespace_BServiceServiceProtocol - internal typealias ClientProtocol = bnamespace_BServiceClientProtocol - internal typealias Client = bnamespace_BServiceClient + internal typealias StreamingServiceProtocol = Bnamespace_BServiceServiceStreamingProtocol + internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol + internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol + internal typealias Client = Bnamespace_BServiceClient } } """ @@ -469,14 +514,18 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { func testTypealiasTranslatorNamespaceNoNamespaceOrder() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: "AService", - namespace: "anamespace", + name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), + namespace: Name( + base: "anamespace", + generatedUpperCase: "Anamespace", + generatedLowerCase: "anamespace" + ), methods: [] ) let serviceB = ServiceDescriptor( documentation: "Documentation for BService", - name: "BService", - namespace: "", + name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bService"), + namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) let expectedSwift = @@ -489,14 +538,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public typealias ClientProtocol = BServiceClientProtocol public typealias Client = BServiceClient } - public enum anamespace { + public enum Anamespace { public enum AService { public enum Methods {} public static let methods: [MethodDescriptor] = [] - public typealias StreamingServiceProtocol = anamespace_AServiceServiceStreamingProtocol - public typealias ServiceProtocol = anamespace_AServiceServiceProtocol - public typealias ClientProtocol = anamespace_AServiceClientProtocol - public typealias Client = anamespace_AServiceClient + public typealias StreamingServiceProtocol = Anamespace_AServiceServiceStreamingProtocol + public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + public typealias ClientProtocol = Anamespace_AServiceClientProtocol + public typealias Client = Anamespace_AServiceClient } } """ From 783e4693fe57f7e1373397d6f96f9fba87700504 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 18 Jan 2024 17:21:16 +0000 Subject: [PATCH 208/580] Fix failing build on some platforms (#1763) --- Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift index ffed5213a..ead6e58d3 100644 --- a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift +++ b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift @@ -149,6 +149,7 @@ extension ServiceContext { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct TestWriter: RPCWriterProtocol { typealias Element = WriterElement From 1e36bdc6e29c365455caaf28c276ae101ca1a569 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:11:16 +0000 Subject: [PATCH 209/580] [CodeGen Protobuf support] Message serializer and deserializer (#1764) Motivation: In the generated code we are specifying types for a serializer and deserializer. As we want to support SwiftProtobuf, we need to implement the generic serializer and deserializer for Protobuf messages, which will be the default if the user doesn't specify other IDL serializer and deserializer. Modifications: Created the serializer and deserializer structs conforming to `GRPCCore.MessageSerializer` and `GRPCCOre.MessageDeserializer` respectively and implemented their serialize and deserialize functions. Added tests for them. Result: The generated code can reference protobuf serializer and deserializer types, as they will be supported in GRPC. --- Package.swift | 23 ++++- Sources/GRPCProtobuf/Coding.swift | 63 ++++++++++++ .../ProtobufCodingTests.swift | 98 +++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCProtobuf/Coding.swift create mode 100644 Tests/GRPCProtobufTests/ProtobufCodingTests.swift diff --git a/Package.swift b/Package.swift index cd9be155f..870d0d16d 100644 --- a/Package.swift +++ b/Package.swift @@ -93,6 +93,7 @@ extension Target.Dependency { static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") static let reflectionService: Self = .target(name: "GRPCReflectionService") static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") + static let grpcProtobuf: Self = .target(name: "GRPCProtobuf") // Target dependencies; internal static let grpcSampleData: Self = .target(name: "GRPCSampleData") @@ -330,6 +331,15 @@ extension Target { ] ) + static let grpcProtobufTests: Target = .testTarget( + name: "GRPCProtobufTests", + dependencies: [ + .grpcCore, + .grpcProtobuf, + .protobuf + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -579,6 +589,15 @@ extension Target { name: "GRPCCodeGen", path: "Sources/GRPCCodeGen" ) + + static let grpcProtobuf: Target = .target( + name: "GRPCProtobuf", + dependencies: [ + .grpcCore, + .protobuf + ], + path: "Sources/GRPCProtobuf" + ) } // MARK: - Products @@ -666,6 +685,7 @@ let package = Package( .grpcHTTP2Core, .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, + .grpcProtobuf, // v2 tests .grpcCoreTests, @@ -674,7 +694,8 @@ let package = Package( .grpcInterceptorsTests, .grpcHTTP2CoreTests, .grpcHTTP2TransportNIOPosixTests, - .grpcHTTP2TransportNIOTransportServicesTests + .grpcHTTP2TransportNIOTransportServicesTests, + .grpcProtobufTests ] ) diff --git a/Sources/GRPCProtobuf/Coding.swift b/Sources/GRPCProtobuf/Coding.swift new file mode 100644 index 000000000..174c87c1e --- /dev/null +++ b/Sources/GRPCProtobuf/Coding.swift @@ -0,0 +1,63 @@ +/* + * 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 Foundation +import GRPCCore +import SwiftProtobuf + +/// Serializes a Protobuf message into a sequence of bytes. +public struct ProtobufSerializer: GRPCCore.MessageSerializer { + public init() {} + + /// Serializes a ``Message`` into a sequence of bytes. + /// + /// - Parameter message: The message to serialize. + /// - Returns: An array of serialized bytes representing the message. + public func serialize(_ message: Message) throws -> [UInt8] { + do { + let data = try message.serializedData() + return Array(data) + } catch let error { + throw RPCError( + code: .invalidArgument, + message: "Can't serialize message of type \(type(of: message)).", + cause: error + ) + } + } +} + +/// Deserializes a sequence of bytes into a Protobuf message. +public struct ProtobufDeserializer: GRPCCore.MessageDeserializer { + public init() {} + + /// Deserializes a sequence of bytes into a ``Message``. + /// + /// - Parameter serializedMessageBytes: The array of bytes to deserialize. + /// - Returns: The deserialized message. + public func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message { + do { + let message = try Message(contiguousBytes: serializedMessageBytes) + return message + } catch let error { + throw RPCError( + code: .invalidArgument, + message: "Can't deserialize to message of type \(Message.self)", + cause: error + ) + } + } +} diff --git a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift new file mode 100644 index 000000000..1c657848c --- /dev/null +++ b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift @@ -0,0 +1,98 @@ +/* + * 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 GRPCProtobuf +import SwiftProtobuf +import XCTest + +final class ProtobufCodingTests: XCTestCase { + func testSerializeDeserializeRoundtrip() throws { + let message = Google_Protobuf_Timestamp.with { + $0.seconds = 4 + } + + let serializer = ProtobufSerializer() + let deserializer = ProtobufDeserializer() + + let bytes = try serializer.serialize(message) + let roundTrip = try deserializer.deserialize(bytes) + XCTAssertEqual(roundTrip, message) + } + + func testSerializerError() throws { + let message = TestMessage() + let serializer = ProtobufSerializer() + + XCTAssertThrowsError( + try serializer.serialize(message) + ) { error in + XCTAssertEqual( + error as? RPCError, + RPCError( + code: .invalidArgument, + message: + """ + Can't serialize message of type TestMessage. + """ + ) + ) + } + } + + func testDeserializerError() throws { + let bytes = Array("%%%%%££££".utf8) + let deserializer = ProtobufDeserializer() + XCTAssertThrowsError( + try deserializer.deserialize(bytes) + ) { error in + XCTAssertEqual( + error as? RPCError, + RPCError( + code: .invalidArgument, + message: + """ + Can't deserialize to message of type TestMessage + """ + ) + ) + } + } +} + +struct TestMessage: SwiftProtobuf.Message { + var text: String = "" + var unknownFields = SwiftProtobuf.UnknownStorage() + static var protoMessageName: String = "Test" + ".ServiceRequest" + init() {} + + mutating func decodeMessage(decoder: inout D) throws where D: SwiftProtobuf.Decoder { + throw RPCError(code: .internalError, message: "Decoding error") + } + + func traverse(visitor: inout V) throws where V: SwiftProtobuf.Visitor { + throw RPCError(code: .internalError, message: "Traversing error") + } + + public var isInitialized: Bool { + if self.text.isEmpty { return false } + return true + } + + func isEqualTo(message: SwiftProtobuf.Message) -> Bool { + return false + } +} From 941f500c3f810801e2bd4dfdfb2f08117eea765c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 22 Jan 2024 14:08:50 +0000 Subject: [PATCH 210/580] Add a handler for managing connections on the server (#1762) Motivation: Servers must manage connections created by clients. Part of this is gracefully closing connections (by sending GOAWAY frames and ratcheting down the last stream ID) in response to various conditions: the client sending too many pings, the connection being idle too long, the connection existing for longer than some configured limit, etc. A previous change added a state machine which handles much of this behaviour. This change adds a channel handler which builds on top of that state machine. Modifications: - Add a channel handler for managing connections on the server. Result: We have a handler in place which can manage connections on the server. --- Package.swift | 5 +- .../Connection/ServerConnectionHandler.swift | 19 - ...ctionManagementHandler+StateMachine.swift} | 9 +- .../ServerConnectionManagementHandler.swift | 473 ++++++++++++++++++ ...ManagementHandler+StateMachineTests.swift} | 6 +- ...rverConnectionManagementHandlerTests.swift | 428 ++++++++++++++++ .../XCTest+FramePayload.swift | 43 ++ 7 files changed, 955 insertions(+), 28 deletions(-) delete mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift rename Sources/GRPCHTTP2Core/Server/Connection/{ServerConnectionHandler+StateMachine.swift => ServerConnectionManagementHandler+StateMachine.swift} (97%) create mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift rename Tests/GRPCHTTP2CoreTests/Server/Connection/{ServerConnectionHandler+StateMachineTests.swift => ServerConnectionManagementHandler+StateMachineTests.swift} (97%) create mode 100644 Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift diff --git a/Package.swift b/Package.swift index 870d0d16d..8d7d65648 100644 --- a/Package.swift +++ b/Package.swift @@ -306,7 +306,10 @@ extension Target { static let grpcHTTP2CoreTests: Target = .testTarget( name: "GRPCHTTP2CoreTests", dependencies: [ - .grpcHTTP2Core + .grpcHTTP2Core, + .nioCore, + .nioHTTP2, + .nioEmbedded, ] ) diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift deleted file mode 100644 index 52dada671..000000000 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler.swift +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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. - */ - -// Temporary namespace. Will be replaced with a channel handler. -enum ServerConnectionHandler { -} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler+StateMachine.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift similarity index 97% rename from Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler+StateMachine.swift rename to Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift index 5a10e41e0..156063adb 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionHandler+StateMachine.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift @@ -17,7 +17,7 @@ import NIOCore import NIOHTTP2 -extension ServerConnectionHandler { +extension ServerConnectionManagementHandler { /// Tracks the state of TCP connections at the server. /// /// The state machine manages the state for the graceful shutdown procedure as well as policing @@ -248,7 +248,7 @@ extension ServerConnectionHandler { } } -extension ServerConnectionHandler.StateMachine { +extension ServerConnectionManagementHandler.StateMachine { fileprivate struct KeepAlive { /// Allow the client to send keep alive pings when there are no active calls. private let allowWithoutCalls: Bool @@ -267,8 +267,7 @@ extension ServerConnectionHandler.StateMachine { /// alive (a low number of strikes is therefore expected and okay). private var pingStrikes: Int - /// The last time a valid ping happened. This may be in the distant past if there is no such - /// time (for example the connection is new and there are no active calls). + /// The last time a valid ping happened. /// /// Note: `distantPast` isn't used to indicate no previous valid ping as `NIODeadline` uses /// the monotonic clock on Linux which uses an undefined starting point and in some cases isn't @@ -320,7 +319,7 @@ extension ServerConnectionHandler.StateMachine { } } -extension ServerConnectionHandler.StateMachine { +extension ServerConnectionManagementHandler.StateMachine { fileprivate enum State { /// The connection is active. struct Active { diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift new file mode 100644 index 000000000..ffdbb8946 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -0,0 +1,473 @@ +/* + * 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 NIOCore +import NIOHTTP2 + +/// A `ChannelHandler` which manages the lifecycle of a gRPC connection over HTTP/2. +/// +/// This handler is responsible for managing several aspects of the connection. These include: +/// 1. Handling the graceful close of connections. When gracefully closing a connection the server +/// sends a GOAWAY frame with the last stream ID set to the maximum stream ID allowed followed by +/// a PING frame. On receipt of the PING frame the server sends another GOAWAY frame with the +/// highest ID of all streams which have been opened. After this, the handler closes the +/// connection once all streams are closed. +/// 2. Enforcing that graceful shutdown doesn't exceed a configured limit (if configured). +/// 3. Gracefully closing the connection once it reaches the maximum configured age (if configured). +/// 4. Gracefully closing the connection once it has been idle for a given period of time (if +/// configured). +/// 5. Periodically sending keep alive pings to the client (if configured) and closing the +/// connection if necessary. +/// 6. Policing pings sent by the client to ensure that the client isn't misconfigured to send +/// too many pings. +/// +/// Some of the behaviours are described in: +/// - [gRFC A8](https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md), and +/// - [gRFC A9](https://github.com/grpc/proposal/blob/master/A9-server-side-conn-mgt.md). +final class ServerConnectionManagementHandler: ChannelDuplexHandler { + typealias InboundIn = HTTP2Frame + typealias InboundOut = HTTP2Frame + typealias OutboundIn = HTTP2Frame + typealias OutboundOut = HTTP2Frame + + /// The `EventLoop` of the `Channel` this handler exists in. + private let eventLoop: EventLoop + + /// The maximum amount of time a connection may be idle for. If the connection remains idle + /// (i.e. has no open streams) for this period of time then the connection will be gracefully + /// closed. + private var maxIdleTimer: Timer? + + /// The maximum age of a connection. If the connection remains open after this amount of time + /// then it will be gracefully closed. + private var maxAgeTimer: Timer? + + /// The maximum amount of time a connection may spend closing gracefully, after which it is + /// closed abruptly. The timer starts after the second GOAWAY frame has been sent. + private var maxGraceTimer: Timer? + + /// The amount of time to wait before sending a keep alive ping. + private var keepAliveTimer: Timer? + + /// The amount of time the client has to reply after sending a keep alive ping. Only used if + /// `keepAliveTimer` is set. + private var keepAliveTimeoutTimer: Timer + + private struct Timer { + /// The delay to wait before running the task. + private let delay: TimeAmount + /// The task to run, if scheduled. + private var task: Scheduled? + + init(delay: TimeAmount) { + self.delay = delay + self.task = nil + } + + /// Schedule a task on the given `EventLoop`. + mutating func schedule(on eventLoop: EventLoop, task: @escaping () throws -> Void) { + self.task?.cancel() + self.task = eventLoop.scheduleTask(in: self.delay, task) + } + + /// Cancels the task, if one was scheduled. + mutating func cancel() { + self.task?.cancel() + self.task = nil + } + } + + /// Opaque data sent in keep alive pings. + private let keepAlivePingData: HTTP2PingData + + /// Whether a flush is pending. + private var flushPending: Bool + /// Whether `channelRead` has been called and `channelReadComplete` hasn't yet been called. + /// Resets once `channelReadComplete` returns. + private var inReadLoop: Bool + + /// The current state of the connection. + private var state: StateMachine + + /// The clock. + private let clock: Clock + + /// A clock providing the current time. + /// + /// This is necessary for testing where a manual clock can be used and advanced from the test. + /// While NIO's `EmbeddedEventLoop` provides control over its view of time (and therefore any + /// events scheduled on it) it doesn't offer a way to get the current time. This is usually done + /// via `NIODeadline`. + enum Clock { + case nio + case manual(Manual) + + func now() -> NIODeadline { + switch self { + case .nio: + return .now() + case .manual(let clock): + return clock.time + } + } + + final class Manual { + private(set) var time: NIODeadline + + init() { + self.time = .uptimeNanoseconds(0) + } + + func advance(by amount: TimeAmount) { + self.time = self.time + amount + } + } + } + + /// Stats about recently written frames. Used to determine whether to reset keep-alive state. + private var frameStats: FrameStats + + struct FrameStats { + private(set) var didWriteHeadersOrData = false + + /// Mark that a HEADERS frame has been written. + mutating func wroteHeaders() { + self.didWriteHeadersOrData = true + } + + /// Mark that DATA frame has been written. + mutating func wroteData() { + self.didWriteHeadersOrData = true + } + + /// Resets the state such that no HEADERS or DATA frames have been written. + mutating func reset() { + self.didWriteHeadersOrData = false + } + } + + /// A synchronous view over this handler. + var syncView: SyncView { + return SyncView(self) + } + + /// A synchronous view over this handler. + /// + /// Methods on this view *must* be called from the same `EventLoop` as the `Channel` in which + /// this handler exists. + struct SyncView { + private let handler: ServerConnectionManagementHandler + + fileprivate init(_ handler: ServerConnectionManagementHandler) { + self.handler = handler + } + + /// Notify the handler that the connection has received a flush event. + func connectionWillFlush() { + // The handler can't rely on `flush(context:)` due to its expected position in the pipeline. + // It's expected to be placed after the HTTP/2 handler (i.e. closer to the application) as + // it needs to receive HTTP/2 frames. However, flushes from stream channels aren't sent down + // the entire connection channel, instead they are sent from the point in the channel they + // are multiplexed from (either the HTTP/2 handler or the HTTP/2 multiplexing handler, + // depending on how multiplexing is configured). + self.handler.eventLoop.assertInEventLoop() + if self.handler.frameStats.didWriteHeadersOrData { + self.handler.frameStats.reset() + self.handler.state.resetKeepAliveState() + } + } + + /// Notify the handler that a HEADERS frame was written in the last write loop. + func wroteHeadersFrame() { + self.handler.eventLoop.assertInEventLoop() + self.handler.frameStats.wroteHeaders() + } + + /// Notify the handler that a DATA frame was written in the last write loop. + func wroteDataFrame() { + self.handler.eventLoop.assertInEventLoop() + self.handler.frameStats.wroteData() + } + } + + /// Creates a new handler which manages the lifecycle of a connection. + /// + /// - Parameters: + /// - eventLoop: The `EventLoop` of the `Channel` this handler is placed in. + /// - maxIdleTime: The maximum amount time a connection may be idle for before being closed. + /// - maxAge: The maximum amount of time a connection may exist before being gracefully closed. + /// - maxGraceTime: The maximum amount of time that the connection has to close gracefully. + /// - keepAliveTime: The amount of time to wait after reading data before sending a keep-alive + /// ping. + /// - keepAliveTimeout: The amount of time the client has to reply after the server sends a + /// keep-alive ping to keep the connection open. The connection is closed if no reply + /// is received. + /// - allowKeepAliveWithoutCalls: Whether the server allows the client to send keep-alive pings + /// when there are no calls in progress. + /// - minPingIntervalWithoutCalls: The minimum allowed interval the client is allowed to send + /// keep-alive pings. Pings more frequent than this interval count as 'strikes' and the + /// connection is closed if there are too many strikes. + /// - clock: A clock providing the current time. + init( + eventLoop: EventLoop, + maxIdleTime: TimeAmount?, + maxAge: TimeAmount?, + maxGraceTime: TimeAmount?, + keepAliveTime: TimeAmount?, + keepAliveTimeout: TimeAmount?, + allowKeepAliveWithoutCalls: Bool, + minPingIntervalWithoutCalls: TimeAmount, + clock: Clock = .nio + ) { + self.eventLoop = eventLoop + + self.maxIdleTimer = maxIdleTime.map { Timer(delay: $0) } + self.maxAgeTimer = maxAge.map { Timer(delay: $0) } + self.maxGraceTimer = maxGraceTime.map { Timer(delay: $0) } + + self.keepAliveTimer = keepAliveTime.map { Timer(delay: $0) } + // Always create a keep alive timeout timer, it's only used if there is a keep alive timer. + self.keepAliveTimeoutTimer = Timer(delay: keepAliveTimeout ?? .seconds(20)) + + // Generate a random value to be used as keep alive ping data. + let pingData = UInt64.random(in: .min ... .max) + self.keepAlivePingData = HTTP2PingData(withInteger: pingData) + + self.state = StateMachine( + allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls, + minPingReceiveIntervalWithoutCalls: minPingIntervalWithoutCalls, + goAwayPingData: HTTP2PingData(withInteger: ~pingData) + ) + + self.flushPending = false + self.inReadLoop = false + self.clock = clock + self.frameStats = FrameStats() + } + + func handlerAdded(context: ChannelHandlerContext) { + assert(context.eventLoop === self.eventLoop) + } + + func channelActive(context: ChannelHandlerContext) { + self.maxAgeTimer?.schedule(on: context.eventLoop) { + self.initiateGracefulShutdown(context: context) + } + + self.maxIdleTimer?.schedule(on: context.eventLoop) { + self.initiateGracefulShutdown(context: context) + } + + self.keepAliveTimer?.schedule(on: context.eventLoop) { + self.keepAliveTimerFired(context: context) + } + + context.fireChannelActive() + } + + func channelInactive(context: ChannelHandlerContext) { + self.maxIdleTimer?.cancel() + self.maxAgeTimer?.cancel() + self.maxGraceTimer?.cancel() + self.keepAliveTimer?.cancel() + self.keepAliveTimeoutTimer.cancel() + context.fireChannelInactive() + } + + func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { + switch event { + case let event as NIOHTTP2StreamCreatedEvent: + // The connection isn't idle if a stream is open. + self.maxIdleTimer?.cancel() + self.state.streamOpened(event.streamID) + + case let event as StreamClosedEvent: + switch self.state.streamClosed(event.streamID) { + case .startIdleTimer: + self.maxIdleTimer?.schedule(on: context.eventLoop) { + self.initiateGracefulShutdown(context: context) + } + + case .close: + context.close(mode: .all, promise: nil) + + case .none: + () + } + + default: + () + } + + context.fireUserInboundEventTriggered(event) + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + self.inReadLoop = true + + // Any read data indicates that the connection is alive so cancel the keep-alive timers. + self.keepAliveTimer?.cancel() + self.keepAliveTimeoutTimer.cancel() + + let frame = self.unwrapInboundIn(data) + switch frame.payload { + case .ping(let data, let ack): + if ack { + self.handlePingAck(context: context, data: data) + } else { + self.handlePing(context: context, data: data) + } + + default: + () // Only interested in PING frames, ignore the rest. + } + + context.fireChannelRead(data) + } + + func channelReadComplete(context: ChannelHandlerContext) { + while self.flushPending { + self.flushPending = false + context.flush() + } + + self.inReadLoop = false + + // Done reading: schedule the keep-alive timer. + self.keepAliveTimer?.schedule(on: context.eventLoop) { + self.keepAliveTimerFired(context: context) + } + + context.fireChannelReadComplete() + } + + func flush(context: ChannelHandlerContext) { + self.maybeFlush(context: context) + } +} + +extension ServerConnectionManagementHandler { + private func maybeFlush(context: ChannelHandlerContext) { + if self.inReadLoop { + self.flushPending = true + } else { + context.flush() + } + } + + private func initiateGracefulShutdown(context: ChannelHandlerContext) { + context.eventLoop.assertInEventLoop() + + // Cancel any timers if initiating shutdown. + self.maxIdleTimer?.cancel() + self.maxAgeTimer?.cancel() + self.keepAliveTimer?.cancel() + self.keepAliveTimeoutTimer.cancel() + + switch self.state.startGracefulShutdown() { + case .sendGoAwayAndPing(let pingData): + // There's a time window between the server sending a GOAWAY frame and the client receiving + // it. During this time the client may open new streams as it doesn't yet know about the + // GOAWAY frame. + // + // The server therefore sends a GOAWAY with the last stream ID set to the maximum stream ID + // and follows it with a PING frame. When the server receives the ack for the PING frame it + // knows that the client has received the initial GOAWAY frame and that no more streams may + // be opened. The server can then send an additional GOAWAY frame with a more representative + // last stream ID. + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway( + lastStreamID: .maxID, + errorCode: .noError, + opaqueData: nil + ) + ) + + let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(pingData, ack: false)) + + context.write(self.wrapOutboundOut(goAway), promise: nil) + context.write(self.wrapOutboundOut(ping), promise: nil) + self.maybeFlush(context: context) + + case .none: + () // Already shutting down. + } + } + + private func handlePing(context: ChannelHandlerContext, data: HTTP2PingData) { + switch self.state.receivedPing(atTime: self.clock.now(), data: data) { + case .enhanceYourCalmThenClose(let streamID): + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway( + lastStreamID: streamID, + errorCode: .enhanceYourCalm, + opaqueData: context.channel.allocator.buffer(string: "too_many_pings") + ) + ) + + context.write(self.wrapOutboundOut(goAway), promise: nil) + self.maybeFlush(context: context) + context.close(promise: nil) + + case .sendAck: + let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(data, ack: true)) + context.write(self.wrapOutboundOut(ping), promise: nil) + self.maybeFlush(context: context) + + case .none: + () + } + } + + private func handlePingAck(context: ChannelHandlerContext, data: HTTP2PingData) { + switch self.state.receivedPingAck(data: data) { + case .sendGoAway(let streamID, let close): + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway(lastStreamID: streamID, errorCode: .noError, opaqueData: nil) + ) + + context.write(self.wrapOutboundOut(goAway), promise: nil) + self.maybeFlush(context: context) + + if close { + context.close(promise: nil) + } else { + // RPCs may have a grace period for finishing once the second GOAWAY frame has finished. + // If this is set close the connection abruptly once the grace period passes. + self.maxGraceTimer?.schedule(on: context.eventLoop) { + context.close(promise: nil) + } + } + + case .none: + () + } + } + + private func keepAliveTimerFired(context: ChannelHandlerContext) { + let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepAlivePingData, ack: false)) + context.write(self.wrapInboundOut(ping), promise: nil) + self.maybeFlush(context: context) + + // Schedule a timeout on waiting for the response. + self.keepAliveTimeoutTimer.schedule(on: context.eventLoop) { + self.initiateGracefulShutdown(context: context) + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionHandler+StateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift similarity index 97% rename from Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionHandler+StateMachineTests.swift rename to Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift index ec4671c8a..47daf4d58 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionHandler+StateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift @@ -20,12 +20,12 @@ import XCTest @testable import GRPCHTTP2Core -final class ServerConnectionHandlerStateMachineTests: XCTestCase { +final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase { private func makeStateMachine( allowKeepAliveWithoutCalls: Bool = false, minPingReceiveIntervalWithoutCalls: TimeAmount = .minutes(5), goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: 42) - ) -> ServerConnectionHandler.StateMachine { + ) -> ServerConnectionManagementHandler.StateMachine { return .init( allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls, minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls, @@ -169,7 +169,7 @@ final class ServerConnectionHandlerStateMachineTests: XCTestCase { } func testPingStrikeUsingMinReceiveInterval( - state: inout ServerConnectionHandler.StateMachine, + state: inout ServerConnectionManagementHandler.StateMachine, interval: TimeAmount, expectedID id: HTTP2StreamID ) { diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift new file mode 100644 index 000000000..abbd6bb52 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift @@ -0,0 +1,428 @@ +/* + * 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 NIOCore +import NIOEmbedded +import NIOHTTP2 +import XCTest + +@testable import GRPCHTTP2Core + +final class ServerConnectionManagementHandlerTests: XCTestCase { + func testIdleTimeoutOnNewConnection() throws { + let connection = try Connection(maxIdleTime: .minutes(1)) + try connection.activate() + // Hit the max idle time. + connection.advanceTime(by: .minutes(1)) + + // Follow the graceful shutdown flow. + try self.testGracefulShutdown(connection: connection, lastStreamID: 0) + + // Closed because no streams were open. + try connection.waitUntilClosed() + } + + func testIdleTimerIsCancelledWhenStreamIsOpened() throws { + let connection = try Connection(maxIdleTime: .minutes(1)) + try connection.activate() + + // Open a stream to cancel the idle timer and run through the max idle time. + connection.streamOpened(1) + connection.advanceTime(by: .minutes(1)) + + // No GOAWAY frame means the timer was cancelled. + XCTAssertNil(try connection.readFrame()) + } + + func testIdleTimerStartsWhenAllStreamsAreClosed() throws { + let connection = try Connection(maxIdleTime: .minutes(1)) + try connection.activate() + + // Open a stream to cancel the idle timer and run through the max idle time. + connection.streamOpened(1) + connection.advanceTime(by: .minutes(1)) + XCTAssertNil(try connection.readFrame()) + + // Close the stream to start the timer again. + connection.streamClosed(1) + connection.advanceTime(by: .minutes(1)) + + // Follow the graceful shutdown flow. + try self.testGracefulShutdown(connection: connection, lastStreamID: 1) + + // Closed because no streams were open. + try connection.waitUntilClosed() + } + + func testMaxAge() throws { + let connection = try Connection(maxAge: .minutes(1)) + try connection.activate() + + // Open some streams. + connection.streamOpened(1) + connection.streamOpened(3) + + // Run to the max age and follow the graceful shutdown flow. + connection.advanceTime(by: .minutes(1)) + try self.testGracefulShutdown(connection: connection, lastStreamID: 3) + + // Close the streams. + connection.streamClosed(1) + connection.streamClosed(3) + + // Connection will be closed now. + try connection.waitUntilClosed() + } + + func testGracefulShutdownRatchetsDownStreamID() throws { + // This test uses the idle timeout to trigger graceful shutdown. The mechanism is the same + // regardless of how it's triggered. + let connection = try Connection(maxIdleTime: .minutes(1)) + try connection.activate() + + // Trigger the shutdown, but open a stream during shutdown. + connection.advanceTime(by: .minutes(1)) + try self.testGracefulShutdown( + connection: connection, + lastStreamID: 1, + streamToOpenBeforePingAck: 1 + ) + + // Close the stream to trigger closing the connection. + connection.streamClosed(1) + try connection.waitUntilClosed() + } + + func testGracefulShutdownGracePeriod() throws { + // This test uses the idle timeout to trigger graceful shutdown. The mechanism is the same + // regardless of how it's triggered. + let connection = try Connection( + maxIdleTime: .minutes(1), + maxGraceTime: .seconds(5) + ) + try connection.activate() + + // Trigger the shutdown, but open a stream during shutdown. + connection.advanceTime(by: .minutes(1)) + try self.testGracefulShutdown( + connection: connection, + lastStreamID: 1, + streamToOpenBeforePingAck: 1 + ) + + // Wait out the grace period without closing the stream. + connection.advanceTime(by: .seconds(5)) + try connection.waitUntilClosed() + } + + func testKeepAliveOnNewConnection() throws { + let connection = try Connection( + keepAliveTime: .minutes(5), + keepAliveTimeout: .seconds(5) + ) + try connection.activate() + + // Wait for the keep alive timer to fire which should cause the server to send a keep + // alive PING. + connection.advanceTime(by: .minutes(5)) + let frame1 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame1.streamID, .rootStream) + try XCTAssertPing(frame1.payload) { data, ack in + XCTAssertFalse(ack) + // Data is opaque, send it back. + try connection.ping(data: data, ack: true) + } + + // Run past the timeout, nothing should happen. + connection.advanceTime(by: .seconds(5)) + XCTAssertNil(try connection.readFrame()) + } + + func testKeepAliveStartsAfterReadLoop() throws { + let connection = try Connection( + keepAliveTime: .minutes(5), + keepAliveTimeout: .seconds(5) + ) + try connection.activate() + + // Write a frame into the channel _without_ calling channel read complete. This will cancel + // the keep alive timer. + let settings = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) + connection.channel.pipeline.fireChannelRead(NIOAny(settings)) + + // Run out the keep alive timer, it shouldn't fire. + connection.advanceTime(by: .minutes(5)) + XCTAssertNil(try connection.readFrame()) + + // Fire channel read complete to start the keep alive timer again. + connection.channel.pipeline.fireChannelReadComplete() + + // Now expire the keep alive timer again, we should read out a PING frame. + connection.advanceTime(by: .minutes(5)) + let frame1 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame1.streamID, .rootStream) + XCTAssertPing(frame1.payload) { data, ack in + XCTAssertFalse(ack) + } + } + + func testKeepAliveOnNewConnectionWithoutResponse() throws { + let connection = try Connection( + keepAliveTime: .minutes(5), + keepAliveTimeout: .seconds(5) + ) + try connection.activate() + + // Wait for the keep alive timer to fire which should cause the server to send a keep + // alive PING. + connection.advanceTime(by: .minutes(5)) + let frame1 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame1.streamID, .rootStream) + XCTAssertPing(frame1.payload) { data, ack in + XCTAssertFalse(ack) + } + + // We didn't ack the PING, the connection should shutdown after the timeout. + connection.advanceTime(by: .seconds(5)) + try self.testGracefulShutdown(connection: connection, lastStreamID: 0) + + // Connection is closed now. + try connection.waitUntilClosed() + } + + func testClientKeepAlivePolicing() throws { + let connection = try Connection( + allowKeepAliveWithoutCalls: true, + minPingIntervalWithoutCalls: .minutes(1) + ) + try connection.activate() + + // The first ping is valid, the second and third are strikes. + for _ in 1 ... 3 { + try connection.ping(data: HTTP2PingData(), ack: false) + let frame = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame.streamID, .rootStream) + XCTAssertPing(frame.payload) { data, ack in + XCTAssertEqual(data, HTTP2PingData()) + XCTAssertTrue(ack) + } + } + + // The fourth ping is the third strike and triggers a GOAWAY. + try connection.ping(data: HTTP2PingData(), ack: false) + let frame = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame.streamID, .rootStream) + XCTAssertGoAway(frame.payload) { streamID, error, data in + XCTAssertEqual(streamID, .rootStream) + XCTAssertEqual(error, .enhanceYourCalm) + XCTAssertEqual(data, ByteBuffer(string: "too_many_pings")) + } + + // The server should close the connection. + try connection.waitUntilClosed() + } + + func testClientKeepAliveWithPermissibleIntervals() throws { + let connection = try Connection( + allowKeepAliveWithoutCalls: true, + minPingIntervalWithoutCalls: .minutes(1), + manualClock: true + ) + try connection.activate() + + for _ in 1 ... 100 { + try connection.ping(data: HTTP2PingData(), ack: false) + let frame = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame.streamID, .rootStream) + XCTAssertPing(frame.payload) { data, ack in + XCTAssertEqual(data, HTTP2PingData()) + XCTAssertTrue(ack) + } + + // Advance by the ping interval. + connection.advanceTime(by: .minutes(1)) + } + } + + func testClientKeepAliveResetState() throws { + let connection = try Connection( + allowKeepAliveWithoutCalls: true, + minPingIntervalWithoutCalls: .minutes(1) + ) + try connection.activate() + + func sendThreeKeepAlivePings() throws { + // The first ping is valid, the second and third are strikes. + for _ in 1 ... 3 { + try connection.ping(data: HTTP2PingData(), ack: false) + let frame = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame.streamID, .rootStream) + XCTAssertPing(frame.payload) { data, ack in + XCTAssertEqual(data, HTTP2PingData()) + XCTAssertTrue(ack) + } + } + } + + try sendThreeKeepAlivePings() + + // "send" a HEADERS frame and flush to reset keep alive state. + connection.syncView.wroteHeadersFrame() + connection.syncView.connectionWillFlush() + + // As above, the first ping is valid, the next two are strikes. + try sendThreeKeepAlivePings() + + // The next ping is the third strike and triggers a GOAWAY. + try connection.ping(data: HTTP2PingData(), ack: false) + let frame = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame.streamID, .rootStream) + XCTAssertGoAway(frame.payload) { streamID, error, data in + XCTAssertEqual(streamID, .rootStream) + XCTAssertEqual(error, .enhanceYourCalm) + XCTAssertEqual(data, ByteBuffer(string: "too_many_pings")) + } + + // The server should close the connection. + try connection.waitUntilClosed() + } +} + +extension ServerConnectionManagementHandlerTests { + private func testGracefulShutdown( + connection: Connection, + lastStreamID: HTTP2StreamID, + streamToOpenBeforePingAck: HTTP2StreamID? = nil + ) throws { + let frame1 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame1.streamID, .rootStream) + XCTAssertGoAway(frame1.payload) { streamID, errorCode, _ in + XCTAssertEqual(streamID, .maxID) + XCTAssertEqual(errorCode, .noError) + } + + // Followed by a PING + let frame2 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame2.streamID, .rootStream) + try XCTAssertPing(frame2.payload) { data, ack in + XCTAssertFalse(ack) + + if let id = streamToOpenBeforePingAck { + connection.streamOpened(id) + } + + // Send the PING ACK. + try connection.ping(data: data, ack: true) + } + + // PING ACK triggers another GOAWAY. + let frame3 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame3.streamID, .rootStream) + XCTAssertGoAway(frame3.payload) { streamID, errorCode, _ in + XCTAssertEqual(streamID, lastStreamID) + XCTAssertEqual(errorCode, .noError) + } + } +} + +extension ServerConnectionManagementHandlerTests { + struct Connection { + let channel: EmbeddedChannel + let syncView: ServerConnectionManagementHandler.SyncView + + var loop: EmbeddedEventLoop { + self.channel.embeddedEventLoop + } + + private let clock: ServerConnectionManagementHandler.Clock + + init( + maxIdleTime: TimeAmount? = nil, + maxAge: TimeAmount? = nil, + maxGraceTime: TimeAmount? = nil, + keepAliveTime: TimeAmount? = nil, + keepAliveTimeout: TimeAmount? = nil, + allowKeepAliveWithoutCalls: Bool = false, + minPingIntervalWithoutCalls: TimeAmount = .minutes(5), + manualClock: Bool = false + ) throws { + if manualClock { + self.clock = .manual(ServerConnectionManagementHandler.Clock.Manual()) + } else { + self.clock = .nio + } + + let loop = EmbeddedEventLoop() + let handler = ServerConnectionManagementHandler( + eventLoop: loop, + maxIdleTime: maxIdleTime, + maxAge: maxAge, + maxGraceTime: maxGraceTime, + keepAliveTime: keepAliveTime, + keepAliveTimeout: keepAliveTimeout, + allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls, + minPingIntervalWithoutCalls: minPingIntervalWithoutCalls, + clock: self.clock + ) + + self.syncView = handler.syncView + self.channel = EmbeddedChannel(handler: handler, loop: loop) + } + + func activate() throws { + try self.channel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() + } + + func advanceTime(by delta: TimeAmount) { + switch self.clock { + case .nio: + () + case .manual(let clock): + clock.advance(by: delta) + } + + self.loop.advanceTime(by: delta) + } + + func streamOpened(_ id: HTTP2StreamID) { + let event = NIOHTTP2StreamCreatedEvent( + streamID: id, + localInitialWindowSize: nil, + remoteInitialWindowSize: nil + ) + self.channel.pipeline.fireUserInboundEventTriggered(event) + } + + func streamClosed(_ id: HTTP2StreamID) { + let event = StreamClosedEvent(streamID: id, reason: nil) + self.channel.pipeline.fireUserInboundEventTriggered(event) + } + + func ping(data: HTTP2PingData, ack: Bool) throws { + let frame = HTTP2Frame(streamID: .rootStream, payload: .ping(data, ack: ack)) + try self.channel.writeInbound(frame) + } + + func readFrame() throws -> HTTP2Frame? { + return try self.channel.readOutbound(as: HTTP2Frame.self) + } + + func waitUntilClosed() throws { + self.channel.embeddedEventLoop.run() + try self.channel.closeFuture.wait() + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift b/Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift new file mode 100644 index 000000000..b6892d0db --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift @@ -0,0 +1,43 @@ +/* + * 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 NIOCore +import NIOHTTP2 +import XCTest + +func XCTAssertGoAway( + _ payload: HTTP2Frame.FramePayload, + verify: (HTTP2StreamID, HTTP2ErrorCode, ByteBuffer?) throws -> Void = { _, _, _ in } +) rethrows { + switch payload { + case .goAway(let lastStreamID, let errorCode, let opaqueData): + try verify(lastStreamID, errorCode, opaqueData) + default: + XCTFail("Expected '.goAway' got '\(payload)'") + } +} + +func XCTAssertPing( + _ payload: HTTP2Frame.FramePayload, + verify: (HTTP2PingData, Bool) throws -> Void = { _, _ in } +) rethrows { + switch payload { + case .ping(let data, ack: let ack): + try verify(data, ack) + default: + XCTFail("Expected '.ping' got '\(payload)'") + } +} From 3a78b54dc56ee5209d2a7ec674815f68eb5a6937 Mon Sep 17 00:00:00 2001 From: Severn Date: Mon, 22 Jan 2024 10:22:15 -0500 Subject: [PATCH 211/580] Fix no output when Client=False,TestClient=True (#1765) --- Sources/protoc-gen-grpc-swift/main.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index d4b159cb2..05d7c3ecb 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -156,7 +156,9 @@ func main(args: [String]) throws { binaryFile.content = serializedFileDescriptorProto response.file.append(binaryFile) } - if !fileDescriptor.services.isEmpty && (options.generateClient || options.generateServer) { + if !fileDescriptor.services.isEmpty + && (options.generateClient || options.generateServer || options.generateTestClient) + { var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() let grpcFileName = uniqueOutputFileName( component: "grpc", From 7b2c8c8394252ffa0d356caddf4ef3fbc3e064b0 Mon Sep 17 00:00:00 2001 From: Severn Date: Mon, 22 Jan 2024 11:07:20 -0500 Subject: [PATCH 212/580] Fix format.sh -f printing usage (#1767) --- scripts/format.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/format.sh b/scripts/format.sh index 748a023aa..d995546f9 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -25,14 +25,20 @@ function usage() { echo >&2 " $0 -[f|l]" echo >&2 "" echo >&2 "Options:" - echo >&2 " -f Format source code in place" + echo >&2 " -f Format source code in place (default)" echo >&2 " -l Lint check without formatting the source code" } +format=true lint=false -while getopts ":lh" opt; do +while getopts ":flh" opt; do case "$opt" in + f) + format=true + lint=false + ;; l) + format=false lint=true ;; h) @@ -99,7 +105,7 @@ if "$lint"; then fi log "Ran swift-format lint with no errors." -else +elif "$format"; then "${SWIFTFORMAT_BIN}" format \ --parallel --recursive --in-place \ "${REPO}/Sources" "${REPO}/Tests" \ @@ -110,4 +116,6 @@ else fi log "Ran swift-format with no errors." +else + fatal "No actions taken." fi From 91f9bd8d841e07f593a70fc55f39ea5d2683a2c0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 22 Jan 2024 17:18:21 +0000 Subject: [PATCH 213/580] Add zlib compressor and decompressor (#1766) Motivation: v2 should support compression. Modifications: - Add a Zlib compressor and decompressor supporting deflate and gzip. - This is loosely based on the code we had in v1 but refactored to fit slightly different requirements. Result: Can compress and decompress messages using deflate/gzip --- Package.swift | 3 +- Sources/GRPCHTTP2Core/Compression/Zlib.swift | 423 ++++++++++++++++++ .../Server/Compression/ZlibTests.swift | 150 +++++++ .../Test Utilities/XCTest+Utilities.swift | 30 ++ 4 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCHTTP2Core/Compression/Zlib.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift diff --git a/Package.swift b/Package.swift index 8d7d65648..93163449f 100644 --- a/Package.swift +++ b/Package.swift @@ -201,7 +201,8 @@ extension Target { dependencies: [ .grpcCore, .nioCore, - .nioHTTP2 + .nioHTTP2, + .cgrpcZlib, ] ) diff --git a/Sources/GRPCHTTP2Core/Compression/Zlib.swift b/Sources/GRPCHTTP2Core/Compression/Zlib.swift new file mode 100644 index 000000000..f8abeff98 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Compression/Zlib.swift @@ -0,0 +1,423 @@ +/* + * 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 CGRPCZlib +import GRPCCore +import NIOCore + +enum Zlib { + enum Method { + case deflate + case gzip + + fileprivate var windowBits: Int32 { + switch self { + case .deflate: + return 15 + case .gzip: + return 31 + } + } + } +} + +extension Zlib { + /// Creates a new compressor for the given compression format. + /// + /// This compressor is only suitable for compressing whole messages at a time. Callers + /// must ``initialize()`` the compressor before using it. + struct Compressor { + private var stream: z_stream + private let method: Method + private var isInitialized = false + + init(method: Method) { + self.method = method + self.stream = z_stream() + } + + /// Initialize the compressor. + mutating func initialize() { + precondition(!self.isInitialized) + self.stream.deflateInit(windowBits: self.method.windowBits) + self.isInitialized = true + } + + static func initialized(_ method: Method) -> Self { + var compressor = Compressor(method: method) + compressor.initialize() + return compressor + } + + /// Compresses the data in `input` into the `output` buffer. + /// + /// - Parameter input: The complete data to be compressed. + /// - Parameter output: The `ByteBuffer` into which the compressed message should be written. + /// - Returns: The number of bytes written into the `output` buffer. + @discardableResult + mutating func compress(_ input: [UInt8], into output: inout ByteBuffer) throws -> Int { + precondition(self.isInitialized) + defer { self.reset() } + let upperBound = self.stream.deflateBound(inputBytes: input.count) + return try self.stream.deflate(input, into: &output, upperBound: upperBound) + } + + /// Resets compression state. + private mutating func reset() { + do { + try self.stream.deflateReset() + } catch { + self.end() + self.stream = z_stream() + self.stream.deflateInit(windowBits: self.method.windowBits) + } + } + + /// Deallocates any resources allocated by Zlib. + mutating func end() { + self.stream.deflateEnd() + } + } +} + +extension Zlib { + /// Creates a new decompressor for the given compression format. + /// + /// This decompressor is only suitable for compressing whole messages at a time. Callers + /// must ``initialize()`` the decompressor before using it. + struct Decompressor { + private var stream: z_stream + private let method: Method + private var isInitialized = false + + init(method: Method) { + self.method = method + self.stream = z_stream() + } + + mutating func initialize() { + precondition(!self.isInitialized) + self.stream.inflateInit(windowBits: self.method.windowBits) + self.isInitialized = true + } + + /// Returns the decompressed bytes from ``input``. + /// + /// - Parameters: + /// - input: The buffer read compressed bytes from. + /// - limit: The largest size a decompressed payload may be. + mutating func decompress(_ input: inout ByteBuffer, limit: Int) throws -> [UInt8] { + precondition(self.isInitialized) + defer { self.reset() } + return try self.stream.inflate(input: &input, limit: limit) + } + + /// Resets decompression state. + private mutating func reset() { + do { + try self.stream.inflateReset() + } catch { + self.end() + self.stream = z_stream() + self.stream.inflateInit(windowBits: self.method.windowBits) + } + } + + /// Deallocates any resources allocated by Zlib. + mutating func end() { + self.stream.inflateEnd() + } + } +} + +struct ZlibError: Error, Hashable { + /// Error code returned from Zlib. + var code: Int + /// Error message produced by Zlib. + var message: String + + init(code: Int, message: String) { + self.code = code + self.message = message + } +} + +extension z_stream { + mutating func inflateInit(windowBits: Int32) { + self.zfree = nil + self.zalloc = nil + self.opaque = nil + + let rc = CGRPCZlib_inflateInit2(&self, windowBits) + // Possible return codes: + // - Z_OK + // - Z_MEM_ERROR: not enough memory + // + // If we can't allocate memory then we can't progress anyway so not throwing an error here is + // okay. + precondition(rc == Z_OK, "inflateInit2 failed with error (\(rc)) \(self.lastError ?? "")") + } + + mutating func inflateReset() throws { + let rc = CGRPCZlib_inflateReset(&self) + + // Possible return codes: + // - Z_OK + // - Z_STREAM_ERROR: the source stream state was inconsistent. + switch rc { + case Z_OK: + () + case Z_STREAM_ERROR: + throw ZlibError(code: Int(rc), message: self.lastError ?? "") + default: + preconditionFailure("inflateReset returned unexpected code (\(rc))") + } + } + + mutating func inflateEnd() { + _ = CGRPCZlib_inflateEnd(&self) + } + + mutating func deflateInit(windowBits: Int32) { + self.zfree = nil + self.zalloc = nil + self.opaque = nil + + let rc = CGRPCZlib_deflateInit2( + &self, + Z_DEFAULT_COMPRESSION, // compression level + Z_DEFLATED, // compression method (this must be Z_DEFLATED) + windowBits, // window size, i.e. deflate/gzip + 8, // memory level (this is the default value in the docs) + Z_DEFAULT_STRATEGY // compression strategy + ) + + // Possible return codes: + // - Z_OK + // - Z_MEM_ERROR: not enough memory + // - Z_STREAM_ERROR: a parameter was invalid + // + // If we can't allocate memory then we can't progress anyway, and we control the parameters + // so not throwing an error here is okay. + precondition(rc == Z_OK, "deflateInit2 failed with error (\(rc)) \(self.lastError ?? "")") + } + + mutating func deflateReset() throws { + let rc = CGRPCZlib_deflateReset(&self) + + // Possible return codes: + // - Z_OK + // - Z_STREAM_ERROR: the source stream state was inconsistent. + switch rc { + case Z_OK: + () + case Z_STREAM_ERROR: + throw ZlibError(code: Int(rc), message: self.lastError ?? "") + default: + preconditionFailure("deflateReset returned unexpected code (\(rc))") + } + } + + mutating func deflateEnd() { + _ = CGRPCZlib_deflateEnd(&self) + } + + mutating func deflateBound(inputBytes: Int) -> Int { + let bound = CGRPCZlib_deflateBound(&self, UInt(inputBytes)) + return Int(bound) + } + + mutating func setNextInputBuffer(_ buffer: UnsafeMutableBufferPointer) { + if let baseAddress = buffer.baseAddress { + self.next_in = baseAddress + self.avail_in = UInt32(buffer.count) + } else { + self.next_in = nil + self.avail_in = 0 + } + } + + mutating func setNextInputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { + if let buffer = buffer, let baseAddress = buffer.baseAddress { + self.next_in = CGRPCZlib_castVoidToBytefPointer(baseAddress) + self.avail_in = UInt32(buffer.count) + } else { + self.next_in = nil + self.avail_in = 0 + } + } + + mutating func setNextOutputBuffer(_ buffer: UnsafeMutableBufferPointer) { + if let baseAddress = buffer.baseAddress { + self.next_out = baseAddress + self.avail_out = UInt32(buffer.count) + } else { + self.next_out = nil + self.avail_out = 0 + } + } + + mutating func setNextOutputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { + if let buffer = buffer, let baseAddress = buffer.baseAddress { + self.next_out = CGRPCZlib_castVoidToBytefPointer(baseAddress) + self.avail_out = UInt32(buffer.count) + } else { + self.next_out = nil + self.avail_out = 0 + } + } + + /// Number of bytes available to read `self.nextInputBuffer`. See also: `z_stream.avail_in`. + var availableInputBytes: Int { + get { + Int(self.avail_in) + } + set { + self.avail_in = UInt32(newValue) + } + } + + /// The remaining writable space in `nextOutputBuffer`. See also: `z_stream.avail_out`. + var availableOutputBytes: Int { + get { + Int(self.avail_out) + } + set { + self.avail_out = UInt32(newValue) + } + } + + /// The total number of bytes written to the output buffer. See also: `z_stream.total_out`. + var totalOutputBytes: Int { + Int(self.total_out) + } + + /// The last error message that zlib wrote. No message is guaranteed on error, however, `nil` is + /// guaranteed if there is no error. See also `z_stream.msg`. + var lastError: String? { + self.msg.map { String(cString: $0) } + } + + mutating func inflate(input: inout ByteBuffer, limit: Int) throws -> [UInt8] { + return try input.readWithUnsafeMutableReadableBytes { inputPointer in + self.setNextInputBuffer(inputPointer) + defer { + self.setNextInputBuffer(nil) + self.setNextOutputBuffer(nil) + } + + // Assume the output will be twice as large as the input. + var output = [UInt8](repeating: 0, count: min(inputPointer.count * 2, limit)) + var offset = 0 + + while true { + let (finished, written) = try output[offset...].withUnsafeMutableBytes { outPointer in + self.setNextOutputBuffer(outPointer) + + let finished: Bool + + // Possible return codes: + // - Z_OK: some progress has been made + // - Z_STREAM_END: the end of the compressed data has been reached and all uncompressed + // output has been produced + // - Z_NEED_DICT: a preset dictionary is needed at this point + // - Z_DATA_ERROR: the input data was corrupted + // - Z_STREAM_ERROR: the stream structure was inconsistent + // - Z_MEM_ERROR there was not enough memory + // - Z_BUF_ERROR if no progress was possible or if there was not enough room in the output + // buffer when Z_FINISH is used. + // + // Note that Z_OK is not okay here since we always flush with Z_FINISH and therefore + // use Z_STREAM_END as our success criteria. + let rc = CGRPCZlib_inflate(&self, Z_FINISH) + switch rc { + case Z_STREAM_END: + finished = true + case Z_BUF_ERROR: + finished = false + default: + throw RPCError( + code: .internalError, + message: "Decompression error", + cause: ZlibError(code: Int(rc), message: self.lastError ?? "") + ) + } + + let size = outPointer.count - self.availableOutputBytes + return (finished, size) + } + + if finished { + output.removeLast(output.count - self.totalOutputBytes) + let bytesRead = inputPointer.count - self.availableInputBytes + return (bytesRead, output) + } else { + offset += written + let newSize = min(output.count * 2, limit) + if newSize == output.count { + throw RPCError(code: .resourceExhausted, message: "Message is too large to decompress.") + } else { + output.append(contentsOf: repeatElement(0, count: newSize - output.count)) + } + } + } + } + } + + mutating func deflate( + _ input: [UInt8], + into output: inout ByteBuffer, + upperBound: Int + ) throws -> Int { + defer { + self.setNextInputBuffer(nil) + self.setNextOutputBuffer(nil) + } + + var input = input + return try input.withUnsafeMutableBytes { input in + self.setNextInputBuffer(input) + + return try output.writeWithUnsafeMutableBytes(minimumWritableBytes: upperBound) { output in + self.setNextOutputBuffer(output) + + let rc = CGRPCZlib_deflate(&self, Z_FINISH) + + // Possible return codes: + // - Z_OK: some progress has been made + // - Z_STREAM_END: all input has been consumed and all output has been produced (only when + // flush is set to Z_FINISH) + // - Z_STREAM_ERROR: the stream state was inconsistent + // - Z_BUF_ERROR: no progress is possible + // + // The documentation notes that Z_BUF_ERROR is not fatal, and deflate() can be called again + // with more input and more output space to continue compressing. However, we + // call `deflateBound()` before `deflate()` which guarantees that the output size will not be + // larger than the value returned by `deflateBound()` if `Z_FINISH` flush is used. As such, + // the only acceptable outcome is `Z_STREAM_END`. + guard rc == Z_STREAM_END else { + throw RPCError( + code: .internalError, + message: "Compression error", + cause: ZlibError(code: Int(rc), message: self.lastError ?? "") + ) + } + + return output.count - self.availableOutputBytes + } + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift new file mode 100644 index 000000000..f0c8ce753 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift @@ -0,0 +1,150 @@ +/* + * 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 NIOCore +import XCTest + +@testable import GRPCHTTP2Core + +final class ZlibTests: XCTestCase { + private let text = """ + Here's to the crazy ones. The misfits. The rebels. The troublemakers. The round pegs in the + square holes. The ones who see things differently. They're not fond of rules. And they have + no respect for the status quo. You can quote them, disagree with them, glorify or vilify them. + About the only thing you can't do is ignore them. Because they change things. They push the + human race forward. And while some may see them as the crazy ones, we see genius. Because + the people who are crazy enough to think they can change the world, are the ones who do. + """ + + private func compress(_ input: [UInt8], method: Zlib.Method) throws -> ByteBuffer { + var compressor = Zlib.Compressor(method: method) + compressor.initialize() + defer { compressor.end() } + + var buffer = ByteBuffer() + try compressor.compress(input, into: &buffer) + return buffer + } + + private func decompress( + _ input: ByteBuffer, + method: Zlib.Method, + limit: Int = .max + ) throws -> [UInt8] { + var decompressor = Zlib.Decompressor(method: method) + decompressor.initialize() + defer { decompressor.end() } + + var input = input + return try decompressor.decompress(&input, limit: limit) + } + + func testRoundTripUsingDeflate() throws { + let original = Array(self.text.utf8) + let compressed = try self.compress(original, method: .deflate) + let decompressed = try self.decompress(compressed, method: .deflate) + XCTAssertEqual(original, decompressed) + } + + func testRoundTripUsingGzip() throws { + let original = Array(self.text.utf8) + let compressed = try self.compress(original, method: .gzip) + let decompressed = try self.decompress(compressed, method: .gzip) + XCTAssertEqual(original, decompressed) + } + + func testRepeatedCompresses() throws { + let original = Array(self.text.utf8) + var compressor = Zlib.Compressor(method: .deflate) + compressor.initialize() + defer { compressor.end() } + + var compressed = ByteBuffer() + let bytesWritten = try compressor.compress(original, into: &compressed) + XCTAssertEqual(compressed.readableBytes, bytesWritten) + + for _ in 0 ..< 10 { + var buffer = ByteBuffer() + try compressor.compress(original, into: &buffer) + XCTAssertEqual(compressed, buffer) + } + } + + func testRepeatedDecompresses() throws { + let original = Array(self.text.utf8) + var decompressor = Zlib.Decompressor(method: .deflate) + decompressor.initialize() + defer { decompressor.end() } + + let compressed = try self.compress(original, method: .deflate) + var input = compressed + let decompressed = try decompressor.decompress(&input, limit: .max) + + for _ in 0 ..< 10 { + var input = compressed + let buffer = try decompressor.decompress(&input, limit: .max) + XCTAssertEqual(buffer, decompressed) + } + } + + func testDecompressGrowsOutputBuffer() throws { + // This compresses down to 17 bytes with deflate. The decompressor sets the output buffer to + // be double the size of the input buffer and will grow it if necessary. This test exercises + // that path. + let original = [UInt8](repeating: 0, count: 1024) + let compressed = try self.compress(original, method: .deflate) + let decompressed = try self.decompress(compressed, method: .deflate) + XCTAssertEqual(decompressed, original) + } + + func testDecompressRespectsLimit() throws { + let compressed = try self.compress(Array(self.text.utf8), method: .deflate) + let limit = compressed.readableBytes - 1 + XCTAssertThrowsError( + ofType: RPCError.self, + try self.decompress(compressed, method: .deflate, limit: limit) + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + } + } + + func testCompressAppendsToBuffer() throws { + var compressor = Zlib.Compressor(method: .deflate) + compressor.initialize() + defer { compressor.end() } + + var buffer = ByteBuffer() + try compressor.compress(Array(repeating: 0, count: 1024), into: &buffer) + + // Should be some readable bytes. + let byteCount1 = buffer.readableBytes + XCTAssertGreaterThan(byteCount1, 0) + + try compressor.compress(Array(repeating: 1, count: 1024), into: &buffer) + + // Should be some readable bytes. + let byteCount2 = buffer.readableBytes + XCTAssertGreaterThan(byteCount2, byteCount1) + + let slice1 = buffer.readSlice(length: byteCount1)! + let decompressed1 = try self.decompress(slice1, method: .deflate) + XCTAssertEqual(decompressed1, Array(repeating: 0, count: 1024)) + + let decompressed2 = try self.decompress(buffer, method: .deflate) + XCTAssertEqual(decompressed2, Array(repeating: 1, count: 1024)) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift new file mode 100644 index 000000000..4cef512d0 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift @@ -0,0 +1,30 @@ +/* + * 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 + +func XCTAssertThrowsError( + ofType: E.Type, + _ expression: @autoclosure () throws -> T, + _ errorHandler: (E) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? E else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + errorHandler(error) + } +} From f7f3d2df3b9d3645af6a34e3ac6ccf0bdf5a0d6d Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 24 Jan 2024 08:03:04 +0000 Subject: [PATCH 214/580] [CodeGenLib] Render preformatted comments (#1770) Motivation: Some IDL representations (SwiftProtobuf) store the methods and services documentation as preformatted strings containing the "///" and new lines. So we need a case in the SwiftStructuredRepresentation and in the TextRenderer. The responsability of providing already formatted comments for the services and methods should be of the user and not of the CodeGenLibrary. Modifications: - created the new case in StructuredSwiiftrepresentation - handled the rendering of this case - modified tests Result: Documentation for methoda and services should pe preformatted in any CodeGenerationRequest object so we avoid generating commenyts with double `///`. --- .../GRPCCodeGen/CodeGenerationRequest.swift | 5 ++- .../Internal/Renderer/TextBasedRenderer.swift | 15 ++++++-- .../StructuredSwiftRepresentation.swift | 8 ++++ .../Translator/ClientCodeTranslator.swift | 11 ++++-- .../IDLToStructuredSwiftTranslator.swift | 2 +- .../Translator/ServerCodeTranslator.swift | 8 ++-- ...lientCodeTranslatorSnippetBasedTests.swift | 38 +++++++++++-------- ...erverCodeTranslatorSnippetBasedTests.swift | 30 +++++++-------- .../Internal/Translator/TestFunctions.swift | 4 +- 9 files changed, 75 insertions(+), 46 deletions(-) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index fab71b977..9711b39d2 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -21,7 +21,8 @@ public struct CodeGenerationRequest { public var fileName: String /// Any comments at the top of the file such as documentation and copyright headers. - /// They will be placed at the top of the generated file. + /// They will be placed at the top of the generated file. They are already formatted, + /// meaning they contain "///" and new lines. public var leadingTrivia: String /// The Swift imports that the generated file depends on. The gRPC specific imports aren't required @@ -218,6 +219,7 @@ public struct CodeGenerationRequest { /// Represents a service described in an IDL file. public struct ServiceDescriptor: Hashable { /// Documentation from comments above the IDL service description. + /// It is already formatted, meaning it contains "///" and new lines. public var documentation: String /// The service name in different formats. @@ -252,6 +254,7 @@ public struct CodeGenerationRequest { /// Represents a method described in an IDL file. public struct MethodDescriptor: Hashable { /// Documentation from comments above the IDL method description. + /// It is already formatted, meaning it contains "///" and new lines. public var documentation: String /// Method name in different formats. diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index f9f8eeba5..3fd683720 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -161,12 +161,19 @@ struct TextBasedRenderer: RendererProtocol { case .mark(let string, sectionBreak: false): prefix = "// MARK:" commentString = string + case .preFormatted(let string): + prefix = "" + commentString = string } - let lines = commentString.transformingLines { line in - if line.isEmpty { return prefix } - return "\(prefix) \(line)" + if prefix.isEmpty { + writer.writeLine(commentString) + } else { + let lines = commentString.transformingLines { line in + if line.isEmpty { return prefix } + return "\(prefix) \(line)" + } + lines.forEach(writer.writeLine) } - lines.forEach(writer.writeLine) } /// Renders the specified import statements. diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index e5b513a51..7a91fcd94 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -130,6 +130,14 @@ enum Comment: Equatable, Codable { /// For example: `// MARK: - Public methods`, with the optional /// section break (`-`). case mark(String, sectionBreak: Bool) + + /// A comment that is already formatted, + /// meaning that it already has the `///` and + /// can contain multiple lines + /// + /// For example both the string and the comment + /// can look like `/// Important type`. + case preFormatted(String) } /// A description of a literal. diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 1b83f991c..7722663df 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -77,7 +77,7 @@ struct ClientCodeTranslator: SpecializedTranslator { codeBlocks.append( .declaration( .commentable( - .doc(service.documentation), + .preFormatted(service.documentation), self.makeClientProtocol(for: service, in: codeGenerationRequest) ) ) @@ -88,7 +88,7 @@ struct ClientCodeTranslator: SpecializedTranslator { codeBlocks.append( .declaration( .commentable( - .doc(service.documentation), + .preFormatted(service.documentation), self.makeClientStruct(for: service, in: codeGenerationRequest) ) ) @@ -179,7 +179,10 @@ extension ClientCodeTranslator { ) return .function(signature: functionSignature, body: body) } else { - return .commentable(.doc(method.documentation), .function(signature: functionSignature)) + return .commentable( + .preFormatted(method.documentation), + .function(signature: functionSignature) + ) } } @@ -324,7 +327,7 @@ extension ClientCodeTranslator { let initializer = self.makeClientVariable() let methods = service.methods.map { Declaration.commentable( - .doc($0.documentation), + .preFormatted($0.documentation), self.makeClientMethod(for: $0, in: service, from: codeGenerationRequest) ) } diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 2f8a81707..b35a85d1b 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -31,7 +31,7 @@ struct IDLToStructuredSwiftTranslator: Translator { accessLevel: accessLevel ) - let topComment = Comment.doc(codeGenerationRequest.leadingTrivia) + let topComment = Comment.preFormatted(codeGenerationRequest.leadingTrivia) let imports = try codeGenerationRequest.dependencies.reduce( into: [ImportDescription(moduleName: "GRPCCore")] ) { partialResult, newDependency in diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index dbf4914d6..feb858a3c 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -107,7 +107,7 @@ extension ServerCodeTranslator { ) -> Declaration { let methods = service.methods.compactMap { Declaration.commentable( - .doc($0.documentation), + .preFormatted($0.documentation), .function( FunctionDescription( signature: self.makeStreamingMethodSignature(for: $0, in: service) @@ -125,7 +125,7 @@ extension ServerCodeTranslator { ) ) - return .commentable(.doc(service.documentation), streamingProtocol) + return .commentable(.preFormatted(service.documentation), streamingProtocol) } private func makeStreamingMethodSignature( @@ -290,7 +290,7 @@ extension ServerCodeTranslator { let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) return .commentable( - .doc(service.documentation), + .preFormatted(service.documentation), .protocol( ProtocolDescription( accessModifier: self.accessModifier, @@ -344,7 +344,7 @@ extension ServerCodeTranslator { ) return .commentable( - .doc(method.documentation), + .preFormatted(method.documentation), .function(FunctionDescription(signature: functionSignature)) ) } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index b97d3ca2f..20045e7f2 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -27,7 +27,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorUnaryMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for MethodA", + documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, @@ -35,7 +35,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] @@ -98,7 +98,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorClientStreamingMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for MethodA", + documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: true, isOutputStreaming: false, @@ -106,7 +106,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] @@ -169,7 +169,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorServerStreamingMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for MethodA", + documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: true, @@ -177,7 +177,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] @@ -240,7 +240,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorBidirectionalStreamingMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for MethodA", + documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: true, isOutputStreaming: true, @@ -248,7 +248,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] @@ -311,7 +311,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorMultipleMethods() throws { let methodA = MethodDescriptor( - documentation: "Documentation for MethodA", + documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: true, isOutputStreaming: false, @@ -319,7 +319,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let methodB = MethodDescriptor( - documentation: "Documentation for MethodB", + documentation: "/// Documentation for MethodB", name: Name(base: "MethodB", generatedUpperCase: "MethodB", generatedLowerCase: "methodB"), isInputStreaming: false, isOutputStreaming: true, @@ -327,7 +327,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [methodA, methodB] @@ -423,7 +423,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorNoNamespaceService() throws { let method = MethodDescriptor( - documentation: "Documentation for MethodA", + documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, @@ -431,7 +431,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [method] @@ -494,7 +494,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { func testClientCodeTranslatorMultipleServices() throws { let serviceA = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name( base: "nammespaceA", @@ -504,7 +504,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { methods: [] ) let serviceB = ServiceDescriptor( - documentation: "Documentation for ServiceB", + documentation: """ + /// Documentation for ServiceB + /// + /// Line 2 + """, name: Name(base: "ServiceB", generatedUpperCase: "ServiceB", generatedLowerCase: ""), namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] @@ -523,10 +527,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceB + /// + /// Line 2 public protocol ServiceBClientProtocol: Sendable {} extension ServiceB.ClientProtocol { } /// Documentation for ServiceB + /// + /// Line 2 public struct ServiceBClient: ServiceB.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index d0a4a5975..3f13ec0c6 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -27,7 +27,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorUnaryMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for unaryMethod", + documentation: "/// Documentation for unaryMethod", name: Name(base: "UnaryMethod", generatedUpperCase: "Unary", generatedLowerCase: "unary"), isInputStreaming: false, isOutputStreaming: false, @@ -35,7 +35,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name( base: "AlongNameForServiceA", generatedUpperCase: "ServiceA", @@ -91,7 +91,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorInputStreamingMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for inputStreamingMethod", + documentation: "/// Documentation for inputStreamingMethod", name: Name( base: "InputStreamingMethod", generatedUpperCase: "InputStreaming", @@ -103,7 +103,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), namespace: Name( base: "namespaceA", @@ -155,7 +155,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorOutputStreamingMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for outputStreamingMethod", + documentation: "/// Documentation for outputStreamingMethod", name: Name( base: "OutputStreamingMethod", generatedUpperCase: "OutputStreaming", @@ -167,7 +167,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name( base: "ServiceATest", generatedUpperCase: "ServiceA", @@ -223,7 +223,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorBidirectionalStreamingMethod() throws { let method = MethodDescriptor( - documentation: "Documentation for bidirectionalStreamingMethod", + documentation: "/// Documentation for bidirectionalStreamingMethod", name: Name( base: "BidirectionalStreamingMethod", generatedUpperCase: "BidirectionalStreaming", @@ -235,7 +235,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name( base: "ServiceATest", generatedUpperCase: "ServiceA", @@ -287,7 +287,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorMultipleMethods() throws { let inputStreamingMethod = MethodDescriptor( - documentation: "Documentation for inputStreamingMethod", + documentation: "/// Documentation for inputStreamingMethod", name: Name( base: "InputStreamingMethod", generatedUpperCase: "InputStreaming", @@ -299,7 +299,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let outputStreamingMethod = MethodDescriptor( - documentation: "Documentation for outputStreamingMethod", + documentation: "/// Documentation for outputStreamingMethod", name: Name( base: "outputStreamingMethod", generatedUpperCase: "OutputStreaming", @@ -311,7 +311,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name( base: "ServiceATest", generatedUpperCase: "ServiceA", @@ -383,7 +383,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorNoNamespaceService() throws { let method = MethodDescriptor( - documentation: "Documentation for MethodA", + documentation: "/// Documentation for MethodA", name: Name(base: "methodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), isInputStreaming: false, isOutputStreaming: false, @@ -391,7 +391,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { outputType: "NamespaceA_ServiceAResponse" ) let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name( base: "ServiceATest", generatedUpperCase: "ServiceA", @@ -443,7 +443,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func testServerCodeTranslatorMoreServicesOrder() throws { let serviceA = ServiceDescriptor( - documentation: "Documentation for ServiceA", + documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), namespace: Name( base: "namespaceA", @@ -453,7 +453,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { methods: [] ) let serviceB = ServiceDescriptor( - documentation: "Documentation for ServiceB", + documentation: "/// Documentation for ServiceB", name: Name(base: "ServiceB", generatedUpperCase: "ServiceB", generatedLowerCase: "serviceB"), namespace: Name( base: "namespaceA", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 1250b92d9..800ff7393 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -76,7 +76,7 @@ internal func makeCodeGenerationRequest( ) -> CodeGenerationRequest { return CodeGenerationRequest( fileName: "test.grpc", - leadingTrivia: "Some really exciting license header 2023.", + leadingTrivia: "/// Some really exciting license header 2023.", dependencies: [], services: services, lookupSerializer: { @@ -93,7 +93,7 @@ internal func makeCodeGenerationRequest( ) -> CodeGenerationRequest { return CodeGenerationRequest( fileName: "test.grpc", - leadingTrivia: "Some really exciting license header 2023.", + leadingTrivia: "/// Some really exciting license header 2023.", dependencies: dependencies, services: [], lookupSerializer: { From 3d747725e08ff56ea8ff96db8a3ff6ed5252d580 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 24 Jan 2024 12:40:22 +0000 Subject: [PATCH 215/580] Make Zlib.Compressor/Decompressor classes (#1769) Motivation: z_stream stores a pointer to itself in its internal state which it checks against in inflate/deflate. As we hold these within structs, and call through to C functions which take a pointer to a z_stream, this address can change as the struct is copied about. This results in errors when calling deflate/inflate. Modifications: - Hold a pointer to the z_stream Result: Harder to misue compressor/decompressor --- Sources/GRPCHTTP2Core/Compression/Zlib.swift | 161 ++++++++---------- .../Server/Compression/ZlibTests.swift | 15 +- 2 files changed, 78 insertions(+), 98 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Compression/Zlib.swift b/Sources/GRPCHTTP2Core/Compression/Zlib.swift index f8abeff98..b94bd8a1d 100644 --- a/Sources/GRPCHTTP2Core/Compression/Zlib.swift +++ b/Sources/GRPCHTTP2Core/Compression/Zlib.swift @@ -37,29 +37,18 @@ enum Zlib { extension Zlib { /// Creates a new compressor for the given compression format. /// - /// This compressor is only suitable for compressing whole messages at a time. Callers - /// must ``initialize()`` the compressor before using it. + /// This compressor is only suitable for compressing whole messages at a time. struct Compressor { - private var stream: z_stream + // TODO: Make this ~Copyable when 5.9 is the lowest supported Swift version. + + private var stream: UnsafeMutablePointer private let method: Method - private var isInitialized = false init(method: Method) { self.method = method - self.stream = z_stream() - } - - /// Initialize the compressor. - mutating func initialize() { - precondition(!self.isInitialized) + self.stream = .allocate(capacity: 1) + self.stream.initialize(to: z_stream()) self.stream.deflateInit(windowBits: self.method.windowBits) - self.isInitialized = true - } - - static func initialized(_ method: Method) -> Self { - var compressor = Compressor(method: method) - compressor.initialize() - return compressor } /// Compresses the data in `input` into the `output` buffer. @@ -68,27 +57,27 @@ extension Zlib { /// - Parameter output: The `ByteBuffer` into which the compressed message should be written. /// - Returns: The number of bytes written into the `output` buffer. @discardableResult - mutating func compress(_ input: [UInt8], into output: inout ByteBuffer) throws -> Int { - precondition(self.isInitialized) + func compress(_ input: [UInt8], into output: inout ByteBuffer) throws -> Int { defer { self.reset() } let upperBound = self.stream.deflateBound(inputBytes: input.count) return try self.stream.deflate(input, into: &output, upperBound: upperBound) } /// Resets compression state. - private mutating func reset() { + private func reset() { do { try self.stream.deflateReset() } catch { self.end() - self.stream = z_stream() + self.stream.initialize(to: z_stream()) self.stream.deflateInit(windowBits: self.method.windowBits) } } /// Deallocates any resources allocated by Zlib. - mutating func end() { + func end() { self.stream.deflateEnd() + self.stream.deallocate() } } } @@ -96,22 +85,18 @@ extension Zlib { extension Zlib { /// Creates a new decompressor for the given compression format. /// - /// This decompressor is only suitable for compressing whole messages at a time. Callers - /// must ``initialize()`` the decompressor before using it. + /// This decompressor is only suitable for compressing whole messages at a time. struct Decompressor { - private var stream: z_stream + // TODO: Make this ~Copyable when 5.9 is the lowest supported Swift version. + + private var stream: UnsafeMutablePointer private let method: Method - private var isInitialized = false init(method: Method) { self.method = method - self.stream = z_stream() - } - - mutating func initialize() { - precondition(!self.isInitialized) + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + self.stream.initialize(to: z_stream()) self.stream.inflateInit(windowBits: self.method.windowBits) - self.isInitialized = true } /// Returns the decompressed bytes from ``input``. @@ -119,26 +104,26 @@ extension Zlib { /// - Parameters: /// - input: The buffer read compressed bytes from. /// - limit: The largest size a decompressed payload may be. - mutating func decompress(_ input: inout ByteBuffer, limit: Int) throws -> [UInt8] { - precondition(self.isInitialized) + func decompress(_ input: inout ByteBuffer, limit: Int) throws -> [UInt8] { defer { self.reset() } return try self.stream.inflate(input: &input, limit: limit) } /// Resets decompression state. - private mutating func reset() { + private func reset() { do { try self.stream.inflateReset() } catch { self.end() - self.stream = z_stream() + self.stream.initialize(to: z_stream()) self.stream.inflateInit(windowBits: self.method.windowBits) } } /// Deallocates any resources allocated by Zlib. - mutating func end() { + func end() { self.stream.inflateEnd() + self.stream.deallocate() } } } @@ -155,13 +140,13 @@ struct ZlibError: Error, Hashable { } } -extension z_stream { - mutating func inflateInit(windowBits: Int32) { - self.zfree = nil - self.zalloc = nil - self.opaque = nil +extension UnsafeMutablePointer { + func inflateInit(windowBits: Int32) { + self.pointee.zfree = nil + self.pointee.zalloc = nil + self.pointee.opaque = nil - let rc = CGRPCZlib_inflateInit2(&self, windowBits) + let rc = CGRPCZlib_inflateInit2(self, windowBits) // Possible return codes: // - Z_OK // - Z_MEM_ERROR: not enough memory @@ -171,8 +156,8 @@ extension z_stream { precondition(rc == Z_OK, "inflateInit2 failed with error (\(rc)) \(self.lastError ?? "")") } - mutating func inflateReset() throws { - let rc = CGRPCZlib_inflateReset(&self) + func inflateReset() throws { + let rc = CGRPCZlib_inflateReset(self) // Possible return codes: // - Z_OK @@ -187,17 +172,17 @@ extension z_stream { } } - mutating func inflateEnd() { - _ = CGRPCZlib_inflateEnd(&self) + func inflateEnd() { + _ = CGRPCZlib_inflateEnd(self) } - mutating func deflateInit(windowBits: Int32) { - self.zfree = nil - self.zalloc = nil - self.opaque = nil + func deflateInit(windowBits: Int32) { + self.pointee.zfree = nil + self.pointee.zalloc = nil + self.pointee.opaque = nil let rc = CGRPCZlib_deflateInit2( - &self, + self, Z_DEFAULT_COMPRESSION, // compression level Z_DEFLATED, // compression method (this must be Z_DEFLATED) windowBits, // window size, i.e. deflate/gzip @@ -215,8 +200,8 @@ extension z_stream { precondition(rc == Z_OK, "deflateInit2 failed with error (\(rc)) \(self.lastError ?? "")") } - mutating func deflateReset() throws { - let rc = CGRPCZlib_deflateReset(&self) + func deflateReset() throws { + let rc = CGRPCZlib_deflateReset(self) // Possible return codes: // - Z_OK @@ -231,87 +216,87 @@ extension z_stream { } } - mutating func deflateEnd() { - _ = CGRPCZlib_deflateEnd(&self) + func deflateEnd() { + _ = CGRPCZlib_deflateEnd(self) } - mutating func deflateBound(inputBytes: Int) -> Int { - let bound = CGRPCZlib_deflateBound(&self, UInt(inputBytes)) + func deflateBound(inputBytes: Int) -> Int { + let bound = CGRPCZlib_deflateBound(self, UInt(inputBytes)) return Int(bound) } - mutating func setNextInputBuffer(_ buffer: UnsafeMutableBufferPointer) { + func setNextInputBuffer(_ buffer: UnsafeMutableBufferPointer) { if let baseAddress = buffer.baseAddress { - self.next_in = baseAddress - self.avail_in = UInt32(buffer.count) + self.pointee.next_in = baseAddress + self.pointee.avail_in = UInt32(buffer.count) } else { - self.next_in = nil - self.avail_in = 0 + self.pointee.next_in = nil + self.pointee.avail_in = 0 } } - mutating func setNextInputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { + func setNextInputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { if let buffer = buffer, let baseAddress = buffer.baseAddress { - self.next_in = CGRPCZlib_castVoidToBytefPointer(baseAddress) - self.avail_in = UInt32(buffer.count) + self.pointee.next_in = CGRPCZlib_castVoidToBytefPointer(baseAddress) + self.pointee.avail_in = UInt32(buffer.count) } else { - self.next_in = nil - self.avail_in = 0 + self.pointee.next_in = nil + self.pointee.avail_in = 0 } } - mutating func setNextOutputBuffer(_ buffer: UnsafeMutableBufferPointer) { + func setNextOutputBuffer(_ buffer: UnsafeMutableBufferPointer) { if let baseAddress = buffer.baseAddress { - self.next_out = baseAddress - self.avail_out = UInt32(buffer.count) + self.pointee.next_out = baseAddress + self.pointee.avail_out = UInt32(buffer.count) } else { - self.next_out = nil - self.avail_out = 0 + self.pointee.next_out = nil + self.pointee.avail_out = 0 } } - mutating func setNextOutputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { + func setNextOutputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { if let buffer = buffer, let baseAddress = buffer.baseAddress { - self.next_out = CGRPCZlib_castVoidToBytefPointer(baseAddress) - self.avail_out = UInt32(buffer.count) + self.pointee.next_out = CGRPCZlib_castVoidToBytefPointer(baseAddress) + self.pointee.avail_out = UInt32(buffer.count) } else { - self.next_out = nil - self.avail_out = 0 + self.pointee.next_out = nil + self.pointee.avail_out = 0 } } /// Number of bytes available to read `self.nextInputBuffer`. See also: `z_stream.avail_in`. var availableInputBytes: Int { get { - Int(self.avail_in) + Int(self.pointee.avail_in) } set { - self.avail_in = UInt32(newValue) + self.pointee.avail_in = UInt32(newValue) } } /// The remaining writable space in `nextOutputBuffer`. See also: `z_stream.avail_out`. var availableOutputBytes: Int { get { - Int(self.avail_out) + Int(self.pointee.avail_out) } set { - self.avail_out = UInt32(newValue) + self.pointee.avail_out = UInt32(newValue) } } /// The total number of bytes written to the output buffer. See also: `z_stream.total_out`. var totalOutputBytes: Int { - Int(self.total_out) + Int(self.pointee.total_out) } /// The last error message that zlib wrote. No message is guaranteed on error, however, `nil` is /// guaranteed if there is no error. See also `z_stream.msg`. var lastError: String? { - self.msg.map { String(cString: $0) } + self.pointee.msg.map { String(cString: $0) } } - mutating func inflate(input: inout ByteBuffer, limit: Int) throws -> [UInt8] { + func inflate(input: inout ByteBuffer, limit: Int) throws -> [UInt8] { return try input.readWithUnsafeMutableReadableBytes { inputPointer in self.setNextInputBuffer(inputPointer) defer { @@ -342,7 +327,7 @@ extension z_stream { // // Note that Z_OK is not okay here since we always flush with Z_FINISH and therefore // use Z_STREAM_END as our success criteria. - let rc = CGRPCZlib_inflate(&self, Z_FINISH) + let rc = CGRPCZlib_inflate(self, Z_FINISH) switch rc { case Z_STREAM_END: finished = true @@ -377,7 +362,7 @@ extension z_stream { } } - mutating func deflate( + func deflate( _ input: [UInt8], into output: inout ByteBuffer, upperBound: Int @@ -394,7 +379,7 @@ extension z_stream { return try output.writeWithUnsafeMutableBytes(minimumWritableBytes: upperBound) { output in self.setNextOutputBuffer(output) - let rc = CGRPCZlib_deflate(&self, Z_FINISH) + let rc = CGRPCZlib_deflate(self, Z_FINISH) // Possible return codes: // - Z_OK: some progress has been made diff --git a/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift index f0c8ce753..bcee1f3e2 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift @@ -31,8 +31,7 @@ final class ZlibTests: XCTestCase { """ private func compress(_ input: [UInt8], method: Zlib.Method) throws -> ByteBuffer { - var compressor = Zlib.Compressor(method: method) - compressor.initialize() + let compressor = Zlib.Compressor(method: method) defer { compressor.end() } var buffer = ByteBuffer() @@ -45,8 +44,7 @@ final class ZlibTests: XCTestCase { method: Zlib.Method, limit: Int = .max ) throws -> [UInt8] { - var decompressor = Zlib.Decompressor(method: method) - decompressor.initialize() + let decompressor = Zlib.Decompressor(method: method) defer { decompressor.end() } var input = input @@ -69,8 +67,7 @@ final class ZlibTests: XCTestCase { func testRepeatedCompresses() throws { let original = Array(self.text.utf8) - var compressor = Zlib.Compressor(method: .deflate) - compressor.initialize() + let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } var compressed = ByteBuffer() @@ -86,8 +83,7 @@ final class ZlibTests: XCTestCase { func testRepeatedDecompresses() throws { let original = Array(self.text.utf8) - var decompressor = Zlib.Decompressor(method: .deflate) - decompressor.initialize() + let decompressor = Zlib.Decompressor(method: .deflate) defer { decompressor.end() } let compressed = try self.compress(original, method: .deflate) @@ -123,8 +119,7 @@ final class ZlibTests: XCTestCase { } func testCompressAppendsToBuffer() throws { - var compressor = Zlib.Compressor(method: .deflate) - compressor.initialize() + let compressor = Zlib.Compressor(method: .deflate) defer { compressor.end() } var buffer = ByteBuffer() From 19d24ab3c77eedeebc55fc84e55193ec34117c96 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 24 Jan 2024 15:07:00 +0000 Subject: [PATCH 216/580] Add `GRPCMessageFramer` (#1768) --- Package.swift | 1 + Sources/GRPCHTTP2Core/GRPCMessageFramer.swift | 98 +++++++++++ Sources/GRPCHTTP2Core/OneOrManyQueue.swift | 165 ++++++++++++++++++ .../GRPCMessageFramerTests.swift | 69 ++++++++ .../OneOrManyQueueTests.swift | 140 +++++++++++++++ 5 files changed, 473 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/GRPCMessageFramer.swift create mode 100644 Sources/GRPCHTTP2Core/OneOrManyQueue.swift create mode 100644 Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift diff --git a/Package.swift b/Package.swift index 93163449f..f61589f4d 100644 --- a/Package.swift +++ b/Package.swift @@ -203,6 +203,7 @@ extension Target { .nioCore, .nioHTTP2, .cgrpcZlib, + .dequeModule ] ) diff --git a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift new file mode 100644 index 000000000..0e7717d3d --- /dev/null +++ b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift @@ -0,0 +1,98 @@ +/* + * 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 NIOCore + +/// A ``GRPCMessageFramer`` helps with the framing of gRPC data frames: +/// - It prepends data with the required metadata (compression flag and message length). +/// - It compresses messages using the specified compression algorithm (if configured). +/// - It coalesces multiple messages (appended into the `Framer` by calling ``append(_:compress:)``) +/// into a single `ByteBuffer`. +struct GRPCMessageFramer { + /// Length of the gRPC message header (1 compression byte, 4 bytes for the length). + static let metadataLength = 5 + + /// Maximum size the `writeBuffer` can be when concatenating multiple frames. + /// This limit will not be considered if only a single message/frame is written into the buffer, meaning + /// frames with messages over 64KB can still be written. + /// - Note: This is expressed as the power of 2 closer to 64KB (i.e., 64KiB) because `ByteBuffer` + /// reserves capacity in powers of 2. This way, we can take advantage of the whole buffer. + static let maximumWriteBufferLength = 65_536 + + private var pendingMessages: OneOrManyQueue + + private struct PendingMessage { + let bytes: [UInt8] + let compress: Bool + } + + private var writeBuffer: ByteBuffer + + init() { + self.pendingMessages = OneOrManyQueue() + self.writeBuffer = ByteBuffer() + } + + /// Queue the given bytes to be framed and potentially coalesced alongside other messages in a `ByteBuffer`. + /// The resulting data will be returned when calling ``GRPCMessageFramer/next()``. + /// If `compress` is true, then the given bytes will be compressed using the configured compression algorithm. + mutating func append(_ bytes: [UInt8], compress: Bool) { + self.pendingMessages.append(PendingMessage(bytes: bytes, compress: compress)) + } + + /// If there are pending messages to be framed, a `ByteBuffer` will be returned with the framed data. + /// Data may also be compressed (if configured) and multiple frames may be coalesced into the same `ByteBuffer`. + /// - Throws: If an error is encountered, such as a compression failure, an error will be thrown. + mutating func next() throws -> ByteBuffer? { + if self.pendingMessages.isEmpty { + // Nothing pending: exit early. + return nil + } + + defer { + // To avoid holding an excessively large buffer, if its size is larger than + // our threshold (`maximumWriteBufferLength`), then reset it to a new `ByteBuffer`. + if self.writeBuffer.capacity > Self.maximumWriteBufferLength { + self.writeBuffer = ByteBuffer() + } + } + + var requiredCapacity = 0 + for message in self.pendingMessages { + requiredCapacity += message.bytes.count + Self.metadataLength + } + self.writeBuffer.clear(minimumCapacity: requiredCapacity) + + while let message = self.pendingMessages.pop() { + try self.encode(message) + } + + return self.writeBuffer + } + + private mutating func encode(_ message: PendingMessage) throws { + if message.compress { + self.writeBuffer.writeInteger(UInt8(1)) // Set compression flag + // TODO: compress message and write the compressed message length + bytes + } else { + self.writeBuffer.writeMultipleIntegers( + UInt8(0), // Clear compression flag + UInt32(message.bytes.count) // Set message length + ) + self.writeBuffer.writeBytes(message.bytes) + } + } +} diff --git a/Sources/GRPCHTTP2Core/OneOrManyQueue.swift b/Sources/GRPCHTTP2Core/OneOrManyQueue.swift new file mode 100644 index 000000000..8995f5641 --- /dev/null +++ b/Sources/GRPCHTTP2Core/OneOrManyQueue.swift @@ -0,0 +1,165 @@ +/* + * 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 DequeModule + +/// A FIFO-queue which allows for a single element to be stored on the stack and defers to a +/// heap-implementation if further elements are added. +/// +/// This is useful when optimising for unary streams where avoiding the cost of a heap +/// allocation is desirable. +internal struct OneOrManyQueue: Collection { + private var backing: Backing + + private enum Backing: Collection { + case none + case one(Element) + case many(Deque) + + var startIndex: Int { + switch self { + case .none, .one: + return 0 + case let .many(elements): + return elements.startIndex + } + } + + var endIndex: Int { + switch self { + case .none: + return 0 + case .one: + return 1 + case let .many(elements): + return elements.endIndex + } + } + + subscript(index: Int) -> Element { + switch self { + case .none: + fatalError("Invalid index") + case let .one(element): + assert(index == 0) + return element + case let .many(elements): + return elements[index] + } + } + + func index(after index: Int) -> Int { + switch self { + case .none: + return 0 + case .one: + return 1 + case let .many(elements): + return elements.index(after: index) + } + } + + var count: Int { + switch self { + case .none: + return 0 + case .one: + return 1 + case let .many(elements): + return elements.count + } + } + + var isEmpty: Bool { + switch self { + case .none: + return true + case .one: + return false + case let .many(elements): + return elements.isEmpty + } + } + + mutating func append(_ element: Element) { + switch self { + case .none: + self = .one(element) + case let .one(one): + var elements = Deque() + elements.reserveCapacity(16) + elements.append(one) + elements.append(element) + self = .many(elements) + case var .many(elements): + self = .none + elements.append(element) + self = .many(elements) + } + } + + mutating func pop() -> Element? { + switch self { + case .none: + return nil + case let .one(element): + self = .none + return element + case var .many(many): + self = .none + let element = many.popFirst() + self = .many(many) + return element + } + } + } + + init() { + self.backing = .none + } + + var isEmpty: Bool { + return self.backing.isEmpty + } + + var count: Int { + return self.backing.count + } + + var startIndex: Int { + return self.backing.startIndex + } + + var endIndex: Int { + return self.backing.endIndex + } + + subscript(index: Int) -> Element { + return self.backing[index] + } + + func index(after index: Int) -> Int { + return self.backing.index(after: index) + } + + mutating func append(_ element: Element) { + self.backing.append(element) + } + + mutating func pop() -> Element? { + return self.backing.pop() + } +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift new file mode 100644 index 000000000..95029536f --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift @@ -0,0 +1,69 @@ +/* + * 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 NIOCore +import XCTest + +@testable import GRPCHTTP2Core + +final class GRPCMessageFramerTests: XCTestCase { + func testSingleWrite() throws { + var framer = GRPCMessageFramer() + framer.append(Array(repeating: 42, count: 128), compress: false) + + var buffer = try XCTUnwrap(framer.next()) + let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compressed) + XCTAssertEqual(length, 128) + XCTAssertEqual(buffer.readSlice(length: Int(length)), ByteBuffer(repeating: 42, count: 128)) + XCTAssertEqual(buffer.readableBytes, 0) + + // No more bufers. + XCTAssertNil(try framer.next()) + } + + func testMultipleWrites() throws { + var framer = GRPCMessageFramer() + + let messages = 100 + for _ in 0 ..< messages { + framer.append(Array(repeating: 42, count: 128), compress: false) + } + + var buffer = try XCTUnwrap(framer.next()) + for _ in 0 ..< messages { + let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertFalse(compressed) + XCTAssertEqual(length, 128) + XCTAssertEqual(buffer.readSlice(length: Int(length)), ByteBuffer(repeating: 42, count: 128)) + } + + XCTAssertEqual(buffer.readableBytes, 0) + + // No more bufers. + XCTAssertNil(try framer.next()) + } +} + +extension ByteBuffer { + mutating func readMessageHeader() -> (Bool, UInt32)? { + if let (compressed, length) = self.readMultipleIntegers(as: (UInt8, UInt32).self) { + return (compressed != 0, length) + } else { + return nil + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift b/Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift new file mode 100644 index 000000000..7f1fbf9f5 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift @@ -0,0 +1,140 @@ +/* + * 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 XCTest + +@testable import GRPCHTTP2Core + +internal final class OneOrManyQueueTests: XCTestCase { + func testIsEmpty() { + XCTAssertTrue(OneOrManyQueue().isEmpty) + } + + func testIsEmptyManyBacked() { + XCTAssertTrue(OneOrManyQueue.manyBacked.isEmpty) + } + + func testCount() { + var queue = OneOrManyQueue() + XCTAssertEqual(queue.count, 0) + queue.append(1) + XCTAssertEqual(queue.count, 1) + } + + func testCountManyBacked() { + var manyBacked = OneOrManyQueue.manyBacked + XCTAssertEqual(manyBacked.count, 0) + for i in 1 ... 100 { + manyBacked.append(1) + XCTAssertEqual(manyBacked.count, i) + } + } + + func testAppendAndPop() { + var queue = OneOrManyQueue() + XCTAssertNil(queue.pop()) + + queue.append(1) + XCTAssertEqual(queue.count, 1) + XCTAssertEqual(queue.pop(), 1) + + XCTAssertNil(queue.pop()) + XCTAssertEqual(queue.count, 0) + XCTAssertTrue(queue.isEmpty) + } + + func testAppendAndPopManyBacked() { + var manyBacked = OneOrManyQueue.manyBacked + XCTAssertNil(manyBacked.pop()) + + manyBacked.append(1) + XCTAssertEqual(manyBacked.count, 1) + manyBacked.append(2) + XCTAssertEqual(manyBacked.count, 2) + + XCTAssertEqual(manyBacked.pop(), 1) + XCTAssertEqual(manyBacked.count, 1) + + XCTAssertEqual(manyBacked.pop(), 2) + XCTAssertEqual(manyBacked.count, 0) + + XCTAssertNil(manyBacked.pop()) + XCTAssertTrue(manyBacked.isEmpty) + } + + func testIndexes() { + var queue = OneOrManyQueue() + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, 0) + + // Non-empty. + queue.append(1) + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, 1) + } + + func testIndexesManyBacked() { + var queue = OneOrManyQueue.manyBacked + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, 0) + + for i in 1 ... 100 { + queue.append(i) + XCTAssertEqual(queue.startIndex, 0) + XCTAssertEqual(queue.endIndex, i) + } + } + + func testIndexAfter() { + var queue = OneOrManyQueue() + XCTAssertEqual(queue.startIndex, queue.endIndex) + XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) + + queue.append(1) + XCTAssertNotEqual(queue.startIndex, queue.endIndex) + XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) + } + + func testSubscript() throws { + var queue = OneOrManyQueue() + queue.append(42) + let index = try XCTUnwrap(queue.firstIndex(of: 42)) + XCTAssertEqual(queue[index], 42) + } + + func testSubscriptManyBacked() throws { + var queue = OneOrManyQueue.manyBacked + for i in 0 ... 100 { + queue.append(i) + } + + for i in 0 ... 100 { + XCTAssertEqual(queue[i], i) + } + } +} + +extension OneOrManyQueue where Element == Int { + static var manyBacked: Self { + var queue = OneOrManyQueue() + // Append and pop to move to the 'many' backing. + queue.append(1) + queue.append(2) + XCTAssertEqual(queue.pop(), 1) + XCTAssertEqual(queue.pop(), 2) + return queue + } +} From a260bb3507483a17bbd0fd836f93bd6ca65122c7 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 24 Jan 2024 15:27:16 +0000 Subject: [PATCH 217/580] Add compression logic to GRPCMessageFramer (#1771) --- Sources/GRPCHTTP2Core/Compression/Zlib.swift | 6 +++ Sources/GRPCHTTP2Core/GRPCMessageFramer.swift | 38 +++++++++-------- .../GRPCMessageFramerTests.swift | 42 ++++++++++++++++++- 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Compression/Zlib.swift b/Sources/GRPCHTTP2Core/Compression/Zlib.swift index b94bd8a1d..7906aa743 100644 --- a/Sources/GRPCHTTP2Core/Compression/Zlib.swift +++ b/Sources/GRPCHTTP2Core/Compression/Zlib.swift @@ -38,6 +38,9 @@ extension Zlib { /// Creates a new compressor for the given compression format. /// /// This compressor is only suitable for compressing whole messages at a time. + /// + /// - Important: ``Compressor/end()`` must be called when the compressor is not needed + /// anymore, to deallocate any resources allocated by `Zlib`. struct Compressor { // TODO: Make this ~Copyable when 5.9 is the lowest supported Swift version. @@ -86,6 +89,9 @@ extension Zlib { /// Creates a new decompressor for the given compression format. /// /// This decompressor is only suitable for compressing whole messages at a time. + /// + /// - Important: ``Decompressor/end()`` must be called when the compressor is not needed + /// anymore, to deallocate any resources allocated by `Zlib`. struct Decompressor { // TODO: Make this ~Copyable when 5.9 is the lowest supported Swift version. diff --git a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift index 0e7717d3d..590b5efeb 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift @@ -32,15 +32,11 @@ struct GRPCMessageFramer { /// reserves capacity in powers of 2. This way, we can take advantage of the whole buffer. static let maximumWriteBufferLength = 65_536 - private var pendingMessages: OneOrManyQueue - - private struct PendingMessage { - let bytes: [UInt8] - let compress: Bool - } + private var pendingMessages: OneOrManyQueue<[UInt8]> private var writeBuffer: ByteBuffer + /// Create a new ``GRPCMessageFramer``. init() { self.pendingMessages = OneOrManyQueue() self.writeBuffer = ByteBuffer() @@ -48,15 +44,16 @@ struct GRPCMessageFramer { /// Queue the given bytes to be framed and potentially coalesced alongside other messages in a `ByteBuffer`. /// The resulting data will be returned when calling ``GRPCMessageFramer/next()``. - /// If `compress` is true, then the given bytes will be compressed using the configured compression algorithm. - mutating func append(_ bytes: [UInt8], compress: Bool) { - self.pendingMessages.append(PendingMessage(bytes: bytes, compress: compress)) + mutating func append(_ bytes: [UInt8]) { + self.pendingMessages.append(bytes) } /// If there are pending messages to be framed, a `ByteBuffer` will be returned with the framed data. /// Data may also be compressed (if configured) and multiple frames may be coalesced into the same `ByteBuffer`. + /// - Parameter compressor: An optional compressor: if present, payloads will be compressed; otherwise + /// they'll be framed as-is. /// - Throws: If an error is encountered, such as a compression failure, an error will be thrown. - mutating func next() throws -> ByteBuffer? { + mutating func next(compressor: Zlib.Compressor? = nil) throws -> ByteBuffer? { if self.pendingMessages.isEmpty { // Nothing pending: exit early. return nil @@ -72,27 +69,34 @@ struct GRPCMessageFramer { var requiredCapacity = 0 for message in self.pendingMessages { - requiredCapacity += message.bytes.count + Self.metadataLength + requiredCapacity += message.count + Self.metadataLength } self.writeBuffer.clear(minimumCapacity: requiredCapacity) while let message = self.pendingMessages.pop() { - try self.encode(message) + try self.encode(message, compressor: compressor) } return self.writeBuffer } - private mutating func encode(_ message: PendingMessage) throws { - if message.compress { + private mutating func encode(_ message: [UInt8], compressor: Zlib.Compressor?) throws { + if let compressor { self.writeBuffer.writeInteger(UInt8(1)) // Set compression flag - // TODO: compress message and write the compressed message length + bytes + + // Write zeroes as length - we'll write the actual compressed size after compression. + let lengthIndex = self.writeBuffer.writerIndex + self.writeBuffer.writeInteger(UInt32(0)) + + // Compress and overwrite the payload length field with the right length. + let writtenBytes = try compressor.compress(message, into: &self.writeBuffer) + self.writeBuffer.setInteger(UInt32(writtenBytes), at: lengthIndex) } else { self.writeBuffer.writeMultipleIntegers( UInt8(0), // Clear compression flag - UInt32(message.bytes.count) // Set message length + UInt32(message.count) // Set message length ) - self.writeBuffer.writeBytes(message.bytes) + self.writeBuffer.writeBytes(message) } } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift index 95029536f..886e60319 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift @@ -22,7 +22,7 @@ import XCTest final class GRPCMessageFramerTests: XCTestCase { func testSingleWrite() throws { var framer = GRPCMessageFramer() - framer.append(Array(repeating: 42, count: 128), compress: false) + framer.append(Array(repeating: 42, count: 128)) var buffer = try XCTUnwrap(framer.next()) let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) @@ -35,12 +35,49 @@ final class GRPCMessageFramerTests: XCTestCase { XCTAssertNil(try framer.next()) } + private func testSingleWrite(compressionMethod: Zlib.Method) throws { + let compressor = Zlib.Compressor(method: compressionMethod) + defer { + compressor.end() + } + var framer = GRPCMessageFramer() + + let message = [UInt8](repeating: 42, count: 128) + framer.append(message) + + var buffer = ByteBuffer() + let testCompressor = Zlib.Compressor(method: compressionMethod) + let compressedSize = try testCompressor.compress(message, into: &buffer) + let compressedMessage = buffer.readSlice(length: compressedSize) + defer { + testCompressor.end() + } + + buffer = try XCTUnwrap(framer.next(compressor: compressor)) + let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) + XCTAssertTrue(compressed) + XCTAssertEqual(length, UInt32(compressedSize)) + XCTAssertEqual(buffer.readSlice(length: Int(length)), compressedMessage) + XCTAssertEqual(buffer.readableBytes, 0) + + // No more bufers. + XCTAssertNil(try framer.next()) + } + + func testSingleWriteDeflateCompressed() throws { + try self.testSingleWrite(compressionMethod: .deflate) + } + + func testSingleWriteGZIPCompressed() throws { + try self.testSingleWrite(compressionMethod: .gzip) + } + func testMultipleWrites() throws { var framer = GRPCMessageFramer() let messages = 100 for _ in 0 ..< messages { - framer.append(Array(repeating: 42, count: 128), compress: false) + framer.append(Array(repeating: 42, count: 128)) } var buffer = try XCTUnwrap(framer.next()) @@ -56,6 +93,7 @@ final class GRPCMessageFramerTests: XCTestCase { // No more bufers. XCTAssertNil(try framer.next()) } + } extension ByteBuffer { From be8e15dd7266924b20d92368a1cfd90653078524 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 25 Jan 2024 14:58:48 +0000 Subject: [PATCH 218/580] Better waiter errors with NIOTS (#1775) Motivation: When establishing a connection using NIOTS, Network.framework may enter a 'waiting' state because of a transient error. This is surfaced as a user inbound event. In many cases the connect call won't resolve until the connect timeout passes (defaults to 20 seconds). Attempts to start a call on this connection during this time will fail with a timeout at the connection pool layer (where the max wait time defaults to 5 seconds) and users will see a 'deadline exceeded' error without any more context. However, we can provide more information by passing up the transient error from Network.framework to the connection pool. This isn't currently possible as events flow up on state changes, in this case from connecting to transient failure when the connect times out. Modifications: - Catch the `WaitingForConnectivity` event and bubble up its error though the idle handler to the connection pool Result: Better errors on connect timeouts in some situations --- Sources/GRPC/ConnectionManager.swift | 49 +++++++++++++--- .../GRPC/ConnectionPool/ConnectionPool.swift | 6 ++ Sources/GRPC/GRPCIdleHandler.swift | 9 +++ .../GRPCTests/GRPCNetworkFrameworkTests.swift | 57 +++++++++++++++++++ 4 files changed, 112 insertions(+), 9 deletions(-) diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 157be809f..0c92c7ff2 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -33,6 +33,7 @@ internal final class ConnectionManager: @unchecked Sendable { internal struct ConnectingState { var backoffIterator: ConnectionBackoffIterator? var reconnect: Reconnect + var connectError: Error? var candidate: EventLoopFuture var readyChannelMuxPromise: EventLoopPromise @@ -656,10 +657,30 @@ internal final class ConnectionManager: @unchecked Sendable { ] ) - case .connecting: - // Ignore the error, the channel promise will notify the manager of any error which occurs - // while connecting. - () + case .connecting(var state): + // Record the error, the channel promise will notify the manager of any error which occurs + // while connecting. It may be overridden by this error if it contains more relevant + // information + if state.connectError == nil { + state.connectError = error + self.state = .connecting(state) + + // The pool is only notified of connection errors when the connection transitions to the + // transient failure state. However, in some cases (i.e. with NIOTS), errors can be thrown + // during the connect but before the connect times out. + // + // This opens up a period of time where you can start a call and have it fail with + // deadline exceeded (because no connection was available within the configured max + // wait time for the pool) but without any diagnostic information. The information is + // available but it hasn't been made available to the pool at that point in time. + // + // The delegate can't easily be modified (it's public API) and a new API doesn't make all + // that much sense so we elect to check whether the delegate is the pool and call it + // directly. + if let pool = self.connectivityDelegate as? ConnectionPool { + pool.sync.updateMostRecentError(error) + } + } case var .active(state): state.error = error @@ -935,16 +956,26 @@ extension ConnectionManager { switch self.state { case let .connecting(connecting): + let reportedError: Error + switch error as? ChannelError { + case .some(.connectTimeout): + // A more relevant error may have been caught earlier. Use that in preference to the + // timeout as it'll likely be more useful. + reportedError = connecting.connectError ?? error + default: + reportedError = error + } + // Should we reconnect? switch connecting.reconnect { // No, shutdown. case .none: self.logger.debug("shutting down connection, no reconnect configured/remaining") self.state = .shutdown( - ShutdownState(closeFuture: self.eventLoop.makeSucceededFuture(()), reason: error) + ShutdownState(closeFuture: self.eventLoop.makeSucceededFuture(()), reason: reportedError) ) - connecting.readyChannelMuxPromise.fail(error) - connecting.candidateMuxPromise.fail(error) + connecting.readyChannelMuxPromise.fail(reportedError) + connecting.candidateMuxPromise.fail(reportedError) // Yes, after a delay. case let .after(delay): @@ -953,10 +984,10 @@ extension ConnectionManager { self.startConnecting() } self.state = .transientFailure( - TransientFailureState(from: connecting, scheduled: scheduled, reason: error) + TransientFailureState(from: connecting, scheduled: scheduled, reason: reportedError) ) // Candidate mux users are not willing to wait. - connecting.candidateMuxPromise.fail(error) + connecting.candidateMuxPromise.fail(reportedError) } // The application must have called shutdown while we were trying to establish a connection diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 34b4bec80..fcce78ad9 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -857,6 +857,12 @@ extension ConnectionPool { self.pool.eventLoop.assertInEventLoop() return self.pool._connections.values.reduce(0) { $0 + $1.reservedStreams } } + + /// Updates the most recent connection error. + internal func updateMostRecentError(_ error: Error) { + self.pool.eventLoop.assertInEventLoop() + self.pool.updateMostRecentError(error) + } } internal var sync: Sync { diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 614d6488e..0471526df 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -17,6 +17,7 @@ import Logging import NIOCore import NIOHTTP2 import NIOTLS +import NIOTransportServices internal final class GRPCIdleHandler: ChannelInboundHandler { typealias InboundIn = HTTP2Frame @@ -287,6 +288,14 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { ) context.fireUserInboundEventTriggered(event) } else { + #if canImport(Network) + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if let waitsForConnectivity = event as? NIOTSNetworkEvents.WaitingForConnectivity { + self.mode.connectionManager?.channelError(waitsForConnectivity.transientError) + } + } + #endif + context.fireUserInboundEventTriggered(event) } } diff --git a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift index ecc81bf36..d0c0e1809 100644 --- a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift +++ b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift @@ -24,6 +24,7 @@ import NIOCore import NIOPosix import NIOSSL import NIOTransportServices +import GRPCSampleData import Security import XCTest @@ -208,6 +209,62 @@ final class GRPCNetworkFrameworkTests: GRPCTestCase { XCTAssertNoThrow(try self.doEchoGet()) } + + func testWaiterPicksUpNWError( + _ configure: (inout GRPCChannelPool.Configuration) -> Void + ) async throws { + let builder = Server.usingTLSBackedByNIOSSL( + on: self.group, + certificateChain: [SampleCertificate.server.certificate], + privateKey: SamplePrivateKey.server + ) + + let server = try await builder.bind(host: "127.0.0.1", port: 0).get() + defer { try? server.close().wait() } + + let client = try GRPCChannelPool.with( + target: .hostAndPort("127.0.0.1", server.channel.localAddress!.port!), + transportSecurity: .tls(.makeClientConfigurationBackedByNetworkFramework()), + eventLoopGroup: self.tsGroup + ) { + configure(&$0) + } + + let echo = Echo_EchoAsyncClient(channel: client) + do { + let _ = try await echo.get(.with { $0.text = "ignored" }) + } catch let error as GRPCConnectionPoolError { + XCTAssertEqual(error.code, .deadlineExceeded) + XCTAssert(error.underlyingError is NWError) + } catch { + XCTFail("Expected GRPCConnectionPoolError") + } + + let promise = self.group.next().makePromise(of: Void.self) + client.closeGracefully(deadline: .now() + .seconds(1), promise: promise) + try await promise.futureResult.get() + } + + func testErrorPickedUpBeforeConnectTimeout() async throws { + try await self.testWaiterPicksUpNWError { + // Configure the wait time to be less than the connect timeout, the waiter + // should fail with the appropriate NWError before the connect times out. + $0.connectionPool.maxWaitTime = .milliseconds(500) + $0.connectionBackoff.minimumConnectionTimeout = 1.0 + } + } + + func testNotWaitingForConnectivity() async throws { + try await self.testWaiterPicksUpNWError { + // The minimum connect time is still high, but setting wait for activity to false + // means it fails on entering the waiting state rather than seeing out the connect + // timeout. + $0.connectionPool.maxWaitTime = .milliseconds(500) + $0.debugChannelInitializer = { channel in + channel.setOption(NIOTSChannelOptions.waitForActivity, value: false) + } + } + } } #endif // canImport(Network) From a6802a80fa511c385ea99de383d2a92087e2657d Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 26 Jan 2024 14:53:34 +0000 Subject: [PATCH 219/580] Add `GRPCMessageDeframer` (#1776) --- Package.swift | 2 + .../GRPCHTTP2Core/GRPCMessageDeframer.swift | 103 ++++++++ .../GRPCMessageDeframerTests.swift | 219 ++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/GRPCMessageDeframer.swift create mode 100644 Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift diff --git a/Package.swift b/Package.swift index f61589f4d..4b194d25b 100644 --- a/Package.swift +++ b/Package.swift @@ -128,6 +128,7 @@ extension Target.Dependency { name: "NIOTransportServices", package: "swift-nio-transport-services" ) + static let nioTestUtils: Self = .product(name: "NIOTestUtils", package: "swift-nio") static let logging: Self = .product(name: "Logging", package: "swift-log") static let protobuf: Self = .product(name: "SwiftProtobuf", package: "swift-protobuf") static let protobufPluginLibrary: Self = .product( @@ -312,6 +313,7 @@ extension Target { .nioCore, .nioHTTP2, .nioEmbedded, + .nioTestUtils, ] ) diff --git a/Sources/GRPCHTTP2Core/GRPCMessageDeframer.swift b/Sources/GRPCHTTP2Core/GRPCMessageDeframer.swift new file mode 100644 index 000000000..af8282bb4 --- /dev/null +++ b/Sources/GRPCHTTP2Core/GRPCMessageDeframer.swift @@ -0,0 +1,103 @@ +/* + * 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 NIOCore + +/// A ``GRPCMessageDeframer`` helps with the deframing of gRPC data frames: +/// - It reads the frame's metadata to know whether the message payload is compressed or not, and its length +/// - It reads and decompresses the payload, if compressed +/// - It helps put together frames that have been split across multiple `ByteBuffers` by the underlying transport +struct GRPCMessageDeframer: NIOSingleStepByteToMessageDecoder { + /// Length of the gRPC message header (1 compression byte, 4 bytes for the length). + static let metadataLength = 5 + static let defaultMaximumPayloadSize = Int.max + + typealias InboundOut = [UInt8] + + private let decompressor: Zlib.Decompressor? + private let maximumPayloadSize: Int + + /// Create a new ``GRPCMessageDeframer``. + /// - Parameters: + /// - maximumPayloadSize: The maximum size a message payload can be. + /// - decompressor: A `Zlib.Decompressor` to use when decompressing compressed gRPC messages. + /// - Important: You must call `end()` on the `decompressor` when you're done using it, to clean + /// up any resources allocated by `Zlib`. + init( + maximumPayloadSize: Int = Self.defaultMaximumPayloadSize, + decompressor: Zlib.Decompressor? = nil + ) { + self.maximumPayloadSize = maximumPayloadSize + self.decompressor = decompressor + } + + mutating func decode(buffer: inout ByteBuffer) throws -> InboundOut? { + guard buffer.readableBytes >= Self.metadataLength else { + // If we cannot read enough bytes to cover the metadata's length, then we + // need to wait for more bytes to become available to us. + return nil + } + + // Store the current reader index in case we don't yet have enough + // bytes in the buffer to decode a full frame, and need to reset it. + // The force-unwraps for the compression flag and message length are safe, + // because we've checked just above that we've got at least enough bytes to + // read all of the metadata. + let originalReaderIndex = buffer.readerIndex + let isMessageCompressed = buffer.readInteger(as: UInt8.self)! == 1 + let messageLength = buffer.readInteger(as: UInt32.self)! + + if messageLength > self.maximumPayloadSize { + throw RPCError( + code: .resourceExhausted, + message: """ + Message has exceeded the configured maximum payload size \ + (max: \(self.maximumPayloadSize), actual: \(messageLength)) + """ + ) + } + + guard var message = buffer.readSlice(length: Int(messageLength)) else { + // `ByteBuffer/readSlice(length:)` returns nil when there are not enough + // bytes to read the requested length. This can happen if we don't yet have + // enough bytes buffered to read the full message payload. + // By reading the metadata though, we have already moved the reader index, + // so we must reset it to its previous, original position for now, + // and return. We'll try decoding again, once more bytes become available + // in our buffer. + buffer.moveReaderIndex(to: originalReaderIndex) + return nil + } + + if isMessageCompressed { + guard let decompressor = self.decompressor else { + // We cannot decompress the payload - throw an error. + throw RPCError( + code: .internalError, + message: "Received a compressed message payload, but no decompressor has been configured." + ) + } + return try decompressor.decompress(&message, limit: self.maximumPayloadSize) + } else { + return Array(buffer: message) + } + } + + mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> InboundOut? { + try self.decode(buffer: &buffer) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift new file mode 100644 index 000000000..98d5fc046 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift @@ -0,0 +1,219 @@ +/* + * 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 NIOCore +import NIOTestUtils +import XCTest + +@testable import GRPCHTTP2Core + +final class GRPCMessageDeframerTests: XCTestCase { + func testReadMultipleMessagesWithoutCompression() throws { + let firstMessage = { + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + buffer.writeInteger(UInt32(16)) + buffer.writeRepeatingByte(42, count: 16) + return buffer + }() + + let secondMessage = { + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + buffer.writeInteger(UInt32(8)) + buffer.writeRepeatingByte(43, count: 8) + return buffer + }() + + try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [ + (firstMessage, [Array(repeating: UInt8(42), count: 16)]), + (secondMessage, [Array(repeating: UInt8(43), count: 8)]), + ]) { + GRPCMessageDeframer() + } + } + + func testReadMessageOverSizeLimitWithoutCompression() throws { + let deframer = GRPCMessageDeframer(maximumPayloadSize: 100) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + buffer.writeInteger(UInt32(101)) + buffer.writeRepeatingByte(42, count: 101) + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: buffer) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" + ) + } + } + + func testReadMessageOverSizeLimitButWithoutActualMessageBytes() throws { + let deframer = GRPCMessageDeframer(maximumPayloadSize: 100) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + // Set the message length field to be over the maximum payload size, but + // don't write the actual message bytes. This is to ensure that the payload + // size limit is enforced _before_ the payload is actually read. + buffer.writeInteger(UInt32(101)) + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: buffer) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" + ) + } + } + + func testCompressedMessageWithoutConfiguringDecompressor() throws { + let deframer = GRPCMessageDeframer(maximumPayloadSize: 100) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(1)) + buffer.writeInteger(UInt32(10)) + buffer.writeRepeatingByte(42, count: 10) + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: buffer) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Received a compressed message payload, but no decompressor has been configured." + ) + } + } + + private func testReadMultipleMessagesWithCompression(method: Zlib.Method) throws { + let decompressor = Zlib.Decompressor(method: method) + let compressor = Zlib.Compressor(method: method) + var framer = GRPCMessageFramer() + defer { + decompressor.end() + compressor.end() + } + + let firstMessage = try { + framer.append(Array(repeating: 42, count: 100)) + return try framer.next(compressor: compressor)! + }() + + let secondMessage = try { + framer.append(Array(repeating: 43, count: 110)) + return try framer.next(compressor: compressor)! + }() + + try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [ + (firstMessage, [Array(repeating: 42, count: 100)]), + (secondMessage, [Array(repeating: 43, count: 110)]), + ]) { + GRPCMessageDeframer(maximumPayloadSize: 1000, decompressor: decompressor) + } + } + + func testReadMultipleMessagesWithDeflateCompression() throws { + try self.testReadMultipleMessagesWithCompression(method: .deflate) + } + + func testReadMultipleMessagesWithGZIPCompression() throws { + try self.testReadMultipleMessagesWithCompression(method: .gzip) + } + + func testReadCompressedMessageOverSizeLimitBeforeDecompressing() throws { + let deframer = GRPCMessageDeframer(maximumPayloadSize: 1) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + let compressor = Zlib.Compressor(method: .gzip) + var framer = GRPCMessageFramer() + defer { + compressor.end() + } + + framer.append(Array(repeating: 42, count: 100)) + let framedMessage = try framer.next(compressor: compressor)! + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: framedMessage) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + """ + Message has exceeded the configured maximum payload size \ + (max: 1, actual: \(framedMessage.readableBytes - GRPCMessageDeframer.metadataLength)) + """ + ) + } + } + + private func testReadDecompressedMessageOverSizeLimit(method: Zlib.Method) throws { + let decompressor = Zlib.Decompressor(method: method) + let deframer = GRPCMessageDeframer(maximumPayloadSize: 100, decompressor: decompressor) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + let compressor = Zlib.Compressor(method: method) + var framer = GRPCMessageFramer() + defer { + decompressor.end() + compressor.end() + } + + framer.append(Array(repeating: 42, count: 101)) + let framedMessage = try framer.next(compressor: compressor)! + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: framedMessage) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual(error.message, "Message is too large to decompress.") + } + } + + func testReadDecompressedMessageOverSizeLimitWithDeflateCompression() throws { + try self.testReadDecompressedMessageOverSizeLimit(method: .deflate) + } + + func testReadDecompressedMessageOverSizeLimitWithGZIPCompression() throws { + try self.testReadDecompressedMessageOverSizeLimit(method: .gzip) + } +} From 373d9f4598167785d685b9009d9444c33ac802b0 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:48:53 +0000 Subject: [PATCH 220/580] [CodeGen Protobuf support] SwiftProtobuf parser to CodeGenerationRequest (#1772) Motivation: We need the parser to transform a FileDescriptor object into a CodeGenerationRequest objet which can be used as input in the new code generation process. Modifications: - Created the parser struct - Created a test for the type Result: FileDescriptors can now be transformed into CodeGenerationRequest, which will be useful in implementing the `protoc-gen-grpc-swift` v2. --------- Co-authored-by: George Barnett --- Package.swift | 29 ++- .../GRPCCodeGen/CodeGenerationRequest.swift | 10 +- .../ProtobufCodeGenParser.swift | 123 +++++++++++ .../ProtobufCodeGenParserTests.swift | 193 ++++++++++++++++++ 4 files changed, 347 insertions(+), 8 deletions(-) create mode 100644 Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift create mode 100644 Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift diff --git a/Package.swift b/Package.swift index 4b194d25b..d0db22a72 100644 --- a/Package.swift +++ b/Package.swift @@ -94,6 +94,7 @@ extension Target.Dependency { static let reflectionService: Self = .target(name: "GRPCReflectionService") static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") static let grpcProtobuf: Self = .target(name: "GRPCProtobuf") + static let grpcProtobufCodeGen: Self = .target(name: "GRPCProtobufCodeGen") // Target dependencies; internal static let grpcSampleData: Self = .target(name: "GRPCSampleData") @@ -235,6 +236,7 @@ extension Target { dependencies: [ .protobuf, .protobufPluginLibrary, + .grpcCodeGen ], exclude: [ "README.md", @@ -341,12 +343,22 @@ extension Target { static let grpcProtobufTests: Target = .testTarget( name: "GRPCProtobufTests", dependencies: [ - .grpcCore, .grpcProtobuf, + .grpcCore, .protobuf ] ) + static let grpcProtobufCodeGenTests: Target = .testTarget( + name: "GRPCProtobufCodeGenTests", + dependencies: [ + .grpcCodeGen, + .grpcProtobufCodeGen, + .protobuf, + .protobufPluginLibrary + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -601,10 +613,19 @@ extension Target { name: "GRPCProtobuf", dependencies: [ .grpcCore, - .protobuf + .protobuf, ], path: "Sources/GRPCProtobuf" ) + static let grpcProtobufCodeGen: Target = .target( + name: "GRPCProtobufCodeGen", + dependencies: [ + .protobuf, + .protobufPluginLibrary, + .grpcCodeGen + ], + path: "Sources/GRPCProtobufCodeGen" + ) } // MARK: - Products @@ -693,6 +714,7 @@ let package = Package( .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf, + .grpcProtobufCodeGen, // v2 tests .grpcCoreTests, @@ -702,7 +724,8 @@ let package = Package( .grpcHTTP2CoreTests, .grpcHTTP2TransportNIOPosixTests, .grpcHTTP2TransportNIOTransportServicesTests, - .grpcProtobufTests + .grpcProtobufTests, + .grpcProtobufCodeGenTests ] ) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 9711b39d2..0c0abb1b9 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -81,7 +81,7 @@ public struct CodeGenerationRequest { } /// Represents an import: a module or a specific item from a module. - public struct Dependency { + public struct Dependency: Equatable { /// If the dependency is an item, the property's value is the item representation. /// If the dependency is a module, this property is nil. public var item: Item? = nil @@ -111,7 +111,7 @@ public struct CodeGenerationRequest { } /// Represents an item imported from a module. - public struct Item { + public struct Item: Equatable { /// The keyword that specifies the item's kind (e.g. `func`, `struct`). public var kind: Kind @@ -124,7 +124,7 @@ public struct CodeGenerationRequest { } /// Represents the imported item's kind. - public struct Kind { + public struct Kind: Equatable { /// Describes the keyword associated with the imported item. internal enum Value: String { case `typealias` @@ -186,8 +186,8 @@ public struct CodeGenerationRequest { } /// Describes any requirement for the `@preconcurrency` attribute. - public struct PreconcurrencyRequirement { - internal enum Value { + public struct PreconcurrencyRequirement: Equatable { + internal enum Value: Equatable { case required case notRequired case requiredOnOS([String]) diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift new file mode 100644 index 000000000..1ff93f6a0 --- /dev/null +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -0,0 +1,123 @@ +/* + * 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 Foundation +import SwiftProtobuf +import SwiftProtobufPluginLibrary + +import struct GRPCCodeGen.CodeGenerationRequest + +/// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. +internal struct ProtobufCodeGenParser { + internal init() {} + internal func parse(input: FileDescriptor) throws -> CodeGenerationRequest { + var header = input.header + // Ensuring there is a blank line after the header. + if !header.isEmpty && !header.hasSuffix("\n\n") { + header.append("\n") + } + let leadingTrivia = """ + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: \(input.name) + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + """ + var dependencies = input.dependencies.map { + CodeGenerationRequest.Dependency(module: $0.name) + } + dependencies.append(CodeGenerationRequest.Dependency(module: "GRPCProtobuf")) + let lookupSerializer: (String) -> String = { messageType in + "ProtobufSerializer<\(messageType)>()" + } + let lookupDeserializer: (String) -> String = { messageType in + "ProtobufDeserializer<\(messageType)>()" + } + let services = input.services.map { + CodeGenerationRequest.ServiceDescriptor(descriptor: $0, package: input.package) + } + + return CodeGenerationRequest( + fileName: input.name, + leadingTrivia: header + leadingTrivia, + dependencies: dependencies, + services: services, + lookupSerializer: lookupSerializer, + lookupDeserializer: lookupDeserializer + ) + } +} + +extension CodeGenerationRequest.ServiceDescriptor { + fileprivate init(descriptor: ServiceDescriptor, package: String) { + let methods = descriptor.methods.map { + CodeGenerationRequest.ServiceDescriptor.MethodDescriptor(descriptor: $0) + } + let name = CodeGenerationRequest.Name( + base: descriptor.name, + generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), + generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name) + ) + let namespace = CodeGenerationRequest.Name( + base: package, + generatedUpperCase: NamingUtils.toUpperCamelCase(package), + generatedLowerCase: NamingUtils.toLowerCamelCase(package) + ) + let documentation = descriptor.protoSourceComments() + self.init(documentation: documentation, name: name, namespace: namespace, methods: methods) + } +} + +extension CodeGenerationRequest.ServiceDescriptor.MethodDescriptor { + fileprivate init(descriptor: MethodDescriptor) { + let name = CodeGenerationRequest.Name( + base: descriptor.name, + generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), + generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name) + ) + let documentation = descriptor.protoSourceComments() + self.init( + documentation: documentation, + name: name, + isInputStreaming: descriptor.clientStreaming, + isOutputStreaming: descriptor.serverStreaming, + inputType: descriptor.inputType.name, + outputType: descriptor.outputType.name + ) + } +} + +extension FileDescriptor { + fileprivate var header: String { + var header = String() + // Field number used to collect the syntax field which is usually the first + // declaration in a.proto file. + // See more here: + // https://github.com/apple/swift-protobuf/blob/main/Protos/SwiftProtobuf/google/protobuf/descriptor.proto + let syntaxPath = IndexPath(index: 12) + if let syntaxLocation = self.sourceCodeInfoLocation(path: syntaxPath) { + header = syntaxLocation.asSourceComment( + commentPrefix: "///", + leadingDetachedPrefix: "//" + ) + } + return header + } +} diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift new file mode 100644 index 000000000..664501d82 --- /dev/null +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -0,0 +1,193 @@ +/* + * 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 GRPCCodeGen +import SwiftProtobuf +import SwiftProtobufPluginLibrary +import XCTest + +@testable import GRPCProtobufCodeGen + +final class ProtobufCodeGenParserTests: XCTestCase { + func testParser() throws { + let parsedCodeGenRequest = try ProtobufCodeGenParser().parse( + input: self.helloWorldFileDescriptor + ) + XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") + XCTAssertEqual( + parsedCodeGenRequest.leadingTrivia, + """ + // Copyright 2015 gRPC authors. + // + // 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. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: helloworld.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + """ + ) + + XCTAssertEqual(parsedCodeGenRequest.services.count, 1) + + let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( + documentation: "/// Sends a greeting.\n", + name: CodeGenerationRequest.Name( + base: "SayHello", + generatedUpperCase: "SayHello", + generatedLowerCase: "sayHello" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "HelloRequest", + outputType: "HelloReply" + ) + guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } + XCTAssertEqual(method, expectedMethod) + + let expectedService = CodeGenerationRequest.ServiceDescriptor( + documentation: "/// The greeting service definition.\n", + name: CodeGenerationRequest.Name( + base: "Greeter", + generatedUpperCase: "Greeter", + generatedLowerCase: "greeter" + ), + namespace: CodeGenerationRequest.Name( + base: "helloworld", + generatedUpperCase: "Helloworld", + generatedLowerCase: "helloworld" + ), + methods: [expectedMethod] + ) + guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } + XCTAssertEqual(service, expectedService) + XCTAssertEqual(service.methods.count, 1) + + XCTAssertEqual( + parsedCodeGenRequest.lookupSerializer("HelloRequest"), + "ProtobufSerializer()" + ) + XCTAssertEqual( + parsedCodeGenRequest.lookupDeserializer("HelloRequest"), + "ProtobufDeserializer()" + ) + XCTAssertEqual(parsedCodeGenRequest.dependencies.count, 1) + XCTAssertEqual( + parsedCodeGenRequest.dependencies[0], + CodeGenerationRequest.Dependency(module: "GRPCProtobuf") + ) + } + + var helloWorldFileDescriptor: FileDescriptor { + let requestType = Google_Protobuf_DescriptorProto.with { + $0.name = "HelloRequest" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "name" + $0.number = 1 + $0.label = .optional + $0.type = .string + $0.jsonName = "name" + } + ] + } + let responseType = Google_Protobuf_DescriptorProto.with { + $0.name = "HelloReply" + $0.field = [ + Google_Protobuf_FieldDescriptorProto.with { + $0.name = "message" + $0.number = 1 + $0.label = .optional + $0.type = .string + $0.jsonName = "message" + } + ] + } + + let service = Google_Protobuf_ServiceDescriptorProto.with { + $0.name = "Greeter" + $0.method = [ + Google_Protobuf_MethodDescriptorProto.with { + $0.name = "SayHello" + $0.inputType = ".helloworld.HelloRequest" + $0.outputType = ".helloworld.HelloReply" + $0.clientStreaming = false + $0.serverStreaming = false + } + ] + } + let protoDescriptor = Google_Protobuf_FileDescriptorProto.with { + $0.name = "helloworld.proto" + $0.package = "helloworld" + $0.messageType = [requestType, responseType] + $0.service = [service] + $0.sourceCodeInfo = Google_Protobuf_SourceCodeInfo.with { + $0.location = [ + Google_Protobuf_SourceCodeInfo.Location.with { + $0.path = [12] + $0.span = [14, 0, 18] + $0.leadingDetachedComments = [ + """ + Copyright 2015 gRPC authors. + + 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. + + """ + ] + }, + Google_Protobuf_SourceCodeInfo.Location.with { + $0.path = [6, 0] + $0.span = [19, 0, 22, 1] + $0.leadingComments = " The greeting service definition.\n" + }, + Google_Protobuf_SourceCodeInfo.Location.with { + $0.path = [6, 0, 2, 0] + $0.span = [21, 2, 53] + $0.leadingComments = " Sends a greeting.\n" + }, + ] + } + $0.syntax = "proto3" + } + let descriptorSet = DescriptorSet(protos: [protoDescriptor]) + return descriptorSet.fileDescriptor(named: "helloworld.proto")! + } +} From 9499327e030af33815fe085887d8669395538c4a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 29 Jan 2024 10:42:50 +0000 Subject: [PATCH 221/580] Add a handler for managing client side keepalive (#1773) Motivation: Clients can be configured to use keepalive which periodically sends PING frames to the server, typically only if streams are open, but this behaviour is configurable. Clients should also close connections after periods of inactivity (if no streams are open). Modifications: Add a client channel handler and state machine which handles client side keepalive and connection idling. The handler also emits connection events to the next handler, these events can be used to determine whether to take the connection out of service (i.e. stop creating new streams on it). Results: The client can manage keepalive and connection idling. --- .../Connection/ClientConnectionHandler.swift | 421 ++++++++++++++++++ Sources/GRPCHTTP2Core/Internal/Timer.swift | 67 +++ .../ServerConnectionManagementHandler.swift | 24 - ...ntConnectionHandlerStateMachineTests.swift | 107 +++++ .../ClientConnectionHandlerTests.swift | 274 ++++++++++++ .../Internal/TimerTests.swift | 99 ++++ 6 files changed, 968 insertions(+), 24 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/Timer.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift new file mode 100644 index 000000000..0b7f27791 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -0,0 +1,421 @@ +/* + * 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 NIOCore +import NIOHTTP2 + +/// An event which happens on a client's HTTP/2 connection. +enum ClientConnectionEvent: Sendable, Hashable { + enum CloseReason: Sendable, Hashable { + /// The server sent a GOAWAY frame to the client. + case goAway(HTTP2ErrorCode, String) + /// The keep alive timer fired and subsequently timed out. + case keepAliveExpired + /// The connection became idle. + case idle + } + + /// The connection has started shutting down, no new streams should be created. + case closing(CloseReason) +} + +/// A `ChannelHandler` which manages part of the lifecycle of a gRPC connection over HTTP/2. +/// +/// This handler is responsible for managing several aspects of the connection. These include: +/// 1. Periodically sending keep alive pings to the server (if configured) and closing the +/// connection if necessary. +/// 2. Closing the connection if it is idle (has no open streams) for a configured amount of time. +/// 3. Forwarding lifecycle events to the next handler. +/// +/// Some of the behaviours are described in [gRFC A8](https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md). +final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandler { + typealias InboundIn = HTTP2Frame + typealias InboundOut = ClientConnectionEvent + + typealias OutboundIn = Never + typealias OutboundOut = HTTP2Frame + + /// The `EventLoop` of the `Channel` this handler exists in. + private let eventLoop: EventLoop + + /// The maximum amount of time the connection may be idle for. If the connection remains idle + /// (i.e. has no open streams) for this period of time then the connection will be gracefully + /// closed. + private var maxIdleTimer: Timer? + + /// The amount of time to wait before sending a keep alive ping. + private var keepAliveTimer: Timer? + + /// The amount of time the client has to reply after sending a keep alive ping. Only used if + /// `keepAliveTimer` is set. + private var keepAliveTimeoutTimer: Timer + + /// Opaque data sent in keep alive pings. + private let keepAlivePingData: HTTP2PingData + + /// The current state of the connection. + private var state: StateMachine + + /// Whether a flush is pending. + private var flushPending: Bool + /// Whether `channelRead` has been called and `channelReadComplete` hasn't yet been called. + /// Resets once `channelReadComplete` returns. + private var inReadLoop: Bool + + /// Creates a new handler which manages the lifecycle of a connection. + /// + /// - Parameters: + /// - eventLoop: The `EventLoop` of the `Channel` this handler is placed in. + /// - maxIdleTime: The maximum amount time a connection may be idle for before being closed. + /// - keepAliveTime: The amount of time to wait after reading data before sending a keep-alive + /// ping. + /// - keepAliveTimeout: The amount of time the client has to reply after the server sends a + /// keep-alive ping to keep the connection open. The connection is closed if no reply + /// is received. + /// - keepAliveWithoutCalls: Whether the client sends keep-alive pings when there are no calls + /// in progress. + init( + eventLoop: EventLoop, + maxIdleTime: TimeAmount?, + keepAliveTime: TimeAmount?, + keepAliveTimeout: TimeAmount?, + keepAliveWithoutCalls: Bool + ) { + self.eventLoop = eventLoop + self.maxIdleTimer = maxIdleTime.map { Timer(delay: $0) } + self.keepAliveTimer = keepAliveTime.map { Timer(delay: $0, repeat: true) } + self.keepAliveTimeoutTimer = Timer(delay: keepAliveTimeout ?? .seconds(20)) + self.keepAlivePingData = HTTP2PingData(withInteger: .random(in: .min ... .max)) + self.state = StateMachine(allowKeepAliveWithoutCalls: keepAliveWithoutCalls) + + self.flushPending = false + self.inReadLoop = false + } + + func handlerAdded(context: ChannelHandlerContext) { + assert(context.eventLoop === self.eventLoop) + } + + func channelActive(context: ChannelHandlerContext) { + self.keepAliveTimer?.schedule(on: context.eventLoop) { + self.keepAliveTimerFired(context: context) + } + + self.maxIdleTimer?.schedule(on: context.eventLoop) { + self.maxIdleTimerFired(context: context) + } + } + + func channelInactive(context: ChannelHandlerContext) { + self.state.closed() + self.keepAliveTimer?.cancel() + self.keepAliveTimeoutTimer.cancel() + } + + func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { + switch event { + case let event as NIOHTTP2StreamCreatedEvent: + // Stream created, so the connection isn't idle. + self.maxIdleTimer?.cancel() + self.state.streamOpened(event.streamID) + + case let event as StreamClosedEvent: + switch self.state.streamClosed(event.streamID) { + case .startIdleTimer(let cancelKeepAlive): + // All streams are closed, restart the idle timer, and stop the keep-alive timer (it may + // not stop if keep-alive is allowed when there are no active calls). + self.maxIdleTimer?.schedule(on: context.eventLoop) { + self.maxIdleTimerFired(context: context) + } + + if cancelKeepAlive { + self.keepAliveTimer?.cancel() + } + + case .close: + // Connection was closing but waiting for all streams to close. They must all be closed + // now so close the connection. + context.close(promise: nil) + + case .none: + () + } + + default: + () + } + + context.fireUserInboundEventTriggered(event) + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let frame = self.unwrapInboundIn(data) + self.inReadLoop = true + + switch frame.payload { + case .goAway(_, let errorCode, let data): + // Receiving a GOAWAY frame means we need to stop creating streams immediately and start + // closing the connection. + switch self.state.beginGracefulShutdown() { + case .sendGoAway(let close): + // gRPC servers may indicate why the GOAWAY was sent in the opaque data. + let message = data.map { String(buffer: $0) } ?? "" + context.fireChannelRead(self.wrapInboundOut(.closing(.goAway(errorCode, message)))) + + // Clients should send GOAWAYs when closing a connection. + self.writeAndFlushGoAway(context: context, errorCode: .noError) + if close { + context.close(promise: nil) + } + + case .none: + () + } + + case .ping(let data, let ack): + // Pings are ack'd by the HTTP/2 handler so we only pay attention to acks here, and in + // particular only those carrying the keep-alive data. + if ack, data == self.keepAlivePingData { + self.keepAliveTimeoutTimer.cancel() + self.keepAliveTimer?.schedule(on: context.eventLoop) { + self.keepAliveTimerFired(context: context) + } + } + + default: + () + } + } + + func channelReadComplete(context: ChannelHandlerContext) { + while self.flushPending { + self.flushPending = false + context.flush() + } + + self.inReadLoop = false + context.fireChannelReadComplete() + } +} + +extension ClientConnectionHandler { + private func maybeFlush(context: ChannelHandlerContext) { + if self.inReadLoop { + self.flushPending = true + } else { + context.flush() + } + } + + private func keepAliveTimerFired(context: ChannelHandlerContext) { + guard self.state.sendKeepAlivePing() else { return } + + // Cancel the keep alive timer when the client sends a ping. The timer is resumed when the ping + // is acknowledged. + self.keepAliveTimer?.cancel() + + let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepAlivePingData, ack: false)) + context.write(self.wrapOutboundOut(ping), promise: nil) + self.maybeFlush(context: context) + + // Schedule a timeout on waiting for the response. + self.keepAliveTimeoutTimer.schedule(on: context.eventLoop) { + self.keepAliveTimeoutExpired(context: context) + } + } + + private func keepAliveTimeoutExpired(context: ChannelHandlerContext) { + guard self.state.beginClosing() else { return } + + context.fireChannelRead(self.wrapInboundOut(.closing(.keepAliveExpired))) + self.writeAndFlushGoAway(context: context, message: "keepalive_expired") + context.close(promise: nil) + } + + private func maxIdleTimerFired(context: ChannelHandlerContext) { + guard self.state.beginClosing() else { return } + + context.fireChannelRead(self.wrapInboundOut(.closing(.idle))) + self.writeAndFlushGoAway(context: context, message: "idle") + context.close(promise: nil) + } + + private func writeAndFlushGoAway( + context: ChannelHandlerContext, + errorCode: HTTP2ErrorCode = .noError, + message: String? = nil + ) { + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway( + lastStreamID: 0, + errorCode: errorCode, + opaqueData: message.map { context.channel.allocator.buffer(string: $0) } + ) + ) + + context.write(self.wrapOutboundOut(goAway), promise: nil) + self.maybeFlush(context: context) + } +} + +extension ClientConnectionHandler { + struct StateMachine { + private var state: State + + private enum State { + case active(Active) + case closing(Closing) + case closed + + struct Active { + var openStreams: Set + var allowKeepAliveWithoutCalls: Bool + + init(allowKeepAliveWithoutCalls: Bool) { + self.openStreams = [] + self.allowKeepAliveWithoutCalls = allowKeepAliveWithoutCalls + } + } + + struct Closing { + var allowKeepAliveWithoutCalls: Bool + var openStreams: Set + + init(from state: Active) { + self.openStreams = state.openStreams + self.allowKeepAliveWithoutCalls = state.allowKeepAliveWithoutCalls + } + } + } + + init(allowKeepAliveWithoutCalls: Bool) { + self.state = .active(State.Active(allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls)) + } + + /// Record that the stream with the given ID has been opened. + mutating func streamOpened(_ id: HTTP2StreamID) { + switch self.state { + case .active(var state): + let (inserted, _) = state.openStreams.insert(id) + assert(inserted, "Can't open stream \(Int(id)), it's already open") + self.state = .active(state) + + case .closing(var state): + let (inserted, _) = state.openStreams.insert(id) + assert(inserted, "Can't open stream \(Int(id)), it's already open") + self.state = .closing(state) + + case .closed: + () + } + } + + enum OnStreamClosed: Equatable { + /// Start the idle timer, after which the connection should be closed gracefully. + case startIdleTimer(cancelKeepAlive: Bool) + /// Close the connection. + case close + /// Do nothing. + case none + } + + /// Record that the stream with the given ID has been closed. + mutating func streamClosed(_ id: HTTP2StreamID) -> OnStreamClosed { + let onStreamClosed: OnStreamClosed + + switch self.state { + case .active(var state): + let removedID = state.openStreams.remove(id) + assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") + if state.openStreams.isEmpty { + onStreamClosed = .startIdleTimer(cancelKeepAlive: !state.allowKeepAliveWithoutCalls) + } else { + onStreamClosed = .none + } + self.state = .active(state) + + case .closing(var state): + let removedID = state.openStreams.remove(id) + assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") + onStreamClosed = state.openStreams.isEmpty ? .close : .none + self.state = .closing(state) + + case .closed: + onStreamClosed = .none + } + + return onStreamClosed + } + + /// Returns whether a keep alive ping should be sent to the server. + mutating func sendKeepAlivePing() -> Bool { + let sendKeepAlivePing: Bool + + // Only send a ping if there are open streams or there are no open streams and keep alive + // is permitted when there are no active calls. + switch self.state { + case .active(let state): + sendKeepAlivePing = !state.openStreams.isEmpty || state.allowKeepAliveWithoutCalls + case .closing(let state): + sendKeepAlivePing = !state.openStreams.isEmpty || state.allowKeepAliveWithoutCalls + case .closed: + sendKeepAlivePing = false + } + + return sendKeepAlivePing + } + + enum OnGracefulShutDown: Equatable { + case sendGoAway(Bool) + case none + } + + mutating func beginGracefulShutdown() -> OnGracefulShutDown { + let onGracefulShutdown: OnGracefulShutDown + + switch self.state { + case .active(let state): + // Only close immediately if there are no open streams. The client doesn't need to + // ratchet down the last stream ID as only the client creates streams in gRPC. + let close = state.openStreams.isEmpty + onGracefulShutdown = .sendGoAway(close) + self.state = .closing(State.Closing(from: state)) + + case .closing, .closed: + onGracefulShutdown = .none + } + + return onGracefulShutdown + } + + /// Returns whether the connection should be closed. + mutating func beginClosing() -> Bool { + switch self.state { + case .active(let active): + self.state = .closing(State.Closing(from: active)) + return true + case .closing, .closed: + return false + } + } + + /// Marks the state as closed. + mutating func closed() { + self.state = .closed + } + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/Timer.swift b/Sources/GRPCHTTP2Core/Internal/Timer.swift new file mode 100644 index 000000000..0d97d148e --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/Timer.swift @@ -0,0 +1,67 @@ +/* + * 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 NIOCore + +struct Timer { + /// The delay to wait before running the task. + private let delay: TimeAmount + /// The task to run, if scheduled. + private var task: Kind? + /// Whether the task to schedule is repeated. + private let `repeat`: Bool + + private enum Kind { + case once(Scheduled) + case repeated(RepeatedTask) + + func cancel() { + switch self { + case .once(let task): + task.cancel() + case .repeated(let task): + task.cancel() + } + } + } + + init(delay: TimeAmount, repeat: Bool = false) { + self.delay = delay + self.task = nil + self.repeat = `repeat` + } + + /// Schedule a task on the given `EventLoop`. + mutating func schedule(on eventLoop: EventLoop, work: @escaping () throws -> Void) { + self.task?.cancel() + + if self.repeat { + let task = eventLoop.scheduleRepeatedTask(initialDelay: self.delay, delay: self.delay) { _ in + try work() + } + self.task = .repeated(task) + } else { + let task = eventLoop.scheduleTask(in: self.delay, work) + self.task = .once(task) + } + } + + /// Cancels the task, if one was scheduled. + mutating func cancel() { + self.task?.cancel() + self.task = nil + } +} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index ffdbb8946..459da88af 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -66,30 +66,6 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { /// `keepAliveTimer` is set. private var keepAliveTimeoutTimer: Timer - private struct Timer { - /// The delay to wait before running the task. - private let delay: TimeAmount - /// The task to run, if scheduled. - private var task: Scheduled? - - init(delay: TimeAmount) { - self.delay = delay - self.task = nil - } - - /// Schedule a task on the given `EventLoop`. - mutating func schedule(on eventLoop: EventLoop, task: @escaping () throws -> Void) { - self.task?.cancel() - self.task = eventLoop.scheduleTask(in: self.delay, task) - } - - /// Cancels the task, if one was scheduled. - mutating func cancel() { - self.task?.cancel() - self.task = nil - } - } - /// Opaque data sent in keep alive pings. private let keepAlivePingData: HTTP2PingData diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift new file mode 100644 index 000000000..206c317e4 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift @@ -0,0 +1,107 @@ +/* + * 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 NIOCore +import NIOEmbedded +import XCTest + +@testable import GRPCHTTP2Core + +final class ClientConnectionHandlerStateMachineTests: XCTestCase { + private func makeStateMachine( + keepAliveWithoutCalls: Bool = false + ) -> ClientConnectionHandler.StateMachine { + return ClientConnectionHandler.StateMachine(allowKeepAliveWithoutCalls: keepAliveWithoutCalls) + } + + func testCloseSomeStreamsWhenActive() { + var state = self.makeStateMachine() + state.streamOpened(1) + state.streamOpened(2) + XCTAssertEqual(state.streamClosed(2), .none) + XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepAlive: true)) + } + + func testCloseSomeStreamsWhenClosing() { + var state = self.makeStateMachine() + state.streamOpened(1) + state.streamOpened(2) + XCTAssertTrue(state.beginClosing()) + XCTAssertEqual(state.streamClosed(2), .none) + XCTAssertEqual(state.streamClosed(1), .close) + } + + func testOpenAndCloseStreamWhenClosed() { + var state = self.makeStateMachine() + state.closed() + state.streamOpened(1) + XCTAssertEqual(state.streamClosed(1), .none) + } + + func testSendKeepAlivePing() { + var state = self.makeStateMachine(keepAliveWithoutCalls: false) + // No streams open so ping isn't allowed. + XCTAssertFalse(state.sendKeepAlivePing()) + + // Stream open, ping allowed. + state.streamOpened(1) + XCTAssertTrue(state.sendKeepAlivePing()) + + // No stream, no ping. + XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepAlive: true)) + XCTAssertFalse(state.sendKeepAlivePing()) + } + + func testSendKeepAlivePingWhenAllowedWithoutCalls() { + var state = self.makeStateMachine(keepAliveWithoutCalls: true) + // Keep alive is allowed when no streams are open, so pings are allowed. + XCTAssertTrue(state.sendKeepAlivePing()) + + state.streamOpened(1) + XCTAssertTrue(state.sendKeepAlivePing()) + + XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepAlive: false)) + XCTAssertTrue(state.sendKeepAlivePing()) + } + + func testSendKeepAlivePingWhenClosing() { + var state = self.makeStateMachine(keepAliveWithoutCalls: false) + state.streamOpened(1) + XCTAssertTrue(state.beginClosing()) + + // Stream is opened and state is closing, ping is allowed. + XCTAssertTrue(state.sendKeepAlivePing()) + } + + func testSendKeepAlivePingWhenClosed() { + var state = self.makeStateMachine(keepAliveWithoutCalls: true) + state.closed() + XCTAssertFalse(state.sendKeepAlivePing()) + } + + func testBeginGracefulShutdownWhenStreamsAreOpen() { + var state = self.makeStateMachine() + state.streamOpened(1) + // Close is false as streams are still open. + XCTAssertEqual(state.beginGracefulShutdown(), .sendGoAway(false)) + } + + func testBeginGracefulShutdownWhenNoStreamsAreOpen() { + var state = self.makeStateMachine() + // Close immediately, not streams are open. + XCTAssertEqual(state.beginGracefulShutdown(), .sendGoAway(true)) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift new file mode 100644 index 000000000..882086751 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -0,0 +1,274 @@ +/* + * 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 NIOCore +import NIOEmbedded +import NIOHTTP2 +import XCTest + +@testable import GRPCHTTP2Core + +final class ClientConnectionHandlerTests: XCTestCase { + func testMaxIdleTime() throws { + let connection = try Connection(maxIdleTime: .minutes(5)) + try connection.activate() + + // Idle with no streams open we should: + // - read out a closing event, + // - write a GOAWAY frame, + // - close. + connection.loop.advanceTime(by: .minutes(5)) + + XCTAssertEqual(try connection.readEvent(), .closing(.idle)) + + let frame = try XCTUnwrap(try connection.readFrame()) + XCTAssertEqual(frame.streamID, .rootStream) + XCTAssertGoAway(frame.payload) { lastStreamID, error, data in + XCTAssertEqual(lastStreamID, .rootStream) + XCTAssertEqual(error, .noError) + XCTAssertEqual(data, ByteBuffer(string: "idle")) + } + + try connection.waitUntilClosed() + } + + func testMaxIdleTimeWhenOpenStreams() throws { + let connection = try Connection(maxIdleTime: .minutes(5)) + try connection.activate() + + // Open a stream, the idle timer should be cancelled. + connection.streamOpened(1) + + // Advance by the idle time, nothing should happen. + connection.loop.advanceTime(by: .minutes(5)) + XCTAssertNil(try connection.readEvent()) + XCTAssertNil(try connection.readFrame()) + + // Close the stream, the idle timer should begin again. + connection.streamClosed(1) + connection.loop.advanceTime(by: .minutes(5)) + let frame = try XCTUnwrap(try connection.readFrame()) + XCTAssertGoAway(frame.payload) { lastStreamID, error, data in + XCTAssertEqual(lastStreamID, .rootStream) + XCTAssertEqual(error, .noError) + XCTAssertEqual(data, ByteBuffer(string: "idle")) + } + + try connection.waitUntilClosed() + } + + func testKeepAliveWithOpenStreams() throws { + let connection = try Connection(keepAliveTime: .minutes(1), keepAliveTimeout: .seconds(10)) + try connection.activate() + + // Open a stream so keep-alive starts. + connection.streamOpened(1) + + for _ in 0 ..< 10 { + // Advance time, a PING should be sent, ACK it. + connection.loop.advanceTime(by: .minutes(1)) + let frame1 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame1.streamID, .rootStream) + try XCTAssertPing(frame1.payload) { data, ack in + XCTAssertFalse(ack) + try connection.ping(data: data, ack: true) + } + + XCTAssertNil(try connection.readFrame()) + } + + // Close the stream, keep-alive pings should stop. + connection.streamClosed(1) + connection.loop.advanceTime(by: .minutes(1)) + XCTAssertNil(try connection.readFrame()) + } + + func testKeepAliveWithNoOpenStreams() throws { + let connection = try Connection(keepAliveTime: .minutes(1), allowKeepAliveWithoutCalls: true) + try connection.activate() + + for _ in 0 ..< 10 { + // Advance time, a PING should be sent, ACK it. + connection.loop.advanceTime(by: .minutes(1)) + let frame1 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame1.streamID, .rootStream) + try XCTAssertPing(frame1.payload) { data, ack in + XCTAssertFalse(ack) + try connection.ping(data: data, ack: true) + } + + XCTAssertNil(try connection.readFrame()) + } + } + + func testKeepAliveWithOpenStreamsTimingOut() throws { + let connection = try Connection(keepAliveTime: .minutes(1), keepAliveTimeout: .seconds(10)) + try connection.activate() + + // Open a stream so keep-alive starts. + connection.streamOpened(1) + + // Advance time, a PING should be sent, don't ACK it. + connection.loop.advanceTime(by: .minutes(1)) + let frame1 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame1.streamID, .rootStream) + XCTAssertPing(frame1.payload) { _, ack in + XCTAssertFalse(ack) + } + + // Advance time by the keep alive timeout. We should: + // - read a connection event + // - read out a GOAWAY frame + // - be closed + connection.loop.advanceTime(by: .seconds(10)) + + XCTAssertEqual(try connection.readEvent(), .closing(.keepAliveExpired)) + + let frame2 = try XCTUnwrap(connection.readFrame()) + XCTAssertEqual(frame2.streamID, .rootStream) + XCTAssertGoAway(frame2.payload) { lastStreamID, error, data in + XCTAssertEqual(lastStreamID, .rootStream) + XCTAssertEqual(error, .noError) + XCTAssertEqual(data, ByteBuffer(string: "keepalive_expired")) + } + + // Doesn't wait for streams to close: the connection is bad. + try connection.waitUntilClosed() + } + + func testPingsAreIgnored() throws { + let connection = try Connection() + try connection.activate() + + // PING frames without ack set should be ignored, we rely on the HTTP/2 handler replying to them. + try connection.ping(data: HTTP2PingData(), ack: false) + XCTAssertNil(try connection.readFrame()) + } + + func testReceiveGoAway() throws { + let connection = try Connection() + try connection.activate() + + try connection.goAway( + lastStreamID: 0, + errorCode: .enhanceYourCalm, + opaqueData: ByteBuffer(string: "too_many_pings") + ) + + // Should read out an event and close (because there are no open streams). + XCTAssertEqual( + try connection.readEvent(), + .closing(.goAway(.enhanceYourCalm, "too_many_pings")) + ) + try connection.waitUntilClosed() + } + + func testReceiveGoAwayWithOpenStreams() throws { + let connection = try Connection() + try connection.activate() + + connection.streamOpened(1) + connection.streamOpened(2) + connection.streamOpened(3) + + try connection.goAway(lastStreamID: .maxID, errorCode: .noError) + + // Should read out an event. + XCTAssertEqual(try connection.readEvent(), .closing(.goAway(.noError, ""))) + + // Close streams so the connection can close. + connection.streamClosed(1) + connection.streamClosed(2) + connection.streamClosed(3) + try connection.waitUntilClosed() + } +} + +extension ClientConnectionHandlerTests { + struct Connection { + let channel: EmbeddedChannel + var loop: EmbeddedEventLoop { + self.channel.embeddedEventLoop + } + + init( + maxIdleTime: TimeAmount? = nil, + keepAliveTime: TimeAmount? = nil, + keepAliveTimeout: TimeAmount? = nil, + allowKeepAliveWithoutCalls: Bool = false + ) throws { + let loop = EmbeddedEventLoop() + let handler = ClientConnectionHandler( + eventLoop: loop, + maxIdleTime: maxIdleTime, + keepAliveTime: keepAliveTime, + keepAliveTimeout: keepAliveTimeout, + keepAliveWithoutCalls: allowKeepAliveWithoutCalls + ) + + self.channel = EmbeddedChannel(handler: handler, loop: loop) + } + + func activate() throws { + try self.channel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() + } + + func streamOpened(_ id: HTTP2StreamID) { + let event = NIOHTTP2StreamCreatedEvent( + streamID: id, + localInitialWindowSize: nil, + remoteInitialWindowSize: nil + ) + self.channel.pipeline.fireUserInboundEventTriggered(event) + } + + func streamClosed(_ id: HTTP2StreamID) { + let event = StreamClosedEvent(streamID: id, reason: nil) + self.channel.pipeline.fireUserInboundEventTriggered(event) + } + + func goAway( + lastStreamID: HTTP2StreamID, + errorCode: HTTP2ErrorCode, + opaqueData: ByteBuffer? = nil + ) throws { + let frame = HTTP2Frame( + streamID: .rootStream, + payload: .goAway(lastStreamID: lastStreamID, errorCode: errorCode, opaqueData: opaqueData) + ) + + try self.channel.writeInbound(frame) + } + + func ping(data: HTTP2PingData, ack: Bool) throws { + let frame = HTTP2Frame(streamID: .rootStream, payload: .ping(data, ack: ack)) + try self.channel.writeInbound(frame) + } + + func readFrame() throws -> HTTP2Frame? { + return try self.channel.readOutbound(as: HTTP2Frame.self) + } + + func readEvent() throws -> ClientConnectionEvent? { + return try self.channel.readInbound(as: ClientConnectionEvent.self) + } + + func waitUntilClosed() throws { + self.channel.embeddedEventLoop.run() + try self.channel.closeFuture.wait() + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift new file mode 100644 index 000000000..fadfe4fd6 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift @@ -0,0 +1,99 @@ +/* + * 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 Atomics +import NIOEmbedded +import XCTest + +@testable import GRPCHTTP2Core + +internal final class TimerTests: XCTestCase { + func testScheduleOneOffTimer() { + let loop = EmbeddedEventLoop() + defer { try! loop.close() } + + var value = 0 + + var timer = Timer(delay: .seconds(1), repeat: false) + timer.schedule(on: loop) { + XCTAssertEqual(value, 0) + value += 1 + } + + loop.advanceTime(by: .milliseconds(999)) + XCTAssertEqual(value, 0) + loop.advanceTime(by: .milliseconds(1)) + XCTAssertEqual(value, 1) + + // Run again to make sure the task wasn't repeated. + loop.advanceTime(by: .seconds(1)) + XCTAssertEqual(value, 1) + } + + func testCancelOneOffTimer() { + let loop = EmbeddedEventLoop() + defer { try! loop.close() } + + var timer = Timer(delay: .seconds(1), repeat: false) + timer.schedule(on: loop) { + XCTFail("Timer wasn't cancelled") + } + + loop.advanceTime(by: .milliseconds(999)) + timer.cancel() + loop.advanceTime(by: .milliseconds(1)) + } + + func testScheduleRepeatedTimer() throws { + let loop = EmbeddedEventLoop() + defer { try! loop.close() } + + var values = [Int]() + + var timer = Timer(delay: .seconds(1), repeat: true) + timer.schedule(on: loop) { + values.append(values.count) + } + + loop.advanceTime(by: .milliseconds(999)) + XCTAssertEqual(values, []) + loop.advanceTime(by: .milliseconds(1)) + XCTAssertEqual(values, [0]) + + loop.advanceTime(by: .seconds(1)) + XCTAssertEqual(values, [0, 1]) + loop.advanceTime(by: .seconds(1)) + XCTAssertEqual(values, [0, 1, 2]) + + timer.cancel() + loop.advanceTime(by: .seconds(1)) + XCTAssertEqual(values, [0, 1, 2]) + } + + func testCancelRepeatedTimer() { + let loop = EmbeddedEventLoop() + defer { try! loop.close() } + + var timer = Timer(delay: .seconds(1), repeat: true) + timer.schedule(on: loop) { + XCTFail("Timer wasn't cancelled") + } + + loop.advanceTime(by: .milliseconds(999)) + timer.cancel() + loop.advanceTime(by: .milliseconds(1)) + } +} From 7bb25061d3cb8161f663991338327a34e1cc9754 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 29 Jan 2024 13:34:40 +0000 Subject: [PATCH 222/580] Use configured connect timeout when retries is none (#1777) Motivation: The connection manager uses an iterator to get values for the connect timeout and the backoff to use if the connection attempt fails. Backoff can also be configured to have a limited number of tries. This controls when the iterator returns `nil`. If the connection attempt retries are disabled then the iterator always returns `nil`. This is an issue because the connection manager doesn't respect the configured connect timeout as the iterator doesn't return a value. Modifications: Check when starting a connection attempt from the idle state whether there a connect timeout is configured if there are no retries. This Result: The connect timeout value is repected if the connection retries aren't enabled. --- Sources/GRPC/ConnectionManager.swift | 27 ++++++++++-- Tests/GRPCTests/ConnectionManagerTests.swift | 45 ++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index 0c92c7ff2..a78a57a7f 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -1011,9 +1011,22 @@ extension ConnectionManager { switch self.state { case .idle: let iterator = self.connectionBackoff?.makeIterator() + + // The iterator produces the connect timeout and the backoff to use for the next attempt. This + // is unfortunate if retries is set to none because we need to connect timeout but not the + // backoff yet the iterator will not return a value to us. To workaround this we grab the + // connect timeout and override it. + let connectTimeoutOverride: TimeAmount? + if let backoff = self.connectionBackoff, backoff.retries == .none { + connectTimeoutOverride = .seconds(timeInterval: backoff.minimumConnectionTimeout) + } else { + connectTimeoutOverride = nil + } + self.startConnecting( backoffIterator: iterator, - muxPromise: self.eventLoop.makePromise() + muxPromise: self.eventLoop.makePromise(), + connectTimeoutOverride: connectTimeoutOverride ) case let .transientFailure(pending): @@ -1039,7 +1052,8 @@ extension ConnectionManager { private func startConnecting( backoffIterator: ConnectionBackoffIterator?, - muxPromise: EventLoopPromise + muxPromise: EventLoopPromise, + connectTimeoutOverride: TimeAmount? = nil ) { let timeoutAndBackoff = backoffIterator?.next() @@ -1048,10 +1062,17 @@ extension ConnectionManager { self.eventLoop.assertInEventLoop() let candidate: EventLoopFuture = self.eventLoop.flatSubmit { + let connectTimeout: TimeAmount? + if let connectTimeoutOverride = connectTimeoutOverride { + connectTimeout = connectTimeoutOverride + } else { + connectTimeout = timeoutAndBackoff.map { TimeAmount.seconds(timeInterval: $0.timeout) } + } + let channel: EventLoopFuture = self.channelProvider.makeChannel( managedBy: self, onEventLoop: self.eventLoop, - connectTimeout: timeoutAndBackoff.map { .seconds(timeInterval: $0.timeout) }, + connectTimeout: connectTimeout, logger: self.logger ) diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 9fb0722af..5bd1ad432 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -1357,6 +1357,51 @@ extension ConnectionManagerTests { XCTAssertGreaterThan(keepalive.interval, .seconds(1)) XCTAssertLessThanOrEqual(keepalive.interval, .seconds(4)) } + + func testConnectTimeoutIsRespectedWithNoRetries() { + // Setup a factory which makes channels. We'll use this as the point to check that the + // connect timeout is as expected. + struct Provider: ConnectionManagerChannelProvider { + func makeChannel( + managedBy connectionManager: ConnectionManager, + onEventLoop eventLoop: any EventLoop, + connectTimeout: TimeAmount?, + logger: Logger + ) -> EventLoopFuture { + XCTAssertEqual(connectTimeout, .seconds(314_159_265)) + return eventLoop.makeFailedFuture(DoomedChannelError()) + } + } + + var configuration = self.defaultConfiguration + configuration.connectionBackoff = ConnectionBackoff( + minimumConnectionTimeout: 314_159_265, + retries: .none + ) + + let manager = ConnectionManager( + configuration: configuration, + channelProvider: Provider(), + connectivityDelegate: self.monitor, + logger: self.logger + ) + + // Setup the state change expectations and trigger them by asking for the multiplexer. + // We expect connecting to shutdown as no connect retries are configured and the factory + // always returns errors. + let multiplexer = self.waitForStateChanges([ + Change(from: .idle, to: .connecting), + Change(from: .connecting, to: .shutdown), + ]) { + let multiplexer = manager.getHTTP2Multiplexer() + self.loop.run() + return multiplexer + } + + XCTAssertThrowsError(try multiplexer.wait()) { error in + XCTAssert(error is DoomedChannelError) + } + } } internal struct Change: Hashable, CustomStringConvertible { From f384235e6916d92644b9d84df1c2fecea5223a9c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 29 Jan 2024 17:28:47 +0000 Subject: [PATCH 223/580] Make testRetriesEventuallySucceed more reliable (#1779) Motivation: `testRetriesEventuallySucceed` fails infrequently. When attempting a retry, the executor checks whether it's safe for the broadcast sequence to be replayed, it does so after removing all subscribers. However, there is a timing window between invalidating subscribers and checking whether it's safe in which a subscribtion can be registered. For retries, this can happen if the server responds before the client has subscribed. In practice this is very unlikely to happen because of network latency. Modifications: Have the server in the test consume the inbound stream before failing, this ensures the subscribtion is created by the time the client realises the call has failed. Result: Test passes more reliably --- .../Client/Internal/ClientRPCExecutorTests+Retries.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index 335044e26..e20f48352 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -38,7 +38,11 @@ extension ClientRPCExecutorTests { } func testRetriesEventuallySucceed() async throws { - let harness = self.makeHarnessForRetries(rejectUntilAttempt: 3, withCode: .unavailable) + let harness = self.makeHarnessForRetries( + rejectUntilAttempt: 3, + withCode: .unavailable, + consumeInboundStream: true + ) try await harness.bidirectional( request: ClientRequest.Stream(metadata: ["foo": "bar"]) { try await $0.write([0]) From 203454343112b2e494b96ff60e17673a135a0cf8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 29 Jan 2024 17:42:09 +0000 Subject: [PATCH 224/580] Make testHedgingWhenAllAttemptsResultInNonFatalCodes more reliable (#1780) Motivation: `testHedgingWhenAllAttemptsResultInNonFatalCodes` fails occasionally. The reason for this is that when the event stating that next scheduled attempt is cancelled is consumed it cancels the next scheduled attempt again. However, this is incorrect, another hedged result may have re-scheduled an attempt which is subsequently cancelled by accident. Modifications: Don't double cancel the next scheduled attempt. result: The test passes more reliably --- .../Client/Internal/ClientRPCExecutor+HedgingExecutor.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index aba9d9423..676dc8d37 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -230,8 +230,7 @@ extension ClientRPCExecutor.HedgingExecutor { } case .cancelled: - // Cancelling also resets the state. - nextScheduledAttempt.cancel() + () } case .attemptCompleted(let outcome): From 2c27ad5123ce2c048eefbecc76a672129b2a4358 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 30 Jan 2024 10:48:05 +0000 Subject: [PATCH 225/580] Make testRetriesCantBeExecutedForTooManyRequestMessages more reliable (#1781) Motivation: The 'testRetriesCantBeExecutedForTooManyRequestMessages' test wedges infrequently. After some diagnosis this appears to be due to the intersection of two bugs. The first is that when yielding an element to the sequence, if some consumers exist, of which a subset are waiting for the next element and the remainder are not 'slow', the continuations of the fastest consumers would not be resumed when a new element was added. The other bug is a timing issue: when waiting for the next element a subscriber may be told to suspend. However, the subscriber must drop and then reacquire the lock to store the continuation. The state wasn't re-checked on storing the continuation which opened up a window where the element may have become present between asking for it and storing the continuation. Modifications: - Don't release the lock between attempting to consume an element and suspending to wait for it. - Resume waiting consumers more frequently. - Make a few tests less flaky Result: Test didn't wedge in 10k iterations --- .../Concurrency Primitives/Lock.swift | 34 ++++++++++++ .../Internal/BroadcastAsyncSequence.swift | 55 ++++++++++++------- 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift index 0cadb250e..5ee259adc 100644 --- a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift +++ b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift @@ -253,6 +253,40 @@ public struct _LockedValueBox { public func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { return try self.storage.withLockedValue(mutate) } + + /// An unsafe view over the locked value box. + /// + /// Prefer ``withLockedValue(_:)`` where possible. + public var unsafe: Unsafe { + Unsafe(storage: self.storage) + } + + public struct Unsafe { + @usableFromInline + let storage: LockStorage + + /// Manually acquire the lock. + @inlinable + public func lock() { + self.storage.lock() + } + + /// Manually release the lock. + @inlinable + public func unlock() { + self.storage.unlock() + } + + /// Mutate the value, assuming the lock has been acquired manually. + @inlinable + public func withValueAssumingLockIsAcquired( + _ mutate: (inout Value) throws -> T + ) rethrows -> T { + return try self.storage.withUnsafeMutablePointerToHeader { value in + try mutate(&value.pointee) + } + } + } } extension _LockedValueBox: Sendable where Value: Sendable {} diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift index 738cfb86d..a56380066 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -261,20 +261,26 @@ final class _BroadcastSequenceStorage: Sendable { func nextElement( forSubscriber id: _BroadcastSequenceStateMachine.Subscriptions.ID ) async throws -> Element? { - let onNext = self._state.withLockedValue { $0.nextElement(forSubscriber: id) } + return try await withTaskCancellationHandler { + self._state.unsafe.lock() + let onNext = self._state.unsafe.withValueAssumingLockIsAcquired { + $0.nextElement(forSubscriber: id) + } - switch onNext { - case .return(let returnAndProduceMore): - returnAndProduceMore.producers.resume() - return try returnAndProduceMore.nextResult.get() + switch onNext { + case .return(let returnAndProduceMore): + self._state.unsafe.unlock() + returnAndProduceMore.producers.resume() + return try returnAndProduceMore.nextResult.get() - case .suspend: - return try await withTaskCancellationHandler { + case .suspend: return try await withCheckedThrowingContinuation { continuation in - let onSetContinuation = self._state.withLockedValue { state in + let onSetContinuation = self._state.unsafe.withValueAssumingLockIsAcquired { state in state.setContinuation(continuation, forSubscription: id) } + self._state.unsafe.unlock() + switch onSetContinuation { case .resume(let continuation, let result): continuation.resume(with: result) @@ -282,17 +288,17 @@ final class _BroadcastSequenceStorage: Sendable { () } } - } onCancel: { - let onCancel = self._state.withLockedValue { state in - state.cancelSubscription(withID: id) - } + } + } onCancel: { + let onCancel = self._state.withLockedValue { state in + state.cancelSubscription(withID: id) + } - switch onCancel { - case .resume(let continuation, let result): - continuation.resume(with: result) - case .none: - () - } + switch onCancel { + case .resume(let continuation, let result): + continuation.resume(with: result) + case .none: + () } } } @@ -572,9 +578,18 @@ struct _BroadcastSequenceStateMachine: Sendable { self.producerToken += 1 onYield = .suspend(token) } else { - // No consumers are slow. Remove the oldest value. + // No consumers are slow, some subscribers exist, a subset of which are waiting + // for a value. Drop the oldest value and resume the fastest consumers. self.elements.removeFirst() - onYield = .none + let continuations = self.subscriptions.takeContinuations().map { + ConsumerContinuations(continuations: $0, result: .success(element)) + } + + if let continuations = continuations { + onYield = .resume(continuations) + } else { + onYield = .none + } } case self.subscriptions.count: From 8ac347070236ee90f272ecc909d970bc9c4c471c Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 31 Jan 2024 13:45:23 +0000 Subject: [PATCH 226/580] Update renderer-blank line after imports, delete blank line from docs (#1782) Motivation: We want to have a blank line between the imports and the generated code and no blank line between the docs and the protocols/methods declarations. Modifications: Updated the TextBasedRenderer and the tests. Result: The generated code will have the right format. --- .../Internal/Renderer/TextBasedRenderer.swift | 31 ++++--- .../IDLToStructuredSwiftTranslator.swift | 2 +- .../Translator/TypealiasTranslator.swift | 2 +- .../Renderer/TextBasedRendererTests.swift | 20 +++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 81 ++++++++++++++++++- .../Internal/Translator/TestFunctions.swift | 24 +----- 6 files changed, 124 insertions(+), 36 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 3fd683720..3c9b1845c 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -136,8 +136,14 @@ struct TextBasedRenderer: RendererProtocol { /// Renders the specified Swift file. func renderFile(_ description: FileDescription) { - if let topComment = description.topComment { renderComment(topComment) } - if let imports = description.imports { renderImports(imports) } + if let topComment = description.topComment { + renderComment(topComment) + writer.writeLine("") + } + if let imports = description.imports { + renderImports(imports) + writer.writeLine("") + } for codeBlock in description.codeBlocks { renderCodeBlock(codeBlock) writer.writeLine("") @@ -165,15 +171,18 @@ struct TextBasedRenderer: RendererProtocol { prefix = "" commentString = string } - if prefix.isEmpty { - writer.writeLine(commentString) - } else { - let lines = commentString.transformingLines { line in - if line.isEmpty { return prefix } - return "\(prefix) \(line)" + + let lines = commentString.transformingLines { line, isLast in + // The last line of a comment that is blank should be dropped. + // Pre formatted documentation might contain such lines. + if line.isEmpty && prefix.isEmpty && isLast { + return nil + } else { + let formattedPrefix = !prefix.isEmpty && !line.isEmpty ? "\(prefix) " : prefix + return "\(formattedPrefix)\(line)" } - lines.forEach(writer.writeLine) } + lines.forEach(writer.writeLine) } /// Renders the specified import statements. @@ -1095,7 +1104,9 @@ extension String { /// The closure takes a string representing one line as a parameter. /// - Parameter work: The closure that transforms each line. /// - Returns: A new string where each line has been transformed using the given closure. - fileprivate func transformingLines(_ work: (String) -> String) -> [String] { asLines().map(work) } + fileprivate func transformingLines(_ work: (String, Bool) -> String?) -> [String] { + asLines().enumeratedWithLastMarker().compactMap(work) + } } extension TextBasedRenderer { diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index b35a85d1b..e195b45b4 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -38,7 +38,7 @@ struct IDLToStructuredSwiftTranslator: Translator { try partialResult.append(translateImport(dependency: newDependency)) } - var codeBlocks: [CodeBlock] = [] + var codeBlocks = [CodeBlock]() codeBlocks.append( contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 308aa83a9..49a92fa33 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -64,7 +64,7 @@ struct TypealiasTranslator: SpecializedTranslator { } func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { - var codeBlocks: [CodeBlock] = [] + var codeBlocks = [CodeBlock]() let services = codeGenerationRequest.services let servicesByNamespace = Dictionary( grouping: services, diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index bae3b2632..6d40aa56e 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -89,6 +89,22 @@ final class Test_TextBasedRenderer: XCTestCase { // Also, bar """# ) + try _test( + .preFormatted("/// Lorem ipsum\n"), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: """ + /// Lorem ipsum + """ + ) + try _test( + .preFormatted("/// Lorem ipsum\n\n/// Lorem ipsum\n"), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: """ + /// Lorem ipsum + + /// Lorem ipsum + """ + ) } func testImports() throws { @@ -825,7 +841,9 @@ final class Test_TextBasedRenderer: XCTestCase { renderedBy: TextBasedRenderer.renderFile, rendersAs: #""" // hi + import Foo + struct Bar {} """# @@ -847,7 +865,9 @@ final class Test_TextBasedRenderer: XCTestCase { renderedBy: TextBasedRenderer.renderFile, rendersAs: #""" // hi + import Foo + struct Bar { struct Baz {} } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 3dfec1358..58a10cb3d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -56,6 +56,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Some really exciting license header 2023. + import GRPCCore import Foo import typealias Foo.Bar @@ -66,6 +67,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { import let Foo.Baq import var Foo.Bag import func Foo.Bak + """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), @@ -93,6 +95,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Some really exciting license header 2023. + import GRPCCore @preconcurrency import Foo @preconcurrency import enum Foo.Bar @@ -101,6 +104,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { #else import Baz #endif + """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), @@ -123,9 +127,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Some really exciting license header 2023. + import GRPCCore @_spi(Secret) import Foo @_spi(Secret) import enum Foo.Bar + """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), @@ -134,17 +140,84 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) } + func testGeneration() throws { + var dependencies = [CodeGenerationRequest.Dependency]() + dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret")) + dependencies.append( + CodeGenerationRequest.Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + spi: "Secret" + ) + ) + + let serviceA = ServiceDescriptor( + documentation: "/// Documentation for AService\n", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), + methods: [] + ) + + let expectedSwift = + """ + /// Some really exciting license header 2023. + + import GRPCCore + @_spi(Secret) import Foo + @_spi(Secret) import enum Foo.Bar + + public enum NamespaceA { + public enum ServiceA { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + } + } + + /// Documentation for AService + public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + + /// Conformance to `GRPCCore.RegistrableRPCService`. + extension NamespaceA.ServiceA.StreamingServiceProtocol { + public func registerMethods(with router: inout GRPCCore.RPCRouter) {} + } + + /// Documentation for AService + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {} + + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { + } + + """ + try self.assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: makeCodeGenerationRequest( + services: [serviceA], + dependencies: dependencies + ), + expectedSwift: expectedSwift, + accessLevel: .public, + server: true + ) + } + private func assertIDLToStructuredSwiftTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, - accessLevel: SourceGenerator.Configuration.AccessLevel + accessLevel: SourceGenerator.Configuration.AccessLevel, + server: Bool = false ) throws { let translator = IDLToStructuredSwiftTranslator() let structuredSwift = try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: accessLevel, client: false, - server: false + server: server ) let renderer = TextBasedRenderer.default let sourceFile = try renderer.render(structured: structuredSwift) @@ -262,7 +335,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameGeneratedNameServicesSameNamespaceError() throws { let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", + documentation: "/// Documentation for AService\n", name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), namespace: Name( base: "namespacea", @@ -272,7 +345,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { methods: [] ) let serviceB = ServiceDescriptor( - documentation: "Documentation for BService", + documentation: "/// Documentation for BService\n", name: Name(base: "BService", generatedUpperCase: "AService", generatedLowerCase: "aService"), namespace: Name( base: "namespacea", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 800ff7393..fd5808816 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -72,30 +72,14 @@ internal func XCTAssertEqualWithDiff( } internal func makeCodeGenerationRequest( - services: [CodeGenerationRequest.ServiceDescriptor] + services: [CodeGenerationRequest.ServiceDescriptor] = [], + dependencies: [CodeGenerationRequest.Dependency] = [] ) -> CodeGenerationRequest { return CodeGenerationRequest( fileName: "test.grpc", - leadingTrivia: "/// Some really exciting license header 2023.", - dependencies: [], - services: services, - lookupSerializer: { - "ProtobufSerializer<\($0)>()" - }, - lookupDeserializer: { - "ProtobufDeserializer<\($0)>()" - } - ) -} - -internal func makeCodeGenerationRequest( - dependencies: [CodeGenerationRequest.Dependency] -) -> CodeGenerationRequest { - return CodeGenerationRequest( - fileName: "test.grpc", - leadingTrivia: "/// Some really exciting license header 2023.", + leadingTrivia: "/// Some really exciting license header 2023.\n", dependencies: dependencies, - services: [], + services: services, lookupSerializer: { "ProtobufSerializer<\($0)>()" }, From ff707e0245eff24f3ebaab9f57921092a11a6845 Mon Sep 17 00:00:00 2001 From: Ivan Glushkov Date: Wed, 31 Jan 2024 14:26:36 +0000 Subject: [PATCH 227/580] Send integer values properly (#1783) --- .../GRPC/ConnectionPool/ConnectionPool.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index fcce78ad9..b1ae58fd3 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -351,7 +351,7 @@ internal final class ConnectionPool { logger.trace( "connection pool has too many waiters", metadata: [ - Metadata.waitersMax: "\(self.maxWaiters)" + Metadata.waitersMax: .stringConvertible(self.maxWaiters) ] ) promise.fail(GRPCConnectionPoolError.tooManyWaiters(connectionError: self._mostRecentError)) @@ -373,7 +373,7 @@ internal final class ConnectionPool { "timed out waiting for a connection", metadata: [ Metadata.waiterID: "\(waiter.id)", - Metadata.waitersCount: "\(self.waiters.count)", + Metadata.waitersCount: .stringConvertible(self.waiters.count), ] ) } @@ -384,7 +384,7 @@ internal final class ConnectionPool { "waiting for a connection to become available", metadata: [ Metadata.waiterID: "\(waiter.id)", - Metadata.waitersCount: "\(self.waiters.count)", + Metadata.waitersCount: .stringConvertible(self.waiters.count), ] ) @@ -394,7 +394,7 @@ internal final class ConnectionPool { self.logger.trace( "enqueued connection waiter", metadata: [ - Metadata.waitersCount: "\(self.waiters.count)" + Metadata.waitersCount: .stringConvertible(self.waiters.count) ] ) @@ -445,10 +445,10 @@ internal final class ConnectionPool { self.logger.debug( "stream reservation load factor greater than or equal to threshold, bringing up additional connection if available", metadata: [ - Metadata.reservationsCount: "\(demand)", - Metadata.reservationsCapacity: "\(capacity)", - Metadata.reservationsLoad: "\(load)", - Metadata.reservationsLoadThreshold: "\(self.reservationLoadThreshold)", + Metadata.reservationsCount: .stringConvertible(demand), + Metadata.reservationsCapacity: .stringConvertible(capacity), + Metadata.reservationsLoad: .stringConvertible(load), + Metadata.reservationsLoadThreshold: .stringConvertible(self.reservationLoadThreshold), ] ) } @@ -783,7 +783,7 @@ extension ConnectionPool { self.logger.trace( "servicing waiters", metadata: [ - Metadata.waitersCount: "\(self.waiters.count)" + Metadata.waitersCount: .stringConvertible(self.waiters.count) ] ) @@ -812,8 +812,8 @@ extension ConnectionPool { self.logger.trace( "done servicing waiters", metadata: [ - Metadata.waitersCount: "\(self.waiters.count)", - Metadata.waitersServiced: "\(serviced)", + Metadata.waitersCount: .stringConvertible(self.waiters.count), + Metadata.waitersServiced: .stringConvertible(serviced), ] ) } From 53396af6fc374c75bb4202ce2d3571ed140ea500 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 1 Feb 2024 11:54:50 +0000 Subject: [PATCH 228/580] [CodeGen Protobuf support] protoc-gen-grpc-swift v2 (#1778) Motivation: We want to have a protoc plugin for grpc-swift v2 which builds on top of the code generator pipeline. Modifications: - Created the ProtobufCodeGenerator struct that encapsulates all setps needed to generate code for a given file descriptor - Created tests for the ProtobufCodeGenerator - added a new option for selecting v2 for the plugin - modified main() accordingly Result: The protoc plugin for grpc-swift will support v2 and will use the CodeGen library. --- Package.swift | 3 +- Sources/GRPCCodeGen/SourceGenerator.swift | 7 + .../ProtobufCodeGenerator.swift | 35 ++ Sources/protoc-gen-grpc-swift/main.swift | 32 +- Sources/protoc-gen-grpc-swift/options.swift | 8 + .../ProtobufCodeGenParserTests.swift | 18 +- .../ProtobufCodeGeneratorTests.swift | 422 ++++++++++++++++++ 7 files changed, 517 insertions(+), 8 deletions(-) create mode 100644 Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift create mode 100644 Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift diff --git a/Package.swift b/Package.swift index d0db22a72..d8975bc11 100644 --- a/Package.swift +++ b/Package.swift @@ -236,7 +236,8 @@ extension Target { dependencies: [ .protobuf, .protobufPluginLibrary, - .grpcCodeGen + .grpcCodeGen, + .grpcProtobufCodeGen ], exclude: [ "README.md", diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift index 98c934e5b..55b1fc54e 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -35,6 +35,13 @@ public struct SourceGenerator: Sendable { /// Whether or not server code should be generated. public var server: Bool + public init(accessLevel: AccessLevel, client: Bool, server: Bool, indentation: Int = 4) { + self.accessLevel = accessLevel + self.indentation = indentation + self.client = client + self.server = server + } + /// The possible access levels for the generated code. public struct AccessLevel: Sendable, Hashable { internal var level: Level diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift new file mode 100644 index 000000000..caa198d7a --- /dev/null +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -0,0 +1,35 @@ +/* + * 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 GRPCCodeGen +import SwiftProtobufPluginLibrary + +public struct ProtobufCodeGenerator { + internal var configuration: SourceGenerator.Configuration + + public init(configuration: SourceGenerator.Configuration) { + self.configuration = configuration + } + + public func generateCode(from fileDescriptor: FileDescriptor) throws -> String { + let parser = ProtobufCodeGenParser() + let sourceGenerator = SourceGenerator(configuration: self.configuration) + + let codeGenerationRequest = try parser.parse(input: fileDescriptor) + let sourceFile = try sourceGenerator.generate(codeGenerationRequest) + return sourceFile.contents + } +} diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 05d7c3ecb..12002c117 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -14,6 +14,8 @@ * limitations under the License. */ import Foundation +import GRPCCodeGen +import GRPCProtobufCodeGen import SwiftProtobuf import SwiftProtobufPluginLibrary @@ -166,9 +168,16 @@ func main(args: [String]) throws { fileNamingOption: options.fileNaming, generatedFiles: &generatedFiles ) - let grpcGenerator = Generator(fileDescriptor, options: options) + if options.v2 { + let grpcGenerator = ProtobufCodeGenerator( + configuration: SourceGenerator.Configuration(options: options) + ) + grpcFile.content = try grpcGenerator.generateCode(from: fileDescriptor) + } else { + let grpcGenerator = Generator(fileDescriptor, options: options) + grpcFile.content = grpcGenerator.code + } grpcFile.name = grpcFileName - grpcFile.content = grpcGenerator.code response.file.append(grpcFile) } } @@ -184,3 +193,22 @@ do { } catch { Log("ERROR: \(error)") } + +extension SourceGenerator.Configuration { + init(options: GeneratorOptions) { + let accessLevel: SourceGenerator.Configuration.AccessLevel + switch options.visibility { + case .internal: + accessLevel = .internal + case .package: + accessLevel = .package + case .public: + accessLevel = .public + } + self.init( + accessLevel: accessLevel, + client: options.generateClient, + server: options.generateServer + ) + } +} diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/options.swift index 155dd7377..e2a9b27dd 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/options.swift @@ -68,6 +68,7 @@ final class GeneratorOptions { private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" private(set) var generateReflectionData = false + private(set) var v2 = false init(parameter: String?) throws { for pair in GeneratorOptions.parseParameter(string: parameter) { @@ -154,6 +155,13 @@ final class GeneratorOptions { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } + case "_V2": + if let value = Bool(pair.value) { + self.v2 = value + } else { + throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) + } + default: throw GenerationError.unknownParameter(name: pair.key) } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 664501d82..25ba47dd9 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -23,8 +23,16 @@ import XCTest final class ProtobufCodeGenParserTests: XCTestCase { func testParser() throws { + let descriptorSet = DescriptorSet(protos: [Google_Protobuf_FileDescriptorProto.helloWorld]) + guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { + return XCTFail( + """ + Could not find the file descriptor of "helloworld.proto". + """ + ) + } let parsedCodeGenRequest = try ProtobufCodeGenParser().parse( - input: self.helloWorldFileDescriptor + input: fileDescriptor ) XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") XCTAssertEqual( @@ -105,8 +113,10 @@ final class ProtobufCodeGenParserTests: XCTestCase { CodeGenerationRequest.Dependency(module: "GRPCProtobuf") ) } +} - var helloWorldFileDescriptor: FileDescriptor { +extension Google_Protobuf_FileDescriptorProto { + static var helloWorld: Google_Protobuf_FileDescriptorProto { let requestType = Google_Protobuf_DescriptorProto.with { $0.name = "HelloRequest" $0.field = [ @@ -144,7 +154,7 @@ final class ProtobufCodeGenParserTests: XCTestCase { } ] } - let protoDescriptor = Google_Protobuf_FileDescriptorProto.with { + return Google_Protobuf_FileDescriptorProto.with { $0.name = "helloworld.proto" $0.package = "helloworld" $0.messageType = [requestType, responseType] @@ -187,7 +197,5 @@ final class ProtobufCodeGenParserTests: XCTestCase { } $0.syntax = "proto3" } - let descriptorSet = DescriptorSet(protos: [protoDescriptor]) - return descriptorSet.fileDescriptor(named: "helloworld.proto")! } } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift new file mode 100644 index 000000000..88c113106 --- /dev/null +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -0,0 +1,422 @@ +/* + * 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. + */ + +#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) + +import GRPCCodeGen +import GRPCProtobufCodeGen +import SwiftProtobuf +import SwiftProtobufPluginLibrary +import XCTest + +final class ProtobufCodeGeneratorTests: XCTestCase { + func testProtobufCodeGenerator() throws { + try testCodeGeneration( + indentation: 4, + visibility: .internal, + client: true, + server: false, + expectedCode: """ + // Copyright 2015 gRPC authors. + // + // 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. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: helloworld.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + import GRPCCore + import GRPCProtobuf + + internal enum Helloworld { + internal enum Greeter { + internal enum Methods { + internal enum SayHello { + internal typealias Input = HelloRequest + internal typealias Output = HelloReply + internal static let descriptor = MethodDescriptor( + service: "helloworld.Greeter", + method: "SayHello" + ) + } + } + internal static let methods: [MethodDescriptor] = [ + Methods.SayHello.descriptor + ] + internal typealias ClientProtocol = Helloworld_GreeterClientProtocol + internal typealias Client = Helloworld_GreeterClient + } + } + + /// The greeting service definition. + internal protocol Helloworld_GreeterClientProtocol: Sendable { + /// Sends a greeting. + func sayHello( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + } + + extension Helloworld.Greeter.ClientProtocol { + internal func sayHello( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.sayHello( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + + /// The greeting service definition. + internal struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { + private let client: GRPCCore.GRPCClient + internal init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Sends a greeting. + internal func sayHello( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Helloworld.Greeter.Methods.SayHello.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + + """ + ) + + try testCodeGeneration( + indentation: 2, + visibility: .public, + client: false, + server: true, + expectedCode: """ + // Copyright 2015 gRPC authors. + // + // 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. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: helloworld.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + import GRPCCore + import GRPCProtobuf + + public enum Helloworld { + public enum Greeter { + public enum Methods { + public enum SayHello { + public typealias Input = HelloRequest + public typealias Output = HelloReply + public static let descriptor = MethodDescriptor( + service: "helloworld.Greeter", + method: "SayHello" + ) + } + } + public static let methods: [MethodDescriptor] = [ + Methods.SayHello.descriptor + ] + public typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol + public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + } + } + + /// The greeting service definition. + public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Sends a greeting. + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + + /// Conformance to `GRPCCore.RegistrableRPCService`. + extension Helloworld.Greeter.StreamingServiceProtocol { + public func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: Helloworld.Greeter.Methods.SayHello.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.sayHello(request: request) + } + ) + } + } + + /// The greeting service definition. + public protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { + /// Sends a greeting. + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + } + + /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. + extension Helloworld.Greeter.ServiceProtocol { + public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + } + + """ + ) + try testCodeGeneration( + indentation: 2, + visibility: .package, + client: true, + server: true, + expectedCode: """ + // Copyright 2015 gRPC authors. + // + // 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. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: helloworld.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + import GRPCCore + import GRPCProtobuf + + package enum Helloworld { + package enum Greeter { + package enum Methods { + package enum SayHello { + package typealias Input = HelloRequest + package typealias Output = HelloReply + package static let descriptor = MethodDescriptor( + service: "helloworld.Greeter", + method: "SayHello" + ) + } + } + package static let methods: [MethodDescriptor] = [ + Methods.SayHello.descriptor + ] + package typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol + package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + package typealias ClientProtocol = Helloworld_GreeterClientProtocol + package typealias Client = Helloworld_GreeterClient + } + } + + /// The greeting service definition. + package protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Sends a greeting. + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + } + + /// Conformance to `GRPCCore.RegistrableRPCService`. + extension Helloworld.Greeter.StreamingServiceProtocol { + package func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + for: Helloworld.Greeter.Methods.SayHello.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.sayHello(request: request) + } + ) + } + } + + /// The greeting service definition. + package protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { + /// Sends a greeting. + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + } + + /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. + extension Helloworld.Greeter.ServiceProtocol { + package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + } + + /// The greeting service definition. + package protocol Helloworld_GreeterClientProtocol: Sendable { + /// Sends a greeting. + func sayHello( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + } + + extension Helloworld.Greeter.ClientProtocol { + package func sayHello( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.sayHello( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + } + + /// The greeting service definition. + package struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { + private let client: GRPCCore.GRPCClient + package init(client: GRPCCore.GRPCClient) { + self.client = client + } + /// Sends a greeting. + package func sayHello( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Helloworld.Greeter.Methods.SayHello.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + } + + """ + ) + } + + func testCodeGeneration( + indentation: Int, + visibility: SourceGenerator.Configuration.AccessLevel, + client: Bool, + server: Bool, + expectedCode: String + ) throws { + let configs = SourceGenerator.Configuration( + accessLevel: visibility, + client: client, + server: server, + indentation: indentation + ) + let descriptorSet = DescriptorSet(protos: [Google_Protobuf_FileDescriptorProto.helloWorld]) + guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { + return XCTFail( + """ + Could not find the file descriptor of "helloworld.proto". + """ + ) + } + let generator = ProtobufCodeGenerator(configuration: configs) + try XCTAssertEqualWithDiff(try generator.generateCode(from: fileDescriptor), expectedCode) + } +} + +private func diff(expected: String, actual: String) throws -> String { + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/env") + process.arguments = [ + "bash", "-c", + "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')", + ] + let pipe = Pipe() + process.standardOutput = pipe + try process.run() + process.waitUntilExit() + let pipeData = try XCTUnwrap( + pipe.fileHandleForReading.readToEnd(), + """ + No output from command: + \(process.executableURL!.path) \(process.arguments!.joined(separator: " ")) + """ + ) + return String(decoding: pipeData, as: UTF8.self) +} + +internal func XCTAssertEqualWithDiff( + _ actual: String, + _ expected: String, + file: StaticString = #filePath, + line: UInt = #line +) throws { + if actual == expected { return } + XCTFail( + """ + XCTAssertEqualWithDiff failed (click for diff) + \(try diff(expected: expected, actual: actual)) + """, + file: file, + line: line + ) +} + +#endif // os(macOS) || os(Linux) From aa033d6b3d8395468cc727ee352c527fd3ef2b52 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 1 Feb 2024 14:20:29 +0000 Subject: [PATCH 229/580] Add scripts to fetch and generate protos (#1784) * Add scripts to fetch and generate protos Motivation: Many of the proto files used in the repo are copied from upstream gRPC repos and built using the Makefile. There's no easy wasy to update the proto files and the Makefile has grown quite unwieldly and tends to be a bit painful to update. It's also inconvenient to run each generate target and targets often get missed. Modifications: - Add a 'Protos' directory containing two scripts: 'fetch.sh' and 'generate.sh'. - 'fetch.sh' clones a few upstream gRPC repos and selectively copies out protos we are interested in into 'Protos/upstream'. This makes it convenient to update all vendored protos. - 'generate.sh' effectively replaces the Makefile with separaet functions for generating grpc code and messages. - Move a few other protos from Sources/Tests into Protos - Remove the Makefile Result: Much easier to update and re-generate protos. --- Makefile | 248 ------ Package.swift | 42 +- Protos/README.md | 13 + .../Model => Protos/examples/echo}/echo.proto | 0 .../examples/route_guide}/route_guide.proto | 0 Protos/fetch.sh | 45 + Protos/generate.sh | 189 +++++ .../tests/normalization}/normalization.proto | 0 Protos/upstream/google/rpc/code.proto | 186 +++++ .../upstream/grpc/examples}/helloworld.proto | 5 +- Protos/upstream/grpc/lookup/v1/rls.proto | 68 ++ .../upstream/grpc/lookup/v1/rls_config.proto | 225 +++++ .../grpc/reflection/v1/reflection.proto | 6 + .../grpc/reflection/v1alpha/reflection.proto | 19 +- .../grpc/service_config/service_config.proto | 789 ++++++++++++++++++ .../HelloWorld/Model/helloworld.grpc.swift | 6 +- .../HelloWorld/Model/helloworld.pb.swift | 28 +- .../Generated/helloworld.grpc.reflection | 2 +- .../v1/reflection-v1.grpc.swift | 2 +- .../v1/reflection-v1.pb.swift | 2 +- .../v1Alpha/reflection-v1alpha.grpc.swift | 2 +- .../v1Alpha/reflection-v1alpha.pb.swift | 20 +- .../Normalization/normalization.pb.swift | 18 +- .../Generated/v1/reflection-v1.grpc.swift | 2 +- .../Generated/v1/reflection-v1.pb.swift | 2 +- .../v1Alpha/reflection-v1alpha.grpc.swift | 2 +- .../v1Alpha/reflection-v1alpha.pb.swift | 20 +- 27 files changed, 1606 insertions(+), 335 deletions(-) delete mode 100644 Makefile create mode 100644 Protos/README.md rename {Sources/Examples/Echo/Model => Protos/examples/echo}/echo.proto (100%) rename {Sources/Examples/RouteGuide/Model => Protos/examples/route_guide}/route_guide.proto (100%) create mode 100755 Protos/fetch.sh create mode 100755 Protos/generate.sh rename {Tests/GRPCTests/Codegen/Normalization => Protos/tests/normalization}/normalization.proto (100%) create mode 100644 Protos/upstream/google/rpc/code.proto rename {Sources/Examples/HelloWorld/Model => Protos/upstream/grpc/examples}/helloworld.proto (93%) create mode 100644 Protos/upstream/grpc/lookup/v1/rls.proto create mode 100644 Protos/upstream/grpc/lookup/v1/rls_config.proto rename Sources/GRPCReflectionService/v1/reflection-v1.proto => Protos/upstream/grpc/reflection/v1/reflection.proto (96%) rename Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto => Protos/upstream/grpc/reflection/v1alpha/reflection.proto (88%) create mode 100644 Protos/upstream/grpc/service_config/service_config.proto diff --git a/Makefile b/Makefile deleted file mode 100644 index 3a540a8f1..000000000 --- a/Makefile +++ /dev/null @@ -1,248 +0,0 @@ -# Which Swift to use. -SWIFT:=swift -# Where products will be built; this is the SPM default. -SWIFT_BUILD_PATH:=./.build -SWIFT_BUILD_CONFIGURATION=debug -SWIFT_FLAGS=--scratch-path=${SWIFT_BUILD_PATH} --configuration=${SWIFT_BUILD_CONFIGURATION} -# Force release configuration (for plugins) -SWIFT_FLAGS_RELEASE=$(patsubst --configuration=%,--configuration=release,$(SWIFT_FLAGS)) - -# protoc plugins. -PROTOC_GEN_SWIFT=${SWIFT_BUILD_PATH}/release/protoc-gen-swift -PROTOC_GEN_GRPC_SWIFT=${SWIFT_BUILD_PATH}/release/protoc-gen-grpc-swift - -SWIFT_BUILD:=${SWIFT} build ${SWIFT_FLAGS} -SWIFT_BUILD_RELEASE:=${SWIFT} build ${SWIFT_FLAGS_RELEASE} -SWIFT_TEST:=${SWIFT} test ${SWIFT_FLAGS} -SWIFT_PACKAGE:=${SWIFT} package ${SWIFT_FLAGS} - -# Name of generated xcodeproj -XCODEPROJ:=GRPC.xcodeproj - -### Package and plugin build targets ########################################### - -all: - ${SWIFT_BUILD} - -Package.resolved: - ${SWIFT_PACKAGE} resolve - -.PHONY: -plugins: ${PROTOC_GEN_SWIFT} ${PROTOC_GEN_GRPC_SWIFT} - cp $^ . - -${PROTOC_GEN_SWIFT}: Package.resolved - ${SWIFT_BUILD_RELEASE} --product protoc-gen-swift - -${PROTOC_GEN_GRPC_SWIFT}: Sources/protoc-gen-grpc-swift/*.swift - ${SWIFT_BUILD_RELEASE} --product protoc-gen-grpc-swift - -interop-test-runner: - ${SWIFT_BUILD} --product GRPCInteroperabilityTests - -interop-backoff-test-runner: - ${SWIFT_BUILD} --product GRPCConnectionBackoffInteropTest - -### Xcodeproj - -.PHONY: -project: ${XCODEPROJ} - -${XCODEPROJ}: - ${SWIFT_PACKAGE} generate-xcodeproj --output $@ - @-ruby scripts/fix-project-settings.rb GRPC.xcodeproj || \ - echo "Consider running 'sudo gem install xcodeproj' to automatically set correct indentation settings for the generated project." - -### Protobuf Generation ######################################################## - -%.pb.swift: %.proto ${PROTOC_GEN_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_SWIFT} \ - --swift_opt=Visibility=Public \ - --swift_out=$(dir $<) - -%.grpc.swift: %.proto ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Visibility=Public \ - --grpc-swift_out=$(dir $<) - -ECHO_PROTO=Sources/Examples/Echo/Model/echo.proto -ECHO_PB=$(ECHO_PROTO:.proto=.pb.swift) -ECHO_GRPC=$(ECHO_PROTO:.proto=.grpc.swift) - -# For Echo we'll generate the test client as well. -${ECHO_GRPC}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Visibility=Public,TestClient=true \ - --grpc-swift_out=$(dir $<) - -# Generates protobufs and gRPC client and server for the Echo example -.PHONY: -generate-echo: ${ECHO_PB} ${ECHO_GRPC} - -HELLOWORLD_PROTO=Sources/Examples/HelloWorld/Model/helloworld.proto -HELLOWORLD_PB=$(HELLOWORLD_PROTO:.proto=.pb.swift) -HELLOWORLD_GRPC=$(HELLOWORLD_PROTO:.proto=.grpc.swift) - -# Generates protobufs and gRPC client and server for the Hello World example -.PHONY: -generate-helloworld: ${HELLOWORLD_PB} ${HELLOWORLD_GRPC} - -ROUTE_GUIDE_PROTO=Sources/Examples/RouteGuide/Model/route_guide.proto -ROUTE_GUIDE_PB=$(ROUTE_GUIDE_PROTO:.proto=.pb.swift) -ROUTE_GUIDE_GRPC=$(ROUTE_GUIDE_PROTO:.proto=.grpc.swift) - -# Generates protobufs and gRPC client and server for the Route Guide example -.PHONY: -generate-route-guide: ${ROUTE_GUIDE_PB} ${ROUTE_GUIDE_GRPC} - -NORMALIZATION_PROTO=Tests/GRPCTests/Codegen/Normalization/normalization.proto -NORMALIZATION_PB=$(NORMALIZATION_PROTO:.proto=.pb.swift) -NORMALIZATION_GRPC=$(NORMALIZATION_PROTO:.proto=.grpc.swift) - -# For normalization we'll explicitly keep the method casing. -${NORMALIZATION_GRPC}: ${NORMALIZATION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=KeepMethodCasing=true \ - --grpc-swift_out=$(dir $<) - -# Generates protobufs and gRPC client and server for the Route Guide example -.PHONY: -generate-normalization: ${NORMALIZATION_PB} ${NORMALIZATION_GRPC} - -SERIALIZATION_GRPC_REFLECTION=Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection - -# For serialization we'll set the ReflectionData option to true. -${SERIALIZATION_GRPC_REFLECTION}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ - --grpc-swift_out=$(dir ${SERIALIZATION_GRPC_REFLECTION}) - -# Generates binary file containing the serialized file descriptor proto for the Serialization test -.PHONY: -generate-reflection-data: ${SERIALIZATION_GRPC_REFLECTION} - -REFLECTION_V1_PROTO=Sources/GRPCReflectionService/v1/reflection-v1.proto -REFLECTION_V1_PB=$(REFLECTION_V1_PROTO:.proto=.pb.swift) -REFLECTION_V1_GRPC=$(REFLECTION_V1_PROTO:.proto=.grpc.swift) - -REFLECTION_V1ALPHA_PROTO=Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto -REFLECTION_V1ALPHA_PB=$(REFLECTION_V1ALPHA_PROTO:.proto=.pb.swift) -REFLECTION_V1ALPHA_GRPC=$(REFLECTION_V1ALPHA_PROTO:.proto=.grpc.swift) - -# For Reflection we'll generate only the Server code. -${REFLECTION_V1_GRPC}: ${REFLECTION_V1_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Client=false \ - --grpc-swift_out=$(dir $<) - -# For Reflection we'll generate only the Server code. -${REFLECTION_V1ALPHA_GRPC}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Client=false \ - --grpc-swift_out=$(dir $<) - -# Generates protobufs and gRPC server for the Reflection Service -.PHONY: -generate-reflection: ${REFLECTION_V1_PB} ${REFLECTION_V1_GRPC} ${REFLECTION_V1ALPHA_PB} ${REFLECTION_V1ALPHA_GRPC} - -TEST_REFLECTION_V1_GRPC=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift -TEST_REFLECTION_V1_PB=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift -TEST_REFLECTION_V1ALPHA_GRPC=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift -TEST_REFLECTION_V1ALPHA_PB=Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift - -# For Testing the Reflection we'll generate only the Client code. -${TEST_REFLECTION_V1_GRPC}: ${REFLECTION_V1_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Client=true,Server=false \ - --grpc-swift_out=$(dir ${TEST_REFLECTION_V1_GRPC}) - -${TEST_REFLECTION_V1_PB}: ${REFLECTION_V1_PROTO} ${PROTOC_GEN_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_SWIFT} \ - --swift_out=$(dir ${TEST_REFLECTION_V1_PB}) - -# For Testing the Reflection we'll generate only the Client code. -${TEST_REFLECTION_V1ALPHA_GRPC}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Client=true,Server=false \ - --grpc-swift_out=$(dir ${TEST_REFLECTION_V1ALPHA_GRPC}) - -${TEST_REFLECTION_V1ALPHA_PB}: ${REFLECTION_V1ALPHA_PROTO} ${PROTOC_GEN_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_SWIFT} \ - --swift_out=$(dir ${TEST_REFLECTION_V1ALPHA_PB}) - -# Generates protobufs and gRPC clients for the Reflection Service Tests -.PHONY: -generate-reflection-test-clients: ${TEST_REFLECTION_V1_PB} ${TEST_REFLECTION_V1_GRPC} ${TEST_REFLECTION_V1ALPHA_PB} ${TEST_REFLECTION_V1ALPHA_GRPC} - -HELLOWORLD_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection - -${HELLOWORLD_SERIALIZED_PROTO_GRPC}: ${HELLOWORLD_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ - --grpc-swift_out=$(dir ${HELLOWORLD_SERIALIZED_PROTO_GRPC}) - -.PHONY: -generate-helloworld-reflection-data: ${HELLOWORLD_SERIALIZED_PROTO_GRPC} - -ECHO_SERIALIZED_PROTO_GRPC=Sources/Examples/ReflectionService/Generated/echo.grpc.reflection - -${ECHO_SERIALIZED_PROTO_GRPC}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} - protoc $< \ - --proto_path=$(dir $<) \ - --plugin=${PROTOC_GEN_GRPC_SWIFT} \ - --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ - --grpc-swift_out=$(dir ${ECHO_SERIALIZED_PROTO_GRPC}) - -.PHONY: -generate-echo-reflection-data: ${ECHO_SERIALIZED_PROTO_GRPC} - -### Testing #################################################################### - -# Normal test suite. -.PHONY: -test: - ${SWIFT_TEST} - -# Normal test suite with TSAN enabled. -.PHONY: -test-tsan: - ${SWIFT_TEST} --sanitize=thread - -# Runs codegen tests. -.PHONY: -test-plugin: ${PROTOC_GEN_GRPC_SWIFT} - PROTOC_GEN_GRPC_SWIFT=${PROTOC_GEN_GRPC_SWIFT} ./dev/codegen-tests/run-tests.sh - -### Misc. ###################################################################### - -.PHONY: -clean: - -rm -rf Packages - -rm -rf ${SWIFT_BUILD_PATH} - -rm -rf ${XCODEPROJ} - -rm -f Package.resolved - -cd Examples/Google/NaturalLanguage && make clean diff --git a/Package.swift b/Package.swift index d8975bc11..b864f5944 100644 --- a/Package.swift +++ b/Package.swift @@ -189,7 +189,7 @@ extension Target { .grpcCore ] ) - + static let grpcInterceptors: Target = .target( name: "GRPCInterceptors", dependencies: [ @@ -208,14 +208,14 @@ extension Target { .dequeModule ] ) - + static let grpcHTTP2TransportNIOPosix: Target = .target( name: "GRPCHTTP2TransportNIOPosix", dependencies: [ .grpcHTTP2Core ] ) - + static let grpcHTTP2TransportNIOTransportServices: Target = .target( name: "GRPCHTTP2TransportNIOTransportServices", dependencies: [ @@ -276,8 +276,7 @@ extension Target { .nioSSL, if: includeNIOSSL ), exclude: [ - "Codegen/Normalization/normalization.proto", - "Codegen/Serialization/echo.grpc.reflection", + "Codegen/Serialization/echo.grpc.reflection" ] ) @@ -298,7 +297,7 @@ extension Target { .grpcInProcessTransport ] ) - + static let grpcInterceptorsTests: Target = .testTarget( name: "GRPCInterceptorsTests", dependencies: [ @@ -319,14 +318,14 @@ extension Target { .nioTestUtils, ] ) - + static let grpcHTTP2TransportNIOPosixTests: Target = .testTarget( name: "GRPCHTTP2TransportNIOPosixTests", dependencies: [ .grpcHTTP2TransportNIOPosix ] ) - + static let grpcHTTP2TransportNIOTransportServicesTests: Target = .testTarget( name: "GRPCHTTP2TransportNIOTransportServicesTests", dependencies: [ @@ -349,7 +348,7 @@ extension Target { .protobuf ] ) - + static let grpcProtobufCodeGenTests: Target = .testTarget( name: "GRPCProtobufCodeGenTests", dependencies: [ @@ -359,7 +358,7 @@ extension Target { .protobufPluginLibrary ] ) - + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -447,10 +446,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/Echo/Model", - exclude: [ - "echo.proto", - ] + path: "Sources/Examples/Echo/Model" ) static let echoImplementation: Target = .target( @@ -489,10 +485,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/HelloWorld/Model", - exclude: [ - "helloworld.proto", - ] + path: "Sources/Examples/HelloWorld/Model" ) static let helloWorldClient: Target = .executableTarget( @@ -526,10 +519,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/RouteGuide/Model", - exclude: [ - "route_guide.proto", - ] + path: "Sources/Examples/RouteGuide/Model" ) static let routeGuideClient: Target = .executableTarget( @@ -580,11 +570,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/GRPCReflectionService", - exclude: [ - "v1/reflection-v1.proto", - "v1Alpha/reflection-v1alpha.proto" - ] + path: "Sources/GRPCReflectionService" ) static let reflectionServer: Target = .executableTarget( @@ -609,7 +595,7 @@ extension Target { name: "GRPCCodeGen", path: "Sources/GRPCCodeGen" ) - + static let grpcProtobuf: Target = .target( name: "GRPCProtobuf", dependencies: [ diff --git a/Protos/README.md b/Protos/README.md new file mode 100644 index 000000000..5133adad2 --- /dev/null +++ b/Protos/README.md @@ -0,0 +1,13 @@ +# Protos + +This directory contains proto messages used by gRPC Swift. These are split +across different directories: + +- `upstream` contains `.proto` files pulled from upstream sources. You can + update them using `fetch.sh`. Note that doing so will replace `upstream` in + its entirety. +- `examples` contains `.proto` files used in common examples. +- `tests` contains `.proto` files used in tests. + +You can run `generate.sh` to re-generate all files generated using `protoc` with +the Swift and gRPC Swift plugins. diff --git a/Sources/Examples/Echo/Model/echo.proto b/Protos/examples/echo/echo.proto similarity index 100% rename from Sources/Examples/Echo/Model/echo.proto rename to Protos/examples/echo/echo.proto diff --git a/Sources/Examples/RouteGuide/Model/route_guide.proto b/Protos/examples/route_guide/route_guide.proto similarity index 100% rename from Sources/Examples/RouteGuide/Model/route_guide.proto rename to Protos/examples/route_guide/route_guide.proto diff --git a/Protos/fetch.sh b/Protos/fetch.sh new file mode 100755 index 000000000..36cf64580 --- /dev/null +++ b/Protos/fetch.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# +# 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. + +set -eu + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +upstream="$here/upstream" + +# Create a temporary directory for the repo checkouts. +checkouts="$(mktemp -d)" + +# Clone the grpc and google protos into the staging area. +git clone --depth 1 https://github.com/grpc/grpc-proto "$checkouts/grpc-proto" +git clone --depth 1 https://github.com/googleapis/googleapis.git "$checkouts/googleapis" + +# Remove the old protos. +rm -rf "$upstream" + +# Create new directories to poulate. These are based on proto package name +# rather than source repository name. +mkdir -p "$upstream/grpc" +mkdir -p "$upstream/google" + +# Copy over the grpc-proto protos. +cp -rp "$checkouts/grpc-proto/grpc/service_config" "$upstream/grpc/service_config" +cp -rp "$checkouts/grpc-proto/grpc/lookup" "$upstream/grpc/lookup" +cp -rp "$checkouts/grpc-proto/grpc/reflection" "$upstream/grpc/reflection" +cp -rp "$checkouts/grpc-proto/grpc/examples" "$upstream/grpc/examples" + +# Copy over the googleapis protos. +mkdir -p "$upstream/google/rpc" +cp -rp "$checkouts/googleapis/google/rpc/code.proto" "$upstream/google/rpc/code.proto" diff --git a/Protos/generate.sh b/Protos/generate.sh new file mode 100755 index 000000000..22a916417 --- /dev/null +++ b/Protos/generate.sh @@ -0,0 +1,189 @@ +#!/bin/bash +# +# 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. + +set -eu + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +root="$here/.." +protoc=$(which protoc) + +# Build the protoc plugins. +swift build -c release --product protoc-gen-swift +swift build -c release --product protoc-gen-grpc-swift + +# Grab the plugin paths. +bin_path=$(swift build -c release --show-bin-path) +protoc_gen_swift="$bin_path/protoc-gen-swift" +protoc_generate_grpc_swift="$bin_path/protoc-gen-grpc-swift" + +# Genreates gRPC by invoking protoc with the gRPC Swift plugin. +# Parameters: +# - $1: .proto file +# - $2: proto path +# - $3: output path +# - $4 onwards: options to forward to the plugin +function generate_grpc { + local proto=$1 + local args=("--plugin=$protoc_generate_grpc_swift" "--proto_path=${2}" "--grpc-swift_out=${3}") + + for option in "${@:4}"; do + args+=("--grpc-swift_opt=$option") + done + + invoke_protoc "${args[@]}" "$proto" +} + +# Genreates messages by invoking protoc with the Swift plugin. +# Parameters: +# - $1: .proto file +# - $2: proto path +# - $3: output path +# - $4 onwards: options to forward to the plugin +function generate_message { + local proto=$1 + local args=("--plugin=$protoc_gen_swift" "--proto_path=$2" "--swift_out=$3") + + for option in "${@:4}"; do + args+=("--swift_opt=$option") + done + + invoke_protoc "${args[@]}" "$proto" +} + +function invoke_protoc { + # Setting -x when running the script produces a lot of output, instead boil + # just echo out the protoc invocations. + echo "$protoc" "$@" + "$protoc" "$@" +} + +#------------------------------------------------------------------------------ + +function generate_echo_example { + local proto="$here/examples/echo/echo.proto" + local output="$root/Sources/Examples/Echo/Model" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" "TestClient=true" +} + +function generate_routeguide_example { + local proto="$here/examples/route_guide/route_guide.proto" + local output="$root/Sources/Examples/RouteGuide/Model" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" +} + +function generate_helloworld_example { + local proto="$here/upstream/grpc/examples/helloworld.proto" + local output="$root/Sources/Examples/HelloWorld/Model" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" +} + +function generate_reflection_service { + local proto_v1="$here/upstream/grpc/reflection/v1/reflection.proto" + local output_v1="$root/Sources/GRPCReflectionService/v1" + + # Messages were accidentally leaked into public API, they shouldn't be but we + # can't undo that change until the next major version. + generate_message "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Public" + generate_grpc "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Internal" "Client=false" + + # Both protos have the same name so will generate Swift files with the same + # name. SwiftPM can't handle this so rename them. + mv "$output_v1/reflection.pb.swift" "$output_v1/reflection-v1.pb.swift" + mv "$output_v1/reflection.grpc.swift" "$output_v1/reflection-v1.grpc.swift" + + local proto_v1alpha="$here/upstream/grpc/reflection/v1alpha/reflection.proto" + local output_v1alpha="$root/Sources/GRPCReflectionService/v1alpha" + + # Messages were accidentally leaked into public API, they shouldn't be but we + # can't undo that change until the next major version. + generate_message "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Public" + generate_grpc "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Internal" "Client=false" + + # Both protos have the same name so will generate Swift files with the same + # name. SwiftPM can't handle this so rename them. + mv "$output_v1alpha/reflection.pb.swift" "$output_v1alpha/reflection-v1alpha.pb.swift" + mv "$output_v1alpha/reflection.grpc.swift" "$output_v1alpha/reflection-v1alpha.grpc.swift" +} + +function generate_reflection_client_for_tests { + local proto_v1="$here/upstream/grpc/reflection/v1/reflection.proto" + local output_v1="$root/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1" + + generate_message "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Internal" + generate_grpc "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Internal" "Server=false" + + # Both protos have the same name so will generate Swift files with the same + # name. SwiftPM can't handle this so rename them. + mv "$output_v1/reflection.pb.swift" "$output_v1/reflection-v1.pb.swift" + mv "$output_v1/reflection.grpc.swift" "$output_v1/reflection-v1.grpc.swift" + + local proto_v1alpha="$here/upstream/grpc/reflection/v1alpha/reflection.proto" + local output_v1alpha="$root/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha" + + generate_message "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Internal" + generate_grpc "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Internal" "Server=false" + + # Both protos have the same name so will generate Swift files with the same + # name. SwiftPM can't handle this so rename them. + mv "$output_v1alpha/reflection.pb.swift" "$output_v1alpha/reflection-v1alpha.pb.swift" + mv "$output_v1alpha/reflection.grpc.swift" "$output_v1alpha/reflection-v1alpha.grpc.swift" +} + +function generate_normalization_for_tests { + local proto="$here/tests/normalization/normalization.proto" + local output="$root/Tests/GRPCTests/Codegen/Normalization" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "KeepMethodCasing=true" +} + +function generate_echo_reflection_data_for_tests { + local proto="$here/examples/echo/echo.proto" + local output="$root/Tests/GRPCTests/Codegen/Serialization" + + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Client=false" "Server=false" "ReflectionData=true" +} + +function generate_reflection_data_example { + local protos=("$here/examples/echo/echo.proto" "$here/upstream/grpc/examples/helloworld.proto") + local output="$root/Sources/Examples/ReflectionService/Generated" + + for proto in "${protos[@]}"; do + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Client=false" "Server=false" "ReflectionData=true" + done +} + +#------------------------------------------------------------------------------ + +# Examples +generate_echo_example +generate_routeguide_example +generate_helloworld_example +generate_reflection_data_example + +# Reflection service and tests +generate_reflection_service +generate_reflection_client_for_tests +generate_echo_reflection_data_for_tests + +# Misc. tests +generate_normalization_for_tests diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.proto b/Protos/tests/normalization/normalization.proto similarity index 100% rename from Tests/GRPCTests/Codegen/Normalization/normalization.proto rename to Protos/tests/normalization/normalization.proto diff --git a/Protos/upstream/google/rpc/code.proto b/Protos/upstream/google/rpc/code.proto new file mode 100644 index 000000000..7c810af40 --- /dev/null +++ b/Protos/upstream/google/rpc/code.proto @@ -0,0 +1,186 @@ +// Copyright 2022 Google LLC +// +// 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. + +syntax = "proto3"; + +package google.rpc; + +option go_package = "google.golang.org/genproto/googleapis/rpc/code;code"; +option java_multiple_files = true; +option java_outer_classname = "CodeProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The canonical error codes for gRPC APIs. +// +// +// Sometimes multiple error codes may apply. Services should return +// the most specific error code that applies. For example, prefer +// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. +// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. +enum Code { + // Not an error; returned on success. + // + // HTTP Mapping: 200 OK + OK = 0; + + // The operation was cancelled, typically by the caller. + // + // HTTP Mapping: 499 Client Closed Request + CANCELLED = 1; + + // Unknown error. For example, this error may be returned when + // a `Status` value received from another address space belongs to + // an error space that is not known in this address space. Also + // errors raised by APIs that do not return enough error information + // may be converted to this error. + // + // HTTP Mapping: 500 Internal Server Error + UNKNOWN = 2; + + // The client specified an invalid argument. Note that this differs + // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments + // that are problematic regardless of the state of the system + // (e.g., a malformed file name). + // + // HTTP Mapping: 400 Bad Request + INVALID_ARGUMENT = 3; + + // The deadline expired before the operation could complete. For operations + // that change the state of the system, this error may be returned + // even if the operation has completed successfully. For example, a + // successful response from a server could have been delayed long + // enough for the deadline to expire. + // + // HTTP Mapping: 504 Gateway Timeout + DEADLINE_EXCEEDED = 4; + + // Some requested entity (e.g., file or directory) was not found. + // + // Note to server developers: if a request is denied for an entire class + // of users, such as gradual feature rollout or undocumented allowlist, + // `NOT_FOUND` may be used. If a request is denied for some users within + // a class of users, such as user-based access control, `PERMISSION_DENIED` + // must be used. + // + // HTTP Mapping: 404 Not Found + NOT_FOUND = 5; + + // The entity that a client attempted to create (e.g., file or directory) + // already exists. + // + // HTTP Mapping: 409 Conflict + ALREADY_EXISTS = 6; + + // The caller does not have permission to execute the specified + // operation. `PERMISSION_DENIED` must not be used for rejections + // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` + // instead for those errors). `PERMISSION_DENIED` must not be + // used if the caller can not be identified (use `UNAUTHENTICATED` + // instead for those errors). This error code does not imply the + // request is valid or the requested entity exists or satisfies + // other pre-conditions. + // + // HTTP Mapping: 403 Forbidden + PERMISSION_DENIED = 7; + + // The request does not have valid authentication credentials for the + // operation. + // + // HTTP Mapping: 401 Unauthorized + UNAUTHENTICATED = 16; + + // Some resource has been exhausted, perhaps a per-user quota, or + // perhaps the entire file system is out of space. + // + // HTTP Mapping: 429 Too Many Requests + RESOURCE_EXHAUSTED = 8; + + // The operation was rejected because the system is not in a state + // required for the operation's execution. For example, the directory + // to be deleted is non-empty, an rmdir operation is applied to + // a non-directory, etc. + // + // Service implementors can use the following guidelines to decide + // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: + // (a) Use `UNAVAILABLE` if the client can retry just the failing call. + // (b) Use `ABORTED` if the client should retry at a higher level. For + // example, when a client-specified test-and-set fails, indicating the + // client should restart a read-modify-write sequence. + // (c) Use `FAILED_PRECONDITION` if the client should not retry until + // the system state has been explicitly fixed. For example, if an "rmdir" + // fails because the directory is non-empty, `FAILED_PRECONDITION` + // should be returned since the client should not retry unless + // the files are deleted from the directory. + // + // HTTP Mapping: 400 Bad Request + FAILED_PRECONDITION = 9; + + // The operation was aborted, typically due to a concurrency issue such as + // a sequencer check failure or transaction abort. + // + // See the guidelines above for deciding between `FAILED_PRECONDITION`, + // `ABORTED`, and `UNAVAILABLE`. + // + // HTTP Mapping: 409 Conflict + ABORTED = 10; + + // The operation was attempted past the valid range. E.g., seeking or + // reading past end-of-file. + // + // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may + // be fixed if the system state changes. For example, a 32-bit file + // system will generate `INVALID_ARGUMENT` if asked to read at an + // offset that is not in the range [0,2^32-1], but it will generate + // `OUT_OF_RANGE` if asked to read from an offset past the current + // file size. + // + // There is a fair bit of overlap between `FAILED_PRECONDITION` and + // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific + // error) when it applies so that callers who are iterating through + // a space can easily look for an `OUT_OF_RANGE` error to detect when + // they are done. + // + // HTTP Mapping: 400 Bad Request + OUT_OF_RANGE = 11; + + // The operation is not implemented or is not supported/enabled in this + // service. + // + // HTTP Mapping: 501 Not Implemented + UNIMPLEMENTED = 12; + + // Internal errors. This means that some invariants expected by the + // underlying system have been broken. This error code is reserved + // for serious errors. + // + // HTTP Mapping: 500 Internal Server Error + INTERNAL = 13; + + // The service is currently unavailable. This is most likely a + // transient condition, which can be corrected by retrying with + // a backoff. Note that it is not always safe to retry + // non-idempotent operations. + // + // See the guidelines above for deciding between `FAILED_PRECONDITION`, + // `ABORTED`, and `UNAVAILABLE`. + // + // HTTP Mapping: 503 Service Unavailable + UNAVAILABLE = 14; + + // Unrecoverable data loss or corruption. + // + // HTTP Mapping: 500 Internal Server Error + DATA_LOSS = 15; +} diff --git a/Sources/Examples/HelloWorld/Model/helloworld.proto b/Protos/upstream/grpc/examples/helloworld.proto similarity index 93% rename from Sources/Examples/HelloWorld/Model/helloworld.proto rename to Protos/upstream/grpc/examples/helloworld.proto index 0da38e581..2be480c2f 100644 --- a/Sources/Examples/HelloWorld/Model/helloworld.proto +++ b/Protos/upstream/grpc/examples/helloworld.proto @@ -11,7 +11,6 @@ // 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. - syntax = "proto3"; option java_multiple_files = true; @@ -23,7 +22,7 @@ package helloworld; // The greeting service definition. service Greeter { - // Sends a greeting. + // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } @@ -32,7 +31,7 @@ message HelloRequest { string name = 1; } -// The response message containing the greetings. +// The response message containing the greetings message HelloReply { string message = 1; } diff --git a/Protos/upstream/grpc/lookup/v1/rls.proto b/Protos/upstream/grpc/lookup/v1/rls.proto new file mode 100644 index 000000000..032afd33d --- /dev/null +++ b/Protos/upstream/grpc/lookup/v1/rls.proto @@ -0,0 +1,68 @@ +// Copyright 2020 The gRPC Authors +// +// 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. + +syntax = "proto3"; + +package grpc.lookup.v1; + +import "google/protobuf/any.proto"; + +option go_package = "google.golang.org/grpc/lookup/grpc_lookup_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.lookup.v1"; +option java_outer_classname = "RlsProto"; + +message RouteLookupRequest { + // Target type allows the client to specify what kind of target format it + // would like from RLS to allow it to find the regional server, e.g. "grpc". + string target_type = 3; + // Possible reasons for making a request. + enum Reason { + REASON_UNKNOWN = 0; // Unused + REASON_MISS = 1; // No data available in local cache + REASON_STALE = 2; // Data in local cache is stale + } + // Reason for making this request. + Reason reason = 5; + // For REASON_STALE, the header_data from the stale response, if any. + string stale_header_data = 6; + // Map of key values extracted via key builders for the gRPC or HTTP request. + map key_map = 4; + // Application-specific optional extensions. + repeated google.protobuf.Any extensions = 7; + + reserved 1, 2; + reserved "server", "path"; +} + +message RouteLookupResponse { + // Prioritized list (best one first) of addressable entities to use + // for routing, using syntax requested by the request target_type. + // The targets will be tried in order until a healthy one is found. + repeated string targets = 3; + // Optional header value to pass along to AFE in the X-Google-RLS-Data header. + // Cached with "target" and sent with all requests that match the request key. + // Allows the RLS to pass its work product to the eventual target. + string header_data = 2; + // Application-specific optional extensions. + repeated google.protobuf.Any extensions = 4; + + reserved 1; + reserved "target"; +} + +service RouteLookupService { + // Lookup returns a target for a single key. + rpc RouteLookup(RouteLookupRequest) returns (RouteLookupResponse) {} +} diff --git a/Protos/upstream/grpc/lookup/v1/rls_config.proto b/Protos/upstream/grpc/lookup/v1/rls_config.proto new file mode 100644 index 000000000..9d2b6c54c --- /dev/null +++ b/Protos/upstream/grpc/lookup/v1/rls_config.proto @@ -0,0 +1,225 @@ +// Copyright 2020 The gRPC Authors +// +// 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. + +syntax = "proto3"; + +package grpc.lookup.v1; + +import "google/protobuf/duration.proto"; + +option go_package = "google.golang.org/grpc/lookup/grpc_lookup_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.lookup.v1"; +option java_outer_classname = "RlsConfigProto"; + +// Extract a key based on a given name (e.g. header name or query parameter +// name). The name must match one of the names listed in the "name" field. If +// the "required_match" field is true, one of the specified names must be +// present for the keybuilder to match. +message NameMatcher { + // The name that will be used in the RLS key_map to refer to this value. + // If required_match is true, you may omit this field or set it to an empty + // string, in which case the matcher will require a match, but won't update + // the key_map. + string key = 1; + + // Ordered list of names (headers or query parameter names) that can supply + // this value; the first one with a non-empty value is used. + repeated string names = 2; + + // If true, make this extraction required; the key builder will not match + // if no value is found. + bool required_match = 3; +} + +// A GrpcKeyBuilder applies to a given gRPC service, name, and headers. +message GrpcKeyBuilder { + // To match, one of the given Name fields must match; the service and method + // fields are specified as fixed strings. The service name is required and + // includes the proto package name. The method name may be omitted, in + // which case any method on the given service is matched. + message Name { + string service = 1; + string method = 2; + } + repeated Name names = 1; + + // If you wish to include the host, service, or method names as keys in the + // generated RouteLookupRequest, specify key names to use in the extra_keys + // submessage. If a key name is empty, no key will be set for that value. + // If this submessage is specified, the normal host/path fields will be left + // unset in the RouteLookupRequest. We are deprecating host/path in the + // RouteLookupRequest, so services should migrate to the ExtraKeys approach. + message ExtraKeys { + string host = 1; + string service = 2; + string method = 3; + } + ExtraKeys extra_keys = 3; + + // Extract keys from all listed headers. + // For gRPC, it is an error to specify "required_match" on the NameMatcher + // protos. + repeated NameMatcher headers = 2; + + // You can optionally set one or more specific key/value pairs to be added to + // the key_map. This can be useful to identify which builder built the key, + // for example if you are suppressing the actual method, but need to + // separately cache and request all the matched methods. + map constant_keys = 4; +} + +// An HttpKeyBuilder applies to a given HTTP URL and headers. +// +// Path and host patterns use the matching syntax from gRPC transcoding to +// extract named key/value pairs from the path and host components of the URL: +// https://github.com/googleapis/googleapis/blob/master/google/api/http.proto +// +// It is invalid to specify the same key name in multiple places in a pattern. +// +// For a service where the project id can be expressed either as a subdomain or +// in the path, separate HttpKeyBuilders must be used: +// host_pattern: 'example.com' path_pattern: '/{id}/{object}/**' +// host_pattern: '{id}.example.com' path_pattern: '/{object}/**' +// If the host is exactly 'example.com', the first path segment will be used as +// the id and the second segment as the object. If the host has a subdomain, the +// subdomain will be used as the id and the first segment as the object. If +// neither pattern matches, no keys will be extracted. +message HttpKeyBuilder { + // host_pattern is an ordered list of host template patterns for the desired + // value. If any host_pattern values are specified, then at least one must + // match, and the last one wins and sets any specified variables. A host + // consists of labels separated by dots. Each label is matched against the + // label in the pattern as follows: + // - "*": Matches any single label. + // - "**": Matches zero or more labels (first or last part of host only). + // - "{=...}": One or more label capture, where "..." can be any + // template that does not include a capture. + // - "{}": A single label capture. Identical to {=*}. + // + // Examples: + // - "example.com": Only applies to the exact host example.com. + // - "*.example.com": Matches subdomains of example.com. + // - "**.example.com": matches example.com, and all levels of subdomains. + // - "{project}.example.com": Extracts the third level subdomain. + // - "{project=**}.example.com": Extracts the third level+ subdomains. + // - "{project=**}": Extracts the entire host. + repeated string host_patterns = 1; + + // path_pattern is an ordered list of path template patterns for the desired + // value. If any path_pattern values are specified, then at least one must + // match, and the last one wins and sets any specified variables. A path + // consists of segments separated by slashes. Each segment is matched against + // the segment in the pattern as follows: + // - "*": Matches any single segment. + // - "**": Matches zero or more segments (first or last part of path only). + // - "{=...}": One or more segment capture, where "..." can be any + // template that does not include a capture. + // - "{}": A single segment capture. Identical to {=*}. + // A custom method may also be specified by appending ":" and the custom + // method name or "*" to indicate any custom method (including no custom + // method). For example, "/*/projects/{project_id}/**:*" extracts + // `{project_id}` for any version, resource and custom method that includes + // it. By default, any custom method will be matched. + // + // Examples: + // - "/v1/{name=messages/*}": extracts a name like "messages/12345". + // - "/v1/messages/{message_id}": extracts a message_id like "12345". + // - "/v1/users/{user_id}/messages/{message_id}": extracts two key values. + repeated string path_patterns = 2; + + // List of query parameter names to try to match. + // For example: ["parent", "name", "resource.name"] + // We extract all the specified query_parameters (case-sensitively). If any + // are marked as "required_match" and are not present, this keybuilder fails + // to match. If a given parameter appears multiple times (?foo=a&foo=b) we + // will report it as a comma-separated string (foo=a,b). + repeated NameMatcher query_parameters = 3; + + // List of headers to try to match. + // We extract all the specified header values (case-insensitively). If any + // are marked as "required_match" and are not present, this keybuilder fails + // to match. If a given header appears multiple times in the request we will + // report it as a comma-separated string, in standard HTTP fashion. + repeated NameMatcher headers = 4; + + // You can optionally set one or more specific key/value pairs to be added to + // the key_map. This can be useful to identify which builder built the key, + // for example if you are suppressing a lot of information from the URL, but + // need to separately cache and request URLs with that content. + map constant_keys = 5; +} + +message RouteLookupConfig { + // Ordered specifications for constructing keys for HTTP requests. Last + // match wins. If no HttpKeyBuilder matches, an empty key_map will be sent to + // the lookup service; it should likely reply with a global default route + // and raise an alert. + repeated HttpKeyBuilder http_keybuilders = 1; + + // Unordered specifications for constructing keys for gRPC requests. All + // GrpcKeyBuilders on this list must have unique "name" fields so that the + // client is free to prebuild a hash map keyed by name. If no GrpcKeyBuilder + // matches, an empty key_map will be sent to the lookup service; it should + // likely reply with a global default route and raise an alert. + repeated GrpcKeyBuilder grpc_keybuilders = 2; + + // The name of the lookup service as a gRPC URI. Typically, this will be + // a subdomain of the target, such as "lookup.datastore.googleapis.com". + string lookup_service = 3; + + // Configure a timeout value for lookup service requests. + // Defaults to 10 seconds if not specified. + google.protobuf.Duration lookup_service_timeout = 4; + + // How long are responses valid for (like HTTP Cache-Control). + // If omitted or zero, the longest valid cache time is used. + // This value is clamped to 5 minutes to avoid unflushable bad responses. + google.protobuf.Duration max_age = 5; + + // After a response has been in the client cache for this amount of time + // and is re-requested, start an asynchronous RPC to re-validate it. + // This value should be less than max_age by at least the length of a + // typical RTT to the Route Lookup Service to fully mask the RTT latency. + // If omitted, keys are only re-requested after they have expired. + google.protobuf.Duration stale_age = 6; + + // Rough indicator of amount of memory to use for the client cache. Some of + // the data structure overhead is not accounted for, so actual memory consumed + // will be somewhat greater than this value. If this field is omitted or set + // to zero, a client default will be used. The value may be capped to a lower + // amount based on client configuration. + int64 cache_size_bytes = 7; + + // This is a list of all the possible targets that can be returned by the + // lookup service. If a target not on this list is returned, it will be + // treated the same as an unhealthy target. + repeated string valid_targets = 8; + + // This value provides a default target to use if needed. If set, it will be + // used if RLS returns an error, times out, or returns an invalid response. + // Note that requests can be routed only to a subdomain of the original + // target, e.g. "us_east_1.cloudbigtable.googleapis.com". + string default_target = 9; + + reserved 10; + reserved "request_processing_strategy"; +} + +// RouteLookupClusterSpecifier is used in xDS to represent a cluster specifier +// plugin for RLS. +message RouteLookupClusterSpecifier { + // The RLS config for this cluster specifier plugin instance. + RouteLookupConfig route_lookup_config = 1; +} diff --git a/Sources/GRPCReflectionService/v1/reflection-v1.proto b/Protos/upstream/grpc/reflection/v1/reflection.proto similarity index 96% rename from Sources/GRPCReflectionService/v1/reflection-v1.proto rename to Protos/upstream/grpc/reflection/v1/reflection.proto index c540fe365..1a2ceedc3 100644 --- a/Sources/GRPCReflectionService/v1/reflection-v1.proto +++ b/Protos/upstream/grpc/reflection/v1/reflection.proto @@ -23,6 +23,11 @@ syntax = "proto3"; package grpc.reflection.v1; +option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.reflection.v1"; +option java_outer_classname = "ServerReflectionProto"; + service ServerReflection { // The reflection service is structured as a bidirectional stream, ensuring // all related requests go to a single server. @@ -139,3 +144,4 @@ message ErrorResponse { int32 error_code = 1; string error_message = 2; } + diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto b/Protos/upstream/grpc/reflection/v1alpha/reflection.proto similarity index 88% rename from Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto rename to Protos/upstream/grpc/reflection/v1alpha/reflection.proto index 816852f82..9cab8a4e0 100644 --- a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.proto +++ b/Protos/upstream/grpc/reflection/v1alpha/reflection.proto @@ -1,4 +1,4 @@ -// Copyright 2016 gRPC authors. +// Copyright 2016 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,13 +11,22 @@ // 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. - // Service exported by server reflection + +// Warning: this entire file is deprecated. Use this instead: +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + syntax = "proto3"; package grpc.reflection.v1alpha; +option deprecated = true; +option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"; +option java_multiple_files = true; +option java_package = "io.grpc.reflection.v1alpha"; +option java_outer_classname = "ServerReflectionProto"; + service ServerReflection { // The reflection service is structured as a bidirectional stream, ensuring // all related requests go to a single server. @@ -44,8 +53,8 @@ message ServerReflectionRequest { // message type with the given field number. ExtensionRequest file_containing_extension = 5; - // Finds the tag numbers used by all known extensions of the given message - // type, and appends them to ExtensionNumberResponse in an undefined order. + // Finds the tag numbers used by all known extensions of extendee_type, and + // appends them to ExtensionNumberResponse in an undefined order. // Its corresponding method is best-effort: it's not guaranteed that the // reflection service will implement this method, and it's not guaranteed // that this method will provide all extensions. Returns @@ -72,7 +81,7 @@ message ExtensionRequest { message ServerReflectionResponse { string valid_host = 1; ServerReflectionRequest original_request = 2; - // The server set one of the following fields accroding to the message_request + // The server set one of the following fields according to the message_request // in the request. oneof message_response { // This message is used to answer file_by_filename, file_containing_symbol, diff --git a/Protos/upstream/grpc/service_config/service_config.proto b/Protos/upstream/grpc/service_config/service_config.proto new file mode 100644 index 000000000..340c21193 --- /dev/null +++ b/Protos/upstream/grpc/service_config/service_config.proto @@ -0,0 +1,789 @@ +// Copyright 2016 The gRPC Authors +// +// 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. + +// A ServiceConfig is supplied when a service is deployed. It mostly contains +// parameters for how clients that connect to the service should behave (for +// example, the load balancing policy to use to pick between service replicas). +// +// The configuration options provided here act as overrides to automatically +// chosen option values. Service owners should be conservative in specifying +// options as the system is likely to choose better values for these options in +// the vast majority of cases. In other words, please specify a configuration +// option only if you really have to, and avoid copy-paste inclusion of configs. +// +// Note that gRPC uses the service config in JSON form, not in protobuf +// form. This proto definition is intended to help document the schema but +// will not actually be used directly by gRPC. + +syntax = "proto3"; + +package grpc.service_config; + +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; +import "google/rpc/code.proto"; +import "grpc/lookup/v1/rls_config.proto"; + +option java_package = "io.grpc.serviceconfig"; +option java_multiple_files = true; +option java_outer_classname = "ServiceConfigProto"; + +// Configuration for a method. +message MethodConfig { + // The names of the methods to which this configuration applies. + // - MethodConfig without names (empty list) will be skipped. + // - Each name entry must be unique across the entire ServiceConfig. + // - If the 'method' field is empty, this MethodConfig specifies the defaults + // for all methods for the specified service. + // - If the 'service' field is empty, the 'method' field must be empty, and + // this MethodConfig specifies the default for all methods (it's the default + // config). + // + // When determining which MethodConfig to use for a given RPC, the most + // specific match wins. For example, let's say that the service config + // contains the following MethodConfig entries: + // + // method_config { name { } ... } + // method_config { name { service: "MyService" } ... } + // method_config { name { service: "MyService" method: "Foo" } ... } + // + // MyService/Foo will use the third entry, because it exactly matches the + // service and method name. MyService/Bar will use the second entry, because + // it provides the default for all methods of MyService. AnotherService/Baz + // will use the first entry, because it doesn't match the other two. + // + // In JSON representation, value "", value `null`, and not present are the + // same. The following are the same Name: + // - { "service": "s" } + // - { "service": "s", "method": null } + // - { "service": "s", "method": "" } + message Name { + string service = 1; // Required. Includes proto package name. + string method = 2; + } + repeated Name name = 1; + + // Whether RPCs sent to this method should wait until the connection is + // ready by default. If false, the RPC will abort immediately if there is + // a transient failure connecting to the server. Otherwise, gRPC will + // attempt to connect until the deadline is exceeded. + // + // The value specified via the gRPC client API will override the value + // set here. However, note that setting the value in the client API will + // also affect transient errors encountered during name resolution, which + // cannot be caught by the value here, since the service config is + // obtained by the gRPC client via name resolution. + google.protobuf.BoolValue wait_for_ready = 2; + + // The default timeout in seconds for RPCs sent to this method. This can be + // overridden in code. If no reply is received in the specified amount of + // time, the request is aborted and a DEADLINE_EXCEEDED error status + // is returned to the caller. + // + // The actual deadline used will be the minimum of the value specified here + // and the value set by the application via the gRPC client API. If either + // one is not set, then the other will be used. If neither is set, then the + // request has no deadline. + google.protobuf.Duration timeout = 3; + + // The maximum allowed payload size for an individual request or object in a + // stream (client->server) in bytes. The size which is measured is the + // serialized payload after per-message compression (but before stream + // compression) in bytes. This applies both to streaming and non-streaming + // requests. + // + // The actual value used is the minimum of the value specified here and the + // value set by the application via the gRPC client API. If either one is + // not set, then the other will be used. If neither is set, then the + // built-in default is used. + // + // If a client attempts to send an object larger than this value, it will not + // be sent and the client will see a ClientError. + // Note that 0 is a valid value, meaning that the request message + // must be empty. + google.protobuf.UInt32Value max_request_message_bytes = 4; + + // The maximum allowed payload size for an individual response or object in a + // stream (server->client) in bytes. The size which is measured is the + // serialized payload after per-message compression (but before stream + // compression) in bytes. This applies both to streaming and non-streaming + // requests. + // + // The actual value used is the minimum of the value specified here and the + // value set by the application via the gRPC client API. If either one is + // not set, then the other will be used. If neither is set, then the + // built-in default is used. + // + // If a server attempts to send an object larger than this value, it will not + // be sent, and a ServerError will be sent to the client instead. + // Note that 0 is a valid value, meaning that the response message + // must be empty. + google.protobuf.UInt32Value max_response_message_bytes = 5; + + // The retry policy for outgoing RPCs. + message RetryPolicy { + // The maximum number of RPC attempts, including the original attempt. + // + // This field is required and must be greater than 1. + // Any value greater than 5 will be treated as if it were 5. + uint32 max_attempts = 1; + + // Exponential backoff parameters. The initial retry attempt will occur at + // random(0, initial_backoff). In general, the nth attempt will occur at + // random(0, + // min(initial_backoff*backoff_multiplier**(n-1), max_backoff)). + // Required. Must be greater than zero. + google.protobuf.Duration initial_backoff = 2; + // Required. Must be greater than zero. + google.protobuf.Duration max_backoff = 3; + float backoff_multiplier = 4; // Required. Must be greater than zero. + + // The set of status codes which may be retried. + // + // This field is required and must be non-empty. + repeated google.rpc.Code retryable_status_codes = 5; + } + + // The hedging policy for outgoing RPCs. Hedged RPCs may execute more than + // once on the server, so only idempotent methods should specify a hedging + // policy. + message HedgingPolicy { + // The hedging policy will send up to max_requests RPCs. + // This number represents the total number of all attempts, including + // the original attempt. + // + // This field is required and must be greater than 1. + // Any value greater than 5 will be treated as if it were 5. + uint32 max_attempts = 1; + + // The first RPC will be sent immediately, but the max_requests-1 subsequent + // hedged RPCs will be sent at intervals of every hedging_delay. Set this + // to 0 to immediately send all max_requests RPCs. + google.protobuf.Duration hedging_delay = 2; + + // The set of status codes which indicate other hedged RPCs may still + // succeed. If a non-fatal status code is returned by the server, hedged + // RPCs will continue. Otherwise, outstanding requests will be canceled and + // the error returned to the client application layer. + // + // This field is optional. + repeated google.rpc.Code non_fatal_status_codes = 3; + } + + // Only one of retry_policy or hedging_policy may be set. If neither is set, + // RPCs will not be retried or hedged. + oneof retry_or_hedging_policy { + RetryPolicy retry_policy = 6; + HedgingPolicy hedging_policy = 7; + } +} + +// Configuration for pick_first LB policy. +message PickFirstConfig { + // If set to true, instructs the LB policy to randomly shuffle the list of + // addresses received from the name resolver before attempting to connect to + // them. + bool shuffle_address_list = 1; +} + +// Configuration for round_robin LB policy. +message RoundRobinConfig {} + +// Configuration for weighted_round_robin LB policy. +message WeightedRoundRobinLbConfig { + // Whether to enable out-of-band utilization reporting collection from + // the endpoints. By default, per-request utilization reporting is used. + google.protobuf.BoolValue enable_oob_load_report = 1; + + // Load reporting interval to request from the server. Note that the + // server may not provide reports as frequently as the client requests. + // Used only when enable_oob_load_report is true. Default is 10 seconds. + google.protobuf.Duration oob_reporting_period = 2; + + // A given endpoint must report load metrics continuously for at least + // this long before the endpoint weight will be used. This avoids + // churn when the set of endpoint addresses changes. Takes effect + // both immediately after we establish a connection to an endpoint and + // after weight_expiration_period has caused us to stop using the most + // recent load metrics. Default is 10 seconds. + google.protobuf.Duration blackout_period = 3; + + // If a given endpoint has not reported load metrics in this long, + // then we stop using the reported weight. This ensures that we do + // not continue to use very stale weights. Once we stop using a stale + // value, if we later start seeing fresh reports again, the + // blackout_period applies. Defaults to 3 minutes. + google.protobuf.Duration weight_expiration_period = 4; + + // How often endpoint weights are recalculated. Values less than 100ms are + // capped at 100ms. Default is 1 second. + google.protobuf.Duration weight_update_period = 5; + + // The multiplier used to adjust endpoint weights with the error rate + // calculated as eps/qps. Configuration is rejected if this value is negative. + // Default is 1.0. + google.protobuf.FloatValue error_utilization_penalty = 6; +} + +// Configuration for outlier_detection LB policy +message OutlierDetectionLoadBalancingConfig { + // The time interval between ejection analysis sweeps. This can result in + // both new ejections as well as addresses being returned to service. Defaults + // to 10000ms or 10s. + google.protobuf.Duration interval = 1; + + // The base time that as address is ejected for. The real time is equal to the + // base time multiplied by the number of times the address has been ejected. + // Defaults to 30000ms or 30s. + google.protobuf.Duration base_ejection_time = 2; + + // The maximum time that an address is ejected for. If not specified, the default value (300000ms or 300s) or + // the base_ejection_time value is applied, whatever is larger. + google.protobuf.Duration max_ejection_time = 3; + + // The maximum % of an address list that can be ejected due to outlier + // detection. Defaults to 10% but will eject at least one address regardless of the value. + google.protobuf.UInt32Value max_ejection_percent = 4; + + // Parameters for the success rate ejection algorithm. + // This algorithm monitors the request success rate for all endpoints and + // ejects individual endpoints whose success rates are statistical outliers. + message SuccessRateEjection { + // This factor is used to determine the ejection threshold for success rate + // outlier ejection. The ejection threshold is the difference between the + // mean success rate, and the product of this factor and the standard + // deviation of the mean success rate: mean - (stdev * + // success_rate_stdev_factor). This factor is divided by a thousand to get a + // double. That is, if the desired factor is 1.9, the runtime value should + // be 1900. Defaults to 1900. + google.protobuf.UInt32Value stdev_factor = 1; + + // The % chance that an address will be actually ejected when an outlier status + // is detected through success rate statistics. This setting can be used to + // disable ejection or to ramp it up slowly. Defaults to 100. + google.protobuf.UInt32Value enforcement_percentage = 2; + + // The number of addresses that must have enough request volume to + // detect success rate outliers. If the number of addresses is less than this + // setting, outlier detection via success rate statistics is not performed + // for any addresses. Defaults to 5. + google.protobuf.UInt32Value minimum_hosts = 3; + + // The minimum number of total requests that must be collected in one + // interval (as defined by the interval duration above) to include this address + // in success rate based outlier detection. If the volume is lower than this + // setting, outlier detection via success rate statistics is not performed + // for that address. Defaults to 100. + google.protobuf.UInt32Value request_volume = 4; + } + + // Parameters for the failure percentage algorithm. + // This algorithm ejects individual endpoints whose failure rate is greater than + // some threshold, independently of any other endpoint. + message FailurePercentageEjection { + // The failure percentage to use when determining failure percentage-based outlier detection. If + // the failure percentage of a given address is greater than or equal to this value, it will be + // ejected. Defaults to 85. + google.protobuf.UInt32Value threshold = 1; + + // The % chance that an address will be actually ejected when an outlier status is detected through + // failure percentage statistics. This setting can be used to disable ejection or to ramp it up + // slowly. Defaults to 100. + google.protobuf.UInt32Value enforcement_percentage = 2; + + // The minimum number of addresses in order to perform failure percentage-based ejection. + // If the total number of addresses is less than this value, failure percentage-based + // ejection will not be performed. Defaults to 5. + google.protobuf.UInt32Value minimum_hosts = 3; + + // The minimum number of total requests that must be collected in one interval (as defined by the + // interval duration above) to perform failure percentage-based ejection for this address. If the + // volume is lower than this setting, failure percentage-based ejection will not be performed for + // this host. Defaults to 50. + google.protobuf.UInt32Value request_volume = 4; + } + + // If set, success rate ejections will be performed + SuccessRateEjection success_rate_ejection = 5; + + // If set, failure rate ejections will be performed + FailurePercentageEjection failure_percentage_ejection = 6; + + // The config for the child policy + repeated LoadBalancingConfig child_policy = 13; +} + +// Configuration for grpclb LB policy. +message GrpcLbConfig { + // Optional. What LB policy to use for routing between the backend + // addresses. If unset, defaults to round_robin. + // Currently, the only supported values are round_robin and pick_first. + // Note that this will be used both in balancer mode and in fallback mode. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig child_policy = 1; + // Optional. If specified, overrides the name of the service to be sent to + // the balancer. + string service_name = 2; + // Optional. The timeout in seconds for receiving the server list from the LB + // server. Defaults to 10s. + google.protobuf.Duration initial_fallback_timeout = 3; +} + +// Configuration for priority LB policy. +message PriorityLoadBalancingPolicyConfig { + // A map of name to child policy configuration. + // The names are used to allow the priority policy to update + // existing child policies instead of creating new ones every + // time it receives a config update. + message Child { + repeated LoadBalancingConfig config = 1; + + // If true, will ignore reresolution requests from this child. + bool ignore_reresolution_requests = 2; + } + map children = 1; + + // A list of child names in decreasing priority order + // (i.e., first element is the highest priority). + repeated string priorities = 2; +} + +// Configuration for weighted_target LB policy. +message WeightedTargetLoadBalancingPolicyConfig { + message Target { + uint32 weight = 1; + repeated LoadBalancingConfig child_policy = 2; + } + map targets = 1; +} + +// Config for RLS LB policy. +message RlsLoadBalancingPolicyConfig { + grpc.lookup.v1.RouteLookupConfig route_lookup_config = 1; + + // Service config to use for the RLS channel. + ServiceConfig route_lookup_channel_service_config = 2; + + repeated LoadBalancingConfig child_policy = 3; + // Field name to add to child policy config to contain the target name. + string child_policy_config_target_field_name = 4; +} + +// Configuration for xds_cluster_manager_experimental LB policy. +message XdsClusterManagerLoadBalancingPolicyConfig { + message Child { + repeated LoadBalancingConfig child_policy = 1; + } + map children = 1; +} + +// Configuration for the cds LB policy. +message CdsConfig { + string cluster = 1; // Required. +} + +// Represents an xDS server. +message XdsServer { + string server_uri = 1 [json_name = "server_uri"]; // Required. + + message ChannelCredentials { + string type = 1; // Required. + google.protobuf.Struct config = 2; // Optional JSON config. + } + // A list of channel creds to use. The first supported type will be used. + repeated ChannelCredentials channel_creds = 2 [json_name = "channel_creds"]; + + // A repeated list of server features. + repeated google.protobuf.Value server_features = 3 + [json_name = "server_features"]; +} + +// Configuration for xds_cluster_resolver LB policy. +message XdsClusterResolverLoadBalancingPolicyConfig { + // Describes a discovery mechanism instance. + // For EDS or LOGICAL_DNS clusters, there will be exactly one + // DiscoveryMechanism, which will describe the cluster of the parent + // CDS policy. + // For aggregate clusters, there will be one DiscoveryMechanism for each + // underlying cluster. + message DiscoveryMechanism { + // Cluster name. + string cluster = 1; + + // LRS server to send load reports to. + // If not present, load reporting will be disabled. + // If set to the empty string, load reporting will be sent to the same + // server that we obtained CDS data from. + // DEPRECATED: Use new lrs_load_reporting_server field instead. + google.protobuf.StringValue lrs_load_reporting_server_name = 2 + [deprecated=true]; + + // LRS server to send load reports to. + // If not present, load reporting will be disabled. + // Supercedes lrs_load_reporting_server_name field. + XdsServer lrs_load_reporting_server = 7; + + // Maximum number of outstanding requests can be made to the upstream + // cluster. Default is 1024. + google.protobuf.UInt32Value max_concurrent_requests = 3; + + enum Type { + UNKNOWN = 0; + EDS = 1; + LOGICAL_DNS = 2; + }; + Type type = 4; + + // For type EDS only. + // EDS service name, as returned in CDS. + // May be unset if not specified in CDS. + string eds_service_name = 5; + + // For type LOGICAL_DNS only. + // DNS name to resolve in "host:port" form. + string dns_hostname = 6; + + // The configuration for outlier_detection child policies + // Within this message, the child_policy field will be ignored + OutlierDetectionLoadBalancingConfig outlier_detection = 8; + + // The configuration for xds_override_host child policy + repeated OverrideHostLoadBalancingPolicyConfig.HealthStatus override_host_status = 9; + + // Telemetry labels associated with this cluster + map telemetry_labels = 10; + } + + // Ordered list of discovery mechanisms. + // Must have at least one element. + // Results from each discovery mechanism are concatenated together in + // successive priorities. + repeated DiscoveryMechanism discovery_mechanisms = 1; + + // xDS LB policy. Will be used as the child config of the xds_cluster_impl LB policy. + repeated LoadBalancingConfig xds_lb_policy = 2; +} + +// Configuration for xds_cluster_impl LB policy. +message XdsClusterImplLoadBalancingPolicyConfig { + // Cluster name. Required. + string cluster = 1; + + // EDS service name. + // Not set if cluster is not an EDS cluster or if it does not + // specify an EDS service name. + string eds_service_name = 2; + + // Server to send load reports to. + // If unset, no load reporting is done. + // If set to empty string, load reporting will be sent to the same + // server as we are getting xds data from. + // DEPRECATED: Use new lrs_load_reporting_server field instead. + google.protobuf.StringValue lrs_load_reporting_server_name = 3 + [deprecated=true]; + + // LRS server to send load reports to. + // If not present, load reporting will be disabled. + // Supercedes lrs_load_reporting_server_name field. + XdsServer lrs_load_reporting_server = 7; + + // Maximum number of outstanding requests can be made to the upstream cluster. + // Default is 1024. + google.protobuf.UInt32Value max_concurrent_requests = 4; + + // Drop configuration. + message DropCategory { + string category = 1; + uint32 requests_per_million = 2; + } + repeated DropCategory drop_categories = 5; + + // Child policy. + repeated LoadBalancingConfig child_policy = 6; + + // Telemetry labels associated with this cluster + map telemetry_labels = 8; +} + +// Configuration for eds LB policy. +message EdsLoadBalancingPolicyConfig { + // Cluster name. Required. + string cluster = 1; + + // EDS service name, as returned in CDS. + // May be unset if not specified in CDS. + string eds_service_name = 2; + + // Server to send load reports to. + // If unset, no load reporting is done. + // If set to empty string, load reporting will be sent to the same + // server as we are getting xds data from. + google.protobuf.StringValue lrs_load_reporting_server_name = 3; + + // Locality-picking policy. + // This policy's config is expected to be in the format used + // by the weighted_target policy. Note that the config should include + // an empty value for the "targets" field; that empty value will be + // replaced by one that is dynamically generated based on the EDS data. + // Optional; defaults to "weighted_target". + repeated LoadBalancingConfig locality_picking_policy = 4; + + // Endpoint-picking policy. + // This will be configured as the policy for each child in the + // locality-policy's config. + // Optional; defaults to "round_robin". + repeated LoadBalancingConfig endpoint_picking_policy = 5; +} + +// Configuration for ring_hash LB policy. +message RingHashLoadBalancingConfig { + // A client-side option will cap these values to 4096. If either of these + // values are greater than the client-side cap, they will be treated + // as the client-side cap value. + uint64 min_ring_size = 1; // Optional, defaults to 1024, max 8M. + uint64 max_ring_size = 2; // Optional, defaults to 4096, max 8M. +} + +// Configuration for lrs LB policy. +message LrsLoadBalancingPolicyConfig { + // Cluster name. Required. + string cluster_name = 1; + + // EDS service name, as returned in CDS. + // May be unset if not specified in CDS. + string eds_service_name = 2; + + // Server to send load reports to. Required. + // If set to empty string, load reporting will be sent to the same + // server as we are getting xds data from. + string lrs_load_reporting_server_name = 3; + + // The locality for which this policy will report load. Required. + message Locality { + string region = 1; + string zone = 2; + string subzone = 3; + } + Locality locality = 4; + + // Endpoint-picking policy. + repeated LoadBalancingConfig child_policy = 5; +} + +// Configuration for the xds_wrr_locality load balancing policy. +message XdsWrrLocalityLoadBalancingPolicyConfig { + repeated LoadBalancingConfig child_policy = 1; +} + +// Configuration for the least_request LB policy. +message LeastRequestLocalityLoadBalancingPolicyConfig { + uint64 choice_count = 1; +} + + +// Configuration for the override_host LB policy. +message OverrideHostLoadBalancingPolicyConfig { + enum HealthStatus { + UNKNOWN = 0; + HEALTHY = 1; + DRAINING = 3; + } + + // valid health status for hosts that are considered when using + // xds_override_host_experimental policy. + // Default is [UNKNOWN, HEALTHY] + repeated HealthStatus override_host_status = 1; + + repeated LoadBalancingConfig child_policy = 2; +} + +// Configuration for xds LB policy. +message XdsConfig { + // Name of balancer to connect to. + string balancer_name = 1 [deprecated = true]; + // Optional. What LB policy to use for intra-locality routing. + // If unset, will use whatever algorithm is specified by the balancer. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig child_policy = 2; + // Optional. What LB policy to use in fallback mode. If not + // specified, defaults to round_robin. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig fallback_policy = 3; + // Optional. Name to use in EDS query. If not present, defaults to + // the server name from the target URI. + string eds_service_name = 4; + // LRS server to send load reports to. + // If not present, load reporting will be disabled. + // If set to the empty string, load reporting will be sent to the same + // server that we obtained CDS data from. + google.protobuf.StringValue lrs_load_reporting_server_name = 5; +} + +// Selects LB policy and provides corresponding configuration. +// +// In general, all instances of this field should be repeated. Clients will +// iterate through the list in order and stop at the first policy that they +// support. This allows the service config to specify custom policies that may +// not be known to all clients. +// +// - If the config for the first supported policy is invalid, the whole service +// config is invalid. +// - If the list doesn't contain any supported policy, the whole service config +// is invalid. +message LoadBalancingConfig { + // Exactly one LB policy may be configured. + oneof policy { + // For each new LB policy supported by gRPC, a new field must be added + // here. The field's name must be the LB policy name and its type is a + // message that provides whatever configuration parameters are needed + // by the LB policy. The configuration message will be passed to the + // LB policy when it is instantiated on the client. + // + // If the LB policy does not require any configuration parameters, the + // message for that LB policy may be empty. + // + // Note that if an LB policy contains another nested LB policy + // (e.g., a gslb policy picks the cluster and then delegates to + // a round_robin policy to pick the backend within that cluster), its + // configuration message may include a nested instance of the + // LoadBalancingConfig message to configure the nested LB policy. + + PickFirstConfig pick_first = 4 [json_name = "pick_first"]; + + RoundRobinConfig round_robin = 1 [json_name = "round_robin"]; + + WeightedRoundRobinLbConfig weighted_round_robin = 20 + [json_name = "weighted_round_robin"]; + + // gRPC lookaside load balancing. + // This will eventually be deprecated by the new xDS-based local + // balancing policy. + GrpcLbConfig grpclb = 3; + + // REMAINING POLICIES ARE EXPERIMENTAL -- DO NOT USE + + PriorityLoadBalancingPolicyConfig priority_experimental = 9 + [json_name = "priority_experimental"]; + WeightedTargetLoadBalancingPolicyConfig weighted_target_experimental = 10 + [json_name = "weighted_target_experimental"]; + OutlierDetectionLoadBalancingConfig outlier_detection = 15 + [json_name = "outlier_detection_experimental"]; + RlsLoadBalancingPolicyConfig rls = 19 [json_name = "rls_experimental"]; + + // xDS-based load balancing. + XdsClusterManagerLoadBalancingPolicyConfig xds_cluster_manager_experimental + = 14 [json_name = "xds_cluster_manager_experimental"]; + CdsConfig cds_experimental = 6 [json_name = "cds_experimental"]; + XdsClusterResolverLoadBalancingPolicyConfig + xds_cluster_resolver_experimental = 11 + [json_name = "xds_cluster_resolver_experimental"]; + XdsClusterImplLoadBalancingPolicyConfig xds_cluster_impl_experimental = 12 + [json_name = "xds_cluster_impl_experimental"]; + OverrideHostLoadBalancingPolicyConfig override_host_experimental = 18 + [json_name = "override_host_experimental"]; + XdsWrrLocalityLoadBalancingPolicyConfig xds_wrr_locality_experimental = 16 + [json_name = "xds_wrr_locality_experimental"]; + RingHashLoadBalancingConfig ring_hash_experimental = 13 + [json_name = "ring_hash_experimental"]; + LeastRequestLocalityLoadBalancingPolicyConfig least_request_experimental = + 17 [json_name = "least_request_experimental"]; + + // Deprecated xDS-related policies. + LrsLoadBalancingPolicyConfig lrs_experimental = 8 + [json_name = "lrs_experimental", deprecated = true]; + EdsLoadBalancingPolicyConfig eds_experimental = 7 + [json_name = "eds_experimental", deprecated = true]; + XdsConfig xds = 2 [deprecated = true]; + XdsConfig xds_experimental = 5 [json_name = "xds_experimental", + deprecated = true]; + + // Next available ID: 21 + } +} + +// A ServiceConfig represents information about a service but is not specific to +// any name resolver. +message ServiceConfig { + // Load balancing policy. + // + // Note that load_balancing_policy is deprecated in favor of + // load_balancing_config; the former will be used only if the latter + // is unset. + // + // If no LB policy is configured here, then the default is pick_first. + // If the policy name is set via the client API, that value overrides + // the value specified here. + // + // If the deprecated load_balancing_policy field is used, note that if the + // resolver returns at least one balancer address (as opposed to backend + // addresses), gRPC will use grpclb (see + // https://github.com/grpc/grpc/blob/master/doc/load-balancing.md), + // regardless of what policy is configured here. However, if the resolver + // returns at least one backend address in addition to the balancer + // address(es), the client may fall back to the requested policy if it + // is unable to reach any of the grpclb load balancers. + enum LoadBalancingPolicy { + UNSPECIFIED = 0; + ROUND_ROBIN = 1; + } + LoadBalancingPolicy load_balancing_policy = 1 [deprecated = true]; + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. If none + // are supported, the service config is considered invalid. + repeated LoadBalancingConfig load_balancing_config = 4; + + // Per-method configuration. + repeated MethodConfig method_config = 2; + + // If a RetryThrottlingPolicy is provided, gRPC will automatically throttle + // retry attempts and hedged RPCs when the client's ratio of failures to + // successes exceeds a threshold. + // + // For each server name, the gRPC client will maintain a token_count which is + // initially set to max_tokens. Every outgoing RPC (regardless of service or + // method invoked) will change token_count as follows: + // + // - Every failed RPC will decrement the token_count by 1. + // - Every successful RPC will increment the token_count by token_ratio. + // + // If token_count is less than or equal to max_tokens / 2, then RPCs will not + // be retried and hedged RPCs will not be sent. + message RetryThrottlingPolicy { + // The number of tokens starts at max_tokens. The token_count will always be + // between 0 and max_tokens. + // + // This field is required and must be greater than zero. + uint32 max_tokens = 1; + + // The amount of tokens to add on each successful RPC. Typically this will + // be some number between 0 and 1, e.g., 0.1. + // + // This field is required and must be greater than zero. Up to 3 decimal + // places are supported. + float token_ratio = 2; + } + RetryThrottlingPolicy retry_throttling = 3; + + message HealthCheckConfig { + // Service name to use in the health-checking request. + google.protobuf.StringValue service_name = 1; + } + HealthCheckConfig health_check_config = 5; + + // next available tag: 6 +} diff --git a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift index 014226e95..affa2b8a5 100644 --- a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift +++ b/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift @@ -29,7 +29,7 @@ extension Helloworld_GreeterClientProtocol { return "helloworld.Greeter" } - /// Sends a greeting. + /// Sends a greeting /// /// - Parameters: /// - request: Request to send to SayHello. @@ -202,7 +202,7 @@ public enum Helloworld_GreeterClientMetadata { public protocol Helloworld_GreeterProvider: CallHandlerProvider { var interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? { get } - /// Sends a greeting. + /// Sends a greeting func sayHello(request: Helloworld_HelloRequest, context: StatusOnlyCallContext) -> EventLoopFuture } @@ -241,7 +241,7 @@ public protocol Helloworld_GreeterAsyncProvider: CallHandlerProvider, Sendable { static var serviceDescriptor: GRPCServiceDescriptor { get } var interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? { get } - /// Sends a greeting. + /// Sends a greeting func sayHello( request: Helloworld_HelloRequest, context: GRPCAsyncServerCallContext diff --git a/Sources/Examples/HelloWorld/Model/helloworld.pb.swift b/Sources/Examples/HelloWorld/Model/helloworld.pb.swift index edfb442c7..7a8a922fb 100644 --- a/Sources/Examples/HelloWorld/Model/helloworld.pb.swift +++ b/Sources/Examples/HelloWorld/Model/helloworld.pb.swift @@ -7,19 +7,19 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -// Copyright 2015 gRPC authors. -// -// 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. +/// Copyright 2015 gRPC authors. +/// +/// 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 Foundation import SwiftProtobuf @@ -47,7 +47,7 @@ public struct Helloworld_HelloRequest { public init() {} } -/// The response message containing the greetings. +/// The response message containing the greetings public struct Helloworld_HelloReply { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for diff --git a/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection b/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection index 39da0fc07..e88e9ac2c 100644 --- a/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection +++ b/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection @@ -1 +1 @@ -ChBoZWxsb3dvcmxkLnByb3RvEgpoZWxsb3dvcmxkIiIKDEhlbGxvUmVxdWVzdBISCgRuYW1lGAEgASgJUgRuYW1lIiYKCkhlbGxvUmVwbHkSGAoHbWVzc2FnZRgBIAEoCVIHbWVzc2FnZTJJCgdHcmVldGVyEj4KCFNheUhlbGxvEhguaGVsbG93b3JsZC5IZWxsb1JlcXVlc3QaFi5oZWxsb3dvcmxkLkhlbGxvUmVwbHkiAEI2Chtpby5ncnBjLmV4YW1wbGVzLmhlbGxvd29ybGRCD0hlbGxvV29ybGRQcm90b1ABogIDSExXSrMICgYSBA4AJQEKvwQKAQwSAw4AEjK0BCBDb3B5cmlnaHQgMjAxNSBnUlBDIGF1dGhvcnMuCgogTGljZW5zZWQgdW5kZXIgdGhlIEFwYWNoZSBMaWNlbnNlLCBWZXJzaW9uIDIuMCAodGhlICJMaWNlbnNlIik7CiB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiBZb3UgbWF5IG9idGFpbiBhIGNvcHkgb2YgdGhlIExpY2Vuc2UgYXQKCiAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzL0xJQ0VOU0UtMi4wCgogVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogZGlzdHJpYnV0ZWQgdW5kZXIgdGhlIExpY2Vuc2UgaXMgZGlzdHJpYnV0ZWQgb24gYW4gIkFTIElTIiBCQVNJUywKIFdJVEhPVVQgV0FSUkFOVElFUyBPUiBDT05ESVRJT05TIE9GIEFOWSBLSU5ELCBlaXRoZXIgZXhwcmVzcyBvciBpbXBsaWVkLgogU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogbGltaXRhdGlvbnMgdW5kZXIgdGhlIExpY2Vuc2UuCgoICgEIEgMQACIKCQoCCAoSAxAAIgoICgEIEgMRADQKCQoCCAESAxEANAoICgEIEgMSADAKCQoCCAgSAxIAMAoICgEIEgMTACEKCQoCCCQSAxMAIQoICgECEgMVABMKLgoCBgASBBgAGwEaIiBUaGUgZ3JlZXRpbmcgc2VydmljZSBkZWZpbml0aW9uLgoKCgoDBgABEgMYCA8KIAoEBgACABIDGgI1GhMgU2VuZHMgYSBncmVldGluZy4KCgwKBQYAAgABEgMaBg4KDAoFBgACAAISAxoQHAoMCgUGAAIAAxIDGicxCj0KAgQAEgQeACABGjEgVGhlIHJlcXVlc3QgbWVzc2FnZSBjb250YWluaW5nIHRoZSB1c2VyJ3MgbmFtZS4KCgoKAwQAARIDHggUCgsKBAQAAgASAx8CEgoMCgUEAAIABRIDHwIICgwKBQQAAgABEgMfCQ0KDAoFBAACAAMSAx8QEQo8CgIEARIEIwAlARowIFRoZSByZXNwb25zZSBtZXNzYWdlIGNvbnRhaW5pbmcgdGhlIGdyZWV0aW5ncy4KCgoKAwQBARIDIwgSCgsKBAQBAgASAyQCFQoMCgUEAQIABRIDJAIICgwKBQQBAgABEgMkCRAKDAoFBAECAAMSAyQTFGIGcHJvdG8z \ No newline at end of file +ChBoZWxsb3dvcmxkLnByb3RvEgpoZWxsb3dvcmxkIiIKDEhlbGxvUmVxdWVzdBISCgRuYW1lGAEgASgJUgRuYW1lIiYKCkhlbGxvUmVwbHkSGAoHbWVzc2FnZRgBIAEoCVIHbWVzc2FnZTJJCgdHcmVldGVyEj4KCFNheUhlbGxvEhguaGVsbG93b3JsZC5IZWxsb1JlcXVlc3QaFi5oZWxsb3dvcmxkLkhlbGxvUmVwbHkiAEI2Chtpby5ncnBjLmV4YW1wbGVzLmhlbGxvd29ybGRCD0hlbGxvV29ybGRQcm90b1ABogIDSExXSrEICgYSBA0AJAEKvwQKAQwSAw0AEhq0BCBDb3B5cmlnaHQgMjAxNSBnUlBDIGF1dGhvcnMuCgogTGljZW5zZWQgdW5kZXIgdGhlIEFwYWNoZSBMaWNlbnNlLCBWZXJzaW9uIDIuMCAodGhlICJMaWNlbnNlIik7CiB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiBZb3UgbWF5IG9idGFpbiBhIGNvcHkgb2YgdGhlIExpY2Vuc2UgYXQKCiAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzL0xJQ0VOU0UtMi4wCgogVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogZGlzdHJpYnV0ZWQgdW5kZXIgdGhlIExpY2Vuc2UgaXMgZGlzdHJpYnV0ZWQgb24gYW4gIkFTIElTIiBCQVNJUywKIFdJVEhPVVQgV0FSUkFOVElFUyBPUiBDT05ESVRJT05TIE9GIEFOWSBLSU5ELCBlaXRoZXIgZXhwcmVzcyBvciBpbXBsaWVkLgogU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogbGltaXRhdGlvbnMgdW5kZXIgdGhlIExpY2Vuc2UuCgoICgEIEgMPACIKCQoCCAoSAw8AIgoICgEIEgMQADQKCQoCCAESAxAANAoICgEIEgMRADAKCQoCCAgSAxEAMAoICgEIEgMSACEKCQoCCCQSAxIAIQoICgECEgMUABMKLgoCBgASBBcAGgEaIiBUaGUgZ3JlZXRpbmcgc2VydmljZSBkZWZpbml0aW9uLgoKCgoDBgABEgMXCA8KHwoEBgACABIDGQI1GhIgU2VuZHMgYSBncmVldGluZwoKDAoFBgACAAESAxkGDgoMCgUGAAIAAhIDGRAcCgwKBQYAAgADEgMZJzEKPQoCBAASBB0AHwEaMSBUaGUgcmVxdWVzdCBtZXNzYWdlIGNvbnRhaW5pbmcgdGhlIHVzZXIncyBuYW1lLgoKCgoDBAABEgMdCBQKCwoEBAACABIDHgISCgwKBQQAAgAFEgMeAggKDAoFBAACAAESAx4JDQoMCgUEAAIAAxIDHhARCjsKAgQBEgQiACQBGi8gVGhlIHJlc3BvbnNlIG1lc3NhZ2UgY29udGFpbmluZyB0aGUgZ3JlZXRpbmdzCgoKCgMEAQESAyIIEgoLCgQEAQIAEgMjAhUKDAoFBAECAAUSAyMCCAoMCgUEAQIAARIDIwkQCgwKBQQBAgADEgMjExRiBnByb3RvMw== \ No newline at end of file diff --git a/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift b/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift index 620845aac..0cd46f660 100644 --- a/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift +++ b/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift @@ -3,7 +3,7 @@ // swift-format-ignore-file // // Generated by the protocol buffer compiler. -// Source: reflection-v1.proto +// Source: reflection.proto // import GRPC import NIO diff --git a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift index 49162fb05..8e0d557c4 100644 --- a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift +++ b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift @@ -2,7 +2,7 @@ // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection-v1.proto +// Source: reflection.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift index eec65dda6..ed52d5924 100644 --- a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift +++ b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift @@ -3,7 +3,7 @@ // swift-format-ignore-file // // Generated by the protocol buffer compiler. -// Source: reflection-v1alpha.proto +// Source: reflection.proto // import GRPC import NIO diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift index 84e739577..1cf7338ba 100644 --- a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift +++ b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift @@ -2,12 +2,12 @@ // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection-v1alpha.proto +// Source: reflection.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -// Copyright 2016 gRPC authors. +// Copyright 2016 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ // 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. - // Service exported by server reflection +// Warning: this entire file is deprecated. Use this instead: +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + import Foundation import SwiftProtobuf @@ -79,8 +81,8 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionRequest { set {messageRequest = .fileContainingExtension(newValue)} } - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Finds the tag numbers used by all known extensions of extendee_type, and + /// appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the /// reflection service will implement this method, and it's not guaranteed /// that this method will provide all extensions. Returns @@ -120,8 +122,8 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// Find the proto file which defines an extension extending the given /// message type with the given field number. case fileContainingExtension(Grpc_Reflection_V1alpha_ExtensionRequest) - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Finds the tag numbers used by all known extensions of extendee_type, and + /// appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the /// reflection service will implement this method, and it's not guaranteed /// that this method will provide all extensions. Returns @@ -202,7 +204,7 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. public mutating func clearOriginalRequest() {self._originalRequest = nil} - /// The server set one of the following fields accroding to the message_request + /// The server set one of the following fields according to the message_request /// in the request. public var messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse? = nil @@ -249,7 +251,7 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionResponse { public var unknownFields = SwiftProtobuf.UnknownStorage() - /// The server set one of the following fields accroding to the message_request + /// The server set one of the following fields according to the message_request /// in the request. public enum OneOf_MessageResponse: Equatable { /// This message is used to answer file_by_filename, file_containing_symbol, diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift index 7183045b3..0316cfd2e 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift @@ -34,17 +34,17 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Normalization_FunctionName { +struct Normalization_FunctionName { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// The name of the invoked function. - public var functionName: String = String() + var functionName: String = String() - public var unknownFields = SwiftProtobuf.UnknownStorage() + var unknownFields = SwiftProtobuf.UnknownStorage() - public init() {} + init() {} } #if swift(>=5.5) && canImport(_Concurrency) @@ -56,12 +56,12 @@ extension Normalization_FunctionName: @unchecked Sendable {} fileprivate let _protobuf_package = "normalization" extension Normalization_FunctionName: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".FunctionName" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + static let protoMessageName: String = _protobuf_package + ".FunctionName" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "functionName"), ] - public mutating func decodeMessage(decoder: inout D) throws { + mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are @@ -73,14 +73,14 @@ extension Normalization_FunctionName: SwiftProtobuf.Message, SwiftProtobuf._Mess } } - public func traverse(visitor: inout V) throws { + func traverse(visitor: inout V) throws { if !self.functionName.isEmpty { try visitor.visitSingularStringField(value: self.functionName, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - public static func ==(lhs: Normalization_FunctionName, rhs: Normalization_FunctionName) -> Bool { + static func ==(lhs: Normalization_FunctionName, rhs: Normalization_FunctionName) -> Bool { if lhs.functionName != rhs.functionName {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift index 6f33bf6a9..57a605e77 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift @@ -3,7 +3,7 @@ // swift-format-ignore-file // // Generated by the protocol buffer compiler. -// Source: reflection-v1.proto +// Source: reflection.proto // import GRPC import NIO diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift index 093d1ee2c..281330e5f 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift @@ -2,7 +2,7 @@ // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection-v1.proto +// Source: reflection.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift index 48b9116aa..b68c8530f 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift @@ -3,7 +3,7 @@ // swift-format-ignore-file // // Generated by the protocol buffer compiler. -// Source: reflection-v1alpha.proto +// Source: reflection.proto // import GRPC import NIO diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift index 6d45fdf41..3642c2866 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift @@ -2,12 +2,12 @@ // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection-v1alpha.proto +// Source: reflection.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -// Copyright 2016 gRPC authors. +// Copyright 2016 The gRPC Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ // 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. - // Service exported by server reflection +// Warning: this entire file is deprecated. Use this instead: +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + import Foundation import SwiftProtobuf @@ -79,8 +81,8 @@ struct Grpc_Reflection_V1alpha_ServerReflectionRequest { set {messageRequest = .fileContainingExtension(newValue)} } - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Finds the tag numbers used by all known extensions of extendee_type, and + /// appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the /// reflection service will implement this method, and it's not guaranteed /// that this method will provide all extensions. Returns @@ -120,8 +122,8 @@ struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// Find the proto file which defines an extension extending the given /// message type with the given field number. case fileContainingExtension(Grpc_Reflection_V1alpha_ExtensionRequest) - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. + /// Finds the tag numbers used by all known extensions of extendee_type, and + /// appends them to ExtensionNumberResponse in an undefined order. /// Its corresponding method is best-effort: it's not guaranteed that the /// reflection service will implement this method, and it's not guaranteed /// that this method will provide all extensions. Returns @@ -202,7 +204,7 @@ struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. mutating func clearOriginalRequest() {self._originalRequest = nil} - /// The server set one of the following fields accroding to the message_request + /// The server set one of the following fields according to the message_request /// in the request. var messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse? = nil @@ -249,7 +251,7 @@ struct Grpc_Reflection_V1alpha_ServerReflectionResponse { var unknownFields = SwiftProtobuf.UnknownStorage() - /// The server set one of the following fields accroding to the message_request + /// The server set one of the following fields according to the message_request /// in the request. enum OneOf_MessageResponse: Equatable { /// This message is used to answer file_by_filename, file_containing_symbol, From d95ec7a74c8579a132007a986b02e91fa95eef74 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 1 Feb 2024 16:22:25 +0000 Subject: [PATCH 230/580] Formatting changes in the generated code (#1786) Motivation: Blank lines between code blocks make the code clearer - we want this for the generated code. Modifications: - changed the rendering functions for protocols, extensions and structs to add blank lines between their functions and properties - changed the tests accordingly Result: The generated code is clearer. --- .../Internal/Renderer/TextBasedRenderer.swift | 33 +++++++++++++++---- .../Renderer/TextBasedRendererTests.swift | 2 -- ...lientCodeTranslatorSnippetBasedTests.swift | 17 ++++++++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 1 - ...erverCodeTranslatorSnippetBasedTests.swift | 3 ++ .../ProtobufCodeGeneratorTests.swift | 7 ++-- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 3c9b1845c..2b701f34d 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -144,9 +144,11 @@ struct TextBasedRenderer: RendererProtocol { renderImports(imports) writer.writeLine("") } - for codeBlock in description.codeBlocks { + for (codeBlock, isLast) in description.codeBlocks.enumeratedWithLastMarker() { renderCodeBlock(codeBlock) - writer.writeLine("") + if !isLast { + writer.writeLine("") + } } } @@ -556,8 +558,13 @@ struct TextBasedRenderer: RendererProtocol { writer.nextLineAppendsToLastLine() } writer.writeLine(" {") - for declaration in extensionDescription.declarations { - writer.withNestedLevel { renderDeclaration(declaration) } + for (declaration, isLast) in extensionDescription.declarations.enumeratedWithLastMarker() { + writer.withNestedLevel { + renderDeclaration(declaration) + if !isLast { + writer.writeLine("") + } + } } writer.writeLine("}") } @@ -701,7 +708,14 @@ struct TextBasedRenderer: RendererProtocol { } writer.writeLine(" {") if !structDesc.members.isEmpty { - writer.withNestedLevel { for member in structDesc.members { renderDeclaration(member) } } + writer.withNestedLevel { + for (member, isLast) in structDesc.members.enumeratedWithLastMarker() { + renderDeclaration(member) + if !isLast { + writer.writeLine("") + } + } + } } else { writer.nextLineAppendsToLastLine() } @@ -723,7 +737,14 @@ struct TextBasedRenderer: RendererProtocol { } writer.writeLine(" {") if !protocolDesc.members.isEmpty { - writer.withNestedLevel { for member in protocolDesc.members { renderDeclaration(member) } } + writer.withNestedLevel { + for (member, isLast) in protocolDesc.members.enumeratedWithLastMarker() { + renderDeclaration(member) + if !isLast { + writer.writeLine("") + } + } + } } else { writer.nextLineAppendsToLastLine() } diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 6d40aa56e..5a56d12fa 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -845,7 +845,6 @@ final class Test_TextBasedRenderer: XCTestCase { import Foo struct Bar {} - """# ) } @@ -871,7 +870,6 @@ final class Test_TextBasedRenderer: XCTestCase { struct Bar { struct Baz {} } - """#, indentation: 2 ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 20045e7f2..8bf9fb58a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -68,9 +68,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } + /// Documentation for MethodA public func methodA( request: ClientRequest.Single, @@ -139,9 +141,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } + /// Documentation for MethodA public func methodA( request: ClientRequest.Stream, @@ -210,9 +214,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } + /// Documentation for MethodA public func methodA( request: ClientRequest.Single, @@ -281,9 +287,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } + /// Documentation for MethodA public func methodA( request: ClientRequest.Stream, @@ -343,6 +351,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { deserializer: some MessageDeserializer, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable + /// Documentation for MethodB func methodB( request: ClientRequest.Single, @@ -363,6 +372,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { body ) } + package func methodB( request: ClientRequest.Single, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R @@ -378,9 +388,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA package struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient + package init(client: GRPCCore.GRPCClient) { self.client = client } + /// Documentation for MethodA package func methodA( request: ClientRequest.Stream, @@ -396,6 +408,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { handler: body ) } + /// Documentation for MethodB package func methodB( request: ClientRequest.Single, @@ -464,9 +477,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA internal struct ServiceAClient: ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient + internal init(client: GRPCCore.GRPCClient) { self.client = client } + /// Documentation for MethodA internal func methodA( request: ClientRequest.Single, @@ -522,6 +537,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } @@ -537,6 +553,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Line 2 public struct ServiceBClient: ServiceB.ClientProtocol { private let client: GRPCCore.GRPCClient + public init(client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 58a10cb3d..4ddd3f612 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -193,7 +193,6 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. extension NamespaceA.ServiceA.ServiceProtocol { } - """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest( diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 3f13ec0c6..e8b21c7f1 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -330,6 +330,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + /// Documentation for outputStreamingMethod func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -358,6 +359,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + /// Documentation for outputStreamingMethod func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } @@ -367,6 +369,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } + internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 88c113106..91bc4556a 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -104,9 +104,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. internal struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient + internal init(client: GRPCCore.GRPCClient) { self.client = client } + /// Sends a greeting. internal func sayHello( request: ClientRequest.Single, @@ -123,7 +125,6 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } } - """ ) @@ -212,7 +213,6 @@ final class ProtobufCodeGeneratorTests: XCTestCase { return ServerResponse.Stream(single: response) } } - """ ) try testCodeGeneration( @@ -331,9 +331,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. package struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient + package init(client: GRPCCore.GRPCClient) { self.client = client } + /// Sends a greeting. package func sayHello( request: ClientRequest.Single, @@ -350,7 +352,6 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } } - """ ) } From 757e135adc83efe6a0b02fb660ec7bcc703d08ec Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:38:09 +0000 Subject: [PATCH 231/580] [CodeGenLib] Change location of method descriptor array in generated code (#1789) Motivation: The method descriptor array should reside inside the Methods enum (which should be called Method). Modifications: - added the descriptor array as a member of the Methods enum - chenged the Methods enum name to Method - changed the tests accordingly Result: Better organized generated code. --- .../Translator/TypealiasTranslator.swift | 22 +++-- ...uredSwiftTranslatorSnippetBasedTests.swift | 5 +- ...TypealiasTranslatorSnippetBasedTests.swift | 91 +++++++++++-------- .../ProtobufCodeGeneratorTests.swift | 24 ++--- 4 files changed, 79 insertions(+), 63 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 49a92fa33..b2f930cf7 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -23,7 +23,7 @@ /// ```swift /// public enum Echo { /// public enum Echo { -/// public enum Methods { +/// public enum Method { /// public enum Get { /// public typealias Input = Echo_EchoRequest /// public typealias Output = Echo_EchoResponse @@ -36,12 +36,13 @@ /// public static let descriptor = MethodDescriptor(service: "echo.Echo", method: "Collect") /// } /// // ... +/// +/// public static let descriptors: [MethodDescriptor] = [ +/// echo.Echo.Get.descriptor, +/// echo.Echo.Collect.descriptor, +/// // ... +/// ] /// } -/// public static let methods: [MethodDescriptor] = [ -/// echo.Echo.Get.descriptor, -/// echo.Echo.Collect.descriptor, -/// // ... -/// ] /// /// public typealias StreamingServiceProtocol = echo_EchoServiceStreamingProtocol /// public typealias ServiceProtocol = echo_EchoServiceProtocol @@ -119,7 +120,7 @@ extension TypealiasTranslator { accessModifier: self.accessModifier, name: service.name.generatedUpperCase ) - var methodsEnum = EnumDescription(accessModifier: self.accessModifier, name: "Methods") + var methodsEnum = EnumDescription(accessModifier: self.accessModifier, name: "Method") let methods = service.methods // Create the method specific enums. @@ -127,11 +128,12 @@ extension TypealiasTranslator { let methodEnum = self.makeMethodEnum(from: method, in: service) methodsEnum.members.append(methodEnum) } - serviceEnum.members.append(.enum(methodsEnum)) // Create the method descriptor array. let methodDescriptorsDeclaration = self.makeMethodDescriptors(for: service) - serviceEnum.members.append(methodDescriptorsDeclaration) + methodsEnum.members.append(methodDescriptorsDeclaration) + + serviceEnum.members.append(.enum(methodsEnum)) if self.server { // Create the streaming and non-streaming service protocol type aliases. @@ -231,7 +233,7 @@ extension TypealiasTranslator { accessModifier: self.accessModifier, isStatic: true, kind: .let, - left: .identifier(.pattern("methods")), + left: .identifier(.pattern("descriptors")), type: .array(.member("MethodDescriptor")), right: .literal(.array(methodDescriptors)) ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 4ddd3f612..4a706f0b4 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -172,8 +172,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public enum NamespaceA { public enum ServiceA { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 1fcf39d61..a3feefba0 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -48,7 +48,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ public enum NamespaceA { public enum ServiceA { - public enum Methods { + public enum Method { public enum MethodA { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse @@ -57,10 +57,10 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { method: "MethodA" ) } + public static let descriptors: [MethodDescriptor] = [ + Methods.MethodA.descriptor + ] } - public static let methods: [MethodDescriptor] = [ - Methods.MethodA.descriptor - ] public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol @@ -93,8 +93,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ public enum NamespaceA { public enum ServiceA { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol @@ -127,8 +128,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ public enum NamespaceA { public enum ServiceA { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } @@ -159,8 +161,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ public enum NamespaceA { public enum ServiceA { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol public typealias Client = NamespaceA_ServiceAClient } @@ -191,8 +194,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ public enum NamespaceA { public enum ServiceA { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } } } """ @@ -224,7 +228,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum ServiceA { - public enum Methods { + public enum Method { public enum MethodA { public typealias Input = ServiceARequest public typealias Output = ServiceAResponse @@ -233,10 +237,10 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { method: "MethodA" ) } + public static let descriptors: [MethodDescriptor] = [ + Methods.MethodA.descriptor + ] } - public static let methods: [MethodDescriptor] = [ - Methods.MethodA.descriptor - ] public typealias StreamingServiceProtocol = ServiceAServiceStreamingProtocol public typealias ServiceProtocol = ServiceAServiceProtocol public typealias ClientProtocol = ServiceAClientProtocol @@ -284,7 +288,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ public enum NamespaceA { public enum ServiceA { - public enum Methods { + public enum Method { public enum MethodA { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse @@ -301,11 +305,11 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { method: "MethodB" ) } + public static let descriptors: [MethodDescriptor] = [ + Methods.MethodA.descriptor, + Methods.MethodB.descriptor + ] } - public static let methods: [MethodDescriptor] = [ - Methods.MethodA.descriptor, - Methods.MethodB.descriptor - ] public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol @@ -338,8 +342,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ package enum NamespaceA { package enum ServiceA { - package enum Methods {} - package static let methods: [MethodDescriptor] = [] + package enum Method { + package static let descriptors: [MethodDescriptor] = [] + } package typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol @@ -384,16 +389,18 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ public enum NamespaceA { public enum Aservice { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias StreamingServiceProtocol = NamespaceA_AserviceServiceStreamingProtocol public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol public typealias ClientProtocol = NamespaceA_AserviceClientProtocol public typealias Client = NamespaceA_AserviceClient } public enum Bservice { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias StreamingServiceProtocol = NamespaceA_BserviceServiceStreamingProtocol public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol public typealias ClientProtocol = NamespaceA_BserviceClientProtocol @@ -429,16 +436,18 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ package enum AService { - package enum Methods {} - package static let methods: [MethodDescriptor] = [] + package enum Method { + package static let descriptors: [MethodDescriptor] = [] + } package typealias StreamingServiceProtocol = AServiceServiceStreamingProtocol package typealias ServiceProtocol = AServiceServiceProtocol package typealias ClientProtocol = AServiceClientProtocol package typealias Client = AServiceClient } package enum BService { - package enum Methods {} - package static let methods: [MethodDescriptor] = [] + package enum Method { + package static let descriptors: [MethodDescriptor] = [] + } package typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol package typealias ServiceProtocol = BServiceServiceProtocol package typealias ClientProtocol = BServiceClientProtocol @@ -482,8 +491,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { """ internal enum Anamespace { internal enum AService { - internal enum Methods {} - internal static let methods: [MethodDescriptor] = [] + internal enum Method { + internal static let descriptors: [MethodDescriptor] = [] + } internal typealias StreamingServiceProtocol = Anamespace_AServiceServiceStreamingProtocol internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol internal typealias ClientProtocol = Anamespace_AServiceClientProtocol @@ -492,8 +502,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { } internal enum Bnamespace { internal enum BService { - internal enum Methods {} - internal static let methods: [MethodDescriptor] = [] + internal enum Method { + internal static let descriptors: [MethodDescriptor] = [] + } internal typealias StreamingServiceProtocol = Bnamespace_BServiceServiceStreamingProtocol internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol @@ -531,8 +542,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum BService { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol public typealias ServiceProtocol = BServiceServiceProtocol public typealias ClientProtocol = BServiceClientProtocol @@ -540,8 +552,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { } public enum Anamespace { public enum AService { - public enum Methods {} - public static let methods: [MethodDescriptor] = [] + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } public typealias StreamingServiceProtocol = Anamespace_AServiceServiceStreamingProtocol public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol public typealias ClientProtocol = Anamespace_AServiceClientProtocol diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 91bc4556a..28b40aaa2 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -58,7 +58,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal enum Helloworld { internal enum Greeter { - internal enum Methods { + internal enum Method { internal enum SayHello { internal typealias Input = HelloRequest internal typealias Output = HelloReply @@ -67,10 +67,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { method: "SayHello" ) } + internal static let descriptors: [MethodDescriptor] = [ + Methods.SayHello.descriptor + ] } - internal static let methods: [MethodDescriptor] = [ - Methods.SayHello.descriptor - ] internal typealias ClientProtocol = Helloworld_GreeterClientProtocol internal typealias Client = Helloworld_GreeterClient } @@ -162,7 +162,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { public enum Helloworld { public enum Greeter { - public enum Methods { + public enum Method { public enum SayHello { public typealias Input = HelloRequest public typealias Output = HelloReply @@ -171,10 +171,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { method: "SayHello" ) } + public static let descriptors: [MethodDescriptor] = [ + Methods.SayHello.descriptor + ] } - public static let methods: [MethodDescriptor] = [ - Methods.SayHello.descriptor - ] public typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol } @@ -249,7 +249,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package enum Helloworld { package enum Greeter { - package enum Methods { + package enum Method { package enum SayHello { package typealias Input = HelloRequest package typealias Output = HelloReply @@ -258,10 +258,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { method: "SayHello" ) } + package static let descriptors: [MethodDescriptor] = [ + Methods.SayHello.descriptor + ] } - package static let methods: [MethodDescriptor] = [ - Methods.SayHello.descriptor - ] package typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol package typealias ClientProtocol = Helloworld_GreeterClientProtocol From 2fa9d9a9164c1f633f5c21207425b97500566e49 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 5 Feb 2024 15:09:30 +0000 Subject: [PATCH 232/580] [CodeGenLib] Fixes for the generated code (#1790) Motivation: - the protocol name for the streaming service used in the typealias does not match the actual streaming service protocol name - we have already replaced the Methods enum name with "Method" so this change has to be reflected in all places where properties of the enum are used - the first argument label of the registerHandler() method is "forMethod" and not "for" Modifications: Modified all the translators that handle these methods or names and their tests. Result: The generated code will be compiled. --- .../Translator/ClientCodeTranslator.swift | 32 +-- .../Translator/ServerCodeTranslator.swift | 20 +- .../Translator/TypealiasTranslator.swift | 10 +- ...lientCodeTranslatorSnippetBasedTests.swift | 184 +++++++++--------- ...uredSwiftTranslatorSnippetBasedTests.swift | 2 +- ...erverCodeTranslatorSnippetBasedTests.swift | 82 ++++---- ...TypealiasTranslatorSnippetBasedTests.swift | 36 ++-- .../ProtobufCodeGeneratorTests.swift | 86 ++++---- 8 files changed, 227 insertions(+), 225 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 7722663df..924415b96 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -24,21 +24,21 @@ /// ```swift /// public protocol Foo_BarClientProtocol: Sendable { /// func baz( -/// request: ClientRequest.Single, -/// serializer: some MessageSerializer, -/// deserializer: some MessageDeserializer, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R -/// ) async throws -> ServerResponse.Stream +/// request: ClientRequest.Single, +/// serializer: some MessageSerializer, +/// deserializer: some MessageDeserializer, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async throws -> ServerResponse.Stream /// } /// extension Foo.Bar.ClientProtocol { /// public func get( -/// request: ClientRequest.Single, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// request: ClientRequest.Single, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R /// ) async rethrows -> R { /// try await self.baz( /// request: request, -/// serializer: ProtobufSerializer(), -/// deserializer: ProtobufDeserializer(), +/// serializer: ProtobufSerializer(), +/// deserializer: ProtobufDeserializer(), /// body /// ) /// } @@ -48,14 +48,14 @@ /// self.client = client /// } /// public func methodA( -/// request: ClientRequest.Stream, -/// serializer: some MessageSerializer, -/// deserializer: some MessageDeserializer, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// request: ClientRequest.Stream, +/// serializer: some MessageSerializer, +/// deserializer: some MessageDeserializer, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R /// ) async rethrows -> R { /// try await self.client.clientStreaming( /// request: request, -/// descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, +/// descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, /// serializer: serializer, /// deserializer: deserializer, /// handler: body @@ -399,7 +399,7 @@ extension ClientCodeTranslator { .init( label: "descriptor", expression: .identifierPattern( - "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase).descriptor" + "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" ) ), .init(label: "serializer", expression: .identifierPattern("serializer")), @@ -439,7 +439,7 @@ extension ClientCodeTranslator { type: InputOutputType ) -> String { var components: String = - "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase)" + "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase)" switch type { case .input: diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index feb858a3c..a20067ee3 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -31,23 +31,23 @@ /// extension foo.Bar.StreamingServiceProtocol { /// public func registerRPCs(with router: inout RPCRouter) { /// router.registerHandler( -/// for: foo.Method.baz.descriptor, -/// deserializer: ProtobufDeserializer(), -/// serializer: ProtobufSerializer(), +/// forMethod: foo.Method.baz.descriptor, +/// deserializer: ProtobufDeserializer(), +/// serializer: ProtobufSerializer(), /// handler: { request in try await self.baz(request: request) } /// ) /// } /// } /// public protocol foo_BarServiceProtocol: foo.Bar.StreamingServiceProtocol { /// func baz( -/// request: ServerRequest.Single -/// ) async throws -> ServerResponse.Single +/// request: ServerRequest.Single +/// ) async throws -> ServerResponse.Single /// } /// // Generated partial conformance to `foo_BarStreamingServiceProtocol`. /// extension foo.Bar.ServiceProtocol { /// public func baz( -/// request: ServerRequest.Stream -/// ) async throws -> ServerResponse.Stream { +/// request: ServerRequest.Stream +/// ) async throws -> ServerResponse.Stream { /// let response = try await self.baz(request: ServerRequest.Single(stream: request) /// return ServerResponse.Stream(single: response) /// } @@ -221,7 +221,7 @@ extension ServerCodeTranslator { var arguments = [FunctionArgumentDescription]() arguments.append( .init( - label: "for", + label: "forMethod", expression: .identifierPattern( self.methodDescriptorPath(for: method, service: service) ) @@ -460,7 +460,7 @@ extension ServerCodeTranslator { type: InputOutputType ) -> String { var components: String = - "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase)" + "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase)" switch type { case .input: @@ -478,7 +478,7 @@ extension ServerCodeTranslator { service: CodeGenerationRequest.ServiceDescriptor ) -> String { return - "\(service.namespacedTypealiasGeneratedName).Methods.\(method.name.generatedUpperCase).descriptor" + "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" } /// Generates the fully qualified name of the type alias for a service protocol. diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index b2f930cf7..3d09dd525 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -38,8 +38,8 @@ /// // ... /// /// public static let descriptors: [MethodDescriptor] = [ -/// echo.Echo.Get.descriptor, -/// echo.Echo.Collect.descriptor, +/// Echo.Echo.Method.Get.descriptor, +/// Echo.Echo.Method.Collect.descriptor, /// // ... /// ] /// } @@ -222,7 +222,9 @@ extension TypealiasTranslator { for methodName in methodNames { let methodDescriptorPath = Expression.memberAccess( MemberAccessDescription( - left: .identifierType(.member(["Methods", methodName])), + left: .identifierType( + .member([service.namespacedTypealiasGeneratedName, "Method", methodName]) + ), right: "descriptor" ) ) @@ -245,7 +247,7 @@ extension TypealiasTranslator { let streamingServiceProtocolTypealias = Declaration.typealias( accessModifier: self.accessModifier, name: "StreamingServiceProtocol", - existingType: .member("\(service.namespacedGeneratedName)ServiceStreamingProtocol") + existingType: .member("\(service.namespacedGeneratedName)StreamingServiceProtocol") ) let serviceProtocolTypealias = Declaration.typealias( accessModifier: self.accessModifier, diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 8bf9fb58a..670895e26 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -46,21 +46,21 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -75,14 +75,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, + descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -119,21 +119,21 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -148,14 +148,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, - descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, + descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -192,21 +192,21 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -221,14 +221,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, - descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, + descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -265,21 +265,21 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } extension NamespaceA.ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -294,14 +294,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, - descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, + descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -317,7 +317,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } - func testClientCodeTranslatorMultipleMethods() throws { + func testClientCodeTranslatorMultipleMethod() throws { let methodA = MethodDescriptor( documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), @@ -346,41 +346,41 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Documentation for MethodB func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } extension NamespaceA.ServiceA.ClientProtocol { package func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } package func methodB( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodB( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -395,14 +395,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA package func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, - descriptor: NamespaceA.ServiceA.Methods.MethodA.descriptor, + descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -411,14 +411,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodB package func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, - descriptor: NamespaceA.ServiceA.Methods.MethodB.descriptor, + descriptor: NamespaceA.ServiceA.Method.MethodB.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -455,21 +455,21 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } extension ServiceA.ClientProtocol { internal func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -484,14 +484,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA internal func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: ServiceA.Methods.MethodA.descriptor, + descriptor: ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 4a706f0b4..e34a197b0 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -175,7 +175,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index e8b21c7f1..bbf8090bb 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -53,15 +53,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod - func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: NamespaceA.ServiceA.Methods.Unary.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA.ServiceA.Method.Unary.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unary(request: request) } @@ -71,11 +71,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod - func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. extension NamespaceA.ServiceA.ServiceProtocol { - public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unary(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -117,15 +117,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: NamespaceA.ServiceA.Methods.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA.ServiceA.Method.InputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } @@ -135,11 +135,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. extension NamespaceA.ServiceA.ServiceProtocol { - package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } @@ -185,15 +185,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: NamespaceA.ServiceA.Methods.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA.ServiceA.Method.OutputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -203,11 +203,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. extension NamespaceA.ServiceA.ServiceProtocol { - public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } @@ -253,15 +253,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: NamespaceA.ServiceA.Methods.BidirectionalStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA.ServiceA.Method.BidirectionalStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.bidirectionalStreaming(request: request) } @@ -271,7 +271,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. extension NamespaceA.ServiceA.ServiceProtocol { @@ -329,26 +329,26 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: NamespaceA.ServiceA.Methods.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA.ServiceA.Method.InputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } ) router.registerHandler( - for: NamespaceA.ServiceA.Methods.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA.ServiceA.Method.OutputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -358,19 +358,19 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. extension NamespaceA.ServiceA.ServiceProtocol { - internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } - internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } @@ -408,15 +408,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA - func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension ServiceA.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: ServiceA.Methods.MethodA.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: ServiceA.Method.MethodA.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.methodA(request: request) } @@ -426,11 +426,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceA internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA - func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single + func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. extension ServiceA.ServiceProtocol { - internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.methodA(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index a3feefba0..d74278d1d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -58,10 +58,10 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - Methods.MethodA.descriptor + NamespaceA.ServiceA.Method.MethodA.descriptor ] } - public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol public typealias Client = NamespaceA_ServiceAClient @@ -96,7 +96,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol public typealias Client = NamespaceA_ServiceAClient @@ -131,7 +131,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } } @@ -238,10 +238,10 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - Methods.MethodA.descriptor + ServiceA.Method.MethodA.descriptor ] } - public typealias StreamingServiceProtocol = ServiceAServiceStreamingProtocol + public typealias StreamingServiceProtocol = ServiceAStreamingServiceProtocol public typealias ServiceProtocol = ServiceAServiceProtocol public typealias ClientProtocol = ServiceAClientProtocol public typealias Client = ServiceAClient @@ -306,11 +306,11 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - Methods.MethodA.descriptor, - Methods.MethodB.descriptor + NamespaceA.ServiceA.Method.MethodA.descriptor, + NamespaceA.ServiceA.Method.MethodB.descriptor ] } - public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol public typealias Client = NamespaceA_ServiceAClient @@ -345,7 +345,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [MethodDescriptor] = [] } - package typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol package typealias Client = NamespaceA_ServiceAClient @@ -392,7 +392,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = NamespaceA_AserviceServiceStreamingProtocol + public typealias StreamingServiceProtocol = NamespaceA_AserviceStreamingServiceProtocol public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol public typealias ClientProtocol = NamespaceA_AserviceClientProtocol public typealias Client = NamespaceA_AserviceClient @@ -401,7 +401,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = NamespaceA_BserviceServiceStreamingProtocol + public typealias StreamingServiceProtocol = NamespaceA_BserviceStreamingServiceProtocol public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol public typealias ClientProtocol = NamespaceA_BserviceClientProtocol public typealias Client = NamespaceA_BserviceClient @@ -439,7 +439,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [MethodDescriptor] = [] } - package typealias StreamingServiceProtocol = AServiceServiceStreamingProtocol + package typealias StreamingServiceProtocol = AServiceStreamingServiceProtocol package typealias ServiceProtocol = AServiceServiceProtocol package typealias ClientProtocol = AServiceClientProtocol package typealias Client = AServiceClient @@ -448,7 +448,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [MethodDescriptor] = [] } - package typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol + package typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol package typealias ServiceProtocol = BServiceServiceProtocol package typealias ClientProtocol = BServiceClientProtocol package typealias Client = BServiceClient @@ -494,7 +494,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } - internal typealias StreamingServiceProtocol = Anamespace_AServiceServiceStreamingProtocol + internal typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol internal typealias ClientProtocol = Anamespace_AServiceClientProtocol internal typealias Client = Anamespace_AServiceClient @@ -505,7 +505,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } - internal typealias StreamingServiceProtocol = Bnamespace_BServiceServiceStreamingProtocol + internal typealias StreamingServiceProtocol = Bnamespace_BServiceStreamingServiceProtocol internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol internal typealias Client = Bnamespace_BServiceClient @@ -545,7 +545,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = BServiceServiceStreamingProtocol + public typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol public typealias ServiceProtocol = BServiceServiceProtocol public typealias ClientProtocol = BServiceClientProtocol public typealias Client = BServiceClient @@ -555,7 +555,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = Anamespace_AServiceServiceStreamingProtocol + public typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol public typealias ClientProtocol = Anamespace_AServiceClientProtocol public typealias Client = Anamespace_AServiceClient diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 28b40aaa2..bcea02742 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -68,7 +68,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } internal static let descriptors: [MethodDescriptor] = [ - Methods.SayHello.descriptor + Helloworld.Greeter.Method.SayHello.descriptor ] } internal typealias ClientProtocol = Helloworld_GreeterClientProtocol @@ -80,22 +80,22 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } extension Helloworld.Greeter.ClientProtocol { internal func sayHello( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -111,14 +111,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. internal func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: Helloworld.Greeter.Methods.SayHello.descriptor, + descriptor: Helloworld.Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -172,10 +172,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - Methods.SayHello.descriptor + Helloworld.Greeter.Method.SayHello.descriptor ] } - public typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol + public typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol } } @@ -183,16 +183,16 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension Helloworld.Greeter.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: Helloworld.Greeter.Methods.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: Helloworld.Greeter.Method.SayHello.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -203,12 +203,12 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. public protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. extension Helloworld.Greeter.ServiceProtocol { - public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -259,10 +259,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } package static let descriptors: [MethodDescriptor] = [ - Methods.SayHello.descriptor + Helloworld.Greeter.Method.SayHello.descriptor ] } - package typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol + package typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol package typealias ClientProtocol = Helloworld_GreeterClientProtocol package typealias Client = Helloworld_GreeterClient @@ -272,16 +272,16 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. package protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension Helloworld.Greeter.StreamingServiceProtocol { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - for: Helloworld.Greeter.Methods.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: Helloworld.Greeter.Method.SayHello.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -292,12 +292,12 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. package protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. extension Helloworld.Greeter.ServiceProtocol { - package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -307,22 +307,22 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } extension Helloworld.Greeter.ClientProtocol { package func sayHello( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -338,14 +338,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. package func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: Helloworld.Greeter.Methods.SayHello.descriptor, + descriptor: Helloworld.Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, handler: body From 0daf4a1710b5d6bee4e750b4136df64354d834d9 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:00:22 +0000 Subject: [PATCH 233/580] [CodeGenLib] Add avialability gurads in generated code (#1791) Motivation: The protocols from GRPCCore that structs and protocols from the generated code conform to can be used only for some OSes, so we need to add the same availability guards in the generated code. Modifications: - created the AvailabilityDescription struct in StructuredSwiftRepresentation - created the declaraion for a guarded declaration - created the rendering functions for guarded declarations and the availability guard - added the guarded declaration in the translators where needed - changed the tests accordingly Result: The generated code will contain the necessary availability guards. --- .../Internal/Renderer/TextBasedRenderer.swift | 17 ++++++++ .../StructuredSwiftRepresentation.swift | 43 +++++++++++++++++++ .../Translator/ClientCodeTranslator.swift | 15 ++++--- .../Translator/ServerCodeTranslator.swift | 10 ++++- .../Translator/SpecializedTranslator.swift | 9 ++++ .../Renderer/TextBasedRendererTests.swift | 15 +++++++ ...lientCodeTranslatorSnippetBasedTests.swift | 8 ++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 2 + ...erverCodeTranslatorSnippetBasedTests.swift | 16 +++++++ .../ProtobufCodeGeneratorTests.swift | 6 +++ 10 files changed, 133 insertions(+), 8 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 2b701f34d..eb3abe149 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -818,6 +818,8 @@ struct TextBasedRenderer: RendererProtocol { renderCommentableDeclaration(comment: comment, declaration: nestedDeclaration) case let .deprecated(deprecation, nestedDeclaration): renderDeprecatedDeclaration(deprecation: deprecation, declaration: nestedDeclaration) + case let .guarded(availability, nestedDeclaration): + renderGuardedDeclaration(availability: availability, declaration: nestedDeclaration) case .variable(let variableDescription): renderVariable(variableDescription) case .extension(let extensionDescription): renderExtension(extensionDescription) case .struct(let structDescription): renderStruct(structDescription) @@ -1074,6 +1076,21 @@ struct TextBasedRenderer: RendererProtocol { writer.writeLine(line) } + /// Renders the specified declaration with an availability guard annotation. + func renderGuardedDeclaration(availability: AvailabilityDescription, declaration: Declaration) { + renderAvailability(availability) + renderDeclaration(declaration) + } + + func renderAvailability(_ availability: AvailabilityDescription) { + var line = "@available(" + for osVersion in availability.osVersions { + line.append("\(osVersion.os.name) \(osVersion.version), ") + } + line.append("*)") + writer.writeLine(line) + } + /// Renders the specified code block item. func renderCodeBlockItem(_ description: CodeBlockItem) { switch description { diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 7a91fcd94..e40b8eb5c 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -764,6 +764,9 @@ indirect enum Declaration: Equatable, Codable { /// A declaration that adds a comment on top of the provided declaration. case deprecated(DeprecationDescription, Declaration) + /// A declaration that adds an availability guard on top of the provided declaration. + case guarded(AvailabilityDescription, Declaration) + /// A variable declaration. case variable(VariableDescription) @@ -801,6 +804,42 @@ struct DeprecationDescription: Equatable, Codable { var renamed: String? } +/// A description of an availability guard. +/// +/// For example: `@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)` +struct AvailabilityDescription: Equatable, Codable { + /// The array of OSes and versions which are specified in the availability guard. + var osVersions: [OSVersion] + init(osVersions: [OSVersion]) { + self.osVersions = osVersions + } + + /// An OS and its version. + struct OSVersion: Equatable, Codable { + var os: OS + var version: String + init(os: OS, version: String) { + self.os = os + self.version = version + } + } + + /// One of the possible OSes. + // swift-format-ignore: DontRepeatTypeInStaticProperties + struct OS: Equatable, Codable { + var name: String + + init(name: String) { + self.name = name + } + + static let macOS = Self(name: "macOS") + static let iOS = Self(name: "iOS") + static let watchOS = Self(name: "watchOS") + static let tvOS = Self(name: "tvOS") + } +} + /// A description of an assignment expression. /// /// For example: `foo = 42`. @@ -1803,6 +1842,7 @@ extension Declaration { switch self { case .commentable(_, let declaration): return declaration.accessModifier case .deprecated(_, let declaration): return declaration.accessModifier + case .guarded(_, let declaration): return declaration.accessModifier case .variable(let variableDescription): return variableDescription.accessModifier case .extension(let extensionDescription): return extensionDescription.accessModifier case .struct(let structDescription): return structDescription.accessModifier @@ -1821,6 +1861,9 @@ extension Declaration { case .deprecated(let deprecationDescription, var declaration): declaration.accessModifier = newValue self = .deprecated(deprecationDescription, declaration) + case .guarded(let availability, var declaration): + declaration.accessModifier = newValue + self = .guarded(availability, declaration) case .variable(var variableDescription): variableDescription.accessModifier = newValue self = .variable(variableDescription) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 924415b96..a669c8f41 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -332,12 +332,15 @@ extension ClientCodeTranslator { ) } - return .struct( - StructDescription( - accessModifier: self.accessModifier, - name: "\(service.namespacedGeneratedName)Client", - conformances: ["\(service.namespacedTypealiasGeneratedName).ClientProtocol"], - members: [clientProperty, initializer] + methods + return .guarded( + self.availabilityGuard, + .struct( + StructDescription( + accessModifier: self.accessModifier, + name: "\(service.namespacedGeneratedName)Client", + conformances: ["\(service.namespacedTypealiasGeneratedName).ClientProtocol"], + members: [clientProperty, initializer] + methods + ) ) ) } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index a20067ee3..eebfd59e1 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -125,7 +125,10 @@ extension ServerCodeTranslator { ) ) - return .commentable(.preFormatted(service.documentation), streamingProtocol) + return .commentable( + .preFormatted(service.documentation), + .guarded(self.availabilityGuard, streamingProtocol) + ) } private func makeStreamingMethodSignature( @@ -188,7 +191,10 @@ extension ServerCodeTranslator { ] ) let registerRPCsBody = self.makeRegisterRPCsMethodBody(for: service, in: codeGenerationRequest) - return .function(signature: registerRPCsSignature, body: registerRPCsBody) + return .guarded( + self.availabilityGuard, + .function(signature: registerRPCsSignature, body: registerRPCsBody) + ) } private func makeRegisterRPCsMethodBody( diff --git a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift index ff73a9481..b73a75c95 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift @@ -46,4 +46,13 @@ extension SpecializedTranslator { } } } + + internal var availabilityGuard: AvailabilityDescription { + AvailabilityDescription(osVersions: [ + .init(os: .macOS, version: "13.0"), + .init(os: .iOS, version: "16.0"), + .init(os: .watchOS, version: "9.0"), + .init(os: .tvOS, version: "16.0"), + ]) + } } diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 5a56d12fa..422673e8b 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -667,6 +667,21 @@ final class Test_TextBasedRenderer: XCTestCase { ) } + func testAvailability() throws { + try _test( + .init(osVersions: [ + .init(os: .macOS, version: "12.0"), + .init(os: .iOS, version: "13.1.2"), + .init(os: .watchOS, version: "8.1.2"), + .init(os: .tvOS, version: "15.0.2"), + ]), + renderedBy: TextBasedRenderer.renderAvailability, + rendersAs: #""" + @available(macOS 12.0, iOS 13.1.2, watchOS 8.1.2, tvOS 15.0.2, *) + """# + ) + } + func testBindingKind() throws { try _test( .var, diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 670895e26..a68bdd5da 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -66,6 +66,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -139,6 +140,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -212,6 +214,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -285,6 +288,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -386,6 +390,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -475,6 +480,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal struct ServiceAClient: ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -535,6 +541,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { extension NamespaceA.ServiceA.ClientProtocol { } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -551,6 +558,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceB /// /// Line 2 + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct ServiceBClient: ServiceB.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index e34a197b0..59016e642 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -181,10 +181,12 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for AService + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index bbf8090bb..731595fcc 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -51,12 +51,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA.ServiceA.Method.Unary.descriptor, @@ -115,12 +117,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA.ServiceA.Method.InputStreaming.descriptor, @@ -183,12 +187,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA.ServiceA.Method.OutputStreaming.descriptor, @@ -251,12 +257,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA.ServiceA.Method.BidirectionalStreaming.descriptor, @@ -327,6 +335,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -336,6 +345,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA.ServiceA.Method.InputStreaming.descriptor, @@ -406,12 +416,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. extension ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: ServiceA.Method.MethodA.descriptor, @@ -468,9 +480,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceA.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceA @@ -479,9 +493,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { extension NamespaceA.ServiceA.ServiceProtocol { } /// Documentation for ServiceB + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA.ServiceB.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceB diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index bcea02742..d6bb08059 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -102,6 +102,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -181,6 +182,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -188,6 +190,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Conformance to `GRPCCore.RegistrableRPCService`. extension Helloworld.Greeter.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Helloworld.Greeter.Method.SayHello.descriptor, @@ -270,6 +273,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -277,6 +281,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Conformance to `GRPCCore.RegistrableRPCService`. extension Helloworld.Greeter.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Helloworld.Greeter.Method.SayHello.descriptor, @@ -329,6 +334,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient From 0a7f75fade7e332ce40daaba4199555ab90058e7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 6 Feb 2024 14:36:39 +0000 Subject: [PATCH 234/580] Add service and method config (#1788) Motivation: When resoling addresses, gRPC clients can discover configuration to use published by service maintainers. This is in the form of 'service config' and typically encoded as JSON. However the structure of the config is a translation of the service config protobuf message using the standard proto to JSON mappings. This change expands on the existing `MethodConfiguration` to allow it to be decoded from JSON and adds a `ServiceConfiguration` which can also be decoded from JSON. Modifications: - Add additional properties to `MethodConfiguration` and make it `Codabale` - Add `ServiceConfiguration` and make it `Codabale` - Add a `RuntimeError` - Add the `service_config.proto` message and its dependencies and generate these messages into the test module. Result: Service config and method config are more fully featured --- Package.swift | 3 +- Protos/generate.sh | 15 + .../Configuration/MethodConfiguration.swift | 676 +++ .../Configuration/ServiceConfiguration.swift | 278 ++ Sources/GRPCCore/GRPCClient.swift | 2 +- Sources/GRPCCore/MethodConfiguration.swift | 290 -- Sources/GRPCCore/RuntimeError.swift | 128 + .../ClientRPCExecutorTestHarness.swift | 5 +- .../ClientRPCExecutorTests+Hedging.swift | 2 +- .../ClientRPCExecutorTests+Retries.swift | 6 +- .../Call/Client/RetryDelaySequenceTests.swift | 4 +- .../Configuration/Generated/code.pb.swift | 307 ++ .../Configuration/Generated/rls.pb.swift | 253 + .../Generated/rls_config.pb.swift | 707 +++ .../Generated/service_config.pb.swift | 4149 +++++++++++++++++ .../MethodConfigurationCodingTests.swift | 427 ++ .../MethodConfigurationTests.swift | 0 .../ServiceConfigurationCodingTests.swift | 261 ++ .../MethodConfigurationsTests.swift | 12 +- .../InProcessClientTransportTests.swift | 6 +- 20 files changed, 7220 insertions(+), 311 deletions(-) create mode 100644 Sources/GRPCCore/Configuration/MethodConfiguration.swift create mode 100644 Sources/GRPCCore/Configuration/ServiceConfiguration.swift delete mode 100644 Sources/GRPCCore/MethodConfiguration.swift create mode 100644 Sources/GRPCCore/RuntimeError.swift create mode 100644 Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift create mode 100644 Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift create mode 100644 Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift create mode 100644 Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift create mode 100644 Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift rename Tests/GRPCCoreTests/{ => Configuration}/MethodConfigurationTests.swift (100%) create mode 100644 Tests/GRPCCoreTests/Configuration/ServiceConfigurationCodingTests.swift diff --git a/Package.swift b/Package.swift index b864f5944..f21b2a5e2 100644 --- a/Package.swift +++ b/Package.swift @@ -286,7 +286,8 @@ extension Target { .grpcCore, .grpcInProcessTransport, .dequeModule, - .atomics + .atomics, + .protobuf, ] ) diff --git a/Protos/generate.sh b/Protos/generate.sh index 22a916417..686ef632d 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -172,6 +172,20 @@ function generate_reflection_data_example { done } +function generate_rpc_code_for_tests { + local protos=( + "$here/upstream/grpc/service_config/service_config.proto" + "$here/upstream/grpc/lookup/v1/rls.proto" + "$here/upstream/grpc/lookup/v1/rls_config.proto" + "$here/upstream/google/rpc/code.proto" + ) + local output="$root/Tests/GRPCCoreTests/Configuration/Generated" + + for proto in "${protos[@]}"; do + generate_message "$proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=DropPath" + done +} + #------------------------------------------------------------------------------ # Examples @@ -187,3 +201,4 @@ generate_echo_reflection_data_for_tests # Misc. tests generate_normalization_for_tests +generate_rpc_code_for_tests diff --git a/Sources/GRPCCore/Configuration/MethodConfiguration.swift b/Sources/GRPCCore/Configuration/MethodConfiguration.swift new file mode 100644 index 000000000..0e0af5d1b --- /dev/null +++ b/Sources/GRPCCore/Configuration/MethodConfiguration.swift @@ -0,0 +1,676 @@ +/* + * Copyright 2023, 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. + */ + +/// Configuration values for executing an RPC. +/// +/// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct MethodConfiguration: Hashable, Sendable { + public struct Name: Sendable, Hashable { + /// The name of the service, including the namespace. + /// + /// If the service is empty then `method` must also be empty and the configuration specifies + /// defaults for all methods. + /// + /// - Precondition: If `service` is empty then `method` must also be empty. + public var service: String { + didSet { try! self.validate() } + } + + /// The name of the method. + /// + /// If the method is empty then the configuration will be the default for all methods in the + /// specified service. + public var method: String + + /// Create a new name. + /// + /// If the service is empty then `method` must also be empty and the configuration specifies + /// defaults for all methods. If only `method` is empty then the configuration applies to + /// all methods in the `service`. + /// + /// - Parameters: + /// - service: The name of the service, including the namespace. + /// - method: The name of the method. + public init(service: String, method: String = "") { + self.service = service + self.method = method + try! self.validate() + } + + private func validate() throws { + if self.service.isEmpty && !self.method.isEmpty { + throw RuntimeError( + code: .invalidArgument, + message: "'method' must be empty if 'service' is empty." + ) + } + } + } + + /// The names of methods which this configuration applies to. + public var names: [Name] + + /// The default timeout for the RPC. + /// + /// If no reply is received in the specified amount of time the request is aborted + /// with an ``RPCError`` with code ``RPCError/Code/deadlineExceeded``. + /// + /// The actual deadline used will be the minimum of the value specified here + /// and the value set by the application by the client API. If either one isn't set + /// then the other value is used. If neither is set then the request has no deadline. + /// + /// The timeout applies to the overall execution of an RPC. If, for example, a retry + /// policy is set then the timeout begins when the first attempt is started and _isn't_ reset + /// when subsequent attempts start. + public var timeout: Duration? + + /// The maximum allowed payload size in bytes for an individual message. + /// + /// If a client attempts to send an object larger than this value, it will not be sent and the + /// client will see an error. Note that 0 is a valid value, meaning that the request message + /// must be empty. + public var maxRequestMessageBytes: Int? + + /// The maximum allowed payload size in bytes for an individual response message. + /// + /// If a server attempts to send an object larger than this value, it will not + /// be sent, and an error will be sent to the client instead. Note that 0 is a valid value, + /// meaning that the response message must be empty. + public var maxResponseMessageBytes: Int? + + /// The policy determining how many times, and when, the RPC is executed. + /// + /// There are two policy types: + /// 1. Retry + /// 2. Hedging + /// + /// The retry policy allows an RPC to be retried a limited number of times if the RPC + /// fails with one of the configured set of status codes. RPCs are only retried if they + /// fail immediately, that is, the first response part received from the server is a + /// status code. + /// + /// The hedging policy allows an RPC to be executed multiple times concurrently. Typically + /// each execution will be staggered by some delay. The first successful response will be + /// reported to the client. Hedging is only suitable for idempotent RPCs. + public var executionPolicy: ExecutionPolicy? + + /// Create an execution configuration. + /// + /// - Parameters: + /// - names: The names of methods this configuration applies to. + /// - timeout: The default timeout for the RPC. + /// - maxRequestMessageBytes: The maximum allowed size of a request message in bytes. + /// - maxResponseMessageBytes: The maximum allowed size of a response message in bytes. + /// - executionPolicy: The execution policy to use for the RPC. + public init( + names: [Name], + timeout: Duration? = nil, + maxRequestMessageBytes: Int? = nil, + maxResponseMessageBytes: Int? = nil, + executionPolicy: ExecutionPolicy? = nil + ) { + self.names = names + self.timeout = timeout + self.maxRequestMessageBytes = maxRequestMessageBytes + self.maxResponseMessageBytes = maxResponseMessageBytes + self.executionPolicy = executionPolicy + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension MethodConfiguration { + /// The execution policy for an RPC. + public enum ExecutionPolicy: Hashable, Sendable { + /// Policy for retrying an RPC. + /// + /// See ``RetryPolicy`` for more details. + case retry(RetryPolicy) + + /// Policy for hedging an RPC. + /// + /// See ``HedgingPolicy`` for more details. + case hedge(HedgingPolicy) + } +} + +/// Policy for retrying an RPC. +/// +/// gRPC retries RPCs when the first response from the server is a status code which matches +/// one of the configured retryable status codes. If the server begins processing the RPC and +/// first responds with metadata and later responds with a retryable status code then the RPC +/// won't be retried. +/// +/// Execution attempts are limited by ``maximumAttempts`` which includes the original attempt. The +/// maximum number of attempts is limited to five. +/// +/// Subsequent attempts are executed after some delay. The first _retry_, or second attempt, will +/// be started after a randomly chosen delay between zero and ``initialBackoff``. More generally, +/// the nth retry will happen after a randomly chosen delay between zero +/// and `min(initialBackoff * backoffMultiplier^(n-1), maximumBackoff)`. +/// +/// For more information see [gRFC A6 Client +/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct RetryPolicy: Hashable, Sendable { + /// The maximum number of RPC attempts, including the original attempt. + /// + /// Must be greater than one, values greater than five are treated as five. + public var maximumAttempts: Int { + didSet { self.maximumAttempts = try! validateMaxAttempts(self.maximumAttempts) } + } + + /// The initial backoff duration. + /// + /// The initial retry will occur after a random amount of time up to this value. + /// + /// - Precondition: Must be greater than zero. + public var initialBackoff: Duration { + willSet { try! Self.validateInitialBackoff(newValue) } + } + + /// The maximum amount of time to backoff for. + /// + /// - Precondition: Must be greater than zero. + public var maximumBackoff: Duration { + willSet { try! Self.validateMaxBackoff(newValue) } + } + + /// The multiplier to apply to backoff. + /// + /// - Precondition: Must be greater than zero. + public var backoffMultiplier: Double { + willSet { try! Self.validateBackoffMultiplier(newValue) } + } + + /// The set of status codes which may be retried. + /// + /// - Precondition: Must not be empty. + public var retryableStatusCodes: Set { + willSet { try! Self.validateRetryableStatusCodes(newValue) } + } + + /// Create a new retry policy. + /// + /// - Parameters: + /// - maximumAttempts: The maximum number of attempts allowed for the RPC. + /// - initialBackoff: The initial backoff period for the first retry attempt. Must be + /// greater than zero. + /// - maximumBackoff: The maximum period of time to wait between attempts. Must be greater than + /// zero. + /// - backoffMultiplier: The exponential backoff multiplier. Must be greater than zero. + /// - retryableStatusCodes: The set of status codes which may be retried. Must not be empty. + /// - Precondition: `maximumAttempts`, `initialBackoff`, `maximumBackoff` and `backoffMultiplier` + /// must be greater than zero. + /// - Precondition: `retryableStatusCodes` must not be empty. + public init( + maximumAttempts: Int, + initialBackoff: Duration, + maximumBackoff: Duration, + backoffMultiplier: Double, + retryableStatusCodes: Set + ) { + self.maximumAttempts = try! validateMaxAttempts(maximumAttempts) + + try! Self.validateInitialBackoff(initialBackoff) + self.initialBackoff = initialBackoff + + try! Self.validateMaxBackoff(maximumBackoff) + self.maximumBackoff = maximumBackoff + + try! Self.validateBackoffMultiplier(backoffMultiplier) + self.backoffMultiplier = backoffMultiplier + + try! Self.validateRetryableStatusCodes(retryableStatusCodes) + self.retryableStatusCodes = retryableStatusCodes + } + + private static func validateInitialBackoff(_ value: Duration) throws { + if value <= .zero { + throw RuntimeError( + code: .invalidArgument, + message: "initialBackoff must be greater than zero" + ) + } + } + + private static func validateMaxBackoff(_ value: Duration) throws { + if value <= .zero { + throw RuntimeError( + code: .invalidArgument, + message: "maximumBackoff must be greater than zero" + ) + } + } + + private static func validateBackoffMultiplier(_ value: Double) throws { + if value <= 0 { + throw RuntimeError( + code: .invalidArgument, + message: "backoffMultiplier must be greater than zero" + ) + } + } + + private static func validateRetryableStatusCodes(_ value: Set) throws { + if value.isEmpty { + throw RuntimeError(code: .invalidArgument, message: "retryableStatusCodes mustn't be empty") + } + } +} + +/// Policy for hedging an RPC. +/// +/// Hedged RPCs may execute more than once on a server so only idempotent methods should +/// be hedged. +/// +/// gRPC executes the RPC at most ``maximumAttempts`` times, staggering each attempt +/// by ``hedgingDelay``. +/// +/// For more information see [gRFC A6 Client +/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct HedgingPolicy: Hashable, Sendable { + /// The maximum number of RPC attempts, including the original attempt. + /// + /// Values greater than five are treated as five. + /// + /// - Precondition: Must be greater than one. + public var maximumAttempts: Int { + didSet { self.maximumAttempts = try! validateMaxAttempts(self.maximumAttempts) } + } + + /// The first RPC will be sent immediately, but each subsequent RPC will be sent at intervals + /// of `hedgingDelay`. Set this to zero to immediately send all RPCs. + public var hedgingDelay: Duration { + willSet { try! Self.validateHedgingDelay(newValue) } + } + + /// The set of status codes which indicate other hedged RPCs may still succeed. + /// + /// If a non-fatal status code is returned by the server, hedged RPCs will continue. + /// Otherwise, outstanding requests will be cancelled and the error returned to the + /// application layer. + public var nonFatalStatusCodes: Set + + /// Create a new hedging policy. + /// + /// - Parameters: + /// - maximumAttempts: The maximum number of attempts allowed for the RPC. + /// - hedgingDelay: The delay between each hedged RPC. + /// - nonFatalStatusCodes: The set of status codes which indicate other hedged RPCs may still + /// succeed. + /// - Precondition: `maximumAttempts` must be greater than zero. + public init( + maximumAttempts: Int, + hedgingDelay: Duration, + nonFatalStatusCodes: Set + ) { + self.maximumAttempts = try! validateMaxAttempts(maximumAttempts) + + try! Self.validateHedgingDelay(hedgingDelay) + self.hedgingDelay = hedgingDelay + self.nonFatalStatusCodes = nonFatalStatusCodes + } + + private static func validateHedgingDelay(_ value: Duration) throws { + if value < .zero { + throw RuntimeError( + code: .invalidArgument, + message: "hedgingDelay must be greater than or equal to zero" + ) + } + } +} + +private func validateMaxAttempts(_ value: Int) throws -> Int { + guard value > 1 else { + throw RuntimeError( + code: .invalidArgument, + message: "max_attempts must be greater than one (was \(value))" + ) + } + + return min(value, 5) +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Duration { + fileprivate init(googleProtobufDuration duration: String) throws { + guard duration.utf8.last == UInt8(ascii: "s"), + let fractionalSeconds = Double(duration.dropLast()) + else { + throw RuntimeError(code: .invalidArgument, message: "Invalid google.protobuf.duration") + } + + let seconds = fractionalSeconds.rounded(.down) + let attoseconds = (fractionalSeconds - seconds) / 1e18 + + self.init(secondsComponent: Int64(seconds), attosecondsComponent: Int64(attoseconds)) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension MethodConfiguration: Codable { + private enum CodingKeys: String, CodingKey { + case name + case timeout + case maxRequestMessageBytes + case maxResponseMessageBytes + case retryPolicy + case hedgingPolicy + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.names = try container.decode([Name].self, forKey: .name) + + let timeout = try container.decodeIfPresent(GoogleProtobufDuration.self, forKey: .timeout) + self.timeout = timeout?.duration + + let maxRequestSize = try container.decodeIfPresent(Int.self, forKey: .maxRequestMessageBytes) + self.maxRequestMessageBytes = maxRequestSize + + let maxResponseSize = try container.decodeIfPresent(Int.self, forKey: .maxResponseMessageBytes) + self.maxResponseMessageBytes = maxResponseSize + + if let policy = try container.decodeIfPresent(HedgingPolicy.self, forKey: .hedgingPolicy) { + self.executionPolicy = .hedge(policy) + } else if let policy = try container.decodeIfPresent(RetryPolicy.self, forKey: .retryPolicy) { + self.executionPolicy = .retry(policy) + } else { + self.executionPolicy = nil + } + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.names, forKey: .name) + try container.encodeIfPresent( + self.timeout.map { GoogleProtobufDuration(duration: $0) }, + forKey: .timeout + ) + try container.encodeIfPresent(self.maxRequestMessageBytes, forKey: .maxRequestMessageBytes) + try container.encodeIfPresent(self.maxResponseMessageBytes, forKey: .maxResponseMessageBytes) + + switch self.executionPolicy { + case .retry(let policy): + try container.encode(policy, forKey: .retryPolicy) + case .hedge(let policy): + try container.encode(policy, forKey: .hedgingPolicy) + case .none: + () + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension MethodConfiguration.Name: Codable { + private enum CodingKeys: String, CodingKey { + case service + case method + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let service = try container.decodeIfPresent(String.self, forKey: .service) + self.service = service ?? "" + + let method = try container.decodeIfPresent(String.self, forKey: .method) + self.method = method ?? "" + + try self.validate() + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.method, forKey: .method) + try container.encode(self.service, forKey: .service) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension RetryPolicy: Codable { + private enum CodingKeys: String, CodingKey { + case maxAttempts + case initialBackoff + case maxBackoff + case backoffMultiplier + case retryableStatusCodes + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let maxAttempts = try container.decode(Int.self, forKey: .maxAttempts) + self.maximumAttempts = try validateMaxAttempts(maxAttempts) + + let initialBackoff = try container.decode(String.self, forKey: .initialBackoff) + self.initialBackoff = try Duration(googleProtobufDuration: initialBackoff) + try Self.validateInitialBackoff(self.initialBackoff) + + let maxBackoff = try container.decode(String.self, forKey: .maxBackoff) + self.maximumBackoff = try Duration(googleProtobufDuration: maxBackoff) + try Self.validateMaxBackoff(self.maximumBackoff) + + self.backoffMultiplier = try container.decode(Double.self, forKey: .backoffMultiplier) + try Self.validateBackoffMultiplier(self.backoffMultiplier) + + let codes = try container.decode([GoogleRPCCode].self, forKey: .retryableStatusCodes) + self.retryableStatusCodes = Set(codes.map { $0.code }) + try Self.validateRetryableStatusCodes(self.retryableStatusCodes) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.maximumAttempts, forKey: .maxAttempts) + try container.encode( + GoogleProtobufDuration(duration: self.initialBackoff), + forKey: .initialBackoff + ) + try container.encode(GoogleProtobufDuration(duration: self.maximumBackoff), forKey: .maxBackoff) + try container.encode(self.backoffMultiplier, forKey: .backoffMultiplier) + try container.encode( + self.retryableStatusCodes.map { $0.googleRPCCode }, + forKey: .retryableStatusCodes + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension HedgingPolicy: Codable { + private enum CodingKeys: String, CodingKey { + case maxAttempts + case hedgingDelay + case nonFatalStatusCodes + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let maxAttempts = try container.decode(Int.self, forKey: .maxAttempts) + self.maximumAttempts = try validateMaxAttempts(maxAttempts) + + let delay = try container.decode(String.self, forKey: .hedgingDelay) + self.hedgingDelay = try Duration(googleProtobufDuration: delay) + + let statusCodes = try container.decode([GoogleRPCCode].self, forKey: .nonFatalStatusCodes) + self.nonFatalStatusCodes = Set(statusCodes.map { $0.code }) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.maximumAttempts, forKey: .maxAttempts) + try container.encode(GoogleProtobufDuration(duration: self.hedgingDelay), forKey: .hedgingDelay) + try container.encode( + self.nonFatalStatusCodes.map { $0.googleRPCCode }, + forKey: .nonFatalStatusCodes + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct GoogleProtobufDuration: Codable { + var duration: Duration + + init(duration: Duration) { + self.duration = duration + } + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let duration = try container.decode(String.self) + + guard duration.utf8.last == UInt8(ascii: "s"), + let fractionalSeconds = Double(duration.dropLast()) + else { + throw RuntimeError(code: .invalidArgument, message: "Invalid google.protobuf.duration") + } + + let seconds = fractionalSeconds.rounded(.down) + let attoseconds = (fractionalSeconds - seconds) * 1e18 + + self.duration = Duration( + secondsComponent: Int64(seconds), + attosecondsComponent: Int64(attoseconds) + ) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + + var seconds = Double(self.duration.components.seconds) + seconds += Double(self.duration.components.attoseconds) / 1e18 + + let durationString = "\(seconds)s" + try container.encode(durationString) + } +} + +struct GoogleRPCCode: Codable { + var code: Status.Code + + init(code: Status.Code) { + self.code = code + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let code: Status.Code? + + if let caseName = try? container.decode(String.self) { + code = Status.Code(googleRPCCode: caseName) + } else if let rawValue = try? container.decode(Int.self) { + code = Status.Code(rawValue: rawValue) + } else { + code = nil + } + + if let code = code { + self.code = code + } else { + throw RuntimeError(code: .invalidArgument, message: "Invalid google.rpc.code") + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.code.googleRPCCode) + } +} + +extension Status.Code { + fileprivate init?(googleRPCCode code: String) { + switch code { + case "OK": + self = .ok + case "CANCELLED": + self = .cancelled + case "UNKNOWN": + self = .unknown + case "INVALID_ARGUMENT": + self = .invalidArgument + case "DEADLINE_EXCEEDED": + self = .deadlineExceeded + case "NOT_FOUND": + self = .notFound + case "ALREADY_EXISTS": + self = .alreadyExists + case "PERMISSION_DENIED": + self = .permissionDenied + case "RESOURCE_EXHAUSTED": + self = .resourceExhausted + case "FAILED_PRECONDITION": + self = .failedPrecondition + case "ABORTED": + self = .aborted + case "OUT_OF_RANGE": + self = .outOfRange + case "UNIMPLEMENTED": + self = .unimplemented + case "INTERNAL": + self = .internalError + case "UNAVAILABLE": + self = .unavailable + case "DATA_LOSS": + self = .dataLoss + case "UNAUTHENTICATED": + self = .unauthenticated + default: + return nil + } + } + + fileprivate var googleRPCCode: String { + switch self.wrapped { + case .ok: + return "OK" + case .cancelled: + return "CANCELLED" + case .unknown: + return "UNKNOWN" + case .invalidArgument: + return "INVALID_ARGUMENT" + case .deadlineExceeded: + return "DEADLINE_EXCEEDED" + case .notFound: + return "NOT_FOUND" + case .alreadyExists: + return "ALREADY_EXISTS" + case .permissionDenied: + return "PERMISSION_DENIED" + case .resourceExhausted: + return "RESOURCE_EXHAUSTED" + case .failedPrecondition: + return "FAILED_PRECONDITION" + case .aborted: + return "ABORTED" + case .outOfRange: + return "OUT_OF_RANGE" + case .unimplemented: + return "UNIMPLEMENTED" + case .internalError: + return "INTERNAL" + case .unavailable: + return "UNAVAILABLE" + case .dataLoss: + return "DATA_LOSS" + case .unauthenticated: + return "UNAUTHENTICATED" + } + } +} diff --git a/Sources/GRPCCore/Configuration/ServiceConfiguration.swift b/Sources/GRPCCore/Configuration/ServiceConfiguration.swift new file mode 100644 index 000000000..54adeaf48 --- /dev/null +++ b/Sources/GRPCCore/Configuration/ServiceConfiguration.swift @@ -0,0 +1,278 @@ +/* + * 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. + */ + +/// Service configuration values. +/// +/// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct ServiceConfiguration: Hashable, Sendable { + /// Per-method configuration. + public var methodConfiguration: [MethodConfiguration] + + /// Load balancing policies. + /// + /// The client iterates through the list in order and picks the first configuration it supports. + /// If no policies are supported then the configuration is considered to be invalid. + public var loadBalancingConfiguration: [LoadBalancingConfiguration] + + /// The policy for throttling retries. + /// + /// If a ``RetryThrottlingPolicy`` is provided, gRPC will automatically throttle retry attempts + /// and hedged RPCs when the client's ratio of failures to successes exceeds a threshold. + /// + /// For each server name, the gRPC client will maintain a `token_count` which is initially set + /// to ``maxTokens``. Every outgoing RPC (regardless of service or method invoked) will change + /// `token_count` as follows: + /// + /// - Every failed RPC will decrement the `token_count` by 1. + /// - Every successful RPC will increment the `token_count` by ``tokenRatio``. + /// + /// If `token_count` is less than or equal to `max_tokens / 2`, then RPCs will not be retried + /// and hedged RPCs will not be sent. + public var retryThrottlingPolicy: RetryThrottlingPolicy? + + /// Creates a new ``ServiceConfiguration``. + /// + /// - Parameters: + /// - methodConfiguration: Per-method configuration. + /// - loadBalancingConfiguration: Load balancing policies. Clients use the the first supported + /// policy when iterating the list in order. + /// - retryThrottlingPolicy: Policy for throttling retries. + public init( + methodConfiguration: [MethodConfiguration] = [], + loadBalancingConfiguration: [LoadBalancingConfiguration] = [], + retryThrottlingPolicy: RetryThrottlingPolicy? = nil + ) { + self.methodConfiguration = methodConfiguration + self.loadBalancingConfiguration = loadBalancingConfiguration + self.retryThrottlingPolicy = retryThrottlingPolicy + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ServiceConfiguration: Codable { + private enum CodingKeys: String, CodingKey { + case methodConfig + case loadBalancingConfig + case retryThrottling + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let methodConfiguration = try container.decodeIfPresent( + [MethodConfiguration].self, + forKey: .methodConfig + ) + self.methodConfiguration = methodConfiguration ?? [] + + let loadBalancingConfiguration = try container.decodeIfPresent( + [LoadBalancingConfiguration].self, + forKey: .loadBalancingConfig + ) + self.loadBalancingConfiguration = loadBalancingConfiguration ?? [] + + self.retryThrottlingPolicy = try container.decodeIfPresent( + RetryThrottlingPolicy.self, + forKey: .retryThrottling + ) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.methodConfiguration, forKey: .methodConfig) + try container.encode(self.loadBalancingConfiguration, forKey: .loadBalancingConfig) + try container.encodeIfPresent(self.retryThrottlingPolicy, forKey: .retryThrottling) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ServiceConfiguration { + /// Configuration used by clients for load-balancing. + public struct LoadBalancingConfiguration: Hashable, Sendable { + private enum Value: Hashable, Sendable { + case pickFirst(PickFirst) + case roundRobin(RoundRobin) + } + + private var value: Value? + private init(_ value: Value) { + self.value = value + } + + /// Creates a pick-first load balancing policy. + /// + /// - Parameter shuffleAddressList: Whether resolved addresses should be shuffled before + /// attempting to connect to them. + public static func pickFirst(shuffleAddressList: Bool) -> Self { + Self(.pickFirst(PickFirst(shuffleAddressList: shuffleAddressList))) + } + + /// Creates a pick-first load balancing policy. + /// + /// - Parameter pickFirst: The pick-first load balancing policy. + public static func pickFirst(_ pickFirst: PickFirst) -> Self { + Self(.pickFirst(pickFirst)) + } + + /// Creates a round-robin load balancing policy. + public static var roundRobin: Self { + Self(.roundRobin(RoundRobin())) + } + + /// The pick-first policy, if configured. + public var pickFirst: PickFirst? { + get { + switch self.value { + case .pickFirst(let value): + return value + default: + return nil + } + } + set { + self.value = newValue.map { .pickFirst($0) } + } + } + + /// The round-robin policy, if configured. + public var roundRobin: RoundRobin? { + get { + switch self.value { + case .roundRobin(let value): + return value + default: + return nil + } + } + set { + self.value = newValue.map { .roundRobin($0) } + } + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ServiceConfiguration.LoadBalancingConfiguration { + /// Configuration for the pick-first load balancing policy. + public struct PickFirst: Hashable, Sendable, Codable { + /// Whether the resolved addresses should be shuffled before attempting to connect to them. + public var shuffleAddressList: Bool + + /// Creates a new pick-first load balancing policy. + /// - Parameter shuffleAddressList: Whether the resolved addresses should be shuffled before + /// attempting to connect to them. + public init(shuffleAddressList: Bool = false) { + self.shuffleAddressList = shuffleAddressList + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let shuffle = try container.decodeIfPresent(Bool.self, forKey: .shuffleAddressList) ?? false + self.shuffleAddressList = shuffle + } + } + + /// Configuration for the round-robin load balancing policy. + public struct RoundRobin: Hashable, Sendable, Codable { + /// Creates a new round-robin load balancing policy. + public init() {} + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ServiceConfiguration.LoadBalancingConfiguration: Codable { + private enum CodingKeys: String, CodingKey { + case roundRobin = "round_robin" + case pickFirst = "pick_first" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let value = try container.decodeIfPresent(RoundRobin.self, forKey: .roundRobin) { + self.value = .roundRobin(value) + } else if let value = try container.decodeIfPresent(PickFirst.self, forKey: .pickFirst) { + self.value = .pickFirst(value) + } else { + self.value = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self.value { + case .pickFirst(let value): + try container.encode(value, forKey: .pickFirst) + case .roundRobin(let value): + try container.encode(value, forKey: .roundRobin) + case .none: + () + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ServiceConfiguration { + public struct RetryThrottlingPolicy: Hashable, Sendable, Codable { + /// The initial, and maximum number of tokens. + /// + /// - Precondition: Must be greater than zero. + public var maxTokens: Int + + /// The amount of tokens to add on each successful RPC. + /// + /// Typically this will be some number between 0 and 1, e.g., 0.1. Up to three decimal places + /// are supported. + /// + /// - Precondition: Must be greater than zero. + public var tokenRatio: Double + + /// Creates a new retry throttling policy. + /// + /// - Parameters: + /// - maxTokens: The initial, and maximum number of tokens. Must be greater than zero. + /// - tokenRatio: The amount of tokens to add on each successful RPC. Must be greater + /// than zero. + public init(maxTokens: Int, tokenRatio: Double) throws { + self.maxTokens = maxTokens + self.tokenRatio = tokenRatio + + try self.validateMaxTokens() + try self.validateTokenRatio() + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.maxTokens = try container.decode(Int.self, forKey: .maxTokens) + self.tokenRatio = try container.decode(Double.self, forKey: .tokenRatio) + + try self.validateMaxTokens() + try self.validateTokenRatio() + } + + private func validateMaxTokens() throws { + if self.maxTokens <= 0 { + throw RuntimeError(code: .invalidArgument, message: "maxTokens must be greater than zero") + } + } + + private func validateTokenRatio() throws { + if self.tokenRatio <= 0 { + throw RuntimeError(code: .invalidArgument, message: "tokenRatio must be greater than zero") + } + } + } +} diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 22f054f59..d2434205e 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -393,7 +393,7 @@ public struct GRPCClient: Sendable { } // No configuration found, return the "vanilla" configuration. - return MethodConfiguration(executionPolicy: nil, timeout: nil) + return MethodConfiguration(names: [], timeout: nil, executionPolicy: nil) } } diff --git a/Sources/GRPCCore/MethodConfiguration.swift b/Sources/GRPCCore/MethodConfiguration.swift deleted file mode 100644 index 14d2f717f..000000000 --- a/Sources/GRPCCore/MethodConfiguration.swift +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -/// Configuration values for executing an RPC. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct MethodConfiguration: Hashable, Sendable { - /// The default timeout for the RPC. - /// - /// If no reply is received in the specified amount of time the request is aborted - /// with an ``RPCError`` with code ``RPCError/Code/deadlineExceeded``. - /// - /// The actual deadline used will be the minimum of the value specified here - /// and the value set by the application by the client API. If either one isn't set - /// then the other value is used. If neither is set then the request has no deadline. - /// - /// The timeout applies to the overall execution of an RPC. If, for example, a retry - /// policy is set then the timeout begins when the first attempt is started and _isn't_ reset - /// when subsequent attempts start. - public var timeout: Duration? - - /// The policy determining how many times, and when, the RPC is executed. - /// - /// There are two policy types: - /// 1. Retry - /// 2. Hedging - /// - /// The retry policy allows an RPC to be retried a limited number of times if the RPC - /// fails with one of the configured set of status codes. RPCs are only retried if they - /// fail immediately, that is, the first response part received from the server is a - /// status code. - /// - /// The hedging policy allows an RPC to be executed multiple times concurrently. Typically - /// each execution will be staggered by some delay. The first successful response will be - /// reported to the client. Hedging is only suitable for idempotent RPCs. - public var executionPolicy: ExecutionPolicy? - - /// Create an execution configuration. - /// - /// - Parameters: - /// - executionPolicy: The execution policy to use for the RPC. - /// - timeout: The default timeout for the RPC. - public init( - executionPolicy: ExecutionPolicy?, - timeout: Duration? - ) { - self.executionPolicy = executionPolicy - self.timeout = timeout - } - - /// Create an execution configuration with a retry policy. - /// - /// - Parameters: - /// - retryPolicy: The policy for retrying the RPC. - /// - timeout: The default timeout for the RPC. - public init( - retryPolicy: RetryPolicy, - timeout: Duration? = nil - ) { - self.executionPolicy = .retry(retryPolicy) - self.timeout = timeout - } - - /// Create an execution configuration with a hedging policy. - /// - /// - Parameters: - /// - hedgingPolicy: The policy for hedging the RPC. - /// - timeout: The default timeout for the RPC. - public init( - hedgingPolicy: HedgingPolicy, - timeout: Duration? = nil - ) { - self.executionPolicy = .hedge(hedgingPolicy) - self.timeout = timeout - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension MethodConfiguration { - /// The execution policy for an RPC. - public enum ExecutionPolicy: Hashable, Sendable { - /// Policy for retrying an RPC. - /// - /// See ``RetryPolicy`` for more details. - case retry(RetryPolicy) - - /// Policy for hedging an RPC. - /// - /// See ``HedgingPolicy`` for more details. - case hedge(HedgingPolicy) - } -} - -/// Policy for retrying an RPC. -/// -/// gRPC retries RPCs when the first response from the server is a status code which matches -/// one of the configured retryable status codes. If the server begins processing the RPC and -/// first responds with metadata and later responds with a retryable status code then the RPC -/// won't be retried. -/// -/// Execution attempts are limited by ``maximumAttempts`` which includes the original attempt. The -/// maximum number of attempts is limited to five. -/// -/// Subsequent attempts are executed after some delay. The first _retry_, or second attempt, will -/// be started after a randomly chosen delay between zero and ``initialBackoff``. More generally, -/// the nth retry will happen after a randomly chosen delay between zero -/// and `min(initialBackoff * backoffMultiplier^(n-1), maximumBackoff)`. -/// -/// For more information see [gRFC A6 Client -/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct RetryPolicy: Hashable, Sendable { - /// The maximum number of RPC attempts, including the original attempt. - /// - /// Must be greater than one, values greater than five are treated as five. - public var maximumAttempts: Int { - didSet { self.maximumAttempts = validateMaxAttempts(self.maximumAttempts) } - } - - /// The initial backoff duration. - /// - /// The initial retry will occur after a random amount of time up to this value. - /// - /// - Precondition: Must be greater than zero. - public var initialBackoff: Duration { - willSet { Self.validateInitialBackoff(newValue) } - } - - /// The maximum amount of time to backoff for. - /// - /// - Precondition: Must be greater than zero. - public var maximumBackoff: Duration { - willSet { Self.validateMaxBackoff(newValue) } - } - - /// The multiplier to apply to backoff. - /// - /// - Precondition: Must be greater than zero. - public var backoffMultiplier: Double { - willSet { Self.validateBackoffMultiplier(newValue) } - } - - /// The set of status codes which may be retried. - /// - /// - Precondition: Must not be empty. - public var retryableStatusCodes: Set { - willSet { Self.validateRetryableStatusCodes(newValue) } - } - - /// Create a new retry policy. - /// - /// - Parameters: - /// - maximumAttempts: The maximum number of attempts allowed for the RPC. - /// - initialBackoff: The initial backoff period for the first retry attempt. Must be - /// greater than zero. - /// - maximumBackoff: The maximum period of time to wait between attempts. Must be greater than - /// zero. - /// - backoffMultiplier: The exponential backoff multiplier. Must be greater than zero. - /// - retryableStatusCodes: The set of status codes which may be retried. Must not be empty. - /// - Precondition: `maximumAttempts`, `initialBackoff`, `maximumBackoff` and `backoffMultiplier` - /// must be greater than zero. - /// - Precondition: `retryableStatusCodes` must not be empty. - public init( - maximumAttempts: Int, - initialBackoff: Duration, - maximumBackoff: Duration, - backoffMultiplier: Double, - retryableStatusCodes: Set - ) { - self.maximumAttempts = validateMaxAttempts(maximumAttempts) - - Self.validateInitialBackoff(initialBackoff) - self.initialBackoff = initialBackoff - - Self.validateMaxBackoff(maximumBackoff) - self.maximumBackoff = maximumBackoff - - Self.validateBackoffMultiplier(backoffMultiplier) - self.backoffMultiplier = backoffMultiplier - - Self.validateRetryableStatusCodes(retryableStatusCodes) - self.retryableStatusCodes = retryableStatusCodes - } - - private static func validateInitialBackoff(_ value: Duration) { - precondition(value.isGreaterThanZero, "initialBackoff must be greater than zero") - } - - private static func validateMaxBackoff(_ value: Duration) { - precondition(value.isGreaterThanZero, "maximumBackoff must be greater than zero") - } - - private static func validateBackoffMultiplier(_ value: Double) { - precondition(value > 0, "backoffMultiplier must be greater than zero") - } - - private static func validateRetryableStatusCodes(_ value: Set) { - precondition(!value.isEmpty, "retryableStatusCodes mustn't be empty") - } -} - -/// Policy for hedging an RPC. -/// -/// Hedged RPCs may execute more than once on a server so only idempotent methods should -/// be hedged. -/// -/// gRPC executes the RPC at most ``maximumAttempts`` times, staggering each attempt -/// by ``hedgingDelay``. -/// -/// For more information see [gRFC A6 Client -/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct HedgingPolicy: Hashable, Sendable { - /// The maximum number of RPC attempts, including the original attempt. - /// - /// Values greater than five are treated as five. - /// - /// - Precondition: Must be greater than one. - public var maximumAttempts: Int { - didSet { self.maximumAttempts = validateMaxAttempts(self.maximumAttempts) } - } - - /// The first RPC will be sent immediately, but each subsequent RPC will be sent at intervals - /// of `hedgingDelay`. Set this to zero to immediately send all RPCs. - public var hedgingDelay: Duration { - willSet { Self.validateHedgingDelay(newValue) } - } - - /// The set of status codes which indicate other hedged RPCs may still succeed. - /// - /// If a non-fatal status code is returned by the server, hedged RPCs will continue. - /// Otherwise, outstanding requests will be cancelled and the error returned to the - /// application layer. - public var nonFatalStatusCodes: Set - - /// Create a new hedging policy. - /// - /// - Parameters: - /// - maximumAttempts: The maximum number of attempts allowed for the RPC. - /// - hedgingDelay: The delay between each hedged RPC. - /// - nonFatalStatusCodes: The set of status codes which indicated other hedged RPCs may still - /// succeed. - /// - Precondition: `maximumAttempts` must be greater than zero. - public init( - maximumAttempts: Int, - hedgingDelay: Duration, - nonFatalStatusCodes: Set - ) { - self.maximumAttempts = validateMaxAttempts(maximumAttempts) - - Self.validateHedgingDelay(hedgingDelay) - self.hedgingDelay = hedgingDelay - self.nonFatalStatusCodes = nonFatalStatusCodes - } - - private static func validateHedgingDelay(_ value: Duration) { - precondition( - value.isGreaterThanOrEqualToZero, - "hedgingDelay must be greater than or equal to zero" - ) - } -} - -private func validateMaxAttempts(_ value: Int) -> Int { - precondition(value > 0, "maximumAttempts must be greater than zero") - return min(value, 5) -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Duration { - fileprivate var isGreaterThanZero: Bool { - self.components.seconds > 0 || self.components.attoseconds > 0 - } - - fileprivate var isGreaterThanOrEqualToZero: Bool { - self.components.seconds >= 0 || self.components.attoseconds >= 0 - } -} diff --git a/Sources/GRPCCore/RuntimeError.swift b/Sources/GRPCCore/RuntimeError.swift new file mode 100644 index 000000000..74b0e7b7a --- /dev/null +++ b/Sources/GRPCCore/RuntimeError.swift @@ -0,0 +1,128 @@ +/* + * 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. + */ + +/// An error thrown at runtime. +/// +/// In contrast to ``RPCError``, the ``RuntimeError`` represents errors which happen at a scope +/// wider than an individual RPC. For example, passing invalid configuration values. +public struct RuntimeError: Error, Hashable, @unchecked Sendable { + private var storage: Storage + + // Ensures the underlying storage is unique. + private mutating func ensureUniqueStorage() { + if !isKnownUniquelyReferenced(&self.storage) { + self.storage = self.storage.copy() + } + } + + /// The code indicating the domain of the error. + public var code: Code { + get { self.storage.code } + set { + self.ensureUniqueStorage() + self.storage.code = newValue + } + } + + /// A message providing more details about the error which may include details specific to this + /// instance of the error. + public var message: String { + get { self.storage.message } + set { + self.ensureUniqueStorage() + self.storage.message = newValue + } + } + + /// The original error which led to this error being thrown. + public var cause: Error? { + get { self.storage.cause } + set { + self.ensureUniqueStorage() + self.storage.cause = newValue + } + } + + /// Creates a new error. + /// + /// - Parameters: + /// - code: The error code. + /// - message: A description of the error. + /// - cause: The original error which led to this error being thrown. + public init(code: Code, message: String, cause: Error? = nil) { + self.storage = Storage(code: code, message: message, cause: cause) + } +} + +extension RuntimeError: CustomStringConvertible { + public var description: String { + if let cause = self.cause { + return "\(self.code): \"\(self.message)\" (cause: \"\(cause)\")" + } else { + return "\(self.code): \"\(self.message)\"" + } + } +} + +extension RuntimeError { + private final class Storage: Hashable { + var code: Code + var message: String + var cause: Error? + + init(code: Code, message: String, cause: Error?) { + self.code = code + self.message = message + self.cause = cause + } + + func copy() -> Storage { + return Storage(code: self.code, message: self.message, cause: self.cause) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + } + + static func == (lhs: Storage, rhs: Storage) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message + } + } +} + +extension RuntimeError { + public struct Code: Hashable, Sendable { + private enum Value { + case invalidArgument + } + + private var value: Value + private init(_ value: Value) { + self.value = value + } + + public static var invalidArgument: Self { + Self(.invalidArgument) + } + } +} + +extension RuntimeError.Code: CustomStringConvertible { + public var description: String { + String(describing: self.value) + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 9aacff2d7..fc882b7ba 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -146,10 +146,7 @@ struct ClientRPCExecutorTestHarness { if let configuration = configuration { executionConfiguration = configuration } else { - executionConfiguration = MethodConfiguration( - executionPolicy: nil, - timeout: nil - ) + executionConfiguration = MethodConfiguration(names: []) } // Execute the request. diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index 79663bfee..ef780fef4 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -200,6 +200,6 @@ extension MethodConfiguration { nonFatalStatusCodes: nonFatalCodes ) - return Self(hedgingPolicy: policy, timeout: timeout) + return Self(names: [], timeout: timeout, executionPolicy: .hedge(policy)) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index e20f48352..e2a529228 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -236,7 +236,7 @@ extension ClientRPCExecutorTests { request: ClientRequest.Stream { try await $0.write([0]) }, - configuration: .init(retryPolicy: retryPolicy) + configuration: .init(names: [], executionPolicy: .retry(retryPolicy)) ) { response in let end = ContinuousClock.now let duration = end - start @@ -272,7 +272,7 @@ extension ClientRPCExecutorTests { request: ClientRequest.Stream { try await $0.write([0]) }, - configuration: .init(retryPolicy: retryPolicy) + configuration: .init(names: [], executionPolicy: .retry(retryPolicy)) ) { response in switch response.accepted { case .success: @@ -304,6 +304,6 @@ extension MethodConfiguration { retryableStatusCodes: codes ) - return Self(retryPolicy: policy, timeout: timeout) + return Self(names: [], timeout: timeout, executionPolicy: .retry(policy)) } } diff --git a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift index 09bc182cd..4ecc2fc65 100644 --- a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -21,7 +21,7 @@ import XCTest final class RetryDelaySequenceTests: XCTestCase { func testSequence() { let policy = RetryPolicy( - maximumAttempts: 1, // ignored here + maximumAttempts: 3, // ignored here initialBackoff: .seconds(1), maximumBackoff: .seconds(8), backoffMultiplier: 2.0, @@ -41,7 +41,7 @@ final class RetryDelaySequenceTests: XCTestCase { func testSequenceSupportsMultipleIteration() { let policy = RetryPolicy( - maximumAttempts: 1, // ignored here + maximumAttempts: 3, // ignored here initialBackoff: .seconds(1), maximumBackoff: .seconds(8), backoffMultiplier: 2.0, diff --git a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift new file mode 100644 index 000000000..a7c135fd0 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift @@ -0,0 +1,307 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: google/rpc/code.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2022 Google LLC +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The canonical error codes for gRPC APIs. +/// +/// +/// Sometimes multiple error codes may apply. Services should return +/// the most specific error code that applies. For example, prefer +/// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. +/// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. +enum Google_Rpc_Code: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Not an error; returned on success. + /// + /// HTTP Mapping: 200 OK + case ok // = 0 + + /// The operation was cancelled, typically by the caller. + /// + /// HTTP Mapping: 499 Client Closed Request + case cancelled // = 1 + + /// Unknown error. For example, this error may be returned when + /// a `Status` value received from another address space belongs to + /// an error space that is not known in this address space. Also + /// errors raised by APIs that do not return enough error information + /// may be converted to this error. + /// + /// HTTP Mapping: 500 Internal Server Error + case unknown // = 2 + + /// The client specified an invalid argument. Note that this differs + /// from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments + /// that are problematic regardless of the state of the system + /// (e.g., a malformed file name). + /// + /// HTTP Mapping: 400 Bad Request + case invalidArgument // = 3 + + /// The deadline expired before the operation could complete. For operations + /// that change the state of the system, this error may be returned + /// even if the operation has completed successfully. For example, a + /// successful response from a server could have been delayed long + /// enough for the deadline to expire. + /// + /// HTTP Mapping: 504 Gateway Timeout + case deadlineExceeded // = 4 + + /// Some requested entity (e.g., file or directory) was not found. + /// + /// Note to server developers: if a request is denied for an entire class + /// of users, such as gradual feature rollout or undocumented allowlist, + /// `NOT_FOUND` may be used. If a request is denied for some users within + /// a class of users, such as user-based access control, `PERMISSION_DENIED` + /// must be used. + /// + /// HTTP Mapping: 404 Not Found + case notFound // = 5 + + /// The entity that a client attempted to create (e.g., file or directory) + /// already exists. + /// + /// HTTP Mapping: 409 Conflict + case alreadyExists // = 6 + + /// The caller does not have permission to execute the specified + /// operation. `PERMISSION_DENIED` must not be used for rejections + /// caused by exhausting some resource (use `RESOURCE_EXHAUSTED` + /// instead for those errors). `PERMISSION_DENIED` must not be + /// used if the caller can not be identified (use `UNAUTHENTICATED` + /// instead for those errors). This error code does not imply the + /// request is valid or the requested entity exists or satisfies + /// other pre-conditions. + /// + /// HTTP Mapping: 403 Forbidden + case permissionDenied // = 7 + + /// The request does not have valid authentication credentials for the + /// operation. + /// + /// HTTP Mapping: 401 Unauthorized + case unauthenticated // = 16 + + /// Some resource has been exhausted, perhaps a per-user quota, or + /// perhaps the entire file system is out of space. + /// + /// HTTP Mapping: 429 Too Many Requests + case resourceExhausted // = 8 + + /// The operation was rejected because the system is not in a state + /// required for the operation's execution. For example, the directory + /// to be deleted is non-empty, an rmdir operation is applied to + /// a non-directory, etc. + /// + /// Service implementors can use the following guidelines to decide + /// between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: + /// (a) Use `UNAVAILABLE` if the client can retry just the failing call. + /// (b) Use `ABORTED` if the client should retry at a higher level. For + /// example, when a client-specified test-and-set fails, indicating the + /// client should restart a read-modify-write sequence. + /// (c) Use `FAILED_PRECONDITION` if the client should not retry until + /// the system state has been explicitly fixed. For example, if an "rmdir" + /// fails because the directory is non-empty, `FAILED_PRECONDITION` + /// should be returned since the client should not retry unless + /// the files are deleted from the directory. + /// + /// HTTP Mapping: 400 Bad Request + case failedPrecondition // = 9 + + /// The operation was aborted, typically due to a concurrency issue such as + /// a sequencer check failure or transaction abort. + /// + /// See the guidelines above for deciding between `FAILED_PRECONDITION`, + /// `ABORTED`, and `UNAVAILABLE`. + /// + /// HTTP Mapping: 409 Conflict + case aborted // = 10 + + /// The operation was attempted past the valid range. E.g., seeking or + /// reading past end-of-file. + /// + /// Unlike `INVALID_ARGUMENT`, this error indicates a problem that may + /// be fixed if the system state changes. For example, a 32-bit file + /// system will generate `INVALID_ARGUMENT` if asked to read at an + /// offset that is not in the range [0,2^32-1], but it will generate + /// `OUT_OF_RANGE` if asked to read from an offset past the current + /// file size. + /// + /// There is a fair bit of overlap between `FAILED_PRECONDITION` and + /// `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific + /// error) when it applies so that callers who are iterating through + /// a space can easily look for an `OUT_OF_RANGE` error to detect when + /// they are done. + /// + /// HTTP Mapping: 400 Bad Request + case outOfRange // = 11 + + /// The operation is not implemented or is not supported/enabled in this + /// service. + /// + /// HTTP Mapping: 501 Not Implemented + case unimplemented // = 12 + + /// Internal errors. This means that some invariants expected by the + /// underlying system have been broken. This error code is reserved + /// for serious errors. + /// + /// HTTP Mapping: 500 Internal Server Error + case `internal` // = 13 + + /// The service is currently unavailable. This is most likely a + /// transient condition, which can be corrected by retrying with + /// a backoff. Note that it is not always safe to retry + /// non-idempotent operations. + /// + /// See the guidelines above for deciding between `FAILED_PRECONDITION`, + /// `ABORTED`, and `UNAVAILABLE`. + /// + /// HTTP Mapping: 503 Service Unavailable + case unavailable // = 14 + + /// Unrecoverable data loss or corruption. + /// + /// HTTP Mapping: 500 Internal Server Error + case dataLoss // = 15 + case UNRECOGNIZED(Int) + + init() { + self = .ok + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .cancelled + case 2: self = .unknown + case 3: self = .invalidArgument + case 4: self = .deadlineExceeded + case 5: self = .notFound + case 6: self = .alreadyExists + case 7: self = .permissionDenied + case 8: self = .resourceExhausted + case 9: self = .failedPrecondition + case 10: self = .aborted + case 11: self = .outOfRange + case 12: self = .unimplemented + case 13: self = .internal + case 14: self = .unavailable + case 15: self = .dataLoss + case 16: self = .unauthenticated + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .ok: return 0 + case .cancelled: return 1 + case .unknown: return 2 + case .invalidArgument: return 3 + case .deadlineExceeded: return 4 + case .notFound: return 5 + case .alreadyExists: return 6 + case .permissionDenied: return 7 + case .resourceExhausted: return 8 + case .failedPrecondition: return 9 + case .aborted: return 10 + case .outOfRange: return 11 + case .unimplemented: return 12 + case .internal: return 13 + case .unavailable: return 14 + case .dataLoss: return 15 + case .unauthenticated: return 16 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Google_Rpc_Code: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Google_Rpc_Code] = [ + .ok, + .cancelled, + .unknown, + .invalidArgument, + .deadlineExceeded, + .notFound, + .alreadyExists, + .permissionDenied, + .unauthenticated, + .resourceExhausted, + .failedPrecondition, + .aborted, + .outOfRange, + .unimplemented, + .internal, + .unavailable, + .dataLoss, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension Google_Rpc_Code: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension Google_Rpc_Code: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "OK"), + 1: .same(proto: "CANCELLED"), + 2: .same(proto: "UNKNOWN"), + 3: .same(proto: "INVALID_ARGUMENT"), + 4: .same(proto: "DEADLINE_EXCEEDED"), + 5: .same(proto: "NOT_FOUND"), + 6: .same(proto: "ALREADY_EXISTS"), + 7: .same(proto: "PERMISSION_DENIED"), + 8: .same(proto: "RESOURCE_EXHAUSTED"), + 9: .same(proto: "FAILED_PRECONDITION"), + 10: .same(proto: "ABORTED"), + 11: .same(proto: "OUT_OF_RANGE"), + 12: .same(proto: "UNIMPLEMENTED"), + 13: .same(proto: "INTERNAL"), + 14: .same(proto: "UNAVAILABLE"), + 15: .same(proto: "DATA_LOSS"), + 16: .same(proto: "UNAUTHENTICATED"), + ] +} diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift new file mode 100644 index 000000000..8300d1877 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift @@ -0,0 +1,253 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/lookup/v1/rls.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2020 The gRPC Authors +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct Grpc_Lookup_V1_RouteLookupRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Target type allows the client to specify what kind of target format it + /// would like from RLS to allow it to find the regional server, e.g. "grpc". + var targetType: String = String() + + /// Reason for making this request. + var reason: Grpc_Lookup_V1_RouteLookupRequest.Reason = .unknown + + /// For REASON_STALE, the header_data from the stale response, if any. + var staleHeaderData: String = String() + + /// Map of key values extracted via key builders for the gRPC or HTTP request. + var keyMap: Dictionary = [:] + + /// Application-specific optional extensions. + var extensions: [SwiftProtobuf.Google_Protobuf_Any] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Possible reasons for making a request. + enum Reason: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Unused + case unknown // = 0 + + /// No data available in local cache + case miss // = 1 + + /// Data in local cache is stale + case stale // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .miss + case 2: self = .stale + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .miss: return 1 + case .stale: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} +} + +#if swift(>=4.2) + +extension Grpc_Lookup_V1_RouteLookupRequest.Reason: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Lookup_V1_RouteLookupRequest.Reason] = [ + .unknown, + .miss, + .stale, + ] +} + +#endif // swift(>=4.2) + +struct Grpc_Lookup_V1_RouteLookupResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Prioritized list (best one first) of addressable entities to use + /// for routing, using syntax requested by the request target_type. + /// The targets will be tried in order until a healthy one is found. + var targets: [String] = [] + + /// Optional header value to pass along to AFE in the X-Google-RLS-Data header. + /// Cached with "target" and sent with all requests that match the request key. + /// Allows the RLS to pass its work product to the eventual target. + var headerData: String = String() + + /// Application-specific optional extensions. + var extensions: [SwiftProtobuf.Google_Protobuf_Any] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Lookup_V1_RouteLookupRequest: @unchecked Sendable {} +extension Grpc_Lookup_V1_RouteLookupRequest.Reason: @unchecked Sendable {} +extension Grpc_Lookup_V1_RouteLookupResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.lookup.v1" + +extension Grpc_Lookup_V1_RouteLookupRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RouteLookupRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 3: .standard(proto: "target_type"), + 5: .same(proto: "reason"), + 6: .standard(proto: "stale_header_data"), + 4: .standard(proto: "key_map"), + 7: .same(proto: "extensions"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 3: try { try decoder.decodeSingularStringField(value: &self.targetType) }() + case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.keyMap) }() + case 5: try { try decoder.decodeSingularEnumField(value: &self.reason) }() + case 6: try { try decoder.decodeSingularStringField(value: &self.staleHeaderData) }() + case 7: try { try decoder.decodeRepeatedMessageField(value: &self.extensions) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.targetType.isEmpty { + try visitor.visitSingularStringField(value: self.targetType, fieldNumber: 3) + } + if !self.keyMap.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.keyMap, fieldNumber: 4) + } + if self.reason != .unknown { + try visitor.visitSingularEnumField(value: self.reason, fieldNumber: 5) + } + if !self.staleHeaderData.isEmpty { + try visitor.visitSingularStringField(value: self.staleHeaderData, fieldNumber: 6) + } + if !self.extensions.isEmpty { + try visitor.visitRepeatedMessageField(value: self.extensions, fieldNumber: 7) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_RouteLookupRequest, rhs: Grpc_Lookup_V1_RouteLookupRequest) -> Bool { + if lhs.targetType != rhs.targetType {return false} + if lhs.reason != rhs.reason {return false} + if lhs.staleHeaderData != rhs.staleHeaderData {return false} + if lhs.keyMap != rhs.keyMap {return false} + if lhs.extensions != rhs.extensions {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Lookup_V1_RouteLookupRequest.Reason: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "REASON_UNKNOWN"), + 1: .same(proto: "REASON_MISS"), + 2: .same(proto: "REASON_STALE"), + ] +} + +extension Grpc_Lookup_V1_RouteLookupResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RouteLookupResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 3: .same(proto: "targets"), + 2: .standard(proto: "header_data"), + 4: .same(proto: "extensions"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 2: try { try decoder.decodeSingularStringField(value: &self.headerData) }() + case 3: try { try decoder.decodeRepeatedStringField(value: &self.targets) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.extensions) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.headerData.isEmpty { + try visitor.visitSingularStringField(value: self.headerData, fieldNumber: 2) + } + if !self.targets.isEmpty { + try visitor.visitRepeatedStringField(value: self.targets, fieldNumber: 3) + } + if !self.extensions.isEmpty { + try visitor.visitRepeatedMessageField(value: self.extensions, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_RouteLookupResponse, rhs: Grpc_Lookup_V1_RouteLookupResponse) -> Bool { + if lhs.targets != rhs.targets {return false} + if lhs.headerData != rhs.headerData {return false} + if lhs.extensions != rhs.extensions {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift new file mode 100644 index 000000000..2d1e22104 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift @@ -0,0 +1,707 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/lookup/v1/rls_config.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2020 The gRPC Authors +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// Extract a key based on a given name (e.g. header name or query parameter +/// name). The name must match one of the names listed in the "name" field. If +/// the "required_match" field is true, one of the specified names must be +/// present for the keybuilder to match. +struct Grpc_Lookup_V1_NameMatcher { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The name that will be used in the RLS key_map to refer to this value. + /// If required_match is true, you may omit this field or set it to an empty + /// string, in which case the matcher will require a match, but won't update + /// the key_map. + var key: String = String() + + /// Ordered list of names (headers or query parameter names) that can supply + /// this value; the first one with a non-empty value is used. + var names: [String] = [] + + /// If true, make this extraction required; the key builder will not match + /// if no value is found. + var requiredMatch: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A GrpcKeyBuilder applies to a given gRPC service, name, and headers. +struct Grpc_Lookup_V1_GrpcKeyBuilder { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var names: [Grpc_Lookup_V1_GrpcKeyBuilder.Name] = [] + + var extraKeys: Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys { + get {return _extraKeys ?? Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys()} + set {_extraKeys = newValue} + } + /// Returns true if `extraKeys` has been explicitly set. + var hasExtraKeys: Bool {return self._extraKeys != nil} + /// Clears the value of `extraKeys`. Subsequent reads from it will return its default value. + mutating func clearExtraKeys() {self._extraKeys = nil} + + /// Extract keys from all listed headers. + /// For gRPC, it is an error to specify "required_match" on the NameMatcher + /// protos. + var headers: [Grpc_Lookup_V1_NameMatcher] = [] + + /// You can optionally set one or more specific key/value pairs to be added to + /// the key_map. This can be useful to identify which builder built the key, + /// for example if you are suppressing the actual method, but need to + /// separately cache and request all the matched methods. + var constantKeys: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// To match, one of the given Name fields must match; the service and method + /// fields are specified as fixed strings. The service name is required and + /// includes the proto package name. The method name may be omitted, in + /// which case any method on the given service is matched. + struct Name { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var service: String = String() + + var method: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + /// If you wish to include the host, service, or method names as keys in the + /// generated RouteLookupRequest, specify key names to use in the extra_keys + /// submessage. If a key name is empty, no key will be set for that value. + /// If this submessage is specified, the normal host/path fields will be left + /// unset in the RouteLookupRequest. We are deprecating host/path in the + /// RouteLookupRequest, so services should migrate to the ExtraKeys approach. + struct ExtraKeys { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var host: String = String() + + var service: String = String() + + var method: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} + + fileprivate var _extraKeys: Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys? = nil +} + +/// An HttpKeyBuilder applies to a given HTTP URL and headers. +/// +/// Path and host patterns use the matching syntax from gRPC transcoding to +/// extract named key/value pairs from the path and host components of the URL: +/// https://github.com/googleapis/googleapis/blob/master/google/api/http.proto +/// +/// It is invalid to specify the same key name in multiple places in a pattern. +/// +/// For a service where the project id can be expressed either as a subdomain or +/// in the path, separate HttpKeyBuilders must be used: +/// host_pattern: 'example.com' path_pattern: '/{id}/{object}/**' +/// host_pattern: '{id}.example.com' path_pattern: '/{object}/**' +/// If the host is exactly 'example.com', the first path segment will be used as +/// the id and the second segment as the object. If the host has a subdomain, the +/// subdomain will be used as the id and the first segment as the object. If +/// neither pattern matches, no keys will be extracted. +struct Grpc_Lookup_V1_HttpKeyBuilder { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// host_pattern is an ordered list of host template patterns for the desired + /// value. If any host_pattern values are specified, then at least one must + /// match, and the last one wins and sets any specified variables. A host + /// consists of labels separated by dots. Each label is matched against the + /// label in the pattern as follows: + /// - "*": Matches any single label. + /// - "**": Matches zero or more labels (first or last part of host only). + /// - "{=...}": One or more label capture, where "..." can be any + /// template that does not include a capture. + /// - "{}": A single label capture. Identical to {=*}. + /// + /// Examples: + /// - "example.com": Only applies to the exact host example.com. + /// - "*.example.com": Matches subdomains of example.com. + /// - "**.example.com": matches example.com, and all levels of subdomains. + /// - "{project}.example.com": Extracts the third level subdomain. + /// - "{project=**}.example.com": Extracts the third level+ subdomains. + /// - "{project=**}": Extracts the entire host. + var hostPatterns: [String] = [] + + /// path_pattern is an ordered list of path template patterns for the desired + /// value. If any path_pattern values are specified, then at least one must + /// match, and the last one wins and sets any specified variables. A path + /// consists of segments separated by slashes. Each segment is matched against + /// the segment in the pattern as follows: + /// - "*": Matches any single segment. + /// - "**": Matches zero or more segments (first or last part of path only). + /// - "{=...}": One or more segment capture, where "..." can be any + /// template that does not include a capture. + /// - "{}": A single segment capture. Identical to {=*}. + /// A custom method may also be specified by appending ":" and the custom + /// method name or "*" to indicate any custom method (including no custom + /// method). For example, "/*/projects/{project_id}/**:*" extracts + /// `{project_id}` for any version, resource and custom method that includes + /// it. By default, any custom method will be matched. + /// + /// Examples: + /// - "/v1/{name=messages/*}": extracts a name like "messages/12345". + /// - "/v1/messages/{message_id}": extracts a message_id like "12345". + /// - "/v1/users/{user_id}/messages/{message_id}": extracts two key values. + var pathPatterns: [String] = [] + + /// List of query parameter names to try to match. + /// For example: ["parent", "name", "resource.name"] + /// We extract all the specified query_parameters (case-sensitively). If any + /// are marked as "required_match" and are not present, this keybuilder fails + /// to match. If a given parameter appears multiple times (?foo=a&foo=b) we + /// will report it as a comma-separated string (foo=a,b). + var queryParameters: [Grpc_Lookup_V1_NameMatcher] = [] + + /// List of headers to try to match. + /// We extract all the specified header values (case-insensitively). If any + /// are marked as "required_match" and are not present, this keybuilder fails + /// to match. If a given header appears multiple times in the request we will + /// report it as a comma-separated string, in standard HTTP fashion. + var headers: [Grpc_Lookup_V1_NameMatcher] = [] + + /// You can optionally set one or more specific key/value pairs to be added to + /// the key_map. This can be useful to identify which builder built the key, + /// for example if you are suppressing a lot of information from the URL, but + /// need to separately cache and request URLs with that content. + var constantKeys: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Lookup_V1_RouteLookupConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Ordered specifications for constructing keys for HTTP requests. Last + /// match wins. If no HttpKeyBuilder matches, an empty key_map will be sent to + /// the lookup service; it should likely reply with a global default route + /// and raise an alert. + var httpKeybuilders: [Grpc_Lookup_V1_HttpKeyBuilder] = [] + + /// Unordered specifications for constructing keys for gRPC requests. All + /// GrpcKeyBuilders on this list must have unique "name" fields so that the + /// client is free to prebuild a hash map keyed by name. If no GrpcKeyBuilder + /// matches, an empty key_map will be sent to the lookup service; it should + /// likely reply with a global default route and raise an alert. + var grpcKeybuilders: [Grpc_Lookup_V1_GrpcKeyBuilder] = [] + + /// The name of the lookup service as a gRPC URI. Typically, this will be + /// a subdomain of the target, such as "lookup.datastore.googleapis.com". + var lookupService: String = String() + + /// Configure a timeout value for lookup service requests. + /// Defaults to 10 seconds if not specified. + var lookupServiceTimeout: SwiftProtobuf.Google_Protobuf_Duration { + get {return _lookupServiceTimeout ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_lookupServiceTimeout = newValue} + } + /// Returns true if `lookupServiceTimeout` has been explicitly set. + var hasLookupServiceTimeout: Bool {return self._lookupServiceTimeout != nil} + /// Clears the value of `lookupServiceTimeout`. Subsequent reads from it will return its default value. + mutating func clearLookupServiceTimeout() {self._lookupServiceTimeout = nil} + + /// How long are responses valid for (like HTTP Cache-Control). + /// If omitted or zero, the longest valid cache time is used. + /// This value is clamped to 5 minutes to avoid unflushable bad responses. + var maxAge: SwiftProtobuf.Google_Protobuf_Duration { + get {return _maxAge ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_maxAge = newValue} + } + /// Returns true if `maxAge` has been explicitly set. + var hasMaxAge: Bool {return self._maxAge != nil} + /// Clears the value of `maxAge`. Subsequent reads from it will return its default value. + mutating func clearMaxAge() {self._maxAge = nil} + + /// After a response has been in the client cache for this amount of time + /// and is re-requested, start an asynchronous RPC to re-validate it. + /// This value should be less than max_age by at least the length of a + /// typical RTT to the Route Lookup Service to fully mask the RTT latency. + /// If omitted, keys are only re-requested after they have expired. + var staleAge: SwiftProtobuf.Google_Protobuf_Duration { + get {return _staleAge ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_staleAge = newValue} + } + /// Returns true if `staleAge` has been explicitly set. + var hasStaleAge: Bool {return self._staleAge != nil} + /// Clears the value of `staleAge`. Subsequent reads from it will return its default value. + mutating func clearStaleAge() {self._staleAge = nil} + + /// Rough indicator of amount of memory to use for the client cache. Some of + /// the data structure overhead is not accounted for, so actual memory consumed + /// will be somewhat greater than this value. If this field is omitted or set + /// to zero, a client default will be used. The value may be capped to a lower + /// amount based on client configuration. + var cacheSizeBytes: Int64 = 0 + + /// This is a list of all the possible targets that can be returned by the + /// lookup service. If a target not on this list is returned, it will be + /// treated the same as an unhealthy target. + var validTargets: [String] = [] + + /// This value provides a default target to use if needed. If set, it will be + /// used if RLS returns an error, times out, or returns an invalid response. + /// Note that requests can be routed only to a subdomain of the original + /// target, e.g. "us_east_1.cloudbigtable.googleapis.com". + var defaultTarget: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _lookupServiceTimeout: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _maxAge: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _staleAge: SwiftProtobuf.Google_Protobuf_Duration? = nil +} + +/// RouteLookupClusterSpecifier is used in xDS to represent a cluster specifier +/// plugin for RLS. +struct Grpc_Lookup_V1_RouteLookupClusterSpecifier { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The RLS config for this cluster specifier plugin instance. + var routeLookupConfig: Grpc_Lookup_V1_RouteLookupConfig { + get {return _routeLookupConfig ?? Grpc_Lookup_V1_RouteLookupConfig()} + set {_routeLookupConfig = newValue} + } + /// Returns true if `routeLookupConfig` has been explicitly set. + var hasRouteLookupConfig: Bool {return self._routeLookupConfig != nil} + /// Clears the value of `routeLookupConfig`. Subsequent reads from it will return its default value. + mutating func clearRouteLookupConfig() {self._routeLookupConfig = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _routeLookupConfig: Grpc_Lookup_V1_RouteLookupConfig? = nil +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Lookup_V1_NameMatcher: @unchecked Sendable {} +extension Grpc_Lookup_V1_GrpcKeyBuilder: @unchecked Sendable {} +extension Grpc_Lookup_V1_GrpcKeyBuilder.Name: @unchecked Sendable {} +extension Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys: @unchecked Sendable {} +extension Grpc_Lookup_V1_HttpKeyBuilder: @unchecked Sendable {} +extension Grpc_Lookup_V1_RouteLookupConfig: @unchecked Sendable {} +extension Grpc_Lookup_V1_RouteLookupClusterSpecifier: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.lookup.v1" + +extension Grpc_Lookup_V1_NameMatcher: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".NameMatcher" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "key"), + 2: .same(proto: "names"), + 3: .standard(proto: "required_match"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.key) }() + case 2: try { try decoder.decodeRepeatedStringField(value: &self.names) }() + case 3: try { try decoder.decodeSingularBoolField(value: &self.requiredMatch) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.key.isEmpty { + try visitor.visitSingularStringField(value: self.key, fieldNumber: 1) + } + if !self.names.isEmpty { + try visitor.visitRepeatedStringField(value: self.names, fieldNumber: 2) + } + if self.requiredMatch != false { + try visitor.visitSingularBoolField(value: self.requiredMatch, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_NameMatcher, rhs: Grpc_Lookup_V1_NameMatcher) -> Bool { + if lhs.key != rhs.key {return false} + if lhs.names != rhs.names {return false} + if lhs.requiredMatch != rhs.requiredMatch {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Lookup_V1_GrpcKeyBuilder: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".GrpcKeyBuilder" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "names"), + 3: .standard(proto: "extra_keys"), + 2: .same(proto: "headers"), + 4: .standard(proto: "constant_keys"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.names) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.headers) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._extraKeys) }() + case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.constantKeys) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.names.isEmpty { + try visitor.visitRepeatedMessageField(value: self.names, fieldNumber: 1) + } + if !self.headers.isEmpty { + try visitor.visitRepeatedMessageField(value: self.headers, fieldNumber: 2) + } + try { if let v = self._extraKeys { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if !self.constantKeys.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.constantKeys, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_GrpcKeyBuilder, rhs: Grpc_Lookup_V1_GrpcKeyBuilder) -> Bool { + if lhs.names != rhs.names {return false} + if lhs._extraKeys != rhs._extraKeys {return false} + if lhs.headers != rhs.headers {return false} + if lhs.constantKeys != rhs.constantKeys {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Lookup_V1_GrpcKeyBuilder.Name: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Lookup_V1_GrpcKeyBuilder.protoMessageName + ".Name" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "service"), + 2: .same(proto: "method"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.service) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.method) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.service.isEmpty { + try visitor.visitSingularStringField(value: self.service, fieldNumber: 1) + } + if !self.method.isEmpty { + try visitor.visitSingularStringField(value: self.method, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_GrpcKeyBuilder.Name, rhs: Grpc_Lookup_V1_GrpcKeyBuilder.Name) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.method != rhs.method {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Lookup_V1_GrpcKeyBuilder.protoMessageName + ".ExtraKeys" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "host"), + 2: .same(proto: "service"), + 3: .same(proto: "method"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.service) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.method) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.host.isEmpty { + try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) + } + if !self.service.isEmpty { + try visitor.visitSingularStringField(value: self.service, fieldNumber: 2) + } + if !self.method.isEmpty { + try visitor.visitSingularStringField(value: self.method, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys, rhs: Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys) -> Bool { + if lhs.host != rhs.host {return false} + if lhs.service != rhs.service {return false} + if lhs.method != rhs.method {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Lookup_V1_HttpKeyBuilder: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HttpKeyBuilder" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "host_patterns"), + 2: .standard(proto: "path_patterns"), + 3: .standard(proto: "query_parameters"), + 4: .same(proto: "headers"), + 5: .standard(proto: "constant_keys"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedStringField(value: &self.hostPatterns) }() + case 2: try { try decoder.decodeRepeatedStringField(value: &self.pathPatterns) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.queryParameters) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.headers) }() + case 5: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.constantKeys) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.hostPatterns.isEmpty { + try visitor.visitRepeatedStringField(value: self.hostPatterns, fieldNumber: 1) + } + if !self.pathPatterns.isEmpty { + try visitor.visitRepeatedStringField(value: self.pathPatterns, fieldNumber: 2) + } + if !self.queryParameters.isEmpty { + try visitor.visitRepeatedMessageField(value: self.queryParameters, fieldNumber: 3) + } + if !self.headers.isEmpty { + try visitor.visitRepeatedMessageField(value: self.headers, fieldNumber: 4) + } + if !self.constantKeys.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.constantKeys, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_HttpKeyBuilder, rhs: Grpc_Lookup_V1_HttpKeyBuilder) -> Bool { + if lhs.hostPatterns != rhs.hostPatterns {return false} + if lhs.pathPatterns != rhs.pathPatterns {return false} + if lhs.queryParameters != rhs.queryParameters {return false} + if lhs.headers != rhs.headers {return false} + if lhs.constantKeys != rhs.constantKeys {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Lookup_V1_RouteLookupConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RouteLookupConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "http_keybuilders"), + 2: .standard(proto: "grpc_keybuilders"), + 3: .standard(proto: "lookup_service"), + 4: .standard(proto: "lookup_service_timeout"), + 5: .standard(proto: "max_age"), + 6: .standard(proto: "stale_age"), + 7: .standard(proto: "cache_size_bytes"), + 8: .standard(proto: "valid_targets"), + 9: .standard(proto: "default_target"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.httpKeybuilders) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.grpcKeybuilders) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.lookupService) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._lookupServiceTimeout) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._maxAge) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._staleAge) }() + case 7: try { try decoder.decodeSingularInt64Field(value: &self.cacheSizeBytes) }() + case 8: try { try decoder.decodeRepeatedStringField(value: &self.validTargets) }() + case 9: try { try decoder.decodeSingularStringField(value: &self.defaultTarget) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.httpKeybuilders.isEmpty { + try visitor.visitRepeatedMessageField(value: self.httpKeybuilders, fieldNumber: 1) + } + if !self.grpcKeybuilders.isEmpty { + try visitor.visitRepeatedMessageField(value: self.grpcKeybuilders, fieldNumber: 2) + } + if !self.lookupService.isEmpty { + try visitor.visitSingularStringField(value: self.lookupService, fieldNumber: 3) + } + try { if let v = self._lookupServiceTimeout { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try { if let v = self._maxAge { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try { if let v = self._staleAge { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + if self.cacheSizeBytes != 0 { + try visitor.visitSingularInt64Field(value: self.cacheSizeBytes, fieldNumber: 7) + } + if !self.validTargets.isEmpty { + try visitor.visitRepeatedStringField(value: self.validTargets, fieldNumber: 8) + } + if !self.defaultTarget.isEmpty { + try visitor.visitSingularStringField(value: self.defaultTarget, fieldNumber: 9) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_RouteLookupConfig, rhs: Grpc_Lookup_V1_RouteLookupConfig) -> Bool { + if lhs.httpKeybuilders != rhs.httpKeybuilders {return false} + if lhs.grpcKeybuilders != rhs.grpcKeybuilders {return false} + if lhs.lookupService != rhs.lookupService {return false} + if lhs._lookupServiceTimeout != rhs._lookupServiceTimeout {return false} + if lhs._maxAge != rhs._maxAge {return false} + if lhs._staleAge != rhs._staleAge {return false} + if lhs.cacheSizeBytes != rhs.cacheSizeBytes {return false} + if lhs.validTargets != rhs.validTargets {return false} + if lhs.defaultTarget != rhs.defaultTarget {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Lookup_V1_RouteLookupClusterSpecifier: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RouteLookupClusterSpecifier" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "route_lookup_config"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._routeLookupConfig) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._routeLookupConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Lookup_V1_RouteLookupClusterSpecifier, rhs: Grpc_Lookup_V1_RouteLookupClusterSpecifier) -> Bool { + if lhs._routeLookupConfig != rhs._routeLookupConfig {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift new file mode 100644 index 000000000..8ebd3ec44 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift @@ -0,0 +1,4149 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/service_config/service_config.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2016 The gRPC Authors +// +// 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. + +// A ServiceConfig is supplied when a service is deployed. It mostly contains +// parameters for how clients that connect to the service should behave (for +// example, the load balancing policy to use to pick between service replicas). +// +// The configuration options provided here act as overrides to automatically +// chosen option values. Service owners should be conservative in specifying +// options as the system is likely to choose better values for these options in +// the vast majority of cases. In other words, please specify a configuration +// option only if you really have to, and avoid copy-paste inclusion of configs. +// +// Note that gRPC uses the service config in JSON form, not in protobuf +// form. This proto definition is intended to help document the schema but +// will not actually be used directly by gRPC. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// Configuration for a method. +struct Grpc_ServiceConfig_MethodConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: [Grpc_ServiceConfig_MethodConfig.Name] = [] + + /// Whether RPCs sent to this method should wait until the connection is + /// ready by default. If false, the RPC will abort immediately if there is + /// a transient failure connecting to the server. Otherwise, gRPC will + /// attempt to connect until the deadline is exceeded. + /// + /// The value specified via the gRPC client API will override the value + /// set here. However, note that setting the value in the client API will + /// also affect transient errors encountered during name resolution, which + /// cannot be caught by the value here, since the service config is + /// obtained by the gRPC client via name resolution. + var waitForReady: SwiftProtobuf.Google_Protobuf_BoolValue { + get {return _waitForReady ?? SwiftProtobuf.Google_Protobuf_BoolValue()} + set {_waitForReady = newValue} + } + /// Returns true if `waitForReady` has been explicitly set. + var hasWaitForReady: Bool {return self._waitForReady != nil} + /// Clears the value of `waitForReady`. Subsequent reads from it will return its default value. + mutating func clearWaitForReady() {self._waitForReady = nil} + + /// The default timeout in seconds for RPCs sent to this method. This can be + /// overridden in code. If no reply is received in the specified amount of + /// time, the request is aborted and a DEADLINE_EXCEEDED error status + /// is returned to the caller. + /// + /// The actual deadline used will be the minimum of the value specified here + /// and the value set by the application via the gRPC client API. If either + /// one is not set, then the other will be used. If neither is set, then the + /// request has no deadline. + var timeout: SwiftProtobuf.Google_Protobuf_Duration { + get {return _timeout ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_timeout = newValue} + } + /// Returns true if `timeout` has been explicitly set. + var hasTimeout: Bool {return self._timeout != nil} + /// Clears the value of `timeout`. Subsequent reads from it will return its default value. + mutating func clearTimeout() {self._timeout = nil} + + /// The maximum allowed payload size for an individual request or object in a + /// stream (client->server) in bytes. The size which is measured is the + /// serialized payload after per-message compression (but before stream + /// compression) in bytes. This applies both to streaming and non-streaming + /// requests. + /// + /// The actual value used is the minimum of the value specified here and the + /// value set by the application via the gRPC client API. If either one is + /// not set, then the other will be used. If neither is set, then the + /// built-in default is used. + /// + /// If a client attempts to send an object larger than this value, it will not + /// be sent and the client will see a ClientError. + /// Note that 0 is a valid value, meaning that the request message + /// must be empty. + var maxRequestMessageBytes: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _maxRequestMessageBytes ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_maxRequestMessageBytes = newValue} + } + /// Returns true if `maxRequestMessageBytes` has been explicitly set. + var hasMaxRequestMessageBytes: Bool {return self._maxRequestMessageBytes != nil} + /// Clears the value of `maxRequestMessageBytes`. Subsequent reads from it will return its default value. + mutating func clearMaxRequestMessageBytes() {self._maxRequestMessageBytes = nil} + + /// The maximum allowed payload size for an individual response or object in a + /// stream (server->client) in bytes. The size which is measured is the + /// serialized payload after per-message compression (but before stream + /// compression) in bytes. This applies both to streaming and non-streaming + /// requests. + /// + /// The actual value used is the minimum of the value specified here and the + /// value set by the application via the gRPC client API. If either one is + /// not set, then the other will be used. If neither is set, then the + /// built-in default is used. + /// + /// If a server attempts to send an object larger than this value, it will not + /// be sent, and a ServerError will be sent to the client instead. + /// Note that 0 is a valid value, meaning that the response message + /// must be empty. + var maxResponseMessageBytes: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _maxResponseMessageBytes ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_maxResponseMessageBytes = newValue} + } + /// Returns true if `maxResponseMessageBytes` has been explicitly set. + var hasMaxResponseMessageBytes: Bool {return self._maxResponseMessageBytes != nil} + /// Clears the value of `maxResponseMessageBytes`. Subsequent reads from it will return its default value. + mutating func clearMaxResponseMessageBytes() {self._maxResponseMessageBytes = nil} + + /// Only one of retry_policy or hedging_policy may be set. If neither is set, + /// RPCs will not be retried or hedged. + var retryOrHedgingPolicy: Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy? = nil + + var retryPolicy: Grpc_ServiceConfig_MethodConfig.RetryPolicy { + get { + if case .retryPolicy(let v)? = retryOrHedgingPolicy {return v} + return Grpc_ServiceConfig_MethodConfig.RetryPolicy() + } + set {retryOrHedgingPolicy = .retryPolicy(newValue)} + } + + var hedgingPolicy: Grpc_ServiceConfig_MethodConfig.HedgingPolicy { + get { + if case .hedgingPolicy(let v)? = retryOrHedgingPolicy {return v} + return Grpc_ServiceConfig_MethodConfig.HedgingPolicy() + } + set {retryOrHedgingPolicy = .hedgingPolicy(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Only one of retry_policy or hedging_policy may be set. If neither is set, + /// RPCs will not be retried or hedged. + enum OneOf_RetryOrHedgingPolicy: Equatable { + case retryPolicy(Grpc_ServiceConfig_MethodConfig.RetryPolicy) + case hedgingPolicy(Grpc_ServiceConfig_MethodConfig.HedgingPolicy) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy, rhs: Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.retryPolicy, .retryPolicy): return { + guard case .retryPolicy(let l) = lhs, case .retryPolicy(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.hedgingPolicy, .hedgingPolicy): return { + guard case .hedgingPolicy(let l) = lhs, case .hedgingPolicy(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + /// The names of the methods to which this configuration applies. + /// - MethodConfig without names (empty list) will be skipped. + /// - Each name entry must be unique across the entire ServiceConfig. + /// - If the 'method' field is empty, this MethodConfig specifies the defaults + /// for all methods for the specified service. + /// - If the 'service' field is empty, the 'method' field must be empty, and + /// this MethodConfig specifies the default for all methods (it's the default + /// config). + /// + /// When determining which MethodConfig to use for a given RPC, the most + /// specific match wins. For example, let's say that the service config + /// contains the following MethodConfig entries: + /// + /// method_config { name { } ... } + /// method_config { name { service: "MyService" } ... } + /// method_config { name { service: "MyService" method: "Foo" } ... } + /// + /// MyService/Foo will use the third entry, because it exactly matches the + /// service and method name. MyService/Bar will use the second entry, because + /// it provides the default for all methods of MyService. AnotherService/Baz + /// will use the first entry, because it doesn't match the other two. + /// + /// In JSON representation, value "", value `null`, and not present are the + /// same. The following are the same Name: + /// - { "service": "s" } + /// - { "service": "s", "method": null } + /// - { "service": "s", "method": "" } + struct Name { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Required. Includes proto package name. + var service: String = String() + + var method: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + /// The retry policy for outgoing RPCs. + struct RetryPolicy { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The maximum number of RPC attempts, including the original attempt. + /// + /// This field is required and must be greater than 1. + /// Any value greater than 5 will be treated as if it were 5. + var maxAttempts: UInt32 = 0 + + /// Exponential backoff parameters. The initial retry attempt will occur at + /// random(0, initial_backoff). In general, the nth attempt will occur at + /// random(0, + /// min(initial_backoff*backoff_multiplier**(n-1), max_backoff)). + /// Required. Must be greater than zero. + var initialBackoff: SwiftProtobuf.Google_Protobuf_Duration { + get {return _initialBackoff ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_initialBackoff = newValue} + } + /// Returns true if `initialBackoff` has been explicitly set. + var hasInitialBackoff: Bool {return self._initialBackoff != nil} + /// Clears the value of `initialBackoff`. Subsequent reads from it will return its default value. + mutating func clearInitialBackoff() {self._initialBackoff = nil} + + /// Required. Must be greater than zero. + var maxBackoff: SwiftProtobuf.Google_Protobuf_Duration { + get {return _maxBackoff ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_maxBackoff = newValue} + } + /// Returns true if `maxBackoff` has been explicitly set. + var hasMaxBackoff: Bool {return self._maxBackoff != nil} + /// Clears the value of `maxBackoff`. Subsequent reads from it will return its default value. + mutating func clearMaxBackoff() {self._maxBackoff = nil} + + /// Required. Must be greater than zero. + var backoffMultiplier: Float = 0 + + /// The set of status codes which may be retried. + /// + /// This field is required and must be non-empty. + var retryableStatusCodes: [Google_Rpc_Code] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _initialBackoff: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _maxBackoff: SwiftProtobuf.Google_Protobuf_Duration? = nil + } + + /// The hedging policy for outgoing RPCs. Hedged RPCs may execute more than + /// once on the server, so only idempotent methods should specify a hedging + /// policy. + struct HedgingPolicy { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The hedging policy will send up to max_requests RPCs. + /// This number represents the total number of all attempts, including + /// the original attempt. + /// + /// This field is required and must be greater than 1. + /// Any value greater than 5 will be treated as if it were 5. + var maxAttempts: UInt32 = 0 + + /// The first RPC will be sent immediately, but the max_requests-1 subsequent + /// hedged RPCs will be sent at intervals of every hedging_delay. Set this + /// to 0 to immediately send all max_requests RPCs. + var hedgingDelay: SwiftProtobuf.Google_Protobuf_Duration { + get {return _hedgingDelay ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_hedgingDelay = newValue} + } + /// Returns true if `hedgingDelay` has been explicitly set. + var hasHedgingDelay: Bool {return self._hedgingDelay != nil} + /// Clears the value of `hedgingDelay`. Subsequent reads from it will return its default value. + mutating func clearHedgingDelay() {self._hedgingDelay = nil} + + /// The set of status codes which indicate other hedged RPCs may still + /// succeed. If a non-fatal status code is returned by the server, hedged + /// RPCs will continue. Otherwise, outstanding requests will be canceled and + /// the error returned to the client application layer. + /// + /// This field is optional. + var nonFatalStatusCodes: [Google_Rpc_Code] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _hedgingDelay: SwiftProtobuf.Google_Protobuf_Duration? = nil + } + + init() {} + + fileprivate var _waitForReady: SwiftProtobuf.Google_Protobuf_BoolValue? = nil + fileprivate var _timeout: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _maxRequestMessageBytes: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _maxResponseMessageBytes: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil +} + +/// Configuration for pick_first LB policy. +struct Grpc_ServiceConfig_PickFirstConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// If set to true, instructs the LB policy to randomly shuffle the list of + /// addresses received from the name resolver before attempting to connect to + /// them. + var shuffleAddressList: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Configuration for round_robin LB policy. +struct Grpc_ServiceConfig_RoundRobinConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Configuration for weighted_round_robin LB policy. +struct Grpc_ServiceConfig_WeightedRoundRobinLbConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Whether to enable out-of-band utilization reporting collection from + /// the endpoints. By default, per-request utilization reporting is used. + var enableOobLoadReport: SwiftProtobuf.Google_Protobuf_BoolValue { + get {return _enableOobLoadReport ?? SwiftProtobuf.Google_Protobuf_BoolValue()} + set {_enableOobLoadReport = newValue} + } + /// Returns true if `enableOobLoadReport` has been explicitly set. + var hasEnableOobLoadReport: Bool {return self._enableOobLoadReport != nil} + /// Clears the value of `enableOobLoadReport`. Subsequent reads from it will return its default value. + mutating func clearEnableOobLoadReport() {self._enableOobLoadReport = nil} + + /// Load reporting interval to request from the server. Note that the + /// server may not provide reports as frequently as the client requests. + /// Used only when enable_oob_load_report is true. Default is 10 seconds. + var oobReportingPeriod: SwiftProtobuf.Google_Protobuf_Duration { + get {return _oobReportingPeriod ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_oobReportingPeriod = newValue} + } + /// Returns true if `oobReportingPeriod` has been explicitly set. + var hasOobReportingPeriod: Bool {return self._oobReportingPeriod != nil} + /// Clears the value of `oobReportingPeriod`. Subsequent reads from it will return its default value. + mutating func clearOobReportingPeriod() {self._oobReportingPeriod = nil} + + /// A given endpoint must report load metrics continuously for at least + /// this long before the endpoint weight will be used. This avoids + /// churn when the set of endpoint addresses changes. Takes effect + /// both immediately after we establish a connection to an endpoint and + /// after weight_expiration_period has caused us to stop using the most + /// recent load metrics. Default is 10 seconds. + var blackoutPeriod: SwiftProtobuf.Google_Protobuf_Duration { + get {return _blackoutPeriod ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_blackoutPeriod = newValue} + } + /// Returns true if `blackoutPeriod` has been explicitly set. + var hasBlackoutPeriod: Bool {return self._blackoutPeriod != nil} + /// Clears the value of `blackoutPeriod`. Subsequent reads from it will return its default value. + mutating func clearBlackoutPeriod() {self._blackoutPeriod = nil} + + /// If a given endpoint has not reported load metrics in this long, + /// then we stop using the reported weight. This ensures that we do + /// not continue to use very stale weights. Once we stop using a stale + /// value, if we later start seeing fresh reports again, the + /// blackout_period applies. Defaults to 3 minutes. + var weightExpirationPeriod: SwiftProtobuf.Google_Protobuf_Duration { + get {return _weightExpirationPeriod ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_weightExpirationPeriod = newValue} + } + /// Returns true if `weightExpirationPeriod` has been explicitly set. + var hasWeightExpirationPeriod: Bool {return self._weightExpirationPeriod != nil} + /// Clears the value of `weightExpirationPeriod`. Subsequent reads from it will return its default value. + mutating func clearWeightExpirationPeriod() {self._weightExpirationPeriod = nil} + + /// How often endpoint weights are recalculated. Values less than 100ms are + /// capped at 100ms. Default is 1 second. + var weightUpdatePeriod: SwiftProtobuf.Google_Protobuf_Duration { + get {return _weightUpdatePeriod ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_weightUpdatePeriod = newValue} + } + /// Returns true if `weightUpdatePeriod` has been explicitly set. + var hasWeightUpdatePeriod: Bool {return self._weightUpdatePeriod != nil} + /// Clears the value of `weightUpdatePeriod`. Subsequent reads from it will return its default value. + mutating func clearWeightUpdatePeriod() {self._weightUpdatePeriod = nil} + + /// The multiplier used to adjust endpoint weights with the error rate + /// calculated as eps/qps. Configuration is rejected if this value is negative. + /// Default is 1.0. + var errorUtilizationPenalty: SwiftProtobuf.Google_Protobuf_FloatValue { + get {return _errorUtilizationPenalty ?? SwiftProtobuf.Google_Protobuf_FloatValue()} + set {_errorUtilizationPenalty = newValue} + } + /// Returns true if `errorUtilizationPenalty` has been explicitly set. + var hasErrorUtilizationPenalty: Bool {return self._errorUtilizationPenalty != nil} + /// Clears the value of `errorUtilizationPenalty`. Subsequent reads from it will return its default value. + mutating func clearErrorUtilizationPenalty() {self._errorUtilizationPenalty = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _enableOobLoadReport: SwiftProtobuf.Google_Protobuf_BoolValue? = nil + fileprivate var _oobReportingPeriod: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _blackoutPeriod: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _weightExpirationPeriod: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _weightUpdatePeriod: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _errorUtilizationPenalty: SwiftProtobuf.Google_Protobuf_FloatValue? = nil +} + +/// Configuration for outlier_detection LB policy +struct Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The time interval between ejection analysis sweeps. This can result in + /// both new ejections as well as addresses being returned to service. Defaults + /// to 10000ms or 10s. + var interval: SwiftProtobuf.Google_Protobuf_Duration { + get {return _interval ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_interval = newValue} + } + /// Returns true if `interval` has been explicitly set. + var hasInterval: Bool {return self._interval != nil} + /// Clears the value of `interval`. Subsequent reads from it will return its default value. + mutating func clearInterval() {self._interval = nil} + + /// The base time that as address is ejected for. The real time is equal to the + /// base time multiplied by the number of times the address has been ejected. + /// Defaults to 30000ms or 30s. + var baseEjectionTime: SwiftProtobuf.Google_Protobuf_Duration { + get {return _baseEjectionTime ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_baseEjectionTime = newValue} + } + /// Returns true if `baseEjectionTime` has been explicitly set. + var hasBaseEjectionTime: Bool {return self._baseEjectionTime != nil} + /// Clears the value of `baseEjectionTime`. Subsequent reads from it will return its default value. + mutating func clearBaseEjectionTime() {self._baseEjectionTime = nil} + + /// The maximum time that an address is ejected for. If not specified, the default value (300000ms or 300s) or + /// the base_ejection_time value is applied, whatever is larger. + var maxEjectionTime: SwiftProtobuf.Google_Protobuf_Duration { + get {return _maxEjectionTime ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_maxEjectionTime = newValue} + } + /// Returns true if `maxEjectionTime` has been explicitly set. + var hasMaxEjectionTime: Bool {return self._maxEjectionTime != nil} + /// Clears the value of `maxEjectionTime`. Subsequent reads from it will return its default value. + mutating func clearMaxEjectionTime() {self._maxEjectionTime = nil} + + /// The maximum % of an address list that can be ejected due to outlier + /// detection. Defaults to 10% but will eject at least one address regardless of the value. + var maxEjectionPercent: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _maxEjectionPercent ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_maxEjectionPercent = newValue} + } + /// Returns true if `maxEjectionPercent` has been explicitly set. + var hasMaxEjectionPercent: Bool {return self._maxEjectionPercent != nil} + /// Clears the value of `maxEjectionPercent`. Subsequent reads from it will return its default value. + mutating func clearMaxEjectionPercent() {self._maxEjectionPercent = nil} + + /// If set, success rate ejections will be performed + var successRateEjection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection { + get {return _successRateEjection ?? Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection()} + set {_successRateEjection = newValue} + } + /// Returns true if `successRateEjection` has been explicitly set. + var hasSuccessRateEjection: Bool {return self._successRateEjection != nil} + /// Clears the value of `successRateEjection`. Subsequent reads from it will return its default value. + mutating func clearSuccessRateEjection() {self._successRateEjection = nil} + + /// If set, failure rate ejections will be performed + var failurePercentageEjection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection { + get {return _failurePercentageEjection ?? Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection()} + set {_failurePercentageEjection = newValue} + } + /// Returns true if `failurePercentageEjection` has been explicitly set. + var hasFailurePercentageEjection: Bool {return self._failurePercentageEjection != nil} + /// Clears the value of `failurePercentageEjection`. Subsequent reads from it will return its default value. + mutating func clearFailurePercentageEjection() {self._failurePercentageEjection = nil} + + /// The config for the child policy + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Parameters for the success rate ejection algorithm. + /// This algorithm monitors the request success rate for all endpoints and + /// ejects individual endpoints whose success rates are statistical outliers. + struct SuccessRateEjection { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// This factor is used to determine the ejection threshold for success rate + /// outlier ejection. The ejection threshold is the difference between the + /// mean success rate, and the product of this factor and the standard + /// deviation of the mean success rate: mean - (stdev * + /// success_rate_stdev_factor). This factor is divided by a thousand to get a + /// double. That is, if the desired factor is 1.9, the runtime value should + /// be 1900. Defaults to 1900. + var stdevFactor: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _stdevFactor ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_stdevFactor = newValue} + } + /// Returns true if `stdevFactor` has been explicitly set. + var hasStdevFactor: Bool {return self._stdevFactor != nil} + /// Clears the value of `stdevFactor`. Subsequent reads from it will return its default value. + mutating func clearStdevFactor() {self._stdevFactor = nil} + + /// The % chance that an address will be actually ejected when an outlier status + /// is detected through success rate statistics. This setting can be used to + /// disable ejection or to ramp it up slowly. Defaults to 100. + var enforcementPercentage: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _enforcementPercentage ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_enforcementPercentage = newValue} + } + /// Returns true if `enforcementPercentage` has been explicitly set. + var hasEnforcementPercentage: Bool {return self._enforcementPercentage != nil} + /// Clears the value of `enforcementPercentage`. Subsequent reads from it will return its default value. + mutating func clearEnforcementPercentage() {self._enforcementPercentage = nil} + + /// The number of addresses that must have enough request volume to + /// detect success rate outliers. If the number of addresses is less than this + /// setting, outlier detection via success rate statistics is not performed + /// for any addresses. Defaults to 5. + var minimumHosts: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _minimumHosts ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_minimumHosts = newValue} + } + /// Returns true if `minimumHosts` has been explicitly set. + var hasMinimumHosts: Bool {return self._minimumHosts != nil} + /// Clears the value of `minimumHosts`. Subsequent reads from it will return its default value. + mutating func clearMinimumHosts() {self._minimumHosts = nil} + + /// The minimum number of total requests that must be collected in one + /// interval (as defined by the interval duration above) to include this address + /// in success rate based outlier detection. If the volume is lower than this + /// setting, outlier detection via success rate statistics is not performed + /// for that address. Defaults to 100. + var requestVolume: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _requestVolume ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_requestVolume = newValue} + } + /// Returns true if `requestVolume` has been explicitly set. + var hasRequestVolume: Bool {return self._requestVolume != nil} + /// Clears the value of `requestVolume`. Subsequent reads from it will return its default value. + mutating func clearRequestVolume() {self._requestVolume = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _stdevFactor: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _enforcementPercentage: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _minimumHosts: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _requestVolume: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + } + + /// Parameters for the failure percentage algorithm. + /// This algorithm ejects individual endpoints whose failure rate is greater than + /// some threshold, independently of any other endpoint. + struct FailurePercentageEjection { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The failure percentage to use when determining failure percentage-based outlier detection. If + /// the failure percentage of a given address is greater than or equal to this value, it will be + /// ejected. Defaults to 85. + var threshold: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _threshold ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_threshold = newValue} + } + /// Returns true if `threshold` has been explicitly set. + var hasThreshold: Bool {return self._threshold != nil} + /// Clears the value of `threshold`. Subsequent reads from it will return its default value. + mutating func clearThreshold() {self._threshold = nil} + + /// The % chance that an address will be actually ejected when an outlier status is detected through + /// failure percentage statistics. This setting can be used to disable ejection or to ramp it up + /// slowly. Defaults to 100. + var enforcementPercentage: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _enforcementPercentage ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_enforcementPercentage = newValue} + } + /// Returns true if `enforcementPercentage` has been explicitly set. + var hasEnforcementPercentage: Bool {return self._enforcementPercentage != nil} + /// Clears the value of `enforcementPercentage`. Subsequent reads from it will return its default value. + mutating func clearEnforcementPercentage() {self._enforcementPercentage = nil} + + /// The minimum number of addresses in order to perform failure percentage-based ejection. + /// If the total number of addresses is less than this value, failure percentage-based + /// ejection will not be performed. Defaults to 5. + var minimumHosts: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _minimumHosts ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_minimumHosts = newValue} + } + /// Returns true if `minimumHosts` has been explicitly set. + var hasMinimumHosts: Bool {return self._minimumHosts != nil} + /// Clears the value of `minimumHosts`. Subsequent reads from it will return its default value. + mutating func clearMinimumHosts() {self._minimumHosts = nil} + + /// The minimum number of total requests that must be collected in one interval (as defined by the + /// interval duration above) to perform failure percentage-based ejection for this address. If the + /// volume is lower than this setting, failure percentage-based ejection will not be performed for + /// this host. Defaults to 50. + var requestVolume: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _requestVolume ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_requestVolume = newValue} + } + /// Returns true if `requestVolume` has been explicitly set. + var hasRequestVolume: Bool {return self._requestVolume != nil} + /// Clears the value of `requestVolume`. Subsequent reads from it will return its default value. + mutating func clearRequestVolume() {self._requestVolume = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _threshold: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _enforcementPercentage: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _minimumHosts: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _requestVolume: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + } + + init() {} + + fileprivate var _interval: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _baseEjectionTime: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _maxEjectionTime: SwiftProtobuf.Google_Protobuf_Duration? = nil + fileprivate var _maxEjectionPercent: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + fileprivate var _successRateEjection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection? = nil + fileprivate var _failurePercentageEjection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection? = nil +} + +/// Configuration for grpclb LB policy. +struct Grpc_ServiceConfig_GrpcLbConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Optional. What LB policy to use for routing between the backend + /// addresses. If unset, defaults to round_robin. + /// Currently, the only supported values are round_robin and pick_first. + /// Note that this will be used both in balancer mode and in fallback mode. + /// Multiple LB policies can be specified; clients will iterate through + /// the list in order and stop at the first policy that they support. + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Optional. If specified, overrides the name of the service to be sent to + /// the balancer. + var serviceName: String = String() + + /// Optional. The timeout in seconds for receiving the server list from the LB + /// server. Defaults to 10s. + var initialFallbackTimeout: SwiftProtobuf.Google_Protobuf_Duration { + get {return _initialFallbackTimeout ?? SwiftProtobuf.Google_Protobuf_Duration()} + set {_initialFallbackTimeout = newValue} + } + /// Returns true if `initialFallbackTimeout` has been explicitly set. + var hasInitialFallbackTimeout: Bool {return self._initialFallbackTimeout != nil} + /// Clears the value of `initialFallbackTimeout`. Subsequent reads from it will return its default value. + mutating func clearInitialFallbackTimeout() {self._initialFallbackTimeout = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _initialFallbackTimeout: SwiftProtobuf.Google_Protobuf_Duration? = nil +} + +/// Configuration for priority LB policy. +struct Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var children: Dictionary = [:] + + /// A list of child names in decreasing priority order + /// (i.e., first element is the highest priority). + var priorities: [String] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// A map of name to child policy configuration. + /// The names are used to allow the priority policy to update + /// existing child policies instead of creating new ones every + /// time it receives a config update. + struct Child { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var config: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// If true, will ignore reresolution requests from this child. + var ignoreReresolutionRequests: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} +} + +/// Configuration for weighted_target LB policy. +struct Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var targets: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct Target { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var weight: UInt32 = 0 + + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} +} + +/// Config for RLS LB policy. +struct Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var routeLookupConfig: Grpc_Lookup_V1_RouteLookupConfig { + get {return _storage._routeLookupConfig ?? Grpc_Lookup_V1_RouteLookupConfig()} + set {_uniqueStorage()._routeLookupConfig = newValue} + } + /// Returns true if `routeLookupConfig` has been explicitly set. + var hasRouteLookupConfig: Bool {return _storage._routeLookupConfig != nil} + /// Clears the value of `routeLookupConfig`. Subsequent reads from it will return its default value. + mutating func clearRouteLookupConfig() {_uniqueStorage()._routeLookupConfig = nil} + + /// Service config to use for the RLS channel. + var routeLookupChannelServiceConfig: Grpc_ServiceConfig_ServiceConfig { + get {return _storage._routeLookupChannelServiceConfig ?? Grpc_ServiceConfig_ServiceConfig()} + set {_uniqueStorage()._routeLookupChannelServiceConfig = newValue} + } + /// Returns true if `routeLookupChannelServiceConfig` has been explicitly set. + var hasRouteLookupChannelServiceConfig: Bool {return _storage._routeLookupChannelServiceConfig != nil} + /// Clears the value of `routeLookupChannelServiceConfig`. Subsequent reads from it will return its default value. + mutating func clearRouteLookupChannelServiceConfig() {_uniqueStorage()._routeLookupChannelServiceConfig = nil} + + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] { + get {return _storage._childPolicy} + set {_uniqueStorage()._childPolicy = newValue} + } + + /// Field name to add to child policy config to contain the target name. + var childPolicyConfigTargetFieldName: String { + get {return _storage._childPolicyConfigTargetFieldName} + set {_uniqueStorage()._childPolicyConfigTargetFieldName = newValue} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +/// Configuration for xds_cluster_manager_experimental LB policy. +struct Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var children: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct Child { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} +} + +/// Configuration for the cds LB policy. +struct Grpc_ServiceConfig_CdsConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Required. + var cluster: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Represents an xDS server. +struct Grpc_ServiceConfig_XdsServer { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Required. + var serverUri: String = String() + + /// A list of channel creds to use. The first supported type will be used. + var channelCreds: [Grpc_ServiceConfig_XdsServer.ChannelCredentials] = [] + + /// A repeated list of server features. + var serverFeatures: [SwiftProtobuf.Google_Protobuf_Value] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct ChannelCredentials { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Required. + var type: String = String() + + /// Optional JSON config. + var config: SwiftProtobuf.Google_Protobuf_Struct { + get {return _config ?? SwiftProtobuf.Google_Protobuf_Struct()} + set {_config = newValue} + } + /// Returns true if `config` has been explicitly set. + var hasConfig: Bool {return self._config != nil} + /// Clears the value of `config`. Subsequent reads from it will return its default value. + mutating func clearConfig() {self._config = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _config: SwiftProtobuf.Google_Protobuf_Struct? = nil + } + + init() {} +} + +/// Configuration for xds_cluster_resolver LB policy. +struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Ordered list of discovery mechanisms. + /// Must have at least one element. + /// Results from each discovery mechanism are concatenated together in + /// successive priorities. + var discoveryMechanisms: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism] = [] + + /// xDS LB policy. Will be used as the child config of the xds_cluster_impl LB policy. + var xdsLbPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Describes a discovery mechanism instance. + /// For EDS or LOGICAL_DNS clusters, there will be exactly one + /// DiscoveryMechanism, which will describe the cluster of the parent + /// CDS policy. + /// For aggregate clusters, there will be one DiscoveryMechanism for each + /// underlying cluster. + struct DiscoveryMechanism { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Cluster name. + var cluster: String { + get {return _storage._cluster} + set {_uniqueStorage()._cluster = newValue} + } + + /// LRS server to send load reports to. + /// If not present, load reporting will be disabled. + /// If set to the empty string, load reporting will be sent to the same + /// server that we obtained CDS data from. + /// DEPRECATED: Use new lrs_load_reporting_server field instead. + var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _storage._lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_uniqueStorage()._lrsLoadReportingServerName = newValue} + } + /// Returns true if `lrsLoadReportingServerName` has been explicitly set. + var hasLrsLoadReportingServerName: Bool {return _storage._lrsLoadReportingServerName != nil} + /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServerName() {_uniqueStorage()._lrsLoadReportingServerName = nil} + + /// LRS server to send load reports to. + /// If not present, load reporting will be disabled. + /// Supercedes lrs_load_reporting_server_name field. + var lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer { + get {return _storage._lrsLoadReportingServer ?? Grpc_ServiceConfig_XdsServer()} + set {_uniqueStorage()._lrsLoadReportingServer = newValue} + } + /// Returns true if `lrsLoadReportingServer` has been explicitly set. + var hasLrsLoadReportingServer: Bool {return _storage._lrsLoadReportingServer != nil} + /// Clears the value of `lrsLoadReportingServer`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServer() {_uniqueStorage()._lrsLoadReportingServer = nil} + + /// Maximum number of outstanding requests can be made to the upstream + /// cluster. Default is 1024. + var maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _storage._maxConcurrentRequests ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_uniqueStorage()._maxConcurrentRequests = newValue} + } + /// Returns true if `maxConcurrentRequests` has been explicitly set. + var hasMaxConcurrentRequests: Bool {return _storage._maxConcurrentRequests != nil} + /// Clears the value of `maxConcurrentRequests`. Subsequent reads from it will return its default value. + mutating func clearMaxConcurrentRequests() {_uniqueStorage()._maxConcurrentRequests = nil} + + var type: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum { + get {return _storage._type} + set {_uniqueStorage()._type = newValue} + } + + /// For type EDS only. + /// EDS service name, as returned in CDS. + /// May be unset if not specified in CDS. + var edsServiceName: String { + get {return _storage._edsServiceName} + set {_uniqueStorage()._edsServiceName = newValue} + } + + /// For type LOGICAL_DNS only. + /// DNS name to resolve in "host:port" form. + var dnsHostname: String { + get {return _storage._dnsHostname} + set {_uniqueStorage()._dnsHostname = newValue} + } + + /// The configuration for outlier_detection child policies + /// Within this message, the child_policy field will be ignored + var outlierDetection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { + get {return _storage._outlierDetection ?? Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig()} + set {_uniqueStorage()._outlierDetection = newValue} + } + /// Returns true if `outlierDetection` has been explicitly set. + var hasOutlierDetection: Bool {return _storage._outlierDetection != nil} + /// Clears the value of `outlierDetection`. Subsequent reads from it will return its default value. + mutating func clearOutlierDetection() {_uniqueStorage()._outlierDetection = nil} + + /// The configuration for xds_override_host child policy + var overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] { + get {return _storage._overrideHostStatus} + set {_uniqueStorage()._overrideHostStatus = newValue} + } + + /// Telemetry labels associated with this cluster + var telemetryLabels: Dictionary { + get {return _storage._telemetryLabels} + set {_uniqueStorage()._telemetryLabels = newValue} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case eds // = 1 + case logicalDns // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .eds + case 2: self = .logicalDns + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .eds: return 1 + case .logicalDns: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance + } + + init() {} +} + +#if swift(>=4.2) + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum] = [ + .unknown, + .eds, + .logicalDns, + ] +} + +#endif // swift(>=4.2) + +/// Configuration for xds_cluster_impl LB policy. +struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Cluster name. Required. + var cluster: String = String() + + /// EDS service name. + /// Not set if cluster is not an EDS cluster or if it does not + /// specify an EDS service name. + var edsServiceName: String = String() + + /// Server to send load reports to. + /// If unset, no load reporting is done. + /// If set to empty string, load reporting will be sent to the same + /// server as we are getting xds data from. + /// DEPRECATED: Use new lrs_load_reporting_server field instead. + var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_lrsLoadReportingServerName = newValue} + } + /// Returns true if `lrsLoadReportingServerName` has been explicitly set. + var hasLrsLoadReportingServerName: Bool {return self._lrsLoadReportingServerName != nil} + /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServerName() {self._lrsLoadReportingServerName = nil} + + /// LRS server to send load reports to. + /// If not present, load reporting will be disabled. + /// Supercedes lrs_load_reporting_server_name field. + var lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer { + get {return _lrsLoadReportingServer ?? Grpc_ServiceConfig_XdsServer()} + set {_lrsLoadReportingServer = newValue} + } + /// Returns true if `lrsLoadReportingServer` has been explicitly set. + var hasLrsLoadReportingServer: Bool {return self._lrsLoadReportingServer != nil} + /// Clears the value of `lrsLoadReportingServer`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServer() {self._lrsLoadReportingServer = nil} + + /// Maximum number of outstanding requests can be made to the upstream cluster. + /// Default is 1024. + var maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _maxConcurrentRequests ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_maxConcurrentRequests = newValue} + } + /// Returns true if `maxConcurrentRequests` has been explicitly set. + var hasMaxConcurrentRequests: Bool {return self._maxConcurrentRequests != nil} + /// Clears the value of `maxConcurrentRequests`. Subsequent reads from it will return its default value. + mutating func clearMaxConcurrentRequests() {self._maxConcurrentRequests = nil} + + var dropCategories: [Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory] = [] + + /// Child policy. + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Telemetry labels associated with this cluster + var telemetryLabels: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Drop configuration. + struct DropCategory { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var category: String = String() + + var requestsPerMillion: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} + + fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil + fileprivate var _lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer? = nil + fileprivate var _maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil +} + +/// Configuration for eds LB policy. +struct Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Cluster name. Required. + var cluster: String = String() + + /// EDS service name, as returned in CDS. + /// May be unset if not specified in CDS. + var edsServiceName: String = String() + + /// Server to send load reports to. + /// If unset, no load reporting is done. + /// If set to empty string, load reporting will be sent to the same + /// server as we are getting xds data from. + var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_lrsLoadReportingServerName = newValue} + } + /// Returns true if `lrsLoadReportingServerName` has been explicitly set. + var hasLrsLoadReportingServerName: Bool {return self._lrsLoadReportingServerName != nil} + /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServerName() {self._lrsLoadReportingServerName = nil} + + /// Locality-picking policy. + /// This policy's config is expected to be in the format used + /// by the weighted_target policy. Note that the config should include + /// an empty value for the "targets" field; that empty value will be + /// replaced by one that is dynamically generated based on the EDS data. + /// Optional; defaults to "weighted_target". + var localityPickingPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Endpoint-picking policy. + /// This will be configured as the policy for each child in the + /// locality-policy's config. + /// Optional; defaults to "round_robin". + var endpointPickingPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil +} + +/// Configuration for ring_hash LB policy. +struct Grpc_ServiceConfig_RingHashLoadBalancingConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// A client-side option will cap these values to 4096. If either of these + /// values are greater than the client-side cap, they will be treated + /// as the client-side cap value. + var minRingSize: UInt64 = 0 + + /// Optional, defaults to 4096, max 8M. + var maxRingSize: UInt64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Configuration for lrs LB policy. +struct Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Cluster name. Required. + var clusterName: String = String() + + /// EDS service name, as returned in CDS. + /// May be unset if not specified in CDS. + var edsServiceName: String = String() + + /// Server to send load reports to. Required. + /// If set to empty string, load reporting will be sent to the same + /// server as we are getting xds data from. + var lrsLoadReportingServerName: String = String() + + var locality: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality { + get {return _locality ?? Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality()} + set {_locality = newValue} + } + /// Returns true if `locality` has been explicitly set. + var hasLocality: Bool {return self._locality != nil} + /// Clears the value of `locality`. Subsequent reads from it will return its default value. + mutating func clearLocality() {self._locality = nil} + + /// Endpoint-picking policy. + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The locality for which this policy will report load. Required. + struct Locality { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var region: String = String() + + var zone: String = String() + + var subzone: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} + + fileprivate var _locality: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality? = nil +} + +/// Configuration for the xds_wrr_locality load balancing policy. +struct Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Configuration for the least_request LB policy. +struct Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var choiceCount: UInt64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Configuration for the override_host LB policy. +struct Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// valid health status for hosts that are considered when using + /// xds_override_host_experimental policy. + /// Default is [UNKNOWN, HEALTHY] + var overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] + + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum HealthStatus: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case healthy // = 1 + case draining // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .healthy + case 3: self = .draining + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .healthy: return 1 + case .draining: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} +} + +#if swift(>=4.2) + +extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [ + .unknown, + .healthy, + .draining, + ] +} + +#endif // swift(>=4.2) + +/// Configuration for xds LB policy. +struct Grpc_ServiceConfig_XdsConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Name of balancer to connect to. + var balancerName: String = String() + + /// Optional. What LB policy to use for intra-locality routing. + /// If unset, will use whatever algorithm is specified by the balancer. + /// Multiple LB policies can be specified; clients will iterate through + /// the list in order and stop at the first policy that they support. + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Optional. What LB policy to use in fallback mode. If not + /// specified, defaults to round_robin. + /// Multiple LB policies can be specified; clients will iterate through + /// the list in order and stop at the first policy that they support. + var fallbackPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Optional. Name to use in EDS query. If not present, defaults to + /// the server name from the target URI. + var edsServiceName: String = String() + + /// LRS server to send load reports to. + /// If not present, load reporting will be disabled. + /// If set to the empty string, load reporting will be sent to the same + /// server that we obtained CDS data from. + var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_lrsLoadReportingServerName = newValue} + } + /// Returns true if `lrsLoadReportingServerName` has been explicitly set. + var hasLrsLoadReportingServerName: Bool {return self._lrsLoadReportingServerName != nil} + /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServerName() {self._lrsLoadReportingServerName = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil +} + +/// Selects LB policy and provides corresponding configuration. +/// +/// In general, all instances of this field should be repeated. Clients will +/// iterate through the list in order and stop at the first policy that they +/// support. This allows the service config to specify custom policies that may +/// not be known to all clients. +/// +/// - If the config for the first supported policy is invalid, the whole service +/// config is invalid. +/// - If the list doesn't contain any supported policy, the whole service config +/// is invalid. +struct Grpc_ServiceConfig_LoadBalancingConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Exactly one LB policy may be configured. + var policy: Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy? = nil + + var pickFirst: Grpc_ServiceConfig_PickFirstConfig { + get { + if case .pickFirst(let v)? = policy {return v} + return Grpc_ServiceConfig_PickFirstConfig() + } + set {policy = .pickFirst(newValue)} + } + + var roundRobin: Grpc_ServiceConfig_RoundRobinConfig { + get { + if case .roundRobin(let v)? = policy {return v} + return Grpc_ServiceConfig_RoundRobinConfig() + } + set {policy = .roundRobin(newValue)} + } + + var weightedRoundRobin: Grpc_ServiceConfig_WeightedRoundRobinLbConfig { + get { + if case .weightedRoundRobin(let v)? = policy {return v} + return Grpc_ServiceConfig_WeightedRoundRobinLbConfig() + } + set {policy = .weightedRoundRobin(newValue)} + } + + /// gRPC lookaside load balancing. + /// This will eventually be deprecated by the new xDS-based local + /// balancing policy. + var grpclb: Grpc_ServiceConfig_GrpcLbConfig { + get { + if case .grpclb(let v)? = policy {return v} + return Grpc_ServiceConfig_GrpcLbConfig() + } + set {policy = .grpclb(newValue)} + } + + var priorityExperimental: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig { + get { + if case .priorityExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig() + } + set {policy = .priorityExperimental(newValue)} + } + + var weightedTargetExperimental: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig { + get { + if case .weightedTargetExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig() + } + set {policy = .weightedTargetExperimental(newValue)} + } + + var outlierDetection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { + get { + if case .outlierDetection(let v)? = policy {return v} + return Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig() + } + set {policy = .outlierDetection(newValue)} + } + + var rls: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig { + get { + if case .rls(let v)? = policy {return v} + return Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig() + } + set {policy = .rls(newValue)} + } + + /// xDS-based load balancing. + var xdsClusterManagerExperimental: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig { + get { + if case .xdsClusterManagerExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig() + } + set {policy = .xdsClusterManagerExperimental(newValue)} + } + + var cdsExperimental: Grpc_ServiceConfig_CdsConfig { + get { + if case .cdsExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_CdsConfig() + } + set {policy = .cdsExperimental(newValue)} + } + + var xdsClusterResolverExperimental: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { + get { + if case .xdsClusterResolverExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig() + } + set {policy = .xdsClusterResolverExperimental(newValue)} + } + + var xdsClusterImplExperimental: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { + get { + if case .xdsClusterImplExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig() + } + set {policy = .xdsClusterImplExperimental(newValue)} + } + + var overrideHostExperimental: Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig { + get { + if case .overrideHostExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig() + } + set {policy = .overrideHostExperimental(newValue)} + } + + var xdsWrrLocalityExperimental: Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig { + get { + if case .xdsWrrLocalityExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig() + } + set {policy = .xdsWrrLocalityExperimental(newValue)} + } + + var ringHashExperimental: Grpc_ServiceConfig_RingHashLoadBalancingConfig { + get { + if case .ringHashExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_RingHashLoadBalancingConfig() + } + set {policy = .ringHashExperimental(newValue)} + } + + var leastRequestExperimental: Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig { + get { + if case .leastRequestExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig() + } + set {policy = .leastRequestExperimental(newValue)} + } + + /// Deprecated xDS-related policies. + var lrsExperimental: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { + get { + if case .lrsExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig() + } + set {policy = .lrsExperimental(newValue)} + } + + var edsExperimental: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig { + get { + if case .edsExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig() + } + set {policy = .edsExperimental(newValue)} + } + + var xds: Grpc_ServiceConfig_XdsConfig { + get { + if case .xds(let v)? = policy {return v} + return Grpc_ServiceConfig_XdsConfig() + } + set {policy = .xds(newValue)} + } + + var xdsExperimental: Grpc_ServiceConfig_XdsConfig { + get { + if case .xdsExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_XdsConfig() + } + set {policy = .xdsExperimental(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Exactly one LB policy may be configured. + enum OneOf_Policy: Equatable { + case pickFirst(Grpc_ServiceConfig_PickFirstConfig) + case roundRobin(Grpc_ServiceConfig_RoundRobinConfig) + case weightedRoundRobin(Grpc_ServiceConfig_WeightedRoundRobinLbConfig) + /// gRPC lookaside load balancing. + /// This will eventually be deprecated by the new xDS-based local + /// balancing policy. + case grpclb(Grpc_ServiceConfig_GrpcLbConfig) + case priorityExperimental(Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig) + case weightedTargetExperimental(Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig) + case outlierDetection(Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig) + case rls(Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig) + /// xDS-based load balancing. + case xdsClusterManagerExperimental(Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig) + case cdsExperimental(Grpc_ServiceConfig_CdsConfig) + case xdsClusterResolverExperimental(Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig) + case xdsClusterImplExperimental(Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig) + case overrideHostExperimental(Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig) + case xdsWrrLocalityExperimental(Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig) + case ringHashExperimental(Grpc_ServiceConfig_RingHashLoadBalancingConfig) + case leastRequestExperimental(Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig) + /// Deprecated xDS-related policies. + case lrsExperimental(Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig) + case edsExperimental(Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig) + case xds(Grpc_ServiceConfig_XdsConfig) + case xdsExperimental(Grpc_ServiceConfig_XdsConfig) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy, rhs: Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.pickFirst, .pickFirst): return { + guard case .pickFirst(let l) = lhs, case .pickFirst(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.roundRobin, .roundRobin): return { + guard case .roundRobin(let l) = lhs, case .roundRobin(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.weightedRoundRobin, .weightedRoundRobin): return { + guard case .weightedRoundRobin(let l) = lhs, case .weightedRoundRobin(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.grpclb, .grpclb): return { + guard case .grpclb(let l) = lhs, case .grpclb(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.priorityExperimental, .priorityExperimental): return { + guard case .priorityExperimental(let l) = lhs, case .priorityExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.weightedTargetExperimental, .weightedTargetExperimental): return { + guard case .weightedTargetExperimental(let l) = lhs, case .weightedTargetExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.outlierDetection, .outlierDetection): return { + guard case .outlierDetection(let l) = lhs, case .outlierDetection(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.rls, .rls): return { + guard case .rls(let l) = lhs, case .rls(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xdsClusterManagerExperimental, .xdsClusterManagerExperimental): return { + guard case .xdsClusterManagerExperimental(let l) = lhs, case .xdsClusterManagerExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.cdsExperimental, .cdsExperimental): return { + guard case .cdsExperimental(let l) = lhs, case .cdsExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xdsClusterResolverExperimental, .xdsClusterResolverExperimental): return { + guard case .xdsClusterResolverExperimental(let l) = lhs, case .xdsClusterResolverExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xdsClusterImplExperimental, .xdsClusterImplExperimental): return { + guard case .xdsClusterImplExperimental(let l) = lhs, case .xdsClusterImplExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.overrideHostExperimental, .overrideHostExperimental): return { + guard case .overrideHostExperimental(let l) = lhs, case .overrideHostExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xdsWrrLocalityExperimental, .xdsWrrLocalityExperimental): return { + guard case .xdsWrrLocalityExperimental(let l) = lhs, case .xdsWrrLocalityExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.ringHashExperimental, .ringHashExperimental): return { + guard case .ringHashExperimental(let l) = lhs, case .ringHashExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.leastRequestExperimental, .leastRequestExperimental): return { + guard case .leastRequestExperimental(let l) = lhs, case .leastRequestExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.lrsExperimental, .lrsExperimental): return { + guard case .lrsExperimental(let l) = lhs, case .lrsExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.edsExperimental, .edsExperimental): return { + guard case .edsExperimental(let l) = lhs, case .edsExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xds, .xds): return { + guard case .xds(let l) = lhs, case .xds(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.xdsExperimental, .xdsExperimental): return { + guard case .xdsExperimental(let l) = lhs, case .xdsExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +/// A ServiceConfig represents information about a service but is not specific to +/// any name resolver. +struct Grpc_ServiceConfig_ServiceConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var loadBalancingPolicy: Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy = .unspecified + + /// Multiple LB policies can be specified; clients will iterate through + /// the list in order and stop at the first policy that they support. If none + /// are supported, the service config is considered invalid. + var loadBalancingConfig: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Per-method configuration. + var methodConfig: [Grpc_ServiceConfig_MethodConfig] = [] + + var retryThrottling: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy { + get {return _retryThrottling ?? Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy()} + set {_retryThrottling = newValue} + } + /// Returns true if `retryThrottling` has been explicitly set. + var hasRetryThrottling: Bool {return self._retryThrottling != nil} + /// Clears the value of `retryThrottling`. Subsequent reads from it will return its default value. + mutating func clearRetryThrottling() {self._retryThrottling = nil} + + var healthCheckConfig: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig { + get {return _healthCheckConfig ?? Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig()} + set {_healthCheckConfig = newValue} + } + /// Returns true if `healthCheckConfig` has been explicitly set. + var hasHealthCheckConfig: Bool {return self._healthCheckConfig != nil} + /// Clears the value of `healthCheckConfig`. Subsequent reads from it will return its default value. + mutating func clearHealthCheckConfig() {self._healthCheckConfig = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Load balancing policy. + /// + /// Note that load_balancing_policy is deprecated in favor of + /// load_balancing_config; the former will be used only if the latter + /// is unset. + /// + /// If no LB policy is configured here, then the default is pick_first. + /// If the policy name is set via the client API, that value overrides + /// the value specified here. + /// + /// If the deprecated load_balancing_policy field is used, note that if the + /// resolver returns at least one balancer address (as opposed to backend + /// addresses), gRPC will use grpclb (see + /// https://github.com/grpc/grpc/blob/master/doc/load-balancing.md), + /// regardless of what policy is configured here. However, if the resolver + /// returns at least one backend address in addition to the balancer + /// address(es), the client may fall back to the requested policy if it + /// is unable to reach any of the grpclb load balancers. + enum LoadBalancingPolicy: SwiftProtobuf.Enum { + typealias RawValue = Int + case unspecified // = 0 + case roundRobin // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .unspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .roundRobin + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unspecified: return 0 + case .roundRobin: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + } + + /// If a RetryThrottlingPolicy is provided, gRPC will automatically throttle + /// retry attempts and hedged RPCs when the client's ratio of failures to + /// successes exceeds a threshold. + /// + /// For each server name, the gRPC client will maintain a token_count which is + /// initially set to max_tokens. Every outgoing RPC (regardless of service or + /// method invoked) will change token_count as follows: + /// + /// - Every failed RPC will decrement the token_count by 1. + /// - Every successful RPC will increment the token_count by token_ratio. + /// + /// If token_count is less than or equal to max_tokens / 2, then RPCs will not + /// be retried and hedged RPCs will not be sent. + struct RetryThrottlingPolicy { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The number of tokens starts at max_tokens. The token_count will always be + /// between 0 and max_tokens. + /// + /// This field is required and must be greater than zero. + var maxTokens: UInt32 = 0 + + /// The amount of tokens to add on each successful RPC. Typically this will + /// be some number between 0 and 1, e.g., 0.1. + /// + /// This field is required and must be greater than zero. Up to 3 decimal + /// places are supported. + var tokenRatio: Float = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + struct HealthCheckConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Service name to use in the health-checking request. + var serviceName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _serviceName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_serviceName = newValue} + } + /// Returns true if `serviceName` has been explicitly set. + var hasServiceName: Bool {return self._serviceName != nil} + /// Clears the value of `serviceName`. Subsequent reads from it will return its default value. + mutating func clearServiceName() {self._serviceName = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _serviceName: SwiftProtobuf.Google_Protobuf_StringValue? = nil + } + + init() {} + + fileprivate var _retryThrottling: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy? = nil + fileprivate var _healthCheckConfig: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig? = nil +} + +#if swift(>=4.2) + +extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy] = [ + .unspecified, + .roundRobin, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_ServiceConfig_MethodConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.Name: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.HedgingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_PickFirstConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_RoundRobinConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_WeightedRoundRobinLbConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection: @unchecked Sendable {} +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection: @unchecked Sendable {} +extension Grpc_ServiceConfig_GrpcLbConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child: @unchecked Sendable {} +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: @unchecked Sendable {} +extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: @unchecked Sendable {} +extension Grpc_ServiceConfig_CdsConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsServer: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory: @unchecked Sendable {} +extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LoadBalancingConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.service_config" + +extension Grpc_ServiceConfig_MethodConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MethodConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 2: .standard(proto: "wait_for_ready"), + 3: .same(proto: "timeout"), + 4: .standard(proto: "max_request_message_bytes"), + 5: .standard(proto: "max_response_message_bytes"), + 6: .standard(proto: "retry_policy"), + 7: .standard(proto: "hedging_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.name) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._waitForReady) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._timeout) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._maxRequestMessageBytes) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._maxResponseMessageBytes) }() + case 6: try { + var v: Grpc_ServiceConfig_MethodConfig.RetryPolicy? + var hadOneofValue = false + if let current = self.retryOrHedgingPolicy { + hadOneofValue = true + if case .retryPolicy(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.retryOrHedgingPolicy = .retryPolicy(v) + } + }() + case 7: try { + var v: Grpc_ServiceConfig_MethodConfig.HedgingPolicy? + var hadOneofValue = false + if let current = self.retryOrHedgingPolicy { + hadOneofValue = true + if case .hedgingPolicy(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.retryOrHedgingPolicy = .hedgingPolicy(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.name.isEmpty { + try visitor.visitRepeatedMessageField(value: self.name, fieldNumber: 1) + } + try { if let v = self._waitForReady { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._timeout { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._maxRequestMessageBytes { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try { if let v = self._maxResponseMessageBytes { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + switch self.retryOrHedgingPolicy { + case .retryPolicy?: try { + guard case .retryPolicy(let v)? = self.retryOrHedgingPolicy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .hedgingPolicy?: try { + guard case .hedgingPolicy(let v)? = self.retryOrHedgingPolicy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_MethodConfig, rhs: Grpc_ServiceConfig_MethodConfig) -> Bool { + if lhs.name != rhs.name {return false} + if lhs._waitForReady != rhs._waitForReady {return false} + if lhs._timeout != rhs._timeout {return false} + if lhs._maxRequestMessageBytes != rhs._maxRequestMessageBytes {return false} + if lhs._maxResponseMessageBytes != rhs._maxResponseMessageBytes {return false} + if lhs.retryOrHedgingPolicy != rhs.retryOrHedgingPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_MethodConfig.Name: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".Name" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "service"), + 2: .same(proto: "method"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.service) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.method) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.service.isEmpty { + try visitor.visitSingularStringField(value: self.service, fieldNumber: 1) + } + if !self.method.isEmpty { + try visitor.visitSingularStringField(value: self.method, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_MethodConfig.Name, rhs: Grpc_ServiceConfig_MethodConfig.Name) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.method != rhs.method {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".RetryPolicy" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "max_attempts"), + 2: .standard(proto: "initial_backoff"), + 3: .standard(proto: "max_backoff"), + 4: .standard(proto: "backoff_multiplier"), + 5: .standard(proto: "retryable_status_codes"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxAttempts) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._initialBackoff) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._maxBackoff) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self.backoffMultiplier) }() + case 5: try { try decoder.decodeRepeatedEnumField(value: &self.retryableStatusCodes) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.maxAttempts != 0 { + try visitor.visitSingularUInt32Field(value: self.maxAttempts, fieldNumber: 1) + } + try { if let v = self._initialBackoff { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._maxBackoff { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if self.backoffMultiplier != 0 { + try visitor.visitSingularFloatField(value: self.backoffMultiplier, fieldNumber: 4) + } + if !self.retryableStatusCodes.isEmpty { + try visitor.visitPackedEnumField(value: self.retryableStatusCodes, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_MethodConfig.RetryPolicy, rhs: Grpc_ServiceConfig_MethodConfig.RetryPolicy) -> Bool { + if lhs.maxAttempts != rhs.maxAttempts {return false} + if lhs._initialBackoff != rhs._initialBackoff {return false} + if lhs._maxBackoff != rhs._maxBackoff {return false} + if lhs.backoffMultiplier != rhs.backoffMultiplier {return false} + if lhs.retryableStatusCodes != rhs.retryableStatusCodes {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_MethodConfig.HedgingPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".HedgingPolicy" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "max_attempts"), + 2: .standard(proto: "hedging_delay"), + 3: .standard(proto: "non_fatal_status_codes"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxAttempts) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._hedgingDelay) }() + case 3: try { try decoder.decodeRepeatedEnumField(value: &self.nonFatalStatusCodes) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.maxAttempts != 0 { + try visitor.visitSingularUInt32Field(value: self.maxAttempts, fieldNumber: 1) + } + try { if let v = self._hedgingDelay { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if !self.nonFatalStatusCodes.isEmpty { + try visitor.visitPackedEnumField(value: self.nonFatalStatusCodes, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_MethodConfig.HedgingPolicy, rhs: Grpc_ServiceConfig_MethodConfig.HedgingPolicy) -> Bool { + if lhs.maxAttempts != rhs.maxAttempts {return false} + if lhs._hedgingDelay != rhs._hedgingDelay {return false} + if lhs.nonFatalStatusCodes != rhs.nonFatalStatusCodes {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_PickFirstConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PickFirstConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "shuffle_address_list"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.shuffleAddressList) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.shuffleAddressList != false { + try visitor.visitSingularBoolField(value: self.shuffleAddressList, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_PickFirstConfig, rhs: Grpc_ServiceConfig_PickFirstConfig) -> Bool { + if lhs.shuffleAddressList != rhs.shuffleAddressList {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_RoundRobinConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RoundRobinConfig" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_RoundRobinConfig, rhs: Grpc_ServiceConfig_RoundRobinConfig) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_WeightedRoundRobinLbConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".WeightedRoundRobinLbConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "enable_oob_load_report"), + 2: .standard(proto: "oob_reporting_period"), + 3: .standard(proto: "blackout_period"), + 4: .standard(proto: "weight_expiration_period"), + 5: .standard(proto: "weight_update_period"), + 6: .standard(proto: "error_utilization_penalty"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._enableOobLoadReport) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._oobReportingPeriod) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._blackoutPeriod) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._weightExpirationPeriod) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._weightUpdatePeriod) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._errorUtilizationPenalty) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._enableOobLoadReport { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._oobReportingPeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._blackoutPeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._weightExpirationPeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try { if let v = self._weightUpdatePeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try { if let v = self._errorUtilizationPenalty { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_WeightedRoundRobinLbConfig, rhs: Grpc_ServiceConfig_WeightedRoundRobinLbConfig) -> Bool { + if lhs._enableOobLoadReport != rhs._enableOobLoadReport {return false} + if lhs._oobReportingPeriod != rhs._oobReportingPeriod {return false} + if lhs._blackoutPeriod != rhs._blackoutPeriod {return false} + if lhs._weightExpirationPeriod != rhs._weightExpirationPeriod {return false} + if lhs._weightUpdatePeriod != rhs._weightUpdatePeriod {return false} + if lhs._errorUtilizationPenalty != rhs._errorUtilizationPenalty {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".OutlierDetectionLoadBalancingConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "interval"), + 2: .standard(proto: "base_ejection_time"), + 3: .standard(proto: "max_ejection_time"), + 4: .standard(proto: "max_ejection_percent"), + 5: .standard(proto: "success_rate_ejection"), + 6: .standard(proto: "failure_percentage_ejection"), + 13: .standard(proto: "child_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._interval) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._baseEjectionTime) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._maxEjectionTime) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._maxEjectionPercent) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._successRateEjection) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._failurePercentageEjection) }() + case 13: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._interval { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._baseEjectionTime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._maxEjectionTime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._maxEjectionPercent { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try { if let v = self._successRateEjection { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try { if let v = self._failurePercentageEjection { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 13) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig) -> Bool { + if lhs._interval != rhs._interval {return false} + if lhs._baseEjectionTime != rhs._baseEjectionTime {return false} + if lhs._maxEjectionTime != rhs._maxEjectionTime {return false} + if lhs._maxEjectionPercent != rhs._maxEjectionPercent {return false} + if lhs._successRateEjection != rhs._successRateEjection {return false} + if lhs._failurePercentageEjection != rhs._failurePercentageEjection {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.protoMessageName + ".SuccessRateEjection" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "stdev_factor"), + 2: .standard(proto: "enforcement_percentage"), + 3: .standard(proto: "minimum_hosts"), + 4: .standard(proto: "request_volume"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._stdevFactor) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._enforcementPercentage) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._minimumHosts) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._requestVolume) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._stdevFactor { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._enforcementPercentage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._minimumHosts { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._requestVolume { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection) -> Bool { + if lhs._stdevFactor != rhs._stdevFactor {return false} + if lhs._enforcementPercentage != rhs._enforcementPercentage {return false} + if lhs._minimumHosts != rhs._minimumHosts {return false} + if lhs._requestVolume != rhs._requestVolume {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.protoMessageName + ".FailurePercentageEjection" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "threshold"), + 2: .standard(proto: "enforcement_percentage"), + 3: .standard(proto: "minimum_hosts"), + 4: .standard(proto: "request_volume"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._threshold) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._enforcementPercentage) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._minimumHosts) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._requestVolume) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._threshold { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._enforcementPercentage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._minimumHosts { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._requestVolume { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection) -> Bool { + if lhs._threshold != rhs._threshold {return false} + if lhs._enforcementPercentage != rhs._enforcementPercentage {return false} + if lhs._minimumHosts != rhs._minimumHosts {return false} + if lhs._requestVolume != rhs._requestVolume {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_GrpcLbConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".GrpcLbConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "child_policy"), + 2: .standard(proto: "service_name"), + 3: .standard(proto: "initial_fallback_timeout"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.serviceName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._initialFallbackTimeout) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 1) + } + if !self.serviceName.isEmpty { + try visitor.visitSingularStringField(value: self.serviceName, fieldNumber: 2) + } + try { if let v = self._initialFallbackTimeout { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_GrpcLbConfig, rhs: Grpc_ServiceConfig_GrpcLbConfig) -> Bool { + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.serviceName != rhs.serviceName {return false} + if lhs._initialFallbackTimeout != rhs._initialFallbackTimeout {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PriorityLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "children"), + 2: .same(proto: "priorities"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.children) }() + case 2: try { try decoder.decodeRepeatedStringField(value: &self.priorities) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.children.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.children, fieldNumber: 1) + } + if !self.priorities.isEmpty { + try visitor.visitRepeatedStringField(value: self.priorities, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig) -> Bool { + if lhs.children != rhs.children {return false} + if lhs.priorities != rhs.priorities {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.protoMessageName + ".Child" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "config"), + 2: .standard(proto: "ignore_reresolution_requests"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.config) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.ignoreReresolutionRequests) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.config.isEmpty { + try visitor.visitRepeatedMessageField(value: self.config, fieldNumber: 1) + } + if self.ignoreReresolutionRequests != false { + try visitor.visitSingularBoolField(value: self.ignoreReresolutionRequests, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child, rhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child) -> Bool { + if lhs.config != rhs.config {return false} + if lhs.ignoreReresolutionRequests != rhs.ignoreReresolutionRequests {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".WeightedTargetLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "targets"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.targets) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.targets.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.targets, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig) -> Bool { + if lhs.targets != rhs.targets {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.protoMessageName + ".Target" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "weight"), + 2: .standard(proto: "child_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.weight) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.weight != 0 { + try visitor.visitSingularUInt32Field(value: self.weight, fieldNumber: 1) + } + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target, rhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target) -> Bool { + if lhs.weight != rhs.weight {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RlsLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "route_lookup_config"), + 2: .standard(proto: "route_lookup_channel_service_config"), + 3: .standard(proto: "child_policy"), + 4: .standard(proto: "child_policy_config_target_field_name"), + ] + + fileprivate class _StorageClass { + var _routeLookupConfig: Grpc_Lookup_V1_RouteLookupConfig? = nil + var _routeLookupChannelServiceConfig: Grpc_ServiceConfig_ServiceConfig? = nil + var _childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + var _childPolicyConfigTargetFieldName: String = String() + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _routeLookupConfig = source._routeLookupConfig + _routeLookupChannelServiceConfig = source._routeLookupChannelServiceConfig + _childPolicy = source._childPolicy + _childPolicyConfigTargetFieldName = source._childPolicyConfigTargetFieldName + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._routeLookupConfig) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._routeLookupChannelServiceConfig) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &_storage._childPolicy) }() + case 4: try { try decoder.decodeSingularStringField(value: &_storage._childPolicyConfigTargetFieldName) }() + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = _storage._routeLookupConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = _storage._routeLookupChannelServiceConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if !_storage._childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._childPolicy, fieldNumber: 3) + } + if !_storage._childPolicyConfigTargetFieldName.isEmpty { + try visitor.visitSingularStringField(value: _storage._childPolicyConfigTargetFieldName, fieldNumber: 4) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._routeLookupConfig != rhs_storage._routeLookupConfig {return false} + if _storage._routeLookupChannelServiceConfig != rhs_storage._routeLookupChannelServiceConfig {return false} + if _storage._childPolicy != rhs_storage._childPolicy {return false} + if _storage._childPolicyConfigTargetFieldName != rhs_storage._childPolicyConfigTargetFieldName {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsClusterManagerLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "children"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.children) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.children.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.children, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig) -> Bool { + if lhs.children != rhs.children {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.protoMessageName + ".Child" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "child_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child, rhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child) -> Bool { + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_CdsConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".CdsConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "cluster"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.cluster.isEmpty { + try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_CdsConfig, rhs: Grpc_ServiceConfig_CdsConfig) -> Bool { + if lhs.cluster != rhs.cluster {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsServer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsServer" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "server_uri"), + 2: .same(proto: "channel_creds"), + 3: .same(proto: "server_features"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.serverUri) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.channelCreds) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.serverFeatures) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.serverUri.isEmpty { + try visitor.visitSingularStringField(value: self.serverUri, fieldNumber: 1) + } + if !self.channelCreds.isEmpty { + try visitor.visitRepeatedMessageField(value: self.channelCreds, fieldNumber: 2) + } + if !self.serverFeatures.isEmpty { + try visitor.visitRepeatedMessageField(value: self.serverFeatures, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsServer, rhs: Grpc_ServiceConfig_XdsServer) -> Bool { + if lhs.serverUri != rhs.serverUri {return false} + if lhs.channelCreds != rhs.channelCreds {return false} + if lhs.serverFeatures != rhs.serverFeatures {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsServer.protoMessageName + ".ChannelCredentials" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "config"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.type) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._config) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.type.isEmpty { + try visitor.visitSingularStringField(value: self.type, fieldNumber: 1) + } + try { if let v = self._config { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsServer.ChannelCredentials, rhs: Grpc_ServiceConfig_XdsServer.ChannelCredentials) -> Bool { + if lhs.type != rhs.type {return false} + if lhs._config != rhs._config {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsClusterResolverLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "discovery_mechanisms"), + 2: .standard(proto: "xds_lb_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.discoveryMechanisms) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.xdsLbPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.discoveryMechanisms.isEmpty { + try visitor.visitRepeatedMessageField(value: self.discoveryMechanisms, fieldNumber: 1) + } + if !self.xdsLbPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.xdsLbPolicy, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig) -> Bool { + if lhs.discoveryMechanisms != rhs.discoveryMechanisms {return false} + if lhs.xdsLbPolicy != rhs.xdsLbPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.protoMessageName + ".DiscoveryMechanism" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "cluster"), + 2: .standard(proto: "lrs_load_reporting_server_name"), + 7: .standard(proto: "lrs_load_reporting_server"), + 3: .standard(proto: "max_concurrent_requests"), + 4: .same(proto: "type"), + 5: .standard(proto: "eds_service_name"), + 6: .standard(proto: "dns_hostname"), + 8: .standard(proto: "outlier_detection"), + 9: .standard(proto: "override_host_status"), + 10: .standard(proto: "telemetry_labels"), + ] + + fileprivate class _StorageClass { + var _cluster: String = String() + var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil + var _lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer? = nil + var _maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + var _type: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum = .unknown + var _edsServiceName: String = String() + var _dnsHostname: String = String() + var _outlierDetection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig? = nil + var _overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] + var _telemetryLabels: Dictionary = [:] + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _cluster = source._cluster + _lrsLoadReportingServerName = source._lrsLoadReportingServerName + _lrsLoadReportingServer = source._lrsLoadReportingServer + _maxConcurrentRequests = source._maxConcurrentRequests + _type = source._type + _edsServiceName = source._edsServiceName + _dnsHostname = source._dnsHostname + _outlierDetection = source._outlierDetection + _overrideHostStatus = source._overrideHostStatus + _telemetryLabels = source._telemetryLabels + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &_storage._cluster) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._lrsLoadReportingServerName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &_storage._maxConcurrentRequests) }() + case 4: try { try decoder.decodeSingularEnumField(value: &_storage._type) }() + case 5: try { try decoder.decodeSingularStringField(value: &_storage._edsServiceName) }() + case 6: try { try decoder.decodeSingularStringField(value: &_storage._dnsHostname) }() + case 7: try { try decoder.decodeSingularMessageField(value: &_storage._lrsLoadReportingServer) }() + case 8: try { try decoder.decodeSingularMessageField(value: &_storage._outlierDetection) }() + case 9: try { try decoder.decodeRepeatedEnumField(value: &_storage._overrideHostStatus) }() + case 10: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &_storage._telemetryLabels) }() + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !_storage._cluster.isEmpty { + try visitor.visitSingularStringField(value: _storage._cluster, fieldNumber: 1) + } + try { if let v = _storage._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._maxConcurrentRequests { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if _storage._type != .unknown { + try visitor.visitSingularEnumField(value: _storage._type, fieldNumber: 4) + } + if !_storage._edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: _storage._edsServiceName, fieldNumber: 5) + } + if !_storage._dnsHostname.isEmpty { + try visitor.visitSingularStringField(value: _storage._dnsHostname, fieldNumber: 6) + } + try { if let v = _storage._lrsLoadReportingServer { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try { if let v = _storage._outlierDetection { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() + if !_storage._overrideHostStatus.isEmpty { + try visitor.visitPackedEnumField(value: _storage._overrideHostStatus, fieldNumber: 9) + } + if !_storage._telemetryLabels.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: _storage._telemetryLabels, fieldNumber: 10) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism, rhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._cluster != rhs_storage._cluster {return false} + if _storage._lrsLoadReportingServerName != rhs_storage._lrsLoadReportingServerName {return false} + if _storage._lrsLoadReportingServer != rhs_storage._lrsLoadReportingServer {return false} + if _storage._maxConcurrentRequests != rhs_storage._maxConcurrentRequests {return false} + if _storage._type != rhs_storage._type {return false} + if _storage._edsServiceName != rhs_storage._edsServiceName {return false} + if _storage._dnsHostname != rhs_storage._dnsHostname {return false} + if _storage._outlierDetection != rhs_storage._outlierDetection {return false} + if _storage._overrideHostStatus != rhs_storage._overrideHostStatus {return false} + if _storage._telemetryLabels != rhs_storage._telemetryLabels {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "EDS"), + 2: .same(proto: "LOGICAL_DNS"), + ] +} + +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsClusterImplLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "cluster"), + 2: .standard(proto: "eds_service_name"), + 3: .standard(proto: "lrs_load_reporting_server_name"), + 7: .standard(proto: "lrs_load_reporting_server"), + 4: .standard(proto: "max_concurrent_requests"), + 5: .standard(proto: "drop_categories"), + 6: .standard(proto: "child_policy"), + 8: .standard(proto: "telemetry_labels"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._maxConcurrentRequests) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.dropCategories) }() + case 6: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServer) }() + case 8: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.telemetryLabels) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.cluster.isEmpty { + try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) + } + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) + } + try { if let v = self._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._maxConcurrentRequests { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + if !self.dropCategories.isEmpty { + try visitor.visitRepeatedMessageField(value: self.dropCategories, fieldNumber: 5) + } + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 6) + } + try { if let v = self._lrsLoadReportingServer { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + if !self.telemetryLabels.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.telemetryLabels, fieldNumber: 8) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig) -> Bool { + if lhs.cluster != rhs.cluster {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} + if lhs._lrsLoadReportingServer != rhs._lrsLoadReportingServer {return false} + if lhs._maxConcurrentRequests != rhs._maxConcurrentRequests {return false} + if lhs.dropCategories != rhs.dropCategories {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.telemetryLabels != rhs.telemetryLabels {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.protoMessageName + ".DropCategory" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "category"), + 2: .standard(proto: "requests_per_million"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.category) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.requestsPerMillion) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.category.isEmpty { + try visitor.visitSingularStringField(value: self.category, fieldNumber: 1) + } + if self.requestsPerMillion != 0 { + try visitor.visitSingularUInt32Field(value: self.requestsPerMillion, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory, rhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory) -> Bool { + if lhs.category != rhs.category {return false} + if lhs.requestsPerMillion != rhs.requestsPerMillion {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EdsLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "cluster"), + 2: .standard(proto: "eds_service_name"), + 3: .standard(proto: "lrs_load_reporting_server_name"), + 4: .standard(proto: "locality_picking_policy"), + 5: .standard(proto: "endpoint_picking_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.localityPickingPolicy) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.endpointPickingPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.cluster.isEmpty { + try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) + } + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) + } + try { if let v = self._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if !self.localityPickingPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.localityPickingPolicy, fieldNumber: 4) + } + if !self.endpointPickingPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.endpointPickingPolicy, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig) -> Bool { + if lhs.cluster != rhs.cluster {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} + if lhs.localityPickingPolicy != rhs.localityPickingPolicy {return false} + if lhs.endpointPickingPolicy != rhs.endpointPickingPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RingHashLoadBalancingConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "min_ring_size"), + 2: .standard(proto: "max_ring_size"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.minRingSize) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.maxRingSize) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.minRingSize != 0 { + try visitor.visitSingularUInt64Field(value: self.minRingSize, fieldNumber: 1) + } + if self.maxRingSize != 0 { + try visitor.visitSingularUInt64Field(value: self.maxRingSize, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_RingHashLoadBalancingConfig, rhs: Grpc_ServiceConfig_RingHashLoadBalancingConfig) -> Bool { + if lhs.minRingSize != rhs.minRingSize {return false} + if lhs.maxRingSize != rhs.maxRingSize {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LrsLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "cluster_name"), + 2: .standard(proto: "eds_service_name"), + 3: .standard(proto: "lrs_load_reporting_server_name"), + 4: .same(proto: "locality"), + 5: .standard(proto: "child_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.clusterName) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.lrsLoadReportingServerName) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._locality) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.clusterName.isEmpty { + try visitor.visitSingularStringField(value: self.clusterName, fieldNumber: 1) + } + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) + } + if !self.lrsLoadReportingServerName.isEmpty { + try visitor.visitSingularStringField(value: self.lrsLoadReportingServerName, fieldNumber: 3) + } + try { if let v = self._locality { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig) -> Bool { + if lhs.clusterName != rhs.clusterName {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs.lrsLoadReportingServerName != rhs.lrsLoadReportingServerName {return false} + if lhs._locality != rhs._locality {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.protoMessageName + ".Locality" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "region"), + 2: .same(proto: "zone"), + 3: .same(proto: "subzone"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.region) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.zone) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.subzone) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.region.isEmpty { + try visitor.visitSingularStringField(value: self.region, fieldNumber: 1) + } + if !self.zone.isEmpty { + try visitor.visitSingularStringField(value: self.zone, fieldNumber: 2) + } + if !self.subzone.isEmpty { + try visitor.visitSingularStringField(value: self.subzone, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality, rhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality) -> Bool { + if lhs.region != rhs.region {return false} + if lhs.zone != rhs.zone {return false} + if lhs.subzone != rhs.subzone {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsWrrLocalityLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "child_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig) -> Bool { + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LeastRequestLocalityLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "choice_count"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.choiceCount) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.choiceCount != 0 { + try visitor.visitSingularUInt64Field(value: self.choiceCount, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig) -> Bool { + if lhs.choiceCount != rhs.choiceCount {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".OverrideHostLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "override_host_status"), + 2: .standard(proto: "child_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedEnumField(value: &self.overrideHostStatus) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.overrideHostStatus.isEmpty { + try visitor.visitPackedEnumField(value: self.overrideHostStatus, fieldNumber: 1) + } + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig) -> Bool { + if lhs.overrideHostStatus != rhs.overrideHostStatus {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "HEALTHY"), + 3: .same(proto: "DRAINING"), + ] +} + +extension Grpc_ServiceConfig_XdsConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "balancer_name"), + 2: .standard(proto: "child_policy"), + 3: .standard(proto: "fallback_policy"), + 4: .standard(proto: "eds_service_name"), + 5: .standard(proto: "lrs_load_reporting_server_name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.balancerName) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.fallbackPolicy) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.balancerName.isEmpty { + try visitor.visitSingularStringField(value: self.balancerName, fieldNumber: 1) + } + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) + } + if !self.fallbackPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.fallbackPolicy, fieldNumber: 3) + } + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 4) + } + try { if let v = self._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsConfig, rhs: Grpc_ServiceConfig_XdsConfig) -> Bool { + if lhs.balancerName != rhs.balancerName {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.fallbackPolicy != rhs.fallbackPolicy {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_LoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LoadBalancingConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 4: .same(proto: "pick_first"), + 1: .same(proto: "round_robin"), + 20: .same(proto: "weighted_round_robin"), + 3: .same(proto: "grpclb"), + 9: .same(proto: "priority_experimental"), + 10: .same(proto: "weighted_target_experimental"), + 15: .unique(proto: "outlier_detection", json: "outlier_detection_experimental"), + 19: .unique(proto: "rls", json: "rls_experimental"), + 14: .same(proto: "xds_cluster_manager_experimental"), + 6: .same(proto: "cds_experimental"), + 11: .same(proto: "xds_cluster_resolver_experimental"), + 12: .same(proto: "xds_cluster_impl_experimental"), + 18: .same(proto: "override_host_experimental"), + 16: .same(proto: "xds_wrr_locality_experimental"), + 13: .same(proto: "ring_hash_experimental"), + 17: .same(proto: "least_request_experimental"), + 8: .same(proto: "lrs_experimental"), + 7: .same(proto: "eds_experimental"), + 2: .same(proto: "xds"), + 5: .same(proto: "xds_experimental"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Grpc_ServiceConfig_RoundRobinConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .roundRobin(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .roundRobin(v) + } + }() + case 2: try { + var v: Grpc_ServiceConfig_XdsConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .xds(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .xds(v) + } + }() + case 3: try { + var v: Grpc_ServiceConfig_GrpcLbConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .grpclb(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .grpclb(v) + } + }() + case 4: try { + var v: Grpc_ServiceConfig_PickFirstConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .pickFirst(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .pickFirst(v) + } + }() + case 5: try { + var v: Grpc_ServiceConfig_XdsConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .xdsExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .xdsExperimental(v) + } + }() + case 6: try { + var v: Grpc_ServiceConfig_CdsConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .cdsExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .cdsExperimental(v) + } + }() + case 7: try { + var v: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .edsExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .edsExperimental(v) + } + }() + case 8: try { + var v: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .lrsExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .lrsExperimental(v) + } + }() + case 9: try { + var v: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .priorityExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .priorityExperimental(v) + } + }() + case 10: try { + var v: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .weightedTargetExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .weightedTargetExperimental(v) + } + }() + case 11: try { + var v: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .xdsClusterResolverExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .xdsClusterResolverExperimental(v) + } + }() + case 12: try { + var v: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .xdsClusterImplExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .xdsClusterImplExperimental(v) + } + }() + case 13: try { + var v: Grpc_ServiceConfig_RingHashLoadBalancingConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .ringHashExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .ringHashExperimental(v) + } + }() + case 14: try { + var v: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .xdsClusterManagerExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .xdsClusterManagerExperimental(v) + } + }() + case 15: try { + var v: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .outlierDetection(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .outlierDetection(v) + } + }() + case 16: try { + var v: Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .xdsWrrLocalityExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .xdsWrrLocalityExperimental(v) + } + }() + case 17: try { + var v: Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .leastRequestExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .leastRequestExperimental(v) + } + }() + case 18: try { + var v: Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .overrideHostExperimental(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .overrideHostExperimental(v) + } + }() + case 19: try { + var v: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .rls(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .rls(v) + } + }() + case 20: try { + var v: Grpc_ServiceConfig_WeightedRoundRobinLbConfig? + var hadOneofValue = false + if let current = self.policy { + hadOneofValue = true + if case .weightedRoundRobin(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.policy = .weightedRoundRobin(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.policy { + case .roundRobin?: try { + guard case .roundRobin(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .xds?: try { + guard case .xds(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .grpclb?: try { + guard case .grpclb(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .pickFirst?: try { + guard case .pickFirst(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .xdsExperimental?: try { + guard case .xdsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .cdsExperimental?: try { + guard case .cdsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .edsExperimental?: try { + guard case .edsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case .lrsExperimental?: try { + guard case .lrsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() + case .priorityExperimental?: try { + guard case .priorityExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() + case .weightedTargetExperimental?: try { + guard case .weightedTargetExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() + case .xdsClusterResolverExperimental?: try { + guard case .xdsClusterResolverExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .xdsClusterImplExperimental?: try { + guard case .xdsClusterImplExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .ringHashExperimental?: try { + guard case .ringHashExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case .xdsClusterManagerExperimental?: try { + guard case .xdsClusterManagerExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + }() + case .outlierDetection?: try { + guard case .outlierDetection(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 15) + }() + case .xdsWrrLocalityExperimental?: try { + guard case .xdsWrrLocalityExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 16) + }() + case .leastRequestExperimental?: try { + guard case .leastRequestExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 17) + }() + case .overrideHostExperimental?: try { + guard case .overrideHostExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 18) + }() + case .rls?: try { + guard case .rls(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 19) + }() + case .weightedRoundRobin?: try { + guard case .weightedRoundRobin(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 20) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_LoadBalancingConfig, rhs: Grpc_ServiceConfig_LoadBalancingConfig) -> Bool { + if lhs.policy != rhs.policy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_ServiceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServiceConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "load_balancing_policy"), + 4: .standard(proto: "load_balancing_config"), + 2: .standard(proto: "method_config"), + 3: .standard(proto: "retry_throttling"), + 5: .standard(proto: "health_check_config"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.loadBalancingPolicy) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.methodConfig) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._retryThrottling) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.loadBalancingConfig) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._healthCheckConfig) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.loadBalancingPolicy != .unspecified { + try visitor.visitSingularEnumField(value: self.loadBalancingPolicy, fieldNumber: 1) + } + if !self.methodConfig.isEmpty { + try visitor.visitRepeatedMessageField(value: self.methodConfig, fieldNumber: 2) + } + try { if let v = self._retryThrottling { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if !self.loadBalancingConfig.isEmpty { + try visitor.visitRepeatedMessageField(value: self.loadBalancingConfig, fieldNumber: 4) + } + try { if let v = self._healthCheckConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_ServiceConfig, rhs: Grpc_ServiceConfig_ServiceConfig) -> Bool { + if lhs.loadBalancingPolicy != rhs.loadBalancingPolicy {return false} + if lhs.loadBalancingConfig != rhs.loadBalancingConfig {return false} + if lhs.methodConfig != rhs.methodConfig {return false} + if lhs._retryThrottling != rhs._retryThrottling {return false} + if lhs._healthCheckConfig != rhs._healthCheckConfig {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNSPECIFIED"), + 1: .same(proto: "ROUND_ROBIN"), + ] +} + +extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_ServiceConfig.protoMessageName + ".RetryThrottlingPolicy" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "max_tokens"), + 2: .standard(proto: "token_ratio"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxTokens) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.tokenRatio) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.maxTokens != 0 { + try visitor.visitSingularUInt32Field(value: self.maxTokens, fieldNumber: 1) + } + if self.tokenRatio != 0 { + try visitor.visitSingularFloatField(value: self.tokenRatio, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy, rhs: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy) -> Bool { + if lhs.maxTokens != rhs.maxTokens {return false} + if lhs.tokenRatio != rhs.tokenRatio {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_ServiceConfig.protoMessageName + ".HealthCheckConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "service_name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._serviceName) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._serviceName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig, rhs: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig) -> Bool { + if lhs._serviceName != rhs._serviceName {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift new file mode 100644 index 000000000..a9365909c --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift @@ -0,0 +1,427 @@ +/* + * 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 Foundation +import SwiftProtobuf +import XCTest + +@testable import GRPCCore + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal final class MethodConfigurationCodingTests: XCTestCase { + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + private func testDecodeThrowsRuntimeError(json: String, as: D.Type) throws { + XCTAssertThrowsError( + ofType: RuntimeError.self, + try self.decoder.decode(D.self, from: Data(json.utf8)) + ) { error in + XCTAssertEqual(error.code, .invalidArgument) + } + } + + func testDecodeMethodConfigName() throws { + let inputs: [(String, MethodConfiguration.Name)] = [ + (#"{"service": "foo.bar", "method": "baz"}"#, .init(service: "foo.bar", method: "baz")), + (#"{"service": "foo.bar"}"#, .init(service: "foo.bar", method: "")), + (#"{}"#, .init(service: "", method: "")), + ] + + for (json, expected) in inputs { + let decoded = try self.decoder.decode(MethodConfiguration.Name.self, from: Data(json.utf8)) + XCTAssertEqual(decoded, expected) + } + } + + func testEncodeDecodeMethodConfigName() throws { + let inputs: [MethodConfiguration.Name] = [ + MethodConfiguration.Name(service: "foo.bar", method: "baz"), + MethodConfiguration.Name(service: "foo.bar", method: ""), + MethodConfiguration.Name(service: "", method: ""), + ] + + // We can't do encode-only tests as the output is non-deterministic (the ordering of + // service/method in the JSON object) + for name in inputs { + let encoded = try self.encoder.encode(name) + let decoded = try self.decoder.decode(MethodConfiguration.Name.self, from: encoded) + XCTAssertEqual(decoded, name) + } + } + + func testDecodeProtobufDuration() throws { + let inputs: [(String, Duration)] = [ + ("1.0s", .seconds(1)), + ("1s", .seconds(1)), + ("1.000000s", .seconds(1)), + ("0s", .zero), + ("100.123s", .milliseconds(100_123)), + ] + + for (input, expected) in inputs { + let json = "\"\(input)\"" + let protoDuration = try self.decoder.decode( + GoogleProtobufDuration.self, + from: Data(json.utf8) + ) + let components = protoDuration.duration.components + + // Conversion is lossy as we go from floating point seconds to integer seconds and + // attoseconds. Allow for millisecond precision. + let divisor: Int64 = 1_000_000_000_000_000 + + XCTAssertEqual(components.seconds, expected.components.seconds) + XCTAssertEqual(components.attoseconds / divisor, expected.components.attoseconds / divisor) + } + } + + func testEncodeProtobufDuration() throws { + let inputs: [(Duration, String)] = [ + (.seconds(1), "\"1.0s\""), + (.zero, "\"0.0s\""), + (.milliseconds(100_123), "\"100.123s\""), + ] + + for (input, expected) in inputs { + let duration = GoogleProtobufDuration(duration: input) + let encoded = try self.encoder.encode(duration) + let json = String(decoding: encoded, as: UTF8.self) + XCTAssertEqual(json, expected) + } + } + + func testDecodeInvalidProtobufDuration() throws { + for timestamp in ["1", "1ss", "1S", "1.0S"] { + let json = "\"\(timestamp)\"" + try self.testDecodeThrowsRuntimeError(json: json, as: GoogleProtobufDuration.self) + } + } + + func testDecodeRPCCodeFromCaseName() throws { + let inputs: [(String, Status.Code)] = [ + ("OK", .ok), + ("CANCELLED", .cancelled), + ("UNKNOWN", .unknown), + ("INVALID_ARGUMENT", .invalidArgument), + ("DEADLINE_EXCEEDED", .deadlineExceeded), + ("NOT_FOUND", .notFound), + ("ALREADY_EXISTS", .alreadyExists), + ("PERMISSION_DENIED", .permissionDenied), + ("RESOURCE_EXHAUSTED", .resourceExhausted), + ("FAILED_PRECONDITION", .failedPrecondition), + ("ABORTED", .aborted), + ("OUT_OF_RANGE", .outOfRange), + ("UNIMPLEMENTED", .unimplemented), + ("INTERNAL", .internalError), + ("UNAVAILABLE", .unavailable), + ("DATA_LOSS", .dataLoss), + ("UNAUTHENTICATED", .unauthenticated), + ] + + for (name, expected) in inputs { + let json = "\"\(name)\"" + let code = try self.decoder.decode(GoogleRPCCode.self, from: Data(json.utf8)) + XCTAssertEqual(code.code, expected) + } + } + + func testDecodeRPCCodeFromRawValue() throws { + let inputs: [(Int, Status.Code)] = [ + (0, .ok), + (1, .cancelled), + (2, .unknown), + (3, .invalidArgument), + (4, .deadlineExceeded), + (5, .notFound), + (6, .alreadyExists), + (7, .permissionDenied), + (8, .resourceExhausted), + (9, .failedPrecondition), + (10, .aborted), + (11, .outOfRange), + (12, .unimplemented), + (13, .internalError), + (14, .unavailable), + (15, .dataLoss), + (16, .unauthenticated), + ] + + for (rawValue, expected) in inputs { + let json = "\(rawValue)" + let code = try self.decoder.decode(GoogleRPCCode.self, from: Data(json.utf8)) + XCTAssertEqual(code.code, expected) + } + } + + func testEncodeDecodeRPCCode() throws { + let codes: [Status.Code] = [ + .ok, + .cancelled, + .unknown, + .invalidArgument, + .deadlineExceeded, + .notFound, + .alreadyExists, + .permissionDenied, + .resourceExhausted, + .failedPrecondition, + .aborted, + .outOfRange, + .unimplemented, + .internalError, + .unavailable, + .dataLoss, + .unauthenticated, + ] + + for code in codes { + let encoded = try self.encoder.encode(GoogleRPCCode(code: code)) + let decoded = try self.decoder.decode(GoogleRPCCode.self, from: encoded) + XCTAssertEqual(decoded.code, code) + } + } + + func testDecodeRetryPolicy() throws { + let json = """ + { + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED", "UNAVAILABLE"] + } + """ + + let expected = RetryPolicy( + maximumAttempts: 3, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(3), + backoffMultiplier: 1.6, + retryableStatusCodes: [.aborted, .unavailable] + ) + + let decoded = try self.decoder.decode(RetryPolicy.self, from: Data(json.utf8)) + XCTAssertEqual(decoded, expected) + } + + func testEncodeDecodeRetryPolicy() throws { + let policy = RetryPolicy( + maximumAttempts: 3, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(3), + backoffMultiplier: 1.6, + retryableStatusCodes: [.aborted] + ) + + let encoded = try self.encoder.encode(policy) + let decoded = try self.decoder.decode(RetryPolicy.self, from: encoded) + XCTAssertEqual(decoded, policy) + } + + func testDecodeRetryPolicyWithInvalidRetryMaxAttempts() throws { + let cases = ["-1", "0", "1"] + for maxAttempts in cases { + let json = """ + { + "maxAttempts": \(maxAttempts), + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED"] + } + """ + + try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + } + } + + func testDecodeRetryPolicyWithInvalidInitialBackoff() throws { + let cases = ["0s", "-1s"] + for backoff in cases { + let json = """ + { + "maxAttempts": 3, + "initialBackoff": "\(backoff)", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED"] + } + """ + try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + } + } + + func testDecodeRetryPolicyWithInvalidMaxBackoff() throws { + let cases = ["0s", "-1s"] + for backoff in cases { + let json = """ + { + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "\(backoff)", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED"] + } + """ + try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + } + } + + func testDecodeRetryPolicyWithInvalidBackoffMultiplier() throws { + let cases = ["0", "-1.5"] + for multiplier in cases { + let json = """ + { + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": \(multiplier), + "retryableStatusCodes": ["ABORTED"] + } + """ + try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + } + } + + func testDecodeRetryPolicyWithEmptyRetryableStatusCodes() throws { + let json = """ + { + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": 1, + "retryableStatusCodes": [] + } + """ + try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + } + + func testDecodeHedgingPolicy() throws { + let json = """ + { + "maxAttempts": 3, + "hedgingDelay": "1s", + "nonFatalStatusCodes": ["ABORTED"] + } + """ + + let expected = HedgingPolicy( + maximumAttempts: 3, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [.aborted] + ) + + let decoded = try self.decoder.decode(HedgingPolicy.self, from: Data(json.utf8)) + XCTAssertEqual(decoded, expected) + } + + func testEncodeDecodeHedgingPolicy() throws { + let policy = HedgingPolicy( + maximumAttempts: 3, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [.aborted] + ) + + let encoded = try self.encoder.encode(policy) + let decoded = try self.decoder.decode(HedgingPolicy.self, from: encoded) + XCTAssertEqual(decoded, policy) + } + + func testMethodConfigDecodeFromJSON() throws { + let config = Grpc_ServiceConfig_MethodConfig.with { + $0.name = [ + .with { + $0.service = "echo.Echo" + $0.method = "Get" + } + ] + + $0.timeout = .with { + $0.seconds = 1 + $0.nanos = 0 + } + + $0.maxRequestMessageBytes = 1024 + $0.maxResponseMessageBytes = 2048 + } + + // Test the 'regular' config. + do { + let jsonConfig = try config.jsonUTF8Data() + let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) + XCTAssertEqual(decoded.names, [MethodConfiguration.Name(service: "echo.Echo", method: "Get")]) + XCTAssertEqual(decoded.timeout, Duration(secondsComponent: 1, attosecondsComponent: 0)) + XCTAssertEqual(decoded.maxRequestMessageBytes, 1024) + XCTAssertEqual(decoded.maxResponseMessageBytes, 2048) + XCTAssertNil(decoded.executionPolicy) + } + + // Test the hedging policy. + do { + var config = config + config.hedgingPolicy = .with { + $0.maxAttempts = 3 + $0.hedgingDelay = .with { $0.seconds = 42 } + $0.nonFatalStatusCodes = [ + .aborted, + .unimplemented, + ] + } + + let jsonConfig = try config.jsonUTF8Data() + let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) + + switch decoded.executionPolicy { + case let .some(.hedge(policy)): + XCTAssertEqual(policy.maximumAttempts, 3) + XCTAssertEqual(policy.hedgingDelay, .seconds(42)) + XCTAssertEqual(policy.nonFatalStatusCodes, [.aborted, .unimplemented]) + default: + XCTFail("Expected hedging policy") + } + } + + // Test the retry policy. + do { + var config = config + config.retryPolicy = .with { + $0.maxAttempts = 3 + $0.initialBackoff = .with { $0.seconds = 1 } + $0.maxBackoff = .with { $0.seconds = 3 } + $0.backoffMultiplier = 1.6 + $0.retryableStatusCodes = [ + .aborted, + .unimplemented, + ] + } + + let jsonConfig = try config.jsonUTF8Data() + let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) + + switch decoded.executionPolicy { + case let .some(.retry(policy)): + XCTAssertEqual(policy.maximumAttempts, 3) + XCTAssertEqual(policy.initialBackoff, .seconds(1)) + XCTAssertEqual(policy.maximumBackoff, .seconds(3)) + XCTAssertEqual(policy.backoffMultiplier, 1.6) + XCTAssertEqual(policy.retryableStatusCodes, [.aborted, .unimplemented]) + default: + XCTFail("Expected hedging policy") + } + } + } +} diff --git a/Tests/GRPCCoreTests/MethodConfigurationTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigurationTests.swift similarity index 100% rename from Tests/GRPCCoreTests/MethodConfigurationTests.swift rename to Tests/GRPCCoreTests/Configuration/MethodConfigurationTests.swift diff --git a/Tests/GRPCCoreTests/Configuration/ServiceConfigurationCodingTests.swift b/Tests/GRPCCoreTests/Configuration/ServiceConfigurationCodingTests.swift new file mode 100644 index 000000000..2dc8a943e --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigurationCodingTests.swift @@ -0,0 +1,261 @@ +/* + * 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 Foundation +import GRPCCore +import XCTest + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class ServiceConfigurationCodingTests: XCTestCase { + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + private func testDecodeThrowsRuntimeError(json: String, as: D.Type) throws { + XCTAssertThrowsError( + ofType: RuntimeError.self, + try self.decoder.decode(D.self, from: Data(json.utf8)) + ) { error in + XCTAssertEqual(error.code, .invalidArgument) + } + } + + private func testRoundTripEncodeDecode(_ value: C) throws { + let encoded = try self.encoder.encode(value) + let decoded = try self.decoder.decode(C.self, from: encoded) + XCTAssertEqual(decoded, value) + } + + func testDecodeRetryThrottlingPolicy() throws { + let json = """ + { + "maxTokens": 10, + "tokenRatio": 0.5 + } + """ + + let expected = try ServiceConfiguration.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) + let policy = try self.decoder.decode( + ServiceConfiguration.RetryThrottlingPolicy.self, + from: Data(json.utf8) + ) + + XCTAssertEqual(policy, expected) + } + + func testEncodeDecodeRetryThrottlingPolicy() throws { + let policy = try ServiceConfiguration.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) + try self.testRoundTripEncodeDecode(policy) + } + + func testDecodeRetryThrottlingPolicyWithInvalidTokens() throws { + let inputs = ["0", "-1", "-42"] + for input in inputs { + let json = """ + { + "maxTokens": \(input), + "tokenRatio": 0.5 + } + """ + + try self.testDecodeThrowsRuntimeError( + json: json, + as: ServiceConfiguration.RetryThrottlingPolicy.self + ) + } + } + + func testDecodeRetryThrottlingPolicyWithInvalidTokenRatio() throws { + let inputs = ["0.0", "-1.0", "-42"] + for input in inputs { + let json = """ + { + "maxTokens": 10, + "tokenRatio": \(input) + } + """ + + try self.testDecodeThrowsRuntimeError( + json: json, + as: ServiceConfiguration.RetryThrottlingPolicy.self + ) + } + } + + func testDecodePickFirstPolicy() throws { + let inputs: [(String, ServiceConfiguration.LoadBalancingConfiguration.PickFirst)] = [ + (#"{"shuffleAddressList": true}"#, .init(shuffleAddressList: true)), + (#"{"shuffleAddressList": false}"#, .init(shuffleAddressList: false)), + (#"{}"#, .init(shuffleAddressList: false)), + ] + + for (input, expected) in inputs { + let pickFirst = try self.decoder.decode( + ServiceConfiguration.LoadBalancingConfiguration.PickFirst.self, + from: Data(input.utf8) + ) + + XCTAssertEqual(pickFirst, expected) + } + } + + func testEncodePickFirstPolicy() throws { + let inputs: [(ServiceConfiguration.LoadBalancingConfiguration.PickFirst, String)] = [ + (.init(shuffleAddressList: true), #"{"shuffleAddressList":true}"#), + (.init(shuffleAddressList: false), #"{"shuffleAddressList":false}"#), + ] + + for (input, expected) in inputs { + let encoded = try self.encoder.encode(input) + XCTAssertEqual(String(decoding: encoded, as: UTF8.self), expected) + } + } + + func testDecodeRoundRobinPolicy() throws { + let json = "{}" + let policy = try self.decoder.decode( + ServiceConfiguration.LoadBalancingConfiguration.RoundRobin.self, + from: Data(json.utf8) + ) + XCTAssertEqual(policy, ServiceConfiguration.LoadBalancingConfiguration.RoundRobin()) + } + + func testEncodeRoundRobinPolicy() throws { + let policy = ServiceConfiguration.LoadBalancingConfiguration.RoundRobin() + let encoded = try self.encoder.encode(policy) + XCTAssertEqual(String(decoding: encoded, as: UTF8.self), "{}") + } + + func testDecodeLoadBalancingConfiguration() throws { + let inputs: [(String, ServiceConfiguration.LoadBalancingConfiguration)] = [ + (#"{"round_robin": {}}"#, .roundRobin), + (#"{"pick_first": {}}"#, .pickFirst(shuffleAddressList: false)), + (#"{"pick_first": {"shuffleAddressList": false}}"#, .pickFirst(shuffleAddressList: false)), + ] + + for (input, expected) in inputs { + let decoded = try self.decoder.decode( + ServiceConfiguration.LoadBalancingConfiguration.self, + from: Data(input.utf8) + ) + XCTAssertEqual(decoded, expected) + } + } + + func testEncodeLoadBalancingConfiguration() throws { + let inputs: [(ServiceConfiguration.LoadBalancingConfiguration, String)] = [ + (.roundRobin, #"{"round_robin":{}}"#), + (.pickFirst(shuffleAddressList: false), #"{"pick_first":{"shuffleAddressList":false}}"#), + ] + + for (input, expected) in inputs { + let encoded = try self.encoder.encode(input) + XCTAssertEqual(String(decoding: encoded, as: UTF8.self), expected) + } + } + + func testDecodeServiceConfigurationFromProtoJSON() throws { + let serviceConfig = Grpc_ServiceConfig_ServiceConfig.with { + $0.methodConfig = [ + Grpc_ServiceConfig_MethodConfig.with { + $0.name = [ + Grpc_ServiceConfig_MethodConfig.Name.with { + $0.service = "foo.Foo" + $0.method = "Bar" + } + ] + $0.timeout = .with { $0.seconds = 1 } + $0.maxRequestMessageBytes = 123 + $0.maxResponseMessageBytes = 456 + } + + ] + $0.loadBalancingConfig = [ + .with { $0.roundRobin = .init() }, + .with { $0.pickFirst = .with { $0.shuffleAddressList = true } }, + ] + $0.retryThrottling = .with { + $0.maxTokens = 10 + $0.tokenRatio = 0.1 + } + } + + let encoded = try serviceConfig.jsonUTF8Data() + let decoded = try self.decoder.decode(ServiceConfiguration.self, from: encoded) + + let expected = ServiceConfiguration( + methodConfiguration: [ + MethodConfiguration( + names: [ + MethodConfiguration.Name(service: "foo.Foo", method: "Bar") + ], + timeout: .seconds(1), + maxRequestMessageBytes: 123, + maxResponseMessageBytes: 456 + ) + ], + loadBalancingConfiguration: [ + .roundRobin, + .pickFirst(shuffleAddressList: true), + ], + retryThrottlingPolicy: try ServiceConfiguration.RetryThrottlingPolicy( + maxTokens: 10, + tokenRatio: 0.1 + ) + ) + + XCTAssertEqual(decoded, expected) + } + + func testEncodeAndDecodeServiceConfiguration() throws { + let serviceConfig = ServiceConfiguration( + methodConfiguration: [ + MethodConfiguration( + names: [ + MethodConfiguration.Name(service: "echo.Echo", method: "Get"), + MethodConfiguration.Name(service: "greeter.HelloWorld"), + ], + timeout: .seconds(42), + maxRequestMessageBytes: 2048, + maxResponseMessageBytes: 4096, + executionPolicy: .hedge( + HedgingPolicy( + maximumAttempts: 3, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [.aborted] + ) + ) + ), + MethodConfiguration( + names: [ + MethodConfiguration.Name(service: "echo.Echo", method: "Update") + ], + timeout: .seconds(300), + maxRequestMessageBytes: 10_000 + ), + ], + loadBalancingConfiguration: [ + .pickFirst(shuffleAddressList: true), + .roundRobin, + ], + retryThrottlingPolicy: try ServiceConfiguration.RetryThrottlingPolicy( + maxTokens: 10, + tokenRatio: 3.141 + ) + ) + + try self.testRoundTripEncodeDecode(serviceConfig) + } +} diff --git a/Tests/GRPCCoreTests/MethodConfigurationsTests.swift b/Tests/GRPCCoreTests/MethodConfigurationsTests.swift index 0d8e88829..165b5a134 100644 --- a/Tests/GRPCCoreTests/MethodConfigurationsTests.swift +++ b/Tests/GRPCCoreTests/MethodConfigurationsTests.swift @@ -24,7 +24,7 @@ final class MethodConfigurationsTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) let descriptor = MethodDescriptor(service: "test", method: "first") @@ -35,7 +35,7 @@ final class MethodConfigurationsTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) configurations[descriptor] = overrideConfiguration XCTAssertEqual(configurations[descriptor], overrideConfiguration) @@ -47,7 +47,7 @@ final class MethodConfigurationsTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test", method: "") @@ -58,7 +58,7 @@ final class MethodConfigurationsTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration let secondDescriptor = MethodDescriptor(service: "test", method: "second") @@ -71,7 +71,7 @@ final class MethodConfigurationsTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test1", method: "first") @@ -82,7 +82,7 @@ final class MethodConfigurationsTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration let secondDescriptor = MethodDescriptor(service: "test2", method: "second") diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 7aa64d05c..18209ac01 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -187,7 +187,7 @@ final class InProcessClientTransportTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(hedgingPolicy: policy) + let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) @@ -203,7 +203,7 @@ final class InProcessClientTransportTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(retryPolicy: retryPolicy) + let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration client = InProcessClientTransport(server: .init(), methodConfiguration: configurations) let secondDescriptor = MethodDescriptor(service: "test", method: "second") @@ -255,7 +255,7 @@ final class InProcessClientTransportTests: XCTestCase { var methodConfiguration = MethodConfigurations() methodConfiguration.setDefaultConfiguration( - configuration ?? .init(retryPolicy: defaultPolicy) + configuration ?? .init(names: [], executionPolicy: .retry(defaultPolicy)) ) return InProcessClientTransport( server: server, From daa008e437c88001b5df77424559dd70dd1e6aef Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:07:44 +0000 Subject: [PATCH 235/580] [ProtobufCodeGen] Use the generated input and output type names (#1792) Motivation: protoc-gen-swift generates message type names that are different from the ones in the file descriptors. The CodeGenerationRequest object should contain the generated names for the ProtobufCodeGen. Modifications: - the parser uses a SwiftProtobufNamer to get the generated type names and adds them in the CodeGenerationRequest objects - propagated the change and modified tests accordingly Result: The input and output type names will be the generated ones and the generated code will compile. --------- Co-authored-by: George Barnett --- .../ProtobufCodeGenParser.swift | 47 ++++++++++++++----- .../ProtobufCodeGenerator.swift | 16 +++++-- Sources/protoc-gen-grpc-swift/main.swift | 5 +- .../ProtobufCodeGenParserTests.swift | 8 ++-- .../ProtobufCodeGeneratorTests.swift | 20 +++++--- 5 files changed, 66 insertions(+), 30 deletions(-) diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index 1ff93f6a0..689aa9a43 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -22,9 +22,19 @@ import struct GRPCCodeGen.CodeGenerationRequest /// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. internal struct ProtobufCodeGenParser { - internal init() {} - internal func parse(input: FileDescriptor) throws -> CodeGenerationRequest { - var header = input.header + let input: FileDescriptor + let namer: SwiftProtobufNamer + + internal init(input: FileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings) { + self.input = input + self.namer = SwiftProtobufNamer( + currentFile: input, + protoFileToModuleMappings: protoFileModuleMappings + ) + } + + internal func parse() throws -> CodeGenerationRequest { + var header = self.input.header // Ensuring there is a blank line after the header. if !header.isEmpty && !header.hasSuffix("\n\n") { header.append("\n") @@ -34,13 +44,13 @@ internal struct ProtobufCodeGenParser { // swift-format-ignore-file // // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: \(input.name) + // Source: \(self.input.name) // // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift """ - var dependencies = input.dependencies.map { + var dependencies = self.input.dependencies.map { CodeGenerationRequest.Dependency(module: $0.name) } dependencies.append(CodeGenerationRequest.Dependency(module: "GRPCProtobuf")) @@ -50,12 +60,16 @@ internal struct ProtobufCodeGenParser { let lookupDeserializer: (String) -> String = { messageType in "ProtobufDeserializer<\(messageType)>()" } - let services = input.services.map { - CodeGenerationRequest.ServiceDescriptor(descriptor: $0, package: input.package) + let services = self.input.services.map { + CodeGenerationRequest.ServiceDescriptor( + descriptor: $0, + package: input.package, + protobufNamer: self.namer + ) } return CodeGenerationRequest( - fileName: input.name, + fileName: self.input.name, leadingTrivia: header + leadingTrivia, dependencies: dependencies, services: services, @@ -66,9 +80,16 @@ internal struct ProtobufCodeGenParser { } extension CodeGenerationRequest.ServiceDescriptor { - fileprivate init(descriptor: ServiceDescriptor, package: String) { + fileprivate init( + descriptor: ServiceDescriptor, + package: String, + protobufNamer: SwiftProtobufNamer + ) { let methods = descriptor.methods.map { - CodeGenerationRequest.ServiceDescriptor.MethodDescriptor(descriptor: $0) + CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( + descriptor: $0, + protobufNamer: protobufNamer + ) } let name = CodeGenerationRequest.Name( base: descriptor.name, @@ -86,7 +107,7 @@ extension CodeGenerationRequest.ServiceDescriptor { } extension CodeGenerationRequest.ServiceDescriptor.MethodDescriptor { - fileprivate init(descriptor: MethodDescriptor) { + fileprivate init(descriptor: MethodDescriptor, protobufNamer: SwiftProtobufNamer) { let name = CodeGenerationRequest.Name( base: descriptor.name, generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), @@ -98,8 +119,8 @@ extension CodeGenerationRequest.ServiceDescriptor.MethodDescriptor { name: name, isInputStreaming: descriptor.clientStreaming, isOutputStreaming: descriptor.serverStreaming, - inputType: descriptor.inputType.name, - outputType: descriptor.outputType.name + inputType: protobufNamer.fullName(message: descriptor.inputType), + outputType: protobufNamer.fullName(message: descriptor.outputType) ) } } diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index caa198d7a..798578705 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -20,15 +20,23 @@ import SwiftProtobufPluginLibrary public struct ProtobufCodeGenerator { internal var configuration: SourceGenerator.Configuration - public init(configuration: SourceGenerator.Configuration) { + public init( + configuration: SourceGenerator.Configuration + ) { self.configuration = configuration } - public func generateCode(from fileDescriptor: FileDescriptor) throws -> String { - let parser = ProtobufCodeGenParser() + public func generateCode( + from fileDescriptor: FileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings + ) throws -> String { + let parser = ProtobufCodeGenParser( + input: fileDescriptor, + protoFileModuleMappings: protoFileModuleMappings + ) let sourceGenerator = SourceGenerator(configuration: self.configuration) - let codeGenerationRequest = try parser.parse(input: fileDescriptor) + let codeGenerationRequest = try parser.parse() let sourceFile = try sourceGenerator.generate(codeGenerationRequest) return sourceFile.contents } diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 12002c117..57cda5f8e 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -172,7 +172,10 @@ func main(args: [String]) throws { let grpcGenerator = ProtobufCodeGenerator( configuration: SourceGenerator.Configuration(options: options) ) - grpcFile.content = try grpcGenerator.generateCode(from: fileDescriptor) + grpcFile.content = try grpcGenerator.generateCode( + from: fileDescriptor, + protoFileModuleMappings: options.protoToModuleMappings + ) } else { let grpcGenerator = Generator(fileDescriptor, options: options) grpcFile.content = grpcGenerator.code diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 25ba47dd9..cfcc948d6 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -31,9 +31,7 @@ final class ProtobufCodeGenParserTests: XCTestCase { """ ) } - let parsedCodeGenRequest = try ProtobufCodeGenParser().parse( - input: fileDescriptor - ) + let parsedCodeGenRequest = try ProtobufCodeGenParser(input: fileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings()).parse() XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") XCTAssertEqual( parsedCodeGenRequest.leadingTrivia, @@ -75,8 +73,8 @@ final class ProtobufCodeGenParserTests: XCTestCase { ), isInputStreaming: false, isOutputStreaming: false, - inputType: "HelloRequest", - outputType: "HelloReply" + inputType: "Helloworld_HelloRequest", + outputType: "Helloworld_HelloReply" ) guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } XCTAssertEqual(method, expectedMethod) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index d6bb08059..8a77eae85 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -60,8 +60,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal enum Greeter { internal enum Method { internal enum SayHello { - internal typealias Input = HelloRequest - internal typealias Output = HelloReply + internal typealias Input = Helloworld_HelloRequest + internal typealias Output = Helloworld_HelloReply internal static let descriptor = MethodDescriptor( service: "helloworld.Greeter", method: "SayHello" @@ -165,8 +165,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { public enum Greeter { public enum Method { public enum SayHello { - public typealias Input = HelloRequest - public typealias Output = HelloReply + public typealias Input = Helloworld_HelloRequest + public typealias Output = Helloworld_HelloReply public static let descriptor = MethodDescriptor( service: "helloworld.Greeter", method: "SayHello" @@ -254,8 +254,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package enum Greeter { package enum Method { package enum SayHello { - package typealias Input = HelloRequest - package typealias Output = HelloReply + package typealias Input = Helloworld_HelloRequest + package typealias Output = Helloworld_HelloReply package static let descriptor = MethodDescriptor( service: "helloworld.Greeter", method: "SayHello" @@ -384,7 +384,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } let generator = ProtobufCodeGenerator(configuration: configs) - try XCTAssertEqualWithDiff(try generator.generateCode(from: fileDescriptor), expectedCode) + try XCTAssertEqualWithDiff( + try generator.generateCode( + from: fileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings() + ), + expectedCode + ) } } From 8bfddf391bb9d515d566270f4b5f4d0b897f301e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 7 Feb 2024 10:57:49 +0000 Subject: [PATCH 236/580] Fix formatting (#1796) --- .../ProtobufCodeGenParserTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index cfcc948d6..f7ac2cf7d 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -31,7 +31,10 @@ final class ProtobufCodeGenParserTests: XCTestCase { """ ) } - let parsedCodeGenRequest = try ProtobufCodeGenParser(input: fileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings()).parse() + let parsedCodeGenRequest = try ProtobufCodeGenParser( + input: fileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings() + ).parse() XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") XCTAssertEqual( parsedCodeGenRequest.leadingTrivia, From d2d187ec40d51b1e917e0ed9271dc7897aa41d44 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 7 Feb 2024 11:15:31 +0000 Subject: [PATCH 237/580] Use ServiceConfiguration more widely (#1794) Motivation: ServiceConfiguration was recently added which has a lot of overlap with MethodConfigurations. These should be de-duped where possible. Modifications: - Make `MethodConfigurations` underscored-public and refactor to use `MethodConfiguration.Name` - Pass `ServiceConfiguration` instead of `MethodConfigurations` where it makes sense - Update docs and tests * [ProtobufCodeGen] Use the generated input and output type names (#1792) Motivation: protoc-gen-swift generates message type names that are different from the ones in the file descriptors. The CodeGenerationRequest object should contain the generated names for the ProtobufCodeGen. Modifications: - the parser uses a SwiftProtobufNamer to get the generated type names and adds them in the CodeGenerationRequest objects - propagated the change and modified tests accordingly Result: The input and output type names will be the generated ones and the generated code will compile. Co-authored-by: Gustavo Cairo --- Sources/GRPCCore/GRPCClient.swift | 81 ++++++++++++------- .../_MethodConfigurations.swift} | 41 ++++++---- .../GRPCCore/Transport/ClientTransport.swift | 8 +- .../GRPCCore/Transport/RetryThrottle.swift | 8 ++ .../InProcessClientTransport.swift | 16 ++-- .../InProcessTransport.swift | 11 +-- .../MethodConfigurationsTests.swift | 6 +- .../Transport/AnyTransport.swift | 4 +- .../Transport/StreamCountingTransport.swift | 4 +- .../Transport/ThrowingTransport.swift | 2 +- .../InProcessClientTransportTests.swift | 62 ++++++++++---- 11 files changed, 158 insertions(+), 85 deletions(-) rename Sources/GRPCCore/{MethodConfigurations.swift => Internal/_MethodConfigurations.swift} (72%) rename Tests/GRPCCoreTests/{ => Internal}/MethodConfigurationsTests.swift (96%) diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index d2434205e..888e893c6 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -28,7 +28,7 @@ import Atomics /// /// However, in most cases you should prefer wrapping the ``GRPCClient`` with a generated stub. /// -/// You can set ``MethodConfiguration``s on this client to override whatever configurations have been +/// You can set ``ServiceConfiguration``s on this client to override whatever configurations have been /// set on the given transport. You can also use ``ClientInterceptor``s to implement cross-cutting /// logic which apply to all RPCs. Example uses of interceptors include authentication and logging. /// @@ -37,21 +37,32 @@ import Atomics /// The following example demonstrates how to create and configure a client. /// /// ```swift -/// // Create a configuration object for the client. +/// // Create a configuration object for the client and override the timeout for the 'Get' method on +/// // the 'echo.Echo' service. This configuration takes precedence over any set by the transport. /// var configuration = GRPCClient.Configuration() -/// -/// // Override the timeout for the 'Get' method on the 'echo.Echo' service. This configuration -/// // takes precedence over any set by the transport. -/// let echoGet = MethodDescriptor(service: "echo.Echo", method: "Get") -/// configuration.method.overrides[echoGet] = MethodConfiguration( -/// executionPolicy: nil, -/// timeout: .seconds(5) +/// configuration.service.override = ServiceConfiguration( +/// methodConfiguration: [ +/// MethodConfiguration( +/// names: [ +/// MethodConfiguration.Name(service: "echo.Echo", method: "Get") +/// ], +/// timeout: .seconds(5) +/// ) +/// ] /// ) /// -/// // Configure a fallback timeout for all RPCs if no configuration is provided in the overrides -/// // or by the transport. -/// let defaultMethodConfiguration = MethodConfiguration(executionPolicy: nil, timeout: seconds(10)) -/// configuration.method.defaults.setDefaultConfiguration(defaultMethodConfiguration) +/// // Configure a fallback timeout for all RPCs (indicated by an empty service and method name) if +/// // no configuration is provided in the overrides or by the transport. +/// configuration.service.defaults = ServiceConfiguration( +/// methodConfiguration: [ +/// MethodConfiguration( +/// names: [ +/// MethodConfiguration.Name(service: "", method: "") +/// ], +/// timeout: .seconds(10) +/// ) +/// ] +/// ) /// /// // Finally create a transport and instantiate the client, adding an interceptor. /// let inProcessServerTransport = InProcessServerTransport() @@ -114,6 +125,11 @@ public struct GRPCClient: Sendable { /// The configuration used by the client. public let configuration: Configuration + /// A cache of method config overrides. + private let methodConfigurationOverrides: _MethodConfigurations + /// A cache of method config defaults. + private let methodConfigurationDefaults: _MethodConfigurations + /// The current state of the client. private let state: ManagedAtomic @@ -149,6 +165,14 @@ public struct GRPCClient: Sendable { self.transport = transport self.interceptors = interceptors self.configuration = configuration + + self.methodConfigurationOverrides = _MethodConfigurations( + serviceConfiguration: self.configuration.service.overrides + ) + self.methodConfigurationDefaults = _MethodConfigurations( + serviceConfiguration: self.configuration.service.defaults + ) + self.state = ManagedAtomic(.notStarted) } @@ -380,15 +404,15 @@ public struct GRPCClient: Sendable { } private func resolveMethodConfiguration(for descriptor: MethodDescriptor) -> MethodConfiguration { - if let configuration = self.configuration.method.overrides[descriptor] { + if let configuration = self.methodConfigurationOverrides[descriptor] { return configuration } - if let configuration = self.transport.executionConfiguration(forMethod: descriptor) { + if let configuration = self.transport.configuration(forMethod: descriptor) { return configuration } - if let configuration = self.configuration.method.defaults[descriptor] { + if let configuration = self.methodConfigurationDefaults[descriptor] { return configuration } @@ -400,41 +424,44 @@ public struct GRPCClient: Sendable { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCClient { public struct Configuration: Sendable { - /// Configuration for how methods are executed. + /// Configuration for how the client interacts with services. /// - /// Method configuration determines how each RPC is executed by the client. Some services and + /// The configuration includes information about how the client should load balance requests, + /// how retries should be throttled and how methods should be executed. Some services and /// transports provide this information to the client when the server name is resolved. However, - /// you override this configuration and set default values should no override be set and the + /// you can override this configuration and set default values should no override be set and the /// transport doesn't provide a value. - public var method: Method + /// + /// See also ``ServiceConfiguration``. + public var service: Service /// Creates a new default configuration. public init() { - self.method = Method() + self.service = Service() } } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCClient.Configuration { - /// Configuration for how methods should be executed. + /// Configuration for how the client interacts with services. /// /// In most cases the client should defer to the configuration provided by the transport as this /// can be provided to the transport as part of name resolution when establishing a connection. /// /// The client first checks ``overrides`` for a configuration, followed by the transport, followed /// by ``defaults``. - public struct Method: Sendable, Hashable { + public struct Service: Sendable, Hashable { /// Configuration to use in precedence to that provided by the transport. - public var overrides: MethodConfigurations + public var overrides: ServiceConfiguration /// Configuration to use only if there are no overrides and the transport doesn't specify /// any configuration. - public var defaults: MethodConfigurations + public var defaults: ServiceConfiguration public init() { - self.overrides = MethodConfigurations() - self.defaults = MethodConfigurations() + self.overrides = ServiceConfiguration() + self.defaults = ServiceConfiguration() } } } diff --git a/Sources/GRPCCore/MethodConfigurations.swift b/Sources/GRPCCore/Internal/_MethodConfigurations.swift similarity index 72% rename from Sources/GRPCCore/MethodConfigurations.swift rename to Sources/GRPCCore/Internal/_MethodConfigurations.swift index c7e741222..ab719439a 100644 --- a/Sources/GRPCCore/MethodConfigurations.swift +++ b/Sources/GRPCCore/Internal/_MethodConfigurations.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +// TODO: when swift(>=5.9), use 'package' access level + /// A collection of ``MethodConfiguration``s, mapped to specific methods or services. /// /// When creating a new instance, no overrides and no default will be set for using when getting @@ -23,12 +25,20 @@ /// /// Use the subscript to get and set configurations for specific methods. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct MethodConfigurations: Sendable, Hashable { - private var elements: [MethodDescriptor: MethodConfiguration] +public struct _MethodConfigurations: Sendable, Hashable { + private var elements: [MethodConfiguration.Name: MethodConfiguration] - /// Create a new ``MethodConfigurations`` with no overrides and no default configuration. - public init() { + /// Create a new ``_MethodConfigurations``. + /// + /// - Parameter serviceConfiguration: The configuration to read ``MethodConfiguration`` from. + public init(serviceConfiguration: ServiceConfiguration = ServiceConfiguration()) { self.elements = [:] + + for configuration in serviceConfiguration.methodConfiguration { + for name in configuration.names { + self.elements[name] = configuration + } + } } /// Get or set the corresponding ``MethodConfiguration`` for the given ``MethodDescriptor``. @@ -44,25 +54,27 @@ public struct MethodConfigurations: Sendable, Hashable { /// - descriptor: The ``MethodDescriptor`` for which to get or set a ``MethodConfiguration``. public subscript(_ descriptor: MethodDescriptor) -> MethodConfiguration? { get { - if let configuration = self.elements[descriptor] { + var name = MethodConfiguration.Name(service: descriptor.service, method: descriptor.method) + + if let configuration = self.elements[name] { return configuration } // Check if the config is set at the service level by clearing the method. - var descriptor = descriptor - descriptor.method = "" + name.method = "" - if let configuration = self.elements[descriptor] { + if let configuration = self.elements[name] { return configuration } // Check if the config is set at the global level by clearing the service and method. - descriptor.service = "" - return self.elements[descriptor] + name.service = "" + return self.elements[name] } set { - self.elements[descriptor] = newValue + let name = MethodConfiguration.Name(service: descriptor.service, method: descriptor.method) + self.elements[name] = newValue } } @@ -70,8 +82,8 @@ public struct MethodConfigurations: Sendable, Hashable { /// /// - Parameter configuration: The default configuration. public mutating func setDefaultConfiguration(_ configuration: MethodConfiguration?) { - let descriptor = MethodDescriptor(service: "", method: "") - self.elements[descriptor] = configuration + let name = MethodConfiguration.Name(service: "", method: "") + self.elements[name] = configuration } /// Set a default configuration for a service. @@ -87,6 +99,7 @@ public struct MethodConfigurations: Sendable, Hashable { _ configuration: MethodConfiguration?, forService service: String ) { - self.elements[MethodDescriptor(service: service, method: "")] = configuration + let name = MethodConfiguration.Name(service: "", method: "") + self.elements[name] = configuration } } diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 989e31801..819cc606a 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -71,11 +71,9 @@ public protocol ClientTransport: Sendable { _ closure: (_ stream: RPCStream) async throws -> T ) async throws -> T - /// Returns the execution configuration for a given method. + /// Returns the configuration for a given method. /// /// - Parameter descriptor: The method to lookup configuration for. - /// - Returns: Execution configuration for the method, if it exists. - func executionConfiguration( - forMethod descriptor: MethodDescriptor - ) -> MethodConfiguration? + /// - Returns: Configuration for the method, if it exists. + func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfiguration? } diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index c57b614a9..8fccce9c1 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -103,6 +103,14 @@ public struct RetryThrottle: Sendable { self.scaledTokensAvailable = LockedValueBox(scaledTokens) } + /// Create a new throttle. + /// + /// - Parameter policy: The policy to use to configure the throttle. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public init(policy: ServiceConfiguration.RetryThrottlingPolicy) { + self.init(maximumTokens: policy.maxTokens, tokenRatio: policy.tokenRatio) + } + /// Records a success, adding a token to the throttle. @usableFromInline func recordSuccess() { diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index 9a9b5ec95..cbb1d9c81 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -98,22 +98,22 @@ public struct InProcessClientTransport: ClientTransport { public let retryThrottle: RetryThrottle? - private let methodConfiguration: MethodConfigurations + private let methodConfiguration: _MethodConfigurations private let state: _LockedValueBox /// Creates a new in-process client transport. /// /// - Parameters: /// - server: The in-process server transport to connect to. - /// - methodConfiguration: Method specific configuration. - /// - retryThrottle: A throttle to apply to RPCs which are hedged or retried. + /// - serviceConfiguration: Service configuration. public init( server: InProcessServerTransport, - methodConfiguration: MethodConfigurations = MethodConfigurations(), - retryThrottle: RetryThrottle? = nil + serviceConfiguration: ServiceConfiguration = ServiceConfiguration() ) { - self.retryThrottle = retryThrottle - self.methodConfiguration = methodConfiguration + self.retryThrottle = serviceConfiguration.retryThrottlingPolicy.map { + RetryThrottle(policy: $0) + } + self.methodConfiguration = _MethodConfigurations(serviceConfiguration: serviceConfiguration) self.state = _LockedValueBox(.unconnected(.init(serverTransport: server))) } @@ -336,7 +336,7 @@ public struct InProcessClientTransport: ClientTransport { /// /// - Parameter descriptor: The method to lookup configuration for. /// - Returns: Execution configuration for the method, if it exists. - public func executionConfiguration( + public func configuration( forMethod descriptor: MethodDescriptor ) -> MethodConfiguration? { self.methodConfiguration[descriptor] diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index e0db45f7c..02592f747 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -22,21 +22,16 @@ public enum InProcessTransport { /// and a client using that server transport. /// /// - Parameters: - /// - methodConfiguration: Method specific configuration used by the client transport to - /// determine how RPCs should be executed. - /// - retryThrottle: The retry throttle the client transport uses to determine whether a call - /// should be retried. + /// - serviceConfiguration: Configuration describing how methods should be executed. /// - Returns: A tuple containing the connected server and client in-process transports. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public static func makePair( - methodConfiguration: MethodConfigurations = MethodConfigurations(), - retryThrottle: RetryThrottle? = nil + serviceConfiguration: ServiceConfiguration = ServiceConfiguration() ) -> (server: InProcessServerTransport, client: InProcessClientTransport) { let server = InProcessServerTransport() let client = InProcessClientTransport( server: server, - methodConfiguration: methodConfiguration, - retryThrottle: retryThrottle + serviceConfiguration: serviceConfiguration ) return (server, client) } diff --git a/Tests/GRPCCoreTests/MethodConfigurationsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigurationsTests.swift similarity index 96% rename from Tests/GRPCCoreTests/MethodConfigurationsTests.swift rename to Tests/GRPCCoreTests/Internal/MethodConfigurationsTests.swift index 165b5a134..53a90ec62 100644 --- a/Tests/GRPCCoreTests/MethodConfigurationsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigurationsTests.swift @@ -25,7 +25,7 @@ final class MethodConfigurationsTests: XCTestCase { nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) - var configurations = MethodConfigurations() + var configurations = _MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) let descriptor = MethodDescriptor(service: "test", method: "first") let retryPolicy = RetryPolicy( @@ -48,7 +48,7 @@ final class MethodConfigurationsTests: XCTestCase { nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) - var configurations = MethodConfigurations() + var configurations = _MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test", method: "") let retryPolicy = RetryPolicy( @@ -72,7 +72,7 @@ final class MethodConfigurationsTests: XCTestCase { nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) - var configurations = MethodConfigurations() + var configurations = _MethodConfigurations() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test1", method: "first") let retryPolicy = RetryPolicy( diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index e211d214c..3be95f564 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -48,7 +48,7 @@ struct AnyClientTransport: ClientTransport, Sendable { } self._configuration = { descriptor in - transport.executionConfiguration(forMethod: descriptor) + transport.configuration(forMethod: descriptor) } } @@ -72,7 +72,7 @@ struct AnyClientTransport: ClientTransport, Sendable { return result as! T } - func executionConfiguration( + func configuration( forMethod descriptor: MethodDescriptor ) -> MethodConfiguration? { self._configuration(descriptor) diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 9c6ebb276..6fa024a5e 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -66,10 +66,10 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } } - func executionConfiguration( + func configuration( forMethod descriptor: MethodDescriptor ) -> MethodConfiguration? { - self.transport.executionConfiguration(forMethod: descriptor) + self.transport.configuration(forMethod: descriptor) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 25dba3cf2..94e14c069 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -36,7 +36,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { // no-op } - func executionConfiguration( + func configuration( forMethod descriptor: MethodDescriptor ) -> MethodConfiguration? { return nil diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 18209ac01..60c55f941 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -187,14 +187,28 @@ final class InProcessClientTransportTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) - var configurations = MethodConfigurations() - configurations.setDefaultConfiguration(defaultConfiguration) - var client = InProcessClientTransport(server: .init(), methodConfiguration: configurations) + var serviceConfiguration = ServiceConfiguration( + methodConfiguration: [ + MethodConfiguration( + names: [ + MethodConfiguration.Name(service: "", method: "") + ], + executionPolicy: .hedge(policy) + ) + ] + ) + + var client = InProcessClientTransport( + server: InProcessServerTransport(), + serviceConfiguration: serviceConfiguration + ) let firstDescriptor = MethodDescriptor(service: "test", method: "first") - XCTAssertEqual(client.executionConfiguration(forMethod: firstDescriptor), defaultConfiguration) + XCTAssertEqual( + client.configuration(forMethod: firstDescriptor), + serviceConfiguration.methodConfiguration.first + ) let retryPolicy = RetryPolicy( maximumAttempts: 10, @@ -203,12 +217,26 @@ final class InProcessClientTransportTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) - configurations[firstDescriptor] = overrideConfiguration - client = InProcessClientTransport(server: .init(), methodConfiguration: configurations) + + let overrideConfiguration = MethodConfiguration( + names: [MethodConfiguration.Name(service: "test", method: "second")], + executionPolicy: .retry(retryPolicy) + ) + serviceConfiguration.methodConfiguration.append(overrideConfiguration) + client = InProcessClientTransport( + server: InProcessServerTransport(), + serviceConfiguration: serviceConfiguration + ) + let secondDescriptor = MethodDescriptor(service: "test", method: "second") - XCTAssertEqual(client.executionConfiguration(forMethod: firstDescriptor), overrideConfiguration) - XCTAssertEqual(client.executionConfiguration(forMethod: secondDescriptor), defaultConfiguration) + XCTAssertEqual( + client.configuration(forMethod: firstDescriptor), + serviceConfiguration.methodConfiguration.first + ) + XCTAssertEqual( + client.configuration(forMethod: secondDescriptor), + serviceConfiguration.methodConfiguration.last + ) } func testOpenMultipleStreamsThenClose() async throws { @@ -242,7 +270,6 @@ final class InProcessClientTransportTests: XCTestCase { } func makeClient( - configuration: MethodConfiguration? = nil, server: InProcessServerTransport = InProcessServerTransport() ) -> InProcessClientTransport { let defaultPolicy = RetryPolicy( @@ -253,13 +280,18 @@ final class InProcessClientTransportTests: XCTestCase { retryableStatusCodes: [.unavailable] ) - var methodConfiguration = MethodConfigurations() - methodConfiguration.setDefaultConfiguration( - configuration ?? .init(names: [], executionPolicy: .retry(defaultPolicy)) + let serviceConfiguration = ServiceConfiguration( + methodConfiguration: [ + MethodConfiguration( + names: [MethodConfiguration.Name(service: "", method: "")], + executionPolicy: .retry(defaultPolicy) + ) + ] ) + return InProcessClientTransport( server: server, - methodConfiguration: methodConfiguration + serviceConfiguration: serviceConfiguration ) } } From 25c33fd54194ab20b92fd6eb421ca760bbde121a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 7 Feb 2024 11:20:01 +0000 Subject: [PATCH 238/580] Additional logging for connection pool (#1795) Motivation: The connection pool logs information about various states but the logger doesn't have an id attached for the sub-pool making it hard determine the state of individual subpools. Modifications: - Add the subpool id to the logger metadata - Add a log when the pool is initialized - Add a log when the pool can't add any more connections Result: Better visibility --- Sources/GRPC/ConnectionManager.swift | 50 ++++++++++++++++++- .../GRPC/ConnectionPool/ConnectionPool.swift | 45 ++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index a78a57a7f..e21be63b2 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -248,6 +248,39 @@ internal final class ConnectionManager: @unchecked Sendable { } } + /// Returns whether the state is 'connecting'. + private var isConnecting: Bool { + self.eventLoop.assertInEventLoop() + switch self.state { + case .connecting: + return true + case .idle, .transientFailure, .active, .ready, .shutdown: + return false + } + } + + /// Returns whether the state is 'ready'. + private var isReady: Bool { + self.eventLoop.assertInEventLoop() + switch self.state { + case .ready: + return true + case .idle, .active, .connecting, .transientFailure, .shutdown: + return false + } + } + + /// Returns whether the state is 'ready'. + private var isTransientFailure: Bool { + self.eventLoop.assertInEventLoop() + switch self.state { + case .transientFailure: + return true + case .idle, .connecting, .active, .ready, .shutdown: + return false + } + } + /// Returns whether the state is 'shutdown'. private var isShutdown: Bool { self.eventLoop.assertInEventLoop() @@ -1140,7 +1173,22 @@ extension ConnectionManager { return self.manager.isIdle } - /// Returne `true` if the connection is in the shutdown state. + /// Returns `true` if the connection is in the connecting state. + internal var isConnecting: Bool { + return self.manager.isConnecting + } + + /// Returns `true` if the connection is in the ready state. + internal var isReady: Bool { + return self.manager.isReady + } + + /// Returns `true` if the connection is in the transient failure state. + internal var isTransientFailure: Bool { + return self.manager.isTransientFailure + } + + /// Returns `true` if the connection is in the shutdown state. internal var isShutdown: Bool { return self.manager.isShutdown } diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index b1ae58fd3..039d788f3 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -103,7 +103,7 @@ internal final class ConnectionPool { /// A logger which always sets "GRPC" as its source. @usableFromInline - internal let logger: GRPCLogger + private(set) var logger: GRPCLogger /// Returns `NIODeadline` representing 'now'. This is useful for testing. @usableFromInline @@ -139,6 +139,18 @@ internal final class ConnectionPool { /// The ID of waiter. @usableFromInline static let waiterID = "pool.waiter.id" + /// The maximum number of connections allowed in the pool. + @usableFromInline + static let connectionsMax = "pool.connections.max" + /// The number of connections in the ready state. + @usableFromInline + static let connectionsReady = "pool.connections.ready" + /// The number of connections in the connecting state. + @usableFromInline + static let connectionsConnecting = "pool.connections.connecting" + /// The number of connections in the transient failure state. + @usableFromInline + static let connectionsTransientFailure = "pool.connections.transientFailure" } @usableFromInline @@ -158,6 +170,7 @@ internal final class ConnectionPool { (0.0 ... 1.0).contains(reservationLoadThreshold), "reservationLoadThreshold must be within the range 0.0 ... 1.0" ) + self.reservationLoadThreshold = reservationLoadThreshold self.assumedMaxConcurrentStreams = assumedMaxConcurrentStreams @@ -179,6 +192,14 @@ internal final class ConnectionPool { /// - Parameter connections: The number of connections to add to the pool. internal func initialize(connections: Int) { assert(self._connections.isEmpty) + self.logger.logger[metadataKey: Metadata.id] = "\(ObjectIdentifier(self))" + self.logger.debug( + "initializing new sub-pool", + metadata: [ + Metadata.waitersMax: .stringConvertible(self.maxWaiters), + Metadata.connectionsMax: .stringConvertible(connections), + ] + ) self._connections.reserveCapacity(connections) while self._connections.count < connections { self.addConnectionToPool() @@ -461,6 +482,20 @@ internal final class ConnectionPool { internal func _startConnectingIdleConnection() { if let index = self._connections.values.firstIndex(where: { $0.manager.sync.isIdle }) { self._connections.values[index].manager.sync.startConnecting() + } else { + let connecting = self._connections.values.count { $0.manager.sync.isConnecting } + let ready = self._connections.values.count { $0.manager.sync.isReady } + let transientFailure = self._connections.values.count { $0.manager.sync.isTransientFailure } + + self.logger.debug( + "no idle connections in pool", + metadata: [ + Metadata.connectionsConnecting: .stringConvertible(connecting), + Metadata.connectionsReady: .stringConvertible(ready), + Metadata.connectionsTransientFailure: .stringConvertible(transientFailure), + Metadata.waitersCount: .stringConvertible(self.waiters.count), + ] + ) } } @@ -965,3 +1000,11 @@ extension GRPCConnectionPoolError: GRPCStatusTransformable { } } } + +extension Sequence { + fileprivate func count(where predicate: (Element) -> Bool) -> Int { + return self.reduce(0) { count, element in + predicate(element) ? count + 1 : count + } + } +} From 8d3341a4179650ee46bd6af637fa83ac703a3cfc Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 7 Feb 2024 15:59:52 +0000 Subject: [PATCH 239/580] Generated code fixtures: availability guards and descriptor paths (#1797) Motivation: The generated code must contain the availability guards on top of all protocols, extensions and typealiases for protocols in order to be compiled.The full descriptors path caused errors for namespaces and services with the same name, so we will use the relative paths to them in the descriptors array from the 'Method' enum. Modifications: - added the availability guards in the translators - modified the descriptors path in the typealias translator (for the descriptors array) - modified all tests accordingly Result: The generated code will be compiled. --- .../Translator/ClientCodeTranslator.swift | 10 ++- .../Translator/ServerCodeTranslator.swift | 37 +++++++---- .../Translator/TypealiasTranslator.swift | 41 ++++++++---- ...lientCodeTranslatorSnippetBasedTests.swift | 16 +++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 5 ++ ...erverCodeTranslatorSnippetBasedTests.swift | 24 +++++++ ...TypealiasTranslatorSnippetBasedTests.swift | 64 +++++++++++++++++-- .../ProtobufCodeGeneratorTests.swift | 24 ++++++- 8 files changed, 188 insertions(+), 33 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index a669c8f41..256b763db 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -22,6 +22,7 @@ /// a representation for the following generated code: /// /// ```swift +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarClientProtocol: Sendable { /// func baz( /// request: ClientRequest.Single, @@ -30,6 +31,7 @@ /// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R /// ) async throws -> ServerResponse.Stream /// } +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// extension Foo.Bar.ClientProtocol { /// public func get( /// request: ClientRequest.Single, @@ -42,6 +44,7 @@ /// body /// ) /// } +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public struct foo_BarClient: foo.Bar.ClientProtocol { /// private let client: GRPCCore.GRPCClient /// public init(client: GRPCCore.GRPCClient) { @@ -120,7 +123,7 @@ extension ClientCodeTranslator { members: methods ) ) - return clientProtocol + return .guarded(self.availabilityGuard, clientProtocol) } private func makeExtensionProtocol( @@ -142,7 +145,10 @@ extension ClientCodeTranslator { declarations: methods ) ) - return clientProtocolExtension + return .guarded( + self.availabilityGuard, + clientProtocolExtension + ) } private func makeClientProtocolMethod( diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index eebfd59e1..c25c56753 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -22,12 +22,14 @@ /// a representation for the following generated code: /// /// ```swift +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol foo_BarServiceStreamingProtocol: GRPCCore.RegistrableRPCService { /// func baz( /// request: ServerRequest.Stream /// ) async throws -> ServerResponse.Stream /// } /// // Generated conformance to `RegistrableRPCService`. +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// extension foo.Bar.StreamingServiceProtocol { /// public func registerRPCs(with router: inout RPCRouter) { /// router.registerHandler( @@ -38,12 +40,14 @@ /// ) /// } /// } +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol foo_BarServiceProtocol: foo.Bar.StreamingServiceProtocol { /// func baz( /// request: ServerRequest.Single /// ) async throws -> ServerResponse.Single /// } /// // Generated partial conformance to `foo_BarStreamingServiceProtocol`. +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// extension foo.Bar.ServiceProtocol { /// public func baz( /// request: ServerRequest.Stream @@ -168,9 +172,12 @@ extension ServerCodeTranslator { ) -> Declaration { let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) let registerRPCMethod = self.makeRegisterRPCsMethod(for: service, in: codeGenerationRequest) - return .extension( - onType: streamingProtocol, - declarations: [registerRPCMethod] + return .guarded( + self.availabilityGuard, + .extension( + onType: streamingProtocol, + declarations: [registerRPCMethod] + ) ) } @@ -297,12 +304,15 @@ extension ServerCodeTranslator { return .commentable( .preFormatted(service.documentation), - .protocol( - ProtocolDescription( - accessModifier: self.accessModifier, - name: protocolName, - conformances: [streamingProtocol], - members: methods + .guarded( + self.availabilityGuard, + .protocol( + ProtocolDescription( + accessModifier: self.accessModifier, + name: protocolName, + conformances: [streamingProtocol], + members: methods + ) ) ) ) @@ -363,9 +373,12 @@ extension ServerCodeTranslator { } let protocolName = self.protocolNameTypealias(service: service, streaming: false) - return .extension( - onType: protocolName, - declarations: methods + return .guarded( + self.availabilityGuard, + .extension( + onType: protocolName, + declarations: methods + ) ) } diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 3d09dd525..19d30de53 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -38,13 +38,15 @@ /// // ... /// /// public static let descriptors: [MethodDescriptor] = [ -/// Echo.Echo.Method.Get.descriptor, -/// Echo.Echo.Method.Collect.descriptor, +/// Get.descriptor, +/// Collect.descriptor, /// // ... /// ] /// } /// +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public typealias StreamingServiceProtocol = echo_EchoServiceStreamingProtocol +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public typealias ServiceProtocol = echo_EchoServiceProtocol /// /// } @@ -223,7 +225,7 @@ extension TypealiasTranslator { let methodDescriptorPath = Expression.memberAccess( MemberAccessDescription( left: .identifierType( - .member([service.namespacedTypealiasGeneratedName, "Method", methodName]) + .member([methodName]) ), right: "descriptor" ) @@ -255,26 +257,41 @@ extension TypealiasTranslator { existingType: .member("\(service.namespacedGeneratedName)ServiceProtocol") ) - return [streamingServiceProtocolTypealias, serviceProtocolTypealias] + return [ + .guarded( + self.availabilityGuard, + streamingServiceProtocolTypealias + ), + .guarded( + self.availabilityGuard, + serviceProtocolTypealias + ), + ] } private func makeClientProtocolTypealias( for service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { - return .typealias( - accessModifier: self.accessModifier, - name: "ClientProtocol", - existingType: .member("\(service.namespacedGeneratedName)ClientProtocol") + return .guarded( + self.availabilityGuard, + .typealias( + accessModifier: self.accessModifier, + name: "ClientProtocol", + existingType: .member("\(service.namespacedGeneratedName)ClientProtocol") + ) ) } private func makeClientStructTypealias( for service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { - return .typealias( - accessModifier: self.accessModifier, - name: "Client", - existingType: .member("\(service.namespacedGeneratedName)Client") + return .guarded( + self.availabilityGuard, + .typealias( + accessModifier: self.accessModifier, + name: "Client", + existingType: .member("\(service.namespacedGeneratedName)Client") + ) ) } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index a68bdd5da..086373e13 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -43,6 +43,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -52,6 +53,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Single, @@ -117,6 +119,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -126,6 +129,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Stream, @@ -191,6 +195,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -200,6 +205,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Single, @@ -265,6 +271,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -274,6 +281,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Stream, @@ -347,6 +355,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -364,6 +373,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ClientProtocol { package func methodA( request: ClientRequest.Stream, @@ -457,6 +467,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -466,6 +477,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceA.ClientProtocol { internal func methodA( request: ClientRequest.Single, @@ -537,7 +549,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable {} + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ClientProtocol { } /// Documentation for ServiceA @@ -552,7 +566,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceB /// /// Line 2 + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol ServiceBClientProtocol: Sendable {} + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceB.ClientProtocol { } /// Documentation for ServiceB diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 59016e642..224c0a909 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -175,7 +175,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } } @@ -185,15 +187,18 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for AService + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 731595fcc..416ed3ebc 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -57,6 +57,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -71,11 +72,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ServiceProtocol { public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unary(request: ServerRequest.Single(stream: request)) @@ -123,6 +126,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -137,11 +141,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ServiceProtocol { package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) @@ -193,6 +199,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -207,11 +214,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ServiceProtocol { public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) @@ -263,6 +272,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -277,11 +287,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ServiceProtocol { } """ @@ -344,6 +356,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -366,6 +379,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single @@ -374,6 +388,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ServiceProtocol { internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) @@ -422,6 +437,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -436,11 +452,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceA.ServiceProtocol { internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.methodA(request: ServerRequest.Single(stream: request)) @@ -483,26 +501,32 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceA + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceA.ServiceProtocol { } /// Documentation for ServiceB @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceB.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceB + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceBServiceProtocol: NamespaceA.ServiceB.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceBStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA.ServiceB.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index d74278d1d..ed0d5bcc8 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -58,12 +58,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - NamespaceA.ServiceA.Method.MethodA.descriptor + MethodA.descriptor ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = NamespaceA_ServiceAClient } } @@ -96,9 +100,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = NamespaceA_ServiceAClient } } @@ -131,7 +139,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } } @@ -164,7 +174,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = NamespaceA_ServiceAClient } } @@ -238,12 +250,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - ServiceA.Method.MethodA.descriptor + MethodA.descriptor ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = ServiceAClient } """ @@ -306,13 +322,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - NamespaceA.ServiceA.Method.MethodA.descriptor, - NamespaceA.ServiceA.Method.MethodB.descriptor + MethodA.descriptor, + MethodB.descriptor ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = NamespaceA_ServiceAClient } } @@ -345,9 +365,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias Client = NamespaceA_ServiceAClient } } @@ -392,18 +416,26 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = NamespaceA_AserviceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = NamespaceA_AserviceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = NamespaceA_AserviceClient } public enum Bservice { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = NamespaceA_BserviceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = NamespaceA_BserviceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = NamespaceA_BserviceClient } } @@ -439,18 +471,26 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias StreamingServiceProtocol = AServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ServiceProtocol = AServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ClientProtocol = AServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias Client = AServiceClient } package enum BService { package enum Method { package static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ServiceProtocol = BServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ClientProtocol = BServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias Client = BServiceClient } """ @@ -494,9 +534,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias ClientProtocol = Anamespace_AServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias Client = Anamespace_AServiceClient } } @@ -505,9 +549,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias StreamingServiceProtocol = Bnamespace_BServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias Client = Bnamespace_BServiceClient } } @@ -545,9 +593,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = BServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = BServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = BServiceClient } public enum Anamespace { @@ -555,9 +607,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ClientProtocol = Anamespace_AServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = Anamespace_AServiceClient } } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 8a77eae85..bca8e8aba 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -68,15 +68,18 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } internal static let descriptors: [MethodDescriptor] = [ - Helloworld.Greeter.Method.SayHello.descriptor + SayHello.descriptor ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias ClientProtocol = Helloworld_GreeterClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal typealias Client = Helloworld_GreeterClient } } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( @@ -87,6 +90,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Helloworld.Greeter.ClientProtocol { internal func sayHello( request: ClientRequest.Single, @@ -173,10 +177,12 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } public static let descriptors: [MethodDescriptor] = [ - Helloworld.Greeter.Method.SayHello.descriptor + SayHello.descriptor ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol } } @@ -189,6 +195,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Helloworld.Greeter.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -204,12 +211,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { /// Sends a greeting. func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Helloworld.Greeter.ServiceProtocol { public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) @@ -262,12 +271,16 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } package static let descriptors: [MethodDescriptor] = [ - Helloworld.Greeter.Method.SayHello.descriptor + SayHello.descriptor ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias ClientProtocol = Helloworld_GreeterClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package typealias Client = Helloworld_GreeterClient } } @@ -280,6 +293,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// Conformance to `GRPCCore.RegistrableRPCService`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Helloworld.Greeter.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -295,12 +309,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { /// Sends a greeting. func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Helloworld.Greeter.ServiceProtocol { package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) @@ -309,6 +325,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( @@ -319,6 +336,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) async throws -> R where R: Sendable } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Helloworld.Greeter.ClientProtocol { package func sayHello( request: ClientRequest.Single, From 5d0cf1c7b2e641e3a8961089e7e4672f4fe8abac Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 8 Feb 2024 11:07:22 +0000 Subject: [PATCH 240/580] Bump version number to 1.21.1 (#1798) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.21.1 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index b225e798d..ac8c68d7d 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 21 /// The patch version. - internal static let patch = 0 + internal static let patch = 1 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From a257af3795864b054eaede0e828f98cd396925a5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 9 Feb 2024 16:48:11 +0000 Subject: [PATCH 241/580] Log connection age when closing a connection (#1799) Motivation: It can be useful for diagnostic purposes to know how long a connection was alive for when it's closed. Modifications: - Add connection age metadata to logs when closing a connection Result: More insight into connection age --- Sources/GRPC/GRPCIdleHandler.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 0471526df..6371cb7a7 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -38,6 +38,17 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { /// The mode we're operating in. private let mode: Mode + /// The time the handler was created. + private let creationTime: NIODeadline + + /// Returns the age of the connection in seconds. + private var connectionAgeInSeconds: UInt64 { + let now = NIODeadline.now() + let nanoseconds = now.uptimeNanoseconds - self.creationTime.uptimeNanoseconds + let seconds = nanoseconds / 1_000_000_000 + return seconds + } + private var context: ChannelHandlerContext? /// The mode of operation: the client tracks additional connection state in the connection @@ -77,6 +88,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { maximumPingsWithoutData: configuration.maximumPingsWithoutData, minimumSentPingIntervalWithoutData: configuration.minimumSentPingIntervalWithoutData ) + self.creationTime = .now() } init( @@ -97,6 +109,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { minimumReceivedPingIntervalWithoutData: configuration.minimumReceivedPingIntervalWithoutData, maximumPingStrikes: configuration.maximumPingStrikes ) + self.creationTime = .now() } private func perform(operations: GRPCIdleHandlerStateMachine.Operations) { @@ -176,7 +189,10 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { // Close on the next event-loop tick so we don't drop any events which are // currently being processed. context.eventLoop.execute { - self.stateMachine.logger.debug("closing connection") + self.stateMachine.logger.debug( + "closing connection", + metadata: ["connection_age_secs": .stringConvertible(self.connectionAgeInSeconds)] + ) context.close(mode: .all, promise: nil) } } From 37a868b6de3a90156ed56ca261551cec5c1b7d6c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 12 Feb 2024 09:40:59 +0000 Subject: [PATCH 242/580] Update thresholds for main (#1802) --- ...ta_Iterate_binary_values_when_only_strings_stored.p90.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json index b59f05063..b4aba1c3f 100644 --- a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ b/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -1,7 +1,7 @@ { "mallocCountTotal" : 2000, "memoryLeaked" : 0, - "releaseCount" : 6001, - "retainCount" : 2000, + "releaseCount" : 7001, + "retainCount" : 3000, "syscalls" : 0 } From d8550a887c27c3ab4a774054238ce5db5158ac7c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 12 Feb 2024 09:45:12 +0000 Subject: [PATCH 243/580] Combine client and server error into runtime error (#1801) Motivation: Previously we had client error and server error to represent errors which could only be thrown by each peer. However, some errors can be thrown by both which resulted in the addition of runtime error. To reduce the number of errors which can be thrown we should merge client and server errors into the runtime error. Modifications: - Merge client and server error codes into runtime error - Remove client error and server error Results: Less code duplication, fewer error types --- Sources/GRPCCore/ClientError.swift | 142 ----------------- Sources/GRPCCore/GRPCClient.swift | 8 +- Sources/GRPCCore/GRPCServer.swift | 14 +- Sources/GRPCCore/RuntimeError.swift | 43 +++++ Sources/GRPCCore/ServerError.swift | 148 ------------------ Tests/GRPCCoreTests/GRPCClientTests.swift | 8 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 8 +- ...rorTests.swift => RuntimeErrorTests.swift} | 10 +- 8 files changed, 67 insertions(+), 314 deletions(-) delete mode 100644 Sources/GRPCCore/ClientError.swift delete mode 100644 Sources/GRPCCore/ServerError.swift rename Tests/GRPCCoreTests/{ServerErrorTests.swift => RuntimeErrorTests.swift} (81%) diff --git a/Sources/GRPCCore/ClientError.swift b/Sources/GRPCCore/ClientError.swift deleted file mode 100644 index ddf4937dc..000000000 --- a/Sources/GRPCCore/ClientError.swift +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -/// A runtime error thrown by the client. -/// -/// In contrast to ``RPCError``, the ``ClientError`` represents errors which happen at a scope -/// wider than an individual RPC. For example, attempting to start a client which is already -/// stopped would result in a ``ClientError``. -public struct ClientError: Error, Hashable, @unchecked Sendable { - private var storage: Storage - - // Ensures the underlying storage is unique. - private mutating func ensureUniqueStorage() { - if !isKnownUniquelyReferenced(&self.storage) { - self.storage = self.storage.copy() - } - } - - /// The code indicating the domain of the error. - public var code: Code { - get { self.storage.code } - set { - self.ensureUniqueStorage() - self.storage.code = newValue - } - } - - /// A message providing more details about the error which may include details specific to this - /// instance of the error. - public var message: String { - get { self.storage.message } - set { - self.ensureUniqueStorage() - self.storage.message = newValue - } - } - - /// The original error which led to this error being thrown. - public var cause: Error? { - get { self.storage.cause } - set { - self.ensureUniqueStorage() - self.storage.cause = newValue - } - } - - /// Creates a new error. - /// - /// - Parameters: - /// - code: The error code. - /// - message: A description of the error. - /// - cause: The original error which led to this error being thrown. - public init(code: Code, message: String, cause: Error? = nil) { - self.storage = Storage(code: code, message: message, cause: cause) - } -} - -extension ClientError: CustomStringConvertible { - public var description: String { - if let cause = self.cause { - return "\(self.code): \"\(self.message)\" (cause: \"\(cause)\")" - } else { - return "\(self.code): \"\(self.message)\"" - } - } -} - -extension ClientError { - private final class Storage: Hashable { - var code: Code - var message: String - var cause: Error? - - init(code: Code, message: String, cause: Error?) { - self.code = code - self.message = message - self.cause = cause - } - - func copy() -> Storage { - return Storage(code: self.code, message: self.message, cause: self.cause) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.code) - hasher.combine(self.message) - } - - static func == (lhs: Storage, rhs: Storage) -> Bool { - return lhs.code == rhs.code && lhs.message == rhs.message - } - } -} - -extension ClientError { - public struct Code: Hashable, Sendable { - private enum Value { - case clientIsAlreadyRunning - case clientIsStopped - case transportError - } - - private var value: Value - private init(_ value: Value) { - self.value = value - } - - /// At attempt to start the client was made but it is already running. - public static var clientIsAlreadyRunning: Self { - Self(.clientIsAlreadyRunning) - } - - /// At attempt to start the client was made but it has already stopped. - public static var clientIsStopped: Self { - Self(.clientIsStopped) - } - - /// The transport threw an error whilst connected. - public static var transportError: Self { - Self(.transportError) - } - } -} - -extension ClientError.Code: CustomStringConvertible { - public var description: String { - String(describing: self.value) - } -} diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 888e893c6..490fa4ea1 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -196,12 +196,12 @@ public struct GRPCClient: Sendable { // The value wasn't exchanged so the original value can't be 'notStarted'. fatalError() case .running: - throw ClientError( + throw RuntimeError( code: .clientIsAlreadyRunning, message: "The client is already running and can only be started once." ) case .stopping, .stopped: - throw ClientError( + throw RuntimeError( code: .clientIsStopped, message: "The client has stopped and can only be started once." ) @@ -216,7 +216,7 @@ public struct GRPCClient: Sendable { do { try await self.transport.connect(lazily: false) } catch { - throw ClientError( + throw RuntimeError( code: .transportError, message: "The transport threw an error while connected.", cause: error @@ -385,7 +385,7 @@ public struct GRPCClient: Sendable { // queuing the request if not yet started. () case .stopping, .stopped: - throw ClientError( + throw RuntimeError( code: .clientIsStopped, message: "Client has been stopped. Can't make any more RPCs." ) diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 7f953fb6e..800759c59 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -55,7 +55,7 @@ import Atomics /// ## Starting and stopping the server /// /// Once you have configured the server call ``run()`` to start it. Calling ``run()`` starts each -/// of the server's transports. A ``ServerError`` is thrown if any of the transports can't be +/// of the server's transports. A ``RuntimeError`` is thrown if any of the transports can't be /// started. /// /// ```swift @@ -154,7 +154,7 @@ public struct GRPCServer: Sendable { /// Starts the server and runs until all registered transports have closed. /// /// No RPCs are processed until all transports are listening. If a transport fails to start - /// listening then all open transports are closed and a ``ServerError`` is thrown. + /// listening then all open transports are closed and a ``RuntimeError`` is thrown. /// /// This function returns when all transports have stopped listening and all requests have been /// handled. You can signal to transports that they should stop listening by calling @@ -163,7 +163,7 @@ public struct GRPCServer: Sendable { /// To stop the server more abruptly you can cancel the task that this function is running in. /// /// - Note: You can only call this function once, repeated calls will result in a - /// ``ServerError`` being thrown. + /// ``RuntimeError`` being thrown. public func run() async throws { let (wasNotStarted, actualState) = self.state.compareExchange( expected: .notStarted, @@ -176,13 +176,13 @@ public struct GRPCServer: Sendable { case .notStarted: fatalError() case .starting, .running: - throw ServerError( + throw RuntimeError( code: .serverIsAlreadyRunning, message: "The server is already running and can only be started once." ) case .stopping, .stopped: - throw ServerError( + throw RuntimeError( code: .serverIsStopped, message: "The server has stopped and can only be started once." ) @@ -195,7 +195,7 @@ public struct GRPCServer: Sendable { } if self.transports.isEmpty { - throw ServerError( + throw RuntimeError( code: .noTransportsConfigured, message: """ Can't start server, no transports are configured. You must add at least one transport \ @@ -217,7 +217,7 @@ public struct GRPCServer: Sendable { // Some listeners may have started and have streams which need closing. await self.rejectRequests(listeners) - throw ServerError( + throw RuntimeError( code: .failedToStartTransport, message: """ Server didn't start because the '\(type(of: transport))' transport threw an error \ diff --git a/Sources/GRPCCore/RuntimeError.swift b/Sources/GRPCCore/RuntimeError.swift index 74b0e7b7a..2af8dc02a 100644 --- a/Sources/GRPCCore/RuntimeError.swift +++ b/Sources/GRPCCore/RuntimeError.swift @@ -108,6 +108,13 @@ extension RuntimeError { public struct Code: Hashable, Sendable { private enum Value { case invalidArgument + case serverIsAlreadyRunning + case serverIsStopped + case failedToStartTransport + case noTransportsConfigured + case clientIsAlreadyRunning + case clientIsStopped + case transportError } private var value: Value @@ -115,9 +122,45 @@ extension RuntimeError { self.value = value } + /// An argument was invalid. public static var invalidArgument: Self { Self(.invalidArgument) } + + /// At attempt to start the server was made but it is already running. + public static var serverIsAlreadyRunning: Self { + Self(.serverIsAlreadyRunning) + } + + /// At attempt to start the server was made but it has already stopped. + public static var serverIsStopped: Self { + Self(.serverIsStopped) + } + + /// The server couldn't be started because a transport failed to start. + public static var failedToStartTransport: Self { + Self(.failedToStartTransport) + } + + /// The server couldn't be started because no transports were configured. + public static var noTransportsConfigured: Self { + Self(.noTransportsConfigured) + } + + /// At attempt to start the client was made but it is already running. + public static var clientIsAlreadyRunning: Self { + Self(.clientIsAlreadyRunning) + } + + /// At attempt to start the client was made but it has already stopped. + public static var clientIsStopped: Self { + Self(.clientIsStopped) + } + + /// The transport threw an error whilst connected. + public static var transportError: Self { + Self(.transportError) + } } } diff --git a/Sources/GRPCCore/ServerError.swift b/Sources/GRPCCore/ServerError.swift deleted file mode 100644 index 45e4f4e95..000000000 --- a/Sources/GRPCCore/ServerError.swift +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -/// A runtime error thrown by the server. -/// -/// In contrast to ``RPCError``, the ``ServerError`` represents errors which happen at a scope -/// wider than an individual RPC. For example, attempting to start a server which is already -/// stopped would result in a ``ServerError``. -public struct ServerError: Error, Hashable, @unchecked Sendable { - private var storage: Storage - - // Ensures the underlying storage is unique. - private mutating func ensureUniqueStorage() { - if !isKnownUniquelyReferenced(&self.storage) { - self.storage = self.storage.copy() - } - } - - /// The code indicating the domain of the error. - public var code: Code { - get { self.storage.code } - set { - self.ensureUniqueStorage() - self.storage.code = newValue - } - } - - /// A message providing more details about the error which may include details specific to this - /// instance of the error. - public var message: String { - get { self.storage.message } - set { - self.ensureUniqueStorage() - self.storage.message = newValue - } - } - - /// The original error which led to this error being thrown. - public var cause: Error? { - get { self.storage.cause } - set { - self.ensureUniqueStorage() - self.storage.cause = newValue - } - } - - /// Creates a new error. - /// - /// - Parameters: - /// - code: The error code. - /// - message: A description of the error. - /// - cause: The original error which led to this error being thrown. - public init(code: Code, message: String, cause: Error? = nil) { - self.storage = Storage(code: code, message: message, cause: cause) - } -} - -extension ServerError: CustomStringConvertible { - public var description: String { - if let cause = self.cause { - return "\(self.code): \"\(self.message)\" (cause: \"\(cause)\")" - } else { - return "\(self.code): \"\(self.message)\"" - } - } -} - -extension ServerError { - private final class Storage: Hashable { - var code: Code - var message: String - var cause: Error? - - init(code: Code, message: String, cause: Error?) { - self.code = code - self.message = message - self.cause = cause - } - - func copy() -> Storage { - return Storage(code: self.code, message: self.message, cause: self.cause) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.code) - hasher.combine(self.message) - } - - static func == (lhs: Storage, rhs: Storage) -> Bool { - return lhs.code == rhs.code && lhs.message == rhs.message - } - } -} - -extension ServerError { - public struct Code: Hashable, Sendable { - private enum Value { - case serverIsAlreadyRunning - case serverIsStopped - case failedToStartTransport - case noTransportsConfigured - } - - private var value: Value - private init(_ value: Value) { - self.value = value - } - - /// At attempt to start the server was made but it is already running. - public static var serverIsAlreadyRunning: Self { - Self(.serverIsAlreadyRunning) - } - - /// At attempt to start the server was made but it has already stopped. - public static var serverIsStopped: Self { - Self(.serverIsStopped) - } - - /// The server couldn't be started because a transport failed to start. - public static var failedToStartTransport: Self { - Self(.failedToStartTransport) - } - - /// The server couldn't be started because no transports were configured. - public static var noTransportsConfigured: Self { - Self(.noTransportsConfigured) - } - } -} - -extension ServerError.Code: CustomStringConvertible { - public var description: String { - String(describing: self.value) - } -} diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index b3149b62b..42f238ce0 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -265,7 +265,7 @@ final class GRPCClientTests: XCTestCase { client.close() // RPC should fail now. - await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await client.unary( request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, @@ -287,7 +287,7 @@ final class GRPCClientTests: XCTestCase { client.close() // Attempts to start a new RPC should fail. - await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await client.unary( request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, @@ -372,7 +372,7 @@ final class GRPCClientTests: XCTestCase { try await task.value // Client is stopped, should throw an error. - await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await client.run() } errorHandler: { error in XCTAssertEqual(error.code, .clientIsStopped) @@ -388,7 +388,7 @@ final class GRPCClientTests: XCTestCase { try await Task.sleep(for: .milliseconds(10)) // Client is already running, should throw an error. - await XCTAssertThrowsErrorAsync(ofType: ClientError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await client.run() } errorHandler: { error in XCTAssertEqual(error.code, .clientIsAlreadyRunning) diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 936461a2c..96824eff7 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -313,7 +313,7 @@ final class GRPCServerTests: XCTestCase { func testTestRunServerWithNoTransport() async throws { let server = GRPCServer(transports: [], services: []) - await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await server.run() } errorHandler: { error in XCTAssertEqual(error.code, .noTransportsConfigured) @@ -328,7 +328,7 @@ final class GRPCServerTests: XCTestCase { try await task.value // Server is stopped, should throw an error. - await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await server.run() } errorHandler: { error in XCTAssertEqual(error.code, .serverIsStopped) @@ -337,7 +337,7 @@ final class GRPCServerTests: XCTestCase { func testRunServerWhenTransportThrows() async throws { let server = GRPCServer(transports: [ThrowOnRunServerTransport()], services: []) - await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await server.run() } errorHandler: { error in XCTAssertEqual(error.code, .failedToStartTransport) @@ -379,7 +379,7 @@ final class GRPCServerTests: XCTestCase { } } - await XCTAssertThrowsErrorAsync(ofType: ServerError.self) { + await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await server.run() } errorHandler: { error in XCTAssertEqual(error.code, .failedToStartTransport) diff --git a/Tests/GRPCCoreTests/ServerErrorTests.swift b/Tests/GRPCCoreTests/RuntimeErrorTests.swift similarity index 81% rename from Tests/GRPCCoreTests/ServerErrorTests.swift rename to Tests/GRPCCoreTests/RuntimeErrorTests.swift index 4413fe038..14cc56f4c 100644 --- a/Tests/GRPCCoreTests/ServerErrorTests.swift +++ b/Tests/GRPCCoreTests/RuntimeErrorTests.swift @@ -17,10 +17,10 @@ import GRPCCore import XCTest @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -final class ServerErrorTests: XCTestCase { +final class RuntimeErrorTests: XCTestCase { func testCopyOnWrite() { - // ServerError has a heap based storage, so check CoW semantics are correctly implemented. - let error1 = ServerError(code: .failedToStartTransport, message: "Failed to start transport") + // RuntimeError has a heap based storage, so check CoW semantics are correctly implemented. + let error1 = RuntimeError(code: .failedToStartTransport, message: "Failed to start transport") var error2 = error1 error2.code = .serverIsAlreadyRunning XCTAssertEqual(error1.code, .failedToStartTransport) @@ -38,10 +38,10 @@ final class ServerErrorTests: XCTestCase { } func testCustomStringConvertible() { - let error1 = ServerError(code: .failedToStartTransport, message: "Failed to start transport") + let error1 = RuntimeError(code: .failedToStartTransport, message: "Failed to start transport") XCTAssertDescription(error1, #"failedToStartTransport: "Failed to start transport""#) - let error2 = ServerError( + let error2 = RuntimeError( code: .failedToStartTransport, message: "Failed to start transport", cause: CancellationError() From 130605f286ac8894bf47c50ff56d8da002997b7b Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:56:12 +0000 Subject: [PATCH 244/580] Fixed .proto file imports handling in ProtobufCodeGenParser (#1804) Motivation: The .proto file imports from a .proto file were added as file imports in the generated code. The correct behavior is to add dependencies on the modules containing generated code and types associated with the .proto files, if they are generated in a different module from the one of the .proto file we are parsing. Modifications: - Created a method that adds only the necessary imports associated with a .proto file into the CodeGenerationRequest object - updated the test for the parser Result: Imports will be generated correctly for '.proto' files. --- .../ProtobufCodeGenParser.swift | 36 ++++++++++++--- .../ProtobufCodeGenerator.swift | 6 ++- Sources/protoc-gen-grpc-swift/main.swift | 3 +- .../ProtobufCodeGenParserTests.swift | 44 ++++++++++++++++--- .../ProtobufCodeGeneratorTests.swift | 28 +++++++++++- 5 files changed, 99 insertions(+), 18 deletions(-) diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index 689aa9a43..8fb8037e9 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -24,9 +24,17 @@ import struct GRPCCodeGen.CodeGenerationRequest internal struct ProtobufCodeGenParser { let input: FileDescriptor let namer: SwiftProtobufNamer + let extraModuleImports: [String] + let protoToModuleMappings: ProtoFileToModuleMappings - internal init(input: FileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings) { + internal init( + input: FileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings, + extraModuleImports: [String] + ) { self.input = input + self.extraModuleImports = extraModuleImports + self.protoToModuleMappings = protoFileModuleMappings self.namer = SwiftProtobufNamer( currentFile: input, protoFileToModuleMappings: protoFileModuleMappings @@ -50,10 +58,6 @@ internal struct ProtobufCodeGenParser { // https://github.com/grpc/grpc-swift """ - var dependencies = self.input.dependencies.map { - CodeGenerationRequest.Dependency(module: $0.name) - } - dependencies.append(CodeGenerationRequest.Dependency(module: "GRPCProtobuf")) let lookupSerializer: (String) -> String = { messageType in "ProtobufSerializer<\(messageType)>()" } @@ -71,7 +75,7 @@ internal struct ProtobufCodeGenParser { return CodeGenerationRequest( fileName: self.input.name, leadingTrivia: header + leadingTrivia, - dependencies: dependencies, + dependencies: self.codeDependencies, services: services, lookupSerializer: lookupSerializer, lookupDeserializer: lookupDeserializer @@ -79,6 +83,26 @@ internal struct ProtobufCodeGenParser { } } +extension ProtobufCodeGenParser { + fileprivate var codeDependencies: [CodeGenerationRequest.Dependency] { + var codeDependencies: [CodeGenerationRequest.Dependency] = [.init(module: "GRPCProtobuf")] + // Adding as dependencies the modules containing generated code or types for + // '.proto' files imported in the '.proto' file we are parsing. + codeDependencies.append( + contentsOf: (self.protoToModuleMappings.neededModules(forFile: self.input) ?? []).map { + CodeGenerationRequest.Dependency(module: $0) + } + ) + // Adding extra imports passed in as an option to the plugin. + codeDependencies.append( + contentsOf: self.extraModuleImports.sorted().map { + CodeGenerationRequest.Dependency(module: $0) + } + ) + return codeDependencies + } +} + extension CodeGenerationRequest.ServiceDescriptor { fileprivate init( descriptor: ServiceDescriptor, diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index 798578705..adde3ddf8 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -28,11 +28,13 @@ public struct ProtobufCodeGenerator { public func generateCode( from fileDescriptor: FileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings + protoFileModuleMappings: ProtoFileToModuleMappings, + extraModuleImports: [String] ) throws -> String { let parser = ProtobufCodeGenParser( input: fileDescriptor, - protoFileModuleMappings: protoFileModuleMappings + protoFileModuleMappings: protoFileModuleMappings, + extraModuleImports: extraModuleImports ) let sourceGenerator = SourceGenerator(configuration: self.configuration) diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 57cda5f8e..07572316f 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -174,7 +174,8 @@ func main(args: [String]) throws { ) grpcFile.content = try grpcGenerator.generateCode( from: fileDescriptor, - protoFileModuleMappings: options.protoToModuleMappings + protoFileModuleMappings: options.protoToModuleMappings, + extraModuleImports: options.extraModuleImports ) } else { let grpcGenerator = Generator(fileDescriptor, options: options) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index f7ac2cf7d..8d6cf3891 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -23,7 +23,20 @@ import XCTest final class ProtobufCodeGenParserTests: XCTestCase { func testParser() throws { - let descriptorSet = DescriptorSet(protos: [Google_Protobuf_FileDescriptorProto.helloWorld]) + let descriptorSet = DescriptorSet( + protos: [ + Google_Protobuf_FileDescriptorProto( + name: "same-module.proto", + package: "same-package" + ), + Google_Protobuf_FileDescriptorProto( + name: "different-module.proto", + package: "different-package" + ), + Google_Protobuf_FileDescriptorProto.helloWorld, + ] + ) + guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { return XCTFail( """ @@ -31,9 +44,18 @@ final class ProtobufCodeGenParserTests: XCTestCase { """ ) } + let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { + $0.mapping = [ + SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { + $0.protoFilePath = ["different-module.proto"] + $0.moduleName = "DifferentModule" + } + ] + } let parsedCodeGenRequest = try ProtobufCodeGenParser( input: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings() + protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), + extraModuleImports: ["ExtraModule"] ).parse() XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") XCTAssertEqual( @@ -65,6 +87,11 @@ final class ProtobufCodeGenParserTests: XCTestCase { """ ) + XCTAssertEqual(parsedCodeGenRequest.dependencies.count, 3) + let expectedDependencyNames = ["GRPCProtobuf", "DifferentModule", "ExtraModule"] + let parsedDependencyNames = parsedCodeGenRequest.dependencies.map { $0.module } + XCTAssertEqual(parsedDependencyNames, expectedDependencyNames) + XCTAssertEqual(parsedCodeGenRequest.services.count, 1) let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( @@ -108,11 +135,6 @@ final class ProtobufCodeGenParserTests: XCTestCase { parsedCodeGenRequest.lookupDeserializer("HelloRequest"), "ProtobufDeserializer()" ) - XCTAssertEqual(parsedCodeGenRequest.dependencies.count, 1) - XCTAssertEqual( - parsedCodeGenRequest.dependencies[0], - CodeGenerationRequest.Dependency(module: "GRPCProtobuf") - ) } } @@ -158,6 +180,8 @@ extension Google_Protobuf_FileDescriptorProto { return Google_Protobuf_FileDescriptorProto.with { $0.name = "helloworld.proto" $0.package = "helloworld" + $0.dependency = ["same-module.proto", "different-module.proto"] + $0.publicDependency = [1, 2] $0.messageType = [requestType, responseType] $0.service = [service] $0.sourceCodeInfo = Google_Protobuf_SourceCodeInfo.with { @@ -199,4 +223,10 @@ extension Google_Protobuf_FileDescriptorProto { $0.syntax = "proto3" } } + + internal init(name: String, package: String) { + self.init() + self.name = name + self.package = package + } } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index bca8e8aba..d2a97b59c 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -55,6 +55,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import GRPCCore import GRPCProtobuf + import DifferentModule + import ExtraModule internal enum Helloworld { internal enum Greeter { @@ -164,6 +166,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import GRPCCore import GRPCProtobuf + import DifferentModule + import ExtraModule public enum Helloworld { public enum Greeter { @@ -258,6 +262,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import GRPCCore import GRPCProtobuf + import DifferentModule + import ExtraModule package enum Helloworld { package enum Greeter { @@ -393,7 +399,15 @@ final class ProtobufCodeGeneratorTests: XCTestCase { server: server, indentation: indentation ) - let descriptorSet = DescriptorSet(protos: [Google_Protobuf_FileDescriptorProto.helloWorld]) + let descriptorSet = DescriptorSet( + protos: [ + Google_Protobuf_FileDescriptorProto(name: "same-module.proto", package: "same-package"), + Google_Protobuf_FileDescriptorProto( + name: "different-module.proto", + package: "different-package" + ), + Google_Protobuf_FileDescriptorProto.helloWorld, + ]) guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { return XCTFail( """ @@ -401,11 +415,21 @@ final class ProtobufCodeGeneratorTests: XCTestCase { """ ) } + + let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { + $0.mapping = [ + SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { + $0.protoFilePath = ["different-module.proto"] + $0.moduleName = "DifferentModule" + } + ] + } let generator = ProtobufCodeGenerator(configuration: configs) try XCTAssertEqualWithDiff( try generator.generateCode( from: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings() + protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), + extraModuleImports: ["ExtraModule"] ), expectedCode ) From e05326be1ccf5bb7fd30de76da7fc847bd58a8cf Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 13 Feb 2024 15:27:52 +0000 Subject: [PATCH 245/580] Add a socket address type (#1803) Motivation: Name resolvers resolve targets to an address we know how to connect to. This could be represented as an IPv4, IPv6, UDS or VSOCK address which can all be wrapped up in a broader 'SocketAddress'. Modifications: - Add a SocketAddress type and tests Results: Can represent an address to connect to --- .../Client/Resolver/SocketAddress.swift | 313 ++++++++++++++++++ .../Client/Resolver/SocketAddressTests.swift | 80 +++++ .../Test Utilities/XCTest+Utilities.swift | 9 + 3 files changed, 402 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift b/Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift new file mode 100644 index 000000000..8f5d961b1 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift @@ -0,0 +1,313 @@ +/* + * 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. + */ + +/// An address to which a socket may connect or bind. +public struct SocketAddress: Hashable, Sendable { + private enum Value: Hashable, Sendable { + case ipv4(IPv4) + case ipv6(IPv6) + case unix(UnixDomainSocket) + case vsock(VirtualSocket) + } + + private var value: Value + private init(_ value: Value) { + self.value = value + } + + /// Returns the address as an IPv4 address, if possible. + public var ipv4: IPv4? { + switch self.value { + case .ipv4(let address): + return address + default: + return nil + } + } + + /// Returns the address as an IPv6 address, if possible. + public var ipv6: IPv6? { + switch self.value { + case .ipv6(let address): + return address + default: + return nil + } + } + + /// Returns the address as an Unix domain socket address, if possible. + public var unixDomainSocket: UnixDomainSocket? { + switch self.value { + case .unix(let address): + return address + default: + return nil + } + } + + /// Returns the address as an VSOCK address, if possible. + public var virtualSocket: VirtualSocket? { + switch self.value { + case .vsock(let address): + return address + default: + return nil + } + } +} + +extension SocketAddress { + /// Creates a socket address by wrapping a ``SocketAddress/IPv4-swift.struct``. + public static func ipv4(_ address: IPv4) -> Self { + return Self(.ipv4(address)) + } + + /// Creates a socket address by wrapping a ``SocketAddress/IPv6-swift.struct``. + public static func ipv6(_ address: IPv6) -> Self { + return Self(.ipv6(address)) + } + + /// Creates a socket address by wrapping a ``SocketAddress/UnixDomainSocket-swift.struct``. + public static func unixDomainSocket(_ address: UnixDomainSocket) -> Self { + return Self(.unix(address)) + } + + /// Creates a socket address by wrapping a ``SocketAddress/VirtualSocket-swift.struct``. + public static func vsock(_ address: VirtualSocket) -> Self { + return Self(.vsock(address)) + } +} + +extension SocketAddress { + /// Creates an IPv4 socket address. + public static func ipv4(host: String, port: Int) -> Self { + return .ipv4(IPv4(host: host, port: port)) + } + + /// Creates an IPv6 socket address. + public static func ipv6(host: String, port: Int) -> Self { + return .ipv6(IPv6(host: host, port: port)) + } + /// Creates a Unix socket address. + public static func unixDomainSocket(path: String) -> Self { + return .unixDomainSocket(UnixDomainSocket(path: path)) + } + + /// Create a Virtual Socket ('vsock') address. + public static func vsock(contextID: VirtualSocket.ContextID, port: VirtualSocket.Port) -> Self { + return .vsock(VirtualSocket(contextID: contextID, port: port)) + } +} + +extension SocketAddress: CustomStringConvertible { + public var description: String { + switch self.value { + case .ipv4(let address): + return String(describing: address) + case .ipv6(let address): + return String(describing: address) + case .unix(let address): + return String(describing: address) + case .vsock(let address): + return String(describing: address) + } + } +} + +extension SocketAddress { + public struct IPv4: Hashable, Sendable { + /// The resolved host address. + public var host: String + /// The port to connect to. + public var port: Int + + /// Creates a new IPv4 address. + /// + /// - Parameters: + /// - host: Resolved host address. + /// - port: Port to connect to. + public init(host: String, port: Int) { + self.host = host + self.port = port + } + } + + public struct IPv6: Hashable, Sendable { + /// The resolved host address. + public var host: String + /// The port to connect to. + public var port: Int + + /// Creates a new IPv6 address. + /// + /// - Parameters: + /// - host: Resolved host address. + /// - port: Port to connect to. + public init(host: String, port: Int) { + self.host = host + self.port = port + } + } + + public struct UnixDomainSocket: Hashable, Sendable { + /// The path name of the Unix domain socket. + public var path: String + + /// Create a new Unix domain socket address. + /// + /// - Parameter path: The path name of the Unix domain socket. + public init(path: String) { + self.path = path + } + } + + public struct VirtualSocket: Hashable, Sendable { + /// A context identifier. + /// + /// Indicates the source or destination which is either a virtual machine or the host. + public var contextID: ContextID + + /// The port number. + public var port: Port + + /// Create a new VSOCK address. + /// + /// - Parameters: + /// - contextID: The context ID (or 'cid') of the address. + /// - port: The port number. + public init(contextID: ContextID, port: Port) { + self.contextID = contextID + self.port = port + } + + public struct Port: Hashable, Sendable, RawRepresentable, ExpressibleByIntegerLiteral { + /// The port number. + public var rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public init(integerLiteral value: UInt32) { + self.rawValue = value + } + + public init(_ value: Int) { + self.init(rawValue: UInt32(bitPattern: Int32(truncatingIfNeeded: value))) + } + + /// Used to bind to any port number. + /// + /// This is equal to `VMADDR_PORT_ANY (-1U)`. + public static var any: Self { + Self(rawValue: UInt32(bitPattern: -1)) + } + } + + public struct ContextID: Hashable, Sendable, RawRepresentable, ExpressibleByIntegerLiteral { + /// The context identifier. + public var rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + public init(integerLiteral value: UInt32) { + self.rawValue = value + } + + public init(_ value: Int) { + self.rawValue = UInt32(bitPattern: Int32(truncatingIfNeeded: value)) + } + + /// Wildcard, matches any address. + /// + /// On all platforms, using this value with `bind(2)` means "any address". + /// + /// On Darwin platforms, the man page states this can be used with `connect(2)` + /// to mean "this host". + /// + /// This is equal to `VMADDR_CID_ANY (-1U)`. + public static var any: Self { + Self(rawValue: UInt32(bitPattern: -1)) + } + + /// The address of the hypervisor. + /// + /// This is equal to `VMADDR_CID_HYPERVISOR (0)`. + public static var hypervisor: Self { + Self(rawValue: 0) + } + + /// The address of the host. + /// + /// This is equal to `VMADDR_CID_HOST (2)`. + public static var host: Self { + Self(rawValue: 2) + } + + /// The address for local communication (loopback). + /// + /// This directs packets to the same host that generated them. This is useful for testing + /// applications on a single host and for debugging. + /// + /// This is equal to `VMADDR_CID_LOCAL (1)` on platforms that define it. + /// + /// - Warning: `VMADDR_CID_LOCAL (1)` is available from Linux 5.6. Its use is unsupported on + /// other platforms. + /// - SeeAlso: https://man7.org/linux/man-pages/man7/vsock.7.html + public static var local: Self { + Self(rawValue: 1) + } + } + } +} + +extension SocketAddress.IPv4: CustomStringConvertible { + public var description: String { + "[ipv4]\(self.host):\(self.port)" + } +} + +extension SocketAddress.IPv6: CustomStringConvertible { + public var description: String { + "[ipv6]\(self.host):\(self.port)" + } +} + +extension SocketAddress.UnixDomainSocket: CustomStringConvertible { + public var description: String { + "[unix]\(self.path)" + } +} + +extension SocketAddress.VirtualSocket: CustomStringConvertible { + public var description: String { + "[vsock]\(self.contextID):\(self.port)" + } +} + +extension SocketAddress.VirtualSocket.ContextID: CustomStringConvertible { + public var description: String { + self == .any ? "-1" : String(describing: self.rawValue) + } +} + +extension SocketAddress.VirtualSocket.Port: CustomStringConvertible { + public var description: String { + self == .any ? "-1" : String(describing: self.rawValue) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift new file mode 100644 index 000000000..ab081a631 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift @@ -0,0 +1,80 @@ +/* + * 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 GRPCHTTP2Core +import XCTest + +final class SocketAddressTests: XCTestCase { + func testSocketAddressUnwrapping() { + var address: SocketAddress = .ipv4(host: "foo", port: 42) + XCTAssertEqual(address.ipv4, SocketAddress.IPv4(host: "foo", port: 42)) + XCTAssertNil(address.ipv6) + XCTAssertNil(address.unixDomainSocket) + XCTAssertNil(address.virtualSocket) + + address = .ipv6(host: "bar", port: 42) + XCTAssertEqual(address.ipv6, SocketAddress.IPv6(host: "bar", port: 42)) + XCTAssertNil(address.ipv4) + XCTAssertNil(address.unixDomainSocket) + XCTAssertNil(address.virtualSocket) + + address = .unixDomainSocket(path: "baz") + XCTAssertEqual(address.unixDomainSocket, SocketAddress.UnixDomainSocket(path: "baz")) + XCTAssertNil(address.ipv4) + XCTAssertNil(address.ipv6) + XCTAssertNil(address.virtualSocket) + + address = .vsock(contextID: .any, port: .any) + XCTAssertEqual(address.virtualSocket, SocketAddress.VirtualSocket(contextID: .any, port: .any)) + XCTAssertNil(address.ipv4) + XCTAssertNil(address.ipv6) + XCTAssertNil(address.unixDomainSocket) + } + + func testSocketAddressDescription() { + var address: SocketAddress = .ipv4(host: "127.0.0.1", port: 42) + XCTAssertDescription(address, "[ipv4]127.0.0.1:42") + + address = .ipv6(host: "::1", port: 42) + XCTAssertDescription(address, "[ipv6]::1:42") + + address = .unixDomainSocket(path: "baz") + XCTAssertDescription(address, "[unix]baz") + + address = .vsock(contextID: 314, port: 159) + XCTAssertDescription(address, "[vsock]314:159") + address = .vsock(contextID: .any, port: .any) + XCTAssertDescription(address, "[vsock]-1:-1") + + } + + func testSocketAddressSubTypesDescription() { + let ipv4 = SocketAddress.IPv4(host: "127.0.0.1", port: 42) + XCTAssertDescription(ipv4, "[ipv4]127.0.0.1:42") + + let ipv6 = SocketAddress.IPv6(host: "foo", port: 42) + XCTAssertDescription(ipv6, "[ipv6]foo:42") + + let uds = SocketAddress.UnixDomainSocket(path: "baz") + XCTAssertDescription(uds, "[unix]baz") + + var vsock = SocketAddress.VirtualSocket(contextID: 314, port: 159) + XCTAssertDescription(vsock, "[vsock]314:159") + vsock.contextID = .any + vsock.port = .any + XCTAssertDescription(vsock, "[vsock]-1:-1") + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift index 4cef512d0..752f12a9a 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift @@ -28,3 +28,12 @@ func XCTAssertThrowsError( errorHandler(error) } } + +func XCTAssertDescription( + _ subject: some CustomStringConvertible, + _ expected: String, + file: StaticString = #filePath, + line: UInt = #line +) { + XCTAssertEqual(String(describing: subject), expected, file: file, line: line) +} From 60d8762d0d301da959f83945b8b9b634f056b968 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:58:40 +0000 Subject: [PATCH 246/580] [CodeGenLib] Remove namespace enum generation (#1808) Motivation: Having a namespace enum in the generated code leads to redeclaration of the package enum when we generate code for .proto files within the same package. Thus, we cannot generate the namespace enums anymore, so we need to generate service enums that contain the namespace in their names. Modifications: - Changed the typealias translator to generate the service enums with namespace prefixed names - Deleted the namespace enum generation - Modified the CodeGenerationRequest validation function for service names - Updated tests accordingly Result: Redeclaration of the same enum will be avoided when generating code using CodeGenLib. Co-authored-by: George Barnett --- .../Translator/ClientCodeTranslator.swift | 8 +- .../IDLToStructuredSwiftTranslator.swift | 72 +--- .../Translator/ServerCodeTranslator.swift | 8 +- .../Translator/TypealiasTranslator.swift | 52 +-- ...lientCodeTranslatorSnippetBasedTests.swift | 180 +++++----- ...uredSwiftTranslatorSnippetBasedTests.swift | 36 +- ...erverCodeTranslatorSnippetBasedTests.swift | 112 +++---- ...TypealiasTranslatorSnippetBasedTests.swift | 314 ++++++++---------- .../ProtobufCodeGeneratorTests.swift | 206 ++++++------ 9 files changed, 448 insertions(+), 540 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 256b763db..2a9585bbd 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -141,7 +141,7 @@ extension ClientCodeTranslator { } let clientProtocolExtension = Declaration.extension( ExtensionDescription( - onType: "\(service.namespacedTypealiasGeneratedName).ClientProtocol", + onType: "\(service.namespacedGeneratedName).ClientProtocol", declarations: methods ) ) @@ -344,7 +344,7 @@ extension ClientCodeTranslator { StructDescription( accessModifier: self.accessModifier, name: "\(service.namespacedGeneratedName)Client", - conformances: ["\(service.namespacedTypealiasGeneratedName).ClientProtocol"], + conformances: ["\(service.namespacedGeneratedName).ClientProtocol"], members: [clientProperty, initializer] + methods ) ) @@ -408,7 +408,7 @@ extension ClientCodeTranslator { .init( label: "descriptor", expression: .identifierPattern( - "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" + "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" ) ), .init(label: "serializer", expression: .identifierPattern("serializer")), @@ -448,7 +448,7 @@ extension ClientCodeTranslator { type: InputOutputType ) -> String { var components: String = - "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase)" + "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase)" switch type { case .input: diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index e195b45b4..79dc6d2f2 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -101,64 +101,30 @@ extension IDLToStructuredSwiftTranslator { private func validateInput(_ codeGenerationRequest: CodeGenerationRequest) throws { try self.checkServiceDescriptorsAreUnique(codeGenerationRequest.services) - let servicesByUpperCaseNamespace = Dictionary( + let servicesByGeneratedEnumName = Dictionary( grouping: codeGenerationRequest.services, - by: { $0.namespace.generatedUpperCase } + by: { $0.namespacedGeneratedName } ) - try self.checkServiceNamesAreUnique(for: servicesByUpperCaseNamespace) + try self.checkServiceEnumNamesAreUnique(for: servicesByGeneratedEnumName) for service in codeGenerationRequest.services { try self.checkMethodNamesAreUnique(in: service) } } - // Verify service names are unique within each namespace and that services with no namespace - // don't have the same names as any of the namespaces. - private func checkServiceNamesAreUnique( - for servicesByUpperCaseNamespace: [String: [CodeGenerationRequest.ServiceDescriptor]] + // Verify service enum names are unique. + private func checkServiceEnumNamesAreUnique( + for servicesByGeneratedEnumName: [String: [CodeGenerationRequest.ServiceDescriptor]] ) throws { - // Check that if there are services in an empty namespace, none have names which match other namespaces, - // to ensure that there are no enums with the same name in the type aliases part of the generated code. - if let noNamespaceServices = servicesByUpperCaseNamespace[""] { - let upperCaseNamespaces = servicesByUpperCaseNamespace.keys - for service in noNamespaceServices { - if upperCaseNamespaces.contains(service.name.generatedUpperCase) { - throw CodeGenError( - code: .nonUniqueServiceName, - message: """ - Services with no namespace must not have the same generated upper case names as the namespaces. \ - \(service.name.generatedUpperCase) is used as a generated upper case name for a service with no namespace and a namespace. - """ - ) - } - } - } - - // Check that the generated upper case names for services are unique within each namespace, to ensure that - // the service enums from each namespace enum have unique names. - for (namespace, services) in servicesByUpperCaseNamespace { - var upperCaseNames: Set = [] - - for service in services { - if upperCaseNames.contains(service.name.generatedUpperCase) { - let errorMessage: String - if namespace.isEmpty { - errorMessage = """ - Services in an empty namespace must have unique generated upper case names. \ - \(service.name.generatedUpperCase) is used as a generated upper case name for multiple services without namespaces. - """ - } else { - errorMessage = """ - Services within the same namespace must have unique generated upper case names. \ - \(service.name.generatedUpperCase) is used as a generated upper case name for multiple services in the \(service.namespace.base) namespace. - """ - } - throw CodeGenError( - code: .nonUniqueServiceName, - message: errorMessage - ) - } - upperCaseNames.insert(service.name.generatedUpperCase) + for (generatedEnumName, services) in servicesByGeneratedEnumName { + if services.count > 1 { + throw CodeGenError( + code: .nonUniqueServiceName, + message: """ + There must be a unique (namespace, service_name) pair for each service. \ + \(generatedEnumName) is used as a _ construction for multiple services. + """ + ) } } } @@ -230,14 +196,6 @@ extension IDLToStructuredSwiftTranslator { } extension CodeGenerationRequest.ServiceDescriptor { - var namespacedTypealiasGeneratedName: String { - if self.namespace.generatedUpperCase.isEmpty { - return self.name.generatedUpperCase - } else { - return "\(self.namespace.generatedUpperCase).\(self.name.generatedUpperCase)" - } - } - var namespacedGeneratedName: String { if self.namespace.generatedUpperCase.isEmpty { return self.name.generatedUpperCase diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index c25c56753..002ae75dc 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -479,7 +479,7 @@ extension ServerCodeTranslator { type: InputOutputType ) -> String { var components: String = - "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase)" + "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase)" switch type { case .input: @@ -497,7 +497,7 @@ extension ServerCodeTranslator { service: CodeGenerationRequest.ServiceDescriptor ) -> String { return - "\(service.namespacedTypealiasGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" + "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" } /// Generates the fully qualified name of the type alias for a service protocol. @@ -506,9 +506,9 @@ extension ServerCodeTranslator { streaming: Bool ) -> String { if streaming { - return "\(service.namespacedTypealiasGeneratedName).StreamingServiceProtocol" + return "\(service.namespacedGeneratedName).StreamingServiceProtocol" } - return "\(service.namespacedTypealiasGeneratedName).ServiceProtocol" + return "\(service.namespacedGeneratedName).ServiceProtocol" } /// Generates the name of a service protocol. diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 19d30de53..8c689aebc 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -69,19 +69,20 @@ struct TypealiasTranslator: SpecializedTranslator { func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { var codeBlocks = [CodeBlock]() let services = codeGenerationRequest.services - let servicesByNamespace = Dictionary( + let servicesByEnumName = Dictionary( grouping: services, - by: { $0.namespace.generatedUpperCase } + by: { $0.namespacedGeneratedName } ) - // Sorting the keys and the services in each list of the dictionary is necessary - // so that the generated enums are deterministically ordered. - for (generatedNamespace, services) in servicesByNamespace.sorted(by: { $0.key < $1.key }) { - let namespaceCodeBlocks = try self.makeNamespaceEnum( - for: generatedNamespace, - containing: services.sorted(by: { $0.name.generatedUpperCase < $1.name.generatedUpperCase }) - ) - codeBlocks.append(contentsOf: namespaceCodeBlocks) + // Sorting the keys of the dictionary is necessary so that the generated enums are deterministically ordered. + for (generatedEnumName, services) in servicesByEnumName.sorted(by: { $0.key < $1.key }) { + for service in services { + codeBlocks.append( + CodeBlock( + item: .declaration(try self.makeServiceEnum(from: service, named: generatedEnumName)) + ) + ) + } } return codeBlocks @@ -89,38 +90,13 @@ struct TypealiasTranslator: SpecializedTranslator { } extension TypealiasTranslator { - private func makeNamespaceEnum( - for namespace: String, - containing services: [CodeGenerationRequest.ServiceDescriptor] - ) throws -> [CodeBlock] { - var serviceDeclarations = [Declaration]() - - // Create the service specific enums. - for service in services { - let serviceEnum = try self.makeServiceEnum(from: service) - serviceDeclarations.append(serviceEnum) - } - - // If there is no namespace, the service enums are independent CodeBlocks. - // If there is a namespace, the associated enum will contain the service enums and will - // be represented as a single CodeBlock element. - if namespace.isEmpty { - return serviceDeclarations.map { - CodeBlock(item: .declaration($0)) - } - } else { - var namespaceEnum = EnumDescription(accessModifier: self.accessModifier, name: namespace) - namespaceEnum.members = serviceDeclarations - return [CodeBlock(item: .declaration(.enum(namespaceEnum)))] - } - } - private func makeServiceEnum( - from service: CodeGenerationRequest.ServiceDescriptor + from service: CodeGenerationRequest.ServiceDescriptor, + named name: String ) throws -> Declaration { var serviceEnum = EnumDescription( accessModifier: self.accessModifier, - name: service.name.generatedUpperCase + name: name ) var methodsEnum = EnumDescription(accessModifier: self.accessModifier, name: "Method") let methods = service.methods diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 086373e13..99c8ef5fb 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -47,29 +47,29 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ClientProtocol { + extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { @@ -78,14 +78,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, + descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -123,29 +123,29 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ClientProtocol { + extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { @@ -154,14 +154,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, - descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, + descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -199,29 +199,29 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ClientProtocol { + extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { @@ -230,14 +230,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, - descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, + descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -275,29 +275,29 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ClientProtocol { + extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { @@ -306,14 +306,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, - descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, + descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -359,49 +359,49 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Documentation for MethodB func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ClientProtocol { + extension NamespaceA_ServiceA.ClientProtocol { package func methodA( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } package func methodB( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodB( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { + package struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient package init(client: GRPCCore.GRPCClient) { @@ -410,14 +410,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA package func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, - descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, + descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -426,14 +426,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodB package func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, - descriptor: NamespaceA.ServiceA.Method.MethodB.descriptor, + descriptor: NamespaceA_ServiceA.Method.MethodB.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -552,11 +552,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable {} @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ClientProtocol { + extension NamespaceA_ServiceA.ClientProtocol { } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA.ServiceA.ClientProtocol { + public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(client: GRPCCore.GRPCClient) { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 224c0a909..82370da1c 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -170,16 +170,14 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { @_spi(Secret) import Foo @_spi(Secret) import enum Foo.Bar - public enum NamespaceA { - public enum ServiceA { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public enum NamespaceA_ServiceA { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } /// Documentation for AService @@ -188,18 +186,18 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA_ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for AService @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {} + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ServiceProtocol { + extension NamespaceA_ServiceA.ServiceProtocol { } """ try self.assertIDLToStructuredSwiftTranslation( @@ -379,8 +377,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { CodeGenError( code: .nonUniqueServiceName, message: """ - Services within the same namespace must have unique generated upper case names. \ - AService is used as a generated upper case name for multiple services in the namespacea namespace. + There must be a unique (namespace, service_name) pair for each service. \ + NamespaceA_AService is used as a _ construction for multiple services. """ ) ) @@ -541,7 +539,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameGeneratedNameNoNamespaceServiceAndNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for SameName service with no namespace", - name: Name(base: "SameName", generatedUpperCase: "SameName", generatedLowerCase: "sameName"), + name: Name( + base: "SameName", + generatedUpperCase: "SameName_BService", + generatedLowerCase: "sameName" + ), namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) @@ -572,8 +574,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { CodeGenError( code: .nonUniqueServiceName, message: """ - Services with no namespace must not have the same generated upper case names as the namespaces. \ - SameName is used as a generated upper case name for a service with no namespace and a namespace. + There must be a unique (namespace, service_name) pair for each service. \ + SameName_BService is used as a _ construction for multiple services. """ ) ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 416ed3ebc..ee909d567 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -54,17 +54,17 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod - func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA_ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: NamespaceA.ServiceA.Method.Unary.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unary(request: request) } @@ -73,14 +73,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod - func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ServiceProtocol { - public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension NamespaceA_ServiceA.ServiceProtocol { + public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unary(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -123,17 +123,17 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA_ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: NamespaceA.ServiceA.Method.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } @@ -142,14 +142,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { + package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ServiceProtocol { - package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension NamespaceA_ServiceA.ServiceProtocol { + package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } @@ -196,17 +196,17 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA_ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: NamespaceA.ServiceA.Method.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -215,14 +215,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ServiceProtocol { - public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension NamespaceA_ServiceA.ServiceProtocol { + public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } @@ -269,17 +269,17 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA_ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: NamespaceA.ServiceA.Method.BidirectionalStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA_ServiceA.Method.BidirectionalStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.bidirectionalStreaming(request: request) } @@ -288,13 +288,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { + package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ServiceProtocol { + extension NamespaceA_ServiceA.ServiceProtocol { } """ @@ -350,28 +350,28 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA_ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: NamespaceA.ServiceA.Method.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } ) router.registerHandler( - forMethod: NamespaceA.ServiceA.Method.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -380,22 +380,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol { + internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ServiceProtocol { - internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension NamespaceA_ServiceA.ServiceProtocol { + internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } - internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } @@ -502,32 +502,32 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.StreamingServiceProtocol { + extension NamespaceA_ServiceA.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceA @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {} + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceA.ServiceProtocol { + extension NamespaceA_ServiceA.ServiceProtocol { } /// Documentation for ServiceB @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceB.StreamingServiceProtocol { + extension NamespaceA_ServiceB.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceB @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public protocol NamespaceA_ServiceBServiceProtocol: NamespaceA.ServiceB.StreamingServiceProtocol {} + public protocol NamespaceA_ServiceBServiceProtocol: NamespaceA_ServiceB.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceBStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension NamespaceA.ServiceB.ServiceProtocol { + extension NamespaceA_ServiceB.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index ed0d5bcc8..1363e9c0e 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -46,30 +46,28 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - public enum NamespaceA { - public enum ServiceA { - public enum Method { - public enum MethodA { - public typealias Input = NamespaceA_ServiceARequest - public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = MethodDescriptor( - service: "namespaceA.ServiceA", - method: "MethodA" - ) - } - public static let descriptors: [MethodDescriptor] = [ - MethodA.descriptor - ] + public enum NamespaceA_ServiceA { + public enum Method { + public enum MethodA { + public typealias Input = NamespaceA_ServiceARequest + public typealias Output = NamespaceA_ServiceAResponse + public static let descriptor = MethodDescriptor( + service: "namespaceA.ServiceA", + method: "MethodA" + ) } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias Client = NamespaceA_ServiceAClient + public static let descriptors: [MethodDescriptor] = [ + MethodA.descriptor + ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = NamespaceA_ServiceAClient } """ @@ -95,20 +93,18 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - public enum NamespaceA { - public enum ServiceA { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias Client = NamespaceA_ServiceAClient + public enum NamespaceA_ServiceA { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = NamespaceA_ServiceAClient } """ @@ -134,16 +130,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - public enum NamespaceA { - public enum ServiceA { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public enum NamespaceA_ServiceA { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } """ @@ -169,16 +163,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - public enum NamespaceA { - public enum ServiceA { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias Client = NamespaceA_ServiceAClient + public enum NamespaceA_ServiceA { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = NamespaceA_ServiceAClient } """ @@ -204,11 +196,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - public enum NamespaceA { - public enum ServiceA { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } + public enum NamespaceA_ServiceA { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] } } """ @@ -302,39 +292,37 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - public enum NamespaceA { - public enum ServiceA { - public enum Method { - public enum MethodA { - public typealias Input = NamespaceA_ServiceARequest - public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = MethodDescriptor( - service: "namespaceA.ServiceA", - method: "MethodA" - ) - } - public enum MethodB { - public typealias Input = NamespaceA_ServiceARequest - public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = MethodDescriptor( - service: "namespaceA.ServiceA", - method: "MethodB" - ) - } - public static let descriptors: [MethodDescriptor] = [ - MethodA.descriptor, - MethodB.descriptor - ] + public enum NamespaceA_ServiceA { + public enum Method { + public enum MethodA { + public typealias Input = NamespaceA_ServiceARequest + public typealias Output = NamespaceA_ServiceAResponse + public static let descriptor = MethodDescriptor( + service: "namespaceA.ServiceA", + method: "MethodA" + ) + } + public enum MethodB { + public typealias Input = NamespaceA_ServiceARequest + public typealias Output = NamespaceA_ServiceAResponse + public static let descriptor = MethodDescriptor( + service: "namespaceA.ServiceA", + method: "MethodB" + ) } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias Client = NamespaceA_ServiceAClient + public static let descriptors: [MethodDescriptor] = [ + MethodA.descriptor, + MethodB.descriptor + ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = NamespaceA_ServiceAClient } """ @@ -360,20 +348,18 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ - package enum NamespaceA { - package enum ServiceA { - package enum Method { - package static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias Client = NamespaceA_ServiceAClient + package enum NamespaceA_ServiceA { + package enum Method { + package static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias Client = NamespaceA_ServiceAClient } """ @@ -411,33 +397,31 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ - public enum NamespaceA { - public enum Aservice { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = NamespaceA_AserviceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ClientProtocol = NamespaceA_AserviceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias Client = NamespaceA_AserviceClient + public enum NamespaceA_Aservice { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] } - public enum Bservice { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = NamespaceA_BserviceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ClientProtocol = NamespaceA_BserviceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias Client = NamespaceA_BserviceClient + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = NamespaceA_AserviceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = NamespaceA_AserviceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = NamespaceA_AserviceClient + } + public enum NamespaceA_Bservice { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = NamespaceA_BserviceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = NamespaceA_BserviceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = NamespaceA_BserviceClient } """ @@ -529,35 +513,31 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ - internal enum Anamespace { - internal enum AService { - internal enum Method { - internal static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias ClientProtocol = Anamespace_AServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias Client = Anamespace_AServiceClient + internal enum Anamespace_AService { + internal enum Method { + internal static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ClientProtocol = Anamespace_AServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias Client = Anamespace_AServiceClient } - internal enum Bnamespace { - internal enum BService { - internal enum Method { - internal static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias StreamingServiceProtocol = Bnamespace_BServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias Client = Bnamespace_BServiceClient + internal enum Bnamespace_BService { + internal enum Method { + internal static let descriptors: [MethodDescriptor] = [] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias StreamingServiceProtocol = Bnamespace_BServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias Client = Bnamespace_BServiceClient } """ @@ -589,6 +569,19 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) let expectedSwift = """ + public enum Anamespace_AService { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = Anamespace_AServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = Anamespace_AServiceClient + } public enum BService { public enum Method { public static let descriptors: [MethodDescriptor] = [] @@ -602,21 +595,6 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public typealias Client = BServiceClient } - public enum Anamespace { - public enum AService { - public enum Method { - public static let descriptors: [MethodDescriptor] = [] - } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ClientProtocol = Anamespace_AServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias Client = Anamespace_AServiceClient - } - } """ try self.assertTypealiasTranslation( diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index d2a97b59c..c61911379 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -58,26 +58,24 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import DifferentModule import ExtraModule - internal enum Helloworld { - internal enum Greeter { - internal enum Method { - internal enum SayHello { - internal typealias Input = Helloworld_HelloRequest - internal typealias Output = Helloworld_HelloReply - internal static let descriptor = MethodDescriptor( - service: "helloworld.Greeter", - method: "SayHello" - ) - } - internal static let descriptors: [MethodDescriptor] = [ - SayHello.descriptor - ] + internal enum Helloworld_Greeter { + internal enum Method { + internal enum SayHello { + internal typealias Input = Helloworld_HelloRequest + internal typealias Output = Helloworld_HelloReply + internal static let descriptor = MethodDescriptor( + service: "helloworld.Greeter", + method: "SayHello" + ) } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias ClientProtocol = Helloworld_GreeterClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias Client = Helloworld_GreeterClient + internal static let descriptors: [MethodDescriptor] = [ + SayHello.descriptor + ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ClientProtocol = Helloworld_GreeterClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias Client = Helloworld_GreeterClient } /// The greeting service definition. @@ -85,23 +83,23 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld.Greeter.ClientProtocol { + extension Helloworld_Greeter.ClientProtocol { internal func sayHello( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -109,7 +107,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { + internal struct Helloworld_GreeterClient: Helloworld_Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient internal init(client: GRPCCore.GRPCClient) { @@ -118,14 +116,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. internal func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: Helloworld.Greeter.Method.SayHello.descriptor, + descriptor: Helloworld_Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -169,44 +167,42 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import DifferentModule import ExtraModule - public enum Helloworld { - public enum Greeter { - public enum Method { - public enum SayHello { - public typealias Input = Helloworld_HelloRequest - public typealias Output = Helloworld_HelloReply - public static let descriptor = MethodDescriptor( - service: "helloworld.Greeter", - method: "SayHello" - ) - } - public static let descriptors: [MethodDescriptor] = [ - SayHello.descriptor - ] + public enum Helloworld_Greeter { + public enum Method { + public enum SayHello { + public typealias Input = Helloworld_HelloRequest + public typealias Output = Helloworld_HelloReply + public static let descriptor = MethodDescriptor( + service: "helloworld.Greeter", + method: "SayHello" + ) } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + public static let descriptors: [MethodDescriptor] = [ + SayHello.descriptor + ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol } /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld.Greeter.StreamingServiceProtocol { + extension Helloworld_Greeter.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: Helloworld.Greeter.Method.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: Helloworld_Greeter.Method.SayHello.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -216,15 +212,15 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { + public protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld.Greeter.ServiceProtocol { - public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension Helloworld_Greeter.ServiceProtocol { + public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -265,48 +261,46 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import DifferentModule import ExtraModule - package enum Helloworld { - package enum Greeter { - package enum Method { - package enum SayHello { - package typealias Input = Helloworld_HelloRequest - package typealias Output = Helloworld_HelloReply - package static let descriptor = MethodDescriptor( - service: "helloworld.Greeter", - method: "SayHello" - ) - } - package static let descriptors: [MethodDescriptor] = [ - SayHello.descriptor - ] + package enum Helloworld_Greeter { + package enum Method { + package enum SayHello { + package typealias Input = Helloworld_HelloRequest + package typealias Output = Helloworld_HelloReply + package static let descriptor = MethodDescriptor( + service: "helloworld.Greeter", + method: "SayHello" + ) } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias ClientProtocol = Helloworld_GreeterClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias Client = Helloworld_GreeterClient + package static let descriptors: [MethodDescriptor] = [ + SayHello.descriptor + ] } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias ClientProtocol = Helloworld_GreeterClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + package typealias Client = Helloworld_GreeterClient } /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld.Greeter.StreamingServiceProtocol { + extension Helloworld_Greeter.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: Helloworld.Greeter.Method.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: Helloworld_Greeter.Method.SayHello.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -316,15 +310,15 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { + package protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld.Greeter.ServiceProtocol { - package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension Helloworld_Greeter.ServiceProtocol { + package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -335,23 +329,23 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld.Greeter.ClientProtocol { + extension Helloworld_Greeter.ClientProtocol { package func sayHello( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -359,7 +353,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol { + package struct Helloworld_GreeterClient: Helloworld_Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient package init(client: GRPCCore.GRPCClient) { @@ -368,14 +362,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. package func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: Helloworld.Greeter.Method.SayHello.descriptor, + descriptor: Helloworld_Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, handler: body From 75b7a3c4b589bd1249666751924afa17aaeda51d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 14 Feb 2024 11:30:34 +0000 Subject: [PATCH 247/580] Update README (#1809) - Remove reference to Makefile - Remove old build instruction - Add note about SwiftPM plugin --- README.md | 56 ++++++++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 91155b7e8..e49c67fa8 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ package dependency to your `Package.swift`: ```swift dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.15.0") + .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.21.0") ] ``` @@ -74,41 +74,36 @@ dependencies: [ ] ``` -##### Xcode - -From Xcode 11 it is possible to [add Swift Package dependencies to Xcode -projects][xcode-spm] and link targets to products of those packages; this is the -easiest way to integrate gRPC Swift with an existing `xcodeproj`. - -##### Manual Integration - -Alternatively, gRPC Swift can be manually integrated into a project: - -1. Build an Xcode project: `swift package generate-xcodeproj`, -1. Add the generated project to your own project, and -1. Add a build dependency on `GRPC`. - ### Getting the `protoc` Plugins Binary releases of `protoc`, the Protocol Buffer Compiler, are available on [GitHub][protobuf-releases]. -To build the plugins, run `make plugins` in the main directory. This uses the -Swift Package Manager to build both of the necessary plugins: -`protoc-gen-swift`, which generates Protocol Buffer support code and -`protoc-gen-grpc-swift`, which generates gRPC interface code. +To build the plugins, run: +- `swift build -c release --product protoc-gen-swift` to build the `protoc` + plugin which generates Protocol Buffer support code, and +- `swift build -c release --product protoc-gen-grpc-swift` to build the `protoc` plugin + which generates gRPC interface code. To install these plugins, just copy the two executables (`protoc-gen-swift` and -`protoc-gen-grpc-swift`) that show up in the main directory into a directory +`protoc-gen-grpc-swift`) from the build directory (`.build/release`) into a directory that is part of your `PATH` environment variable. Alternatively the full path to the plugins can be specified when using `protoc`. -#### Homebrew +### Using the Swift Package Manager plugin -The plugins are available from [homebrew](https://brew.sh) and can be installed with: -```bash - $ brew install swift-protobuf grpc-swift -``` +You can also use the Swift Package Manager build plugin to generate messages and +gRPC code at build time rather than using `protoc` to generate them ahead of +time. Using this method Swift Package Manager takes care of building +`protoc-gen-swift` and `protoc-gen-grpc-swift` for you. + +One important distinction between using the Swift Package Manager build plugin +and generating the code ahead of time is that the build plugin has an implicit +dependency on `protoc`. It's therefore unsuitable for _libraries_ as they can't +guarantee that end users will have `protoc` available at compile time. + +You can find more documentation about the Swift Package Manager build plugin in +[Using the Swift Package Manager plugin][spm-plugin-grpc]. ## Examples @@ -140,25 +135,25 @@ The `docs` directory contains documentation, including: - How to configure keepalive in [`docs/keepalive.md`][docs-keepalive] - Support for Apple Platforms and NIO Transport Services in [`docs/apple-platforms.md`][docs-apple] - + ## Benchmarks -Benchmarks for `grpc-swift` are in a separate Swift Package in the `Performance/Benchmarks` subfolder of this repository. +Benchmarks for `grpc-swift` are in a separate Swift Package in the `Performance/Benchmarks` subfolder of this repository. They use the [`package-benchmark`](https://github.com/ordo-one/package-benchmark) plugin. Benchmarks depends on the [`jemalloc`](https://jemalloc.net) memory allocation library, which is used by `package-benchmark` to capture memory allocation statistics. -An installation guide can be found in the [Getting Started article](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted#Installing-Prerequisites-and-Platform-Support) of `package-benchmark`. +An installation guide can be found in the [Getting Started article](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted#Installing-Prerequisites-and-Platform-Support) of `package-benchmark`. Afterwards you can run the benchmarks from CLI by going to the `Performance/Benchmarks` subfolder (e.g. `cd Performance/Benchmarks`) and invoking: ``` swift package benchmark ``` -Profiling benchmarks or building the benchmarks in release mode in Xcode with `jemalloc` is currently not supported and requires disabling `jemalloc`. +Profiling benchmarks or building the benchmarks in release mode in Xcode with `jemalloc` is currently not supported and requires disabling `jemalloc`. Make sure Xcode is closed and then open it from the CLI with the `BENCHMARK_DISABLE_JEMALLOC=true` environment variable set e.g.: ``` BENCHMARK_DISABLE_JEMALLOC=true xed . ``` -For more information please refer to `swift package benchmark --help` or the [documentation of `package-benchmark`](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark). +For more information please refer to `swift package benchmark --help` or the [documentation of `package-benchmark`](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark). ## Security @@ -190,3 +185,4 @@ Please get involved! See our [guidelines for contributing](CONTRIBUTING.md). [branch-old]: https://github.com/grpc/grpc-swift/tree/cgrpc [examples-out-of-source]: https://github.com/grpc/grpc-swift/tree/main/Examples [examples-in-source]: https://github.com/grpc/grpc-swift/tree/main/Sources/Examples +[spm-plugin-grpc]: https://swiftpackageindex.com/grpc/grpc-swift/main/documentation/protoc-gen-grpc-swift/spm-plugin From ac0083c21ecc8d8836946eef61a8755040ac8194 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 14 Feb 2024 16:17:58 +0000 Subject: [PATCH 248/580] Fix namespace name for nested package names (#1806) Motivation: The current implementation of the ProtobufParser replaces the dots with their scalar value for the CodeGenerationRequest names. We want the dots to be replaced by "_"s. Modifications: - updated the Name object generation for packages in the ProtobufCodeGenParser to handle correctly nested package names - added more ProtobufCodeGen tests for a simple package name, a nested one and an empty one Result: Tha dots from the package name will be replaced by underscores in the input and output types, as well as in the generated protocols and extensions. --- .../ProtobufCodeGenParser.swift | 38 ++- .../ProtobufCodeGenParserTests.swift | 278 +++++++++++++++--- .../ProtobufCodeGeneratorTests.swift | 120 ++++---- 3 files changed, 339 insertions(+), 97 deletions(-) diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index 8fb8037e9..59904ce46 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -68,7 +68,8 @@ internal struct ProtobufCodeGenParser { CodeGenerationRequest.ServiceDescriptor( descriptor: $0, package: input.package, - protobufNamer: self.namer + protobufNamer: self.namer, + file: self.input ) } @@ -107,7 +108,8 @@ extension CodeGenerationRequest.ServiceDescriptor { fileprivate init( descriptor: ServiceDescriptor, package: String, - protobufNamer: SwiftProtobufNamer + protobufNamer: SwiftProtobufNamer, + file: FileDescriptor ) { let methods = descriptor.methods.map { CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( @@ -120,10 +122,13 @@ extension CodeGenerationRequest.ServiceDescriptor { generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name) ) + + // Packages that are based on the path of the '.proto' file usually + // contain dots. For example: "grpc.test". let namespace = CodeGenerationRequest.Name( base: package, - generatedUpperCase: NamingUtils.toUpperCamelCase(package), - generatedLowerCase: NamingUtils.toLowerCamelCase(package) + generatedUpperCase: protobufNamer.formattedUpperCasePackage(file: file), + generatedLowerCase: protobufNamer.formattedLowerCasePackage(file: file) ) let documentation = descriptor.protoSourceComments() self.init(documentation: documentation, name: name, namespace: namespace, methods: methods) @@ -166,3 +171,28 @@ extension FileDescriptor { return header } } + +extension SwiftProtobufNamer { + internal func formattedUpperCasePackage(file: FileDescriptor) -> String { + let unformattedPackage = self.typePrefix(forFile: file) + return unformattedPackage.trimTrailingUnderscores() + } + + internal func formattedLowerCasePackage(file: FileDescriptor) -> String { + let upperCasePackage = self.formattedUpperCasePackage(file: file) + let lowerCaseComponents = upperCasePackage.split(separator: "_").map { component in + NamingUtils.toLowerCamelCase(String(component)) + } + return lowerCaseComponents.joined(separator: "_") + } +} + +extension String { + internal func trimTrailingUnderscores() -> String { + if let index = self.lastIndex(where: { $0 != "_" }) { + return String(self[...index]) + } else { + return "" + } + } +} diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 8d6cf3891..5c34d4af6 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -57,42 +57,8 @@ final class ProtobufCodeGenParserTests: XCTestCase { protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), extraModuleImports: ["ExtraModule"] ).parse() - XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") - XCTAssertEqual( - parsedCodeGenRequest.leadingTrivia, - """ - // Copyright 2015 gRPC authors. - // - // 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. - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: helloworld.proto - // - // For information on using the generated types, please see the documentation: - // https://github.com/grpc/grpc-swift - - """ - ) - - XCTAssertEqual(parsedCodeGenRequest.dependencies.count, 3) - let expectedDependencyNames = ["GRPCProtobuf", "DifferentModule", "ExtraModule"] - let parsedDependencyNames = parsedCodeGenRequest.dependencies.map { $0.module } - XCTAssertEqual(parsedDependencyNames, expectedDependencyNames) - - XCTAssertEqual(parsedCodeGenRequest.services.count, 1) + self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( documentation: "/// Sends a greeting.\n", @@ -127,6 +93,168 @@ final class ProtobufCodeGenParserTests: XCTestCase { XCTAssertEqual(service, expectedService) XCTAssertEqual(service.methods.count, 1) + XCTAssertEqual( + parsedCodeGenRequest.lookupSerializer("Helloworld_HelloRequest"), + "ProtobufSerializer()" + ) + XCTAssertEqual( + parsedCodeGenRequest.lookupDeserializer("Helloworld_HelloRequest"), + "ProtobufDeserializer()" + ) + } + + func testParserNestedPackage() throws { + let descriptorSet = DescriptorSet( + protos: [ + Google_Protobuf_FileDescriptorProto( + name: "same-module.proto", + package: "same-package" + ), + Google_Protobuf_FileDescriptorProto( + name: "different-module.proto", + package: "different-package" + ), + Google_Protobuf_FileDescriptorProto.helloWorldNestedPackage, + ] + ) + + guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { + return XCTFail( + """ + Could not find the file descriptor of "helloworld.proto". + """ + ) + } + let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { + $0.mapping = [ + SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { + $0.protoFilePath = ["different-module.proto"] + $0.moduleName = "DifferentModule" + } + ] + } + let parsedCodeGenRequest = try ProtobufCodeGenParser( + input: fileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), + extraModuleImports: ["ExtraModule"] + ).parse() + + self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) + + let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( + documentation: "/// Sends a greeting.\n", + name: CodeGenerationRequest.Name( + base: "SayHello", + generatedUpperCase: "SayHello", + generatedLowerCase: "sayHello" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "Hello_World_HelloRequest", + outputType: "Hello_World_HelloReply" + ) + guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } + XCTAssertEqual(method, expectedMethod) + + let expectedService = CodeGenerationRequest.ServiceDescriptor( + documentation: "/// The greeting service definition.\n", + name: CodeGenerationRequest.Name( + base: "Greeter", + generatedUpperCase: "Greeter", + generatedLowerCase: "greeter" + ), + namespace: CodeGenerationRequest.Name( + base: "hello.world", + generatedUpperCase: "Hello_World", + generatedLowerCase: "hello_world" + ), + methods: [expectedMethod] + ) + guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } + XCTAssertEqual(service, expectedService) + XCTAssertEqual(service.methods.count, 1) + + XCTAssertEqual( + parsedCodeGenRequest.lookupSerializer("Hello_World_HelloRequest"), + "ProtobufSerializer()" + ) + XCTAssertEqual( + parsedCodeGenRequest.lookupDeserializer("Hello_World_HelloRequest"), + "ProtobufDeserializer()" + ) + } + + func testParserEmptyPackage() throws { + let descriptorSet = DescriptorSet( + protos: [ + Google_Protobuf_FileDescriptorProto( + name: "same-module.proto", + package: "same-package" + ), + Google_Protobuf_FileDescriptorProto( + name: "different-module.proto", + package: "different-package" + ), + Google_Protobuf_FileDescriptorProto.helloWorldEmptyPackage, + ] + ) + + guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { + return XCTFail( + """ + Could not find the file descriptor of "helloworld.proto". + """ + ) + } + let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { + $0.mapping = [ + SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { + $0.protoFilePath = ["different-module.proto"] + $0.moduleName = "DifferentModule" + } + ] + } + let parsedCodeGenRequest = try ProtobufCodeGenParser( + input: fileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), + extraModuleImports: ["ExtraModule"] + ).parse() + + self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) + + let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( + documentation: "/// Sends a greeting.\n", + name: CodeGenerationRequest.Name( + base: "SayHello", + generatedUpperCase: "SayHello", + generatedLowerCase: "sayHello" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "HelloRequest", + outputType: "HelloReply" + ) + guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } + XCTAssertEqual(method, expectedMethod) + + let expectedService = CodeGenerationRequest.ServiceDescriptor( + documentation: "/// The greeting service definition.\n", + name: CodeGenerationRequest.Name( + base: "Greeter", + generatedUpperCase: "Greeter", + generatedLowerCase: "greeter" + ), + namespace: CodeGenerationRequest.Name( + base: "", + generatedUpperCase: "", + generatedLowerCase: "" + ), + methods: [expectedMethod] + ) + guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } + XCTAssertEqual(service, expectedService) + XCTAssertEqual(service.methods.count, 1) + XCTAssertEqual( parsedCodeGenRequest.lookupSerializer("HelloRequest"), "ProtobufSerializer()" @@ -138,6 +266,45 @@ final class ProtobufCodeGenParserTests: XCTestCase { } } +extension ProtobufCodeGenParserTests { + func testCommonHelloworldParsedRequestFields(for request: CodeGenerationRequest) { + XCTAssertEqual(request.fileName, "helloworld.proto") + XCTAssertEqual( + request.leadingTrivia, + """ + // Copyright 2015 gRPC authors. + // + // 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. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: helloworld.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + """ + ) + XCTAssertEqual(request.dependencies.count, 3) + let expectedDependencyNames = ["GRPCProtobuf", "DifferentModule", "ExtraModule"] + let parsedDependencyNames = request.dependencies.map { $0.module } + XCTAssertEqual(parsedDependencyNames, expectedDependencyNames) + XCTAssertEqual(request.services.count, 1) + } +} + extension Google_Protobuf_FileDescriptorProto { static var helloWorld: Google_Protobuf_FileDescriptorProto { let requestType = Google_Protobuf_DescriptorProto.with { @@ -224,6 +391,47 @@ extension Google_Protobuf_FileDescriptorProto { } } + static var helloWorldNestedPackage: Google_Protobuf_FileDescriptorProto { + let service = Google_Protobuf_ServiceDescriptorProto.with { + $0.name = "Greeter" + $0.method = [ + Google_Protobuf_MethodDescriptorProto.with { + $0.name = "SayHello" + $0.inputType = ".hello.world.HelloRequest" + $0.outputType = ".hello.world.HelloReply" + $0.clientStreaming = false + $0.serverStreaming = false + } + ] + } + + var helloWorldCopy = self.helloWorld + helloWorldCopy.package = "hello.world" + helloWorldCopy.service = [service] + + return helloWorldCopy + } + + static var helloWorldEmptyPackage: Google_Protobuf_FileDescriptorProto { + let service = Google_Protobuf_ServiceDescriptorProto.with { + $0.name = "Greeter" + $0.method = [ + Google_Protobuf_MethodDescriptorProto.with { + $0.name = "SayHello" + $0.inputType = ".HelloRequest" + $0.outputType = ".HelloReply" + $0.clientStreaming = false + $0.serverStreaming = false + } + ] + } + var helloWorldCopy = self.helloWorld + helloWorldCopy.package = "" + helloWorldCopy.service = [service] + + return helloWorldCopy + } + internal init(name: String, package: String) { self.init() self.name = name diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index c61911379..a047ce077 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -25,6 +25,7 @@ import XCTest final class ProtobufCodeGeneratorTests: XCTestCase { func testProtobufCodeGenerator() throws { try testCodeGeneration( + proto: Google_Protobuf_FileDescriptorProto.helloWorldNestedPackage, indentation: 4, visibility: .internal, client: true, @@ -58,13 +59,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import DifferentModule import ExtraModule - internal enum Helloworld_Greeter { + internal enum Hello_World_Greeter { internal enum Method { internal enum SayHello { - internal typealias Input = Helloworld_HelloRequest - internal typealias Output = Helloworld_HelloReply + internal typealias Input = Hello_World_HelloRequest + internal typealias Output = Hello_World_HelloReply internal static let descriptor = MethodDescriptor( - service: "helloworld.Greeter", + service: "hello.world.Greeter", method: "SayHello" ) } @@ -73,33 +74,33 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ] } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias ClientProtocol = Helloworld_GreeterClientProtocol + internal typealias ClientProtocol = Hello_World_GreeterClientProtocol @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal typealias Client = Helloworld_GreeterClient + internal typealias Client = Hello_World_GreeterClient } /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal protocol Helloworld_GreeterClientProtocol: Sendable { + internal protocol Hello_World_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld_Greeter.ClientProtocol { + extension Hello_World_Greeter.ClientProtocol { internal func sayHello( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -107,7 +108,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - internal struct Helloworld_GreeterClient: Helloworld_Greeter.ClientProtocol { + internal struct Hello_World_GreeterClient: Hello_World_Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient internal init(client: GRPCCore.GRPCClient) { @@ -116,14 +117,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. internal func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: Helloworld_Greeter.Method.SayHello.descriptor, + descriptor: Hello_World_Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -134,6 +135,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) try testCodeGeneration( + proto: Google_Protobuf_FileDescriptorProto.helloWorld, indentation: 2, visibility: .public, client: false, @@ -228,6 +230,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { """ ) try testCodeGeneration( + proto: Google_Protobuf_FileDescriptorProto.helloWorldEmptyPackage, indentation: 2, visibility: .package, client: true, @@ -261,13 +264,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import DifferentModule import ExtraModule - package enum Helloworld_Greeter { + package enum Greeter { package enum Method { package enum SayHello { - package typealias Input = Helloworld_HelloRequest - package typealias Output = Helloworld_HelloReply + package typealias Input = HelloRequest + package typealias Output = HelloReply package static let descriptor = MethodDescriptor( - service: "helloworld.Greeter", + service: "Greeter", method: "SayHello" ) } @@ -276,31 +279,31 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ] } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol + package typealias StreamingServiceProtocol = GreeterStreamingServiceProtocol @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + package typealias ServiceProtocol = GreeterServiceProtocol @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias ClientProtocol = Helloworld_GreeterClientProtocol + package typealias ClientProtocol = GreeterClientProtocol @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package typealias Client = Helloworld_GreeterClient + package typealias Client = GreeterClient } /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + package protocol GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld_Greeter.StreamingServiceProtocol { + extension Greeter.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( - forMethod: Helloworld_Greeter.Method.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + forMethod: Greeter.Method.SayHello.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -310,15 +313,15 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { + package protocol GreeterServiceProtocol: Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } - /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. + /// Partial conformance to `GreeterStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld_Greeter.ServiceProtocol { - package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + extension Greeter.ServiceProtocol { + package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -326,26 +329,26 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package protocol Helloworld_GreeterClientProtocol: Sendable { + package protocol GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - extension Helloworld_Greeter.ClientProtocol { + extension Greeter.ClientProtocol { package func sayHello( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), body ) } @@ -353,7 +356,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// The greeting service definition. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - package struct Helloworld_GreeterClient: Helloworld_Greeter.ClientProtocol { + package struct GreeterClient: Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient package init(client: GRPCCore.GRPCClient) { @@ -362,14 +365,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. package func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, - descriptor: Helloworld_Greeter.Method.SayHello.descriptor, + descriptor: Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, handler: body @@ -381,6 +384,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } func testCodeGeneration( + proto: Google_Protobuf_FileDescriptorProto, indentation: Int, visibility: SourceGenerator.Configuration.AccessLevel, client: Bool, @@ -400,7 +404,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { name: "different-module.proto", package: "different-package" ), - Google_Protobuf_FileDescriptorProto.helloWorld, + proto, ]) guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { return XCTFail( From c18bbd4069e3edb418ad9c240847e5e53cbc875e Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:25:42 +0000 Subject: [PATCH 249/580] Generate interoperability service and messages (#1800) Motivation: We need the generated sevice and messages for implementing the test service. Modifications: - added the .proto files for the interoperability tests - modified the generate.sh script - added the new module for the generated files - generated the files Result: We will have the generated server code and messages we need to implement the test service. --- Package.swift | 14 +- Protos/generate.sh | 18 + .../src/proto/grpc/testing/empty.proto | 28 + .../proto/grpc/testing/empty_service.proto | 23 + .../src/proto/grpc/testing/messages.proto | 165 +++ .../src/proto/grpc/testing/test.proto | 79 ++ .../Generated/empty.pb.swift | 79 ++ .../Generated/empty_service.grpc.swift | 81 ++ .../Generated/empty_service.pb.swift | 35 + .../Generated/messages.pb.swift | 950 ++++++++++++++++++ .../Generated/test.grpc.swift | 918 +++++++++++++++++ .../Generated/test.pb.swift | 38 + 12 files changed, 2426 insertions(+), 2 deletions(-) create mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/empty.proto create mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto create mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/messages.proto create mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/test.proto create mode 100644 Sources/InteroperabilityTests/Generated/empty.pb.swift create mode 100644 Sources/InteroperabilityTests/Generated/empty_service.grpc.swift create mode 100644 Sources/InteroperabilityTests/Generated/empty_service.pb.swift create mode 100644 Sources/InteroperabilityTests/Generated/messages.pb.swift create mode 100644 Sources/InteroperabilityTests/Generated/test.grpc.swift create mode 100644 Sources/InteroperabilityTests/Generated/test.pb.swift diff --git a/Package.swift b/Package.swift index f21b2a5e2..a4a223855 100644 --- a/Package.swift +++ b/Package.swift @@ -105,7 +105,8 @@ extension Target.Dependency { static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") static let interopTestImplementation: Self = .target(name: "GRPCInteroperabilityTestsImplementation") - + static let interoperabilityTests: Self = .target(name: "InteroperabilityTests") + // Product dependencies static let argumentParser: Self = .product( name: "ArgumentParser", @@ -378,6 +379,14 @@ extension Target { ] ) + static let interoperabilityTestImplementation: Target = .target( + name: "InteroperabilityTests", + dependencies: [ + .grpcCore, + .grpcProtobuf + ] + ) + static let interopTestImplementation: Target = .target( name: "GRPCInteroperabilityTestsImplementation", dependencies: [ @@ -703,6 +712,7 @@ let package = Package( .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf, .grpcProtobufCodeGen, + .interoperabilityTestImplementation, // v2 tests .grpcCoreTests, @@ -713,7 +723,7 @@ let package = Package( .grpcHTTP2TransportNIOPosixTests, .grpcHTTP2TransportNIOTransportServicesTests, .grpcProtobufTests, - .grpcProtobufCodeGenTests + .grpcProtobufCodeGenTests, ] ) diff --git a/Protos/generate.sh b/Protos/generate.sh index 686ef632d..6a392597e 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -186,6 +186,21 @@ function generate_rpc_code_for_tests { done } +function generate_service_messages_interop_tests { + local protos=( + "$here/tests/interoperability/src/proto/grpc/testing/empty_service.proto" + "$here/tests/interoperability/src/proto/grpc/testing/empty.proto" + "$here/tests/interoperability/src/proto/grpc/testing/messages.proto" + "$here/tests/interoperability/src/proto/grpc/testing/test.proto" + ) + local output="$root/Sources/InteroperabilityTests/Generated" + + for proto in "${protos[@]}"; do + generate_message "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "FileNaming=DropPath" + generate_grpc "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "Server=true" "_V2=true" "FileNaming=DropPath" + done +} + #------------------------------------------------------------------------------ # Examples @@ -199,6 +214,9 @@ generate_reflection_service generate_reflection_client_for_tests generate_echo_reflection_data_for_tests +# Interoperability tests +generate_service_messages_interop_tests + # Misc. tests generate_normalization_for_tests generate_rpc_code_for_tests diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/empty.proto b/Protos/tests/interoperability/src/proto/grpc/testing/empty.proto new file mode 100644 index 000000000..dc4cc6067 --- /dev/null +++ b/Protos/tests/interoperability/src/proto/grpc/testing/empty.proto @@ -0,0 +1,28 @@ + +// Copyright 2015 gRPC authors. +// +// 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. + +syntax = "proto3"; + +package grpc.testing; + +// An empty message that you can re-use to avoid defining duplicated empty +// messages in your project. A typical example is to use it as argument or the +// return value of a service API. For instance: +// +// service Foo { +// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; +// }; +// +message Empty {} \ No newline at end of file diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto b/Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto new file mode 100644 index 000000000..42e9cee1c --- /dev/null +++ b/Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto @@ -0,0 +1,23 @@ + +// Copyright 2018 gRPC authors. +// +// 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. + +syntax = "proto3"; + +package grpc.testing; + +// A service that has zero methods. +// See https://github.com/grpc/grpc/issues/15574 +service EmptyService { +} \ No newline at end of file diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/messages.proto b/Protos/tests/interoperability/src/proto/grpc/testing/messages.proto new file mode 100644 index 000000000..bbc4d6988 --- /dev/null +++ b/Protos/tests/interoperability/src/proto/grpc/testing/messages.proto @@ -0,0 +1,165 @@ + +// Copyright 2015-2016 gRPC authors. +// +// 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 definitions to be used by integration test service definitions. + +syntax = "proto3"; + +package grpc.testing; + +// TODO(dgq): Go back to using well-known types once +// https://github.com/grpc/grpc/issues/6980 has been fixed. +// import "google/protobuf/wrappers.proto"; +message BoolValue { + // The bool value. + bool value = 1; +} + +// The type of payload that should be returned. +enum PayloadType { + // Compressable text format. + COMPRESSABLE = 0; +} + +// A block of data, to simply increase gRPC message size. +message Payload { + // The type of data in body. + PayloadType type = 1; + // Primary contents of payload. + bytes body = 2; +} + +// A protobuf representation for grpc status. This is used by test +// clients to specify a status that the server should attempt to return. +message EchoStatus { + int32 code = 1; + string message = 2; +} + +// Unary request. +message SimpleRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + PayloadType response_type = 1; + + // Desired payload size in the response from the server. + int32 response_size = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether SimpleResponse should include username. + bool fill_username = 4; + + // Whether SimpleResponse should include OAuth scope. + bool fill_oauth_scope = 5; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue response_compressed = 6; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // Whether the server should expect this request to be compressed. + BoolValue expect_compressed = 8; +} + +// Unary response, as configured by the request. +message SimpleResponse { + // Payload to increase message size. + Payload payload = 1; + // The user the request came from, for verifying authentication was + // successful when the client expected it. + string username = 2; + // OAuth scope. + string oauth_scope = 3; +} + +// Client-streaming request. +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + Payload payload = 1; + + // Whether the server should expect this request to be compressed. This field + // is "nullable" in order to interoperate seamlessly with servers not able to + // implement the full compression tests by introspecting the call to verify + // the request's compression status. + BoolValue expect_compressed = 2; + + // Not expecting any payload from the response. +} + +// Client-streaming response. +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + int32 aggregated_payload_size = 1; +} + +// Configuration for a particular response. +message ResponseParameters { + // Desired payload sizes in responses from the server. + int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + int32 interval_us = 2; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue compressed = 3; +} + +// Server-streaming request. +message StreamingOutputCallRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + PayloadType response_type = 1; + + // Configuration for each expected response message. + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether server should return a given status + EchoStatus response_status = 7; +} + +// Server-streaming response, as configured by the request and parameters. +message StreamingOutputCallResponse { + // Payload to increase response size. + Payload payload = 1; +} + +// For reconnect interop test only. +// Client tells server what reconnection parameters it used. +message ReconnectParams { + int32 max_reconnect_backoff_ms = 1; +} + +// For reconnect interop test only. +// Server tells client whether its reconnects are following the spec and the +// reconnect backoffs it saw. +message ReconnectInfo { + bool passed = 1; + repeated int32 backoff_ms = 2; +} \ No newline at end of file diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/test.proto b/Protos/tests/interoperability/src/proto/grpc/testing/test.proto new file mode 100644 index 000000000..c049c8fa0 --- /dev/null +++ b/Protos/tests/interoperability/src/proto/grpc/testing/test.proto @@ -0,0 +1,79 @@ + +// Copyright 2015-2016 gRPC authors. +// +// 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +syntax = "proto3"; + +import "src/proto/grpc/testing/empty.proto"; +import "src/proto/grpc/testing/messages.proto"; + +package grpc.testing; + +// A simple service to test the various types of RPCs and experiment with +// performance with various types of payload. +service TestService { + // One empty request followed by one empty response. + rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); + + // One request followed by one response. + rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by one response. Response has cache control + // headers set such that a caching HTTP proxy (such as GFE) can + // satisfy subsequent requests. + rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); + + // One request followed by a sequence of responses (streamed download). + // The server returns the payload with client desired type and sizes. + rpc StreamingOutputCall(StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by one response (streamed upload). + // The server returns the aggregated size of client payload as the result. + rpc StreamingInputCall(stream StreamingInputCallRequest) + returns (StreamingInputCallResponse); + + // A sequence of requests with each request served by the server immediately. + // As one request could lead to multiple responses, this interface + // demonstrates the idea of full duplexing. + rpc FullDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // A sequence of requests followed by a sequence of responses. + // The server buffers all the client requests and then serves them in order. A + // stream of responses are returned to the client when the server starts with + // first request. + rpc HalfDuplexCall(stream StreamingOutputCallRequest) + returns (stream StreamingOutputCallResponse); + + // The test server will not implement this method. It will be used + // to test the behavior when clients call unimplemented methods. + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A simple service NOT implemented at servers so clients can test for +// that case. +service UnimplementedService { + // A call that no server should implement + rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); +} + +// A service used to control reconnect server. +service ReconnectService { + rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); + rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); +} diff --git a/Sources/InteroperabilityTests/Generated/empty.pb.swift b/Sources/InteroperabilityTests/Generated/empty.pb.swift new file mode 100644 index 000000000..bee17ecca --- /dev/null +++ b/Sources/InteroperabilityTests/Generated/empty.pb.swift @@ -0,0 +1,79 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: src/proto/grpc/testing/empty.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 gRPC authors. +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// An empty message that you can re-use to avoid defining duplicated empty +/// messages in your project. A typical example is to use it as argument or the +/// return value of a service API. For instance: +/// +/// service Foo { +/// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; +/// }; +public struct Grpc_Testing_Empty { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Testing_Empty: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.testing" + +extension Grpc_Testing_Empty: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".Empty" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_Empty, rhs: Grpc_Testing_Empty) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift new file mode 100644 index 000000000..0db0ea780 --- /dev/null +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -0,0 +1,81 @@ +// Copyright 2018 gRPC authors. +// +// 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: src/proto/grpc/testing/empty_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +public enum Grpc_Testing_EmptyService { + public enum Method { + public static let descriptors: [MethodDescriptor] = [] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = Grpc_Testing_EmptyServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = Grpc_Testing_EmptyServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = Grpc_Testing_EmptyServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = Grpc_Testing_EmptyServiceClient +} + +/// A service that has zero methods. +/// See https://github.com/grpc/grpc/issues/15574 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_EmptyServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_EmptyService.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func registerMethods(with router: inout GRPCCore.RPCRouter) {} +} + +/// A service that has zero methods. +/// See https://github.com/grpc/grpc/issues/15574 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_EmptyServiceServiceProtocol: Grpc_Testing_EmptyService.StreamingServiceProtocol {} + +/// Partial conformance to `Grpc_Testing_EmptyServiceStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_EmptyService.ServiceProtocol { +} + +/// A service that has zero methods. +/// See https://github.com/grpc/grpc/issues/15574 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_EmptyServiceClientProtocol: Sendable {} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_EmptyService.ClientProtocol { +} + +/// A service that has zero methods. +/// See https://github.com/grpc/grpc/issues/15574 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct Grpc_Testing_EmptyServiceClient: Grpc_Testing_EmptyService.ClientProtocol { + private let client: GRPCCore.GRPCClient + + public init(client: GRPCCore.GRPCClient) { + self.client = client + } +} \ No newline at end of file diff --git a/Sources/InteroperabilityTests/Generated/empty_service.pb.swift b/Sources/InteroperabilityTests/Generated/empty_service.pb.swift new file mode 100644 index 000000000..c6a096809 --- /dev/null +++ b/Sources/InteroperabilityTests/Generated/empty_service.pb.swift @@ -0,0 +1,35 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: src/proto/grpc/testing/empty_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2018 gRPC authors. +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} diff --git a/Sources/InteroperabilityTests/Generated/messages.pb.swift b/Sources/InteroperabilityTests/Generated/messages.pb.swift new file mode 100644 index 000000000..fce2cbc98 --- /dev/null +++ b/Sources/InteroperabilityTests/Generated/messages.pb.swift @@ -0,0 +1,950 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: src/proto/grpc/testing/messages.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015-2016 gRPC authors. +// +// 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 definitions to be used by integration test service definitions. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The type of payload that should be returned. +public enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { + public typealias RawValue = Int + + /// Compressable text format. + case compressable // = 0 + case UNRECOGNIZED(Int) + + public init() { + self = .compressable + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .compressable + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .compressable: return 0 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Grpc_Testing_PayloadType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Grpc_Testing_PayloadType] = [ + .compressable, + ] +} + +#endif // swift(>=4.2) + +/// TODO(dgq): Go back to using well-known types once +/// https://github.com/grpc/grpc/issues/6980 has been fixed. +/// import "google/protobuf/wrappers.proto"; +public struct Grpc_Testing_BoolValue { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The bool value. + public var value: Bool = false + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// A block of data, to simply increase gRPC message size. +public struct Grpc_Testing_Payload { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The type of data in body. + public var type: Grpc_Testing_PayloadType = .compressable + + /// Primary contents of payload. + public var body: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// A protobuf representation for grpc status. This is used by test +/// clients to specify a status that the server should attempt to return. +public struct Grpc_Testing_EchoStatus { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var code: Int32 = 0 + + public var message: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// Unary request. +public struct Grpc_Testing_SimpleRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Desired payload type in the response from the server. + /// If response_type is RANDOM, server randomly chooses one from other formats. + public var responseType: Grpc_Testing_PayloadType = .compressable + + /// Desired payload size in the response from the server. + public var responseSize: Int32 = 0 + + /// Optional input payload sent along with the request. + public var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + public var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + public mutating func clearPayload() {self._payload = nil} + + /// Whether SimpleResponse should include username. + public var fillUsername: Bool = false + + /// Whether SimpleResponse should include OAuth scope. + public var fillOauthScope: Bool = false + + /// Whether to request the server to compress the response. This field is + /// "nullable" in order to interoperate seamlessly with clients not able to + /// implement the full compression tests by introspecting the call to verify + /// the response's compression status. + public var responseCompressed: Grpc_Testing_BoolValue { + get {return _responseCompressed ?? Grpc_Testing_BoolValue()} + set {_responseCompressed = newValue} + } + /// Returns true if `responseCompressed` has been explicitly set. + public var hasResponseCompressed: Bool {return self._responseCompressed != nil} + /// Clears the value of `responseCompressed`. Subsequent reads from it will return its default value. + public mutating func clearResponseCompressed() {self._responseCompressed = nil} + + /// Whether server should return a given status + public var responseStatus: Grpc_Testing_EchoStatus { + get {return _responseStatus ?? Grpc_Testing_EchoStatus()} + set {_responseStatus = newValue} + } + /// Returns true if `responseStatus` has been explicitly set. + public var hasResponseStatus: Bool {return self._responseStatus != nil} + /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. + public mutating func clearResponseStatus() {self._responseStatus = nil} + + /// Whether the server should expect this request to be compressed. + public var expectCompressed: Grpc_Testing_BoolValue { + get {return _expectCompressed ?? Grpc_Testing_BoolValue()} + set {_expectCompressed = newValue} + } + /// Returns true if `expectCompressed` has been explicitly set. + public var hasExpectCompressed: Bool {return self._expectCompressed != nil} + /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. + public mutating func clearExpectCompressed() {self._expectCompressed = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil + fileprivate var _responseCompressed: Grpc_Testing_BoolValue? = nil + fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil + fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil +} + +/// Unary response, as configured by the request. +public struct Grpc_Testing_SimpleResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Payload to increase message size. + public var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + public var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + public mutating func clearPayload() {self._payload = nil} + + /// The user the request came from, for verifying authentication was + /// successful when the client expected it. + public var username: String = String() + + /// OAuth scope. + public var oauthScope: String = String() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil +} + +/// Client-streaming request. +public struct Grpc_Testing_StreamingInputCallRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Optional input payload sent along with the request. + public var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + public var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + public mutating func clearPayload() {self._payload = nil} + + /// Whether the server should expect this request to be compressed. This field + /// is "nullable" in order to interoperate seamlessly with servers not able to + /// implement the full compression tests by introspecting the call to verify + /// the request's compression status. + public var expectCompressed: Grpc_Testing_BoolValue { + get {return _expectCompressed ?? Grpc_Testing_BoolValue()} + set {_expectCompressed = newValue} + } + /// Returns true if `expectCompressed` has been explicitly set. + public var hasExpectCompressed: Bool {return self._expectCompressed != nil} + /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. + public mutating func clearExpectCompressed() {self._expectCompressed = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil + fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil +} + +/// Client-streaming response. +public struct Grpc_Testing_StreamingInputCallResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Aggregated size of payloads received from the client. + public var aggregatedPayloadSize: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// Configuration for a particular response. +public struct Grpc_Testing_ResponseParameters { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Desired payload sizes in responses from the server. + public var size: Int32 = 0 + + /// Desired interval between consecutive responses in the response stream in + /// microseconds. + public var intervalUs: Int32 = 0 + + /// Whether to request the server to compress the response. This field is + /// "nullable" in order to interoperate seamlessly with clients not able to + /// implement the full compression tests by introspecting the call to verify + /// the response's compression status. + public var compressed: Grpc_Testing_BoolValue { + get {return _compressed ?? Grpc_Testing_BoolValue()} + set {_compressed = newValue} + } + /// Returns true if `compressed` has been explicitly set. + public var hasCompressed: Bool {return self._compressed != nil} + /// Clears the value of `compressed`. Subsequent reads from it will return its default value. + public mutating func clearCompressed() {self._compressed = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _compressed: Grpc_Testing_BoolValue? = nil +} + +/// Server-streaming request. +public struct Grpc_Testing_StreamingOutputCallRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Desired payload type in the response from the server. + /// If response_type is RANDOM, the payload from each response in the stream + /// might be of different types. This is to simulate a mixed type of payload + /// stream. + public var responseType: Grpc_Testing_PayloadType = .compressable + + /// Configuration for each expected response message. + public var responseParameters: [Grpc_Testing_ResponseParameters] = [] + + /// Optional input payload sent along with the request. + public var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + public var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + public mutating func clearPayload() {self._payload = nil} + + /// Whether server should return a given status + public var responseStatus: Grpc_Testing_EchoStatus { + get {return _responseStatus ?? Grpc_Testing_EchoStatus()} + set {_responseStatus = newValue} + } + /// Returns true if `responseStatus` has been explicitly set. + public var hasResponseStatus: Bool {return self._responseStatus != nil} + /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. + public mutating func clearResponseStatus() {self._responseStatus = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil + fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil +} + +/// Server-streaming response, as configured by the request and parameters. +public struct Grpc_Testing_StreamingOutputCallResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Payload to increase response size. + public var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + public var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + public mutating func clearPayload() {self._payload = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil +} + +/// For reconnect interop test only. +/// Client tells server what reconnection parameters it used. +public struct Grpc_Testing_ReconnectParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var maxReconnectBackoffMs: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// For reconnect interop test only. +/// Server tells client whether its reconnects are following the spec and the +/// reconnect backoffs it saw. +public struct Grpc_Testing_ReconnectInfo { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var passed: Bool = false + + public var backoffMs: [Int32] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Testing_PayloadType: @unchecked Sendable {} +extension Grpc_Testing_BoolValue: @unchecked Sendable {} +extension Grpc_Testing_Payload: @unchecked Sendable {} +extension Grpc_Testing_EchoStatus: @unchecked Sendable {} +extension Grpc_Testing_SimpleRequest: @unchecked Sendable {} +extension Grpc_Testing_SimpleResponse: @unchecked Sendable {} +extension Grpc_Testing_StreamingInputCallRequest: @unchecked Sendable {} +extension Grpc_Testing_StreamingInputCallResponse: @unchecked Sendable {} +extension Grpc_Testing_ResponseParameters: @unchecked Sendable {} +extension Grpc_Testing_StreamingOutputCallRequest: @unchecked Sendable {} +extension Grpc_Testing_StreamingOutputCallResponse: @unchecked Sendable {} +extension Grpc_Testing_ReconnectParams: @unchecked Sendable {} +extension Grpc_Testing_ReconnectInfo: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.testing" + +extension Grpc_Testing_PayloadType: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "COMPRESSABLE"), + ] +} + +extension Grpc_Testing_BoolValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".BoolValue" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.value) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.value != false { + try visitor.visitSingularBoolField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_BoolValue, rhs: Grpc_Testing_BoolValue) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_Payload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".Payload" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "body"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.body) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.type != .compressable { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) + } + if !self.body.isEmpty { + try visitor.visitSingularBytesField(value: self.body, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_Payload, rhs: Grpc_Testing_Payload) -> Bool { + if lhs.type != rhs.type {return false} + if lhs.body != rhs.body {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_EchoStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".EchoStatus" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "code"), + 2: .same(proto: "message"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.code != 0 { + try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) + } + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_EchoStatus, rhs: Grpc_Testing_EchoStatus) -> Bool { + if lhs.code != rhs.code {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_SimpleRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SimpleRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "response_type"), + 2: .standard(proto: "response_size"), + 3: .same(proto: "payload"), + 4: .standard(proto: "fill_username"), + 5: .standard(proto: "fill_oauth_scope"), + 6: .standard(proto: "response_compressed"), + 7: .standard(proto: "response_status"), + 8: .standard(proto: "expect_compressed"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.responseSize) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.fillUsername) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.fillOauthScope) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._responseCompressed) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() + case 8: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.responseType != .compressable { + try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) + } + if self.responseSize != 0 { + try visitor.visitSingularInt32Field(value: self.responseSize, fieldNumber: 2) + } + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if self.fillUsername != false { + try visitor.visitSingularBoolField(value: self.fillUsername, fieldNumber: 4) + } + if self.fillOauthScope != false { + try visitor.visitSingularBoolField(value: self.fillOauthScope, fieldNumber: 5) + } + try { if let v = self._responseCompressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + try { if let v = self._responseStatus { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try { if let v = self._expectCompressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_SimpleRequest, rhs: Grpc_Testing_SimpleRequest) -> Bool { + if lhs.responseType != rhs.responseType {return false} + if lhs.responseSize != rhs.responseSize {return false} + if lhs._payload != rhs._payload {return false} + if lhs.fillUsername != rhs.fillUsername {return false} + if lhs.fillOauthScope != rhs.fillOauthScope {return false} + if lhs._responseCompressed != rhs._responseCompressed {return false} + if lhs._responseStatus != rhs._responseStatus {return false} + if lhs._expectCompressed != rhs._expectCompressed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_SimpleResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".SimpleResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "payload"), + 2: .same(proto: "username"), + 3: .standard(proto: "oauth_scope"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.username) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.oauthScope) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.username.isEmpty { + try visitor.visitSingularStringField(value: self.username, fieldNumber: 2) + } + if !self.oauthScope.isEmpty { + try visitor.visitSingularStringField(value: self.oauthScope, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_SimpleResponse, rhs: Grpc_Testing_SimpleResponse) -> Bool { + if lhs._payload != rhs._payload {return false} + if lhs.username != rhs.username {return false} + if lhs.oauthScope != rhs.oauthScope {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingInputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "payload"), + 2: .standard(proto: "expect_compressed"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._expectCompressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_StreamingInputCallRequest, rhs: Grpc_Testing_StreamingInputCallRequest) -> Bool { + if lhs._payload != rhs._payload {return false} + if lhs._expectCompressed != rhs._expectCompressed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingInputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "aggregated_payload_size"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.aggregatedPayloadSize) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.aggregatedPayloadSize != 0 { + try visitor.visitSingularInt32Field(value: self.aggregatedPayloadSize, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_StreamingInputCallResponse, rhs: Grpc_Testing_StreamingInputCallResponse) -> Bool { + if lhs.aggregatedPayloadSize != rhs.aggregatedPayloadSize {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ResponseParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ResponseParameters" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "size"), + 2: .standard(proto: "interval_us"), + 3: .same(proto: "compressed"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.intervalUs) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._compressed) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.size != 0 { + try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) + } + if self.intervalUs != 0 { + try visitor.visitSingularInt32Field(value: self.intervalUs, fieldNumber: 2) + } + try { if let v = self._compressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_ResponseParameters, rhs: Grpc_Testing_ResponseParameters) -> Bool { + if lhs.size != rhs.size {return false} + if lhs.intervalUs != rhs.intervalUs {return false} + if lhs._compressed != rhs._compressed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingOutputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "response_type"), + 2: .standard(proto: "response_parameters"), + 3: .same(proto: "payload"), + 7: .standard(proto: "response_status"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.responseParameters) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.responseType != .compressable { + try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) + } + if !self.responseParameters.isEmpty { + try visitor.visitRepeatedMessageField(value: self.responseParameters, fieldNumber: 2) + } + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._responseStatus { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_StreamingOutputCallRequest, rhs: Grpc_Testing_StreamingOutputCallRequest) -> Bool { + if lhs.responseType != rhs.responseType {return false} + if lhs.responseParameters != rhs.responseParameters {return false} + if lhs._payload != rhs._payload {return false} + if lhs._responseStatus != rhs._responseStatus {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingOutputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "payload"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_StreamingOutputCallResponse, rhs: Grpc_Testing_StreamingOutputCallResponse) -> Bool { + if lhs._payload != rhs._payload {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ReconnectParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ReconnectParams" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "max_reconnect_backoff_ms"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.maxReconnectBackoffMs) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.maxReconnectBackoffMs != 0 { + try visitor.visitSingularInt32Field(value: self.maxReconnectBackoffMs, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_ReconnectParams, rhs: Grpc_Testing_ReconnectParams) -> Bool { + if lhs.maxReconnectBackoffMs != rhs.maxReconnectBackoffMs {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ReconnectInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ReconnectInfo" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "passed"), + 2: .standard(proto: "backoff_ms"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.passed) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.backoffMs) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.passed != false { + try visitor.visitSingularBoolField(value: self.passed, fieldNumber: 1) + } + if !self.backoffMs.isEmpty { + try visitor.visitPackedInt32Field(value: self.backoffMs, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Grpc_Testing_ReconnectInfo, rhs: Grpc_Testing_ReconnectInfo) -> Bool { + if lhs.passed != rhs.passed {return false} + if lhs.backoffMs != rhs.backoffMs {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift new file mode 100644 index 000000000..a13018c17 --- /dev/null +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -0,0 +1,918 @@ +// Copyright 2015-2016 gRPC authors. +// +// 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: src/proto/grpc/testing/test.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +public enum Grpc_Testing_ReconnectService { + public enum Method { + public enum Start { + public typealias Input = Grpc_Testing_ReconnectParams + public typealias Output = Grpc_Testing_Empty + public static let descriptor = MethodDescriptor( + service: "grpc.testing.ReconnectService", + method: "Start" + ) + } + public enum Stop { + public typealias Input = Grpc_Testing_Empty + public typealias Output = Grpc_Testing_ReconnectInfo + public static let descriptor = MethodDescriptor( + service: "grpc.testing.ReconnectService", + method: "Stop" + ) + } + public static let descriptors: [MethodDescriptor] = [ + Start.descriptor, + Stop.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = Grpc_Testing_ReconnectServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = Grpc_Testing_ReconnectServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = Grpc_Testing_ReconnectServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = Grpc_Testing_ReconnectServiceClient +} + +public enum Grpc_Testing_TestService { + public enum Method { + public enum EmptyCall { + public typealias Input = Grpc_Testing_Empty + public typealias Output = Grpc_Testing_Empty + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "EmptyCall" + ) + } + public enum UnaryCall { + public typealias Input = Grpc_Testing_SimpleRequest + public typealias Output = Grpc_Testing_SimpleResponse + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "UnaryCall" + ) + } + public enum CacheableUnaryCall { + public typealias Input = Grpc_Testing_SimpleRequest + public typealias Output = Grpc_Testing_SimpleResponse + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "CacheableUnaryCall" + ) + } + public enum StreamingOutputCall { + public typealias Input = Grpc_Testing_StreamingOutputCallRequest + public typealias Output = Grpc_Testing_StreamingOutputCallResponse + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "StreamingOutputCall" + ) + } + public enum StreamingInputCall { + public typealias Input = Grpc_Testing_StreamingInputCallRequest + public typealias Output = Grpc_Testing_StreamingInputCallResponse + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "StreamingInputCall" + ) + } + public enum FullDuplexCall { + public typealias Input = Grpc_Testing_StreamingOutputCallRequest + public typealias Output = Grpc_Testing_StreamingOutputCallResponse + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "FullDuplexCall" + ) + } + public enum HalfDuplexCall { + public typealias Input = Grpc_Testing_StreamingOutputCallRequest + public typealias Output = Grpc_Testing_StreamingOutputCallResponse + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "HalfDuplexCall" + ) + } + public enum UnimplementedCall { + public typealias Input = Grpc_Testing_Empty + public typealias Output = Grpc_Testing_Empty + public static let descriptor = MethodDescriptor( + service: "grpc.testing.TestService", + method: "UnimplementedCall" + ) + } + public static let descriptors: [MethodDescriptor] = [ + EmptyCall.descriptor, + UnaryCall.descriptor, + CacheableUnaryCall.descriptor, + StreamingOutputCall.descriptor, + StreamingInputCall.descriptor, + FullDuplexCall.descriptor, + HalfDuplexCall.descriptor, + UnimplementedCall.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = Grpc_Testing_TestServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = Grpc_Testing_TestServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = Grpc_Testing_TestServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = Grpc_Testing_TestServiceClient +} + +public enum Grpc_Testing_UnimplementedService { + public enum Method { + public enum UnimplementedCall { + public typealias Input = Grpc_Testing_Empty + public typealias Output = Grpc_Testing_Empty + public static let descriptor = MethodDescriptor( + service: "grpc.testing.UnimplementedService", + method: "UnimplementedCall" + ) + } + public static let descriptors: [MethodDescriptor] = [ + UnimplementedCall.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias StreamingServiceProtocol = Grpc_Testing_UnimplementedServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ServiceProtocol = Grpc_Testing_UnimplementedServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias ClientProtocol = Grpc_Testing_UnimplementedServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public typealias Client = Grpc_Testing_UnimplementedServiceClient +} + +/// A simple service to test the various types of RPCs and experiment with +/// performance with various types of payload. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// One empty request followed by one empty response. + func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// One request followed by one response. + func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// One request followed by one response. Response has cache control + /// headers set such that a caching HTTP proxy (such as GFE) can + /// satisfy subsequent requests. + func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// One request followed by a sequence of responses (streamed download). + /// The server returns the payload with client desired type and sizes. + func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// A sequence of requests followed by one response (streamed upload). + /// The server returns the aggregated size of client payload as the result. + func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// A sequence of requests with each request served by the server immediately. + /// As one request could lead to multiple responses, this interface + /// demonstrates the idea of full duplexing. + func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// A sequence of requests followed by a sequence of responses. + /// The server buffers all the client requests and then serves them in order. A + /// stream of responses are returned to the client when the server starts with + /// first request. + func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// The test server will not implement this method. It will be used + /// to test the behavior when clients call unimplemented methods. + func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_TestService.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.EmptyCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.emptyCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.UnaryCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.unaryCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.cacheableUnaryCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.streamingOutputCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.streamingInputCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.fullDuplexCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.halfDuplexCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.unimplementedCall(request: request) + } + ) + } +} + +/// A simple service to test the various types of RPCs and experiment with +/// performance with various types of payload. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestService.StreamingServiceProtocol { + /// One empty request followed by one empty response. + func emptyCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + + /// One request followed by one response. + func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + + /// One request followed by one response. Response has cache control + /// headers set such that a caching HTTP proxy (such as GFE) can + /// satisfy subsequent requests. + func cacheableUnaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + + /// One request followed by a sequence of responses (streamed download). + /// The server returns the payload with client desired type and sizes. + func streamingOutputCall(request: ServerRequest.Single) async throws -> ServerResponse.Stream + + /// A sequence of requests followed by one response (streamed upload). + /// The server returns the aggregated size of client payload as the result. + func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Single + + /// A sequence of requests with each request served by the server immediately. + /// As one request could lead to multiple responses, this interface + /// demonstrates the idea of full duplexing. + func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// A sequence of requests followed by a sequence of responses. + /// The server buffers all the client requests and then serves them in order. A + /// stream of responses are returned to the client when the server starts with + /// first request. + func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// The test server will not implement this method. It will be used + /// to test the behavior when clients call unimplemented methods. + func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single +} + +/// Partial conformance to `Grpc_Testing_TestServiceStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_TestService.ServiceProtocol { + public func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.emptyCall(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + public func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + public func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.cacheableUnaryCall(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + public func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.streamingOutputCall(request: ServerRequest.Single(stream: request)) + return response + } + + public func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.streamingInputCall(request: request) + return ServerResponse.Stream(single: response) + } + + public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } +} + +/// A simple service NOT implemented at servers so clients can test for +/// that case. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_UnimplementedServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// A call that no server should implement + func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.unimplementedCall(request: request) + } + ) + } +} + +/// A simple service NOT implemented at servers so clients can test for +/// that case. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_UnimplementedServiceServiceProtocol: Grpc_Testing_UnimplementedService.StreamingServiceProtocol { + /// A call that no server should implement + func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single +} + +/// Partial conformance to `Grpc_Testing_UnimplementedServiceStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_UnimplementedService.ServiceProtocol { + public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } +} + +/// A service used to control reconnect server. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Grpc_Testing_ReconnectService.Method.Start.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.start(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_ReconnectService.Method.Stop.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.stop(request: request) + } + ) + } +} + +/// A service used to control reconnect server. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_ReconnectService.StreamingServiceProtocol { + func start(request: ServerRequest.Single) async throws -> ServerResponse.Single + + func stop(request: ServerRequest.Single) async throws -> ServerResponse.Single +} + +/// Partial conformance to `Grpc_Testing_ReconnectServiceStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_ReconnectService.ServiceProtocol { + public func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.start(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + public func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.stop(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } +} + +/// A simple service to test the various types of RPCs and experiment with +/// performance with various types of payload. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { + /// One empty request followed by one empty response. + func emptyCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// One request followed by one response. + func unaryCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// One request followed by one response. Response has cache control + /// headers set such that a caching HTTP proxy (such as GFE) can + /// satisfy subsequent requests. + func cacheableUnaryCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// One request followed by a sequence of responses (streamed download). + /// The server returns the payload with client desired type and sizes. + func streamingOutputCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + /// A sequence of requests followed by one response (streamed upload). + /// The server returns the aggregated size of client payload as the result. + func streamingInputCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// A sequence of requests with each request served by the server immediately. + /// As one request could lead to multiple responses, this interface + /// demonstrates the idea of full duplexing. + func fullDuplexCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + /// A sequence of requests followed by a sequence of responses. + /// The server buffers all the client requests and then serves them in order. A + /// stream of responses are returned to the client when the server starts with + /// first request. + func halfDuplexCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + /// The test server will not implement this method. It will be used + /// to test the behavior when clients call unimplemented methods. + func unimplementedCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_TestService.ClientProtocol { + public func emptyCall( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.emptyCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func unaryCall( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.unaryCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func cacheableUnaryCall( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.cacheableUnaryCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func streamingOutputCall( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.streamingOutputCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func streamingInputCall( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.streamingInputCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func fullDuplexCall( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.fullDuplexCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func halfDuplexCall( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.halfDuplexCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func unimplementedCall( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.unimplementedCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } +} + +/// A simple service to test the various types of RPCs and experiment with +/// performance with various types of payload. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientProtocol { + private let client: GRPCCore.GRPCClient + + public init(client: GRPCCore.GRPCClient) { + self.client = client + } + + /// One empty request followed by one empty response. + public func emptyCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_TestService.Method.EmptyCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// One request followed by one response. + public func unaryCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_TestService.Method.UnaryCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// One request followed by one response. Response has cache control + /// headers set such that a caching HTTP proxy (such as GFE) can + /// satisfy subsequent requests. + public func cacheableUnaryCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// One request followed by a sequence of responses (streamed download). + /// The server returns the payload with client desired type and sizes. + public func streamingOutputCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// A sequence of requests followed by one response (streamed upload). + /// The server returns the aggregated size of client payload as the result. + public func streamingInputCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// A sequence of requests with each request served by the server immediately. + /// As one request could lead to multiple responses, this interface + /// demonstrates the idea of full duplexing. + public func fullDuplexCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// A sequence of requests followed by a sequence of responses. + /// The server buffers all the client requests and then serves them in order. A + /// stream of responses are returned to the client when the server starts with + /// first request. + public func halfDuplexCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// The test server will not implement this method. It will be used + /// to test the behavior when clients call unimplemented methods. + public func unimplementedCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } +} + +/// A simple service NOT implemented at servers so clients can test for +/// that case. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { + /// A call that no server should implement + func unimplementedCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_UnimplementedService.ClientProtocol { + public func unimplementedCall( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.unimplementedCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } +} + +/// A simple service NOT implemented at servers so clients can test for +/// that case. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_UnimplementedService.ClientProtocol { + private let client: GRPCCore.GRPCClient + + public init(client: GRPCCore.GRPCClient) { + self.client = client + } + + /// A call that no server should implement + public func unimplementedCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } +} + +/// A service used to control reconnect server. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { + func start( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + func stop( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_ReconnectService.ClientProtocol { + public func start( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.start( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + public func stop( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.stop( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } +} + +/// A service used to control reconnect server. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService.ClientProtocol { + private let client: GRPCCore.GRPCClient + + public init(client: GRPCCore.GRPCClient) { + self.client = client + } + + public func start( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_ReconnectService.Method.Start.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + public func stop( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_ReconnectService.Method.Stop.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } +} \ No newline at end of file diff --git a/Sources/InteroperabilityTests/Generated/test.pb.swift b/Sources/InteroperabilityTests/Generated/test.pb.swift new file mode 100644 index 000000000..8fb71aea8 --- /dev/null +++ b/Sources/InteroperabilityTests/Generated/test.pb.swift @@ -0,0 +1,38 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: src/proto/grpc/testing/test.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015-2016 gRPC authors. +// +// 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} From ae7907a11fe40ce7ec2117c5ae760eb4e3399344 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 16 Feb 2024 08:45:11 +0000 Subject: [PATCH 250/580] Add name resolver protocols and registry (#1807) Motivation: When creating a client users will provide a target to connect to. This is usually a host to resolve via DNS but may be a Unix domain socket, pre-resolved IP address or something custom. Users get a resolver from a registry by asking it to resolve a given target. Resolvers then provide a set of endpoints and a service configuration. These values can change over time. Modifications: - Add the `NameResolver` struct which provides an `AsyncSequence` of `NameResolutionResult`s. - Add `NameResolutionResult` which provides an array of `Endpoint`s and a `ServiceConfiguration`. - Add the `NameResolverFactory` protocol which returns a `NameResolver` for a given resolvable target - Add the `ResolvableTarget` protocol - Add the `NameResolverRegistry` which holds a set of name resolver factories and can return a `NameResolver` for a given target Note, the actual resolver factories and targets will be added in a later PR. Result: All the boilerplate is in place for name resolvers. --- .../Client/Resolver/NameResolver.swift | 129 +++++++++++++++ .../Resolver/NameResolverRegistry.swift | 151 ++++++++++++++++++ .../Resolver/NameResolverRegistryTests.swift | 131 +++++++++++++++ .../Test Utilities/XCTest+Utilities.swift | 21 +++ 4 files changed, 432 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift new file mode 100644 index 000000000..4ebb23ce5 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.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. + */ + +import GRPCCore + +/// A name resolver can provide resolved addresses and service configuration values over time. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct NameResolver: Sendable { + /// A sequence of name resolution results. + /// + /// Resolvers may be push or pull based. Resolvers with the ``UpdateMode-swift.enum/push`` + /// update mode have addresses pushed to them by an external source and you should subscribe + /// to changes in addresses by awaiting for new values in a loop. + /// + /// Resolvers with the ``UpdateMode-swift.enum/pull`` update mode shouldn't be subscribed to, + /// instead you should create an iterator and ask for new results as and when necessary. + public var names: RPCAsyncSequence + + /// How ``names`` is updated and should be consumed. + public let updateMode: UpdateMode + + public struct UpdateMode: Hashable, Sendable { + enum Value: Hashable, Sendable { + case push + case pull + } + + let value: Value + + private init(_ value: Value) { + self.value = value + } + + /// Addresses are pushed to the resolve by an external source. + public static var push: Self { Self(.push) } + + /// Addresses are resolved lazily, when the caller asks them to be resolved. + public static var pull: Self { Self(.pull) } + } + + /// Create a new name resolver. + public init(results: RPCAsyncSequence, updateMode: UpdateMode) { + self.names = results + self.updateMode = updateMode + } +} + +/// The result of name resolution, a list of endpoints to connect to and the service +/// configuration reported by the resolver. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct NameResolutionResult: Hashable, Sendable { + /// A list of endpoints to connect to. + public var endpoints: [Endpoint] + + /// The service configuration reported by the resolver, or an error if it couldn't be parsed. + public var serviceConfiguration: Result + + public init( + endpoints: [Endpoint], + serviceConfiguration: Result + ) { + self.endpoints = endpoints + self.serviceConfiguration = serviceConfiguration + } +} + +/// A group of addresses which are considered equivalent when establishing a connection. +public struct Endpoint: Hashable, Sendable { + /// A list of equivalent addresses. + /// + /// Earlier addresses are typically but not always connected to first. Some load balancers may + /// choose to ignore the order. + public var addresses: [SocketAddress] + + /// Create a new ``Endpoint``. + /// - Parameter addresses: A list of equivalent addresses. + public init(addresses: [SocketAddress]) { + self.addresses = addresses + } +} + +/// A resolver capable of resolving targets of type ``Target``. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol NameResolverFactory { + /// The type of ``ResolvableTarget`` this factory makes resolvers for. + associatedtype Target: ResolvableTarget + + /// Creates a resolver for the given target. + /// + /// - Parameter target: The target to make a resolver for. + /// - Returns: The name resolver for the target. + func resolver(for target: Target) -> NameResolver +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension NameResolverFactory { + /// Returns whether the given target is compatible with this factory. + /// + /// - Parameter target: The target to check the compatibility of. + /// - Returns: Whether the target is compatible with this factory. + func isCompatible(withTarget target: Other) -> Bool { + return target is Target + } + + /// Returns a name resolver if the given target is compatible. + /// + /// - Parameter target: The target to make a name resolver for. + /// - Returns: A name resolver or `nil` if the target isn't compatible. + func makeResolverIfCompatible(_ target: Other) -> NameResolver? { + guard let target = target as? Target else { return nil } + return self.resolver(for: target) + } +} + +/// A target which can be resolved to a ``SocketAddress``. +public protocol ResolvableTarget {} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift new file mode 100644 index 000000000..a45ced024 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift @@ -0,0 +1,151 @@ +/* + * 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. + */ + +/// A registry for name resolver factories. +/// +/// The registry provides name resolvers for resolvable targets. You can control which name +/// resolvers are available by registering and removing resolvers by type. The following code +/// demonstrates how to create a registry, add and remove resolver factories, and create a resolver. +/// +/// ```swift +/// // Create a new resolver registry with the default resolvers. +/// var registry = NameResolverRegistry.defaults +/// +/// // Register a custom resolver, the registry can now resolve targets of +/// // type `CustomResolver.ResolvableTarget`. +/// registry.registerFactory(CustomResolver()) +/// +/// // Remove the Unix Domain Socket and VSOCK resolvers, if they exist. +/// registry.removeFactory(ofType: NameResolvers.UnixDomainSocket.self) +/// registry.removeFactory(ofType: NameResolvers.VSOCK.self) +/// +/// // Resolve an IPv4 target +/// if let resolver = registry.makeResolver(for: .ipv4(host: "localhost", port: 80)) { +/// // ... +/// } +/// ``` +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct NameResolverRegistry { + private enum Factory { + case other(any NameResolverFactory) + + init(_ factory: some NameResolverFactory) { + self = .other(factory) + } + + func makeResolverIfCompatible(_ target: Target) -> NameResolver? { + switch self { + case .other(let factory): + return factory.makeResolverIfCompatible(target) + } + } + + func hasTarget(_ target: Target) -> Bool { + switch self { + case .other(let factory): + return factory.isCompatible(withTarget: target) + } + } + + func `is`(ofType factoryType: Factory.Type) -> Bool { + switch self { + case .other(let factory): + return type(of: factory) == factoryType + } + } + } + + private var factories: [Factory] + + /// Creates a new name resolver registry with no resolve factories. + public init() { + self.factories = [] + } + + /// Returns a new name resolver registry with the default factories registered. + public static var defaults: Self { + return NameResolverRegistry() + } + + /// The number of resolver factories in the registry. + public var count: Int { + return self.factories.count + } + + /// Whether there are no resolver factories in the registry. + public var isEmpty: Bool { + return self.factories.isEmpty + } + + /// Registers a new name resolver factory. + /// + /// Any factories of the same type are removed prior to inserting the factory. + /// + /// - Parameter factory: The factory to register. + public mutating func registerFactory(_ factory: Factory) { + self.removeFactory(ofType: Factory.self) + self.factories.append(Self.Factory(factory)) + } + + /// Removes any factories which have the given type + /// + /// - Parameter type: The type of factory to remove. + /// - Returns: Whether a factory was removed. + @discardableResult + public mutating func removeFactory( + ofType type: Factory.Type + ) -> Bool { + let factoryCount = self.factories.count + self.factories.removeAll { + $0.is(ofType: Factory.self) + } + return self.factories.count < factoryCount + } + + /// Returns whether the registry contains a factory of the given type. + /// + /// - Parameter type: The type of factory to look for. + /// - Returns: Whether the registry contained the factory of the given type. + public func containsFactory(ofType type: Factory.Type) -> Bool { + self.factories.contains { + $0.is(ofType: Factory.self) + } + } + + /// Returns whether the registry contains a factory capable of resolving the given target. + /// + /// - Parameter target: + /// - Returns: Whether the registry contains a resolve capable of resolving the target. + public func containsFactory(capableOfResolving target: some ResolvableTarget) -> Bool { + self.factories.contains { $0.hasTarget(target) } + } + + /// Makes a ``NameResolver`` for the target, if a suitable factory exists. + /// + /// If multiple factories exist which are capable of resolving the target then the first + /// is used. + /// + /// - Parameter target: The target to make a resolver for. + /// - Returns: The resolver, or `nil` if no factory could make a resolver for the target. + public func makeResolver(for target: some ResolvableTarget) -> NameResolver? { + for factory in self.factories { + if let resolver = factory.makeResolverIfCompatible(target) { + return resolver + } + } + return nil + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift new file mode 100644 index 000000000..e3402f438 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift @@ -0,0 +1,131 @@ +/* + * 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 GRPCHTTP2Core +import XCTest + +final class NameResolverRegistryTests: XCTestCase { + struct FailingResolver: NameResolverFactory { + typealias Target = StringTarget + + private let code: RPCError.Code + + init(code: RPCError.Code = .unavailable) { + self.code = code + } + + func resolver(for target: NameResolverRegistryTests.StringTarget) -> NameResolver { + let stream = AsyncThrowingStream(NameResolutionResult.self) { + $0.yield(with: .failure(RPCError(code: self.code, message: target.value))) + } + + return NameResolver(results: RPCAsyncSequence(wrapping: stream), updateMode: .pull) + } + } + + struct StringTarget: ResolvableTarget { + var value: String + + init(value: String) { + self.value = value + } + } + + func testEmptyNameResolvers() { + let resolvers = NameResolverRegistry() + XCTAssert(resolvers.isEmpty) + XCTAssertEqual(resolvers.count, 0) + } + + func testRegisterFactory() async throws { + var resolvers = NameResolverRegistry() + resolvers.registerFactory(FailingResolver(code: .unknown)) + XCTAssertEqual(resolvers.count, 1) + + do { + let resolver = resolvers.makeResolver(for: StringTarget(value: "foo")) + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + var iterator = resolver?.names.makeAsyncIterator() + _ = try await iterator?.next() + } errorHandler: { error in + XCTAssertEqual(error.code, .unknown) + } + } + + // Adding a resolver of the same type replaces it. Use the code of the thrown error to + // distinguish between the instances. + resolvers.registerFactory(FailingResolver(code: .cancelled)) + XCTAssertEqual(resolvers.count, 1) + + do { + let resolver = resolvers.makeResolver(for: StringTarget(value: "foo")) + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + var iterator = resolver?.names.makeAsyncIterator() + _ = try await iterator?.next() + } errorHandler: { error in + XCTAssertEqual(error.code, .cancelled) + } + } + } + + func testRemoveFactory() { + var resolvers = NameResolverRegistry() + resolvers.registerFactory(FailingResolver()) + XCTAssertEqual(resolvers.count, 1) + + resolvers.removeFactory(ofType: FailingResolver.self) + XCTAssertEqual(resolvers.count, 0) + + // Removing an unknown factory is a no-op. + resolvers.removeFactory(ofType: FailingResolver.self) + XCTAssertEqual(resolvers.count, 0) + } + + func testContainsFactoryOfType() { + var resolvers = NameResolverRegistry() + XCTAssertFalse(resolvers.containsFactory(ofType: FailingResolver.self)) + + resolvers.registerFactory(FailingResolver()) + XCTAssertTrue(resolvers.containsFactory(ofType: FailingResolver.self)) + } + + func testContainsFactoryCapableOfResolving() { + var resolvers = NameResolverRegistry() + XCTAssertFalse(resolvers.containsFactory(capableOfResolving: StringTarget(value: ""))) + + resolvers.registerFactory(FailingResolver()) + XCTAssertTrue(resolvers.containsFactory(capableOfResolving: StringTarget(value: ""))) + } + + func testMakeFailingResolver() async throws { + var resolvers = NameResolverRegistry() + XCTAssertNil(resolvers.makeResolver(for: StringTarget(value: ""))) + + resolvers.registerFactory(FailingResolver()) + + let resolver = try XCTUnwrap(resolvers.makeResolver(for: StringTarget(value: "foo"))) + XCTAssertEqual(resolver.updateMode, .pull) + + var iterator = resolver.names.makeAsyncIterator() + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + try await iterator.next() + } errorHandler: { error in + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(error.message, "foo") + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift index 752f12a9a..b02289ee2 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift @@ -37,3 +37,24 @@ func XCTAssertDescription( ) { XCTAssertEqual(String(describing: subject), expected, file: file, line: line) } + +func XCTUnwrapAsync(_ expression: () async throws -> T?) async throws -> T { + let value = try await expression() + return try XCTUnwrap(value) +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +func XCTAssertThrowsErrorAsync( + ofType: E.Type = E.self, + _ expression: () async throws -> T, + errorHandler: (E) -> Void +) async { + do { + _ = try await expression() + XCTFail("Expression didn't throw") + } catch let error as E { + errorHandler(error) + } catch { + XCTFail("Error had unexpected type '\(type(of: error))'") + } +} From f9facfd40101316be3c611724601a97139740142 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Feb 2024 12:18:04 +0100 Subject: [PATCH 251/580] Add missing availability annotations in tests (#1813) --- .../Client/Resolver/NameResolverRegistryTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift index e3402f438..79e0d0c33 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift @@ -18,6 +18,7 @@ import GRPCCore import GRPCHTTP2Core import XCTest +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class NameResolverRegistryTests: XCTestCase { struct FailingResolver: NameResolverFactory { typealias Target = StringTarget From bc684cccb4975861e53a4f43c1fdf99ff3c1084e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Feb 2024 13:52:46 +0100 Subject: [PATCH 252/580] Add IPv4 and IPv6 resolvers (#1810) Motivation: Users must be able to specify already-resolved IPv4 and IPv6 addresses to the name resolver registry and have it return a name resolver. These resolvers are effectively passthrough resolvers which attach a service configuration to the provided addresses. Modifications: - Add IPv4 and IPv6 targets and resolvers Result: - Name resolver factory can resolve IP addresses --- .../Client/Resolver/NameResolver+IPv4.swift | 75 ++++++++++++ .../Client/Resolver/NameResolver+IPv6.swift | 75 ++++++++++++ .../Client/Resolver/NameResolver.swift | 15 ++- .../Resolver/NameResolverRegistry.swift | 31 ++++- .../Internal/ConstantAsyncSequence.swift | 48 ++++++++ .../Resolver/NameResolverRegistryTests.swift | 108 +++++++++++++++++- 6 files changed, 345 insertions(+), 7 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift new file mode 100644 index 000000000..e4dffcfaf --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift @@ -0,0 +1,75 @@ +/* + * 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 + +extension ResolvableTargets { + /// A resolvable target for IPv4 addresses. + /// + /// IPv4 addresses can be resolved by the ``NameResolvers/IPv4`` resolver which creates a + /// separate ``Endpoint`` for each address. + public struct IPv4: ResolvableTarget { + /// The IPv4 addresses. + public var addresses: [SocketAddress.IPv4] + + /// Create a new IPv4 target. + /// - Parameter addresses: The IPv4 addresses. + public init(addresses: [SocketAddress.IPv4]) { + self.addresses = addresses + } + } +} + +extension ResolvableTarget where Self == ResolvableTargets.IPv4 { + /// Creates a new resolvable IPv4 target for a single address. + /// - Parameters: + /// - host: The host address. + /// - port: The port on the host. + /// - Returns: A ``ResolvableTarget``. + public static func ipv4(host: String, port: Int = 443) -> Self { + let address = SocketAddress.IPv4(host: host, port: port) + return Self(addresses: [address]) + } + + /// Creates a new resolvable IPv4 target from the provided host-port pairs. + /// + /// - Parameter pairs: An array of host-port pairs. + /// - Returns: A ``ResolvableTarget``. + public static func ipv4(pairs: [(host: String, port: Int)]) -> Self { + let address = pairs.map { SocketAddress.IPv4(host: $0.host, port: $0.port) } + return Self(addresses: address) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension NameResolvers { + /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv4`` targets. + /// + /// The name resolver for a given target always produces the same values, with one endpoint per + /// address in the target. This resolver doesn't support fetching service configuration. + public struct IPv4: NameResolverFactory { + public typealias Target = ResolvableTargets.IPv4 + + /// Create a new IPv4 resolver factory. + public init() {} + + public func resolver(for target: Target) -> NameResolver { + let endpoints = target.addresses.map { Endpoint(addresses: [.ipv4($0)]) } + let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfiguration: nil) + return NameResolver(names: .constant(resolutionResult), updateMode: .pull) + } + } +} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift new file mode 100644 index 000000000..10348e44d --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift @@ -0,0 +1,75 @@ +/* + * 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 + +extension ResolvableTargets { + /// A resolvable target for IPv4 addresses. + /// + /// IPv4 addresses can be resolved by the ``NameResolvers/IPv6`` resolver which creates a + /// separate ``Endpoint`` for each address. + public struct IPv6: ResolvableTarget { + /// The IPv6 addresses. + public var addresses: [SocketAddress.IPv6] + + /// Create a new IPv6 target. + /// - Parameter addresses: The IPv6 addresses. + public init(addresses: [SocketAddress.IPv6]) { + self.addresses = addresses + } + } +} + +extension ResolvableTarget where Self == ResolvableTargets.IPv6 { + /// Creates a new resolvable IPv6 target for a single address. + /// - Parameters: + /// - host: The host address. + /// - port: The port on the host. + /// - Returns: A ``ResolvableTarget``. + public static func ipv6(host: String, port: Int = 443) -> Self { + let address = SocketAddress.IPv6(host: host, port: port) + return Self(addresses: [address]) + } + + /// Creates a new resolvable IPv6 target from the provided host-port pairs. + /// + /// - Parameter pairs: An array of host-port pairs. + /// - Returns: A ``ResolvableTarget``. + public static func ipv6(pairs: [(host: String, port: Int)]) -> Self { + let address = pairs.map { SocketAddress.IPv6(host: $0.host, port: $0.port) } + return Self(addresses: address) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension NameResolvers { + /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv6`` targets. + /// + /// The name resolver for a given target always produces the same values, with one endpoint per + /// address in the target. This resolver doesn't support fetching service configuration. + public struct IPv6: NameResolverFactory { + public typealias Target = ResolvableTargets.IPv6 + + /// Create a new IPv6 resolver factory. + public init() {} + + public func resolver(for target: Target) -> NameResolver { + let endpoints = target.addresses.map { Endpoint(addresses: [.ipv6($0)]) } + let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfiguration: nil) + return NameResolver(names: .constant(resolutionResult), updateMode: .pull) + } + } +} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift index 4ebb23ce5..4887b20c4 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift @@ -52,8 +52,8 @@ public struct NameResolver: Sendable { } /// Create a new name resolver. - public init(results: RPCAsyncSequence, updateMode: UpdateMode) { - self.names = results + public init(names: RPCAsyncSequence, updateMode: UpdateMode) { + self.names = names self.updateMode = updateMode } } @@ -66,11 +66,12 @@ public struct NameResolutionResult: Hashable, Sendable { public var endpoints: [Endpoint] /// The service configuration reported by the resolver, or an error if it couldn't be parsed. - public var serviceConfiguration: Result + /// This value may be `nil` if the resolver doesn't support fetching service configuration. + public var serviceConfiguration: Result? public init( endpoints: [Endpoint], - serviceConfiguration: Result + serviceConfiguration: Result? ) { self.endpoints = endpoints self.serviceConfiguration = serviceConfiguration @@ -127,3 +128,9 @@ extension NameResolverFactory { /// A target which can be resolved to a ``SocketAddress``. public protocol ResolvableTarget {} + +/// A namespace for resolvable targets. +public enum ResolvableTargets {} + +/// A namespace for name resolver factories. +public enum NameResolvers {} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift index a45ced024..479352279 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift @@ -40,14 +40,26 @@ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct NameResolverRegistry { private enum Factory { + case ipv4(NameResolvers.IPv4) + case ipv6(NameResolvers.IPv6) case other(any NameResolverFactory) init(_ factory: some NameResolverFactory) { - self = .other(factory) + if let ipv4 = factory as? NameResolvers.IPv4 { + self = .ipv4(ipv4) + } else if let ipv6 = factory as? NameResolvers.IPv6 { + self = .ipv6(ipv6) + } else { + self = .other(factory) + } } func makeResolverIfCompatible(_ target: Target) -> NameResolver? { switch self { + case .ipv4(let factory): + return factory.makeResolverIfCompatible(target) + case .ipv6(let factory): + return factory.makeResolverIfCompatible(target) case .other(let factory): return factory.makeResolverIfCompatible(target) } @@ -55,6 +67,10 @@ public struct NameResolverRegistry { func hasTarget(_ target: Target) -> Bool { switch self { + case .ipv4(let factory): + return factory.isCompatible(withTarget: target) + case .ipv6(let factory): + return factory.isCompatible(withTarget: target) case .other(let factory): return factory.isCompatible(withTarget: target) } @@ -62,6 +78,10 @@ public struct NameResolverRegistry { func `is`(ofType factoryType: Factory.Type) -> Bool { switch self { + case .ipv4: + return NameResolvers.IPv4.self == factoryType + case .ipv6: + return NameResolvers.IPv6.self == factoryType case .other(let factory): return type(of: factory) == factoryType } @@ -76,8 +96,15 @@ public struct NameResolverRegistry { } /// Returns a new name resolver registry with the default factories registered. + /// + /// The default resolvers include: + /// - ``NameResolvers/IPv4``, + /// - ``NameResolvers/IPv6``. public static var defaults: Self { - return NameResolverRegistry() + var resolvers = NameResolverRegistry() + resolvers.registerFactory(NameResolvers.IPv4()) + resolvers.registerFactory(NameResolvers.IPv6()) + return resolvers } /// The number of resolver factories in the registry. diff --git a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift new file mode 100644 index 000000000..b4e0ba843 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift @@ -0,0 +1,48 @@ +/* + * 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 + +private struct ConstantAsyncSequence: AsyncSequence { + private let element: Element + + init(element: Element) { + self.element = element + } + + func makeAsyncIterator() -> AsyncIterator { + return AsyncIterator(element: self.element) + } + + struct AsyncIterator: AsyncIteratorProtocol { + private let element: Element + + fileprivate init(element: Element) { + self.element = element + } + + func next() async throws -> Element? { + return self.element + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension RPCAsyncSequence { + static func constant(_ element: Element) -> RPCAsyncSequence { + return RPCAsyncSequence(wrapping: ConstantAsyncSequence(element: element)) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift index 79e0d0c33..f0cd3e584 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift @@ -34,7 +34,7 @@ final class NameResolverRegistryTests: XCTestCase { $0.yield(with: .failure(RPCError(code: self.code, message: target.value))) } - return NameResolver(results: RPCAsyncSequence(wrapping: stream), updateMode: .pull) + return NameResolver(names: RPCAsyncSequence(wrapping: stream), updateMode: .pull) } } @@ -129,4 +129,110 @@ final class NameResolverRegistryTests: XCTestCase { XCTAssertEqual(error.message, "foo") } } + + func testDefaultResolvers() { + let resolvers = NameResolverRegistry.defaults + XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv4.self)) + XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv6.self)) + } + + func testMakeResolver() { + let resolvers = NameResolverRegistry() + XCTAssertNil(resolvers.makeResolver(for: .ipv4(host: "foo"))) + } + + func testCustomResolver() async throws { + struct EmptyTarget: ResolvableTarget { + static var scheme: String { "empty" } + } + + struct CustomResolver: NameResolverFactory { + func resolver(for target: EmptyTarget) -> NameResolver { + return NameResolver( + names: RPCAsyncSequence(wrapping: AsyncStream { $0.finish() }), + updateMode: .push + ) + } + } + + var resolvers = NameResolverRegistry.defaults + resolvers.registerFactory(CustomResolver()) + let resolver = try XCTUnwrap(resolvers.makeResolver(for: EmptyTarget())) + XCTAssertEqual(resolver.updateMode, .push) + for try await _ in resolver.names { + XCTFail("Expected an empty sequence") + } + } + + func testIPv4ResolverForSingleHost() async throws { + let factory = NameResolvers.IPv4() + let resolver = factory.resolver(for: .ipv4(host: "foo", port: 1234)) + + XCTAssertEqual(resolver.updateMode, .pull) + + // The IPv4 resolver always returns the same values. + var iterator = resolver.names.makeAsyncIterator() + for _ in 0 ..< 1000 { + let result = try await XCTUnwrapAsync { try await iterator.next() } + XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.ipv4(host: "foo", port: 1234)])]) + XCTAssertNil(result.serviceConfiguration) + } + } + + func testIPv4ResolverForMultipleHosts() async throws { + let factory = NameResolvers.IPv4() + let resolver = factory.resolver(for: .ipv4(pairs: [("foo", 443), ("bar", 444)])) + + XCTAssertEqual(resolver.updateMode, .pull) + + // The IPv4 resolver always returns the same values. + var iterator = resolver.names.makeAsyncIterator() + for _ in 0 ..< 1000 { + let result = try await XCTUnwrapAsync { try await iterator.next() } + XCTAssertEqual( + result.endpoints, + [ + Endpoint(addresses: [.ipv4(host: "foo", port: 443)]), + Endpoint(addresses: [.ipv4(host: "bar", port: 444)]), + ] + ) + XCTAssertNil(result.serviceConfiguration) + } + } + + func testIPv6ResolverForSingleHost() async throws { + let factory = NameResolvers.IPv6() + let resolver = factory.resolver(for: .ipv6(host: "foo", port: 1234)) + + XCTAssertEqual(resolver.updateMode, .pull) + + // The IPv6 resolver always returns the same values. + var iterator = resolver.names.makeAsyncIterator() + for _ in 0 ..< 1000 { + let result = try await XCTUnwrapAsync { try await iterator.next() } + XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.ipv6(host: "foo", port: 1234)])]) + XCTAssertNil(result.serviceConfiguration) + } + } + + func testIPv6ResolverForMultipleHosts() async throws { + let factory = NameResolvers.IPv6() + let resolver = factory.resolver(for: .ipv6(pairs: [("foo", 443), ("bar", 444)])) + + XCTAssertEqual(resolver.updateMode, .pull) + + // The IPv6 resolver always returns the same values. + var iterator = resolver.names.makeAsyncIterator() + for _ in 0 ..< 1000 { + let result = try await XCTUnwrapAsync { try await iterator.next() } + XCTAssertEqual( + result.endpoints, + [ + Endpoint(addresses: [.ipv6(host: "foo", port: 443)]), + Endpoint(addresses: [.ipv6(host: "bar", port: 444)]), + ] + ) + XCTAssertNil(result.serviceConfiguration) + } + } } From 7469629ce5fc26d73b3742558eba1418d9915776 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Feb 2024 14:38:49 +0100 Subject: [PATCH 253/580] Add UnixDomainSocket resolver (#1814) Motivation: Users should be able to connect to Unix Domain Sockets. To do these they need a UDS target and resolver. Modifications: - Add a UDS target and resolver - Update the registry to include it by default Result: Can resolve UDS targets --- .../Client/Resolver/NameResolver+UDS.swift | 60 +++++++++++++++++++ .../Resolver/NameResolverRegistry.swift | 13 +++- .../Internal/ConstantAsyncSequence.swift | 1 + .../Resolver/NameResolverRegistryTests.swift | 17 ++++++ 4 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift new file mode 100644 index 000000000..403c6a0a3 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift @@ -0,0 +1,60 @@ +/* + * 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 + +extension ResolvableTargets { + /// A resolvable target for Unix Domain Socket address. + /// + /// ``UnixDomainSocket`` addresses can be resolved by the ``NameResolvers/UnixDomainSocket`` + /// resolver which creates a single ``Endpoint`` for target address. + public struct UnixDomainSocket: ResolvableTarget { + /// The Unix Domain Socket address. + public var address: SocketAddress.UnixDomainSocket + + /// Create a new Unix Domain Socket address. + public init(address: SocketAddress.UnixDomainSocket) { + self.address = address + } + } +} + +extension ResolvableTarget where Self == ResolvableTargets.UnixDomainSocket { + /// Creates a new resolvable Unix Domain Socket target. + /// - Parameter path: The path of the socket. + public static func unixDomainSocket(path: String) -> Self { + return Self(address: SocketAddress.UnixDomainSocket(path: path)) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension NameResolvers { + /// A ``NameResolverFactory`` for ``ResolvableTargets/UnixDomainSocket`` targets. + /// + /// The name resolver for a given target always produces the same values, with a single endpoint. + /// This resolver doesn't support fetching service configuration. + public struct UnixDomainSocket: NameResolverFactory { + public typealias Target = ResolvableTargets.UnixDomainSocket + + public init() {} + + public func resolver(for target: Target) -> NameResolver { + let endpoint = Endpoint(addresses: [.unixDomainSocket(target.address)]) + let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfiguration: nil) + return NameResolver(names: .constant(resolutionResult), updateMode: .pull) + } + } +} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift index 479352279..58d0248e3 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift @@ -42,6 +42,7 @@ public struct NameResolverRegistry { private enum Factory { case ipv4(NameResolvers.IPv4) case ipv6(NameResolvers.IPv6) + case unix(NameResolvers.UnixDomainSocket) case other(any NameResolverFactory) init(_ factory: some NameResolverFactory) { @@ -49,6 +50,8 @@ public struct NameResolverRegistry { self = .ipv4(ipv4) } else if let ipv6 = factory as? NameResolvers.IPv6 { self = .ipv6(ipv6) + } else if let unix = factory as? NameResolvers.UnixDomainSocket { + self = .unix(unix) } else { self = .other(factory) } @@ -60,6 +63,8 @@ public struct NameResolverRegistry { return factory.makeResolverIfCompatible(target) case .ipv6(let factory): return factory.makeResolverIfCompatible(target) + case .unix(let factory): + return factory.makeResolverIfCompatible(target) case .other(let factory): return factory.makeResolverIfCompatible(target) } @@ -71,6 +76,8 @@ public struct NameResolverRegistry { return factory.isCompatible(withTarget: target) case .ipv6(let factory): return factory.isCompatible(withTarget: target) + case .unix(let factory): + return factory.isCompatible(withTarget: target) case .other(let factory): return factory.isCompatible(withTarget: target) } @@ -82,6 +89,8 @@ public struct NameResolverRegistry { return NameResolvers.IPv4.self == factoryType case .ipv6: return NameResolvers.IPv6.self == factoryType + case .unix: + return NameResolvers.UnixDomainSocket.self == factoryType case .other(let factory): return type(of: factory) == factoryType } @@ -99,11 +108,13 @@ public struct NameResolverRegistry { /// /// The default resolvers include: /// - ``NameResolvers/IPv4``, - /// - ``NameResolvers/IPv6``. + /// - ``NameResolvers/IPv6``, + /// - ``NameResolvers/UnixDomainSocket``. public static var defaults: Self { var resolvers = NameResolverRegistry() resolvers.registerFactory(NameResolvers.IPv4()) resolvers.registerFactory(NameResolvers.IPv6()) + resolvers.registerFactory(NameResolvers.UnixDomainSocket()) return resolvers } diff --git a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift index b4e0ba843..f7b1d11b3 100644 --- a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift +++ b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift @@ -16,6 +16,7 @@ import GRPCCore +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private struct ConstantAsyncSequence: AsyncSequence { private let element: Element diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift index f0cd3e584..8f18de1bc 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift @@ -134,6 +134,8 @@ final class NameResolverRegistryTests: XCTestCase { let resolvers = NameResolverRegistry.defaults XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv4.self)) XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv6.self)) + XCTAssert(resolvers.containsFactory(ofType: NameResolvers.UnixDomainSocket.self)) + XCTAssertEqual(resolvers.count, 3) } func testMakeResolver() { @@ -235,4 +237,19 @@ final class NameResolverRegistryTests: XCTestCase { XCTAssertNil(result.serviceConfiguration) } } + + func testUDSResolver() async throws { + let factory = NameResolvers.UnixDomainSocket() + let resolver = factory.resolver(for: .unixDomainSocket(path: "/foo")) + + XCTAssertEqual(resolver.updateMode, .pull) + + // The UDS resolver always returns the same values. + var iterator = resolver.names.makeAsyncIterator() + for _ in 0 ..< 1000 { + let result = try await XCTUnwrapAsync { try await iterator.next() } + XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.unixDomainSocket(path: "/foo")])]) + XCTAssertNil(result.serviceConfiguration) + } + } } From 8c66acecb7466476b4ac2ff07a5acebd1f6eb9a5 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:57:18 +0000 Subject: [PATCH 254/580] [Interop] Implemented the Test Service Provider (#1812) Motivation: We need the Test Service Provider in order to implement the interoperability tests. Modifications: Created the Provider and implemented methods required by the conformance to the ServiceProtocol from the generated code for the Test Service. Result: We will be able to implement a makeInteroperabilityTestServer. --- .../InteroperabilityTests/TestService.swift | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 Sources/InteroperabilityTests/TestService.swift diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift new file mode 100644 index 000000000..bb522fec5 --- /dev/null +++ b/Sources/InteroperabilityTests/TestService.swift @@ -0,0 +1,191 @@ +/* + * 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 Foundation +import GRPCCore + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { + internal func unimplementedCall( + request: ServerRequest.Single + ) async throws + -> ServerResponse.Single + { + throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") + } + + /// Server implements `emptyCall` which immediately returns the empty message. + internal func emptyCall( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let message = Grpc_Testing_TestService.Method.EmptyCall.Output() + return ServerResponse.Single(message: message) + } + + /// Server implements `unaryCall` which immediately returns a `SimpleResponse` with a payload + /// body of size `SimpleRequest.responseSize` bytes and type as appropriate for the + /// `SimpleRequest.responseType`. + /// + /// If the server does not support the `responseType`, then it should fail the RPC with + /// `INVALID_ARGUMENT`. + internal func unaryCall( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + // If the request has a responseStatus set, the server should return that status. + // If the code is an error code, the server will throw an error containing that code + // and the message set in the responseStatus. + // If the code is `ok`, the server will automatically send back an `ok` status. + if request.message.responseStatus.isInitialized { + guard let code = Status.Code(rawValue: Int(request.message.responseStatus.code)) else { + throw RPCError(code: .invalidArgument, message: "The response status code is invalid.") + } + let status = Status( + code: code, + message: request.message.responseStatus.message + ) + if let error = RPCError(status: status) { + throw error + } + } + + if case .UNRECOGNIZED = request.message.responseType { + throw RPCError(code: .invalidArgument, message: "The response type is not recognized.") + } + + let responseMessage = Grpc_Testing_TestService.Method.UnaryCall.Output.with { response in + response.payload = Grpc_Testing_Payload.with { payload in + payload.body = Data(repeating: 0, count: Int(request.message.responseSize)) + payload.type = request.message.responseType + } + } + + return ServerResponse.Single(message: responseMessage) + } + + /// Server gets the default `SimpleRequest` proto as the request. The content of the request is + /// ignored. It returns the `SimpleResponse` proto with the payload set to current timestamp. + /// The timestamp is an integer representing current time with nanosecond resolution. This + /// integer is formated as ASCII decimal in the response. The format is not really important as + /// long as the response payload is different for each request. In addition it adds cache control + /// headers such that the response can be cached by proxies in the response path. Server should + /// be behind a caching proxy for this test to pass. Currently we set the max-age to 60 seconds. + internal func cacheableUnaryCall( + request: ServerRequest.Single + ) async throws + -> ServerResponse.Single + { + throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") + } + + /// Server implements `streamingOutputCall` by replying, in order, with one + /// `StreamingOutputCallResponse` for each `ResponseParameter`s in `StreamingOutputCallRequest`. + /// Each `StreamingOutputCallResponse` should have a payload body of size `ResponseParameter.size` + /// bytes, as specified by its respective `ResponseParameter`. After sending all responses, it + /// closes with OK. + internal func streamingOutputCall( + request: ServerRequest.Single< + Grpc_Testing_TestService.Method.StreamingOutputCall.Input + > + ) async throws + -> ServerResponse.Stream + { + return ServerResponse.Stream { writer in + for responseParameter in request.message.responseParameters { + let response = Grpc_Testing_StreamingOutputCallResponse.with { response in + response.payload = Grpc_Testing_Payload.with { payload in + payload.body = Data(repeating: 0, count: Int(responseParameter.size)) + } + } + try await writer.write(response) + // We convert the `intervalUs` value from microseconds to nanoseconds. + try await Task.sleep(nanoseconds: UInt64(responseParameter.intervalUs) * 1000) + } + return [:] + } + } + + /// Server implements `streamingInputCall` which upon half close immediately returns a + /// `StreamingInputCallResponse` where `aggregatedPayloadSize` is the sum of all request payload + /// bodies received. + internal func streamingInputCall( + request: ServerRequest.Stream + ) async throws + -> ServerResponse.Single + { + var aggregatedPayloadSize = 0 + + for try await message in request.messages { + aggregatedPayloadSize += message.payload.body.count + } + + let responseMessage = Grpc_Testing_TestService.Method.StreamingInputCall.Output.with { + $0.aggregatedPayloadSize = Int32(aggregatedPayloadSize) + } + + return ServerResponse.Single(message: responseMessage) + } + + /// Server implements `fullDuplexCall` by replying, in order, with one + /// `StreamingOutputCallResponse` for each `ResponseParameter`s in each + /// `StreamingOutputCallRequest`. Each `StreamingOutputCallResponse` should have a payload body + /// of size `ResponseParameter.size` bytes, as specified by its respective `ResponseParameter`s. + /// After receiving half close and sending all responses, it closes with OK. + internal func fullDuplexCall( + request: ServerRequest.Stream + ) async throws + -> ServerResponse.Stream + { + return ServerResponse.Stream { writer in + for try await message in request.messages { + // If a request message has a responseStatus set, the server should return that status. + // If the code is an error code, the server will throw an error containing that code + // and the message set in the responseStatus. + // If the code is `ok`, the server will automatically send back an `ok` status with the response. + if message.responseStatus.isInitialized { + guard let code = Status.Code(rawValue: Int(message.responseStatus.code)) else { + throw RPCError(code: .invalidArgument, message: "The response status code is invalid.") + } + + let status = Status(code: code, message: message.responseStatus.message) + if let error = RPCError(status: status) { + throw error + } + } + + for responseParameter in message.responseParameters { + let response = Grpc_Testing_StreamingOutputCallResponse.with { response in + response.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: Int(responseParameter.size)) + } + } + try await writer.write(response) + } + } + return [:] + } + } + + /// This is not implemented as it is not described in the specification. + /// + /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md + internal func halfDuplexCall( + request: ServerRequest.Stream + ) async throws + -> ServerResponse.Stream + { + throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") + } +} From 160b1f8060295221feee6498a23e651baa5a2754 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 20 Feb 2024 11:34:14 +0100 Subject: [PATCH 255/580] Add another missing availability annotation in tests (#1816) --- Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift index b02289ee2..0179f8bcf 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift @@ -38,6 +38,7 @@ func XCTAssertDescription( XCTAssertEqual(String(describing: subject), expected, file: file, line: line) } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTUnwrapAsync(_ expression: () async throws -> T?) async throws -> T { let value = try await expression() return try XCTUnwrap(value) From 97b09fa69312d7b25bab145dc4ad6d75d45b1895 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:52:13 +0000 Subject: [PATCH 256/580] [InteropTests] Create test framework (#1817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: We can’t use XCTest for the interoperability tests because we need to be able to run them as an executable target. However, we need to be able to write tests which assert various things. Modifications: Implemented an AssertionFailure struct and a function that checks if the result of an expression is true. From this function we can build up any other assertion functions depending on what we will need for the interop tests. Result: We will have a basic test framework for the interp tests. --- .../AssertionFailure.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Sources/InteroperabilityTests/AssertionFailure.swift diff --git a/Sources/InteroperabilityTests/AssertionFailure.swift b/Sources/InteroperabilityTests/AssertionFailure.swift new file mode 100644 index 000000000..fea2d18c3 --- /dev/null +++ b/Sources/InteroperabilityTests/AssertionFailure.swift @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/// Failure assertion for interoperability testing. +/// +/// This is required because the tests must be able to run without XCTest. +public struct AssertionFailure: Error { + public var message: String + public var file: String + public var line: Int +} + +/// Asserts that the value of an expression is `true`. +public func assertTrue( + _ expression: @autoclosure () throws -> Bool, + _ message: String = "The statement is not true.", + file: String = #fileID, + line: Int = #line +) throws { + guard try expression() else { + throw AssertionFailure(message: message, file: file, line: line) + } +} From a6a4539aa06bf99d1c3986ea8bc581947aa00995 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Feb 2024 14:36:56 +0100 Subject: [PATCH 257/580] Add VirtualSocket resolver (#1815) Motivation: Users should be able to connect to Virtual Sockets. To do these they need a VSOCK target and resolver. Modifications: - Add a VSOCK target and resolver - Update the registry to include it by default Result: Can resolve VSOCK targets --- .../Client/Resolver/NameResolver+VSOCK.swift | 64 +++++++++++++++++++ .../Resolver/NameResolverRegistry.swift | 17 ++++- .../Resolver/NameResolverRegistryTests.swift | 18 +++++- 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift new file mode 100644 index 000000000..5992031bc --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift @@ -0,0 +1,64 @@ +/* + * 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 + +extension ResolvableTargets { + /// A resolvable target for Virtual Socket addresses. + /// + /// ``VirtualSocket`` addresses can be resolved by the ``NameResolvers/VirtualSocket`` + /// resolver which creates a single ``Endpoint`` for target address. + public struct VirtualSocket: ResolvableTarget { + public var address: SocketAddress.VirtualSocket + + public init(address: SocketAddress.VirtualSocket) { + self.address = address + } + } +} + +extension ResolvableTarget where Self == ResolvableTargets.VirtualSocket { + /// Creates a new resolvable Virtual Socket target. + /// - Parameters: + /// - contextID: The context ID ('cid') of the service. + /// - port: The port to connect to. + public static func vsock( + contextID: SocketAddress.VirtualSocket.ContextID, + port: SocketAddress.VirtualSocket.Port + ) -> Self { + let address = SocketAddress.VirtualSocket(contextID: contextID, port: port) + return ResolvableTargets.VirtualSocket(address: address) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension NameResolvers { + /// A ``NameResolverFactory`` for ``ResolvableTargets/VirtualSocket`` targets. + /// + /// The name resolver for a given target always produces the same values, with a single endpoint. + /// This resolver doesn't support fetching service configuration. + public struct VirtualSocket: NameResolverFactory { + public typealias Target = ResolvableTargets.VirtualSocket + + public init() {} + + public func resolver(for target: Target) -> NameResolver { + let endpoint = Endpoint(addresses: [.vsock(target.address)]) + let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfiguration: nil) + return NameResolver(names: .constant(resolutionResult), updateMode: .pull) + } + } +} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift index 58d0248e3..a1393bd90 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift @@ -28,9 +28,9 @@ /// // type `CustomResolver.ResolvableTarget`. /// registry.registerFactory(CustomResolver()) /// -/// // Remove the Unix Domain Socket and VSOCK resolvers, if they exist. +/// // Remove the Unix Domain Socket and Virtual Socket resolvers, if they exist. /// registry.removeFactory(ofType: NameResolvers.UnixDomainSocket.self) -/// registry.removeFactory(ofType: NameResolvers.VSOCK.self) +/// registry.removeFactory(ofType: NameResolvers.VirtualSocket.self) /// /// // Resolve an IPv4 target /// if let resolver = registry.makeResolver(for: .ipv4(host: "localhost", port: 80)) { @@ -43,6 +43,7 @@ public struct NameResolverRegistry { case ipv4(NameResolvers.IPv4) case ipv6(NameResolvers.IPv6) case unix(NameResolvers.UnixDomainSocket) + case vsock(NameResolvers.VirtualSocket) case other(any NameResolverFactory) init(_ factory: some NameResolverFactory) { @@ -52,6 +53,8 @@ public struct NameResolverRegistry { self = .ipv6(ipv6) } else if let unix = factory as? NameResolvers.UnixDomainSocket { self = .unix(unix) + } else if let vsock = factory as? NameResolvers.VirtualSocket { + self = .vsock(vsock) } else { self = .other(factory) } @@ -65,6 +68,8 @@ public struct NameResolverRegistry { return factory.makeResolverIfCompatible(target) case .unix(let factory): return factory.makeResolverIfCompatible(target) + case .vsock(let factory): + return factory.makeResolverIfCompatible(target) case .other(let factory): return factory.makeResolverIfCompatible(target) } @@ -78,6 +83,8 @@ public struct NameResolverRegistry { return factory.isCompatible(withTarget: target) case .unix(let factory): return factory.isCompatible(withTarget: target) + case .vsock(let factory): + return factory.isCompatible(withTarget: target) case .other(let factory): return factory.isCompatible(withTarget: target) } @@ -91,6 +98,8 @@ public struct NameResolverRegistry { return NameResolvers.IPv6.self == factoryType case .unix: return NameResolvers.UnixDomainSocket.self == factoryType + case .vsock: + return NameResolvers.VirtualSocket.self == factoryType case .other(let factory): return type(of: factory) == factoryType } @@ -109,12 +118,14 @@ public struct NameResolverRegistry { /// The default resolvers include: /// - ``NameResolvers/IPv4``, /// - ``NameResolvers/IPv6``, - /// - ``NameResolvers/UnixDomainSocket``. + /// - ``NameResolvers/UnixDomainSocket``, + /// - ``NameResolvers/VirtualSocket``. public static var defaults: Self { var resolvers = NameResolverRegistry() resolvers.registerFactory(NameResolvers.IPv4()) resolvers.registerFactory(NameResolvers.IPv6()) resolvers.registerFactory(NameResolvers.UnixDomainSocket()) + resolvers.registerFactory(NameResolvers.VirtualSocket()) return resolvers } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift index 8f18de1bc..75bd6f5d7 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift @@ -135,7 +135,8 @@ final class NameResolverRegistryTests: XCTestCase { XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv4.self)) XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv6.self)) XCTAssert(resolvers.containsFactory(ofType: NameResolvers.UnixDomainSocket.self)) - XCTAssertEqual(resolvers.count, 3) + XCTAssert(resolvers.containsFactory(ofType: NameResolvers.VirtualSocket.self)) + XCTAssertEqual(resolvers.count, 4) } func testMakeResolver() { @@ -252,4 +253,19 @@ final class NameResolverRegistryTests: XCTestCase { XCTAssertNil(result.serviceConfiguration) } } + + func testVSOCKResolver() async throws { + let factory = NameResolvers.VirtualSocket() + let resolver = factory.resolver(for: .vsock(contextID: .any, port: .any)) + + XCTAssertEqual(resolver.updateMode, .pull) + + // The VSOCK resolver always returns the same values. + var iterator = resolver.names.makeAsyncIterator() + for _ in 0 ..< 1000 { + let result = try await XCTUnwrapAsync { try await iterator.next() } + XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.vsock(contextID: .any, port: .any)])]) + XCTAssertNil(result.serviceConfiguration) + } + } } From 61b7262e53e7879e3efaf03fe1bcd2d29bc8a545 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 23 Feb 2024 10:45:27 +0000 Subject: [PATCH 258/580] Notify of quiescing when there are no open streams (#1819) --- Sources/GRPC/GRPCIdleHandlerStateMachine.swift | 6 ++++-- Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift b/Sources/GRPC/GRPCIdleHandlerStateMachine.swift index 9da96068d..5fbbe5c71 100644 --- a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift +++ b/Sources/GRPC/GRPCIdleHandlerStateMachine.swift @@ -414,12 +414,12 @@ struct GRPCIdleHandlerStateMachine { switch self.state { case let .operating(state): + operations.notifyConnectionManager(about: .quiescing) if state.hasOpenStreams { // There are open streams: send a GOAWAY frame and wait for the stream count to reach zero. // // It's okay if we haven't seen a SETTINGS frame at this point; we've initiated the shutdown // so making a connection is ready isn't necessary. - operations.notifyConnectionManager(about: .quiescing) // TODO: we should ratchet down the last initiated stream after 1-RTT. // @@ -475,8 +475,8 @@ struct GRPCIdleHandlerStateMachine { // A SETTINGS frame MUST follow the connection preface. (RFC 7540 § 3.5) assert(state.hasSeenSettings) + operations.notifyConnectionManager(about: .quiescing) if state.hasOpenStreams { - operations.notifyConnectionManager(about: .quiescing) switch state.role { case .client: // The server sent us a GOAWAY we'll just stop opening new streams and will send a GOAWAY @@ -500,7 +500,9 @@ struct GRPCIdleHandlerStateMachine { case let .waitingToIdle(state): // There can't be any open streams, but we have a few loose ends to clear up: we need to // cancel the idle timeout, send a GOAWAY frame and then close. + // We should also notify the connection manager that quiescing is happening. self.state = .closing(.init(fromWaitingToIdle: state)) + operations.notifyConnectionManager(about: .quiescing) operations.cancelIdleTask(state.idleTask) operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) operations.closeChannel() diff --git a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift b/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift index c17da7f99..50eb009a6 100644 --- a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift +++ b/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift @@ -194,6 +194,7 @@ class GRPCIdleHandlerStateMachineTests: GRPCTestCase { op3.assertGoAway(streamID: .rootStream) op3.assertShouldClose() op3.assertCancelIdleTimeout() + op3.assertConnectionManager(.quiescing) // Close; we were going to go idle anyway. let op4 = stateMachine.channelInactive() @@ -207,6 +208,7 @@ class GRPCIdleHandlerStateMachineTests: GRPCTestCase { let op1 = stateMachine.initiateGracefulShutdown() op1.assertGoAway(streamID: .rootStream) op1.assertShouldClose() + op1.assertConnectionManager(.quiescing) // Closed. let op2 = stateMachine.channelInactive() @@ -223,6 +225,7 @@ class GRPCIdleHandlerStateMachineTests: GRPCTestCase { // Initiate shutdown. let op2 = stateMachine.initiateGracefulShutdown() op2.assertShouldNotClose() + op2.assertConnectionManager(.quiescing) // Receive a GOAWAY; no change. let op3 = stateMachine.receiveGoAway() @@ -467,8 +470,9 @@ class GRPCIdleHandlerStateMachineTests: GRPCTestCase { let op5 = stateMachine.receiveGoAway() // We're the client, there are no server initiated streams, so GOAWAY with root stream. op5.assertGoAway(streamID: 0) - // No open streams, so we can close now. + // No open streams, so we can close now. Also assert the connection manager got a quiescing event. op5.assertShouldClose() + op5.assertConnectionManager(.quiescing) // Closed. let op6 = stateMachine.channelInactive() @@ -495,6 +499,7 @@ class GRPCIdleHandlerStateMachineTests: GRPCTestCase { let op4 = stateMachine.receiveGoAway() op4.assertGoAway(streamID: .maxID) op4.assertShouldPingAfterGoAway() + op4.assertConnectionManager(.quiescing) // Create another stream. This is fine, the client hasn't ack'd the ping yet. let op5 = stateMachine.streamCreated(withID: 7) From 9f0956e48a2bec764b3e23d250ff832751d115d3 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:26:16 +0000 Subject: [PATCH 259/580] Interop Tests implementation (#1821) Motivation: The test cases test certain functionalities of the new GRPC server and client. Modifications: - created the InteroperabilityTest protocol with the run method - created the InteroperabilityTestCase enum that will be used by main to run certain tests according to the input - implemented the interop tests for the features we support (no caching or compression tests) - added new assertions to the test framework Result: We will be able to implement the main() function that runs the interop tests using in process transport. --- .../GRPCCore/Call/Client/ClientResponse.swift | 4 +- .../AssertionFailure.swift | 21 + .../Internal/AsyncStream+MakeStream.swift | 32 + .../InteroperabilityTestCase.swift | 98 +++ .../InteroperabilityTestCases.swift | 695 ++++++++++++++++++ 5 files changed, 848 insertions(+), 2 deletions(-) create mode 100644 Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift create mode 100644 Sources/InteroperabilityTests/InteroperabilityTestCase.swift create mode 100644 Sources/InteroperabilityTests/InteroperabilityTestCases.swift diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index 1ccdcfd00..49ea3973c 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -344,9 +344,9 @@ extension ClientResponse.Stream { } } - /// Returns metadata received from the server at the end of the response. + /// Returns the messages received from the server. /// - /// Unlike ``metadata``, for rejected RPCs the metadata returned may contain values. + /// For rejected RPCs the `RPCAsyncSequence` throws a `RPCError``. public var messages: RPCAsyncSequence { switch self.accepted { case let .success(contents): diff --git a/Sources/InteroperabilityTests/AssertionFailure.swift b/Sources/InteroperabilityTests/AssertionFailure.swift index fea2d18c3..112a36ee3 100644 --- a/Sources/InteroperabilityTests/AssertionFailure.swift +++ b/Sources/InteroperabilityTests/AssertionFailure.swift @@ -21,6 +21,12 @@ public struct AssertionFailure: Error { public var message: String public var file: String public var line: Int + + public init(message: String, file: String = #fileID, line: Int = #line) { + self.message = message + self.file = file + self.line = line + } } /// Asserts that the value of an expression is `true`. @@ -34,3 +40,18 @@ public func assertTrue( throw AssertionFailure(message: message, file: file, line: line) } } + +/// Asserts that the two given values are equal. +public func assertEqual( + _ value1: T, + _ value2: T, + file: String = #fileID, + line: Int = #line +) throws { + return try assertTrue( + value1 == value2, + "'\(value1)' is not equal to '\(value2)'", + file: file, + line: line + ) +} diff --git a/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift b/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..5f1b75dd6 --- /dev/null +++ b/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift @@ -0,0 +1,32 @@ +/* + * 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. + */ + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift new file mode 100644 index 000000000..6a909e6ea --- /dev/null +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -0,0 +1,98 @@ +/* + * 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 + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public protocol InteroperabilityTest { + /// Run a test case using the given connection. + /// + /// The test case is considered unsuccessful if any exception is thrown, conversely if no + /// exceptions are thrown it is successful. + /// + /// - Parameter client: The client to use for the test. + /// - Throws: Any exception may be thrown to indicate an unsuccessful test. + func run(client: GRPCClient) async throws +} + +/// Test cases as listed by the [gRPC interoperability test description +/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). +/// +/// This is not a complete list, the following tests have not been implemented: +/// - cacheable_unary +/// - client-compressed-unary +/// - server-compressed-unary +/// - client_compressed_streaming +/// - server_compressed_streaming +/// - compute_engine_creds +/// - jwt_token_creds +/// - oauth2_auth_token +/// - per_rpc_creds +/// - google_default_credentials +/// - compute_engine_channel_credentials +/// - cancel_after_begin +/// - cancel_after_first_response +/// +/// Note: Tests for compression have not been implemented yet as compression is +/// not supported. Once the API which allows for compression will be implemented +/// these tests should be added. +public enum InteroperabilityTestCase: String, CaseIterable { + case emptyUnary = "empty_unary" + case largeUnary = "large_unary" + case clientStreaming = "client_streaming" + case serverStreaming = "server_streaming" + case pingPong = "ping_pong" + case emptyStream = "empty_stream" + case customMetadata = "custom_metadata" + case statusCodeAndMessage = "status_code_and_message" + case specialStatusMessage = "special_status_message" + case unimplementedMethod = "unimplemented_method" + case unimplementedService = "unimplemented_service" + + public var name: String { + return self.rawValue + } +} + +@available(macOS 13.0, *) +extension InteroperabilityTestCase { + /// Return a new instance of the test case. + public func makeTest() -> InteroperabilityTest { + switch self { + case .emptyUnary: + return EmptyUnary() + case .largeUnary: + return LargeUnary() + case .clientStreaming: + return ClientStreaming() + case .serverStreaming: + return ServerStreaming() + case .pingPong: + return PingPong() + case .emptyStream: + return EmptyStream() + case .customMetadata: + return CustomMetadata() + case .statusCodeAndMessage: + return StatusCodeAndMessage() + case .specialStatusMessage: + return SpecialStatusMessage() + case .unimplementedMethod: + return UnimplementedMethod() + case .unimplementedService: + return UnimplementedService() + } + } +} diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift new file mode 100644 index 000000000..cfebda759 --- /dev/null +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -0,0 +1,695 @@ +/* + * 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 struct Foundation.Data + +/// This test verifies that implementations support zero-size messages. Ideally, client +/// implementations would verify that the request and response were zero bytes serialized, but +/// this is generally prohibitive to perform, so is not required. +/// +/// Server features: +/// - EmptyCall +/// +/// Procedure: +/// 1. Client calls EmptyCall with the default Empty message +/// +/// Client asserts: +/// - call was successful +/// - response is non-null +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct EmptyUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + try await testServiceClient.emptyCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { response in + try assertEqual(response.message, Grpc_Testing_Empty()) + } + } +} + +/// This test verifies unary calls succeed in sending messages, and touches on flow control (even +/// if compression is enabled on the channel). +/// +/// Server features: +/// - UnaryCall +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - response payload body is 314159 bytes in size +/// - clients are free to assert that the response payload body contents are zero and comparing +/// the entire response message against a golden response +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct LargeUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = Grpc_Testing_SimpleRequest.with { request in + request.responseSize = 314_159 + request.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: request) + ) { response in + try assertEqual( + response.message.payload, + Grpc_Testing_Payload.with { + $0.body = Data(count: 314_159) + } + ) + } + } +} + +/// This test verifies that client-only streaming succeeds. +/// +/// Server features: +/// - StreamingInputCall +/// +/// Procedure: +/// 1. Client calls StreamingInputCall +/// 2. Client sends: +/// ``` +/// { +/// payload:{ +/// body: 27182 bytes of zeros +/// } +/// } +/// ``` +/// 3. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 8 bytes of zeros +/// } +/// } +/// ``` +/// 4. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 1828 bytes of zeros +/// } +/// } +/// ``` +/// 5. Client then sends: +/// ``` +/// { +/// payload:{ +/// body: 45904 bytes of zeros +/// } +/// } +/// ``` +/// 6. Client half-closes +/// +/// Client asserts: +/// - call was successful +/// - response aggregated_payload_size is 74922 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct ClientStreaming: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = ClientRequest.Stream { writer in + for bytes in [27182, 8, 1828, 45904] { + let message = Grpc_Testing_StreamingInputCallRequest.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: bytes) + } + } + try await writer.write(message) + } + } + + try await testServiceClient.streamingInputCall(request: request) { response in + try assertEqual(response.message.aggregatedPayloadSize, 74922) + } + } +} + +/// This test verifies that server-only streaming succeeds. +/// +/// Server features: +/// - StreamingOutputCall +/// +/// Procedure: +/// 1. Client calls StreamingOutputCall with StreamingOutputCallRequest: +/// ``` +/// { +/// response_parameters:{ +/// size: 31415 +/// } +/// response_parameters:{ +/// size: 9 +/// } +/// response_parameters:{ +/// size: 2653 +/// } +/// response_parameters:{ +/// size: 58979 +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - exactly four responses +/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 +/// - clients are free to assert that the response payload body contents are zero and +/// comparing the entire response messages against golden responses +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct ServerStreaming: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let responseSizes = [31415, 9, 2653, 58979] + let request = Grpc_Testing_StreamingOutputCallRequest.with { request in + request.responseParameters = responseSizes.map { + var parameter = Grpc_Testing_ResponseParameters() + parameter.size = Int32($0) + return parameter + } + } + + try await testServiceClient.streamingOutputCall( + request: ClientRequest.Single(message: request) + ) { response in + var responseParts = response.messages.makeAsyncIterator() + // There are 4 response sizes, so if there isn't a message for each one, + // it means that the client didn't receive 4 messages back. + for responseSize in responseSizes { + if let message = try await responseParts.next() { + try assertEqual(message.payload.body.count, responseSize) + } else { + throw AssertionFailure( + message: "There were less than four responses received." + ) + } + } + // Check that there were not more than 4 responses from the server. + try assertEqual(try await responseParts.next(), nil) + } + } +} + +/// This test verifies that full duplex bidi is supported. +/// +/// Server features: +/// - FullDuplexCall +/// +/// Procedure: +/// 1. Client calls FullDuplexCall with: +/// ``` +/// { +/// response_parameters:{ +/// size: 31415 +/// } +/// payload:{ +/// body: 27182 bytes of zeros +/// } +/// } +/// ``` +/// 2. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 9 +/// } +/// payload:{ +/// body: 8 bytes of zeros +/// } +/// } +/// ``` +/// 3. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 2653 +/// } +/// payload:{ +/// body: 1828 bytes of zeros +/// } +/// } +/// ``` +/// 4. After getting a reply, it sends: +/// ``` +/// { +/// response_parameters:{ +/// size: 58979 +/// } +/// payload:{ +/// body: 45904 bytes of zeros +/// } +/// } +/// ``` +/// 5. After getting a reply, client half-closes +/// +/// Client asserts: +/// - call was successful +/// - exactly four responses +/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 +/// - clients are free to assert that the response payload body contents are zero and +/// comparing the entire response messages against golden responses +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct PingPong: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let ids = AsyncStream.makeStream(of: Int.self) + + let request = ClientRequest.Stream { writer in + let sizes = [(31_415, 27_182), (9, 8), (2_653, 1_828), (58_979, 45_904)] + for try await id in ids.stream { + var message = Grpc_Testing_StreamingOutputCallRequest() + switch id { + case 1 ... 4: + let (responseSize, bodySize) = sizes[id - 1] + message.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = Int32(responseSize) + } + ] + message.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: bodySize) + } + default: + // When the id is higher than 4 it means the client received all the expected responses + // and it doesn't need to send another message. + return + } + try await writer.write(message) + } + } + ids.continuation.yield(1) + try await testServiceClient.fullDuplexCall(request: request) { response in + var id = 1 + for try await message in response.messages { + switch id { + case 1: + try assertEqual(message.payload.body, Data(count: 31_415)) + case 2: + try assertEqual(message.payload.body, Data(count: 9)) + case 3: + try assertEqual(message.payload.body, Data(count: 2_653)) + case 4: + try assertEqual(message.payload.body, Data(count: 58_979)) + default: + throw AssertionFailure( + message: "We should only receive messages with ids between 1 and 4." + ) + } + + // Add the next id to the continuation. + id += 1 + ids.continuation.yield(id) + } + } + } +} + +/// This test verifies that streams support having zero-messages in both directions. +/// +/// Server features: +/// - FullDuplexCall +/// +/// Procedure: +/// 1. Client calls FullDuplexCall and then half-closes +/// +/// Client asserts: +/// - call was successful +/// - exactly zero responses +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct EmptyStream: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request = ClientRequest.Stream { _ in } + + try await testServiceClient.fullDuplexCall(request: request) { response in + var messages = response.messages.makeAsyncIterator() + try await assertEqual(messages.next(), nil) + } + } +} + +/// This test verifies that custom metadata in either binary or ascii format can be sent as +/// initial-metadata by the client and as both initial- and trailing-metadata by the server. +/// +/// Server features: +/// - UnaryCall +/// - FullDuplexCall +/// - Echo Metadata +/// +/// Procedure: +/// 1. The client attaches custom metadata with the following keys and values +/// to a UnaryCall with request: +/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" +/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab +/// ``` +/// { +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// 2. The client attaches custom metadata with the following keys and values +/// to a FullDuplexCall with request: +/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" +/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab +/// ``` +/// { +/// response_parameters:{ +/// size: 314159 +/// } +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// and then half-closes +/// +/// Client asserts: +/// - call was successful +/// - metadata with key "x-grpc-test-echo-initial" and value "test_initial_metadata_value" is +/// received in the initial metadata for calls in Procedure steps 1 and 2. +/// - metadata with key "x-grpc-test-echo-trailing-bin" and value 0xababab is received in the +/// trailing metadata for calls in Procedure steps 1 and 2. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct CustomMetadata: InteroperabilityTest { + let initialMetadataName = "x-grpc-test-echo-initial" + let initialMetadataValue = "test_initial_metadata_value" + + let trailingMetadataName = "x-grpc-test-echo-trailing-bin" + let trailingMetadataValue: [UInt8] = [0xAB, 0xAB, 0xAB] + + func checkInitialMetadata(_ metadata: Metadata) throws { + let values = metadata[self.initialMetadataName] + try assertEqual(Array(values), [.string(self.initialMetadataValue)]) + } + + func checkTrailingMetadata(_ metadata: Metadata) throws { + let values = metadata[self.trailingMetadataName] + try assertEqual(Array(values), [.binary(self.trailingMetadataValue)]) + } + + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let unaryRequest = Grpc_Testing_SimpleRequest.with { request in + request.responseSize = 314_159 + request.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + let metadata: Metadata = [ + self.initialMetadataName: .string(self.initialMetadataValue), + self.trailingMetadataName: .binary(self.trailingMetadataValue), + ] + + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: unaryRequest, metadata: metadata) + ) { response in + // Check the initial metadata. + let receivedInitialMetadata = response.metadata + try checkInitialMetadata(receivedInitialMetadata) + + // Check the message. + try assertEqual(response.message.payload.body, Data(count: 314_159)) + + // Check the trailing metadata. + try checkTrailingMetadata(response.trailingMetadata) + } + + let streamingRequest = ClientRequest.Stream(metadata: metadata) { writer in + let message = Grpc_Testing_StreamingOutputCallRequest.with { + $0.responseParameters = [ + Grpc_Testing_ResponseParameters.with { + $0.size = 314_159 + } + ] + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 271_828) + } + } + try await writer.write(message) + } + + try await testServiceClient.fullDuplexCall(request: streamingRequest) { response in + switch response.accepted { + case .success(let contents): + // Check the initial metadata. + let receivedInitialMetadata = response.metadata + try self.checkInitialMetadata(receivedInitialMetadata) + + let parts = try await contents.bodyParts.reduce(into: []) { $0.append($1) } + try assertEqual(parts.count, 2) + + for part in parts { + switch part { + // Check the message. + case .message(let message): + try assertEqual(message.payload.body, Data(count: 314_159)) + // Check the trailing metadata. + case .trailingMetadata(let receivedTrailingMetadata): + try self.checkTrailingMetadata(receivedTrailingMetadata) + } + } + case .failure(_): + throw AssertionFailure( + message: "The client should have received a response from the server." + ) + } + } + } +} + +/// This test verifies unary calls succeed in sending messages, and propagate back status code and +/// message sent along with the messages. +/// +/// Server features: +/// - UnaryCall +/// - FullDuplexCall +/// - Echo Status +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "test status message" +/// } +/// } +/// ``` +/// 2. Client calls FullDuplexCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "test status message" +/// } +/// } +/// ``` +/// 3. and then half-closes +/// +/// Client asserts: +/// - received status code is the same as the sent code for both Procedure steps 1 and 2 +/// - received status message is the same as the sent message for both Procedure steps 1 and 2 +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct StatusCodeAndMessage: InteroperabilityTest { + let expectedCode = 2 + let expectedMessage = "test status message" + + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let message = Grpc_Testing_SimpleRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = Int32(self.expectedCode) + $0.message = self.expectedMessage + } + } + + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: message) + ) { response in + switch response.accepted { + case .failure(let error): + try assertEqual(error.code.rawValue, self.expectedCode) + try assertEqual(error.message, self.expectedMessage) + case .success(_): + throw AssertionFailure( + message: + "The client should receive an error with the status code and message sent by the client." + ) + } + } + + let request = ClientRequest.Stream { writer in + let message = Grpc_Testing_StreamingOutputCallRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = Int32(self.expectedCode) + $0.message = self.expectedMessage + } + } + try await writer.write(message) + } + + try await testServiceClient.fullDuplexCall(request: request) { response in + do { + for try await _ in response.messages { + throw AssertionFailure( + message: + "The client should receive an error with the status code and message sent by the client." + ) + } + } catch let error as RPCError { + try assertEqual(error.code.rawValue, self.expectedCode) + try assertEqual(error.message, self.expectedMessage) + } + } + } +} + +/// This test verifies Unicode and whitespace is correctly processed in status message. "\t" is +/// horizontal tab. "\r" is carriage return. "\n" is line feed. +/// +/// Server features: +/// - UnaryCall +/// - Echo Status +/// +/// Procedure: +/// 1. Client calls UnaryCall with: +/// ``` +/// { +/// response_status:{ +/// code: 2 +/// message: "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is the same as the sent code for Procedure step 1 +/// - received status message is the same as the sent message for Procedure step 1, including all +/// whitespace characters +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct SpecialStatusMessage: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let responseMessage = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" + let message = Grpc_Testing_SimpleRequest.with { + $0.responseStatus = Grpc_Testing_EchoStatus.with { + $0.code = 2 + $0.message = responseMessage + } + } + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: message) + ) { response in + switch response.accepted { + case .success(_): + throw AssertionFailure( + message: "The response should be an error with the error code 2." + ) + case .failure(let error): + try assertEqual(error.code.rawValue, 2) + try assertEqual(error.message, responseMessage) + } + } + } +} + +/// This test verifies that calling an unimplemented RPC method returns the UNIMPLEMENTED status +/// code. +/// +/// Server features: N/A +/// +/// Procedure: +/// 1. Client calls grpc.testing.TestService/UnimplementedCall with an empty request (defined as +/// grpc.testing.Empty): +/// ``` +/// { +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is 12 (UNIMPLEMENTED) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct UnimplementedMethod: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + try await testServiceClient.unimplementedCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { response in + let result = response.accepted + switch result { + case .success(_): + throw AssertionFailure( + message: "The result should be an error." + ) + case .failure(let error): + try assertEqual(error.code, .unimplemented) + } + } + } +} + +/// This test verifies calling an unimplemented server returns the UNIMPLEMENTED status code. +/// +/// Server features: N/A +/// +/// Procedure: +/// 1. Client calls grpc.testing.UnimplementedService/UnimplementedCall with an empty request +/// (defined as grpc.testing.Empty): +/// ``` +/// { +/// } +/// ``` +/// +/// Client asserts: +/// - received status code is 12 (UNIMPLEMENTED) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct UnimplementedService: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(client: client) + try await unimplementedServiceClient.unimplementedCall( + request: ClientRequest.Single(message: Grpc_Testing_Empty()) + ) { response in + let result = response.accepted + switch result { + case .success(_): + throw AssertionFailure( + message: "The result should be an error." + ) + case .failure(let error): + try assertEqual(error.code, .unimplemented) + } + } + } +} From 7028cfb537f1d3640a714aaa7b8e8126218dd1e8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Mar 2024 15:09:04 +0000 Subject: [PATCH 260/580] Raise minimum swift version to 5.8 (#1825) --- .github/workflows/ci.yaml | 20 +++++++++---------- Package.swift | 6 +++--- Performance/Benchmarks/Package.swift | 2 +- ...wiftBenchmark.Metadata_Add_binary.p90.json | 2 +- ...wiftBenchmark.Metadata_Add_string.p90.json | 0 ...hmark.Metadata_Iterate_all_values.p90.json | 4 ++-- ...es_when_only_binary_values_stored.p90.json | 4 ++-- ...y_values_when_only_strings_stored.p90.json | 4 ++-- ...rk.Metadata_Iterate_string_values.p90.json | 4 ++-- ...rk.Metadata_Remove_values_for_key.p90.json | 2 +- Performance/QPSBenchmark/Package.swift | 2 +- README.md | 3 ++- Sources/GRPC/Docs.docc/index.md | 3 ++- 13 files changed, 29 insertions(+), 27 deletions(-) rename Performance/Benchmarks/Thresholds/{5.7 => 5.10}/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json (78%) rename Performance/Benchmarks/Thresholds/{5.7 => 5.10}/GRPCSwiftBenchmark.Metadata_Add_string.p90.json (100%) rename Performance/Benchmarks/Thresholds/{5.7 => 5.10}/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json (58%) rename Performance/Benchmarks/Thresholds/{5.7 => 5.10}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json (58%) rename Performance/Benchmarks/Thresholds/{5.7 => 5.10}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json (59%) rename Performance/Benchmarks/Thresholds/{5.7 => 5.10}/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json (58%) rename Performance/Benchmarks/Thresholds/{5.7 => 5.10}/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json (77%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d793ca32d..84076c692 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,13 +23,13 @@ jobs: - image: swiftlang/swift:nightly-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.9-jammy + - image: swift:5.10-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.8-focal + - image: swift:5.9-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.7-focal + - image: swift:5.8-focal # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" name: Build and Test on ${{ matrix.image }} @@ -60,8 +60,8 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.9-jammy - swift-version: 5.9 + - image: swift:5.10-jammy + swift-version: '5.10' env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -71,8 +71,8 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.8-focal - swift-version: 5.8 + - image: swift:5.9-jammy + swift-version: 5.9 env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -82,8 +82,8 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.7-focal - swift-version: 5.7 + - image: swift:5.8-focal + swift-version: 5.8 env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -116,9 +116,9 @@ jobs: matrix: include: - image: swiftlang/swift:nightly-jammy + - image: swift:5.10-jammy - image: swift:5.9-jammy - image: swift:5.8-focal - - image: swift:5.7-focal name: Integration Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: diff --git a/Package.swift b/Package.swift index a4a223855..5e980bf59 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 /* * Copyright 2017, gRPC Authors All rights reserved. * @@ -106,7 +106,7 @@ extension Target.Dependency { static let interopTestImplementation: Self = .target(name: "GRPCInteroperabilityTestsImplementation") static let interoperabilityTests: Self = .target(name: "InteroperabilityTests") - + // Product dependencies static let argumentParser: Self = .product( name: "ArgumentParser", @@ -386,7 +386,7 @@ extension Target { .grpcProtobuf ] ) - + static let interopTestImplementation: Target = .target( name: "GRPCInteroperabilityTestsImplementation", dependencies: [ diff --git a/Performance/Benchmarks/Package.swift b/Performance/Benchmarks/Package.swift index 1fb63864b..2c1fe63ab 100644 --- a/Performance/Benchmarks/Package.swift +++ b/Performance/Benchmarks/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.8 /* * Copyright 2023, gRPC Authors All rights reserved. * diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 78% rename from Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json rename to Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json index b20317867..b642696c1 100644 --- a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json +++ b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -1,7 +1,7 @@ { "mallocCountTotal" : 11, "memoryLeaked" : 0, - "releaseCount" : 1011, + "releaseCount" : 3012, "retainCount" : 2000, "syscalls" : 0 } diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_string.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Add_string.p90.json rename to Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json similarity index 58% rename from Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json rename to Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json index 5605d8671..1b1303873 100644 --- a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json +++ b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json @@ -1,7 +1,7 @@ { "mallocCountTotal" : 0, "memoryLeaked" : 0, - "releaseCount" : 1005, - "retainCount" : 1005, + "releaseCount" : 3001, + "retainCount" : 1000, "syscalls" : 0 } diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json similarity index 58% rename from Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json rename to Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json index 5605d8671..1b1303873 100644 --- a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json +++ b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json @@ -1,7 +1,7 @@ { "mallocCountTotal" : 0, "memoryLeaked" : 0, - "releaseCount" : 1005, - "retainCount" : 1005, + "releaseCount" : 3001, + "retainCount" : 1000, "syscalls" : 0 } diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json similarity index 59% rename from Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json index 451ede0a1..b59f05063 100644 --- a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -1,7 +1,7 @@ { "mallocCountTotal" : 2000, "memoryLeaked" : 0, - "releaseCount" : 4005, - "retainCount" : 2005, + "releaseCount" : 6001, + "retainCount" : 2000, "syscalls" : 0 } diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json similarity index 58% rename from Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json rename to Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json index 5605d8671..1b1303873 100644 --- a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json +++ b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json @@ -1,7 +1,7 @@ { "mallocCountTotal" : 0, "memoryLeaked" : 0, - "releaseCount" : 1005, - "retainCount" : 1005, + "releaseCount" : 3001, + "retainCount" : 1000, "syscalls" : 0 } diff --git a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json similarity index 77% rename from Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename to Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json index 295dd81c5..5750750bc 100644 --- a/Performance/Benchmarks/Thresholds/5.7/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json +++ b/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json @@ -1,7 +1,7 @@ { "mallocCountTotal" : 0, "memoryLeaked" : 0, - "releaseCount" : 2002000, + "releaseCount" : 2002001, "retainCount" : 1999000, "syscalls" : 0 } diff --git a/Performance/QPSBenchmark/Package.swift b/Performance/QPSBenchmark/Package.swift index 76bb18d92..5a26bda5f 100644 --- a/Performance/QPSBenchmark/Package.swift +++ b/Performance/QPSBenchmark/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 /* * Copyright 2020, gRPC Authors All rights reserved. * diff --git a/README.md b/README.md index e49c67fa8..0b892bcca 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ gRPC Swift Version | Earliest Swift Version `1.8.0 ..< 1.11.0` | 5.4 `1.11.0..< 1.16.0`.| 5.5 `1.16.0..< 1.20.0` | 5.6 -`1.20.0...` | 5.7 +`1.20.0..< 1.22.0` | 5.7 +`1.22.0...` | 5.8 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md index c93880bc7..02ed91538 100644 --- a/Sources/GRPC/Docs.docc/index.md +++ b/Sources/GRPC/Docs.docc/index.md @@ -28,7 +28,8 @@ gRPC Swift Version | Earliest Swift Version `1.8.0 ..< 1.11.0` | 5.4 `1.11.0..< 1.16.0`.| 5.5 `1.16.0..< 1.20.0` | 5.6 -`1.20.0...` | 5.7 +`1.20.0..< 1.22.0` | 5.7 +`1.22.0...` | 5.8 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. From ec5b39631fae80e2fc065c5ba13dcd48eeac0859 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Mon, 11 Mar 2024 09:44:10 +0000 Subject: [PATCH 261/580] Add a minimum connections configuration to the ConnectionPool (#1822) Motivation: Sometimes, depending on the characteristics of the traffic, we may need to execute requests in spikes. It's possible that when we get a large spike of requests, a lot of connections are opened, which are subsequently closed by the idle timer. However, when another spike comes next, because we don't have any available open connections, latency will suffer as a result of us having to open new connections. Modifications: Add a ConnectionPool configuration to allow a minimum number of connections to remain open, that is, make a number of connections not go idle. Result: A number of connections will remain open even when there are no open streams on them. --- Sources/GRPC/ClientConnection.swift | 1 + Sources/GRPC/ConnectionManager.swift | 17 +++- .../GRPC/ConnectionPool/ConnectionPool.swift | 53 +++++++++++- .../GRPC/ConnectionPool/GRPCChannelPool.swift | 4 + Sources/GRPC/ConnectionPool/PoolManager.swift | 8 ++ .../GRPC/ConnectionPool/PooledChannel.swift | 1 + Sources/GRPC/GRPCIdleHandler.swift | 16 ++-- Tests/GRPCTests/ConnectionManagerTests.swift | 4 + .../ConnectionPool/ConnectionPoolTests.swift | 86 ++++++++++++++++++- .../PoolManagerStateMachineTests.swift | 1 + 10 files changed, 180 insertions(+), 11 deletions(-) diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index d238b7def..26adea540 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -131,6 +131,7 @@ public final class ClientConnection: Sendable { self.connectionManager = ConnectionManager( configuration: configuration, connectivityDelegate: monitor, + idleBehavior: .closeWhenIdleTimeout, logger: configuration.backgroundActivityLogger ) } diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index e21be63b2..d30d12ee0 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -25,6 +25,14 @@ import NIOHTTP2 // event loop is being used. @usableFromInline internal final class ConnectionManager: @unchecked Sendable { + + /// Whether the connection managed by this manager should be allowed to go idle and be closed, or + /// if it should remain open indefinitely even when there are no active streams. + internal enum IdleBehavior { + case closeWhenIdleTimeout + case neverGoIdle + } + internal enum Reconnect { case none case after(TimeInterval) @@ -324,6 +332,9 @@ internal final class ConnectionManager: @unchecked Sendable { /// attempts should be made at all. private let connectionBackoff: ConnectionBackoff? + /// Whether this connection should be allowed to go idle (and thus be closed when the idle timer fires). + internal let idleBehavior: IdleBehavior + /// A logger. internal var logger: Logger @@ -356,12 +367,14 @@ internal final class ConnectionManager: @unchecked Sendable { configuration: ClientConnection.Configuration, channelProvider: ConnectionManagerChannelProvider? = nil, connectivityDelegate: ConnectionManagerConnectivityDelegate?, + idleBehavior: IdleBehavior, logger: Logger ) { self.init( eventLoop: configuration.eventLoopGroup.next(), channelProvider: channelProvider ?? DefaultChannelProvider(configuration: configuration), callStartBehavior: configuration.callStartBehavior.wrapped, + idleBehavior: idleBehavior, connectionBackoff: configuration.connectionBackoff, connectivityDelegate: connectivityDelegate, http2Delegate: nil, @@ -373,6 +386,7 @@ internal final class ConnectionManager: @unchecked Sendable { eventLoop: EventLoop, channelProvider: ConnectionManagerChannelProvider, callStartBehavior: CallStartBehavior.Behavior, + idleBehavior: IdleBehavior, connectionBackoff: ConnectionBackoff?, connectivityDelegate: ConnectionManagerConnectivityDelegate?, http2Delegate: ConnectionManagerHTTP2Delegate?, @@ -392,6 +406,7 @@ internal final class ConnectionManager: @unchecked Sendable { self.connectionBackoff = connectionBackoff self.connectivityDelegate = connectivityDelegate self.http2Delegate = http2Delegate + self.idleBehavior = idleBehavior self.connectionID = connectionID self.channelNumber = channelNumber @@ -799,7 +814,7 @@ internal final class ConnectionManager: @unchecked Sendable { // Yes, after some time. case let .after(delay): let error = GRPCStatus(code: .unavailable, message: "Connection closed while connecting") - // Fail the candidate mux promise. KEep the 'readyChannelMuxPromise' as we'll try again. + // Fail the candidate mux promise. Keep the 'readyChannelMuxPromise' as we'll try again. connecting.candidateMuxPromise.fail(error) let scheduled = self.eventLoop.scheduleTask(in: .seconds(timeInterval: delay)) { diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 039d788f3..9ea75b405 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -85,6 +85,11 @@ internal final class ConnectionPool { @usableFromInline internal let maxWaiters: Int + /// The number of connections in the pool that should always be kept open (i.e. they won't go idle). + /// In other words, it's the number of connections for which we should ignore idle timers. + @usableFromInline + internal let minConnections: Int + /// Configuration for backoff between subsequence connection attempts. @usableFromInline internal let connectionBackoff: ConnectionBackoff @@ -157,6 +162,7 @@ internal final class ConnectionPool { init( eventLoop: EventLoop, maxWaiters: Int, + minConnections: Int, reservationLoadThreshold: Double, assumedMaxConcurrentStreams: Int, connectionBackoff: ConnectionBackoff, @@ -176,6 +182,7 @@ internal final class ConnectionPool { self._connections = [:] self.maxWaiters = maxWaiters + self.minConnections = minConnections self.waiters = CircularBuffer(initialCapacity: 16) self.eventLoop = eventLoop @@ -201,17 +208,25 @@ internal final class ConnectionPool { ] ) self._connections.reserveCapacity(connections) + var numberOfKeepOpenConnections = self.minConnections while self._connections.count < connections { - self.addConnectionToPool() + // If we have less than the minimum number of connections, don't let + // the new connection close when idle. + let idleBehavior = + numberOfKeepOpenConnections > 0 + ? ConnectionManager.IdleBehavior.neverGoIdle : .closeWhenIdleTimeout + numberOfKeepOpenConnections -= 1 + self.addConnectionToPool(idleBehavior: idleBehavior) } } /// Make and add a new connection to the pool. - private func addConnectionToPool() { + private func addConnectionToPool(idleBehavior: ConnectionManager.IdleBehavior) { let manager = ConnectionManager( eventLoop: self.eventLoop, channelProvider: self.channelProvider, callStartBehavior: .waitsForConnectivity, + idleBehavior: idleBehavior, connectionBackoff: self.connectionBackoff, connectivityDelegate: self, http2Delegate: self, @@ -220,6 +235,19 @@ internal final class ConnectionPool { let id = manager.id self._connections[id] = PerConnectionState(manager: manager) self.delegate?.connectionAdded(id: .init(id)) + + // If it's one of the connections that should be kept open, then connect + // straight away. + switch idleBehavior { + case .neverGoIdle: + self.eventLoop.execute { + if manager.sync.isIdle { + manager.sync.startConnecting() + } + } + case .closeWhenIdleTimeout: + () + } } // MARK: - Called from the pool manager @@ -689,8 +717,9 @@ extension ConnectionPool: ConnectionManagerConnectivityDelegate { // Grab the number of reserved streams (before invalidating the index by adding a connection). let reservedStreams = self._connections.values[index].reservedStreams - // Replace the connection with a new idle one. - self.addConnectionToPool() + // Replace the connection with a new idle one. Keep the idle behavior, so that + // if it's a connection that should be kept alive, we maintain it. + self.addConnectionToPool(idleBehavior: manager.idleBehavior) // Since we're removing this connection from the pool (and no new streams can be created on // the connection), the pool manager can ignore any streams reserved against this connection. @@ -881,6 +910,22 @@ extension ConnectionPool { return self.pool._connections.values.reduce(0) { $0 &+ ($1.manager.sync.isIdle ? 1 : 0) } } + /// The number of active (i.e. connecting or ready) connections in the pool. + internal var activeConnections: Int { + self.pool.eventLoop.assertInEventLoop() + return self.pool._connections.values.reduce(0) { + $0 &+ (($1.manager.sync.isReady || $1.manager.sync.isConnecting) ? 1 : 0) + } + } + + /// The number of connections in the pool in transient failure state. + internal var transientFailureConnections: Int { + self.pool.eventLoop.assertInEventLoop() + return self.pool._connections.values.reduce(0) { + $0 &+ ($1.manager.sync.isTransientFailure ? 1 : 0) + } + } + /// The number of streams currently available to reserve across all connections in the pool. internal var availableStreams: Int { self.pool.eventLoop.assertInEventLoop() diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift index 8076342a0..0af26fecc 100644 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift @@ -275,6 +275,10 @@ extension GRPCChannelPool.Configuration { /// Defaults to 100. public var maxWaitersPerEventLoop: Int = 100 + /// The minimum number of connections to keep open in this pool, per EventLoop. + /// This number of connections per EventLoop will never go idle and be closed. + public var minConnectionsPerEventLoop: Int = 0 + /// The maximum amount of time a caller is willing to wait for a stream for before timing out. /// /// Defaults to 30 seconds. diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index 49d184b7e..593d11e7d 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -33,6 +33,11 @@ internal final class PoolManager { @usableFromInline var maxWaiters: Int + /// The minimum number of connections to keep open per pool. + /// This number of connections will never go idle and be closed. + @usableFromInline + var minConnections: Int + /// A load threshold in the range `0.0 ... 1.0` beyond which another connection will be started /// (assuming there is a connection available to start). @usableFromInline @@ -62,6 +67,7 @@ internal final class PoolManager { internal init( maxConnections: Int, maxWaiters: Int, + minConnections: Int, loadThreshold: Double, assumedMaxConcurrentStreams: Int = 100, connectionBackoff: ConnectionBackoff, @@ -70,6 +76,7 @@ internal final class PoolManager { ) { self.maxConnections = maxConnections self.maxWaiters = maxWaiters + self.minConnections = minConnections self.loadThreshold = loadThreshold self.assumedMaxConcurrentStreams = assumedMaxConcurrentStreams self.connectionBackoff = connectionBackoff @@ -225,6 +232,7 @@ internal final class PoolManager { return ConnectionPool( eventLoop: eventLoop, maxWaiters: configuration.maxWaiters, + minConnections: configuration.minConnections, reservationLoadThreshold: configuration.loadThreshold, assumedMaxConcurrentStreams: configuration.assumedMaxConcurrentStreams, connectionBackoff: configuration.connectionBackoff, diff --git a/Sources/GRPC/ConnectionPool/PooledChannel.swift b/Sources/GRPC/ConnectionPool/PooledChannel.swift index 1b706451f..a8715070e 100644 --- a/Sources/GRPC/ConnectionPool/PooledChannel.swift +++ b/Sources/GRPC/ConnectionPool/PooledChannel.swift @@ -96,6 +96,7 @@ internal final class PooledChannel: GRPCChannel { perPoolConfiguration: .init( maxConnections: configuration.connectionPool.connectionsPerEventLoop, maxWaiters: configuration.connectionPool.maxWaitersPerEventLoop, + minConnections: configuration.connectionPool.minConnectionsPerEventLoop, loadThreshold: configuration.connectionPool.reservationLoadThreshold, assumedMaxConcurrentStreams: 100, connectionBackoff: configuration.connectionBackoff, diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift index 6371cb7a7..0f9492163 100644 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ b/Sources/GRPC/GRPCIdleHandler.swift @@ -24,7 +24,8 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { typealias OutboundOut = HTTP2Frame /// The amount of time to wait before closing the channel when there are no active streams. - private let idleTimeout: TimeAmount + /// If nil, then we shouldn't schedule idle tasks. + private let idleTimeout: TimeAmount? /// The ping handler. private var pingHandler: PingHandler @@ -78,7 +79,12 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { logger: Logger ) { self.mode = .client(connectionManager, multiplexer) - self.idleTimeout = idleTimeout + switch connectionManager.idleBehavior { + case .neverGoIdle: + self.idleTimeout = nil + case .closeWhenIdleTimeout: + self.idleTimeout = idleTimeout + } self.stateMachine = .init(role: .client, logger: logger) self.pingHandler = PingHandler( pingCode: 5, @@ -135,7 +141,7 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { } // Handle idle timeout creation/cancellation. - if let idleTask = operations.idleTask { + if let idleTimeout = self.idleTimeout, let idleTask = operations.idleTask { switch idleTask { case let .cancel(task): self.stateMachine.logger.debug("idle timeout task cancelled") @@ -145,9 +151,9 @@ internal final class GRPCIdleHandler: ChannelInboundHandler { if self.idleTimeout != .nanoseconds(.max), let context = self.context { self.stateMachine.logger.debug( "scheduling idle timeout task", - metadata: [MetadataKey.delayMs: "\(self.idleTimeout.milliseconds)"] + metadata: [MetadataKey.delayMs: "\(idleTimeout.milliseconds)"] ) - let task = context.eventLoop.scheduleTask(in: self.idleTimeout) { + let task = context.eventLoop.scheduleTask(in: idleTimeout) { self.stateMachine.logger.debug("idle timeout task fired") self.idleTimeoutFired() } diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index 5bd1ad432..ff27d53f8 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -59,6 +59,7 @@ class ConnectionManagerTests: GRPCTestCase { configuration: configuration, channelProvider: channelProvider.map { HookedChannelProvider($0) }, connectivityDelegate: self.monitor, + idleBehavior: .closeWhenIdleTimeout, logger: self.logger ) } @@ -948,6 +949,7 @@ extension ConnectionManagerTests { return loop.makeFailedFuture(DoomedChannelError()) }, connectivityDelegate: nil, + idleBehavior: .closeWhenIdleTimeout, logger: self.logger ) let candidate = manager.getHTTP2Multiplexer() @@ -1207,6 +1209,7 @@ extension ConnectionManagerTests { return eventLoop.makeSucceededFuture(channel) }, callStartBehavior: .waitsForConnectivity, + idleBehavior: .closeWhenIdleTimeout, connectionBackoff: ConnectionBackoff(), connectivityDelegate: nil, http2Delegate: http2, @@ -1383,6 +1386,7 @@ extension ConnectionManagerTests { configuration: configuration, channelProvider: Provider(), connectivityDelegate: self.monitor, + idleBehavior: .closeWhenIdleTimeout, logger: self.logger ) diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 5c2738774..65bb416cd 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -53,6 +53,8 @@ final class ConnectionPoolTests: GRPCTestCase { private func makePool( waiters: Int = 1000, reservationLoadThreshold: Double = 0.9, + minConnections: Int = 0, + assumedMaxConcurrentStreams: Int = 100, now: @escaping () -> NIODeadline = { .now() }, connectionBackoff: ConnectionBackoff = ConnectionBackoff(), delegate: GRPCConnectionPoolDelegate? = nil, @@ -63,8 +65,9 @@ final class ConnectionPoolTests: GRPCTestCase { return ConnectionPool( eventLoop: self.eventLoop, maxWaiters: waiters, + minConnections: minConnections, reservationLoadThreshold: reservationLoadThreshold, - assumedMaxConcurrentStreams: 100, + assumedMaxConcurrentStreams: assumedMaxConcurrentStreams, connectionBackoff: connectionBackoff, channelProvider: channelProvider, streamLender: HookedStreamLender( @@ -1069,6 +1072,87 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertEqual(error.code, .deadlineExceeded) XCTAssertNotEqual(error.code, .shutdown) } + + func testMinimumConnectionsAreOpenRightAfterInitializing() { + let controller = ChannelController() + let pool = self.makePool(minConnections: 5, channelProvider: controller) + + pool.initialize(connections: 20) + self.eventLoop.run() + + XCTAssertEqual(pool.sync.connections, 20) + XCTAssertEqual(pool.sync.idleConnections, 15) + XCTAssertEqual(pool.sync.activeConnections, 5) + XCTAssertEqual(pool.sync.waiters, 0) + XCTAssertEqual(pool.sync.availableStreams, 0) + XCTAssertEqual(pool.sync.reservedStreams, 0) + XCTAssertEqual(pool.sync.transientFailureConnections, 0) + } + + func testMinimumConnectionsAreOpenAfterOneIsQuiesced() { + let controller = ChannelController() + let pool = self.makePool( + minConnections: 1, + assumedMaxConcurrentStreams: 1, + channelProvider: controller + ) + + // Initialize two connections, and make sure that only one of them is active, + // since we have set minConnections to 1. + pool.initialize(connections: 2) + self.eventLoop.run() + XCTAssertEqual(pool.sync.connections, 2) + XCTAssertEqual(pool.sync.idleConnections, 1) + XCTAssertEqual(pool.sync.activeConnections, 1) + XCTAssertEqual(pool.sync.transientFailureConnections, 0) + + // Open two streams, which, because the maxConcurrentStreams is 1, will + // create two channels. + let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + $0.eventLoop.makeSucceededVoidFuture() + } + let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + $0.eventLoop.makeSucceededVoidFuture() + } + + // Start creating the channels. + self.eventLoop.run() + + // Make both connections ready. + controller.connectChannel(atIndex: 0) + controller.sendSettingsToChannel(atIndex: 0) + controller.connectChannel(atIndex: 1) + controller.sendSettingsToChannel(atIndex: 1) + + // Run the loop to create the streams/connections. + self.eventLoop.run() + XCTAssertNoThrow(try w1.wait()) + controller.openStreamInChannel(atIndex: 0) + XCTAssertNoThrow(try w2.wait()) + controller.openStreamInChannel(atIndex: 1) + + XCTAssertEqual(pool.sync.connections, 2) + XCTAssertEqual(pool.sync.idleConnections, 0) + XCTAssertEqual(pool.sync.activeConnections, 2) + XCTAssertEqual(pool.sync.transientFailureConnections, 0) + + // Quiesce the connection that should be kept alive. + // Another connection should be brought back up immediately after, to maintain + // the minimum number of active connections that won't go idle. + controller.sendGoAwayToChannel(atIndex: 0) + XCTAssertEqual(pool.sync.connections, 3) + XCTAssertEqual(pool.sync.idleConnections, 1) + XCTAssertEqual(pool.sync.activeConnections, 2) + XCTAssertEqual(pool.sync.transientFailureConnections, 0) + + // Now quiesce the other one. This will add a new idle connection, but it + // won't connect it right away. + controller.sendGoAwayToChannel(atIndex: 1) + XCTAssertEqual(pool.sync.connections, 4) + XCTAssertEqual(pool.sync.idleConnections, 2) + XCTAssertEqual(pool.sync.activeConnections, 2) + XCTAssertEqual(pool.sync.transientFailureConnections, 0) + } } extension ConnectionPool { diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift index 438bb461d..02f33fa33 100644 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift @@ -33,6 +33,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { return ConnectionPool( eventLoop: eventLoop, maxWaiters: maxWaiters, + minConnections: 0, reservationLoadThreshold: loadThreshold, assumedMaxConcurrentStreams: maxConcurrentStreams, connectionBackoff: connectionBackoff, From a060296853ac7a53bcae94b4e346fc64439af3b8 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:33:04 +0000 Subject: [PATCH 262/580] Echo Metadata in TestService (#1824) Motivation: We want to be able to test that the server receives metadata and can send both initial and trailing metadata. Modifications: - added extensions for server requests that extract the initial and trailing metadata that should be echoed - added the initial and trailing metadata n the server responses Result: The interop tests will test that the server sends metadata as initial and trailing. --- .../InteroperabilityTests/TestService.swift | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift index bb522fec5..d20796c35 100644 --- a/Sources/InteroperabilityTests/TestService.swift +++ b/Sources/InteroperabilityTests/TestService.swift @@ -32,7 +32,12 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { request: ServerRequest.Single ) async throws -> ServerResponse.Single { let message = Grpc_Testing_TestService.Method.EmptyCall.Output() - return ServerResponse.Single(message: message) + let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() + return ServerResponse.Single( + message: message, + metadata: initialMetadata, + trailingMetadata: trailingMetadata + ) } /// Server implements `unaryCall` which immediately returns a `SimpleResponse` with a payload @@ -72,7 +77,13 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { } } - return ServerResponse.Single(message: responseMessage) + let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() + + return ServerResponse.Single( + message: responseMessage, + metadata: initialMetadata, + trailingMetadata: trailingMetadata + ) } /// Server gets the default `SimpleRequest` proto as the request. The content of the request is @@ -102,7 +113,8 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() + return ServerResponse.Stream(metadata: initialMetadata) { writer in for responseParameter in request.message.responseParameters { let response = Grpc_Testing_StreamingOutputCallResponse.with { response in response.payload = Grpc_Testing_Payload.with { payload in @@ -113,7 +125,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { // We convert the `intervalUs` value from microseconds to nanoseconds. try await Task.sleep(nanoseconds: UInt64(responseParameter.intervalUs) * 1000) } - return [:] + return trailingMetadata } } @@ -135,7 +147,12 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { $0.aggregatedPayloadSize = Int32(aggregatedPayloadSize) } - return ServerResponse.Single(message: responseMessage) + let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() + return ServerResponse.Single( + message: responseMessage, + metadata: initialMetadata, + trailingMetadata: trailingMetadata + ) } /// Server implements `fullDuplexCall` by replying, in order, with one @@ -148,7 +165,8 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() + return ServerResponse.Stream(metadata: initialMetadata) { writer in for try await message in request.messages { // If a request message has a responseStatus set, the server should return that status. // If the code is an error code, the server will throw an error containing that code @@ -174,7 +192,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { try await writer.write(response) } } - return [:] + return trailingMetadata } } @@ -189,3 +207,18 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") } } + +extension Metadata { + fileprivate func makeInitialAndTrailingMetadata() -> (Metadata, Metadata) { + var initialMetadata = Metadata() + var trailingMetadata = Metadata() + for value in self[stringValues: "x-grpc-test-echo-initial"] { + initialMetadata.addString(value, forKey: "x-grpc-test-echo-initial") + } + for value in self[binaryValues: "x-grpc-test-echo-trailing-bin"] { + trailingMetadata.addBinary(value, forKey: "x-grpc-test-echo-trailing-bin") + } + + return (initialMetadata, trailingMetadata) + } +} From 108a0b9db119d9f5f05133e3e9d448dd7248761e Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:04:28 +0000 Subject: [PATCH 263/580] Unit tests for interop tests (#1823) Motivation: We need unit tests that run the interop tests with the in process transport. Modifications: Created a test target and implemented the tests for each interop test, that catch AssertionFailure errors when the interop tests fail. Result: We will run the interop tests with the in process transport. --- Package.swift | 10 ++ .../InteroperabilityTestCase.swift | 2 +- .../InProcessInteroperabilityTests.swift | 99 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift diff --git a/Package.swift b/Package.swift index 5e980bf59..41b6bffa8 100644 --- a/Package.swift +++ b/Package.swift @@ -361,6 +361,15 @@ extension Target { ] ) + static let inProcessInteroperabilityTests: Target = .testTarget( + name: "InProcessInteroperabilityTests", + dependencies: [ + .grpcInProcessTransport, + .interoperabilityTests, + .grpcCore + ] + ) + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -724,6 +733,7 @@ let package = Package( .grpcHTTP2TransportNIOTransportServicesTests, .grpcProtobufTests, .grpcProtobufCodeGenTests, + .inProcessInteroperabilityTests ] ) diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index 6a909e6ea..90a34d287 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -66,7 +66,7 @@ public enum InteroperabilityTestCase: String, CaseIterable { } } -@available(macOS 13.0, *) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension InteroperabilityTestCase { /// Return a new instance of the test case. public func makeTest() -> InteroperabilityTest { diff --git a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift new file mode 100644 index 000000000..f06549cd2 --- /dev/null +++ b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift @@ -0,0 +1,99 @@ +/* + * 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 GRPCInProcessTransport +import XCTest + +@testable import InteroperabilityTests + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class InProcessInteroperabilityTests: XCTestCase { + func runInProcessTransport( + interopTestCase: InteroperabilityTestCase + ) async throws { + do { + let inProcess = InProcessTransport.makePair() + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + let server = GRPCServer(transports: [inProcess.server], services: [TestService()]) + try await server.run() + } + + group.addTask { + try await withThrowingTaskGroup(of: Void.self) { clientGroup in + let client = GRPCClient(transport: inProcess.client) + clientGroup.addTask { + try await client.run() + } + try await interopTestCase.makeTest().run(client: client) + + clientGroup.cancelAll() + } + } + + try await group.next() + group.cancelAll() + } + } catch let error as AssertionFailure { + XCTFail(error.message) + } + } + + func testEmtyUnary() async throws { + try await self.runInProcessTransport(interopTestCase: .emptyUnary) + } + + func testLargeUnary() async throws { + try await self.runInProcessTransport(interopTestCase: .largeUnary) + } + + func testClientStreaming() async throws { + try await self.runInProcessTransport(interopTestCase: .clientStreaming) + } + + func testServerStreaming() async throws { + try await self.runInProcessTransport(interopTestCase: .serverStreaming) + } + + func testPingPong() async throws { + try await self.runInProcessTransport(interopTestCase: .pingPong) + } + + func testEmptyStream() async throws { + try await self.runInProcessTransport(interopTestCase: .emptyStream) + } + + func testCustomMetdata() async throws { + try await self.runInProcessTransport(interopTestCase: .customMetadata) + } + + func testStatusCodeAndMessage() async throws { + try await self.runInProcessTransport(interopTestCase: .statusCodeAndMessage) + } + + func testSpecialStatusMessage() async throws { + try await self.runInProcessTransport(interopTestCase: .specialStatusMessage) + } + + func testUnimplementedMethod() async throws { + try await self.runInProcessTransport(interopTestCase: .unimplementedMethod) + } + + func testUnimplementedService() async throws { + try await self.runInProcessTransport(interopTestCase: .unimplementedService) + } +} From 53e2739912b0d3090cfa6a8345fcadbe6fe2ba1a Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 13 Mar 2024 15:16:43 +0000 Subject: [PATCH 264/580] Add target and generated code for QPS (#1827) Motivation: The first step in implementing QPS testing for V2 is to add the proto files representing the services and messages used in QPS and generating the code for them. Modifications: - Modified the fetch script to add the proto files we need - Modified the generate script to generate code for them -Created executable target for QPS worker Result: We can start implementing the performance worker. --- Package.swift | 10 + Protos/fetch.sh | 10 +- Protos/generate.sh | 26 + Protos/upstream/grpc/core/stats.proto | 38 + .../grpc/testing/benchmark_service.proto | 48 + Protos/upstream/grpc/testing/control.proto | 299 ++ Protos/upstream/grpc/testing/messages.proto | 347 +++ Protos/upstream/grpc/testing/payloads.proto | 44 + Protos/upstream/grpc/testing/stats.proto | 87 + .../grpc/testing/worker_service.proto | 49 + .../Generated/grpc_core_stats.pb.swift | 312 +++ .../grpc_testing_benchmark_service.grpc.swift | 410 +++ .../grpc_testing_benchmark_service.pb.swift | 38 + .../Generated/grpc_testing_control.pb.swift | 2420 +++++++++++++++++ .../Generated/grpc_testing_messages.pb.swift | 2200 +++++++++++++++ .../Generated/grpc_testing_payloads.pb.swift | 335 +++ .../Generated/grpc_testing_stats.pb.swift | 470 ++++ .../grpc_testing_worker_service.grpc.swift | 179 ++ .../grpc_testing_worker_service.pb.swift | 38 + Sources/performance-worker/main.swift | 18 + 20 files changed, 7377 insertions(+), 1 deletion(-) create mode 100644 Protos/upstream/grpc/core/stats.proto create mode 100644 Protos/upstream/grpc/testing/benchmark_service.proto create mode 100644 Protos/upstream/grpc/testing/control.proto create mode 100644 Protos/upstream/grpc/testing/messages.proto create mode 100644 Protos/upstream/grpc/testing/payloads.proto create mode 100644 Protos/upstream/grpc/testing/stats.proto create mode 100644 Protos/upstream/grpc/testing/worker_service.proto create mode 100644 Sources/performance-worker/Generated/grpc_core_stats.pb.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_control.pb.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_messages.pb.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_stats.pb.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift create mode 100644 Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift create mode 100644 Sources/performance-worker/main.swift diff --git a/Package.swift b/Package.swift index 41b6bffa8..e6b4fb0bb 100644 --- a/Package.swift +++ b/Package.swift @@ -91,6 +91,7 @@ extension Target.Dependency { static let grpc: Self = .target(name: grpcTargetName) static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") + static let performanceWorker: Self = .target(name: "performance-worker") static let reflectionService: Self = .target(name: "GRPCReflectionService") static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") static let grpcProtobuf: Self = .target(name: "GRPCProtobuf") @@ -245,6 +246,14 @@ extension Target { ] ) + static let performanceWorker: Target = .executableTarget( + name: "performance-worker", + dependencies: [ + .grpcCore, + .grpcProtobuf + ] + ) + static let grpcSwiftPlugin: Target = .plugin( name: "GRPCSwiftPlugin", capability: .buildTool(), @@ -722,6 +731,7 @@ let package = Package( .grpcProtobuf, .grpcProtobufCodeGen, .interoperabilityTestImplementation, + .performanceWorker, // v2 tests .grpcCoreTests, diff --git a/Protos/fetch.sh b/Protos/fetch.sh index 36cf64580..ba24d1558 100755 --- a/Protos/fetch.sh +++ b/Protos/fetch.sh @@ -31,14 +31,22 @@ rm -rf "$upstream" # Create new directories to poulate. These are based on proto package name # rather than source repository name. -mkdir -p "$upstream/grpc" mkdir -p "$upstream/google" +mkdir -p "$upstream/grpc/testing" +mkdir -p "$upstream/grpc/core" # Copy over the grpc-proto protos. cp -rp "$checkouts/grpc-proto/grpc/service_config" "$upstream/grpc/service_config" cp -rp "$checkouts/grpc-proto/grpc/lookup" "$upstream/grpc/lookup" cp -rp "$checkouts/grpc-proto/grpc/reflection" "$upstream/grpc/reflection" cp -rp "$checkouts/grpc-proto/grpc/examples" "$upstream/grpc/examples" +cp -rp "$checkouts/grpc-proto/grpc/testing/benchmark_service.proto" "$upstream/grpc/testing/benchmark_service.proto" +cp -rp "$checkouts/grpc-proto/grpc/testing/messages.proto" "$upstream/grpc/testing/messages.proto" +cp -rp "$checkouts/grpc-proto/grpc/testing/worker_service.proto" "$upstream/grpc/testing/worker_service.proto" +cp -rp "$checkouts/grpc-proto/grpc/testing/control.proto" "$upstream/grpc/testing/control.proto" +cp -rp "$checkouts/grpc-proto/grpc/testing/payloads.proto" "$upstream/grpc/testing/payloads.proto" +cp -rp "$checkouts/grpc-proto/grpc/testing/stats.proto" "$upstream/grpc/testing/stats.proto" +cp -rp "$checkouts/grpc-proto/grpc/core/stats.proto" "$upstream/grpc/core/stats.proto" # Copy over the googleapis protos. mkdir -p "$upstream/google/rpc" diff --git a/Protos/generate.sh b/Protos/generate.sh index 6a392597e..86f91b557 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -201,6 +201,29 @@ function generate_service_messages_interop_tests { done } +function generate_worker_service { + local protos=( + "$here/upstream/grpc/testing/payloads.proto" + "$here/upstream/grpc/testing/control.proto" + "$here/upstream/grpc/testing/messages.proto" + "$here/upstream/grpc/testing/stats.proto" + "$here/upstream/grpc/testing/benchmark_service.proto" + "$here/upstream/grpc/testing/worker_service.proto" + ) + local output="$root/Sources/performance-worker/Generated" + + generate_message "$here/upstream/grpc/core/stats.proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=PathToUnderscores" + + for proto in "${protos[@]}"; do + generate_message "$proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=PathToUnderscores" + if [ "$proto" == "$here/upstream/grpc/testing/worker_service.proto" ]; then + generate_grpc "$proto" "$here/upstream" "$output" "Visibility=Internal" "Client=false" "_V2=true" "FileNaming=PathToUnderscores" + else + generate_grpc "$proto" "$here/upstream" "$output" "Visibility=Internal" "_V2=true" "FileNaming=PathToUnderscores" + fi + done +} + #------------------------------------------------------------------------------ # Examples @@ -220,3 +243,6 @@ generate_service_messages_interop_tests # Misc. tests generate_normalization_for_tests generate_rpc_code_for_tests + +# Performance worker service +generate_worker_service diff --git a/Protos/upstream/grpc/core/stats.proto b/Protos/upstream/grpc/core/stats.proto new file mode 100644 index 000000000..ac181b043 --- /dev/null +++ b/Protos/upstream/grpc/core/stats.proto @@ -0,0 +1,38 @@ +// Copyright 2017 gRPC authors. +// +// 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. + +syntax = "proto3"; + +package grpc.core; + +message Bucket { + double start = 1; + uint64 count = 2; +} + +message Histogram { + repeated Bucket buckets = 1; +} + +message Metric { + string name = 1; + oneof value { + uint64 count = 10; + Histogram histogram = 11; + } +} + +message Stats { + repeated Metric metrics = 1; +} diff --git a/Protos/upstream/grpc/testing/benchmark_service.proto b/Protos/upstream/grpc/testing/benchmark_service.proto new file mode 100644 index 000000000..5209bd6ef --- /dev/null +++ b/Protos/upstream/grpc/testing/benchmark_service.proto @@ -0,0 +1,48 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. +syntax = "proto3"; + +import "grpc/testing/messages.proto"; + +package grpc.testing; + +option java_multiple_files = true; +option java_package = "io.grpc.testing"; +option java_outer_classname = "BenchmarkServiceProto"; + +service BenchmarkService { + // One request followed by one response. + // The server returns the client payload as-is. + rpc UnaryCall(SimpleRequest) returns (SimpleResponse); + + // Repeated sequence of one request followed by one response. + // Should be called streaming ping-pong + // The server returns the client payload as-is on each response + rpc StreamingCall(stream SimpleRequest) returns (stream SimpleResponse); + + // Single-sided unbounded streaming from client to server + // The server returns the client payload as-is once the client does WritesDone + rpc StreamingFromClient(stream SimpleRequest) returns (SimpleResponse); + + // Single-sided unbounded streaming from server to client + // The server repeatedly returns the client payload as-is + rpc StreamingFromServer(SimpleRequest) returns (stream SimpleResponse); + + // Two-sided unbounded streaming between server to client + // Both sides send the content of their own choice to the other + rpc StreamingBothWays(stream SimpleRequest) returns (stream SimpleResponse); +} diff --git a/Protos/upstream/grpc/testing/control.proto b/Protos/upstream/grpc/testing/control.proto new file mode 100644 index 000000000..e309e5f9c --- /dev/null +++ b/Protos/upstream/grpc/testing/control.proto @@ -0,0 +1,299 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +syntax = "proto3"; + +import "grpc/testing/payloads.proto"; +import "grpc/testing/stats.proto"; +import "google/protobuf/timestamp.proto"; + +package grpc.testing; + +option java_multiple_files = true; +option java_package = "io.grpc.testing"; +option java_outer_classname = "ControlProto"; + +enum ClientType { + // Many languages support a basic distinction between using + // sync or async client, and this allows the specification + SYNC_CLIENT = 0; + ASYNC_CLIENT = 1; + OTHER_CLIENT = 2; // used for some language-specific variants + CALLBACK_CLIENT = 3; +} + +enum ServerType { + SYNC_SERVER = 0; + ASYNC_SERVER = 1; + ASYNC_GENERIC_SERVER = 2; + OTHER_SERVER = 3; // used for some language-specific variants + CALLBACK_SERVER = 4; +} + +enum RpcType { + UNARY = 0; + STREAMING = 1; + STREAMING_FROM_CLIENT = 2; + STREAMING_FROM_SERVER = 3; + STREAMING_BOTH_WAYS = 4; +} + +// Parameters of poisson process distribution, which is a good representation +// of activity coming in from independent identical stationary sources. +message PoissonParams { + // The rate of arrivals (a.k.a. lambda parameter of the exp distribution). + double offered_load = 1; +} + +// Once an RPC finishes, immediately start a new one. +// No configuration parameters needed. +message ClosedLoopParams {} + +message LoadParams { + oneof load { + ClosedLoopParams closed_loop = 1; + PoissonParams poisson = 2; + }; +} + +// presence of SecurityParams implies use of TLS +message SecurityParams { + bool use_test_ca = 1; + string server_host_override = 2; + string cred_type = 3; +} + +message ChannelArg { + string name = 1; + oneof value { + string str_value = 2; + int32 int_value = 3; + } +} + +message ClientConfig { + // List of targets to connect to. At least one target needs to be specified. + repeated string server_targets = 1; + ClientType client_type = 2; + SecurityParams security_params = 3; + // How many concurrent RPCs to start for each channel. + // For synchronous client, use a separate thread for each outstanding RPC. + int32 outstanding_rpcs_per_channel = 4; + // Number of independent client channels to create. + // i-th channel will connect to server_target[i % server_targets.size()] + int32 client_channels = 5; + // Only for async client. Number of threads to use to start/manage RPCs. + int32 async_client_threads = 7; + RpcType rpc_type = 8; + // The requested load for the entire client (aggregated over all the threads). + LoadParams load_params = 10; + PayloadConfig payload_config = 11; + HistogramParams histogram_params = 12; + + // Specify the cores we should run the client on, if desired + repeated int32 core_list = 13; + int32 core_limit = 14; + + // If we use an OTHER_CLIENT client_type, this string gives more detail + string other_client_api = 15; + + repeated ChannelArg channel_args = 16; + + // Number of threads that share each completion queue + int32 threads_per_cq = 17; + + // Number of messages on a stream before it gets finished/restarted + int32 messages_per_stream = 18; + + // Use coalescing API when possible. + bool use_coalesce_api = 19; + + // If 0, disabled. Else, specifies the period between gathering latency + // medians in milliseconds. + int32 median_latency_collection_interval_millis = 20; + + // Number of client processes. 0 indicates no restriction. + int32 client_processes = 21; +} + +message ClientStatus { ClientStats stats = 1; } + +// Request current stats +message Mark { + // if true, the stats will be reset after taking their snapshot. + bool reset = 1; +} + +message ClientArgs { + oneof argtype { + ClientConfig setup = 1; + Mark mark = 2; + } +} + +message ServerConfig { + ServerType server_type = 1; + SecurityParams security_params = 2; + // Port on which to listen. Zero means pick unused port. + int32 port = 4; + // Only for async server. Number of threads used to serve the requests. + int32 async_server_threads = 7; + // Specify the number of cores to limit server to, if desired + int32 core_limit = 8; + // payload config, used in generic server. + // Note this must NOT be used in proto (non-generic) servers. For proto servers, + // 'response sizes' must be configured from the 'response_size' field of the + // 'SimpleRequest' objects in RPC requests. + PayloadConfig payload_config = 9; + + // Specify the cores we should run the server on, if desired + repeated int32 core_list = 10; + + // If we use an OTHER_SERVER client_type, this string gives more detail + string other_server_api = 11; + + // Number of threads that share each completion queue + int32 threads_per_cq = 12; + + // c++-only options (for now) -------------------------------- + + // Buffer pool size (no buffer pool specified if unset) + int32 resource_quota_size = 1001; + repeated ChannelArg channel_args = 1002; + + // Number of server processes. 0 indicates no restriction. + int32 server_processes = 21; +} + +message ServerArgs { + oneof argtype { + ServerConfig setup = 1; + Mark mark = 2; + } +} + +message ServerStatus { + ServerStats stats = 1; + // the port bound by the server + int32 port = 2; + // Number of cores available to the server + int32 cores = 3; +} + +message CoreRequest { +} + +message CoreResponse { + // Number of cores available on the server + int32 cores = 1; +} + +message Void { +} + +// A single performance scenario: input to qps_json_driver +message Scenario { + // Human readable name for this scenario + string name = 1; + // Client configuration + ClientConfig client_config = 2; + // Number of clients to start for the test + int32 num_clients = 3; + // Server configuration + ServerConfig server_config = 4; + // Number of servers to start for the test + int32 num_servers = 5; + // Warmup period, in seconds + int32 warmup_seconds = 6; + // Benchmark time, in seconds + int32 benchmark_seconds = 7; + // Number of workers to spawn locally (usually zero) + int32 spawn_local_worker_count = 8; +} + +// A set of scenarios to be run with qps_json_driver +message Scenarios { + repeated Scenario scenarios = 1; +} + +// Basic summary that can be computed from ClientStats and ServerStats +// once the scenario has finished. +message ScenarioResultSummary +{ + // Total number of operations per second over all clients. What is counted as 1 'operation' depends on the benchmark scenarios: + // For unary benchmarks, an operation is processing of a single unary RPC. + // For streaming benchmarks, an operation is processing of a single ping pong of request and response. + double qps = 1; + // QPS per server core. + double qps_per_server_core = 2; + // The total server cpu load based on system time across all server processes, expressed as percentage of a single cpu core. + // For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server + // processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. + // Same explanation for the total client cpu load below. + double server_system_time = 3; + // The total server cpu load based on user time across all server processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) + double server_user_time = 4; + // The total client cpu load based on system time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) + double client_system_time = 5; + // The total client cpu load based on user time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) + double client_user_time = 6; + + // X% latency percentiles (in nanoseconds) + double latency_50 = 7; + double latency_90 = 8; + double latency_95 = 9; + double latency_99 = 10; + double latency_999 = 11; + + // server cpu usage percentage + double server_cpu_usage = 12; + + // Number of requests that succeeded/failed + double successful_requests_per_second = 13; + double failed_requests_per_second = 14; + + // Number of polls called inside completion queue per request + double client_polls_per_request = 15; + double server_polls_per_request = 16; + + // Queries per CPU-sec over all servers or clients + double server_queries_per_cpu_sec = 17; + double client_queries_per_cpu_sec = 18; + + + // Start and end time for the test scenario + google.protobuf.Timestamp start_time = 19; + google.protobuf.Timestamp end_time =20; +} + +// Results of a single benchmark scenario. +message ScenarioResult { + // Inputs used to run the scenario. + Scenario scenario = 1; + // Histograms from all clients merged into one histogram. + HistogramData latencies = 2; + // Client stats for each client + repeated ClientStats client_stats = 3; + // Server stats for each server + repeated ServerStats server_stats = 4; + // Number of cores available to each server + repeated int32 server_cores = 5; + // An after-the-fact computed summary + ScenarioResultSummary summary = 6; + // Information on success or failure of each worker + repeated bool client_success = 7; + repeated bool server_success = 8; + // Number of failed requests (one row per status code seen) + repeated RequestResultCount request_results = 9; +} diff --git a/Protos/upstream/grpc/testing/messages.proto b/Protos/upstream/grpc/testing/messages.proto new file mode 100644 index 000000000..99e34dcc8 --- /dev/null +++ b/Protos/upstream/grpc/testing/messages.proto @@ -0,0 +1,347 @@ +// Copyright 2015-2016 gRPC authors. +// +// 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 definitions to be used by integration test service definitions. + +syntax = "proto3"; + +package grpc.testing; + +option java_package = "io.grpc.testing.integration"; + +// TODO(dgq): Go back to using well-known types once +// https://github.com/grpc/grpc/issues/6980 has been fixed. +// import "google/protobuf/wrappers.proto"; +message BoolValue { + // The bool value. + bool value = 1; +} + +// The type of payload that should be returned. +enum PayloadType { + // Compressable text format. + COMPRESSABLE = 0; +} + +// A block of data, to simply increase gRPC message size. +message Payload { + // The type of data in body. + PayloadType type = 1; + // Primary contents of payload. + bytes body = 2; +} + +// A protobuf representation for grpc status. This is used by test +// clients to specify a status that the server should attempt to return. +message EchoStatus { + int32 code = 1; + string message = 2; +} + +// The type of route that a client took to reach a server w.r.t. gRPCLB. +// The server must fill in "fallback" if it detects that the RPC reached +// the server via the "gRPCLB fallback" path, and "backend" if it detects +// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got +// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly +// how this detection is done is context and server dependent. +enum GrpclbRouteType { + // Server didn't detect the route that a client took to reach it. + GRPCLB_ROUTE_TYPE_UNKNOWN = 0; + // Indicates that a client reached a server via gRPCLB fallback. + GRPCLB_ROUTE_TYPE_FALLBACK = 1; + // Indicates that a client reached a server as a gRPCLB-given backend. + GRPCLB_ROUTE_TYPE_BACKEND = 2; +} + +// Unary request. +message SimpleRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, server randomly chooses one from other formats. + PayloadType response_type = 1; + + // Desired payload size in the response from the server. + int32 response_size = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether SimpleResponse should include username. + bool fill_username = 4; + + // Whether SimpleResponse should include OAuth scope. + bool fill_oauth_scope = 5; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue response_compressed = 6; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // Whether the server should expect this request to be compressed. + BoolValue expect_compressed = 8; + + // Whether SimpleResponse should include server_id. + bool fill_server_id = 9; + + // Whether SimpleResponse should include grpclb_route_type. + bool fill_grpclb_route_type = 10; + + // If set the server should record this metrics report data for the current RPC. + TestOrcaReport orca_per_query_report = 11; +} + +// Unary response, as configured by the request. +message SimpleResponse { + // Payload to increase message size. + Payload payload = 1; + // The user the request came from, for verifying authentication was + // successful when the client expected it. + string username = 2; + // OAuth scope. + string oauth_scope = 3; + + // Server ID. This must be unique among different server instances, + // but the same across all RPC's made to a particular server instance. + string server_id = 4; + // gRPCLB Path. + GrpclbRouteType grpclb_route_type = 5; + + // Server hostname. + string hostname = 6; +} + +// Client-streaming request. +message StreamingInputCallRequest { + // Optional input payload sent along with the request. + Payload payload = 1; + + // Whether the server should expect this request to be compressed. This field + // is "nullable" in order to interoperate seamlessly with servers not able to + // implement the full compression tests by introspecting the call to verify + // the request's compression status. + BoolValue expect_compressed = 2; + + // Not expecting any payload from the response. +} + +// Client-streaming response. +message StreamingInputCallResponse { + // Aggregated size of payloads received from the client. + int32 aggregated_payload_size = 1; +} + +// Configuration for a particular response. +message ResponseParameters { + // Desired payload sizes in responses from the server. + int32 size = 1; + + // Desired interval between consecutive responses in the response stream in + // microseconds. + int32 interval_us = 2; + + // Whether to request the server to compress the response. This field is + // "nullable" in order to interoperate seamlessly with clients not able to + // implement the full compression tests by introspecting the call to verify + // the response's compression status. + BoolValue compressed = 3; +} + +// Server-streaming request. +message StreamingOutputCallRequest { + // Desired payload type in the response from the server. + // If response_type is RANDOM, the payload from each response in the stream + // might be of different types. This is to simulate a mixed type of payload + // stream. + PayloadType response_type = 1; + + // Configuration for each expected response message. + repeated ResponseParameters response_parameters = 2; + + // Optional input payload sent along with the request. + Payload payload = 3; + + // Whether server should return a given status + EchoStatus response_status = 7; + + // If set the server should update this metrics report data at the OOB server. + TestOrcaReport orca_oob_report = 8; +} + +// Server-streaming response, as configured by the request and parameters. +message StreamingOutputCallResponse { + // Payload to increase response size. + Payload payload = 1; +} + +// For reconnect interop test only. +// Client tells server what reconnection parameters it used. +message ReconnectParams { + int32 max_reconnect_backoff_ms = 1; +} + +// For reconnect interop test only. +// Server tells client whether its reconnects are following the spec and the +// reconnect backoffs it saw. +message ReconnectInfo { + bool passed = 1; + repeated int32 backoff_ms = 2; +} + +message LoadBalancerStatsRequest { + // Request stats for the next num_rpcs sent by client. + int32 num_rpcs = 1; + // If num_rpcs have not completed within timeout_sec, return partial results. + int32 timeout_sec = 2; + // Response header + trailer metadata entries we want the values of. + // Matching of the keys is case-insensitive as per rfc7540#section-8.1.2 + // * (asterisk) is a special value that will return all metadata entries + repeated string metadata_keys = 3; +} + +message LoadBalancerStatsResponse { + enum MetadataType { + UNKNOWN = 0; + INITIAL = 1; + TRAILING = 2; + } + message MetadataEntry { + // Key, exactly as received from the server. Case may be different from what + // was requested in the LoadBalancerStatsRequest) + string key = 1; + // Value, exactly as received from the server. + string value = 2; + // Metadata type + MetadataType type = 3; + } + message RpcMetadata { + // metadata values for each rpc for the keys specified in + // LoadBalancerStatsRequest.metadata_keys. + repeated MetadataEntry metadata = 1; + } + message MetadataByPeer { + // List of RpcMetadata in for each RPC with a given peer + repeated RpcMetadata rpc_metadata = 1; + } + message RpcsByPeer { + // The number of completed RPCs for each peer. + map rpcs_by_peer = 1; + } + // The number of completed RPCs for each peer. + map rpcs_by_peer = 1; + // The number of RPCs that failed to record a remote peer. + int32 num_failures = 2; + map rpcs_by_method = 3; + // All the metadata of all RPCs for each peer. + map metadatas_by_peer = 4; +} + +// Request for retrieving a test client's accumulated stats. +message LoadBalancerAccumulatedStatsRequest {} + +// Accumulated stats for RPCs sent by a test client. +message LoadBalancerAccumulatedStatsResponse { + // The total number of RPCs have ever issued for each type. + // Deprecated: use stats_per_method.rpcs_started instead. + map num_rpcs_started_by_method = 1 [deprecated = true]; + // The total number of RPCs have ever completed successfully for each type. + // Deprecated: use stats_per_method.result instead. + map num_rpcs_succeeded_by_method = 2 [deprecated = true]; + // The total number of RPCs have ever failed for each type. + // Deprecated: use stats_per_method.result instead. + map num_rpcs_failed_by_method = 3 [deprecated = true]; + + message MethodStats { + // The number of RPCs that were started for this method. + int32 rpcs_started = 1; + + // The number of RPCs that completed with each status for this method. The + // key is the integral value of a google.rpc.Code; the value is the count. + map result = 2; + } + + // Per-method RPC statistics. The key is the RpcType in string form; e.g. + // 'EMPTY_CALL' or 'UNARY_CALL' + map stats_per_method = 4; +} + +// Configurations for a test client. +message ClientConfigureRequest { + // Type of RPCs to send. + enum RpcType { + EMPTY_CALL = 0; + UNARY_CALL = 1; + } + + // Metadata to be attached for the given type of RPCs. + message Metadata { + RpcType type = 1; + string key = 2; + string value = 3; + } + + // The types of RPCs the client sends. + repeated RpcType types = 1; + // The collection of custom metadata to be attached to RPCs sent by the client. + repeated Metadata metadata = 2; + // The deadline to use, in seconds, for all RPCs. If unset or zero, the + // client will use the default from the command-line. + int32 timeout_sec = 3; +} + +// Response for updating a test client's configuration. +message ClientConfigureResponse {} + +message MemorySize { + int64 rss = 1; +} + +// Metrics data the server will update and send to the client. It mirrors orca load report +// https://github.com/cncf/xds/blob/eded343319d09f30032952beda9840bbd3dcf7ac/xds/data/orca/v3/orca_load_report.proto#L15, +// but avoids orca dependency. Used by both per-query and out-of-band reporting tests. +message TestOrcaReport { + double cpu_utilization = 1; + double memory_utilization = 2; + map request_cost = 3; + map utilization = 4; +} + +// Status that will be return to callers of the Hook method +message SetReturnStatusRequest { + int32 grpc_code_to_return = 1; + string grpc_status_description = 2; +} + +message HookRequest { + enum HookRequestCommand { + // Default value + UNSPECIFIED = 0; + // Start the HTTP endpoint + START = 1; + // Stop + STOP = 2; + // Return from HTTP GET/POST + RETURN = 3; + } + HookRequestCommand command = 1; + int32 grpc_code_to_return = 2; + string grpc_status_description = 3; + // Server port to listen to + int32 server_port = 4; +} + +message HookResponse { +} diff --git a/Protos/upstream/grpc/testing/payloads.proto b/Protos/upstream/grpc/testing/payloads.proto new file mode 100644 index 000000000..8cbc9db6c --- /dev/null +++ b/Protos/upstream/grpc/testing/payloads.proto @@ -0,0 +1,44 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +syntax = "proto3"; + +package grpc.testing; + +option java_multiple_files = true; +option java_package = "io.grpc.testing"; +option java_outer_classname = "PayloadsProto"; + +message ByteBufferParams { + int32 req_size = 1; + int32 resp_size = 2; +} + +message SimpleProtoParams { + int32 req_size = 1; + int32 resp_size = 2; +} + +message ComplexProtoParams { + // TODO (vpai): Fill this in once the details of complex, representative + // protos are decided +} + +message PayloadConfig { + oneof payload { + ByteBufferParams bytebuf_params = 1; + SimpleProtoParams simple_params = 2; + ComplexProtoParams complex_params = 3; + } +} diff --git a/Protos/upstream/grpc/testing/stats.proto b/Protos/upstream/grpc/testing/stats.proto new file mode 100644 index 000000000..1f0fae4e5 --- /dev/null +++ b/Protos/upstream/grpc/testing/stats.proto @@ -0,0 +1,87 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +syntax = "proto3"; + +package grpc.testing; + +import "grpc/core/stats.proto"; + +option java_multiple_files = true; +option java_package = "io.grpc.testing"; +option java_outer_classname = "StatsProto"; + +message ServerStats { + // wall clock time change in seconds since last reset + double time_elapsed = 1; + + // change in user time (in seconds) used by the server since last reset + double time_user = 2; + + // change in server time (in seconds) used by the server process and all + // threads since last reset + double time_system = 3; + + // change in total cpu time of the server (data from proc/stat) + uint64 total_cpu_time = 4; + + // change in idle time of the server (data from proc/stat) + uint64 idle_cpu_time = 5; + + // Number of polls called inside completion queue + uint64 cq_poll_count = 6; + + // Core library stats + grpc.core.Stats core_stats = 7; +} + +// Histogram params based on grpc/support/histogram.c +message HistogramParams { + double resolution = 1; // first bucket is [0, 1 + resolution) + double max_possible = 2; // use enough buckets to allow this value +} + +// Histogram data based on grpc/support/histogram.c +message HistogramData { + repeated uint32 bucket = 1; + double min_seen = 2; + double max_seen = 3; + double sum = 4; + double sum_of_squares = 5; + double count = 6; +} + +message RequestResultCount { + int32 status_code = 1; + int64 count = 2; +} + +message ClientStats { + // Latency histogram. Data points are in nanoseconds. + HistogramData latencies = 1; + + // See ServerStats for details. + double time_elapsed = 2; + double time_user = 3; + double time_system = 4; + + // Number of failed requests (one row per status code seen) + repeated RequestResultCount request_results = 5; + + // Number of polls called inside completion queue + uint64 cq_poll_count = 6; + + // Core library stats + grpc.core.Stats core_stats = 7; +} diff --git a/Protos/upstream/grpc/testing/worker_service.proto b/Protos/upstream/grpc/testing/worker_service.proto new file mode 100644 index 000000000..ff3aa2931 --- /dev/null +++ b/Protos/upstream/grpc/testing/worker_service.proto @@ -0,0 +1,49 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +// An integration test service that covers all the method signature permutations +// of unary/streaming requests/responses. +syntax = "proto3"; + +import "grpc/testing/control.proto"; + +package grpc.testing; + +option java_multiple_files = true; +option java_package = "io.grpc.testing"; +option java_outer_classname = "WorkerServiceProto"; + +service WorkerService { + // Start server with specified workload. + // First request sent specifies the ServerConfig followed by ServerStatus + // response. After that, a "Mark" can be sent anytime to request the latest + // stats. Closing the stream will initiate shutdown of the test server + // and once the shutdown has finished, the OK status is sent to terminate + // this RPC. + rpc RunServer(stream ServerArgs) returns (stream ServerStatus); + + // Start client with specified workload. + // First request sent specifies the ClientConfig followed by ClientStatus + // response. After that, a "Mark" can be sent anytime to request the latest + // stats. Closing the stream will initiate shutdown of the test client + // and once the shutdown has finished, the OK status is sent to terminate + // this RPC. + rpc RunClient(stream ClientArgs) returns (stream ClientStatus); + + // Just return the core count - unary call + rpc CoreCount(CoreRequest) returns (CoreResponse); + + // Quit this worker + rpc QuitWorker(Void) returns (Void); +} diff --git a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift new file mode 100644 index 000000000..9f7d794b8 --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift @@ -0,0 +1,312 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/core/stats.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2017 gRPC authors. +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct Grpc_Core_Bucket { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var start: Double = 0 + + var count: UInt64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Core_Histogram { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var buckets: [Grpc_Core_Bucket] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Core_Metric { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: String = String() + + var value: Grpc_Core_Metric.OneOf_Value? = nil + + var count: UInt64 { + get { + if case .count(let v)? = value {return v} + return 0 + } + set {value = .count(newValue)} + } + + var histogram: Grpc_Core_Histogram { + get { + if case .histogram(let v)? = value {return v} + return Grpc_Core_Histogram() + } + set {value = .histogram(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Value: Equatable { + case count(UInt64) + case histogram(Grpc_Core_Histogram) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Core_Metric.OneOf_Value, rhs: Grpc_Core_Metric.OneOf_Value) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.count, .count): return { + guard case .count(let l) = lhs, case .count(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.histogram, .histogram): return { + guard case .histogram(let l) = lhs, case .histogram(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +struct Grpc_Core_Stats { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var metrics: [Grpc_Core_Metric] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Core_Bucket: @unchecked Sendable {} +extension Grpc_Core_Histogram: @unchecked Sendable {} +extension Grpc_Core_Metric: @unchecked Sendable {} +extension Grpc_Core_Metric.OneOf_Value: @unchecked Sendable {} +extension Grpc_Core_Stats: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.core" + +extension Grpc_Core_Bucket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Bucket" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "start"), + 2: .same(proto: "count"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularDoubleField(value: &self.start) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.count) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.start != 0 { + try visitor.visitSingularDoubleField(value: self.start, fieldNumber: 1) + } + if self.count != 0 { + try visitor.visitSingularUInt64Field(value: self.count, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Core_Bucket, rhs: Grpc_Core_Bucket) -> Bool { + if lhs.start != rhs.start {return false} + if lhs.count != rhs.count {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Core_Histogram: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Histogram" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "buckets"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.buckets) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.buckets.isEmpty { + try visitor.visitRepeatedMessageField(value: self.buckets, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Core_Histogram, rhs: Grpc_Core_Histogram) -> Bool { + if lhs.buckets != rhs.buckets {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Core_Metric: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Metric" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 10: .same(proto: "count"), + 11: .same(proto: "histogram"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + case 10: try { + var v: UInt64? + try decoder.decodeSingularUInt64Field(value: &v) + if let v = v { + if self.value != nil {try decoder.handleConflictingOneOf()} + self.value = .count(v) + } + }() + case 11: try { + var v: Grpc_Core_Histogram? + var hadOneofValue = false + if let current = self.value { + hadOneofValue = true + if case .histogram(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.value = .histogram(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + switch self.value { + case .count?: try { + guard case .count(let v)? = self.value else { preconditionFailure() } + try visitor.visitSingularUInt64Field(value: v, fieldNumber: 10) + }() + case .histogram?: try { + guard case .histogram(let v)? = self.value else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Core_Metric, rhs: Grpc_Core_Metric) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Core_Stats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Stats" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "metrics"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.metrics) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.metrics.isEmpty { + try visitor.visitRepeatedMessageField(value: self.metrics, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Core_Stats, rhs: Grpc_Core_Stats) -> Bool { + if lhs.metrics != rhs.metrics {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift new file mode 100644 index 000000000..6dfe17fa0 --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -0,0 +1,410 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +/// An integration test service that covers all the method signature permutations +/// of unary/streaming requests/responses. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/benchmark_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +internal enum Grpc_Testing_BenchmarkService { + internal enum Method { + internal enum UnaryCall { + internal typealias Input = Grpc_Testing_SimpleRequest + internal typealias Output = Grpc_Testing_SimpleResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.BenchmarkService", + method: "UnaryCall" + ) + } + internal enum StreamingCall { + internal typealias Input = Grpc_Testing_SimpleRequest + internal typealias Output = Grpc_Testing_SimpleResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.BenchmarkService", + method: "StreamingCall" + ) + } + internal enum StreamingFromClient { + internal typealias Input = Grpc_Testing_SimpleRequest + internal typealias Output = Grpc_Testing_SimpleResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.BenchmarkService", + method: "StreamingFromClient" + ) + } + internal enum StreamingFromServer { + internal typealias Input = Grpc_Testing_SimpleRequest + internal typealias Output = Grpc_Testing_SimpleResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.BenchmarkService", + method: "StreamingFromServer" + ) + } + internal enum StreamingBothWays { + internal typealias Input = Grpc_Testing_SimpleRequest + internal typealias Output = Grpc_Testing_SimpleResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.BenchmarkService", + method: "StreamingBothWays" + ) + } + internal static let descriptors: [MethodDescriptor] = [ + UnaryCall.descriptor, + StreamingCall.descriptor, + StreamingFromClient.descriptor, + StreamingFromServer.descriptor, + StreamingBothWays.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias StreamingServiceProtocol = Grpc_Testing_BenchmarkServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ServiceProtocol = Grpc_Testing_BenchmarkServiceServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ClientProtocol = Grpc_Testing_BenchmarkServiceClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias Client = Grpc_Testing_BenchmarkServiceClient +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// One request followed by one response. + /// The server returns the client payload as-is. + func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Repeated sequence of one request followed by one response. + /// Should be called streaming ping-pong + /// The server returns the client payload as-is on each response + func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Single-sided unbounded streaming from client to server + /// The server returns the client payload as-is once the client does WritesDone + func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Single-sided unbounded streaming from server to client + /// The server repeatedly returns the client payload as-is + func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Two-sided unbounded streaming between server to client + /// Both sides send the content of their own choice to the other + func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.unaryCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.streamingCall(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.streamingFromClient(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.streamingFromServer(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.streamingBothWays(request: request) + } + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_BenchmarkService.StreamingServiceProtocol { + /// One request followed by one response. + /// The server returns the client payload as-is. + func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + + /// Repeated sequence of one request followed by one response. + /// Should be called streaming ping-pong + /// The server returns the client payload as-is on each response + func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Single-sided unbounded streaming from client to server + /// The server returns the client payload as-is once the client does WritesDone + func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Single + + /// Single-sided unbounded streaming from server to client + /// The server repeatedly returns the client payload as-is + func streamingFromServer(request: ServerRequest.Single) async throws -> ServerResponse.Stream + + /// Two-sided unbounded streaming between server to client + /// Both sides send the content of their own choice to the other + func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Partial conformance to `Grpc_Testing_BenchmarkServiceStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_BenchmarkService.ServiceProtocol { + internal func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + internal func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.streamingFromClient(request: request) + return ServerResponse.Stream(single: response) + } + + internal func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.streamingFromServer(request: ServerRequest.Single(stream: request)) + return response + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { + /// One request followed by one response. + /// The server returns the client payload as-is. + func unaryCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// Repeated sequence of one request followed by one response. + /// Should be called streaming ping-pong + /// The server returns the client payload as-is on each response + func streamingCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + /// Single-sided unbounded streaming from client to server + /// The server returns the client payload as-is once the client does WritesDone + func streamingFromClient( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// Single-sided unbounded streaming from server to client + /// The server repeatedly returns the client payload as-is + func streamingFromServer( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + /// Two-sided unbounded streaming between server to client + /// Both sides send the content of their own choice to the other + func streamingBothWays( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_BenchmarkService.ClientProtocol { + internal func unaryCall( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.unaryCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + internal func streamingCall( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.streamingCall( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + internal func streamingFromClient( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.streamingFromClient( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + internal func streamingFromServer( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.streamingFromServer( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } + + internal func streamingBothWays( + request: ClientRequest.Stream, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.streamingBothWays( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + body + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkService.ClientProtocol { + private let client: GRPCCore.GRPCClient + + internal init(client: GRPCCore.GRPCClient) { + self.client = client + } + + /// One request followed by one response. + /// The server returns the client payload as-is. + internal func unaryCall( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// Repeated sequence of one request followed by one response. + /// Should be called streaming ping-pong + /// The server returns the client payload as-is on each response + internal func streamingCall( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// Single-sided unbounded streaming from client to server + /// The server returns the client payload as-is once the client does WritesDone + internal func streamingFromClient( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// Single-sided unbounded streaming from server to client + /// The server repeatedly returns the client payload as-is + internal func streamingFromServer( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } + + /// Two-sided unbounded streaming between server to client + /// Both sides send the content of their own choice to the other + internal func streamingBothWays( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, + serializer: serializer, + deserializer: deserializer, + handler: body + ) + } +} \ No newline at end of file diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift new file mode 100644 index 000000000..745e5b757 --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift @@ -0,0 +1,38 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/benchmark_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 gRPC authors. +// +// 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. + +/// An integration test service that covers all the method signature permutations +/// of unary/streaming requests/responses. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} diff --git a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift new file mode 100644 index 000000000..3f3fb66c7 --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift @@ -0,0 +1,2420 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/control.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 gRPC authors. +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +enum Grpc_Testing_ClientType: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Many languages support a basic distinction between using + /// sync or async client, and this allows the specification + case syncClient // = 0 + case asyncClient // = 1 + + /// used for some language-specific variants + case otherClient // = 2 + case callbackClient // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .syncClient + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .syncClient + case 1: self = .asyncClient + case 2: self = .otherClient + case 3: self = .callbackClient + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .syncClient: return 0 + case .asyncClient: return 1 + case .otherClient: return 2 + case .callbackClient: return 3 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Grpc_Testing_ClientType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_ClientType] = [ + .syncClient, + .asyncClient, + .otherClient, + .callbackClient, + ] +} + +#endif // swift(>=4.2) + +enum Grpc_Testing_ServerType: SwiftProtobuf.Enum { + typealias RawValue = Int + case syncServer // = 0 + case asyncServer // = 1 + case asyncGenericServer // = 2 + + /// used for some language-specific variants + case otherServer // = 3 + case callbackServer // = 4 + case UNRECOGNIZED(Int) + + init() { + self = .syncServer + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .syncServer + case 1: self = .asyncServer + case 2: self = .asyncGenericServer + case 3: self = .otherServer + case 4: self = .callbackServer + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .syncServer: return 0 + case .asyncServer: return 1 + case .asyncGenericServer: return 2 + case .otherServer: return 3 + case .callbackServer: return 4 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Grpc_Testing_ServerType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_ServerType] = [ + .syncServer, + .asyncServer, + .asyncGenericServer, + .otherServer, + .callbackServer, + ] +} + +#endif // swift(>=4.2) + +enum Grpc_Testing_RpcType: SwiftProtobuf.Enum { + typealias RawValue = Int + case unary // = 0 + case streaming // = 1 + case streamingFromClient // = 2 + case streamingFromServer // = 3 + case streamingBothWays // = 4 + case UNRECOGNIZED(Int) + + init() { + self = .unary + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unary + case 1: self = .streaming + case 2: self = .streamingFromClient + case 3: self = .streamingFromServer + case 4: self = .streamingBothWays + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unary: return 0 + case .streaming: return 1 + case .streamingFromClient: return 2 + case .streamingFromServer: return 3 + case .streamingBothWays: return 4 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Grpc_Testing_RpcType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_RpcType] = [ + .unary, + .streaming, + .streamingFromClient, + .streamingFromServer, + .streamingBothWays, + ] +} + +#endif // swift(>=4.2) + +/// Parameters of poisson process distribution, which is a good representation +/// of activity coming in from independent identical stationary sources. +struct Grpc_Testing_PoissonParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The rate of arrivals (a.k.a. lambda parameter of the exp distribution). + var offeredLoad: Double = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Once an RPC finishes, immediately start a new one. +/// No configuration parameters needed. +struct Grpc_Testing_ClosedLoopParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_LoadParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var load: Grpc_Testing_LoadParams.OneOf_Load? = nil + + var closedLoop: Grpc_Testing_ClosedLoopParams { + get { + if case .closedLoop(let v)? = load {return v} + return Grpc_Testing_ClosedLoopParams() + } + set {load = .closedLoop(newValue)} + } + + var poisson: Grpc_Testing_PoissonParams { + get { + if case .poisson(let v)? = load {return v} + return Grpc_Testing_PoissonParams() + } + set {load = .poisson(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Load: Equatable { + case closedLoop(Grpc_Testing_ClosedLoopParams) + case poisson(Grpc_Testing_PoissonParams) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Testing_LoadParams.OneOf_Load, rhs: Grpc_Testing_LoadParams.OneOf_Load) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.closedLoop, .closedLoop): return { + guard case .closedLoop(let l) = lhs, case .closedLoop(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.poisson, .poisson): return { + guard case .poisson(let l) = lhs, case .poisson(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +/// presence of SecurityParams implies use of TLS +struct Grpc_Testing_SecurityParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var useTestCa: Bool = false + + var serverHostOverride: String = String() + + var credType: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_ChannelArg { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: String = String() + + var value: Grpc_Testing_ChannelArg.OneOf_Value? = nil + + var strValue: String { + get { + if case .strValue(let v)? = value {return v} + return String() + } + set {value = .strValue(newValue)} + } + + var intValue: Int32 { + get { + if case .intValue(let v)? = value {return v} + return 0 + } + set {value = .intValue(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Value: Equatable { + case strValue(String) + case intValue(Int32) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Testing_ChannelArg.OneOf_Value, rhs: Grpc_Testing_ChannelArg.OneOf_Value) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.strValue, .strValue): return { + guard case .strValue(let l) = lhs, case .strValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.intValue, .intValue): return { + guard case .intValue(let l) = lhs, case .intValue(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +struct Grpc_Testing_ClientConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// List of targets to connect to. At least one target needs to be specified. + var serverTargets: [String] { + get {return _storage._serverTargets} + set {_uniqueStorage()._serverTargets = newValue} + } + + var clientType: Grpc_Testing_ClientType { + get {return _storage._clientType} + set {_uniqueStorage()._clientType = newValue} + } + + var securityParams: Grpc_Testing_SecurityParams { + get {return _storage._securityParams ?? Grpc_Testing_SecurityParams()} + set {_uniqueStorage()._securityParams = newValue} + } + /// Returns true if `securityParams` has been explicitly set. + var hasSecurityParams: Bool {return _storage._securityParams != nil} + /// Clears the value of `securityParams`. Subsequent reads from it will return its default value. + mutating func clearSecurityParams() {_uniqueStorage()._securityParams = nil} + + /// How many concurrent RPCs to start for each channel. + /// For synchronous client, use a separate thread for each outstanding RPC. + var outstandingRpcsPerChannel: Int32 { + get {return _storage._outstandingRpcsPerChannel} + set {_uniqueStorage()._outstandingRpcsPerChannel = newValue} + } + + /// Number of independent client channels to create. + /// i-th channel will connect to server_target[i % server_targets.size()] + var clientChannels: Int32 { + get {return _storage._clientChannels} + set {_uniqueStorage()._clientChannels = newValue} + } + + /// Only for async client. Number of threads to use to start/manage RPCs. + var asyncClientThreads: Int32 { + get {return _storage._asyncClientThreads} + set {_uniqueStorage()._asyncClientThreads = newValue} + } + + var rpcType: Grpc_Testing_RpcType { + get {return _storage._rpcType} + set {_uniqueStorage()._rpcType = newValue} + } + + /// The requested load for the entire client (aggregated over all the threads). + var loadParams: Grpc_Testing_LoadParams { + get {return _storage._loadParams ?? Grpc_Testing_LoadParams()} + set {_uniqueStorage()._loadParams = newValue} + } + /// Returns true if `loadParams` has been explicitly set. + var hasLoadParams: Bool {return _storage._loadParams != nil} + /// Clears the value of `loadParams`. Subsequent reads from it will return its default value. + mutating func clearLoadParams() {_uniqueStorage()._loadParams = nil} + + var payloadConfig: Grpc_Testing_PayloadConfig { + get {return _storage._payloadConfig ?? Grpc_Testing_PayloadConfig()} + set {_uniqueStorage()._payloadConfig = newValue} + } + /// Returns true if `payloadConfig` has been explicitly set. + var hasPayloadConfig: Bool {return _storage._payloadConfig != nil} + /// Clears the value of `payloadConfig`. Subsequent reads from it will return its default value. + mutating func clearPayloadConfig() {_uniqueStorage()._payloadConfig = nil} + + var histogramParams: Grpc_Testing_HistogramParams { + get {return _storage._histogramParams ?? Grpc_Testing_HistogramParams()} + set {_uniqueStorage()._histogramParams = newValue} + } + /// Returns true if `histogramParams` has been explicitly set. + var hasHistogramParams: Bool {return _storage._histogramParams != nil} + /// Clears the value of `histogramParams`. Subsequent reads from it will return its default value. + mutating func clearHistogramParams() {_uniqueStorage()._histogramParams = nil} + + /// Specify the cores we should run the client on, if desired + var coreList: [Int32] { + get {return _storage._coreList} + set {_uniqueStorage()._coreList = newValue} + } + + var coreLimit: Int32 { + get {return _storage._coreLimit} + set {_uniqueStorage()._coreLimit = newValue} + } + + /// If we use an OTHER_CLIENT client_type, this string gives more detail + var otherClientApi: String { + get {return _storage._otherClientApi} + set {_uniqueStorage()._otherClientApi = newValue} + } + + var channelArgs: [Grpc_Testing_ChannelArg] { + get {return _storage._channelArgs} + set {_uniqueStorage()._channelArgs = newValue} + } + + /// Number of threads that share each completion queue + var threadsPerCq: Int32 { + get {return _storage._threadsPerCq} + set {_uniqueStorage()._threadsPerCq = newValue} + } + + /// Number of messages on a stream before it gets finished/restarted + var messagesPerStream: Int32 { + get {return _storage._messagesPerStream} + set {_uniqueStorage()._messagesPerStream = newValue} + } + + /// Use coalescing API when possible. + var useCoalesceApi: Bool { + get {return _storage._useCoalesceApi} + set {_uniqueStorage()._useCoalesceApi = newValue} + } + + /// If 0, disabled. Else, specifies the period between gathering latency + /// medians in milliseconds. + var medianLatencyCollectionIntervalMillis: Int32 { + get {return _storage._medianLatencyCollectionIntervalMillis} + set {_uniqueStorage()._medianLatencyCollectionIntervalMillis = newValue} + } + + /// Number of client processes. 0 indicates no restriction. + var clientProcesses: Int32 { + get {return _storage._clientProcesses} + set {_uniqueStorage()._clientProcesses = newValue} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +struct Grpc_Testing_ClientStatus { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var stats: Grpc_Testing_ClientStats { + get {return _stats ?? Grpc_Testing_ClientStats()} + set {_stats = newValue} + } + /// Returns true if `stats` has been explicitly set. + var hasStats: Bool {return self._stats != nil} + /// Clears the value of `stats`. Subsequent reads from it will return its default value. + mutating func clearStats() {self._stats = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _stats: Grpc_Testing_ClientStats? = nil +} + +/// Request current stats +struct Grpc_Testing_Mark { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// if true, the stats will be reset after taking their snapshot. + var reset: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_ClientArgs { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var argtype: Grpc_Testing_ClientArgs.OneOf_Argtype? = nil + + var setup: Grpc_Testing_ClientConfig { + get { + if case .setup(let v)? = argtype {return v} + return Grpc_Testing_ClientConfig() + } + set {argtype = .setup(newValue)} + } + + var mark: Grpc_Testing_Mark { + get { + if case .mark(let v)? = argtype {return v} + return Grpc_Testing_Mark() + } + set {argtype = .mark(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Argtype: Equatable { + case setup(Grpc_Testing_ClientConfig) + case mark(Grpc_Testing_Mark) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Testing_ClientArgs.OneOf_Argtype, rhs: Grpc_Testing_ClientArgs.OneOf_Argtype) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.setup, .setup): return { + guard case .setup(let l) = lhs, case .setup(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mark, .mark): return { + guard case .mark(let l) = lhs, case .mark(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +struct Grpc_Testing_ServerConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var serverType: Grpc_Testing_ServerType = .syncServer + + var securityParams: Grpc_Testing_SecurityParams { + get {return _securityParams ?? Grpc_Testing_SecurityParams()} + set {_securityParams = newValue} + } + /// Returns true if `securityParams` has been explicitly set. + var hasSecurityParams: Bool {return self._securityParams != nil} + /// Clears the value of `securityParams`. Subsequent reads from it will return its default value. + mutating func clearSecurityParams() {self._securityParams = nil} + + /// Port on which to listen. Zero means pick unused port. + var port: Int32 = 0 + + /// Only for async server. Number of threads used to serve the requests. + var asyncServerThreads: Int32 = 0 + + /// Specify the number of cores to limit server to, if desired + var coreLimit: Int32 = 0 + + /// payload config, used in generic server. + /// Note this must NOT be used in proto (non-generic) servers. For proto servers, + /// 'response sizes' must be configured from the 'response_size' field of the + /// 'SimpleRequest' objects in RPC requests. + var payloadConfig: Grpc_Testing_PayloadConfig { + get {return _payloadConfig ?? Grpc_Testing_PayloadConfig()} + set {_payloadConfig = newValue} + } + /// Returns true if `payloadConfig` has been explicitly set. + var hasPayloadConfig: Bool {return self._payloadConfig != nil} + /// Clears the value of `payloadConfig`. Subsequent reads from it will return its default value. + mutating func clearPayloadConfig() {self._payloadConfig = nil} + + /// Specify the cores we should run the server on, if desired + var coreList: [Int32] = [] + + /// If we use an OTHER_SERVER client_type, this string gives more detail + var otherServerApi: String = String() + + /// Number of threads that share each completion queue + var threadsPerCq: Int32 = 0 + + /// Buffer pool size (no buffer pool specified if unset) + var resourceQuotaSize: Int32 = 0 + + var channelArgs: [Grpc_Testing_ChannelArg] = [] + + /// Number of server processes. 0 indicates no restriction. + var serverProcesses: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _securityParams: Grpc_Testing_SecurityParams? = nil + fileprivate var _payloadConfig: Grpc_Testing_PayloadConfig? = nil +} + +struct Grpc_Testing_ServerArgs { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var argtype: Grpc_Testing_ServerArgs.OneOf_Argtype? = nil + + var setup: Grpc_Testing_ServerConfig { + get { + if case .setup(let v)? = argtype {return v} + return Grpc_Testing_ServerConfig() + } + set {argtype = .setup(newValue)} + } + + var mark: Grpc_Testing_Mark { + get { + if case .mark(let v)? = argtype {return v} + return Grpc_Testing_Mark() + } + set {argtype = .mark(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Argtype: Equatable { + case setup(Grpc_Testing_ServerConfig) + case mark(Grpc_Testing_Mark) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Testing_ServerArgs.OneOf_Argtype, rhs: Grpc_Testing_ServerArgs.OneOf_Argtype) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.setup, .setup): return { + guard case .setup(let l) = lhs, case .setup(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.mark, .mark): return { + guard case .mark(let l) = lhs, case .mark(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +struct Grpc_Testing_ServerStatus { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var stats: Grpc_Testing_ServerStats { + get {return _stats ?? Grpc_Testing_ServerStats()} + set {_stats = newValue} + } + /// Returns true if `stats` has been explicitly set. + var hasStats: Bool {return self._stats != nil} + /// Clears the value of `stats`. Subsequent reads from it will return its default value. + mutating func clearStats() {self._stats = nil} + + /// the port bound by the server + var port: Int32 = 0 + + /// Number of cores available to the server + var cores: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _stats: Grpc_Testing_ServerStats? = nil +} + +struct Grpc_Testing_CoreRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_CoreResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Number of cores available on the server + var cores: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_Void { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A single performance scenario: input to qps_json_driver +struct Grpc_Testing_Scenario { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Human readable name for this scenario + var name: String { + get {return _storage._name} + set {_uniqueStorage()._name = newValue} + } + + /// Client configuration + var clientConfig: Grpc_Testing_ClientConfig { + get {return _storage._clientConfig ?? Grpc_Testing_ClientConfig()} + set {_uniqueStorage()._clientConfig = newValue} + } + /// Returns true if `clientConfig` has been explicitly set. + var hasClientConfig: Bool {return _storage._clientConfig != nil} + /// Clears the value of `clientConfig`. Subsequent reads from it will return its default value. + mutating func clearClientConfig() {_uniqueStorage()._clientConfig = nil} + + /// Number of clients to start for the test + var numClients: Int32 { + get {return _storage._numClients} + set {_uniqueStorage()._numClients = newValue} + } + + /// Server configuration + var serverConfig: Grpc_Testing_ServerConfig { + get {return _storage._serverConfig ?? Grpc_Testing_ServerConfig()} + set {_uniqueStorage()._serverConfig = newValue} + } + /// Returns true if `serverConfig` has been explicitly set. + var hasServerConfig: Bool {return _storage._serverConfig != nil} + /// Clears the value of `serverConfig`. Subsequent reads from it will return its default value. + mutating func clearServerConfig() {_uniqueStorage()._serverConfig = nil} + + /// Number of servers to start for the test + var numServers: Int32 { + get {return _storage._numServers} + set {_uniqueStorage()._numServers = newValue} + } + + /// Warmup period, in seconds + var warmupSeconds: Int32 { + get {return _storage._warmupSeconds} + set {_uniqueStorage()._warmupSeconds = newValue} + } + + /// Benchmark time, in seconds + var benchmarkSeconds: Int32 { + get {return _storage._benchmarkSeconds} + set {_uniqueStorage()._benchmarkSeconds = newValue} + } + + /// Number of workers to spawn locally (usually zero) + var spawnLocalWorkerCount: Int32 { + get {return _storage._spawnLocalWorkerCount} + set {_uniqueStorage()._spawnLocalWorkerCount = newValue} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +/// A set of scenarios to be run with qps_json_driver +struct Grpc_Testing_Scenarios { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var scenarios: [Grpc_Testing_Scenario] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Basic summary that can be computed from ClientStats and ServerStats +/// once the scenario has finished. +struct Grpc_Testing_ScenarioResultSummary { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Total number of operations per second over all clients. What is counted as 1 'operation' depends on the benchmark scenarios: + /// For unary benchmarks, an operation is processing of a single unary RPC. + /// For streaming benchmarks, an operation is processing of a single ping pong of request and response. + var qps: Double { + get {return _storage._qps} + set {_uniqueStorage()._qps = newValue} + } + + /// QPS per server core. + var qpsPerServerCore: Double { + get {return _storage._qpsPerServerCore} + set {_uniqueStorage()._qpsPerServerCore = newValue} + } + + /// The total server cpu load based on system time across all server processes, expressed as percentage of a single cpu core. + /// For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server + /// processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. + /// Same explanation for the total client cpu load below. + var serverSystemTime: Double { + get {return _storage._serverSystemTime} + set {_uniqueStorage()._serverSystemTime = newValue} + } + + /// The total server cpu load based on user time across all server processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) + var serverUserTime: Double { + get {return _storage._serverUserTime} + set {_uniqueStorage()._serverUserTime = newValue} + } + + /// The total client cpu load based on system time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) + var clientSystemTime: Double { + get {return _storage._clientSystemTime} + set {_uniqueStorage()._clientSystemTime = newValue} + } + + /// The total client cpu load based on user time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) + var clientUserTime: Double { + get {return _storage._clientUserTime} + set {_uniqueStorage()._clientUserTime = newValue} + } + + /// X% latency percentiles (in nanoseconds) + var latency50: Double { + get {return _storage._latency50} + set {_uniqueStorage()._latency50 = newValue} + } + + var latency90: Double { + get {return _storage._latency90} + set {_uniqueStorage()._latency90 = newValue} + } + + var latency95: Double { + get {return _storage._latency95} + set {_uniqueStorage()._latency95 = newValue} + } + + var latency99: Double { + get {return _storage._latency99} + set {_uniqueStorage()._latency99 = newValue} + } + + var latency999: Double { + get {return _storage._latency999} + set {_uniqueStorage()._latency999 = newValue} + } + + /// server cpu usage percentage + var serverCpuUsage: Double { + get {return _storage._serverCpuUsage} + set {_uniqueStorage()._serverCpuUsage = newValue} + } + + /// Number of requests that succeeded/failed + var successfulRequestsPerSecond: Double { + get {return _storage._successfulRequestsPerSecond} + set {_uniqueStorage()._successfulRequestsPerSecond = newValue} + } + + var failedRequestsPerSecond: Double { + get {return _storage._failedRequestsPerSecond} + set {_uniqueStorage()._failedRequestsPerSecond = newValue} + } + + /// Number of polls called inside completion queue per request + var clientPollsPerRequest: Double { + get {return _storage._clientPollsPerRequest} + set {_uniqueStorage()._clientPollsPerRequest = newValue} + } + + var serverPollsPerRequest: Double { + get {return _storage._serverPollsPerRequest} + set {_uniqueStorage()._serverPollsPerRequest = newValue} + } + + /// Queries per CPU-sec over all servers or clients + var serverQueriesPerCpuSec: Double { + get {return _storage._serverQueriesPerCpuSec} + set {_uniqueStorage()._serverQueriesPerCpuSec = newValue} + } + + var clientQueriesPerCpuSec: Double { + get {return _storage._clientQueriesPerCpuSec} + set {_uniqueStorage()._clientQueriesPerCpuSec = newValue} + } + + /// Start and end time for the test scenario + var startTime: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _storage._startTime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_uniqueStorage()._startTime = newValue} + } + /// Returns true if `startTime` has been explicitly set. + var hasStartTime: Bool {return _storage._startTime != nil} + /// Clears the value of `startTime`. Subsequent reads from it will return its default value. + mutating func clearStartTime() {_uniqueStorage()._startTime = nil} + + var endTime: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _storage._endTime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_uniqueStorage()._endTime = newValue} + } + /// Returns true if `endTime` has been explicitly set. + var hasEndTime: Bool {return _storage._endTime != nil} + /// Clears the value of `endTime`. Subsequent reads from it will return its default value. + mutating func clearEndTime() {_uniqueStorage()._endTime = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +/// Results of a single benchmark scenario. +struct Grpc_Testing_ScenarioResult { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Inputs used to run the scenario. + var scenario: Grpc_Testing_Scenario { + get {return _scenario ?? Grpc_Testing_Scenario()} + set {_scenario = newValue} + } + /// Returns true if `scenario` has been explicitly set. + var hasScenario: Bool {return self._scenario != nil} + /// Clears the value of `scenario`. Subsequent reads from it will return its default value. + mutating func clearScenario() {self._scenario = nil} + + /// Histograms from all clients merged into one histogram. + var latencies: Grpc_Testing_HistogramData { + get {return _latencies ?? Grpc_Testing_HistogramData()} + set {_latencies = newValue} + } + /// Returns true if `latencies` has been explicitly set. + var hasLatencies: Bool {return self._latencies != nil} + /// Clears the value of `latencies`. Subsequent reads from it will return its default value. + mutating func clearLatencies() {self._latencies = nil} + + /// Client stats for each client + var clientStats: [Grpc_Testing_ClientStats] = [] + + /// Server stats for each server + var serverStats: [Grpc_Testing_ServerStats] = [] + + /// Number of cores available to each server + var serverCores: [Int32] = [] + + /// An after-the-fact computed summary + var summary: Grpc_Testing_ScenarioResultSummary { + get {return _summary ?? Grpc_Testing_ScenarioResultSummary()} + set {_summary = newValue} + } + /// Returns true if `summary` has been explicitly set. + var hasSummary: Bool {return self._summary != nil} + /// Clears the value of `summary`. Subsequent reads from it will return its default value. + mutating func clearSummary() {self._summary = nil} + + /// Information on success or failure of each worker + var clientSuccess: [Bool] = [] + + var serverSuccess: [Bool] = [] + + /// Number of failed requests (one row per status code seen) + var requestResults: [Grpc_Testing_RequestResultCount] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _scenario: Grpc_Testing_Scenario? = nil + fileprivate var _latencies: Grpc_Testing_HistogramData? = nil + fileprivate var _summary: Grpc_Testing_ScenarioResultSummary? = nil +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Testing_ClientType: @unchecked Sendable {} +extension Grpc_Testing_ServerType: @unchecked Sendable {} +extension Grpc_Testing_RpcType: @unchecked Sendable {} +extension Grpc_Testing_PoissonParams: @unchecked Sendable {} +extension Grpc_Testing_ClosedLoopParams: @unchecked Sendable {} +extension Grpc_Testing_LoadParams: @unchecked Sendable {} +extension Grpc_Testing_LoadParams.OneOf_Load: @unchecked Sendable {} +extension Grpc_Testing_SecurityParams: @unchecked Sendable {} +extension Grpc_Testing_ChannelArg: @unchecked Sendable {} +extension Grpc_Testing_ChannelArg.OneOf_Value: @unchecked Sendable {} +extension Grpc_Testing_ClientConfig: @unchecked Sendable {} +extension Grpc_Testing_ClientStatus: @unchecked Sendable {} +extension Grpc_Testing_Mark: @unchecked Sendable {} +extension Grpc_Testing_ClientArgs: @unchecked Sendable {} +extension Grpc_Testing_ClientArgs.OneOf_Argtype: @unchecked Sendable {} +extension Grpc_Testing_ServerConfig: @unchecked Sendable {} +extension Grpc_Testing_ServerArgs: @unchecked Sendable {} +extension Grpc_Testing_ServerArgs.OneOf_Argtype: @unchecked Sendable {} +extension Grpc_Testing_ServerStatus: @unchecked Sendable {} +extension Grpc_Testing_CoreRequest: @unchecked Sendable {} +extension Grpc_Testing_CoreResponse: @unchecked Sendable {} +extension Grpc_Testing_Void: @unchecked Sendable {} +extension Grpc_Testing_Scenario: @unchecked Sendable {} +extension Grpc_Testing_Scenarios: @unchecked Sendable {} +extension Grpc_Testing_ScenarioResultSummary: @unchecked Sendable {} +extension Grpc_Testing_ScenarioResult: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.testing" + +extension Grpc_Testing_ClientType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "SYNC_CLIENT"), + 1: .same(proto: "ASYNC_CLIENT"), + 2: .same(proto: "OTHER_CLIENT"), + 3: .same(proto: "CALLBACK_CLIENT"), + ] +} + +extension Grpc_Testing_ServerType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "SYNC_SERVER"), + 1: .same(proto: "ASYNC_SERVER"), + 2: .same(proto: "ASYNC_GENERIC_SERVER"), + 3: .same(proto: "OTHER_SERVER"), + 4: .same(proto: "CALLBACK_SERVER"), + ] +} + +extension Grpc_Testing_RpcType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNARY"), + 1: .same(proto: "STREAMING"), + 2: .same(proto: "STREAMING_FROM_CLIENT"), + 3: .same(proto: "STREAMING_FROM_SERVER"), + 4: .same(proto: "STREAMING_BOTH_WAYS"), + ] +} + +extension Grpc_Testing_PoissonParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PoissonParams" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "offered_load"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularDoubleField(value: &self.offeredLoad) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.offeredLoad != 0 { + try visitor.visitSingularDoubleField(value: self.offeredLoad, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_PoissonParams, rhs: Grpc_Testing_PoissonParams) -> Bool { + if lhs.offeredLoad != rhs.offeredLoad {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClosedLoopParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClosedLoopParams" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClosedLoopParams, rhs: Grpc_Testing_ClosedLoopParams) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LoadParams" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "closed_loop"), + 2: .same(proto: "poisson"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Grpc_Testing_ClosedLoopParams? + var hadOneofValue = false + if let current = self.load { + hadOneofValue = true + if case .closedLoop(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.load = .closedLoop(v) + } + }() + case 2: try { + var v: Grpc_Testing_PoissonParams? + var hadOneofValue = false + if let current = self.load { + hadOneofValue = true + if case .poisson(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.load = .poisson(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.load { + case .closedLoop?: try { + guard case .closedLoop(let v)? = self.load else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .poisson?: try { + guard case .poisson(let v)? = self.load else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadParams, rhs: Grpc_Testing_LoadParams) -> Bool { + if lhs.load != rhs.load {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_SecurityParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SecurityParams" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "use_test_ca"), + 2: .standard(proto: "server_host_override"), + 3: .standard(proto: "cred_type"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.useTestCa) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.serverHostOverride) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.credType) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.useTestCa != false { + try visitor.visitSingularBoolField(value: self.useTestCa, fieldNumber: 1) + } + if !self.serverHostOverride.isEmpty { + try visitor.visitSingularStringField(value: self.serverHostOverride, fieldNumber: 2) + } + if !self.credType.isEmpty { + try visitor.visitSingularStringField(value: self.credType, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_SecurityParams, rhs: Grpc_Testing_SecurityParams) -> Bool { + if lhs.useTestCa != rhs.useTestCa {return false} + if lhs.serverHostOverride != rhs.serverHostOverride {return false} + if lhs.credType != rhs.credType {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ChannelArg: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ChannelArg" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 2: .standard(proto: "str_value"), + 3: .standard(proto: "int_value"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + case 2: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.value != nil {try decoder.handleConflictingOneOf()} + self.value = .strValue(v) + } + }() + case 3: try { + var v: Int32? + try decoder.decodeSingularInt32Field(value: &v) + if let v = v { + if self.value != nil {try decoder.handleConflictingOneOf()} + self.value = .intValue(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + switch self.value { + case .strValue?: try { + guard case .strValue(let v)? = self.value else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 2) + }() + case .intValue?: try { + guard case .intValue(let v)? = self.value else { preconditionFailure() } + try visitor.visitSingularInt32Field(value: v, fieldNumber: 3) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ChannelArg, rhs: Grpc_Testing_ChannelArg) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClientConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClientConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "server_targets"), + 2: .standard(proto: "client_type"), + 3: .standard(proto: "security_params"), + 4: .standard(proto: "outstanding_rpcs_per_channel"), + 5: .standard(proto: "client_channels"), + 7: .standard(proto: "async_client_threads"), + 8: .standard(proto: "rpc_type"), + 10: .standard(proto: "load_params"), + 11: .standard(proto: "payload_config"), + 12: .standard(proto: "histogram_params"), + 13: .standard(proto: "core_list"), + 14: .standard(proto: "core_limit"), + 15: .standard(proto: "other_client_api"), + 16: .standard(proto: "channel_args"), + 17: .standard(proto: "threads_per_cq"), + 18: .standard(proto: "messages_per_stream"), + 19: .standard(proto: "use_coalesce_api"), + 20: .standard(proto: "median_latency_collection_interval_millis"), + 21: .standard(proto: "client_processes"), + ] + + fileprivate class _StorageClass { + var _serverTargets: [String] = [] + var _clientType: Grpc_Testing_ClientType = .syncClient + var _securityParams: Grpc_Testing_SecurityParams? = nil + var _outstandingRpcsPerChannel: Int32 = 0 + var _clientChannels: Int32 = 0 + var _asyncClientThreads: Int32 = 0 + var _rpcType: Grpc_Testing_RpcType = .unary + var _loadParams: Grpc_Testing_LoadParams? = nil + var _payloadConfig: Grpc_Testing_PayloadConfig? = nil + var _histogramParams: Grpc_Testing_HistogramParams? = nil + var _coreList: [Int32] = [] + var _coreLimit: Int32 = 0 + var _otherClientApi: String = String() + var _channelArgs: [Grpc_Testing_ChannelArg] = [] + var _threadsPerCq: Int32 = 0 + var _messagesPerStream: Int32 = 0 + var _useCoalesceApi: Bool = false + var _medianLatencyCollectionIntervalMillis: Int32 = 0 + var _clientProcesses: Int32 = 0 + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _serverTargets = source._serverTargets + _clientType = source._clientType + _securityParams = source._securityParams + _outstandingRpcsPerChannel = source._outstandingRpcsPerChannel + _clientChannels = source._clientChannels + _asyncClientThreads = source._asyncClientThreads + _rpcType = source._rpcType + _loadParams = source._loadParams + _payloadConfig = source._payloadConfig + _histogramParams = source._histogramParams + _coreList = source._coreList + _coreLimit = source._coreLimit + _otherClientApi = source._otherClientApi + _channelArgs = source._channelArgs + _threadsPerCq = source._threadsPerCq + _messagesPerStream = source._messagesPerStream + _useCoalesceApi = source._useCoalesceApi + _medianLatencyCollectionIntervalMillis = source._medianLatencyCollectionIntervalMillis + _clientProcesses = source._clientProcesses + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedStringField(value: &_storage._serverTargets) }() + case 2: try { try decoder.decodeSingularEnumField(value: &_storage._clientType) }() + case 3: try { try decoder.decodeSingularMessageField(value: &_storage._securityParams) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &_storage._outstandingRpcsPerChannel) }() + case 5: try { try decoder.decodeSingularInt32Field(value: &_storage._clientChannels) }() + case 7: try { try decoder.decodeSingularInt32Field(value: &_storage._asyncClientThreads) }() + case 8: try { try decoder.decodeSingularEnumField(value: &_storage._rpcType) }() + case 10: try { try decoder.decodeSingularMessageField(value: &_storage._loadParams) }() + case 11: try { try decoder.decodeSingularMessageField(value: &_storage._payloadConfig) }() + case 12: try { try decoder.decodeSingularMessageField(value: &_storage._histogramParams) }() + case 13: try { try decoder.decodeRepeatedInt32Field(value: &_storage._coreList) }() + case 14: try { try decoder.decodeSingularInt32Field(value: &_storage._coreLimit) }() + case 15: try { try decoder.decodeSingularStringField(value: &_storage._otherClientApi) }() + case 16: try { try decoder.decodeRepeatedMessageField(value: &_storage._channelArgs) }() + case 17: try { try decoder.decodeSingularInt32Field(value: &_storage._threadsPerCq) }() + case 18: try { try decoder.decodeSingularInt32Field(value: &_storage._messagesPerStream) }() + case 19: try { try decoder.decodeSingularBoolField(value: &_storage._useCoalesceApi) }() + case 20: try { try decoder.decodeSingularInt32Field(value: &_storage._medianLatencyCollectionIntervalMillis) }() + case 21: try { try decoder.decodeSingularInt32Field(value: &_storage._clientProcesses) }() + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !_storage._serverTargets.isEmpty { + try visitor.visitRepeatedStringField(value: _storage._serverTargets, fieldNumber: 1) + } + if _storage._clientType != .syncClient { + try visitor.visitSingularEnumField(value: _storage._clientType, fieldNumber: 2) + } + try { if let v = _storage._securityParams { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if _storage._outstandingRpcsPerChannel != 0 { + try visitor.visitSingularInt32Field(value: _storage._outstandingRpcsPerChannel, fieldNumber: 4) + } + if _storage._clientChannels != 0 { + try visitor.visitSingularInt32Field(value: _storage._clientChannels, fieldNumber: 5) + } + if _storage._asyncClientThreads != 0 { + try visitor.visitSingularInt32Field(value: _storage._asyncClientThreads, fieldNumber: 7) + } + if _storage._rpcType != .unary { + try visitor.visitSingularEnumField(value: _storage._rpcType, fieldNumber: 8) + } + try { if let v = _storage._loadParams { + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + } }() + try { if let v = _storage._payloadConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } }() + try { if let v = _storage._histogramParams { + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + } }() + if !_storage._coreList.isEmpty { + try visitor.visitPackedInt32Field(value: _storage._coreList, fieldNumber: 13) + } + if _storage._coreLimit != 0 { + try visitor.visitSingularInt32Field(value: _storage._coreLimit, fieldNumber: 14) + } + if !_storage._otherClientApi.isEmpty { + try visitor.visitSingularStringField(value: _storage._otherClientApi, fieldNumber: 15) + } + if !_storage._channelArgs.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._channelArgs, fieldNumber: 16) + } + if _storage._threadsPerCq != 0 { + try visitor.visitSingularInt32Field(value: _storage._threadsPerCq, fieldNumber: 17) + } + if _storage._messagesPerStream != 0 { + try visitor.visitSingularInt32Field(value: _storage._messagesPerStream, fieldNumber: 18) + } + if _storage._useCoalesceApi != false { + try visitor.visitSingularBoolField(value: _storage._useCoalesceApi, fieldNumber: 19) + } + if _storage._medianLatencyCollectionIntervalMillis != 0 { + try visitor.visitSingularInt32Field(value: _storage._medianLatencyCollectionIntervalMillis, fieldNumber: 20) + } + if _storage._clientProcesses != 0 { + try visitor.visitSingularInt32Field(value: _storage._clientProcesses, fieldNumber: 21) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClientConfig, rhs: Grpc_Testing_ClientConfig) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._serverTargets != rhs_storage._serverTargets {return false} + if _storage._clientType != rhs_storage._clientType {return false} + if _storage._securityParams != rhs_storage._securityParams {return false} + if _storage._outstandingRpcsPerChannel != rhs_storage._outstandingRpcsPerChannel {return false} + if _storage._clientChannels != rhs_storage._clientChannels {return false} + if _storage._asyncClientThreads != rhs_storage._asyncClientThreads {return false} + if _storage._rpcType != rhs_storage._rpcType {return false} + if _storage._loadParams != rhs_storage._loadParams {return false} + if _storage._payloadConfig != rhs_storage._payloadConfig {return false} + if _storage._histogramParams != rhs_storage._histogramParams {return false} + if _storage._coreList != rhs_storage._coreList {return false} + if _storage._coreLimit != rhs_storage._coreLimit {return false} + if _storage._otherClientApi != rhs_storage._otherClientApi {return false} + if _storage._channelArgs != rhs_storage._channelArgs {return false} + if _storage._threadsPerCq != rhs_storage._threadsPerCq {return false} + if _storage._messagesPerStream != rhs_storage._messagesPerStream {return false} + if _storage._useCoalesceApi != rhs_storage._useCoalesceApi {return false} + if _storage._medianLatencyCollectionIntervalMillis != rhs_storage._medianLatencyCollectionIntervalMillis {return false} + if _storage._clientProcesses != rhs_storage._clientProcesses {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClientStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClientStatus" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "stats"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._stats) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._stats { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClientStatus, rhs: Grpc_Testing_ClientStatus) -> Bool { + if lhs._stats != rhs._stats {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_Mark: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Mark" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "reset"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.reset) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.reset != false { + try visitor.visitSingularBoolField(value: self.reset, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_Mark, rhs: Grpc_Testing_Mark) -> Bool { + if lhs.reset != rhs.reset {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClientArgs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClientArgs" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "setup"), + 2: .same(proto: "mark"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Grpc_Testing_ClientConfig? + var hadOneofValue = false + if let current = self.argtype { + hadOneofValue = true + if case .setup(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.argtype = .setup(v) + } + }() + case 2: try { + var v: Grpc_Testing_Mark? + var hadOneofValue = false + if let current = self.argtype { + hadOneofValue = true + if case .mark(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.argtype = .mark(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.argtype { + case .setup?: try { + guard case .setup(let v)? = self.argtype else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .mark?: try { + guard case .mark(let v)? = self.argtype else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClientArgs, rhs: Grpc_Testing_ClientArgs) -> Bool { + if lhs.argtype != rhs.argtype {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ServerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "server_type"), + 2: .standard(proto: "security_params"), + 4: .same(proto: "port"), + 7: .standard(proto: "async_server_threads"), + 8: .standard(proto: "core_limit"), + 9: .standard(proto: "payload_config"), + 10: .standard(proto: "core_list"), + 11: .standard(proto: "other_server_api"), + 12: .standard(proto: "threads_per_cq"), + 1001: .standard(proto: "resource_quota_size"), + 1002: .standard(proto: "channel_args"), + 21: .standard(proto: "server_processes"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.serverType) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._securityParams) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.port) }() + case 7: try { try decoder.decodeSingularInt32Field(value: &self.asyncServerThreads) }() + case 8: try { try decoder.decodeSingularInt32Field(value: &self.coreLimit) }() + case 9: try { try decoder.decodeSingularMessageField(value: &self._payloadConfig) }() + case 10: try { try decoder.decodeRepeatedInt32Field(value: &self.coreList) }() + case 11: try { try decoder.decodeSingularStringField(value: &self.otherServerApi) }() + case 12: try { try decoder.decodeSingularInt32Field(value: &self.threadsPerCq) }() + case 21: try { try decoder.decodeSingularInt32Field(value: &self.serverProcesses) }() + case 1001: try { try decoder.decodeSingularInt32Field(value: &self.resourceQuotaSize) }() + case 1002: try { try decoder.decodeRepeatedMessageField(value: &self.channelArgs) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.serverType != .syncServer { + try visitor.visitSingularEnumField(value: self.serverType, fieldNumber: 1) + } + try { if let v = self._securityParams { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if self.port != 0 { + try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 4) + } + if self.asyncServerThreads != 0 { + try visitor.visitSingularInt32Field(value: self.asyncServerThreads, fieldNumber: 7) + } + if self.coreLimit != 0 { + try visitor.visitSingularInt32Field(value: self.coreLimit, fieldNumber: 8) + } + try { if let v = self._payloadConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + } }() + if !self.coreList.isEmpty { + try visitor.visitPackedInt32Field(value: self.coreList, fieldNumber: 10) + } + if !self.otherServerApi.isEmpty { + try visitor.visitSingularStringField(value: self.otherServerApi, fieldNumber: 11) + } + if self.threadsPerCq != 0 { + try visitor.visitSingularInt32Field(value: self.threadsPerCq, fieldNumber: 12) + } + if self.serverProcesses != 0 { + try visitor.visitSingularInt32Field(value: self.serverProcesses, fieldNumber: 21) + } + if self.resourceQuotaSize != 0 { + try visitor.visitSingularInt32Field(value: self.resourceQuotaSize, fieldNumber: 1001) + } + if !self.channelArgs.isEmpty { + try visitor.visitRepeatedMessageField(value: self.channelArgs, fieldNumber: 1002) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ServerConfig, rhs: Grpc_Testing_ServerConfig) -> Bool { + if lhs.serverType != rhs.serverType {return false} + if lhs._securityParams != rhs._securityParams {return false} + if lhs.port != rhs.port {return false} + if lhs.asyncServerThreads != rhs.asyncServerThreads {return false} + if lhs.coreLimit != rhs.coreLimit {return false} + if lhs._payloadConfig != rhs._payloadConfig {return false} + if lhs.coreList != rhs.coreList {return false} + if lhs.otherServerApi != rhs.otherServerApi {return false} + if lhs.threadsPerCq != rhs.threadsPerCq {return false} + if lhs.resourceQuotaSize != rhs.resourceQuotaSize {return false} + if lhs.channelArgs != rhs.channelArgs {return false} + if lhs.serverProcesses != rhs.serverProcesses {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ServerArgs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerArgs" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "setup"), + 2: .same(proto: "mark"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Grpc_Testing_ServerConfig? + var hadOneofValue = false + if let current = self.argtype { + hadOneofValue = true + if case .setup(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.argtype = .setup(v) + } + }() + case 2: try { + var v: Grpc_Testing_Mark? + var hadOneofValue = false + if let current = self.argtype { + hadOneofValue = true + if case .mark(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.argtype = .mark(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.argtype { + case .setup?: try { + guard case .setup(let v)? = self.argtype else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .mark?: try { + guard case .mark(let v)? = self.argtype else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ServerArgs, rhs: Grpc_Testing_ServerArgs) -> Bool { + if lhs.argtype != rhs.argtype {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ServerStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerStatus" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "stats"), + 2: .same(proto: "port"), + 3: .same(proto: "cores"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._stats) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.port) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.cores) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._stats { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.port != 0 { + try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 2) + } + if self.cores != 0 { + try visitor.visitSingularInt32Field(value: self.cores, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ServerStatus, rhs: Grpc_Testing_ServerStatus) -> Bool { + if lhs._stats != rhs._stats {return false} + if lhs.port != rhs.port {return false} + if lhs.cores != rhs.cores {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_CoreRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".CoreRequest" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_CoreRequest, rhs: Grpc_Testing_CoreRequest) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_CoreResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".CoreResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "cores"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.cores) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.cores != 0 { + try visitor.visitSingularInt32Field(value: self.cores, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_CoreResponse, rhs: Grpc_Testing_CoreResponse) -> Bool { + if lhs.cores != rhs.cores {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_Void: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Void" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_Void, rhs: Grpc_Testing_Void) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_Scenario: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Scenario" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 2: .standard(proto: "client_config"), + 3: .standard(proto: "num_clients"), + 4: .standard(proto: "server_config"), + 5: .standard(proto: "num_servers"), + 6: .standard(proto: "warmup_seconds"), + 7: .standard(proto: "benchmark_seconds"), + 8: .standard(proto: "spawn_local_worker_count"), + ] + + fileprivate class _StorageClass { + var _name: String = String() + var _clientConfig: Grpc_Testing_ClientConfig? = nil + var _numClients: Int32 = 0 + var _serverConfig: Grpc_Testing_ServerConfig? = nil + var _numServers: Int32 = 0 + var _warmupSeconds: Int32 = 0 + var _benchmarkSeconds: Int32 = 0 + var _spawnLocalWorkerCount: Int32 = 0 + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _name = source._name + _clientConfig = source._clientConfig + _numClients = source._numClients + _serverConfig = source._serverConfig + _numServers = source._numServers + _warmupSeconds = source._warmupSeconds + _benchmarkSeconds = source._benchmarkSeconds + _spawnLocalWorkerCount = source._spawnLocalWorkerCount + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &_storage._name) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._clientConfig) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &_storage._numClients) }() + case 4: try { try decoder.decodeSingularMessageField(value: &_storage._serverConfig) }() + case 5: try { try decoder.decodeSingularInt32Field(value: &_storage._numServers) }() + case 6: try { try decoder.decodeSingularInt32Field(value: &_storage._warmupSeconds) }() + case 7: try { try decoder.decodeSingularInt32Field(value: &_storage._benchmarkSeconds) }() + case 8: try { try decoder.decodeSingularInt32Field(value: &_storage._spawnLocalWorkerCount) }() + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !_storage._name.isEmpty { + try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 1) + } + try { if let v = _storage._clientConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if _storage._numClients != 0 { + try visitor.visitSingularInt32Field(value: _storage._numClients, fieldNumber: 3) + } + try { if let v = _storage._serverConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + if _storage._numServers != 0 { + try visitor.visitSingularInt32Field(value: _storage._numServers, fieldNumber: 5) + } + if _storage._warmupSeconds != 0 { + try visitor.visitSingularInt32Field(value: _storage._warmupSeconds, fieldNumber: 6) + } + if _storage._benchmarkSeconds != 0 { + try visitor.visitSingularInt32Field(value: _storage._benchmarkSeconds, fieldNumber: 7) + } + if _storage._spawnLocalWorkerCount != 0 { + try visitor.visitSingularInt32Field(value: _storage._spawnLocalWorkerCount, fieldNumber: 8) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_Scenario, rhs: Grpc_Testing_Scenario) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._name != rhs_storage._name {return false} + if _storage._clientConfig != rhs_storage._clientConfig {return false} + if _storage._numClients != rhs_storage._numClients {return false} + if _storage._serverConfig != rhs_storage._serverConfig {return false} + if _storage._numServers != rhs_storage._numServers {return false} + if _storage._warmupSeconds != rhs_storage._warmupSeconds {return false} + if _storage._benchmarkSeconds != rhs_storage._benchmarkSeconds {return false} + if _storage._spawnLocalWorkerCount != rhs_storage._spawnLocalWorkerCount {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_Scenarios: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Scenarios" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "scenarios"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.scenarios) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.scenarios.isEmpty { + try visitor.visitRepeatedMessageField(value: self.scenarios, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_Scenarios, rhs: Grpc_Testing_Scenarios) -> Bool { + if lhs.scenarios != rhs.scenarios {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ScenarioResultSummary: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ScenarioResultSummary" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "qps"), + 2: .standard(proto: "qps_per_server_core"), + 3: .standard(proto: "server_system_time"), + 4: .standard(proto: "server_user_time"), + 5: .standard(proto: "client_system_time"), + 6: .standard(proto: "client_user_time"), + 7: .standard(proto: "latency_50"), + 8: .standard(proto: "latency_90"), + 9: .standard(proto: "latency_95"), + 10: .standard(proto: "latency_99"), + 11: .standard(proto: "latency_999"), + 12: .standard(proto: "server_cpu_usage"), + 13: .standard(proto: "successful_requests_per_second"), + 14: .standard(proto: "failed_requests_per_second"), + 15: .standard(proto: "client_polls_per_request"), + 16: .standard(proto: "server_polls_per_request"), + 17: .standard(proto: "server_queries_per_cpu_sec"), + 18: .standard(proto: "client_queries_per_cpu_sec"), + 19: .standard(proto: "start_time"), + 20: .standard(proto: "end_time"), + ] + + fileprivate class _StorageClass { + var _qps: Double = 0 + var _qpsPerServerCore: Double = 0 + var _serverSystemTime: Double = 0 + var _serverUserTime: Double = 0 + var _clientSystemTime: Double = 0 + var _clientUserTime: Double = 0 + var _latency50: Double = 0 + var _latency90: Double = 0 + var _latency95: Double = 0 + var _latency99: Double = 0 + var _latency999: Double = 0 + var _serverCpuUsage: Double = 0 + var _successfulRequestsPerSecond: Double = 0 + var _failedRequestsPerSecond: Double = 0 + var _clientPollsPerRequest: Double = 0 + var _serverPollsPerRequest: Double = 0 + var _serverQueriesPerCpuSec: Double = 0 + var _clientQueriesPerCpuSec: Double = 0 + var _startTime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + var _endTime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _qps = source._qps + _qpsPerServerCore = source._qpsPerServerCore + _serverSystemTime = source._serverSystemTime + _serverUserTime = source._serverUserTime + _clientSystemTime = source._clientSystemTime + _clientUserTime = source._clientUserTime + _latency50 = source._latency50 + _latency90 = source._latency90 + _latency95 = source._latency95 + _latency99 = source._latency99 + _latency999 = source._latency999 + _serverCpuUsage = source._serverCpuUsage + _successfulRequestsPerSecond = source._successfulRequestsPerSecond + _failedRequestsPerSecond = source._failedRequestsPerSecond + _clientPollsPerRequest = source._clientPollsPerRequest + _serverPollsPerRequest = source._serverPollsPerRequest + _serverQueriesPerCpuSec = source._serverQueriesPerCpuSec + _clientQueriesPerCpuSec = source._clientQueriesPerCpuSec + _startTime = source._startTime + _endTime = source._endTime + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularDoubleField(value: &_storage._qps) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &_storage._qpsPerServerCore) }() + case 3: try { try decoder.decodeSingularDoubleField(value: &_storage._serverSystemTime) }() + case 4: try { try decoder.decodeSingularDoubleField(value: &_storage._serverUserTime) }() + case 5: try { try decoder.decodeSingularDoubleField(value: &_storage._clientSystemTime) }() + case 6: try { try decoder.decodeSingularDoubleField(value: &_storage._clientUserTime) }() + case 7: try { try decoder.decodeSingularDoubleField(value: &_storage._latency50) }() + case 8: try { try decoder.decodeSingularDoubleField(value: &_storage._latency90) }() + case 9: try { try decoder.decodeSingularDoubleField(value: &_storage._latency95) }() + case 10: try { try decoder.decodeSingularDoubleField(value: &_storage._latency99) }() + case 11: try { try decoder.decodeSingularDoubleField(value: &_storage._latency999) }() + case 12: try { try decoder.decodeSingularDoubleField(value: &_storage._serverCpuUsage) }() + case 13: try { try decoder.decodeSingularDoubleField(value: &_storage._successfulRequestsPerSecond) }() + case 14: try { try decoder.decodeSingularDoubleField(value: &_storage._failedRequestsPerSecond) }() + case 15: try { try decoder.decodeSingularDoubleField(value: &_storage._clientPollsPerRequest) }() + case 16: try { try decoder.decodeSingularDoubleField(value: &_storage._serverPollsPerRequest) }() + case 17: try { try decoder.decodeSingularDoubleField(value: &_storage._serverQueriesPerCpuSec) }() + case 18: try { try decoder.decodeSingularDoubleField(value: &_storage._clientQueriesPerCpuSec) }() + case 19: try { try decoder.decodeSingularMessageField(value: &_storage._startTime) }() + case 20: try { try decoder.decodeSingularMessageField(value: &_storage._endTime) }() + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if _storage._qps != 0 { + try visitor.visitSingularDoubleField(value: _storage._qps, fieldNumber: 1) + } + if _storage._qpsPerServerCore != 0 { + try visitor.visitSingularDoubleField(value: _storage._qpsPerServerCore, fieldNumber: 2) + } + if _storage._serverSystemTime != 0 { + try visitor.visitSingularDoubleField(value: _storage._serverSystemTime, fieldNumber: 3) + } + if _storage._serverUserTime != 0 { + try visitor.visitSingularDoubleField(value: _storage._serverUserTime, fieldNumber: 4) + } + if _storage._clientSystemTime != 0 { + try visitor.visitSingularDoubleField(value: _storage._clientSystemTime, fieldNumber: 5) + } + if _storage._clientUserTime != 0 { + try visitor.visitSingularDoubleField(value: _storage._clientUserTime, fieldNumber: 6) + } + if _storage._latency50 != 0 { + try visitor.visitSingularDoubleField(value: _storage._latency50, fieldNumber: 7) + } + if _storage._latency90 != 0 { + try visitor.visitSingularDoubleField(value: _storage._latency90, fieldNumber: 8) + } + if _storage._latency95 != 0 { + try visitor.visitSingularDoubleField(value: _storage._latency95, fieldNumber: 9) + } + if _storage._latency99 != 0 { + try visitor.visitSingularDoubleField(value: _storage._latency99, fieldNumber: 10) + } + if _storage._latency999 != 0 { + try visitor.visitSingularDoubleField(value: _storage._latency999, fieldNumber: 11) + } + if _storage._serverCpuUsage != 0 { + try visitor.visitSingularDoubleField(value: _storage._serverCpuUsage, fieldNumber: 12) + } + if _storage._successfulRequestsPerSecond != 0 { + try visitor.visitSingularDoubleField(value: _storage._successfulRequestsPerSecond, fieldNumber: 13) + } + if _storage._failedRequestsPerSecond != 0 { + try visitor.visitSingularDoubleField(value: _storage._failedRequestsPerSecond, fieldNumber: 14) + } + if _storage._clientPollsPerRequest != 0 { + try visitor.visitSingularDoubleField(value: _storage._clientPollsPerRequest, fieldNumber: 15) + } + if _storage._serverPollsPerRequest != 0 { + try visitor.visitSingularDoubleField(value: _storage._serverPollsPerRequest, fieldNumber: 16) + } + if _storage._serverQueriesPerCpuSec != 0 { + try visitor.visitSingularDoubleField(value: _storage._serverQueriesPerCpuSec, fieldNumber: 17) + } + if _storage._clientQueriesPerCpuSec != 0 { + try visitor.visitSingularDoubleField(value: _storage._clientQueriesPerCpuSec, fieldNumber: 18) + } + try { if let v = _storage._startTime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 19) + } }() + try { if let v = _storage._endTime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 20) + } }() + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ScenarioResultSummary, rhs: Grpc_Testing_ScenarioResultSummary) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._qps != rhs_storage._qps {return false} + if _storage._qpsPerServerCore != rhs_storage._qpsPerServerCore {return false} + if _storage._serverSystemTime != rhs_storage._serverSystemTime {return false} + if _storage._serverUserTime != rhs_storage._serverUserTime {return false} + if _storage._clientSystemTime != rhs_storage._clientSystemTime {return false} + if _storage._clientUserTime != rhs_storage._clientUserTime {return false} + if _storage._latency50 != rhs_storage._latency50 {return false} + if _storage._latency90 != rhs_storage._latency90 {return false} + if _storage._latency95 != rhs_storage._latency95 {return false} + if _storage._latency99 != rhs_storage._latency99 {return false} + if _storage._latency999 != rhs_storage._latency999 {return false} + if _storage._serverCpuUsage != rhs_storage._serverCpuUsage {return false} + if _storage._successfulRequestsPerSecond != rhs_storage._successfulRequestsPerSecond {return false} + if _storage._failedRequestsPerSecond != rhs_storage._failedRequestsPerSecond {return false} + if _storage._clientPollsPerRequest != rhs_storage._clientPollsPerRequest {return false} + if _storage._serverPollsPerRequest != rhs_storage._serverPollsPerRequest {return false} + if _storage._serverQueriesPerCpuSec != rhs_storage._serverQueriesPerCpuSec {return false} + if _storage._clientQueriesPerCpuSec != rhs_storage._clientQueriesPerCpuSec {return false} + if _storage._startTime != rhs_storage._startTime {return false} + if _storage._endTime != rhs_storage._endTime {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ScenarioResult: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ScenarioResult" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "scenario"), + 2: .same(proto: "latencies"), + 3: .standard(proto: "client_stats"), + 4: .standard(proto: "server_stats"), + 5: .standard(proto: "server_cores"), + 6: .same(proto: "summary"), + 7: .standard(proto: "client_success"), + 8: .standard(proto: "server_success"), + 9: .standard(proto: "request_results"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._scenario) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._latencies) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.clientStats) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.serverStats) }() + case 5: try { try decoder.decodeRepeatedInt32Field(value: &self.serverCores) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._summary) }() + case 7: try { try decoder.decodeRepeatedBoolField(value: &self.clientSuccess) }() + case 8: try { try decoder.decodeRepeatedBoolField(value: &self.serverSuccess) }() + case 9: try { try decoder.decodeRepeatedMessageField(value: &self.requestResults) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._scenario { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._latencies { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if !self.clientStats.isEmpty { + try visitor.visitRepeatedMessageField(value: self.clientStats, fieldNumber: 3) + } + if !self.serverStats.isEmpty { + try visitor.visitRepeatedMessageField(value: self.serverStats, fieldNumber: 4) + } + if !self.serverCores.isEmpty { + try visitor.visitPackedInt32Field(value: self.serverCores, fieldNumber: 5) + } + try { if let v = self._summary { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + if !self.clientSuccess.isEmpty { + try visitor.visitPackedBoolField(value: self.clientSuccess, fieldNumber: 7) + } + if !self.serverSuccess.isEmpty { + try visitor.visitPackedBoolField(value: self.serverSuccess, fieldNumber: 8) + } + if !self.requestResults.isEmpty { + try visitor.visitRepeatedMessageField(value: self.requestResults, fieldNumber: 9) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ScenarioResult, rhs: Grpc_Testing_ScenarioResult) -> Bool { + if lhs._scenario != rhs._scenario {return false} + if lhs._latencies != rhs._latencies {return false} + if lhs.clientStats != rhs.clientStats {return false} + if lhs.serverStats != rhs.serverStats {return false} + if lhs.serverCores != rhs.serverCores {return false} + if lhs._summary != rhs._summary {return false} + if lhs.clientSuccess != rhs.clientSuccess {return false} + if lhs.serverSuccess != rhs.serverSuccess {return false} + if lhs.requestResults != rhs.requestResults {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift b/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift new file mode 100644 index 000000000..e1239674f --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift @@ -0,0 +1,2200 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/messages.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015-2016 gRPC authors. +// +// 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 definitions to be used by integration test service definitions. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The type of payload that should be returned. +enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Compressable text format. + case compressable // = 0 + case UNRECOGNIZED(Int) + + init() { + self = .compressable + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .compressable + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .compressable: return 0 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Grpc_Testing_PayloadType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_PayloadType] = [ + .compressable, + ] +} + +#endif // swift(>=4.2) + +/// The type of route that a client took to reach a server w.r.t. gRPCLB. +/// The server must fill in "fallback" if it detects that the RPC reached +/// the server via the "gRPCLB fallback" path, and "backend" if it detects +/// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got +/// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly +/// how this detection is done is context and server dependent. +enum Grpc_Testing_GrpclbRouteType: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Server didn't detect the route that a client took to reach it. + case unknown // = 0 + + /// Indicates that a client reached a server via gRPCLB fallback. + case fallback // = 1 + + /// Indicates that a client reached a server as a gRPCLB-given backend. + case backend // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .fallback + case 2: self = .backend + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .fallback: return 1 + case .backend: return 2 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension Grpc_Testing_GrpclbRouteType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_GrpclbRouteType] = [ + .unknown, + .fallback, + .backend, + ] +} + +#endif // swift(>=4.2) + +/// TODO(dgq): Go back to using well-known types once +/// https://github.com/grpc/grpc/issues/6980 has been fixed. +/// import "google/protobuf/wrappers.proto"; +struct Grpc_Testing_BoolValue { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The bool value. + var value: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A block of data, to simply increase gRPC message size. +struct Grpc_Testing_Payload { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The type of data in body. + var type: Grpc_Testing_PayloadType = .compressable + + /// Primary contents of payload. + var body: Data = Data() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A protobuf representation for grpc status. This is used by test +/// clients to specify a status that the server should attempt to return. +struct Grpc_Testing_EchoStatus { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var code: Int32 = 0 + + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Unary request. +struct Grpc_Testing_SimpleRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Desired payload type in the response from the server. + /// If response_type is RANDOM, server randomly chooses one from other formats. + var responseType: Grpc_Testing_PayloadType = .compressable + + /// Desired payload size in the response from the server. + var responseSize: Int32 = 0 + + /// Optional input payload sent along with the request. + var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + mutating func clearPayload() {self._payload = nil} + + /// Whether SimpleResponse should include username. + var fillUsername: Bool = false + + /// Whether SimpleResponse should include OAuth scope. + var fillOauthScope: Bool = false + + /// Whether to request the server to compress the response. This field is + /// "nullable" in order to interoperate seamlessly with clients not able to + /// implement the full compression tests by introspecting the call to verify + /// the response's compression status. + var responseCompressed: Grpc_Testing_BoolValue { + get {return _responseCompressed ?? Grpc_Testing_BoolValue()} + set {_responseCompressed = newValue} + } + /// Returns true if `responseCompressed` has been explicitly set. + var hasResponseCompressed: Bool {return self._responseCompressed != nil} + /// Clears the value of `responseCompressed`. Subsequent reads from it will return its default value. + mutating func clearResponseCompressed() {self._responseCompressed = nil} + + /// Whether server should return a given status + var responseStatus: Grpc_Testing_EchoStatus { + get {return _responseStatus ?? Grpc_Testing_EchoStatus()} + set {_responseStatus = newValue} + } + /// Returns true if `responseStatus` has been explicitly set. + var hasResponseStatus: Bool {return self._responseStatus != nil} + /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. + mutating func clearResponseStatus() {self._responseStatus = nil} + + /// Whether the server should expect this request to be compressed. + var expectCompressed: Grpc_Testing_BoolValue { + get {return _expectCompressed ?? Grpc_Testing_BoolValue()} + set {_expectCompressed = newValue} + } + /// Returns true if `expectCompressed` has been explicitly set. + var hasExpectCompressed: Bool {return self._expectCompressed != nil} + /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. + mutating func clearExpectCompressed() {self._expectCompressed = nil} + + /// Whether SimpleResponse should include server_id. + var fillServerID: Bool = false + + /// Whether SimpleResponse should include grpclb_route_type. + var fillGrpclbRouteType: Bool = false + + /// If set the server should record this metrics report data for the current RPC. + var orcaPerQueryReport: Grpc_Testing_TestOrcaReport { + get {return _orcaPerQueryReport ?? Grpc_Testing_TestOrcaReport()} + set {_orcaPerQueryReport = newValue} + } + /// Returns true if `orcaPerQueryReport` has been explicitly set. + var hasOrcaPerQueryReport: Bool {return self._orcaPerQueryReport != nil} + /// Clears the value of `orcaPerQueryReport`. Subsequent reads from it will return its default value. + mutating func clearOrcaPerQueryReport() {self._orcaPerQueryReport = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil + fileprivate var _responseCompressed: Grpc_Testing_BoolValue? = nil + fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil + fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil + fileprivate var _orcaPerQueryReport: Grpc_Testing_TestOrcaReport? = nil +} + +/// Unary response, as configured by the request. +struct Grpc_Testing_SimpleResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Payload to increase message size. + var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + mutating func clearPayload() {self._payload = nil} + + /// The user the request came from, for verifying authentication was + /// successful when the client expected it. + var username: String = String() + + /// OAuth scope. + var oauthScope: String = String() + + /// Server ID. This must be unique among different server instances, + /// but the same across all RPC's made to a particular server instance. + var serverID: String = String() + + /// gRPCLB Path. + var grpclbRouteType: Grpc_Testing_GrpclbRouteType = .unknown + + /// Server hostname. + var hostname: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil +} + +/// Client-streaming request. +struct Grpc_Testing_StreamingInputCallRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Optional input payload sent along with the request. + var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + mutating func clearPayload() {self._payload = nil} + + /// Whether the server should expect this request to be compressed. This field + /// is "nullable" in order to interoperate seamlessly with servers not able to + /// implement the full compression tests by introspecting the call to verify + /// the request's compression status. + var expectCompressed: Grpc_Testing_BoolValue { + get {return _expectCompressed ?? Grpc_Testing_BoolValue()} + set {_expectCompressed = newValue} + } + /// Returns true if `expectCompressed` has been explicitly set. + var hasExpectCompressed: Bool {return self._expectCompressed != nil} + /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. + mutating func clearExpectCompressed() {self._expectCompressed = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil + fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil +} + +/// Client-streaming response. +struct Grpc_Testing_StreamingInputCallResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Aggregated size of payloads received from the client. + var aggregatedPayloadSize: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Configuration for a particular response. +struct Grpc_Testing_ResponseParameters { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Desired payload sizes in responses from the server. + var size: Int32 = 0 + + /// Desired interval between consecutive responses in the response stream in + /// microseconds. + var intervalUs: Int32 = 0 + + /// Whether to request the server to compress the response. This field is + /// "nullable" in order to interoperate seamlessly with clients not able to + /// implement the full compression tests by introspecting the call to verify + /// the response's compression status. + var compressed: Grpc_Testing_BoolValue { + get {return _compressed ?? Grpc_Testing_BoolValue()} + set {_compressed = newValue} + } + /// Returns true if `compressed` has been explicitly set. + var hasCompressed: Bool {return self._compressed != nil} + /// Clears the value of `compressed`. Subsequent reads from it will return its default value. + mutating func clearCompressed() {self._compressed = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _compressed: Grpc_Testing_BoolValue? = nil +} + +/// Server-streaming request. +struct Grpc_Testing_StreamingOutputCallRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Desired payload type in the response from the server. + /// If response_type is RANDOM, the payload from each response in the stream + /// might be of different types. This is to simulate a mixed type of payload + /// stream. + var responseType: Grpc_Testing_PayloadType = .compressable + + /// Configuration for each expected response message. + var responseParameters: [Grpc_Testing_ResponseParameters] = [] + + /// Optional input payload sent along with the request. + var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + mutating func clearPayload() {self._payload = nil} + + /// Whether server should return a given status + var responseStatus: Grpc_Testing_EchoStatus { + get {return _responseStatus ?? Grpc_Testing_EchoStatus()} + set {_responseStatus = newValue} + } + /// Returns true if `responseStatus` has been explicitly set. + var hasResponseStatus: Bool {return self._responseStatus != nil} + /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. + mutating func clearResponseStatus() {self._responseStatus = nil} + + /// If set the server should update this metrics report data at the OOB server. + var orcaOobReport: Grpc_Testing_TestOrcaReport { + get {return _orcaOobReport ?? Grpc_Testing_TestOrcaReport()} + set {_orcaOobReport = newValue} + } + /// Returns true if `orcaOobReport` has been explicitly set. + var hasOrcaOobReport: Bool {return self._orcaOobReport != nil} + /// Clears the value of `orcaOobReport`. Subsequent reads from it will return its default value. + mutating func clearOrcaOobReport() {self._orcaOobReport = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil + fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil + fileprivate var _orcaOobReport: Grpc_Testing_TestOrcaReport? = nil +} + +/// Server-streaming response, as configured by the request and parameters. +struct Grpc_Testing_StreamingOutputCallResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Payload to increase response size. + var payload: Grpc_Testing_Payload { + get {return _payload ?? Grpc_Testing_Payload()} + set {_payload = newValue} + } + /// Returns true if `payload` has been explicitly set. + var hasPayload: Bool {return self._payload != nil} + /// Clears the value of `payload`. Subsequent reads from it will return its default value. + mutating func clearPayload() {self._payload = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _payload: Grpc_Testing_Payload? = nil +} + +/// For reconnect interop test only. +/// Client tells server what reconnection parameters it used. +struct Grpc_Testing_ReconnectParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var maxReconnectBackoffMs: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// For reconnect interop test only. +/// Server tells client whether its reconnects are following the spec and the +/// reconnect backoffs it saw. +struct Grpc_Testing_ReconnectInfo { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var passed: Bool = false + + var backoffMs: [Int32] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_LoadBalancerStatsRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Request stats for the next num_rpcs sent by client. + var numRpcs: Int32 = 0 + + /// If num_rpcs have not completed within timeout_sec, return partial results. + var timeoutSec: Int32 = 0 + + /// Response header + trailer metadata entries we want the values of. + /// Matching of the keys is case-insensitive as per rfc7540#section-8.1.2 + /// * (asterisk) is a special value that will return all metadata entries + var metadataKeys: [String] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_LoadBalancerStatsResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The number of completed RPCs for each peer. + var rpcsByPeer: Dictionary = [:] + + /// The number of RPCs that failed to record a remote peer. + var numFailures: Int32 = 0 + + var rpcsByMethod: Dictionary = [:] + + /// All the metadata of all RPCs for each peer. + var metadatasByPeer: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum MetadataType: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case initial // = 1 + case trailing // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .initial + case 2: self = .trailing + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .initial: return 1 + case .trailing: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + } + + struct MetadataEntry { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Key, exactly as received from the server. Case may be different from what + /// was requested in the LoadBalancerStatsRequest) + var key: String = String() + + /// Value, exactly as received from the server. + var value: String = String() + + /// Metadata type + var type: Grpc_Testing_LoadBalancerStatsResponse.MetadataType = .unknown + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + struct RpcMetadata { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// metadata values for each rpc for the keys specified in + /// LoadBalancerStatsRequest.metadata_keys. + var metadata: [Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + struct MetadataByPeer { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// List of RpcMetadata in for each RPC with a given peer + var rpcMetadata: [Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + struct RpcsByPeer { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The number of completed RPCs for each peer. + var rpcsByPeer: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} +} + +#if swift(>=4.2) + +extension Grpc_Testing_LoadBalancerStatsResponse.MetadataType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_LoadBalancerStatsResponse.MetadataType] = [ + .unknown, + .initial, + .trailing, + ] +} + +#endif // swift(>=4.2) + +/// Request for retrieving a test client's accumulated stats. +struct Grpc_Testing_LoadBalancerAccumulatedStatsRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Accumulated stats for RPCs sent by a test client. +struct Grpc_Testing_LoadBalancerAccumulatedStatsResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The total number of RPCs have ever issued for each type. + /// Deprecated: use stats_per_method.rpcs_started instead. + var numRpcsStartedByMethod: Dictionary = [:] + + /// The total number of RPCs have ever completed successfully for each type. + /// Deprecated: use stats_per_method.result instead. + var numRpcsSucceededByMethod: Dictionary = [:] + + /// The total number of RPCs have ever failed for each type. + /// Deprecated: use stats_per_method.result instead. + var numRpcsFailedByMethod: Dictionary = [:] + + /// Per-method RPC statistics. The key is the RpcType in string form; e.g. + /// 'EMPTY_CALL' or 'UNARY_CALL' + var statsPerMethod: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + struct MethodStats { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The number of RPCs that were started for this method. + var rpcsStarted: Int32 = 0 + + /// The number of RPCs that completed with each status for this method. The + /// key is the integral value of a google.rpc.Code; the value is the count. + var result: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} +} + +/// Configurations for a test client. +struct Grpc_Testing_ClientConfigureRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The types of RPCs the client sends. + var types: [Grpc_Testing_ClientConfigureRequest.RpcType] = [] + + /// The collection of custom metadata to be attached to RPCs sent by the client. + var metadata: [Grpc_Testing_ClientConfigureRequest.Metadata] = [] + + /// The deadline to use, in seconds, for all RPCs. If unset or zero, the + /// client will use the default from the command-line. + var timeoutSec: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// Type of RPCs to send. + enum RpcType: SwiftProtobuf.Enum { + typealias RawValue = Int + case emptyCall // = 0 + case unaryCall // = 1 + case UNRECOGNIZED(Int) + + init() { + self = .emptyCall + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .emptyCall + case 1: self = .unaryCall + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .emptyCall: return 0 + case .unaryCall: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + } + + /// Metadata to be attached for the given type of RPCs. + struct Metadata { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var type: Grpc_Testing_ClientConfigureRequest.RpcType = .emptyCall + + var key: String = String() + + var value: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + + init() {} +} + +#if swift(>=4.2) + +extension Grpc_Testing_ClientConfigureRequest.RpcType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_ClientConfigureRequest.RpcType] = [ + .emptyCall, + .unaryCall, + ] +} + +#endif // swift(>=4.2) + +/// Response for updating a test client's configuration. +struct Grpc_Testing_ClientConfigureResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_MemorySize { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var rss: Int64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Metrics data the server will update and send to the client. It mirrors orca load report +/// https://github.com/cncf/xds/blob/eded343319d09f30032952beda9840bbd3dcf7ac/xds/data/orca/v3/orca_load_report.proto#L15, +/// but avoids orca dependency. Used by both per-query and out-of-band reporting tests. +struct Grpc_Testing_TestOrcaReport { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var cpuUtilization: Double = 0 + + var memoryUtilization: Double = 0 + + var requestCost: Dictionary = [:] + + var utilization: Dictionary = [:] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Status that will be return to callers of the Hook method +struct Grpc_Testing_SetReturnStatusRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var grpcCodeToReturn: Int32 = 0 + + var grpcStatusDescription: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_HookRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var command: Grpc_Testing_HookRequest.HookRequestCommand = .unspecified + + var grpcCodeToReturn: Int32 = 0 + + var grpcStatusDescription: String = String() + + /// Server port to listen to + var serverPort: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum HookRequestCommand: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// Default value + case unspecified // = 0 + + /// Start the HTTP endpoint + case start // = 1 + + /// Stop + case stop // = 2 + + /// Return from HTTP GET/POST + case `return` // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .unspecified + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unspecified + case 1: self = .start + case 2: self = .stop + case 3: self = .return + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unspecified: return 0 + case .start: return 1 + case .stop: return 2 + case .return: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} +} + +#if swift(>=4.2) + +extension Grpc_Testing_HookRequest.HookRequestCommand: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_HookRequest.HookRequestCommand] = [ + .unspecified, + .start, + .stop, + .return, + ] +} + +#endif // swift(>=4.2) + +struct Grpc_Testing_HookResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Testing_PayloadType: @unchecked Sendable {} +extension Grpc_Testing_GrpclbRouteType: @unchecked Sendable {} +extension Grpc_Testing_BoolValue: @unchecked Sendable {} +extension Grpc_Testing_Payload: @unchecked Sendable {} +extension Grpc_Testing_EchoStatus: @unchecked Sendable {} +extension Grpc_Testing_SimpleRequest: @unchecked Sendable {} +extension Grpc_Testing_SimpleResponse: @unchecked Sendable {} +extension Grpc_Testing_StreamingInputCallRequest: @unchecked Sendable {} +extension Grpc_Testing_StreamingInputCallResponse: @unchecked Sendable {} +extension Grpc_Testing_ResponseParameters: @unchecked Sendable {} +extension Grpc_Testing_StreamingOutputCallRequest: @unchecked Sendable {} +extension Grpc_Testing_StreamingOutputCallResponse: @unchecked Sendable {} +extension Grpc_Testing_ReconnectParams: @unchecked Sendable {} +extension Grpc_Testing_ReconnectInfo: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerStatsRequest: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerStatsResponse: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerStatsResponse.MetadataType: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerAccumulatedStatsRequest: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse: @unchecked Sendable {} +extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats: @unchecked Sendable {} +extension Grpc_Testing_ClientConfigureRequest: @unchecked Sendable {} +extension Grpc_Testing_ClientConfigureRequest.RpcType: @unchecked Sendable {} +extension Grpc_Testing_ClientConfigureRequest.Metadata: @unchecked Sendable {} +extension Grpc_Testing_ClientConfigureResponse: @unchecked Sendable {} +extension Grpc_Testing_MemorySize: @unchecked Sendable {} +extension Grpc_Testing_TestOrcaReport: @unchecked Sendable {} +extension Grpc_Testing_SetReturnStatusRequest: @unchecked Sendable {} +extension Grpc_Testing_HookRequest: @unchecked Sendable {} +extension Grpc_Testing_HookRequest.HookRequestCommand: @unchecked Sendable {} +extension Grpc_Testing_HookResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.testing" + +extension Grpc_Testing_PayloadType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "COMPRESSABLE"), + ] +} + +extension Grpc_Testing_GrpclbRouteType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "GRPCLB_ROUTE_TYPE_UNKNOWN"), + 1: .same(proto: "GRPCLB_ROUTE_TYPE_FALLBACK"), + 2: .same(proto: "GRPCLB_ROUTE_TYPE_BACKEND"), + ] +} + +extension Grpc_Testing_BoolValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".BoolValue" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "value"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.value != false { + try visitor.visitSingularBoolField(value: self.value, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_BoolValue, rhs: Grpc_Testing_BoolValue) -> Bool { + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_Payload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Payload" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "body"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() + case 2: try { try decoder.decodeSingularBytesField(value: &self.body) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.type != .compressable { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) + } + if !self.body.isEmpty { + try visitor.visitSingularBytesField(value: self.body, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_Payload, rhs: Grpc_Testing_Payload) -> Bool { + if lhs.type != rhs.type {return false} + if lhs.body != rhs.body {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_EchoStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EchoStatus" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "code"), + 2: .same(proto: "message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.code != 0 { + try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) + } + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_EchoStatus, rhs: Grpc_Testing_EchoStatus) -> Bool { + if lhs.code != rhs.code {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_SimpleRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SimpleRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "response_type"), + 2: .standard(proto: "response_size"), + 3: .same(proto: "payload"), + 4: .standard(proto: "fill_username"), + 5: .standard(proto: "fill_oauth_scope"), + 6: .standard(proto: "response_compressed"), + 7: .standard(proto: "response_status"), + 8: .standard(proto: "expect_compressed"), + 9: .standard(proto: "fill_server_id"), + 10: .standard(proto: "fill_grpclb_route_type"), + 11: .standard(proto: "orca_per_query_report"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.responseSize) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.fillUsername) }() + case 5: try { try decoder.decodeSingularBoolField(value: &self.fillOauthScope) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._responseCompressed) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() + case 8: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() + case 9: try { try decoder.decodeSingularBoolField(value: &self.fillServerID) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.fillGrpclbRouteType) }() + case 11: try { try decoder.decodeSingularMessageField(value: &self._orcaPerQueryReport) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.responseType != .compressable { + try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) + } + if self.responseSize != 0 { + try visitor.visitSingularInt32Field(value: self.responseSize, fieldNumber: 2) + } + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if self.fillUsername != false { + try visitor.visitSingularBoolField(value: self.fillUsername, fieldNumber: 4) + } + if self.fillOauthScope != false { + try visitor.visitSingularBoolField(value: self.fillOauthScope, fieldNumber: 5) + } + try { if let v = self._responseCompressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + try { if let v = self._responseStatus { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try { if let v = self._expectCompressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() + if self.fillServerID != false { + try visitor.visitSingularBoolField(value: self.fillServerID, fieldNumber: 9) + } + if self.fillGrpclbRouteType != false { + try visitor.visitSingularBoolField(value: self.fillGrpclbRouteType, fieldNumber: 10) + } + try { if let v = self._orcaPerQueryReport { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_SimpleRequest, rhs: Grpc_Testing_SimpleRequest) -> Bool { + if lhs.responseType != rhs.responseType {return false} + if lhs.responseSize != rhs.responseSize {return false} + if lhs._payload != rhs._payload {return false} + if lhs.fillUsername != rhs.fillUsername {return false} + if lhs.fillOauthScope != rhs.fillOauthScope {return false} + if lhs._responseCompressed != rhs._responseCompressed {return false} + if lhs._responseStatus != rhs._responseStatus {return false} + if lhs._expectCompressed != rhs._expectCompressed {return false} + if lhs.fillServerID != rhs.fillServerID {return false} + if lhs.fillGrpclbRouteType != rhs.fillGrpclbRouteType {return false} + if lhs._orcaPerQueryReport != rhs._orcaPerQueryReport {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_SimpleResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SimpleResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "payload"), + 2: .same(proto: "username"), + 3: .standard(proto: "oauth_scope"), + 4: .standard(proto: "server_id"), + 5: .standard(proto: "grpclb_route_type"), + 6: .same(proto: "hostname"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.username) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.oauthScope) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.serverID) }() + case 5: try { try decoder.decodeSingularEnumField(value: &self.grpclbRouteType) }() + case 6: try { try decoder.decodeSingularStringField(value: &self.hostname) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.username.isEmpty { + try visitor.visitSingularStringField(value: self.username, fieldNumber: 2) + } + if !self.oauthScope.isEmpty { + try visitor.visitSingularStringField(value: self.oauthScope, fieldNumber: 3) + } + if !self.serverID.isEmpty { + try visitor.visitSingularStringField(value: self.serverID, fieldNumber: 4) + } + if self.grpclbRouteType != .unknown { + try visitor.visitSingularEnumField(value: self.grpclbRouteType, fieldNumber: 5) + } + if !self.hostname.isEmpty { + try visitor.visitSingularStringField(value: self.hostname, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_SimpleResponse, rhs: Grpc_Testing_SimpleResponse) -> Bool { + if lhs._payload != rhs._payload {return false} + if lhs.username != rhs.username {return false} + if lhs.oauthScope != rhs.oauthScope {return false} + if lhs.serverID != rhs.serverID {return false} + if lhs.grpclbRouteType != rhs.grpclbRouteType {return false} + if lhs.hostname != rhs.hostname {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingInputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".StreamingInputCallRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "payload"), + 2: .standard(proto: "expect_compressed"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._expectCompressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_StreamingInputCallRequest, rhs: Grpc_Testing_StreamingInputCallRequest) -> Bool { + if lhs._payload != rhs._payload {return false} + if lhs._expectCompressed != rhs._expectCompressed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingInputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".StreamingInputCallResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "aggregated_payload_size"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.aggregatedPayloadSize) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.aggregatedPayloadSize != 0 { + try visitor.visitSingularInt32Field(value: self.aggregatedPayloadSize, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_StreamingInputCallResponse, rhs: Grpc_Testing_StreamingInputCallResponse) -> Bool { + if lhs.aggregatedPayloadSize != rhs.aggregatedPayloadSize {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ResponseParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ResponseParameters" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "size"), + 2: .standard(proto: "interval_us"), + 3: .same(proto: "compressed"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.intervalUs) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._compressed) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.size != 0 { + try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) + } + if self.intervalUs != 0 { + try visitor.visitSingularInt32Field(value: self.intervalUs, fieldNumber: 2) + } + try { if let v = self._compressed { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ResponseParameters, rhs: Grpc_Testing_ResponseParameters) -> Bool { + if lhs.size != rhs.size {return false} + if lhs.intervalUs != rhs.intervalUs {return false} + if lhs._compressed != rhs._compressed {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingOutputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "response_type"), + 2: .standard(proto: "response_parameters"), + 3: .same(proto: "payload"), + 7: .standard(proto: "response_status"), + 8: .standard(proto: "orca_oob_report"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.responseParameters) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() + case 8: try { try decoder.decodeSingularMessageField(value: &self._orcaOobReport) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.responseType != .compressable { + try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) + } + if !self.responseParameters.isEmpty { + try visitor.visitRepeatedMessageField(value: self.responseParameters, fieldNumber: 2) + } + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._responseStatus { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try { if let v = self._orcaOobReport { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_StreamingOutputCallRequest, rhs: Grpc_Testing_StreamingOutputCallRequest) -> Bool { + if lhs.responseType != rhs.responseType {return false} + if lhs.responseParameters != rhs.responseParameters {return false} + if lhs._payload != rhs._payload {return false} + if lhs._responseStatus != rhs._responseStatus {return false} + if lhs._orcaOobReport != rhs._orcaOobReport {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_StreamingOutputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "payload"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._payload { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_StreamingOutputCallResponse, rhs: Grpc_Testing_StreamingOutputCallResponse) -> Bool { + if lhs._payload != rhs._payload {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ReconnectParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ReconnectParams" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "max_reconnect_backoff_ms"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.maxReconnectBackoffMs) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.maxReconnectBackoffMs != 0 { + try visitor.visitSingularInt32Field(value: self.maxReconnectBackoffMs, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ReconnectParams, rhs: Grpc_Testing_ReconnectParams) -> Bool { + if lhs.maxReconnectBackoffMs != rhs.maxReconnectBackoffMs {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ReconnectInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ReconnectInfo" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "passed"), + 2: .standard(proto: "backoff_ms"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.passed) }() + case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.backoffMs) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.passed != false { + try visitor.visitSingularBoolField(value: self.passed, fieldNumber: 1) + } + if !self.backoffMs.isEmpty { + try visitor.visitPackedInt32Field(value: self.backoffMs, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ReconnectInfo, rhs: Grpc_Testing_ReconnectInfo) -> Bool { + if lhs.passed != rhs.passed {return false} + if lhs.backoffMs != rhs.backoffMs {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerStatsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LoadBalancerStatsRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "num_rpcs"), + 2: .standard(proto: "timeout_sec"), + 3: .standard(proto: "metadata_keys"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.numRpcs) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.timeoutSec) }() + case 3: try { try decoder.decodeRepeatedStringField(value: &self.metadataKeys) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.numRpcs != 0 { + try visitor.visitSingularInt32Field(value: self.numRpcs, fieldNumber: 1) + } + if self.timeoutSec != 0 { + try visitor.visitSingularInt32Field(value: self.timeoutSec, fieldNumber: 2) + } + if !self.metadataKeys.isEmpty { + try visitor.visitRepeatedStringField(value: self.metadataKeys, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerStatsRequest, rhs: Grpc_Testing_LoadBalancerStatsRequest) -> Bool { + if lhs.numRpcs != rhs.numRpcs {return false} + if lhs.timeoutSec != rhs.timeoutSec {return false} + if lhs.metadataKeys != rhs.metadataKeys {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerStatsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LoadBalancerStatsResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "rpcs_by_peer"), + 2: .standard(proto: "num_failures"), + 3: .standard(proto: "rpcs_by_method"), + 4: .standard(proto: "metadatas_by_peer"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.rpcsByPeer) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.numFailures) }() + case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.rpcsByMethod) }() + case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.metadatasByPeer) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.rpcsByPeer.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.rpcsByPeer, fieldNumber: 1) + } + if self.numFailures != 0 { + try visitor.visitSingularInt32Field(value: self.numFailures, fieldNumber: 2) + } + if !self.rpcsByMethod.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.rpcsByMethod, fieldNumber: 3) + } + if !self.metadatasByPeer.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.metadatasByPeer, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse, rhs: Grpc_Testing_LoadBalancerStatsResponse) -> Bool { + if lhs.rpcsByPeer != rhs.rpcsByPeer {return false} + if lhs.numFailures != rhs.numFailures {return false} + if lhs.rpcsByMethod != rhs.rpcsByMethod {return false} + if lhs.metadatasByPeer != rhs.metadatasByPeer {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerStatsResponse.MetadataType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "INITIAL"), + 2: .same(proto: "TRAILING"), + ] +} + +extension Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".MetadataEntry" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "key"), + 2: .same(proto: "value"), + 3: .same(proto: "type"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.key) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.value) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.type) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.key.isEmpty { + try visitor.visitSingularStringField(value: self.key, fieldNumber: 1) + } + if !self.value.isEmpty { + try visitor.visitSingularStringField(value: self.value, fieldNumber: 2) + } + if self.type != .unknown { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry, rhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry) -> Bool { + if lhs.key != rhs.key {return false} + if lhs.value != rhs.value {return false} + if lhs.type != rhs.type {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".RpcMetadata" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "metadata"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.metadata) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.metadata.isEmpty { + try visitor.visitRepeatedMessageField(value: self.metadata, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata, rhs: Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata) -> Bool { + if lhs.metadata != rhs.metadata {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".MetadataByPeer" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "rpc_metadata"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.rpcMetadata) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.rpcMetadata.isEmpty { + try visitor.visitRepeatedMessageField(value: self.rpcMetadata, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer, rhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer) -> Bool { + if lhs.rpcMetadata != rhs.rpcMetadata {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".RpcsByPeer" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "rpcs_by_peer"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.rpcsByPeer) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.rpcsByPeer.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.rpcsByPeer, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer, rhs: Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer) -> Bool { + if lhs.rpcsByPeer != rhs.rpcsByPeer {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerAccumulatedStatsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LoadBalancerAccumulatedStatsRequest" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerAccumulatedStatsRequest, rhs: Grpc_Testing_LoadBalancerAccumulatedStatsRequest) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LoadBalancerAccumulatedStatsResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "num_rpcs_started_by_method"), + 2: .standard(proto: "num_rpcs_succeeded_by_method"), + 3: .standard(proto: "num_rpcs_failed_by_method"), + 4: .standard(proto: "stats_per_method"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.numRpcsStartedByMethod) }() + case 2: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.numRpcsSucceededByMethod) }() + case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.numRpcsFailedByMethod) }() + case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.statsPerMethod) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.numRpcsStartedByMethod.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.numRpcsStartedByMethod, fieldNumber: 1) + } + if !self.numRpcsSucceededByMethod.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.numRpcsSucceededByMethod, fieldNumber: 2) + } + if !self.numRpcsFailedByMethod.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.numRpcsFailedByMethod, fieldNumber: 3) + } + if !self.statsPerMethod.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.statsPerMethod, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse, rhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse) -> Bool { + if lhs.numRpcsStartedByMethod != rhs.numRpcsStartedByMethod {return false} + if lhs.numRpcsSucceededByMethod != rhs.numRpcsSucceededByMethod {return false} + if lhs.numRpcsFailedByMethod != rhs.numRpcsFailedByMethod {return false} + if lhs.statsPerMethod != rhs.statsPerMethod {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Testing_LoadBalancerAccumulatedStatsResponse.protoMessageName + ".MethodStats" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "rpcs_started"), + 2: .same(proto: "result"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.rpcsStarted) }() + case 2: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.result) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.rpcsStarted != 0 { + try visitor.visitSingularInt32Field(value: self.rpcsStarted, fieldNumber: 1) + } + if !self.result.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.result, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats, rhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats) -> Bool { + if lhs.rpcsStarted != rhs.rpcsStarted {return false} + if lhs.result != rhs.result {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClientConfigureRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClientConfigureRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "types"), + 2: .same(proto: "metadata"), + 3: .standard(proto: "timeout_sec"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedEnumField(value: &self.types) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.metadata) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.timeoutSec) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.types.isEmpty { + try visitor.visitPackedEnumField(value: self.types, fieldNumber: 1) + } + if !self.metadata.isEmpty { + try visitor.visitRepeatedMessageField(value: self.metadata, fieldNumber: 2) + } + if self.timeoutSec != 0 { + try visitor.visitSingularInt32Field(value: self.timeoutSec, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClientConfigureRequest, rhs: Grpc_Testing_ClientConfigureRequest) -> Bool { + if lhs.types != rhs.types {return false} + if lhs.metadata != rhs.metadata {return false} + if lhs.timeoutSec != rhs.timeoutSec {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClientConfigureRequest.RpcType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "EMPTY_CALL"), + 1: .same(proto: "UNARY_CALL"), + ] +} + +extension Grpc_Testing_ClientConfigureRequest.Metadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_Testing_ClientConfigureRequest.protoMessageName + ".Metadata" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "key"), + 3: .same(proto: "value"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.key) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.value) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.type != .emptyCall { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) + } + if !self.key.isEmpty { + try visitor.visitSingularStringField(value: self.key, fieldNumber: 2) + } + if !self.value.isEmpty { + try visitor.visitSingularStringField(value: self.value, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClientConfigureRequest.Metadata, rhs: Grpc_Testing_ClientConfigureRequest.Metadata) -> Bool { + if lhs.type != rhs.type {return false} + if lhs.key != rhs.key {return false} + if lhs.value != rhs.value {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClientConfigureResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClientConfigureResponse" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClientConfigureResponse, rhs: Grpc_Testing_ClientConfigureResponse) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_MemorySize: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MemorySize" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "rss"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt64Field(value: &self.rss) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.rss != 0 { + try visitor.visitSingularInt64Field(value: self.rss, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_MemorySize, rhs: Grpc_Testing_MemorySize) -> Bool { + if lhs.rss != rhs.rss {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_TestOrcaReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".TestOrcaReport" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "cpu_utilization"), + 2: .standard(proto: "memory_utilization"), + 3: .standard(proto: "request_cost"), + 4: .same(proto: "utilization"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularDoubleField(value: &self.cpuUtilization) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &self.memoryUtilization) }() + case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.requestCost) }() + case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.utilization) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.cpuUtilization != 0 { + try visitor.visitSingularDoubleField(value: self.cpuUtilization, fieldNumber: 1) + } + if self.memoryUtilization != 0 { + try visitor.visitSingularDoubleField(value: self.memoryUtilization, fieldNumber: 2) + } + if !self.requestCost.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.requestCost, fieldNumber: 3) + } + if !self.utilization.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.utilization, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_TestOrcaReport, rhs: Grpc_Testing_TestOrcaReport) -> Bool { + if lhs.cpuUtilization != rhs.cpuUtilization {return false} + if lhs.memoryUtilization != rhs.memoryUtilization {return false} + if lhs.requestCost != rhs.requestCost {return false} + if lhs.utilization != rhs.utilization {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_SetReturnStatusRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SetReturnStatusRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "grpc_code_to_return"), + 2: .standard(proto: "grpc_status_description"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.grpcCodeToReturn) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.grpcStatusDescription) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.grpcCodeToReturn != 0 { + try visitor.visitSingularInt32Field(value: self.grpcCodeToReturn, fieldNumber: 1) + } + if !self.grpcStatusDescription.isEmpty { + try visitor.visitSingularStringField(value: self.grpcStatusDescription, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_SetReturnStatusRequest, rhs: Grpc_Testing_SetReturnStatusRequest) -> Bool { + if lhs.grpcCodeToReturn != rhs.grpcCodeToReturn {return false} + if lhs.grpcStatusDescription != rhs.grpcStatusDescription {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_HookRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HookRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "command"), + 2: .standard(proto: "grpc_code_to_return"), + 3: .standard(proto: "grpc_status_description"), + 4: .standard(proto: "server_port"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.command) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.grpcCodeToReturn) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.grpcStatusDescription) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.serverPort) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.command != .unspecified { + try visitor.visitSingularEnumField(value: self.command, fieldNumber: 1) + } + if self.grpcCodeToReturn != 0 { + try visitor.visitSingularInt32Field(value: self.grpcCodeToReturn, fieldNumber: 2) + } + if !self.grpcStatusDescription.isEmpty { + try visitor.visitSingularStringField(value: self.grpcStatusDescription, fieldNumber: 3) + } + if self.serverPort != 0 { + try visitor.visitSingularInt32Field(value: self.serverPort, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_HookRequest, rhs: Grpc_Testing_HookRequest) -> Bool { + if lhs.command != rhs.command {return false} + if lhs.grpcCodeToReturn != rhs.grpcCodeToReturn {return false} + if lhs.grpcStatusDescription != rhs.grpcStatusDescription {return false} + if lhs.serverPort != rhs.serverPort {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_HookRequest.HookRequestCommand: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNSPECIFIED"), + 1: .same(proto: "START"), + 2: .same(proto: "STOP"), + 3: .same(proto: "RETURN"), + ] +} + +extension Grpc_Testing_HookResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HookResponse" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_HookResponse, rhs: Grpc_Testing_HookResponse) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift new file mode 100644 index 000000000..4b04eec01 --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift @@ -0,0 +1,335 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/payloads.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 gRPC authors. +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct Grpc_Testing_ByteBufferParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var reqSize: Int32 = 0 + + var respSize: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_SimpleProtoParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var reqSize: Int32 = 0 + + var respSize: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// TODO (vpai): Fill this in once the details of complex, representative +/// protos are decided +struct Grpc_Testing_ComplexProtoParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_PayloadConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var payload: Grpc_Testing_PayloadConfig.OneOf_Payload? = nil + + var bytebufParams: Grpc_Testing_ByteBufferParams { + get { + if case .bytebufParams(let v)? = payload {return v} + return Grpc_Testing_ByteBufferParams() + } + set {payload = .bytebufParams(newValue)} + } + + var simpleParams: Grpc_Testing_SimpleProtoParams { + get { + if case .simpleParams(let v)? = payload {return v} + return Grpc_Testing_SimpleProtoParams() + } + set {payload = .simpleParams(newValue)} + } + + var complexParams: Grpc_Testing_ComplexProtoParams { + get { + if case .complexParams(let v)? = payload {return v} + return Grpc_Testing_ComplexProtoParams() + } + set {payload = .complexParams(newValue)} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum OneOf_Payload: Equatable { + case bytebufParams(Grpc_Testing_ByteBufferParams) + case simpleParams(Grpc_Testing_SimpleProtoParams) + case complexParams(Grpc_Testing_ComplexProtoParams) + + #if !swift(>=4.1) + static func ==(lhs: Grpc_Testing_PayloadConfig.OneOf_Payload, rhs: Grpc_Testing_PayloadConfig.OneOf_Payload) -> Bool { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch (lhs, rhs) { + case (.bytebufParams, .bytebufParams): return { + guard case .bytebufParams(let l) = lhs, case .bytebufParams(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.simpleParams, .simpleParams): return { + guard case .simpleParams(let l) = lhs, case .simpleParams(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.complexParams, .complexParams): return { + guard case .complexParams(let l) = lhs, case .complexParams(let r) = rhs else { preconditionFailure() } + return l == r + }() + default: return false + } + } + #endif + } + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Testing_ByteBufferParams: @unchecked Sendable {} +extension Grpc_Testing_SimpleProtoParams: @unchecked Sendable {} +extension Grpc_Testing_ComplexProtoParams: @unchecked Sendable {} +extension Grpc_Testing_PayloadConfig: @unchecked Sendable {} +extension Grpc_Testing_PayloadConfig.OneOf_Payload: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.testing" + +extension Grpc_Testing_ByteBufferParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ByteBufferParams" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "req_size"), + 2: .standard(proto: "resp_size"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.reqSize) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.respSize) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.reqSize != 0 { + try visitor.visitSingularInt32Field(value: self.reqSize, fieldNumber: 1) + } + if self.respSize != 0 { + try visitor.visitSingularInt32Field(value: self.respSize, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ByteBufferParams, rhs: Grpc_Testing_ByteBufferParams) -> Bool { + if lhs.reqSize != rhs.reqSize {return false} + if lhs.respSize != rhs.respSize {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_SimpleProtoParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SimpleProtoParams" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "req_size"), + 2: .standard(proto: "resp_size"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.reqSize) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.respSize) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.reqSize != 0 { + try visitor.visitSingularInt32Field(value: self.reqSize, fieldNumber: 1) + } + if self.respSize != 0 { + try visitor.visitSingularInt32Field(value: self.respSize, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_SimpleProtoParams, rhs: Grpc_Testing_SimpleProtoParams) -> Bool { + if lhs.reqSize != rhs.reqSize {return false} + if lhs.respSize != rhs.respSize {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ComplexProtoParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ComplexProtoParams" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ComplexProtoParams, rhs: Grpc_Testing_ComplexProtoParams) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_PayloadConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PayloadConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "bytebuf_params"), + 2: .standard(proto: "simple_params"), + 3: .standard(proto: "complex_params"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { + var v: Grpc_Testing_ByteBufferParams? + var hadOneofValue = false + if let current = self.payload { + hadOneofValue = true + if case .bytebufParams(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payload = .bytebufParams(v) + } + }() + case 2: try { + var v: Grpc_Testing_SimpleProtoParams? + var hadOneofValue = false + if let current = self.payload { + hadOneofValue = true + if case .simpleParams(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payload = .simpleParams(v) + } + }() + case 3: try { + var v: Grpc_Testing_ComplexProtoParams? + var hadOneofValue = false + if let current = self.payload { + hadOneofValue = true + if case .complexParams(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payload = .complexParams(v) + } + }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + switch self.payload { + case .bytebufParams?: try { + guard case .bytebufParams(let v)? = self.payload else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .simpleParams?: try { + guard case .simpleParams(let v)? = self.payload else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .complexParams?: try { + guard case .complexParams(let v)? = self.payload else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_PayloadConfig, rhs: Grpc_Testing_PayloadConfig) -> Bool { + if lhs.payload != rhs.payload {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift new file mode 100644 index 000000000..7e61ab4fa --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift @@ -0,0 +1,470 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/stats.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 gRPC authors. +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct Grpc_Testing_ServerStats { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// wall clock time change in seconds since last reset + var timeElapsed: Double = 0 + + /// change in user time (in seconds) used by the server since last reset + var timeUser: Double = 0 + + /// change in server time (in seconds) used by the server process and all + /// threads since last reset + var timeSystem: Double = 0 + + /// change in total cpu time of the server (data from proc/stat) + var totalCpuTime: UInt64 = 0 + + /// change in idle time of the server (data from proc/stat) + var idleCpuTime: UInt64 = 0 + + /// Number of polls called inside completion queue + var cqPollCount: UInt64 = 0 + + /// Core library stats + var coreStats: Grpc_Core_Stats { + get {return _coreStats ?? Grpc_Core_Stats()} + set {_coreStats = newValue} + } + /// Returns true if `coreStats` has been explicitly set. + var hasCoreStats: Bool {return self._coreStats != nil} + /// Clears the value of `coreStats`. Subsequent reads from it will return its default value. + mutating func clearCoreStats() {self._coreStats = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _coreStats: Grpc_Core_Stats? = nil +} + +/// Histogram params based on grpc/support/histogram.c +struct Grpc_Testing_HistogramParams { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// first bucket is [0, 1 + resolution) + var resolution: Double = 0 + + /// use enough buckets to allow this value + var maxPossible: Double = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Histogram data based on grpc/support/histogram.c +struct Grpc_Testing_HistogramData { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var bucket: [UInt32] = [] + + var minSeen: Double = 0 + + var maxSeen: Double = 0 + + var sum: Double = 0 + + var sumOfSquares: Double = 0 + + var count: Double = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_RequestResultCount { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var statusCode: Int32 = 0 + + var count: Int64 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Testing_ClientStats { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Latency histogram. Data points are in nanoseconds. + var latencies: Grpc_Testing_HistogramData { + get {return _latencies ?? Grpc_Testing_HistogramData()} + set {_latencies = newValue} + } + /// Returns true if `latencies` has been explicitly set. + var hasLatencies: Bool {return self._latencies != nil} + /// Clears the value of `latencies`. Subsequent reads from it will return its default value. + mutating func clearLatencies() {self._latencies = nil} + + /// See ServerStats for details. + var timeElapsed: Double = 0 + + var timeUser: Double = 0 + + var timeSystem: Double = 0 + + /// Number of failed requests (one row per status code seen) + var requestResults: [Grpc_Testing_RequestResultCount] = [] + + /// Number of polls called inside completion queue + var cqPollCount: UInt64 = 0 + + /// Core library stats + var coreStats: Grpc_Core_Stats { + get {return _coreStats ?? Grpc_Core_Stats()} + set {_coreStats = newValue} + } + /// Returns true if `coreStats` has been explicitly set. + var hasCoreStats: Bool {return self._coreStats != nil} + /// Clears the value of `coreStats`. Subsequent reads from it will return its default value. + mutating func clearCoreStats() {self._coreStats = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _latencies: Grpc_Testing_HistogramData? = nil + fileprivate var _coreStats: Grpc_Core_Stats? = nil +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Testing_ServerStats: @unchecked Sendable {} +extension Grpc_Testing_HistogramParams: @unchecked Sendable {} +extension Grpc_Testing_HistogramData: @unchecked Sendable {} +extension Grpc_Testing_RequestResultCount: @unchecked Sendable {} +extension Grpc_Testing_ClientStats: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.testing" + +extension Grpc_Testing_ServerStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServerStats" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "time_elapsed"), + 2: .standard(proto: "time_user"), + 3: .standard(proto: "time_system"), + 4: .standard(proto: "total_cpu_time"), + 5: .standard(proto: "idle_cpu_time"), + 6: .standard(proto: "cq_poll_count"), + 7: .standard(proto: "core_stats"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularDoubleField(value: &self.timeElapsed) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &self.timeUser) }() + case 3: try { try decoder.decodeSingularDoubleField(value: &self.timeSystem) }() + case 4: try { try decoder.decodeSingularUInt64Field(value: &self.totalCpuTime) }() + case 5: try { try decoder.decodeSingularUInt64Field(value: &self.idleCpuTime) }() + case 6: try { try decoder.decodeSingularUInt64Field(value: &self.cqPollCount) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._coreStats) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.timeElapsed != 0 { + try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 1) + } + if self.timeUser != 0 { + try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 2) + } + if self.timeSystem != 0 { + try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 3) + } + if self.totalCpuTime != 0 { + try visitor.visitSingularUInt64Field(value: self.totalCpuTime, fieldNumber: 4) + } + if self.idleCpuTime != 0 { + try visitor.visitSingularUInt64Field(value: self.idleCpuTime, fieldNumber: 5) + } + if self.cqPollCount != 0 { + try visitor.visitSingularUInt64Field(value: self.cqPollCount, fieldNumber: 6) + } + try { if let v = self._coreStats { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ServerStats, rhs: Grpc_Testing_ServerStats) -> Bool { + if lhs.timeElapsed != rhs.timeElapsed {return false} + if lhs.timeUser != rhs.timeUser {return false} + if lhs.timeSystem != rhs.timeSystem {return false} + if lhs.totalCpuTime != rhs.totalCpuTime {return false} + if lhs.idleCpuTime != rhs.idleCpuTime {return false} + if lhs.cqPollCount != rhs.cqPollCount {return false} + if lhs._coreStats != rhs._coreStats {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_HistogramParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HistogramParams" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "resolution"), + 2: .standard(proto: "max_possible"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularDoubleField(value: &self.resolution) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &self.maxPossible) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.resolution != 0 { + try visitor.visitSingularDoubleField(value: self.resolution, fieldNumber: 1) + } + if self.maxPossible != 0 { + try visitor.visitSingularDoubleField(value: self.maxPossible, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_HistogramParams, rhs: Grpc_Testing_HistogramParams) -> Bool { + if lhs.resolution != rhs.resolution {return false} + if lhs.maxPossible != rhs.maxPossible {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_HistogramData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HistogramData" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "bucket"), + 2: .standard(proto: "min_seen"), + 3: .standard(proto: "max_seen"), + 4: .same(proto: "sum"), + 5: .standard(proto: "sum_of_squares"), + 6: .same(proto: "count"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedUInt32Field(value: &self.bucket) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &self.minSeen) }() + case 3: try { try decoder.decodeSingularDoubleField(value: &self.maxSeen) }() + case 4: try { try decoder.decodeSingularDoubleField(value: &self.sum) }() + case 5: try { try decoder.decodeSingularDoubleField(value: &self.sumOfSquares) }() + case 6: try { try decoder.decodeSingularDoubleField(value: &self.count) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.bucket.isEmpty { + try visitor.visitPackedUInt32Field(value: self.bucket, fieldNumber: 1) + } + if self.minSeen != 0 { + try visitor.visitSingularDoubleField(value: self.minSeen, fieldNumber: 2) + } + if self.maxSeen != 0 { + try visitor.visitSingularDoubleField(value: self.maxSeen, fieldNumber: 3) + } + if self.sum != 0 { + try visitor.visitSingularDoubleField(value: self.sum, fieldNumber: 4) + } + if self.sumOfSquares != 0 { + try visitor.visitSingularDoubleField(value: self.sumOfSquares, fieldNumber: 5) + } + if self.count != 0 { + try visitor.visitSingularDoubleField(value: self.count, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_HistogramData, rhs: Grpc_Testing_HistogramData) -> Bool { + if lhs.bucket != rhs.bucket {return false} + if lhs.minSeen != rhs.minSeen {return false} + if lhs.maxSeen != rhs.maxSeen {return false} + if lhs.sum != rhs.sum {return false} + if lhs.sumOfSquares != rhs.sumOfSquares {return false} + if lhs.count != rhs.count {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_RequestResultCount: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RequestResultCount" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "status_code"), + 2: .same(proto: "count"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.statusCode) }() + case 2: try { try decoder.decodeSingularInt64Field(value: &self.count) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.statusCode != 0 { + try visitor.visitSingularInt32Field(value: self.statusCode, fieldNumber: 1) + } + if self.count != 0 { + try visitor.visitSingularInt64Field(value: self.count, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_RequestResultCount, rhs: Grpc_Testing_RequestResultCount) -> Bool { + if lhs.statusCode != rhs.statusCode {return false} + if lhs.count != rhs.count {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Testing_ClientStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ClientStats" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "latencies"), + 2: .standard(proto: "time_elapsed"), + 3: .standard(proto: "time_user"), + 4: .standard(proto: "time_system"), + 5: .standard(proto: "request_results"), + 6: .standard(proto: "cq_poll_count"), + 7: .standard(proto: "core_stats"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._latencies) }() + case 2: try { try decoder.decodeSingularDoubleField(value: &self.timeElapsed) }() + case 3: try { try decoder.decodeSingularDoubleField(value: &self.timeUser) }() + case 4: try { try decoder.decodeSingularDoubleField(value: &self.timeSystem) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.requestResults) }() + case 6: try { try decoder.decodeSingularUInt64Field(value: &self.cqPollCount) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._coreStats) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._latencies { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if self.timeElapsed != 0 { + try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 2) + } + if self.timeUser != 0 { + try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 3) + } + if self.timeSystem != 0 { + try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 4) + } + if !self.requestResults.isEmpty { + try visitor.visitRepeatedMessageField(value: self.requestResults, fieldNumber: 5) + } + if self.cqPollCount != 0 { + try visitor.visitSingularUInt64Field(value: self.cqPollCount, fieldNumber: 6) + } + try { if let v = self._coreStats { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Testing_ClientStats, rhs: Grpc_Testing_ClientStats) -> Bool { + if lhs._latencies != rhs._latencies {return false} + if lhs.timeElapsed != rhs.timeElapsed {return false} + if lhs.timeUser != rhs.timeUser {return false} + if lhs.timeSystem != rhs.timeSystem {return false} + if lhs.requestResults != rhs.requestResults {return false} + if lhs.cqPollCount != rhs.cqPollCount {return false} + if lhs._coreStats != rhs._coreStats {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift new file mode 100644 index 000000000..0796543c2 --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -0,0 +1,179 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +/// An integration test service that covers all the method signature permutations +/// of unary/streaming requests/responses. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/worker_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +internal enum Grpc_Testing_WorkerService { + internal enum Method { + internal enum RunServer { + internal typealias Input = Grpc_Testing_ServerArgs + internal typealias Output = Grpc_Testing_ServerStatus + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.WorkerService", + method: "RunServer" + ) + } + internal enum RunClient { + internal typealias Input = Grpc_Testing_ClientArgs + internal typealias Output = Grpc_Testing_ClientStatus + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.WorkerService", + method: "RunClient" + ) + } + internal enum CoreCount { + internal typealias Input = Grpc_Testing_CoreRequest + internal typealias Output = Grpc_Testing_CoreResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.WorkerService", + method: "CoreCount" + ) + } + internal enum QuitWorker { + internal typealias Input = Grpc_Testing_Void + internal typealias Output = Grpc_Testing_Void + internal static let descriptor = MethodDescriptor( + service: "grpc.testing.WorkerService", + method: "QuitWorker" + ) + } + internal static let descriptors: [MethodDescriptor] = [ + RunServer.descriptor, + RunClient.descriptor, + CoreCount.descriptor, + QuitWorker.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias StreamingServiceProtocol = Grpc_Testing_WorkerServiceStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ServiceProtocol = Grpc_Testing_WorkerServiceServiceProtocol +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Start server with specified workload. + /// First request sent specifies the ServerConfig followed by ServerStatus + /// response. After that, a "Mark" can be sent anytime to request the latest + /// stats. Closing the stream will initiate shutdown of the test server + /// and once the shutdown has finished, the OK status is sent to terminate + /// this RPC. + func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Start client with specified workload. + /// First request sent specifies the ClientConfig followed by ClientStatus + /// response. After that, a "Mark" can be sent anytime to request the latest + /// stats. Closing the stream will initiate shutdown of the test client + /// and once the shutdown has finished, the OK status is sent to terminate + /// this RPC. + func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Just return the core count - unary call + func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Quit this worker + func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_WorkerService.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Grpc_Testing_WorkerService.Method.RunServer.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.runServer(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_WorkerService.Method.RunClient.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.runClient(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_WorkerService.Method.CoreCount.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.coreCount(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Testing_WorkerService.Method.QuitWorker.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.quitWorker(request: request) + } + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_WorkerService.StreamingServiceProtocol { + /// Start server with specified workload. + /// First request sent specifies the ServerConfig followed by ServerStatus + /// response. After that, a "Mark" can be sent anytime to request the latest + /// stats. Closing the stream will initiate shutdown of the test server + /// and once the shutdown has finished, the OK status is sent to terminate + /// this RPC. + func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Start client with specified workload. + /// First request sent specifies the ClientConfig followed by ClientStatus + /// response. After that, a "Mark" can be sent anytime to request the latest + /// stats. Closing the stream will initiate shutdown of the test client + /// and once the shutdown has finished, the OK status is sent to terminate + /// this RPC. + func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Just return the core count - unary call + func coreCount(request: ServerRequest.Single) async throws -> ServerResponse.Single + + /// Quit this worker + func quitWorker(request: ServerRequest.Single) async throws -> ServerResponse.Single +} + +/// Partial conformance to `Grpc_Testing_WorkerServiceStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Testing_WorkerService.ServiceProtocol { + internal func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.coreCount(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + internal func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.quitWorker(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } +} \ No newline at end of file diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift new file mode 100644 index 000000000..6cacd66fa --- /dev/null +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift @@ -0,0 +1,38 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: grpc/testing/worker_service.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 gRPC authors. +// +// 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. + +/// An integration test service that covers all the method signature permutations +/// of unary/streaming requests/responses. + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} diff --git a/Sources/performance-worker/main.swift b/Sources/performance-worker/main.swift new file mode 100644 index 000000000..0dddaca9f --- /dev/null +++ b/Sources/performance-worker/main.swift @@ -0,0 +1,18 @@ +/* + * 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. + */ + +func main(args: [String]) throws { +} From f2f13efd745756724c1d6c4a6bb45221b77f3bb9 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 15 Mar 2024 10:19:01 +0000 Subject: [PATCH 265/580] Add GRPCStreamStateMachine (#1787) --- Sources/GRPCCore/Internal/Base64.swift | 167 +- Sources/GRPCCore/Metadata.swift | 11 + .../Compression/CompressionAlgorithm.swift | 52 + .../GRPCStreamStateMachine.swift | 1404 +++++++++++ .../GRPCHTTP2Core/Internal/ContentType.swift | 40 + .../GRPCStatusMessageMarshaller.swift | 205 ++ .../GRPCStreamStateMachineTests.swift | 2194 +++++++++++++++++ .../GRPCStatusMessageMarshallerTests.swift | 44 + 8 files changed, 4097 insertions(+), 20 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift create mode 100644 Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/ContentType.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift create mode 100644 Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift diff --git a/Sources/GRPCCore/Internal/Base64.swift b/Sources/GRPCCore/Internal/Base64.swift index 6265b996b..f2078331f 100644 --- a/Sources/GRPCCore/Internal/Base64.swift +++ b/Sources/GRPCCore/Internal/Base64.swift @@ -26,11 +26,11 @@ met: 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. + notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED @@ -70,10 +70,9 @@ SOFTWARE. */ -internal enum Base64 {} - -extension Base64 { - internal struct DecodingOptions: OptionSet { +// swift-format-ignore: DontRepeatTypeInStaticProperties +enum Base64 { + struct DecodingOptions: OptionSet { internal let rawValue: UInt internal init(rawValue: UInt) { self.rawValue = rawValue } @@ -81,14 +80,49 @@ extension Base64 { internal static let omitPaddingCharacter = DecodingOptions(rawValue: UInt(1 << 1)) } - internal enum DecodingError: Error, Equatable { + enum DecodingError: Error, Equatable { case invalidLength case invalidCharacter(UInt8) case unexpectedPaddingCharacter case unexpectedEnd } - internal static func decode( + static func encode(bytes: Buffer) -> String where Buffer.Element == UInt8 { + guard !bytes.isEmpty else { + return "" + } + + // In Base64, 3 bytes become 4 output characters, and we pad to the + // nearest multiple of four. + let base64StringLength = ((bytes.count + 2) / 3) * 4 + let alphabet = Base64.encodeBase64 + + return String(customUnsafeUninitializedCapacity: base64StringLength) { backingStorage in + var input = bytes.makeIterator() + var offset = 0 + while let firstByte = input.next() { + let secondByte = input.next() + let thirdByte = input.next() + + backingStorage[offset] = Base64.encode(alphabet: alphabet, firstByte: firstByte) + backingStorage[offset + 1] = Base64.encode( + alphabet: alphabet, + firstByte: firstByte, + secondByte: secondByte + ) + backingStorage[offset + 2] = Base64.encode( + alphabet: alphabet, + secondByte: secondByte, + thirdByte: thirdByte + ) + backingStorage[offset + 3] = Base64.encode(alphabet: alphabet, thirdByte: thirdByte) + offset += 4 + } + return offset + } + } + + static func decode( string encoded: String, options: DecodingOptions = [] ) throws -> [UInt8] { @@ -204,7 +238,7 @@ extension Base64 { } } - static func withUnsafeDecodingTablesAsBufferPointers( + private static func withUnsafeDecodingTablesAsBufferPointers( options: Base64.DecodingOptions, _ body: ( UnsafeBufferPointer, UnsafeBufferPointer, UnsafeBufferPointer, @@ -232,10 +266,64 @@ extension Base64 { } } - internal static let encodePaddingCharacter: UInt8 = 61 - static let badCharacter: UInt32 = 0x01FF_FFFF + private static let encodePaddingCharacter: UInt8 = 61 + + private static let encodeBase64: [UInt8] = [ + UInt8(ascii: "A"), UInt8(ascii: "B"), UInt8(ascii: "C"), UInt8(ascii: "D"), + UInt8(ascii: "E"), UInt8(ascii: "F"), UInt8(ascii: "G"), UInt8(ascii: "H"), + UInt8(ascii: "I"), UInt8(ascii: "J"), UInt8(ascii: "K"), UInt8(ascii: "L"), + UInt8(ascii: "M"), UInt8(ascii: "N"), UInt8(ascii: "O"), UInt8(ascii: "P"), + UInt8(ascii: "Q"), UInt8(ascii: "R"), UInt8(ascii: "S"), UInt8(ascii: "T"), + UInt8(ascii: "U"), UInt8(ascii: "V"), UInt8(ascii: "W"), UInt8(ascii: "X"), + UInt8(ascii: "Y"), UInt8(ascii: "Z"), UInt8(ascii: "a"), UInt8(ascii: "b"), + UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f"), + UInt8(ascii: "g"), UInt8(ascii: "h"), UInt8(ascii: "i"), UInt8(ascii: "j"), + UInt8(ascii: "k"), UInt8(ascii: "l"), UInt8(ascii: "m"), UInt8(ascii: "n"), + UInt8(ascii: "o"), UInt8(ascii: "p"), UInt8(ascii: "q"), UInt8(ascii: "r"), + UInt8(ascii: "s"), UInt8(ascii: "t"), UInt8(ascii: "u"), UInt8(ascii: "v"), + UInt8(ascii: "w"), UInt8(ascii: "x"), UInt8(ascii: "y"), UInt8(ascii: "z"), + UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"), + UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"), + UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "+"), UInt8(ascii: "/"), + ] + + private static func encode(alphabet: [UInt8], firstByte: UInt8) -> UInt8 { + let index = firstByte >> 2 + return alphabet[Int(index)] + } + + private static func encode(alphabet: [UInt8], firstByte: UInt8, secondByte: UInt8?) -> UInt8 { + var index = (firstByte & 0b00000011) << 4 + if let secondByte = secondByte { + index += (secondByte & 0b11110000) >> 4 + } + return alphabet[Int(index)] + } - static let decoding0: [UInt32] = [ + private static func encode(alphabet: [UInt8], secondByte: UInt8?, thirdByte: UInt8?) -> UInt8 { + guard let secondByte = secondByte else { + // No second byte means we are just emitting padding. + return Base64.encodePaddingCharacter + } + var index = (secondByte & 0b00001111) << 2 + if let thirdByte = thirdByte { + index += (thirdByte & 0b11000000) >> 6 + } + return alphabet[Int(index)] + } + + private static func encode(alphabet: [UInt8], thirdByte: UInt8?) -> UInt8 { + guard let thirdByte = thirdByte else { + // No third byte means just padding. + return Base64.encodePaddingCharacter + } + let index = thirdByte & 0b00111111 + return alphabet[Int(index)] + } + + private static let badCharacter: UInt32 = 0x01FF_FFFF + + private static let decoding0: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -281,7 +369,7 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - static let decoding1: [UInt32] = [ + private static let decoding1: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -327,7 +415,7 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - static let decoding2: [UInt32] = [ + private static let decoding2: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -373,7 +461,7 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - static let decoding3: [UInt32] = [ + private static let decoding3: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, @@ -419,7 +507,7 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - static let decoding0url: [UInt32] = [ + private static let decoding0url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -465,7 +553,7 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - static let decoding1url: [UInt32] = [ + private static let decoding1url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -511,7 +599,7 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - static let decoding2url: [UInt32] = [ + private static let decoding2url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -557,7 +645,7 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] - static let decoding3url: [UInt32] = [ + private static let decoding3url: [UInt32] = [ 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 0 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 6 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, // 12 @@ -603,3 +691,42 @@ extension Base64 { 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, 0x01FF_FFFF, ] } + +extension String { + /// This is a backport of a proposed String initializer that will allow writing directly into an uninitialized String's backing memory. + /// + /// As this API does not exist prior to 5.3 on Linux, or on older Apple platforms, we fake it out with a pointer and accept the extra copy. + init( + backportUnsafeUninitializedCapacity capacity: Int, + initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int + ) rethrows { + + // The buffer will store zero terminated C string + let buffer = UnsafeMutableBufferPointer.allocate(capacity: capacity + 1) + defer { + buffer.deallocate() + } + + let initializedCount = try initializer(buffer) + precondition(initializedCount <= capacity, "Overran buffer in initializer!") + + // add zero termination + buffer[initializedCount] = 0 + + self = String(cString: buffer.baseAddress!) + } + + init( + customUnsafeUninitializedCapacity capacity: Int, + initializingUTF8With initializer: (_ buffer: UnsafeMutableBufferPointer) throws -> Int + ) rethrows { + if #available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) { + try self.init(unsafeUninitializedCapacity: capacity, initializingUTF8With: initializer) + } else { + try self.init( + backportUnsafeUninitializedCapacity: capacity, + initializingUTF8With: initializer + ) + } + } +} diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 0b2e560f8..217c8a784 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -85,6 +85,17 @@ public struct Metadata: Sendable, Hashable { public enum Value: Sendable, Hashable { case string(String) case binary([UInt8]) + + /// The value as a String. If it was originally stored as a binary, the base64-encoded String version + /// of the binary data will be returned instead. + public func encoded() -> String { + switch self { + case .string(let string): + return string + case .binary(let bytes): + return Base64.encode(bytes: bytes) + } + } } /// A metadata key-value pair. diff --git a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift new file mode 100644 index 000000000..82de83eed --- /dev/null +++ b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift @@ -0,0 +1,52 @@ +/* + * 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. + */ + +/// 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) + + // 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 + } + + internal enum Algorithm: String { + case identity + case deflate + case gzip + } + + internal let algorithm: Algorithm + + private init(_ algorithm: Algorithm) { + self.algorithm = algorithm + } + + internal init?(rawValue: String) { + guard let algorithm = Algorithm(rawValue: rawValue) else { + return nil + } + self.algorithm = algorithm + } +} diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift new file mode 100644 index 000000000..0cd0ca797 --- /dev/null +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -0,0 +1,1404 @@ +/* + * 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 NIOCore +import NIOHPACK +import NIOHTTP1 + +enum Scheme: String { + case http + case https +} + +enum GRPCStreamStateMachineConfiguration { + case client(ClientConfiguration) + case server(ServerConfiguration) + + struct ClientConfiguration { + var methodDescriptor: MethodDescriptor + var scheme: Scheme + var outboundEncoding: CompressionAlgorithm + var acceptedEncodings: [CompressionAlgorithm] + } + + struct ServerConfiguration { + var scheme: Scheme + var acceptedEncodings: [CompressionAlgorithm] + } +} + +private enum GRPCStreamStateMachineState { + case clientIdleServerIdle(ClientIdleServerIdleState) + case clientOpenServerIdle(ClientOpenServerIdleState) + case clientOpenServerOpen(ClientOpenServerOpenState) + case clientOpenServerClosed(ClientOpenServerClosedState) + case clientClosedServerIdle(ClientClosedServerIdleState) + case clientClosedServerOpen(ClientClosedServerOpenState) + case clientClosedServerClosed(ClientClosedServerClosedState) + + struct ClientIdleServerIdleState { + let maximumPayloadSize: Int + } + + struct ClientOpenServerIdleState { + let maximumPayloadSize: Int + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm? + + // The deframer must be optional because the client will not have one configured + // until the server opens and sends a grpc-encoding header. + // It will be present for the server though, because even though it's idle, + // it can still receive compressed messages from the client. + let deframer: NIOSingleStepByteToMessageProcessor? + var decompressor: Zlib.Decompressor? + + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + + init( + previousState: ClientIdleServerIdleState, + compressor: Zlib.Compressor?, + framer: GRPCMessageFramer, + decompressor: Zlib.Decompressor?, + deframer: NIOSingleStepByteToMessageProcessor? + ) { + self.maximumPayloadSize = previousState.maximumPayloadSize + self.compressor = compressor + self.framer = framer + self.decompressor = decompressor + self.deframer = deframer + self.inboundMessageBuffer = .init() + } + } + + struct ClientOpenServerOpenState { + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + let deframer: NIOSingleStepByteToMessageProcessor + var decompressor: Zlib.Decompressor? + + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + + init( + previousState: ClientOpenServerIdleState, + deframer: NIOSingleStepByteToMessageProcessor, + decompressor: Zlib.Decompressor? + ) { + self.framer = previousState.framer + self.compressor = previousState.compressor + + self.deframer = deframer + self.decompressor = decompressor + + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + } + + struct ClientOpenServerClosedState { + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + let deframer: NIOSingleStepByteToMessageProcessor? + var decompressor: Zlib.Decompressor? + + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + + init(previousState: ClientOpenServerOpenState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + init(previousState: ClientOpenServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + // The server went directly from idle to closed - this means it sent a + // trailers-only response: + // - if we're the client, the previous state was a nil deframer, but that + // is okay because we don't need a deframer as the server won't be sending + // any messages; + // - if we're the server, we'll keep whatever deframer we had. + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + } + } + + struct ClientClosedServerIdleState { + let maximumPayloadSize: Int + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm? + + let deframer: NIOSingleStepByteToMessageProcessor? + var decompressor: Zlib.Decompressor? + + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + + /// We are closing the client as soon as it opens (i.e., endStream was set when receiving the client's + /// initial metadata). We don't need to know a decompression algorithm, since we won't receive + /// any more messages from the client anyways, as it's closed. + init( + previousState: ClientIdleServerIdleState, + compressionAlgorithm: CompressionAlgorithm + ) { + self.maximumPayloadSize = previousState.maximumPayloadSize + + if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { + self.compressor = Zlib.Compressor(method: zlibMethod) + } + self.framer = GRPCMessageFramer() + self.outboundCompression = compressionAlgorithm + // We don't need a deframer since we won't receive any messages from the + // client: it's closed. + self.deframer = nil + self.inboundMessageBuffer = .init() + } + + init(previousState: ClientOpenServerIdleState) { + self.maximumPayloadSize = previousState.maximumPayloadSize + self.framer = previousState.framer + self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + } + + struct ClientClosedServerOpenState { + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + let deframer: NIOSingleStepByteToMessageProcessor? + var decompressor: Zlib.Decompressor? + + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + + init(previousState: ClientOpenServerOpenState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.deframer = previousState.deframer + self.decompressor = previousState.decompressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + /// This should be called from the server path, as the deframer will already be configured in this scenario. + init(previousState: ClientClosedServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + + // In the case of the server, we don't need to deframe/decompress any more + // messages, since the client's closed. + self.deframer = nil + self.decompressor = nil + + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + /// This should only be called from the client path, as the deframer has not yet been set up. + init( + previousState: ClientClosedServerIdleState, + decompressionAlgorithm: CompressionAlgorithm + ) { + self.framer = previousState.framer + self.compressor = previousState.compressor + + // In the case of the client, it will only be able to set up the deframer + // after it receives the chosen encoding from the server. + if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { + self.decompressor = Zlib.Decompressor(method: zlibMethod) + } + let decoder = GRPCMessageDeframer( + maximumPayloadSize: previousState.maximumPayloadSize, + decompressor: self.decompressor + ) + self.deframer = NIOSingleStepByteToMessageProcessor(decoder) + + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + } + + struct ClientClosedServerClosedState { + // We still need the framer and compressor in case the server has closed + // but its buffer is not yet empty and still needs to send messages out to + // the client. + var framer: GRPCMessageFramer + var compressor: Zlib.Compressor? + + // These are already deframed, so we don't need the deframer anymore. + var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + + init(previousState: ClientClosedServerOpenState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + init(previousState: ClientClosedServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + init(previousState: ClientOpenServerIdleState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + + init(previousState: ClientOpenServerClosedState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct GRPCStreamStateMachine { + private var state: GRPCStreamStateMachineState + private var configuration: GRPCStreamStateMachineConfiguration + private var skipAssertions: Bool + + init( + configuration: GRPCStreamStateMachineConfiguration, + maximumPayloadSize: Int, + skipAssertions: Bool = false + ) { + self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) + self.configuration = configuration + self.skipAssertions = skipAssertions + } + + mutating func send(metadata: Metadata) throws -> HPACKHeaders { + switch self.configuration { + case .client(let clientConfiguration): + return try self.clientSend(metadata: metadata, configuration: clientConfiguration) + case .server(let serverConfiguration): + return try self.serverSend(metadata: metadata, configuration: serverConfiguration) + } + } + + mutating func send(message: [UInt8], endStream: Bool) throws { + switch self.configuration { + case .client: + try self.clientSend(message: message, endStream: endStream) + case .server: + if endStream { + try self.invalidState( + "Can't end response stream by sending a message - send(status:metadata:trailersOnly:) must be called" + ) + } + try self.serverSend(message: message) + } + } + + mutating func send( + status: Status, + metadata: Metadata + ) throws -> HPACKHeaders { + switch self.configuration { + case .client: + try self.invalidState( + "Client cannot send status and trailer." + ) + case .server: + return try self.serverSend( + status: status, + customMetadata: metadata + ) + } + } + + enum OnMetadataReceived: Equatable { + case receivedMetadata(Metadata) + + // Client-specific actions + case receivedStatusAndMetadata(status: Status, metadata: Metadata) + case doNothing + + // Server-specific actions + case rejectRPC(trailers: HPACKHeaders) + } + + mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { + switch self.configuration { + case .client: + return try self.clientReceive(metadata: metadata, endStream: endStream) + case .server(let serverConfiguration): + return try self.serverReceive( + metadata: metadata, + endStream: endStream, + configuration: serverConfiguration + ) + } + } + + mutating func receive(message: ByteBuffer, endStream: Bool) throws { + switch self.configuration { + case .client: + try self.clientReceive(bytes: message, endStream: endStream) + case .server: + try self.serverReceive(bytes: message, endStream: endStream) + } + } + + /// The result of requesting the next outbound message. + enum OnNextOutboundMessage: Equatable { + /// Either the receiving party is closed, so we shouldn't send any more messages; or the sender is done + /// writing messages (i.e. we are now closed). + case noMoreMessages + /// There isn't a message ready to be sent, but we could still receive more, so keep trying. + case awaitMoreMessages + /// A message is ready to be sent. + case sendMessage(ByteBuffer) + } + + mutating func nextOutboundMessage() throws -> OnNextOutboundMessage { + switch self.configuration { + case .client: + return try self.clientNextOutboundMessage() + case .server: + return try self.serverNextOutboundMessage() + } + } + + /// The result of requesting the next inbound message. + enum OnNextInboundMessage: Equatable { + /// The sender is done writing messages and there are no more messages to be received. + case noMoreMessages + /// There isn't a message ready to be sent, but we could still receive more, so keep trying. + case awaitMoreMessages + /// A message has been received. + case receiveMessage([UInt8]) + } + + mutating func nextInboundMessage() -> OnNextInboundMessage { + switch self.configuration { + case .client: + return self.clientNextInboundMessage() + case .server: + return self.serverNextInboundMessage() + } + } + + mutating func tearDown() { + switch self.state { + case .clientIdleServerIdle: + () + case .clientOpenServerIdle(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientOpenServerOpen(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientOpenServerClosed(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientClosedServerIdle(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientClosedServerOpen(let state): + state.compressor?.end() + state.decompressor?.end() + case .clientClosedServerClosed(let state): + state.compressor?.end() + } + } +} + +// - MARK: Client + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCStreamStateMachine { + private func makeClientHeaders( + methodDescriptor: MethodDescriptor, + scheme: Scheme, + outboundEncoding: CompressionAlgorithm?, + acceptedEncodings: [CompressionAlgorithm], + customMetadata: Metadata + ) -> HPACKHeaders { + var headers = HPACKHeaders() + headers.reserveCapacity(7 + customMetadata.count) + + // Add required headers. + // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests + + // The order is important here: reserved HTTP2 headers (those starting with `:`) + // must come before all other headers. + headers.add("POST", forKey: .method) + headers.add(scheme.rawValue, forKey: .scheme) + headers.add(methodDescriptor.fullyQualifiedMethod, forKey: .path) + + // Add required gRPC headers. + headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) + headers.add("trailers", forKey: .te) // Used to detect incompatible proxies + + if let encoding = outboundEncoding, encoding != .identity { + headers.add(encoding.name, forKey: .encoding) + } + + for acceptedEncoding in acceptedEncodings { + headers.add(acceptedEncoding.name, forKey: .acceptEncoding) + } + + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) + } + + return headers + } + + private mutating func clientSend( + metadata: Metadata, + configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration + ) throws -> HPACKHeaders { + // Client sends metadata only when opening the stream. + switch self.state { + case .clientIdleServerIdle(let state): + let outboundEncoding = configuration.outboundEncoding + let compressor = Zlib.Method(encoding: outboundEncoding) + .flatMap { Zlib.Compressor(method: $0) } + self.state = .clientOpenServerIdle( + .init( + previousState: state, + compressor: compressor, + framer: GRPCMessageFramer(), + decompressor: nil, + deframer: nil + ) + ) + return self.makeClientHeaders( + methodDescriptor: configuration.methodDescriptor, + scheme: configuration.scheme, + outboundEncoding: configuration.outboundEncoding, + acceptedEncodings: configuration.acceptedEncodings, + customMetadata: metadata + ) + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: + try self.invalidState( + "Client is already open: shouldn't be sending metadata." + ) + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + try self.invalidState( + "Client is closed: can't send metadata." + ) + } + } + + private mutating func clientSend(message: [UInt8], endStream: Bool) throws { + // Client sends message. + switch self.state { + case .clientIdleServerIdle: + try self.invalidState("Client not yet open.") + case .clientOpenServerIdle(var state): + state.framer.append(message) + if endStream { + self.state = .clientClosedServerIdle(.init(previousState: state)) + } else { + self.state = .clientOpenServerIdle(state) + } + case .clientOpenServerOpen(var state): + state.framer.append(message) + if endStream { + self.state = .clientClosedServerOpen(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) + } + case .clientOpenServerClosed(let state): + // The server has closed, so it makes no sense to send the rest of the request. + // However, do close if endStream is set. + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + try self.invalidState( + "Client is closed, cannot send a message." + ) + } + } + + /// Returns the client's next request to the server. + /// - Returns: The request to be made to the server. + private mutating func clientNextOutboundMessage() throws -> OnNextOutboundMessage { + switch self.state { + case .clientIdleServerIdle: + try self.invalidState("Client is not open yet.") + case .clientOpenServerIdle(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerIdle(state) + return request.map { .sendMessage($0) } ?? .awaitMoreMessages + case .clientOpenServerOpen(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerOpen(state) + return request.map { .sendMessage($0) } ?? .awaitMoreMessages + case .clientClosedServerIdle(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerIdle(state) + if let request { + return .sendMessage(request) + } else { + return .noMoreMessages + } + case .clientClosedServerOpen(var state): + let request = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerOpen(state) + if let request { + return .sendMessage(request) + } else { + return .noMoreMessages + } + case .clientOpenServerClosed, .clientClosedServerClosed: + // No point in sending any more requests if the server is closed. + return .noMoreMessages + } + } + + private enum ServerHeadersValidationResult { + case valid + case invalid(OnMetadataReceived) + } + + private mutating func clientValidateHeadersReceivedFromServer( + _ metadata: HPACKHeaders + ) -> ServerHeadersValidationResult { + var httpStatus: String? { + metadata.firstString(forKey: .status) + } + var grpcStatus: Status.Code? { + metadata.firstString(forKey: .grpcStatus) + .flatMap { Int($0) } + .flatMap { Status.Code(rawValue: $0) } + } + guard httpStatus == "200" || grpcStatus != nil else { + let httpStatusCode = + httpStatus + .flatMap { Int($0) } + .map { HTTPResponseStatus(statusCode: $0) } + + guard let httpStatusCode else { + return .invalid( + .receivedStatusAndMetadata( + status: .init(code: .unknown, message: "HTTP Status Code is missing."), + metadata: Metadata(headers: metadata) + ) + ) + } + + if (100 ... 199).contains(httpStatusCode.code) { + // For 1xx status codes, the entire header should be skipped and a + // subsequent header should be read. + // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + return .invalid(.doNothing) + } + + // Forward the mapped status code. + return .invalid( + .receivedStatusAndMetadata( + status: .init( + code: Status.Code(httpStatusCode: httpStatusCode), + message: "Unexpected non-200 HTTP Status Code." + ), + metadata: Metadata(headers: metadata) + ) + ) + } + + let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) + guard contentTypeHeader.flatMap(ContentType.init) != nil else { + return .invalid( + .receivedStatusAndMetadata( + status: .init( + code: .internalError, + message: "Missing \(GRPCHTTP2Keys.contentType) header" + ), + metadata: Metadata(headers: metadata) + ) + ) + } + + return .valid + } + + private enum ProcessInboundEncodingResult { + case error(OnMetadataReceived) + case success(CompressionAlgorithm) + } + + private func processInboundEncoding(_ metadata: HPACKHeaders) -> ProcessInboundEncodingResult { + let inboundEncoding: CompressionAlgorithm + if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { + guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { + return .error( + .receivedStatusAndMetadata( + status: .init( + code: .internalError, + message: + "The server picked a compression algorithm ('\(serverEncoding)') the client does not know about." + ), + metadata: Metadata(headers: metadata) + ) + ) + } + inboundEncoding = parsedEncoding + } else { + inboundEncoding = .identity + } + return .success(inboundEncoding) + } + + private func validateAndReturnStatusAndMetadata( + _ metadata: HPACKHeaders + ) throws -> OnMetadataReceived { + let rawStatusCode = metadata.firstString(forKey: .grpcStatus) + guard let rawStatusCode, + let intStatusCode = Int(rawStatusCode), + let statusCode = Status.Code(rawValue: intStatusCode) + else { + let message = + "Non-initial metadata must be a trailer containing a valid grpc-status" + + (rawStatusCode.flatMap { "but was \($0)" } ?? "") + throw RPCError(code: .unknown, message: message) + } + + let statusMessage = + metadata.firstString(forKey: .grpcStatusMessage) + .map { GRPCStatusMessageMarshaller.unmarshall($0) } ?? "" + + var convertedMetadata = Metadata(headers: metadata) + convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + + return .receivedStatusAndMetadata( + status: Status(code: statusCode, message: statusMessage), + metadata: convertedMetadata + ) + } + + private mutating func clientReceive( + metadata: HPACKHeaders, + endStream: Bool + ) throws -> OnMetadataReceived { + switch self.state { + case .clientOpenServerIdle(let state): + switch (self.clientValidateHeadersReceivedFromServer(metadata), endStream) { + case (.invalid(let action), true): + // The headers are invalid, but the server signalled that it was + // closing the stream, so close both client and server. + self.state = .clientClosedServerClosed(.init(previousState: state)) + return action + case (.invalid(let action), false): + self.state = .clientClosedServerIdle(.init(previousState: state)) + return action + case (.valid, true): + // This is a trailers-only response: close server. + self.state = .clientOpenServerClosed(.init(previousState: state)) + return try self.validateAndReturnStatusAndMetadata(metadata) + case (.valid, false): + switch self.processInboundEncoding(metadata) { + case .error(let failure): + return failure + case .success(let inboundEncoding): + let decompressor = Zlib.Method(encoding: inboundEncoding) + .flatMap { Zlib.Decompressor(method: $0) } + let deframer = GRPCMessageDeframer( + maximumPayloadSize: state.maximumPayloadSize, + decompressor: decompressor + ) + + self.state = .clientOpenServerOpen( + .init( + previousState: state, + deframer: NIOSingleStepByteToMessageProcessor(deframer), + decompressor: decompressor + ) + ) + return .receivedMetadata(Metadata(headers: metadata)) + } + } + + case .clientOpenServerOpen(let state): + // This state is valid even if endStream is not set: server can send + // trailing metadata without END_STREAM set, and follow it with an + // empty message frame where it is set. + // However, we must make sure that grpc-status is set, otherwise this + // is an invalid state. + if endStream { + self.state = .clientOpenServerClosed(.init(previousState: state)) + } + return try self.validateAndReturnStatusAndMetadata(metadata) + + case .clientClosedServerIdle(let state): + switch (self.clientValidateHeadersReceivedFromServer(metadata), endStream) { + case (.invalid(let action), true): + // The headers are invalid, but the server signalled that it was + // closing the stream, so close the server side too. + self.state = .clientClosedServerClosed(.init(previousState: state)) + return action + case (.invalid(let action), false): + // Client is already closed, so we don't need to update our state. + return action + case (.valid, true): + // This is a trailers-only response: close server. + self.state = .clientClosedServerClosed(.init(previousState: state)) + return try self.validateAndReturnStatusAndMetadata(metadata) + case (.valid, false): + switch self.processInboundEncoding(metadata) { + case .error(let failure): + return failure + case .success(let inboundEncoding): + self.state = .clientClosedServerOpen( + .init( + previousState: state, + decompressionAlgorithm: inboundEncoding + ) + ) + return .receivedMetadata(Metadata(headers: metadata)) + } + } + + case .clientClosedServerOpen(let state): + // This state is valid even if endStream is not set: server can send + // trailing metadata without END_STREAM set, and follow it with an + // empty message frame where it is set. + // However, we must make sure that grpc-status is set, otherwise this + // is an invalid state. + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } + return try self.validateAndReturnStatusAndMetadata(metadata) + + case .clientClosedServerClosed: + // We could end up here if we received a grpc-status header in a previous + // frame (which would have already close the server) and then we receive + // an empty frame with EOS set. + // We wouldn't want to throw in that scenario, so we just ignore it. + // Note that we don't want to ignore it if EOS is not set here though, as + // then it would be an invalid payload. + if !endStream || metadata.count > 0 { + try self.invalidState( + "Server is closed, nothing could have been sent." + ) + } + return .doNothing + case .clientIdleServerIdle: + try self.invalidState( + "Server cannot have sent metadata if the client is idle." + ) + case .clientOpenServerClosed: + try self.invalidState( + "Server is closed, nothing could have been sent." + ) + } + } + + private mutating func clientReceive(bytes: ByteBuffer, endStream: Bool) throws { + // This is a message received by the client, from the server. + switch self.state { + case .clientIdleServerIdle: + try self.invalidState( + "Cannot have received anything from server if client is not yet open." + ) + case .clientOpenServerIdle, .clientClosedServerIdle: + try self.invalidState( + "Server cannot have sent a message before sending the initial metadata." + ) + case .clientOpenServerOpen(var state): + try state.deframer.process(buffer: bytes) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + if endStream { + self.state = .clientOpenServerClosed(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) + } + case .clientClosedServerOpen(var state): + // The client may have sent the end stream and thus it's closed, + // but the server may still be responding. + // The client must have a deframer set up, so force-unwrap is okay. + try state.deframer!.process(buffer: bytes) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } else { + self.state = .clientClosedServerOpen(state) + } + case .clientOpenServerClosed, .clientClosedServerClosed: + try self.invalidState( + "Cannot have received anything from a closed server." + ) + } + } + + private mutating func clientNextInboundMessage() -> OnNextInboundMessage { + switch self.state { + case .clientOpenServerOpen(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerOpen(state) + return message.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientOpenServerClosed(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerClosed(state) + return message.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientClosedServerOpen(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerOpen(state) + return message.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientClosedServerClosed(var state): + let message = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerClosed(state) + return message.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientIdleServerIdle, + .clientOpenServerIdle, + .clientClosedServerIdle: + return .awaitMoreMessages + } + } + + private func invalidState(_ message: String, line: UInt = #line) throws -> Never { + if !self.skipAssertions { + assertionFailure(message, line: line) + } + throw RPCError(code: .internalError, message: message) + } +} + +// - MARK: Server + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCStreamStateMachine { + private func makeResponseHeaders( + outboundEncoding: CompressionAlgorithm?, + configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration, + customMetadata: Metadata + ) -> HPACKHeaders { + // Response headers always contain :status (HTTP Status 200) and content-type. + // They may also contain grpc-encoding, grpc-accept-encoding, and custom metadata. + var headers = HPACKHeaders() + headers.reserveCapacity(4 + customMetadata.count) + + headers.add("200", forKey: .status) + headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) + + if let outboundEncoding, outboundEncoding != .identity { + headers.add(outboundEncoding.name, forKey: .encoding) + } + + for acceptedEncoding in configuration.acceptedEncodings { + headers.add(acceptedEncoding.name, forKey: .acceptEncoding) + } + + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) + } + + return headers + } + + private mutating func serverSend( + metadata: Metadata, + configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration + ) throws -> HPACKHeaders { + // Server sends initial metadata + switch self.state { + case .clientOpenServerIdle(let state): + self.state = .clientOpenServerOpen( + .init( + previousState: state, + // In the case of the server, it will already have a deframer set up, + // because it already knows what encoding the client is using: + // it's okay to force-unwrap. + deframer: state.deframer!, + decompressor: state.decompressor + ) + ) + return self.makeResponseHeaders( + outboundEncoding: state.outboundCompression, + configuration: configuration, + customMetadata: metadata + ) + case .clientClosedServerIdle(let state): + self.state = .clientClosedServerOpen(.init(previousState: state)) + return self.makeResponseHeaders( + outboundEncoding: state.outboundCompression, + configuration: configuration, + customMetadata: metadata + ) + case .clientIdleServerIdle: + try self.invalidState( + "Client cannot be idle if server is sending initial metadata: it must have opened." + ) + case .clientOpenServerClosed, .clientClosedServerClosed: + try self.invalidState( + "Server cannot send metadata if closed." + ) + case .clientOpenServerOpen, .clientClosedServerOpen: + try self.invalidState( + "Server has already sent initial metadata." + ) + } + } + + private mutating func serverSend(message: [UInt8]) throws { + switch self.state { + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + try self.invalidState( + "Server must have sent initial metadata before sending a message." + ) + case .clientOpenServerOpen(var state): + state.framer.append(message) + self.state = .clientOpenServerOpen(state) + case .clientClosedServerOpen(var state): + state.framer.append(message) + self.state = .clientClosedServerOpen(state) + case .clientOpenServerClosed, .clientClosedServerClosed: + try self.invalidState( + "Server can't send a message if it's closed." + ) + } + } + + private func makeTrailers( + status: Status, + customMetadata: Metadata?, + trailersOnly: Bool + ) -> HPACKHeaders { + // Trailers always contain the grpc-status header, and optionally, + // grpc-status-message, and custom metadata. + // If it's a trailers-only response, they will also contain :status and + // content-type. + var headers = HPACKHeaders() + let customMetadataCount = customMetadata?.count ?? 0 + if trailersOnly { + // Reserve 4 for capacity: 3 for the required headers, and 1 for the + // optional status message. + headers.reserveCapacity(4 + customMetadataCount) + headers.add("200", forKey: .status) + headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) + } else { + // Reserve 2 for capacity: one for the required grpc-status, and + // one for the optional message. + headers.reserveCapacity(2 + customMetadataCount) + } + + headers.add(String(status.code.rawValue), forKey: .grpcStatus) + + if !status.message.isEmpty { + if let percentEncodedMessage = GRPCStatusMessageMarshaller.marshall(status.message) { + headers.add(percentEncodedMessage, forKey: .grpcStatusMessage) + } + } + + if let customMetadata { + for metadataPair in customMetadata { + headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) + } + } + + return headers + } + + private mutating func serverSend( + status: Status, + customMetadata: Metadata + ) throws -> HPACKHeaders { + // Close the server. + switch self.state { + case .clientOpenServerOpen(let state): + self.state = .clientOpenServerClosed(.init(previousState: state)) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: false + ) + case .clientClosedServerOpen(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: false + ) + case .clientOpenServerIdle(let state): + self.state = .clientOpenServerClosed(.init(previousState: state)) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: true + ) + case .clientClosedServerIdle(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return self.makeTrailers( + status: status, + customMetadata: customMetadata, + trailersOnly: true + ) + case .clientIdleServerIdle: + try self.invalidState( + "Server can't send status if client is idle." + ) + case .clientOpenServerClosed, .clientClosedServerClosed: + try self.invalidState( + "Server can't send anything if closed." + ) + } + } + + private mutating func serverReceive( + metadata: HPACKHeaders, + endStream: Bool, + configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration + ) throws -> OnMetadataReceived { + switch self.state { + case .clientIdleServerIdle(let state): + let contentType = metadata.firstString(forKey: .contentType) + .flatMap { ContentType(value: $0) } + guard contentType != nil else { + // Respond with HTTP-level Unsupported Media Type status code. + var trailers = HPACKHeaders() + trailers.add("415", forKey: .status) + return .rejectRPC(trailers: trailers) + } + + let path = metadata.firstString(forKey: .path) + .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } + guard path != nil else { + let status = Status( + code: .unimplemented, + message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." + ) + let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) + return .rejectRPC(trailers: trailers) + } + + 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 + let encodingValues = metadata.values( + forHeader: GRPCHTTP2Keys.encoding.rawValue, + canonicalForm: true + ) + var encodingValuesIterator = encodingValues.makeIterator() + if let rawEncoding = encodingValuesIterator.next() { + guard encodingValuesIterator.next() == nil else { + let status = Status( + code: .internalError, + message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + ) + let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) + return .rejectRPC(trailers: trailers) + } + + guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)), + isIdentityOrCompatibleEncoding(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 trailers = self.makeTrailers( + status: Status(code: .unimplemented, message: statusMessage), + customMetadata: customMetadata, + trailersOnly: true + ) + return .rejectRPC(trailers: trailers) + } + + // Server supports client's encoding. + inboundEncoding = clientEncoding + } else { + inboundEncoding = .identity + } + + // 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 + let clientAdvertisedEncodings = metadata.values( + forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue, + canonicalForm: true + ) + // Find the preferred encoding and use it to compress responses. + // 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) + { + outboundEncoding = algorithm + break + } + } + + if endStream { + self.state = .clientClosedServerIdle( + .init( + previousState: state, + compressionAlgorithm: outboundEncoding + ) + ) + } else { + let compressor = Zlib.Method(encoding: outboundEncoding) + .flatMap { Zlib.Compressor(method: $0) } + let decompressor = Zlib.Method(encoding: inboundEncoding) + .flatMap { Zlib.Decompressor(method: $0) } + let deframer = GRPCMessageDeframer( + maximumPayloadSize: state.maximumPayloadSize, + decompressor: decompressor + ) + + self.state = .clientOpenServerIdle( + .init( + previousState: state, + compressor: compressor, + framer: GRPCMessageFramer(), + decompressor: decompressor, + deframer: NIOSingleStepByteToMessageProcessor(deframer) + ) + ) + } + + return .receivedMetadata(Metadata(headers: metadata)) + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: + try self.invalidState( + "Client shouldn't have sent metadata twice." + ) + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + try self.invalidState( + "Client can't have sent metadata if closed." + ) + } + } + + private mutating func serverReceive(bytes: ByteBuffer, endStream: Bool) throws { + switch self.state { + case .clientIdleServerIdle: + try self.invalidState( + "Can't have received a message if client is idle." + ) + case .clientOpenServerIdle(var state): + // Deframer must be present on the server side, as we know the decompression + // algorithm from the moment the client opens. + try state.deframer!.process(buffer: bytes) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + + if endStream { + self.state = .clientClosedServerIdle(.init(previousState: state)) + } else { + self.state = .clientOpenServerIdle(state) + } + case .clientOpenServerOpen(var state): + try state.deframer.process(buffer: bytes) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + + if endStream { + self.state = .clientClosedServerOpen(.init(previousState: state)) + } else { + self.state = .clientOpenServerOpen(state) + } + case .clientOpenServerClosed(let state): + // Client is not done sending request, but server has already closed. + // Ignore the rest of the request: do nothing, unless endStream is set, + // in which case close the client. + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: state)) + } + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + try self.invalidState( + "Client can't send a message if closed." + ) + } + } + + private mutating func serverNextOutboundMessage() throws -> OnNextOutboundMessage { + switch self.state { + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: + try self.invalidState("Server is not open yet.") + case .clientOpenServerOpen(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerOpen(state) + return response.map { .sendMessage($0) } ?? .awaitMoreMessages + case .clientClosedServerOpen(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerOpen(state) + return response.map { .sendMessage($0) } ?? .awaitMoreMessages + case .clientOpenServerClosed(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientOpenServerClosed(state) + if let response { + return .sendMessage(response) + } else { + return .noMoreMessages + } + case .clientClosedServerClosed(var state): + let response = try state.framer.next(compressor: state.compressor) + self.state = .clientClosedServerClosed(state) + if let response { + return .sendMessage(response) + } else { + return .noMoreMessages + } + } + } + + private mutating func serverNextInboundMessage() -> OnNextInboundMessage { + switch self.state { + case .clientOpenServerIdle(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerIdle(state) + return request.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientOpenServerOpen(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerOpen(state) + return request.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientOpenServerClosed(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientOpenServerClosed(state) + return request.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientClosedServerOpen(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerOpen(state) + return request.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientClosedServerClosed(var state): + let request = state.inboundMessageBuffer.pop() + self.state = .clientClosedServerClosed(state) + return request.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientClosedServerIdle: + return .noMoreMessages + case .clientIdleServerIdle: + return .awaitMoreMessages + } + } +} + +extension MethodDescriptor { + init?(fullyQualifiedMethod: String) { + let split = fullyQualifiedMethod.split(separator: "/") + guard split.count == 2 else { + return nil + } + self.init(service: String(split[0]), method: String(split[1])) + } +} + +internal enum GRPCHTTP2Keys: String { + case path = ":path" + case contentType = "content-type" + case encoding = "grpc-encoding" + case acceptEncoding = "grpc-accept-encoding" + case scheme = ":scheme" + case method = ":method" + case te = "te" + case status = ":status" + case grpcStatus = "grpc-status" + case grpcStatusMessage = "grpc-status-message" +} + +extension HPACKHeaders { + internal func firstString(forKey key: GRPCHTTP2Keys) -> String? { + self.values(forHeader: key.rawValue, canonicalForm: true).first(where: { _ in true }).map { + String($0) + } + } + + internal mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { + self.add(name: key.rawValue, value: value) + } +} + +extension Zlib.Method { + init?(encoding: CompressionAlgorithm) { + switch encoding { + case .identity: + return nil + case .deflate: + self = .deflate + case .gzip: + self = .gzip + default: + return nil + } + } +} + +extension Metadata { + init(headers: HPACKHeaders) { + var metadata = Metadata() + metadata.reserveCapacity(headers.count) + for header in headers { + if header.name.hasSuffix("-bin") { + do { + let decodedBinary = try header.value.base64Decoded() + metadata.addBinary(decodedBinary, forKey: header.name) + } catch { + metadata.addString(header.value, forKey: header.name) + } + } else { + metadata.addString(header.value, forKey: header.name) + } + } + self = metadata + } +} + +extension Status.Code { + // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + init(httpStatusCode: HTTPResponseStatus) { + switch httpStatusCode { + case .badRequest: + self = .internalError + case .unauthorized: + self = .unauthenticated + case .forbidden: + self = .permissionDenied + case .notFound: + self = .unimplemented + case .tooManyRequests, .badGateway, .serviceUnavailable, .gatewayTimeout: + self = .unavailable + default: + self = .unknown + } + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/ContentType.swift b/Sources/GRPCHTTP2Core/Internal/ContentType.swift new file mode 100644 index 000000000..2e098d39f --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/ContentType.swift @@ -0,0 +1,40 @@ +/* + * 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. + */ + +// See: +// - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md +enum ContentType { + case grpc + + init?(value: String) { + switch value { + case "application/grpc", + "application/grpc+proto": + self = .grpc + + default: + return nil + } + } + + var canonicalValue: String { + switch self { + case .grpc: + // This is more widely supported than "application/grpc+proto" + return "application/grpc" + } + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift new file mode 100644 index 000000000..4f8b1eb40 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift @@ -0,0 +1,205 @@ +/* + * 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. + */ + +enum GRPCStatusMessageMarshaller { + /// Adds percent encoding to the given message. + /// + /// - Parameter message: Message to percent encode. + /// - Returns: Percent encoded string, or `nil` if it could not be encoded. + static func marshall(_ message: String) -> String? { + return percentEncode(message) + } + + /// Removes percent encoding from the given message. + /// + /// - Parameter message: Message to remove encoding from. + /// - Returns: The string with percent encoding removed, or the input string if the encoding + /// could not be removed. + static func unmarshall(_ message: String) -> String { + return removePercentEncoding(message) + } +} + +extension GRPCStatusMessageMarshaller { + /// Adds percent encoding to the given message. + /// + /// gRPC uses percent encoding as defined in RFC 3986 § 2.1 but with a different set of restricted + /// characters. The allowed characters are all visible printing characters except for (`%`, + /// `0x25`). That is: `0x20`-`0x24`, `0x26`-`0x7E`. + /// + /// - Parameter message: The message to encode. + /// - Returns: Percent encoded string, or `nil` if it could not be encoded. + private static func percentEncode(_ message: String) -> String? { + let utf8 = message.utf8 + + let encodedLength = self.percentEncodedLength(for: utf8) + // Fast-path: all characters are valid, nothing to encode. + if encodedLength == utf8.count { + return message + } + + var bytes: [UInt8] = [] + bytes.reserveCapacity(encodedLength) + + for char in message.utf8 { + switch char { + // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses + case 0x20 ... 0x24, + 0x26 ... 0x7E: + bytes.append(char) + + default: + bytes.append(UInt8(ascii: "%")) + bytes.append(self.toHex(char >> 4)) + bytes.append(self.toHex(char & 0xF)) + } + } + + return String(decoding: bytes, as: UTF8.self) + } + + /// Returns the percent encoded length of the given `UTF8View`. + private static func percentEncodedLength(for view: String.UTF8View) -> Int { + var count = view.count + for byte in view { + switch byte { + case 0x20 ... 0x24, + 0x26 ... 0x7E: + () + + default: + count += 2 + } + } + return count + } + + /// Encode the given byte as hexadecimal. + /// + /// - Precondition: Only the four least significant bits may be set. + /// - Parameter nibble: The nibble to convert to hexadecimal. + private static func toHex(_ nibble: UInt8) -> UInt8 { + assert(nibble & 0xF == nibble) + + switch nibble { + case 0 ... 9: + return nibble &+ UInt8(ascii: "0") + default: + return nibble &+ (UInt8(ascii: "A") &- 10) + } + } + + /// Remove gRPC percent encoding from `message`. If any portion of the string could not be decoded + /// then the encoded message will be returned. + /// + /// - Parameter message: The message to remove percent encoding from. + /// - Returns: The decoded message. + private static func removePercentEncoding(_ message: String) -> String { + let utf8 = message.utf8 + + let decodedLength = self.percentDecodedLength(for: utf8) + // Fast-path: no decoding to do! Note that we may also have detected that the encoding is + // invalid, in which case we will return the encoded message: this is fine. + if decodedLength == utf8.count { + return message + } + + var chars: [UInt8] = [] + // We can't decode more characters than are already encoded. + chars.reserveCapacity(decodedLength) + + var currentIndex = utf8.startIndex + let endIndex = utf8.endIndex + + while currentIndex < endIndex { + let byte = utf8[currentIndex] + + switch byte { + case UInt8(ascii: "%"): + guard let (nextIndex, nextNextIndex) = utf8.nextTwoIndices(after: currentIndex), + let nextHex = fromHex(utf8[nextIndex]), + let nextNextHex = fromHex(utf8[nextNextIndex]) + else { + // If we can't decode the message, aborting and returning the encoded message is fine + // according to the spec. + return message + } + chars.append((nextHex << 4) | nextNextHex) + currentIndex = nextNextIndex + + default: + chars.append(byte) + } + + currentIndex = utf8.index(after: currentIndex) + } + + return String(decoding: chars, as: Unicode.UTF8.self) + } + + /// Returns the expected length of the decoded `UTF8View`. + private static func percentDecodedLength(for view: String.UTF8View) -> Int { + var encoded = 0 + + for byte in view { + switch byte { + case UInt8(ascii: "%"): + // This can't overflow since it can't be larger than view.count. + encoded &+= 1 + + default: + () + } + } + + let notEncoded = view.count - (encoded * 3) + + guard notEncoded >= 0 else { + // We've received gibberish: more '%' than expected. gRPC allows for the status message to + // be left encoded should it be incorrectly encoded. We'll do exactly that by returning + // the number of bytes in the view which will causes us to take the fast-path exit. + return view.count + } + + return notEncoded + encoded + } + + private static func fromHex(_ byte: UInt8) -> UInt8? { + switch byte { + case UInt8(ascii: "0") ... UInt8(ascii: "9"): + return byte &- UInt8(ascii: "0") + case UInt8(ascii: "A") ... UInt8(ascii: "Z"): + return byte &- (UInt8(ascii: "A") &- 10) + case UInt8(ascii: "a") ... UInt8(ascii: "z"): + return byte &- (UInt8(ascii: "a") &- 10) + default: + return nil + } + } +} + +extension String.UTF8View { + /// Return the next two valid indices after the given index. The indices are considered valid if + /// they less than `endIndex`. + fileprivate func nextTwoIndices(after index: Index) -> (Index, Index)? { + let secondIndex = self.index(index, offsetBy: 2) + guard secondIndex < self.endIndex else { + return nil + } + + return (self.index(after: index), secondIndex) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift new file mode 100644 index 000000000..34e615623 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -0,0 +1,2194 @@ +/* + * 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 NIOCore +import NIOHPACK +import XCTest + +@testable import GRPCHTTP2Core + +private enum TargetStateMachineState: CaseIterable { + case clientIdleServerIdle + case clientOpenServerIdle + case clientOpenServerOpen + case clientOpenServerClosed + case clientClosedServerIdle + case clientClosedServerOpen + case clientClosedServerClosed +} + +extension HPACKHeaders { + // Client + fileprivate static let clientInitialMetadata: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + fileprivate static let clientInitialMetadataWithDeflateCompression: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "https", + GRPCHTTP2Keys.te.rawValue: "te", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + GRPCHTTP2Keys.encoding.rawValue: "deflate", + ] + fileprivate static let clientInitialMetadataWithGzipCompression: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "https", + GRPCHTTP2Keys.te.rawValue: "te", + GRPCHTTP2Keys.acceptEncoding.rawValue: "gzip", + GRPCHTTP2Keys.encoding.rawValue: "gzip", + ] + fileprivate static let receivedWithoutContentType: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test" + ] + fileprivate static let receivedWithInvalidContentType: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid", + ] + fileprivate static let receivedWithoutEndpoint: Self = [ + GRPCHTTP2Keys.contentType.rawValue: "application/grpc" + ] + + // Server + fileprivate static let serverInitialMetadata: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + fileprivate static let serverInitialMetadataWithDeflateCompression: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + fileprivate static let serverTrailers: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.grpcStatus.rawValue: "0", + ] +} + +final class GRPCStreamClientStateMachineTests: XCTestCase { + private func makeClientStateMachine( + targetState: TargetStateMachineState, + compressionEnabled: Bool = false + ) -> GRPCStreamStateMachine { + var stateMachine = GRPCStreamStateMachine( + configuration: .client( + .init( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: compressionEnabled ? .deflate : .identity, + acceptedEncodings: [.deflate] + ) + ), + maximumPayloadSize: 100, + skipAssertions: true + ) + + let serverMetadata: HPACKHeaders = + compressionEnabled ? .serverInitialMetadataWithDeflateCompression : .serverInitialMetadata + switch targetState { + case .clientIdleServerIdle: + break + case .clientOpenServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + case .clientOpenServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + case .clientOpenServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + case .clientClosedServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + case .clientClosedServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + case .clientClosedServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + // Open server + XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + } + + return stateMachine + } + + // - MARK: Send Metadata + + func testSendMetadataWhenClientIdleAndServerIdle() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + XCTAssertNoThrow(try stateMachine.send(metadata: [])) + } + + func testSendMetadataWhenClientAlreadyOpen() throws { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { + error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") + } + } + } + + func testSendMetadataWhenClientAlreadyClosed() throws { + for targetState in [ + TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, + .clientClosedServerClosed, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { + error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } + } + + // - MARK: Send Message + + func testSendMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Try to send a message without opening (i.e. without sending initial metadata) + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client not yet open.") + } + } + + func testSendMessageWhenClientOpen() { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Now send a message + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + } + + func testSendMessageWhenClientClosed() { + for targetState in [ + TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, + .clientClosedServerClosed, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Try sending another message: it should fail + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + } + } + } + + // - MARK: Send Status and Trailers + + func testSendStatusAndTrailers() { + for targetState in TargetStateMachineState.allCases { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // This operation is never allowed on the client. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: Status(code: .ok, message: ""), + metadata: .init() + ) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client cannot send status and trailer.") + } + } + } + + // - MARK: Receive initial metadata + + func testReceiveInitialMetadataWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") + } + } + + func testReceiveInvalidInitialMetadataWhenServerIdle() throws { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Receive metadata with unexpected non-200 status code + let action = try stateMachine.receive( + metadata: [GRPCHTTP2Keys.status.rawValue: "300"], + endStream: false + ) + + XCTAssertEqual( + action, + .receivedStatusAndMetadata( + status: .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code."), + metadata: [":status": "300"] + ) + ) + } + } + + func testReceiveInitialMetadataWhenServerIdle() throws { + for targetState in [ + TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Receive metadata = open server + let action = try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) + + var expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") + XCTAssertEqual(action, .receivedMetadata(expectedMetadata)) + } + } + + func testReceiveInitialMetadataWhenServerOpen() throws { + for targetState in [ + TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Receiving initial metadata again should throw if grpc-status is not present. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) + ) { error in + XCTAssertEqual(error.code, .unknown) + XCTAssertEqual( + error.message, + "Non-initial metadata must be a trailer containing a valid grpc-status" + ) + } + + // Now make sure everything works well if we include grpc-status + let action = try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) + + var expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + expectedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") + XCTAssertEqual( + action, + .receivedStatusAndMetadata( + status: Status(code: .ok, message: ""), + metadata: expectedMetadata + ) + ) + } + } + + func testReceiveInitialMetadataWhenServerClosed() { + for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") + } + } + } + + // - MARK: Receive end trailers + + func testReceiveEndTrailerWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Receive an end trailer + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") + } + } + + func testReceiveEndTrailerWhenClientOpenAndServerIdle() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) + + // Receive a trailers-only response + let trailersOnlyResponse: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall( + "Some status message" + )!, + "custom-key": "custom-value", + ] + let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) + switch trailers { + case .receivedStatusAndMetadata(let status, let metadata): + XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) + XCTAssertEqual( + metadata, + [ + ":status": "200", + "content-type": "application/grpc", + "custom-key": "custom-value", + ] + ) + case .receivedMetadata, .doNothing, .rejectRPC: + XCTFail("Expected .receivedStatusAndMetadata") + } + } + + func testReceiveEndTrailerWhenServerOpen() throws { + for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + // Receive an end trailer + let action = try stateMachine.receive( + metadata: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + ], + endStream: true + ) + + let expectedMetadata: Metadata = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + "custom": "123", + ] + XCTAssertEqual( + action, + .receivedStatusAndMetadata( + status: .init(code: .ok, message: ""), + metadata: expectedMetadata + ) + ) + } + } + + func testReceiveEndTrailerWhenClientOpenAndServerClosed() { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) + + // Receive another end trailer + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .init(), endStream: true) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") + } + } + + func testReceiveEndTrailerWhenClientClosedAndServerIdle() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientClosedServerIdle) + + // Server sends a trailers-only response + let trailersOnlyResponse: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall( + "Some status message" + )!, + "custom-key": "custom-value", + ] + let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) + switch trailers { + case .receivedStatusAndMetadata(let status, let metadata): + XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) + XCTAssertEqual( + metadata, + [ + ":status": "200", + "content-type": "application/grpc", + "custom-key": "custom-value", + ] + ) + case .receivedMetadata, .doNothing, .rejectRPC: + XCTFail("Expected .receivedStatusAndMetadata") + } + } + + func testReceiveEndTrailerWhenClientClosedAndServerClosed() { + var stateMachine = self.makeClientStateMachine(targetState: .clientClosedServerClosed) + + // Close server again (endStream = true) and assert we don't throw. + // This can happen if the previous close was caused by a grpc-status header + // and then the server sends an empty frame with EOS set. + XCTAssertEqual(try stateMachine.receive(metadata: .init(), endStream: true), .doNothing) + } + + // - MARK: Receive message + + func testReceiveMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Cannot have received anything from server if client is not yet open." + ) + } + } + + func testReceiveMessageWhenServerIdle() { + for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Server cannot have sent a message before sending the initial metadata." + ) + } + } + } + + func testReceiveMessageWhenServerOpen() throws { + for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + } + } + + func testReceiveMessageWhenServerClosed() { + for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") + } + } + } + + // - MARK: Next outbound message + + func testNextOutboundMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is not open yet.") + } + } + + func testNextOutboundMessageWhenClientOpenAndServerOpenOrIdle() throws { + for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual( + try stateMachine.nextOutboundMessage(), + .sendMessage(ByteBuffer(bytes: expectedBytes)) + ) + + // And then make sure that nothing else is returned anymore + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + } + } + + func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerIdle, + compressionEnabled: true + ) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + + let request = try stateMachine.nextOutboundMessage() + let framedMessage = try self.frameMessage(originalMessage, compress: true) + XCTAssertEqual(request, .sendMessage(framedMessage)) + } + + func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + + let request = try stateMachine.nextOutboundMessage() + let framedMessage = try self.frameMessage(originalMessage, compress: true) + XCTAssertEqual(request, .sendMessage(framedMessage)) + } + + func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) + + // No more messages to send + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + + // Queue a message, but assert the action is .noMoreMessages nevertheless, + // because the server is closed. + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + } + + func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) + + // Send a message and close client + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + + // Make sure that getting the next outbound message _does_ return the message + // we have enqueued. + let request = try stateMachine.nextOutboundMessage() + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(request, .sendMessage(ByteBuffer(bytes: expectedBytes))) + + // And then make sure that nothing else is returned anymore + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + } + + func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) + + // Send a message and close client + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + + // Make sure that getting the next outbound message _does_ return the message + // we have enqueued. + let request = try stateMachine.nextOutboundMessage() + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(request, .sendMessage(ByteBuffer(bytes: expectedBytes))) + + // And then make sure that nothing else is returned anymore + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + } + + func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) + // Send a message + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Even though we have enqueued a message, don't send it, because the server + // is closed. + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + } + + // - MARK: Next inbound message + + func testNextInboundMessageWhenServerIdle() { + for targetState in [ + TargetStateMachineState.clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle, + ] { + var stateMachine = self.makeClientStateMachine(targetState: targetState) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + } + + func testNextInboundMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + let receivedBytes = try self.frameMessage(originalMessage, compress: true) + try stateMachine.receive(message: receivedBytes, endStream: false) + + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + func testNextInboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Even though the client is closed, because it received a message while open, + // we must get the message now. + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientClosedAndServerClosed() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + + // Close client + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + + // Even though the client is closed, because it received a message while open, + // we must get the message now. + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + // - MARK: Common paths + + func testNormalFlow() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let clientInitialMetadata = try stateMachine.send(metadata: .init()) + XCTAssertEqual( + clientInitialMetadata, + [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + ) + + // Server sends initial metadata + let serverInitialHeadersAction = try stateMachine.receive( + metadata: .serverInitialMetadata, + endStream: false + ) + XCTAssertEqual( + serverInitialHeadersAction, + .receivedMetadata([ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ]) + ) + + // Client sends messages + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let message = [UInt8]([1, 2, 3, 4]) + let framedMessage = try self.frameMessage(message, compress: false) + try stateMachine.send(message: message, endStream: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + // Server sends response + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + + let firstResponseBytes = [UInt8]([5, 6, 7]) + let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let secondResponseBytes = [UInt8]([8, 9, 10]) + let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + try stateMachine.receive(message: firstResponse, endStream: false) + try stateMachine.receive(message: secondResponse, endStream: false) + + // Make sure messages have arrived + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + + // Client sends end + try stateMachine.send(message: [], endStream: true) + + // Server ends + let metadataReceivedAction = try stateMachine.receive( + metadata: .serverTrailers, + endStream: true + ) + let receivedMetadata = { + var m = Metadata(headers: .serverTrailers) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + return m + }() + XCTAssertEqual( + metadataReceivedAction, + .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + ) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + func testClientClosesBeforeServerOpens() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let clientInitialMetadata = try stateMachine.send(metadata: .init()) + XCTAssertEqual( + clientInitialMetadata, + [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + ) + + // Client sends messages and ends + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let message = [UInt8]([1, 2, 3, 4]) + let framedMessage = try self.frameMessage(message, compress: false) + try stateMachine.send(message: message, endStream: true) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + + // Server sends initial metadata + let serverInitialHeadersAction = try stateMachine.receive( + metadata: .serverInitialMetadata, + endStream: false + ) + XCTAssertEqual( + serverInitialHeadersAction, + .receivedMetadata([ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ]) + ) + + // Server sends response + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + + let firstResponseBytes = [UInt8]([5, 6, 7]) + let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let secondResponseBytes = [UInt8]([8, 9, 10]) + let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + try stateMachine.receive(message: firstResponse, endStream: false) + try stateMachine.receive(message: secondResponse, endStream: false) + + // Make sure messages have arrived + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + + // Server ends + let metadataReceivedAction = try stateMachine.receive( + metadata: .serverTrailers, + endStream: true + ) + let receivedMetadata = { + var m = Metadata(headers: .serverTrailers) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + return m + }() + XCTAssertEqual( + metadataReceivedAction, + .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + ) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + func testClientClosesBeforeServerResponds() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let clientInitialMetadata = try stateMachine.send(metadata: .init()) + XCTAssertEqual( + clientInitialMetadata, + [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + ) + + // Client sends messages + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let message = [UInt8]([1, 2, 3, 4]) + let framedMessage = try self.frameMessage(message, compress: false) + try stateMachine.send(message: message, endStream: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + // Server sends initial metadata + let serverInitialHeadersAction = try stateMachine.receive( + metadata: .serverInitialMetadata, + endStream: false + ) + XCTAssertEqual( + serverInitialHeadersAction, + .receivedMetadata([ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ]) + ) + + // Client sends end + try stateMachine.send(message: [], endStream: true) + + // Server sends response + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + + let firstResponseBytes = [UInt8]([5, 6, 7]) + let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let secondResponseBytes = [UInt8]([8, 9, 10]) + let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + try stateMachine.receive(message: firstResponse, endStream: false) + try stateMachine.receive(message: secondResponse, endStream: false) + + // Make sure messages have arrived + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + + // Server ends + let metadataReceivedAction = try stateMachine.receive( + metadata: .serverTrailers, + endStream: true + ) + let receivedMetadata = { + var m = Metadata(headers: .serverTrailers) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) + m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) + return m + }() + XCTAssertEqual( + metadataReceivedAction, + .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + ) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } +} + +final class GRPCStreamServerStateMachineTests: XCTestCase { + private func makeServerStateMachine( + targetState: TargetStateMachineState, + compressionEnabled: Bool = false + ) -> GRPCStreamStateMachine { + + var stateMachine = GRPCStreamStateMachine( + configuration: .server( + .init( + scheme: .http, + acceptedEncodings: [.deflate] + ) + ), + maximumPayloadSize: 100, + skipAssertions: true + ) + + let clientMetadata: HPACKHeaders = + compressionEnabled ? .clientInitialMetadataWithDeflateCompression : .clientInitialMetadata + switch targetState { + case .clientIdleServerIdle: + break + case .clientOpenServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + case .clientOpenServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + case .clientOpenServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + // Close server + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + ) + case .clientClosedServerIdle: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + case .clientClosedServerOpen: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + case .clientClosedServerClosed: + // Open client + XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + // Open server + XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + // Close server + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + ) + } + + return stateMachine + } + + // - MARK: Send Metadata + + func testSendMetadataWhenClientIdleAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(metadata: .init()) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Client cannot be idle if server is sending initial metadata: it must have opened." + ) + } + } + + func testSendMetadataWhenClientOpenAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + } + + func testSendMetadataWhenClientOpenAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + // Try sending metadata again: should throw + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(metadata: .init()) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server has already sent initial metadata.") + } + } + + func testSendMetadataWhenClientOpenAndServerClosed() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + } + } + + func testSendMetadataWhenClientClosedAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + + // We should be allowed to send initial metadata if client is closed: + // client may be finished sending request but may still be awaiting response. + XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + } + + func testSendMetadataWhenClientClosedAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server has already sent initial metadata.") + } + } + + func testSendMetadataWhenClientClosedAndServerClosed() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) + + // Try sending metadata again: should throw + XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + } + } + + // - MARK: Send Message + + func testSendMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Server must have sent initial metadata before sending a message." + ) + } + } + + func testSendMessageWhenClientOpenAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + + // Now send a message + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Server must have sent initial metadata before sending a message." + ) + } + } + + func testSendMessageWhenClientOpenAndServerOpen() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + // Now send a message + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + + func testSendMessageWhenClientOpenAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) + + // Try sending another message: it should fail + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + func testSendMessageWhenClientClosedAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Server must have sent initial metadata before sending a message." + ) + } + } + + func testSendMessageWhenClientClosedAndServerOpen() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) + + // Try sending a message: even though client is closed, we should send it + // because it may be expecting a response. + XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + } + + func testSendMessageWhenClientClosedAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) + + // Try sending another message: it should fail + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + // - MARK: Send Status and Trailers + + func testSendStatusAndTrailersWhenClientIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init() + ) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send status if client is idle.") + } + } + + func testSendStatusAndTrailersWhenClientOpenAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + + let trailers = try stateMachine.send( + status: .init(code: .unknown, message: "RPC unknown"), + metadata: .init() + ) + + // Make sure it's a trailers-only response: it must have :status header and content-type + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "2", + "grpc-status-message": "RPC unknown", + ] + ) + + // Try sending another message: it should fail because server is now closed. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + func testSendStatusAndTrailersWhenClientOpenAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + let trailers = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init() + ) + + // Make sure it's NOT a trailers-only response, because the server was + // already open (so it sent initial metadata): it shouldn't have :status or content-type headers + XCTAssertEqual(trailers, ["grpc-status": "0"]) + + // Try sending another message: it should fail because server is now closed. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init() + ) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send anything if closed.") + } + } + + func testSendStatusAndTrailersWhenClientClosedAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + + let trailers = try stateMachine.send( + status: .init(code: .unknown, message: "RPC unknown"), + metadata: .init() + ) + + // Make sure it's a trailers-only response: it must have :status header and content-type + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "2", + "grpc-status-message": "RPC unknown", + ] + ) + + // Try sending another message: it should fail because server is now closed. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + func testSendStatusAndTrailersWhenClientClosedAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) + + let trailers = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init() + ) + + // Make sure it's NOT a trailers-only response, because the server was + // already open (so it sent initial metadata): it shouldn't have :status or content-type headers + XCTAssertEqual(trailers, ["grpc-status": "0"]) + + // Try sending another message: it should fail because server is now closed. + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send(message: [], endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send a message if it's closed.") + } + } + + func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: .init() + ) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send anything if closed.") + } + } + + // - MARK: Receive metadata + + func testReceiveMetadataWhenClientIdleAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + XCTAssertEqual( + action, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: true) + XCTAssertEqual( + action, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithoutContentType, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.firstString(forKey: .status), "415") + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithInvalidContentType, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual(trailers.count, 1) + XCTAssertEqual(trailers.firstString(forKey: .status), "415") + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithoutEndpoint, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "12", + "grpc-status-message": "No :path header has been set.", + ] + ) + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_ServerUnsupportedEncoding() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + // Try opening client with a compression algorithm that is not accepted + // by the server. + let action = try stateMachine.receive( + metadata: .clientInitialMetadataWithGzipCompression, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + "grpc-status": "12", + "grpc-status-message": + "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding", + ] + ) + } + } + + //TODO: add more encoding-related validation tests (for both client and server) + // and message encoding tests + + func testReceiveMetadataWhenClientOpenAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + + // Try receiving initial metadata again - should fail + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") + } + } + + func testReceiveMetadataWhenClientOpenAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") + } + } + + func testReceiveMetadataWhenClientOpenAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") + } + } + + func testReceiveMetadataWhenClientClosedAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") + } + } + + func testReceiveMetadataWhenClientClosedAndServerOpen() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") + } + } + + func testReceiveMetadataWhenClientClosedAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") + } + } + + // - MARK: Receive message + + func testReceiveMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Can't have received a message if client is idle.") + } + } + + func testReceiveMessageWhenClientOpenAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + + // Receive messages successfully: the second one should close client. + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Verify client is now closed + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + // Receive messages successfully: the second one should close client. + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Verify client is now closed + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientOpenAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) + + // Client is not done sending request, don't fail. + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + } + + func testReceiveMessageWhenClientClosedAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientClosedAndServerOpen() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testReceiveMessageWhenClientClosedAndServerClosed() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(message: .init(), endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + // - MARK: Next outbound message + + func testNextOutboundMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + + let response = try stateMachine.nextOutboundMessage() + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + + // And then make sure that nothing else is returned + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + } + + func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = self.makeServerStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + + let response = try stateMachine.nextOutboundMessage() + let framedMessage = try self.frameMessage(originalMessage, compress: true) + XCTAssertEqual(response, .sendMessage(framedMessage)) + } + + func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + // Send message and close server + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + ) + + let response = try stateMachine.nextOutboundMessage() + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + + // And then make sure that nothing else is returned anymore + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + } + + func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.nextOutboundMessage() + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server is not open yet.") + } + } + + func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + // Send a message + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Send another message + XCTAssertNoThrow(try stateMachine.send(message: [43, 43], endStream: false)) + + // Make sure that getting the next outbound message _does_ return the message + // we have enqueued. + let response = try stateMachine.nextOutboundMessage() + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + // End of first message - beginning of second + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 43, 43, // original message + ] + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + + // And then make sure that nothing else is returned anymore + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + } + + func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) + + // Send a message and close server + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + ) + + // We have enqueued a message, make sure we return it even though server is closed, + // because we haven't yet drained all of the pending messages. + let response = try stateMachine.nextOutboundMessage() + let expectedBytes: [UInt8] = [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ] + XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + + // And then make sure that nothing else is returned anymore + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + } + + // - MARK: Next inbound message + + func testNextInboundMessageWhenClientIdleAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientOpenAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientOpenAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { + var stateMachine = self.makeServerStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + let receivedBytes = try self.frameMessage(originalMessage, compress: true) + + try stateMachine.receive(message: receivedBytes, endStream: false) + + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientOpenAndServerClosed() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + ) + + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + } + + func testNextInboundMessageWhenClientClosedAndServerIdle() { + var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + func testNextInboundMessageWhenClientClosedAndServerOpen() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Even though the client is closed, because the server received a message + // while it was still open, we must get the message now. + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + func testNextInboundMessageWhenClientClosedAndServerClosed() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) + + let receivedBytes = ByteBuffer(bytes: [ + 0, // compression flag: unset + 0, 0, 0, 2, // message length: 2 bytes + 42, 42, // original message + ]) + try stateMachine.receive(message: receivedBytes, endStream: false) + + // Close server + XCTAssertNoThrow( + try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + ) + + // Close client + XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + + // Even though the client and server are closed, because the server received + // a message while the client was still open, we must get the message now. + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + // - MARK: Common paths + + func testNormalFlow() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let receiveMetadataAction = try stateMachine.receive( + metadata: .clientInitialMetadata, + endStream: false + ) + XCTAssertEqual( + receiveMetadataAction, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + + // Server sends initial metadata + let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) + XCTAssertEqual( + sentInitialHeaders, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + "custom": "value", + ] + ) + + // Client sends messages + let deframedMessage = [UInt8]([1, 2, 3, 4]) + let completeMessage = try self.frameMessage(deframedMessage, compress: false) + // Split message into two parts to make sure the stitching together of the frames works well + let firstMessage = completeMessage.getSlice(at: 0, length: 4)! + let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! + + try stateMachine.receive(message: firstMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + try stateMachine.receive(message: secondMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) + + // Server sends response + let firstResponse = [UInt8]([5, 6, 7]) + let secondResponse = [UInt8]([8, 9, 10]) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, endStream: false) + try stateMachine.send(message: secondResponse, endStream: false) + + // Make sure messages are outbound + let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + + // Client sends end + try stateMachine.receive(message: ByteBuffer(), endStream: true) + + // Server ends + let response = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + XCTAssertEqual(response, ["grpc-status": "0"]) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + func testClientClosesBeforeServerOpens() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let receiveMetadataAction = try stateMachine.receive( + metadata: .clientInitialMetadata, + endStream: false + ) + XCTAssertEqual( + receiveMetadataAction, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + + // Client sends messages + let deframedMessage = [UInt8]([1, 2, 3, 4]) + let completeMessage = try self.frameMessage(deframedMessage, compress: false) + // Split message into two parts to make sure the stitching together of the frames works well + let firstMessage = completeMessage.getSlice(at: 0, length: 4)! + let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! + + try stateMachine.receive(message: firstMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + try stateMachine.receive(message: secondMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) + + // Client sends end + try stateMachine.receive(message: ByteBuffer(), endStream: true) + + // Server sends initial metadata + let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) + XCTAssertEqual( + sentInitialHeaders, + [ + "custom": "value", + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ] + ) + + // Server sends response + let firstResponse = [UInt8]([5, 6, 7]) + let secondResponse = [UInt8]([8, 9, 10]) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, endStream: false) + try stateMachine.send(message: secondResponse, endStream: false) + + // Make sure messages are outbound + let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + + // Server ends + let response = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + XCTAssertEqual(response, ["grpc-status": "0"]) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } + + func testClientClosesBeforeServerResponds() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + // Client sends metadata + let receiveMetadataAction = try stateMachine.receive( + metadata: .clientInitialMetadata, + endStream: false + ) + XCTAssertEqual( + receiveMetadataAction, + .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + ) + + // Client sends messages + let deframedMessage = [UInt8]([1, 2, 3, 4]) + let completeMessage = try self.frameMessage(deframedMessage, compress: false) + // Split message into two parts to make sure the stitching together of the frames works well + let firstMessage = completeMessage.getSlice(at: 0, length: 4)! + let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! + + try stateMachine.receive(message: firstMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + try stateMachine.receive(message: secondMessage, endStream: false) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) + + // Server sends initial metadata + let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) + XCTAssertEqual( + sentInitialHeaders, + [ + "custom": "value", + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ] + ) + + // Client sends end + try stateMachine.receive(message: ByteBuffer(), endStream: true) + + // Server sends response + let firstResponse = [UInt8]([5, 6, 7]) + let secondResponse = [UInt8]([8, 9, 10]) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, endStream: false) + try stateMachine.send(message: secondResponse, endStream: false) + + // Make sure messages are outbound + let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + + // Server ends + let response = try stateMachine.send( + status: .init(code: .ok, message: ""), + metadata: [] + ) + XCTAssertEqual(response, ["grpc-status": "0"]) + + XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) + } +} + +extension XCTestCase { + func assertRejectedRPC( + _ action: GRPCStreamStateMachine.OnMetadataReceived, + expression: (HPACKHeaders) throws -> Void + ) rethrows { + guard case .rejectRPC(let trailers) = action else { + XCTFail("RPC should have been rejected.") + return + } + try expression(trailers) + } + + func frameMessage(_ message: [UInt8], compress: Bool) throws -> ByteBuffer { + try frameMessages([message], compress: compress) + } + + func frameMessages(_ messages: [[UInt8]], compress: Bool) throws -> ByteBuffer { + var framer = GRPCMessageFramer() + let compressor: Zlib.Compressor? = { + if compress { + return Zlib.Compressor(method: .deflate) + } else { + return nil + } + }() + defer { compressor?.end() } + for message in messages { + framer.append(message) + } + return try XCTUnwrap(framer.next(compressor: compressor)) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift new file mode 100644 index 000000000..ac659fad9 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift @@ -0,0 +1,44 @@ +/* + * 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 XCTest + +@testable import GRPCHTTP2Core + +class GRPCStatusMessageMarshallerTests: XCTestCase { + func testASCIIMarshallingAndUnmarshalling() { + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("Hello, World!"), "Hello, World!") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("Hello, World!"), "Hello, World!") + } + + func testPercentMarshallingAndUnmarshalling() { + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("%"), "%25") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%25"), "%") + + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("25%"), "25%25") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("25%25"), "25%") + } + + func testUnicodeMarshalling() { + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("🚀"), "%F0%9F%9A%80") + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%F0%9F%9A%80"), "🚀") + + let message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" + let marshalled = + "%09%0Atest with whitespace%0D%0Aand Unicode BMP %E2%98%BA and non-BMP %F0%9F%98%88%09%0A" + XCTAssertEqual(GRPCStatusMessageMarshaller.marshall(message), marshalled) + XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall(marshalled), message) + } +} From d29225163c4dd4663bfbe1645d3bc92a9c7100f3 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 15 Mar 2024 16:09:06 +0000 Subject: [PATCH 266/580] Add missing availability guards (#1830) --- Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 34e615623..a9de430a1 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -88,6 +88,7 @@ extension HPACKHeaders { ] } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCStreamClientStateMachineTests: XCTestCase { private func makeClientStateMachine( targetState: TargetStateMachineState, @@ -1045,6 +1046,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCStreamServerStateMachineTests: XCTestCase { private func makeServerStateMachine( targetState: TargetStateMachineState, @@ -2161,6 +2163,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } extension XCTestCase { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) func assertRejectedRPC( _ action: GRPCStreamStateMachine.OnMetadataReceived, expression: (HPACKHeaders) throws -> Void From 3c65b40ca7cb3297f717c209059a3c65f2cba585 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 15 Mar 2024 16:38:36 +0000 Subject: [PATCH 267/580] Add missing transitions to GRPCStreamStateMachine (#1831) --- .../GRPCStreamStateMachine.swift | 104 +++++++++-- .../GRPCStreamStateMachineTests.swift | 173 +++++++++++++++++- 2 files changed, 264 insertions(+), 13 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 0cd0ca797..753409587 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -110,7 +110,7 @@ private enum GRPCStreamStateMachineState { } struct ClientOpenServerClosedState { - var framer: GRPCMessageFramer + var framer: GRPCMessageFramer? var compressor: Zlib.Compressor? let deframer: NIOSingleStepByteToMessageProcessor? @@ -118,6 +118,21 @@ private enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + // This transition should only happen on the server-side when, upon receiving + // initial client metadata, some of the headers are invalid and we must reject + // the RPC. + // We will mark the client as open (because it sent initial metadata albeit + // invalid) but we'll close the server, meaning all future messages sent from + // the client will be ignored. Because of this, we won't need to frame or + // deframe any messages, as we won't be reading or writing any messages. + init(previousState: ClientIdleServerIdleState) { + self.framer = nil + self.compressor = nil + self.deframer = nil + self.decompressor = nil + self.inboundMessageBuffer = .init() + } + init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -240,12 +255,25 @@ private enum GRPCStreamStateMachineState { // We still need the framer and compressor in case the server has closed // but its buffer is not yet empty and still needs to send messages out to // the client. - var framer: GRPCMessageFramer + var framer: GRPCMessageFramer? var compressor: Zlib.Compressor? // These are already deframed, so we don't need the deframer anymore. var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + // This transition should only happen on the server-side when, upon receiving + // initial client metadata, some of the headers are invalid and we must reject + // the RPC. + // We will mark the client as closed (because it set the EOS flag, even if + // the initial metadata was invalid) and we'll close the server too. + // Because of this, we won't need to frame any messages, as we + // won't be writing any messages. + init(previousState: ClientIdleServerIdleState) { + self.framer = nil + self.compressor = nil + self.inboundMessageBuffer = .init() + } + init(previousState: ClientClosedServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -1062,6 +1090,21 @@ extension GRPCStreamStateMachine { } } + mutating private func closeServerAndBuildRejectRPCAction( + currentState: GRPCStreamStateMachineState.ClientIdleServerIdleState, + endStream: Bool, + rejectWithStatus status: Status + ) -> OnMetadataReceived { + if endStream { + self.state = .clientClosedServerClosed(.init(previousState: currentState)) + } else { + self.state = .clientOpenServerClosed(.init(previousState: currentState)) + } + + let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) + return .rejectRPC(trailers: trailers) + } + private mutating func serverReceive( metadata: HPACKHeaders, endStream: Bool, @@ -1071,7 +1114,9 @@ extension GRPCStreamStateMachine { case .clientIdleServerIdle(let state): let contentType = metadata.firstString(forKey: .contentType) .flatMap { ContentType(value: $0) } - guard contentType != nil else { + if contentType == nil { + self.state = .clientOpenServerClosed(.init(previousState: state)) + // Respond with HTTP-level Unsupported Media Type status code. var trailers = HPACKHeaders() trailers.add("415", forKey: .status) @@ -1080,13 +1125,50 @@ extension GRPCStreamStateMachine { let path = metadata.firstString(forKey: .path) .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } - guard path != nil else { - let status = Status( - code: .unimplemented, - message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." + if path == nil { + return self.closeServerAndBuildRejectRPCAction( + currentState: state, + endStream: endStream, + rejectWithStatus: Status( + code: .unimplemented, + message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." + ) + ) + } + + let scheme = metadata.firstString(forKey: .scheme) + .flatMap { Scheme(rawValue: $0) } + if scheme == nil { + return self.closeServerAndBuildRejectRPCAction( + currentState: state, + endStream: endStream, + rejectWithStatus: Status( + code: .invalidArgument, + message: ":scheme header must be present and one of \"http\" or \"https\"." + ) + ) + } + + guard let method = metadata.firstString(forKey: .method), method == "POST" else { + return self.closeServerAndBuildRejectRPCAction( + currentState: state, + endStream: endStream, + rejectWithStatus: Status( + code: .invalidArgument, + message: ":method header is expected to be present and have a value of \"POST\"." + ) + ) + } + + guard let te = metadata.firstString(forKey: .te), te == "trailers" else { + return self.closeServerAndBuildRejectRPCAction( + currentState: state, + endStream: endStream, + rejectWithStatus: Status( + code: .invalidArgument, + message: "\"te\" header is expected to be present and have a value of \"trailers\"." + ) ) - let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) - return .rejectRPC(trailers: trailers) } func isIdentityOrCompatibleEncoding(_ clientEncoding: CompressionAlgorithm) -> Bool { @@ -1265,7 +1347,7 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerOpen(state) return response.map { .sendMessage($0) } ?? .awaitMoreMessages case .clientOpenServerClosed(var state): - let response = try state.framer.next(compressor: state.compressor) + let response = try state.framer?.next(compressor: state.compressor) self.state = .clientOpenServerClosed(state) if let response { return .sendMessage(response) @@ -1273,7 +1355,7 @@ extension GRPCStreamStateMachine { return .noMoreMessages } case .clientClosedServerClosed(var state): - let response = try state.framer.next(compressor: state.compressor) + let response = try state.framer?.next(compressor: state.compressor) self.state = .clientClosedServerClosed(state) if let response { return .sendMessage(response) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index a9de430a1..53470c9aa 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -45,7 +45,7 @@ extension HPACKHeaders { GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "https", - GRPCHTTP2Keys.te.rawValue: "te", + GRPCHTTP2Keys.te.rawValue: "trailers", GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", GRPCHTTP2Keys.encoding.rawValue: "deflate", ] @@ -54,7 +54,7 @@ extension HPACKHeaders { GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "https", - GRPCHTTP2Keys.te.rawValue: "te", + GRPCHTTP2Keys.te.rawValue: "trailers", GRPCHTTP2Keys.acceptEncoding.rawValue: "gzip", GRPCHTTP2Keys.encoding.rawValue: "gzip", ] @@ -68,6 +68,45 @@ extension HPACKHeaders { fileprivate static let receivedWithoutEndpoint: Self = [ GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] + fileprivate static let receivedWithoutTE: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + ] + fileprivate static let receivedWithInvalidTE: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "invalidte", + ] + fileprivate static let receivedWithoutMethod: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + fileprivate static let receivedWithInvalidMethod: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "GET", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + fileprivate static let receivedWithoutScheme: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + fileprivate static let receivedWithInvalidScheme: Self = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "invalidscheme", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] // Server fileprivate static let serverInitialMetadata: Self = [ @@ -1502,6 +1541,136 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } } + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingTE() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithoutTE, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "3", + "grpc-status-message": + "\"te\" header is expected to be present and have a value of \"trailers\".", + ] + ) + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidTE() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithInvalidTE, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "3", + "grpc-status-message": + "\"te\" header is expected to be present and have a value of \"trailers\".", + ] + ) + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingMethod() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithoutMethod, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "3", + "grpc-status-message": + ":method header is expected to be present and have a value of \"POST\".", + ] + ) + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidMethod() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithInvalidMethod, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "3", + "grpc-status-message": + ":method header is expected to be present and have a value of \"POST\".", + ] + ) + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingScheme() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithoutScheme, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "3", + "grpc-status-message": ":scheme header must be present and one of \"http\" or \"https\".", + ] + ) + } + } + + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidScheme() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + metadata: .receivedWithInvalidScheme, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "3", + "grpc-status-message": ":scheme header must be present and one of \"http\" or \"https\".", + ] + ) + } + } + func testReceiveMetadataWhenClientIdleAndServerIdle_ServerUnsupportedEncoding() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) From 62653baa516db2d061b7e0100b1c18440c4e4441 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:44:22 +0000 Subject: [PATCH 268/580] QPS Benchmark Service implementation (#1828) Motivation: The 2 workers used in QPS testing are a Benchmark service client and server respectivelly, so we need to implement the Benchmark Service. Modifications: Implemented the 'BenchmarkService' struct that defines the service protocol methods, based on their documentation. Result: We will be able to proceed with the Wrorker Service implementation. --- .../performance-worker/BenchmarkService.swift | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 Sources/performance-worker/BenchmarkService.swift diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift new file mode 100644 index 000000000..67d1de679 --- /dev/null +++ b/Sources/performance-worker/BenchmarkService.swift @@ -0,0 +1,176 @@ +/* + * 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 Atomics +import GRPCCore + +import struct Foundation.Data + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { + /// Used to check if the server can be streaming responses. + private let working = ManagedAtomic(true) + + /// One request followed by one response. + /// The server returns a client payload with the size requested by the client. + func unaryCall( + request: GRPCCore.ServerRequest.Single + ) async throws + -> GRPCCore.ServerResponse.Single + { + // Throw an error if the status is not `ok`. Otherwise, an `ok` status is automatically sent + // if the request is successful. + if request.message.responseStatus.isInitialized { + try self.checkOkStatus(request.message.responseStatus) + } + + return ServerResponse.Single( + message: Grpc_Testing_BenchmarkService.Method.UnaryCall.Output.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: Int(request.message.responseSize)) + } + } + ) + } + + /// Repeated sequence of one request followed by one response. + /// The server returns a payload with the size requested by the client for each received message. + func streamingCall( + request: GRPCCore.ServerRequest.Stream + ) async throws + -> GRPCCore.ServerResponse.Stream + { + return ServerResponse.Stream { writer in + for try await message in request.messages { + if message.responseStatus.isInitialized { + try self.checkOkStatus(message.responseStatus) + } + try await writer.write( + Grpc_Testing_BenchmarkService.Method.StreamingCall.Output.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: Int(message.responseSize)) + } + } + ) + } + return [:] + } + } + + /// Single-sided unbounded streaming from client to server. + /// The server returns a payload with the size requested by the client once the client does WritesDone. + func streamingFromClient( + request: ServerRequest.Stream + ) async throws + -> ServerResponse.Single + { + var responseSize = 0 + for try await message in request.messages { + if message.responseStatus.isInitialized { + try self.checkOkStatus(message.responseStatus) + } + responseSize = Int(message.responseSize) + } + + return ServerResponse.Single( + message: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.Output.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: responseSize) + } + } + ) + } + + /// Single-sided unbounded streaming from server to client. + /// The server repeatedly returns a payload with the size requested by the client. + func streamingFromServer( + request: ServerRequest.Single + ) async throws + -> ServerResponse.Stream + { + if request.message.responseStatus.isInitialized { + try self.checkOkStatus(request.message.responseStatus) + } + let response = Grpc_Testing_BenchmarkService.Method.StreamingCall.Output.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: Int(request.message.responseSize)) + } + } + return ServerResponse.Stream { writer in + while working.load(ordering: .relaxed) { + try await writer.write(response) + } + return [:] + } + } + + /// Two-sided unbounded streaming between server to client. + /// Both sides send the content of their own choice to the other. + func streamingBothWays( + request: GRPCCore.ServerRequest.Stream< + Grpc_Testing_BenchmarkService.Method.StreamingBothWays.Input + > + ) async throws + -> ServerResponse.Stream + { + // The 100 size is used by the other implementations as well. + // We are using the same canned response size for all responses + // as it is allowed by the spec. + let response = Grpc_Testing_BenchmarkService.Method.StreamingCall.Output.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: 100) + } + } + + // Marks if the inbound streaming is ongoing or finished. + let inboundStreaming = ManagedAtomic(true) + + return ServerResponse.Stream { writer in + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for try await message in request.messages { + if message.responseStatus.isInitialized { + try self.checkOkStatus(message.responseStatus) + } + } + inboundStreaming.store(false, ordering: .relaxed) + } + group.addTask { + while inboundStreaming.load(ordering: .relaxed) + && self.working.load(ordering: .acquiring) + { + try await writer.write(response) + } + } + try await group.next() + group.cancelAll() + return [:] + } + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension BenchmarkService { + private func checkOkStatus(_ responseStatus: Grpc_Testing_EchoStatus) throws { + guard let code = Status.Code(rawValue: Int(responseStatus.code)) else { + throw RPCError(code: .invalidArgument, message: "The response status code is invalid.") + } + if let code = RPCError.Code(code) { + throw RPCError(code: code, message: responseStatus.message) + } + } +} From 800fadca8cb06705618017b6327658698c034c36 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:48:50 +0000 Subject: [PATCH 269/580] Implement 'quit' and 'core count' RPCs on the worker service (#1833) Motivation: These are 2 of the RPCs on the worker service that we need for benchmarking. Modifications: - Created the WorkerService struct that has a GRPCClient and a GRPCServer property - Implemented the 'coreCount' and 'quitWorker' RPCs Result: - The driver will be able to request 'quitWorker' and 'coreCount' from the workers --- Package.swift | 3 +- .../performance-worker/WorkerService.swift | 100 ++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 Sources/performance-worker/WorkerService.swift diff --git a/Package.swift b/Package.swift index e6b4fb0bb..83d52b749 100644 --- a/Package.swift +++ b/Package.swift @@ -250,7 +250,8 @@ extension Target { name: "performance-worker", dependencies: [ .grpcCore, - .grpcProtobuf + .grpcProtobuf, + .nioCore ] ) diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift new file mode 100644 index 000000000..b1d419c95 --- /dev/null +++ b/Sources/performance-worker/WorkerService.swift @@ -0,0 +1,100 @@ +/* + * 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 NIOConcurrencyHelpers +import NIOCore + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable { + private let state: NIOLockedValueBox + + init() { + let clientAndServer = State() + self.state = NIOLockedValueBox(clientAndServer) + } + + private struct State { + var role: Role? + + enum Role { + case client(GRPCClient) + case server(GRPCServer) + } + + init() {} + + init(role: Role) { + self.role = role + } + + init(server: GRPCServer) { + self.role = .server(server) + } + + init(client: GRPCClient) { + self.role = .client(client) + } + } + + func quitWorker( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + + let role = self.state.withLockedValue { state in + defer { state.role = nil } + return state.role + } + + if let role = role { + switch role { + case .client(let client): + client.close() + case .server(let server): + server.stopListening() + } + } + + return ServerResponse.Single(message: Grpc_Testing_WorkerService.Method.QuitWorker.Output()) + } + + func coreCount( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let coreCount = System.coreCount + return ServerResponse.Single( + message: Grpc_Testing_WorkerService.Method.CoreCount.Output.with { + $0.cores = Int32(coreCount) + } + ) + } + + func runServer( + request: GRPCCore.ServerRequest.Stream + ) async throws + -> GRPCCore.ServerResponse.Stream + { + throw RPCError(code: .unimplemented, message: "This RPC has not been implemented yet.") + } + + func runClient( + request: GRPCCore.ServerRequest.Stream + ) async throws + -> GRPCCore.ServerResponse.Stream + { + throw RPCError(code: .unimplemented, message: "This RPC has not been implemented yet.") + } +} From 2d62bdf4f2da89f13ad109e8cf026fe57e219fb6 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 21 Mar 2024 13:54:40 +0000 Subject: [PATCH 270/580] Add GRPCServerStreamHandler (#1832) --- .../GRPCStreamStateMachine.swift | 2 +- .../Server/GRPCServerStreamHandler.swift | 217 +++++ .../Server/GRPCServerStreamHandlerTests.swift | 789 ++++++++++++++++++ 3 files changed, 1007 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 753409587..c3eda1363 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -332,7 +332,7 @@ struct GRPCStreamStateMachine { case .server: if endStream { try self.invalidState( - "Can't end response stream by sending a message - send(status:metadata:trailersOnly:) must be called" + "Can't end response stream by sending a message - send(status:metadata:) must be called" ) } try self.serverSend(message: message) diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift new file mode 100644 index 000000000..c4da0d4ed --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -0,0 +1,217 @@ +/* + * 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 NIOCore +import NIOHTTP2 + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class GRPCServerStreamHandler: ChannelDuplexHandler { + typealias InboundIn = HTTP2Frame.FramePayload + typealias InboundOut = RPCRequestPart + + typealias OutboundIn = RPCResponsePart + typealias OutboundOut = HTTP2Frame.FramePayload + + private var stateMachine: GRPCStreamStateMachine + + private var isReading = false + private var flushPending = false + + // We buffer the final status + trailers to avoid reordering issues (i.e., + // if there are messages still not written into the channel because flush has + // not been called, but the server sends back trailers). + private var pendingTrailers: HTTP2Frame.FramePayload? + + init( + scheme: Scheme, + acceptedEncodings: [CompressionAlgorithm], + maximumPayloadSize: Int, + skipStateMachineAssertions: Bool = false + ) { + self.stateMachine = .init( + configuration: .server(.init(scheme: scheme, acceptedEncodings: acceptedEncodings)), + maximumPayloadSize: maximumPayloadSize, + skipAssertions: skipStateMachineAssertions + ) + } +} + +// - MARK: ChannelInboundHandler + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCServerStreamHandler { + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + self.isReading = true + let frame = self.unwrapInboundIn(data) + switch frame { + case .data(let frameData): + let endStream = frameData.endStream + switch frameData.data { + case .byteBuffer(let buffer): + do { + try self.stateMachine.receive(message: buffer, endStream: endStream) + loop: while true { + switch self.stateMachine.nextInboundMessage() { + case .receiveMessage(let message): + context.fireChannelRead(self.wrapInboundOut(.message(message))) + case .awaitMoreMessages: + break loop + case .noMoreMessages: + context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + break loop + } + } + } catch { + context.fireErrorCaught(error) + } + + case .fileRegion: + preconditionFailure("Unexpected IOData.fileRegion") + } + + case .headers(let headers): + do { + let action = try self.stateMachine.receive( + metadata: headers.headers, + endStream: headers.endStream + ) + switch action { + case .receivedMetadata(let metadata): + context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) + + case .rejectRPC(let trailers): + self.flushPending = true + let response = HTTP2Frame.FramePayload.headers(.init(headers: trailers, endStream: true)) + context.write(self.wrapOutboundOut(response), promise: nil) + + case .receivedStatusAndMetadata: + throw RPCError( + code: .internalError, + message: "Server cannot get receivedStatusAndMetadata." + ) + + case .doNothing: + throw RPCError(code: .internalError, message: "Server cannot receive doNothing.") + } + } catch { + context.fireErrorCaught(error) + } + + case .ping, .goAway, .priority, .rstStream, .settings, .pushPromise, .windowUpdate, + .alternativeService, .origin: + () + } + } + + func channelReadComplete(context: ChannelHandlerContext) { + self.isReading = false + if self.flushPending { + self.flushPending = false + context.flush() + } + context.fireChannelReadComplete() + } + + func handlerRemoved(context: ChannelHandlerContext) { + self.stateMachine.tearDown() + } +} + +// - MARK: ChannelOutboundHandler + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCServerStreamHandler { + func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + let frame = self.unwrapOutboundIn(data) + switch frame { + case .metadata(let metadata): + do { + self.flushPending = true + let headers = try self.stateMachine.send(metadata: metadata) + context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: nil) + // TODO: move the promise handling into the state machine + promise?.succeed() + } catch { + context.fireErrorCaught(error) + // TODO: move the promise handling into the state machine + promise?.fail(error) + } + + case .message(let message): + do { + try self.stateMachine.send(message: message, endStream: false) + // TODO: move the promise handling into the state machine + promise?.succeed() + } catch { + context.fireErrorCaught(error) + // TODO: move the promise handling into the state machine + promise?.fail(error) + } + + case .status(let status, let metadata): + do { + let headers = try self.stateMachine.send(status: status, metadata: metadata) + let response = HTTP2Frame.FramePayload.headers(.init(headers: headers, endStream: true)) + self.pendingTrailers = response + // TODO: move the promise handling into the state machine + promise?.succeed() + } catch { + context.fireErrorCaught(error) + // TODO: move the promise handling into the state machine + promise?.fail(error) + } + } + } + + func flush(context: ChannelHandlerContext) { + if self.isReading { + // We don't want to flush yet if we're still in a read loop. + return + } + + do { + loop: while true { + switch try self.stateMachine.nextOutboundMessage() { + case .sendMessage(let byteBuffer): + self.flushPending = true + context.write( + self.wrapOutboundOut(.data(.init(data: .byteBuffer(byteBuffer)))), + promise: nil + ) + + case .noMoreMessages: + if let pendingTrailers = self.pendingTrailers { + self.flushPending = true + self.pendingTrailers = nil + context.write(self.wrapOutboundOut(pendingTrailers), promise: nil) + } + break loop + + case .awaitMoreMessages: + break loop + } + } + + if self.flushPending { + self.flushPending = false + context.flush() + } + } catch { + context.fireErrorCaught(error) + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift new file mode 100644 index 000000000..5839a7aa3 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -0,0 +1,789 @@ +/* + * 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 NIOCore +import NIOEmbedded +import NIOHPACK +import NIOHTTP2 +import XCTest + +@testable import GRPCHTTP2Core + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class GRPCServerStreamHandlerTests: XCTestCase { + func testH2FramesAreIgnored() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + let framesToBeIgnored: [HTTP2Frame.FramePayload] = [ + .ping(.init(), ack: false), + .goAway(lastStreamID: .rootStream, errorCode: .cancel, opaqueData: nil), + // TODO: add .priority(StreamPriorityData) - right now, StreamPriorityData's + // initialiser is internal, so I can't create one of these frames. + .rstStream(.cancel), + .settings(.ack), + .pushPromise(.init(pushedStreamID: .maxID, headers: [:])), + .windowUpdate(windowSizeIncrement: 4), + .alternativeService(origin: nil, field: nil), + .origin([]), + ] + + for toBeIgnored in framesToBeIgnored { + XCTAssertNoThrow(try channel.writeInbound(toBeIgnored)) + XCTAssertNil(try channel.readInbound(as: HTTP2Frame.FramePayload.self)) + } + } + + func testClientInitialMetadataWithoutContentTypeResultsInRejectedRPC() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata without content-type + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we have sent a trailers-only response + let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() + + XCTAssertEqual(writtenTrailersOnlyResponse.headers, [":status": "415"]) + XCTAssertTrue(writtenTrailersOnlyResponse.endStream) + } + + func testClientInitialMetadataWithoutMethodResultsInRejectedRPC() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata without :method + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we have sent a trailers-only response + let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenTrailersOnlyResponse.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: + ":method header is expected to be present and have a value of \"POST\".", + ] + ) + XCTAssertTrue(writtenTrailersOnlyResponse.endStream) + } + + func testClientInitialMetadataWithoutSchemeResultsInRejectedRPC() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata without :scheme + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we have sent a trailers-only response + let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenTrailersOnlyResponse.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: + ":scheme header must be present and one of \"http\" or \"https\".", + ] + ) + XCTAssertTrue(writtenTrailersOnlyResponse.endStream) + } + + func testClientInitialMetadataWithoutPathResultsInRejectedRPC() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata without :path + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we have sent a trailers-only response + let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenTrailersOnlyResponse.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.unimplemented.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: "No :path header has been set.", + ] + ) + XCTAssertTrue(writtenTrailersOnlyResponse.endStream) + } + + func testClientInitialMetadataWithoutTEResultsInRejectedRPC() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata without TE + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we have sent a trailers-only response + let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenTrailersOnlyResponse.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: + "\"te\" header is expected to be present and have a value of \"trailers\".", + ] + ) + XCTAssertTrue(writtenTrailersOnlyResponse.endStream) + } + + func testNotAcceptedEncodingResultsInRejectedRPC() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.encoding.rawValue: "deflate", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + // Make sure we have sent a trailers-only response + let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenTrailersOnlyResponse.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.unimplemented.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: "Compression is not supported", + ] + ) + XCTAssertTrue(writtenTrailersOnlyResponse.endStream) + } + + func testOverMaximumPayloadSize() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // Write back server's initial metadata + let headers: HPACKHeaders = [ + "some-custom-header": "some-custom-value" + ] + let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) + XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) + + // Make sure we wrote back the initial metadata + let writtenHeaders = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenHeaders.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + "some-custom-header": "some-custom-value", + ] + ) + + // Receive client's message + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message + let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + "Message has exceeded the configured maximum payload size (max: 1, actual: 42)" + ) + } + + // Make sure we haven't sent a response back and that we didn't read the received message + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) + } + + func testClientEndsStream() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata with end stream set + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata, endStream: true)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // Write back server's initial metadata + let headers: HPACKHeaders = [ + "some-custom-header": "some-custom-value" + ] + let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) + XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) + + // Make sure we wrote back the initial metadata + let writtenHeaders = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenHeaders.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + "some-custom-header": "some-custom-value", + ] + ) + + // We should throw if the client sends another message, since it's closed the stream already. + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message + let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client can't send a message if closed.") + } + } + + func testNormalFlow() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // Write back server's initial metadata + let headers: HPACKHeaders = [ + "some-custom-header": "some-custom-value" + ] + let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) + XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) + + // Make sure we wrote back the initial metadata + let writtenHeaders = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenHeaders.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + "some-custom-header": "some-custom-value", + ] + ) + + // Receive client's message + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message + let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) + XCTAssertNoThrow(try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload))) + + // Make sure we haven't sent back an error response, and that we read the message properly + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.message([UInt8](repeating: 0, count: 42)) + ) + + // Write back response + let serverDataPayload = RPCResponsePart.message([UInt8](repeating: 1, count: 42)) + XCTAssertNoThrow(try channel.writeOutbound(serverDataPayload)) + + // Make sure we wrote back the right message + let writtenMessage = try channel.assertReadDataOutbound() + + var expectedBuffer = ByteBuffer() + expectedBuffer.writeInteger(UInt8(0)) // not compressed + expectedBuffer.writeInteger(UInt32(42)) // message length + expectedBuffer.writeRepeatingByte(1, count: 42) // message + XCTAssertEqual(writtenMessage.data, .byteBuffer(expectedBuffer)) + + // Send back status to end RPC + let trailers = RPCResponsePart.status( + .init(code: .dataLoss, message: "Test data loss"), + ["custom-header": "custom-value"] + ) + XCTAssertNoThrow(try channel.writeOutbound(trailers)) + + // Make sure we wrote back the status and trailers + let writtenStatus = try channel.assertReadHeadersOutbound() + + XCTAssertTrue(writtenStatus.endStream) + XCTAssertEqual( + writtenStatus.headers, + [ + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.dataLoss.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: "Test data loss", + "custom-header": "custom-value", + ] + ) + } + + func testReceiveMessageSplitAcrossMultipleBuffers() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // Write back server's initial metadata + let headers: HPACKHeaders = [ + "some-custom-header": "some-custom-value" + ] + let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) + XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) + + // Make sure we wrote back the initial metadata + let writtenHeaders = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenHeaders.headers, + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + "some-custom-header": "some-custom-value", + ] + ) + + // Receive client's first message + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) + + buffer.clear() + buffer.writeInteger(UInt32(30)) // message length + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) + + buffer.clear() + buffer.writeRepeatingByte(0, count: 10) // first part of the message + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) + + buffer.clear() + buffer.writeRepeatingByte(1, count: 10) // second part of the message + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) + + buffer.clear() + buffer.writeRepeatingByte(2, count: 10) // third part of the message + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + + // Make sure we haven't sent back an error response, and that we read the message properly + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.message( + [UInt8](repeating: 0, count: 10) + [UInt8](repeating: 1, count: 10) + + [UInt8](repeating: 2, count: 10) + ) + ) + } + + func testSendMultipleMessagesInSingleBuffer() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // Write back server's initial metadata + let headers: HPACKHeaders = [ + "some-custom-header": "some-custom-value" + ] + let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) + XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) + + // Read out the metadata + _ = try channel.readOutbound(as: HTTP2Frame.FramePayload.self) + + // This is where this test actually begins. We want to write two messages + // without flushing, and make sure that no messages are sent down the pipeline + // until we flush. Once we flush, both messages should be sent in the same ByteBuffer. + + // Write back first message and make sure nothing's written in the channel. + XCTAssertNoThrow(channel.write(RPCResponsePart.message([UInt8](repeating: 1, count: 4)))) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + // Write back second message and make sure nothing's written in the channel. + XCTAssertNoThrow(channel.write(RPCResponsePart.message([UInt8](repeating: 2, count: 4)))) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + // Now flush and check we *do* write the data. + channel.flush() + + let writtenMessage = try channel.assertReadDataOutbound() + + // Make sure both messages have been framed together in the ByteBuffer. + XCTAssertEqual( + writtenMessage.data, + .byteBuffer( + .init(bytes: [ + // First message + 0, // Compression disabled + 0, 0, 0, 4, // Message length + 1, 1, 1, 1, // First message data + + // Second message + 0, // Compression disabled + 0, 0, 0, 4, // Message length + 2, 2, 2, 2, // Second message data + ]) + ) + ) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + } + + func testMessageAndStatusAreNotReordered() throws { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // Write back server's initial metadata + let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: [:])) + XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) + + // Read out the metadata + _ = try channel.readOutbound(as: HTTP2Frame.FramePayload.self) + + // This is where this test actually begins. We want to write a message followed + // by status and trailers, and only flush after both writes. + // Because messages are buffered and potentially bundled together in a single + // ByteBuffer by the GPRCMessageFramer, we want to make sure that the status + // and trailers won't be written before the messages. + + // Write back message and make sure nothing's written in the channel. + XCTAssertNoThrow(channel.write(RPCResponsePart.message([UInt8](repeating: 1, count: 4)))) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + // Write status + metadata and make sure nothing's written. + XCTAssertNoThrow(channel.write(RPCResponsePart.status(.init(code: .ok, message: ""), [:]))) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + // Now flush and check we *do* write the data in the right order: message first, + // trailers second. + channel.flush() + + let writtenMessage = try channel.assertReadDataOutbound() + + // Make sure we first get message. + XCTAssertEqual( + writtenMessage.data, + .byteBuffer( + .init(bytes: [ + // First message + 0, // Compression disabled + 0, 0, 0, 4, // Message length + 1, 1, 1, 1, // First message data + ]) + ) + ) + XCTAssertFalse(writtenMessage.endStream) + + // Make sure we get trailers. + let writtenTrailers = try channel.assertReadHeadersOutbound() + XCTAssertEqual(writtenTrailers.headers, ["grpc-status": "0"]) + XCTAssertTrue(writtenTrailers.endStream) + + // Make sure we get nothing else. + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + } +} + +extension EmbeddedChannel { + fileprivate func assertReadHeadersOutbound() throws -> HTTP2Frame.FramePayload.Headers { + guard + case .headers(let writtenHeaders) = try XCTUnwrap( + try self.readOutbound(as: HTTP2Frame.FramePayload.self) + ) + else { + throw TestError.assertionFailure("Expected to write headers") + } + return writtenHeaders + } + + fileprivate func assertReadDataOutbound() throws -> HTTP2Frame.FramePayload.Data { + guard + case .data(let writtenMessage) = try XCTUnwrap( + try self.readOutbound(as: HTTP2Frame.FramePayload.self) + ) + else { + throw TestError.assertionFailure("Expected to write data") + } + return writtenMessage + } +} + +private enum TestError: Error { + case assertionFailure(String) +} From f7dc1aefd283a2cd3c31de8f9d8fc3b13ca83ee4 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 22 Mar 2024 08:21:00 +0000 Subject: [PATCH 271/580] Rename GRPCStreamStateMachine receive methods (#1836) --- .../GRPCStreamStateMachine.swift | 66 +++--- .../Server/GRPCServerStreamHandler.swift | 4 +- .../GRPCStreamStateMachineTests.swift | 202 +++++++++--------- 3 files changed, 136 insertions(+), 136 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index c3eda1363..8abd927f8 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -367,25 +367,25 @@ struct GRPCStreamStateMachine { case rejectRPC(trailers: HPACKHeaders) } - mutating func receive(metadata: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { + mutating func receive(headers: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.configuration { case .client: - return try self.clientReceive(metadata: metadata, endStream: endStream) + return try self.clientReceive(headers: headers, endStream: endStream) case .server(let serverConfiguration): return try self.serverReceive( - metadata: metadata, + headers: headers, endStream: endStream, configuration: serverConfiguration ) } } - mutating func receive(message: ByteBuffer, endStream: Bool) throws { + mutating func receive(buffer: ByteBuffer, endStream: Bool) throws { switch self.configuration { case .client: - try self.clientReceive(bytes: message, endStream: endStream) + try self.clientReceive(buffer: buffer, endStream: endStream) case .server: - try self.serverReceive(bytes: message, endStream: endStream) + try self.serverReceive(buffer: buffer, endStream: endStream) } } @@ -722,12 +722,12 @@ extension GRPCStreamStateMachine { } private mutating func clientReceive( - metadata: HPACKHeaders, + headers: HPACKHeaders, endStream: Bool ) throws -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): - switch (self.clientValidateHeadersReceivedFromServer(metadata), endStream) { + switch (self.clientValidateHeadersReceivedFromServer(headers), endStream) { case (.invalid(let action), true): // The headers are invalid, but the server signalled that it was // closing the stream, so close both client and server. @@ -739,9 +739,9 @@ extension GRPCStreamStateMachine { case (.valid, true): // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) - return try self.validateAndReturnStatusAndMetadata(metadata) + return try self.validateAndReturnStatusAndMetadata(headers) case (.valid, false): - switch self.processInboundEncoding(metadata) { + switch self.processInboundEncoding(headers) { case .error(let failure): return failure case .success(let inboundEncoding): @@ -759,7 +759,7 @@ extension GRPCStreamStateMachine { decompressor: decompressor ) ) - return .receivedMetadata(Metadata(headers: metadata)) + return .receivedMetadata(Metadata(headers: headers)) } } @@ -772,10 +772,10 @@ extension GRPCStreamStateMachine { if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) } - return try self.validateAndReturnStatusAndMetadata(metadata) + return try self.validateAndReturnStatusAndMetadata(headers) case .clientClosedServerIdle(let state): - switch (self.clientValidateHeadersReceivedFromServer(metadata), endStream) { + switch (self.clientValidateHeadersReceivedFromServer(headers), endStream) { case (.invalid(let action), true): // The headers are invalid, but the server signalled that it was // closing the stream, so close the server side too. @@ -787,9 +787,9 @@ extension GRPCStreamStateMachine { case (.valid, true): // This is a trailers-only response: close server. self.state = .clientClosedServerClosed(.init(previousState: state)) - return try self.validateAndReturnStatusAndMetadata(metadata) + return try self.validateAndReturnStatusAndMetadata(headers) case (.valid, false): - switch self.processInboundEncoding(metadata) { + switch self.processInboundEncoding(headers) { case .error(let failure): return failure case .success(let inboundEncoding): @@ -799,7 +799,7 @@ extension GRPCStreamStateMachine { decompressionAlgorithm: inboundEncoding ) ) - return .receivedMetadata(Metadata(headers: metadata)) + return .receivedMetadata(Metadata(headers: headers)) } } @@ -812,7 +812,7 @@ extension GRPCStreamStateMachine { if endStream { self.state = .clientClosedServerClosed(.init(previousState: state)) } - return try self.validateAndReturnStatusAndMetadata(metadata) + return try self.validateAndReturnStatusAndMetadata(headers) case .clientClosedServerClosed: // We could end up here if we received a grpc-status header in a previous @@ -821,7 +821,7 @@ extension GRPCStreamStateMachine { // We wouldn't want to throw in that scenario, so we just ignore it. // Note that we don't want to ignore it if EOS is not set here though, as // then it would be an invalid payload. - if !endStream || metadata.count > 0 { + if !endStream || headers.count > 0 { try self.invalidState( "Server is closed, nothing could have been sent." ) @@ -838,7 +838,7 @@ extension GRPCStreamStateMachine { } } - private mutating func clientReceive(bytes: ByteBuffer, endStream: Bool) throws { + private mutating func clientReceive(buffer: ByteBuffer, endStream: Bool) throws { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: @@ -850,7 +850,7 @@ extension GRPCStreamStateMachine { "Server cannot have sent a message before sending the initial metadata." ) case .clientOpenServerOpen(var state): - try state.deframer.process(buffer: bytes) { deframedMessage in + try state.deframer.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } if endStream { @@ -862,7 +862,7 @@ extension GRPCStreamStateMachine { // The client may have sent the end stream and thus it's closed, // but the server may still be responding. // The client must have a deframer set up, so force-unwrap is okay. - try state.deframer!.process(buffer: bytes) { deframedMessage in + try state.deframer!.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } if endStream { @@ -1106,13 +1106,13 @@ extension GRPCStreamStateMachine { } private mutating func serverReceive( - metadata: HPACKHeaders, + headers: HPACKHeaders, endStream: Bool, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration ) throws -> OnMetadataReceived { switch self.state { case .clientIdleServerIdle(let state): - let contentType = metadata.firstString(forKey: .contentType) + let contentType = headers.firstString(forKey: .contentType) .flatMap { ContentType(value: $0) } if contentType == nil { self.state = .clientOpenServerClosed(.init(previousState: state)) @@ -1123,7 +1123,7 @@ extension GRPCStreamStateMachine { return .rejectRPC(trailers: trailers) } - let path = metadata.firstString(forKey: .path) + let path = headers.firstString(forKey: .path) .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } if path == nil { return self.closeServerAndBuildRejectRPCAction( @@ -1136,7 +1136,7 @@ extension GRPCStreamStateMachine { ) } - let scheme = metadata.firstString(forKey: .scheme) + let scheme = headers.firstString(forKey: .scheme) .flatMap { Scheme(rawValue: $0) } if scheme == nil { return self.closeServerAndBuildRejectRPCAction( @@ -1149,7 +1149,7 @@ extension GRPCStreamStateMachine { ) } - guard let method = metadata.firstString(forKey: .method), method == "POST" else { + guard let method = headers.firstString(forKey: .method), method == "POST" else { return self.closeServerAndBuildRejectRPCAction( currentState: state, endStream: endStream, @@ -1160,7 +1160,7 @@ extension GRPCStreamStateMachine { ) } - guard let te = metadata.firstString(forKey: .te), te == "trailers" else { + guard let te = headers.firstString(forKey: .te), te == "trailers" else { return self.closeServerAndBuildRejectRPCAction( currentState: state, endStream: endStream, @@ -1178,7 +1178,7 @@ extension GRPCStreamStateMachine { // Firstly, find out if we support the client's chosen encoding, and reject // the RPC if we don't. let inboundEncoding: CompressionAlgorithm - let encodingValues = metadata.values( + let encodingValues = headers.values( forHeader: GRPCHTTP2Keys.encoding.rawValue, canonicalForm: true ) @@ -1236,7 +1236,7 @@ extension GRPCStreamStateMachine { // 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 - let clientAdvertisedEncodings = metadata.values( + let clientAdvertisedEncodings = headers.values( forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue, canonicalForm: true ) @@ -1280,7 +1280,7 @@ extension GRPCStreamStateMachine { ) } - return .receivedMetadata(Metadata(headers: metadata)) + return .receivedMetadata(Metadata(headers: headers)) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: try self.invalidState( "Client shouldn't have sent metadata twice." @@ -1292,7 +1292,7 @@ extension GRPCStreamStateMachine { } } - private mutating func serverReceive(bytes: ByteBuffer, endStream: Bool) throws { + private mutating func serverReceive(buffer: ByteBuffer, endStream: Bool) throws { switch self.state { case .clientIdleServerIdle: try self.invalidState( @@ -1301,7 +1301,7 @@ extension GRPCStreamStateMachine { case .clientOpenServerIdle(var state): // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. - try state.deframer!.process(buffer: bytes) { deframedMessage in + try state.deframer!.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } @@ -1311,7 +1311,7 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerIdle(state) } case .clientOpenServerOpen(var state): - try state.deframer.process(buffer: bytes) { deframedMessage in + try state.deframer.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index c4da0d4ed..e7317df5a 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -63,7 +63,7 @@ extension GRPCServerStreamHandler { switch frameData.data { case .byteBuffer(let buffer): do { - try self.stateMachine.receive(message: buffer, endStream: endStream) + try self.stateMachine.receive(buffer: buffer, endStream: endStream) loop: while true { switch self.stateMachine.nextInboundMessage() { case .receiveMessage(let message): @@ -86,7 +86,7 @@ extension GRPCServerStreamHandler { case .headers(let headers): do { let action = try self.stateMachine.receive( - metadata: headers.headers, + headers: headers.headers, endStream: headers.endStream ) switch action { diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 53470c9aa..01e329420 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -158,14 +158,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) case .clientOpenServerClosed: // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) case .clientClosedServerIdle: // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) @@ -175,18 +175,18 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) case .clientClosedServerClosed: // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server - XCTAssertNoThrow(try stateMachine.receive(metadata: serverMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) } return stateMachine @@ -301,7 +301,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false) + try stateMachine.receive(headers: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") @@ -316,7 +316,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive metadata with unexpected non-200 status code let action = try stateMachine.receive( - metadata: [GRPCHTTP2Keys.status.rawValue: "300"], + headers: [GRPCHTTP2Keys.status.rawValue: "300"], endStream: false ) @@ -338,7 +338,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive metadata = open server let action = try stateMachine.receive( - metadata: [ + headers: [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", @@ -369,7 +369,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, try stateMachine.receive( - metadata: [ + headers: [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", @@ -388,7 +388,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Now make sure everything works well if we include grpc-status let action = try stateMachine.receive( - metadata: [ + headers: [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, @@ -423,7 +423,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: false) + try stateMachine.receive(headers: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") @@ -439,7 +439,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive an end trailer XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true) + try stateMachine.receive(headers: .init(), endStream: true) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") @@ -459,7 +459,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { )!, "custom-key": "custom-value", ] - let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) + let trailers = try stateMachine.receive(headers: trailersOnlyResponse, endStream: true) switch trailers { case .receivedStatusAndMetadata(let status, let metadata): XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) @@ -482,7 +482,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive an end trailer let action = try stateMachine.receive( - metadata: [ + headers: [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, @@ -514,7 +514,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive another end trailer XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .init(), endStream: true) + try stateMachine.receive(headers: .init(), endStream: true) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") @@ -534,7 +534,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { )!, "custom-key": "custom-value", ] - let trailers = try stateMachine.receive(metadata: trailersOnlyResponse, endStream: true) + let trailers = try stateMachine.receive(headers: trailersOnlyResponse, endStream: true) switch trailers { case .receivedStatusAndMetadata(let status, let metadata): XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) @@ -557,7 +557,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Close server again (endStream = true) and assert we don't throw. // This can happen if the previous close was caused by a grpc-status header // and then the server sends an empty frame with EOS set. - XCTAssertEqual(try stateMachine.receive(metadata: .init(), endStream: true), .doNothing) + XCTAssertEqual(try stateMachine.receive(headers: .init(), endStream: true), .doNothing) } // - MARK: Receive message @@ -567,7 +567,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -583,7 +583,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -598,8 +598,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) } } @@ -609,7 +609,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") @@ -744,7 +744,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -773,7 +773,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -787,7 +787,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let originalMessage = [UInt8]([42, 42, 43, 43]) let receivedBytes = try self.frameMessage(originalMessage, compress: true) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -801,10 +801,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) @@ -818,7 +818,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -837,10 +837,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) // Close server - XCTAssertNoThrow(try stateMachine.receive(metadata: .serverTrailers, endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) // Close client XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) @@ -872,7 +872,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Server sends initial metadata let serverInitialHeadersAction = try stateMachine.receive( - metadata: .serverInitialMetadata, + headers: .serverInitialMetadata, endStream: false ) XCTAssertEqual( @@ -900,8 +900,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) let secondResponseBytes = [UInt8]([8, 9, 10]) let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) - try stateMachine.receive(message: firstResponse, endStream: false) - try stateMachine.receive(message: secondResponse, endStream: false) + try stateMachine.receive(buffer: firstResponse, endStream: false) + try stateMachine.receive(buffer: secondResponse, endStream: false) // Make sure messages have arrived XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) @@ -913,7 +913,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Server ends let metadataReceivedAction = try stateMachine.receive( - metadata: .serverTrailers, + headers: .serverTrailers, endStream: true ) let receivedMetadata = { @@ -959,7 +959,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Server sends initial metadata let serverInitialHeadersAction = try stateMachine.receive( - metadata: .serverInitialMetadata, + headers: .serverInitialMetadata, endStream: false ) XCTAssertEqual( @@ -978,8 +978,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) let secondResponseBytes = [UInt8]([8, 9, 10]) let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) - try stateMachine.receive(message: firstResponse, endStream: false) - try stateMachine.receive(message: secondResponse, endStream: false) + try stateMachine.receive(buffer: firstResponse, endStream: false) + try stateMachine.receive(buffer: secondResponse, endStream: false) // Make sure messages have arrived XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) @@ -988,7 +988,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Server ends let metadataReceivedAction = try stateMachine.receive( - metadata: .serverTrailers, + headers: .serverTrailers, endStream: true ) let receivedMetadata = { @@ -1034,7 +1034,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Server sends initial metadata let serverInitialHeadersAction = try stateMachine.receive( - metadata: .serverInitialMetadata, + headers: .serverInitialMetadata, endStream: false ) XCTAssertEqual( @@ -1056,8 +1056,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) let secondResponseBytes = [UInt8]([8, 9, 10]) let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) - try stateMachine.receive(message: firstResponse, endStream: false) - try stateMachine.receive(message: secondResponse, endStream: false) + try stateMachine.receive(buffer: firstResponse, endStream: false) + try stateMachine.receive(buffer: secondResponse, endStream: false) // Make sure messages have arrived XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) @@ -1066,7 +1066,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Server ends let metadataReceivedAction = try stateMachine.receive( - metadata: .serverTrailers, + headers: .serverTrailers, endStream: true ) let receivedMetadata = { @@ -1110,15 +1110,15 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { break case .clientOpenServerIdle: // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) case .clientOpenServerOpen: // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) case .clientOpenServerClosed: // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) // Close server @@ -1130,23 +1130,23 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) case .clientClosedServerIdle: // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) case .clientClosedServerOpen: // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) case .clientClosedServerClosed: // Open client - XCTAssertNoThrow(try stateMachine.receive(metadata: clientMetadata, endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) // Open server XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Close server XCTAssertNoThrow( try stateMachine.send( @@ -1475,7 +1475,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testReceiveMetadataWhenClientIdleAndServerIdle() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) XCTAssertEqual( action, .receivedMetadata(Metadata(headers: .clientInitialMetadata)) @@ -1485,7 +1485,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - let action = try stateMachine.receive(metadata: .clientInitialMetadata, endStream: true) + let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: true) XCTAssertEqual( action, .receivedMetadata(Metadata(headers: .clientInitialMetadata)) @@ -1496,7 +1496,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithoutContentType, + headers: .receivedWithoutContentType, endStream: false ) @@ -1510,7 +1510,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithInvalidContentType, + headers: .receivedWithInvalidContentType, endStream: false ) @@ -1524,7 +1524,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithoutEndpoint, + headers: .receivedWithoutEndpoint, endStream: false ) @@ -1545,7 +1545,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithoutTE, + headers: .receivedWithoutTE, endStream: false ) @@ -1567,7 +1567,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithInvalidTE, + headers: .receivedWithInvalidTE, endStream: false ) @@ -1589,7 +1589,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithoutMethod, + headers: .receivedWithoutMethod, endStream: false ) @@ -1611,7 +1611,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithInvalidMethod, + headers: .receivedWithInvalidMethod, endStream: false ) @@ -1633,7 +1633,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithoutScheme, + headers: .receivedWithoutScheme, endStream: false ) @@ -1654,7 +1654,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) let action = try stateMachine.receive( - metadata: .receivedWithInvalidScheme, + headers: .receivedWithInvalidScheme, endStream: false ) @@ -1677,7 +1677,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try opening client with a compression algorithm that is not accepted // by the server. let action = try stateMachine.receive( - metadata: .clientInitialMetadataWithGzipCompression, + headers: .clientInitialMetadataWithGzipCompression, endStream: false ) @@ -1705,7 +1705,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try receiving initial metadata again - should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") @@ -1717,7 +1717,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") @@ -1729,7 +1729,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") @@ -1741,7 +1741,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") @@ -1753,7 +1753,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") @@ -1765,7 +1765,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(metadata: .clientInitialMetadata, endStream: false) + try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") @@ -1779,7 +1779,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't have received a message if client is idle.") @@ -1790,13 +1790,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) // Receive messages successfully: the second one should close client. - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Verify client is now closed XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") @@ -1807,13 +1807,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Receive messages successfully: the second one should close client. - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Verify client is now closed XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") @@ -1824,7 +1824,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) // Client is not done sending request, don't fail. - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: false)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) } func testReceiveMessageWhenClientClosedAndServerIdle() { @@ -1832,7 +1832,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") @@ -1844,7 +1844,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") @@ -1856,7 +1856,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.receive(message: .init(), endStream: false) + try stateMachine.receive(buffer: .init(), endStream: false) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") @@ -1979,7 +1979,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Send another message XCTAssertNoThrow(try stateMachine.send(message: [43, 43], endStream: false)) @@ -2048,7 +2048,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -2063,7 +2063,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let originalMessage = [UInt8]([42, 42, 43, 43]) let receivedBytes = try self.frameMessage(originalMessage, compress: true) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -2077,7 +2077,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) // Close server XCTAssertNoThrow( @@ -2104,10 +2104,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Even though the client is closed, because the server received a message // while it was still open, we must get the message now. @@ -2123,7 +2123,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(message: receivedBytes, endStream: false) + try stateMachine.receive(buffer: receivedBytes, endStream: false) // Close server XCTAssertNoThrow( @@ -2134,7 +2134,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) // Close client - XCTAssertNoThrow(try stateMachine.receive(message: .init(), endStream: true)) + XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Even though the client and server are closed, because the server received // a message while the client was still open, we must get the message now. @@ -2149,7 +2149,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Client sends metadata let receiveMetadataAction = try stateMachine.receive( - metadata: .clientInitialMetadata, + headers: .clientInitialMetadata, endStream: false ) XCTAssertEqual( @@ -2176,9 +2176,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - try stateMachine.receive(message: firstMessage, endStream: false) + try stateMachine.receive(buffer: firstMessage, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - try stateMachine.receive(message: secondMessage, endStream: false) + try stateMachine.receive(buffer: secondMessage, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Server sends response @@ -2193,7 +2193,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) // Client sends end - try stateMachine.receive(message: ByteBuffer(), endStream: true) + try stateMachine.receive(buffer: ByteBuffer(), endStream: true) // Server ends let response = try stateMachine.send( @@ -2211,7 +2211,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Client sends metadata let receiveMetadataAction = try stateMachine.receive( - metadata: .clientInitialMetadata, + headers: .clientInitialMetadata, endStream: false ) XCTAssertEqual( @@ -2226,13 +2226,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - try stateMachine.receive(message: firstMessage, endStream: false) + try stateMachine.receive(buffer: firstMessage, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - try stateMachine.receive(message: secondMessage, endStream: false) + try stateMachine.receive(buffer: secondMessage, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Client sends end - try stateMachine.receive(message: ByteBuffer(), endStream: true) + try stateMachine.receive(buffer: ByteBuffer(), endStream: true) // Server sends initial metadata let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) @@ -2273,7 +2273,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Client sends metadata let receiveMetadataAction = try stateMachine.receive( - metadata: .clientInitialMetadata, + headers: .clientInitialMetadata, endStream: false ) XCTAssertEqual( @@ -2288,9 +2288,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - try stateMachine.receive(message: firstMessage, endStream: false) + try stateMachine.receive(buffer: firstMessage, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - try stateMachine.receive(message: secondMessage, endStream: false) + try stateMachine.receive(buffer: secondMessage, endStream: false) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Server sends initial metadata @@ -2306,7 +2306,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) // Client sends end - try stateMachine.receive(message: ByteBuffer(), endStream: true) + try stateMachine.receive(buffer: ByteBuffer(), endStream: true) // Server sends response let firstResponse = [UInt8]([5, 6, 7]) From 1acd57508d5037a22ccd0cdb875368eb039d093f Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:11:26 +0000 Subject: [PATCH 272/580] Server Stats Collection (#1834) * Server Stats Collection Motivation: In order to implement the 'runServer' RPC on the WorkerService we need to capture a snapshot with the resorces usage for the server and compute the difference between 2 of these snapshots. Modifications: - Created the ServerStats struct that contains all stats needed in the QPS benchmarking from the server - Implemented the init() that collects all these stats - Implemented the function that computes the differences between 2 snapshots of ServerStats Result: We will be able to implement the `runServer` RPC. --- Package.swift | 6 +- Sources/performance-worker/ServerStats.swift | 130 +++++++++++++++++++ 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 Sources/performance-worker/ServerStats.swift diff --git a/Package.swift b/Package.swift index 83d52b749..699ff4baa 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.58.0" + from: "2.64.0" ), .package( url: "https://github.com/apple/swift-nio-http2.git", @@ -132,6 +132,7 @@ extension Target.Dependency { package: "swift-nio-transport-services" ) static let nioTestUtils: Self = .product(name: "NIOTestUtils", package: "swift-nio") + static let nioFileSystem: Self = .product(name: "_NIOFileSystem", package: "swift-nio") static let logging: Self = .product(name: "Logging", package: "swift-log") static let protobuf: Self = .product(name: "SwiftProtobuf", package: "swift-protobuf") static let protobufPluginLibrary: Self = .product( @@ -251,7 +252,8 @@ extension Target { dependencies: [ .grpcCore, .grpcProtobuf, - .nioCore + .nioCore, + .nioFileSystem ] ) diff --git a/Sources/performance-worker/ServerStats.swift b/Sources/performance-worker/ServerStats.swift new file mode 100644 index 000000000..44d7c8b50 --- /dev/null +++ b/Sources/performance-worker/ServerStats.swift @@ -0,0 +1,130 @@ +/* + * 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 Dispatch +import NIOCore +import NIOFileSystem + +#if canImport(Darwin) +import Darwin +#elseif canImport(Musl) +import Musl +#elseif canImport(Glibc) +import Glibc +#else +let badOS = { fatalError("unsupported OS") }() +#endif + +#if canImport(Darwin) +private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF +#elseif canImport(Musl) || canImport(Glibc) +private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF.rawValue +#endif + +/// Current server stats. +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +internal struct ServerStats: Sendable { + var time: Double + var userTime: Double + var systemTime: Double + var totalCPUTime: UInt64 + var idleCPUTime: UInt64 + + init( + time: Double, + userTime: Double, + systemTime: Double, + totalCPUTime: UInt64, + idleCPUTime: UInt64 + ) { + self.time = time + self.userTime = userTime + self.systemTime = systemTime + self.totalCPUTime = totalCPUTime + self.idleCPUTime = idleCPUTime + } + + init() async throws { + self.time = Double(DispatchTime.now().uptimeNanoseconds) * 1e-9 + var usage = rusage() + if getrusage(OUR_RUSAGE_SELF, &usage) == 0 { + // Adding the seconds with the microseconds transformed into seconds to get the + // real number of seconds as a `Double`. + self.userTime = Double(usage.ru_utime.tv_sec) + Double(usage.ru_utime.tv_usec) * 1e-6 + self.systemTime = Double(usage.ru_stime.tv_sec) + Double(usage.ru_stime.tv_usec) * 1e-6 + } else { + self.userTime = 0 + self.systemTime = 0 + } + let (totalCPUTime, idleCPUTime) = try await ServerStats.getTotalAndIdleCPUTime() + self.totalCPUTime = totalCPUTime + self.idleCPUTime = idleCPUTime + } + + internal func difference(to stats: ServerStats) -> ServerStats { + return ServerStats( + time: self.time - stats.time, + userTime: self.userTime - stats.userTime, + systemTime: self.systemTime - stats.systemTime, + totalCPUTime: self.totalCPUTime - stats.totalCPUTime, + idleCPUTime: self.idleCPUTime - stats.idleCPUTime + ) + } + + /// Computes the total and idle CPU time after extracting stats from the first line of '/proc/stat'. + /// + /// The first line in '/proc/stat' file looks as follows: + /// CPU [user] [nice] [system] [idle] [iowait] [irq] [softirq] + /// The totalCPUTime is computed as follows: + /// total = user + nice + system + idle + private static func getTotalAndIdleCPUTime() async throws -> ( + totalCPUTime: UInt64, idleCPUTime: UInt64 + ) { + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) + let contents: ByteBuffer + do { + contents = try await ByteBuffer( + contentsOf: "/proc/stat", + maximumSizeAllowed: .kilobytes(20) + ) + } catch { + return (0, 0) + } + + let view = contents.readableBytesView + guard let firstNewLineIndex = view.firstIndex(of: UInt8(ascii: "\n")) else { + return (0, 0) + } + let firstLine = String(buffer: ByteBuffer(view[0 ... firstNewLineIndex])) + + let lineComponents = firstLine.components(separatedBy: " ") + if lineComponents.count < 5 || lineComponents[0] != "CPU" { + return (0, 0) + } + + let CPUTime: [UInt64] = lineComponents[1 ... 4].compactMap { UInt64($0) } + if CPUTime.count < 4 { + return (0, 0) + } + + let totalCPUTime = CPUTime.reduce(0, +) + return (totalCPUTime, CPUTime[3]) + + #else + return (0, 0) + #endif + } +} From bc96e8cc8840c36d77c7dfa0a7bbb75462af2ef9 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 22 Mar 2024 13:03:52 +0000 Subject: [PATCH 273/580] Add a closeOutbound func to GRPCStreamStateMachine and remove endStream param from send(message:) (#1837) --- .../GRPCStreamStateMachine.swift | 57 +++++++----- .../Server/GRPCServerStreamHandler.swift | 2 +- .../GRPCStreamStateMachineTests.swift | 93 ++++++++++--------- 3 files changed, 82 insertions(+), 70 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 8abd927f8..eb37b76ff 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -325,20 +325,24 @@ struct GRPCStreamStateMachine { } } - mutating func send(message: [UInt8], endStream: Bool) throws { + mutating func send(message: [UInt8]) throws { switch self.configuration { case .client: - try self.clientSend(message: message, endStream: endStream) + try self.clientSend(message: message) case .server: - if endStream { - try self.invalidState( - "Can't end response stream by sending a message - send(status:metadata:) must be called" - ) - } try self.serverSend(message: message) } } + mutating func closeOutbound() throws { + switch self.configuration { + case .client: + try self.clientCloseOutbound() + case .server: + try self.invalidState("Server cannot call close: it must send status and trailers.") + } + } + mutating func send( status: Status, metadata: Metadata @@ -532,31 +536,36 @@ extension GRPCStreamStateMachine { } } - private mutating func clientSend(message: [UInt8], endStream: Bool) throws { - // Client sends message. + private mutating func clientSend(message: [UInt8]) throws { switch self.state { case .clientIdleServerIdle: try self.invalidState("Client not yet open.") case .clientOpenServerIdle(var state): state.framer.append(message) - if endStream { - self.state = .clientClosedServerIdle(.init(previousState: state)) - } else { - self.state = .clientOpenServerIdle(state) - } + self.state = .clientOpenServerIdle(state) case .clientOpenServerOpen(var state): state.framer.append(message) - if endStream { - self.state = .clientClosedServerOpen(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) - } - case .clientOpenServerClosed(let state): + self.state = .clientOpenServerOpen(state) + case .clientOpenServerClosed: // The server has closed, so it makes no sense to send the rest of the request. - // However, do close if endStream is set. - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } + () + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + try self.invalidState( + "Client is closed, cannot send a message." + ) + } + } + + private mutating func clientCloseOutbound() throws { + switch self.state { + case .clientIdleServerIdle: + try self.invalidState("Client not yet open.") + case .clientOpenServerIdle(let state): + self.state = .clientClosedServerIdle(.init(previousState: state)) + case .clientOpenServerOpen(let state): + self.state = .clientClosedServerOpen(.init(previousState: state)) + case .clientOpenServerClosed(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: try self.invalidState( "Client is closed, cannot send a message." diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index e7317df5a..22cd50330 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -153,7 +153,7 @@ extension GRPCServerStreamHandler { case .message(let message): do { - try self.stateMachine.send(message: message, endStream: false) + try self.stateMachine.send(message: message) // TODO: move the promise handling into the state machine promise?.succeed() } catch { diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 01e329420..29261257f 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -170,21 +170,21 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow(try stateMachine.closeOutbound()) case .clientClosedServerOpen: // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow(try stateMachine.closeOutbound()) case .clientClosedServerClosed: // Open client XCTAssertNoThrow(try stateMachine.send(metadata: [])) // Open server XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Close server XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) } @@ -238,7 +238,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try to send a message without opening (i.e. without sending initial metadata) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client not yet open.") @@ -252,7 +252,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: targetState) // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [])) } } @@ -266,7 +266,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") @@ -637,7 +637,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) let expectedBytes: [UInt8] = [ 0, // compression flag: unset @@ -663,7 +663,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage)) let request = try stateMachine.nextOutboundMessage() let framedMessage = try self.frameMessage(originalMessage, compress: true) @@ -679,7 +679,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage)) let request = try stateMachine.nextOutboundMessage() let framedMessage = try self.frameMessage(originalMessage, compress: true) @@ -694,7 +694,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Queue a message, but assert the action is .noMoreMessages nevertheless, // because the server is closed. - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) } @@ -702,7 +702,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) // Send a message and close client - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Make sure that getting the next outbound message _does_ return the message // we have enqueued. @@ -722,7 +723,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) // Send a message and close client - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: true)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Make sure that getting the next outbound message _does_ return the message // we have enqueued. @@ -741,13 +743,13 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) // Send a message - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) // Close server XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Even though we have enqueued a message, don't send it, because the server // is closed. @@ -821,7 +823,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { try stateMachine.receive(buffer: receivedBytes, endStream: false) // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Even though the client is closed, because it received a message while open, // we must get the message now. @@ -843,7 +845,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) // Close client - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: true)) + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Even though the client is closed, because it received a message while open, // we must get the message now. @@ -889,7 +891,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let message = [UInt8]([1, 2, 3, 4]) let framedMessage = try self.frameMessage(message, compress: false) - try stateMachine.send(message: message, endStream: false) + try stateMachine.send(message: message) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) @@ -909,7 +911,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) // Client sends end - try stateMachine.send(message: [], endStream: true) + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Server ends let metadataReceivedAction = try stateMachine.receive( @@ -953,7 +955,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let message = [UInt8]([1, 2, 3, 4]) let framedMessage = try self.frameMessage(message, compress: false) - try stateMachine.send(message: message, endStream: true) + XCTAssertNoThrow(try stateMachine.send(message: message)) + XCTAssertNoThrow(try stateMachine.closeOutbound()) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) @@ -1028,7 +1031,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let message = [UInt8]([1, 2, 3, 4]) let framedMessage = try self.frameMessage(message, compress: false) - try stateMachine.send(message: message, endStream: false) + try stateMachine.send(message: message) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) @@ -1046,8 +1049,8 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ]) ) - // Client sends end - try stateMachine.send(message: [], endStream: true) + // Client closes + XCTAssertNoThrow(try stateMachine.closeOutbound()) // Server sends response XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -1239,7 +1242,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -1255,7 +1258,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Now send a message XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -1269,7 +1272,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [])) } func testSendMessageWhenClientOpenAndServerClosed() { @@ -1278,7 +1281,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1290,7 +1293,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -1305,7 +1308,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending a message: even though client is closed, we should send it // because it may be expecting a response. - XCTAssertNoThrow(try stateMachine.send(message: [], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [])) } func testSendMessageWhenClientClosedAndServerClosed() { @@ -1314,7 +1317,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1360,7 +1363,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1382,7 +1385,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1426,7 +1429,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1448,7 +1451,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: [], endStream: false) + try stateMachine.send(message: []) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1906,7 +1909,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) let response = try stateMachine.nextOutboundMessage() let expectedBytes: [UInt8] = [ @@ -1929,7 +1932,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage, endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage)) let response = try stateMachine.nextOutboundMessage() let framedMessage = try self.frameMessage(originalMessage, compress: true) @@ -1940,7 +1943,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Send message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), @@ -1976,13 +1979,13 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Send a message - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) // Close client XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Send another message - XCTAssertNoThrow(try stateMachine.send(message: [43, 43], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [43, 43])) // Make sure that getting the next outbound message _does_ return the message // we have enqueued. @@ -2006,7 +2009,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) // Send a message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], endStream: false)) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), @@ -2185,8 +2188,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstResponse = [UInt8]([5, 6, 7]) let secondResponse = [UInt8]([8, 9, 10]) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse, endStream: false) - try stateMachine.send(message: secondResponse, endStream: false) + try stateMachine.send(message: firstResponse) + try stateMachine.send(message: secondResponse) // Make sure messages are outbound let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) @@ -2250,8 +2253,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstResponse = [UInt8]([5, 6, 7]) let secondResponse = [UInt8]([8, 9, 10]) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse, endStream: false) - try stateMachine.send(message: secondResponse, endStream: false) + try stateMachine.send(message: firstResponse) + try stateMachine.send(message: secondResponse) // Make sure messages are outbound let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) @@ -2312,8 +2315,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstResponse = [UInt8]([5, 6, 7]) let secondResponse = [UInt8]([8, 9, 10]) XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse, endStream: false) - try stateMachine.send(message: secondResponse, endStream: false) + try stateMachine.send(message: firstResponse) + try stateMachine.send(message: secondResponse) // Make sure messages are outbound let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) From 9c91043a8b463822b49cea240fef4ef7ecb10d0b Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Mon, 25 Mar 2024 12:54:24 +0000 Subject: [PATCH 274/580] Add GRPCServerFlushNotificationHandler (#1839) * Add GRPCServerFlushNotificationHandler * Change channel outbound handler required typealiases to Any --- .../GRPCServerFlushNotificationHandler.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift diff --git a/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift new file mode 100644 index 000000000..85aa23608 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift @@ -0,0 +1,36 @@ +/* + * 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 NIOCore + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class GRPCServerFlushNotificationHandler: ChannelOutboundHandler { + typealias OutboundIn = Any + typealias OutboundOut = Any + + private let serverConnectionManagementHandler: ServerConnectionManagementHandler + + init( + serverConnectionManagementHandler: ServerConnectionManagementHandler + ) { + self.serverConnectionManagementHandler = serverConnectionManagementHandler + } + + func flush(context: ChannelHandlerContext) { + self.serverConnectionManagementHandler.syncView.connectionWillFlush() + context.flush() + } +} From 847a9348e445dde4ed42eed9529c687727a3473e Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 26 Mar 2024 16:29:35 +0000 Subject: [PATCH 275/580] Add GRPCClientStreamHandler (#1838) --- .../Client/GRPCClientStreamHandler.swift | 238 ++++++ .../GRPCStreamStateMachine.swift | 34 +- .../Client/GRPCClientStreamHandlerTests.swift | 724 ++++++++++++++++++ .../Server/GRPCServerStreamHandlerTests.swift | 14 +- 4 files changed, 995 insertions(+), 15 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift new file mode 100644 index 000000000..6db75f843 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -0,0 +1,238 @@ +/* + * 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 NIOCore +import NIOHTTP2 + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class GRPCClientStreamHandler: ChannelDuplexHandler { + typealias InboundIn = HTTP2Frame.FramePayload + typealias InboundOut = RPCResponsePart + + typealias OutboundIn = RPCRequestPart + typealias OutboundOut = HTTP2Frame.FramePayload + + private var stateMachine: GRPCStreamStateMachine + + private var isReading = false + private var flushPending = false + + init( + methodDescriptor: MethodDescriptor, + scheme: Scheme, + outboundEncoding: CompressionAlgorithm, + acceptedEncodings: [CompressionAlgorithm], + maximumPayloadSize: Int, + skipStateMachineAssertions: Bool = false + ) { + self.stateMachine = .init( + configuration: .client( + .init( + methodDescriptor: methodDescriptor, + scheme: scheme, + outboundEncoding: outboundEncoding, + acceptedEncodings: acceptedEncodings + ) + ), + maximumPayloadSize: maximumPayloadSize, + skipAssertions: skipStateMachineAssertions + ) + } +} + +// - MARK: ChannelInboundHandler + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCClientStreamHandler { + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + self.isReading = true + let frame = self.unwrapInboundIn(data) + switch frame { + case .data(let frameData): + let endStream = frameData.endStream + switch frameData.data { + case .byteBuffer(let buffer): + do { + try self.stateMachine.receive(buffer: buffer, endStream: endStream) + loop: while true { + switch self.stateMachine.nextInboundMessage() { + case .receiveMessage(let message): + context.fireChannelRead(self.wrapInboundOut(.message(message))) + case .awaitMoreMessages: + break loop + case .noMoreMessages: + context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + break loop + } + } + } catch { + context.fireErrorCaught(error) + } + + case .fileRegion: + preconditionFailure("Unexpected IOData.fileRegion") + } + + case .headers(let headers): + do { + let action = try self.stateMachine.receive( + headers: headers.headers, + endStream: headers.endStream + ) + switch action { + case .receivedMetadata(let metadata): + context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) + + case .rejectRPC: + throw RPCError( + code: .internalError, + message: "Client cannot get rejectRPC." + ) + + case .receivedStatusAndMetadata(let status, let metadata): + context.fireChannelRead(self.wrapInboundOut(.status(status, metadata))) + + case .doNothing: + () + } + } catch { + context.fireErrorCaught(error) + } + + case .ping, .goAway, .priority, .rstStream, .settings, .pushPromise, .windowUpdate, + .alternativeService, .origin: + () + } + } + + func channelReadComplete(context: ChannelHandlerContext) { + self.isReading = false + if self.flushPending { + self.flushPending = false + self.flush(context: context) + } + context.fireChannelReadComplete() + } + + func handlerRemoved(context: ChannelHandlerContext) { + self.stateMachine.tearDown() + } +} + +// - MARK: ChannelOutboundHandler + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCClientStreamHandler { + func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + switch self.unwrapOutboundIn(data) { + case .metadata(let metadata): + do { + self.flushPending = true + let headers = try self.stateMachine.send(metadata: metadata) + context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: nil) + // TODO: move the promise handling into the state machine + promise?.succeed() + } catch { + context.fireErrorCaught(error) + // TODO: move the promise handling into the state machine + promise?.fail(error) + } + + case .message(let message): + do { + try self.stateMachine.send(message: message) + // TODO: move the promise handling into the state machine + promise?.succeed() + } catch { + context.fireErrorCaught(error) + // TODO: move the promise handling into the state machine + promise?.fail(error) + } + } + } + + func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise?) { + switch mode { + case .output, .all: + do { + try self.stateMachine.closeOutbound() + // Force a flush by calling _flush + // (otherwise, we'd skip flushing if we're in a read loop) + self._flush(context: context) + context.close(mode: mode, promise: promise) + } catch { + promise?.fail(error) + context.fireErrorCaught(error) + } + + case .input: + context.close(mode: .input, promise: promise) + } + } + + func flush(context: ChannelHandlerContext) { + if self.isReading { + // We don't want to flush yet if we're still in a read loop. + self.flushPending = true + return + } + + self._flush(context: context) + } + + private func _flush(context: ChannelHandlerContext) { + do { + loop: while true { + switch try self.stateMachine.nextOutboundMessage() { + case .sendMessage(let byteBuffer): + self.flushPending = true + context.write( + self.wrapOutboundOut(.data(.init(data: .byteBuffer(byteBuffer)))), + promise: nil + ) + + case .noMoreMessages: + // Write an empty data frame with the EOS flag set, to signal the RPC + // request is now finished. + context.write( + self.wrapOutboundOut( + HTTP2Frame.FramePayload.data( + .init( + data: .byteBuffer(.init()), + endStream: true + ) + ) + ), + promise: nil + ) + + context.flush() + break loop + + case .awaitMoreMessages: + if self.flushPending { + self.flushPending = false + context.flush() + } + break loop + } + } + } catch { + context.fireErrorCaught(error) + } + } +} diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index eb37b76ff..41961b568 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -373,8 +373,12 @@ struct GRPCStreamStateMachine { mutating func receive(headers: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { switch self.configuration { - case .client: - return try self.clientReceive(headers: headers, endStream: endStream) + case .client(let clientConfiguration): + return try self.clientReceive( + headers: headers, + endStream: endStream, + configuration: clientConfiguration + ) case .server(let serverConfiguration): return try self.serverReceive( headers: headers, @@ -567,9 +571,7 @@ extension GRPCStreamStateMachine { case .clientOpenServerClosed(let state): self.state = .clientClosedServerClosed(.init(previousState: state)) case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState( - "Client is closed, cannot send a message." - ) + try self.invalidState("Client is already closed.") } } @@ -665,7 +667,7 @@ extension GRPCStreamStateMachine { .receivedStatusAndMetadata( status: .init( code: .internalError, - message: "Missing \(GRPCHTTP2Keys.contentType) header" + message: "Missing \(GRPCHTTP2Keys.contentType.rawValue) header" ), metadata: Metadata(headers: metadata) ) @@ -680,10 +682,15 @@ extension GRPCStreamStateMachine { case success(CompressionAlgorithm) } - private func processInboundEncoding(_ metadata: HPACKHeaders) -> ProcessInboundEncodingResult { + private func processInboundEncoding( + headers: HPACKHeaders, + configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration + ) -> ProcessInboundEncodingResult { let inboundEncoding: CompressionAlgorithm - if let serverEncoding = metadata.first(name: GRPCHTTP2Keys.encoding.rawValue) { - guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding) else { + if let serverEncoding = headers.first(name: GRPCHTTP2Keys.encoding.rawValue) { + guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding), + configuration.acceptedEncodings.contains(parsedEncoding) + else { return .error( .receivedStatusAndMetadata( status: .init( @@ -691,7 +698,7 @@ extension GRPCStreamStateMachine { message: "The server picked a compression algorithm ('\(serverEncoding)') the client does not know about." ), - metadata: Metadata(headers: metadata) + metadata: Metadata(headers: headers) ) ) } @@ -732,7 +739,8 @@ extension GRPCStreamStateMachine { private mutating func clientReceive( headers: HPACKHeaders, - endStream: Bool + endStream: Bool, + configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration ) throws -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): @@ -750,7 +758,7 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerClosed(.init(previousState: state)) return try self.validateAndReturnStatusAndMetadata(headers) case (.valid, false): - switch self.processInboundEncoding(headers) { + switch self.processInboundEncoding(headers: headers, configuration: configuration) { case .error(let failure): return failure case .success(let inboundEncoding): @@ -798,7 +806,7 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerClosed(.init(previousState: state)) return try self.validateAndReturnStatusAndMetadata(headers) case (.valid, false): - switch self.processInboundEncoding(headers) { + switch self.processInboundEncoding(headers: headers, configuration: configuration) { case .error(let failure): return failure case .success(let inboundEncoding): diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift new file mode 100644 index 000000000..3b0d461b1 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift @@ -0,0 +1,724 @@ +/* + * 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 NIOCore +import NIOEmbedded +import NIOHPACK +import NIOHTTP1 +import NIOHTTP2 +import XCTest + +@testable import GRPCHTTP2Core + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class GRPCClientStreamHandlerTests: XCTestCase { + func testH2FramesAreIgnored() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + let framesToBeIgnored: [HTTP2Frame.FramePayload] = [ + .ping(.init(), ack: false), + .goAway(lastStreamID: .rootStream, errorCode: .cancel, opaqueData: nil), + // TODO: add .priority(StreamPriorityData) - right now, StreamPriorityData's + // initialiser is internal, so I can't create one of these frames. + .rstStream(.cancel), + .settings(.ack), + .pushPromise(.init(pushedStreamID: .maxID, headers: [:])), + .windowUpdate(windowSizeIncrement: 4), + .alternativeService(origin: nil, field: nil), + .origin([]), + ] + + for toBeIgnored in framesToBeIgnored { + XCTAssertNoThrow(try channel.writeInbound(toBeIgnored)) + XCTAssertNil(try channel.readInbound(as: HTTP2Frame.FramePayload.self)) + } + } + + func testServerInitialMetadataMissingHTTPStatusCodeResultsInFinishedRPC() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + let request = RPCRequestPart.metadata([:]) + XCTAssertNoThrow(try channel.writeOutbound(request)) + + // Receive server's initial metadata without :status + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue + ] + + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init(code: .unknown, message: "HTTP Status Code is missing."), + Metadata(headers: serverInitialMetadata) + ) + ) + } + + func testServerInitialMetadata1xxHTTPStatusCodeResultsInNothingRead() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + let request = RPCRequestPart.metadata([:]) + XCTAssertNoThrow(try channel.writeOutbound(request)) + + // Receive server's initial metadata with 1xx status + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "104", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + ] + + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + } + + func testServerInitialMetadataOtherNon200HTTPStatusCodeResultsInFinishedRPC() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + let request = RPCRequestPart.metadata([:]) + XCTAssertNoThrow(try channel.writeOutbound(request)) + + // Receive server's initial metadata with non-200 and non-1xx :status + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: String(HTTPResponseStatus.tooManyRequests.code), + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + ] + + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init(code: .unavailable, message: "Unexpected non-200 HTTP Status Code."), + Metadata(headers: serverInitialMetadata) + ) + ) + } + + func testServerInitialMetadataMissingContentTypeResultsInFinishedRPC() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + let request = RPCRequestPart.metadata([:]) + XCTAssertNoThrow(try channel.writeOutbound(request)) + + // Receive server's initial metadata without content-type + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200" + ] + + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init(code: .internalError, message: "Missing content-type header"), + Metadata(headers: serverInitialMetadata) + ) + ) + } + + func testNotAcceptedEncodingResultsInFinishedRPC() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .deflate, + acceptedEncodings: [.deflate], + maximumPayloadSize: 1 + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + XCTAssertNoThrow( + try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) + ) + + // Make sure we have sent right metadata. + let writtenMetadata = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenMetadata.headers, + [ + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + GRPCHTTP2Keys.encoding.rawValue: "deflate", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] + ) + + // Server sends initial metadata with unsupported encoding + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "gzip", + ] + + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init( + code: .internalError, + message: + "The server picked a compression algorithm ('gzip') the client does not know about." + ), + Metadata(headers: serverInitialMetadata) + ) + ) + } + + func testOverMaximumPayloadSize() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + XCTAssertNoThrow( + try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) + ) + + // Make sure we have sent right metadata. + let writtenMetadata = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenMetadata.headers, + [ + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + ) + + // Server sends initial metadata + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .metadata(Metadata(headers: serverInitialMetadata)) + ) + + // Server sends message over payload limit + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message + let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + "Message has exceeded the configured maximum payload size (max: 1, actual: 42)" + ) + } + + // Make sure we didn't read the received message + XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) + } + + func testServerEndsStream() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Write client's initial metadata + XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + let writtenInitialMetadata = try channel.assertReadHeadersOutbound() + XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) + + // Receive server's initial metadata with end stream set + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.grpcStatus.rawValue: "0", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers( + .init( + headers: serverInitialMetadata, + endStream: true + ) + ) + ) + ) + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init(code: .ok, message: ""), + [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + ] + ) + ) + + // We should throw if the server sends another message, since it's closed the stream already. + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message + let serverDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeInbound(HTTP2Frame.FramePayload.data(serverDataPayload)) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") + } + } + + func testNormalFlow() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 100, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + let request = RPCRequestPart.metadata([:]) + XCTAssertNoThrow(try channel.writeOutbound(request)) + + // Make sure we have sent the corresponding frame, and that nothing has been written back. + let writtenHeaders = try channel.assertReadHeadersOutbound() + XCTAssertEqual( + writtenHeaders.headers, + [ + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + + ] + ) + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + + // Receive server's initial metadata + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + "some-custom-header": "some-custom-value", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + RPCResponsePart.metadata(Metadata(headers: serverInitialMetadata)) + ) + + // Send a message + XCTAssertNoThrow( + try channel.writeOutbound(RPCRequestPart.message(.init(repeating: 1, count: 42))) + ) + + // Assert we wrote it successfully into the channel + let writtenMessage = try channel.assertReadDataOutbound() + var expectedBuffer = ByteBuffer() + expectedBuffer.writeInteger(UInt8(0)) // not compressed + expectedBuffer.writeInteger(UInt32(42)) // message length + expectedBuffer.writeRepeatingByte(1, count: 42) // message + XCTAssertEqual(writtenMessage.data, .byteBuffer(expectedBuffer)) + + // Half-close the outbound end: this would be triggered by finishing the client's writer. + XCTAssertNoThrow(channel.close(mode: .output, promise: nil)) + + // Flush to make sure the EOS is written. + channel.flush() + + // Make sure the EOS frame was sent + let emptyEOSFrame = try channel.assertReadDataOutbound() + XCTAssertEqual(emptyEOSFrame.data, .byteBuffer(.init())) + XCTAssertTrue(emptyEOSFrame.endStream) + + // Make sure we cannot write anymore because client's closed. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(RPCRequestPart.message(.init(repeating: 1, count: 42))) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + } + + // This is needed to clear the EmbeddedChannel's stored error, otherwise + // it will be thrown when writing inbound. + try? channel.throwIfErrorCaught() + + // Server sends back response message + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message + let serverDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer)) + XCTAssertNoThrow(try channel.writeInbound(HTTP2Frame.FramePayload.data(serverDataPayload))) + + // Make sure we read the message properly + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + RPCResponsePart.message([UInt8](repeating: 0, count: 42)) + ) + + // Server sends status to end RPC + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers( + .init(headers: [ + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.dataLoss.rawValue), + GRPCHTTP2Keys.grpcStatusMessage.rawValue: "Test data loss", + "custom-header": "custom-value", + ]) + ) + ) + ) + + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status(.init(code: .dataLoss, message: "Test data loss"), ["custom-header": "custom-value"]) + ) + } + + func testReceiveMessageSplitAcrossMultipleBuffers() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 100, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + let request = RPCRequestPart.metadata([:]) + XCTAssertNoThrow(try channel.writeOutbound(request)) + + // Make sure we have sent the corresponding frame, and that nothing has been written back. + let writtenHeaders = try channel.assertReadHeadersOutbound() + XCTAssertEqual( + writtenHeaders.headers, + [ + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + + ] + ) + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + + // Receive server's initial metadata + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + "some-custom-header": "some-custom-value", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + RPCResponsePart.metadata(Metadata(headers: serverInitialMetadata)) + ) + + // Send a message + XCTAssertNoThrow( + try channel.writeOutbound(RPCRequestPart.message(.init(repeating: 1, count: 42))) + ) + + // Assert we wrote it successfully into the channel + let writtenMessage = try channel.assertReadDataOutbound() + var expectedBuffer = ByteBuffer() + expectedBuffer.writeInteger(UInt8(0)) // not compressed + expectedBuffer.writeInteger(UInt32(42)) // message length + expectedBuffer.writeRepeatingByte(1, count: 42) // message + XCTAssertEqual(writtenMessage.data, .byteBuffer(expectedBuffer)) + + // Receive server's first message + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + + buffer.clear() + buffer.writeInteger(UInt32(30)) // message length + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + + buffer.clear() + buffer.writeRepeatingByte(0, count: 10) // first part of the message + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + + buffer.clear() + buffer.writeRepeatingByte(1, count: 10) // second part of the message + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + + buffer.clear() + buffer.writeRepeatingByte(2, count: 10) // third part of the message + XCTAssertNoThrow( + try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) + ) + + // Make sure we read the message properly + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + RPCResponsePart.message( + [UInt8](repeating: 0, count: 10) + [UInt8](repeating: 1, count: 10) + + [UInt8](repeating: 2, count: 10) + ) + ) + } + + func testSendMultipleMessagesInSingleBuffer() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 100, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + let request = RPCRequestPart.metadata([:]) + XCTAssertNoThrow(try channel.writeOutbound(request)) + + // Make sure we have sent the corresponding frame, and that nothing has been written back. + let writtenHeaders = try channel.assertReadHeadersOutbound() + XCTAssertEqual( + writtenHeaders.headers, + [ + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + + ] + ) + XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) + + // Receive server's initial metadata + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + "some-custom-header": "some-custom-value", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + RPCResponsePart.metadata(Metadata(headers: serverInitialMetadata)) + ) + + // This is where this test actually begins. We want to write two messages + // without flushing, and make sure that no messages are sent down the pipeline + // until we flush. Once we flush, both messages should be sent in the same ByteBuffer. + + // Write back first message and make sure nothing's written in the channel. + XCTAssertNoThrow(channel.write(RPCRequestPart.message([UInt8](repeating: 1, count: 4)))) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + // Write back second message and make sure nothing's written in the channel. + XCTAssertNoThrow(channel.write(RPCRequestPart.message([UInt8](repeating: 2, count: 4)))) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + // Now flush and check we *do* write the data. + channel.flush() + + let writtenMessage = try channel.assertReadDataOutbound() + + // Make sure both messages have been framed together in the ByteBuffer. + XCTAssertEqual( + writtenMessage.data, + .byteBuffer( + .init(bytes: [ + // First message + 0, // Compression disabled + 0, 0, 0, 4, // Message length + 1, 1, 1, 1, // First message data + + // Second message + 0, // Compression disabled + 0, 0, 0, 4, // Message length + 2, 2, 2, 2, // Second message data + ]) + ) + ) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + } +} + +extension EmbeddedChannel { + fileprivate func assertReadHeadersOutbound() throws -> HTTP2Frame.FramePayload.Headers { + guard + case .headers(let writtenHeaders) = try XCTUnwrap( + try self.readOutbound(as: HTTP2Frame.FramePayload.self) + ) + else { + throw TestError.assertionFailure("Expected to write headers") + } + return writtenHeaders + } + + fileprivate func assertReadDataOutbound() throws -> HTTP2Frame.FramePayload.Data { + guard + case .data(let writtenMessage) = try XCTUnwrap( + try self.readOutbound(as: HTTP2Frame.FramePayload.self) + ) + else { + throw TestError.assertionFailure("Expected to write data") + } + return writtenMessage + } +} + +private enum TestError: Error { + case assertionFailure(String) +} diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index 5839a7aa3..9d56824be 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -257,7 +257,6 @@ final class GRPCServerStreamHandlerTests: XCTestCase { ) ) - // Make sure we haven't sent back an error response, and that we read the initial metadata // Make sure we have sent a trailers-only response let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() @@ -413,7 +412,8 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100 + maximumPayloadSize: 100, + skipStateMachineAssertions: true ) let channel = EmbeddedChannel(handler: handler) @@ -505,6 +505,16 @@ final class GRPCServerStreamHandlerTests: XCTestCase { "custom-header": "custom-value", ] ) + + // Try writing and assert it throws to make sure we don't allow writes + // after closing. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(trailers) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server can't send anything if closed.") + } } func testReceiveMessageSplitAcrossMultipleBuffers() throws { From 97994e1617e21045e76096e0d187a8d8573f6bd4 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 27 Mar 2024 15:07:12 +0000 Subject: [PATCH 276/580] Handle write promises correctly (#1843) --- .../Client/GRPCClientStreamHandler.swift | 20 +- Sources/GRPCHTTP2Core/GRPCMessageFramer.swift | 22 +- .../GRPCStreamStateMachine.swift | 63 +++-- .../Server/GRPCServerStreamHandler.swift | 35 ++- .../GRPCMessageDeframerTests.swift | 18 +- .../GRPCMessageFramerTests.swift | 48 +++- .../GRPCStreamStateMachineTests.swift | 255 +++++++++++------- 7 files changed, 272 insertions(+), 189 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index 6db75f843..5be8f1025 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -143,24 +143,18 @@ extension GRPCClientStreamHandler { do { self.flushPending = true let headers = try self.stateMachine.send(metadata: metadata) - context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: nil) - // TODO: move the promise handling into the state machine - promise?.succeed() + context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: promise) } catch { - context.fireErrorCaught(error) - // TODO: move the promise handling into the state machine promise?.fail(error) + context.fireErrorCaught(error) } case .message(let message): do { - try self.stateMachine.send(message: message) - // TODO: move the promise handling into the state machine - promise?.succeed() + try self.stateMachine.send(message: message, promise: promise) } catch { - context.fireErrorCaught(error) - // TODO: move the promise handling into the state machine promise?.fail(error) + context.fireErrorCaught(error) } } } @@ -197,12 +191,12 @@ extension GRPCClientStreamHandler { private func _flush(context: ChannelHandlerContext) { do { loop: while true { - switch try self.stateMachine.nextOutboundMessage() { - case .sendMessage(let byteBuffer): + switch try self.stateMachine.nextOutboundFrame() { + case .sendFrame(let byteBuffer, let promise): self.flushPending = true context.write( self.wrapOutboundOut(.data(.init(data: .byteBuffer(byteBuffer)))), - promise: nil + promise: promise ) case .noMoreMessages: diff --git a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift index 590b5efeb..29da4b7dc 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift @@ -32,7 +32,7 @@ struct GRPCMessageFramer { /// reserves capacity in powers of 2. This way, we can take advantage of the whole buffer. static let maximumWriteBufferLength = 65_536 - private var pendingMessages: OneOrManyQueue<[UInt8]> + private var pendingMessages: OneOrManyQueue<(bytes: [UInt8], promise: EventLoopPromise?)> private var writeBuffer: ByteBuffer @@ -44,8 +44,8 @@ struct GRPCMessageFramer { /// Queue the given bytes to be framed and potentially coalesced alongside other messages in a `ByteBuffer`. /// The resulting data will be returned when calling ``GRPCMessageFramer/next()``. - mutating func append(_ bytes: [UInt8]) { - self.pendingMessages.append(bytes) + mutating func append(_ bytes: [UInt8], promise: EventLoopPromise?) { + self.pendingMessages.append((bytes, promise)) } /// If there are pending messages to be framed, a `ByteBuffer` will be returned with the framed data. @@ -53,7 +53,9 @@ struct GRPCMessageFramer { /// - Parameter compressor: An optional compressor: if present, payloads will be compressed; otherwise /// they'll be framed as-is. /// - Throws: If an error is encountered, such as a compression failure, an error will be thrown. - mutating func next(compressor: Zlib.Compressor? = nil) throws -> ByteBuffer? { + mutating func next( + compressor: Zlib.Compressor? = nil + ) throws -> (bytes: ByteBuffer, promise: EventLoopPromise?)? { if self.pendingMessages.isEmpty { // Nothing pending: exit early. return nil @@ -69,15 +71,21 @@ struct GRPCMessageFramer { var requiredCapacity = 0 for message in self.pendingMessages { - requiredCapacity += message.count + Self.metadataLength + requiredCapacity += message.bytes.count + Self.metadataLength } self.writeBuffer.clear(minimumCapacity: requiredCapacity) + var pendingWritePromise: EventLoopPromise? while let message = self.pendingMessages.pop() { - try self.encode(message, compressor: compressor) + try self.encode(message.bytes, compressor: compressor) + if let existingPendingWritePromise = pendingWritePromise { + existingPendingWritePromise.futureResult.cascade(to: message.promise) + } else { + pendingWritePromise = message.promise + } } - return self.writeBuffer + return (bytes: self.writeBuffer, promise: pendingWritePromise) } private mutating func encode(_ message: [UInt8], compressor: Zlib.Compressor?) throws { diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 41961b568..1c8eca27f 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -325,12 +325,12 @@ struct GRPCStreamStateMachine { } } - mutating func send(message: [UInt8]) throws { + mutating func send(message: [UInt8], promise: EventLoopPromise?) throws { switch self.configuration { case .client: - try self.clientSend(message: message) + try self.clientSend(message: message, promise: promise) case .server: - try self.serverSend(message: message) + try self.serverSend(message: message, promise: promise) } } @@ -397,23 +397,26 @@ struct GRPCStreamStateMachine { } } - /// The result of requesting the next outbound message. - enum OnNextOutboundMessage: Equatable { - /// Either the receiving party is closed, so we shouldn't send any more messages; or the sender is done + /// The result of requesting the next outbound frame, which may contain multiple messages. + enum OnNextOutboundFrame { + /// Either the receiving party is closed, so we shouldn't send any more frames; or the sender is done /// writing messages (i.e. we are now closed). case noMoreMessages - /// There isn't a message ready to be sent, but we could still receive more, so keep trying. + /// There isn't a frame ready to be sent, but we could still receive more messages, so keep trying. case awaitMoreMessages - /// A message is ready to be sent. - case sendMessage(ByteBuffer) + /// A frame is ready to be sent. + case sendFrame( + frame: ByteBuffer, + promise: EventLoopPromise? + ) } - mutating func nextOutboundMessage() throws -> OnNextOutboundMessage { + mutating func nextOutboundFrame() throws -> OnNextOutboundFrame { switch self.configuration { case .client: - return try self.clientNextOutboundMessage() + return try self.clientNextOutboundFrame() case .server: - return try self.serverNextOutboundMessage() + return try self.serverNextOutboundFrame() } } @@ -540,15 +543,15 @@ extension GRPCStreamStateMachine { } } - private mutating func clientSend(message: [UInt8]) throws { + private mutating func clientSend(message: [UInt8], promise: EventLoopPromise?) throws { switch self.state { case .clientIdleServerIdle: try self.invalidState("Client not yet open.") case .clientOpenServerIdle(var state): - state.framer.append(message) + state.framer.append(message, promise: promise) self.state = .clientOpenServerIdle(state) case .clientOpenServerOpen(var state): - state.framer.append(message) + state.framer.append(message, promise: promise) self.state = .clientOpenServerOpen(state) case .clientOpenServerClosed: // The server has closed, so it makes no sense to send the rest of the request. @@ -577,23 +580,25 @@ extension GRPCStreamStateMachine { /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. - private mutating func clientNextOutboundMessage() throws -> OnNextOutboundMessage { + private mutating func clientNextOutboundFrame() throws -> OnNextOutboundFrame { switch self.state { case .clientIdleServerIdle: try self.invalidState("Client is not open yet.") case .clientOpenServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) - return request.map { .sendMessage($0) } ?? .awaitMoreMessages + return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } + ?? .awaitMoreMessages case .clientOpenServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return request.map { .sendMessage($0) } ?? .awaitMoreMessages + return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } + ?? .awaitMoreMessages case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) if let request { - return .sendMessage(request) + return .sendFrame(frame: request.bytes, promise: request.promise) } else { return .noMoreMessages } @@ -601,7 +606,7 @@ extension GRPCStreamStateMachine { let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) if let request { - return .sendMessage(request) + return .sendFrame(frame: request.bytes, promise: request.promise) } else { return .noMoreMessages } @@ -1003,17 +1008,17 @@ extension GRPCStreamStateMachine { } } - private mutating func serverSend(message: [UInt8]) throws { + private mutating func serverSend(message: [UInt8], promise: EventLoopPromise?) throws { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: try self.invalidState( "Server must have sent initial metadata before sending a message." ) case .clientOpenServerOpen(var state): - state.framer.append(message) + state.framer.append(message, promise: promise) self.state = .clientOpenServerOpen(state) case .clientClosedServerOpen(var state): - state.framer.append(message) + state.framer.append(message, promise: promise) self.state = .clientClosedServerOpen(state) case .clientOpenServerClosed, .clientClosedServerClosed: try self.invalidState( @@ -1351,23 +1356,25 @@ extension GRPCStreamStateMachine { } } - private mutating func serverNextOutboundMessage() throws -> OnNextOutboundMessage { + private mutating func serverNextOutboundFrame() throws -> OnNextOutboundFrame { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: try self.invalidState("Server is not open yet.") case .clientOpenServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return response.map { .sendMessage($0) } ?? .awaitMoreMessages + return response.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } + ?? .awaitMoreMessages case .clientClosedServerOpen(var state): let response = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - return response.map { .sendMessage($0) } ?? .awaitMoreMessages + return response.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } + ?? .awaitMoreMessages case .clientOpenServerClosed(var state): let response = try state.framer?.next(compressor: state.compressor) self.state = .clientOpenServerClosed(state) if let response { - return .sendMessage(response) + return .sendFrame(frame: response.bytes, promise: response.promise) } else { return .noMoreMessages } @@ -1375,7 +1382,7 @@ extension GRPCStreamStateMachine { let response = try state.framer?.next(compressor: state.compressor) self.state = .clientClosedServerClosed(state) if let response { - return .sendMessage(response) + return .sendFrame(frame: response.bytes, promise: response.promise) } else { return .noMoreMessages } diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 22cd50330..14c576c20 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -34,7 +34,8 @@ final class GRPCServerStreamHandler: ChannelDuplexHandler { // We buffer the final status + trailers to avoid reordering issues (i.e., // if there are messages still not written into the channel because flush has // not been called, but the server sends back trailers). - private var pendingTrailers: HTTP2Frame.FramePayload? + private var pendingTrailers: + (trailers: HTTP2Frame.FramePayload, promise: EventLoopPromise?)? init( scheme: Scheme, @@ -142,37 +143,28 @@ extension GRPCServerStreamHandler { do { self.flushPending = true let headers = try self.stateMachine.send(metadata: metadata) - context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: nil) - // TODO: move the promise handling into the state machine - promise?.succeed() + context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: promise) } catch { - context.fireErrorCaught(error) - // TODO: move the promise handling into the state machine promise?.fail(error) + context.fireErrorCaught(error) } case .message(let message): do { - try self.stateMachine.send(message: message) - // TODO: move the promise handling into the state machine - promise?.succeed() + try self.stateMachine.send(message: message, promise: promise) } catch { - context.fireErrorCaught(error) - // TODO: move the promise handling into the state machine promise?.fail(error) + context.fireErrorCaught(error) } case .status(let status, let metadata): do { let headers = try self.stateMachine.send(status: status, metadata: metadata) let response = HTTP2Frame.FramePayload.headers(.init(headers: headers, endStream: true)) - self.pendingTrailers = response - // TODO: move the promise handling into the state machine - promise?.succeed() + self.pendingTrailers = (response, promise) } catch { - context.fireErrorCaught(error) - // TODO: move the promise handling into the state machine promise?.fail(error) + context.fireErrorCaught(error) } } } @@ -185,19 +177,22 @@ extension GRPCServerStreamHandler { do { loop: while true { - switch try self.stateMachine.nextOutboundMessage() { - case .sendMessage(let byteBuffer): + switch try self.stateMachine.nextOutboundFrame() { + case .sendFrame(let byteBuffer, let promise): self.flushPending = true context.write( self.wrapOutboundOut(.data(.init(data: .byteBuffer(byteBuffer)))), - promise: nil + promise: promise ) case .noMoreMessages: if let pendingTrailers = self.pendingTrailers { self.flushPending = true self.pendingTrailers = nil - context.write(self.wrapOutboundOut(pendingTrailers), promise: nil) + context.write( + self.wrapOutboundOut(pendingTrailers.trailers), + promise: pendingTrailers.promise + ) } break loop diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift index 98d5fc046..103c89f70 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift @@ -129,19 +129,19 @@ final class GRPCMessageDeframerTests: XCTestCase { } let firstMessage = try { - framer.append(Array(repeating: 42, count: 100)) + framer.append(Array(repeating: 42, count: 100), promise: nil) return try framer.next(compressor: compressor)! }() let secondMessage = try { - framer.append(Array(repeating: 43, count: 110)) + framer.append(Array(repeating: 43, count: 110), promise: nil) return try framer.next(compressor: compressor)! }() try ByteToMessageDecoderVerifier.verifyDecoder( inputOutputPairs: [ - (firstMessage, [Array(repeating: 42, count: 100)]), - (secondMessage, [Array(repeating: 43, count: 110)]), + (firstMessage.bytes, [Array(repeating: 42, count: 100)]), + (secondMessage.bytes, [Array(repeating: 43, count: 110)]), ]) { GRPCMessageDeframer(maximumPayloadSize: 1000, decompressor: decompressor) } @@ -164,12 +164,12 @@ final class GRPCMessageDeframerTests: XCTestCase { compressor.end() } - framer.append(Array(repeating: 42, count: 100)) + framer.append(Array(repeating: 42, count: 100), promise: nil) let framedMessage = try framer.next(compressor: compressor)! XCTAssertThrowsError( ofType: RPCError.self, - try processor.process(buffer: framedMessage) { _ in + try processor.process(buffer: framedMessage.bytes) { _ in XCTFail("No message should be produced.") } ) { error in @@ -178,7 +178,7 @@ final class GRPCMessageDeframerTests: XCTestCase { error.message, """ Message has exceeded the configured maximum payload size \ - (max: 1, actual: \(framedMessage.readableBytes - GRPCMessageDeframer.metadataLength)) + (max: 1, actual: \(framedMessage.bytes.readableBytes - GRPCMessageDeframer.metadataLength)) """ ) } @@ -195,12 +195,12 @@ final class GRPCMessageDeframerTests: XCTestCase { compressor.end() } - framer.append(Array(repeating: 42, count: 101)) + framer.append(Array(repeating: 42, count: 101), promise: nil) let framedMessage = try framer.next(compressor: compressor)! XCTAssertThrowsError( ofType: RPCError.self, - try processor.process(buffer: framedMessage) { _ in + try processor.process(buffer: framedMessage.bytes) { _ in XCTFail("No message should be produced.") } ) { error in diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift index 886e60319..bf9696e73 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift @@ -15,6 +15,7 @@ */ import NIOCore +import NIOEmbedded import XCTest @testable import GRPCHTTP2Core @@ -22,9 +23,9 @@ import XCTest final class GRPCMessageFramerTests: XCTestCase { func testSingleWrite() throws { var framer = GRPCMessageFramer() - framer.append(Array(repeating: 42, count: 128)) + framer.append(Array(repeating: 42, count: 128), promise: nil) - var buffer = try XCTUnwrap(framer.next()) + var buffer = try XCTUnwrap(framer.next()).bytes let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) XCTAssertFalse(compressed) XCTAssertEqual(length, 128) @@ -43,7 +44,7 @@ final class GRPCMessageFramerTests: XCTestCase { var framer = GRPCMessageFramer() let message = [UInt8](repeating: 42, count: 128) - framer.append(message) + framer.append(message, promise: nil) var buffer = ByteBuffer() let testCompressor = Zlib.Compressor(method: compressionMethod) @@ -53,7 +54,7 @@ final class GRPCMessageFramerTests: XCTestCase { testCompressor.end() } - buffer = try XCTUnwrap(framer.next(compressor: compressor)) + buffer = try XCTUnwrap(framer.next(compressor: compressor)).bytes let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) XCTAssertTrue(compressed) XCTAssertEqual(length, UInt32(compressedSize)) @@ -74,26 +75,47 @@ final class GRPCMessageFramerTests: XCTestCase { func testMultipleWrites() throws { var framer = GRPCMessageFramer() - - let messages = 100 - for _ in 0 ..< messages { - framer.append(Array(repeating: 42, count: 128)) + let eventLoop = EmbeddedEventLoop() + + // Create 100 messages and link a different promise with each of them. + let messagesCount = 100 + var promises = [EventLoopPromise]() + promises.reserveCapacity(messagesCount) + for _ in 0 ..< messagesCount { + let promise = eventLoop.makePromise(of: Void.self) + promises.append(promise) + framer.append(Array(repeating: 42, count: 128), promise: promise) } - var buffer = try XCTUnwrap(framer.next()) - for _ in 0 ..< messages { + let nextFrame = try XCTUnwrap(framer.next()) + + // Assert the messages have been framed all together in the same frame. + var buffer = nextFrame.bytes + for _ in 0 ..< messagesCount { let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) XCTAssertFalse(compressed) XCTAssertEqual(length, 128) XCTAssertEqual(buffer.readSlice(length: Int(length)), ByteBuffer(repeating: 42, count: 128)) } - XCTAssertEqual(buffer.readableBytes, 0) - // No more bufers. + // Assert the promise returned from the framer is the promise linked to the + // first message appended to the framer. + let returnedPromise = nextFrame.promise + XCTAssertEqual(returnedPromise?.futureResult, promises.first?.futureResult) + + // Succeed the returned promise to simulate a write into the channel + // succeeding, and assert that all other promises have been chained and are + // also succeeded as a result. + returnedPromise?.succeed() + XCTAssertEqual(promises.count, messagesCount) + for promise in promises { + try promise.futureResult.assertSuccess().wait() + } + + // No more frames. XCTAssertNil(try framer.next()) } - } extension ByteBuffer { diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 29261257f..324d47fdb 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -16,6 +16,7 @@ import GRPCCore import NIOCore +import NIOEmbedded import NIOHPACK import XCTest @@ -238,7 +239,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try to send a message without opening (i.e. without sending initial metadata) XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client not yet open.") @@ -252,7 +253,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: targetState) // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [])) + XCTAssertNoThrow(try stateMachine.send(message: [], promise: nil)) } } @@ -266,7 +267,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") @@ -624,7 +625,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.nextOutboundMessage() + try stateMachine.nextOutboundFrame() ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is not open yet.") @@ -635,9 +636,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) let expectedBytes: [UInt8] = [ 0, // compression flag: unset @@ -645,12 +646,12 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 42, 42, // original message ] XCTAssertEqual( - try stateMachine.nextOutboundMessage(), - .sendMessage(ByteBuffer(bytes: expectedBytes)) + try stateMachine.nextOutboundFrame(), + .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil) ) // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) } } @@ -660,14 +661,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { compressionEnabled: true ) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage)) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) - let request = try stateMachine.nextOutboundMessage() + let request = try stateMachine.nextOutboundFrame() let framedMessage = try self.frameMessage(originalMessage, compress: true) - XCTAssertEqual(request, .sendMessage(framedMessage)) + XCTAssertEqual(request, .sendFrame(frame: framedMessage, promise: nil)) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { @@ -676,74 +677,74 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { compressionEnabled: true ) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage)) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) - let request = try stateMachine.nextOutboundMessage() + let request = try stateMachine.nextOutboundFrame() let framedMessage = try self.frameMessage(originalMessage, compress: true) - XCTAssertEqual(request, .sendMessage(framedMessage)) + XCTAssertEqual(request, .sendFrame(frame: framedMessage, promise: nil)) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) // No more messages to send - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) // Queue a message, but assert the action is .noMoreMessages nevertheless, // because the server is closed. - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) // Send a message and close client - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) XCTAssertNoThrow(try stateMachine.closeOutbound()) // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - let request = try stateMachine.nextOutboundMessage() + let request = try stateMachine.nextOutboundFrame() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(request, .sendMessage(ByteBuffer(bytes: expectedBytes))) + XCTAssertEqual(request, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) // Send a message and close client - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) XCTAssertNoThrow(try stateMachine.closeOutbound()) // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - let request = try stateMachine.nextOutboundMessage() + let request = try stateMachine.nextOutboundFrame() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(request, .sendMessage(ByteBuffer(bytes: expectedBytes))) + XCTAssertEqual(request, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) // Send a message - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) // Close server XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) @@ -753,7 +754,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Even though we have enqueued a message, don't send it, because the server // is closed. - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) } // - MARK: Next inbound message @@ -887,13 +888,16 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) // Client sends messages - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let message = [UInt8]([1, 2, 3, 4]) let framedMessage = try self.frameMessage(message, compress: false) - try stateMachine.send(message: message) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: message, promise: nil) + XCTAssertEqual( + try stateMachine.nextOutboundFrame(), + .sendFrame(frame: framedMessage, promise: nil) + ) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) // Server sends response XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -929,7 +933,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) ) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } @@ -951,14 +955,17 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) // Client sends messages and ends - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let message = [UInt8]([1, 2, 3, 4]) let framedMessage = try self.frameMessage(message, compress: false) - XCTAssertNoThrow(try stateMachine.send(message: message)) + XCTAssertNoThrow(try stateMachine.send(message: message, promise: nil)) XCTAssertNoThrow(try stateMachine.closeOutbound()) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual( + try stateMachine.nextOutboundFrame(), + .sendFrame(frame: framedMessage, promise: nil) + ) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) // Server sends initial metadata let serverInitialHeadersAction = try stateMachine.receive( @@ -1005,7 +1012,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) ) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } @@ -1027,13 +1034,16 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) // Client sends messages - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let message = [UInt8]([1, 2, 3, 4]) let framedMessage = try self.frameMessage(message, compress: false) - try stateMachine.send(message: message) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessage)) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + try stateMachine.send(message: message, promise: nil) + XCTAssertEqual( + try stateMachine.nextOutboundFrame(), + .sendFrame(frame: framedMessage, promise: nil) + ) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) // Server sends initial metadata let serverInitialHeadersAction = try stateMachine.receive( @@ -1083,7 +1093,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) ) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } } @@ -1242,7 +1252,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -1258,7 +1268,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Now send a message XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -1272,7 +1282,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [])) + XCTAssertNoThrow(try stateMachine.send(message: [], promise: nil)) } func testSendMessageWhenClientOpenAndServerClosed() { @@ -1281,7 +1291,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1293,7 +1303,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual( @@ -1308,7 +1318,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending a message: even though client is closed, we should send it // because it may be expecting a response. - XCTAssertNoThrow(try stateMachine.send(message: [])) + XCTAssertNoThrow(try stateMachine.send(message: [], promise: nil)) } func testSendMessageWhenClientClosedAndServerClosed() { @@ -1317,7 +1327,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1363,7 +1373,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1385,7 +1395,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1429,7 +1439,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1451,7 +1461,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.send(message: []) + try stateMachine.send(message: [], promise: nil) ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") @@ -1873,7 +1883,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.nextOutboundMessage() + try stateMachine.nextOutboundFrame() ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") @@ -1885,7 +1895,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.nextOutboundMessage() + try stateMachine.nextOutboundFrame() ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") @@ -1897,7 +1907,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.nextOutboundMessage() + try stateMachine.nextOutboundFrame() ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") @@ -1907,20 +1917,20 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - let response = try stateMachine.nextOutboundMessage() + let response = try stateMachine.nextOutboundFrame() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) // And then make sure that nothing else is returned - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) } func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { @@ -1929,21 +1939,21 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { compressionEnabled: true ) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage)) + XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) - let response = try stateMachine.nextOutboundMessage() + let response = try stateMachine.nextOutboundFrame() let framedMessage = try self.frameMessage(originalMessage, compress: true) - XCTAssertEqual(response, .sendMessage(framedMessage)) + XCTAssertEqual(response, .sendFrame(frame: framedMessage, promise: nil)) } func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Send message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), @@ -1951,16 +1961,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) ) - let response = try stateMachine.nextOutboundMessage() + let response = try stateMachine.nextOutboundFrame() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { @@ -1968,7 +1978,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertThrowsError( ofType: RPCError.self, - try stateMachine.nextOutboundMessage() + try stateMachine.nextOutboundFrame() ) { error in XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") @@ -1979,17 +1989,17 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) // Send a message - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) // Close client XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) // Send another message - XCTAssertNoThrow(try stateMachine.send(message: [43, 43])) + XCTAssertNoThrow(try stateMachine.send(message: [43, 43], promise: nil)) // Make sure that getting the next outbound message _does_ return the message // we have enqueued. - let response = try stateMachine.nextOutboundMessage() + let response = try stateMachine.nextOutboundFrame() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes @@ -1999,17 +2009,17 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 43, 43, // original message ] - XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) } func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) // Send a message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42])) + XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) XCTAssertNoThrow( try stateMachine.send( status: .init(code: .ok, message: ""), @@ -2019,16 +2029,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // We have enqueued a message, make sure we return it even though server is closed, // because we haven't yet drained all of the pending messages. - let response = try stateMachine.nextOutboundMessage() + let response = try stateMachine.nextOutboundFrame() let expectedBytes: [UInt8] = [ 0, // compression flag: unset 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ] - XCTAssertEqual(response, .sendMessage(ByteBuffer(bytes: expectedBytes))) + XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) } // - MARK: Next inbound message @@ -2185,15 +2195,35 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Server sends response + let eventLoop = EmbeddedEventLoop() + let firstPromise = eventLoop.makePromise(of: Void.self) + let secondPromise = eventLoop.makePromise(of: Void.self) + let firstResponse = [UInt8]([5, 6, 7]) let secondResponse = [UInt8]([8, 9, 10]) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse) - try stateMachine.send(message: secondResponse) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) + + try stateMachine.send(message: firstResponse, promise: firstPromise) + try stateMachine.send(message: secondResponse, promise: secondPromise) // Make sure messages are outbound let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + + guard + case .sendFrame(let nextOutboundByteBuffer, let nextOutboundPromise) = + try stateMachine.nextOutboundFrame() + else { + XCTFail("Should have received .sendMessage") + return + } + XCTAssertEqual(nextOutboundByteBuffer, framedMessages) + XCTAssertTrue(firstPromise.futureResult === nextOutboundPromise?.futureResult) + + // Make sure that the promises associated with each sent message are chained + // together: when succeeding the one returned by the state machine on + // `nextOutboundMessage()`, the others should also be succeeded. + firstPromise.succeed() + try secondPromise.futureResult.assertSuccess().wait() // Client sends end try stateMachine.receive(buffer: ByteBuffer(), endStream: true) @@ -2205,7 +2235,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) XCTAssertEqual(response, ["grpc-status": "0"]) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } @@ -2252,13 +2282,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Server sends response let firstResponse = [UInt8]([5, 6, 7]) let secondResponse = [UInt8]([8, 9, 10]) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse) - try stateMachine.send(message: secondResponse) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, promise: nil) + try stateMachine.send(message: secondResponse, promise: nil) // Make sure messages are outbound let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + XCTAssertEqual( + try stateMachine.nextOutboundFrame(), + .sendFrame(frame: framedMessages, promise: nil) + ) // Server ends let response = try stateMachine.send( @@ -2267,7 +2300,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) XCTAssertEqual(response, ["grpc-status": "0"]) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } @@ -2314,13 +2347,16 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Server sends response let firstResponse = [UInt8]([5, 6, 7]) let secondResponse = [UInt8]([8, 9, 10]) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse) - try stateMachine.send(message: secondResponse) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) + try stateMachine.send(message: firstResponse, promise: nil) + try stateMachine.send(message: secondResponse, promise: nil) // Make sure messages are outbound let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .sendMessage(framedMessages)) + XCTAssertEqual( + try stateMachine.nextOutboundFrame(), + .sendFrame(frame: framedMessages, promise: nil) + ) // Server ends let response = try stateMachine.send( @@ -2329,7 +2365,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) XCTAssertEqual(response, ["grpc-status": "0"]) - XCTAssertEqual(try stateMachine.nextOutboundMessage(), .noMoreMessages) + XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } } @@ -2362,8 +2398,29 @@ extension XCTestCase { }() defer { compressor?.end() } for message in messages { - framer.append(message) + framer.append(message, promise: nil) + } + return try XCTUnwrap(framer.next(compressor: compressor)).bytes + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCStreamStateMachine.OnNextOutboundFrame: Equatable { + public static func == ( + lhs: GRPCStreamStateMachine.OnNextOutboundFrame, + rhs: GRPCStreamStateMachine.OnNextOutboundFrame + ) -> Bool { + switch (lhs, rhs) { + case (.noMoreMessages, .noMoreMessages): + return true + case (.awaitMoreMessages, .awaitMoreMessages): + return true + case (.sendFrame(let lhsMessage, _), .sendFrame(let rhsMessage, _)): + // Note that we're not comparing the EventLoopPromises here, as they're + // not Equatable. This is fine though, since we only use this in tests. + return lhsMessage == rhsMessage + default: + return false } - return try XCTUnwrap(framer.next(compressor: compressor)) } } From 29bc8bcb9cf6c393f99b320a604469ba18c0901c Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 27 Mar 2024 15:26:44 +0000 Subject: [PATCH 277/580] Fix several bugs in GRPC stream handlers and state machine (#1844) --- .../Client/GRPCClientStreamHandler.swift | 57 +++++-- .../GRPCStreamStateMachine.swift | 91 ++++++++--- .../Server/GRPCServerStreamHandler.swift | 26 ++-- .../Client/GRPCClientStreamHandlerTests.swift | 73 ++++++++- .../GRPCStreamStateMachineTests.swift | 147 ++++++++++++++---- 5 files changed, 321 insertions(+), 73 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index 5be8f1025..18e36ab26 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -67,16 +67,25 @@ extension GRPCClientStreamHandler { switch frameData.data { case .byteBuffer(let buffer): do { - try self.stateMachine.receive(buffer: buffer, endStream: endStream) - loop: while true { - switch self.stateMachine.nextInboundMessage() { - case .receiveMessage(let message): - context.fireChannelRead(self.wrapInboundOut(.message(message))) - case .awaitMoreMessages: - break loop - case .noMoreMessages: - context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - break loop + switch try self.stateMachine.receive(buffer: buffer, endStream: endStream) { + case .endRPCAndForwardErrorStatus(let status): + context.fireChannelRead(self.wrapInboundOut(.status(status, [:]))) + context.close(promise: nil) + + case .readInbound: + loop: while true { + switch self.stateMachine.nextInboundMessage() { + case .receiveMessage(let message): + context.fireChannelRead(self.wrapInboundOut(.message(message))) + case .awaitMoreMessages: + break loop + case .noMoreMessages: + // This could only happen if the server sends a data frame with EOS + // set, without sending status and trailers. + // If this happens, we should have forwarded an error status above + // so we should never reach this point. Do nothing. + break loop + } } } } catch { @@ -105,6 +114,7 @@ extension GRPCClientStreamHandler { case .receivedStatusAndMetadata(let status, let metadata): context.fireChannelRead(self.wrapInboundOut(.status(status, metadata))) + context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) case .doNothing: () @@ -161,7 +171,29 @@ extension GRPCClientStreamHandler { func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise?) { switch mode { - case .output, .all: + case .input: + context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + promise?.succeed() + + case .output: + // We flush all pending messages and update the internal state machine's + // state, but we don't close the outbound end of the channel, because + // forwarding the close in this case would cause the HTTP2 stream handler + // to close the whole channel (as the mode is ignored in its implementation). + do { + try self.stateMachine.closeOutbound() + // Force a flush by calling _flush instead of flush + // (otherwise, we'd skip flushing if we're in a read loop) + self._flush(context: context) + promise?.succeed() + } catch { + promise?.fail(error) + context.fireErrorCaught(error) + } + + case .all: + // Since we're closing the whole channel here, we *do* forward the close + // down the pipeline. do { try self.stateMachine.closeOutbound() // Force a flush by calling _flush @@ -172,9 +204,6 @@ extension GRPCClientStreamHandler { promise?.fail(error) context.fireErrorCaught(error) } - - case .input: - context.close(mode: .input, promise: promise) } } diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 1c8eca27f..4554f863e 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -292,6 +292,12 @@ private enum GRPCStreamStateMachineState { self.inboundMessageBuffer = previousState.inboundMessageBuffer } + init(previousState: ClientOpenServerOpenState) { + self.framer = previousState.framer + self.compressor = previousState.compressor + self.inboundMessageBuffer = previousState.inboundMessageBuffer + } + init(previousState: ClientOpenServerClosedState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -388,12 +394,24 @@ struct GRPCStreamStateMachine { } } - mutating func receive(buffer: ByteBuffer, endStream: Bool) throws { + enum OnBufferReceivedAction: Equatable { + case readInbound + + // Client-specific actions + + // This will be returned when the server sends a data frame with EOS set. + // This is invalid as per the protocol specification, because the server + // can only close by sending trailers, not by setting EOS when sending + // a message. + case endRPCAndForwardErrorStatus(Status) + } + + mutating func receive(buffer: ByteBuffer, endStream: Bool) throws -> OnBufferReceivedAction { switch self.configuration { case .client: - try self.clientReceive(buffer: buffer, endStream: endStream) + return try self.clientReceive(buffer: buffer, endStream: endStream) case .server: - try self.serverReceive(buffer: buffer, endStream: endStream) + return try self.serverReceive(buffer: buffer, endStream: endStream) } } @@ -729,7 +747,7 @@ extension GRPCStreamStateMachine { } let statusMessage = - metadata.firstString(forKey: .grpcStatusMessage) + metadata.firstString(forKey: .grpcStatusMessage, canonicalForm: false) .map { GRPCStatusMessageMarshaller.unmarshall($0) } ?? "" var convertedMetadata = Metadata(headers: metadata) @@ -860,38 +878,68 @@ extension GRPCStreamStateMachine { } } - private mutating func clientReceive(buffer: ByteBuffer, endStream: Bool) throws { + private mutating func clientReceive( + buffer: ByteBuffer, + endStream: Bool + ) throws -> OnBufferReceivedAction { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: try self.invalidState( "Cannot have received anything from server if client is not yet open." ) + case .clientOpenServerIdle, .clientClosedServerIdle: try self.invalidState( "Server cannot have sent a message before sending the initial metadata." ) + case .clientOpenServerOpen(var state): + if endStream { + // This is invalid as per the protocol specification, because the server + // can only close by sending trailers, not by setting EOS when sending + // a message. + self.state = .clientClosedServerClosed(.init(previousState: state)) + return .endRPCAndForwardErrorStatus( + Status( + code: .internalError, + message: """ + Server sent EOS alongside a data frame, but server is only allowed \ + to close by sending status and trailers. + """ + ) + ) + } + try state.deframer.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } + self.state = .clientOpenServerOpen(state) + return .readInbound + + case .clientClosedServerOpen(var state): if endStream { - self.state = .clientOpenServerClosed(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) + self.state = .clientClosedServerClosed(.init(previousState: state)) + return .endRPCAndForwardErrorStatus( + Status( + code: .internalError, + message: """ + Server sent EOS alongside a data frame, but server is only allowed \ + to close by sending status and trailers. + """ + ) + ) } - case .clientClosedServerOpen(var state): + // The client may have sent the end stream and thus it's closed, // but the server may still be responding. // The client must have a deframer set up, so force-unwrap is okay. try state.deframer!.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } else { - self.state = .clientClosedServerOpen(state) - } + self.state = .clientClosedServerOpen(state) + return .readInbound + case .clientOpenServerClosed, .clientClosedServerClosed: try self.invalidState( "Cannot have received anything from a closed server." @@ -1314,7 +1362,10 @@ extension GRPCStreamStateMachine { } } - private mutating func serverReceive(buffer: ByteBuffer, endStream: Bool) throws { + private mutating func serverReceive( + buffer: ByteBuffer, + endStream: Bool + ) throws -> OnBufferReceivedAction { switch self.state { case .clientIdleServerIdle: try self.invalidState( @@ -1354,6 +1405,7 @@ extension GRPCStreamStateMachine { "Client can't send a message if closed." ) } + return .readInbound } private mutating func serverNextOutboundFrame() throws -> OnNextOutboundFrame { @@ -1443,10 +1495,11 @@ internal enum GRPCHTTP2Keys: String { } extension HPACKHeaders { - internal func firstString(forKey key: GRPCHTTP2Keys) -> String? { - self.values(forHeader: key.rawValue, canonicalForm: true).first(where: { _ in true }).map { - String($0) - } + internal func firstString(forKey key: GRPCHTTP2Keys, canonicalForm: Bool = true) -> String? { + self.values(forHeader: key.rawValue, canonicalForm: canonicalForm).first(where: { _ in true }) + .map { + String($0) + } } internal mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 14c576c20..3624679e9 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -64,16 +64,22 @@ extension GRPCServerStreamHandler { switch frameData.data { case .byteBuffer(let buffer): do { - try self.stateMachine.receive(buffer: buffer, endStream: endStream) - loop: while true { - switch self.stateMachine.nextInboundMessage() { - case .receiveMessage(let message): - context.fireChannelRead(self.wrapInboundOut(.message(message))) - case .awaitMoreMessages: - break loop - case .noMoreMessages: - context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - break loop + switch try self.stateMachine.receive(buffer: buffer, endStream: endStream) { + case .endRPCAndForwardErrorStatus: + preconditionFailure( + "OnBufferReceivedAction.endRPCAndForwardErrorStatus should never be returned for the server." + ) + case .readInbound: + loop: while true { + switch self.stateMachine.nextInboundMessage() { + case .receiveMessage(let message): + context.fireChannelRead(self.wrapInboundOut(.message(message))) + case .awaitMoreMessages: + break loop + case .noMoreMessages: + context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + break loop + } } } } catch { diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift index 3b0d461b1..6e7d68acd 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift @@ -305,7 +305,10 @@ final class GRPCClientStreamHandlerTests: XCTestCase { buffer.writeInteger(UInt8(0)) // not compressed buffer.writeInteger(UInt32(42)) // message length buffer.writeRepeatingByte(0, count: 42) // message - let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) + let clientDataPayload = HTTP2Frame.FramePayload.Data( + data: .byteBuffer(buffer), + endStream: false + ) XCTAssertThrowsError( ofType: RPCError.self, try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) @@ -321,6 +324,74 @@ final class GRPCClientStreamHandlerTests: XCTestCase { XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) } + func testServerSendsEOSWhenSendingMessage_ResultsInErrorStatus() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .identity, + acceptedEncodings: [], + maximumPayloadSize: 100, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Send client's initial metadata + XCTAssertNoThrow( + try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) + ) + + // Make sure we have sent right metadata. + let writtenMetadata = try channel.assertReadHeadersOutbound() + + XCTAssertEqual( + writtenMetadata.headers, + [ + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + ) + + // Server sends initial metadata + let serverInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) + ) + ) + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .metadata(Metadata(headers: serverInitialMetadata)) + ) + + // Server sends message with EOS set. + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) // not compressed + buffer.writeInteger(UInt32(42)) // message length + buffer.writeRepeatingByte(0, count: 42) // message + let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) + XCTAssertNoThrow(try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload))) + + // Make sure we got status + trailers with the right error. + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + Status( + code: .internalError, + message: + "Server sent EOS alongside a data frame, but server is only allowed to close by sending status and trailers." + ), + [:] + ) + ) + } + func testServerEndsStream() throws { let handler = GRPCClientStreamHandler( methodDescriptor: .init(service: "test", method: "test"), diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 324d47fdb..a886d0053 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -456,14 +456,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall( - "Some status message" + "Some, status, message" )!, "custom-key": "custom-value", ] let trailers = try stateMachine.receive(headers: trailersOnlyResponse, endStream: true) switch trailers { case .receivedStatusAndMetadata(let status, let metadata): - XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) + XCTAssertEqual(status, Status(code: .internalError, message: "Some, status, message")) XCTAssertEqual( metadata, [ @@ -599,8 +599,22 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) + XCTAssertEqual( + try stateMachine.receive(buffer: .init(), endStream: false), + .readInbound + ) + XCTAssertEqual( + try stateMachine.receive(buffer: .init(), endStream: true), + .endRPCAndForwardErrorStatus( + Status( + code: .internalError, + message: """ + Server sent EOS alongside a data frame, but server is only allowed \ + to close by sending status and trailers. + """ + ) + ) + ) } } @@ -776,7 +790,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -790,7 +807,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let originalMessage = [UInt8]([42, 42, 43, 43]) let receivedBytes = try self.frameMessage(originalMessage, compress: true) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -804,7 +824,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) // Close server XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) @@ -821,7 +844,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) // Close client XCTAssertNoThrow(try stateMachine.closeOutbound()) @@ -840,7 +866,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) // Close server XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) @@ -906,8 +935,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) let secondResponseBytes = [UInt8]([8, 9, 10]) let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) - try stateMachine.receive(buffer: firstResponse, endStream: false) - try stateMachine.receive(buffer: secondResponse, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: firstResponse, endStream: false), + .readInbound + ) + XCTAssertEqual( + try stateMachine.receive(buffer: secondResponse, endStream: false), + .readInbound + ) // Make sure messages have arrived XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) @@ -988,8 +1023,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) let secondResponseBytes = [UInt8]([8, 9, 10]) let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) - try stateMachine.receive(buffer: firstResponse, endStream: false) - try stateMachine.receive(buffer: secondResponse, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: firstResponse, endStream: false), + .readInbound + ) + XCTAssertEqual( + try stateMachine.receive(buffer: secondResponse, endStream: false), + .readInbound + ) // Make sure messages have arrived XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) @@ -1069,8 +1110,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) let secondResponseBytes = [UInt8]([8, 9, 10]) let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) - try stateMachine.receive(buffer: firstResponse, endStream: false) - try stateMachine.receive(buffer: secondResponse, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: firstResponse, endStream: false), + .readInbound + ) + XCTAssertEqual( + try stateMachine.receive(buffer: secondResponse, endStream: false), + .readInbound + ) // Make sure messages have arrived XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) @@ -2061,7 +2108,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -2076,7 +2126,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let originalMessage = [UInt8]([42, 42, 43, 43]) let receivedBytes = try self.frameMessage(originalMessage, compress: true) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) @@ -2090,7 +2143,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) // Close server XCTAssertNoThrow( @@ -2117,7 +2173,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) // Close client XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) @@ -2136,7 +2195,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { 0, 0, 0, 2, // message length: 2 bytes 42, 42, // original message ]) - try stateMachine.receive(buffer: receivedBytes, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: receivedBytes, endStream: false), + .readInbound + ) // Close server XCTAssertNoThrow( @@ -2189,9 +2251,15 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - try stateMachine.receive(buffer: firstMessage, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: firstMessage, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - try stateMachine.receive(buffer: secondMessage, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: secondMessage, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Server sends response @@ -2226,7 +2294,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { try secondPromise.futureResult.assertSuccess().wait() // Client sends end - try stateMachine.receive(buffer: ByteBuffer(), endStream: true) + XCTAssertEqual( + try stateMachine.receive(buffer: ByteBuffer(), endStream: true), + .readInbound + ) // Server ends let response = try stateMachine.send( @@ -2259,13 +2330,22 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - try stateMachine.receive(buffer: firstMessage, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: firstMessage, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - try stateMachine.receive(buffer: secondMessage, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: secondMessage, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Client sends end - try stateMachine.receive(buffer: ByteBuffer(), endStream: true) + XCTAssertEqual( + try stateMachine.receive(buffer: ByteBuffer(), endStream: true), + .readInbound + ) // Server sends initial metadata let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) @@ -2324,9 +2404,15 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - try stateMachine.receive(buffer: firstMessage, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: firstMessage, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - try stateMachine.receive(buffer: secondMessage, endStream: false) + XCTAssertEqual( + try stateMachine.receive(buffer: secondMessage, endStream: false), + .readInbound + ) XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) // Server sends initial metadata @@ -2342,7 +2428,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) // Client sends end - try stateMachine.receive(buffer: ByteBuffer(), endStream: true) + XCTAssertEqual( + try stateMachine.receive(buffer: ByteBuffer(), endStream: true), + .readInbound + ) // Server sends response let firstResponse = [UInt8]([5, 6, 7]) From 393b02b1c39cc82fb24e57f24fa446f43e8124c9 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 2 Apr 2024 14:06:04 +0100 Subject: [PATCH 278/580] Update Version.swift (#1846) --- Sources/GRPC/Version.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index ac8c68d7d..ef41a7b8d 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,10 +19,10 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 21 + internal static let minor = 22 /// The patch version. - internal static let patch = 1 + internal static let patch = 0 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 5d08a1ea41929f1beec4f6eea3b7098887d8bbf1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 2 Apr 2024 15:18:23 +0100 Subject: [PATCH 279/580] Update formatting script (#1845) Motivation: The formatting script only checkes Sources and Tests. The Plugins and Performance directory are currently not checked. Moreover the formatter is using the 5.9 version. Modifications: - Update the formatter to use 5.10 - Upadte the formatting script to check Plugins and Performance Result: Formatting checks more places --- Plugins/GRPCSwiftPlugin/plugin.swift | 18 ++++++++++-------- scripts/format.sh | 10 +++++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index 4f4d6e6e6..f29000bc0 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -36,9 +36,9 @@ struct GRPCSwiftPlugin { return "The input file '\(path)' does not have a '.proto' extension." case let .noConfigFound(path): return """ - No configuration file found named '\(path)'. The file must not be listed in the \ - 'exclude:' argument for the target in Package.swift. - """ + No configuration file found named '\(path)'. The file must not be listed in the \ + 'exclude:' argument for the target in Package.swift. + """ } } } @@ -99,11 +99,13 @@ struct GRPCSwiftPlugin { sourceFiles: FileList, tool: (String) throws -> PackagePlugin.PluginContext.Tool ) throws -> [Command] { - guard let configurationFilePath = sourceFiles.first( - where: { - $0.path.lastComponent == Self.configurationFileName - } - )?.path else { + guard + let configurationFilePath = sourceFiles.first( + where: { + $0.path.lastComponent == Self.configurationFileName + } + )?.path + else { throw PluginError.noConfigFound(Self.configurationFileName) } diff --git a/scripts/format.sh b/scripts/format.sh index d995546f9..2eda38c00 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -56,7 +56,7 @@ THIS_SCRIPT=$0 HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" REPO="$HERE/.." SWIFTFORMAT_DIR="$HERE/.swift-format-source" -SWIFTFORMAT_VERSION=509.0.0 +SWIFTFORMAT_VERSION=510.0.0 # Clone SwiftFormat if we don't already have it. if [ ! -d "$SWIFTFORMAT_DIR" ]; then @@ -91,7 +91,9 @@ fi if "$lint"; then "${SWIFTFORMAT_BIN}" lint \ --parallel --recursive --strict \ - "${REPO}/Sources" "${REPO}/Tests" \ + "${REPO}/Sources" \ + "${REPO}/Tests" \ + "${REPO}/Plugins" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then @@ -108,7 +110,9 @@ if "$lint"; then elif "$format"; then "${SWIFTFORMAT_BIN}" format \ --parallel --recursive --in-place \ - "${REPO}/Sources" "${REPO}/Tests" \ + "${REPO}/Sources" \ + "${REPO}/Tests" \ + "${REPO}/Plugins" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then From e1ef29d8a3a40a330d2a57b999972425f7de9845 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 3 Apr 2024 11:13:58 +0100 Subject: [PATCH 280/580] Update `release` GitHub workflow action (#1847) * Update GitHub release workflow * Update docs to remove references to `make plugins` * Copy build artefacts into current dir --------- Co-authored-by: George Barnett --- .github/workflows/release.yml | 6 +++++- Sources/GRPC/Docs.docc/index.md | 10 ++++++++-- docs/plugin.md | 9 +-------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b3eb987a..32b23c35a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,11 @@ jobs: run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Build the plugins - run: make plugins + run: | + swift build --configuration=release --product protoc-gen-swift + cp ./.build/release/protoc-gen-swift . + swift build --configuration=release --product protoc-gen-grpc-swift + cp ./.build/release/protoc-gen-grpc-swift . - name: Zip the plugins run: | diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md index 02ed91538..e9657a9c1 100644 --- a/Sources/GRPC/Docs.docc/index.md +++ b/Sources/GRPC/Docs.docc/index.md @@ -63,8 +63,14 @@ dependencies: [ Binary releases of `protoc`, the Protocol Buffer Compiler, are available on [GitHub][protobuf-releases]. -To build the plugins, run `make plugins` in the main directory. This uses the -Swift Package Manager to build both of the necessary plugins: +To build the plugins, run the following in the main directory: + +```sh +$ swift build --product protoc-gen-swift +$ swift build --product protoc-gen-grpc-swift +``` + +This uses the Swift Package Manager to build both of the necessary plugins: `protoc-gen-swift`, which generates Protocol Buffer support code and `protoc-gen-grpc-swift`, which generates gRPC interface code. diff --git a/docs/plugin.md b/docs/plugin.md index d09800c89..0d3cbcb89 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -5,14 +5,7 @@ compiler `protoc` to generate classes for clients and services. ## Building the Plugin -The `protoc-gen-grpc-swift` plugin can be built by using the Makefile in the -top-level directory: - -```sh -$ make plugins -``` - -The Swift Package Manager may also be invoked directly to build the plugin: +The `protoc-gen-grpc-swift` plugin can be built using the Swift Package Manager: ```sh $ swift build --product protoc-gen-grpc-swift From f84657dce1bf52e837ce9bebee30caf7756eebc9 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:53:09 +0100 Subject: [PATCH 281/580] 'runServer' implementation for Performance Worker Service (#1840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: The run server RPC starts a server and reports stats back to the client. We can’t yet start a real server, because we don’t yet have the http/2 transport, but we can stub it out and do the rest of the RPC which reports stats back to the client. Modifications: - implemented the methods that start the server and collects the stats - added the initial stats property used in computing the requested server stats Result: We will be able to start the server from a performance worker. --- .../performance-worker/WorkerService.swift | 123 ++++++++++++++++-- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index b1d419c95..845c01fff 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -32,7 +32,17 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable enum Role { case client(GRPCClient) - case server(GRPCServer) + case server(ServerState) + } + + struct ServerState { + var server: GRPCServer + var stats: ServerStats + + init(server: GRPCServer, stats: ServerStats) { + self.server = server + self.stats = stats + } } init() {} @@ -41,12 +51,41 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable self.role = role } - init(server: GRPCServer) { - self.role = .server(server) + var server: GRPCServer? { + switch self.role { + case let .server(serverState): + return serverState.server + case .client, .none: + return nil + } + } + + mutating func serverStats(replaceWith newStats: ServerStats? = nil) -> ServerStats? { + switch self.role { + case var .server(serverState): + let stats = serverState.stats + if let newStats = newStats { + serverState.stats = newStats + self.role = .server(serverState) + } + return stats + case .client, .none: + return nil + } } - init(client: GRPCClient) { - self.role = .client(client) + mutating func setupServer(server: GRPCServer, stats: ServerStats) throws { + let serverState = State.ServerState(server: server, stats: stats) + switch self.role { + case .server(_): + throw RPCError(code: .alreadyExists, message: "A server has already been set up.") + + case .client(_): + throw RPCError(code: .failedPrecondition, message: "This worker has a client setup.") + + case .none: + self.role = .server(serverState) + } } } @@ -63,8 +102,8 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable switch role { case .client(let client): client.close() - case .server(let server): - server.stopListening() + case .server(let serverState): + serverState.server.stopListening() } } @@ -87,7 +126,34 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable ) async throws -> GRPCCore.ServerResponse.Stream { - throw RPCError(code: .unimplemented, message: "This RPC has not been implemented yet.") + return ServerResponse.Stream { writer in + try await withThrowingTaskGroup(of: Void.self) { group in + for try await message in request.messages { + switch message.argtype { + case let .some(.setup(serverConfig)): + let server = try await self.setupServer(serverConfig) + group.addTask { try await server.run() } + + case let .some(.mark(mark)): + let response = try await self.makeServerStatsResponse(reset: mark.reset) + try await writer.write(response) + + case .none: + () + } + } + + try await group.next() + } + + let server = self.state.withLockedValue { state in + defer { state.role = nil } + return state.server + } + + server?.stopListening() + return [:] + } } func runClient( @@ -98,3 +164,44 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable throw RPCError(code: .unimplemented, message: "This RPC has not been implemented yet.") } } + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension WorkerService { + private func setupServer(_ config: Grpc_Testing_ServerConfig) async throws -> GRPCServer { + let server = GRPCServer(transports: [], services: [BenchmarkService()]) + let stats = try await ServerStats() + + try self.state.withLockedValue { state in + try state.setupServer(server: server, stats: stats) + } + + return server + } + + private func makeServerStatsResponse( + reset: Bool + ) async throws -> Grpc_Testing_WorkerService.Method.RunServer.Output { + let currentStats = try await ServerStats() + let initialStats = self.state.withLockedValue { state in + return state.serverStats(replaceWith: reset ? currentStats : nil) + } + + guard let initialStats = initialStats else { + throw RPCError( + code: .notFound, + message: "There are no initial server stats. A server must be setup before calling 'mark'." + ) + } + + let differences = currentStats.difference(to: initialStats) + return Grpc_Testing_WorkerService.Method.RunServer.Output.with { + $0.stats = Grpc_Testing_ServerStats.with { + $0.idleCpuTime = differences.idleCPUTime + $0.timeElapsed = differences.time + $0.timeSystem = differences.systemTime + $0.timeUser = differences.userTime + $0.totalCpuTime = differences.totalCPUTime + } + } + } +} From 78a20cbadee2a76fe29140bb9eae61768e2b43cf Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 3 Apr 2024 15:03:05 +0100 Subject: [PATCH 282/580] Add wait-for-ready to method config (#1850) Motivation: The wait-for-ready property was missing from method config. Modifications: - Add wait-for-ready property and parsing Result: wait-for-ready is available in method config --- .../Configuration/MethodConfiguration.swift | 13 +++++++++++++ .../MethodConfigurationCodingTests.swift | 3 +++ 2 files changed, 16 insertions(+) diff --git a/Sources/GRPCCore/Configuration/MethodConfiguration.swift b/Sources/GRPCCore/Configuration/MethodConfiguration.swift index 0e0af5d1b..f34211abc 100644 --- a/Sources/GRPCCore/Configuration/MethodConfiguration.swift +++ b/Sources/GRPCCore/Configuration/MethodConfiguration.swift @@ -64,6 +64,12 @@ public struct MethodConfiguration: Hashable, Sendable { /// The names of methods which this configuration applies to. public var names: [Name] + /// Whether RPCs for this method should wait until the connection is ready. + /// + /// If `false` the RPC will abort immediately if there is a transient failure connecting to + /// the server. Otherwise gRPC will attempt to connect until the deadline is exceeded. + public var waitForReady: Bool? + /// The default timeout for the RPC. /// /// If no reply is received in the specified amount of time the request is aborted @@ -112,18 +118,21 @@ public struct MethodConfiguration: Hashable, Sendable { /// /// - Parameters: /// - names: The names of methods this configuration applies to. + /// - waitForReady: Whether RPCs sent to this method should wait until the connection is ready. /// - timeout: The default timeout for the RPC. /// - maxRequestMessageBytes: The maximum allowed size of a request message in bytes. /// - maxResponseMessageBytes: The maximum allowed size of a response message in bytes. /// - executionPolicy: The execution policy to use for the RPC. public init( names: [Name], + waitForReady: Bool? = nil, timeout: Duration? = nil, maxRequestMessageBytes: Int? = nil, maxResponseMessageBytes: Int? = nil, executionPolicy: ExecutionPolicy? = nil ) { self.names = names + self.waitForReady = waitForReady self.timeout = timeout self.maxRequestMessageBytes = maxRequestMessageBytes self.maxResponseMessageBytes = maxResponseMessageBytes @@ -367,6 +376,7 @@ extension Duration { extension MethodConfiguration: Codable { private enum CodingKeys: String, CodingKey { case name + case waitForReady case timeout case maxRequestMessageBytes case maxResponseMessageBytes @@ -378,6 +388,9 @@ extension MethodConfiguration: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) self.names = try container.decode([Name].self, forKey: .name) + let waitForReady = try container.decodeIfPresent(Bool.self, forKey: .waitForReady) + self.waitForReady = waitForReady + let timeout = try container.decodeIfPresent(GoogleProtobufDuration.self, forKey: .timeout) self.timeout = timeout?.duration diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift index a9365909c..6fb7f17ea 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift @@ -350,6 +350,8 @@ internal final class MethodConfigurationCodingTests: XCTestCase { } ] + $0.waitForReady = true + $0.timeout = .with { $0.seconds = 1 $0.nanos = 0 @@ -364,6 +366,7 @@ internal final class MethodConfigurationCodingTests: XCTestCase { let jsonConfig = try config.jsonUTF8Data() let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) XCTAssertEqual(decoded.names, [MethodConfiguration.Name(service: "echo.Echo", method: "Get")]) + XCTAssertEqual(decoded.waitForReady, true) XCTAssertEqual(decoded.timeout, Duration(secondsComponent: 1, attosecondsComponent: 0)) XCTAssertEqual(decoded.maxRequestMessageBytes, 1024) XCTAssertEqual(decoded.maxResponseMessageBytes, 2048) From e6195dca9b3aa3ea5507b25753526282ca6902ce Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 3 Apr 2024 15:40:19 +0100 Subject: [PATCH 283/580] Allow manually running `release` GH workflow (#1848) --- .github/workflows/release.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32b23c35a..a82b5b39c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,12 @@ name: Release on: + workflow_dispatch: + inputs: + releaseVersion: + description: The release version for which to build and upload artifacts + required: true + type: string release: types: [ published ] @@ -11,8 +17,13 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Extract release version + - name: Extract release version when job started by release being published run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + if: ${{ github.event_name == 'release' }} + + - name: Extract release version when job manually started + run: echo "RELEASE_VERSION=${{ inputs.releaseVersion }}" >> $GITHUB_ENV + if: ${{ github.event_name == 'workflow_dispatch' }} - name: Build the plugins run: | From 0da4b47775dfd594c50908e6f5b84f2a83023a76 Mon Sep 17 00:00:00 2001 From: Sami Suteria Date: Wed, 3 Apr 2024 23:57:17 -0700 Subject: [PATCH 284/580] Add reflection support to plugin (#1835) --- Plugins/GRPCSwiftPlugin/plugin.swift | 14 ++++++++ .../ReflectionServiceTutorial.md | 33 +++++++++++++++++++ .../Docs.docc/spm-plugin.md | 3 +- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index f29000bc0..bd0ee045b 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -65,6 +65,8 @@ struct GRPCSwiftPlugin { var server: Bool? /// Whether client code is generated. var client: Bool? + /// Whether reflection data is generated. + var reflectionData: Bool? /// Determines whether the casing of generated function names is kept. var keepMethodCasing: Bool? } @@ -186,6 +188,10 @@ struct GRPCSwiftPlugin { protocArgs.append("--grpc-swift_opt=Client=\(generateClientCode)") } + if let generateReflectionData = invocation.reflectionData { + protocArgs.append("--grpc-swift_opt=ReflectionData=\(generateReflectionData)") + } + if let keepMethodCasingOption = invocation.keepMethodCasing { protocArgs.append("--grpc-swift_opt=KeepMethodCasing=\(keepMethodCasingOption)") } @@ -207,6 +213,14 @@ struct GRPCSwiftPlugin { // Add the outputPath as an output file outputFiles.append(protobufOutputPath) + + if invocation.reflectionData == true { + // Remove .swift extension and add .reflection extension + file.removeLast(5) + file.append("reflection") + let reflectionOutputPath = outputDirectory.appending(file) + outputFiles.append(reflectionOutputPath) + } } // Construct the command. Specifying the input and output paths lets the build diff --git a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md index 68db44286..0ad6941bf 100644 --- a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md +++ b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md @@ -102,6 +102,38 @@ let reflectionService = try ReflectionService( ) ``` +### Swift Package Manager Plugin + +Reflection data can also be generated via the SPM plugin by including `"reflectionData": true` in `grpc-swift-config.json`. This will generate the same reflection data as running `protoc` above. The generated reflection files are added to your module Bundle and can be accessed at runtime. More about [spm-plugin][spm-plugin] can be found here. + +```json +{ + "invocations": [ + { + "protoFiles": [ + "helloworld.proto" + ], + "visibility": "public", + "server": true, + "reflectionData": true + } + ] +} +``` + +To instantiate the `ReflectionService` you can search for files with the extension `reflection` in your module Bundle. + +```swift +let reflectionDataFilePaths = Bundle.module.paths( + forResourcesOfType: "reflection", + inDirectory: nil +) +let reflectionService = try ReflectionService( + reflectionDataFilePaths: reflectionDataFilePaths, + version: .v1Alpha +) +``` + ### Running the server In our example the server isn't configured with TLS and listens on localhost port 1234. @@ -192,3 +224,4 @@ Note that when specifying a service, a method or a symbol, we have to use the fu [echo-proto]: ../../Examples/Echo/Model/echo.proto [grpcurl-v188]: https://github.com/fullstorydev/grpcurl/releases/tag/v1.8.8 [swiftpm-resources]: https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageDescription.md#resource +[spm-plugin]: ../../protoc-gen-grpc-swift/Docs.docc/spm-plugin.md \ No newline at end of file diff --git a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md index 807b766fe..61a2e10f0 100644 --- a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md +++ b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md @@ -81,7 +81,8 @@ Sources ], "visibility": "public", "client": false, - "keepMethodCasing": false + "keepMethodCasing": false, + "reflectionData": true } ] } From 26e177e6340ce573037d62fb2e5bb1ef1cea5f02 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 Apr 2024 13:15:56 +0100 Subject: [PATCH 285/580] Add subpool stats (#1852) Modification: It can be helpful to track the state of the connection pool as a whole over time to understand how heavily utilised it is. This is awkward to do at the moment because delegate functions are called on connection state changes. There's also no insight into how many RPCs are queued waiting for a stream. Modifications: - Add a 'GRPCSubPoolStats' type capturing stats from each subpool. This includes: - counts of connections in each state - streams in use - streams which are free to use - number of rpcs waiting for a stream - Add a delegate method which is passed an array of subpool stats (one per subpool) and add a no-op default implementation. - Add configuration to determine how frequently stats are collected Result: More insight into pool stats --- Sources/GRPC/ConnectionManager.swift | 8 +-- .../ConnectionPool/ConnectionManagerID.swift | 17 ++--- .../GRPC/ConnectionPool/ConnectionPool.swift | 57 ++++++++++++++- .../ConnectionPool/ConnectionPoolIDs.swift | 59 ++++++++++++++++ .../GRPC/ConnectionPool/GRPCChannelPool.swift | 69 ++++++++++++++++++- Sources/GRPC/ConnectionPool/PoolManager.swift | 46 +++++++++++-- .../PoolManagerStateMachine.swift | 23 +++++-- .../GRPC/ConnectionPool/PooledChannel.swift | 3 +- .../ConnectionPoolDelegates.swift | 18 ++++- .../ConnectionPool/ConnectionPoolTests.swift | 3 +- .../ConnectionPool/GRPCChannelPoolTests.swift | 29 ++++++++ .../PoolManagerStateMachineTests.swift | 10 +-- 12 files changed, 303 insertions(+), 39 deletions(-) create mode 100644 Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift index d30d12ee0..93cbc7527 100644 --- a/Sources/GRPC/ConnectionManager.swift +++ b/Sources/GRPC/ConnectionManager.swift @@ -338,12 +338,12 @@ internal final class ConnectionManager: @unchecked Sendable { /// A logger. internal var logger: Logger - private let connectionID: String + internal let id: ConnectionManagerID private var channelNumber: UInt64 private var channelNumberLock = NIOLock() private var _connectionIDAndNumber: String { - return "\(self.connectionID)/\(self.channelNumber)" + return "\(self.id)/\(self.channelNumber)" } private var connectionIDAndNumber: String { @@ -394,7 +394,7 @@ internal final class ConnectionManager: @unchecked Sendable { ) { // Setup the logger. var logger = logger - let connectionID = UUID().uuidString + let connectionID = ConnectionManagerID() let channelNumber: UInt64 = 0 logger[metadataKey: MetadataKey.connectionID] = "\(connectionID)/\(channelNumber)" @@ -408,7 +408,7 @@ internal final class ConnectionManager: @unchecked Sendable { self.http2Delegate = http2Delegate self.idleBehavior = idleBehavior - self.connectionID = connectionID + self.id = connectionID self.channelNumber = channelNumber self.logger = logger } diff --git a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift b/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift index 33a7220ed..a0ce0519f 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift @@ -14,25 +14,20 @@ * limitations under the License. */ +import struct Foundation.UUID + @usableFromInline internal struct ConnectionManagerID: Hashable, CustomStringConvertible, Sendable { @usableFromInline - internal let _id: ObjectIdentifier + internal let id: String @usableFromInline - internal init(_ manager: ConnectionManager) { - self._id = ObjectIdentifier(manager) + internal init() { + self.id = UUID().uuidString } @usableFromInline internal var description: String { - return String(describing: self._id) - } -} - -extension ConnectionManager { - @usableFromInline - internal var id: ConnectionManagerID { - return ConnectionManagerID(self) + return String(describing: self.id) } } diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 9ea75b405..4b678f15e 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import Atomics import Logging import NIOConcurrencyHelpers import NIOCore @@ -108,12 +110,16 @@ internal final class ConnectionPool { /// A logger which always sets "GRPC" as its source. @usableFromInline - private(set) var logger: GRPCLogger + internal let logger: GRPCLogger /// Returns `NIODeadline` representing 'now'. This is useful for testing. @usableFromInline internal let now: () -> NIODeadline + /// The ID of this sub-pool. + @usableFromInline + internal let id: GRPCSubPoolID + /// Logging metadata keys. @usableFromInline internal enum Metadata { @@ -190,8 +196,14 @@ internal final class ConnectionPool { self.channelProvider = channelProvider self.streamLender = streamLender self.delegate = delegate - self.logger = logger self.now = now + + let id = GRPCSubPoolID.next() + var logger = logger + logger[metadataKey: Metadata.id] = "\(id)" + + self.id = id + self.logger = logger } /// Initialize the connection pool. @@ -199,7 +211,6 @@ internal final class ConnectionPool { /// - Parameter connections: The number of connections to add to the pool. internal func initialize(connections: Int) { assert(self._connections.isEmpty) - self.logger.logger[metadataKey: Metadata.id] = "\(ObjectIdentifier(self))" self.logger.debug( "initializing new sub-pool", metadata: [ @@ -628,6 +639,46 @@ internal final class ConnectionPool { promise.succeed(()) } } + + internal func stats() -> EventLoopFuture { + let promise = self.eventLoop.makePromise(of: GRPCSubPoolStats.self) + + if self.eventLoop.inEventLoop { + self._stats(promise: promise) + } else { + self.eventLoop.execute { + self._stats(promise: promise) + } + } + + return promise.futureResult + } + + private func _stats(promise: EventLoopPromise) { + self.eventLoop.assertInEventLoop() + + var stats = GRPCSubPoolStats(id: self.id) + + for connection in self._connections.values { + let sync = connection.manager.sync + if sync.isIdle { + stats.connectionStates.idle += 1 + } else if sync.isConnecting { + stats.connectionStates.connecting += 1 + } else if sync.isReady { + stats.connectionStates.ready += 1 + } else if sync.isTransientFailure { + stats.connectionStates.transientFailure += 1 + } + + stats.streamsInUse += connection.reservedStreams + stats.streamsFreeToUse += connection.availableStreams + } + + stats.rpcsWaiting += self.waiters.count + + promise.succeed(stats) + } } extension ConnectionPool: ConnectionManagerConnectivityDelegate { diff --git a/Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift b/Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift new file mode 100644 index 000000000..350189172 --- /dev/null +++ b/Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift @@ -0,0 +1,59 @@ +/* + * 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 Atomics + +enum RawID { + private static let source = ManagedAtomic(0) + + static func next() -> Int { + self.source.loadThenWrappingIncrement(ordering: .relaxed) + } +} + +/// The ID of a connection pool. +public struct GRPCConnectionPoolID: Hashable, Sendable, CustomStringConvertible { + private var rawValue: Int + + private init(rawValue: Int) { + self.rawValue = rawValue + } + + public static func next() -> Self { + return Self(rawValue: RawID.next()) + } + + public var description: String { + "ConnectionPool(\(self.rawValue))" + } +} + +/// The ID of a sub-pool in a connection pool. +public struct GRPCSubPoolID: Hashable, Sendable, CustomStringConvertible { + private var rawValue: Int + + private init(rawValue: Int) { + self.rawValue = rawValue + } + + public static func next() -> Self { + return Self(rawValue: RawID.next()) + } + + public var description: String { + "SubPool(\(self.rawValue))" + } +} diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift index 0af26fecc..8a6cede36 100644 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift @@ -179,6 +179,11 @@ extension GRPCChannelPool { /// pool. public var delegate: GRPCConnectionPoolDelegate? + /// The period at which connection pool stats are published to the ``delegate``. + /// + /// Ignored if either this value or ``delegate`` are `nil`. + public var statsPeriod: TimeAmount? + /// A logger used for background activity, such as connection state changes. public var backgroundActivityLogger = Logger( label: "io.grpc", @@ -354,7 +359,7 @@ public protocol GRPCConnectionPoolDelegate: Sendable { /// time and is reported via ``connectionUtilizationChanged(id:streamsUsed:streamCapacity:)``. The func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) - /// The utlization of the connection changed; a stream may have been used, returned or the + /// The utilization of the connection changed; a stream may have been used, returned or the /// maximum number of concurrent streams available on the connection changed. func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) @@ -365,4 +370,66 @@ public protocol GRPCConnectionPoolDelegate: Sendable { /// The connection was closed. The connection may be established again in the future (notified /// via ``startedConnecting(id:)``). func connectionClosed(id: GRPCConnectionID, error: Error?) + + /// Stats about the current state of the connection pool. + /// + /// Each ``GRPCConnectionPoolStats`` includes the stats for a sub-pool. Each sub-pool is tied + /// to an `EventLoop`. + /// + /// Unlike the other delegate methods, this is called periodically based on the value + /// of ``GRPCChannelPool/Configuration/statsPeriod``. + func connectionPoolStats(_ stats: [GRPCSubPoolStats], id: GRPCConnectionPoolID) +} + +extension GRPCConnectionPoolDelegate { + public func connectionPoolStats(_ stats: [GRPCSubPoolStats], id: GRPCConnectionPoolID) { + // Default conformance to avoid breaking changes. + } +} + +public struct GRPCSubPoolStats: Sendable, Hashable { + public struct ConnectionStates: Sendable, Hashable { + /// The number of idle connections. + public var idle: Int + /// The number of connections trying to establish a connection. + public var connecting: Int + /// The number of connections which are ready to use. + public var ready: Int + /// The number of connections which are backing off waiting to attempt to connect. + public var transientFailure: Int + + public init() { + self.idle = 0 + self.connecting = 0 + self.ready = 0 + self.transientFailure = 0 + } + } + + /// The ID of the subpool. + public var id: GRPCSubPoolID + + /// Counts of connection states. + public var connectionStates: ConnectionStates + + /// The number of streams currently being used. + public var streamsInUse: Int + + /// The number of streams which are currently free to use. + /// + /// The sum of this value and `streamsInUse` gives the capacity of the pool. + public var streamsFreeToUse: Int + + /// The number of RPCs currently waiting for a stream. + /// + /// RPCs waiting for a stream are also known as 'waiters'. + public var rpcsWaiting: Int + + public init(id: GRPCSubPoolID) { + self.id = id + self.connectionStates = ConnectionStates() + self.streamsInUse = 0 + self.streamsFreeToUse = 0 + self.rpcsWaiting = 0 + } } diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index 593d11e7d..27729c01b 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -63,6 +63,9 @@ internal final class PoolManager { @usableFromInline var delegate: GRPCConnectionPoolDelegate? + @usableFromInline + var statsPeriod: TimeAmount? + @usableFromInline internal init( maxConnections: Int, @@ -72,7 +75,8 @@ internal final class PoolManager { assumedMaxConcurrentStreams: Int = 100, connectionBackoff: ConnectionBackoff, channelProvider: DefaultChannelProvider, - delegate: GRPCConnectionPoolDelegate? + delegate: GRPCConnectionPoolDelegate?, + statsPeriod: TimeAmount? ) { self.maxConnections = maxConnections self.maxWaiters = maxWaiters @@ -82,6 +86,7 @@ internal final class PoolManager { self.connectionBackoff = connectionBackoff self.channelProvider = channelProvider self.delegate = delegate + self.statsPeriod = statsPeriod } } @@ -113,6 +118,9 @@ internal final class PoolManager { @usableFromInline internal let group: EventLoopGroup + @usableFromInline + internal let id: GRPCConnectionPoolID + /// Make a new pool manager and initialize it. /// /// The pool manager manages one connection pool per event loop in the provided `EventLoopGroup`. @@ -140,6 +148,7 @@ internal final class PoolManager { self._state = PoolManagerStateMachine(.inactive) self._pools = [] self.group = group + self.id = .next() // The pool relies on the identity of each `EventLoop` in the `EventLoopGroup` being unique. In // practice this is unlikely to happen unless a custom `EventLoopGroup` is constructed, because @@ -158,7 +167,7 @@ internal final class PoolManager { self.lock.withLock { assert( self._state.isShutdownOrShuttingDown, - "The pool manager (\(ObjectIdentifier(self))) must be shutdown before going out of scope." + "The pool manager (\(self.id)) must be shutdown before going out of scope." ) } } @@ -175,7 +184,7 @@ internal final class PoolManager { logger: GRPCLogger ) { var logger = logger - logger[metadataKey: Metadata.id] = "\(ObjectIdentifier(self))" + logger[metadataKey: Metadata.id] = "\(self.id)" let pools = self.makePools(perPoolConfiguration: configuration, logger: logger) @@ -200,13 +209,27 @@ internal final class PoolManager { ) } + let statsTask: RepeatedTask? + if let period = configuration.statsPeriod, let delegate = configuration.delegate { + let loop = self.group.next() + statsTask = loop.scheduleRepeatedTask(initialDelay: period, delay: period) { _ in + self.emitStats(delegate: delegate) + } + } else { + statsTask = nil + } + self.lock.withLock { assert(self._pools.isEmpty) self._pools = pools // We'll blow up if we've already been initialized, that's fine, we don't allow callers to // call `initialize` directly. - self._state.activatePools(keyedBy: poolKeys, assumingPerPoolCapacity: assumedCapacity) + self._state.activatePools( + keyedBy: poolKeys, + assumingPerPoolCapacity: assumedCapacity, + statsTask: statsTask + ) } for pool in pools { @@ -331,7 +354,8 @@ internal final class PoolManager { } switch (action, pools) { - case let (.shutdownPools, .some(pools)): + case let (.shutdownPools(statsTask), .some(pools)): + statsTask?.cancel(promise: nil) promise.futureResult.whenComplete { _ in self.shutdownComplete() } EventLoopFuture.andAllSucceed(pools.map { $0.shutdown(mode: mode) }, promise: promise) @@ -353,6 +377,18 @@ internal final class PoolManager { self._state.shutdownComplete() } } + + // MARK: - Stats + + private func emitStats(delegate: GRPCConnectionPoolDelegate) { + let pools = self.lock.withLock { self._pools } + if pools.isEmpty { return } + + let statsFutures = pools.map { $0.stats() } + EventLoopFuture.whenAllSucceed(statsFutures, on: self.group.any()).whenSuccess { stats in + delegate.connectionPoolStats(stats, id: self.id) + } + } } // MARK: - Connection Pool to Pool Manager diff --git a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift b/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift index 5f0484a33..c3674a8f0 100644 --- a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift +++ b/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift @@ -40,10 +40,14 @@ internal struct PoolManagerStateMachine { @usableFromInline internal var pools: [EventLoopID: PerPoolState] + @usableFromInline + internal var statsTask: RepeatedTask? + @usableFromInline internal init( poolKeys: [PoolManager.ConnectionPoolKey], - assumedMaxAvailableStreamsPerPool: Int + assumedMaxAvailableStreamsPerPool: Int, + statsTask: RepeatedTask? ) { self.pools = Dictionary( uniqueKeysWithValues: poolKeys.map { key in @@ -54,6 +58,7 @@ internal struct PoolManagerStateMachine { return (key.eventLoopID, value) } ) + self.statsTask = statsTask } } @@ -92,12 +97,18 @@ internal struct PoolManagerStateMachine { @usableFromInline internal mutating func activatePools( keyedBy keys: [PoolManager.ConnectionPoolKey], - assumingPerPoolCapacity capacity: Int + assumingPerPoolCapacity capacity: Int, + statsTask: RepeatedTask? ) { self.modifyingState { state in switch state { case .inactive: - state = .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: capacity)) + let active = ActiveState( + poolKeys: keys, + assumedMaxAvailableStreamsPerPool: capacity, + statsTask: statsTask + ) + state = .active(active) case .active, .shuttingDown, .shutdown, ._modifying: preconditionFailure() @@ -180,7 +191,7 @@ internal struct PoolManagerStateMachine { } enum ShutdownAction { - case shutdownPools + case shutdownPools(RepeatedTask?) case alreadyShutdown case alreadyShuttingDown(EventLoopFuture) } @@ -192,9 +203,9 @@ internal struct PoolManagerStateMachine { state = .shutdown return .alreadyShutdown - case .active: + case .active(let active): state = .shuttingDown(promise.futureResult) - return .shutdownPools + return .shutdownPools(active.statsTask) case let .shuttingDown(future): return .alreadyShuttingDown(future) diff --git a/Sources/GRPC/ConnectionPool/PooledChannel.swift b/Sources/GRPC/ConnectionPool/PooledChannel.swift index a8715070e..4d2b06c49 100644 --- a/Sources/GRPC/ConnectionPool/PooledChannel.swift +++ b/Sources/GRPC/ConnectionPool/PooledChannel.swift @@ -101,7 +101,8 @@ internal final class PooledChannel: GRPCChannel { assumedMaxConcurrentStreams: 100, connectionBackoff: configuration.connectionBackoff, channelProvider: provider, - delegate: configuration.delegate + delegate: configuration.delegate, + statsPeriod: configuration.statsPeriod ), logger: configuration.backgroundActivityLogger.wrapped ) diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift index 0de3de52d..cb0a677cd 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift @@ -104,8 +104,9 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { case connectionUtilizationChanged(GRPCConnectionID, Int, Int) case connectionQuiescing(GRPCConnectionID) case connectionRemoved(GRPCConnectionID) + case stats([GRPCSubPoolStats], GRPCConnectionPoolID) - var id: GRPCConnectionID { + var id: GRPCConnectionID? { switch self { case let .connectionAdded(id), let .startedConnecting(id), @@ -116,6 +117,8 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { let .connectionQuiescing(id), let .connectionRemoved(id): return id + case .stats: + return nil } } } @@ -139,6 +142,13 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { } } + func removeAll() -> CircularBuffer { + return self.lock.withLock { + defer { self.events.removeAll() } + return self.events + } + } + func connectionAdded(id: GRPCConnectionID) { self.lock.withLock { self.events.append(.connectionAdded(id)) @@ -186,6 +196,12 @@ final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { self.events.append(.connectionRemoved(id)) } } + + func connectionPoolStats(_ stats: [GRPCSubPoolStats], id: GRPCConnectionPoolID) { + self.lock.withLock { + self.events.append(.stats(stats, id)) + } + } } extension EventRecordingConnectionPoolDelegate: @unchecked Sendable {} diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 65bb416cd..53d44b677 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -1046,8 +1046,7 @@ final class ConnectionPoolTests: GRPCTestCase { // Two connections must be removed. for _ in 0 ..< 2 { if let event = recorder.popFirst() { - let id = event.id - XCTAssertEqual(event, .connectionRemoved(id)) + XCTAssertEqual(event, event.id.map { .connectionRemoved($0) }) } else { XCTFail("Expected .connectionRemoved") } diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift index 52b3f9377..90b53057d 100644 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift @@ -589,5 +589,34 @@ final class GRPCChannelPoolTests: GRPCTestCase { } XCTAssertNoThrow(try EventLoopFuture.andAllSucceed(rpcs, on: self.group.any()).wait()) } + + func testDelegateGetsCalledWithStats() throws { + let recorder = EventRecordingConnectionPoolDelegate() + + self.configureEventLoopGroup(threads: 4) + self.startServer(withTLS: false) + self.startChannel(withTLS: false) { + $0.statsPeriod = .milliseconds(1) + $0.delegate = recorder + } + + let scheduled = self.group.next().scheduleTask(in: .milliseconds(100)) { + _ = self.channel?.close() + } + + try scheduled.futureResult.wait() + + let events = recorder.removeAll() + let statsEvents = events.compactMap { event in + switch event { + case .stats(let stats, _): + return stats + default: + return nil + } + } + + XCTAssertGreaterThan(statsEvents.count, 0) + } } #endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift index 02f33fa33..096bf16b4 100644 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift @@ -79,7 +79,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) let keys = self.makeConnectionPoolKeys(for: pools) var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100)) + .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) ) for (index, loop) in group.loops.enumerated() { @@ -99,7 +99,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) let keys = self.makeConnectionPoolKeys(for: pools) var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100)) + .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) ) let anotherLoop = EmbeddedEventLoop() @@ -118,7 +118,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) let keys = self.makeConnectionPoolKeys(for: pools) var state = PoolManagerStateMachine(.inactive) - state.activatePools(keyedBy: keys, assumingPerPoolCapacity: 100) + state.activatePools(keyedBy: keys, assumingPerPoolCapacity: 100, statsTask: nil) // Reserve some streams. for (index, loop) in group.loops.enumerated() { @@ -177,7 +177,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) let keys = self.makeConnectionPoolKeys(for: pools) var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100)) + .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) ) let reservePreferredLoop = state.reserveStream(preferringPoolWithEventLoopID: nil) @@ -230,7 +230,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) let keys = self.makeConnectionPoolKeys(for: pools) var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100)) + .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) ) let promise = group.loops[0].makePromise(of: Void.self) From 6ee1ed29e12d9c54cb6dc3ff7ff426e5c37390a0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 Apr 2024 14:17:05 +0100 Subject: [PATCH 286/580] Remove GRPCLogger (#1853) Motivation: GRPCLogger was added so that the source of logs was always set to 'GRPC'. This was done because swift-log would set it to the parent directory of the source file assuming it would be the module which isn't always the case. This was fixed some time ago in swift-log making GRPCLogger redundant. Modifications: - Remove GRPCLogger Result: Less code --- .../GRPC/ConnectionPool/ConnectionPool.swift | 16 +-- Sources/GRPC/ConnectionPool/PoolManager.swift | 8 +- .../GRPC/ConnectionPool/PooledChannel.swift | 4 +- Sources/GRPC/GRPCClientChannelHandler.swift | 4 +- Sources/GRPC/GRPCLogger.swift | 129 ------------------ .../ClientInterceptorContext.swift | 2 +- .../ClientInterceptorPipeline.swift | 8 +- .../GRPC/Interceptor/ClientTransport.swift | 7 +- Sources/GRPC/_EmbeddedThroughput.swift | 2 +- .../ClientInterceptorPipelineTests.swift | 2 +- .../ConnectionPool/ConnectionPoolTests.swift | 72 +++++----- .../PoolManagerStateMachineTests.swift | 2 +- .../GRPCClientChannelHandlerTests.swift | 2 +- Tests/GRPCTests/GRPCLoggerTests.swift | 40 ------ Tests/GRPCTests/GRPCStatusCodeTests.swift | 2 +- .../InterceptedRPCCancellationTests.swift | 33 ++--- 16 files changed, 74 insertions(+), 259 deletions(-) delete mode 100644 Sources/GRPC/GRPCLogger.swift delete mode 100644 Tests/GRPCTests/GRPCLoggerTests.swift diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift index 4b678f15e..7ae82b6e6 100644 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ b/Sources/GRPC/ConnectionPool/ConnectionPool.swift @@ -108,9 +108,9 @@ internal final class ConnectionPool { @usableFromInline internal var delegate: GRPCConnectionPoolDelegate? - /// A logger which always sets "GRPC" as its source. + /// A logger. @usableFromInline - internal let logger: GRPCLogger + internal let logger: Logger /// Returns `NIODeadline` representing 'now'. This is useful for testing. @usableFromInline @@ -175,7 +175,7 @@ internal final class ConnectionPool { channelProvider: ConnectionManagerChannelProvider, streamLender: StreamLender, delegate: GRPCConnectionPoolDelegate?, - logger: GRPCLogger, + logger: Logger, now: @escaping () -> NIODeadline = NIODeadline.now ) { precondition( @@ -241,7 +241,7 @@ internal final class ConnectionPool { connectionBackoff: self.connectionBackoff, connectivityDelegate: self, http2Delegate: self, - logger: self.logger.unwrapped + logger: self.logger ) let id = manager.id self._connections[id] = PerConnectionState(manager: manager) @@ -274,7 +274,7 @@ internal final class ConnectionPool { internal func makeStream( deadline: NIODeadline, promise: EventLoopPromise, - logger: GRPCLogger, + logger: Logger, initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { if self.eventLoop.inEventLoop { @@ -300,7 +300,7 @@ internal final class ConnectionPool { @inlinable internal func makeStream( deadline: NIODeadline, - logger: GRPCLogger, + logger: Logger, initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> EventLoopFuture { let promise = self.eventLoop.makePromise(of: Channel.self) @@ -336,7 +336,7 @@ internal final class ConnectionPool { internal func _makeStream( deadline: NIODeadline, promise: EventLoopPromise, - logger: GRPCLogger, + logger: Logger, initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { self.eventLoop.assertInEventLoop() @@ -403,7 +403,7 @@ internal final class ConnectionPool { internal func _enqueueWaiter( deadline: NIODeadline, promise: EventLoopPromise, - logger: GRPCLogger, + logger: Logger, initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) { // Don't overwhelm the pool with too many waiters. diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift index 27729c01b..dd7a6ba92 100644 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ b/Sources/GRPC/ConnectionPool/PoolManager.swift @@ -136,7 +136,7 @@ internal final class PoolManager { internal static func makeInitializedPoolManager( using group: EventLoopGroup, perPoolConfiguration: PerPoolConfiguration, - logger: GRPCLogger + logger: Logger ) -> PoolManager { let manager = PoolManager(privateButUsableFromInline_group: group) manager.initialize(perPoolConfiguration: perPoolConfiguration, logger: logger) @@ -181,7 +181,7 @@ internal final class PoolManager { /// - logger: A logger. private func initialize( perPoolConfiguration configuration: PerPoolConfiguration, - logger: GRPCLogger + logger: Logger ) { var logger = logger logger[metadataKey: Metadata.id] = "\(self.id)" @@ -244,7 +244,7 @@ internal final class PoolManager { /// - Returns: An array of `ConnectionPool`s. private func makePools( perPoolConfiguration configuration: PerPoolConfiguration, - logger: GRPCLogger + logger: Logger ) -> [ConnectionPool] { let eventLoops = self.group.makeIterator() return eventLoops.map { eventLoop in @@ -311,7 +311,7 @@ internal final class PoolManager { internal func makeStream( preferredEventLoop: EventLoop?, deadline: NIODeadline, - logger: GRPCLogger, + logger: Logger, streamInitializer initializer: @escaping @Sendable (Channel) -> EventLoopFuture ) -> PooledStreamChannel { let preferredEventLoopID = preferredEventLoop.map { EventLoopID($0) } diff --git a/Sources/GRPC/ConnectionPool/PooledChannel.swift b/Sources/GRPC/ConnectionPool/PooledChannel.swift index 4d2b06c49..963cc406f 100644 --- a/Sources/GRPC/ConnectionPool/PooledChannel.swift +++ b/Sources/GRPC/ConnectionPool/PooledChannel.swift @@ -104,7 +104,7 @@ internal final class PooledChannel: GRPCChannel { delegate: configuration.delegate, statsPeriod: configuration.statsPeriod ), - logger: configuration.backgroundActivityLogger.wrapped + logger: configuration.backgroundActivityLogger ) } @@ -119,7 +119,7 @@ internal final class PooledChannel: GRPCChannel { let streamChannel = self._pool.makeStream( preferredEventLoop: preferredEventLoop, deadline: deadline, - logger: GRPCLogger(wrapping: callOptions.logger) + logger: callOptions.logger ) { channel in return channel.eventLoop.makeSucceededVoidFuture() } diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift index fe29c56a2..4fbc2ed9b 100644 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ b/Sources/GRPC/GRPCClientChannelHandler.swift @@ -284,7 +284,7 @@ public enum GRPCCallType: Hashable, Sendable { /// } /// ``` internal final class GRPCClientChannelHandler { - private let logger: GRPCLogger + private let logger: Logger private var stateMachine: GRPCClientStateMachine private let maximumReceiveMessageLength: Int @@ -297,7 +297,7 @@ internal final class GRPCClientChannelHandler { internal init( callType: GRPCCallType, maximumReceiveMessageLength: Int, - logger: GRPCLogger + logger: Logger ) { self.logger = logger self.maximumReceiveMessageLength = maximumReceiveMessageLength diff --git a/Sources/GRPC/GRPCLogger.swift b/Sources/GRPC/GRPCLogger.swift deleted file mode 100644 index 2f81fac21..000000000 --- a/Sources/GRPC/GRPCLogger.swift +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 Logging -import NIOCore - -/// Wraps `Logger` to always provide the source as "GRPC". -/// -/// See https://github.com/apple/swift-log/issues/145 for rationale. -@usableFromInline -internal struct GRPCLogger { - @usableFromInline - internal var logger: Logger - - internal var unwrapped: Logger { - return self.logger - } - - @inlinable - internal init(wrapping logger: Logger) { - self.logger = logger - } - - internal subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { - get { - return self.logger[metadataKey: metadataKey] - } - set { - self.logger[metadataKey: metadataKey] = newValue - } - } - - @usableFromInline - internal func trace( - _ message: @autoclosure () -> Logger.Message, - metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #fileID, - function: String = #function, - line: UInt = #line - ) { - self.logger.trace( - message(), - metadata: metadata(), - source: "GRPC", - file: file, - function: function, - line: line - ) - } - - @usableFromInline - internal func debug( - _ message: @autoclosure () -> Logger.Message, - metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #fileID, - function: String = #function, - line: UInt = #line - ) { - self.logger.debug( - message(), - metadata: metadata(), - source: "GRPC", - file: file, - function: function, - line: line - ) - } - - @usableFromInline - internal func notice( - _ message: @autoclosure () -> Logger.Message, - metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #fileID, - function: String = #function, - line: UInt = #line - ) { - self.logger.notice( - message(), - metadata: metadata(), - source: "GRPC", - file: file, - function: function, - line: line - ) - } - - @usableFromInline - internal func warning( - _ message: @autoclosure () -> Logger.Message, - metadata: @autoclosure () -> Logger.Metadata? = nil, - file: String = #fileID, - function: String = #function, - line: UInt = #line - ) { - self.logger.warning( - message(), - metadata: metadata(), - source: "GRPC", - file: file, - function: function, - line: line - ) - } -} - -extension GRPCLogger { - internal mutating func addIPAddressMetadata(local: SocketAddress?, remote: SocketAddress?) { - self.logger.addIPAddressMetadata(local: local, remote: remote) - } -} - -extension Logger { - @inlinable - internal var wrapped: GRPCLogger { - return GRPCLogger(wrapping: self) - } -} diff --git a/Sources/GRPC/Interceptor/ClientInterceptorContext.swift b/Sources/GRPC/Interceptor/ClientInterceptorContext.swift index bfc192546..a90dead7f 100644 --- a/Sources/GRPC/Interceptor/ClientInterceptorContext.swift +++ b/Sources/GRPC/Interceptor/ClientInterceptorContext.swift @@ -36,7 +36,7 @@ public struct ClientInterceptorContext { /// A logger. public var logger: Logger { - return self._pipeline.logger.unwrapped + return self._pipeline.logger } /// The type of the RPC, e.g. "unary". diff --git a/Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift b/Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift index 37de2a928..ec767de5d 100644 --- a/Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift +++ b/Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift @@ -61,7 +61,7 @@ import NIOHTTP2 internal final class ClientInterceptorPipeline { /// A logger. @usableFromInline - internal var logger: GRPCLogger + internal var logger: Logger /// The `EventLoop` this RPC is being executed on. @usableFromInline @@ -135,7 +135,7 @@ internal final class ClientInterceptorPipeline { internal init( eventLoop: EventLoop, details: CallDetails, - logger: GRPCLogger, + logger: Logger, interceptors: [ClientInterceptor], errorDelegate: ClientErrorDelegate?, onError: @escaping (Error) -> Void, @@ -289,13 +289,13 @@ internal final class ClientInterceptorPipeline { unwrappedError = errorContext.error self._errorDelegate?.didCatchError( errorContext.error, - logger: self.logger.unwrapped, + logger: self.logger, file: errorContext.file, line: errorContext.line ) } else { unwrappedError = error - self._errorDelegate?.didCatchErrorWithoutContext(error, logger: self.logger.unwrapped) + self._errorDelegate?.didCatchErrorWithoutContext(error, logger: self.logger) } // Emit the unwrapped error. diff --git a/Sources/GRPC/Interceptor/ClientTransport.swift b/Sources/GRPC/Interceptor/ClientTransport.swift index f2ed7b86a..795b29312 100644 --- a/Sources/GRPC/Interceptor/ClientTransport.swift +++ b/Sources/GRPC/Interceptor/ClientTransport.swift @@ -68,7 +68,7 @@ internal final class ClientTransport { internal let callDetails: CallDetails /// A logger. - internal var logger: GRPCLogger + internal var logger: Logger /// Is the call streaming requests? private var isStreamingRequests: Bool { @@ -119,15 +119,14 @@ internal final class ClientTransport { self.callEventLoop = eventLoop self.callDetails = details self.onStart = onStart - let logger = GRPCLogger(wrapping: details.options.logger) - self.logger = logger + self.logger = details.options.logger self.serializer = serializer self.deserializer = deserializer // The references to self held by the pipeline are dropped when it is closed. self._pipeline = ClientInterceptorPipeline( eventLoop: eventLoop, details: details, - logger: logger, + logger: details.options.logger, interceptors: interceptors, errorDelegate: errorDelegate, onError: onError, diff --git a/Sources/GRPC/_EmbeddedThroughput.swift b/Sources/GRPC/_EmbeddedThroughput.swift index 0efcad3c0..397392d66 100644 --- a/Sources/GRPC/_EmbeddedThroughput.swift +++ b/Sources/GRPC/_EmbeddedThroughput.swift @@ -33,7 +33,7 @@ extension EmbeddedChannel { GRPCClientChannelHandler( callType: callType, maximumReceiveMessageLength: .max, - logger: GRPCLogger(wrapping: logger) + logger: logger ), GRPCClientCodecHandler( serializer: ProtobufSerializer(), diff --git a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift index ee6ca9bb3..9a1d000e0 100644 --- a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift @@ -45,7 +45,7 @@ class ClientInterceptorPipelineTests: GRPCTestCase { return ClientInterceptorPipeline( eventLoop: self.embeddedEventLoop, details: callDetails, - logger: callDetails.options.logger.wrapped, + logger: callDetails.options.logger, interceptors: interceptors, errorDelegate: errorDelegate, onError: onError, diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift index 53d44b677..d25489630 100644 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift @@ -75,7 +75,7 @@ final class ConnectionPoolTests: GRPCTestCase { onUpdateMaxAvailableStreams: onMaximumReservationsChange ), delegate: delegate, - logger: self.logger.wrapped, + logger: self.logger, now: now ) } @@ -158,7 +158,7 @@ final class ConnectionPoolTests: GRPCTestCase { } XCTAssertNoThrow(try pool.shutdown().wait()) - let stream = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let stream = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -174,12 +174,12 @@ final class ConnectionPoolTests: GRPCTestCase { } let waiting = (0 ..< maxWaiters).map { _ in - return pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + return pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } } - let tooManyWaiters = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let tooManyWaiters = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -201,7 +201,7 @@ final class ConnectionPoolTests: GRPCTestCase { self.noChannelExpected($0, $1) } - let waiter = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } XCTAssertEqual(pool.sync.waiters, 1) @@ -221,7 +221,7 @@ final class ConnectionPoolTests: GRPCTestCase { self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) - let waiter = pool.makeStream(deadline: .uptimeNanoseconds(5), logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .uptimeNanoseconds(5), logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } XCTAssertEqual(pool.sync.waiters, 1) @@ -242,7 +242,7 @@ final class ConnectionPoolTests: GRPCTestCase { // No channels yet. XCTAssertEqual(controller.count, 0) - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Start creating the channel. @@ -277,7 +277,7 @@ final class ConnectionPoolTests: GRPCTestCase { let (pool, controller) = self.setUpPoolAndController() pool.initialize(connections: 1) - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Start creating the channel. @@ -297,7 +297,7 @@ final class ConnectionPoolTests: GRPCTestCase { // connection we won't have to wait. XCTAssertEqual(pool.sync.waiters, 0) XCTAssertEqual(pool.sync.reservedStreams, 1) - let notWaiting = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let notWaiting = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Still no waiters. @@ -320,7 +320,7 @@ final class ConnectionPoolTests: GRPCTestCase { // Enqueue twice as many waiters as the connection will be able to handle. let maxConcurrentStreams = 10 let waiters = (0 ..< maxConcurrentStreams * 2).map { _ in - return pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + return pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } } @@ -373,7 +373,7 @@ final class ConnectionPoolTests: GRPCTestCase { ) pool.initialize(connections: 1) - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Start creating the channel. @@ -392,7 +392,7 @@ final class ConnectionPoolTests: GRPCTestCase { // Create a handful of streams. XCTAssertEqual(pool.sync.availableStreams, 9) for _ in 0 ..< 5 { - let notWaiting = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let notWaiting = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } self.eventLoop.run() @@ -422,7 +422,7 @@ final class ConnectionPoolTests: GRPCTestCase { // Reserve a bunch of streams. let waiters = (0 ..< 10).map { _ in - return pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + return pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } } @@ -443,7 +443,7 @@ final class ConnectionPoolTests: GRPCTestCase { // Add a waiter. XCTAssertEqual(pool.sync.waiters, 0) - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } XCTAssertEqual(pool.sync.waiters, 1) @@ -484,11 +484,11 @@ final class ConnectionPoolTests: GRPCTestCase { }) pool.initialize(connections: 1) - let waiter1 = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger.wrapped) { + let waiter1 = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } - let waiter2 = pool.makeStream(deadline: .uptimeNanoseconds(15), logger: self.logger.wrapped) { + let waiter2 = pool.makeStream(deadline: .uptimeNanoseconds(15), logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -533,7 +533,7 @@ final class ConnectionPoolTests: GRPCTestCase { // No demand so all three connections are idle. XCTAssertEqual(pool.sync.idleConnections, 3) - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -550,7 +550,7 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertNoThrow(try w1.wait()) controller.openStreamInChannel(atIndex: 0) - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -562,7 +562,7 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertEqual(pool.sync.idleConnections, 1) // Add more demand before the second connection comes up. - let w3 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w3 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -586,7 +586,7 @@ final class ConnectionPoolTests: GRPCTestCase { pool.initialize(connections: 1) XCTAssertEqual(pool.sync.connections, 1) - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Start creating the channel. @@ -619,7 +619,7 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertEqual(pool.sync.idleConnections, 1) // Ask for another stream: this will be on the new idle connection. - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } self.eventLoop.run() @@ -664,7 +664,7 @@ final class ConnectionPoolTests: GRPCTestCase { pool.initialize(connections: 1) XCTAssertEqual(pool.sync.connections, 1) - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Start creating the channel. @@ -686,13 +686,13 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertEqual(pool.sync.idleConnections, 0) // Enqueue two waiters. One to time out before the reconnect happens. - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } let w3 = pool.makeStream( deadline: .uptimeNanoseconds(UInt64(TimeAmount.milliseconds(500).nanoseconds)), - logger: self.logger.wrapped + logger: self.logger ) { $0.eventLoop.makeSucceededVoidFuture() } @@ -742,11 +742,11 @@ final class ConnectionPoolTests: GRPCTestCase { // passed but no connection has previously failed) // - w2 will fail because of a timeout but after the underlying channel has failed to connect so // should have that additional failure information. - let w1 = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger.wrapped) { + let w1 = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } - let w2 = pool.makeStream(deadline: .uptimeNanoseconds(20), logger: self.logger.wrapped) { + let w2 = pool.makeStream(deadline: .uptimeNanoseconds(20), logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -803,7 +803,7 @@ final class ConnectionPoolTests: GRPCTestCase { // These streams should succeed when the new connection is up. We'll limit the connection to 10 // streams when we bring it up. let streams = (0 ..< 10).map { _ in - pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } } @@ -832,14 +832,14 @@ final class ConnectionPoolTests: GRPCTestCase { let now = NIODeadline.now() self.eventLoop.advanceTime(to: now) let waiters = (0 ..< 10).map { _ in - pool.makeStream(deadline: now + .seconds(1), logger: self.logger.wrapped) { + pool.makeStream(deadline: now + .seconds(1), logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } } // This is one waiter more than is allowed so it should hit too-many-waiters. We don't expect // an inner error though, the connection is just busy. - let tooManyWaiters = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let tooManyWaiters = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } XCTAssertThrowsError(try tooManyWaiters.wait()) { error in @@ -893,7 +893,7 @@ final class ConnectionPoolTests: GRPCTestCase { }) pool.initialize(connections: 1) - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Start creating the channel. @@ -950,7 +950,7 @@ final class ConnectionPoolTests: GRPCTestCase { let connID1 = try assertConnectionAdded(recorder.popFirst()) let connID2 = try assertConnectionAdded(recorder.popFirst()) - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } // Start creating the channel. @@ -984,7 +984,7 @@ final class ConnectionPoolTests: GRPCTestCase { // Okay, more utilization! for n in 2 ... 8 { - let w = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -996,7 +996,7 @@ final class ConnectionPoolTests: GRPCTestCase { // The utilisation threshold before bringing up a new connection is 0.9; we have 8 open streams // (out of 10) now so opening the next should trigger a connect on the other connection. - let w9 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w9 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } XCTAssertEqual(recorder.popFirst(), .startedConnecting(secondConn)) @@ -1013,7 +1013,7 @@ final class ConnectionPoolTests: GRPCTestCase { XCTAssertEqual(recorder.popFirst(), .connectSucceeded(secondConn, 10)) // The next stream should be on the new connection. - let w10 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w10 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } @@ -1107,10 +1107,10 @@ final class ConnectionPoolTests: GRPCTestCase { // Open two streams, which, because the maxConcurrentStreams is 1, will // create two channels. - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger.wrapped) { + let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { $0.eventLoop.makeSucceededVoidFuture() } diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift index 096bf16b4..ed956d740 100644 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift @@ -43,7 +43,7 @@ class PoolManagerStateMachineTests: GRPCTestCase { onUpdateMaxAvailableStreams: { _ in } ), delegate: nil, - logger: self.logger.wrapped + logger: self.logger ) } diff --git a/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift b/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift index 1f0757177..85feb8ea5 100644 --- a/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift +++ b/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift @@ -39,7 +39,7 @@ class GRPCClientChannelHandlerTests: GRPCTestCase { let handler = GRPCClientChannelHandler( callType: .unary, maximumReceiveMessageLength: .max, - logger: GRPCLogger(wrapping: self.clientLogger) + logger: self.clientLogger ) let channel = EmbeddedChannel(handler: handler) diff --git a/Tests/GRPCTests/GRPCLoggerTests.swift b/Tests/GRPCTests/GRPCLoggerTests.swift deleted file mode 100644 index 73f7ac0f8..000000000 --- a/Tests/GRPCTests/GRPCLoggerTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 Logging -import XCTest - -@testable import GRPC - -final class GRPCLoggerTests: GRPCTestCase { - func testLogSourceIsGRPC() { - let recorder = CapturingLogHandlerFactory(printWhenCaptured: false) - let logger = Logger(label: "io.grpc.testing", factory: recorder.make(_:)) - - var gRPCLogger = GRPCLogger(wrapping: logger) - gRPCLogger[metadataKey: "foo"] = "bar" - - gRPCLogger.debug("foo") - gRPCLogger.trace("bar") - - let logs = recorder.clearCapturedLogs() - XCTAssertEqual(logs.count, 2) - for log in logs { - XCTAssertEqual(log.source, "GRPC") - XCTAssertEqual(gRPCLogger[metadataKey: "foo"], "bar") - } - } -} diff --git a/Tests/GRPCTests/GRPCStatusCodeTests.swift b/Tests/GRPCTests/GRPCStatusCodeTests.swift index 58d2eec79..227645d6a 100644 --- a/Tests/GRPCTests/GRPCStatusCodeTests.swift +++ b/Tests/GRPCTests/GRPCStatusCodeTests.swift @@ -34,7 +34,7 @@ class GRPCStatusCodeTests: GRPCTestCase { let handler = GRPCClientChannelHandler( callType: .unary, maximumReceiveMessageLength: .max, - logger: GRPCLogger(wrapping: self.logger) + logger: self.logger ) self.channel = EmbeddedChannel(handler: handler) } diff --git a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift index 3d7917ac9..2b18bcf28 100644 --- a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift +++ b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift @@ -15,6 +15,7 @@ */ import EchoImplementation import EchoModel +import Logging import NIOCore import NIOPosix import XCTest @@ -86,10 +87,10 @@ final class MagicRequiredServerInterceptor< switch part { case let .metadata(metadata): if metadata.contains(name: "magic") { - context.log.debug("metadata contains magic; accepting rpc") + context.logger.debug("metadata contains magic; accepting rpc") context.receive(part) } else { - context.log.debug("metadata does not contains magic; rejecting rpc") + context.logger.debug("metadata does not contains magic; rejecting rpc") let status = GRPCStatus(code: .permissionDenied, message: nil) context.send(.end(status, [:]), promise: nil) } @@ -116,7 +117,7 @@ final class MagicAddingClientInterceptor< context: ClientInterceptorContext ) { if let retry = self.retry { - context.log.debug("cancelling retry RPC") + context.logger.debug("cancelling retry RPC") retry.cancel(promise: promise) } else { context.cancel(promise: promise) @@ -129,7 +130,7 @@ final class MagicAddingClientInterceptor< context: ClientInterceptorContext ) { if let retry = self.retry { - context.log.debug("retrying part \(part)") + context.logger.debug("retrying part \(part)") retry.send(part, promise: promise) } else { switch part { @@ -161,7 +162,7 @@ final class MagicAddingClientInterceptor< XCTAssertNil(self.retry) - context.log.debug("initial rpc failed, retrying") + context.logger.debug("initial rpc failed, retrying") self.retry = self.channel.makeCall( path: context.path, @@ -171,33 +172,17 @@ final class MagicAddingClientInterceptor< ) self.retry!.invoke { - context.log.debug("intercepting error from retried rpc") + context.logger.debug("intercepting error from retried rpc") context.errorCaught($0) } onResponsePart: { responsePart in - context.log.debug("intercepting response part from retried rpc") + context.logger.debug("intercepting response part from retried rpc") context.receive(responsePart) } while let requestPart = self.requestParts.popFirst() { - context.log.debug("replaying \(requestPart) on new rpc") + context.logger.debug("replaying \(requestPart) on new rpc") self.retry!.send(requestPart, promise: nil) } } } } - -// MARK: - GRPC Logger - -// Our tests also check the "Source" of a logger is "GRPC". That assertion fails when we log from -// tests so we'll use our internal logger instead. -extension ClientInterceptorContext { - var log: GRPCLogger { - return GRPCLogger(wrapping: self.logger) - } -} - -extension ServerInterceptorContext { - var log: GRPCLogger { - return GRPCLogger(wrapping: self.logger) - } -} From 4fc6c25881151fa62c5cb02a9a6115c8e223a72a Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:29:16 +0100 Subject: [PATCH 287/580] Performance worker - client stats for runClient RPC (#1851) * Performance worker - configuring clients for runClient RPC Motivation: The `runClient()` RPC receives a stream of messages that can either request to set up clients based on some parameters sent in the message or send back stats on the currently running clients. Modifications: - implemented the resource usage class for clients similar to the server stats one - implemented the LatencyHistogram struct - added a new abstraction layer over the GRPCClient - BenchmarkClient. It stores parameters for the client extracted from tne input messages and it implements the `run()` method that starts the client abd makes the given number of RPCs, while registering the latency for each one, and at the end it creates a histogram containing all the latencies. Result: Configuring the clients for the performance tests is now possible and the latency histogram gets created. --- .../performance-worker/BenchmarkClient.swift | 96 +++++++++++++ Sources/performance-worker/RPCStats.swift | 132 ++++++++++++++++++ ...{ServerStats.swift => ResourceUsage.swift} | 56 +++++++- .../performance-worker/WorkerService.swift | 39 +++++- 4 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 Sources/performance-worker/BenchmarkClient.swift create mode 100644 Sources/performance-worker/RPCStats.swift rename Sources/performance-worker/{ServerStats.swift => ResourceUsage.swift} (76%) diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift new file mode 100644 index 000000000..7cecbc4ac --- /dev/null +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -0,0 +1,96 @@ +/* + * 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 Foundation +import GRPCCore +import NIOConcurrencyHelpers + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct BenchmarkClient { + private var client: GRPCClient + private var rpcNumber: Int32 + private var rpcType: Grpc_Testing_RpcType + private let rpcStats: NIOLockedValueBox + + init( + client: GRPCClient, + rpcNumber: Int32, + rpcType: Grpc_Testing_RpcType, + histogramParams: Grpc_Testing_HistogramParams? + ) { + self.client = client + self.rpcNumber = rpcNumber + self.rpcType = rpcType + + let histogram: RPCStats.LatencyHistogram + if let histogramParams = histogramParams { + histogram = .init( + resolution: histogramParams.resolution, + maxBucketStart: histogramParams.maxPossible + ) + } else { + histogram = .init() + } + + self.rpcStats = NIOLockedValueBox(RPCStats(latencyHistogram: histogram)) + } + + internal func run() async throws { + let benchmarkClient = Grpc_Testing_BenchmarkServiceClient(client: client) + return try await withThrowingTaskGroup(of: Void.self) { clientGroup in + // Start the client. + clientGroup.addTask { try await client.run() } + + // Make the requests to the server and register the latency for each one. + try await withThrowingTaskGroup(of: Void.self) { rpcsGroup in + for _ in 0 ..< self.rpcNumber { + rpcsGroup.addTask { + let (latency, errorCode) = self.makeRPC(client: benchmarkClient, rpcType: self.rpcType) + self.rpcStats.withLockedValue { + $0.latencyHistogram.record(latency) + if let errorCode = errorCode { + $0.requestResultCount[errorCode, default: 1] += 1 + } + } + } + } + try await rpcsGroup.waitForAll() + } + + try await clientGroup.next() + } + } + + // The result is the number of nanoseconds for processing the RPC. + private func makeRPC( + client: Grpc_Testing_BenchmarkServiceClient, + rpcType: Grpc_Testing_RpcType + ) -> (latency: Double, errorCode: RPCError.Code?) { + switch rpcType { + case .unary, .streaming, .streamingFromClient, .streamingFromServer, .streamingBothWays, + .UNRECOGNIZED: + let startTime = DispatchTime.now().uptimeNanoseconds + let endTime = DispatchTime.now().uptimeNanoseconds + return ( + latency: Double(endTime - startTime), errorCode: RPCError.Code(.unimplemented) + ) + } + } + + internal func shutdown() { + self.client.close() + } +} diff --git a/Sources/performance-worker/RPCStats.swift b/Sources/performance-worker/RPCStats.swift new file mode 100644 index 000000000..3b3aeb9f3 --- /dev/null +++ b/Sources/performance-worker/RPCStats.swift @@ -0,0 +1,132 @@ +/* + * 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 Foundation +import GRPCCore +import NIOConcurrencyHelpers + +/// Stores the real time latency histogram and error code count dictionary, +/// for the RPCs made by a particular GRPCClient. It gets updated after +/// each finished RPC. +/// +/// The time latency is measured in nanoseconds. +struct RPCStats { + var latencyHistogram: LatencyHistogram + var requestResultCount: [RPCError.Code: Int64] + + init(latencyHistogram: LatencyHistogram, requestResultCount: [RPCError.Code: Int64] = [:]) { + self.latencyHistogram = latencyHistogram + self.requestResultCount = requestResultCount + } + + /// Histograms are stored with exponentially increasing bucket sizes. + /// The first bucket is [0, `multiplier`) where `multiplier` = 1 + resolution + /// Bucket n (n>=1) contains [`multiplier`**n, `multiplier`**(n+1)) + /// There are sufficient buckets to reach max_bucket_start + struct LatencyHistogram { + var sum: Double + var sumOfSquares: Double + var countOfValuesSeen: Double + var multiplier: Double + var oneOnLogMultiplier: Double + var minSeen: Double + var maxSeen: Double + var maxPossible: Double + var buckets: [UInt32] + + /// Initialise a histogram. + /// - parameters: + /// - resolution: Defines the width of the buckets - see the description of this structure. + /// - maxBucketStart: Defines the start of the greatest valued bucket. + init(resolution: Double = 0.01, maxBucketStart: Double = 60e9) { + precondition(resolution > 0.0) + precondition(maxBucketStart > resolution) + self.sum = 0.0 + self.sumOfSquares = 0.0 + self.multiplier = 1.0 + resolution + self.oneOnLogMultiplier = 1.0 / log(1.0 + resolution) + self.maxPossible = maxBucketStart + self.countOfValuesSeen = 0.0 + self.minSeen = maxBucketStart + self.maxSeen = 0.0 + let numBuckets = + LatencyHistogram.uncheckedBucket( + forValue: maxBucketStart, + oneOnLogMultiplier: self.oneOnLogMultiplier + ) + 1 + precondition(numBuckets > 1) + precondition(numBuckets < 100_000_000) + self.buckets = .init(repeating: 0, count: numBuckets) + } + + struct HistorgramShapeMismatch: Error {} + + /// Determine a bucket index given a value - does no bounds checking + private static func uncheckedBucket(forValue value: Double, oneOnLogMultiplier: Double) -> Int { + return Int(log(value) * oneOnLogMultiplier) + } + + private func bucket(forValue value: Double) -> Int { + let bucket = LatencyHistogram.uncheckedBucket( + forValue: min(self.maxPossible, max(0, value)), + oneOnLogMultiplier: self.oneOnLogMultiplier + ) + assert(bucket < self.buckets.count) + assert(bucket >= 0) + return bucket + } + + /// Add a value to this histogram, updating buckets and stats + /// - parameters: + /// - value: The value to add. + public mutating func record(_ value: Double) { + self.sum += value + self.sumOfSquares += value * value + self.countOfValuesSeen += 1 + if value < self.minSeen { + self.minSeen = value + } + if value > self.maxSeen { + self.maxSeen = value + } + self.buckets[self.bucket(forValue: value)] += 1 + } + + /// Merge two histograms together updating `self` + /// - parameters: + /// - source: the other histogram to merge into this. + public mutating func merge(_ other: LatencyHistogram) throws { + guard (self.buckets.count == other.buckets.count) || (self.multiplier == other.multiplier) + else { + // Fail because these histograms don't match. + throw HistorgramShapeMismatch() + } + + self.sum += other.sum + self.sumOfSquares += other.sumOfSquares + self.countOfValuesSeen += other.countOfValuesSeen + if other.minSeen < self.minSeen { + self.minSeen = other.minSeen + } + if other.maxSeen > self.maxSeen { + self.maxSeen = other.maxSeen + } + for bucket in 0 ..< self.buckets.count { + self.buckets[bucket] += other.buckets[bucket] + } + } + } +} diff --git a/Sources/performance-worker/ServerStats.swift b/Sources/performance-worker/ResourceUsage.swift similarity index 76% rename from Sources/performance-worker/ServerStats.swift rename to Sources/performance-worker/ResourceUsage.swift index 44d7c8b50..7582a8328 100644 --- a/Sources/performance-worker/ServerStats.swift +++ b/Sources/performance-worker/ResourceUsage.swift @@ -34,7 +34,44 @@ private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF.rawValue #endif -/// Current server stats. +/// Client resource usage stats. +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +internal struct ClientStats: Sendable { + var time: Double + var userTime: Double + var systemTime: Double + + init( + time: Double, + userTime: Double, + systemTime: Double + ) { + self.time = time + self.userTime = userTime + self.systemTime = systemTime + } + + init() { + self.time = Double(DispatchTime.now().uptimeNanoseconds) * 1e-9 + if let usage = System.resourceUsage() { + self.userTime = Double(usage.ru_utime.tv_sec) + Double(usage.ru_utime.tv_usec) * 1e-6 + self.systemTime = Double(usage.ru_stime.tv_sec) + Double(usage.ru_stime.tv_usec) * 1e-6 + } else { + self.userTime = 0 + self.systemTime = 0 + } + } + + internal func difference(to state: ClientStats) -> ClientStats { + return ClientStats( + time: self.time - state.time, + userTime: self.userTime - state.userTime, + systemTime: self.systemTime - state.systemTime + ) + } +} + +/// Server resource usage stats. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) internal struct ServerStats: Sendable { var time: Double @@ -59,10 +96,7 @@ internal struct ServerStats: Sendable { init() async throws { self.time = Double(DispatchTime.now().uptimeNanoseconds) * 1e-9 - var usage = rusage() - if getrusage(OUR_RUSAGE_SELF, &usage) == 0 { - // Adding the seconds with the microseconds transformed into seconds to get the - // real number of seconds as a `Double`. + if let usage = System.resourceUsage() { self.userTime = Double(usage.ru_utime.tv_sec) + Double(usage.ru_utime.tv_usec) * 1e-6 self.systemTime = Double(usage.ru_stime.tv_sec) + Double(usage.ru_stime.tv_usec) * 1e-6 } else { @@ -128,3 +162,15 @@ internal struct ServerStats: Sendable { #endif } } + +extension System { + fileprivate static func resourceUsage() -> rusage? { + var usage = rusage() + + if getrusage(OUR_RUSAGE_SELF, &usage) == 0 { + return usage + } else { + return nil + } + } +} diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 845c01fff..74934ca4d 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -31,7 +31,7 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable var role: Role? enum Role { - case client(GRPCClient) + case client(ClientState) case server(ServerState) } @@ -45,6 +45,25 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } } + struct ClientState { + var clients: [BenchmarkClient] + var stats: ClientStats + + init( + clients: [BenchmarkClient], + stats: ClientStats + ) { + self.clients = clients + self.stats = stats + } + + func shutdownClients() throws { + for benchmarkClient in self.clients { + benchmarkClient.shutdown() + } + } + } + init() {} init(role: Role) { @@ -74,6 +93,20 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } } + mutating func clientStats(replaceWith newStats: ClientStats? = nil) -> ClientStats? { + switch self.role { + case var .client(clientState): + let stats = clientState.stats + if let newStats = newStats { + clientState.stats = newStats + self.role = .client(clientState) + } + return stats + case .server, .none: + return nil + } + } + mutating func setupServer(server: GRPCServer, stats: ServerStats) throws { let serverState = State.ServerState(server: server, stats: stats) switch self.role { @@ -100,8 +133,8 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable if let role = role { switch role { - case .client(let client): - client.close() + case .client(let clientState): + try clientState.shutdownClients() case .server(let serverState): serverState.server.stopListening() } From 81e4529821baf64cf13d8abdaddc8d951c522c18 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 10 Apr 2024 15:35:56 +0100 Subject: [PATCH 288/580] Allow the client connection handler to be closed gracefully (#1855) Motivation: There's currently no way to gracefully closing the client connection. Modifications: - Add a user outbound event to the `ClientConnectionHandler` to gracefully shutdown the connection. Result: Client connection can be instructed to close gracefully. --- .../Connection/ClientConnectionHandler.swift | 91 +++++++++++++++++-- ...ntConnectionHandlerStateMachineTests.swift | 8 +- .../ClientConnectionHandlerTests.swift | 18 ++++ 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 0b7f27791..8910e4598 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -26,6 +26,8 @@ enum ClientConnectionEvent: Sendable, Hashable { case keepAliveExpired /// The connection became idle. case idle + /// The local peer initiated the close. + case initiatedLocally } /// The connection has started shutting down, no new streams should be created. @@ -48,6 +50,11 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl typealias OutboundIn = Never typealias OutboundOut = HTTP2Frame + enum OutboundEvent: Hashable, Sendable { + /// Close the connection gracefully + case closeGracefully + } + /// The `EventLoop` of the `Channel` this handler exists in. private let eventLoop: EventLoop @@ -120,7 +127,12 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } func channelInactive(context: ChannelHandlerContext) { - self.state.closed() + switch self.state.closed() { + case .none: + () + case .succeed(let promise): + promise.succeed() + } self.keepAliveTimer?.cancel() self.keepAliveTimeoutTimer.cancel() } @@ -169,7 +181,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl case .goAway(_, let errorCode, let data): // Receiving a GOAWAY frame means we need to stop creating streams immediately and start // closing the connection. - switch self.state.beginGracefulShutdown() { + switch self.state.beginGracefulShutdown(promise: nil) { case .sendGoAway(let close): // gRPC servers may indicate why the GOAWAY was sent in the opaque data. let message = data.map { String(buffer: $0) } ?? "" @@ -209,6 +221,32 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl self.inReadLoop = false context.fireChannelReadComplete() } + + func triggerUserOutboundEvent( + context: ChannelHandlerContext, + event: Any, + promise: EventLoopPromise? + ) { + if let event = event as? OutboundEvent { + switch event { + case .closeGracefully: + switch self.state.beginGracefulShutdown(promise: promise) { + case .sendGoAway(let close): + context.fireChannelRead(self.wrapInboundOut(.closing(.initiatedLocally))) + // Clients should send GOAWAYs when closing a connection. + self.writeAndFlushGoAway(context: context, errorCode: .noError) + if close { + context.close(promise: nil) + } + + case .none: + () + } + } + } else { + context.triggerUserOutboundEvent(event, promise: promise) + } + } } extension ClientConnectionHandler { @@ -294,10 +332,12 @@ extension ClientConnectionHandler { struct Closing { var allowKeepAliveWithoutCalls: Bool var openStreams: Set + var closePromise: Optional> - init(from state: Active) { + init(from state: Active, closePromise: EventLoopPromise?) { self.openStreams = state.openStreams self.allowKeepAliveWithoutCalls = state.allowKeepAliveWithoutCalls + self.closePromise = closePromise } } } @@ -384,7 +424,7 @@ extension ClientConnectionHandler { case none } - mutating func beginGracefulShutdown() -> OnGracefulShutDown { + mutating func beginGracefulShutdown(promise: EventLoopPromise?) -> OnGracefulShutDown { let onGracefulShutdown: OnGracefulShutDown switch self.state { @@ -393,9 +433,14 @@ extension ClientConnectionHandler { // ratchet down the last stream ID as only the client creates streams in gRPC. let close = state.openStreams.isEmpty onGracefulShutdown = .sendGoAway(close) - self.state = .closing(State.Closing(from: state)) + self.state = .closing(State.Closing(from: state, closePromise: promise)) - case .closing, .closed: + case .closing(var state): + state.closePromise.setOrCascade(to: promise) + self.state = .closing(state) + onGracefulShutdown = .none + + case .closed: onGracefulShutdown = .none } @@ -406,16 +451,44 @@ extension ClientConnectionHandler { mutating func beginClosing() -> Bool { switch self.state { case .active(let active): - self.state = .closing(State.Closing(from: active)) + self.state = .closing(State.Closing(from: active, closePromise: nil)) return true case .closing, .closed: return false } } + enum OnClosed { + case succeed(EventLoopPromise) + case none + } + /// Marks the state as closed. - mutating func closed() { - self.state = .closed + mutating func closed() -> OnClosed { + switch self.state { + case .active, .closed: + self.state = .closed + return .none + case .closing(let closing): + self.state = .closed + return closing.closePromise.map { .succeed($0) } ?? .none + } + } + } +} + +extension Optional { + // TODO: replace with https://github.com/apple/swift-nio/pull/2697 + mutating func setOrCascade( + to promise: EventLoopPromise? + ) where Wrapped == EventLoopPromise { + guard let promise = promise else { return } + + switch self { + case .none: + self = .some(promise) + case .some(let existing): + existing.futureResult.cascade(to: promise) } } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift index 206c317e4..a0037f49f 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift @@ -46,7 +46,7 @@ final class ClientConnectionHandlerStateMachineTests: XCTestCase { func testOpenAndCloseStreamWhenClosed() { var state = self.makeStateMachine() - state.closed() + _ = state.closed() state.streamOpened(1) XCTAssertEqual(state.streamClosed(1), .none) } @@ -88,7 +88,7 @@ final class ClientConnectionHandlerStateMachineTests: XCTestCase { func testSendKeepAlivePingWhenClosed() { var state = self.makeStateMachine(keepAliveWithoutCalls: true) - state.closed() + _ = state.closed() XCTAssertFalse(state.sendKeepAlivePing()) } @@ -96,12 +96,12 @@ final class ClientConnectionHandlerStateMachineTests: XCTestCase { var state = self.makeStateMachine() state.streamOpened(1) // Close is false as streams are still open. - XCTAssertEqual(state.beginGracefulShutdown(), .sendGoAway(false)) + XCTAssertEqual(state.beginGracefulShutdown(promise: nil), .sendGoAway(false)) } func testBeginGracefulShutdownWhenNoStreamsAreOpen() { var state = self.makeStateMachine() // Close immediately, not streams are open. - XCTAssertEqual(state.beginGracefulShutdown(), .sendGoAway(true)) + XCTAssertEqual(state.beginGracefulShutdown(promise: nil), .sendGoAway(true)) } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 882086751..e4a296be4 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -195,6 +195,17 @@ final class ClientConnectionHandlerTests: XCTestCase { connection.streamClosed(3) try connection.waitUntilClosed() } + + func testOutboundGracefulClose() throws { + let connection = try Connection() + try connection.activate() + + connection.streamOpened(1) + let closed = connection.closeGracefully() + XCTAssertEqual(try connection.readEvent(), .closing(.initiatedLocally)) + connection.streamClosed(1) + try closed.wait() + } } extension ClientConnectionHandlerTests { @@ -270,5 +281,12 @@ extension ClientConnectionHandlerTests { self.channel.embeddedEventLoop.run() try self.channel.closeFuture.wait() } + + func closeGracefully() -> EventLoopFuture { + let promise = self.channel.embeddedEventLoop.makePromise(of: Void.self) + let event = ClientConnectionHandler.OutboundEvent.closeGracefully + self.channel.pipeline.triggerUserOutboundEvent(event, promise: promise) + return promise.futureResult + } } } From 04dc601d57bd1fe62ecae5405ccaf8b974919201 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 11 Apr 2024 16:23:22 +0100 Subject: [PATCH 289/580] Add call options (#1854) Motivation: The API currently doesn't allow for per-method options to be passed to each call. Users must do so by setting method config on the client. This isn't particularly ergonomic and also constrains options to the method config which is insufficient as at a call level users should be able to control compression as well. Moreover, these options also need to percolate further down the stack as they may be used by the transport when creating a stream (compression and max message size are some examples). Modifications: - Add `CallOptions`, which contains the options in method config but also includes compression options. - Percolate these changes down the stack by making them a required paramter to the `makeStream` function on the `ClientTransport`. - Remove the client configuration as the default configuration should be set by the transport and overrides are set at the call level. Result: - Easier to configure per-call options --- .../GRPCCore/Call/Client/CallOptions.swift | 171 ++++++++++++++++++ .../ClientRPCExecutor+HedgingExecutor.swift | 10 +- .../ClientRPCExecutor+OneShotExecutor.swift | 3 +- .../ClientRPCExecutor+RetryExecutor.swift | 6 +- .../Client/Internal/ClientRPCExecutor.swift | 13 +- .../Configuration/MethodConfiguration.swift | 53 +++++- Sources/GRPCCore/GRPCClient.swift | 104 ++--------- .../GRPCCore/Transport/ClientTransport.swift | 2 + .../InProcessClientTransport.swift | 2 + .../ClientRPCExecutorTestHarness.swift | 38 ++-- .../ClientRPCExecutorTests+Hedging.swift | 16 +- .../ClientRPCExecutorTests+Retries.swift | 33 ++-- .../MethodConfigurationCodingTests.swift | 4 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 56 ++++-- .../Transport/AnyTransport.swift | 8 +- .../Transport/StreamCountingTransport.swift | 6 +- .../Transport/ThrowingTransport.swift | 1 + .../InProcessClientTransportTests.swift | 25 ++- 18 files changed, 385 insertions(+), 166 deletions(-) create mode 100644 Sources/GRPCCore/Call/Client/CallOptions.swift diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift new file mode 100644 index 000000000..660c55cd5 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -0,0 +1,171 @@ +/* + * 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. + */ + +/// Options applied to a call. +/// +/// If set, these options are used in preference to any options configured on +/// the client or its transport. +/// +/// You can create the default set of options, which defers all possible +/// configuration to the transport, by using ``CallOptions/defaults``. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct CallOptions: Sendable { + /// The default timeout for the RPC. + /// + /// If no reply is received in the specified amount of time the request is aborted + /// with an ``RPCError`` with code ``RPCError/Code/deadlineExceeded``. + /// + /// The actual deadline used will be the minimum of the value specified here + /// and the value set by the application by the client API. If either one isn't set + /// then the other value is used. If neither is set then the request has no deadline. + /// + /// The timeout applies to the overall execution of an RPC. If, for example, a retry + /// policy is set then the timeout begins when the first attempt is started and _isn't_ reset + /// when subsequent attempts start. + public var timeout: Duration? + + /// Whether RPCs for this method should wait until the connection is ready. + /// + /// If `false` the RPC will abort immediately if there is a transient failure connecting to + /// the server. Otherwise gRPC will attempt to connect until the deadline is exceeded. + public var waitForReady: Bool? + + /// The maximum allowed payload size in bytes for an individual request message. + /// + /// If a client attempts to send an object larger than this value, it will not be sent and the + /// client will see an error. Note that 0 is a valid value, meaning that the request message + /// must be empty. + /// + /// Note that if compression is used the uncompressed message size is validated. + public var maxRequestMessageBytes: Int? + + /// The maximum allowed payload size in bytes for an individual response message. + /// + /// If a server attempts to send an object larger than this value, it will not + /// be sent, and an error will be sent to the client instead. Note that 0 is a valid value, + /// meaning that the response message must be empty. + /// + /// Note that if compression is used the uncompressed message size is validated. + public var maxResponseMessageBytes: Int? + + /// The policy determining how many times, and when, the RPC is executed. + /// + /// There are two policy types: + /// 1. Retry + /// 2. Hedging + /// + /// The retry policy allows an RPC to be retried a limited number of times if the RPC + /// fails with one of the configured set of status codes. RPCs are only retried if they + /// fail immediately, that is, the first response part received from the server is a + /// status code. + /// + /// The hedging policy allows an RPC to be executed multiple times concurrently. Typically + /// each execution will be staggered by some delay. The first successful response will be + /// 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 + + public struct Compression: Sendable { + /// Whether request messages should be compressed. + /// + /// Note that this option is _advisory_: transports are not required to support compression. + public var requests: Bool + + /// Whether response messages are permitted to be compressed. + public var responses: Bool + + /// 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) + } + } + + internal init( + timeout: Duration?, + waitForReady: Bool?, + maxRequestMessageBytes: Int?, + maxResponseMessageBytes: Int?, + executionPolicy: RPCExecutionPolicy?, + compression: Compression + ) { + self.timeout = timeout + self.waitForReady = waitForReady + self.maxRequestMessageBytes = maxRequestMessageBytes + self.maxResponseMessageBytes = maxResponseMessageBytes + self.executionPolicy = executionPolicy + self.compression = compression + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +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``. + public static var defaults: Self { + Self( + timeout: nil, + waitForReady: nil, + maxRequestMessageBytes: nil, + maxResponseMessageBytes: nil, + executionPolicy: nil, + compression: .disabled + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension CallOptions { + mutating func formUnion(with methodConfig: MethodConfiguration?) { + guard let methodConfig = methodConfig else { return } + + self.timeout.setIfNone(to: methodConfig.timeout) + self.waitForReady.setIfNone(to: methodConfig.waitForReady) + self.maxRequestMessageBytes.setIfNone(to: methodConfig.maxRequestMessageBytes) + self.maxResponseMessageBytes.setIfNone(to: methodConfig.maxResponseMessageBytes) + self.executionPolicy.setIfNone(to: methodConfig.executionPolicy) + } +} + +extension Optional { + fileprivate mutating func setIfNone(to value: Self) { + switch self { + case .some: + () + case .none: + self = value + } + } +} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 676dc8d37..122d3ef0d 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -69,6 +69,7 @@ extension ClientRPCExecutor.HedgingExecutor { func execute( request: ClientRequest.Stream, method: MethodDescriptor, + options: CallOptions, responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R { // The high level approach is to have two levels of task group. In the outer level tasks are @@ -109,6 +110,7 @@ extension ClientRPCExecutor.HedgingExecutor { let result = await self.executeAttempt( request: replayableRequest, method: method, + options: options, responseHandler: responseHandler ) @@ -149,6 +151,7 @@ extension ClientRPCExecutor.HedgingExecutor { func executeAttempt( request: ClientRequest.Stream, method: MethodDescriptor, + options: CallOptions, responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async -> Result { await withTaskGroup( @@ -182,6 +185,7 @@ extension ClientRPCExecutor.HedgingExecutor { let result = await self._startAttempt( request: request, method: method, + options: options, attempt: attempt, state: state, picker: picker, @@ -211,6 +215,7 @@ extension ClientRPCExecutor.HedgingExecutor { let result = await self._startAttempt( request: request, method: method, + options: options, attempt: attempt, state: state, picker: picker, @@ -259,6 +264,7 @@ extension ClientRPCExecutor.HedgingExecutor { let result = await self._startAttempt( request: request, method: method, + options: options, attempt: attempt, state: state, picker: picker, @@ -305,6 +311,7 @@ extension ClientRPCExecutor.HedgingExecutor { func _startAttempt( request: ClientRequest.Stream, method: MethodDescriptor, + options: CallOptions, attempt: Int, state: LockedValueBox, picker: (stream: BroadcastAsyncSequence, continuation: BroadcastAsyncSequence.Source), @@ -312,7 +319,8 @@ extension ClientRPCExecutor.HedgingExecutor { ) async -> _HedgingAttemptTaskResult.AttemptResult { do { return try await self.transport.withStream( - descriptor: method + descriptor: method, + options: options ) { stream -> _HedgingAttemptTaskResult.AttemptResult in return await withTaskGroup(of: _HedgingAttemptSubtaskResult.self) { group in group.addTask { diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index 291f52da9..ad741a928 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -63,6 +63,7 @@ extension ClientRPCExecutor.OneShotExecutor { func execute( request: ClientRequest.Stream, method: MethodDescriptor, + options: CallOptions, responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R { let result = await withTaskGroup( @@ -70,7 +71,7 @@ extension ClientRPCExecutor.OneShotExecutor { returning: Result.self ) { group in do { - return try await self.transport.withStream(descriptor: method) { stream in + return try await self.transport.withStream(descriptor: method, options: options) { stream in if let timeout = self.timeout { group.addTask { let result = await Result { diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 1f9fd549a..5ac25a692 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -69,6 +69,7 @@ extension ClientRPCExecutor.RetryExecutor { func execute( request: ClientRequest.Stream, method: MethodDescriptor, + options: CallOptions, responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R { // There's quite a lot going on here... @@ -119,7 +120,10 @@ extension ClientRPCExecutor.RetryExecutor { for attempt in 1 ... self.policy.maximumAttempts { do { - let attemptResult = try await self.transport.withStream(descriptor: method) { stream in + let attemptResult = try await self.transport.withStream( + descriptor: method, + options: options + ) { stream in group.addTask { await withTaskGroup( of: _RetryExecutorSubTask.self, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index d91ec3d46..db95e3501 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -35,18 +35,18 @@ enum ClientRPCExecutor { static func execute( request: ClientRequest.Stream, method: MethodDescriptor, - configuration: MethodConfiguration, + options: CallOptions, serializer: some MessageSerializer, deserializer: some MessageDeserializer, transport: some ClientTransport, interceptors: [any ClientInterceptor], handler: @Sendable @escaping (ClientResponse.Stream) async throws -> Result ) async throws -> Result { - switch configuration.executionPolicy { + switch options.executionPolicy?.wrapped { case .none: let oneShotExecutor = OneShotExecutor( transport: transport, - timeout: configuration.timeout, + timeout: options.timeout, interceptors: interceptors, serializer: serializer, deserializer: deserializer @@ -55,6 +55,7 @@ enum ClientRPCExecutor { return try await oneShotExecutor.execute( request: request, method: method, + options: options, responseHandler: handler ) @@ -62,7 +63,7 @@ enum ClientRPCExecutor { let retryExecutor = RetryExecutor( transport: transport, policy: policy, - timeout: configuration.timeout, + timeout: options.timeout, interceptors: interceptors, serializer: serializer, deserializer: deserializer, @@ -72,6 +73,7 @@ enum ClientRPCExecutor { return try await retryExecutor.execute( request: request, method: method, + options: options, responseHandler: handler ) @@ -79,7 +81,7 @@ enum ClientRPCExecutor { let hedging = HedgingExecutor( transport: transport, policy: policy, - timeout: configuration.timeout, + timeout: options.timeout, interceptors: interceptors, serializer: serializer, deserializer: deserializer, @@ -89,6 +91,7 @@ enum ClientRPCExecutor { return try await hedging.execute( request: request, method: method, + options: options, responseHandler: handler ) } diff --git a/Sources/GRPCCore/Configuration/MethodConfiguration.swift b/Sources/GRPCCore/Configuration/MethodConfiguration.swift index f34211abc..af37253ca 100644 --- a/Sources/GRPCCore/Configuration/MethodConfiguration.swift +++ b/Sources/GRPCCore/Configuration/MethodConfiguration.swift @@ -89,6 +89,8 @@ public struct MethodConfiguration: Hashable, Sendable { /// If a client attempts to send an object larger than this value, it will not be sent and the /// client will see an error. Note that 0 is a valid value, meaning that the request message /// must be empty. + /// + /// Note that if compression is used the uncompressed message size is validated. public var maxRequestMessageBytes: Int? /// The maximum allowed payload size in bytes for an individual response message. @@ -96,6 +98,8 @@ public struct MethodConfiguration: Hashable, Sendable { /// If a server attempts to send an object larger than this value, it will not /// be sent, and an error will be sent to the client instead. Note that 0 is a valid value, /// meaning that the response message must be empty. + /// + /// Note that if compression is used the uncompressed message size is validated. public var maxResponseMessageBytes: Int? /// The policy determining how many times, and when, the RPC is executed. @@ -112,7 +116,7 @@ public struct MethodConfiguration: Hashable, Sendable { /// The hedging policy allows an RPC to be executed multiple times concurrently. Typically /// each execution will be staggered by some delay. The first successful response will be /// reported to the client. Hedging is only suitable for idempotent RPCs. - public var executionPolicy: ExecutionPolicy? + public var executionPolicy: RPCExecutionPolicy? /// Create an execution configuration. /// @@ -129,7 +133,7 @@ public struct MethodConfiguration: Hashable, Sendable { timeout: Duration? = nil, maxRequestMessageBytes: Int? = nil, maxResponseMessageBytes: Int? = nil, - executionPolicy: ExecutionPolicy? = nil + executionPolicy: RPCExecutionPolicy? = nil ) { self.names = names self.waitForReady = waitForReady @@ -141,9 +145,9 @@ public struct MethodConfiguration: Hashable, Sendable { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension MethodConfiguration { - /// The execution policy for an RPC. - public enum ExecutionPolicy: Hashable, Sendable { +public struct RPCExecutionPolicy: Hashable, Sendable { + @usableFromInline + enum Wrapped: Hashable, Sendable { /// Policy for retrying an RPC. /// /// See ``RetryPolicy`` for more details. @@ -154,6 +158,43 @@ extension MethodConfiguration { /// See ``HedgingPolicy`` for more details. case hedge(HedgingPolicy) } + + @usableFromInline + let wrapped: Wrapped + + private init(_ wrapped: Wrapped) { + self.wrapped = wrapped + } + + /// Returns the retry policy, if it was set. + public var retry: RetryPolicy? { + switch self.wrapped { + case .retry(let policy): + return policy + case .hedge: + return nil + } + } + + /// Returns the hedging policy, if it was set. + public var hedge: HedgingPolicy? { + switch self.wrapped { + case .hedge(let policy): + return policy + case .retry: + return nil + } + } + + /// Create a new retry policy.`` + public static func retry(_ policy: RetryPolicy) -> Self { + Self(.retry(policy)) + } + + /// Create a new hedging policy.`` + public static func hedge(_ policy: HedgingPolicy) -> Self { + Self(.hedge(policy)) + } } /// Policy for retrying an RPC. @@ -419,7 +460,7 @@ extension MethodConfiguration: Codable { try container.encodeIfPresent(self.maxRequestMessageBytes, forKey: .maxRequestMessageBytes) try container.encodeIfPresent(self.maxResponseMessageBytes, forKey: .maxResponseMessageBytes) - switch self.executionPolicy { + switch self.executionPolicy?.wrapped { case .retry(let policy): try container.encode(policy, forKey: .retryPolicy) case .hedge(let policy): diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 490fa4ea1..b267ffb35 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -122,14 +122,6 @@ public struct GRPCClient: Sendable { /// the appropriate handler. private let interceptors: [any ClientInterceptor] - /// The configuration used by the client. - public let configuration: Configuration - - /// A cache of method config overrides. - private let methodConfigurationOverrides: _MethodConfigurations - /// A cache of method config defaults. - private let methodConfigurationDefaults: _MethodConfigurations - /// The current state of the client. private let state: ManagedAtomic @@ -156,23 +148,12 @@ public struct GRPCClient: Sendable { /// are called. The first interceptor added will be the first interceptor to intercept each /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. - /// - configuration: Configuration for the client. public init( transport: some ClientTransport, - interceptors: [any ClientInterceptor] = [], - configuration: Configuration = Configuration() + interceptors: [any ClientInterceptor] = [] ) { self.transport = transport self.interceptors = interceptors - self.configuration = configuration - - self.methodConfigurationOverrides = _MethodConfigurations( - serviceConfiguration: self.configuration.service.overrides - ) - self.methodConfigurationDefaults = _MethodConfigurations( - serviceConfiguration: self.configuration.service.defaults - ) - self.state = ManagedAtomic(.notStarted) } @@ -284,6 +265,7 @@ public struct GRPCClient: Sendable { /// - descriptor: The method descriptor for which to execute this request. /// - serializer: A request serializer. /// - deserializer: A response deserializer. + /// - options: Call specific options. /// - handler: A unary response handler. /// /// - Returns: The return value from the `handler`. @@ -292,13 +274,15 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( request: ClientRequest.Stream(single: request), descriptor: descriptor, serializer: serializer, - deserializer: deserializer + deserializer: deserializer, + options: options ) { stream in let singleResponse = await ClientResponse.Single(stream: stream) return try await handler(singleResponse) @@ -312,6 +296,7 @@ public struct GRPCClient: Sendable { /// - descriptor: The method descriptor for which to execute this request. /// - serializer: A request serializer. /// - deserializer: A response deserializer. + /// - options: Call specific options. /// - handler: A unary response handler. /// /// - Returns: The return value from the `handler`. @@ -320,13 +305,15 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( request: request, descriptor: descriptor, serializer: serializer, - deserializer: deserializer + deserializer: deserializer, + options: options ) { stream in let singleResponse = await ClientResponse.Single(stream: stream) return try await handler(singleResponse) @@ -340,6 +327,7 @@ public struct GRPCClient: Sendable { /// - descriptor: The method descriptor for which to execute this request. /// - serializer: A request serializer. /// - deserializer: A response deserializer. + /// - options: Call specific options. /// - handler: A response stream handler. /// /// - Returns: The return value from the `handler`. @@ -348,6 +336,7 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( @@ -355,6 +344,7 @@ public struct GRPCClient: Sendable { descriptor: descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: handler ) } @@ -369,6 +359,7 @@ public struct GRPCClient: Sendable { /// - descriptor: The method descriptor for which to execute this request. /// - serializer: A request serializer. /// - deserializer: A response deserializer. + /// - options: Call specific options. /// - handler: A response stream handler. /// /// - Returns: The return value from the `handler`. @@ -377,6 +368,7 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { switch self.state.load(ordering: .sequentiallyConsistent) { @@ -391,10 +383,14 @@ public struct GRPCClient: Sendable { ) } + let methodConfig = self.transport.configuration(forMethod: descriptor) + var options = options + options.formUnion(with: methodConfig) + return try await ClientRPCExecutor.execute( request: request, method: descriptor, - configuration: self.resolveMethodConfiguration(for: descriptor), + options: options, serializer: serializer, deserializer: deserializer, transport: self.transport, @@ -402,66 +398,4 @@ public struct GRPCClient: Sendable { handler: handler ) } - - private func resolveMethodConfiguration(for descriptor: MethodDescriptor) -> MethodConfiguration { - if let configuration = self.methodConfigurationOverrides[descriptor] { - return configuration - } - - if let configuration = self.transport.configuration(forMethod: descriptor) { - return configuration - } - - if let configuration = self.methodConfigurationDefaults[descriptor] { - return configuration - } - - // No configuration found, return the "vanilla" configuration. - return MethodConfiguration(names: [], timeout: nil, executionPolicy: nil) - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCClient { - public struct Configuration: Sendable { - /// Configuration for how the client interacts with services. - /// - /// The configuration includes information about how the client should load balance requests, - /// how retries should be throttled and how methods should be executed. Some services and - /// transports provide this information to the client when the server name is resolved. However, - /// you can override this configuration and set default values should no override be set and the - /// transport doesn't provide a value. - /// - /// See also ``ServiceConfiguration``. - public var service: Service - - /// Creates a new default configuration. - public init() { - self.service = Service() - } - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCClient.Configuration { - /// Configuration for how the client interacts with services. - /// - /// In most cases the client should defer to the configuration provided by the transport as this - /// can be provided to the transport as part of name resolution when establishing a connection. - /// - /// The client first checks ``overrides`` for a configuration, followed by the transport, followed - /// by ``defaults``. - public struct Service: Sendable, Hashable { - /// Configuration to use in precedence to that provided by the transport. - public var overrides: ServiceConfiguration - - /// Configuration to use only if there are no overrides and the transport doesn't specify - /// any configuration. - public var defaults: ServiceConfiguration - - public init() { - self.overrides = ServiceConfiguration() - self.defaults = ServiceConfiguration() - } - } } diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 819cc606a..89604117e 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -64,10 +64,12 @@ public protocol ClientTransport: Sendable { /// /// - Parameters: /// - descriptor: A description of the method to open a stream for. + /// - options: Options specific to the stream. /// - closure: A closure that takes the opened stream as parameter. /// - Returns: Whatever value was returned from `closure`. func withStream( descriptor: MethodDescriptor, + options: CallOptions, _ closure: (_ stream: RPCStream) async throws -> T ) async throws -> T diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index cbb1d9c81..afeaf4fdb 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -225,10 +225,12 @@ public struct InProcessClientTransport: ClientTransport { /// /// - Parameters: /// - descriptor: A description of the method to open a stream for. + /// - options: Options specific to the stream. /// - closure: A closure that takes the opened stream as parameter. /// - Returns: Whatever value was returned from `closure`. public func withStream( descriptor: MethodDescriptor, + options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { let request = RPCAsyncSequence._makeBackpressuredStream(watermarks: (16, 32)) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index fc882b7ba..5df62c007 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -68,53 +68,46 @@ struct ClientRPCExecutorTestHarness { func unary( request: ClientRequest.Single<[UInt8]>, - configuration: MethodConfiguration? = nil, + options: CallOptions = .defaults, handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void ) async throws { - try await self.bidirectional( - request: ClientRequest.Stream(single: request), - configuration: configuration - ) { response in + try await self.bidirectional(request: ClientRequest.Stream(single: request), options: options) { + response in try await handler(ClientResponse.Single(stream: response)) } } func clientStreaming( request: ClientRequest.Stream<[UInt8]>, - configuration: MethodConfiguration? = nil, + options: CallOptions = .defaults, handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void ) async throws { - try await self.bidirectional( - request: request, - configuration: configuration - ) { response in + try await self.bidirectional(request: request, options: options) { response in try await handler(ClientResponse.Single(stream: response)) } } func serverStreaming( request: ClientRequest.Single<[UInt8]>, - configuration: MethodConfiguration? = nil, + options: CallOptions = .defaults, handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void ) async throws { - try await self.bidirectional( - request: ClientRequest.Stream(single: request), - configuration: configuration - ) { response in + try await self.bidirectional(request: ClientRequest.Stream(single: request), options: options) { + response in try await handler(response) } } func bidirectional( request: ClientRequest.Stream<[UInt8]>, - configuration: MethodConfiguration? = nil, + options: CallOptions = .defaults, handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void ) async throws { try await self.execute( request: request, serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), - configuration: configuration, + options: options, handler: handler ) } @@ -123,7 +116,7 @@ struct ClientRPCExecutorTestHarness { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - configuration: MethodConfiguration?, + options: CallOptions, handler: @escaping @Sendable (ClientResponse.Stream) async throws -> Void ) async throws { try await withThrowingTaskGroup(of: Void.self) { group in @@ -142,18 +135,11 @@ struct ClientRPCExecutorTestHarness { try await self.clientTransport.connect(lazily: false) } - let executionConfiguration: MethodConfiguration - if let configuration = configuration { - executionConfiguration = configuration - } else { - executionConfiguration = MethodConfiguration(names: []) - } - // Execute the request. try await ClientRPCExecutor.execute( request: request, method: MethodDescriptor(service: "foo", method: "bar"), - configuration: executionConfiguration, + options: options, serializer: serializer, deserializer: deserializer, transport: self.clientTransport, diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index ef780fef4..b9126775e 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -29,7 +29,7 @@ extension ClientRPCExecutorTests { try await $0.write([1]) try await $0.write([2]) }, - configuration: .hedge(nonFatalCodes: [.unavailable]) + options: .hedge(nonFatalCodes: [.unavailable]) ) { response in XCTAssertRejected(response) { error in XCTAssertEqual(error.code, .unavailable) @@ -55,7 +55,7 @@ extension ClientRPCExecutorTests { }, // Set a long delay to reduce the risk of racing the second attempt and checking the number // of streams being opened. - configuration: .hedge(delay: .seconds(5), nonFatalCodes: []) + options: .hedge(delay: .seconds(5), nonFatalCodes: []) ) { response in XCTAssertRejected(response) { error in XCTAssertEqual(error.code, .aborted) @@ -89,7 +89,7 @@ extension ClientRPCExecutorTests { try await $0.write([1]) try await $0.write([2]) }, - configuration: .hedge( + options: .hedge( maximumAttempts: 5, delay: .milliseconds(10), nonFatalCodes: [.unavailable] @@ -133,7 +133,7 @@ extension ClientRPCExecutorTests { try await $0.write([1]) try await $0.write([2]) }, - configuration: .hedge( + options: .hedge( maximumAttempts: 5, delay: .seconds(60), // High delay, server pushback will override this. nonFatalCodes: [.unavailable] @@ -172,7 +172,7 @@ extension ClientRPCExecutorTests { request: ClientRequest.Stream { try await $0.write([0]) }, - configuration: .hedge(delay: .seconds(60), nonFatalCodes: [.unavailable]) + options: .hedge(delay: .seconds(60), nonFatalCodes: [.unavailable]) ) { response in XCTAssertRejected(response) { error in XCTAssertEqual(error.code, .unavailable) @@ -187,7 +187,7 @@ extension ClientRPCExecutorTests { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension MethodConfiguration { +extension CallOptions { fileprivate static func hedge( maximumAttempts: Int = 5, delay: Duration = .milliseconds(25), @@ -200,6 +200,8 @@ extension MethodConfiguration { nonFatalStatusCodes: nonFatalCodes ) - return Self(names: [], timeout: timeout, executionPolicy: .hedge(policy)) + var options = CallOptions.defaults + options.executionPolicy = .hedge(policy) + return options } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index e2a529228..91cc355d9 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -49,7 +49,7 @@ extension ClientRPCExecutorTests { try await $0.write([1]) try await $0.write([2]) }, - configuration: .retry(codes: [.unavailable]) + options: .retry(codes: [.unavailable]) ) { response in XCTAssertEqual( response.metadata, @@ -73,7 +73,7 @@ extension ClientRPCExecutorTests { request: ClientRequest.Stream(metadata: ["foo": "bar"]) { try await $0.write([0, 1, 2]) }, - configuration: .retry(codes: [.aborted]) + options: .retry(codes: [.aborted]) ) { response in switch response.accepted { case .success: @@ -94,7 +94,7 @@ extension ClientRPCExecutorTests { request: ClientRequest.Stream(metadata: ["foo": "bar"]) { try await $0.write([0, 1, 2]) }, - configuration: .retry(maximumAttempts: 2, codes: [.unavailable]) + options: .retry(maximumAttempts: 2, codes: [.unavailable]) ) { response in switch response.accepted { case .success: @@ -123,7 +123,7 @@ extension ClientRPCExecutorTests { try await $0.write([]) } }, - configuration: .retry(codes: [.unavailable]) + options: .retry(codes: [.unavailable]) ) { response in switch response.accepted { case .success: @@ -153,7 +153,7 @@ extension ClientRPCExecutorTests { try await $0.write([1]) try await $0.write([2]) }, - configuration: .retry(codes: [.unavailable], timeout: .zero) + options: .retry(codes: [.unavailable], timeout: .zero) ) { response in XCTFail("Response not expected to be handled") } @@ -174,7 +174,7 @@ extension ClientRPCExecutorTests { try await $0.write([1]) try await $0.write([2]) }, - configuration: .retry(codes: [.unavailable], timeout: .milliseconds(50)) + options: .retry(codes: [.unavailable], timeout: .milliseconds(50)) ) { response in XCTFail("Response not expected to be handled") } @@ -198,7 +198,7 @@ extension ClientRPCExecutorTests { try await $0.write([1]) try await $0.write([2]) }, - configuration: .retry(codes: [.unavailable], timeout: .milliseconds(150)) + options: .retry(codes: [.unavailable], timeout: .milliseconds(150)) ) { response in XCTFail("Response not expected to be handled") } @@ -236,7 +236,7 @@ extension ClientRPCExecutorTests { request: ClientRequest.Stream { try await $0.write([0]) }, - configuration: .init(names: [], executionPolicy: .retry(retryPolicy)) + options: .retry(retryPolicy) ) { response in let end = ContinuousClock.now let duration = end - start @@ -272,7 +272,7 @@ extension ClientRPCExecutorTests { request: ClientRequest.Stream { try await $0.write([0]) }, - configuration: .init(names: [], executionPolicy: .retry(retryPolicy)) + options: .retry(retryPolicy) ) { response in switch response.accepted { case .success: @@ -290,7 +290,15 @@ extension ClientRPCExecutorTests { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension MethodConfiguration { +extension CallOptions { + fileprivate static func retry( + _ policy: RetryPolicy + ) -> Self { + var options: CallOptions = .defaults + options.executionPolicy = .retry(policy) + return options + } + fileprivate static func retry( maximumAttempts: Int = 5, codes: Set, @@ -304,6 +312,9 @@ extension MethodConfiguration { retryableStatusCodes: codes ) - return Self(names: [], timeout: timeout, executionPolicy: .retry(policy)) + var options: CallOptions = .defaults + options.executionPolicy = .retry(policy) + options.timeout = timeout + return options } } diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift index 6fb7f17ea..e4b6fbe62 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift @@ -388,7 +388,7 @@ internal final class MethodConfigurationCodingTests: XCTestCase { let jsonConfig = try config.jsonUTF8Data() let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) - switch decoded.executionPolicy { + switch decoded.executionPolicy?.wrapped { case let .some(.hedge(policy)): XCTAssertEqual(policy.maximumAttempts, 3) XCTAssertEqual(policy.hedgingDelay, .seconds(42)) @@ -415,7 +415,7 @@ internal final class MethodConfigurationCodingTests: XCTestCase { let jsonConfig = try config.jsonUTF8Data() let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) - switch decoded.executionPolicy { + switch decoded.executionPolicy?.wrapped { case let .some(.retry(policy)): XCTAssertEqual(policy.maximumAttempts, 3) XCTAssertEqual(policy.initialBackoff, .seconds(1)) diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 96824eff7..9acfc4f21 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -49,7 +49,10 @@ final class GRPCServerTests: XCTestCase { func testServerHandlesUnary() async throws { try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in - try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3, 1, 4, 1, 5])) stream.outbound.finish() @@ -73,7 +76,10 @@ final class GRPCServerTests: XCTestCase { func testServerHandlesClientStreaming() async throws { try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in - try await client.withStream(descriptor: BinaryEcho.Methods.collect) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.collect, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3])) try await stream.outbound.write(.message([1])) @@ -101,7 +107,10 @@ final class GRPCServerTests: XCTestCase { func testServerHandlesServerStreaming() async throws { try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in - try await client.withStream(descriptor: BinaryEcho.Methods.expand) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.expand, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3, 1, 4, 1, 5])) stream.outbound.finish() @@ -127,7 +136,10 @@ final class GRPCServerTests: XCTestCase { func testServerHandlesBidirectionalStreaming() async throws { try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in - try await client.withStream(descriptor: BinaryEcho.Methods.update) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.update, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) for byte in [3, 1, 4, 1, 5] as [UInt8] { try await stream.outbound.write(.message([byte])) @@ -156,7 +168,8 @@ final class GRPCServerTests: XCTestCase { func testUnimplementedMethod() async throws { try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in try await client.withStream( - descriptor: MethodDescriptor(service: "not", method: "implemented") + descriptor: MethodDescriptor(service: "not", method: "implemented"), + options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) stream.outbound.finish() @@ -175,7 +188,10 @@ final class GRPCServerTests: XCTestCase { await withThrowingTaskGroup(of: Void.self) { group in for i in UInt8.min ..< UInt8.max { group.addTask { - try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([i])) stream.outbound.finish() @@ -210,7 +226,10 @@ final class GRPCServerTests: XCTestCase { .requestCounter(counter2), ] ) { client, _ in - try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) stream.outbound.finish() @@ -233,7 +252,8 @@ final class GRPCServerTests: XCTestCase { interceptors: [.requestCounter(counter)] ) { client, _ in try await client.withStream( - descriptor: MethodDescriptor(service: "not", method: "implemented") + descriptor: MethodDescriptor(service: "not", method: "implemented"), + options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) stream.outbound.finish() @@ -258,7 +278,10 @@ final class GRPCServerTests: XCTestCase { // RPC should fail now. await XCTAssertThrowsRPCErrorAsync { - try await client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in XCTFail("Stream shouldn't be opened") } } errorHandler: { error in @@ -269,7 +292,10 @@ final class GRPCServerTests: XCTestCase { func testInFlightRPCsCanContinueAfterServerStopListening() async throws { try await withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, server in - try await client.withStream(descriptor: BinaryEcho.Methods.update) { stream in + try await client.withStream( + descriptor: BinaryEcho.Methods.update, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) var iterator = stream.inbound.makeAsyncIterator() // Don't need to validate the response, just that the server is running. @@ -366,7 +392,10 @@ final class GRPCServerTests: XCTestCase { } group.addTask { - try await inProcess.client.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await inProcess.client.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in // The stream is open to the in-process transport. Let the other transport start. signal.continuation.finish() try await stream.outbound.write(.metadata([:])) @@ -390,7 +419,10 @@ final class GRPCServerTests: XCTestCase { } private func doEchoGet(using transport: some ClientTransport) async throws { - try await transport.withStream(descriptor: BinaryEcho.Methods.get) { stream in + try await transport.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([0])) stream.outbound.finish() diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index 3be95f564..baf9e873a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -24,6 +24,7 @@ struct AnyClientTransport: ClientTransport, Sendable { private let _withStream: @Sendable ( _ method: MethodDescriptor, + _ options: CallOptions, _ body: (RPCStream) async throws -> Any ) async throws -> Any private let _connect: @Sendable (Bool) async throws -> Void @@ -33,8 +34,8 @@ struct AnyClientTransport: ClientTransport, Sendable { init(wrapping transport: Transport) where Transport.Inbound == Inbound, Transport.Outbound == Outbound { self._retryThrottle = { transport.retryThrottle } - self._withStream = { descriptor, closure in - try await transport.withStream(descriptor: descriptor) { stream in + self._withStream = { descriptor, options, closure in + try await transport.withStream(descriptor: descriptor, options: options) { stream in try await closure(stream) as Any } } @@ -66,9 +67,10 @@ struct AnyClientTransport: ClientTransport, Sendable { func withStream( descriptor: MethodDescriptor, + options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { - let result = try await self._withStream(descriptor, closure) + let result = try await self._withStream(descriptor, options, closure) return result as! T } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 6fa024a5e..3fb4f125f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -53,10 +53,14 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { func withStream( descriptor: MethodDescriptor, + options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { do { - return try await self.transport.withStream(descriptor: descriptor) { stream in + return try await self.transport.withStream( + descriptor: descriptor, + options: options + ) { stream in self._streamsOpened.wrappingIncrement(ordering: .sequentiallyConsistent) return try await closure(stream) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 94e14c069..3a3e5828a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -44,6 +44,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { func withStream( descriptor: MethodDescriptor, + options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { throw RPCError(code: self.code, message: "") diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 60c55f941..a59e096c7 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -111,7 +111,10 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.withStream(descriptor: .init(service: "test", method: "test")) { _ in + try await client.withStream( + descriptor: .init(service: "test", method: "test"), + options: .defaults + ) { _ in // Once the pending stream is opened, close the client to new connections, // so that, once this closure is executed and this stream is closed, // the client will return from `connect(lazily:)`. @@ -136,7 +139,10 @@ final class InProcessClientTransportTests: XCTestCase { client.close() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await client.withStream(descriptor: .init(service: "test", method: "test")) { _ in } + try await client.withStream( + descriptor: .init(service: "test", method: "test"), + options: .defaults + ) { _ in } } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) } @@ -152,7 +158,10 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream(descriptor: .init(service: "test", method: "test")) { stream in + try await client.withStream( + descriptor: .init(service: "test", method: "test"), + options: .defaults + ) { stream in try await stream.outbound.write(.message([1])) stream.outbound.finish() let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } @@ -249,13 +258,19 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream(descriptor: .init(service: "test", method: "test")) { stream in + try await client.withStream( + descriptor: .init(service: "test", method: "test"), + options: .defaults + ) { stream in try await Task.sleep(for: .milliseconds(100)) } } group.addTask { - try await client.withStream(descriptor: .init(service: "test", method: "test")) { stream in + try await client.withStream( + descriptor: .init(service: "test", method: "test"), + options: .defaults + ) { stream in try await Task.sleep(for: .milliseconds(100)) } } From bcd5b92bcafb4db4a8cccb72426912605bbf1b7e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 11 Apr 2024 17:39:34 +0100 Subject: [PATCH 290/580] Rename a few things (#1856) Motivation: "Service config" is the name of a gRPC feature, yet we've named the corresponding type "ServiceConfiguration". We should follow the gRPC naming here. In a similar vein, "keepAlive" should be "keepalive". Modifications: - Rename "ServiceConfiguration" to "ServiceConfig" - Rename "MethodConfiguration" to "MethodConfig" - Rename various "keepAlive"s to "keepalive" Result: Naming is more consistent with gRPC --- .../GRPCCore/Call/Client/CallOptions.swift | 2 +- ...Configuration.swift => MethodConfig.swift} | 6 +- ...onfiguration.swift => ServiceConfig.swift} | 30 +++--- Sources/GRPCCore/GRPCClient.swift | 18 ++-- ...nfigurations.swift => MethodConfigs.swift} | 34 +++---- .../GRPCCore/Transport/ClientTransport.swift | 2 +- .../GRPCCore/Transport/RetryThrottle.swift | 2 +- .../Connection/ClientConnectionHandler.swift | 98 +++++++++---------- .../Client/Resolver/NameResolver+IPv4.swift | 2 +- .../Client/Resolver/NameResolver+IPv6.swift | 2 +- .../Client/Resolver/NameResolver+UDS.swift | 2 +- .../Client/Resolver/NameResolver+VSOCK.swift | 2 +- .../Client/Resolver/NameResolver.swift | 6 +- ...ectionManagementHandler+StateMachine.swift | 32 +++--- .../ServerConnectionManagementHandler.swift | 56 +++++------ .../InProcessClientTransport.swift | 14 +-- .../InProcessTransport.swift | 6 +- ...ts.swift => MethodConfigCodingTests.swift} | 24 ++--- ...ionTests.swift => MethodConfigTests.swift} | 2 +- ...s.swift => ServiceConfigCodingTests.swift} | 62 ++++++------ ...nsTests.swift => MethodConfigsTests.swift} | 20 ++-- .../Transport/AnyTransport.swift | 4 +- .../Transport/StreamCountingTransport.swift | 2 +- .../Transport/ThrowingTransport.swift | 2 +- ...ntConnectionHandlerStateMachineTests.swift | 42 ++++---- .../ClientConnectionHandlerTests.swift | 26 ++--- .../Resolver/NameResolverRegistryTests.swift | 12 +-- ...nManagementHandler+StateMachineTests.swift | 16 +-- ...rverConnectionManagementHandlerTests.swift | 48 ++++----- .../InProcessClientTransportTests.swift | 34 +++---- 30 files changed, 304 insertions(+), 304 deletions(-) rename Sources/GRPCCore/Configuration/{MethodConfiguration.swift => MethodConfig.swift} (99%) rename Sources/GRPCCore/Configuration/{ServiceConfiguration.swift => ServiceConfig.swift} (92%) rename Sources/GRPCCore/Internal/{_MethodConfigurations.swift => MethodConfigs.swift} (75%) rename Tests/GRPCCoreTests/Configuration/{MethodConfigurationCodingTests.swift => MethodConfigCodingTests.swift} (92%) rename Tests/GRPCCoreTests/Configuration/{MethodConfigurationTests.swift => MethodConfigTests.swift} (96%) rename Tests/GRPCCoreTests/Configuration/{ServiceConfigurationCodingTests.swift => ServiceConfigCodingTests.swift} (75%) rename Tests/GRPCCoreTests/Internal/{MethodConfigurationsTests.swift => MethodConfigsTests.swift} (78%) diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index 660c55cd5..c73b6af73 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -148,7 +148,7 @@ extension CallOptions { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { - mutating func formUnion(with methodConfig: MethodConfiguration?) { + mutating func formUnion(with methodConfig: MethodConfig?) { guard let methodConfig = methodConfig else { return } self.timeout.setIfNone(to: methodConfig.timeout) diff --git a/Sources/GRPCCore/Configuration/MethodConfiguration.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift similarity index 99% rename from Sources/GRPCCore/Configuration/MethodConfiguration.swift rename to Sources/GRPCCore/Configuration/MethodConfig.swift index af37253ca..f8d8e7eb6 100644 --- a/Sources/GRPCCore/Configuration/MethodConfiguration.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -18,7 +18,7 @@ /// /// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct MethodConfiguration: Hashable, Sendable { +public struct MethodConfig: Hashable, Sendable { public struct Name: Sendable, Hashable { /// The name of the service, including the namespace. /// @@ -414,7 +414,7 @@ extension Duration { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension MethodConfiguration: Codable { +extension MethodConfig: Codable { private enum CodingKeys: String, CodingKey { case name case waitForReady @@ -472,7 +472,7 @@ extension MethodConfiguration: Codable { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension MethodConfiguration.Name: Codable { +extension MethodConfig.Name: Codable { private enum CodingKeys: String, CodingKey { case service case method diff --git a/Sources/GRPCCore/Configuration/ServiceConfiguration.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift similarity index 92% rename from Sources/GRPCCore/Configuration/ServiceConfiguration.swift rename to Sources/GRPCCore/Configuration/ServiceConfig.swift index 54adeaf48..22ee5fcd3 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfiguration.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -18,9 +18,9 @@ /// /// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct ServiceConfiguration: Hashable, Sendable { +public struct ServiceConfig: Hashable, Sendable { /// Per-method configuration. - public var methodConfiguration: [MethodConfiguration] + public var methodConfig: [MethodConfig] /// Load balancing policies. /// @@ -44,26 +44,26 @@ public struct ServiceConfiguration: Hashable, Sendable { /// and hedged RPCs will not be sent. public var retryThrottlingPolicy: RetryThrottlingPolicy? - /// Creates a new ``ServiceConfiguration``. + /// Creates a new ``ServiceConfig``. /// /// - Parameters: - /// - methodConfiguration: Per-method configuration. + /// - methodConfig: Per-method configuration. /// - loadBalancingConfiguration: Load balancing policies. Clients use the the first supported /// policy when iterating the list in order. /// - retryThrottlingPolicy: Policy for throttling retries. public init( - methodConfiguration: [MethodConfiguration] = [], + methodConfig: [MethodConfig] = [], loadBalancingConfiguration: [LoadBalancingConfiguration] = [], retryThrottlingPolicy: RetryThrottlingPolicy? = nil ) { - self.methodConfiguration = methodConfiguration + self.methodConfig = methodConfig self.loadBalancingConfiguration = loadBalancingConfiguration self.retryThrottlingPolicy = retryThrottlingPolicy } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ServiceConfiguration: Codable { +extension ServiceConfig: Codable { private enum CodingKeys: String, CodingKey { case methodConfig case loadBalancingConfig @@ -73,11 +73,11 @@ extension ServiceConfiguration: Codable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let methodConfiguration = try container.decodeIfPresent( - [MethodConfiguration].self, + let methodConfig = try container.decodeIfPresent( + [MethodConfig].self, forKey: .methodConfig ) - self.methodConfiguration = methodConfiguration ?? [] + self.methodConfig = methodConfig ?? [] let loadBalancingConfiguration = try container.decodeIfPresent( [LoadBalancingConfiguration].self, @@ -93,14 +93,14 @@ extension ServiceConfiguration: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.methodConfiguration, forKey: .methodConfig) + try container.encode(self.methodConfig, forKey: .methodConfig) try container.encode(self.loadBalancingConfiguration, forKey: .loadBalancingConfig) try container.encodeIfPresent(self.retryThrottlingPolicy, forKey: .retryThrottling) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ServiceConfiguration { +extension ServiceConfig { /// Configuration used by clients for load-balancing. public struct LoadBalancingConfiguration: Hashable, Sendable { private enum Value: Hashable, Sendable { @@ -166,7 +166,7 @@ extension ServiceConfiguration { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ServiceConfiguration.LoadBalancingConfiguration { +extension ServiceConfig.LoadBalancingConfiguration { /// Configuration for the pick-first load balancing policy. public struct PickFirst: Hashable, Sendable, Codable { /// Whether the resolved addresses should be shuffled before attempting to connect to them. @@ -194,7 +194,7 @@ extension ServiceConfiguration.LoadBalancingConfiguration { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ServiceConfiguration.LoadBalancingConfiguration: Codable { +extension ServiceConfig.LoadBalancingConfiguration: Codable { private enum CodingKeys: String, CodingKey { case roundRobin = "round_robin" case pickFirst = "pick_first" @@ -225,7 +225,7 @@ extension ServiceConfiguration.LoadBalancingConfiguration: Codable { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ServiceConfiguration { +extension ServiceConfig { public struct RetryThrottlingPolicy: Hashable, Sendable, Codable { /// The initial, and maximum number of tokens. /// diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index b267ffb35..2aa701817 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -28,7 +28,7 @@ import Atomics /// /// However, in most cases you should prefer wrapping the ``GRPCClient`` with a generated stub. /// -/// You can set ``ServiceConfiguration``s on this client to override whatever configurations have been +/// You can set ``ServiceConfig``s on this client to override whatever configurations have been /// set on the given transport. You can also use ``ClientInterceptor``s to implement cross-cutting /// logic which apply to all RPCs. Example uses of interceptors include authentication and logging. /// @@ -40,11 +40,11 @@ import Atomics /// // Create a configuration object for the client and override the timeout for the 'Get' method on /// // the 'echo.Echo' service. This configuration takes precedence over any set by the transport. /// var configuration = GRPCClient.Configuration() -/// configuration.service.override = ServiceConfiguration( -/// methodConfiguration: [ -/// MethodConfiguration( +/// configuration.service.override = ServiceConfig( +/// methodConfig: [ +/// MethodConfig( /// names: [ -/// MethodConfiguration.Name(service: "echo.Echo", method: "Get") +/// MethodConfig.Name(service: "echo.Echo", method: "Get") /// ], /// timeout: .seconds(5) /// ) @@ -53,11 +53,11 @@ import Atomics /// /// // Configure a fallback timeout for all RPCs (indicated by an empty service and method name) if /// // no configuration is provided in the overrides or by the transport. -/// configuration.service.defaults = ServiceConfiguration( -/// methodConfiguration: [ -/// MethodConfiguration( +/// configuration.service.defaults = ServiceConfig( +/// methodConfig: [ +/// MethodConfig( /// names: [ -/// MethodConfiguration.Name(service: "", method: "") +/// MethodConfig.Name(service: "", method: "") /// ], /// timeout: .seconds(10) /// ) diff --git a/Sources/GRPCCore/Internal/_MethodConfigurations.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift similarity index 75% rename from Sources/GRPCCore/Internal/_MethodConfigurations.swift rename to Sources/GRPCCore/Internal/MethodConfigs.swift index ab719439a..7cc6ec53d 100644 --- a/Sources/GRPCCore/Internal/_MethodConfigurations.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -16,7 +16,7 @@ // TODO: when swift(>=5.9), use 'package' access level -/// A collection of ``MethodConfiguration``s, mapped to specific methods or services. +/// A collection of ``MethodConfig``s, mapped to specific methods or services. /// /// When creating a new instance, no overrides and no default will be set for using when getting /// a configuration for a method that has not been given a specific override. @@ -25,23 +25,23 @@ /// /// Use the subscript to get and set configurations for specific methods. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct _MethodConfigurations: Sendable, Hashable { - private var elements: [MethodConfiguration.Name: MethodConfiguration] +public struct _MethodConfigs: Sendable, Hashable { + private var elements: [MethodConfig.Name: MethodConfig] - /// Create a new ``_MethodConfigurations``. + /// Create a new ``_MethodConfigs``. /// - /// - Parameter serviceConfiguration: The configuration to read ``MethodConfiguration`` from. - public init(serviceConfiguration: ServiceConfiguration = ServiceConfiguration()) { + /// - Parameter serviceConfig: The configuration to read ``MethodConfig`` from. + public init(serviceConfig: ServiceConfig = ServiceConfig()) { self.elements = [:] - for configuration in serviceConfiguration.methodConfiguration { + for configuration in serviceConfig.methodConfig { for name in configuration.names { self.elements[name] = configuration } } } - /// Get or set the corresponding ``MethodConfiguration`` for the given ``MethodDescriptor``. + /// Get or set the corresponding ``MethodConfig`` for the given ``MethodDescriptor``. /// /// Configuration is hierarchical and can be set per-method, per-service /// (``setDefaultConfiguration(_:forService:)``) and globally (``setDefaultConfiguration(_:)``). @@ -51,10 +51,10 @@ public struct _MethodConfigurations: Sendable, Hashable { /// global configuration is returned, if present. /// /// - Parameters: - /// - descriptor: The ``MethodDescriptor`` for which to get or set a ``MethodConfiguration``. - public subscript(_ descriptor: MethodDescriptor) -> MethodConfiguration? { + /// - descriptor: The ``MethodDescriptor`` for which to get or set a ``MethodConfig``. + public subscript(_ descriptor: MethodDescriptor) -> MethodConfig? { get { - var name = MethodConfiguration.Name(service: descriptor.service, method: descriptor.method) + var name = MethodConfig.Name(service: descriptor.service, method: descriptor.method) if let configuration = self.elements[name] { return configuration @@ -73,7 +73,7 @@ public struct _MethodConfigurations: Sendable, Hashable { } set { - let name = MethodConfiguration.Name(service: descriptor.service, method: descriptor.method) + let name = MethodConfig.Name(service: descriptor.service, method: descriptor.method) self.elements[name] = newValue } } @@ -81,8 +81,8 @@ public struct _MethodConfigurations: Sendable, Hashable { /// Set a default configuration for all methods that have no overrides. /// /// - Parameter configuration: The default configuration. - public mutating func setDefaultConfiguration(_ configuration: MethodConfiguration?) { - let name = MethodConfiguration.Name(service: "", method: "") + public mutating func setDefaultConfiguration(_ configuration: MethodConfig?) { + let name = MethodConfig.Name(service: "", method: "") self.elements[name] = configuration } @@ -90,16 +90,16 @@ public struct _MethodConfigurations: Sendable, Hashable { /// /// If getting a configuration for a method that's part of a service, and the method itself doesn't have an /// override, then this configuration will be used instead of the default configuration passed when creating - /// this instance of ``MethodConfigurations``. + /// this instance of ``MethodConfigs``. /// /// - Parameters: /// - configuration: The default configuration for the service. /// - service: The name of the service for which this override applies. public mutating func setDefaultConfiguration( - _ configuration: MethodConfiguration?, + _ configuration: MethodConfig?, forService service: String ) { - let name = MethodConfiguration.Name(service: "", method: "") + let name = MethodConfig.Name(service: "", method: "") self.elements[name] = configuration } } diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 89604117e..b4ef40de7 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -77,5 +77,5 @@ public protocol ClientTransport: Sendable { /// /// - Parameter descriptor: The method to lookup configuration for. /// - Returns: Configuration for the method, if it exists. - func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfiguration? + func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? } diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index 8fccce9c1..15658a0ae 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -107,7 +107,7 @@ public struct RetryThrottle: Sendable { /// /// - Parameter policy: The policy to use to configure the throttle. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public init(policy: ServiceConfiguration.RetryThrottlingPolicy) { + public init(policy: ServiceConfig.RetryThrottlingPolicy) { self.init(maximumTokens: policy.maxTokens, tokenRatio: policy.tokenRatio) } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 8910e4598..adf30f554 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -23,7 +23,7 @@ enum ClientConnectionEvent: Sendable, Hashable { /// The server sent a GOAWAY frame to the client. case goAway(HTTP2ErrorCode, String) /// The keep alive timer fired and subsequently timed out. - case keepAliveExpired + case keepaliveExpired /// The connection became idle. case idle /// The local peer initiated the close. @@ -64,14 +64,14 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl private var maxIdleTimer: Timer? /// The amount of time to wait before sending a keep alive ping. - private var keepAliveTimer: Timer? + private var keepaliveTimer: Timer? /// The amount of time the client has to reply after sending a keep alive ping. Only used if - /// `keepAliveTimer` is set. - private var keepAliveTimeoutTimer: Timer + /// `keepaliveTimer` is set. + private var keepaliveTimeoutTimer: Timer /// Opaque data sent in keep alive pings. - private let keepAlivePingData: HTTP2PingData + private let keepalivePingData: HTTP2PingData /// The current state of the connection. private var state: StateMachine @@ -87,26 +87,26 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl /// - Parameters: /// - eventLoop: The `EventLoop` of the `Channel` this handler is placed in. /// - maxIdleTime: The maximum amount time a connection may be idle for before being closed. - /// - keepAliveTime: The amount of time to wait after reading data before sending a keep-alive + /// - keepaliveTime: The amount of time to wait after reading data before sending a keep-alive /// ping. - /// - keepAliveTimeout: The amount of time the client has to reply after the server sends a + /// - keepaliveTimeout: The amount of time the client has to reply after the server sends a /// keep-alive ping to keep the connection open. The connection is closed if no reply /// is received. - /// - keepAliveWithoutCalls: Whether the client sends keep-alive pings when there are no calls + /// - keepaliveWithoutCalls: Whether the client sends keep-alive pings when there are no calls /// in progress. init( eventLoop: EventLoop, maxIdleTime: TimeAmount?, - keepAliveTime: TimeAmount?, - keepAliveTimeout: TimeAmount?, - keepAliveWithoutCalls: Bool + keepaliveTime: TimeAmount?, + keepaliveTimeout: TimeAmount?, + keepaliveWithoutCalls: Bool ) { self.eventLoop = eventLoop self.maxIdleTimer = maxIdleTime.map { Timer(delay: $0) } - self.keepAliveTimer = keepAliveTime.map { Timer(delay: $0, repeat: true) } - self.keepAliveTimeoutTimer = Timer(delay: keepAliveTimeout ?? .seconds(20)) - self.keepAlivePingData = HTTP2PingData(withInteger: .random(in: .min ... .max)) - self.state = StateMachine(allowKeepAliveWithoutCalls: keepAliveWithoutCalls) + self.keepaliveTimer = keepaliveTime.map { Timer(delay: $0, repeat: true) } + self.keepaliveTimeoutTimer = Timer(delay: keepaliveTimeout ?? .seconds(20)) + self.keepalivePingData = HTTP2PingData(withInteger: .random(in: .min ... .max)) + self.state = StateMachine(allowKeepaliveWithoutCalls: keepaliveWithoutCalls) self.flushPending = false self.inReadLoop = false @@ -117,8 +117,8 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } func channelActive(context: ChannelHandlerContext) { - self.keepAliveTimer?.schedule(on: context.eventLoop) { - self.keepAliveTimerFired(context: context) + self.keepaliveTimer?.schedule(on: context.eventLoop) { + self.keepaliveTimerFired(context: context) } self.maxIdleTimer?.schedule(on: context.eventLoop) { @@ -133,8 +133,8 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl case .succeed(let promise): promise.succeed() } - self.keepAliveTimer?.cancel() - self.keepAliveTimeoutTimer.cancel() + self.keepaliveTimer?.cancel() + self.keepaliveTimeoutTimer.cancel() } func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { @@ -146,15 +146,15 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl case let event as StreamClosedEvent: switch self.state.streamClosed(event.streamID) { - case .startIdleTimer(let cancelKeepAlive): + case .startIdleTimer(let cancelKeepalive): // All streams are closed, restart the idle timer, and stop the keep-alive timer (it may // not stop if keep-alive is allowed when there are no active calls). self.maxIdleTimer?.schedule(on: context.eventLoop) { self.maxIdleTimerFired(context: context) } - if cancelKeepAlive { - self.keepAliveTimer?.cancel() + if cancelKeepalive { + self.keepaliveTimer?.cancel() } case .close: @@ -200,10 +200,10 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl case .ping(let data, let ack): // Pings are ack'd by the HTTP/2 handler so we only pay attention to acks here, and in // particular only those carrying the keep-alive data. - if ack, data == self.keepAlivePingData { - self.keepAliveTimeoutTimer.cancel() - self.keepAliveTimer?.schedule(on: context.eventLoop) { - self.keepAliveTimerFired(context: context) + if ack, data == self.keepalivePingData { + self.keepaliveTimeoutTimer.cancel() + self.keepaliveTimer?.schedule(on: context.eventLoop) { + self.keepaliveTimerFired(context: context) } } @@ -258,27 +258,27 @@ extension ClientConnectionHandler { } } - private func keepAliveTimerFired(context: ChannelHandlerContext) { - guard self.state.sendKeepAlivePing() else { return } + private func keepaliveTimerFired(context: ChannelHandlerContext) { + guard self.state.sendKeepalivePing() else { return } // Cancel the keep alive timer when the client sends a ping. The timer is resumed when the ping // is acknowledged. - self.keepAliveTimer?.cancel() + self.keepaliveTimer?.cancel() - let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepAlivePingData, ack: false)) + let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepalivePingData, ack: false)) context.write(self.wrapOutboundOut(ping), promise: nil) self.maybeFlush(context: context) // Schedule a timeout on waiting for the response. - self.keepAliveTimeoutTimer.schedule(on: context.eventLoop) { - self.keepAliveTimeoutExpired(context: context) + self.keepaliveTimeoutTimer.schedule(on: context.eventLoop) { + self.keepaliveTimeoutExpired(context: context) } } - private func keepAliveTimeoutExpired(context: ChannelHandlerContext) { + private func keepaliveTimeoutExpired(context: ChannelHandlerContext) { guard self.state.beginClosing() else { return } - context.fireChannelRead(self.wrapInboundOut(.closing(.keepAliveExpired))) + context.fireChannelRead(self.wrapInboundOut(.closing(.keepaliveExpired))) self.writeAndFlushGoAway(context: context, message: "keepalive_expired") context.close(promise: nil) } @@ -321,29 +321,29 @@ extension ClientConnectionHandler { struct Active { var openStreams: Set - var allowKeepAliveWithoutCalls: Bool + var allowKeepaliveWithoutCalls: Bool - init(allowKeepAliveWithoutCalls: Bool) { + init(allowKeepaliveWithoutCalls: Bool) { self.openStreams = [] - self.allowKeepAliveWithoutCalls = allowKeepAliveWithoutCalls + self.allowKeepaliveWithoutCalls = allowKeepaliveWithoutCalls } } struct Closing { - var allowKeepAliveWithoutCalls: Bool + var allowKeepaliveWithoutCalls: Bool var openStreams: Set var closePromise: Optional> init(from state: Active, closePromise: EventLoopPromise?) { self.openStreams = state.openStreams - self.allowKeepAliveWithoutCalls = state.allowKeepAliveWithoutCalls + self.allowKeepaliveWithoutCalls = state.allowKeepaliveWithoutCalls self.closePromise = closePromise } } } - init(allowKeepAliveWithoutCalls: Bool) { - self.state = .active(State.Active(allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls)) + init(allowKeepaliveWithoutCalls: Bool) { + self.state = .active(State.Active(allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls)) } /// Record that the stream with the given ID has been opened. @@ -366,7 +366,7 @@ extension ClientConnectionHandler { enum OnStreamClosed: Equatable { /// Start the idle timer, after which the connection should be closed gracefully. - case startIdleTimer(cancelKeepAlive: Bool) + case startIdleTimer(cancelKeepalive: Bool) /// Close the connection. case close /// Do nothing. @@ -382,7 +382,7 @@ extension ClientConnectionHandler { let removedID = state.openStreams.remove(id) assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") if state.openStreams.isEmpty { - onStreamClosed = .startIdleTimer(cancelKeepAlive: !state.allowKeepAliveWithoutCalls) + onStreamClosed = .startIdleTimer(cancelKeepalive: !state.allowKeepaliveWithoutCalls) } else { onStreamClosed = .none } @@ -402,21 +402,21 @@ extension ClientConnectionHandler { } /// Returns whether a keep alive ping should be sent to the server. - mutating func sendKeepAlivePing() -> Bool { - let sendKeepAlivePing: Bool + mutating func sendKeepalivePing() -> Bool { + let sendKeepalivePing: Bool // Only send a ping if there are open streams or there are no open streams and keep alive // is permitted when there are no active calls. switch self.state { case .active(let state): - sendKeepAlivePing = !state.openStreams.isEmpty || state.allowKeepAliveWithoutCalls + sendKeepalivePing = !state.openStreams.isEmpty || state.allowKeepaliveWithoutCalls case .closing(let state): - sendKeepAlivePing = !state.openStreams.isEmpty || state.allowKeepAliveWithoutCalls + sendKeepalivePing = !state.openStreams.isEmpty || state.allowKeepaliveWithoutCalls case .closed: - sendKeepAlivePing = false + sendKeepalivePing = false } - return sendKeepAlivePing + return sendKeepalivePing } enum OnGracefulShutDown: Equatable { diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift index e4dffcfaf..99ac87f00 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift @@ -68,7 +68,7 @@ extension NameResolvers { public func resolver(for target: Target) -> NameResolver { let endpoints = target.addresses.map { Endpoint(addresses: [.ipv4($0)]) } - let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfiguration: nil) + let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfig: nil) return NameResolver(names: .constant(resolutionResult), updateMode: .pull) } } diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift index 10348e44d..5b2276168 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift @@ -68,7 +68,7 @@ extension NameResolvers { public func resolver(for target: Target) -> NameResolver { let endpoints = target.addresses.map { Endpoint(addresses: [.ipv6($0)]) } - let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfiguration: nil) + let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfig: nil) return NameResolver(names: .constant(resolutionResult), updateMode: .pull) } } diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift index 403c6a0a3..c169dce38 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift @@ -53,7 +53,7 @@ extension NameResolvers { public func resolver(for target: Target) -> NameResolver { let endpoint = Endpoint(addresses: [.unixDomainSocket(target.address)]) - let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfiguration: nil) + let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfig: nil) return NameResolver(names: .constant(resolutionResult), updateMode: .pull) } } diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift index 5992031bc..78bf3eed0 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift @@ -57,7 +57,7 @@ extension NameResolvers { public func resolver(for target: Target) -> NameResolver { let endpoint = Endpoint(addresses: [.vsock(target.address)]) - let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfiguration: nil) + let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfig: nil) return NameResolver(names: .constant(resolutionResult), updateMode: .pull) } } diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift index 4887b20c4..dab910bd5 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift @@ -67,14 +67,14 @@ public struct NameResolutionResult: Hashable, Sendable { /// The service configuration reported by the resolver, or an error if it couldn't be parsed. /// This value may be `nil` if the resolver doesn't support fetching service configuration. - public var serviceConfiguration: Result? + public var serviceConfig: Result? public init( endpoints: [Endpoint], - serviceConfiguration: Result? + serviceConfig: Result? ) { self.endpoints = endpoints - self.serviceConfiguration = serviceConfiguration + self.serviceConfig = serviceConfig } } diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift index 156063adb..a9b123950 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift @@ -33,23 +33,23 @@ extension ServerConnectionManagementHandler { /// Create a new state machine. /// /// - Parameters: - /// - allowKeepAliveWithoutCalls: Whether the client is permitted to send keep alive pings + /// - allowKeepaliveWithoutCalls: Whether the client is permitted to send keep alive pings /// when there are no active calls. /// - minPingReceiveIntervalWithoutCalls: The minimum time interval required between keep /// alive pings when there are no active calls. /// - goAwayPingData: Opaque data sent to the client in a PING frame when the server /// initiates graceful shutdown. init( - allowKeepAliveWithoutCalls: Bool, + allowKeepaliveWithoutCalls: Bool, minPingReceiveIntervalWithoutCalls: TimeAmount, goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: .random(in: .min ... .max)) ) { - let keepAlive = KeepAlive( - allowWithoutCalls: allowKeepAliveWithoutCalls, + let keepalive = Keepalive( + allowWithoutCalls: allowKeepaliveWithoutCalls, minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls ) - self.state = .active(State.Active(keepAlive: keepAlive)) + self.state = .active(State.Active(keepalive: keepalive)) self.goAwayPingData = goAwayPingData } @@ -128,7 +128,7 @@ extension ServerConnectionManagementHandler { switch self.state { case .active(var state): - let tooManyPings = state.keepAlive.receivedPing( + let tooManyPings = state.keepalive.receivedPing( atTime: time, hasOpenStreams: !state.openStreams.isEmpty ) @@ -142,7 +142,7 @@ extension ServerConnectionManagementHandler { } case .closing(var state): - let tooManyPings = state.keepAlive.receivedPing( + let tooManyPings = state.keepalive.receivedPing( atTime: time, hasOpenStreams: !state.openStreams.isEmpty ) @@ -226,14 +226,14 @@ extension ServerConnectionManagementHandler { } /// Reset the state of keep-alive policing. - mutating func resetKeepAliveState() { + mutating func resetKeepaliveState() { switch self.state { case .active(var state): - state.keepAlive.reset() + state.keepalive.reset() self.state = .active(state) case .closing(var state): - state.keepAlive.reset() + state.keepalive.reset() self.state = .closing(state) case .closed: @@ -249,7 +249,7 @@ extension ServerConnectionManagementHandler { } extension ServerConnectionManagementHandler.StateMachine { - fileprivate struct KeepAlive { + fileprivate struct Keepalive { /// Allow the client to send keep alive pings when there are no active calls. private let allowWithoutCalls: Bool @@ -328,12 +328,12 @@ extension ServerConnectionManagementHandler.StateMachine { /// The ID of the most recently opened stream (zero indicates no streams have been opened yet). var lastStreamID: HTTP2StreamID /// The state of keep alive. - var keepAlive: KeepAlive + var keepalive: Keepalive - init(keepAlive: KeepAlive) { + init(keepalive: Keepalive) { self.openStreams = [] self.lastStreamID = .rootStream - self.keepAlive = keepAlive + self.keepalive = keepalive } } @@ -345,14 +345,14 @@ extension ServerConnectionManagementHandler.StateMachine { /// The ID of the most recently opened stream (zero indicates no streams have been opened yet). var lastStreamID: HTTP2StreamID /// The state of keep alive. - var keepAlive: KeepAlive + var keepalive: Keepalive /// Whether the second GOAWAY frame has been sent with a lower stream ID. var sentSecondGoAway: Bool init(from state: Active) { self.openStreams = state.openStreams self.lastStreamID = state.lastStreamID - self.keepAlive = state.keepAlive + self.keepalive = state.keepalive self.sentSecondGoAway = false } } diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index 459da88af..ab1bb4792 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -60,14 +60,14 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { private var maxGraceTimer: Timer? /// The amount of time to wait before sending a keep alive ping. - private var keepAliveTimer: Timer? + private var keepaliveTimer: Timer? /// The amount of time the client has to reply after sending a keep alive ping. Only used if - /// `keepAliveTimer` is set. - private var keepAliveTimeoutTimer: Timer + /// `keepaliveTimer` is set. + private var keepaliveTimeoutTimer: Timer /// Opaque data sent in keep alive pings. - private let keepAlivePingData: HTTP2PingData + private let keepalivePingData: HTTP2PingData /// Whether a flush is pending. private var flushPending: Bool @@ -162,7 +162,7 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.handler.eventLoop.assertInEventLoop() if self.handler.frameStats.didWriteHeadersOrData { self.handler.frameStats.reset() - self.handler.state.resetKeepAliveState() + self.handler.state.resetKeepaliveState() } } @@ -186,12 +186,12 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { /// - maxIdleTime: The maximum amount time a connection may be idle for before being closed. /// - maxAge: The maximum amount of time a connection may exist before being gracefully closed. /// - maxGraceTime: The maximum amount of time that the connection has to close gracefully. - /// - keepAliveTime: The amount of time to wait after reading data before sending a keep-alive + /// - keepaliveTime: The amount of time to wait after reading data before sending a keep-alive /// ping. - /// - keepAliveTimeout: The amount of time the client has to reply after the server sends a + /// - keepaliveTimeout: The amount of time the client has to reply after the server sends a /// keep-alive ping to keep the connection open. The connection is closed if no reply /// is received. - /// - allowKeepAliveWithoutCalls: Whether the server allows the client to send keep-alive pings + /// - allowKeepaliveWithoutCalls: Whether the server allows the client to send keep-alive pings /// when there are no calls in progress. /// - minPingIntervalWithoutCalls: The minimum allowed interval the client is allowed to send /// keep-alive pings. Pings more frequent than this interval count as 'strikes' and the @@ -202,9 +202,9 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { maxIdleTime: TimeAmount?, maxAge: TimeAmount?, maxGraceTime: TimeAmount?, - keepAliveTime: TimeAmount?, - keepAliveTimeout: TimeAmount?, - allowKeepAliveWithoutCalls: Bool, + keepaliveTime: TimeAmount?, + keepaliveTimeout: TimeAmount?, + allowKeepaliveWithoutCalls: Bool, minPingIntervalWithoutCalls: TimeAmount, clock: Clock = .nio ) { @@ -214,16 +214,16 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.maxAgeTimer = maxAge.map { Timer(delay: $0) } self.maxGraceTimer = maxGraceTime.map { Timer(delay: $0) } - self.keepAliveTimer = keepAliveTime.map { Timer(delay: $0) } + self.keepaliveTimer = keepaliveTime.map { Timer(delay: $0) } // Always create a keep alive timeout timer, it's only used if there is a keep alive timer. - self.keepAliveTimeoutTimer = Timer(delay: keepAliveTimeout ?? .seconds(20)) + self.keepaliveTimeoutTimer = Timer(delay: keepaliveTimeout ?? .seconds(20)) // Generate a random value to be used as keep alive ping data. let pingData = UInt64.random(in: .min ... .max) - self.keepAlivePingData = HTTP2PingData(withInteger: pingData) + self.keepalivePingData = HTTP2PingData(withInteger: pingData) self.state = StateMachine( - allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls, + allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls, minPingReceiveIntervalWithoutCalls: minPingIntervalWithoutCalls, goAwayPingData: HTTP2PingData(withInteger: ~pingData) ) @@ -247,8 +247,8 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.initiateGracefulShutdown(context: context) } - self.keepAliveTimer?.schedule(on: context.eventLoop) { - self.keepAliveTimerFired(context: context) + self.keepaliveTimer?.schedule(on: context.eventLoop) { + self.keepaliveTimerFired(context: context) } context.fireChannelActive() @@ -258,8 +258,8 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.maxIdleTimer?.cancel() self.maxAgeTimer?.cancel() self.maxGraceTimer?.cancel() - self.keepAliveTimer?.cancel() - self.keepAliveTimeoutTimer.cancel() + self.keepaliveTimer?.cancel() + self.keepaliveTimeoutTimer.cancel() context.fireChannelInactive() } @@ -295,8 +295,8 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.inReadLoop = true // Any read data indicates that the connection is alive so cancel the keep-alive timers. - self.keepAliveTimer?.cancel() - self.keepAliveTimeoutTimer.cancel() + self.keepaliveTimer?.cancel() + self.keepaliveTimeoutTimer.cancel() let frame = self.unwrapInboundIn(data) switch frame.payload { @@ -323,8 +323,8 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.inReadLoop = false // Done reading: schedule the keep-alive timer. - self.keepAliveTimer?.schedule(on: context.eventLoop) { - self.keepAliveTimerFired(context: context) + self.keepaliveTimer?.schedule(on: context.eventLoop) { + self.keepaliveTimerFired(context: context) } context.fireChannelReadComplete() @@ -350,8 +350,8 @@ extension ServerConnectionManagementHandler { // Cancel any timers if initiating shutdown. self.maxIdleTimer?.cancel() self.maxAgeTimer?.cancel() - self.keepAliveTimer?.cancel() - self.keepAliveTimeoutTimer.cancel() + self.keepaliveTimer?.cancel() + self.keepaliveTimeoutTimer.cancel() switch self.state.startGracefulShutdown() { case .sendGoAwayAndPing(let pingData): @@ -436,13 +436,13 @@ extension ServerConnectionManagementHandler { } } - private func keepAliveTimerFired(context: ChannelHandlerContext) { - let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepAlivePingData, ack: false)) + private func keepaliveTimerFired(context: ChannelHandlerContext) { + let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepalivePingData, ack: false)) context.write(self.wrapInboundOut(ping), promise: nil) self.maybeFlush(context: context) // Schedule a timeout on waiting for the response. - self.keepAliveTimeoutTimer.schedule(on: context.eventLoop) { + self.keepaliveTimeoutTimer.schedule(on: context.eventLoop) { self.initiateGracefulShutdown(context: context) } } diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index afeaf4fdb..af911c659 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -98,22 +98,22 @@ public struct InProcessClientTransport: ClientTransport { public let retryThrottle: RetryThrottle? - private let methodConfiguration: _MethodConfigurations + private let methodConfig: _MethodConfigs private let state: _LockedValueBox /// Creates a new in-process client transport. /// /// - Parameters: /// - server: The in-process server transport to connect to. - /// - serviceConfiguration: Service configuration. + /// - serviceConfig: Service configuration. public init( server: InProcessServerTransport, - serviceConfiguration: ServiceConfiguration = ServiceConfiguration() + serviceConfig: ServiceConfig = ServiceConfig() ) { - self.retryThrottle = serviceConfiguration.retryThrottlingPolicy.map { + self.retryThrottle = serviceConfig.retryThrottlingPolicy.map { RetryThrottle(policy: $0) } - self.methodConfiguration = _MethodConfigurations(serviceConfiguration: serviceConfiguration) + self.methodConfig = _MethodConfigs(serviceConfig: serviceConfig) self.state = _LockedValueBox(.unconnected(.init(serverTransport: server))) } @@ -340,7 +340,7 @@ public struct InProcessClientTransport: ClientTransport { /// - Returns: Execution configuration for the method, if it exists. public func configuration( forMethod descriptor: MethodDescriptor - ) -> MethodConfiguration? { - self.methodConfiguration[descriptor] + ) -> MethodConfig? { + self.methodConfig[descriptor] } } diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index 02592f747..53a8eb921 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -22,16 +22,16 @@ public enum InProcessTransport { /// and a client using that server transport. /// /// - Parameters: - /// - serviceConfiguration: Configuration describing how methods should be executed. + /// - serviceConfig: Configuration describing how methods should be executed. /// - Returns: A tuple containing the connected server and client in-process transports. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public static func makePair( - serviceConfiguration: ServiceConfiguration = ServiceConfiguration() + serviceConfig: ServiceConfig = ServiceConfig() ) -> (server: InProcessServerTransport, client: InProcessClientTransport) { let server = InProcessServerTransport() let client = InProcessClientTransport( server: server, - serviceConfiguration: serviceConfiguration + serviceConfig: serviceConfig ) return (server, client) } diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift similarity index 92% rename from Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift rename to Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift index e4b6fbe62..a0d309c25 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigurationCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift @@ -21,7 +21,7 @@ import XCTest @testable import GRPCCore @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -internal final class MethodConfigurationCodingTests: XCTestCase { +internal final class MethodConfigCodingTests: XCTestCase { private let encoder = JSONEncoder() private let decoder = JSONDecoder() @@ -35,30 +35,30 @@ internal final class MethodConfigurationCodingTests: XCTestCase { } func testDecodeMethodConfigName() throws { - let inputs: [(String, MethodConfiguration.Name)] = [ + let inputs: [(String, MethodConfig.Name)] = [ (#"{"service": "foo.bar", "method": "baz"}"#, .init(service: "foo.bar", method: "baz")), (#"{"service": "foo.bar"}"#, .init(service: "foo.bar", method: "")), (#"{}"#, .init(service: "", method: "")), ] for (json, expected) in inputs { - let decoded = try self.decoder.decode(MethodConfiguration.Name.self, from: Data(json.utf8)) + let decoded = try self.decoder.decode(MethodConfig.Name.self, from: Data(json.utf8)) XCTAssertEqual(decoded, expected) } } func testEncodeDecodeMethodConfigName() throws { - let inputs: [MethodConfiguration.Name] = [ - MethodConfiguration.Name(service: "foo.bar", method: "baz"), - MethodConfiguration.Name(service: "foo.bar", method: ""), - MethodConfiguration.Name(service: "", method: ""), + let inputs: [MethodConfig.Name] = [ + MethodConfig.Name(service: "foo.bar", method: "baz"), + MethodConfig.Name(service: "foo.bar", method: ""), + MethodConfig.Name(service: "", method: ""), ] // We can't do encode-only tests as the output is non-deterministic (the ordering of // service/method in the JSON object) for name in inputs { let encoded = try self.encoder.encode(name) - let decoded = try self.decoder.decode(MethodConfiguration.Name.self, from: encoded) + let decoded = try self.decoder.decode(MethodConfig.Name.self, from: encoded) XCTAssertEqual(decoded, name) } } @@ -364,8 +364,8 @@ internal final class MethodConfigurationCodingTests: XCTestCase { // Test the 'regular' config. do { let jsonConfig = try config.jsonUTF8Data() - let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) - XCTAssertEqual(decoded.names, [MethodConfiguration.Name(service: "echo.Echo", method: "Get")]) + let decoded = try self.decoder.decode(MethodConfig.self, from: jsonConfig) + XCTAssertEqual(decoded.names, [MethodConfig.Name(service: "echo.Echo", method: "Get")]) XCTAssertEqual(decoded.waitForReady, true) XCTAssertEqual(decoded.timeout, Duration(secondsComponent: 1, attosecondsComponent: 0)) XCTAssertEqual(decoded.maxRequestMessageBytes, 1024) @@ -386,7 +386,7 @@ internal final class MethodConfigurationCodingTests: XCTestCase { } let jsonConfig = try config.jsonUTF8Data() - let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) + let decoded = try self.decoder.decode(MethodConfig.self, from: jsonConfig) switch decoded.executionPolicy?.wrapped { case let .some(.hedge(policy)): @@ -413,7 +413,7 @@ internal final class MethodConfigurationCodingTests: XCTestCase { } let jsonConfig = try config.jsonUTF8Data() - let decoded = try self.decoder.decode(MethodConfiguration.self, from: jsonConfig) + let decoded = try self.decoder.decode(MethodConfig.self, from: jsonConfig) switch decoded.executionPolicy?.wrapped { case let .some(.retry(policy)): diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigurationTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift similarity index 96% rename from Tests/GRPCCoreTests/Configuration/MethodConfigurationTests.swift rename to Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift index 8104b9fd6..0b3406efb 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigurationTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift @@ -17,7 +17,7 @@ import GRPCCore import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class MethodConfigurationTests: XCTestCase { +final class MethodConfigTests: XCTestCase { func testRetryPolicyClampsMaxAttempts() { var policy = RetryPolicy( maximumAttempts: 10, diff --git a/Tests/GRPCCoreTests/Configuration/ServiceConfigurationCodingTests.swift b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift similarity index 75% rename from Tests/GRPCCoreTests/Configuration/ServiceConfigurationCodingTests.swift rename to Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift index 2dc8a943e..8cfcebf9e 100644 --- a/Tests/GRPCCoreTests/Configuration/ServiceConfigurationCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift @@ -19,7 +19,7 @@ import GRPCCore import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class ServiceConfigurationCodingTests: XCTestCase { +final class ServiceConfigCodingTests: XCTestCase { private let encoder = JSONEncoder() private let decoder = JSONDecoder() @@ -46,9 +46,9 @@ final class ServiceConfigurationCodingTests: XCTestCase { } """ - let expected = try ServiceConfiguration.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) + let expected = try ServiceConfig.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) let policy = try self.decoder.decode( - ServiceConfiguration.RetryThrottlingPolicy.self, + ServiceConfig.RetryThrottlingPolicy.self, from: Data(json.utf8) ) @@ -56,7 +56,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { } func testEncodeDecodeRetryThrottlingPolicy() throws { - let policy = try ServiceConfiguration.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) + let policy = try ServiceConfig.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) try self.testRoundTripEncodeDecode(policy) } @@ -72,7 +72,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { try self.testDecodeThrowsRuntimeError( json: json, - as: ServiceConfiguration.RetryThrottlingPolicy.self + as: ServiceConfig.RetryThrottlingPolicy.self ) } } @@ -89,13 +89,13 @@ final class ServiceConfigurationCodingTests: XCTestCase { try self.testDecodeThrowsRuntimeError( json: json, - as: ServiceConfiguration.RetryThrottlingPolicy.self + as: ServiceConfig.RetryThrottlingPolicy.self ) } } func testDecodePickFirstPolicy() throws { - let inputs: [(String, ServiceConfiguration.LoadBalancingConfiguration.PickFirst)] = [ + let inputs: [(String, ServiceConfig.LoadBalancingConfiguration.PickFirst)] = [ (#"{"shuffleAddressList": true}"#, .init(shuffleAddressList: true)), (#"{"shuffleAddressList": false}"#, .init(shuffleAddressList: false)), (#"{}"#, .init(shuffleAddressList: false)), @@ -103,7 +103,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { for (input, expected) in inputs { let pickFirst = try self.decoder.decode( - ServiceConfiguration.LoadBalancingConfiguration.PickFirst.self, + ServiceConfig.LoadBalancingConfiguration.PickFirst.self, from: Data(input.utf8) ) @@ -112,7 +112,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { } func testEncodePickFirstPolicy() throws { - let inputs: [(ServiceConfiguration.LoadBalancingConfiguration.PickFirst, String)] = [ + let inputs: [(ServiceConfig.LoadBalancingConfiguration.PickFirst, String)] = [ (.init(shuffleAddressList: true), #"{"shuffleAddressList":true}"#), (.init(shuffleAddressList: false), #"{"shuffleAddressList":false}"#), ] @@ -126,20 +126,20 @@ final class ServiceConfigurationCodingTests: XCTestCase { func testDecodeRoundRobinPolicy() throws { let json = "{}" let policy = try self.decoder.decode( - ServiceConfiguration.LoadBalancingConfiguration.RoundRobin.self, + ServiceConfig.LoadBalancingConfiguration.RoundRobin.self, from: Data(json.utf8) ) - XCTAssertEqual(policy, ServiceConfiguration.LoadBalancingConfiguration.RoundRobin()) + XCTAssertEqual(policy, ServiceConfig.LoadBalancingConfiguration.RoundRobin()) } func testEncodeRoundRobinPolicy() throws { - let policy = ServiceConfiguration.LoadBalancingConfiguration.RoundRobin() + let policy = ServiceConfig.LoadBalancingConfiguration.RoundRobin() let encoded = try self.encoder.encode(policy) XCTAssertEqual(String(decoding: encoded, as: UTF8.self), "{}") } func testDecodeLoadBalancingConfiguration() throws { - let inputs: [(String, ServiceConfiguration.LoadBalancingConfiguration)] = [ + let inputs: [(String, ServiceConfig.LoadBalancingConfiguration)] = [ (#"{"round_robin": {}}"#, .roundRobin), (#"{"pick_first": {}}"#, .pickFirst(shuffleAddressList: false)), (#"{"pick_first": {"shuffleAddressList": false}}"#, .pickFirst(shuffleAddressList: false)), @@ -147,7 +147,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { for (input, expected) in inputs { let decoded = try self.decoder.decode( - ServiceConfiguration.LoadBalancingConfiguration.self, + ServiceConfig.LoadBalancingConfiguration.self, from: Data(input.utf8) ) XCTAssertEqual(decoded, expected) @@ -155,7 +155,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { } func testEncodeLoadBalancingConfiguration() throws { - let inputs: [(ServiceConfiguration.LoadBalancingConfiguration, String)] = [ + let inputs: [(ServiceConfig.LoadBalancingConfiguration, String)] = [ (.roundRobin, #"{"round_robin":{}}"#), (.pickFirst(shuffleAddressList: false), #"{"pick_first":{"shuffleAddressList":false}}"#), ] @@ -166,7 +166,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { } } - func testDecodeServiceConfigurationFromProtoJSON() throws { + func testDecodeServiceConfigFromProtoJSON() throws { let serviceConfig = Grpc_ServiceConfig_ServiceConfig.with { $0.methodConfig = [ Grpc_ServiceConfig_MethodConfig.with { @@ -193,13 +193,13 @@ final class ServiceConfigurationCodingTests: XCTestCase { } let encoded = try serviceConfig.jsonUTF8Data() - let decoded = try self.decoder.decode(ServiceConfiguration.self, from: encoded) + let decoded = try self.decoder.decode(ServiceConfig.self, from: encoded) - let expected = ServiceConfiguration( - methodConfiguration: [ - MethodConfiguration( + let expected = ServiceConfig( + methodConfig: [ + MethodConfig( names: [ - MethodConfiguration.Name(service: "foo.Foo", method: "Bar") + MethodConfig.Name(service: "foo.Foo", method: "Bar") ], timeout: .seconds(1), maxRequestMessageBytes: 123, @@ -210,7 +210,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { .roundRobin, .pickFirst(shuffleAddressList: true), ], - retryThrottlingPolicy: try ServiceConfiguration.RetryThrottlingPolicy( + retryThrottlingPolicy: try ServiceConfig.RetryThrottlingPolicy( maxTokens: 10, tokenRatio: 0.1 ) @@ -219,13 +219,13 @@ final class ServiceConfigurationCodingTests: XCTestCase { XCTAssertEqual(decoded, expected) } - func testEncodeAndDecodeServiceConfiguration() throws { - let serviceConfig = ServiceConfiguration( - methodConfiguration: [ - MethodConfiguration( + func testEncodeAndDecodeServiceConfig() throws { + let serviceConfig = ServiceConfig( + methodConfig: [ + MethodConfig( names: [ - MethodConfiguration.Name(service: "echo.Echo", method: "Get"), - MethodConfiguration.Name(service: "greeter.HelloWorld"), + MethodConfig.Name(service: "echo.Echo", method: "Get"), + MethodConfig.Name(service: "greeter.HelloWorld"), ], timeout: .seconds(42), maxRequestMessageBytes: 2048, @@ -238,9 +238,9 @@ final class ServiceConfigurationCodingTests: XCTestCase { ) ) ), - MethodConfiguration( + MethodConfig( names: [ - MethodConfiguration.Name(service: "echo.Echo", method: "Update") + MethodConfig.Name(service: "echo.Echo", method: "Update") ], timeout: .seconds(300), maxRequestMessageBytes: 10_000 @@ -250,7 +250,7 @@ final class ServiceConfigurationCodingTests: XCTestCase { .pickFirst(shuffleAddressList: true), .roundRobin, ], - retryThrottlingPolicy: try ServiceConfiguration.RetryThrottlingPolicy( + retryThrottlingPolicy: try ServiceConfig.RetryThrottlingPolicy( maxTokens: 10, tokenRatio: 3.141 ) diff --git a/Tests/GRPCCoreTests/Internal/MethodConfigurationsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift similarity index 78% rename from Tests/GRPCCoreTests/Internal/MethodConfigurationsTests.swift rename to Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift index 53a90ec62..4bf2b1113 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigurationsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -17,15 +17,15 @@ import GRPCCore import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class MethodConfigurationsTests: XCTestCase { +final class MethodConfigsTests: XCTestCase { func testGetConfigurationForKnownMethod() async throws { let policy = HedgingPolicy( maximumAttempts: 10, hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) - var configurations = _MethodConfigurations() + let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) + var configurations = _MethodConfigs() configurations.setDefaultConfiguration(defaultConfiguration) let descriptor = MethodDescriptor(service: "test", method: "first") let retryPolicy = RetryPolicy( @@ -35,7 +35,7 @@ final class MethodConfigurationsTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) + let overrideConfiguration = MethodConfig(names: [], executionPolicy: .retry(retryPolicy)) configurations[descriptor] = overrideConfiguration XCTAssertEqual(configurations[descriptor], overrideConfiguration) @@ -47,8 +47,8 @@ final class MethodConfigurationsTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) - var configurations = _MethodConfigurations() + let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) + var configurations = _MethodConfigs() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test", method: "") let retryPolicy = RetryPolicy( @@ -58,7 +58,7 @@ final class MethodConfigurationsTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) + let overrideConfiguration = MethodConfig(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration let secondDescriptor = MethodDescriptor(service: "test", method: "second") @@ -71,8 +71,8 @@ final class MethodConfigurationsTests: XCTestCase { hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) - let defaultConfiguration = MethodConfiguration(names: [], executionPolicy: .hedge(policy)) - var configurations = _MethodConfigurations() + let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) + var configurations = _MethodConfigs() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test1", method: "first") let retryPolicy = RetryPolicy( @@ -82,7 +82,7 @@ final class MethodConfigurationsTests: XCTestCase { backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration(names: [], executionPolicy: .retry(retryPolicy)) + let overrideConfiguration = MethodConfig(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration let secondDescriptor = MethodDescriptor(service: "test2", method: "second") diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index baf9e873a..b146529d1 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -29,7 +29,7 @@ struct AnyClientTransport: ClientTransport, Sendable { ) async throws -> Any private let _connect: @Sendable (Bool) async throws -> Void private let _close: @Sendable () -> Void - private let _configuration: @Sendable (MethodDescriptor) -> MethodConfiguration? + private let _configuration: @Sendable (MethodDescriptor) -> MethodConfig? init(wrapping transport: Transport) where Transport.Inbound == Inbound, Transport.Outbound == Outbound { @@ -76,7 +76,7 @@ struct AnyClientTransport: ClientTransport, Sendable { func configuration( forMethod descriptor: MethodDescriptor - ) -> MethodConfiguration? { + ) -> MethodConfig? { self._configuration(descriptor) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 3fb4f125f..0c81fb17c 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -72,7 +72,7 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { func configuration( forMethod descriptor: MethodDescriptor - ) -> MethodConfiguration? { + ) -> MethodConfig? { self.transport.configuration(forMethod: descriptor) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 3a3e5828a..113c2cf53 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -38,7 +38,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { func configuration( forMethod descriptor: MethodDescriptor - ) -> MethodConfiguration? { + ) -> MethodConfig? { return nil } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift index a0037f49f..c4bd2bfe0 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift @@ -22,9 +22,9 @@ import XCTest final class ClientConnectionHandlerStateMachineTests: XCTestCase { private func makeStateMachine( - keepAliveWithoutCalls: Bool = false + keepaliveWithoutCalls: Bool = false ) -> ClientConnectionHandler.StateMachine { - return ClientConnectionHandler.StateMachine(allowKeepAliveWithoutCalls: keepAliveWithoutCalls) + return ClientConnectionHandler.StateMachine(allowKeepaliveWithoutCalls: keepaliveWithoutCalls) } func testCloseSomeStreamsWhenActive() { @@ -32,7 +32,7 @@ final class ClientConnectionHandlerStateMachineTests: XCTestCase { state.streamOpened(1) state.streamOpened(2) XCTAssertEqual(state.streamClosed(2), .none) - XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepAlive: true)) + XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepalive: true)) } func testCloseSomeStreamsWhenClosing() { @@ -51,45 +51,45 @@ final class ClientConnectionHandlerStateMachineTests: XCTestCase { XCTAssertEqual(state.streamClosed(1), .none) } - func testSendKeepAlivePing() { - var state = self.makeStateMachine(keepAliveWithoutCalls: false) + func testSendKeepalivePing() { + var state = self.makeStateMachine(keepaliveWithoutCalls: false) // No streams open so ping isn't allowed. - XCTAssertFalse(state.sendKeepAlivePing()) + XCTAssertFalse(state.sendKeepalivePing()) // Stream open, ping allowed. state.streamOpened(1) - XCTAssertTrue(state.sendKeepAlivePing()) + XCTAssertTrue(state.sendKeepalivePing()) // No stream, no ping. - XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepAlive: true)) - XCTAssertFalse(state.sendKeepAlivePing()) + XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepalive: true)) + XCTAssertFalse(state.sendKeepalivePing()) } - func testSendKeepAlivePingWhenAllowedWithoutCalls() { - var state = self.makeStateMachine(keepAliveWithoutCalls: true) + func testSendKeepalivePingWhenAllowedWithoutCalls() { + var state = self.makeStateMachine(keepaliveWithoutCalls: true) // Keep alive is allowed when no streams are open, so pings are allowed. - XCTAssertTrue(state.sendKeepAlivePing()) + XCTAssertTrue(state.sendKeepalivePing()) state.streamOpened(1) - XCTAssertTrue(state.sendKeepAlivePing()) + XCTAssertTrue(state.sendKeepalivePing()) - XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepAlive: false)) - XCTAssertTrue(state.sendKeepAlivePing()) + XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepalive: false)) + XCTAssertTrue(state.sendKeepalivePing()) } - func testSendKeepAlivePingWhenClosing() { - var state = self.makeStateMachine(keepAliveWithoutCalls: false) + func testSendKeepalivePingWhenClosing() { + var state = self.makeStateMachine(keepaliveWithoutCalls: false) state.streamOpened(1) XCTAssertTrue(state.beginClosing()) // Stream is opened and state is closing, ping is allowed. - XCTAssertTrue(state.sendKeepAlivePing()) + XCTAssertTrue(state.sendKeepalivePing()) } - func testSendKeepAlivePingWhenClosed() { - var state = self.makeStateMachine(keepAliveWithoutCalls: true) + func testSendKeepalivePingWhenClosed() { + var state = self.makeStateMachine(keepaliveWithoutCalls: true) _ = state.closed() - XCTAssertFalse(state.sendKeepAlivePing()) + XCTAssertFalse(state.sendKeepalivePing()) } func testBeginGracefulShutdownWhenStreamsAreOpen() { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index e4a296be4..c9d828645 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -70,8 +70,8 @@ final class ClientConnectionHandlerTests: XCTestCase { try connection.waitUntilClosed() } - func testKeepAliveWithOpenStreams() throws { - let connection = try Connection(keepAliveTime: .minutes(1), keepAliveTimeout: .seconds(10)) + func testKeepaliveWithOpenStreams() throws { + let connection = try Connection(keepaliveTime: .minutes(1), keepaliveTimeout: .seconds(10)) try connection.activate() // Open a stream so keep-alive starts. @@ -96,8 +96,8 @@ final class ClientConnectionHandlerTests: XCTestCase { XCTAssertNil(try connection.readFrame()) } - func testKeepAliveWithNoOpenStreams() throws { - let connection = try Connection(keepAliveTime: .minutes(1), allowKeepAliveWithoutCalls: true) + func testKeepaliveWithNoOpenStreams() throws { + let connection = try Connection(keepaliveTime: .minutes(1), allowKeepaliveWithoutCalls: true) try connection.activate() for _ in 0 ..< 10 { @@ -114,8 +114,8 @@ final class ClientConnectionHandlerTests: XCTestCase { } } - func testKeepAliveWithOpenStreamsTimingOut() throws { - let connection = try Connection(keepAliveTime: .minutes(1), keepAliveTimeout: .seconds(10)) + func testKeepaliveWithOpenStreamsTimingOut() throws { + let connection = try Connection(keepaliveTime: .minutes(1), keepaliveTimeout: .seconds(10)) try connection.activate() // Open a stream so keep-alive starts. @@ -135,7 +135,7 @@ final class ClientConnectionHandlerTests: XCTestCase { // - be closed connection.loop.advanceTime(by: .seconds(10)) - XCTAssertEqual(try connection.readEvent(), .closing(.keepAliveExpired)) + XCTAssertEqual(try connection.readEvent(), .closing(.keepaliveExpired)) let frame2 = try XCTUnwrap(connection.readFrame()) XCTAssertEqual(frame2.streamID, .rootStream) @@ -217,17 +217,17 @@ extension ClientConnectionHandlerTests { init( maxIdleTime: TimeAmount? = nil, - keepAliveTime: TimeAmount? = nil, - keepAliveTimeout: TimeAmount? = nil, - allowKeepAliveWithoutCalls: Bool = false + keepaliveTime: TimeAmount? = nil, + keepaliveTimeout: TimeAmount? = nil, + allowKeepaliveWithoutCalls: Bool = false ) throws { let loop = EmbeddedEventLoop() let handler = ClientConnectionHandler( eventLoop: loop, maxIdleTime: maxIdleTime, - keepAliveTime: keepAliveTime, - keepAliveTimeout: keepAliveTimeout, - keepAliveWithoutCalls: allowKeepAliveWithoutCalls + keepaliveTime: keepaliveTime, + keepaliveTimeout: keepaliveTimeout, + keepaliveWithoutCalls: allowKeepaliveWithoutCalls ) self.channel = EmbeddedChannel(handler: handler, loop: loop) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift index 75bd6f5d7..11102f999 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift @@ -178,7 +178,7 @@ final class NameResolverRegistryTests: XCTestCase { for _ in 0 ..< 1000 { let result = try await XCTUnwrapAsync { try await iterator.next() } XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.ipv4(host: "foo", port: 1234)])]) - XCTAssertNil(result.serviceConfiguration) + XCTAssertNil(result.serviceConfig) } } @@ -199,7 +199,7 @@ final class NameResolverRegistryTests: XCTestCase { Endpoint(addresses: [.ipv4(host: "bar", port: 444)]), ] ) - XCTAssertNil(result.serviceConfiguration) + XCTAssertNil(result.serviceConfig) } } @@ -214,7 +214,7 @@ final class NameResolverRegistryTests: XCTestCase { for _ in 0 ..< 1000 { let result = try await XCTUnwrapAsync { try await iterator.next() } XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.ipv6(host: "foo", port: 1234)])]) - XCTAssertNil(result.serviceConfiguration) + XCTAssertNil(result.serviceConfig) } } @@ -235,7 +235,7 @@ final class NameResolverRegistryTests: XCTestCase { Endpoint(addresses: [.ipv6(host: "bar", port: 444)]), ] ) - XCTAssertNil(result.serviceConfiguration) + XCTAssertNil(result.serviceConfig) } } @@ -250,7 +250,7 @@ final class NameResolverRegistryTests: XCTestCase { for _ in 0 ..< 1000 { let result = try await XCTUnwrapAsync { try await iterator.next() } XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.unixDomainSocket(path: "/foo")])]) - XCTAssertNil(result.serviceConfiguration) + XCTAssertNil(result.serviceConfig) } } @@ -265,7 +265,7 @@ final class NameResolverRegistryTests: XCTestCase { for _ in 0 ..< 1000 { let result = try await XCTUnwrapAsync { try await iterator.next() } XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.vsock(contextID: .any, port: .any)])]) - XCTAssertNil(result.serviceConfiguration) + XCTAssertNil(result.serviceConfig) } } } diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift index 47daf4d58..77a067a5b 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift @@ -22,12 +22,12 @@ import XCTest final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase { private func makeStateMachine( - allowKeepAliveWithoutCalls: Bool = false, + allowKeepaliveWithoutCalls: Bool = false, minPingReceiveIntervalWithoutCalls: TimeAmount = .minutes(5), goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: 42) ) -> ServerConnectionManagementHandler.StateMachine { return .init( - allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls, + allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls, minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls, goAwayPingData: goAwayPingData ) @@ -193,9 +193,9 @@ final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase { XCTAssertEqual(state.receivedPing(atTime: time, data: data), .enhanceYourCalmThenClose(id)) } - func testPingStrikesWhenKeepAliveIsNotPermittedWithoutCalls() { + func testPingStrikesWhenKeepaliveIsNotPermittedWithoutCalls() { let initialState = self.makeStateMachine( - allowKeepAliveWithoutCalls: false, + allowKeepaliveWithoutCalls: false, minPingReceiveIntervalWithoutCalls: .minutes(5) ) @@ -207,9 +207,9 @@ final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase { self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .hours(2), expectedID: 0) } - func testPingStrikesWhenKeepAliveIsPermittedWithoutCalls() { + func testPingStrikesWhenKeepaliveIsPermittedWithoutCalls() { var state = self.makeStateMachine( - allowKeepAliveWithoutCalls: true, + allowKeepaliveWithoutCalls: true, minPingReceiveIntervalWithoutCalls: .minutes(5) ) @@ -218,7 +218,7 @@ final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase { func testResetPingStrikeState() { var state = self.makeStateMachine( - allowKeepAliveWithoutCalls: true, + allowKeepaliveWithoutCalls: true, minPingReceiveIntervalWithoutCalls: .minutes(5) ) @@ -234,7 +234,7 @@ final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase { XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) // Reset the ping strike state and test ping strikes as normal. - state.resetKeepAliveState() + state.resetKeepaliveState() self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 0) } } diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift index abbd6bb52..738221b10 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift @@ -128,10 +128,10 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { try connection.waitUntilClosed() } - func testKeepAliveOnNewConnection() throws { + func testKeepaliveOnNewConnection() throws { let connection = try Connection( - keepAliveTime: .minutes(5), - keepAliveTimeout: .seconds(5) + keepaliveTime: .minutes(5), + keepaliveTimeout: .seconds(5) ) try connection.activate() @@ -151,10 +151,10 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { XCTAssertNil(try connection.readFrame()) } - func testKeepAliveStartsAfterReadLoop() throws { + func testKeepaliveStartsAfterReadLoop() throws { let connection = try Connection( - keepAliveTime: .minutes(5), - keepAliveTimeout: .seconds(5) + keepaliveTime: .minutes(5), + keepaliveTimeout: .seconds(5) ) try connection.activate() @@ -179,10 +179,10 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { } } - func testKeepAliveOnNewConnectionWithoutResponse() throws { + func testKeepaliveOnNewConnectionWithoutResponse() throws { let connection = try Connection( - keepAliveTime: .minutes(5), - keepAliveTimeout: .seconds(5) + keepaliveTime: .minutes(5), + keepaliveTimeout: .seconds(5) ) try connection.activate() @@ -203,9 +203,9 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { try connection.waitUntilClosed() } - func testClientKeepAlivePolicing() throws { + func testClientKeepalivePolicing() throws { let connection = try Connection( - allowKeepAliveWithoutCalls: true, + allowKeepaliveWithoutCalls: true, minPingIntervalWithoutCalls: .minutes(1) ) try connection.activate() @@ -235,9 +235,9 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { try connection.waitUntilClosed() } - func testClientKeepAliveWithPermissibleIntervals() throws { + func testClientKeepaliveWithPermissibleIntervals() throws { let connection = try Connection( - allowKeepAliveWithoutCalls: true, + allowKeepaliveWithoutCalls: true, minPingIntervalWithoutCalls: .minutes(1), manualClock: true ) @@ -257,14 +257,14 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { } } - func testClientKeepAliveResetState() throws { + func testClientKeepaliveResetState() throws { let connection = try Connection( - allowKeepAliveWithoutCalls: true, + allowKeepaliveWithoutCalls: true, minPingIntervalWithoutCalls: .minutes(1) ) try connection.activate() - func sendThreeKeepAlivePings() throws { + func sendThreeKeepalivePings() throws { // The first ping is valid, the second and third are strikes. for _ in 1 ... 3 { try connection.ping(data: HTTP2PingData(), ack: false) @@ -277,14 +277,14 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { } } - try sendThreeKeepAlivePings() + try sendThreeKeepalivePings() // "send" a HEADERS frame and flush to reset keep alive state. connection.syncView.wroteHeadersFrame() connection.syncView.connectionWillFlush() // As above, the first ping is valid, the next two are strikes. - try sendThreeKeepAlivePings() + try sendThreeKeepalivePings() // The next ping is the third strike and triggers a GOAWAY. try connection.ping(data: HTTP2PingData(), ack: false) @@ -353,9 +353,9 @@ extension ServerConnectionManagementHandlerTests { maxIdleTime: TimeAmount? = nil, maxAge: TimeAmount? = nil, maxGraceTime: TimeAmount? = nil, - keepAliveTime: TimeAmount? = nil, - keepAliveTimeout: TimeAmount? = nil, - allowKeepAliveWithoutCalls: Bool = false, + keepaliveTime: TimeAmount? = nil, + keepaliveTimeout: TimeAmount? = nil, + allowKeepaliveWithoutCalls: Bool = false, minPingIntervalWithoutCalls: TimeAmount = .minutes(5), manualClock: Bool = false ) throws { @@ -371,9 +371,9 @@ extension ServerConnectionManagementHandlerTests { maxIdleTime: maxIdleTime, maxAge: maxAge, maxGraceTime: maxGraceTime, - keepAliveTime: keepAliveTime, - keepAliveTimeout: keepAliveTimeout, - allowKeepAliveWithoutCalls: allowKeepAliveWithoutCalls, + keepaliveTime: keepaliveTime, + keepaliveTimeout: keepaliveTimeout, + allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls, minPingIntervalWithoutCalls: minPingIntervalWithoutCalls, clock: self.clock ) diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index a59e096c7..c9289f1b9 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -197,11 +197,11 @@ final class InProcessClientTransportTests: XCTestCase { nonFatalStatusCodes: [] ) - var serviceConfiguration = ServiceConfiguration( - methodConfiguration: [ - MethodConfiguration( + var serviceConfig = ServiceConfig( + methodConfig: [ + MethodConfig( names: [ - MethodConfiguration.Name(service: "", method: "") + MethodConfig.Name(service: "", method: "") ], executionPolicy: .hedge(policy) ) @@ -210,13 +210,13 @@ final class InProcessClientTransportTests: XCTestCase { var client = InProcessClientTransport( server: InProcessServerTransport(), - serviceConfiguration: serviceConfiguration + serviceConfig: serviceConfig ) let firstDescriptor = MethodDescriptor(service: "test", method: "first") XCTAssertEqual( client.configuration(forMethod: firstDescriptor), - serviceConfiguration.methodConfiguration.first + serviceConfig.methodConfig.first ) let retryPolicy = RetryPolicy( @@ -227,24 +227,24 @@ final class InProcessClientTransportTests: XCTestCase { retryableStatusCodes: [.unavailable] ) - let overrideConfiguration = MethodConfiguration( - names: [MethodConfiguration.Name(service: "test", method: "second")], + let overrideConfiguration = MethodConfig( + names: [MethodConfig.Name(service: "test", method: "second")], executionPolicy: .retry(retryPolicy) ) - serviceConfiguration.methodConfiguration.append(overrideConfiguration) + serviceConfig.methodConfig.append(overrideConfiguration) client = InProcessClientTransport( server: InProcessServerTransport(), - serviceConfiguration: serviceConfiguration + serviceConfig: serviceConfig ) let secondDescriptor = MethodDescriptor(service: "test", method: "second") XCTAssertEqual( client.configuration(forMethod: firstDescriptor), - serviceConfiguration.methodConfiguration.first + serviceConfig.methodConfig.first ) XCTAssertEqual( client.configuration(forMethod: secondDescriptor), - serviceConfiguration.methodConfiguration.last + serviceConfig.methodConfig.last ) } @@ -295,10 +295,10 @@ final class InProcessClientTransportTests: XCTestCase { retryableStatusCodes: [.unavailable] ) - let serviceConfiguration = ServiceConfiguration( - methodConfiguration: [ - MethodConfiguration( - names: [MethodConfiguration.Name(service: "", method: "")], + let serviceConfig = ServiceConfig( + methodConfig: [ + MethodConfig( + names: [MethodConfig.Name(service: "", method: "")], executionPolicy: .retry(defaultPolicy) ) ] @@ -306,7 +306,7 @@ final class InProcessClientTransportTests: XCTestCase { return InProcessClientTransport( server: server, - serviceConfiguration: serviceConfiguration + serviceConfig: serviceConfig ) } } From 192a0d37ef0eb12357fccf610b6bc13716d42712 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:05:00 +0100 Subject: [PATCH 291/580] runClient RPC response in performance worker (#1857) Motivation: We need the `runClient` RPC to monitor the performance of the benchmark clients. Modifications: - implemented the methods that merge the histograms and responses count - implemented the method that creates the response for `runClient` - implemented the `runClient` method Result: We can construct a latency histogram that represents the data from all clients and the `runClient` method returns the stats about the clients started by the worker. --- .../performance-worker/BenchmarkClient.swift | 6 + Sources/performance-worker/RPCStats.swift | 12 +- .../performance-worker/WorkerService.swift | 169 +++++++++++++++++- 3 files changed, 184 insertions(+), 3 deletions(-) diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 7cecbc4ac..63d7506b6 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -48,6 +48,12 @@ struct BenchmarkClient { self.rpcStats = NIOLockedValueBox(RPCStats(latencyHistogram: histogram)) } + internal var currentStats: RPCStats { + return self.rpcStats.withLockedValue { stats in + return stats + } + } + internal func run() async throws { let benchmarkClient = Grpc_Testing_BenchmarkServiceClient(client: client) return try await withThrowingTaskGroup(of: Void.self) { clientGroup in diff --git a/Sources/performance-worker/RPCStats.swift b/Sources/performance-worker/RPCStats.swift index 3b3aeb9f3..7d864fb4f 100644 --- a/Sources/performance-worker/RPCStats.swift +++ b/Sources/performance-worker/RPCStats.swift @@ -107,7 +107,7 @@ struct RPCStats { /// Merge two histograms together updating `self` /// - parameters: - /// - source: the other histogram to merge into this. + /// - other: the other histogram to merge into this. public mutating func merge(_ other: LatencyHistogram) throws { guard (self.buckets.count == other.buckets.count) || (self.multiplier == other.multiplier) else { @@ -129,4 +129,14 @@ struct RPCStats { } } } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + mutating func merge(_ other: RPCStats) throws { + try self.latencyHistogram.merge( + other.latencyHistogram + ) + self.requestResultCount.merge(other.requestResultCount) { (current, new) in + current + new + } + } } diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 74934ca4d..e9e61d14f 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -48,13 +48,16 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable struct ClientState { var clients: [BenchmarkClient] var stats: ClientStats + var rpcStats: RPCStats init( clients: [BenchmarkClient], - stats: ClientStats + stats: ClientStats, + rpcStats: RPCStats ) { self.clients = clients self.stats = stats + self.rpcStats = rpcStats } func shutdownClients() throws { @@ -79,6 +82,24 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } } + var clients: [BenchmarkClient]? { + switch self.role { + case let .client(clientState): + return clientState.clients + case .server, .none: + return nil + } + } + + var clientRPCStats: RPCStats? { + switch self.role { + case let .client(clientState): + return clientState.rpcStats + case .server, .none: + return nil + } + } + mutating func serverStats(replaceWith newStats: ServerStats? = nil) -> ServerStats? { switch self.role { case var .server(serverState): @@ -120,6 +141,45 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable self.role = .server(serverState) } } + + mutating func setupClients( + benchmarkClients: [BenchmarkClient], + stats: ClientStats, + rpcStats: RPCStats + ) throws { + let clientState = State.ClientState( + clients: benchmarkClients, + stats: stats, + rpcStats: rpcStats + ) + switch self.role { + case .server(_): + throw RPCError(code: .alreadyExists, message: "This worker has a server setup.") + + case .client(_): + throw RPCError(code: .failedPrecondition, message: "Clients have already been set up.") + + case .none: + self.role = .client(clientState) + } + } + + mutating func updateRPCStats() throws { + switch self.role { + case var .client(clientState): + let benchmarkClients = clientState.clients + var rpcStats = clientState.rpcStats + for benchmarkClient in benchmarkClients { + try rpcStats.merge(benchmarkClient.currentStats) + } + + clientState.rpcStats = rpcStats + self.role = .client(clientState) + + case .server, .none: + () + } + } } func quitWorker( @@ -194,7 +254,33 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable ) async throws -> GRPCCore.ServerResponse.Stream { - throw RPCError(code: .unimplemented, message: "This RPC has not been implemented yet.") + return ServerResponse.Stream { writer in + try await withThrowingTaskGroup(of: Void.self) { group in + for try await message in request.messages { + switch message.argtype { + case let .setup(config): + // Create the clients with the initial stats. + let clients = try await self.setupClients(config) + + for client in clients { + group.addTask { + try await client.run() + } + } + + case let .mark(mark): + let response = try await self.makeClientStatsResponse(reset: mark.reset) + try await writer.write(response) + + case .none: + () + } + } + try await group.waitForAll() + + return [:] + } + } } } @@ -237,4 +323,83 @@ extension WorkerService { } } } + + private func setupClients(_ config: Grpc_Testing_ClientConfig) async throws -> [BenchmarkClient] { + var clients = [BenchmarkClient]() + for _ in 0 ..< config.clientChannels { + let grpcClient = self.makeGRPCClient() + clients.append( + BenchmarkClient( + client: grpcClient, + rpcNumber: config.outstandingRpcsPerChannel, + rpcType: config.rpcType, + histogramParams: config.histogramParams + ) + ) + } + let stats = ClientStats() + let histogram = RPCStats.LatencyHistogram( + resolution: config.histogramParams.resolution, + maxBucketStart: config.histogramParams.maxPossible + ) + + try self.state.withLockedValue { state in + try state.setupClients( + benchmarkClients: clients, + stats: stats, + rpcStats: RPCStats(latencyHistogram: histogram) + ) + } + + return clients + } + + func makeGRPCClient() -> GRPCClient { + fatalError() + } + + private func makeClientStatsResponse( + reset: Bool + ) async throws -> Grpc_Testing_WorkerService.Method.RunClient.Output { + let currentUsageStats = ClientStats() + let (initialUsageStats, rpcStats) = try self.state.withLockedValue { state in + let initialUsageStats = state.clientStats(replaceWith: reset ? currentUsageStats : nil) + try state.updateRPCStats() + let rpcStats = state.clientRPCStats + return (initialUsageStats, rpcStats) + } + + guard let initialUsageStats = initialUsageStats, let rpcStats = rpcStats else { + throw RPCError( + code: .notFound, + message: "There are no initial client stats. Clients must be setup before calling 'mark'." + ) + } + + let differences = currentUsageStats.difference(to: initialUsageStats) + + let requestResults = rpcStats.requestResultCount.map { (key, value) in + return Grpc_Testing_RequestResultCount.with { + $0.statusCode = Int32(key.rawValue) + $0.count = value + } + } + + return Grpc_Testing_WorkerService.Method.RunClient.Output.with { + $0.stats = Grpc_Testing_ClientStats.with { + $0.timeElapsed = differences.time + $0.timeSystem = differences.systemTime + $0.timeUser = differences.userTime + $0.requestResults = requestResults + $0.latencies = Grpc_Testing_HistogramData.with { + $0.bucket = rpcStats.latencyHistogram.buckets + $0.minSeen = rpcStats.latencyHistogram.minSeen + $0.maxSeen = rpcStats.latencyHistogram.maxSeen + $0.sum = rpcStats.latencyHistogram.sum + $0.sumOfSquares = rpcStats.latencyHistogram.sumOfSquares + $0.count = rpcStats.latencyHistogram.countOfValuesSeen + } + } + } + } } From dac2d68ab589e2a0da09f784aeb0fc14da2d22ae Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 16 Apr 2024 10:41:53 +0100 Subject: [PATCH 292/580] 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 --- .../GRPCCore/Call/Client/CallOptions.swift | 53 +++---- .../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, 298 insertions(+), 121 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..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 - - public struct Compression: Sendable { - /// Whether request messages should be compressed. - /// - /// Note that this option is _advisory_: transports are not required to support compression. - public var requests: Bool - - /// Whether response messages are permitted to be compressed. - public var responses: Bool - - /// 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) - } - } + /// 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 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 @@ -131,9 +112,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 +120,7 @@ extension CallOptions { maxRequestMessageBytes: nil, maxResponseMessageBytes: nil, executionPolicy: nil, - compression: .disabled + compression: nil ) } } diff --git a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift new file mode 100644 index 000000000..e3b39e048 --- /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: IndexingIterator<[CompressionAlgorithm.Value]> + + 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 a637cf3ca910d601020ce1971d9f8c26ea73a2ed Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 17 Apr 2024 16:07:05 +0100 Subject: [PATCH 293/580] Add connection backoff (#1860) Motivation: Connection attempts should be made with a backoff period between them. Modifications: - Add a connection backoff struct which can make an iterator to produces duration to backoff by Result: Can do backoff --- .../Client/Connection/ConnectionBackoff.swift | 87 +++++++++++++++++++ .../Connection/ConnectionBackoffTests.swift | 71 +++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift new file mode 100644 index 000000000..49feb2282 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift @@ -0,0 +1,87 @@ +/* + * 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct ConnectionBackoff { + var initial: Duration + var max: Duration + var multiplier: Double + var jitter: Double + + init(initial: Duration, max: Duration, multiplier: Double, jitter: Double) { + self.initial = initial + self.max = max + self.multiplier = multiplier + self.jitter = jitter + } + + func makeIterator() -> Iterator { + return Iterator(self) + } + + // Deliberately not conforming to `IteratorProtocol` as `next()` never returns `nil` which + // isn't expressible via `IteratorProtocol`. + struct Iterator { + private var isInitial: Bool + private var currentBackoffSeconds: Double + + private let jitter: Double + private let multiplier: Double + private let maxBackoffSeconds: Double + + init(_ backoff: ConnectionBackoff) { + self.isInitial = true + self.currentBackoffSeconds = Self.seconds(from: backoff.initial) + self.jitter = backoff.jitter + self.multiplier = backoff.multiplier + self.maxBackoffSeconds = Self.seconds(from: backoff.max) + } + + private static func seconds(from duration: Duration) -> Double { + var seconds = Double(duration.components.seconds) + seconds += Double(duration.components.attoseconds) / 1e18 + return seconds + } + + private static func duration(from seconds: Double) -> Duration { + let nanoseconds = seconds * 1e9 + let wholeNanos = Int64(nanoseconds) + return .nanoseconds(wholeNanos) + } + + mutating func next() -> Duration { + // The initial backoff doesn't get jittered. + if self.isInitial { + self.isInitial = false + return Self.duration(from: self.currentBackoffSeconds) + } + + // Scale up the last backoff. + self.currentBackoffSeconds *= self.multiplier + + // Limit it to the max backoff. + if self.currentBackoffSeconds > self.maxBackoffSeconds { + self.currentBackoffSeconds = self.maxBackoffSeconds + } + + let backoff = self.currentBackoffSeconds + let jitter = Double.random(in: -(self.jitter * backoff) ... self.jitter * backoff) + let jitteredBackoff = backoff + jitter + + return Self.duration(from: jitteredBackoff) + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift new file mode 100644 index 000000000..3898513ca --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift @@ -0,0 +1,71 @@ +/* + * 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 XCTest + +@testable import GRPCHTTP2Core + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class ConnectionBackoffTests: XCTestCase { + func testUnjitteredBackoff() { + let backoff = ConnectionBackoff( + initial: .seconds(10), + max: .seconds(30), + multiplier: 1.5, + jitter: 0.0 + ) + + var iterator = backoff.makeIterator() + XCTAssertEqual(iterator.next(), .seconds(10)) + // 10 * 1.5 = 15 seconds + XCTAssertEqual(iterator.next(), .seconds(15)) + // 15 * 1.5 = 22.5 seconds + XCTAssertEqual(iterator.next(), .seconds(22.5)) + // 22.5 * 1.5 = 33.75 seconds, clamped to 30 seconds, all future values will be the same. + XCTAssertEqual(iterator.next(), .seconds(30)) + XCTAssertEqual(iterator.next(), .seconds(30)) + XCTAssertEqual(iterator.next(), .seconds(30)) + } + + func testJitteredBackoff() { + let backoff = ConnectionBackoff( + initial: .seconds(10), + max: .seconds(30), + multiplier: 1.5, + jitter: 0.1 + ) + + var iterator = backoff.makeIterator() + + // Initial isn't jittered. + XCTAssertEqual(iterator.next(), .seconds(10)) + + // Next value should be 10 * 1.5 = 15 seconds ± 1.5 seconds + var expected: ClosedRange = .seconds(13.5) ... .seconds(16.5) + XCTAssert(expected.contains(iterator.next())) + + // Next value should be 15 * 1.5 = 22.5 seconds ± 2.25 seconds + expected = .seconds(20.25) ... .seconds(24.75) + XCTAssert(expected.contains(iterator.next())) + + // Next value should be 22.5 * 1.5 = 33.75 seconds, clamped to 30 seconds ± 3 seconds. + // All future values will be in the same range. + expected = .seconds(27) ... .seconds(33) + XCTAssert(expected.contains(iterator.next())) + XCTAssert(expected.contains(iterator.next())) + XCTAssert(expected.contains(iterator.next())) + } +} From bdb7458e45a65ed8b8a44fced6a710f5aff0b1b3 Mon Sep 17 00:00:00 2001 From: Stefana-Ioana Dranca <66513820+stefanadranca@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:07:20 +0100 Subject: [PATCH 294/580] BenchmarkClient RPCs implementation (#1861) Motivation: When the WorkerService holds and monitors BenchmarkClients, they need to make the requests to the server, as configured in the config input the WorkerService receives from the Test Driver. Modifications: - implemented a helper function that computes the latency and extracts the error code for a RPC - implemented the body of the makrRPC function that makes one of the 5 possible RPCs Result: The BenchmarkClient implementation for performance testing will be completed. --- .../performance-worker/BenchmarkClient.swift | 163 ++++++++++++++++-- .../Internal/AsyncStream+MakeStream.swift | 32 ++++ .../performance-worker/WorkerService.swift | 20 ++- 3 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 Sources/performance-worker/Internal/AsyncStream+MakeStream.swift diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 63d7506b6..7945003b9 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -22,17 +22,23 @@ import NIOConcurrencyHelpers struct BenchmarkClient { private var client: GRPCClient private var rpcNumber: Int32 - private var rpcType: Grpc_Testing_RpcType + private var rpcType: RPCType + private var messagesPerStream: Int32 + private var protoParams: Grpc_Testing_SimpleProtoParams private let rpcStats: NIOLockedValueBox init( client: GRPCClient, rpcNumber: Int32, - rpcType: Grpc_Testing_RpcType, + rpcType: RPCType, + messagesPerStream: Int32, + protoParams: Grpc_Testing_SimpleProtoParams, histogramParams: Grpc_Testing_HistogramParams? ) { self.client = client self.rpcNumber = rpcNumber + self.messagesPerStream = messagesPerStream + self.protoParams = protoParams self.rpcType = rpcType let histogram: RPCStats.LatencyHistogram @@ -48,6 +54,14 @@ struct BenchmarkClient { self.rpcStats = NIOLockedValueBox(RPCStats(latencyHistogram: histogram)) } + enum RPCType { + case unary + case streaming + case streamingFromClient + case streamingFromServer + case streamingBothWays + } + internal var currentStats: RPCStats { return self.rpcStats.withLockedValue { stats in return stats @@ -64,7 +78,9 @@ struct BenchmarkClient { try await withThrowingTaskGroup(of: Void.self) { rpcsGroup in for _ in 0 ..< self.rpcNumber { rpcsGroup.addTask { - let (latency, errorCode) = self.makeRPC(client: benchmarkClient, rpcType: self.rpcType) + let (latency, errorCode) = try await self.makeRPC( + benchmarkClient: benchmarkClient + ) self.rpcStats.withLockedValue { $0.latencyHistogram.record(latency) if let errorCode = errorCode { @@ -80,19 +96,138 @@ struct BenchmarkClient { } } + private func timeIt( + _ body: () async throws -> R + ) async rethrows -> (R, nanoseconds: Double) { + let startTime = DispatchTime.now().uptimeNanoseconds + let result = try await body() + let endTime = DispatchTime.now().uptimeNanoseconds + return (result, nanoseconds: Double(endTime - startTime)) + } + // The result is the number of nanoseconds for processing the RPC. private func makeRPC( - client: Grpc_Testing_BenchmarkServiceClient, - rpcType: Grpc_Testing_RpcType - ) -> (latency: Double, errorCode: RPCError.Code?) { - switch rpcType { - case .unary, .streaming, .streamingFromClient, .streamingFromServer, .streamingBothWays, - .UNRECOGNIZED: - let startTime = DispatchTime.now().uptimeNanoseconds - let endTime = DispatchTime.now().uptimeNanoseconds - return ( - latency: Double(endTime - startTime), errorCode: RPCError.Code(.unimplemented) - ) + benchmarkClient: Grpc_Testing_BenchmarkServiceClient + ) async throws -> (latency: Double, errorCode: RPCError.Code?) { + let message = Grpc_Testing_SimpleRequest.with { + $0.responseSize = self.protoParams.respSize + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: Int(self.protoParams.reqSize)) + } + } + + switch self.rpcType { + case .unary: + let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { + do { + try await benchmarkClient.unaryCall( + request: ClientRequest.Single(message: message) + ) { response in + _ = try response.message + } + return nil + } catch let error as RPCError { + return error.code + } catch { + return .unknown + } + } + return (latency: nanoseconds, errorCode) + + // Repeated sequence of one request followed by one response. + // It is a ping-pong of messages between the client and the server. + case .streaming: + let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { + do { + let ids = AsyncStream.makeStream(of: Int.self) + let streamingRequest = ClientRequest.Stream { writer in + for try await id in ids.stream { + if id <= self.messagesPerStream { + try await writer.write(message) + } else { + return + } + } + } + + ids.continuation.yield(1) + + try await benchmarkClient.streamingCall(request: streamingRequest) { response in + var id = 1 + for try await _ in response.messages { + id += 1 + ids.continuation.yield(id) + } + } + return nil + } catch let error as RPCError { + return error.code + } catch { + return .unknown + } + } + return (latency: nanoseconds, errorCode) + + case .streamingFromClient: + let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { + do { + let streamingRequest = ClientRequest.Stream { writer in + for _ in 1 ... self.messagesPerStream { + try await writer.write(message) + } + } + + try await benchmarkClient.streamingFromClient( + request: streamingRequest + ) { response in + _ = try response.message + } + return nil + } catch let error as RPCError { + return error.code + } catch { + return .unknown + } + } + return (latency: nanoseconds, errorCode) + + case .streamingFromServer: + let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { + do { + try await benchmarkClient.streamingFromServer( + request: ClientRequest.Single(message: message) + ) { response in + for try await _ in response.messages {} + } + return nil + } catch let error as RPCError { + return error.code + } catch { + return .unknown + } + } + return (latency: nanoseconds, errorCode) + + case .streamingBothWays: + let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { + do { + let streamingRequest = ClientRequest.Stream { writer in + for _ in 1 ... self.messagesPerStream { + try await writer.write(message) + } + } + + try await benchmarkClient.streamingBothWays(request: streamingRequest) { response in + for try await _ in response.messages {} + } + return nil + } catch let error as RPCError { + return error.code + } catch { + return .unknown + } + } + return (latency: nanoseconds, errorCode) } } diff --git a/Sources/performance-worker/Internal/AsyncStream+MakeStream.swift b/Sources/performance-worker/Internal/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..5f1b75dd6 --- /dev/null +++ b/Sources/performance-worker/Internal/AsyncStream+MakeStream.swift @@ -0,0 +1,32 @@ +/* + * 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. + */ + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index e9e61d14f..260cde4a1 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -325,6 +325,22 @@ extension WorkerService { } private func setupClients(_ config: Grpc_Testing_ClientConfig) async throws -> [BenchmarkClient] { + let rpcType: BenchmarkClient.RPCType + switch config.rpcType { + case .unary: + rpcType = .unary + case .streaming: + rpcType = .streaming + case .streamingFromClient: + rpcType = .streamingFromClient + case .streamingFromServer: + rpcType = .streamingFromServer + case .streamingBothWays: + rpcType = .streamingBothWays + case .UNRECOGNIZED: + throw RPCError(code: .unknown, message: "The RPC type is UNRECOGNIZED.") + } + var clients = [BenchmarkClient]() for _ in 0 ..< config.clientChannels { let grpcClient = self.makeGRPCClient() @@ -332,7 +348,9 @@ extension WorkerService { BenchmarkClient( client: grpcClient, rpcNumber: config.outstandingRpcsPerChannel, - rpcType: config.rpcType, + rpcType: rpcType, + messagesPerStream: config.messagesPerStream, + protoParams: config.payloadConfig.simpleParams, histogramParams: config.histogramParams ) ) From ec685b3d6707d8c607e43934f4d3b95ed63f4871 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 22 Apr 2024 10:15:29 +0100 Subject: [PATCH 295/580] Add the HTTP/2 connection (#1859) Motivation: To build up a subchannel we need the notion of a connection to a backend as a building block. The connection provides a single HTTP/2 connection to the remote peer and doesn't deal with backoff or reconnects. Modifications: - Add the 'Connection' object which provides multiplexed streams to a connected backend - Add a 'connector' API which provides a NIO channel and a multiplexer on which streams can be created - Add test Utilities and tests Result: Can create a connection to a backend and run streams on it. --- .../Connection/ClientConnectionHandler.swift | 5 +- .../Client/Connection/Connection.swift | 434 ++++++++++++++++++ .../Client/Connection/ConnectionFactory.swift | 48 ++ .../Internal/AsyncStream+MakeStream.swift | 32 ++ .../Internal/Result+Catching.swift | 30 ++ .../ClientConnectionHandlerTests.swift | 3 +- .../Connection/Connection+Equatable.swift | 62 +++ .../Client/Connection/ConnectionTests.swift | 219 +++++++++ .../Connection/Utilities/ConnectionTest.swift | 202 ++++++++ .../Utilities/HTTP2Connectors.swift | 155 +++++++ .../MethodDescriptor+Common.swift | 23 + 11 files changed, 1209 insertions(+), 4 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/Connection.swift create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/Result+Catching.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index adf30f554..44450717b 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -18,8 +18,9 @@ import NIOCore import NIOHTTP2 /// An event which happens on a client's HTTP/2 connection. -enum ClientConnectionEvent: Sendable, Hashable { - enum CloseReason: Sendable, Hashable { +@_spi(Package) +public enum ClientConnectionEvent: Sendable, Hashable { + public enum CloseReason: Sendable, Hashable { /// The server sent a GOAWAY frame to the client. case goAway(HTTP2ErrorCode, String) /// The keep alive timer fired and subsequently timed out. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift new file mode 100644 index 000000000..d9cbe0fd7 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -0,0 +1,434 @@ +/* + * 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 NIOConcurrencyHelpers +import NIOCore +import NIOHTTP2 + +/// A `Connection` provides communication to a single remote peer. +/// +/// Each `Connection` object is 'one-shot': it may only be used for a single connection over +/// its lifetime. If a connect attempt fails then the `Connection` must be discarded and a new one +/// must be created. However, an active connection may be used multiple times to provide streams +/// to the backend. +/// +/// To use the `Connection` you must run it in a task. You can consume event updates by listening +/// to `events`: +/// +/// ```swift +/// await withTaskGroup(of: Void.self) { group in +/// group.addTask { await connection.run() } +/// +/// for await event in connection.events { +/// switch event { +/// case .connectSucceeded: +/// // ... +/// default: +/// // ... +/// } +/// } +/// } +/// ``` +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Connection: Sendable { + /// Events which can happen over the lifetime of the connection. + enum Event: Sendable { + /// The connect attempt succeeded and the connection is ready to use. + case connectSucceeded + /// The connect attempt failed. + case connectFailed(any Error) + /// The connection received a GOAWAY and will close soon. No new streams + /// should be opened on this connection. + case goingAway(HTTP2ErrorCode, String) + /// The connection is closed. + case closed(Connection.CloseReason) + } + + /// The reason the connection closed. + enum CloseReason: Sendable { + /// Closed because an idle timeout fired. + case idleTimeout + /// Closed because a keepalive timer fired. + case keepaliveTimeout + /// Closed because the caller initiated shutdown and all RPCs on the connection finished. + case initiatedLocally + /// Closed because the remote peer initiate shutdown (i.e. sent a GOAWAY frame). + case remote + /// Closed because the connection encountered an unexpected error. + case error(Error) + } + + /// Inputs to the 'run' method. + private enum Input: Sendable { + case close + } + + /// Events which have happened to the connection. + private let event: (stream: AsyncStream, continuation: AsyncStream.Continuation) + + /// Events which the connection must react to. + private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) + + /// The address to connect to. + private let address: SocketAddress + + /// The default compression algorithm used for requests. + private let defaultCompression: CompressionAlgorithm + + /// The set of enabled compression algorithms. + private let enabledCompression: CompressionAlgorithmSet + + /// A connector used to establish a connection. + private let http2Connector: any HTTP2Connector + + /// The state of the connection. + private let state: NIOLockedValueBox + + /// The default max request message size in bytes, 4 MiB. + private static var defaultMaxRequestMessageSizeBytes: Int { + 4 * 1024 * 1024 + } + + /// A stream of events which can happen to the connection. + var events: AsyncStream { + self.event.stream + } + + init( + address: SocketAddress, + http2Connector: any HTTP2Connector, + defaultCompression: CompressionAlgorithm, + enabledCompression: CompressionAlgorithmSet + ) { + self.address = address + self.defaultCompression = defaultCompression + self.enabledCompression = enabledCompression + self.http2Connector = http2Connector + self.event = AsyncStream.makeStream(of: Event.self) + self.input = AsyncStream.makeStream(of: Input.self) + self.state = NIOLockedValueBox(.notConnected) + } + + /// Connect and run the connection. + /// + /// This function returns when the connection has closed. You can observe connection events + /// by consuming the ``events`` sequence. + func run() async { + let connectResult = await Result { + try await self.http2Connector.establishConnection(to: self.address) + } + + switch connectResult { + case .success(let connected): + // Connected successfully, update state and report the event. + self.state.withLockedValue { state in + state.connected(connected) + } + + self.event.continuation.yield(.connectSucceeded) + + await withDiscardingTaskGroup { group in + // Add a task to run the connection and consume events. + group.addTask { + try? await connected.channel.executeThenClose { inbound, outbound in + await self.consumeConnectionEvents(inbound) + } + } + + // Meanwhile, consume input events. This sequence will end when the connection has closed. + for await input in self.input.stream { + switch input { + case .close: + let asyncChannel = self.state.withLockedValue { $0.beginClosing() } + if let channel = asyncChannel?.channel { + let event = ClientConnectionHandler.OutboundEvent.closeGracefully + channel.triggerUserOutboundEvent(event, promise: nil) + } + } + } + } + + case .failure(let error): + // Connect failed, this connection is no longer useful. + self.state.withLockedValue { $0.closed() } + self.finishStreams(withEvent: .connectFailed(error)) + } + } + + /// Gracefully close the connection. + func close() { + self.input.continuation.yield(.close) + } + + /// Make a stream using the connection if it's connected. + /// + /// - Parameter descriptor: A descriptor of the method to create a stream for. + /// - Returns: The open stream. + func makeStream(descriptor: MethodDescriptor, options: CallOptions) async throws -> Stream { + let (multiplexer, scheme) = try self.state.withLockedValue { state in + switch state { + case .connected(let connected): + return (connected.multiplexer, connected.scheme) + case .notConnected, .closing, .closed: + throw RPCError(code: .unavailable, message: "subchannel isn't ready") + } + } + + let compression: CompressionAlgorithm + if let override = options.compression { + compression = self.enabledCompression.contains(override) ? override : .none + } else { + compression = self.defaultCompression + } + + let maxRequestSize = options.maxRequestMessageBytes ?? Self.defaultMaxRequestMessageSizeBytes + + do { + let stream = try await multiplexer.openStream { channel in + channel.eventLoop.makeCompletedFuture { + let streamHandler = GRPCClientStreamHandler( + methodDescriptor: descriptor, + scheme: scheme, + outboundEncoding: compression, + acceptedEncodings: self.enabledCompression, + maximumPayloadSize: maxRequestSize + ) + try channel.pipeline.syncOperations.addHandler(streamHandler) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + isOutboundHalfClosureEnabled: true, + inboundType: RPCResponsePart.self, + outboundType: RPCRequestPart.self + ) + ) + } + } + + return Stream(wrapping: stream, descriptor: descriptor) + } catch { + throw RPCError(code: .unavailable, message: "subchannel is unavailable", cause: error) + } + } + + private func consumeConnectionEvents( + _ connectionEvents: NIOAsyncChannelInboundStream + ) async { + do { + var channelCloseReason: ClientConnectionEvent.CloseReason? + + for try await connectionEvent in connectionEvents { + switch connectionEvent { + case .closing(let reason): + self.state.withLockedValue { $0.closing() } + + switch reason { + case .goAway(let errorCode, let reason): + // The connection will close at some point soon, yield a notification for this + // because the close might not be imminent and this could result in address resolution. + self.event.continuation.yield(.goingAway(errorCode, reason)) + case .idle, .keepaliveExpired, .initiatedLocally: + // The connection will be closed imminently in these cases there's no need to do + // anything. + () + } + + // Take the reason with the highest precedence. A GOAWAY may be superseded by user + // closing, for example. + if channelCloseReason.map({ reason.precedence > $0.precedence }) ?? true { + channelCloseReason = reason + } + } + } + + let connectionCloseReason: Self.CloseReason + switch channelCloseReason { + case .keepaliveExpired: + connectionCloseReason = .keepaliveTimeout + + case .idle: + // Connection became idle, that's fine. + connectionCloseReason = .idleTimeout + + case .goAway: + // Remote peer told us to GOAWAY. + connectionCloseReason = .remote + + case .initiatedLocally, .none: + // Shutdown was initiated locally. + connectionCloseReason = .initiatedLocally + } + + // The connection events sequence has finished: the connection is now closed. + self.state.withLockedValue { $0.closed() } + self.finishStreams(withEvent: .closed(connectionCloseReason)) + } catch { + // Any error must come from consuming the inbound channel meaning that the connection + // must be borked, wrap it up and close. + let rpcError = RPCError(code: .unavailable, message: "connection closed", cause: error) + self.state.withLockedValue { $0.closed() } + self.finishStreams(withEvent: .closed(.error(rpcError))) + } + } + + private func finishStreams(withEvent event: Event) { + self.event.continuation.yield(event) + self.event.continuation.finish() + self.input.continuation.finish() + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection { + struct Stream { + typealias Inbound = NIOAsyncChannelInboundStream + + struct Outbound: ClosableRPCWriterProtocol { + typealias Element = RPCRequestPart + + private let requestWriter: NIOAsyncChannelOutboundWriter + private let http2Stream: NIOAsyncChannel + + fileprivate init( + requestWriter: NIOAsyncChannelOutboundWriter, + http2Stream: NIOAsyncChannel + ) { + self.requestWriter = requestWriter + self.http2Stream = http2Stream + } + + func write(contentsOf elements: some Sequence) async throws { + try await self.requestWriter.write(contentsOf: elements) + } + + func finish() { + self.requestWriter.finish() + } + + func finish(throwing error: any Error) { + // Fire the error inbound; this fails the inbound writer. + self.http2Stream.channel.pipeline.fireErrorCaught(error) + } + } + + let descriptor: MethodDescriptor + + private let http2Stream: NIOAsyncChannel + + init( + wrapping stream: NIOAsyncChannel, + descriptor: MethodDescriptor + ) { + self.http2Stream = stream + self.descriptor = descriptor + } + + func execute( + _ closure: (_ inbound: Inbound, _ outbound: Outbound) async throws -> T + ) async throws -> T where T: Sendable { + try await self.http2Stream.executeThenClose { inbound, outbound in + return try await closure( + inbound, + Outbound(requestWriter: outbound, http2Stream: self.http2Stream) + ) + } + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection { + private enum State { + /// The connection is idle or connecting. + case notConnected + /// A connection has been established with the remote peer. + case connected(Connected) + /// The connection has started to close. This may be initiated locally or by the remote. + case closing + /// The connection has closed. This is a terminal state. + case closed + + struct Connected { + /// The connection channel. + var channel: NIOAsyncChannel + /// Multiplexer for creating HTTP/2 streams. + var multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer + /// Whether the connection is plaintext, `false` implies TLS is being used. + var scheme: Scheme + + init(_ connection: HTTP2Connection) { + self.channel = connection.channel + self.multiplexer = connection.multiplexer + self.scheme = connection.isPlaintext ? .http : .https + } + } + + mutating func connected(_ channel: HTTP2Connection) { + switch self { + case .notConnected: + self = .connected(State.Connected(channel)) + case .connected, .closing, .closed: + fatalError("Invalid state: 'run()' must only be called once") + } + } + + mutating func beginClosing() -> NIOAsyncChannel? { + switch self { + case .notConnected: + fatalError("Invalid state: 'run()' must be called first") + case .connected(let connected): + self = .closing + return connected.channel + case .closing, .closed: + return nil + } + } + + mutating func closing() { + switch self { + case .notConnected: + // Not reachable: happens as a result of a connection event, that can only happen if + // the connection has started (i.e. must be in the 'connected' state or later). + fatalError("Invalid state") + case .connected: + self = .closing + case .closing, .closed: + () + } + } + + mutating func closed() { + self = .closed + } + } +} + +extension ClientConnectionEvent.CloseReason { + fileprivate var precedence: Int { + switch self { + case .goAway: + return 0 + case .idle: + return 1 + case .keepaliveExpired: + return 2 + case .initiatedLocally: + return 3 + } + } +} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift new file mode 100644 index 000000000..1ec651ff3 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift @@ -0,0 +1,48 @@ +/* + * 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 NIOCore +import NIOHTTP2 +import NIOPosix + +@_spi(Package) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol HTTP2Connector: Sendable { + func establishConnection(to address: SocketAddress) async throws -> HTTP2Connection +} + +@_spi(Package) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct HTTP2Connection { + /// The underlying TCP connection wrapped up for use with gRPC. + var channel: NIOAsyncChannel + + /// An HTTP/2 stream multiplexer. + var multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer + + /// Whether the connection is insecure (i.e. plaintext). + var isPlaintext: Bool + + public init( + channel: NIOAsyncChannel, + multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer, + isPlaintext: Bool + ) { + self.channel = channel + self.multiplexer = multiplexer + self.isPlaintext = isPlaintext + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift b/Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..5f1b75dd6 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift @@ -0,0 +1,32 @@ +/* + * 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. + */ + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Sources/GRPCHTTP2Core/Internal/Result+Catching.swift b/Sources/GRPCHTTP2Core/Internal/Result+Catching.swift new file mode 100644 index 000000000..1cd809e42 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/Result+Catching.swift @@ -0,0 +1,30 @@ +/* + * 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. + */ + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Result where Failure == any Error { + /// Like `Result(catching:)`, but `async`. + /// + /// - Parameter body: An `async` closure to catch the result of. + @inlinable + init(catching body: () async throws -> Success) async { + do { + self = .success(try await body()) + } catch { + self = .failure(error) + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index c9d828645..d258b2ace 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -14,13 +14,12 @@ * limitations under the License. */ +@_spi(Package) @testable import GRPCHTTP2Core import NIOCore import NIOEmbedded import NIOHTTP2 import XCTest -@testable import GRPCHTTP2Core - final class ClientConnectionHandlerTests: XCTestCase { func testMaxIdleTime() throws { let connection = try Connection(maxIdleTime: .minutes(5)) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift new file mode 100644 index 000000000..e493881cb --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift @@ -0,0 +1,62 @@ +/* + * 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 + +@testable import GRPCHTTP2Core + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection.Event: Equatable { + public static func == (lhs: Connection.Event, rhs: Connection.Event) -> Bool { + switch (lhs, rhs) { + case (.connectSucceeded, .connectSucceeded), + (.connectFailed, .connectFailed): + return true + + case (.goingAway(let lhsCode, let lhsReason), .goingAway(let rhsCode, let rhsReason)): + return lhsCode == rhsCode && lhsReason == rhsReason + + case (.closed(let lhsReason), .closed(let rhsReason)): + return lhsReason == rhsReason + + default: + return false + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection.CloseReason: Equatable { + public static func == (lhs: Connection.CloseReason, rhs: Connection.CloseReason) -> Bool { + switch (lhs, rhs) { + case (.idleTimeout, .idleTimeout), + (.keepaliveTimeout, .keepaliveTimeout), + (.initiatedLocally, .initiatedLocally), + (.remote, .remote): + return true + + case (.error(let lhsError), .error(let rhsError)): + if let lhs = lhsError as? RPCError, let rhs = rhsError as? RPCError { + return lhs == rhs + } else { + return true + } + + default: + return false + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift new file mode 100644 index 000000000..2be26da0f --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift @@ -0,0 +1,219 @@ +/* + * 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 DequeModule +import GRPCCore +@_spi(Package) @testable import GRPCHTTP2Core +import NIOConcurrencyHelpers +import NIOCore +import NIOHPACK +import NIOHTTP2 +import NIOPosix +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class ConnectionTests: XCTestCase { + func testConnectThenClose() async throws { + try await ConnectionTest.run(connector: .posix()) { context, event in + switch event { + case .connectSucceeded: + context.connection.close() + default: + () + } + } validateEvents: { _, events in + XCTAssertEqual(events, [.connectSucceeded, .closed(.initiatedLocally)]) + } + } + + func testConnectThenIdleTimeout() async throws { + try await ConnectionTest.run(connector: .posix(maxIdleTime: .milliseconds(50))) { _, events in + XCTAssertEqual(events, [.connectSucceeded, .closed(.idleTimeout)]) + } + } + + func testConnectThenKeepaliveTimeout() async throws { + try await ConnectionTest.run( + connector: .posix( + keepaliveTime: .milliseconds(50), + keepaliveTimeout: .milliseconds(10), + keepaliveWithoutCalls: true, + dropPingAcks: true + ) + ) { _, events in + XCTAssertEqual(events, [.connectSucceeded, .closed(.keepaliveTimeout)]) + } + } + + func testGoAwayWhenConnected() async throws { + try await ConnectionTest.run(connector: .posix()) { context, event in + switch event { + case .connectSucceeded: + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway( + lastStreamID: 0, + errorCode: .noError, + opaqueData: ByteBuffer(string: "Hello!") + ) + ) + + let accepted = try context.server.acceptedChannel + accepted.writeAndFlush(goAway, promise: nil) + + default: + () + } + } validateEvents: { _, events in + XCTAssertEqual(events, [.connectSucceeded, .goingAway(.noError, "Hello!"), .closed(.remote)]) + } + } + + func testConnectFails() async throws { + let error = RPCError(code: .unimplemented, message: "") + try await ConnectionTest.run(connector: .throwing(error)) { _, events in + XCTAssertEqual(events, [.connectFailed(error)]) + } + } + + func testMakeStreamOnActiveConnection() async throws { + try await ConnectionTest.run(connector: .posix()) { context, event in + switch event { + case .connectSucceeded: + let stream = try await context.connection.makeStream( + descriptor: .echoGet, + options: .defaults + ) + try await stream.execute { inbound, outbound in + try await outbound.write(.metadata(["foo": "bar", "bar": "baz"])) + try await outbound.write(.message([0, 1, 2])) + outbound.finish() + + var parts = [RPCResponsePart]() + for try await part in inbound { + switch part { + case .metadata(let metadata): + // Filter out any transport specific metadata + parts.append(.metadata(Metadata(metadata.suffix(2)))) + case .message, .status: + parts.append(part) + } + } + + let expected: [RPCResponsePart] = [ + .metadata(["foo": "bar", "bar": "baz"]), + .message([0, 1, 2]), + .status(Status(code: .ok, message: ""), [:]), + ] + XCTAssertEqual(parts, expected) + } + + context.connection.close() + + default: + () + } + } validateEvents: { _, events in + XCTAssertEqual(events, [.connectSucceeded, .closed(.initiatedLocally)]) + } + } + + func testMakeStreamOnClosedConnection() async throws { + try await ConnectionTest.run(connector: .posix()) { context, event in + switch event { + case .connectSucceeded: + context.connection.close() + case .closed: + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + _ = try await context.connection.makeStream(descriptor: .echoGet, options: .defaults) + } errorHandler: { error in + XCTAssertEqual(error.code, .unavailable) + } + default: + () + } + } validateEvents: { context, events in + XCTAssertEqual(events, [.connectSucceeded, .closed(.initiatedLocally)]) + } + } + + func testMakeStreamOnNotRunningConnection() async throws { + let connection = Connection( + address: .ipv4(host: "ignored", port: 0), + http2Connector: .never, + defaultCompression: .none, + enabledCompression: .none + ) + + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + _ = try await connection.makeStream(descriptor: .echoGet, options: .defaults) + } errorHandler: { error in + XCTAssertEqual(error.code, .unavailable) + } + } +} + +extension ClientBootstrap { + func connect( + to address: GRPCHTTP2Core.SocketAddress, + _ configure: @Sendable @escaping (Channel) -> EventLoopFuture + ) async throws -> T { + if let ipv4 = address.ipv4 { + return try await self.connect( + host: ipv4.host, + port: ipv4.port, + channelInitializer: configure + ) + } else if let ipv6 = address.ipv6 { + return try await self.connect( + host: ipv6.host, + port: ipv6.port, + channelInitializer: configure + ) + } else if let uds = address.unixDomainSocket { + return try await self.connect( + unixDomainSocketPath: uds.path, + channelInitializer: configure + ) + } else if let vsock = address.virtualSocket { + return try await self.connect( + to: VsockAddress( + cid: .init(Int(vsock.contextID.rawValue)), + port: .init(Int(vsock.port.rawValue)) + ), + channelInitializer: configure + ) + } else { + throw RPCError(code: .unimplemented, message: "Unhandled socket address: \(address)") + } + } +} + +extension Metadata { + init(_ sequence: some Sequence) { + var metadata = Metadata() + for (key, value) in sequence { + switch value { + case .string(let value): + metadata.addString(value, forKey: key) + case .binary(let value): + metadata.addBinary(value, forKey: key) + } + } + + self = metadata + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift new file mode 100644 index 000000000..5e2b3c520 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -0,0 +1,202 @@ +/* + * 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 DequeModule +import GRPCCore +@_spi(Package) @testable import GRPCHTTP2Core +import NIOCore +import NIOHTTP2 +import NIOPosix + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +enum ConnectionTest { + struct Context { + var server: Server + var connection: Connection + } + + static func run( + connector: HTTP2Connector, + handlEvents: ( + _ context: Context, + _ event: Connection.Event + ) async throws -> Void = { _, _ in }, + validateEvents: (_ context: Context, _ events: [Connection.Event]) -> Void + ) async throws { + let server = Server() + let address = try await server.bind() + + try await withThrowingTaskGroup(of: Void.self) { group in + let connection = Connection( + address: address, + http2Connector: connector, + defaultCompression: .none, + enabledCompression: .none + ) + let context = Context(server: server, connection: connection) + group.addTask { await connection.run() } + + var events: [Connection.Event] = [] + for await event in connection.events { + events.append(event) + try await handlEvents(context, event) + } + + validateEvents(context, events) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension ConnectionTest { + /// A server which only expected to accept a single connection. + final class Server { + private let eventLoop: any EventLoop + private var listener: (any Channel)? + private let client: EventLoopPromise + + init() { + self.eventLoop = .singletonMultiThreadedEventLoopGroup.next() + self.client = self.eventLoop.next().makePromise() + } + + deinit { + self.listener?.close(promise: nil) + self.client.futureResult.whenSuccess { $0.close(mode: .all, promise: nil) } + } + + var acceptedChannel: Channel { + get throws { + try self.client.futureResult.wait() + } + } + + func bind() async throws -> GRPCHTTP2Core.SocketAddress { + precondition(self.listener == nil, "\(#function) must only be called once") + + let hasAcceptedChannel = try await self.eventLoop.submit { + NIOLoopBoundBox(false, eventLoop: self.eventLoop) + }.get() + + let bootstrap = ServerBootstrap(group: self.eventLoop).childChannelInitializer { channel in + precondition(!hasAcceptedChannel.value, "already accepted a channel") + hasAcceptedChannel.value = true + + return channel.eventLoop.makeCompletedFuture { + let sync = channel.pipeline.syncOperations + let h2 = NIOHTTP2Handler(mode: .server) + let mux = HTTP2StreamMultiplexer(mode: .server, channel: channel) { stream in + let sync = stream.pipeline.syncOperations + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: .none, + maximumPayloadSize: .max + ) + + return stream.eventLoop.makeCompletedFuture { + try sync.addHandler(handler) + try sync.addHandler(EchoHandler()) + } + } + + try sync.addHandler(h2) + try sync.addHandler(mux) + try sync.addHandlers(SucceedOnSettingsAck(promise: self.client)) + } + } + + let channel = try await bootstrap.bind(host: "127.0.0.1", port: 0).get() + self.listener = channel + return .ipv4(host: "127.0.0.1", port: channel.localAddress!.port!) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension ConnectionTest { + /// Succeeds a promise when a SETTINGS frame ack has been read. + private final class SucceedOnSettingsAck: ChannelInboundHandler { + typealias InboundIn = HTTP2Frame + typealias InboundOut = HTTP2Frame + + private let promise: EventLoopPromise + + init(promise: EventLoopPromise) { + self.promise = promise + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let frame = self.unwrapInboundIn(data) + switch frame.payload { + case .settings(.ack): + self.promise.succeed(context.channel) + default: + () + } + + context.fireChannelRead(data) + } + } + + private final class EchoHandler: ChannelInboundHandler { + typealias InboundIn = RPCRequestPart + typealias OutboundOut = RPCResponsePart + + private var received: Deque = [] + private var receivedEnd = false + + func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { + if let event = event as? ChannelEvent, event == .inputClosed { + self.receivedEnd = true + } + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + self.received.append(self.unwrapInboundIn(data)) + } + + func channelReadComplete(context: ChannelHandlerContext) { + while let part = self.received.popFirst() { + switch part { + case .metadata(let metadata): + var filtered = Metadata() + + // Remove any pseudo-headers. + for (key, value) in metadata where !key.hasPrefix(":") { + switch value { + case .string(let value): + filtered.addString(value, forKey: key) + case .binary(let value): + filtered.addBinary(value, forKey: key) + } + } + + context.write(self.wrapOutboundOut(.metadata(filtered)), promise: nil) + + case .message(let message): + context.write(self.wrapOutboundOut(.message(message)), promise: nil) + } + } + + if self.receivedEnd { + let status = Status(code: .ok, message: "") + context.write(self.wrapOutboundOut(.status(status, [:])), promise: nil) + } + + context.flush() + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift new file mode 100644 index 000000000..0463ac157 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift @@ -0,0 +1,155 @@ +/* + * 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 +@_spi(Package) @testable import GRPCHTTP2Core +import NIOCore +import NIOHTTP2 +import NIOPosix + +@_spi(Package) +extension HTTP2Connector where Self == ThrowingConnector { + /// A connector which throws the given error on a connect attempt. + static func throwing(_ error: RPCError) -> Self { + return ThrowingConnector(error: error) + } +} + +@_spi(Package) +extension HTTP2Connector where Self == NeverConnector { + /// A connector which fatal errors if a connect attempt is made. + static var never: Self { + NeverConnector() + } +} + +@_spi(Package) +extension HTTP2Connector where Self == NIOPosixConnector { + /// A connector which uses NIOPosix to establish a connection. + static func posix( + maxIdleTime: TimeAmount? = nil, + keepaliveTime: TimeAmount? = nil, + keepaliveTimeout: TimeAmount? = nil, + keepaliveWithoutCalls: Bool = false, + dropPingAcks: Bool = false + ) -> Self { + return NIOPosixConnector( + maxIdleTime: maxIdleTime, + keepaliveTime: keepaliveTime, + keepaliveTimeout: keepaliveTimeout, + keepaliveWithoutCalls: keepaliveWithoutCalls, + dropPingAcks: dropPingAcks + ) + } +} + +struct ThrowingConnector: HTTP2Connector { + private let error: RPCError + + init(error: RPCError) { + self.error = error + } + + func establishConnection( + to address: GRPCHTTP2Core.SocketAddress + ) async throws -> HTTP2Connection { + throw self.error + } +} + +struct NeverConnector: HTTP2Connector { + func establishConnection( + to address: GRPCHTTP2Core.SocketAddress + ) async throws -> HTTP2Connection { + fatalError("\(#function) called unexpectedly") + } +} + +struct NIOPosixConnector: HTTP2Connector { + private let eventLoopGroup: any EventLoopGroup + private let maxIdleTime: TimeAmount? + private let keepaliveTime: TimeAmount? + private let keepaliveTimeout: TimeAmount? + private let keepaliveWithoutCalls: Bool + private let dropPingAcks: Bool + + init( + eventLoopGroup: (any EventLoopGroup)? = nil, + maxIdleTime: TimeAmount? = nil, + keepaliveTime: TimeAmount? = nil, + keepaliveTimeout: TimeAmount? = nil, + keepaliveWithoutCalls: Bool = false, + dropPingAcks: Bool = false + ) { + self.eventLoopGroup = eventLoopGroup ?? .singletonMultiThreadedEventLoopGroup + self.maxIdleTime = maxIdleTime + self.keepaliveTime = keepaliveTime + self.keepaliveTimeout = keepaliveTimeout + self.keepaliveWithoutCalls = keepaliveWithoutCalls + self.dropPingAcks = dropPingAcks + } + + func establishConnection( + to address: GRPCHTTP2Core.SocketAddress + ) async throws -> HTTP2Connection { + return try await ClientBootstrap(group: self.eventLoopGroup).connect(to: address) { channel in + channel.eventLoop.makeCompletedFuture { + let sync = channel.pipeline.syncOperations + + let multiplexer = try sync.configureAsyncHTTP2Pipeline(mode: .client) { stream in + // Server shouldn't be opening streams. + stream.close() + } + + if self.dropPingAcks { + try sync.addHandler(PingAckDropper()) + } + + let connectionHandler = ClientConnectionHandler( + eventLoop: channel.eventLoop, + maxIdleTime: self.maxIdleTime, + keepaliveTime: self.keepaliveTime, + keepaliveTimeout: self.keepaliveTimeout, + keepaliveWithoutCalls: self.keepaliveWithoutCalls + ) + + try sync.addHandler(connectionHandler) + + let asyncChannel = try NIOAsyncChannel( + wrappingChannelSynchronously: channel + ) + + return HTTP2Connection(channel: asyncChannel, multiplexer: multiplexer, isPlaintext: true) + } + } + } + + /// Drops all acks for PING frames. This is useful to help trigger the keepalive timeout. + final class PingAckDropper: ChannelInboundHandler { + typealias InboundIn = HTTP2Frame + typealias InboundOut = HTTP2Frame + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let frame = self.unwrapInboundIn(data) + switch frame.payload { + case .ping(_, ack: true): + () // drop-it + default: + context.fireChannelRead(data) + } + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift new file mode 100644 index 000000000..a9a31c688 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift @@ -0,0 +1,23 @@ +/* + * 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 + +extension MethodDescriptor { + static var echoGet: Self { + MethodDescriptor(service: "echo.Echo", method: "Get") + } +} From 6a90b7e77e29f9bda6c2b3a4165a40d6c02cfda1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 22 Apr 2024 13:38:25 +0100 Subject: [PATCH 296/580] Bump version number to 1.23.0 (#1863) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.23.0 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index ef41a7b8d..1808c5bf9 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -19,7 +19,7 @@ internal enum Version { internal static let major = 1 /// The minor version. - internal static let minor = 22 + internal static let minor = 23 /// The patch version. internal static let patch = 0 From 2d56a49303eae16889a5707e7369eba727701ddd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 22 Apr 2024 17:41:47 +0100 Subject: [PATCH 297/580] Delay the point at which a connect 'succeeds' (#1864) Motivation: One issue with the connection merged in #1859 is that it considers connect succeeded to be the point at which the TCP connection succeeds. However, when considered as an HTTP/2 connection, this is too early: the connection may fail a TLS handshake or the server might not be an HTTP/2 server. The connect should only be considered a success when the initial SETTINGS frame is received from the server. Modifications: - Add a new connection event 'ready' and react to this appropriately in the connection - Add tests Result: The `Connection` is only considered ready when the first SETTINGS frame is received. --- .../Connection/ClientConnectionHandler.swift | 36 +++++++++ .../Client/Connection/Connection.swift | 81 ++++++++++++++----- .../ClientConnectionHandlerTests.swift | 21 +++++ .../Client/Connection/ConnectionTests.swift | 15 ++++ .../Connection/Utilities/ConnectionTest.swift | 58 ++++++++----- .../Test Utilities/XCTest+Utilities.swift | 8 ++ 6 files changed, 176 insertions(+), 43 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 44450717b..36f9ef683 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -31,6 +31,9 @@ public enum ClientConnectionEvent: Sendable, Hashable { case initiatedLocally } + /// The connection is now ready. + case ready + /// The connection has started shutting down, no new streams should be created. case closing(CloseReason) } @@ -208,6 +211,16 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } } + case .settings(.settings(_)): + let isInitialSettings = self.state.receivedSettings() + + // The first settings frame indicates that the connection is now ready to use. The channel + // becoming active is insufficient as, for example, a TLS handshake may fail after + // establishing the TCP connection, or the server isn't configured for gRPC (or HTTP/2). + if isInitialSettings { + context.fireChannelRead(self.wrapInboundOut(.ready)) + } + default: () } @@ -323,10 +336,18 @@ extension ClientConnectionHandler { struct Active { var openStreams: Set var allowKeepaliveWithoutCalls: Bool + var receivedConnectionPreface: Bool init(allowKeepaliveWithoutCalls: Bool) { self.openStreams = [] self.allowKeepaliveWithoutCalls = allowKeepaliveWithoutCalls + self.receivedConnectionPreface = false + } + + mutating func receivedSettings() -> Bool { + let isFirstSettingsFrame = !self.receivedConnectionPreface + self.receivedConnectionPreface = true + return isFirstSettingsFrame } } @@ -347,6 +368,21 @@ extension ClientConnectionHandler { self.state = .active(State.Active(allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls)) } + /// Record that a SETTINGS frame was received from the remote peer. + /// + /// - Returns: `true` if this was the first SETTINGS frame received. + mutating func receivedSettings() -> Bool { + switch self.state { + case .active(var active): + let isFirstSettingsFrame = active.receivedSettings() + self.state = .active(active) + return isFirstSettingsFrame + + case .closing, .closed: + return false + } + } + /// Record that the stream with the given ID has been opened. mutating func streamOpened(_ id: HTTP2StreamID) { switch self.state { diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index d9cbe0fd7..ae566eeb8 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -139,8 +139,6 @@ struct Connection: Sendable { state.connected(connected) } - self.event.continuation.yield(.connectSucceeded) - await withDiscardingTaskGroup { group in // Add a task to run the connection and consume events. group.addTask { @@ -229,11 +227,34 @@ struct Connection: Sendable { private func consumeConnectionEvents( _ connectionEvents: NIOAsyncChannelInboundStream ) async { + // The connection becomes 'ready' when the initial HTTP/2 SETTINGS frame is received. + // Establishing a TCP connection is insufficient as the TLS handshake may not complete or the + // server might not be configured for gRPC or HTTP/2. + // + // This state is tracked here so that if the connection events sequence finishes and the + // connection never became ready then the connection can report that the connect failed. + var isReady = false + + func makeNeverReadyError(cause: (any Error)?) -> RPCError { + return RPCError( + code: .unavailable, + message: """ + The server accepted the TCP connection but closed the connection before completing \ + the HTTP/2 connection preface. + """, + cause: cause + ) + } + do { var channelCloseReason: ClientConnectionEvent.CloseReason? for try await connectionEvent in connectionEvents { switch connectionEvent { + case .ready: + isReady = true + self.event.continuation.yield(.connectSucceeded) + case .closing(let reason): self.state.withLockedValue { $0.closing() } @@ -256,33 +277,50 @@ struct Connection: Sendable { } } - let connectionCloseReason: Self.CloseReason - switch channelCloseReason { - case .keepaliveExpired: - connectionCloseReason = .keepaliveTimeout + let finalEvent: Event + if isReady { + let connectionCloseReason: Self.CloseReason + switch channelCloseReason { + case .keepaliveExpired: + connectionCloseReason = .keepaliveTimeout - case .idle: - // Connection became idle, that's fine. - connectionCloseReason = .idleTimeout + case .idle: + // Connection became idle, that's fine. + connectionCloseReason = .idleTimeout - case .goAway: - // Remote peer told us to GOAWAY. - connectionCloseReason = .remote + case .goAway: + // Remote peer told us to GOAWAY. + connectionCloseReason = .remote - case .initiatedLocally, .none: - // Shutdown was initiated locally. - connectionCloseReason = .initiatedLocally + case .initiatedLocally, .none: + // Shutdown was initiated locally. + connectionCloseReason = .initiatedLocally + } + + finalEvent = .closed(connectionCloseReason) + } else { + // The connection never became ready, this therefore counts as a failed connect attempt. + finalEvent = .connectFailed(makeNeverReadyError(cause: nil)) } // The connection events sequence has finished: the connection is now closed. self.state.withLockedValue { $0.closed() } - self.finishStreams(withEvent: .closed(connectionCloseReason)) + self.finishStreams(withEvent: finalEvent) } catch { - // Any error must come from consuming the inbound channel meaning that the connection - // must be borked, wrap it up and close. - let rpcError = RPCError(code: .unavailable, message: "connection closed", cause: error) + let finalEvent: Event + + if isReady { + // Any error must come from consuming the inbound channel meaning that the connection + // must be borked, wrap it up and close. + let rpcError = RPCError(code: .unavailable, message: "connection closed", cause: error) + finalEvent = .closed(.error(rpcError)) + } else { + // The connection never became ready, this therefore counts as a failed connect attempt. + finalEvent = .connectFailed(makeNeverReadyError(cause: error)) + } + self.state.withLockedValue { $0.closed() } - self.finishStreams(withEvent: .closed(.error(rpcError))) + self.finishStreams(withEvent: finalEvent) } } @@ -356,7 +394,8 @@ extension Connection { private enum State { /// The connection is idle or connecting. case notConnected - /// A connection has been established with the remote peer. + /// A TCP connection has been established with the remote peer. However, the connection may not + /// be ready to use yet. case connected(Connected) /// The connection has started to close. This may be initiated locally or by the remote. case closing diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index d258b2ace..9aac5098c 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -205,6 +205,22 @@ final class ClientConnectionHandlerTests: XCTestCase { connection.streamClosed(1) try closed.wait() } + + func testReceiveInitialSettings() throws { + let connection = try Connection() + try connection.activate() + + // Nothing yet. + XCTAssertNil(try connection.readEvent()) + + // Write the initial settings. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + + // Receiving another settings frame should be a no-op. + try connection.settings([]) + XCTAssertNil(try connection.readEvent()) + } } extension ClientConnectionHandlerTests { @@ -268,6 +284,11 @@ extension ClientConnectionHandlerTests { try self.channel.writeInbound(frame) } + func settings(_ settings: [HTTP2Setting]) throws { + let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) + try self.channel.writeInbound(frame) + } + func readFrame() throws -> HTTP2Frame? { return try self.channel.readOutbound(as: HTTP2Frame.self) } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift index 2be26da0f..c9cb69100 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift @@ -89,6 +89,21 @@ final class ConnectionTests: XCTestCase { } } + func testConnectFailsOnAcceptedThenClosedTCPConnection() async throws { + try await ConnectionTest.run(connector: .posix(), server: .closeOnAccept) { _, events in + XCTAssertEqual(events.count, 1) + let event = try XCTUnwrap(events.first) + switch event { + case .connectFailed(let error): + XCTAssert(error, as: RPCError.self) { rpcError in + XCTAssertEqual(rpcError.code, .unavailable) + } + default: + XCTFail("Expected '.connectFailed', got '\(event)'") + } + } + } + func testMakeStreamOnActiveConnection() async throws { try await ConnectionTest.run(connector: .posix()) { context, event in switch event { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift index 5e2b3c520..d0f7771ac 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -30,13 +30,14 @@ enum ConnectionTest { static func run( connector: HTTP2Connector, + server mode: Server.Mode = .regular, handlEvents: ( _ context: Context, _ event: Connection.Event ) async throws -> Void = { _, _ in }, - validateEvents: (_ context: Context, _ events: [Connection.Event]) -> Void + validateEvents: (_ context: Context, _ events: [Connection.Event]) throws -> Void ) async throws { - let server = Server() + let server = Server(mode: mode) let address = try await server.bind() try await withThrowingTaskGroup(of: Void.self) { group in @@ -55,7 +56,7 @@ enum ConnectionTest { try await handlEvents(context, event) } - validateEvents(context, events) + try validateEvents(context, events) } } } @@ -67,8 +68,15 @@ extension ConnectionTest { private let eventLoop: any EventLoop private var listener: (any Channel)? private let client: EventLoopPromise + private let mode: Mode - init() { + enum Mode { + case regular + case closeOnAccept + } + + init(mode: Mode) { + self.mode = mode self.eventLoop = .singletonMultiThreadedEventLoopGroup.next() self.client = self.eventLoop.next().makePromise() } @@ -95,26 +103,32 @@ extension ConnectionTest { precondition(!hasAcceptedChannel.value, "already accepted a channel") hasAcceptedChannel.value = true - return channel.eventLoop.makeCompletedFuture { - let sync = channel.pipeline.syncOperations - let h2 = NIOHTTP2Handler(mode: .server) - let mux = HTTP2StreamMultiplexer(mode: .server, channel: channel) { stream in - let sync = stream.pipeline.syncOperations - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: .none, - maximumPayloadSize: .max - ) - - return stream.eventLoop.makeCompletedFuture { - try sync.addHandler(handler) - try sync.addHandler(EchoHandler()) + switch self.mode { + case .closeOnAccept: + return channel.close() + + case .regular: + return channel.eventLoop.makeCompletedFuture { + let sync = channel.pipeline.syncOperations + let h2 = NIOHTTP2Handler(mode: .server) + let mux = HTTP2StreamMultiplexer(mode: .server, channel: channel) { stream in + let sync = stream.pipeline.syncOperations + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: .none, + maximumPayloadSize: .max + ) + + return stream.eventLoop.makeCompletedFuture { + try sync.addHandler(handler) + try sync.addHandler(EchoHandler()) + } } - } - try sync.addHandler(h2) - try sync.addHandler(mux) - try sync.addHandlers(SucceedOnSettingsAck(promise: self.client)) + try sync.addHandler(h2) + try sync.addHandler(mux) + try sync.addHandlers(SucceedOnSettingsAck(promise: self.client)) + } } } diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift index 0179f8bcf..6103f1b27 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift @@ -59,3 +59,11 @@ func XCTAssertThrowsErrorAsync( XCTFail("Error had unexpected type '\(type(of: error))'") } } + +func XCTAssert(_ value: Any, as type: T.Type, _ verify: (T) throws -> Void) rethrows { + if let value = value as? T { + try verify(value) + } else { + XCTFail("\(value) couldn't be cast to \(T.self)") + } +} From 63af6098abb47b88fc7362c7f362221a8cef7c51 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 23 Apr 2024 10:09:55 +0100 Subject: [PATCH 298/580] Add missing availbility guards in tests (#1866) Motivation: Some test helpers were missing availbility guards. Also, some tests used integer literal which would overlflow an `Int` when it's only 32-bits. Modifications: - Add missing availbility guards - Use Int64 explicitly to avoid overlflow Result: Tests build on other platforms --- .../GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift | 4 ++-- Tests/GRPCCoreTests/TimeoutTests.swift | 4 ++-- .../Client/Connection/ConnectionTests.swift | 1 + .../Client/Connection/Utilities/HTTP2Connectors.swift | 6 ++++++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift index 4ecc2fc65..2a6930a68 100644 --- a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -66,7 +66,7 @@ final class RetryDelaySequenceTests: XCTestCase { (.seconds(1), 1.0), (.milliseconds(1500), 1.5), (.nanoseconds(1_000_000_000), 1.0), - (.nanoseconds(3_141_592_653), 3.141592653), + (.nanoseconds(3_141_592_653 as Int64), 3.141592653), ] for (duration, expected) in testData { @@ -80,7 +80,7 @@ final class RetryDelaySequenceTests: XCTestCase { (1.0, .seconds(1)), (1.5, .milliseconds(1500)), (1.0, .nanoseconds(1_000_000_000)), - (3.141592653, .nanoseconds(3_141_592_653)), + (3.141592653, .nanoseconds(3_141_592_653 as Int64)), ] for (seconds, expected) in testData { diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index aeb98d154..80be5ea1f 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -127,7 +127,7 @@ final class TimeoutTests: XCTestCase { } func testEncodeValidTimeout_Seconds_TooLong_Hours() { - let duration = Duration.seconds(9_999_999_999) + let duration = Duration.seconds(9_999_999_999 as Int64) let timeout = Timeout(duration: duration) // The conversion from seconds to hours results in a loss of precision. // 9,999,999,999 seconds / 60 = 166,666,666.65 minutes -rounding up-> @@ -142,7 +142,7 @@ final class TimeoutTests: XCTestCase { } func testEncodeValidTimeout_Seconds_TooLong_MaxAmount() { - let duration = Duration.seconds(999_999_999_999) + let duration = Duration.seconds(999_999_999_999 as Int64) let timeout = Timeout(duration: duration) // The conversion from seconds to hours results in a number that still has // more than the maximum allowed 8 digits, so we must clamp it. diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift index c9cb69100..5cddc63d1 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift @@ -182,6 +182,7 @@ final class ConnectionTests: XCTestCase { } extension ClientBootstrap { + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func connect( to address: GRPCHTTP2Core.SocketAddress, _ configure: @Sendable @escaping (Channel) -> EventLoopFuture diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift index 0463ac157..0357fd428 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift @@ -21,6 +21,7 @@ import NIOHTTP2 import NIOPosix @_spi(Package) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension HTTP2Connector where Self == ThrowingConnector { /// A connector which throws the given error on a connect attempt. static func throwing(_ error: RPCError) -> Self { @@ -29,6 +30,7 @@ extension HTTP2Connector where Self == ThrowingConnector { } @_spi(Package) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension HTTP2Connector where Self == NeverConnector { /// A connector which fatal errors if a connect attempt is made. static var never: Self { @@ -37,6 +39,7 @@ extension HTTP2Connector where Self == NeverConnector { } @_spi(Package) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension HTTP2Connector where Self == NIOPosixConnector { /// A connector which uses NIOPosix to establish a connection. static func posix( @@ -56,6 +59,7 @@ extension HTTP2Connector where Self == NIOPosixConnector { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ThrowingConnector: HTTP2Connector { private let error: RPCError @@ -70,6 +74,7 @@ struct ThrowingConnector: HTTP2Connector { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct NeverConnector: HTTP2Connector { func establishConnection( to address: GRPCHTTP2Core.SocketAddress @@ -78,6 +83,7 @@ struct NeverConnector: HTTP2Connector { } } +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct NIOPosixConnector: HTTP2Connector { private let eventLoopGroup: any EventLoopGroup private let maxIdleTime: TimeAmount? From 9c782201f7a473fb9d486ad6919a9684537711b6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 23 Apr 2024 10:53:28 +0100 Subject: [PATCH 299/580] A few helpers to pave the way for subchannel (#1867) Motivation: To keep the subchannel PR smaller, a few related but standalone bits can be added separately. Modifications: - Bump the NIO version to pickup a NIOAsyncChannel fix and a new cascade function - Add the `ConnectivityState` enum - Add `ProcessUniqueID` and `SubchannelID` Result: Subchannel PR will be smaller. --- Package.swift | 9 ++-- .../Connection/ClientConnectionHandler.swift | 16 ------- .../Client/Connection/ConnectivityState.swift | 45 +++++++++++++++++++ .../Internal/ProcessUniqueID.swift | 39 ++++++++++++++++ .../Internal/ProcessUniqueIDTests.swift | 45 +++++++++++++++++++ 5 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift diff --git a/Package.swift b/Package.swift index 699ff4baa..4b10151a0 100644 --- a/Package.swift +++ b/Package.swift @@ -32,7 +32,7 @@ let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == ni let packageDependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-nio.git", - from: "2.64.0" + from: "2.65.0" ), .package( url: "https://github.com/apple/swift-nio-http2.git", @@ -208,7 +208,8 @@ extension Target { .nioCore, .nioHTTP2, .cgrpcZlib, - .dequeModule + .dequeModule, + .atomics ] ) @@ -256,7 +257,7 @@ extension Target { .nioFileSystem ] ) - + static let grpcSwiftPlugin: Target = .plugin( name: "GRPCSwiftPlugin", capability: .buildTool(), @@ -381,7 +382,7 @@ extension Target { .grpcCore ] ) - + static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 36f9ef683..00a254bc3 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -513,19 +513,3 @@ extension ClientConnectionHandler { } } } - -extension Optional { - // TODO: replace with https://github.com/apple/swift-nio/pull/2697 - mutating func setOrCascade( - to promise: EventLoopPromise? - ) where Wrapped == EventLoopPromise { - guard let promise = promise else { return } - - switch self { - case .none: - self = .some(promise) - case .some(let existing): - existing.futureResult.cascade(to: promise) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift new file mode 100644 index 000000000..da471c4c1 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift @@ -0,0 +1,45 @@ +/* + * 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. + */ + +enum ConnectivityState: Sendable, Hashable { + /// This channel isn't trying to create a connection because of a lack of new or pending RPCs. + /// + /// New streams may be created in this state. Doing so will cause the channel to enter the + /// connecting state. + case idle + + /// The channel is trying to establish a connection and is waiting to make progress on one of the + /// steps involved in name resolution, TCP connection establishment or TLS handshake. + case connecting + + /// The channel has successfully established a connection all the way through TLS handshake (or + /// equivalent) and protocol-level (HTTP/2, etc) handshaking. + case ready + + /// There has been some transient failure (such as a TCP 3-way handshake timing out or a socket + /// error). Channels in this state will eventually switch to the ``connecting`` state and try to + /// establish a connection again. Since retries are done with exponential backoff, channels that + /// fail to connect will start out spending very little time in this state but as the attempts + /// fail repeatedly, the channel will spend increasingly large amounts of time in this state. + case transientFailure + + /// This channel has started shutting down. Any new RPCs should fail immediately. Pending RPCs + /// may continue running until the application cancels them. Channels may enter this state either + /// because the application explicitly requested a shutdown or if a non-recoverable error has + /// happened during attempts to connect. Channels that have entered this state will never leave + /// this state. + case shutdown +} diff --git a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift new file mode 100644 index 000000000..bba747c44 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift @@ -0,0 +1,39 @@ +/* + * 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 Atomics + +/// An ID which is unique within this process. +struct ProcessUniqueID: Hashable, Sendable, CustomStringConvertible { + private static let source = ManagedAtomic(UInt64(0)) + private let rawValue: UInt64 + + init() { + self.rawValue = Self.source.loadThenWrappingIncrement(ordering: .relaxed) + } + + var description: String { + String(describing: self.rawValue) + } +} + +/// A process-unique ID for a subchannel. +struct SubchannelID: Hashable, Sendable, CustomStringConvertible { + private let id = ProcessUniqueID() + var description: String { + "subchan_\(self.id)" + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift new file mode 100644 index 000000000..4d4004026 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift @@ -0,0 +1,45 @@ +/* + * 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 XCTest + +@testable import GRPCHTTP2Core + +final class ProcessUniqueIDTests: XCTestCase { + func testProcessUniqueIDIsUnique() { + var ids: Set = [] + for _ in 1 ... 100 { + let (inserted, _) = ids.insert(ProcessUniqueID()) + XCTAssertTrue(inserted) + } + + XCTAssertEqual(ids.count, 100) + } + + func testProcessUniqueIDDescription() { + let id = ProcessUniqueID() + let description = String(describing: id) + // We can't verify the exact description as we don't know what value to expect, we only + // know that it'll be an integer. + XCTAssertNotNil(UInt64(description)) + } + + func testSubchannelIDDescription() { + let id = SubchannelID() + let description = String(describing: id) + XCTAssert(description.hasPrefix("subchan_")) + } +} From 8bc72fd0c01e626143b6d763f4ceb5a98cfa3ad5 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:22:06 +0100 Subject: [PATCH 300/580] Add `removeAll(where:)` to `Metadata`. (#1869) Motivation: Sometimes users want to remove some values from `Metadata`. At the moment they can only remove one at a time or all values. Modifications: Add `removeAll(where:)` to `Metadata`. Result: Users can now remove values from `Metadata` that match a given predicate. --- Sources/GRPCCore/Metadata.swift | 13 +++++++++++++ Tests/GRPCCoreTests/MetadataTests.swift | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 217c8a784..2d9e64349 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -226,6 +226,19 @@ public struct Metadata: Sendable, Hashable { public mutating func removeAll(keepingCapacity: Bool) { self.elements.removeAll(keepingCapacity: keepingCapacity) } + + /// Removes all elements which match the given predicate. + /// + /// - Parameter predicate: Returns `true` if the element should be removed. + /// + /// - Complexity: O(*n*), where *n* is the number of entries in the metadata instance. + public mutating func removeAll( + where predicate: (_ key: String, _ value: Value) throws -> Bool + ) rethrows { + try self.elements.removeAll { pair in + try predicate(pair.key, pair.value) + } + } } extension Metadata: RandomAccessCollection { diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index fca7125b0..9170255ad 100644 --- a/Tests/GRPCCoreTests/MetadataTests.swift +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -205,4 +205,26 @@ final class MetadataTests: XCTestCase { XCTAssertEqual(stringIterator.next(), "value2") XCTAssertNil(stringIterator.next()) } + + func testRemoveAllWhere() { + let metadata: Metadata = [ + "testKey1": "value1", + "testKey2": "value2", + "testKey3": "value1", + ] + + var metadata1 = metadata + metadata1.removeAll { _, value in + value == "value1" + } + + XCTAssertEqual(metadata1, ["testKey2": "value2"]) + + var metadata2 = metadata + metadata2.removeAll { key, _ in + key == "testKey2" + } + + XCTAssertEqual(metadata2, ["testKey1": "value1", "testKey3": "value1"]) + } } From 5dc50ed70306671842275530f0289778860beb0d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 29 Apr 2024 13:52:26 +0100 Subject: [PATCH 301/580] Remove lazy connect parameter from client transport (#1870) Motivation: The client transport has a parameter to enabled lazy connectivity. If set the transport shouldn't create a connection until a stream is created. However, this isn't applicable for all transports, instead it should be configured on concrete transport types. Modifications: - Remove 'lazily' parameter Result: Better API --- Sources/GRPCCore/GRPCClient.swift | 2 +- .../GRPCCore/Transport/ClientTransport.swift | 10 ++-------- .../InProcessClientTransport.swift | 16 +++++++-------- .../ClientRPCExecutorTestHarness.swift | 2 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 6 +++--- .../Transport/AnyTransport.swift | 10 +++++----- .../Transport/StreamCountingTransport.swift | 4 ++-- .../Transport/ThrowingTransport.swift | 2 +- .../InProcessClientTransportTests.swift | 20 +++++++++---------- 9 files changed, 32 insertions(+), 40 deletions(-) diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 2aa701817..00f49d5ab 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -195,7 +195,7 @@ public struct GRPCClient: Sendable { } do { - try await self.transport.connect(lazily: false) + try await self.transport.connect() } catch { throw RuntimeError( code: .transportError, diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index b4ef40de7..b730dd10c 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -36,13 +36,7 @@ public protocol ClientTransport: Sendable { /// maintains connections. The function exits when all open streams have been closed and new connections /// are no longer required by the caller who signals this by calling ``close()``, or by cancelling the /// task this function runs in. - /// - /// - Parameter lazily: Whether the transport should establish connections lazily, that is, - /// when the first stream is opened or eagerly, when this function is called. If `false` - /// then the transport should attempt to establish a connection immediately. Note that - /// this is a _hint_: transports aren't required to respect this value and you should - /// refer to the documentation of the transport you're using to check whether it's supported. - func connect(lazily: Bool) async throws + func connect() async throws /// Signal to the transport that no new streams may be created. /// @@ -50,7 +44,7 @@ public protocol ClientTransport: Sendable { /// should result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. /// /// If you want to forcefully cancel all active streams then cancel the task - /// running ``connect(lazily:)``. + /// running ``connect()``. func close() /// Opens a stream using the transport, and uses it as input into a user-provided closure. diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index af911c659..b513541f9 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -26,13 +26,13 @@ import GRPCCore /// ``ClientRPCExecutionConfiguration``s which are specific, per-method configurations for your /// transport. /// -/// Once you have a client, you must keep a long-running task executing ``connect(lazily:)``, which +/// Once you have a client, you must keep a long-running task executing ``connect()``, which /// will return only once all streams have been finished and ``close()`` has been called on this client; or /// when the containing task is cancelled. /// /// To execute requests using this client, use ``withStream(descriptor:_:)``. If this function is -/// called before ``connect(lazily:)`` is called, then any streams will remain pending and the call will -/// block until ``connect(lazily:)`` is called or the task is cancelled. +/// called before ``connect()`` is called, then any streams will remain pending and the call will +/// block until ``connect()`` is called or the task is cancelled. /// /// - SeeAlso: ``ClientTransport`` @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -127,9 +127,7 @@ public struct InProcessClientTransport: ClientTransport { /// maintains connections. The function exits when all open streams have been closed and new connections /// are no longer required by the caller who signals this by calling ``close()``, or by cancelling the /// task this function runs in. - /// - /// - Parameter lazily: This parameter is ignored in this implementation. - public func connect(lazily: Bool) async throws { + public func connect() async throws { let (stream, continuation) = AsyncStream.makeStream() try self.state.withLockedValue { state in switch state { @@ -157,7 +155,7 @@ public struct InProcessClientTransport: ClientTransport { } for await _ in stream { - // This for-await loop will exit (and thus `connect(lazily:)` will return) + // This for-await loop will exit (and thus `connect()` will return) // only when the task is cancelled, or when the stream's continuation is // finished - whichever happens first. // The continuation will be finished when `close()` is called and there @@ -190,7 +188,7 @@ public struct InProcessClientTransport: ClientTransport { /// Existing streams may run to completion naturally but calling ``withStream(descriptor:_:)`` /// will result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. /// - /// If you want to forcefully cancel all active streams then cancel the task running ``connect(lazily:)``. + /// If you want to forcefully cancel all active streams then cancel the task running ``connect()``. public func close() { let maybeContinuation: AsyncStream.Continuation? = self.state.withLockedValue { state in switch state { @@ -220,7 +218,7 @@ public struct InProcessClientTransport: ClientTransport { /// is closing or has been closed. /// /// This implementation will queue any streams (and thus block this call) if this function is called before - /// ``connect(lazily:)``, until a connection is established - at which point all streams will be + /// ``connect()``, until a connection is established - at which point all streams will be /// created. /// /// - Parameters: diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 5df62c007..48026730a 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -132,7 +132,7 @@ struct ClientRPCExecutorTestHarness { } group.addTask { - try await self.clientTransport.connect(lazily: false) + try await self.clientTransport.connect() } // Execute the request. diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 9acfc4f21..02028a1e8 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -38,7 +38,7 @@ final class GRPCServerTests: XCTestCase { } group.addTask { - try await inProcess.client.connect(lazily: true) + try await inProcess.client.connect() } try await body(inProcess.client, server) @@ -325,7 +325,7 @@ final class GRPCServerTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try? await inProcess.client.connect(lazily: true) + try? await inProcess.client.connect() } try await self.doEchoGet(using: inProcess.client) @@ -388,7 +388,7 @@ final class GRPCServerTests: XCTestCase { // other transport to throw. This stream should be failed by the server. await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await inProcess.client.connect(lazily: true) + try await inProcess.client.connect() } group.addTask { diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index b146529d1..27fd02e68 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -27,7 +27,7 @@ struct AnyClientTransport: ClientTransport, Sendable { _ options: CallOptions, _ body: (RPCStream) async throws -> Any ) async throws -> Any - private let _connect: @Sendable (Bool) async throws -> Void + private let _connect: @Sendable () async throws -> Void private let _close: @Sendable () -> Void private let _configuration: @Sendable (MethodDescriptor) -> MethodConfig? @@ -40,8 +40,8 @@ struct AnyClientTransport: ClientTransport, Sendable { } } - self._connect = { lazily in - try await transport.connect(lazily: lazily) + self._connect = { + try await transport.connect() } self._close = { @@ -57,8 +57,8 @@ struct AnyClientTransport: ClientTransport, Sendable { self._retryThrottle() } - func connect(lazily: Bool) async throws { - try await self._connect(lazily) + func connect() async throws { + try await self._connect() } func close() { diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 0c81fb17c..5a1260293 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -43,8 +43,8 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { self.transport.retryThrottle } - func connect(lazily: Bool) async throws { - try await self.transport.connect(lazily: lazily) + func connect() async throws { + try await self.transport.connect() } func close() { diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 113c2cf53..844278494 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -28,7 +28,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { let retryThrottle: RetryThrottle? = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) - func connect(lazily: Bool) async throws { + func connect() async throws { // no-op } diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index c9289f1b9..2dfa6db00 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -27,11 +27,11 @@ final class InProcessClientTransportTests: XCTestCase { await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.connect(lazily: false) + try await client.connect() } group.addTask { - try await client.connect(lazily: false) + try await client.connect() } await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { @@ -49,7 +49,7 @@ final class InProcessClientTransportTests: XCTestCase { client.close() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await client.connect(lazily: false) + try await client.connect() } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) } @@ -60,7 +60,7 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.connect(lazily: false) + try await client.connect() } group.addTask { try await Task.sleep(for: .milliseconds(100)) @@ -70,7 +70,7 @@ final class InProcessClientTransportTests: XCTestCase { group.cancelAll() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await client.connect(lazily: false) + try await client.connect() } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) } @@ -95,7 +95,7 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.connect(lazily: false) + try await client.connect() } group.addTask { try await Task.sleep(for: .milliseconds(100)) @@ -117,7 +117,7 @@ final class InProcessClientTransportTests: XCTestCase { ) { _ in // Once the pending stream is opened, close the client to new connections, // so that, once this closure is executed and this stream is closed, - // the client will return from `connect(lazily:)`. + // the client will return from `connect()`. client.close() } } @@ -126,7 +126,7 @@ final class InProcessClientTransportTests: XCTestCase { // Add a sleep to make sure connection happens after `withStream` has been called, // to test pending streams are handled correctly. try await Task.sleep(for: .milliseconds(100)) - try await client.connect(lazily: false) + try await client.connect() } try await group.waitForAll() @@ -154,7 +154,7 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.connect(lazily: false) + try await client.connect() } group.addTask { @@ -254,7 +254,7 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.connect(lazily: false) + try await client.connect() } group.addTask { From 047cce9f7cb5b8ec7fce35ca565a3af35612ec23 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:49:29 +0100 Subject: [PATCH 302/580] Enable `Metadata` to be initializable from a `Sequence` of `Elements` (#1871) Motivation: This would be a convenient API to have. Modifications: Add a new `public init` to `Metadata` which takes `some Sequence` and initializes `Metadata` collection from the `Sequence` of `Elements`. Result: Users can now initialize `Metadata` from a `Sequence` of `Elements`. --- Sources/GRPCCore/Metadata.swift | 7 +++++++ Tests/GRPCCoreTests/MetadataTests.swift | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 2d9e64349..5e701a2ad 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -130,6 +130,13 @@ public struct Metadata: Sendable, Hashable { self.elements = [] } + /// Initialize `Metadata` from a `Sequence` of `Element`s. + public init(_ elements: some Sequence) { + self.elements = elements.map { key, value in + KeyValuePair(key: key, value: value) + } + } + /// Reserve the specified minimum capacity in the collection. /// /// - Parameter minimumCapacity: The minimum capacity to reserve in the collection. diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index 9170255ad..40fb2a47b 100644 --- a/Tests/GRPCCoreTests/MetadataTests.swift +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -17,6 +17,19 @@ import GRPCCore import XCTest final class MetadataTests: XCTestCase { + func testInitFromSequence() { + let elements: [Metadata.Element] = [ + (key: "key1", value: "value1"), + (key: "key2", value: "value2"), + (key: "key3", value: "value3"), + ] + + let metadata = Metadata(elements) + let expected: Metadata = ["key1": "value1", "key2": "value2", "key3": "value3"] + + XCTAssertEqual(metadata, expected) + } + func testAddStringValue() { var metadata = Metadata() XCTAssertTrue(metadata.isEmpty) From 90a68388a52ec05ed6485455e899b368412f5f72 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 1 May 2024 12:48:59 +0100 Subject: [PATCH 303/580] Add Subchannel (#1868) Motivation: To build a load balancer we need subchannels. Subchannels wrap connections to provide a connection to a backend. They allow multiple addresses to be tried when establishing a connection as well retry logic for establishing a connection (with backoff). Modifications: - Add a Subchannel type which wraps Connection objects with a similar API: a subchannel must be run, changes in its state are reported via the events sequence, and state changes can be requested by calling `connect`/`close` etc. - Add tests Result: Can build up a load balancer using subchannels. --- .../Connection/LoadBalancers/Subchannel.swift | 578 ++++++++++++++++++ .../LoadBalancers/SubchannelTests.swift | 392 ++++++++++++ .../Connection/Utilities/ConnectionTest.swift | 2 +- .../Connection/Utilities/TestServer.swift | 139 +++++ 4 files changed, 1110 insertions(+), 1 deletion(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift new file mode 100644 index 000000000..ae46e3c31 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift @@ -0,0 +1,578 @@ +/* + * 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 NIOConcurrencyHelpers + +/// A ``Subchannel`` provides communication to a single ``Endpoint``. +/// +/// Each ``Subchannel`` starts in an 'idle' state where it isn't attempting to connect to an +/// endpoint. You can tell it to start connecting by calling ``connect()`` and you can listen +/// to connectivity state changes by consuming the ``events`` sequence. +/// +/// You must call ``close()`` on the ``Subchannel`` when it's no longer required. This will move +/// it to the ``ConnectivityState/shutdown`` state: existing RPCs may continue but all subsequent +/// calls to ``makeStream(descriptor:options:)`` will fail. +/// +/// To use the ``Subchannel`` you must run it in a task: +/// +/// ```swift +/// await withTaskGroup(of: Void.self) { group in +/// group.addTask { await subchannel.run() } +/// +/// for await event in subchannel.events { +/// switch event { +/// case .connectivityStateChanged(.ready): +/// // ... +/// default: +/// // ... +/// } +/// } +/// } +/// ``` +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Subchannel { + enum Event: Sendable, Hashable { + /// The connection received a GOAWAY and will close soon. No new streams + /// should be opened on this connection. + case goingAway + /// The connectivity state of the subchannel changed. + case connectivityStateChanged(ConnectivityState) + /// The subchannel requests that the load balancer re-resolves names. + case requiresNameResolution + } + + private enum Input: Sendable { + /// Request that the connection starts connecting. + case connect + /// A backoff period has ended. + case backedOff + /// Close the connection, if possible. + case close + /// Handle the event from the underlying connection object. + case handleConnectionEvent(Connection.Event) + } + + /// Events which can happen to the subchannel. + private let event: (stream: AsyncStream, continuation: AsyncStream.Continuation) + + /// Inputs which this subchannel should react to. + private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) + + /// The state of the subchannel. + private let state: NIOLockedValueBox + + /// The endpoint this subchannel is targeting. + let endpoint: Endpoint + + /// The ID of the subchannel. + let id: SubchannelID + + /// A factory for connections. + private let connector: any HTTP2Connector + + /// The connection backoff configuration used by the subchannel when establishing a connection. + private let backoff: ConnectionBackoff + + /// The default compression algorithm used for requests. + private let defaultCompression: CompressionAlgorithm + + /// The set of enabled compression algorithms. + private let enabledCompression: CompressionAlgorithmSet + + init( + endpoint: Endpoint, + id: SubchannelID, + connector: any HTTP2Connector, + backoff: ConnectionBackoff, + defaultCompression: CompressionAlgorithm, + enabledCompression: CompressionAlgorithmSet + ) { + assert(!endpoint.addresses.isEmpty, "endpoint.addresses mustn't be empty") + + self.state = NIOLockedValueBox(.notConnected) + self.endpoint = endpoint + self.id = id + self.connector = connector + self.backoff = backoff + self.defaultCompression = defaultCompression + self.enabledCompression = enabledCompression + self.event = AsyncStream.makeStream(of: Event.self) + self.input = AsyncStream.makeStream(of: Input.self) + // Subchannel always starts in the idle state. + self.event.continuation.yield(.connectivityStateChanged(.idle)) + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Subchannel { + /// A stream of events which can happen to the subchannel. + var events: AsyncStream { + self.event.stream + } + + /// Run the subchannel. + /// + /// Running the subchannel will attempt to maintain a connection to a remote endpoint. At times + /// the connection may be idle but it will reconnect on-demand when a stream is requested. If + /// connect attempts fail then the subchannel may progressively spend longer in a transient + /// failure state. + /// + /// Events and state changes can be observed via the ``events`` stream. + func run() async { + await withDiscardingTaskGroup { group in + for await input in self.input.stream { + switch input { + case .connect: + self.handleConnectInput(in: &group) + case .backedOff: + self.handleBackedOffInput(in: &group) + case .close: + self.handleCloseInput(in: &group) + case .handleConnectionEvent(let event): + self.handleConnectionEvent(event, in: &group) + } + } + } + + // Once the task group is done, the event stream must also be finished. In normal operation + // this is handled via other paths. For cancellation it must be finished explicitly. + if Task.isCancelled { + self.event.continuation.finish() + } + } + + /// Initiate a connection attempt, if possible. + func connect() { + self.input.continuation.yield(.connect) + } + + /// Initiates graceful shutdown, if possible. + func close() { + self.input.continuation.yield(.close) + } + + /// Make a stream using the subchannel if it's ready. + /// + /// - Parameter descriptor: A descriptor of the method to create a stream for. + /// - Returns: The open stream. + func makeStream( + descriptor: MethodDescriptor, + options: CallOptions + ) async throws -> Connection.Stream { + let connection: Connection? = self.state.withLockedValue { state in + switch state { + case .notConnected, .connecting, .closing, .closed: + return nil + case .connected(let connected): + return connected.connection + } + } + + guard let connection = connection else { + throw RPCError(code: .unavailable, message: "subchannel isn't ready") + } + + return try await connection.makeStream(descriptor: descriptor, options: options) + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Subchannel { + private func handleConnectInput(in group: inout DiscardingTaskGroup) { + let connection = self.state.withLockedValue { state in + state.makeConnection( + to: self.endpoint.addresses, + using: self.connector, + backoff: self.backoff, + defaultCompression: self.defaultCompression, + enabledCompression: self.enabledCompression + ) + } + + guard let connection = connection else { + // Not in a state to start a connection. + return + } + + // About to start connecting a new connection; emit a state change event. + self.event.continuation.yield(.connectivityStateChanged(.connecting)) + self.runConnection(connection, in: &group) + } + + private func handleBackedOffInput(in group: inout DiscardingTaskGroup) { + switch self.state.withLockedValue({ $0.backedOff() }) { + case .none: + () + + case .connect(let connection): + // About to start connecting, emit a state change event. + self.event.continuation.yield(.connectivityStateChanged(.connecting)) + self.runConnection(connection, in: &group) + + case .shutdown: + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + // Close the event streams. + self.event.continuation.finish() + self.input.continuation.finish() + } + } + + private func handleCloseInput(in group: inout DiscardingTaskGroup) { + switch self.state.withLockedValue({ $0.close() }) { + case .none: + () + + case .close(let connection): + connection.close() + + case .shutdown: + // Connection closed because the load balancer asked it to, so notify the load balancer. + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + // At this point there are no more events: close the event streams. + self.event.continuation.finish() + self.input.continuation.finish() + } + } + + private func handleConnectionEvent( + _ event: Connection.Event, + in group: inout DiscardingTaskGroup + ) { + switch event { + case .connectSucceeded: + self.handleConnectSucceededEvent() + case .connectFailed: + self.handleConnectFailedEvent(in: &group) + case .goingAway: + self.handleGoingAwayEvent() + case .closed(let reason): + self.handleConnectionClosedEvent(reason, in: &group) + } + } + + private func handleConnectSucceededEvent() { + switch self.state.withLockedValue({ $0.connectSucceeded() }) { + case .updateState: + // Emit a connectivity state change: the load balancer can now use this subchannel. + self.event.continuation.yield(.connectivityStateChanged(.ready)) + + case .close(let connection): + connection.close() + + case .none: + () + } + } + + private func handleConnectFailedEvent(in group: inout DiscardingTaskGroup) { + let onConnectFailed = self.state.withLockedValue { $0.connectFailed(connector: self.connector) } + switch onConnectFailed { + case .connect(let connection): + // Try the next address. + self.runConnection(connection, in: &group) + + case .backoff(let duration): + // All addresses have been tried, backoff for some time. + self.event.continuation.yield(.connectivityStateChanged(.transientFailure)) + group.addTask { + do { + try await Task.sleep(for: duration) + self.input.continuation.yield(.backedOff) + } catch { + // Can only be a cancellation error, swallow it. No further connection attempts will be + // made. + () + } + } + + case .shutdown: + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + // No more events, close the streams. + self.event.continuation.finish() + self.input.continuation.finish() + + case .none: + () + } + } + + private func handleGoingAwayEvent() { + let isGoingAway = self.state.withLockedValue { $0.goingAway() } + guard isGoingAway else { return } + + // Notify the load balancer that the subchannel is going away to stop it from being used. + self.event.continuation.yield(.goingAway) + // A GOAWAY also means that the load balancer should re-resolve as the available servers + // may have changed. + self.event.continuation.yield(.requiresNameResolution) + } + + private func handleConnectionClosedEvent( + _ reason: Connection.CloseReason, + in group: inout DiscardingTaskGroup + ) { + let isClosed = self.state.withLockedValue { $0.closed(reason: reason) } + guard isClosed else { return } + + switch reason { + case .idleTimeout: + // Connection closed due to an idle timeout; notify the load balancer about this. + self.event.continuation.yield(.connectivityStateChanged(.idle)) + + case .keepaliveTimeout, .error: + // Unclean closes trigger a transient failure state change and a name resolution. + self.event.continuation.yield(.connectivityStateChanged(.transientFailure)) + self.event.continuation.yield(.requiresNameResolution) + + // Attempt to reconnect. + self.handleConnectInput(in: &group) + + case .initiatedLocally, .remote: + // Connection closed because the load balancer (or remote peer) asked it to, so notify the + // load balancer. In the case of 'remote' (i.e. a GOAWAY), the load balancer will have + // already reacted to a separate 'goingAway' event. + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + + // At this point there are no more events: close the event streams. + self.event.continuation.finish() + self.input.continuation.finish() + } + } + + private func runConnection(_ connection: Connection, in group: inout DiscardingTaskGroup) { + group.addTask { + await connection.run() + } + + group.addTask { + for await event in connection.events { + self.input.continuation.yield(.handleConnectionEvent(event)) + } + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Subchannel { + private enum State { + /// Not connected and not actively connecting. + case notConnected + /// A connection attempt is in-progress. + case connecting(Connecting) + /// A connection has been established. + case connected(Connected) + /// The subchannel is closing. + case closing(Closing) + /// The subchannel is closed. + case closed + + struct Connecting { + var connection: Connection + let addresses: [SocketAddress] + var addressIterator: Array.Iterator + var backoff: ConnectionBackoff.Iterator + } + + struct Connected { + var connection: Connection + + init(from state: Connecting) { + self.connection = state.connection + } + } + + struct Closing { + var connection: Connection + + init(from state: Connecting) { + self.connection = state.connection + } + + init(from state: Connected) { + self.connection = state.connection + } + } + + mutating func makeConnection( + to addresses: [SocketAddress], + using connector: any HTTP2Connector, + backoff: ConnectionBackoff, + defaultCompression: CompressionAlgorithm, + enabledCompression: CompressionAlgorithmSet + ) -> Connection? { + switch self { + case .notConnected: + var iterator = addresses.makeIterator() + let address = iterator.next()! // addresses must not be empty. + + let connection = Connection( + address: address, + http2Connector: connector, + defaultCompression: defaultCompression, + enabledCompression: enabledCompression + ) + + let connecting = State.Connecting( + connection: connection, + addresses: addresses, + addressIterator: iterator, + backoff: backoff.makeIterator() + ) + + self = .connecting(connecting) + return connection + + case .connecting, .connected, .closing, .closed: + return nil + } + } + + enum OnClose { + case none + case shutdown + case close(Connection) + } + + mutating func close() -> OnClose { + let onClose: OnClose + + switch self { + case .notConnected: + onClose = .shutdown + + case .connecting(let state): + self = .closing(Closing(from: state)) + // Do nothing; the connection hasn't been established yet so can't be closed. + onClose = .none + + case .connected(let state): + self = .closing(Closing(from: state)) + onClose = .close(state.connection) + + case .closing, .closed: + onClose = .none + } + + return onClose + } + + enum OnConnectSucceeded { + case updateState + case close(Connection) + case none + } + + mutating func connectSucceeded() -> OnConnectSucceeded { + switch self { + case .connecting(let state): + self = .connected(Connected(from: state)) + return .updateState + case .closing(let state): + self = .closing(state) + return .close(state.connection) + case .notConnected, .connected, .closed: + return .none + } + } + + enum OnConnectFailed { + case none + case connect(Connection) + case backoff(Duration) + case shutdown + } + + mutating func connectFailed(connector: any HTTP2Connector) -> OnConnectFailed { + switch self { + case .connecting(var connecting): + if let address = connecting.addressIterator.next() { + connecting.connection = Connection( + address: address, + http2Connector: connector, + defaultCompression: .none, + enabledCompression: .all + ) + self = .connecting(connecting) + return .connect(connecting.connection) + } else { + connecting.addressIterator = connecting.addresses.makeIterator() + let address = connecting.addressIterator.next()! + connecting.connection = Connection( + address: address, + http2Connector: connector, + defaultCompression: .none, + enabledCompression: .all + ) + let backoff = connecting.backoff.next() + self = .connecting(connecting) + return .backoff(backoff) + } + + case .closing: + self = .closed + return .shutdown + + case .notConnected, .connected, .closed: + return .none + } + } + + enum OnBackedOff { + case none + case connect(Connection) + case shutdown + } + + mutating func backedOff() -> OnBackedOff { + switch self { + case .connecting(let state): + return .connect(state.connection) + case .closing: + self = .closed + return .shutdown + case .notConnected, .connected, .closed: + return .none + } + } + + mutating func goingAway() -> Bool { + switch self { + case .connected(let state): + self = .closing(Closing(from: state)) + return true + case .notConnected, .closing, .connecting, .closed: + return false + } + } + + mutating func closed(reason: Connection.CloseReason) -> Bool { + switch self { + case .connected, .closing: + switch reason { + case .idleTimeout, .keepaliveTimeout, .error: + self = .notConnected + case .initiatedLocally, .remote: + self = .closed + } + + return true + case .notConnected, .connecting, .closed: + return false + } + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift new file mode 100644 index 000000000..28831ee1b --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -0,0 +1,392 @@ +/* + * 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 +@_spi(Package) @testable import GRPCHTTP2Core +import NIOCore +import NIOHTTP2 +import NIOPosix +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class SubchannelTests: XCTestCase { + func testMakeStreamOnIdleSubchannel() async throws { + let subchannel = self.makeSubchannel( + address: .unixDomainSocket(path: "ignored"), + connector: .never + ) + + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) + } errorHandler: { error in + XCTAssertEqual(error.code, .unavailable) + } + + subchannel.close() + } + + func testMakeStreamOnShutdownSubchannel() async throws { + let subchannel = self.makeSubchannel( + address: .unixDomainSocket(path: "ignored"), + connector: .never + ) + + subchannel.close() + await subchannel.run() + + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) + } errorHandler: { error in + XCTAssertEqual(error.code, .unavailable) + } + } + + func testMakeStreamOnReadySubchannel() async throws { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + let subchannel = self.makeSubchannel(address: address, connector: .posix()) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.run { inbound, outbound in + for try await part in inbound { + switch part { + case .metadata: + try await outbound.write(.metadata([:])) + case .message(let message): + try await outbound.write(.message(message)) + } + } + try await outbound.write(.status(Status(code: .ok, message: ""), [:])) + } + } + + group.addTask { + await subchannel.run() + } + + subchannel.connect() + + for await event in subchannel.events { + switch event { + case .connectivityStateChanged(.ready): + let stream = try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) + try await stream.execute { inbound, outbound in + try await outbound.write(.metadata([:])) + try await outbound.write(.message([0, 1, 2])) + outbound.finish() + + for try await part in inbound { + switch part { + case .metadata: + () // Don't validate, contains http/2 specific metadata too. + case .message(let message): + XCTAssertEqual(message, [0, 1, 2]) + case .status(let status, _): + XCTAssertEqual(status.code, .ok) + XCTAssertEqual(status.message, "") + } + } + } + subchannel.close() + + default: + () + } + } + + group.cancelAll() + } + } + + func testConnectEventuallySucceeds() async throws { + let path = "test-connect-eventually-succeeds" + let subchannel = self.makeSubchannel( + address: .unixDomainSocket(path: path), + connector: .posix(), + backoff: .fixed(at: .milliseconds(100)) + ) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { await subchannel.run() } + + var hasServer = false + var events = [Subchannel.Event]() + + for await event in subchannel.events { + events.append(event) + switch event { + case .connectivityStateChanged(.idle): + subchannel.connect() + + case .connectivityStateChanged(.transientFailure): + // Don't start more than one server. + if hasServer { continue } + hasServer = true + + group.addTask { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + _ = try await server.bind(to: .uds(path)) + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + + case .connectivityStateChanged(.ready): + subchannel.close() + + case .connectivityStateChanged(.shutdown): + group.cancelAll() + + default: + () + } + } + + // First four events are known: + XCTAssertEqual( + Array(events.prefix(4)), + [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.transientFailure), + .connectivityStateChanged(.connecting), + ] + ) + + // Because there is backoff timing involved, the subchannel may flip from transient failure + // to connecting multiple times. Just check that it eventually becomes ready and is then + // shutdown. + XCTAssertEqual( + Array(events.suffix(2)), + [ + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + ) + } + } + + func testConnectIteratesThroughAddresses() async throws { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + let subchannel = self.makeSubchannel( + addresses: [ + .unixDomainSocket(path: "not-listening-1"), + .unixDomainSocket(path: "not-listening-2"), + address, + ], + connector: .posix() + ) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + + group.addTask { + await subchannel.run() + } + + for await event in subchannel.events { + switch event { + case .connectivityStateChanged(.idle): + subchannel.connect() + case .connectivityStateChanged(.ready): + subchannel.close() + case .connectivityStateChanged(.shutdown): + group.cancelAll() + default: + () + } + } + } + } + + func testConnectIteratesThroughAddressesWithBackoff() async throws { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let udsPath = "test-wrap-around-addrs" + + let subchannel = self.makeSubchannel( + addresses: [ + .unixDomainSocket(path: "not-listening-1"), + .unixDomainSocket(path: "not-listening-2"), + .unixDomainSocket(path: udsPath), + ], + connector: .posix(), + backoff: .fixed(at: .zero) // Skip the backoff period + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + await subchannel.run() + } + + var isServerRunning = false + + for await event in subchannel.events { + switch event { + case .connectivityStateChanged(.idle): + subchannel.connect() + + case .connectivityStateChanged(.transientFailure): + // The subchannel enters the transient failure state when all addresses have been tried. + // Bind the server now so that the next attempts succeeds. + if isServerRunning { break } + isServerRunning = true + + let address = try await server.bind(to: .uds(udsPath)) + XCTAssertEqual(address, .unixDomainSocket(path: udsPath)) + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + + case .connectivityStateChanged(.ready): + subchannel.close() + + case .connectivityStateChanged(.shutdown): + group.cancelAll() + + default: + () + } + } + } + } + + func testConnectedReceivesGoAway() async throws { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + let subchannel = self.makeSubchannel(address: address, connector: .posix()) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + + group.addTask { + await subchannel.run() + } + + var events = [Subchannel.Event]() + + for await event in subchannel.events { + events.append(event) + + switch event { + case .connectivityStateChanged(.idle): + subchannel.connect() + + case .connectivityStateChanged(.ready): + // Now the subchannel is ready, send a GOAWAY from the server. + let channel = try XCTUnwrap(server.clients.first) + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway(lastStreamID: 0, errorCode: .cancel, opaqueData: nil) + ) + try await channel.writeAndFlush(goAway) + + case .connectivityStateChanged(.shutdown): + group.cancelAll() + + default: + () + } + } + + let expectedEvents: [Subchannel.Event] = [ + // Normal connect flow. + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + // GOAWAY triggers name resolution too. + .goingAway, + .requiresNameResolution, + // Finally, shutdown. + .connectivityStateChanged(.shutdown), + ] + + XCTAssertEqual(expectedEvents, events) + } + } + + func testCancelReadySubchannel() async throws { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + let subchannel = self.makeSubchannel(address: address, connector: .posix()) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + + group.addTask { + subchannel.connect() + await subchannel.run() + } + + for await event in subchannel.events { + switch event { + case .connectivityStateChanged(.ready): + group.cancelAll() + default: + () + } + } + } + } + + private func makeSubchannel( + addresses: [GRPCHTTP2Core.SocketAddress], + connector: any HTTP2Connector, + backoff: ConnectionBackoff? = nil + ) -> Subchannel { + return Subchannel( + endpoint: Endpoint(addresses: addresses), + id: SubchannelID(), + connector: connector, + backoff: backoff ?? .defaults, + defaultCompression: .none, + enabledCompression: .none + ) + } + + private func makeSubchannel( + address: GRPCHTTP2Core.SocketAddress, + connector: any HTTP2Connector, + backoff: ConnectionBackoff? = nil + ) -> Subchannel { + self.makeSubchannel(addresses: [address], connector: connector, backoff: backoff) + } +} + +extension ConnectionBackoff { + static func fixed(at interval: Duration, jitter: Double = 0.0) -> Self { + return Self(initial: interval, max: interval, multiplier: 1.0, jitter: jitter) + } + + static var defaults: Self { + ConnectionBackoff(initial: .seconds(10), max: .seconds(120), multiplier: 1.6, jitter: 1.2) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift index d0f7771ac..5412b5fa9 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -165,7 +165,7 @@ extension ConnectionTest { } } - private final class EchoHandler: ChannelInboundHandler { + final class EchoHandler: ChannelInboundHandler { typealias InboundIn = RPCRequestPart typealias OutboundOut = RPCResponsePart diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift new file mode 100644 index 000000000..471a81bd3 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -0,0 +1,139 @@ +/* + * 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 NIOConcurrencyHelpers +import NIOCore +import NIOHTTP2 +import NIOPosix + +@testable import GRPCHTTP2Core + +final class TestServer: Sendable { + private let eventLoopGroup: any EventLoopGroup + private typealias Stream = NIOAsyncChannel + private typealias Multiplexer = NIOHTTP2AsyncSequence + + private let connected: NIOLockedValueBox<[Channel]> + + typealias Inbound = NIOAsyncChannelInboundStream + typealias Outbound = NIOAsyncChannelOutboundWriter + + private let server: NIOLockedValueBox?> + + init(eventLoopGroup: any EventLoopGroup) { + self.eventLoopGroup = eventLoopGroup + self.server = NIOLockedValueBox(nil) + self.connected = NIOLockedValueBox([]) + } + + enum Target { + case localhost + case uds(String) + } + + var clients: [Channel] { + return self.connected.withLockedValue { $0 } + } + + func bind(to target: Target = .localhost) async throws -> GRPCHTTP2Core.SocketAddress { + precondition(self.server.withLockedValue { $0 } == nil) + + @Sendable + func configure(_ channel: Channel) -> EventLoopFuture { + self.connected.withLockedValue { + $0.append(channel) + } + + channel.closeFuture.whenSuccess { + self.connected.withLockedValue { connected in + guard let index = connected.firstIndex(where: { $0 === channel }) else { return } + connected.remove(at: index) + } + } + + return channel.eventLoop.makeCompletedFuture { + let sync = channel.pipeline.syncOperations + let multiplexer = try sync.configureAsyncHTTP2Pipeline(mode: .server) { stream in + stream.eventLoop.makeCompletedFuture { + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: .all, + maximumPayloadSize: .max + ) + + try stream.pipeline.syncOperations.addHandlers(handler) + return try NIOAsyncChannel( + wrappingChannelSynchronously: stream, + configuration: .init( + inboundType: RPCRequestPart.self, + outboundType: RPCResponsePart.self + ) + ) + } + } + + return multiplexer.inbound + } + } + + let bootstrap = ServerBootstrap(group: self.eventLoopGroup) + let server: NIOAsyncChannel + let address: GRPCHTTP2Core.SocketAddress + + switch target { + case .localhost: + server = try await bootstrap.bind(host: "127.0.0.1", port: 0) { channel in + configure(channel) + } + address = .ipv4(host: "127.0.0.1", port: server.channel.localAddress!.port!) + + case .uds(let path): + server = try await bootstrap.bind(unixDomainSocketPath: path, cleanupExistingSocketFile: true) + { channel in + configure(channel) + } + address = .unixDomainSocket(path: server.channel.localAddress!.pathname!) + } + + self.server.withLockedValue { $0 = server } + return address + } + + func run(_ handle: @Sendable @escaping (Inbound, Outbound) async throws -> Void) async throws { + guard let server = self.server.withLockedValue({ $0 }) else { + fatalError("bind() must be called first") + } + + try await server.executeThenClose { inbound, _ in + try await withThrowingTaskGroup(of: Void.self) { multiplexerGroup in + for try await multiplexer in inbound { + multiplexerGroup.addTask { + try await withThrowingTaskGroup(of: Void.self) { streamGroup in + for try await stream in multiplexer { + streamGroup.addTask { + try await stream.executeThenClose { inbound, outbound in + try await handle(inbound, outbound) + } + } + } + } + } + } + } + } + } +} From 71b4fe85ea5aad09a7391a7a284e1bd7deebdd34 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 2 May 2024 10:29:12 +0100 Subject: [PATCH 304/580] Add missing availability guards to tests (#1874) --- .../Client/Connection/LoadBalancers/SubchannelTests.swift | 1 + .../Client/Connection/Utilities/TestServer.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 28831ee1b..709587fbe 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -381,6 +381,7 @@ final class SubchannelTests: XCTestCase { } } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ConnectionBackoff { static func fixed(at interval: Duration, jitter: Double = 0.0) -> Self { return Self(initial: interval, max: interval, multiplier: 1.0, jitter: jitter) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift index 471a81bd3..2c133849e 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -22,6 +22,7 @@ import NIOPosix @testable import GRPCHTTP2Core +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class TestServer: Sendable { private let eventLoopGroup: any EventLoopGroup private typealias Stream = NIOAsyncChannel From 07eade28c84e2d5e9237b7a6f19469bd34f843a2 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Thu, 2 May 2024 14:08:03 +0100 Subject: [PATCH 305/580] Add `Hashable` conformance to `GRPC.ConnectionTarget` (#1876) Motivation: `GRPC.ConnectionTarget` is not `Hashable` and, for example, cannot be stored in a set. Modifications: Add `Hashable` conformance to `GRPC.ConnectionTarget`. Result: `GRPC.ConnectionTarget` is now `Hashable`. --- Sources/GRPC/ClientConnection.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift index 26adea540..e2f6dbdcb 100644 --- a/Sources/GRPC/ClientConnection.swift +++ b/Sources/GRPC/ClientConnection.swift @@ -270,8 +270,8 @@ extension ClientConnection: GRPCChannel { // MARK: - Configuration structures /// A target to connect to. -public struct ConnectionTarget: Sendable { - internal enum Wrapped { +public struct ConnectionTarget: Sendable, Hashable { + internal enum Wrapped: Hashable { case hostAndPort(String, Int) case unixDomainSocket(String) case socketAddress(SocketAddress) From fe7132284c7de46c1564555566d56265f1f043c5 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 14 May 2024 10:03:10 +0100 Subject: [PATCH 306/580] Fix unusual fatal error when closing outbound client stream (#1877) --- Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift | 3 ++- .../InProcessInteroperabilityTests.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index c063b9030..441beb220 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -609,7 +609,8 @@ extension GRPCStreamStateMachine { case .clientOpenServerClosed(let state): self.state = .clientClosedServerClosed(.init(previousState: state)) case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState("Client is already closed.") + // Client is already closed - nothing to do. + () } } diff --git a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift index f06549cd2..20e4a9f56 100644 --- a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift +++ b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift @@ -53,7 +53,7 @@ final class InProcessInteroperabilityTests: XCTestCase { } } - func testEmtyUnary() async throws { + func testEmptyUnary() async throws { try await self.runInProcessTransport(interopTestCase: .emptyUnary) } From 07744c75f66380b70a3f795bca1b37e2289aaea0 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 14 May 2024 11:05:53 +0100 Subject: [PATCH 307/580] Add additional stream message compression unit tests (#1879) Co-authored-by: George Barnett --- .../GRPCStreamStateMachineTests.swift | 215 +++++++++++++++--- 1 file changed, 187 insertions(+), 28 deletions(-) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index dc7f4aa96..4fe777e2b 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -121,6 +121,12 @@ extension HPACKHeaders { GRPCHTTP2Keys.encoding.rawValue: "deflate", GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] + fileprivate static let serverInitialMetadataWithGZIPCompression: Self = [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "gzip", + GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", + ] fileprivate static let serverTrailers: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, @@ -331,6 +337,93 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { } } + func testReceiveInitialMetadataWhenServerIdle_ClientUnsupportedEncoding() throws { + // Create client with deflate compression enabled + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerIdle, + compressionEnabled: true + ) + + // Try opening server with gzip compression, which client does not support. + let action = try stateMachine.receive( + headers: .serverInitialMetadataWithGZIPCompression, + endStream: false + ) + + XCTAssertEqual( + action, + .receivedStatusAndMetadata( + status: Status( + code: .internalError, + message: + "The server picked a compression algorithm ('gzip') the client does not know about." + ), + metadata: [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "gzip", + "grpc-accept-encoding": "deflate", + ] + ) + ) + } + + func testReceiveMessage_ClientCompressionEnabled() throws { + // Enable deflate compression on client + var stateMachine = self.makeClientStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + + // Receiving uncompressed message should still work. + let receivedUncompressedBytes = try self.frameMessage(originalMessage, compression: .none) + XCTAssertNoThrow(try stateMachine.receive(buffer: receivedUncompressedBytes, endStream: false)) + var receivedAction = stateMachine.nextInboundMessage() + switch receivedAction { + case .noMoreMessages, .awaitMoreMessages: + XCTFail("Should have received message") + case .receiveMessage(let receivedMessaged): + XCTAssertEqual(originalMessage, receivedMessaged) + } + + // Receiving compressed message with deflate should work + let receivedDeflateCompressedBytes = try self.frameMessage( + originalMessage, + compression: .deflate + ) + XCTAssertNoThrow( + try stateMachine.receive(buffer: receivedDeflateCompressedBytes, endStream: false) + ) + receivedAction = stateMachine.nextInboundMessage() + switch receivedAction { + case .noMoreMessages, .awaitMoreMessages: + XCTFail("Should have received message") + case .receiveMessage(let receivedMessaged): + XCTAssertEqual(originalMessage, receivedMessaged) + } + + // Receiving compressed message with gzip (unsupported) should throw error + let receivedGZIPCompressedBytes = try self.frameMessage(originalMessage, compression: .gzip) + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Decompression error") + } + receivedAction = stateMachine.nextInboundMessage() + switch receivedAction { + case .awaitMoreMessages: + () + case .noMoreMessages: + XCTFail("Should be awaiting for more messages") + case .receiveMessage: + XCTFail("Should not have received message") + } + } + func testReceiveInitialMetadataWhenServerIdle() throws { for targetState in [ TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, @@ -681,7 +774,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) let request = try stateMachine.nextOutboundFrame() - let framedMessage = try self.frameMessage(originalMessage, compress: true) + let framedMessage = try self.frameMessage(originalMessage, compression: .deflate) XCTAssertEqual(request, .sendFrame(frame: framedMessage, promise: nil)) } @@ -697,7 +790,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) let request = try stateMachine.nextOutboundFrame() - let framedMessage = try self.frameMessage(originalMessage, compress: true) + let framedMessage = try self.frameMessage(originalMessage, compression: .deflate) XCTAssertEqual(request, .sendFrame(frame: framedMessage, promise: nil)) } @@ -806,7 +899,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) let originalMessage = [UInt8]([42, 42, 43, 43]) - let receivedBytes = try self.frameMessage(originalMessage, compress: true) + let receivedBytes = try self.frameMessage(originalMessage, compression: .deflate) XCTAssertEqual( try stateMachine.receive(buffer: receivedBytes, endStream: false), .readInbound @@ -920,7 +1013,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let message = [UInt8]([1, 2, 3, 4]) - let framedMessage = try self.frameMessage(message, compress: false) + let framedMessage = try self.frameMessage(message, compression: .none) try stateMachine.send(message: message, promise: nil) XCTAssertEqual( try stateMachine.nextOutboundFrame(), @@ -932,9 +1025,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) let firstResponseBytes = [UInt8]([5, 6, 7]) - let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let firstResponse = try self.frameMessage(firstResponseBytes, compression: .none) let secondResponseBytes = [UInt8]([8, 9, 10]) - let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + let secondResponse = try self.frameMessage(secondResponseBytes, compression: .none) XCTAssertEqual( try stateMachine.receive(buffer: firstResponse, endStream: false), .readInbound @@ -993,7 +1086,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let message = [UInt8]([1, 2, 3, 4]) - let framedMessage = try self.frameMessage(message, compress: false) + let framedMessage = try self.frameMessage(message, compression: .none) XCTAssertNoThrow(try stateMachine.send(message: message, promise: nil)) XCTAssertNoThrow(try stateMachine.closeOutbound()) XCTAssertEqual( @@ -1020,9 +1113,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) let firstResponseBytes = [UInt8]([5, 6, 7]) - let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let firstResponse = try self.frameMessage(firstResponseBytes, compression: .none) let secondResponseBytes = [UInt8]([8, 9, 10]) - let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + let secondResponse = try self.frameMessage(secondResponseBytes, compression: .none) XCTAssertEqual( try stateMachine.receive(buffer: firstResponse, endStream: false), .readInbound @@ -1078,7 +1171,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) let message = [UInt8]([1, 2, 3, 4]) - let framedMessage = try self.frameMessage(message, compress: false) + let framedMessage = try self.frameMessage(message, compression: .none) try stateMachine.send(message: message, promise: nil) XCTAssertEqual( try stateMachine.nextOutboundFrame(), @@ -1107,9 +1200,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) let firstResponseBytes = [UInt8]([5, 6, 7]) - let firstResponse = try self.frameMessage(firstResponseBytes, compress: false) + let firstResponse = try self.frameMessage(firstResponseBytes, compression: .none) let secondResponseBytes = [UInt8]([8, 9, 10]) - let secondResponse = try self.frameMessage(secondResponseBytes, compress: false) + let secondResponse = try self.frameMessage(secondResponseBytes, compression: .none) XCTAssertEqual( try stateMachine.receive(buffer: firstResponse, endStream: false), .readInbound @@ -1757,9 +1850,6 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } } - //TODO: add more encoding-related validation tests (for both client and server) - // and message encoding tests - func testReceiveMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) @@ -1881,6 +1971,62 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } } + func testReceiveMessage_ServerCompressionEnabled() throws { + // Enable deflate compression on server + var stateMachine = self.makeServerStateMachine( + targetState: .clientOpenServerOpen, + compressionEnabled: true + ) + + let originalMessage = [UInt8]([42, 42, 43, 43]) + + // Receiving uncompressed message should still work. + let receivedUncompressedBytes = try self.frameMessage(originalMessage, compression: .none) + XCTAssertNoThrow(try stateMachine.receive(buffer: receivedUncompressedBytes, endStream: false)) + var receivedAction = stateMachine.nextInboundMessage() + switch receivedAction { + case .noMoreMessages, .awaitMoreMessages: + XCTFail("Should have received message") + case .receiveMessage(let receivedMessaged): + XCTAssertEqual(originalMessage, receivedMessaged) + } + + // Receiving compressed message with deflate should work + let receivedDeflateCompressedBytes = try self.frameMessage( + originalMessage, + compression: .deflate + ) + XCTAssertNoThrow( + try stateMachine.receive(buffer: receivedDeflateCompressedBytes, endStream: false) + ) + receivedAction = stateMachine.nextInboundMessage() + switch receivedAction { + case .noMoreMessages, .awaitMoreMessages: + XCTFail("Should have received message") + case .receiveMessage(let receivedMessaged): + XCTAssertEqual(originalMessage, receivedMessaged) + } + + // Receiving compressed message with gzip (unsupported) should throw error + let receivedGZIPCompressedBytes = try self.frameMessage(originalMessage, compression: .gzip) + XCTAssertThrowsError( + ofType: RPCError.self, + try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Decompression error") + } + receivedAction = stateMachine.nextInboundMessage() + switch receivedAction { + case .awaitMoreMessages: + () + case .noMoreMessages: + XCTFail("Should be awaiting for more messages") + case .receiveMessage: + XCTFail("Should not have received message") + } + } + func testReceiveMessageWhenClientOpenAndServerClosed() { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) @@ -1993,7 +2139,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) let response = try stateMachine.nextOutboundFrame() - let framedMessage = try self.frameMessage(originalMessage, compress: true) + let framedMessage = try self.frameMessage(originalMessage, compression: .deflate) XCTAssertEqual(response, .sendFrame(frame: framedMessage, promise: nil)) } @@ -2125,7 +2271,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) let originalMessage = [UInt8]([42, 42, 43, 43]) - let receivedBytes = try self.frameMessage(originalMessage, compress: true) + let receivedBytes = try self.frameMessage(originalMessage, compression: .deflate) XCTAssertEqual( try stateMachine.receive(buffer: receivedBytes, endStream: false), @@ -2247,7 +2393,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Client sends messages let deframedMessage = [UInt8]([1, 2, 3, 4]) - let completeMessage = try self.frameMessage(deframedMessage, compress: false) + let completeMessage = try self.frameMessage(deframedMessage, compression: .none) // Split message into two parts to make sure the stitching together of the frames works well let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! @@ -2276,7 +2422,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { try stateMachine.send(message: secondResponse, promise: secondPromise) // Make sure messages are outbound - let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + let framedMessages = try self.frameMessages( + [firstResponse, secondResponse], + compression: .none + ) guard case .sendFrame(let nextOutboundByteBuffer, let nextOutboundPromise) = @@ -2326,7 +2475,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Client sends messages let deframedMessage = [UInt8]([1, 2, 3, 4]) - let completeMessage = try self.frameMessage(deframedMessage, compress: false) + let completeMessage = try self.frameMessage(deframedMessage, compression: .none) // Split message into two parts to make sure the stitching together of the frames works well let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! @@ -2368,7 +2517,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { try stateMachine.send(message: secondResponse, promise: nil) // Make sure messages are outbound - let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + let framedMessages = try self.frameMessages( + [firstResponse, secondResponse], + compression: .none + ) XCTAssertEqual( try stateMachine.nextOutboundFrame(), .sendFrame(frame: framedMessages, promise: nil) @@ -2400,7 +2552,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Client sends messages let deframedMessage = [UInt8]([1, 2, 3, 4]) - let completeMessage = try self.frameMessage(deframedMessage, compress: false) + let completeMessage = try self.frameMessage(deframedMessage, compression: .none) // Split message into two parts to make sure the stitching together of the frames works well let firstMessage = completeMessage.getSlice(at: 0, length: 4)! let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! @@ -2442,7 +2594,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { try stateMachine.send(message: secondResponse, promise: nil) // Make sure messages are outbound - let framedMessages = try self.frameMessages([firstResponse, secondResponse], compress: false) + let framedMessages = try self.frameMessages( + [firstResponse, secondResponse], + compression: .none + ) XCTAssertEqual( try stateMachine.nextOutboundFrame(), .sendFrame(frame: framedMessages, promise: nil) @@ -2473,16 +2628,20 @@ extension XCTestCase { try expression(trailers) } - func frameMessage(_ message: [UInt8], compress: Bool) throws -> ByteBuffer { - try frameMessages([message], compress: compress) + func frameMessage(_ message: [UInt8], compression: CompressionAlgorithm) throws -> ByteBuffer { + try frameMessages([message], compression: compression) } - func frameMessages(_ messages: [[UInt8]], compress: Bool) throws -> ByteBuffer { + func frameMessages(_ messages: [[UInt8]], compression: CompressionAlgorithm) throws -> ByteBuffer + { var framer = GRPCMessageFramer() let compressor: Zlib.Compressor? = { - if compress { + switch compression { + case .deflate: return Zlib.Compressor(method: .deflate) - } else { + case .gzip: + return Zlib.Compressor(method: .gzip) + default: return nil } }() From 0e9def87923e3f4219737fd191e96e1e24a13906 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 15 May 2024 17:46:55 +0100 Subject: [PATCH 308/580] Round robin load balancer (#1873) Motivation: To build a grpc channel we need to support different load-balancers. One of these is the round-robin load balancer which is added by this patch. Modifications: Add a `RoundRobinLoadBalancer` which maintains a set of subchannels and picks between ready subchannels for RPCs using the round-robin algorithm. Result: Can maintain a set of connections to a service using and pick between them using round-robin --- .../LoadBalancers/LoadBalancerEvent.swift | 23 + .../RoundRobinLoadBalancer.swift | 731 ++++++++++++++++++ .../Internal/ProcessUniqueID.swift | 8 + .../RoundRobinLoadBalancerTests.swift | 483 ++++++++++++ .../Internal/ProcessUniqueIDTests.swift | 6 + .../Test Utilities/Task+Poll.swift | 37 + .../Test Utilities/XCTest+Utilities.swift | 10 + 7 files changed, 1298 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift new file mode 100644 index 000000000..466f7b675 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/// Events emitted by load-balancers. +enum LoadBalancerEvent: Sendable, Hashable { + /// The connectivity state of the subchannel changed. + case connectivityStateChanged(ConnectivityState) + /// The subchannel requests that the load balancer re-resolves names. + case requiresNameResolution +} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift new file mode 100644 index 000000000..1699bb7f7 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -0,0 +1,731 @@ +/* + * 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 + +/// A load-balancer which maintains to a set of subchannels and uses round-robin to pick a +/// subchannel when picking a subchannel to use. +/// +/// This load-balancer starts in an 'idle' state and begins connecting when a set of addresses is +/// provided to it with ``updateAddresses(_:)``. Repeated calls to ``updateAddresses(_:)`` will +/// update the subchannels gracefully: new subchannels will be added for new addresses and existing +/// subchannels will be removed if their addresses are no longer present. +/// +/// The state of the load-balancer is aggregated across the state of its subchannels, changes in +/// the aggregate state are reported up via ``events``. +/// +/// You must call ``close()`` on the load-balancer when it's no longer required. This will move +/// it to the ``ConnectivityState/shutdown`` state: existing RPCs may continue but all subsequent +/// calls to ``makeStream(descriptor:options:)`` will fail. +/// +/// To use this load-balancer you must run it in a task: +/// +/// ```swift +/// await withDiscardingTaskGroup { group in +/// // Run the load-balancer +/// group.addTask { await roundRobin.run() } +/// +/// // Update its address list +/// let endpoints: [Endpoint] = [ +/// Endpoint(addresses: [.ipv4(host: "127.0.0.1", port: 1001)]), +/// Endpoint(addresses: [.ipv4(host: "127.0.0.1", port: 1002)]), +/// Endpoint(addresses: [.ipv4(host: "127.0.0.1", port: 1003)]) +/// ] +/// roundRobin.updateAddresses(endpoints) +/// +/// // Consume state update events +/// for await event in roundRobin.events { +/// switch event { +/// case .connectivityStateChanged(.ready): +/// // ... +/// default: +/// // ... +/// } +/// } +/// } +/// ``` +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct RoundRobinLoadBalancer { + enum Input: Sendable, Hashable { + /// Update the addresses used by the load balancer to the following endpoints. + case updateAddresses([Endpoint]) + /// Close the load balancer. + case close + } + + /// A key for an endpoint which identifies it uniquely, regardless of the ordering of addresses. + private struct EndpointKey: Hashable, Sendable, CustomStringConvertible { + /// Opaque data. + private let opaque: [String] + + /// The endpoint this key is for. + let endpoint: Endpoint + + init(_ endpoint: Endpoint) { + self.endpoint = endpoint + self.opaque = endpoint.addresses.map { String(describing: $0) }.sorted() + } + + var description: String { + String(describing: self.endpoint.addresses) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.opaque) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.opaque == rhs.opaque + } + } + + /// Events which can happen to the load balancer. + private let event: + ( + stream: AsyncStream, + continuation: AsyncStream.Continuation + ) + + /// Inputs which this load balancer should react to. + private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) + + /// The state of the load balancer. + private let state: _LockedValueBox + + /// A connector, capable of creating connections. + private let connector: any HTTP2Connector + + /// Connection backoff configuration. + private let backoff: ConnectionBackoff + + /// The default compression algorithm to use. Can be overridden on a per-call basis. + private let defaultCompression: CompressionAlgorithm + + /// The set of enabled compression algorithms. + private let enabledCompression: CompressionAlgorithmSet + + init( + connector: any HTTP2Connector, + backoff: ConnectionBackoff, + defaultCompression: CompressionAlgorithm, + enabledCompression: CompressionAlgorithmSet + ) { + self.connector = connector + self.backoff = backoff + self.defaultCompression = defaultCompression + self.enabledCompression = enabledCompression + + self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) + self.input = AsyncStream.makeStream(of: Input.self) + self.state = _LockedValueBox(.active(State.Active())) + + // The load balancer starts in the idle state. + self.event.continuation.yield(.connectivityStateChanged(.idle)) + } + + /// A stream of events which can happen to the load balancer. + var events: AsyncStream { + self.event.stream + } + + /// Runs the load balancer, returning when it has closed. + /// + /// You can monitor events which happen on the load balancer with ``events``. + func run() async { + await withDiscardingTaskGroup { group in + for await input in self.input.stream { + switch input { + case .updateAddresses(let addresses): + self.handleUpdateAddresses(addresses, in: &group) + case .close: + self.handleCloseInput() + } + } + } + + if Task.isCancelled { + // Finish the event stream as it's unlikely to have been finished by a regular code path. + self.event.continuation.finish() + } + } + + /// Update the addresses used by the load balancer. + /// + /// This may result in new subchannels being created and some subchannels being removed. + func updateAddresses(_ endpoints: [Endpoint]) { + self.input.continuation.yield(.updateAddresses(endpoints)) + } + + /// Close the load balancer, and all subchannels it manages. + func close() { + self.input.continuation.yield(.close) + } + + /// Pick a ready subchannel from the load balancer. + /// + /// - Returns: A subchannel, or `nil` if there aren't any ready subchannels. + func pickSubchannel() -> Subchannel? { + switch self.state.withLockedValue({ $0.pickSubchannel() }) { + case .picked(let subchannel): + return subchannel + + case .notAvailable(let subchannels): + // Tell the subchannels to start connecting. + for subchannel in subchannels { + subchannel.connect() + } + return nil + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension RoundRobinLoadBalancer { + /// Handles an update in endpoints. + /// + /// The load-balancer will diff the set of endpoints with the existing set of endpoints: + /// - endpoints which are new will have subchannels created for them, + /// - endpoints which existed previously but are not present in `endpoints` are closed, + /// - endpoints which existed previously and are still present in `endpoints` are untouched. + /// + /// This process is gradual: the load-balancer won't remove an old endpoint until a subchannel + /// for a corresponding new subchannel becomes ready. + /// + /// - Parameters: + /// - endpoints: Endpoints which should have subchannels. Must not be empty. + /// - group: The group which should manage and run new subchannels. + private func handleUpdateAddresses(_ endpoints: [Endpoint], in group: inout DiscardingTaskGroup) { + if endpoints.isEmpty { return } + + // Compute the keys for each endpoint. + let newEndpoints = Set(endpoints.map { EndpointKey($0) }) + + let (added, removed, newState) = self.state.withLockedValue { state in + state.updateSubchannels(newEndpoints: newEndpoints) { endpoint, id in + Subchannel( + endpoint: endpoint, + id: id, + connector: self.connector, + backoff: self.backoff, + defaultCompression: self.defaultCompression, + enabledCompression: self.enabledCompression + ) + } + } + + // Publish the new connectivity state. + if let newState = newState { + self.event.continuation.yield(.connectivityStateChanged(newState)) + } + + // Run each of the new subchannels. + for subchannel in added { + let key = EndpointKey(subchannel.endpoint) + self.runSubchannel(subchannel, forKey: key, in: &group) + } + + // Old subchannels are removed when new subchannels become ready. Excess subchannels are only + // present if there are more to remove than to add. These are the excess subchannels which + // are closed now. + for subchannel in removed { + subchannel.close() + } + } + + private func runSubchannel( + _ subchannel: Subchannel, + forKey key: EndpointKey, + in group: inout DiscardingTaskGroup + ) { + // Start running it and tell it to connect. + subchannel.connect() + group.addTask { + await subchannel.run() + } + + group.addTask { + for await event in subchannel.events { + switch event { + case .connectivityStateChanged(let state): + self.handleSubchannelConnectivityStateChange(state, key: key) + case .goingAway: + self.handleSubchannelGoingAway(key: key) + case .requiresNameResolution: + self.event.continuation.yield(.requiresNameResolution) + } + } + } + } + + private func handleSubchannelConnectivityStateChange( + _ connectivityState: ConnectivityState, + key: EndpointKey + ) { + let onChange = self.state.withLockedValue { state in + state.updateSubchannelConnectivityState(connectivityState, key: key) + } + + switch onChange { + case .publishStateChange(let aggregateState): + self.event.continuation.yield(.connectivityStateChanged(aggregateState)) + + case .closeAndPublishStateChange(let subchannel, let aggregateState): + self.event.continuation.yield(.connectivityStateChanged(aggregateState)) + subchannel.close() + + case .close(let subchannel): + subchannel.close() + + case .closed: + // All subchannels are closed; finish the streams so the run loop exits. + self.event.continuation.finish() + self.input.continuation.finish() + + case .none: + () + } + } + + private func handleSubchannelGoingAway(key: EndpointKey) { + switch self.state.withLockedValue({ $0.parkSubchannel(withKey: key) }) { + case .closeAndUpdateState(_, let connectivityState): + // No need to close the subchannel, it's already going away and will close itself. + if let connectivityState = connectivityState { + self.event.continuation.yield(.connectivityStateChanged(connectivityState)) + } + case .none: + () + } + } + + private func handleCloseInput() { + switch self.state.withLockedValue({ $0.close() }) { + case .closeSubchannels(let subchannels): + // Publish a new shutdown state, this LB is no longer usable for new RPCs. + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + + // Close the subchannels. + for subchannel in subchannels { + subchannel.close() + } + + case .closed: + // No subchannels to close. + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + self.event.continuation.finish() + self.input.continuation.finish() + + case .none: + () + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension RoundRobinLoadBalancer { + private enum State { + case active(Active) + case closing(Closing) + case closed + + struct Active { + private(set) var aggregateConnectivityState: ConnectivityState + private var picker: Picker? + + var endpoints: [Endpoint] + var subchannels: [EndpointKey: SubchannelState] + var parkedSubchannels: [EndpointKey: Subchannel] + + init() { + self.endpoints = [] + self.subchannels = [:] + self.parkedSubchannels = [:] + self.aggregateConnectivityState = .idle + self.picker = nil + } + + mutating func updateConnectivityState( + _ state: ConnectivityState, + key: EndpointKey + ) -> OnSubchannelConnectivityStateUpdate { + if let changed = self.subchannels[key]?.updateState(state) { + guard changed else { return .none } + + let subchannelToClose: Subchannel? + + switch state { + case .ready: + if let index = self.subchannels.firstIndex(where: { $0.value.markedForRemoval }) { + let (key, subchannelState) = self.subchannels.remove(at: index) + self.parkedSubchannels[key] = subchannelState.subchannel + subchannelToClose = subchannelState.subchannel + } else { + subchannelToClose = nil + } + + case .idle, .connecting, .transientFailure, .shutdown: + subchannelToClose = nil + } + + let aggregateState = self.refreshPickerAndAggregateState() + + switch (subchannelToClose, aggregateState) { + case (.some(let subchannel), .some(let state)): + return .closeAndPublishStateChange(subchannel, state) + case (.some(let subchannel), .none): + return .close(subchannel) + case (.none, .some(let state)): + return .publishStateChange(state) + case (.none, .none): + return .none + } + } else { + switch state { + case .idle, .connecting, .ready, .transientFailure: + () + case .shutdown: + self.parkedSubchannels.removeValue(forKey: key) + } + + return .none + } + } + + mutating func refreshPickerAndAggregateState() -> ConnectivityState? { + let ready = self.subchannels.values.compactMap { $0.state == .ready ? $0.subchannel : nil } + self.picker = Picker(subchannels: ready) + + let aggregate = ConnectivityState.aggregate(self.subchannels.values.map { $0.state }) + if aggregate == self.aggregateConnectivityState { + return nil + } else { + self.aggregateConnectivityState = aggregate + return aggregate + } + } + + mutating func pick() -> Subchannel? { + self.picker?.pick() + } + + mutating func markForRemoval( + _ keys: some Sequence, + numberToRemoveNow: Int + ) -> [Subchannel] { + var numberToRemoveNow = numberToRemoveNow + var keyIterator = keys.makeIterator() + var subchannelsToClose = [Subchannel]() + + while numberToRemoveNow > 0, let key = keyIterator.next() { + if let subchannelState = self.subchannels.removeValue(forKey: key) { + numberToRemoveNow -= 1 + self.parkedSubchannels[key] = subchannelState.subchannel + subchannelsToClose.append(subchannelState.subchannel) + } + } + + while let key = keyIterator.next() { + self.subchannels[key]?.markForRemoval() + } + + return subchannelsToClose + } + + mutating func registerSubchannels( + withKeys keys: some Sequence, + _ makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel + ) -> [Subchannel] { + var subchannels = [Subchannel]() + + for key in keys { + let subchannel = makeSubchannel(key.endpoint, SubchannelID()) + subchannels.append(subchannel) + self.subchannels[key] = SubchannelState(subchannel: subchannel) + } + + return subchannels + } + } + + struct Closing { + enum Reason: Sendable, Hashable { + case goAway + case user + } + + var reason: Reason + var parkedSubchannels: [EndpointKey: Subchannel] + + mutating func updateConnectivityState(_ state: ConnectivityState, key: EndpointKey) -> Bool { + switch state { + case .idle, .connecting, .ready, .transientFailure: + () + case .shutdown: + self.parkedSubchannels.removeValue(forKey: key) + } + + return self.parkedSubchannels.isEmpty + } + } + + struct SubchannelState { + var subchannel: Subchannel + var state: ConnectivityState + var markedForRemoval: Bool + + init(subchannel: Subchannel) { + self.subchannel = subchannel + self.state = .idle + self.markedForRemoval = false + } + + mutating func updateState(_ newState: ConnectivityState) -> Bool { + // The transition from transient failure to connecting is ignored. + // + // See: https://github.com/grpc/grpc/blob/master/doc/load-balancing.md + if self.state == .transientFailure, newState == .connecting { + return false + } + + let oldState = self.state + self.state = newState + return oldState != newState + } + + mutating func markForRemoval() { + self.markedForRemoval = true + } + } + + struct Picker { + private var subchannels: [Subchannel] + private var index: Int + + init?(subchannels: [Subchannel]) { + if subchannels.isEmpty { return nil } + + self.subchannels = subchannels + self.index = (0 ..< subchannels.count).randomElement()! + } + + mutating func pick() -> Subchannel { + defer { + self.index = (self.index + 1) % self.subchannels.count + } + return self.subchannels[self.index] + } + } + + mutating func updateSubchannels( + newEndpoints: Set, + makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel + ) -> (run: [Subchannel], close: [Subchannel], newState: ConnectivityState?) { + switch self { + case .active(var state): + let existingEndpoints = Set(state.subchannels.keys) + let keysToAdd = newEndpoints.subtracting(existingEndpoints) + let keysToRemove = existingEndpoints.subtracting(newEndpoints) + + if keysToRemove.isEmpty && keysToAdd.isEmpty { + // Nothing to do. + return (run: [], close: [], newState: nil) + } + + // The load balancer should keep subchannels to remove in service until new subchannels + // can replace each of them so that requests can continue to be served. + // + // If there are more keys to remove than to add, remove some now. + let numberToRemoveNow = max(keysToRemove.count - keysToAdd.count, 0) + + let removed = state.markForRemoval(keysToRemove, numberToRemoveNow: numberToRemoveNow) + let added = state.registerSubchannels(withKeys: keysToAdd, makeSubchannel) + + let newState = state.refreshPickerAndAggregateState() + self = .active(state) + return (run: added, close: removed, newState: newState) + + case .closing, .closed: + // Nothing to do. + return (run: [], close: [], newState: nil) + } + + } + + enum OnParkChannel { + case closeAndUpdateState(Subchannel, ConnectivityState?) + case none + } + + mutating func parkSubchannel(withKey key: EndpointKey) -> OnParkChannel { + switch self { + case .active(var state): + guard let subchannelState = state.subchannels.removeValue(forKey: key) else { + return .none + } + + // Parking the subchannel may invalidate the picker and the aggregate state, refresh both. + state.parkedSubchannels[key] = subchannelState.subchannel + let newState = state.refreshPickerAndAggregateState() + self = .active(state) + return .closeAndUpdateState(subchannelState.subchannel, newState) + + case .closing, .closed: + return .none + } + } + + mutating func registerSubchannels( + withKeys keys: some Sequence, + _ makeSubchannel: (Endpoint) -> Subchannel + ) -> [Subchannel] { + switch self { + case .active(var state): + var subchannels = [Subchannel]() + + for key in keys { + let subchannel = makeSubchannel(key.endpoint) + subchannels.append(subchannel) + state.subchannels[key] = SubchannelState(subchannel: subchannel) + } + + self = .active(state) + return subchannels + + case .closing, .closed: + return [] + } + } + + enum OnSubchannelConnectivityStateUpdate { + case closeAndPublishStateChange(Subchannel, ConnectivityState) + case publishStateChange(ConnectivityState) + case close(Subchannel) + case closed + case none + } + + mutating func updateSubchannelConnectivityState( + _ connectivityState: ConnectivityState, + key: EndpointKey + ) -> OnSubchannelConnectivityStateUpdate { + switch self { + case .active(var state): + let result = state.updateConnectivityState(connectivityState, key: key) + self = .active(state) + return result + + case .closing(var state): + if state.updateConnectivityState(connectivityState, key: key) { + self = .closed + return .closed + } else { + self = .closing(state) + return .none + } + + case .closed: + return .none + } + } + + enum OnClose { + case closeSubchannels([Subchannel]) + case closed + case none + } + + mutating func close() -> OnClose { + switch self { + case .active(var active): + var subchannelsToClose = [Subchannel]() + + for (id, subchannelState) in active.subchannels { + subchannelsToClose.append(subchannelState.subchannel) + active.parkedSubchannels[id] = subchannelState.subchannel + } + + if subchannelsToClose.isEmpty { + self = .closed + return .closed + } else { + self = .closing(Closing(reason: .user, parkedSubchannels: active.parkedSubchannels)) + return .closeSubchannels(subchannelsToClose) + } + + case .closing, .closed: + return .none + } + } + + enum OnPickSubchannel { + case picked(Subchannel) + case notAvailable([Subchannel]) + } + + mutating func pickSubchannel() -> OnPickSubchannel { + let onMakeStream: OnPickSubchannel + + switch self { + case .active(var active): + if let subchannel = active.pick() { + onMakeStream = .picked(subchannel) + } else { + switch active.aggregateConnectivityState { + case .idle: + onMakeStream = .notAvailable(active.subchannels.values.map { $0.subchannel }) + case .connecting, .ready, .transientFailure, .shutdown: + onMakeStream = .notAvailable([]) + } + } + self = .active(active) + + case .closing, .closed: + onMakeStream = .notAvailable([]) + } + + return onMakeStream + } + } +} + +extension ConnectivityState { + static func aggregate(_ states: some Collection) -> ConnectivityState { + // See https://github.com/grpc/grpc/blob/master/doc/load-balancing.md + + // If any one subchannel is in READY state, the channel's state is READY. + if states.contains(where: { $0 == .ready }) { + return .ready + } + + // Otherwise, if there is any subchannel in state CONNECTING, the channel's state is CONNECTING. + if states.contains(where: { $0 == .connecting }) { + return .connecting + } + + // Otherwise, if there is any subchannel in state IDLE, the channel's state is IDLE. + if states.contains(where: { $0 == .idle }) { + return .idle + } + + // Otherwise, if all subchannels are in state TRANSIENT_FAILURE, the channel's state + // is TRANSIENT_FAILURE. + if states.allSatisfy({ $0 == .transientFailure }) { + return .transientFailure + } + + return .shutdown + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift index bba747c44..87f888626 100644 --- a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift +++ b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift @@ -37,3 +37,11 @@ struct SubchannelID: Hashable, Sendable, CustomStringConvertible { "subchan_\(self.id)" } } + +/// A process-unique ID for a load-balancer. +struct LoadBalancerID: Hashable, Sendable, CustomStringConvertible { + private let id = ProcessUniqueID() + var description: String { + "lb_\(self.id)" + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift new file mode 100644 index 000000000..f9f969228 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift @@ -0,0 +1,483 @@ +/* + * 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 Atomics +import GRPCCore +@_spi(Package) @testable import GRPCHTTP2Core +import NIOHTTP2 +import NIOPosix +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class RoundRobinLoadBalancerTests: XCTestCase { + func testMultipleConnectionsAreEstablished() async throws { + try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + // Update the addresses for the load balancer, this will trigger subchannels to be created + // for each. + let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } + context.loadBalancer.updateAddresses(endpoints) + + case .connectivityStateChanged(.ready): + // Poll until each server has one connected client. + try await XCTPoll(every: .milliseconds(10)) { + context.servers.allSatisfy { server, _ in server.clients.count == 1 } + } + + // Close to end the test. + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testSubchannelsArePickedEvenly() async throws { + try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + // Update the addresses for the load balancer, this will trigger subchannels to be created + // for each. + let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } + context.loadBalancer.updateAddresses(endpoints) + + case .connectivityStateChanged(.ready): + // Subchannel is ready. This happens when any subchannel becomes ready. Loop until + // we can pick three distinct subchannels. + try await XCTPoll(every: .milliseconds(10)) { + var subchannelIDs = Set() + for _ in 0 ..< 3 { + let subchannel = try XCTUnwrap(context.loadBalancer.pickSubchannel()) + subchannelIDs.insert(subchannel.id) + } + return subchannelIDs.count == 3 + } + + // Now that all are ready, load should be distributed evenly among them. + var counts = [SubchannelID: Int]() + + for round in 1 ... 10 { + for _ in 1 ... 3 { + if let subchannel = context.loadBalancer.pickSubchannel() { + counts[subchannel.id, default: 0] += 1 + } else { + XCTFail("Didn't pick subchannel from ready load balancer") + } + } + + XCTAssertEqual(counts.count, 3, "\(counts)") + XCTAssert(counts.values.allSatisfy({ $0 == round }), "\(counts)") + } + + // Close to finish the test. + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testAddressUpdatesAreHandledGracefully() async throws { + try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + // Do the first connect. + let endpoints = [Endpoint(addresses: [context.servers[0].address])] + context.loadBalancer.updateAddresses(endpoints) + + case .connectivityStateChanged(.ready): + // Now the first connection should be established. + do { + try await XCTPoll(every: .milliseconds(10)) { + context.servers[0].server.clients.count == 1 + } + } + + // First connection is okay, add a second. + do { + let endpoints = [ + Endpoint(addresses: [context.servers[0].address]), + Endpoint(addresses: [context.servers[1].address]), + ] + context.loadBalancer.updateAddresses(endpoints) + + try await XCTPoll(every: .milliseconds(10)) { + context.servers.prefix(2).allSatisfy { $0.server.clients.count == 1 } + } + } + + // Remove those two endpoints and add a third. + do { + let endpoints = [Endpoint(addresses: [context.servers[2].address])] + context.loadBalancer.updateAddresses(endpoints) + + try await XCTPoll(every: .milliseconds(10)) { + let disconnected = context.servers.prefix(2).allSatisfy { $0.server.clients.isEmpty } + let connected = context.servers.last!.server.clients.count == 1 + return disconnected && connected + } + } + + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + // Transitioning to new addresses should be graceful, i.e. a complete change shouldn't + // result in dropping away from the ready state. + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testSameAddressUpdatesAreIgnored() async throws { + try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } + context.loadBalancer.updateAddresses(endpoints) + + case .connectivityStateChanged(.ready): + // Update with the same addresses, these should be ignored. + let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } + context.loadBalancer.updateAddresses(endpoints) + + // We should still have three connections. + try await XCTPoll(every: .milliseconds(10)) { + context.servers.allSatisfy { $0.server.clients.count == 1 } + } + + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testEmptyAddressUpdatesAreIgnored() async throws { + try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } + context.loadBalancer.updateAddresses(endpoints) + + case .connectivityStateChanged(.ready): + // Update with no-addresses, should be ignored so a subchannel can still be picked. + context.loadBalancer.updateAddresses([]) + + // We should still have three connections. + try await XCTPoll(every: .milliseconds(10)) { + context.servers.allSatisfy { $0.server.clients.count == 1 } + } + + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testSubchannelReceivesGoAway() async throws { + try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + // Trigger the connect. + let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } + context.loadBalancer.updateAddresses(endpoints) + + case .connectivityStateChanged(.ready): + // Wait for all servers to become ready. + try await XCTPoll(every: .milliseconds(10)) { + context.servers.allSatisfy { $0.server.clients.count == 1 } + } + + // The above only checks whether each server has a client, the test relies on all three + // subchannels being ready, poll until we get three distinct IDs. + var ids = Set() + try await XCTPoll(every: .milliseconds(10)) { + for _ in 1 ... 3 { + if let subchannel = context.loadBalancer.pickSubchannel() { + ids.insert(subchannel.id) + } + } + return ids.count == 3 + } + + // Pick the first server and send a GOAWAY to the client. + let client = context.servers[0].server.clients[0] + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway(lastStreamID: 0, errorCode: .cancel, opaqueData: nil) + ) + + // Send a GOAWAY, this should eventually close the subchannel and trigger a name + // resolution. + client.writeAndFlush(goAway, promise: nil) + + case .requiresNameResolution: + // One subchannel should've been taken out, meaning we can only pick from the remaining two: + let id1 = try XCTUnwrap(context.loadBalancer.pickSubchannel()?.id) + let id2 = try XCTUnwrap(context.loadBalancer.pickSubchannel()?.id) + let id3 = try XCTUnwrap(context.loadBalancer.pickSubchannel()?.id) + XCTAssertNotEqual(id1, id2) + XCTAssertEqual(id1, id3) + + // End the test. + context.loadBalancer.close() + + default: + () + } + + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .requiresNameResolution, + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testPickSubchannelWhenNotReady() { + let loadBalancer = RoundRobinLoadBalancer( + connector: .never, + backoff: .defaults, + defaultCompression: .none, + enabledCompression: .none + ) + + XCTAssertNil(loadBalancer.pickSubchannel()) + } + + func testPickSubchannelWhenClosed() async { + let loadBalancer = RoundRobinLoadBalancer( + connector: .never, + backoff: .defaults, + defaultCompression: .none, + enabledCompression: .none + ) + + loadBalancer.close() + await loadBalancer.run() + + XCTAssertNil(loadBalancer.pickSubchannel()) + } + + func testPickOnIdleLoadBalancerTriggersConnect() async throws { + let idle = ManagedAtomic(0) + let ready = ManagedAtomic(0) + + try await RoundRobinLoadBalancerTest.run( + servers: 1, + connector: .posix(maxIdleTime: .milliseconds(25)) // Aggressively idle the connection + ) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let idleCount = idle.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) + + switch idleCount { + case 1: + // The first idle happens when the load balancer in started, give it a set of addresses + // which it will connect to. Wait for it to be ready and then idle again. + let address = context.servers[0].address + let endpoints = [Endpoint(addresses: [address])] + context.loadBalancer.updateAddresses(endpoints) + + case 2: + // Load-balancer has the endpoints but all are idle. Picking will trigger a connect. + XCTAssertNil(context.loadBalancer.pickSubchannel()) + + case 3: + // Connection idled again. Shut it down. + context.loadBalancer.close() + + default: + XCTFail("Became idle too many times") + } + + case .connectivityStateChanged(.ready): + let readyCount = ready.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) + + if readyCount == 2 { + XCTAssertNotNil(context.loadBalancer.pickSubchannel()) + } + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.idle), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +enum RoundRobinLoadBalancerTest { + struct Context { + let servers: [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)] + let loadBalancer: RoundRobinLoadBalancer + } + + static func run( + servers serverCount: Int, + connector: any HTTP2Connector, + backoff: ConnectionBackoff = .defaults, + timeout: Duration = .seconds(10), + function: String = #function, + handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, + verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in } + ) async throws { + enum TestEvent { + case timedOut + case completed(Result) + } + + try await withThrowingTaskGroup(of: TestEvent.self) { group in + group.addTask { + try? await Task.sleep(for: timeout) + return .timedOut + } + + group.addTask { + do { + try await Self._run( + servers: serverCount, + connector: connector, + backoff: backoff, + handleEvent: handleEvent, + verifyEvents: verifyEvents + ) + return .completed(.success(())) + } catch { + return .completed(.failure(error)) + } + } + + let result = try await group.next()! + group.cancelAll() + + switch result { + case .timedOut: + XCTFail("'\(function)' timed out after \(timeout)") + case .completed(let result): + try result.get() + } + } + } + + private static func _run( + servers serverCount: Int, + connector: some HTTP2Connector, + backoff: ConnectionBackoff, + handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, + verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void + ) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + // Create the test servers. + var servers = [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)]() + for _ in 1 ... serverCount { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + servers.append((server, address)) + + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + } + + // Create the load balancer. + let loadBalancer = RoundRobinLoadBalancer( + connector: connector, + backoff: backoff, + defaultCompression: .none, + enabledCompression: .none + ) + + group.addTask { + await loadBalancer.run() + } + + let context = Context(servers: servers, loadBalancer: loadBalancer) + + var events = [LoadBalancerEvent]() + for await event in loadBalancer.events { + events.append(event) + try await handleEvent(context, event) + } + + verifyEvents(events) + group.cancelAll() + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift index 4d4004026..a63001413 100644 --- a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift @@ -42,4 +42,10 @@ final class ProcessUniqueIDTests: XCTestCase { let description = String(describing: id) XCTAssert(description.hasPrefix("subchan_")) } + + func testLoadBalancerIDDescription() { + let id = LoadBalancerID() + let description = String(describing: id) + XCTAssert(description.hasPrefix("lb_")) + } } diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift new file mode 100644 index 000000000..1183b9df1 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift @@ -0,0 +1,37 @@ +/* + * 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. + */ + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Task where Success == Never, Failure == Never { + static func poll( + every interval: Duration, + timeLimit: Duration = .seconds(5), + until predicate: () async throws -> Bool + ) async throws -> Bool { + var start = ContinuousClock.now + let end = start.advanced(by: timeLimit) + + while end > .now { + let canReturn = try await predicate() + if canReturn { return true } + + start = start.advanced(by: interval) + try await Task.sleep(until: start) + } + + return false + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift index 6103f1b27..1e0bb3515 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift @@ -67,3 +67,13 @@ func XCTAssert(_ value: Any, as type: T.Type, _ verify: (T) throws -> Void) r XCTFail("\(value) couldn't be cast to \(T.self)") } } + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +func XCTPoll( + every interval: Duration, + timeLimit: Duration = .seconds(5), + until predicate: () async throws -> Bool +) async throws { + let becameTrue = try await Task.poll(every: interval, timeLimit: timeLimit, until: predicate) + XCTAssertTrue(becameTrue, "Predicate didn't return true within \(timeLimit)") +} From 0b47aeac54cc5d3dba04fa12b4eb5a9a990b0a63 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 16 May 2024 11:05:41 +0100 Subject: [PATCH 309/580] Make ServerTransport protocol possible to use with structured concurrency (#1865) Motivation: The existing `ServerProtocol` has some design issues: its `listen` method returns the async sequence of streams to be handled, while at the same time it's `async` and must run for as long as the transport is up and running. This means we'll never get the sequence of streams back in most transport implementations (such as H2). Modifications: This PR tries to solve this by changing the protocol so that the `listen` method now takes a closure which allows handling an individual stream. This forces the transport implementations to maintain the right structured task hierarchy, as tasks to run this stream handler will have to be added as children of the long-running "listen" task. Result: Server Transports can now be used in structured concurrency. --- Package.swift | 2 +- Sources/GRPCCore/GRPCServer.swift | 216 +++--------------- Sources/GRPCCore/RuntimeError.swift | 12 - .../GRPCCore/Transport/ServerTransport.swift | 11 +- ...swift => GRPCHTTP2TransportNIOPosix.swift} | 6 + ...CHTTP2TransportNIOTransportServices.swift} | 6 + .../InProcessClientTransport.swift | 2 +- .../InProcessServerTransport.swift | 22 +- .../InProcessTransport.swift | 2 +- .../performance-worker/WorkerService.swift | 15 +- ...ientRPCExecutorTestHarness+Transport.swift | 2 +- .../ClientRPCExecutorTestHarness.swift | 13 +- ...PCExecutorTestHasness+ServerBehavior.swift | 4 +- .../ClientRPCExecutorTests+Hedging.swift | 2 +- .../ClientRPCExecutorTests+Retries.swift | 2 +- .../Internal/ClientRPCExecutorTests.swift | 2 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 6 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 69 +----- Tests/GRPCCoreTests/RuntimeErrorTests.swift | 12 +- .../Transport/AnyTransport.swift | 26 +-- .../Transport/StreamCountingTransport.swift | 12 +- .../Transport/ThrowingTransport.swift | 7 +- .../InProcessClientTransportTests.swift | 8 +- .../InProcessServerTransportTests.swift | 74 +++--- .../InProcessInteroperabilityTests.swift | 4 +- 25 files changed, 167 insertions(+), 370 deletions(-) rename Sources/GRPCHTTP2TransportNIOPosix/{Empty.swift => GRPCHTTP2TransportNIOPosix.swift} (83%) rename Sources/GRPCHTTP2TransportNIOTransportServices/{Empty.swift => GRPCHTTP2TransportNIOTransportServices.swift} (82%) diff --git a/Package.swift b/Package.swift index 4b10151a0..c97a915f7 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-nio-http2.git", - from: "1.24.1" + from: "1.31.0" ), .package( url: "https://github.com/apple/swift-nio-transport-services.git", diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 800759c59..19021e684 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -23,8 +23,8 @@ import Atomics /// streams to a service to handle the RPC or rejects them with an appropriate error if no service /// can handle the RPC. /// -/// A ``GRPCServer`` may listen with multiple transports (for example, HTTP/2 and in-process) and route -/// requests from each transport to the same service instance. You can also use "interceptors", +/// A ``GRPCServer`` listens with a specific transport implementation (for example, HTTP/2 or in-process), +/// and routes requests from the transport to the service instance. You can also use "interceptors", /// to implement cross-cutting logic which apply to all accepted RPCs. Example uses of interceptors /// include request filtering, authentication, and logging. Once requests have been intercepted /// they are passed to a handler which in turn returns a response to send back to the client. @@ -46,7 +46,7 @@ import Atomics /// /// // Finally create the server. /// let server = GRPCServer( -/// transports: [inProcessTransport], +/// transport: inProcessTransport, /// services: [greeter, echo], /// interceptors: [statsRecorder] /// ) @@ -54,9 +54,9 @@ import Atomics /// /// ## Starting and stopping the server /// -/// Once you have configured the server call ``run()`` to start it. Calling ``run()`` starts each -/// of the server's transports. A ``RuntimeError`` is thrown if any of the transports can't be -/// started. +/// Once you have configured the server call ``run()`` to start it. Calling ``run()`` starts the server's +/// transport too. A ``RuntimeError`` is thrown if the transport can't be started or encounters some other +/// runtime error. /// /// ```swift /// // Start running the server. @@ -73,9 +73,8 @@ import Atomics public struct GRPCServer: Sendable { typealias Stream = RPCStream - /// A collection of ``ServerTransport`` implementations that the server uses to listen - /// for new requests. - private let transports: [any ServerTransport] + /// The ``ServerTransport`` implementation that the server uses to listen for new requests. + private let transport: any ServerTransport /// The services registered which the server is serving. private let router: RPCRouter @@ -92,11 +91,8 @@ public struct GRPCServer: Sendable { private let state: ManagedAtomic private enum State: UInt8, AtomicValue { - /// The server hasn't been started yet. Can transition to `starting` or `stopped`. + /// The server hasn't been started yet. Can transition to `running` or `stopped`. case notStarted - /// The server is starting but isn't accepting requests yet. Can transition to `running` - /// and `stopping`. - case starting /// The server is running and accepting RPCs. Can transition to `stopping`. case running /// The server is stopping and no new RPCs will be accepted. Existing RPCs may run to @@ -110,7 +106,7 @@ public struct GRPCServer: Sendable { /// Creates a new server with no resources. /// /// - Parameters: - /// - transports: The transports the server should listen on. + /// - transport: The transport the server should listen on. /// - services: Services offered by the server. /// - interceptors: A collection of interceptors providing cross-cutting functionality to each /// accepted RPC. The order in which interceptors are added reflects the order in which they @@ -118,7 +114,7 @@ public struct GRPCServer: Sendable { /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. public init( - transports: [any ServerTransport], + transport: any ServerTransport, services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [] ) { @@ -127,13 +123,13 @@ public struct GRPCServer: Sendable { service.registerMethods(with: &router) } - self.init(transports: transports, router: router, interceptors: interceptors) + self.init(transport: transport, router: router, interceptors: interceptors) } /// Creates a new server with no resources. /// /// - Parameters: - /// - transports: The transports the server should listen on. + /// - transport: The transport the server should listen on. /// - router: A ``RPCRouter`` used by the server to route accepted streams to method handlers. /// - interceptors: A collection of interceptors providing cross-cutting functionality to each /// accepted RPC. The order in which interceptors are added reflects the order in which they @@ -141,23 +137,23 @@ public struct GRPCServer: Sendable { /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. public init( - transports: [any ServerTransport], + transport: any ServerTransport, router: RPCRouter, interceptors: [any ServerInterceptor] = [] ) { self.state = ManagedAtomic(.notStarted) - self.transports = transports + self.transport = transport self.router = router self.interceptors = interceptors } - /// Starts the server and runs until all registered transports have closed. + /// Starts the server and runs until the registered transport has closed. /// - /// No RPCs are processed until all transports are listening. If a transport fails to start - /// listening then all open transports are closed and a ``RuntimeError`` is thrown. + /// No RPCs are processed until the configured transport is listening. If the transport fails to start + /// listening, or if it encounters a runtime error, then ``RuntimeError`` is thrown. /// - /// This function returns when all transports have stopped listening and all requests have been - /// handled. You can signal to transports that they should stop listening by calling + /// This function returns when the configured transport has stopped listening and all requests have been + /// handled. You can signal to the transport that it should stop listening by calling /// ``stopListening()``. The server will continue to process existing requests. /// /// To stop the server more abruptly you can cancel the task that this function is running in. @@ -167,7 +163,7 @@ public struct GRPCServer: Sendable { public func run() async throws { let (wasNotStarted, actualState) = self.state.compareExchange( expected: .notStarted, - desired: .starting, + desired: .running, ordering: .sequentiallyConsistent ) @@ -175,7 +171,7 @@ public struct GRPCServer: Sendable { switch actualState { case .notStarted: fatalError() - case .starting, .running: + case .running: throw RuntimeError( code: .serverIsAlreadyRunning, message: "The server is already running and can only be started once." @@ -194,152 +190,16 @@ public struct GRPCServer: Sendable { self.state.store(.stopped, ordering: .sequentiallyConsistent) } - if self.transports.isEmpty { - throw RuntimeError( - code: .noTransportsConfigured, - message: """ - Can't start server, no transports are configured. You must add at least one transport \ - to the server before calling 'run()'. - """ - ) - } - - var listeners: [RPCAsyncSequence] = [] - listeners.reserveCapacity(self.transports.count) - - for transport in self.transports { - do { - let listener = try await transport.listen() - listeners.append(listener) - } catch let cause { - // Failed to start, so start stopping. - self.state.store(.stopping, ordering: .sequentiallyConsistent) - // Some listeners may have started and have streams which need closing. - await self.rejectRequests(listeners) - - throw RuntimeError( - code: .failedToStartTransport, - message: """ - Server didn't start because the '\(type(of: transport))' transport threw an error \ - while starting. - """, - cause: cause - ) + do { + try await transport.listen { stream in + await self.router.handle(stream: stream, interceptors: self.interceptors) } - } - - // May have been told to stop listening while starting the transports. - let (wasStarting, _) = self.state.compareExchange( - expected: .starting, - desired: .running, - ordering: .sequentiallyConsistent - ) - - // If the server is stopping then notify the transport and then consume them: there may be - // streams opened at a lower level (e.g. HTTP/2) which are already open and need to be consumed. - if wasStarting { - await self.handleRequests(listeners) - } else { - await self.rejectRequests(listeners) - } - } - - private func rejectRequests(_ listeners: [RPCAsyncSequence]) async { - // Tell the active listeners to stop listening. - for transport in self.transports.prefix(listeners.count) { - transport.stopListening() - } - - // Drain any open streams on active listeners. - await withTaskGroup(of: Void.self) { group in - let unavailable = Status( - code: .unavailable, - message: "The server isn't ready to accept requests." + } catch { + throw RuntimeError( + code: .transportError, + message: "Server transport threw an error.", + cause: error ) - - for listener in listeners { - do { - for try await stream in listener { - group.addTask { - try? await stream.outbound.write(.status(unavailable, [:])) - stream.outbound.finish() - } - } - } catch { - // Suppress any errors, the original error from the transport which failed to start - // should be thrown. - } - } - } - } - - private func handleRequests(_ listeners: [RPCAsyncSequence]) async { - #if swift(>=5.9) - if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { - await self.handleRequestsInDiscardingTaskGroup(listeners) - } else { - await self.handleRequestsInTaskGroup(listeners) - } - #else - await self.handleRequestsInTaskGroup(listeners) - #endif - } - - #if swift(>=5.9) - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) - private func handleRequestsInDiscardingTaskGroup(_ listeners: [RPCAsyncSequence]) async { - await withDiscardingTaskGroup { group in - for listener in listeners { - group.addTask { - await withDiscardingTaskGroup { subGroup in - do { - for try await stream in listener { - subGroup.addTask { - await self.router.handle(stream: stream, interceptors: self.interceptors) - } - } - } catch { - // If the listener threw then the connection must be broken, cancel all work. - subGroup.cancelAll() - } - } - } - } - } - } - #endif - - private func handleRequestsInTaskGroup(_ listeners: [RPCAsyncSequence]) async { - // If the discarding task group isn't available then fall back to using a regular task group - // with a limit on subtasks. Most servers will use an HTTP/2 based transport, most - // implementations limit connections to 100 concurrent streams. A limit of 4096 gives the server - // scope to handle nearly 41 completely saturated connections. - let maxConcurrentSubTasks = 4096 - let tasks = ManagedAtomic(0) - - await withTaskGroup(of: Void.self) { group in - for listener in listeners { - group.addTask { - await withTaskGroup(of: Void.self) { subGroup in - do { - for try await stream in listener { - let taskCount = tasks.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) - if taskCount >= maxConcurrentSubTasks { - _ = await subGroup.next() - tasks.wrappingDecrement(ordering: .sequentiallyConsistent) - } - - subGroup.addTask { - await self.router.handle(stream: stream, interceptors: self.interceptors) - } - } - } catch { - // If the listener threw then the connection must be broken, cancel all work. - subGroup.cancelAll() - } - } - } - } } } @@ -357,9 +217,7 @@ public struct GRPCServer: Sendable { ) if wasRunning { - for transport in self.transports { - transport.stopListening() - } + self.transport.stopListening() } else { switch actual { case .notStarted: @@ -374,18 +232,6 @@ public struct GRPCServer: Sendable { self.stopListening() } - case .starting: - let (exchanged, _) = self.state.compareExchange( - expected: .starting, - desired: .stopping, - ordering: .sequentiallyConsistent - ) - - // Lost a race with 'run()', try again. - if !exchanged { - self.stopListening() - } - case .running: // Unreachable, this branch only happens when the initial exchange didn't take place. fatalError() diff --git a/Sources/GRPCCore/RuntimeError.swift b/Sources/GRPCCore/RuntimeError.swift index 2af8dc02a..d2791a873 100644 --- a/Sources/GRPCCore/RuntimeError.swift +++ b/Sources/GRPCCore/RuntimeError.swift @@ -110,8 +110,6 @@ extension RuntimeError { case invalidArgument case serverIsAlreadyRunning case serverIsStopped - case failedToStartTransport - case noTransportsConfigured case clientIsAlreadyRunning case clientIsStopped case transportError @@ -137,16 +135,6 @@ extension RuntimeError { Self(.serverIsStopped) } - /// The server couldn't be started because a transport failed to start. - public static var failedToStartTransport: Self { - Self(.failedToStartTransport) - } - - /// The server couldn't be started because no transports were configured. - public static var noTransportsConfigured: Self { - Self(.noTransportsConfigured) - } - /// At attempt to start the client was made but it is already running. public static var clientIsAlreadyRunning: Self { Self(.clientIsAlreadyRunning) diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index 05ca4e9ef..522be9670 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -14,22 +14,25 @@ * limitations under the License. */ +/// A protocol server transport implementations must conform to. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol ServerTransport: Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable - /// Starts the transport and returns a sequence of accepted streams to handle. + /// Starts the transport. /// /// Implementations will typically bind to a listening port when this function is called - /// and start accepting new connections. Each accepted inbound RPC stream should be published - /// to the async sequence returned by the function. + /// and start accepting new connections. Each accepted inbound RPC stream will be handed over to + /// the provided `streamHandler` to handle accordingly. /// /// You can call ``stopListening()`` to stop the transport from accepting new streams. Existing /// streams must be allowed to complete naturally. However, transports may also enforce a grace /// period after which any open streams may be cancelled. You can also cancel the task running /// ``listen()`` to abruptly close connections and streams. - func listen() async throws -> RPCAsyncSequence> + func listen( + _ streamHandler: @escaping (RPCStream) async -> Void + ) async throws /// Indicates to the transport that no new streams should be accepted. /// diff --git a/Sources/GRPCHTTP2TransportNIOPosix/Empty.swift b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift similarity index 83% rename from Sources/GRPCHTTP2TransportNIOPosix/Empty.swift rename to Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift index 43d5caa47..8f707b798 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/Empty.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift @@ -13,3 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import GRPCCore + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct GRPCHTTP2TransportNIOPosix { +} diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/Empty.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2TransportNIOTransportServices.swift similarity index 82% rename from Sources/GRPCHTTP2TransportNIOTransportServices/Empty.swift rename to Sources/GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2TransportNIOTransportServices.swift index 43d5caa47..a1e1d80a7 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/Empty.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2TransportNIOTransportServices.swift @@ -13,3 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import GRPCCore + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct GRPCHTTP2TransportNIOTransportServices { +} diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index b513541f9..34a6f6975 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -35,7 +35,7 @@ import GRPCCore /// block until ``connect()`` is called or the task is cancelled. /// /// - SeeAlso: ``ClientTransport`` -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public struct InProcessClientTransport: ClientTransport { private enum State: Sendable { struct UnconnectedState { diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 845d38c19..85df88b74 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -26,7 +26,7 @@ import GRPCCore /// To stop listening to new requests, call ``stopListening()``. /// /// - SeeAlso: ``ClientTransport`` -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public struct InProcessServerTransport: ServerTransport, Sendable { public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable @@ -39,8 +39,8 @@ public struct InProcessServerTransport: ServerTransport, Sendable { (self.newStreams, self.newStreamsContinuation) = AsyncStream.makeStream() } - /// Publish a new ``RPCStream``, which will be returned by the transport's ``RPCAsyncSequence``, - /// returned when calling ``listen()``. + /// Publish a new ``RPCStream``, which will be returned by the transport's ``events`` + /// successful case. /// /// - Parameter stream: The new ``RPCStream`` to publish. /// - Throws: ``RPCError`` with code ``RPCError/Code-swift.struct/failedPrecondition`` @@ -55,12 +55,16 @@ public struct InProcessServerTransport: ServerTransport, Sendable { } } - /// Return a new ``RPCAsyncSequence`` that will contain all published ``RPCStream``s published - /// to this transport using the ``acceptStream(_:)`` method. - /// - /// - Returns: An ``RPCAsyncSequence`` of all published ``RPCStream``s. - public func listen() async throws -> RPCAsyncSequence> { - RPCAsyncSequence(wrapping: self.newStreams) + public func listen( + _ streamHandler: @escaping (RPCStream) async -> Void + ) async throws { + await withDiscardingTaskGroup { group in + for await stream in self.newStreams { + group.addTask { + await streamHandler(stream) + } + } + } } /// Stop listening to any new ``RPCStream`` publications. diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index 53a8eb921..d6dc262a2 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -24,7 +24,7 @@ public enum InProcessTransport { /// - Parameters: /// - serviceConfig: Configuration describing how methods should be executed. /// - Returns: A tuple containing the connected server and client in-process transports. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public static func makePair( serviceConfig: ServiceConfig = ServiceConfig() ) -> (server: InProcessServerTransport, client: InProcessClientTransport) { diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 260cde4a1..09af07dd9 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -18,7 +18,7 @@ import GRPCCore import NIOConcurrencyHelpers import NIOCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable { private let state: NIOLockedValueBox @@ -284,10 +284,10 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension WorkerService { private func setupServer(_ config: Grpc_Testing_ServerConfig) async throws -> GRPCServer { - let server = GRPCServer(transports: [], services: [BenchmarkService()]) + let server = GRPCServer(transport: NoOpServerTransport(), services: [BenchmarkService()]) let stats = try await ServerStats() try self.state.withLockedValue { state in @@ -421,3 +421,12 @@ extension WorkerService { } } } + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct NoOpServerTransport: ServerTransport { + func listen( + _ streamHandler: @escaping (RPCStream) async -> Void + ) async throws {} + + func stopListening() {} +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index e02cb4ce5..18039ed97 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -17,7 +17,7 @@ import Atomics import GRPCCore import GRPCInProcessTransport -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension InProcessServerTransport { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 48026730a..c8f82c983 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -25,7 +25,7 @@ import XCTest /// of the server to allow for flexible testing scenarios with minimal boilerplate. The harness /// also tracks how many streams the client has opened, how many streams the server accepted, and /// how many streams the client failed to open. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) struct ClientRPCExecutorTestHarness { private let server: ServerStreamHandler private let clientTransport: StreamCountingClientTransport @@ -40,7 +40,7 @@ struct ClientRPCExecutorTestHarness { } var serverStreamsAccepted: Int { - self.serverTransport.acceptedStreams + self.serverTransport.acceptedStreamsCount } init(transport: Transport = .inProcess, server: ServerStreamHandler) { @@ -121,13 +121,8 @@ struct ClientRPCExecutorTestHarness { ) async throws { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await withThrowingTaskGroup(of: Void.self) { serverGroup in - let streams = try await self.serverTransport.listen() - for try await stream in streams { - serverGroup.addTask { - try await self.server.handle(stream: stream) - } - } + try await self.serverTransport.listen { stream in + try? await self.server.handle(stream: stream) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift index 41537e83f..d4ad99097 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension ClientRPCExecutorTestHarness { struct ServerStreamHandler: Sendable { private let handler: @@ -48,7 +48,7 @@ extension ClientRPCExecutorTestHarness { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { return Self { diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index b9126775e..94209a1d8 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension ClientRPCExecutorTests { func testHedgingWhenAllAttemptsResultInNonFatalCodes() async throws { let harness = ClientRPCExecutorTestHarness( diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index 91cc355d9..ccea73429 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension ClientRPCExecutorTests { fileprivate func makeHarnessForRetries( rejectUntilAttempt firstSuccessfulAttempt: Int, diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 65da3de7a..355a4fda5 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 42f238ce0..9cecfba71 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -18,7 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], @@ -27,7 +27,7 @@ final class GRPCClientTests: XCTestCase { ) async throws { let inProcess = InProcessTransport.makePair() let client = GRPCClient(transport: inProcess.client, interceptors: interceptors) - let server = GRPCServer(transports: [inProcess.server], services: services) + let server = GRPCServer(transport: inProcess.server, services: services) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -320,7 +320,7 @@ final class GRPCClientTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let server = GRPCServer(transports: [inProcess.server], services: [BinaryEcho()]) + let server = GRPCServer(transport: inProcess.server, services: [BinaryEcho()]) try await server.run() } diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 02028a1e8..507803dab 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -18,7 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], @@ -27,7 +27,7 @@ final class GRPCServerTests: XCTestCase { ) async throws { let inProcess = InProcessTransport.makePair() let server = GRPCServer( - transports: [inProcess.server], + transport: inProcess.server, services: services, interceptors: interceptors ) @@ -319,7 +319,7 @@ final class GRPCServerTests: XCTestCase { func testCancelRunningServer() async throws { let inProcess = InProcessTransport.makePair() let task = Task { - let server = GRPCServer(transports: [inProcess.server], services: [BinaryEcho()]) + let server = GRPCServer(transport: inProcess.server, services: [BinaryEcho()]) try await server.run() } @@ -337,17 +337,8 @@ final class GRPCServerTests: XCTestCase { } } - func testTestRunServerWithNoTransport() async throws { - let server = GRPCServer(transports: [], services: []) - await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { - try await server.run() - } errorHandler: { error in - XCTAssertEqual(error.code, .noTransportsConfigured) - } - } - func testTestRunStoppedServer() async throws { - let server = GRPCServer(transports: [InProcessServerTransport()], services: []) + let server = GRPCServer(transport: InProcessServerTransport(), services: []) // Run the server. let task = Task { try await server.run() } task.cancel() @@ -362,59 +353,11 @@ final class GRPCServerTests: XCTestCase { } func testRunServerWhenTransportThrows() async throws { - let server = GRPCServer(transports: [ThrowOnRunServerTransport()], services: []) + let server = GRPCServer(transport: ThrowOnRunServerTransport(), services: []) await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { try await server.run() } errorHandler: { error in - XCTAssertEqual(error.code, .failedToStartTransport) - } - } - - func testRunServerDrainsRunningTransportsWhenOneFailsToStart() async throws { - // Register the in process transport first and allow it to come up. - let inProcess = InProcessTransport.makePair() - - // Register a transport waits for a signal before throwing. - let signal = AsyncStream.makeStream(of: Void.self) - let server = GRPCServer( - transports: [ - inProcess.server, - ThrowOnSignalServerTransport(signal: signal.stream), - ], - services: [] - ) - - // Connect the in process client and start an RPC. When the stream is opened signal the - // other transport to throw. This stream should be failed by the server. - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await inProcess.client.connect() - } - - group.addTask { - try await inProcess.client.withStream( - descriptor: BinaryEcho.Methods.get, - options: .defaults - ) { stream in - // The stream is open to the in-process transport. Let the other transport start. - signal.continuation.finish() - try await stream.outbound.write(.metadata([:])) - stream.outbound.finish() - - let parts = try await stream.inbound.collect() - XCTAssertStatus(parts.first) { status, _ in - XCTAssertEqual(status.code, .unavailable) - } - } - } - - await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { - try await server.run() - } errorHandler: { error in - XCTAssertEqual(error.code, .failedToStartTransport) - } - - group.cancelAll() + XCTAssertEqual(error.code, .transportError) } } diff --git a/Tests/GRPCCoreTests/RuntimeErrorTests.swift b/Tests/GRPCCoreTests/RuntimeErrorTests.swift index 14cc56f4c..2d30f96f0 100644 --- a/Tests/GRPCCoreTests/RuntimeErrorTests.swift +++ b/Tests/GRPCCoreTests/RuntimeErrorTests.swift @@ -20,10 +20,10 @@ import XCTest final class RuntimeErrorTests: XCTestCase { func testCopyOnWrite() { // RuntimeError has a heap based storage, so check CoW semantics are correctly implemented. - let error1 = RuntimeError(code: .failedToStartTransport, message: "Failed to start transport") + let error1 = RuntimeError(code: .transportError, message: "Failed to start transport") var error2 = error1 error2.code = .serverIsAlreadyRunning - XCTAssertEqual(error1.code, .failedToStartTransport) + XCTAssertEqual(error1.code, .transportError) XCTAssertEqual(error2.code, .serverIsAlreadyRunning) var error3 = error1 @@ -38,17 +38,17 @@ final class RuntimeErrorTests: XCTestCase { } func testCustomStringConvertible() { - let error1 = RuntimeError(code: .failedToStartTransport, message: "Failed to start transport") - XCTAssertDescription(error1, #"failedToStartTransport: "Failed to start transport""#) + let error1 = RuntimeError(code: .transportError, message: "Failed to start transport") + XCTAssertDescription(error1, #"transportError: "Failed to start transport""#) let error2 = RuntimeError( - code: .failedToStartTransport, + code: .transportError, message: "Failed to start transport", cause: CancellationError() ) XCTAssertDescription( error2, - #"failedToStartTransport: "Failed to start transport" (cause: "CancellationError()")"# + #"transportError: "Failed to start transport" (cause: "CancellationError()")"# ) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index 27fd02e68..7fbd83a9a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -86,29 +86,19 @@ struct AnyServerTransport: ServerTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable - private let _listen: @Sendable () async throws -> RPCAsyncSequence> + private let _listen: + @Sendable (@escaping (RPCStream) async -> Void) async throws -> Void private let _stopListening: @Sendable () -> Void init(wrapping transport: Transport) { - self._listen = { - let mapped = try await transport.listen().map { stream in - return RPCStream( - descriptor: stream.descriptor, - inbound: RPCAsyncSequence(wrapping: stream.inbound), - outbound: RPCWriter.Closable(wrapping: stream.outbound) - ) - } - - return RPCAsyncSequence(wrapping: mapped) - } - - self._stopListening = { - transport.stopListening() - } + self._listen = { streamHandler in try await transport.listen(streamHandler) } + self._stopListening = { transport.stopListening() } } - func listen() async throws -> RPCAsyncSequence> { - try await self._listen() + func listen( + _ streamHandler: @escaping (RPCStream) async -> Void + ) async throws { + try await self._listen(streamHandler) } func stopListening() { diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 5a1260293..e1b52d45e 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -85,7 +85,7 @@ struct StreamCountingServerTransport: ServerTransport, Sendable { private let transport: AnyServerTransport private let _acceptedStreams = ManagedAtomic(0) - var acceptedStreams: Int { + var acceptedStreamsCount: Int { self._acceptedStreams.load(ordering: .sequentiallyConsistent) } @@ -93,13 +93,13 @@ struct StreamCountingServerTransport: ServerTransport, Sendable { self.transport = AnyServerTransport(wrapping: transport) } - func listen() async throws -> RPCAsyncSequence> { - let mapped = try await self.transport.listen().map { stream in + func listen( + _ streamHandler: @escaping (RPCStream) async -> Void + ) async throws { + try await self.transport.listen { stream in self._acceptedStreams.wrappingIncrement(ordering: .sequentiallyConsistent) - return stream + await streamHandler(stream) } - - return RPCAsyncSequence(wrapping: mapped) } func stopListening() { diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 844278494..80dd0741f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -53,7 +53,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ThrowOnRunServerTransport: ServerTransport { - func listen() async throws -> RPCAsyncSequence> { + func listen(_ streamHandler: (RPCStream) async -> Void) async throws { throw RPCError( code: .unavailable, message: "The '\(type(of: self))' transport is never available." @@ -73,8 +73,11 @@ struct ThrowOnSignalServerTransport: ServerTransport { self.signal = signal } - func listen() async throws -> RPCAsyncSequence> { + func listen( + _ streamHandler: (GRPCCore.RPCStream) async -> Void + ) async throws { for await _ in self.signal {} + throw RPCError( code: .unavailable, message: "The '\(type(of: self))' transport is never available." diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 2dfa6db00..f25174286 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -18,7 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class InProcessClientTransportTests: XCTestCase { struct FailTest: Error {} @@ -171,9 +171,9 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - for try await stream in try await server.listen() { - let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } - try await stream.outbound.write(RPCResponsePart.message([42])) + try await server.listen { stream in + let receivedMessages = try? await stream.inbound.reduce(into: []) { $0.append($1) } + try? await stream.outbound.write(RPCResponsePart.message([42])) stream.outbound.finish() XCTAssertEqual(receivedMessages, [.message([1])]) diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 78466ae8a..6e1991d9a 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -19,7 +19,7 @@ import XCTest @testable import GRPCCore @testable import GRPCInProcessTransport -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessServerTransport() @@ -38,14 +38,20 @@ final class InProcessServerTransportTests: XCTestCase { ) ) - let streamSequence = try await transport.listen() - var streamSequenceInterator = streamSequence.makeAsyncIterator() + let messages = LockedValueBox<[RPCRequestPart]?>(nil) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await transport.listen { stream in + let partValue = try? await stream.inbound.reduce(into: []) { $0.append($1) } + messages.withLockedValue { $0 = partValue } + transport.stopListening() + } + } - try transport.acceptStream(stream) + try transport.acceptStream(stream) + } - let testStream = try await streamSequenceInterator.next() - let messages = try await testStream?.inbound.reduce(into: []) { $0.append($1) } - XCTAssertEqual(messages, [.message([42])]) + XCTAssertEqual(messages.withLockedValue { $0 }, [.message([42])]) } func testStopListening() async throws { @@ -67,41 +73,39 @@ final class InProcessServerTransportTests: XCTestCase { ) ) - let streamSequence = try await transport.listen() - var streamSequenceInterator = streamSequence.makeAsyncIterator() - try transport.acceptStream(firstStream) - let firstTestStream = try await streamSequenceInterator.next() - let firstStreamMessages = try await firstTestStream?.inbound.reduce(into: []) { $0.append($1) } - XCTAssertEqual(firstStreamMessages, [.message([42])]) + try await transport.listen { stream in + let firstStreamMessages = try? await stream.inbound.reduce(into: []) { + $0.append($1) + } + XCTAssertEqual(firstStreamMessages, [.message([42])]) - transport.stopListening() + transport.stopListening() - let secondStream = RPCStream< - RPCAsyncSequence, RPCWriter.Closable - >( - descriptor: .init(service: "testService1", method: "testMethod1"), - inbound: RPCAsyncSequence( - wrapping: AsyncStream { - $0.yield(.message([42])) - $0.finish() - } - ), - outbound: .init( - wrapping: BufferedStream.Source( - storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) + let secondStream = RPCStream< + RPCAsyncSequence, RPCWriter.Closable + >( + descriptor: .init(service: "testService1", method: "testMethod1"), + inbound: RPCAsyncSequence( + wrapping: AsyncStream { + $0.yield(.message([42])) + $0.finish() + } + ), + outbound: .init( + wrapping: BufferedStream.Source( + storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) + ) ) ) - ) - XCTAssertThrowsError(ofType: RPCError.self) { - try transport.acceptStream(secondStream) - } errorHandler: { error in - XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertThrowsError(ofType: RPCError.self) { + try transport.acceptStream(secondStream) + } errorHandler: { error in + XCTAssertEqual(error.code, .failedPrecondition) + XCTAssertEqual(error.message, "The server transport is closed.") + } } - - let secondTestStream = try await streamSequenceInterator.next() - XCTAssertNil(secondTestStream) } } diff --git a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift index 20e4a9f56..19965cfd0 100644 --- a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift +++ b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift @@ -20,7 +20,7 @@ import XCTest @testable import InteroperabilityTests -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class InProcessInteroperabilityTests: XCTestCase { func runInProcessTransport( interopTestCase: InteroperabilityTestCase @@ -29,7 +29,7 @@ final class InProcessInteroperabilityTests: XCTestCase { let inProcess = InProcessTransport.makePair() try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - let server = GRPCServer(transports: [inProcess.server], services: [TestService()]) + let server = GRPCServer(transport: inProcess.server, services: [TestService()]) try await server.run() } From 8b6a8f4098fb2e5742a80e2d74176e6a69946d1d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 22 May 2024 16:36:21 +0100 Subject: [PATCH 310/580] Conform client/server connection handlers to h2 stream delegate (#1875) Motivation: The natural NIO API to use when dealing with HTTP/2 multiplexing doesn't emit user inbound events when streams are created and closed. Instead this is signalled via the `NIOHTTP2StreamDelegate`. Modifications: - Conform the `ClientConnectionHandler` and the `ServerConnectionManagementHandler` to `NIOHTTP2StreamDelegate`. - Update tests Result: `ClientConnectionHandler` and `ServerConnectionManagementHandler` can be used as stream delegates with NIO HTTP/2 is configured for async multiplexing. --- .../Connection/ClientConnectionHandler.swift | 69 ++++++++++++------- .../ServerConnectionManagementHandler.swift | 50 ++++++++++---- .../ClientConnectionHandlerTests.swift | 12 ++-- ...rverConnectionManagementHandlerTests.swift | 12 ++-- 4 files changed, 89 insertions(+), 54 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 00a254bc3..d270eba73 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -86,6 +86,9 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl /// Resets once `channelReadComplete` returns. private var inReadLoop: Bool + /// The context of the channel this handler is in. + private var context: ChannelHandlerContext? + /// Creates a new handler which manages the lifecycle of a connection. /// /// - Parameters: @@ -118,6 +121,11 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl func handlerAdded(context: ChannelHandlerContext) { assert(context.eventLoop === self.eventLoop) + self.context = context + } + + func handlerRemoved(context: ChannelHandlerContext) { + self.context = nil } func channelActive(context: ChannelHandlerContext) { @@ -144,31 +152,10 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { switch event { case let event as NIOHTTP2StreamCreatedEvent: - // Stream created, so the connection isn't idle. - self.maxIdleTimer?.cancel() - self.state.streamOpened(event.streamID) + self.streamCreated(event.streamID, channel: context.channel) case let event as StreamClosedEvent: - switch self.state.streamClosed(event.streamID) { - case .startIdleTimer(let cancelKeepalive): - // All streams are closed, restart the idle timer, and stop the keep-alive timer (it may - // not stop if keep-alive is allowed when there are no active calls). - self.maxIdleTimer?.schedule(on: context.eventLoop) { - self.maxIdleTimerFired(context: context) - } - - if cancelKeepalive { - self.keepaliveTimer?.cancel() - } - - case .close: - // Connection was closing but waiting for all streams to close. They must all be closed - // now so close the connection. - context.close(promise: nil) - - case .none: - () - } + self.streamClosed(event.streamID, channel: context.channel) default: () @@ -263,6 +250,42 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } } +extension ClientConnectionHandler: NIOHTTP2StreamDelegate { + func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { + self.eventLoop.assertInEventLoop() + + // Stream created, so the connection isn't idle. + self.maxIdleTimer?.cancel() + self.state.streamOpened(id) + } + + func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { + guard let context = self.context else { return } + self.eventLoop.assertInEventLoop() + + switch self.state.streamClosed(id) { + case .startIdleTimer(let cancelKeepalive): + // All streams are closed, restart the idle timer, and stop the keep-alive timer (it may + // not stop if keep-alive is allowed when there are no active calls). + self.maxIdleTimer?.schedule(on: context.eventLoop) { + self.maxIdleTimerFired(context: context) + } + + if cancelKeepalive { + self.keepaliveTimer?.cancel() + } + + case .close: + // Connection was closing but waiting for all streams to close. They must all be closed + // now so close the connection. + context.close(promise: nil) + + case .none: + () + } + } +} + extension ClientConnectionHandler { private func maybeFlush(context: ChannelHandlerContext) { if self.inReadLoop { diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index ab1bb4792..b144c4c8c 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -75,6 +75,9 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { /// Resets once `channelReadComplete` returns. private var inReadLoop: Bool + /// The context of the channel this handler is in. + private var context: ChannelHandlerContext? + /// The current state of the connection. private var state: StateMachine @@ -236,6 +239,11 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { func handlerAdded(context: ChannelHandlerContext) { assert(context.eventLoop === self.eventLoop) + self.context = context + } + + func handlerRemoved(context: ChannelHandlerContext) { + self.context = nil } func channelActive(context: ChannelHandlerContext) { @@ -266,23 +274,10 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { switch event { case let event as NIOHTTP2StreamCreatedEvent: - // The connection isn't idle if a stream is open. - self.maxIdleTimer?.cancel() - self.state.streamOpened(event.streamID) + self.streamCreated(event.streamID, channel: context.channel) case let event as StreamClosedEvent: - switch self.state.streamClosed(event.streamID) { - case .startIdleTimer: - self.maxIdleTimer?.schedule(on: context.eventLoop) { - self.initiateGracefulShutdown(context: context) - } - - case .close: - context.close(mode: .all, promise: nil) - - case .none: - () - } + self.streamClosed(event.streamID, channel: context.channel) default: () @@ -335,6 +330,31 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { } } +extension ServerConnectionManagementHandler: NIOHTTP2StreamDelegate { + func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { + // The connection isn't idle if a stream is open. + self.maxIdleTimer?.cancel() + self.state.streamOpened(id) + } + + func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { + guard let context = self.context else { return } + + switch self.state.streamClosed(id) { + case .startIdleTimer: + self.maxIdleTimer?.schedule(on: context.eventLoop) { + self.initiateGracefulShutdown(context: context) + } + + case .close: + context.close(mode: .all, promise: nil) + + case .none: + () + } + } +} + extension ServerConnectionManagementHandler { private func maybeFlush(context: ChannelHandlerContext) { if self.inReadLoop { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 9aac5098c..6483456d3 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -226,6 +226,7 @@ final class ClientConnectionHandlerTests: XCTestCase { extension ClientConnectionHandlerTests { struct Connection { let channel: EmbeddedChannel + let streamDelegate: any NIOHTTP2StreamDelegate var loop: EmbeddedEventLoop { self.channel.embeddedEventLoop } @@ -245,6 +246,7 @@ extension ClientConnectionHandlerTests { keepaliveWithoutCalls: allowKeepaliveWithoutCalls ) + self.streamDelegate = handler self.channel = EmbeddedChannel(handler: handler, loop: loop) } @@ -253,17 +255,11 @@ extension ClientConnectionHandlerTests { } func streamOpened(_ id: HTTP2StreamID) { - let event = NIOHTTP2StreamCreatedEvent( - streamID: id, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - self.channel.pipeline.fireUserInboundEventTriggered(event) + self.streamDelegate.streamCreated(id, channel: self.channel) } func streamClosed(_ id: HTTP2StreamID) { - let event = StreamClosedEvent(streamID: id, reason: nil) - self.channel.pipeline.fireUserInboundEventTriggered(event) + self.streamDelegate.streamClosed(id, channel: self.channel) } func goAway( diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift index 738221b10..67f2d226b 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift @@ -341,6 +341,7 @@ extension ServerConnectionManagementHandlerTests { extension ServerConnectionManagementHandlerTests { struct Connection { let channel: EmbeddedChannel + let streamDelegate: any NIOHTTP2StreamDelegate let syncView: ServerConnectionManagementHandler.SyncView var loop: EmbeddedEventLoop { @@ -378,6 +379,7 @@ extension ServerConnectionManagementHandlerTests { clock: self.clock ) + self.streamDelegate = handler self.syncView = handler.syncView self.channel = EmbeddedChannel(handler: handler, loop: loop) } @@ -398,17 +400,11 @@ extension ServerConnectionManagementHandlerTests { } func streamOpened(_ id: HTTP2StreamID) { - let event = NIOHTTP2StreamCreatedEvent( - streamID: id, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - self.channel.pipeline.fireUserInboundEventTriggered(event) + self.streamDelegate.streamCreated(id, channel: self.channel) } func streamClosed(_ id: HTTP2StreamID) { - let event = StreamClosedEvent(streamID: id, reason: nil) - self.channel.pipeline.fireUserInboundEventTriggered(event) + self.streamDelegate.streamClosed(id, channel: self.channel) } func ping(data: HTTP2PingData, ack: Bool) throws { From 492da8913fd1df7568f6b3c95f60bcf2202082ca Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 May 2024 09:55:55 +0100 Subject: [PATCH 311/580] Add some slack to allocation tests (#1884) Motivation: Occasionally one or two allocations are added which we want to ignore in benchmarks where we care about changes in the 1000s. Modifications: - Add some slack to benchmarks - Pass in the config to each of the benchmarks, which was previously dropped on the floor - Add the benchmarks to the formatter Result: Allocation limits allow some slack --- .../GRPCSwiftBenchmark/Benchmarks.swift | 100 ++++++++++-------- scripts/format.sh | 2 + 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift b/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift index ddd481f4d..827f12bdf 100644 --- a/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift +++ b/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift @@ -18,65 +18,67 @@ import GRPCCore let benchmarks = { Benchmark.defaultConfiguration = .init( - metrics: [ - .mallocCountTotal, - .syscalls, - .readSyscalls, - .writeSyscalls, - .memoryLeaked, - .retainCount, - .releaseCount, - ] + metrics: [ + .mallocCountTotal, + .syscalls, + .readSyscalls, + .writeSyscalls, + .memoryLeaked, + .retainCount, + .releaseCount, + ] ) - + // async code is currently still quite flaky in the number of retain/release it does so we don't measure them today - var configWithoutRetainRelease = Benchmark.defaultConfiguration - configWithoutRetainRelease.metrics.removeAll(where: { $0 == .retainCount || $0 == .releaseCount }) - - Benchmark("Metadata_Add_string") { benchmark in + var config = Benchmark.defaultConfiguration + config.metrics.removeAll(where: { $0 == .retainCount || $0 == .releaseCount }) + // Let p90 be off by 50. Benchmarks should do 1000s of iterations so this is small enough. + config.thresholds = [.mallocCountTotal: BenchmarkThresholds(absolute: [.p90: 50])] + + Benchmark("Metadata_Add_string", configuration: config) { benchmark in for _ in benchmark.scaledIterations { var metadata = Metadata() - for i in 0..<1000 { - metadata.addString("\(i)", forKey: "\(i)") + for i in 0 ..< 1000 { + metadata.addString("\(i)", forKey: "\(i)") } } } - - Benchmark("Metadata_Add_binary") { benchmark in + + Benchmark("Metadata_Add_binary", configuration: config) { benchmark in let value: [UInt8] = [1, 2, 3] for _ in benchmark.scaledIterations { var metadata = Metadata() - + benchmark.startMeasurement() - for i in 0..<1000 { - metadata.addBinary(value, forKey: "\(i)") + for i in 0 ..< 1000 { + metadata.addBinary(value, forKey: "\(i)") } benchmark.stopMeasurement() } } - - Benchmark("Metadata_Remove_values_for_key") { benchmark in + + Benchmark("Metadata_Remove_values_for_key", configuration: config) { benchmark in for _ in benchmark.scaledIterations { var metadata = Metadata() - for i in 0..<1000 { - metadata.addString("value", forKey: "\(i)") + for i in 0 ..< 1000 { + metadata.addString("value", forKey: "\(i)") } - + benchmark.startMeasurement() - for i in 0..<1000 { - metadata.removeAllValues(forKey: "\(i)") + for i in 0 ..< 1000 { + metadata.removeAllValues(forKey: "\(i)") } benchmark.stopMeasurement() } } - - Benchmark("Metadata_Iterate_all_values") { benchmark in + + Benchmark("Metadata_Iterate_all_values", configuration: config) { benchmark in for _ in benchmark.scaledIterations { var metadata = Metadata() - for i in 0..<1000 { + for i in 0 ..< 1000 { metadata.addString("value", forKey: "key") } - + benchmark.startMeasurement() for value in metadata["key"] { blackHole(value) @@ -84,14 +86,14 @@ let benchmarks = { benchmark.stopMeasurement() } } - - Benchmark("Metadata_Iterate_string_values") { benchmark in + + Benchmark("Metadata_Iterate_string_values", configuration: config) { benchmark in for _ in benchmark.scaledIterations { var metadata = Metadata() - for i in 0..<1000 { - metadata.addString("\(i)", forKey: "key") + for i in 0 ..< 1000 { + metadata.addString("\(i)", forKey: "key") } - + benchmark.startMeasurement() for value in metadata[stringValues: "key"] { blackHole(value) @@ -99,14 +101,17 @@ let benchmarks = { benchmark.stopMeasurement() } } - - Benchmark("Metadata_Iterate_binary_values_when_only_binary_values_stored") { benchmark in + + Benchmark( + "Metadata_Iterate_binary_values_when_only_binary_values_stored", + configuration: config + ) { benchmark in for _ in benchmark.scaledIterations { var metadata = Metadata() - for i in 0..<1000 { + for i in 0 ..< 1000 { metadata.addBinary([1], forKey: "key") } - + benchmark.startMeasurement() for value in metadata[binaryValues: "key"] { blackHole(value) @@ -114,14 +119,17 @@ let benchmarks = { benchmark.stopMeasurement() } } - - Benchmark("Metadata_Iterate_binary_values_when_only_strings_stored") { benchmark in + + Benchmark( + "Metadata_Iterate_binary_values_when_only_strings_stored", + configuration: config + ) { benchmark in for _ in benchmark.scaledIterations { var metadata = Metadata() - for i in 0..<1000 { - metadata.addString("\(i)", forKey: "key") + for i in 0 ..< 1000 { + metadata.addString("\(i)", forKey: "key") } - + benchmark.startMeasurement() for value in metadata[binaryValues: "key"] { blackHole(value) diff --git a/scripts/format.sh b/scripts/format.sh index 2eda38c00..1e3d7801b 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -94,6 +94,7 @@ if "$lint"; then "${REPO}/Sources" \ "${REPO}/Tests" \ "${REPO}/Plugins" \ + "${REPO}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then @@ -113,6 +114,7 @@ elif "$format"; then "${REPO}/Sources" \ "${REPO}/Tests" \ "${REPO}/Plugins" \ + "${REPO}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then From 78ac169249ac8df5d74fb67f712f126e88c3660b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 May 2024 10:30:36 +0100 Subject: [PATCH 312/580] Add http2 transport namespace and config (#1880) Motivation: The grpc channel will require some config. This can be dervied from config which we'll make public for the posix and niots transports. As we need the config to create the channel, we need all the bits that make up that config and a namespace to hold them in. Modifications: - Add an 'HTTP2ClientTransport' namespace and a nested 'Config' namespace - Add config and defaults Result: Config is in place for the grpc channel --- .../Client/HTTP2ClientTransport.swift | 147 ++++++++++++++++++ .../HTTP2ClientTransportConfigTests.swift | 45 ++++++ 2 files changed, 192 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift new file mode 100644 index 000000000..1350b5399 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift @@ -0,0 +1,147 @@ +/* + * 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 + +/// A namespace for the HTTP/2 client transport. +public enum HTTP2ClientTransport {} + +extension HTTP2ClientTransport { + /// A namespace for HTTP/2 client transport configuration. + public enum Config {} +} + +extension HTTP2ClientTransport.Config { + public struct Compression: Sendable { + /// The default algorithm used for compressing outbound messages. + /// + /// This can be overridden on a per-call basis via ``CallOptions``. + public var algorithm: CompressionAlgorithm + + /// Compression algorithms enabled for inbound messages. + /// + /// - Note: ``CompressionAlgorithm/none`` is always supported, even if it isn't set here. + public var enabledAlgorithms: CompressionAlgorithmSet + + /// Creates a new compression configuration. + /// + /// - SeeAlso: ``defaults``. + public init(algorithm: CompressionAlgorithm, enabledAlgorithms: CompressionAlgorithmSet) { + self.algorithm = algorithm + self.enabledAlgorithms = enabledAlgorithms + } + + /// Default values, compression is disabled. + public static var defaults: Self { + Self(algorithm: .none, enabledAlgorithms: .none) + } + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public struct Keepalive: Sendable { + /// The amount of time to wait after reading data before sending a keepalive ping. + /// + /// - Note: The transport may choose to increase this value if it is less than 10 seconds. + public var time: Duration + + /// The amount of time the server has to respond to a keepalive ping before the connection + /// is closed. + public var timeout: Duration + + /// Whether the client sends keepalive pings when there are no calls in progress. + public var permitWithoutCalls: Bool + + /// Creates a new keepalive configuration. + public init(time: Duration, timeout: Duration, permitWithoutCalls: Bool) { + self.time = time + self.timeout = timeout + self.permitWithoutCalls = permitWithoutCalls + } + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public struct Idle: Sendable { + /// The maximum amount of time a connection may be idle before it's closed. + public var maxTime: Duration + + /// Creates an idle configuration. + public init(maxTime: Duration) { + self.maxTime = maxTime + } + + /// Default values, a 30 minute max idle time. + public static var defaults: Self { + Self(maxTime: .seconds(30 * 60)) + } + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public struct Backoff: Sendable { + /// The initial duration to wait before reattempting to establish a connection. + public var initial: Duration + + /// The maximum duration to wait (before jitter is applied) to wait between connect attempts. + public var max: Duration + + /// The scaling factor applied to the backoff duration between connect attempts. + public var multiplier: Double + + /// An amount to randomize the backoff by. + /// + /// If backoff is computed to be 10 seconds and jitter is set to `0.2`, then the amount of + /// jitter will be selected randomly from the range `-0.2 ✕ 10` seconds to `0.2 ✕ 10` seconds. + /// The resulting backoff will therefore be between 8 seconds and 12 seconds. + public var jitter: Double + + /// Creates a new backoff configuration. + public init(initial: Duration, max: Duration, multiplier: Double, jitter: Double) { + self.initial = initial + self.max = max + self.multiplier = multiplier + self.jitter = jitter + } + + /// Default values, initial backoff is one second and maximum back off is two minutes. The + /// multiplier is `1.6` and the jitter is set to `0.2`. + public static var defaults: Self { + Self(initial: .seconds(1), max: .seconds(120), multiplier: 1.6, jitter: 0.2) + } + } + + public struct HTTP2: Sendable { + /// The max frame size, in bytes. + /// + /// The actual value used is clamped to `(1 << 14) ... (1 << 24) - 1` (the min and max values + /// allowed by RFC 9113 § 6.5.2). + public var maxFrameSize: Int + + /// The target flow control window size, in bytes. + /// + /// The value is clamped to `... (1 << 31) - 1`. + public var targetWindowSize: Int + + /// Creates a new HTTP/2 configuration. + public init(maxFrameSize: Int, targetWindowSize: Int) { + self.maxFrameSize = maxFrameSize + self.targetWindowSize = targetWindowSize + } + + /// Default values, max frame size is 16KiB, and the target window size is 8MiB. + public static var defaults: Self { + Self(maxFrameSize: 1 << 14, targetWindowSize: 8 * 1024 * 1024) + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift b/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift new file mode 100644 index 000000000..c5853196a --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift @@ -0,0 +1,45 @@ +/* + * 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 GRPCHTTP2Core +import XCTest + +final class HTTP2ClientTransportConfigTests: XCTestCase { + func testCompressionDefaults() { + let config = HTTP2ClientTransport.Config.Compression.defaults + XCTAssertEqual(config.algorithm, .none) + XCTAssertEqual(config.enabledAlgorithms, .none) + } + + func testIdleDefaults() { + let config = HTTP2ClientTransport.Config.Idle.defaults + XCTAssertEqual(config.maxTime, .seconds(30 * 60)) + } + + func testBackoffDefaults() { + let config = HTTP2ClientTransport.Config.Backoff.defaults + XCTAssertEqual(config.initial, .seconds(1)) + XCTAssertEqual(config.max, .seconds(120)) + XCTAssertEqual(config.multiplier, 1.6) + XCTAssertEqual(config.jitter, 0.2) + } + + func testHTTP2Defaults() { + let config = HTTP2ClientTransport.Config.HTTP2.defaults + XCTAssertEqual(config.maxFrameSize, 16384) + XCTAssertEqual(config.targetWindowSize, 8 * 1024 * 1024) + } +} From c4353e9e441a30a460f4c0b0d78bea4a81ce702b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 May 2024 10:49:45 +0100 Subject: [PATCH 313/580] Add a request queue for the grpc channel (#1881) Motivation: The grpc channel needs to enqueue requests when the channel isn't ready to handle RPCs. When the channel becomes ready, it can attempt to execute the RPCs on a load balancer. Modifications: - Add a request queue. The queue stores continuations for a `LoadBalancer`. Elements can be removed in-order (popped) or by ID (in case of cancellation). - Add a queue ID Result: Requests can be queued --- .../LoadBalancers/LoadBalancer.swift | 69 +++++ .../RoundRobinLoadBalancer.swift | 4 + .../Client/Connection/RequestQueue.swift | 105 +++++++ .../Internal/ProcessUniqueID.swift | 8 + .../Client/Connection/RequestQueueTests.swift | 259 ++++++++++++++++++ .../Internal/ProcessUniqueIDTests.swift | 6 + 6 files changed, 451 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift new file mode 100644 index 000000000..9b6788eac --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift @@ -0,0 +1,69 @@ +/* + * 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. + */ + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +enum LoadBalancer: Sendable { + case roundRobin(RoundRobinLoadBalancer) +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension LoadBalancer { + init(_ loadBalancer: RoundRobinLoadBalancer) { + self = .roundRobin(loadBalancer) + } + + var id: LoadBalancerID { + switch self { + case .roundRobin(let loadBalancer): + return loadBalancer.id + } + } + + var events: AsyncStream { + switch self { + case .roundRobin(let loadBalancer): + return loadBalancer.events + } + } + + func run() async { + switch self { + case .roundRobin(let loadBalancer): + await loadBalancer.run() + } + } + + func updateAddresses(_ endpoints: [Endpoint]) { + switch self { + case .roundRobin(let loadBalancer): + loadBalancer.updateAddresses(endpoints) + } + } + + func close() { + switch self { + case .roundRobin(let loadBalancer): + loadBalancer.close() + } + } + + func pickSubchannel() -> Subchannel? { + switch self { + case .roundRobin(let loadBalancer): + return loadBalancer.pickSubchannel() + } + } +} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift index 1699bb7f7..fa0d97429 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -117,6 +117,9 @@ struct RoundRobinLoadBalancer { /// The set of enabled compression algorithms. private let enabledCompression: CompressionAlgorithmSet + /// The ID of this load balancer. + internal let id: LoadBalancerID + init( connector: any HTTP2Connector, backoff: ConnectionBackoff, @@ -127,6 +130,7 @@ struct RoundRobinLoadBalancer { self.backoff = backoff self.defaultCompression = defaultCompression self.enabledCompression = enabledCompression + self.id = LoadBalancerID() self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) self.input = AsyncStream.makeStream(of: Input.self) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift new file mode 100644 index 000000000..e043bf24a --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift @@ -0,0 +1,105 @@ +/* + * 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 DequeModule + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct RequestQueue { + typealias Continuation = CheckedContinuation + + private struct QueueEntry { + var continuation: Continuation + var waitForReady: Bool + } + + /// IDs of entries in the order they should be processed. + /// + /// If an ID is popped from the queue but isn't present in `entriesByID` then it must've + /// been removed directly by its ID, this is fine. + private var ids: Deque + + /// Entries keyed by their ID. + private var entriesByID: [QueueEntryID: QueueEntry] + + init() { + self.ids = [] + self.entriesByID = [:] + } + + /// Remove the first continuation from the queue. + mutating func popFirst() -> Continuation? { + while let id = self.ids.popFirst() { + if let waiter = self.entriesByID.removeValue(forKey: id) { + return waiter.continuation + } + } + + assert(self.entriesByID.isEmpty) + return nil + } + + /// Append a continuation to the queue. + /// + /// - Parameters: + /// - continuation: The continuation to append. + /// - waitForReady: Whether the request associated with the continuation is willing to wait for + /// the channel to become ready. + /// - id: The unique ID of the queue entry. + mutating func append(continuation: Continuation, waitForReady: Bool, id: QueueEntryID) { + let entry = QueueEntry(continuation: continuation, waitForReady: waitForReady) + let removed = self.entriesByID.updateValue(entry, forKey: id) + assert(removed == nil, "id '\(id)' reused") + self.ids.append(id) + } + + /// Remove the waiter with the given ID, if it exists. + mutating func removeEntry(withID id: QueueEntryID) -> Continuation? { + let waiter = self.entriesByID.removeValue(forKey: id) + return waiter?.continuation + } + + /// Remove all waiters, returning their continuations. + mutating func removeAll() -> [Continuation] { + let continuations = Array(self.entriesByID.values.map { $0.continuation }) + self.ids.removeAll(keepingCapacity: true) + self.entriesByID.removeAll(keepingCapacity: true) + return continuations + } + + /// Remove all entries which were appended to the queue with a value of `false` + /// for `waitForReady`. + mutating func removeFastFailingEntries() -> [Continuation] { + var removed = [Continuation]() + var remainingIDs = Deque() + var remainingEntriesByID = [QueueEntryID: QueueEntry]() + + while let id = self.ids.popFirst() { + guard let waiter = self.entriesByID.removeValue(forKey: id) else { continue } + + if waiter.waitForReady { + remainingEntriesByID[id] = waiter + remainingIDs.append(id) + } else { + removed.append(waiter.continuation) + } + } + + assert(self.entriesByID.isEmpty) + self.entriesByID = remainingEntriesByID + self.ids = remainingIDs + return removed + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift index 87f888626..95a6564a2 100644 --- a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift +++ b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift @@ -45,3 +45,11 @@ struct LoadBalancerID: Hashable, Sendable, CustomStringConvertible { "lb_\(self.id)" } } + +/// A process-unique ID for an entry in a queue. +struct QueueEntryID: Hashable, Sendable, CustomStringConvertible { + private let id = ProcessUniqueID() + var description: String { + "q_entry_\(self.id)" + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift new file mode 100644 index 000000000..712ab045b --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift @@ -0,0 +1,259 @@ +/* + * 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 + +@testable import GRPCHTTP2Core + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class RequestQueueTests: XCTestCase { + struct AnErrorToAvoidALeak: Error {} + + func testPopFirstEmpty() { + var queue = RequestQueue() + XCTAssertNil(queue.popFirst()) + } + + func testPopFirstNonEmpty() async { + _ = try? await withCheckedThrowingContinuation { continuation in + var queue = RequestQueue() + let id = QueueEntryID() + + queue.append(continuation: continuation, waitForReady: false, id: id) + guard let popped = queue.popFirst() else { + return XCTFail("Missing continuation") + } + XCTAssertNil(queue.popFirst()) + + popped.resume(throwing: AnErrorToAvoidALeak()) + } + } + + func testPopFirstMultiple() async { + await withTaskGroup(of: QueueEntryID.self) { group in + let queue = _LockedValueBox(RequestQueue()) + let signal1 = AsyncStream.makeStream(of: Void.self) + let signal2 = AsyncStream.makeStream(of: Void.self) + + let id1 = QueueEntryID() + let id2 = QueueEntryID() + + group.addTask { + _ = try? await withCheckedThrowingContinuation { continuation in + queue.withLockedValue { + $0.append(continuation: continuation, waitForReady: false, id: id1) + } + + signal1.continuation.yield() + signal1.continuation.finish() + } + + return id1 + } + + group.addTask { + // Wait until instructed to append. + for await _ in signal1.stream {} + + _ = try? await withCheckedThrowingContinuation { continuation in + queue.withLockedValue { + $0.append(continuation: continuation, waitForReady: false, id: id2) + } + + signal2.continuation.yield() + signal2.continuation.finish() + } + + return id2 + } + + // Wait for both continuations to be enqueued. + for await _ in signal2.stream {} + + for id in [id1, id2] { + let continuation = queue.withLockedValue { $0.popFirst() } + continuation?.resume(throwing: AnErrorToAvoidALeak()) + let actual = await group.next() + XCTAssertEqual(id, actual) + } + } + } + + func testRemoveEntryByID() async { + _ = try? await withCheckedThrowingContinuation { continuation in + var queue = RequestQueue() + let id = QueueEntryID() + + queue.append(continuation: continuation, waitForReady: false, id: id) + guard let popped = queue.removeEntry(withID: id) else { + return XCTFail("Missing continuation") + } + XCTAssertNil(queue.removeEntry(withID: id)) + + popped.resume(throwing: AnErrorToAvoidALeak()) + } + } + + func testRemoveEntryByIDMultiple() async { + await withTaskGroup(of: QueueEntryID.self) { group in + let queue = _LockedValueBox(RequestQueue()) + let signal1 = AsyncStream.makeStream(of: Void.self) + let signal2 = AsyncStream.makeStream(of: Void.self) + + let id1 = QueueEntryID() + let id2 = QueueEntryID() + + group.addTask { + _ = try? await withCheckedThrowingContinuation { continuation in + queue.withLockedValue { + $0.append(continuation: continuation, waitForReady: false, id: id1) + } + + signal1.continuation.yield() + signal1.continuation.finish() + } + + return id1 + } + + group.addTask { + // Wait until instructed to append. + for await _ in signal1.stream {} + + _ = try? await withCheckedThrowingContinuation { continuation in + queue.withLockedValue { + $0.append(continuation: continuation, waitForReady: false, id: id2) + } + + signal2.continuation.yield() + signal2.continuation.finish() + } + + return id2 + } + + // Wait for both continuations to be enqueued. + for await _ in signal2.stream {} + + for id in [id1, id2] { + let continuation = queue.withLockedValue { $0.removeEntry(withID: id) } + continuation?.resume(throwing: AnErrorToAvoidALeak()) + let actual = await group.next() + XCTAssertEqual(id, actual) + } + } + } + + func testRemoveFastFailingEntries() async throws { + let queue = _LockedValueBox(RequestQueue()) + let enqueued = AsyncStream.makeStream(of: Void.self) + + try await withThrowingTaskGroup(of: Void.self) { group in + var waitForReadyIDs = [QueueEntryID]() + var failFastIDs = [QueueEntryID]() + + for _ in 0 ..< 50 { + waitForReadyIDs.append(QueueEntryID()) + failFastIDs.append(QueueEntryID()) + } + + for ids in [waitForReadyIDs, failFastIDs] { + let waitForReady = ids == waitForReadyIDs + for id in ids { + group.addTask { + do { + _ = try await withCheckedThrowingContinuation { continuation in + queue.withLockedValue { + $0.append(continuation: continuation, waitForReady: waitForReady, id: id) + } + enqueued.continuation.yield() + } + } catch is AnErrorToAvoidALeak { + () + } + } + } + } + + // Wait for all continuations to be enqueued. + var numberEnqueued = 0 + for await _ in enqueued.stream { + numberEnqueued += 1 + if numberEnqueued == (waitForReadyIDs.count + failFastIDs.count) { + enqueued.continuation.finish() + } + } + + // Remove all fast-failing continuations. + let continuations = queue.withLockedValue { + $0.removeFastFailingEntries() + } + + for continuation in continuations { + continuation.resume(throwing: AnErrorToAvoidALeak()) + } + + for id in failFastIDs { + queue.withLockedValue { + XCTAssertNil($0.removeEntry(withID: id)) + } + } + + for id in waitForReadyIDs { + let maybeContinuation = queue.withLockedValue { $0.removeEntry(withID: id) } + let continuation = try XCTUnwrap(maybeContinuation) + continuation.resume(throwing: AnErrorToAvoidALeak()) + } + } + } + + func testRemoveAll() async throws { + let queue = _LockedValueBox(RequestQueue()) + let enqueued = AsyncStream.makeStream(of: Void.self) + + await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0 ..< 10 { + group.addTask { + _ = try await withCheckedThrowingContinuation { continuation in + queue.withLockedValue { + $0.append(continuation: continuation, waitForReady: false, id: QueueEntryID()) + } + + enqueued.continuation.yield() + } + } + } + + // Wait for all continuations to be enqueued. + var numberEnqueued = 0 + for await _ in enqueued.stream { + numberEnqueued += 1 + if numberEnqueued == 10 { + enqueued.continuation.finish() + } + } + + let continuations = queue.withLockedValue { $0.removeAll() } + XCTAssertEqual(continuations.count, 10) + XCTAssertNil(queue.withLockedValue { $0.popFirst() }) + + for continuation in continuations { + continuation.resume(throwing: AnErrorToAvoidALeak()) + } + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift index a63001413..d510031e3 100644 --- a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift @@ -48,4 +48,10 @@ final class ProcessUniqueIDTests: XCTestCase { let description = String(describing: id) XCTAssert(description.hasPrefix("lb_")) } + + func testQueueEntryDescription() { + let id = QueueEntryID() + let description = String(describing: id) + XCTAssert(description.hasPrefix("q_entry_")) + } } From aeaccdf7ce0c0d23c39164d39898f29d2ef837b8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 May 2024 10:57:12 +0100 Subject: [PATCH 314/580] Remove storage indirection from errors (#1882) Motivation: Existential boxing rules don't apply to errors: `any Error` is unconditionally boxed. This means that there's no allocation benefit to backing errors with storage classes. Modifications: - Remove the backing storage class of `RPCError` and `RuntimeError` Result: Fewer allocations --- Sources/GRPCCore/RPCError.swift | 88 +++++++---------------------- Sources/GRPCCore/RuntimeError.swift | 75 ++++++------------------ 2 files changed, 35 insertions(+), 128 deletions(-) diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 2eb5fd210..2113d9776 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -17,33 +17,12 @@ /// An error representing the outcome of an RPC. /// /// See also ``Status``. -public struct RPCError: @unchecked Sendable, Hashable, Error { - // @unchecked because it relies on heap allocated storage and 'isKnownUniquelyReferenced' - - private var storage: Storage - private mutating func ensureStorageIsUnique() { - if !isKnownUniquelyReferenced(&self.storage) { - self.storage = self.storage.copy() - } - } - +public struct RPCError: Sendable, Hashable, Error { /// A code representing the high-level domain of the error. - public var code: Code { - get { self.storage.code } - set { - self.ensureStorageIsUnique() - self.storage.code = newValue - } - } + public var code: Code /// A message providing additional context about the error. - public var message: String { - get { self.storage.message } - set { - self.ensureStorageIsUnique() - self.storage.message = newValue - } - } + public var message: String /// Metadata associated with the error. /// @@ -51,22 +30,10 @@ public struct RPCError: @unchecked Sendable, Hashable, Error { /// conversely any ``RPCError`` received by the client may include metadata sent by a service. /// /// Note that clients and servers may synthesise errors which may not include metadata. - public var metadata: Metadata { - get { self.storage.metadata } - set { - self.ensureStorageIsUnique() - self.storage.metadata = newValue - } - } + public var metadata: Metadata /// The original error which led to this error being thrown. - public var cause: Error? { - get { self.storage.cause } - set { - self.ensureStorageIsUnique() - self.storage.cause = newValue - } - } + public var cause: Error? /// Create a new RPC error. /// @@ -76,7 +43,10 @@ public struct RPCError: @unchecked Sendable, Hashable, Error { /// - metadata: Any metadata to attach to the error. /// - cause: An underlying error which led to this error being thrown. public init(code: Code, message: String, metadata: Metadata = [:], cause: Error? = nil) { - self.storage = Storage(code: code, message: message, metadata: metadata, cause: cause) + self.code = code + self.message = message + self.metadata = metadata + self.cause = cause } /// Create a new RPC error from the provided ``Status``. @@ -90,6 +60,16 @@ public struct RPCError: @unchecked Sendable, Hashable, Error { guard let code = Code(status.code) else { return nil } self.init(code: code, message: status.message, metadata: metadata) } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + hasher.combine(self.metadata) + } + + public static func == (lhs: RPCError, rhs: RPCError) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message && lhs.metadata == rhs.metadata + } } extension RPCError: CustomStringConvertible { @@ -98,36 +78,6 @@ extension RPCError: CustomStringConvertible { } } -extension RPCError { - private final class Storage: Hashable { - var code: RPCError.Code - var message: String - var metadata: Metadata - var cause: Error? - - init(code: RPCError.Code, message: String, metadata: Metadata, cause: Error?) { - self.code = code - self.message = message - self.metadata = metadata - self.cause = cause - } - - func copy() -> Self { - Self(code: self.code, message: self.message, metadata: self.metadata, cause: self.cause) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.code) - hasher.combine(self.message) - hasher.combine(self.metadata) - } - - static func == (lhs: RPCError.Storage, rhs: RPCError.Storage) -> Bool { - return lhs.code == rhs.code && lhs.message == rhs.message && lhs.metadata == rhs.metadata - } - } -} - extension RPCError { public struct Code: Hashable, Sendable, CustomStringConvertible { /// The numeric value of the error code. diff --git a/Sources/GRPCCore/RuntimeError.swift b/Sources/GRPCCore/RuntimeError.swift index d2791a873..e43bc800e 100644 --- a/Sources/GRPCCore/RuntimeError.swift +++ b/Sources/GRPCCore/RuntimeError.swift @@ -18,43 +18,16 @@ /// /// In contrast to ``RPCError``, the ``RuntimeError`` represents errors which happen at a scope /// wider than an individual RPC. For example, passing invalid configuration values. -public struct RuntimeError: Error, Hashable, @unchecked Sendable { - private var storage: Storage - - // Ensures the underlying storage is unique. - private mutating func ensureUniqueStorage() { - if !isKnownUniquelyReferenced(&self.storage) { - self.storage = self.storage.copy() - } - } - +public struct RuntimeError: Error, Hashable, Sendable { /// The code indicating the domain of the error. - public var code: Code { - get { self.storage.code } - set { - self.ensureUniqueStorage() - self.storage.code = newValue - } - } + public var code: Code /// A message providing more details about the error which may include details specific to this /// instance of the error. - public var message: String { - get { self.storage.message } - set { - self.ensureUniqueStorage() - self.storage.message = newValue - } - } + public var message: String /// The original error which led to this error being thrown. - public var cause: Error? { - get { self.storage.cause } - set { - self.ensureUniqueStorage() - self.storage.cause = newValue - } - } + public var cause: Error? /// Creates a new error. /// @@ -63,7 +36,18 @@ public struct RuntimeError: Error, Hashable, @unchecked Sendable { /// - message: A description of the error. /// - cause: The original error which led to this error being thrown. public init(code: Code, message: String, cause: Error? = nil) { - self.storage = Storage(code: code, message: message, cause: cause) + self.code = code + self.message = message + self.cause = cause + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.code) + hasher.combine(self.message) + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.code == rhs.code && lhs.message == rhs.message } } @@ -77,33 +61,6 @@ extension RuntimeError: CustomStringConvertible { } } -extension RuntimeError { - private final class Storage: Hashable { - var code: Code - var message: String - var cause: Error? - - init(code: Code, message: String, cause: Error?) { - self.code = code - self.message = message - self.cause = cause - } - - func copy() -> Storage { - return Storage(code: self.code, message: self.message, cause: self.cause) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.code) - hasher.combine(self.message) - } - - static func == (lhs: Storage, rhs: Storage) -> Bool { - return lhs.code == rhs.code && lhs.message == rhs.message - } - } -} - extension RuntimeError { public struct Code: Hashable, Sendable { private enum Value { From d13d647221214b4971ad184ab633752b81428126 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 May 2024 11:04:55 +0100 Subject: [PATCH 315/580] Skip testMakeStreamOnShutdownSubchannel on 5.8 and earlier (#1885) Motivation: `testMakeStreamOnShutdownSubchannel` occasionally hits an assertion failure in pthread caused by a bug in the Swift concurrency runtime. This was fixed in 5.9. Modifications: - Skip the test for older compiler versions Result: Fewer false negatives --- .../Client/Connection/LoadBalancers/SubchannelTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 709587fbe..6c48a71de 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -39,6 +39,10 @@ final class SubchannelTests: XCTestCase { } func testMakeStreamOnShutdownSubchannel() async throws { + #if compiler(<5.9) + throw XCTSkip("Occasionally crashes due to a Swift 5.8 concurrency runtime bug") + #endif + let subchannel = self.makeSubchannel( address: .unixDomainSocket(path: "ignored"), connector: .never From 91be64e04035db0824e59da67c1fa339456d004b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 May 2024 12:58:42 +0100 Subject: [PATCH 316/580] Update actions/checkout to v4 (#1886) Motivation: actions/checkout@v3 uses Node 16 which is end-of-life Modifications: - Switch to v4 Result: Using supported path --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 84076c692..564857d80 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: container: image: swift steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Formatting and License Headers check" run: | ./scripts/sanity.sh @@ -37,7 +37,7 @@ jobs: container: image: ${{ matrix.image }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 🔧 Build run: swift build ${{ matrix.swift-build-flags }} timeout-minutes: 20 @@ -98,7 +98,7 @@ jobs: container: image: ${{ matrix.image }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 🧮 Allocation Counting Tests run: ./Performance/allocations/test-allocation-counts.sh env: ${{ matrix.env }} @@ -124,7 +124,7 @@ jobs: container: image: ${{ matrix.image }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build without NIOSSL run: swift build env: From 73a34ffc0f28c4f3ca264b03c01577bde8d15c30 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 May 2024 14:10:22 +0100 Subject: [PATCH 317/580] Cache swift-format (#1887) Motivation: The CI takes ~7 minutes to run, the longest job is the formatting check which spends most of its time compiling swift-format. Modifications: - Cache the swift-format build directory Result: Faster CI --- .github/workflows/ci.yaml | 8 ++++++++ scripts/license-check.sh | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 564857d80..22497f678 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,9 +12,17 @@ jobs: image: swift steps: - uses: actions/checkout@v4 + - uses: actions/cache/restore@v4 + with: + path: scripts/.swift-format-source + key: ${{ runner.os }}-swift-format - name: "Formatting and License Headers check" run: | ./scripts/sanity.sh + - uses: actions/cache/save@v4 + with: + path: scripts/.swift-format-source + key: ${{ runner.os }}-swift-format unit-tests: strategy: fail-fast: false diff --git a/scripts/license-check.sh b/scripts/license-check.sh index 496197ce9..9005fadd6 100755 --- a/scripts/license-check.sh +++ b/scripts/license-check.sh @@ -102,6 +102,10 @@ check_copyright_headers() { ! -name '*.grpc.swift' \ ! -name 'LinuxMain.swift' \ ! -name 'XCTestManifests.swift' \ + ! -path './FuzzTesting/.build/*' \ + ! -path './Performance/QPSBenchmark/.build/*' \ + ! -path './Performance/Benchmarks/.build/*' \ + ! -path './scripts/.swift-format-source/*' \ ! -path './.build/*') } From c596dfb5ecd6374c254fd5e65e97612b3e965be3 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Fri, 24 May 2024 08:45:15 +0100 Subject: [PATCH 318/580] Update tutorial docs (#1889) --- Sources/Examples/RouteGuide/README.md | 5 +++-- docs/basic-tutorial.md | 17 +++++++++-------- docs/quick-start.md | 12 ++++++------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Sources/Examples/RouteGuide/README.md b/Sources/Examples/RouteGuide/README.md index 6d6f33dad..fe553deab 100644 --- a/Sources/Examples/RouteGuide/README.md +++ b/Sources/Examples/RouteGuide/README.md @@ -22,11 +22,12 @@ $ swift run RouteGuideClient ## Regenerating client and server code -For simplicity, a Makefile is provided in the root of this package with a target +For simplicity, a shell script ([grpc-swift/Protos/generate.sh][run-protoc]) is provided to generate client and server code: ```sh -$ make generate-route-guide +$ Protos/generate.sh ``` [basic-tutorial]: ../../../docs/basic-tutorial.md +[run-protoc]: ../../../Protos/generate.sh diff --git a/docs/basic-tutorial.md b/docs/basic-tutorial.md index 2c13c2a16..4c67256ee 100644 --- a/docs/basic-tutorial.md +++ b/docs/basic-tutorial.md @@ -55,7 +55,7 @@ $ cd grpc-swift/Sources/Examples/RouteGuide Our first step (as you'll know from the [Overview][grpc-docs]) is to define the gRPC *service* and the method *request* and *response* types using [protocol buffers][protocol-buffers]. You can see the complete .proto file in -[`grpc-swift/Sources/Examples/RouteGuide/Model/route_guide.proto`][routeguide-proto]. +[`grpc-swift/Protos/examples/route_guide/route_guide.proto`][routeguide-proto]. To define a service, we specify a named `service` in the .proto file: @@ -142,12 +142,12 @@ Protobuf][swift-protobuf]) and the other for gRPC. You need to use the [proto3][protobuf-releases] compiler (which supports both proto2 and proto3 syntax) in order to generate gRPC services. -For simplicity, we've provided a Makefile in the `grpc-swift` directory that +For simplicity, we've provided a shell script ([grpc-swift/Protos/generate.sh][run-protoc]) that runs protoc for you with the appropriate plugin, input, and output (if you want to run this yourself, make sure you've installed protoc first): ```sh -$ make generate-route-guide +$ Protos/generate.sh ``` Running this command generates the following files in the @@ -161,8 +161,8 @@ Running this command generates the following files in the Let's look at how to run the same command manually: ```sh -$ protoc Sources/Examples/RouteGuide/Model/route_guide.proto \ - --proto_path=Sources/Examples/RouteGuide/Model \ +$ protoc Protos/examples/route_guide/route_guide.proto \ + --proto_path=Protos/examples/route_guide \ --plugin=./.build/debug/protoc-gen-swift \ --swift_opt=Visibility=Public \ --swift_out=Sources/Examples/RouteGuide/Model \ @@ -455,7 +455,7 @@ program from exiting (since `close()` is never called on the server). In this section, we'll look at creating a Swift client for our `RouteGuide` service. You can see our complete example client code in -[grpc-swift/Sources/Examples/RouteGuide/Client/main.swift][routeguide-client]. +[grpc-swift/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift][routeguide-client]. #### Creating a stub @@ -604,10 +604,11 @@ Follow the instructions in the Route Guide example directory [protobuf-docs]: https://developers.google.com/protocol-buffers/docs/proto3 [protobuf-releases]: https://github.com/google/protobuf/releases [protocol-buffers]: https://developers.google.com/protocol-buffers/docs/overview -[routeguide-client]: ../Sources/Examples/RouteGuide/Client/main.swift -[routeguide-proto]: ../Sources/Examples/RouteGuide/Model/route_guide.proto +[routeguide-client]: ../Sources/Examples/RouteGuide/Client/RouteGuideClient.swift +[routeguide-proto]: ../Protos/examples/route_guide/route_guide.proto [routeguide-provider]: ../Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift [routeguide-readme]: ../Sources/Examples/RouteGuide/README.md [routeguide-source]: ../Sources/Examples/RouteGuide +[run-protoc]: ../Protos/generate.sh [swift-protobuf-guide]: https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md [swift-protobuf]: https://github.com/apple/swift-protobuf diff --git a/docs/quick-start.md b/docs/quick-start.md index 5ba056c65..6dbf5ae21 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -6,9 +6,9 @@ #### Swift Version -gRPC requires Swift 5.0 or higher. +gRPC requires Swift 5.8 or higher. -#### Install Protocol Buffers v3 +#### Install Protocol Buffers Install the protoc compiler that is used to generate gRPC service code. The simplest way to do this is to download pre-compiled binaries for your @@ -27,8 +27,8 @@ clones the entire repository, but you just need the examples for this quickstart and other tutorials): ```sh -$ # Clone the repository at the latest release to get the example code: -$ git clone -b 1.13.0 https://github.com/grpc/grpc-swift +$ # Clone the repository at the latest release to get the example code (replacing x.y.z with the latest release, for example 1.13.0): +$ git clone -b x.y.z https://github.com/grpc/grpc-swift $ # Navigate to the repository $ cd grpc-swift/ ``` @@ -81,7 +81,7 @@ message HelloReply { ``` Let's update this so that the `Greeter` service has two methods. Edit -`Sources/Examples/HelloWorld/Model/helloworld.proto` and update it with a new +`Protos/upstream/grpc/examples/helloworld.proto` and update it with a new `SayHelloAgain` method, with the same request and response types: ```proto @@ -115,7 +115,7 @@ contains our generated gRPC client and server classes. From the `grpc-swift` directory run ```sh -make generate-helloworld +$ Protos/generate.sh ``` This also regenerates classes for populating, serializing, and retrieving our From 716d6d48b3c5f8a44399bbcebb864e8146a36086 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 May 2024 09:22:09 +0100 Subject: [PATCH 319/580] Add missing availability to tests (#1890) Co-authored-by: Gustavo Cairo --- .../Client/HTTP2ClientTransportConfigTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift b/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift index c5853196a..b376f8fcd 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift @@ -17,6 +17,7 @@ import GRPCHTTP2Core import XCTest +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class HTTP2ClientTransportConfigTests: XCTestCase { func testCompressionDefaults() { let config = HTTP2ClientTransport.Config.Compression.defaults From 1bdb831b955ea0a6908510df6e7f545dd9dd2d1d Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 28 May 2024 10:27:14 +0100 Subject: [PATCH 320/580] Server transport changes (#1891) --- .../Client/Connection/Connection.swift | 2 +- .../Client/GRPCClientStreamHandler.swift | 4 +- .../GRPCStreamStateMachine.swift | 40 ++-- .../ServerConnectionManagementHandler.swift | 3 + .../Server/GRPCServerStreamHandler.swift | 27 ++- .../Connection/Utilities/ConnectionTest.swift | 3 +- .../Connection/Utilities/TestServer.swift | 3 +- .../GRPCStreamStateMachineTests.swift | 94 +++++++-- .../Server/GRPCServerStreamHandlerTests.swift | 193 ++++++++++++++---- 9 files changed, 284 insertions(+), 85 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index ae566eeb8..ea81ce34d 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -408,7 +408,7 @@ extension Connection { /// Multiplexer for creating HTTP/2 streams. var multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer /// Whether the connection is plaintext, `false` implies TLS is being used. - var scheme: Scheme + var scheme: GRPCStreamStateMachineConfiguration.Scheme init(_ connection: HTTP2Connection) { self.channel = connection.channel diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index e03693995..ac00a9531 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -33,7 +33,7 @@ final class GRPCClientStreamHandler: ChannelDuplexHandler { init( methodDescriptor: MethodDescriptor, - scheme: Scheme, + scheme: GRPCStreamStateMachineConfiguration.Scheme, outboundEncoding: CompressionAlgorithm, acceptedEncodings: CompressionAlgorithmSet, maximumPayloadSize: Int, @@ -103,7 +103,7 @@ extension GRPCClientStreamHandler { endStream: headers.endStream ) switch action { - case .receivedMetadata(let metadata): + case .receivedMetadata(let metadata, _): context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) case .rejectRPC: diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 441beb220..996574fe7 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -19,15 +19,15 @@ import NIOCore import NIOHPACK import NIOHTTP1 -enum Scheme: String { - case http - case https -} - enum GRPCStreamStateMachineConfiguration { case client(ClientConfiguration) case server(ServerConfiguration) + enum Scheme: String { + case http + case https + } + struct ClientConfiguration { var methodDescriptor: MethodDescriptor var scheme: Scheme @@ -384,7 +384,7 @@ struct GRPCStreamStateMachine { } enum OnMetadataReceived: Equatable { - case receivedMetadata(Metadata) + case receivedMetadata(Metadata, MethodDescriptor?) // Client-specific actions case receivedStatusAndMetadata(status: Status, metadata: Metadata) @@ -505,7 +505,7 @@ struct GRPCStreamStateMachine { extension GRPCStreamStateMachine { private func makeClientHeaders( methodDescriptor: MethodDescriptor, - scheme: Scheme, + scheme: GRPCStreamStateMachineConfiguration.Scheme, outboundEncoding: CompressionAlgorithm?, acceptedEncodings: CompressionAlgorithmSet, customMetadata: Metadata @@ -817,7 +817,7 @@ extension GRPCStreamStateMachine { decompressor: decompressor ) ) - return .receivedMetadata(Metadata(headers: headers)) + return .receivedMetadata(Metadata(headers: headers), nil) } } @@ -857,7 +857,7 @@ extension GRPCStreamStateMachine { decompressionAlgorithm: inboundEncoding ) ) - return .receivedMetadata(Metadata(headers: headers)) + return .receivedMetadata(Metadata(headers: headers), nil) } } @@ -1211,21 +1211,31 @@ extension GRPCStreamStateMachine { return .rejectRPC(trailers: trailers) } - let path = headers.firstString(forKey: .path) - .flatMap { MethodDescriptor(fullyQualifiedMethod: $0) } - if path == nil { + guard let pathHeader = headers.firstString(forKey: .path) else { return self.closeServerAndBuildRejectRPCAction( currentState: state, endStream: endStream, rejectWithStatus: Status( - code: .unimplemented, + code: .invalidArgument, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." ) ) } + guard let path = MethodDescriptor(fullyQualifiedMethod: pathHeader) else { + return self.closeServerAndBuildRejectRPCAction( + currentState: state, + endStream: endStream, + rejectWithStatus: Status( + code: .unimplemented, + message: + "The given \(GRPCHTTP2Keys.path.rawValue) (\(pathHeader)) does not correspond to a valid method." + ) + ) + } + let scheme = headers.firstString(forKey: .scheme) - .flatMap { Scheme(rawValue: $0) } + .flatMap { GRPCStreamStateMachineConfiguration.Scheme(rawValue: $0) } if scheme == nil { return self.closeServerAndBuildRejectRPCAction( currentState: state, @@ -1355,7 +1365,7 @@ extension GRPCStreamStateMachine { ) } - return .receivedMetadata(Metadata(headers: headers)) + return .receivedMetadata(Metadata(headers: headers), path) case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: try self.invalidState( "Client shouldn't have sent metadata twice." diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index b144c4c8c..4739651aa 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -279,6 +279,9 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { case let event as StreamClosedEvent: self.streamClosed(event.streamID, channel: context.channel) + case is ChannelShouldQuiesceEvent: + self.initiateGracefulShutdown(context: context) + default: () } diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 4a19e22f8..2d9cfb17b 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -37,10 +37,13 @@ final class GRPCServerStreamHandler: ChannelDuplexHandler { private var pendingTrailers: (trailers: HTTP2Frame.FramePayload, promise: EventLoopPromise?)? + private let methodDescriptorPromise: EventLoopPromise + init( - scheme: Scheme, + scheme: GRPCStreamStateMachineConfiguration.Scheme, acceptedEncodings: CompressionAlgorithmSet, maximumPayloadSize: Int, + methodDescriptorPromise: EventLoopPromise, skipStateMachineAssertions: Bool = false ) { self.stateMachine = .init( @@ -48,6 +51,7 @@ final class GRPCServerStreamHandler: ChannelDuplexHandler { maximumPayloadSize: maximumPayloadSize, skipAssertions: skipStateMachineAssertions ) + self.methodDescriptorPromise = methodDescriptorPromise } } @@ -97,11 +101,22 @@ extension GRPCServerStreamHandler { endStream: headers.endStream ) switch action { - case .receivedMetadata(let metadata): - context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) + case .receivedMetadata(let metadata, let methodDescriptor): + if let methodDescriptor = methodDescriptor { + self.methodDescriptorPromise.succeed(methodDescriptor) + context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) + } else { + assertionFailure("Method descriptor should have been present if we received metadata.") + } case .rejectRPC(let trailers): self.flushPending = true + self.methodDescriptorPromise.fail( + RPCError( + code: .unavailable, + message: "RPC was rejected." + ) + ) let response = HTTP2Frame.FramePayload.headers(.init(headers: trailers, endStream: true)) context.write(self.wrapOutboundOut(response), promise: nil) @@ -135,6 +150,12 @@ extension GRPCServerStreamHandler { func handlerRemoved(context: ChannelHandlerContext) { self.stateMachine.tearDown() + self.methodDescriptorPromise.fail( + RPCError( + code: .unavailable, + message: "RPC stream was closed before we got any Metadata." + ) + ) } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift index 5412b5fa9..8dd2f0766 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -116,7 +116,8 @@ extension ConnectionTest { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: .none, - maximumPayloadSize: .max + maximumPayloadSize: .max, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) return stream.eventLoop.makeCompletedFuture { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift index 2c133849e..4d9e84d38 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -73,7 +73,8 @@ final class TestServer: Sendable { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: .all, - maximumPayloadSize: .max + maximumPayloadSize: .max, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try stream.pipeline.syncOperations.addHandlers(handler) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 4fe777e2b..9cb686731 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -66,6 +66,10 @@ extension HPACKHeaders { GRPCHTTP2Keys.path.rawValue: "test/test", GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid", ] + fileprivate static let receivedWithInvalidPath: Self = [ + GRPCHTTP2Keys.path.rawValue: "someinvalidpath", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + ] fileprivate static let receivedWithoutEndpoint: Self = [ GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] @@ -449,7 +453,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "custom": "123", ] expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") - XCTAssertEqual(action, .receivedMetadata(expectedMetadata)) + XCTAssertEqual(action, .receivedMetadata(expectedMetadata, nil)) } } @@ -1002,11 +1006,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) XCTAssertEqual( serverInitialHeadersAction, - .receivedMetadata([ - ":status": "200", - "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", - ]) + .receivedMetadata( + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ], + nil + ) ) // Client sends messages @@ -1102,11 +1109,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) XCTAssertEqual( serverInitialHeadersAction, - .receivedMetadata([ - ":status": "200", - "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", - ]) + .receivedMetadata( + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ], + nil + ) ) // Server sends response @@ -1186,11 +1196,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) XCTAssertEqual( serverInitialHeadersAction, - .receivedMetadata([ - ":status": "200", - "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", - ]) + .receivedMetadata( + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-accept-encoding": "deflate", + ], + nil + ) ) // Client closes @@ -1631,7 +1644,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) XCTAssertEqual( action, - .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + .receivedMetadata( + Metadata(headers: .clientInitialMetadata), + MethodDescriptor(fullyQualifiedMethod: "test/test") + ) ) } @@ -1641,7 +1657,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: true) XCTAssertEqual( action, - .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + .receivedMetadata( + Metadata(headers: .clientInitialMetadata), + MethodDescriptor(fullyQualifiedMethod: "test/test") + ) ) } @@ -1687,13 +1706,35 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { [ ":status": "200", "content-type": "application/grpc", - "grpc-status": "12", + "grpc-status": String(Status.Code.invalidArgument.rawValue), "grpc-status-message": "No :path header has been set.", ] ) } } + func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidPath() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + + let action = try stateMachine.receive( + headers: .receivedWithInvalidPath, + endStream: false + ) + + self.assertRejectedRPC(action) { trailers in + XCTAssertEqual( + trailers, + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": String(Status.Code.unimplemented.rawValue), + "grpc-status-message": + "The given :path (someinvalidpath) does not correspond to a valid method.", + ] + ) + } + } + func testReceiveMetadataWhenClientIdleAndServerIdle_MissingTE() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) @@ -2376,7 +2417,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) XCTAssertEqual( receiveMetadataAction, - .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + .receivedMetadata( + Metadata(headers: .clientInitialMetadata), + MethodDescriptor(fullyQualifiedMethod: "test/test") + ) ) // Server sends initial metadata @@ -2470,7 +2514,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) XCTAssertEqual( receiveMetadataAction, - .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + .receivedMetadata( + Metadata(headers: .clientInitialMetadata), + MethodDescriptor(fullyQualifiedMethod: "test/test") + ) ) // Client sends messages @@ -2547,7 +2594,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) XCTAssertEqual( receiveMetadataAction, - .receivedMetadata(Metadata(headers: .clientInitialMetadata)) + .receivedMetadata( + Metadata(headers: .clientInitialMetadata), + MethodDescriptor(fullyQualifiedMethod: "test/test") + ) ) // Client sends messages diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index e05be5d8e..ef7162f14 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -26,13 +26,14 @@ import XCTest @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCServerStreamHandlerTests: XCTestCase { func testH2FramesAreIgnored() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1 + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) let framesToBeIgnored: [HTTP2Frame.FramePayload] = [ .ping(.init(), ack: false), @@ -54,13 +55,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testClientInitialMetadataWithoutContentTypeResultsInRejectedRPC() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1 + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata without content-type let clientInitialMetadata: HPACKHeaders = [ @@ -83,13 +85,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testClientInitialMetadataWithoutMethodResultsInRejectedRPC() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1 + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata without :method let clientInitialMetadata: HPACKHeaders = [ @@ -121,13 +124,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testClientInitialMetadataWithoutSchemeResultsInRejectedRPC() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1 + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata without :scheme let clientInitialMetadata: HPACKHeaders = [ @@ -159,13 +163,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testClientInitialMetadataWithoutPathResultsInRejectedRPC() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1 + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata without :path let clientInitialMetadata: HPACKHeaders = [ @@ -188,7 +193,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.unimplemented.rawValue), + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), GRPCHTTP2Keys.grpcStatusMessage.rawValue: "No :path header has been set.", ] ) @@ -196,13 +201,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testClientInitialMetadataWithoutTEResultsInRejectedRPC() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1 + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata without TE let clientInitialMetadata: HPACKHeaders = [ @@ -234,13 +240,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testNotAcceptedEncodingResultsInRejectedRPC() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100 + maximumPayloadSize: 100, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata let clientInitialMetadata: HPACKHeaders = [ @@ -275,13 +282,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testOverMaximumPayloadSize() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1 + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata let clientInitialMetadata: HPACKHeaders = [ @@ -346,14 +354,15 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testClientEndsStream() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maximumPayloadSize: 1, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self), skipStateMachineAssertions: true ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata with end stream set let clientInitialMetadata: HPACKHeaders = [ @@ -411,14 +420,15 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testNormalFlow() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maximumPayloadSize: 42, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self), skipStateMachineAssertions: true ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata let clientInitialMetadata: HPACKHeaders = [ @@ -520,13 +530,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testReceiveMessageSplitAcrossMultipleBuffers() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100 + maximumPayloadSize: 100, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata let clientInitialMetadata: HPACKHeaders = [ @@ -615,13 +626,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testSendMultipleMessagesInSingleBuffer() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100 + maximumPayloadSize: 100, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata let clientInitialMetadata: HPACKHeaders = [ @@ -692,13 +704,14 @@ final class GRPCServerStreamHandlerTests: XCTestCase { } func testMessageAndStatusAreNotReordered() throws { + let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100 + maximumPayloadSize: 100, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) - - let channel = EmbeddedChannel(handler: handler) + try channel.pipeline.syncOperations.addHandler(handler) // Receive client's initial metadata let clientInitialMetadata: HPACKHeaders = [ @@ -770,6 +783,103 @@ final class GRPCServerStreamHandlerTests: XCTestCase { // Make sure we get nothing else. XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) } + + func testMethodDescriptorPromiseSucceeds() throws { + let channel = EmbeddedChannel() + let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + methodDescriptorPromise: promise, + skipStateMachineAssertions: true + ) + try channel.pipeline.syncOperations.addHandler(handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "SomeService/SomeMethod", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + XCTAssertEqual( + try promise.futureResult.wait(), + MethodDescriptor(fullyQualifiedMethod: "SomeService/SomeMethod") + ) + } + + func testMethodDescriptorPromiseIsFailedWhenHandlerRemoved() throws { + let channel = EmbeddedChannel() + let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + methodDescriptorPromise: promise, + skipStateMachineAssertions: true + ) + try channel.pipeline.syncOperations.addHandler(handler) + + try channel.pipeline.syncOperations.removeHandler(handler).wait() + + XCTAssertThrowsError( + ofType: RPCError.self, + try promise.futureResult.wait() + ) { error in + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(error.message, "RPC stream was closed before we got any Metadata.") + } + } + + func testMethodDescriptorPromiseIsFailedIfRPCRejected() throws { + let channel = EmbeddedChannel() + let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + methodDescriptorPromise: promise, + skipStateMachineAssertions: true + ) + try channel.pipeline.syncOperations.addHandler(handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "SomeService/SomeMethod", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/not-valid-contenttype", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + XCTAssertThrowsError( + ofType: RPCError.self, + try promise.futureResult.wait() + ) { error in + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(error.message, "RPC was rejected.") + } + } } extension EmbeddedChannel { @@ -799,3 +909,6 @@ extension EmbeddedChannel { private enum TestError: Error { case assertionFailure(String) } + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCServerStreamHandler: RemovableChannelHandler {} From 025d0df08243eaea8ff10ec7975787cd8e6be64d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 28 May 2024 10:32:28 +0100 Subject: [PATCH 321/580] Improve naming (#1888) Motivation: We recently standardised on 'config' over 'configuration' for service and method config. However, load balancing config was missed. We should also standardise other service config names; retry throttling policy should become retry throttling. Modifications: - replace configuration with config - drop 'policy' from retry throttling policy Result: Better naming --- .../Configuration/ServiceConfig.swift | 38 ++++++++--------- .../GRPCCore/Transport/RetryThrottle.swift | 2 +- .../InProcessClientTransport.swift | 4 +- .../ServiceConfigCodingTests.swift | 42 ++++++++----------- 4 files changed, 39 insertions(+), 47 deletions(-) diff --git a/Sources/GRPCCore/Configuration/ServiceConfig.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift index 22ee5fcd3..d5d160bb3 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -26,11 +26,11 @@ public struct ServiceConfig: Hashable, Sendable { /// /// The client iterates through the list in order and picks the first configuration it supports. /// If no policies are supported then the configuration is considered to be invalid. - public var loadBalancingConfiguration: [LoadBalancingConfiguration] + public var loadBalancingConfig: [LoadBalancingConfig] /// The policy for throttling retries. /// - /// If a ``RetryThrottlingPolicy`` is provided, gRPC will automatically throttle retry attempts + /// If ``RetryThrottling`` is provided, gRPC will automatically throttle retry attempts /// and hedged RPCs when the client's ratio of failures to successes exceeds a threshold. /// /// For each server name, the gRPC client will maintain a `token_count` which is initially set @@ -42,23 +42,23 @@ public struct ServiceConfig: Hashable, Sendable { /// /// If `token_count` is less than or equal to `max_tokens / 2`, then RPCs will not be retried /// and hedged RPCs will not be sent. - public var retryThrottlingPolicy: RetryThrottlingPolicy? + public var retryThrottling: RetryThrottling? /// Creates a new ``ServiceConfig``. /// /// - Parameters: /// - methodConfig: Per-method configuration. - /// - loadBalancingConfiguration: Load balancing policies. Clients use the the first supported + /// - loadBalancingConfig: Load balancing policies. Clients use the the first supported /// policy when iterating the list in order. - /// - retryThrottlingPolicy: Policy for throttling retries. + /// - retryThrottling: Policy for throttling retries. public init( methodConfig: [MethodConfig] = [], - loadBalancingConfiguration: [LoadBalancingConfiguration] = [], - retryThrottlingPolicy: RetryThrottlingPolicy? = nil + loadBalancingConfig: [LoadBalancingConfig] = [], + retryThrottling: RetryThrottling? = nil ) { self.methodConfig = methodConfig - self.loadBalancingConfiguration = loadBalancingConfiguration - self.retryThrottlingPolicy = retryThrottlingPolicy + self.loadBalancingConfig = loadBalancingConfig + self.retryThrottling = retryThrottling } } @@ -80,13 +80,13 @@ extension ServiceConfig: Codable { self.methodConfig = methodConfig ?? [] let loadBalancingConfiguration = try container.decodeIfPresent( - [LoadBalancingConfiguration].self, + [LoadBalancingConfig].self, forKey: .loadBalancingConfig ) - self.loadBalancingConfiguration = loadBalancingConfiguration ?? [] + self.loadBalancingConfig = loadBalancingConfiguration ?? [] - self.retryThrottlingPolicy = try container.decodeIfPresent( - RetryThrottlingPolicy.self, + self.retryThrottling = try container.decodeIfPresent( + RetryThrottling.self, forKey: .retryThrottling ) } @@ -94,15 +94,15 @@ extension ServiceConfig: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.methodConfig, forKey: .methodConfig) - try container.encode(self.loadBalancingConfiguration, forKey: .loadBalancingConfig) - try container.encodeIfPresent(self.retryThrottlingPolicy, forKey: .retryThrottling) + try container.encode(self.loadBalancingConfig, forKey: .loadBalancingConfig) + try container.encodeIfPresent(self.retryThrottling, forKey: .retryThrottling) } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceConfig { /// Configuration used by clients for load-balancing. - public struct LoadBalancingConfiguration: Hashable, Sendable { + public struct LoadBalancingConfig: Hashable, Sendable { private enum Value: Hashable, Sendable { case pickFirst(PickFirst) case roundRobin(RoundRobin) @@ -166,7 +166,7 @@ extension ServiceConfig { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ServiceConfig.LoadBalancingConfiguration { +extension ServiceConfig.LoadBalancingConfig { /// Configuration for the pick-first load balancing policy. public struct PickFirst: Hashable, Sendable, Codable { /// Whether the resolved addresses should be shuffled before attempting to connect to them. @@ -194,7 +194,7 @@ extension ServiceConfig.LoadBalancingConfiguration { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ServiceConfig.LoadBalancingConfiguration: Codable { +extension ServiceConfig.LoadBalancingConfig: Codable { private enum CodingKeys: String, CodingKey { case roundRobin = "round_robin" case pickFirst = "pick_first" @@ -226,7 +226,7 @@ extension ServiceConfig.LoadBalancingConfiguration: Codable { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceConfig { - public struct RetryThrottlingPolicy: Hashable, Sendable, Codable { + public struct RetryThrottling: Hashable, Sendable, Codable { /// The initial, and maximum number of tokens. /// /// - Precondition: Must be greater than zero. diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index 15658a0ae..0e94d85d6 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -107,7 +107,7 @@ public struct RetryThrottle: Sendable { /// /// - Parameter policy: The policy to use to configure the throttle. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public init(policy: ServiceConfig.RetryThrottlingPolicy) { + public init(policy: ServiceConfig.RetryThrottling) { self.init(maximumTokens: policy.maxTokens, tokenRatio: policy.tokenRatio) } diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index 34a6f6975..d7fb93bd8 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -110,9 +110,7 @@ public struct InProcessClientTransport: ClientTransport { server: InProcessServerTransport, serviceConfig: ServiceConfig = ServiceConfig() ) { - self.retryThrottle = serviceConfig.retryThrottlingPolicy.map { - RetryThrottle(policy: $0) - } + self.retryThrottle = serviceConfig.retryThrottling.map { RetryThrottle(policy: $0) } self.methodConfig = _MethodConfigs(serviceConfig: serviceConfig) self.state = _LockedValueBox(.unconnected(.init(serverTransport: server))) } diff --git a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift index 8cfcebf9e..03786cc0c 100644 --- a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift @@ -46,9 +46,9 @@ final class ServiceConfigCodingTests: XCTestCase { } """ - let expected = try ServiceConfig.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) + let expected = try ServiceConfig.RetryThrottling(maxTokens: 10, tokenRatio: 0.5) let policy = try self.decoder.decode( - ServiceConfig.RetryThrottlingPolicy.self, + ServiceConfig.RetryThrottling.self, from: Data(json.utf8) ) @@ -56,7 +56,7 @@ final class ServiceConfigCodingTests: XCTestCase { } func testEncodeDecodeRetryThrottlingPolicy() throws { - let policy = try ServiceConfig.RetryThrottlingPolicy(maxTokens: 10, tokenRatio: 0.5) + let policy = try ServiceConfig.RetryThrottling(maxTokens: 10, tokenRatio: 0.5) try self.testRoundTripEncodeDecode(policy) } @@ -72,7 +72,7 @@ final class ServiceConfigCodingTests: XCTestCase { try self.testDecodeThrowsRuntimeError( json: json, - as: ServiceConfig.RetryThrottlingPolicy.self + as: ServiceConfig.RetryThrottling.self ) } } @@ -89,13 +89,13 @@ final class ServiceConfigCodingTests: XCTestCase { try self.testDecodeThrowsRuntimeError( json: json, - as: ServiceConfig.RetryThrottlingPolicy.self + as: ServiceConfig.RetryThrottling.self ) } } func testDecodePickFirstPolicy() throws { - let inputs: [(String, ServiceConfig.LoadBalancingConfiguration.PickFirst)] = [ + let inputs: [(String, ServiceConfig.LoadBalancingConfig.PickFirst)] = [ (#"{"shuffleAddressList": true}"#, .init(shuffleAddressList: true)), (#"{"shuffleAddressList": false}"#, .init(shuffleAddressList: false)), (#"{}"#, .init(shuffleAddressList: false)), @@ -103,7 +103,7 @@ final class ServiceConfigCodingTests: XCTestCase { for (input, expected) in inputs { let pickFirst = try self.decoder.decode( - ServiceConfig.LoadBalancingConfiguration.PickFirst.self, + ServiceConfig.LoadBalancingConfig.PickFirst.self, from: Data(input.utf8) ) @@ -112,7 +112,7 @@ final class ServiceConfigCodingTests: XCTestCase { } func testEncodePickFirstPolicy() throws { - let inputs: [(ServiceConfig.LoadBalancingConfiguration.PickFirst, String)] = [ + let inputs: [(ServiceConfig.LoadBalancingConfig.PickFirst, String)] = [ (.init(shuffleAddressList: true), #"{"shuffleAddressList":true}"#), (.init(shuffleAddressList: false), #"{"shuffleAddressList":false}"#), ] @@ -126,20 +126,20 @@ final class ServiceConfigCodingTests: XCTestCase { func testDecodeRoundRobinPolicy() throws { let json = "{}" let policy = try self.decoder.decode( - ServiceConfig.LoadBalancingConfiguration.RoundRobin.self, + ServiceConfig.LoadBalancingConfig.RoundRobin.self, from: Data(json.utf8) ) - XCTAssertEqual(policy, ServiceConfig.LoadBalancingConfiguration.RoundRobin()) + XCTAssertEqual(policy, ServiceConfig.LoadBalancingConfig.RoundRobin()) } func testEncodeRoundRobinPolicy() throws { - let policy = ServiceConfig.LoadBalancingConfiguration.RoundRobin() + let policy = ServiceConfig.LoadBalancingConfig.RoundRobin() let encoded = try self.encoder.encode(policy) XCTAssertEqual(String(decoding: encoded, as: UTF8.self), "{}") } func testDecodeLoadBalancingConfiguration() throws { - let inputs: [(String, ServiceConfig.LoadBalancingConfiguration)] = [ + let inputs: [(String, ServiceConfig.LoadBalancingConfig)] = [ (#"{"round_robin": {}}"#, .roundRobin), (#"{"pick_first": {}}"#, .pickFirst(shuffleAddressList: false)), (#"{"pick_first": {"shuffleAddressList": false}}"#, .pickFirst(shuffleAddressList: false)), @@ -147,7 +147,7 @@ final class ServiceConfigCodingTests: XCTestCase { for (input, expected) in inputs { let decoded = try self.decoder.decode( - ServiceConfig.LoadBalancingConfiguration.self, + ServiceConfig.LoadBalancingConfig.self, from: Data(input.utf8) ) XCTAssertEqual(decoded, expected) @@ -155,7 +155,7 @@ final class ServiceConfigCodingTests: XCTestCase { } func testEncodeLoadBalancingConfiguration() throws { - let inputs: [(ServiceConfig.LoadBalancingConfiguration, String)] = [ + let inputs: [(ServiceConfig.LoadBalancingConfig, String)] = [ (.roundRobin, #"{"round_robin":{}}"#), (.pickFirst(shuffleAddressList: false), #"{"pick_first":{"shuffleAddressList":false}}"#), ] @@ -206,14 +206,11 @@ final class ServiceConfigCodingTests: XCTestCase { maxResponseMessageBytes: 456 ) ], - loadBalancingConfiguration: [ + loadBalancingConfig: [ .roundRobin, .pickFirst(shuffleAddressList: true), ], - retryThrottlingPolicy: try ServiceConfig.RetryThrottlingPolicy( - maxTokens: 10, - tokenRatio: 0.1 - ) + retryThrottling: try ServiceConfig.RetryThrottling(maxTokens: 10, tokenRatio: 0.1) ) XCTAssertEqual(decoded, expected) @@ -246,14 +243,11 @@ final class ServiceConfigCodingTests: XCTestCase { maxRequestMessageBytes: 10_000 ), ], - loadBalancingConfiguration: [ + loadBalancingConfig: [ .pickFirst(shuffleAddressList: true), .roundRobin, ], - retryThrottlingPolicy: try ServiceConfig.RetryThrottlingPolicy( - maxTokens: 10, - tokenRatio: 3.141 - ) + retryThrottling: try ServiceConfig.RetryThrottling(maxTokens: 10, tokenRatio: 3.141) ) try self.testRoundTripEncodeDecode(serviceConfig) From 831b57a079a3c6a198403fb0ac43ac6a30811cc8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 28 May 2024 15:50:07 +0100 Subject: [PATCH 322/580] Add a sendable view to ClientConnectionHandler/ServerConnectionManagementHandler (#1895) Motivation: In #1875 the `ClientConnectionHandler` and `ServerConnectionManagementHandler` had conformance added to `NIOHTTP2StreamDelegate`. This, in turn, requires that they are `Sendable`. However, they aren't really `Sendable` as most of their API relies on being on the correct event-loop. Modifications: - Add stream-delegate views over each where the conformance ensures that the methods are called on the appropriate event loop Result: Fewer warnings --- .../Connection/ClientConnectionHandler.swift | 44 ++++++++++++++++--- .../ServerConnectionManagementHandler.swift | 44 ++++++++++++++++--- .../ClientConnectionHandlerTests.swift | 2 +- ...rverConnectionManagementHandlerTests.swift | 2 +- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index d270eba73..7b2fbc51c 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -152,10 +152,10 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { switch event { case let event as NIOHTTP2StreamCreatedEvent: - self.streamCreated(event.streamID, channel: context.channel) + self._streamCreated(event.streamID, channel: context.channel) case let event as StreamClosedEvent: - self.streamClosed(event.streamID, channel: context.channel) + self._streamClosed(event.streamID, channel: context.channel) default: () @@ -250,8 +250,42 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } } -extension ClientConnectionHandler: NIOHTTP2StreamDelegate { - func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { +extension ClientConnectionHandler { + struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { + // @unchecked is okay: the only methods do the appropriate event-loop dance. + + private let handler: ClientConnectionHandler + + init(_ handler: ClientConnectionHandler) { + self.handler = handler + } + + func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { + if self.handler.eventLoop.inEventLoop { + self.handler._streamCreated(id, channel: channel) + } else { + self.handler.eventLoop.execute { + self.handler._streamCreated(id, channel: channel) + } + } + } + + func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { + if self.handler.eventLoop.inEventLoop { + self.handler._streamClosed(id, channel: channel) + } else { + self.handler.eventLoop.execute { + self.handler._streamClosed(id, channel: channel) + } + } + } + } + + var http2StreamDelegate: HTTP2StreamDelegate { + return HTTP2StreamDelegate(self) + } + + private func _streamCreated(_ id: HTTP2StreamID, channel: any Channel) { self.eventLoop.assertInEventLoop() // Stream created, so the connection isn't idle. @@ -259,7 +293,7 @@ extension ClientConnectionHandler: NIOHTTP2StreamDelegate { self.state.streamOpened(id) } - func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { + private func _streamClosed(_ id: HTTP2StreamID, channel: any Channel) { guard let context = self.context else { return } self.eventLoop.assertInEventLoop() diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index 4739651aa..2e433582e 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -274,10 +274,10 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { switch event { case let event as NIOHTTP2StreamCreatedEvent: - self.streamCreated(event.streamID, channel: context.channel) + self._streamCreated(event.streamID, channel: context.channel) case let event as StreamClosedEvent: - self.streamClosed(event.streamID, channel: context.channel) + self._streamClosed(event.streamID, channel: context.channel) case is ChannelShouldQuiesceEvent: self.initiateGracefulShutdown(context: context) @@ -333,14 +333,48 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { } } -extension ServerConnectionManagementHandler: NIOHTTP2StreamDelegate { - func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { +extension ServerConnectionManagementHandler { + struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { + // @unchecked is okay: the only methods do the appropriate event-loop dance. + + private let handler: ServerConnectionManagementHandler + + init(_ handler: ServerConnectionManagementHandler) { + self.handler = handler + } + + func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { + if self.handler.eventLoop.inEventLoop { + self.handler._streamCreated(id, channel: channel) + } else { + self.handler.eventLoop.execute { + self.handler._streamCreated(id, channel: channel) + } + } + } + + func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { + if self.handler.eventLoop.inEventLoop { + self.handler._streamClosed(id, channel: channel) + } else { + self.handler.eventLoop.execute { + self.handler._streamClosed(id, channel: channel) + } + } + } + } + + var http2StreamDelegate: HTTP2StreamDelegate { + return HTTP2StreamDelegate(self) + } + + private func _streamCreated(_ id: HTTP2StreamID, channel: any Channel) { // The connection isn't idle if a stream is open. self.maxIdleTimer?.cancel() self.state.streamOpened(id) } - func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { + private func _streamClosed(_ id: HTTP2StreamID, channel: any Channel) { guard let context = self.context else { return } switch self.state.streamClosed(id) { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 6483456d3..1f7a7983f 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -246,7 +246,7 @@ extension ClientConnectionHandlerTests { keepaliveWithoutCalls: allowKeepaliveWithoutCalls ) - self.streamDelegate = handler + self.streamDelegate = handler.http2StreamDelegate self.channel = EmbeddedChannel(handler: handler, loop: loop) } diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift index 67f2d226b..672f6264b 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift @@ -379,7 +379,7 @@ extension ServerConnectionManagementHandlerTests { clock: self.clock ) - self.streamDelegate = handler + self.streamDelegate = handler.http2StreamDelegate self.syncView = handler.syncView self.channel = EmbeddedChannel(handler: handler, loop: loop) } From 827303d0dbda9aec9f70224dfb8b1971921bf152 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 28 May 2024 15:54:48 +0100 Subject: [PATCH 323/580] Refactor round robin tests (#1894) Motivation: The test harness for the round-robin load balancer tests doesn't need to be so tightly coupled to round-robin tests. It can also be used for the pick-first load balancer. Modifications: - Move the RR LB test into its own file - Refactor so the test context holds a `LoadBalancer` rather than a `RoundRobinLoadBalancer` Result: Can be easily extended for pick-first LB tests --- .../LoadBalancers/LoadBalancerTest.swift | 148 ++++++++++++++++++ .../RoundRobinLoadBalancerTests.swift | 139 +++------------- 2 files changed, 166 insertions(+), 121 deletions(-) create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift new file mode 100644 index 000000000..92295ff81 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift @@ -0,0 +1,148 @@ +/* + * 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. + */ + +@_spi(Package) @testable import GRPCHTTP2Core +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +enum LoadBalancerTest { + struct Context { + let servers: [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)] + let loadBalancer: LoadBalancer + } + + static func roundRobin( + servers serverCount: Int, + connector: any HTTP2Connector, + backoff: ConnectionBackoff = .defaults, + timeout: Duration = .seconds(10), + function: String = #function, + handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, + verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in } + ) async throws { + try await Self.run( + servers: serverCount, + timeout: timeout, + function: function, + handleEvent: handleEvent, + verifyEvents: verifyEvents + ) { + let roundRobin = RoundRobinLoadBalancer( + connector: connector, + backoff: backoff, + defaultCompression: .none, + enabledCompression: .none + ) + return .roundRobin(roundRobin) + } + } + + private static func run( + servers serverCount: Int, + timeout: Duration, + function: String, + handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, + verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in }, + makeLoadBalancer: @escaping @Sendable () -> LoadBalancer + ) async throws { + enum TestEvent { + case timedOut + case completed(Result) + } + + try await withThrowingTaskGroup(of: TestEvent.self) { group in + group.addTask { + try? await Task.sleep(for: timeout) + return .timedOut + } + + group.addTask { + do { + try await Self._run( + servers: serverCount, + handleEvent: handleEvent, + verifyEvents: verifyEvents, + makeLoadBalancer: makeLoadBalancer + ) + return .completed(.success(())) + } catch { + return .completed(.failure(error)) + } + } + + let result = try await group.next()! + group.cancelAll() + + switch result { + case .timedOut: + XCTFail("'\(function)' timed out after \(timeout)") + case .completed(let result): + try result.get() + } + } + } + + private static func _run( + servers serverCount: Int, + handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, + verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void, + makeLoadBalancer: @escaping @Sendable () -> LoadBalancer + ) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + // Create the test servers. + var servers = [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)]() + for _ in 0 ..< serverCount { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + servers.append((server, address)) + + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + } + + // Create the load balancer. + let loadBalancer = makeLoadBalancer() + + group.addTask { + await loadBalancer.run() + } + + let context = Context(servers: servers, loadBalancer: loadBalancer) + + var events = [LoadBalancerEvent]() + for await event in loadBalancer.events { + events.append(event) + try await handleEvent(context, event) + } + + verifyEvents(events) + group.cancelAll() + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension LoadBalancerTest.Context { + var roundRobin: RoundRobinLoadBalancer? { + switch self.loadBalancer { + case .roundRobin(let loadBalancer): + return loadBalancer + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift index f9f969228..c7060578f 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift @@ -24,13 +24,13 @@ import XCTest @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class RoundRobinLoadBalancerTests: XCTestCase { func testMultipleConnectionsAreEstablished() async throws { - try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): // Update the addresses for the load balancer, this will trigger subchannels to be created // for each. let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) case .connectivityStateChanged(.ready): // Poll until each server has one connected client. @@ -56,13 +56,13 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } func testSubchannelsArePickedEvenly() async throws { - try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): // Update the addresses for the load balancer, this will trigger subchannels to be created // for each. let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) case .connectivityStateChanged(.ready): // Subchannel is ready. This happens when any subchannel becomes ready. Loop until @@ -110,12 +110,12 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } func testAddressUpdatesAreHandledGracefully() async throws { - try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): // Do the first connect. let endpoints = [Endpoint(addresses: [context.servers[0].address])] - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) case .connectivityStateChanged(.ready): // Now the first connection should be established. @@ -131,7 +131,7 @@ final class RoundRobinLoadBalancerTests: XCTestCase { Endpoint(addresses: [context.servers[0].address]), Endpoint(addresses: [context.servers[1].address]), ] - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) try await XCTPoll(every: .milliseconds(10)) { context.servers.prefix(2).allSatisfy { $0.server.clients.count == 1 } @@ -141,7 +141,7 @@ final class RoundRobinLoadBalancerTests: XCTestCase { // Remove those two endpoints and add a third. do { let endpoints = [Endpoint(addresses: [context.servers[2].address])] - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) try await XCTPoll(every: .milliseconds(10)) { let disconnected = context.servers.prefix(2).allSatisfy { $0.server.clients.isEmpty } @@ -169,16 +169,16 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } func testSameAddressUpdatesAreIgnored() async throws { - try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) case .connectivityStateChanged(.ready): // Update with the same addresses, these should be ignored. let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) // We should still have three connections. try await XCTPoll(every: .milliseconds(10)) { @@ -202,15 +202,15 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } func testEmptyAddressUpdatesAreIgnored() async throws { - try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) case .connectivityStateChanged(.ready): // Update with no-addresses, should be ignored so a subchannel can still be picked. - context.loadBalancer.updateAddresses([]) + context.roundRobin!.updateAddresses([]) // We should still have three connections. try await XCTPoll(every: .milliseconds(10)) { @@ -234,12 +234,12 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } func testSubchannelReceivesGoAway() async throws { - try await RoundRobinLoadBalancerTest.run(servers: 3, connector: .posix()) { context, event in + try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): // Trigger the connect. let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) case .connectivityStateChanged(.ready): // Wait for all servers to become ready. @@ -284,7 +284,6 @@ final class RoundRobinLoadBalancerTests: XCTestCase { default: () } - } verifyEvents: { events in let expected: [LoadBalancerEvent] = [ .connectivityStateChanged(.idle), @@ -326,7 +325,7 @@ final class RoundRobinLoadBalancerTests: XCTestCase { let idle = ManagedAtomic(0) let ready = ManagedAtomic(0) - try await RoundRobinLoadBalancerTest.run( + try await LoadBalancerTest.roundRobin( servers: 1, connector: .posix(maxIdleTime: .milliseconds(25)) // Aggressively idle the connection ) { context, event in @@ -340,7 +339,7 @@ final class RoundRobinLoadBalancerTests: XCTestCase { // which it will connect to. Wait for it to be ready and then idle again. let address = context.servers[0].address let endpoints = [Endpoint(addresses: [address])] - context.loadBalancer.updateAddresses(endpoints) + context.roundRobin!.updateAddresses(endpoints) case 2: // Load-balancer has the endpoints but all are idle. Picking will trigger a connect. @@ -379,105 +378,3 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } } } - -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -enum RoundRobinLoadBalancerTest { - struct Context { - let servers: [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)] - let loadBalancer: RoundRobinLoadBalancer - } - - static func run( - servers serverCount: Int, - connector: any HTTP2Connector, - backoff: ConnectionBackoff = .defaults, - timeout: Duration = .seconds(10), - function: String = #function, - handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, - verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in } - ) async throws { - enum TestEvent { - case timedOut - case completed(Result) - } - - try await withThrowingTaskGroup(of: TestEvent.self) { group in - group.addTask { - try? await Task.sleep(for: timeout) - return .timedOut - } - - group.addTask { - do { - try await Self._run( - servers: serverCount, - connector: connector, - backoff: backoff, - handleEvent: handleEvent, - verifyEvents: verifyEvents - ) - return .completed(.success(())) - } catch { - return .completed(.failure(error)) - } - } - - let result = try await group.next()! - group.cancelAll() - - switch result { - case .timedOut: - XCTFail("'\(function)' timed out after \(timeout)") - case .completed(let result): - try result.get() - } - } - } - - private static func _run( - servers serverCount: Int, - connector: some HTTP2Connector, - backoff: ConnectionBackoff, - handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, - verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void - ) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - // Create the test servers. - var servers = [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)]() - for _ in 1 ... serverCount { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - servers.append((server, address)) - - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - } - - // Create the load balancer. - let loadBalancer = RoundRobinLoadBalancer( - connector: connector, - backoff: backoff, - defaultCompression: .none, - enabledCompression: .none - ) - - group.addTask { - await loadBalancer.run() - } - - let context = Context(servers: servers, loadBalancer: loadBalancer) - - var events = [LoadBalancerEvent]() - for await event in loadBalancer.events { - events.append(event) - try await handleEvent(context, event) - } - - verifyEvents(events) - group.cancelAll() - } - } -} From 3272c32b4276f8878c9cff196abb6863bf3e7531 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 30 May 2024 08:02:38 +0100 Subject: [PATCH 324/580] Add NIOPosix server transport implementation (#1883) Motivation: We want to provide some commonly used server transport implementations out of the box. Modifications: This PR adds a `ServerTransport` implementation based on `NIOPosix`. Result: A `NIOPosix` implementation of a server transport. --- .../Streaming/RPCWriter+Closable.swift | 2 +- .../Client/Connection/Connection.swift | 4 +- .../Internal/NIOChannelPipeline+GRPC.swift | 102 +++++++++ .../Server/Connection/ServerConnection.swift | 52 +++++ .../Server/HTTP2ServerTransport.swift | 163 ++++++++++++++ .../GRPCHTTP2TransportNIOPosix.swift | 203 +++++++++++++++++- ...CExecutorTestHarness+ServerBehavior.swift} | 0 .../HTTP2ServerTransportConfigTests.swift | 53 +++++ 8 files changed, 574 insertions(+), 5 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift create mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift create mode 100644 Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift rename Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/{ClientRPCExecutorTestHasness+ServerBehavior.swift => ClientRPCExecutorTestHarness+ServerBehavior.swift} (100%) create mode 100644 Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift diff --git a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift index 8746a0b0e..01418e631 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -24,7 +24,7 @@ extension RPCWriter { /// /// - Parameter other: The writer to wrap. @inlinable - init(wrapping other: some ClosableRPCWriterProtocol) { + public init(wrapping other: some ClosableRPCWriterProtocol) { self.writer = other } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index ea81ce34d..b25f1fd6a 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -391,7 +391,7 @@ extension Connection { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Connection { - private enum State { + private enum State: Sendable { /// The connection is idle or connecting. case notConnected /// A TCP connection has been established with the remote peer. However, the connection may not @@ -402,7 +402,7 @@ extension Connection { /// The connection has closed. This is a terminal state. case closed - struct Connected { + struct Connected: Sendable { /// The connection channel. var channel: NIOAsyncChannel /// Multiplexer for creating HTTP/2 streams. diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift new file mode 100644 index 000000000..85b6aea49 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -0,0 +1,102 @@ +/* + * 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 NIOCore +import NIOHPACK +import NIOHTTP2 + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension ChannelPipeline.SynchronousOperations { + @_spi(Package) public typealias HTTP2ConnectionChannel = NIOAsyncChannel + @_spi(Package) public typealias HTTP2StreamMultiplexer = NIOHTTP2Handler.AsyncStreamMultiplexer< + (NIOAsyncChannel, EventLoopFuture) + > + + @_spi(Package) + public func configureGRPCServerPipeline( + channel: any Channel, + compressionConfig: HTTP2ServerTransport.Config.Compression, + keepaliveConfig: HTTP2ServerTransport.Config.Keepalive, + connectionConfig: HTTP2ServerTransport.Config.Connection, + http2Config: HTTP2ServerTransport.Config.HTTP2, + rpcConfig: HTTP2ServerTransport.Config.RPC, + useTLS: Bool + ) throws -> (HTTP2ConnectionChannel, HTTP2StreamMultiplexer) { + let serverConnectionHandler = ServerConnectionManagementHandler( + eventLoop: self.eventLoop, + maxIdleTime: connectionConfig.maxIdleTime.map { TimeAmount($0) }, + maxAge: connectionConfig.maxAge.map { TimeAmount($0) }, + maxGraceTime: connectionConfig.maxGraceTime.map { TimeAmount($0) }, + keepaliveTime: TimeAmount(keepaliveConfig.time), + keepaliveTimeout: TimeAmount(keepaliveConfig.timeout), + allowKeepaliveWithoutCalls: keepaliveConfig.permitWithoutCalls, + minPingIntervalWithoutCalls: TimeAmount(keepaliveConfig.minPingIntervalWithoutCalls) + ) + let flushNotificationHandler = GRPCServerFlushNotificationHandler( + serverConnectionManagementHandler: serverConnectionHandler + ) + try self.addHandler(flushNotificationHandler) + + var http2HandlerConnectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() + var http2HandlerHTTP2Settings = HTTP2Settings([ + HTTP2Setting(parameter: .initialWindowSize, value: http2Config.targetWindowSize), + HTTP2Setting(parameter: .maxFrameSize, value: http2Config.maxFrameSize), + HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize), + ]) + if let maxConcurrentStreams = http2Config.maxConcurrentStreams { + http2HandlerHTTP2Settings.append( + HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams) + ) + } + http2HandlerConnectionConfiguration.initialSettings = http2HandlerHTTP2Settings + + var http2HandlerStreamConfiguration = NIOHTTP2Handler.StreamConfiguration() + http2HandlerStreamConfiguration.targetWindowSize = http2Config.targetWindowSize + + let streamMultiplexer = try self.configureAsyncHTTP2Pipeline( + mode: .server, + configuration: NIOHTTP2Handler.Configuration( + connection: http2HandlerConnectionConfiguration, + stream: http2HandlerStreamConfiguration + ) + ) { streamChannel in + return streamChannel.eventLoop.makeCompletedFuture { + let methodDescriptorPromise = streamChannel.eventLoop.makePromise(of: MethodDescriptor.self) + let streamHandler = GRPCServerStreamHandler( + scheme: useTLS ? .https : .http, + acceptedEncodings: compressionConfig.enabledAlgorithms, + maximumPayloadSize: rpcConfig.maxRequestPayloadSize, + methodDescriptorPromise: methodDescriptorPromise + ) + try streamChannel.pipeline.syncOperations.addHandler(streamHandler) + + let asyncStreamChannel = try NIOAsyncChannel( + wrappingChannelSynchronously: streamChannel + ) + return (asyncStreamChannel, methodDescriptorPromise.futureResult) + } + } + + try self.addHandler(serverConnectionHandler) + + let connectionChannel = try NIOAsyncChannel( + wrappingChannelSynchronously: channel + ) + + return (connectionChannel, streamMultiplexer) + } +} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift new file mode 100644 index 000000000..da5cf5661 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift @@ -0,0 +1,52 @@ +/* + * 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 NIOCore + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public enum ServerConnection { + public enum Stream { + @_spi(Package) + public struct Outbound: ClosableRPCWriterProtocol { + public typealias Element = RPCResponsePart + + private let responseWriter: NIOAsyncChannelOutboundWriter + private let http2Stream: NIOAsyncChannel + + public init( + responseWriter: NIOAsyncChannelOutboundWriter, + http2Stream: NIOAsyncChannel + ) { + self.responseWriter = responseWriter + self.http2Stream = http2Stream + } + + public func write(contentsOf elements: some Sequence) async throws { + try await self.responseWriter.write(contentsOf: elements) + } + + public func finish() { + self.responseWriter.finish() + } + + public func finish(throwing error: any Error) { + // Fire the error inbound; this fails the inbound writer. + self.http2Stream.channel.pipeline.fireErrorCaught(error) + } + } + } +} diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift new file mode 100644 index 000000000..0e6d8ffdb --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift @@ -0,0 +1,163 @@ +/* + * 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 NIOHTTP2 + +/// A namespace for the HTTP/2 server transport. +public enum HTTP2ServerTransport {} + +extension HTTP2ServerTransport { + /// A namespace for HTTP/2 server transport configuration. + public enum Config {} +} + +extension HTTP2ServerTransport.Config { + public struct Compression: Sendable { + /// Compression algorithms enabled for inbound messages. + /// + /// - Note: ``CompressionAlgorithm/none`` is always supported, even if it isn't set here. + public var enabledAlgorithms: CompressionAlgorithmSet + + /// Creates a new compression configuration. + /// + /// - SeeAlso: ``defaults``. + public init(enabledAlgorithms: CompressionAlgorithmSet) { + self.enabledAlgorithms = enabledAlgorithms + } + + /// Default values, compression is disabled. + public static var defaults: Self { + Self(enabledAlgorithms: .none) + } + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public struct Keepalive: Sendable { + /// The amount of time to wait after reading data before sending a keepalive ping. + public var time: Duration + + /// The amount of time the server has to respond to a keepalive ping before the connection is closed. + public var timeout: Duration + + /// Whether the server allows the client to send keepalive pings when there are no calls in progress. + public var permitWithoutCalls: Bool + + /// The minimum allowed interval the client is allowed to send keep-alive pings. + /// Pings more frequent than this interval count as 'strikes' and the connection is closed if there are + /// too many strikes. + public var minPingIntervalWithoutCalls: Duration + + /// Creates a new keepalive configuration. + public init( + time: Duration, + timeout: Duration, + permitWithoutCalls: Bool, + minPingIntervalWithoutCalls: Duration + ) { + self.time = time + self.timeout = timeout + self.permitWithoutCalls = permitWithoutCalls + self.minPingIntervalWithoutCalls = minPingIntervalWithoutCalls + } + + /// Default values. The time after reading data a ping should be sent defaults to 2 hours, the timeout for + /// keepalive pings defaults to 20 seconds, pings are not permitted when no calls are in progress, and + /// the minimum allowed interval for clients to send pings defaults to 5 minutes. + public static var defaults: Self { + Self( + time: .seconds(2 * 60 * 60), // 2 hours + timeout: .seconds(20), + permitWithoutCalls: false, + minPingIntervalWithoutCalls: .seconds(5 * 60) // 5 minutes + ) + } + } + + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public struct Connection: Sendable { + /// The maximum amount of time a connection may exist before being gracefully closed. + public var maxAge: Duration? + + /// The maximum amount of time that the connection has to close gracefully. + public var maxGraceTime: Duration? + + /// The maximum amount of time a connection may be idle before it's closed. + public var maxIdleTime: Duration? + + public init( + maxAge: Duration?, + maxGraceTime: Duration?, + maxIdleTime: Duration? + ) { + self.maxAge = maxAge + self.maxGraceTime = maxGraceTime + self.maxIdleTime = maxIdleTime + } + + /// Default values. All the max connection age, max grace time, and max idle time default to infinite. + public static var defaults: Self { + Self(maxAge: nil, maxGraceTime: nil, maxIdleTime: nil) + } + } + + public struct HTTP2: Sendable { + /// The maximum frame size to be used in an HTTP/2 connection. + public var maxFrameSize: Int + + /// The target window size for this connection. + /// + /// - Note: This will also be set as the initial window size for the connection. + public var targetWindowSize: Int + + /// The number of concurrent streams on the HTTP/2 connection. + public var maxConcurrentStreams: Int? + + public init( + maxFrameSize: Int, + targetWindowSize: Int, + maxConcurrentStreams: Int? + ) { + self.maxFrameSize = maxFrameSize + self.targetWindowSize = targetWindowSize + self.maxConcurrentStreams = maxConcurrentStreams + } + + /// Default values. The max frame size defaults to 2^14, the target window size defaults to 2^16-1, and + /// the max concurrent streams default to infinite. + public static var defaults: Self { + Self( + maxFrameSize: 1 << 14, + targetWindowSize: (1 << 16) - 1, + maxConcurrentStreams: nil + ) + } + } + + public struct RPC: Sendable { + /// The maximum request payload size. + public var maxRequestPayloadSize: Int + + public init(maxRequestPayloadSize: Int) { + self.maxRequestPayloadSize = maxRequestPayloadSize + } + + /// Default values. Maximum request payload size defaults to 4MiB. + public static var defaults: Self { + Self(maxRequestPayloadSize: 4 * 1024 * 1024) + } + } +} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift index 8f707b798..32d5c49c7 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift @@ -15,7 +15,206 @@ */ import GRPCCore +@_spi(Package) import GRPCHTTP2Core +import NIOCore +import NIOExtras +import NIOPosix -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCHTTP2TransportNIOPosix { +extension HTTP2ServerTransport { + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + public struct Posix: ServerTransport { + private let address: GRPCHTTP2Core.SocketAddress + private let config: Config + private let eventLoopGroup: MultiThreadedEventLoopGroup + private let serverQuiescingHelper: ServerQuiescingHelper + + public init( + address: GRPCHTTP2Core.SocketAddress, + config: Config, + eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup + ) { + self.address = address + self.config = config + self.eventLoopGroup = eventLoopGroup + self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) + } + + public func listen( + _ streamHandler: @escaping (RPCStream) async -> Void + ) async throws { + let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup) + .serverChannelInitializer { channel in + let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( + channel: channel + ) + return channel.pipeline.addHandler(quiescingHandler) + } + .bind(to: self.address) { channel in + channel.eventLoop.makeCompletedFuture { + return try channel.pipeline.syncOperations.configureGRPCServerPipeline( + channel: channel, + compressionConfig: self.config.compression, + keepaliveConfig: self.config.keepalive, + connectionConfig: self.config.connection, + http2Config: self.config.http2, + rpcConfig: self.config.rpc, + useTLS: false + ) + } + } + + try await serverChannel.executeThenClose { inbound in + try await withThrowingDiscardingTaskGroup { serverTaskGroup in + for try await (connectionChannel, streamMultiplexer) in inbound { + serverTaskGroup.addTask { + try await connectionChannel + .executeThenClose { connectionInbound, connectionOutbound in + await withDiscardingTaskGroup { connectionTaskGroup in + connectionTaskGroup.addTask { + do { + for try await _ in connectionInbound {} + } catch { + // We don't want to close the channel if one connection throws. + return + } + } + + connectionTaskGroup.addTask { + await withDiscardingTaskGroup { streamTaskGroup in + do { + for try await (http2Stream, methodDescriptor) in streamMultiplexer.inbound + { + streamTaskGroup.addTask { + // It's okay to ignore these errors: + // - If we get an error because the http2Stream failed to close, then there's nothing we can do + // - If we get an error because the inner closure threw, then the only possible scenario in which + // that could happen is if methodDescriptor.get() throws - in which case, it means we never got + // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. + try? await http2Stream.executeThenClose { inbound, outbound in + guard let descriptor = try? await methodDescriptor.get() else { + return + } + let rpcStream = RPCStream( + descriptor: descriptor, + inbound: RPCAsyncSequence(wrapping: inbound), + outbound: RPCWriter.Closable( + wrapping: ServerConnection.Stream.Outbound( + responseWriter: outbound, + http2Stream: http2Stream + ) + ) + ) + await streamHandler(rpcStream) + } + } + } + } catch { + // We don't want to close the whole connection if one stream throws. + return + } + } + } + } + } + } + } + } + } + } + + public func stopListening() { + self.serverQuiescingHelper.initiateShutdown(promise: nil) + } + } + +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension HTTP2ServerTransport.Posix { + /// Configuration for the ``GRPCHTTP2TransportNIOPosix/GRPCHTTP2Core/HTTP2ServerTransport/Posix``. + public struct Config: Sendable { + /// Compression configuration. + public var compression: HTTP2ServerTransport.Config.Compression + /// Keepalive configuration. + public var keepalive: HTTP2ServerTransport.Config.Keepalive + /// Connection configuration. + public var connection: HTTP2ServerTransport.Config.Connection + /// HTTP2 configuration. + public var http2: HTTP2ServerTransport.Config.HTTP2 + /// RPC configuration. + public var rpc: HTTP2ServerTransport.Config.RPC + + /// Construct a new `Config`. + /// - Parameters: + /// - compression: Compression configuration. + /// - keepalive: Keepalive configuration. + /// - connection: Connection configuration. + /// - http2: HTTP2 configuration. + /// - rpc: RPC configuration. + public init( + compression: HTTP2ServerTransport.Config.Compression, + keepalive: HTTP2ServerTransport.Config.Keepalive, + connection: HTTP2ServerTransport.Config.Connection, + http2: HTTP2ServerTransport.Config.HTTP2, + rpc: HTTP2ServerTransport.Config.RPC + ) { + self.compression = compression + self.keepalive = keepalive + self.connection = connection + self.http2 = http2 + self.rpc = rpc + } + + /// Default values for the different configurations. + public static var defaults: Self { + Self( + compression: .defaults, + keepalive: .defaults, + connection: .defaults, + http2: .defaults, + rpc: .defaults + ) + } + } +} + +extension NIOCore.SocketAddress { + fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws { + if let ipv4 = socketAddress.ipv4 { + self = try Self(ipAddress: ipv4.host, port: ipv4.port) + } else if let ipv6 = socketAddress.ipv6 { + self = try Self(ipAddress: ipv6.host, port: ipv6.port) + } else if let unixDomainSocket = socketAddress.unixDomainSocket { + self = try Self(unixDomainSocketPath: unixDomainSocket.path) + } else { + throw RPCError( + code: .internalError, + message: + "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)." + ) + } + } +} + +extension ServerBootstrap { + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + fileprivate func bind( + to address: GRPCHTTP2Core.SocketAddress, + childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture + ) async throws -> NIOAsyncChannel { + if let virtualSocket = address.virtualSocket { + return try await self.bind( + to: VsockAddress( + cid: VsockAddress.ContextID(rawValue: virtualSocket.contextID.rawValue), + port: VsockAddress.Port(rawValue: virtualSocket.port.rawValue) + ), + childChannelInitializer: childChannelInitializer + ) + } else { + return try await self.bind( + to: NIOCore.SocketAddress(address), + childChannelInitializer: childChannelInitializer + ) + } + } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift similarity index 100% rename from Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHasness+ServerBehavior.swift rename to Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift diff --git a/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift b/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift new file mode 100644 index 000000000..f57a1373b --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift @@ -0,0 +1,53 @@ +/* + * 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 GRPCHTTP2Core +import XCTest + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +final class HTTP2ServerTransportConfigTests: XCTestCase { + func testCompressionDefaults() { + let config = HTTP2ServerTransport.Config.Compression.defaults + XCTAssertEqual(config.enabledAlgorithms, .none) + } + + func testKeepaliveDefaults() { + let config = HTTP2ServerTransport.Config.Keepalive.defaults + XCTAssertEqual(config.time, .seconds(7200)) + XCTAssertEqual(config.timeout, .seconds(20)) + XCTAssertEqual(config.permitWithoutCalls, false) + XCTAssertEqual(config.minPingIntervalWithoutCalls, .seconds(300)) + } + + func testConnectionDefaults() { + let config = HTTP2ServerTransport.Config.Connection.defaults + XCTAssertNil(config.maxAge) + XCTAssertNil(config.maxGraceTime) + XCTAssertNil(config.maxIdleTime) + } + + func testHTTP2Defaults() { + let config = HTTP2ServerTransport.Config.HTTP2.defaults + XCTAssertEqual(config.maxFrameSize, 16_384) + XCTAssertEqual(config.targetWindowSize, 65_535) + XCTAssertNil(config.maxConcurrentStreams) + } + + func testRPCDefaults() { + let config = HTTP2ServerTransport.Config.RPC.defaults + XCTAssertEqual(config.maxRequestPayloadSize, 4_194_304) + } +} From f16405dfef1e64ea37b8cc45842e1bb9bc135f53 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 30 May 2024 11:05:50 +0100 Subject: [PATCH 325/580] Add missing dependency to package manifest (#1896) Motivation: 3272c32b uses NIOExtras but didn't add the dependency to the package manifest. This can cause compilation issues. Modifications: - Add the missing dependency Result: Fewer issues --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index c97a915f7..b937596e9 100644 --- a/Package.swift +++ b/Package.swift @@ -216,7 +216,8 @@ extension Target { static let grpcHTTP2TransportNIOPosix: Target = .target( name: "GRPCHTTP2TransportNIOPosix", dependencies: [ - .grpcHTTP2Core + .grpcHTTP2Core, + .nioExtras ] ) From 4448b7e6877061d3f9c43cad7290daf1923a3d7c Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 31 May 2024 08:15:37 +0100 Subject: [PATCH 326/580] Fix wrong gRPC status message header name (#1899) --- .../GRPCStreamStateMachine.swift | 4 ++-- .../GRPCStreamStateMachineTests.swift | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 996574fe7..e262f9e32 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -1099,7 +1099,7 @@ extension GRPCStreamStateMachine { trailersOnly: Bool ) -> HPACKHeaders { // Trailers always contain the grpc-status header, and optionally, - // grpc-status-message, and custom metadata. + // grpc-message, and custom metadata. // If it's a trailers-only response, they will also contain :status and // content-type. var headers = HPACKHeaders() @@ -1506,7 +1506,7 @@ internal enum GRPCHTTP2Keys: String { case te = "te" case status = ":status" case grpcStatus = "grpc-status" - case grpcStatusMessage = "grpc-status-message" + case grpcStatusMessage = "grpc-message" } extension HPACKHeaders { diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 9cb686731..fb71c8048 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -1519,7 +1519,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "2", - "grpc-status-message": "RPC unknown", + "grpc-message": "RPC unknown", ] ) @@ -1585,7 +1585,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "2", - "grpc-status-message": "RPC unknown", + "grpc-message": "RPC unknown", ] ) @@ -1707,7 +1707,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": String(Status.Code.invalidArgument.rawValue), - "grpc-status-message": "No :path header has been set.", + "grpc-message": "No :path header has been set.", ] ) } @@ -1728,7 +1728,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": String(Status.Code.unimplemented.rawValue), - "grpc-status-message": + "grpc-message": "The given :path (someinvalidpath) does not correspond to a valid method.", ] ) @@ -1750,7 +1750,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "3", - "grpc-status-message": + "grpc-message": "\"te\" header is expected to be present and have a value of \"trailers\".", ] ) @@ -1772,7 +1772,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "3", - "grpc-status-message": + "grpc-message": "\"te\" header is expected to be present and have a value of \"trailers\".", ] ) @@ -1794,7 +1794,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "3", - "grpc-status-message": + "grpc-message": ":method header is expected to be present and have a value of \"POST\".", ] ) @@ -1816,7 +1816,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "3", - "grpc-status-message": + "grpc-message": ":method header is expected to be present and have a value of \"POST\".", ] ) @@ -1838,7 +1838,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "3", - "grpc-status-message": ":scheme header must be present and one of \"http\" or \"https\".", + "grpc-message": ":scheme header must be present and one of \"http\" or \"https\".", ] ) } @@ -1859,7 +1859,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-status": "3", - "grpc-status-message": ":scheme header must be present and one of \"http\" or \"https\".", + "grpc-message": ":scheme header must be present and one of \"http\" or \"https\".", ] ) } @@ -1883,7 +1883,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { "content-type": "application/grpc", "grpc-accept-encoding": "deflate", "grpc-status": "12", - "grpc-status-message": + "grpc-message": "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding", "grpc-accept-encoding": "identity", ] From fd0f881842793981455bf1b7aff7e1c0b2c6b1b9 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 31 May 2024 09:31:36 +0100 Subject: [PATCH 327/580] Add server executable for interop tests (#1897) --- Package.swift | 20 +++--- .../InteroperabilityTests/TestService.swift | 20 +++--- .../InteroperabilityTestsExecutable.swift | 63 +++++++++++++++++++ .../InProcessInteroperabilityTests.swift | 3 +- 4 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 Sources/interoperability-tests/InteroperabilityTestsExecutable.swift diff --git a/Package.swift b/Package.swift index b937596e9..c29d908fd 100644 --- a/Package.swift +++ b/Package.swift @@ -335,13 +335,6 @@ extension Target { ] ) - static let grpcHTTP2TransportNIOPosixTests: Target = .testTarget( - name: "GRPCHTTP2TransportNIOPosixTests", - dependencies: [ - .grpcHTTP2TransportNIOPosix - ] - ) - static let grpcHTTP2TransportNIOTransportServicesTests: Target = .testTarget( name: "GRPCHTTP2TransportNIOTransportServicesTests", dependencies: [ @@ -410,6 +403,17 @@ extension Target { ] ) + static let interoperabilityTestsExecutable: Target = .executableTarget( + name: "interoperability-tests", + dependencies: [ + .grpcCore, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .interoperabilityTests, + .argumentParser + ] + ) + static let interopTestImplementation: Target = .target( name: "GRPCInteroperabilityTestsImplementation", dependencies: [ @@ -736,6 +740,7 @@ let package = Package( .grpcProtobuf, .grpcProtobufCodeGen, .interoperabilityTestImplementation, + .interoperabilityTestsExecutable, .performanceWorker, // v2 tests @@ -744,7 +749,6 @@ let package = Package( .grpcCodeGenTests, .grpcInterceptorsTests, .grpcHTTP2CoreTests, - .grpcHTTP2TransportNIOPosixTests, .grpcHTTP2TransportNIOTransportServicesTests, .grpcProtobufTests, .grpcProtobufCodeGenTests, diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift index d20796c35..5472f23d1 100644 --- a/Sources/InteroperabilityTests/TestService.swift +++ b/Sources/InteroperabilityTests/TestService.swift @@ -18,8 +18,10 @@ import Foundation import GRPCCore @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { - internal func unimplementedCall( +public struct TestService: Grpc_Testing_TestService.ServiceProtocol { + public init() {} + + public func unimplementedCall( request: ServerRequest.Single ) async throws -> ServerResponse.Single @@ -28,7 +30,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { } /// Server implements `emptyCall` which immediately returns the empty message. - internal func emptyCall( + public func emptyCall( request: ServerRequest.Single ) async throws -> ServerResponse.Single { let message = Grpc_Testing_TestService.Method.EmptyCall.Output() @@ -46,7 +48,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// /// If the server does not support the `responseType`, then it should fail the RPC with /// `INVALID_ARGUMENT`. - internal func unaryCall( + public func unaryCall( request: ServerRequest.Single ) async throws -> ServerResponse.Single { // If the request has a responseStatus set, the server should return that status. @@ -93,7 +95,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// long as the response payload is different for each request. In addition it adds cache control /// headers such that the response can be cached by proxies in the response path. Server should /// be behind a caching proxy for this test to pass. Currently we set the max-age to 60 seconds. - internal func cacheableUnaryCall( + public func cacheableUnaryCall( request: ServerRequest.Single ) async throws -> ServerResponse.Single @@ -106,7 +108,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// Each `StreamingOutputCallResponse` should have a payload body of size `ResponseParameter.size` /// bytes, as specified by its respective `ResponseParameter`. After sending all responses, it /// closes with OK. - internal func streamingOutputCall( + public func streamingOutputCall( request: ServerRequest.Single< Grpc_Testing_TestService.Method.StreamingOutputCall.Input > @@ -132,7 +134,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// Server implements `streamingInputCall` which upon half close immediately returns a /// `StreamingInputCallResponse` where `aggregatedPayloadSize` is the sum of all request payload /// bodies received. - internal func streamingInputCall( + public func streamingInputCall( request: ServerRequest.Stream ) async throws -> ServerResponse.Single @@ -160,7 +162,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// `StreamingOutputCallRequest`. Each `StreamingOutputCallResponse` should have a payload body /// of size `ResponseParameter.size` bytes, as specified by its respective `ResponseParameter`s. /// After receiving half close and sending all responses, it closes with OK. - internal func fullDuplexCall( + public func fullDuplexCall( request: ServerRequest.Stream ) async throws -> ServerResponse.Stream @@ -199,7 +201,7 @@ internal struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// This is not implemented as it is not described in the specification. /// /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md - internal func halfDuplexCall( + public func halfDuplexCall( request: ServerRequest.Stream ) async throws -> ServerResponse.Stream diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift new file mode 100644 index 000000000..428abd983 --- /dev/null +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -0,0 +1,63 @@ +/* + * 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 ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix +import InteroperabilityTests +import NIOPosix + +@main +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct InteroperabilityTestsExecutable: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "gRPC Swift Interoperability Runner", + subcommands: [StartServer.self, ListTests.self] + ) + + struct StartServer: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Start the gRPC Swift interoperability test server." + ) + + @Option(help: "The port to listen on for new connections") + var port: Int + + func run() async throws { + var transportConfig = HTTP2ServerTransport.Posix.Config.defaults + transportConfig.compression.enabledAlgorithms = .all + let transport = HTTP2ServerTransport.Posix( + address: .ipv4(host: "0.0.0.0", port: self.port), + config: transportConfig + ) + let server = GRPCServer(transport: transport, services: [TestService()]) + try await server.run() + } + } + + struct ListTests: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "List all interoperability test names." + ) + + func run() throws { + for testCase in InteroperabilityTestCase.allCases { + print(testCase.name) + } + } + } +} diff --git a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift index 19965cfd0..6567f942b 100644 --- a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift +++ b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift @@ -16,10 +16,9 @@ import GRPCCore import GRPCInProcessTransport +import InteroperabilityTests import XCTest -@testable import InteroperabilityTests - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class InProcessInteroperabilityTests: XCTestCase { func runInProcessTransport( From 3c66c5f29a88d6ef1d60ec6242121e8ed58350cd Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 31 May 2024 14:55:08 +0100 Subject: [PATCH 328/580] Always store compression algorithm in stream state machine (#1898) --- .../GRPCStreamStateMachine.swift | 42 ++++++--- .../GRPCStreamStateMachineTests.swift | 87 ++++++++++++------- 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index e262f9e32..6f5fad990 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -75,7 +75,7 @@ private enum GRPCStreamStateMachineState { let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm? + var outboundCompression: CompressionAlgorithm // The deframer must be optional because the client will not have one configured // until the server opens and sends a grpc-encoding header. @@ -89,12 +89,14 @@ private enum GRPCStreamStateMachineState { init( previousState: ClientIdleServerIdleState, compressor: Zlib.Compressor?, + outboundCompression: CompressionAlgorithm, framer: GRPCMessageFramer, decompressor: Zlib.Decompressor?, deframer: NIOSingleStepByteToMessageProcessor? ) { self.maximumPayloadSize = previousState.maximumPayloadSize self.compressor = compressor + self.outboundCompression = outboundCompression self.framer = framer self.decompressor = decompressor self.deframer = deframer @@ -105,6 +107,7 @@ private enum GRPCStreamStateMachineState { struct ClientOpenServerOpenState { var framer: GRPCMessageFramer var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm let deframer: NIOSingleStepByteToMessageProcessor var decompressor: Zlib.Decompressor? @@ -118,6 +121,7 @@ private enum GRPCStreamStateMachineState { ) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.deframer = deframer self.decompressor = decompressor @@ -129,6 +133,7 @@ private enum GRPCStreamStateMachineState { struct ClientOpenServerClosedState { var framer: GRPCMessageFramer? var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? @@ -145,6 +150,7 @@ private enum GRPCStreamStateMachineState { init(previousState: ClientIdleServerIdleState) { self.framer = nil self.compressor = nil + self.outboundCompression = .none self.deframer = nil self.decompressor = nil self.inboundMessageBuffer = .init() @@ -153,6 +159,7 @@ private enum GRPCStreamStateMachineState { init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.deframer = previousState.deframer self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer @@ -161,6 +168,7 @@ private enum GRPCStreamStateMachineState { init(previousState: ClientOpenServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.inboundMessageBuffer = previousState.inboundMessageBuffer // The server went directly from idle to closed - this means it sent a // trailers-only response: @@ -177,7 +185,7 @@ private enum GRPCStreamStateMachineState { let maximumPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm? + var outboundCompression: CompressionAlgorithm let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? @@ -195,9 +203,12 @@ private enum GRPCStreamStateMachineState { if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { self.compressor = Zlib.Compressor(method: zlibMethod) + self.outboundCompression = compressionAlgorithm + } else { + self.compressor = nil + self.outboundCompression = .none } self.framer = GRPCMessageFramer() - self.outboundCompression = compressionAlgorithm // We don't need a deframer since we won't receive any messages from the // client: it's closed. self.deframer = nil @@ -218,6 +229,7 @@ private enum GRPCStreamStateMachineState { struct ClientClosedServerOpenState { var framer: GRPCMessageFramer var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm let deframer: NIOSingleStepByteToMessageProcessor? var decompressor: Zlib.Decompressor? @@ -227,6 +239,7 @@ private enum GRPCStreamStateMachineState { init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.deframer = previousState.deframer self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer @@ -236,6 +249,7 @@ private enum GRPCStreamStateMachineState { init(previousState: ClientClosedServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression // In the case of the server, we don't need to deframe/decompress any more // messages, since the client's closed. @@ -252,6 +266,7 @@ private enum GRPCStreamStateMachineState { ) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression // In the case of the client, it will only be able to set up the deframer // after it receives the chosen encoding from the server. @@ -274,6 +289,7 @@ private enum GRPCStreamStateMachineState { // the client. var framer: GRPCMessageFramer? var compressor: Zlib.Compressor? + var outboundCompression: CompressionAlgorithm // These are already deframed, so we don't need the deframer anymore. var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -288,36 +304,42 @@ private enum GRPCStreamStateMachineState { init(previousState: ClientIdleServerIdleState) { self.framer = nil self.compressor = nil + self.outboundCompression = .none self.inboundMessageBuffer = .init() } init(previousState: ClientClosedServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.inboundMessageBuffer = previousState.inboundMessageBuffer } init(previousState: ClientClosedServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.inboundMessageBuffer = previousState.inboundMessageBuffer } init(previousState: ClientOpenServerIdleState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.inboundMessageBuffer = previousState.inboundMessageBuffer } init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.inboundMessageBuffer = previousState.inboundMessageBuffer } init(previousState: ClientOpenServerClosedState) { self.framer = previousState.framer self.compressor = previousState.compressor + self.outboundCompression = previousState.outboundCompression self.inboundMessageBuffer = previousState.inboundMessageBuffer } } @@ -555,6 +577,7 @@ extension GRPCStreamStateMachine { .init( previousState: state, compressor: compressor, + outboundCompression: outboundEncoding, framer: GRPCMessageFramer(), decompressor: nil, deframer: nil @@ -1019,10 +1042,6 @@ extension GRPCStreamStateMachine { headers.add(outboundEncoding.name, forKey: .encoding) } - for acceptedEncoding in configuration.acceptedEncodings.elements.filter({ $0 != .none }) { - headers.add(acceptedEncoding.name, forKey: .acceptEncoding) - } - for metadataPair in customMetadata { headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) } @@ -1037,6 +1056,7 @@ extension GRPCStreamStateMachine { // Server sends initial metadata switch self.state { case .clientOpenServerIdle(let state): + let outboundEncoding = state.outboundCompression self.state = .clientOpenServerOpen( .init( previousState: state, @@ -1048,14 +1068,15 @@ extension GRPCStreamStateMachine { ) ) return self.makeResponseHeaders( - outboundEncoding: state.outboundCompression, + outboundEncoding: outboundEncoding, configuration: configuration, customMetadata: metadata ) case .clientClosedServerIdle(let state): + let outboundEncoding = state.outboundCompression self.state = .clientClosedServerOpen(.init(previousState: state)) return self.makeResponseHeaders( - outboundEncoding: state.outboundCompression, + outboundEncoding: outboundEncoding, configuration: configuration, customMetadata: metadata ) @@ -1326,8 +1347,6 @@ extension GRPCStreamStateMachine { canonicalForm: true ) // Find the preferred encoding and use it to compress responses. - // If it's identity, just skip it altogether, since we won't be - // compressing. for clientAdvertisedEncoding in clientAdvertisedEncodings { if let algorithm = CompressionAlgorithm(name: clientAdvertisedEncoding), configuration.acceptedEncodings.contains(algorithm) @@ -1358,6 +1377,7 @@ extension GRPCStreamStateMachine { .init( previousState: state, compressor: compressor, + outboundCompression: outboundEncoding, framer: GRPCMessageFramer(), decompressor: decompressor, deframer: NIOSingleStepByteToMessageProcessor(deframer) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index fb71c8048..a423f701b 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -117,19 +117,16 @@ extension HPACKHeaders { fileprivate static let serverInitialMetadata: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] fileprivate static let serverInitialMetadataWithDeflateCompression: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] fileprivate static let serverInitialMetadataWithGZIPCompression: Self = [ GRPCHTTP2Keys.status.rawValue: "200", GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "gzip", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", ] fileprivate static let serverTrailers: Self = [ GRPCHTTP2Keys.status.rawValue: "200", @@ -366,7 +363,6 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ":status": "200", "content-type": "application/grpc", "grpc-encoding": "gzip", - "grpc-accept-encoding": "deflate", ] ) ) @@ -1010,7 +1006,6 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { [ ":status": "200", "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", ], nil ) @@ -1113,7 +1108,6 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { [ ":status": "200", "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", ], nil ) @@ -1200,7 +1194,6 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { [ ":status": "200", "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", ], nil ) @@ -1255,14 +1248,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { final class GRPCStreamServerStateMachineTests: XCTestCase { private func makeServerStateMachine( targetState: TargetStateMachineState, - compressionEnabled: Bool = false + deflateCompressionEnabled: Bool = false ) -> GRPCStreamStateMachine { var stateMachine = GRPCStreamStateMachine( configuration: .server( .init( scheme: .http, - acceptedEncodings: [.deflate] + acceptedEncodings: deflateCompressionEnabled ? [.deflate] : [] ) ), maximumPayloadSize: 100, @@ -1270,7 +1263,8 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) let clientMetadata: HPACKHeaders = - compressionEnabled ? .clientInitialMetadataWithDeflateCompression : .clientInitialMetadata + deflateCompressionEnabled + ? .clientInitialMetadataWithDeflateCompression : .clientInitialMetadata switch targetState { case .clientIdleServerIdle: break @@ -1343,8 +1337,34 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testSendMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) + var stateMachine = self.makeServerStateMachine( + targetState: .clientOpenServerIdle, + deflateCompressionEnabled: false + ) + XCTAssertEqual( + try stateMachine.send(metadata: .init()), + [ + ":status": "200", + "content-type": "application/grpc", + ] + ) + } + + func testSendMetadataWhenClientOpenAndServerIdle_AndCompressionEnabled() { + // Enable deflate compression on server + var stateMachine = self.makeServerStateMachine( + targetState: .clientOpenServerIdle, + deflateCompressionEnabled: true + ) + + XCTAssertEqual( + try stateMachine.send(metadata: .init()), + [ + ":status": "200", + "content-type": "application/grpc", + "grpc-encoding": "deflate", + ] + ) } func testSendMetadataWhenClientOpenAndServerOpen() throws { @@ -1866,7 +1886,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { } func testReceiveMetadataWhenClientIdleAndServerIdle_ServerUnsupportedEncoding() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) + var stateMachine = self.makeServerStateMachine( + targetState: .clientIdleServerIdle, + deflateCompressionEnabled: true + ) // Try opening client with a compression algorithm that is not accepted // by the server. @@ -1876,18 +1899,23 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", - "grpc-status": "12", - "grpc-message": - "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding", - "grpc-accept-encoding": "identity", - ] - ) + let expected: HPACKHeaders = [ + ":status": "200", + "content-type": "application/grpc", + "grpc-status": "12", + "grpc-message": + "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding", + "grpc-accept-encoding": "deflate", + "grpc-accept-encoding": "identity", + ] + XCTAssertEqual(expected.count, trailers.count, "Expected \(expected) but got \(trailers)") + for header in trailers { + XCTAssertTrue( + expected.contains { name, value, _ in + header.name == name && header.value == header.value + } + ) + } } } @@ -2016,7 +2044,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Enable deflate compression on server var stateMachine = self.makeServerStateMachine( targetState: .clientOpenServerOpen, - compressionEnabled: true + deflateCompressionEnabled: true ) let originalMessage = [UInt8]([42, 42, 43, 43]) @@ -2171,7 +2199,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = self.makeServerStateMachine( targetState: .clientOpenServerOpen, - compressionEnabled: true + deflateCompressionEnabled: true ) XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) @@ -2308,7 +2336,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { var stateMachine = self.makeServerStateMachine( targetState: .clientOpenServerOpen, - compressionEnabled: true + deflateCompressionEnabled: true ) let originalMessage = [UInt8]([42, 42, 43, 43]) @@ -2430,7 +2458,6 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { [ ":status": "200", "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", "custom": "value", ] ) @@ -2552,7 +2579,6 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { "custom": "value", ":status": "200", "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", ] ) @@ -2626,7 +2652,6 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { "custom": "value", ":status": "200", "content-type": "application/grpc", - "grpc-accept-encoding": "deflate", ] ) From e7cbb254edc65ffca10ee1e2bb7688bb4c32e2f6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 4 Jun 2024 13:12:53 +0100 Subject: [PATCH 329/580] Add grpc channel (#1893) Motivation: The grpc channel provides a long-lived connection to some remote backend(s). This allows us to build up a client transport. Modifications: - Add the `GRPCChannel`, a long-lives way of providing connections to a remote backend Result: Can make RPCs to a remote backend --- .../GRPCCore/Call/Client/CallOptions.swift | 3 +- .../Client/Connection/GRPCChannel.swift | 914 ++++++++++++++++++ ...iscardingTaskGroup+CancellableHandle.swift | 79 ++ .../Internal/NIOChannelPipeline+GRPC.swift | 67 ++ .../Client/Connection/GRPCChannelTests.swift | 596 ++++++++++++ .../Connection/Utilities/NameResolvers.swift | 74 ++ .../Connection/Utilities/TestServer.swift | 54 +- .../AsyncStream+MakeStream.swift | 32 + .../MethodDescriptor+Common.swift | 11 + 9 files changed, 1820 insertions(+), 10 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift create mode 100644 Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index b9aaba59e..a68c9a530 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -127,7 +127,8 @@ extension CallOptions { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { - mutating func formUnion(with methodConfig: MethodConfig?) { + @_spi(Package) + public mutating func formUnion(with methodConfig: MethodConfig?) { guard let methodConfig = methodConfig else { return } self.timeout.setIfNone(to: methodConfig.timeout) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift new file mode 100644 index 000000000..38e234f6c --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -0,0 +1,914 @@ +/* + * 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 Atomics +import DequeModule +@_spi(Package) import GRPCCore + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@_spi(Package) +public struct GRPCChannel: ClientTransport { + private enum Input: Sendable { + /// Close the channel, if possible. + case close + /// Handle the result of a name resolution. + case handleResolutionResult(NameResolutionResult) + /// Handle the event from the underlying connection object. + case handleLoadBalancerEvent(LoadBalancerEvent, LoadBalancerID) + } + + /// Events which can happen to the channel. + private let _connectivityState: + ( + stream: AsyncStream, + continuation: AsyncStream.Continuation + ) + + /// Inputs which this channel should react to. + private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) + + /// A resolver providing resolved names to the channel. + private let resolver: NameResolver + + /// The state of the channel. + private let state: _LockedValueBox + + /// The maximum number of times to attempt to create a stream per RPC. + /// + /// This is the value used by other gRPC implementations. + private static let maxStreamCreationAttempts = 5 + + /// A factory for connections. + private let connector: any HTTP2Connector + + /// The connection backoff configuration used by the subchannel when establishing a connection. + private let backoff: ConnectionBackoff + + /// The default compression algorithm used for requests. + private let defaultCompression: CompressionAlgorithm + + /// The set of enabled compression algorithms. + private let enabledCompression: CompressionAlgorithmSet + + /// The default service config to use. + /// + /// Used when the resolver doesn't provide one. + private let defaultServiceConfig: ServiceConfig + + // These are both read frequently and updated infrequently so may be a bottleneck. + private let _methodConfig: _LockedValueBox<_MethodConfigs> + private let _retryThrottle: _LockedValueBox + + @_spi(Package) + public init( + resolver: NameResolver, + connector: any HTTP2Connector, + config: Config, + defaultServiceConfig: ServiceConfig + ) { + self.resolver = resolver + self.state = _LockedValueBox(StateMachine()) + self._connectivityState = AsyncStream.makeStream() + self.input = AsyncStream.makeStream() + self.connector = connector + + self.backoff = ConnectionBackoff( + initial: config.backoff.initial, + max: config.backoff.max, + multiplier: config.backoff.multiplier, + jitter: config.backoff.jitter + ) + self.defaultCompression = config.compression.algorithm + self.enabledCompression = config.compression.enabledAlgorithms + self.defaultServiceConfig = defaultServiceConfig + + let throttle = defaultServiceConfig.retryThrottling.map { RetryThrottle(policy: $0) } + self._retryThrottle = _LockedValueBox(throttle) + + let methodConfig = _MethodConfigs(serviceConfig: defaultServiceConfig) + self._methodConfig = _LockedValueBox(methodConfig) + } + + /// The connectivity state of the channel. + var connectivityState: AsyncStream { + self._connectivityState.stream + } + + /// Returns a throttle which gRPC uses to determine whether retries can be executed. + public var retryThrottle: RetryThrottle? { + self._retryThrottle.withLockedValue { $0 } + } + + /// Returns the configuration for a given method. + /// + /// - Parameter descriptor: The method to lookup configuration for. + /// - Returns: Configuration for the method, if it exists. + public func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { + self._methodConfig.withLockedValue { $0[descriptor] } + } + + /// Establishes and maintains a connection to the remote destination. + public func connect() async { + self.state.withLockedValue { $0.start() } + self._connectivityState.continuation.yield(.idle) + + await withDiscardingTaskGroup { group in + var iterator: Optional.AsyncIterator> + + // The resolver can either push or pull values. If it pushes values the channel should + // listen for new results. Otherwise the channel will pull values as and when necessary. + switch self.resolver.updateMode.value { + case .push: + iterator = nil + + let handle = group.addCancellableTask { + do { + for try await result in self.resolver.names { + self.input.continuation.yield(.handleResolutionResult(result)) + } + self.close() + } catch { + self.close() + } + } + + // When the channel is closed gracefully, the task group running the load balancer mustn't + // be cancelled (otherwise in-flight RPCs would fail), but the push based resolver will + // continue indefinitely. Store its handle and cancel it on close when closing the channel. + self.state.withLockedValue { state in + state.setNameResolverTaskHandle(handle) + } + + case .pull: + iterator = self.resolver.names.makeAsyncIterator() + await self.resolve(iterator: &iterator, in: &group) + } + + // Resolver is setup, start handling events. + for await input in self.input.stream { + switch input { + case .close: + self.handleClose(in: &group) + + case .handleResolutionResult(let result): + self.handleNameResolutionResult(result, in: &group) + + case .handleLoadBalancerEvent(let event, let id): + await self.handleLoadBalancerEvent( + event, + loadBalancerID: id, + in: &group, + iterator: &iterator + ) + } + } + } + + if Task.isCancelled { + self._connectivityState.continuation.finish() + } + } + + /// Signal to the transport that no new streams may be created and that connections should be + /// closed when all streams are closed. + public func close() { + self.input.continuation.yield(.close) + } + + /// Opens a stream using the transport, and uses it as input into a user-provided closure. + public func withStream( + descriptor: MethodDescriptor, + options: CallOptions, + _ closure: (_ stream: RPCStream) async throws -> T + ) async throws -> T { + // Merge options from the call with those from the service config. + let methodConfig = self.configuration(forMethod: descriptor) + var options = options + options.formUnion(with: methodConfig) + + for attempt in 1 ... Self.maxStreamCreationAttempts { + switch await self.makeStream(descriptor: descriptor, options: options) { + case .created(let stream): + return try await stream.execute { inbound, outbound in + let rpcStream = RPCStream( + descriptor: stream.descriptor, + inbound: RPCAsyncSequence(wrapping: inbound), + outbound: RPCWriter.Closable(wrapping: outbound) + ) + return try await closure(rpcStream) + } + + case .tryAgain(let error): + if error is CancellationError || attempt == Self.maxStreamCreationAttempts { + throw error + } else { + continue + } + + case .stopTrying(let error): + throw error + } + } + + fatalError("Internal inconsistency") + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel { + @_spi(Package) + public struct Config: Sendable { + /// Configuration for HTTP/2 connections. + var http2: HTTP2ClientTransport.Config.HTTP2 + + /// Configuration for backoff used when establishing a connection. + var backoff: HTTP2ClientTransport.Config.Backoff + + /// Configuration for dealing with idle connections. + var idle: HTTP2ClientTransport.Config.Idle? + + /// Configuration for keepalive. + var keepalive: HTTP2ClientTransport.Config.Keepalive? + + /// Compression configuration. + var compression: HTTP2ClientTransport.Config.Compression + + @_spi(Package) + public init( + http2: HTTP2ClientTransport.Config.HTTP2, + backoff: HTTP2ClientTransport.Config.Backoff, + idle: HTTP2ClientTransport.Config.Idle?, + keepalive: HTTP2ClientTransport.Config.Keepalive?, + compression: HTTP2ClientTransport.Config.Compression + ) { + self.http2 = http2 + self.backoff = backoff + self.idle = idle + self.keepalive = keepalive + self.compression = compression + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel { + enum MakeStreamResult { + /// A stream was created, use it. + case created(Connection.Stream) + /// An error occurred while trying to create a stream, try again if possible. + case tryAgain(Error) + /// An unrecoverable error occurred (e.g. the channel is closed), fail the RPC and don't retry. + case stopTrying(Error) + } + + private func makeStream( + descriptor: MethodDescriptor, + options: CallOptions + ) async -> MakeStreamResult { + let waitForReady = options.waitForReady ?? true + switch self.state.withLockedValue({ $0.makeStream(waitForReady: waitForReady) }) { + case .useLoadBalancer(let loadBalancer): + return await self.makeStream( + descriptor: descriptor, + options: options, + loadBalancer: loadBalancer + ) + + case .joinQueue: + do { + let loadBalancer = try await self.enqueue(waitForReady: waitForReady) + return await self.makeStream( + descriptor: descriptor, + options: options, + loadBalancer: loadBalancer + ) + } catch { + // All errors from enqueue are non-recoverable: either the channel is shutting down or + // the request has been cancelled. + return .stopTrying(error) + } + + case .failRPC: + return .stopTrying(RPCError(code: .unavailable, message: "channel isn't ready")) + } + } + + private func makeStream( + descriptor: MethodDescriptor, + options: CallOptions, + loadBalancer: LoadBalancer + ) async -> MakeStreamResult { + guard let subchannel = loadBalancer.pickSubchannel() else { + return .tryAgain(RPCError(code: .unavailable, message: "channel isn't ready")) + } + + let methodConfig = self.configuration(forMethod: descriptor) + var options = options + options.formUnion(with: methodConfig) + + do { + let stream = try await subchannel.makeStream(descriptor: descriptor, options: options) + return .created(stream) + } catch { + return .tryAgain(error) + } + } + + private func enqueue(waitForReady: Bool) async throws -> LoadBalancer { + let id = QueueEntryID() + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + if Task.isCancelled { + continuation.resume(throwing: CancellationError()) + return + } + + let enqueued = self.state.withLockedValue { state in + state.enqueue(continuation: continuation, waitForReady: waitForReady, id: id) + } + + // Not enqueued because the channel is shutdown or shutting down. + if !enqueued { + let error = RPCError(code: .unavailable, message: "channel is shutdown") + continuation.resume(throwing: error) + } + } + } onCancel: { + let continuation = self.state.withLockedValue { state in + state.dequeueContinuation(id: id) + } + + continuation?.resume(throwing: CancellationError()) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel { + private func handleClose(in group: inout DiscardingTaskGroup) { + switch self.state.withLockedValue({ $0.close() }) { + case .close(let current, let next, let resolver): + resolver?.cancel() + current.close() + next?.close() + self._connectivityState.continuation.yield(.shutdown) + + case .cancelAll(let continuations): + for continuation in continuations { + continuation.resume(throwing: RPCError(code: .unavailable, message: "channel is closed")) + } + self._connectivityState.continuation.yield(.shutdown) + group.cancelAll() + + case .none: + () + } + } + + private func handleNameResolutionResult( + _ result: NameResolutionResult, + in group: inout DiscardingTaskGroup + ) { + // Ignore empty endpoint lists. + if result.endpoints.isEmpty { return } + + switch result.serviceConfig ?? .success(self.defaultServiceConfig) { + case .success(let config): + // Update per RPC configuration. + let methodConfig = _MethodConfigs(serviceConfig: config) + self._methodConfig.withLockedValue { $0 = methodConfig } + + let retryThrottle = config.retryThrottling.map { RetryThrottle(policy: $0) } + self._retryThrottle.withLockedValue { $0 = retryThrottle } + + // Update the load balancer. + self.updateLoadBalancer(serviceConfig: config, endpoints: result.endpoints, in: &group) + + case .failure: + self.close() + } + } + + private func updateLoadBalancer( + serviceConfig: ServiceConfig, + endpoints: [Endpoint], + in group: inout DiscardingTaskGroup + ) { + // Pick the first applicable policy, else fallback to pick-first. + for policy in serviceConfig.loadBalancingConfig { + let onUpdatePolicy: GRPCChannel.StateMachine.OnChangeLoadBalancer + + if policy.roundRobin != nil { + onUpdatePolicy = self.state.withLockedValue { state in + state.changeLoadBalancerKind(to: .roundRobin) { + let loadBalancer = RoundRobinLoadBalancer( + connector: self.connector, + backoff: self.backoff, + defaultCompression: self.defaultCompression, + enabledCompression: self.enabledCompression + ) + return .roundRobin(loadBalancer) + } + } + } else if policy.pickFirst != nil { + fatalError("TODO: use pick-first when supported") + } else { + // Policy isn't known, ignore it. + continue + } + + self.handleLoadBalancerChange(onUpdatePolicy, endpoints: endpoints, in: &group) + return + } + + // No suitable config was found, fallback to pick-first. + fatalError("TODO: use pick-first when supported") + } + + private func handleLoadBalancerChange( + _ update: StateMachine.OnChangeLoadBalancer, + endpoints: [Endpoint], + in group: inout DiscardingTaskGroup + ) { + switch update { + case .runLoadBalancer(let new, let old): + old?.close() + new.updateAddresses(endpoints) + + group.addTask { + await new.run() + } + + group.addTask { + for await event in new.events { + self.input.continuation.yield(.handleLoadBalancerEvent(event, new.id)) + } + } + + case .updateLoadBalancer(let existing): + existing.updateAddresses(endpoints) + + case .none: + () + } + } + + private func handleLoadBalancerEvent( + _ event: LoadBalancerEvent, + loadBalancerID: LoadBalancerID, + in group: inout DiscardingTaskGroup, + iterator: inout RPCAsyncSequence.AsyncIterator? + ) async { + switch event { + case .connectivityStateChanged(let connectivityState): + let actions = self.state.withLockedValue { state in + state.loadBalancerStateChanged(to: connectivityState, id: loadBalancerID) + } + + if let newState = actions.publishState { + self._connectivityState.continuation.yield(newState) + } + + if let subchannel = actions.close { + subchannel.close() + } + + if let resumable = actions.resumeContinuations { + for continuation in resumable.continuations { + continuation.resume(with: resumable.result) + } + } + + if actions.finish { + // Fully closed. + self._connectivityState.continuation.finish() + self.input.continuation.finish() + } + + case .requiresNameResolution: + await self.resolve(iterator: &iterator, in: &group) + } + } + + private func resolve( + iterator: inout RPCAsyncSequence.AsyncIterator?, + in group: inout DiscardingTaskGroup + ) async { + guard var iterator = iterator else { return } + + do { + if let result = try await iterator.next() { + self.handleNameResolutionResult(result, in: &group) + } else { + self.close() + } + } catch { + self.close() + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel { + struct StateMachine { + enum State { + case notRunning(NotRunning) + case running(Running) + case stopping(Stopping) + case stopped + case _modifying + + struct NotRunning { + /// Queue of requests waiting for a load-balancer. + var queue: RequestQueue + /// A handle to the name resolver task. + var nameResolverHandle: CancellableTaskHandle? + + init() { + self.queue = RequestQueue() + } + } + + struct Running { + /// The connectivity state of the channel. + var connectivityState: ConnectivityState + /// The load-balancer currently in use. + var current: LoadBalancer + /// The next load-balancer to use. This will be promoted to `current` when it enters the + /// ready state. + var next: LoadBalancer? + /// Previously created load-balancers which are in the process of shutting down. + var past: [LoadBalancerID: LoadBalancer] + /// Queue of requests wait for a load-balancer. + var queue: RequestQueue + /// A handle to the name resolver task. + var nameResolverHandle: CancellableTaskHandle? + + init( + from state: NotRunning, + loadBalancer: LoadBalancer + ) { + self.connectivityState = .idle + self.current = loadBalancer + self.next = nil + self.past = [:] + self.queue = state.queue + self.nameResolverHandle = state.nameResolverHandle + } + } + + struct Stopping { + /// Previously created load-balancers which are in the process of shutting down. + var past: [LoadBalancerID: LoadBalancer] + + init(from state: Running) { + self.past = state.past + } + + init(loadBalancers: [LoadBalancerID: LoadBalancer]) { + self.past = loadBalancers + } + } + } + + /// The current state. + private var state: State + /// Whether the channel is running. + private var running: Bool + + init() { + self.state = .notRunning(State.NotRunning()) + self.running = false + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel.StateMachine { + mutating func start() { + precondition(!self.running, "channel must only be started once") + self.running = true + } + + mutating func setNameResolverTaskHandle(_ handle: CancellableTaskHandle) { + switch self.state { + case .notRunning(var state): + state.nameResolverHandle = handle + self.state = .notRunning(state) + case .running, .stopping, .stopped, ._modifying: + fatalError("Invalid state") + } + } + + enum LoadBalancerKind { + case roundRobin + + func matches(loadBalancer: LoadBalancer) -> Bool { + switch (self, loadBalancer) { + case (.roundRobin, .roundRobin): + return true + } + } + } + + enum OnChangeLoadBalancer { + case runLoadBalancer(LoadBalancer, stop: LoadBalancer?) + case updateLoadBalancer(LoadBalancer) + case none + } + + mutating func changeLoadBalancerKind( + to newLoadBalancerKind: LoadBalancerKind, + _ makeLoadBalancer: () -> LoadBalancer + ) -> OnChangeLoadBalancer { + let onChangeLoadBalancer: OnChangeLoadBalancer + + switch self.state { + case .notRunning(let state): + let loadBalancer = makeLoadBalancer() + let state = State.Running(from: state, loadBalancer: loadBalancer) + self.state = .running(state) + onChangeLoadBalancer = .runLoadBalancer(state.current, stop: nil) + + case .running(var state): + self.state = ._modifying + + if let next = state.next { + if newLoadBalancerKind.matches(loadBalancer: next) { + onChangeLoadBalancer = .updateLoadBalancer(next) + } else { + // The 'next' didn't become ready in time. Close it and replace it with a load-balancer + // of the next kind. + let nextNext = makeLoadBalancer() + let previous = state.next + state.next = nextNext + state.past[next.id] = next + onChangeLoadBalancer = .runLoadBalancer(nextNext, stop: previous) + } + } else { + if newLoadBalancerKind.matches(loadBalancer: state.current) { + onChangeLoadBalancer = .updateLoadBalancer(state.current) + } else { + // Create the 'next' load-balancer, it'll replace 'current' when it becomes ready. + let next = makeLoadBalancer() + state.next = next + onChangeLoadBalancer = .runLoadBalancer(next, stop: nil) + } + } + + self.state = .running(state) + + case .stopping, .stopped: + onChangeLoadBalancer = .none + + case ._modifying: + fatalError("Invalid state") + } + + return onChangeLoadBalancer + } + + struct ConnectivityStateChangeActions { + var close: LoadBalancer? = nil + var publishState: ConnectivityState? = nil + var resumeContinuations: ResumableContinuations? = nil + var finish: Bool = false + + struct ResumableContinuations { + var continuations: [CheckedContinuation] + var result: Result + } + } + + mutating func loadBalancerStateChanged( + to connectivityState: ConnectivityState, + id: LoadBalancerID + ) -> ConnectivityStateChangeActions { + var actions = ConnectivityStateChangeActions() + + switch self.state { + case .running(var state): + self.state = ._modifying + + if id == state.current.id { + // No change in state, ignore. + if state.connectivityState == connectivityState { + self.state = .running(state) + break + } + + state.connectivityState = connectivityState + actions.publishState = connectivityState + + switch connectivityState { + case .ready: + // Current load-balancer became ready; resume all continuations in the queue. + let continuations = state.queue.removeAll() + actions.resumeContinuations = ConnectivityStateChangeActions.ResumableContinuations( + continuations: continuations, + result: .success(state.current) + ) + + case .transientFailure, .shutdown: // shutdown includes shutting down + // Current load-balancer failed. Remove all the 'fast-failing' continuations in the + // queue, these are RPCs which set the 'wait for ready' option to false. The rest of + // the entries in the queue will wait for a load-balancer to become ready. + let continuations = state.queue.removeFastFailingEntries() + actions.resumeContinuations = ConnectivityStateChangeActions.ResumableContinuations( + continuations: continuations, + result: .failure(RPCError(code: .unavailable, message: "channel isn't ready")) + ) + + case .idle, .connecting: + () // Ignore. + } + } else if let next = state.next, next.id == id { + // State change came from the next LB, if it's now ready promote it to be the current. + switch connectivityState { + case .ready: + // Next load-balancer is ready, promote it to current. + let previous = state.current + state.past[previous.id] = previous + state.current = next + state.next = nil + + actions.close = previous + + if state.connectivityState != connectivityState { + actions.publishState = connectivityState + } + + actions.resumeContinuations = ConnectivityStateChangeActions.ResumableContinuations( + continuations: state.queue.removeAll(), + result: .success(next) + ) + + case .idle, .connecting, .transientFailure, .shutdown: + () + } + } + + self.state = .running(state) + + case .stopping(var state): + self.state = ._modifying + + // Remove the load balancer if it's now shutdown. + switch connectivityState { + case .shutdown: + state.past.removeValue(forKey: id) + case .idle, .connecting, .ready, .transientFailure: + () + } + + // If that was the last load-balancer then finish the input streams so that the channel + // eventually finishes. + if state.past.isEmpty { + actions.finish = true + self.state = .stopped + } else { + self.state = .stopping(state) + } + + case .notRunning, .stopped: + () + + case ._modifying: + fatalError("Invalid state") + } + + return actions + } + + enum OnMakeStream { + /// Use the given load-balancer to make a stream. + case useLoadBalancer(LoadBalancer) + /// Join the queue and wait until a load-balancer becomes ready. + case joinQueue + /// Fail the stream request, the channel isn't in a suitable state. + case failRPC + } + + func makeStream(waitForReady: Bool) -> OnMakeStream { + let onMakeStream: OnMakeStream + + switch self.state { + case .notRunning: + onMakeStream = .joinQueue + + case .running(let state): + switch state.connectivityState { + case .idle, .connecting: + onMakeStream = .joinQueue + case .ready: + onMakeStream = .useLoadBalancer(state.current) + case .transientFailure: + onMakeStream = waitForReady ? .joinQueue : .failRPC + case .shutdown: + onMakeStream = .failRPC + } + + case .stopping, .stopped: + onMakeStream = .failRPC + + case ._modifying: + fatalError("Invalid state") + } + + return onMakeStream + } + + mutating func enqueue( + continuation: CheckedContinuation, + waitForReady: Bool, + id: QueueEntryID + ) -> Bool { + switch self.state { + case .notRunning(var state): + self.state = ._modifying + state.queue.append(continuation: continuation, waitForReady: waitForReady, id: id) + self.state = .notRunning(state) + return true + case .running(var state): + self.state = ._modifying + state.queue.append(continuation: continuation, waitForReady: waitForReady, id: id) + self.state = .running(state) + return true + case .stopping, .stopped: + return false + case ._modifying: + fatalError("Invalid state") + } + } + + mutating func dequeueContinuation( + id: QueueEntryID + ) -> CheckedContinuation? { + switch self.state { + case .notRunning(var state): + self.state = ._modifying + let continuation = state.queue.removeEntry(withID: id) + self.state = .notRunning(state) + return continuation + + case .running(var state): + self.state = ._modifying + let continuation = state.queue.removeEntry(withID: id) + self.state = .running(state) + return continuation + + case .stopping, .stopped: + return nil + + case ._modifying: + fatalError("Invalid state") + } + } + + enum OnClose { + case none + case cancelAll([RequestQueue.Continuation]) + case close(LoadBalancer, LoadBalancer?, CancellableTaskHandle?) + } + + mutating func close() -> OnClose { + let onClose: OnClose + + switch self.state { + case .notRunning(var state): + self.state = .stopped + onClose = .cancelAll(state.queue.removeAll()) + + case .running(var state): + onClose = .close(state.current, state.next, state.nameResolverHandle) + + state.past[state.current.id] = state.current + if let next = state.next { + state.past[next.id] = next + } + + self.state = .stopping(State.Stopping(loadBalancers: state.past)) + + case .stopping, .stopped: + onClose = .none + + case ._modifying: + fatalError("Invalid state") + } + + return onClose + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift b/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift new file mode 100644 index 000000000..c97574f37 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift @@ -0,0 +1,79 @@ +/* + * 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. + */ + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension DiscardingTaskGroup { + /// Adds a child task to the group which is individually cancellable. + /// + /// - Parameter operation: The task to add to the group. + /// - Returns: A handle which can be used to cancel the task without cancelling the rest of + /// the group. + @inlinable + mutating func addCancellableTask( + _ operation: @Sendable @escaping () async -> Void + ) -> CancellableTaskHandle { + let signal = AsyncStream.makeStream(of: Void.self) + self.addTask { + return await withTaskGroup(of: FinishedOrCancelled.self) { group in + group.addTask { + await operation() + return .finished + } + + group.addTask { + for await _ in signal.stream {} + return .cancelled + } + + let first = await group.next()! + group.cancelAll() + let second = await group.next()! + + switch (first, second) { + case (.finished, .cancelled), (.cancelled, .finished): + return + default: + fatalError("Internal inconsistency") + } + } + } + + return CancellableTaskHandle(continuation: signal.continuation) + } + + @usableFromInline + enum FinishedOrCancelled { + case finished + case cancelled + } +} + +@usableFromInline +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct CancellableTaskHandle: Sendable { + @usableFromInline + private(set) var continuation: AsyncStream.Continuation + + @inlinable + init(continuation: AsyncStream.Continuation) { + self.continuation = continuation + } + + @inlinable + func cancel() { + self.continuation.finish() + } +} diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index 85b6aea49..85b5fb5be 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -100,3 +100,70 @@ extension ChannelPipeline.SynchronousOperations { return (connectionChannel, streamMultiplexer) } } + +extension ChannelPipeline.SynchronousOperations { + @_spi(Package) + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + public func configureGRPCClientPipeline( + channel: Channel, + config: GRPCChannel.Config + ) throws -> ( + NIOAsyncChannel, + NIOHTTP2Handler.AsyncStreamMultiplexer + ) { + // Window size which mustn't exceed 2^32 - 1 (RFC 9113 § 6.1.3). + let clampedTargetWindowSize = min(config.http2.targetWindowSize, (1 << 31) - 1) + + // Max frame size must be in the range 2^14 ..< 2^24 (RFC 9113 § 6.1.3). + let clampedMaxFrameSize: Int + if config.http2.maxFrameSize >= (1 << 24) { + clampedMaxFrameSize = (1 << 24) - 1 + } else if config.http2.maxFrameSize < (1 << 14) { + clampedMaxFrameSize = (1 << 14) + } else { + clampedMaxFrameSize = config.http2.maxFrameSize + } + + // Use NIOs defaults as a starting point. + var http2 = NIOHTTP2Handler.Configuration() + http2.stream.targetWindowSize = clampedTargetWindowSize + http2.connection.initialSettings = [ + // Disallow servers from creating push streams. + HTTP2Setting(parameter: .enablePush, value: 0), + // Set the initial window size and max frame size to the clamped configured values. + HTTP2Setting(parameter: .initialWindowSize, value: clampedTargetWindowSize), + HTTP2Setting(parameter: .maxFrameSize, value: clampedMaxFrameSize), + // Use NIOs default max header list size (16kB) + HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize), + ] + + let multiplexer = try self.configureAsyncHTTP2Pipeline( + mode: .client, + configuration: http2 + ) { stream in + // Shouldn't happen, push-promises are disabled so the server shouldn't be able to + // open streams. + stream.close() + } + + let connectionHandler = ClientConnectionHandler( + eventLoop: self.eventLoop, + maxIdleTime: config.idle.map { TimeAmount($0.maxTime) }, + keepaliveTime: config.keepalive.map { TimeAmount($0.time) }, + keepaliveTimeout: config.keepalive.map { TimeAmount($0.timeout) }, + keepaliveWithoutCalls: config.keepalive?.permitWithoutCalls ?? false + ) + + try self.addHandler(connectionHandler) + + let connection = try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + inboundType: ClientConnectionEvent.self, + outboundType: Void.self + ) + ) + + return (connection, multiplexer) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift new file mode 100644 index 000000000..5fde16d06 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -0,0 +1,596 @@ +/* + * 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 +@_spi(Package) @testable import GRPCHTTP2Core +import NIOHTTP2 +import NIOPosix +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class GRPCChannelTests: XCTestCase { + func testDefaultServiceConfig() throws { + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoGet)])] + serviceConfig.retryThrottling = try ServiceConfig.RetryThrottling( + maxTokens: 100, + tokenRatio: 0.1 + ) + + let channel = GRPCChannel( + resolver: .static(endpoints: []), + connector: .never, + config: .defaults, + defaultServiceConfig: serviceConfig + ) + + XCTAssertNotNil(channel.configuration(forMethod: .echoGet)) + XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + + let throttle = try XCTUnwrap(channel.retryThrottle) + XCTAssertEqual(throttle.maximumTokens, 100) + XCTAssertEqual(throttle.tokenRatio, 0.1) + } + + func testServiceConfigFromResolver() async throws { + // Verify that service config from the resolver takes precedence over the default service + // config. This is done indirectly by checking method config and retry throttle config. + + // Create a service config to provide via the resolver. + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoGet)])] + serviceConfig.retryThrottling = try ServiceConfig.RetryThrottling( + maxTokens: 100, + tokenRatio: 0.1 + ) + + // Need a server to connect to, no RPCs will be created though. + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + + let channel = GRPCChannel( + resolver: .static(endpoints: [Endpoint(addresses: [address])], serviceConfig: serviceConfig), + connector: .posix(), + config: .defaults, + defaultServiceConfig: ServiceConfig() + ) + + // Not resolved yet so the default (empty) service config is used. + XCTAssertNil(channel.configuration(forMethod: .echoGet)) + XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + XCTAssertNil(channel.retryThrottle) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await server.run(.never) + } + + group.addTask { + await channel.connect() + } + + for await event in channel.connectivityState { + switch event { + case .ready: + // When the channel is ready it must have the service config from the resolver. + XCTAssertNotNil(channel.configuration(forMethod: .echoGet)) + XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + + let throttle = try XCTUnwrap(channel.retryThrottle) + XCTAssertEqual(throttle.maximumTokens, 100) + XCTAssertEqual(throttle.tokenRatio, 0.1) + + // Now close. + channel.close() + + default: + () + } + } + + group.cancelAll() + } + } + + func testServiceConfigFromResolverAfterUpdate() async throws { + // Verify that the channel uses service config from the resolver and that it uses the latest + // version provided by the resolver. This is done indirectly by checking method config and retry + // throttle config. + + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + + let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) + let channel = GRPCChannel( + resolver: resolver, + connector: .posix(), + config: .defaults, + defaultServiceConfig: ServiceConfig() + ) + + // Not resolved yet so the default (empty) service config is used. + XCTAssertNil(channel.configuration(forMethod: .echoGet)) + XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + XCTAssertNil(channel.retryThrottle) + + // Yield the first address list and service config. + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoGet)])] + serviceConfig.retryThrottling = try ServiceConfig.RetryThrottling( + maxTokens: 100, + tokenRatio: 0.1 + ) + let resolutionResult = NameResolutionResult( + endpoints: [Endpoint(address)], + serviceConfig: .success(serviceConfig) + ) + continuation.yield(resolutionResult) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await server.run(.never) + } + + group.addTask { + await channel.connect() + } + + for await event in channel.connectivityState { + switch event { + case .ready: + // When the channel it must have the service config from the resolver. + XCTAssertNotNil(channel.configuration(forMethod: .echoGet)) + XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + let throttle = try XCTUnwrap(channel.retryThrottle) + XCTAssertEqual(throttle.maximumTokens, 100) + XCTAssertEqual(throttle.tokenRatio, 0.1) + + // Now yield a new service config with the same addresses. + var resolutionResult = resolutionResult + serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoUpdate)])] + serviceConfig.retryThrottling = nil + resolutionResult.serviceConfig = .success(serviceConfig) + continuation.yield(resolutionResult) + + // This should be propagated quickly. + try await XCTPoll(every: .milliseconds(10)) { + let noConfigForGet = channel.configuration(forMethod: .echoGet) == nil + let configForUpdate = channel.configuration(forMethod: .echoUpdate) != nil + let noThrottle = channel.retryThrottle == nil + return noConfigForGet && configForUpdate && noThrottle + } + + channel.close() + + default: + () + } + } + + group.cancelAll() + } + } + + func testPushBasedResolutionUpdates() async throws { + // Verify that the channel responds to name resolution changes which are pushed into + // the resolver. Do this by starting two servers and only making the address of one available + // via the resolver at a time. Server identity is provided via metadata in the RPC. + + // Start a few servers. + let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address1 = try await server1.bind() + + let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address2 = try await server2.bind() + + // Setup a resolver and push some changes into it. + let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) + let resolution1 = NameResolutionResult(endpoints: [Endpoint(address1)], serviceConfig: nil) + continuation.yield(resolution1) + + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + let channel = GRPCChannel( + resolver: resolver, + connector: .posix(), + config: .defaults, + defaultServiceConfig: serviceConfig + ) + + try await withThrowingDiscardingTaskGroup { group in + // Servers respond with their own address in the trailing metadata. + for (server, address) in [(server1, address1), (server2, address2)] { + group.addTask { + try await server.run { inbound, outbound in + let status = Status(code: .ok, message: "") + let metadata: Metadata = ["server-addr": "\(address)"] + try await outbound.write(.status(status, metadata)) + outbound.finish() + } + } + } + + group.addTask { + await channel.connect() + } + + // The stream will be queued until the channel is ready. + let serverAddress1 = try await channel.serverAddress() + XCTAssertEqual(serverAddress1, "\(address1)") + XCTAssertEqual(server1.clients.count, 1) + XCTAssertEqual(server2.clients.count, 0) + + // Yield the second address. Because this happens asynchronously there's no guarantee that + // the next stream will be made against the same server, so poll until the servers have the + // appropriate connections. + let resolution2 = NameResolutionResult(endpoints: [Endpoint(address2)], serviceConfig: nil) + continuation.yield(resolution2) + + try await XCTPoll(every: .milliseconds(10)) { + server1.clients.count == 0 && server2.clients.count == 1 + } + + let serverAddress2 = try await channel.serverAddress() + XCTAssertEqual(serverAddress2, "\(address2)") + + group.cancelAll() + } + } + + func testPullBasedResolutionUpdates() async throws { + // Verify that the channel responds to name resolution changes which are pulled because a + // subchannel asked the channel to re-resolve. Do this by starting two servers and changing + // which is available via resolution updates. Server identity is provided via metadata in + // the RPC. + + // Start a few servers. + let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address1 = try await server1.bind() + + let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address2 = try await server2.bind() + + // Setup a resolve which we push changes into. + let (resolver, continuation) = NameResolver.dynamic(updateMode: .pull) + + // Yield the addresses. + for address in [address1, address2] { + let resolution = NameResolutionResult(endpoints: [Endpoint(address)], serviceConfig: nil) + continuation.yield(resolution) + } + + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + let channel = GRPCChannel( + resolver: resolver, + connector: .posix(), + config: .defaults, + defaultServiceConfig: serviceConfig + ) + + try await withThrowingDiscardingTaskGroup { group in + // Servers respond with their own address in the trailing metadata. + for (server, address) in [(server1, address1), (server2, address2)] { + group.addTask { + try await server.run { inbound, outbound in + let status = Status(code: .ok, message: "") + let metadata: Metadata = ["server-addr": "\(address)"] + try await outbound.write(.status(status, metadata)) + outbound.finish() + } + } + } + + group.addTask { + await channel.connect() + } + + // The stream will be queued until the channel is ready. + let serverAddress1 = try await channel.serverAddress() + XCTAssertEqual(serverAddress1, "\(address1)") + XCTAssertEqual(server1.clients.count, 1) + XCTAssertEqual(server2.clients.count, 0) + + // Tell the first server to GOAWAY. This will cause the subchannel to re-resolve. + let server1Client = try XCTUnwrap(server1.clients.first) + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway(lastStreamID: 1, errorCode: .noError, opaqueData: nil) + ) + try await server1Client.writeAndFlush(goAway) + + // Poll until the first client drops, addresses are re-resolved, and a connection is + // established to server2. + try await XCTPoll(every: .milliseconds(10)) { + server1.clients.count == 0 && server2.clients.count == 1 + } + + let serverAddress2 = try await channel.serverAddress() + XCTAssertEqual(serverAddress2, "\(address2)") + + group.cancelAll() + } + } + + func testCloseWhenRPCsAreInProgress() async throws { + try XCTSkipIf(true, "https://github.com/apple/swift-nio-http2/pull/439") + + // Verify that closing the channel while there are RPCs in progress allows the RPCs to finish + // gracefully. + + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await server.run(.echo) + } + + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + + let channel = GRPCChannel( + resolver: .static(endpoints: [Endpoint(address)]), + connector: .posix(), + config: .defaults, + defaultServiceConfig: serviceConfig + ) + + group.addTask { + await channel.connect() + } + + try await channel.withStream(descriptor: .echoGet, options: .defaults) { stream in + try await stream.outbound.write(.metadata([:])) + + var iterator = stream.inbound.makeAsyncIterator() + let part1 = try await iterator.next() + switch part1 { + case .metadata: + // Got metadata, close the channel. + channel.close() + case .message, .status, .none: + XCTFail("Expected metadata, got \(String(describing: part1))") + } + + for await state in channel.connectivityState { + switch state { + case .shutdown: + // Happens when shutting-down has been initiated, so finish the RPC. + stream.outbound.finish() + + let part2 = try await iterator.next() + switch part2 { + case .status(let status, _): + XCTAssertEqual(status.code, .ok) + case .metadata, .message, .none: + XCTFail("Expected status, got \(String(describing: part2))") + } + + default: + () + } + } + } + + group.cancelAll() + } + } + + func testQueueRequestsWhileNotReady() async throws { + // Verify that requests are queued until the channel becomes ready. As creating streams + // will race with the channel becoming ready, we add numerous tasks to the task group which + // each create a stream before making the server address known to the channel via the resolver. + // This isn't perfect as the resolution _could_ happen before attempting to create all streams + // although this is unlikely. + + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + + let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + let channel = GRPCChannel( + resolver: resolver, + connector: .posix(), + config: .defaults, + defaultServiceConfig: serviceConfig + ) + + enum Subtask { case rpc, other } + try await withThrowingTaskGroup(of: Subtask.self) { group in + // Run the server. + group.addTask { + try await server.run { inbound, outbound in + for try await part in inbound { + switch part { + case .metadata: + try await outbound.write(.metadata([:])) + case .message(let bytes): + try await outbound.write(.message(bytes)) + } + } + + let status = Status(code: .ok, message: "") + try await outbound.write(.status(status, [:])) + outbound.finish() + } + + return .other + } + + group.addTask { + await channel.connect() + return .other + } + + // Start a bunch of requests. These won't start until an address is yielded, they should + // be queued though. + for _ in 1 ... 100 { + group.addTask { + try await channel.withStream(descriptor: .echoGet, options: .defaults) { stream in + try await stream.outbound.write(.metadata([:])) + stream.outbound.finish() + + for try await part in stream.inbound { + switch part { + case .metadata, .message: + () + case .status(let status, _): + XCTAssertEqual(status.code, .ok) + } + } + } + + return .rpc + } + } + + // At least some of the RPCs should have been queued by now. + let resolution = NameResolutionResult(endpoints: [Endpoint(address)], serviceConfig: nil) + continuation.yield(resolution) + + var outstandingRPCs = 100 + for try await subtask in group { + switch subtask { + case .rpc: + outstandingRPCs -= 1 + + // All RPCs done, close the channel and cancel the group to stop the server. + if outstandingRPCs == 0 { + channel.close() + group.cancelAll() + } + + case .other: + () + } + } + } + } + + func testQueueRequestsFailFast() async throws { + // Verifies that if 'waitsForReady' is 'false', that queued requests are failed when there is + // a transient failure. The transient failure is triggered by attempting to connect to a + // non-existent server. + + let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + let channel = GRPCChannel( + resolver: resolver, + connector: .posix(), + config: .defaults, + defaultServiceConfig: serviceConfig + ) + + enum Subtask { case rpc, other } + try await withThrowingTaskGroup(of: Subtask.self) { group in + group.addTask { + await channel.connect() + return .other + } + + for _ in 1 ... 100 { + group.addTask { + var options = CallOptions.defaults + options.waitForReady = false + + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + try await channel.withStream(descriptor: .echoGet, options: options) { _ in + XCTFail("Unexpected stream") + } + } errorHandler: { error in + XCTAssertEqual(error.code, .unavailable) + } + + return .rpc + } + } + + // At least some of the RPCs should have been queued by now. + let resolution = NameResolutionResult( + endpoints: [Endpoint(.unixDomainSocket(path: "/test-queue-requests-fail-fast"))], + serviceConfig: nil + ) + continuation.yield(resolution) + + var outstandingRPCs = 100 + for try await subtask in group { + switch subtask { + case .rpc: + outstandingRPCs -= 1 + + // All RPCs done, close the channel and cancel the group to stop the server. + if outstandingRPCs == 0 { + channel.close() + group.cancelAll() + } + + case .other: + () + } + } + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel.Config { + static var defaults: Self { + Self( + http2: .defaults, + backoff: .defaults, + idle: .defaults, + keepalive: nil, + compression: .defaults + ) + } +} + +extension Endpoint { + init(_ addresses: SocketAddress...) { + self.init(addresses: addresses) + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel { + fileprivate func serverAddress() async throws -> String? { + let values: Metadata.StringValues? = try await self.withStream( + descriptor: .echoGet, + options: .defaults + ) { stream in + try await stream.outbound.write(.metadata([:])) + stream.outbound.finish() + + for try await part in stream.inbound { + switch part { + case .metadata, .message: + XCTFail("Unexpected part: \(part)") + case .status(_, let metadata): + return metadata[stringValues: "server-addr"] + } + } + return nil + } + + return values?.first(where: { _ in true }) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift new file mode 100644 index 000000000..ba4ab7cf2 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift @@ -0,0 +1,74 @@ +/* + * 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 GRPCHTTP2Core + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension NameResolver { + static func `static`( + endpoints: [Endpoint], + serviceConfig: ServiceConfig? = nil + ) -> Self { + let result = NameResolutionResult( + endpoints: endpoints, + serviceConfig: serviceConfig.map { .success($0) } + ) + + return NameResolver( + names: RPCAsyncSequence(wrapping: ConstantAsyncSequence(element: result)), + updateMode: .pull + ) + } + + static func `dynamic`( + updateMode: UpdateMode + ) -> (Self, AsyncStream.Continuation) { + let (stream, continuation) = AsyncStream.makeStream(of: NameResolutionResult.self) + let resolver = NameResolver(names: RPCAsyncSequence(wrapping: stream), updateMode: updateMode) + return (resolver, continuation) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +struct ConstantAsyncSequence: AsyncSequence { + private let result: Result + + init(element: Element) { + self.result = .success(element) + } + + init(error: any Error) { + self.result = .failure(error) + } + + func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(result: self.result) + } + + struct AsyncIterator: AsyncIteratorProtocol { + private let result: Result + + fileprivate init(result: Result) { + self.result = result + } + + func next() async throws -> Element? { + try self.result.get() + } + } + +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift index 4d9e84d38..bb678a108 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -19,6 +19,7 @@ import NIOConcurrencyHelpers import NIOCore import NIOHTTP2 import NIOPosix +import XCTest @testable import GRPCHTTP2Core @@ -120,15 +121,17 @@ final class TestServer: Sendable { fatalError("bind() must be called first") } - try await server.executeThenClose { inbound, _ in - try await withThrowingTaskGroup(of: Void.self) { multiplexerGroup in - for try await multiplexer in inbound { - multiplexerGroup.addTask { - try await withThrowingTaskGroup(of: Void.self) { streamGroup in - for try await stream in multiplexer { - streamGroup.addTask { - try await stream.executeThenClose { inbound, outbound in - try await handle(inbound, outbound) + do { + try await server.executeThenClose { inbound, _ in + try await withThrowingTaskGroup(of: Void.self) { multiplexerGroup in + for try await multiplexer in inbound { + multiplexerGroup.addTask { + try await withThrowingTaskGroup(of: Void.self) { streamGroup in + for try await stream in multiplexer { + streamGroup.addTask { + try await stream.executeThenClose { inbound, outbound in + try await handle(inbound, outbound) + } } } } @@ -136,6 +139,39 @@ final class TestServer: Sendable { } } } + } catch is CancellationError { + () + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension TestServer { + enum RunHandler { + case echo + case never + } + + func run(_ handler: RunHandler) async throws { + switch handler { + case .echo: + try await self.run { inbound, outbound in + for try await part in inbound { + switch part { + case .metadata: + try await outbound.write(.metadata([:])) + case .message(let bytes): + try await outbound.write(.message(bytes)) + } + } + } + + case .never: + try await self.run { inbound, outbound in + XCTFail("Unexpected stream") + try await outbound.write(.status(Status(code: .unavailable, message: ""), [:])) + outbound.finish() + } } } } diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift new file mode 100644 index 000000000..68b492869 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift @@ -0,0 +1,32 @@ +/* + * Copyright 2023, 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. + */ + +#if swift(<5.9) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncStream { + @inlinable + static func makeStream( + of elementType: Element.Type = Element.self, + bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded + ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { + var continuation: AsyncStream.Continuation! + let stream = AsyncStream(Element.self, bufferingPolicy: limit) { + continuation = $0 + } + return (stream, continuation) + } +} +#endif diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift index a9a31c688..e7c1ef50a 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift @@ -20,4 +20,15 @@ extension MethodDescriptor { static var echoGet: Self { MethodDescriptor(service: "echo.Echo", method: "Get") } + + static var echoUpdate: Self { + MethodDescriptor(service: "echo.Echo", method: "Update") + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension MethodConfig.Name { + init(_ descriptor: MethodDescriptor) { + self = MethodConfig.Name(service: descriptor.service, method: descriptor.method) + } } From 7bba845045b21b3e13f7ed21de17bee6607024b1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 4 Jun 2024 14:33:57 +0100 Subject: [PATCH 330/580] Better distinguish between connection drops and regular closes (#1901) Motivation: If a connection is closed because the tcp connection is dropped then the `Connection` doesn't receive any indication as to why it was closed which ends up being classified as a locally initiated close. In turns the subchannel will be shutdown and can't be reused. This isn't correct: we know the connection was dropped unexpectedly and we whether there were any active RPCs at the time. This means the subchannel can transition to idle if there were no active RPCs or transient failure if there were. Modifications: - Add a new 'unexpected' close reason to the connection handler which optionally includes an error. This can provide further diagnostic information as to why a connection has been dropped. - Alter the semantics in subchannel such that go-aways and unexpected closes when there are no RPCs transition cause a transition to the idle state, and unexpected closes when there are RPCs lead to a transition to the transient failure state. Result: More sensible state updates when connections are closed --- .../Connection/ClientConnectionHandler.swift | 38 +++- .../Client/Connection/Connection.swift | 26 ++- .../RoundRobinLoadBalancer.swift | 57 ++++-- .../Connection/LoadBalancers/Subchannel.swift | 17 +- .../ClientConnectionHandlerTests.swift | 60 ++++++ .../Connection/Connection+Equatable.swift | 43 +++- .../Client/Connection/ConnectionTests.swift | 21 ++ .../LoadBalancers/SubchannelTests.swift | 184 +++++++++++++++++- 8 files changed, 406 insertions(+), 40 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 7b2fbc51c..1c4d21a6f 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -19,8 +19,8 @@ import NIOHTTP2 /// An event which happens on a client's HTTP/2 connection. @_spi(Package) -public enum ClientConnectionEvent: Sendable, Hashable { - public enum CloseReason: Sendable, Hashable { +public enum ClientConnectionEvent: Sendable { + public enum CloseReason: Sendable { /// The server sent a GOAWAY frame to the client. case goAway(HTTP2ErrorCode, String) /// The keep alive timer fired and subsequently timed out. @@ -29,6 +29,8 @@ public enum ClientConnectionEvent: Sendable, Hashable { case idle /// The local peer initiated the close. case initiatedLocally + /// The connection was closed unexpectedly + case unexpected(Error?, isIdle: Bool) } /// The connection is now ready. @@ -142,9 +144,15 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl switch self.state.closed() { case .none: () + + case .unexpectedClose(let error, let isIdle): + let event = self.wrapInboundOut(.closing(.unexpected(error, isIdle: isIdle))) + context.fireChannelRead(event) + case .succeed(let promise): promise.succeed() } + self.keepaliveTimer?.cancel() self.keepaliveTimeoutTimer.cancel() } @@ -164,6 +172,11 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl context.fireUserInboundEventTriggered(event) } + func errorCaught(context: ChannelHandlerContext, error: any Error) { + self.state.receivedError(error) + context.fireErrorCaught(error) + } + func channelRead(context: ChannelHandlerContext, data: NIOAny) { let frame = self.unwrapInboundIn(data) self.inReadLoop = true @@ -394,11 +407,13 @@ extension ClientConnectionHandler { var openStreams: Set var allowKeepaliveWithoutCalls: Bool var receivedConnectionPreface: Bool + var error: (any Error)? init(allowKeepaliveWithoutCalls: Bool) { self.openStreams = [] self.allowKeepaliveWithoutCalls = allowKeepaliveWithoutCalls self.receivedConnectionPreface = false + self.error = nil } mutating func receivedSettings() -> Bool { @@ -440,6 +455,17 @@ extension ClientConnectionHandler { } } + /// Record that an error was received. + mutating func receivedError(_ error: any Error) { + switch self.state { + case .active(var active): + active.error = error + self.state = .active(active) + case .closing, .closed: + () + } + } + /// Record that the stream with the given ID has been opened. mutating func streamOpened(_ id: HTTP2StreamID) { switch self.state { @@ -554,18 +580,22 @@ extension ClientConnectionHandler { enum OnClosed { case succeed(EventLoopPromise) + case unexpectedClose(Error?, isIdle: Bool) case none } /// Marks the state as closed. mutating func closed() -> OnClosed { switch self.state { - case .active, .closed: + case .active(let state): self.state = .closed - return .none + return .unexpectedClose(state.error, isIdle: state.openStreams.isEmpty) case .closing(let closing): self.state = .closed return closing.closePromise.map { .succeed($0) } ?? .none + case .closed: + self.state = .closed + return .none } } } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index b25f1fd6a..c5c0e6838 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -69,7 +69,7 @@ struct Connection: Sendable { /// Closed because the remote peer initiate shutdown (i.e. sent a GOAWAY frame). case remote /// Closed because the connection encountered an unexpected error. - case error(Error) + case error(Error, wasIdle: Bool) } /// Inputs to the 'run' method. @@ -263,7 +263,7 @@ struct Connection: Sendable { // The connection will close at some point soon, yield a notification for this // because the close might not be imminent and this could result in address resolution. self.event.continuation.yield(.goingAway(errorCode, reason)) - case .idle, .keepaliveExpired, .initiatedLocally: + case .idle, .keepaliveExpired, .initiatedLocally, .unexpected: // The connection will be closed imminently in these cases there's no need to do // anything. () @@ -292,9 +292,25 @@ struct Connection: Sendable { // Remote peer told us to GOAWAY. connectionCloseReason = .remote - case .initiatedLocally, .none: + case .initiatedLocally: // Shutdown was initiated locally. connectionCloseReason = .initiatedLocally + + case .unexpected(let error, let isIdle): + let error = RPCError( + code: .unavailable, + message: "The TCP connection was dropped unexpectedly.", + cause: error + ) + connectionCloseReason = .error(error, wasIdle: isIdle) + + case .none: + let error = RPCError( + code: .unavailable, + message: "The TCP connection was dropped unexpectedly.", + cause: nil + ) + connectionCloseReason = .error(error, wasIdle: true) } finalEvent = .closed(connectionCloseReason) @@ -313,7 +329,7 @@ struct Connection: Sendable { // Any error must come from consuming the inbound channel meaning that the connection // must be borked, wrap it up and close. let rpcError = RPCError(code: .unavailable, message: "connection closed", cause: error) - finalEvent = .closed(.error(rpcError)) + finalEvent = .closed(.error(rpcError, wasIdle: true)) } else { // The connection never became ready, this therefore counts as a failed connect attempt. finalEvent = .connectFailed(makeNeverReadyError(cause: error)) @@ -460,6 +476,8 @@ extension Connection { extension ClientConnectionEvent.CloseReason { fileprivate var precedence: Int { switch self { + case .unexpected: + return -1 case .goAway: return 0 case .idle: diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift index fa0d97429..f2f785295 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -305,8 +305,8 @@ extension RoundRobinLoadBalancer { private func handleSubchannelGoingAway(key: EndpointKey) { switch self.state.withLockedValue({ $0.parkSubchannel(withKey: key) }) { - case .closeAndUpdateState(_, let connectivityState): - // No need to close the subchannel, it's already going away and will close itself. + case .closeAndUpdateState(let subchannel, let connectivityState): + subchannel.close() if let connectivityState = connectivityState { self.event.continuation.yield(.connectivityStateChanged(connectivityState)) } @@ -398,10 +398,18 @@ extension RoundRobinLoadBalancer { } } else { switch state { - case .idle, .connecting, .ready, .transientFailure: - () + case .idle: + // The subchannel can be parked before it's shutdown. If there are no active RPCs then + // it will enter the idle state instead. If that happens, close it. + if let parked = self.parkedSubchannels[key] { + return .close(parked) + } else { + return .none + } case .shutdown: self.parkedSubchannels.removeValue(forKey: key) + case .connecting, .ready, .transientFailure: + () } return .none @@ -473,15 +481,38 @@ extension RoundRobinLoadBalancer { var reason: Reason var parkedSubchannels: [EndpointKey: Subchannel] - mutating func updateConnectivityState(_ state: ConnectivityState, key: EndpointKey) -> Bool { + mutating func updateConnectivityState( + _ state: ConnectivityState, + key: EndpointKey + ) -> (OnSubchannelConnectivityStateUpdate, RoundRobinLoadBalancer.State) { + let result: OnSubchannelConnectivityStateUpdate + let nextState: RoundRobinLoadBalancer.State + switch state { - case .idle, .connecting, .ready, .transientFailure: - () + case .idle: + if let parked = self.parkedSubchannels[key] { + result = .close(parked) + } else { + result = .none + } + nextState = .closing(self) + case .shutdown: self.parkedSubchannels.removeValue(forKey: key) + if self.parkedSubchannels.isEmpty { + nextState = .closed + result = .closed + } else { + nextState = .closing(self) + result = .none + } + + case .connecting, .ready, .transientFailure: + result = .none + nextState = .closing(self) } - return self.parkedSubchannels.isEmpty + return (result, nextState) } } @@ -632,13 +663,9 @@ extension RoundRobinLoadBalancer { return result case .closing(var state): - if state.updateConnectivityState(connectivityState, key: key) { - self = .closed - return .closed - } else { - self = .closing(state) - return .none - } + let (result, nextState) = state.updateConnectivityState(connectivityState, key: key) + self = nextState + return result case .closed: return .none diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift index ae46e3c31..22c395b83 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift @@ -329,11 +329,12 @@ extension Subchannel { guard isClosed else { return } switch reason { - case .idleTimeout: - // Connection closed due to an idle timeout; notify the load balancer about this. + case .idleTimeout, .remote, .error(_, wasIdle: true): + // Connection closed due to an idle timeout or the remote telling it to GOAWAY; notify the + // load balancer about this. self.event.continuation.yield(.connectivityStateChanged(.idle)) - case .keepaliveTimeout, .error: + case .keepaliveTimeout, .error(_, wasIdle: false): // Unclean closes trigger a transient failure state change and a name resolution. self.event.continuation.yield(.connectivityStateChanged(.transientFailure)) self.event.continuation.yield(.requiresNameResolution) @@ -341,10 +342,8 @@ extension Subchannel { // Attempt to reconnect. self.handleConnectInput(in: &group) - case .initiatedLocally, .remote: - // Connection closed because the load balancer (or remote peer) asked it to, so notify the - // load balancer. In the case of 'remote' (i.e. a GOAWAY), the load balancer will have - // already reacted to a separate 'goingAway' event. + case .initiatedLocally: + // Connection closed because the load balancer asked it to, so notify the load balancer. self.event.continuation.yield(.connectivityStateChanged(.shutdown)) // At this point there are no more events: close the event streams. @@ -563,9 +562,9 @@ extension Subchannel { switch self { case .connected, .closing: switch reason { - case .idleTimeout, .keepaliveTimeout, .error: + case .idleTimeout, .keepaliveTimeout, .error, .remote: self = .notConnected - case .initiatedLocally, .remote: + case .initiatedLocally: self = .closed } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 1f7a7983f..7054b204a 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -221,6 +221,66 @@ final class ClientConnectionHandlerTests: XCTestCase { try connection.settings([]) XCTAssertNil(try connection.readEvent()) } + + func testReceiveErrorWhenIdle() throws { + let connection = try Connection() + try connection.activate() + + // Write the initial settings. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + + // Write an error and close. + let error = CancellationError() + connection.channel.pipeline.fireErrorCaught(error) + connection.channel.close(mode: .all, promise: nil) + + XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(error, isIdle: true))) + } + + func testReceiveErrorWhenStreamsAreOpen() throws { + let connection = try Connection() + try connection.activate() + + // Write the initial settings. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + + // Open a stream. + connection.streamOpened(1) + + // Write an error and close. + let error = CancellationError() + connection.channel.pipeline.fireErrorCaught(error) + connection.channel.close(mode: .all, promise: nil) + + XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(error, isIdle: false))) + } + + func testUnexpectedCloseWhenIdle() throws { + let connection = try Connection() + try connection.activate() + + // Write the initial settings. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + + connection.channel.close(mode: .all, promise: nil) + XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(nil, isIdle: true))) + } + + func testUnexpectedCloseWhenStreamsAreOpen() throws { + let connection = try Connection() + try connection.activate() + + // Write the initial settings. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + + connection.streamOpened(1) + connection.channel.close(mode: .all, promise: nil) + XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(nil, isIdle: false))) + } } extension ClientConnectionHandlerTests { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift index e493881cb..63e8a45fd 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift @@ -15,8 +15,7 @@ */ import GRPCCore - -@testable import GRPCHTTP2Core +@_spi(Package) @testable import GRPCHTTP2Core @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Connection.Event: Equatable { @@ -48,11 +47,11 @@ extension Connection.CloseReason: Equatable { (.remote, .remote): return true - case (.error(let lhsError), .error(let rhsError)): + case (.error(let lhsError, let lhsStreams), .error(let rhsError, let rhsStreams)): if let lhs = lhsError as? RPCError, let rhs = rhsError as? RPCError { - return lhs == rhs + return lhs == rhs && lhsStreams == rhsStreams } else { - return true + return lhsStreams == rhsStreams } default: @@ -60,3 +59,37 @@ extension Connection.CloseReason: Equatable { } } } + +extension ClientConnectionEvent: Equatable { + public static func == (lhs: ClientConnectionEvent, rhs: ClientConnectionEvent) -> Bool { + switch (lhs, rhs) { + case (.ready, .ready): + return true + case (.closing(let lhsReason), .closing(let rhsReason)): + return lhsReason == rhsReason + default: + return false + } + } +} + +extension ClientConnectionEvent.CloseReason: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case (.goAway(let lhsCode, let lhsMessage), .goAway(let rhsCode, let rhsMessage)): + return lhsCode == rhsCode && lhsMessage == rhsMessage + case (.unexpected(let lhsError, let lhsIsIdle), .unexpected(let rhsError, let rhsIsIdle)): + if let lhs = lhsError as? RPCError, let rhs = rhsError as? RPCError { + return lhs == rhs && lhsIsIdle == rhsIsIdle + } else { + return lhsIsIdle == rhsIsIdle + } + case (.keepaliveExpired, .keepaliveExpired), + (.idle, .idle), + (.initiatedLocally, .initiatedLocally): + return true + default: + return false + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift index 5cddc63d1..550d2a05c 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift @@ -82,6 +82,27 @@ final class ConnectionTests: XCTestCase { } } + func testConnectionDropWhenConnected() async throws { + try await ConnectionTest.run(connector: .posix()) { context, event in + switch event { + case .connectSucceeded: + let accepted = try context.server.acceptedChannel + accepted.close(mode: .all, promise: nil) + + default: + () + } + } validateEvents: { _, events in + let error = RPCError( + code: .unavailable, + message: "The TCP connection was dropped unexpectedly." + ) + + let expected: [Connection.Event] = [.connectSucceeded, .closed(.error(error, wasIdle: true))] + XCTAssertEqual(events, expected) + } + } + func testConnectFails() async throws { let error = RPCError(code: .unimplemented, message: "") try await ConnectionTest.run(connector: .throwing(error)) { _, events in diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 6c48a71de..4ce6e4bd9 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -275,6 +275,177 @@ final class SubchannelTests: XCTestCase { } } + func testIdleTimeout() async throws { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + let subchannel = self.makeSubchannel( + address: address, + connector: .posix(maxIdleTime: .milliseconds(1)) // Aggressively idle + ) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + await subchannel.run() + } + + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected stream") + } + } + + var idleCount = 0 + var events = [Subchannel.Event]() + for await event in subchannel.events { + events.append(event) + switch event { + case .connectivityStateChanged(.idle): + idleCount += 1 + if idleCount == 1 { + subchannel.connect() + } else { + subchannel.close() + } + + case .connectivityStateChanged(.shutdown): + group.cancelAll() + + default: + () + } + } + + let expected: [Subchannel.Event] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.idle), + .connectivityStateChanged(.shutdown), + ] + + XCTAssertEqual(events, expected) + } + } + + func testConnectionDropWhenIdle() async throws { + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + let subchannel = self.makeSubchannel(address: address, connector: .posix()) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + await subchannel.run() + } + + group.addTask { + try await server.run { _, _ in + XCTFail("Unexpected RPC") + } + } + + var events = [Subchannel.Event]() + var idleCount = 0 + + for await event in subchannel.events { + events.append(event) + + switch event { + case .connectivityStateChanged(.idle): + idleCount += 1 + switch idleCount { + case 1: + subchannel.connect() + case 2: + subchannel.close() + default: + XCTFail("Unexpected idle") + } + + case .connectivityStateChanged(.ready): + // Close the connection without a GOAWAY. + server.clients.first?.close(mode: .all, promise: nil) + + case .connectivityStateChanged(.shutdown): + group.cancelAll() + + default: + () + } + } + + let expected: [Subchannel.Event] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.idle), + .connectivityStateChanged(.shutdown), + ] + + XCTAssertEqual(events, expected) + } + } + + func testConnectionDropWithOpenStreams() async throws { + try XCTSkipIf(true, "HTTP/2 stream delegate API isn't currently exposed") + + let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address = try await server.bind() + let subchannel = self.makeSubchannel(address: address, connector: .posix()) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + await subchannel.run() + } + + group.addTask { + try await server.run { inbound, outbound in + // Sleep, the RPC will be cancelled at the end of the test because the connection + // will be purposefully dropped. + try await Task.sleep(for: .seconds(300)) + } + } + + var events = [Subchannel.Event]() + for await event in subchannel.events { + events.append(event) + switch event { + case .connectivityStateChanged(.idle): + subchannel.connect() + + case .connectivityStateChanged(.ready): + let stream = try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) + try await stream.execute { inbound, outbound in + try await outbound.write(.metadata([:])) + // Stream is definitely open. Bork the connection. + server.clients.first?.close(mode: .all, promise: nil) + for try await _ in inbound { + () + } + } + + case .connectivityStateChanged(.transientFailure): + subchannel.close() + + case .connectivityStateChanged(.shutdown): + group.cancelAll() + + default: + () + } + } + + let expected: [Subchannel.Event] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.transientFailure), + .connectivityStateChanged(.shutdown), + ] + + XCTAssertEqual(events, expected) + } + } + func testConnectedReceivesGoAway() async throws { let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) let address = try await server.bind() @@ -293,12 +464,18 @@ final class SubchannelTests: XCTestCase { var events = [Subchannel.Event]() + var idleCount = 0 for await event in subchannel.events { events.append(event) switch event { case .connectivityStateChanged(.idle): - subchannel.connect() + idleCount += 1 + if idleCount == 1 { + subchannel.connect() + } else if idleCount == 2 { + subchannel.close() + } case .connectivityStateChanged(.ready): // Now the subchannel is ready, send a GOAWAY from the server. @@ -322,10 +499,11 @@ final class SubchannelTests: XCTestCase { .connectivityStateChanged(.idle), .connectivityStateChanged(.connecting), .connectivityStateChanged(.ready), - // GOAWAY triggers name resolution too. + // GOAWAY triggers name resolution and idling. .goingAway, .requiresNameResolution, - // Finally, shutdown. + .connectivityStateChanged(.idle), + // The second idle triggers a close. .connectivityStateChanged(.shutdown), ] From 7875e56cd68c596e83d431acd4710ff95e9538a7 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 4 Jun 2024 15:29:36 +0100 Subject: [PATCH 331/580] Add request compression check to interop's TestService (#1903) --- .../InteroperabilityTests/TestService.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift index 5472f23d1..da8c8567b 100644 --- a/Sources/InteroperabilityTests/TestService.swift +++ b/Sources/InteroperabilityTests/TestService.swift @@ -51,6 +51,18 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { public func unaryCall( request: ServerRequest.Single ) async throws -> ServerResponse.Single { + // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is + // set), so we have to check via the encoding header. Note that it is possible for the header + // to be set and for the message to not be compressed. + let isRequestCompressed = + request.metadata["grpc-encoding"].filter({ $0 != "identity" }).count > 0 + if request.message.expectCompressed.value, !isRequestCompressed { + throw RPCError( + code: .invalidArgument, + message: "Expected compressed request, but 'grpc-encoding' was missing" + ) + } + // If the request has a responseStatus set, the server should return that status. // If the code is an error code, the server will throw an error containing that code // and the message set in the responseStatus. @@ -139,9 +151,21 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { ) async throws -> ServerResponse.Single { + let isRequestCompressed = + request.metadata["grpc-encoding"].filter({ $0 != "identity" }).count > 0 var aggregatedPayloadSize = 0 for try await message in request.messages { + // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is + // set), so we have to check via the encoding header. Note that it is possible for the header + // to be set and for the message to not be compressed. + if message.expectCompressed.value, !isRequestCompressed { + throw RPCError( + code: .invalidArgument, + message: "Expected compressed request, but 'grpc-encoding' was missing" + ) + } + aggregatedPayloadSize += message.payload.body.count } From cfdf2251d3a7ee103d6417dadcb5732c78e59e56 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 4 Jun 2024 16:02:21 +0100 Subject: [PATCH 332/580] Update to a newer version of NIO HTTP/2 (#1904) Motivation: swift-nio-http2 1.32.0 contains new API we need for configuring an async http/2 pipeline with a stream delegate. Modifications: - Bump the minimum version - Wire through the stream delegate - Fix a test bug where the server didn't send the final status Result: Stream delegates are wired up --- Package.swift | 2 +- .../Connection/ClientConnectionHandler.swift | 4 ++-- .../Internal/NIOChannelPipeline+GRPC.swift | 18 +++++++++------- .../Client/Connection/GRPCChannelTests.swift | 2 -- .../LoadBalancers/SubchannelTests.swift | 4 ++-- .../Utilities/HTTP2Connectors.swift | 21 +++++++++++-------- .../Connection/Utilities/TestServer.swift | 1 + 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Package.swift b/Package.swift index c29d908fd..44182b025 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-nio-http2.git", - from: "1.31.0" + from: "1.32.0" ), .package( url: "https://github.com/apple/swift-nio-transport-services.git", diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 1c4d21a6f..7e823c762 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -247,8 +247,8 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl switch self.state.beginGracefulShutdown(promise: promise) { case .sendGoAway(let close): context.fireChannelRead(self.wrapInboundOut(.closing(.initiatedLocally))) - // Clients should send GOAWAYs when closing a connection. - self.writeAndFlushGoAway(context: context, errorCode: .noError) + // The client could send a GOAWAY at this point but it's not really necessary, the server + // can't open streams anyway, the client will just close the connection when it's done. if close { context.close(promise: nil) } diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index 85b5fb5be..05eb71129 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -69,6 +69,7 @@ extension ChannelPipeline.SynchronousOperations { let streamMultiplexer = try self.configureAsyncHTTP2Pipeline( mode: .server, + streamDelegate: serverConnectionHandler.http2StreamDelegate, configuration: NIOHTTP2Handler.Configuration( connection: http2HandlerConnectionConfiguration, stream: http2HandlerStreamConfiguration @@ -137,8 +138,17 @@ extension ChannelPipeline.SynchronousOperations { HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize), ] + let connectionHandler = ClientConnectionHandler( + eventLoop: self.eventLoop, + maxIdleTime: config.idle.map { TimeAmount($0.maxTime) }, + keepaliveTime: config.keepalive.map { TimeAmount($0.time) }, + keepaliveTimeout: config.keepalive.map { TimeAmount($0.timeout) }, + keepaliveWithoutCalls: config.keepalive?.permitWithoutCalls ?? false + ) + let multiplexer = try self.configureAsyncHTTP2Pipeline( mode: .client, + streamDelegate: connectionHandler.http2StreamDelegate, configuration: http2 ) { stream in // Shouldn't happen, push-promises are disabled so the server shouldn't be able to @@ -146,14 +156,6 @@ extension ChannelPipeline.SynchronousOperations { stream.close() } - let connectionHandler = ClientConnectionHandler( - eventLoop: self.eventLoop, - maxIdleTime: config.idle.map { TimeAmount($0.maxTime) }, - keepaliveTime: config.keepalive.map { TimeAmount($0.time) }, - keepaliveTimeout: config.keepalive.map { TimeAmount($0.timeout) }, - keepaliveWithoutCalls: config.keepalive?.permitWithoutCalls ?? false - ) - try self.addHandler(connectionHandler) let connection = try NIOAsyncChannel( diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index 5fde16d06..842b686c5 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -329,8 +329,6 @@ final class GRPCChannelTests: XCTestCase { } func testCloseWhenRPCsAreInProgress() async throws { - try XCTSkipIf(true, "https://github.com/apple/swift-nio-http2/pull/439") - // Verify that closing the channel while there are RPCs in progress allows the RPCs to finish // gracefully. diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 4ce6e4bd9..93b8e0f2b 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -386,8 +386,6 @@ final class SubchannelTests: XCTestCase { } func testConnectionDropWithOpenStreams() async throws { - try XCTSkipIf(true, "HTTP/2 stream delegate API isn't currently exposed") - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) let address = try await server.bind() let subchannel = self.makeSubchannel(address: address, connector: .posix()) @@ -439,6 +437,8 @@ final class SubchannelTests: XCTestCase { .connectivityStateChanged(.connecting), .connectivityStateChanged(.ready), .connectivityStateChanged(.transientFailure), + .requiresNameResolution, + .connectivityStateChanged(.connecting), .connectivityStateChanged(.shutdown), ] diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift index 0357fd428..11782f5e6 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift @@ -115,15 +115,6 @@ struct NIOPosixConnector: HTTP2Connector { channel.eventLoop.makeCompletedFuture { let sync = channel.pipeline.syncOperations - let multiplexer = try sync.configureAsyncHTTP2Pipeline(mode: .client) { stream in - // Server shouldn't be opening streams. - stream.close() - } - - if self.dropPingAcks { - try sync.addHandler(PingAckDropper()) - } - let connectionHandler = ClientConnectionHandler( eventLoop: channel.eventLoop, maxIdleTime: self.maxIdleTime, @@ -132,6 +123,18 @@ struct NIOPosixConnector: HTTP2Connector { keepaliveWithoutCalls: self.keepaliveWithoutCalls ) + let multiplexer = try sync.configureAsyncHTTP2Pipeline( + mode: .client, + streamDelegate: connectionHandler.http2StreamDelegate + ) { stream in + // Server shouldn't be opening streams. + stream.close() + } + + if self.dropPingAcks { + try sync.addHandler(PingAckDropper()) + } + try sync.addHandler(connectionHandler) let asyncChannel = try NIOAsyncChannel( diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift index bb678a108..488165162 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -164,6 +164,7 @@ extension TestServer { try await outbound.write(.message(bytes)) } } + try await outbound.write(.status(Status(code: .ok, message: ""), [:])) } case .never: From 427984137a6b65b48299523a8dcbb2c33a169e0f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 5 Jun 2024 08:16:19 +0100 Subject: [PATCH 333/580] Use different test error (#1905) Motivation: Some tests arbitrarily used a CancellationError. This requires availability guards on the tests which were missing. Modifications: - Switch to a different error type which doesn't require availability Result: Tests build on more platforms --- .../Client/Connection/ClientConnectionHandlerTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 7054b204a..827ea71ec 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +import GRPCCore @_spi(Package) @testable import GRPCHTTP2Core import NIOCore import NIOEmbedded @@ -231,7 +232,7 @@ final class ClientConnectionHandlerTests: XCTestCase { XCTAssertEqual(try connection.readEvent(), .ready) // Write an error and close. - let error = CancellationError() + let error = RPCError(code: .aborted, message: "") connection.channel.pipeline.fireErrorCaught(error) connection.channel.close(mode: .all, promise: nil) @@ -250,7 +251,7 @@ final class ClientConnectionHandlerTests: XCTestCase { connection.streamOpened(1) // Write an error and close. - let error = CancellationError() + let error = RPCError(code: .aborted, message: "") connection.channel.pipeline.fireErrorCaught(error) connection.channel.close(mode: .all, promise: nil) From a13b94e15df2848fe707d10501d445157b8317af Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 5 Jun 2024 12:54:13 +0100 Subject: [PATCH 334/580] Normalise config (#1906) Motivation: In 3272c32b the config for max idle time got moved into the connection config for the server. As a follow up, the client should do the same. While doing this it became more obvious that keepalive should also be folded in to the connection config, as both relate to managing existing connections. Moreover the server keepalive config can be a little confusing because it includes config the server should use to keep the connection alive and config the server should use to police how the client is keeping the connection alive. Modifications: - Add a 'connection' config for the client and move the max idle time to it. - Move the 'keepalive' config for client and server into their respective 'connection' configs - Add a separate server keepalive config for policing the client keepalive and nest it in the server keepalive config Result: Easier to understand config --- .../Client/Connection/GRPCChannel.swift | 13 ++-- .../Client/HTTP2ClientTransport.swift | 34 +++++++---- .../Internal/NIOChannelPipeline+GRPC.swift | 19 +++--- .../Server/HTTP2ServerTransport.swift | 60 ++++++++++++++----- .../GRPCHTTP2TransportNIOPosix.swift | 7 --- .../Client/Connection/GRPCChannelTests.swift | 3 +- .../HTTP2ClientTransportConfigTests.swift | 7 ++- .../HTTP2ServerTransportConfigTests.swift | 4 +- 8 files changed, 89 insertions(+), 58 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 38e234f6c..5053bdb26 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -237,11 +237,8 @@ extension GRPCChannel { /// Configuration for backoff used when establishing a connection. var backoff: HTTP2ClientTransport.Config.Backoff - /// Configuration for dealing with idle connections. - var idle: HTTP2ClientTransport.Config.Idle? - - /// Configuration for keepalive. - var keepalive: HTTP2ClientTransport.Config.Keepalive? + /// Configuration for connection management. + var connection: HTTP2ClientTransport.Config.Connection /// Compression configuration. var compression: HTTP2ClientTransport.Config.Compression @@ -250,14 +247,12 @@ extension GRPCChannel { public init( http2: HTTP2ClientTransport.Config.HTTP2, backoff: HTTP2ClientTransport.Config.Backoff, - idle: HTTP2ClientTransport.Config.Idle?, - keepalive: HTTP2ClientTransport.Config.Keepalive?, + connection: HTTP2ClientTransport.Config.Connection, compression: HTTP2ClientTransport.Config.Compression ) { self.http2 = http2 self.backoff = backoff - self.idle = idle - self.keepalive = keepalive + self.connection = connection self.compression = compression } } diff --git a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift index 1350b5399..c2440689a 100644 --- a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift +++ b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift @@ -62,29 +62,43 @@ extension HTTP2ClientTransport.Config { public var timeout: Duration /// Whether the client sends keepalive pings when there are no calls in progress. - public var permitWithoutCalls: Bool + public var allowWithoutCalls: Bool /// Creates a new keepalive configuration. - public init(time: Duration, timeout: Duration, permitWithoutCalls: Bool) { + public init(time: Duration, timeout: Duration, allowWithoutCalls: Bool) { self.time = time self.timeout = timeout - self.permitWithoutCalls = permitWithoutCalls + self.allowWithoutCalls = allowWithoutCalls } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Idle: Sendable { + public struct Connection: Sendable { /// The maximum amount of time a connection may be idle before it's closed. - public var maxTime: Duration + /// + /// Connections are considered idle when there are no open streams on them. Idle connections + /// can be closed after a configured amount of time to free resources. Note that servers may + /// separately monitor and close idle connections. + public var maxIdleTime: Duration? + + /// Configuration for keepalive. + /// + /// Keepalive is typically applied to connection which have open streams. It can be useful to + /// detect dropped connections, particularly if the streams running on a connection don't have + /// much activity. + /// + /// See also: gRFC A8: Client-side Keepalive. + public var keepalive: Keepalive? - /// Creates an idle configuration. - public init(maxTime: Duration) { - self.maxTime = maxTime + /// Creates a connection configuration. + public init(maxIdleTime: Duration, keepalive: Keepalive?) { + self.maxIdleTime = maxIdleTime + self.keepalive = keepalive } - /// Default values, a 30 minute max idle time. + /// Default values, a 30 minute max idle time and no keepalive. public static var defaults: Self { - Self(maxTime: .seconds(30 * 60)) + Self(maxIdleTime: .seconds(30 * 60), keepalive: nil) } } diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index 05eb71129..bd71a0f36 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -30,7 +30,6 @@ extension ChannelPipeline.SynchronousOperations { public func configureGRPCServerPipeline( channel: any Channel, compressionConfig: HTTP2ServerTransport.Config.Compression, - keepaliveConfig: HTTP2ServerTransport.Config.Keepalive, connectionConfig: HTTP2ServerTransport.Config.Connection, http2Config: HTTP2ServerTransport.Config.HTTP2, rpcConfig: HTTP2ServerTransport.Config.RPC, @@ -41,10 +40,12 @@ extension ChannelPipeline.SynchronousOperations { maxIdleTime: connectionConfig.maxIdleTime.map { TimeAmount($0) }, maxAge: connectionConfig.maxAge.map { TimeAmount($0) }, maxGraceTime: connectionConfig.maxGraceTime.map { TimeAmount($0) }, - keepaliveTime: TimeAmount(keepaliveConfig.time), - keepaliveTimeout: TimeAmount(keepaliveConfig.timeout), - allowKeepaliveWithoutCalls: keepaliveConfig.permitWithoutCalls, - minPingIntervalWithoutCalls: TimeAmount(keepaliveConfig.minPingIntervalWithoutCalls) + keepaliveTime: TimeAmount(connectionConfig.keepalive.time), + keepaliveTimeout: TimeAmount(connectionConfig.keepalive.timeout), + allowKeepaliveWithoutCalls: connectionConfig.keepalive.clientBehavior.allowWithoutCalls, + minPingIntervalWithoutCalls: TimeAmount( + connectionConfig.keepalive.clientBehavior.minPingIntervalWithoutCalls + ) ) let flushNotificationHandler = GRPCServerFlushNotificationHandler( serverConnectionManagementHandler: serverConnectionHandler @@ -140,10 +141,10 @@ extension ChannelPipeline.SynchronousOperations { let connectionHandler = ClientConnectionHandler( eventLoop: self.eventLoop, - maxIdleTime: config.idle.map { TimeAmount($0.maxTime) }, - keepaliveTime: config.keepalive.map { TimeAmount($0.time) }, - keepaliveTimeout: config.keepalive.map { TimeAmount($0.timeout) }, - keepaliveWithoutCalls: config.keepalive?.permitWithoutCalls ?? false + maxIdleTime: config.connection.maxIdleTime.map { TimeAmount($0) }, + keepaliveTime: config.connection.keepalive.map { TimeAmount($0.time) }, + keepaliveTimeout: config.connection.keepalive.map { TimeAmount($0.timeout) }, + keepaliveWithoutCalls: config.connection.keepalive?.allowWithoutCalls ?? false ) let multiplexer = try self.configureAsyncHTTP2Pipeline( diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift index 0e6d8ffdb..ffeb21511 100644 --- a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift +++ b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift @@ -53,25 +53,18 @@ extension HTTP2ServerTransport.Config { /// The amount of time the server has to respond to a keepalive ping before the connection is closed. public var timeout: Duration - /// Whether the server allows the client to send keepalive pings when there are no calls in progress. - public var permitWithoutCalls: Bool - - /// The minimum allowed interval the client is allowed to send keep-alive pings. - /// Pings more frequent than this interval count as 'strikes' and the connection is closed if there are - /// too many strikes. - public var minPingIntervalWithoutCalls: Duration + /// Configuration for how the server enforces client keepalive. + public var clientBehavior: ClientKeepaliveBehavior /// Creates a new keepalive configuration. public init( time: Duration, timeout: Duration, - permitWithoutCalls: Bool, - minPingIntervalWithoutCalls: Duration + clientBehavior: ClientKeepaliveBehavior ) { self.time = time self.timeout = timeout - self.permitWithoutCalls = permitWithoutCalls - self.minPingIntervalWithoutCalls = minPingIntervalWithoutCalls + self.clientBehavior = clientBehavior } /// Default values. The time after reading data a ping should be sent defaults to 2 hours, the timeout for @@ -81,12 +74,38 @@ extension HTTP2ServerTransport.Config { Self( time: .seconds(2 * 60 * 60), // 2 hours timeout: .seconds(20), - permitWithoutCalls: false, - minPingIntervalWithoutCalls: .seconds(5 * 60) // 5 minutes + clientBehavior: .defaults ) } } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + public struct ClientKeepaliveBehavior: Sendable { + /// The minimum allowed interval the client is allowed to send keep-alive pings. + /// Pings more frequent than this interval count as 'strikes' and the connection is closed if there are + /// too many strikes. + public var minPingIntervalWithoutCalls: Duration + + /// Whether the server allows the client to send keepalive pings when there are no calls in progress. + public var allowWithoutCalls: Bool + + /// Creates a new configuration for permitted client keepalive behavior. + public init( + minPingIntervalWithoutCalls: Duration, + allowWithoutCalls: Bool + ) { + self.minPingIntervalWithoutCalls = minPingIntervalWithoutCalls + self.allowWithoutCalls = allowWithoutCalls + } + + /// Default values. The time after reading data a ping should be sent defaults to 2 hours, the timeout for + /// keepalive pings defaults to 20 seconds, pings are not permitted when no calls are in progress, and + /// the minimum allowed interval for clients to send pings defaults to 5 minutes. + public static var defaults: Self { + Self(minPingIntervalWithoutCalls: .seconds(5 * 60), allowWithoutCalls: false) + } + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct Connection: Sendable { /// The maximum amount of time a connection may exist before being gracefully closed. @@ -98,19 +117,28 @@ extension HTTP2ServerTransport.Config { /// The maximum amount of time a connection may be idle before it's closed. public var maxIdleTime: Duration? + /// Configuration for keepalive used to detect broken connections. + /// + /// - SeeAlso: gRFC A8 for client side keepalive, and gRFC A9 for server connection management. + public var keepalive: Keepalive + public init( maxAge: Duration?, maxGraceTime: Duration?, - maxIdleTime: Duration? + maxIdleTime: Duration?, + keepalive: Keepalive ) { self.maxAge = maxAge self.maxGraceTime = maxGraceTime self.maxIdleTime = maxIdleTime + self.keepalive = keepalive } - /// Default values. All the max connection age, max grace time, and max idle time default to infinite. + /// Default values. The max connection age, max grace time, and max idle time default to + /// `nil` (i.e. infinite). See ``HTTP2ServerTransport/Config/Keepalive/defaults`` for keepalive + /// defaults. public static var defaults: Self { - Self(maxAge: nil, maxGraceTime: nil, maxIdleTime: nil) + Self(maxAge: nil, maxGraceTime: nil, maxIdleTime: nil, keepalive: .defaults) } } diff --git a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift index 32d5c49c7..0325728f4 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift @@ -54,7 +54,6 @@ extension HTTP2ServerTransport { return try channel.pipeline.syncOperations.configureGRPCServerPipeline( channel: channel, compressionConfig: self.config.compression, - keepaliveConfig: self.config.keepalive, connectionConfig: self.config.connection, http2Config: self.config.http2, rpcConfig: self.config.rpc, @@ -135,8 +134,6 @@ extension HTTP2ServerTransport.Posix { public struct Config: Sendable { /// Compression configuration. public var compression: HTTP2ServerTransport.Config.Compression - /// Keepalive configuration. - public var keepalive: HTTP2ServerTransport.Config.Keepalive /// Connection configuration. public var connection: HTTP2ServerTransport.Config.Connection /// HTTP2 configuration. @@ -147,19 +144,16 @@ extension HTTP2ServerTransport.Posix { /// Construct a new `Config`. /// - Parameters: /// - compression: Compression configuration. - /// - keepalive: Keepalive configuration. /// - connection: Connection configuration. /// - http2: HTTP2 configuration. /// - rpc: RPC configuration. public init( compression: HTTP2ServerTransport.Config.Compression, - keepalive: HTTP2ServerTransport.Config.Keepalive, connection: HTTP2ServerTransport.Config.Connection, http2: HTTP2ServerTransport.Config.HTTP2, rpc: HTTP2ServerTransport.Config.RPC ) { self.compression = compression - self.keepalive = keepalive self.connection = connection self.http2 = http2 self.rpc = rpc @@ -169,7 +163,6 @@ extension HTTP2ServerTransport.Posix { public static var defaults: Self { Self( compression: .defaults, - keepalive: .defaults, connection: .defaults, http2: .defaults, rpc: .defaults diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index 842b686c5..eb940d86b 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -555,8 +555,7 @@ extension GRPCChannel.Config { Self( http2: .defaults, backoff: .defaults, - idle: .defaults, - keepalive: nil, + connection: .defaults, compression: .defaults ) } diff --git a/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift b/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift index b376f8fcd..8b5857008 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift @@ -25,9 +25,10 @@ final class HTTP2ClientTransportConfigTests: XCTestCase { XCTAssertEqual(config.enabledAlgorithms, .none) } - func testIdleDefaults() { - let config = HTTP2ClientTransport.Config.Idle.defaults - XCTAssertEqual(config.maxTime, .seconds(30 * 60)) + func testConnectionDefaults() { + let config = HTTP2ClientTransport.Config.Connection.defaults + XCTAssertEqual(config.maxIdleTime, .seconds(30 * 60)) + XCTAssertNil(config.keepalive) } func testBackoffDefaults() { diff --git a/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift b/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift index f57a1373b..d7e1c06b8 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift @@ -28,8 +28,8 @@ final class HTTP2ServerTransportConfigTests: XCTestCase { let config = HTTP2ServerTransport.Config.Keepalive.defaults XCTAssertEqual(config.time, .seconds(7200)) XCTAssertEqual(config.timeout, .seconds(20)) - XCTAssertEqual(config.permitWithoutCalls, false) - XCTAssertEqual(config.minPingIntervalWithoutCalls, .seconds(300)) + XCTAssertEqual(config.clientBehavior.allowWithoutCalls, false) + XCTAssertEqual(config.clientBehavior.minPingIntervalWithoutCalls, .seconds(300)) } func testConnectionDefaults() { From 87b18f8460311a9223959d1953b68e5a5b57a303 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 7 Jun 2024 09:34:29 +0100 Subject: [PATCH 335/580] ":path" should have a leading "/" (#1910) Motivation: The value of the ":path" header field should be absolute. Currently the fully qualified method descriptor is used which doesn't contain a leading slash. Modifications: - Add a leading slash - Rename paramet - Update tests Result: ":path" is correct --- .../GRPCStreamStateMachine.swift | 14 +++++-- .../Client/GRPCClientStreamHandlerTests.swift | 14 +++---- .../GRPCStreamStateMachineTests.swift | 38 +++++++++---------- .../Server/GRPCServerStreamHandlerTests.swift | 4 +- 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 6f5fad990..5f166d9f2 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -542,7 +542,7 @@ extension GRPCStreamStateMachine { // must come before all other headers. headers.add("POST", forKey: .method) headers.add(scheme.rawValue, forKey: .scheme) - headers.add(methodDescriptor.fullyQualifiedMethod, forKey: .path) + headers.add(methodDescriptor.path, forKey: .path) // Add required gRPC headers. headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) @@ -1243,7 +1243,7 @@ extension GRPCStreamStateMachine { ) } - guard let path = MethodDescriptor(fullyQualifiedMethod: pathHeader) else { + guard let path = MethodDescriptor(path: pathHeader) else { return self.closeServerAndBuildRejectRPCAction( currentState: state, endStream: endStream, @@ -1507,8 +1507,8 @@ extension GRPCStreamStateMachine { } extension MethodDescriptor { - init?(fullyQualifiedMethod: String) { - let split = fullyQualifiedMethod.split(separator: "/") + init?(path: String) { + let split = path.split(separator: "/") guard split.count == 2 else { return nil } @@ -1596,3 +1596,9 @@ extension Status.Code { } } } + +extension MethodDescriptor { + var path: String { + return "/\(self.service)/\(self.method)" + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift index b8ea4f59f..5357bc8aa 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift @@ -220,7 +220,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { [ GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", GRPCHTTP2Keys.encoding.rawValue: "deflate", @@ -279,7 +279,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { [ GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", ] @@ -349,7 +349,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { [ GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", ] @@ -407,7 +407,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { // Write client's initial metadata XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", @@ -481,7 +481,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { [ GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", @@ -599,7 +599,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { [ GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", @@ -704,7 +704,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { [ GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index a423f701b..502c24942 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -35,14 +35,14 @@ private enum TargetStateMachineState: CaseIterable { extension HPACKHeaders { // Client fileprivate static let clientInitialMetadata: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", ] fileprivate static let clientInitialMetadataWithDeflateCompression: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "https", @@ -51,7 +51,7 @@ extension HPACKHeaders { GRPCHTTP2Keys.encoding.rawValue: "deflate", ] fileprivate static let clientInitialMetadataWithGzipCompression: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.scheme.rawValue: "https", @@ -60,10 +60,10 @@ extension HPACKHeaders { GRPCHTTP2Keys.encoding.rawValue: "gzip", ] fileprivate static let receivedWithoutContentType: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test" + GRPCHTTP2Keys.path.rawValue: "/test/test" ] fileprivate static let receivedWithInvalidContentType: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid", ] fileprivate static let receivedWithInvalidPath: Self = [ @@ -74,39 +74,39 @@ extension HPACKHeaders { GRPCHTTP2Keys.contentType.rawValue: "application/grpc" ] fileprivate static let receivedWithoutTE: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", ] fileprivate static let receivedWithInvalidTE: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "invalidte", ] fileprivate static let receivedWithoutMethod: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", ] fileprivate static let receivedWithInvalidMethod: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "GET", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", ] fileprivate static let receivedWithoutScheme: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", GRPCHTTP2Keys.te.rawValue: "trailers", ] fileprivate static let receivedWithInvalidScheme: Self = [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "invalidscheme", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", @@ -986,7 +986,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual( clientInitialMetadata, [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", @@ -1075,7 +1075,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual( clientInitialMetadata, [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", @@ -1162,7 +1162,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual( clientInitialMetadata, [ - GRPCHTTP2Keys.path.rawValue: "test/test", + GRPCHTTP2Keys.path.rawValue: "/test/test", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", @@ -1666,7 +1666,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { action, .receivedMetadata( Metadata(headers: .clientInitialMetadata), - MethodDescriptor(fullyQualifiedMethod: "test/test") + MethodDescriptor(path: "/test/test") ) ) } @@ -1679,7 +1679,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { action, .receivedMetadata( Metadata(headers: .clientInitialMetadata), - MethodDescriptor(fullyQualifiedMethod: "test/test") + MethodDescriptor(path: "/test/test") ) ) } @@ -2447,7 +2447,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { receiveMetadataAction, .receivedMetadata( Metadata(headers: .clientInitialMetadata), - MethodDescriptor(fullyQualifiedMethod: "test/test") + MethodDescriptor(path: "/test/test") ) ) @@ -2543,7 +2543,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { receiveMetadataAction, .receivedMetadata( Metadata(headers: .clientInitialMetadata), - MethodDescriptor(fullyQualifiedMethod: "test/test") + MethodDescriptor(path: "/test/test") ) ) @@ -2622,7 +2622,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { receiveMetadataAction, .receivedMetadata( Metadata(headers: .clientInitialMetadata), - MethodDescriptor(fullyQualifiedMethod: "test/test") + MethodDescriptor(path: "/test/test") ) ) diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index ef7162f14..6c8a0ba75 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -798,7 +798,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { // Receive client's initial metadata let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "SomeService/SomeMethod", + GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", GRPCHTTP2Keys.scheme.rawValue: "http", GRPCHTTP2Keys.method.rawValue: "POST", GRPCHTTP2Keys.contentType.rawValue: "application/grpc", @@ -819,7 +819,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { XCTAssertEqual( try promise.futureResult.wait(), - MethodDescriptor(fullyQualifiedMethod: "SomeService/SomeMethod") + MethodDescriptor(path: "/SomeService/SomeMethod") ) } From 1ac484d73fd9bdc8920b4e0aa56ed0c10346e3ed Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 7 Jun 2024 10:53:03 +0100 Subject: [PATCH 336/580] Better handle errors on client connections (#1908) Motivation: At the moment if an error is encountered on an active client connection an error is thrown from the async channel. This results in the `Connection` being closed and, without further information, the subchannel is returned to the idle state. This can lead to some tests being flaky. Rather than throwing the error down the pipeline, we should store it and close the connection so that the client connection handler can fire an approriate close reason (with the error) down the pipeline before becoming inactive. This allows the `Connection` to inform the `Subchannel` whether it should attempt to reconnect or remain idle. Modifications: - Call close when receiving an error in `ClientConnectionHandler` rather than forwarding it - Make `testConnectionDropWithOpenStreams` more reliable Result: More consistent behavior --- .../Connection/ClientConnectionHandler.swift | 5 +++- .../LoadBalancers/SubchannelTests.swift | 28 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 7e823c762..5fce9c72f 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -173,8 +173,11 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } func errorCaught(context: ChannelHandlerContext, error: any Error) { + // Store the error and close, this will result in the final close event being fired down + // the pipeline with an appropriate close reason and appropriate error. (This avoids + // the async channel just throwing the error.) self.state.receivedError(error) - context.fireErrorCaught(error) + context.close(mode: .all, promise: nil) } func channelRead(context: ChannelHandlerContext, data: NIOAny) { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 93b8e0f2b..68c4d7d06 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -404,6 +404,8 @@ final class SubchannelTests: XCTestCase { } var events = [Subchannel.Event]() + var readyCount = 0 + for await event in subchannel.events { events.append(event) switch event { @@ -411,19 +413,24 @@ final class SubchannelTests: XCTestCase { subchannel.connect() case .connectivityStateChanged(.ready): - let stream = try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) - try await stream.execute { inbound, outbound in - try await outbound.write(.metadata([:])) - // Stream is definitely open. Bork the connection. - server.clients.first?.close(mode: .all, promise: nil) - for try await _ in inbound { - () + readyCount += 1 + // When the connection becomes ready the first time, open a stream and forcibly close the + // channel. This will result in an automatic reconnect. Close the subchannel when that + // happens. + if readyCount == 1 { + let stream = try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) + try await stream.execute { inbound, outbound in + try await outbound.write(.metadata([:])) + // Stream is definitely open. Bork the connection. + server.clients.first?.close(mode: .all, promise: nil) + for try await _ in inbound { + () + } } + } else if readyCount == 2 { + subchannel.close() } - case .connectivityStateChanged(.transientFailure): - subchannel.close() - case .connectivityStateChanged(.shutdown): group.cancelAll() @@ -439,6 +446,7 @@ final class SubchannelTests: XCTestCase { .connectivityStateChanged(.transientFailure), .requiresNameResolution, .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), .connectivityStateChanged(.shutdown), ] From bf5d9205621a4e8bb63fabfb1023f1dc06a3ba28 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Fri, 7 Jun 2024 13:09:23 +0100 Subject: [PATCH 337/580] Pass `CallOptions` through generated stub (#1911) Motivation: `CallOptions` is passed through the RPC methods in `GRPCClient` and set to the default if none (`CallOptions`) is provided. In order to make `CallOptions` easier to access, it will be passed through the RPC methods in the generated stub. Modifications: - Unset the default `CallOptions` for the RPC methods in `GRPCClient`. - Add a new `options: CallOptions` parameter to the RPC methods in the generated stub, with a default value of `CallOptions.defaults`. Result: `CallOptions` will be passed through the generated stub rather than through `GRPCClient`. --- .../Translator/ClientCodeTranslator.swift | 42 ++++++++++---- Sources/GRPCCore/GRPCClient.swift | 8 +-- .../Generated/test.grpc.swift | 55 +++++++++++++++++++ .../grpc_testing_benchmark_service.grpc.swift | 25 +++++++++ .../Generated/grpc_testing_control.pb.swift | 30 +++++++++- ...lientCodeTranslatorSnippetBasedTests.swift | 35 ++++++++++++ .../Generated/service_config.pb.swift | 20 ++++++- Tests/GRPCCoreTests/GRPCClientTests.swift | 48 ++++++++++------ .../ProtobufCodeGeneratorTests.swift | 10 ++++ 9 files changed, 236 insertions(+), 37 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 2a9585bbd..77cb6d639 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -111,7 +111,8 @@ extension ClientCodeTranslator { for: $0, in: service, from: codeGenerationRequest, - generateSerializerDeserializer: false + includeBody: false, + includeDefaultCallOptions: false ) } @@ -135,8 +136,9 @@ extension ClientCodeTranslator { for: $0, in: service, from: codeGenerationRequest, - generateSerializerDeserializer: true, - accessModifier: self.accessModifier + includeBody: true, + accessModifier: self.accessModifier, + includeDefaultCallOptions: true ) } let clientProtocolExtension = Declaration.extension( @@ -155,14 +157,18 @@ extension ClientCodeTranslator { for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest, - generateSerializerDeserializer: Bool, - accessModifier: AccessModifier? = nil + includeBody: Bool, + accessModifier: AccessModifier? = nil, + includeDefaultCallOptions: Bool ) -> Declaration { + let isProtocolExtension = includeBody let methodParameters = self.makeParameters( for: method, in: service, from: codeGenerationRequest, - generateSerializerDeserializer: generateSerializerDeserializer + // The serializer/deserializer for the protocol extension method will be auto-generated. + includeSerializationParameters: !isProtocolExtension, + includeDefaultCallOptions: includeDefaultCallOptions ) let functionSignature = FunctionSignatureDescription( accessModifier: accessModifier, @@ -177,8 +183,8 @@ extension ClientCodeTranslator { whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]) ) - if generateSerializerDeserializer { - let body = self.makeSerializerDeserializerCall( + if includeBody { + let body = self.makeClientProtocolMethodCall( for: method, in: service, from: codeGenerationRequest @@ -192,7 +198,7 @@ extension ClientCodeTranslator { } } - private func makeSerializerDeserializerCall( + private func makeClientProtocolMethodCall( for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest @@ -222,6 +228,7 @@ extension ClientCodeTranslator { ) ) ), + FunctionArgumentDescription(label: "options", expression: .identifierPattern("options")), FunctionArgumentDescription(expression: .identifierPattern("body")), ] ) @@ -235,15 +242,24 @@ extension ClientCodeTranslator { for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest, - generateSerializerDeserializer: Bool + includeSerializationParameters: Bool, + includeDefaultCallOptions: Bool ) -> [ParameterDescription] { var parameters = [ParameterDescription]() parameters.append(self.clientRequestParameter(for: method, in: service)) - if !generateSerializerDeserializer { + if includeSerializationParameters { parameters.append(self.serializerParameter(for: method, in: service)) parameters.append(self.deserializerParameter(for: method, in: service)) } + parameters.append( + ParameterDescription( + label: "options", + type: .member("CallOptions"), + defaultValue: includeDefaultCallOptions + ? .memberAccess(MemberAccessDescription(right: "defaults")) : nil + ) + ) parameters.append(self.bodyParameter(for: method, in: service)) return parameters } @@ -393,7 +409,8 @@ extension ClientCodeTranslator { for: method, in: service, from: codeGenerationRequest, - generateSerializerDeserializer: false + includeSerializationParameters: true, + includeDefaultCallOptions: true ) let grpcMethodName = self.clientMethod( isInputStreaming: method.isInputStreaming, @@ -413,6 +430,7 @@ extension ClientCodeTranslator { ), .init(label: "serializer", expression: .identifierPattern("serializer")), .init(label: "deserializer", expression: .identifierPattern("deserializer")), + .init(label: "options", expression: .identifierPattern("options")), .init(label: "handler", expression: .identifierPattern("body")), ] ) diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 00f49d5ab..91f79c24d 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -274,7 +274,7 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - options: CallOptions = .defaults, + options: CallOptions, handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( @@ -305,7 +305,7 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - options: CallOptions = .defaults, + options: CallOptions, handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( @@ -336,7 +336,7 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - options: CallOptions = .defaults, + options: CallOptions, handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( @@ -368,7 +368,7 @@ public struct GRPCClient: Sendable { descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - options: CallOptions = .defaults, + options: CallOptions, handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { switch self.state.load(ordering: .sequentiallyConsistent) { diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index a13018c17..71b6cb901 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -459,6 +459,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -467,6 +468,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -477,6 +479,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -486,6 +489,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable @@ -495,6 +499,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -505,6 +510,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable @@ -516,6 +522,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable @@ -525,6 +532,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -533,96 +541,112 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { extension Grpc_Testing_TestService.ClientProtocol { public func emptyCall( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.emptyCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func unaryCall( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func cacheableUnaryCall( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.cacheableUnaryCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func streamingOutputCall( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingOutputCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func streamingInputCall( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingInputCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func fullDuplexCall( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.fullDuplexCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func halfDuplexCall( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.halfDuplexCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func unimplementedCall( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -643,6 +667,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -650,6 +675,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.EmptyCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -659,6 +685,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -666,6 +693,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.UnaryCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -677,6 +705,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -684,6 +713,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -694,6 +724,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( @@ -701,6 +732,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -711,6 +743,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( @@ -718,6 +751,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -729,6 +763,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( @@ -736,6 +771,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -748,6 +784,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( @@ -755,6 +792,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -765,6 +803,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -772,6 +811,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro descriptor: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -786,6 +826,7 @@ public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -794,12 +835,14 @@ public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { extension Grpc_Testing_UnimplementedService.ClientProtocol { public func unimplementedCall( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -820,6 +863,7 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -827,6 +871,7 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente descriptor: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -839,6 +884,7 @@ public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -846,6 +892,7 @@ public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -854,24 +901,28 @@ public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { extension Grpc_Testing_ReconnectService.ClientProtocol { public func start( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.start( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } public func stop( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.stop( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -890,6 +941,7 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -897,6 +949,7 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService descriptor: Grpc_Testing_ReconnectService.Method.Start.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -905,6 +958,7 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -912,6 +966,7 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService descriptor: Grpc_Testing_ReconnectService.Method.Stop.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 6dfe17fa0..caa9bf8d1 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -210,6 +210,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -220,6 +221,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable @@ -229,6 +231,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -238,6 +241,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable @@ -247,6 +251,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @@ -255,60 +260,70 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { extension Grpc_Testing_BenchmarkService.ClientProtocol { internal func unaryCall( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } internal func streamingCall( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingCall( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } internal func streamingFromClient( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingFromClient( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } internal func streamingFromServer( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingFromServer( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } internal func streamingBothWays( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingBothWays( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -328,6 +343,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -335,6 +351,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi descriptor: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -346,6 +363,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( @@ -353,6 +371,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi descriptor: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -363,6 +382,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( @@ -370,6 +390,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi descriptor: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -380,6 +401,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( @@ -387,6 +409,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi descriptor: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -397,6 +420,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( @@ -404,6 +428,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi descriptor: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } diff --git a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift index 3f3fb66c7..3da181b00 100644 --- a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift @@ -1364,7 +1364,15 @@ extension Grpc_Testing_ClientConfig: SwiftProtobuf.Message, SwiftProtobuf._Messa var _medianLatencyCollectionIntervalMillis: Int32 = 0 var _clientProcesses: Int32 = 0 - static let defaultInstance = _StorageClass() + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif private init() {} @@ -1982,7 +1990,15 @@ extension Grpc_Testing_Scenario: SwiftProtobuf.Message, SwiftProtobuf._MessageIm var _benchmarkSeconds: Int32 = 0 var _spawnLocalWorkerCount: Int32 = 0 - static let defaultInstance = _StorageClass() + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif private init() {} @@ -2162,7 +2178,15 @@ extension Grpc_Testing_ScenarioResultSummary: SwiftProtobuf.Message, SwiftProtob var _startTime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil var _endTime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil - static let defaultInstance = _StorageClass() + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif private init() {} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 99c8ef5fb..3a8f10d35 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -50,6 +50,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -57,12 +58,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -81,6 +84,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -88,6 +92,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -126,6 +131,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -133,12 +139,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -157,6 +165,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( @@ -164,6 +173,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -202,6 +212,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @@ -209,12 +220,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -233,6 +246,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( @@ -240,6 +254,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -278,6 +293,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @@ -285,12 +301,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -309,6 +327,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( @@ -316,6 +335,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -362,6 +382,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable @@ -370,6 +391,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @@ -377,24 +399,28 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { extension NamespaceA_ServiceA.ClientProtocol { package func methodA( request: ClientRequest.Stream, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } package func methodB( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodB( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -413,6 +439,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Stream, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( @@ -420,6 +447,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -429,6 +457,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( @@ -436,6 +465,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { descriptor: NamespaceA_ServiceA.Method.MethodB.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -474,6 +504,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -481,12 +512,14 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { extension ServiceA.ClientProtocol { internal func methodA( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -505,6 +538,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -512,6 +546,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { descriptor: ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } diff --git a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift index 8ebd3ec44..06e530c04 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift @@ -2680,7 +2680,15 @@ extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: SwiftProtobuf.Message var _childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] var _childPolicyConfigTargetFieldName: String = String() - static let defaultInstance = _StorageClass() + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif private init() {} @@ -3004,7 +3012,15 @@ extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.Discove var _overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] var _telemetryLabels: Dictionary = [:] - static let defaultInstance = _StorageClass() + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif private init() {} diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 9cecfba71..fedc798ee 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -68,7 +68,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in let message = try response.message XCTAssertEqual(message, [3, 1, 4, 1, 5]) @@ -86,7 +87,8 @@ final class GRPCClientTests: XCTestCase { }), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in let message = try response.message XCTAssertEqual(message, [3, 1, 4, 1, 5]) @@ -100,7 +102,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.expand, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in var responseParts = response.messages.makeAsyncIterator() for byte in [3, 1, 4, 1, 5] as [UInt8] { @@ -121,7 +124,8 @@ final class GRPCClientTests: XCTestCase { }), descriptor: BinaryEcho.Methods.update, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in var responseParts = response.messages.makeAsyncIterator() for byte in [3, 1, 4, 1, 5] as [UInt8] { @@ -138,7 +142,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: MethodDescriptor(service: "not", method: "implemented"), serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in XCTAssertThrowsRPCError(try response.accepted.get()) { error in XCTAssertEqual(error.code, .unimplemented) @@ -157,7 +162,8 @@ final class GRPCClientTests: XCTestCase { }), descriptor: MethodDescriptor(service: "not", method: "implemented"), serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in XCTAssertThrowsRPCError(try response.accepted.get()) { error in XCTAssertEqual(error.code, .unimplemented) @@ -172,7 +178,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: MethodDescriptor(service: "not", method: "implemented"), serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in XCTAssertThrowsRPCError(try response.accepted.get()) { error in XCTAssertEqual(error.code, .unimplemented) @@ -191,7 +198,8 @@ final class GRPCClientTests: XCTestCase { }), descriptor: MethodDescriptor(service: "not", method: "implemented"), serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in XCTAssertThrowsRPCError(try response.accepted.get()) { error in XCTAssertEqual(error.code, .unimplemented) @@ -209,7 +217,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [i]), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in let message = try response.message XCTAssertEqual(message, [i]) @@ -236,7 +245,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in XCTAssertRejected(response) { error in XCTAssertEqual(error.code, .unavailable) @@ -255,7 +265,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in let message = try response.message XCTAssertEqual(message, [3, 1, 4, 1, 5]) @@ -270,7 +281,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { _ in } } errorHandler: { error in XCTAssertEqual(error.code, .clientIsStopped) @@ -292,7 +304,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { _ in } } errorHandler: { error in XCTAssertEqual(error.code, .clientIsStopped) @@ -306,7 +319,8 @@ final class GRPCClientTests: XCTestCase { }), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in let message = try response.message XCTAssertEqual(message, [3, 1, 4, 1, 5]) @@ -338,7 +352,8 @@ final class GRPCClientTests: XCTestCase { }), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in XCTAssertRejected(response) { error in XCTAssertEqual(error.code, .unknown) @@ -351,7 +366,8 @@ final class GRPCClientTests: XCTestCase { request: .init(message: [3, 1, 4, 1, 5]), descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), - deserializer: IdentityDeserializer() + deserializer: IdentityDeserializer(), + options: .defaults ) { response in let message = try response.message XCTAssertEqual(message, [3, 1, 4, 1, 5]) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index a047ce077..07f275658 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -87,6 +87,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -95,12 +96,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { extension Hello_World_Greeter.ClientProtocol { internal func sayHello( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -120,6 +123,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -127,6 +131,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { descriptor: Hello_World_Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } @@ -335,6 +340,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @@ -343,12 +349,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { extension Greeter.ClientProtocol { package func sayHello( request: ClientRequest.Single, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, serializer: ProtobufSerializer(), deserializer: ProtobufDeserializer(), + options: options, body ) } @@ -368,6 +376,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { request: ClientRequest.Single, serializer: some MessageSerializer, deserializer: some MessageDeserializer, + options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( @@ -375,6 +384,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { descriptor: Greeter.Method.SayHello.descriptor, serializer: serializer, deserializer: deserializer, + options: options, handler: body ) } From 706023bc9c4ed6382ad390aacd70b4c4fe1dd5a1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 7 Jun 2024 15:11:01 +0100 Subject: [PATCH 338/580] Add a NIOPosix based client transport (#1909) Motivation: We have all the pieces in place for a client transport, we can now create one. Modifications: - Add a NIOPosix based client transport Result: Can create a NIOPosix based client transport --- Package.swift | 8 - .../GRPCHTTP2TransportNIOPosix.swift | 11 +- .../HTTP2ClientTransport+Posix.swift | 204 ++++++++++++++++++ .../NIOClientBootstrap+SocketAddress.swift | 69 ++++++ .../GRPCHTTP2TransportNIOPosixTests.swift | 17 -- ...P2TransportNIOTransportServicesTests.swift | 17 -- 6 files changed, 277 insertions(+), 49 deletions(-) create mode 100644 Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift create mode 100644 Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift delete mode 100644 Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift delete mode 100644 Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift diff --git a/Package.swift b/Package.swift index 44182b025..d3437fd99 100644 --- a/Package.swift +++ b/Package.swift @@ -335,13 +335,6 @@ extension Target { ] ) - static let grpcHTTP2TransportNIOTransportServicesTests: Target = .testTarget( - name: "GRPCHTTP2TransportNIOTransportServicesTests", - dependencies: [ - .grpcHTTP2TransportNIOTransportServices - ] - ) - static let grpcCodeGenTests: Target = .testTarget( name: "GRPCCodeGenTests", dependencies: [ @@ -749,7 +742,6 @@ let package = Package( .grpcCodeGenTests, .grpcInterceptorsTests, .grpcHTTP2CoreTests, - .grpcHTTP2TransportNIOTransportServicesTests, .grpcProtobufTests, .grpcProtobufCodeGenTests, .inProcessInteroperabilityTests diff --git a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift index 0325728f4..505695bc8 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift @@ -174,11 +174,11 @@ extension HTTP2ServerTransport.Posix { extension NIOCore.SocketAddress { fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws { if let ipv4 = socketAddress.ipv4 { - self = try Self(ipAddress: ipv4.host, port: ipv4.port) + self = try Self(ipv4) } else if let ipv6 = socketAddress.ipv6 { - self = try Self(ipAddress: ipv6.host, port: ipv6.port) + self = try Self(ipv6) } else if let unixDomainSocket = socketAddress.unixDomainSocket { - self = try Self(unixDomainSocketPath: unixDomainSocket.path) + self = try Self(unixDomainSocket) } else { throw RPCError( code: .internalError, @@ -197,10 +197,7 @@ extension ServerBootstrap { ) async throws -> NIOAsyncChannel { if let virtualSocket = address.virtualSocket { return try await self.bind( - to: VsockAddress( - cid: VsockAddress.ContextID(rawValue: virtualSocket.contextID.rawValue), - port: VsockAddress.Port(rawValue: virtualSocket.port.rawValue) - ), + to: VsockAddress(virtualSocket), childChannelInitializer: childChannelInitializer ) } else { diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift new file mode 100644 index 000000000..e65913cd8 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -0,0 +1,204 @@ +/* + * 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 +@_spi(Package) import GRPCHTTP2Core +import NIOCore +import NIOPosix + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension HTTP2ClientTransport { + /// A `ClientTransport` using HTTP/2 built on top of `NIOPosix`. + /// + /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use + /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended + /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of + /// the `HTTP2ClientTransport`. + /// + /// To use this transport you need to provide a 'target' to connect to which will be resolved + /// by an appropriate resolver from the resolver registry. By default the resolver registry can + /// resolve DNS targets, IPv4 and IPv6 targets, Unix domain socket targets, and Virtual Socket + /// targets. If you use a custom target you must also provide an appropriately configured + /// registry. + /// + /// You can control various aspects of connection creation, management and RPC behavior via the + /// ``Config``. Load balancing policies and other RPC specific behavior can be configured via + /// the ``ServiceConfig`` (if it isn't provided by a resolver). + /// + /// Beyond creating the transport you don't need to interact with it directly, instead, pass it + /// to a `GRPCClient`: + /// + /// ```swift + /// try await withThrowingDiscardingTaskGroup { + /// let transport = try HTTP2ClientTransport.Posix(target: .dns(host: "example.com")) + /// let client = GRPCClient(transport: transport) + /// group.addTask { + /// try await client.run() + /// } + /// + /// // ... + /// } + /// ``` + public struct Posix: ClientTransport { + private let channel: GRPCChannel + + /// Creates a new Posix based HTTP/2 client transport. + /// + /// - Parameters: + /// - target: A target to resolve. + /// - resolverRegistry: A registry of resolver factories. + /// - config: Configuration for the transport. + /// - serviceConfig: Service config controlling how the transport should establish and + /// load-balance connections. + /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must + /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from + /// a `MultiThreadedEventLoopGroup`. + /// - Throws: When no suitable resolver could be found for the `target`. + public init( + target: any ResolvableTarget, + resolverRegistry: NameResolverRegistry = .defaults, + config: Config = .defaults, + serviceConfig: ServiceConfig = ServiceConfig(), + eventLoopGroup: any EventLoopGroup = .singletonMultiThreadedEventLoopGroup + ) throws { + guard let resolver = resolverRegistry.makeResolver(for: target) else { + throw RuntimeError( + code: .transportError, + message: """ + No suitable resolvers to resolve '\(target)'. You must make sure that the resolver \ + registry has a suitable name resolver factory registered for the given target. + """ + ) + } + + // Configure a connector. + self.channel = GRPCChannel( + resolver: resolver, + connector: Connector(eventLoopGroup: eventLoopGroup, config: config), + config: GRPCChannel.Config(posix: config), + defaultServiceConfig: serviceConfig + ) + } + + public var retryThrottle: RetryThrottle? { + self.channel.retryThrottle + } + + public func connect() async { + await self.channel.connect() + } + + public func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { + self.channel.configuration(forMethod: descriptor) + } + + public func close() { + self.channel.close() + } + + public func withStream( + descriptor: MethodDescriptor, + options: CallOptions, + _ closure: (RPCStream) async throws -> T + ) async throws -> T { + try await self.channel.withStream(descriptor: descriptor, options: options, closure) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension HTTP2ClientTransport.Posix { + struct Connector: HTTP2Connector { + private let config: HTTP2ClientTransport.Posix.Config + private let eventLoopGroup: any EventLoopGroup + + init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) { + self.eventLoopGroup = eventLoopGroup + self.config = config + } + + func establishConnection( + to address: GRPCHTTP2Core.SocketAddress + ) async throws -> HTTP2Connection { + let (channel, multiplexer) = try await ClientBootstrap( + group: self.eventLoopGroup + ).connect(to: address) { channel in + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.configureGRPCClientPipeline( + channel: channel, + config: GRPCChannel.Config(posix: self.config) + ) + } + } + + return HTTP2Connection(channel: channel, multiplexer: multiplexer, isPlaintext: false) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension HTTP2ClientTransport.Posix { + public struct Config: Sendable { + /// Configuration for HTTP/2 connections. + public var http2: HTTP2ClientTransport.Config.HTTP2 + + /// Configuration for backoff used when establishing a connection. + public var backoff: HTTP2ClientTransport.Config.Backoff + + /// Configuration for connection management. + public var connection: HTTP2ClientTransport.Config.Connection + + /// Compression configuration. + public var compression: HTTP2ClientTransport.Config.Compression + + /// Creates a new connection configuration. + /// + /// See also ``defaults``. + public init( + http2: HTTP2ClientTransport.Config.HTTP2, + backoff: HTTP2ClientTransport.Config.Backoff, + connection: HTTP2ClientTransport.Config.Connection, + compression: HTTP2ClientTransport.Config.Compression + ) { + self.http2 = http2 + self.connection = connection + self.backoff = backoff + self.compression = compression + } + + /// Default values. + public static var defaults: Self { + Self( + http2: .defaults, + backoff: .defaults, + connection: .defaults, + compression: .defaults + ) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension GRPCChannel.Config { + init(posix: HTTP2ClientTransport.Posix.Config) { + self.init( + http2: posix.http2, + backoff: posix.backoff, + connection: posix.connection, + compression: posix.compression + ) + } +} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift new file mode 100644 index 000000000..0385ed2f4 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift @@ -0,0 +1,69 @@ +/* + * 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 GRPCHTTP2Core +import NIOCore +import NIOPosix + +extension ClientBootstrap { + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + func connect( + to address: GRPCHTTP2Core.SocketAddress, + _ configure: @Sendable @escaping (Channel) -> EventLoopFuture + ) async throws -> Result { + if let ipv4 = address.ipv4 { + return try await self.connect(to: NIOCore.SocketAddress(ipv4), channelInitializer: configure) + } else if let ipv6 = address.ipv6 { + return try await self.connect(to: NIOCore.SocketAddress(ipv6), channelInitializer: configure) + } else if let uds = address.unixDomainSocket { + return try await self.connect(to: NIOCore.SocketAddress(uds), channelInitializer: configure) + } else if let vsock = address.virtualSocket { + return try await self.connect(to: VsockAddress(vsock), channelInitializer: configure) + } else { + throw RuntimeError( + code: .transportError, + message: """ + Unhandled socket address '\(address)', this is a gRPC Swift bug. Please file an issue \ + against the project. + """ + ) + } + } +} + +extension NIOCore.SocketAddress { + init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { + try self.init(ipAddress: address.host, port: address.port) + } + + init(_ address: GRPCHTTP2Core.SocketAddress.IPv6) throws { + try self.init(ipAddress: address.host, port: address.port) + } + + init(_ address: GRPCHTTP2Core.SocketAddress.UnixDomainSocket) throws { + try self.init(unixDomainSocketPath: address.path) + } +} + +extension NIOPosix.VsockAddress { + init(_ address: GRPCHTTP2Core.SocketAddress.VirtualSocket) { + self.init( + cid: ContextID(rawValue: address.contextID.rawValue), + port: Port(rawValue: address.port.rawValue) + ) + } +} diff --git a/Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift deleted file mode 100644 index 3630df68b..000000000 --- a/Tests/GRPCHTTP2TransportNIOPosixTests/GRPCHTTP2TransportNIOPosixTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -// Add tests to this package. diff --git a/Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift deleted file mode 100644 index 3630df68b..000000000 --- a/Tests/GRPCHTTP2TransportNIOTransportServicesTests/GRPCHTTP2TransportNIOTransportServicesTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -// Add tests to this package. From b170fb883fb2bc481e454583c7ea2a13010dbcee Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 10 Jun 2024 14:24:11 +0100 Subject: [PATCH 339/580] Add the pick-first load balancer (#1907) --- .../Client/Connection/GRPCChannel.swift | 22 +- .../LoadBalancers/LoadBalancer.swift | 18 +- .../LoadBalancers/PickFirstLoadBalancer.swift | 609 ++++++++++++++++++ .../LoadBalancers/LoadBalancerTest.swift | 37 ++ .../PickFirstLoadBalancerTests.swift | 333 ++++++++++ 5 files changed, 1010 insertions(+), 9 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 5053bdb26..02e6a765c 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -438,10 +438,17 @@ extension GRPCChannel { endpoints: [Endpoint], in group: inout DiscardingTaskGroup ) { + assert(!endpoints.isEmpty, "endpoints must be non-empty") + switch update { case .runLoadBalancer(let new, let old): old?.close() - new.updateAddresses(endpoints) + switch new { + case .roundRobin(let loadBalancer): + loadBalancer.updateAddresses(endpoints) + case .pickFirst(let loadBalancer): + loadBalancer.updateEndpoint(endpoints.first!) + } group.addTask { await new.run() @@ -454,7 +461,12 @@ extension GRPCChannel { } case .updateLoadBalancer(let existing): - existing.updateAddresses(endpoints) + switch existing { + case .roundRobin(let loadBalancer): + loadBalancer.updateAddresses(endpoints) + case .pickFirst(let loadBalancer): + loadBalancer.updateEndpoint(endpoints.first!) + } case .none: () @@ -610,11 +622,17 @@ extension GRPCChannel.StateMachine { enum LoadBalancerKind { case roundRobin + case pickFirst func matches(loadBalancer: LoadBalancer) -> Bool { switch (self, loadBalancer) { case (.roundRobin, .roundRobin): return true + case (.pickFirst, .pickFirst): + return true + case (.roundRobin, .pickFirst), + (.pickFirst, .roundRobin): + return false } } } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift index 9b6788eac..f5465178f 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift @@ -17,6 +17,7 @@ @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) enum LoadBalancer: Sendable { case roundRobin(RoundRobinLoadBalancer) + case pickFirst(PickFirstLoadBalancer) } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) @@ -29,6 +30,8 @@ extension LoadBalancer { switch self { case .roundRobin(let loadBalancer): return loadBalancer.id + case .pickFirst(let loadBalancer): + return loadBalancer.id } } @@ -36,6 +39,8 @@ extension LoadBalancer { switch self { case .roundRobin(let loadBalancer): return loadBalancer.events + case .pickFirst(let loadBalancer): + return loadBalancer.events } } @@ -43,13 +48,8 @@ extension LoadBalancer { switch self { case .roundRobin(let loadBalancer): await loadBalancer.run() - } - } - - func updateAddresses(_ endpoints: [Endpoint]) { - switch self { - case .roundRobin(let loadBalancer): - loadBalancer.updateAddresses(endpoints) + case .pickFirst(let loadBalancer): + await loadBalancer.run() } } @@ -57,6 +57,8 @@ extension LoadBalancer { switch self { case .roundRobin(let loadBalancer): loadBalancer.close() + case .pickFirst(let loadBalancer): + loadBalancer.close() } } @@ -64,6 +66,8 @@ extension LoadBalancer { switch self { case .roundRobin(let loadBalancer): return loadBalancer.pickSubchannel() + case .pickFirst(let loadBalancer): + return loadBalancer.pickSubchannel() } } } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift new file mode 100644 index 000000000..1b9f54fb0 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift @@ -0,0 +1,609 @@ +/* + * 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 + +/// A load-balancer which has a single subchannel. +/// +/// This load-balancer starts in an 'idle' state and begins connecting when a set of addresses is +/// provided to it with ``updateEndpoint(_:)``. Repeated calls to ``updateEndpoint(_:)`` will +/// update the subchannel gracefully: RPCs will continue to use the old subchannel until the new +/// subchannel becomes ready. +/// +/// You must call ``close()`` on the load-balancer when it's no longer required. This will move +/// it to the ``ConnectivityState/shutdown`` state: existing RPCs may continue but all subsequent +/// calls to ``makeStream(descriptor:options:)`` will fail. +/// +/// To use this load-balancer you must run it in a task: +/// +/// ```swift +/// await withDiscardingTaskGroup { group in +/// // Run the load-balancer +/// group.addTask { await pickFirst.run() } +/// +/// // Update its endpoint. +/// let endpoint = Endpoint( +/// addresses: [ +/// .ipv4(host: "127.0.0.1", port: 1001), +/// .ipv4(host: "127.0.0.1", port: 1002), +/// .ipv4(host: "127.0.0.1", port: 1003) +/// ] +/// ) +/// pickFirst.updateEndpoint(endpoint) +/// +/// // Consume state update events +/// for await event in pickFirst.events { +/// switch event { +/// case .connectivityStateChanged(.ready): +/// // ... +/// default: +/// // ... +/// } +/// } +/// } +/// ``` +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct PickFirstLoadBalancer { + enum Input: Sendable, Hashable { + /// Update the addresses used by the load balancer to the following endpoints. + case updateEndpoint(Endpoint) + /// Close the load balancer. + case close + } + + /// Events which can happen to the load balancer. + private let event: + ( + stream: AsyncStream, + continuation: AsyncStream.Continuation + ) + + /// Inputs which this load balancer should react to. + private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) + + /// A connector, capable of creating connections. + private let connector: any HTTP2Connector + + /// Connection backoff configuration. + private let backoff: ConnectionBackoff + + /// The default compression algorithm to use. Can be overridden on a per-call basis. + private let defaultCompression: CompressionAlgorithm + + /// The set of enabled compression algorithms. + private let enabledCompression: CompressionAlgorithmSet + + /// The state of the load-balancer. + private let state: _LockedValueBox + + /// The ID of this load balancer. + internal let id: LoadBalancerID + + init( + connector: any HTTP2Connector, + backoff: ConnectionBackoff, + defaultCompression: CompressionAlgorithm, + enabledCompression: CompressionAlgorithmSet + ) { + self.connector = connector + self.backoff = backoff + self.defaultCompression = defaultCompression + self.enabledCompression = enabledCompression + self.id = LoadBalancerID() + self.state = _LockedValueBox(State()) + + self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) + self.input = AsyncStream.makeStream(of: Input.self) + // The load balancer starts in the idle state. + self.event.continuation.yield(.connectivityStateChanged(.idle)) + } + + /// A stream of events which can happen to the load balancer. + var events: AsyncStream { + self.event.stream + } + + /// Runs the load balancer, returning when it has closed. + /// + /// You can monitor events which happen on the load balancer with ``events``. + func run() async { + await withDiscardingTaskGroup { group in + for await input in self.input.stream { + switch input { + case .updateEndpoint(let endpoint): + self.handleUpdateEndpoint(endpoint, in: &group) + case .close: + self.handleCloseInput() + } + } + } + + if Task.isCancelled { + // Finish the event stream as it's unlikely to have been finished by a regular code path. + self.event.continuation.finish() + } + } + + /// Update the addresses used by the load balancer. + /// + /// This may result in new subchannels being created and some subchannels being removed. + func updateEndpoint(_ endpoint: Endpoint) { + self.input.continuation.yield(.updateEndpoint(endpoint)) + } + + /// Close the load balancer, and all subchannels it manages. + func close() { + self.input.continuation.yield(.close) + } + + /// Pick a ready subchannel from the load balancer. + /// + /// - Returns: A subchannel, or `nil` if there aren't any ready subchannels. + func pickSubchannel() -> Subchannel? { + let onPickSubchannel = self.state.withLockedValue { $0.pickSubchannel() } + switch onPickSubchannel { + case .picked(let subchannel): + return subchannel + case .notAvailable(let subchannel): + subchannel?.connect() + return nil + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension PickFirstLoadBalancer { + private func handleUpdateEndpoint(_ endpoint: Endpoint, in group: inout DiscardingTaskGroup) { + if endpoint.addresses.isEmpty { return } + + let onUpdate = self.state.withLockedValue { state in + state.updateEndpoint(endpoint) { endpoint, id in + Subchannel( + endpoint: endpoint, + id: id, + connector: self.connector, + backoff: self.backoff, + defaultCompression: self.defaultCompression, + enabledCompression: self.enabledCompression + ) + } + } + + switch onUpdate { + case .connect(let newSubchannel, close: let oldSubchannel): + self.runSubchannel(newSubchannel, in: &group) + oldSubchannel?.close() + + case .none: + () + } + } + + private func runSubchannel( + _ subchannel: Subchannel, + in group: inout DiscardingTaskGroup + ) { + // Start running it and tell it to connect. + subchannel.connect() + group.addTask { + await subchannel.run() + } + + group.addTask { + for await event in subchannel.events { + switch event { + case .connectivityStateChanged(let state): + self.handleSubchannelConnectivityStateChange(state, id: subchannel.id) + case .goingAway: + self.handleGoAway(id: subchannel.id) + case .requiresNameResolution: + self.event.continuation.yield(.requiresNameResolution) + } + } + } + } + + private func handleSubchannelConnectivityStateChange( + _ connectivityState: ConnectivityState, + id: SubchannelID + ) { + let onUpdateState = self.state.withLockedValue { + $0.updateSubchannelConnectivityState(connectivityState, id: id) + } + + switch onUpdateState { + case .close(let subchannel): + subchannel.close() + case .closeAndPublishStateChange(let subchannel, let connectivityState): + subchannel.close() + self.event.continuation.yield(.connectivityStateChanged(connectivityState)) + case .publishStateChange(let connectivityState): + self.event.continuation.yield(.connectivityStateChanged(connectivityState)) + case .closed: + self.event.continuation.finish() + self.input.continuation.finish() + case .none: + () + } + } + + private func handleGoAway(id: SubchannelID) { + self.state.withLockedValue { state in + state.receivedGoAway(id: id) + } + } + + private func handleCloseInput() { + let onClose = self.state.withLockedValue { $0.close() } + switch onClose { + case .closeSubchannels(let subchannel1, let subchannel2): + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + subchannel1.close() + subchannel2?.close() + + case .closed: + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + self.event.continuation.finish() + self.input.continuation.finish() + + case .none: + () + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension PickFirstLoadBalancer { + enum State: Sendable { + case active(Active) + case closing(Closing) + case closed + + init() { + self = .active(Active()) + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension PickFirstLoadBalancer.State { + struct Active: Sendable { + var endpoint: Endpoint? + var connectivityState: ConnectivityState + var current: Subchannel? + var next: Subchannel? + var parked: [SubchannelID: Subchannel] + var isCurrentGoingAway: Bool + + init() { + self.endpoint = nil + self.connectivityState = .idle + self.current = nil + self.next = nil + self.parked = [:] + self.isCurrentGoingAway = false + } + } + + struct Closing: Sendable { + var parked: [SubchannelID: Subchannel] + + init(from state: Active) { + self.parked = state.parked + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension PickFirstLoadBalancer.State.Active { + mutating func updateEndpoint( + _ endpoint: Endpoint, + makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel + ) -> PickFirstLoadBalancer.State.OnUpdateEndpoint { + if self.endpoint == endpoint { return .none } + + let onUpdateEndpoint: PickFirstLoadBalancer.State.OnUpdateEndpoint + + let id = SubchannelID() + let newSubchannel = makeSubchannel(endpoint, id) + + switch (self.current, self.next) { + case (.some(let current), .none): + if self.connectivityState == .idle { + // Current subchannel is idle and we have a new endpoint, move straight to the new + // subchannel. + self.current = newSubchannel + self.parked[current.id] = current + onUpdateEndpoint = .connect(newSubchannel, close: current) + } else { + // Current subchannel is in a non-idle state, set it as the next subchannel and promote + // it when it becomes ready. + self.next = newSubchannel + onUpdateEndpoint = .connect(newSubchannel, close: nil) + } + + case (.some, .some(let next)): + // Current and next subchannel exist. Replace the next subchannel. + self.next = newSubchannel + self.parked[next.id] = next + onUpdateEndpoint = .connect(newSubchannel, close: next) + + case (.none, .none): + self.current = newSubchannel + onUpdateEndpoint = .connect(newSubchannel, close: nil) + + case (.none, .some(let next)): + self.current = newSubchannel + self.next = nil + self.parked[next.id] = next + onUpdateEndpoint = .connect(newSubchannel, close: next) + } + + return onUpdateEndpoint + } + + mutating func updateSubchannelConnectivityState( + _ connectivityState: ConnectivityState, + id: SubchannelID + ) -> (PickFirstLoadBalancer.State.OnConnectivityStateUpdate, PickFirstLoadBalancer.State) { + let onUpdate: PickFirstLoadBalancer.State.OnConnectivityStateUpdate + + if let current = self.current, current.id == id { + if connectivityState == self.connectivityState { + onUpdate = .none + } else { + self.connectivityState = connectivityState + onUpdate = .publishStateChange(connectivityState) + } + } else if let next = self.next, next.id == id { + // if it becomes ready then promote it + switch connectivityState { + case .ready: + if self.connectivityState != connectivityState { + self.connectivityState = connectivityState + + if let current = self.current { + onUpdate = .closeAndPublishStateChange(current, connectivityState) + } else { + onUpdate = .publishStateChange(connectivityState) + } + + self.current = next + self.isCurrentGoingAway = false + } else { + // No state change to publish, just roll over. + onUpdate = self.current.map { .close($0) } ?? .none + self.current = next + self.isCurrentGoingAway = false + } + + case .idle, .connecting, .transientFailure, .shutdown: + onUpdate = .none + } + + } else { + switch connectivityState { + case .idle: + if let subchannel = self.parked[id] { + onUpdate = .close(subchannel) + } else { + onUpdate = .none + } + + case .shutdown: + self.parked.removeValue(forKey: id) + onUpdate = .none + + case .connecting, .ready, .transientFailure: + onUpdate = .none + } + } + + return (onUpdate, .active(self)) + } + + mutating func receivedGoAway(id: SubchannelID) { + if let current = self.current, current.id == id { + // When receiving a GOAWAY the subchannel will ask for an address to be re-resolved and the + // connection will eventually become idle. At this point we wait: the connection remains + // in its current state. + self.isCurrentGoingAway = true + } else if let next = self.next, next.id == id { + // The next connection is going away, park it. + // connection. + self.next = nil + self.parked[next.id] = next + } + } + + mutating func close() -> (PickFirstLoadBalancer.State.OnClose, PickFirstLoadBalancer.State) { + let onClose: PickFirstLoadBalancer.State.OnClose + let nextState: PickFirstLoadBalancer.State + + if let current = self.current { + self.parked[current.id] = current + if let next = self.next { + self.parked[next.id] = next + onClose = .closeSubchannels(current, next) + } else { + onClose = .closeSubchannels(current, nil) + } + nextState = .closing(PickFirstLoadBalancer.State.Closing(from: self)) + } else { + onClose = .closed + nextState = .closed + } + + return (onClose, nextState) + } + + func pickSubchannel() -> PickFirstLoadBalancer.State.OnPickSubchannel { + let onPick: PickFirstLoadBalancer.State.OnPickSubchannel + + if let current = self.current, !self.isCurrentGoingAway { + switch self.connectivityState { + case .idle: + onPick = .notAvailable(current) + case .ready: + onPick = .picked(current) + case .connecting, .transientFailure, .shutdown: + onPick = .notAvailable(nil) + } + } else { + onPick = .notAvailable(nil) + } + + return onPick + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension PickFirstLoadBalancer.State.Closing { + mutating func updateSubchannelConnectivityState( + _ connectivityState: ConnectivityState, + id: SubchannelID + ) -> (PickFirstLoadBalancer.State.OnConnectivityStateUpdate, PickFirstLoadBalancer.State) { + let onUpdate: PickFirstLoadBalancer.State.OnConnectivityStateUpdate + let nextState: PickFirstLoadBalancer.State + + switch connectivityState { + case .idle: + if let subchannel = self.parked[id] { + onUpdate = .close(subchannel) + } else { + onUpdate = .none + } + nextState = .closing(self) + + case .shutdown: + if self.parked.removeValue(forKey: id) != nil { + if self.parked.isEmpty { + onUpdate = .closed + nextState = .closed + } else { + onUpdate = .none + nextState = .closing(self) + } + } else { + onUpdate = .none + nextState = .closing(self) + } + + case .connecting, .ready, .transientFailure: + onUpdate = .none + nextState = .closing(self) + } + + return (onUpdate, nextState) + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension PickFirstLoadBalancer.State { + enum OnUpdateEndpoint { + case connect(Subchannel, close: Subchannel?) + case none + } + + mutating func updateEndpoint( + _ endpoint: Endpoint, + makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel + ) -> OnUpdateEndpoint { + let onUpdateEndpoint: OnUpdateEndpoint + + switch self { + case .active(var state): + onUpdateEndpoint = state.updateEndpoint(endpoint) { endpoint, id in + makeSubchannel(endpoint, id) + } + self = .active(state) + + case .closing, .closed: + onUpdateEndpoint = .none + } + + return onUpdateEndpoint + } + + enum OnConnectivityStateUpdate { + case closeAndPublishStateChange(Subchannel, ConnectivityState) + case publishStateChange(ConnectivityState) + case close(Subchannel) + case closed + case none + } + + mutating func updateSubchannelConnectivityState( + _ connectivityState: ConnectivityState, + id: SubchannelID + ) -> OnConnectivityStateUpdate { + let onUpdateState: OnConnectivityStateUpdate + + switch self { + case .active(var state): + (onUpdateState, self) = state.updateSubchannelConnectivityState(connectivityState, id: id) + case .closing(var state): + (onUpdateState, self) = state.updateSubchannelConnectivityState(connectivityState, id: id) + case .closed: + onUpdateState = .none + } + + return onUpdateState + } + + mutating func receivedGoAway(id: SubchannelID) { + switch self { + case .active(var state): + state.receivedGoAway(id: id) + self = .active(state) + case .closing, .closed: + () + } + } + + enum OnClose { + case closeSubchannels(Subchannel, Subchannel?) + case closed + case none + } + + mutating func close() -> OnClose { + let onClose: OnClose + + switch self { + case .active(var state): + (onClose, self) = state.close() + case .closing, .closed: + onClose = .none + } + + return onClose + } + + enum OnPickSubchannel { + case picked(Subchannel) + case notAvailable(Subchannel?) + } + + func pickSubchannel() -> OnPickSubchannel { + switch self { + case .active(let state): + return state.pickSubchannel() + case .closing, .closed: + return .notAvailable(nil) + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift index 92295ff81..bfb8908ff 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift @@ -24,6 +24,32 @@ enum LoadBalancerTest { let loadBalancer: LoadBalancer } + static func pickFirst( + servers serverCount: Int, + connector: any HTTP2Connector, + backoff: ConnectionBackoff = .defaults, + timeout: Duration = .seconds(10), + function: String = #function, + handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, + verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in } + ) async throws { + try await Self.run( + servers: serverCount, + timeout: timeout, + function: function, + handleEvent: handleEvent, + verifyEvents: verifyEvents + ) { + let pickFirst = PickFirstLoadBalancer( + connector: connector, + backoff: backoff, + defaultCompression: .none, + enabledCompression: .none + ) + return .pickFirst(pickFirst) + } + } + static func roundRobin( servers serverCount: Int, connector: any HTTP2Connector, @@ -143,6 +169,17 @@ extension LoadBalancerTest.Context { switch self.loadBalancer { case .roundRobin(let loadBalancer): return loadBalancer + case .pickFirst: + return nil + } + } + + var pickFirst: PickFirstLoadBalancer? { + switch self.loadBalancer { + case .roundRobin: + return nil + case .pickFirst(let loadBalancer): + return loadBalancer } } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift new file mode 100644 index 000000000..958bba4a4 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift @@ -0,0 +1,333 @@ +/* + * 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 Atomics +import GRPCCore +@_spi(Package) @testable import GRPCHTTP2Core +import NIOHTTP2 +import NIOPosix +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class PickFirstLoadBalancerTests: XCTestCase { + func testPickFirstConnectsToServer() async throws { + try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let endpoint = Endpoint(addresses: context.servers.map { $0.address }) + context.pickFirst!.updateEndpoint(endpoint) + case .connectivityStateChanged(.ready): + context.loadBalancer.close() + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testPickSubchannelWhenNotReady() async throws { + try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + XCTAssertNil(context.loadBalancer.pickSubchannel()) + context.loadBalancer.close() + case .connectivityStateChanged(.shutdown): + XCTAssertNil(context.loadBalancer.pickSubchannel()) + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testPickSubchannelReturnsSameSubchannel() async throws { + try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let endpoint = Endpoint(addresses: context.servers.map { $0.address }) + context.pickFirst!.updateEndpoint(endpoint) + + case .connectivityStateChanged(.ready): + var ids = Set() + for _ in 0 ..< 100 { + let subchannel = try XCTUnwrap(context.loadBalancer.pickSubchannel()) + ids.insert(subchannel.id) + } + XCTAssertEqual(ids.count, 1) + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testEndpointUpdateHandledGracefully() async throws { + try await LoadBalancerTest.pickFirst(servers: 2, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let endpoint = Endpoint(addresses: [context.servers[0].address]) + context.pickFirst!.updateEndpoint(endpoint) + + case .connectivityStateChanged(.ready): + // Must be connected to server-0. + try await XCTPoll(every: .milliseconds(10)) { + context.servers[0].server.clients.count == 1 + } + + // Update the endpoint so that it contains server-1. + let endpoint = Endpoint(addresses: [context.servers[1].address]) + context.pickFirst!.updateEndpoint(endpoint) + + // Should remain in the ready state + try await XCTPoll(every: .milliseconds(10)) { + context.servers[0].server.clients.isEmpty && context.servers[1].server.clients.count == 1 + } + + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testSameEndpointUpdateIsIgnored() async throws { + try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let endpoint = Endpoint(addresses: context.servers.map { $0.address }) + context.pickFirst!.updateEndpoint(endpoint) + + case .connectivityStateChanged(.ready): + // Must be connected to server-0. + try await XCTPoll(every: .milliseconds(10)) { + context.servers[0].server.clients.count == 1 + } + + // Update the endpoint. This should be a no-op, server should remain connected. + let endpoint = Endpoint(addresses: context.servers.map { $0.address }) + context.pickFirst!.updateEndpoint(endpoint) + try await XCTPoll(every: .milliseconds(10)) { + context.servers[0].server.clients.count == 1 + } + + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testEmptyEndpointUpdateIsIgnored() async throws { + // Checks that an update using the empty endpoint is ignored. + try await LoadBalancerTest.pickFirst(servers: 0, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let endpoint = Endpoint(addresses: []) + // Should no-op. + context.pickFirst!.updateEndpoint(endpoint) + context.loadBalancer.close() + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testPickOnIdleTriggersConnect() async throws { + // Tests that picking a subchannel when the load balancer is idle triggers a reconnect and + // becomes ready again. Uses a very short idle time to re-enter the idle state. + let idle = ManagedAtomic(0) + + try await LoadBalancerTest.pickFirst( + servers: 1, + connector: .posix(maxIdleTime: .milliseconds(1)) // Aggressively idle the connection + ) { context, event in + switch event { + case .connectivityStateChanged(.idle): + let idleCount = idle.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) + + switch idleCount { + case 1: + // The first idle happens when the load balancer in started, give it an endpoint + // which it will connect to. Wait for it to be ready and then idle again. + let endpoint = Endpoint(addresses: context.servers.map { $0.address }) + context.pickFirst!.updateEndpoint(endpoint) + case 2: + // Load-balancer has the endpoints but all are idle. Picking will trigger a connect. + XCTAssertNil(context.loadBalancer.pickSubchannel()) + case 3: + // Connection idled again. Shut it down. + context.loadBalancer.close() + + default: + XCTFail("Became idle too many times") + } + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.idle), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testPickFirstConnectionDropReturnsToIdle() async throws { + // Checks that when the load balancers connection is unexpectedly dropped when there are no + // open streams that it returns to the idle state. + let idleCount = ManagedAtomic(0) + + try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + switch idleCount.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) { + case 1: + let endpoint = Endpoint(addresses: context.servers.map { $0.address }) + context.pickFirst!.updateEndpoint(endpoint) + case 2: + context.loadBalancer.close() + default: + () + } + + case .connectivityStateChanged(.ready): + // Drop the connection. + context.servers[0].server.clients[0].close(mode: .all, promise: nil) + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.idle), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } + + func testPickFirstReceivesGoAway() async throws { + let idleCount = ManagedAtomic(0) + try await LoadBalancerTest.pickFirst(servers: 2, connector: .posix()) { context, event in + switch event { + case .connectivityStateChanged(.idle): + switch idleCount.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) { + case 1: + // Provide the address of the first server. + context.pickFirst!.updateEndpoint(Endpoint(context.servers[0].address)) + case 2: + // Provide the address of the second server. + context.pickFirst!.updateEndpoint(Endpoint(context.servers[1].address)) + default: + () + } + + case .connectivityStateChanged(.ready): + switch idleCount.load(ordering: .sequentiallyConsistent) { + case 1: + // Must be connected to server 1, send a GOAWAY frame. + let channel = context.servers[0].server.clients.first! + let goAway = HTTP2Frame( + streamID: .rootStream, + payload: .goAway(lastStreamID: 0, errorCode: .noError, opaqueData: nil) + ) + channel.writeAndFlush(goAway, promise: nil) + + case 2: + // Must only be connected to server 2 now. + XCTAssertEqual(context.servers[0].server.clients.count, 0) + XCTAssertEqual(context.servers[1].server.clients.count, 1) + context.loadBalancer.close() + + default: + () + } + + default: + () + } + } verifyEvents: { events in + let expected: [LoadBalancerEvent] = [ + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .requiresNameResolution, + .connectivityStateChanged(.idle), + .connectivityStateChanged(.connecting), + .connectivityStateChanged(.ready), + .connectivityStateChanged(.shutdown), + ] + XCTAssertEqual(events, expected) + } + } +} From eb503ca6263e1911783de7b037a780e97d3e6d2f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 10 Jun 2024 14:28:58 +0100 Subject: [PATCH 340/580] Start the idle timer when the connection is ready (#1915) Motivation: Some of the idle tests are flaky because the idle timer races the connection becoming ready which can lead to state transitions the tests aren't expecting. The root of this is caused by the idle timer starting on channel active instead of when the connection becomes ready. Modification: - Only start the idle/keepalive timers when the connection becomes ready Result: More reliable tests --- .../Connection/ClientConnectionHandler.swift | 18 ++++++++---------- .../ClientConnectionHandlerTests.swift | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 5fce9c72f..55de44a3a 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -130,16 +130,6 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl self.context = nil } - func channelActive(context: ChannelHandlerContext) { - self.keepaliveTimer?.schedule(on: context.eventLoop) { - self.keepaliveTimerFired(context: context) - } - - self.maxIdleTimer?.schedule(on: context.eventLoop) { - self.maxIdleTimerFired(context: context) - } - } - func channelInactive(context: ChannelHandlerContext) { switch self.state.closed() { case .none: @@ -221,6 +211,14 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl // becoming active is insufficient as, for example, a TLS handshake may fail after // establishing the TCP connection, or the server isn't configured for gRPC (or HTTP/2). if isInitialSettings { + self.keepaliveTimer?.schedule(on: context.eventLoop) { + self.keepaliveTimerFired(context: context) + } + + self.maxIdleTimer?.schedule(on: context.eventLoop) { + self.maxIdleTimerFired(context: context) + } + context.fireChannelRead(self.wrapInboundOut(.ready)) } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 827ea71ec..77350d149 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -26,6 +26,10 @@ final class ClientConnectionHandlerTests: XCTestCase { let connection = try Connection(maxIdleTime: .minutes(5)) try connection.activate() + // Write the initial settings to ready the connection. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + // Idle with no streams open we should: // - read out a closing event, // - write a GOAWAY frame, @@ -74,6 +78,10 @@ final class ClientConnectionHandlerTests: XCTestCase { let connection = try Connection(keepaliveTime: .minutes(1), keepaliveTimeout: .seconds(10)) try connection.activate() + // Write the initial settings to ready the connection. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + // Open a stream so keep-alive starts. connection.streamOpened(1) @@ -100,6 +108,10 @@ final class ClientConnectionHandlerTests: XCTestCase { let connection = try Connection(keepaliveTime: .minutes(1), allowKeepaliveWithoutCalls: true) try connection.activate() + // Write the initial settings to ready the connection. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + for _ in 0 ..< 10 { // Advance time, a PING should be sent, ACK it. connection.loop.advanceTime(by: .minutes(1)) @@ -118,6 +130,10 @@ final class ClientConnectionHandlerTests: XCTestCase { let connection = try Connection(keepaliveTime: .minutes(1), keepaliveTimeout: .seconds(10)) try connection.activate() + // Write the initial settings to ready the connection. + try connection.settings([]) + XCTAssertEqual(try connection.readEvent(), .ready) + // Open a stream so keep-alive starts. connection.streamOpened(1) From 7f2a6df1687a64ba87952483bd0311a6cde11d2f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 11 Jun 2024 16:13:30 +0100 Subject: [PATCH 341/580] Add Swift 6 nightly CI (#1919) --- .github/workflows/ci.yaml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 22497f678..04c501c47 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,7 +31,10 @@ jobs: - image: swiftlang/swift:nightly-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.10-jammy + - image: swiftlang/swift:nightly-6.0-jammy + # No TSAN because of: https://github.com/apple/swift/issues/59068 + # swift-test-flags: "--sanitize=thread" + - image: swift:5.10.1-noble # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - image: swift:5.9-jammy @@ -68,7 +71,18 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.10-jammy + - image: swiftlang/swift:nightly-6.0-jammy + swift-version: main + env: + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 + MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 + MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 + MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 + MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 + - image: swift:5.10.1-noble swift-version: '5.10' env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 @@ -124,7 +138,8 @@ jobs: matrix: include: - image: swiftlang/swift:nightly-jammy - - image: swift:5.10-jammy + - image: swiftlang/swift:nightly-6.0-jammy + - image: swift:5.10.1-noble - image: swift:5.9-jammy - image: swift:5.8-focal name: Integration Tests on ${{ matrix.image }} From 0e4aac54e8483ea0f818e76eac7b390149a0ab8e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 11 Jun 2024 16:27:21 +0100 Subject: [PATCH 342/580] Add editorconfig (#1920) Motivation: Different projects use different indentation settings. We can configure project specific settings in '.editorconfig' which is supported in Xcode 16 beta. Modifications: - Add '.editorconfig' with settings for our project Result: Less friction when developing multiple projects with different editor settings. --- .editorconfig | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..48ded96de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# 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. + +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 From 8f919480861beabff8d44fc20cff2669392655b0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 12 Jun 2024 13:01:46 +0100 Subject: [PATCH 343/580] Remove a few retroactive conformance warnings (#1918) Motivation: Swift 6 warns when types retroactively conform to a protocol. There are a few places where we do this, for example in testing where we add 'best effort' equatable conformance. These warnings can be suppressed by marking the conformance as `@retroactive`. Modifications: - Add `@retroactive` to a conformances in tests - Move conformance to `RemovableChannelHandler` to the core module the server stream handler Result: Fewer warnings --- .../Server/GRPCServerStreamHandler.swift | 2 +- .../Connection/Connection+Equatable.swift | 30 ++++++++++++++++--- .../Server/GRPCServerStreamHandlerTests.swift | 3 -- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 2d9cfb17b..db9b3062e 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -19,7 +19,7 @@ import NIOCore import NIOHTTP2 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCServerStreamHandler: ChannelDuplexHandler { +final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandler { typealias InboundIn = HTTP2Frame.FramePayload typealias InboundOut = RPCRequestPart diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift index 63e8a45fd..8e912639c 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift @@ -17,8 +17,30 @@ import GRPCCore @_spi(Package) @testable import GRPCHTTP2Core +// Equatable conformance for these types is 'best effort', this is sufficient for testing but not +// for general use. As such the conformance is added in the test module and must be declared +// as a `@retroactive` conformance. +#if compiler(>=6.0) @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension Connection.Event: Equatable { +extension Connection.Event: @retroactive Equatable {} +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection.CloseReason: @retroactive Equatable {} + +extension ClientConnectionEvent: @retroactive Equatable {} +extension ClientConnectionEvent.CloseReason: @retroactive Equatable {} + +#else +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection.Event: Equatable {} +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection.CloseReason: Equatable {} + +extension ClientConnectionEvent: Equatable {} +extension ClientConnectionEvent.CloseReason: Equatable {} +#endif + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension Connection.Event { public static func == (lhs: Connection.Event, rhs: Connection.Event) -> Bool { switch (lhs, rhs) { case (.connectSucceeded, .connectSucceeded), @@ -38,7 +60,7 @@ extension Connection.Event: Equatable { } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension Connection.CloseReason: Equatable { +extension Connection.CloseReason { public static func == (lhs: Connection.CloseReason, rhs: Connection.CloseReason) -> Bool { switch (lhs, rhs) { case (.idleTimeout, .idleTimeout), @@ -60,7 +82,7 @@ extension Connection.CloseReason: Equatable { } } -extension ClientConnectionEvent: Equatable { +extension ClientConnectionEvent { public static func == (lhs: ClientConnectionEvent, rhs: ClientConnectionEvent) -> Bool { switch (lhs, rhs) { case (.ready, .ready): @@ -73,7 +95,7 @@ extension ClientConnectionEvent: Equatable { } } -extension ClientConnectionEvent.CloseReason: Equatable { +extension ClientConnectionEvent.CloseReason { public static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.goAway(let lhsCode, let lhsMessage), .goAway(let rhsCode, let rhsMessage)): diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index 6c8a0ba75..36999efc7 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -909,6 +909,3 @@ extension EmbeddedChannel { private enum TestError: Error { case assertionFailure(String) } - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCServerStreamHandler: RemovableChannelHandler {} From 05d322f7cecee52477e5e67165d62978915e4fa7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 12 Jun 2024 14:00:19 +0100 Subject: [PATCH 344/580] Don't allow invalid paths (#1917) Motivation: The ":path" pseudo header indicates which RPC is to be called. The format for this is "//". The server currently allows the leading prefix to be missing. Modifications: - Require paths to have a leading prefix Result: Better policing --- .../GRPCStreamStateMachine.swift | 16 +++++++++---- .../Server/GRPCServerStreamHandlerTests.swift | 24 +++++++++---------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 5f166d9f2..b4d8a57cb 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -1508,11 +1508,17 @@ extension GRPCStreamStateMachine { extension MethodDescriptor { init?(path: String) { - let split = path.split(separator: "/") - guard split.count == 2 else { - return nil - } - self.init(service: String(split[0]), method: String(split[1])) + var view = path[...] + guard view.popFirst() == "/" else { return nil } + + // Find the index of the "/" separating the service and method names. + guard var index = view.firstIndex(of: "/") else { return nil } + + let service = String(view[.. Date: Wed, 12 Jun 2024 14:52:41 +0100 Subject: [PATCH 345/580] Add client to v2 interop tests (#1921) --- .../InteroperabilityTestsExecutable.swift | 80 ++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift index 428abd983..81e623265 100644 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -26,7 +26,7 @@ import NIOPosix struct InteroperabilityTestsExecutable: AsyncParsableCommand { static var configuration = CommandConfiguration( abstract: "gRPC Swift Interoperability Runner", - subcommands: [StartServer.self, ListTests.self] + subcommands: [StartServer.self, ListTests.self, RunTests.self] ) struct StartServer: AsyncParsableCommand { @@ -60,4 +60,82 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { } } } + + struct RunTests: AsyncParsableCommand { + static var configuration = CommandConfiguration( + abstract: """ + Run gRPC interoperability tests using a gRPC Swift client. + You can specify a test name as an argument to run a single test. + If no test name is given, all interoperability tests will be run. + """ + ) + + @Option(help: "The host the server is running on") + var host: String + + @Option(help: "The port to connect to") + var port: Int + + @Argument(help: "The name of the tests to run. If none, all tests will be run.") + var testNames: [String] = InteroperabilityTestCase.allCases.map { $0.name } + + func run() async throws { + let client = try self.buildClient(host: self.host, port: self.port) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + for testName in testNames { + guard let testCase = InteroperabilityTestCase(rawValue: testName) else { + print(InteroperabilityTestError.testNotFound(name: testName)) + continue + } + await self.runTest(testCase, using: client) + } + + client.close() + } + } + + private func buildClient(host: String, port: Int) throws -> GRPCClient { + var transportConfig = HTTP2ClientTransport.Posix.Config.defaults + transportConfig.compression.enabledAlgorithms = .all + let serviceConfig = ServiceConfig(loadBalancingConfig: [.roundRobin]) + let transport = try HTTP2ClientTransport.Posix( + target: .ipv4(host: host, port: port), + config: transportConfig, + serviceConfig: serviceConfig + ) + return GRPCClient(transport: transport) + } + + private func runTest( + _ testCase: InteroperabilityTestCase, + using client: GRPCClient + ) async { + print("Running '\(testCase.name)' ... ", terminator: "") + do { + try await testCase.makeTest().run(client: client) + print("PASSED") + } catch { + print("FAILED\n" + String(describing: InteroperabilityTestError.testFailed(cause: error))) + } + } + } +} + +enum InteroperabilityTestError: Error, CustomStringConvertible { + case testNotFound(name: String) + case testFailed(cause: any Error) + + var description: String { + switch self { + case .testNotFound(let name): + return "Test \"\(name)\" not found." + case .testFailed(let cause): + return "Test failed with error: \(String(describing: cause))" + } + } } From b740f8d3788f1652565cfabb4de770fc2799b94d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 12 Jun 2024 15:11:55 +0100 Subject: [PATCH 346/580] Make testConnectionDropWithOpenStreams more reliable (#1922) Motivation: 'testConnectionDropWithOpenStreams' occasionally fails by going into the idle state rather than transient failure when the connection is dropped. This happens because the connection doesn't believe there to be any streams open. There is a race here between the stream being opened and NIO firing the stream opened event. If the connection is dropped between those two events then the test will fail. Modifications: - Wait for the server to respond with metadata before dropping the connection Result: More stability --- .../LoadBalancers/SubchannelTests.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 68c4d7d06..e56065ed9 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -396,11 +396,7 @@ final class SubchannelTests: XCTestCase { } group.addTask { - try await server.run { inbound, outbound in - // Sleep, the RPC will be cancelled at the end of the test because the connection - // will be purposefully dropped. - try await Task.sleep(for: .seconds(300)) - } + try await server.run(.echo) } var events = [Subchannel.Event]() @@ -421,11 +417,17 @@ final class SubchannelTests: XCTestCase { let stream = try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) try await stream.execute { inbound, outbound in try await outbound.write(.metadata([:])) + + // Wait for the metadata to be echo'd back. + var iterator = inbound.makeAsyncIterator() + let _ = try await iterator.next() + // Stream is definitely open. Bork the connection. server.clients.first?.close(mode: .all, promise: nil) - for try await _ in inbound { - () - } + + // Wait for the next message which won't arrive, client won't send a message. The + // stream should fail + let _ = try await iterator.next() } } else if readyCount == 2 { subchannel.close() From 09b30466f65bd80e9d9680563ff2395ebda594ef Mon Sep 17 00:00:00 2001 From: woxtu Date: Thu, 13 Jun 2024 20:54:16 +0900 Subject: [PATCH 347/580] Update links (#1926) --- CODE-OF-CONDUCT.md | 2 +- CONTRIBUTING.md | 4 ++-- GOVERNANCE.md | 2 +- MAINTAINERS.md | 4 ++-- docs/plugin.md | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md index 9d4213ebc..f62af384c 100644 --- a/CODE-OF-CONDUCT.md +++ b/CODE-OF-CONDUCT.md @@ -1,3 +1,3 @@ ## Community Code of Conduct -gRPC follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). +gRPC follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32c9ada58..fe4675837 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ # How to contribute We definitely welcome patches and contributions to grpc-swift! Please read the gRPC -organization's [governance rules](https://github.com/grpc/grpc-community/blob/master/governance.md) -and [contribution guidelines](https://github.com/grpc/grpc-community/blob/master/CONTRIBUTING.md) before proceeding. +organization's [governance rules](https://github.com/grpc/grpc-community/blob/main/governance.md) +and [contribution guidelines](https://github.com/grpc/grpc-community/blob/main/CONTRIBUTING.md) before proceeding. Here are some guidelines and information about how to participate. diff --git a/GOVERNANCE.md b/GOVERNANCE.md index d6ff26747..c75441937 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1 +1 @@ -This repository is governed by the gRPC organization's [governance rules](https://github.com/grpc/grpc-community/blob/master/governance.md). +This repository is governed by the gRPC organization's [governance rules](https://github.com/grpc/grpc-community/blob/main/governance.md). diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 07125030a..08702817f 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -2,9 +2,9 @@ This page lists all active maintainers of this repository. If you were a maintainer and would like to add your name to the Emeritus list, please send us a PR. -See [GOVERNANCE.md](https://github.com/grpc/grpc-community/blob/master/governance.md) +See [GOVERNANCE.md](https://github.com/grpc/grpc-community/blob/main/governance.md) for governance guidelines and how to become a maintainer. -See [CONTRIBUTING.md](https://github.com/grpc/grpc-community/blob/master/CONTRIBUTING.md) +See [CONTRIBUTING.md](https://github.com/grpc/grpc-community/blob/main/CONTRIBUTING.md) for general contribution guidelines. ## Maintainers (in alphabetical order) diff --git a/docs/plugin.md b/docs/plugin.md index 0d3cbcb89..06da5dac6 100644 --- a/docs/plugin.md +++ b/docs/plugin.md @@ -119,6 +119,6 @@ protoc --grpc-swift_opt=Client=true,Server=false --grpc-swift_out=. ``` [protocol-buffers]: https://developers.google.com/protocol-buffers/docs/overview -[swift-protobuf-filenaming]: https://github.com/apple/swift-protobuf/blob/master/Documentation/PLUGIN.md#generation-option-filenaming---naming-of-generated-sources -[swift-protobuf-module-mappings]: https://github.com/apple/swift-protobuf/blob/master/Documentation/PLUGIN.md#generation-option-protopathmodulemappings---swift-module-names-for-proto-paths +[swift-protobuf-filenaming]: https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md#generation-option-filenaming---naming-of-generated-sources +[swift-protobuf-module-mappings]: https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md#generation-option-protopathmodulemappings---swift-module-names-for-proto-paths [swift-protobuf-module-name]: https://github.com/apple/swift-protobuf/commit/9df381f72ff22062080d434e9c2f68e71ee44298#diff-1b08f0a80bd568509049d851b8d8af90d1f2db3cd8711eaba974b5380cd59bf3 From 362126d98e64fe30a85bbc5d51668102b68b7f41 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 13 Jun 2024 13:18:55 +0100 Subject: [PATCH 348/580] Fix a few server state machine transitions (#1924) Motivation: The server stream state machine had a few incorrect or missing state transitions. Modification: - Add missing transition when receiving headers - Close the stream if the client misbehaves - Don't read inbound when the server has closed - Read inbound when the client is closed and the server isn't Result: Fewer bugs --- .../Client/GRPCClientStreamHandler.swift | 7 + .../GRPCStreamStateMachine.swift | 253 ++++++++---------- .../Server/GRPCServerStreamHandler.swift | 6 + .../GRPCStreamStateMachineTests.swift | 54 ++-- .../Server/GRPCServerStreamHandlerTests.swift | 32 +++ .../Test Utilities/XCTest+Utilities.swift | 6 +- 6 files changed, 187 insertions(+), 171 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index ac00a9531..0fa723554 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -87,6 +87,9 @@ extension GRPCClientStreamHandler { break loop } } + + case .doNothing: + () } } catch { context.fireErrorCaught(error) @@ -116,6 +119,10 @@ extension GRPCClientStreamHandler { context.fireChannelRead(self.wrapInboundOut(.status(status, metadata))) context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + case .protocolViolation: + // Should only happen for servers + assertionFailure("Unexpected protocol violation") + case .doNothing: () } diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index b4d8a57cb..0158fbcc8 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -414,6 +414,7 @@ struct GRPCStreamStateMachine { // Server-specific actions case rejectRPC(trailers: HPACKHeaders) + case protocolViolation } mutating func receive(headers: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { @@ -435,6 +436,7 @@ struct GRPCStreamStateMachine { enum OnBufferReceivedAction: Equatable { case readInbound + case doNothing // Client-specific actions @@ -1114,46 +1116,6 @@ extension GRPCStreamStateMachine { } } - private func makeTrailers( - status: Status, - customMetadata: Metadata?, - trailersOnly: Bool - ) -> HPACKHeaders { - // Trailers always contain the grpc-status header, and optionally, - // grpc-message, and custom metadata. - // If it's a trailers-only response, they will also contain :status and - // content-type. - var headers = HPACKHeaders() - let customMetadataCount = customMetadata?.count ?? 0 - if trailersOnly { - // Reserve 4 for capacity: 3 for the required headers, and 1 for the - // optional status message. - headers.reserveCapacity(4 + customMetadataCount) - headers.add("200", forKey: .status) - headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) - } else { - // Reserve 2 for capacity: one for the required grpc-status, and - // one for the optional message. - headers.reserveCapacity(2 + customMetadataCount) - } - - headers.add(String(status.code.rawValue), forKey: .grpcStatus) - - if !status.message.isEmpty { - if let percentEncodedMessage = GRPCStatusMessageMarshaller.marshall(status.message) { - headers.add(percentEncodedMessage, forKey: .grpcStatusMessage) - } - } - - if let customMetadata { - for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) - } - } - - return headers - } - private mutating func serverSend( status: Status, customMetadata: Metadata @@ -1162,32 +1124,16 @@ extension GRPCStreamStateMachine { switch self.state { case .clientOpenServerOpen(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) - return self.makeTrailers( - status: status, - customMetadata: customMetadata, - trailersOnly: false - ) + return .trailers(status: status, metadata: customMetadata) case .clientClosedServerOpen(let state): self.state = .clientClosedServerClosed(.init(previousState: state)) - return self.makeTrailers( - status: status, - customMetadata: customMetadata, - trailersOnly: false - ) + return .trailers(status: status, metadata: customMetadata) case .clientOpenServerIdle(let state): self.state = .clientOpenServerClosed(.init(previousState: state)) - return self.makeTrailers( - status: status, - customMetadata: customMetadata, - trailersOnly: true - ) + return .trailersOnly(status: status, metadata: customMetadata) case .clientClosedServerIdle(let state): self.state = .clientClosedServerClosed(.init(previousState: state)) - return self.makeTrailers( - status: status, - customMetadata: customMetadata, - trailersOnly: true - ) + return .trailersOnly(status: status, metadata: customMetadata) case .clientIdleServerIdle: try self.invalidState( "Server can't send status if client is idle." @@ -1199,26 +1145,22 @@ extension GRPCStreamStateMachine { } } - mutating private func closeServerAndBuildRejectRPCAction( - currentState: GRPCStreamStateMachineState.ClientIdleServerIdleState, - endStream: Bool, - rejectWithStatus status: Status - ) -> OnMetadataReceived { - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: currentState)) - } else { - self.state = .clientOpenServerClosed(.init(previousState: currentState)) - } - - let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) - return .rejectRPC(trailers: trailers) - } - private mutating func serverReceive( headers: HPACKHeaders, endStream: Bool, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration ) throws -> OnMetadataReceived { + func closeServer( + from state: GRPCStreamStateMachineState.ClientIdleServerIdleState, + endStream: Bool + ) -> GRPCStreamStateMachineState { + if endStream { + return .clientClosedServerClosed(.init(previousState: state)) + } else { + return .clientOpenServerClosed(.init(previousState: state)) + } + } + switch self.state { case .clientIdleServerIdle(let state): let contentType = headers.firstString(forKey: .contentType) @@ -1233,10 +1175,9 @@ extension GRPCStreamStateMachine { } guard let pathHeader = headers.firstString(forKey: .path) else { - return self.closeServerAndBuildRejectRPCAction( - currentState: state, - endStream: endStream, - rejectWithStatus: Status( + self.state = closeServer(from: state, endStream: endStream) + return .rejectRPC( + trailers: .trailersOnly( code: .invalidArgument, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." ) @@ -1244,10 +1185,9 @@ extension GRPCStreamStateMachine { } guard let path = MethodDescriptor(path: pathHeader) else { - return self.closeServerAndBuildRejectRPCAction( - currentState: state, - endStream: endStream, - rejectWithStatus: Status( + self.state = closeServer(from: state, endStream: endStream) + return .rejectRPC( + trailers: .trailersOnly( code: .unimplemented, message: "The given \(GRPCHTTP2Keys.path.rawValue) (\(pathHeader)) does not correspond to a valid method." @@ -1258,10 +1198,9 @@ extension GRPCStreamStateMachine { let scheme = headers.firstString(forKey: .scheme) .flatMap { GRPCStreamStateMachineConfiguration.Scheme(rawValue: $0) } if scheme == nil { - return self.closeServerAndBuildRejectRPCAction( - currentState: state, - endStream: endStream, - rejectWithStatus: Status( + self.state = closeServer(from: state, endStream: endStream) + return .rejectRPC( + trailers: .trailersOnly( code: .invalidArgument, message: ":scheme header must be present and one of \"http\" or \"https\"." ) @@ -1269,10 +1208,9 @@ extension GRPCStreamStateMachine { } guard let method = headers.firstString(forKey: .method), method == "POST" else { - return self.closeServerAndBuildRejectRPCAction( - currentState: state, - endStream: endStream, - rejectWithStatus: Status( + self.state = closeServer(from: state, endStream: endStream) + return .rejectRPC( + trailers: .trailersOnly( code: .invalidArgument, message: ":method header is expected to be present and have a value of \"POST\"." ) @@ -1280,10 +1218,9 @@ extension GRPCStreamStateMachine { } guard let te = headers.firstString(forKey: .te), te == "trailers" else { - return self.closeServerAndBuildRejectRPCAction( - currentState: state, - endStream: endStream, - rejectWithStatus: Status( + self.state = closeServer(from: state, endStream: endStream) + return .rejectRPC( + trailers: .trailersOnly( code: .invalidArgument, message: "\"te\" header is expected to be present and have a value of \"trailers\"." ) @@ -1300,36 +1237,31 @@ extension GRPCStreamStateMachine { var encodingValuesIterator = encodingValues.makeIterator() if let rawEncoding = encodingValuesIterator.next() { guard encodingValuesIterator.next() == nil else { - let status = Status( - code: .internalError, - message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + self.state = closeServer(from: state, endStream: endStream) + return .rejectRPC( + trailers: .trailersOnly( + code: .internalError, + message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." + ) ) - let trailers = self.makeTrailers(status: status, customMetadata: nil, trailersOnly: true) - return .rejectRPC(trailers: trailers) } guard let clientEncoding = CompressionAlgorithm(name: rawEncoding), configuration.acceptedEncodings.contains(clientEncoding) else { - let statusMessage = """ - \(rawEncoding) compression is not supported; \ - supported algorithms are listed in grpc-accept-encoding - """ + self.state = closeServer(from: state, endStream: endStream) + var trailers = HPACKHeaders.trailersOnly( + code: .unimplemented, + message: """ + \(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 - ) + trailers.add(name: GRPCHTTP2Keys.acceptEncoding.rawValue, value: acceptedEncoding.name) } - let trailers = self.makeTrailers( - status: Status(code: .unimplemented, message: statusMessage), - customMetadata: customMetadata, - trailersOnly: true - ) return .rejectRPC(trailers: trailers) } @@ -1386,14 +1318,13 @@ extension GRPCStreamStateMachine { } return .receivedMetadata(Metadata(headers: headers), path) + case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - try self.invalidState( - "Client shouldn't have sent metadata twice." - ) + // Metadata has already been received, should only be sent once by clients. + return .protocolViolation + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState( - "Client can't have sent metadata if closed." - ) + try self.invalidState("Client can't have sent metadata if closed.") } } @@ -1401,11 +1332,12 @@ extension GRPCStreamStateMachine { buffer: ByteBuffer, endStream: Bool ) throws -> OnBufferReceivedAction { + let action: OnBufferReceivedAction + switch self.state { case .clientIdleServerIdle: - try self.invalidState( - "Can't have received a message if client is idle." - ) + try self.invalidState("Can't have received a message if client is idle.") + case .clientOpenServerIdle(var state): // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. @@ -1418,6 +1350,9 @@ extension GRPCStreamStateMachine { } else { self.state = .clientOpenServerIdle(state) } + + action = .readInbound + case .clientOpenServerOpen(var state): try state.deframer.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) @@ -1428,6 +1363,9 @@ extension GRPCStreamStateMachine { } else { self.state = .clientOpenServerOpen(state) } + + action = .readInbound + case .clientOpenServerClosed(let state): // Client is not done sending request, but server has already closed. // Ignore the rest of the request: do nothing, unless endStream is set, @@ -1435,12 +1373,14 @@ extension GRPCStreamStateMachine { if endStream { self.state = .clientClosedServerClosed(.init(previousState: state)) } + + action = .doNothing + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState( - "Client can't send a message if closed." - ) + try self.invalidState("Client can't send a message if closed.") } - return .readInbound + + return action } private mutating func serverNextOutboundFrame() throws -> OnNextOutboundFrame { @@ -1482,24 +1422,26 @@ extension GRPCStreamStateMachine { let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerIdle(state) return request.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientOpenServerOpen(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) return request.map { .receiveMessage($0) } ?? .awaitMoreMessages - case .clientOpenServerClosed(var state): + + case .clientClosedServerIdle(var state): let request = state.inboundMessageBuffer.pop() - self.state = .clientOpenServerClosed(state) - return request.map { .receiveMessage($0) } ?? .awaitMoreMessages + self.state = .clientClosedServerIdle(state) + return request.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientClosedServerOpen(var state): let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) return request.map { .receiveMessage($0) } ?? .noMoreMessages - case .clientClosedServerClosed(var state): - let request = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerClosed(state) - return request.map { .receiveMessage($0) } ?? .noMoreMessages - case .clientClosedServerIdle: + + case .clientOpenServerClosed, .clientClosedServerClosed: + // Server has closed, no need to read. return .noMoreMessages + case .clientIdleServerIdle: return .awaitMoreMessages } @@ -1546,6 +1488,45 @@ extension HPACKHeaders { internal mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { self.add(name: key.rawValue, value: value) } + + static func trailersOnly(code: Status.Code, message: String, metadata: Metadata = [:]) -> Self { + return .trailersOnly(status: Status(code: code, message: message), metadata: metadata) + } + + static func trailersOnly(status: Status, metadata: Metadata = [:]) -> Self { + return .makeTrailers(isTrailersOnly: true, status: status, metadata: metadata) + } + + static func trailers(status: Status, metadata: Metadata = [:]) -> Self { + return .makeTrailers(isTrailersOnly: false, status: status, metadata: metadata) + } + + private static func makeTrailers( + isTrailersOnly: Bool, + status: Status, + metadata: Metadata + ) -> Self { + var trailers = HPACKHeaders() + + if isTrailersOnly { + trailers.reserveCapacity(4 + metadata.count) + trailers.add("200", forKey: .status) + trailers.add(ContentType.grpc.canonicalValue, forKey: .contentType) + } else { + trailers.reserveCapacity(2 + metadata.count) + } + + trailers.add(String(status.code.rawValue), forKey: .grpcStatus) + if !status.message.isEmpty, let encoded = GRPCStatusMessageMarshaller.marshall(status.message) { + trailers.add(encoded, forKey: .grpcStatusMessage) + } + + for (key, value) in metadata { + trailers.add(name: key, value: value.encoded()) + } + + return trailers + } } extension Zlib.Method { diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index db9b3062e..14e7f838f 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -85,6 +85,8 @@ extension GRPCServerStreamHandler { break loop } } + case .doNothing: + () } } catch { context.fireErrorCaught(error) @@ -126,6 +128,10 @@ extension GRPCServerStreamHandler { message: "Server cannot get receivedStatusAndMetadata." ) + case .protocolViolation: + context.writeAndFlush(self.wrapOutboundOut(.rstStream(.protocolError)), promise: nil) + context.close(promise: nil) + case .doNothing: throw RPCError(code: .internalError, message: "Server cannot receive doNothing.") } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 502c24942..e0804b737 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -565,7 +565,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "custom-key": "custom-value", ] ) - case .receivedMetadata, .doNothing, .rejectRPC: + case .receivedMetadata, .doNothing, .rejectRPC, .protocolViolation: XCTFail("Expected .receivedStatusAndMetadata") } } @@ -640,7 +640,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "custom-key": "custom-value", ] ) - case .receivedMetadata, .doNothing, .rejectRPC: + case .receivedMetadata, .doNothing, .rejectRPC, .protocolViolation: XCTFail("Expected .receivedStatusAndMetadata") } } @@ -1922,38 +1922,23 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { func testReceiveMetadataWhenClientOpenAndServerIdle() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - // Try receiving initial metadata again - should fail - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") - } + // Try receiving initial metadata again - should be a protocol violation + let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) + XCTAssertEqual(action, .protocolViolation) } func testReceiveMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") - } + let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) + XCTAssertEqual(action, .protocolViolation) } - func testReceiveMetadataWhenClientOpenAndServerClosed() { + func testReceiveMetadataWhenClientOpenAndServerClosed() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client shouldn't have sent metadata twice.") - } + let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) + XCTAssertEqual(action, .protocolViolation) } func testReceiveMetadataWhenClientClosedAndServerIdle() { @@ -2100,7 +2085,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) // Client is not done sending request, don't fail. - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) + XCTAssertEqual(try stateMachine.receive(buffer: ByteBuffer(), endStream: false), .doNothing) } func testReceiveMessageWhenClientClosedAndServerIdle() { @@ -2372,12 +2357,17 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { ) ) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) + XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } - func testNextInboundMessageWhenClientClosedAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) + func testNextInboundMessageWhenClientClosedAndServerIdle() throws { + var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) + let action = try stateMachine.receive( + buffer: ByteBuffer(repeating: 0, count: 5), + endStream: true + ) + XCTAssertEqual(action, .readInbound) + XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([])) XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } @@ -2427,9 +2417,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Close client XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - // Even though the client and server are closed, because the server received - // a message while the client was still open, we must get the message now. - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) + // The server is closed, the message should be dropped. XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index e96007795..f50b85cb1 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -625,6 +625,38 @@ final class GRPCServerStreamHandlerTests: XCTestCase { ) } + func testReceiveMultipleHeaders() throws { + let channel = EmbeddedChannel() + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) + ) + try channel.pipeline.syncOperations.addHandler(handler) + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "/test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + try channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata))) + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + // Receive them again. Should be a protocol violation. + try channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata))) + let payload = try XCTUnwrap(channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + + switch payload { + case .rstStream(let errorCode): + XCTAssertEqual(errorCode, .protocolError) + default: + XCTFail("Expected RST_STREAM, got \(payload)") + } + } + func testSendMultipleMessagesInSingleBuffer() throws { let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift index 1e0bb3515..2e036f985 100644 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift @@ -18,12 +18,14 @@ import XCTest func XCTAssertThrowsError( ofType: E.Type, + file: StaticString = #filePath, + line: UInt = #line, _ expression: @autoclosure () throws -> T, _ errorHandler: (E) -> Void ) { - XCTAssertThrowsError(try expression()) { error in + XCTAssertThrowsError(try expression(), file: file, line: line) { error in guard let error = error as? E else { - return XCTFail("Error had unexpected type '\(type(of: error))'") + return XCTFail("Error had unexpected type '\(type(of: error))'", file: file, line: line) } errorHandler(error) } From 261aea9476a5170ac77cd9d495728a5d779aa0a7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 13 Jun 2024 13:40:59 +0100 Subject: [PATCH 349/580] Add 'cause' to error description if present (#1925) Motivation: A description of an `RPCError`'s `cause` isn't included in its description if present, it should be to provide more context. Modifications: - Add cause to the description if present Result: More context in errors --- Sources/GRPCCore/RPCError.swift | 6 +++++- Tests/GRPCCoreTests/RPCErrorTests.swift | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 2113d9776..72223bf3b 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -74,7 +74,11 @@ public struct RPCError: Sendable, Hashable, Error { extension RPCError: CustomStringConvertible { public var description: String { - "\(self.code): \"\(self.message)\"" + if let cause = self.cause { + return "\(self.code): \"\(self.message)\" (cause: \"\(cause)\")" + } else { + return "\(self.code): \"\(self.message)\"" + } } } diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift index 87949ccdd..3dcac25d6 100644 --- a/Tests/GRPCCoreTests/RPCErrorTests.swift +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -40,6 +40,10 @@ final class RPCErrorTests: XCTestCase { XCTAssertDescription(RPCError(code: .dataLoss, message: ""), #"dataLoss: """#) XCTAssertDescription(RPCError(code: .unknown, message: "message"), #"unknown: "message""#) XCTAssertDescription(RPCError(code: .aborted, message: "message"), #"aborted: "message""#) + XCTAssertDescription( + RPCError(code: .aborted, message: "message", cause: CancellationError()), + #"aborted: "message" (cause: "CancellationError()")"# + ) } func testErrorFromStatus() throws { From 6dad5bc6b6e88332190df6daaa28a08230be0b89 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 13 Jun 2024 14:02:46 +0100 Subject: [PATCH 350/580] Allow single response to contain initial metadata and error (#1927) Motivation: Currently the client API doesn't allow for users to distinguish between receiving metadata and a failed status and just a failed status. The distinction being that the server "accepted" the former request but it resulted in failure and the rejected the latter. Modifications: - Allow this to be represented by turning the message in the contents to be a result Result: More flexible API --- .../GRPCCore/Call/Client/ClientResponse.swift | 36 ++++++++++++++++--- .../Internal/ClientResponse+Convenience.swift | 2 +- .../Call/Client/ClientResponseTests.swift | 11 ++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index 49ea3973c..967b09050 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -59,7 +59,7 @@ extension ClientResponse { /// // The explicit API: /// switch response { /// case .success(let contents): - /// print("Received response with message '\(contents.message)'") + /// print("Received response with message '\(try contents.message.get())'") /// case .failure(let error): /// print("RPC failed with code '\(error.code)'") /// } @@ -80,8 +80,9 @@ extension ClientResponse { /// level metadata provided by the service. public var metadata: Metadata - /// The response message received from the server. - public var message: Message + /// The response message received from the server, or an error of the RPC failed with a + /// non-ok status. + public var message: Result /// Metadata received from the server at the end of the response. /// @@ -101,9 +102,23 @@ extension ClientResponse { trailingMetadata: Metadata ) { self.metadata = metadata - self.message = message + self.message = .success(message) self.trailingMetadata = trailingMetadata } + + /// Creates a `Contents`. + /// + /// - Parameters: + /// - metadata: Metadata received from the server at the beginning of the response. + /// - error: Error received from the server. + public init( + metadata: Metadata, + error: RPCError + ) { + self.metadata = metadata + self.message = .failure(error) + self.trailingMetadata = error.metadata + } } /// Whether the RPC was accepted or rejected. @@ -263,6 +278,17 @@ extension ClientResponse.Single { self.accepted = .success(contents) } + /// Creates a new accepted response with a failed outcome. + /// + /// - Parameters: + /// - messageType: The type of message. + /// - metadata: Metadata received from the server at the beginning of the response. + /// - error: An error describing why the RPC failed. + public init(of messageType: Message.Type = Message.self, metadata: Metadata, error: RPCError) { + let contents = Contents(metadata: metadata, error: error) + self.accepted = .success(contents) + } + /// Creates a new failed response. /// /// - Parameters: @@ -289,7 +315,7 @@ extension ClientResponse.Single { /// - Throws: ``RPCError`` if the request failed. public var message: Message { get throws { - try self.accepted.map { $0.message }.get() + try self.accepted.flatMap { $0.message }.get() } } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift index 505a43af4..281d23dd2 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -70,7 +70,7 @@ extension ClientResponse.Single { } } catch let error as RPCError { // Known error type. - self.accepted = .failure(error) + self.accepted = .success(Contents(metadata: contents.metadata, error: error)) } catch { // Unexpected, but should be handled nonetheless. self.accepted = .failure(RPCError(code: .unknown, message: String(describing: error))) diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index 72f1e1c1f..d435c6fb8 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -43,6 +43,17 @@ final class ClientResponseTests: XCTestCase { XCTAssertEqual(response.trailingMetadata, ["bar": "baz"]) } + func testAcceptedButFailedSingleResponseConvenienceMethods() { + let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"]) + let response = ClientResponse.Single(of: String.self, metadata: ["foo": "bar"], error: error) + + XCTAssertEqual(response.metadata, ["foo": "bar"]) + XCTAssertThrowsRPCError(try response.message) { + XCTAssertEqual($0, error) + } + XCTAssertEqual(response.trailingMetadata, ["bar": "baz"]) + } + func testAcceptedStreamResponseConvenienceMethods() async throws { let response = ClientResponse.Stream( of: String.self, From f2ade1b6c9fa9717a0947bc7f82108f14b299fed Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 13 Jun 2024 22:44:05 +0100 Subject: [PATCH 351/580] Fix availaibility in RPCError test (#1929) --- Tests/GRPCCoreTests/RPCErrorTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift index 3dcac25d6..dc65122b0 100644 --- a/Tests/GRPCCoreTests/RPCErrorTests.swift +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -40,9 +40,11 @@ final class RPCErrorTests: XCTestCase { XCTAssertDescription(RPCError(code: .dataLoss, message: ""), #"dataLoss: """#) XCTAssertDescription(RPCError(code: .unknown, message: "message"), #"unknown: "message""#) XCTAssertDescription(RPCError(code: .aborted, message: "message"), #"aborted: "message""#) + + struct TestError: Error {} XCTAssertDescription( - RPCError(code: .aborted, message: "message", cause: CancellationError()), - #"aborted: "message" (cause: "CancellationError()")"# + RPCError(code: .aborted, message: "message", cause: TestError()), + #"aborted: "message" (cause: "TestError()")"# ) } From ae8207457dc4f0fc7118fc2fb8ff29d4e2f7e69b Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 14 Jun 2024 07:40:58 +0100 Subject: [PATCH 352/580] Add missing v2 interop tests (#1928) * Add missing v2 interop tests * PR changes --- .../GRPCStreamStateMachine.swift | 27 +- .../InteroperabilityTestCase.swift | 27 +- .../InteroperabilityTestCases.swift | 325 +++++++++++++++++- .../GRPCStreamStateMachineTests.swift | 5 + 4 files changed, 361 insertions(+), 23 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 0158fbcc8..78d887b7d 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -192,6 +192,24 @@ private enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + /// This transition should only happen on the client-side. + /// It can happen if the request times out before the client outbound can be opened, or if the stream is + /// unexpectedly closed for some other reason on the client before it can transition to open. + init(previousState: ClientIdleServerIdleState) { + self.maximumPayloadSize = previousState.maximumPayloadSize + // We don't need a compressor since we won't be sending any messages. + self.framer = GRPCMessageFramer() + self.compressor = nil + self.outboundCompression = .none + + // We haven't received anything from the server. + self.deframer = nil + self.decompressor = nil + + self.inboundMessageBuffer = .init() + } + + /// This transition should only happen on the server-side. /// We are closing the client as soon as it opens (i.e., endStream was set when receiving the client's /// initial metadata). We don't need to know a decompression algorithm, since we won't receive /// any more messages from the client anyways, as it's closed. @@ -625,8 +643,8 @@ extension GRPCStreamStateMachine { private mutating func clientCloseOutbound() throws { switch self.state { - case .clientIdleServerIdle: - try self.invalidState("Client not yet open.") + case .clientIdleServerIdle(let state): + self.state = .clientClosedServerIdle(.init(previousState: state)) case .clientOpenServerIdle(let state): self.state = .clientClosedServerIdle(.init(previousState: state)) case .clientOpenServerOpen(let state): @@ -645,16 +663,19 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle: try self.invalidState("Client is not open yet.") + case .clientOpenServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } ?? .awaitMoreMessages + case .clientOpenServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } ?? .awaitMoreMessages + case .clientClosedServerIdle(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) @@ -663,6 +684,7 @@ extension GRPCStreamStateMachine { } else { return .noMoreMessages } + case .clientClosedServerOpen(var state): let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) @@ -671,6 +693,7 @@ extension GRPCStreamStateMachine { } else { return .noMoreMessages } + case .clientOpenServerClosed, .clientClosedServerClosed: // No point in sending any more requests if the server is closed. return .noMoreMessages diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index 90a34d287..d36d8f32d 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -27,23 +27,23 @@ public protocol InteroperabilityTest { func run(client: GRPCClient) async throws } -/// Test cases as listed by the [gRPC interoperability test description -/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). +/// Test cases as listed by the [gRPC interoperability test description specification] +/// (https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). /// /// This is not a complete list, the following tests have not been implemented: -/// - cacheable_unary -/// - client-compressed-unary -/// - server-compressed-unary -/// - client_compressed_streaming -/// - server_compressed_streaming +/// - cacheable_unary (caching not supported) +/// - cancel_after_begin (if the client cancels the task running the request, there's no response to be +/// received, so we can't check we got back a Cancelled status code) +/// - cancel_after_first_response (same reason as above) +/// - client_compressed_streaming (we don't support per-message compression, so we can't implement this) /// - compute_engine_creds /// - jwt_token_creds /// - oauth2_auth_token /// - per_rpc_creds /// - google_default_credentials /// - compute_engine_channel_credentials -/// - cancel_after_begin -/// - cancel_after_first_response +/// - timeout_on_sleeping_server (timeouts end up being surfaced as `CancellationError`s, so we +/// can't really implement this test) /// /// Note: Tests for compression have not been implemented yet as compression is /// not supported. Once the API which allows for compression will be implemented @@ -51,8 +51,11 @@ public protocol InteroperabilityTest { public enum InteroperabilityTestCase: String, CaseIterable { case emptyUnary = "empty_unary" case largeUnary = "large_unary" + case clientCompressedUnary = "client_compressed_unary" + case serverCompressedUnary = "server_compressed_unary" case clientStreaming = "client_streaming" case serverStreaming = "server_streaming" + case serverCompressedStreaming = "server_compressed_streaming" case pingPong = "ping_pong" case emptyStream = "empty_stream" case customMetadata = "custom_metadata" @@ -75,10 +78,16 @@ extension InteroperabilityTestCase { return EmptyUnary() case .largeUnary: return LargeUnary() + case .clientCompressedUnary: + return ClientCompressedUnary() + case .serverCompressedUnary: + return ServerCompressedUnary() case .clientStreaming: return ClientStreaming() case .serverStreaming: return ServerStreaming() + case .serverCompressedStreaming: + return ServerCompressedStreaming() case .pingPong: return PingPong() case .emptyStream: diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index cfebda759..f2b320ea0 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -88,6 +88,214 @@ struct LargeUnary: InteroperabilityTest { } } +/// This test verifies the client can compress unary messages by sending two unary calls, for +/// compressed and uncompressed payloads. It also sends an initial probing request to verify +/// whether the server supports the CompressedRequest feature by checking if the probing call +/// fails with an `INVALID_ARGUMENT` status. +/// +/// Server features: +/// - UnaryCall +/// - CompressedRequest +/// +/// Procedure: +/// 1. Client calls UnaryCall with the feature probe, an *uncompressed* message: +/// ``` +/// { +/// expect_compressed:{ +/// value: true +/// } +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// 2. Client calls UnaryCall with the *compressed* message: +/// ``` +/// { +/// expect_compressed:{ +/// value: true +/// } +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// 3. Client calls UnaryCall with the *uncompressed* message: +/// ``` +/// { +/// expect_compressed:{ +/// value: false +/// } +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - First call failed with `INVALID_ARGUMENT` status. +/// - Subsequent calls were successful. +/// - Response payload body is 314159 bytes in size. +/// - Clients are free to assert that the response payload body contents are zeros and comparing the +/// entire response message against a golden response. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +class ClientCompressedUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let compressedRequest = Grpc_Testing_SimpleRequest.with { request in + request.expectCompressed = .with { $0.value = true } + request.responseSize = 314_159 + request.payload = .with { $0.body = Data(repeating: 0, count: 271_828) } + } + + var uncompressedRequest = compressedRequest + uncompressedRequest.expectCompressed = .with { $0.value = false } + + // For unary RPCs we disable compression at the call level. + var options = CallOptions.defaults + + // With compression expected but *disabled*. + options.compression = CompressionAlgorithm.none + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: compressedRequest), + options: options + ) { response in + switch response.accepted { + case .success: + throw AssertionFailure(message: "The result should be an error.") + case .failure(let error): + try assertEqual(error.code, .invalidArgument) + } + } + + // With compression expected and enabled. + options.compression = .gzip + + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: compressedRequest), + options: options + ) { response in + switch response.accepted { + case .success(let success): + try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) + case .failure: + throw AssertionFailure(message: "Response should have been accepted.") + } + } + + // With compression not expected and disabled. + options.compression = CompressionAlgorithm.none + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: uncompressedRequest), + options: options + ) { response in + switch response.accepted { + case .success(let success): + try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) + case .failure: + throw AssertionFailure(message: "Response should have been accepted.") + } + } + } +} + +/// This test verifies the server can compress unary messages. It sends two unary +/// requests, expecting the server's response to be compressed or not according to +/// the `response_compressed` boolean. +/// +/// Whether compression was actually performed is determined by the compression bit +/// in the response's message flags. *Note that some languages may not have access +/// to the message flags, in which case the client will be unable to verify that +/// the `response_compressed` boolean is obeyed by the server*. +/// +/// +/// Server features: +/// - UnaryCall +/// - CompressedResponse +/// +/// Procedure: +/// 1. Client calls UnaryCall with `SimpleRequest`: +/// ``` +/// { +/// response_compressed:{ +/// value: true +/// } +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// ``` +/// { +/// response_compressed:{ +/// value: false +/// } +/// response_size: 314159 +/// payload:{ +/// body: 271828 bytes of zeros +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - if supported by the implementation, when `response_compressed` is true, the response MUST have +/// the compressed message flag set. +/// - if supported by the implementation, when `response_compressed` is false, the response MUST NOT +/// have the compressed message flag set. +/// - response payload body is 314159 bytes in size in both cases. +/// - clients are free to assert that the response payload body contents are zero and comparing the +/// entire response message against a golden response +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +class ServerCompressedUnary: InteroperabilityTest { + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + + let compressedRequest = Grpc_Testing_SimpleRequest.with { request in + request.responseCompressed = .with { $0.value = true } + request.responseSize = 314_159 + request.payload = .with { $0.body = Data(repeating: 0, count: 271_828) } + } + + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: compressedRequest) + ) { response in + // We can't verify that the compression bit was set, instead we verify that the encoding header + // was sent by the server. This isn't quite the same since as it can still be set but the + // compression may _not_ be set. + try assertTrue(response.metadata["grpc-encoding"].contains { $0 != "identity" }) + + switch response.accepted { + case .success(let success): + try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) + case .failure: + throw AssertionFailure(message: "Response should have been accepted.") + } + } + + var uncompressedRequest = compressedRequest + uncompressedRequest.responseCompressed.value = false + try await testServiceClient.unaryCall( + request: ClientRequest.Single(message: compressedRequest) + ) { response in + // We can't even check for the 'grpc-encoding' header here since it could be set with the + // compression bit on the message not set. + switch response.accepted { + case .success(let success): + try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) + case .failure: + throw AssertionFailure( + message: "Response should have been accepted." + ) + } + } + } +} + /// This test verifies that client-only streaming succeeds. /// /// Server features: @@ -217,6 +425,103 @@ struct ServerStreaming: InteroperabilityTest { } } +/// This test verifies that the server can compress streaming messages and disable compression on +/// individual messages, expecting the server's response to be compressed or not according to the +/// `response_compressed` boolean. +/// +/// Whether compression was actually performed is determined by the compression bit in the +/// response's message flags. *Note that some languages may not have access to the message flags, in +/// which case the client will be unable to verify that the `response_compressed` boolean is obeyed +/// by the server*. +/// +/// Server features: +/// - StreamingOutputCall +/// - CompressedResponse +/// +/// Procedure: +/// 1. Client calls StreamingOutputCall with `StreamingOutputCallRequest`: +/// ``` +/// { +/// response_parameters:{ +/// compressed: { +/// value: true +/// } +/// size: 31415 +/// } +/// response_parameters:{ +/// compressed: { +/// value: false +/// } +/// size: 92653 +/// } +/// } +/// ``` +/// +/// Client asserts: +/// - call was successful +/// - exactly two responses +/// - if supported by the implementation, when `response_compressed` is false, the response's +/// messages MUST NOT have the compressed message flag set. +/// - if supported by the implementation, when `response_compressed` is true, the response's +/// messages MUST have the compressed message flag set. +/// - response payload bodies are sized (in order): 31415, 92653 +/// - clients are free to assert that the response payload body contents are zero and comparing the +/// entire response messages against golden responses +class ServerCompressedStreaming: InteroperabilityTest { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + func run(client: GRPCClient) async throws { + let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let request: Grpc_Testing_StreamingOutputCallRequest = .with { request in + request.responseParameters = [ + .with { + $0.compressed = .with { $0.value = true } + $0.size = 31415 + }, + .with { + $0.compressed = .with { $0.value = false } + $0.size = 92653 + }, + ] + } + let responseSizes = [31415, 92653] + + try await testServiceClient.streamingOutputCall( + request: ClientRequest.Single(message: request) + ) { response in + var payloads = [Grpc_Testing_Payload]() + + switch response.accepted { + case .success(let success): + // We can't verify that the compression bit was set, instead we verify that the encoding header + // was sent by the server. This isn't quite the same since as it can still be set but the + // compression may be not set. + try assertTrue(success.metadata["grpc-encoding"].contains { $0 != "identity" }) + + for try await part in success.bodyParts { + switch part { + case .message(let message): + payloads.append(message.payload) + case .trailingMetadata: + () + } + } + + case .failure: + throw AssertionFailure(message: "Response should have been accepted.") + } + + try assertEqual( + payloads, + responseSizes.map { size in + Grpc_Testing_Payload.with { + $0.body = Data(repeating: 0, count: size) + } + } + ) + } + } +} + /// This test verifies that full duplex bidi is supported. /// /// Server features: @@ -478,7 +783,7 @@ struct CustomMetadata: InteroperabilityTest { try self.checkTrailingMetadata(receivedTrailingMetadata) } } - case .failure(_): + case .failure: throw AssertionFailure( message: "The client should have received a response from the server." ) @@ -541,7 +846,7 @@ struct StatusCodeAndMessage: InteroperabilityTest { case .failure(let error): try assertEqual(error.code.rawValue, self.expectedCode) try assertEqual(error.message, self.expectedMessage) - case .success(_): + case .success: throw AssertionFailure( message: "The client should receive an error with the status code and message sent by the client." @@ -613,7 +918,7 @@ struct SpecialStatusMessage: InteroperabilityTest { request: ClientRequest.Single(message: message) ) { response in switch response.accepted { - case .success(_): + case .success: throw AssertionFailure( message: "The response should be an error with the error code 2." ) @@ -647,9 +952,8 @@ struct UnimplementedMethod: InteroperabilityTest { try await testServiceClient.unimplementedCall( request: ClientRequest.Single(message: Grpc_Testing_Empty()) ) { response in - let result = response.accepted - switch result { - case .success(_): + switch response.accepted { + case .success: throw AssertionFailure( message: "The result should be an error." ) @@ -681,12 +985,9 @@ struct UnimplementedService: InteroperabilityTest { try await unimplementedServiceClient.unimplementedCall( request: ClientRequest.Single(message: Grpc_Testing_Empty()) ) { response in - let result = response.accepted - switch result { - case .success(_): - throw AssertionFailure( - message: "The result should be an error." - ) + switch response.accepted { + case .success: + throw AssertionFailure(message: "The result should be an error.") case .failure(let error): try assertEqual(error.code, .unimplemented) } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index e0804b737..2cdea833f 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -1067,6 +1067,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } + func testClientClosesBeforeItCanOpen() throws { + var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) + XCTAssertNoThrow(try stateMachine.closeOutbound()) + } + func testClientClosesBeforeServerOpens() throws { var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) From 4bc22afb273574c04411e62428b9b167eaa084be Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 14 Jun 2024 11:29:36 +0100 Subject: [PATCH 353/580] Add a service for testing (#1916) Motivation: The interop tests cover core functionality. However, we'd like to test more scenarios in more depth. To do so we can use a service which responds however the client asks it to. This allows, for example, the client to ask the server to send a status-only response, or to respond with N messages before failing with a particular status. Modifications: - Add a definition for a 'control' service - Implement the control service Result: We have a service which the client can control the behaviour of allowing us to exercise different code paths. --- Package.swift | 11 + Protos/generate.sh | 13 +- Protos/tests/control/control.proto | 116 +++++ .../ControlService.swift | 194 ++++++++ .../Generated/control.grpc.swift | 345 +++++++++++++ .../Generated/control.pb.swift | 459 ++++++++++++++++++ 6 files changed, 1136 insertions(+), 2 deletions(-) create mode 100644 Protos/tests/control/control.proto create mode 100644 Tests/GRPCHTTP2TransportTests/ControlService.swift create mode 100644 Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift create mode 100644 Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift diff --git a/Package.swift b/Package.swift index d3437fd99..f8a1c7287 100644 --- a/Package.swift +++ b/Package.swift @@ -335,6 +335,16 @@ extension Target { ] ) + static let grpcHTTP2TransportTests: Target = .testTarget( + name: "GRPCHTTP2TransportTests", + dependencies: [ + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .grpcHTTP2TransportNIOTransportServices, + .grpcProtobuf + ] + ) + static let grpcCodeGenTests: Target = .testTarget( name: "GRPCCodeGenTests", dependencies: [ @@ -742,6 +752,7 @@ let package = Package( .grpcCodeGenTests, .grpcInterceptorsTests, .grpcHTTP2CoreTests, + .grpcHTTP2TransportTests, .grpcProtobufTests, .grpcProtobufCodeGenTests, .inProcessInteroperabilityTests diff --git a/Protos/generate.sh b/Protos/generate.sh index 86f91b557..48a5be1ed 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -186,6 +186,14 @@ function generate_rpc_code_for_tests { done } +function generate_http2_transport_tests_service { + local proto="$here/tests/control/control.proto" + local output="$root/Tests/GRPCHTTP2TransportTests/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "Client=true" "Server=true" "_V2=true" +} + function generate_service_messages_interop_tests { local protos=( "$here/tests/interoperability/src/proto/grpc/testing/empty_service.proto" @@ -194,7 +202,7 @@ function generate_service_messages_interop_tests { "$here/tests/interoperability/src/proto/grpc/testing/test.proto" ) local output="$root/Sources/InteroperabilityTests/Generated" - + for proto in "${protos[@]}"; do generate_message "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "FileNaming=DropPath" generate_grpc "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "Server=true" "_V2=true" "FileNaming=DropPath" @@ -211,7 +219,7 @@ function generate_worker_service { "$here/upstream/grpc/testing/worker_service.proto" ) local output="$root/Sources/performance-worker/Generated" - + generate_message "$here/upstream/grpc/core/stats.proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=PathToUnderscores" for proto in "${protos[@]}"; do @@ -243,6 +251,7 @@ generate_service_messages_interop_tests # Misc. tests generate_normalization_for_tests generate_rpc_code_for_tests +generate_http2_transport_tests_service # Performance worker service generate_worker_service diff --git a/Protos/tests/control/control.proto b/Protos/tests/control/control.proto new file mode 100644 index 000000000..fb32eaa20 --- /dev/null +++ b/Protos/tests/control/control.proto @@ -0,0 +1,116 @@ +/* + * 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. + */ + +syntax = "proto3"; + +// A controllable service for testing. +// +// The control service has one RPC of each kind, the input to each RPC controls +// the output. +service Control { + rpc Unary(ControlInput) returns (ControlOutput) {} + rpc ServerStream(ControlInput) returns (stream ControlOutput) {} + rpc ClientStream(stream ControlInput) returns (ControlOutput) {} + rpc BidiStream(stream ControlInput) returns (stream ControlOutput) {} +} + +message ControlInput { + // Whether metadata should be echo'd back in the initial metadata. + // + // Ignored if the initial metadata has already been sent back to the + // client. + // + // Each header field name in the request headers will be prefixed with + // "echo-". For example the header field name "foo" will be returned + // as "echo-foo. Note that semicolons aren't valid in HTTP header field + // names (apart from pseudo headers). As such all semicolons should be + // removed (":path" should become "echo-path"). + bool echo_metadata_in_headers = 1; + + // Parameters for response messages. + PayloadParameters message_params = 2; + + // The number of response messages. + int32 number_of_messages = 3; + + // The status code and message to use at the end of the RPC. + // + // If this is set then the RPC will be ended after `number_of_messages` + // messages have been sent back to the client. + RPCStatus status = 5; + + // Whether the response should be trailers only. + // + // Ignored unless it's set on the first message on the stream. When set + // the RPC will be completed with a trailers-only response using the + // status code and message from 'status'. The request metadata will be + // included if 'echo_metadata_in_trailers' is set. + // + // If this is set then 'number_of_messages', 'message_params', and + // 'echo_metadata_in_headers' are ignored. + bool is_trailers_only = 6; + + // Whether metadata should be echo'd back in the trailing metadata. + // + // Ignored unless 'status' is set. + // + // Each header field name in the request headers will be prefixed with + // "echo-". For example the header field name "foo" will be returned + // as "echo-foo. Note that semicolons aren't valid in HTTP header field + // names (apart from pseudo headers). As such all semicolons should be + // removed (":path" should become "echo-path"). + bool echo_metadata_in_trailers = 4; +} + +message RPCStatus { + // Status code indicating the outcome of the RPC. + StatusCode code = 1; + + // The message to include with the 'code' at the end of the RPC. + string message = 2; +} + +enum StatusCode { + OK = 0; + CANCELLED = 1; + UNKNOWN = 2; + INVALID_ARGUMENT = 3; + DEADLINE_EXCEEDED = 4; + NOT_FOUND = 5; + ALREADY_EXISTS = 6; + PERMISSION_DENIED = 7; + RESOURCE_EXHAUSTED = 8; + FAILED_PRECONDITION = 9; + ABORTED = 10; + OUT_OF_RANGE = 11; + UNIMPLEMENTED = 12; + INTERNAL = 13; + UNAVAILABLE = 14; + DATA_LOSS = 15; + UNAUTHENTICATED = 16; +} + +message PayloadParameters { + // The number of bytes to put into the output payload. + int32 size = 1; + + // The content to use in the payload. The value is truncated to an octet. + uint32 content = 2; +} + +message ControlOutput { + bytes payload = 1; +} diff --git a/Tests/GRPCHTTP2TransportTests/ControlService.swift b/Tests/GRPCHTTP2TransportTests/ControlService.swift new file mode 100644 index 000000000..07cbc31ee --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/ControlService.swift @@ -0,0 +1,194 @@ +/* + * 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 struct Foundation.Data + +struct ControlService: ControlStreamingServiceProtocol { + func unary( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + try await self.handle(request: request) + } + + func serverStream( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + try await self.handle(request: request) + } + + func clientStream( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + try await self.handle(request: request) + } + + func bidiStream( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + try await self.handle(request: request) + } +} + +extension ControlService { + private func handle( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + var iterator = request.messages.makeAsyncIterator() + + guard let message = try await iterator.next() else { + // Empty input stream, empty output stream. + return ServerResponse.Stream { _ in [:] } + } + + // Check if the request is for a trailers-only response. + if message.hasStatus, message.isTrailersOnly { + let trailers = message.echoMetadataInTrailers ? request.metadata.echo() : [:] + let code = Status.Code(rawValue: message.status.code.rawValue).flatMap { RPCError.Code($0) } + + if let code = code { + throw RPCError(code: code, message: message.status.message, metadata: trailers) + } else { + // Invalid code, the request is invalid, so throw an appropriate error. + throw RPCError( + code: .invalidArgument, + message: "Trailers only response must use a non-OK status code" + ) + } + } + + // Not a trailers-only response. Should the metadata be echo'd back? + let metadata = message.echoMetadataInHeaders ? request.metadata.echo() : [:] + + // The iterator needs to be transferred into the response. This is okay: we won't touch the + // iterator again from the current concurrency domain. + let transfer = UnsafeTransfer(iterator) + + return ServerResponse.Stream(metadata: metadata) { writer in + // Finish dealing with the first message. + switch try await self.processMessage(message, metadata: request.metadata, writer: writer) { + case .return(let metadata): + return metadata + case .continue: + () + } + + var iterator = transfer.wrappedValue + // Process the rest of the messages. + while let message = try await iterator.next() { + switch try await self.processMessage(message, metadata: request.metadata, writer: writer) { + case .return(let metadata): + return metadata + case .continue: + () + } + } + + // Input stream finished without explicitly setting a status; finish the RPC cleanly. + return [:] + } + } + + private enum NextProcessingStep { + case `return`(Metadata) + case `continue` + } + + private func processMessage( + _ input: ControlInput, + metadata: Metadata, + writer: RPCWriter + ) async throws -> NextProcessingStep { + // If messages were requested, build a response and send them back. + if input.numberOfMessages > 0 { + let output = ControlOutput.with { + $0.payload = Data( + repeating: UInt8(truncatingIfNeeded: input.messageParams.content), + count: Int(input.messageParams.size) + ) + } + + for _ in 0 ..< input.numberOfMessages { + try await writer.write(output) + } + } + + // Check whether the RPC should be finished (i.e. the input `hasStatus`). + guard input.hasStatus else { + if input.echoMetadataInTrailers { + // There was no status in the input, but echo metadata in trailers was set. This is an + // implicit 'ok' status. + let trailers = input.echoMetadataInTrailers ? metadata.echo() : [:] + return .return(trailers) + } else { + // No status, and not echoing back metadata. Continue consuming the input stream. + return .continue + } + } + + // Build the trailers. + let trailers = input.echoMetadataInTrailers ? metadata.echo() : [:] + + if input.status.code == .ok { + return .return(trailers) + } + + // Non-OK status code, throw an error. + let code = Status.Code(rawValue: input.status.code.rawValue).flatMap { RPCError.Code($0) } + + if let code = code { + // Valid error code, throw it. + throw RPCError(code: code, message: input.status.message, metadata: trailers) + } else { + // Invalid error code, throw an appropriate error. + throw RPCError( + code: .invalidArgument, + message: "Invalid error code '\(input.status.code)'" + ) + } + } +} + +extension Metadata { + fileprivate func echo() -> Self { + var copy = Metadata() + copy.reserveCapacity(self.count) + + for (key, value) in self { + // Header field names mustn't contain ":". + let key = "echo-" + key.replacingOccurrences(of: ":", with: "") + switch value { + case .string(let stringValue): + copy.addString(stringValue, forKey: key) + case .binary(let binaryValue): + copy.addBinary(binaryValue, forKey: key) + } + } + + return copy + } +} + +private struct UnsafeTransfer { + var wrappedValue: Wrapped + + init(_ wrappedValue: Wrapped) { + self.wrappedValue = wrappedValue + } +} + +extension UnsafeTransfer: @unchecked Sendable {} diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift new file mode 100644 index 000000000..c60dbcb74 --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -0,0 +1,345 @@ +// +// 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: control.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +internal enum Control { + internal enum Method { + internal enum Unary { + internal typealias Input = ControlInput + internal typealias Output = ControlOutput + internal static let descriptor = MethodDescriptor( + service: "Control", + method: "Unary" + ) + } + internal enum ServerStream { + internal typealias Input = ControlInput + internal typealias Output = ControlOutput + internal static let descriptor = MethodDescriptor( + service: "Control", + method: "ServerStream" + ) + } + internal enum ClientStream { + internal typealias Input = ControlInput + internal typealias Output = ControlOutput + internal static let descriptor = MethodDescriptor( + service: "Control", + method: "ClientStream" + ) + } + internal enum BidiStream { + internal typealias Input = ControlInput + internal typealias Output = ControlOutput + internal static let descriptor = MethodDescriptor( + service: "Control", + method: "BidiStream" + ) + } + internal static let descriptors: [MethodDescriptor] = [ + Unary.descriptor, + ServerStream.descriptor, + ClientStream.descriptor, + BidiStream.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias StreamingServiceProtocol = ControlStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ServiceProtocol = ControlServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ClientProtocol = ControlClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias Client = ControlClient +} + +/// A controllable service for testing. +/// +/// The control service has one RPC of each kind, the input to each RPC controls +/// the output. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Control.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Control.Method.Unary.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.unary(request: request) + } + ) + router.registerHandler( + forMethod: Control.Method.ServerStream.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.serverStream(request: request) + } + ) + router.registerHandler( + forMethod: Control.Method.ClientStream.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.clientStream(request: request) + } + ) + router.registerHandler( + forMethod: Control.Method.BidiStream.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.bidiStream(request: request) + } + ) + } +} + +/// A controllable service for testing. +/// +/// The control service has one RPC of each kind, the input to each RPC controls +/// the output. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { + func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single + + func serverStream(request: ServerRequest.Single) async throws -> ServerResponse.Stream + + func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Single + + func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Partial conformance to `ControlStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Control.ServiceProtocol { + internal func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.unary(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + internal func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.serverStream(request: ServerRequest.Single(stream: request)) + return response + } + + internal func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.clientStream(request: request) + return ServerResponse.Stream(single: response) + } +} + +/// A controllable service for testing. +/// +/// The control service has one RPC of each kind, the input to each RPC controls +/// the output. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol ControlClientProtocol: Sendable { + func unary( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + func serverStream( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + func clientStream( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + func bidiStream( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Control.ClientProtocol { + internal func unary( + request: ClientRequest.Single, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.unary( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } + + internal func serverStream( + request: ClientRequest.Single, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.serverStream( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } + + internal func clientStream( + request: ClientRequest.Stream, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.clientStream( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } + + internal func bidiStream( + request: ClientRequest.Stream, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.bidiStream( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } +} + +/// A controllable service for testing. +/// +/// The control service has one RPC of each kind, the input to each RPC controls +/// the output. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal struct ControlClient: Control.ClientProtocol { + private let client: GRPCCore.GRPCClient + + internal init(client: GRPCCore.GRPCClient) { + self.client = client + } + + internal func unary( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Control.Method.Unary.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + internal func serverStream( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Control.Method.ServerStream.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + internal func clientStream( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Control.Method.ClientStream.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + internal func bidiStream( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Control.Method.BidiStream.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } +} \ No newline at end of file diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift new file mode 100644 index 000000000..891499b2f --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift @@ -0,0 +1,459 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: control.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +enum StatusCode: SwiftProtobuf.Enum { + typealias RawValue = Int + case ok // = 0 + case cancelled // = 1 + case unknown // = 2 + case invalidArgument // = 3 + case deadlineExceeded // = 4 + case notFound // = 5 + case alreadyExists // = 6 + case permissionDenied // = 7 + case resourceExhausted // = 8 + case failedPrecondition // = 9 + case aborted // = 10 + case outOfRange // = 11 + case unimplemented // = 12 + case `internal` // = 13 + case unavailable // = 14 + case dataLoss // = 15 + case unauthenticated // = 16 + case UNRECOGNIZED(Int) + + init() { + self = .ok + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .ok + case 1: self = .cancelled + case 2: self = .unknown + case 3: self = .invalidArgument + case 4: self = .deadlineExceeded + case 5: self = .notFound + case 6: self = .alreadyExists + case 7: self = .permissionDenied + case 8: self = .resourceExhausted + case 9: self = .failedPrecondition + case 10: self = .aborted + case 11: self = .outOfRange + case 12: self = .unimplemented + case 13: self = .internal + case 14: self = .unavailable + case 15: self = .dataLoss + case 16: self = .unauthenticated + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .ok: return 0 + case .cancelled: return 1 + case .unknown: return 2 + case .invalidArgument: return 3 + case .deadlineExceeded: return 4 + case .notFound: return 5 + case .alreadyExists: return 6 + case .permissionDenied: return 7 + case .resourceExhausted: return 8 + case .failedPrecondition: return 9 + case .aborted: return 10 + case .outOfRange: return 11 + case .unimplemented: return 12 + case .internal: return 13 + case .unavailable: return 14 + case .dataLoss: return 15 + case .unauthenticated: return 16 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension StatusCode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [StatusCode] = [ + .ok, + .cancelled, + .unknown, + .invalidArgument, + .deadlineExceeded, + .notFound, + .alreadyExists, + .permissionDenied, + .resourceExhausted, + .failedPrecondition, + .aborted, + .outOfRange, + .unimplemented, + .internal, + .unavailable, + .dataLoss, + .unauthenticated, + ] +} + +#endif // swift(>=4.2) + +struct ControlInput { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Whether metadata should be echo'd back in the initial metadata. + /// + /// Ignored if the initial metadata has already been sent back to the + /// client. + /// + /// Each header field name in the request headers will be prefixed with + /// "echo-". For example the header field name "foo" will be returned + /// as "echo-foo. Note that semicolons aren't valid in HTTP header field + /// names (apart from pseudo headers). As such all semicolons should be + /// removed (":path" should become "echo-path"). + var echoMetadataInHeaders: Bool = false + + /// Parameters for response messages. + var messageParams: PayloadParameters { + get {return _messageParams ?? PayloadParameters()} + set {_messageParams = newValue} + } + /// Returns true if `messageParams` has been explicitly set. + var hasMessageParams: Bool {return self._messageParams != nil} + /// Clears the value of `messageParams`. Subsequent reads from it will return its default value. + mutating func clearMessageParams() {self._messageParams = nil} + + /// The number of response messages. + var numberOfMessages: Int32 = 0 + + /// The status code and message to use at the end of the RPC. + /// + /// If this is set then the RPC will be ended after `number_of_messages` + /// messages have been sent back to the client. + var status: RPCStatus { + get {return _status ?? RPCStatus()} + set {_status = newValue} + } + /// Returns true if `status` has been explicitly set. + var hasStatus: Bool {return self._status != nil} + /// Clears the value of `status`. Subsequent reads from it will return its default value. + mutating func clearStatus() {self._status = nil} + + /// Whether the response should be trailers only. + /// + /// Ignored unless it's set on the first message on the stream. When set + /// the RPC will be completed with a trailers-only response using the + /// status code and message from 'status'. The request metadata will be + /// included if 'echo_metadata_in_trailers' is set. + /// + /// If this is set then 'number_of_messages', 'message_params', and + /// 'echo_metadata_in_headers' are ignored. + var isTrailersOnly: Bool = false + + /// Whether metadata should be echo'd back in the trailing metadata. + /// + /// Ignored unless 'status' is set. + /// + /// Each header field name in the request headers will be prefixed with + /// "echo-". For example the header field name "foo" will be returned + /// as "echo-foo. Note that semicolons aren't valid in HTTP header field + /// names (apart from pseudo headers). As such all semicolons should be + /// removed (":path" should become "echo-path"). + var echoMetadataInTrailers: Bool = false + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _messageParams: PayloadParameters? = nil + fileprivate var _status: RPCStatus? = nil +} + +struct RPCStatus { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Status code indicating the outcome of the RPC. + var code: StatusCode = .ok + + /// The message to include with the 'code' at the end of the RPC. + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct PayloadParameters { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The number of bytes to put into the output payload. + var size: Int32 = 0 + + /// The conent to use in the payload. The value is truncated to an octet. + var content: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct ControlOutput { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var payload: Data = Data() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension StatusCode: @unchecked Sendable {} +extension ControlInput: @unchecked Sendable {} +extension RPCStatus: @unchecked Sendable {} +extension PayloadParameters: @unchecked Sendable {} +extension ControlOutput: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension StatusCode: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "OK"), + 1: .same(proto: "CANCELLED"), + 2: .same(proto: "UNKNOWN"), + 3: .same(proto: "INVALID_ARGUMENT"), + 4: .same(proto: "DEADLINE_EXCEEDED"), + 5: .same(proto: "NOT_FOUND"), + 6: .same(proto: "ALREADY_EXISTS"), + 7: .same(proto: "PERMISSION_DENIED"), + 8: .same(proto: "RESOURCE_EXHAUSTED"), + 9: .same(proto: "FAILED_PRECONDITION"), + 10: .same(proto: "ABORTED"), + 11: .same(proto: "OUT_OF_RANGE"), + 12: .same(proto: "UNIMPLEMENTED"), + 13: .same(proto: "INTERNAL"), + 14: .same(proto: "UNAVAILABLE"), + 15: .same(proto: "DATA_LOSS"), + 16: .same(proto: "UNAUTHENTICATED"), + ] +} + +extension ControlInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "ControlInput" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "echo_metadata_in_headers"), + 2: .standard(proto: "message_params"), + 3: .standard(proto: "number_of_messages"), + 5: .same(proto: "status"), + 6: .standard(proto: "is_trailers_only"), + 4: .standard(proto: "echo_metadata_in_trailers"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBoolField(value: &self.echoMetadataInHeaders) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._messageParams) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.numberOfMessages) }() + case 4: try { try decoder.decodeSingularBoolField(value: &self.echoMetadataInTrailers) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._status) }() + case 6: try { try decoder.decodeSingularBoolField(value: &self.isTrailersOnly) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.echoMetadataInHeaders != false { + try visitor.visitSingularBoolField(value: self.echoMetadataInHeaders, fieldNumber: 1) + } + try { if let v = self._messageParams { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + if self.numberOfMessages != 0 { + try visitor.visitSingularInt32Field(value: self.numberOfMessages, fieldNumber: 3) + } + if self.echoMetadataInTrailers != false { + try visitor.visitSingularBoolField(value: self.echoMetadataInTrailers, fieldNumber: 4) + } + try { if let v = self._status { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + if self.isTrailersOnly != false { + try visitor.visitSingularBoolField(value: self.isTrailersOnly, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ControlInput, rhs: ControlInput) -> Bool { + if lhs.echoMetadataInHeaders != rhs.echoMetadataInHeaders {return false} + if lhs._messageParams != rhs._messageParams {return false} + if lhs.numberOfMessages != rhs.numberOfMessages {return false} + if lhs._status != rhs._status {return false} + if lhs.isTrailersOnly != rhs.isTrailersOnly {return false} + if lhs.echoMetadataInTrailers != rhs.echoMetadataInTrailers {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension RPCStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "RPCStatus" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "code"), + 2: .same(proto: "message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.code) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.code != .ok { + try visitor.visitSingularEnumField(value: self.code, fieldNumber: 1) + } + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: RPCStatus, rhs: RPCStatus) -> Bool { + if lhs.code != rhs.code {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension PayloadParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "PayloadParameters" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "size"), + 2: .same(proto: "content"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.content) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.size != 0 { + try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) + } + if self.content != 0 { + try visitor.visitSingularUInt32Field(value: self.content, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: PayloadParameters, rhs: PayloadParameters) -> Bool { + if lhs.size != rhs.size {return false} + if lhs.content != rhs.content {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ControlOutput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "ControlOutput" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "payload"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularBytesField(value: &self.payload) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.payload.isEmpty { + try visitor.visitSingularBytesField(value: self.payload, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ControlOutput, rhs: ControlOutput) -> Bool { + if lhs.payload != rhs.payload {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} From 2a62be5ea53796b99eaa61bd062e76e258534782 Mon Sep 17 00:00:00 2001 From: woxtu Date: Fri, 14 Jun 2024 20:03:18 +0900 Subject: [PATCH 354/580] Update platform names (#1932) --- Sources/GRPC/PlatformSupport.swift | 30 +++++++++++----------- Sources/GRPC/Server.swift | 4 +-- Tests/GRPCTests/FunctionalTests.swift | 6 ++--- Tests/GRPCTests/ZeroLengthWriteTests.swift | 10 ++++---- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift index efa6009f5..b7f4ff5a4 100644 --- a/Sources/GRPC/PlatformSupport.swift +++ b/Sources/GRPC/PlatformSupport.swift @@ -57,7 +57,7 @@ public struct NetworkImplementation: Hashable { #if canImport(Network) /// Network.framework (NIOTransportServices). - @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) + @available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) public static let networkFramework = NetworkImplementation(.networkFramework) #endif @@ -66,7 +66,7 @@ public struct NetworkImplementation: Hashable { internal static func matchingEventLoopGroup(_ group: EventLoopGroup) -> NetworkImplementation { #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { if PlatformSupport.isTransportServicesEventLoopGroup(group) { return .networkFramework } @@ -84,13 +84,13 @@ extension NetworkPreference { /// 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, *)`. + /// - `@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)`, or + /// - `#available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)`. public var implementation: NetworkImplementation { switch self.wrapped { case .best: #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { return .networkFramework } else { // Older platforms must use the POSIX loop. @@ -136,7 +136,7 @@ extension ClientBootstrapProtocol { extension ClientBootstrap: ClientBootstrapProtocol {} #if canImport(Network) -@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) +@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) extension NIOTSConnectionBootstrap: ClientBootstrapProtocol { public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture { preconditionFailure("NIOTSConnectionBootstrap does not support withConnectedSocket(_:)") @@ -182,7 +182,7 @@ extension ServerBootstrapProtocol { extension ServerBootstrap: ServerBootstrapProtocol {} #if canImport(Network) -@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) +@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) extension NIOTSListenerBootstrap: ServerBootstrapProtocol { public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture { preconditionFailure("NIOTSListenerBootstrap does not support withBoundSocket(_:)") @@ -213,7 +213,7 @@ public enum PlatformSupport { switch networkPreference.implementation.wrapped { case .networkFramework: #if canImport(Network) - guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { + guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { logger.critical("Network.framework can be imported but is not supported on this platform") // This is gated by the availability of `.networkFramework` so should never happen. fatalError(".networkFramework is being used on an unsupported platform") @@ -241,7 +241,7 @@ public enum PlatformSupport { ) -> ClientBootstrapProtocol { logger.debug("making client bootstrap with event loop group of type \(type(of: group))") #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { if isTransportServicesEventLoopGroup(group) { logger.debug( "Network.framework is available and the EventLoopGroup is compatible with NIOTS, creating a NIOTSConnectionBootstrap" @@ -260,7 +260,7 @@ public enum PlatformSupport { internal static func isTransportServicesEventLoopGroup(_ group: EventLoopGroup) -> Bool { #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { return group is NIOTSEventLoopGroup || group is QoSEventLoop } #endif @@ -279,7 +279,7 @@ public enum PlatformSupport { } #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let transportServicesBootstrap = bootstrap as? NIOTSConnectionBootstrap { return transportServicesBootstrap.tlsOptions(from: tlsConfigruation) @@ -301,7 +301,7 @@ public enum PlatformSupport { ) -> ServerBootstrapProtocol { logger.debug("making server bootstrap with event loop group of type \(type(of: group))") #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { if let tsGroup = group as? NIOTSEventLoopGroup { logger .debug( @@ -330,7 +330,7 @@ public enum PlatformSupport { /// See https://github.com/apple/swift-nio-transport-services/pull/72 for more. static func requiresZeroLengthWriteWorkaround(group: EventLoopGroup, hasTLS: Bool) -> Bool { #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { if group is NIOTSEventLoopGroup || group is QoSEventLoop { // We need the zero-length write workaround on NIOTS when not using TLS. return !hasTLS @@ -358,7 +358,7 @@ extension PlatformSupport { loopCount: Int ) -> EventLoopGroup { #if canImport(Network) - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { if configuration.isNetworkFrameworkTLSBackend { return NIOTSEventLoopGroup(loopCount: loopCount) } @@ -384,7 +384,7 @@ extension GRPCTLSConfiguration { switch networkPreference.implementation.wrapped { case .networkFramework: #if canImport(Network) - guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { + guard #available(macOS 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") } diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift index 1f0ce6e40..9117fd7e2 100644 --- a/Sources/GRPC/Server.swift +++ b/Sources/GRPC/Server.swift @@ -121,7 +121,7 @@ public final class Server: @unchecked Sendable { #if canImport(Network) if let tlsConfiguration = configuration.tlsConfiguration { - if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), + if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let transportServicesBootstrap = bootstrap as? NIOTSListenerBootstrap { _ = transportServicesBootstrap.tlsOptions(from: tlsConfiguration) @@ -172,7 +172,7 @@ public final class Server: @unchecked Sendable { hasTLS: configuration.tlsConfiguration != nil ) if requiresZeroLengthWorkaround, - #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) + #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { try sync.addHandler(NIOFilterEmptyWritesHandler()) } diff --git a/Tests/GRPCTests/FunctionalTests.swift b/Tests/GRPCTests/FunctionalTests.swift index 1ce33561b..96b759b69 100644 --- a/Tests/GRPCTests/FunctionalTests.swift +++ b/Tests/GRPCTests/FunctionalTests.swift @@ -371,7 +371,7 @@ class FunctionalTestsMutualAuthentication: FunctionalTestsInsecureTransport { // 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, *) +@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) class FunctionalTestsInsecureTransportNIOTS: FunctionalTestsInsecureTransport { override var networkPreference: NetworkPreference { #if canImport(Network) @@ -457,7 +457,7 @@ class FunctionalTestsInsecureTransportNIOTS: FunctionalTestsInsecureTransport { } #if canImport(NIOSSL) -@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) +@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) class FunctionalTestsAnonymousClientNIOTS: FunctionalTestsInsecureTransportNIOTS { override var transportSecurity: TransportSecurity { return .anonymousClient @@ -512,7 +512,7 @@ class FunctionalTestsAnonymousClientNIOTS: FunctionalTestsInsecureTransportNIOTS } } -@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) +@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) class FunctionalTestsMutualAuthenticationNIOTS: FunctionalTestsInsecureTransportNIOTS { override var transportSecurity: TransportSecurity { return .mutualAuthentication diff --git a/Tests/GRPCTests/ZeroLengthWriteTests.swift b/Tests/GRPCTests/ZeroLengthWriteTests.swift index 9b98a3520..1cb070de1 100644 --- a/Tests/GRPCTests/ZeroLengthWriteTests.swift +++ b/Tests/GRPCTests/ZeroLengthWriteTests.swift @@ -134,7 +134,7 @@ final class ZeroLengthWriteTests: GRPCTestCase { ) { // We can only run this test on platforms where the zero-length write workaround _could_ be added. #if canImport(Network) - guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } + guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } let group = PlatformSupport.makeEventLoopGroup( loopCount: 1, networkPreference: networkPreference @@ -171,7 +171,7 @@ final class ZeroLengthWriteTests: GRPCTestCase { func testZeroLengthWriteTestPosixSecure() throws { // We can only run this test on platforms where the zero-length write workaround _could_ be added. #if canImport(Network) - guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } + guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } let serverExpectation = self.noZeroLengthWriteExpectation() let clientExpectation = self.noZeroLengthWriteExpectation() @@ -195,7 +195,7 @@ final class ZeroLengthWriteTests: GRPCTestCase { func testZeroLengthWriteTestPosixInsecure() throws { // We can only run this test on platforms where the zero-length write workaround _could_ be added. #if canImport(Network) - guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } + guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } let serverExpectation = self.noZeroLengthWriteExpectation() let clientExpectation = self.noZeroLengthWriteExpectation() @@ -219,7 +219,7 @@ final class ZeroLengthWriteTests: GRPCTestCase { func testZeroLengthWriteTestNetworkFrameworkSecure() throws { // We can only run this test on platforms where the zero-length write workaround _could_ be added. #if canImport(Network) - guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } + guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } let serverExpectation = self.noZeroLengthWriteExpectation() let clientExpectation = self.noZeroLengthWriteExpectation() @@ -243,7 +243,7 @@ final class ZeroLengthWriteTests: GRPCTestCase { func testZeroLengthWriteTestNetworkFrameworkInsecure() throws { // We can only run this test on platforms where the zero-length write workaround _could_ be added. #if canImport(Network) - guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } + guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } let serverExpectation = self.zeroLengthWriteExpectation() let clientExpectation = self.zeroLengthWriteExpectation() From a2bf82c6e8d804cef2b468e552fe64c6d26ac79b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 14 Jun 2024 12:36:02 +0100 Subject: [PATCH 355/580] Default the server config (#1931) Motivation: The server transport takes config which has a natural set of defaults. They should be the default argument. Modifications: - Add default arg Result: Easier to use --- ...TransportNIOPosix.swift => HTTP2ServerTransport+Posix.swift} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename Sources/GRPCHTTP2TransportNIOPosix/{GRPCHTTP2TransportNIOPosix.swift => HTTP2ServerTransport+Posix.swift} (99%) diff --git a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift similarity index 99% rename from Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift rename to Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 505695bc8..9bce6e4fa 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/GRPCHTTP2TransportNIOPosix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -30,7 +30,7 @@ extension HTTP2ServerTransport { public init( address: GRPCHTTP2Core.SocketAddress, - config: Config, + config: Config = .defaults, eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup ) { self.address = address From 14553706c6149c8084ee86e2102d1dbec7c31220 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 14 Jun 2024 14:29:50 +0100 Subject: [PATCH 356/580] Add pick first to grpc-channel (#1923) Motivation: The pick first load balancer was added in b170fb88. However, `GRPCChannel` wasn't updated to support it. Modifications: Add support to `GRPCChannel` Result: `GRPCChannel` can use the pick first LB --- .../Client/Connection/GRPCChannel.swift | 114 ++++++---- .../Client/Connection/GRPCChannelTests.swift | 200 ++++++++++++++++++ 2 files changed, 272 insertions(+), 42 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 02e6a765c..177aa734a 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -397,40 +397,87 @@ extension GRPCChannel { } } + enum SupportedLoadBalancerConfig { + case roundRobin + case pickFirst(ServiceConfig.LoadBalancingConfig.PickFirst) + + init?(_ config: ServiceConfig.LoadBalancingConfig) { + if let pickFirst = config.pickFirst { + self = .pickFirst(pickFirst) + } else if config.roundRobin != nil { + self = .roundRobin + } else { + return nil + } + } + + func matches(loadBalancer: LoadBalancer) -> Bool { + switch (self, loadBalancer) { + case (.roundRobin, .roundRobin): + return true + case (.pickFirst, .pickFirst): + return true + case (.roundRobin, .pickFirst), + (.pickFirst, .roundRobin): + return false + } + } + } + private func updateLoadBalancer( serviceConfig: ServiceConfig, endpoints: [Endpoint], in group: inout DiscardingTaskGroup ) { - // Pick the first applicable policy, else fallback to pick-first. - for policy in serviceConfig.loadBalancingConfig { - let onUpdatePolicy: GRPCChannel.StateMachine.OnChangeLoadBalancer - - if policy.roundRobin != nil { - onUpdatePolicy = self.state.withLockedValue { state in - state.changeLoadBalancerKind(to: .roundRobin) { - let loadBalancer = RoundRobinLoadBalancer( - connector: self.connector, - backoff: self.backoff, - defaultCompression: self.defaultCompression, - enabledCompression: self.enabledCompression - ) - return .roundRobin(loadBalancer) - } + assert(!endpoints.isEmpty, "endpoints must be non-empty") + + // Find the first supported config. + var configFromServiceConfig: SupportedLoadBalancerConfig? + for config in serviceConfig.loadBalancingConfig { + if let config = SupportedLoadBalancerConfig(config) { + configFromServiceConfig = config + break + } + } + + let onUpdatePolicy: GRPCChannel.StateMachine.OnChangeLoadBalancer + var endpoints = endpoints + + // Fallback to pick-first if no other config applies. + let loadBalancerConfig = configFromServiceConfig ?? .pickFirst(.init(shuffleAddressList: false)) + switch loadBalancerConfig { + case .roundRobin: + onUpdatePolicy = self.state.withLockedValue { state in + state.changeLoadBalancerKind(to: loadBalancerConfig) { + let loadBalancer = RoundRobinLoadBalancer( + connector: self.connector, + backoff: self.backoff, + defaultCompression: self.defaultCompression, + enabledCompression: self.enabledCompression + ) + return .roundRobin(loadBalancer) } - } else if policy.pickFirst != nil { - fatalError("TODO: use pick-first when supported") - } else { - // Policy isn't known, ignore it. - continue } - self.handleLoadBalancerChange(onUpdatePolicy, endpoints: endpoints, in: &group) - return + case .pickFirst(let pickFirst): + if pickFirst.shuffleAddressList { + endpoints[0].addresses.shuffle() + } + + onUpdatePolicy = self.state.withLockedValue { state in + state.changeLoadBalancerKind(to: loadBalancerConfig) { + let loadBalancer = PickFirstLoadBalancer( + connector: self.connector, + backoff: self.backoff, + defaultCompression: self.defaultCompression, + enabledCompression: self.enabledCompression + ) + return .pickFirst(loadBalancer) + } + } } - // No suitable config was found, fallback to pick-first. - fatalError("TODO: use pick-first when supported") + self.handleLoadBalancerChange(onUpdatePolicy, endpoints: endpoints, in: &group) } private func handleLoadBalancerChange( @@ -620,23 +667,6 @@ extension GRPCChannel.StateMachine { } } - enum LoadBalancerKind { - case roundRobin - case pickFirst - - func matches(loadBalancer: LoadBalancer) -> Bool { - switch (self, loadBalancer) { - case (.roundRobin, .roundRobin): - return true - case (.pickFirst, .pickFirst): - return true - case (.roundRobin, .pickFirst), - (.pickFirst, .roundRobin): - return false - } - } - } - enum OnChangeLoadBalancer { case runLoadBalancer(LoadBalancer, stop: LoadBalancer?) case updateLoadBalancer(LoadBalancer) @@ -644,7 +674,7 @@ extension GRPCChannel.StateMachine { } mutating func changeLoadBalancerKind( - to newLoadBalancerKind: LoadBalancerKind, + to newLoadBalancerKind: GRPCChannel.SupportedLoadBalancerConfig, _ makeLoadBalancer: () -> LoadBalancer ) -> OnChangeLoadBalancer { let onChangeLoadBalancer: OnChangeLoadBalancer diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index eb940d86b..f24520aeb 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -547,6 +547,206 @@ final class GRPCChannelTests: XCTestCase { } } } + + func testLoadBalancerChangingFromRoundRobinToPickFirst() async throws { + // The test will push different configs to the resolver, first a round-robin LB, then a + // pick-first LB. + let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) + let channel = GRPCChannel( + resolver: resolver, + connector: .posix(), + config: .defaults, + defaultServiceConfig: ServiceConfig() + ) + + // Start a few servers. + let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address1 = try await server1.bind() + + let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address2 = try await server2.bind() + + let server3 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address3 = try await server3.bind() + + try await withThrowingTaskGroup(of: Void.self) { group in + // Run the servers, no RPCs will be run against them. + for server in [server1, server2, server3] { + group.addTask { + try await server.run(.never) + } + } + + group.addTask { + await channel.connect() + } + + for await event in channel.connectivityState { + switch event { + case .idle: + let endpoints = [address1, address2].map { Endpoint(addresses: [$0]) } + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + let resolutionResult = NameResolutionResult( + endpoints: endpoints, + serviceConfig: .success(serviceConfig) + ) + + // Push the first resolution result which uses round robin. This will result in the + // channel becoming ready. + continuation.yield(resolutionResult) + + case .ready: + // Channel is ready, server 1 and 2 should have clients shortly. + try await XCTPoll(every: .milliseconds(10)) { + server1.clients.count == 1 && server2.clients.count == 1 && server3.clients.count == 0 + } + + // Both subchannels are ready, prepare and yield an update to the resolver. + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.pickFirst(shuffleAddressList: false)] + let resolutionResult = NameResolutionResult( + endpoints: [Endpoint(addresses: [address3])], + serviceConfig: .success(serviceConfig) + ) + continuation.yield(resolutionResult) + + // Only server 3 should have a connection. + try await XCTPoll(every: .milliseconds(10)) { + server1.clients.count == 0 && server2.clients.count == 0 && server3.clients.count == 1 + } + + channel.close() + + case .shutdown: + group.cancelAll() + + default: + () + } + } + } + } + + func testPickFirstShufflingAddressList() async throws { + // This test checks that the pick first load-balancer has its address list shuffled. We can't + // assert this deterministically, so instead we'll run an experiment a number of times. Each + // round will create N servers and provide them as endpoints to the pick-first load balancer. + // The channel will establish a connection to one of the servers and its identity will be noted. + let numberOfRounds = 100 + let numberOfServers = 2 + + let servers = (0 ..< numberOfServers).map { _ in + TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + } + + var addresses = [SocketAddress]() + for server in servers { + let address = try await server.bind() + addresses.append(address) + } + + let endpoint = Endpoint(addresses: addresses) + var counts = Array(repeating: 0, count: addresses.count) + + // Supply service config on init, not via the load-balancer. + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.pickFirst(shuffleAddressList: true)] + + try await withThrowingDiscardingTaskGroup { group in + // Run the servers. + for server in servers { + group.addTask { + try await server.run(.never) + } + } + + // Run the experiment. + for _ in 0 ..< numberOfRounds { + let channel = GRPCChannel( + resolver: .static(endpoints: [endpoint]), + connector: .posix(), + config: .defaults, + defaultServiceConfig: serviceConfig + ) + + group.addTask { + await channel.connect() + } + + for await state in channel.connectivityState { + switch state { + case .ready: + for index in servers.indices { + if servers[index].clients.count == 1 { + counts[index] += 1 + break + } + } + channel.close() + default: + () + } + } + } + + // Stop the servers. + group.cancelAll() + } + + // The address list is shuffled, so there's no guarantee how many times we'll hit each server. + // Assert that the minimum a server should be hit is 10% of the time. + let expected = Double(numberOfRounds) / Double(numberOfServers) + let minimum = expected * 0.1 + XCTAssert(counts.allSatisfy({ Double($0) >= minimum }), "\(counts)") + } + + func testPickFirstIsFallbackPolicy() async throws { + // Start a few servers. + let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address1 = try await server1.bind() + + let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) + let address2 = try await server2.bind() + + // Prepare a channel with an empty service config. + let channel = GRPCChannel( + resolver: .static(endpoints: [Endpoint(address1, address2)]), + connector: .posix(), + config: .defaults, + defaultServiceConfig: ServiceConfig() + ) + + try await withThrowingDiscardingTaskGroup { group in + // Run the servers. + for server in [server1, server2] { + group.addTask { + try await server.run(.never) + } + } + + group.addTask { + await channel.connect() + } + + for try await state in channel.connectivityState { + switch state { + case .ready: + // Only server 1 should have a connection. + try await XCTPoll(every: .milliseconds(10)) { + server1.clients.count == 1 && server2.clients.count == 0 + } + + channel.close() + + default: + () + } + } + + group.cancelAll() + } + } } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) From 0c9620142f7c9fa7c2bac2bf961d74fd47e25a07 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 14 Jun 2024 17:31:43 +0100 Subject: [PATCH 357/580] Add missing availability (#1936) --- Tests/GRPCHTTP2TransportTests/ControlService.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/GRPCHTTP2TransportTests/ControlService.swift b/Tests/GRPCHTTP2TransportTests/ControlService.swift index 07cbc31ee..4f089673e 100644 --- a/Tests/GRPCHTTP2TransportTests/ControlService.swift +++ b/Tests/GRPCHTTP2TransportTests/ControlService.swift @@ -18,6 +18,7 @@ import GRPCCore import struct Foundation.Data +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct ControlService: ControlStreamingServiceProtocol { func unary( request: ServerRequest.Stream @@ -44,6 +45,7 @@ struct ControlService: ControlStreamingServiceProtocol { } } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ControlService { private func handle( request: ServerRequest.Stream From 84534555bb73bfa278e6993399efefc90ba4da42 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 18 Jun 2024 08:47:01 +0100 Subject: [PATCH 358/580] Fix various warnings (#1935) Motivation: Swift 6 compiler emits various warnings which we can fix/suppress. Modifications: - Add `@unchecked Sendable` where appropriate - Add a `@retroactive` where appropriate - Remove use of an inappropriate retroactive conformance Result: Fewer warnings --- .../Echo/Implementation/Interceptors.swift | 4 +++- .../GRPCTestingConvenienceMethods.swift | 10 +++------- .../InteroperabilityTestCases.swift | 14 ++++++------- .../Server/ReflectionServiceV1.swift | 3 +-- .../Server/ReflectionServiceV1Alpha.swift | 16 +++++++++++++-- .../GRPCStreamStateMachineTests.swift | 10 +++++++++- .../ClientInterceptorPipelineTests.swift | 10 +++++++--- .../DelegatingClientInterceptor.swift | 2 +- Tests/GRPCTests/GRPCPingHandlerTests.swift | 8 +++++++- .../InterceptedRPCCancellationTests.swift | 4 ++-- Tests/GRPCTests/InterceptorsTests.swift | 17 +++++++++++----- .../ServerInterceptorPipelineTests.swift | 20 ++++++++++++++----- Tests/GRPCTests/ServerInterceptorTests.swift | 7 +++++-- 13 files changed, 86 insertions(+), 39 deletions(-) diff --git a/Sources/Examples/Echo/Implementation/Interceptors.swift b/Sources/Examples/Echo/Implementation/Interceptors.swift index 9a248b7f9..232e3bedc 100644 --- a/Sources/Examples/Echo/Implementation/Interceptors.swift +++ b/Sources/Examples/Echo/Implementation/Interceptors.swift @@ -20,7 +20,9 @@ import NIOCore // All client interceptors derive from the 'ClientInterceptor' base class. We know the request and // response types for all Echo RPCs are the same: so we'll use them concretely here, allowing us // to access fields on each type as we intercept them. -class LoggingEchoClientInterceptor: ClientInterceptor { +class LoggingEchoClientInterceptor: ClientInterceptor, + @unchecked Sendable +{ /// Called when the interceptor has received a request part to handle. /// /// - Parameters: diff --git a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift b/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift index 18de4e07b..4b291ba13 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift @@ -40,13 +40,9 @@ extension Grpc_Testing_Payload { // MARK: - Bool value -extension Grpc_Testing_BoolValue: ExpressibleByBooleanLiteral { - public typealias BooleanLiteralType = Bool - - public init(booleanLiteral value: Bool) { - self = .with { - $0.value = value - } +extension Grpc_Testing_BoolValue { + public init(_ value: Bool) { + self = .with { $0.value = value } } } diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift index 9a33664fd..a9fe0c16a 100644 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift +++ b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift @@ -180,13 +180,13 @@ class ClientCompressedUnary: InteroperabilityTest { let client = Grpc_Testing_TestServiceNIOClient(channel: connection) let compressedRequest = Grpc_Testing_SimpleRequest.with { request in - request.expectCompressed = true + request.expectCompressed = .init(true) request.responseSize = 314_159 request.payload = .zeros(count: 271_828) } var uncompressedRequest = compressedRequest - uncompressedRequest.expectCompressed = false + uncompressedRequest.expectCompressed = .init(false) // For unary RPCs we disable compression at the call level. @@ -268,7 +268,7 @@ class ServerCompressedUnary: InteroperabilityTest { let client = Grpc_Testing_TestServiceNIOClient(channel: connection) let compressedRequest = Grpc_Testing_SimpleRequest.with { request in - request.responseCompressed = true + request.responseCompressed = .init(true) request.responseSize = 314_159 request.payload = .zeros(count: 271_828) } @@ -427,7 +427,7 @@ class ClientCompressedStreaming: InteroperabilityTest { // compression here will stop that header from being sent. let probe = client.streamingInputCall() let probeRequest: Grpc_Testing_StreamingInputCallRequest = .with { request in - request.expectCompressed = true + request.expectCompressed = .init(true) request.payload = .zeros(count: 27182) } @@ -443,7 +443,7 @@ class ClientCompressedStreaming: InteroperabilityTest { // The first message is identical to the probe message, we'll reuse that. // The second should not be compressed. let secondMessage: Grpc_Testing_StreamingInputCallRequest = .with { request in - request.expectCompressed = false + request.expectCompressed = .init(false) request.payload = .zeros(count: 45904) } @@ -565,11 +565,11 @@ class ServerCompressedStreaming: InteroperabilityTest { let request: Grpc_Testing_StreamingOutputCallRequest = .with { request in request.responseParameters = [ .with { - $0.compressed = true + $0.compressed = .init(true) $0.size = 31415 }, .with { - $0.compressed = false + $0.compressed = .init(false) $0.size = 92653 }, ] diff --git a/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift b/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift index c8109ee88..703baddc4 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift @@ -191,8 +191,7 @@ extension Result Grpc_Reflection_V1_ServerReflectionResponse { let result = self.recover().attachRequest(request) - // Safe to '!' as the failure type is 'Never'. - return try! result.get() + return result.get() } } diff --git a/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift b/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift index 6686c78cd..02388edd7 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift @@ -197,8 +197,7 @@ extension Result Grpc_Reflection_V1alpha_ServerReflectionResponse { let result = self.recover().attachRequest(request) - // Safe to '!' as the failure type is 'Never'. - return try! result.get() + return result.get() } } @@ -212,3 +211,16 @@ where Success == Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageR } } } + +#if compiler(<6.0) +extension Result where Failure == Never { + func get() -> Success { + switch self { + case .success(let success): + return success + case .failure: + fatalError("Unreachable") + } + } +} +#endif diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 2cdea833f..e33c2632f 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -2722,7 +2722,7 @@ extension XCTestCase { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCStreamStateMachine.OnNextOutboundFrame: Equatable { +extension GRPCStreamStateMachine.OnNextOutboundFrame { public static func == ( lhs: GRPCStreamStateMachine.OnNextOutboundFrame, rhs: GRPCStreamStateMachine.OnNextOutboundFrame @@ -2741,3 +2741,11 @@ extension GRPCStreamStateMachine.OnNextOutboundFrame: Equatable { } } } + +#if compiler(>=6.0) +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCStreamStateMachine.OnNextOutboundFrame: @retroactive Equatable {} +#else +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension GRPCStreamStateMachine.OnNextOutboundFrame: Equatable {} +#endif diff --git a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift index 9a1d000e0..d20860586 100644 --- a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift @@ -136,7 +136,9 @@ class ClientInterceptorPipelineTests: GRPCTestCase { var cancelled = false var timedOut = false - class FailOnCancel: ClientInterceptor { + class FailOnCancel: ClientInterceptor, + @unchecked Sendable + { override func cancel( promise: EventLoopPromise?, context: ClientInterceptorContext @@ -299,7 +301,9 @@ class ClientInterceptorPipelineTests: GRPCTestCase { // MARK: - Test Interceptors /// A simple interceptor which records and then forwards and request and response parts it sees. -class RecordingInterceptor: ClientInterceptor { +class RecordingInterceptor: ClientInterceptor, @unchecked + Sendable +{ var requestParts: [GRPCClientRequestPart] = [] var responseParts: [GRPCClientResponsePart] = [] @@ -322,7 +326,7 @@ class RecordingInterceptor: ClientInterceptor { +class StringRequestReverser: ClientInterceptor, @unchecked Sendable { override func send( _ part: GRPCClientRequestPart, promise: EventLoopPromise?, diff --git a/Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift b/Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift index a5a68ca98..26ddb2deb 100644 --- a/Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift +++ b/Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift @@ -22,7 +22,7 @@ import SwiftProtobuf final class DelegatingClientInterceptor< Request: Message, Response: Message ->: ClientInterceptor { +>: ClientInterceptor, @unchecked Sendable { typealias RequestPart = GRPCClientRequestPart typealias ResponsePart = GRPCClientResponsePart typealias Context = ClientInterceptorContext diff --git a/Tests/GRPCTests/GRPCPingHandlerTests.swift b/Tests/GRPCTests/GRPCPingHandlerTests.swift index caab4face..554a33062 100644 --- a/Tests/GRPCTests/GRPCPingHandlerTests.swift +++ b/Tests/GRPCTests/GRPCPingHandlerTests.swift @@ -360,7 +360,13 @@ class GRPCPingHandlerTests: GRPCTestCase { } } -extension PingHandler.Action: Equatable { +#if compiler(>=6.0) +extension PingHandler.Action: @retroactive Equatable {} +#else +extension PingHandler.Action: Equatable {} +#endif + +extension PingHandler.Action { public static func == (lhs: PingHandler.Action, rhs: PingHandler.Action) -> Bool { switch (lhs, rhs) { case (.none, .none): diff --git a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift index 2b18bcf28..61e41bb90 100644 --- a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift +++ b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift @@ -79,7 +79,7 @@ final class InterceptedRPCCancellationTests: GRPCTestCase { final class MagicRequiredServerInterceptor< Request: Message, Response: Message ->: ServerInterceptor { +>: ServerInterceptor, @unchecked Sendable { override func receive( _ part: GRPCServerRequestPart, context: ServerInterceptorContext @@ -103,7 +103,7 @@ final class MagicRequiredServerInterceptor< final class MagicAddingClientInterceptor< Request: Message, Response: Message ->: ClientInterceptor { +>: ClientInterceptor, @unchecked Sendable { private let channel: GRPCChannel private var requestParts = CircularBuffer>() private var retry: Call? diff --git a/Tests/GRPCTests/InterceptorsTests.swift b/Tests/GRPCTests/InterceptorsTests.swift index e411fdec5..c27a99008 100644 --- a/Tests/GRPCTests/InterceptorsTests.swift +++ b/Tests/GRPCTests/InterceptorsTests.swift @@ -176,7 +176,9 @@ private class HelloWorldClientInterceptorFactory: } } -class RemoteAddressExistsInterceptor: ServerInterceptor { +class RemoteAddressExistsInterceptor: + ServerInterceptor, @unchecked Sendable +{ override func receive( _ part: GRPCServerRequestPart, context: ServerInterceptorContext @@ -187,7 +189,8 @@ class RemoteAddressExistsInterceptor: ServerInterceptor: - ServerInterceptor + ServerInterceptor, + @unchecked Sendable { override func receive( _ part: GRPCServerRequestPart, @@ -220,7 +223,7 @@ final class HelloWorldServerInterceptorFactory: Helloworld_GreeterServerIntercep } class NotReallyAuthClientInterceptor: - ClientInterceptor + ClientInterceptor, @unchecked Sendable { private let client: Helloworld_GreeterNIOClient @@ -323,7 +326,9 @@ class NotReallyAuthClientInterceptor: } } -final class EchoReverseInterceptor: ClientInterceptor { +final class EchoReverseInterceptor: ClientInterceptor, + @unchecked Sendable +{ override func send( _ part: GRPCClientRequestPart, promise: EventLoopPromise?, @@ -398,7 +403,9 @@ final class CountOnCloseInterceptors: Echo_EchoServerInterceptorFactoryProtocol } } -final class CountOnCloseServerInterceptor: ServerInterceptor { +final class CountOnCloseServerInterceptor: ServerInterceptor, + @unchecked Sendable +{ private let counter: ManagedAtomic init(counter: ManagedAtomic) { diff --git a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift index d5f10a71a..5f61afc4f 100644 --- a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift +++ b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +import NIOConcurrencyHelpers import NIOCore import NIOEmbedded import NIOHPACK @@ -125,16 +126,25 @@ class ServerInterceptorPipelineTests: GRPCTestCase { } internal class RecordingServerInterceptor: - ServerInterceptor + ServerInterceptor, @unchecked Sendable { - var requestParts: [GRPCServerRequestPart] = [] - var responseParts: [GRPCServerResponsePart] = [] + private let lock = NIOLock() + private var _requestParts: [GRPCServerRequestPart] = [] + private var _responseParts: [GRPCServerResponsePart] = [] + + var requestParts: [GRPCServerRequestPart] { + self.lock.withLock { self._requestParts } + } + + var responseParts: [GRPCServerResponsePart] { + self.lock.withLock { self._responseParts } + } override func receive( _ part: GRPCServerRequestPart, context: ServerInterceptorContext ) { - self.requestParts.append(part) + self.lock.withLock { self._requestParts.append(part) } context.receive(part) } @@ -143,7 +153,7 @@ internal class RecordingServerInterceptor: promise: EventLoopPromise?, context: ServerInterceptorContext ) { - self.responseParts.append(part) + self.lock.withLock { self._responseParts.append(part) } context.send(part, promise: promise) } } diff --git a/Tests/GRPCTests/ServerInterceptorTests.swift b/Tests/GRPCTests/ServerInterceptorTests.swift index 34a8cc760..f4ff18da5 100644 --- a/Tests/GRPCTests/ServerInterceptorTests.swift +++ b/Tests/GRPCTests/ServerInterceptorTests.swift @@ -204,7 +204,10 @@ final class EchoInterceptorFactory: Echo_EchoServerInterceptorFactoryProtocol { } } -class ExtraRequestPartEmitter: ServerInterceptor { +class ExtraRequestPartEmitter: + ServerInterceptor, + @unchecked Sendable +{ enum Part { case metadata case message @@ -293,7 +296,7 @@ class EchoFromInterceptor: Echo_EchoProvider { // Since all methods use the same request/response types, we can use a single interceptor to // respond to all of them. - class Interceptor: ServerInterceptor { + class Interceptor: ServerInterceptor, @unchecked Sendable { private var collectedRequests: [Echo_EchoRequest] = [] override func receive( From fea1b7226ccd3794ff782725c5915949fc3e776a Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Tue, 18 Jun 2024 08:53:53 +0100 Subject: [PATCH 359/580] Remove input/output typealiases for RPC methods (#1937) Motivation: The generated code for v2 includes typealiases for the input/output types for each RPC method. These typealiases end up making things less clear to the end user as they must follow the typealias to know what type is being used. Modifications: - Adjust `GRPCCodeGen` (both for the client and server) to not generate typealiases for the input/output types. - Adjust `TestService` to use the actual message types. Result: The generated code will use the actual message types for the input/output types. --- .../Translator/ClientCodeTranslator.swift | 106 ++--- .../Translator/ServerCodeTranslator.swift | 90 ++--- .../Generated/test.grpc.swift | 370 +++++++++--------- .../InteroperabilityTests/TestService.swift | 38 +- .../grpc_testing_benchmark_service.grpc.swift | 166 ++++---- .../grpc_testing_worker_service.grpc.swift | 36 +- ...lientCodeTranslatorSnippetBasedTests.swift | 168 ++++---- ...erverCodeTranslatorSnippetBasedTests.swift | 68 ++-- .../ProtobufCodeGeneratorTests.swift | 68 ++-- 9 files changed, 520 insertions(+), 590 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 77cb6d639..a9ccf4d1a 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -18,51 +18,56 @@ /// specifications, using types from ``StructuredSwiftRepresentation``. /// /// For example, in the case of a service called "Bar", in the "foo" namespace which has -/// one method "baz", the ``ClientCodeTranslator`` will create +/// one method "baz" with input type "Input" and output type "Output", the ``ClientCodeTranslator`` will create /// a representation for the following generated code: /// /// ```swift /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarClientProtocol: Sendable { -/// func baz( -/// request: ClientRequest.Single, -/// serializer: some MessageSerializer, -/// deserializer: some MessageDeserializer, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R -/// ) async throws -> ServerResponse.Stream +/// func baz( +/// request: ClientRequest.Single, +/// serializer: some MessageSerializer, +/// deserializer: some MessageDeserializer, +/// options: CallOptions = .defaults, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async throws -> R where R: Sendable /// } /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// extension Foo.Bar.ClientProtocol { -/// public func get( -/// request: ClientRequest.Single, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R -/// ) async rethrows -> R { +/// extension Foo_Bar.ClientProtocol { +/// public func baz( +/// request: ClientRequest.Single, +/// options: CallOptions = .defaults, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async throws -> R where R: Sendable { /// try await self.baz( /// request: request, -/// serializer: ProtobufSerializer(), -/// deserializer: ProtobufDeserializer(), +/// serializer: ProtobufSerializer(), +/// deserializer: ProtobufDeserializer(), +/// options: options, /// body /// ) /// } /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// public struct foo_BarClient: foo.Bar.ClientProtocol { +/// public struct Foo_BarClient: Foo_Bar.ClientProtocol { /// private let client: GRPCCore.GRPCClient /// public init(client: GRPCCore.GRPCClient) { /// self.client = client /// } -/// public func methodA( -/// request: ClientRequest.Stream, -/// serializer: some MessageSerializer, -/// deserializer: some MessageDeserializer, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R -/// ) async rethrows -> R { -/// try await self.client.clientStreaming( -/// request: request, -/// descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, -/// serializer: serializer, -/// deserializer: deserializer, -/// handler: body -/// ) +/// public func methodA( +/// request: ClientRequest.Stream, +/// serializer: some MessageSerializer, +/// deserializer: some MessageDeserializer, +/// options: CallOptions = .defaults, +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// ) async throws -> R where R: Sendable { +/// try await self.client.unary( +/// request: request, +/// descriptor: NamespaceA.ServiceA.Method.MethodA.descriptor, +/// serializer: serializer, +/// deserializer: deserializer, +/// options: options, +/// handler: body +/// ) /// } /// } ///``` @@ -214,18 +219,12 @@ extension ClientCodeTranslator { FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), FunctionArgumentDescription( label: "serializer", - expression: .identifierPattern( - codeGenerationRequest.lookupSerializer( - self.methodInputOutputTypealias(for: method, service: service, type: .input) - ) - ) + expression: .identifierPattern(codeGenerationRequest.lookupSerializer(method.inputType)) ), FunctionArgumentDescription( label: "deserializer", expression: .identifierPattern( - codeGenerationRequest.lookupDeserializer( - self.methodInputOutputTypealias(for: method, service: service, type: .output) - ) + codeGenerationRequest.lookupDeserializer(method.outputType) ) ), FunctionArgumentDescription(label: "options", expression: .identifierPattern("options")), @@ -273,9 +272,7 @@ extension ClientCodeTranslator { label: "request", type: .generic( wrapper: clientRequestType, - wrapped: .member( - self.methodInputOutputTypealias(for: method, service: service, type: .input) - ) + wrapped: .member(method.inputType) ) ) } @@ -289,9 +286,7 @@ extension ClientCodeTranslator { type: ExistingTypeDescription.some( .generic( wrapper: .member("MessageSerializer"), - wrapped: .member( - self.methodInputOutputTypealias(for: method, service: service, type: .input) - ) + wrapped: .member(method.inputType) ) ) ) @@ -306,9 +301,7 @@ extension ClientCodeTranslator { type: ExistingTypeDescription.some( .generic( wrapper: .member("MessageDeserializer"), - wrapped: .member( - self.methodInputOutputTypealias(for: method, service: service, type: .output) - ) + wrapped: .member(method.outputType) ) ) ) @@ -321,9 +314,7 @@ extension ClientCodeTranslator { let clientStreaming = method.isOutputStreaming ? "Stream" : "Single" let closureParameterType = ExistingTypeDescription.generic( wrapper: .member(["ClientResponse", clientStreaming]), - wrapped: .member( - self.methodInputOutputTypealias(for: method, service: service, type: .output) - ) + wrapped: .member(method.outputType) ) let bodyClosure = ClosureSignatureDescription( @@ -458,23 +449,4 @@ extension ClientCodeTranslator { case input case output } - - /// Generates the fully qualified name of the typealias for the input or output type of a method. - private func methodInputOutputTypealias( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - service: CodeGenerationRequest.ServiceDescriptor, - type: InputOutputType - ) -> String { - var components: String = - "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase)" - - switch type { - case .input: - components.append(".Input") - case .output: - components.append(".Output") - } - - return components - } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 002ae75dc..391a8d002 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -18,41 +18,41 @@ /// specifications, using types from ``StructuredSwiftRepresentation``. /// /// For example, in the case of a service called "Bar", in the "foo" namespace which has -/// one method "baz", the ``ServerCodeTranslator`` will create +/// one method "baz" with input type "Input" and output type "Output", the ``ServerCodeTranslator`` will create /// a representation for the following generated code: /// /// ```swift /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// public protocol foo_BarServiceStreamingProtocol: GRPCCore.RegistrableRPCService { +/// public protocol Foo_BarStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// func baz( -/// request: ServerRequest.Stream -/// ) async throws -> ServerResponse.Stream +/// request: ServerRequest.Stream +/// ) async throws -> ServerResponse.Stream /// } -/// // Generated conformance to `RegistrableRPCService`. +/// // Conformance to `GRPCCore.RegistrableRPCService`. /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// extension foo.Bar.StreamingServiceProtocol { -/// public func registerRPCs(with router: inout RPCRouter) { +/// extension Foo_Bar.StreamingServiceProtocol { +/// public func registerMethods(with router: inout GRPCCore.RPCRouter) { /// router.registerHandler( -/// forMethod: foo.Method.baz.descriptor, -/// deserializer: ProtobufDeserializer(), -/// serializer: ProtobufSerializer(), +/// forMethod: Foo_Bar.Method.baz.descriptor, +/// deserializer: ProtobufDeserializer(), +/// serializer: ProtobufSerializer(), /// handler: { request in try await self.baz(request: request) } /// ) /// } /// } /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// public protocol foo_BarServiceProtocol: foo.Bar.StreamingServiceProtocol { +/// public protocol Foo_BarServiceProtocol: Foo_Bar.StreamingServiceProtocol { /// func baz( -/// request: ServerRequest.Single -/// ) async throws -> ServerResponse.Single +/// request: ServerRequest.Single +/// ) async throws -> ServerResponse.Single /// } -/// // Generated partial conformance to `foo_BarStreamingServiceProtocol`. +/// // Partial conformance to `Foo_BarStreamingServiceProtocol`. /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// extension foo.Bar.ServiceProtocol { +/// extension Foo_Bar.ServiceProtocol { /// public func baz( -/// request: ServerRequest.Stream -/// ) async throws -> ServerResponse.Stream { -/// let response = try await self.baz(request: ServerRequest.Single(stream: request) +/// request: ServerRequest.Stream +/// ) async throws -> ServerResponse.Stream { +/// let response = try await self.baz(request: ServerRequest.Single(stream: request)) /// return ServerResponse.Stream(single: response) /// } /// } @@ -148,9 +148,7 @@ extension ServerCodeTranslator { label: "request", type: .generic( wrapper: .member(["ServerRequest", "Stream"]), - wrapped: .member( - self.methodInputOutputTypealias(for: method, service: service, type: .input) - ) + wrapped: .member(method.inputType) ) ) ], @@ -158,9 +156,7 @@ extension ServerCodeTranslator { returnType: .identifierType( .generic( wrapper: .member(["ServerResponse", "Stream"]), - wrapped: .member( - self.methodInputOutputTypealias(for: method, service: service, type: .output) - ) + wrapped: .member(method.outputType) ) ) ) @@ -244,11 +240,7 @@ extension ServerCodeTranslator { arguments.append( .init( label: "deserializer", - expression: .identifierPattern( - codeGenerationRequest.lookupDeserializer( - self.methodInputOutputTypealias(for: method, service: service, type: .input) - ) - ) + expression: .identifierPattern(codeGenerationRequest.lookupDeserializer(method.inputType)) ) ) @@ -256,11 +248,7 @@ extension ServerCodeTranslator { .init( label: "serializer", expression: - .identifierPattern( - codeGenerationRequest.lookupSerializer( - self.methodInputOutputTypealias(for: method, service: service, type: .output) - ) - ) + .identifierPattern(codeGenerationRequest.lookupSerializer(method.outputType)) ) ) @@ -326,17 +314,6 @@ extension ServerCodeTranslator { let inputStreaming = method.isInputStreaming ? "Stream" : "Single" let outputStreaming = method.isOutputStreaming ? "Stream" : "Single" - let inputTypealiasComponents = self.methodInputOutputTypealias( - for: method, - service: service, - type: .input - ) - let outputTypealiasComponents = self.methodInputOutputTypealias( - for: method, - service: service, - type: .output - ) - let functionSignature = FunctionSignatureDescription( accessModifier: accessModifier, kind: .function(name: method.name.generatedLowerCase), @@ -346,7 +323,7 @@ extension ServerCodeTranslator { type: .generic( wrapper: .member(["ServerRequest", inputStreaming]), - wrapped: .member(inputTypealiasComponents) + wrapped: .member(method.inputType) ) ) ], @@ -354,7 +331,7 @@ extension ServerCodeTranslator { returnType: .identifierType( .generic( wrapper: .member(["ServerResponse", outputStreaming]), - wrapped: .member(outputTypealiasComponents) + wrapped: .member(method.outputType) ) ) ) @@ -472,25 +449,6 @@ extension ServerCodeTranslator { case output } - /// Generates the fully qualified name of the typealias for the input or output type of a method. - private func methodInputOutputTypealias( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - service: CodeGenerationRequest.ServiceDescriptor, - type: InputOutputType - ) -> String { - var components: String = - "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase)" - - switch type { - case .input: - components.append(".Input") - case .output: - components.append(".Output") - } - - return components - } - /// Generates the fully qualified name of a method descriptor. private func methodDescriptorPath( for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index 71b6cb901..3defa4bc4 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -176,38 +176,38 @@ public enum Grpc_Testing_UnimplementedService { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One empty request followed by one empty response. - func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// One request followed by one response. - func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. - func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. - func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. - func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. - func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. - func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. - func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -217,64 +217,64 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_TestService.Method.EmptyCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.emptyCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.UnaryCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unaryCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.cacheableUnaryCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.streamingOutputCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.streamingInputCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.fullDuplexCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.halfDuplexCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unimplementedCall(request: request) } @@ -287,69 +287,69 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestService.StreamingServiceProtocol { /// One empty request followed by one empty response. - func emptyCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func emptyCall(request: ServerRequest.Single) async throws -> ServerResponse.Single /// One request followed by one response. - func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. - func cacheableUnaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func cacheableUnaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. - func streamingOutputCall(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func streamingOutputCall(request: ServerRequest.Single) async throws -> ServerResponse.Stream /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. - func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Single /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. - func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. - func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. - func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Grpc_Testing_TestServiceStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_TestService.ServiceProtocol { - public func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.emptyCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - public func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - public func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.cacheableUnaryCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - public func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingOutputCall(request: ServerRequest.Single(stream: request)) return response } - public func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingInputCall(request: request) return ServerResponse.Stream(single: response) } - public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -360,7 +360,7 @@ extension Grpc_Testing_TestService.ServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Grpc_Testing_UnimplementedServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// A call that no server should implement - func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -370,8 +370,8 @@ extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unimplementedCall(request: request) } @@ -384,13 +384,13 @@ extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Grpc_Testing_UnimplementedServiceServiceProtocol: Grpc_Testing_UnimplementedService.StreamingServiceProtocol { /// A call that no server should implement - func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Grpc_Testing_UnimplementedServiceStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_UnimplementedService.ServiceProtocol { - public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -399,9 +399,9 @@ extension Grpc_Testing_UnimplementedService.ServiceProtocol { /// A service used to control reconnect server. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -411,16 +411,16 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_ReconnectService.Method.Start.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.start(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_ReconnectService.Method.Stop.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.stop(request: request) } @@ -431,20 +431,20 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { /// A service used to control reconnect server. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_ReconnectService.StreamingServiceProtocol { - func start(request: ServerRequest.Single) async throws -> ServerResponse.Single + func start(request: ServerRequest.Single) async throws -> ServerResponse.Single - func stop(request: ServerRequest.Single) async throws -> ServerResponse.Single + func stop(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Grpc_Testing_ReconnectServiceStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_ReconnectService.ServiceProtocol { - public func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.start(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - public func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.stop(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -456,62 +456,62 @@ extension Grpc_Testing_ReconnectService.ServiceProtocol { public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { /// One empty request followed by one empty response. func emptyCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// One request followed by one response. func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. func cacheableUnaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. func streamingOutputCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. func streamingInputCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. func fullDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// A sequence of requests followed by a sequence of responses. @@ -519,133 +519,133 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { /// stream of responses are returned to the client when the server starts with /// first request. func halfDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_TestService.ClientProtocol { public func emptyCall( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.emptyCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func unaryCall( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func cacheableUnaryCall( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.cacheableUnaryCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func streamingOutputCall( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingOutputCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func streamingInputCall( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingInputCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func fullDuplexCall( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.fullDuplexCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func halfDuplexCall( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.halfDuplexCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func unimplementedCall( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -664,11 +664,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// One empty request followed by one empty response. public func emptyCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -682,11 +682,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// One request followed by one response. public func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -702,11 +702,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. public func cacheableUnaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -721,11 +721,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. public func streamingOutputCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -740,11 +740,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. public func streamingInputCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -760,11 +760,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. public func fullDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -781,11 +781,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// stream of responses are returned to the client when the server starts with /// first request. public func halfDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -800,11 +800,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. public func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -823,25 +823,25 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { /// A call that no server should implement func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_UnimplementedService.ClientProtocol { public func unimplementedCall( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -860,11 +860,11 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente /// A call that no server should implement public func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -881,47 +881,47 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { func start( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable func stop( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_ReconnectService.ClientProtocol { public func start( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.start( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } public func stop( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.stop( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -938,11 +938,11 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService } public func start( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -955,11 +955,11 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService } public func stop( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift index da8c8567b..0307c4e1a 100644 --- a/Sources/InteroperabilityTests/TestService.swift +++ b/Sources/InteroperabilityTests/TestService.swift @@ -22,18 +22,18 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { public init() {} public func unimplementedCall( - request: ServerRequest.Single + request: ServerRequest.Single ) async throws - -> ServerResponse.Single + -> ServerResponse.Single { throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") } /// Server implements `emptyCall` which immediately returns the empty message. public func emptyCall( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { - let message = Grpc_Testing_TestService.Method.EmptyCall.Output() + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let message = Grpc_Testing_Empty() let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() return ServerResponse.Single( message: message, @@ -49,8 +49,8 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// If the server does not support the `responseType`, then it should fail the RPC with /// `INVALID_ARGUMENT`. public func unaryCall( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is // set), so we have to check via the encoding header. Note that it is possible for the header // to be set and for the message to not be compressed. @@ -84,7 +84,7 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { throw RPCError(code: .invalidArgument, message: "The response type is not recognized.") } - let responseMessage = Grpc_Testing_TestService.Method.UnaryCall.Output.with { response in + let responseMessage = Grpc_Testing_SimpleResponse.with { response in response.payload = Grpc_Testing_Payload.with { payload in payload.body = Data(repeating: 0, count: Int(request.message.responseSize)) payload.type = request.message.responseType @@ -108,9 +108,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// headers such that the response can be cached by proxies in the response path. Server should /// be behind a caching proxy for this test to pass. Currently we set the max-age to 60 seconds. public func cacheableUnaryCall( - request: ServerRequest.Single + request: ServerRequest.Single ) async throws - -> ServerResponse.Single + -> ServerResponse.Single { throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") } @@ -122,10 +122,10 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// closes with OK. public func streamingOutputCall( request: ServerRequest.Single< - Grpc_Testing_TestService.Method.StreamingOutputCall.Input + Grpc_Testing_StreamingOutputCallRequest > ) async throws - -> ServerResponse.Stream + -> ServerResponse.Stream { let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() return ServerResponse.Stream(metadata: initialMetadata) { writer in @@ -147,9 +147,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// `StreamingInputCallResponse` where `aggregatedPayloadSize` is the sum of all request payload /// bodies received. public func streamingInputCall( - request: ServerRequest.Stream + request: ServerRequest.Stream ) async throws - -> ServerResponse.Single + -> ServerResponse.Single { let isRequestCompressed = request.metadata["grpc-encoding"].filter({ $0 != "identity" }).count > 0 @@ -169,7 +169,7 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { aggregatedPayloadSize += message.payload.body.count } - let responseMessage = Grpc_Testing_TestService.Method.StreamingInputCall.Output.with { + let responseMessage = Grpc_Testing_StreamingInputCallResponse.with { $0.aggregatedPayloadSize = Int32(aggregatedPayloadSize) } @@ -187,9 +187,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// of size `ResponseParameter.size` bytes, as specified by its respective `ResponseParameter`s. /// After receiving half close and sending all responses, it closes with OK. public func fullDuplexCall( - request: ServerRequest.Stream + request: ServerRequest.Stream ) async throws - -> ServerResponse.Stream + -> ServerResponse.Stream { let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() return ServerResponse.Stream(metadata: initialMetadata) { writer in @@ -226,9 +226,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md public func halfDuplexCall( - request: ServerRequest.Stream + request: ServerRequest.Stream ) async throws - -> ServerResponse.Stream + -> ServerResponse.Stream { throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") } diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index caa9bf8d1..c5c433b34 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -91,24 +91,24 @@ internal enum Grpc_Testing_BenchmarkService { internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One request followed by one response. /// The server returns the client payload as-is. - func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response - func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is - func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other - func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -118,40 +118,40 @@ extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unaryCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.streamingCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.streamingFromClient(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.streamingFromServer(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.streamingBothWays(request: request) } @@ -163,40 +163,40 @@ extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_BenchmarkService.StreamingServiceProtocol { /// One request followed by one response. /// The server returns the client payload as-is. - func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response - func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Single /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is - func streamingFromServer(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func streamingFromServer(request: ServerRequest.Single) async throws -> ServerResponse.Stream /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other - func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `Grpc_Testing_BenchmarkServiceStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_BenchmarkService.ServiceProtocol { - internal func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - internal func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingFromClient(request: request) return ServerResponse.Stream(single: response) } - internal func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingFromServer(request: ServerRequest.Single(stream: request)) return response } @@ -207,122 +207,122 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { /// One request followed by one response. /// The server returns the client payload as-is. func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response func streamingCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone func streamingFromClient( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is func streamingFromServer( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other func streamingBothWays( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_BenchmarkService.ClientProtocol { internal func unaryCall( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } internal func streamingCall( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } internal func streamingFromClient( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingFromClient( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } internal func streamingFromServer( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingFromServer( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } internal func streamingBothWays( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingBothWays( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -340,11 +340,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// One request followed by one response. /// The server returns the client payload as-is. internal func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -360,11 +360,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response internal func streamingCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -379,11 +379,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone internal func streamingFromClient( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -398,11 +398,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is internal func streamingFromServer( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -417,11 +417,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other internal func streamingBothWays( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index 0796543c2..0e05eaa89 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -82,7 +82,7 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// stats. Closing the stream will initiate shutdown of the test server /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus @@ -90,13 +90,13 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// stats. Closing the stream will initiate shutdown of the test client /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Just return the core count - unary call - func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Quit this worker - func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -106,32 +106,32 @@ extension Grpc_Testing_WorkerService.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.RunServer.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.runServer(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.RunClient.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.runClient(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.CoreCount.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.coreCount(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.QuitWorker.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.quitWorker(request: request) } @@ -147,7 +147,7 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// stats. Closing the stream will initiate shutdown of the test server /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus @@ -155,24 +155,24 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// stats. Closing the stream will initiate shutdown of the test client /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Just return the core count - unary call - func coreCount(request: ServerRequest.Single) async throws -> ServerResponse.Single + func coreCount(request: ServerRequest.Single) async throws -> ServerResponse.Single /// Quit this worker - func quitWorker(request: ServerRequest.Single) async throws -> ServerResponse.Single + func quitWorker(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Grpc_Testing_WorkerServiceStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Grpc_Testing_WorkerService.ServiceProtocol { - internal func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.coreCount(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - internal func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.quitWorker(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 3a8f10d35..4ec08dcfc 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -47,24 +47,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -81,11 +81,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -128,24 +128,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -162,11 +162,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -209,24 +209,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -243,11 +243,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -290,24 +290,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -324,11 +324,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -379,47 +379,47 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Documentation for MethodB func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ClientProtocol { package func methodA( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } package func methodB( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodB( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -436,11 +436,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA package func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -454,11 +454,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodB package func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -501,24 +501,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceA.ClientProtocol { internal func methodA( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -535,11 +535,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA internal func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index ee909d567..9d59ebb6d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -54,7 +54,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod - func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -63,8 +63,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unary(request: request) } @@ -75,12 +75,12 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod - func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unary(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -123,7 +123,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -132,8 +132,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } @@ -144,12 +144,12 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } @@ -196,7 +196,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -205,8 +205,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -217,12 +217,12 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } @@ -269,7 +269,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -278,8 +278,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.BidirectionalStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.bidirectionalStreaming(request: request) } @@ -290,7 +290,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -350,10 +350,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -362,16 +362,16 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } ) router.registerHandler( forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -382,20 +382,20 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) return ServerResponse.Stream(single: response) } - internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) return response } @@ -434,7 +434,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA - func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -443,8 +443,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: ServiceA.Method.MethodA.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.methodA(request: request) } @@ -455,12 +455,12 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA - func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single + func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceA.ServiceProtocol { - internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.methodA(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 07f275658..fd0d89d5f 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -84,25 +84,25 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal protocol Hello_World_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Hello_World_Greeter.ClientProtocol { internal func sayHello( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -120,11 +120,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. internal func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -198,7 +198,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -208,8 +208,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Helloworld_Greeter.Method.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -221,13 +221,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Helloworld_Greeter.ServiceProtocol { - public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -297,7 +297,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -307,8 +307,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Greeter.Method.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -320,13 +320,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package protocol GreeterServiceProtocol: Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `GreeterStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Greeter.ServiceProtocol { - package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } @@ -337,25 +337,25 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package protocol GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Greeter.ClientProtocol { package func sayHello( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -373,11 +373,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. package func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, From 8ad7aab95c7cdff58eeff775b69e3caa007b9ac8 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 18 Jun 2024 13:20:34 +0100 Subject: [PATCH 360/580] Add NIOTS server transport (#1934) --- Package.swift | 8 +- .../NIOSocketAddress+GRPCSocketAddress.swift} | 17 +- .../NIOClientBootstrap+SocketAddress.swift | 16 +- ...TP2ServerTransport+TransportServices.swift | 222 ++++++++++++++++++ 4 files changed, 244 insertions(+), 19 deletions(-) rename Sources/{GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2TransportNIOTransportServices.swift => GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift} (56%) create mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift diff --git a/Package.swift b/Package.swift index f8a1c7287..4f6797c8e 100644 --- a/Package.swift +++ b/Package.swift @@ -216,7 +216,9 @@ extension Target { static let grpcHTTP2TransportNIOPosix: Target = .target( name: "GRPCHTTP2TransportNIOPosix", dependencies: [ + .grpcCore, .grpcHTTP2Core, + .nioPosix, .nioExtras ] ) @@ -224,7 +226,11 @@ extension Target { static let grpcHTTP2TransportNIOTransportServices: Target = .target( name: "GRPCHTTP2TransportNIOTransportServices", dependencies: [ - .grpcHTTP2Core + .grpcCore, + .grpcHTTP2Core, + .nioCore, + .nioExtras, + .nioTransportServices ] ) diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2TransportNIOTransportServices.swift b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift similarity index 56% rename from Sources/GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2TransportNIOTransportServices.swift rename to Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift index a1e1d80a7..dea75ac66 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2TransportNIOTransportServices.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift @@ -14,8 +14,19 @@ * limitations under the License. */ -import GRPCCore +import NIOCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCHTTP2TransportNIOTransportServices { +@_spi(Package) +extension NIOCore.SocketAddress { + public init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { + try self.init(ipAddress: address.host, port: address.port) + } + + public init(_ address: GRPCHTTP2Core.SocketAddress.IPv6) throws { + try self.init(ipAddress: address.host, port: address.port) + } + + public init(_ address: GRPCHTTP2Core.SocketAddress.UnixDomainSocket) throws { + try self.init(unixDomainSocketPath: address.path) + } } diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift index 0385ed2f4..b9b81f431 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift @@ -15,7 +15,7 @@ */ import GRPCCore -import GRPCHTTP2Core +@_spi(Package) import GRPCHTTP2Core import NIOCore import NIOPosix @@ -45,20 +45,6 @@ extension ClientBootstrap { } } -extension NIOCore.SocketAddress { - init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { - try self.init(ipAddress: address.host, port: address.port) - } - - init(_ address: GRPCHTTP2Core.SocketAddress.IPv6) throws { - try self.init(ipAddress: address.host, port: address.port) - } - - init(_ address: GRPCHTTP2Core.SocketAddress.UnixDomainSocket) throws { - try self.init(unixDomainSocketPath: address.path) - } -} - extension NIOPosix.VsockAddress { init(_ address: GRPCHTTP2Core.SocketAddress.VirtualSocket) { self.init( diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift new file mode 100644 index 000000000..68e5a8593 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -0,0 +1,222 @@ +/* + * 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. + */ + +#if canImport(Network) +import GRPCCore +@_spi(Package) import GRPCHTTP2Core +import NIOCore +import NIOExtras +import NIOTransportServices + +extension HTTP2ServerTransport { + /// A NIO Transport Services-backed implementation of a server transport. + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + public struct TransportServices: ServerTransport { + private let address: GRPCHTTP2Core.SocketAddress + private let config: Config + private let eventLoopGroup: NIOTSEventLoopGroup + private let serverQuiescingHelper: ServerQuiescingHelper + + /// Create a new `TransportServices` transport. + /// + /// - Parameters: + /// - address: The address to which the server should be bound. + /// - config: The transport configuration. + /// - eventLoopGroup: The ELG from which to get ELs to run this transport. + public init( + address: GRPCHTTP2Core.SocketAddress, + config: Config = .defaults, + eventLoopGroup: NIOTSEventLoopGroup = .singletonNIOTSEventLoopGroup + ) { + self.address = address + self.config = config + self.eventLoopGroup = eventLoopGroup + self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) + } + + public func listen( + _ streamHandler: @escaping (RPCStream) async -> Void + ) async throws { + let serverChannel = try await NIOTSListenerBootstrap(group: self.eventLoopGroup) + .serverChannelInitializer { channel in + let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( + channel: channel + ) + return channel.pipeline.addHandler(quiescingHandler) + } + .bind(to: self.address) { channel in + channel.eventLoop.makeCompletedFuture { + return try channel.pipeline.syncOperations.configureGRPCServerPipeline( + channel: channel, + compressionConfig: self.config.compression, + connectionConfig: self.config.connection, + http2Config: self.config.http2, + rpcConfig: self.config.rpc, + useTLS: false + ) + } + } + + try await serverChannel.executeThenClose { inbound in + try await withThrowingDiscardingTaskGroup { serverTaskGroup in + for try await (connectionChannel, streamMultiplexer) in inbound { + serverTaskGroup.addTask { + try await connectionChannel + .executeThenClose { connectionInbound, connectionOutbound in + await withDiscardingTaskGroup { connectionTaskGroup in + connectionTaskGroup.addTask { + do { + for try await _ in connectionInbound {} + } catch { + // We don't want to close the channel if one connection throws. + return + } + } + + connectionTaskGroup.addTask { + await withDiscardingTaskGroup { streamTaskGroup in + do { + for try await (http2Stream, methodDescriptor) in streamMultiplexer.inbound + { + streamTaskGroup.addTask { + // It's okay to ignore these errors: + // - If we get an error because the http2Stream failed to close, then there's nothing we can do + // - If we get an error because the inner closure threw, then the only possible scenario in which + // that could happen is if methodDescriptor.get() throws - in which case, it means we never got + // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. + try? await http2Stream.executeThenClose { inbound, outbound in + guard let descriptor = try? await methodDescriptor.get() else { + return + } + let rpcStream = RPCStream( + descriptor: descriptor, + inbound: RPCAsyncSequence(wrapping: inbound), + outbound: RPCWriter.Closable( + wrapping: ServerConnection.Stream.Outbound( + responseWriter: outbound, + http2Stream: http2Stream + ) + ) + ) + await streamHandler(rpcStream) + } + } + } + } catch { + // We don't want to close the whole connection if one stream throws. + return + } + } + } + } + } + } + } + } + } + } + + public func stopListening() { + self.serverQuiescingHelper.initiateShutdown(promise: nil) + } + } + +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension HTTP2ServerTransport.TransportServices { + /// Configuration for the ``GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2Core/HTTP2ServerTransport/TransportServices``. + public struct Config: Sendable { + /// Compression configuration. + public var compression: HTTP2ServerTransport.Config.Compression + /// Connection configuration. + public var connection: HTTP2ServerTransport.Config.Connection + /// HTTP2 configuration. + public var http2: HTTP2ServerTransport.Config.HTTP2 + /// RPC configuration. + public var rpc: HTTP2ServerTransport.Config.RPC + + /// Construct a new `Config`. + /// - Parameters: + /// - compression: Compression configuration. + /// - connection: Connection configuration. + /// - http2: HTTP2 configuration. + /// - rpc: RPC configuration. + public init( + compression: HTTP2ServerTransport.Config.Compression, + connection: HTTP2ServerTransport.Config.Connection, + http2: HTTP2ServerTransport.Config.HTTP2, + rpc: HTTP2ServerTransport.Config.RPC + ) { + self.compression = compression + self.connection = connection + self.http2 = http2 + self.rpc = rpc + } + + /// Default values for the different configurations. + public static var defaults: Self { + Self( + compression: .defaults, + connection: .defaults, + http2: .defaults, + rpc: .defaults + ) + } + } +} + +extension NIOCore.SocketAddress { + fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws { + if let ipv4 = socketAddress.ipv4 { + self = try Self(ipv4) + } else if let ipv6 = socketAddress.ipv6 { + self = try Self(ipv6) + } else if let unixDomainSocket = socketAddress.unixDomainSocket { + self = try Self(unixDomainSocket) + } else { + throw RPCError( + code: .internalError, + message: + "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)." + ) + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension NIOTSListenerBootstrap { + fileprivate func bind( + to address: GRPCHTTP2Core.SocketAddress, + childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture + ) async throws -> NIOAsyncChannel { + if let virtualSocket = address.virtualSocket { + throw RuntimeError( + code: .transportError, + message: """ + Virtual sockets are not supported by 'HTTP2ServerTransport.TransportServices'. \ + Please use the 'HTTP2ServerTransport.Posix' transport. + """ + ) + } else { + return try await self.bind( + to: NIOCore.SocketAddress(address), + childChannelInitializer: childChannelInitializer + ) + } + } +} +#endif From 468a51951a35be59c920e1fc40bc38d36b8581d0 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 18 Jun 2024 14:28:29 +0100 Subject: [PATCH 361/580] Expose listening address on HTTP2 server transport (#1933) Motivation: We should expose the listening address on the H2 server transports so that our client can connect to it. Modifications: Exposed a `listeningAddress` async property. It will block until we bind or error, return the listening address while it's bound and listening, and throw an error when it's not listening anymore. Result: We can now know what address our server transport's listening on for requests. --- .../NIOSocketAddress+GRPCSocketAddress.swift | 22 +++ .../HTTP2ServerTransport+Posix.swift | 144 ++++++++++++++++ .../HTTP2TransportNIOPosixTests.swift | 160 ++++++++++++++++++ .../XCTestCase+Vsock.swift | 34 ++++ 4 files changed, 360 insertions(+) create mode 100644 Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift create mode 100644 Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift diff --git a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift index dea75ac66..b506bc4d8 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift @@ -16,6 +16,28 @@ import NIOCore +@_spi(Package) +extension GRPCHTTP2Core.SocketAddress { + public init(_ nioSocketAddress: NIOCore.SocketAddress) { + switch nioSocketAddress { + case .v4(let address): + self = .ipv4( + host: address.host, + port: nioSocketAddress.port ?? 0 + ) + + case .v6(let address): + self = .ipv6( + host: address.host, + port: nioSocketAddress.port ?? 0 + ) + + case .unixDomainSocket: + self = .unixDomainSocket(path: nioSocketAddress.pathname ?? "") + } + } +} + @_spi(Package) extension NIOCore.SocketAddress { public init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 9bce6e4fa..8d5175fb8 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -21,6 +21,7 @@ import NIOExtras import NIOPosix extension HTTP2ServerTransport { + /// A NIOPosix-backed implementation of a server transport. @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public struct Posix: ServerTransport { private let address: GRPCHTTP2Core.SocketAddress @@ -28,6 +29,124 @@ extension HTTP2ServerTransport { private let eventLoopGroup: MultiThreadedEventLoopGroup private let serverQuiescingHelper: ServerQuiescingHelper + private enum State { + case idle(EventLoopPromise) + case listening(EventLoopFuture) + case closedOrInvalidAddress(RuntimeError) + + public var listeningAddressFuture: EventLoopFuture { + get throws { + switch self { + case .idle(let eventLoopPromise): + return eventLoopPromise.futureResult + case .listening(let eventLoopFuture): + return eventLoopFuture + case .closedOrInvalidAddress(let runtimeError): + throw runtimeError + } + } + } + + enum OnBound { + case succeedPromise( + _ promise: EventLoopPromise, + address: GRPCHTTP2Core.SocketAddress + ) + case failPromise( + _ promise: EventLoopPromise, + error: RuntimeError + ) + } + + mutating func addressBound( + _ address: NIOCore.SocketAddress?, + userProvidedAddress: GRPCHTTP2Core.SocketAddress + ) -> OnBound { + switch self { + case .idle(let listeningAddressPromise): + if let address { + self = .listening(listeningAddressPromise.futureResult) + return .succeedPromise( + listeningAddressPromise, + address: GRPCHTTP2Core.SocketAddress(address) + ) + + } else if userProvidedAddress.virtualSocket != nil { + self = .listening(listeningAddressPromise.futureResult) + return .succeedPromise(listeningAddressPromise, address: userProvidedAddress) + + } else { + assertionFailure("Unknown address type") + let invalidAddressError = RuntimeError( + code: .transportError, + message: "Unknown address type returned by transport." + ) + self = .closedOrInvalidAddress(invalidAddressError) + return .failPromise(listeningAddressPromise, error: invalidAddressError) + } + + case .listening, .closedOrInvalidAddress: + fatalError( + "Invalid state: addressBound should only be called once and when in idle state" + ) + } + } + + enum OnClose { + case failPromise( + EventLoopPromise, + error: RuntimeError + ) + case doNothing + } + + mutating func close() -> OnClose { + let serverStoppedError = RuntimeError( + code: .serverIsStopped, + message: """ + There is no listening address bound for this server: there may have been \ + an error which caused the transport to close, or it may have shut down. + """ + ) + + switch self { + case .idle(let listeningAddressPromise): + self = .closedOrInvalidAddress(serverStoppedError) + return .failPromise(listeningAddressPromise, error: serverStoppedError) + + case .listening: + self = .closedOrInvalidAddress(serverStoppedError) + return .doNothing + + case .closedOrInvalidAddress: + return .doNothing + } + } + } + + private let listeningAddressState: _LockedValueBox + + /// The listening address for this server transport. + /// + /// It is an `async` property because it will only return once the address has been successfully bound. + /// + /// - Throws: A runtime error will be thrown if the address could not be bound or is not bound any + /// longer, because the transport isn't listening anymore. It can also throw if the transport returned an + /// invalid address. + public var listeningAddress: GRPCHTTP2Core.SocketAddress { + get async throws { + try await self.listeningAddressState + .withLockedValue { try $0.listeningAddressFuture } + .get() + } + } + + /// Create a new `Posix` transport. + /// + /// - Parameters: + /// - address: The address to which the server should be bound. + /// - config: The transport configuration. + /// - eventLoopGroup: The ELG from which to get ELs to run this transport. public init( address: GRPCHTTP2Core.SocketAddress, config: Config = .defaults, @@ -37,11 +156,23 @@ extension HTTP2ServerTransport { self.config = config self.eventLoopGroup = eventLoopGroup self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) + + let eventLoop = eventLoopGroup.any() + self.listeningAddressState = _LockedValueBox(.idle(eventLoop.makePromise())) } public func listen( _ streamHandler: @escaping (RPCStream) async -> Void ) async throws { + defer { + switch self.listeningAddressState.withLockedValue({ $0.close() }) { + case .failPromise(let promise, let error): + promise.fail(error) + case .doNothing: + () + } + } + let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup) .serverChannelInitializer { channel in let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( @@ -62,6 +193,19 @@ extension HTTP2ServerTransport { } } + let action = self.listeningAddressState.withLockedValue { + $0.addressBound( + serverChannel.channel.localAddress, + userProvidedAddress: self.address + ) + } + switch action { + case .succeedPromise(let promise, let address): + promise.succeed(address) + case .failPromise(let promise, let error): + promise.fail(error) + } + try await serverChannel.executeThenClose { inbound in try await withThrowingDiscardingTaskGroup { serverTaskGroup in for try await (connectionChannel, streamMultiplexer) in inbound { diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift new file mode 100644 index 000000000..988d56735 --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -0,0 +1,160 @@ +/* + * 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 GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class HTTP2TransportNIOPosixTests: XCTestCase { + func testGetListeningAddress_IPv4() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( + address: .ipv4(host: "0.0.0.0", port: 0) + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + let address = try await transport.listeningAddress + let ipv4Address = try XCTUnwrap(address.ipv4) + XCTAssertNotEqual(ipv4Address.port, 0) + transport.stopListening() + } + } + } + + func testGetListeningAddress_IPv6() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(address: .ipv6(host: "::1", port: 0)) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + let address = try await transport.listeningAddress + let ipv6Address = try XCTUnwrap(address.ipv6) + XCTAssertNotEqual(ipv6Address.port, 0) + transport.stopListening() + } + } + } + + func testGetListeningAddress_UnixDomainSocket() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( + address: .unixDomainSocket(path: "/tmp/test") + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + let address = try await transport.listeningAddress + XCTAssertEqual( + address.unixDomainSocket, + GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/test") + ) + transport.stopListening() + } + } + } + + func testGetListeningAddress_Vsock() async throws { + try XCTSkipUnless(self.vsockAvailable(), "Vsock unavailable") + + let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( + address: .vsock(contextID: .any, port: .any) + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + let address = try await transport.listeningAddress + XCTAssertNotNil(address.virtualSocket) + transport.stopListening() + } + } + } + + func testGetListeningAddress_InvalidAddress() async { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( + address: .unixDomainSocket(path: "/this/should/be/an/invalid/path") + ) + + try? await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + do { + _ = try await transport.listeningAddress + XCTFail("Should have thrown a RuntimeError") + } catch let error as RuntimeError { + XCTAssertEqual(error.code, .serverIsStopped) + XCTAssertEqual( + error.message, + """ + There is no listening address bound for this server: there may have \ + been an error which caused the transport to close, or it may have shut down. + """ + ) + } + } + } + } + + func testGetListeningAddress_StoppedListening() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( + address: .ipv4(host: "0.0.0.0", port: 0) + ) + + try? await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + + do { + _ = try await transport.listeningAddress + XCTFail("Should have thrown a RuntimeError") + } catch let error as RuntimeError { + XCTAssertEqual(error.code, .serverIsStopped) + XCTAssertEqual( + error.message, + """ + There is no listening address bound for this server: there may have \ + been an error which caused the transport to close, or it may have shut down. + """ + ) + } + } + + group.addTask { + let address = try await transport.listeningAddress + XCTAssertNotNil(address.ipv4) + transport.stopListening() + } + } + } +} diff --git a/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift b/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift new file mode 100644 index 000000000..cb613fb63 --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift @@ -0,0 +1,34 @@ +/* + * 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 NIOPosix +import XCTest + +extension XCTestCase { + func vsockAvailable() -> Bool { + let fd: CInt + #if os(Linux) + fd = socket(AF_VSOCK, CInt(SOCK_STREAM.rawValue), 0) + #elseif canImport(Darwin) + fd = socket(AF_VSOCK, SOCK_STREAM, 0) + #else + fd = -1 + #endif + if fd == -1 { return false } + precondition(close(fd) == 0) + return true + } +} From 8ae00398b63f38adbf54ac549f14147b18cc0f8f Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:28:58 +0100 Subject: [PATCH 362/580] Enable SwiftPM build plugin to use the new code generator plugin (#1938) Motivation: The SwiftPM build plugin could not be configured to use the new code generator plugin. Modifications: - Add a `_V2` boolean option to the SwiftPM build plugin configuration. If this option is set, it is passed to protoc-gen-grpc-swift which then uses the new code generator plugin. Result: The SwiftPM build plugin can now be configured use the new code generator plugin. --- Plugins/GRPCSwiftPlugin/plugin.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index bd0ee045b..42e412f49 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -69,6 +69,8 @@ struct GRPCSwiftPlugin { var reflectionData: Bool? /// Determines whether the casing of generated function names is kept. var keepMethodCasing: Bool? + /// Whether the invocation is for `grpc-swift` v2. + var _V2: Bool? } /// Specify the directory in which to search for @@ -155,7 +157,7 @@ struct GRPCSwiftPlugin { /// - protocPath: The path to the `protoc` binary. /// - protocGenSwiftPath: The path to the `protoc-gen-swift` binary. /// - outputDirectory: The output directory for the generated files. - /// - importPaths: List of paths to pass with "-I " to `protoc` + /// - importPaths: List of paths to pass with "-I " to `protoc`. /// - Returns: The build command configured based on the arguments private func invokeProtoc( directory: Path, @@ -196,6 +198,10 @@ struct GRPCSwiftPlugin { protocArgs.append("--grpc-swift_opt=KeepMethodCasing=\(keepMethodCasingOption)") } + if let v2 = invocation._V2 { + protocArgs.append("--grpc-swift_opt=_V2=\(v2)") + } + var inputFiles = [Path]() var outputFiles = [Path]() From 2dc430890e223ffa8169d39896e6d70c52591e0f Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Tue, 18 Jun 2024 22:26:03 +0100 Subject: [PATCH 363/580] Expose listening address on NIOTS server transport (#1939) --- .../HTTP2ServerTransport+Posix.swift | 2 +- ...TP2ServerTransport+TransportServices.swift | 129 +++++++++++++++- .../HTTP2TransportNIOPosixTests.swift | 8 +- ...P2TransportNIOTransportServicesTests.swift | 144 ++++++++++++++++++ 4 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 8d5175fb8..9151216ef 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -34,7 +34,7 @@ extension HTTP2ServerTransport { case listening(EventLoopFuture) case closedOrInvalidAddress(RuntimeError) - public var listeningAddressFuture: EventLoopFuture { + var listeningAddressFuture: EventLoopFuture { get throws { switch self { case .idle(let eventLoopPromise): diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 68e5a8593..2801ecf6f 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -30,6 +30,111 @@ extension HTTP2ServerTransport { private let eventLoopGroup: NIOTSEventLoopGroup private let serverQuiescingHelper: ServerQuiescingHelper + private enum State { + case idle(EventLoopPromise) + case listening(EventLoopFuture) + case closedOrInvalidAddress(RuntimeError) + + var listeningAddressFuture: EventLoopFuture { + get throws { + switch self { + case .idle(let eventLoopPromise): + return eventLoopPromise.futureResult + case .listening(let eventLoopFuture): + return eventLoopFuture + case .closedOrInvalidAddress(let runtimeError): + throw runtimeError + } + } + } + + enum OnBound { + case succeedPromise( + _ promise: EventLoopPromise, + address: GRPCHTTP2Core.SocketAddress + ) + case failPromise( + _ promise: EventLoopPromise, + error: RuntimeError + ) + } + + mutating func addressBound(_ address: NIOCore.SocketAddress?) -> OnBound { + switch self { + case .idle(let listeningAddressPromise): + if let address { + self = .listening(listeningAddressPromise.futureResult) + return .succeedPromise( + listeningAddressPromise, + address: GRPCHTTP2Core.SocketAddress(address) + ) + + } else { + assertionFailure("Unknown address type") + let invalidAddressError = RuntimeError( + code: .transportError, + message: "Unknown address type returned by transport." + ) + self = .closedOrInvalidAddress(invalidAddressError) + return .failPromise(listeningAddressPromise, error: invalidAddressError) + } + + case .listening, .closedOrInvalidAddress: + fatalError( + "Invalid state: addressBound should only be called once and when in idle state" + ) + } + } + + enum OnClose { + case failPromise( + EventLoopPromise, + error: RuntimeError + ) + case doNothing + } + + mutating func close() -> OnClose { + let serverStoppedError = RuntimeError( + code: .serverIsStopped, + message: """ + There is no listening address bound for this server: there may have been \ + an error which caused the transport to close, or it may have shut down. + """ + ) + + switch self { + case .idle(let listeningAddressPromise): + self = .closedOrInvalidAddress(serverStoppedError) + return .failPromise(listeningAddressPromise, error: serverStoppedError) + + case .listening: + self = .closedOrInvalidAddress(serverStoppedError) + return .doNothing + + case .closedOrInvalidAddress: + return .doNothing + } + } + } + + private let listeningAddressState: _LockedValueBox + + /// The listening address for this server transport. + /// + /// It is an `async` property because it will only return once the address has been successfully bound. + /// + /// - Throws: A runtime error will be thrown if the address could not be bound or is not bound any + /// longer, because the transport isn't listening anymore. It can also throw if the transport returned an + /// invalid address. + public var listeningAddress: GRPCHTTP2Core.SocketAddress { + get async throws { + try await self.listeningAddressState + .withLockedValue { try $0.listeningAddressFuture } + .get() + } + } + /// Create a new `TransportServices` transport. /// /// - Parameters: @@ -45,11 +150,23 @@ extension HTTP2ServerTransport { self.config = config self.eventLoopGroup = eventLoopGroup self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) + + let eventLoop = eventLoopGroup.any() + self.listeningAddressState = _LockedValueBox(.idle(eventLoop.makePromise())) } public func listen( _ streamHandler: @escaping (RPCStream) async -> Void ) async throws { + defer { + switch self.listeningAddressState.withLockedValue({ $0.close() }) { + case .failPromise(let promise, let error): + promise.fail(error) + case .doNothing: + () + } + } + let serverChannel = try await NIOTSListenerBootstrap(group: self.eventLoopGroup) .serverChannelInitializer { channel in let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( @@ -70,6 +187,16 @@ extension HTTP2ServerTransport { } } + let action = self.listeningAddressState.withLockedValue { + $0.addressBound(serverChannel.channel.localAddress) + } + switch action { + case .succeedPromise(let promise, let address): + promise.succeed(address) + case .failPromise(let promise, let error): + promise.fail(error) + } + try await serverChannel.executeThenClose { inbound in try await withThrowingDiscardingTaskGroup { serverTaskGroup in for try await (connectionChannel, streamMultiplexer) in inbound { @@ -203,7 +330,7 @@ extension NIOTSListenerBootstrap { to address: GRPCHTTP2Core.SocketAddress, childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture ) async throws -> NIOAsyncChannel { - if let virtualSocket = address.virtualSocket { + if address.virtualSocket != nil { throw RuntimeError( code: .transportError, message: """ diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index 988d56735..09124889f 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -41,7 +41,9 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testGetListeningAddress_IPv6() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(address: .ipv6(host: "::1", port: 0)) + let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( + address: .ipv6(host: "::1", port: 0) + ) try await withThrowingDiscardingTaskGroup { group in group.addTask { @@ -59,7 +61,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { func testGetListeningAddress_UnixDomainSocket() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .unixDomainSocket(path: "/tmp/test") + address: .unixDomainSocket(path: "/tmp/posix-uds-test") ) try await withThrowingDiscardingTaskGroup { group in @@ -71,7 +73,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { let address = try await transport.listeningAddress XCTAssertEqual( address.unixDomainSocket, - GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/test") + GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/posix-uds-test") ) transport.stopListening() } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift new file mode 100644 index 000000000..a6446ea7f --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -0,0 +1,144 @@ +/* + * 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. + */ + +#if canImport(Network) +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOTransportServices +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class HTTP2TransportNIOTransportServicesTests: XCTestCase { + func testGetListeningAddress_IPv4() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( + address: .ipv4(host: "0.0.0.0", port: 0) + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + let address = try await transport.listeningAddress + let ipv4Address = try XCTUnwrap(address.ipv4) + XCTAssertNotEqual(ipv4Address.port, 0) + transport.stopListening() + } + } + } + + func testGetListeningAddress_IPv6() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( + address: .ipv6(host: "::1", port: 0) + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + let address = try await transport.listeningAddress + let ipv6Address = try XCTUnwrap(address.ipv6) + XCTAssertNotEqual(ipv6Address.port, 0) + transport.stopListening() + } + } + } + + func testGetListeningAddress_UnixDomainSocket() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( + address: .unixDomainSocket(path: "/tmp/niots-uds-test") + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + let address = try await transport.listeningAddress + XCTAssertEqual( + address.unixDomainSocket, + GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/niots-uds-test") + ) + transport.stopListening() + } + } + } + + func testGetListeningAddress_InvalidAddress() async { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( + address: .unixDomainSocket(path: "/this/should/be/an/invalid/path") + ) + + try? await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + } + + group.addTask { + do { + _ = try await transport.listeningAddress + XCTFail("Should have thrown a RuntimeError") + } catch let error as RuntimeError { + XCTAssertEqual(error.code, .serverIsStopped) + XCTAssertEqual( + error.message, + """ + There is no listening address bound for this server: there may have \ + been an error which caused the transport to close, or it may have shut down. + """ + ) + } + } + } + } + + func testGetListeningAddress_StoppedListening() async throws { + let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( + address: .ipv4(host: "0.0.0.0", port: 0) + ) + + try? await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await transport.listen { _ in } + + do { + _ = try await transport.listeningAddress + XCTFail("Should have thrown a RuntimeError") + } catch let error as RuntimeError { + XCTAssertEqual(error.code, .serverIsStopped) + XCTAssertEqual( + error.message, + """ + There is no listening address bound for this server: there may have \ + been an error which caused the transport to close, or it may have shut down. + """ + ) + } + } + + group.addTask { + let address = try await transport.listeningAddress + XCTAssertNotNil(address.ipv4) + transport.stopListening() + } + } + } +} +#endif From 9058509a7b3d7854aa6696d9e05e4e7e256e6fa5 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 19 Jun 2024 08:01:31 +0100 Subject: [PATCH 364/580] Enable so_reuseaddr on server transport implementations (#1940) --- .../HTTP2ServerTransport+Posix.swift | 4 ++++ .../HTTP2ServerTransport+TransportServices.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 9151216ef..d743f4b83 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -174,6 +174,10 @@ extension HTTP2ServerTransport { } let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup) + .serverChannelOption( + ChannelOptions.socketOption(.so_reuseaddr), + value: 1 + ) .serverChannelInitializer { channel in let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( channel: channel diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 2801ecf6f..8a72cb627 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -168,6 +168,10 @@ extension HTTP2ServerTransport { } let serverChannel = try await NIOTSListenerBootstrap(group: self.eventLoopGroup) + .serverChannelOption( + ChannelOptions.socketOption(.so_reuseaddr), + value: 1 + ) .serverChannelInitializer { channel in let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( channel: channel From f475dab85d14b7941c9467e24971a578a71da4a0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 20 Jun 2024 09:08:37 +0100 Subject: [PATCH 365/580] Don't double ack pings (#1941) Motivation: The `ServerConnectionManagementHandler` acks pings, however NIOs HTTP/2 handler does this already. This results in pings received by the server being acked twice. Modifications: - Don't ack pings in the `ServerConnectionManagementHandler` - Update tests Result: Pings aren't ack'd twice --- .../ServerConnectionManagementHandler.swift | 4 +--- ...rverConnectionManagementHandlerTests.swift | 21 +++---------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index 2e433582e..2f4fe2da7 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -458,9 +458,7 @@ extension ServerConnectionManagementHandler { context.close(promise: nil) case .sendAck: - let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(data, ack: true)) - context.write(self.wrapOutboundOut(ping), promise: nil) - self.maybeFlush(context: context) + () // ACKs are sent by NIO's HTTP/2 handler, don't double ack. case .none: () diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift index 672f6264b..c0e0805a6 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift @@ -213,12 +213,7 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { // The first ping is valid, the second and third are strikes. for _ in 1 ... 3 { try connection.ping(data: HTTP2PingData(), ack: false) - let frame = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame.streamID, .rootStream) - XCTAssertPing(frame.payload) { data, ack in - XCTAssertEqual(data, HTTP2PingData()) - XCTAssertTrue(ack) - } + XCTAssertNil(try connection.readFrame()) } // The fourth ping is the third strike and triggers a GOAWAY. @@ -245,12 +240,7 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { for _ in 1 ... 100 { try connection.ping(data: HTTP2PingData(), ack: false) - let frame = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame.streamID, .rootStream) - XCTAssertPing(frame.payload) { data, ack in - XCTAssertEqual(data, HTTP2PingData()) - XCTAssertTrue(ack) - } + XCTAssertNil(try connection.readFrame()) // Advance by the ping interval. connection.advanceTime(by: .minutes(1)) @@ -268,12 +258,7 @@ final class ServerConnectionManagementHandlerTests: XCTestCase { // The first ping is valid, the second and third are strikes. for _ in 1 ... 3 { try connection.ping(data: HTTP2PingData(), ack: false) - let frame = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame.streamID, .rootStream) - XCTAssertPing(frame.payload) { data, ack in - XCTAssertEqual(data, HTTP2PingData()) - XCTAssertTrue(ack) - } + XCTAssertNil(try connection.readFrame()) } } From e9490d17b9fd06f94eb33f4c710a9dcdfbcfa1c7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 21 Jun 2024 12:13:45 +0100 Subject: [PATCH 366/580] Fix more retroactive warnings (#1945) Motivation: The route guide example added retroactive conformance to types it doesn't own. Modifications: Avoid retroactive conformance Result: Fewer warnings --- .../RouteGuide/Client/RouteGuideClient.swift | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift index 4f9c63d97..f3171b92f 100644 --- a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift +++ b/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift @@ -79,9 +79,9 @@ extension RouteGuideExample { let feature = try await self.routeGuide.getFeature(point) if !feature.name.isEmpty { - print("Found feature called '\(feature.name)' at \(feature.location)") + print("Found feature called '\(feature.name)' at \(feature.location.formatted)") } else { - print("Found no feature at \(feature.location)") + print("Found no feature at \(feature.location.formatted)") } } catch { print("RPC failed: \(error)") @@ -113,7 +113,7 @@ extension RouteGuideExample { do { var resultCount = 1 for try await feature in self.routeGuide.listFeatures(rectangle) { - print("Result #\(resultCount): \(feature)") + print("Result #\(resultCount): \(feature.name) at \(feature.location.formatted)") resultCount += 1 } } catch { @@ -134,7 +134,7 @@ extension RouteGuideExample { for i in 1 ... featuresToVisit { if let feature = features.randomElement() { let point = feature.location - print("Visiting point #\(i) at \(point)") + print("Visiting point #\(i) at \(point.formatted)") try await recordRoute.requestStream.send(point) // Sleep for 0.2s ... 1.0s before sending the next point. @@ -181,7 +181,7 @@ extension RouteGuideExample { // Add a task to send each message adding a small sleep between each. group.addTask { for note in notes { - print("Sending message '\(note.message)' at \(note.location)") + print("Sending message '\(note.message)' at \(note.location.formatted)") try await routeChat.requestStream.send(note) // Sleep for 0.2s ... 1.0s before sending the next note. try await Task.sleep(nanoseconds: UInt64.random(in: UInt64(2e8) ... UInt64(1e9))) @@ -193,7 +193,7 @@ extension RouteGuideExample { // Add a task to print each message received on the response stream. group.addTask { for try await note in routeChat.responseStream { - print("Received message '\(note.message)' at \(note.location)") + print("Received message '\(note.message)' at \(note.location.formatted)") } } @@ -235,14 +235,8 @@ struct RouteGuide: AsyncParsableCommand { } } -extension Routeguide_Point: CustomStringConvertible { - public var description: String { +extension Routeguide_Point { + var formatted: String { return "(\(self.latitude), \(self.longitude))" } } - -extension Routeguide_Feature: CustomStringConvertible { - public var description: String { - return "\(self.name) at \(self.location)" - } -} From cd00d5cdcc089431b39b0563ad5aa9ab77d39d4c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 21 Jun 2024 12:21:58 +0100 Subject: [PATCH 367/580] Various fixes to the benchmark client and server (#1944) Motivation: The benchmark client and server were implemented before we had a working transport making them impossible to test. As it turns out there were a few issues. Modifications: - BenchmarkClient: - Some RPC types have been removed. All other implementations only support unary and streaming and we never run scenarios using the other types. - The implementation of the streaming RPC has been altered to record the latency per request/response message pair. This aligns with other implementations. - The implementation of the streaming RPC has also been changed to send the response message sequence into the request writer. This yields a fairly substantial performance improvement (~3.5x) over the existing implementation. - The streaming RPC now respects the messages per stream config being zero (meaning no limit). - An 'is shutting down' atomic is used to stop the client from initiating new RPCs before closing. - BenchmarkService: - No semantic changes; the typealiases have been desugared following from changes in fea1b722 - WorkerService: - The state machine has been tightened up a bit to more clearly separate state from side effects and to avoid leaking the implementation of the state machine into the service. - Added logic for creating clients and servers with an HTTP/2 transport. Result: Can run perf tests --- Package.swift | 2 + .../performance-worker/BenchmarkClient.swift | 257 ++++----- .../performance-worker/BenchmarkService.swift | 66 ++- Sources/performance-worker/RPCStats.swift | 4 +- .../performance-worker/WorkerService.swift | 510 +++++++++++------- 5 files changed, 490 insertions(+), 349 deletions(-) diff --git a/Package.swift b/Package.swift index 4f6797c8e..e42408bdf 100644 --- a/Package.swift +++ b/Package.swift @@ -259,6 +259,8 @@ extension Target { name: "performance-worker", dependencies: [ .grpcCore, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, .grpcProtobuf, .nioCore, .nioFileSystem diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 7945003b9..6f17f961a 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -14,41 +14,68 @@ * limitations under the License. */ +import Atomics import Foundation import GRPCCore import NIOConcurrencyHelpers @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct BenchmarkClient { + private let _isShuttingDown = ManagedAtomic(false) + + /// Whether the benchmark client is shutting down. Used to control when to stop sending messages + /// or creating new RPCs. + private var isShuttingDown: Bool { + self._isShuttingDown.load(ordering: .relaxed) + } + + /// The underlying client. private var client: GRPCClient - private var rpcNumber: Int32 + + /// The number of concurrent RPCs to run. + private var concurrentRPCs: Int + + /// The type of RPC to make against the server. private var rpcType: RPCType - private var messagesPerStream: Int32 - private var protoParams: Grpc_Testing_SimpleProtoParams + + /// The max number of messages to send on a stream before replacing the RPC with a new one. A + /// value of zero means there is no limit. + private var messagesPerStream: Int + private var noMessageLimit: Bool { self.messagesPerStream == 0 } + + /// The message to send for all RPC types to the server. + private let message: Grpc_Testing_SimpleRequest + + /// Per RPC stats. private let rpcStats: NIOLockedValueBox init( client: GRPCClient, - rpcNumber: Int32, + concurrentRPCs: Int, rpcType: RPCType, - messagesPerStream: Int32, + messagesPerStream: Int, protoParams: Grpc_Testing_SimpleProtoParams, histogramParams: Grpc_Testing_HistogramParams? ) { self.client = client - self.rpcNumber = rpcNumber + self.concurrentRPCs = concurrentRPCs self.messagesPerStream = messagesPerStream - self.protoParams = protoParams self.rpcType = rpcType + self.message = .with { + $0.responseSize = protoParams.respSize + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: Int(protoParams.reqSize)) + } + } let histogram: RPCStats.LatencyHistogram if let histogramParams = histogramParams { - histogram = .init( + histogram = RPCStats.LatencyHistogram( resolution: histogramParams.resolution, maxBucketStart: histogramParams.maxPossible ) } else { - histogram = .init() + histogram = RPCStats.LatencyHistogram() } self.rpcStats = NIOLockedValueBox(RPCStats(latencyHistogram: histogram)) @@ -57,9 +84,6 @@ struct BenchmarkClient { enum RPCType { case unary case streaming - case streamingFromClient - case streamingFromServer - case streamingBothWays } internal var currentStats: RPCStats { @@ -69,33 +93,53 @@ struct BenchmarkClient { } internal func run() async throws { - let benchmarkClient = Grpc_Testing_BenchmarkServiceClient(client: client) + let benchmarkClient = Grpc_Testing_BenchmarkServiceClient(client: self.client) return try await withThrowingTaskGroup(of: Void.self) { clientGroup in // Start the client. - clientGroup.addTask { try await client.run() } + clientGroup.addTask { + try await self.client.run() + } - // Make the requests to the server and register the latency for each one. try await withThrowingTaskGroup(of: Void.self) { rpcsGroup in - for _ in 0 ..< self.rpcNumber { + // Start one task for each concurrent RPC and keep looping in that task until indicated + // to stop. + for _ in 0 ..< self.concurrentRPCs { rpcsGroup.addTask { - let (latency, errorCode) = try await self.makeRPC( - benchmarkClient: benchmarkClient - ) - self.rpcStats.withLockedValue { - $0.latencyHistogram.record(latency) - if let errorCode = errorCode { - $0.requestResultCount[errorCode, default: 1] += 1 + while !self.isShuttingDown { + switch self.rpcType { + case .unary: + await self.unary(benchmark: benchmarkClient) + + case .streaming: + await self.streaming(benchmark: benchmarkClient) } } } } + try await rpcsGroup.waitForAll() } + self.client.close() try await clientGroup.next() } } + private func record(latencyNanos: Double, errorCode: RPCError.Code?) { + self.rpcStats.withLockedValue { stats in + stats.latencyHistogram.record(latencyNanos) + if let errorCode = errorCode { + stats.requestResultCount[errorCode, default: 0] += 1 + } + } + } + + private func record(errorCode: RPCError.Code) { + self.rpcStats.withLockedValue { stats in + stats.requestResultCount[errorCode, default: 0] += 1 + } + } + private func timeIt( _ body: () async throws -> R ) async rethrows -> (R, nanoseconds: Double) { @@ -105,133 +149,92 @@ struct BenchmarkClient { return (result, nanoseconds: Double(endTime - startTime)) } - // The result is the number of nanoseconds for processing the RPC. - private func makeRPC( - benchmarkClient: Grpc_Testing_BenchmarkServiceClient - ) async throws -> (latency: Double, errorCode: RPCError.Code?) { - let message = Grpc_Testing_SimpleRequest.with { - $0.responseSize = self.protoParams.respSize - $0.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: Int(self.protoParams.reqSize)) + private func unary(benchmark: Grpc_Testing_BenchmarkServiceClient) async { + let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { + do { + try await benchmark.unaryCall(request: ClientRequest.Single(message: self.message)) { + _ = try $0.message + } + return nil + } catch let error as RPCError { + return error.code + } catch { + return .unknown } } - switch self.rpcType { - case .unary: - let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { - do { - try await benchmarkClient.unaryCall( - request: ClientRequest.Single(message: message) - ) { response in - _ = try response.message - } - return nil - } catch let error as RPCError { - return error.code - } catch { - return .unknown - } - } - return (latency: nanoseconds, errorCode) + self.record(latencyNanos: nanoseconds, errorCode: errorCode) + } - // Repeated sequence of one request followed by one response. - // It is a ping-pong of messages between the client and the server. - case .streaming: - let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { - do { - let ids = AsyncStream.makeStream(of: Int.self) - let streamingRequest = ClientRequest.Stream { writer in - for try await id in ids.stream { - if id <= self.messagesPerStream { - try await writer.write(message) - } else { - return - } - } - } + private func streaming(benchmark: Grpc_Testing_BenchmarkServiceClient) async { + // Streaming RPCs ping-pong messages back and forth. To achieve this the response message + // stream is sent to the request closure, and the request closure indicates the outcome back + // to the response handler to keep the RPC alive for the appropriate amount of time. + let status = AsyncStream.makeStream(of: RPCError.self) + let response = AsyncStream.makeStream(of: RPCAsyncSequence.self) - ids.continuation.yield(1) + let request = ClientRequest.Stream(of: Grpc_Testing_SimpleRequest.self) { writer in + defer { status.continuation.finish() } - try await benchmarkClient.streamingCall(request: streamingRequest) { response in - var id = 1 - for try await _ in response.messages { - id += 1 - ids.continuation.yield(id) - } - } - return nil - } catch let error as RPCError { - return error.code - } catch { - return .unknown - } + // The time at which the last message was sent. + var lastMessageSendTime = DispatchTime.now() + try await writer.write(self.message) + + // Wait for the response stream. + var iterator = response.stream.makeAsyncIterator() + guard let responses = await iterator.next() else { + throw RPCError(code: .internalError, message: "") } - return (latency: nanoseconds, errorCode) - case .streamingFromClient: - let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { - do { - let streamingRequest = ClientRequest.Stream { writer in - for _ in 1 ... self.messagesPerStream { - try await writer.write(message) - } - } + // Record the first latency. + let now = DispatchTime.now() + let nanos = now.uptimeNanoseconds - lastMessageSendTime.uptimeNanoseconds + lastMessageSendTime = now + self.record(latencyNanos: Double(nanos), errorCode: nil) - try await benchmarkClient.streamingFromClient( - request: streamingRequest - ) { response in - _ = try response.message - } - return nil - } catch let error as RPCError { - return error.code - } catch { - return .unknown - } - } - return (latency: nanoseconds, errorCode) + // Now start looping. Only stop when the max messages per stream is hit or told to stop. + var responseIterator = responses.makeAsyncIterator() + var messagesSent = 1 - case .streamingFromServer: - let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { + while !self.isShuttingDown && (self.noMessageLimit || messagesSent < self.messagesPerStream) { + messagesSent += 1 do { - try await benchmarkClient.streamingFromServer( - request: ClientRequest.Single(message: message) - ) { response in - for try await _ in response.messages {} + if try await responseIterator.next() != nil { + let now = DispatchTime.now() + let nanos = now.uptimeNanoseconds - lastMessageSendTime.uptimeNanoseconds + lastMessageSendTime = now + self.record(latencyNanos: Double(nanos), errorCode: nil) + try await writer.write(message) + } else { + break } - return nil } catch let error as RPCError { - return error.code + status.continuation.yield(error) + break } catch { - return .unknown + status.continuation.yield(RPCError(code: .unknown, message: "")) + break } } - return (latency: nanoseconds, errorCode) - - case .streamingBothWays: - let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { - do { - let streamingRequest = ClientRequest.Stream { writer in - for _ in 1 ... self.messagesPerStream { - try await writer.write(message) - } - } + } - try await benchmarkClient.streamingBothWays(request: streamingRequest) { response in - for try await _ in response.messages {} - } - return nil - } catch let error as RPCError { - return error.code - } catch { - return .unknown + do { + try await benchmark.streamingCall(request: request) { + response.continuation.yield($0.messages) + response.continuation.finish() + for await errorCode in status.stream { + throw errorCode } } - return (latency: nanoseconds, errorCode) + } catch let error as RPCError { + self.record(errorCode: error.code) + } catch { + self.record(errorCode: .unknown) } } internal func shutdown() { + self._isShuttingDown.store(true, ordering: .relaxed) self.client.close() } } diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift index 67d1de679..3d0bb03fe 100644 --- a/Sources/performance-worker/BenchmarkService.swift +++ b/Sources/performance-worker/BenchmarkService.swift @@ -27,10 +27,8 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// One request followed by one response. /// The server returns a client payload with the size requested by the client. func unaryCall( - request: GRPCCore.ServerRequest.Single - ) async throws - -> GRPCCore.ServerResponse.Single - { + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { // Throw an error if the status is not `ok`. Otherwise, an `ok` status is automatically sent // if the request is successful. if request.message.responseStatus.isInitialized { @@ -38,7 +36,7 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { } return ServerResponse.Single( - message: Grpc_Testing_BenchmarkService.Method.UnaryCall.Output.with { + message: .with { $0.payload = Grpc_Testing_Payload.with { $0.body = Data(count: Int(request.message.responseSize)) } @@ -49,23 +47,23 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Repeated sequence of one request followed by one response. /// The server returns a payload with the size requested by the client for each received message. func streamingCall( - request: GRPCCore.ServerRequest.Stream - ) async throws - -> GRPCCore.ServerResponse.Stream - { + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for try await message in request.messages { if message.responseStatus.isInitialized { try self.checkOkStatus(message.responseStatus) } - try await writer.write( - Grpc_Testing_BenchmarkService.Method.StreamingCall.Output.with { - $0.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: Int(message.responseSize)) - } + + let responseMessage = Grpc_Testing_SimpleResponse.with { + $0.payload = Grpc_Testing_Payload.with { + $0.body = Data(count: Int(message.responseSize)) } - ) + } + + try await writer.write(responseMessage) } + return [:] } } @@ -73,10 +71,8 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Single-sided unbounded streaming from client to server. /// The server returns a payload with the size requested by the client once the client does WritesDone. func streamingFromClient( - request: ServerRequest.Stream - ) async throws - -> ServerResponse.Single - { + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { var responseSize = 0 for try await message in request.messages { if message.responseStatus.isInitialized { @@ -86,8 +82,8 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { } return ServerResponse.Single( - message: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.Output.with { - $0.payload = Grpc_Testing_Payload.with { + message: .with { + $0.payload = .with { $0.body = Data(count: responseSize) } } @@ -97,20 +93,20 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Single-sided unbounded streaming from server to client. /// The server repeatedly returns a payload with the size requested by the client. func streamingFromServer( - request: ServerRequest.Single - ) async throws - -> ServerResponse.Stream - { + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { if request.message.responseStatus.isInitialized { try self.checkOkStatus(request.message.responseStatus) } - let response = Grpc_Testing_BenchmarkService.Method.StreamingCall.Output.with { - $0.payload = Grpc_Testing_Payload.with { + + let response = Grpc_Testing_SimpleResponse.with { + $0.payload = .with { $0.body = Data(count: Int(request.message.responseSize)) } } + return ServerResponse.Stream { writer in - while working.load(ordering: .relaxed) { + while self.working.load(ordering: .relaxed) { try await writer.write(response) } return [:] @@ -120,17 +116,13 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Two-sided unbounded streaming between server to client. /// Both sides send the content of their own choice to the other. func streamingBothWays( - request: GRPCCore.ServerRequest.Stream< - Grpc_Testing_BenchmarkService.Method.StreamingBothWays.Input - > - ) async throws - -> ServerResponse.Stream - { + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { // The 100 size is used by the other implementations as well. // We are using the same canned response size for all responses // as it is allowed by the spec. - let response = Grpc_Testing_BenchmarkService.Method.StreamingCall.Output.with { - $0.payload = Grpc_Testing_Payload.with { + let response = Grpc_Testing_SimpleResponse.with { + $0.payload = .with { $0.body = Data(count: 100) } } @@ -148,6 +140,7 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { } inboundStreaming.store(false, ordering: .relaxed) } + group.addTask { while inboundStreaming.load(ordering: .relaxed) && self.working.load(ordering: .acquiring) @@ -155,6 +148,7 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { try await writer.write(response) } } + try await group.next() group.cancelAll() return [:] diff --git a/Sources/performance-worker/RPCStats.swift b/Sources/performance-worker/RPCStats.swift index 7d864fb4f..bc2bba74b 100644 --- a/Sources/performance-worker/RPCStats.swift +++ b/Sources/performance-worker/RPCStats.swift @@ -132,9 +132,7 @@ struct RPCStats { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) mutating func merge(_ other: RPCStats) throws { - try self.latencyHistogram.merge( - other.latencyHistogram - ) + try self.latencyHistogram.merge(other.latencyHistogram) self.requestResultCount.merge(other.requestResultCount) { (current, new) in current + new } diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 09af07dd9..083ba94c8 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -15,92 +15,46 @@ */ import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix import NIOConcurrencyHelpers import NIOCore +import NIOPosix @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable { +final class WorkerService: Sendable { private let state: NIOLockedValueBox init() { - let clientAndServer = State() - self.state = NIOLockedValueBox(clientAndServer) + self.state = NIOLockedValueBox(State()) } private struct State { - var role: Role? + private var role: Role enum Role { - case client(ClientState) - case server(ServerState) + case none + case client(Client) + case server(Server) } - struct ServerState { + struct Server { var server: GRPCServer var stats: ServerStats - - init(server: GRPCServer, stats: ServerStats) { - self.server = server - self.stats = stats - } + var eventLoopGroup: MultiThreadedEventLoopGroup } - struct ClientState { + struct Client { var clients: [BenchmarkClient] var stats: ClientStats var rpcStats: RPCStats - - init( - clients: [BenchmarkClient], - stats: ClientStats, - rpcStats: RPCStats - ) { - self.clients = clients - self.stats = stats - self.rpcStats = rpcStats - } - - func shutdownClients() throws { - for benchmarkClient in self.clients { - benchmarkClient.shutdown() - } - } - } - - init() {} - - init(role: Role) { - self.role = role - } - - var server: GRPCServer? { - switch self.role { - case let .server(serverState): - return serverState.server - case .client, .none: - return nil - } - } - - var clients: [BenchmarkClient]? { - switch self.role { - case let .client(clientState): - return clientState.clients - case .server, .none: - return nil - } } - var clientRPCStats: RPCStats? { - switch self.role { - case let .client(clientState): - return clientState.rpcStats - case .server, .none: - return nil - } + init() { + self.role = .none } - mutating func serverStats(replaceWith newStats: ServerStats? = nil) -> ServerStats? { + mutating func collectServerStats(replaceWith newStats: ServerStats? = nil) -> ServerStats? { switch self.role { case var .server(serverState): let stats = serverState.stats @@ -114,98 +68,186 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } } - mutating func clientStats(replaceWith newStats: ClientStats? = nil) -> ClientStats? { + mutating func collectClientStats( + replaceWith newStats: ClientStats? = nil + ) -> (ClientStats, RPCStats)? { switch self.role { - case var .client(clientState): - let stats = clientState.stats + case var .client(state): + // Grab the existing stats and update if necessary. + let stats = state.stats if let newStats = newStats { - clientState.stats = newStats - self.role = .client(clientState) + state.stats = newStats } - return stats + + // Merge in RPC stats from each client. + for client in state.clients { + try? state.rpcStats.merge(client.currentStats) + } + + self.role = .client(state) + return (stats, state.rpcStats) + case .server, .none: return nil } } - mutating func setupServer(server: GRPCServer, stats: ServerStats) throws { - let serverState = State.ServerState(server: server, stats: stats) - switch self.role { - case .server(_): - throw RPCError(code: .alreadyExists, message: "A server has already been set up.") + enum OnStartedServer { + case runServer + case invalidState(RPCError) + } - case .client(_): - throw RPCError(code: .failedPrecondition, message: "This worker has a client setup.") + mutating func startedServer( + _ server: GRPCServer, + stats: ServerStats, + eventLoopGroup: MultiThreadedEventLoopGroup + ) -> OnStartedServer { + let action: OnStartedServer + switch self.role { case .none: - self.role = .server(serverState) + let state = State.Server(server: server, stats: stats, eventLoopGroup: eventLoopGroup) + self.role = .server(state) + action = .runServer + case .server: + let error = RPCError(code: .alreadyExists, message: "A server has already been set up.") + action = .invalidState(error) + case .client: + let error = RPCError(code: .failedPrecondition, message: "This worker has a client setup.") + action = .invalidState(error) } + + return action } - mutating func setupClients( - benchmarkClients: [BenchmarkClient], + enum OnStartedClients { + case runClients + case invalidState(RPCError) + } + + mutating func startedClients( + _ clients: [BenchmarkClient], stats: ClientStats, rpcStats: RPCStats - ) throws { - let clientState = State.ClientState( - clients: benchmarkClients, - stats: stats, - rpcStats: rpcStats - ) + ) -> OnStartedClients { + let action: OnStartedClients + switch self.role { - case .server(_): - throw RPCError(code: .alreadyExists, message: "This worker has a server setup.") + case .none: + let state = State.Client(clients: clients, stats: stats, rpcStats: rpcStats) + self.role = .client(state) + action = .runClients + case .server: + let error = RPCError(code: .alreadyExists, message: "This worker has a server setup.") + action = .invalidState(error) + case .client: + let error = RPCError( + code: .failedPrecondition, + message: "Clients have already been set up." + ) + action = .invalidState(error) + } - case .client(_): - throw RPCError(code: .failedPrecondition, message: "Clients have already been set up.") + return action + } + + enum OnServerShutDown { + case shutdown(MultiThreadedEventLoopGroup) + case nothing + } + mutating func serverShutdown() -> OnServerShutDown { + switch self.role { + case .client: + preconditionFailure("Invalid state") + case .server(let state): + self.role = .none + return .shutdown(state.eventLoopGroup) case .none: - self.role = .client(clientState) + return .nothing } } - mutating func updateRPCStats() throws { + enum OnStopListening { + case stopListening(GRPCServer) + case nothing + } + + func stopListening() -> OnStopListening { switch self.role { - case var .client(clientState): - let benchmarkClients = clientState.clients - var rpcStats = clientState.rpcStats - for benchmarkClient in benchmarkClients { - try rpcStats.merge(benchmarkClient.currentStats) - } + case .client: + preconditionFailure("Invalid state") + case .server(let state): + return .stopListening(state.server) + case .none: + return .nothing + } + } - clientState.rpcStats = rpcStats - self.role = .client(clientState) + enum OnCloseClient { + case close([BenchmarkClient]) + case nothing + } - case .server, .none: - () + mutating func closeClients() -> OnCloseClient { + switch self.role { + case .client(let state): + self.role = .none + return .close(state.clients) + case .server: + preconditionFailure("Invalid state") + case .none: + return .nothing + } + } + + enum OnQuitWorker { + case shutDownServer(GRPCServer) + case shutDownClients([BenchmarkClient]) + case nothing + } + + mutating func quit() -> OnQuitWorker { + switch self.role { + case .none: + return .nothing + case .client(let state): + self.role = .none + return .shutDownClients(state.clients) + case .server(let state): + self.role = .none + return .shutDownServer(state.server) } } } +} +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { func quitWorker( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let onQuit = self.state.withLockedValue { $0.quit() } - let role = self.state.withLockedValue { state in - defer { state.role = nil } - return state.role - } + switch onQuit { + case .nothing: + () - if let role = role { - switch role { - case .client(let clientState): - try clientState.shutdownClients() - case .server(let serverState): - serverState.server.stopListening() + case .shutDownClients(let clients): + for client in clients { + client.shutdown() } + + case .shutDownServer(let server): + server.stopListening() } - return ServerResponse.Single(message: Grpc_Testing_WorkerService.Method.QuitWorker.Output()) + return ServerResponse.Single(message: Grpc_Testing_Void()) } func coreCount( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { let coreCount = System.coreCount return ServerResponse.Single( message: Grpc_Testing_WorkerService.Method.CoreCount.Output.with { @@ -215,17 +257,52 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } func runServer( - request: GRPCCore.ServerRequest.Stream - ) async throws - -> GRPCCore.ServerResponse.Stream - { + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in try await withThrowingTaskGroup(of: Void.self) { group in for try await message in request.messages { switch message.argtype { case let .some(.setup(serverConfig)): - let server = try await self.setupServer(serverConfig) - group.addTask { try await server.run() } + let (server, transport) = try await self.startServer(serverConfig) + group.addTask { + let result: Result + + do { + try await server.run() + result = .success(()) + } catch { + result = .failure(error) + } + + switch self.state.withLockedValue({ $0.serverShutdown() }) { + case .shutdown(let eventLoopGroup): + try await eventLoopGroup.shutdownGracefully() + case .nothing: + () + } + + try result.get() + } + + // Wait for the server to bind. + let address = try await transport.listeningAddress + + let port: Int + if let ipv4 = address.ipv4 { + port = ipv4.port + } else if let ipv6 = address.ipv6 { + port = ipv6.port + } else { + throw RPCError( + code: .internalError, + message: "Server listening on unsupported address '\(address)'" + ) + } + + // Tell the client what port the server is listening on. + let message = Grpc_Testing_ServerStatus.with { $0.port = Int32(port) } + try await writer.write(message) case let .some(.mark(mark)): let response = try await self.makeServerStatsResponse(reset: mark.reset) @@ -236,24 +313,23 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } } - try await group.next() - } - - let server = self.state.withLockedValue { state in - defer { state.role = nil } - return state.server + // Request stream ended, tell the server to stop listening. Once it's finished it will + // shutdown its ELG. + switch self.state.withLockedValue({ $0.stopListening() }) { + case .stopListening(let server): + server.stopListening() + case .nothing: + () + } } - server?.stopListening() return [:] } } func runClient( - request: GRPCCore.ServerRequest.Stream - ) async throws - -> GRPCCore.ServerResponse.Stream - { + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in try await withThrowingTaskGroup(of: Void.self) { group in for try await message in request.messages { @@ -268,6 +344,9 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable } } + let message = try await self.makeClientStatsResponse(reset: false) + try await writer.write(message) + case let .mark(mark): let response = try await self.makeClientStatsResponse(reset: mark.reset) try await writer.write(response) @@ -276,6 +355,16 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable () } } + + switch self.state.withLockedValue({ $0.closeClients() }) { + case .close(let clients): + for client in clients { + client.shutdown() + } + case .nothing: + () + } + try await group.waitForAll() return [:] @@ -286,15 +375,44 @@ final class WorkerService: Grpc_Testing_WorkerService.ServiceProtocol, Sendable @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension WorkerService { - private func setupServer(_ config: Grpc_Testing_ServerConfig) async throws -> GRPCServer { - let server = GRPCServer(transport: NoOpServerTransport(), services: [BenchmarkService()]) + private func startServer( + _ serverConfig: Grpc_Testing_ServerConfig + ) async throws -> (GRPCServer, HTTP2ServerTransport.Posix) { + // Prepare an ELG, the test might require more than the default of one. + let numberOfThreads: Int + if serverConfig.asyncServerThreads > 0 { + numberOfThreads = Int(serverConfig.asyncServerThreads) + } else { + numberOfThreads = System.coreCount + } + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads) + + // Don't restrict the max payload size, the client is always trusted. + var config = HTTP2ServerTransport.Posix.Config.defaults + config.rpc.maxRequestPayloadSize = .max + + let transport = HTTP2ServerTransport.Posix( + address: .ipv4(host: "127.0.0.1", port: Int(serverConfig.port)), + config: config, + eventLoopGroup: eventLoopGroup + ) + + let server = GRPCServer(transport: transport, services: [BenchmarkService()]) let stats = try await ServerStats() - try self.state.withLockedValue { state in - try state.setupServer(server: server, stats: stats) + // Hold on to the server and ELG in the state machine. + let action = self.state.withLockedValue { + $0.startedServer(server, stats: stats, eventLoopGroup: eventLoopGroup) } - return server + switch action { + case .runServer: + return (server, transport) + case .invalidState(let error): + server.stopListening() + try await eventLoopGroup.shutdownGracefully() + throw error + } } private func makeServerStatsResponse( @@ -302,7 +420,7 @@ extension WorkerService { ) async throws -> Grpc_Testing_WorkerService.Method.RunServer.Output { let currentStats = try await ServerStats() let initialStats = self.state.withLockedValue { state in - return state.serverStats(replaceWith: reset ? currentStats : nil) + return state.collectServerStats(replaceWith: reset ? currentStats : nil) } guard let initialStats = initialStats else { @@ -325,69 +443,90 @@ extension WorkerService { } private func setupClients(_ config: Grpc_Testing_ClientConfig) async throws -> [BenchmarkClient] { - let rpcType: BenchmarkClient.RPCType - switch config.rpcType { - case .unary: - rpcType = .unary - case .streaming: - rpcType = .streaming - case .streamingFromClient: - rpcType = .streamingFromClient - case .streamingFromServer: - rpcType = .streamingFromServer - case .streamingBothWays: - rpcType = .streamingBothWays - case .UNRECOGNIZED: - throw RPCError(code: .unknown, message: "The RPC type is UNRECOGNIZED.") + guard let rpcType = BenchmarkClient.RPCType(config.rpcType) else { + throw RPCError(code: .invalidArgument, message: "Unknown RPC type") } + // Parse the server targets into resolvable targets. + let ipv4Addresses = try self.parseServerTargets(config.serverTargets) + let target = ResolvableTargets.IPv4(addresses: ipv4Addresses) + var clients = [BenchmarkClient]() for _ in 0 ..< config.clientChannels { - let grpcClient = self.makeGRPCClient() - clients.append( - BenchmarkClient( - client: grpcClient, - rpcNumber: config.outstandingRpcsPerChannel, - rpcType: rpcType, - messagesPerStream: config.messagesPerStream, - protoParams: config.payloadConfig.simpleParams, - histogramParams: config.histogramParams - ) + let transport = try HTTP2ClientTransport.Posix(target: target) + let client = BenchmarkClient( + client: GRPCClient(transport: transport), + concurrentRPCs: Int(config.outstandingRpcsPerChannel), + rpcType: rpcType, + messagesPerStream: Int(config.messagesPerStream), + protoParams: config.payloadConfig.simpleParams, + histogramParams: config.histogramParams ) + + clients.append(client) } + let stats = ClientStats() let histogram = RPCStats.LatencyHistogram( resolution: config.histogramParams.resolution, maxBucketStart: config.histogramParams.maxPossible ) + let rpcStats = RPCStats(latencyHistogram: histogram) - try self.state.withLockedValue { state in - try state.setupClients( - benchmarkClients: clients, - stats: stats, - rpcStats: RPCStats(latencyHistogram: histogram) - ) + let action = self.state.withLockedValue { state in + state.startedClients(clients, stats: stats, rpcStats: rpcStats) + } + + switch action { + case .runClients: + return clients + case .invalidState(let error): + for client in clients { + client.shutdown() + } + throw error } + } - return clients + private func parseServerTarget(_ target: String) -> GRPCHTTP2Core.SocketAddress.IPv4? { + guard let index = target.firstIndex(of: ":") else { return nil } + + let host = target[.. GRPCClient { - fatalError() + private func parseServerTargets( + _ targets: [String] + ) throws -> [GRPCHTTP2Core.SocketAddress.IPv4] { + try targets.map { target in + if let ipv4 = self.parseServerTarget(target) { + return ipv4 + } else { + throw RPCError( + code: .invalidArgument, + message: """ + Couldn't parse target '\(target)'. Must be in the format ':' for IPv4 \ + or '[]:' for IPv6. + """ + ) + } + } } private func makeClientStatsResponse( reset: Bool ) async throws -> Grpc_Testing_WorkerService.Method.RunClient.Output { let currentUsageStats = ClientStats() - let (initialUsageStats, rpcStats) = try self.state.withLockedValue { state in - let initialUsageStats = state.clientStats(replaceWith: reset ? currentUsageStats : nil) - try state.updateRPCStats() - let rpcStats = state.clientRPCStats - return (initialUsageStats, rpcStats) + + let stats = self.state.withLockedValue { state in + state.collectClientStats(replaceWith: reset ? currentUsageStats : nil) } - guard let initialUsageStats = initialUsageStats, let rpcStats = rpcStats else { + guard let (initialUsageStats, rpcStats) = stats else { throw RPCError( code: .notFound, message: "There are no initial client stats. Clients must be setup before calling 'mark'." @@ -422,11 +561,16 @@ extension WorkerService { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -struct NoOpServerTransport: ServerTransport { - func listen( - _ streamHandler: @escaping (RPCStream) async -> Void - ) async throws {} - - func stopListening() {} +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension BenchmarkClient.RPCType { + init?(_ rpcType: Grpc_Testing_RpcType) { + switch rpcType { + case .unary: + self = .unary + case .streaming: + self = .streaming + default: + return nil + } + } } From a7d3af6072b39dbd8a24a3cd5419629289dcd2c9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 21 Jun 2024 12:35:47 +0100 Subject: [PATCH 368/580] Avoid dropping continuations in grpc channel (#1942) Motivation: If the request queue was non-empty and close was called on the grpc channel, the request queue would be dropped along with any continuations. The request queue can be non-empty if the active load balancer isn't in the ready state (i.e. connecting). Moreover, if close is called while a subchannel is connecting it can result in the shutdown event being fired twice. Modifications: - Fail any continuations in the request queue when closing the grpc channel - Alter when shutdown events are fired by the subchannel. At the moment they're typically fired when close is called and on some paths, when the connection is closed as well. Shutdown events are now fired when entering the closing state or when transitioning directly to closed (i.e. from idle/connected). Result: Continuations aren't dropped --- .../Client/Connection/GRPCChannel.swift | 10 +- .../Connection/LoadBalancers/Subchannel.swift | 178 ++++++++++++------ .../Client/Connection/GRPCChannelTests.swift | 52 +++++ 3 files changed, 176 insertions(+), 64 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 177aa734a..fb1f10729 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -355,10 +355,13 @@ extension GRPCChannel { extension GRPCChannel { private func handleClose(in group: inout DiscardingTaskGroup) { switch self.state.withLockedValue({ $0.close() }) { - case .close(let current, let next, let resolver): + case .close(let current, let next, let resolver, let continuations): resolver?.cancel() current.close() next?.close() + for continuation in continuations { + continuation.resume(throwing: RPCError(code: .unavailable, message: "channel is closed")) + } self._connectivityState.continuation.yield(.shutdown) case .cancelAll(let continuations): @@ -924,7 +927,7 @@ extension GRPCChannel.StateMachine { enum OnClose { case none case cancelAll([RequestQueue.Continuation]) - case close(LoadBalancer, LoadBalancer?, CancellableTaskHandle?) + case close(LoadBalancer, LoadBalancer?, CancellableTaskHandle?, [RequestQueue.Continuation]) } mutating func close() -> OnClose { @@ -936,7 +939,8 @@ extension GRPCChannel.StateMachine { onClose = .cancelAll(state.queue.removeAll()) case .running(var state): - onClose = .close(state.current, state.next, state.nameResolverHandle) + let continuations = state.queue.removeAll() + onClose = .close(state.current, state.next, state.nameResolverHandle, continuations) state.past[state.current.id] = state.current if let next = state.next { diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift index 22c395b83..d1c88a490 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift @@ -218,16 +218,13 @@ extension Subchannel { case .none: () + case .close(let connection): + connection.close() + case .connect(let connection): // About to start connecting, emit a state change event. self.event.continuation.yield(.connectivityStateChanged(.connecting)) self.runConnection(connection, in: &group) - - case .shutdown: - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - // Close the event streams. - self.event.continuation.finish() - self.input.continuation.finish() } } @@ -236,10 +233,12 @@ extension Subchannel { case .none: () - case .close(let connection): + case .emitShutdownAndClose(let connection): + // Connection closed because the load balancer asked it to, so notify the load balancer. + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) connection.close() - case .shutdown: + case .emitShutdownAndFinish: // Connection closed because the load balancer asked it to, so notify the load balancer. self.event.continuation.yield(.connectivityStateChanged(.shutdown)) // At this point there are no more events: close the event streams. @@ -266,11 +265,12 @@ extension Subchannel { private func handleConnectSucceededEvent() { switch self.state.withLockedValue({ $0.connectSucceeded() }) { - case .updateState: + case .updateStateToReady: // Emit a connectivity state change: the load balancer can now use this subchannel. self.event.continuation.yield(.connectivityStateChanged(.ready)) - case .close(let connection): + case .closeAndEmitShutdown(let connection): + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) connection.close() case .none: @@ -299,11 +299,9 @@ extension Subchannel { } } - case .shutdown: + case .closeAndEmitShutdownEvent(let connection): self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - // No more events, close the streams. - self.event.continuation.finish() - self.input.continuation.finish() + connection.close() case .none: () @@ -325,26 +323,24 @@ extension Subchannel { _ reason: Connection.CloseReason, in group: inout DiscardingTaskGroup ) { - let isClosed = self.state.withLockedValue { $0.closed(reason: reason) } - guard isClosed else { return } + switch self.state.withLockedValue({ $0.closed(reason: reason) }) { + case .nothing: + () - switch reason { - case .idleTimeout, .remote, .error(_, wasIdle: true): - // Connection closed due to an idle timeout or the remote telling it to GOAWAY; notify the - // load balancer about this. + case .emitIdle: self.event.continuation.yield(.connectivityStateChanged(.idle)) - case .keepaliveTimeout, .error(_, wasIdle: false): + case .emitTransientFailureAndReconnect: // Unclean closes trigger a transient failure state change and a name resolution. self.event.continuation.yield(.connectivityStateChanged(.transientFailure)) self.event.continuation.yield(.requiresNameResolution) - // Attempt to reconnect. self.handleConnectInput(in: &group) - case .initiatedLocally: - // Connection closed because the load balancer asked it to, so notify the load balancer. - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + case .finish(let emitShutdown): + if emitShutdown { + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + } // At this point there are no more events: close the event streams. self.event.continuation.finish() @@ -384,6 +380,7 @@ extension Subchannel { let addresses: [SocketAddress] var addressIterator: Array.Iterator var backoff: ConnectionBackoff.Iterator + var shutdownRequested: Bool = false } struct Connected { @@ -442,8 +439,8 @@ extension Subchannel { enum OnClose { case none - case shutdown - case close(Connection) + case emitShutdownAndFinish + case emitShutdownAndClose(Connection) } mutating func close() -> OnClose { @@ -451,16 +448,17 @@ extension Subchannel { switch self { case .notConnected: - onClose = .shutdown + onClose = .emitShutdownAndFinish - case .connecting(let state): - self = .closing(Closing(from: state)) + case .connecting(var state): + state.shutdownRequested = true + self = .connecting(state) // Do nothing; the connection hasn't been established yet so can't be closed. onClose = .none case .connected(let state): self = .closing(Closing(from: state)) - onClose = .close(state.connection) + onClose = .emitShutdownAndClose(state.connection) case .closing, .closed: onClose = .none @@ -470,19 +468,27 @@ extension Subchannel { } enum OnConnectSucceeded { - case updateState - case close(Connection) + case updateStateToReady + case closeAndEmitShutdown(Connection) case none } mutating func connectSucceeded() -> OnConnectSucceeded { switch self { case .connecting(let state): - self = .connected(Connected(from: state)) - return .updateState + if state.shutdownRequested { + self = .closing(Closing(from: state)) + return .closeAndEmitShutdown(state.connection) + } else { + self = .connected(Connected(from: state)) + return .updateStateToReady + } + case .closing(let state): - self = .closing(state) - return .close(state.connection) + // Shouldn't happen via the connecting state. + assertionFailure("Invalid state") + return .closeAndEmitShutdown(state.connection) + case .notConnected, .connected, .closed: return .none } @@ -491,58 +497,76 @@ extension Subchannel { enum OnConnectFailed { case none case connect(Connection) + case closeAndEmitShutdownEvent(Connection) case backoff(Duration) - case shutdown } mutating func connectFailed(connector: any HTTP2Connector) -> OnConnectFailed { + let onConnectFailed: OnConnectFailed + switch self { - case .connecting(var connecting): - if let address = connecting.addressIterator.next() { - connecting.connection = Connection( + case .connecting(var state): + if state.shutdownRequested { + // Subchannel has been asked to shutdown, do so now. + self = .closing(Closing(from: state)) + onConnectFailed = .closeAndEmitShutdownEvent(state.connection) + } else if let address = state.addressIterator.next() { + state.connection = Connection( address: address, http2Connector: connector, defaultCompression: .none, enabledCompression: .all ) - self = .connecting(connecting) - return .connect(connecting.connection) + self = .connecting(state) + onConnectFailed = .connect(state.connection) } else { - connecting.addressIterator = connecting.addresses.makeIterator() - let address = connecting.addressIterator.next()! - connecting.connection = Connection( + state.addressIterator = state.addresses.makeIterator() + let address = state.addressIterator.next()! + state.connection = Connection( address: address, http2Connector: connector, defaultCompression: .none, enabledCompression: .all ) - let backoff = connecting.backoff.next() - self = .connecting(connecting) - return .backoff(backoff) + let backoff = state.backoff.next() + self = .connecting(state) + onConnectFailed = .backoff(backoff) } case .closing: - self = .closed - return .shutdown + // Should be handled via connection.closeRequested + assertionFailure("Invalid state") + onConnectFailed = .none case .notConnected, .connected, .closed: - return .none + onConnectFailed = .none } + + return onConnectFailed } enum OnBackedOff { case none case connect(Connection) - case shutdown + case close(Connection) } mutating func backedOff() -> OnBackedOff { switch self { case .connecting(let state): - return .connect(state.connection) + if state.shutdownRequested { + self = .closing(Closing(from: state)) + return .close(state.connection) + } else { + self = .connecting(state) + return .connect(state.connection) + } + case .closing: - self = .closed - return .shutdown + // Shouldn't happen via the connecting state. + assertionFailure("Invalid state") + return .none + case .notConnected, .connected, .closed: return .none } @@ -558,20 +582,52 @@ extension Subchannel { } } - mutating func closed(reason: Connection.CloseReason) -> Bool { + enum OnClosed { + case nothing + case emitIdle + case emitTransientFailureAndReconnect + case finish(emitShutdown: Bool) + } + + mutating func closed(reason: Connection.CloseReason) -> OnClosed { + let onClosed: OnClosed + switch self { - case .connected, .closing: + case .connected: + switch reason { + case .idleTimeout, .remote, .error(_, wasIdle: true): + self = .notConnected + onClosed = .emitIdle + + case .keepaliveTimeout, .error(_, wasIdle: false): + self = .notConnected + onClosed = .emitTransientFailureAndReconnect + + case .initiatedLocally: + self = .closed + onClosed = .finish(emitShutdown: true) + } + + case .closing: switch reason { - case .idleTimeout, .keepaliveTimeout, .error, .remote: + case .idleTimeout, .remote, .error(_, wasIdle: true): + self = .notConnected + onClosed = .emitIdle + + case .keepaliveTimeout, .error(_, wasIdle: false): self = .notConnected + onClosed = .emitTransientFailureAndReconnect + case .initiatedLocally: self = .closed + onClosed = .finish(emitShutdown: false) } - return true case .notConnected, .connecting, .closed: - return false + onClosed = .nothing } + + return onClosed } } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index f24520aeb..0d7f4f230 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -747,6 +747,58 @@ final class GRPCChannelTests: XCTestCase { group.cancelAll() } } + + func testQueueRequestsThenClose() async throws { + let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) + continuation.yield(.init(endpoints: [Endpoint()], serviceConfig: nil)) + + // Set a high backoff so the channel stays in transient failure for long enough. + var config = GRPCChannel.Config.defaults + config.backoff.initial = .seconds(120) + + let channel = GRPCChannel( + resolver: .static( + endpoints: [ + Endpoint(.unixDomainSocket(path: "/testQueueRequestsThenClose")) + ] + ), + connector: .posix(), + config: .defaults, + defaultServiceConfig: ServiceConfig() + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + await channel.connect() + } + + for try await state in channel.connectivityState { + switch state { + case .transientFailure: + group.addTask { + // Sleep a little to increase the chances of the stream being queued before the channel + // reacts to the close. + try await Task.sleep(for: .milliseconds(10)) + channel.close() + } + + // Try to open a new stream. + await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { + try await channel.withStream(descriptor: .echoGet, options: .defaults) { stream in + XCTFail("Unexpected new stream") + } + } errorHandler: { error in + XCTAssertEqual(error.code, .unavailable) + } + + default: + () + } + } + + group.cancelAll() + } + } } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) From ed90b7c7aebd409e45ffa45a89bcd6de6af87c79 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 21 Jun 2024 13:47:09 +0100 Subject: [PATCH 369/580] Add performance worker CLI (#1946) Motivation: The perf worker needs a CLI to configure the port to listen on as well as start the worker server. Modification: - Add CLI Result: Can run performance-worker --- Package.swift | 3 +- .../PerformanceWorker.swift | 70 +++++++++++++++++++ Sources/performance-worker/main.swift | 18 ----- 3 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 Sources/performance-worker/PerformanceWorker.swift delete mode 100644 Sources/performance-worker/main.swift diff --git a/Package.swift b/Package.swift index e42408bdf..831db2548 100644 --- a/Package.swift +++ b/Package.swift @@ -263,7 +263,8 @@ extension Target { .grpcHTTP2TransportNIOPosix, .grpcProtobuf, .nioCore, - .nioFileSystem + .nioFileSystem, + .argumentParser ] ) diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift new file mode 100644 index 000000000..cf057c20d --- /dev/null +++ b/Sources/performance-worker/PerformanceWorker.swift @@ -0,0 +1,70 @@ +/* + * 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 ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix +import NIOPosix + +@main +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct PerformanceWorker: AsyncParsableCommand { + static var configuration: CommandConfiguration { + CommandConfiguration( + commandName: "performance-worker", + discussion: """ + This program starts a gRPC server running the 'worker' service. The worker service is \ + instructed by a driver program to become a benchmark client or a benchmark server. + + Typically at least two workers are started (at least one server and one client), and the \ + driver instructs benchmark clients to execute various scenarios against benchmark servers. \ + Results are reported back to the driver once scenarios have been completed. + + See https://grpc.io/docs/guides/benchmarking for more details. + """ + ) + } + + @Option( + name: .customLong("driver_port"), + help: "Port to listen on for connections from the driver." + ) + var driverPort: Int + + func run() async throws { + debugOnly { + print("[WARNING] performance-worker built in DEBUG mode, results won't be representative.") + } + + let transport = HTTP2ServerTransport.Posix( + address: .ipv4(host: "127.0.0.1", port: self.driverPort), + config: .defaults + ) + + let server = GRPCServer(transport: transport, services: [WorkerService()]) + try await server.run() + } +} + +private func debugOnly(_ body: () -> Void) { + assert(alwaysTrue(body)) +} + +private func alwaysTrue(_ body: () -> Void) -> Bool { + body() + return true +} diff --git a/Sources/performance-worker/main.swift b/Sources/performance-worker/main.swift deleted file mode 100644 index 0dddaca9f..000000000 --- a/Sources/performance-worker/main.swift +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -func main(args: [String]) throws { -} From 9d98758d90f6c23101ac2d730bf3f1a754f73aaa Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 21 Jun 2024 15:51:27 +0100 Subject: [PATCH 370/580] Propagate timeout to server (#1947) Motivation: gRPC timeouts can be propagated to the server via the 'grpc-timeout' metadata field. The server can then also choose to enforce the timeout. At the moment this value isn't propagated, but it should be. Modifications: - Convert the timeout to a deadline when it's passed to the rpc executor and use this internally. Compute a timeout from the deadline and current time when executing an RPC. Result: Timeout is propagated to server --- .../ClientRPCExecutor+HedgingExecutor.swift | 17 ++++--- .../ClientRPCExecutor+OneShotExecutor.swift | 15 +++--- .../ClientRPCExecutor+RetryExecutor.swift | 18 ++++--- .../Client/Internal/ClientRPCExecutor.swift | 8 ++-- ...PCExecutorTestHarness+ServerBehavior.swift | 3 +- .../Internal/ClientRPCExecutorTests.swift | 48 ++++++++++++++++++- 6 files changed, 85 insertions(+), 24 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 122d3ef0d..7b9f03e8f 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -32,7 +32,7 @@ extension ClientRPCExecutor { @usableFromInline let policy: HedgingPolicy @usableFromInline - let timeout: Duration? + let deadline: ContinuousClock.Instant? @usableFromInline let interceptors: [any ClientInterceptor] @usableFromInline @@ -46,7 +46,7 @@ extension ClientRPCExecutor { init( transport: Transport, policy: HedgingPolicy, - timeout: Duration?, + deadline: ContinuousClock.Instant?, interceptors: [any ClientInterceptor], serializer: Serializer, deserializer: Deserializer, @@ -54,7 +54,7 @@ extension ClientRPCExecutor { ) { self.transport = transport self.policy = policy - self.timeout = timeout + self.deadline = deadline self.interceptors = interceptors self.serializer = serializer self.deserializer = deserializer @@ -83,10 +83,10 @@ extension ClientRPCExecutor.HedgingExecutor { // all other in flight attempts. Each attempt is started at a fixed interval unless the server // explicitly overrides the period using "pushback". let result = await withTaskGroup(of: _HedgingTaskResult.self) { group in - if let timeout = self.timeout { + if let deadline = self.deadline { group.addTask { let result = await Result { - try await Task.sleep(for: timeout, clock: .continuous) + try await Task.sleep(until: deadline, clock: .continuous) } return .timedOut(result) } @@ -103,7 +103,12 @@ extension ClientRPCExecutor.HedgingExecutor { } group.addTask { - let replayableRequest = ClientRequest.Stream(metadata: request.metadata) { writer in + var metadata = request.metadata + if let deadline = self.deadline { + metadata.timeout = ContinuousClock.now.duration(to: deadline) + } + + let replayableRequest = ClientRequest.Stream(metadata: metadata) { writer in try await writer.write(contentsOf: broadcast.stream) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index ad741a928..417ccd59d 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -32,7 +32,7 @@ extension ClientRPCExecutor { @usableFromInline let transport: Transport @usableFromInline - let timeout: Duration? + let deadline: ContinuousClock.Instant? @usableFromInline let interceptors: [any ClientInterceptor] @usableFromInline @@ -43,13 +43,13 @@ extension ClientRPCExecutor { @inlinable init( transport: Transport, - timeout: Duration?, + deadline: ContinuousClock.Instant?, interceptors: [any ClientInterceptor], serializer: Serializer, deserializer: Deserializer ) { self.transport = transport - self.timeout = timeout + self.deadline = deadline self.interceptors = interceptors self.serializer = serializer self.deserializer = deserializer @@ -72,10 +72,13 @@ extension ClientRPCExecutor.OneShotExecutor { ) { group in do { return try await self.transport.withStream(descriptor: method, options: options) { stream in - if let timeout = self.timeout { + var request = request + + if let deadline = self.deadline { + request.metadata.timeout = ContinuousClock.now.duration(to: deadline) group.addTask { let result = await Result { - try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + try await Task.sleep(until: deadline, clock: .continuous) } return .timedOut(result) } @@ -87,7 +90,7 @@ extension ClientRPCExecutor.OneShotExecutor { return .streamExecutorCompleted } - group.addTask { + group.addTask { [request] in let response = await ClientRPCExecutor.unsafeExecute( request: request, method: method, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 5ac25a692..0bdd30852 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -32,7 +32,7 @@ extension ClientRPCExecutor { @usableFromInline let policy: RetryPolicy @usableFromInline - let timeout: Duration? + let deadline: ContinuousClock.Instant? @usableFromInline let interceptors: [any ClientInterceptor] @usableFromInline @@ -46,7 +46,7 @@ extension ClientRPCExecutor { init( transport: Transport, policy: RetryPolicy, - timeout: Duration?, + deadline: ContinuousClock.Instant?, interceptors: [any ClientInterceptor], serializer: Serializer, deserializer: Deserializer, @@ -54,7 +54,7 @@ extension ClientRPCExecutor { ) { self.transport = transport self.policy = policy - self.timeout = timeout + self.deadline = deadline self.interceptors = interceptors self.serializer = serializer self.deserializer = deserializer @@ -94,10 +94,10 @@ extension ClientRPCExecutor.RetryExecutor { returning: Result.self ) { group in // Add a task to limit the overall execution time of the RPC. - if let timeout = self.timeout { + if let deadline = self.deadline { group.addTask { let result = await Result { - try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + try await Task.sleep(until: deadline, clock: .continuous) } return .timedOut(result) } @@ -136,8 +136,14 @@ extension ClientRPCExecutor.RetryExecutor { } thisAttemptGroup.addTask { + var metadata = request.metadata + // Work out the timeout from the deadline. + if let deadline = self.deadline { + metadata.timeout = ContinuousClock.now.duration(to: deadline) + } + let response = await ClientRPCExecutor.unsafeExecute( - request: ClientRequest.Stream(metadata: request.metadata) { + request: ClientRequest.Stream(metadata: metadata) { try await $0.write(contentsOf: retry.stream) }, method: method, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index db95e3501..a01bd91e2 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -42,11 +42,13 @@ enum ClientRPCExecutor { interceptors: [any ClientInterceptor], handler: @Sendable @escaping (ClientResponse.Stream) async throws -> Result ) async throws -> Result { + let deadline = options.timeout.map { ContinuousClock.now + $0 } + switch options.executionPolicy?.wrapped { case .none: let oneShotExecutor = OneShotExecutor( transport: transport, - timeout: options.timeout, + deadline: deadline, interceptors: interceptors, serializer: serializer, deserializer: deserializer @@ -63,7 +65,7 @@ enum ClientRPCExecutor { let retryExecutor = RetryExecutor( transport: transport, policy: policy, - timeout: options.timeout, + deadline: deadline, interceptors: interceptors, serializer: serializer, deserializer: deserializer, @@ -81,7 +83,7 @@ enum ClientRPCExecutor { let hedging = HedgingExecutor( transport: transport, policy: policy, - timeout: options.timeout, + deadline: deadline, interceptors: interceptors, serializer: serializer, deserializer: deserializer, diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index d4ad99097..f3962bf86 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -51,8 +51,7 @@ extension ClientRPCExecutorTestHarness { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { - return Self { - stream in + return Self { stream in let response = stream.inbound.map { part -> RPCResponsePart in switch part { case .metadata(let metadata): diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 355a4fda5..2a59101b4 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import GRPCCore + import XCTest +@testable import GRPCCore + @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { @@ -223,4 +225,48 @@ final class ClientRPCExecutorTests: XCTestCase { XCTAssertEqual(tester.clientStreamOpenFailures, 1) XCTAssertEqual(tester.serverStreamsAccepted, 0) } + + func testTimeoutIsPropagated() async throws { + // 'nil' means no retires or hedging, just try to execute the RPC once. + var policies: [RPCExecutionPolicy?] = [nil] + + let retryPolicy = RetryPolicy( + maximumAttempts: 5, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(1), + backoffMultiplier: 1.6, + retryableStatusCodes: [.unavailable] + ) + policies.append(.retry(retryPolicy)) + + let hedgingPolicy = HedgingPolicy( + maximumAttempts: 5, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [.unavailable] + ) + policies.append(.hedge(hedgingPolicy)) + + for policy in policies { + let timeout = Duration.seconds(120) + var options = CallOptions.defaults + options.timeout = timeout + options.executionPolicy = policy + + let tester = ClientRPCExecutorTestHarness(transport: .inProcess, server: .echo) + try await tester.unary( + request: ClientRequest.Single(message: []), + options: options + ) { response in + let timeoutMetadata = Array(response.metadata[stringValues: "grpc-timeout"]) + let parsed = try XCTUnwrap(timeoutMetadata.first.flatMap { Timeout(decoding: $0) }) + + // The timeout is handled as a deadline internally and gets converted back to a timeout + // when transmitted as metadata, so allow some leeway when checking the value. + let leeway = Duration.seconds(1) + let acceptable: ClosedRange = timeout - leeway ... timeout + leeway + + XCTAssert(acceptable.contains(parsed.duration)) + } + } + } } From 82447fccc036eb05ea8ffea822827fc866214497 Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Mon, 24 Jun 2024 12:56:29 +0100 Subject: [PATCH 371/580] Manually cleanup unix domain socket path in NIOTS test (#1951) --- .../HTTP2TransportNIOTransportServicesTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index a6446ea7f..f6b0de779 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -64,6 +64,10 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( address: .unixDomainSocket(path: "/tmp/niots-uds-test") ) + defer { + // NIOTS does not unlink the UDS on close. + try? FileManager.default.removeItem(atPath: "/tmp/niots-uds-test") + } try await withThrowingDiscardingTaskGroup { group in group.addTask { From 621bcf2ec562101df5f22d803f693b66d70634fe Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 24 Jun 2024 16:49:55 +0100 Subject: [PATCH 372/580] Make Subchannel internal state handling clearer (#1949) Motivation: The subchannel conflates closing (recoverable) with shutting down (terminal). Shutting down is initiated by a higher level (load-balancer, user) while closing happens when the subchannel closes unexpectedtly or is no longer required (i.e. becomes idle). A closed subchannel can be re-opeend, a shutdown subchannel can't. This distinction isn't clear enough in the state handling. Modifications: - Rename 'close' to 'shutDown' where applicable - Add a new 'shutting down' state and renaming the 'closing' state to 'going away'. - Add a state machine diagram - Fix up a few state transitions Result: More robust state handling --- .../LoadBalancers/PickFirstLoadBalancer.swift | 10 +- .../RoundRobinLoadBalancer.swift | 10 +- .../Connection/LoadBalancers/Subchannel.swift | 239 +++++++++++------- .../Client/Connection/GRPCChannelTests.swift | 3 - .../LoadBalancers/SubchannelTests.swift | 22 +- 5 files changed, 164 insertions(+), 120 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift index 1b9f54fb0..203c74742 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift @@ -185,7 +185,7 @@ extension PickFirstLoadBalancer { switch onUpdate { case .connect(let newSubchannel, close: let oldSubchannel): self.runSubchannel(newSubchannel, in: &group) - oldSubchannel?.close() + oldSubchannel?.shutDown() case .none: () @@ -226,9 +226,9 @@ extension PickFirstLoadBalancer { switch onUpdateState { case .close(let subchannel): - subchannel.close() + subchannel.shutDown() case .closeAndPublishStateChange(let subchannel, let connectivityState): - subchannel.close() + subchannel.shutDown() self.event.continuation.yield(.connectivityStateChanged(connectivityState)) case .publishStateChange(let connectivityState): self.event.continuation.yield(.connectivityStateChanged(connectivityState)) @@ -251,8 +251,8 @@ extension PickFirstLoadBalancer { switch onClose { case .closeSubchannels(let subchannel1, let subchannel2): self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - subchannel1.close() - subchannel2?.close() + subchannel1.shutDown() + subchannel2?.shutDown() case .closed: self.event.continuation.yield(.connectivityStateChanged(.shutdown)) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift index f2f785295..8039ca655 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -245,7 +245,7 @@ extension RoundRobinLoadBalancer { // present if there are more to remove than to add. These are the excess subchannels which // are closed now. for subchannel in removed { - subchannel.close() + subchannel.shutDown() } } @@ -288,10 +288,10 @@ extension RoundRobinLoadBalancer { case .closeAndPublishStateChange(let subchannel, let aggregateState): self.event.continuation.yield(.connectivityStateChanged(aggregateState)) - subchannel.close() + subchannel.shutDown() case .close(let subchannel): - subchannel.close() + subchannel.shutDown() case .closed: // All subchannels are closed; finish the streams so the run loop exits. @@ -306,7 +306,7 @@ extension RoundRobinLoadBalancer { private func handleSubchannelGoingAway(key: EndpointKey) { switch self.state.withLockedValue({ $0.parkSubchannel(withKey: key) }) { case .closeAndUpdateState(let subchannel, let connectivityState): - subchannel.close() + subchannel.shutDown() if let connectivityState = connectivityState { self.event.continuation.yield(.connectivityStateChanged(connectivityState)) } @@ -323,7 +323,7 @@ extension RoundRobinLoadBalancer { // Close the subchannels. for subchannel in subchannels { - subchannel.close() + subchannel.shutDown() } case .closed: diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift index d1c88a490..491b54705 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift @@ -23,7 +23,7 @@ import NIOConcurrencyHelpers /// endpoint. You can tell it to start connecting by calling ``connect()`` and you can listen /// to connectivity state changes by consuming the ``events`` sequence. /// -/// You must call ``close()`` on the ``Subchannel`` when it's no longer required. This will move +/// You must call ``shutDown()`` on the ``Subchannel`` when it's no longer required. This will move /// it to the ``ConnectivityState/shutdown`` state: existing RPCs may continue but all subsequent /// calls to ``makeStream(descriptor:options:)`` will fail. /// @@ -60,8 +60,8 @@ struct Subchannel { case connect /// A backoff period has ended. case backedOff - /// Close the connection, if possible. - case close + /// Shuts down the connection, if possible. + case shutDown /// Handle the event from the underlying connection object. case handleConnectionEvent(Connection.Event) } @@ -103,7 +103,7 @@ struct Subchannel { ) { assert(!endpoint.addresses.isEmpty, "endpoint.addresses mustn't be empty") - self.state = NIOLockedValueBox(.notConnected) + self.state = NIOLockedValueBox(.notConnected(.initial)) self.endpoint = endpoint self.id = id self.connector = connector @@ -140,8 +140,8 @@ extension Subchannel { self.handleConnectInput(in: &group) case .backedOff: self.handleBackedOffInput(in: &group) - case .close: - self.handleCloseInput(in: &group) + case .shutDown: + self.handleShutDownInput(in: &group) case .handleConnectionEvent(let event): self.handleConnectionEvent(event, in: &group) } @@ -161,8 +161,8 @@ extension Subchannel { } /// Initiates graceful shutdown, if possible. - func close() { - self.input.continuation.yield(.close) + func shutDown() { + self.input.continuation.yield(.shutDown) } /// Make a stream using the subchannel if it's ready. @@ -175,7 +175,7 @@ extension Subchannel { ) async throws -> Connection.Stream { let connection: Connection? = self.state.withLockedValue { state in switch state { - case .notConnected, .connecting, .closing, .closed: + case .notConnected, .connecting, .goingAway, .shuttingDown, .shutDown: return nil case .connected(let connected): return connected.connection @@ -218,8 +218,9 @@ extension Subchannel { case .none: () - case .close(let connection): - connection.close() + case .finish: + self.event.continuation.finish() + self.input.continuation.finish() case .connect(let connection): // About to start connecting, emit a state change event. @@ -228,11 +229,15 @@ extension Subchannel { } } - private func handleCloseInput(in group: inout DiscardingTaskGroup) { - switch self.state.withLockedValue({ $0.close() }) { + private func handleShutDownInput(in group: inout DiscardingTaskGroup) { + switch self.state.withLockedValue({ $0.shutDown() }) { case .none: () + case .emitShutdown: + // Connection closed because the load balancer asked it to, so notify the load balancer. + self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + case .emitShutdownAndClose(let connection): // Connection closed because the load balancer asked it to, so notify the load balancer. self.event.continuation.yield(.connectivityStateChanged(.shutdown)) @@ -269,8 +274,10 @@ extension Subchannel { // Emit a connectivity state change: the load balancer can now use this subchannel. self.event.continuation.yield(.connectivityStateChanged(.ready)) - case .closeAndEmitShutdown(let connection): + case .finishAndClose(let connection): self.event.continuation.yield(.connectivityStateChanged(.shutdown)) + self.event.continuation.finish() + self.input.continuation.finish() connection.close() case .none: @@ -299,9 +306,9 @@ extension Subchannel { } } - case .closeAndEmitShutdownEvent(let connection): - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - connection.close() + case .finish: + self.event.continuation.finish() + self.input.continuation.finish() case .none: () @@ -363,24 +370,58 @@ extension Subchannel { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Subchannel { + /// ┌───────────────┐ + /// ┌───────▶│ NOT CONNECTED │───────────shutDown─────────────┐ + /// │ └───────────────┘ │ + /// │ │ │ + /// │ connFailed──┤connect │ + /// │ /backedOff │ │ + /// │ │ ▼ │ + /// │ │ ┌───────────────┐ │ + /// │ └──│ CONNECTING │──────┐ │ + /// │ └───────────────┘ │ │ + /// │ │ │ │ + /// closed connSucceeded │ │ + /// │ │ │ │ + /// │ ▼ │ │ + /// │ ┌───────────────┐ │ ┌───────────────┐ │ + /// │ │ CONNECTED │──shutDown──▶│ SHUTTING DOWN │ │ + /// │ └───────────────┘ │ └───────────────┘ │ + /// │ │ │ │ │ + /// │ goAway │ closed │ + /// │ │ │ │ │ + /// │ ▼ │ ▼ │ + /// │ ┌───────────────┐ │ ┌───────────────┐ │ + /// └────────│ GOING AWAY │──────┘ │ SHUT DOWN │◀─┘ + /// └───────────────┘ └───────────────┘ private enum State { /// Not connected and not actively connecting. - case notConnected + case notConnected(NotConnected) /// A connection attempt is in-progress. case connecting(Connecting) /// A connection has been established. case connected(Connected) - /// The subchannel is closing. - case closing(Closing) - /// The subchannel is closed. - case closed + /// The subchannel is going away. It may return to the 'notConnected' state when the underlying + /// connection has closed. + case goingAway(GoingAway) + /// The subchannel is shutting down, it will enter the 'shutDown' state when closed, it may not + /// enter any other state. + case shuttingDown(ShuttingDown) + /// The subchannel is shutdown, this is a terminal state. + case shutDown(ShutDown) + + struct NotConnected { + private init() {} + static let initial = NotConnected() + init(from state: Connected) {} + init(from state: GoingAway) {} + } struct Connecting { var connection: Connection let addresses: [SocketAddress] var addressIterator: Array.Iterator var backoff: ConnectionBackoff.Iterator - var shutdownRequested: Bool = false } struct Connected { @@ -391,7 +432,7 @@ extension Subchannel { } } - struct Closing { + struct GoingAway { var connection: Connection init(from state: Connecting) { @@ -403,6 +444,28 @@ extension Subchannel { } } + struct ShuttingDown { + var connection: Connection + + init(from state: Connecting) { + self.connection = state.connection + } + + init(from state: Connected) { + self.connection = state.connection + } + + init(from state: GoingAway) { + self.connection = state.connection + } + } + + struct ShutDown { + init(from state: ShuttingDown) {} + init(from state: GoingAway) {} + init(from state: NotConnected) {} + } + mutating func makeConnection( to addresses: [SocketAddress], using connector: any HTTP2Connector, @@ -432,7 +495,7 @@ extension Subchannel { self = .connecting(connecting) return connection - case .connecting, .connected, .closing, .closed: + case .connecting, .connected, .goingAway, .shuttingDown, .shutDown: return nil } } @@ -441,63 +504,62 @@ extension Subchannel { case none case emitShutdownAndFinish case emitShutdownAndClose(Connection) + case emitShutdown } - mutating func close() -> OnClose { - let onClose: OnClose + mutating func shutDown() -> OnClose { + let onShutDown: OnClose switch self { - case .notConnected: - onClose = .emitShutdownAndFinish + case .notConnected(let state): + self = .shutDown(ShutDown(from: state)) + onShutDown = .emitShutdownAndFinish - case .connecting(var state): - state.shutdownRequested = true - self = .connecting(state) - // Do nothing; the connection hasn't been established yet so can't be closed. - onClose = .none + case .connecting(let state): + // Only emit the shutdown; there's no connection to close yet. + self = .shuttingDown(ShuttingDown(from: state)) + onShutDown = .emitShutdown case .connected(let state): - self = .closing(Closing(from: state)) - onClose = .emitShutdownAndClose(state.connection) + self = .shuttingDown(ShuttingDown(from: state)) + onShutDown = .emitShutdownAndClose(state.connection) - case .closing, .closed: - onClose = .none + case .goingAway(let state): + self = .shuttingDown(ShuttingDown(from: state)) + onShutDown = .emitShutdown + + case .shuttingDown, .shutDown: + onShutDown = .none } - return onClose + return onShutDown } enum OnConnectSucceeded { case updateStateToReady - case closeAndEmitShutdown(Connection) + case finishAndClose(Connection) case none } mutating func connectSucceeded() -> OnConnectSucceeded { switch self { case .connecting(let state): - if state.shutdownRequested { - self = .closing(Closing(from: state)) - return .closeAndEmitShutdown(state.connection) - } else { - self = .connected(Connected(from: state)) - return .updateStateToReady - } + self = .connected(Connected(from: state)) + return .updateStateToReady - case .closing(let state): - // Shouldn't happen via the connecting state. - assertionFailure("Invalid state") - return .closeAndEmitShutdown(state.connection) + case .shuttingDown(let state): + self = .shutDown(ShutDown(from: state)) + return .finishAndClose(state.connection) - case .notConnected, .connected, .closed: + case .notConnected, .connected, .goingAway, .shutDown: return .none } } enum OnConnectFailed { case none + case finish case connect(Connection) - case closeAndEmitShutdownEvent(Connection) case backoff(Duration) } @@ -506,11 +568,7 @@ extension Subchannel { switch self { case .connecting(var state): - if state.shutdownRequested { - // Subchannel has been asked to shutdown, do so now. - self = .closing(Closing(from: state)) - onConnectFailed = .closeAndEmitShutdownEvent(state.connection) - } else if let address = state.addressIterator.next() { + if let address = state.addressIterator.next() { state.connection = Connection( address: address, http2Connector: connector, @@ -533,12 +591,11 @@ extension Subchannel { onConnectFailed = .backoff(backoff) } - case .closing: - // Should be handled via connection.closeRequested - assertionFailure("Invalid state") - onConnectFailed = .none + case .shuttingDown(let state): + self = .shutDown(ShutDown(from: state)) + onConnectFailed = .finish - case .notConnected, .connected, .closed: + case .notConnected, .connected, .goingAway, .shutDown: onConnectFailed = .none } @@ -548,26 +605,20 @@ extension Subchannel { enum OnBackedOff { case none case connect(Connection) - case close(Connection) + case finish } mutating func backedOff() -> OnBackedOff { switch self { case .connecting(let state): - if state.shutdownRequested { - self = .closing(Closing(from: state)) - return .close(state.connection) - } else { - self = .connecting(state) - return .connect(state.connection) - } + self = .connecting(state) + return .connect(state.connection) - case .closing: - // Shouldn't happen via the connecting state. - assertionFailure("Invalid state") - return .none + case .shuttingDown(let state): + self = .shutDown(ShutDown(from: state)) + return .finish - case .notConnected, .connected, .closed: + case .notConnected, .connected, .goingAway, .shutDown: return .none } } @@ -575,9 +626,9 @@ extension Subchannel { mutating func goingAway() -> Bool { switch self { case .connected(let state): - self = .closing(Closing(from: state)) + self = .goingAway(GoingAway(from: state)) return true - case .notConnected, .closing, .connecting, .closed: + case .notConnected, .goingAway, .connecting, .shuttingDown, .shutDown: return false } } @@ -593,37 +644,33 @@ extension Subchannel { let onClosed: OnClosed switch self { - case .connected: + case .connected(let state): switch reason { case .idleTimeout, .remote, .error(_, wasIdle: true): - self = .notConnected + self = .notConnected(NotConnected(from: state)) onClosed = .emitIdle case .keepaliveTimeout, .error(_, wasIdle: false): - self = .notConnected + self = .notConnected(NotConnected(from: state)) onClosed = .emitTransientFailureAndReconnect case .initiatedLocally: - self = .closed + // Should be in the 'shuttingDown' state. + assertionFailure("Invalid state") + let shuttingDown = State.ShuttingDown(from: state) + self = .shutDown(ShutDown(from: shuttingDown)) onClosed = .finish(emitShutdown: true) } - case .closing: - switch reason { - case .idleTimeout, .remote, .error(_, wasIdle: true): - self = .notConnected - onClosed = .emitIdle - - case .keepaliveTimeout, .error(_, wasIdle: false): - self = .notConnected - onClosed = .emitTransientFailureAndReconnect + case .goingAway(let state): + self = .notConnected(NotConnected(from: state)) + onClosed = .emitIdle - case .initiatedLocally: - self = .closed - onClosed = .finish(emitShutdown: false) - } + case .shuttingDown(let state): + self = .shutDown(ShutDown(from: state)) + return .finish(emitShutdown: false) - case .notConnected, .connecting, .closed: + case .notConnected, .connecting, .shutDown: onClosed = .nothing } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index 0d7f4f230..3a58dc054 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -749,9 +749,6 @@ final class GRPCChannelTests: XCTestCase { } func testQueueRequestsThenClose() async throws { - let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) - continuation.yield(.init(endpoints: [Endpoint()], serviceConfig: nil)) - // Set a high backoff so the channel stays in transient failure for long enough. var config = GRPCChannel.Config.defaults config.backoff.initial = .seconds(120) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index e56065ed9..501fb9d27 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -35,7 +35,7 @@ final class SubchannelTests: XCTestCase { XCTAssertEqual(error.code, .unavailable) } - subchannel.close() + subchannel.shutDown() } func testMakeStreamOnShutdownSubchannel() async throws { @@ -48,7 +48,7 @@ final class SubchannelTests: XCTestCase { connector: .never ) - subchannel.close() + subchannel.shutDown() await subchannel.run() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { @@ -105,7 +105,7 @@ final class SubchannelTests: XCTestCase { } } } - subchannel.close() + subchannel.shutDown() default: () @@ -121,7 +121,7 @@ final class SubchannelTests: XCTestCase { let subchannel = self.makeSubchannel( address: .unixDomainSocket(path: path), connector: .posix(), - backoff: .fixed(at: .milliseconds(100)) + backoff: .fixed(at: .milliseconds(10)) ) await withThrowingTaskGroup(of: Void.self) { group in @@ -150,7 +150,7 @@ final class SubchannelTests: XCTestCase { } case .connectivityStateChanged(.ready): - subchannel.close() + subchannel.shutDown() case .connectivityStateChanged(.shutdown): group.cancelAll() @@ -212,7 +212,7 @@ final class SubchannelTests: XCTestCase { case .connectivityStateChanged(.idle): subchannel.connect() case .connectivityStateChanged(.ready): - subchannel.close() + subchannel.shutDown() case .connectivityStateChanged(.shutdown): group.cancelAll() default: @@ -263,7 +263,7 @@ final class SubchannelTests: XCTestCase { } case .connectivityStateChanged(.ready): - subchannel.close() + subchannel.shutDown() case .connectivityStateChanged(.shutdown): group.cancelAll() @@ -304,7 +304,7 @@ final class SubchannelTests: XCTestCase { if idleCount == 1 { subchannel.connect() } else { - subchannel.close() + subchannel.shutDown() } case .connectivityStateChanged(.shutdown): @@ -356,7 +356,7 @@ final class SubchannelTests: XCTestCase { case 1: subchannel.connect() case 2: - subchannel.close() + subchannel.shutDown() default: XCTFail("Unexpected idle") } @@ -430,7 +430,7 @@ final class SubchannelTests: XCTestCase { let _ = try await iterator.next() } } else if readyCount == 2 { - subchannel.close() + subchannel.shutDown() } case .connectivityStateChanged(.shutdown): @@ -484,7 +484,7 @@ final class SubchannelTests: XCTestCase { if idleCount == 1 { subchannel.connect() } else if idleCount == 2 { - subchannel.close() + subchannel.shutDown() } case .connectivityStateChanged(.ready): From 115633331706c050d3d7d3e68e9908acb0c41187 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 25 Jun 2024 10:41:11 +0100 Subject: [PATCH 373/580] Add HTTP/2 transport integration tests (#1948) Motivation: The http/2 transports are only tested via unit tests. Modifications: Add a number of integration tests for the HTTP/2 transports Result: Better test coverage --- .../HTTP2TransportTests.swift | 1430 +++++++++++++++++ .../HTTP2StatusCodeServer.swift | 112 ++ .../Test Utilities/XCTest+Utilities.swift | 30 + 3 files changed, 1572 insertions(+) create mode 100644 Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift create mode 100644 Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift create mode 100644 Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift new file mode 100644 index 000000000..e15953486 --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -0,0 +1,1430 @@ +/* + * 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 GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix +import GRPCHTTP2TransportNIOTransportServices +import GRPCProtobuf +import XCTest + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +final class HTTP2TransportTests: XCTestCase { + // A combination of client and server transport kinds. + struct Transport: Sendable, CustomStringConvertible { + var server: Kind + var client: Kind + + enum Kind: Sendable, CustomStringConvertible { + case posix + case niots + + var description: String { + switch self { + case .posix: + return "NIOPosix" + case .niots: + return "NIOTS" + } + } + } + + var description: String { + "server=\(self.server) client=\(self.client)" + } + } + + func forEachTransportPair( + _ transport: [Transport] = .supported, + enableControlService: Bool = true, + clientCompression: CompressionAlgorithm = .none, + clientEnabledCompression: CompressionAlgorithmSet = .none, + serverCompression: CompressionAlgorithmSet = .none, + _ execute: (ControlClient, Transport) async throws -> Void + ) async throws { + for pair in transport { + try await withThrowingTaskGroup(of: Void.self) { group in + let address = try await self.runServer( + in: &group, + kind: pair.server, + enableControlService: enableControlService, + compression: serverCompression + ) + + let target: any ResolvableTarget + if let ipv4 = address.ipv4 { + target = .ipv4(host: ipv4.host, port: ipv4.port) + } else if let ipv6 = address.ipv6 { + target = .ipv6(host: ipv6.host, port: ipv6.port) + } else if let uds = address.unixDomainSocket { + target = .unixDomainSocket(path: uds.path) + } else { + XCTFail("Unexpected address to connect to") + return + } + + let client = try self.makeClient( + kind: pair.client, + target: target, + compression: clientCompression, + enabledCompression: clientEnabledCompression + ) + + group.addTask { + try await client.run() + } + + do { + let control = ControlClient(client: client) + try await execute(control, pair) + } catch { + XCTFail("Unexpected error: '\(error)' (\(pair))") + } + + client.close() + group.cancelAll() + } + } + } + + func forEachClientAndHTTPStatusCodeServer( + _ kind: [Transport.Kind] = [.posix], + _ execute: (ControlClient, Transport.Kind) async throws -> Void + ) async throws { + for clientKind in kind { + try await withThrowingTaskGroup(of: Void.self) { group in + let server = HTTP2StatusCodeServer() + group.addTask { + try await server.run() + } + + let address = try await server.listeningAddress + let client = try self.makeClient( + kind: clientKind, + target: .ipv4(host: address.host, port: address.port), + compression: .none, + enabledCompression: .none + ) + group.addTask { + try await client.run() + } + + do { + let control = ControlClient(client: client) + try await execute(control, clientKind) + } catch { + XCTFail("Unexpected error: '\(error)' (\(clientKind))") + } + + group.cancelAll() + } + } + } + + private func runServer( + in group: inout ThrowingTaskGroup, + kind: Transport.Kind, + enableControlService: Bool, + compression: CompressionAlgorithmSet + ) async throws -> GRPCHTTP2Core.SocketAddress { + let services = enableControlService ? [ControlService()] : [] + + switch kind { + case .posix: + var config = HTTP2ServerTransport.Posix.Config.defaults + config.compression.enabledAlgorithms = compression + let transport = HTTP2ServerTransport.Posix( + address: .ipv4(host: "127.0.0.1", port: 0), + config: config + ) + + let server = GRPCServer(transport: transport, services: services) + group.addTask { + try await server.run() + } + + return try await transport.listeningAddress + + case .niots: + #if canImport(Network) + var config = HTTP2ServerTransport.TransportServices.Config.defaults + config.compression.enabledAlgorithms = compression + let transport = HTTP2ServerTransport.TransportServices( + address: .ipv4(host: "127.0.0.1", port: 0), + config: config + ) + + let server = GRPCServer(transport: transport, services: services) + group.addTask { + try await server.run() + } + + return try await transport.listeningAddress + #else + throw XCTSkip("Transport not supported on this platform") + #endif + } + } + + private func makeClient( + kind: Transport.Kind, + target: any ResolvableTarget, + compression: CompressionAlgorithm, + enabledCompression: CompressionAlgorithmSet + ) throws -> GRPCClient { + let transport: any ClientTransport + + switch kind { + case .posix: + var config = HTTP2ClientTransport.Posix.Config.defaults + config.compression.algorithm = compression + config.compression.enabledAlgorithms = enabledCompression + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + transport = try HTTP2ClientTransport.Posix( + target: target, + config: config, + serviceConfig: serviceConfig + ) + + case .niots: + fatalError("NIOTS isn't supported yet") + } + + return GRPCClient(transport: transport) + } + + func testUnaryOK() async throws { + // Client sends one message, server sends back metadata, a single message, and an ok status with + // trailing metadata. + try await self.forEachTransportPair { control, pair in + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.echoMetadataInTrailers = true + $0.numberOfMessages = 1 + $0.messageParams = .with { + $0.content = 0 + $0.size = 1024 + } + } + + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Single(message: input, metadata: metadata) + + try await control.unary(request: request) { response in + let message = try response.message + XCTAssertEqual(message.payload, Data(repeating: 0, count: 1024), "\(pair)") + + let initial = response.metadata + XCTAssertEqual(Array(initial["echo-test-key"]), ["test-value"], "\(pair)") + + let trailing = response.trailingMetadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + } + } + + func testUnaryNotOK() async throws { + // Client sends one message, server sends back metadata, a single message, and a non-ok status + // with trailing metadata. + try await self.forEachTransportPair { control, pair in + let input = ControlInput.with { + $0.echoMetadataInTrailers = true + $0.numberOfMessages = 1 + $0.messageParams = .with { + $0.content = 0 + $0.size = 1024 + } + $0.status = .with { + $0.code = .aborted + $0.message = "\(#function)" + } + } + + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Single(message: input, metadata: metadata) + + try await control.unary(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in + XCTAssertEqual(error.code, .aborted) + XCTAssertEqual(error.message, "\(#function)") + + let trailing = error.metadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + + let trailing = response.trailingMetadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + } + } + + func testUnaryRejected() async throws { + // Client sends one message, server sends non-ok status with trailing metadata. + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Single( + message: .trailersOnly(code: .aborted, message: "\(#function)", echoMetadata: true), + metadata: metadata + ) + + try await control.unary(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in + XCTAssertEqual(error.code, .aborted, "\(pair)") + XCTAssertEqual(error.message, "\(#function)", "\(pair)") + + let trailing = error.metadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + + // No initial metadata for trailers-only. + XCTAssertEqual(response.metadata, [:]) + + let trailing = response.trailingMetadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + } + } + + func testClientStreamingOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: metadata + ) { writer in + try await writer.write(.echoMetadata) + // Send a few messages which are ignored. + try await writer.write(.noOp) + try await writer.write(.noOp) + try await writer.write(.noOp) + // Send a message. + try await writer.write(.messages(1, repeating: 42, count: 1024)) + // ... and the final status. + try await writer.write(.status(code: .ok, message: "", echoMetadata: true)) + } + + try await control.clientStream(request: request) { response in + let message = try response.message + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") + + let initial = response.metadata + XCTAssertEqual(Array(initial["echo-test-key"]), ["test-value"], "\(pair)") + + let trailing = response.trailingMetadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + } + } + + func testClientStreamingNotOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: metadata + ) { writer in + try await writer.write(.echoMetadata) + // Send a few messages which are ignored. + try await writer.write(.noOp) + try await writer.write(.noOp) + try await writer.write(.noOp) + // Send a message. + try await writer.write(.messages(1, repeating: 42, count: 1024)) + // Send the final status. + try await writer.write(.status(code: .aborted, message: "\(#function)", echoMetadata: true)) + } + + try await control.clientStream(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in + XCTAssertEqual(error.code, .aborted, "\(pair)") + XCTAssertEqual(error.message, "\(#function)", "\(pair)") + + let trailing = error.metadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + + let initial = response.metadata + XCTAssertEqual(Array(initial["echo-test-key"]), ["test-value"], "\(pair)") + + let trailing = response.trailingMetadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + } + } + + func testClientStreamingRejected() async throws { + // Client sends one message, server sends non-ok status with trailing metadata. + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: metadata + ) { writer in + let message: ControlInput = .trailersOnly( + code: .aborted, + message: "\(#function)", + echoMetadata: true + ) + + try await writer.write(message) + } + + try await control.clientStream(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in + XCTAssertEqual(error.code, .aborted, "\(pair)") + XCTAssertEqual(error.message, "\(#function)", "\(pair)") + + let trailing = error.metadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + + // No initial metadata for trailers-only. + XCTAssertEqual(response.metadata, [:]) + + let trailing = response.trailingMetadata + XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") + } + } + } + + func testServerStreamingOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.echoMetadataInTrailers = true + $0.numberOfMessages = 5 + $0.messageParams = .with { + $0.content = 42 + $0.size = 1024 + } + } + + let request = ClientRequest.Single(message: input, metadata: metadata) + try await control.serverStream(request: request) { response in + switch response.accepted { + case .success(let contents): + XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") + + var messagesReceived = 0 + for try await part in contents.bodyParts { + switch part { + case .message(let message): + messagesReceived += 1 + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) + case .trailingMetadata(let metadata): + XCTAssertEqual(Array(metadata["echo-test-key"]), ["test-value"], "\(pair)") + } + } + + XCTAssertEqual(messagesReceived, 5) + + case .failure(let error): + throw error + } + } + } + } + + func testServerStreamingEmptyOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + // Echo back metadata, but don't send any messages. + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.echoMetadataInTrailers = true + } + + let request = ClientRequest.Single(message: input, metadata: metadata) + try await control.serverStream(request: request) { response in + switch response.accepted { + case .success(let contents): + XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") + + for try await part in contents.bodyParts { + switch part { + case .message: + XCTFail("Unexpected message") + case .trailingMetadata(let metadata): + XCTAssertEqual(Array(metadata["echo-test-key"]), ["test-value"], "\(pair)") + } + } + + case .failure(let error): + throw error + } + } + } + } + + func testServerStreamingNotOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.echoMetadataInTrailers = true + $0.numberOfMessages = 5 + $0.messageParams = .with { + $0.content = 42 + $0.size = 1024 + } + $0.status = .with { + $0.code = .aborted + $0.message = "\(#function)" + } + } + + let request = ClientRequest.Single(message: input, metadata: metadata) + try await control.serverStream(request: request) { response in + switch response.accepted { + case .success(let contents): + XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") + + var messagesReceived = 0 + do { + for try await part in contents.bodyParts { + switch part { + case .message(let message): + messagesReceived += 1 + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) + case .trailingMetadata: + XCTFail("Unexpected trailing metadata, should be provided in RPCError") + } + } + XCTFail("Expected error to be thrown") + } catch let error as RPCError { + XCTAssertEqual(error.code, .aborted) + XCTAssertEqual(error.message, "\(#function)") + XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") + } + + XCTAssertEqual(messagesReceived, 5) + + case .failure(let error): + throw error + } + } + } + } + + func testServerStreamingEmptyNotOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.echoMetadataInTrailers = true + $0.status = .with { + $0.code = .aborted + $0.message = "\(#function)" + } + } + + let request = ClientRequest.Single(message: input, metadata: metadata) + try await control.serverStream(request: request) { response in + switch response.accepted { + case .success(let contents): + XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") + + do { + for try await _ in contents.bodyParts { + XCTFail("Unexpected message, \(pair)") + } + XCTFail("Expected error to be thrown") + } catch let error as RPCError { + XCTAssertEqual(error.code, .aborted) + XCTAssertEqual(error.message, "\(#function)") + XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") + } + + case .failure(let error): + throw error + } + } + } + } + + func testServerStreamingRejected() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Single( + message: .trailersOnly(code: .aborted, message: "\(#function)", echoMetadata: true), + metadata: metadata + ) + + try await control.serverStream(request: request) { response in + switch response.accepted { + case .success: + XCTFail("Expected RPC to be rejected \(pair)") + case .failure(let error): + XCTAssertEqual(error.code, .aborted, "\(pair)") + XCTAssertEqual(error.message, "\(#function)", "\(pair)") + XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") + } + } + } + } + + func testBidiStreamingOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: metadata + ) { writer in + try await writer.write(.echoMetadata) + // Send a few messages, each is echo'd back. + try await writer.write(.messages(1, repeating: 42, count: 1024)) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + // Send the final status. + try await writer.write(.status(code: .ok, message: "", echoMetadata: true)) + } + + try await control.bidiStream(request: request) { response in + switch response.accepted { + case .success(let contents): + XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") + + var messagesReceived = 0 + for try await part in contents.bodyParts { + switch part { + case .message(let message): + messagesReceived += 1 + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) + case .trailingMetadata(let metadata): + XCTAssertEqual(Array(metadata["echo-test-key"]), ["test-value"], "\(pair)") + } + } + XCTAssertEqual(messagesReceived, 3) + + case .failure(let error): + throw error + } + } + } + } + + func testBidiStreamingEmptyOK() async throws { + try await self.forEachTransportPair { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { _ in } + try await control.bidiStream(request: request) { response in + switch response.accepted { + case .success(let contents): + var receivedTrailingMetadata = false + for try await part in contents.bodyParts { + switch part { + case .message: + XCTFail("Unexpected message \(pair)") + case .trailingMetadata: + XCTAssertFalse(receivedTrailingMetadata, "\(pair)") + receivedTrailingMetadata = true + } + } + case .failure(let error): + throw error + } + } + } + } + + func testBidiStreamingNotOK() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: metadata + ) { writer in + try await writer.write(.echoMetadata) + // Send a few messages, each is echo'd back. + try await writer.write(.messages(1, repeating: 42, count: 1024)) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + // Send the final status. + try await writer.write(.status(code: .aborted, message: "\(#function)", echoMetadata: true)) + } + + try await control.bidiStream(request: request) { response in + switch response.accepted { + case .success(let contents): + XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") + + var messagesReceived = 0 + do { + for try await part in contents.bodyParts { + switch part { + case .message(let message): + messagesReceived += 1 + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) + case .trailingMetadata: + XCTFail("Trailing metadata should be provided by error") + } + } + XCTFail("Should've thrown error \(pair)") + } catch let error as RPCError { + XCTAssertEqual(error.code, .aborted) + XCTAssertEqual(error.message, "\(#function)") + XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") + } + + XCTAssertEqual(messagesReceived, 3) + + case .failure(let error): + throw error + } + } + } + } + + func testBidiStreamingRejected() async throws { + try await self.forEachTransportPair { control, pair in + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: metadata + ) { writer in + try await writer.write( + .trailersOnly( + code: .aborted, + message: "\(#function)", + echoMetadata: true + ) + ) + } + + try await control.bidiStream(request: request) { response in + switch response.accepted { + case .success: + XCTFail("Expected RPC to fail \(pair)") + case .failure(let error): + XCTAssertEqual(error.code, .aborted) + XCTAssertEqual(error.message, "\(#function)") + XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"]) + } + } + } + } + + // MARK: - Not Implemented + + func testUnaryNotImplemented() async throws { + try await self.forEachTransportPair(enableControlService: false) { control, pair in + let request = ClientRequest.Single(message: ControlInput()) + try await control.unary(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + func testClientStreamingNotImplemented() async throws { + try await self.forEachTransportPair(enableControlService: false) { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { _ in } + try await control.clientStream(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + func testServerStreamingNotImplemented() async throws { + try await self.forEachTransportPair(enableControlService: false) { control, pair in + let request = ClientRequest.Single(message: ControlInput()) + try await control.serverStream(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.accepted.get()) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + func testBidiStreamingNotImplemented() async throws { + try await self.forEachTransportPair(enableControlService: false) { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { _ in } + try await control.bidiStream(request: request) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.accepted.get()) { error in + XCTAssertEqual(error.code, .unimplemented) + } + } + } + } + + // MARK: - Compression tests + + private func testUnaryCompression( + client: CompressionAlgorithm, + server: CompressionAlgorithm, + control: ControlClient, + pair: Transport + ) async throws { + let message = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + $0.messageParams = .with { + $0.content = 42 + $0.size = 1024 + } + } + + var options = CallOptions.defaults + options.compression = client + + try await control.unary( + request: ClientRequest.Single(message: message), + options: options + ) { response in + // Check the client algorithm. + switch client { + case .deflate, .gzip: + // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. + let encoding = Array(response.metadata["echo-grpc-encoding"]) + XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + // Check the server algorithm. + switch server { + case .deflate, .gzip: + let encoding = Array(response.metadata["grpc-encoding"]) + XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + let message = try response.message + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") + } + } + + private func testClientStreamingCompression( + client: CompressionAlgorithm, + server: CompressionAlgorithm, + control: ControlClient, + pair: Transport + ) async throws { + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + try await writer.write(.echoMetadata) + try await writer.write(.noOp) + try await writer.write(.noOp) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + } + + var options = CallOptions.defaults + options.compression = client + + try await control.clientStream(request: request, options: options) { response in + // Check the client algorithm. + switch client { + case .deflate, .gzip: + // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. + let encoding = Array(response.metadata["echo-grpc-encoding"]) + XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + // Check the server algorithm. + switch server { + case .deflate, .gzip: + let encoding = Array(response.metadata["grpc-encoding"]) + XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + let message = try response.message + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") + } + } + + private func testServerStreamingCompression( + client: CompressionAlgorithm, + server: CompressionAlgorithm, + control: ControlClient, + pair: Transport + ) async throws { + let message = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 5 + $0.messageParams = .with { + $0.content = 42 + $0.size = 1024 + } + } + + var options = CallOptions.defaults + options.compression = client + + try await control.serverStream( + request: ClientRequest.Single(message: message), + options: options + ) { response in + // Check the client algorithm. + switch client { + case .deflate, .gzip: + // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. + let encoding = Array(response.metadata["echo-grpc-encoding"]) + XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + // Check the server algorithm. + switch server { + case .deflate, .gzip: + let encoding = Array(response.metadata["grpc-encoding"]) + XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + for try await message in response.messages { + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") + } + } + } + + private func testBidiStreamingCompression( + client: CompressionAlgorithm, + server: CompressionAlgorithm, + control: ControlClient, + pair: Transport + ) async throws { + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + try await writer.write(.echoMetadata) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + try await writer.write(.messages(1, repeating: 42, count: 1024)) + } + + var options = CallOptions.defaults + options.compression = client + + try await control.bidiStream(request: request, options: options) { response in + // Check the client algorithm. + switch client { + case .deflate, .gzip: + // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. + let encoding = Array(response.metadata["echo-grpc-encoding"]) + XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + // Check the server algorithm. + switch server { + case .deflate, .gzip: + let encoding = Array(response.metadata["grpc-encoding"]) + XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") + case .none: + () + default: + XCTFail("Unhandled compression '\(client)'") + } + + for try await message in response.messages { + XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") + } + } + } + + func testUnaryDeflateCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .deflate, + clientEnabledCompression: .deflate, + serverCompression: .deflate + ) { control, pair in + try await self.testUnaryCompression( + client: .deflate, + server: .deflate, + control: control, + pair: pair + ) + } + } + + func testUnaryGzipCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .gzip, + clientEnabledCompression: .gzip, + serverCompression: .gzip + ) { control, pair in + try await self.testUnaryCompression( + client: .gzip, + server: .gzip, + control: control, + pair: pair + ) + } + } + + func testClientStreamingDeflateCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .deflate, + clientEnabledCompression: .deflate, + serverCompression: .deflate + ) { control, pair in + try await self.testClientStreamingCompression( + client: .deflate, + server: .deflate, + control: control, + pair: pair + ) + } + } + + func testClientStreamingGzipCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .gzip, + clientEnabledCompression: .gzip, + serverCompression: .gzip + ) { control, pair in + try await self.testClientStreamingCompression( + client: .gzip, + server: .gzip, + control: control, + pair: pair + ) + } + } + + func testServerStreamingDeflateCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .deflate, + clientEnabledCompression: .deflate, + serverCompression: .deflate + ) { control, pair in + try await self.testServerStreamingCompression( + client: .deflate, + server: .deflate, + control: control, + pair: pair + ) + } + } + + func testServerStreamingGzipCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .gzip, + clientEnabledCompression: .gzip, + serverCompression: .gzip + ) { control, pair in + try await self.testServerStreamingCompression( + client: .gzip, + server: .gzip, + control: control, + pair: pair + ) + } + } + + func testBidiStreamingDeflateCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .deflate, + clientEnabledCompression: .deflate, + serverCompression: .deflate + ) { control, pair in + try await self.testBidiStreamingCompression( + client: .deflate, + server: .deflate, + control: control, + pair: pair + ) + } + } + + func testBidiStreamingGzipCompression() async throws { + try await self.forEachTransportPair( + clientCompression: .gzip, + clientEnabledCompression: .gzip, + serverCompression: .gzip + ) { control, pair in + try await self.testBidiStreamingCompression( + client: .gzip, + server: .gzip, + control: control, + pair: pair + ) + } + } + + func testUnaryUnsupportedCompression() async throws { + try await self.forEachTransportPair( + clientEnabledCompression: .all, + serverCompression: .gzip + ) { control, pair in + let message = ControlInput.with { + $0.numberOfMessages = 1 + $0.messageParams = .with { + $0.content = 42 + $0.size = 1024 + } + } + let request = ClientRequest.Single(message: message) + + var options = CallOptions.defaults + options.compression = .deflate + try await control.unary(request: request, options: options) { response in + switch response.accepted { + case .success: + XCTFail("RPC should've been rejected") + case .failure(let error): + let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) + // "identity" may or may not be included, so only test for values which must be present. + XCTAssertTrue(acceptEncoding.contains("gzip")) + XCTAssertFalse(acceptEncoding.contains("deflate")) + } + } + } + } + + func testClientStreamingUnsupportedCompression() async throws { + try await self.forEachTransportPair( + clientEnabledCompression: .all, + serverCompression: .gzip + ) { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + try await writer.write(.noOp) + } + + var options = CallOptions.defaults + options.compression = .deflate + try await control.clientStream(request: request, options: options) { response in + switch response.accepted { + case .success: + XCTFail("RPC should've been rejected") + case .failure(let error): + let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) + // "identity" may or may not be included, so only test for values which must be present. + XCTAssertTrue(acceptEncoding.contains("gzip")) + XCTAssertFalse(acceptEncoding.contains("deflate")) + } + } + } + } + + func testServerStreamingUnsupportedCompression() async throws { + try await self.forEachTransportPair( + clientEnabledCompression: .all, + serverCompression: .gzip + ) { control, pair in + let message = ControlInput.with { + $0.numberOfMessages = 1 + $0.messageParams = .with { + $0.content = 42 + $0.size = 1024 + } + } + let request = ClientRequest.Single(message: message) + + var options = CallOptions.defaults + options.compression = .deflate + try await control.serverStream(request: request, options: options) { response in + switch response.accepted { + case .success: + XCTFail("RPC should've been rejected") + case .failure(let error): + let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) + // "identity" may or may not be included, so only test for values which must be present. + XCTAssertTrue(acceptEncoding.contains("gzip")) + XCTAssertFalse(acceptEncoding.contains("deflate")) + } + } + } + } + + func testBidiStreamingUnsupportedCompression() async throws { + try await self.forEachTransportPair( + clientEnabledCompression: .all, + serverCompression: .gzip + ) { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + try await writer.write(.noOp) + } + + var options = CallOptions.defaults + options.compression = .deflate + try await control.bidiStream(request: request, options: options) { response in + switch response.accepted { + case .success: + XCTFail("RPC should've been rejected") + case .failure(let error): + let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) + // "identity" may or may not be included, so only test for values which must be present. + XCTAssertTrue(acceptEncoding.contains("gzip")) + XCTAssertFalse(acceptEncoding.contains("deflate")) + } + } + } + } + + func testUnaryTimeoutPropagatedToServer() async throws { + try await self.forEachTransportPair { control, pair in + let message = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + } + + let request = ClientRequest.Single(message: message) + var options = CallOptions.defaults + options.timeout = .seconds(10) + try await control.unary(request: request, options: options) { response in + let timeout = Array(response.metadata["echo-grpc-timeout"]) + XCTAssertEqual(timeout.count, 1) + } + } + } + + func testClientStreamingTimeoutPropagatedToServer() async throws { + try await self.forEachTransportPair { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + let message = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + } + try await writer.write(message) + } + + var options = CallOptions.defaults + options.timeout = .seconds(10) + try await control.clientStream(request: request, options: options) { response in + let timeout = Array(response.metadata["echo-grpc-timeout"]) + XCTAssertEqual(timeout.count, 1) + } + } + } + + func testServerStreamingTimeoutPropagatedToServer() async throws { + try await self.forEachTransportPair { control, pair in + let message = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + } + + let request = ClientRequest.Single(message: message) + var options = CallOptions.defaults + options.timeout = .seconds(10) + try await control.serverStream(request: request, options: options) { response in + let timeout = Array(response.metadata["echo-grpc-timeout"]) + XCTAssertEqual(timeout.count, 1) + } + } + } + + func testBidiStreamingTimeoutPropagatedToServer() async throws { + try await self.forEachTransportPair { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + try await writer.write(.echoMetadata) + } + + var options = CallOptions.defaults + options.timeout = .seconds(10) + try await control.bidiStream(request: request, options: options) { response in + let timeout = Array(response.metadata["echo-grpc-timeout"]) + XCTAssertEqual(timeout.count, 1) + } + } + } + + private static let httpToStatusCodePairs: [(Int, RPCError.Code)] = [ + // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + (400, .internalError), + (401, .unauthenticated), + (403, .permissionDenied), + (404, .unimplemented), + (418, .unknown), + (429, .unavailable), + (502, .unavailable), + (503, .unavailable), + (504, .unavailable), + (504, .unavailable), + ] + + func testUnaryAgainstNonGRPCServer() async throws { + try await self.forEachClientAndHTTPStatusCodeServer { control, kind in + for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { + // Tell the server what to respond with. + let metadata: Metadata = ["response-status": "\(httpCode)"] + + try await control.unary( + request: ClientRequest.Single(message: .noOp, metadata: metadata) + ) { response in + switch response.accepted { + case .success: + XCTFail("RPC should have failed with '\(expectedStatus)'") + case .failure(let error): + XCTAssertEqual(error.code, expectedStatus) + } + } + } + } + } + + func testClientStreamingAgainstNonGRPCServer() async throws { + try await self.forEachClientAndHTTPStatusCodeServer { control, kind in + for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { + // Tell the server what to respond with. + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: ["response-status": "\(httpCode)"] + ) { _ in + } + + try await control.clientStream(request: request) { response in + switch response.accepted { + case .success: + XCTFail("RPC should have failed with '\(expectedStatus)'") + case .failure(let error): + XCTAssertEqual(error.code, expectedStatus) + } + } + } + } + } + + func testServerStreamingAgainstNonGRPCServer() async throws { + try await self.forEachClientAndHTTPStatusCodeServer { control, kind in + for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { + // Tell the server what to respond with. + let metadata: Metadata = ["response-status": "\(httpCode)"] + + try await control.serverStream( + request: ClientRequest.Single(message: .noOp, metadata: metadata) + ) { response in + switch response.accepted { + case .success: + XCTFail("RPC should have failed with '\(expectedStatus)'") + case .failure(let error): + XCTAssertEqual(error.code, expectedStatus) + } + } + } + } + } + + func testBidiStreamingAgainstNonGRPCServer() async throws { + try await self.forEachClientAndHTTPStatusCodeServer { control, kind in + for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { + // Tell the server what to respond with. + let request = ClientRequest.Stream( + of: ControlInput.self, + metadata: ["response-status": "\(httpCode)"] + ) { _ in + } + + try await control.bidiStream(request: request) { response in + switch response.accepted { + case .success: + XCTFail("RPC should have failed with '\(expectedStatus)'") + case .failure(let error): + XCTAssertEqual(error.code, expectedStatus) + } + } + } + } + } +} + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension [HTTP2TransportTests.Transport] { + static let supported = [ + HTTP2TransportTests.Transport(server: .posix, client: .posix), + HTTP2TransportTests.Transport(server: .niots, client: .posix), + ] +} + +extension ControlInput { + fileprivate static let echoMetadata = Self.with { + $0.echoMetadataInHeaders = true + } + + fileprivate static let noOp = Self() + + fileprivate static func messages( + _ numberOfMessages: Int, + repeating: UInt8, + count: Int + ) -> Self { + return Self.with { + $0.numberOfMessages = Int32(numberOfMessages) + $0.messageParams = .with { + $0.content = UInt32(repeating) + $0.size = Int32(count) + } + } + } + + fileprivate static func status( + code: Status.Code, + message: String, + echoMetadata: Bool + ) -> Self { + return Self.with { + $0.echoMetadataInTrailers = echoMetadata + $0.status = .with { + $0.code = StatusCode(rawValue: code.rawValue)! + $0.message = message + } + } + } + + fileprivate static func trailersOnly( + code: Status.Code, + message: String, + echoMetadata: Bool + ) -> Self { + return Self.with { + $0.echoMetadataInTrailers = echoMetadata + $0.isTrailersOnly = true + $0.status = .with { + $0.code = StatusCode(rawValue: code.rawValue)! + $0.message = message + } + } + } +} + +extension CompressionAlgorithm { + var name: String { + switch self { + case .deflate: + return "deflate" + case .gzip: + return "gzip" + case .none: + return "identity" + default: + return "" + } + } +} diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift new file mode 100644 index 000000000..093fffafc --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift @@ -0,0 +1,112 @@ +/* + * 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 GRPCHTTP2Core +import NIOCore +import NIOHPACK +import NIOHTTP2 +import NIOPosix + +/// An HTTP/2 test server which only responds to request headers by sending response headers and +/// then closing. Each stream will be closed with the ":status" set to the value of the +/// "response-status" header field in the request headers. +final class HTTP2StatusCodeServer { + private let address: EventLoopPromise + private let eventLoopGroup: MultiThreadedEventLoopGroup + + var listeningAddress: GRPCHTTP2Core.SocketAddress.IPv4 { + get async throws { + try await self.address.futureResult.get() + } + } + + init() { + self.eventLoopGroup = .singleton + self.address = self.eventLoopGroup.next().makePromise() + } + + func run() async throws { + do { + let channel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup) + .bind(host: "127.0.0.1", port: 0) { channel in + channel.eventLoop.makeCompletedFuture { + let sync = channel.pipeline.syncOperations + let multiplexer = try sync.configureAsyncHTTP2Pipeline(mode: .server) { stream in + stream.eventLoop.makeCompletedFuture { + try NIOAsyncChannel( + wrappingChannelSynchronously: stream + ) + } + } + + let wrapped = try NIOAsyncChannel( + wrappingChannelSynchronously: channel + ) + + return (wrapped, multiplexer) + } + } + + let port = channel.channel.localAddress!.port! + self.address.succeed(.init(host: "127.0.0.1", port: port)) + + try await channel.executeThenClose { inbound in + try await withThrowingTaskGroup(of: Void.self) { acceptedGroup in + for try await (accepted, mux) in inbound { + acceptedGroup.addTask { + try await withThrowingTaskGroup(of: Void.self) { connectionGroup in + // Run the connection. + connectionGroup.addTask { + try await accepted.executeThenClose { inbound, outbound in + for try await _ in inbound {} + } + } + + // Consume the streams. + for try await stream in mux.inbound { + connectionGroup.addTask { + try await stream.executeThenClose { inbound, outbound in + do { + for try await frame in inbound { + switch frame { + case .headers(let requestHeaders): + if let status = requestHeaders.headers.first(name: "response-status") { + let headers: HPACKHeaders = [":status": "\(status)"] + try await outbound.write( + .headers(.init(headers: headers, endStream: true)) + ) + } + + default: + () // Ignore the others + } + } + } catch { + // Ignore errors + } + } + } + } + } + } + } + } + } + } catch { + self.address.fail(error) + } + } +} diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift new file mode 100644 index 000000000..4cef512d0 --- /dev/null +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift @@ -0,0 +1,30 @@ +/* + * 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 + +func XCTAssertThrowsError( + ofType: E.Type, + _ expression: @autoclosure () throws -> T, + _ errorHandler: (E) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? E else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + errorHandler(error) + } +} From 80fdaaef1cfb703bd043c9e2f1d05f1dc8474d6e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 26 Jun 2024 08:48:58 +0100 Subject: [PATCH 374/580] Add missing availability (#1952) --- .../Test Utilities/HTTP2StatusCodeServer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift index 093fffafc..867585d09 100644 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift @@ -23,6 +23,7 @@ import NIOPosix /// An HTTP/2 test server which only responds to request headers by sending response headers and /// then closing. Each stream will be closed with the ":status" set to the value of the /// "response-status" header field in the request headers. +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) final class HTTP2StatusCodeServer { private let address: EventLoopPromise private let eventLoopGroup: MultiThreadedEventLoopGroup From 1cf1053cc9ea9e2e1cba73aaac028fc20b5985e7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 2 Jul 2024 12:50:24 +0100 Subject: [PATCH 375/580] Add integ test for SwiftPM plugin (#1956) Motivation: There's no integration test for the SwiftPM plugn. Add a basic test which generates and builds the package using the plugin. Modifications: - Add a script to generate a package using the plugin and build it - Update CI Result: SwiftPM plugin has basic test --- .github/workflows/ci.yaml | 9 +++ scripts/run-plugin-tests.sh | 112 ++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100755 scripts/run-plugin-tests.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 04c501c47..fe8c02f57 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -138,16 +138,25 @@ jobs: matrix: include: - image: swiftlang/swift:nightly-jammy + swift-tools-version: '6.0' - image: swiftlang/swift:nightly-6.0-jammy + swift-tools-version: '6.0' - image: swift:5.10.1-noble + swift-tools-version: '5.10' - image: swift:5.9-jammy + swift-tools-version: '5.9' - image: swift:5.8-focal + swift-tools-version: '5.8' name: Integration Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: image: ${{ matrix.image }} steps: - uses: actions/checkout@v4 + - name: Install protoc + run: apt update && apt install -y protobuf-compiler + - name: SwiftPM plugin test + run: ./scripts/run-plugin-tests.sh ${{ matrix.swift-tools-version }} - name: Build without NIOSSL run: swift build env: diff --git a/scripts/run-plugin-tests.sh b/scripts/run-plugin-tests.sh new file mode 100755 index 000000000..a0af91597 --- /dev/null +++ b/scripts/run-plugin-tests.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# 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. + +set -eux + +HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +GRPC_PATH="${HERE}/.." + +function generate_package_manifest { + local version=$1 + local grpc_path=$2 + + echo "// swift-tools-version: $version" + echo "import PackageDescription" + echo "" + echo "let package = Package(" + echo " name: \"Foo\"," + echo " dependencies: [" + echo " .package(path: \"$grpc_path\")," + echo " .package(url: \"https://github.com/apple/swift-protobuf\", from: \"1.26.0\")" + echo " ]," + echo " targets: [" + echo " .executableTarget(" + echo " name: \"Foo\"," + echo " dependencies: [" + echo " .product(name: \"GRPC\", package: \"grpc-swift\")," + echo " ]," + echo " path: \"Sources/Foo\"," + echo " plugins: [" + echo " .plugin(name: \"GRPCSwiftPlugin\", package: \"grpc-swift\")," + echo " .plugin(name: \"SwiftProtobufPlugin\", package: \"swift-protobuf\")," + echo " ]" + echo " )," + echo " ]" + echo ")" +} + +function generate_grpc_plugin_config { + cat < "$dir/Package.swift" + generate_grpc_plugin_config > "$dir/Sources/Foo/grpc-swift-config.json" + generate_protobuf_plugin_config > "$dir/Sources/Foo/swift-protobuf-config.json" + generate_proto > "$dir/Sources/Foo/Foo.proto" + generate_main > "$dir/Sources/Foo/main.swift" + + PROTOC_PATH=$protoc_path swift build --package-path "$dir" +} + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 SWIFT_TOOLS_VERSION" + exit 1 +fi + +generate_and_build "$1" "${GRPC_PATH}" From 36ffcb49fdc9737a5ad1058dd152fadbedddf805 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 2 Jul 2024 14:08:17 +0100 Subject: [PATCH 376/580] Swift 6-ify the package manifest (#1955) Motivation: v2 will require the Swift 6 language mode. In order to continue developing v1 and v2 on main we'll need a Swift 6 specific manifest version in addition to the 5.x version. Moving forward we'll have two package manifests, `Package.swift` for 5.x containing only v1, and `Package@swift-6.swift` using tools version 6.0 targetting v1 and v2. Modifications: - Remove v2 targets from Package.swift - Add a second manifest and make it warning free with Swift 6, this requires turning `static let`s to `static var`s. - Conditionalise some imports in `protoc-gen-grpc-swift` (as it is shared by v1 and v2) - Fix an error in the plugin for Swift 6 Result: v2 now only compiles with a Swift 6 compiler --- .github/workflows/ci.yaml | 6 +- Package.swift | 255 ----- Package@swift-6.swift | 927 ++++++++++++++++++ ...wiftBenchmark.Metadata_Add_binary.p90.json | 7 - ...wiftBenchmark.Metadata_Add_string.p90.json | 7 - ...hmark.Metadata_Iterate_all_values.p90.json | 7 - ...es_when_only_binary_values_stored.p90.json | 7 - ...y_values_when_only_strings_stored.p90.json | 7 - ...rk.Metadata_Iterate_string_values.p90.json | 7 - ...rk.Metadata_Remove_values_for_key.p90.json | 7 - ...wiftBenchmark.Metadata_Add_binary.p90.json | 7 - ...wiftBenchmark.Metadata_Add_string.p90.json | 7 - ...hmark.Metadata_Iterate_all_values.p90.json | 7 - ...es_when_only_binary_values_stored.p90.json | 7 - ...y_values_when_only_strings_stored.p90.json | 7 - ...rk.Metadata_Iterate_string_values.p90.json | 7 - ...rk.Metadata_Remove_values_for_key.p90.json | 7 - ...wiftBenchmark.Metadata_Add_binary.p90.json | 0 ...wiftBenchmark.Metadata_Add_string.p90.json | 0 ...hmark.Metadata_Iterate_all_values.p90.json | 0 ...es_when_only_binary_values_stored.p90.json | 0 ...y_values_when_only_strings_stored.p90.json | 0 ...rk.Metadata_Iterate_string_values.p90.json | 0 ...rk.Metadata_Remove_values_for_key.p90.json | 0 Plugins/GRPCSwiftPlugin/plugin.swift | 6 +- Sources/protoc-gen-grpc-swift/main.swift | 15 +- Sources/protoc-gen-grpc-swift/options.swift | 4 + scripts/license-check.sh | 5 + 28 files changed, 956 insertions(+), 360 deletions(-) create mode 100644 Package@swift-6.swift delete mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json delete mode 100644 Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename Performance/Benchmarks/Thresholds/{5.10 => 6.0}/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json (100%) rename Performance/Benchmarks/Thresholds/{5.10 => 6.0}/GRPCSwiftBenchmark.Metadata_Add_string.p90.json (100%) rename Performance/Benchmarks/Thresholds/{5.10 => 6.0}/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json (100%) rename Performance/Benchmarks/Thresholds/{5.10 => 6.0}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json (100%) rename Performance/Benchmarks/Thresholds/{5.10 => 6.0}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json (100%) rename Performance/Benchmarks/Thresholds/{5.10 => 6.0}/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json (100%) rename Performance/Benchmarks/Thresholds/{5.10 => 6.0}/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe8c02f57..6e05d6bac 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -61,7 +61,7 @@ jobs: matrix: include: - image: swiftlang/swift:nightly-jammy - swift-version: main + swift-version: 'main' env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -72,7 +72,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swiftlang/swift:nightly-6.0-jammy - swift-version: main + swift-version: '6.0' env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 @@ -126,9 +126,11 @@ jobs: env: ${{ matrix.env }} timeout-minutes: 20 - name: Install jemalloc for benchmarking + if: ${{ matrix.swift-version == '6.0' || matrix.swift-version == 'main' }} run: apt update && apt-get install -y libjemalloc-dev timeout-minutes: 20 - name: Run Benchmarks + if: ${{ matrix.swift-version == '6.0' || matrix.swift-version == 'main' }} working-directory: ./Performance/Benchmarks run: swift package benchmark baseline check --no-progress --check-absolute-path Thresholds/${{ matrix.swift-version }}/ timeout-minutes: 20 diff --git a/Package.swift b/Package.swift index 831db2548..f3e2adbbe 100644 --- a/Package.swift +++ b/Package.swift @@ -50,10 +50,6 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-collections.git", from: "1.0.5" ), - .package( - url: "https://github.com/apple/swift-atomics.git", - from: "1.2.0" - ), .package( url: "https://github.com/apple/swift-protobuf.git", from: "1.20.2" @@ -68,14 +64,6 @@ let packageDependencies: [Package.Dependency] = [ // version and indluded async support. from: "1.1.1" ), - .package( - url: "https://github.com/apple/swift-docc-plugin", - from: "1.0.0" - ), - .package( - url: "https://github.com/apple/swift-distributed-tracing.git", - from: "1.0.0" - ), ].appending( .package( url: "https://github.com/apple/swift-nio-ssl.git", @@ -91,11 +79,7 @@ extension Target.Dependency { static let grpc: Self = .target(name: grpcTargetName) static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") - static let performanceWorker: Self = .target(name: "performance-worker") static let reflectionService: Self = .target(name: "GRPCReflectionService") - static let grpcCodeGen: Self = .target(name: "GRPCCodeGen") - static let grpcProtobuf: Self = .target(name: "GRPCProtobuf") - static let grpcProtobufCodeGen: Self = .target(name: "GRPCProtobufCodeGen") // Target dependencies; internal static let grpcSampleData: Self = .target(name: "GRPCSampleData") @@ -140,15 +124,6 @@ extension Target.Dependency { package: "swift-protobuf" ) static let dequeModule: Self = .product(name: "DequeModule", package: "swift-collections") - static let atomics: Self = .product(name: "Atomics", package: "swift-atomics") - static let tracing: Self = .product(name: "Tracing", package: "swift-distributed-tracing") - - static let grpcCore: Self = .target(name: "GRPCCore") - static let grpcInProcessTransport: Self = .target(name: "GRPCInProcessTransport") - static let grpcInterceptors: Self = .target(name: "GRPCInterceptors") - static let grpcHTTP2Core: Self = .target(name: "GRPCHTTP2Core") - static let grpcHTTP2TransportNIOPosix: Self = .target(name: "GRPCHTTP2TransportNIOPosix") - static let grpcHTTP2TransportNIOTransportServices: Self = .target(name: "GRPCHTTP2TransportNIOTransportServices") } // MARK: - Targets @@ -177,63 +152,6 @@ extension Target { path: "Sources/GRPC" ) - static let grpcCore: Target = .target( - name: "GRPCCore", - dependencies: [ - .dequeModule, - .atomics - ], - path: "Sources/GRPCCore" - ) - - static let grpcInProcessTransport: Target = .target( - name: "GRPCInProcessTransport", - dependencies: [ - .grpcCore - ] - ) - - static let grpcInterceptors: Target = .target( - name: "GRPCInterceptors", - dependencies: [ - .grpcCore, - .tracing - ] - ) - - static let grpcHTTP2Core: Target = .target( - name: "GRPCHTTP2Core", - dependencies: [ - .grpcCore, - .nioCore, - .nioHTTP2, - .cgrpcZlib, - .dequeModule, - .atomics - ] - ) - - static let grpcHTTP2TransportNIOPosix: Target = .target( - name: "GRPCHTTP2TransportNIOPosix", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .nioPosix, - .nioExtras - ] - ) - - static let grpcHTTP2TransportNIOTransportServices: Target = .target( - name: "GRPCHTTP2TransportNIOTransportServices", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .nioCore, - .nioExtras, - .nioTransportServices - ] - ) - static let cgrpcZlib: Target = .target( name: cgrpcZlibTargetName, path: "Sources/CGRPCZlib", @@ -247,27 +165,12 @@ extension Target { dependencies: [ .protobuf, .protobufPluginLibrary, - .grpcCodeGen, - .grpcProtobufCodeGen ], exclude: [ "README.md", ] ) - static let performanceWorker: Target = .executableTarget( - name: "performance-worker", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .grpcProtobuf, - .nioCore, - .nioFileSystem, - .argumentParser - ] - ) - static let grpcSwiftPlugin: Target = .plugin( name: "GRPCSwiftPlugin", capability: .buildTool(), @@ -304,91 +207,6 @@ extension Target { ] ) - static let grpcCoreTests: Target = .testTarget( - name: "GRPCCoreTests", - dependencies: [ - .grpcCore, - .grpcInProcessTransport, - .dequeModule, - .atomics, - .protobuf, - ] - ) - - static let grpcInProcessTransportTests: Target = .testTarget( - name: "GRPCInProcessTransportTests", - dependencies: [ - .grpcCore, - .grpcInProcessTransport - ] - ) - - static let grpcInterceptorsTests: Target = .testTarget( - name: "GRPCInterceptorsTests", - dependencies: [ - .grpcCore, - .tracing, - .nioCore, - .grpcInterceptors - ] - ) - - static let grpcHTTP2CoreTests: Target = .testTarget( - name: "GRPCHTTP2CoreTests", - dependencies: [ - .grpcHTTP2Core, - .nioCore, - .nioHTTP2, - .nioEmbedded, - .nioTestUtils, - ] - ) - - static let grpcHTTP2TransportTests: Target = .testTarget( - name: "GRPCHTTP2TransportTests", - dependencies: [ - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .grpcHTTP2TransportNIOTransportServices, - .grpcProtobuf - ] - ) - - static let grpcCodeGenTests: Target = .testTarget( - name: "GRPCCodeGenTests", - dependencies: [ - .grpcCodeGen - ] - ) - - static let grpcProtobufTests: Target = .testTarget( - name: "GRPCProtobufTests", - dependencies: [ - .grpcProtobuf, - .grpcCore, - .protobuf - ] - ) - - static let grpcProtobufCodeGenTests: Target = .testTarget( - name: "GRPCProtobufCodeGenTests", - dependencies: [ - .grpcCodeGen, - .grpcProtobufCodeGen, - .protobuf, - .protobufPluginLibrary - ] - ) - - static let inProcessInteroperabilityTests: Target = .testTarget( - name: "InProcessInteroperabilityTests", - dependencies: [ - .grpcInProcessTransport, - .interoperabilityTests, - .grpcCore - ] - ) - static let interopTestModels: Target = .target( name: "GRPCInteroperabilityTestModels", dependencies: [ @@ -407,25 +225,6 @@ extension Target { ] ) - static let interoperabilityTestImplementation: Target = .target( - name: "InteroperabilityTests", - dependencies: [ - .grpcCore, - .grpcProtobuf - ] - ) - - static let interoperabilityTestsExecutable: Target = .executableTarget( - name: "interoperability-tests", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .interoperabilityTests, - .argumentParser - ] - ) - static let interopTestImplementation: Target = .target( name: "GRPCInteroperabilityTestsImplementation", dependencies: [ @@ -639,29 +438,6 @@ extension Target { .copy("Generated") ] ) - - static let grpcCodeGen: Target = .target( - name: "GRPCCodeGen", - path: "Sources/GRPCCodeGen" - ) - - static let grpcProtobuf: Target = .target( - name: "GRPCProtobuf", - dependencies: [ - .grpcCore, - .protobuf, - ], - path: "Sources/GRPCProtobuf" - ) - static let grpcProtobufCodeGen: Target = .target( - name: "GRPCProtobufCodeGen", - dependencies: [ - .protobuf, - .protobufPluginLibrary, - .grpcCodeGen - ], - path: "Sources/GRPCProtobufCodeGen" - ) } // MARK: - Products @@ -672,11 +448,6 @@ extension Product { targets: [grpcTargetName] ) - static let grpcCore: Product = .library( - name: "_GRPCCore", - targets: ["GRPCCore"] - ) - static let cgrpcZlib: Product = .library( name: cgrpcZlibProductName, targets: [cgrpcZlibTargetName] @@ -704,7 +475,6 @@ let package = Package( name: grpcPackageName, products: [ .grpc, - .grpcCore, .cgrpcZlib, .grpcReflectionService, .protocGenGRPCSwift, @@ -740,31 +510,6 @@ let package = Package( .routeGuideServer, .packetCapture, .reflectionServer, - - // v2 - .grpcCore, - .grpcInProcessTransport, - .grpcCodeGen, - .grpcInterceptors, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .grpcHTTP2TransportNIOTransportServices, - .grpcProtobuf, - .grpcProtobufCodeGen, - .interoperabilityTestImplementation, - .interoperabilityTestsExecutable, - .performanceWorker, - - // v2 tests - .grpcCoreTests, - .grpcInProcessTransportTests, - .grpcCodeGenTests, - .grpcInterceptorsTests, - .grpcHTTP2CoreTests, - .grpcHTTP2TransportTests, - .grpcProtobufTests, - .grpcProtobufCodeGenTests, - .inProcessInteroperabilityTests ] ) diff --git a/Package@swift-6.swift b/Package@swift-6.swift new file mode 100644 index 000000000..c89ca9b08 --- /dev/null +++ b/Package@swift-6.swift @@ -0,0 +1,927 @@ +// swift-tools-version:6.0 +/* + * 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 PackageDescription +// swiftformat puts the next import before the tools version. +// swiftformat:disable:next sortImports +import class Foundation.ProcessInfo + +let grpcPackageName = "grpc-swift" +let grpcProductName = "GRPC" +let cgrpcZlibProductName = "CGRPCZlib" +let grpcTargetName = grpcProductName +let cgrpcZlibTargetName = cgrpcZlibProductName + +let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil + +// MARK: - Package Dependencies + +let packageDependencies: [Package.Dependency] = [ + .package( + url: "https://github.com/apple/swift-nio.git", + from: "2.65.0" + ), + .package( + url: "https://github.com/apple/swift-nio-http2.git", + from: "1.32.0" + ), + .package( + url: "https://github.com/apple/swift-nio-transport-services.git", + from: "1.15.0" + ), + .package( + url: "https://github.com/apple/swift-nio-extras.git", + from: "1.4.0" + ), + .package( + url: "https://github.com/apple/swift-collections.git", + from: "1.0.5" + ), + .package( + url: "https://github.com/apple/swift-atomics.git", + from: "1.2.0" + ), + .package( + url: "https://github.com/apple/swift-protobuf.git", + from: "1.20.2" + ), + .package( + url: "https://github.com/apple/swift-log.git", + from: "1.4.4" + ), + .package( + url: "https://github.com/apple/swift-argument-parser.git", + // Version is higher than in other Package@swift manifests: 1.1.0 raised the minimum Swift + // version and indluded async support. + from: "1.1.1" + ), + .package( + url: "https://github.com/apple/swift-distributed-tracing.git", + from: "1.0.0" + ), +].appending( + .package( + url: "https://github.com/apple/swift-nio-ssl.git", + from: "2.23.0" + ), + if: includeNIOSSL +) + +// MARK: - Target Dependencies + +extension Target.Dependency { + // Target dependencies; external + static var grpc: Self { .target(name: grpcTargetName) } + static var cgrpcZlib: Self { .target(name: cgrpcZlibTargetName) } + static var protocGenGRPCSwift: Self { .target(name: "protoc-gen-grpc-swift") } + static var performanceWorker: Self { .target(name: "performance-worker") } + static var reflectionService: Self { .target(name: "GRPCReflectionService") } + static var grpcCodeGen: Self { .target(name: "GRPCCodeGen") } + static var grpcProtobuf: Self { .target(name: "GRPCProtobuf") } + static var grpcProtobufCodeGen: Self { .target(name: "GRPCProtobufCodeGen") } + + // Target dependencies; internal + static var grpcSampleData: Self { .target(name: "GRPCSampleData") } + static var echoModel: Self { .target(name: "EchoModel") } + static var echoImplementation: Self { .target(name: "EchoImplementation") } + static var helloWorldModel: Self { .target(name: "HelloWorldModel") } + static var routeGuideModel: Self { .target(name: "RouteGuideModel") } + static var interopTestModels: Self { .target(name: "GRPCInteroperabilityTestModels") } + static var interopTestImplementation: Self { + .target(name: "GRPCInteroperabilityTestsImplementation") + } + static var interoperabilityTests: Self { .target(name: "InteroperabilityTests") } + + // Product dependencies + static var argumentParser: Self { + .product( + name: "ArgumentParser", + package: "swift-argument-parser" + ) + } + static var nio: Self { .product(name: "NIO", package: "swift-nio") } + static var nioConcurrencyHelpers: Self { + .product( + name: "NIOConcurrencyHelpers", + package: "swift-nio" + ) + } + static var nioCore: Self { .product(name: "NIOCore", package: "swift-nio") } + static var nioEmbedded: Self { .product(name: "NIOEmbedded", package: "swift-nio") } + static var nioExtras: Self { .product(name: "NIOExtras", package: "swift-nio-extras") } + static var nioFoundationCompat: Self { .product(name: "NIOFoundationCompat", package: "swift-nio") } + static var nioHTTP1: Self { .product(name: "NIOHTTP1", package: "swift-nio") } + static var nioHTTP2: Self { .product(name: "NIOHTTP2", package: "swift-nio-http2") } + static var nioPosix: Self { .product(name: "NIOPosix", package: "swift-nio") } + static var nioSSL: Self { .product(name: "NIOSSL", package: "swift-nio-ssl") } + static var nioTLS: Self { .product(name: "NIOTLS", package: "swift-nio") } + static var nioTransportServices: Self { + .product( + name: "NIOTransportServices", + package: "swift-nio-transport-services" + ) + } + static var nioTestUtils: Self { .product(name: "NIOTestUtils", package: "swift-nio") } + static var nioFileSystem: Self { .product(name: "_NIOFileSystem", package: "swift-nio") } + static var logging: Self { .product(name: "Logging", package: "swift-log") } + static var protobuf: Self { .product(name: "SwiftProtobuf", package: "swift-protobuf") } + static var protobufPluginLibrary: Self { + .product( + name: "SwiftProtobufPluginLibrary", + package: "swift-protobuf" + ) + } + static var dequeModule: Self { .product(name: "DequeModule", package: "swift-collections") } + static var atomics: Self { .product(name: "Atomics", package: "swift-atomics") } + static var tracing: Self { .product(name: "Tracing", package: "swift-distributed-tracing") } + + static var grpcCore: Self { .target(name: "GRPCCore") } + static var grpcInProcessTransport: Self { .target(name: "GRPCInProcessTransport") } + static var grpcInterceptors: Self { .target(name: "GRPCInterceptors") } + static var grpcHTTP2Core: Self { .target(name: "GRPCHTTP2Core") } + static var grpcHTTP2TransportNIOPosix: Self { .target(name: "GRPCHTTP2TransportNIOPosix") } + static var grpcHTTP2TransportNIOTransportServices: Self { .target(name: "GRPCHTTP2TransportNIOTransportServices") } +} + +// MARK: - Targets + +extension Target { + static var grpc: Target { + .target( + name: grpcTargetName, + dependencies: [ + .cgrpcZlib, + .nio, + .nioCore, + .nioPosix, + .nioEmbedded, + .nioFoundationCompat, + .nioTLS, + .nioTransportServices, + .nioHTTP1, + .nioHTTP2, + .nioExtras, + .logging, + .protobuf, + .dequeModule, + ].appending( + .nioSSL, if: includeNIOSSL + ), + path: "Sources/GRPC", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcCore: Target { + .target( + name: "GRPCCore", + dependencies: [ + .dequeModule, + .atomics + ], + path: "Sources/GRPCCore", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcInProcessTransport: Target { + .target( + name: "GRPCInProcessTransport", + dependencies: [ + .grpcCore + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcInterceptors: Target { + .target( + name: "GRPCInterceptors", + dependencies: [ + .grpcCore, + .tracing + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcHTTP2Core: Target { + .target( + name: "GRPCHTTP2Core", + dependencies: [ + .grpcCore, + .nioCore, + .nioHTTP2, + .cgrpcZlib, + .dequeModule, + .atomics + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcHTTP2TransportNIOPosix: Target { + .target( + name: "GRPCHTTP2TransportNIOPosix", + dependencies: [ + .grpcCore, + .grpcHTTP2Core, + .nioPosix, + .nioExtras + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcHTTP2TransportNIOTransportServices: Target { + .target( + name: "GRPCHTTP2TransportNIOTransportServices", + dependencies: [ + .grpcCore, + .grpcHTTP2Core, + .nioCore, + .nioExtras, + .nioTransportServices + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var cgrpcZlib: Target { + .target( + name: cgrpcZlibTargetName, + path: "Sources/CGRPCZlib", + linkerSettings: [ + .linkedLibrary("z"), + ] + ) + } + + static var protocGenGRPCSwift: Target { + .executableTarget( + name: "protoc-gen-grpc-swift", + dependencies: [ + .protobuf, + .protobufPluginLibrary, + .grpcCodeGen, + .grpcProtobufCodeGen + ], + exclude: [ + "README.md", + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var performanceWorker: Target { + .executableTarget( + name: "performance-worker", + dependencies: [ + .grpcCore, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .grpcProtobuf, + .nioCore, + .nioFileSystem, + .argumentParser + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcSwiftPlugin: Target { + .plugin( + name: "GRPCSwiftPlugin", + capability: .buildTool(), + dependencies: [ + .protocGenGRPCSwift, + ] + ) + } + + static var grpcTests: Target { + .testTarget( + name: "GRPCTests", + dependencies: [ + .grpc, + .echoModel, + .echoImplementation, + .helloWorldModel, + .interopTestModels, + .interopTestImplementation, + .grpcSampleData, + .nioCore, + .nioConcurrencyHelpers, + .nioPosix, + .nioTLS, + .nioHTTP1, + .nioHTTP2, + .nioEmbedded, + .nioTransportServices, + .logging, + .reflectionService + ].appending( + .nioSSL, if: includeNIOSSL + ), + exclude: [ + "Codegen/Serialization/echo.grpc.reflection" + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcCoreTests: Target { + .testTarget( + name: "GRPCCoreTests", + dependencies: [ + .grpcCore, + .grpcInProcessTransport, + .dequeModule, + .atomics, + .protobuf, + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcInProcessTransportTests: Target { + .testTarget( + name: "GRPCInProcessTransportTests", + dependencies: [ + .grpcCore, + .grpcInProcessTransport + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcInterceptorsTests: Target { + .testTarget( + name: "GRPCInterceptorsTests", + dependencies: [ + .grpcCore, + .tracing, + .nioCore, + .grpcInterceptors + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcHTTP2CoreTests: Target { + .testTarget( + name: "GRPCHTTP2CoreTests", + dependencies: [ + .grpcHTTP2Core, + .nioCore, + .nioHTTP2, + .nioEmbedded, + .nioTestUtils, + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcHTTP2TransportTests: Target { + .testTarget( + name: "GRPCHTTP2TransportTests", + dependencies: [ + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .grpcHTTP2TransportNIOTransportServices, + .grpcProtobuf + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcCodeGenTests: Target { + .testTarget( + name: "GRPCCodeGenTests", + dependencies: [ + .grpcCodeGen + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcProtobufTests: Target { + .testTarget( + name: "GRPCProtobufTests", + dependencies: [ + .grpcProtobuf, + .grpcCore, + .protobuf + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcProtobufCodeGenTests: Target { + .testTarget( + name: "GRPCProtobufCodeGenTests", + dependencies: [ + .grpcCodeGen, + .grpcProtobufCodeGen, + .protobuf, + .protobufPluginLibrary + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var inProcessInteroperabilityTests: Target { + .testTarget( + name: "InProcessInteroperabilityTests", + dependencies: [ + .grpcInProcessTransport, + .interoperabilityTests, + .grpcCore + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var interopTestModels: Target { + .target( + name: "GRPCInteroperabilityTestModels", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + exclude: [ + "README.md", + "generate.sh", + "src/proto/grpc/testing/empty.proto", + "src/proto/grpc/testing/empty_service.proto", + "src/proto/grpc/testing/messages.proto", + "src/proto/grpc/testing/test.proto", + "unimplemented_call.patch", + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var interoperabilityTestImplementation: Target { + .target( + name: "InteroperabilityTests", + dependencies: [ + .grpcCore, + .grpcProtobuf + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var interoperabilityTestsExecutable: Target { + .executableTarget( + name: "interoperability-tests", + dependencies: [ + .grpcCore, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .interoperabilityTests, + .argumentParser + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var interopTestImplementation: Target { + .target( + name: "GRPCInteroperabilityTestsImplementation", + dependencies: [ + .grpc, + .interopTestModels, + .nioCore, + .nioPosix, + .nioHTTP1, + .logging, + ].appending( + .nioSSL, if: includeNIOSSL + ), + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var interopTests: Target { + .executableTarget( + name: "GRPCInteroperabilityTests", + dependencies: [ + .grpc, + .interopTestImplementation, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var backoffInteropTest: Target { + .executableTarget( + name: "GRPCConnectionBackoffInteropTest", + dependencies: [ + .grpc, + .interopTestModels, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ], + exclude: [ + "README.md", + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var perfTests: Target { + .executableTarget( + name: "GRPCPerformanceTests", + dependencies: [ + .grpc, + .grpcSampleData, + .nioCore, + .nioEmbedded, + .nioPosix, + .nioHTTP2, + .argumentParser, + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcSampleData: Target { + .target( + name: "GRPCSampleData", + dependencies: includeNIOSSL ? [.nioSSL] : [], + exclude: [ + "bundle.p12", + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var echoModel: Target { + .target( + name: "EchoModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/Echo/Model", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var echoImplementation: Target { + .target( + name: "EchoImplementation", + dependencies: [ + .echoModel, + .grpc, + .nioCore, + .nioHTTP2, + .protobuf, + ], + path: "Sources/Examples/Echo/Implementation", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var echo: Target { + .executableTarget( + name: "Echo", + dependencies: [ + .grpc, + .echoModel, + .echoImplementation, + .grpcSampleData, + .nioCore, + .nioPosix, + .logging, + .argumentParser, + ].appending( + .nioSSL, if: includeNIOSSL + ), + path: "Sources/Examples/Echo/Runtime", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var helloWorldModel: Target { + .target( + name: "HelloWorldModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/HelloWorld/Model", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var helloWorldClient: Target { + .executableTarget( + name: "HelloWorldClient", + dependencies: [ + .grpc, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/HelloWorld/Client", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var helloWorldServer: Target { + .executableTarget( + name: "HelloWorldServer", + dependencies: [ + .grpc, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/HelloWorld/Server", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var routeGuideModel: Target { + .target( + name: "RouteGuideModel", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/Examples/RouteGuide/Model", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var routeGuideClient: Target { + .executableTarget( + name: "RouteGuideClient", + dependencies: [ + .grpc, + .routeGuideModel, + .nioCore, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/RouteGuide/Client", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var routeGuideServer: Target { + .executableTarget( + name: "RouteGuideServer", + dependencies: [ + .grpc, + .routeGuideModel, + .nioCore, + .nioConcurrencyHelpers, + .nioPosix, + .argumentParser, + ], + path: "Sources/Examples/RouteGuide/Server", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var packetCapture: Target { + .executableTarget( + name: "PacketCapture", + dependencies: [ + .grpc, + .echoModel, + .nioCore, + .nioPosix, + .nioExtras, + .argumentParser, + ], + path: "Sources/Examples/PacketCapture", + exclude: [ + "README.md", + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var reflectionService: Target { + .target( + name: "GRPCReflectionService", + dependencies: [ + .grpc, + .nio, + .protobuf, + ], + path: "Sources/GRPCReflectionService", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var reflectionServer: Target { + .executableTarget( + name: "ReflectionServer", + dependencies: [ + .grpc, + .reflectionService, + .helloWorldModel, + .nioCore, + .nioPosix, + .argumentParser, + .echoModel, + .echoImplementation + ], + path: "Sources/Examples/ReflectionService", + resources: [ + .copy("Generated") + ], + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcCodeGen: Target { + .target( + name: "GRPCCodeGen", + path: "Sources/GRPCCodeGen", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcProtobuf: Target { + .target( + name: "GRPCProtobuf", + dependencies: [ + .grpcCore, + .protobuf, + ], + path: "Sources/GRPCProtobuf", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } + + static var grpcProtobufCodeGen: Target { + .target( + name: "GRPCProtobufCodeGen", + dependencies: [ + .protobuf, + .protobufPluginLibrary, + .grpcCodeGen + ], + path: "Sources/GRPCProtobufCodeGen", + swiftSettings: [.swiftLanguageVersion(.v5)] + ) + } +} + +// MARK: - Products + +extension Product { + static var grpc: Product { + .library( + name: grpcProductName, + targets: [grpcTargetName] + ) + } + + static var grpcCore: Product { + .library( + name: "_GRPCCore", + targets: ["GRPCCore"] + ) + } + + static var cgrpcZlib: Product { + .library( + name: cgrpcZlibProductName, + targets: [cgrpcZlibTargetName] + ) + } + + static var grpcReflectionService: Product { + .library( + name: "GRPCReflectionService", + targets: ["GRPCReflectionService"] + ) + } + + static var protocGenGRPCSwift: Product { + .executable( + name: "protoc-gen-grpc-swift", + targets: ["protoc-gen-grpc-swift"] + ) + } + + static var grpcSwiftPlugin: Product { + .plugin( + name: "GRPCSwiftPlugin", + targets: ["GRPCSwiftPlugin"] + ) + } +} + +// MARK: - Package + +let package = Package( + name: grpcPackageName, + products: [ + .grpc, + .grpcCore, + .cgrpcZlib, + .grpcReflectionService, + .protocGenGRPCSwift, + .grpcSwiftPlugin, + ], + dependencies: packageDependencies, + targets: [ + // Products + .grpc, + .cgrpcZlib, + .protocGenGRPCSwift, + .grpcSwiftPlugin, + .reflectionService, + + // Tests etc. + .grpcTests, + .interopTestModels, + .interopTestImplementation, + .interopTests, + .backoffInteropTest, + .perfTests, + .grpcSampleData, + + // Examples + .echoModel, + .echoImplementation, + .echo, + .helloWorldModel, + .helloWorldClient, + .helloWorldServer, + .routeGuideModel, + .routeGuideClient, + .routeGuideServer, + .packetCapture, + .reflectionServer, + + // v2 + .grpcCore, + .grpcInProcessTransport, + .grpcCodeGen, + .grpcInterceptors, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .grpcHTTP2TransportNIOTransportServices, + .grpcProtobuf, + .grpcProtobufCodeGen, + .interoperabilityTestImplementation, + .interoperabilityTestsExecutable, + .performanceWorker, + + // v2 tests + .grpcCoreTests, + .grpcInProcessTransportTests, + .grpcCodeGenTests, + .grpcInterceptorsTests, + .grpcHTTP2CoreTests, + .grpcHTTP2TransportTests, + .grpcProtobufTests, + .grpcProtobufCodeGenTests, + .inProcessInteroperabilityTests + ] +) + +extension Array { + func appending(_ element: Element, if condition: Bool) -> [Element] { + if condition { + return self + [element] + } else { + return self + } + } +} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json deleted file mode 100644 index b20317867..000000000 --- a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 11, - "memoryLeaked" : 0, - "releaseCount" : 1011, - "retainCount" : 2000, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json deleted file mode 100644 index 7fde30a69..000000000 --- a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Add_string.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 11, - "memoryLeaked" : 0, - "releaseCount" : 4012, - "retainCount" : 2000, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json deleted file mode 100644 index ff39fd3c1..000000000 --- a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 1002, - "retainCount" : 1001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json deleted file mode 100644 index ff39fd3c1..000000000 --- a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 1002, - "retainCount" : 1001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json deleted file mode 100644 index 9b82ead83..000000000 --- a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 2000, - "memoryLeaked" : 0, - "releaseCount" : 4002, - "retainCount" : 2001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json deleted file mode 100644 index ff39fd3c1..000000000 --- a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 1002, - "retainCount" : 1001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json deleted file mode 100644 index 295dd81c5..000000000 --- a/Performance/Benchmarks/Thresholds/5.8/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 2002000, - "retainCount" : 1999000, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json deleted file mode 100644 index b20317867..000000000 --- a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 11, - "memoryLeaked" : 0, - "releaseCount" : 1011, - "retainCount" : 2000, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json deleted file mode 100644 index 7fde30a69..000000000 --- a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Add_string.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 11, - "memoryLeaked" : 0, - "releaseCount" : 4012, - "retainCount" : 2000, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json deleted file mode 100644 index ff39fd3c1..000000000 --- a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 1002, - "retainCount" : 1001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json deleted file mode 100644 index ff39fd3c1..000000000 --- a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 1002, - "retainCount" : 1001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json deleted file mode 100644 index 9b82ead83..000000000 --- a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 2000, - "memoryLeaked" : 0, - "releaseCount" : 4002, - "retainCount" : 2001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json deleted file mode 100644 index ff39fd3c1..000000000 --- a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 1002, - "retainCount" : 1001, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json deleted file mode 100644 index 295dd81c5..000000000 --- a/Performance/Benchmarks/Thresholds/5.9/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mallocCountTotal" : 0, - "memoryLeaked" : 0, - "releaseCount" : 2002000, - "retainCount" : 1999000, - "syscalls" : 0 -} diff --git a/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json rename to Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json diff --git a/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Add_string.p90.json rename to Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json rename to Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json diff --git a/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json rename to Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json diff --git a/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json diff --git a/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json rename to Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json diff --git a/Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/5.10/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename to Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index 42e412f49..acb47fb0a 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -22,7 +22,7 @@ struct GRPCSwiftPlugin { /// Errors thrown by the `GRPCSwiftPlugin` enum PluginError: Error, CustomStringConvertible { /// Indicates that the target where the plugin was applied to was not `SourceModuleTarget`. - case invalidTarget(Target) + case invalidTarget(String) /// Indicates that the file extension of an input file was not `.proto`. case invalidInputFileExtension(String) /// Indicates that there was no configuration file at the required location. @@ -31,7 +31,7 @@ struct GRPCSwiftPlugin { var description: String { switch self { case let .invalidTarget(target): - return "Expected a SwiftSourceModuleTarget but got '\(type(of: target))'." + return "Expected a SwiftSourceModuleTarget but got '\(target)'." case let .invalidInputFileExtension(path): return "The input file '\(path)' does not have a '.proto' extension." case let .noConfigFound(path): @@ -259,7 +259,7 @@ extension GRPCSwiftPlugin: BuildToolPlugin { target: Target ) async throws -> [Command] { guard let swiftTarget = target as? SwiftSourceModuleTarget else { - throw PluginError.invalidTarget(target) + throw PluginError.invalidTarget("\(type(of: target))") } return try self.createBuildCommands( pluginWorkDirectory: context.pluginWorkDirectory, diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 07572316f..36dee4503 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -14,11 +14,14 @@ * limitations under the License. */ import Foundation -import GRPCCodeGen -import GRPCProtobufCodeGen import SwiftProtobuf import SwiftProtobufPluginLibrary +#if compiler(>=6.0) +import GRPCCodeGen +import GRPCProtobufCodeGen +#endif + func Log(_ message: String) { FileHandle.standardError.write((message + "\n").data(using: .utf8)!) } @@ -168,6 +171,8 @@ func main(args: [String]) throws { fileNamingOption: options.fileNaming, generatedFiles: &generatedFiles ) + + #if compiler(>=6.0) if options.v2 { let grpcGenerator = ProtobufCodeGenerator( configuration: SourceGenerator.Configuration(options: options) @@ -181,6 +186,10 @@ func main(args: [String]) throws { let grpcGenerator = Generator(fileDescriptor, options: options) grpcFile.content = grpcGenerator.code } + #else + let grpcGenerator = Generator(fileDescriptor, options: options) + grpcFile.content = grpcGenerator.code + #endif grpcFile.name = grpcFileName response.file.append(grpcFile) } @@ -198,6 +207,7 @@ do { Log("ERROR: \(error)") } +#if compiler(>=6.0) extension SourceGenerator.Configuration { init(options: GeneratorOptions) { let accessLevel: SourceGenerator.Configuration.AccessLevel @@ -216,3 +226,4 @@ extension SourceGenerator.Configuration { ) } } +#endif diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/options.swift index e2a9b27dd..ec1f9e8e0 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/options.swift @@ -68,7 +68,9 @@ final class GeneratorOptions { private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" private(set) var generateReflectionData = false + #if compiler(>=6.0) private(set) var v2 = false + #endif init(parameter: String?) throws { for pair in GeneratorOptions.parseParameter(string: parameter) { @@ -155,12 +157,14 @@ final class GeneratorOptions { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } + #if compiler(>=6.0) case "_V2": if let value = Bool(pair.value) { self.v2 = value } else { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } + #endif default: throw GenerationError.unknownParameter(name: pair.key) diff --git a/scripts/license-check.sh b/scripts/license-check.sh index 9005fadd6..8bcdc9769 100755 --- a/scripts/license-check.sh +++ b/scripts/license-check.sh @@ -74,6 +74,11 @@ check_copyright_headers() { drop_first=1 expected_lines=15 ;; + */Package@swift-*.swift) + expected_sha="$SWIFT_SHA" + drop_first=1 + expected_lines=15 + ;; */Package@swift-*.*.swift) expected_sha="$SWIFT_SHA" drop_first=1 From 754bdadcdbd5ee3a2106013fd9b23f52cfc48a6a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 2 Jul 2024 15:31:10 +0100 Subject: [PATCH 377/580] Test SwiftPM plugin for v2 (#1957) Motivation: The SwiftPM plugin integration test only tests the plugin for gRPC Swift v1. Modification: - Allow the test to run for v2 Result: Better coverage --- .github/workflows/ci.yaml | 12 +++++-- Package@swift-6.swift | 14 ++++++-- scripts/run-plugin-tests.sh | 67 +++++++++++++++++++++++++------------ 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6e05d6bac..26d4e0447 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -141,14 +141,19 @@ jobs: include: - image: swiftlang/swift:nightly-jammy swift-tools-version: '6.0' + supports-v2: true - image: swiftlang/swift:nightly-6.0-jammy swift-tools-version: '6.0' + supports-v2: true - image: swift:5.10.1-noble swift-tools-version: '5.10' + supports-v2: false - image: swift:5.9-jammy swift-tools-version: '5.9' + supports-v2: false - image: swift:5.8-focal swift-tools-version: '5.8' + supports-v2: false name: Integration Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -157,8 +162,11 @@ jobs: - uses: actions/checkout@v4 - name: Install protoc run: apt update && apt install -y protobuf-compiler - - name: SwiftPM plugin test - run: ./scripts/run-plugin-tests.sh ${{ matrix.swift-tools-version }} + - name: SwiftPM plugin test (v1) + run: ./scripts/run-plugin-tests.sh ${{ matrix.swift-tools-version }} "v1" + - name: SwiftPM plugin test (v2) + if: ${{ matrix.supports-v2 }} + run: ./scripts/run-plugin-tests.sh ${{ matrix.swift-tools-version }} "v2" - name: Build without NIOSSL run: swift build env: diff --git a/Package@swift-6.swift b/Package@swift-6.swift index c89ca9b08..24f1982d0 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -810,13 +810,20 @@ extension Product { ) } - static var grpcCore: Product { + static var _grpcCore: Product { .library( name: "_GRPCCore", targets: ["GRPCCore"] ) } + static var _grpcProtobuf: Product { + .library( + name: "_GRPCProtobuf", + targets: ["GRPCProtobuf"] + ) + } + static var cgrpcZlib: Product { .library( name: cgrpcZlibProductName, @@ -851,12 +858,15 @@ extension Product { let package = Package( name: grpcPackageName, products: [ + // v1 .grpc, - .grpcCore, .cgrpcZlib, .grpcReflectionService, .protocGenGRPCSwift, .grpcSwiftPlugin, + // v2 + ._grpcCore, + ._grpcProtobuf, ], dependencies: packageDependencies, targets: [ diff --git a/scripts/run-plugin-tests.sh b/scripts/run-plugin-tests.sh index a0af91597..c772508c2 100755 --- a/scripts/run-plugin-tests.sh +++ b/scripts/run-plugin-tests.sh @@ -20,10 +20,11 @@ HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" GRPC_PATH="${HERE}/.." function generate_package_manifest { - local version=$1 + local tools_version=$1 local grpc_path=$2 + local grpc_version=$3 - echo "// swift-tools-version: $version" + echo "// swift-tools-version: $tools_version" echo "import PackageDescription" echo "" echo "let package = Package(" @@ -36,7 +37,14 @@ function generate_package_manifest { echo " .executableTarget(" echo " name: \"Foo\"," echo " dependencies: [" - echo " .product(name: \"GRPC\", package: \"grpc-swift\")," + + if [ "$grpc_version" == "v1" ]; then + echo " .product(name: \"GRPC\", package: \"grpc-swift\")," + elif [ "$grpc_version" == "v2" ]; then + echo " .product(name: \"_GRPCCore\", package: \"grpc-swift\")," + echo " .product(name: \"_GRPCProtobuf\", package: \"grpc-swift\")," + fi + echo " ]," echo " path: \"Sources/Foo\"," echo " plugins: [" @@ -49,20 +57,30 @@ function generate_package_manifest { } function generate_grpc_plugin_config { - cat < "$dir/Package.swift" - generate_grpc_plugin_config > "$dir/Sources/Foo/grpc-swift-config.json" + generate_package_manifest "$tools_version" "$grpc_path" "$grpc_version" > "$dir/Package.swift" + generate_protobuf_plugin_config > "$dir/Sources/Foo/swift-protobuf-config.json" generate_proto > "$dir/Sources/Foo/Foo.proto" generate_main > "$dir/Sources/Foo/main.swift" + generate_grpc_plugin_config "$grpc_version" > "$dir/Sources/Foo/grpc-swift-config.json" PROTOC_PATH=$protoc_path swift build --package-path "$dir" } -if [[ $# -lt 1 ]]; then - echo "Usage: $0 SWIFT_TOOLS_VERSION" +if [[ $# -lt 2 ]]; then + echo "Usage: $0 SWIFT_TOOLS_VERSION GRPC_SWIFT_VERSION" +fi + +if [ "$2" != "v1" ] && [ "$2" != "v2" ]; then + echo "Invalid gRPC Swift version '$2' (must be 'v1' or 'v2')" exit 1 fi -generate_and_build "$1" "${GRPC_PATH}" +generate_and_build "$1" "${GRPC_PATH}" "$2" From 804c256d8680bd012ad36cb72b3ca5567cc398d1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 3 Jul 2024 13:43:06 +0100 Subject: [PATCH 378/580] Enable Swift 6 language mode for a number of v2 modules (#1959) Enable Swift 6 language mode for a number of v2 modules Motivation: v2 will eventually require the v6 language, doing this in one go is a reasonably large change so adopt a few targets at a time. Modifications: - Fixup warnings/errors in the following targets: - GRPCInterceptors - GRPCInProcessTransport - GRPCHTTP2Core - GRPCHTTP2TransportNIOPosix - GRPCHTTP2TransportNIOTransportServices - GRPCCodeGen - GRPCProtobufCodeGenTests - GRPCProtobufCodeGen - GRPCProtobufTests - GRPCProtobuf - interoperability-tests - performance-worker - Fixup warnings/errors but remain in the v5 language mode for the following: - GRPCInterceptorsTests Result: More modules use v6 language mode --- Package@swift-6.swift | 24 ++++---- .../GRPCCore/Transport/ClientTransport.swift | 2 +- .../GRPCCore/Transport/ServerTransport.swift | 2 +- .../Connection/ClientConnectionHandler.swift | 41 ++++++++++++-- .../Client/Connection/ConnectionFactory.swift | 2 +- .../Client/Connection/GRPCChannel.swift | 2 +- .../Internal/ConstantAsyncSequence.swift | 4 +- ...iscardingTaskGroup+CancellableHandle.swift | 2 +- Sources/GRPCHTTP2Core/Internal/Timer.swift | 2 +- .../ServerConnectionManagementHandler.swift | 43 +++++++++++--- .../HTTP2ClientTransport+Posix.swift | 2 +- .../HTTP2ServerTransport+Posix.swift | 2 +- .../NIOClientBootstrap+SocketAddress.swift | 2 +- ...TP2ServerTransport+TransportServices.swift | 2 +- .../InProcessServerTransport.swift | 2 +- .../OnFinishAsyncSequence.swift | 8 +-- .../InteroperabilityTestsExecutable.swift | 8 +-- .../TracingInterceptorTests.swift | 9 ++- .../TracingTestsUtilities.swift | 56 +++++++++++++------ .../ProtobufCodingTests.swift | 2 +- 20 files changed, 152 insertions(+), 65 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 24f1982d0..54f32d6ec 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -203,7 +203,7 @@ extension Target { dependencies: [ .grpcCore ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -214,7 +214,7 @@ extension Target { .grpcCore, .tracing ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -229,7 +229,7 @@ extension Target { .dequeModule, .atomics ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -242,7 +242,7 @@ extension Target { .nioPosix, .nioExtras ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -256,7 +256,7 @@ extension Target { .nioExtras, .nioTransportServices ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -298,7 +298,7 @@ extension Target { .nioFileSystem, .argumentParser ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -426,7 +426,7 @@ extension Target { .grpcCore, .protobuf ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -439,7 +439,7 @@ extension Target { .protobuf, .protobufPluginLibrary ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -497,7 +497,7 @@ extension Target { .interoperabilityTests, .argumentParser ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -770,7 +770,7 @@ extension Target { .target( name: "GRPCCodeGen", path: "Sources/GRPCCodeGen", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -782,7 +782,7 @@ extension Target { .protobuf, ], path: "Sources/GRPCProtobuf", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } @@ -795,7 +795,7 @@ extension Target { .grpcCodeGen ], path: "Sources/GRPCProtobufCodeGen", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6)] ) } } diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index b730dd10c..90d47e6fd 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -61,7 +61,7 @@ public protocol ClientTransport: Sendable { /// - options: Options specific to the stream. /// - closure: A closure that takes the opened stream as parameter. /// - Returns: Whatever value was returned from `closure`. - func withStream( + func withStream( descriptor: MethodDescriptor, options: CallOptions, _ closure: (_ stream: RPCStream) async throws -> T diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index 522be9670..5a3547e30 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -31,7 +31,7 @@ public protocol ServerTransport: Sendable { /// period after which any open streams may be cancelled. You can also cancel the task running /// ``listen()`` to abruptly close connections and streams. func listen( - _ streamHandler: @escaping (RPCStream) async -> Void + _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws /// Indicates to the transport that no new streams should be accepted. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 55de44a3a..10d0b2ced 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -198,9 +198,10 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl // Pings are ack'd by the HTTP/2 handler so we only pay attention to acks here, and in // particular only those carrying the keep-alive data. if ack, data == self.keepalivePingData { + let loopBound = LoopBoundView(handler: self, context: context) self.keepaliveTimeoutTimer.cancel() self.keepaliveTimer?.schedule(on: context.eventLoop) { - self.keepaliveTimerFired(context: context) + loopBound.keepaliveTimerFired() } } @@ -211,12 +212,13 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl // becoming active is insufficient as, for example, a TLS handshake may fail after // establishing the TCP connection, or the server isn't configured for gRPC (or HTTP/2). if isInitialSettings { + let loopBound = LoopBoundView(handler: self, context: context) self.keepaliveTimer?.schedule(on: context.eventLoop) { - self.keepaliveTimerFired(context: context) + loopBound.keepaliveTimerFired() } self.maxIdleTimer?.schedule(on: context.eventLoop) { - self.maxIdleTimerFired(context: context) + loopBound.maxIdleTimerFired() } context.fireChannelRead(self.wrapInboundOut(.ready)) @@ -264,6 +266,33 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } } +extension ClientConnectionHandler { + struct LoopBoundView: @unchecked Sendable { + private let handler: ClientConnectionHandler + private let context: ChannelHandlerContext + + init(handler: ClientConnectionHandler, context: ChannelHandlerContext) { + self.handler = handler + self.context = context + } + + func keepaliveTimerFired() { + self.context.eventLoop.assertInEventLoop() + self.handler.keepaliveTimerFired(context: self.context) + } + + func keepaliveTimeoutExpired() { + self.context.eventLoop.assertInEventLoop() + self.handler.keepaliveTimeoutExpired(context: self.context) + } + + func maxIdleTimerFired() { + self.context.eventLoop.assertInEventLoop() + self.handler.maxIdleTimerFired(context: self.context) + } + } +} + extension ClientConnectionHandler { struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { // @unchecked is okay: the only methods do the appropriate event-loop dance. @@ -315,8 +344,9 @@ extension ClientConnectionHandler { case .startIdleTimer(let cancelKeepalive): // All streams are closed, restart the idle timer, and stop the keep-alive timer (it may // not stop if keep-alive is allowed when there are no active calls). + let loopBound = LoopBoundView(handler: self, context: context) self.maxIdleTimer?.schedule(on: context.eventLoop) { - self.maxIdleTimerFired(context: context) + loopBound.maxIdleTimerFired() } if cancelKeepalive { @@ -355,8 +385,9 @@ extension ClientConnectionHandler { self.maybeFlush(context: context) // Schedule a timeout on waiting for the response. + let loopBound = LoopBoundView(handler: self, context: context) self.keepaliveTimeoutTimer.schedule(on: context.eventLoop) { - self.keepaliveTimeoutExpired(context: context) + loopBound.keepaliveTimeoutExpired() } } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift index 1ec651ff3..63612ad69 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift @@ -26,7 +26,7 @@ public protocol HTTP2Connector: Sendable { @_spi(Package) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct HTTP2Connection { +public struct HTTP2Connection: Sendable { /// The underlying TCP connection wrapped up for use with gRPC. var channel: NIOAsyncChannel diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index fb1f10729..0dbf0c8c9 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -189,7 +189,7 @@ public struct GRPCChannel: ClientTransport { } /// Opens a stream using the transport, and uses it as input into a user-provided closure. - public func withStream( + public func withStream( descriptor: MethodDescriptor, options: CallOptions, _ closure: (_ stream: RPCStream) async throws -> T diff --git a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift index f7b1d11b3..6e3e93265 100644 --- a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift +++ b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift @@ -17,7 +17,7 @@ import GRPCCore @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -private struct ConstantAsyncSequence: AsyncSequence { +private struct ConstantAsyncSequence: AsyncSequence, Sendable { private let element: Element init(element: Element) { @@ -42,7 +42,7 @@ private struct ConstantAsyncSequence: AsyncSequence { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension RPCAsyncSequence { +extension RPCAsyncSequence where Element: Sendable { static func constant(_ element: Element) -> RPCAsyncSequence { return RPCAsyncSequence(wrapping: ConstantAsyncSequence(element: element)) } diff --git a/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift b/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift index c97574f37..11f818c28 100644 --- a/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift +++ b/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift @@ -55,7 +55,7 @@ extension DiscardingTaskGroup { } @usableFromInline - enum FinishedOrCancelled { + enum FinishedOrCancelled: Sendable { case finished case cancelled } diff --git a/Sources/GRPCHTTP2Core/Internal/Timer.swift b/Sources/GRPCHTTP2Core/Internal/Timer.swift index 0d97d148e..0be004347 100644 --- a/Sources/GRPCHTTP2Core/Internal/Timer.swift +++ b/Sources/GRPCHTTP2Core/Internal/Timer.swift @@ -45,7 +45,7 @@ struct Timer { } /// Schedule a task on the given `EventLoop`. - mutating func schedule(on eventLoop: EventLoop, work: @escaping () throws -> Void) { + mutating func schedule(on eventLoop: EventLoop, work: @escaping @Sendable () throws -> Void) { self.task?.cancel() if self.repeat { diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index 2f4fe2da7..a7d9b9bef 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -247,16 +247,18 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { } func channelActive(context: ChannelHandlerContext) { + let view = LoopBoundView(handler: self, context: context) + self.maxAgeTimer?.schedule(on: context.eventLoop) { - self.initiateGracefulShutdown(context: context) + view.initiateGracefulShutdown() } self.maxIdleTimer?.schedule(on: context.eventLoop) { - self.initiateGracefulShutdown(context: context) + view.initiateGracefulShutdown() } self.keepaliveTimer?.schedule(on: context.eventLoop) { - self.keepaliveTimerFired(context: context) + view.keepaliveTimerFired() } context.fireChannelActive() @@ -321,8 +323,9 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.inReadLoop = false // Done reading: schedule the keep-alive timer. + let view = LoopBoundView(handler: self, context: context) self.keepaliveTimer?.schedule(on: context.eventLoop) { - self.keepaliveTimerFired(context: context) + view.keepaliveTimerFired() } context.fireChannelReadComplete() @@ -333,6 +336,29 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { } } +extension ServerConnectionManagementHandler { + struct LoopBoundView: @unchecked Sendable { + private let handler: ServerConnectionManagementHandler + private let context: ChannelHandlerContext + + init(handler: ServerConnectionManagementHandler, context: ChannelHandlerContext) { + self.handler = handler + self.context = context + } + + func initiateGracefulShutdown() { + self.context.eventLoop.assertInEventLoop() + self.handler.initiateGracefulShutdown(context: self.context) + } + + func keepaliveTimerFired() { + self.context.eventLoop.assertInEventLoop() + self.handler.keepaliveTimerFired(context: self.context) + } + + } +} + extension ServerConnectionManagementHandler { struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { // @unchecked is okay: the only methods do the appropriate event-loop dance. @@ -379,8 +405,9 @@ extension ServerConnectionManagementHandler { switch self.state.streamClosed(id) { case .startIdleTimer: + let loopBound = LoopBoundView(handler: self, context: context) self.maxIdleTimer?.schedule(on: context.eventLoop) { - self.initiateGracefulShutdown(context: context) + loopBound.initiateGracefulShutdown() } case .close: @@ -481,8 +508,9 @@ extension ServerConnectionManagementHandler { } else { // RPCs may have a grace period for finishing once the second GOAWAY frame has finished. // If this is set close the connection abruptly once the grace period passes. + let loopBound = NIOLoopBound(context, eventLoop: context.eventLoop) self.maxGraceTimer?.schedule(on: context.eventLoop) { - context.close(promise: nil) + loopBound.value.close(promise: nil) } } @@ -497,8 +525,9 @@ extension ServerConnectionManagementHandler { self.maybeFlush(context: context) // Schedule a timeout on waiting for the response. + let loopBound = LoopBoundView(handler: self, context: context) self.keepaliveTimeoutTimer.schedule(on: context.eventLoop) { - self.initiateGracefulShutdown(context: context) + loopBound.initiateGracefulShutdown() } } } diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index e65913cd8..7d0d4d74b 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -109,7 +109,7 @@ extension HTTP2ClientTransport { self.channel.close() } - public func withStream( + public func withStream( descriptor: MethodDescriptor, options: CallOptions, _ closure: (RPCStream) async throws -> T diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index d743f4b83..2996af1bd 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -162,7 +162,7 @@ extension HTTP2ServerTransport { } public func listen( - _ streamHandler: @escaping (RPCStream) async -> Void + _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { defer { switch self.listeningAddressState.withLockedValue({ $0.close() }) { diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift index b9b81f431..ebeec013d 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift @@ -21,7 +21,7 @@ import NIOPosix extension ClientBootstrap { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - func connect( + func connect( to address: GRPCHTTP2Core.SocketAddress, _ configure: @Sendable @escaping (Channel) -> EventLoopFuture ) async throws -> Result { diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 8a72cb627..fe340f191 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -156,7 +156,7 @@ extension HTTP2ServerTransport { } public func listen( - _ streamHandler: @escaping (RPCStream) async -> Void + _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { defer { switch self.listeningAddressState.withLockedValue({ $0.close() }) { diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 85df88b74..5803dc93a 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -56,7 +56,7 @@ public struct InProcessServerTransport: ServerTransport, Sendable { } public func listen( - _ streamHandler: @escaping (RPCStream) async -> Void + _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { await withDiscardingTaskGroup { group in for await stream in self.newStreams { diff --git a/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift b/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift index 311e1fcd8..d07a8efec 100644 --- a/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift +++ b/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift @@ -20,8 +20,8 @@ struct OnFinishAsyncSequence: AsyncSequence, Sendable { init( wrapping other: S, - onFinish: @escaping () -> Void - ) where S.Element == Element { + onFinish: @escaping @Sendable () -> Void + ) where S.Element == Element, S: Sendable { self._makeAsyncIterator = { AsyncIterator(wrapping: other.makeAsyncIterator(), onFinish: onFinish) } @@ -33,11 +33,11 @@ struct OnFinishAsyncSequence: AsyncSequence, Sendable { struct AsyncIterator: AsyncIteratorProtocol { private var iterator: any AsyncIteratorProtocol - private var onFinish: (() -> Void)? + private var onFinish: (@Sendable () -> Void)? fileprivate init( wrapping other: Iterator, - onFinish: @escaping () -> Void + onFinish: @escaping @Sendable () -> Void ) where Iterator: AsyncIteratorProtocol, Iterator.Element == Element { self.iterator = other self.onFinish = onFinish diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift index 81e623265..f0c8fb75b 100644 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -24,13 +24,13 @@ import NIOPosix @main @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) struct InteroperabilityTestsExecutable: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( abstract: "gRPC Swift Interoperability Runner", subcommands: [StartServer.self, ListTests.self, RunTests.self] ) struct StartServer: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( abstract: "Start the gRPC Swift interoperability test server." ) @@ -50,7 +50,7 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { } struct ListTests: ParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( abstract: "List all interoperability test names." ) @@ -62,7 +62,7 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { } struct RunTests: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( abstract: """ Run gRPC interoperability tests using a gRPC Swift client. You can specify a test name as an argument to run a single test. diff --git a/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift b/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift index 98fbb2558..fa2053a4f 100644 --- a/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift +++ b/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift @@ -175,8 +175,9 @@ final class TracingInterceptorTests: XCTestCase { method: "testServerInterceptorErrorResponse" ) let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: false) + let single = ServerRequest.Single(metadata: ["trace-id": "some-trace-id"], message: [UInt8]()) let response = try await interceptor.intercept( - request: .init(single: .init(metadata: ["trace-id": "some-trace-id"], message: [])), + request: .init(single: single), context: .init(descriptor: methodDescriptor) ) { _, _ in ServerResponse.Stream(error: .init(code: .unknown, message: "Test error")) @@ -203,8 +204,9 @@ final class TracingInterceptorTests: XCTestCase { ) let (stream, continuation) = AsyncStream.makeStream() let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: false) + let single = ServerRequest.Single(metadata: ["trace-id": "some-trace-id"], message: [UInt8]()) let response = try await interceptor.intercept( - request: .init(single: .init(metadata: ["trace-id": "some-trace-id"], message: [])), + request: .init(single: single), context: .init(descriptor: methodDescriptor) ) { _, _ in { [serviceContext = ServiceContext.current] in @@ -267,8 +269,9 @@ final class TracingInterceptorTests: XCTestCase { ) let (stream, continuation) = AsyncStream.makeStream() let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: true) + let single = ServerRequest.Single(metadata: ["trace-id": "some-trace-id"], message: [UInt8]()) let response = try await interceptor.intercept( - request: .init(single: .init(metadata: ["trace-id": "some-trace-id"], message: [])), + request: .init(single: single), context: .init(descriptor: methodDescriptor) ) { _, _ in { [serviceContext = ServiceContext.current] in diff --git a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift index ead6e58d3..90d6eac0c 100644 --- a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift +++ b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift @@ -21,10 +21,11 @@ import Tracing final class TestTracer: Tracer { typealias Span = TestSpan - private var testSpans: NIOLockedValueBox<[String: TestSpan]> = .init([:]) + private let testSpans: NIOLockedValueBox<[String: TestSpan]> = .init([:]) func getEventsForTestSpan(ofOperationName operationName: String) -> [SpanEvent] { - self.testSpans.withLockedValue({ $0[operationName] })?.events ?? [] + let span = self.testSpans.withLockedValue({ $0[operationName] }) + return span?.events ?? [] } func extract( @@ -67,13 +68,35 @@ final class TestTracer: Tracer { } } -class TestSpan: Span { - var context: ServiceContextModule.ServiceContext - var operationName: String - var attributes: Tracing.SpanAttributes - var isRecording: Bool - private(set) var status: Tracing.SpanStatus? - private(set) var events: [Tracing.SpanEvent] = [] +struct TestSpan: Span, Sendable { + private struct State { + var context: ServiceContextModule.ServiceContext + var operationName: String + var attributes: Tracing.SpanAttributes + var status: Tracing.SpanStatus? + var events: [Tracing.SpanEvent] = [] + } + + private let state: NIOLockedValueBox + let isRecording: Bool + + var context: ServiceContextModule.ServiceContext { + self.state.withLockedValue { $0.context } + } + + var operationName: String { + get { self.state.withLockedValue { $0.operationName } } + nonmutating set { self.state.withLockedValue { $0.operationName = newValue } } + } + + var attributes: Tracing.SpanAttributes { + get { self.state.withLockedValue { $0.attributes } } + nonmutating set { self.state.withLockedValue { $0.attributes = newValue } } + } + + var events: [Tracing.SpanEvent] { + self.state.withLockedValue { $0.events } + } init( context: ServiceContextModule.ServiceContext, @@ -81,18 +104,17 @@ class TestSpan: Span { attributes: Tracing.SpanAttributes = [:], isRecording: Bool = true ) { - self.context = context - self.operationName = operationName - self.attributes = attributes + let state = State(context: context, operationName: operationName, attributes: attributes) + self.state = NIOLockedValueBox(state) self.isRecording = isRecording } func setStatus(_ status: Tracing.SpanStatus) { - self.status = status + self.state.withLockedValue { $0.status = status } } func addEvent(_ event: Tracing.SpanEvent) { - self.events.append(event) + self.state.withLockedValue { $0.events.append(event) } } func recordError( @@ -109,7 +131,9 @@ class TestSpan: Span { } func addLink(_ link: Tracing.SpanLink) { - self.context.spanLinks?.append(link) + self.state.withLockedValue { + $0.context.spanLinks?.append(link) + } } func end(at instant: @autoclosure () -> Instant) where Instant: Tracing.TracerInstant { @@ -150,7 +174,7 @@ extension ServiceContext { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct TestWriter: RPCWriterProtocol { +struct TestWriter: RPCWriterProtocol { typealias Element = WriterElement private let streamContinuation: AsyncStream.Continuation diff --git a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift index 1c657848c..2f92e4cca 100644 --- a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift +++ b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift @@ -76,7 +76,7 @@ final class ProtobufCodingTests: XCTestCase { struct TestMessage: SwiftProtobuf.Message { var text: String = "" var unknownFields = SwiftProtobuf.UnknownStorage() - static var protoMessageName: String = "Test" + ".ServiceRequest" + static let protoMessageName: String = "Test.ServiceRequest" init() {} mutating func decodeMessage(decoder: inout D) throws where D: SwiftProtobuf.Decoder { From d1aa3b8c44ec4fb52ea046b64be975962045b7cc Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 3 Jul 2024 13:49:30 +0100 Subject: [PATCH 379/580] Fix warnings in SwiftPM plugin (#1958) Motivation: SwiftPMs 'Path' was all but deprecated in Swift 6 resulting in numerous warnings for the SwiftPM build plugin. It has largely been replaced by URL. Modifications: - Add a wrapper type to the plugin which wraps Path or URL depending on the compiler version. Result: No build plugin warnings for Swift 6 compilers --- Plugins/GRPCSwiftPlugin/plugin.swift | 171 +++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 26 deletions(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index acb47fb0a..38e697b04 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -99,43 +99,42 @@ struct GRPCSwiftPlugin { /// - tool: The tool method from the context. /// - Returns: The build commands configured based on the arguments. func createBuildCommands( - pluginWorkDirectory: PackagePlugin.Path, + pluginWorkDirectory: PathLike, sourceFiles: FileList, tool: (String) throws -> PackagePlugin.PluginContext.Tool ) throws -> [Command] { - guard - let configurationFilePath = sourceFiles.first( - where: { - $0.path.lastComponent == Self.configurationFileName - } - )?.path - else { + let maybeConfigFile = sourceFiles.map { PathLike($0) }.first { + $0.lastComponent == Self.configurationFileName + } + + guard let configurationFilePath = maybeConfigFile else { throw PluginError.noConfigFound(Self.configurationFileName) } - let data = try Data(contentsOf: URL(fileURLWithPath: "\(configurationFilePath)")) + let data = try Data(contentsOf: URL(configurationFilePath)) let configuration = try JSONDecoder().decode(Configuration.self, from: data) try self.validateConfiguration(configuration) let targetDirectory = configurationFilePath.removingLastComponent() - var importPaths: [Path] = [targetDirectory] + var importPaths: [PathLike] = [targetDirectory] if let configuredImportPaths = configuration.importPaths { - importPaths.append(contentsOf: configuredImportPaths.map { Path($0) }) + importPaths.append(contentsOf: configuredImportPaths.map { PathLike($0) }) } // We need to find the path of protoc and protoc-gen-grpc-swift - let protocPath: Path + let protocPath: PathLike if let configuredProtocPath = configuration.protocPath { - protocPath = Path(configuredProtocPath) + protocPath = PathLike(configuredProtocPath) } else if let environmentPath = ProcessInfo.processInfo.environment["PROTOC_PATH"] { // The user set the env variable, so let's take that - protocPath = Path(environmentPath) + protocPath = PathLike(environmentPath) } else { // The user didn't set anything so let's try see if SPM can find a binary for us - protocPath = try tool("protoc").path + protocPath = try PathLike(tool("protoc")) } - let protocGenGRPCSwiftPath = try tool("protoc-gen-grpc-swift").path + + let protocGenGRPCSwiftPath = try PathLike(tool("protoc-gen-grpc-swift")) return configuration.invocations.map { invocation in self.invokeProtoc( @@ -160,12 +159,12 @@ struct GRPCSwiftPlugin { /// - importPaths: List of paths to pass with "-I " to `protoc`. /// - Returns: The build command configured based on the arguments private func invokeProtoc( - directory: Path, + directory: PathLike, invocation: Configuration.Invocation, - protocPath: Path, - protocGenGRPCSwiftPath: Path, - outputDirectory: Path, - importPaths: [Path] + protocPath: PathLike, + protocGenGRPCSwiftPath: PathLike, + outputDirectory: PathLike, + importPaths: [PathLike] ) -> Command { // Construct the `protoc` arguments. var protocArgs = [ @@ -202,12 +201,12 @@ struct GRPCSwiftPlugin { protocArgs.append("--grpc-swift_opt=_V2=\(v2)") } - var inputFiles = [Path]() - var outputFiles = [Path]() + var inputFiles = [PathLike]() + var outputFiles = [PathLike]() for var file in invocation.protoFiles { // Append the file to the protoc args so that it is used for generating - protocArgs.append("\(file)") + protocArgs.append(file) inputFiles.append(directory.appending(file)) // The name of the output file is based on the name of the input file. @@ -261,14 +260,128 @@ extension GRPCSwiftPlugin: BuildToolPlugin { guard let swiftTarget = target as? SwiftSourceModuleTarget else { throw PluginError.invalidTarget("\(type(of: target))") } + + #if compiler(<6.0) + let workDirectory = PathLike(context.pluginWorkDirectory) + #else + let workDirectory = PathLike(context.pluginWorkDirectoryURL) + #endif + return try self.createBuildCommands( - pluginWorkDirectory: context.pluginWorkDirectory, + pluginWorkDirectory: workDirectory, sourceFiles: swiftTarget.sourceFiles, tool: context.tool ) } } +// 'Path' was effectively deprecated in Swift 6 in favour of URL. ('Effectively' because all +// methods, properties, and conformances have been deprecated but the type hasn't.) This type wraps +// either depending on the compiler version. +struct PathLike: CustomStringConvertible { + #if compiler(<6.0) + typealias Value = Path + #else + typealias Value = URL + #endif + + private(set) var value: Value + + init(_ value: Value) { + self.value = value + } + + init(_ path: String) { + #if compiler(<6.0) + self.value = Path(path) + #else + self.value = URL(fileURLWithPath: path) + #endif + } + + init(_ element: FileList.Element) { + #if compiler(<6.0) + self.value = element.path + #else + self.value = element.url + #endif + } + + init(_ element: PluginContext.Tool) { + #if compiler(<6.0) + self.value = element.path + #else + self.value = element.url + #endif + } + + var description: String { + #if compiler(<6.0) + return String(describing: self.value) + #elseif canImport(Darwin) + return self.value.path() + #else + return self.value.path + #endif + } + + var lastComponent: String { + #if compiler(<6.0) + return self.value.lastComponent + #else + return self.value.lastPathComponent + #endif + } + + func removingLastComponent() -> Self { + var copy = self + #if compiler(<6.0) + copy.value = self.value.removingLastComponent() + #else + copy.value = self.value.deletingLastPathComponent() + #endif + return copy + } + + func appending(_ path: String) -> Self { + var copy = self + #if compiler(<6.0) + copy.value = self.value.appending(path) + #else + copy.value = self.value.appendingPathComponent(path) + #endif + return copy + } +} + +extension Command { + static func buildCommand( + displayName: String?, + executable: PathLike, + arguments: [String], + inputFiles: [PathLike], + outputFiles: [PathLike] + ) -> PackagePlugin.Command { + return Self.buildCommand( + displayName: displayName, + executable: executable.value, + arguments: arguments, + inputFiles: inputFiles.map { $0.value }, + outputFiles: outputFiles.map { $0.value } + ) + } +} + +extension URL { + init(_ pathLike: PathLike) { + #if compiler(<6.0) + self = URL(fileURLWithPath: "\(pathLike.value)") + #else + self = pathLike.value + #endif + } +} + #if canImport(XcodeProjectPlugin) import XcodeProjectPlugin @@ -277,8 +390,14 @@ extension GRPCSwiftPlugin: XcodeBuildToolPlugin { context: XcodePluginContext, target: XcodeTarget ) throws -> [Command] { + #if compiler(<6.0) + let workDirectory = PathLike(context.pluginWorkDirectory) + #else + let workDirectory = PathLike(URL(string: String(describing: context.pluginWorkDirectory))!) + #endif + return try self.createBuildCommands( - pluginWorkDirectory: context.pluginWorkDirectory, + pluginWorkDirectory: workDirectory, sourceFiles: target.inputFiles, tool: context.tool ) From 7f11d31011f0dff5ed5c945a5cbb7a6b629f29bb Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:25:23 +0100 Subject: [PATCH 380/580] Initial set-up for the health checking service (#1960) Motivation: This setup is needed as part of the implementation of the health checking service. Modifications: Pull in updates for the upstream protos and re-generate stubs. Update the Protos/fetch.sh script to copy over grpc-proto/grpc/health/v1/health.proto and generate the health checking stubs. Update the Protos/generate.sh script to generate the health checking service and messages for v2 and only the server. Add a target to the package manifest called GRPCHealth. Result: The stubs for the health checking service are now available. --- Package@swift-6.swift | 13 + Protos/fetch.sh | 2 + Protos/generate.sh | 11 + Protos/upstream/google/rpc/code.proto | 2 +- Protos/upstream/grpc/health/v1/health.proto | 73 + .../grpc/service_config/service_config.proto | 371 +- .../Health/Generated/health.grpc.swift | 163 + .../Services/Health/Generated/health.pb.swift | 195 + .../Configuration/Generated/code.pb.swift | 2 +- .../Generated/service_config.pb.swift | 3162 +++++++++-------- .../Generated/control.grpc.swift | 134 +- .../Generated/control.pb.swift | 2 +- 12 files changed, 2311 insertions(+), 1819 deletions(-) create mode 100644 Protos/upstream/grpc/health/v1/health.proto create mode 100644 Sources/Services/Health/Generated/health.grpc.swift create mode 100644 Sources/Services/Health/Generated/health.pb.swift diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 54f32d6ec..b36b01ede 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -798,6 +798,18 @@ extension Target { swiftSettings: [.swiftLanguageVersion(.v6)] ) } + + static var grpcHealth: Target { + .target( + name: "GRPCHealth", + dependencies: [ + .grpcCore, + .grpcProtobuf + ], + path: "Sources/Services/Health", + swiftSettings: [.swiftLanguageVersion(.v6)] + ) + } } // MARK: - Products @@ -909,6 +921,7 @@ let package = Package( .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf, .grpcProtobufCodeGen, + .grpcHealth, .interoperabilityTestImplementation, .interoperabilityTestsExecutable, .performanceWorker, diff --git a/Protos/fetch.sh b/Protos/fetch.sh index ba24d1558..5d0cee63b 100755 --- a/Protos/fetch.sh +++ b/Protos/fetch.sh @@ -34,6 +34,7 @@ rm -rf "$upstream" mkdir -p "$upstream/google" mkdir -p "$upstream/grpc/testing" mkdir -p "$upstream/grpc/core" +mkdir -p "$upstream/grpc/health/v1" # Copy over the grpc-proto protos. cp -rp "$checkouts/grpc-proto/grpc/service_config" "$upstream/grpc/service_config" @@ -47,6 +48,7 @@ cp -rp "$checkouts/grpc-proto/grpc/testing/control.proto" "$upstream/grpc/testin cp -rp "$checkouts/grpc-proto/grpc/testing/payloads.proto" "$upstream/grpc/testing/payloads.proto" cp -rp "$checkouts/grpc-proto/grpc/testing/stats.proto" "$upstream/grpc/testing/stats.proto" cp -rp "$checkouts/grpc-proto/grpc/core/stats.proto" "$upstream/grpc/core/stats.proto" +cp -rp "$checkouts/grpc-proto/grpc/health/v1/health.proto" "$upstream/grpc/health/v1/health.proto" # Copy over the googleapis protos. mkdir -p "$upstream/google/rpc" diff --git a/Protos/generate.sh b/Protos/generate.sh index 48a5be1ed..99dcf177b 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -232,6 +232,14 @@ function generate_worker_service { done } +function generate_health_service { + local proto="$here/upstream/grpc/health/v1/health.proto" + local output="$root/Sources/Services/Health/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "Client=false" "Server=true" "_V2=true" +} + #------------------------------------------------------------------------------ # Examples @@ -255,3 +263,6 @@ generate_http2_transport_tests_service # Performance worker service generate_worker_service + +# Health +generate_health_service diff --git a/Protos/upstream/google/rpc/code.proto b/Protos/upstream/google/rpc/code.proto index 7c810af40..ba8f2bf9e 100644 --- a/Protos/upstream/google/rpc/code.proto +++ b/Protos/upstream/google/rpc/code.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Protos/upstream/grpc/health/v1/health.proto b/Protos/upstream/grpc/health/v1/health.proto new file mode 100644 index 000000000..13b03f567 --- /dev/null +++ b/Protos/upstream/grpc/health/v1/health.proto @@ -0,0 +1,73 @@ +// Copyright 2015 The gRPC Authors +// +// 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. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto + +syntax = "proto3"; + +package grpc.health.v1; + +option csharp_namespace = "Grpc.Health.V1"; +option go_package = "google.golang.org/grpc/health/grpc_health_v1"; +option java_multiple_files = true; +option java_outer_classname = "HealthProto"; +option java_package = "io.grpc.health.v1"; + +message HealthCheckRequest { + string service = 1; +} + +message HealthCheckResponse { + enum ServingStatus { + UNKNOWN = 0; + SERVING = 1; + NOT_SERVING = 2; + SERVICE_UNKNOWN = 3; // Used only by the Watch method. + } + ServingStatus status = 1; +} + +// Health is gRPC's mechanism for checking whether a server is able to handle +// RPCs. Its semantics are documented in +// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. +service Health { + // Check gets the health of the specified service. If the requested service + // is unknown, the call will fail with status NOT_FOUND. If the caller does + // not specify a service name, the server should respond with its overall + // health status. + // + // Clients should set a deadline when calling Check, and can declare the + // server unhealthy if they do not receive a timely response. + // + // Check implementations should be idempotent and side effect free. + rpc Check(HealthCheckRequest) returns (HealthCheckResponse); + + // Performs a watch for the serving status of the requested service. + // The server will immediately send back a message indicating the current + // serving status. It will then subsequently send a new message whenever + // the service's serving status changes. + // + // If the requested service is unknown when the call is received, the + // server will send a message setting the serving status to + // SERVICE_UNKNOWN but will *not* terminate the call. If at some + // future point, the serving status of the service becomes known, the + // server will send a new message with the service's serving status. + // + // If the call terminates with status UNIMPLEMENTED, then clients + // should assume this method is not supported and should not retry the + // call. If the call terminates with any other status (including OK), + // clients should retry the call with appropriate exponential backoff. + rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); +} diff --git a/Protos/upstream/grpc/service_config/service_config.proto b/Protos/upstream/grpc/service_config/service_config.proto index 340c21193..3367d0c45 100644 --- a/Protos/upstream/grpc/service_config/service_config.proto +++ b/Protos/upstream/grpc/service_config/service_config.proto @@ -317,7 +317,7 @@ message OutlierDetectionLoadBalancingConfig { // If set, success rate ejections will be performed SuccessRateEjection success_rate_ejection = 5; - + // If set, failure rate ejections will be performed FailurePercentageEjection failure_percentage_ejection = 6; @@ -393,88 +393,9 @@ message XdsClusterManagerLoadBalancingPolicyConfig { // Configuration for the cds LB policy. message CdsConfig { string cluster = 1; // Required. -} - -// Represents an xDS server. -message XdsServer { - string server_uri = 1 [json_name = "server_uri"]; // Required. - - message ChannelCredentials { - string type = 1; // Required. - google.protobuf.Struct config = 2; // Optional JSON config. - } - // A list of channel creds to use. The first supported type will be used. - repeated ChannelCredentials channel_creds = 2 [json_name = "channel_creds"]; - - // A repeated list of server features. - repeated google.protobuf.Value server_features = 3 - [json_name = "server_features"]; -} - -// Configuration for xds_cluster_resolver LB policy. -message XdsClusterResolverLoadBalancingPolicyConfig { - // Describes a discovery mechanism instance. - // For EDS or LOGICAL_DNS clusters, there will be exactly one - // DiscoveryMechanism, which will describe the cluster of the parent - // CDS policy. - // For aggregate clusters, there will be one DiscoveryMechanism for each - // underlying cluster. - message DiscoveryMechanism { - // Cluster name. - string cluster = 1; - - // LRS server to send load reports to. - // If not present, load reporting will be disabled. - // If set to the empty string, load reporting will be sent to the same - // server that we obtained CDS data from. - // DEPRECATED: Use new lrs_load_reporting_server field instead. - google.protobuf.StringValue lrs_load_reporting_server_name = 2 - [deprecated=true]; - - // LRS server to send load reports to. - // If not present, load reporting will be disabled. - // Supercedes lrs_load_reporting_server_name field. - XdsServer lrs_load_reporting_server = 7; - // Maximum number of outstanding requests can be made to the upstream - // cluster. Default is 1024. - google.protobuf.UInt32Value max_concurrent_requests = 3; - - enum Type { - UNKNOWN = 0; - EDS = 1; - LOGICAL_DNS = 2; - }; - Type type = 4; - - // For type EDS only. - // EDS service name, as returned in CDS. - // May be unset if not specified in CDS. - string eds_service_name = 5; - - // For type LOGICAL_DNS only. - // DNS name to resolve in "host:port" form. - string dns_hostname = 6; - - // The configuration for outlier_detection child policies - // Within this message, the child_policy field will be ignored - OutlierDetectionLoadBalancingConfig outlier_detection = 8; - - // The configuration for xds_override_host child policy - repeated OverrideHostLoadBalancingPolicyConfig.HealthStatus override_host_status = 9; - - // Telemetry labels associated with this cluster - map telemetry_labels = 10; - } - - // Ordered list of discovery mechanisms. - // Must have at least one element. - // Results from each discovery mechanism are concatenated together in - // successive priorities. - repeated DiscoveryMechanism discovery_mechanisms = 1; - - // xDS LB policy. Will be used as the child config of the xds_cluster_impl LB policy. - repeated LoadBalancingConfig xds_lb_policy = 2; + // If true, a dynamic subscription will be started for the cluster. + bool is_dynamic = 2; } // Configuration for xds_cluster_impl LB policy. @@ -482,10 +403,15 @@ message XdsClusterImplLoadBalancingPolicyConfig { // Cluster name. Required. string cluster = 1; + // Child policy. + repeated LoadBalancingConfig child_policy = 6; + + // REMAINING FIELDS ARE DEPRECATED. + // EDS service name. // Not set if cluster is not an EDS cluster or if it does not // specify an EDS service name. - string eds_service_name = 2; + string eds_service_name = 2 [deprecated = true]; // Server to send load reports to. // If unset, no load reporting is done. @@ -498,54 +424,21 @@ message XdsClusterImplLoadBalancingPolicyConfig { // LRS server to send load reports to. // If not present, load reporting will be disabled. // Supercedes lrs_load_reporting_server_name field. - XdsServer lrs_load_reporting_server = 7; + XdsServer lrs_load_reporting_server = 7 [deprecated = true]; // Maximum number of outstanding requests can be made to the upstream cluster. // Default is 1024. - google.protobuf.UInt32Value max_concurrent_requests = 4; + google.protobuf.UInt32Value max_concurrent_requests = 4 [deprecated = true]; // Drop configuration. message DropCategory { string category = 1; uint32 requests_per_million = 2; } - repeated DropCategory drop_categories = 5; - - // Child policy. - repeated LoadBalancingConfig child_policy = 6; + repeated DropCategory drop_categories = 5 [deprecated = true]; // Telemetry labels associated with this cluster - map telemetry_labels = 8; -} - -// Configuration for eds LB policy. -message EdsLoadBalancingPolicyConfig { - // Cluster name. Required. - string cluster = 1; - - // EDS service name, as returned in CDS. - // May be unset if not specified in CDS. - string eds_service_name = 2; - - // Server to send load reports to. - // If unset, no load reporting is done. - // If set to empty string, load reporting will be sent to the same - // server as we are getting xds data from. - google.protobuf.StringValue lrs_load_reporting_server_name = 3; - - // Locality-picking policy. - // This policy's config is expected to be in the format used - // by the weighted_target policy. Note that the config should include - // an empty value for the "targets" field; that empty value will be - // replaced by one that is dynamically generated based on the EDS data. - // Optional; defaults to "weighted_target". - repeated LoadBalancingConfig locality_picking_policy = 4; - - // Endpoint-picking policy. - // This will be configured as the policy for each child in the - // locality-policy's config. - // Optional; defaults to "round_robin". - repeated LoadBalancingConfig endpoint_picking_policy = 5; + map telemetry_labels = 8 [deprecated = true]; } // Configuration for ring_hash LB policy. @@ -557,32 +450,6 @@ message RingHashLoadBalancingConfig { uint64 max_ring_size = 2; // Optional, defaults to 4096, max 8M. } -// Configuration for lrs LB policy. -message LrsLoadBalancingPolicyConfig { - // Cluster name. Required. - string cluster_name = 1; - - // EDS service name, as returned in CDS. - // May be unset if not specified in CDS. - string eds_service_name = 2; - - // Server to send load reports to. Required. - // If set to empty string, load reporting will be sent to the same - // server as we are getting xds data from. - string lrs_load_reporting_server_name = 3; - - // The locality for which this policy will report load. Required. - message Locality { - string region = 1; - string zone = 2; - string subzone = 3; - } - Locality locality = 4; - - // Endpoint-picking policy. - repeated LoadBalancingConfig child_policy = 5; -} - // Configuration for the xds_wrr_locality load balancing policy. message XdsWrrLocalityLoadBalancingPolicyConfig { repeated LoadBalancingConfig child_policy = 1; @@ -593,45 +460,20 @@ message LeastRequestLocalityLoadBalancingPolicyConfig { uint64 choice_count = 1; } - -// Configuration for the override_host LB policy. +// Configuration for the xds_override_host LB policy. message OverrideHostLoadBalancingPolicyConfig { + string cluster_name = 3; + repeated LoadBalancingConfig child_policy = 2; + enum HealthStatus { UNKNOWN = 0; HEALTHY = 1; DRAINING = 3; } - // valid health status for hosts that are considered when using // xds_override_host_experimental policy. // Default is [UNKNOWN, HEALTHY] - repeated HealthStatus override_host_status = 1; - - repeated LoadBalancingConfig child_policy = 2; -} - -// Configuration for xds LB policy. -message XdsConfig { - // Name of balancer to connect to. - string balancer_name = 1 [deprecated = true]; - // Optional. What LB policy to use for intra-locality routing. - // If unset, will use whatever algorithm is specified by the balancer. - // Multiple LB policies can be specified; clients will iterate through - // the list in order and stop at the first policy that they support. - repeated LoadBalancingConfig child_policy = 2; - // Optional. What LB policy to use in fallback mode. If not - // specified, defaults to round_robin. - // Multiple LB policies can be specified; clients will iterate through - // the list in order and stop at the first policy that they support. - repeated LoadBalancingConfig fallback_policy = 3; - // Optional. Name to use in EDS query. If not present, defaults to - // the server name from the target URI. - string eds_service_name = 4; - // LRS server to send load reports to. - // If not present, load reporting will be disabled. - // If set to the empty string, load reporting will be sent to the same - // server that we obtained CDS data from. - google.protobuf.StringValue lrs_load_reporting_server_name = 5; + repeated HealthStatus override_host_status = 1 [deprecated = true]; } // Selects LB policy and provides corresponding configuration. @@ -689,9 +531,6 @@ message LoadBalancingConfig { XdsClusterManagerLoadBalancingPolicyConfig xds_cluster_manager_experimental = 14 [json_name = "xds_cluster_manager_experimental"]; CdsConfig cds_experimental = 6 [json_name = "cds_experimental"]; - XdsClusterResolverLoadBalancingPolicyConfig - xds_cluster_resolver_experimental = 11 - [json_name = "xds_cluster_resolver_experimental"]; XdsClusterImplLoadBalancingPolicyConfig xds_cluster_impl_experimental = 12 [json_name = "xds_cluster_impl_experimental"]; OverrideHostLoadBalancingPolicyConfig override_host_experimental = 18 @@ -704,6 +543,9 @@ message LoadBalancingConfig { 17 [json_name = "least_request_experimental"]; // Deprecated xDS-related policies. + XdsClusterResolverLoadBalancingPolicyConfig + xds_cluster_resolver_experimental = 11 + [json_name = "xds_cluster_resolver_experimental", deprecated = true]; LrsLoadBalancingPolicyConfig lrs_experimental = 8 [json_name = "lrs_experimental", deprecated = true]; EdsLoadBalancingPolicyConfig eds_experimental = 7 @@ -787,3 +629,174 @@ message ServiceConfig { // next available tag: 6 } + +// +// DEPRECATED MESSAGES -- DO NOT USE +// + +// Represents an xDS server. +// Deprecated. +message XdsServer { + string server_uri = 1 [json_name = "server_uri"]; // Required. + + message ChannelCredentials { + string type = 1; // Required. + google.protobuf.Struct config = 2; // Optional JSON config. + } + // A list of channel creds to use. The first supported type will be used. + repeated ChannelCredentials channel_creds = 2 [json_name = "channel_creds"]; + + // A repeated list of server features. + repeated google.protobuf.Value server_features = 3 + [json_name = "server_features"]; +} + +// Configuration for xds_cluster_resolver LB policy. +// Deprecated. +message XdsClusterResolverLoadBalancingPolicyConfig { + // Describes a discovery mechanism instance. + // For EDS or LOGICAL_DNS clusters, there will be exactly one + // DiscoveryMechanism, which will describe the cluster of the parent + // CDS policy. + // For aggregate clusters, there will be one DiscoveryMechanism for each + // underlying cluster. + message DiscoveryMechanism { + // Cluster name. + string cluster = 1; + + // LRS server to send load reports to. + // If not present, load reporting will be disabled. + // If set to the empty string, load reporting will be sent to the same + // server that we obtained CDS data from. + // DEPRECATED: Use new lrs_load_reporting_server field instead. + google.protobuf.StringValue lrs_load_reporting_server_name = 2 + [deprecated=true]; + + // LRS server to send load reports to. + // If not present, load reporting will be disabled. + // Supercedes lrs_load_reporting_server_name field. + XdsServer lrs_load_reporting_server = 7; + + // Maximum number of outstanding requests can be made to the upstream + // cluster. Default is 1024. + google.protobuf.UInt32Value max_concurrent_requests = 3; + + enum Type { + UNKNOWN = 0; + EDS = 1; + LOGICAL_DNS = 2; + }; + Type type = 4; + + // For type EDS only. + // EDS service name, as returned in CDS. + // May be unset if not specified in CDS. + string eds_service_name = 5; + + // For type LOGICAL_DNS only. + // DNS name to resolve in "host:port" form. + string dns_hostname = 6; + + // The configuration for outlier_detection child policies + // Within this message, the child_policy field will be ignored + OutlierDetectionLoadBalancingConfig outlier_detection = 8; + + // The configuration for xds_override_host child policy + repeated OverrideHostLoadBalancingPolicyConfig.HealthStatus override_host_status = 9; + + // Telemetry labels associated with this cluster + map telemetry_labels = 10; + } + + // Ordered list of discovery mechanisms. + // Must have at least one element. + // Results from each discovery mechanism are concatenated together in + // successive priorities. + repeated DiscoveryMechanism discovery_mechanisms = 1; + + // xDS LB policy. Will be used as the child config of the xds_cluster_impl LB policy. + repeated LoadBalancingConfig xds_lb_policy = 2; +} + +// Configuration for lrs LB policy. +// Deprecated. +message LrsLoadBalancingPolicyConfig { + // Cluster name. Required. + string cluster_name = 1; + + // EDS service name, as returned in CDS. + // May be unset if not specified in CDS. + string eds_service_name = 2; + + // Server to send load reports to. Required. + // If set to empty string, load reporting will be sent to the same + // server as we are getting xds data from. + string lrs_load_reporting_server_name = 3; + + // The locality for which this policy will report load. Required. + message Locality { + string region = 1; + string zone = 2; + string subzone = 3; + } + Locality locality = 4; + + // Endpoint-picking policy. + repeated LoadBalancingConfig child_policy = 5; +} + +// Configuration for eds LB policy. +// Deprecated. +message EdsLoadBalancingPolicyConfig { + // Cluster name. Required. + string cluster = 1; + + // EDS service name, as returned in CDS. + // May be unset if not specified in CDS. + string eds_service_name = 2; + + // Server to send load reports to. + // If unset, no load reporting is done. + // If set to empty string, load reporting will be sent to the same + // server as we are getting xds data from. + google.protobuf.StringValue lrs_load_reporting_server_name = 3; + + // Locality-picking policy. + // This policy's config is expected to be in the format used + // by the weighted_target policy. Note that the config should include + // an empty value for the "targets" field; that empty value will be + // replaced by one that is dynamically generated based on the EDS data. + // Optional; defaults to "weighted_target". + repeated LoadBalancingConfig locality_picking_policy = 4; + + // Endpoint-picking policy. + // This will be configured as the policy for each child in the + // locality-policy's config. + // Optional; defaults to "round_robin". + repeated LoadBalancingConfig endpoint_picking_policy = 5; +} + +// Configuration for xds LB policy. +// Deprecated. +message XdsConfig { + // Name of balancer to connect to. + string balancer_name = 1 [deprecated = true]; + // Optional. What LB policy to use for intra-locality routing. + // If unset, will use whatever algorithm is specified by the balancer. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig child_policy = 2; + // Optional. What LB policy to use in fallback mode. If not + // specified, defaults to round_robin. + // Multiple LB policies can be specified; clients will iterate through + // the list in order and stop at the first policy that they support. + repeated LoadBalancingConfig fallback_policy = 3; + // Optional. Name to use in EDS query. If not present, defaults to + // the server name from the target URI. + string eds_service_name = 4; + // LRS server to send load reports to. + // If not present, load reporting will be disabled. + // If set to the empty string, load reporting will be sent to the same + // server that we obtained CDS data from. + google.protobuf.StringValue lrs_load_reporting_server_name = 5; +} diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift new file mode 100644 index 000000000..96d485fa3 --- /dev/null +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -0,0 +1,163 @@ +// Copyright 2015 The gRPC Authors +// +// 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. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: health.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +internal enum Grpc_Health_V1_Health { + internal enum Method { + internal enum Check { + internal typealias Input = Grpc_Health_V1_HealthCheckRequest + internal typealias Output = Grpc_Health_V1_HealthCheckResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.health.v1.Health", + method: "Check" + ) + } + internal enum Watch { + internal typealias Input = Grpc_Health_V1_HealthCheckRequest + internal typealias Output = Grpc_Health_V1_HealthCheckResponse + internal static let descriptor = MethodDescriptor( + service: "grpc.health.v1.Health", + method: "Watch" + ) + } + internal static let descriptors: [MethodDescriptor] = [ + Check.descriptor, + Watch.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias StreamingServiceProtocol = Grpc_Health_V1_HealthStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ServiceProtocol = Grpc_Health_V1_HealthServiceProtocol +} + +/// Health is gRPC's mechanism for checking whether a server is able to handle +/// RPCs. Its semantics are documented in +/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Check gets the health of the specified service. If the requested service + /// is unknown, the call will fail with status NOT_FOUND. If the caller does + /// not specify a service name, the server should respond with its overall + /// health status. + /// + /// Clients should set a deadline when calling Check, and can declare the + /// server unhealthy if they do not receive a timely response. + /// + /// Check implementations should be idempotent and side effect free. + func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + func watch(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Health_V1_Health.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Grpc_Health_V1_Health.Method.Check.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.check(request: request) + } + ) + router.registerHandler( + forMethod: Grpc_Health_V1_Health.Method.Watch.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.watch(request: request) + } + ) + } +} + +/// Health is gRPC's mechanism for checking whether a server is able to handle +/// RPCs. Its semantics are documented in +/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.StreamingServiceProtocol { + /// Check gets the health of the specified service. If the requested service + /// is unknown, the call will fail with status NOT_FOUND. If the caller does + /// not specify a service name, the server should respond with its overall + /// health status. + /// + /// Clients should set a deadline when calling Check, and can declare the + /// server unhealthy if they do not receive a timely response. + /// + /// Check implementations should be idempotent and side effect free. + func check(request: ServerRequest.Single) async throws -> ServerResponse.Single + + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + func watch(request: ServerRequest.Single) async throws -> ServerResponse.Stream +} + +/// Partial conformance to `Grpc_Health_V1_HealthStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Grpc_Health_V1_Health.ServiceProtocol { + internal func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.check(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + internal func watch(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.watch(request: ServerRequest.Single(stream: request)) + return response + } +} \ No newline at end of file diff --git a/Sources/Services/Health/Generated/health.pb.swift b/Sources/Services/Health/Generated/health.pb.swift new file mode 100644 index 000000000..2d0981eec --- /dev/null +++ b/Sources/Services/Health/Generated/health.pb.swift @@ -0,0 +1,195 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: health.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 The gRPC Authors +// +// 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. + +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct Grpc_Health_V1_HealthCheckRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var service: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Grpc_Health_V1_HealthCheckResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown + + var unknownFields = SwiftProtobuf.UnknownStorage() + + enum ServingStatus: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case serving // = 1 + case notServing // = 2 + + /// Used only by the Watch method. + case serviceUnknown // = 3 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .serving + case 2: self = .notServing + case 3: self = .serviceUnknown + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .serving: return 1 + case .notServing: return 2 + case .serviceUnknown: return 3 + case .UNRECOGNIZED(let i): return i + } + } + + } + + init() {} +} + +#if swift(>=4.2) + +extension Grpc_Health_V1_HealthCheckResponse.ServingStatus: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ + .unknown, + .serving, + .notServing, + .serviceUnknown, + ] +} + +#endif // swift(>=4.2) + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_Health_V1_HealthCheckRequest: @unchecked Sendable {} +extension Grpc_Health_V1_HealthCheckResponse: @unchecked Sendable {} +extension Grpc_Health_V1_HealthCheckResponse.ServingStatus: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.health.v1" + +extension Grpc_Health_V1_HealthCheckRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HealthCheckRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "service"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.service) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.service.isEmpty { + try visitor.visitSingularStringField(value: self.service, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Health_V1_HealthCheckRequest, rhs: Grpc_Health_V1_HealthCheckRequest) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Health_V1_HealthCheckResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HealthCheckResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "status"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.status) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.status != .unknown { + try visitor.visitSingularEnumField(value: self.status, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_Health_V1_HealthCheckResponse, rhs: Grpc_Health_V1_HealthCheckResponse) -> Bool { + if lhs.status != rhs.status {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_Health_V1_HealthCheckResponse.ServingStatus: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "SERVING"), + 2: .same(proto: "NOT_SERVING"), + 3: .same(proto: "SERVICE_UNKNOWN"), + ] +} diff --git a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift index a7c135fd0..cb135bfe0 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift @@ -7,7 +7,7 @@ // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ -// Copyright 2022 Google LLC +// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift index 06e530c04..da501cada 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift @@ -857,224 +857,14 @@ struct Grpc_ServiceConfig_CdsConfig { /// Required. var cluster: String = String() - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Represents an xDS server. -struct Grpc_ServiceConfig_XdsServer { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. - var serverUri: String = String() - - /// A list of channel creds to use. The first supported type will be used. - var channelCreds: [Grpc_ServiceConfig_XdsServer.ChannelCredentials] = [] - - /// A repeated list of server features. - var serverFeatures: [SwiftProtobuf.Google_Protobuf_Value] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - struct ChannelCredentials { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. - var type: String = String() - - /// Optional JSON config. - var config: SwiftProtobuf.Google_Protobuf_Struct { - get {return _config ?? SwiftProtobuf.Google_Protobuf_Struct()} - set {_config = newValue} - } - /// Returns true if `config` has been explicitly set. - var hasConfig: Bool {return self._config != nil} - /// Clears the value of `config`. Subsequent reads from it will return its default value. - mutating func clearConfig() {self._config = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _config: SwiftProtobuf.Google_Protobuf_Struct? = nil - } - - init() {} -} - -/// Configuration for xds_cluster_resolver LB policy. -struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Ordered list of discovery mechanisms. - /// Must have at least one element. - /// Results from each discovery mechanism are concatenated together in - /// successive priorities. - var discoveryMechanisms: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism] = [] - - /// xDS LB policy. Will be used as the child config of the xds_cluster_impl LB policy. - var xdsLbPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + /// If true, a dynamic subscription will be started for the cluster. + var isDynamic: Bool = false var unknownFields = SwiftProtobuf.UnknownStorage() - /// Describes a discovery mechanism instance. - /// For EDS or LOGICAL_DNS clusters, there will be exactly one - /// DiscoveryMechanism, which will describe the cluster of the parent - /// CDS policy. - /// For aggregate clusters, there will be one DiscoveryMechanism for each - /// underlying cluster. - struct DiscoveryMechanism { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Cluster name. - var cluster: String { - get {return _storage._cluster} - set {_uniqueStorage()._cluster = newValue} - } - - /// LRS server to send load reports to. - /// If not present, load reporting will be disabled. - /// If set to the empty string, load reporting will be sent to the same - /// server that we obtained CDS data from. - /// DEPRECATED: Use new lrs_load_reporting_server field instead. - var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { - get {return _storage._lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} - set {_uniqueStorage()._lrsLoadReportingServerName = newValue} - } - /// Returns true if `lrsLoadReportingServerName` has been explicitly set. - var hasLrsLoadReportingServerName: Bool {return _storage._lrsLoadReportingServerName != nil} - /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. - mutating func clearLrsLoadReportingServerName() {_uniqueStorage()._lrsLoadReportingServerName = nil} - - /// LRS server to send load reports to. - /// If not present, load reporting will be disabled. - /// Supercedes lrs_load_reporting_server_name field. - var lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer { - get {return _storage._lrsLoadReportingServer ?? Grpc_ServiceConfig_XdsServer()} - set {_uniqueStorage()._lrsLoadReportingServer = newValue} - } - /// Returns true if `lrsLoadReportingServer` has been explicitly set. - var hasLrsLoadReportingServer: Bool {return _storage._lrsLoadReportingServer != nil} - /// Clears the value of `lrsLoadReportingServer`. Subsequent reads from it will return its default value. - mutating func clearLrsLoadReportingServer() {_uniqueStorage()._lrsLoadReportingServer = nil} - - /// Maximum number of outstanding requests can be made to the upstream - /// cluster. Default is 1024. - var maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value { - get {return _storage._maxConcurrentRequests ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} - set {_uniqueStorage()._maxConcurrentRequests = newValue} - } - /// Returns true if `maxConcurrentRequests` has been explicitly set. - var hasMaxConcurrentRequests: Bool {return _storage._maxConcurrentRequests != nil} - /// Clears the value of `maxConcurrentRequests`. Subsequent reads from it will return its default value. - mutating func clearMaxConcurrentRequests() {_uniqueStorage()._maxConcurrentRequests = nil} - - var type: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum { - get {return _storage._type} - set {_uniqueStorage()._type = newValue} - } - - /// For type EDS only. - /// EDS service name, as returned in CDS. - /// May be unset if not specified in CDS. - var edsServiceName: String { - get {return _storage._edsServiceName} - set {_uniqueStorage()._edsServiceName = newValue} - } - - /// For type LOGICAL_DNS only. - /// DNS name to resolve in "host:port" form. - var dnsHostname: String { - get {return _storage._dnsHostname} - set {_uniqueStorage()._dnsHostname = newValue} - } - - /// The configuration for outlier_detection child policies - /// Within this message, the child_policy field will be ignored - var outlierDetection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { - get {return _storage._outlierDetection ?? Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig()} - set {_uniqueStorage()._outlierDetection = newValue} - } - /// Returns true if `outlierDetection` has been explicitly set. - var hasOutlierDetection: Bool {return _storage._outlierDetection != nil} - /// Clears the value of `outlierDetection`. Subsequent reads from it will return its default value. - mutating func clearOutlierDetection() {_uniqueStorage()._outlierDetection = nil} - - /// The configuration for xds_override_host child policy - var overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] { - get {return _storage._overrideHostStatus} - set {_uniqueStorage()._overrideHostStatus = newValue} - } - - /// Telemetry labels associated with this cluster - var telemetryLabels: Dictionary { - get {return _storage._telemetryLabels} - set {_uniqueStorage()._telemetryLabels = newValue} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - case unknown // = 0 - case eds // = 1 - case logicalDns // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .eds - case 2: self = .logicalDns - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .eds: return 1 - case .logicalDns: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - } - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance - } - init() {} } -#if swift(>=4.2) - -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum] = [ - .unknown, - .eds, - .logicalDns, - ] -} - -#endif // swift(>=4.2) - /// Configuration for xds_cluster_impl LB policy. struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { // SwiftProtobuf.Message conformance is added in an extension below. See the @@ -1084,6 +874,9 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { /// Cluster name. Required. var cluster: String = String() + /// Child policy. + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + /// EDS service name. /// Not set if cluster is not an EDS cluster or if it does not /// specify an EDS service name. @@ -1128,9 +921,6 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { var dropCategories: [Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory] = [] - /// Child policy. - var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - /// Telemetry labels associated with this cluster var telemetryLabels: Dictionary = [:] @@ -1158,125 +948,24 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { fileprivate var _maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil } -/// Configuration for eds LB policy. -struct Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig { +/// Configuration for ring_hash LB policy. +struct Grpc_ServiceConfig_RingHashLoadBalancingConfig { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - /// Cluster name. Required. - var cluster: String = String() + /// A client-side option will cap these values to 4096. If either of these + /// values are greater than the client-side cap, they will be treated + /// as the client-side cap value. + var minRingSize: UInt64 = 0 - /// EDS service name, as returned in CDS. - /// May be unset if not specified in CDS. - var edsServiceName: String = String() + /// Optional, defaults to 4096, max 8M. + var maxRingSize: UInt64 = 0 - /// Server to send load reports to. - /// If unset, no load reporting is done. - /// If set to empty string, load reporting will be sent to the same - /// server as we are getting xds data from. - var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { - get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} - set {_lrsLoadReportingServerName = newValue} - } - /// Returns true if `lrsLoadReportingServerName` has been explicitly set. - var hasLrsLoadReportingServerName: Bool {return self._lrsLoadReportingServerName != nil} - /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. - mutating func clearLrsLoadReportingServerName() {self._lrsLoadReportingServerName = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() - /// Locality-picking policy. - /// This policy's config is expected to be in the format used - /// by the weighted_target policy. Note that the config should include - /// an empty value for the "targets" field; that empty value will be - /// replaced by one that is dynamically generated based on the EDS data. - /// Optional; defaults to "weighted_target". - var localityPickingPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - - /// Endpoint-picking policy. - /// This will be configured as the policy for each child in the - /// locality-policy's config. - /// Optional; defaults to "round_robin". - var endpointPickingPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil -} - -/// Configuration for ring_hash LB policy. -struct Grpc_ServiceConfig_RingHashLoadBalancingConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// A client-side option will cap these values to 4096. If either of these - /// values are greater than the client-side cap, they will be treated - /// as the client-side cap value. - var minRingSize: UInt64 = 0 - - /// Optional, defaults to 4096, max 8M. - var maxRingSize: UInt64 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Configuration for lrs LB policy. -struct Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Cluster name. Required. - var clusterName: String = String() - - /// EDS service name, as returned in CDS. - /// May be unset if not specified in CDS. - var edsServiceName: String = String() - - /// Server to send load reports to. Required. - /// If set to empty string, load reporting will be sent to the same - /// server as we are getting xds data from. - var lrsLoadReportingServerName: String = String() - - var locality: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality { - get {return _locality ?? Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality()} - set {_locality = newValue} - } - /// Returns true if `locality` has been explicitly set. - var hasLocality: Bool {return self._locality != nil} - /// Clears the value of `locality`. Subsequent reads from it will return its default value. - mutating func clearLocality() {self._locality = nil} - - /// Endpoint-picking policy. - var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The locality for which this policy will report load. Required. - struct Locality { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var region: String = String() - - var zone: String = String() - - var subzone: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - init() {} - - fileprivate var _locality: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality? = nil -} + init() {} +} /// Configuration for the xds_wrr_locality load balancing policy. struct Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig { @@ -1304,19 +993,21 @@ struct Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig { init() {} } -/// Configuration for the override_host LB policy. +/// Configuration for the xds_override_host LB policy. struct Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. + var clusterName: String = String() + + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + /// valid health status for hosts that are considered when using /// xds_override_host_experimental policy. /// Default is [UNKNOWN, HEALTHY] var overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] - var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - var unknownFields = SwiftProtobuf.UnknownStorage() enum HealthStatus: SwiftProtobuf.Enum { @@ -1366,51 +1057,6 @@ extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: #endif // swift(>=4.2) -/// Configuration for xds LB policy. -struct Grpc_ServiceConfig_XdsConfig { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Name of balancer to connect to. - var balancerName: String = String() - - /// Optional. What LB policy to use for intra-locality routing. - /// If unset, will use whatever algorithm is specified by the balancer. - /// Multiple LB policies can be specified; clients will iterate through - /// the list in order and stop at the first policy that they support. - var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - - /// Optional. What LB policy to use in fallback mode. If not - /// specified, defaults to round_robin. - /// Multiple LB policies can be specified; clients will iterate through - /// the list in order and stop at the first policy that they support. - var fallbackPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - - /// Optional. Name to use in EDS query. If not present, defaults to - /// the server name from the target URI. - var edsServiceName: String = String() - - /// LRS server to send load reports to. - /// If not present, load reporting will be disabled. - /// If set to the empty string, load reporting will be sent to the same - /// server that we obtained CDS data from. - var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { - get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} - set {_lrsLoadReportingServerName = newValue} - } - /// Returns true if `lrsLoadReportingServerName` has been explicitly set. - var hasLrsLoadReportingServerName: Bool {return self._lrsLoadReportingServerName != nil} - /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. - mutating func clearLrsLoadReportingServerName() {self._lrsLoadReportingServerName = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil -} - /// Selects LB policy and provides corresponding configuration. /// /// In general, all instances of this field should be repeated. Clients will @@ -1514,14 +1160,6 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { set {policy = .cdsExperimental(newValue)} } - var xdsClusterResolverExperimental: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { - get { - if case .xdsClusterResolverExperimental(let v)? = policy {return v} - return Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig() - } - set {policy = .xdsClusterResolverExperimental(newValue)} - } - var xdsClusterImplExperimental: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { get { if case .xdsClusterImplExperimental(let v)? = policy {return v} @@ -1563,6 +1201,14 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { } /// Deprecated xDS-related policies. + var xdsClusterResolverExperimental: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { + get { + if case .xdsClusterResolverExperimental(let v)? = policy {return v} + return Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig() + } + set {policy = .xdsClusterResolverExperimental(newValue)} + } + var lrsExperimental: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { get { if case .lrsExperimental(let v)? = policy {return v} @@ -1613,13 +1259,13 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { /// xDS-based load balancing. case xdsClusterManagerExperimental(Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig) case cdsExperimental(Grpc_ServiceConfig_CdsConfig) - case xdsClusterResolverExperimental(Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig) case xdsClusterImplExperimental(Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig) case overrideHostExperimental(Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig) case xdsWrrLocalityExperimental(Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig) case ringHashExperimental(Grpc_ServiceConfig_RingHashLoadBalancingConfig) case leastRequestExperimental(Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig) /// Deprecated xDS-related policies. + case xdsClusterResolverExperimental(Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig) case lrsExperimental(Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig) case edsExperimental(Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig) case xds(Grpc_ServiceConfig_XdsConfig) @@ -1671,10 +1317,6 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { guard case .cdsExperimental(let l) = lhs, case .cdsExperimental(let r) = rhs else { preconditionFailure() } return l == r }() - case (.xdsClusterResolverExperimental, .xdsClusterResolverExperimental): return { - guard case .xdsClusterResolverExperimental(let l) = lhs, case .xdsClusterResolverExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() case (.xdsClusterImplExperimental, .xdsClusterImplExperimental): return { guard case .xdsClusterImplExperimental(let l) = lhs, case .xdsClusterImplExperimental(let r) = rhs else { preconditionFailure() } return l == r @@ -1695,6 +1337,10 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { guard case .leastRequestExperimental(let l) = lhs, case .leastRequestExperimental(let r) = rhs else { preconditionFailure() } return l == r }() + case (.xdsClusterResolverExperimental, .xdsClusterResolverExperimental): return { + guard case .xdsClusterResolverExperimental(let l) = lhs, case .xdsClusterResolverExperimental(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.lrsExperimental, .lrsExperimental): return { guard case .lrsExperimental(let l) = lhs, case .lrsExperimental(let r) = rhs else { preconditionFailure() } return l == r @@ -1879,443 +1525,468 @@ extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: CaseIterable { #endif // swift(>=4.2) -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_ServiceConfig_MethodConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.Name: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.HedgingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_PickFirstConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_RoundRobinConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_WeightedRoundRobinLbConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection: @unchecked Sendable {} -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection: @unchecked Sendable {} -extension Grpc_ServiceConfig_GrpcLbConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child: @unchecked Sendable {} -extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: @unchecked Sendable {} -extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: @unchecked Sendable {} -extension Grpc_ServiceConfig_CdsConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsServer: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory: @unchecked Sendable {} -extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LoadBalancingConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) +/// Represents an xDS server. +/// Deprecated. +struct Grpc_ServiceConfig_XdsServer { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. -// MARK: - Code below here is support for the SwiftProtobuf runtime. + /// Required. + var serverUri: String = String() -fileprivate let _protobuf_package = "grpc.service_config" + /// A list of channel creds to use. The first supported type will be used. + var channelCreds: [Grpc_ServiceConfig_XdsServer.ChannelCredentials] = [] -extension Grpc_ServiceConfig_MethodConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".MethodConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .standard(proto: "wait_for_ready"), - 3: .same(proto: "timeout"), - 4: .standard(proto: "max_request_message_bytes"), - 5: .standard(proto: "max_response_message_bytes"), - 6: .standard(proto: "retry_policy"), - 7: .standard(proto: "hedging_policy"), - ] + /// A repeated list of server features. + var serverFeatures: [SwiftProtobuf.Google_Protobuf_Value] = [] - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.name) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._waitForReady) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._timeout) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._maxRequestMessageBytes) }() - case 5: try { try decoder.decodeSingularMessageField(value: &self._maxResponseMessageBytes) }() - case 6: try { - var v: Grpc_ServiceConfig_MethodConfig.RetryPolicy? - var hadOneofValue = false - if let current = self.retryOrHedgingPolicy { - hadOneofValue = true - if case .retryPolicy(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.retryOrHedgingPolicy = .retryPolicy(v) - } - }() - case 7: try { - var v: Grpc_ServiceConfig_MethodConfig.HedgingPolicy? - var hadOneofValue = false - if let current = self.retryOrHedgingPolicy { - hadOneofValue = true - if case .hedgingPolicy(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.retryOrHedgingPolicy = .hedgingPolicy(v) - } - }() - default: break - } - } - } + var unknownFields = SwiftProtobuf.UnknownStorage() - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitRepeatedMessageField(value: self.name, fieldNumber: 1) - } - try { if let v = self._waitForReady { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = self._timeout { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._maxRequestMessageBytes { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - try { if let v = self._maxResponseMessageBytes { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - } }() - switch self.retryOrHedgingPolicy { - case .retryPolicy?: try { - guard case .retryPolicy(let v)? = self.retryOrHedgingPolicy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .hedgingPolicy?: try { - guard case .hedgingPolicy(let v)? = self.retryOrHedgingPolicy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - }() - case nil: break + struct ChannelCredentials { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Required. + var type: String = String() + + /// Optional JSON config. + var config: SwiftProtobuf.Google_Protobuf_Struct { + get {return _config ?? SwiftProtobuf.Google_Protobuf_Struct()} + set {_config = newValue} } - try unknownFields.traverse(visitor: &visitor) - } + /// Returns true if `config` has been explicitly set. + var hasConfig: Bool {return self._config != nil} + /// Clears the value of `config`. Subsequent reads from it will return its default value. + mutating func clearConfig() {self._config = nil} - static func ==(lhs: Grpc_ServiceConfig_MethodConfig, rhs: Grpc_ServiceConfig_MethodConfig) -> Bool { - if lhs.name != rhs.name {return false} - if lhs._waitForReady != rhs._waitForReady {return false} - if lhs._timeout != rhs._timeout {return false} - if lhs._maxRequestMessageBytes != rhs._maxRequestMessageBytes {return false} - if lhs._maxResponseMessageBytes != rhs._maxResponseMessageBytes {return false} - if lhs.retryOrHedgingPolicy != rhs.retryOrHedgingPolicy {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _config: SwiftProtobuf.Google_Protobuf_Struct? = nil } + + init() {} } -extension Grpc_ServiceConfig_MethodConfig.Name: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".Name" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "service"), - 2: .same(proto: "method"), - ] +/// Configuration for xds_cluster_resolver LB policy. +/// Deprecated. +struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.service) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.method) }() - default: break - } - } - } + /// Ordered list of discovery mechanisms. + /// Must have at least one element. + /// Results from each discovery mechanism are concatenated together in + /// successive priorities. + var discoveryMechanisms: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism] = [] - func traverse(visitor: inout V) throws { - if !self.service.isEmpty { - try visitor.visitSingularStringField(value: self.service, fieldNumber: 1) - } - if !self.method.isEmpty { - try visitor.visitSingularStringField(value: self.method, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } + /// xDS LB policy. Will be used as the child config of the xds_cluster_impl LB policy. + var xdsLbPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - static func ==(lhs: Grpc_ServiceConfig_MethodConfig.Name, rhs: Grpc_ServiceConfig_MethodConfig.Name) -> Bool { - if lhs.service != rhs.service {return false} - if lhs.method != rhs.method {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} + var unknownFields = SwiftProtobuf.UnknownStorage() -extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".RetryPolicy" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "max_attempts"), - 2: .standard(proto: "initial_backoff"), - 3: .standard(proto: "max_backoff"), - 4: .standard(proto: "backoff_multiplier"), - 5: .standard(proto: "retryable_status_codes"), - ] + /// Describes a discovery mechanism instance. + /// For EDS or LOGICAL_DNS clusters, there will be exactly one + /// DiscoveryMechanism, which will describe the cluster of the parent + /// CDS policy. + /// For aggregate clusters, there will be one DiscoveryMechanism for each + /// underlying cluster. + struct DiscoveryMechanism { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxAttempts) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._initialBackoff) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._maxBackoff) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.backoffMultiplier) }() - case 5: try { try decoder.decodeRepeatedEnumField(value: &self.retryableStatusCodes) }() - default: break - } + /// Cluster name. + var cluster: String { + get {return _storage._cluster} + set {_uniqueStorage()._cluster = newValue} } - } - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.maxAttempts != 0 { - try visitor.visitSingularUInt32Field(value: self.maxAttempts, fieldNumber: 1) - } - try { if let v = self._initialBackoff { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = self._maxBackoff { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if self.backoffMultiplier != 0 { - try visitor.visitSingularFloatField(value: self.backoffMultiplier, fieldNumber: 4) + /// LRS server to send load reports to. + /// If not present, load reporting will be disabled. + /// If set to the empty string, load reporting will be sent to the same + /// server that we obtained CDS data from. + /// DEPRECATED: Use new lrs_load_reporting_server field instead. + var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _storage._lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_uniqueStorage()._lrsLoadReportingServerName = newValue} } - if !self.retryableStatusCodes.isEmpty { - try visitor.visitPackedEnumField(value: self.retryableStatusCodes, fieldNumber: 5) + /// Returns true if `lrsLoadReportingServerName` has been explicitly set. + var hasLrsLoadReportingServerName: Bool {return _storage._lrsLoadReportingServerName != nil} + /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServerName() {_uniqueStorage()._lrsLoadReportingServerName = nil} + + /// LRS server to send load reports to. + /// If not present, load reporting will be disabled. + /// Supercedes lrs_load_reporting_server_name field. + var lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer { + get {return _storage._lrsLoadReportingServer ?? Grpc_ServiceConfig_XdsServer()} + set {_uniqueStorage()._lrsLoadReportingServer = newValue} } - try unknownFields.traverse(visitor: &visitor) - } + /// Returns true if `lrsLoadReportingServer` has been explicitly set. + var hasLrsLoadReportingServer: Bool {return _storage._lrsLoadReportingServer != nil} + /// Clears the value of `lrsLoadReportingServer`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServer() {_uniqueStorage()._lrsLoadReportingServer = nil} - static func ==(lhs: Grpc_ServiceConfig_MethodConfig.RetryPolicy, rhs: Grpc_ServiceConfig_MethodConfig.RetryPolicy) -> Bool { - if lhs.maxAttempts != rhs.maxAttempts {return false} - if lhs._initialBackoff != rhs._initialBackoff {return false} - if lhs._maxBackoff != rhs._maxBackoff {return false} - if lhs.backoffMultiplier != rhs.backoffMultiplier {return false} - if lhs.retryableStatusCodes != rhs.retryableStatusCodes {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} + /// Maximum number of outstanding requests can be made to the upstream + /// cluster. Default is 1024. + var maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value { + get {return _storage._maxConcurrentRequests ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} + set {_uniqueStorage()._maxConcurrentRequests = newValue} + } + /// Returns true if `maxConcurrentRequests` has been explicitly set. + var hasMaxConcurrentRequests: Bool {return _storage._maxConcurrentRequests != nil} + /// Clears the value of `maxConcurrentRequests`. Subsequent reads from it will return its default value. + mutating func clearMaxConcurrentRequests() {_uniqueStorage()._maxConcurrentRequests = nil} -extension Grpc_ServiceConfig_MethodConfig.HedgingPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".HedgingPolicy" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "max_attempts"), - 2: .standard(proto: "hedging_delay"), - 3: .standard(proto: "non_fatal_status_codes"), - ] + var type: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum { + get {return _storage._type} + set {_uniqueStorage()._type = newValue} + } - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxAttempts) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._hedgingDelay) }() - case 3: try { try decoder.decodeRepeatedEnumField(value: &self.nonFatalStatusCodes) }() - default: break - } + /// For type EDS only. + /// EDS service name, as returned in CDS. + /// May be unset if not specified in CDS. + var edsServiceName: String { + get {return _storage._edsServiceName} + set {_uniqueStorage()._edsServiceName = newValue} } - } - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.maxAttempts != 0 { - try visitor.visitSingularUInt32Field(value: self.maxAttempts, fieldNumber: 1) - } - try { if let v = self._hedgingDelay { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if !self.nonFatalStatusCodes.isEmpty { - try visitor.visitPackedEnumField(value: self.nonFatalStatusCodes, fieldNumber: 3) + /// For type LOGICAL_DNS only. + /// DNS name to resolve in "host:port" form. + var dnsHostname: String { + get {return _storage._dnsHostname} + set {_uniqueStorage()._dnsHostname = newValue} } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_ServiceConfig_MethodConfig.HedgingPolicy, rhs: Grpc_ServiceConfig_MethodConfig.HedgingPolicy) -> Bool { - if lhs.maxAttempts != rhs.maxAttempts {return false} - if lhs._hedgingDelay != rhs._hedgingDelay {return false} - if lhs.nonFatalStatusCodes != rhs.nonFatalStatusCodes {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} -extension Grpc_ServiceConfig_PickFirstConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PickFirstConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "shuffle_address_list"), - ] + /// The configuration for outlier_detection child policies + /// Within this message, the child_policy field will be ignored + var outlierDetection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { + get {return _storage._outlierDetection ?? Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig()} + set {_uniqueStorage()._outlierDetection = newValue} + } + /// Returns true if `outlierDetection` has been explicitly set. + var hasOutlierDetection: Bool {return _storage._outlierDetection != nil} + /// Clears the value of `outlierDetection`. Subsequent reads from it will return its default value. + mutating func clearOutlierDetection() {_uniqueStorage()._outlierDetection = nil} - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.shuffleAddressList) }() - default: break - } + /// The configuration for xds_override_host child policy + var overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] { + get {return _storage._overrideHostStatus} + set {_uniqueStorage()._overrideHostStatus = newValue} } - } - func traverse(visitor: inout V) throws { - if self.shuffleAddressList != false { - try visitor.visitSingularBoolField(value: self.shuffleAddressList, fieldNumber: 1) + /// Telemetry labels associated with this cluster + var telemetryLabels: Dictionary { + get {return _storage._telemetryLabels} + set {_uniqueStorage()._telemetryLabels = newValue} } - try unknownFields.traverse(visitor: &visitor) - } - static func ==(lhs: Grpc_ServiceConfig_PickFirstConfig, rhs: Grpc_ServiceConfig_PickFirstConfig) -> Bool { - if lhs.shuffleAddressList != rhs.shuffleAddressList {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} + var unknownFields = SwiftProtobuf.UnknownStorage() -extension Grpc_ServiceConfig_RoundRobinConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".RoundRobinConfig" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() + enum TypeEnum: SwiftProtobuf.Enum { + typealias RawValue = Int + case unknown // = 0 + case eds // = 1 + case logicalDns // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .eds + case 2: self = .logicalDns + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .eds: return 1 + case .logicalDns: return 2 + case .UNRECOGNIZED(let i): return i + } + } - mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { } - } - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } + init() {} - static func ==(lhs: Grpc_ServiceConfig_RoundRobinConfig, rhs: Grpc_ServiceConfig_RoundRobinConfig) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true + fileprivate var _storage = _StorageClass.defaultInstance } + + init() {} } -extension Grpc_ServiceConfig_WeightedRoundRobinLbConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".WeightedRoundRobinLbConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "enable_oob_load_report"), - 2: .standard(proto: "oob_reporting_period"), - 3: .standard(proto: "blackout_period"), - 4: .standard(proto: "weight_expiration_period"), - 5: .standard(proto: "weight_update_period"), - 6: .standard(proto: "error_utilization_penalty"), +#if swift(>=4.2) + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum] = [ + .unknown, + .eds, + .logicalDns, ] +} - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._enableOobLoadReport) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._oobReportingPeriod) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._blackoutPeriod) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._weightExpirationPeriod) }() - case 5: try { try decoder.decodeSingularMessageField(value: &self._weightUpdatePeriod) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._errorUtilizationPenalty) }() - default: break - } - } - } +#endif // swift(>=4.2) - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._enableOobLoadReport { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._oobReportingPeriod { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = self._blackoutPeriod { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._weightExpirationPeriod { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - try { if let v = self._weightUpdatePeriod { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - } }() - try { if let v = self._errorUtilizationPenalty { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try unknownFields.traverse(visitor: &visitor) +/// Configuration for lrs LB policy. +/// Deprecated. +struct Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Cluster name. Required. + var clusterName: String = String() + + /// EDS service name, as returned in CDS. + /// May be unset if not specified in CDS. + var edsServiceName: String = String() + + /// Server to send load reports to. Required. + /// If set to empty string, load reporting will be sent to the same + /// server as we are getting xds data from. + var lrsLoadReportingServerName: String = String() + + var locality: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality { + get {return _locality ?? Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality()} + set {_locality = newValue} } + /// Returns true if `locality` has been explicitly set. + var hasLocality: Bool {return self._locality != nil} + /// Clears the value of `locality`. Subsequent reads from it will return its default value. + mutating func clearLocality() {self._locality = nil} - static func ==(lhs: Grpc_ServiceConfig_WeightedRoundRobinLbConfig, rhs: Grpc_ServiceConfig_WeightedRoundRobinLbConfig) -> Bool { - if lhs._enableOobLoadReport != rhs._enableOobLoadReport {return false} - if lhs._oobReportingPeriod != rhs._oobReportingPeriod {return false} - if lhs._blackoutPeriod != rhs._blackoutPeriod {return false} - if lhs._weightExpirationPeriod != rhs._weightExpirationPeriod {return false} - if lhs._weightUpdatePeriod != rhs._weightUpdatePeriod {return false} - if lhs._errorUtilizationPenalty != rhs._errorUtilizationPenalty {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true + /// Endpoint-picking policy. + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + /// The locality for which this policy will report load. Required. + struct Locality { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var region: String = String() + + var zone: String = String() + + var subzone: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} } + + init() {} + + fileprivate var _locality: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality? = nil } -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".OutlierDetectionLoadBalancingConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "interval"), - 2: .standard(proto: "base_ejection_time"), - 3: .standard(proto: "max_ejection_time"), - 4: .standard(proto: "max_ejection_percent"), - 5: .standard(proto: "success_rate_ejection"), - 6: .standard(proto: "failure_percentage_ejection"), - 13: .standard(proto: "child_policy"), - ] +/// Configuration for eds LB policy. +/// Deprecated. +struct Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 + /// Cluster name. Required. + var cluster: String = String() + + /// EDS service name, as returned in CDS. + /// May be unset if not specified in CDS. + var edsServiceName: String = String() + + /// Server to send load reports to. + /// If unset, no load reporting is done. + /// If set to empty string, load reporting will be sent to the same + /// server as we are getting xds data from. + var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_lrsLoadReportingServerName = newValue} + } + /// Returns true if `lrsLoadReportingServerName` has been explicitly set. + var hasLrsLoadReportingServerName: Bool {return self._lrsLoadReportingServerName != nil} + /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServerName() {self._lrsLoadReportingServerName = nil} + + /// Locality-picking policy. + /// This policy's config is expected to be in the format used + /// by the weighted_target policy. Note that the config should include + /// an empty value for the "targets" field; that empty value will be + /// replaced by one that is dynamically generated based on the EDS data. + /// Optional; defaults to "weighted_target". + var localityPickingPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Endpoint-picking policy. + /// This will be configured as the policy for each child in the + /// locality-policy's config. + /// Optional; defaults to "round_robin". + var endpointPickingPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil +} + +/// Configuration for xds LB policy. +/// Deprecated. +struct Grpc_ServiceConfig_XdsConfig { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Name of balancer to connect to. + var balancerName: String = String() + + /// Optional. What LB policy to use for intra-locality routing. + /// If unset, will use whatever algorithm is specified by the balancer. + /// Multiple LB policies can be specified; clients will iterate through + /// the list in order and stop at the first policy that they support. + var childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Optional. What LB policy to use in fallback mode. If not + /// specified, defaults to round_robin. + /// Multiple LB policies can be specified; clients will iterate through + /// the list in order and stop at the first policy that they support. + var fallbackPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + + /// Optional. Name to use in EDS query. If not present, defaults to + /// the server name from the target URI. + var edsServiceName: String = String() + + /// LRS server to send load reports to. + /// If not present, load reporting will be disabled. + /// If set to the empty string, load reporting will be sent to the same + /// server that we obtained CDS data from. + var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { + get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} + set {_lrsLoadReportingServerName = newValue} + } + /// Returns true if `lrsLoadReportingServerName` has been explicitly set. + var hasLrsLoadReportingServerName: Bool {return self._lrsLoadReportingServerName != nil} + /// Clears the value of `lrsLoadReportingServerName`. Subsequent reads from it will return its default value. + mutating func clearLrsLoadReportingServerName() {self._lrsLoadReportingServerName = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Grpc_ServiceConfig_MethodConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.Name: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_MethodConfig.HedgingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_PickFirstConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_RoundRobinConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_WeightedRoundRobinLbConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection: @unchecked Sendable {} +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection: @unchecked Sendable {} +extension Grpc_ServiceConfig_GrpcLbConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child: @unchecked Sendable {} +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: @unchecked Sendable {} +extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: @unchecked Sendable {} +extension Grpc_ServiceConfig_CdsConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory: @unchecked Sendable {} +extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: @unchecked Sendable {} +extension Grpc_ServiceConfig_LoadBalancingConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: @unchecked Sendable {} +extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsServer: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: @unchecked Sendable {} +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: @unchecked Sendable {} +extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: @unchecked Sendable {} +extension Grpc_ServiceConfig_XdsConfig: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "grpc.service_config" + +extension Grpc_ServiceConfig_MethodConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MethodConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 2: .standard(proto: "wait_for_ready"), + 3: .same(proto: "timeout"), + 4: .standard(proto: "max_request_message_bytes"), + 5: .standard(proto: "max_response_message_bytes"), + 6: .standard(proto: "retry_policy"), + 7: .standard(proto: "hedging_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._interval) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._baseEjectionTime) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._maxEjectionTime) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._maxEjectionPercent) }() - case 5: try { try decoder.decodeSingularMessageField(value: &self._successRateEjection) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._failurePercentageEjection) }() - case 13: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.name) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._waitForReady) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._timeout) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._maxRequestMessageBytes) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._maxResponseMessageBytes) }() + case 6: try { + var v: Grpc_ServiceConfig_MethodConfig.RetryPolicy? + var hadOneofValue = false + if let current = self.retryOrHedgingPolicy { + hadOneofValue = true + if case .retryPolicy(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.retryOrHedgingPolicy = .retryPolicy(v) + } + }() + case 7: try { + var v: Grpc_ServiceConfig_MethodConfig.HedgingPolicy? + var hadOneofValue = false + if let current = self.retryOrHedgingPolicy { + hadOneofValue = true + if case .hedgingPolicy(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.retryOrHedgingPolicy = .hedgingPolicy(v) + } + }() default: break } } @@ -2326,50 +1997,52 @@ extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: SwiftProtobuf. // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._interval { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._baseEjectionTime { + if !self.name.isEmpty { + try visitor.visitRepeatedMessageField(value: self.name, fieldNumber: 1) + } + try { if let v = self._waitForReady { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() - try { if let v = self._maxEjectionTime { + try { if let v = self._timeout { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - try { if let v = self._maxEjectionPercent { + try { if let v = self._maxRequestMessageBytes { try visitor.visitSingularMessageField(value: v, fieldNumber: 4) } }() - try { if let v = self._successRateEjection { + try { if let v = self._maxResponseMessageBytes { try visitor.visitSingularMessageField(value: v, fieldNumber: 5) } }() - try { if let v = self._failurePercentageEjection { + switch self.retryOrHedgingPolicy { + case .retryPolicy?: try { + guard case .retryPolicy(let v)? = self.retryOrHedgingPolicy else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - if !self.childPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 13) + }() + case .hedgingPolicy?: try { + guard case .hedgingPolicy(let v)? = self.retryOrHedgingPolicy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case nil: break } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig) -> Bool { - if lhs._interval != rhs._interval {return false} - if lhs._baseEjectionTime != rhs._baseEjectionTime {return false} - if lhs._maxEjectionTime != rhs._maxEjectionTime {return false} - if lhs._maxEjectionPercent != rhs._maxEjectionPercent {return false} - if lhs._successRateEjection != rhs._successRateEjection {return false} - if lhs._failurePercentageEjection != rhs._failurePercentageEjection {return false} - if lhs.childPolicy != rhs.childPolicy {return false} + static func ==(lhs: Grpc_ServiceConfig_MethodConfig, rhs: Grpc_ServiceConfig_MethodConfig) -> Bool { + if lhs.name != rhs.name {return false} + if lhs._waitForReady != rhs._waitForReady {return false} + if lhs._timeout != rhs._timeout {return false} + if lhs._maxRequestMessageBytes != rhs._maxRequestMessageBytes {return false} + if lhs._maxResponseMessageBytes != rhs._maxResponseMessageBytes {return false} + if lhs.retryOrHedgingPolicy != rhs.retryOrHedgingPolicy {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.protoMessageName + ".SuccessRateEjection" +extension Grpc_ServiceConfig_MethodConfig.Name: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".Name" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "stdev_factor"), - 2: .standard(proto: "enforcement_percentage"), - 3: .standard(proto: "minimum_hosts"), - 4: .standard(proto: "request_volume"), + 1: .same(proto: "service"), + 2: .same(proto: "method"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2378,52 +2051,39 @@ extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjec // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._stdevFactor) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._enforcementPercentage) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._minimumHosts) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._requestVolume) }() + case 1: try { try decoder.decodeSingularStringField(value: &self.service) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.method) }() default: break } } } func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._stdevFactor { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._enforcementPercentage { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = self._minimumHosts { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._requestVolume { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() + if !self.service.isEmpty { + try visitor.visitSingularStringField(value: self.service, fieldNumber: 1) + } + if !self.method.isEmpty { + try visitor.visitSingularStringField(value: self.method, fieldNumber: 2) + } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection) -> Bool { - if lhs._stdevFactor != rhs._stdevFactor {return false} - if lhs._enforcementPercentage != rhs._enforcementPercentage {return false} - if lhs._minimumHosts != rhs._minimumHosts {return false} - if lhs._requestVolume != rhs._requestVolume {return false} + static func ==(lhs: Grpc_ServiceConfig_MethodConfig.Name, rhs: Grpc_ServiceConfig_MethodConfig.Name) -> Bool { + if lhs.service != rhs.service {return false} + if lhs.method != rhs.method {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.protoMessageName + ".FailurePercentageEjection" +extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".RetryPolicy" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "threshold"), - 2: .standard(proto: "enforcement_percentage"), - 3: .standard(proto: "minimum_hosts"), - 4: .standard(proto: "request_volume"), + 1: .standard(proto: "max_attempts"), + 2: .standard(proto: "initial_backoff"), + 3: .standard(proto: "max_backoff"), + 4: .standard(proto: "backoff_multiplier"), + 5: .standard(proto: "retryable_status_codes"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2432,10 +2092,11 @@ extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercenta // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._threshold) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._enforcementPercentage) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._minimumHosts) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._requestVolume) }() + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxAttempts) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._initialBackoff) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._maxBackoff) }() + case 4: try { try decoder.decodeSingularFloatField(value: &self.backoffMultiplier) }() + case 5: try { try decoder.decodeRepeatedEnumField(value: &self.retryableStatusCodes) }() default: break } } @@ -2446,37 +2107,41 @@ extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercenta // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._threshold { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._enforcementPercentage { + if self.maxAttempts != 0 { + try visitor.visitSingularUInt32Field(value: self.maxAttempts, fieldNumber: 1) + } + try { if let v = self._initialBackoff { try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() - try { if let v = self._minimumHosts { + try { if let v = self._maxBackoff { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - try { if let v = self._requestVolume { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() + if self.backoffMultiplier != 0 { + try visitor.visitSingularFloatField(value: self.backoffMultiplier, fieldNumber: 4) + } + if !self.retryableStatusCodes.isEmpty { + try visitor.visitPackedEnumField(value: self.retryableStatusCodes, fieldNumber: 5) + } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection) -> Bool { - if lhs._threshold != rhs._threshold {return false} - if lhs._enforcementPercentage != rhs._enforcementPercentage {return false} - if lhs._minimumHosts != rhs._minimumHosts {return false} - if lhs._requestVolume != rhs._requestVolume {return false} + static func ==(lhs: Grpc_ServiceConfig_MethodConfig.RetryPolicy, rhs: Grpc_ServiceConfig_MethodConfig.RetryPolicy) -> Bool { + if lhs.maxAttempts != rhs.maxAttempts {return false} + if lhs._initialBackoff != rhs._initialBackoff {return false} + if lhs._maxBackoff != rhs._maxBackoff {return false} + if lhs.backoffMultiplier != rhs.backoffMultiplier {return false} + if lhs.retryableStatusCodes != rhs.retryableStatusCodes {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_GrpcLbConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".GrpcLbConfig" +extension Grpc_ServiceConfig_MethodConfig.HedgingPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_MethodConfig.protoMessageName + ".HedgingPolicy" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "child_policy"), - 2: .standard(proto: "service_name"), - 3: .standard(proto: "initial_fallback_timeout"), + 1: .standard(proto: "max_attempts"), + 2: .standard(proto: "hedging_delay"), + 3: .standard(proto: "non_fatal_status_codes"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2485,9 +2150,9 @@ extension Grpc_ServiceConfig_GrpcLbConfig: SwiftProtobuf.Message, SwiftProtobuf. // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.serviceName) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._initialFallbackTimeout) }() + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxAttempts) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._hedgingDelay) }() + case 3: try { try decoder.decodeRepeatedEnumField(value: &self.nonFatalStatusCodes) }() default: break } } @@ -2498,32 +2163,31 @@ extension Grpc_ServiceConfig_GrpcLbConfig: SwiftProtobuf.Message, SwiftProtobuf. // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - if !self.childPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 1) - } - if !self.serviceName.isEmpty { - try visitor.visitSingularStringField(value: self.serviceName, fieldNumber: 2) + if self.maxAttempts != 0 { + try visitor.visitSingularUInt32Field(value: self.maxAttempts, fieldNumber: 1) } - try { if let v = self._initialFallbackTimeout { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + try { if let v = self._hedgingDelay { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() + if !self.nonFatalStatusCodes.isEmpty { + try visitor.visitPackedEnumField(value: self.nonFatalStatusCodes, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_GrpcLbConfig, rhs: Grpc_ServiceConfig_GrpcLbConfig) -> Bool { - if lhs.childPolicy != rhs.childPolicy {return false} - if lhs.serviceName != rhs.serviceName {return false} - if lhs._initialFallbackTimeout != rhs._initialFallbackTimeout {return false} + static func ==(lhs: Grpc_ServiceConfig_MethodConfig.HedgingPolicy, rhs: Grpc_ServiceConfig_MethodConfig.HedgingPolicy) -> Bool { + if lhs.maxAttempts != rhs.maxAttempts {return false} + if lhs._hedgingDelay != rhs._hedgingDelay {return false} + if lhs.nonFatalStatusCodes != rhs.nonFatalStatusCodes {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PriorityLoadBalancingPolicyConfig" +extension Grpc_ServiceConfig_PickFirstConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PickFirstConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "children"), - 2: .same(proto: "priorities"), + 1: .standard(proto: "shuffle_address_list"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2532,73 +2196,54 @@ extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: SwiftProtobuf.Me // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.children) }() - case 2: try { try decoder.decodeRepeatedStringField(value: &self.priorities) }() + case 1: try { try decoder.decodeSingularBoolField(value: &self.shuffleAddressList) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.children.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.children, fieldNumber: 1) - } - if !self.priorities.isEmpty { - try visitor.visitRepeatedStringField(value: self.priorities, fieldNumber: 2) + if self.shuffleAddressList != false { + try visitor.visitSingularBoolField(value: self.shuffleAddressList, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig) -> Bool { - if lhs.children != rhs.children {return false} - if lhs.priorities != rhs.priorities {return false} + static func ==(lhs: Grpc_ServiceConfig_PickFirstConfig, rhs: Grpc_ServiceConfig_PickFirstConfig) -> Bool { + if lhs.shuffleAddressList != rhs.shuffleAddressList {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.protoMessageName + ".Child" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "config"), - 2: .standard(proto: "ignore_reresolution_requests"), - ] +extension Grpc_ServiceConfig_RoundRobinConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RoundRobinConfig" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.config) }() - case 2: try { try decoder.decodeSingularBoolField(value: &self.ignoreReresolutionRequests) }() - default: break - } + while let _ = try decoder.nextFieldNumber() { } } func traverse(visitor: inout V) throws { - if !self.config.isEmpty { - try visitor.visitRepeatedMessageField(value: self.config, fieldNumber: 1) - } - if self.ignoreReresolutionRequests != false { - try visitor.visitSingularBoolField(value: self.ignoreReresolutionRequests, fieldNumber: 2) - } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child, rhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child) -> Bool { - if lhs.config != rhs.config {return false} - if lhs.ignoreReresolutionRequests != rhs.ignoreReresolutionRequests {return false} + static func ==(lhs: Grpc_ServiceConfig_RoundRobinConfig, rhs: Grpc_ServiceConfig_RoundRobinConfig) -> Bool { if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".WeightedTargetLoadBalancingPolicyConfig" +extension Grpc_ServiceConfig_WeightedRoundRobinLbConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".WeightedRoundRobinLbConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "targets"), + 1: .standard(proto: "enable_oob_load_report"), + 2: .standard(proto: "oob_reporting_period"), + 3: .standard(proto: "blackout_period"), + 4: .standard(proto: "weight_expiration_period"), + 5: .standard(proto: "weight_update_period"), + 6: .standard(proto: "error_utilization_penalty"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2607,31 +2252,65 @@ extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: SwiftProto // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.targets) }() + case 1: try { try decoder.decodeSingularMessageField(value: &self._enableOobLoadReport) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._oobReportingPeriod) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._blackoutPeriod) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._weightExpirationPeriod) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._weightUpdatePeriod) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._errorUtilizationPenalty) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.targets.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.targets, fieldNumber: 1) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._enableOobLoadReport { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._oobReportingPeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._blackoutPeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._weightExpirationPeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try { if let v = self._weightUpdatePeriod { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try { if let v = self._errorUtilizationPenalty { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig) -> Bool { - if lhs.targets != rhs.targets {return false} + static func ==(lhs: Grpc_ServiceConfig_WeightedRoundRobinLbConfig, rhs: Grpc_ServiceConfig_WeightedRoundRobinLbConfig) -> Bool { + if lhs._enableOobLoadReport != rhs._enableOobLoadReport {return false} + if lhs._oobReportingPeriod != rhs._oobReportingPeriod {return false} + if lhs._blackoutPeriod != rhs._blackoutPeriod {return false} + if lhs._weightExpirationPeriod != rhs._weightExpirationPeriod {return false} + if lhs._weightUpdatePeriod != rhs._weightUpdatePeriod {return false} + if lhs._errorUtilizationPenalty != rhs._errorUtilizationPenalty {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.protoMessageName + ".Target" +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".OutlierDetectionLoadBalancingConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "weight"), - 2: .standard(proto: "child_policy"), + 1: .same(proto: "interval"), + 2: .standard(proto: "base_ejection_time"), + 3: .standard(proto: "max_ejection_time"), + 4: .standard(proto: "max_ejection_percent"), + 5: .standard(proto: "success_rate_ejection"), + 6: .standard(proto: "failure_percentage_ejection"), + 13: .standard(proto: "child_policy"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2640,135 +2319,121 @@ extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: Swi // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt32Field(value: &self.weight) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 1: try { try decoder.decodeSingularMessageField(value: &self._interval) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._baseEjectionTime) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._maxEjectionTime) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._maxEjectionPercent) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._successRateEjection) }() + case 6: try { try decoder.decodeSingularMessageField(value: &self._failurePercentageEjection) }() + case 13: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() default: break } } } func traverse(visitor: inout V) throws { - if self.weight != 0 { - try visitor.visitSingularUInt32Field(value: self.weight, fieldNumber: 1) - } - if !self.childPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target, rhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target) -> Bool { - if lhs.weight != rhs.weight {return false} - if lhs.childPolicy != rhs.childPolicy {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".RlsLoadBalancingPolicyConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "route_lookup_config"), - 2: .standard(proto: "route_lookup_channel_service_config"), - 3: .standard(proto: "child_policy"), - 4: .standard(proto: "child_policy_config_target_field_name"), - ] - - fileprivate class _StorageClass { - var _routeLookupConfig: Grpc_Lookup_V1_RouteLookupConfig? = nil - var _routeLookupChannelServiceConfig: Grpc_ServiceConfig_ServiceConfig? = nil - var _childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] - var _childPolicyConfigTargetFieldName: String = String() - - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif - - private init() {} - - init(copying source: _StorageClass) { - _routeLookupConfig = source._routeLookupConfig - _routeLookupChannelServiceConfig = source._routeLookupChannelServiceConfig - _childPolicy = source._childPolicy - _childPolicyConfigTargetFieldName = source._childPolicyConfigTargetFieldName + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._interval { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._baseEjectionTime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._maxEjectionTime { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._maxEjectionPercent { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + try { if let v = self._successRateEjection { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try { if let v = self._failurePercentageEjection { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 13) } + try unknownFields.traverse(visitor: &visitor) } - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage + static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig) -> Bool { + if lhs._interval != rhs._interval {return false} + if lhs._baseEjectionTime != rhs._baseEjectionTime {return false} + if lhs._maxEjectionTime != rhs._maxEjectionTime {return false} + if lhs._maxEjectionPercent != rhs._maxEjectionPercent {return false} + if lhs._successRateEjection != rhs._successRateEjection {return false} + if lhs._failurePercentageEjection != rhs._failurePercentageEjection {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true } +} + +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.protoMessageName + ".SuccessRateEjection" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "stdev_factor"), + 2: .standard(proto: "enforcement_percentage"), + 3: .standard(proto: "minimum_hosts"), + 4: .standard(proto: "request_volume"), + ] mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &_storage._routeLookupConfig) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._routeLookupChannelServiceConfig) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &_storage._childPolicy) }() - case 4: try { try decoder.decodeSingularStringField(value: &_storage._childPolicyConfigTargetFieldName) }() - default: break - } + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._stdevFactor) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._enforcementPercentage) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._minimumHosts) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._requestVolume) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = _storage._routeLookupConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = _storage._routeLookupChannelServiceConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if !_storage._childPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._childPolicy, fieldNumber: 3) - } - if !_storage._childPolicyConfigTargetFieldName.isEmpty { - try visitor.visitSingularStringField(value: _storage._childPolicyConfigTargetFieldName, fieldNumber: 4) - } - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._stdevFactor { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._enforcementPercentage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._minimumHosts { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._requestVolume { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._routeLookupConfig != rhs_storage._routeLookupConfig {return false} - if _storage._routeLookupChannelServiceConfig != rhs_storage._routeLookupChannelServiceConfig {return false} - if _storage._childPolicy != rhs_storage._childPolicy {return false} - if _storage._childPolicyConfigTargetFieldName != rhs_storage._childPolicyConfigTargetFieldName {return false} - return true - } - if !storagesAreEqual {return false} - } + static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection) -> Bool { + if lhs._stdevFactor != rhs._stdevFactor {return false} + if lhs._enforcementPercentage != rhs._enforcementPercentage {return false} + if lhs._minimumHosts != rhs._minimumHosts {return false} + if lhs._requestVolume != rhs._requestVolume {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".XdsClusterManagerLoadBalancingPolicyConfig" +extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.protoMessageName + ".FailurePercentageEjection" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "children"), + 1: .same(proto: "threshold"), + 2: .standard(proto: "enforcement_percentage"), + 3: .standard(proto: "minimum_hosts"), + 4: .standard(proto: "request_volume"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2777,30 +2442,51 @@ extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: SwiftPr // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.children) }() + case 1: try { try decoder.decodeSingularMessageField(value: &self._threshold) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._enforcementPercentage) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._minimumHosts) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._requestVolume) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.children.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.children, fieldNumber: 1) - } + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._threshold { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._enforcementPercentage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = self._minimumHosts { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._requestVolume { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig) -> Bool { - if lhs.children != rhs.children {return false} + static func ==(lhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection, rhs: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection) -> Bool { + if lhs._threshold != rhs._threshold {return false} + if lhs._enforcementPercentage != rhs._enforcementPercentage {return false} + if lhs._minimumHosts != rhs._minimumHosts {return false} + if lhs._requestVolume != rhs._requestVolume {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.protoMessageName + ".Child" +extension Grpc_ServiceConfig_GrpcLbConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".GrpcLbConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "child_policy"), + 2: .standard(proto: "service_name"), + 3: .standard(proto: "initial_fallback_timeout"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2810,29 +2496,44 @@ extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: S // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.serviceName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._initialFallbackTimeout) }() default: break } } } func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if !self.childPolicy.isEmpty { try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 1) } + if !self.serviceName.isEmpty { + try visitor.visitSingularStringField(value: self.serviceName, fieldNumber: 2) + } + try { if let v = self._initialFallbackTimeout { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child, rhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child) -> Bool { + static func ==(lhs: Grpc_ServiceConfig_GrpcLbConfig, rhs: Grpc_ServiceConfig_GrpcLbConfig) -> Bool { if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.serviceName != rhs.serviceName {return false} + if lhs._initialFallbackTimeout != rhs._initialFallbackTimeout {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_CdsConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".CdsConfig" +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PriorityLoadBalancingPolicyConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "cluster"), + 1: .same(proto: "children"), + 2: .same(proto: "priorities"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2841,32 +2542,36 @@ extension Grpc_ServiceConfig_CdsConfig: SwiftProtobuf.Message, SwiftProtobuf._Me // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.children) }() + case 2: try { try decoder.decodeRepeatedStringField(value: &self.priorities) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.cluster.isEmpty { - try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) + if !self.children.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.children, fieldNumber: 1) + } + if !self.priorities.isEmpty { + try visitor.visitRepeatedStringField(value: self.priorities, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_CdsConfig, rhs: Grpc_ServiceConfig_CdsConfig) -> Bool { - if lhs.cluster != rhs.cluster {return false} + static func ==(lhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig) -> Bool { + if lhs.children != rhs.children {return false} + if lhs.priorities != rhs.priorities {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_XdsServer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".XdsServer" +extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.protoMessageName + ".Child" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "server_uri"), - 2: .same(proto: "channel_creds"), - 3: .same(proto: "server_features"), + 1: .same(proto: "config"), + 2: .standard(proto: "ignore_reresolution_requests"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2875,41 +2580,35 @@ extension Grpc_ServiceConfig_XdsServer: SwiftProtobuf.Message, SwiftProtobuf._Me // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.serverUri) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.channelCreds) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &self.serverFeatures) }() + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.config) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.ignoreReresolutionRequests) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.serverUri.isEmpty { - try visitor.visitSingularStringField(value: self.serverUri, fieldNumber: 1) - } - if !self.channelCreds.isEmpty { - try visitor.visitRepeatedMessageField(value: self.channelCreds, fieldNumber: 2) + if !self.config.isEmpty { + try visitor.visitRepeatedMessageField(value: self.config, fieldNumber: 1) } - if !self.serverFeatures.isEmpty { - try visitor.visitRepeatedMessageField(value: self.serverFeatures, fieldNumber: 3) + if self.ignoreReresolutionRequests != false { + try visitor.visitSingularBoolField(value: self.ignoreReresolutionRequests, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsServer, rhs: Grpc_ServiceConfig_XdsServer) -> Bool { - if lhs.serverUri != rhs.serverUri {return false} - if lhs.channelCreds != rhs.channelCreds {return false} - if lhs.serverFeatures != rhs.serverFeatures {return false} + static func ==(lhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child, rhs: Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child) -> Bool { + if lhs.config != rhs.config {return false} + if lhs.ignoreReresolutionRequests != rhs.ignoreReresolutionRequests {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_XdsServer.protoMessageName + ".ChannelCredentials" +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".WeightedTargetLoadBalancingPolicyConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "config"), + 1: .same(proto: "targets"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2918,40 +2617,31 @@ extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: SwiftProtobuf.Message // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.type) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._config) }() + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.targets) }() default: break } } } func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.type.isEmpty { - try visitor.visitSingularStringField(value: self.type, fieldNumber: 1) + if !self.targets.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.targets, fieldNumber: 1) } - try { if let v = self._config { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsServer.ChannelCredentials, rhs: Grpc_ServiceConfig_XdsServer.ChannelCredentials) -> Bool { - if lhs.type != rhs.type {return false} - if lhs._config != rhs._config {return false} + static func ==(lhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig) -> Bool { + if lhs.targets != rhs.targets {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".XdsClusterResolverLoadBalancingPolicyConfig" +extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.protoMessageName + ".Target" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "discovery_mechanisms"), - 2: .standard(proto: "xds_lb_policy"), + 1: .same(proto: "weight"), + 2: .standard(proto: "child_policy"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2960,57 +2650,45 @@ extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: SwiftP // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.discoveryMechanisms) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.xdsLbPolicy) }() + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.weight) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.discoveryMechanisms.isEmpty { - try visitor.visitRepeatedMessageField(value: self.discoveryMechanisms, fieldNumber: 1) + if self.weight != 0 { + try visitor.visitSingularUInt32Field(value: self.weight, fieldNumber: 1) } - if !self.xdsLbPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.xdsLbPolicy, fieldNumber: 2) + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig) -> Bool { - if lhs.discoveryMechanisms != rhs.discoveryMechanisms {return false} - if lhs.xdsLbPolicy != rhs.xdsLbPolicy {return false} + static func ==(lhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target, rhs: Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target) -> Bool { + if lhs.weight != rhs.weight {return false} + if lhs.childPolicy != rhs.childPolicy {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.protoMessageName + ".DiscoveryMechanism" +extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RlsLoadBalancingPolicyConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "cluster"), - 2: .standard(proto: "lrs_load_reporting_server_name"), - 7: .standard(proto: "lrs_load_reporting_server"), - 3: .standard(proto: "max_concurrent_requests"), - 4: .same(proto: "type"), - 5: .standard(proto: "eds_service_name"), - 6: .standard(proto: "dns_hostname"), - 8: .standard(proto: "outlier_detection"), - 9: .standard(proto: "override_host_status"), - 10: .standard(proto: "telemetry_labels"), + 1: .standard(proto: "route_lookup_config"), + 2: .standard(proto: "route_lookup_channel_service_config"), + 3: .standard(proto: "child_policy"), + 4: .standard(proto: "child_policy_config_target_field_name"), ] fileprivate class _StorageClass { - var _cluster: String = String() - var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil - var _lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer? = nil - var _maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil - var _type: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum = .unknown - var _edsServiceName: String = String() - var _dnsHostname: String = String() - var _outlierDetection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig? = nil - var _overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] - var _telemetryLabels: Dictionary = [:] + var _routeLookupConfig: Grpc_Lookup_V1_RouteLookupConfig? = nil + var _routeLookupChannelServiceConfig: Grpc_ServiceConfig_ServiceConfig? = nil + var _childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] + var _childPolicyConfigTargetFieldName: String = String() #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. @@ -3025,16 +2703,10 @@ extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.Discove private init() {} init(copying source: _StorageClass) { - _cluster = source._cluster - _lrsLoadReportingServerName = source._lrsLoadReportingServerName - _lrsLoadReportingServer = source._lrsLoadReportingServer - _maxConcurrentRequests = source._maxConcurrentRequests - _type = source._type - _edsServiceName = source._edsServiceName - _dnsHostname = source._dnsHostname - _outlierDetection = source._outlierDetection - _overrideHostStatus = source._overrideHostStatus - _telemetryLabels = source._telemetryLabels + _routeLookupConfig = source._routeLookupConfig + _routeLookupChannelServiceConfig = source._routeLookupChannelServiceConfig + _childPolicy = source._childPolicy + _childPolicyConfigTargetFieldName = source._childPolicyConfigTargetFieldName } } @@ -3053,16 +2725,10 @@ extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.Discove // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &_storage._cluster) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._lrsLoadReportingServerName) }() - case 3: try { try decoder.decodeSingularMessageField(value: &_storage._maxConcurrentRequests) }() - case 4: try { try decoder.decodeSingularEnumField(value: &_storage._type) }() - case 5: try { try decoder.decodeSingularStringField(value: &_storage._edsServiceName) }() - case 6: try { try decoder.decodeSingularStringField(value: &_storage._dnsHostname) }() - case 7: try { try decoder.decodeSingularMessageField(value: &_storage._lrsLoadReportingServer) }() - case 8: try { try decoder.decodeSingularMessageField(value: &_storage._outlierDetection) }() - case 9: try { try decoder.decodeRepeatedEnumField(value: &_storage._overrideHostStatus) }() - case 10: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &_storage._telemetryLabels) }() + case 1: try { try decoder.decodeSingularMessageField(value: &_storage._routeLookupConfig) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._routeLookupChannelServiceConfig) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &_storage._childPolicy) }() + case 4: try { try decoder.decodeSingularStringField(value: &_storage._childPolicyConfigTargetFieldName) }() default: break } } @@ -3075,55 +2741,31 @@ extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.Discove // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - if !_storage._cluster.isEmpty { - try visitor.visitSingularStringField(value: _storage._cluster, fieldNumber: 1) - } - try { if let v = _storage._lrsLoadReportingServerName { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = _storage._maxConcurrentRequests { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if _storage._type != .unknown { - try visitor.visitSingularEnumField(value: _storage._type, fieldNumber: 4) - } - if !_storage._edsServiceName.isEmpty { - try visitor.visitSingularStringField(value: _storage._edsServiceName, fieldNumber: 5) - } - if !_storage._dnsHostname.isEmpty { - try visitor.visitSingularStringField(value: _storage._dnsHostname, fieldNumber: 6) - } - try { if let v = _storage._lrsLoadReportingServer { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + try { if let v = _storage._routeLookupConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) } }() - try { if let v = _storage._outlierDetection { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + try { if let v = _storage._routeLookupChannelServiceConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) } }() - if !_storage._overrideHostStatus.isEmpty { - try visitor.visitPackedEnumField(value: _storage._overrideHostStatus, fieldNumber: 9) + if !_storage._childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._childPolicy, fieldNumber: 3) } - if !_storage._telemetryLabels.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: _storage._telemetryLabels, fieldNumber: 10) + if !_storage._childPolicyConfigTargetFieldName.isEmpty { + try visitor.visitSingularStringField(value: _storage._childPolicyConfigTargetFieldName, fieldNumber: 4) } } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism, rhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism) -> Bool { + static func ==(lhs: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig) -> Bool { if lhs._storage !== rhs._storage { let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in let _storage = _args.0 let rhs_storage = _args.1 - if _storage._cluster != rhs_storage._cluster {return false} - if _storage._lrsLoadReportingServerName != rhs_storage._lrsLoadReportingServerName {return false} - if _storage._lrsLoadReportingServer != rhs_storage._lrsLoadReportingServer {return false} - if _storage._maxConcurrentRequests != rhs_storage._maxConcurrentRequests {return false} - if _storage._type != rhs_storage._type {return false} - if _storage._edsServiceName != rhs_storage._edsServiceName {return false} - if _storage._dnsHostname != rhs_storage._dnsHostname {return false} - if _storage._outlierDetection != rhs_storage._outlierDetection {return false} - if _storage._overrideHostStatus != rhs_storage._overrideHostStatus {return false} - if _storage._telemetryLabels != rhs_storage._telemetryLabels {return false} + if _storage._routeLookupConfig != rhs_storage._routeLookupConfig {return false} + if _storage._routeLookupChannelServiceConfig != rhs_storage._routeLookupChannelServiceConfig {return false} + if _storage._childPolicy != rhs_storage._childPolicy {return false} + if _storage._childPolicyConfigTargetFieldName != rhs_storage._childPolicyConfigTargetFieldName {return false} return true } if !storagesAreEqual {return false} @@ -3133,25 +2775,10 @@ extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.Discove } } -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "EDS"), - 2: .same(proto: "LOGICAL_DNS"), - ] -} - -extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".XdsClusterImplLoadBalancingPolicyConfig" +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsClusterManagerLoadBalancingPolicyConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "cluster"), - 2: .standard(proto: "eds_service_name"), - 3: .standard(proto: "lrs_load_reporting_server_name"), - 7: .standard(proto: "lrs_load_reporting_server"), - 4: .standard(proto: "max_concurrent_requests"), - 5: .standard(proto: "drop_categories"), - 6: .standard(proto: "child_policy"), - 8: .standard(proto: "telemetry_labels"), + 1: .same(proto: "children"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3160,70 +2787,30 @@ extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: SwiftProto // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._maxConcurrentRequests) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &self.dropCategories) }() - case 6: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServer) }() - case 8: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.telemetryLabels) }() + case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.children) }() default: break } } } func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.cluster.isEmpty { - try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) - } - if !self.edsServiceName.isEmpty { - try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) - } - try { if let v = self._lrsLoadReportingServerName { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._maxConcurrentRequests { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - if !self.dropCategories.isEmpty { - try visitor.visitRepeatedMessageField(value: self.dropCategories, fieldNumber: 5) - } - if !self.childPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 6) - } - try { if let v = self._lrsLoadReportingServer { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - if !self.telemetryLabels.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.telemetryLabels, fieldNumber: 8) + if !self.children.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.children, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig) -> Bool { - if lhs.cluster != rhs.cluster {return false} - if lhs.edsServiceName != rhs.edsServiceName {return false} - if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} - if lhs._lrsLoadReportingServer != rhs._lrsLoadReportingServer {return false} - if lhs._maxConcurrentRequests != rhs._maxConcurrentRequests {return false} - if lhs.dropCategories != rhs.dropCategories {return false} - if lhs.childPolicy != rhs.childPolicy {return false} - if lhs.telemetryLabels != rhs.telemetryLabels {return false} + static func ==(lhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig) -> Bool { + if lhs.children != rhs.children {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.protoMessageName + ".DropCategory" +extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.protoMessageName + ".Child" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "category"), - 2: .standard(proto: "requests_per_million"), + 1: .standard(proto: "child_policy"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3232,39 +2819,31 @@ extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategor // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.category) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.requestsPerMillion) }() + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.category.isEmpty { - try visitor.visitSingularStringField(value: self.category, fieldNumber: 1) - } - if self.requestsPerMillion != 0 { - try visitor.visitSingularUInt32Field(value: self.requestsPerMillion, fieldNumber: 2) + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory, rhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory) -> Bool { - if lhs.category != rhs.category {return false} - if lhs.requestsPerMillion != rhs.requestsPerMillion {return false} + static func ==(lhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child, rhs: Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child) -> Bool { + if lhs.childPolicy != rhs.childPolicy {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".EdsLoadBalancingPolicyConfig" +extension Grpc_ServiceConfig_CdsConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".CdsConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "cluster"), - 2: .standard(proto: "eds_service_name"), - 3: .standard(proto: "lrs_load_reporting_server_name"), - 4: .standard(proto: "locality_picking_policy"), - 5: .standard(proto: "endpoint_picking_policy"), + 2: .standard(proto: "is_dynamic"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3274,54 +2853,41 @@ extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: SwiftProtobuf.Message // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() - case 4: try { try decoder.decodeRepeatedMessageField(value: &self.localityPickingPolicy) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &self.endpointPickingPolicy) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.isDynamic) }() default: break } } } func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 if !self.cluster.isEmpty { try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) } - if !self.edsServiceName.isEmpty { - try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) - } - try { if let v = self._lrsLoadReportingServerName { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if !self.localityPickingPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.localityPickingPolicy, fieldNumber: 4) - } - if !self.endpointPickingPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.endpointPickingPolicy, fieldNumber: 5) + if self.isDynamic != false { + try visitor.visitSingularBoolField(value: self.isDynamic, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig) -> Bool { + static func ==(lhs: Grpc_ServiceConfig_CdsConfig, rhs: Grpc_ServiceConfig_CdsConfig) -> Bool { if lhs.cluster != rhs.cluster {return false} - if lhs.edsServiceName != rhs.edsServiceName {return false} - if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} - if lhs.localityPickingPolicy != rhs.localityPickingPolicy {return false} - if lhs.endpointPickingPolicy != rhs.endpointPickingPolicy {return false} + if lhs.isDynamic != rhs.isDynamic {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".RingHashLoadBalancingConfig" +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsClusterImplLoadBalancingPolicyConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "min_ring_size"), - 2: .standard(proto: "max_ring_size"), + 1: .same(proto: "cluster"), + 6: .standard(proto: "child_policy"), + 2: .standard(proto: "eds_service_name"), + 3: .standard(proto: "lrs_load_reporting_server_name"), + 7: .standard(proto: "lrs_load_reporting_server"), + 4: .standard(proto: "max_concurrent_requests"), + 5: .standard(proto: "drop_categories"), + 8: .standard(proto: "telemetry_labels"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3330,39 +2896,70 @@ extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: SwiftProtobuf.Message, // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt64Field(value: &self.minRingSize) }() - case 2: try { try decoder.decodeSingularUInt64Field(value: &self.maxRingSize) }() + case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._maxConcurrentRequests) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.dropCategories) }() + case 6: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 7: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServer) }() + case 8: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.telemetryLabels) }() default: break } } } func traverse(visitor: inout V) throws { - if self.minRingSize != 0 { - try visitor.visitSingularUInt64Field(value: self.minRingSize, fieldNumber: 1) + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.cluster.isEmpty { + try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) } - if self.maxRingSize != 0 { - try visitor.visitSingularUInt64Field(value: self.maxRingSize, fieldNumber: 2) + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) + } + try { if let v = self._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try { if let v = self._maxConcurrentRequests { + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + } }() + if !self.dropCategories.isEmpty { + try visitor.visitRepeatedMessageField(value: self.dropCategories, fieldNumber: 5) + } + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 6) + } + try { if let v = self._lrsLoadReportingServer { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + if !self.telemetryLabels.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.telemetryLabels, fieldNumber: 8) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_RingHashLoadBalancingConfig, rhs: Grpc_ServiceConfig_RingHashLoadBalancingConfig) -> Bool { - if lhs.minRingSize != rhs.minRingSize {return false} - if lhs.maxRingSize != rhs.maxRingSize {return false} + static func ==(lhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig) -> Bool { + if lhs.cluster != rhs.cluster {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} + if lhs._lrsLoadReportingServer != rhs._lrsLoadReportingServer {return false} + if lhs._maxConcurrentRequests != rhs._maxConcurrentRequests {return false} + if lhs.dropCategories != rhs.dropCategories {return false} + if lhs.telemetryLabels != rhs.telemetryLabels {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".LrsLoadBalancingPolicyConfig" +extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.protoMessageName + ".DropCategory" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "cluster_name"), - 2: .standard(proto: "eds_service_name"), - 3: .standard(proto: "lrs_load_reporting_server_name"), - 4: .same(proto: "locality"), - 5: .standard(proto: "child_policy"), + 1: .same(proto: "category"), + 2: .standard(proto: "requests_per_million"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3371,56 +2968,36 @@ extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: SwiftProtobuf.Message // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.clusterName) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.lrsLoadReportingServerName) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._locality) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 1: try { try decoder.decodeSingularStringField(value: &self.category) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.requestsPerMillion) }() default: break } } } func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.clusterName.isEmpty { - try visitor.visitSingularStringField(value: self.clusterName, fieldNumber: 1) - } - if !self.edsServiceName.isEmpty { - try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) - } - if !self.lrsLoadReportingServerName.isEmpty { - try visitor.visitSingularStringField(value: self.lrsLoadReportingServerName, fieldNumber: 3) + if !self.category.isEmpty { + try visitor.visitSingularStringField(value: self.category, fieldNumber: 1) } - try { if let v = self._locality { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - if !self.childPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 5) + if self.requestsPerMillion != 0 { + try visitor.visitSingularUInt32Field(value: self.requestsPerMillion, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig) -> Bool { - if lhs.clusterName != rhs.clusterName {return false} - if lhs.edsServiceName != rhs.edsServiceName {return false} - if lhs.lrsLoadReportingServerName != rhs.lrsLoadReportingServerName {return false} - if lhs._locality != rhs._locality {return false} - if lhs.childPolicy != rhs.childPolicy {return false} + static func ==(lhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory, rhs: Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory) -> Bool { + if lhs.category != rhs.category {return false} + if lhs.requestsPerMillion != rhs.requestsPerMillion {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.protoMessageName + ".Locality" +extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RingHashLoadBalancingConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "region"), - 2: .same(proto: "zone"), - 3: .same(proto: "subzone"), + 1: .standard(proto: "min_ring_size"), + 2: .standard(proto: "max_ring_size"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3429,31 +3006,26 @@ extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: SwiftProtobu // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.region) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.zone) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.subzone) }() + case 1: try { try decoder.decodeSingularUInt64Field(value: &self.minRingSize) }() + case 2: try { try decoder.decodeSingularUInt64Field(value: &self.maxRingSize) }() default: break } } } func traverse(visitor: inout V) throws { - if !self.region.isEmpty { - try visitor.visitSingularStringField(value: self.region, fieldNumber: 1) - } - if !self.zone.isEmpty { - try visitor.visitSingularStringField(value: self.zone, fieldNumber: 2) + if self.minRingSize != 0 { + try visitor.visitSingularUInt64Field(value: self.minRingSize, fieldNumber: 1) } - if !self.subzone.isEmpty { - try visitor.visitSingularStringField(value: self.subzone, fieldNumber: 3) + if self.maxRingSize != 0 { + try visitor.visitSingularUInt64Field(value: self.maxRingSize, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality, rhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality) -> Bool { - if lhs.region != rhs.region {return false} - if lhs.zone != rhs.zone {return false} - if lhs.subzone != rhs.subzone {return false} + static func ==(lhs: Grpc_ServiceConfig_RingHashLoadBalancingConfig, rhs: Grpc_ServiceConfig_RingHashLoadBalancingConfig) -> Bool { + if lhs.minRingSize != rhs.minRingSize {return false} + if lhs.maxRingSize != rhs.maxRingSize {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3526,8 +3098,9 @@ extension Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig: Swif extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".OverrideHostLoadBalancingPolicyConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "override_host_status"), + 3: .standard(proto: "cluster_name"), 2: .standard(proto: "child_policy"), + 1: .standard(proto: "override_host_status"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3538,6 +3111,7 @@ extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: SwiftProtobu switch fieldNumber { case 1: try { try decoder.decodeRepeatedEnumField(value: &self.overrideHostStatus) }() case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.clusterName) }() default: break } } @@ -3550,12 +3124,16 @@ extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: SwiftProtobu if !self.childPolicy.isEmpty { try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) } + if !self.clusterName.isEmpty { + try visitor.visitSingularStringField(value: self.clusterName, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig) -> Bool { - if lhs.overrideHostStatus != rhs.overrideHostStatus {return false} + if lhs.clusterName != rhs.clusterName {return false} if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.overrideHostStatus != rhs.overrideHostStatus {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3569,89 +3147,29 @@ extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: ] } -extension Grpc_ServiceConfig_XdsConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".XdsConfig" +extension Grpc_ServiceConfig_LoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LoadBalancingConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "balancer_name"), - 2: .standard(proto: "child_policy"), - 3: .standard(proto: "fallback_policy"), - 4: .standard(proto: "eds_service_name"), - 5: .standard(proto: "lrs_load_reporting_server_name"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.balancerName) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &self.fallbackPolicy) }() - case 4: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() - case 5: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.balancerName.isEmpty { - try visitor.visitSingularStringField(value: self.balancerName, fieldNumber: 1) - } - if !self.childPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) - } - if !self.fallbackPolicy.isEmpty { - try visitor.visitRepeatedMessageField(value: self.fallbackPolicy, fieldNumber: 3) - } - if !self.edsServiceName.isEmpty { - try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 4) - } - try { if let v = self._lrsLoadReportingServerName { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_ServiceConfig_XdsConfig, rhs: Grpc_ServiceConfig_XdsConfig) -> Bool { - if lhs.balancerName != rhs.balancerName {return false} - if lhs.childPolicy != rhs.childPolicy {return false} - if lhs.fallbackPolicy != rhs.fallbackPolicy {return false} - if lhs.edsServiceName != rhs.edsServiceName {return false} - if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_ServiceConfig_LoadBalancingConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".LoadBalancingConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 4: .same(proto: "pick_first"), - 1: .same(proto: "round_robin"), - 20: .same(proto: "weighted_round_robin"), - 3: .same(proto: "grpclb"), - 9: .same(proto: "priority_experimental"), - 10: .same(proto: "weighted_target_experimental"), - 15: .unique(proto: "outlier_detection", json: "outlier_detection_experimental"), - 19: .unique(proto: "rls", json: "rls_experimental"), - 14: .same(proto: "xds_cluster_manager_experimental"), - 6: .same(proto: "cds_experimental"), - 11: .same(proto: "xds_cluster_resolver_experimental"), - 12: .same(proto: "xds_cluster_impl_experimental"), - 18: .same(proto: "override_host_experimental"), - 16: .same(proto: "xds_wrr_locality_experimental"), - 13: .same(proto: "ring_hash_experimental"), - 17: .same(proto: "least_request_experimental"), - 8: .same(proto: "lrs_experimental"), - 7: .same(proto: "eds_experimental"), - 2: .same(proto: "xds"), - 5: .same(proto: "xds_experimental"), + 4: .same(proto: "pick_first"), + 1: .same(proto: "round_robin"), + 20: .same(proto: "weighted_round_robin"), + 3: .same(proto: "grpclb"), + 9: .same(proto: "priority_experimental"), + 10: .same(proto: "weighted_target_experimental"), + 15: .unique(proto: "outlier_detection", json: "outlier_detection_experimental"), + 19: .unique(proto: "rls", json: "rls_experimental"), + 14: .same(proto: "xds_cluster_manager_experimental"), + 6: .same(proto: "cds_experimental"), + 12: .same(proto: "xds_cluster_impl_experimental"), + 18: .same(proto: "override_host_experimental"), + 16: .same(proto: "xds_wrr_locality_experimental"), + 13: .same(proto: "ring_hash_experimental"), + 17: .same(proto: "least_request_experimental"), + 11: .same(proto: "xds_cluster_resolver_experimental"), + 8: .same(proto: "lrs_experimental"), + 7: .same(proto: "eds_experimental"), + 2: .same(proto: "xds"), + 5: .same(proto: "xds_experimental"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3930,107 +3448,586 @@ extension Grpc_ServiceConfig_LoadBalancingConfig: SwiftProtobuf.Message, SwiftPr // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - switch self.policy { - case .roundRobin?: try { - guard case .roundRobin(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .xds?: try { - guard case .xds(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case .grpclb?: try { - guard case .grpclb(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case .pickFirst?: try { - guard case .pickFirst(let v)? = self.policy else { preconditionFailure() } + switch self.policy { + case .roundRobin?: try { + guard case .roundRobin(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + }() + case .xds?: try { + guard case .xds(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .grpclb?: try { + guard case .grpclb(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .pickFirst?: try { + guard case .pickFirst(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .xdsExperimental?: try { + guard case .xdsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .cdsExperimental?: try { + guard case .cdsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .edsExperimental?: try { + guard case .edsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() + case .lrsExperimental?: try { + guard case .lrsExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + }() + case .priorityExperimental?: try { + guard case .priorityExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() + case .weightedTargetExperimental?: try { + guard case .weightedTargetExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() + case .xdsClusterResolverExperimental?: try { + guard case .xdsClusterResolverExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .xdsClusterImplExperimental?: try { + guard case .xdsClusterImplExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .ringHashExperimental?: try { + guard case .ringHashExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case .xdsClusterManagerExperimental?: try { + guard case .xdsClusterManagerExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + }() + case .outlierDetection?: try { + guard case .outlierDetection(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 15) + }() + case .xdsWrrLocalityExperimental?: try { + guard case .xdsWrrLocalityExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 16) + }() + case .leastRequestExperimental?: try { + guard case .leastRequestExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 17) + }() + case .overrideHostExperimental?: try { + guard case .overrideHostExperimental(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 18) + }() + case .rls?: try { + guard case .rls(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 19) + }() + case .weightedRoundRobin?: try { + guard case .weightedRoundRobin(let v)? = self.policy else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 20) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_LoadBalancingConfig, rhs: Grpc_ServiceConfig_LoadBalancingConfig) -> Bool { + if lhs.policy != rhs.policy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_ServiceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ServiceConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "load_balancing_policy"), + 4: .standard(proto: "load_balancing_config"), + 2: .standard(proto: "method_config"), + 3: .standard(proto: "retry_throttling"), + 5: .standard(proto: "health_check_config"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.loadBalancingPolicy) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.methodConfig) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._retryThrottling) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.loadBalancingConfig) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._healthCheckConfig) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.loadBalancingPolicy != .unspecified { + try visitor.visitSingularEnumField(value: self.loadBalancingPolicy, fieldNumber: 1) + } + if !self.methodConfig.isEmpty { + try visitor.visitRepeatedMessageField(value: self.methodConfig, fieldNumber: 2) + } + try { if let v = self._retryThrottling { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if !self.loadBalancingConfig.isEmpty { + try visitor.visitRepeatedMessageField(value: self.loadBalancingConfig, fieldNumber: 4) + } + try { if let v = self._healthCheckConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_ServiceConfig, rhs: Grpc_ServiceConfig_ServiceConfig) -> Bool { + if lhs.loadBalancingPolicy != rhs.loadBalancingPolicy {return false} + if lhs.loadBalancingConfig != rhs.loadBalancingConfig {return false} + if lhs.methodConfig != rhs.methodConfig {return false} + if lhs._retryThrottling != rhs._retryThrottling {return false} + if lhs._healthCheckConfig != rhs._healthCheckConfig {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNSPECIFIED"), + 1: .same(proto: "ROUND_ROBIN"), + ] +} + +extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_ServiceConfig.protoMessageName + ".RetryThrottlingPolicy" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "max_tokens"), + 2: .standard(proto: "token_ratio"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxTokens) }() + case 2: try { try decoder.decodeSingularFloatField(value: &self.tokenRatio) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.maxTokens != 0 { + try visitor.visitSingularUInt32Field(value: self.maxTokens, fieldNumber: 1) + } + if self.tokenRatio != 0 { + try visitor.visitSingularFloatField(value: self.tokenRatio, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy, rhs: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy) -> Bool { + if lhs.maxTokens != rhs.maxTokens {return false} + if lhs.tokenRatio != rhs.tokenRatio {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_ServiceConfig.protoMessageName + ".HealthCheckConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "service_name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._serviceName) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._serviceName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig, rhs: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig) -> Bool { + if lhs._serviceName != rhs._serviceName {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsServer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsServer" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "server_uri"), + 2: .same(proto: "channel_creds"), + 3: .same(proto: "server_features"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.serverUri) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.channelCreds) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.serverFeatures) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.serverUri.isEmpty { + try visitor.visitSingularStringField(value: self.serverUri, fieldNumber: 1) + } + if !self.channelCreds.isEmpty { + try visitor.visitRepeatedMessageField(value: self.channelCreds, fieldNumber: 2) + } + if !self.serverFeatures.isEmpty { + try visitor.visitRepeatedMessageField(value: self.serverFeatures, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsServer, rhs: Grpc_ServiceConfig_XdsServer) -> Bool { + if lhs.serverUri != rhs.serverUri {return false} + if lhs.channelCreds != rhs.channelCreds {return false} + if lhs.serverFeatures != rhs.serverFeatures {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsServer.protoMessageName + ".ChannelCredentials" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "type"), + 2: .same(proto: "config"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.type) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._config) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.type.isEmpty { + try visitor.visitSingularStringField(value: self.type, fieldNumber: 1) + } + try { if let v = self._config { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsServer.ChannelCredentials, rhs: Grpc_ServiceConfig_XdsServer.ChannelCredentials) -> Bool { + if lhs.type != rhs.type {return false} + if lhs._config != rhs._config {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsClusterResolverLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "discovery_mechanisms"), + 2: .standard(proto: "xds_lb_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.discoveryMechanisms) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.xdsLbPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.discoveryMechanisms.isEmpty { + try visitor.visitRepeatedMessageField(value: self.discoveryMechanisms, fieldNumber: 1) + } + if !self.xdsLbPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.xdsLbPolicy, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig) -> Bool { + if lhs.discoveryMechanisms != rhs.discoveryMechanisms {return false} + if lhs.xdsLbPolicy != rhs.xdsLbPolicy {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.protoMessageName + ".DiscoveryMechanism" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "cluster"), + 2: .standard(proto: "lrs_load_reporting_server_name"), + 7: .standard(proto: "lrs_load_reporting_server"), + 3: .standard(proto: "max_concurrent_requests"), + 4: .same(proto: "type"), + 5: .standard(proto: "eds_service_name"), + 6: .standard(proto: "dns_hostname"), + 8: .standard(proto: "outlier_detection"), + 9: .standard(proto: "override_host_status"), + 10: .standard(proto: "telemetry_labels"), + ] + + fileprivate class _StorageClass { + var _cluster: String = String() + var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil + var _lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer? = nil + var _maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value? = nil + var _type: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum = .unknown + var _edsServiceName: String = String() + var _dnsHostname: String = String() + var _outlierDetection: Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig? = nil + var _overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] + var _telemetryLabels: Dictionary = [:] + + #if swift(>=5.10) + // This property is used as the initial default value for new instances of the type. + // The type itself is protecting the reference to its storage via CoW semantics. + // This will force a copy to be made of this reference when the first mutation occurs; + // hence, it is safe to mark this as `nonisolated(unsafe)`. + static nonisolated(unsafe) let defaultInstance = _StorageClass() + #else + static let defaultInstance = _StorageClass() + #endif + + private init() {} + + init(copying source: _StorageClass) { + _cluster = source._cluster + _lrsLoadReportingServerName = source._lrsLoadReportingServerName + _lrsLoadReportingServer = source._lrsLoadReportingServer + _maxConcurrentRequests = source._maxConcurrentRequests + _type = source._type + _edsServiceName = source._edsServiceName + _dnsHostname = source._dnsHostname + _outlierDetection = source._outlierDetection + _overrideHostStatus = source._overrideHostStatus + _telemetryLabels = source._telemetryLabels + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &_storage._cluster) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._lrsLoadReportingServerName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &_storage._maxConcurrentRequests) }() + case 4: try { try decoder.decodeSingularEnumField(value: &_storage._type) }() + case 5: try { try decoder.decodeSingularStringField(value: &_storage._edsServiceName) }() + case 6: try { try decoder.decodeSingularStringField(value: &_storage._dnsHostname) }() + case 7: try { try decoder.decodeSingularMessageField(value: &_storage._lrsLoadReportingServer) }() + case 8: try { try decoder.decodeSingularMessageField(value: &_storage._outlierDetection) }() + case 9: try { try decoder.decodeRepeatedEnumField(value: &_storage._overrideHostStatus) }() + case 10: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &_storage._telemetryLabels) }() + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !_storage._cluster.isEmpty { + try visitor.visitSingularStringField(value: _storage._cluster, fieldNumber: 1) + } + try { if let v = _storage._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._maxConcurrentRequests { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if _storage._type != .unknown { + try visitor.visitSingularEnumField(value: _storage._type, fieldNumber: 4) + } + if !_storage._edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: _storage._edsServiceName, fieldNumber: 5) + } + if !_storage._dnsHostname.isEmpty { + try visitor.visitSingularStringField(value: _storage._dnsHostname, fieldNumber: 6) + } + try { if let v = _storage._lrsLoadReportingServer { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + try { if let v = _storage._outlierDetection { + try visitor.visitSingularMessageField(value: v, fieldNumber: 8) + } }() + if !_storage._overrideHostStatus.isEmpty { + try visitor.visitPackedEnumField(value: _storage._overrideHostStatus, fieldNumber: 9) + } + if !_storage._telemetryLabels.isEmpty { + try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: _storage._telemetryLabels, fieldNumber: 10) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism, rhs: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._cluster != rhs_storage._cluster {return false} + if _storage._lrsLoadReportingServerName != rhs_storage._lrsLoadReportingServerName {return false} + if _storage._lrsLoadReportingServer != rhs_storage._lrsLoadReportingServer {return false} + if _storage._maxConcurrentRequests != rhs_storage._maxConcurrentRequests {return false} + if _storage._type != rhs_storage._type {return false} + if _storage._edsServiceName != rhs_storage._edsServiceName {return false} + if _storage._dnsHostname != rhs_storage._dnsHostname {return false} + if _storage._outlierDetection != rhs_storage._outlierDetection {return false} + if _storage._overrideHostStatus != rhs_storage._overrideHostStatus {return false} + if _storage._telemetryLabels != rhs_storage._telemetryLabels {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "EDS"), + 2: .same(proto: "LOGICAL_DNS"), + ] +} + +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".LrsLoadBalancingPolicyConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "cluster_name"), + 2: .standard(proto: "eds_service_name"), + 3: .standard(proto: "lrs_load_reporting_server_name"), + 4: .same(proto: "locality"), + 5: .standard(proto: "child_policy"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.clusterName) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.lrsLoadReportingServerName) }() + case 4: try { try decoder.decodeSingularMessageField(value: &self._locality) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.clusterName.isEmpty { + try visitor.visitSingularStringField(value: self.clusterName, fieldNumber: 1) + } + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) + } + if !self.lrsLoadReportingServerName.isEmpty { + try visitor.visitSingularStringField(value: self.lrsLoadReportingServerName, fieldNumber: 3) + } + try { if let v = self._locality { try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .xdsExperimental?: try { - guard case .xdsExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .cdsExperimental?: try { - guard case .cdsExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .edsExperimental?: try { - guard case .edsExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - }() - case .lrsExperimental?: try { - guard case .lrsExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - }() - case .priorityExperimental?: try { - guard case .priorityExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 9) - }() - case .weightedTargetExperimental?: try { - guard case .weightedTargetExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) - }() - case .xdsClusterResolverExperimental?: try { - guard case .xdsClusterResolverExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - }() - case .xdsClusterImplExperimental?: try { - guard case .xdsClusterImplExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 12) - }() - case .ringHashExperimental?: try { - guard case .ringHashExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 13) - }() - case .xdsClusterManagerExperimental?: try { - guard case .xdsClusterManagerExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 14) - }() - case .outlierDetection?: try { - guard case .outlierDetection(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 15) - }() - case .xdsWrrLocalityExperimental?: try { - guard case .xdsWrrLocalityExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 16) - }() - case .leastRequestExperimental?: try { - guard case .leastRequestExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 17) - }() - case .overrideHostExperimental?: try { - guard case .overrideHostExperimental(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 18) - }() - case .rls?: try { - guard case .rls(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 19) - }() - case .weightedRoundRobin?: try { - guard case .weightedRoundRobin(let v)? = self.policy else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 20) - }() - case nil: break + } }() + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 5) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_LoadBalancingConfig, rhs: Grpc_ServiceConfig_LoadBalancingConfig) -> Bool { - if lhs.policy != rhs.policy {return false} + static func ==(lhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig) -> Bool { + if lhs.clusterName != rhs.clusterName {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs.lrsLoadReportingServerName != rhs.lrsLoadReportingServerName {return false} + if lhs._locality != rhs._locality {return false} + if lhs.childPolicy != rhs.childPolicy {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_ServiceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServiceConfig" +extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.protoMessageName + ".Locality" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "load_balancing_policy"), - 4: .standard(proto: "load_balancing_config"), - 2: .standard(proto: "method_config"), - 3: .standard(proto: "retry_throttling"), - 5: .standard(proto: "health_check_config"), + 1: .same(proto: "region"), + 2: .same(proto: "zone"), + 3: .same(proto: "subzone"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -4039,62 +4036,44 @@ extension Grpc_ServiceConfig_ServiceConfig: SwiftProtobuf.Message, SwiftProtobuf // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.loadBalancingPolicy) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.methodConfig) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._retryThrottling) }() - case 4: try { try decoder.decodeRepeatedMessageField(value: &self.loadBalancingConfig) }() - case 5: try { try decoder.decodeSingularMessageField(value: &self._healthCheckConfig) }() + case 1: try { try decoder.decodeSingularStringField(value: &self.region) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.zone) }() + case 3: try { try decoder.decodeSingularStringField(value: &self.subzone) }() default: break } } } func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.loadBalancingPolicy != .unspecified { - try visitor.visitSingularEnumField(value: self.loadBalancingPolicy, fieldNumber: 1) + if !self.region.isEmpty { + try visitor.visitSingularStringField(value: self.region, fieldNumber: 1) } - if !self.methodConfig.isEmpty { - try visitor.visitRepeatedMessageField(value: self.methodConfig, fieldNumber: 2) + if !self.zone.isEmpty { + try visitor.visitSingularStringField(value: self.zone, fieldNumber: 2) } - try { if let v = self._retryThrottling { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if !self.loadBalancingConfig.isEmpty { - try visitor.visitRepeatedMessageField(value: self.loadBalancingConfig, fieldNumber: 4) + if !self.subzone.isEmpty { + try visitor.visitSingularStringField(value: self.subzone, fieldNumber: 3) } - try { if let v = self._healthCheckConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - } }() try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_ServiceConfig, rhs: Grpc_ServiceConfig_ServiceConfig) -> Bool { - if lhs.loadBalancingPolicy != rhs.loadBalancingPolicy {return false} - if lhs.loadBalancingConfig != rhs.loadBalancingConfig {return false} - if lhs.methodConfig != rhs.methodConfig {return false} - if lhs._retryThrottling != rhs._retryThrottling {return false} - if lhs._healthCheckConfig != rhs._healthCheckConfig {return false} + static func ==(lhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality, rhs: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality) -> Bool { + if lhs.region != rhs.region {return false} + if lhs.zone != rhs.zone {return false} + if lhs.subzone != rhs.subzone {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSPECIFIED"), - 1: .same(proto: "ROUND_ROBIN"), - ] -} - -extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_ServiceConfig.protoMessageName + ".RetryThrottlingPolicy" +extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EdsLoadBalancingPolicyConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "max_tokens"), - 2: .standard(proto: "token_ratio"), + 1: .same(proto: "cluster"), + 2: .standard(proto: "eds_service_name"), + 3: .standard(proto: "lrs_load_reporting_server_name"), + 4: .standard(proto: "locality_picking_policy"), + 5: .standard(proto: "endpoint_picking_policy"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -4103,35 +4082,58 @@ extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: SwiftProtobuf. // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt32Field(value: &self.maxTokens) }() - case 2: try { try decoder.decodeSingularFloatField(value: &self.tokenRatio) }() + case 1: try { try decoder.decodeSingularStringField(value: &self.cluster) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() + case 4: try { try decoder.decodeRepeatedMessageField(value: &self.localityPickingPolicy) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &self.endpointPickingPolicy) }() default: break } } } func traverse(visitor: inout V) throws { - if self.maxTokens != 0 { - try visitor.visitSingularUInt32Field(value: self.maxTokens, fieldNumber: 1) + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.cluster.isEmpty { + try visitor.visitSingularStringField(value: self.cluster, fieldNumber: 1) } - if self.tokenRatio != 0 { - try visitor.visitSingularFloatField(value: self.tokenRatio, fieldNumber: 2) + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 2) + } + try { if let v = self._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if !self.localityPickingPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.localityPickingPolicy, fieldNumber: 4) + } + if !self.endpointPickingPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.endpointPickingPolicy, fieldNumber: 5) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy, rhs: Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy) -> Bool { - if lhs.maxTokens != rhs.maxTokens {return false} - if lhs.tokenRatio != rhs.tokenRatio {return false} + static func ==(lhs: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig, rhs: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig) -> Bool { + if lhs.cluster != rhs.cluster {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} + if lhs.localityPickingPolicy != rhs.localityPickingPolicy {return false} + if lhs.endpointPickingPolicy != rhs.endpointPickingPolicy {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } -extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_ServiceConfig_ServiceConfig.protoMessageName + ".HealthCheckConfig" +extension Grpc_ServiceConfig_XdsConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".XdsConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "service_name"), + 1: .standard(proto: "balancer_name"), + 2: .standard(proto: "child_policy"), + 3: .standard(proto: "fallback_policy"), + 4: .standard(proto: "eds_service_name"), + 5: .standard(proto: "lrs_load_reporting_server_name"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -4140,7 +4142,11 @@ extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: SwiftProtobuf.Mess // allocates stack space for every case branch when no optimizations are // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._serviceName) }() + case 1: try { try decoder.decodeSingularStringField(value: &self.balancerName) }() + case 2: try { try decoder.decodeRepeatedMessageField(value: &self.childPolicy) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.fallbackPolicy) }() + case 4: try { try decoder.decodeSingularStringField(value: &self.edsServiceName) }() + case 5: try { try decoder.decodeSingularMessageField(value: &self._lrsLoadReportingServerName) }() default: break } } @@ -4151,14 +4157,30 @@ extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: SwiftProtobuf.Mess // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._serviceName { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + if !self.balancerName.isEmpty { + try visitor.visitSingularStringField(value: self.balancerName, fieldNumber: 1) + } + if !self.childPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.childPolicy, fieldNumber: 2) + } + if !self.fallbackPolicy.isEmpty { + try visitor.visitRepeatedMessageField(value: self.fallbackPolicy, fieldNumber: 3) + } + if !self.edsServiceName.isEmpty { + try visitor.visitSingularStringField(value: self.edsServiceName, fieldNumber: 4) + } + try { if let v = self._lrsLoadReportingServerName { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) } }() try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig, rhs: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig) -> Bool { - if lhs._serviceName != rhs._serviceName {return false} + static func ==(lhs: Grpc_ServiceConfig_XdsConfig, rhs: Grpc_ServiceConfig_XdsConfig) -> Bool { + if lhs.balancerName != rhs.balancerName {return false} + if lhs.childPolicy != rhs.childPolicy {return false} + if lhs.fallbackPolicy != rhs.fallbackPolicy {return false} + if lhs.edsServiceName != rhs.edsServiceName {return false} + if lhs._lrsLoadReportingServerName != rhs._lrsLoadReportingServerName {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index c60dbcb74..5fc3f8a66 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -82,13 +82,13 @@ internal enum Control { /// the output. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -98,32 +98,32 @@ extension Control.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Control.Method.Unary.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.unary(request: request) } ) router.registerHandler( forMethod: Control.Method.ServerStream.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.serverStream(request: request) } ) router.registerHandler( forMethod: Control.Method.ClientStream.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.clientStream(request: request) } ) router.registerHandler( forMethod: Control.Method.BidiStream.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), handler: { request in try await self.bidiStream(request: request) } @@ -137,29 +137,29 @@ extension Control.StreamingServiceProtocol { /// the output. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { - func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single - func serverStream(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func serverStream(request: ServerRequest.Single) async throws -> ServerResponse.Stream - func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Single - func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `ControlStreamingServiceProtocol`. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Control.ServiceProtocol { - internal func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unary(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - internal func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.serverStream(request: ServerRequest.Single(stream: request)) return response } - internal func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.clientStream(request: request) return ServerResponse.Stream(single: response) } @@ -172,91 +172,91 @@ extension Control.ServiceProtocol { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal protocol ControlClientProtocol: Sendable { func unary( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable func serverStream( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable func clientStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable func bidiStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Control.ClientProtocol { internal func unary( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.unary( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } internal func serverStream( - request: ClientRequest.Single, + request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.serverStream( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } internal func clientStream( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.clientStream( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) } internal func bidiStream( - request: ClientRequest.Stream, + request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.bidiStream( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), options: options, body ) @@ -276,11 +276,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func unary( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -293,11 +293,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func serverStream( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -310,11 +310,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func clientStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -327,11 +327,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func bidiStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift index 891499b2f..4a322b666 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift @@ -232,7 +232,7 @@ struct PayloadParameters { /// The number of bytes to put into the output payload. var size: Int32 = 0 - /// The conent to use in the payload. The value is truncated to an octet. + /// The content to use in the payload. The value is truncated to an octet. var content: UInt32 = 0 var unknownFields = SwiftProtobuf.UnknownStorage() From b15469e439f69d34b861796233d4a1bf2e01006d Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 3 Jul 2024 15:51:55 +0100 Subject: [PATCH 381/580] Synthesise status when stream closes unexpectedly (#1953) Motivation When the stream closes unexpectedly (the H2 stream channel becomes inactive, an error is fired through the pipeline, or we receive a RST_STREAM frame), the client-side inbound sequence either finishes or throws an error. To make it easier for higher layers to retry, we want to synthesise a Status instead. Modifications On the client side, write a Status when one of the aforementioned unexpected closures happens. On the server side, make sure we're always firing an error when closing unexpectedly. On both, make sure we transition the stream state machine to closed. Result Inbound sequence now returns a Status instead of finishing/throwing --- .../Client/GRPCClientStreamHandler.swift | 28 ++- .../GRPCStreamStateMachine.swift | 110 +++++++++++ .../Server/GRPCServerStreamHandler.swift | 30 ++- .../Client/GRPCClientStreamHandlerTests.swift | 156 ++++++++++++++- .../GRPCStreamStateMachineTests.swift | 165 ++++++++++++++++ .../Server/GRPCServerStreamHandlerTests.swift | 182 +++++++++++++++++- 6 files changed, 662 insertions(+), 9 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index 0fa723554..675bb06a9 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -130,7 +130,10 @@ extension GRPCClientStreamHandler { context.fireErrorCaught(error) } - case .ping, .goAway, .priority, .rstStream, .settings, .pushPromise, .windowUpdate, + case .rstStream: + self.handleUnexpectedInboundClose(context: context, reason: .streamReset) + + case .ping, .goAway, .priority, .settings, .pushPromise, .windowUpdate, .alternativeService, .origin: () } @@ -148,6 +151,29 @@ extension GRPCClientStreamHandler { func handlerRemoved(context: ChannelHandlerContext) { self.stateMachine.tearDown() } + + func channelInactive(context: ChannelHandlerContext) { + self.handleUnexpectedInboundClose(context: context, reason: .channelInactive) + context.fireChannelInactive() + } + + func errorCaught(context: ChannelHandlerContext, error: any Error) { + self.handleUnexpectedInboundClose(context: context, reason: .errorThrown(error)) + } + + private func handleUnexpectedInboundClose( + context: ChannelHandlerContext, + reason: GRPCStreamStateMachine.UnexpectedInboundCloseReason + ) { + switch self.stateMachine.unexpectedInboundClose(reason: reason) { + case .forwardStatus_clientOnly(let status): + context.fireChannelRead(self.wrapInboundOut(.status(status, [:]))) + case .doNothing: + () + case .fireError_serverOnly: + assertionFailure("`fireError` should only happen on the server side, never on the client.") + } + } } // - MARK: ChannelOutboundHandler diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 78d887b7d..b2f083ff0 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -539,6 +539,38 @@ struct GRPCStreamStateMachine { state.compressor?.end() } } + + enum OnUnexpectedInboundClose { + case forwardStatus_clientOnly(Status) + case fireError_serverOnly(any Error) + case doNothing + + init(serverCloseReason: UnexpectedInboundCloseReason) { + switch serverCloseReason { + case .streamReset, .channelInactive: + self = .fireError_serverOnly(RPCError(serverCloseReason)) + case .errorThrown(let error): + self = .fireError_serverOnly(error) + } + } + } + + enum UnexpectedInboundCloseReason { + case streamReset + case channelInactive + case errorThrown(any Error) + } + + mutating func unexpectedInboundClose( + reason: UnexpectedInboundCloseReason + ) -> OnUnexpectedInboundClose { + switch self.configuration { + case .client: + return self.clientUnexpectedInboundClose(reason: reason) + case .server: + return self.serverUnexpectedInboundClose(reason: reason) + } + } } // - MARK: Client @@ -1044,6 +1076,35 @@ extension GRPCStreamStateMachine { } throw RPCError(code: .internalError, message: message) } + + private mutating func clientUnexpectedInboundClose( + reason: UnexpectedInboundCloseReason + ) -> OnUnexpectedInboundClose { + switch self.state { + case .clientIdleServerIdle(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return .forwardStatus_clientOnly(Status(RPCError(reason))) + + case .clientOpenServerIdle(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return .forwardStatus_clientOnly(Status(RPCError(reason))) + + case .clientClosedServerIdle(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return .forwardStatus_clientOnly(Status(RPCError(reason))) + + case .clientOpenServerOpen(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return .forwardStatus_clientOnly(Status(RPCError(reason))) + + case .clientClosedServerOpen(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return .forwardStatus_clientOnly(Status(RPCError(reason))) + + case .clientOpenServerClosed, .clientClosedServerClosed: + return .doNothing + } + } } // - MARK: Server @@ -1469,6 +1530,31 @@ extension GRPCStreamStateMachine { return .awaitMoreMessages } } + + private mutating func serverUnexpectedInboundClose( + reason: UnexpectedInboundCloseReason + ) -> OnUnexpectedInboundClose { + switch self.state { + case .clientIdleServerIdle(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return OnUnexpectedInboundClose(serverCloseReason: reason) + + case .clientOpenServerIdle(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return OnUnexpectedInboundClose(serverCloseReason: reason) + + case .clientOpenServerOpen(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return OnUnexpectedInboundClose(serverCloseReason: reason) + + case .clientOpenServerClosed(let state): + self.state = .clientClosedServerClosed(.init(previousState: state)) + return OnUnexpectedInboundClose(serverCloseReason: reason) + + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: + return .doNothing + } + } } extension MethodDescriptor { @@ -1612,3 +1698,27 @@ extension MethodDescriptor { return "/\(self.service)/\(self.method)" } } + +extension RPCError { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + fileprivate init(_ reason: GRPCStreamStateMachine.UnexpectedInboundCloseReason) { + switch reason { + case .streamReset: + self = RPCError( + code: .unavailable, + message: "Stream unexpectedly closed: a RST_STREAM frame was received." + ) + case .channelInactive: + self = RPCError(code: .unavailable, message: "Stream unexpectedly closed.") + case .errorThrown: + self = RPCError(code: .unavailable, message: "Stream unexpectedly closed with error.") + } + } +} + +extension Status { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + fileprivate init(_ error: RPCError) { + self = Status(code: Status.Code(error.code), message: error.message) + } +} diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 14e7f838f..d9d31e35d 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -139,7 +139,10 @@ extension GRPCServerStreamHandler { context.fireErrorCaught(error) } - case .ping, .goAway, .priority, .rstStream, .settings, .pushPromise, .windowUpdate, + case .rstStream: + self.handleUnexpectedInboundClose(context: context, reason: .streamReset) + + case .ping, .goAway, .priority, .settings, .pushPromise, .windowUpdate, .alternativeService, .origin: () } @@ -163,6 +166,31 @@ extension GRPCServerStreamHandler { ) ) } + + func channelInactive(context: ChannelHandlerContext) { + self.handleUnexpectedInboundClose(context: context, reason: .channelInactive) + context.fireChannelInactive() + } + + func errorCaught(context: ChannelHandlerContext, error: any Error) { + self.handleUnexpectedInboundClose(context: context, reason: .errorThrown(error)) + } + + private func handleUnexpectedInboundClose( + context: ChannelHandlerContext, + reason: GRPCStreamStateMachine.UnexpectedInboundCloseReason + ) { + switch self.stateMachine.unexpectedInboundClose(reason: reason) { + case .fireError_serverOnly(let wrappedError): + context.fireErrorCaught(wrappedError) + case .doNothing: + () + case .forwardStatus_clientOnly: + assertionFailure( + "`forwardStatus` should only happen on the client side, never on the server." + ) + } + } } // - MARK: ChannelOutboundHandler diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift index 5357bc8aa..275f313ac 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift @@ -40,9 +40,10 @@ final class GRPCClientStreamHandlerTests: XCTestCase { let framesToBeIgnored: [HTTP2Frame.FramePayload] = [ .ping(.init(), ack: false), .goAway(lastStreamID: .rootStream, errorCode: .cancel, opaqueData: nil), - // TODO: add .priority(StreamPriorityData) - right now, StreamPriorityData's - // initialiser is internal, so I can't create one of these frames. - .rstStream(.cancel), + // TODO: uncomment when it's possible to build a `StreamPriorityData`. + // .priority( + // HTTP2Frame.StreamPriorityData(exclusive: false, dependency: .rootStream, weight: 4) + // ), .settings(.ack), .pushPromise(.init(pushedStreamID: .maxID, headers: [:])), .windowUpdate(windowSizeIncrement: 4), @@ -764,6 +765,155 @@ final class GRPCClientStreamHandlerTests: XCTestCase { ) XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) } + + func testUnexpectedStreamClose_ErrorFired() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .none, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Write client's initial metadata + XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "/test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + let writtenInitialMetadata = try channel.assertReadHeadersOutbound() + XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) + + // An error is fired down the pipeline + let thrownError = ChannelError.connectTimeout(.milliseconds(100)) + channel.pipeline.fireErrorCaught(thrownError) + + // The client receives a status explaining the stream was closed because of the thrown error. + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init( + code: .unavailable, + message: "Stream unexpectedly closed with error." + ), + [:] + ) + ) + + // We should now be closed: check we can't write anymore. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } + + func testUnexpectedStreamClose_ChannelInactive() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .none, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Write client's initial metadata + XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "/test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + let writtenInitialMetadata = try channel.assertReadHeadersOutbound() + XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) + + // Channel becomes inactive + channel.pipeline.fireChannelInactive() + + // The client receives a status explaining the stream was closed. + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init(code: .unavailable, message: "Stream unexpectedly closed."), + [:] + ) + ) + + // We should now be closed: check we can't write anymore. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } + + func testUnexpectedStreamClose_ResetStreamFrame() throws { + let handler = GRPCClientStreamHandler( + methodDescriptor: .init(service: "test", method: "test"), + scheme: .http, + outboundEncoding: .none, + acceptedEncodings: [], + maximumPayloadSize: 1, + skipStateMachineAssertions: true + ) + + let channel = EmbeddedChannel(handler: handler) + + // Write client's initial metadata + XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "/test/test", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + let writtenInitialMetadata = try channel.assertReadHeadersOutbound() + XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) + + // Receive RST_STREAM + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.rstStream(.internalError) + ) + ) + + // The client receives a status explaining RST_STREAM was sent. + XCTAssertEqual( + try channel.readInbound(as: RPCResponsePart.self), + .status( + .init( + code: .unavailable, + message: "Stream unexpectedly closed: a RST_STREAM frame was received." + ), + [:] + ) + ) + + // We should now be closed: check we can't write anymore. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + } + } } extension EmbeddedChannel { diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index e33c2632f..221aa252b 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -976,6 +976,90 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } + // - MARK: Unexpected close + + func testUnexpectedCloseWhenServerIdleOrOpen() throws { + let thrownError = RPCError(code: .deadlineExceeded, message: "Test error") + let reasonAndExpectedStatusPairs = [ + ( + GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, + Status(code: .unavailable, message: "Stream unexpectedly closed.") + ), + ( + GRPCStreamStateMachine.UnexpectedInboundCloseReason.streamReset, + Status( + code: .unavailable, + message: "Stream unexpectedly closed: a RST_STREAM frame was received." + ) + ), + ( + GRPCStreamStateMachine.UnexpectedInboundCloseReason.errorThrown(thrownError), + Status( + code: .unavailable, + message: "Stream unexpectedly closed with error." + ) + ), + ] + let states = [ + TargetStateMachineState.clientIdleServerIdle, + .clientOpenServerIdle, + .clientOpenServerOpen, + .clientClosedServerIdle, + .clientClosedServerOpen, + ] + + for state in states { + for (closeReason, expectedStatus) in reasonAndExpectedStatusPairs { + var stateMachine = self.makeClientStateMachine(targetState: state) + var action = stateMachine.unexpectedInboundClose(reason: closeReason) + + guard case .forwardStatus_clientOnly(let status) = action else { + XCTFail("Should have been `fireError` but was `\(action)` (state: \(state)).") + return + } + XCTAssertEqual(status, expectedStatus) + + // Calling unexpectedInboundClose again should return `doNothing` because + // we're already closed. + action = stateMachine.unexpectedInboundClose(reason: closeReason) + guard case .doNothing = action else { + XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") + return + } + } + } + } + + func testUnexpectedCloseWhenServerClosed() throws { + let closeReasons = [ + GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, + .streamReset, + .errorThrown(RPCError(code: .deadlineExceeded, message: "Test error")), + ] + let states = [ + TargetStateMachineState.clientOpenServerClosed, + .clientClosedServerClosed, + ] + + for state in states { + for closeReason in closeReasons { + var stateMachine = self.makeClientStateMachine(targetState: state) + var action = stateMachine.unexpectedInboundClose(reason: closeReason) + guard case .doNothing = action else { + XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") + return + } + + // Calling unexpectedInboundClose again should return `doNothing` again. + action = stateMachine.unexpectedInboundClose(reason: closeReason) + guard case .doNothing = action else { + XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") + return + } + } + } + } + // - MARK: Common paths func testNormalFlow() throws { @@ -2426,6 +2510,87 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) } + // - MARK: Unexpected close + + func testUnexpectedCloseWhenClientIdleOrOpen() throws { + let reasonAndExpectedErrorPairs = [ + ( + GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, + RPCError(code: .unavailable, message: "Stream unexpectedly closed.") + ), + ( + GRPCStreamStateMachine.UnexpectedInboundCloseReason.streamReset, + RPCError( + code: .unavailable, + message: "Stream unexpectedly closed: a RST_STREAM frame was received." + ) + ), + ( + GRPCStreamStateMachine.UnexpectedInboundCloseReason.errorThrown( + RPCError(code: .deadlineExceeded, message: "Test error") + ), + RPCError(code: .deadlineExceeded, message: "Test error") + ), + ] + let states = [ + TargetStateMachineState.clientIdleServerIdle, + .clientOpenServerIdle, + .clientOpenServerOpen, + .clientOpenServerClosed, + ] + + for state in states { + for (closeReason, expectedError) in reasonAndExpectedErrorPairs { + var stateMachine = self.makeServerStateMachine(targetState: state) + var action = stateMachine.unexpectedInboundClose(reason: closeReason) + guard case .fireError_serverOnly(let error) = action else { + XCTFail("Should have been `fireError` but was `\(action)` (state: \(state)).") + return + } + XCTAssertEqual(error as? RPCError, expectedError) + + // Calling unexpectedInboundClose again should return `doNothing` because + // we're already closed. + action = stateMachine.unexpectedInboundClose(reason: closeReason) + guard case .doNothing = action else { + XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") + return + } + } + } + } + + func testUnexpectedCloseWhenClientClosed() throws { + let closeReasons = [ + GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, + .streamReset, + .errorThrown(RPCError(code: .deadlineExceeded, message: "Test error")), + ] + let states = [ + TargetStateMachineState.clientClosedServerIdle, + .clientClosedServerOpen, + .clientClosedServerClosed, + ] + + for state in states { + for closeReason in closeReasons { + var stateMachine = self.makeServerStateMachine(targetState: state) + var action = stateMachine.unexpectedInboundClose(reason: closeReason) + guard case .doNothing = action else { + XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") + return + } + + // Calling unexpectedInboundClose again should return `doNothing` again. + action = stateMachine.unexpectedInboundClose(reason: closeReason) + guard case .doNothing = action else { + XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") + return + } + } + } + } + // - MARK: Common paths func testNormalFlow() throws { diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index f50b85cb1..d9e6e8aeb 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -38,9 +38,10 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let framesToBeIgnored: [HTTP2Frame.FramePayload] = [ .ping(.init(), ack: false), .goAway(lastStreamID: .rootStream, errorCode: .cancel, opaqueData: nil), - // TODO: add .priority(StreamPriorityData) - right now, StreamPriorityData's - // initialiser is internal, so I can't create one of these frames. - .rstStream(.cancel), + // TODO: uncomment when it's possible to build a `StreamPriorityData`. + // .priority( + // HTTP2Frame.StreamPriorityData(exclusive: false, dependency: .rootStream, weight: 4) + // ), .settings(.ack), .pushPromise(.init(pushedStreamID: .maxID, headers: [:])), .windowUpdate(windowSizeIncrement: 4), @@ -646,7 +647,15 @@ final class GRPCServerStreamHandlerTests: XCTestCase { XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) // Receive them again. Should be a protocol violation. - try channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata))) + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) { error in + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(error.message, "Stream unexpectedly closed.") + } let payload = try XCTUnwrap(channel.readOutbound(as: HTTP2Frame.FramePayload.self)) switch payload { @@ -912,6 +921,171 @@ final class GRPCServerStreamHandlerTests: XCTestCase { XCTAssertEqual(error.message, "RPC was rejected.") } } + + func testUnexpectedStreamClose_ErrorFired() throws { + let channel = EmbeddedChannel() + let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + methodDescriptorPromise: promise, + skipStateMachineAssertions: true + ) + try channel.pipeline.syncOperations.addHandler(handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // An error is fired down the pipeline + let thrownError = ChannelError.connectTimeout(.milliseconds(100)) + channel.pipeline.fireErrorCaught(thrownError) + + // The server handler simply forwards the error. + XCTAssertThrowsError( + ofType: type(of: thrownError), + try channel.throwIfErrorCaught() + ) { error in + XCTAssertEqual(error, thrownError) + } + + // We should now be closed: check we can't write anymore. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + } + } + + func testUnexpectedStreamClose_ChannelInactive() throws { + let channel = EmbeddedChannel() + let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + methodDescriptorPromise: promise, + skipStateMachineAssertions: true + ) + try channel.pipeline.syncOperations.addHandler(handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // Channel becomes inactive + channel.pipeline.fireChannelInactive() + + // The server handler fires an error + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.throwIfErrorCaught() + ) { error in + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(error.message, "Stream unexpectedly closed.") + } + + // We should now be closed: check we can't write anymore. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + } + } + + func testUnexpectedStreamClose_ResetStreamFrame() throws { + let channel = EmbeddedChannel() + let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) + let handler = GRPCServerStreamHandler( + scheme: .http, + acceptedEncodings: [], + maximumPayloadSize: 100, + methodDescriptorPromise: promise, + skipStateMachineAssertions: true + ) + try channel.pipeline.syncOperations.addHandler(handler) + + // Receive client's initial metadata + let clientInitialMetadata: HPACKHeaders = [ + GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", + GRPCHTTP2Keys.scheme.rawValue: "http", + GRPCHTTP2Keys.method.rawValue: "POST", + GRPCHTTP2Keys.contentType.rawValue: "application/grpc", + GRPCHTTP2Keys.te.rawValue: "trailers", + ] + XCTAssertNoThrow( + try channel.writeInbound( + HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) + ) + ) + + // Make sure we haven't sent back an error response, and that we read the initial metadata + XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) + XCTAssertEqual( + try channel.readInbound(as: RPCRequestPart.self), + RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) + ) + + // We receive RST_STREAM frame + // Assert the server handler fires an error + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeInbound( + HTTP2Frame.FramePayload.rstStream(.internalError) + ) + ) { error in + XCTAssertEqual(error.code, .unavailable) + XCTAssertEqual(error.message, "Stream unexpectedly closed: a RST_STREAM frame was received.") + } + + // We should now be closed: check we can't write anymore. + XCTAssertThrowsError( + ofType: RPCError.self, + try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + } + } } extension EmbeddedChannel { From f77ea8088cc8cdfe19bdba6ae4d32ca27b5c7d8c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 3 Jul 2024 16:22:34 +0100 Subject: [PATCH 382/580] Move v1 examples to v1 subdirectory (#1963) Motivation: We'll need to add examples for v2, to make the separation more obvious we can put them in version specfici subdirecoties. Modifications: - Move examples to v1 - Update package manifest - Update proto generation script - Update docs/tutorials Result: v1 examples are separated in their own directory --- Package.swift | 22 ++++++++-------- Package@swift-6.swift | 22 ++++++++-------- Protos/generate.sh | 21 ++++++++++----- .../Implementation/EchoAsyncProvider.swift | 0 .../Echo/Implementation/EchoProvider.swift | 0 .../HPACKHeaders+Prettify.swift | 0 .../Echo/Implementation/Interceptors.swift | 0 .../{ => v1}/Echo/Model/echo.grpc.swift | 0 .../{ => v1}/Echo/Model/echo.pb.swift | 0 Sources/Examples/{ => v1}/Echo/README.md | 0 .../Examples/{ => v1}/Echo/Runtime/Echo.swift | 0 .../{ => v1}/Echo/Runtime/Empty.swift | 0 .../HelloWorld/Client/HelloWorldClient.swift | 0 .../HelloWorld/Model/helloworld.grpc.swift | 0 .../HelloWorld/Model/helloworld.pb.swift | 0 .../Examples/{ => v1}/HelloWorld/README.md | 0 .../HelloWorld/Server/GreeterProvider.swift | 0 .../HelloWorld/Server/HelloWorldServer.swift | 0 .../{ => v1}/PacketCapture/Empty.swift | 0 .../PacketCapture/PacketCapture.swift | 0 .../Examples/{ => v1}/PacketCapture/README.md | 0 Sources/Examples/{ => v1}/README.md | 0 .../Generated/echo.grpc.reflection | 0 .../Generated/helloworld.grpc.reflection | 0 .../ReflectionService/GreeterProvider.swift | 0 .../ReflectionService/ReflectionServer.swift | 0 .../{ => v1}/RouteGuide/Client/Empty.swift | 0 .../RouteGuide/Client/RouteGuideClient.swift | 0 .../RouteGuide/Model/route_guide.grpc.swift | 0 .../RouteGuide/Model/route_guide.pb.swift | 0 .../Examples/{ => v1}/RouteGuide/README.md | 0 .../Server/RouteGuideProvider.swift | 0 .../RouteGuide/Server/RouteGuideServer.swift | 0 .../{ => v1}/RouteGuide/route_guide_db.json | 0 .../Benchmarks/echo.grpc.swift | 2 +- .../Benchmarks/echo.pb.swift | 2 +- .../ReflectionServiceTutorial.md | 26 +++++++++---------- docs/basic-tutorial.md | 24 ++++++++--------- docs/client-without-codegen.md | 2 +- docs/interceptors-tutorial.md | 2 +- docs/quick-start.md | 6 ++--- 41 files changed, 69 insertions(+), 60 deletions(-) rename Sources/Examples/{ => v1}/Echo/Implementation/EchoAsyncProvider.swift (100%) rename Sources/Examples/{ => v1}/Echo/Implementation/EchoProvider.swift (100%) rename Sources/Examples/{ => v1}/Echo/Implementation/HPACKHeaders+Prettify.swift (100%) rename Sources/Examples/{ => v1}/Echo/Implementation/Interceptors.swift (100%) rename Sources/Examples/{ => v1}/Echo/Model/echo.grpc.swift (100%) rename Sources/Examples/{ => v1}/Echo/Model/echo.pb.swift (100%) rename Sources/Examples/{ => v1}/Echo/README.md (100%) rename Sources/Examples/{ => v1}/Echo/Runtime/Echo.swift (100%) rename Sources/Examples/{ => v1}/Echo/Runtime/Empty.swift (100%) rename Sources/Examples/{ => v1}/HelloWorld/Client/HelloWorldClient.swift (100%) rename Sources/Examples/{ => v1}/HelloWorld/Model/helloworld.grpc.swift (100%) rename Sources/Examples/{ => v1}/HelloWorld/Model/helloworld.pb.swift (100%) rename Sources/Examples/{ => v1}/HelloWorld/README.md (100%) rename Sources/Examples/{ => v1}/HelloWorld/Server/GreeterProvider.swift (100%) rename Sources/Examples/{ => v1}/HelloWorld/Server/HelloWorldServer.swift (100%) rename Sources/Examples/{ => v1}/PacketCapture/Empty.swift (100%) rename Sources/Examples/{ => v1}/PacketCapture/PacketCapture.swift (100%) rename Sources/Examples/{ => v1}/PacketCapture/README.md (100%) rename Sources/Examples/{ => v1}/README.md (100%) rename Sources/Examples/{ => v1}/ReflectionService/Generated/echo.grpc.reflection (100%) rename Sources/Examples/{ => v1}/ReflectionService/Generated/helloworld.grpc.reflection (100%) rename Sources/Examples/{ => v1}/ReflectionService/GreeterProvider.swift (100%) rename Sources/Examples/{ => v1}/ReflectionService/ReflectionServer.swift (100%) rename Sources/Examples/{ => v1}/RouteGuide/Client/Empty.swift (100%) rename Sources/Examples/{ => v1}/RouteGuide/Client/RouteGuideClient.swift (100%) rename Sources/Examples/{ => v1}/RouteGuide/Model/route_guide.grpc.swift (100%) rename Sources/Examples/{ => v1}/RouteGuide/Model/route_guide.pb.swift (100%) rename Sources/Examples/{ => v1}/RouteGuide/README.md (100%) rename Sources/Examples/{ => v1}/RouteGuide/Server/RouteGuideProvider.swift (100%) rename Sources/Examples/{ => v1}/RouteGuide/Server/RouteGuideServer.swift (100%) rename Sources/Examples/{ => v1}/RouteGuide/route_guide_db.json (100%) diff --git a/Package.swift b/Package.swift index f3e2adbbe..4a0ff1b07 100644 --- a/Package.swift +++ b/Package.swift @@ -294,7 +294,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/Echo/Model" + path: "Sources/Examples/v1/Echo/Model" ) static let echoImplementation: Target = .target( @@ -306,7 +306,7 @@ extension Target { .nioHTTP2, .protobuf, ], - path: "Sources/Examples/Echo/Implementation" + path: "Sources/Examples/v1/Echo/Implementation" ) static let echo: Target = .executableTarget( @@ -323,7 +323,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - path: "Sources/Examples/Echo/Runtime" + path: "Sources/Examples/v1/Echo/Runtime" ) static let helloWorldModel: Target = .target( @@ -333,7 +333,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/HelloWorld/Model" + path: "Sources/Examples/v1/HelloWorld/Model" ) static let helloWorldClient: Target = .executableTarget( @@ -345,7 +345,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/HelloWorld/Client" + path: "Sources/Examples/v1/HelloWorld/Client" ) static let helloWorldServer: Target = .executableTarget( @@ -357,7 +357,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/HelloWorld/Server" + path: "Sources/Examples/v1/HelloWorld/Server" ) static let routeGuideModel: Target = .target( @@ -367,7 +367,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/RouteGuide/Model" + path: "Sources/Examples/v1/RouteGuide/Model" ) static let routeGuideClient: Target = .executableTarget( @@ -379,7 +379,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/RouteGuide/Client" + path: "Sources/Examples/v1/RouteGuide/Client" ) static let routeGuideServer: Target = .executableTarget( @@ -392,7 +392,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/RouteGuide/Server" + path: "Sources/Examples/v1/RouteGuide/Server" ) static let packetCapture: Target = .executableTarget( @@ -405,7 +405,7 @@ extension Target { .nioExtras, .argumentParser, ], - path: "Sources/Examples/PacketCapture", + path: "Sources/Examples/v1/PacketCapture", exclude: [ "README.md", ] @@ -433,7 +433,7 @@ extension Target { .echoModel, .echoImplementation ], - path: "Sources/Examples/ReflectionService", + path: "Sources/Examples/v1/ReflectionService", resources: [ .copy("Generated") ] diff --git a/Package@swift-6.swift b/Package@swift-6.swift index b36b01ede..f28bc8629 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -586,7 +586,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/Echo/Model", + path: "Sources/Examples/v1/Echo/Model", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -601,7 +601,7 @@ extension Target { .nioHTTP2, .protobuf, ], - path: "Sources/Examples/Echo/Implementation", + path: "Sources/Examples/v1/Echo/Implementation", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -621,7 +621,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - path: "Sources/Examples/Echo/Runtime", + path: "Sources/Examples/v1/Echo/Runtime", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -634,7 +634,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/HelloWorld/Model", + path: "Sources/Examples/v1/HelloWorld/Model", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -649,7 +649,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/HelloWorld/Client", + path: "Sources/Examples/v1/HelloWorld/Client", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -664,7 +664,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/HelloWorld/Server", + path: "Sources/Examples/v1/HelloWorld/Server", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -677,7 +677,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/RouteGuide/Model", + path: "Sources/Examples/v1/RouteGuide/Model", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -692,7 +692,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/RouteGuide/Client", + path: "Sources/Examples/v1/RouteGuide/Client", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -708,7 +708,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/RouteGuide/Server", + path: "Sources/Examples/v1/RouteGuide/Server", swiftSettings: [.swiftLanguageVersion(.v5)] ) } @@ -724,7 +724,7 @@ extension Target { .nioExtras, .argumentParser, ], - path: "Sources/Examples/PacketCapture", + path: "Sources/Examples/v1/PacketCapture", exclude: [ "README.md", ], @@ -758,7 +758,7 @@ extension Target { .echoModel, .echoImplementation ], - path: "Sources/Examples/ReflectionService", + path: "Sources/Examples/v1/ReflectionService", resources: [ .copy("Generated") ], diff --git a/Protos/generate.sh b/Protos/generate.sh index 99dcf177b..4be32ec1d 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -72,17 +72,25 @@ function invoke_protoc { #------------------------------------------------------------------------------ -function generate_echo_example { +function generate_echo_v1_example { local proto="$here/examples/echo/echo.proto" - local output="$root/Sources/Examples/Echo/Model" + local output="$root/Sources/Examples/v1/Echo/Model" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" "TestClient=true" } +function generate_echo_v2_example { + local proto="$here/examples/echo/echo.proto" + local output="$root/Sources/Examples/v2/Echo/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" +} + function generate_routeguide_example { local proto="$here/examples/route_guide/route_guide.proto" - local output="$root/Sources/Examples/RouteGuide/Model" + local output="$root/Sources/Examples/v1/RouteGuide/Model" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" @@ -90,7 +98,7 @@ function generate_routeguide_example { function generate_helloworld_example { local proto="$here/upstream/grpc/examples/helloworld.proto" - local output="$root/Sources/Examples/HelloWorld/Model" + local output="$root/Sources/Examples/v1/HelloWorld/Model" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" @@ -165,7 +173,7 @@ function generate_echo_reflection_data_for_tests { function generate_reflection_data_example { local protos=("$here/examples/echo/echo.proto" "$here/upstream/grpc/examples/helloworld.proto") - local output="$root/Sources/Examples/ReflectionService/Generated" + local output="$root/Sources/Examples/v1/ReflectionService/Generated" for proto in "${protos[@]}"; do generate_grpc "$proto" "$(dirname "$proto")" "$output" "Client=false" "Server=false" "ReflectionData=true" @@ -243,7 +251,8 @@ function generate_health_service { #------------------------------------------------------------------------------ # Examples -generate_echo_example +generate_echo_v1_example +generate_echo_v2_example generate_routeguide_example generate_helloworld_example generate_reflection_data_example diff --git a/Sources/Examples/Echo/Implementation/EchoAsyncProvider.swift b/Sources/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift similarity index 100% rename from Sources/Examples/Echo/Implementation/EchoAsyncProvider.swift rename to Sources/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift diff --git a/Sources/Examples/Echo/Implementation/EchoProvider.swift b/Sources/Examples/v1/Echo/Implementation/EchoProvider.swift similarity index 100% rename from Sources/Examples/Echo/Implementation/EchoProvider.swift rename to Sources/Examples/v1/Echo/Implementation/EchoProvider.swift diff --git a/Sources/Examples/Echo/Implementation/HPACKHeaders+Prettify.swift b/Sources/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift similarity index 100% rename from Sources/Examples/Echo/Implementation/HPACKHeaders+Prettify.swift rename to Sources/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift diff --git a/Sources/Examples/Echo/Implementation/Interceptors.swift b/Sources/Examples/v1/Echo/Implementation/Interceptors.swift similarity index 100% rename from Sources/Examples/Echo/Implementation/Interceptors.swift rename to Sources/Examples/v1/Echo/Implementation/Interceptors.swift diff --git a/Sources/Examples/Echo/Model/echo.grpc.swift b/Sources/Examples/v1/Echo/Model/echo.grpc.swift similarity index 100% rename from Sources/Examples/Echo/Model/echo.grpc.swift rename to Sources/Examples/v1/Echo/Model/echo.grpc.swift diff --git a/Sources/Examples/Echo/Model/echo.pb.swift b/Sources/Examples/v1/Echo/Model/echo.pb.swift similarity index 100% rename from Sources/Examples/Echo/Model/echo.pb.swift rename to Sources/Examples/v1/Echo/Model/echo.pb.swift diff --git a/Sources/Examples/Echo/README.md b/Sources/Examples/v1/Echo/README.md similarity index 100% rename from Sources/Examples/Echo/README.md rename to Sources/Examples/v1/Echo/README.md diff --git a/Sources/Examples/Echo/Runtime/Echo.swift b/Sources/Examples/v1/Echo/Runtime/Echo.swift similarity index 100% rename from Sources/Examples/Echo/Runtime/Echo.swift rename to Sources/Examples/v1/Echo/Runtime/Echo.swift diff --git a/Sources/Examples/Echo/Runtime/Empty.swift b/Sources/Examples/v1/Echo/Runtime/Empty.swift similarity index 100% rename from Sources/Examples/Echo/Runtime/Empty.swift rename to Sources/Examples/v1/Echo/Runtime/Empty.swift diff --git a/Sources/Examples/HelloWorld/Client/HelloWorldClient.swift b/Sources/Examples/v1/HelloWorld/Client/HelloWorldClient.swift similarity index 100% rename from Sources/Examples/HelloWorld/Client/HelloWorldClient.swift rename to Sources/Examples/v1/HelloWorld/Client/HelloWorldClient.swift diff --git a/Sources/Examples/HelloWorld/Model/helloworld.grpc.swift b/Sources/Examples/v1/HelloWorld/Model/helloworld.grpc.swift similarity index 100% rename from Sources/Examples/HelloWorld/Model/helloworld.grpc.swift rename to Sources/Examples/v1/HelloWorld/Model/helloworld.grpc.swift diff --git a/Sources/Examples/HelloWorld/Model/helloworld.pb.swift b/Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift similarity index 100% rename from Sources/Examples/HelloWorld/Model/helloworld.pb.swift rename to Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift diff --git a/Sources/Examples/HelloWorld/README.md b/Sources/Examples/v1/HelloWorld/README.md similarity index 100% rename from Sources/Examples/HelloWorld/README.md rename to Sources/Examples/v1/HelloWorld/README.md diff --git a/Sources/Examples/HelloWorld/Server/GreeterProvider.swift b/Sources/Examples/v1/HelloWorld/Server/GreeterProvider.swift similarity index 100% rename from Sources/Examples/HelloWorld/Server/GreeterProvider.swift rename to Sources/Examples/v1/HelloWorld/Server/GreeterProvider.swift diff --git a/Sources/Examples/HelloWorld/Server/HelloWorldServer.swift b/Sources/Examples/v1/HelloWorld/Server/HelloWorldServer.swift similarity index 100% rename from Sources/Examples/HelloWorld/Server/HelloWorldServer.swift rename to Sources/Examples/v1/HelloWorld/Server/HelloWorldServer.swift diff --git a/Sources/Examples/PacketCapture/Empty.swift b/Sources/Examples/v1/PacketCapture/Empty.swift similarity index 100% rename from Sources/Examples/PacketCapture/Empty.swift rename to Sources/Examples/v1/PacketCapture/Empty.swift diff --git a/Sources/Examples/PacketCapture/PacketCapture.swift b/Sources/Examples/v1/PacketCapture/PacketCapture.swift similarity index 100% rename from Sources/Examples/PacketCapture/PacketCapture.swift rename to Sources/Examples/v1/PacketCapture/PacketCapture.swift diff --git a/Sources/Examples/PacketCapture/README.md b/Sources/Examples/v1/PacketCapture/README.md similarity index 100% rename from Sources/Examples/PacketCapture/README.md rename to Sources/Examples/v1/PacketCapture/README.md diff --git a/Sources/Examples/README.md b/Sources/Examples/v1/README.md similarity index 100% rename from Sources/Examples/README.md rename to Sources/Examples/v1/README.md diff --git a/Sources/Examples/ReflectionService/Generated/echo.grpc.reflection b/Sources/Examples/v1/ReflectionService/Generated/echo.grpc.reflection similarity index 100% rename from Sources/Examples/ReflectionService/Generated/echo.grpc.reflection rename to Sources/Examples/v1/ReflectionService/Generated/echo.grpc.reflection diff --git a/Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection b/Sources/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection similarity index 100% rename from Sources/Examples/ReflectionService/Generated/helloworld.grpc.reflection rename to Sources/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection diff --git a/Sources/Examples/ReflectionService/GreeterProvider.swift b/Sources/Examples/v1/ReflectionService/GreeterProvider.swift similarity index 100% rename from Sources/Examples/ReflectionService/GreeterProvider.swift rename to Sources/Examples/v1/ReflectionService/GreeterProvider.swift diff --git a/Sources/Examples/ReflectionService/ReflectionServer.swift b/Sources/Examples/v1/ReflectionService/ReflectionServer.swift similarity index 100% rename from Sources/Examples/ReflectionService/ReflectionServer.swift rename to Sources/Examples/v1/ReflectionService/ReflectionServer.swift diff --git a/Sources/Examples/RouteGuide/Client/Empty.swift b/Sources/Examples/v1/RouteGuide/Client/Empty.swift similarity index 100% rename from Sources/Examples/RouteGuide/Client/Empty.swift rename to Sources/Examples/v1/RouteGuide/Client/Empty.swift diff --git a/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift b/Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift similarity index 100% rename from Sources/Examples/RouteGuide/Client/RouteGuideClient.swift rename to Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift diff --git a/Sources/Examples/RouteGuide/Model/route_guide.grpc.swift b/Sources/Examples/v1/RouteGuide/Model/route_guide.grpc.swift similarity index 100% rename from Sources/Examples/RouteGuide/Model/route_guide.grpc.swift rename to Sources/Examples/v1/RouteGuide/Model/route_guide.grpc.swift diff --git a/Sources/Examples/RouteGuide/Model/route_guide.pb.swift b/Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift similarity index 100% rename from Sources/Examples/RouteGuide/Model/route_guide.pb.swift rename to Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift diff --git a/Sources/Examples/RouteGuide/README.md b/Sources/Examples/v1/RouteGuide/README.md similarity index 100% rename from Sources/Examples/RouteGuide/README.md rename to Sources/Examples/v1/RouteGuide/README.md diff --git a/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift b/Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift similarity index 100% rename from Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift rename to Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift diff --git a/Sources/Examples/RouteGuide/Server/RouteGuideServer.swift b/Sources/Examples/v1/RouteGuide/Server/RouteGuideServer.swift similarity index 100% rename from Sources/Examples/RouteGuide/Server/RouteGuideServer.swift rename to Sources/Examples/v1/RouteGuide/Server/RouteGuideServer.swift diff --git a/Sources/Examples/RouteGuide/route_guide_db.json b/Sources/Examples/v1/RouteGuide/route_guide_db.json similarity index 100% rename from Sources/Examples/RouteGuide/route_guide_db.json rename to Sources/Examples/v1/RouteGuide/route_guide_db.json diff --git a/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift b/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift index 4461d497d..d7bd05ba6 120000 --- a/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift @@ -1 +1 @@ -../../Examples/Echo/Model/echo.grpc.swift \ No newline at end of file +../../Examples/v1/Echo/Model/echo.grpc.swift \ No newline at end of file diff --git a/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift b/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift index b4fd86913..f6d794789 120000 --- a/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift @@ -1 +1 @@ -../../Examples/Echo/Model/echo.pb.swift \ No newline at end of file +../../Examples/v1/Echo/Model/echo.pb.swift \ No newline at end of file diff --git a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md index 0ad6941bf..c5205c75f 100644 --- a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md +++ b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md @@ -4,7 +4,7 @@ This tutorial goes through the steps of adding Reflection service to a server, running it and testing it using gRPCurl. The server used in this example is implemented at - [Sources/Examples/ReflectionService/ReflectionServer.swift][reflection-server] + [Sources/Examples/v1/ReflectionService/ReflectionServer.swift][reflection-server] and it supports the "Greeter", "Echo", and "Reflection" services. @@ -38,27 +38,27 @@ describing the services of the server and the version of the reflection service. The server from this example uses the `GreeterProvider` and the `EchoProvider`, besides the `ReflectionService`. -The associated proto files are located at `Sources/Examples/HelloWorld/Model/helloworld.proto`, and -`Sources/Examples/Echo/Model/echo.proto` respectively. +The associated proto files are located at `Sources/Examples/v1/HelloWorld/Model/helloworld.proto`, and +`Sources/Examples/v1/Echo/Model/echo.proto` respectively. In order to generate the reflection data for the `helloworld.proto`, you can run the following command: ```sh -$ protoc Sources/Examples/HelloWorld/Model/helloworld.proto \ - --proto_path=Sources/Examples/HelloWorld/Model \ +$ protoc Sources/Examples/v1/HelloWorld/Model/helloworld.proto \ + --proto_path=Sources/Examples/v1/HelloWorld/Model \ --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ - --grpc-swift_out=Sources/Examples/ReflectionService/Generated + --grpc-swift_out=Sources/Examples/v1/ReflectionService/Generated ``` Let's break the command down: - The first argument passed to `protoc` is the path to the `.proto` file to generate reflection data - for: [`Sources/Examples/HelloWorld/Model/helloworld.proto`][helloworld-proto]. -- The `proto_path` flag is the path to search for imports: `Sources/Examples/HelloWorld/Model`. + for: [`Sources/Examples/v1/HelloWorld/Model/helloworld.proto`][helloworld-proto]. +- The `proto_path` flag is the path to search for imports: `Sources/Examples/v1/HelloWorld/Model`. - The 'grpc-swift_opt' flag allows us to list options for the Swift generator. To generate only the reflection data set: `Client=false,Server=false,ReflectionData=true`. - The `grpc-swift_out` flag is used to set the path of the directory - where the generated file will be located: `Sources/Examples/ReflectionService/Generated`. + where the generated file will be located: `Sources/Examples/v1/ReflectionService/Generated`. This command assumes that the `protoc-gen-grpc-swift` plugin is in your `$PATH` environment variable. You can learn how to get the plugin from this section of the `grpc-swift` README: @@ -219,9 +219,9 @@ Note that when specifying a service, a method or a symbol, we have to use the fu [grpc-cli]: https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md [v1]: ../v1/reflection-v1.proto [v1alpha]: ../v1Alpha/reflection-v1alpha.proto -[reflection-server]: ../../Examples/ReflectionService/ReflectionServer.swift -[helloworld-proto]: ../../Examples/HelloWorld/Model/helloworld.proto -[echo-proto]: ../../Examples/Echo/Model/echo.proto +[reflection-server]: ../../Examples/v1/ReflectionService/ReflectionServer.swift +[helloworld-proto]: ../../Examples/v1/HelloWorld/Model/helloworld.proto +[echo-proto]: ../../Examples/v1/Echo/Model/echo.proto [grpcurl-v188]: https://github.com/fullstorydev/grpcurl/releases/tag/v1.8.8 [swiftpm-resources]: https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageDescription.md#resource -[spm-plugin]: ../../protoc-gen-grpc-swift/Docs.docc/spm-plugin.md \ No newline at end of file +[spm-plugin]: ../../protoc-gen-grpc-swift/Docs.docc/spm-plugin.md diff --git a/docs/basic-tutorial.md b/docs/basic-tutorial.md index 4c67256ee..a48f7736c 100644 --- a/docs/basic-tutorial.md +++ b/docs/basic-tutorial.md @@ -34,7 +34,7 @@ updating. ### Example code and setup The example code for our tutorial is in -[grpc/grpc-swift/Sources/Examples/RouteGuide][routeguide-source]. +[grpc/grpc-swift/Sources/Examples/v1/RouteGuide][routeguide-source]. To download the example, clone the latest release in `grpc-swift` repository by running the following command (replacing `x.y.z` with the latest release, for example `1.7.0`): @@ -43,10 +43,10 @@ example `1.7.0`): $ git clone -b x.y.z https://github.com/grpc/grpc-swift ``` -Then change your current directory to `grpc-swift/Sources/Examples/RouteGuide`: +Then change your current directory to `grpc-swift/Sources/Examples/v1/RouteGuide`: ```sh -$ cd grpc-swift/Sources/Examples/RouteGuide +$ cd grpc-swift/Sources/Examples/v1/RouteGuide ``` @@ -151,7 +151,7 @@ $ Protos/generate.sh ``` Running this command generates the following files in the -`Sources/Examples/RouteGuide/Model` directory: +`Sources/Examples/v1/RouteGuide/Model` directory: - `route_guide.pb.swift`, which contains the implementation of your generated message classes @@ -165,10 +165,10 @@ $ protoc Protos/examples/route_guide/route_guide.proto \ --proto_path=Protos/examples/route_guide \ --plugin=./.build/debug/protoc-gen-swift \ --swift_opt=Visibility=Public \ - --swift_out=Sources/Examples/RouteGuide/Model \ + --swift_out=Sources/Examples/v1/RouteGuide/Model \ --plugin=./.build/debug/protoc-gen-grpc-swift \ --grpc-swift_opt=Visibility=Public \ - --grpc-swift_out=Sources/Examples/RouteGuide/Model + --grpc-swift_out=Sources/Examples/v1/RouteGuide/Model ``` We invoke the protocol buffer compiler `protoc` with the path to our service @@ -197,7 +197,7 @@ There are two parts to making our `RouteGuide` service do its job: service responses. You can find our example `RouteGuide` provider in -[grpc-swift/Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift][routeguide-provider]. +[grpc-swift/Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift][routeguide-provider]. Let's take a closer look at how it works. #### Implementing RouteGuide @@ -455,7 +455,7 @@ program from exiting (since `close()` is never called on the server). In this section, we'll look at creating a Swift client for our `RouteGuide` service. You can see our complete example client code in -[grpc-swift/Sources/Examples/RouteGuide/Client/RouteGuideClient.swift][routeguide-client]. +[grpc-swift/Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift][routeguide-client]. #### Creating a stub @@ -604,11 +604,11 @@ Follow the instructions in the Route Guide example directory [protobuf-docs]: https://developers.google.com/protocol-buffers/docs/proto3 [protobuf-releases]: https://github.com/google/protobuf/releases [protocol-buffers]: https://developers.google.com/protocol-buffers/docs/overview -[routeguide-client]: ../Sources/Examples/RouteGuide/Client/RouteGuideClient.swift +[routeguide-client]: ../Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift [routeguide-proto]: ../Protos/examples/route_guide/route_guide.proto -[routeguide-provider]: ../Sources/Examples/RouteGuide/Server/RouteGuideProvider.swift -[routeguide-readme]: ../Sources/Examples/RouteGuide/README.md -[routeguide-source]: ../Sources/Examples/RouteGuide +[routeguide-provider]: ../Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift +[routeguide-readme]: ../Sources/Examples/v1/RouteGuide/README.md +[routeguide-source]: ../Sources/Examples/v1/RouteGuide [run-protoc]: ../Protos/generate.sh [swift-protobuf-guide]: https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md [swift-protobuf]: https://github.com/apple/swift-protobuf diff --git a/docs/client-without-codegen.md b/docs/client-without-codegen.md index 4a24cf3c1..ddb07132c 100644 --- a/docs/client-without-codegen.md +++ b/docs/client-without-codegen.md @@ -27,4 +27,4 @@ using `makeClientStreamingCall`, `makeServerStreamingCall`, and These methods are also available on generated clients, allowing you to call methods which have been added to the service since the client was generated. -[helloworld-source]: ../Sources/Examples/HelloWorld +[helloworld-source]: ../Sources/Examples/v1/HelloWorld diff --git a/docs/interceptors-tutorial.md b/docs/interceptors-tutorial.md index 36648067d..aee221c7a 100644 --- a/docs/interceptors-tutorial.md +++ b/docs/interceptors-tutorial.md @@ -293,4 +293,4 @@ by invoking methods on the `context` from that `EventLoop`. [quick-start]: ./quick-start.md [basic-tutorial]: ./basic-tutorial.md -[echo-example]: ../Sources/Examples/Echo +[echo-example]: ../Sources/Examples/v1/Echo diff --git a/docs/quick-start.md b/docs/quick-start.md index 6dbf5ae21..476928503 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -109,7 +109,7 @@ message HelloReply { ### Update and run the application We need to regenerate -`Sources/Examples/HelloWorld/Model/helloworld.grpc.swift`, which +`Sources/Examples/v1/HelloWorld/Model/helloworld.grpc.swift`, which contains our generated gRPC client and server classes. From the `grpc-swift` directory run @@ -127,7 +127,7 @@ parts of our example application. #### Update the server In the same directory, open -`Sources/Examples/HelloWorld/Server/GreeterProvider.swift`. Implement the new +`Sources/Examples/v1/HelloWorld/Server/GreeterProvider.swift`. Implement the new method like this: ```swift @@ -159,7 +159,7 @@ final class GreeterProvider: Helloworld_GreeterAsyncProvider { #### Update the client In the same directory, open -`Sources/Examples/HelloWorld/Client/HelloWorldClient.swift`. Call the new method like this: +`Sources/Examples/v1/HelloWorld/Client/HelloWorldClient.swift`. Call the new method like this: ```swift func run() async throws { From 43aa501c505b0447291755fd350914c1df4e94bc Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 4 Jul 2024 08:01:39 +0100 Subject: [PATCH 383/580] Add a v2 Echo example (#1964) Motivation: We should migrate the echo example to v2. Modification: Add a v2 echo example Result: We can run echo for v2. --- Package@swift-6.swift | 22 +- Sources/Examples/v2/Echo/Echo.swift | 27 ++ .../v2/Echo/Generated/echo.grpc.swift | 344 ++++++++++++++++++ .../Examples/v2/Echo/Generated/echo.pb.swift | 134 +++++++ .../v2/Echo/Subcommands/ClientArguments.swift | 35 ++ .../v2/Echo/Subcommands/Collect.swift | 59 +++ .../Examples/v2/Echo/Subcommands/Expand.swift | 55 +++ .../Examples/v2/Echo/Subcommands/Get.swift | 52 +++ .../Examples/v2/Echo/Subcommands/Serve.swift | 77 ++++ .../Examples/v2/Echo/Subcommands/Update.swift | 60 +++ 10 files changed, 864 insertions(+), 1 deletion(-) create mode 100644 Sources/Examples/v2/Echo/Echo.swift create mode 100644 Sources/Examples/v2/Echo/Generated/echo.grpc.swift create mode 100644 Sources/Examples/v2/Echo/Generated/echo.pb.swift create mode 100644 Sources/Examples/v2/Echo/Subcommands/ClientArguments.swift create mode 100644 Sources/Examples/v2/Echo/Subcommands/Collect.swift create mode 100644 Sources/Examples/v2/Echo/Subcommands/Expand.swift create mode 100644 Sources/Examples/v2/Echo/Subcommands/Get.swift create mode 100644 Sources/Examples/v2/Echo/Subcommands/Serve.swift create mode 100644 Sources/Examples/v2/Echo/Subcommands/Update.swift diff --git a/Package@swift-6.swift b/Package@swift-6.swift index f28bc8629..65d053bfa 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -626,6 +626,23 @@ extension Target { ) } + static var echo_v2: Target { + .executableTarget( + name: "echo-v2", + dependencies: [ + .grpcCore, + .grpcProtobuf, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .argumentParser, + ].appending( + .nioSSL, if: includeNIOSSL + ), + path: "Sources/Examples/v2/Echo", + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + ) + } + static var helloWorldModel: Target { .target( name: "HelloWorldModel", @@ -935,7 +952,10 @@ let package = Package( .grpcHTTP2TransportTests, .grpcProtobufTests, .grpcProtobufCodeGenTests, - .inProcessInteroperabilityTests + .inProcessInteroperabilityTests, + + // v2 examples + .echo_v2, ] ) diff --git a/Sources/Examples/v2/Echo/Echo.swift b/Sources/Examples/v2/Echo/Echo.swift new file mode 100644 index 000000000..7d8f26efb --- /dev/null +++ b/Sources/Examples/v2/Echo/Echo.swift @@ -0,0 +1,27 @@ +/* + * 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 ArgumentParser + +@main +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Echo: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "echo", + abstract: "A multi-tool to run an echo server and execute RPCs against it.", + subcommands: [Serve.self, Get.self, Collect.self, Expand.self, Update.self] + ) +} diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift new file mode 100644 index 000000000..10428bc78 --- /dev/null +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -0,0 +1,344 @@ +// Copyright (c) 2015, Google Inc. +// +// 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: echo.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +internal enum Echo_Echo { + internal enum Method { + internal enum Get { + internal typealias Input = Echo_EchoRequest + internal typealias Output = Echo_EchoResponse + internal static let descriptor = MethodDescriptor( + service: "echo.Echo", + method: "Get" + ) + } + internal enum Expand { + internal typealias Input = Echo_EchoRequest + internal typealias Output = Echo_EchoResponse + internal static let descriptor = MethodDescriptor( + service: "echo.Echo", + method: "Expand" + ) + } + internal enum Collect { + internal typealias Input = Echo_EchoRequest + internal typealias Output = Echo_EchoResponse + internal static let descriptor = MethodDescriptor( + service: "echo.Echo", + method: "Collect" + ) + } + internal enum Update { + internal typealias Input = Echo_EchoRequest + internal typealias Output = Echo_EchoResponse + internal static let descriptor = MethodDescriptor( + service: "echo.Echo", + method: "Update" + ) + } + internal static let descriptors: [MethodDescriptor] = [ + Get.descriptor, + Expand.descriptor, + Collect.descriptor, + Update.descriptor + ] + } + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias StreamingServiceProtocol = Echo_EchoStreamingServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ServiceProtocol = Echo_EchoServiceProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias ClientProtocol = Echo_EchoClientProtocol + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal typealias Client = Echo_EchoClient +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Immediately returns an echo of a request. + func get(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Splits a request into words and returns each word in a stream of messages. + func expand(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Collects a stream of messages and returns them concatenated when the caller closes. + func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + + /// Streams back messages as they are received in an input stream. + func update(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Echo_Echo.StreamingServiceProtocol { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Echo_Echo.Method.Get.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.get(request: request) + } + ) + router.registerHandler( + forMethod: Echo_Echo.Method.Expand.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.expand(request: request) + } + ) + router.registerHandler( + forMethod: Echo_Echo.Method.Collect.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.collect(request: request) + } + ) + router.registerHandler( + forMethod: Echo_Echo.Method.Update.descriptor, + deserializer: ProtobufDeserializer(), + serializer: ProtobufSerializer(), + handler: { request in + try await self.update(request: request) + } + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { + /// Immediately returns an echo of a request. + func get(request: ServerRequest.Single) async throws -> ServerResponse.Single + + /// Splits a request into words and returns each word in a stream of messages. + func expand(request: ServerRequest.Single) async throws -> ServerResponse.Stream + + /// Collects a stream of messages and returns them concatenated when the caller closes. + func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Single + + /// Streams back messages as they are received in an input stream. + func update(request: ServerRequest.Stream) async throws -> ServerResponse.Stream +} + +/// Partial conformance to `Echo_EchoStreamingServiceProtocol`. +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Echo_Echo.ServiceProtocol { + internal func get(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.get(request: ServerRequest.Single(stream: request)) + return ServerResponse.Stream(single: response) + } + + internal func expand(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.expand(request: ServerRequest.Single(stream: request)) + return response + } + + internal func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + let response = try await self.collect(request: request) + return ServerResponse.Stream(single: response) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal protocol Echo_EchoClientProtocol: Sendable { + /// Immediately returns an echo of a request. + func get( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// Splits a request into words and returns each word in a stream of messages. + func expand( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + /// Collects a stream of messages and returns them concatenated when the caller closes. + func collect( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// Streams back messages as they are received in an input stream. + func update( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Echo_Echo.ClientProtocol { + internal func get( + request: ClientRequest.Single, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.get( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } + + internal func expand( + request: ClientRequest.Single, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.expand( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } + + internal func collect( + request: ClientRequest.Stream, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.collect( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } + + internal func update( + request: ClientRequest.Stream, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.update( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { + private let client: GRPCCore.GRPCClient + + internal init(client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Immediately returns an echo of a request. + internal func get( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Echo_Echo.Method.Get.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + /// Splits a request into words and returns each word in a stream of messages. + internal func expand( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Echo_Echo.Method.Expand.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + /// Collects a stream of messages and returns them concatenated when the caller closes. + internal func collect( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Echo_Echo.Method.Collect.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + /// Streams back messages as they are received in an input stream. + internal func update( + request: ClientRequest.Stream, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Echo_Echo.Method.Update.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } +} \ No newline at end of file diff --git a/Sources/Examples/v2/Echo/Generated/echo.pb.swift b/Sources/Examples/v2/Echo/Generated/echo.pb.swift new file mode 100644 index 000000000..3ee4e8d36 --- /dev/null +++ b/Sources/Examples/v2/Echo/Generated/echo.pb.swift @@ -0,0 +1,134 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: echo.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright (c) 2015, Google Inc. +// +// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct Echo_EchoRequest { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The text of a message to be echoed. + var text: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Echo_EchoResponse { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The text of an echo response. + var text: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +#if swift(>=5.5) && canImport(_Concurrency) +extension Echo_EchoRequest: @unchecked Sendable {} +extension Echo_EchoResponse: @unchecked Sendable {} +#endif // swift(>=5.5) && canImport(_Concurrency) + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "echo" + +extension Echo_EchoRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EchoRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "text"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.text.isEmpty { + try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Echo_EchoRequest, rhs: Echo_EchoRequest) -> Bool { + if lhs.text != rhs.text {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Echo_EchoResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EchoResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "text"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.text.isEmpty { + try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Echo_EchoResponse, rhs: Echo_EchoResponse) -> Bool { + if lhs.text != rhs.text {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/Examples/v2/Echo/Subcommands/ClientArguments.swift b/Sources/Examples/v2/Echo/Subcommands/ClientArguments.swift new file mode 100644 index 000000000..7dea8e59f --- /dev/null +++ b/Sources/Examples/v2/Echo/Subcommands/ClientArguments.swift @@ -0,0 +1,35 @@ +/* + * 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 ArgumentParser +import GRPCHTTP2Core + +struct ClientArguments: ParsableArguments { + @Option(help: "The server's listening port") + var port: Int = 1234 + + @Option(help: "The number of times to repeat the call") + var repetitions: Int = 1 + + @Option(help: "Message to send to the server") + var message: String +} + +extension ClientArguments { + var target: any ResolvableTarget { + return .ipv4(host: "127.0.0.1", port: self.port) + } +} diff --git a/Sources/Examples/v2/Echo/Subcommands/Collect.swift b/Sources/Examples/v2/Echo/Subcommands/Collect.swift new file mode 100644 index 000000000..65594b02e --- /dev/null +++ b/Sources/Examples/v2/Echo/Subcommands/Collect.swift @@ -0,0 +1,59 @@ +/* + * 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 ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Collect: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Makes a client streaming RPC to the echo server." + ) + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let echo = Echo_EchoClient(client: client) + + for _ in 0 ..< self.arguments.repetitions { + let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in + for part in self.arguments.message.split(separator: " ") { + print("collect → \(part)") + try await writer.write(.with { $0.text = String(part) }) + } + } + + try await echo.collect(request: request) { response in + let message = try response.message + print("collect ← \(message.text)") + } + } + + client.close() + } + } +} diff --git a/Sources/Examples/v2/Echo/Subcommands/Expand.swift b/Sources/Examples/v2/Echo/Subcommands/Expand.swift new file mode 100644 index 000000000..a920cb47a --- /dev/null +++ b/Sources/Examples/v2/Echo/Subcommands/Expand.swift @@ -0,0 +1,55 @@ +/* + * 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 ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Expand: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Makes a server streaming RPC to the echo server." + ) + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let echo = Echo_EchoClient(client: client) + + for _ in 0 ..< self.arguments.repetitions { + let message = Echo_EchoRequest.with { $0.text = self.arguments.message } + print("expand → \(message.text)") + try await echo.expand(request: ClientRequest.Single(message: message)) { response in + for try await message in response.messages { + print("get ← \(message.text)") + } + } + } + + client.close() + } + } +} diff --git a/Sources/Examples/v2/Echo/Subcommands/Get.swift b/Sources/Examples/v2/Echo/Subcommands/Get.swift new file mode 100644 index 000000000..94937d5df --- /dev/null +++ b/Sources/Examples/v2/Echo/Subcommands/Get.swift @@ -0,0 +1,52 @@ +/* + * 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 ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Get: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Makes a unary RPC to the echo server.") + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let echo = Echo_EchoClient(client: client) + + for _ in 0 ..< self.arguments.repetitions { + let message = Echo_EchoRequest.with { $0.text = self.arguments.message } + print("get → \(message.text)") + try await echo.get(request: ClientRequest.Single(message: message)) { response in + let message = try response.message + print("get ← \(message.text)") + } + } + + client.close() + } + } +} diff --git a/Sources/Examples/v2/Echo/Subcommands/Serve.swift b/Sources/Examples/v2/Echo/Subcommands/Serve.swift new file mode 100644 index 000000000..62658d139 --- /dev/null +++ b/Sources/Examples/v2/Echo/Subcommands/Serve.swift @@ -0,0 +1,77 @@ +/* + * 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 ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Serve: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Starts an echo server.") + + @Option(help: "The port to listen on") + var port: Int = 1234 + + func run() async throws { + let transport = HTTP2ServerTransport.Posix(address: .ipv4(host: "127.0.0.1", port: self.port)) + let server = GRPCServer(transport: transport, services: [EchoService()]) + try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await server.run() } + let address = try await transport.listeningAddress + print("server listening on \(address)") + } + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +struct EchoService: Echo_EchoServiceProtocol { + func get( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + return ServerResponse.Single(message: .with { $0.text = request.message.text }) + } + + func collect( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + let messages = try await request.messages.reduce(into: []) { $0.append($1.text) } + let joined = messages.joined(separator: " ") + return ServerResponse.Single(message: .with { $0.text = joined }) + } + + func expand( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + let parts = request.message.text.split(separator: " ") + let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } } + try await writer.write(contentsOf: messages) + return [:] + } + } + + func update( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for try await message in request.messages { + try await writer.write(.with { $0.text = message.text }) + } + return [:] + } + } +} diff --git a/Sources/Examples/v2/Echo/Subcommands/Update.swift b/Sources/Examples/v2/Echo/Subcommands/Update.swift new file mode 100644 index 000000000..d0d06ddbc --- /dev/null +++ b/Sources/Examples/v2/Echo/Subcommands/Update.swift @@ -0,0 +1,60 @@ +/* + * 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 ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +struct Update: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Makes a bidirectional server streaming RPC to the echo server." + ) + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let echo = Echo_EchoClient(client: client) + + for _ in 0 ..< self.arguments.repetitions { + let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in + for part in self.arguments.message.split(separator: " ") { + print("update → \(part)") + try await writer.write(.with { $0.text = String(part) }) + } + } + + try await echo.update(request: request) { response in + for try await message in response.messages { + print("update ← \(message.text)") + } + } + } + + client.close() + } + } +} From d6db47efe824a0dd07f9eef07ae9c9486d4270ef Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 5 Jul 2024 09:09:41 +0100 Subject: [PATCH 384/580] Update test certs (#1966) Motivation: The self-signed test certs expired resulting in test failures. Modifications: - Update test certs Result: Tests pass --- .../GRPCSampleData/GRPCSwiftCertificate.swift | 380 +++++++++--------- 1 file changed, 190 insertions(+), 190 deletions(-) diff --git a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift index 042ffc063..6f1704006 100644 --- a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift +++ b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023, gRPC Authors All rights reserved. + * 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. @@ -33,49 +33,49 @@ public struct SampleCertificate { public static let ca = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), commonName: "some-ca", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) public static let otherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), commonName: "some-other-ca", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) public static let server = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) public static let exampleServer = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), commonName: "example.com", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) public static let serverSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) public static let client = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) public static let clientSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) public static let exampleServerWithExplicitCurve = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_720_088_924) + notAfter: Date(timeIntervalSince1970: 1_751_648_938) ) } @@ -106,258 +106,258 @@ public struct SamplePrivateKey { private let caCert = """ -----BEGIN CERTIFICATE----- - MIICoDCCAYgCCQCgCA1/0dKfFjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz - b21lLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowEjEQMA4GA1UE - AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALTi2aJy - Vw3E0OQwNIm9GZOG4E/Rc0atKoJes9yWaMrMPGwoenLEc2JNIvJSdBGZHKO7HKAG - OnffpqVIXtRBIU7l8HEhX97Q+knI6wPz8O7JaGVf6KznLa2eFO6xGM1pogO7m+/M - 0mw8LSftn2IEiJk9v00qj+WgfwJJqL/TUZRoT5M2+u99uiaW7bnI1+1vawo5i7A1 - zfN6SBud5K/BaEYcAjxX1JMWCJLWSuOFZArWX7Je2MP+LqZkjh8kQO+d8ZZaLSIs - ujd6x6/r365Sl24l4auNfWy/5V1Ctfxl4avupAm7CpmEFpswe/ucNHkD0drUCzvt - hBeR3coLXWgbQs0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJm1Yntrrl6WxPbsA - s1DrI9YHdQjUNkouX0PtGp4yKrP7hwTclIhHjlGaQRJ2p1I7hllCMCPDa2YZa714 - XhtvEmpWOeLXMFolpKEn83kccvkQviZ3yd2lKH64jDX1/g2Rf6dXhDZMKrMAkEdx - X3JwZwPxwb8VDtac7TkVgOcQFHRzdX2g6pQXz3eNsjckGNJgzzl/ln6DrHHDbruI - M7bfnc2ZCBcHUCLWts8LnX2ekUq9KOxMe4e3sD27sKPizklNfGH4Rdg4LByhkx3S - GGR3ziWyixfcs4BNhA5mbsvb8vpPdtOh1oFt+TtPxlQ2FQOnSHk6wF285XggYYgv - p8pG5Q== + MIICoDCCAYgCCQDhjLeDGLctlTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz + b21lLWNhMB4XDTI0MDcwNDE3MDg1OFoXDTI1MDcwNDE3MDg1OFowEjEQMA4GA1UE + AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzrSLrp + IcroapZ1CB2jz5N1O+S22oLpKyYE2KT+lXN1Bp44ni4bkklrXSNwyqwldqh9gk5m + HRvXA00nNkXD4dx0wjDJqxgs3AME58EIWo3MKrCNUS4cnD6qeQNf9ZZkxE8dWq1U + ZKhAVMuSWDMRYNZvSsiNjsMSRwIaPrpyDuUhAlG49HCmYLkBEzMckAhq1T1eiPwi + zae9d+CO7P34CSm3hYmjV7eiiwRhmPWpJwt53SrZvjjwzVpzZjcP+RDef4v+PFpQ + mvEfIl4H+T+IHacgkIJdvaxRn9uktf12naDHk0UvQI67JLKleU+QshcSScWb8FA6 + 7mcD8cdfu1y2+vcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKdizzBFh65tDUIwz + raukYSPIRm3erLD+Yky/bnenZrhofo4yLyoGu3UCwAvSjcXkEH2wPdRrJGm1nrQG + yKauWmjFjD3AA3qidrPBJXg8REWpjQjXvrjPztI6N4OVaXsDFBXczr/7E1Ot/fu3 + rGGjv2lD4fFdEUb7vvmywwWdVG2eK33xuoGpUWNtHg781QiAW5XQlMTR8Nghdc/B + yX3BlsR0ube//l3BKmWfSQbRM8vRQwO19VmZyaxjFwiQviW5ds3b7KReqxJn5UbU + brcfWGL+eg0/lOWTVQkoIwHBsmAZnIE98AeC8OGuGKMRxqkYqZ9Fsg8DVUMdqQFe + Dbu9pQ== -----END CERTIFICATE----- """ private let otherCACert = """ -----BEGIN CERTIFICATE----- - MIICrDCCAZQCCQC3Iplq4Q/2+jANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z - b21lLW90aGVyLWNhMB4XDTIzMDcwNTEwMjg0NFoXDTI0MDcwNDEwMjg0NFowGDEW + MIICrDCCAZQCCQD8FZzejuvygjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z + b21lLW90aGVyLWNhMB4XDTI0MDcwNDE3MDg1OFoXDTI1MDcwNDE3MDg1OFowGDEW MBQGA1UEAwwNc29tZS1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC - AQoCggEBAOko0E/i9WlJS5eBAfJsQ1Xz5cAse959qRz4LSE2PsRXYIqGD+CHSlxf - K549WPYfCTEJxT4+MCwU9MfyHSTmhYo/MEA6K1jMznZULhYFriLLiGBCB238W0Xo - bEf3EN9xrHlmHaYrN9EwI6Qiq/AYkpAmbrlgbLW5Ig03YWTODS8k4R1nrkB609BC - DBEyzBiCjgzo0xVduTgf6iiEfUg+dlvkeH+4qjLU0DRJq0g7YIM/kEX/zL2YUad5 - 9aytkDjO30IhcjQC+wvhCLBn6FDyYOpthaGM1cbMLG3efMpGAtyny2qATo63yVmf - kd8ftmV86BidAm+tCnFwBzxfXd4CB00CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA - Qxth0x5noVZrWZs67kBpjhiNI5Zg4/IMFukL4qv4XqC4AkwJJ4XaMAVTgtZ+mGmr - yOJ6pEzw0C7nWmTtlUjQu32Z+YNLSnE6wcIEx8ed1fwI0kezcyBBrg+Rs1vDNi1c - Tshq0by3RBuuSLclYrR64pmzYj4XJjABYIPurmtBCh4iwVhEe3tYs8I5vlKhmvA3 - ZTnqs21wD0v7FA4aM4EguFfLTMlBuD7U4G+agXvtcV4tXzQSh7RaXB06Mt4mNJ1k - LfqH39ZEnzeqUVm0vn283hvH9RzTYuHZu8J9wtmDrSTb6EcA4kpnILOgjhyLNL5G - EZi+HPA+wJ2bsRVlAxmuMA== + AQoCggEBAPtxP9YCOZggO5iivxe4CEHk3DdfYXKul+jOdW/dU4P4pwKU/YutQu+F + 7tPnGYaP7eO3if8PtbILio0lubk+uSbTZ5hRteL/3yj9UN6jL4vaVOkSunpbP+/d + HXdB8Aa0hzZlNPbG9+7TADGryxqt1KzgVo+KBAEY0p2vRc3E6HtLBnSkzlw3wi1X + zmuy8WCnayTmdqt95djsJE8PNX+GtTNfaNtZ1M5qx4FiPqqJqFhgCKJHy4LRrO3u + K5IfGVy8zfXkFSXfqvl9NKz71xecBIRMQJCATEpG6GXSyb+vmnOuAKZ+fVVLw7Kf + oPYwG3sh2GRDfa2pfAoP0vjXZri8AWkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA + 525Wy4Cyx2Pd91RJufYWRgkAwI8KLe4KV+SoZiLNJzfBRemqUq0P36l2lSotq4bp + I04d5HXkl8eQ7dje/RFWXMHfNQXRq06+KUfh1XA17GQ+VOEHFc0PYKW78ydXXZDk + PS2+y6Ru/MD989Aoecr2JvD7sSEmSvprtPxuNYHifZKbaw2c8HbR0Z3WawxoIFRV + zeb4aGUncWj7IKNRmL4f7CDA3Old0fwIRKYcxv5awTHK01PE4Yxo89M2RqRFiJQy + xbmAl1y7D1nLlfzHjKXRP6wpBylPcSssTuOXfi+U3Mv87iNDvzKTVYUD3c6wT/QQ + bIRjD4xVam65aa8pewlzRA== -----END CERTIFICATE----- """ private let serverCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMMCWxvY2Fs - aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuWLHyxPM/F - sviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv3x6sdRdT - 0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAkbWL6X4RG - 1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb0GM1yvBa - j88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6ZQ2x3H6cS - cTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU4P+ZzHEw - r/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGpBsuzx72mOBa9o7m1eNh2cY - H6MrNi1b6vTaA3SOH68RDxg2qx6UrKxI34/No7FaOzRrfs9vUaKXHwwBnDxMskH5 - iTmVAGegumDQE3Bd11j+v1tKxXWS/bvWH7tfK6taoex76ktR3L8qO+Hp8n4YKuSb - qJScIhMPIg7fWPonLvcszGFPdBIxU3YkAZJZFeom/s1WhWCYXsJZSYOXv4YRlaU5 - ozeV3v9icDptaxNY7n4U6C32eykMjowJJ9dcOD+ib3PF88S+utmZnSEGYu+5bnXy - 6MGWZcYH1wQ0RpNC+YzjQcGsKwHfaoBS4WFEK2fJdRfX4owZOu6HO1zhyoLpqw== + Fw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMMCWxvY2Fs + aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANTYUmWVpEP9ym/T + dYpDxr1z0bMZ7HiFMDT3EW0XGrEs2Rta6QbPxP3lSIW9G1OdVJMugg8EB4yoPrRc + x6ttS64X48DyfvEH4VbdX+gHZUeTIloN9GispPiIiY5Qodq9/JeSjAG744lnKWKm + 48w4A5bWb4zlZ8s7lNTiDphll686+oIuhhxAYI4nuKKsPhLatvclq/O6a1BypQv7 + 7psyo1gxcRs7gRLIGchcByI75ofkAcpT0/p67rhCECOFwvJt+8uuMoVnAPxcAgBO + jgPhbxS6DcOSzoliQYtOBv2YIKllplgOE+0eYxAvxBLfGY/wh3SeEhVKF8G8ltT6 + Anp2MPcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEACG9shmrPmQvt+3dKpEuzR2BA + GLrgON4zgc8byFIORHatPiM108LoEp2a8O+B3rFJqWbBBrowDBRR/5th7kLvyDsV + J33Rb2mXaJv8K8hNkbSs5Ow5go9M5LveMcgRqiQTuYLuvgman3LvlGveEc5N2yar + NCVmZfZ0ofTul+QqxaYBw2GlaXmzsyvpbZfowskYdGm/PipThWsuVy/e7BLHZsr7 + FF8f7qfbDsuVd0UpWPSlIHYUJLP4Bhj87YWnOQgXSEgq2cxybZHmahiG7YmZzg++ + Oz3B1+jGtb6nKk961X5LyGsitGjEULovA8tHQEJXWvKpYQTBXOMUCKWslrje3Q== -----END CERTIFICATE----- """ private let serverSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl - ci1jYTAeFw0yMzA3MDUxMDI4NDRaFw0yNDA3MDQxMDI4NDRaMBQxEjAQBgNVBAMM - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKTZdzuW - LHyxPM/FsviBBXpSzl2MxJxDkmir8DSdXO5E1sHCAymTaxy9bOdi1XUZbRTyKfTv - 3x6sdRdT0Gs2WjhL0yFT9IEVrGZADt3GIYoHYZU56Yn/nLglGQZqIeo33wyPEIAk - bWL6X4RG1Hc6nJQxhw1aaVtsYNAoWjAVzP773TZgyRcsGliqHtYpD0q0b+EfmPkb - 0GM1yvBaj88dWWFFlG00aZFQatSkIrPbkXG0Mu4/1UxYDEuxOYrIFkFMfR8V8h6Z - Q2x3H6cScTJ2TpIlw3rO6E0J/HYaVhmvJpevIPQhvH/Q+vM1bkvaIkckLchW7VgU - 4P+ZzHEwr/xMcqMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAY3vY+hng2gLh9t8q - /fvZewBiLAjsePbgRGT/xO4zCi3JwbHt07oGyQfo63ok5IJIrj3MPVy7N/oGJF0Y - niQrIhXs0NCKEZ/P9amh6wZJKAOtfD9t3oiNWTx56shm1vFQTTUdpykK0b37jGiK - y0N0p8M27ym/gQGTixfHNBtA0p+rmdDErOHqfU5Px3iQfmMmf4hxXPOSkGMixyre - 3AR6wURMGLUCLVxi0sQYNd4fGo/GwbswTSJI7+sypZHMwpXbaN7KjorkSmI8UuoY - aGEewReM008rQWGWf3ybmNCChhru82lPQGMp6y9fN0s591iIzjpCXixzd1j1V4oY - yXRecw== + ci1jYTAeFw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMM + CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANTYUmWV + pEP9ym/TdYpDxr1z0bMZ7HiFMDT3EW0XGrEs2Rta6QbPxP3lSIW9G1OdVJMugg8E + B4yoPrRcx6ttS64X48DyfvEH4VbdX+gHZUeTIloN9GispPiIiY5Qodq9/JeSjAG7 + 44lnKWKm48w4A5bWb4zlZ8s7lNTiDphll686+oIuhhxAYI4nuKKsPhLatvclq/O6 + a1BypQv77psyo1gxcRs7gRLIGchcByI75ofkAcpT0/p67rhCECOFwvJt+8uuMoVn + APxcAgBOjgPhbxS6DcOSzoliQYtOBv2YIKllplgOE+0eYxAvxBLfGY/wh3SeEhVK + F8G8ltT6Anp2MPcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA1cUeJkmPC+qjNzTY + JJ6LUofrVfIXZbiZf2NugNx/iLAabrLSd4pZjIXQszWbd5t+wftyUBqUd1Z/GuPj + XHmfES4ZUm1Fyu75KZjNzzXsXrlZ09IHIWvxAcA3FtqbDElw7naExbzcpup6s+45 + MgAMcFnMIoMj4Cdt9Ky0kzWKdl7tmNxIF+PNQby6JCVb7HPUgs3hqSalwDo9ddYF + 8lvJ5Q4ZzlKLL1zUKkpD7I4M6hvNLgHdnLa9nwtT40u0bbGr9z25W+YouJy5N4pD + YsOhm4xYF9VBZIEsr/ZSrI2RtABu7I5NVNWhF7JAIYspur1Rghl5bRkSaQ1jf/BK + Epog6w== -----END CERTIFICATE----- """ private let serverKey = """ -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEApNl3O5YsfLE8z8Wy+IEFelLOXYzEnEOSaKvwNJ1c7kTWwcID - KZNrHL1s52LVdRltFPIp9O/fHqx1F1PQazZaOEvTIVP0gRWsZkAO3cYhigdhlTnp - if+cuCUZBmoh6jffDI8QgCRtYvpfhEbUdzqclDGHDVppW2xg0ChaMBXM/vvdNmDJ - FywaWKoe1ikPSrRv4R+Y+RvQYzXK8FqPzx1ZYUWUbTRpkVBq1KQis9uRcbQy7j/V - TFgMS7E5isgWQUx9HxXyHplDbHcfpxJxMnZOkiXDes7oTQn8dhpWGa8ml68g9CG8 - f9D68zVuS9oiRyQtyFbtWBTg/5nMcTCv/ExyowIDAQABAoIBAAq5FpdqqlQmF0WQ - n5aoldmiH0hYisV7Y7+pR4O0pMHe+nU6EIiYzUPeUoIunKH0WHMfWXlUTRgqsacl - zY3byDyXOhGV63amGUPBcPYeGDppRoC1dqqCVQhpaVpQdwpMPhcMC0+6jt78WFA7 - Z0CmMF83ZYiJ1AadYyLHLS6pjF8dmkj/Rd6yeLIVkKr4xHxou7au/6WKorop5XLM - fEyWC2iotha2dkXw3i324n0qrbR2v/EYLnAn75uA9FF/pJWe6iPc6H5tfBSnzmO6 - fkZ2rCrDt4ANabg6WMmRdrZXFHSR/JlPPyh4T4iJGenkLltKZG+wWSm2nVXE0DYt - JQdmhiECgYEAz3EclGIrk63Hp/2mAHAOIOUGh6+Tk4JA+ibHSzziVZZqJsGQ9jcK - eOn6TX5674+aNzo2ROnHCT6u2tCQEl5/lrB8YpYh3F6aaNqPvFwqRhDViOw2l8KL - Ic40x19o5ur3Ss914htwTxiEzQVB/n+5zhE7W4N/RaDIT2hedWR19PECgYEAy3AF - CiHa6P+pbhskoSETtxbWkhDENpXat2dlFRDrNN9T2NZNVmIxCjAE52arduCxaLTP - hazyq4d7FZ4OkxfbJY9D2HnBS6mF0RHB0gZXZ7iB/uEr0KcTex5saqX9TF3YA5Wj - PNVtOM37IIaLJ1qOmfXf4yL3EVlI30eNwfoMkNMCgYAv0VAYOET5Rs7GP6b7ZNks - 5f5KWsO29giKYVQBWOiHeCPCCU6kIu3sD2teX7Bw9nZDEs0dt5Hk5Kkj0X3UbioV - D1us0hS+GqSXVQJbFhe8jPbcGC9BblvqEAGEj867pCAbA5WV6GNMKEe8huC+jKzE - /p3jK320DCsAevuDLgQu0QKBgHvE60v+zPB0muAiI2bkeNorSuAS001iXm62uQjY - AkFondqOhv7HPo60KEegbzEkAstxNdBeKEWzZ27/el6DZRC02NIbQT6HJKLN6t2c - fhDccDphRAbtnyyIle1Mj46miYWkxGt+bbThnKdtM7v9nESPEmdeHnKvn2Y4YkZh - msOBAoGAaarkv8JjjmIgjRZrJ7r4dkzZwZa/msm+/NHr3nlXK227ExMeFRPmzYls - zIofM+DoEk1sDXRfnv+8EU8Dn1DYSq6M6W8xrm7Ulpzj0kXE4f9TD+MUwSNCQ6Gg - zLRkHQBKblIa0lEvlulLtJT2UN9AnCmvTH2R11wD87DWjFDZKD8= + MIIEpAIBAAKCAQEA1NhSZZWkQ/3Kb9N1ikPGvXPRsxnseIUwNPcRbRcasSzZG1rp + Bs/E/eVIhb0bU51Uky6CDwQHjKg+tFzHq21LrhfjwPJ+8QfhVt1f6AdlR5MiWg30 + aKyk+IiJjlCh2r38l5KMAbvjiWcpYqbjzDgDltZvjOVnyzuU1OIOmGWXrzr6gi6G + HEBgjie4oqw+Etq29yWr87prUHKlC/vumzKjWDFxGzuBEsgZyFwHIjvmh+QBylPT + +nruuEIQI4XC8m37y64yhWcA/FwCAE6OA+FvFLoNw5LOiWJBi04G/ZggqWWmWA4T + 7R5jEC/EEt8Zj/CHdJ4SFUoXwbyW1PoCenYw9wIDAQABAoIBABbY/cdPz+lIhgGJ + BnYIHn5Zv2nlX3/0dB9LYkB+mWvpb4jDMn57sR68DRPmH9fS7LA77tQjz5emu8xq + pTherCANCnK81SmUefj0HIZwvMt5HNfj5ZeS6MaRCYsQVr9/Y2z12zeYbq1iOIwR + dCSI4sG/VQwf2At14t0TQxPS2/yAOwctWo2g4KVcHWUXTw+qKoNANX67h6tznLRA + QH8fG3T19+wBtuSVq/VBiJTHwAjdHXyHz+eRr7eSxANIewrgmrfbJRpXwevnzVsz + 9WZedkcTKBn7yj6ZaoRMwuwfAspJ0S6p320ehPbdo96Wh25BXNs3VJ2/GcUfkAKe + UGSymfkCgYEA+b68gdqr/IclrDPtFhGGJAYnKMZ6uQXkAij+x8w8X1Q/avkVLxFB + jBAEd7DLD/brCY6BMmq/mzmWJiSpdaF501rwne2DVcsq5U1N3rIV6AWxpp368Zfa + w2tut+bmq4ZEQtK4kJP2WXxE8HtQNSZ42qbGF0Rw0RoZ1pTr9oSkwCsCgYEA2iz/ + U6iedewf+Dqcggii6gGZEVpC/kXs6m7xxVgE6Nt2/tHskun2B/3Kqoq4KxOkR1In + saG5GJIfUfVhnwiydAp9t+jlu7mplpFCF1hZpC/pwa3EP/tB8roLXlcRkL7257TR + /4u9YHY85PpXUycsYZDibfxYLwenzePCZH7TIGUCgYEA14FnWQZA8qAMOhR0uV5V + yjAlCmJ6873JirOlZvMuBXTFZKGbTgot7ZbExCOilhwTpSN7CO5keKWwkyl/sSmt + 3lvS1fRmKFowob2bPFef359KNOSN7nuDIq5J1BdDZS9vJ9p9uQR0x7McKge+pp6U + GtlehiVg1I8ZTLklBIxhPhECgYEAuKs9suIWvlmO9d0mfCozOz7/AOEVs4QcdJJT + smY+QZsBrc6iH/hId5sp4BBqsot9kaDIWGI6+cE1IXpBlwsVgYMfxnsreSo9kWSC + PKBbv82OXpFme4GA4KL43HF2PL5m3tj+pv7w3KU4Bdif8ZJGzo6EGfRt7+Da+DrA + X6+5pMECgYAkMyh/ypYwsTqkYalgRk1lPwe/EAP28lWFsyckIzBuP+CwOMg5q5o3 + mg1GEE3hWCY9P1cLTedewHFBYpfiiR4hDKSyivZElV32C5Kg1uHxGc86FFGyAl3v + dEAYBVnuHnW4u1JpBh5a0rRa4U2UYeq9l9rZRJzsjV7FxkTIRgdNQw== -----END RSA PRIVATE KEY----- """ private let exampleServerCert = """ -----BEGIN CERTIFICATE----- MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNVBAMMC2V4YW1w - bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0yKJAjr3evjW - 69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+95XROgElY3DsuARH5wuksTi+mek5 - J5MObbUhHtGIaoqVaDew6TokawwQyBwngKudssu9/6rq38m33OSEv/5oc6xtTdKJ - Lrmqqf664+QajxCeec9CPMGJExQ25c1A5QOkkyC5xR+TcRRIcKPaDZ9aj6JlcD58 - QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+qNV2Pl5DZL8ujEX8XCf8EyHc5GNYN - 5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9JyM+wu/a0iMVBr4Yk73VVhOaH+aUI - eNHaD7fhlQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB1++0U/7RduytrWh2bu6uz - sFk4XK+5eIhKn4DMq6vKXFQYF94Tkz8K2RDGzZ3Cl8qRU7dLwlHrUgqFI89XMFAM - LjumIWoMnfik8A6cBmp/HqURzXPNv6Wgn4MtU7aDs8WAEsGYAo5TTtqVJUGc2Mlf - NkW3MQ/RTfUncamx2wNFjwLmGTuERgHA/OA8WQVnMDI5JLXH5sigdOMTkqgkGzhg - 8NVWnqubG4b4a7W3xl4s2FjqglqXP3vu+c1F6cWJfKgOXIqd8NduJ+p2FJZ1rW2c - 3jkHoqBLqA4/zua+HUn5ICcUZrZid7HgmlUoR/4n+dbjT3Jdpp4BpNn3q8JuWE4Q + Fw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBYxFDASBgNVBAMMC2V4YW1w + bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2BfL1dswYSgf + q+ZSp3Uy4bSlCayFuBeKc34W/XRl7+YJd8f/EJulI4gwYPjcBWYyFyHLzEyAjhJ8 + HMFW4a1PFNJ6gc3Djo60/qfhJ2/uAX8E/Gi7pVezKMsMwx6iBjG0SiN1zcQzGnns + k12TRZWfsIl/RR5F1tZWMLggHXasT9DBZht4Ya9jx7nte4T43vFfWlzJHu7L3L2i + 8tBTv3d53msjImMF6pUIqpDQ9doo+jGI4ApqbfeKUfy+/OxrKuhVMXzYh9dhXnDc + nJnhgxa51MumiJ9apqeUBT+rW+1zhYpIUE5Su0TgYXjNGecb+OwMoy8WYRJIusD6 + QVebKRhAqwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC7s83kfzkbh2GdbJYGCG/v + nWz/aZnLm542wyi0aXZNodNF/mTO4jSm8RKeAwLX+f1hleI32CG/d9/zSrvwOZiW + ar2fccfVqA1UuRXT8XyZB/WUxKPxuT1WtMVbS/nLS51XL2PJRn50JRAwnw7vH/MJ + Lb7IqY8MmIYZrnEBMfAqkPvSvN9eXZYrNuLQaeqV/I97XKz8FJ4IbR9Mz51STcHJ + 65lwzi/Tfdxq5awVfGx8bp+uj0aow6NPkpVqYS78LrCYVa4RYau9vHNVoHOzOumi + dXRxp2NlVy4kJwd7RX4p5qE4OSNBpRgQBCtXYXWBV8eHIQo8mfDGcAMPfMFQ35TY -----END CERTIFICATE----- """ private let exampleServerKey = """ -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEA0yKJAjr3evjW69/krEH8V3hLdVBuJX7YnhyEPVT0k+0Q4jJ+ - 95XROgElY3DsuARH5wuksTi+mek5J5MObbUhHtGIaoqVaDew6TokawwQyBwngKud - ssu9/6rq38m33OSEv/5oc6xtTdKJLrmqqf664+QajxCeec9CPMGJExQ25c1A5QOk - kyC5xR+TcRRIcKPaDZ9aj6JlcD58QxD672fQP3exR2iQQ1YZDfAdF/hcgh2ISI+q - NV2Pl5DZL8ujEX8XCf8EyHc5GNYN5nlT+Z9EjoDFTBpy8nNbp84rks0Ru36OYX9J - yM+wu/a0iMVBr4Yk73VVhOaH+aUIeNHaD7fhlQIDAQABAoIBAElHYToO8ToTB7US - HjHTLRvGupna8n+9CL3Hs/X9eG2nCAcZ84tGyjlRkIJ0/RPZGIOOPPjtcunEUnvz - xDw7c2VY3/nqY3Sqb5JjBaTJqUFq1CMKbU9S+3yy+5X0UwYtog1o5SPQopcyDT7U - XfFmYcMatkUVRYuNbbXcjhC7IVqcQpPzPrBaGJ/cm8ZCdTJIWCrJfsI2mgDA9d+B - k5c5uQxhohPWFdZsGGTdJgRCJww1mlAHJXxR06hkBJEnG/vmRaCvsjBgrOtP0/iE - y8NBETAYMl1Ms+w9Pv+pcE8eHgAvicNkKiOlvLTeval1ZIV236IiGlvls808SaYb - RCTlN7kCgYEA+/sFtAMj/ZPacAcuv1mTFKaYUFwbeZAZGHVyniwfgRRvPDBtqQ9i - vGWOLL2fLqbp00sy9LS0FNo8OSnJSGux2IEmoHdNWDNzyORkXLhB9Lov5VqKj6V3 - PO6JswgPtNrb+3fQulAq7YkU6qPdrtHGdU+to/lHQrD6FkMLdKz/zdMCgYEA1oC3 - nOv/PsUwNWlUzkxfgQ0Pnv32NKfoFu2cWL9C0FlCgQxakm03JrbcfjEQafPR64jE - 7uhe7aueiQz339jlMxyJk5BFNn/nIOBmLUFD3wV7xcsU1mRnbl/a4R1Wxk7vwmW1 - s4LRu35iiWb/UkblsTA/qdMcigRlRPkOg3TYqfcCgYBWgWgE06swa+jq2txmnrbK - uSLDO8vG4PxslC2ENbufEcfaTvnmtzx7VxYHMBYM6wqNGlzk+4BzRDS2nyzV6vsE - S9pZ7nskE43lYts9pZgnDyBQSdQV2oVj6rRlPRg/S3+IBisnO0xxfcUrhJQfZy8N - qQwAphybvawtplixdo7fNwKBgQCEvxfipzJJOGNDSrJPEXixNtIKBQUPRTIermHp - kkPZCMRddLXAlJJjBRujhN2xlFC/QN8PMwM8ds8f5cSo5WPCo9CIX+pVdgYllHnn - W9KS/KPCnpGAtJZF+lBMrIl9JHDAj41JUJZXQDne6rzrwDB53XAouxuYVmwNqUxQ - EknbtQKBgQCylI9b7Syz5pBzZZNBSn4eqzlsdYgG3WUMGyEyvVzTjfbovWK98xeE - A5F8BNesnoopCW9MtW2QJ9iSLn24sNpj0Uvw9pobB3uDj5Jn0oIndGe7N/7beYhE - HjkJ4liJkM5Q1oUOvVFFPJWEm99cP8urojakeMO3la3tGaElwHVWWQ== + MIIEpAIBAAKCAQEA2BfL1dswYSgfq+ZSp3Uy4bSlCayFuBeKc34W/XRl7+YJd8f/ + EJulI4gwYPjcBWYyFyHLzEyAjhJ8HMFW4a1PFNJ6gc3Djo60/qfhJ2/uAX8E/Gi7 + pVezKMsMwx6iBjG0SiN1zcQzGnnsk12TRZWfsIl/RR5F1tZWMLggHXasT9DBZht4 + Ya9jx7nte4T43vFfWlzJHu7L3L2i8tBTv3d53msjImMF6pUIqpDQ9doo+jGI4Apq + bfeKUfy+/OxrKuhVMXzYh9dhXnDcnJnhgxa51MumiJ9apqeUBT+rW+1zhYpIUE5S + u0TgYXjNGecb+OwMoy8WYRJIusD6QVebKRhAqwIDAQABAoIBAQCPtvPHnOkGFKtL + pfieimF2nq+MSYL9NhrMSLV9hyYscG8njIlkQD+J7A9QzvF1Xcw+eimSC+cLlduZ + PDRODvcjQABdx70hWGOjYX9qvRQrRpDIVddGVZc/sBsiwYK8X94p2H+Gg9AA8cmX + EIrbonD79dYA3+tOwGm+KRaiwcRDp7b9q3pVeTJGEn4+njXPzA1hADuF/i/CtFJj + d9PMamPRVDBj+ixQ7NpUwpTldIGFf4b1l54LIvqtZoRX470XuTc8Tw7BYXjEl4jv + 3mkhTwUIJ8W9VQCAvH4sz6Z/hM1rlgdk1a2o5aoQ7ohJvpb3jwGMvLNFVTSls78Y + 3rcpGKjxAoGBAPwVCQgE9LE5AQEaNWkbLgicyz0/qX6pVK7f9CDcrUxMcVo01Zjo + cXr79Q9xw4Wl9gvzKq/YAYtY/SxyTL0A0ip9OwBHAyeGLvxEiodOCQ4qxKO4xAcy + Y4GjveQwKANPttlJCPHgny3HqC8swYLf0dAGoYtBM53A9lPbvOKPFkDlAoGBANtz + keSFim5V+vlpovxITdPowJzzaBXnKRFBEjcdBJsNio4iPuVoVHjMYaVkyVZ1FQ+s + MTIYH0mTLlabQcIXHRF/gw6miEIILtBcJtcdE73gbyvZXgveweMsmZfP+TCKMgCO + OYjZT+SIiyB6zCctA3z/bM4I2taoOAhHtasU/7JPAoGAXFBXvlgSQ9RcScsPRC5v + 7Td+Ni/aIkhgeqoI/P/Tdt2HpUEz94soA6HBXKaMs6TTNg0W1M6FwkIUdPJmp9Bl + Jqo1sSRQQ2kgS8HN+T7akhWXbV18bCZHynHsWGRKQuwuSeQ1Il7f7CPxs1TwiLzu + WQAUqKp3/I1tp8gQo+dCfwECgYEApjXJKQDv0RO0C9WziUqmD7r4r6c3jWdQVm4n + grCqvVkrOO29H3m+iOObjW5hg+cXtZAgjqVwhQRBk3zx+DQTYx5lv+Hnz8Ns2YkC + Lekq+6QR728p6Omlhg9QoYf2X4o7xunxr7GP7jJw1X/MQlu4iaLX4NEaFnzAO508 + fkBgTccCgYAV/S8vuIjaku8H86uPu1F3rMoUI3En5vCKHsEbef72FPIJNLqeoTX7 + 1jKtxNBpoTnzv6CgGq7NRzFBdYwvcxbrMxVs/k76zgMp+EUY+SsPbQRSllum72pA + XrfC2tdOnAx7NO4dAYLpmqfTAzKGDFcf4MiRwRiYwzQ8OIw+bmH8Tg== -----END RSA PRIVATE KEY----- """ private let clientCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMMCWxvY2Fs - aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr8dF1Nzg/ - gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjPtPJxRSgj - qUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o9qVe7uQr - bSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK8hUfMq5K - PGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+B64HeKX9 - xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p4q/NHRvO - ADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsjJ+nFYDH31PmM9YpKGuytOw - DQYVLFYWIGybZC7FSESzlGx1GOZ5nY1AWj7gCnAvc7/Ct3efI/7qA0tSB+rbd2tA - /qqU0/0FJLZDyiXrpNzhFoYkg2VzZRSmFdsJbhzjNJM7iJRsWEhXn/7qydLyp1vj - i7DlYQVI2QgEQQz7BMJ6D3zPRxlyDzlVjr7l54M8RX9Dj8Oj9Sajd0RlkLiGW7YR - TC2nNebpRGN57Hi8dCM3xLWQcJ0N7BK0A67MnQaRbUQ0DMvxXO0+HUHxpfN39P/H - 6Y81QkFAeeCMCsSWTGHspIJ8teKk+KmIe3xZ72taWNge1Cu3xas7Zsl1lbI2mw== + Fw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMMCWxvY2Fs + aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMT3FOJyfZpu6Ypq + du3bMbdgWiVprdNMODJsRURJzKj7znKC1tLcD7gvv/536DKzpAl3VF/pbdd/yDU6 + JuXfel+2qxx3cvM+QLqBFEggUt9vd0rCs1DvZ9rT7A755hM2146gdE55g2JvXsJG + C6gBskr40qkt5JrKSqOys5VE1TeifBt4lwCQphAB6qNeZM+0MEz6PSeeSFjVFZlo + IdyF3+swCsioTvzbfXs9p14HthJzL3edoorbtJ8upZdUlwJNZ2AX7K34sL1SDM+b + Ox+eoGLJ1OhPW+CHvjFMzpM3Q1dyTqR1SosBEiuYVpk/SAS/pBSNMp3yKqF1T3ch + 4oJOb7UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAejNVcNvE3sNgMC7a1nm1gPKt + mnzVpuk/7Wls4ldH2yB6QupoWBimNAWyCsvc+l1qsbPJf18JbVK8FviRTs6Fka1g + +MFMqMs/SlcAgmSlkfHdpu3zZqUEgQxF8pNJJ/Dr2ypkxEMkZnS/g4KiPdQGnTAU + uHTtTm3DiNpyCCJFoDq3xMb+qTj8UTlZI45HukmqPKINobW6IHuislYQvhZnRXM9 + pwu4L379lI848bncYCVhlQJMP4bQTWhaUQgWpqIrxHatLAplWorTrCUzS1qT3uJN + B/zptXK9+LLU8Nc+pqR2kTBhMN5a1nN6MzPSi6UWwX+evr/NRigkOReHDqx9GQ== -----END CERTIFICATE----- """ private let clientSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl - ci1jYTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBQxEjAQBgNVBAMM - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOP8JtVr - 8dF1Nzg/gesU5U06YksuOu5xSphv3GEtsIY9i49rqTk6t4z1aNJgBG+2fPKqCMjP - tPJxRSgjqUhoxSS15Ap6yeDN9VJGGb2TH1jAYFHmVb6nB6gDdYdn9isYO+laIZ2o - 9qVe7uQrbSWHxr4ZqOlVGM5kiBTIIoyQJYnExEZ6nz6nqBVj6ZkZ3Nww9zRQ8AjK - 8hUfMq5KPGohTqAvIu3NUwU12AePtneb31Fpc1DKWM3ZeC8kNHheZuuIlBhrBmJ+ - B64HeKX9xfl15zI9ziDLSzMa9IL6ZV+3c6IBiTeGavf5cjHO1atkQIX+jCRQwb+p - 4q/NHRvOADhAXesCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAiEKKo8JIG29d16ZT - 6d4ERj/o/3B2rwpTvSxmVaon3Zzz0gQ+HhuEH9D3XzzZ//P7qe8PxpcZ75veuv7X - ZcIPK+L7QqLAR/RrbWSbhI8CpQ0WX2MitKkz+cdRCey8/4JF4g8PXMMuFrP6fGEm - 79l4aJoAiTNJ98qufUzD63kqU+kpPGjML6rnJFfwTVAWu/7Sy92u052IsoZfiKx0 - yN1vYr9jLD48n26YsyVjuuqqMW+OKxzRGA3xCa02W3cILQb0NVv4hM0+yGd1laKe - 1zGHzuaeCIL9bFGBtxRXTWyyEG9z5nohEz/waHpUHg5VcbrkLOIIAhsolLuDKQyl - JanCRA== + ci1jYTAeFw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMM + CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMT3FOJy + fZpu6Ypqdu3bMbdgWiVprdNMODJsRURJzKj7znKC1tLcD7gvv/536DKzpAl3VF/p + bdd/yDU6JuXfel+2qxx3cvM+QLqBFEggUt9vd0rCs1DvZ9rT7A755hM2146gdE55 + g2JvXsJGC6gBskr40qkt5JrKSqOys5VE1TeifBt4lwCQphAB6qNeZM+0MEz6PSee + SFjVFZloIdyF3+swCsioTvzbfXs9p14HthJzL3edoorbtJ8upZdUlwJNZ2AX7K34 + sL1SDM+bOx+eoGLJ1OhPW+CHvjFMzpM3Q1dyTqR1SosBEiuYVpk/SAS/pBSNMp3y + KqF1T3ch4oJOb7UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKLBSZDrKiqAUiJCi + da+cdf2oMEh9Tz9vlGik2O3KW2uW+eG0kF1D0PSNN0yGcmGY6eTiDu2MRlmBmRIz + c3TTRArhXsRA7BQ4uyHOL3uRvcRUMk5DEtGwLrVAr79RM8jgPdom6Ws/fPzqxr8h + umm4zc6ixLFhfGqI5QbIwVyBO4jLxTmwjuJSjlHOuSKLa74K88oKlFUR74ANyu+U + e5/+q4SzvmRZG0sd2x1ZqmPajJ8nG2orhMu/k2VC8JQhuRigTXnapCUCVmeKA7eu + dcwqBMU6QyvmwcTTR8GRlpfOR7rXEZUa2qak4kf76iNWOHzYkgNI3nOsCe70bFIy + Eeh5yg== -----END CERTIFICATE----- """ private let clientKey = """ -----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKCAQEA4/wm1Wvx0XU3OD+B6xTlTTpiSy467nFKmG/cYS2whj2Lj2up - OTq3jPVo0mAEb7Z88qoIyM+08nFFKCOpSGjFJLXkCnrJ4M31UkYZvZMfWMBgUeZV - vqcHqAN1h2f2Kxg76Vohnaj2pV7u5CttJYfGvhmo6VUYzmSIFMgijJAlicTERnqf - PqeoFWPpmRnc3DD3NFDwCMryFR8yrko8aiFOoC8i7c1TBTXYB4+2d5vfUWlzUMpY - zdl4LyQ0eF5m64iUGGsGYn4Hrgd4pf3F+XXnMj3OIMtLMxr0gvplX7dzogGJN4Zq - 9/lyMc7Vq2RAhf6MJFDBv6nir80dG84AOEBd6wIDAQABAoIBAGOV2hSxoSCAVg2Q - 2BwqtXrFfPggCofrHs11V0tvnMMWkSalvXaNKm49KHt0i5uMmAmbslidOgoI5k+B - PEmv0iWV+jWFqzcyX+1/R3Eimbe32JsNxPiRl2uRjz4FcGckn87vmu12R762uB0c - xwF0zKBvLvQ1Qq+tBDAnt8e0k2EYqgl7LEIb/1vDsxyVLNLzpSBfENYbE24YD0rI - /PQwot0UJhWVFnYxYczbQSLxhep1tRzhLaUY3k1SrvGvG/TM389TK5upJoV1/emc - EgRCnBPM2geOq59Bul0ri9bvXyVh19VegNM8MLwdf9/UtDFEhApYyoePve96LspM - aOQSe2ECgYEA+NkCXElSy+TdPLH4944PGdXNm6GtuhlRjMc3pz+r/5gQ9J68rIRt - OJewx6bQx8lOFYxzhiTbN2wY7ylm9/Cn6jk7zfUXrqq73tbWgS+YSWajoPZM4sDB - QdQuOkArdLGkHMM5+2U81d/D7o5nb4/ACCY1XtvrNUqgBe6tyHe/INECgYEA6omj - GVY/yYK1RsRNrTmYipZbnR0UPpTsVevdAoX9TrHG5AHTgmh/ONGGCx3/dgmLqZO+ - D4MZSGyK1appxJjpsKzkM5T7X+bznHWzXe6kGnzDzkgBzx4N4s3LHPZ4uUvNnZwi - h281KEBsblKOu4khDk8jL3LXjRxGYwOjqdisYfsCgYEA9ho4OWjSl486NYKVhM5b - pONLunUFSR0tB5smMSPJSLftXN94HO3CzstGK82QgWVW8fy7a5kbrA4eArjheqfo - iL4dpSyVRUrZDiNOdOjLJRx7Cv9LPp3/AsmDBlzcHUZp1YBF4ZhXt/Ta4xy2syBp - fCW9dpjsXwH0jKll+PJkdWECgYAjgDHv495D4kUOMSiQz+cHEztKzNwDnQco+kq5 - 1w5Amyg/2wbo9mhLcWuYwzGn7En3oSVjs7RgAg4ByYm4+GxnEcR5ClQCcDLvu+Eq - lrTATaJV1xBvCV2QtxXHjIc5hP/am4eeeHbTYO0IxfZU7KzUPaZVyExYT69XzXU4 - gFOXgQKBgFbrLumJh27/nHtvUM0xh7RZ61NplRXDLez8DinSlNI9ZKl197LKdBzB - 6cHi59SojJFJY6QAdqdtenj33KHKjgdc3rH1VvipytPBJRO2qohBpYuSZiY2y+Df - dW493Y3+mwD6VsGFFvBPSC3jhDBeIYxajEJChzkbClVDRS0muLQv + MIIEogIBAAKCAQEAxPcU4nJ9mm7pimp27dsxt2BaJWmt00w4MmxFREnMqPvOcoLW + 0twPuC+//nfoMrOkCXdUX+lt13/INTom5d96X7arHHdy8z5AuoEUSCBS3293SsKz + UO9n2tPsDvnmEzbXjqB0TnmDYm9ewkYLqAGySvjSqS3kmspKo7KzlUTVN6J8G3iX + AJCmEAHqo15kz7QwTPo9J55IWNUVmWgh3IXf6zAKyKhO/Nt9ez2nXge2EnMvd52i + itu0ny6ll1SXAk1nYBfsrfiwvVIMz5s7H56gYsnU6E9b4Ie+MUzOkzdDV3JOpHVK + iwESK5hWmT9IBL+kFI0ynfIqoXVPdyHigk5vtQIDAQABAoIBAGH7cD4+KlGa/z7G + O6eTtSW+HtohukE013ft+H9CHzepHEhG4ks/AerkhiQ2ziH6z42N+UFFRElB3fzs + ktEj3SKkInckzOBIhbbB468FtXRFZRihxsZqckWfyvygQF4qmAzxsSogtMVRFdib + M80+Gs3E/jb/B4whOgQ5L7D/7vmfUaOkD1tmm6mHfITihfdN9Jurg5OBZX2fWSHf + j9j6VbfLjoIatDGcC4MTiMkcIOYECAeXtpGYVdyH4K5y+UDCyo0b3qG+QuzE87gS + h2yVJNZKR6Y1vmcGqXQATVoZPGzS6Lc0/kUqcW50uoGBV5o5Sr4OKOUPIFCRwYa7 + 3LFW84ECgYEA42KEyM1vzXrDXrcGEp7GxZ0Z96yd5/XF3+AZ5R2snahRzDa+PzV2 + GPPZIVe8/bmJUxhWr3krHsUD21+KjNXY2l4ioAo2r4dmVR4vH9upGfWYutlw0pH8 + kurGq7H6lpwUfvb80AF3MXnwaqYGLUPX3cVOt9mEl0hEsqzlDrVSBcUCgYEA3cCO + RIWn8al+gse+xVqfpgGN9DuYM0qJuxvYpMy24EeJwGLyumwSesH6aK7zn2A9gME2 + eWFB93sL880QUeoGT2pHwBZAtC/4PvbNlrHO2W333/aMNVATBoGDIEUM8cjMkDr0 + jHx/iheLvXp81F5+W6o+sSSCI0RxkNXA/FcoUTECgYAw+fNn3PgL5jlWmU1xjUl7 + Hw+MzV1lrQZl5jstomqfurWDqvbnXniFf2BxUhie/euaPk/Nk+e5xO3Dvpx1IUqI + HmaO2iRVQnDEPLAhyIpv0PqIpHUspc0lR/Rq3vb+obe4cTKbCvXFbmJeVkxWS5qf + ZfRCnVN10lcZtSvRMzTrkQKBgGFrzxTbg0TwKdxa1Lzva2QLGspJxDwEay4AtdTw + +wbdZu9WiTzNbfDwd4q2EeHa7io6uCvrRofrTvz1Ak56efs5vfvtys9eo7lFxFyI + EVAEt/l033Qska8yBuGOdHlktjpHLFjr+Tw5y/KadWz3dpve11wLpgDIePwgbIBv + 6g6BAoGAA5lMXjQeQ26I5vXJTWd9NEvb7IVJeAheZOA03ruIPR4dVhorhsFt72bQ + +tugqU9aGlhxvCuvbBpEz8ZYhYxVe/tvDpe/upCCdGg4lwETje2FtKiYig7vMxKS + AwFfL4q1Bs2J1siAXCVSZ9fWFDGXWNbd1eLNzAfYqS766tBZyas= -----END RSA PRIVATE KEY----- """ private let serverExplicitCurveCert = """ -----BEGIN CERTIFICATE----- - MIICEDCCAbYCCQC7a34VXIF7+DAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt - cGxlLmNvbTAeFw0yMzA3MDUxMDI4NDVaFw0yNDA3MDQxMDI4NDVaMBYxFDASBgNV + MIICDzCCAbYCCQC4FdI3dXof+TAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt + cGxlLmNvbTAeFw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBYxFDASBgNV BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo - N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABN5Q - sDW36YI12PFC/kRnACzCt8a5lqjaFu6QNl0Y0ZYaiE9MdR+EOGcCfoSGf9r8n1Yl - peOOLlvsXQ0UO8WJbsYwCgYIKoZIzj0EAwIDSAAwRQIgGd0bh4HWEd3ytsCEGaw0 - m567URfCk1u6sY4I77U64zQCIQD5hOn0PDS4eYR+kBB5MadQtcBtz8gjtW/OJcfV - D1NSHw== + N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABBJm + 1grK+MXu4JYZqU6zD19w4MhWuhpPadq7PlpSxz4scMbWorth6pN/9hiqLAwKex7h + IuaSXF+83OESJPTOagQwCgYIKoZIzj0EAwIDRwAwRAIgNGW97RYxaltyx02UMyHc + E0AGSlMG3VFETZ5M9pMZGksCICgrTsLJXfOVdpyAjmI2vBibtZ/BisMftfXxupt3 + vM98 -----END CERTIFICATE----- """ private let serverExplicitCurveKey = """ -----BEGIN EC PRIVATE KEY----- - MIIBaAIBAQQgHqp+i/1N/Iq8DUruPu0ep9WiB9I+n1Ox6qFucixKbr6ggfowgfcC + MIIBaAIBAQQgD0lwrHX5wodoQFB4jhY7eqH4x5oBgL8aMjK9XndZODWggfowgfcC AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 - YyVRAgEBoUQDQgAE3lCwNbfpgjXY8UL+RGcALMK3xrmWqNoW7pA2XRjRlhqIT0x1 - H4Q4ZwJ+hIZ/2vyfViWl444uW+xdDRQ7xYluxg== + YyVRAgEBoUQDQgAEEmbWCsr4xe7glhmpTrMPX3DgyFa6Gk9p2rs+WlLHPixwxtai + u2Hqk3/2GKosDAp7HuEi5pJcX7zc4RIk9M5qBA== -----END EC PRIVATE KEY----- """ From 42b27f31914c799d27c2c0fe050bf65c7c987fc3 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Fri, 5 Jul 2024 09:15:11 +0100 Subject: [PATCH 385/580] Eliminate unnecessary allocations by using single-element writes (#1965) Using the collection-based write was wrapping single elements into a collection and write(contentsOf:) was being called. This always results in the element being wrapped in a Deque in the NIOAsyncWriter. This commit moves the write(element:) method to be required for the RPCWriter protocol so that we can call the single element write and avoid this boxing to save some allocations. --- .../Streaming/Internal/RPCWriter+Map.swift | 5 +++++ .../RPCWriter+MessageToRPCResponsePart.swift | 5 +++++ .../Internal/RPCWriter+Serialize.swift | 5 +++++ .../GRPCCore/Streaming/RPCWriter+Closable.swift | 13 ++++++++++++- Sources/GRPCCore/Streaming/RPCWriter.swift | 12 +++++++++++- .../GRPCCore/Streaming/RPCWriterProtocol.swift | 17 +++++++++-------- .../Client/Connection/Connection.swift | 4 ++++ .../Server/Connection/ServerConnection.swift | 4 ++++ Sources/GRPCInterceptors/HookedWriter.swift | 6 ++++++ .../Test Utilities/RPCWriter+Utilities.swift | 12 ++++++++++-- .../TracingTestsUtilities.swift | 8 ++++++-- 11 files changed, 77 insertions(+), 14 deletions(-) diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift index f99e4852b..c09d50240 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -31,6 +31,11 @@ struct MapRPCWriter: RPCWriterProtocol { self.transform = transform } + @inlinable + func write(_ element: Element) async throws { + try await self.base.write(self.transform(element)) + } + @inlinable func write(contentsOf elements: some Sequence) async throws { let transformed = elements.lazy.map { self.transform($0) } diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift index 23a7999b8..24ff04c64 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift @@ -31,6 +31,11 @@ struct MessageToRPCResponsePartWriter: RPCWriterP self.base = RPCWriter(wrapping: base) } + @inlinable + func write(_ element: Element) async throws { + try await self.base.write(.message(self.serializer.serialize(element))) + } + @inlinable func write(contentsOf elements: some Sequence) async throws { let requestParts = try elements.map { message -> RPCResponsePart in diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift index 5bc2a0e41..62ea48739 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -31,6 +31,11 @@ struct SerializingRPCWriter: RPCWriterProtocol { self.base = RPCWriter(wrapping: base) } + @inlinable + func write(_ element: Element) async throws { + try await self.base.write(self.serializer.serialize(element)) + } + @inlinable func write(contentsOf elements: some Sequence) async throws { let requestParts = try elements.map { message in diff --git a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift index 01418e631..30099b040 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -28,9 +28,20 @@ extension RPCWriter { self.writer = other } + /// Writes a single element. + /// + /// This function suspends until the element has been accepted. Implementers can use this + /// to exert backpressure on callers. + /// + /// - Parameter element: The element to write. + @inlinable + public func write(_ element: Element) async throws { + try await self.writer.write(element) + } + /// Writes a sequence of elements. /// - /// This function suspends until the elements have been accepted. Implements can use this + /// This function suspends until the elements have been accepted. Implementers can use this /// to exert backpressure on callers. /// /// - Parameter elements: The elements to write. diff --git a/Sources/GRPCCore/Streaming/RPCWriter.swift b/Sources/GRPCCore/Streaming/RPCWriter.swift index 8eed40265..d6c3825d7 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter.swift @@ -26,9 +26,19 @@ public struct RPCWriter: Sendable, RPCWriterProtocol { self.writer = other } + /// Writes a single element. + /// + /// This function suspends until the element has been accepted. Implementers can use this + /// to exert backpressure on callers. + /// + /// - Parameter element: The element to write. + public func write(_ element: Element) async throws { + try await self.writer.write(element) + } + /// Writes a sequence of elements. /// - /// This function suspends until the elements have been accepted. Implements can use this + /// This function suspends until the elements have been accepted. Implementers can use this /// to exert backpressure on callers. /// /// - Parameter elements: The elements to write. diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index 6de7722c5..7d4a4e379 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -20,9 +20,17 @@ public protocol RPCWriterProtocol: Sendable { /// The type of value written. associatedtype Element + /// Writes a single element. + /// + /// This function suspends until the element has been accepted. Implementers can use this + /// to exert backpressure on callers. + /// + /// - Parameter element: The element to write. + func write(_ element: Element) async throws + /// Writes a sequence of elements. /// - /// This function suspends until the elements have been accepted. Implements can use this + /// This function suspends until the elements have been accepted. Implementers can use this /// to exert backpressure on callers. /// /// - Parameter elements: The elements to write. @@ -31,13 +39,6 @@ public protocol RPCWriterProtocol: Sendable { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriterProtocol { - /// Writes a single element into the sink. - /// - /// - Parameter element: The element to write. - public func write(_ element: Element) async throws { - try await self.write(contentsOf: CollectionOfOne(element)) - } - /// Writes an `AsyncSequence` of values into the sink. /// /// - Parameter elements: The elements to write. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index c5c0e6838..9eed38a2b 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -366,6 +366,10 @@ extension Connection { self.http2Stream = http2Stream } + func write(_ element: RPCRequestPart) async throws { + try await self.requestWriter.write(element) + } + func write(contentsOf elements: some Sequence) async throws { try await self.requestWriter.write(contentsOf: elements) } diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift index da5cf5661..2279bd922 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift @@ -35,6 +35,10 @@ public enum ServerConnection { self.http2Stream = http2Stream } + public func write(_ element: RPCResponsePart) async throws { + try await self.responseWriter.write(element) + } + public func write(contentsOf elements: some Sequence) async throws { try await self.responseWriter.write(contentsOf: elements) } diff --git a/Sources/GRPCInterceptors/HookedWriter.swift b/Sources/GRPCInterceptors/HookedWriter.swift index b4bb52eed..ed4977dca 100644 --- a/Sources/GRPCInterceptors/HookedWriter.swift +++ b/Sources/GRPCInterceptors/HookedWriter.swift @@ -32,6 +32,12 @@ struct HookedWriter: RPCWriterProtocol { self.afterEachWrite = afterEachWrite } + func write(_ element: Element) async throws { + self.beforeEachWrite() + try await self.writer.write(element) + self.afterEachWrite() + } + func write(contentsOf elements: some Sequence) async throws { self.beforeEachWrite() try await self.writer.write(contentsOf: elements) diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift index e58bd7084..5160b29b5 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -31,6 +31,10 @@ extension RPCWriter { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private struct FailOnWrite: RPCWriterProtocol { + func write(_ element: Element) async throws { + XCTFail("Unexpected write") + } + func write(contentsOf elements: some Sequence) async throws { XCTFail("Unexpected write") } @@ -44,9 +48,13 @@ private struct AsyncStreamGatheringWriter: RPCWriterProtocol { self.continuation = continuation } - func write(contentsOf elements: some Sequence) async throws { + func write(_ element: Element) { + self.continuation.yield(element) + } + + func write(contentsOf elements: some Sequence) { for element in elements { - self.continuation.yield(element) + self.write(element) } } } diff --git a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift index 90d6eac0c..1b3a567e4 100644 --- a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift +++ b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift @@ -183,9 +183,13 @@ struct TestWriter: RPCWriterProtocol { self.streamContinuation = streamContinuation } - func write(contentsOf elements: some Sequence) async throws { + func write(_ element: WriterElement) { + self.streamContinuation.yield(element) + } + + func write(contentsOf elements: some Sequence) { elements.forEach { element in - self.streamContinuation.yield(element) + self.write(element) } } } From e28f11752a711d7c3a439aa8bae4d9259fe10699 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 5 Jul 2024 10:08:26 +0100 Subject: [PATCH 386/580] Enable ExistentialAny for v2 (#1967) Motivation: The use of existentials can be made more obvious by enabling the upcoming 'ExistentialAny' feature. Modifications: Enable 'ExistentialAny' and fix warnings. Result: Use of existentials is more obvious --- Package@swift-6.swift | 30 +++--- .../ClientRPCExecutor+HedgingExecutor.swift | 14 +-- .../ClientRPCExecutor+OneShotExecutor.swift | 6 +- .../ClientRPCExecutor+RetryExecutor.swift | 10 +- .../Internal/ClientStreamExecutor.swift | 2 +- .../Server/Internal/ServerRPCExecutor.swift | 2 +- .../GRPCCore/Configuration/MethodConfig.swift | 6 +- .../Configuration/ServiceConfig.swift | 12 +-- Sources/GRPCCore/RPCError.swift | 4 +- Sources/GRPCCore/RuntimeError.swift | 4 +- .../Internal/BroadcastAsyncSequence.swift | 44 ++++---- .../Streaming/Internal/BufferedStream.swift | 100 +++++++++--------- .../Streaming/RPCWriter+Closable.swift | 2 +- .../Streaming/RPCWriterProtocol.swift | 2 +- .../Connection/ClientConnectionHandler.swift | 8 +- .../Client/Connection/Connection.swift | 2 +- .../Client/Connection/GRPCChannel.swift | 12 +-- .../Client/Connection/RequestQueue.swift | 2 +- .../Internal/NIOChannelPipeline+GRPC.swift | 2 +- Sources/GRPCHTTP2Core/Internal/Timer.swift | 2 +- .../ServerConnectionManagementHandler.swift | 4 +- .../HTTP2ServerTransport+Posix.swift | 2 +- .../NIOClientBootstrap+SocketAddress.swift | 2 +- ...TP2ServerTransport+TransportServices.swift | 2 +- .../InteroperabilityTestCase.swift | 2 +- .../performance-worker/WorkerService.swift | 2 +- .../ProtobufCodingTests.swift | 2 +- 27 files changed, 141 insertions(+), 141 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 65d053bfa..59430cfcb 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -193,7 +193,7 @@ extension Target { .atomics ], path: "Sources/GRPCCore", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v5), .enableUpcomingFeature("ExistentialAny")] ) } @@ -203,7 +203,7 @@ extension Target { dependencies: [ .grpcCore ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -214,7 +214,7 @@ extension Target { .grpcCore, .tracing ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -229,7 +229,7 @@ extension Target { .dequeModule, .atomics ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -242,7 +242,7 @@ extension Target { .nioPosix, .nioExtras ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -256,7 +256,7 @@ extension Target { .nioExtras, .nioTransportServices ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -298,7 +298,7 @@ extension Target { .nioFileSystem, .argumentParser ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -426,7 +426,7 @@ extension Target { .grpcCore, .protobuf ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -439,7 +439,7 @@ extension Target { .protobuf, .protobufPluginLibrary ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -451,7 +451,7 @@ extension Target { .interoperabilityTests, .grpcCore ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v5), .enableUpcomingFeature("ExistentialAny")] ) } @@ -483,7 +483,7 @@ extension Target { .grpcCore, .grpcProtobuf ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v5), .enableUpcomingFeature("ExistentialAny")] ) } @@ -497,7 +497,7 @@ extension Target { .interoperabilityTests, .argumentParser ], - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -787,7 +787,7 @@ extension Target { .target( name: "GRPCCodeGen", path: "Sources/GRPCCodeGen", - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -799,7 +799,7 @@ extension Target { .protobuf, ], path: "Sources/GRPCProtobuf", - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -812,7 +812,7 @@ extension Target { .grpcCodeGen ], path: "Sources/GRPCProtobufCodeGen", - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 7b9f03e8f..7a59b001a 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -158,10 +158,10 @@ extension ClientRPCExecutor.HedgingExecutor { method: MethodDescriptor, options: CallOptions, responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R - ) async -> Result { + ) async -> Result { await withTaskGroup( of: _HedgingAttemptTaskResult.self, - returning: Result.self + returning: Result.self ) { group in // The strategy here is to have two types of task running in the group: // - To execute an RPC attempt. @@ -508,9 +508,9 @@ extension ClientRPCExecutor.HedgingExecutor { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline enum _HedgingTaskResult { - case rpcHandled(Result) - case finishedRequest(Result) - case timedOut(Result) + case rpcHandled(Result) + case finishedRequest(Result) + case timedOut(Result) } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -522,8 +522,8 @@ enum _HedgingAttemptTaskResult { @usableFromInline enum AttemptResult { case unusableResponse(ClientResponse.Stream, Metadata.RetryPushback?) - case usableResponse(Result) - case noStreamAvailable(Error) + case usableResponse(Result) + case noStreamAvailable(any Error) } @usableFromInline diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index 417ccd59d..b5cd76c9b 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -68,7 +68,7 @@ extension ClientRPCExecutor.OneShotExecutor { ) async throws -> R { let result = await withTaskGroup( of: _OneShotExecutorTask.self, - returning: Result.self + returning: Result.self ) { group in do { return try await self.transport.withStream(descriptor: method, options: options) { stream in @@ -146,6 +146,6 @@ extension ClientRPCExecutor.OneShotExecutor { @usableFromInline enum _OneShotExecutorTask { case streamExecutorCompleted - case timedOut(Result) - case responseHandled(Result) + case timedOut(Result) + case responseHandled(Result) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 0bdd30852..abe653b38 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -91,7 +91,7 @@ extension ClientRPCExecutor.RetryExecutor { // retries may be skipped if the throttle is applied. let result = await withTaskGroup( of: _RetryExecutorTask.self, - returning: Result.self + returning: Result.self ) { group in // Add a task to limit the overall execution time of the RPC. if let deadline = self.deadline { @@ -312,16 +312,16 @@ extension ClientRPCExecutor.RetryExecutor { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline enum _RetryExecutorTask { - case timedOut(Result) - case handledResponse(Result) + case timedOut(Result) + case handledResponse(Result) case retry(Duration?) - case outboundFinished(Result) + case outboundFinished(Result) } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline enum _RetryExecutorSubTask { case streamProcessed - case handledResponse(Result) + case handledResponse(Result) case retry(Duration?) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 285f8abeb..1a57a80f6 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -149,7 +149,7 @@ internal struct ClientStreamExecutor { on stream: Transport.Inbound ) async -> OnFirstResponsePart { var iterator = stream.makeAsyncIterator() - let result = await Result { + let result = await Result { switch try await iterator.next() { case .metadata(let metadata): return .metadata(metadata, UnsafeTransfer(iterator)) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 9bf0e75a5..183e7ca85 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -241,7 +241,7 @@ struct ServerRPCExecutor { @usableFromInline enum ServerExecutorTask { - case timedOut(Result) + case timedOut(Result) case executed } } diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index f8d8e7eb6..b02d25f0e 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -478,7 +478,7 @@ extension MethodConfig.Name: Codable { case method } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let service = try container.decodeIfPresent(String.self, forKey: .service) @@ -622,7 +622,7 @@ struct GoogleRPCCode: Codable { self.code = code } - init(from decoder: Decoder) throws { + init(from decoder: any Decoder) throws { let container = try decoder.singleValueContainer() let code: Status.Code? @@ -641,7 +641,7 @@ struct GoogleRPCCode: Codable { } } - func encode(to encoder: Encoder) throws { + func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.code.googleRPCCode) } diff --git a/Sources/GRPCCore/Configuration/ServiceConfig.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift index d5d160bb3..2f711b0ba 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -70,7 +70,7 @@ extension ServiceConfig: Codable { case retryThrottling } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let methodConfig = try container.decodeIfPresent( @@ -91,7 +91,7 @@ extension ServiceConfig: Codable { ) } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.methodConfig, forKey: .methodConfig) try container.encode(self.loadBalancingConfig, forKey: .loadBalancingConfig) @@ -179,7 +179,7 @@ extension ServiceConfig.LoadBalancingConfig { self.shuffleAddressList = shuffleAddressList } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let shuffle = try container.decodeIfPresent(Bool.self, forKey: .shuffleAddressList) ?? false self.shuffleAddressList = shuffle @@ -200,7 +200,7 @@ extension ServiceConfig.LoadBalancingConfig: Codable { case pickFirst = "pick_first" } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if let value = try container.decodeIfPresent(RoundRobin.self, forKey: .roundRobin) { self.value = .roundRobin(value) @@ -211,7 +211,7 @@ extension ServiceConfig.LoadBalancingConfig: Codable { } } - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self.value { case .pickFirst(let value): @@ -254,7 +254,7 @@ extension ServiceConfig { try self.validateTokenRatio() } - public init(from decoder: Decoder) throws { + public init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.maxTokens = try container.decode(Int.self, forKey: .maxTokens) self.tokenRatio = try container.decode(Double.self, forKey: .tokenRatio) diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 72223bf3b..b9147007d 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -33,7 +33,7 @@ public struct RPCError: Sendable, Hashable, Error { public var metadata: Metadata /// The original error which led to this error being thrown. - public var cause: Error? + public var cause: (any Error)? /// Create a new RPC error. /// @@ -42,7 +42,7 @@ public struct RPCError: Sendable, Hashable, Error { /// - message: A message providing additional context about the code. /// - metadata: Any metadata to attach to the error. /// - cause: An underlying error which led to this error being thrown. - public init(code: Code, message: String, metadata: Metadata = [:], cause: Error? = nil) { + public init(code: Code, message: String, metadata: Metadata = [:], cause: (any Error)? = nil) { self.code = code self.message = message self.metadata = metadata diff --git a/Sources/GRPCCore/RuntimeError.swift b/Sources/GRPCCore/RuntimeError.swift index e43bc800e..357aa59f7 100644 --- a/Sources/GRPCCore/RuntimeError.swift +++ b/Sources/GRPCCore/RuntimeError.swift @@ -27,7 +27,7 @@ public struct RuntimeError: Error, Hashable, Sendable { public var message: String /// The original error which led to this error being thrown. - public var cause: Error? + public var cause: (any Error)? /// Creates a new error. /// @@ -35,7 +35,7 @@ public struct RuntimeError: Error, Hashable, Sendable { /// - code: The error code. /// - message: A description of the error. /// - cause: The original error which led to this error being thrown. - public init(code: Code, message: String, cause: Error? = nil) { + public init(code: Code, message: String, cause: (any Error)? = nil) { self.code = code self.message = message self.cause = cause diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift index a56380066..6367fa859 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -129,7 +129,7 @@ extension BroadcastAsyncSequence { } @inlinable - func finish(with result: Result) { + func finish(with result: Result) { self._storage.finish(result) } @@ -139,7 +139,7 @@ extension BroadcastAsyncSequence { } @inlinable - func finish(throwing error: Error) { + func finish(throwing error: any Error) { self.finish(with: .failure(error)) } } @@ -232,7 +232,7 @@ final class _BroadcastSequenceStorage: Sendable { /// /// - Parameter result: Whether the stream is finishing cleanly or because of an error. @inlinable - func finish(_ result: Result) { + func finish(_ result: Result) { let action = self._state.withLockedValue { state in state.finish(result: result) } switch action { case .none: @@ -334,19 +334,19 @@ final class _BroadcastSequenceStorage: Sendable { @usableFromInline struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline - typealias ConsumerContinuation = CheckedContinuation + typealias ConsumerContinuation = CheckedContinuation @usableFromInline - typealias ProducerContinuation = CheckedContinuation + typealias ProducerContinuation = CheckedContinuation @usableFromInline struct ConsumerContinuations { @usableFromInline var continuations: _OneOrMany @usableFromInline - var result: Result + var result: Result @inlinable - init(continuations: _OneOrMany, result: Result) { + init(continuations: _OneOrMany, result: Result) { self.continuations = continuations self.result = result } @@ -369,10 +369,10 @@ struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline var continuations: [ProducerContinuation] @usableFromInline - var result: Result + var result: Result @inlinable - init(continuations: [ProducerContinuation], result: Result) { + init(continuations: [ProducerContinuation], result: Result) { self.continuations = continuations self.result = result } @@ -435,7 +435,7 @@ struct _BroadcastSequenceStateMachine: Sendable { } @inlinable - mutating func finish(result: Result) -> OnFinish { + mutating func finish(result: Result) -> OnFinish { let continuations = self.subscriptions.removeSubscribersWithContinuations() return .resume( .init(continuations: continuations, result: result.map { nil }), @@ -755,7 +755,7 @@ struct _BroadcastSequenceStateMachine: Sendable { } @inlinable - mutating func finish(result: Result) -> OnFinish { + mutating func finish(result: Result) -> OnFinish { let continuations = self.subscriptions.removeSubscribersWithContinuations() let producers = self.producers.map { $0.0 } self.producers.removeAll() @@ -824,10 +824,10 @@ struct _BroadcastSequenceStateMachine: Sendable { /// The terminating result of the sequence. @usableFromInline - let result: Result + let result: Result @inlinable - init(from state: Initial, result: Result) { + init(from state: Initial, result: Result) { self.elements = Elements() self.subscriptions = Subscriptions() self.subscriptionsToDrop = [] @@ -835,7 +835,7 @@ struct _BroadcastSequenceStateMachine: Sendable { } @inlinable - init(from state: Subscribed, result: Result) { + init(from state: Subscribed, result: Result) { self.elements = Elements() self.subscriptions = state.subscriptions self.subscriptionsToDrop = [] @@ -843,7 +843,7 @@ struct _BroadcastSequenceStateMachine: Sendable { } @inlinable - init(from state: Streaming, result: Result) { + init(from state: Streaming, result: Result) { self.elements = state.elements self.subscriptions = state.subscriptions self.subscriptionsToDrop = state.subscriptionsToDrop @@ -1063,7 +1063,7 @@ struct _BroadcastSequenceStateMachine: Sendable { } @inlinable - mutating func finish(result: Result) -> OnFinish { + mutating func finish(result: Result) -> OnFinish { let onFinish: OnFinish switch self._state { @@ -1098,15 +1098,15 @@ struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline struct ReturnAndResumeProducers { @usableFromInline - var nextResult: Result + var nextResult: Result @usableFromInline var producers: ProducerContinuations @inlinable init( - nextResult: Result, + nextResult: Result, producers: [ProducerContinuation] = [], - producerResult: Result = .success(()) + producerResult: Result = .success(()) ) { self.nextResult = nextResult self.producers = ProducerContinuations(continuations: producers, result: producerResult) @@ -1153,7 +1153,7 @@ struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline enum OnSetContinuation { case none - case resume(ConsumerContinuation, Result) + case resume(ConsumerContinuation, Result) } @inlinable @@ -1192,7 +1192,7 @@ struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline enum OnCancelSubscription { case none - case resume(ConsumerContinuation, Result) + case resume(ConsumerContinuation, Result) } @inlinable @@ -1270,7 +1270,7 @@ struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline enum OnWaitToProduceMore { case none - case resume(ProducerContinuation, Result) + case resume(ProducerContinuation, Result) } @inlinable diff --git a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift index 79da6833f..86e7547fb 100644 --- a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift +++ b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift @@ -98,7 +98,7 @@ import DequeModule /// ``` /// extension QuakeMonitor { /// -/// static var throwingQuakes: BufferedStream { +/// static var throwingQuakes: BufferedStream { /// BufferedStream { continuation in /// let monitor = QuakeMonitor() /// monitor.quakeHandler = { quake in @@ -392,7 +392,7 @@ extension BufferedStream { @inlinable internal func enqueueCallback( callbackToken: WriteResult.CallbackToken, - onProduceMore: @escaping @Sendable (Result) -> Void + onProduceMore: @escaping @Sendable (Result) -> Void ) { self._backing.storage.enqueueProducer( callbackToken: callbackToken, @@ -426,14 +426,14 @@ extension BufferedStream { @inlinable internal func write( contentsOf sequence: S, - onProduceMore: @escaping @Sendable (Result) -> Void + onProduceMore: @escaping @Sendable (Result) -> Void ) where Element == S.Element, S: Sequence { do { let writeResult = try self.write(contentsOf: sequence) switch writeResult { case .produceMore: - onProduceMore(Result.success(())) + onProduceMore(Result.success(())) case .enqueueCallback(let callbackToken): self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) @@ -456,7 +456,7 @@ extension BufferedStream { @inlinable internal func write( _ element: Element, - onProduceMore: @escaping @Sendable (Result) -> Void + onProduceMore: @escaping @Sendable (Result) -> Void ) { self.write(contentsOf: CollectionOfOne(element), onProduceMore: onProduceMore) } @@ -542,7 +542,7 @@ extension BufferedStream { /// - Parameters: /// - error: The error to throw, or `nil`, to finish normally. @inlinable - internal func finish(throwing error: Error?) { + internal func finish(throwing error: (any Error)?) { self._backing.storage.finish(error) } } @@ -558,9 +558,9 @@ extension BufferedStream { @inlinable internal static func makeStream( of elementType: Element.Type = Element.self, - throwing failureType: Error.Type = Error.self, + throwing failureType: (any Error).Type = (any Error).self, backPressureStrategy: Source.BackPressureStrategy - ) -> (`Self`, Source) where Error == Error { + ) -> (`Self`, Source) where any Error == any Error { let storage = _BackPressuredStorage( backPressureStrategy: backPressureStrategy._internalBackPressureStrategy ) @@ -774,7 +774,7 @@ extension BufferedStream { @inlinable func enqueueProducer( callbackToken: Source.WriteResult.CallbackToken, - onProduceMore: @escaping @Sendable (Result) -> Void + onProduceMore: @escaping @Sendable (Result) -> Void ) { let action = self._stateMachine.withCriticalRegion { $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) @@ -782,10 +782,10 @@ extension BufferedStream { switch action { case .resumeProducer(let onProduceMore): - onProduceMore(Result.success(())) + onProduceMore(Result.success(())) case .resumeProducerWithError(let onProduceMore, let error): - onProduceMore(Result.failure(error)) + onProduceMore(Result.failure(error)) case .none: break @@ -800,7 +800,7 @@ extension BufferedStream { switch action { case .resumeProducerWithCancellationError(let onProduceMore): - onProduceMore(Result.failure(CancellationError())) + onProduceMore(Result.failure(CancellationError())) case .none: break @@ -808,7 +808,7 @@ extension BufferedStream { } @inlinable - func finish(_ failure: Error?) { + func finish(_ failure: (any Error)?) { let action = self._stateMachine.withCriticalRegion { $0.finish(failure) } @@ -853,7 +853,7 @@ extension BufferedStream { case .returnElementAndResumeProducers(let element, let producerContinuations): for producerContinuation in producerContinuations { - producerContinuation(Result.success(())) + producerContinuation(Result.success(())) } return element @@ -895,7 +895,7 @@ extension BufferedStream { ): continuation.resume(returning: element) for producerContinuation in producerContinuations { - producerContinuation(Result.success(())) + producerContinuation(Result.success(())) } case .resumeConsumerWithErrorAndCallOnTermination( @@ -996,10 +996,10 @@ extension BufferedStream { var buffer: Deque /// The optional consumer continuation. @usableFromInline - var consumerContinuation: CheckedContinuation? + var consumerContinuation: CheckedContinuation? /// The producer continuations. @usableFromInline - var producerContinuations: Deque<(UInt, (Result) -> Void)> + var producerContinuations: Deque<(UInt, (Result) -> Void)> /// The producers that have been cancelled. @usableFromInline var cancelledAsyncProducers: Deque @@ -1013,8 +1013,8 @@ extension BufferedStream { iteratorInitialized: Bool, onTermination: (@Sendable () -> Void)? = nil, buffer: Deque, - consumerContinuation: CheckedContinuation? = nil, - producerContinuations: Deque<(UInt, (Result) -> Void)>, + consumerContinuation: CheckedContinuation? = nil, + producerContinuations: Deque<(UInt, (Result) -> Void)>, cancelledAsyncProducers: Deque, hasOutstandingDemand: Bool ) { @@ -1039,7 +1039,7 @@ extension BufferedStream { var buffer: Deque /// The failure that should be thrown after the last element has been consumed. @usableFromInline - var failure: Error? + var failure: (any Error)? /// The onTermination callback. @usableFromInline var onTermination: (@Sendable () -> Void)? @@ -1048,7 +1048,7 @@ extension BufferedStream { init( iteratorInitialized: Bool, buffer: Deque, - failure: Error? = nil, + failure: (any Error)? = nil, onTermination: (@Sendable () -> Void)? ) { self.iteratorInitialized = iteratorInitialized @@ -1157,7 +1157,7 @@ extension BufferedStream { case callOnTermination((@Sendable () -> Void)?) /// Indicates that all producers should be failed and `onTermination` should be called. case failProducersAndCallOnTermination( - [(Result) -> Void], + [(Result) -> Void], (@Sendable () -> Void)? ) } @@ -1269,7 +1269,7 @@ extension BufferedStream { case callOnTermination((@Sendable () -> Void)?) /// Indicates that all producers should be failed and `onTermination` should be called. case failProducersAndCallOnTermination( - [(Result) -> Void], + [(Result) -> Void], (@Sendable () -> Void)? ) } @@ -1331,12 +1331,12 @@ extension BufferedStream { case callOnTermination((() -> Void)?) /// Indicates that all producers should be failed and `onTermination` should be called. case failProducersAndCallOnTermination( - CheckedContinuation?, - [(Result) -> Void], + CheckedContinuation?, + [(Result) -> Void], (@Sendable () -> Void)? ) /// Indicates that all producers should be failed. - case failProducers([(Result) -> Void]) + case failProducers([(Result) -> Void]) } @inlinable @@ -1395,12 +1395,12 @@ extension BufferedStream { ) /// Indicates that the consumer should be resumed and the producer should be notified to produce more. case resumeConsumerAndReturnProduceMore( - continuation: CheckedContinuation, + continuation: CheckedContinuation, element: Element ) /// Indicates that the consumer should be resumed and the producer should be suspended. case resumeConsumerAndReturnEnqueue( - continuation: CheckedContinuation, + continuation: CheckedContinuation, element: Element, callbackToken: Source.WriteResult.CallbackToken ) @@ -1410,7 +1410,7 @@ extension BufferedStream { @inlinable init( callbackToken: Source.WriteResult.CallbackToken?, - continuationAndElement: (CheckedContinuation, Element)? = nil + continuationAndElement: (CheckedContinuation, Element)? = nil ) { switch (callbackToken, continuationAndElement) { case (.none, .none): @@ -1510,15 +1510,15 @@ extension BufferedStream { @usableFromInline enum EnqueueProducerAction { /// Indicates that the producer should be notified to produce more. - case resumeProducer((Result) -> Void) + case resumeProducer((Result) -> Void) /// Indicates that the producer should be notified about an error. - case resumeProducerWithError((Result) -> Void, Error) + case resumeProducerWithError((Result) -> Void, any Error) } @inlinable mutating func enqueueProducer( callbackToken: Source.WriteResult.CallbackToken, - onProduceMore: @Sendable @escaping (Result) -> Void + onProduceMore: @Sendable @escaping (Result) -> Void ) -> EnqueueProducerAction? { switch self._state { case .initial: @@ -1560,7 +1560,7 @@ extension BufferedStream { @usableFromInline enum CancelProducerAction { /// Indicates that the producer should be notified about cancellation. - case resumeProducerWithCancellationError((Result) -> Void) + case resumeProducerWithCancellationError((Result) -> Void) } @inlinable @@ -1610,18 +1610,18 @@ extension BufferedStream { /// Indicates that the consumer should be resumed with the failure, the producers /// should be resumed with an error and `onTermination` should be called. case resumeConsumerAndCallOnTermination( - consumerContinuation: CheckedContinuation, - failure: Error?, + consumerContinuation: CheckedContinuation, + failure: (any Error)?, onTermination: (() -> Void)? ) /// Indicates that the producers should be resumed with an error. case resumeProducers( - producerContinuations: [(Result) -> Void] + producerContinuations: [(Result) -> Void] ) } @inlinable - mutating func finish(_ failure: Error?) -> FinishAction? { + mutating func finish(_ failure: (any Error)?) -> FinishAction? { switch self._state { case .initial(let initial): // Nothing was yielded nor did anybody call next @@ -1685,9 +1685,9 @@ extension BufferedStream { /// Indicates that the element should be returned to the caller. case returnElement(Element) /// Indicates that the element should be returned to the caller and that all producers should be called. - case returnElementAndResumeProducers(Element, [(Result) -> Void]) + case returnElementAndResumeProducers(Element, [(Result) -> Void]) /// Indicates that the `Error` should be returned to the caller and that `onTermination` should be called. - case returnErrorAndCallOnTermination(Error?, (() -> Void)?) + case returnErrorAndCallOnTermination((any Error)?, (() -> Void)?) /// Indicates that the `nil` should be returned to the caller. case returnNil /// Indicates that the `Task` of the caller should be suspended. @@ -1779,26 +1779,26 @@ extension BufferedStream { @usableFromInline enum SuspendNextAction { /// Indicates that the consumer should be resumed. - case resumeConsumerWithElement(CheckedContinuation, Element) + case resumeConsumerWithElement(CheckedContinuation, Element) /// Indicates that the consumer and all producers should be resumed. case resumeConsumerWithElementAndProducers( - CheckedContinuation, + CheckedContinuation, Element, - [(Result) -> Void] + [(Result) -> Void] ) /// Indicates that the consumer should be resumed with the failure and that `onTermination` should be called. case resumeConsumerWithErrorAndCallOnTermination( - CheckedContinuation, - Error?, + CheckedContinuation, + (any Error)?, (() -> Void)? ) /// Indicates that the consumer should be resumed with `nil`. - case resumeConsumerWithNil(CheckedContinuation) + case resumeConsumerWithNil(CheckedContinuation) } @inlinable mutating func suspendNext( - continuation: CheckedContinuation + continuation: CheckedContinuation ) -> SuspendNextAction? { switch self._state { case .initial: @@ -1879,11 +1879,11 @@ extension BufferedStream { enum CancelNextAction { /// Indicates that the continuation should be resumed with a cancellation error, the producers should be finished and call onTermination. case resumeConsumerWithCancellationErrorAndCallOnTermination( - CheckedContinuation, + CheckedContinuation, (() -> Void)? ) /// Indicates that the producers should be finished and call onTermination. - case failProducersAndCallOnTermination([(Result) -> Void], (() -> Void)?) + case failProducersAndCallOnTermination([(Result) -> Void], (() -> Void)?) } @inlinable @@ -1930,7 +1930,7 @@ extension BufferedStream.Source: ClosableRPCWriterProtocol { } @inlinable - func finish(throwing error: Error) { - self.finish(throwing: error as Error?) + func finish(throwing error: any Error) { + self.finish(throwing: error as (any Error)?) } } diff --git a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift index 30099b040..7462766b7 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -64,7 +64,7 @@ extension RPCWriter { /// All writes after ``finish(throwing:)`` has been called should result in an error /// being thrown. @inlinable - public func finish(throwing error: Error) { + public func finish(throwing error: any Error) { self.writer.finish(throwing: error) } } diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index 7d4a4e379..fc10e8a63 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -63,5 +63,5 @@ public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// /// All writes after ``finish(throwing:)`` has been called should result in an error /// being thrown. - func finish(throwing error: Error) + func finish(throwing error: any Error) } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 10d0b2ced..90a7cad45 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -30,7 +30,7 @@ public enum ClientConnectionEvent: Sendable { /// The local peer initiated the close. case initiatedLocally /// The connection was closed unexpectedly - case unexpected(Error?, isIdle: Bool) + case unexpected((any Error)?, isIdle: Bool) } /// The connection is now ready. @@ -62,7 +62,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } /// The `EventLoop` of the `Channel` this handler exists in. - private let eventLoop: EventLoop + private let eventLoop: any EventLoop /// The maximum amount of time the connection may be idle for. If the connection remains idle /// (i.e. has no open streams) for this period of time then the connection will be gracefully @@ -104,7 +104,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl /// - keepaliveWithoutCalls: Whether the client sends keep-alive pings when there are no calls /// in progress. init( - eventLoop: EventLoop, + eventLoop: any EventLoop, maxIdleTime: TimeAmount?, keepaliveTime: TimeAmount?, keepaliveTimeout: TimeAmount?, @@ -612,7 +612,7 @@ extension ClientConnectionHandler { enum OnClosed { case succeed(EventLoopPromise) - case unexpectedClose(Error?, isIdle: Bool) + case unexpectedClose((any Error)?, isIdle: Bool) case none } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index 9eed38a2b..8c7c88908 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -69,7 +69,7 @@ struct Connection: Sendable { /// Closed because the remote peer initiate shutdown (i.e. sent a GOAWAY frame). case remote /// Closed because the connection encountered an unexpected error. - case error(Error, wasIdle: Bool) + case error(any Error, wasIdle: Bool) } /// Inputs to the 'run' method. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 0dbf0c8c9..6e7d57145 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -264,9 +264,9 @@ extension GRPCChannel { /// A stream was created, use it. case created(Connection.Stream) /// An error occurred while trying to create a stream, try again if possible. - case tryAgain(Error) + case tryAgain(any Error) /// An unrecoverable error occurred (e.g. the channel is closed), fail the RPC and don't retry. - case stopTrying(Error) + case stopTrying(any Error) } private func makeStream( @@ -734,8 +734,8 @@ extension GRPCChannel.StateMachine { var finish: Bool = false struct ResumableContinuations { - var continuations: [CheckedContinuation] - var result: Result + var continuations: [CheckedContinuation] + var result: Result } } @@ -878,7 +878,7 @@ extension GRPCChannel.StateMachine { } mutating func enqueue( - continuation: CheckedContinuation, + continuation: CheckedContinuation, waitForReady: Bool, id: QueueEntryID ) -> Bool { @@ -902,7 +902,7 @@ extension GRPCChannel.StateMachine { mutating func dequeueContinuation( id: QueueEntryID - ) -> CheckedContinuation? { + ) -> CheckedContinuation? { switch self.state { case .notRunning(var state): self.state = ._modifying diff --git a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift index e043bf24a..492dc9b61 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift @@ -18,7 +18,7 @@ import DequeModule @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) struct RequestQueue { - typealias Continuation = CheckedContinuation + typealias Continuation = CheckedContinuation private struct QueueEntry { var continuation: Continuation diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index bd71a0f36..de4d4b68a 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -107,7 +107,7 @@ extension ChannelPipeline.SynchronousOperations { @_spi(Package) @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) public func configureGRPCClientPipeline( - channel: Channel, + channel: any Channel, config: GRPCChannel.Config ) throws -> ( NIOAsyncChannel, diff --git a/Sources/GRPCHTTP2Core/Internal/Timer.swift b/Sources/GRPCHTTP2Core/Internal/Timer.swift index 0be004347..89795380b 100644 --- a/Sources/GRPCHTTP2Core/Internal/Timer.swift +++ b/Sources/GRPCHTTP2Core/Internal/Timer.swift @@ -45,7 +45,7 @@ struct Timer { } /// Schedule a task on the given `EventLoop`. - mutating func schedule(on eventLoop: EventLoop, work: @escaping @Sendable () throws -> Void) { + mutating func schedule(on eventLoop: any EventLoop, work: @escaping @Sendable () throws -> Void) { self.task?.cancel() if self.repeat { diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index a7d9b9bef..c4b63eb8f 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -44,7 +44,7 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { typealias OutboundOut = HTTP2Frame /// The `EventLoop` of the `Channel` this handler exists in. - private let eventLoop: EventLoop + private let eventLoop: any EventLoop /// The maximum amount of time a connection may be idle for. If the connection remains idle /// (i.e. has no open streams) for this period of time then the connection will be gracefully @@ -201,7 +201,7 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { /// connection is closed if there are too many strikes. /// - clock: A clock providing the current time. init( - eventLoop: EventLoop, + eventLoop: any EventLoop, maxIdleTime: TimeAmount?, maxAge: TimeAmount?, maxGraceTime: TimeAmount?, diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 2996af1bd..95d7f7e59 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -341,7 +341,7 @@ extension ServerBootstrap { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) fileprivate func bind( to address: GRPCHTTP2Core.SocketAddress, - childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture + childChannelInitializer: @escaping @Sendable (any Channel) -> EventLoopFuture ) async throws -> NIOAsyncChannel { if let virtualSocket = address.virtualSocket { return try await self.bind( diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift index ebeec013d..d81c92c06 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift @@ -23,7 +23,7 @@ extension ClientBootstrap { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func connect( to address: GRPCHTTP2Core.SocketAddress, - _ configure: @Sendable @escaping (Channel) -> EventLoopFuture + _ configure: @Sendable @escaping (any Channel) -> EventLoopFuture ) async throws -> Result { if let ipv4 = address.ipv4 { return try await self.connect(to: NIOCore.SocketAddress(ipv4), channelInitializer: configure) diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index fe340f191..93c596cbb 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -332,7 +332,7 @@ extension NIOCore.SocketAddress { extension NIOTSListenerBootstrap { fileprivate func bind( to address: GRPCHTTP2Core.SocketAddress, - childChannelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture + childChannelInitializer: @escaping @Sendable (any Channel) -> EventLoopFuture ) async throws -> NIOAsyncChannel { if address.virtualSocket != nil { throw RuntimeError( diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index d36d8f32d..71cdc55f0 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -72,7 +72,7 @@ public enum InteroperabilityTestCase: String, CaseIterable { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension InteroperabilityTestCase { /// Return a new instance of the test case. - public func makeTest() -> InteroperabilityTest { + public func makeTest() -> any InteroperabilityTest { switch self { case .emptyUnary: return EmptyUnary() diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 083ba94c8..b55c37f1b 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -266,7 +266,7 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { case let .some(.setup(serverConfig)): let (server, transport) = try await self.startServer(serverConfig) group.addTask { - let result: Result + let result: Result do { try await server.run() diff --git a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift index 2f92e4cca..dd2202af1 100644 --- a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift +++ b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift @@ -92,7 +92,7 @@ struct TestMessage: SwiftProtobuf.Message { return true } - func isEqualTo(message: SwiftProtobuf.Message) -> Bool { + func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { return false } } From 2ad120702e7b8ef68e25856aaeed0c7d40089454 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 5 Jul 2024 10:30:05 +0100 Subject: [PATCH 387/580] Avoid task group overheaed if there is no timeout (#1968) Motivation: Task groups and tasks come with associated costs. If we can avoid using them we should. The server knows when receiving a request whether it needs to enforce a deadline on that RPC, if it doesn't then it can avoid using a task group and save some allocations. Modifications: - Avoid creating a task group if there's no timeout Result: Fewer allocations --- .../Server/Internal/ServerRPCExecutor.swift | 50 ++++++++++++++++--- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 183e7ca85..2e3923f46 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -71,15 +71,53 @@ struct ServerRPCExecutor { handler: @escaping @Sendable ( _ request: ServerRequest.Stream ) async throws -> ServerResponse.Stream + ) async { + if let timeout = metadata.timeout { + await Self._processRPCWithTimeout( + timeout: timeout, + method: method, + metadata: metadata, + inbound: inbound, + outbound: outbound, + deserializer: deserializer, + serializer: serializer, + interceptors: interceptors, + handler: handler + ) + } else { + await Self._processRPC( + method: method, + metadata: metadata, + inbound: inbound, + outbound: outbound, + deserializer: deserializer, + serializer: serializer, + interceptors: interceptors, + handler: handler + ) + } + } + + @inlinable + static func _processRPCWithTimeout( + timeout: Duration, + method: MethodDescriptor, + metadata: Metadata, + inbound: UnsafeTransfer.AsyncIterator>, + outbound: RPCWriter.Closable, + deserializer: some MessageDeserializer, + serializer: some MessageSerializer, + interceptors: [any ServerInterceptor], + handler: @escaping @Sendable ( + ServerRequest.Stream + ) async throws -> ServerResponse.Stream ) async { await withTaskGroup(of: ServerExecutorTask.self) { group in - if let timeout = metadata.timeout { - group.addTask { - let result = await Result { - try await Task.sleep(for: timeout, clock: .continuous) - } - return .timedOut(result) + group.addTask { + let result = await Result { + try await Task.sleep(for: timeout, clock: .continuous) } + return .timedOut(result) } group.addTask { From b9b4711990a08f3dc93aaa4d27080645a8517329 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 5 Jul 2024 11:20:48 +0100 Subject: [PATCH 388/580] Avoid unnecessary CoWs in server state machine (#1969) Motivation: The server connection management state machine unnecessarily CoWs when streams are opened and closed. Modifications: - Add a `_modifying` state and switch to it before modifying the streams set Result: Fewer allocations --- ...ectionManagementHandler+StateMachine.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift index a9b123950..4aabf09a5 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift @@ -57,12 +57,14 @@ extension ServerConnectionManagementHandler { mutating func streamOpened(_ id: HTTP2StreamID) { switch self.state { case .active(var state): + self.state = ._modifying state.lastStreamID = id let (inserted, _) = state.openStreams.insert(id) assert(inserted, "Can't open stream \(Int(id)), it's already open") self.state = .active(state) case .closing(var state): + self.state = ._modifying state.lastStreamID = id let (inserted, _) = state.openStreams.insert(id) assert(inserted, "Can't open stream \(Int(id)), it's already open") @@ -70,6 +72,9 @@ extension ServerConnectionManagementHandler { case .closed: () + + case ._modifying: + preconditionFailure() } } @@ -88,12 +93,14 @@ extension ServerConnectionManagementHandler { switch self.state { case .active(var state): + self.state = ._modifying let removedID = state.openStreams.remove(id) assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") onStreamClosed = state.openStreams.isEmpty ? .startIdleTimer : .none self.state = .active(state) case .closing(var state): + self.state = ._modifying let removedID = state.openStreams.remove(id) assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") // If the second GOAWAY hasn't been sent it isn't safe to close if there are no open @@ -104,6 +111,9 @@ extension ServerConnectionManagementHandler { case .closed: onStreamClosed = .none + + case ._modifying: + preconditionFailure() } return onStreamClosed @@ -128,6 +138,7 @@ extension ServerConnectionManagementHandler { switch self.state { case .active(var state): + self.state = ._modifying let tooManyPings = state.keepalive.receivedPing( atTime: time, hasOpenStreams: !state.openStreams.isEmpty @@ -142,6 +153,7 @@ extension ServerConnectionManagementHandler { } case .closing(var state): + self.state = ._modifying let tooManyPings = state.keepalive.receivedPing( atTime: time, hasOpenStreams: !state.openStreams.isEmpty @@ -157,6 +169,9 @@ extension ServerConnectionManagementHandler { case .closed: onPing = .none + + case ._modifying: + preconditionFailure() } return onPing @@ -176,6 +191,8 @@ extension ServerConnectionManagementHandler { switch self.state { case .closing(var state): + self.state = ._modifying + // If only one GOAWAY has been sent and the data matches the data from the GOAWAY ping then // the server should send another GOAWAY ratcheting down the last stream ID. If no streams // are open then the server can close the connection immediately after, otherwise it must @@ -194,8 +211,13 @@ extension ServerConnectionManagementHandler { onPingAck = .none } + self.state = .closing(state) + case .active, .closed: onPingAck = .none + + case ._modifying: + preconditionFailure() } return onPingAck @@ -220,6 +242,9 @@ extension ServerConnectionManagementHandler { case .closing, .closed: onStartGracefulShutdown = .none + + case ._modifying: + preconditionFailure() } return onStartGracefulShutdown @@ -229,15 +254,20 @@ extension ServerConnectionManagementHandler { mutating func resetKeepaliveState() { switch self.state { case .active(var state): + self.state = ._modifying state.keepalive.reset() self.state = .active(state) case .closing(var state): + self.state = ._modifying state.keepalive.reset() self.state = .closing(state) case .closed: () + + case ._modifying: + preconditionFailure() } } @@ -360,5 +390,6 @@ extension ServerConnectionManagementHandler.StateMachine { case active(Active) case closing(Closing) case closed + case _modifying } } From 595a416bd6dab33f51bf0623d0fbf672429c25cb Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 5 Jul 2024 11:25:02 +0100 Subject: [PATCH 389/580] Avoid an error allocation in the server stream handler (#1970) Motivation: Existential errors unconditionally allocate. In the server stream handler a promise is failed on tear down via an API which takes an existential error. This means the handler always allocates, even if the promise has been completed. Modifications: - Statically allocate the error as an existential Result: Fewer allocations --- .../Server/GRPCServerStreamHandler.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index d9d31e35d..29d994b53 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -39,6 +39,13 @@ final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandl private let methodDescriptorPromise: EventLoopPromise + // Existential errors unconditionally allocate, avoid this per-use allocation by doing it + // statically. + private static let handlerRemovedBeforeDescriptorResolved: any Error = RPCError( + code: .unavailable, + message: "RPC stream was closed before we got any Metadata." + ) + init( scheme: GRPCStreamStateMachineConfiguration.Scheme, acceptedEncodings: CompressionAlgorithmSet, @@ -159,12 +166,7 @@ extension GRPCServerStreamHandler { func handlerRemoved(context: ChannelHandlerContext) { self.stateMachine.tearDown() - self.methodDescriptorPromise.fail( - RPCError( - code: .unavailable, - message: "RPC stream was closed before we got any Metadata." - ) - ) + self.methodDescriptorPromise.fail(Self.handlerRemovedBeforeDescriptorResolved) } func channelInactive(context: ChannelHandlerContext) { From 951b4d3f3ada63687a72f2fe76547827871f67c5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 8 Jul 2024 15:44:56 +0100 Subject: [PATCH 390/580] Fix ':scheme' for posix client (#1972) Motivation: The client doesn't support TLS (yet) but uses the 'https' scheme. Modifications: - Use the correct scheme - Add tests Result: Fewer bugs --- .../HTTP2ClientTransport+Posix.swift | 2 +- .../HTTP2TransportTests.swift | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index 7d0d4d74b..74b82a587 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -144,7 +144,7 @@ extension HTTP2ClientTransport.Posix { } } - return HTTP2Connection(channel: channel, multiplexer: multiplexer, isPlaintext: false) + return HTTP2Connection(channel: channel, multiplexer: multiplexer, isPlaintext: true) } } } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index e15953486..60fbbbe21 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -1353,6 +1353,62 @@ final class HTTP2TransportTests: XCTestCase { } } } + + func testUnaryScheme() async throws { + try await self.forEachTransportPair { control, pair in + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + } + let request = ClientRequest.Single(message: input) + try await control.unary(request: request) { response in + XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) + } + } + } + + func testServerStreamingScheme() async throws { + try await self.forEachTransportPair { control, pair in + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + } + let request = ClientRequest.Single(message: input) + try await control.serverStream(request: request) { response in + XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) + } + } + } + + func testClientStreamingScheme() async throws { + try await self.forEachTransportPair { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + } + try await writer.write(input) + } + try await control.clientStream(request: request) { response in + XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) + } + } + } + + func testBidiStreamingScheme() async throws { + try await self.forEachTransportPair { control, pair in + let request = ClientRequest.Stream(of: ControlInput.self) { writer in + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.numberOfMessages = 1 + } + try await writer.write(input) + } + try await control.bidiStream(request: request) { response in + XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) + } + } + } } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) From a1760349d585a507e3fd5c114ad0824af07617ce Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 8 Jul 2024 17:19:43 +0100 Subject: [PATCH 391/580] Update to protobuf 1.27.0 (#1973) Motivation: SwiftProtobuf just released a 1.27.0 containing a few years worth of changes and fixes. Modifications: - Update min version - Fix test - Regenerate protos Result: Protobuf is more up-to-date --- Package.swift | 2 +- Package@swift-6.swift | 2 +- Sources/Examples/v1/Echo/Model/echo.pb.swift | 9 +- .../v1/HelloWorld/Model/helloworld.pb.swift | 9 +- .../v1/RouteGuide/Model/route_guide.pb.swift | 18 +- .../Examples/v2/Echo/Generated/echo.pb.swift | 9 +- .../v1/reflection-v1.pb.swift | 89 +---- .../v1Alpha/reflection-v1alpha.pb.swift | 105 ++---- .../Generated/empty.pb.swift | 10 +- .../Generated/messages.pb.swift | 50 +-- .../Services/Health/Generated/health.pb.swift | 34 +- .../Generated/grpc_core_stats.pb.swift | 38 +- .../Generated/grpc_testing_control.pb.swift | 227 +++--------- .../Generated/grpc_testing_messages.pb.swift | 203 ++++------- .../Generated/grpc_testing_payloads.pb.swift | 44 +-- .../Generated/grpc_testing_stats.pb.swift | 44 +-- .../Configuration/Generated/code.pb.swift | 14 +- .../Configuration/Generated/rls.pb.swift | 32 +- .../Generated/rls_config.pb.swift | 24 +- .../Generated/service_config.pb.swift | 333 +++++------------- .../Generated/control.pb.swift | 26 +- .../ProtobufCodeGenParserTests.swift | 2 +- .../Normalization/normalization.pb.swift | 6 +- .../Generated/v1/reflection-v1.pb.swift | 89 +---- .../v1Alpha/reflection-v1alpha.pb.swift | 105 ++---- 25 files changed, 392 insertions(+), 1132 deletions(-) diff --git a/Package.swift b/Package.swift index 4a0ff1b07..4599df9ec 100644 --- a/Package.swift +++ b/Package.swift @@ -52,7 +52,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.20.2" + from: "1.27.0" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 59430cfcb..2edd9e065 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -56,7 +56,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.20.2" + from: "1.27.0" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Sources/Examples/v1/Echo/Model/echo.pb.swift b/Sources/Examples/v1/Echo/Model/echo.pb.swift index c5042aa77..bff6995e1 100644 --- a/Sources/Examples/v1/Echo/Model/echo.pb.swift +++ b/Sources/Examples/v1/Echo/Model/echo.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -public struct Echo_EchoRequest { +public struct Echo_EchoRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -47,7 +47,7 @@ public struct Echo_EchoRequest { public init() {} } -public struct Echo_EchoResponse { +public struct Echo_EchoResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -60,11 +60,6 @@ public struct Echo_EchoResponse { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Echo_EchoRequest: @unchecked Sendable {} -extension Echo_EchoResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "echo" diff --git a/Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift b/Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift index 7a8a922fb..a1ded002d 100644 --- a/Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift +++ b/Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift @@ -35,7 +35,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The request message containing the user's name. -public struct Helloworld_HelloRequest { +public struct Helloworld_HelloRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -48,7 +48,7 @@ public struct Helloworld_HelloRequest { } /// The response message containing the greetings -public struct Helloworld_HelloReply { +public struct Helloworld_HelloReply: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -60,11 +60,6 @@ public struct Helloworld_HelloReply { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Helloworld_HelloRequest: @unchecked Sendable {} -extension Helloworld_HelloReply: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "helloworld" diff --git a/Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift b/Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift index 682e4a809..0fe32db43 100644 --- a/Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift +++ b/Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift @@ -38,7 +38,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// (degrees multiplied by 10**7 and rounded to the nearest integer). /// Latitudes should be in the range +/- 90 degrees and longitude should be in /// the range +/- 180 degrees (inclusive). -public struct Routeguide_Point { +public struct Routeguide_Point: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -54,7 +54,7 @@ public struct Routeguide_Point { /// A latitude-longitude rectangle, represented as two diagonally opposite /// points "lo" and "hi". -public struct Routeguide_Rectangle { +public struct Routeguide_Rectangle: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -90,7 +90,7 @@ public struct Routeguide_Rectangle { /// A feature names something at a given point. /// /// If a feature could not be named, the name is empty. -public struct Routeguide_Feature { +public struct Routeguide_Feature: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -116,7 +116,7 @@ public struct Routeguide_Feature { } /// A RouteNote is a message sent while at a given point. -public struct Routeguide_RouteNote { +public struct Routeguide_RouteNote: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -146,7 +146,7 @@ public struct Routeguide_RouteNote { /// It contains the number of individual points received, the number of /// detected features, and the total distance covered as the cumulative sum of /// the distance between each point. -public struct Routeguide_RouteSummary { +public struct Routeguide_RouteSummary: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -168,14 +168,6 @@ public struct Routeguide_RouteSummary { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Routeguide_Point: @unchecked Sendable {} -extension Routeguide_Rectangle: @unchecked Sendable {} -extension Routeguide_Feature: @unchecked Sendable {} -extension Routeguide_RouteNote: @unchecked Sendable {} -extension Routeguide_RouteSummary: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "routeguide" diff --git a/Sources/Examples/v2/Echo/Generated/echo.pb.swift b/Sources/Examples/v2/Echo/Generated/echo.pb.swift index 3ee4e8d36..47249e093 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.pb.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Echo_EchoRequest { +struct Echo_EchoRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -47,7 +47,7 @@ struct Echo_EchoRequest { init() {} } -struct Echo_EchoResponse { +struct Echo_EchoResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -60,11 +60,6 @@ struct Echo_EchoResponse { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Echo_EchoRequest: @unchecked Sendable {} -extension Echo_EchoResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "echo" diff --git a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift index 8e0d557c4..ca5442979 100644 --- a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift +++ b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift @@ -42,7 +42,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The message sent by the client when calling ServerReflectionInfo method. -public struct Grpc_Reflection_V1_ServerReflectionRequest { +public struct Grpc_Reflection_V1_ServerReflectionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -115,7 +115,7 @@ public struct Grpc_Reflection_V1_ServerReflectionRequest { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. - public enum OneOf_MessageRequest: Equatable { + public enum OneOf_MessageRequest: Equatable, Sendable { /// Find a proto file by the file name. case fileByFilename(String) /// Find the proto file that declares the given fully-qualified symbol name. @@ -138,36 +138,6 @@ public struct Grpc_Reflection_V1_ServerReflectionRequest { /// checked. case listServices(String) - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileByFilename, .fileByFilename): return { - guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingSymbol, .fileContainingSymbol): return { - guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingExtension, .fileContainingExtension): return { - guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { - guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServices, .listServices): return { - guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -175,7 +145,7 @@ public struct Grpc_Reflection_V1_ServerReflectionRequest { /// The type name and extension number sent by the client when requesting /// file_containing_extension. -public struct Grpc_Reflection_V1_ExtensionRequest { +public struct Grpc_Reflection_V1_ExtensionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -191,7 +161,7 @@ public struct Grpc_Reflection_V1_ExtensionRequest { } /// The message sent by the server to answer ServerReflectionInfo method. -public struct Grpc_Reflection_V1_ServerReflectionResponse { +public struct Grpc_Reflection_V1_ServerReflectionResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -256,7 +226,7 @@ public struct Grpc_Reflection_V1_ServerReflectionResponse { /// The server sets one of the following fields according to the message_request /// in the request. - public enum OneOf_MessageResponse: Equatable { + public enum OneOf_MessageResponse: Equatable, Sendable { /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. /// As the repeated label is not allowed in oneof fields, we use a @@ -271,32 +241,6 @@ public struct Grpc_Reflection_V1_ServerReflectionResponse { /// This message is used when an error occurs. case errorResponse(Grpc_Reflection_V1_ErrorResponse) - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileDescriptorResponse, .fileDescriptorResponse): return { - guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { - guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServicesResponse, .listServicesResponse): return { - guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorResponse, .errorResponse): return { - guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -307,7 +251,7 @@ public struct Grpc_Reflection_V1_ServerReflectionResponse { /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. -public struct Grpc_Reflection_V1_FileDescriptorResponse { +public struct Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -324,7 +268,7 @@ public struct Grpc_Reflection_V1_FileDescriptorResponse { /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. -public struct Grpc_Reflection_V1_ExtensionNumberResponse { +public struct Grpc_Reflection_V1_ExtensionNumberResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -341,7 +285,7 @@ public struct Grpc_Reflection_V1_ExtensionNumberResponse { } /// A list of ServiceResponse sent by the server answering list_services request. -public struct Grpc_Reflection_V1_ListServiceResponse { +public struct Grpc_Reflection_V1_ListServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -357,7 +301,7 @@ public struct Grpc_Reflection_V1_ListServiceResponse { /// The information of a single service used by ListServiceResponse to answer /// list_services request. -public struct Grpc_Reflection_V1_ServiceResponse { +public struct Grpc_Reflection_V1_ServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -372,7 +316,7 @@ public struct Grpc_Reflection_V1_ServiceResponse { } /// The error code and error message sent by the server when an error occurs. -public struct Grpc_Reflection_V1_ErrorResponse { +public struct Grpc_Reflection_V1_ErrorResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -387,19 +331,6 @@ public struct Grpc_Reflection_V1_ErrorResponse { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Reflection_V1_ServerReflectionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1_ExtensionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServerReflectionResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ExtensionNumberResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ListServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ErrorResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.reflection.v1" diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift index 1cf7338ba..f4569ce38 100644 --- a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift +++ b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift @@ -39,7 +39,9 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The message sent by the client when calling ServerReflectionInfo method. -public struct Grpc_Reflection_V1alpha_ServerReflectionRequest { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_ServerReflectionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -112,7 +114,7 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. - public enum OneOf_MessageRequest: Equatable { + public enum OneOf_MessageRequest: Equatable, Sendable { /// Find a proto file by the file name. case fileByFilename(String) /// Find the proto file that declares the given fully-qualified symbol name. @@ -135,36 +137,6 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// checked. case listServices(String) - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileByFilename, .fileByFilename): return { - guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingSymbol, .fileContainingSymbol): return { - guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingExtension, .fileContainingExtension): return { - guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { - guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServices, .listServices): return { - guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -172,7 +144,9 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// The type name and extension number sent by the client when requesting /// file_containing_extension. -public struct Grpc_Reflection_V1alpha_ExtensionRequest { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_ExtensionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -188,7 +162,9 @@ public struct Grpc_Reflection_V1alpha_ExtensionRequest { } /// The message sent by the server to answer ServerReflectionInfo method. -public struct Grpc_Reflection_V1alpha_ServerReflectionResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_ServerReflectionResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -253,7 +229,7 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// The server set one of the following fields according to the message_request /// in the request. - public enum OneOf_MessageResponse: Equatable { + public enum OneOf_MessageResponse: Equatable, Sendable { /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. As /// the repeated label is not allowed in oneof fields, we use a @@ -268,32 +244,6 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// This message is used when an error occurs. case errorResponse(Grpc_Reflection_V1alpha_ErrorResponse) - #if !swift(>=4.1) - public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileDescriptorResponse, .fileDescriptorResponse): return { - guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { - guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServicesResponse, .listServicesResponse): return { - guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorResponse, .errorResponse): return { - guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } public init() {} @@ -304,7 +254,9 @@ public struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. -public struct Grpc_Reflection_V1alpha_FileDescriptorResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -321,7 +273,9 @@ public struct Grpc_Reflection_V1alpha_FileDescriptorResponse { /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. -public struct Grpc_Reflection_V1alpha_ExtensionNumberResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_ExtensionNumberResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -338,7 +292,9 @@ public struct Grpc_Reflection_V1alpha_ExtensionNumberResponse { } /// A list of ServiceResponse sent by the server answering list_services request. -public struct Grpc_Reflection_V1alpha_ListServiceResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_ListServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -354,7 +310,9 @@ public struct Grpc_Reflection_V1alpha_ListServiceResponse { /// The information of a single service used by ListServiceResponse to answer /// list_services request. -public struct Grpc_Reflection_V1alpha_ServiceResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_ServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -369,7 +327,9 @@ public struct Grpc_Reflection_V1alpha_ServiceResponse { } /// The error code and error message sent by the server when an error occurs. -public struct Grpc_Reflection_V1alpha_ErrorResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +public struct Grpc_Reflection_V1alpha_ErrorResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -384,19 +344,6 @@ public struct Grpc_Reflection_V1alpha_ErrorResponse { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Reflection_V1alpha_ServerReflectionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ExtensionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServerReflectionResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ListServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ErrorResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.reflection.v1alpha" diff --git a/Sources/InteroperabilityTests/Generated/empty.pb.swift b/Sources/InteroperabilityTests/Generated/empty.pb.swift index bee17ecca..e6965f03e 100644 --- a/Sources/InteroperabilityTests/Generated/empty.pb.swift +++ b/Sources/InteroperabilityTests/Generated/empty.pb.swift @@ -41,7 +41,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// service Foo { /// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; /// }; -public struct Grpc_Testing_Empty { +public struct Grpc_Testing_Empty: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -51,10 +51,6 @@ public struct Grpc_Testing_Empty { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_Empty: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.testing" @@ -64,8 +60,8 @@ extension Grpc_Testing_Empty: SwiftProtobuf.Message, SwiftProtobuf._MessageImple public static let _protobuf_nameMap = SwiftProtobuf._NameMap() public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } public func traverse(visitor: inout V) throws { diff --git a/Sources/InteroperabilityTests/Generated/messages.pb.swift b/Sources/InteroperabilityTests/Generated/messages.pb.swift index fce2cbc98..34ad924a9 100644 --- a/Sources/InteroperabilityTests/Generated/messages.pb.swift +++ b/Sources/InteroperabilityTests/Generated/messages.pb.swift @@ -37,7 +37,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The type of payload that should be returned. -public enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { +public enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum, Swift.CaseIterable { public typealias RawValue = Int /// Compressable text format. @@ -62,23 +62,17 @@ public enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Grpc_Testing_PayloadType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [Grpc_Testing_PayloadType] = [ .compressable, ] -} -#endif // swift(>=4.2) +} /// TODO(dgq): Go back to using well-known types once /// https://github.com/grpc/grpc/issues/6980 has been fixed. /// import "google/protobuf/wrappers.proto"; -public struct Grpc_Testing_BoolValue { +public struct Grpc_Testing_BoolValue: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -92,7 +86,7 @@ public struct Grpc_Testing_BoolValue { } /// A block of data, to simply increase gRPC message size. -public struct Grpc_Testing_Payload { +public struct Grpc_Testing_Payload: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -110,7 +104,7 @@ public struct Grpc_Testing_Payload { /// A protobuf representation for grpc status. This is used by test /// clients to specify a status that the server should attempt to return. -public struct Grpc_Testing_EchoStatus { +public struct Grpc_Testing_EchoStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -125,7 +119,7 @@ public struct Grpc_Testing_EchoStatus { } /// Unary request. -public struct Grpc_Testing_SimpleRequest { +public struct Grpc_Testing_SimpleRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -197,7 +191,7 @@ public struct Grpc_Testing_SimpleRequest { } /// Unary response, as configured by the request. -public struct Grpc_Testing_SimpleResponse { +public struct Grpc_Testing_SimpleResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -227,7 +221,7 @@ public struct Grpc_Testing_SimpleResponse { } /// Client-streaming request. -public struct Grpc_Testing_StreamingInputCallRequest { +public struct Grpc_Testing_StreamingInputCallRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -264,7 +258,7 @@ public struct Grpc_Testing_StreamingInputCallRequest { } /// Client-streaming response. -public struct Grpc_Testing_StreamingInputCallResponse { +public struct Grpc_Testing_StreamingInputCallResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -278,7 +272,7 @@ public struct Grpc_Testing_StreamingInputCallResponse { } /// Configuration for a particular response. -public struct Grpc_Testing_ResponseParameters { +public struct Grpc_Testing_ResponseParameters: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -311,7 +305,7 @@ public struct Grpc_Testing_ResponseParameters { } /// Server-streaming request. -public struct Grpc_Testing_StreamingOutputCallRequest { +public struct Grpc_Testing_StreamingOutputCallRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -354,7 +348,7 @@ public struct Grpc_Testing_StreamingOutputCallRequest { } /// Server-streaming response, as configured by the request and parameters. -public struct Grpc_Testing_StreamingOutputCallResponse { +public struct Grpc_Testing_StreamingOutputCallResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -378,7 +372,7 @@ public struct Grpc_Testing_StreamingOutputCallResponse { /// For reconnect interop test only. /// Client tells server what reconnection parameters it used. -public struct Grpc_Testing_ReconnectParams { +public struct Grpc_Testing_ReconnectParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -393,7 +387,7 @@ public struct Grpc_Testing_ReconnectParams { /// For reconnect interop test only. /// Server tells client whether its reconnects are following the spec and the /// reconnect backoffs it saw. -public struct Grpc_Testing_ReconnectInfo { +public struct Grpc_Testing_ReconnectInfo: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -407,22 +401,6 @@ public struct Grpc_Testing_ReconnectInfo { public init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_PayloadType: @unchecked Sendable {} -extension Grpc_Testing_BoolValue: @unchecked Sendable {} -extension Grpc_Testing_Payload: @unchecked Sendable {} -extension Grpc_Testing_EchoStatus: @unchecked Sendable {} -extension Grpc_Testing_SimpleRequest: @unchecked Sendable {} -extension Grpc_Testing_SimpleResponse: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ResponseParameters: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ReconnectParams: @unchecked Sendable {} -extension Grpc_Testing_ReconnectInfo: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.testing" diff --git a/Sources/Services/Health/Generated/health.pb.swift b/Sources/Services/Health/Generated/health.pb.swift index 2d0981eec..883a55443 100644 --- a/Sources/Services/Health/Generated/health.pb.swift +++ b/Sources/Services/Health/Generated/health.pb.swift @@ -37,7 +37,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Grpc_Health_V1_HealthCheckRequest { +struct Grpc_Health_V1_HealthCheckRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -49,7 +49,7 @@ struct Grpc_Health_V1_HealthCheckRequest { init() {} } -struct Grpc_Health_V1_HealthCheckResponse { +struct Grpc_Health_V1_HealthCheckResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -58,7 +58,7 @@ struct Grpc_Health_V1_HealthCheckResponse { var unknownFields = SwiftProtobuf.UnknownStorage() - enum ServingStatus: SwiftProtobuf.Enum { + enum ServingStatus: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unknown // = 0 case serving // = 1 @@ -92,31 +92,19 @@ struct Grpc_Health_V1_HealthCheckResponse { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ + .unknown, + .serving, + .notServing, + .serviceUnknown, + ] + } init() {} } -#if swift(>=4.2) - -extension Grpc_Health_V1_HealthCheckResponse.ServingStatus: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ - .unknown, - .serving, - .notServing, - .serviceUnknown, - ] -} - -#endif // swift(>=4.2) - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Health_V1_HealthCheckRequest: @unchecked Sendable {} -extension Grpc_Health_V1_HealthCheckResponse: @unchecked Sendable {} -extension Grpc_Health_V1_HealthCheckResponse.ServingStatus: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.health.v1" diff --git a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift index 9f7d794b8..8ae7f8aeb 100644 --- a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift +++ b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Grpc_Core_Bucket { +struct Grpc_Core_Bucket: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -48,7 +48,7 @@ struct Grpc_Core_Bucket { init() {} } -struct Grpc_Core_Histogram { +struct Grpc_Core_Histogram: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -60,7 +60,7 @@ struct Grpc_Core_Histogram { init() {} } -struct Grpc_Core_Metric { +struct Grpc_Core_Metric: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -87,34 +87,16 @@ struct Grpc_Core_Metric { var unknownFields = SwiftProtobuf.UnknownStorage() - enum OneOf_Value: Equatable { + enum OneOf_Value: Equatable, Sendable { case count(UInt64) case histogram(Grpc_Core_Histogram) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Core_Metric.OneOf_Value, rhs: Grpc_Core_Metric.OneOf_Value) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.count, .count): return { - guard case .count(let l) = lhs, case .count(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.histogram, .histogram): return { - guard case .histogram(let l) = lhs, case .histogram(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} } -struct Grpc_Core_Stats { +struct Grpc_Core_Stats: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -126,14 +108,6 @@ struct Grpc_Core_Stats { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Core_Bucket: @unchecked Sendable {} -extension Grpc_Core_Histogram: @unchecked Sendable {} -extension Grpc_Core_Metric: @unchecked Sendable {} -extension Grpc_Core_Metric.OneOf_Value: @unchecked Sendable {} -extension Grpc_Core_Stats: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.core" @@ -159,7 +133,7 @@ extension Grpc_Core_Bucket: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme } func traverse(visitor: inout V) throws { - if self.start != 0 { + if self.start.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.start, fieldNumber: 1) } if self.count != 0 { diff --git a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift index 3da181b00..44d5a5b61 100644 --- a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -enum Grpc_Testing_ClientType: SwiftProtobuf.Enum { +enum Grpc_Testing_ClientType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int /// Many languages support a basic distinction between using @@ -71,11 +71,6 @@ enum Grpc_Testing_ClientType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Grpc_Testing_ClientType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [Grpc_Testing_ClientType] = [ .syncClient, @@ -83,11 +78,10 @@ extension Grpc_Testing_ClientType: CaseIterable { .otherClient, .callbackClient, ] -} -#endif // swift(>=4.2) +} -enum Grpc_Testing_ServerType: SwiftProtobuf.Enum { +enum Grpc_Testing_ServerType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case syncServer // = 0 case asyncServer // = 1 @@ -124,11 +118,6 @@ enum Grpc_Testing_ServerType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Grpc_Testing_ServerType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [Grpc_Testing_ServerType] = [ .syncServer, @@ -137,11 +126,10 @@ extension Grpc_Testing_ServerType: CaseIterable { .otherServer, .callbackServer, ] -} -#endif // swift(>=4.2) +} -enum Grpc_Testing_RpcType: SwiftProtobuf.Enum { +enum Grpc_Testing_RpcType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unary // = 0 case streaming // = 1 @@ -176,11 +164,6 @@ enum Grpc_Testing_RpcType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Grpc_Testing_RpcType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [Grpc_Testing_RpcType] = [ .unary, @@ -189,13 +172,12 @@ extension Grpc_Testing_RpcType: CaseIterable { .streamingFromServer, .streamingBothWays, ] -} -#endif // swift(>=4.2) +} /// Parameters of poisson process distribution, which is a good representation /// of activity coming in from independent identical stationary sources. -struct Grpc_Testing_PoissonParams { +struct Grpc_Testing_PoissonParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -210,7 +192,7 @@ struct Grpc_Testing_PoissonParams { /// Once an RPC finishes, immediately start a new one. /// No configuration parameters needed. -struct Grpc_Testing_ClosedLoopParams { +struct Grpc_Testing_ClosedLoopParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -220,7 +202,7 @@ struct Grpc_Testing_ClosedLoopParams { init() {} } -struct Grpc_Testing_LoadParams { +struct Grpc_Testing_LoadParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -245,35 +227,17 @@ struct Grpc_Testing_LoadParams { var unknownFields = SwiftProtobuf.UnknownStorage() - enum OneOf_Load: Equatable { + enum OneOf_Load: Equatable, Sendable { case closedLoop(Grpc_Testing_ClosedLoopParams) case poisson(Grpc_Testing_PoissonParams) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Testing_LoadParams.OneOf_Load, rhs: Grpc_Testing_LoadParams.OneOf_Load) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.closedLoop, .closedLoop): return { - guard case .closedLoop(let l) = lhs, case .closedLoop(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.poisson, .poisson): return { - guard case .poisson(let l) = lhs, case .poisson(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} } /// presence of SecurityParams implies use of TLS -struct Grpc_Testing_SecurityParams { +struct Grpc_Testing_SecurityParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -289,7 +253,7 @@ struct Grpc_Testing_SecurityParams { init() {} } -struct Grpc_Testing_ChannelArg { +struct Grpc_Testing_ChannelArg: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -316,34 +280,16 @@ struct Grpc_Testing_ChannelArg { var unknownFields = SwiftProtobuf.UnknownStorage() - enum OneOf_Value: Equatable { + enum OneOf_Value: Equatable, Sendable { case strValue(String) case intValue(Int32) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Testing_ChannelArg.OneOf_Value, rhs: Grpc_Testing_ChannelArg.OneOf_Value) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.strValue, .strValue): return { - guard case .strValue(let l) = lhs, case .strValue(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.intValue, .intValue): return { - guard case .intValue(let l) = lhs, case .intValue(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} } -struct Grpc_Testing_ClientConfig { +struct Grpc_Testing_ClientConfig: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -481,7 +427,7 @@ struct Grpc_Testing_ClientConfig { fileprivate var _storage = _StorageClass.defaultInstance } -struct Grpc_Testing_ClientStatus { +struct Grpc_Testing_ClientStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -503,7 +449,7 @@ struct Grpc_Testing_ClientStatus { } /// Request current stats -struct Grpc_Testing_Mark { +struct Grpc_Testing_Mark: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -516,7 +462,7 @@ struct Grpc_Testing_Mark { init() {} } -struct Grpc_Testing_ClientArgs { +struct Grpc_Testing_ClientArgs: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -541,34 +487,16 @@ struct Grpc_Testing_ClientArgs { var unknownFields = SwiftProtobuf.UnknownStorage() - enum OneOf_Argtype: Equatable { + enum OneOf_Argtype: Equatable, Sendable { case setup(Grpc_Testing_ClientConfig) case mark(Grpc_Testing_Mark) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Testing_ClientArgs.OneOf_Argtype, rhs: Grpc_Testing_ClientArgs.OneOf_Argtype) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.setup, .setup): return { - guard case .setup(let l) = lhs, case .setup(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mark, .mark): return { - guard case .mark(let l) = lhs, case .mark(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} } -struct Grpc_Testing_ServerConfig { +struct Grpc_Testing_ServerConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -631,7 +559,7 @@ struct Grpc_Testing_ServerConfig { fileprivate var _payloadConfig: Grpc_Testing_PayloadConfig? = nil } -struct Grpc_Testing_ServerArgs { +struct Grpc_Testing_ServerArgs: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -656,34 +584,16 @@ struct Grpc_Testing_ServerArgs { var unknownFields = SwiftProtobuf.UnknownStorage() - enum OneOf_Argtype: Equatable { + enum OneOf_Argtype: Equatable, Sendable { case setup(Grpc_Testing_ServerConfig) case mark(Grpc_Testing_Mark) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Testing_ServerArgs.OneOf_Argtype, rhs: Grpc_Testing_ServerArgs.OneOf_Argtype) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.setup, .setup): return { - guard case .setup(let l) = lhs, case .setup(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.mark, .mark): return { - guard case .mark(let l) = lhs, case .mark(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} } -struct Grpc_Testing_ServerStatus { +struct Grpc_Testing_ServerStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -710,7 +620,7 @@ struct Grpc_Testing_ServerStatus { fileprivate var _stats: Grpc_Testing_ServerStats? = nil } -struct Grpc_Testing_CoreRequest { +struct Grpc_Testing_CoreRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -720,7 +630,7 @@ struct Grpc_Testing_CoreRequest { init() {} } -struct Grpc_Testing_CoreResponse { +struct Grpc_Testing_CoreResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -733,7 +643,7 @@ struct Grpc_Testing_CoreResponse { init() {} } -struct Grpc_Testing_Void { +struct Grpc_Testing_Void: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -744,7 +654,7 @@ struct Grpc_Testing_Void { } /// A single performance scenario: input to qps_json_driver -struct Grpc_Testing_Scenario { +struct Grpc_Testing_Scenario: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -813,7 +723,7 @@ struct Grpc_Testing_Scenario { } /// A set of scenarios to be run with qps_json_driver -struct Grpc_Testing_Scenarios { +struct Grpc_Testing_Scenarios: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -827,7 +737,7 @@ struct Grpc_Testing_Scenarios { /// Basic summary that can be computed from ClientStats and ServerStats /// once the scenario has finished. -struct Grpc_Testing_ScenarioResultSummary { +struct Grpc_Testing_ScenarioResultSummary: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -965,7 +875,7 @@ struct Grpc_Testing_ScenarioResultSummary { } /// Results of a single benchmark scenario. -struct Grpc_Testing_ScenarioResult { +struct Grpc_Testing_ScenarioResult: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1026,35 +936,6 @@ struct Grpc_Testing_ScenarioResult { fileprivate var _summary: Grpc_Testing_ScenarioResultSummary? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_ClientType: @unchecked Sendable {} -extension Grpc_Testing_ServerType: @unchecked Sendable {} -extension Grpc_Testing_RpcType: @unchecked Sendable {} -extension Grpc_Testing_PoissonParams: @unchecked Sendable {} -extension Grpc_Testing_ClosedLoopParams: @unchecked Sendable {} -extension Grpc_Testing_LoadParams: @unchecked Sendable {} -extension Grpc_Testing_LoadParams.OneOf_Load: @unchecked Sendable {} -extension Grpc_Testing_SecurityParams: @unchecked Sendable {} -extension Grpc_Testing_ChannelArg: @unchecked Sendable {} -extension Grpc_Testing_ChannelArg.OneOf_Value: @unchecked Sendable {} -extension Grpc_Testing_ClientConfig: @unchecked Sendable {} -extension Grpc_Testing_ClientStatus: @unchecked Sendable {} -extension Grpc_Testing_Mark: @unchecked Sendable {} -extension Grpc_Testing_ClientArgs: @unchecked Sendable {} -extension Grpc_Testing_ClientArgs.OneOf_Argtype: @unchecked Sendable {} -extension Grpc_Testing_ServerConfig: @unchecked Sendable {} -extension Grpc_Testing_ServerArgs: @unchecked Sendable {} -extension Grpc_Testing_ServerArgs.OneOf_Argtype: @unchecked Sendable {} -extension Grpc_Testing_ServerStatus: @unchecked Sendable {} -extension Grpc_Testing_CoreRequest: @unchecked Sendable {} -extension Grpc_Testing_CoreResponse: @unchecked Sendable {} -extension Grpc_Testing_Void: @unchecked Sendable {} -extension Grpc_Testing_Scenario: @unchecked Sendable {} -extension Grpc_Testing_Scenarios: @unchecked Sendable {} -extension Grpc_Testing_ScenarioResultSummary: @unchecked Sendable {} -extension Grpc_Testing_ScenarioResult: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.testing" @@ -1107,7 +988,7 @@ extension Grpc_Testing_PoissonParams: SwiftProtobuf.Message, SwiftProtobuf._Mess } func traverse(visitor: inout V) throws { - if self.offeredLoad != 0 { + if self.offeredLoad.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.offeredLoad, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) @@ -1125,8 +1006,8 @@ extension Grpc_Testing_ClosedLoopParams: SwiftProtobuf.Message, SwiftProtobuf._M static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { @@ -1902,8 +1783,8 @@ extension Grpc_Testing_CoreRequest: SwiftProtobuf.Message, SwiftProtobuf._Messag static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { @@ -1953,8 +1834,8 @@ extension Grpc_Testing_Void: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { @@ -2261,58 +2142,58 @@ extension Grpc_Testing_ScenarioResultSummary: SwiftProtobuf.Message, SwiftProtob // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - if _storage._qps != 0 { + if _storage._qps.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._qps, fieldNumber: 1) } - if _storage._qpsPerServerCore != 0 { + if _storage._qpsPerServerCore.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._qpsPerServerCore, fieldNumber: 2) } - if _storage._serverSystemTime != 0 { + if _storage._serverSystemTime.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._serverSystemTime, fieldNumber: 3) } - if _storage._serverUserTime != 0 { + if _storage._serverUserTime.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._serverUserTime, fieldNumber: 4) } - if _storage._clientSystemTime != 0 { + if _storage._clientSystemTime.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._clientSystemTime, fieldNumber: 5) } - if _storage._clientUserTime != 0 { + if _storage._clientUserTime.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._clientUserTime, fieldNumber: 6) } - if _storage._latency50 != 0 { + if _storage._latency50.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._latency50, fieldNumber: 7) } - if _storage._latency90 != 0 { + if _storage._latency90.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._latency90, fieldNumber: 8) } - if _storage._latency95 != 0 { + if _storage._latency95.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._latency95, fieldNumber: 9) } - if _storage._latency99 != 0 { + if _storage._latency99.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._latency99, fieldNumber: 10) } - if _storage._latency999 != 0 { + if _storage._latency999.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._latency999, fieldNumber: 11) } - if _storage._serverCpuUsage != 0 { + if _storage._serverCpuUsage.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._serverCpuUsage, fieldNumber: 12) } - if _storage._successfulRequestsPerSecond != 0 { + if _storage._successfulRequestsPerSecond.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._successfulRequestsPerSecond, fieldNumber: 13) } - if _storage._failedRequestsPerSecond != 0 { + if _storage._failedRequestsPerSecond.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._failedRequestsPerSecond, fieldNumber: 14) } - if _storage._clientPollsPerRequest != 0 { + if _storage._clientPollsPerRequest.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._clientPollsPerRequest, fieldNumber: 15) } - if _storage._serverPollsPerRequest != 0 { + if _storage._serverPollsPerRequest.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._serverPollsPerRequest, fieldNumber: 16) } - if _storage._serverQueriesPerCpuSec != 0 { + if _storage._serverQueriesPerCpuSec.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._serverQueriesPerCpuSec, fieldNumber: 17) } - if _storage._clientQueriesPerCpuSec != 0 { + if _storage._clientQueriesPerCpuSec.bitPattern != 0 { try visitor.visitSingularDoubleField(value: _storage._clientQueriesPerCpuSec, fieldNumber: 18) } try { if let v = _storage._startTime { diff --git a/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift b/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift index e1239674f..5d0680338 100644 --- a/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift @@ -37,7 +37,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The type of payload that should be returned. -enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { +enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int /// Compressable text format. @@ -62,18 +62,12 @@ enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Grpc_Testing_PayloadType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [Grpc_Testing_PayloadType] = [ .compressable, ] -} -#endif // swift(>=4.2) +} /// The type of route that a client took to reach a server w.r.t. gRPCLB. /// The server must fill in "fallback" if it detects that the RPC reached @@ -81,7 +75,7 @@ extension Grpc_Testing_PayloadType: CaseIterable { /// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got /// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly /// how this detection is done is context and server dependent. -enum Grpc_Testing_GrpclbRouteType: SwiftProtobuf.Enum { +enum Grpc_Testing_GrpclbRouteType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int /// Server didn't detect the route that a client took to reach it. @@ -116,25 +110,19 @@ enum Grpc_Testing_GrpclbRouteType: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Grpc_Testing_GrpclbRouteType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [Grpc_Testing_GrpclbRouteType] = [ .unknown, .fallback, .backend, ] -} -#endif // swift(>=4.2) +} /// TODO(dgq): Go back to using well-known types once /// https://github.com/grpc/grpc/issues/6980 has been fixed. /// import "google/protobuf/wrappers.proto"; -struct Grpc_Testing_BoolValue { +struct Grpc_Testing_BoolValue: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -148,7 +136,7 @@ struct Grpc_Testing_BoolValue { } /// A block of data, to simply increase gRPC message size. -struct Grpc_Testing_Payload { +struct Grpc_Testing_Payload: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -166,7 +154,7 @@ struct Grpc_Testing_Payload { /// A protobuf representation for grpc status. This is used by test /// clients to specify a status that the server should attempt to return. -struct Grpc_Testing_EchoStatus { +struct Grpc_Testing_EchoStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -181,7 +169,7 @@ struct Grpc_Testing_EchoStatus { } /// Unary request. -struct Grpc_Testing_SimpleRequest { +struct Grpc_Testing_SimpleRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -270,7 +258,7 @@ struct Grpc_Testing_SimpleRequest { } /// Unary response, as configured by the request. -struct Grpc_Testing_SimpleResponse { +struct Grpc_Testing_SimpleResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -310,7 +298,7 @@ struct Grpc_Testing_SimpleResponse { } /// Client-streaming request. -struct Grpc_Testing_StreamingInputCallRequest { +struct Grpc_Testing_StreamingInputCallRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -347,7 +335,7 @@ struct Grpc_Testing_StreamingInputCallRequest { } /// Client-streaming response. -struct Grpc_Testing_StreamingInputCallResponse { +struct Grpc_Testing_StreamingInputCallResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -361,7 +349,7 @@ struct Grpc_Testing_StreamingInputCallResponse { } /// Configuration for a particular response. -struct Grpc_Testing_ResponseParameters { +struct Grpc_Testing_ResponseParameters: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -394,7 +382,7 @@ struct Grpc_Testing_ResponseParameters { } /// Server-streaming request. -struct Grpc_Testing_StreamingOutputCallRequest { +struct Grpc_Testing_StreamingOutputCallRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -448,7 +436,7 @@ struct Grpc_Testing_StreamingOutputCallRequest { } /// Server-streaming response, as configured by the request and parameters. -struct Grpc_Testing_StreamingOutputCallResponse { +struct Grpc_Testing_StreamingOutputCallResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -472,7 +460,7 @@ struct Grpc_Testing_StreamingOutputCallResponse { /// For reconnect interop test only. /// Client tells server what reconnection parameters it used. -struct Grpc_Testing_ReconnectParams { +struct Grpc_Testing_ReconnectParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -487,7 +475,7 @@ struct Grpc_Testing_ReconnectParams { /// For reconnect interop test only. /// Server tells client whether its reconnects are following the spec and the /// reconnect backoffs it saw. -struct Grpc_Testing_ReconnectInfo { +struct Grpc_Testing_ReconnectInfo: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -501,7 +489,7 @@ struct Grpc_Testing_ReconnectInfo { init() {} } -struct Grpc_Testing_LoadBalancerStatsRequest { +struct Grpc_Testing_LoadBalancerStatsRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -522,7 +510,7 @@ struct Grpc_Testing_LoadBalancerStatsRequest { init() {} } -struct Grpc_Testing_LoadBalancerStatsResponse { +struct Grpc_Testing_LoadBalancerStatsResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -540,7 +528,7 @@ struct Grpc_Testing_LoadBalancerStatsResponse { var unknownFields = SwiftProtobuf.UnknownStorage() - enum MetadataType: SwiftProtobuf.Enum { + enum MetadataType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unknown // = 0 case initial // = 1 @@ -569,9 +557,16 @@ struct Grpc_Testing_LoadBalancerStatsResponse { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_LoadBalancerStatsResponse.MetadataType] = [ + .unknown, + .initial, + .trailing, + ] + } - struct MetadataEntry { + struct MetadataEntry: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -591,7 +586,7 @@ struct Grpc_Testing_LoadBalancerStatsResponse { init() {} } - struct RpcMetadata { + struct RpcMetadata: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -605,7 +600,7 @@ struct Grpc_Testing_LoadBalancerStatsResponse { init() {} } - struct MetadataByPeer { + struct MetadataByPeer: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -618,7 +613,7 @@ struct Grpc_Testing_LoadBalancerStatsResponse { init() {} } - struct RpcsByPeer { + struct RpcsByPeer: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -634,21 +629,8 @@ struct Grpc_Testing_LoadBalancerStatsResponse { init() {} } -#if swift(>=4.2) - -extension Grpc_Testing_LoadBalancerStatsResponse.MetadataType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_LoadBalancerStatsResponse.MetadataType] = [ - .unknown, - .initial, - .trailing, - ] -} - -#endif // swift(>=4.2) - /// Request for retrieving a test client's accumulated stats. -struct Grpc_Testing_LoadBalancerAccumulatedStatsRequest { +struct Grpc_Testing_LoadBalancerAccumulatedStatsRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -659,21 +641,27 @@ struct Grpc_Testing_LoadBalancerAccumulatedStatsRequest { } /// Accumulated stats for RPCs sent by a test client. -struct Grpc_Testing_LoadBalancerAccumulatedStatsResponse { +struct Grpc_Testing_LoadBalancerAccumulatedStatsResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// The total number of RPCs have ever issued for each type. /// Deprecated: use stats_per_method.rpcs_started instead. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var numRpcsStartedByMethod: Dictionary = [:] /// The total number of RPCs have ever completed successfully for each type. /// Deprecated: use stats_per_method.result instead. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var numRpcsSucceededByMethod: Dictionary = [:] /// The total number of RPCs have ever failed for each type. /// Deprecated: use stats_per_method.result instead. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var numRpcsFailedByMethod: Dictionary = [:] /// Per-method RPC statistics. The key is the RpcType in string form; e.g. @@ -682,7 +670,7 @@ struct Grpc_Testing_LoadBalancerAccumulatedStatsResponse { var unknownFields = SwiftProtobuf.UnknownStorage() - struct MethodStats { + struct MethodStats: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -703,7 +691,7 @@ struct Grpc_Testing_LoadBalancerAccumulatedStatsResponse { } /// Configurations for a test client. -struct Grpc_Testing_ClientConfigureRequest { +struct Grpc_Testing_ClientConfigureRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -721,7 +709,7 @@ struct Grpc_Testing_ClientConfigureRequest { var unknownFields = SwiftProtobuf.UnknownStorage() /// Type of RPCs to send. - enum RpcType: SwiftProtobuf.Enum { + enum RpcType: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case emptyCall // = 0 case unaryCall // = 1 @@ -747,10 +735,16 @@ struct Grpc_Testing_ClientConfigureRequest { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_ClientConfigureRequest.RpcType] = [ + .emptyCall, + .unaryCall, + ] + } /// Metadata to be attached for the given type of RPCs. - struct Metadata { + struct Metadata: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -769,20 +763,8 @@ struct Grpc_Testing_ClientConfigureRequest { init() {} } -#if swift(>=4.2) - -extension Grpc_Testing_ClientConfigureRequest.RpcType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_ClientConfigureRequest.RpcType] = [ - .emptyCall, - .unaryCall, - ] -} - -#endif // swift(>=4.2) - /// Response for updating a test client's configuration. -struct Grpc_Testing_ClientConfigureResponse { +struct Grpc_Testing_ClientConfigureResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -792,7 +774,7 @@ struct Grpc_Testing_ClientConfigureResponse { init() {} } -struct Grpc_Testing_MemorySize { +struct Grpc_Testing_MemorySize: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -807,7 +789,7 @@ struct Grpc_Testing_MemorySize { /// Metrics data the server will update and send to the client. It mirrors orca load report /// https://github.com/cncf/xds/blob/eded343319d09f30032952beda9840bbd3dcf7ac/xds/data/orca/v3/orca_load_report.proto#L15, /// but avoids orca dependency. Used by both per-query and out-of-band reporting tests. -struct Grpc_Testing_TestOrcaReport { +struct Grpc_Testing_TestOrcaReport: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -826,7 +808,7 @@ struct Grpc_Testing_TestOrcaReport { } /// Status that will be return to callers of the Hook method -struct Grpc_Testing_SetReturnStatusRequest { +struct Grpc_Testing_SetReturnStatusRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -840,7 +822,7 @@ struct Grpc_Testing_SetReturnStatusRequest { init() {} } -struct Grpc_Testing_HookRequest { +struct Grpc_Testing_HookRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -856,7 +838,7 @@ struct Grpc_Testing_HookRequest { var unknownFields = SwiftProtobuf.UnknownStorage() - enum HookRequestCommand: SwiftProtobuf.Enum { + enum HookRequestCommand: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int /// Default value @@ -896,26 +878,20 @@ struct Grpc_Testing_HookRequest { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Testing_HookRequest.HookRequestCommand] = [ + .unspecified, + .start, + .stop, + .return, + ] + } init() {} } -#if swift(>=4.2) - -extension Grpc_Testing_HookRequest.HookRequestCommand: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_HookRequest.HookRequestCommand] = [ - .unspecified, - .start, - .stop, - .return, - ] -} - -#endif // swift(>=4.2) - -struct Grpc_Testing_HookResponse { +struct Grpc_Testing_HookResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -925,43 +901,6 @@ struct Grpc_Testing_HookResponse { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_PayloadType: @unchecked Sendable {} -extension Grpc_Testing_GrpclbRouteType: @unchecked Sendable {} -extension Grpc_Testing_BoolValue: @unchecked Sendable {} -extension Grpc_Testing_Payload: @unchecked Sendable {} -extension Grpc_Testing_EchoStatus: @unchecked Sendable {} -extension Grpc_Testing_SimpleRequest: @unchecked Sendable {} -extension Grpc_Testing_SimpleResponse: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ResponseParameters: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ReconnectParams: @unchecked Sendable {} -extension Grpc_Testing_ReconnectInfo: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsRequest: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse.MetadataType: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerAccumulatedStatsRequest: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse: @unchecked Sendable {} -extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats: @unchecked Sendable {} -extension Grpc_Testing_ClientConfigureRequest: @unchecked Sendable {} -extension Grpc_Testing_ClientConfigureRequest.RpcType: @unchecked Sendable {} -extension Grpc_Testing_ClientConfigureRequest.Metadata: @unchecked Sendable {} -extension Grpc_Testing_ClientConfigureResponse: @unchecked Sendable {} -extension Grpc_Testing_MemorySize: @unchecked Sendable {} -extension Grpc_Testing_TestOrcaReport: @unchecked Sendable {} -extension Grpc_Testing_SetReturnStatusRequest: @unchecked Sendable {} -extension Grpc_Testing_HookRequest: @unchecked Sendable {} -extension Grpc_Testing_HookRequest.HookRequestCommand: @unchecked Sendable {} -extension Grpc_Testing_HookResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.testing" @@ -1785,8 +1724,8 @@ extension Grpc_Testing_LoadBalancerAccumulatedStatsRequest: SwiftProtobuf.Messag static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { @@ -1987,8 +1926,8 @@ extension Grpc_Testing_ClientConfigureResponse: SwiftProtobuf.Message, SwiftProt static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { @@ -2058,10 +1997,10 @@ extension Grpc_Testing_TestOrcaReport: SwiftProtobuf.Message, SwiftProtobuf._Mes } func traverse(visitor: inout V) throws { - if self.cpuUtilization != 0 { + if self.cpuUtilization.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.cpuUtilization, fieldNumber: 1) } - if self.memoryUtilization != 0 { + if self.memoryUtilization.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.memoryUtilization, fieldNumber: 2) } if !self.requestCost.isEmpty { @@ -2185,8 +2124,8 @@ extension Grpc_Testing_HookResponse: SwiftProtobuf.Message, SwiftProtobuf._Messa static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { diff --git a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift index 4b04eec01..7f5023fbe 100644 --- a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Grpc_Testing_ByteBufferParams { +struct Grpc_Testing_ByteBufferParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -48,7 +48,7 @@ struct Grpc_Testing_ByteBufferParams { init() {} } -struct Grpc_Testing_SimpleProtoParams { +struct Grpc_Testing_SimpleProtoParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -64,7 +64,7 @@ struct Grpc_Testing_SimpleProtoParams { /// TODO (vpai): Fill this in once the details of complex, representative /// protos are decided -struct Grpc_Testing_ComplexProtoParams { +struct Grpc_Testing_ComplexProtoParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -74,7 +74,7 @@ struct Grpc_Testing_ComplexProtoParams { init() {} } -struct Grpc_Testing_PayloadConfig { +struct Grpc_Testing_PayloadConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -107,46 +107,16 @@ struct Grpc_Testing_PayloadConfig { var unknownFields = SwiftProtobuf.UnknownStorage() - enum OneOf_Payload: Equatable { + enum OneOf_Payload: Equatable, Sendable { case bytebufParams(Grpc_Testing_ByteBufferParams) case simpleParams(Grpc_Testing_SimpleProtoParams) case complexParams(Grpc_Testing_ComplexProtoParams) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Testing_PayloadConfig.OneOf_Payload, rhs: Grpc_Testing_PayloadConfig.OneOf_Payload) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.bytebufParams, .bytebufParams): return { - guard case .bytebufParams(let l) = lhs, case .bytebufParams(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.simpleParams, .simpleParams): return { - guard case .simpleParams(let l) = lhs, case .simpleParams(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.complexParams, .complexParams): return { - guard case .complexParams(let l) = lhs, case .complexParams(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_ByteBufferParams: @unchecked Sendable {} -extension Grpc_Testing_SimpleProtoParams: @unchecked Sendable {} -extension Grpc_Testing_ComplexProtoParams: @unchecked Sendable {} -extension Grpc_Testing_PayloadConfig: @unchecked Sendable {} -extension Grpc_Testing_PayloadConfig.OneOf_Payload: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.testing" @@ -232,8 +202,8 @@ extension Grpc_Testing_ComplexProtoParams: SwiftProtobuf.Message, SwiftProtobuf. static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { diff --git a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift index 7e61ab4fa..79f3c2664 100644 --- a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Grpc_Testing_ServerStats { +struct Grpc_Testing_ServerStats: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -76,7 +76,7 @@ struct Grpc_Testing_ServerStats { } /// Histogram params based on grpc/support/histogram.c -struct Grpc_Testing_HistogramParams { +struct Grpc_Testing_HistogramParams: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -93,7 +93,7 @@ struct Grpc_Testing_HistogramParams { } /// Histogram data based on grpc/support/histogram.c -struct Grpc_Testing_HistogramData { +struct Grpc_Testing_HistogramData: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -115,7 +115,7 @@ struct Grpc_Testing_HistogramData { init() {} } -struct Grpc_Testing_RequestResultCount { +struct Grpc_Testing_RequestResultCount: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -129,7 +129,7 @@ struct Grpc_Testing_RequestResultCount { init() {} } -struct Grpc_Testing_ClientStats { +struct Grpc_Testing_ClientStats: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -175,14 +175,6 @@ struct Grpc_Testing_ClientStats { fileprivate var _coreStats: Grpc_Core_Stats? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_ServerStats: @unchecked Sendable {} -extension Grpc_Testing_HistogramParams: @unchecked Sendable {} -extension Grpc_Testing_HistogramData: @unchecked Sendable {} -extension Grpc_Testing_RequestResultCount: @unchecked Sendable {} -extension Grpc_Testing_ClientStats: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.testing" @@ -222,13 +214,13 @@ extension Grpc_Testing_ServerStats: SwiftProtobuf.Message, SwiftProtobuf._Messag // allocates stack space for every if/case branch local when no optimizations // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and // https://github.com/apple/swift-protobuf/issues/1182 - if self.timeElapsed != 0 { + if self.timeElapsed.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 1) } - if self.timeUser != 0 { + if self.timeUser.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 2) } - if self.timeSystem != 0 { + if self.timeSystem.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 3) } if self.totalCpuTime != 0 { @@ -280,10 +272,10 @@ extension Grpc_Testing_HistogramParams: SwiftProtobuf.Message, SwiftProtobuf._Me } func traverse(visitor: inout V) throws { - if self.resolution != 0 { + if self.resolution.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.resolution, fieldNumber: 1) } - if self.maxPossible != 0 { + if self.maxPossible.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.maxPossible, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) @@ -329,19 +321,19 @@ extension Grpc_Testing_HistogramData: SwiftProtobuf.Message, SwiftProtobuf._Mess if !self.bucket.isEmpty { try visitor.visitPackedUInt32Field(value: self.bucket, fieldNumber: 1) } - if self.minSeen != 0 { + if self.minSeen.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.minSeen, fieldNumber: 2) } - if self.maxSeen != 0 { + if self.maxSeen.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.maxSeen, fieldNumber: 3) } - if self.sum != 0 { + if self.sum.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.sum, fieldNumber: 4) } - if self.sumOfSquares != 0 { + if self.sumOfSquares.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.sumOfSquares, fieldNumber: 5) } - if self.count != 0 { + if self.count.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.count, fieldNumber: 6) } try unknownFields.traverse(visitor: &visitor) @@ -435,13 +427,13 @@ extension Grpc_Testing_ClientStats: SwiftProtobuf.Message, SwiftProtobuf._Messag try { if let v = self._latencies { try visitor.visitSingularMessageField(value: v, fieldNumber: 1) } }() - if self.timeElapsed != 0 { + if self.timeElapsed.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 2) } - if self.timeUser != 0 { + if self.timeUser.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 3) } - if self.timeSystem != 0 { + if self.timeSystem.bitPattern != 0 { try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 4) } if !self.requestResults.isEmpty { diff --git a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift index cb135bfe0..4810fb7f7 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift @@ -41,7 +41,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// the most specific error code that applies. For example, prefer /// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. /// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. -enum Google_Rpc_Code: SwiftProtobuf.Enum { +enum Google_Rpc_Code: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int /// Not an error; returned on success. @@ -249,11 +249,6 @@ enum Google_Rpc_Code: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension Google_Rpc_Code: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [Google_Rpc_Code] = [ .ok, @@ -274,13 +269,8 @@ extension Google_Rpc_Code: CaseIterable { .unavailable, .dataLoss, ] -} - -#endif // swift(>=4.2) -#if swift(>=5.5) && canImport(_Concurrency) -extension Google_Rpc_Code: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) +} // MARK: - Code below here is support for the SwiftProtobuf runtime. diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift index 8300d1877..cb3f67f7e 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Grpc_Lookup_V1_RouteLookupRequest { +struct Grpc_Lookup_V1_RouteLookupRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -58,7 +58,7 @@ struct Grpc_Lookup_V1_RouteLookupRequest { var unknownFields = SwiftProtobuf.UnknownStorage() /// Possible reasons for making a request. - enum Reason: SwiftProtobuf.Enum { + enum Reason: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int /// Unused @@ -93,25 +93,19 @@ struct Grpc_Lookup_V1_RouteLookupRequest { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_Lookup_V1_RouteLookupRequest.Reason] = [ + .unknown, + .miss, + .stale, + ] + } init() {} } -#if swift(>=4.2) - -extension Grpc_Lookup_V1_RouteLookupRequest.Reason: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Lookup_V1_RouteLookupRequest.Reason] = [ - .unknown, - .miss, - .stale, - ] -} - -#endif // swift(>=4.2) - -struct Grpc_Lookup_V1_RouteLookupResponse { +struct Grpc_Lookup_V1_RouteLookupResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -134,12 +128,6 @@ struct Grpc_Lookup_V1_RouteLookupResponse { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Lookup_V1_RouteLookupRequest: @unchecked Sendable {} -extension Grpc_Lookup_V1_RouteLookupRequest.Reason: @unchecked Sendable {} -extension Grpc_Lookup_V1_RouteLookupResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.lookup.v1" diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift index 2d1e22104..1be022821 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift @@ -38,7 +38,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// name). The name must match one of the names listed in the "name" field. If /// the "required_match" field is true, one of the specified names must be /// present for the keybuilder to match. -struct Grpc_Lookup_V1_NameMatcher { +struct Grpc_Lookup_V1_NameMatcher: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -63,7 +63,7 @@ struct Grpc_Lookup_V1_NameMatcher { } /// A GrpcKeyBuilder applies to a given gRPC service, name, and headers. -struct Grpc_Lookup_V1_GrpcKeyBuilder { +struct Grpc_Lookup_V1_GrpcKeyBuilder: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -96,7 +96,7 @@ struct Grpc_Lookup_V1_GrpcKeyBuilder { /// fields are specified as fixed strings. The service name is required and /// includes the proto package name. The method name may be omitted, in /// which case any method on the given service is matched. - struct Name { + struct Name: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -116,7 +116,7 @@ struct Grpc_Lookup_V1_GrpcKeyBuilder { /// If this submessage is specified, the normal host/path fields will be left /// unset in the RouteLookupRequest. We are deprecating host/path in the /// RouteLookupRequest, so services should migrate to the ExtraKeys approach. - struct ExtraKeys { + struct ExtraKeys: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -153,7 +153,7 @@ struct Grpc_Lookup_V1_GrpcKeyBuilder { /// the id and the second segment as the object. If the host has a subdomain, the /// subdomain will be used as the id and the first segment as the object. If /// neither pattern matches, no keys will be extracted. -struct Grpc_Lookup_V1_HttpKeyBuilder { +struct Grpc_Lookup_V1_HttpKeyBuilder: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -226,7 +226,7 @@ struct Grpc_Lookup_V1_HttpKeyBuilder { init() {} } -struct Grpc_Lookup_V1_RouteLookupConfig { +struct Grpc_Lookup_V1_RouteLookupConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -314,7 +314,7 @@ struct Grpc_Lookup_V1_RouteLookupConfig { /// RouteLookupClusterSpecifier is used in xDS to represent a cluster specifier /// plugin for RLS. -struct Grpc_Lookup_V1_RouteLookupClusterSpecifier { +struct Grpc_Lookup_V1_RouteLookupClusterSpecifier: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -336,16 +336,6 @@ struct Grpc_Lookup_V1_RouteLookupClusterSpecifier { fileprivate var _routeLookupConfig: Grpc_Lookup_V1_RouteLookupConfig? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Lookup_V1_NameMatcher: @unchecked Sendable {} -extension Grpc_Lookup_V1_GrpcKeyBuilder: @unchecked Sendable {} -extension Grpc_Lookup_V1_GrpcKeyBuilder.Name: @unchecked Sendable {} -extension Grpc_Lookup_V1_GrpcKeyBuilder.ExtraKeys: @unchecked Sendable {} -extension Grpc_Lookup_V1_HttpKeyBuilder: @unchecked Sendable {} -extension Grpc_Lookup_V1_RouteLookupConfig: @unchecked Sendable {} -extension Grpc_Lookup_V1_RouteLookupClusterSpecifier: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.lookup.v1" diff --git a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift index da501cada..532eeeed1 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift @@ -49,7 +49,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// Configuration for a method. -struct Grpc_ServiceConfig_MethodConfig { +struct Grpc_ServiceConfig_MethodConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -165,28 +165,10 @@ struct Grpc_ServiceConfig_MethodConfig { /// Only one of retry_policy or hedging_policy may be set. If neither is set, /// RPCs will not be retried or hedged. - enum OneOf_RetryOrHedgingPolicy: Equatable { + enum OneOf_RetryOrHedgingPolicy: Equatable, Sendable { case retryPolicy(Grpc_ServiceConfig_MethodConfig.RetryPolicy) case hedgingPolicy(Grpc_ServiceConfig_MethodConfig.HedgingPolicy) - #if !swift(>=4.1) - static func ==(lhs: Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy, rhs: Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.retryPolicy, .retryPolicy): return { - guard case .retryPolicy(let l) = lhs, case .retryPolicy(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.hedgingPolicy, .hedgingPolicy): return { - guard case .hedgingPolicy(let l) = lhs, case .hedgingPolicy(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } /// The names of the methods to which this configuration applies. @@ -216,7 +198,7 @@ struct Grpc_ServiceConfig_MethodConfig { /// - { "service": "s" } /// - { "service": "s", "method": null } /// - { "service": "s", "method": "" } - struct Name { + struct Name: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -232,7 +214,7 @@ struct Grpc_ServiceConfig_MethodConfig { } /// The retry policy for outgoing RPCs. - struct RetryPolicy { + struct RetryPolicy: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -286,7 +268,7 @@ struct Grpc_ServiceConfig_MethodConfig { /// The hedging policy for outgoing RPCs. Hedged RPCs may execute more than /// once on the server, so only idempotent methods should specify a hedging /// policy. - struct HedgingPolicy { + struct HedgingPolicy: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -335,7 +317,7 @@ struct Grpc_ServiceConfig_MethodConfig { } /// Configuration for pick_first LB policy. -struct Grpc_ServiceConfig_PickFirstConfig { +struct Grpc_ServiceConfig_PickFirstConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -351,7 +333,7 @@ struct Grpc_ServiceConfig_PickFirstConfig { } /// Configuration for round_robin LB policy. -struct Grpc_ServiceConfig_RoundRobinConfig { +struct Grpc_ServiceConfig_RoundRobinConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -362,7 +344,7 @@ struct Grpc_ServiceConfig_RoundRobinConfig { } /// Configuration for weighted_round_robin LB policy. -struct Grpc_ServiceConfig_WeightedRoundRobinLbConfig { +struct Grpc_ServiceConfig_WeightedRoundRobinLbConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -455,7 +437,7 @@ struct Grpc_ServiceConfig_WeightedRoundRobinLbConfig { } /// Configuration for outlier_detection LB policy -struct Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { +struct Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -534,7 +516,7 @@ struct Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { /// Parameters for the success rate ejection algorithm. /// This algorithm monitors the request success rate for all endpoints and /// ejects individual endpoints whose success rates are statistical outliers. - struct SuccessRateEjection { + struct SuccessRateEjection: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -607,7 +589,7 @@ struct Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { /// Parameters for the failure percentage algorithm. /// This algorithm ejects individual endpoints whose failure rate is greater than /// some threshold, independently of any other endpoint. - struct FailurePercentageEjection { + struct FailurePercentageEjection: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -682,7 +664,7 @@ struct Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig { } /// Configuration for grpclb LB policy. -struct Grpc_ServiceConfig_GrpcLbConfig { +struct Grpc_ServiceConfig_GrpcLbConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -718,7 +700,7 @@ struct Grpc_ServiceConfig_GrpcLbConfig { } /// Configuration for priority LB policy. -struct Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -735,7 +717,7 @@ struct Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig { /// The names are used to allow the priority policy to update /// existing child policies instead of creating new ones every /// time it receives a config update. - struct Child { + struct Child: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -754,7 +736,7 @@ struct Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig { } /// Configuration for weighted_target LB policy. -struct Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -763,7 +745,7 @@ struct Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig { var unknownFields = SwiftProtobuf.UnknownStorage() - struct Target { + struct Target: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -781,7 +763,7 @@ struct Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig { } /// Config for RLS LB policy. -struct Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -824,7 +806,7 @@ struct Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig { } /// Configuration for xds_cluster_manager_experimental LB policy. -struct Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -833,7 +815,7 @@ struct Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig { var unknownFields = SwiftProtobuf.UnknownStorage() - struct Child { + struct Child: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -849,7 +831,7 @@ struct Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig { } /// Configuration for the cds LB policy. -struct Grpc_ServiceConfig_CdsConfig { +struct Grpc_ServiceConfig_CdsConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -866,7 +848,7 @@ struct Grpc_ServiceConfig_CdsConfig { } /// Configuration for xds_cluster_impl LB policy. -struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -880,6 +862,8 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { /// EDS service name. /// Not set if cluster is not an EDS cluster or if it does not /// specify an EDS service name. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var edsServiceName: String = String() /// Server to send load reports to. @@ -887,6 +871,8 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { /// If set to empty string, load reporting will be sent to the same /// server as we are getting xds data from. /// DEPRECATED: Use new lrs_load_reporting_server field instead. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { get {return _lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} set {_lrsLoadReportingServerName = newValue} @@ -899,6 +885,8 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { /// LRS server to send load reports to. /// If not present, load reporting will be disabled. /// Supercedes lrs_load_reporting_server_name field. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var lrsLoadReportingServer: Grpc_ServiceConfig_XdsServer { get {return _lrsLoadReportingServer ?? Grpc_ServiceConfig_XdsServer()} set {_lrsLoadReportingServer = newValue} @@ -910,6 +898,8 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { /// Maximum number of outstanding requests can be made to the upstream cluster. /// Default is 1024. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var maxConcurrentRequests: SwiftProtobuf.Google_Protobuf_UInt32Value { get {return _maxConcurrentRequests ?? SwiftProtobuf.Google_Protobuf_UInt32Value()} set {_maxConcurrentRequests = newValue} @@ -919,15 +909,18 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { /// Clears the value of `maxConcurrentRequests`. Subsequent reads from it will return its default value. mutating func clearMaxConcurrentRequests() {self._maxConcurrentRequests = nil} + /// NOTE: This field was marked as deprecated in the .proto file. var dropCategories: [Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory] = [] /// Telemetry labels associated with this cluster + /// + /// NOTE: This field was marked as deprecated in the .proto file. var telemetryLabels: Dictionary = [:] var unknownFields = SwiftProtobuf.UnknownStorage() /// Drop configuration. - struct DropCategory { + struct DropCategory: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -949,7 +942,7 @@ struct Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig { } /// Configuration for ring_hash LB policy. -struct Grpc_ServiceConfig_RingHashLoadBalancingConfig { +struct Grpc_ServiceConfig_RingHashLoadBalancingConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -968,7 +961,7 @@ struct Grpc_ServiceConfig_RingHashLoadBalancingConfig { } /// Configuration for the xds_wrr_locality load balancing policy. -struct Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -981,7 +974,7 @@ struct Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig { } /// Configuration for the least_request LB policy. -struct Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -994,7 +987,7 @@ struct Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig { } /// Configuration for the xds_override_host LB policy. -struct Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1006,11 +999,13 @@ struct Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig { /// valid health status for hosts that are considered when using /// xds_override_host_experimental policy. /// Default is [UNKNOWN, HEALTHY] + /// + /// NOTE: This field was marked as deprecated in the .proto file. var overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] var unknownFields = SwiftProtobuf.UnknownStorage() - enum HealthStatus: SwiftProtobuf.Enum { + enum HealthStatus: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unknown // = 0 case healthy // = 1 @@ -1039,24 +1034,18 @@ struct Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [ + .unknown, + .healthy, + .draining, + ] + } init() {} } -#if swift(>=4.2) - -extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [ - .unknown, - .healthy, - .draining, - ] -} - -#endif // swift(>=4.2) - /// Selects LB policy and provides corresponding configuration. /// /// In general, all instances of this field should be repeated. Clients will @@ -1068,7 +1057,7 @@ extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: /// config is invalid. /// - If the list doesn't contain any supported policy, the whole service config /// is invalid. -struct Grpc_ServiceConfig_LoadBalancingConfig { +struct Grpc_ServiceConfig_LoadBalancingConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1201,6 +1190,8 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { } /// Deprecated xDS-related policies. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var xdsClusterResolverExperimental: Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { get { if case .xdsClusterResolverExperimental(let v)? = policy {return v} @@ -1209,6 +1200,7 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { set {policy = .xdsClusterResolverExperimental(newValue)} } + /// NOTE: This field was marked as deprecated in the .proto file. var lrsExperimental: Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { get { if case .lrsExperimental(let v)? = policy {return v} @@ -1217,6 +1209,7 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { set {policy = .lrsExperimental(newValue)} } + /// NOTE: This field was marked as deprecated in the .proto file. var edsExperimental: Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig { get { if case .edsExperimental(let v)? = policy {return v} @@ -1225,6 +1218,7 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { set {policy = .edsExperimental(newValue)} } + /// NOTE: This field was marked as deprecated in the .proto file. var xds: Grpc_ServiceConfig_XdsConfig { get { if case .xds(let v)? = policy {return v} @@ -1233,6 +1227,7 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { set {policy = .xds(newValue)} } + /// NOTE: This field was marked as deprecated in the .proto file. var xdsExperimental: Grpc_ServiceConfig_XdsConfig { get { if case .xdsExperimental(let v)? = policy {return v} @@ -1244,7 +1239,7 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { var unknownFields = SwiftProtobuf.UnknownStorage() /// Exactly one LB policy may be configured. - enum OneOf_Policy: Equatable { + enum OneOf_Policy: Equatable, Sendable { case pickFirst(Grpc_ServiceConfig_PickFirstConfig) case roundRobin(Grpc_ServiceConfig_RoundRobinConfig) case weightedRoundRobin(Grpc_ServiceConfig_WeightedRoundRobinLbConfig) @@ -1265,102 +1260,18 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { case ringHashExperimental(Grpc_ServiceConfig_RingHashLoadBalancingConfig) case leastRequestExperimental(Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig) /// Deprecated xDS-related policies. + /// + /// NOTE: This field was marked as deprecated in the .proto file. case xdsClusterResolverExperimental(Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig) + /// NOTE: This field was marked as deprecated in the .proto file. case lrsExperimental(Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig) + /// NOTE: This field was marked as deprecated in the .proto file. case edsExperimental(Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig) + /// NOTE: This field was marked as deprecated in the .proto file. case xds(Grpc_ServiceConfig_XdsConfig) + /// NOTE: This field was marked as deprecated in the .proto file. case xdsExperimental(Grpc_ServiceConfig_XdsConfig) - #if !swift(>=4.1) - static func ==(lhs: Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy, rhs: Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.pickFirst, .pickFirst): return { - guard case .pickFirst(let l) = lhs, case .pickFirst(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.roundRobin, .roundRobin): return { - guard case .roundRobin(let l) = lhs, case .roundRobin(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.weightedRoundRobin, .weightedRoundRobin): return { - guard case .weightedRoundRobin(let l) = lhs, case .weightedRoundRobin(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.grpclb, .grpclb): return { - guard case .grpclb(let l) = lhs, case .grpclb(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.priorityExperimental, .priorityExperimental): return { - guard case .priorityExperimental(let l) = lhs, case .priorityExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.weightedTargetExperimental, .weightedTargetExperimental): return { - guard case .weightedTargetExperimental(let l) = lhs, case .weightedTargetExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.outlierDetection, .outlierDetection): return { - guard case .outlierDetection(let l) = lhs, case .outlierDetection(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.rls, .rls): return { - guard case .rls(let l) = lhs, case .rls(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xdsClusterManagerExperimental, .xdsClusterManagerExperimental): return { - guard case .xdsClusterManagerExperimental(let l) = lhs, case .xdsClusterManagerExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.cdsExperimental, .cdsExperimental): return { - guard case .cdsExperimental(let l) = lhs, case .cdsExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xdsClusterImplExperimental, .xdsClusterImplExperimental): return { - guard case .xdsClusterImplExperimental(let l) = lhs, case .xdsClusterImplExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.overrideHostExperimental, .overrideHostExperimental): return { - guard case .overrideHostExperimental(let l) = lhs, case .overrideHostExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xdsWrrLocalityExperimental, .xdsWrrLocalityExperimental): return { - guard case .xdsWrrLocalityExperimental(let l) = lhs, case .xdsWrrLocalityExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.ringHashExperimental, .ringHashExperimental): return { - guard case .ringHashExperimental(let l) = lhs, case .ringHashExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.leastRequestExperimental, .leastRequestExperimental): return { - guard case .leastRequestExperimental(let l) = lhs, case .leastRequestExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xdsClusterResolverExperimental, .xdsClusterResolverExperimental): return { - guard case .xdsClusterResolverExperimental(let l) = lhs, case .xdsClusterResolverExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.lrsExperimental, .lrsExperimental): return { - guard case .lrsExperimental(let l) = lhs, case .lrsExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.edsExperimental, .edsExperimental): return { - guard case .edsExperimental(let l) = lhs, case .edsExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xds, .xds): return { - guard case .xds(let l) = lhs, case .xds(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.xdsExperimental, .xdsExperimental): return { - guard case .xdsExperimental(let l) = lhs, case .xdsExperimental(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} @@ -1368,11 +1279,12 @@ struct Grpc_ServiceConfig_LoadBalancingConfig { /// A ServiceConfig represents information about a service but is not specific to /// any name resolver. -struct Grpc_ServiceConfig_ServiceConfig { +struct Grpc_ServiceConfig_ServiceConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. + /// NOTE: This field was marked as deprecated in the .proto file. var loadBalancingPolicy: Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy = .unspecified /// Multiple LB policies can be specified; clients will iterate through @@ -1421,7 +1333,7 @@ struct Grpc_ServiceConfig_ServiceConfig { /// returns at least one backend address in addition to the balancer /// address(es), the client may fall back to the requested policy if it /// is unable to reach any of the grpclb load balancers. - enum LoadBalancingPolicy: SwiftProtobuf.Enum { + enum LoadBalancingPolicy: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unspecified // = 0 case roundRobin // = 1 @@ -1447,6 +1359,12 @@ struct Grpc_ServiceConfig_ServiceConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy] = [ + .unspecified, + .roundRobin, + ] + } /// If a RetryThrottlingPolicy is provided, gRPC will automatically throttle @@ -1462,7 +1380,7 @@ struct Grpc_ServiceConfig_ServiceConfig { /// /// If token_count is less than or equal to max_tokens / 2, then RPCs will not /// be retried and hedged RPCs will not be sent. - struct RetryThrottlingPolicy { + struct RetryThrottlingPolicy: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1485,7 +1403,7 @@ struct Grpc_ServiceConfig_ServiceConfig { init() {} } - struct HealthCheckConfig { + struct HealthCheckConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1513,21 +1431,9 @@ struct Grpc_ServiceConfig_ServiceConfig { fileprivate var _healthCheckConfig: Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig? = nil } -#if swift(>=4.2) - -extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy] = [ - .unspecified, - .roundRobin, - ] -} - -#endif // swift(>=4.2) - /// Represents an xDS server. /// Deprecated. -struct Grpc_ServiceConfig_XdsServer { +struct Grpc_ServiceConfig_XdsServer: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1543,7 +1449,7 @@ struct Grpc_ServiceConfig_XdsServer { var unknownFields = SwiftProtobuf.UnknownStorage() - struct ChannelCredentials { + struct ChannelCredentials: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1573,7 +1479,7 @@ struct Grpc_ServiceConfig_XdsServer { /// Configuration for xds_cluster_resolver LB policy. /// Deprecated. -struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1595,7 +1501,7 @@ struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { /// CDS policy. /// For aggregate clusters, there will be one DiscoveryMechanism for each /// underlying cluster. - struct DiscoveryMechanism { + struct DiscoveryMechanism: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1611,6 +1517,8 @@ struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { /// If set to the empty string, load reporting will be sent to the same /// server that we obtained CDS data from. /// DEPRECATED: Use new lrs_load_reporting_server field instead. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue { get {return _storage._lrsLoadReportingServerName ?? SwiftProtobuf.Google_Protobuf_StringValue()} set {_uniqueStorage()._lrsLoadReportingServerName = newValue} @@ -1688,7 +1596,7 @@ struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { var unknownFields = SwiftProtobuf.UnknownStorage() - enum TypeEnum: SwiftProtobuf.Enum { + enum TypeEnum: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case unknown // = 0 case eds // = 1 @@ -1717,6 +1625,13 @@ struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { } } + // The compiler won't synthesize support with the UNRECOGNIZED case. + static let allCases: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum] = [ + .unknown, + .eds, + .logicalDns, + ] + } init() {} @@ -1727,22 +1642,9 @@ struct Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig { init() {} } -#if swift(>=4.2) - -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum] = [ - .unknown, - .eds, - .logicalDns, - ] -} - -#endif // swift(>=4.2) - /// Configuration for lrs LB policy. /// Deprecated. -struct Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1774,7 +1676,7 @@ struct Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { var unknownFields = SwiftProtobuf.UnknownStorage() /// The locality for which this policy will report load. Required. - struct Locality { + struct Locality: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1797,7 +1699,7 @@ struct Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig { /// Configuration for eds LB policy. /// Deprecated. -struct Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig { +struct Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -1845,12 +1747,14 @@ struct Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig { /// Configuration for xds LB policy. /// Deprecated. -struct Grpc_ServiceConfig_XdsConfig { +struct Grpc_ServiceConfig_XdsConfig: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// Name of balancer to connect to. + /// + /// NOTE: This field was marked as deprecated in the .proto file. var balancerName: String = String() /// Optional. What LB policy to use for intra-locality routing. @@ -1889,51 +1793,6 @@ struct Grpc_ServiceConfig_XdsConfig { fileprivate var _lrsLoadReportingServerName: SwiftProtobuf.Google_Protobuf_StringValue? = nil } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_ServiceConfig_MethodConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.OneOf_RetryOrHedgingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.Name: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_MethodConfig.HedgingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_PickFirstConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_RoundRobinConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_WeightedRoundRobinLbConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.SuccessRateEjection: @unchecked Sendable {} -extension Grpc_ServiceConfig_OutlierDetectionLoadBalancingConfig.FailurePercentageEjection: @unchecked Sendable {} -extension Grpc_ServiceConfig_GrpcLbConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_PriorityLoadBalancingPolicyConfig.Child: @unchecked Sendable {} -extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_WeightedTargetLoadBalancingPolicyConfig.Target: @unchecked Sendable {} -extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterManagerLoadBalancingPolicyConfig.Child: @unchecked Sendable {} -extension Grpc_ServiceConfig_CdsConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterImplLoadBalancingPolicyConfig.DropCategory: @unchecked Sendable {} -extension Grpc_ServiceConfig_RingHashLoadBalancingConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsWrrLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LeastRequestLocalityLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus: @unchecked Sendable {} -extension Grpc_ServiceConfig_LoadBalancingConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LoadBalancingConfig.OneOf_Policy: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig.LoadBalancingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: @unchecked Sendable {} -extension Grpc_ServiceConfig_ServiceConfig.HealthCheckConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsServer: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsServer.ChannelCredentials: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.DiscoveryMechanism.TypeEnum: @unchecked Sendable {} -extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_LrsLoadBalancingPolicyConfig.Locality: @unchecked Sendable {} -extension Grpc_ServiceConfig_EdsLoadBalancingPolicyConfig: @unchecked Sendable {} -extension Grpc_ServiceConfig_XdsConfig: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.service_config" @@ -2116,7 +1975,7 @@ extension Grpc_ServiceConfig_MethodConfig.RetryPolicy: SwiftProtobuf.Message, Sw try { if let v = self._maxBackoff { try visitor.visitSingularMessageField(value: v, fieldNumber: 3) } }() - if self.backoffMultiplier != 0 { + if self.backoffMultiplier.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.backoffMultiplier, fieldNumber: 4) } if !self.retryableStatusCodes.isEmpty { @@ -2221,8 +2080,8 @@ extension Grpc_ServiceConfig_RoundRobinConfig: SwiftProtobuf.Message, SwiftProto static let _protobuf_nameMap = SwiftProtobuf._NameMap() mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} } func traverse(visitor: inout V) throws { @@ -3632,7 +3491,7 @@ extension Grpc_ServiceConfig_ServiceConfig.RetryThrottlingPolicy: SwiftProtobuf. if self.maxTokens != 0 { try visitor.visitSingularUInt32Field(value: self.maxTokens, fieldNumber: 1) } - if self.tokenRatio != 0 { + if self.tokenRatio.bitPattern != 0 { try visitor.visitSingularFloatField(value: self.tokenRatio, fieldNumber: 2) } try unknownFields.traverse(visitor: &visitor) diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift index 4a322b666..a9eff91b7 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift @@ -35,7 +35,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -enum StatusCode: SwiftProtobuf.Enum { +enum StatusCode: SwiftProtobuf.Enum, Swift.CaseIterable { typealias RawValue = Int case ok // = 0 case cancelled // = 1 @@ -106,11 +106,6 @@ enum StatusCode: SwiftProtobuf.Enum { } } -} - -#if swift(>=4.2) - -extension StatusCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static let allCases: [StatusCode] = [ .ok, @@ -131,11 +126,10 @@ extension StatusCode: CaseIterable { .dataLoss, .unauthenticated, ] -} -#endif // swift(>=4.2) +} -struct ControlInput { +struct ControlInput: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -208,7 +202,7 @@ struct ControlInput { fileprivate var _status: RPCStatus? = nil } -struct RPCStatus { +struct RPCStatus: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -224,7 +218,7 @@ struct RPCStatus { init() {} } -struct PayloadParameters { +struct PayloadParameters: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -240,7 +234,7 @@ struct PayloadParameters { init() {} } -struct ControlOutput { +struct ControlOutput: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -252,14 +246,6 @@ struct ControlOutput { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension StatusCode: @unchecked Sendable {} -extension ControlInput: @unchecked Sendable {} -extension RPCStatus: @unchecked Sendable {} -extension PayloadParameters: @unchecked Sendable {} -extension ControlOutput: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. extension StatusCode: SwiftProtobuf._ProtoNameProviding { diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 5c34d4af6..12241b53e 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -348,7 +348,7 @@ extension Google_Protobuf_FileDescriptorProto { $0.name = "helloworld.proto" $0.package = "helloworld" $0.dependency = ["same-module.proto", "different-module.proto"] - $0.publicDependency = [1, 2] + $0.publicDependency = [0, 1] $0.messageType = [requestType, responseType] $0.service = [service] $0.sourceCodeInfo = Google_Protobuf_SourceCodeInfo.with { diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift index 0316cfd2e..784909215 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift @@ -34,7 +34,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Normalization_FunctionName { +struct Normalization_FunctionName: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -47,10 +47,6 @@ struct Normalization_FunctionName { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Normalization_FunctionName: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "normalization" diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift index 281330e5f..e5b316a72 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift @@ -42,7 +42,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The message sent by the client when calling ServerReflectionInfo method. -struct Grpc_Reflection_V1_ServerReflectionRequest { +struct Grpc_Reflection_V1_ServerReflectionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -115,7 +115,7 @@ struct Grpc_Reflection_V1_ServerReflectionRequest { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. - enum OneOf_MessageRequest: Equatable { + enum OneOf_MessageRequest: Equatable, Sendable { /// Find a proto file by the file name. case fileByFilename(String) /// Find the proto file that declares the given fully-qualified symbol name. @@ -138,36 +138,6 @@ struct Grpc_Reflection_V1_ServerReflectionRequest { /// checked. case listServices(String) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileByFilename, .fileByFilename): return { - guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingSymbol, .fileContainingSymbol): return { - guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingExtension, .fileContainingExtension): return { - guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { - guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServices, .listServices): return { - guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} @@ -175,7 +145,7 @@ struct Grpc_Reflection_V1_ServerReflectionRequest { /// The type name and extension number sent by the client when requesting /// file_containing_extension. -struct Grpc_Reflection_V1_ExtensionRequest { +struct Grpc_Reflection_V1_ExtensionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -191,7 +161,7 @@ struct Grpc_Reflection_V1_ExtensionRequest { } /// The message sent by the server to answer ServerReflectionInfo method. -struct Grpc_Reflection_V1_ServerReflectionResponse { +struct Grpc_Reflection_V1_ServerReflectionResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -256,7 +226,7 @@ struct Grpc_Reflection_V1_ServerReflectionResponse { /// The server sets one of the following fields according to the message_request /// in the request. - enum OneOf_MessageResponse: Equatable { + enum OneOf_MessageResponse: Equatable, Sendable { /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. /// As the repeated label is not allowed in oneof fields, we use a @@ -271,32 +241,6 @@ struct Grpc_Reflection_V1_ServerReflectionResponse { /// This message is used when an error occurs. case errorResponse(Grpc_Reflection_V1_ErrorResponse) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileDescriptorResponse, .fileDescriptorResponse): return { - guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { - guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServicesResponse, .listServicesResponse): return { - guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorResponse, .errorResponse): return { - guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} @@ -307,7 +251,7 @@ struct Grpc_Reflection_V1_ServerReflectionResponse { /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. -struct Grpc_Reflection_V1_FileDescriptorResponse { +struct Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -324,7 +268,7 @@ struct Grpc_Reflection_V1_FileDescriptorResponse { /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. -struct Grpc_Reflection_V1_ExtensionNumberResponse { +struct Grpc_Reflection_V1_ExtensionNumberResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -341,7 +285,7 @@ struct Grpc_Reflection_V1_ExtensionNumberResponse { } /// A list of ServiceResponse sent by the server answering list_services request. -struct Grpc_Reflection_V1_ListServiceResponse { +struct Grpc_Reflection_V1_ListServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -357,7 +301,7 @@ struct Grpc_Reflection_V1_ListServiceResponse { /// The information of a single service used by ListServiceResponse to answer /// list_services request. -struct Grpc_Reflection_V1_ServiceResponse { +struct Grpc_Reflection_V1_ServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -372,7 +316,7 @@ struct Grpc_Reflection_V1_ServiceResponse { } /// The error code and error message sent by the server when an error occurs. -struct Grpc_Reflection_V1_ErrorResponse { +struct Grpc_Reflection_V1_ErrorResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -387,19 +331,6 @@ struct Grpc_Reflection_V1_ErrorResponse { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Reflection_V1_ServerReflectionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1_ExtensionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServerReflectionResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ExtensionNumberResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ListServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1_ErrorResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.reflection.v1" diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift index 3642c2866..fdf2554d5 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift @@ -39,7 +39,9 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// The message sent by the client when calling ServerReflectionInfo method. -struct Grpc_Reflection_V1alpha_ServerReflectionRequest { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_ServerReflectionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -112,7 +114,7 @@ struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// To use reflection service, the client should set one of the following /// fields in message_request. The server distinguishes requests by their /// defined field and then handles them using corresponding methods. - enum OneOf_MessageRequest: Equatable { + enum OneOf_MessageRequest: Equatable, Sendable { /// Find a proto file by the file name. case fileByFilename(String) /// Find the proto file that declares the given fully-qualified symbol name. @@ -135,36 +137,6 @@ struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// checked. case listServices(String) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileByFilename, .fileByFilename): return { - guard case .fileByFilename(let l) = lhs, case .fileByFilename(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingSymbol, .fileContainingSymbol): return { - guard case .fileContainingSymbol(let l) = lhs, case .fileContainingSymbol(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.fileContainingExtension, .fileContainingExtension): return { - guard case .fileContainingExtension(let l) = lhs, case .fileContainingExtension(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersOfType, .allExtensionNumbersOfType): return { - guard case .allExtensionNumbersOfType(let l) = lhs, case .allExtensionNumbersOfType(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServices, .listServices): return { - guard case .listServices(let l) = lhs, case .listServices(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} @@ -172,7 +144,9 @@ struct Grpc_Reflection_V1alpha_ServerReflectionRequest { /// The type name and extension number sent by the client when requesting /// file_containing_extension. -struct Grpc_Reflection_V1alpha_ExtensionRequest { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_ExtensionRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -188,7 +162,9 @@ struct Grpc_Reflection_V1alpha_ExtensionRequest { } /// The message sent by the server to answer ServerReflectionInfo method. -struct Grpc_Reflection_V1alpha_ServerReflectionResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_ServerReflectionResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -253,7 +229,7 @@ struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// The server set one of the following fields according to the message_request /// in the request. - enum OneOf_MessageResponse: Equatable { + enum OneOf_MessageResponse: Equatable, Sendable { /// This message is used to answer file_by_filename, file_containing_symbol, /// file_containing_extension requests with transitive dependencies. As /// the repeated label is not allowed in oneof fields, we use a @@ -268,32 +244,6 @@ struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// This message is used when an error occurs. case errorResponse(Grpc_Reflection_V1alpha_ErrorResponse) - #if !swift(>=4.1) - static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.fileDescriptorResponse, .fileDescriptorResponse): return { - guard case .fileDescriptorResponse(let l) = lhs, case .fileDescriptorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.allExtensionNumbersResponse, .allExtensionNumbersResponse): return { - guard case .allExtensionNumbersResponse(let l) = lhs, case .allExtensionNumbersResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.listServicesResponse, .listServicesResponse): return { - guard case .listServicesResponse(let l) = lhs, case .listServicesResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.errorResponse, .errorResponse): return { - guard case .errorResponse(let l) = lhs, case .errorResponse(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif } init() {} @@ -304,7 +254,9 @@ struct Grpc_Reflection_V1alpha_ServerReflectionResponse { /// Serialized FileDescriptorProto messages sent by the server answering /// a file_by_filename, file_containing_symbol, or file_containing_extension /// request. -struct Grpc_Reflection_V1alpha_FileDescriptorResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -321,7 +273,9 @@ struct Grpc_Reflection_V1alpha_FileDescriptorResponse { /// A list of extension numbers sent by the server answering /// all_extension_numbers_of_type request. -struct Grpc_Reflection_V1alpha_ExtensionNumberResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_ExtensionNumberResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -338,7 +292,9 @@ struct Grpc_Reflection_V1alpha_ExtensionNumberResponse { } /// A list of ServiceResponse sent by the server answering list_services request. -struct Grpc_Reflection_V1alpha_ListServiceResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_ListServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -354,7 +310,9 @@ struct Grpc_Reflection_V1alpha_ListServiceResponse { /// The information of a single service used by ListServiceResponse to answer /// list_services request. -struct Grpc_Reflection_V1alpha_ServiceResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_ServiceResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -369,7 +327,9 @@ struct Grpc_Reflection_V1alpha_ServiceResponse { } /// The error code and error message sent by the server when an error occurs. -struct Grpc_Reflection_V1alpha_ErrorResponse { +/// +/// NOTE: The whole .proto file that defined this message was marked as deprecated. +struct Grpc_Reflection_V1alpha_ErrorResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. @@ -384,19 +344,6 @@ struct Grpc_Reflection_V1alpha_ErrorResponse { init() {} } -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Reflection_V1alpha_ServerReflectionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ExtensionRequest: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServerReflectionResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ListServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ServiceResponse: @unchecked Sendable {} -extension Grpc_Reflection_V1alpha_ErrorResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "grpc.reflection.v1alpha" From e661dc8ea008b281c08d8fbb1762e1cafe1b9285 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 8 Jul 2024 18:07:49 +0100 Subject: [PATCH 392/580] Reuse inbound request headers storage for server streams (#1971) Motivation: Each stream starts with headers. When the server accepts a stream it will respond with headers and eventually, once the RPC has been completed, respond with trailers. For each RPC on the server, three sets of headers are allocated. However, once the response headers are received we drop any ref to them (as they are copied into metadata), rather then dropping it on the floor we can hold onto the headers and reuse their already allocated storage for both the initial and trailing metadata. Assuming there is enough capacity this saves two allocations per RPC. Modifications: - Hold on to headers in the stream state machine, clearning and re-populating them when necessary for the initial and trailing resposne headers. - Add a '_modifying' state. Result: Fewer allocations --- .../GRPCStreamStateMachine.swift | 218 ++++++++++++++---- 1 file changed, 178 insertions(+), 40 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index b2f083ff0..3ab917dbb 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -66,6 +66,7 @@ private enum GRPCStreamStateMachineState { case clientClosedServerIdle(ClientClosedServerIdleState) case clientClosedServerOpen(ClientClosedServerOpenState) case clientClosedServerClosed(ClientClosedServerClosedState) + case _modifying struct ClientIdleServerIdleState { let maximumPayloadSize: Int @@ -86,13 +87,18 @@ private enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + // Store the headers received from the remote peer, its storage can be reused when sending + // headers back to the remote peer. + var headers: HPACKHeaders + init( previousState: ClientIdleServerIdleState, compressor: Zlib.Compressor?, outboundCompression: CompressionAlgorithm, framer: GRPCMessageFramer, decompressor: Zlib.Decompressor?, - deframer: NIOSingleStepByteToMessageProcessor? + deframer: NIOSingleStepByteToMessageProcessor?, + headers: HPACKHeaders ) { self.maximumPayloadSize = previousState.maximumPayloadSize self.compressor = compressor @@ -101,6 +107,7 @@ private enum GRPCStreamStateMachineState { self.decompressor = decompressor self.deframer = deframer self.inboundMessageBuffer = .init() + self.headers = headers } } @@ -114,6 +121,10 @@ private enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + // Store the headers received from the remote peer, its storage can be reused when sending + // headers back to the remote peer. + var headers: HPACKHeaders + init( previousState: ClientOpenServerIdleState, deframer: NIOSingleStepByteToMessageProcessor, @@ -127,6 +138,7 @@ private enum GRPCStreamStateMachineState { self.decompressor = decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + self.headers = previousState.headers } } @@ -192,6 +204,10 @@ private enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + // Store the headers received from the remote peer, its storage can be reused when sending + // headers back to the remote peer. + var headers: HPACKHeaders + /// This transition should only happen on the client-side. /// It can happen if the request times out before the client outbound can be opened, or if the stream is /// unexpectedly closed for some other reason on the client before it can transition to open. @@ -207,6 +223,7 @@ private enum GRPCStreamStateMachineState { self.decompressor = nil self.inboundMessageBuffer = .init() + self.headers = [:] } /// This transition should only happen on the server-side. @@ -215,7 +232,8 @@ private enum GRPCStreamStateMachineState { /// any more messages from the client anyways, as it's closed. init( previousState: ClientIdleServerIdleState, - compressionAlgorithm: CompressionAlgorithm + compressionAlgorithm: CompressionAlgorithm, + headers: HPACKHeaders ) { self.maximumPayloadSize = previousState.maximumPayloadSize @@ -231,6 +249,7 @@ private enum GRPCStreamStateMachineState { // client: it's closed. self.deframer = nil self.inboundMessageBuffer = .init() + self.headers = headers } init(previousState: ClientOpenServerIdleState) { @@ -241,6 +260,7 @@ private enum GRPCStreamStateMachineState { self.deframer = previousState.deframer self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + self.headers = previousState.headers } } @@ -254,6 +274,10 @@ private enum GRPCStreamStateMachineState { var inboundMessageBuffer: OneOrManyQueue<[UInt8]> + // Store the headers received from the remote peer, its storage can be reused when sending + // headers back to the remote peer. + var headers: HPACKHeaders + init(previousState: ClientOpenServerOpenState) { self.framer = previousState.framer self.compressor = previousState.compressor @@ -261,6 +285,7 @@ private enum GRPCStreamStateMachineState { self.deframer = previousState.deframer self.decompressor = previousState.decompressor self.inboundMessageBuffer = previousState.inboundMessageBuffer + self.headers = previousState.headers } /// This should be called from the server path, as the deframer will already be configured in this scenario. @@ -275,6 +300,7 @@ private enum GRPCStreamStateMachineState { self.decompressor = nil self.inboundMessageBuffer = previousState.inboundMessageBuffer + self.headers = previousState.headers } /// This should only be called from the client path, as the deframer has not yet been set up. @@ -298,6 +324,7 @@ private enum GRPCStreamStateMachineState { self.deframer = NIOSingleStepByteToMessageProcessor(decoder) self.inboundMessageBuffer = previousState.inboundMessageBuffer + self.headers = previousState.headers } } @@ -537,6 +564,8 @@ struct GRPCStreamStateMachine { state.decompressor?.end() case .clientClosedServerClosed(let state): state.compressor?.end() + case ._modifying: + preconditionFailure() } } @@ -632,7 +661,8 @@ extension GRPCStreamStateMachine { outboundCompression: outboundEncoding, framer: GRPCMessageFramer(), decompressor: nil, - deframer: nil + deframer: nil, + headers: [:] ) ) return self.makeClientHeaders( @@ -650,6 +680,8 @@ extension GRPCStreamStateMachine { try self.invalidState( "Client is closed: can't send metadata." ) + case ._modifying: + preconditionFailure() } } @@ -657,19 +689,28 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle: try self.invalidState("Client not yet open.") + case .clientOpenServerIdle(var state): + self.state = ._modifying state.framer.append(message, promise: promise) self.state = .clientOpenServerIdle(state) + case .clientOpenServerOpen(var state): + self.state = ._modifying state.framer.append(message, promise: promise) self.state = .clientOpenServerOpen(state) + case .clientOpenServerClosed: // The server has closed, so it makes no sense to send the rest of the request. () + case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: try self.invalidState( "Client is closed, cannot send a message." ) + + case ._modifying: + preconditionFailure() } } @@ -686,6 +727,8 @@ extension GRPCStreamStateMachine { case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: // Client is already closed - nothing to do. () + case ._modifying: + preconditionFailure() } } @@ -697,18 +740,21 @@ extension GRPCStreamStateMachine { try self.invalidState("Client is not open yet.") case .clientOpenServerIdle(var state): + self.state = ._modifying let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerIdle(state) return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } ?? .awaitMoreMessages case .clientOpenServerOpen(var state): + self.state = ._modifying let request = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } ?? .awaitMoreMessages case .clientClosedServerIdle(var state): + self.state = ._modifying let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerIdle(state) if let request { @@ -718,6 +764,7 @@ extension GRPCStreamStateMachine { } case .clientClosedServerOpen(var state): + self.state = ._modifying let request = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) if let request { @@ -729,6 +776,9 @@ extension GRPCStreamStateMachine { case .clientOpenServerClosed, .clientClosedServerClosed: // No point in sending any more requests if the server is closed. return .noMoreMessages + + case ._modifying: + preconditionFailure() } } @@ -973,6 +1023,8 @@ extension GRPCStreamStateMachine { try self.invalidState( "Server is closed, nothing could have been sent." ) + case ._modifying: + preconditionFailure() } } @@ -993,6 +1045,7 @@ extension GRPCStreamStateMachine { ) case .clientOpenServerOpen(var state): + self.state = ._modifying if endStream { // This is invalid as per the protocol specification, because the server // can only close by sending trailers, not by setting EOS when sending @@ -1016,6 +1069,7 @@ extension GRPCStreamStateMachine { return .readInbound case .clientClosedServerOpen(var state): + self.state = ._modifying if endStream { self.state = .clientClosedServerClosed(.init(previousState: state)) return .endRPCAndForwardErrorStatus( @@ -1042,31 +1096,43 @@ extension GRPCStreamStateMachine { try self.invalidState( "Cannot have received anything from a closed server." ) + case ._modifying: + preconditionFailure() } } private mutating func clientNextInboundMessage() -> OnNextInboundMessage { switch self.state { case .clientOpenServerOpen(var state): + self.state = ._modifying let message = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) return message.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientOpenServerClosed(var state): + self.state = ._modifying let message = state.inboundMessageBuffer.pop() self.state = .clientOpenServerClosed(state) return message.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientClosedServerOpen(var state): + self.state = ._modifying let message = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) return message.map { .receiveMessage($0) } ?? .awaitMoreMessages + case .clientClosedServerClosed(var state): + self.state = ._modifying let message = state.inboundMessageBuffer.pop() self.state = .clientClosedServerClosed(state) return message.map { .receiveMessage($0) } ?? .noMoreMessages + case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: return .awaitMoreMessages + case ._modifying: + preconditionFailure() } } @@ -1103,6 +1169,9 @@ extension GRPCStreamStateMachine { case .clientOpenServerClosed, .clientClosedServerClosed: return .doNothing + + case ._modifying: + preconditionFailure() } } } @@ -1111,14 +1180,16 @@ extension GRPCStreamStateMachine { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine { - private func makeResponseHeaders( + private func formResponseHeaders( + in headers: inout HPACKHeaders, outboundEncoding: CompressionAlgorithm?, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration, customMetadata: Metadata - ) -> HPACKHeaders { + ) { + headers.removeAll(keepingCapacity: true) + // Response headers always contain :status (HTTP Status 200) and content-type. // They may also contain grpc-encoding, grpc-accept-encoding, and custom metadata. - var headers = HPACKHeaders() headers.reserveCapacity(4 + customMetadata.count) headers.add("200", forKey: .status) @@ -1131,8 +1202,6 @@ extension GRPCStreamStateMachine { for metadataPair in customMetadata { headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) } - - return headers } private mutating func serverSend( @@ -1141,8 +1210,16 @@ extension GRPCStreamStateMachine { ) throws -> HPACKHeaders { // Server sends initial metadata switch self.state { - case .clientOpenServerIdle(let state): + case .clientOpenServerIdle(var state): + self.state = ._modifying let outboundEncoding = state.outboundCompression + self.formResponseHeaders( + in: &state.headers, + outboundEncoding: outboundEncoding, + configuration: configuration, + customMetadata: metadata + ) + self.state = .clientOpenServerOpen( .init( previousState: state, @@ -1153,19 +1230,21 @@ extension GRPCStreamStateMachine { decompressor: state.decompressor ) ) - return self.makeResponseHeaders( - outboundEncoding: outboundEncoding, - configuration: configuration, - customMetadata: metadata - ) - case .clientClosedServerIdle(let state): + + return state.headers + + case .clientClosedServerIdle(var state): + self.state = ._modifying let outboundEncoding = state.outboundCompression - self.state = .clientClosedServerOpen(.init(previousState: state)) - return self.makeResponseHeaders( + self.formResponseHeaders( + in: &state.headers, outboundEncoding: outboundEncoding, configuration: configuration, customMetadata: metadata ) + self.state = .clientClosedServerOpen(.init(previousState: state)) + return state.headers + case .clientIdleServerIdle: try self.invalidState( "Client cannot be idle if server is sending initial metadata: it must have opened." @@ -1178,6 +1257,8 @@ extension GRPCStreamStateMachine { try self.invalidState( "Server has already sent initial metadata." ) + case ._modifying: + preconditionFailure() } } @@ -1187,16 +1268,23 @@ extension GRPCStreamStateMachine { try self.invalidState( "Server must have sent initial metadata before sending a message." ) + case .clientOpenServerOpen(var state): + self.state = ._modifying state.framer.append(message, promise: promise) self.state = .clientOpenServerOpen(state) + case .clientClosedServerOpen(var state): + self.state = ._modifying state.framer.append(message, promise: promise) self.state = .clientClosedServerOpen(state) + case .clientOpenServerClosed, .clientClosedServerClosed: try self.invalidState( "Server can't send a message if it's closed." ) + case ._modifying: + preconditionFailure() } } @@ -1206,18 +1294,30 @@ extension GRPCStreamStateMachine { ) throws -> HPACKHeaders { // Close the server. switch self.state { - case .clientOpenServerOpen(let state): + case .clientOpenServerOpen(var state): + self.state = ._modifying + state.headers.formTrailers(status: status, metadata: customMetadata) self.state = .clientOpenServerClosed(.init(previousState: state)) - return .trailers(status: status, metadata: customMetadata) - case .clientClosedServerOpen(let state): + return state.headers + + case .clientClosedServerOpen(var state): + self.state = ._modifying + state.headers.formTrailers(status: status, metadata: customMetadata) self.state = .clientClosedServerClosed(.init(previousState: state)) - return .trailers(status: status, metadata: customMetadata) - case .clientOpenServerIdle(let state): + return state.headers + + case .clientOpenServerIdle(var state): + self.state = ._modifying + state.headers.formTrailersOnly(status: status, metadata: customMetadata) self.state = .clientOpenServerClosed(.init(previousState: state)) - return .trailersOnly(status: status, metadata: customMetadata) - case .clientClosedServerIdle(let state): + return state.headers + + case .clientClosedServerIdle(var state): + self.state = ._modifying + state.headers.formTrailersOnly(status: status, metadata: customMetadata) self.state = .clientClosedServerClosed(.init(previousState: state)) - return .trailersOnly(status: status, metadata: customMetadata) + return state.headers + case .clientIdleServerIdle: try self.invalidState( "Server can't send status if client is idle." @@ -1226,6 +1326,8 @@ extension GRPCStreamStateMachine { try self.invalidState( "Server can't send anything if closed." ) + case ._modifying: + preconditionFailure() } } @@ -1376,7 +1478,8 @@ extension GRPCStreamStateMachine { self.state = .clientClosedServerIdle( .init( previousState: state, - compressionAlgorithm: outboundEncoding + compressionAlgorithm: outboundEncoding, + headers: headers ) ) } else { @@ -1396,7 +1499,8 @@ extension GRPCStreamStateMachine { outboundCompression: outboundEncoding, framer: GRPCMessageFramer(), decompressor: decompressor, - deframer: NIOSingleStepByteToMessageProcessor(deframer) + deframer: NIOSingleStepByteToMessageProcessor(deframer), + headers: headers ) ) } @@ -1409,6 +1513,9 @@ extension GRPCStreamStateMachine { case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: try self.invalidState("Client can't have sent metadata if closed.") + + case ._modifying: + preconditionFailure() } } @@ -1423,6 +1530,7 @@ extension GRPCStreamStateMachine { try self.invalidState("Can't have received a message if client is idle.") case .clientOpenServerIdle(var state): + self.state = ._modifying // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. try state.deframer!.process(buffer: buffer) { deframedMessage in @@ -1438,6 +1546,7 @@ extension GRPCStreamStateMachine { action = .readInbound case .clientOpenServerOpen(var state): + self.state = ._modifying try state.deframer.process(buffer: buffer) { deframedMessage in state.inboundMessageBuffer.append(deframedMessage) } @@ -1462,6 +1571,9 @@ extension GRPCStreamStateMachine { case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: try self.invalidState("Client can't send a message if closed.") + + case ._modifying: + preconditionFailure() } return action @@ -1471,17 +1583,23 @@ extension GRPCStreamStateMachine { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: try self.invalidState("Server is not open yet.") + case .clientOpenServerOpen(var state): + self.state = ._modifying let response = try state.framer.next(compressor: state.compressor) self.state = .clientOpenServerOpen(state) return response.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } ?? .awaitMoreMessages + case .clientClosedServerOpen(var state): + self.state = ._modifying let response = try state.framer.next(compressor: state.compressor) self.state = .clientClosedServerOpen(state) return response.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } ?? .awaitMoreMessages + case .clientOpenServerClosed(var state): + self.state = ._modifying let response = try state.framer?.next(compressor: state.compressor) self.state = .clientOpenServerClosed(state) if let response { @@ -1489,7 +1607,9 @@ extension GRPCStreamStateMachine { } else { return .noMoreMessages } + case .clientClosedServerClosed(var state): + self.state = ._modifying let response = try state.framer?.next(compressor: state.compressor) self.state = .clientClosedServerClosed(state) if let response { @@ -1497,27 +1617,33 @@ extension GRPCStreamStateMachine { } else { return .noMoreMessages } + case ._modifying: + preconditionFailure() } } private mutating func serverNextInboundMessage() -> OnNextInboundMessage { switch self.state { case .clientOpenServerIdle(var state): + self.state = ._modifying let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerIdle(state) return request.map { .receiveMessage($0) } ?? .awaitMoreMessages case .clientOpenServerOpen(var state): + self.state = ._modifying let request = state.inboundMessageBuffer.pop() self.state = .clientOpenServerOpen(state) return request.map { .receiveMessage($0) } ?? .awaitMoreMessages case .clientClosedServerIdle(var state): + self.state = ._modifying let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerIdle(state) return request.map { .receiveMessage($0) } ?? .noMoreMessages case .clientClosedServerOpen(var state): + self.state = ._modifying let request = state.inboundMessageBuffer.pop() self.state = .clientClosedServerOpen(state) return request.map { .receiveMessage($0) } ?? .noMoreMessages @@ -1528,6 +1654,9 @@ extension GRPCStreamStateMachine { case .clientIdleServerIdle: return .awaitMoreMessages + + case ._modifying: + preconditionFailure() } } @@ -1553,6 +1682,9 @@ extension GRPCStreamStateMachine { case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: return .doNothing + + case ._modifying: + preconditionFailure() } } } @@ -1587,35 +1719,43 @@ internal enum GRPCHTTP2Keys: String { } extension HPACKHeaders { - internal func firstString(forKey key: GRPCHTTP2Keys, canonicalForm: Bool = true) -> String? { + func firstString(forKey key: GRPCHTTP2Keys, canonicalForm: Bool = true) -> String? { self.values(forHeader: key.rawValue, canonicalForm: canonicalForm).first(where: { _ in true }) .map { String($0) } } - internal mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { + fileprivate mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { self.add(name: key.rawValue, value: value) } - static func trailersOnly(code: Status.Code, message: String, metadata: Metadata = [:]) -> Self { - return .trailersOnly(status: Status(code: code, message: message), metadata: metadata) + fileprivate static func trailersOnly(code: Status.Code, message: String) -> Self { + var trailers = HPACKHeaders() + HPACKHeaders.formTrailers( + &trailers, + isTrailersOnly: true, + status: Status(code: code, message: message), + metadata: [:] + ) + return trailers } - static func trailersOnly(status: Status, metadata: Metadata = [:]) -> Self { - return .makeTrailers(isTrailersOnly: true, status: status, metadata: metadata) + fileprivate mutating func formTrailersOnly(status: Status, metadata: Metadata = [:]) { + Self.formTrailers(&self, isTrailersOnly: true, status: status, metadata: metadata) } - static func trailers(status: Status, metadata: Metadata = [:]) -> Self { - return .makeTrailers(isTrailersOnly: false, status: status, metadata: metadata) + fileprivate mutating func formTrailers(status: Status, metadata: Metadata = [:]) { + Self.formTrailers(&self, isTrailersOnly: false, status: status, metadata: metadata) } - private static func makeTrailers( + private static func formTrailers( + _ trailers: inout HPACKHeaders, isTrailersOnly: Bool, status: Status, metadata: Metadata - ) -> Self { - var trailers = HPACKHeaders() + ) { + trailers.removeAll(keepingCapacity: true) if isTrailersOnly { trailers.reserveCapacity(4 + metadata.count) @@ -1633,8 +1773,6 @@ extension HPACKHeaders { for (key, value) in metadata { trailers.add(name: key, value: value.encoded()) } - - return trailers } } From 147debff53646078edcfc88f3607127c71fd4684 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 Jul 2024 16:20:44 +0100 Subject: [PATCH 393/580] Avoid throwing from the stream state machine (#1975) Motivation: The stream state machine throws on a number of paths rather than returning an action for the caller to execute. This extra flow control path can lead to the state not being correctly updated (and left in the modifying state). The only valid error the stream state machine may throw is one for being in an invalid state. It's safe to throw this error as the state won't change when an invalid state is reached. This can be encoded using typed throws. Modifications: - Use typed throws in the stream state machine, this required pushing typed throws down into the compressor - A few other parts of the state machine needed modifying to add new actions to avoid throwing Result: The only error the state machine may throw is for an invalid state. --- .../Client/GRPCClientStreamHandler.swift | 46 +-- Sources/GRPCHTTP2Core/Compression/Zlib.swift | 67 ++-- Sources/GRPCHTTP2Core/GRPCMessageFramer.swift | 28 +- .../GRPCStreamStateMachine.swift | 304 +++++++++++------- .../Server/GRPCServerStreamHandler.swift | 42 ++- .../Client/GRPCClientStreamHandlerTests.swift | 31 +- .../GRPCMessageDeframerTests.swift | 18 ++ .../GRPCStreamStateMachineTests.swift | 267 +++++++-------- .../Server/GRPCServerStreamHandlerTests.swift | 17 +- 9 files changed, 463 insertions(+), 357 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index 675bb06a9..073dcc058 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -68,10 +68,13 @@ extension GRPCClientStreamHandler { case .byteBuffer(let buffer): do { switch try self.stateMachine.receive(buffer: buffer, endStream: endStream) { - case .endRPCAndForwardErrorStatus(let status): + case .endRPCAndForwardErrorStatus_clientOnly(let status): context.fireChannelRead(self.wrapInboundOut(.status(status, [:]))) context.close(promise: nil) + case .forwardErrorAndClose_serverOnly: + assertionFailure("Unexpected client action") + case .readInbound: loop: while true { switch self.stateMachine.nextInboundMessage() { @@ -91,7 +94,8 @@ extension GRPCClientStreamHandler { case .doNothing: () } - } catch { + } catch let invalidState { + let error = RPCError(invalidState) context.fireErrorCaught(error) } @@ -109,24 +113,18 @@ extension GRPCClientStreamHandler { case .receivedMetadata(let metadata, _): context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) - case .rejectRPC: - throw RPCError( - code: .internalError, - message: "Client cannot get rejectRPC." - ) - - case .receivedStatusAndMetadata(let status, let metadata): + case .receivedStatusAndMetadata_clientOnly(let status, let metadata): context.fireChannelRead(self.wrapInboundOut(.status(status, metadata))) context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - case .protocolViolation: - // Should only happen for servers - assertionFailure("Unexpected protocol violation") + case .rejectRPC_serverOnly, .protocolViolation_serverOnly: + assertionFailure("Unexpected action '\(action)'") case .doNothing: () } - } catch { + } catch let invalidState { + let error = RPCError(invalidState) context.fireErrorCaught(error) } @@ -187,7 +185,8 @@ extension GRPCClientStreamHandler { self.flushPending = true let headers = try self.stateMachine.send(metadata: metadata) context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: promise) - } catch { + } catch let invalidState { + let error = RPCError(invalidState) promise?.fail(error) context.fireErrorCaught(error) } @@ -195,7 +194,8 @@ extension GRPCClientStreamHandler { case .message(let message): do { try self.stateMachine.send(message: message, promise: promise) - } catch { + } catch let invalidState { + let error = RPCError(invalidState) promise?.fail(error) context.fireErrorCaught(error) } @@ -219,7 +219,8 @@ extension GRPCClientStreamHandler { // (otherwise, we'd skip flushing if we're in a read loop) self._flush(context: context) promise?.succeed() - } catch { + } catch let invalidState { + let error = RPCError(invalidState) promise?.fail(error) context.fireErrorCaught(error) } @@ -233,7 +234,8 @@ extension GRPCClientStreamHandler { // (otherwise, we'd skip flushing if we're in a read loop) self._flush(context: context) context.close(mode: mode, promise: promise) - } catch { + } catch let invalidState { + let error = RPCError(invalidState) promise?.fail(error) context.fireErrorCaught(error) } @@ -285,10 +287,16 @@ extension GRPCClientStreamHandler { context.flush() } break loop + + case .closeAndFailPromise(let promise, let error): + context.close(mode: .all, promise: nil) + promise?.fail(error) + break loop } + } - } catch { - context.fireErrorCaught(error) + } catch let invalidState { + context.fireErrorCaught(RPCError(invalidState)) } } } diff --git a/Sources/GRPCHTTP2Core/Compression/Zlib.swift b/Sources/GRPCHTTP2Core/Compression/Zlib.swift index 7906aa743..da6252275 100644 --- a/Sources/GRPCHTTP2Core/Compression/Zlib.swift +++ b/Sources/GRPCHTTP2Core/Compression/Zlib.swift @@ -60,7 +60,7 @@ extension Zlib { /// - Parameter output: The `ByteBuffer` into which the compressed message should be written. /// - Returns: The number of bytes written into the `output` buffer. @discardableResult - func compress(_ input: [UInt8], into output: inout ByteBuffer) throws -> Int { + func compress(_ input: [UInt8], into output: inout ByteBuffer) throws(ZlibError) -> Int { defer { self.reset() } let upperBound = self.stream.deflateBound(inputBytes: input.count) return try self.stream.deflate(input, into: &output, upperBound: upperBound) @@ -372,43 +372,48 @@ extension UnsafeMutablePointer { _ input: [UInt8], into output: inout ByteBuffer, upperBound: Int - ) throws -> Int { + ) throws(ZlibError) -> Int { defer { self.setNextInputBuffer(nil) self.setNextOutputBuffer(nil) } - var input = input - return try input.withUnsafeMutableBytes { input in - self.setNextInputBuffer(input) - - return try output.writeWithUnsafeMutableBytes(minimumWritableBytes: upperBound) { output in - self.setNextOutputBuffer(output) - - let rc = CGRPCZlib_deflate(self, Z_FINISH) - - // Possible return codes: - // - Z_OK: some progress has been made - // - Z_STREAM_END: all input has been consumed and all output has been produced (only when - // flush is set to Z_FINISH) - // - Z_STREAM_ERROR: the stream state was inconsistent - // - Z_BUF_ERROR: no progress is possible - // - // The documentation notes that Z_BUF_ERROR is not fatal, and deflate() can be called again - // with more input and more output space to continue compressing. However, we - // call `deflateBound()` before `deflate()` which guarantees that the output size will not be - // larger than the value returned by `deflateBound()` if `Z_FINISH` flush is used. As such, - // the only acceptable outcome is `Z_STREAM_END`. - guard rc == Z_STREAM_END else { - throw RPCError( - code: .internalError, - message: "Compression error", - cause: ZlibError(code: Int(rc), message: self.lastError ?? "") - ) - } + do { + var input = input + return try input.withUnsafeMutableBytes { input in + self.setNextInputBuffer(input) + + return try output.writeWithUnsafeMutableBytes(minimumWritableBytes: upperBound) { output in + self.setNextOutputBuffer(output) + + let rc = CGRPCZlib_deflate(self, Z_FINISH) - return output.count - self.availableOutputBytes + // Possible return codes: + // - Z_OK: some progress has been made + // - Z_STREAM_END: all input has been consumed and all output has been produced (only when + // flush is set to Z_FINISH) + // - Z_STREAM_ERROR: the stream state was inconsistent + // - Z_BUF_ERROR: no progress is possible + // + // The documentation notes that Z_BUF_ERROR is not fatal, and deflate() can be called again + // with more input and more output space to continue compressing. However, we + // call `deflateBound()` before `deflate()` which guarantees that the output size will not be + // larger than the value returned by `deflateBound()` if `Z_FINISH` flush is used. As such, + // the only acceptable outcome is `Z_STREAM_END`. + guard rc == Z_STREAM_END else { + throw ZlibError(code: Int(rc), message: self.lastError ?? "") + } + + return output.count - self.availableOutputBytes + } } + } catch let error as ZlibError { + throw error + } catch { + // Shouldn't happen as 'withUnsafeMutableBytes' and 'writeWithUnsafeMutableBytes' are + // marked 'rethrows' (but don't support typed throws, yet) and the closure only throws + // an 'RPCError' which is handled above. + fatalError("Unexpected error of type \(type(of: error))") } } } diff --git a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift index 29da4b7dc..d80e1d526 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +import GRPCCore import NIOCore /// A ``GRPCMessageFramer`` helps with the framing of gRPC data frames: @@ -53,9 +54,9 @@ struct GRPCMessageFramer { /// - Parameter compressor: An optional compressor: if present, payloads will be compressed; otherwise /// they'll be framed as-is. /// - Throws: If an error is encountered, such as a compression failure, an error will be thrown. - mutating func next( + mutating func nextResult( compressor: Zlib.Compressor? = nil - ) throws -> (bytes: ByteBuffer, promise: EventLoopPromise?)? { + ) -> (result: Result, promise: EventLoopPromise?)? { if self.pendingMessages.isEmpty { // Nothing pending: exit early. return nil @@ -77,18 +78,19 @@ struct GRPCMessageFramer { var pendingWritePromise: EventLoopPromise? while let message = self.pendingMessages.pop() { - try self.encode(message.bytes, compressor: compressor) - if let existingPendingWritePromise = pendingWritePromise { - existingPendingWritePromise.futureResult.cascade(to: message.promise) - } else { - pendingWritePromise = message.promise + pendingWritePromise.setOrCascade(to: message.promise) + + do { + try self.encode(message.bytes, compressor: compressor) + } catch let rpcError { + return (result: .failure(rpcError), promise: pendingWritePromise) } } - return (bytes: self.writeBuffer, promise: pendingWritePromise) + return (result: .success(self.writeBuffer), promise: pendingWritePromise) } - private mutating func encode(_ message: [UInt8], compressor: Zlib.Compressor?) throws { + private mutating func encode(_ message: [UInt8], compressor: Zlib.Compressor?) throws(RPCError) { if let compressor { self.writeBuffer.writeInteger(UInt8(1)) // Set compression flag @@ -97,8 +99,12 @@ struct GRPCMessageFramer { self.writeBuffer.writeInteger(UInt32(0)) // Compress and overwrite the payload length field with the right length. - let writtenBytes = try compressor.compress(message, into: &self.writeBuffer) - self.writeBuffer.setInteger(UInt32(writtenBytes), at: lengthIndex) + do { + let writtenBytes = try compressor.compress(message, into: &self.writeBuffer) + self.writeBuffer.setInteger(UInt32(writtenBytes), at: lengthIndex) + } catch let zlibError { + throw RPCError(code: .internalError, message: "Compression failed", cause: zlibError) + } } else { self.writeBuffer.writeMultipleIntegers( UInt8(0), // Clear compression flag diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 3ab917dbb..c8dada299 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -396,6 +396,13 @@ struct GRPCStreamStateMachine { private var configuration: GRPCStreamStateMachineConfiguration private var skipAssertions: Bool + struct InvalidState: Error { + var message: String + init(_ message: String) { + self.message = message + } + } + init( configuration: GRPCStreamStateMachineConfiguration, maximumPayloadSize: Int, @@ -406,7 +413,7 @@ struct GRPCStreamStateMachine { self.skipAssertions = skipAssertions } - mutating func send(metadata: Metadata) throws -> HPACKHeaders { + mutating func send(metadata: Metadata) throws(InvalidState) -> HPACKHeaders { switch self.configuration { case .client(let clientConfiguration): return try self.clientSend(metadata: metadata, configuration: clientConfiguration) @@ -415,7 +422,7 @@ struct GRPCStreamStateMachine { } } - mutating func send(message: [UInt8], promise: EventLoopPromise?) throws { + mutating func send(message: [UInt8], promise: EventLoopPromise?) throws(InvalidState) { switch self.configuration { case .client: try self.clientSend(message: message, promise: promise) @@ -424,7 +431,7 @@ struct GRPCStreamStateMachine { } } - mutating func closeOutbound() throws { + mutating func closeOutbound() throws(InvalidState) { switch self.configuration { case .client: try self.clientCloseOutbound() @@ -436,7 +443,7 @@ struct GRPCStreamStateMachine { mutating func send( status: Status, metadata: Metadata - ) throws -> HPACKHeaders { + ) throws(InvalidState) -> HPACKHeaders { switch self.configuration { case .client: try self.invalidState( @@ -452,17 +459,20 @@ struct GRPCStreamStateMachine { enum OnMetadataReceived: Equatable { case receivedMetadata(Metadata, MethodDescriptor?) + case doNothing // Client-specific actions - case receivedStatusAndMetadata(status: Status, metadata: Metadata) - case doNothing + case receivedStatusAndMetadata_clientOnly(status: Status, metadata: Metadata) // Server-specific actions - case rejectRPC(trailers: HPACKHeaders) - case protocolViolation + case rejectRPC_serverOnly(trailers: HPACKHeaders) + case protocolViolation_serverOnly } - mutating func receive(headers: HPACKHeaders, endStream: Bool) throws -> OnMetadataReceived { + mutating func receive( + headers: HPACKHeaders, + endStream: Bool + ) throws(InvalidState) -> OnMetadataReceived { switch self.configuration { case .client(let clientConfiguration): return try self.clientReceive( @@ -483,16 +493,19 @@ struct GRPCStreamStateMachine { case readInbound case doNothing - // Client-specific actions - // This will be returned when the server sends a data frame with EOS set. // This is invalid as per the protocol specification, because the server // can only close by sending trailers, not by setting EOS when sending // a message. - case endRPCAndForwardErrorStatus(Status) + case endRPCAndForwardErrorStatus_clientOnly(Status) + + case forwardErrorAndClose_serverOnly(RPCError) } - mutating func receive(buffer: ByteBuffer, endStream: Bool) throws -> OnBufferReceivedAction { + mutating func receive( + buffer: ByteBuffer, + endStream: Bool + ) throws(InvalidState) -> OnBufferReceivedAction { switch self.configuration { case .client: return try self.clientReceive(buffer: buffer, endStream: endStream) @@ -513,9 +526,19 @@ struct GRPCStreamStateMachine { frame: ByteBuffer, promise: EventLoopPromise? ) + case closeAndFailPromise(EventLoopPromise?, RPCError) + + init(result: Result, promise: EventLoopPromise?) { + switch result { + case .success(let buffer): + self = .sendFrame(frame: buffer, promise: promise) + case .failure(let error): + self = .closeAndFailPromise(promise, error) + } + } } - mutating func nextOutboundFrame() throws -> OnNextOutboundFrame { + mutating func nextOutboundFrame() throws(InvalidState) -> OnNextOutboundFrame { switch self.configuration { case .client: return try self.clientNextOutboundFrame() @@ -647,7 +670,7 @@ extension GRPCStreamStateMachine { private mutating func clientSend( metadata: Metadata, configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration - ) throws -> HPACKHeaders { + ) throws(InvalidState) -> HPACKHeaders { // Client sends metadata only when opening the stream. switch self.state { case .clientIdleServerIdle(let state): @@ -685,7 +708,10 @@ extension GRPCStreamStateMachine { } } - private mutating func clientSend(message: [UInt8], promise: EventLoopPromise?) throws { + private mutating func clientSend( + message: [UInt8], + promise: EventLoopPromise? + ) throws(InvalidState) { switch self.state { case .clientIdleServerIdle: try self.invalidState("Client not yet open.") @@ -714,7 +740,7 @@ extension GRPCStreamStateMachine { } } - private mutating func clientCloseOutbound() throws { + private mutating func clientCloseOutbound() throws(InvalidState) { switch self.state { case .clientIdleServerIdle(let state): self.state = .clientClosedServerIdle(.init(previousState: state)) @@ -734,41 +760,52 @@ extension GRPCStreamStateMachine { /// Returns the client's next request to the server. /// - Returns: The request to be made to the server. - private mutating func clientNextOutboundFrame() throws -> OnNextOutboundFrame { + private mutating func clientNextOutboundFrame() throws(InvalidState) -> OnNextOutboundFrame { + switch self.state { case .clientIdleServerIdle: try self.invalidState("Client is not open yet.") case .clientOpenServerIdle(var state): self.state = ._modifying - let request = try state.framer.next(compressor: state.compressor) + let next = state.framer.nextResult(compressor: state.compressor) self.state = .clientOpenServerIdle(state) - return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } - ?? .awaitMoreMessages + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) + } else { + return .awaitMoreMessages + } case .clientOpenServerOpen(var state): self.state = ._modifying - let request = try state.framer.next(compressor: state.compressor) + let next = state.framer.nextResult(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return request.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } - ?? .awaitMoreMessages + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) + } else { + return .awaitMoreMessages + } case .clientClosedServerIdle(var state): self.state = ._modifying - let request = try state.framer.next(compressor: state.compressor) + let next = state.framer.nextResult(compressor: state.compressor) self.state = .clientClosedServerIdle(state) - if let request { - return .sendFrame(frame: request.bytes, promise: request.promise) + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) } else { return .noMoreMessages } case .clientClosedServerOpen(var state): self.state = ._modifying - let request = try state.framer.next(compressor: state.compressor) + let next = state.framer.nextResult(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - if let request { - return .sendFrame(frame: request.bytes, promise: request.promise) + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) } else { return .noMoreMessages } @@ -806,7 +843,7 @@ extension GRPCStreamStateMachine { guard let httpStatusCode else { return .invalid( - .receivedStatusAndMetadata( + .receivedStatusAndMetadata_clientOnly( status: .init(code: .unknown, message: "HTTP Status Code is missing."), metadata: Metadata(headers: metadata) ) @@ -822,7 +859,7 @@ extension GRPCStreamStateMachine { // Forward the mapped status code. return .invalid( - .receivedStatusAndMetadata( + .receivedStatusAndMetadata_clientOnly( status: .init( code: Status.Code(httpStatusCode: httpStatusCode), message: "Unexpected non-200 HTTP Status Code." @@ -835,7 +872,7 @@ extension GRPCStreamStateMachine { let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) guard contentTypeHeader.flatMap(ContentType.init) != nil else { return .invalid( - .receivedStatusAndMetadata( + .receivedStatusAndMetadata_clientOnly( status: .init( code: .internalError, message: "Missing \(GRPCHTTP2Keys.contentType.rawValue) header" @@ -863,7 +900,7 @@ extension GRPCStreamStateMachine { configuration.acceptedEncodings.contains(parsedEncoding) else { return .error( - .receivedStatusAndMetadata( + .receivedStatusAndMetadata_clientOnly( status: .init( code: .internalError, message: @@ -880,39 +917,43 @@ extension GRPCStreamStateMachine { return .success(inboundEncoding) } - private func validateAndReturnStatusAndMetadata( - _ metadata: HPACKHeaders - ) throws -> OnMetadataReceived { - let rawStatusCode = metadata.firstString(forKey: .grpcStatus) - guard let rawStatusCode, - let intStatusCode = Int(rawStatusCode), - let statusCode = Status.Code(rawValue: intStatusCode) - else { - let message = - "Non-initial metadata must be a trailer containing a valid grpc-status" - + (rawStatusCode.flatMap { "but was \($0)" } ?? "") - throw RPCError(code: .unknown, message: message) - } - - let statusMessage = - metadata.firstString(forKey: .grpcStatusMessage, canonicalForm: false) - .map { GRPCStatusMessageMarshaller.unmarshall($0) } ?? "" - - var convertedMetadata = Metadata(headers: metadata) + private func validateTrailers( + _ trailers: HPACKHeaders + ) throws(InvalidState) -> OnMetadataReceived { + let statusValue = trailers.firstString(forKey: .grpcStatus) + let statusCode = statusValue.flatMap { + Int($0) + }.flatMap { + Status.Code(rawValue: $0) + } + + let status: Status + if let code = statusCode { + let messageFieldValue = trailers.firstString(forKey: .grpcStatusMessage, canonicalForm: false) + let message = messageFieldValue.map { GRPCStatusMessageMarshaller.unmarshall($0) } ?? "" + status = Status(code: code, message: message) + } else { + let message: String + if let statusValue = statusValue { + message = "Invalid 'grpc-status' in trailers (\(statusValue))" + } else { + message = "No 'grpc-status' value in trailers" + } + status = Status(code: .unknown, message: message) + } + + var convertedMetadata = Metadata(headers: trailers) convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) - return .receivedStatusAndMetadata( - status: Status(code: statusCode, message: statusMessage), - metadata: convertedMetadata - ) + return .receivedStatusAndMetadata_clientOnly(status: status, metadata: convertedMetadata) } private mutating func clientReceive( headers: HPACKHeaders, endStream: Bool, configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration - ) throws -> OnMetadataReceived { + ) throws(InvalidState) -> OnMetadataReceived { switch self.state { case .clientOpenServerIdle(let state): switch (self.clientValidateHeadersReceivedFromServer(headers), endStream) { @@ -927,7 +968,7 @@ extension GRPCStreamStateMachine { case (.valid, true): // This is a trailers-only response: close server. self.state = .clientOpenServerClosed(.init(previousState: state)) - return try self.validateAndReturnStatusAndMetadata(headers) + return try self.validateTrailers(headers) case (.valid, false): switch self.processInboundEncoding(headers: headers, configuration: configuration) { case .error(let failure): @@ -960,7 +1001,7 @@ extension GRPCStreamStateMachine { if endStream { self.state = .clientOpenServerClosed(.init(previousState: state)) } - return try self.validateAndReturnStatusAndMetadata(headers) + return try self.validateTrailers(headers) case .clientClosedServerIdle(let state): switch (self.clientValidateHeadersReceivedFromServer(headers), endStream) { @@ -975,7 +1016,7 @@ extension GRPCStreamStateMachine { case (.valid, true): // This is a trailers-only response: close server. self.state = .clientClosedServerClosed(.init(previousState: state)) - return try self.validateAndReturnStatusAndMetadata(headers) + return try self.validateTrailers(headers) case (.valid, false): switch self.processInboundEncoding(headers: headers, configuration: configuration) { case .error(let failure): @@ -1000,7 +1041,7 @@ extension GRPCStreamStateMachine { if endStream { self.state = .clientClosedServerClosed(.init(previousState: state)) } - return try self.validateAndReturnStatusAndMetadata(headers) + return try self.validateTrailers(headers) case .clientClosedServerClosed: // We could end up here if we received a grpc-status header in a previous @@ -1031,7 +1072,7 @@ extension GRPCStreamStateMachine { private mutating func clientReceive( buffer: ByteBuffer, endStream: Bool - ) throws -> OnBufferReceivedAction { + ) throws(InvalidState) -> OnBufferReceivedAction { // This is a message received by the client, from the server. switch self.state { case .clientIdleServerIdle: @@ -1051,7 +1092,7 @@ extension GRPCStreamStateMachine { // can only close by sending trailers, not by setting EOS when sending // a message. self.state = .clientClosedServerClosed(.init(previousState: state)) - return .endRPCAndForwardErrorStatus( + return .endRPCAndForwardErrorStatus_clientOnly( Status( code: .internalError, message: """ @@ -1062,17 +1103,23 @@ extension GRPCStreamStateMachine { ) } - try state.deframer.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) + do { + try state.deframer.process(buffer: buffer) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + self.state = .clientOpenServerOpen(state) + return .readInbound + } catch { + self.state = .clientOpenServerOpen(state) + let status = Status(code: .internalError, message: "Failed to decode message") + return .endRPCAndForwardErrorStatus_clientOnly(status) } - self.state = .clientOpenServerOpen(state) - return .readInbound case .clientClosedServerOpen(var state): self.state = ._modifying if endStream { self.state = .clientClosedServerClosed(.init(previousState: state)) - return .endRPCAndForwardErrorStatus( + return .endRPCAndForwardErrorStatus_clientOnly( Status( code: .internalError, message: """ @@ -1086,11 +1133,17 @@ extension GRPCStreamStateMachine { // The client may have sent the end stream and thus it's closed, // but the server may still be responding. // The client must have a deframer set up, so force-unwrap is okay. - try state.deframer!.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) + do { + try state.deframer!.process(buffer: buffer) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + self.state = .clientClosedServerOpen(state) + return .readInbound + } catch { + self.state = .clientClosedServerOpen(state) + let status = Status(code: .internalError, message: "Failed to decode message") + return .endRPCAndForwardErrorStatus_clientOnly(status) } - self.state = .clientClosedServerOpen(state) - return .readInbound case .clientOpenServerClosed, .clientClosedServerClosed: try self.invalidState( @@ -1136,11 +1189,11 @@ extension GRPCStreamStateMachine { } } - private func invalidState(_ message: String, line: UInt = #line) throws -> Never { + private func invalidState(_ message: String, line: UInt = #line) throws(InvalidState) -> Never { if !self.skipAssertions { assertionFailure(message, line: line) } - throw RPCError(code: .internalError, message: message) + throw InvalidState(message) } private mutating func clientUnexpectedInboundClose( @@ -1207,7 +1260,7 @@ extension GRPCStreamStateMachine { private mutating func serverSend( metadata: Metadata, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration - ) throws -> HPACKHeaders { + ) throws(InvalidState) -> HPACKHeaders { // Server sends initial metadata switch self.state { case .clientOpenServerIdle(var state): @@ -1262,7 +1315,10 @@ extension GRPCStreamStateMachine { } } - private mutating func serverSend(message: [UInt8], promise: EventLoopPromise?) throws { + private mutating func serverSend( + message: [UInt8], + promise: EventLoopPromise? + ) throws(InvalidState) { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: try self.invalidState( @@ -1291,7 +1347,7 @@ extension GRPCStreamStateMachine { private mutating func serverSend( status: Status, customMetadata: Metadata - ) throws -> HPACKHeaders { + ) throws(InvalidState) -> HPACKHeaders { // Close the server. switch self.state { case .clientOpenServerOpen(var state): @@ -1335,7 +1391,7 @@ extension GRPCStreamStateMachine { headers: HPACKHeaders, endStream: Bool, configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration - ) throws -> OnMetadataReceived { + ) throws(InvalidState) -> OnMetadataReceived { func closeServer( from state: GRPCStreamStateMachineState.ClientIdleServerIdleState, endStream: Bool @@ -1357,12 +1413,12 @@ extension GRPCStreamStateMachine { // Respond with HTTP-level Unsupported Media Type status code. var trailers = HPACKHeaders() trailers.add("415", forKey: .status) - return .rejectRPC(trailers: trailers) + return .rejectRPC_serverOnly(trailers: trailers) } guard let pathHeader = headers.firstString(forKey: .path) else { self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC( + return .rejectRPC_serverOnly( trailers: .trailersOnly( code: .invalidArgument, message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." @@ -1372,7 +1428,7 @@ extension GRPCStreamStateMachine { guard let path = MethodDescriptor(path: pathHeader) else { self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC( + return .rejectRPC_serverOnly( trailers: .trailersOnly( code: .unimplemented, message: @@ -1385,7 +1441,7 @@ extension GRPCStreamStateMachine { .flatMap { GRPCStreamStateMachineConfiguration.Scheme(rawValue: $0) } if scheme == nil { self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC( + return .rejectRPC_serverOnly( trailers: .trailersOnly( code: .invalidArgument, message: ":scheme header must be present and one of \"http\" or \"https\"." @@ -1395,7 +1451,7 @@ extension GRPCStreamStateMachine { guard let method = headers.firstString(forKey: .method), method == "POST" else { self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC( + return .rejectRPC_serverOnly( trailers: .trailersOnly( code: .invalidArgument, message: ":method header is expected to be present and have a value of \"POST\"." @@ -1405,7 +1461,7 @@ extension GRPCStreamStateMachine { guard let te = headers.firstString(forKey: .te), te == "trailers" else { self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC( + return .rejectRPC_serverOnly( trailers: .trailersOnly( code: .invalidArgument, message: "\"te\" header is expected to be present and have a value of \"trailers\"." @@ -1424,7 +1480,7 @@ extension GRPCStreamStateMachine { if let rawEncoding = encodingValuesIterator.next() { guard encodingValuesIterator.next() == nil else { self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC( + return .rejectRPC_serverOnly( trailers: .trailersOnly( code: .internalError, message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." @@ -1448,7 +1504,7 @@ extension GRPCStreamStateMachine { trailers.add(name: GRPCHTTP2Keys.acceptEncoding.rawValue, value: acceptedEncoding.name) } - return .rejectRPC(trailers: trailers) + return .rejectRPC_serverOnly(trailers: trailers) } // Server supports client's encoding. @@ -1509,7 +1565,7 @@ extension GRPCStreamStateMachine { case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: // Metadata has already been received, should only be sent once by clients. - return .protocolViolation + return .protocolViolation_serverOnly case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: try self.invalidState("Client can't have sent metadata if closed.") @@ -1522,7 +1578,7 @@ extension GRPCStreamStateMachine { private mutating func serverReceive( buffer: ByteBuffer, endStream: Bool - ) throws -> OnBufferReceivedAction { + ) throws(InvalidState) -> OnBufferReceivedAction { let action: OnBufferReceivedAction switch self.state { @@ -1533,8 +1589,15 @@ extension GRPCStreamStateMachine { self.state = ._modifying // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. - try state.deframer!.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) + + do { + try state.deframer!.process(buffer: buffer) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + action = .readInbound + } catch { + let error = RPCError(code: .internalError, message: "Failed to decode message") + action = .forwardErrorAndClose_serverOnly(error) } if endStream { @@ -1543,12 +1606,17 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerIdle(state) } - action = .readInbound - case .clientOpenServerOpen(var state): self.state = ._modifying - try state.deframer.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) + + do { + try state.deframer.process(buffer: buffer) { deframedMessage in + state.inboundMessageBuffer.append(deframedMessage) + } + action = .readInbound + } catch { + let error = RPCError(code: .internalError, message: "Failed to decode message") + action = .forwardErrorAndClose_serverOnly(error) } if endStream { @@ -1557,8 +1625,6 @@ extension GRPCStreamStateMachine { self.state = .clientOpenServerOpen(state) } - action = .readInbound - case .clientOpenServerClosed(let state): // Client is not done sending request, but server has already closed. // Ignore the rest of the request: do nothing, unless endStream is set, @@ -1579,44 +1645,55 @@ extension GRPCStreamStateMachine { return action } - private mutating func serverNextOutboundFrame() throws -> OnNextOutboundFrame { + private mutating func serverNextOutboundFrame() throws(InvalidState) -> OnNextOutboundFrame { switch self.state { case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: try self.invalidState("Server is not open yet.") case .clientOpenServerOpen(var state): self.state = ._modifying - let response = try state.framer.next(compressor: state.compressor) + let next = state.framer.nextResult(compressor: state.compressor) self.state = .clientOpenServerOpen(state) - return response.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } - ?? .awaitMoreMessages + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) + } else { + return .awaitMoreMessages + } case .clientClosedServerOpen(var state): self.state = ._modifying - let response = try state.framer.next(compressor: state.compressor) + let next = state.framer.nextResult(compressor: state.compressor) self.state = .clientClosedServerOpen(state) - return response.map { .sendFrame(frame: $0.bytes, promise: $0.promise) } - ?? .awaitMoreMessages + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) + } else { + return .awaitMoreMessages + } case .clientOpenServerClosed(var state): self.state = ._modifying - let response = try state.framer?.next(compressor: state.compressor) + let next = state.framer?.nextResult(compressor: state.compressor) self.state = .clientOpenServerClosed(state) - if let response { - return .sendFrame(frame: response.bytes, promise: response.promise) + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) } else { return .noMoreMessages } case .clientClosedServerClosed(var state): self.state = ._modifying - let response = try state.framer?.next(compressor: state.compressor) + let next = state.framer?.nextResult(compressor: state.compressor) self.state = .clientClosedServerClosed(state) - if let response { - return .sendFrame(frame: response.bytes, promise: response.promise) + + if let next = next { + return OnNextOutboundFrame(result: next.result, promise: next.promise) } else { return .noMoreMessages } + case ._modifying: preconditionFailure() } @@ -1860,3 +1937,10 @@ extension Status { self = Status(code: Status.Code(error.code), message: error.message) } } + +extension RPCError { + @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + init(_ invalidState: GRPCStreamStateMachine.InvalidState) { + self = RPCError(code: .internalError, message: "Invalid state", cause: invalidState) + } +} diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 29d994b53..125ee604b 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -76,10 +76,15 @@ extension GRPCServerStreamHandler { case .byteBuffer(let buffer): do { switch try self.stateMachine.receive(buffer: buffer, endStream: endStream) { - case .endRPCAndForwardErrorStatus: + case .endRPCAndForwardErrorStatus_clientOnly: preconditionFailure( "OnBufferReceivedAction.endRPCAndForwardErrorStatus should never be returned for the server." ) + + case .forwardErrorAndClose_serverOnly(let error): + context.fireErrorCaught(error) + context.close(mode: .all, promise: nil) + case .readInbound: loop: while true { switch self.stateMachine.nextInboundMessage() { @@ -95,7 +100,8 @@ extension GRPCServerStreamHandler { case .doNothing: () } - } catch { + } catch let invalidState { + let error = RPCError(invalidState) context.fireErrorCaught(error) } @@ -118,7 +124,7 @@ extension GRPCServerStreamHandler { assertionFailure("Method descriptor should have been present if we received metadata.") } - case .rejectRPC(let trailers): + case .rejectRPC_serverOnly(let trailers): self.flushPending = true self.methodDescriptorPromise.fail( RPCError( @@ -129,20 +135,18 @@ extension GRPCServerStreamHandler { let response = HTTP2Frame.FramePayload.headers(.init(headers: trailers, endStream: true)) context.write(self.wrapOutboundOut(response), promise: nil) - case .receivedStatusAndMetadata: - throw RPCError( - code: .internalError, - message: "Server cannot get receivedStatusAndMetadata." - ) + case .receivedStatusAndMetadata_clientOnly: + assertionFailure("Unexpected action") - case .protocolViolation: + case .protocolViolation_serverOnly: context.writeAndFlush(self.wrapOutboundOut(.rstStream(.protocolError)), promise: nil) context.close(promise: nil) case .doNothing: - throw RPCError(code: .internalError, message: "Server cannot receive doNothing.") + () } - } catch { + } catch let invalidState { + let error = RPCError(invalidState) context.fireErrorCaught(error) } @@ -207,7 +211,8 @@ extension GRPCServerStreamHandler { self.flushPending = true let headers = try self.stateMachine.send(metadata: metadata) context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: promise) - } catch { + } catch let invalidState { + let error = RPCError(invalidState) promise?.fail(error) context.fireErrorCaught(error) } @@ -215,7 +220,8 @@ extension GRPCServerStreamHandler { case .message(let message): do { try self.stateMachine.send(message: message, promise: promise) - } catch { + } catch let invalidState { + let error = RPCError(invalidState) promise?.fail(error) context.fireErrorCaught(error) } @@ -225,7 +231,8 @@ extension GRPCServerStreamHandler { let headers = try self.stateMachine.send(status: status, metadata: metadata) let response = HTTP2Frame.FramePayload.headers(.init(headers: headers, endStream: true)) self.pendingTrailers = (response, promise) - } catch { + } catch let invalidState { + let error = RPCError(invalidState) promise?.fail(error) context.fireErrorCaught(error) } @@ -261,6 +268,10 @@ extension GRPCServerStreamHandler { case .awaitMoreMessages: break loop + + case .closeAndFailPromise(let promise, let error): + context.close(mode: .all, promise: nil) + promise?.fail(error) } } @@ -268,7 +279,8 @@ extension GRPCServerStreamHandler { self.flushPending = false context.flush() } - } catch { + } catch let invalidState { + let error = RPCError(invalidState) context.fireErrorCaught(error) } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift index 275f313ac..ab6da3f3f 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift @@ -310,19 +310,16 @@ final class GRPCClientStreamHandlerTests: XCTestCase { data: .byteBuffer(buffer), endStream: false ) - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - "Message has exceeded the configured maximum payload size (max: 1, actual: 42)" - ) - } - // Make sure we didn't read the received message - XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) + // Invalid payload should result in error status and stream being closed + try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) + let part = try channel.readInbound(as: RPCResponsePart.self) + XCTAssertEqual( + part, + .status(Status(code: .internalError, message: "Failed to decode message"), [:]) + ) + channel.embeddedEventLoop.run() + try channel.closeFuture.wait() } func testServerSendsEOSWhenSendingMessage_ResultsInErrorStatus() throws { @@ -455,7 +452,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { try channel.writeInbound(HTTP2Frame.FramePayload.data(serverDataPayload)) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") + XCTAssertEqual(error.message, "Invalid state") } } @@ -537,7 +534,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { try channel.writeOutbound(RPCRequestPart.message(.init(repeating: 1, count: 42))) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed, cannot send a message.") + XCTAssertEqual(error.message, "Invalid state") } // This is needed to clear the EmbeddedChannel's stored error, otherwise @@ -812,7 +809,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + XCTAssertEqual(error.message, "Invalid state") } } @@ -858,7 +855,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + XCTAssertEqual(error.message, "Invalid state") } } @@ -911,7 +908,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") + XCTAssertEqual(error.message, "Invalid state") } } } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift index 103c89f70..bcb3e375f 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift @@ -217,3 +217,21 @@ final class GRPCMessageDeframerTests: XCTestCase { try self.testReadDecompressedMessageOverSizeLimit(method: .gzip) } } + +extension GRPCMessageFramer { + mutating func next( + compressor: Zlib.Compressor? = nil + ) throws(RPCError) -> (bytes: ByteBuffer, promise: EventLoopPromise?)? { + if let (result, promise) = self.nextResult(compressor: compressor) { + switch result { + case .success(let buffer): + return (bytes: buffer, promise: promise) + case .failure(let error): + promise?.fail(error) + throw error + } + } else { + return nil + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 221aa252b..62589bf74 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -214,9 +214,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: targetState) // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { + XCTAssertThrowsError( + ofType: GRPCStreamStateMachine.InvalidState.self, + try stateMachine.send(metadata: .init()) + ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") } } @@ -230,9 +232,11 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: targetState) // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { + XCTAssertThrowsError( + ofType: GRPCStreamStateMachine.InvalidState.self, + try stateMachine.send(metadata: .init()) + ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed: can't send metadata.") } } @@ -245,10 +249,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try to send a message without opening (i.e. without sending initial metadata) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client not yet open.") } } @@ -273,10 +276,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is closed, cannot send a message.") } } @@ -290,13 +292,12 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // This operation is never allowed on the client. XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send( status: Status(code: .ok, message: ""), metadata: .init() ) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client cannot send status and trailer.") } } @@ -308,10 +309,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(headers: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } @@ -330,7 +330,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual( action, - .receivedStatusAndMetadata( + .receivedStatusAndMetadata_clientOnly( status: .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code."), metadata: [":status": "300"] ) @@ -353,7 +353,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { XCTAssertEqual( action, - .receivedStatusAndMetadata( + .receivedStatusAndMetadata_clientOnly( status: Status( code: .internalError, message: @@ -406,13 +406,14 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receiving compressed message with gzip (unsupported) should throw error let receivedGZIPCompressedBytes = try self.frameMessage(originalMessage, compression: .gzip) - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Decompression error") - } + let action = try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) + XCTAssertEqual( + action, + .endRPCAndForwardErrorStatus_clientOnly( + Status(code: .internalError, message: "Failed to decode message") + ) + ) + receivedAction = stateMachine.nextInboundMessage() switch receivedAction { case .awaitMoreMessages: @@ -459,32 +460,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ] { var stateMachine = self.makeClientStateMachine(targetState: targetState) - // Receiving initial metadata again should throw if grpc-status is not present. - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive( - headers: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - "custom-bin": String(base64Encoding: [42, 43, 44]), - ], - endStream: false - ) - ) { error in - XCTAssertEqual(error.code, .unknown) - XCTAssertEqual( - error.message, - "Non-initial metadata must be a trailer containing a valid grpc-status" - ) - } - - // Now make sure everything works well if we include grpc-status - let action = try stateMachine.receive( + let action1 = try stateMachine.receive( headers: [ GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, GRPCHTTP2Keys.encoding.rawValue: "deflate", "custom": "123", @@ -493,17 +471,36 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { endStream: false ) - var expectedMetadata: Metadata = [ + let expectedStatus = Status(code: .unknown, message: "No 'grpc-status' value in trailers") + let expectedMetadata: Metadata = [ ":status": "200", "content-type": "application/grpc", "grpc-encoding": "deflate", "custom": "123", + "custom-bin": .binary([42, 43, 44]), ] - expectedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) - expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") + XCTAssertEqual( - action, - .receivedStatusAndMetadata( + action1, + .receivedStatusAndMetadata_clientOnly(status: expectedStatus, metadata: expectedMetadata) + ) + + // Now make sure everything works well if we include grpc-status + let action2 = try stateMachine.receive( + headers: [ + GRPCHTTP2Keys.status.rawValue: "200", + GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), + GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, + GRPCHTTP2Keys.encoding.rawValue: "deflate", + "custom": "123", + "custom-bin": String(base64Encoding: [42, 43, 44]), + ], + endStream: false + ) + + XCTAssertEqual( + action2, + .receivedStatusAndMetadata_clientOnly( status: Status(code: .ok, message: ""), metadata: expectedMetadata ) @@ -516,10 +513,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: targetState) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(headers: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } @@ -532,10 +528,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive an end trailer XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(headers: .init(), endStream: true) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") } } @@ -555,7 +550,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ] let trailers = try stateMachine.receive(headers: trailersOnlyResponse, endStream: true) switch trailers { - case .receivedStatusAndMetadata(let status, let metadata): + case .receivedStatusAndMetadata_clientOnly(let status, let metadata): XCTAssertEqual(status, Status(code: .internalError, message: "Some, status, message")) XCTAssertEqual( metadata, @@ -565,7 +560,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "custom-key": "custom-value", ] ) - case .receivedMetadata, .doNothing, .rejectRPC, .protocolViolation: + case .receivedMetadata, .doNothing, .rejectRPC_serverOnly, .protocolViolation_serverOnly: XCTFail("Expected .receivedStatusAndMetadata") } } @@ -594,7 +589,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ] XCTAssertEqual( action, - .receivedStatusAndMetadata( + .receivedStatusAndMetadata_clientOnly( status: .init(code: .ok, message: ""), metadata: expectedMetadata ) @@ -607,10 +602,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { // Receive another end trailer XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(headers: .init(), endStream: true) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") } } @@ -630,7 +624,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ] let trailers = try stateMachine.receive(headers: trailersOnlyResponse, endStream: true) switch trailers { - case .receivedStatusAndMetadata(let status, let metadata): + case .receivedStatusAndMetadata_clientOnly(let status, let metadata): XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) XCTAssertEqual( metadata, @@ -640,7 +634,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { "custom-key": "custom-value", ] ) - case .receivedMetadata, .doNothing, .rejectRPC, .protocolViolation: + case .receivedMetadata, .doNothing, .rejectRPC_serverOnly, .protocolViolation_serverOnly: XCTFail("Expected .receivedStatusAndMetadata") } } @@ -660,10 +654,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, "Cannot have received anything from server if client is not yet open." @@ -676,10 +669,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: targetState) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, "Server cannot have sent a message before sending the initial metadata." @@ -698,7 +690,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { ) XCTAssertEqual( try stateMachine.receive(buffer: .init(), endStream: true), - .endRPCAndForwardErrorStatus( + .endRPCAndForwardErrorStatus_clientOnly( Status( code: .internalError, message: """ @@ -716,10 +708,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: targetState) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") } } @@ -731,10 +722,9 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.nextOutboundFrame() ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client is not open yet.") } } @@ -1144,7 +1134,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { }() XCTAssertEqual( metadataReceivedAction, - .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + .receivedStatusAndMetadata_clientOnly( + status: .init(code: .ok, message: ""), + metadata: receivedMetadata + ) ) XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) @@ -1236,7 +1229,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { }() XCTAssertEqual( metadataReceivedAction, - .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + .receivedStatusAndMetadata_clientOnly( + status: .init(code: .ok, message: ""), + metadata: receivedMetadata + ) ) XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) @@ -1325,7 +1321,10 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { }() XCTAssertEqual( metadataReceivedAction, - .receivedStatusAndMetadata(status: .init(code: .ok, message: ""), metadata: receivedMetadata) + .receivedStatusAndMetadata_clientOnly( + status: .init(code: .ok, message: ""), + metadata: receivedMetadata + ) ) XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) @@ -1414,10 +1413,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(metadata: .init()) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, "Client cannot be idle if server is sending initial metadata: it must have opened." @@ -1461,10 +1459,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending metadata again: should throw XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(metadata: .init()) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server has already sent initial metadata.") } } @@ -1473,8 +1470,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) + XCTAssertThrowsError( + ofType: GRPCStreamStateMachine.InvalidState.self, + try stateMachine.send(metadata: .init()) + ) { error in XCTAssertEqual(error.message, "Server cannot send metadata if closed.") } } @@ -1491,8 +1490,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) + XCTAssertThrowsError( + ofType: GRPCStreamStateMachine.InvalidState.self, + try stateMachine.send(metadata: .init()) + ) { error in XCTAssertEqual(error.message, "Server has already sent initial metadata.") } } @@ -1501,8 +1502,10 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) // Try sending metadata again: should throw - XCTAssertThrowsError(ofType: RPCError.self, try stateMachine.send(metadata: .init())) { error in - XCTAssertEqual(error.code, .internalError) + XCTAssertThrowsError( + ofType: GRPCStreamStateMachine.InvalidState.self, + try stateMachine.send(metadata: .init()) + ) { error in XCTAssertEqual(error.message, "Server cannot send metadata if closed.") } } @@ -1513,10 +1516,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, "Server must have sent initial metadata before sending a message." @@ -1529,10 +1531,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Now send a message XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, "Server must have sent initial metadata before sending a message." @@ -1552,10 +1553,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1564,10 +1564,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual( error.message, "Server must have sent initial metadata before sending a message." @@ -1588,10 +1587,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1602,13 +1600,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send( status: .init(code: .ok, message: ""), metadata: .init() ) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send status if client is idle.") } } @@ -1634,10 +1631,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1656,10 +1652,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1668,13 +1663,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send( status: .init(code: .ok, message: ""), metadata: .init() ) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } } @@ -1700,10 +1694,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1722,10 +1715,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try sending another message: it should fail because server is now closed. XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send(message: [], promise: nil) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send a message if it's closed.") } } @@ -1734,13 +1726,12 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.send( status: .init(code: .ok, message: ""), metadata: .init() ) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server can't send anything if closed.") } } @@ -2013,31 +2004,30 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Try receiving initial metadata again - should be a protocol violation let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - XCTAssertEqual(action, .protocolViolation) + XCTAssertEqual(action, .protocolViolation_serverOnly) } func testReceiveMetadataWhenClientOpenAndServerOpen() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - XCTAssertEqual(action, .protocolViolation) + XCTAssertEqual(action, .protocolViolation_serverOnly) } func testReceiveMetadataWhenClientOpenAndServerClosed() throws { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - XCTAssertEqual(action, .protocolViolation) + XCTAssertEqual(action, .protocolViolation_serverOnly) } func testReceiveMetadataWhenClientClosedAndServerIdle() { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } @@ -2046,10 +2036,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } @@ -2058,10 +2047,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") } } @@ -2072,10 +2060,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Can't have received a message if client is idle.") } } @@ -2089,10 +2076,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Verify client is now closed XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -2106,10 +2092,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Verify client is now closed XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -2152,13 +2137,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { // Receiving compressed message with gzip (unsupported) should throw error let receivedGZIPCompressedBytes = try self.frameMessage(originalMessage, compression: .gzip) - XCTAssertThrowsError( - ofType: RPCError.self, - try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Decompression error") - } + let action = try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) + XCTAssertEqual( + action, + .forwardErrorAndClose_serverOnly( + RPCError(code: .internalError, message: "Failed to decode message") + ) + ) + receivedAction = stateMachine.nextInboundMessage() switch receivedAction { case .awaitMoreMessages: @@ -2181,10 +2167,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -2193,10 +2178,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -2205,10 +2189,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.receive(buffer: .init(), endStream: false) ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Client can't send a message if closed.") } } @@ -2219,10 +2202,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.nextOutboundFrame() ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -2231,10 +2213,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.nextOutboundFrame() ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -2243,10 +2224,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.nextOutboundFrame() ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -2314,10 +2294,9 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) XCTAssertThrowsError( - ofType: RPCError.self, + ofType: GRPCStreamStateMachine.InvalidState.self, try stateMachine.nextOutboundFrame() ) { error in - XCTAssertEqual(error.code, .internalError) XCTAssertEqual(error.message, "Server is not open yet.") } } @@ -2854,7 +2833,7 @@ extension XCTestCase { _ action: GRPCStreamStateMachine.OnMetadataReceived, expression: (HPACKHeaders) throws -> Void ) rethrows { - guard case .rejectRPC(let trailers) = action else { + guard case .rejectRPC_serverOnly(let trailers) = action else { XCTFail("RPC should have been rejected.") return } diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index d9e6e8aeb..e7e3a7ad2 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -342,11 +342,8 @@ final class GRPCServerStreamHandlerTests: XCTestCase { ofType: RPCError.self, try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - "Message has exceeded the configured maximum payload size (max: 1, actual: 42)" - ) + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual(error.message, "Failed to decode message") } // Make sure we haven't sent a response back and that we didn't read the received message @@ -416,7 +413,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Client can't send a message if closed.") + XCTAssertEqual(error.message, "Invalid state") } } @@ -526,7 +523,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { try channel.writeOutbound(trailers) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server can't send anything if closed.") + XCTAssertEqual(error.message, "Invalid state") } } @@ -973,7 +970,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + XCTAssertEqual(error.message, "Invalid state") } } @@ -1028,7 +1025,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + XCTAssertEqual(error.message, "Invalid state") } } @@ -1083,7 +1080,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) ) { error in XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Server cannot send metadata if closed.") + XCTAssertEqual(error.message, "Invalid state") } } } From 0f9da0cca05644a1461c1f669f4c25ef7d529d9d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 9 Jul 2024 16:40:23 +0100 Subject: [PATCH 394/580] Use new protobuf API for (de)/serializing (#1974) Motivation: Currently the protobuf serialization is to `Data` which is then copied into an `Array` as required by the grpc api. Protobuf has new API which allows us to serialize directly into `Array`. Modifications: - use the new API - avoid the deprecated API Result: Fewer allocations --- Sources/GRPCProtobuf/Coding.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/GRPCProtobuf/Coding.swift b/Sources/GRPCProtobuf/Coding.swift index 174c87c1e..ecd4b48f8 100644 --- a/Sources/GRPCProtobuf/Coding.swift +++ b/Sources/GRPCProtobuf/Coding.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -import Foundation import GRPCCore import SwiftProtobuf @@ -28,8 +27,7 @@ public struct ProtobufSerializer: GRPCCore.Messa /// - Returns: An array of serialized bytes representing the message. public func serialize(_ message: Message) throws -> [UInt8] { do { - let data = try message.serializedData() - return Array(data) + return try message.serializedBytes() } catch let error { throw RPCError( code: .invalidArgument, @@ -50,7 +48,7 @@ public struct ProtobufDeserializer: GRPCCore.Mes /// - Returns: The deserialized message. public func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message { do { - let message = try Message(contiguousBytes: serializedMessageBytes) + let message = try Message(serializedBytes: serializedMessageBytes) return message } catch let error { throw RPCError( From 6bcc734213ce70ad5b50c745e6e8db91b14b5d6e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 10 Jul 2024 10:35:25 +0100 Subject: [PATCH 395/580] Use package ACL for v2 (#1976) Motivation: v2 uses '@_spi(Package)' in a number of places. Now that we require a Swift 6 compiler the 'package' access modifier can be used in its place. Modifications: - replace `@_spi(Package)` with `package` - use `package` access instead of `@testable` - remove a number of backports required for older compiler versions which are no longer necessary Result: - Less use of underscored API --- .../GRPCCore/Call/Client/CallOptions.swift | 3 +- .../Coding/CompressionAlgorithm.swift | 6 +-- .../Concurrency Primitives/Lock.swift | 23 ++++----- Sources/GRPCCore/Internal/MethodConfigs.swift | 12 ++--- .../Internal/Task+SleepBackport.swift | 29 ----------- .../Internal/AsyncStream+MakeStream.swift | 32 ------------ .../Connection/ClientConnectionHandler.swift | 43 ++++++++-------- .../Client/Connection/Connection.swift | 39 ++++++++------- .../Client/Connection/ConnectionBackoff.swift | 18 +++---- .../Client/Connection/ConnectionFactory.swift | 8 ++- .../Client/Connection/ConnectivityState.swift | 2 +- .../Client/Connection/GRPCChannel.swift | 50 +++++++++---------- .../LoadBalancers/LoadBalancer.swift | 12 ++--- .../LoadBalancers/LoadBalancerEvent.swift | 2 +- .../LoadBalancers/PickFirstLoadBalancer.swift | 18 +++---- .../RoundRobinLoadBalancer.swift | 18 +++---- .../Connection/LoadBalancers/Subchannel.swift | 18 +++---- .../Client/GRPCClientStreamHandler.swift | 2 +- .../Compression/CompressionAlgorithm.swift | 2 +- .../GRPCStreamStateMachine.swift | 15 +++--- .../Internal/AsyncStream+MakeStream.swift | 32 ------------ .../Internal/NIOChannelPipeline+GRPC.swift | 10 ++-- .../NIOSocketAddress+GRPCSocketAddress.swift | 10 ++-- .../Internal/ProcessUniqueID.swift | 5 +- .../Server/Connection/ServerConnection.swift | 15 +++--- .../Server/GRPCServerStreamHandler.swift | 32 ++++++------ .../HTTP2ClientTransport+Posix.swift | 2 +- .../HTTP2ServerTransport+Posix.swift | 6 +-- .../NIOClientBootstrap+SocketAddress.swift | 2 +- ...TP2ServerTransport+TransportServices.swift | 6 +-- .../InProcessClientTransport.swift | 8 +-- .../Internal/AsyncStream+MakeStream.swift | 32 ------------ .../Internal/AsyncStream+MakeStream.swift | 32 ------------ .../Internal/AsyncStream+MakeStream.swift | 32 ------------ .../Internal/MethodConfigsTests.swift | 6 +-- .../AsyncSequence+Utilities.swift | 33 ------------ .../ClientConnectionHandlerTests.swift | 2 +- .../Connection/Connection+Equatable.swift | 10 ++-- .../Client/Connection/ConnectionTests.swift | 2 +- .../Client/Connection/GRPCChannelTests.swift | 2 +- .../LoadBalancers/LoadBalancerTest.swift | 2 +- .../PickFirstLoadBalancerTests.swift | 2 +- .../RoundRobinLoadBalancerTests.swift | 2 +- .../LoadBalancers/SubchannelTests.swift | 6 +-- .../Client/Connection/RequestQueueTests.swift | 8 +-- .../Connection/Utilities/ConnectionTest.swift | 2 +- .../Utilities/HTTP2Connectors.swift | 5 +- .../AsyncStream+MakeStream.swift | 32 ------------ .../TracingTestsUtilities.swift | 16 ------ 49 files changed, 209 insertions(+), 497 deletions(-) delete mode 100644 Sources/GRPCCore/Internal/Task+SleepBackport.swift delete mode 100644 Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift delete mode 100644 Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift delete mode 100644 Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift delete mode 100644 Sources/performance-worker/Internal/AsyncStream+MakeStream.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index a68c9a530..ce6388050 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -127,8 +127,7 @@ extension CallOptions { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { - @_spi(Package) - public mutating func formUnion(with methodConfig: MethodConfig?) { + package mutating func formUnion(with methodConfig: MethodConfig?) { guard let methodConfig = methodConfig else { return } self.timeout.setIfNone(to: methodConfig.timeout) diff --git a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift index e3b39e048..7b54e6636 100644 --- a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift +++ b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift @@ -16,15 +16,13 @@ /// Message compression algorithms. public struct CompressionAlgorithm: Hashable, Sendable { - @_spi(Package) - public enum Value: UInt8, Hashable, Sendable, CaseIterable { + package enum Value: UInt8, Hashable, Sendable, CaseIterable { case none = 0 case deflate case gzip } - @_spi(Package) - public let value: Value + package let value: Value fileprivate init(_ algorithm: Value) { self.value = algorithm diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift index 5ee259adc..cd334df5f 100644 --- a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift +++ b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift @@ -237,49 +237,48 @@ extension UnsafeMutablePointer { } @usableFromInline -internal typealias LockedValueBox = _LockedValueBox - -// TODO: Use 'package' ACL when 5.9 is the minimum Swift version. -public struct _LockedValueBox { +package struct LockedValueBox { @usableFromInline let storage: LockStorage @inlinable - public init(_ value: Value) { + package init(_ value: Value) { self.storage = .create(value: value) } @inlinable - public func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + package func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { return try self.storage.withLockedValue(mutate) } /// An unsafe view over the locked value box. /// /// Prefer ``withLockedValue(_:)`` where possible. - public var unsafe: Unsafe { + @usableFromInline + package var unsafe: Unsafe { Unsafe(storage: self.storage) } - public struct Unsafe { + @usableFromInline + package struct Unsafe { @usableFromInline let storage: LockStorage /// Manually acquire the lock. @inlinable - public func lock() { + package func lock() { self.storage.lock() } /// Manually release the lock. @inlinable - public func unlock() { + package func unlock() { self.storage.unlock() } /// Mutate the value, assuming the lock has been acquired manually. @inlinable - public func withValueAssumingLockIsAcquired( + package func withValueAssumingLockIsAcquired( _ mutate: (inout Value) throws -> T ) rethrows -> T { return try self.storage.withUnsafeMutablePointerToHeader { value in @@ -289,4 +288,4 @@ public struct _LockedValueBox { } } -extension _LockedValueBox: Sendable where Value: Sendable {} +extension LockedValueBox: Sendable where Value: Sendable {} diff --git a/Sources/GRPCCore/Internal/MethodConfigs.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift index 7cc6ec53d..46ae4ddce 100644 --- a/Sources/GRPCCore/Internal/MethodConfigs.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -14,8 +14,6 @@ * limitations under the License. */ -// TODO: when swift(>=5.9), use 'package' access level - /// A collection of ``MethodConfig``s, mapped to specific methods or services. /// /// When creating a new instance, no overrides and no default will be set for using when getting @@ -25,13 +23,13 @@ /// /// Use the subscript to get and set configurations for specific methods. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct _MethodConfigs: Sendable, Hashable { +package struct MethodConfigs: Sendable, Hashable { private var elements: [MethodConfig.Name: MethodConfig] /// Create a new ``_MethodConfigs``. /// /// - Parameter serviceConfig: The configuration to read ``MethodConfig`` from. - public init(serviceConfig: ServiceConfig = ServiceConfig()) { + package init(serviceConfig: ServiceConfig = ServiceConfig()) { self.elements = [:] for configuration in serviceConfig.methodConfig { @@ -52,7 +50,7 @@ public struct _MethodConfigs: Sendable, Hashable { /// /// - Parameters: /// - descriptor: The ``MethodDescriptor`` for which to get or set a ``MethodConfig``. - public subscript(_ descriptor: MethodDescriptor) -> MethodConfig? { + package subscript(_ descriptor: MethodDescriptor) -> MethodConfig? { get { var name = MethodConfig.Name(service: descriptor.service, method: descriptor.method) @@ -81,7 +79,7 @@ public struct _MethodConfigs: Sendable, Hashable { /// Set a default configuration for all methods that have no overrides. /// /// - Parameter configuration: The default configuration. - public mutating func setDefaultConfiguration(_ configuration: MethodConfig?) { + package mutating func setDefaultConfiguration(_ configuration: MethodConfig?) { let name = MethodConfig.Name(service: "", method: "") self.elements[name] = configuration } @@ -95,7 +93,7 @@ public struct _MethodConfigs: Sendable, Hashable { /// - Parameters: /// - configuration: The default configuration for the service. /// - service: The name of the service for which this override applies. - public mutating func setDefaultConfiguration( + package mutating func setDefaultConfiguration( _ configuration: MethodConfig?, forService service: String ) { diff --git a/Sources/GRPCCore/Internal/Task+SleepBackport.swift b/Sources/GRPCCore/Internal/Task+SleepBackport.swift deleted file mode 100644 index cd8620da8..000000000 --- a/Sources/GRPCCore/Internal/Task+SleepBackport.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -#if swift(<5.9) -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Task where Success == Never, Failure == Never { - @inlinable - static func sleep( - for duration: C.Instant.Duration, - tolerance: C.Instant.Duration? = nil, - clock: C = ContinuousClock() - ) async throws { - try await clock.sleep(until: clock.now.advanced(by: duration), tolerance: tolerance) - } -} -#endif diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift b/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift deleted file mode 100644 index 68b492869..000000000 --- a/Sources/GRPCCore/Streaming/Internal/AsyncStream+MakeStream.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - @inlinable - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} -#endif diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 90a7cad45..be4006c32 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -18,9 +18,8 @@ import NIOCore import NIOHTTP2 /// An event which happens on a client's HTTP/2 connection. -@_spi(Package) -public enum ClientConnectionEvent: Sendable { - public enum CloseReason: Sendable { +package enum ClientConnectionEvent: Sendable { + package enum CloseReason: Sendable { /// The server sent a GOAWAY frame to the client. case goAway(HTTP2ErrorCode, String) /// The keep alive timer fired and subsequently timed out. @@ -49,14 +48,14 @@ public enum ClientConnectionEvent: Sendable { /// 3. Forwarding lifecycle events to the next handler. /// /// Some of the behaviours are described in [gRFC A8](https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md). -final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandler { - typealias InboundIn = HTTP2Frame - typealias InboundOut = ClientConnectionEvent +package final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandler { + package typealias InboundIn = HTTP2Frame + package typealias InboundOut = ClientConnectionEvent - typealias OutboundIn = Never - typealias OutboundOut = HTTP2Frame + package typealias OutboundIn = Never + package typealias OutboundOut = HTTP2Frame - enum OutboundEvent: Hashable, Sendable { + package enum OutboundEvent: Hashable, Sendable { /// Close the connection gracefully case closeGracefully } @@ -103,7 +102,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl /// is received. /// - keepaliveWithoutCalls: Whether the client sends keep-alive pings when there are no calls /// in progress. - init( + package init( eventLoop: any EventLoop, maxIdleTime: TimeAmount?, keepaliveTime: TimeAmount?, @@ -121,16 +120,16 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl self.inReadLoop = false } - func handlerAdded(context: ChannelHandlerContext) { + package func handlerAdded(context: ChannelHandlerContext) { assert(context.eventLoop === self.eventLoop) self.context = context } - func handlerRemoved(context: ChannelHandlerContext) { + package func handlerRemoved(context: ChannelHandlerContext) { self.context = nil } - func channelInactive(context: ChannelHandlerContext) { + package func channelInactive(context: ChannelHandlerContext) { switch self.state.closed() { case .none: () @@ -147,7 +146,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl self.keepaliveTimeoutTimer.cancel() } - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { + package func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { switch event { case let event as NIOHTTP2StreamCreatedEvent: self._streamCreated(event.streamID, channel: context.channel) @@ -162,7 +161,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl context.fireUserInboundEventTriggered(event) } - func errorCaught(context: ChannelHandlerContext, error: any Error) { + package func errorCaught(context: ChannelHandlerContext, error: any Error) { // Store the error and close, this will result in the final close event being fired down // the pipeline with an appropriate close reason and appropriate error. (This avoids // the async channel just throwing the error.) @@ -170,7 +169,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl context.close(mode: .all, promise: nil) } - func channelRead(context: ChannelHandlerContext, data: NIOAny) { + package func channelRead(context: ChannelHandlerContext, data: NIOAny) { let frame = self.unwrapInboundIn(data) self.inReadLoop = true @@ -229,7 +228,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl } } - func channelReadComplete(context: ChannelHandlerContext) { + package func channelReadComplete(context: ChannelHandlerContext) { while self.flushPending { self.flushPending = false context.flush() @@ -239,7 +238,7 @@ final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandl context.fireChannelReadComplete() } - func triggerUserOutboundEvent( + package func triggerUserOutboundEvent( context: ChannelHandlerContext, event: Any, promise: EventLoopPromise? @@ -294,7 +293,7 @@ extension ClientConnectionHandler { } extension ClientConnectionHandler { - struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { + package struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { // @unchecked is okay: the only methods do the appropriate event-loop dance. private let handler: ClientConnectionHandler @@ -303,7 +302,7 @@ extension ClientConnectionHandler { self.handler = handler } - func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { + package func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { if self.handler.eventLoop.inEventLoop { self.handler._streamCreated(id, channel: channel) } else { @@ -313,7 +312,7 @@ extension ClientConnectionHandler { } } - func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { + package func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { if self.handler.eventLoop.inEventLoop { self.handler._streamClosed(id, channel: channel) } else { @@ -324,7 +323,7 @@ extension ClientConnectionHandler { } } - var http2StreamDelegate: HTTP2StreamDelegate { + package var http2StreamDelegate: HTTP2StreamDelegate { return HTTP2StreamDelegate(self) } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index 8c7c88908..9bd3f97bd 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -44,9 +44,9 @@ import NIOHTTP2 /// } /// ``` @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -struct Connection: Sendable { +package struct Connection: Sendable { /// Events which can happen over the lifetime of the connection. - enum Event: Sendable { + package enum Event: Sendable { /// The connect attempt succeeded and the connection is ready to use. case connectSucceeded /// The connect attempt failed. @@ -59,7 +59,7 @@ struct Connection: Sendable { } /// The reason the connection closed. - enum CloseReason: Sendable { + package enum CloseReason: Sendable { /// Closed because an idle timeout fired. case idleTimeout /// Closed because a keepalive timer fired. @@ -104,11 +104,11 @@ struct Connection: Sendable { } /// A stream of events which can happen to the connection. - var events: AsyncStream { + package var events: AsyncStream { self.event.stream } - init( + package init( address: SocketAddress, http2Connector: any HTTP2Connector, defaultCompression: CompressionAlgorithm, @@ -127,7 +127,7 @@ struct Connection: Sendable { /// /// This function returns when the connection has closed. You can observe connection events /// by consuming the ``events`` sequence. - func run() async { + package func run() async { let connectResult = await Result { try await self.http2Connector.establishConnection(to: self.address) } @@ -168,7 +168,7 @@ struct Connection: Sendable { } /// Gracefully close the connection. - func close() { + package func close() { self.input.continuation.yield(.close) } @@ -176,7 +176,10 @@ struct Connection: Sendable { /// /// - Parameter descriptor: A descriptor of the method to create a stream for. /// - Returns: The open stream. - func makeStream(descriptor: MethodDescriptor, options: CallOptions) async throws -> Stream { + package func makeStream( + descriptor: MethodDescriptor, + options: CallOptions + ) async throws -> Stream { let (multiplexer, scheme) = try self.state.withLockedValue { state in switch state { case .connected(let connected): @@ -349,11 +352,11 @@ struct Connection: Sendable { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Connection { - struct Stream { - typealias Inbound = NIOAsyncChannelInboundStream + package struct Stream { + package typealias Inbound = NIOAsyncChannelInboundStream - struct Outbound: ClosableRPCWriterProtocol { - typealias Element = RPCRequestPart + package struct Outbound: ClosableRPCWriterProtocol { + package typealias Element = RPCRequestPart private let requestWriter: NIOAsyncChannelOutboundWriter private let http2Stream: NIOAsyncChannel @@ -366,19 +369,19 @@ extension Connection { self.http2Stream = http2Stream } - func write(_ element: RPCRequestPart) async throws { + package func write(_ element: RPCRequestPart) async throws { try await self.requestWriter.write(element) } - func write(contentsOf elements: some Sequence) async throws { + package func write(contentsOf elements: some Sequence) async throws { try await self.requestWriter.write(contentsOf: elements) } - func finish() { + package func finish() { self.requestWriter.finish() } - func finish(throwing error: any Error) { + package func finish(throwing error: any Error) { // Fire the error inbound; this fails the inbound writer. self.http2Stream.channel.pipeline.fireErrorCaught(error) } @@ -396,7 +399,7 @@ extension Connection { self.descriptor = descriptor } - func execute( + package func execute( _ closure: (_ inbound: Inbound, _ outbound: Outbound) async throws -> T ) async throws -> T where T: Sendable { try await self.http2Stream.executeThenClose { inbound, outbound in @@ -428,7 +431,7 @@ extension Connection { /// Multiplexer for creating HTTP/2 streams. var multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer /// Whether the connection is plaintext, `false` implies TLS is being used. - var scheme: GRPCStreamStateMachineConfiguration.Scheme + var scheme: Scheme init(_ connection: HTTP2Connection) { self.channel = connection.channel diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift index 49feb2282..8e0ed5e66 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift @@ -15,26 +15,26 @@ */ @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -struct ConnectionBackoff { - var initial: Duration - var max: Duration - var multiplier: Double - var jitter: Double +package struct ConnectionBackoff { + package var initial: Duration + package var max: Duration + package var multiplier: Double + package var jitter: Double - init(initial: Duration, max: Duration, multiplier: Double, jitter: Double) { + package init(initial: Duration, max: Duration, multiplier: Double, jitter: Double) { self.initial = initial self.max = max self.multiplier = multiplier self.jitter = jitter } - func makeIterator() -> Iterator { + package func makeIterator() -> Iterator { return Iterator(self) } // Deliberately not conforming to `IteratorProtocol` as `next()` never returns `nil` which // isn't expressible via `IteratorProtocol`. - struct Iterator { + package struct Iterator { private var isInitial: Bool private var currentBackoffSeconds: Double @@ -62,7 +62,7 @@ struct ConnectionBackoff { return .nanoseconds(wholeNanos) } - mutating func next() -> Duration { + package mutating func next() -> Duration { // The initial backoff doesn't get jittered. if self.isInitial { self.isInitial = false diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift index 63612ad69..57225bc67 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift @@ -18,15 +18,13 @@ import NIOCore import NIOHTTP2 import NIOPosix -@_spi(Package) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol HTTP2Connector: Sendable { +package protocol HTTP2Connector: Sendable { func establishConnection(to address: SocketAddress) async throws -> HTTP2Connection } -@_spi(Package) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct HTTP2Connection: Sendable { +package struct HTTP2Connection: Sendable { /// The underlying TCP connection wrapped up for use with gRPC. var channel: NIOAsyncChannel @@ -36,7 +34,7 @@ public struct HTTP2Connection: Sendable { /// Whether the connection is insecure (i.e. plaintext). var isPlaintext: Bool - public init( + package init( channel: NIOAsyncChannel, multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer, isPlaintext: Bool diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift index da471c4c1..6f4b000ca 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -enum ConnectivityState: Sendable, Hashable { +package enum ConnectivityState: Sendable, Hashable { /// This channel isn't trying to create a connection because of a lack of new or pending RPCs. /// /// New streams may be created in this state. Doing so will cause the channel to enter the diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 6e7d57145..36fb2cfc0 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -16,11 +16,10 @@ import Atomics import DequeModule -@_spi(Package) import GRPCCore +import GRPCCore @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -@_spi(Package) -public struct GRPCChannel: ClientTransport { +package struct GRPCChannel: ClientTransport { private enum Input: Sendable { /// Close the channel, if possible. case close @@ -44,7 +43,7 @@ public struct GRPCChannel: ClientTransport { private let resolver: NameResolver /// The state of the channel. - private let state: _LockedValueBox + private let state: LockedValueBox /// The maximum number of times to attempt to create a stream per RPC. /// @@ -69,18 +68,17 @@ public struct GRPCChannel: ClientTransport { private let defaultServiceConfig: ServiceConfig // These are both read frequently and updated infrequently so may be a bottleneck. - private let _methodConfig: _LockedValueBox<_MethodConfigs> - private let _retryThrottle: _LockedValueBox + private let _methodConfig: LockedValueBox + private let _retryThrottle: LockedValueBox - @_spi(Package) - public init( + package init( resolver: NameResolver, connector: any HTTP2Connector, config: Config, defaultServiceConfig: ServiceConfig ) { self.resolver = resolver - self.state = _LockedValueBox(StateMachine()) + self.state = LockedValueBox(StateMachine()) self._connectivityState = AsyncStream.makeStream() self.input = AsyncStream.makeStream() self.connector = connector @@ -96,19 +94,19 @@ public struct GRPCChannel: ClientTransport { self.defaultServiceConfig = defaultServiceConfig let throttle = defaultServiceConfig.retryThrottling.map { RetryThrottle(policy: $0) } - self._retryThrottle = _LockedValueBox(throttle) + self._retryThrottle = LockedValueBox(throttle) - let methodConfig = _MethodConfigs(serviceConfig: defaultServiceConfig) - self._methodConfig = _LockedValueBox(methodConfig) + let methodConfig = MethodConfigs(serviceConfig: defaultServiceConfig) + self._methodConfig = LockedValueBox(methodConfig) } /// The connectivity state of the channel. - var connectivityState: AsyncStream { + package var connectivityState: AsyncStream { self._connectivityState.stream } /// Returns a throttle which gRPC uses to determine whether retries can be executed. - public var retryThrottle: RetryThrottle? { + package var retryThrottle: RetryThrottle? { self._retryThrottle.withLockedValue { $0 } } @@ -116,12 +114,12 @@ public struct GRPCChannel: ClientTransport { /// /// - Parameter descriptor: The method to lookup configuration for. /// - Returns: Configuration for the method, if it exists. - public func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { + package func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { self._methodConfig.withLockedValue { $0[descriptor] } } /// Establishes and maintains a connection to the remote destination. - public func connect() async { + package func connect() async { self.state.withLockedValue { $0.start() } self._connectivityState.continuation.yield(.idle) @@ -184,12 +182,12 @@ public struct GRPCChannel: ClientTransport { /// Signal to the transport that no new streams may be created and that connections should be /// closed when all streams are closed. - public func close() { + package func close() { self.input.continuation.yield(.close) } /// Opens a stream using the transport, and uses it as input into a user-provided closure. - public func withStream( + package func withStream( descriptor: MethodDescriptor, options: CallOptions, _ closure: (_ stream: RPCStream) async throws -> T @@ -229,22 +227,20 @@ public struct GRPCChannel: ClientTransport { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension GRPCChannel { - @_spi(Package) - public struct Config: Sendable { + package struct Config: Sendable { /// Configuration for HTTP/2 connections. - var http2: HTTP2ClientTransport.Config.HTTP2 + package var http2: HTTP2ClientTransport.Config.HTTP2 /// Configuration for backoff used when establishing a connection. - var backoff: HTTP2ClientTransport.Config.Backoff + package var backoff: HTTP2ClientTransport.Config.Backoff /// Configuration for connection management. - var connection: HTTP2ClientTransport.Config.Connection + package var connection: HTTP2ClientTransport.Config.Connection /// Compression configuration. - var compression: HTTP2ClientTransport.Config.Compression + package var compression: HTTP2ClientTransport.Config.Compression - @_spi(Package) - public init( + package init( http2: HTTP2ClientTransport.Config.HTTP2, backoff: HTTP2ClientTransport.Config.Backoff, connection: HTTP2ClientTransport.Config.Connection, @@ -386,7 +382,7 @@ extension GRPCChannel { switch result.serviceConfig ?? .success(self.defaultServiceConfig) { case .success(let config): // Update per RPC configuration. - let methodConfig = _MethodConfigs(serviceConfig: config) + let methodConfig = MethodConfigs(serviceConfig: config) self._methodConfig.withLockedValue { $0 = methodConfig } let retryThrottle = config.retryThrottling.map { RetryThrottle(policy: $0) } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift index f5465178f..f6916e3c7 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift @@ -15,14 +15,14 @@ */ @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -enum LoadBalancer: Sendable { +package enum LoadBalancer: Sendable { case roundRobin(RoundRobinLoadBalancer) case pickFirst(PickFirstLoadBalancer) } @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension LoadBalancer { - init(_ loadBalancer: RoundRobinLoadBalancer) { + package init(_ loadBalancer: RoundRobinLoadBalancer) { self = .roundRobin(loadBalancer) } @@ -35,7 +35,7 @@ extension LoadBalancer { } } - var events: AsyncStream { + package var events: AsyncStream { switch self { case .roundRobin(let loadBalancer): return loadBalancer.events @@ -44,7 +44,7 @@ extension LoadBalancer { } } - func run() async { + package func run() async { switch self { case .roundRobin(let loadBalancer): await loadBalancer.run() @@ -53,7 +53,7 @@ extension LoadBalancer { } } - func close() { + package func close() { switch self { case .roundRobin(let loadBalancer): loadBalancer.close() @@ -62,7 +62,7 @@ extension LoadBalancer { } } - func pickSubchannel() -> Subchannel? { + package func pickSubchannel() -> Subchannel? { switch self { case .roundRobin(let loadBalancer): return loadBalancer.pickSubchannel() diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift index 466f7b675..439471ac6 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift @@ -15,7 +15,7 @@ */ /// Events emitted by load-balancers. -enum LoadBalancerEvent: Sendable, Hashable { +package enum LoadBalancerEvent: Sendable, Hashable { /// The connectivity state of the subchannel changed. case connectivityStateChanged(ConnectivityState) /// The subchannel requests that the load balancer re-resolves names. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift index 203c74742..d654fcff1 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift @@ -56,7 +56,7 @@ import GRPCCore /// } /// ``` @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -struct PickFirstLoadBalancer { +package struct PickFirstLoadBalancer { enum Input: Sendable, Hashable { /// Update the addresses used by the load balancer to the following endpoints. case updateEndpoint(Endpoint) @@ -87,12 +87,12 @@ struct PickFirstLoadBalancer { private let enabledCompression: CompressionAlgorithmSet /// The state of the load-balancer. - private let state: _LockedValueBox + private let state: LockedValueBox /// The ID of this load balancer. internal let id: LoadBalancerID - init( + package init( connector: any HTTP2Connector, backoff: ConnectionBackoff, defaultCompression: CompressionAlgorithm, @@ -103,7 +103,7 @@ struct PickFirstLoadBalancer { self.defaultCompression = defaultCompression self.enabledCompression = enabledCompression self.id = LoadBalancerID() - self.state = _LockedValueBox(State()) + self.state = LockedValueBox(State()) self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) self.input = AsyncStream.makeStream(of: Input.self) @@ -112,14 +112,14 @@ struct PickFirstLoadBalancer { } /// A stream of events which can happen to the load balancer. - var events: AsyncStream { + package var events: AsyncStream { self.event.stream } /// Runs the load balancer, returning when it has closed. /// /// You can monitor events which happen on the load balancer with ``events``. - func run() async { + package func run() async { await withDiscardingTaskGroup { group in for await input in self.input.stream { switch input { @@ -140,19 +140,19 @@ struct PickFirstLoadBalancer { /// Update the addresses used by the load balancer. /// /// This may result in new subchannels being created and some subchannels being removed. - func updateEndpoint(_ endpoint: Endpoint) { + package func updateEndpoint(_ endpoint: Endpoint) { self.input.continuation.yield(.updateEndpoint(endpoint)) } /// Close the load balancer, and all subchannels it manages. - func close() { + package func close() { self.input.continuation.yield(.close) } /// Pick a ready subchannel from the load balancer. /// /// - Returns: A subchannel, or `nil` if there aren't any ready subchannels. - func pickSubchannel() -> Subchannel? { + package func pickSubchannel() -> Subchannel? { let onPickSubchannel = self.state.withLockedValue { $0.pickSubchannel() } switch onPickSubchannel { case .picked(let subchannel): diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift index 8039ca655..b6c838955 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -58,7 +58,7 @@ import GRPCCore /// } /// ``` @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -struct RoundRobinLoadBalancer { +package struct RoundRobinLoadBalancer { enum Input: Sendable, Hashable { /// Update the addresses used by the load balancer to the following endpoints. case updateAddresses([Endpoint]) @@ -103,7 +103,7 @@ struct RoundRobinLoadBalancer { private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) /// The state of the load balancer. - private let state: _LockedValueBox + private let state: LockedValueBox /// A connector, capable of creating connections. private let connector: any HTTP2Connector @@ -120,7 +120,7 @@ struct RoundRobinLoadBalancer { /// The ID of this load balancer. internal let id: LoadBalancerID - init( + package init( connector: any HTTP2Connector, backoff: ConnectionBackoff, defaultCompression: CompressionAlgorithm, @@ -134,21 +134,21 @@ struct RoundRobinLoadBalancer { self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) self.input = AsyncStream.makeStream(of: Input.self) - self.state = _LockedValueBox(.active(State.Active())) + self.state = LockedValueBox(.active(State.Active())) // The load balancer starts in the idle state. self.event.continuation.yield(.connectivityStateChanged(.idle)) } /// A stream of events which can happen to the load balancer. - var events: AsyncStream { + package var events: AsyncStream { self.event.stream } /// Runs the load balancer, returning when it has closed. /// /// You can monitor events which happen on the load balancer with ``events``. - func run() async { + package func run() async { await withDiscardingTaskGroup { group in for await input in self.input.stream { switch input { @@ -169,19 +169,19 @@ struct RoundRobinLoadBalancer { /// Update the addresses used by the load balancer. /// /// This may result in new subchannels being created and some subchannels being removed. - func updateAddresses(_ endpoints: [Endpoint]) { + package func updateAddresses(_ endpoints: [Endpoint]) { self.input.continuation.yield(.updateAddresses(endpoints)) } /// Close the load balancer, and all subchannels it manages. - func close() { + package func close() { self.input.continuation.yield(.close) } /// Pick a ready subchannel from the load balancer. /// /// - Returns: A subchannel, or `nil` if there aren't any ready subchannels. - func pickSubchannel() -> Subchannel? { + package func pickSubchannel() -> Subchannel? { switch self.state.withLockedValue({ $0.pickSubchannel() }) { case .picked(let subchannel): return subchannel diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift index 491b54705..c4e122300 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift @@ -44,8 +44,8 @@ import NIOConcurrencyHelpers /// } /// ``` @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -struct Subchannel { - enum Event: Sendable, Hashable { +package struct Subchannel { + package enum Event: Sendable, Hashable { /// The connection received a GOAWAY and will close soon. No new streams /// should be opened on this connection. case goingAway @@ -79,7 +79,7 @@ struct Subchannel { let endpoint: Endpoint /// The ID of the subchannel. - let id: SubchannelID + package let id: SubchannelID /// A factory for connections. private let connector: any HTTP2Connector @@ -93,7 +93,7 @@ struct Subchannel { /// The set of enabled compression algorithms. private let enabledCompression: CompressionAlgorithmSet - init( + package init( endpoint: Endpoint, id: SubchannelID, connector: any HTTP2Connector, @@ -120,7 +120,7 @@ struct Subchannel { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Subchannel { /// A stream of events which can happen to the subchannel. - var events: AsyncStream { + package var events: AsyncStream { self.event.stream } @@ -132,7 +132,7 @@ extension Subchannel { /// failure state. /// /// Events and state changes can be observed via the ``events`` stream. - func run() async { + package func run() async { await withDiscardingTaskGroup { group in for await input in self.input.stream { switch input { @@ -156,12 +156,12 @@ extension Subchannel { } /// Initiate a connection attempt, if possible. - func connect() { + package func connect() { self.input.continuation.yield(.connect) } /// Initiates graceful shutdown, if possible. - func shutDown() { + package func shutDown() { self.input.continuation.yield(.shutDown) } @@ -169,7 +169,7 @@ extension Subchannel { /// /// - Parameter descriptor: A descriptor of the method to create a stream for. /// - Returns: The open stream. - func makeStream( + package func makeStream( descriptor: MethodDescriptor, options: CallOptions ) async throws -> Connection.Stream { diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index 073dcc058..b219a517e 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -33,7 +33,7 @@ final class GRPCClientStreamHandler: ChannelDuplexHandler { init( methodDescriptor: MethodDescriptor, - scheme: GRPCStreamStateMachineConfiguration.Scheme, + scheme: Scheme, outboundEncoding: CompressionAlgorithm, acceptedEncodings: CompressionAlgorithmSet, maximumPayloadSize: Int, diff --git a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift index 7401fc8c3..7988549d1 100644 --- a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift +++ b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@_spi(Package) import GRPCCore +import GRPCCore extension CompressionAlgorithm { init?(name: String) { diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index c8dada299..4ab206bd9 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -19,15 +19,15 @@ import NIOCore import NIOHPACK import NIOHTTP1 +package enum Scheme: String { + case http + case https +} + enum GRPCStreamStateMachineConfiguration { case client(ClientConfiguration) case server(ServerConfiguration) - enum Scheme: String { - case http - case https - } - struct ClientConfiguration { var methodDescriptor: MethodDescriptor var scheme: Scheme @@ -631,7 +631,7 @@ struct GRPCStreamStateMachine { extension GRPCStreamStateMachine { private func makeClientHeaders( methodDescriptor: MethodDescriptor, - scheme: GRPCStreamStateMachineConfiguration.Scheme, + scheme: Scheme, outboundEncoding: CompressionAlgorithm?, acceptedEncodings: CompressionAlgorithmSet, customMetadata: Metadata @@ -1437,8 +1437,7 @@ extension GRPCStreamStateMachine { ) } - let scheme = headers.firstString(forKey: .scheme) - .flatMap { GRPCStreamStateMachineConfiguration.Scheme(rawValue: $0) } + let scheme = headers.firstString(forKey: .scheme).flatMap { Scheme(rawValue: $0) } if scheme == nil { self.state = closeServer(from: state, endStream: endStream) return .rejectRPC_serverOnly( diff --git a/Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift b/Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift deleted file mode 100644 index 5f1b75dd6..000000000 --- a/Sources/GRPCHTTP2Core/Internal/AsyncStream+MakeStream.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - @inlinable - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} -#endif diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index de4d4b68a..653615b35 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -21,13 +21,12 @@ import NIOHTTP2 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ChannelPipeline.SynchronousOperations { - @_spi(Package) public typealias HTTP2ConnectionChannel = NIOAsyncChannel - @_spi(Package) public typealias HTTP2StreamMultiplexer = NIOHTTP2Handler.AsyncStreamMultiplexer< + package typealias HTTP2ConnectionChannel = NIOAsyncChannel + package typealias HTTP2StreamMultiplexer = NIOHTTP2Handler.AsyncStreamMultiplexer< (NIOAsyncChannel, EventLoopFuture) > - @_spi(Package) - public func configureGRPCServerPipeline( + package func configureGRPCServerPipeline( channel: any Channel, compressionConfig: HTTP2ServerTransport.Config.Compression, connectionConfig: HTTP2ServerTransport.Config.Connection, @@ -104,9 +103,8 @@ extension ChannelPipeline.SynchronousOperations { } extension ChannelPipeline.SynchronousOperations { - @_spi(Package) @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) - public func configureGRPCClientPipeline( + package func configureGRPCClientPipeline( channel: any Channel, config: GRPCChannel.Config ) throws -> ( diff --git a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift index b506bc4d8..cf015466a 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift @@ -16,9 +16,8 @@ import NIOCore -@_spi(Package) extension GRPCHTTP2Core.SocketAddress { - public init(_ nioSocketAddress: NIOCore.SocketAddress) { + package init(_ nioSocketAddress: NIOCore.SocketAddress) { switch nioSocketAddress { case .v4(let address): self = .ipv4( @@ -38,17 +37,16 @@ extension GRPCHTTP2Core.SocketAddress { } } -@_spi(Package) extension NIOCore.SocketAddress { - public init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { + package init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { try self.init(ipAddress: address.host, port: address.port) } - public init(_ address: GRPCHTTP2Core.SocketAddress.IPv6) throws { + package init(_ address: GRPCHTTP2Core.SocketAddress.IPv6) throws { try self.init(ipAddress: address.host, port: address.port) } - public init(_ address: GRPCHTTP2Core.SocketAddress.UnixDomainSocket) throws { + package init(_ address: GRPCHTTP2Core.SocketAddress.UnixDomainSocket) throws { try self.init(unixDomainSocketPath: address.path) } } diff --git a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift index 95a6564a2..7c9bbba55 100644 --- a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift +++ b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift @@ -31,9 +31,10 @@ struct ProcessUniqueID: Hashable, Sendable, CustomStringConvertible { } /// A process-unique ID for a subchannel. -struct SubchannelID: Hashable, Sendable, CustomStringConvertible { +package struct SubchannelID: Hashable, Sendable, CustomStringConvertible { private let id = ProcessUniqueID() - var description: String { + package init() {} + package var description: String { "subchan_\(self.id)" } } diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift index 2279bd922..2997580df 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift @@ -20,14 +20,13 @@ import NIOCore @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public enum ServerConnection { public enum Stream { - @_spi(Package) - public struct Outbound: ClosableRPCWriterProtocol { - public typealias Element = RPCResponsePart + package struct Outbound: ClosableRPCWriterProtocol { + package typealias Element = RPCResponsePart private let responseWriter: NIOAsyncChannelOutboundWriter private let http2Stream: NIOAsyncChannel - public init( + package init( responseWriter: NIOAsyncChannelOutboundWriter, http2Stream: NIOAsyncChannel ) { @@ -35,19 +34,19 @@ public enum ServerConnection { self.http2Stream = http2Stream } - public func write(_ element: RPCResponsePart) async throws { + package func write(_ element: RPCResponsePart) async throws { try await self.responseWriter.write(element) } - public func write(contentsOf elements: some Sequence) async throws { + package func write(contentsOf elements: some Sequence) async throws { try await self.responseWriter.write(contentsOf: elements) } - public func finish() { + package func finish() { self.responseWriter.finish() } - public func finish(throwing error: any Error) { + package func finish(throwing error: any Error) { // Fire the error inbound; this fails the inbound writer. self.http2Stream.channel.pipeline.fireErrorCaught(error) } diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 125ee604b..d0f17601e 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -19,12 +19,12 @@ import NIOCore import NIOHTTP2 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandler { - typealias InboundIn = HTTP2Frame.FramePayload - typealias InboundOut = RPCRequestPart +package final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandler { + package typealias InboundIn = HTTP2Frame.FramePayload + package typealias InboundOut = RPCRequestPart - typealias OutboundIn = RPCResponsePart - typealias OutboundOut = HTTP2Frame.FramePayload + package typealias OutboundIn = RPCResponsePart + package typealias OutboundOut = HTTP2Frame.FramePayload private var stateMachine: GRPCStreamStateMachine @@ -46,8 +46,8 @@ final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandl message: "RPC stream was closed before we got any Metadata." ) - init( - scheme: GRPCStreamStateMachineConfiguration.Scheme, + package init( + scheme: Scheme, acceptedEncodings: CompressionAlgorithmSet, maximumPayloadSize: Int, methodDescriptorPromise: EventLoopPromise, @@ -66,7 +66,7 @@ final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandl @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCServerStreamHandler { - func channelRead(context: ChannelHandlerContext, data: NIOAny) { + package func channelRead(context: ChannelHandlerContext, data: NIOAny) { self.isReading = true let frame = self.unwrapInboundIn(data) switch frame { @@ -159,7 +159,7 @@ extension GRPCServerStreamHandler { } } - func channelReadComplete(context: ChannelHandlerContext) { + package func channelReadComplete(context: ChannelHandlerContext) { self.isReading = false if self.flushPending { self.flushPending = false @@ -168,17 +168,17 @@ extension GRPCServerStreamHandler { context.fireChannelReadComplete() } - func handlerRemoved(context: ChannelHandlerContext) { + package func handlerRemoved(context: ChannelHandlerContext) { self.stateMachine.tearDown() self.methodDescriptorPromise.fail(Self.handlerRemovedBeforeDescriptorResolved) } - func channelInactive(context: ChannelHandlerContext) { + package func channelInactive(context: ChannelHandlerContext) { self.handleUnexpectedInboundClose(context: context, reason: .channelInactive) context.fireChannelInactive() } - func errorCaught(context: ChannelHandlerContext, error: any Error) { + package func errorCaught(context: ChannelHandlerContext, error: any Error) { self.handleUnexpectedInboundClose(context: context, reason: .errorThrown(error)) } @@ -203,7 +203,11 @@ extension GRPCServerStreamHandler { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCServerStreamHandler { - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { + package func write( + context: ChannelHandlerContext, + data: NIOAny, + promise: EventLoopPromise? + ) { let frame = self.unwrapOutboundIn(data) switch frame { case .metadata(let metadata): @@ -239,7 +243,7 @@ extension GRPCServerStreamHandler { } } - func flush(context: ChannelHandlerContext) { + package func flush(context: ChannelHandlerContext) { if self.isReading { // We don't want to flush yet if we're still in a read loop. return diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index 74b82a587..303cbea0c 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@_spi(Package) import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOPosix diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 95d7f7e59..ea73cc126 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@_spi(Package) import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOExtras import NIOPosix @@ -124,7 +124,7 @@ extension HTTP2ServerTransport { } } - private let listeningAddressState: _LockedValueBox + private let listeningAddressState: LockedValueBox /// The listening address for this server transport. /// @@ -158,7 +158,7 @@ extension HTTP2ServerTransport { self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) let eventLoop = eventLoopGroup.any() - self.listeningAddressState = _LockedValueBox(.idle(eventLoop.makePromise())) + self.listeningAddressState = LockedValueBox(.idle(eventLoop.makePromise())) } public func listen( diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift index d81c92c06..6a056abed 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@_spi(Package) import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOPosix diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 93c596cbb..169116d62 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -16,7 +16,7 @@ #if canImport(Network) import GRPCCore -@_spi(Package) import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOExtras import NIOTransportServices @@ -118,7 +118,7 @@ extension HTTP2ServerTransport { } } - private let listeningAddressState: _LockedValueBox + private let listeningAddressState: LockedValueBox /// The listening address for this server transport. /// @@ -152,7 +152,7 @@ extension HTTP2ServerTransport { self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) let eventLoop = eventLoopGroup.any() - self.listeningAddressState = _LockedValueBox(.idle(eventLoop.makePromise())) + self.listeningAddressState = LockedValueBox(.idle(eventLoop.makePromise())) } public func listen( diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index d7fb93bd8..3667640ad 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -98,8 +98,8 @@ public struct InProcessClientTransport: ClientTransport { public let retryThrottle: RetryThrottle? - private let methodConfig: _MethodConfigs - private let state: _LockedValueBox + private let methodConfig: MethodConfigs + private let state: LockedValueBox /// Creates a new in-process client transport. /// @@ -111,8 +111,8 @@ public struct InProcessClientTransport: ClientTransport { serviceConfig: ServiceConfig = ServiceConfig() ) { self.retryThrottle = serviceConfig.retryThrottling.map { RetryThrottle(policy: $0) } - self.methodConfig = _MethodConfigs(serviceConfig: serviceConfig) - self.state = _LockedValueBox(.unconnected(.init(serverTransport: server))) + self.methodConfig = MethodConfigs(serviceConfig: serviceConfig) + self.state = LockedValueBox(.unconnected(.init(serverTransport: server))) } /// Establish and maintain a connection to the remote destination. diff --git a/Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift b/Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift deleted file mode 100644 index 68b492869..000000000 --- a/Sources/GRPCInProcessTransport/Internal/AsyncStream+MakeStream.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - @inlinable - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} -#endif diff --git a/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift b/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift deleted file mode 100644 index 5f1b75dd6..000000000 --- a/Sources/InteroperabilityTests/Internal/AsyncStream+MakeStream.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - @inlinable - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} -#endif diff --git a/Sources/performance-worker/Internal/AsyncStream+MakeStream.swift b/Sources/performance-worker/Internal/AsyncStream+MakeStream.swift deleted file mode 100644 index 5f1b75dd6..000000000 --- a/Sources/performance-worker/Internal/AsyncStream+MakeStream.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - @inlinable - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} -#endif diff --git a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift index 4bf2b1113..cbbcb9d77 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -25,7 +25,7 @@ final class MethodConfigsTests: XCTestCase { nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) - var configurations = _MethodConfigs() + var configurations = MethodConfigs() configurations.setDefaultConfiguration(defaultConfiguration) let descriptor = MethodDescriptor(service: "test", method: "first") let retryPolicy = RetryPolicy( @@ -48,7 +48,7 @@ final class MethodConfigsTests: XCTestCase { nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) - var configurations = _MethodConfigs() + var configurations = MethodConfigs() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test", method: "") let retryPolicy = RetryPolicy( @@ -72,7 +72,7 @@ final class MethodConfigsTests: XCTestCase { nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) - var configurations = _MethodConfigs() + var configurations = MethodConfigs() configurations.setDefaultConfiguration(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test1", method: "first") let retryPolicy = RetryPolicy( diff --git a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift index 87bc7f75d..b82b7185e 100644 --- a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift @@ -20,36 +20,3 @@ extension AsyncSequence { return try await self.reduce(into: []) { $0.append($1) } } } - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncThrowingStream { - static func makeStream( - of elementType: Element.Type = Element.self, - throwing failureType: Failure.Type = Failure.self, - bufferingPolicy limit: AsyncThrowingStream.Continuation.BufferingPolicy = - .unbounded - ) -> ( - stream: AsyncThrowingStream, - continuation: AsyncThrowingStream.Continuation - ) where Failure == Error { - var continuation: AsyncThrowingStream.Continuation! - let stream = AsyncThrowingStream(bufferingPolicy: limit) { continuation = $0 } - return (stream, continuation!) - } -} -#endif diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 77350d149..54d534a8c 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOEmbedded import NIOHTTP2 diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift index 8e912639c..1e043dba3 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core // Equatable conformance for these types is 'best effort', this is sufficient for testing but not // for general use. As such the conformance is added in the test module and must be declared @@ -41,7 +41,7 @@ extension ClientConnectionEvent.CloseReason: Equatable {} @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Connection.Event { - public static func == (lhs: Connection.Event, rhs: Connection.Event) -> Bool { + package static func == (lhs: Connection.Event, rhs: Connection.Event) -> Bool { switch (lhs, rhs) { case (.connectSucceeded, .connectSucceeded), (.connectFailed, .connectFailed): @@ -61,7 +61,7 @@ extension Connection.Event { @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Connection.CloseReason { - public static func == (lhs: Connection.CloseReason, rhs: Connection.CloseReason) -> Bool { + package static func == (lhs: Connection.CloseReason, rhs: Connection.CloseReason) -> Bool { switch (lhs, rhs) { case (.idleTimeout, .idleTimeout), (.keepaliveTimeout, .keepaliveTimeout), @@ -83,7 +83,7 @@ extension Connection.CloseReason { } extension ClientConnectionEvent { - public static func == (lhs: ClientConnectionEvent, rhs: ClientConnectionEvent) -> Bool { + package static func == (lhs: ClientConnectionEvent, rhs: ClientConnectionEvent) -> Bool { switch (lhs, rhs) { case (.ready, .ready): return true @@ -96,7 +96,7 @@ extension ClientConnectionEvent { } extension ClientConnectionEvent.CloseReason { - public static func == (lhs: Self, rhs: Self) -> Bool { + package static func == (lhs: Self, rhs: Self) -> Bool { switch (lhs, rhs) { case (.goAway(let lhsCode, let lhsMessage), .goAway(let rhsCode, let rhsMessage)): return lhsCode == rhsCode && lhsMessage == rhsMessage diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift index 550d2a05c..a5e063468 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift @@ -16,7 +16,7 @@ import DequeModule import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOConcurrencyHelpers import NIOCore import NIOHPACK diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index 3a58dc054..d68a36914 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOHTTP2 import NIOPosix import XCTest diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift index bfb8908ff..e30ec996b 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import XCTest @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift index 958bba4a4..48b8e9e5c 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift @@ -16,7 +16,7 @@ import Atomics import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOHTTP2 import NIOPosix import XCTest diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift index c7060578f..9b155ef95 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift @@ -16,7 +16,7 @@ import Atomics import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOHTTP2 import NIOPosix import XCTest diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index 501fb9d27..d8c2bf41c 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOHTTP2 import NIOPosix @@ -39,10 +39,6 @@ final class SubchannelTests: XCTestCase { } func testMakeStreamOnShutdownSubchannel() async throws { - #if compiler(<5.9) - throw XCTSkip("Occasionally crashes due to a Swift 5.8 concurrency runtime bug") - #endif - let subchannel = self.makeSubchannel( address: .unixDomainSocket(path: "ignored"), connector: .never diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift index 712ab045b..cacf22708 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift @@ -45,7 +45,7 @@ final class RequestQueueTests: XCTestCase { func testPopFirstMultiple() async { await withTaskGroup(of: QueueEntryID.self) { group in - let queue = _LockedValueBox(RequestQueue()) + let queue = LockedValueBox(RequestQueue()) let signal1 = AsyncStream.makeStream(of: Void.self) let signal2 = AsyncStream.makeStream(of: Void.self) @@ -110,7 +110,7 @@ final class RequestQueueTests: XCTestCase { func testRemoveEntryByIDMultiple() async { await withTaskGroup(of: QueueEntryID.self) { group in - let queue = _LockedValueBox(RequestQueue()) + let queue = LockedValueBox(RequestQueue()) let signal1 = AsyncStream.makeStream(of: Void.self) let signal2 = AsyncStream.makeStream(of: Void.self) @@ -159,7 +159,7 @@ final class RequestQueueTests: XCTestCase { } func testRemoveFastFailingEntries() async throws { - let queue = _LockedValueBox(RequestQueue()) + let queue = LockedValueBox(RequestQueue()) let enqueued = AsyncStream.makeStream(of: Void.self) try await withThrowingTaskGroup(of: Void.self) { group in @@ -222,7 +222,7 @@ final class RequestQueueTests: XCTestCase { } func testRemoveAll() async throws { - let queue = _LockedValueBox(RequestQueue()) + let queue = LockedValueBox(RequestQueue()) let enqueued = AsyncStream.makeStream(of: Void.self) await withThrowingTaskGroup(of: Void.self) { group in diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift index 8dd2f0766..62509f4ad 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -16,7 +16,7 @@ import DequeModule import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOHTTP2 import NIOPosix diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift index 11782f5e6..4c08cb018 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift @@ -15,12 +15,11 @@ */ import GRPCCore -@_spi(Package) @testable import GRPCHTTP2Core +import GRPCHTTP2Core import NIOCore import NIOHTTP2 import NIOPosix -@_spi(Package) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension HTTP2Connector where Self == ThrowingConnector { /// A connector which throws the given error on a connect attempt. @@ -29,7 +28,6 @@ extension HTTP2Connector where Self == ThrowingConnector { } } -@_spi(Package) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension HTTP2Connector where Self == NeverConnector { /// A connector which fatal errors if a connect attempt is made. @@ -38,7 +36,6 @@ extension HTTP2Connector where Self == NeverConnector { } } -@_spi(Package) @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension HTTP2Connector where Self == NIOPosixConnector { /// A connector which uses NIOPosix to establish a connection. diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift deleted file mode 100644 index 68b492869..000000000 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/AsyncStream+MakeStream.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - @inlinable - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} -#endif diff --git a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift index 1b3a567e4..218541fa2 100644 --- a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift +++ b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift @@ -193,19 +193,3 @@ struct TestWriter: RPCWriterProtocol { } } } - -#if swift(<5.9) -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - static func makeStream( - of elementType: Element.Type = Element.self, - bufferingPolicy limit: AsyncStream.Continuation.BufferingPolicy = .unbounded - ) -> (stream: AsyncStream, continuation: AsyncStream.Continuation) { - var continuation: AsyncStream.Continuation! - let stream = AsyncStream(Element.self, bufferingPolicy: limit) { - continuation = $0 - } - return (stream, continuation) - } -} -#endif From 5241c584067d1f112304538aef98e9d2c9b81c71 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 10 Jul 2024 10:49:53 +0100 Subject: [PATCH 396/580] Avoid using NIOSingleStepByteToMessageProcessor (#1977) Motivation: NIOSingleStepByteToMessageProcessor is a class and therefore has an allocation cost. We can roll our own to avoid this cost. Modifications: - Rename 'GRPCMessageDeframer' tp 'GRPCMessageDecoder' (as it still conforms to NIOs decoder protocol) - Add a 'GRPCMessageDeframer' struct which has a slightly simpler API than the 'NIOSingleStepByteToMessageProcessor' but does essentially the same thing Result: Fewer allocations --- ...eframer.swift => GRPCMessageDecoder.swift} | 58 +++- .../GRPCStreamStateMachine.swift | 61 ++-- .../GRPCMessageDecoderTests.swift | 237 ++++++++++++++++ .../GRPCMessageDeframerTests.swift | 263 +++++------------- 4 files changed, 384 insertions(+), 235 deletions(-) rename Sources/GRPCHTTP2Core/{GRPCMessageDeframer.swift => GRPCMessageDecoder.swift} (70%) create mode 100644 Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift diff --git a/Sources/GRPCHTTP2Core/GRPCMessageDeframer.swift b/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift similarity index 70% rename from Sources/GRPCHTTP2Core/GRPCMessageDeframer.swift rename to Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift index af8282bb4..ca65e6942 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageDeframer.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift @@ -17,14 +17,13 @@ import GRPCCore import NIOCore -/// A ``GRPCMessageDeframer`` helps with the deframing of gRPC data frames: +/// A ``GRPCMessageDecoder`` helps with the deframing of gRPC data frames: /// - It reads the frame's metadata to know whether the message payload is compressed or not, and its length /// - It reads and decompresses the payload, if compressed /// - It helps put together frames that have been split across multiple `ByteBuffers` by the underlying transport -struct GRPCMessageDeframer: NIOSingleStepByteToMessageDecoder { +struct GRPCMessageDecoder: NIOSingleStepByteToMessageDecoder { /// Length of the gRPC message header (1 compression byte, 4 bytes for the length). static let metadataLength = 5 - static let defaultMaximumPayloadSize = Int.max typealias InboundOut = [UInt8] @@ -38,7 +37,7 @@ struct GRPCMessageDeframer: NIOSingleStepByteToMessageDecoder { /// - Important: You must call `end()` on the `decompressor` when you're done using it, to clean /// up any resources allocated by `Zlib`. init( - maximumPayloadSize: Int = Self.defaultMaximumPayloadSize, + maximumPayloadSize: Int, decompressor: Zlib.Decompressor? = nil ) { self.maximumPayloadSize = maximumPayloadSize @@ -101,3 +100,54 @@ struct GRPCMessageDeframer: NIOSingleStepByteToMessageDecoder { try self.decode(buffer: &buffer) } } + +package struct GRPCMessageDeframer { + private var decoder: GRPCMessageDecoder + private var buffer: Optional + + package var _readerIndex: Int? { + self.buffer?.readerIndex + } + + init(maxPayloadSize: Int, decompressor: Zlib.Decompressor?) { + self.decoder = GRPCMessageDecoder( + maximumPayloadSize: maxPayloadSize, + decompressor: decompressor + ) + self.buffer = nil + } + + package init(maxPayloadSize: Int) { + self.decoder = GRPCMessageDecoder(maximumPayloadSize: maxPayloadSize, decompressor: nil) + self.buffer = nil + } + + package mutating func append(_ buffer: ByteBuffer) { + if self.buffer == nil || self.buffer!.readableBytes == 0 { + self.buffer = buffer + } else { + // Avoid having too many read bytes in the buffer which can lead to the buffer growing much + // larger than is necessary. + let readerIndex = self.buffer!.readerIndex + if readerIndex > 1024 && readerIndex > (self.buffer!.capacity / 2) { + self.buffer!.discardReadBytes() + } + self.buffer!.writeImmutableBuffer(buffer) + } + } + + package mutating func decodeNext() throws -> [UInt8]? { + guard (self.buffer?.readableBytes ?? 0) > 0 else { return nil } + // Above checks mean this is both non-nil and non-empty. + let message = try self.decoder.decode(buffer: &self.buffer!) + return message + } +} + +extension GRPCMessageDeframer { + mutating func decode(into queue: inout OneOrManyQueue<[UInt8]>) throws { + while let next = try self.decodeNext() { + queue.append(next) + } + } +} diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 4ab206bd9..8d6d93f3b 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -82,7 +82,7 @@ private enum GRPCStreamStateMachineState { // until the server opens and sends a grpc-encoding header. // It will be present for the server though, because even though it's idle, // it can still receive compressed messages from the client. - let deframer: NIOSingleStepByteToMessageProcessor? + var deframer: GRPCMessageDeframer? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -97,7 +97,7 @@ private enum GRPCStreamStateMachineState { outboundCompression: CompressionAlgorithm, framer: GRPCMessageFramer, decompressor: Zlib.Decompressor?, - deframer: NIOSingleStepByteToMessageProcessor?, + deframer: GRPCMessageDeframer?, headers: HPACKHeaders ) { self.maximumPayloadSize = previousState.maximumPayloadSize @@ -116,7 +116,7 @@ private enum GRPCStreamStateMachineState { var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm - let deframer: NIOSingleStepByteToMessageProcessor + var deframer: GRPCMessageDeframer var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -127,7 +127,7 @@ private enum GRPCStreamStateMachineState { init( previousState: ClientOpenServerIdleState, - deframer: NIOSingleStepByteToMessageProcessor, + deframer: GRPCMessageDeframer, decompressor: Zlib.Decompressor? ) { self.framer = previousState.framer @@ -147,7 +147,7 @@ private enum GRPCStreamStateMachineState { var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm - let deframer: NIOSingleStepByteToMessageProcessor? + let deframer: GRPCMessageDeframer? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -199,7 +199,7 @@ private enum GRPCStreamStateMachineState { var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm - let deframer: NIOSingleStepByteToMessageProcessor? + let deframer: GRPCMessageDeframer? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -269,7 +269,7 @@ private enum GRPCStreamStateMachineState { var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm - let deframer: NIOSingleStepByteToMessageProcessor? + var deframer: GRPCMessageDeframer? var decompressor: Zlib.Decompressor? var inboundMessageBuffer: OneOrManyQueue<[UInt8]> @@ -317,11 +317,11 @@ private enum GRPCStreamStateMachineState { if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { self.decompressor = Zlib.Decompressor(method: zlibMethod) } - let decoder = GRPCMessageDeframer( - maximumPayloadSize: previousState.maximumPayloadSize, + + self.deframer = GRPCMessageDeframer( + maxPayloadSize: previousState.maximumPayloadSize, decompressor: self.decompressor ) - self.deframer = NIOSingleStepByteToMessageProcessor(decoder) self.inboundMessageBuffer = previousState.inboundMessageBuffer self.headers = previousState.headers @@ -976,15 +976,14 @@ extension GRPCStreamStateMachine { case .success(let inboundEncoding): let decompressor = Zlib.Method(encoding: inboundEncoding) .flatMap { Zlib.Decompressor(method: $0) } - let deframer = GRPCMessageDeframer( - maximumPayloadSize: state.maximumPayloadSize, - decompressor: decompressor - ) self.state = .clientOpenServerOpen( .init( previousState: state, - deframer: NIOSingleStepByteToMessageProcessor(deframer), + deframer: GRPCMessageDeframer( + maxPayloadSize: state.maximumPayloadSize, + decompressor: decompressor + ), decompressor: decompressor ) ) @@ -1103,10 +1102,10 @@ extension GRPCStreamStateMachine { ) } + state.deframer.append(buffer) + do { - try state.deframer.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) - } + try state.deframer.decode(into: &state.inboundMessageBuffer) self.state = .clientOpenServerOpen(state) return .readInbound } catch { @@ -1134,9 +1133,8 @@ extension GRPCStreamStateMachine { // but the server may still be responding. // The client must have a deframer set up, so force-unwrap is okay. do { - try state.deframer!.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) - } + state.deframer!.append(buffer) + try state.deframer!.decode(into: &state.inboundMessageBuffer) self.state = .clientClosedServerOpen(state) return .readInbound } catch { @@ -1542,10 +1540,6 @@ extension GRPCStreamStateMachine { .flatMap { Zlib.Compressor(method: $0) } let decompressor = Zlib.Method(encoding: inboundEncoding) .flatMap { Zlib.Decompressor(method: $0) } - let deframer = GRPCMessageDeframer( - maximumPayloadSize: state.maximumPayloadSize, - decompressor: decompressor - ) self.state = .clientOpenServerIdle( .init( @@ -1554,7 +1548,10 @@ extension GRPCStreamStateMachine { outboundCompression: outboundEncoding, framer: GRPCMessageFramer(), decompressor: decompressor, - deframer: NIOSingleStepByteToMessageProcessor(deframer), + deframer: GRPCMessageDeframer( + maxPayloadSize: state.maximumPayloadSize, + decompressor: decompressor + ), headers: headers ) ) @@ -1588,11 +1585,9 @@ extension GRPCStreamStateMachine { self.state = ._modifying // Deframer must be present on the server side, as we know the decompression // algorithm from the moment the client opens. - do { - try state.deframer!.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) - } + state.deframer!.append(buffer) + try state.deframer!.decode(into: &state.inboundMessageBuffer) action = .readInbound } catch { let error = RPCError(code: .internalError, message: "Failed to decode message") @@ -1607,11 +1602,9 @@ extension GRPCStreamStateMachine { case .clientOpenServerOpen(var state): self.state = ._modifying - do { - try state.deframer.process(buffer: buffer) { deframedMessage in - state.inboundMessageBuffer.append(deframedMessage) - } + state.deframer.append(buffer) + try state.deframer.decode(into: &state.inboundMessageBuffer) action = .readInbound } catch { let error = RPCError(code: .internalError, message: "Failed to decode message") diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift new file mode 100644 index 000000000..c0d9ec811 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift @@ -0,0 +1,237 @@ +/* + * 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 NIOCore +import NIOTestUtils +import XCTest + +@testable import GRPCHTTP2Core + +final class GRPCMessageDecoderTests: XCTestCase { + func testReadMultipleMessagesWithoutCompression() throws { + let firstMessage = { + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + buffer.writeInteger(UInt32(16)) + buffer.writeRepeatingByte(42, count: 16) + return buffer + }() + + let secondMessage = { + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + buffer.writeInteger(UInt32(8)) + buffer.writeRepeatingByte(43, count: 8) + return buffer + }() + + try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [ + (firstMessage, [Array(repeating: UInt8(42), count: 16)]), + (secondMessage, [Array(repeating: UInt8(43), count: 8)]), + ]) { + GRPCMessageDecoder(maximumPayloadSize: .max) + } + } + + func testReadMessageOverSizeLimitWithoutCompression() throws { + let deframer = GRPCMessageDecoder(maximumPayloadSize: 100) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + buffer.writeInteger(UInt32(101)) + buffer.writeRepeatingByte(42, count: 101) + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: buffer) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" + ) + } + } + + func testReadMessageOverSizeLimitButWithoutActualMessageBytes() throws { + let deframer = GRPCMessageDecoder(maximumPayloadSize: 100) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(0)) + // Set the message length field to be over the maximum payload size, but + // don't write the actual message bytes. This is to ensure that the payload + // size limit is enforced _before_ the payload is actually read. + buffer.writeInteger(UInt32(101)) + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: buffer) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" + ) + } + } + + func testCompressedMessageWithoutConfiguringDecompressor() throws { + let deframer = GRPCMessageDecoder(maximumPayloadSize: 100) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + + var buffer = ByteBuffer() + buffer.writeInteger(UInt8(1)) + buffer.writeInteger(UInt32(10)) + buffer.writeRepeatingByte(42, count: 10) + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: buffer) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .internalError) + XCTAssertEqual( + error.message, + "Received a compressed message payload, but no decompressor has been configured." + ) + } + } + + private func testReadMultipleMessagesWithCompression(method: Zlib.Method) throws { + let decompressor = Zlib.Decompressor(method: method) + let compressor = Zlib.Compressor(method: method) + var framer = GRPCMessageFramer() + defer { + decompressor.end() + compressor.end() + } + + let firstMessage = try { + framer.append(Array(repeating: 42, count: 100), promise: nil) + return try framer.next(compressor: compressor)! + }() + + let secondMessage = try { + framer.append(Array(repeating: 43, count: 110), promise: nil) + return try framer.next(compressor: compressor)! + }() + + try ByteToMessageDecoderVerifier.verifyDecoder( + inputOutputPairs: [ + (firstMessage.bytes, [Array(repeating: 42, count: 100)]), + (secondMessage.bytes, [Array(repeating: 43, count: 110)]), + ]) { + GRPCMessageDecoder(maximumPayloadSize: 1000, decompressor: decompressor) + } + } + + func testReadMultipleMessagesWithDeflateCompression() throws { + try self.testReadMultipleMessagesWithCompression(method: .deflate) + } + + func testReadMultipleMessagesWithGZIPCompression() throws { + try self.testReadMultipleMessagesWithCompression(method: .gzip) + } + + func testReadCompressedMessageOverSizeLimitBeforeDecompressing() throws { + let deframer = GRPCMessageDecoder(maximumPayloadSize: 1) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + let compressor = Zlib.Compressor(method: .gzip) + var framer = GRPCMessageFramer() + defer { + compressor.end() + } + + framer.append(Array(repeating: 42, count: 100), promise: nil) + let framedMessage = try framer.next(compressor: compressor)! + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: framedMessage.bytes) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual( + error.message, + """ + Message has exceeded the configured maximum payload size \ + (max: 1, actual: \(framedMessage.bytes.readableBytes - GRPCMessageDecoder.metadataLength)) + """ + ) + } + } + + private func testReadDecompressedMessageOverSizeLimit(method: Zlib.Method) throws { + let decompressor = Zlib.Decompressor(method: method) + let deframer = GRPCMessageDecoder(maximumPayloadSize: 100, decompressor: decompressor) + let processor = NIOSingleStepByteToMessageProcessor(deframer) + let compressor = Zlib.Compressor(method: method) + var framer = GRPCMessageFramer() + defer { + decompressor.end() + compressor.end() + } + + framer.append(Array(repeating: 42, count: 101), promise: nil) + let framedMessage = try framer.next(compressor: compressor)! + + XCTAssertThrowsError( + ofType: RPCError.self, + try processor.process(buffer: framedMessage.bytes) { _ in + XCTFail("No message should be produced.") + } + ) { error in + XCTAssertEqual(error.code, .resourceExhausted) + XCTAssertEqual(error.message, "Message is too large to decompress.") + } + } + + func testReadDecompressedMessageOverSizeLimitWithDeflateCompression() throws { + try self.testReadDecompressedMessageOverSizeLimit(method: .deflate) + } + + func testReadDecompressedMessageOverSizeLimitWithGZIPCompression() throws { + try self.testReadDecompressedMessageOverSizeLimit(method: .gzip) + } +} + +extension GRPCMessageFramer { + mutating func next( + compressor: Zlib.Compressor? = nil + ) throws(RPCError) -> (bytes: ByteBuffer, promise: EventLoopPromise?)? { + if let (result, promise) = self.nextResult(compressor: compressor) { + switch result { + case .success(let buffer): + return (bytes: buffer, promise: promise) + case .failure(let error): + promise?.fail(error) + throw error + } + } else { + return nil + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift index bcb3e375f..a5ef3d8b4 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift @@ -14,224 +14,93 @@ * limitations under the License. */ -import GRPCCore +import GRPCHTTP2Core import NIOCore -import NIOTestUtils import XCTest -@testable import GRPCHTTP2Core - final class GRPCMessageDeframerTests: XCTestCase { - func testReadMultipleMessagesWithoutCompression() throws { - let firstMessage = { - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - buffer.writeInteger(UInt32(16)) - buffer.writeRepeatingByte(42, count: 16) - return buffer - }() - - let secondMessage = { - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - buffer.writeInteger(UInt32(8)) - buffer.writeRepeatingByte(43, count: 8) - return buffer - }() - - try ByteToMessageDecoderVerifier.verifyDecoder( - inputOutputPairs: [ - (firstMessage, [Array(repeating: UInt8(42), count: 16)]), - (secondMessage, [Array(repeating: UInt8(43), count: 8)]), - ]) { - GRPCMessageDeframer() - } - } - - func testReadMessageOverSizeLimitWithoutCompression() throws { - let deframer = GRPCMessageDeframer(maximumPayloadSize: 100) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - buffer.writeInteger(UInt32(101)) - buffer.writeRepeatingByte(42, count: 101) - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: buffer) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" - ) - } - } + // Most of the functionality is tested by the 'GRPCMessageDecoder' tests. - func testReadMessageOverSizeLimitButWithoutActualMessageBytes() throws { - let deframer = GRPCMessageDeframer(maximumPayloadSize: 100) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - // Set the message length field to be over the maximum payload size, but - // don't write the actual message bytes. This is to ensure that the payload - // size limit is enforced _before_ the payload is actually read. - buffer.writeInteger(UInt32(101)) - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: buffer) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" - ) - } + func testDecodeNoBytes() { + var deframer = GRPCMessageDeframer(maxPayloadSize: .max) + XCTAssertNil(try deframer.decodeNext()) } - func testCompressedMessageWithoutConfiguringDecompressor() throws { - let deframer = GRPCMessageDeframer(maximumPayloadSize: 100) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(1)) - buffer.writeInteger(UInt32(10)) - buffer.writeRepeatingByte(42, count: 10) - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: buffer) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual( - error.message, - "Received a compressed message payload, but no decompressor has been configured." - ) - } + func testDecodeNotEnoughBytes() { + var deframer = GRPCMessageDeframer(maxPayloadSize: .max) + let bytes: [UInt8] = [ + 0x0, // Compression byte (not compressed) + 0x0, 0x0, 0x0, 0x1, // Length (1) + ] + deframer.append(ByteBuffer(bytes: bytes)) + XCTAssertNil(try deframer.decodeNext()) } - private func testReadMultipleMessagesWithCompression(method: Zlib.Method) throws { - let decompressor = Zlib.Decompressor(method: method) - let compressor = Zlib.Compressor(method: method) - var framer = GRPCMessageFramer() - defer { - decompressor.end() - compressor.end() - } - - let firstMessage = try { - framer.append(Array(repeating: 42, count: 100), promise: nil) - return try framer.next(compressor: compressor)! - }() - - let secondMessage = try { - framer.append(Array(repeating: 43, count: 110), promise: nil) - return try framer.next(compressor: compressor)! - }() - - try ByteToMessageDecoderVerifier.verifyDecoder( - inputOutputPairs: [ - (firstMessage.bytes, [Array(repeating: 42, count: 100)]), - (secondMessage.bytes, [Array(repeating: 43, count: 110)]), - ]) { - GRPCMessageDeframer(maximumPayloadSize: 1000, decompressor: decompressor) - } + func testDecodeZeroLengthMessage() { + var deframer = GRPCMessageDeframer(maxPayloadSize: .max) + let bytes: [UInt8] = [ + 0x0, // Compression byte (not compressed) + 0x0, 0x0, 0x0, 0x0, // Length (0) + ] + deframer.append(ByteBuffer(bytes: bytes)) + XCTAssertEqual(try deframer.decodeNext(), []) } - func testReadMultipleMessagesWithDeflateCompression() throws { - try self.testReadMultipleMessagesWithCompression(method: .deflate) + func testDecodeMessage() { + var deframer = GRPCMessageDeframer(maxPayloadSize: .max) + let bytes: [UInt8] = [ + 0x0, // Compression byte (not compressed) + 0x0, 0x0, 0x0, 0x1, // Length (1) + 0xf, // Payload + ] + deframer.append(ByteBuffer(bytes: bytes)) + XCTAssertEqual(try deframer.decodeNext(), [0xf]) } - func testReadMultipleMessagesWithGZIPCompression() throws { - try self.testReadMultipleMessagesWithCompression(method: .gzip) - } + func testDripFeedAndDecode() { + var deframer = GRPCMessageDeframer(maxPayloadSize: .max) + let bytes: [UInt8] = [ + 0x0, // Compression byte (not compressed) + 0x0, 0x0, 0x0, 0x1, // Length (1) + ] - func testReadCompressedMessageOverSizeLimitBeforeDecompressing() throws { - let deframer = GRPCMessageDeframer(maximumPayloadSize: 1) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - let compressor = Zlib.Compressor(method: .gzip) - var framer = GRPCMessageFramer() - defer { - compressor.end() + for byte in bytes { + deframer.append(ByteBuffer(bytes: [byte])) + XCTAssertNil(try deframer.decodeNext()) } - framer.append(Array(repeating: 42, count: 100), promise: nil) - let framedMessage = try framer.next(compressor: compressor)! - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: framedMessage.bytes) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - """ - Message has exceeded the configured maximum payload size \ - (max: 1, actual: \(framedMessage.bytes.readableBytes - GRPCMessageDeframer.metadataLength)) - """ - ) - } + // Drip feed the last byte. + deframer.append(ByteBuffer(bytes: [0xf])) + XCTAssertEqual(try deframer.decodeNext(), [0xf]) } - private func testReadDecompressedMessageOverSizeLimit(method: Zlib.Method) throws { - let decompressor = Zlib.Decompressor(method: method) - let deframer = GRPCMessageDeframer(maximumPayloadSize: 100, decompressor: decompressor) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - let compressor = Zlib.Compressor(method: method) - var framer = GRPCMessageFramer() - defer { - decompressor.end() - compressor.end() - } + func testReadBytesAreDiscarded() throws { + var deframer = GRPCMessageDeframer(maxPayloadSize: .max) - framer.append(Array(repeating: 42, count: 101), promise: nil) - let framedMessage = try framer.next(compressor: compressor)! - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: framedMessage.bytes) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual(error.message, "Message is too large to decompress.") - } - } + var input = ByteBuffer() + input.writeInteger(UInt8(0)) // Compression byte (not compressed) + input.writeInteger(UInt32(1024)) // Length + input.writeRepeatingByte(42, count: 1024) // Payload - func testReadDecompressedMessageOverSizeLimitWithDeflateCompression() throws { - try self.testReadDecompressedMessageOverSizeLimit(method: .deflate) - } + input.writeInteger(UInt8(0)) // Compression byte (not compressed) + input.writeInteger(UInt32(1024)) // Length + input.writeRepeatingByte(43, count: 512) // Payload (most of it) - func testReadDecompressedMessageOverSizeLimitWithGZIPCompression() throws { - try self.testReadDecompressedMessageOverSizeLimit(method: .gzip) - } -} + deframer.append(input) + XCTAssertEqual(deframer._readerIndex, 0) -extension GRPCMessageFramer { - mutating func next( - compressor: Zlib.Compressor? = nil - ) throws(RPCError) -> (bytes: ByteBuffer, promise: EventLoopPromise?)? { - if let (result, promise) = self.nextResult(compressor: compressor) { - switch result { - case .success(let buffer): - return (bytes: buffer, promise: promise) - case .failure(let error): - promise?.fail(error) - throw error - } - } else { - return nil - } + let message1 = try deframer.decodeNext() + XCTAssertEqual(message1, Array(repeating: 42, count: 1024)) + XCTAssertNotEqual(deframer._readerIndex, 0) + + // Append the final byte. This should discard any read bytes and set the reader index back + // to zero. + deframer.append(ByteBuffer(repeating: 43, count: 512)) + XCTAssertEqual(deframer._readerIndex, 0) + + // Read the message + let message2 = try deframer.decodeNext() + XCTAssertEqual(message2, Array(repeating: 43, count: 1024)) + XCTAssertNotEqual(deframer._readerIndex, 0) } } From 73829b093e6306fb939f30471cb094942162ae64 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 11 Jul 2024 16:23:59 +0100 Subject: [PATCH 397/580] Add underscored products for v2 (#1978) Motivation: Some users want to kick the tyres of v2, to do that they need a product to depend on. Modifications: - Add a general 'GRPCHTTP2Transport' target which exports the both NIOPosix transport and the NIOTS transport. - Add a '_GRPCHTTP2Transport' product - Add a '_GRPCInProcessTransport' product - Add exported imports to the h2 transport and in-process transport so the core module isn't explicitly required Result: Users can depend on pre-release products which lack stable API guarantees --- Package@swift-6.swift | 44 +++++++++++++++++-- Sources/GRPCHTTP2Transport/Exports.swift | 20 +++++++++ .../GRPCHTTP2TransportNIOPosix/Exports.swift | 18 ++++++++ .../Exports.swift | 18 ++++++++ Sources/GRPCInProcessTransport/Exports.swift | 17 +++++++ 5 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 Sources/GRPCHTTP2Transport/Exports.swift create mode 100644 Sources/GRPCHTTP2TransportNIOPosix/Exports.swift create mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift create mode 100644 Sources/GRPCInProcessTransport/Exports.swift diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 2edd9e065..54725c91c 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -260,6 +260,19 @@ extension Target { ) } + static var grpcHTTP2Transport: Target { + .target( + name: "GRPCHTTP2Transport", + dependencies: [ + .grpcCore, + .grpcHTTP2Core, + .grpcHTTP2TransportNIOPosix, + .grpcHTTP2TransportNIOTransportServices, + ], + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + ) + } + static var cgrpcZlib: Target { .target( name: cgrpcZlibTargetName, @@ -853,6 +866,20 @@ extension Product { ) } + static var _grpcInProcessTransport: Product { + .library( + name: "_GRPCInProcessTransport", + targets: ["GRPCInProcessTransport"] + ) + } + + static var _grpcHTTP2Transport: Product { + .library( + name: "_GRPCHTTP2Transport", + targets: ["GRPCHTTP2Transport"] + ) + } + static var cgrpcZlib: Product { .library( name: cgrpcZlibProductName, @@ -896,6 +923,8 @@ let package = Package( // v2 ._grpcCore, ._grpcProtobuf, + ._grpcHTTP2Transport, + ._grpcInProcessTransport, ], dependencies: packageDependencies, targets: [ @@ -930,20 +959,29 @@ let package = Package( // v2 .grpcCore, - .grpcInProcessTransport, .grpcCodeGen, - .grpcInterceptors, + + // v2 transports + .grpcInProcessTransport, .grpcHTTP2Core, .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, + .grpcHTTP2Transport, + + // v2 Protobuf support .grpcProtobuf, .grpcProtobufCodeGen, + + // v2 add-ons + .grpcInterceptors, .grpcHealth, + + // v2 integration testing .interoperabilityTestImplementation, .interoperabilityTestsExecutable, .performanceWorker, - // v2 tests + // v2 unit tests .grpcCoreTests, .grpcInProcessTransportTests, .grpcCodeGenTests, diff --git a/Sources/GRPCHTTP2Transport/Exports.swift b/Sources/GRPCHTTP2Transport/Exports.swift new file mode 100644 index 000000000..64f679933 --- /dev/null +++ b/Sources/GRPCHTTP2Transport/Exports.swift @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@_exported import GRPCCore +@_exported import GRPCHTTP2Core +@_exported import GRPCHTTP2TransportNIOPosix +@_exported import GRPCHTTP2TransportNIOTransportServices diff --git a/Sources/GRPCHTTP2TransportNIOPosix/Exports.swift b/Sources/GRPCHTTP2TransportNIOPosix/Exports.swift new file mode 100644 index 000000000..395308d46 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOPosix/Exports.swift @@ -0,0 +1,18 @@ +/* + * 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. + */ + +@_exported import GRPCCore +@_exported import GRPCHTTP2Core diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift new file mode 100644 index 000000000..395308d46 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift @@ -0,0 +1,18 @@ +/* + * 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. + */ + +@_exported import GRPCCore +@_exported import GRPCHTTP2Core diff --git a/Sources/GRPCInProcessTransport/Exports.swift b/Sources/GRPCInProcessTransport/Exports.swift new file mode 100644 index 000000000..1f32ac4d1 --- /dev/null +++ b/Sources/GRPCInProcessTransport/Exports.swift @@ -0,0 +1,17 @@ +/* + * 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. + */ + +@_exported import GRPCCore From d7a8f024095058485ddaf93acb0466f81c4051cd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 15 Jul 2024 15:24:01 +0100 Subject: [PATCH 398/580] Simplfy top-level streamin handling in server transports (#1979) Modification: The top-level connection handling code in the two NIO H2 transport is a little hard to follow because the nesting gets quite deep. So much so that it took me some time to notice an unnecessary task group in there. Modifications: - Split the top-level server code into a couple of functions - Remove unnecessary task group Result: Easier code to follow --- .../HTTP2ServerTransport+Posix.swift | 118 ++++++++++-------- ...TP2ServerTransport+TransportServices.swift | 118 ++++++++++-------- 2 files changed, 134 insertions(+), 102 deletions(-) diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index ea73cc126..c46516be0 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -18,6 +18,7 @@ import GRPCCore import GRPCHTTP2Core import NIOCore import NIOExtras +import NIOHTTP2 import NIOPosix extension HTTP2ServerTransport { @@ -211,64 +212,79 @@ extension HTTP2ServerTransport { } try await serverChannel.executeThenClose { inbound in - try await withThrowingDiscardingTaskGroup { serverTaskGroup in + try await withThrowingDiscardingTaskGroup { group in for try await (connectionChannel, streamMultiplexer) in inbound { - serverTaskGroup.addTask { - try await connectionChannel - .executeThenClose { connectionInbound, connectionOutbound in - await withDiscardingTaskGroup { connectionTaskGroup in - connectionTaskGroup.addTask { - do { - for try await _ in connectionInbound {} - } catch { - // We don't want to close the channel if one connection throws. - return - } - } - - connectionTaskGroup.addTask { - await withDiscardingTaskGroup { streamTaskGroup in - do { - for try await (http2Stream, methodDescriptor) in streamMultiplexer.inbound - { - streamTaskGroup.addTask { - // It's okay to ignore these errors: - // - If we get an error because the http2Stream failed to close, then there's nothing we can do - // - If we get an error because the inner closure threw, then the only possible scenario in which - // that could happen is if methodDescriptor.get() throws - in which case, it means we never got - // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. - try? await http2Stream.executeThenClose { inbound, outbound in - guard let descriptor = try? await methodDescriptor.get() else { - return - } - let rpcStream = RPCStream( - descriptor: descriptor, - inbound: RPCAsyncSequence(wrapping: inbound), - outbound: RPCWriter.Closable( - wrapping: ServerConnection.Stream.Outbound( - responseWriter: outbound, - http2Stream: http2Stream - ) - ) - ) - await streamHandler(rpcStream) - } - } - } - } catch { - // We don't want to close the whole connection if one stream throws. - return - } - } - } - } - } + group.addTask { + try await self.handleConnection( + connectionChannel, + multiplexer: streamMultiplexer, + streamHandler: streamHandler + ) } } } } } + private func handleConnection( + _ connection: NIOAsyncChannel, + multiplexer: ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer, + streamHandler: @escaping @Sendable (RPCStream) async -> Void + ) async throws { + try await connection.executeThenClose { inbound, _ in + await withDiscardingTaskGroup { group in + group.addTask { + do { + for try await _ in inbound {} + } catch { + // We don't want to close the channel if one connection throws. + return + } + } + + do { + for try await (stream, descriptor) in multiplexer.inbound { + group.addTask { + await self.handleStream(stream, handler: streamHandler, descriptor: descriptor) + } + } + } catch { + return + } + } + } + } + + private func handleStream( + _ stream: NIOAsyncChannel, + handler streamHandler: @escaping @Sendable (RPCStream) async -> Void, + descriptor: EventLoopFuture + ) async { + // It's okay to ignore these errors: + // - If we get an error because the http2Stream failed to close, then there's nothing we can do + // - If we get an error because the inner closure threw, then the only possible scenario in which + // that could happen is if methodDescriptor.get() throws - in which case, it means we never got + // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. + try? await stream.executeThenClose { inbound, outbound in + guard let descriptor = try? await descriptor.get() else { + return + } + + let rpcStream = RPCStream( + descriptor: descriptor, + inbound: RPCAsyncSequence(wrapping: inbound), + outbound: RPCWriter.Closable( + wrapping: ServerConnection.Stream.Outbound( + responseWriter: outbound, + http2Stream: stream + ) + ) + ) + + await streamHandler(rpcStream) + } + } + public func stopListening() { self.serverQuiescingHelper.initiateShutdown(promise: nil) } diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 169116d62..bd9ee0b91 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -19,6 +19,7 @@ import GRPCCore import GRPCHTTP2Core import NIOCore import NIOExtras +import NIOHTTP2 import NIOTransportServices extension HTTP2ServerTransport { @@ -202,64 +203,79 @@ extension HTTP2ServerTransport { } try await serverChannel.executeThenClose { inbound in - try await withThrowingDiscardingTaskGroup { serverTaskGroup in + try await withThrowingDiscardingTaskGroup { group in for try await (connectionChannel, streamMultiplexer) in inbound { - serverTaskGroup.addTask { - try await connectionChannel - .executeThenClose { connectionInbound, connectionOutbound in - await withDiscardingTaskGroup { connectionTaskGroup in - connectionTaskGroup.addTask { - do { - for try await _ in connectionInbound {} - } catch { - // We don't want to close the channel if one connection throws. - return - } - } - - connectionTaskGroup.addTask { - await withDiscardingTaskGroup { streamTaskGroup in - do { - for try await (http2Stream, methodDescriptor) in streamMultiplexer.inbound - { - streamTaskGroup.addTask { - // It's okay to ignore these errors: - // - If we get an error because the http2Stream failed to close, then there's nothing we can do - // - If we get an error because the inner closure threw, then the only possible scenario in which - // that could happen is if methodDescriptor.get() throws - in which case, it means we never got - // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. - try? await http2Stream.executeThenClose { inbound, outbound in - guard let descriptor = try? await methodDescriptor.get() else { - return - } - let rpcStream = RPCStream( - descriptor: descriptor, - inbound: RPCAsyncSequence(wrapping: inbound), - outbound: RPCWriter.Closable( - wrapping: ServerConnection.Stream.Outbound( - responseWriter: outbound, - http2Stream: http2Stream - ) - ) - ) - await streamHandler(rpcStream) - } - } - } - } catch { - // We don't want to close the whole connection if one stream throws. - return - } - } - } - } - } + group.addTask { + try await self.handleConnection( + connectionChannel, + multiplexer: streamMultiplexer, + streamHandler: streamHandler + ) } } } } } + private func handleConnection( + _ connection: NIOAsyncChannel, + multiplexer: ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer, + streamHandler: @escaping @Sendable (RPCStream) async -> Void + ) async throws { + try await connection.executeThenClose { inbound, _ in + await withDiscardingTaskGroup { group in + group.addTask { + do { + for try await _ in inbound {} + } catch { + // We don't want to close the channel if one connection throws. + return + } + } + + do { + for try await (stream, descriptor) in multiplexer.inbound { + group.addTask { + await self.handleStream(stream, handler: streamHandler, descriptor: descriptor) + } + } + } catch { + return + } + } + } + } + + private func handleStream( + _ stream: NIOAsyncChannel, + handler streamHandler: @escaping @Sendable (RPCStream) async -> Void, + descriptor: EventLoopFuture + ) async { + // It's okay to ignore these errors: + // - If we get an error because the http2Stream failed to close, then there's nothing we can do + // - If we get an error because the inner closure threw, then the only possible scenario in which + // that could happen is if methodDescriptor.get() throws - in which case, it means we never got + // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. + try? await stream.executeThenClose { inbound, outbound in + guard let descriptor = try? await descriptor.get() else { + return + } + + let rpcStream = RPCStream( + descriptor: descriptor, + inbound: RPCAsyncSequence(wrapping: inbound), + outbound: RPCWriter.Closable( + wrapping: ServerConnection.Stream.Outbound( + responseWriter: outbound, + http2Stream: stream + ) + ) + ) + + await streamHandler(rpcStream) + } + } + public func stopListening() { self.serverQuiescingHelper.initiateShutdown(promise: nil) } From ef027289327f2a8a82d50ced595a7c9d45469a8d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 16 Jul 2024 12:03:35 +0100 Subject: [PATCH 399/580] Propagate generics from AsyncSequence to RPCAsyncSequence (#1980) Motivation: `RPCAsyncSequence` was added before we raised the minimum swift version to 6.0. As a result it used a fully erased sequence where the iterator had to upcast from `Any` to the expected type. This causes an allocation per message. Modification: - Make `RPCAsyncSequence` generic over its `Element` and its `Failure` type - This required bumping its availability to the latest SDKs which is rather viral. - Modify the code generator to allow visionOS to be specified - Modify the code gen translator to bump the availability of generated code - Regenerate Result: Fewer allocations --- Sources/Examples/v2/Echo/Echo.swift | 2 +- .../v2/Echo/Generated/echo.grpc.swift | 26 ++-- .../v2/Echo/Subcommands/Collect.swift | 2 +- .../Examples/v2/Echo/Subcommands/Expand.swift | 2 +- .../Examples/v2/Echo/Subcommands/Get.swift | 2 +- .../Examples/v2/Echo/Subcommands/Serve.swift | 4 +- .../Examples/v2/Echo/Subcommands/Update.swift | 2 +- .../StructuredSwiftRepresentation.swift | 1 + .../Translator/SpecializedTranslator.swift | 9 +- .../Call/Client/ClientInterceptor.swift | 2 +- .../GRPCCore/Call/Client/ClientResponse.swift | 12 +- .../ClientRPCExecutor+HedgingExecutor.swift | 8 +- .../ClientRPCExecutor+OneShotExecutor.swift | 4 +- .../ClientRPCExecutor+RetryExecutor.swift | 4 +- .../Client/Internal/ClientRPCExecutor.swift | 4 +- .../Internal/ClientResponse+Convenience.swift | 6 +- .../Internal/ClientStreamExecutor.swift | 2 +- .../Server/Internal/ServerRPCExecutor.swift | 22 ++-- Sources/GRPCCore/Call/Server/RPCRouter.swift | 19 ++- .../Call/Server/RegistrableRPCService.swift | 2 +- .../Call/Server/ServerInterceptor.swift | 2 +- .../GRPCCore/Call/Server/ServerRequest.swift | 10 +- Sources/GRPCCore/GRPCClient.swift | 2 +- Sources/GRPCCore/GRPCServer.swift | 2 +- .../Internal/AsyncSequenceOfOne.swift | 21 +++- .../Internal/RPCAsyncSequence+Buffered.swift | 4 +- .../GRPCCore/Streaming/RPCAsyncSequence.swift | 46 ++++--- .../GRPCCore/Transport/ClientTransport.swift | 4 +- .../GRPCCore/Transport/ServerTransport.swift | 4 +- .../Client/Connection/GRPCChannel.swift | 20 ++-- .../Client/Resolver/NameResolver+IPv4.swift | 2 +- .../Client/Resolver/NameResolver+IPv6.swift | 2 +- .../Client/Resolver/NameResolver+UDS.swift | 2 +- .../Client/Resolver/NameResolver+VSOCK.swift | 2 +- .../Client/Resolver/NameResolver.swift | 10 +- .../Resolver/NameResolverRegistry.swift | 2 +- .../Internal/ConstantAsyncSequence.swift | 6 +- .../Internal/NIOChannelPipeline+GRPC.swift | 2 +- .../HTTP2ClientTransport+Posix.swift | 8 +- .../HTTP2ServerTransport+Posix.swift | 4 +- ...TP2ServerTransport+TransportServices.swift | 4 +- .../InProcessClientTransport.swift | 20 +++- .../InProcessServerTransport.swift | 4 +- .../InProcessTransport.swift | 2 +- .../ClientTracingInterceptor.swift | 2 +- .../ServerTracingInterceptor.swift | 2 +- .../Generated/empty_service.grpc.swift | 24 ++-- .../Generated/test.grpc.swift | 72 +++++------ .../InteroperabilityTestCase.swift | 4 +- .../InteroperabilityTestCases.swift | 28 ++--- .../InteroperabilityTests/TestService.swift | 2 +- .../Health/Generated/health.grpc.swift | 14 +-- .../InteroperabilityTestsExecutable.swift | 2 +- .../performance-worker/BenchmarkClient.swift | 6 +- .../performance-worker/BenchmarkService.swift | 4 +- .../grpc_testing_benchmark_service.grpc.swift | 24 ++-- .../grpc_testing_worker_service.grpc.swift | 14 +-- .../PerformanceWorker.swift | 2 +- .../performance-worker/WorkerService.swift | 8 +- ...lientCodeTranslatorSnippetBasedTests.swift | 48 ++++---- ...uredSwiftTranslatorSnippetBasedTests.swift | 14 +-- ...erverCodeTranslatorSnippetBasedTests.swift | 80 ++++++------- ...TypealiasTranslatorSnippetBasedTests.swift | 112 +++++++++--------- .../Call/Client/ClientResponseTests.swift | 4 +- ...PCExecutorTestHarness+ServerBehavior.swift | 23 +++- ...ientRPCExecutorTestHarness+Transport.swift | 2 +- .../ClientRPCExecutorTestHarness.swift | 2 +- .../ClientRPCExecutorTests+Hedging.swift | 2 +- .../ClientRPCExecutorTests+Retries.swift | 2 +- .../Internal/ClientRPCExecutorTests.swift | 2 +- .../ServerRPCExecutorTestHarness.swift | 10 +- .../Internal/ServerRPCExecutorTests.swift | 2 +- .../Call/Server/RPCRouterTests.swift | 2 +- .../Call/Server/ServerRequestTests.swift | 2 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 2 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 2 +- .../Internal/AsyncSequenceOfOne.swift | 7 +- .../Call/Client/ClientInterceptors.swift | 8 +- .../Call/Server/ServerInterceptors.swift | 8 +- .../RPCAsyncSequence+Utilities.swift | 6 +- .../Test Utilities/Services/BinaryEcho.swift | 2 +- .../Transport/AnyTransport.swift | 14 ++- .../Transport/StreamCountingTransport.swift | 8 +- .../Transport/ThrowingTransport.swift | 4 +- .../Test Utilities/XCTest+Utilities.swift | 2 +- .../Client/Connection/GRPCChannelTests.swift | 6 +- .../Connection/Utilities/NameResolvers.swift | 6 +- .../Resolver/NameResolverRegistryTests.swift | 4 +- .../ControlService.swift | 4 +- .../Generated/control.grpc.swift | 24 ++-- .../HTTP2TransportNIOPosixTests.swift | 2 +- ...P2TransportNIOTransportServicesTests.swift | 2 +- .../HTTP2TransportTests.swift | 4 +- .../InProcessClientTransportTests.swift | 2 +- .../InProcessServerTransportTests.swift | 19 +-- .../TracingInterceptorTests.swift | 20 ++-- .../ProtobufCodeGeneratorTests.swift | 48 ++++---- .../InProcessInteroperabilityTests.swift | 2 +- 98 files changed, 555 insertions(+), 486 deletions(-) diff --git a/Sources/Examples/v2/Echo/Echo.swift b/Sources/Examples/v2/Echo/Echo.swift index 7d8f26efb..8ff07f420 100644 --- a/Sources/Examples/v2/Echo/Echo.swift +++ b/Sources/Examples/v2/Echo/Echo.swift @@ -17,7 +17,7 @@ import ArgumentParser @main -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Echo: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "echo", diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index 10428bc78..175f6f5a3 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -65,17 +65,17 @@ internal enum Echo_Echo { Update.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Echo_EchoStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ServiceProtocol = Echo_EchoServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ClientProtocol = Echo_EchoClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Echo_EchoClient } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Immediately returns an echo of a request. func get(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -91,9 +91,9 @@ internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCServ } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Echo_Echo.Method.Get.descriptor, @@ -130,7 +130,7 @@ extension Echo_Echo.StreamingServiceProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { /// Immediately returns an echo of a request. func get(request: ServerRequest.Single) async throws -> ServerResponse.Single @@ -146,7 +146,7 @@ internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { } /// Partial conformance to `Echo_EchoStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ServiceProtocol { internal func get(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.get(request: ServerRequest.Single(stream: request)) @@ -164,7 +164,7 @@ extension Echo_Echo.ServiceProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoClientProtocol: Sendable { /// Immediately returns an echo of a request. func get( @@ -203,7 +203,7 @@ internal protocol Echo_EchoClientProtocol: Sendable { ) async throws -> R where R: Sendable } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ClientProtocol { internal func get( request: ClientRequest.Single, @@ -262,7 +262,7 @@ extension Echo_Echo.ClientProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -341,4 +341,4 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { handler: body ) } -} \ No newline at end of file +} diff --git a/Sources/Examples/v2/Echo/Subcommands/Collect.swift b/Sources/Examples/v2/Echo/Subcommands/Collect.swift index 65594b02e..1b799171f 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Collect.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Collect.swift @@ -19,7 +19,7 @@ import GRPCCore import GRPCHTTP2Core import GRPCHTTP2TransportNIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Collect: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "Makes a client streaming RPC to the echo server." diff --git a/Sources/Examples/v2/Echo/Subcommands/Expand.swift b/Sources/Examples/v2/Echo/Subcommands/Expand.swift index a920cb47a..5430bac25 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Expand.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Expand.swift @@ -19,7 +19,7 @@ import GRPCCore import GRPCHTTP2Core import GRPCHTTP2TransportNIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Expand: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "Makes a server streaming RPC to the echo server." diff --git a/Sources/Examples/v2/Echo/Subcommands/Get.swift b/Sources/Examples/v2/Echo/Subcommands/Get.swift index 94937d5df..85d56ad50 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Get.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Get.swift @@ -19,7 +19,7 @@ import GRPCCore import GRPCHTTP2Core import GRPCHTTP2TransportNIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Get: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Makes a unary RPC to the echo server.") diff --git a/Sources/Examples/v2/Echo/Subcommands/Serve.swift b/Sources/Examples/v2/Echo/Subcommands/Serve.swift index 62658d139..ceb4149d0 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Serve.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Serve.swift @@ -19,7 +19,7 @@ import GRPCCore import GRPCHTTP2Core import GRPCHTTP2TransportNIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Serve: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Starts an echo server.") @@ -37,7 +37,7 @@ struct Serve: AsyncParsableCommand { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EchoService: Echo_EchoServiceProtocol { func get( request: ServerRequest.Single diff --git a/Sources/Examples/v2/Echo/Subcommands/Update.swift b/Sources/Examples/v2/Echo/Subcommands/Update.swift index d0d06ddbc..7ce4b2394 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Update.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Update.swift @@ -19,7 +19,7 @@ import GRPCCore import GRPCHTTP2Core import GRPCHTTP2TransportNIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Update: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "Makes a bidirectional server streaming RPC to the echo server." diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index e40b8eb5c..b96601db7 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -837,6 +837,7 @@ struct AvailabilityDescription: Equatable, Codable { static let iOS = Self(name: "iOS") static let watchOS = Self(name: "watchOS") static let tvOS = Self(name: "tvOS") + static let visionOS = Self(name: "visionOS") } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift index b73a75c95..eb74b30e7 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift @@ -49,10 +49,11 @@ extension SpecializedTranslator { internal var availabilityGuard: AvailabilityDescription { AvailabilityDescription(osVersions: [ - .init(os: .macOS, version: "13.0"), - .init(os: .iOS, version: "16.0"), - .init(os: .watchOS, version: "9.0"), - .init(os: .tvOS, version: "16.0"), + .init(os: .macOS, version: "15.0"), + .init(os: .iOS, version: "18.0"), + .init(os: .watchOS, version: "11.0"), + .init(os: .tvOS, version: "18.0"), + .init(os: .visionOS, version: "2.0"), ]) } } diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index bbdaf9b30..999bdee25 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -88,7 +88,7 @@ /// ``` /// /// For server-side interceptors see ``ServerInterceptor``. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ClientInterceptor: Sendable { /// Intercept a request object. /// diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index 967b09050..a031933fd 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -204,7 +204,7 @@ extension ClientResponse { /// print("RPC failed with code '\(error.code)'") /// } /// ``` - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Stream: Sendable { public struct Contents: Sendable { /// Metadata received from the server at the beginning of the response. @@ -219,7 +219,7 @@ extension ClientResponse { /// If the RPC fails then the sequence will throw an ``RPCError``. /// /// The sequence may only be iterated once. - public var bodyParts: RPCAsyncSequence + public var bodyParts: RPCAsyncSequence /// Parts received from the server. public enum BodyPart: Sendable { @@ -236,7 +236,7 @@ extension ClientResponse { /// - bodyParts: An `AsyncSequence` of parts received from the server. public init( metadata: Metadata, - bodyParts: RPCAsyncSequence + bodyParts: RPCAsyncSequence ) { self.metadata = metadata self.bodyParts = bodyParts @@ -332,7 +332,7 @@ extension ClientResponse.Single { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientResponse.Stream { /// Creates a new accepted response. /// @@ -343,7 +343,7 @@ extension ClientResponse.Stream { public init( of messageType: Message.Type = Message.self, metadata: Metadata, - bodyParts: RPCAsyncSequence + bodyParts: RPCAsyncSequence ) { let contents = Contents(metadata: metadata, bodyParts: bodyParts) self.accepted = .success(contents) @@ -373,7 +373,7 @@ extension ClientResponse.Stream { /// Returns the messages received from the server. /// /// For rejected RPCs the `RPCAsyncSequence` throws a `RPCError``. - public var messages: RPCAsyncSequence { + public var messages: RPCAsyncSequence { switch self.accepted { case let .success(contents): let filtered = contents.bodyParts.compactMap { diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 7a59b001a..d537c70c1 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { @usableFromInline struct HedgingExecutor< @@ -63,7 +63,7 @@ extension ClientRPCExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.HedgingExecutor { @inlinable func execute( @@ -513,7 +513,7 @@ enum _HedgingTaskResult { case timedOut(Result) } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline enum _HedgingAttemptTaskResult { case attemptCompleted(AttemptResult) @@ -533,7 +533,7 @@ enum _HedgingAttemptTaskResult { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline enum _HedgingAttemptSubtaskResult { case attemptPicked(Bool) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index b5cd76c9b..b587df6eb 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { /// An executor for requests which doesn't apply retries or hedging. The request has just one /// attempt at execution. @@ -57,7 +57,7 @@ extension ClientRPCExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.OneShotExecutor { @inlinable func execute( diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index abe653b38..da9c79780 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { @usableFromInline struct RetryExecutor< @@ -63,7 +63,7 @@ extension ClientRPCExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.RetryExecutor { @inlinable func execute( diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index a01bd91e2..673dbb2b7 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline enum ClientRPCExecutor { /// Execute the request and handle its response. @@ -100,7 +100,7 @@ enum ClientRPCExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { /// Executes a request on a given stream processor. /// diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift index 281d23dd2..ecba7cf57 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientResponse.Single { /// Converts a streaming response into a single response. /// @@ -82,7 +82,7 @@ extension ClientResponse.Single { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientResponse.Stream { /// Creates a streaming response from the given status and metadata. /// @@ -103,7 +103,7 @@ extension ClientResponse.Stream { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientResponse.Stream { /// Returns a new response which maps the messages of this response. /// diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 1a57a80f6..13f3b1aa3 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline internal struct ClientStreamExecutor { /// The client transport to execute the stream on. diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 2e3923f46..f2261f1fc 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline struct ServerRPCExecutor { /// Executes an RPC using the provided handler. @@ -27,7 +27,10 @@ struct ServerRPCExecutor { /// - handler: A handler which turns the request into a response. @inlinable static func execute( - stream: RPCStream, RPCWriter.Closable>, + stream: RPCStream< + RPCAsyncSequence, + RPCWriter.Closable + >, deserializer: some MessageDeserializer, serializer: some MessageSerializer, interceptors: [any ServerInterceptor], @@ -63,7 +66,7 @@ struct ServerRPCExecutor { static func _execute( method: MethodDescriptor, metadata: Metadata, - inbound: UnsafeTransfer.AsyncIterator>, + inbound: UnsafeTransfer.AsyncIterator>, outbound: RPCWriter.Closable, deserializer: some MessageDeserializer, serializer: some MessageSerializer, @@ -103,7 +106,7 @@ struct ServerRPCExecutor { timeout: Duration, method: MethodDescriptor, metadata: Metadata, - inbound: UnsafeTransfer.AsyncIterator>, + inbound: UnsafeTransfer.AsyncIterator>, outbound: RPCWriter.Closable, deserializer: some MessageDeserializer, serializer: some MessageSerializer, @@ -156,7 +159,7 @@ struct ServerRPCExecutor { static func _processRPC( method: MethodDescriptor, metadata: Metadata, - inbound: UnsafeTransfer.AsyncIterator>, + inbound: UnsafeTransfer.AsyncIterator>, outbound: RPCWriter.Closable, deserializer: some MessageDeserializer, serializer: some MessageSerializer, @@ -233,7 +236,7 @@ struct ServerRPCExecutor { @inlinable static func _waitForFirstRequestPart( - inbound: RPCAsyncSequence + inbound: RPCAsyncSequence ) async -> OnFirstRequestPart { var iterator = inbound.makeAsyncIterator() let part = await Result { try await iterator.next() } @@ -273,7 +276,10 @@ struct ServerRPCExecutor { @usableFromInline enum OnFirstRequestPart { - case process(Metadata, UnsafeTransfer.AsyncIterator>) + case process( + Metadata, + UnsafeTransfer.AsyncIterator> + ) case reject(RPCError) } @@ -284,7 +290,7 @@ struct ServerRPCExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRPCExecutor { @inlinable static func _intercept( diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index f4a041c41..93c125c7b 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -32,14 +32,17 @@ /// 1. Remove individual methods by calling ``removeHandler(forMethod:)``, or /// 2. Implement ``RegistrableRPCService/registerMethods(with:)`` to register only the methods you /// want to be served. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct RPCRouter: Sendable { @usableFromInline struct RPCHandler: Sendable { @usableFromInline let _fn: @Sendable ( - _ stream: RPCStream, RPCWriter.Closable>, + _ stream: RPCStream< + RPCAsyncSequence, + RPCWriter.Closable + >, _ interceptors: [any ServerInterceptor] ) async -> Void @@ -65,7 +68,10 @@ public struct RPCRouter: Sendable { @inlinable func handle( - stream: RPCStream, RPCWriter.Closable>, + stream: RPCStream< + RPCAsyncSequence, + RPCWriter.Closable + >, interceptors: [any ServerInterceptor] ) async { await self._fn(stream, interceptors) @@ -134,10 +140,13 @@ public struct RPCRouter: Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RPCRouter { internal func handle( - stream: RPCStream, RPCWriter.Closable>, + stream: RPCStream< + RPCAsyncSequence, + RPCWriter.Closable + >, interceptors: [any ServerInterceptor] ) async { if let handler = self.handlers[stream.descriptor] { diff --git a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift index c19004522..d9236c75b 100644 --- a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift +++ b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift @@ -22,7 +22,7 @@ /// generated conformance by implementing ``registerMethods(with:)`` manually by calling /// ``RPCRouter/registerHandler(forMethod:deserializer:serializer:handler:)`` for each method /// you want to register with the router. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol RegistrableRPCService: Sendable { /// Registers methods to server with the provided ``RPCRouter``. /// diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index ca115ae43..69981b529 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -61,7 +61,7 @@ /// ``` /// /// For server-side interceptors see ``ClientInterceptor``. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ServerInterceptor: Sendable { /// Intercept a request object. /// diff --git a/Sources/GRPCCore/Call/Server/ServerRequest.swift b/Sources/GRPCCore/Call/Server/ServerRequest.swift index dafbee3d7..962d5f049 100644 --- a/Sources/GRPCCore/Call/Server/ServerRequest.swift +++ b/Sources/GRPCCore/Call/Server/ServerRequest.swift @@ -41,7 +41,7 @@ extension ServerRequest { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRequest { /// A request received at the server containing a stream of messages. public struct Stream: Sendable { @@ -54,14 +54,14 @@ extension ServerRequest { /// A sequence of messages received from the client. /// /// The sequence may be iterated at most once. - public var messages: RPCAsyncSequence + public var messages: RPCAsyncSequence /// Create a new streaming request. /// /// - Parameters: /// - metadata: Metadata received from the client. /// - messages: A sequence of messages received from the client. - public init(metadata: Metadata, messages: RPCAsyncSequence) { + public init(metadata: Metadata, messages: RPCAsyncSequence) { self.metadata = metadata self.messages = messages } @@ -70,14 +70,14 @@ extension ServerRequest { // MARK: - Conversion -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRequest.Stream { public init(single request: ServerRequest.Single) { self.init(metadata: request.metadata, messages: .one(request.message)) } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRequest.Single { public init(stream request: ServerRequest.Stream) async throws { var iterator = request.messages.makeAsyncIterator() diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 91f79c24d..c4cc1ed9f 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -109,7 +109,7 @@ import Atomics /// more abruptly you can cancel the task running your client. If your application requires /// additional resources that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. private let transport: any ClientTransport diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 19021e684..b24d90fd1 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -69,7 +69,7 @@ import Atomics /// you can cancel the task running your server. If your application requires additional resources /// that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct GRPCServer: Sendable { typealias Stream = RPCStream diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift index 2e79773b5..c0a72e176 100644 --- a/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift @@ -14,25 +14,27 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RPCAsyncSequence { /// Returns an ``RPCAsyncSequence`` containing just the given element. @inlinable static func one(_ element: Element) -> Self { - return Self(wrapping: AsyncSequenceOfOne(result: .success(element))) + let source = AsyncSequenceOfOne(result: .success(element)) + return RPCAsyncSequence(wrapping: source) } /// Returns an ``RPCAsyncSequence`` throwing the given error. @inlinable - static func throwing(_ error: E) -> Self { - return Self(wrapping: AsyncSequenceOfOne(result: .failure(error))) + static func throwing(_ error: Failure) -> Self { + let source = AsyncSequenceOfOne(result: .failure(error)) + return RPCAsyncSequence(wrapping: source) } } /// An `AsyncSequence` of a single value. @usableFromInline @available(macOS 10.15, iOS 13.0, tvOS 13, watchOS 6, *) -struct AsyncSequenceOfOne: AsyncSequence { +struct AsyncSequenceOfOne: AsyncSequence, Sendable { @usableFromInline let result: Result @@ -57,11 +59,18 @@ struct AsyncSequenceOfOne: AsyncSequence { } @inlinable - mutating func next() async throws -> Element? { + mutating func next( + isolation actor: isolated (any Actor)? + ) async throws(Failure) -> Element? { guard let result = self.result else { return nil } self.result = nil return try result.get() } + + @inlinable + mutating func next() async throws -> Element? { + try await self.next(isolation: nil) + } } } diff --git a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift index ff59ea2bb..aa367b780 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension RPCAsyncSequence { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension RPCAsyncSequence where Failure == any Error { @inlinable static func makeBackpressuredStream( of elementType: Element.Type = Element.self, diff --git a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift index 6e2e89350..791bb8ffc 100644 --- a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift @@ -15,33 +15,51 @@ */ /// A type-erasing `AsyncSequence`. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct RPCAsyncSequence: AsyncSequence, Sendable { - private let _makeAsyncIterator: @Sendable () -> AsyncIterator +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct RPCAsyncSequence: AsyncSequence, @unchecked Sendable { + // @unchecked Sendable is required because 'any' doesn't support composition with primary + // associated types. (see: https://github.com/swiftlang/swift/issue/63877) + // + // To work around that limitation the 'init' requires that the async sequence being wrapped + // is 'Sendable' but that constraint must be dropped internally. This is safe, the compiler just + // can't prove it. + @usableFromInline + let _wrapped: any AsyncSequence /// Creates an ``RPCAsyncSequence`` by wrapping another `AsyncSequence`. - public init(wrapping other: S) where S.Element == Element { - self._makeAsyncIterator = { - AsyncIterator(wrapping: other.makeAsyncIterator()) - } + @inlinable + public init>( + wrapping other: Source + ) where Source: Sendable { + self._wrapped = other } + @inlinable public func makeAsyncIterator() -> AsyncIterator { - self._makeAsyncIterator() + AsyncIterator(wrapping: self._wrapped.makeAsyncIterator()) } public struct AsyncIterator: AsyncIteratorProtocol { - private var iterator: any AsyncIteratorProtocol + @usableFromInline + private(set) var iterator: any AsyncIteratorProtocol - fileprivate init( - wrapping other: Iterator - ) where Iterator: AsyncIteratorProtocol, Iterator.Element == Element { + @inlinable + init( + wrapping other: some AsyncIteratorProtocol + ) { self.iterator = other } + @inlinable + public mutating func next( + isolation actor: isolated (any Actor)? + ) async throws(Failure) -> Element? { + try await self.iterator.next(isolation: `actor`) + } + + @inlinable public mutating func next() async throws -> Element? { - let elem = try await self.iterator.next() - return elem as? Element + try await self.next(isolation: nil) } } } diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 90d47e6fd..9b91ca5f1 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ClientTransport: Sendable { - typealias Inbound = RPCAsyncSequence + typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable /// Returns a throttle which gRPC uses to determine whether retries can be executed. diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index 5a3547e30..6d4d4851f 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -15,9 +15,9 @@ */ /// A protocol server transport implementations must conform to. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ServerTransport: Sendable { - typealias Inbound = RPCAsyncSequence + typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable /// Starts the transport. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 36fb2cfc0..c36df29a5 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -18,7 +18,7 @@ import Atomics import DequeModule import GRPCCore -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct GRPCChannel: ClientTransport { private enum Input: Sendable { /// Close the channel, if possible. @@ -124,7 +124,7 @@ package struct GRPCChannel: ClientTransport { self._connectivityState.continuation.yield(.idle) await withDiscardingTaskGroup { group in - var iterator: Optional.AsyncIterator> + var iterator: Optional.AsyncIterator> // The resolver can either push or pull values. If it pushes values the channel should // listen for new results. Otherwise the channel will pull values as and when necessary. @@ -203,7 +203,7 @@ package struct GRPCChannel: ClientTransport { return try await stream.execute { inbound, outbound in let rpcStream = RPCStream( descriptor: stream.descriptor, - inbound: RPCAsyncSequence(wrapping: inbound), + inbound: RPCAsyncSequence(wrapping: inbound), outbound: RPCWriter.Closable(wrapping: outbound) ) return try await closure(rpcStream) @@ -225,7 +225,7 @@ package struct GRPCChannel: ClientTransport { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel { package struct Config: Sendable { /// Configuration for HTTP/2 connections. @@ -254,7 +254,7 @@ extension GRPCChannel { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel { enum MakeStreamResult { /// A stream was created, use it. @@ -347,7 +347,7 @@ extension GRPCChannel { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel { private func handleClose(in group: inout DiscardingTaskGroup) { switch self.state.withLockedValue({ $0.close() }) { @@ -523,7 +523,7 @@ extension GRPCChannel { _ event: LoadBalancerEvent, loadBalancerID: LoadBalancerID, in group: inout DiscardingTaskGroup, - iterator: inout RPCAsyncSequence.AsyncIterator? + iterator: inout RPCAsyncSequence.AsyncIterator? ) async { switch event { case .connectivityStateChanged(let connectivityState): @@ -557,7 +557,7 @@ extension GRPCChannel { } private func resolve( - iterator: inout RPCAsyncSequence.AsyncIterator?, + iterator: inout RPCAsyncSequence.AsyncIterator?, in group: inout DiscardingTaskGroup ) async { guard var iterator = iterator else { return } @@ -574,7 +574,7 @@ extension GRPCChannel { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel { struct StateMachine { enum State { @@ -649,7 +649,7 @@ extension GRPCChannel { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel.StateMachine { mutating func start() { precondition(!self.running, "channel must only be started once") diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift index 99ac87f00..e6c2bf828 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift @@ -54,7 +54,7 @@ extension ResolvableTarget where Self == ResolvableTargets.IPv4 { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NameResolvers { /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv4`` targets. /// diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift index 5b2276168..c3681d228 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift @@ -54,7 +54,7 @@ extension ResolvableTarget where Self == ResolvableTargets.IPv6 { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NameResolvers { /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv6`` targets. /// diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift index c169dce38..149b541fa 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift @@ -40,7 +40,7 @@ extension ResolvableTarget where Self == ResolvableTargets.UnixDomainSocket { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NameResolvers { /// A ``NameResolverFactory`` for ``ResolvableTargets/UnixDomainSocket`` targets. /// diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift index 78bf3eed0..0e7b5a1f5 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift @@ -44,7 +44,7 @@ extension ResolvableTarget where Self == ResolvableTargets.VirtualSocket { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NameResolvers { /// A ``NameResolverFactory`` for ``ResolvableTargets/VirtualSocket`` targets. /// diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift index dab910bd5..93f44b48a 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift @@ -17,7 +17,7 @@ import GRPCCore /// A name resolver can provide resolved addresses and service configuration values over time. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NameResolver: Sendable { /// A sequence of name resolution results. /// @@ -27,7 +27,7 @@ public struct NameResolver: Sendable { /// /// Resolvers with the ``UpdateMode-swift.enum/pull`` update mode shouldn't be subscribed to, /// instead you should create an iterator and ask for new results as and when necessary. - public var names: RPCAsyncSequence + public var names: RPCAsyncSequence /// How ``names`` is updated and should be consumed. public let updateMode: UpdateMode @@ -52,7 +52,7 @@ public struct NameResolver: Sendable { } /// Create a new name resolver. - public init(names: RPCAsyncSequence, updateMode: UpdateMode) { + public init(names: RPCAsyncSequence, updateMode: UpdateMode) { self.names = names self.updateMode = updateMode } @@ -94,7 +94,7 @@ public struct Endpoint: Hashable, Sendable { } /// A resolver capable of resolving targets of type ``Target``. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NameResolverFactory { /// The type of ``ResolvableTarget`` this factory makes resolvers for. associatedtype Target: ResolvableTarget @@ -106,7 +106,7 @@ public protocol NameResolverFactory { func resolver(for target: Target) -> NameResolver } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NameResolverFactory { /// Returns whether the given target is compatible with this factory. /// diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift index a1393bd90..c8e847196 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift @@ -37,7 +37,7 @@ /// // ... /// } /// ``` -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NameResolverRegistry { private enum Factory { case ipv4(NameResolvers.IPv4) diff --git a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift index 6e3e93265..0792d4e0a 100644 --- a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift +++ b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift @@ -41,9 +41,9 @@ private struct ConstantAsyncSequence: AsyncSequence, Sendable } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension RPCAsyncSequence where Element: Sendable { - static func constant(_ element: Element) -> RPCAsyncSequence { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension RPCAsyncSequence where Element: Sendable, Failure == any Error { + static func constant(_ element: Element) -> RPCAsyncSequence { return RPCAsyncSequence(wrapping: ConstantAsyncSequence(element: element)) } } diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index 653615b35..2a7685491 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -103,7 +103,7 @@ extension ChannelPipeline.SynchronousOperations { } extension ChannelPipeline.SynchronousOperations { - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package func configureGRPCClientPipeline( channel: any Channel, config: GRPCChannel.Config diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index 303cbea0c..7348174d8 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -19,7 +19,7 @@ import GRPCHTTP2Core import NIOCore import NIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ClientTransport { /// A `ClientTransport` using HTTP/2 built on top of `NIOPosix`. /// @@ -119,7 +119,7 @@ extension HTTP2ClientTransport { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ClientTransport.Posix { struct Connector: HTTP2Connector { private let config: HTTP2ClientTransport.Posix.Config @@ -149,7 +149,7 @@ extension HTTP2ClientTransport.Posix { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ClientTransport.Posix { public struct Config: Sendable { /// Configuration for HTTP/2 connections. @@ -191,7 +191,7 @@ extension HTTP2ClientTransport.Posix { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel.Config { init(posix: HTTP2ClientTransport.Posix.Config) { self.init( diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index c46516be0..1de2301aa 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -23,7 +23,7 @@ import NIOPosix extension HTTP2ServerTransport { /// A NIOPosix-backed implementation of a server transport. - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Posix: ServerTransport { private let address: GRPCHTTP2Core.SocketAddress private let config: Config @@ -292,7 +292,7 @@ extension HTTP2ServerTransport { } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ServerTransport.Posix { /// Configuration for the ``GRPCHTTP2TransportNIOPosix/GRPCHTTP2Core/HTTP2ServerTransport/Posix``. public struct Config: Sendable { diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index bd9ee0b91..8d85d7a25 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -24,7 +24,7 @@ import NIOTransportServices extension HTTP2ServerTransport { /// A NIO Transport Services-backed implementation of a server transport. - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct TransportServices: ServerTransport { private let address: GRPCHTTP2Core.SocketAddress private let config: Config @@ -283,7 +283,7 @@ extension HTTP2ServerTransport { } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ServerTransport.TransportServices { /// Configuration for the ``GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2Core/HTTP2ServerTransport/TransportServices``. public struct Config: Sendable { diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index 3667640ad..ca4c02853 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -35,7 +35,7 @@ import GRPCCore /// block until ``connect()`` is called or the task is cancelled. /// /// - SeeAlso: ``ClientTransport`` -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct InProcessClientTransport: ClientTransport { private enum State: Sendable { struct UnconnectedState { @@ -54,7 +54,9 @@ public struct InProcessClientTransport: ClientTransport { var openStreams: [Int: ( RPCStream, - RPCStream, RPCWriter.Closable> + RPCStream< + RPCAsyncSequence, RPCWriter.Closable + > )] var signalEndContinuation: AsyncStream.Continuation @@ -73,7 +75,9 @@ public struct InProcessClientTransport: ClientTransport { var openStreams: [Int: ( RPCStream, - RPCStream, RPCWriter.Closable> + RPCStream< + RPCAsyncSequence, RPCWriter.Closable + > )] var signalEndContinuation: AsyncStream.Continuation? @@ -93,7 +97,7 @@ public struct InProcessClientTransport: ClientTransport { case closed(ClosedState) } - public typealias Inbound = RPCAsyncSequence + public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable public let retryThrottle: RetryThrottle? @@ -229,8 +233,12 @@ public struct InProcessClientTransport: ClientTransport { options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { - let request = RPCAsyncSequence._makeBackpressuredStream(watermarks: (16, 32)) - let response = RPCAsyncSequence._makeBackpressuredStream(watermarks: (16, 32)) + let request = RPCAsyncSequence._makeBackpressuredStream( + watermarks: (16, 32) + ) + let response = RPCAsyncSequence._makeBackpressuredStream( + watermarks: (16, 32) + ) let clientStream = RPCStream( descriptor: descriptor, diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 5803dc93a..a4ea05e7b 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -26,9 +26,9 @@ import GRPCCore /// To stop listening to new requests, call ``stopListening()``. /// /// - SeeAlso: ``ClientTransport`` -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct InProcessServerTransport: ServerTransport, Sendable { - public typealias Inbound = RPCAsyncSequence + public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable private let newStreams: AsyncStream> diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index d6dc262a2..ec553e72a 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -24,7 +24,7 @@ public enum InProcessTransport { /// - Parameters: /// - serviceConfig: Configuration describing how methods should be executed. /// - Returns: A tuple containing the connected server and client in-process transports. - @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public static func makePair( serviceConfig: ServiceConfig = ServiceConfig() ) -> (server: InProcessServerTransport, client: InProcessClientTransport) { diff --git a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift index 2bb9395c5..11ba12d28 100644 --- a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift +++ b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift @@ -23,7 +23,7 @@ import Tracing /// metadata. It will then be picked up by the server-side ``ServerTracingInterceptor``. /// /// For more information, refer to the documentation for `swift-distributed-tracing`. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct ClientTracingInterceptor: ClientInterceptor { private let injector: ClientRequestInjector private let emitEventOnEachWrite: Bool diff --git a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift index d91da7051..78b992a40 100644 --- a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift +++ b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift @@ -21,7 +21,7 @@ import Tracing /// /// The extracted tracing information is made available to user code via the current `ServiceContext`. /// For more information, refer to the documentation for `swift-distributed-tracing`. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct ServerTracingInterceptor: ServerInterceptor { private let extractor: ServerRequestExtractor private let emitEventOnEachWrite: Bool diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift index 0db0ea780..81c4c40e5 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -28,50 +28,50 @@ public enum Grpc_Testing_EmptyService { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Grpc_Testing_EmptyServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = Grpc_Testing_EmptyServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = Grpc_Testing_EmptyServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = Grpc_Testing_EmptyServiceClient } /// A service that has zero methods. /// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_EmptyServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_EmptyService.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// A service that has zero methods. /// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_EmptyServiceServiceProtocol: Grpc_Testing_EmptyService.StreamingServiceProtocol {} /// Partial conformance to `Grpc_Testing_EmptyServiceStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_EmptyService.ServiceProtocol { } /// A service that has zero methods. /// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_EmptyServiceClientProtocol: Sendable {} -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_EmptyService.ClientProtocol { } /// A service that has zero methods. /// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_EmptyServiceClient: Grpc_Testing_EmptyService.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index 3defa4bc4..8cbff1728 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -50,13 +50,13 @@ public enum Grpc_Testing_ReconnectService { Stop.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Grpc_Testing_ReconnectServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = Grpc_Testing_ReconnectServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = Grpc_Testing_ReconnectServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = Grpc_Testing_ReconnectServiceClient } @@ -137,13 +137,13 @@ public enum Grpc_Testing_TestService { UnimplementedCall.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Grpc_Testing_TestServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = Grpc_Testing_TestServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = Grpc_Testing_TestServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = Grpc_Testing_TestServiceClient } @@ -161,19 +161,19 @@ public enum Grpc_Testing_UnimplementedService { UnimplementedCall.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Grpc_Testing_UnimplementedServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = Grpc_Testing_UnimplementedServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = Grpc_Testing_UnimplementedServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = Grpc_Testing_UnimplementedServiceClient } /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One empty request followed by one empty response. func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -211,9 +211,9 @@ public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.Regis } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_TestService.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_TestService.Method.EmptyCall.descriptor, @@ -284,7 +284,7 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestService.StreamingServiceProtocol { /// One empty request followed by one empty response. func emptyCall(request: ServerRequest.Single) async throws -> ServerResponse.Single @@ -322,7 +322,7 @@ public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestServic } /// Partial conformance to `Grpc_Testing_TestServiceStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_TestService.ServiceProtocol { public func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.emptyCall(request: ServerRequest.Single(stream: request)) @@ -357,16 +357,16 @@ extension Grpc_Testing_TestService.ServiceProtocol { /// A simple service NOT implemented at servers so clients can test for /// that case. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_UnimplementedServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// A call that no server should implement func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, @@ -381,14 +381,14 @@ extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { /// A simple service NOT implemented at servers so clients can test for /// that case. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_UnimplementedServiceServiceProtocol: Grpc_Testing_UnimplementedService.StreamingServiceProtocol { /// A call that no server should implement func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Grpc_Testing_UnimplementedServiceStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_UnimplementedService.ServiceProtocol { public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) @@ -397,7 +397,7 @@ extension Grpc_Testing_UnimplementedService.ServiceProtocol { } /// A service used to control reconnect server. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -405,9 +405,9 @@ public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore. } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_ReconnectService.Method.Start.descriptor, @@ -429,7 +429,7 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { } /// A service used to control reconnect server. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_ReconnectService.StreamingServiceProtocol { func start(request: ServerRequest.Single) async throws -> ServerResponse.Single @@ -437,7 +437,7 @@ public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_Recon } /// Partial conformance to `Grpc_Testing_ReconnectServiceStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_ReconnectService.ServiceProtocol { public func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.start(request: ServerRequest.Single(stream: request)) @@ -452,7 +452,7 @@ extension Grpc_Testing_ReconnectService.ServiceProtocol { /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { /// One empty request followed by one empty response. func emptyCall( @@ -537,7 +537,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { ) async throws -> R where R: Sendable } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_TestService.ClientProtocol { public func emptyCall( request: ClientRequest.Single, @@ -654,7 +654,7 @@ extension Grpc_Testing_TestService.ClientProtocol { /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -819,7 +819,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// A simple service NOT implemented at servers so clients can test for /// that case. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { /// A call that no server should implement func unimplementedCall( @@ -831,7 +831,7 @@ public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { ) async throws -> R where R: Sendable } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_UnimplementedService.ClientProtocol { public func unimplementedCall( request: ClientRequest.Single, @@ -850,7 +850,7 @@ extension Grpc_Testing_UnimplementedService.ClientProtocol { /// A simple service NOT implemented at servers so clients can test for /// that case. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_UnimplementedService.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -878,7 +878,7 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente } /// A service used to control reconnect server. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { func start( request: ClientRequest.Single, @@ -897,7 +897,7 @@ public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { ) async throws -> R where R: Sendable } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_ReconnectService.ClientProtocol { public func start( request: ClientRequest.Single, @@ -929,7 +929,7 @@ extension Grpc_Testing_ReconnectService.ClientProtocol { } /// A service used to control reconnect server. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index 71cdc55f0..d41a700b7 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -15,7 +15,7 @@ */ import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol InteroperabilityTest { /// Run a test case using the given connection. /// @@ -69,7 +69,7 @@ public enum InteroperabilityTestCase: String, CaseIterable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension InteroperabilityTestCase { /// Return a new instance of the test case. public func makeTest() -> any InteroperabilityTest { diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index f2b320ea0..4d5902b26 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -31,7 +31,7 @@ import struct Foundation.Data /// Client asserts: /// - call was successful /// - response is non-null -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EmptyUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -65,7 +65,7 @@ struct EmptyUnary: InteroperabilityTest { /// - response payload body is 314159 bytes in size /// - clients are free to assert that the response payload body contents are zero and comparing /// the entire response message against a golden response -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct LargeUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -141,7 +141,7 @@ struct LargeUnary: InteroperabilityTest { /// - Response payload body is 314159 bytes in size. /// - Clients are free to assert that the response payload body contents are zeros and comparing the /// entire response message against a golden response. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) class ClientCompressedUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -250,7 +250,7 @@ class ClientCompressedUnary: InteroperabilityTest { /// - response payload body is 314159 bytes in size in both cases. /// - clients are free to assert that the response payload body contents are zero and comparing the /// entire response message against a golden response -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) class ServerCompressedUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -340,7 +340,7 @@ class ServerCompressedUnary: InteroperabilityTest { /// Client asserts: /// - call was successful /// - response aggregated_payload_size is 74922 -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ClientStreaming: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -391,7 +391,7 @@ struct ClientStreaming: InteroperabilityTest { /// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 /// - clients are free to assert that the response payload body contents are zero and /// comparing the entire response messages against golden responses -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ServerStreaming: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -468,7 +468,7 @@ struct ServerStreaming: InteroperabilityTest { /// - clients are free to assert that the response payload body contents are zero and comparing the /// entire response messages against golden responses class ServerCompressedStreaming: InteroperabilityTest { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) let request: Grpc_Testing_StreamingOutputCallRequest = .with { request in @@ -580,7 +580,7 @@ class ServerCompressedStreaming: InteroperabilityTest { /// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 /// - clients are free to assert that the response payload body contents are zero and /// comparing the entire response messages against golden responses -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct PingPong: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -647,7 +647,7 @@ struct PingPong: InteroperabilityTest { /// Client asserts: /// - call was successful /// - exactly zero responses -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EmptyStream: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -703,7 +703,7 @@ struct EmptyStream: InteroperabilityTest { /// received in the initial metadata for calls in Procedure steps 1 and 2. /// - metadata with key "x-grpc-test-echo-trailing-bin" and value 0xababab is received in the /// trailing metadata for calls in Procedure steps 1 and 2. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct CustomMetadata: InteroperabilityTest { let initialMetadataName = "x-grpc-test-echo-initial" let initialMetadataValue = "test_initial_metadata_value" @@ -824,7 +824,7 @@ struct CustomMetadata: InteroperabilityTest { /// Client asserts: /// - received status code is the same as the sent code for both Procedure steps 1 and 2 /// - received status message is the same as the sent message for both Procedure steps 1 and 2 -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct StatusCodeAndMessage: InteroperabilityTest { let expectedCode = 2 let expectedMessage = "test status message" @@ -902,7 +902,7 @@ struct StatusCodeAndMessage: InteroperabilityTest { /// - received status code is the same as the sent code for Procedure step 1 /// - received status message is the same as the sent message for Procedure step 1, including all /// whitespace characters -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct SpecialStatusMessage: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -945,7 +945,7 @@ struct SpecialStatusMessage: InteroperabilityTest { /// /// Client asserts: /// - received status code is 12 (UNIMPLEMENTED) -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct UnimplementedMethod: InteroperabilityTest { func run(client: GRPCClient) async throws { let testServiceClient = Grpc_Testing_TestService.Client(client: client) @@ -978,7 +978,7 @@ struct UnimplementedMethod: InteroperabilityTest { /// /// Client asserts: /// - received status code is 12 (UNIMPLEMENTED) -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct UnimplementedService: InteroperabilityTest { func run(client: GRPCClient) async throws { let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(client: client) diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift index 0307c4e1a..669cf5c41 100644 --- a/Sources/InteroperabilityTests/TestService.swift +++ b/Sources/InteroperabilityTests/TestService.swift @@ -17,7 +17,7 @@ import Foundation import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct TestService: Grpc_Testing_TestService.ServiceProtocol { public init() {} diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 96d485fa3..769b3a893 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -50,16 +50,16 @@ internal enum Grpc_Health_V1_Health { Watch.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Grpc_Health_V1_HealthStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ServiceProtocol = Grpc_Health_V1_HealthServiceProtocol } /// Health is gRPC's mechanism for checking whether a server is able to handle /// RPCs. Its semantics are documented in /// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Check gets the health of the specified service. If the requested service /// is unknown, the call will fail with status NOT_FOUND. If the caller does @@ -91,9 +91,9 @@ internal protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.Regist } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Health_V1_Health.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Health_V1_Health.Method.Check.descriptor, @@ -117,7 +117,7 @@ extension Grpc_Health_V1_Health.StreamingServiceProtocol { /// Health is gRPC's mechanism for checking whether a server is able to handle /// RPCs. Its semantics are documented in /// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.StreamingServiceProtocol { /// Check gets the health of the specified service. If the requested service /// is unknown, the call will fail with status NOT_FOUND. If the caller does @@ -149,7 +149,7 @@ internal protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.St } /// Partial conformance to `Grpc_Health_V1_HealthStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Health_V1_Health.ServiceProtocol { internal func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.check(request: ServerRequest.Single(stream: request)) diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift index f0c8fb75b..3694a5827 100644 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -22,7 +22,7 @@ import InteroperabilityTests import NIOPosix @main -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct InteroperabilityTestsExecutable: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "gRPC Swift Interoperability Runner", diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 6f17f961a..588055363 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -19,7 +19,7 @@ import Foundation import GRPCCore import NIOConcurrencyHelpers -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct BenchmarkClient { private let _isShuttingDown = ManagedAtomic(false) @@ -171,7 +171,9 @@ struct BenchmarkClient { // stream is sent to the request closure, and the request closure indicates the outcome back // to the response handler to keep the RPC alive for the appropriate amount of time. let status = AsyncStream.makeStream(of: RPCError.self) - let response = AsyncStream.makeStream(of: RPCAsyncSequence.self) + let response = AsyncStream.makeStream( + of: RPCAsyncSequence.self + ) let request = ClientRequest.Stream(of: Grpc_Testing_SimpleRequest.self) { writer in defer { status.continuation.finish() } diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift index 3d0bb03fe..45dd1a972 100644 --- a/Sources/performance-worker/BenchmarkService.swift +++ b/Sources/performance-worker/BenchmarkService.swift @@ -19,7 +19,7 @@ import GRPCCore import struct Foundation.Data -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Used to check if the server can be streaming responses. private let working = ManagedAtomic(true) @@ -157,7 +157,7 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension BenchmarkService { private func checkOkStatus(_ responseStatus: Grpc_Testing_EchoStatus) throws { guard let code = Status.Code(rawValue: Int(responseStatus.code)) else { diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index c5c433b34..315c57233 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -77,17 +77,17 @@ internal enum Grpc_Testing_BenchmarkService { StreamingBothWays.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Grpc_Testing_BenchmarkServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ServiceProtocol = Grpc_Testing_BenchmarkServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ClientProtocol = Grpc_Testing_BenchmarkServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Grpc_Testing_BenchmarkServiceClient } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One request followed by one response. /// The server returns the client payload as-is. @@ -112,9 +112,9 @@ internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCor } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, @@ -159,7 +159,7 @@ extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_BenchmarkService.StreamingServiceProtocol { /// One request followed by one response. /// The server returns the client payload as-is. @@ -184,7 +184,7 @@ internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_Ben } /// Partial conformance to `Grpc_Testing_BenchmarkServiceStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_BenchmarkService.ServiceProtocol { internal func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) @@ -202,7 +202,7 @@ extension Grpc_Testing_BenchmarkService.ServiceProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { /// One request followed by one response. /// The server returns the client payload as-is. @@ -256,7 +256,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { ) async throws -> R where R: Sendable } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_BenchmarkService.ClientProtocol { internal func unaryCall( request: ClientRequest.Single, @@ -329,7 +329,7 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkService.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index 0e05eaa89..3cfdba799 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -68,13 +68,13 @@ internal enum Grpc_Testing_WorkerService { QuitWorker.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Grpc_Testing_WorkerServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ServiceProtocol = Grpc_Testing_WorkerServiceServiceProtocol } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Start server with specified workload. /// First request sent specifies the ServerConfig followed by ServerStatus @@ -100,9 +100,9 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_WorkerService.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.RunServer.descriptor, @@ -139,7 +139,7 @@ extension Grpc_Testing_WorkerService.StreamingServiceProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_WorkerService.StreamingServiceProtocol { /// Start server with specified workload. /// First request sent specifies the ServerConfig followed by ServerStatus @@ -165,7 +165,7 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker } /// Partial conformance to `Grpc_Testing_WorkerServiceStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_WorkerService.ServiceProtocol { internal func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.coreCount(request: ServerRequest.Single(stream: request)) diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift index cf057c20d..ca8c5005b 100644 --- a/Sources/performance-worker/PerformanceWorker.swift +++ b/Sources/performance-worker/PerformanceWorker.swift @@ -21,7 +21,7 @@ import GRPCHTTP2TransportNIOPosix import NIOPosix @main -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct PerformanceWorker: AsyncParsableCommand { static var configuration: CommandConfiguration { CommandConfiguration( diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index b55c37f1b..aa800e20a 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -21,7 +21,7 @@ import NIOConcurrencyHelpers import NIOCore import NIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class WorkerService: Sendable { private let state: NIOLockedValueBox @@ -222,7 +222,7 @@ final class WorkerService: Sendable { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { func quitWorker( request: ServerRequest.Single @@ -373,7 +373,7 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension WorkerService { private func startServer( _ serverConfig: Grpc_Testing_ServerConfig @@ -561,7 +561,7 @@ extension WorkerService { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension BenchmarkClient.RPCType { init?(_ rpcType: Grpc_Testing_RpcType) { switch rpcType { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 4ec08dcfc..bd4e5d268 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -43,7 +43,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -54,7 +54,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Single, @@ -71,7 +71,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -124,7 +124,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -135,7 +135,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Stream, @@ -152,7 +152,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -205,7 +205,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -216,7 +216,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Single, @@ -233,7 +233,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -286,7 +286,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -297,7 +297,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: ClientRequest.Stream, @@ -314,7 +314,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -375,7 +375,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -395,7 +395,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { package func methodA( request: ClientRequest.Stream, @@ -426,7 +426,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -497,7 +497,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -508,7 +508,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ClientProtocol { internal func methodA( request: ClientRequest.Single, @@ -525,7 +525,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct ServiceAClient: ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -584,13 +584,13 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAClientProtocol: Sendable {} - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -601,15 +601,15 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for ServiceB /// /// Line 2 - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ServiceBClientProtocol: Sendable {} - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceB.ClientProtocol { } /// Documentation for ServiceB /// /// Line 2 - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct ServiceBClient: ServiceB.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 82370da1c..f66778bbf 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -174,29 +174,29 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } /// Documentation for AService - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for AService - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 9d59ebb6d..ebff94eda 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -51,15 +51,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, @@ -72,13 +72,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unary(request: ServerRequest.Single(stream: request)) @@ -120,15 +120,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, @@ -141,13 +141,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) @@ -193,15 +193,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, @@ -214,13 +214,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) @@ -266,15 +266,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.BidirectionalStreaming.descriptor, @@ -287,13 +287,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } """ @@ -347,7 +347,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -356,9 +356,9 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, @@ -379,7 +379,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single @@ -388,7 +388,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.inputStreaming(request: request) @@ -431,15 +431,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: ServiceA.Method.MethodA.descriptor, @@ -452,13 +452,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ServiceProtocol { internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.methodA(request: ServerRequest.Single(stream: request)) @@ -498,35 +498,35 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceA - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } /// Documentation for ServiceB - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceB.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for ServiceB - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceBServiceProtocol: NamespaceA_ServiceB.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceBStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceB.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 1363e9c0e..1c73deb9a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -60,13 +60,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { MethodA.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } """ @@ -97,13 +97,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } """ @@ -134,9 +134,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } """ @@ -167,9 +167,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } """ @@ -243,13 +243,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { MethodA.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = ServiceAClient } """ @@ -315,13 +315,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { MethodB.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } """ @@ -352,13 +352,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = NamespaceA_ServiceAClient } """ @@ -401,26 +401,26 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_AserviceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_AserviceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_AserviceClient } public enum NamespaceA_Bservice { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_BserviceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_BserviceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_BserviceClient } """ @@ -455,26 +455,26 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = AServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ServiceProtocol = AServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ClientProtocol = AServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = AServiceClient } package enum BService { package enum Method { package static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ServiceProtocol = BServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ClientProtocol = BServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = BServiceClient } """ @@ -517,26 +517,26 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ClientProtocol = Anamespace_AServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Anamespace_AServiceClient } internal enum Bnamespace_BService { internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Bnamespace_BServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Bnamespace_BServiceClient } """ @@ -573,26 +573,26 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = Anamespace_AServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = Anamespace_AServiceClient } public enum BService { public enum Method { public static let descriptors: [MethodDescriptor] = [] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = BServiceServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = BServiceClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = BServiceClient } """ diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index d435c6fb8..a284112be 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ClientResponseTests: XCTestCase { func testAcceptedSingleResponseConvenienceMethods() { let response = ClientResponse.Single( @@ -59,7 +59,7 @@ final class ClientResponseTests: XCTestCase { of: String.self, metadata: ["foo": "bar"], bodyParts: RPCAsyncSequence( - wrapping: AsyncStream { + wrapping: AsyncThrowingStream { $0.yield(.message("foo")) $0.yield(.message("bar")) $0.yield(.message("baz")) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index f3962bf86..a2bed0d7d 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -18,17 +18,23 @@ import XCTest @testable import GRPCCore -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTestHarness { struct ServerStreamHandler: Sendable { private let handler: @Sendable ( - _ stream: RPCStream, RPCWriter.Closable> + _ stream: RPCStream< + RPCAsyncSequence, + RPCWriter.Closable + > ) async throws -> Void init( _ handler: @escaping @Sendable ( - RPCStream, RPCWriter.Closable> + RPCStream< + RPCAsyncSequence, + RPCWriter.Closable + > ) async throws -> Void ) { self.handler = handler @@ -36,10 +42,15 @@ extension ClientRPCExecutorTestHarness { func handle( stream: RPCStream - ) async throws where Inbound.Element == RPCRequestPart, Outbound.Element == RPCResponsePart { + ) async throws + where + Inbound.Element == RPCRequestPart, + Inbound.Failure == any Error, + Outbound.Element == RPCResponsePart + { let erased = RPCStream( descriptor: stream.descriptor, - inbound: RPCAsyncSequence(wrapping: stream.inbound), + inbound: RPCAsyncSequence(wrapping: stream.inbound), outbound: RPCWriter.Closable(wrapping: stream.outbound) ) @@ -48,7 +59,7 @@ extension ClientRPCExecutorTestHarness { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { return Self { stream in diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 18039ed97..30f0e1000 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -17,7 +17,7 @@ import Atomics import GRPCCore import GRPCInProcessTransport -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension InProcessServerTransport { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index c8f82c983..9a23448ac 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -25,7 +25,7 @@ import XCTest /// of the server to allow for flexible testing scenarios with minimal boilerplate. The harness /// also tracks how many streams the client has opened, how many streams the server accepted, and /// how many streams the client failed to open. -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ClientRPCExecutorTestHarness { private let server: ServerStreamHandler private let clientTransport: StreamCountingClientTransport diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index 94209a1d8..ffeeae724 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTests { func testHedgingWhenAllAttemptsResultInNonFatalCodes() async throws { let harness = ClientRPCExecutorTestHarness( diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index ccea73429..ca094dba9 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTests { fileprivate func makeHarnessForRetries( rejectUntilAttempt firstSuccessfulAttempt: Int, diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 2a59101b4..43c8a1225 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -18,7 +18,7 @@ import XCTest @testable import GRPCCore -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index 9febd2269..154a87bca 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -17,7 +17,7 @@ import XCTest @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ServerRPCExecutorTestHarness { struct ServerHandler: Sendable { let fn: @Sendable (ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -54,7 +54,7 @@ struct ServerRPCExecutorTestHarness { ServerRequest.Stream ) async throws -> ServerResponse.Stream, producer: @escaping (RPCWriter.Closable) async throws -> Void, - consumer: @escaping (RPCAsyncSequence) async throws -> Void + consumer: @escaping (RPCAsyncSequence) async throws -> Void ) async throws { try await self.execute( deserializer: deserializer, @@ -70,7 +70,7 @@ struct ServerRPCExecutorTestHarness { serializer: some MessageSerializer, handler: ServerHandler, producer: @escaping (RPCWriter.Closable) async throws -> Void, - consumer: @escaping (RPCAsyncSequence) async throws -> Void + consumer: @escaping (RPCAsyncSequence) async throws -> Void ) async throws { let input = RPCAsyncSequence.makeBackpressuredStream( of: RPCRequestPart.self, @@ -112,7 +112,7 @@ struct ServerRPCExecutorTestHarness { func execute( handler: ServerHandler<[UInt8], [UInt8]> = .echo, producer: @escaping (RPCWriter.Closable) async throws -> Void, - consumer: @escaping (RPCAsyncSequence) async throws -> Void + consumer: @escaping (RPCAsyncSequence) async throws -> Void ) async throws { try await self.execute( deserializer: IdentityDeserializer(), @@ -124,7 +124,7 @@ struct ServerRPCExecutorTestHarness { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRPCExecutorTestHarness.ServerHandler where Input == Output { static var echo: Self { return Self { request in diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 4f3c3e731..e552991fa 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -19,7 +19,7 @@ import XCTest @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ServerRPCExecutorTests: XCTestCase { func testEchoNoMessages() async throws { let harness = ServerRPCExecutorTestHarness() diff --git a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift index 8fd0c1a98..c1d9a023f 100644 --- a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift @@ -17,7 +17,7 @@ import GRPCCore import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class RPCRouterTests: XCTestCase { func testEmptyRouter() async throws { var router = RPCRouter() diff --git a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift index 50446f224..532e5e51c 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift @@ -16,7 +16,7 @@ @_spi(Testing) import GRPCCore import XCTest -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ServerRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let single = ServerRequest.Single(metadata: ["bar": "baz"], message: "foo") diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index fedc798ee..55ba15505 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -18,7 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 507803dab..6c66d7ca0 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -18,7 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], diff --git a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift index 472bda341..648e935e9 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift @@ -18,16 +18,17 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal final class AsyncSequenceOfOneTests: XCTestCase { func testSuccessPath() async throws { - let sequence = RPCAsyncSequence.one("foo") + let sequence = RPCAsyncSequence.one("foo") let contents = try await sequence.collect() XCTAssertEqual(contents, ["foo"]) } func testFailurePath() async throws { - let sequence = RPCAsyncSequence.throwing(RPCError(code: .cancelled, message: "foo")) + let error = RPCError(code: .cancelled, message: "foo") + let sequence = RPCAsyncSequence.throwing(error) do { let _ = try await sequence.collect() diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index 46264437f..6a4ed2a41 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -16,7 +16,7 @@ import Atomics import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientInterceptor where Self == RejectAllClientInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllClientInterceptor(error: error, throw: false) @@ -28,7 +28,7 @@ extension ClientInterceptor where Self == RejectAllClientInterceptor { } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientInterceptor where Self == RequestCountingClientInterceptor { static func requestCounter(_ counter: ManagedAtomic) -> Self { return RequestCountingClientInterceptor(counter: counter) @@ -36,7 +36,7 @@ extension ClientInterceptor where Self == RequestCountingClientInterceptor { } /// Rejects all RPCs with the provided error. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RejectAllClientInterceptor: ClientInterceptor { /// The error to reject all RPCs with. let error: RPCError @@ -65,7 +65,7 @@ struct RejectAllClientInterceptor: ClientInterceptor { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RequestCountingClientInterceptor: ClientInterceptor { /// The number of requests made. let counter: ManagedAtomic diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 169b90969..c70970eed 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -16,7 +16,7 @@ import Atomics import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerInterceptor where Self == RejectAllServerInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllServerInterceptor(error: error, throw: false) @@ -27,7 +27,7 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerInterceptor where Self == RequestCountingServerInterceptor { static func requestCounter(_ counter: ManagedAtomic) -> Self { return RequestCountingServerInterceptor(counter: counter) @@ -35,7 +35,7 @@ extension ServerInterceptor where Self == RequestCountingServerInterceptor { } /// Rejects all RPCs with the provided error. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RejectAllServerInterceptor: ServerInterceptor { /// The error to reject all RPCs with. let error: RPCError @@ -64,7 +64,7 @@ struct RejectAllServerInterceptor: ServerInterceptor { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RequestCountingServerInterceptor: ServerInterceptor { /// The number of requests made. let counter: ManagedAtomic diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift index 23a87d58b..b996e3d27 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift @@ -15,14 +15,14 @@ */ import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension RPCAsyncSequence { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension RPCAsyncSequence where Failure == any Error { static func elements(_ elements: Element...) -> Self { return .elements(elements) } static func elements(_ elements: [Element]) -> Self { - let stream = AsyncStream { + let stream = AsyncThrowingStream { for element in elements { $0.yield(element) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index edcc358fc..f1b462376 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -16,7 +16,7 @@ import GRPCCore import XCTest -@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct BinaryEcho: RegistrableRPCService { func get( _ request: ServerRequest.Single<[UInt8]> diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index 7fbd83a9a..d385f719e 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -15,9 +15,9 @@ */ @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct AnyClientTransport: ClientTransport, Sendable { - typealias Inbound = RPCAsyncSequence + typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable private let _retryThrottle: @Sendable () -> RetryThrottle? @@ -81,13 +81,15 @@ struct AnyClientTransport: ClientTransport, Sendable { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct AnyServerTransport: ServerTransport, Sendable { - typealias Inbound = RPCAsyncSequence + typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable private let _listen: - @Sendable (@escaping (RPCStream) async -> Void) async throws -> Void + @Sendable ( + @escaping @Sendable (RPCStream) async -> Void + ) async throws -> Void private let _stopListening: @Sendable () -> Void init(wrapping transport: Transport) { @@ -96,7 +98,7 @@ struct AnyServerTransport: ServerTransport, Sendable { } func listen( - _ streamHandler: @escaping (RPCStream) async -> Void + _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { try await self._listen(streamHandler) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index e1b52d45e..0c49e7820 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -17,9 +17,9 @@ import Atomics @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct StreamCountingClientTransport: ClientTransport, Sendable { - typealias Inbound = RPCAsyncSequence + typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable private let transport: AnyClientTransport @@ -77,9 +77,9 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct StreamCountingServerTransport: ServerTransport, Sendable { - typealias Inbound = RPCAsyncSequence + typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable private let transport: AnyServerTransport diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 80dd0741f..4e5dca3ab 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -15,9 +15,9 @@ */ @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ThrowOnStreamCreationTransport: ClientTransport { - typealias Inbound = RPCAsyncSequence + typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable private let code: RPCError.Code diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 88aec4a1f..d2e8511bc 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -95,7 +95,7 @@ func XCTAssertThrowsRPCErrorAsync( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func XCTAssertRejected( _ response: ClientResponse.Stream, errorHandler: (RPCError) -> Void diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index d68a36914..8757445f1 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -20,7 +20,7 @@ import NIOHTTP2 import NIOPosix import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class GRPCChannelTests: XCTestCase { func testDefaultServiceConfig() throws { var serviceConfig = ServiceConfig() @@ -798,7 +798,7 @@ final class GRPCChannelTests: XCTestCase { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel.Config { static var defaults: Self { Self( @@ -816,7 +816,7 @@ extension Endpoint { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel { fileprivate func serverAddress() async throws -> String? { let values: Metadata.StringValues? = try await self.withStream( diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift index ba4ab7cf2..68d8dfbf7 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift @@ -17,7 +17,7 @@ import GRPCCore import GRPCHTTP2Core -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NameResolver { static func `static`( endpoints: [Endpoint], @@ -36,8 +36,8 @@ extension NameResolver { static func `dynamic`( updateMode: UpdateMode - ) -> (Self, AsyncStream.Continuation) { - let (stream, continuation) = AsyncStream.makeStream(of: NameResolutionResult.self) + ) -> (Self, AsyncThrowingStream.Continuation) { + let (stream, continuation) = AsyncThrowingStream.makeStream(of: NameResolutionResult.self) let resolver = NameResolver(names: RPCAsyncSequence(wrapping: stream), updateMode: updateMode) return (resolver, continuation) } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift index 11102f999..82d27a421 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift @@ -18,7 +18,7 @@ import GRPCCore import GRPCHTTP2Core import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class NameResolverRegistryTests: XCTestCase { struct FailingResolver: NameResolverFactory { typealias Target = StringTarget @@ -152,7 +152,7 @@ final class NameResolverRegistryTests: XCTestCase { struct CustomResolver: NameResolverFactory { func resolver(for target: EmptyTarget) -> NameResolver { return NameResolver( - names: RPCAsyncSequence(wrapping: AsyncStream { $0.finish() }), + names: RPCAsyncSequence(wrapping: AsyncThrowingStream { $0.finish() }), updateMode: .push ) } diff --git a/Tests/GRPCHTTP2TransportTests/ControlService.swift b/Tests/GRPCHTTP2TransportTests/ControlService.swift index 4f089673e..dfb83b132 100644 --- a/Tests/GRPCHTTP2TransportTests/ControlService.swift +++ b/Tests/GRPCHTTP2TransportTests/ControlService.swift @@ -18,7 +18,7 @@ import GRPCCore import struct Foundation.Data -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ControlService: ControlStreamingServiceProtocol { func unary( request: ServerRequest.Stream @@ -45,7 +45,7 @@ struct ControlService: ControlStreamingServiceProtocol { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ControlService { private func handle( request: ServerRequest.Stream diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index 5fc3f8a66..61e0f85b5 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -66,13 +66,13 @@ internal enum Control { BidiStream.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = ControlStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ServiceProtocol = ControlServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ClientProtocol = ControlClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = ControlClient } @@ -80,7 +80,7 @@ internal enum Control { /// /// The control service has one RPC of each kind, the input to each RPC controls /// the output. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCService { func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -92,9 +92,9 @@ internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCServic } /// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Control.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Control.Method.Unary.descriptor, @@ -135,7 +135,7 @@ extension Control.StreamingServiceProtocol { /// /// The control service has one RPC of each kind, the input to each RPC controls /// the output. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single @@ -147,7 +147,7 @@ internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { } /// Partial conformance to `ControlStreamingServiceProtocol`. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Control.ServiceProtocol { internal func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unary(request: ServerRequest.Single(stream: request)) @@ -169,7 +169,7 @@ extension Control.ServiceProtocol { /// /// The control service has one RPC of each kind, the input to each RPC controls /// the output. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlClientProtocol: Sendable { func unary( request: ClientRequest.Single, @@ -204,7 +204,7 @@ internal protocol ControlClientProtocol: Sendable { ) async throws -> R where R: Sendable } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Control.ClientProtocol { internal func unary( request: ClientRequest.Single, @@ -267,7 +267,7 @@ extension Control.ClientProtocol { /// /// The control service has one RPC of each kind, the input to each RPC controls /// the output. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct ControlClient: Control.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index 09124889f..252fbc29f 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -19,7 +19,7 @@ import GRPCHTTP2Core import GRPCHTTP2TransportNIOPosix import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportNIOPosixTests: XCTestCase { func testGetListeningAddress_IPv4() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index f6b0de779..cca04cf05 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -20,7 +20,7 @@ import GRPCHTTP2Core import GRPCHTTP2TransportNIOTransportServices import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_IPv4() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 60fbbbe21..7d21dae8c 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -21,7 +21,7 @@ import GRPCHTTP2TransportNIOTransportServices import GRPCProtobuf import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportTests: XCTestCase { // A combination of client and server transport kinds. struct Transport: Sendable, CustomStringConvertible { @@ -1411,7 +1411,7 @@ final class HTTP2TransportTests: XCTestCase { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension [HTTP2TransportTests.Transport] { static let supported = [ HTTP2TransportTests.Transport(server: .posix, client: .posix), diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index f25174286..90de7c27a 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -18,7 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class InProcessClientTransportTests: XCTestCase { struct FailTest: Error {} diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 6e1991d9a..4d9c997d9 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -19,14 +19,17 @@ import XCTest @testable import GRPCCore @testable import GRPCInProcessTransport -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessServerTransport() - let stream = RPCStream, RPCWriter.Closable>( + let stream = RPCStream< + RPCAsyncSequence, + RPCWriter.Closable + >( descriptor: .init(service: "testService", method: "testMethod"), - inbound: RPCAsyncSequence( - wrapping: AsyncStream { + inbound: RPCAsyncSequence( + wrapping: AsyncThrowingStream { $0.yield(.message([42])) $0.finish() } @@ -57,11 +60,11 @@ final class InProcessServerTransportTests: XCTestCase { func testStopListening() async throws { let transport = InProcessServerTransport() let firstStream = RPCStream< - RPCAsyncSequence, RPCWriter.Closable + RPCAsyncSequence, RPCWriter.Closable >( descriptor: .init(service: "testService1", method: "testMethod1"), inbound: RPCAsyncSequence( - wrapping: AsyncStream { + wrapping: AsyncThrowingStream { $0.yield(.message([42])) $0.finish() } @@ -84,11 +87,11 @@ final class InProcessServerTransportTests: XCTestCase { transport.stopListening() let secondStream = RPCStream< - RPCAsyncSequence, RPCWriter.Closable + RPCAsyncSequence, RPCWriter.Closable >( descriptor: .init(service: "testService1", method: "testMethod1"), inbound: RPCAsyncSequence( - wrapping: AsyncStream { + wrapping: AsyncThrowingStream { $0.yield(.message([42])) $0.finish() } diff --git a/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift b/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift index fa2053a4f..5535c2d6c 100644 --- a/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift +++ b/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift @@ -20,13 +20,12 @@ import XCTest @testable import GRPCInterceptors -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class TracingInterceptorTests: XCTestCase { override class func setUp() { InstrumentationSystem.bootstrap(TestTracer()) } - #if swift(>=5.8) // Compiling these tests fails in 5.7 func testClientInterceptor() async throws { var serviceContext = ServiceContext.topLevel let traceIDString = UUID().uuidString @@ -56,10 +55,10 @@ final class TracingInterceptorTests: XCTestCase { return .init( metadata: [], - bodyParts: .init( - wrapping: AsyncStream { cont in - cont.yield(.message(["response"])) - cont.finish() + bodyParts: RPCAsyncSequence( + wrapping: AsyncThrowingStream { + $0.yield(.message(["response"])) + $0.finish() } ) ) @@ -121,10 +120,10 @@ final class TracingInterceptorTests: XCTestCase { return .init( metadata: [], - bodyParts: .init( - wrapping: AsyncStream { cont in - cont.yield(.message(["response"])) - cont.finish() + bodyParts: RPCAsyncSequence( + wrapping: AsyncThrowingStream { + $0.yield(.message(["response"])) + $0.finish() } ) ) @@ -167,7 +166,6 @@ final class TracingInterceptorTests: XCTestCase { ) } } - #endif // swift >= 5.7 func testServerInterceptorErrorResponse() async throws { let methodDescriptor = MethodDescriptor( diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index fd0d89d5f..9d847a6fa 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -73,14 +73,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { SayHello.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias ClientProtocol = Hello_World_GreeterClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Hello_World_GreeterClient } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Hello_World_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( @@ -92,7 +92,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Hello_World_Greeter.ClientProtocol { internal func sayHello( request: ClientRequest.Single, @@ -110,7 +110,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Hello_World_GreeterClient: Hello_World_Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -188,23 +188,23 @@ final class ProtobufCodeGeneratorTests: XCTestCase { SayHello.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Helloworld_Greeter.Method.SayHello.descriptor, @@ -218,14 +218,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting. func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.ServiceProtocol { public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) @@ -283,27 +283,27 @@ final class ProtobufCodeGeneratorTests: XCTestCase { SayHello.descriptor ] } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = GreeterStreamingServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ServiceProtocol = GreeterServiceProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ClientProtocol = GreeterClientProtocol - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = GreeterClient } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Greeter.StreamingServiceProtocol { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Greeter.Method.SayHello.descriptor, @@ -317,14 +317,14 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterServiceProtocol: Greeter.StreamingServiceProtocol { /// Sends a greeting. func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } /// Partial conformance to `GreeterStreamingServiceProtocol`. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Greeter.ServiceProtocol { package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) @@ -333,7 +333,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( @@ -345,7 +345,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) async throws -> R where R: Sendable } - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Greeter.ClientProtocol { package func sayHello( request: ClientRequest.Single, @@ -363,7 +363,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct GreeterClient: Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift index 6567f942b..6c4be3353 100644 --- a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift +++ b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift @@ -19,7 +19,7 @@ import GRPCInProcessTransport import InteroperabilityTests import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class InProcessInteroperabilityTests: XCTestCase { func runInProcessTransport( interopTestCase: InteroperabilityTestCase From 8af38b12ecefa75c204787dad0068538a089a42c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 16 Jul 2024 15:23:20 +0100 Subject: [PATCH 400/580] Avoid unnecessary CoWs in client connection handler (#1981) Motivation: The client connection handler keeps track of open streams in a set. This currently CoWs unnecessarily. Modifications: Add a modifying state and temporarily switch to it to avoid CoWs. Result: Fewer allocations --- .../Connection/ClientConnectionHandler.swift | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index be4006c32..e58b071da 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -433,6 +433,7 @@ extension ClientConnectionHandler { case active(Active) case closing(Closing) case closed + case _modifying struct Active { var openStreams: Set @@ -477,12 +478,16 @@ extension ClientConnectionHandler { mutating func receivedSettings() -> Bool { switch self.state { case .active(var active): + self.state = ._modifying let isFirstSettingsFrame = active.receivedSettings() self.state = .active(active) return isFirstSettingsFrame case .closing, .closed: return false + + case ._modifying: + preconditionFailure() } } @@ -490,10 +495,13 @@ extension ClientConnectionHandler { mutating func receivedError(_ error: any Error) { switch self.state { case .active(var active): + self.state = ._modifying active.error = error self.state = .active(active) case .closing, .closed: () + case ._modifying: + preconditionFailure() } } @@ -501,17 +509,22 @@ extension ClientConnectionHandler { mutating func streamOpened(_ id: HTTP2StreamID) { switch self.state { case .active(var state): + self.state = ._modifying let (inserted, _) = state.openStreams.insert(id) assert(inserted, "Can't open stream \(Int(id)), it's already open") self.state = .active(state) case .closing(var state): + self.state = ._modifying let (inserted, _) = state.openStreams.insert(id) assert(inserted, "Can't open stream \(Int(id)), it's already open") self.state = .closing(state) case .closed: () + + case ._modifying: + preconditionFailure() } } @@ -530,6 +543,7 @@ extension ClientConnectionHandler { switch self.state { case .active(var state): + self.state = ._modifying let removedID = state.openStreams.remove(id) assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") if state.openStreams.isEmpty { @@ -540,6 +554,7 @@ extension ClientConnectionHandler { self.state = .active(state) case .closing(var state): + self.state = ._modifying let removedID = state.openStreams.remove(id) assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") onStreamClosed = state.openStreams.isEmpty ? .close : .none @@ -547,13 +562,16 @@ extension ClientConnectionHandler { case .closed: onStreamClosed = .none + + case ._modifying: + preconditionFailure() } return onStreamClosed } /// Returns whether a keep alive ping should be sent to the server. - mutating func sendKeepalivePing() -> Bool { + func sendKeepalivePing() -> Bool { let sendKeepalivePing: Bool // Only send a ping if there are open streams or there are no open streams and keep alive @@ -565,6 +583,8 @@ extension ClientConnectionHandler { sendKeepalivePing = !state.openStreams.isEmpty || state.allowKeepaliveWithoutCalls case .closed: sendKeepalivePing = false + case ._modifying: + preconditionFailure() } return sendKeepalivePing @@ -580,6 +600,7 @@ extension ClientConnectionHandler { switch self.state { case .active(let state): + self.state = ._modifying // Only close immediately if there are no open streams. The client doesn't need to // ratchet down the last stream ID as only the client creates streams in gRPC. let close = state.openStreams.isEmpty @@ -587,12 +608,16 @@ extension ClientConnectionHandler { self.state = .closing(State.Closing(from: state, closePromise: promise)) case .closing(var state): + self.state = ._modifying state.closePromise.setOrCascade(to: promise) self.state = .closing(state) onGracefulShutdown = .none case .closed: onGracefulShutdown = .none + + case ._modifying: + preconditionFailure() } return onGracefulShutdown @@ -606,6 +631,8 @@ extension ClientConnectionHandler { return true case .closing, .closed: return false + case ._modifying: + preconditionFailure() } } @@ -627,6 +654,8 @@ extension ClientConnectionHandler { case .closed: self.state = .closed return .none + case ._modifying: + preconditionFailure() } } } From 9ae65503c0d7726f158154eec1899beb5e538ac3 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 17 Jul 2024 07:58:41 +0100 Subject: [PATCH 401/580] Save two allocations by making `MapRPCWriter` and `SerializingRPCWriter` generic over wrapped writer (#1983) Motivation: We currently wrap a base writer into an `RPCWriter` inside the `MapRPCWriter` and `SerializingRPCWriter`. This results in an unnecessary allocation in both cases. In the case of unary requests, this means two extra allocations per requests on the client side. Modifications: Make the `MapRPCWriter` and `SerializingRPCWriter` generic over the base writer, to avoid the extra allocations caused by wrapping them in an `RPCWriter`. Result: Two fewer allocations per unary request on the client side. --- .../GRPCCore/Streaming/Internal/RPCWriter+Map.swift | 8 ++++---- .../Streaming/Internal/RPCWriter+Serialize.swift | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift index c09d50240..620d9e137 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -16,18 +16,18 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline -struct MapRPCWriter: RPCWriterProtocol { +struct MapRPCWriter>: RPCWriterProtocol { @usableFromInline typealias Element = Value @usableFromInline - let base: RPCWriter + let base: Base @usableFromInline let transform: @Sendable (Value) -> Mapped @inlinable - init(base: some RPCWriterProtocol, transform: @escaping @Sendable (Value) -> Mapped) { - self.base = RPCWriter(wrapping: base) + init(base: Base, transform: @escaping @Sendable (Value) -> Mapped) { + self.base = base self.transform = transform } diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift index 62ea48739..f72df4ee6 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -16,19 +16,22 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline -struct SerializingRPCWriter: RPCWriterProtocol { +struct SerializingRPCWriter< + Base: RPCWriterProtocol<[UInt8]>, + Serializer: MessageSerializer +>: RPCWriterProtocol { @usableFromInline typealias Element = Serializer.Message @usableFromInline - let base: RPCWriter<[UInt8]> + let base: Base @usableFromInline let serializer: Serializer @inlinable - init(serializer: Serializer, base: some RPCWriterProtocol<[UInt8]>) { + init(serializer: Serializer, base: Base) { self.serializer = serializer - self.base = RPCWriter(wrapping: base) + self.base = base } @inlinable From c4f1a5f4bfd264279ac26f483009911f856ebba5 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Wed, 17 Jul 2024 08:09:03 +0100 Subject: [PATCH 402/580] Add `ServiceDescriptor` to GRPCCore (#1982) Motivation: `ServiceDescriptor` describes a service. It is needed for services that interact with the Health service. Modifications: - Add a `public struct ServiceDescriptor` to GRPCCore. Result: Services will be describable using `ServiceDescriptor`. --- Sources/GRPCCore/ServiceDescriptor.swift | 45 +++++++++++++++++++ .../ServiceDescriptorTests.swift | 26 +++++++++++ 2 files changed, 71 insertions(+) create mode 100644 Sources/GRPCCore/ServiceDescriptor.swift create mode 100644 Tests/GRPCCoreTests/ServiceDescriptorTests.swift diff --git a/Sources/GRPCCore/ServiceDescriptor.swift b/Sources/GRPCCore/ServiceDescriptor.swift new file mode 100644 index 000000000..b09730c3b --- /dev/null +++ b/Sources/GRPCCore/ServiceDescriptor.swift @@ -0,0 +1,45 @@ +/* + * 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. + */ + +/// A description of a service. +public struct ServiceDescriptor: Sendable, Hashable { + /// The name of the package the service belongs to. For example, "helloworld". + /// An empty string means that the service does not belong to any package. + public var package: String + + /// The name of the service. For example, "Greeter". + public var service: String + + /// The fully qualified service name in the format: + /// - "package.service": if a package name is specified. For example, "helloworld.Greeter". + /// - "service": if a package name is not specified. For example, "Greeter". + public var fullyQualifiedService: String { + if self.package.isEmpty { + return self.service + } + + return "\(self.package).\(self.service)" + } + + /// - Parameters: + /// - package: The name of the package the service belongs to. For example, "helloworld". + /// An empty string means that the service does not belong to any package. + /// - service: The name of the service. For example, "Greeter". + public init(package: String, service: String) { + self.package = package + self.service = service + } +} diff --git a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift new file mode 100644 index 000000000..0adfe524a --- /dev/null +++ b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift @@ -0,0 +1,26 @@ +/* + * 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 ServiceDescriptorTests: XCTestCase { + func testFullyQualifiedName() { + let descriptor = ServiceDescriptor(package: "foo.bar", service: "Baz") + XCTAssertEqual(descriptor.package, "foo.bar") + XCTAssertEqual(descriptor.service, "Baz") + XCTAssertEqual(descriptor.fullyQualifiedService, "foo.bar.Baz") + } +} From c671f832c6da54fafdc92f3a9d032a11ceddb960 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 17 Jul 2024 11:23:15 +0100 Subject: [PATCH 403/580] Better closing control over GOAWAYs (#1984) Motivation: The v2 client treats all GOAWAY frames equally and, if not already closing, will stop creating streams on that connection. This is okay in the 'normal' case where the error code is NO_ERROR. However if there's a protocol violation or some other connection level error the connection should be closed. Modifications: - Have the closing state record whether it's closing gracefully allowing for closes to be 'upgraded' (from graceful to not-so-graceful). - Check whether the error code in the GOAWAY frame is NO_ERROR and take the non-graceful path if the code isn't NO_ERROR. - Add missing 'fireChannelInactive' - Add tests Result: Client will close shut down on a protocol error --- .../Connection/ClientConnectionHandler.swift | 55 +++++++++++++------ ...ntConnectionHandlerStateMachineTests.swift | 8 +++ .../ClientConnectionHandlerTests.swift | 20 +++++++ 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index e58b071da..5138dee21 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -144,6 +144,7 @@ package final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutbo self.keepaliveTimer?.cancel() self.keepaliveTimeoutTimer.cancel() + context.fireChannelInactive() } package func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { @@ -175,22 +176,32 @@ package final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutbo switch frame.payload { case .goAway(_, let errorCode, let data): - // Receiving a GOAWAY frame means we need to stop creating streams immediately and start - // closing the connection. - switch self.state.beginGracefulShutdown(promise: nil) { - case .sendGoAway(let close): - // gRPC servers may indicate why the GOAWAY was sent in the opaque data. - let message = data.map { String(buffer: $0) } ?? "" - context.fireChannelRead(self.wrapInboundOut(.closing(.goAway(errorCode, message)))) - - // Clients should send GOAWAYs when closing a connection. - self.writeAndFlushGoAway(context: context, errorCode: .noError) - if close { + if errorCode == .noError { + // Receiving a GOAWAY frame means we need to stop creating streams immediately and start + // closing the connection. + switch self.state.beginGracefulShutdown(promise: nil) { + case .sendGoAway(let close): + // gRPC servers may indicate why the GOAWAY was sent in the opaque data. + let message = data.map { String(buffer: $0) } ?? "" + context.fireChannelRead(self.wrapInboundOut(.closing(.goAway(errorCode, message)))) + + // Clients should send GOAWAYs when closing a connection. + self.writeAndFlushGoAway(context: context, errorCode: .noError) + if close { + context.close(promise: nil) + } + + case .none: + () + } + } else { + // Some error, begin closing. + if self.state.beginClosing() { + // gRPC servers may indicate why the GOAWAY was sent in the opaque data. + let message = data.map { String(buffer: $0) } ?? "" + context.fireChannelRead(self.wrapInboundOut(.closing(.goAway(errorCode, message)))) context.close(promise: nil) } - - case .none: - () } case .ping(let data, let ack): @@ -459,9 +470,11 @@ extension ClientConnectionHandler { var allowKeepaliveWithoutCalls: Bool var openStreams: Set var closePromise: Optional> + var isGraceful: Bool - init(from state: Active, closePromise: EventLoopPromise?) { + init(from state: Active, isGraceful: Bool, closePromise: EventLoopPromise?) { self.openStreams = state.openStreams + self.isGraceful = isGraceful self.allowKeepaliveWithoutCalls = state.allowKeepaliveWithoutCalls self.closePromise = closePromise } @@ -605,7 +618,7 @@ extension ClientConnectionHandler { // ratchet down the last stream ID as only the client creates streams in gRPC. let close = state.openStreams.isEmpty onGracefulShutdown = .sendGoAway(close) - self.state = .closing(State.Closing(from: state, closePromise: promise)) + self.state = .closing(State.Closing(from: state, isGraceful: true, closePromise: promise)) case .closing(var state): self.state = ._modifying @@ -627,9 +640,15 @@ extension ClientConnectionHandler { mutating func beginClosing() -> Bool { switch self.state { case .active(let active): - self.state = .closing(State.Closing(from: active, closePromise: nil)) + self.state = .closing(State.Closing(from: active, isGraceful: false, closePromise: nil)) return true - case .closing, .closed: + case .closing(var state): + self.state = ._modifying + let forceShutdown = state.isGraceful + state.isGraceful = false + self.state = .closing(state) + return forceShutdown + case .closed: return false case ._modifying: preconditionFailure() diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift index c4bd2bfe0..fdf14aa2d 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift @@ -44,6 +44,13 @@ final class ClientConnectionHandlerStateMachineTests: XCTestCase { XCTAssertEqual(state.streamClosed(1), .close) } + func testCloseWhenAlreadyClosingGracefully() { + var state = self.makeStateMachine() + state.streamOpened(1) + XCTAssertEqual(state.beginGracefulShutdown(promise: nil), .sendGoAway(false)) + XCTAssertTrue(state.beginClosing()) + } + func testOpenAndCloseStreamWhenClosed() { var state = self.makeStateMachine() _ = state.closed() @@ -104,4 +111,5 @@ final class ClientConnectionHandlerStateMachineTests: XCTestCase { // Close immediately, not streams are open. XCTAssertEqual(state.beginGracefulShutdown(promise: nil), .sendGoAway(true)) } + } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift index 54d534a8c..87ac5538c 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift @@ -212,6 +212,26 @@ final class ClientConnectionHandlerTests: XCTestCase { try connection.waitUntilClosed() } + func testGoAwayWithNoErrorThenGoAwayWithProtocolError() throws { + let connection = try Connection() + try connection.activate() + + connection.streamOpened(1) + connection.streamOpened(2) + connection.streamOpened(3) + + try connection.goAway(lastStreamID: .maxID, errorCode: .noError) + // Should read out an event. + XCTAssertEqual(try connection.readEvent(), .closing(.goAway(.noError, ""))) + + // Upgrade the close from graceful to 'error'. + try connection.goAway(lastStreamID: .maxID, errorCode: .protocolError) + // Should read out an event and the connection will be closed without waiting for notification + // from existing streams. + XCTAssertEqual(try connection.readEvent(), .closing(.goAway(.protocolError, ""))) + try connection.waitUntilClosed() + } + func testOutboundGracefulClose() throws { let connection = try Connection() try connection.activate() From aa5f4a89675aef89d8e01772347cd5d4bcc25d74 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 22 Jul 2024 14:12:36 +0100 Subject: [PATCH 404/580] Enable Swift 6 language mode for more modules (#1985) Motivation: v2 will support Swift 6 only. We should enable the Swift 6 language mode across all v2 modules. This has mostly been done already for non-test modules. This change enables it for test modules as well. Modifications: Enable Swift 6 language mode and existential any for test modules Result: Closer to fully supporting Swift 6 language mode --- Package@swift-6.swift | 14 +- .../Internal/Renderer/TextBasedRenderer.swift | 3 + Sources/GRPCCore/Metadata.swift | 15 +- .../GRPCCore/Streaming/RPCAsyncSequence.swift | 7 +- Sources/GRPCCore/Streaming/RPCWriter.swift | 2 +- Sources/GRPCHTTP2Core/Internal/Timer.swift | 11 +- .../InteroperabilityTestCase.swift | 2 +- .../Renderer/TextBasedRendererTests.swift | 170 +++++++++--------- .../ServerRPCExecutorTestHarness.swift | 26 ++- .../Internal/BufferedStreamTests.swift | 14 +- .../Test Utilities/Coding+JSON.swift | 5 +- .../Test Utilities/RPCWriter+Utilities.swift | 2 +- .../Transport/AnyTransport.swift | 4 +- .../Transport/StreamCountingTransport.swift | 2 +- .../Test Utilities/XCTest+Utilities.swift | 2 +- .../Connection/Connection+Equatable.swift | 14 +- .../Client/Connection/ConnectionTests.swift | 4 +- .../LoadBalancers/LoadBalancerTest.swift | 2 +- .../Connection/Utilities/ConnectionTest.swift | 24 +-- .../Connection/Utilities/NameResolvers.swift | 8 +- .../Connection/Utilities/TestServer.swift | 6 +- .../GRPCStreamStateMachineTests.swift | 5 - .../Internal/TimerTests.swift | 34 ++-- .../HTTP2TransportTests.swift | 2 +- .../HTTP2StatusCodeServer.swift | 2 +- 25 files changed, 191 insertions(+), 189 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 54725c91c..31c28b55b 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -366,7 +366,7 @@ extension Target { .atomics, .protobuf, ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -377,7 +377,7 @@ extension Target { .grpcCore, .grpcInProcessTransport ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -390,7 +390,7 @@ extension Target { .nioCore, .grpcInterceptors ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -404,7 +404,7 @@ extension Target { .nioEmbedded, .nioTestUtils, ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -417,7 +417,7 @@ extension Target { .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -427,7 +427,7 @@ extension Target { dependencies: [ .grpcCodeGen ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -464,7 +464,7 @@ extension Target { .interoperabilityTests, .grpcCore ], - swiftSettings: [.swiftLanguageVersion(.v5), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] ) } diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index eb3abe149..35c7173e0 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -104,6 +104,9 @@ final class StringCodeWriter { func nextLineAppendsToLastLine() { nextWriteAppendsToLastLine = true } } +@available(*, unavailable) +extension TextBasedRenderer: Sendable {} + /// A renderer that uses string interpolation and concatenation /// to convert the provided structure code into raw string form. struct TextBasedRenderer: RendererProtocol { diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 5e701a2ad..8326eb336 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -289,12 +289,11 @@ extension Metadata: RandomAccessCollection { } extension Metadata { - /// A sequence of metadata values for a given key. - public struct Values: Sequence { + public struct Values: Sequence, Sendable { /// An iterator for all metadata ``Value``s associated with a given key. - public struct Iterator: IteratorProtocol { + public struct Iterator: IteratorProtocol, Sendable { private var metadataIterator: Metadata.Iterator private let key: String @@ -339,12 +338,11 @@ extension Metadata { extension Metadata { /// A sequence of metadata string values for a given key. - public struct StringValues: Sequence { - + public struct StringValues: Sequence, Sendable { /// An iterator for all string values associated with a given key. /// /// This iterator will only return values originally stored as strings for a given key. - public struct Iterator: IteratorProtocol { + public struct Iterator: IteratorProtocol, Sendable { private var values: Values.Iterator init(values: Values) { @@ -388,15 +386,14 @@ extension Metadata { } extension Metadata { - /// A sequence of metadata binary values for a given key. - public struct BinaryValues: Sequence { + public struct BinaryValues: Sequence, Sendable { /// An iterator for all binary data values associated with a given key. /// /// This iterator will return values originally stored as binary data for a given key, and will also try to /// decode values stored as strings as if they were base64-encoded strings. - public struct Iterator: IteratorProtocol { + public struct Iterator: IteratorProtocol, Sendable { private var values: Values.Iterator init(values: Values) { diff --git a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift index 791bb8ffc..d3603fe00 100644 --- a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift @@ -16,9 +16,12 @@ /// A type-erasing `AsyncSequence`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct RPCAsyncSequence: AsyncSequence, @unchecked Sendable { +public struct RPCAsyncSequence< + Element: Sendable, + Failure: Error +>: AsyncSequence, @unchecked Sendable { // @unchecked Sendable is required because 'any' doesn't support composition with primary - // associated types. (see: https://github.com/swiftlang/swift/issue/63877) + // associated types. (see: https://github.com/swiftlang/swift/issues/63877) // // To work around that limitation the 'init' requires that the async sequence being wrapped // is 'Sendable' but that constraint must be dropped internally. This is safe, the compiler just diff --git a/Sources/GRPCCore/Streaming/RPCWriter.swift b/Sources/GRPCCore/Streaming/RPCWriter.swift index d6c3825d7..d0b7b940b 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter.swift @@ -16,7 +16,7 @@ /// A type-erasing ``RPCWriterProtocol``. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct RPCWriter: Sendable, RPCWriterProtocol { +public struct RPCWriter: Sendable, RPCWriterProtocol { private let writer: any RPCWriterProtocol /// Creates an ``RPCWriter`` by wrapping the `other` writer. diff --git a/Sources/GRPCHTTP2Core/Internal/Timer.swift b/Sources/GRPCHTTP2Core/Internal/Timer.swift index 89795380b..7c53333d1 100644 --- a/Sources/GRPCHTTP2Core/Internal/Timer.swift +++ b/Sources/GRPCHTTP2Core/Internal/Timer.swift @@ -16,7 +16,7 @@ import NIOCore -struct Timer { +package struct Timer { /// The delay to wait before running the task. private let delay: TimeAmount /// The task to run, if scheduled. @@ -38,14 +38,17 @@ struct Timer { } } - init(delay: TimeAmount, repeat: Bool = false) { + package init(delay: TimeAmount, repeat: Bool = false) { self.delay = delay self.task = nil self.repeat = `repeat` } /// Schedule a task on the given `EventLoop`. - mutating func schedule(on eventLoop: any EventLoop, work: @escaping @Sendable () throws -> Void) { + package mutating func schedule( + on eventLoop: any EventLoop, + work: @escaping @Sendable () throws -> Void + ) { self.task?.cancel() if self.repeat { @@ -60,7 +63,7 @@ struct Timer { } /// Cancels the task, if one was scheduled. - mutating func cancel() { + package mutating func cancel() { self.task?.cancel() self.task = nil } diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index d41a700b7..74fdcd35b 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -48,7 +48,7 @@ public protocol InteroperabilityTest { /// Note: Tests for compression have not been implemented yet as compression is /// not supported. Once the API which allows for compression will be implemented /// these tests should be added. -public enum InteroperabilityTestCase: String, CaseIterable { +public enum InteroperabilityTestCase: String, CaseIterable, Sendable { case emptyUnary = "empty_unary" case largeUnary = "large_unary" case clientCompressedUnary = "client_compressed_unary" diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 422673e8b..5e7227bbf 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -41,7 +41,7 @@ final class Test_TextBasedRenderer: XCTestCase { Also, bar """# ), - renderedBy: TextBasedRenderer.renderComment, + renderedBy: { $0.renderComment(_:) }, rendersAs: #""" // Generated by foo // @@ -56,7 +56,7 @@ final class Test_TextBasedRenderer: XCTestCase { Also, bar """# ), - renderedBy: TextBasedRenderer.renderComment, + renderedBy: { $0.renderComment(_:) }, rendersAs: #""" /// Generated by foo /// @@ -65,14 +65,14 @@ final class Test_TextBasedRenderer: XCTestCase { ) try _test( .mark("Lorem ipsum", sectionBreak: false), - renderedBy: TextBasedRenderer.renderComment, + renderedBy: { $0.renderComment(_:) }, rendersAs: #""" // MARK: Lorem ipsum """# ) try _test( .mark("Lorem ipsum", sectionBreak: true), - renderedBy: TextBasedRenderer.renderComment, + renderedBy: { $0.renderComment(_:) }, rendersAs: #""" // MARK: - Lorem ipsum """# @@ -83,7 +83,7 @@ final class Test_TextBasedRenderer: XCTestCase { Generated by foo\r\nAlso, bar """ ), - renderedBy: TextBasedRenderer.renderComment, + renderedBy: { $0.renderComment(_:) }, rendersAs: #""" // Generated by foo // Also, bar @@ -91,14 +91,14 @@ final class Test_TextBasedRenderer: XCTestCase { ) try _test( .preFormatted("/// Lorem ipsum\n"), - renderedBy: TextBasedRenderer.renderComment, + renderedBy: { $0.renderComment(_:) }, rendersAs: """ /// Lorem ipsum """ ) try _test( .preFormatted("/// Lorem ipsum\n\n/// Lorem ipsum\n"), - renderedBy: TextBasedRenderer.renderComment, + renderedBy: { $0.renderComment(_:) }, rendersAs: """ /// Lorem ipsum @@ -108,10 +108,10 @@ final class Test_TextBasedRenderer: XCTestCase { } func testImports() throws { - try _test(nil, renderedBy: TextBasedRenderer.renderImports, rendersAs: "") + try _test(nil, renderedBy: { $0.renderImports(_:) }, rendersAs: "") try _test( [ImportDescription(moduleName: "Foo"), ImportDescription(moduleName: "Bar")], - renderedBy: TextBasedRenderer.renderImports, + renderedBy: { $0.renderImports(_:) }, rendersAs: #""" import Foo import Bar @@ -119,14 +119,14 @@ final class Test_TextBasedRenderer: XCTestCase { ) try _test( [ImportDescription(moduleName: "Foo", spi: "Secret")], - renderedBy: TextBasedRenderer.renderImports, + renderedBy: { $0.renderImports(_:) }, rendersAs: #""" @_spi(Secret) import Foo """# ) try _test( [ImportDescription(moduleName: "Foo", preconcurrency: .onOS(["Bar", "Baz"]))], - renderedBy: TextBasedRenderer.renderImports, + renderedBy: { $0.renderImports(_:) }, rendersAs: #""" #if os(Bar) || os(Baz) @preconcurrency import Foo @@ -140,7 +140,7 @@ final class Test_TextBasedRenderer: XCTestCase { ImportDescription(moduleName: "Foo", preconcurrency: .always), ImportDescription(moduleName: "Bar", spi: "Secret", preconcurrency: .always), ], - renderedBy: TextBasedRenderer.renderImports, + renderedBy: { $0.renderImports(_:) }, rendersAs: #""" @preconcurrency import Foo @preconcurrency @_spi(Secret) import Bar @@ -186,7 +186,7 @@ final class Test_TextBasedRenderer: XCTestCase { item: ImportDescription.Item(kind: .func, name: "PreconcurrencyBar") ), ], - renderedBy: TextBasedRenderer.renderImports, + renderedBy: { $0.renderImports(_:) }, rendersAs: #""" import typealias Foo.Bar import struct Foo.Baz @@ -205,28 +205,28 @@ final class Test_TextBasedRenderer: XCTestCase { func testAccessModifiers() throws { try _test( .public, - renderedBy: TextBasedRenderer.renderedAccessModifier, + renderedBy: { $0.renderedAccessModifier(_:) }, rendersAs: #""" public """# ) try _test( .internal, - renderedBy: TextBasedRenderer.renderedAccessModifier, + renderedBy: { $0.renderedAccessModifier(_:) }, rendersAs: #""" internal """# ) try _test( .fileprivate, - renderedBy: TextBasedRenderer.renderedAccessModifier, + renderedBy: { $0.renderedAccessModifier(_:) }, rendersAs: #""" fileprivate """# ) try _test( .private, - renderedBy: TextBasedRenderer.renderedAccessModifier, + renderedBy: { $0.renderedAccessModifier(_:) }, rendersAs: #""" private """# @@ -236,35 +236,35 @@ final class Test_TextBasedRenderer: XCTestCase { func testLiterals() throws { try _test( .string("hi"), - renderedBy: TextBasedRenderer.renderLiteral, + renderedBy: { $0.renderLiteral(_:) }, rendersAs: #""" "hi" """# ) try _test( .string("this string: \"foo\""), - renderedBy: TextBasedRenderer.renderLiteral, + renderedBy: { $0.renderLiteral(_:) }, rendersAs: #""" #"this string: "foo""# """# ) try _test( .nil, - renderedBy: TextBasedRenderer.renderLiteral, + renderedBy: { $0.renderLiteral(_:) }, rendersAs: #""" nil """# ) try _test( .array([]), - renderedBy: TextBasedRenderer.renderLiteral, + renderedBy: { $0.renderLiteral(_:) }, rendersAs: #""" [] """# ) try _test( .array([.literal(.nil)]), - renderedBy: TextBasedRenderer.renderLiteral, + renderedBy: { $0.renderLiteral(_:) }, rendersAs: #""" [ nil @@ -273,7 +273,7 @@ final class Test_TextBasedRenderer: XCTestCase { ) try _test( .array([.literal(.nil), .literal(.nil)]), - renderedBy: TextBasedRenderer.renderLiteral, + renderedBy: { $0.renderLiteral(_:) }, rendersAs: #""" [ nil, @@ -286,21 +286,21 @@ final class Test_TextBasedRenderer: XCTestCase { func testExpression() throws { try _test( .literal(.nil), - renderedBy: TextBasedRenderer.renderExpression, + renderedBy: { $0.renderExpression(_:) }, rendersAs: #""" nil """# ) try _test( .identifierPattern("foo"), - renderedBy: TextBasedRenderer.renderExpression, + renderedBy: { $0.renderExpression(_:) }, rendersAs: #""" foo """# ) try _test( .memberAccess(.init(left: .identifierPattern("foo"), right: "bar")), - renderedBy: TextBasedRenderer.renderExpression, + renderedBy: { $0.renderExpression(_:) }, rendersAs: #""" foo.bar """# @@ -312,7 +312,7 @@ final class Test_TextBasedRenderer: XCTestCase { arguments: [.init(label: nil, expression: .identifierPattern("foo"))] ) ), - renderedBy: TextBasedRenderer.renderExpression, + renderedBy: { $0.renderExpression(_:) }, rendersAs: #""" callee(foo) """# @@ -322,14 +322,14 @@ final class Test_TextBasedRenderer: XCTestCase { func testDeclaration() throws { try _test( .variable(kind: .let, left: "foo"), - renderedBy: TextBasedRenderer.renderDeclaration, + renderedBy: { $0.renderDeclaration(_:) }, rendersAs: #""" let foo """# ) try _test( .extension(.init(onType: "String", declarations: [])), - renderedBy: TextBasedRenderer.renderDeclaration, + renderedBy: { $0.renderDeclaration(_:) }, rendersAs: #""" extension String { } @@ -337,35 +337,35 @@ final class Test_TextBasedRenderer: XCTestCase { ) try _test( .struct(.init(name: "Foo")), - renderedBy: TextBasedRenderer.renderDeclaration, + renderedBy: { $0.renderDeclaration(_:) }, rendersAs: #""" struct Foo {} """# ) try _test( .protocol(.init(name: "Foo")), - renderedBy: TextBasedRenderer.renderDeclaration, + renderedBy: { $0.renderDeclaration(_:) }, rendersAs: #""" protocol Foo {} """# ) try _test( .enum(.init(name: "Foo")), - renderedBy: TextBasedRenderer.renderDeclaration, + renderedBy: { $0.renderDeclaration(_:) }, rendersAs: #""" enum Foo {} """# ) try _test( .typealias(.init(name: "foo", existingType: .member(["Foo", "Bar"]))), - renderedBy: TextBasedRenderer.renderDeclaration, + renderedBy: { $0.renderDeclaration(_:) }, rendersAs: #""" typealias foo = Foo.Bar """# ) try _test( .function(FunctionDescription.init(kind: .function(name: "foo"), body: [])), - renderedBy: TextBasedRenderer.renderDeclaration, + renderedBy: { $0.renderDeclaration(_:) }, rendersAs: #""" func foo() {} """# @@ -375,21 +375,21 @@ final class Test_TextBasedRenderer: XCTestCase { func testFunctionKind() throws { try _test( .initializer, - renderedBy: TextBasedRenderer.renderedFunctionKind, + renderedBy: { $0.renderedFunctionKind(_:) }, rendersAs: #""" init """# ) try _test( .function(name: "funky"), - renderedBy: TextBasedRenderer.renderedFunctionKind, + renderedBy: { $0.renderedFunctionKind(_:) }, rendersAs: #""" func funky """# ) try _test( .function(name: "funky", isStatic: true), - renderedBy: TextBasedRenderer.renderedFunctionKind, + renderedBy: { $0.renderedFunctionKind(_:) }, rendersAs: #""" static func funky """# @@ -399,14 +399,14 @@ final class Test_TextBasedRenderer: XCTestCase { func testFunctionKeyword() throws { try _test( .throws, - renderedBy: TextBasedRenderer.renderedFunctionKeyword, + renderedBy: { $0.renderedFunctionKeyword(_:) }, rendersAs: #""" throws """# ) try _test( .async, - renderedBy: TextBasedRenderer.renderedFunctionKeyword, + renderedBy: { $0.renderedFunctionKeyword(_:) }, rendersAs: #""" async """# @@ -416,35 +416,35 @@ final class Test_TextBasedRenderer: XCTestCase { func testParameter() throws { try _test( .init(label: "l", name: "n", type: .member("T"), defaultValue: .literal(.nil)), - renderedBy: TextBasedRenderer.renderParameter, + renderedBy: { $0.renderParameter(_:) }, rendersAs: #""" l n: T = nil """# ) try _test( .init(label: nil, name: "n", type: .member("T"), defaultValue: .literal(.nil)), - renderedBy: TextBasedRenderer.renderParameter, + renderedBy: { $0.renderParameter(_:) }, rendersAs: #""" _ n: T = nil """# ) try _test( .init(label: "l", name: nil, type: .member("T"), defaultValue: .literal(.nil)), - renderedBy: TextBasedRenderer.renderParameter, + renderedBy: { $0.renderParameter(_:) }, rendersAs: #""" l: T = nil """# ) try _test( .init(label: nil, name: nil, type: .member("T"), defaultValue: .literal(.nil)), - renderedBy: TextBasedRenderer.renderParameter, + renderedBy: { $0.renderParameter(_:) }, rendersAs: #""" _: T = nil """# ) try _test( .init(label: nil, name: nil, type: .member("T"), defaultValue: nil), - renderedBy: TextBasedRenderer.renderParameter, + renderedBy: { $0.renderParameter(_:) }, rendersAs: #""" _: T """# @@ -461,7 +461,7 @@ final class Test_TextBasedRenderer: XCTestCase { whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]), body: [] ), - renderedBy: TextBasedRenderer.renderFunction, + renderedBy: { $0.renderFunction(_:) }, rendersAs: #""" public func f() where R: Sendable {} """# @@ -477,7 +477,7 @@ final class Test_TextBasedRenderer: XCTestCase { ]), body: [] ), - renderedBy: TextBasedRenderer.renderFunction, + renderedBy: { $0.renderFunction(_:) }, rendersAs: #""" public func f() where R: Sendable, T: Encodable {} """# @@ -487,7 +487,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testFunction() throws { try _test( .init(accessModifier: .public, kind: .function(name: "f"), parameters: [], body: []), - renderedBy: TextBasedRenderer.renderFunction, + renderedBy: { $0.renderFunction(_:) }, rendersAs: #""" public func f() {} """# @@ -499,7 +499,7 @@ final class Test_TextBasedRenderer: XCTestCase { parameters: [.init(label: "a", name: "b", type: .member("C"), defaultValue: nil)], body: [] ), - renderedBy: TextBasedRenderer.renderFunction, + renderedBy: { $0.renderFunction(_:) }, rendersAs: #""" public func f(a b: C) {} """# @@ -514,7 +514,7 @@ final class Test_TextBasedRenderer: XCTestCase { ], body: [] ), - renderedBy: TextBasedRenderer.renderFunction, + renderedBy: { $0.renderFunction(_:) }, rendersAs: #""" public func f( a b: C, @@ -529,7 +529,7 @@ final class Test_TextBasedRenderer: XCTestCase { keywords: [.async, .throws], returnType: .identifierType(TypeName.string) ), - renderedBy: TextBasedRenderer.renderFunction, + renderedBy: { $0.renderFunction(_:) }, rendersAs: #""" func f() async throws -> Swift.String """# @@ -539,7 +539,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testIdentifiers() throws { try _test( .pattern("foo"), - renderedBy: TextBasedRenderer.renderIdentifier, + renderedBy: { $0.renderIdentifier(_:) }, rendersAs: #""" foo """# @@ -549,14 +549,14 @@ final class Test_TextBasedRenderer: XCTestCase { func testMemberAccess() throws { try _test( .init(left: .identifierPattern("foo"), right: "bar"), - renderedBy: TextBasedRenderer.renderMemberAccess, + renderedBy: { $0.renderMemberAccess(_:) }, rendersAs: #""" foo.bar """# ) try _test( .init(left: nil, right: "bar"), - renderedBy: TextBasedRenderer.renderMemberAccess, + renderedBy: { $0.renderMemberAccess(_:) }, rendersAs: #""" .bar """# @@ -566,14 +566,14 @@ final class Test_TextBasedRenderer: XCTestCase { func testFunctionCallArgument() throws { try _test( .init(label: "foo", expression: .identifierPattern("bar")), - renderedBy: TextBasedRenderer.renderFunctionCallArgument, + renderedBy: { $0.renderFunctionCallArgument(_:) }, rendersAs: #""" foo: bar """# ) try _test( .init(label: nil, expression: .identifierPattern("bar")), - renderedBy: TextBasedRenderer.renderFunctionCallArgument, + renderedBy: { $0.renderFunctionCallArgument(_:) }, rendersAs: #""" bar """# @@ -583,7 +583,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testFunctionCall() throws { try _test( .functionCall(.init(calledExpression: .identifierPattern("callee"))), - renderedBy: TextBasedRenderer.renderExpression, + renderedBy: { $0.renderExpression(_:) }, rendersAs: #""" callee() """# @@ -595,7 +595,7 @@ final class Test_TextBasedRenderer: XCTestCase { arguments: [.init(label: "foo", expression: .identifierPattern("bar"))] ) ), - renderedBy: TextBasedRenderer.renderExpression, + renderedBy: { $0.renderExpression(_:) }, rendersAs: #""" callee(foo: bar) """# @@ -610,7 +610,7 @@ final class Test_TextBasedRenderer: XCTestCase { ] ) ), - renderedBy: TextBasedRenderer.renderExpression, + renderedBy: { $0.renderExpression(_:) }, rendersAs: #""" callee( foo: bar, @@ -627,7 +627,7 @@ final class Test_TextBasedRenderer: XCTestCase { onType: "Info", declarations: [.variable(kind: .let, left: "foo", type: .member("Int"))] ), - renderedBy: TextBasedRenderer.renderExtension, + renderedBy: { $0.renderExtension(_:) }, rendersAs: #""" public extension Info { let foo: Int @@ -639,28 +639,28 @@ final class Test_TextBasedRenderer: XCTestCase { func testDeprecation() throws { try _test( .init(), - renderedBy: TextBasedRenderer.renderDeprecation, + renderedBy: { $0.renderDeprecation(_:) }, rendersAs: #""" @available(*, deprecated) """# ) try _test( .init(message: "some message"), - renderedBy: TextBasedRenderer.renderDeprecation, + renderedBy: { $0.renderDeprecation(_:) }, rendersAs: #""" @available(*, deprecated, message: "some message") """# ) try _test( .init(renamed: "newSymbol(param:)"), - renderedBy: TextBasedRenderer.renderDeprecation, + renderedBy: { $0.renderDeprecation(_:) }, rendersAs: #""" @available(*, deprecated, renamed: "newSymbol(param:)") """# ) try _test( .init(message: "some message", renamed: "newSymbol(param:)"), - renderedBy: TextBasedRenderer.renderDeprecation, + renderedBy: { $0.renderDeprecation(_:) }, rendersAs: #""" @available(*, deprecated, message: "some message", renamed: "newSymbol(param:)") """# @@ -675,7 +675,7 @@ final class Test_TextBasedRenderer: XCTestCase { .init(os: .watchOS, version: "8.1.2"), .init(os: .tvOS, version: "15.0.2"), ]), - renderedBy: TextBasedRenderer.renderAvailability, + renderedBy: { $0.renderAvailability(_:) }, rendersAs: #""" @available(macOS 12.0, iOS 13.1.2, watchOS 8.1.2, tvOS 15.0.2, *) """# @@ -685,14 +685,14 @@ final class Test_TextBasedRenderer: XCTestCase { func testBindingKind() throws { try _test( .var, - renderedBy: TextBasedRenderer.renderedBindingKind, + renderedBy: { $0.renderedBindingKind(_:) }, rendersAs: #""" var """# ) try _test( .let, - renderedBy: TextBasedRenderer.renderedBindingKind, + renderedBy: { $0.renderedBindingKind(_:) }, rendersAs: #""" let """# @@ -709,7 +709,7 @@ final class Test_TextBasedRenderer: XCTestCase { type: .init(TypeName.string), right: .literal(.string("bar")) ), - renderedBy: TextBasedRenderer.renderVariable, + renderedBy: { $0.renderVariable(_:) }, rendersAs: #""" public static let foo: Swift.String = "bar" """# @@ -723,7 +723,7 @@ final class Test_TextBasedRenderer: XCTestCase { type: nil, right: nil ), - renderedBy: TextBasedRenderer.renderVariable, + renderedBy: { $0.renderVariable(_:) }, rendersAs: #""" internal var foo """# @@ -735,7 +735,7 @@ final class Test_TextBasedRenderer: XCTestCase { type: .init(TypeName.int), getter: [CodeBlock.expression(.literal(.int(42)))] ), - renderedBy: TextBasedRenderer.renderVariable, + renderedBy: { $0.renderVariable(_:) }, rendersAs: #""" var foo: Swift.Int { 42 @@ -750,7 +750,7 @@ final class Test_TextBasedRenderer: XCTestCase { getter: [CodeBlock.expression(.literal(.int(42)))], getterEffects: [.throws] ), - renderedBy: TextBasedRenderer.renderVariable, + renderedBy: { $0.renderVariable(_:) }, rendersAs: #""" var foo: Swift.Int { get throws { @@ -764,7 +764,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testStruct() throws { try _test( .init(name: "Structy"), - renderedBy: TextBasedRenderer.renderStruct, + renderedBy: { $0.renderStruct(_:) }, rendersAs: #""" struct Structy {} """# @@ -774,7 +774,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testProtocol() throws { try _test( .init(name: "Protocoly"), - renderedBy: TextBasedRenderer.renderProtocol, + renderedBy: { $0.renderProtocol(_:) }, rendersAs: #""" protocol Protocoly {} """# @@ -784,7 +784,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testEnum() throws { try _test( .init(name: "Enumy"), - renderedBy: TextBasedRenderer.renderEnum, + renderedBy: { $0.renderEnum(_:) }, rendersAs: #""" enum Enumy {} """# @@ -794,14 +794,14 @@ final class Test_TextBasedRenderer: XCTestCase { func testCodeBlockItem() throws { try _test( .declaration(.variable(kind: .let, left: "foo")), - renderedBy: TextBasedRenderer.renderCodeBlockItem, + renderedBy: { $0.renderCodeBlockItem(_:) }, rendersAs: #""" let foo """# ) try _test( .expression(.literal(.nil)), - renderedBy: TextBasedRenderer.renderCodeBlockItem, + renderedBy: { $0.renderCodeBlockItem(_:) }, rendersAs: #""" nil """# @@ -814,7 +814,7 @@ final class Test_TextBasedRenderer: XCTestCase { comment: .inline("- MARK: Section"), item: .declaration(.variable(kind: .let, left: "foo")) ), - renderedBy: TextBasedRenderer.renderCodeBlock, + renderedBy: { $0.renderCodeBlock(_:) }, rendersAs: #""" // - MARK: Section let foo @@ -822,7 +822,7 @@ final class Test_TextBasedRenderer: XCTestCase { ) try _test( .init(comment: nil, item: .declaration(.variable(kind: .let, left: "foo"))), - renderedBy: TextBasedRenderer.renderCodeBlock, + renderedBy: { $0.renderCodeBlock(_:) }, rendersAs: #""" let foo """# @@ -832,14 +832,14 @@ final class Test_TextBasedRenderer: XCTestCase { func testTypealias() throws { try _test( .init(name: "inty", existingType: .member("Int")), - renderedBy: TextBasedRenderer.renderTypealias, + renderedBy: { $0.renderTypealias(_:) }, rendersAs: #""" typealias inty = Int """# ) try _test( .init(accessModifier: .private, name: "inty", existingType: .member("Int")), - renderedBy: TextBasedRenderer.renderTypealias, + renderedBy: { $0.renderTypealias(_:) }, rendersAs: #""" private typealias inty = Int """# @@ -853,7 +853,7 @@ final class Test_TextBasedRenderer: XCTestCase { imports: [.init(moduleName: "Foo")], codeBlocks: [.init(comment: nil, item: .declaration(.struct(.init(name: "Bar"))))] ), - renderedBy: TextBasedRenderer.renderFile, + renderedBy: { $0.renderFile(_:) }, rendersAs: #""" // hi @@ -876,7 +876,7 @@ final class Test_TextBasedRenderer: XCTestCase { ) ] ), - renderedBy: TextBasedRenderer.renderFile, + renderedBy: { $0.renderFile(_:) }, rendersAs: #""" // hi @@ -891,7 +891,7 @@ final class Test_TextBasedRenderer: XCTestCase { try _test( .array([.literal(.nil), .literal(.nil)]), - renderedBy: TextBasedRenderer.renderLiteral, + renderedBy: { $0.renderLiteral(_:) }, rendersAs: #""" [ nil, @@ -909,7 +909,7 @@ final class Test_TextBasedRenderer: XCTestCase { getter: [CodeBlock.expression(.literal(.int(42)))], getterEffects: [.throws] ), - renderedBy: TextBasedRenderer.renderVariable, + renderedBy: { $0.renderVariable(_:) }, rendersAs: #""" var foo: Swift.Int { get throws { @@ -928,7 +928,7 @@ extension Test_TextBasedRenderer { _ input: Input, renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> String), rendersAs output: String, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, indentation: Int = 4 ) throws { @@ -940,7 +940,7 @@ extension Test_TextBasedRenderer { _ input: Input, renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> Void), rendersAs output: String, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line, indentation: Int = 4 ) throws { diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index 154a87bca..d924808b6 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -19,7 +19,7 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ServerRPCExecutorTestHarness { - struct ServerHandler: Sendable { + struct ServerHandler: Sendable { let fn: @Sendable (ServerRequest.Stream) async throws -> ServerResponse.Stream init( @@ -53,8 +53,12 @@ struct ServerRPCExecutorTestHarness { handler: @escaping @Sendable ( ServerRequest.Stream ) async throws -> ServerResponse.Stream, - producer: @escaping (RPCWriter.Closable) async throws -> Void, - consumer: @escaping (RPCAsyncSequence) async throws -> Void + producer: @escaping @Sendable ( + RPCWriter.Closable + ) async throws -> Void, + consumer: @escaping @Sendable ( + RPCAsyncSequence + ) async throws -> Void ) async throws { try await self.execute( deserializer: deserializer, @@ -69,8 +73,12 @@ struct ServerRPCExecutorTestHarness { deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: ServerHandler, - producer: @escaping (RPCWriter.Closable) async throws -> Void, - consumer: @escaping (RPCAsyncSequence) async throws -> Void + producer: @escaping @Sendable ( + RPCWriter.Closable + ) async throws -> Void, + consumer: @escaping @Sendable ( + RPCAsyncSequence + ) async throws -> Void ) async throws { let input = RPCAsyncSequence.makeBackpressuredStream( of: RPCRequestPart.self, @@ -111,8 +119,12 @@ struct ServerRPCExecutorTestHarness { func execute( handler: ServerHandler<[UInt8], [UInt8]> = .echo, - producer: @escaping (RPCWriter.Closable) async throws -> Void, - consumer: @escaping (RPCAsyncSequence) async throws -> Void + producer: @escaping @Sendable ( + RPCWriter.Closable + ) async throws -> Void, + consumer: @escaping @Sendable ( + RPCAsyncSequence + ) async throws -> Void ) async throws { try await self.execute( deserializer: IdentityDeserializer(), diff --git a/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift b/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift index b2e9f173a..041019f1d 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift @@ -590,7 +590,7 @@ final class BufferedStreamTests: XCTestCase { of: Int.self, backPressureStrategy: .watermark(low: 0, high: 0) ) - let (producerStream, producerContinuation) = AsyncThrowingStream.makeStream() + let (producerStream, producerContinuation) = AsyncThrowingStream.makeStream() var iterator = stream.makeAsyncIterator() source?.write(1) { @@ -688,7 +688,7 @@ final class BufferedStreamTests: XCTestCase { backPressureStrategy: .watermark(low: 1, high: 2) ) - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() try await source.write(1) @@ -749,7 +749,7 @@ final class BufferedStreamTests: XCTestCase { ) var iterator = stream.makeAsyncIterator() - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() let writeResult = try { try source.write(1) }() @@ -779,7 +779,7 @@ final class BufferedStreamTests: XCTestCase { ) var iterator = stream.makeAsyncIterator() - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() let writeResult = try { try source.write(1) }() @@ -809,7 +809,7 @@ final class BufferedStreamTests: XCTestCase { ) var iterator = stream.makeAsyncIterator() - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() let writeResult = try { try source.write(1) }() @@ -843,7 +843,7 @@ final class BufferedStreamTests: XCTestCase { backPressureStrategy: .watermark(low: 1, high: 2) ) - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() try await source.write(1) @@ -877,7 +877,7 @@ final class BufferedStreamTests: XCTestCase { backPressureStrategy: .watermark(low: 1, high: 2) ) - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() + let (producerStream, producerSource) = AsyncThrowingStream.makeStream() try await source.write(1) diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift index da49f7bd2..3008cf3ef 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift @@ -19,12 +19,10 @@ import struct Foundation.Data import class Foundation.JSONDecoder import class Foundation.JSONEncoder -private let jsonEncoder = JSONEncoder() -private let jsonDecoder = JSONDecoder() - struct JSONSerializer: MessageSerializer { func serialize(_ message: Message) throws -> [UInt8] { do { + let jsonEncoder = JSONEncoder() return try Array(jsonEncoder.encode(message)) } catch { throw RPCError(code: .internalError, message: "Can't serialize message to JSON. \(error)") @@ -35,6 +33,7 @@ struct JSONSerializer: MessageSerializer { struct JSONDeserializer: MessageDeserializer { func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message { do { + let jsonDecoder = JSONDecoder() return try jsonDecoder.decode(Message.self, from: Data(serializedMessageBytes)) } catch { throw RPCError(code: .internalError, message: "Can't deserialze message from JSON. \(error)") diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift index 5160b29b5..b5a7b0771 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -41,7 +41,7 @@ private struct FailOnWrite: RPCWriterProtocol { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -private struct AsyncStreamGatheringWriter: RPCWriterProtocol { +private struct AsyncStreamGatheringWriter: RPCWriterProtocol { let continuation: AsyncStream.Continuation init(continuation: AsyncStream.Continuation) { diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index d385f719e..b09293117 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -25,7 +25,7 @@ struct AnyClientTransport: ClientTransport, Sendable { @Sendable ( _ method: MethodDescriptor, _ options: CallOptions, - _ body: (RPCStream) async throws -> Any + _ body: (RPCStream) async throws -> (any Sendable) ) async throws -> Any private let _connect: @Sendable () async throws -> Void private let _close: @Sendable () -> Void @@ -36,7 +36,7 @@ struct AnyClientTransport: ClientTransport, Sendable { self._retryThrottle = { transport.retryThrottle } self._withStream = { descriptor, options, closure in try await transport.withStream(descriptor: descriptor, options: options) { stream in - try await closure(stream) as Any + try await closure(stream) as (any Sendable) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 0c49e7820..28111bd15 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -94,7 +94,7 @@ struct StreamCountingServerTransport: ServerTransport, Sendable { } func listen( - _ streamHandler: @escaping (RPCStream) async -> Void + _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { try await self.transport.listen { stream in self._acceptedStreams.wrappingIncrement(ordering: .sequentiallyConsistent) diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index d2e8511bc..a6bb1ee50 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -28,7 +28,7 @@ func XCTAssertDescription( @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsErrorAsync( _ expression: () async throws -> T, - errorHandler: (Error) -> Void + errorHandler: (any Error) -> Void ) async { do { _ = try await expression() diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift index 1e043dba3..4a72424aa 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift @@ -18,18 +18,7 @@ import GRPCCore import GRPCHTTP2Core // Equatable conformance for these types is 'best effort', this is sufficient for testing but not -// for general use. As such the conformance is added in the test module and must be declared -// as a `@retroactive` conformance. -#if compiler(>=6.0) -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension Connection.Event: @retroactive Equatable {} -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension Connection.CloseReason: @retroactive Equatable {} - -extension ClientConnectionEvent: @retroactive Equatable {} -extension ClientConnectionEvent.CloseReason: @retroactive Equatable {} - -#else +// for general use. @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Connection.Event: Equatable {} @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) @@ -37,7 +26,6 @@ extension Connection.CloseReason: Equatable {} extension ClientConnectionEvent: Equatable {} extension ClientConnectionEvent.CloseReason: Equatable {} -#endif @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) extension Connection.Event { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift index a5e063468..1f38b5634 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift @@ -204,9 +204,9 @@ final class ConnectionTests: XCTestCase { extension ClientBootstrap { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - func connect( + func connect( to address: GRPCHTTP2Core.SocketAddress, - _ configure: @Sendable @escaping (Channel) -> EventLoopFuture + _ configure: @Sendable @escaping (any Channel) -> EventLoopFuture ) async throws -> T { if let ipv4 = address.ipv4 { return try await self.connect( diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift index e30ec996b..9971e92de 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift @@ -86,7 +86,7 @@ enum LoadBalancerTest { ) async throws { enum TestEvent { case timedOut - case completed(Result) + case completed(Result) } try await withThrowingTaskGroup(of: TestEvent.self) { group in diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift index 62509f4ad..d42fccdb2 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -29,7 +29,7 @@ enum ConnectionTest { } static func run( - connector: HTTP2Connector, + connector: any HTTP2Connector, server mode: Server.Mode = .regular, handlEvents: ( _ context: Context, @@ -67,10 +67,10 @@ extension ConnectionTest { final class Server { private let eventLoop: any EventLoop private var listener: (any Channel)? - private let client: EventLoopPromise + private let client: EventLoopPromise private let mode: Mode - enum Mode { + enum Mode: Sendable { case regular case closeOnAccept } @@ -86,7 +86,7 @@ extension ConnectionTest { self.client.futureResult.whenSuccess { $0.close(mode: .all, promise: nil) } } - var acceptedChannel: Channel { + var acceptedChannel: any Channel { get throws { try self.client.futureResult.wait() } @@ -95,15 +95,17 @@ extension ConnectionTest { func bind() async throws -> GRPCHTTP2Core.SocketAddress { precondition(self.listener == nil, "\(#function) must only be called once") - let hasAcceptedChannel = try await self.eventLoop.submit { - NIOLoopBoundBox(false, eventLoop: self.eventLoop) + let hasAcceptedChannel = try await self.eventLoop.submit { [loop = self.eventLoop] in + NIOLoopBoundBox(false, eventLoop: loop) }.get() - let bootstrap = ServerBootstrap(group: self.eventLoop).childChannelInitializer { channel in + let bootstrap = ServerBootstrap( + group: self.eventLoop + ).childChannelInitializer { [mode = self.mode, client = self.client] channel in precondition(!hasAcceptedChannel.value, "already accepted a channel") hasAcceptedChannel.value = true - switch self.mode { + switch mode { case .closeOnAccept: return channel.close() @@ -128,7 +130,7 @@ extension ConnectionTest { try sync.addHandler(h2) try sync.addHandler(mux) - try sync.addHandlers(SucceedOnSettingsAck(promise: self.client)) + try sync.addHandlers(SucceedOnSettingsAck(promise: client)) } } } @@ -147,9 +149,9 @@ extension ConnectionTest { typealias InboundIn = HTTP2Frame typealias InboundOut = HTTP2Frame - private let promise: EventLoopPromise + private let promise: EventLoopPromise - init(promise: EventLoopPromise) { + init(promise: EventLoopPromise) { self.promise = promise } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift index 68d8dfbf7..9d3973025 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift @@ -44,8 +44,8 @@ extension NameResolver { } @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -struct ConstantAsyncSequence: AsyncSequence { - private let result: Result +struct ConstantAsyncSequence: AsyncSequence, Sendable { + private let result: Result init(element: Element) { self.result = .success(element) @@ -60,9 +60,9 @@ struct ConstantAsyncSequence: AsyncSequence { } struct AsyncIterator: AsyncIteratorProtocol { - private let result: Result + private let result: Result - fileprivate init(result: Result) { + fileprivate init(result: Result) { self.result = result } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift index 488165162..b59c4be5e 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -29,7 +29,7 @@ final class TestServer: Sendable { private typealias Stream = NIOAsyncChannel private typealias Multiplexer = NIOHTTP2AsyncSequence - private let connected: NIOLockedValueBox<[Channel]> + private let connected: NIOLockedValueBox<[any Channel]> typealias Inbound = NIOAsyncChannelInboundStream typealias Outbound = NIOAsyncChannelOutboundWriter @@ -47,7 +47,7 @@ final class TestServer: Sendable { case uds(String) } - var clients: [Channel] { + var clients: [any Channel] { return self.connected.withLockedValue { $0 } } @@ -55,7 +55,7 @@ final class TestServer: Sendable { precondition(self.server.withLockedValue { $0 } == nil) @Sendable - func configure(_ channel: Channel) -> EventLoopFuture { + func configure(_ channel: any Channel) -> EventLoopFuture { self.connected.withLockedValue { $0.append(channel) } diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 62589bf74..5bec991a3 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -2886,10 +2886,5 @@ extension GRPCStreamStateMachine.OnNextOutboundFrame { } } -#if compiler(>=6.0) -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCStreamStateMachine.OnNextOutboundFrame: @retroactive Equatable {} -#else @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension GRPCStreamStateMachine.OnNextOutboundFrame: Equatable {} -#endif diff --git a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift index fadfe4fd6..77726347d 100644 --- a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift @@ -15,32 +15,33 @@ */ import Atomics +import GRPCCore +import GRPCHTTP2Core import NIOEmbedded import XCTest -@testable import GRPCHTTP2Core - internal final class TimerTests: XCTestCase { func testScheduleOneOffTimer() { let loop = EmbeddedEventLoop() defer { try! loop.close() } - var value = 0 - + let value = LockedValueBox(0) var timer = Timer(delay: .seconds(1), repeat: false) timer.schedule(on: loop) { - XCTAssertEqual(value, 0) - value += 1 + value.withLockedValue { + XCTAssertEqual($0, 0) + $0 += 1 + } } loop.advanceTime(by: .milliseconds(999)) - XCTAssertEqual(value, 0) + XCTAssertEqual(value.withLockedValue { $0 }, 0) loop.advanceTime(by: .milliseconds(1)) - XCTAssertEqual(value, 1) + XCTAssertEqual(value.withLockedValue { $0 }, 1) // Run again to make sure the task wasn't repeated. loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(value, 1) + XCTAssertEqual(value.withLockedValue { $0 }, 1) } func testCancelOneOffTimer() { @@ -61,26 +62,25 @@ internal final class TimerTests: XCTestCase { let loop = EmbeddedEventLoop() defer { try! loop.close() } - var values = [Int]() - + let values = LockedValueBox([Int]()) var timer = Timer(delay: .seconds(1), repeat: true) timer.schedule(on: loop) { - values.append(values.count) + values.withLockedValue { $0.append($0.count) } } loop.advanceTime(by: .milliseconds(999)) - XCTAssertEqual(values, []) + XCTAssertEqual(values.withLockedValue { $0 }, []) loop.advanceTime(by: .milliseconds(1)) - XCTAssertEqual(values, [0]) + XCTAssertEqual(values.withLockedValue { $0 }, [0]) loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(values, [0, 1]) + XCTAssertEqual(values.withLockedValue { $0 }, [0, 1]) loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(values, [0, 1, 2]) + XCTAssertEqual(values.withLockedValue { $0 }, [0, 1, 2]) timer.cancel() loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(values, [0, 1, 2]) + XCTAssertEqual(values.withLockedValue { $0 }, [0, 1, 2]) } func testCancelRepeatedTimer() { diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 7d21dae8c..cf4cd16d7 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -135,7 +135,7 @@ final class HTTP2TransportTests: XCTestCase { } private func runServer( - in group: inout ThrowingTaskGroup, + in group: inout ThrowingTaskGroup, kind: Transport.Kind, enableControlService: Bool, compression: CompressionAlgorithmSet diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift index 867585d09..d4f6e00ee 100644 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift @@ -24,7 +24,7 @@ import NIOPosix /// then closing. Each stream will be closed with the ":status" set to the value of the /// "response-status" header field in the request headers. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -final class HTTP2StatusCodeServer { +final class HTTP2StatusCodeServer: Sendable { private let address: EventLoopPromise private let eventLoopGroup: MultiThreadedEventLoopGroup From 04e6e0e3460d20a6fac11f5dcf8b553e0d73b8cd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 23 Jul 2024 07:49:55 +0100 Subject: [PATCH 405/580] Avoid allocating a closure for the client rpc executor (#1986) Motivation: The client RPC executor unconditionally allocates a closure when intercepting a request. This can be avoided if there are no interceptors. Modifications: - Special case the no-interceptors path Result: Fewer allocations --- .../Client/Internal/ClientRPCExecutor.swift | 95 +++++++++++++------ 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 673dbb2b7..e404876ea 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -130,42 +130,75 @@ extension ClientRPCExecutor { ) async -> ClientResponse.Stream { let context = ClientInterceptorContext(descriptor: method) - return await Self._intercept( - request: request, - context: context, - interceptors: interceptors - ) { request, context in - // Let the server know this is a retry. - var metadata = request.metadata - if attempt > 1 { - metadata.previousRPCAttempts = attempt &- 1 - } - - var response = await streamProcessor.execute( - request: ClientRequest.Stream<[UInt8]>(metadata: metadata) { writer in - try await request.producer(.serializing(into: writer, with: serializer)) - }, - method: context.descriptor, + if interceptors.isEmpty { + return await Self._runRPC( + request: request, + context: context, + attempt: attempt, + serializer: serializer, + deserializer: deserializer, + streamProcessor: streamProcessor, stream: stream ) + } else { + return await Self._intercept( + request: request, + context: context, + interceptors: interceptors + ) { request, context in + return await Self._runRPC( + request: request, + context: context, + attempt: attempt, + serializer: serializer, + deserializer: deserializer, + streamProcessor: streamProcessor, + stream: stream + ) + } + } + } - // Attach the number of previous attempts, it can be useful information for callers. - if attempt > 1 { - switch response.accepted { - case .success(var contents): - contents.metadata.previousRPCAttempts = attempt &- 1 - response.accepted = .success(contents) + @inlinable + static func _runRPC( + request: ClientRequest.Stream, + context: ClientInterceptorContext, + attempt: Int, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + streamProcessor: ClientStreamExecutor, + stream: RPCStream + ) async -> ClientResponse.Stream { + // Let the server know this is a retry. + var metadata = request.metadata + if attempt > 1 { + metadata.previousRPCAttempts = attempt &- 1 + } - case .failure(var error): - error.metadata.previousRPCAttempts = attempt &- 1 - response.accepted = .failure(error) - } - } + var response = await streamProcessor.execute( + request: ClientRequest.Stream<[UInt8]>(metadata: metadata) { writer in + try await request.producer(.serializing(into: writer, with: serializer)) + }, + method: context.descriptor, + stream: stream + ) - return response.map { bytes in - try deserializer.deserialize(bytes) + // Attach the number of previous attempts, it can be useful information for callers. + if attempt > 1 { + switch response.accepted { + case .success(var contents): + contents.metadata.previousRPCAttempts = attempt &- 1 + response.accepted = .success(contents) + + case .failure(var error): + error.metadata.previousRPCAttempts = attempt &- 1 + response.accepted = .failure(error) } } + + return response.map { bytes in + try deserializer.deserialize(bytes) + } } @inlinable @@ -173,7 +206,7 @@ extension ClientRPCExecutor { request: ClientRequest.Stream, context: ClientInterceptorContext, interceptors: [any ClientInterceptor], - finally: @escaping @Sendable ( + finally: @Sendable ( _ request: ClientRequest.Stream, _ context: ClientInterceptorContext ) async -> ClientResponse.Stream @@ -191,7 +224,7 @@ extension ClientRPCExecutor { request: ClientRequest.Stream, context: ClientInterceptorContext, iterator: Array.Iterator, - finally: @escaping @Sendable ( + finally: @Sendable ( _ request: ClientRequest.Stream, _ context: ClientInterceptorContext ) async -> ClientResponse.Stream From c00c957e2aba5bfa8b29bcac9b487555fd420a1a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 23 Jul 2024 15:42:17 +0100 Subject: [PATCH 406/580] Fix broken symlinks (#1988) Motivation: Reshuffling the example code in f77ea808 broke some symlinks which fuzzing relied on. Modifications: - Fix broken symlinks - Bump tools version Result: Fuzzing builds --- FuzzTesting/Package.swift | 2 +- FuzzTesting/Sources/EchoImplementation | 2 +- FuzzTesting/Sources/EchoModel | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FuzzTesting/Package.swift b/FuzzTesting/Package.swift index 4d4da31e5..0f62ecf69 100644 --- a/FuzzTesting/Package.swift +++ b/FuzzTesting/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 /* * Copyright 2021, gRPC Authors All rights reserved. * diff --git a/FuzzTesting/Sources/EchoImplementation b/FuzzTesting/Sources/EchoImplementation index d43c0890e..eaa26a3be 120000 --- a/FuzzTesting/Sources/EchoImplementation +++ b/FuzzTesting/Sources/EchoImplementation @@ -1 +1 @@ -../../Sources/Examples/Echo/Implementation \ No newline at end of file +../../Sources/Examples/v1/Echo/Implementation \ No newline at end of file diff --git a/FuzzTesting/Sources/EchoModel b/FuzzTesting/Sources/EchoModel index bae97d151..d0ff42971 120000 --- a/FuzzTesting/Sources/EchoModel +++ b/FuzzTesting/Sources/EchoModel @@ -1 +1 @@ -../../Sources/Examples/Echo/Model \ No newline at end of file +../../Sources/Examples/v1/Echo/Model \ No newline at end of file From d3827db148448c27e0e79163d5753cefc91ac9a4 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 24 Jul 2024 10:30:00 +0100 Subject: [PATCH 407/580] Remove unnecessary README (#1989) --- Sources/GRPCHTTP2Core/README.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Sources/GRPCHTTP2Core/README.md diff --git a/Sources/GRPCHTTP2Core/README.md b/Sources/GRPCHTTP2Core/README.md deleted file mode 100644 index a0f051cc7..000000000 --- a/Sources/GRPCHTTP2Core/README.md +++ /dev/null @@ -1 +0,0 @@ -# gRPC HTTP2 Core From 06b7e97e3974bfd86b81d24bc64541057860ca0b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 24 Jul 2024 13:42:17 +0100 Subject: [PATCH 408/580] Use plugin work directory URL (#1990) Motivation: The `Path` API in the plugins is deprecated from Swift 6. This was only deprecated in Xcode 16 Beta 4. Modifications: - Use the newly added URL API Result: Fewer warnings --- Plugins/GRPCSwiftPlugin/plugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift index 38e697b04..107b3f787 100644 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ b/Plugins/GRPCSwiftPlugin/plugin.swift @@ -393,7 +393,7 @@ extension GRPCSwiftPlugin: XcodeBuildToolPlugin { #if compiler(<6.0) let workDirectory = PathLike(context.pluginWorkDirectory) #else - let workDirectory = PathLike(URL(string: String(describing: context.pluginWorkDirectory))!) + let workDirectory = PathLike(context.pluginWorkDirectoryURL) #endif return try self.createBuildCommands( From 899c1218276bf4e2273bc91bf93b7a744911a0f5 Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Wed, 24 Jul 2024 16:48:26 +0100 Subject: [PATCH 409/580] Save an allocation in BufferedStream (#1991) --- Sources/GRPCCore/Streaming/Internal/BufferedStream.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift index 86e7547fb..f6fd3dcc1 100644 --- a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift +++ b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift @@ -635,16 +635,14 @@ extension BufferedStream { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BufferedStream { - // We are unchecked Sendable since we are protecting our state with a lock. @usableFromInline - final class _BackPressuredStorage: Sendable { - /// The state machine + struct _BackPressuredStorage: Sendable { @usableFromInline let _stateMachine: _ManagedCriticalState<_StateMachine> @usableFromInline var onTermination: (@Sendable () -> Void)? { - set { + nonmutating set { self._stateMachine.withCriticalRegion { $0._onTermination = newValue } From 33a25b7050910cb23ef84c0a0edafbd05671999b Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 25 Jul 2024 08:18:04 +0100 Subject: [PATCH 410/580] Change visibility of `_makeBackpressuredStream` to `package` (#1992) --- .../Streaming/Internal/RPCAsyncSequence+Buffered.swift | 10 +--------- .../InProcessClientTransport.swift | 4 ++-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift index aa367b780..3a0c75aa6 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift @@ -17,7 +17,7 @@ @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RPCAsyncSequence where Failure == any Error { @inlinable - static func makeBackpressuredStream( + package static func makeBackpressuredStream( of elementType: Element.Type = Element.self, watermarks: (low: Int, high: Int) ) -> (stream: Self, writer: RPCWriter.Closable) { @@ -28,12 +28,4 @@ extension RPCAsyncSequence where Failure == any Error { return (RPCAsyncSequence(wrapping: stream), RPCWriter.Closable(wrapping: continuation)) } - - @inlinable - public static func _makeBackpressuredStream( - of elementType: Element.Type = Element.self, - watermarks: (low: Int, high: Int) - ) -> (stream: Self, writer: RPCWriter.Closable) { - return Self.makeBackpressuredStream(of: elementType, watermarks: watermarks) - } } diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index ca4c02853..a34020676 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -233,10 +233,10 @@ public struct InProcessClientTransport: ClientTransport { options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { - let request = RPCAsyncSequence._makeBackpressuredStream( + let request = RPCAsyncSequence.makeBackpressuredStream( watermarks: (16, 32) ) - let response = RPCAsyncSequence._makeBackpressuredStream( + let response = RPCAsyncSequence.makeBackpressuredStream( watermarks: (16, 32) ) From 0aad7094c3f3c973b7dee92c05b8fa64a5c29177 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 25 Jul 2024 14:37:41 +0100 Subject: [PATCH 411/580] Add language mode shim to SwiftPM (#1995) Motivation: Nightly toolchains now use 'swiftLanguageMode' instead of 'swiftLanguageVersion'. Modifications: - Add a temporary shim until there's an Xcode with 'swiftLanguageMode' Result: CI builds --- Package@swift-6.swift | 104 ++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 31c28b55b..ea133c329 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -27,6 +27,20 @@ let cgrpcZlibTargetName = cgrpcZlibProductName let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil +// Temporary shim: nightly toolchains renamed 'swiftLanguageVersion' to 'swiftLanguageMode'. This +// isn't yet available in a beta Xcode. +// +// See also: https://github.com/swiftlang/swift-package-manager/issues/7823 +extension SwiftSetting { + static func _swiftLanguageMode(_ version: SwiftVersion) -> SwiftSetting { + #if os(Linux) + return .swiftLanguageMode(version) + #else + return .swiftLanguageVersion(version) + #endif + } +} + // MARK: - Package Dependencies let packageDependencies: [Package.Dependency] = [ @@ -181,7 +195,7 @@ extension Target { .nioSSL, if: includeNIOSSL ), path: "Sources/GRPC", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -193,7 +207,7 @@ extension Target { .atomics ], path: "Sources/GRPCCore", - swiftSettings: [.swiftLanguageVersion(.v5), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v5), .enableUpcomingFeature("ExistentialAny")] ) } @@ -203,7 +217,7 @@ extension Target { dependencies: [ .grpcCore ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -214,7 +228,7 @@ extension Target { .grpcCore, .tracing ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -229,7 +243,7 @@ extension Target { .dequeModule, .atomics ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -242,7 +256,7 @@ extension Target { .nioPosix, .nioExtras ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -256,7 +270,7 @@ extension Target { .nioExtras, .nioTransportServices ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -269,7 +283,7 @@ extension Target { .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -295,7 +309,7 @@ extension Target { exclude: [ "README.md", ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -311,7 +325,7 @@ extension Target { .nioFileSystem, .argumentParser ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -352,7 +366,7 @@ extension Target { exclude: [ "Codegen/Serialization/echo.grpc.reflection" ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -366,7 +380,7 @@ extension Target { .atomics, .protobuf, ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -377,7 +391,7 @@ extension Target { .grpcCore, .grpcInProcessTransport ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -390,7 +404,7 @@ extension Target { .nioCore, .grpcInterceptors ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -404,7 +418,7 @@ extension Target { .nioEmbedded, .nioTestUtils, ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -417,7 +431,7 @@ extension Target { .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -427,7 +441,7 @@ extension Target { dependencies: [ .grpcCodeGen ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -439,7 +453,7 @@ extension Target { .grpcCore, .protobuf ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -452,7 +466,7 @@ extension Target { .protobuf, .protobufPluginLibrary ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -464,7 +478,7 @@ extension Target { .interoperabilityTests, .grpcCore ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -485,7 +499,7 @@ extension Target { "src/proto/grpc/testing/test.proto", "unimplemented_call.patch", ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -496,7 +510,7 @@ extension Target { .grpcCore, .grpcProtobuf ], - swiftSettings: [.swiftLanguageVersion(.v5), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v5), .enableUpcomingFeature("ExistentialAny")] ) } @@ -510,7 +524,7 @@ extension Target { .interoperabilityTests, .argumentParser ], - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -527,7 +541,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -542,7 +556,7 @@ extension Target { .logging, .argumentParser, ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -560,7 +574,7 @@ extension Target { exclude: [ "README.md", ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -576,7 +590,7 @@ extension Target { .nioHTTP2, .argumentParser, ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -587,7 +601,7 @@ extension Target { exclude: [ "bundle.p12", ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -600,7 +614,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/Echo/Model", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -615,7 +629,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/Echo/Implementation", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -635,7 +649,7 @@ extension Target { .nioSSL, if: includeNIOSSL ), path: "Sources/Examples/v1/Echo/Runtime", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -652,7 +666,7 @@ extension Target { .nioSSL, if: includeNIOSSL ), path: "Sources/Examples/v2/Echo", - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -665,7 +679,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/HelloWorld/Model", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -680,7 +694,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/HelloWorld/Client", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -695,7 +709,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/HelloWorld/Server", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -708,7 +722,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/RouteGuide/Model", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -723,7 +737,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/RouteGuide/Client", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -739,7 +753,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/RouteGuide/Server", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -758,7 +772,7 @@ extension Target { exclude: [ "README.md", ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -771,7 +785,7 @@ extension Target { .protobuf, ], path: "Sources/GRPCReflectionService", - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -792,7 +806,7 @@ extension Target { resources: [ .copy("Generated") ], - swiftSettings: [.swiftLanguageVersion(.v5)] + swiftSettings: [._swiftLanguageMode(.v5)] ) } @@ -800,7 +814,7 @@ extension Target { .target( name: "GRPCCodeGen", path: "Sources/GRPCCodeGen", - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -812,7 +826,7 @@ extension Target { .protobuf, ], path: "Sources/GRPCProtobuf", - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -825,7 +839,7 @@ extension Target { .grpcCodeGen ], path: "Sources/GRPCProtobufCodeGen", - swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -837,7 +851,7 @@ extension Target { .grpcProtobuf ], path: "Sources/Services/Health", - swiftSettings: [.swiftLanguageVersion(.v6)] + swiftSettings: [._swiftLanguageMode(.v6)] ) } } From 945f7f779bbac0a201374a6e70f9d28e534f7cf7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 26 Jul 2024 10:25:14 +0100 Subject: [PATCH 412/580] Shutdown server gracefully in h2 transport tests (#1996) Motivation: The h2 transport tests cancel the task group running them when the test has completed. Cancelling the server makes https://github.com/apple/swift-nio/issues/2813 more likely to occur. Modifications: - Tell the server to stop listening and let the test stop gracefully Result: Fewer runtime issues --- .../HTTP2TransportTests.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index cf4cd16d7..7ffeee209 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -57,7 +57,7 @@ final class HTTP2TransportTests: XCTestCase { ) async throws { for pair in transport { try await withThrowingTaskGroup(of: Void.self) { group in - let address = try await self.runServer( + let (server, address) = try await self.runServer( in: &group, kind: pair.server, enableControlService: enableControlService, @@ -94,8 +94,8 @@ final class HTTP2TransportTests: XCTestCase { XCTFail("Unexpected error: '\(error)' (\(pair))") } + server.stopListening() client.close() - group.cancelAll() } } } @@ -139,7 +139,7 @@ final class HTTP2TransportTests: XCTestCase { kind: Transport.Kind, enableControlService: Bool, compression: CompressionAlgorithmSet - ) async throws -> GRPCHTTP2Core.SocketAddress { + ) async throws -> (GRPCServer, GRPCHTTP2Core.SocketAddress) { let services = enableControlService ? [ControlService()] : [] switch kind { @@ -156,7 +156,8 @@ final class HTTP2TransportTests: XCTestCase { try await server.run() } - return try await transport.listeningAddress + let address = try await transport.listeningAddress + return (server, address) case .niots: #if canImport(Network) @@ -172,7 +173,8 @@ final class HTTP2TransportTests: XCTestCase { try await server.run() } - return try await transport.listeningAddress + let address = try await transport.listeningAddress + return (server, address) #else throw XCTSkip("Transport not supported on this platform") #endif From 3f749833b24e5eb0ffa11aaef7e0e0e5c3aca488 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 29 Jul 2024 11:32:32 +0100 Subject: [PATCH 413/580] Simplify client rpc execution (#1993) * Simplify client rpc execution Motivation: The existing code for executing RPCs on the client is layered, the one-shot, retry and hedging executors serialize the messages of the requests they're processing into bytes. These 'raw' requests are processed by the stream executor which is structured in such a way as requiring a task group of events it needs to handled. These events are handled in a run method (which is in turn another task for the caller to run). This requires quite a bit of machinery (and allocations) for it to work. Modifications: - Push the serialization/deserialization of messages down into the stream executor. This lets a typed request be transformed directly to `RPCRequestPart`s and for `RPCResponsePart`s to be transformed directly to a typed response (rather than via an intermediary request/response typed to `[UInt8]`). - Remove the sendability requirement from the `next` function for client interceptors. This makes sense: interceptors should be straight-line code so shouldn't be shuffled off into a subtask. This unlocks the ability to remove the task group and event stream previously used by the stream executor as it can add child tasks to a provided task group (rather than needing a separate task group with an event stream). - Apply this change to the one-shot, hedging, and retry executors. Result: - ~35% reduction in allocations for unary RPCs on the client * use task group void * fix comment --- .../Call/Client/ClientInterceptor.swift | 2 +- .../ClientRPCExecutor+HedgingExecutor.swift | 87 +++--- .../ClientRPCExecutor+OneShotExecutor.swift | 163 ++++++----- .../ClientRPCExecutor+RetryExecutor.swift | 252 +++++++++--------- .../Client/Internal/ClientRPCExecutor.swift | 99 ++----- .../Internal/ClientStreamExecutor.swift | 213 ++++++++------- .../Streaming/Internal/RPCWriter+Map.swift | 8 +- .../ClientTracingInterceptor.swift | 6 +- .../Call/Client/ClientInterceptors.swift | 4 +- 9 files changed, 397 insertions(+), 437 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 999bdee25..2876fbe76 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -102,7 +102,7 @@ public protocol ClientInterceptor: Sendable { func intercept( request: ClientRequest.Stream, context: ClientInterceptorContext, - next: @Sendable ( + next: ( _ request: ClientRequest.Stream, _ context: ClientInterceptorContext ) async throws -> ClientResponse.Stream diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index d537c70c1..5bb27190f 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -103,12 +103,7 @@ extension ClientRPCExecutor.HedgingExecutor { } group.addTask { - var metadata = request.metadata - if let deadline = self.deadline { - metadata.timeout = ContinuousClock.now.duration(to: deadline) - } - - let replayableRequest = ClientRequest.Stream(metadata: metadata) { writer in + let replayableRequest = ClientRequest.Stream(metadata: request.metadata) { writer in try await writer.write(contentsOf: broadcast.stream) } @@ -243,6 +238,10 @@ extension ClientRPCExecutor.HedgingExecutor { () } + case .attemptPicked: + // Not used by this task group. + fatalError("Internal inconsistency") + case .attemptCompleted(let outcome): switch outcome { case .usableResponse(let response): @@ -327,7 +326,7 @@ extension ClientRPCExecutor.HedgingExecutor { descriptor: method, options: options ) { stream -> _HedgingAttemptTaskResult.AttemptResult in - return await withTaskGroup(of: _HedgingAttemptSubtaskResult.self) { group in + return await withTaskGroup(of: _HedgingAttemptTaskResult.self) { group in group.addTask { do { // The picker stream will have at most one element. @@ -340,35 +339,27 @@ extension ClientRPCExecutor.HedgingExecutor { } } - let processor = ClientStreamExecutor(transport: self.transport) - group.addTask { - await processor.run() - return .processorFinished - } - - group.addTask { - let response = await ClientRPCExecutor.unsafeExecute( - request: request, - method: method, - attempt: attempt, - serializer: self.serializer, - deserializer: self.deserializer, - interceptors: self.interceptors, - streamProcessor: processor, - stream: stream - ) - return .response(response) - } - - for await next in group { - switch next { - case .attemptPicked(let wasPicked): - if !wasPicked { - group.cancelAll() + let result = await withTaskGroup( + of: Void.self, + returning: _HedgingAttemptTaskResult.AttemptResult.self + ) { group in + var request = request + if let deadline = self.deadline { + request.metadata.timeout = ContinuousClock.now.duration(to: deadline) } - case .response(let response): + let response = await ClientRPCExecutor._execute( + in: &group, + request: request, + method: method, + attempt: attempt, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + stream: stream + ) + switch response.accepted { case .success: self.transport.retryThrottle?.recordSuccess() @@ -405,10 +396,25 @@ extension ClientRPCExecutor.HedgingExecutor { } } } + } - case .processorFinished: - // Processor finished, wait for the response outcome. - () + return .attemptCompleted(result) + } + + for await next in group { + switch next { + case .attemptPicked(let wasPicked): + if !wasPicked { + group.cancelAll() + } + + case .attemptCompleted(let result): + group.cancelAll() + return result + + case .scheduledAttemptFired: + // Not used by this task group. + fatalError("Internal inconsistency") } } @@ -516,6 +522,7 @@ enum _HedgingTaskResult { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline enum _HedgingAttemptTaskResult { + case attemptPicked(Bool) case attemptCompleted(AttemptResult) case scheduledAttemptFired(ScheduleEvent) @@ -532,11 +539,3 @@ enum _HedgingAttemptTaskResult { case cancelled } } - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -@usableFromInline -enum _HedgingAttemptSubtaskResult { - case attemptPicked(Bool) - case processorFinished - case response(ClientResponse.Stream) -} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index b587df6eb..47668a8f9 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -66,86 +66,115 @@ extension ClientRPCExecutor.OneShotExecutor { options: CallOptions, responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R { - let result = await withTaskGroup( - of: _OneShotExecutorTask.self, - returning: Result.self - ) { group in + let result: Result + + if let deadline = self.deadline { + var request = request + request.metadata.timeout = ContinuousClock.now.duration(to: deadline) + result = await withDeadline(deadline) { + await self._execute( + request: request, + method: method, + options: options, + responseHandler: responseHandler + ) + } + } else { + result = await self._execute( + request: request, + method: method, + options: options, + responseHandler: responseHandler + ) + } + + return try result.get() + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension ClientRPCExecutor.OneShotExecutor { + @inlinable + func _execute( + request: ClientRequest.Stream, + method: MethodDescriptor, + options: CallOptions, + responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async -> Result { + return await withTaskGroup(of: Void.self, returning: Result.self) { group in do { return try await self.transport.withStream(descriptor: method, options: options) { stream in - var request = request - - if let deadline = self.deadline { - request.metadata.timeout = ContinuousClock.now.duration(to: deadline) - group.addTask { - let result = await Result { - try await Task.sleep(until: deadline, clock: .continuous) - } - return .timedOut(result) - } + let response = await ClientRPCExecutor._execute( + in: &group, + request: request, + method: method, + attempt: 1, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + stream: stream + ) + + let result = await Result { + try await responseHandler(response) } - let streamExecutor = ClientStreamExecutor(transport: self.transport) - group.addTask { - await streamExecutor.run() - return .streamExecutorCompleted - } - - group.addTask { [request] in - let response = await ClientRPCExecutor.unsafeExecute( - request: request, - method: method, - attempt: 1, - serializer: self.serializer, - deserializer: self.deserializer, - interceptors: self.interceptors, - streamProcessor: streamExecutor, - stream: stream - ) - - let result = await Result { - try await responseHandler(response) - } - - return .responseHandled(result) - } + // The user handler can finish before the stream. Cancel it if that's the case. + group.cancelAll() - while let result = await group.next() { - switch result { - case .streamExecutorCompleted: - // Stream finished; wait for the response to be handled. - () - - case .timedOut(.success): - // The deadline passed; cancel the ongoing work group. - group.cancelAll() - - case .timedOut(.failure): - // The deadline task failed (because the task was cancelled). Wait for the response - // to be handled. - () - - case .responseHandled(let result): - // Response handled: cancel any other remaining tasks. - group.cancelAll() - return result - } - } - - // Unreachable: exactly one task returns `responseHandled` and we return when it completes. - fatalError("Internal inconsistency") + return result } } catch { return .failure(error) } } + } +} - return try result.get() +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +@inlinable +func withDeadline( + _ deadline: ContinuousClock.Instant, + execute: @escaping () async -> Result +) async -> Result { + return await withTaskGroup(of: _DeadlineChildTaskResult.self) { group in + group.addTask { + do { + try await Task.sleep(until: deadline) + return .deadlinePassed + } catch { + return .timeoutCancelled + } + } + + group.addTask { + let result = await execute() + return .taskCompleted(result) + } + + while let next = await group.next() { + switch next { + case .deadlinePassed: + // Timeout expired; cancel the work. + group.cancelAll() + + case .timeoutCancelled: + () // Wait for more tasks to finish. + + case .taskCompleted(let result): + // The work finished. Cancel any remaining tasks. + group.cancelAll() + return result + } + } + + fatalError("Internal inconsistency") } } @usableFromInline -enum _OneShotExecutorTask { - case streamExecutorCompleted - case timedOut(Result) - case responseHandled(Result) +enum _DeadlineChildTaskResult { + case deadlinePassed + case timeoutCancelled + case taskCompleted(Value) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index da9c79780..7c29a6c05 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -125,131 +125,20 @@ extension ClientRPCExecutor.RetryExecutor { options: options ) { stream in group.addTask { - await withTaskGroup( - of: _RetryExecutorSubTask.self, - returning: _RetryExecutorTask.self - ) { thisAttemptGroup in - let streamExecutor = ClientStreamExecutor(transport: self.transport) - thisAttemptGroup.addTask { - await streamExecutor.run() - return .streamProcessed - } - - thisAttemptGroup.addTask { - var metadata = request.metadata - // Work out the timeout from the deadline. - if let deadline = self.deadline { - metadata.timeout = ContinuousClock.now.duration(to: deadline) - } - - let response = await ClientRPCExecutor.unsafeExecute( - request: ClientRequest.Stream(metadata: metadata) { - try await $0.write(contentsOf: retry.stream) - }, - method: method, - attempt: attempt, - serializer: self.serializer, - deserializer: self.deserializer, - interceptors: self.interceptors, - streamProcessor: streamExecutor, - stream: stream - ) - - let shouldRetry: Bool - let retryDelayOverride: Duration? - - switch response.accepted { - case .success: - // Request was accepted. This counts as success to the throttle and there's no need - // to retry. - self.transport.retryThrottle?.recordSuccess() - retryDelayOverride = nil - shouldRetry = false - - case .failure(let error): - // The request was rejected. Determine whether a retry should be carried out. The - // following conditions must be checked: - // - // - Whether the status code is retryable. - // - Whether more attempts are permitted by the config. - // - Whether the throttle permits another retry to be carried out. - // - Whether the server pushed back to either stop further retries or to override - // the delay before the next retry. - let code = Status.Code(error.code) - let isRetryableStatusCode = self.policy.retryableStatusCodes.contains(code) - - if isRetryableStatusCode { - // Counted as failure for throttling. - let throttled = self.transport.retryThrottle?.recordFailure() ?? false - - // Status code can be retried, Did the server send pushback? - switch error.metadata.retryPushback { - case .retryAfter(let delay): - // Pushback: only retry if our config permits it. - shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled - retryDelayOverride = delay - case .stopRetrying: - // Server told us to stop trying. - shouldRetry = false - retryDelayOverride = nil - case .none: - // No pushback: only retry if our config permits it. - shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled - retryDelayOverride = nil - break - } - } else { - // Not-retryable; this is considered a success. - self.transport.retryThrottle?.recordSuccess() - shouldRetry = false - retryDelayOverride = nil - } - } - - if shouldRetry { - // Cancel subscribers of the broadcast sequence. This is safe as we are the only - // subscriber and maximises the chances that 'isKnownSafeForNextSubscriber' will - // return true. - // - // Note: this must only be called if we should retry, otherwise we may cancel a - // subscriber for an accepted request. - retry.stream.invalidateAllSubscriptions() - - // Only retry if we know it's safe for the next subscriber, that is, the first - // element is still in the buffer. It's safe to call this because there's only - // ever one attempt at a time and the existing subscribers have been invalidated. - if retry.stream.isKnownSafeForNextSubscriber { - return .retry(retryDelayOverride) - } - } - - // Not retrying or not safe to retry. - let result = await Result { - // Check for cancellation; the RPC may have timed out in which case we should skip - // the response handler. - try Task.checkCancellation() - return try await responseHandler(response) - } - return .handledResponse(result) - } - - while let result = await thisAttemptGroup.next() { - switch result { - case .streamProcessed: - () // Continue processing; wait for the response to be handled. - - case .retry(let delayOverride): - thisAttemptGroup.cancelAll() - return .retry(delayOverride) - - case .handledResponse(let result): - thisAttemptGroup.cancelAll() - return .handledResponse(result) - } - } - - fatalError("Internal inconsistency") + var metadata = request.metadata + // Work out the timeout from the deadline. + if let deadline = self.deadline { + metadata.timeout = ContinuousClock.now.duration(to: deadline) } + + return await self.executeAttempt( + stream: stream, + metadata: metadata, + retryStream: retry.stream, + method: method, + attempt: attempt, + responseHandler: responseHandler + ) } loop: while let next = await group.next() { @@ -307,6 +196,113 @@ extension ClientRPCExecutor.RetryExecutor { return try result.get() } + + @inlinable + func executeAttempt( + stream: RPCStream, + metadata: Metadata, + retryStream: BroadcastAsyncSequence, + method: MethodDescriptor, + attempt: Int, + responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async -> _RetryExecutorTask { + return await withTaskGroup( + of: Void.self, + returning: _RetryExecutorTask.self + ) { group in + let request = ClientRequest.Stream(metadata: metadata) { + try await $0.write(contentsOf: retryStream) + } + + let response = await ClientRPCExecutor._execute( + in: &group, + request: request, + method: method, + attempt: attempt, + serializer: self.serializer, + deserializer: self.deserializer, + interceptors: self.interceptors, + stream: stream + ) + + let shouldRetry: Bool + let retryDelayOverride: Duration? + + switch response.accepted { + case .success: + // Request was accepted. This counts as success to the throttle and there's no need + // to retry. + self.transport.retryThrottle?.recordSuccess() + retryDelayOverride = nil + shouldRetry = false + + case .failure(let error): + // The request was rejected. Determine whether a retry should be carried out. The + // following conditions must be checked: + // + // - Whether the status code is retryable. + // - Whether more attempts are permitted by the config. + // - Whether the throttle permits another retry to be carried out. + // - Whether the server pushed back to either stop further retries or to override + // the delay before the next retry. + let code = Status.Code(error.code) + let isRetryableStatusCode = self.policy.retryableStatusCodes.contains(code) + + if isRetryableStatusCode { + // Counted as failure for throttling. + let throttled = self.transport.retryThrottle?.recordFailure() ?? false + + // Status code can be retried, Did the server send pushback? + switch error.metadata.retryPushback { + case .retryAfter(let delay): + // Pushback: only retry if our config permits it. + shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + retryDelayOverride = delay + case .stopRetrying: + // Server told us to stop trying. + shouldRetry = false + retryDelayOverride = nil + case .none: + // No pushback: only retry if our config permits it. + shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + retryDelayOverride = nil + break + } + } else { + // Not-retryable; this is considered a success. + self.transport.retryThrottle?.recordSuccess() + shouldRetry = false + retryDelayOverride = nil + } + } + + if shouldRetry { + // Cancel subscribers of the broadcast sequence. This is safe as we are the only + // subscriber and maximises the chances that 'isKnownSafeForNextSubscriber' will + // return true. + // + // Note: this must only be called if we should retry, otherwise we may cancel a + // subscriber for an accepted request. + retryStream.invalidateAllSubscriptions() + + // Only retry if we know it's safe for the next subscriber, that is, the first + // element is still in the buffer. It's safe to call this because there's only + // ever one attempt at a time and the existing subscribers have been invalidated. + if retryStream.isKnownSafeForNextSubscriber { + return .retry(retryDelayOverride) + } + } + + // Not retrying or not safe to retry. + let result = await Result { + // Check for cancellation; the RPC may have timed out in which case we should skip + // the response handler. + try Task.checkCancellation() + return try await responseHandler(response) + } + return .handledResponse(result) + } + } } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -317,11 +313,3 @@ enum _RetryExecutorTask { case retry(Duration?) case outboundFinished(Result) } - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -@usableFromInline -enum _RetryExecutorSubTask { - case streamProcessed - case handledResponse(Result) - case retry(Duration?) -} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index e404876ea..5811d20b1 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -104,9 +104,6 @@ enum ClientRPCExecutor { extension ClientRPCExecutor { /// Executes a request on a given stream processor. /// - /// - Warning: This method is "unsafe" because the `streamProcessor` must be running in a task - /// while this function is executing. - /// /// - Parameters: /// - request: The request to execute. /// - method: A description of the method to execute the request against. @@ -115,116 +112,58 @@ extension ClientRPCExecutor { /// - deserializer: A deserializer to convert bytes to output messages. /// - interceptors: An array of interceptors which the request and response pass through. The /// interceptors will be called in the order of the array. - /// - streamProcessor: A processor which executes the serialized request. /// - Returns: The deserialized response. - @inlinable - static func unsafeExecute( + @inlinable // would be private + static func _execute( + in group: inout TaskGroup, request: ClientRequest.Stream, method: MethodDescriptor, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, interceptors: [any ClientInterceptor], - streamProcessor: ClientStreamExecutor, - stream: RPCStream + stream: RPCStream ) async -> ClientResponse.Stream { let context = ClientInterceptorContext(descriptor: method) if interceptors.isEmpty { - return await Self._runRPC( + return await ClientStreamExecutor.execute( + in: &group, request: request, context: context, attempt: attempt, serializer: serializer, deserializer: deserializer, - streamProcessor: streamProcessor, stream: stream ) } else { return await Self._intercept( + in: &group, request: request, context: context, - interceptors: interceptors - ) { request, context in - return await Self._runRPC( + iterator: interceptors.makeIterator() + ) { group, request, context in + return await ClientStreamExecutor.execute( + in: &group, request: request, context: context, attempt: attempt, serializer: serializer, deserializer: deserializer, - streamProcessor: streamProcessor, stream: stream ) } } } - @inlinable - static func _runRPC( - request: ClientRequest.Stream, - context: ClientInterceptorContext, - attempt: Int, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - streamProcessor: ClientStreamExecutor, - stream: RPCStream - ) async -> ClientResponse.Stream { - // Let the server know this is a retry. - var metadata = request.metadata - if attempt > 1 { - metadata.previousRPCAttempts = attempt &- 1 - } - - var response = await streamProcessor.execute( - request: ClientRequest.Stream<[UInt8]>(metadata: metadata) { writer in - try await request.producer(.serializing(into: writer, with: serializer)) - }, - method: context.descriptor, - stream: stream - ) - - // Attach the number of previous attempts, it can be useful information for callers. - if attempt > 1 { - switch response.accepted { - case .success(var contents): - contents.metadata.previousRPCAttempts = attempt &- 1 - response.accepted = .success(contents) - - case .failure(var error): - error.metadata.previousRPCAttempts = attempt &- 1 - response.accepted = .failure(error) - } - } - - return response.map { bytes in - try deserializer.deserialize(bytes) - } - } - - @inlinable - static func _intercept( - request: ClientRequest.Stream, - context: ClientInterceptorContext, - interceptors: [any ClientInterceptor], - finally: @Sendable ( - _ request: ClientRequest.Stream, - _ context: ClientInterceptorContext - ) async -> ClientResponse.Stream - ) async -> ClientResponse.Stream { - return await self._intercept( - request: request, - context: context, - iterator: interceptors.makeIterator(), - finally: finally - ) - } - @inlinable static func _intercept( + in group: inout TaskGroup, request: ClientRequest.Stream, context: ClientInterceptorContext, iterator: Array.Iterator, - finally: @Sendable ( + finally: ( + _ group: inout TaskGroup, _ request: ClientRequest.Stream, _ context: ClientInterceptorContext ) async -> ClientResponse.Stream @@ -236,7 +175,13 @@ extension ClientRPCExecutor { let iter = iterator do { return try await interceptor.intercept(request: request, context: context) { - await self._intercept(request: $0, context: $1, iterator: iter, finally: finally) + await self._intercept( + in: &group, + request: $0, + context: $1, + iterator: iter, + finally: finally + ) } } catch let error as RPCError { return ClientResponse.Stream(error: error) @@ -246,7 +191,7 @@ extension ClientRPCExecutor { } case .none: - return await finally(request, context) + return await finally(&group, request, context) } } } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 13f3b1aa3..32172b6d3 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -16,59 +16,7 @@ @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline -internal struct ClientStreamExecutor { - /// The client transport to execute the stream on. - @usableFromInline - let _transport: Transport - - /// An `AsyncStream` and continuation to send and receive processing events on. - @usableFromInline - let _work: (stream: AsyncStream<_Event>, continuation: AsyncStream<_Event>.Continuation) - - @usableFromInline - let _watermarks: (low: Int, high: Int) - - @usableFromInline - enum _Event: Sendable { - /// Send the request on the outbound stream. - case request(ClientRequest.Stream<[UInt8]>, Transport.Outbound) - /// Receive the response from the inbound stream. - case response( - RPCWriter.Contents.BodyPart>.Closable, - UnsafeTransfer - ) - } - - @inlinable - init(transport: Transport, responseStreamWatermarks: (low: Int, high: Int) = (16, 32)) { - self._transport = transport - self._work = AsyncStream.makeStream() - self._watermarks = responseStreamWatermarks - } - - /// Run the stream executor. - /// - /// This is required to be running until the response returned from ``execute(request:method:)`` - /// has been processed. - @inlinable - func run() async { - await withTaskGroup(of: Void.self) { group in - for await event in self._work.stream { - switch event { - case .request(let request, let outboundStream): - group.addTask { - await self._processRequest(request, on: outboundStream) - } - - case .response(let writer, let iterator): - group.addTask { - await self._processResponse(writer: writer, iterator: iterator) - } - } - } - } - } - +internal enum ClientStreamExecutor { /// Execute a request on the stream executor. /// /// The ``run()`` method must be running at the same time as this method. @@ -78,36 +26,51 @@ internal struct ClientStreamExecutor { /// - method: A description of the method to call. /// - Returns: A streamed response. @inlinable - func execute( - request: ClientRequest.Stream<[UInt8]>, - method: MethodDescriptor, - stream: RPCStream - ) async -> ClientResponse.Stream<[UInt8]> { - // Each execution method can add work to process in the 'run' method. They must not add - // new work once they return. - defer { self._work.continuation.finish() } - - // Start processing the request. - self._work.continuation.yield(.request(request, stream.outbound)) + static func execute( + in group: inout TaskGroup, + request: ClientRequest.Stream, + context: ClientInterceptorContext, + attempt: Int, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + stream: RPCStream + ) async -> ClientResponse.Stream { + // Let the server know this is a retry. + var metadata = request.metadata + if attempt > 1 { + metadata.previousRPCAttempts = attempt &- 1 + } - let part = await self._waitForFirstResponsePart(on: stream.inbound) + group.addTask { + await Self._processRequest(on: stream.outbound, request: request, serializer: serializer) + } + let part = await Self._waitForFirstResponsePart(on: stream.inbound) // Wait for the first response to determine how to handle the response. switch part { - case .metadata(let metadata, let iterator): - // Expected happy case: the server is processing the request. + case .metadata(var metadata, let iterator): + // Attach the number of previous attempts, it can be useful information for callers. + if attempt > 1 { + metadata.previousRPCAttempts = attempt &- 1 + } + + let bodyParts = RawBodyPartToMessageSequence( + base: AsyncIteratorSequence(iterator.wrappedValue), + deserializer: deserializer + ) - // TODO: (optimisation) use a hint about whether the response is streamed. Use a specialised - // sequence to avoid allocations if it isn't - let responses = RPCAsyncSequence.makeBackpressuredStream( - of: ClientResponse.Stream<[UInt8]>.Contents.BodyPart.self, - watermarks: self._watermarks + // Expected happy case: the server is processing the request. + return ClientResponse.Stream( + metadata: metadata, + bodyParts: RPCAsyncSequence(wrapping: bodyParts) ) - self._work.continuation.yield(.response(responses.writer, iterator)) - return ClientResponse.Stream(metadata: metadata, bodyParts: responses.stream) + case .status(let status, var metadata): + // Attach the number of previous attempts, it can be useful information for callers. + if attempt > 1 { + metadata.previousRPCAttempts = attempt &- 1 + } - case .status(let status, let metadata): // Expected unhappy (but okay) case; the server rejected the request. return ClientResponse.Stream(status: status, metadata: metadata) @@ -117,14 +80,15 @@ internal struct ClientStreamExecutor { } } - @inlinable - func _processRequest>( - _ request: ClientRequest.Stream<[UInt8]>, - on stream: Stream + @inlinable // would be private + static func _processRequest( + on stream: some ClosableRPCWriterProtocol, + request: ClientRequest.Stream, + serializer: some MessageSerializer ) async { let result = await Result { try await stream.write(.metadata(request.metadata)) - try await request.producer(.map(into: stream) { .message($0) }) + try await request.producer(.map(into: stream) { .message(try serializer.serialize($0)) }) }.castError(to: RPCError.self) { other in RPCError(code: .unknown, message: "Write failed.", cause: other) } @@ -139,14 +103,14 @@ internal struct ClientStreamExecutor { @usableFromInline enum OnFirstResponsePart: Sendable { - case metadata(Metadata, UnsafeTransfer) + case metadata(Metadata, UnsafeTransfer) case status(Status, Metadata) case failed(RPCError) } - @inlinable - func _waitForFirstResponsePart( - on stream: Transport.Inbound + @inlinable // would be private + static func _waitForFirstResponsePart( + on stream: ClientTransport.Inbound ) async -> OnFirstResponsePart { var iterator = stream.makeAsyncIterator() let result = await Result { @@ -193,15 +157,56 @@ internal struct ClientStreamExecutor { } } - @inlinable - func _processResponse( - writer: RPCWriter.Contents.BodyPart>.Closable, - iterator: UnsafeTransfer - ) async { - var iterator = iterator.wrappedValue - let result = await Result { - while let next = try await iterator.next() { - switch next { + @usableFromInline + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + struct RawBodyPartToMessageSequence< + Base: AsyncSequence, + Message: Sendable, + Deserializer: MessageDeserializer, + Failure: Error + >: AsyncSequence { + @usableFromInline + typealias Element = AsyncIterator.Element + + @usableFromInline + let base: Base + @usableFromInline + let deserializer: Deserializer + + @inlinable + init(base: Base, deserializer: Deserializer) { + self.base = base + self.deserializer = deserializer + } + + @inlinable + func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(base: self.base.makeAsyncIterator(), deserializer: self.deserializer) + } + + @usableFromInline + struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline + typealias Element = ClientResponse.Stream.Contents.BodyPart + + @usableFromInline + var base: Base.AsyncIterator + @usableFromInline + let deserializer: Deserializer + + @inlinable + init(base: Base.AsyncIterator, deserializer: Deserializer) { + self.base = base + self.deserializer = deserializer + } + + @inlinable + mutating func next( + isolation actor: isolated (any Actor)? + ) async throws(any Error) -> ClientResponse.Stream.Contents.BodyPart? { + guard let part = try await self.base.next(isolation: `actor`) else { return nil } + + switch part { case .metadata(let metadata): let error = RPCError( code: .internalError, @@ -213,30 +218,22 @@ internal struct ClientStreamExecutor { throw error case .message(let bytes): - try await writer.write(.message(bytes)) + let message = try self.deserializer.deserialize(bytes) + return .message(message) case .status(let status, let metadata): if let error = RPCError(status: status, metadata: metadata) { throw error } else { - try await writer.write(.trailingMetadata(metadata)) + return .trailingMetadata(metadata) } } } - }.castError(to: RPCError.self) { error in - RPCError( - code: .unknown, - message: "Can't write to output stream, cancelling RPC.", - cause: error - ) - } - // Make sure the writer is finished. - switch result { - case .success: - writer.finish() - case .failure(let error): - writer.finish(throwing: error) + @inlinable + mutating func next() async throws -> ClientResponse.Stream.Contents.BodyPart? { + try await self.next(isolation: nil) + } } } } diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift index 620d9e137..3afe49425 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -23,10 +23,10 @@ struct MapRPCWriter>: RPCWriterPr @usableFromInline let base: Base @usableFromInline - let transform: @Sendable (Value) -> Mapped + let transform: @Sendable (Value) throws -> Mapped @inlinable - init(base: Base, transform: @escaping @Sendable (Value) -> Mapped) { + init(base: Base, transform: @escaping @Sendable (Value) throws -> Mapped) { self.base = base self.transform = transform } @@ -38,7 +38,7 @@ struct MapRPCWriter>: RPCWriterPr @inlinable func write(contentsOf elements: some Sequence) async throws { - let transformed = elements.lazy.map { self.transform($0) } + let transformed = try elements.lazy.map { try self.transform($0) } try await self.base.write(contentsOf: transformed) } } @@ -48,7 +48,7 @@ extension RPCWriter { @inlinable static func map( into writer: some RPCWriterProtocol, - transform: @Sendable @escaping (Element) -> Mapped + transform: @Sendable @escaping (Element) throws -> Mapped ) -> Self { let mapper = MapRPCWriter(base: writer, transform: transform) return RPCWriter(wrapping: mapper) diff --git a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift index 11ba12d28..22dbcd134 100644 --- a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift +++ b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift @@ -46,8 +46,10 @@ public struct ClientTracingInterceptor: ClientInterceptor { public func intercept( request: ClientRequest.Stream, context: ClientInterceptorContext, - next: @Sendable (ClientRequest.Stream, ClientInterceptorContext) async throws -> - ClientResponse.Stream + next: ( + ClientRequest.Stream, + ClientInterceptorContext + ) async throws -> ClientResponse.Stream ) async throws -> ClientResponse.Stream where Input: Sendable, Output: Sendable { var request = request let tracer = InstrumentationSystem.tracer diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index 6a4ed2a41..1b35a0704 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -52,7 +52,7 @@ struct RejectAllClientInterceptor: ClientInterceptor { func intercept( request: ClientRequest.Stream, context: ClientInterceptorContext, - next: @Sendable ( + next: ( ClientRequest.Stream, ClientInterceptorContext ) async throws -> ClientResponse.Stream @@ -77,7 +77,7 @@ struct RequestCountingClientInterceptor: ClientInterceptor { func intercept( request: ClientRequest.Stream, context: ClientInterceptorContext, - next: @Sendable ( + next: ( ClientRequest.Stream, ClientInterceptorContext ) async throws -> ClientResponse.Stream From 44fa45141192ded09a578c80eae0bf2cdbf42859 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 29 Jul 2024 14:58:33 +0100 Subject: [PATCH 414/580] Update test certs (#1998) Motivation: The self-signed test certs expired resulting in test failures. Modifications: - Update test certs with scripts/make-sample-certs.py - Update bundle.p12 Result: Tests pass --- .../GRPCSampleData/GRPCSwiftCertificate.swift | 378 +++++++++--------- Sources/GRPCSampleData/bundle.p12 | Bin 2405 -> 2405 bytes scripts/make-sample-certs.py | 32 +- 3 files changed, 219 insertions(+), 191 deletions(-) diff --git a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift index 6f1704006..1b626c06e 100644 --- a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift +++ b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift @@ -33,49 +33,49 @@ public struct SampleCertificate { public static let ca = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), commonName: "some-ca", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) public static let otherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), commonName: "some-other-ca", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) public static let server = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) public static let exampleServer = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), commonName: "example.com", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) public static let serverSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) public static let client = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) public static let clientSignedByOtherCA = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) public static let exampleServerWithExplicitCurve = SampleCertificate( certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_751_648_938) + notAfter: Date(timeIntervalSince1970: 1_753_797_065) ) } @@ -106,258 +106,258 @@ public struct SamplePrivateKey { private let caCert = """ -----BEGIN CERTIFICATE----- - MIICoDCCAYgCCQDhjLeDGLctlTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz - b21lLWNhMB4XDTI0MDcwNDE3MDg1OFoXDTI1MDcwNDE3MDg1OFowEjEQMA4GA1UE - AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzrSLrp - IcroapZ1CB2jz5N1O+S22oLpKyYE2KT+lXN1Bp44ni4bkklrXSNwyqwldqh9gk5m - HRvXA00nNkXD4dx0wjDJqxgs3AME58EIWo3MKrCNUS4cnD6qeQNf9ZZkxE8dWq1U - ZKhAVMuSWDMRYNZvSsiNjsMSRwIaPrpyDuUhAlG49HCmYLkBEzMckAhq1T1eiPwi - zae9d+CO7P34CSm3hYmjV7eiiwRhmPWpJwt53SrZvjjwzVpzZjcP+RDef4v+PFpQ - mvEfIl4H+T+IHacgkIJdvaxRn9uktf12naDHk0UvQI67JLKleU+QshcSScWb8FA6 - 7mcD8cdfu1y2+vcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKdizzBFh65tDUIwz - raukYSPIRm3erLD+Yky/bnenZrhofo4yLyoGu3UCwAvSjcXkEH2wPdRrJGm1nrQG - yKauWmjFjD3AA3qidrPBJXg8REWpjQjXvrjPztI6N4OVaXsDFBXczr/7E1Ot/fu3 - rGGjv2lD4fFdEUb7vvmywwWdVG2eK33xuoGpUWNtHg781QiAW5XQlMTR8Nghdc/B - yX3BlsR0ube//l3BKmWfSQbRM8vRQwO19VmZyaxjFwiQviW5ds3b7KReqxJn5UbU - brcfWGL+eg0/lOWTVQkoIwHBsmAZnIE98AeC8OGuGKMRxqkYqZ9Fsg8DVUMdqQFe - Dbu9pQ== + MIICoDCCAYgCCQCu3t2RYSXASjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz + b21lLWNhMB4XDTI0MDcyOTEzNTEwNVoXDTI1MDcyOTEzNTEwNVowEjEQMA4GA1UE + AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOtVwFmJ + Znuf0gC8tZSVasYrSbiDiYGUJd701SskU+RbzNZl7paYIBcM2iAy4L6S2w02ehfa + RZoatGoKKhTZnyMu9NAYM1xAGiODfqC0s467udVBU6J2rU8olhm1ChZqfVBxcd9y + AF7VjvN1N3gnGM2klAWFIgqaHoFAqINwHROjycAnr40uXCLNLukkt90AmMtL5Rah + Sh0wOrx0E5OiiqWyWkjePTcMTwiRaYrUepo+EGFdmERDyiJtp5t4pcqdInJ6uA4s + eiev9NEiGdWeJy83lIdo3N777r8cK9VDsHxHGiz72ZKE35MeIEk9weC1ph81KIZV + cUDuO8nRPwWvBDUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAT6hoeq4sJdqkaN+p + QvpF9cZ4DJLw0dFujcWQtYpPCtMVQx14QSXaPGmUG0GLVJ5mUvzV0cwUC58JDXmS + CDQ/vBnfoWQyblFQDZXOP5aDGOTmNIpFn8hutqsSDvMteh8R3zvJZBr+CQtP2Bos + TH3TcnchhKq580hYazFJJ1P4jOqBXIQb3Osnm8WjJpGuDtOP8DW2Q2AdN/8Zl+FQ + OrwiGMwghkZm2O91tYKvr45VxvyIpah36d5IFyAP7xIT4ua7X7ZyaCMjBmlK1QHd + kKUVuyR2bLgpIRpj/KQY/UOdl1zu3MUs9OkG0suPrY3EOa0K7hDkXnHjX2ZipSw7 + TAuG9Q== -----END CERTIFICATE----- """ private let otherCACert = """ -----BEGIN CERTIFICATE----- - MIICrDCCAZQCCQD8FZzejuvygjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z - b21lLW90aGVyLWNhMB4XDTI0MDcwNDE3MDg1OFoXDTI1MDcwNDE3MDg1OFowGDEW + MIICrDCCAZQCCQDjS9iNRZ49lzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z + b21lLW90aGVyLWNhMB4XDTI0MDcyOTEzNTEwNVoXDTI1MDcyOTEzNTEwNVowGDEW MBQGA1UEAwwNc29tZS1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC - AQoCggEBAPtxP9YCOZggO5iivxe4CEHk3DdfYXKul+jOdW/dU4P4pwKU/YutQu+F - 7tPnGYaP7eO3if8PtbILio0lubk+uSbTZ5hRteL/3yj9UN6jL4vaVOkSunpbP+/d - HXdB8Aa0hzZlNPbG9+7TADGryxqt1KzgVo+KBAEY0p2vRc3E6HtLBnSkzlw3wi1X - zmuy8WCnayTmdqt95djsJE8PNX+GtTNfaNtZ1M5qx4FiPqqJqFhgCKJHy4LRrO3u - K5IfGVy8zfXkFSXfqvl9NKz71xecBIRMQJCATEpG6GXSyb+vmnOuAKZ+fVVLw7Kf - oPYwG3sh2GRDfa2pfAoP0vjXZri8AWkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA - 525Wy4Cyx2Pd91RJufYWRgkAwI8KLe4KV+SoZiLNJzfBRemqUq0P36l2lSotq4bp - I04d5HXkl8eQ7dje/RFWXMHfNQXRq06+KUfh1XA17GQ+VOEHFc0PYKW78ydXXZDk - PS2+y6Ru/MD989Aoecr2JvD7sSEmSvprtPxuNYHifZKbaw2c8HbR0Z3WawxoIFRV - zeb4aGUncWj7IKNRmL4f7CDA3Old0fwIRKYcxv5awTHK01PE4Yxo89M2RqRFiJQy - xbmAl1y7D1nLlfzHjKXRP6wpBylPcSssTuOXfi+U3Mv87iNDvzKTVYUD3c6wT/QQ - bIRjD4xVam65aa8pewlzRA== + AQoCggEBAMC3EEHu7uYtDsH0RZEQHQol1oDEOxL+SDH7dbKIv0UpgW5LXLQDDGR9 + FlKQbeNNtMt5mTd4TalqxZz+eMUhfrv0k3f1heEky/Wz53uRFHVNbDLEf/wa6QMd + 99HOBePy2yDWdQC5/R6zLwjM3LuzZ146QMk0b4tx+/hjSkUKUO6GVQEJrO8DTdij + XAco/3jCeM8wofQZQ6ipZ00gxI3BpubPgj60yRW7+aulHPlZmZuv3kDDmVcL+V3c + V0n0GVckV62xMWMnYGNXqAajkK97f+mlo+zZ2exkGV/2Kja2VT+wZKEkO9RfL6XC + 23hG9pjx5OmD1lihlwYve7VFSo56xvUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA + TubObtDxGjUya7GPqrfC9gP2aQ/mBjprZGzzga0ksWQC4jIhq3qOCYVROBNHeqjH + mH3aleRrq9/QE6/fP7D6YruX6WEJ0hzFxf8eoVGYqETiNndlo9485bNVTB3afL2m + +qLKsvOoSvfO4iYrgvteFKycGICSR63EfN2AJFVNfMPATk7DILJo8gnMx/keKcgG + WWQaKHEeN2ufZRTDXz2/YNWx5K/w/L+/MDqZ9tZvWTiD/q+rQ9q7hbbbpCxrNgZF + 3PnNPtu9cTvaDl9p0liudFUc7FoI1PtEzT5hTMxYWoyNoFn9hUaVNreJKvS78nsx + F4VLaY8K8w3ruk8p0Igclg== -----END CERTIFICATE----- """ private let serverCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMMCWxvY2Fs - aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANTYUmWVpEP9ym/T - dYpDxr1z0bMZ7HiFMDT3EW0XGrEs2Rta6QbPxP3lSIW9G1OdVJMugg8EB4yoPrRc - x6ttS64X48DyfvEH4VbdX+gHZUeTIloN9GispPiIiY5Qodq9/JeSjAG744lnKWKm - 48w4A5bWb4zlZ8s7lNTiDphll686+oIuhhxAYI4nuKKsPhLatvclq/O6a1BypQv7 - 7psyo1gxcRs7gRLIGchcByI75ofkAcpT0/p67rhCECOFwvJt+8uuMoVnAPxcAgBO - jgPhbxS6DcOSzoliQYtOBv2YIKllplgOE+0eYxAvxBLfGY/wh3SeEhVKF8G8ltT6 - Anp2MPcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEACG9shmrPmQvt+3dKpEuzR2BA - GLrgON4zgc8byFIORHatPiM108LoEp2a8O+B3rFJqWbBBrowDBRR/5th7kLvyDsV - J33Rb2mXaJv8K8hNkbSs5Ow5go9M5LveMcgRqiQTuYLuvgman3LvlGveEc5N2yar - NCVmZfZ0ofTul+QqxaYBw2GlaXmzsyvpbZfowskYdGm/PipThWsuVy/e7BLHZsr7 - FF8f7qfbDsuVd0UpWPSlIHYUJLP4Bhj87YWnOQgXSEgq2cxybZHmahiG7YmZzg++ - Oz3B1+jGtb6nKk961X5LyGsitGjEULovA8tHQEJXWvKpYQTBXOMUCKWslrje3Q== + Fw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMMCWxvY2Fs + aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMJH2M/mJGXZneOE + 5UWbicTg1BxkdNND50p0fO/35CG4jDQ3CekXUuQ6kK6ZJ2idDQTOWJqd/jSB7Ctc + zmZ9KBAfhP9PHMZQaVQSo+tpvX6vC/hw3PCOEne1l8H8O957hBdOhEDg1crAZ33M + cTOtxTSNw7hh0OXzLyOTfq6h3nHyvjuj82fn8nyJ9lARDZ8grdLS5LVE+Je1G3My + kXJKJoYCGQHGDKmj7o1nrwiii20uE0gnjwGEiTO1ngKQGXzL6guuR1bMmE1UIPD7 + IySu8Yg2nI8YB96dVNFaiB7gJg9Nde7a7GHPh+4t0NSqLlBL+k94c2J8lWgN38bZ + ugoknf0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAXmSnx5fjn0Z9GLQYkaXxKUoc + rYPkmzRCocso3GNMWz3kde351UmPpX3tf11638aIKO0xzJ6PZyYowdbCXZs4Co/o + pYyeW2LOoxLwSBF8wFMAPN3FB54c/KfancXGV1ULTlhfpnoZvUPnqJDYoxFRUkIQ + wVtlyA/p5Zfc9U8czer42eo5aj9D9ircBt4k6hx9IY99YvyNeFfMq4TLOgJZkZT7 + 2AImVq4kBvIUVrK86MGyRuNbAWP4fY5OOymT0rEKA6U5Lx+c9PPaFgozbGk4QAMB + ZTwv8ymHAKdcgiDRAoQ2NhkSlySnKi4oEwcKLYPuyrpt1eG2Lx993gdSa4z2eQ== -----END CERTIFICATE----- """ private let serverSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl - ci1jYTAeFw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMM - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANTYUmWV - pEP9ym/TdYpDxr1z0bMZ7HiFMDT3EW0XGrEs2Rta6QbPxP3lSIW9G1OdVJMugg8E - B4yoPrRcx6ttS64X48DyfvEH4VbdX+gHZUeTIloN9GispPiIiY5Qodq9/JeSjAG7 - 44lnKWKm48w4A5bWb4zlZ8s7lNTiDphll686+oIuhhxAYI4nuKKsPhLatvclq/O6 - a1BypQv77psyo1gxcRs7gRLIGchcByI75ofkAcpT0/p67rhCECOFwvJt+8uuMoVn - APxcAgBOjgPhbxS6DcOSzoliQYtOBv2YIKllplgOE+0eYxAvxBLfGY/wh3SeEhVK - F8G8ltT6Anp2MPcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA1cUeJkmPC+qjNzTY - JJ6LUofrVfIXZbiZf2NugNx/iLAabrLSd4pZjIXQszWbd5t+wftyUBqUd1Z/GuPj - XHmfES4ZUm1Fyu75KZjNzzXsXrlZ09IHIWvxAcA3FtqbDElw7naExbzcpup6s+45 - MgAMcFnMIoMj4Cdt9Ky0kzWKdl7tmNxIF+PNQby6JCVb7HPUgs3hqSalwDo9ddYF - 8lvJ5Q4ZzlKLL1zUKkpD7I4M6hvNLgHdnLa9nwtT40u0bbGr9z25W+YouJy5N4pD - YsOhm4xYF9VBZIEsr/ZSrI2RtABu7I5NVNWhF7JAIYspur1Rghl5bRkSaQ1jf/BK - Epog6w== + ci1jYTAeFw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMM + CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMJH2M/m + JGXZneOE5UWbicTg1BxkdNND50p0fO/35CG4jDQ3CekXUuQ6kK6ZJ2idDQTOWJqd + /jSB7CtczmZ9KBAfhP9PHMZQaVQSo+tpvX6vC/hw3PCOEne1l8H8O957hBdOhEDg + 1crAZ33McTOtxTSNw7hh0OXzLyOTfq6h3nHyvjuj82fn8nyJ9lARDZ8grdLS5LVE + +Je1G3MykXJKJoYCGQHGDKmj7o1nrwiii20uE0gnjwGEiTO1ngKQGXzL6guuR1bM + mE1UIPD7IySu8Yg2nI8YB96dVNFaiB7gJg9Nde7a7GHPh+4t0NSqLlBL+k94c2J8 + lWgN38bZugoknf0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOzQ4ZiHOY9mZyE5e + aQPZn7FE93yZrnvZcuRwrv2WI5vQj70wU4oKdm6RuBbntercKgrP6xIf2mNrUSQk + A0XfB70QZYHKD/Uoy/NXn2CwwExXixQNUv8OaytiR2PGDk2hdeqmcTEo18/v2sT0 + 32PpizVqRTfxARtu7gWt2P+n/RaL9Dj8JqB6vxv4rL2HkrDys3lT5UZwH4W81Lfw + hFI7gHRt9CjzpDIP/GFszdvTHLgozMXGKu+1UKWLepn1XEaKyQlS+CNMVGdI8qHn + 2KvU3L4zzB1MgJsTEmz+rdGtc7paBSHpLqp1DbrU+RjXCG+POBsWpRcHGkM8Q82X + e2/YQg== -----END CERTIFICATE----- """ private let serverKey = """ -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEA1NhSZZWkQ/3Kb9N1ikPGvXPRsxnseIUwNPcRbRcasSzZG1rp - Bs/E/eVIhb0bU51Uky6CDwQHjKg+tFzHq21LrhfjwPJ+8QfhVt1f6AdlR5MiWg30 - aKyk+IiJjlCh2r38l5KMAbvjiWcpYqbjzDgDltZvjOVnyzuU1OIOmGWXrzr6gi6G - HEBgjie4oqw+Etq29yWr87prUHKlC/vumzKjWDFxGzuBEsgZyFwHIjvmh+QBylPT - +nruuEIQI4XC8m37y64yhWcA/FwCAE6OA+FvFLoNw5LOiWJBi04G/ZggqWWmWA4T - 7R5jEC/EEt8Zj/CHdJ4SFUoXwbyW1PoCenYw9wIDAQABAoIBABbY/cdPz+lIhgGJ - BnYIHn5Zv2nlX3/0dB9LYkB+mWvpb4jDMn57sR68DRPmH9fS7LA77tQjz5emu8xq - pTherCANCnK81SmUefj0HIZwvMt5HNfj5ZeS6MaRCYsQVr9/Y2z12zeYbq1iOIwR - dCSI4sG/VQwf2At14t0TQxPS2/yAOwctWo2g4KVcHWUXTw+qKoNANX67h6tznLRA - QH8fG3T19+wBtuSVq/VBiJTHwAjdHXyHz+eRr7eSxANIewrgmrfbJRpXwevnzVsz - 9WZedkcTKBn7yj6ZaoRMwuwfAspJ0S6p320ehPbdo96Wh25BXNs3VJ2/GcUfkAKe - UGSymfkCgYEA+b68gdqr/IclrDPtFhGGJAYnKMZ6uQXkAij+x8w8X1Q/avkVLxFB - jBAEd7DLD/brCY6BMmq/mzmWJiSpdaF501rwne2DVcsq5U1N3rIV6AWxpp368Zfa - w2tut+bmq4ZEQtK4kJP2WXxE8HtQNSZ42qbGF0Rw0RoZ1pTr9oSkwCsCgYEA2iz/ - U6iedewf+Dqcggii6gGZEVpC/kXs6m7xxVgE6Nt2/tHskun2B/3Kqoq4KxOkR1In - saG5GJIfUfVhnwiydAp9t+jlu7mplpFCF1hZpC/pwa3EP/tB8roLXlcRkL7257TR - /4u9YHY85PpXUycsYZDibfxYLwenzePCZH7TIGUCgYEA14FnWQZA8qAMOhR0uV5V - yjAlCmJ6873JirOlZvMuBXTFZKGbTgot7ZbExCOilhwTpSN7CO5keKWwkyl/sSmt - 3lvS1fRmKFowob2bPFef359KNOSN7nuDIq5J1BdDZS9vJ9p9uQR0x7McKge+pp6U - GtlehiVg1I8ZTLklBIxhPhECgYEAuKs9suIWvlmO9d0mfCozOz7/AOEVs4QcdJJT - smY+QZsBrc6iH/hId5sp4BBqsot9kaDIWGI6+cE1IXpBlwsVgYMfxnsreSo9kWSC - PKBbv82OXpFme4GA4KL43HF2PL5m3tj+pv7w3KU4Bdif8ZJGzo6EGfRt7+Da+DrA - X6+5pMECgYAkMyh/ypYwsTqkYalgRk1lPwe/EAP28lWFsyckIzBuP+CwOMg5q5o3 - mg1GEE3hWCY9P1cLTedewHFBYpfiiR4hDKSyivZElV32C5Kg1uHxGc86FFGyAl3v - dEAYBVnuHnW4u1JpBh5a0rRa4U2UYeq9l9rZRJzsjV7FxkTIRgdNQw== + MIIEpAIBAAKCAQEAwkfYz+YkZdmd44TlRZuJxODUHGR000PnSnR87/fkIbiMNDcJ + 6RdS5DqQrpknaJ0NBM5Ymp3+NIHsK1zOZn0oEB+E/08cxlBpVBKj62m9fq8L+HDc + 8I4Sd7WXwfw73nuEF06EQODVysBnfcxxM63FNI3DuGHQ5fMvI5N+rqHecfK+O6Pz + Z+fyfIn2UBENnyCt0tLktUT4l7UbczKRckomhgIZAcYMqaPujWevCKKLbS4TSCeP + AYSJM7WeApAZfMvqC65HVsyYTVQg8PsjJK7xiDacjxgH3p1U0VqIHuAmD0117trs + Yc+H7i3Q1KouUEv6T3hzYnyVaA3fxtm6CiSd/QIDAQABAoIBAA7RuikJjgcy1UdQ + kMiBd73LxIIx63Nd/5t/TTRkvUMRN6iX9iqQe+Mq0HRw/D+Pkzmln76ThJtuuZwJ + JTlOHKs2LEfpOfGqmo4uKdDALRMnuQsHWOMEg0YcVOoYGlz7IPVCKPZl8AjaKkq/ + OHdPrvY2RhKfa3bO2O6mxof9kuEwF90l+CjxAcKd4GGMFE+tUjfCxveA02eDHAgm + dwgUGDKFLzgiOgKeBjh9kdLP181o3b5jHVqaw5ZkekYSS7KdLZr9dl1qbJ7xFhbj + Jnls98aQ3Kn4zF+LJex44Zf5R/9Gfxul9QtGIyNJtsGhsmF9j+9POqRGyFfyiu9x + guJ7sqECgYEA6+IwRW7wfjXzTSukhKzb385g8P+UiIghNHW8OSiVBR2mOhbRtvZd + +qi35WXK5mr4cK2jrrU0v5Ddvs10xlMyPUkxIOrwsBw/OdPKzRfg+uaei8ldI+ue + tYjnL2hoDVZxMUX0cX7Kju6MUWkf6R3J75av51AVVcvWtSSRu4hVqIUCgYEA0tli + M3txGAOfxrhYxmk/vYYB3eE6gVpEZWo1F/3BnJaH7MeLmjpC/aXp5Srs0GwG31Nx + TNO0nFu1ech17XatlZqk0eEkKau+w/wyd+v0xTy6d49SMvL3yY0H9I2O/TGWwZr3 + wO45pZtEML5S6VEIPf1lj20GEiY7oLm2cBd3VRkCgYEAoPCr9MPTzJkszstnLarv + Pg2GsQgApQMUfMGT0f/xZRMstleZcNc5meuBxT+lp3720ZJ3qp0yRz4lPaja8vIS + xiPpJEeIPvCW5vKtXS/crfOp20Bhjz+VAtFMw1jeHbOL+Y18Ue+rbsgt7uHmBtzv + ScwraoyGcgppDSDNWgGUSC0CgYAkpdISvq7ujJq10I7llZ+Vkng6l44ys3zV37rw + u5NuYx+nARv7p4rDSZY41dgpdc1P/dHgl5952drWGwicSJdtPF7PeAFwGMDkka43 + 99QogCCs7UVNQ7vb1V5/nCcxTPA2IHhVmVJ9vVoB2uLQWNxE4glH/5whhXGxwvW5 + z+pW6QKBgQDn1kRJ+Y98UpDWKdG/7NLsSkHL+Nkf8GXu66fl435Pys5U44oTDNcu + jMDtymBg0IE3lng1WbNILV7O9r9OKt1HH4L7eepzKJLP93PbpLneBlHQG9LvJmsD + 3ErhTxSu80oglR1Hy2UjL70cE1nPUUpUr8yciey0G1tbnxvsWIqGtQ== -----END RSA PRIVATE KEY----- """ private let exampleServerCert = """ -----BEGIN CERTIFICATE----- MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBYxFDASBgNVBAMMC2V4YW1w - bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2BfL1dswYSgf - q+ZSp3Uy4bSlCayFuBeKc34W/XRl7+YJd8f/EJulI4gwYPjcBWYyFyHLzEyAjhJ8 - HMFW4a1PFNJ6gc3Djo60/qfhJ2/uAX8E/Gi7pVezKMsMwx6iBjG0SiN1zcQzGnns - k12TRZWfsIl/RR5F1tZWMLggHXasT9DBZht4Ya9jx7nte4T43vFfWlzJHu7L3L2i - 8tBTv3d53msjImMF6pUIqpDQ9doo+jGI4ApqbfeKUfy+/OxrKuhVMXzYh9dhXnDc - nJnhgxa51MumiJ9apqeUBT+rW+1zhYpIUE5Su0TgYXjNGecb+OwMoy8WYRJIusD6 - QVebKRhAqwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQC7s83kfzkbh2GdbJYGCG/v - nWz/aZnLm542wyi0aXZNodNF/mTO4jSm8RKeAwLX+f1hleI32CG/d9/zSrvwOZiW - ar2fccfVqA1UuRXT8XyZB/WUxKPxuT1WtMVbS/nLS51XL2PJRn50JRAwnw7vH/MJ - Lb7IqY8MmIYZrnEBMfAqkPvSvN9eXZYrNuLQaeqV/I97XKz8FJ4IbR9Mz51STcHJ - 65lwzi/Tfdxq5awVfGx8bp+uj0aow6NPkpVqYS78LrCYVa4RYau9vHNVoHOzOumi - dXRxp2NlVy4kJwd7RX4p5qE4OSNBpRgQBCtXYXWBV8eHIQo8mfDGcAMPfMFQ35TY + Fw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBYxFDASBgNVBAMMC2V4YW1w + bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsqJThZkRGF4y + yfnnYQBuV+UCrwfiXoNvkxtEWufNah1mIWt7biM+s181Dfn52Lj8GUsNiMEZ6qrX + xBzNwo55tmsoxqywxUS4G2FA4nrniAs6UD7hywKt1zosBrneAPclLBblFwJQsQhC + DEgpsl/DDt5oHPRb5x1zB8DuB2zQhpvEu/pCX5OUlCLf0X1YxUCDU2yYGABokWSg + adHgZ+kAB+Cbt/zH+zibdUS1IpVtz90BuoftS6Iwed5XxPCe9FCc/P1vkPd9KiZT + OhREB3Ci8XfqPKSv9BRGbdg2C9tkmkgVTKcjhfkULsBdahrCLna8nOtoUXf1LJCC + IMDjjDfUiQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBj9JfAiC1qFkC7+kearHOB + RDGiAFyxT3cQuSOgQPoU0WoBaQ+/YhFp8zxJHlQEcQTmicODItJA1kGj8iGT18uE + Tno1lg7nkkMhoY/Q59yaMKLdfe6aETN2eqh8GJZdUwhOKO3dQBqUQuj25gxVR+1a + 1bcsv3ds8sNXdUNJM12iXzt5lAgwhLWX0SbxuApB+6rcQBKiqAoo3KY9N5tiEbRy + 1VeMkAl/C926+W2nOAQCxSryZWEUX5EL0VARfxBjrH6KzDk876HtrLuDb2LHFNJU + w+3nE69pEtXzMEYAQgv4yMQJZx6CtCjwS+Oxr5A3AJPk3nUSzOXVYTe5rk3hmC5I -----END CERTIFICATE----- """ private let exampleServerKey = """ -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEA2BfL1dswYSgfq+ZSp3Uy4bSlCayFuBeKc34W/XRl7+YJd8f/ - EJulI4gwYPjcBWYyFyHLzEyAjhJ8HMFW4a1PFNJ6gc3Djo60/qfhJ2/uAX8E/Gi7 - pVezKMsMwx6iBjG0SiN1zcQzGnnsk12TRZWfsIl/RR5F1tZWMLggHXasT9DBZht4 - Ya9jx7nte4T43vFfWlzJHu7L3L2i8tBTv3d53msjImMF6pUIqpDQ9doo+jGI4Apq - bfeKUfy+/OxrKuhVMXzYh9dhXnDcnJnhgxa51MumiJ9apqeUBT+rW+1zhYpIUE5S - u0TgYXjNGecb+OwMoy8WYRJIusD6QVebKRhAqwIDAQABAoIBAQCPtvPHnOkGFKtL - pfieimF2nq+MSYL9NhrMSLV9hyYscG8njIlkQD+J7A9QzvF1Xcw+eimSC+cLlduZ - PDRODvcjQABdx70hWGOjYX9qvRQrRpDIVddGVZc/sBsiwYK8X94p2H+Gg9AA8cmX - EIrbonD79dYA3+tOwGm+KRaiwcRDp7b9q3pVeTJGEn4+njXPzA1hADuF/i/CtFJj - d9PMamPRVDBj+ixQ7NpUwpTldIGFf4b1l54LIvqtZoRX470XuTc8Tw7BYXjEl4jv - 3mkhTwUIJ8W9VQCAvH4sz6Z/hM1rlgdk1a2o5aoQ7ohJvpb3jwGMvLNFVTSls78Y - 3rcpGKjxAoGBAPwVCQgE9LE5AQEaNWkbLgicyz0/qX6pVK7f9CDcrUxMcVo01Zjo - cXr79Q9xw4Wl9gvzKq/YAYtY/SxyTL0A0ip9OwBHAyeGLvxEiodOCQ4qxKO4xAcy - Y4GjveQwKANPttlJCPHgny3HqC8swYLf0dAGoYtBM53A9lPbvOKPFkDlAoGBANtz - keSFim5V+vlpovxITdPowJzzaBXnKRFBEjcdBJsNio4iPuVoVHjMYaVkyVZ1FQ+s - MTIYH0mTLlabQcIXHRF/gw6miEIILtBcJtcdE73gbyvZXgveweMsmZfP+TCKMgCO - OYjZT+SIiyB6zCctA3z/bM4I2taoOAhHtasU/7JPAoGAXFBXvlgSQ9RcScsPRC5v - 7Td+Ni/aIkhgeqoI/P/Tdt2HpUEz94soA6HBXKaMs6TTNg0W1M6FwkIUdPJmp9Bl - Jqo1sSRQQ2kgS8HN+T7akhWXbV18bCZHynHsWGRKQuwuSeQ1Il7f7CPxs1TwiLzu - WQAUqKp3/I1tp8gQo+dCfwECgYEApjXJKQDv0RO0C9WziUqmD7r4r6c3jWdQVm4n - grCqvVkrOO29H3m+iOObjW5hg+cXtZAgjqVwhQRBk3zx+DQTYx5lv+Hnz8Ns2YkC - Lekq+6QR728p6Omlhg9QoYf2X4o7xunxr7GP7jJw1X/MQlu4iaLX4NEaFnzAO508 - fkBgTccCgYAV/S8vuIjaku8H86uPu1F3rMoUI3En5vCKHsEbef72FPIJNLqeoTX7 - 1jKtxNBpoTnzv6CgGq7NRzFBdYwvcxbrMxVs/k76zgMp+EUY+SsPbQRSllum72pA - XrfC2tdOnAx7NO4dAYLpmqfTAzKGDFcf4MiRwRiYwzQ8OIw+bmH8Tg== + MIIEpAIBAAKCAQEAsqJThZkRGF4yyfnnYQBuV+UCrwfiXoNvkxtEWufNah1mIWt7 + biM+s181Dfn52Lj8GUsNiMEZ6qrXxBzNwo55tmsoxqywxUS4G2FA4nrniAs6UD7h + ywKt1zosBrneAPclLBblFwJQsQhCDEgpsl/DDt5oHPRb5x1zB8DuB2zQhpvEu/pC + X5OUlCLf0X1YxUCDU2yYGABokWSgadHgZ+kAB+Cbt/zH+zibdUS1IpVtz90Buoft + S6Iwed5XxPCe9FCc/P1vkPd9KiZTOhREB3Ci8XfqPKSv9BRGbdg2C9tkmkgVTKcj + hfkULsBdahrCLna8nOtoUXf1LJCCIMDjjDfUiQIDAQABAoIBAF65NRDi2e3SBZyU + p90IHXr+NS4bQC5eBAw9qUGLKaHbdQzDse/1QIpdMgT3SUVi0kuXQNYDj3qgnUmg + /HrukhvpNvYjHJl+lyHtsDpocd3yFjn3HkRIZ2Z5sl7esJpSc6OtgE1zLNazSlK4 + 8WNk5Eo+JXc1HIaxVw4FgDLvwKOfkjgzr4W0bvHR/FaJ6ChMfsRaZjrIoDHvIuY/ + mV/1jI0t6hOf3VU3NTg/8gwu35vcVNqe24qV6dVk9dJikyE7P+/e2c5VCwqAcrGL + V/Gnf7iaqHcUxDFihWWMFBP+yVAeQ26rrAzLxWSn+qb1fJ8igIJhTfdHJFjQbuoP + UsoFAAECgYEA6i0RowjUjOakJD/USKHOD0Dy7ql8DehMvAurbuxtN0jXPVaR4ebt + 3jjyQkIrllRtAcZnZH2OlzM5mKQvUdhriMPvgZj4fpepYBbNcuXIDYXgfe5j0Na3 + XtVRjBvm2gwC4OY9G3HFubNVjjR0AxZaIfUeqzl+XsX8t+tms8WvbQECgYEAw0gl + nnHTYtuw1p1mmPZFYJ5P3DFaqtkRnBPgq7XVgRCjmU0SYEQ6ogNbGESXQBDrKYIg + IqpaZvuSv6nEy+b8aEuvkTsRqmu+gK1taATnZRhrzjzUeMOAVOn1gK86GcSq5Rmx + Bj+ie5lBj+yxU+wJg0hRGNik/ltYVGKf/DNDf4kCgYEAiz5bQ19Hy7SFG4zctIeJ + 2GYdTa53tmlP32zs9hsdYgcs/SsRuYqwHDguTRm9gzkWTDzmU8mY1O0/rTTLclZG + st8W9i+4asXRj/JfHZfmWawmbZsnvRE/neMoBzC8FyGXQJWG9l+zW5V4JQOpjABp + fdGb9+JK8x21BMOzoOfGRQECgYEAuyvImtAguu001s9wygWpw4yZoMRRUdXSkhVf + T1VueVFYbRQ5G7nptOWgh2cezVIqA9PsNy2ujmxsYHY44PLZVKHOelXyfbTdl/oi + FgQ1QWmh0r/tKn6/3yOLorbQ6mfdIM96JDIT64GeHHPSF0zyZTmIOVdU9VLaG6+Y + BiOge3kCgYAYe0+Dseqoy4KXICkHcdacbULJnm8ZZ0SpjoBhKWSS3gyC7Anx8UoO + lSz/4owNrD/96NnlnxItq0Pi7ZU30TBdP1ZX7RuwQqS8ORO9xOSVrgzZR/PZCa3V + ziqGo+jUjGowA795F7/hgb3fNML5dUpLe+JEEo/OuQH6Jh8puYlYBQ== -----END RSA PRIVATE KEY----- """ private let clientCert = """ -----BEGIN CERTIFICATE----- MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMMCWxvY2Fs - aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMT3FOJyfZpu6Ypq - du3bMbdgWiVprdNMODJsRURJzKj7znKC1tLcD7gvv/536DKzpAl3VF/pbdd/yDU6 - JuXfel+2qxx3cvM+QLqBFEggUt9vd0rCs1DvZ9rT7A755hM2146gdE55g2JvXsJG - C6gBskr40qkt5JrKSqOys5VE1TeifBt4lwCQphAB6qNeZM+0MEz6PSeeSFjVFZlo - IdyF3+swCsioTvzbfXs9p14HthJzL3edoorbtJ8upZdUlwJNZ2AX7K34sL1SDM+b - Ox+eoGLJ1OhPW+CHvjFMzpM3Q1dyTqR1SosBEiuYVpk/SAS/pBSNMp3yKqF1T3ch - 4oJOb7UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAejNVcNvE3sNgMC7a1nm1gPKt - mnzVpuk/7Wls4ldH2yB6QupoWBimNAWyCsvc+l1qsbPJf18JbVK8FviRTs6Fka1g - +MFMqMs/SlcAgmSlkfHdpu3zZqUEgQxF8pNJJ/Dr2ypkxEMkZnS/g4KiPdQGnTAU - uHTtTm3DiNpyCCJFoDq3xMb+qTj8UTlZI45HukmqPKINobW6IHuislYQvhZnRXM9 - pwu4L379lI848bncYCVhlQJMP4bQTWhaUQgWpqIrxHatLAplWorTrCUzS1qT3uJN - B/zptXK9+LLU8Nc+pqR2kTBhMN5a1nN6MzPSi6UWwX+evr/NRigkOReHDqx9GQ== + Fw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMMCWxvY2Fs + aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqEdoBgLtT1p+jn + xjEXCQCpS6g5EIyHwjpIxC6gX49wACiFqNz67EmkDTX0HIPgk+/4wI5ljP7mYPzh + NAMFU4P8gDpYhKXLQyaNno1VTXxgpINIp2OXrhtLtkT6oO0hXTFVJCnsO9uyi7UR + 0sBZbXBiAlmnPSMaY15UkzJvS49zBEJ7qnKeZyAer7V9dYe8OhtWt7kVD6sVhf3a + 7QlwQCdbg3jowodpM3mvHnU8W6JBJ6p7dtAG3zDFyHY0erzc4bfPKqJEtV6YRVij + 3zRCEjlU6A7c66y8V66eieNOB2FzEvutOwNrnrWfaR8jjafbhdZZIai9/GJd8w60 + rOBQoxkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAqAYuUyEwGoDK2tOXPVHAFBaN + 7D6SlHQBxYDuI5jYfJWBfdw3+Dc/OoBXHtkg2OQIV315+uIYHguhScvL4GBmEjgn + 17zKGciymTPJ3eTcb6IIXJIkJr89YM5tyr7cveEUXRugSdAtX0aCaURRr2H4ycjk + NLaSJyqCb02g9Ny0/5pql/v3gdY1XGF/hDEMwpLb5TxTt3VMtYj4r59Yz/5e/950 + MeINqAokIoLVtnYA+YW/Vj+T/ut9dFiC9E7arAw2z4zZ3uWvDVHTxhPQplUbpfyu + /rwx/GpotyGL1qU/JKOur2Y5Is8lfGkKZ6OJWAOPG+ZqO233+s1tH/SEQkIfIA== -----END CERTIFICATE----- """ private let clientSignedByOtherCACert = """ -----BEGIN CERTIFICATE----- MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl - ci1jYTAeFw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBQxEjAQBgNVBAMM - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMT3FOJy - fZpu6Ypqdu3bMbdgWiVprdNMODJsRURJzKj7znKC1tLcD7gvv/536DKzpAl3VF/p - bdd/yDU6JuXfel+2qxx3cvM+QLqBFEggUt9vd0rCs1DvZ9rT7A755hM2146gdE55 - g2JvXsJGC6gBskr40qkt5JrKSqOys5VE1TeifBt4lwCQphAB6qNeZM+0MEz6PSee - SFjVFZloIdyF3+swCsioTvzbfXs9p14HthJzL3edoorbtJ8upZdUlwJNZ2AX7K34 - sL1SDM+bOx+eoGLJ1OhPW+CHvjFMzpM3Q1dyTqR1SosBEiuYVpk/SAS/pBSNMp3y - KqF1T3ch4oJOb7UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKLBSZDrKiqAUiJCi - da+cdf2oMEh9Tz9vlGik2O3KW2uW+eG0kF1D0PSNN0yGcmGY6eTiDu2MRlmBmRIz - c3TTRArhXsRA7BQ4uyHOL3uRvcRUMk5DEtGwLrVAr79RM8jgPdom6Ws/fPzqxr8h - umm4zc6ixLFhfGqI5QbIwVyBO4jLxTmwjuJSjlHOuSKLa74K88oKlFUR74ANyu+U - e5/+q4SzvmRZG0sd2x1ZqmPajJ8nG2orhMu/k2VC8JQhuRigTXnapCUCVmeKA7eu - dcwqBMU6QyvmwcTTR8GRlpfOR7rXEZUa2qak4kf76iNWOHzYkgNI3nOsCe70bFIy - Eeh5yg== + ci1jYTAeFw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMM + CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqEdoBg + LtT1p+jnxjEXCQCpS6g5EIyHwjpIxC6gX49wACiFqNz67EmkDTX0HIPgk+/4wI5l + jP7mYPzhNAMFU4P8gDpYhKXLQyaNno1VTXxgpINIp2OXrhtLtkT6oO0hXTFVJCns + O9uyi7UR0sBZbXBiAlmnPSMaY15UkzJvS49zBEJ7qnKeZyAer7V9dYe8OhtWt7kV + D6sVhf3a7QlwQCdbg3jowodpM3mvHnU8W6JBJ6p7dtAG3zDFyHY0erzc4bfPKqJE + tV6YRVij3zRCEjlU6A7c66y8V66eieNOB2FzEvutOwNrnrWfaR8jjafbhdZZIai9 + /GJd8w60rOBQoxkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEADl9prZ95iXY74KpV + Vm5L/whTnfXQ2t1BVYD+nOKYyipAuVu+gTbBgseF7Ly+mEM0ewIgFgGbYZsO82Tz + nCCYZY+ablJkewNOjn3DAsr3kTjIFnC4fpDbYQMw3IHEOWdollRLGv0d5SJNc9z+ + N4pB8y53Uz2nYBUKGc+HEGKRwn0XZL5Vmd+OnT9Ry0wlYh3NYcTxAY8ArtyJq9h+ + ROG4YH3en8e7RIGg1uB/m515Gm+CA4WphjErEiy5VH4YFAYtBWCxO/h2gPOwX+8o + UnpdgUOkzB/YAc7S7OGGngz2IyBf+Rz/JC41uF4+efg8ijoZlWcO4/gB1yLiofBD + /MgUQQ== -----END CERTIFICATE----- """ private let clientKey = """ -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAxPcU4nJ9mm7pimp27dsxt2BaJWmt00w4MmxFREnMqPvOcoLW - 0twPuC+//nfoMrOkCXdUX+lt13/INTom5d96X7arHHdy8z5AuoEUSCBS3293SsKz - UO9n2tPsDvnmEzbXjqB0TnmDYm9ewkYLqAGySvjSqS3kmspKo7KzlUTVN6J8G3iX - AJCmEAHqo15kz7QwTPo9J55IWNUVmWgh3IXf6zAKyKhO/Nt9ez2nXge2EnMvd52i - itu0ny6ll1SXAk1nYBfsrfiwvVIMz5s7H56gYsnU6E9b4Ie+MUzOkzdDV3JOpHVK - iwESK5hWmT9IBL+kFI0ynfIqoXVPdyHigk5vtQIDAQABAoIBAGH7cD4+KlGa/z7G - O6eTtSW+HtohukE013ft+H9CHzepHEhG4ks/AerkhiQ2ziH6z42N+UFFRElB3fzs - ktEj3SKkInckzOBIhbbB468FtXRFZRihxsZqckWfyvygQF4qmAzxsSogtMVRFdib - M80+Gs3E/jb/B4whOgQ5L7D/7vmfUaOkD1tmm6mHfITihfdN9Jurg5OBZX2fWSHf - j9j6VbfLjoIatDGcC4MTiMkcIOYECAeXtpGYVdyH4K5y+UDCyo0b3qG+QuzE87gS - h2yVJNZKR6Y1vmcGqXQATVoZPGzS6Lc0/kUqcW50uoGBV5o5Sr4OKOUPIFCRwYa7 - 3LFW84ECgYEA42KEyM1vzXrDXrcGEp7GxZ0Z96yd5/XF3+AZ5R2snahRzDa+PzV2 - GPPZIVe8/bmJUxhWr3krHsUD21+KjNXY2l4ioAo2r4dmVR4vH9upGfWYutlw0pH8 - kurGq7H6lpwUfvb80AF3MXnwaqYGLUPX3cVOt9mEl0hEsqzlDrVSBcUCgYEA3cCO - RIWn8al+gse+xVqfpgGN9DuYM0qJuxvYpMy24EeJwGLyumwSesH6aK7zn2A9gME2 - eWFB93sL880QUeoGT2pHwBZAtC/4PvbNlrHO2W333/aMNVATBoGDIEUM8cjMkDr0 - jHx/iheLvXp81F5+W6o+sSSCI0RxkNXA/FcoUTECgYAw+fNn3PgL5jlWmU1xjUl7 - Hw+MzV1lrQZl5jstomqfurWDqvbnXniFf2BxUhie/euaPk/Nk+e5xO3Dvpx1IUqI - HmaO2iRVQnDEPLAhyIpv0PqIpHUspc0lR/Rq3vb+obe4cTKbCvXFbmJeVkxWS5qf - ZfRCnVN10lcZtSvRMzTrkQKBgGFrzxTbg0TwKdxa1Lzva2QLGspJxDwEay4AtdTw - +wbdZu9WiTzNbfDwd4q2EeHa7io6uCvrRofrTvz1Ak56efs5vfvtys9eo7lFxFyI - EVAEt/l033Qska8yBuGOdHlktjpHLFjr+Tw5y/KadWz3dpve11wLpgDIePwgbIBv - 6g6BAoGAA5lMXjQeQ26I5vXJTWd9NEvb7IVJeAheZOA03ruIPR4dVhorhsFt72bQ - +tugqU9aGlhxvCuvbBpEz8ZYhYxVe/tvDpe/upCCdGg4lwETje2FtKiYig7vMxKS - AwFfL4q1Bs2J1siAXCVSZ9fWFDGXWNbd1eLNzAfYqS766tBZyas= + MIIEogIBAAKCAQEAuoR2gGAu1PWn6OfGMRcJAKlLqDkQjIfCOkjELqBfj3AAKIWo + 3PrsSaQNNfQcg+CT7/jAjmWM/uZg/OE0AwVTg/yAOliEpctDJo2ejVVNfGCkg0in + Y5euG0u2RPqg7SFdMVUkKew727KLtRHSwFltcGICWac9IxpjXlSTMm9Lj3MEQnuq + cp5nIB6vtX11h7w6G1a3uRUPqxWF/drtCXBAJ1uDeOjCh2kzea8edTxbokEnqnt2 + 0AbfMMXIdjR6vNzht88qokS1XphFWKPfNEISOVToDtzrrLxXrp6J404HYXMS+607 + A2uetZ9pHyONp9uF1lkhqL38Yl3zDrSs4FCjGQIDAQABAoIBAFcoiTuqNpg7h1BV + 5o6QBhvyALHGoM4aro+P62UieiVMIDbPZr6E3x/2clnxDdYuftMXuduQ5tdCjrX9 + AtIajhFSUBVzweC74FBGw32mDASAIMBcliP7AFgvBCitub+15JemArU4eCxM/e4K + OyK5Z2Op2RFODkq2DRNKkFJ0IaoRN3fDSPLXg865RMSjDEd2I0gsADdh12Dk8+x+ + 5tpiQGLIfgBgWcqQrTl908sHB00WwlH166sT6k1G+SFRPK60r2fhOpyQelTUC+Zl + IOAtydE2ypsWG5Z3LnNkPwbwJl8m2hoL3M23syMnsxwTKQIblpYd3YdRR/5EozUf + f33p2IECgYEA6z8VBggDNb29CZgWjJUS3N/xuNyN6K1/jew2LyoAYX5zZL1/pTLE + Cm2MvJglY6B0r0/3eF6bBYGpHT9TWj3yzYlV0Q8iAdbz6se7skFrm7XXv50Bmjo8 + epzvVjM/oAvEz1/2bQXvZRTyunNwdyHBd9QCiuAHU8xuq8Qvq5+BNBECgYEAyvjc + sWwZQJiU7alx5ynDB25GRbXu4APTzaz99vaw/V8DNsYw5c0habq3JfaC8Q0bse8Z + G675M3F+gFRPG9TxqwSuYF9bpz1CQtKAT3pRXjjJQM3vfdixQjgBYspJMPKDi+qC + Dzhr8VBE16HxMArMgDKzYP/gmjHnRlcT12udZokCgYAcd+7YYwHYcBS/Y3tfGe9F + cYh0IaS+wrhL+Yj5HjEbm0zlpRUcbc9Rn75HWHY130YfrSK6m2BRQ0au9mnk4thO + TU9oVFd+N4AfKnqpcMdP+aqZUqvN+Tw2bmV8XglWGfaAThGpUe2NowJY0/2JPTmH + gc2o9sGMP5IpET3fnBbrsQKBgHLSCXbM4hQqvMUdf/P3Kf8AIPy6iOFtCNpnLFwS + /di3cQgBYhP90RMQrx7orvZSJgKocZm5h/vUDm3mQ8JI2lWWllaqWxzmiJ9omXFc + jr8wfJkOZpbYiJ4fNJmAOZtY9ZWnGeAmWNnwQKGDWP+GfF1hURxkY9iWtnCSPgU1 + OZuRAoGAOT0RQvvTVwxBU/BFRNJLSCjyJee8bz7+B/TmNui1Afyv+GBgzPeh5Z+L + vUi1MlvdlTdUVb1LmFgmidHgjRCYDEUxVEl3HmNHljCJqAXcJA61bMfItoteCHr6 + RMrN29F8q/ZPKbTgT5eH6tBX2meUqEDotTbdgVT84IhWyWOVF+g= -----END RSA PRIVATE KEY----- """ private let serverExplicitCurveCert = """ -----BEGIN CERTIFICATE----- - MIICDzCCAbYCCQC4FdI3dXof+TAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt - cGxlLmNvbTAeFw0yNDA3MDQxNzA4NTlaFw0yNTA3MDQxNzA4NTlaMBYxFDASBgNV + MIICEDCCAbYCCQCV4KgFB2WjmjAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt + cGxlLmNvbTAeFw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBYxFDASBgNV BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo - N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABBJm - 1grK+MXu4JYZqU6zD19w4MhWuhpPadq7PlpSxz4scMbWorth6pN/9hiqLAwKex7h - IuaSXF+83OESJPTOagQwCgYIKoZIzj0EAwIDRwAwRAIgNGW97RYxaltyx02UMyHc - E0AGSlMG3VFETZ5M9pMZGksCICgrTsLJXfOVdpyAjmI2vBibtZ/BisMftfXxupt3 - vM98 + N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABDJg + pBr9ZhidkGWnjW+hvhPLTUH9V4iNr+WNsb2HjQK4NloOauRQ4mlc534XeBya5tRy + aczylZHH6uC7ULCA8XcwCgYIKoZIzj0EAwIDSAAwRQIgVqWCUtszDMJU5ropnKDh + UhsHq8r0ARIfTsjSKSdung8CIQChqts3cpW/OOp5PS2bEm23Bf7SWksW2kRvXj6E + pjFODQ== -----END CERTIFICATE----- """ private let serverExplicitCurveKey = """ -----BEGIN EC PRIVATE KEY----- - MIIBaAIBAQQgD0lwrHX5wodoQFB4jhY7eqH4x5oBgL8aMjK9XndZODWggfowgfcC + MIIBaAIBAQQgYvOsKzMIHYIhfoUF1YqrM64ZR0Aotb++nOzoDB5mPrqggfowgfcC AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 - YyVRAgEBoUQDQgAEEmbWCsr4xe7glhmpTrMPX3DgyFa6Gk9p2rs+WlLHPixwxtai - u2Hqk3/2GKosDAp7HuEi5pJcX7zc4RIk9M5qBA== + YyVRAgEBoUQDQgAEMmCkGv1mGJ2QZaeNb6G+E8tNQf1XiI2v5Y2xvYeNArg2Wg5q + 5FDiaVznfhd4HJrm1HJpzPKVkcfq4LtQsIDxdw== -----END EC PRIVATE KEY----- """ diff --git a/Sources/GRPCSampleData/bundle.p12 b/Sources/GRPCSampleData/bundle.p12 index c32d3287272daa1445a9b6ca45bbfc60a71d9db4..2d9708523eb19b1330f974a686ac1cc540e19f90 100644 GIT binary patch delta 2271 zcmV<52q5?666F$*U4K!NI*0S?1w8@+2mpYB1AyVSB6@Q0vm}5(npekB8i)U83o1zQjf?nAhRP*G!ng1i?Kn)PIbLC}|oEvmL@_g-30Z9}7o< z@SihRAVs@jMAyjrBU%~P`?8$WdC(5ZiHH6vnksa9eF}`pE970j5g=qqBRU%Z-B5mU z#a_wmR@=rWmrznyh@5gR`5^_}IVc7zi2uZ#fvOITUMt&|g2y!0U-t}n!@f-Fd28c8 zKPFHOBF8~woZ;Gk(@M&o&63Ay0gEK;#6qv42Z9W0PM~M7# z>Za}GXZW+@Pf^JCIwSLzll~kq0U^{gOR3bzLGe4qnmkQO%O3qD0_;1?kHo5v#>@?&@4ZqW(ik}7^b*(gEL!qvHlB`jLy>Q6z<|6kT-J; z4_OBCdzlt%{S@Fi0!w>xg1a_QpD#Wd8qD!1cP|c;s4!>;5kycjpD&pwQdOOxU5jfE z?tCrhjV>^A+Y#`;9y}p%N-OSd(Y(8Tuot0MMaVfLml}aT>kh78v21MMj|_5i8k6k< zMt{^6M-*oqB=G_Q2ml0v1jq=m0||VTqxH%q9+J_j)7Mh9%wCz=Gex7gFWmXy*tb8p z7~wC97{g(4?(uPbxnO(3qFi=4rRubXwe$5Gkb+wjG=kr=R)xWCD)f{b24R|*+kHdT z6OW~^n)+>eWlc{3$c+tXt}pDfPlxmJ`hS61(fa^S5iYfk$`7A}#ZoHNSB*$N~6m}#{yW_C|yWJA#Q-QJ}+{(gZPaKeR5ZTZ6r)Smbszg{n0_v z3n30+CE`33lh`{YC5i9HvMaXD*FSIM*16SjEBucazAL3duJxGtL`Hnw=-(HlFyLBz zQvrEt8x1>W7md@q_?fty3O{=iv>@Za~^PO6^e9Bw;s_yN`K#z{$hv- zv``$1R@?%Mcz!W$S+1^R>ib!ggi%WLm=f(c`p>p>;!bAokQ!G8oXtw9IpFmmuAPLX z(ynX+q^k^|B2oGgtg+8|1I8p#4)y#>oQ9O_wST?S;dTkt!T*KGF$IyoTFp}O1=Jp2+;kVss_HdD^vDGx z%+J09U;tYL1O$R2sj?v40NHcs4@5ZtqFB-&^;0Gr53OfoJU=Zq542^jH`5$VLj7 zkZG_lu2r;dl3pM7LsV6=W%PdO6y zvhx$M;J%`qTs%YewlQ~KKb~@DF7<%bh*^p4d@yfHhjclFb$b;bN1&);N?5Oo(+{sz zdjrUavM>{w^Kk9Sqdi`*Re(L~U%qY);=4JU?4H(}&qTUx5Pz823KYjqGgK?3u;+D! zTPRd%9Ce_{v8L!o)xD;szIwGuGYu=7^D%=0WEarQ&VP}GxnEx&pVSbu2U*&E0KU%8 zau-)N!qZpEqp0t3fYC>fv(4LEB#tLThUa9Gg zlJ&d#q)FYXd3v%ofFHFGcrLhm{>=&Az4;=nsN9g}9e>0@hB`p_ad@y4ukF61tGd}3 zbSkfi4-ZoMHNNwz8^VF*mAkM6(D>s&7_NK}^E@SEm*lZ$kS?Sv-k4;ybk_Fiw zOlYC1eJHw;y_Yi8cVX6Y=2sn#pmKMCe(k}vyzbnrj?zT-R+{rtr7*G{4Ev&l>Fh25 zxp8=?=YN6?WwX9IdU=j8Y*t5S&#IgcE2-X@Ck*vNM%^9SMt)z^^zv#vl!}*I04Srbh3+bw7xAn z54Qoo$AMaOVik~kG|O4^Xx0x+-D@0Kv0dEB zCug+&$#=a0q7tGn8vwDiP@k2r8A{VctFm2l`@(@TL*AqV%adevs=s*hj+*C|l3dw;hWgB*fx-v@fL-2U6*PBjTmnqVrgvFE+C zr;MuQ<;7GaXOhr6G3pf(@tUpAB6;=VJ~$k;K=sriDr~C8C#E|TV0PPFzb=4*JGS6# z43F)ChYNit5r88A$V6p2AvP?Qh?w)U*VHv$iX}^(<(MDrSia;|G3Ic`Ec70K9e?c; zE0}`2vzPMk7+)mGWAR1&LMwh$Rc^R*^v<+KS?1R7SZy|gZiJIY z3KZsJ;GYRHw#L>2X5<%`dhxaf6HrY4Li|HM+xr!KSGOd&!Op(ho-|eop#V+9;2bWU znh6MzNwKRl#-8JEF;P=u4KCV`_}I~%C@Mx-tIjmQdt$fzn?cXXFsn1 z5Z{+MK@IY+m4(tjkAg>cNU#q#yW)XtoR}Y zY}#1xuVZT6_f5IX59jKfGjc8XVawUey0pF4y5GeYj!V|tPNFr$Wvds;p??*Mp!fgJ zZ4e4_GtEF=S@L*8vNVQ^61oqxee*9bpeclHT%aft>t=_w*+#6P+o;23i3s|#!Vm$) zuVb+<;Cuw0MMyxCuw6yu>`zN2$>tOa3DO>IagGdZk z@a9n1ZN&*gnF`i5swxXb3xDjKTD#lFnE{?v@nf;Mb|oYU231S;MfJBMCw3d>|LZNR zQsx-`5ZVP3^-4nKWBO%nPmt^7rou~JW+V-hnh(PdP&mDXg`2py;hlI&C@fMr!H~Kz z#e}^;^xZ4l+3c3yiiAz!Q$U%MBA~;*Xy}i}>=OgQkK-y1q$bCAoqrV4gxfF;fumaGwi@9?^+YhN-xaCAy>-^z?N@sktv14UF1-G}MddRCTL?*P_B^mJEHz?sV8D zxX$9-C15VuN74SI$7WFcq48U0$E@=p zS47vhbSOwcTu}Gg8sQ&fpZu3txUY)vf$I)qf8huloM_)d^M5!k)m?f$N)9`ioLoM# zCO+*O_q!kCm72-HY@8Va$OuiSZkL-z*AbXgheDa3fC1fMbD+R-a)v4;b`U=(jJ04E z^x^o#n)jk%GutjR{}tB#Um!w;q=pTa0X*0IaQT-ac!-=h#sMu1bo9XApc~{otri-+ zKT7K}d$yTZ7k_ANvY9!ELuD1+4n2hUCYLxeBep1ka4W#ijr!ZTvwHfX{@Cy7nK80> zSC(+(PdY5Kt>uP0mFRbPeEDmz12N#Yt~;mB-D>HS=CYbY(sRtG-=jg|N|84UcdG{^ zg%$)cB`_lf2`Yw2hW8Bt2^BFG1Qe}fvb`arhn7L*!aSz+UiwLab-c$gF)$%82?hl# t4g&%j1povTwF!~lvMg%fdo=O`D_)qpy6AwR1PI1vV&|}<-mC%w2mnyhZqWb$ diff --git a/scripts/make-sample-certs.py b/scripts/make-sample-certs.py index cc387515c..8f4ec0596 100755 --- a/scripts/make-sample-certs.py +++ b/scripts/make-sample-certs.py @@ -13,9 +13,13 @@ # 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 argparse +import datetime import os +import shutil import subprocess -import datetime +import tempfile TEMPLATE = """\ /* @@ -194,7 +198,7 @@ def extract_key(ec_key_and_params): return "\n".join(lines).strip() -if __name__ == "__main__": +def update_sample_certs(): now = datetime.datetime.now() # makecert uses an expiry of 365 days. delta = datetime.timedelta(days=365) @@ -231,3 +235,27 @@ def extract_key(ec_key_and_params): formatted = TEMPLATE.format(**kwargs) with open("Sources/GRPCSampleData/GRPCSwiftCertificate.swift", "w") as fh: fh.write(formatted) + + +def update_p12_bundle(): + tmp_dir = tempfile.TemporaryDirectory() + subprocess.check_call(["git", "clone", "--single-branch", "--branch", + "make-p12-bundle-for-grpc-swift-tests", + "https://github.com/glbrntt/swift-nio-ssl", + tmp_dir.name]) + + subprocess.check_call(["./make-pkcs12.sh"], cwd=tmp_dir.name) + shutil.copyfile(tmp_dir.name + "/bundle.p12", "Sources/GRPCSampleData/bundle.p12") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--no-p12-bundle", action="store_false", dest="p12") + parser.add_argument("--no-sample-certs", action="store_false", dest="sample_certs") + args = parser.parse_args() + + if args.sample_certs: + update_sample_certs() + + if args.p12: + update_p12_bundle() From 1f067db2044d11c6bf6a7b047b15ec0796141345 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 1 Aug 2024 11:13:18 +0200 Subject: [PATCH 415/580] Fix tests to work with unreleased swift-nio changes (#2001) --- Tests/GRPCTests/ConnectionManagerTests.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift index ff27d53f8..e416684cc 100644 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ b/Tests/GRPCTests/ConnectionManagerTests.swift @@ -989,6 +989,9 @@ extension ConnectionManagerTests { let manager = self.makeConnectionManager(configuration: configuration) { _, _ in self.loop.makeFailedFuture(DoomedChannelError()) } + defer { + try! manager.shutdown().wait() + } self.waitForStateChanges([ Change(from: .idle, to: .connecting), @@ -1154,9 +1157,7 @@ extension ConnectionManagerTests { func testHTTP2Delegates() throws { let channel = EmbeddedChannel(loop: self.loop) - defer { - XCTAssertNoThrow(try channel.finish()) - } + // The channel gets shut down by the connection manager. let multiplexer = HTTP2StreamMultiplexer( mode: .client, @@ -1215,6 +1216,11 @@ extension ConnectionManagerTests { http2Delegate: http2, logger: self.logger ) + defer { + let future = manager.shutdown() + self.loop.run() + try! future.wait() + } // Start connecting. let futureMultiplexer = manager.getHTTP2Multiplexer() From b8deae2a4e8b9993d152be7eb335fd83fadba76c Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Thu, 1 Aug 2024 13:16:17 +0100 Subject: [PATCH 416/580] Clamp HTTP2 config values in configureGRPCServerPipeline (#2000) --- .../Internal/NIOChannelPipeline+GRPC.swift | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index 2a7685491..d44ea5109 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -51,10 +51,13 @@ extension ChannelPipeline.SynchronousOperations { ) try self.addHandler(flushNotificationHandler) + let clampedTargetWindowSize = self.clampTargetWindowSize(http2Config.targetWindowSize) + let clampedMaxFrameSize = self.clampMaxFrameSize(http2Config.maxFrameSize) + var http2HandlerConnectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() var http2HandlerHTTP2Settings = HTTP2Settings([ - HTTP2Setting(parameter: .initialWindowSize, value: http2Config.targetWindowSize), - HTTP2Setting(parameter: .maxFrameSize, value: http2Config.maxFrameSize), + HTTP2Setting(parameter: .initialWindowSize, value: clampedTargetWindowSize), + HTTP2Setting(parameter: .maxFrameSize, value: clampedMaxFrameSize), HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize), ]) if let maxConcurrentStreams = http2Config.maxConcurrentStreams { @@ -65,7 +68,7 @@ extension ChannelPipeline.SynchronousOperations { http2HandlerConnectionConfiguration.initialSettings = http2HandlerHTTP2Settings var http2HandlerStreamConfiguration = NIOHTTP2Handler.StreamConfiguration() - http2HandlerStreamConfiguration.targetWindowSize = http2Config.targetWindowSize + http2HandlerStreamConfiguration.targetWindowSize = clampedTargetWindowSize let streamMultiplexer = try self.configureAsyncHTTP2Pipeline( mode: .server, @@ -111,18 +114,8 @@ extension ChannelPipeline.SynchronousOperations { NIOAsyncChannel, NIOHTTP2Handler.AsyncStreamMultiplexer ) { - // Window size which mustn't exceed 2^32 - 1 (RFC 9113 § 6.1.3). - let clampedTargetWindowSize = min(config.http2.targetWindowSize, (1 << 31) - 1) - - // Max frame size must be in the range 2^14 ..< 2^24 (RFC 9113 § 6.1.3). - let clampedMaxFrameSize: Int - if config.http2.maxFrameSize >= (1 << 24) { - clampedMaxFrameSize = (1 << 24) - 1 - } else if config.http2.maxFrameSize < (1 << 14) { - clampedMaxFrameSize = (1 << 14) - } else { - clampedMaxFrameSize = config.http2.maxFrameSize - } + let clampedTargetWindowSize = self.clampTargetWindowSize(config.http2.targetWindowSize) + let clampedMaxFrameSize = self.clampMaxFrameSize(config.http2.maxFrameSize) // Use NIOs defaults as a starting point. var http2 = NIOHTTP2Handler.Configuration() @@ -168,3 +161,23 @@ extension ChannelPipeline.SynchronousOperations { return (connection, multiplexer) } } + +extension ChannelPipeline.SynchronousOperations { + /// Max frame size must be in the range `2^14 ..< 2^24` (RFC 9113 § 4.2). + fileprivate func clampMaxFrameSize(_ maxFrameSize: Int) -> Int { + let clampedMaxFrameSize: Int + if maxFrameSize >= (1 << 24) { + clampedMaxFrameSize = (1 << 24) - 1 + } else if maxFrameSize < (1 << 14) { + clampedMaxFrameSize = (1 << 14) + } else { + clampedMaxFrameSize = maxFrameSize + } + return clampedMaxFrameSize + } + + /// Window size which mustn't exceed `2^31 - 1` (RFC 9113 § 6.5.2). + internal func clampTargetWindowSize(_ targetWindowSize: Int) -> Int { + min(targetWindowSize, (1 << 31) - 1) + } +} From 58acae6120d8f730bb6628e88b160311c40b6b1b Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Mon, 5 Aug 2024 16:09:45 +0100 Subject: [PATCH 417/580] Enable `InternalImportsByDefault` in v2 modules (#2003) Starting with Swift 5.10, `InternalImportsByDefault` is an optional feature flag to make all `import` statements be `internal` by default, instead of `public`, which has been the default since the beginning in Swift. With a future language mode, this will become the new default, and all imports will be `internal` unless otherwise specified. The main reason for this change is to minimise dependency creep. This PR enables the feature flag on v2 modules, and adds `public`/`package` access modifiers to `import`s where required. Note that sadly, `@usableFromInline` has not been implemented for `import` statements, so in `@inlinable`/`@usableFromInline` contexts where non-publicly-imported types are used, the only workaround for now is to import the module as `public`. I've left a comment next to these imports. --- Package@swift-6.swift | 68 +++++++++++++++---- .../Client/Internal/RetryDelaySequence.swift | 4 +- Sources/GRPCCore/GRPCClient.swift | 2 +- Sources/GRPCCore/GRPCServer.swift | 2 +- .../Concurrency Primitives/Lock.swift | 4 +- .../Internal/AsyncIteratorSequence.swift | 3 +- .../Internal/BroadcastAsyncSequence.swift | 3 +- .../Streaming/Internal/BufferedStream.swift | 2 +- Sources/GRPCCore/Timeout.swift | 3 +- .../Connection/ClientConnectionHandler.swift | 4 +- .../Client/Connection/Connection.swift | 8 +-- .../Client/Connection/ConnectionFactory.swift | 6 +- .../Client/Connection/GRPCChannel.swift | 6 +- .../LoadBalancers/PickFirstLoadBalancer.swift | 2 +- .../RoundRobinLoadBalancer.swift | 2 +- .../Connection/LoadBalancers/Subchannel.swift | 4 +- .../Client/Connection/RequestQueue.swift | 2 +- .../Client/GRPCClientStreamHandler.swift | 6 +- .../Client/HTTP2ClientTransport.swift | 2 +- .../Client/Resolver/NameResolver+IPv4.swift | 2 +- .../Client/Resolver/NameResolver+IPv6.swift | 2 +- .../Client/Resolver/NameResolver+UDS.swift | 2 +- .../Client/Resolver/NameResolver+VSOCK.swift | 2 +- .../Client/Resolver/NameResolver.swift | 2 +- .../Compression/CompressionAlgorithm.swift | 2 +- Sources/GRPCHTTP2Core/Compression/Zlib.swift | 6 +- .../GRPCHTTP2Core/GRPCMessageDecoder.swift | 4 +- Sources/GRPCHTTP2Core/GRPCMessageFramer.swift | 4 +- .../GRPCStreamStateMachine.swift | 8 +-- .../Internal/ConstantAsyncSequence.swift | 2 +- .../Internal/NIOChannelPipeline+GRPC.swift | 8 +-- .../NIOSocketAddress+GRPCSocketAddress.swift | 2 +- .../Internal/ProcessUniqueID.swift | 2 +- Sources/GRPCHTTP2Core/Internal/Timer.swift | 2 +- Sources/GRPCHTTP2Core/OneOrManyQueue.swift | 2 +- .../GRPCServerFlushNotificationHandler.swift | 2 +- .../Server/Connection/ServerConnection.swift | 4 +- ...ectionManagementHandler+StateMachine.swift | 4 +- .../ServerConnectionManagementHandler.swift | 4 +- .../Server/GRPCServerStreamHandler.swift | 6 +- .../Server/HTTP2ServerTransport.swift | 4 +- .../HTTP2ClientTransport+Posix.swift | 8 +-- .../HTTP2ServerTransport+Posix.swift | 12 ++-- .../NIOClientBootstrap+SocketAddress.swift | 8 +-- ...TP2ServerTransport+TransportServices.swift | 13 ++-- .../InProcessClientTransport.swift | 2 +- .../InProcessServerTransport.swift | 2 +- .../InProcessTransport.swift | 3 +- .../ClientTracingInterceptor.swift | 4 +- Sources/GRPCInterceptors/HookedWriter.swift | 4 +- .../ServerTracingInterceptor.swift | 4 +- Sources/GRPCProtobuf/Coding.swift | 4 +- .../ProtobufCodeGenParser.swift | 8 +-- .../ProtobufCodeGenerator.swift | 4 +- 54 files changed, 167 insertions(+), 118 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index ea133c329..ba5f2176d 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -207,7 +207,11 @@ extension Target { .atomics ], path: "Sources/GRPCCore", - swiftSettings: [._swiftLanguageMode(.v5), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v5), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -217,7 +221,11 @@ extension Target { dependencies: [ .grpcCore ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -228,7 +236,11 @@ extension Target { .grpcCore, .tracing ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -243,7 +255,11 @@ extension Target { .dequeModule, .atomics ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -256,7 +272,11 @@ extension Target { .nioPosix, .nioExtras ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -270,7 +290,11 @@ extension Target { .nioExtras, .nioTransportServices ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -283,7 +307,11 @@ extension Target { .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -510,7 +538,7 @@ extension Target { .grpcCore, .grpcProtobuf ], - swiftSettings: [._swiftLanguageMode(.v5), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -814,7 +842,11 @@ extension Target { .target( name: "GRPCCodeGen", path: "Sources/GRPCCodeGen", - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -826,7 +858,11 @@ extension Target { .protobuf, ], path: "Sources/GRPCProtobuf", - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -839,7 +875,11 @@ extension Target { .grpcCodeGen ], path: "Sources/GRPCProtobufCodeGen", - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -851,7 +891,11 @@ extension Target { .grpcProtobuf ], path: "Sources/Services/Health", - swiftSettings: [._swiftLanguageMode(.v6)] + swiftSettings: [ + ._swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } } diff --git a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift index b07691e4e..d22c6fd52 100644 --- a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -14,9 +14,9 @@ * limitations under the License. */ #if canImport(Darwin) -import Darwin +public import Darwin // should be @usableFromInline #else -import Glibc +public import Glibc // should be @usableFromInline #endif @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index c4cc1ed9f..1ba012640 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import Atomics +internal import Atomics /// A gRPC client. /// diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index b24d90fd1..e0700f380 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import Atomics +internal import Atomics /// A gRPC server. /// diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift index cd334df5f..7bb9e4ca2 100644 --- a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift +++ b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift @@ -28,9 +28,9 @@ //===----------------------------------------------------------------------===// #if canImport(Darwin) -import Darwin +public import Darwin // should be @usableFromInline #elseif canImport(Glibc) -import Glibc +public import Glibc // should be @usableFromInline #endif @usableFromInline diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift b/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift index 8319d46d1..05596c6d4 100644 --- a/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + +public import Atomics // should be @usableFromInline @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @usableFromInline diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift index 6367fa859..5f812eb11 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import DequeModule + +public import DequeModule // should be @usableFromInline /// An `AsyncSequence` which can broadcast its values to multiple consumers concurrently. /// diff --git a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift index f6fd3dcc1..f2692c2a6 100644 --- a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift +++ b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift @@ -25,7 +25,7 @@ // //===----------------------------------------------------------------------===// -import DequeModule +public import DequeModule // should be @usableFromInline /// An asynchronous sequence generated from an error-throwing closure that /// calls a continuation to produce new elements. diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index b350fdaea..e4e174e98 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Dispatch + +internal import Dispatch /// A timeout for a gRPC call. /// diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift index 5138dee21..4cf354857 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import NIOCore -import NIOHTTP2 +package import NIOCore +package import NIOHTTP2 /// An event which happens on a client's HTTP/2 connection. package enum ClientConnectionEvent: Sendable { diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index 9bd3f97bd..9f28ed6b3 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import GRPCCore -import NIOConcurrencyHelpers -import NIOCore -import NIOHTTP2 +package import GRPCCore +internal import NIOConcurrencyHelpers +package import NIOCore +package import NIOHTTP2 /// A `Connection` provides communication to a single remote peer. /// diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift index 57225bc67..c56e507db 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import NIOCore -import NIOHTTP2 -import NIOPosix +package import NIOCore +package import NIOHTTP2 +internal import NIOPosix @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) package protocol HTTP2Connector: Sendable { diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index c36df29a5..50122c11c 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import Atomics -import DequeModule -import GRPCCore +internal import Atomics +internal import DequeModule +package import GRPCCore @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct GRPCChannel: ClientTransport { diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift index d654fcff1..d0e66a5a3 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +package import GRPCCore /// A load-balancer which has a single subchannel. /// diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift index b6c838955..a6a8418b0 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +package import GRPCCore /// A load-balancer which maintains to a set of subchannels and uses round-robin to pick a /// subchannel when picking a subchannel to use. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift index c4e122300..cd1b197b6 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import NIOConcurrencyHelpers +package import GRPCCore +internal import NIOConcurrencyHelpers /// A ``Subchannel`` provides communication to a single ``Endpoint``. /// diff --git a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift index 492dc9b61..7d4876eea 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import DequeModule +internal import DequeModule @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) struct RequestQueue { diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index b219a517e..3640af268 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import GRPCCore -import NIOCore -import NIOHTTP2 +internal import GRPCCore +internal import NIOCore +internal import NIOHTTP2 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCClientStreamHandler: ChannelDuplexHandler { diff --git a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift index c2440689a..e4cd97784 100644 --- a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift +++ b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +public import GRPCCore /// A namespace for the HTTP/2 client transport. public enum HTTP2ClientTransport {} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift index e6c2bf828..d3b66ea18 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore extension ResolvableTargets { /// A resolvable target for IPv4 addresses. diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift index c3681d228..6c41a0353 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore extension ResolvableTargets { /// A resolvable target for IPv4 addresses. diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift index 149b541fa..b957bffd5 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore extension ResolvableTargets { /// A resolvable target for Unix Domain Socket address. diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift index 0e7b5a1f5..e8c6c815b 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore extension ResolvableTargets { /// A resolvable target for Virtual Socket addresses. diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift index 93f44b48a..ad1436fe2 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +public import GRPCCore /// A name resolver can provide resolved addresses and service configuration values over time. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift index 7988549d1..1608f4c1e 100644 --- a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift +++ b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore extension CompressionAlgorithm { init?(name: String) { diff --git a/Sources/GRPCHTTP2Core/Compression/Zlib.swift b/Sources/GRPCHTTP2Core/Compression/Zlib.swift index da6252275..a2a25110f 100644 --- a/Sources/GRPCHTTP2Core/Compression/Zlib.swift +++ b/Sources/GRPCHTTP2Core/Compression/Zlib.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import CGRPCZlib -import GRPCCore -import NIOCore +internal import CGRPCZlib +internal import GRPCCore +internal import NIOCore enum Zlib { enum Method { diff --git a/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift b/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift index ca65e6942..e8e4f1c8f 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import NIOCore +internal import GRPCCore +package import NIOCore /// A ``GRPCMessageDecoder`` helps with the deframing of gRPC data frames: /// - It reads the frame's metadata to know whether the message payload is compressed or not, and its length diff --git a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift index d80e1d526..059c3e161 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import NIOCore +internal import GRPCCore +internal import NIOCore /// A ``GRPCMessageFramer`` helps with the framing of gRPC data frames: /// - It prepends data with the required metadata (compression flag and message length). diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 8d6d93f3b..b9a08862e 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import GRPCCore -import NIOCore -import NIOHPACK -import NIOHTTP1 +internal import GRPCCore +internal import NIOCore +internal import NIOHPACK +internal import NIOHTTP1 package enum Scheme: String { case http diff --git a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift index 0792d4e0a..d1ecef17e 100644 --- a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift +++ b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private struct ConstantAsyncSequence: AsyncSequence, Sendable { diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index d44ea5109..dbbd837d2 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import GRPCCore -import NIOCore -import NIOHPACK -import NIOHTTP2 +package import GRPCCore +package import NIOCore +internal import NIOHPACK +package import NIOHTTP2 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ChannelPipeline.SynchronousOperations { diff --git a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift index cf015466a..db7eb537d 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import NIOCore +package import NIOCore extension GRPCHTTP2Core.SocketAddress { package init(_ nioSocketAddress: NIOCore.SocketAddress) { diff --git a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift index 7c9bbba55..d80658e00 100644 --- a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift +++ b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import Atomics +internal import Atomics /// An ID which is unique within this process. struct ProcessUniqueID: Hashable, Sendable, CustomStringConvertible { diff --git a/Sources/GRPCHTTP2Core/Internal/Timer.swift b/Sources/GRPCHTTP2Core/Internal/Timer.swift index 7c53333d1..bfc4ff29a 100644 --- a/Sources/GRPCHTTP2Core/Internal/Timer.swift +++ b/Sources/GRPCHTTP2Core/Internal/Timer.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import NIOCore +package import NIOCore package struct Timer { /// The delay to wait before running the task. diff --git a/Sources/GRPCHTTP2Core/OneOrManyQueue.swift b/Sources/GRPCHTTP2Core/OneOrManyQueue.swift index 8995f5641..fdf88186c 100644 --- a/Sources/GRPCHTTP2Core/OneOrManyQueue.swift +++ b/Sources/GRPCHTTP2Core/OneOrManyQueue.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import DequeModule +internal import DequeModule /// A FIFO-queue which allows for a single element to be stored on the stack and defers to a /// heap-implementation if further elements are added. diff --git a/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift index 85aa23608..1bbffc205 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import NIOCore +internal import NIOCore @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class GRPCServerFlushNotificationHandler: ChannelOutboundHandler { diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift index 2997580df..84d1d25a2 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import NIOCore +package import GRPCCore +package import NIOCore @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public enum ServerConnection { diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift index 4aabf09a5..8ca7660d6 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import NIOCore -import NIOHTTP2 +internal import NIOCore +internal import NIOHTTP2 extension ServerConnectionManagementHandler { /// Tracks the state of TCP connections at the server. diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index c4b63eb8f..350cf44eb 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import NIOCore -import NIOHTTP2 +internal import NIOCore +internal import NIOHTTP2 /// A `ChannelHandler` which manages the lifecycle of a gRPC connection over HTTP/2. /// diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index d0f17601e..df4c184eb 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import GRPCCore -import NIOCore -import NIOHTTP2 +package import GRPCCore +package import NIOCore +package import NIOHTTP2 @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandler { diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift index ffeb21511..c0aef9094 100644 --- a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift +++ b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import NIOHTTP2 +public import GRPCCore +internal import NIOHTTP2 /// A namespace for the HTTP/2 server transport. public enum HTTP2ServerTransport {} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index 7348174d8..1d7cff13e 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import GRPCCore -import GRPCHTTP2Core -import NIOCore -import NIOPosix +public import GRPCCore +public import GRPCHTTP2Core // should be @usableFromInline +public import NIOCore +public import NIOPosix // has to be public because of default argument value in init @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ClientTransport { diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 1de2301aa..0d806dc73 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -14,12 +14,12 @@ * limitations under the License. */ -import GRPCCore -import GRPCHTTP2Core -import NIOCore -import NIOExtras -import NIOHTTP2 -import NIOPosix +public import GRPCCore +public import GRPCHTTP2Core +internal import NIOCore +internal import NIOExtras +internal import NIOHTTP2 +public import NIOPosix // has to be public because of default argument value in init extension HTTP2ServerTransport { /// A NIOPosix-backed implementation of a server transport. diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift index 6a056abed..5eb6e193e 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import GRPCCore -import GRPCHTTP2Core -import NIOCore -import NIOPosix +internal import GRPCCore +internal import GRPCHTTP2Core +internal import NIOCore +internal import NIOPosix extension ClientBootstrap { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 8d85d7a25..93d6f3fd8 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -15,12 +15,13 @@ */ #if canImport(Network) -import GRPCCore -import GRPCHTTP2Core -import NIOCore -import NIOExtras -import NIOHTTP2 -import NIOTransportServices +public import GRPCCore +public import NIOTransportServices // has to be public because of default argument value in init +public import GRPCHTTP2Core + +internal import NIOCore +internal import NIOExtras +internal import NIOHTTP2 extension HTTP2ServerTransport { /// A NIO Transport Services-backed implementation of a server transport. diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index a34020676..c8f9412ec 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +public import GRPCCore /// An in-process implementation of a ``ClientTransport``. /// diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index a4ea05e7b..412d1bd58 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +public import GRPCCore /// An in-process implementation of a ``ServerTransport``. /// diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index ec553e72a..32a2002e9 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import GRPCCore + +public import GRPCCore public enum InProcessTransport { /// Returns a pair containing an ``InProcessServerTransport`` and an ``InProcessClientTransport``. diff --git a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift index 22dbcd134..065d9936e 100644 --- a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift +++ b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import Tracing +public import GRPCCore +internal import Tracing /// A client interceptor that injects tracing information into the request. /// diff --git a/Sources/GRPCInterceptors/HookedWriter.swift b/Sources/GRPCInterceptors/HookedWriter.swift index ed4977dca..8a9994033 100644 --- a/Sources/GRPCInterceptors/HookedWriter.swift +++ b/Sources/GRPCInterceptors/HookedWriter.swift @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import GRPCCore -import Tracing +internal import GRPCCore +internal import Tracing @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct HookedWriter: RPCWriterProtocol { diff --git a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift index 78b992a40..5a568e425 100644 --- a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift +++ b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import Tracing +public import GRPCCore +internal import Tracing /// A server interceptor that extracts tracing information from the request. /// diff --git a/Sources/GRPCProtobuf/Coding.swift b/Sources/GRPCProtobuf/Coding.swift index ecd4b48f8..4aa0718cf 100644 --- a/Sources/GRPCProtobuf/Coding.swift +++ b/Sources/GRPCProtobuf/Coding.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCore -import SwiftProtobuf +public import GRPCCore +public import SwiftProtobuf /// Serializes a Protobuf message into a sequence of bytes. public struct ProtobufSerializer: GRPCCore.MessageSerializer { diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index 59904ce46..824da05a6 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -14,11 +14,11 @@ * limitations under the License. */ -import Foundation -import SwiftProtobuf -import SwiftProtobufPluginLibrary +internal import Foundation +internal import SwiftProtobuf +internal import SwiftProtobufPluginLibrary -import struct GRPCCodeGen.CodeGenerationRequest +internal import struct GRPCCodeGen.CodeGenerationRequest /// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. internal struct ProtobufCodeGenParser { diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index adde3ddf8..0fd4e7727 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import GRPCCodeGen -import SwiftProtobufPluginLibrary +public import GRPCCodeGen +public import SwiftProtobufPluginLibrary public struct ProtobufCodeGenerator { internal var configuration: SourceGenerator.Configuration From 8a72c8d2b2fbabc23b37e49e05083fb407094620 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:57:16 +0100 Subject: [PATCH 418/580] Implement Health service (#1987) Motivation: Implement the Health service defined in `Protos/upstream/grpc/health/v1/health.proto`. Modifications: - Implement the "check" and "watch" methods for the service. - Implement providers (public APIs) to interact with the service. - Add tests. Result: The Health service will be available for use in grpc-swift. --------- Co-authored-by: George Barnett --- Package@swift-6.swift | 18 +- Protos/generate.sh | 4 +- .../Health/Generated/health.grpc.swift | 187 ++++++++++-- .../Services/Health/Generated/health.pb.swift | 50 +-- Sources/Services/Health/Health.swift | 127 ++++++++ Sources/Services/Health/HealthService.swift | 132 ++++++++ Sources/Services/Health/ServingStatus.swift | 38 +++ Tests/Services/HealthTests/HealthTests.swift | 287 ++++++++++++++++++ .../Test Utilities/XCTest+Utilities.swift | 30 ++ 9 files changed, 826 insertions(+), 47 deletions(-) create mode 100644 Sources/Services/Health/Health.swift create mode 100644 Sources/Services/Health/HealthService.swift create mode 100644 Sources/Services/Health/ServingStatus.swift create mode 100644 Tests/Services/HealthTests/HealthTests.swift create mode 100644 Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift diff --git a/Package@swift-6.swift b/Package@swift-6.swift index ba5f2176d..5cbb769bd 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -168,6 +168,7 @@ extension Target.Dependency { static var grpcHTTP2Core: Self { .target(name: "GRPCHTTP2Core") } static var grpcHTTP2TransportNIOPosix: Self { .target(name: "GRPCHTTP2TransportNIOPosix") } static var grpcHTTP2TransportNIOTransportServices: Self { .target(name: "GRPCHTTP2TransportNIOTransportServices") } + static var grpcHealth: Self { .target(name: "GRPCHealth") } } // MARK: - Targets @@ -510,6 +511,19 @@ extension Target { ) } + static var grpcHealthTests: Target { + .testTarget( + name: "GRPCHealthTests", + dependencies: [ + .grpcHealth, + .grpcProtobuf, + .grpcInProcessTransport + ], + path: "Tests/Services/HealthTests", + swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + ) + } + static var interopTestModels: Target { .target( name: "GRPCInteroperabilityTestModels", @@ -893,8 +907,7 @@ extension Target { path: "Sources/Services/Health", swiftSettings: [ ._swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") + .enableUpcomingFeature("ExistentialAny") ] ) } @@ -1046,6 +1059,7 @@ let package = Package( .grpcInterceptorsTests, .grpcHTTP2CoreTests, .grpcHTTP2TransportTests, + .grpcHealthTests, .grpcProtobufTests, .grpcProtobufCodeGenTests, .inProcessInteroperabilityTests, diff --git a/Protos/generate.sh b/Protos/generate.sh index 4be32ec1d..3e5c05631 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -244,8 +244,8 @@ function generate_health_service { local proto="$here/upstream/grpc/health/v1/health.proto" local output="$root/Sources/Services/Health/Generated" - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "Client=false" "Server=true" "_V2=true" + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" "Client=true" "Server=true" "_V2=true" } #------------------------------------------------------------------------------ diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 769b3a893..05c4387b4 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -27,40 +27,44 @@ import GRPCCore import GRPCProtobuf -internal enum Grpc_Health_V1_Health { - internal enum Method { - internal enum Check { - internal typealias Input = Grpc_Health_V1_HealthCheckRequest - internal typealias Output = Grpc_Health_V1_HealthCheckResponse - internal static let descriptor = MethodDescriptor( +package enum Grpc_Health_V1_Health { + package enum Method { + package enum Check { + package typealias Input = Grpc_Health_V1_HealthCheckRequest + package typealias Output = Grpc_Health_V1_HealthCheckResponse + package static let descriptor = MethodDescriptor( service: "grpc.health.v1.Health", method: "Check" ) } - internal enum Watch { - internal typealias Input = Grpc_Health_V1_HealthCheckRequest - internal typealias Output = Grpc_Health_V1_HealthCheckResponse - internal static let descriptor = MethodDescriptor( + package enum Watch { + package typealias Input = Grpc_Health_V1_HealthCheckRequest + package typealias Output = Grpc_Health_V1_HealthCheckResponse + package static let descriptor = MethodDescriptor( service: "grpc.health.v1.Health", method: "Watch" ) } - internal static let descriptors: [MethodDescriptor] = [ + package static let descriptors: [MethodDescriptor] = [ Check.descriptor, Watch.descriptor ] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Grpc_Health_V1_HealthStreamingServiceProtocol + package typealias StreamingServiceProtocol = Grpc_Health_V1_HealthStreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Grpc_Health_V1_HealthServiceProtocol + package typealias ServiceProtocol = Grpc_Health_V1_HealthServiceProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + package typealias ClientProtocol = Grpc_Health_V1_HealthClientProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + package typealias Client = Grpc_Health_V1_HealthClient } /// Health is gRPC's mechanism for checking whether a server is able to handle /// RPCs. Its semantics are documented in /// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.RegistrableRPCService { +package protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Check gets the health of the specified service. If the requested service /// is unknown, the call will fail with status NOT_FOUND. If the caller does /// not specify a service name, the server should respond with its overall @@ -94,7 +98,7 @@ internal protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.Regist @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Health_V1_Health.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Health_V1_Health.Method.Check.descriptor, deserializer: ProtobufDeserializer(), @@ -118,7 +122,7 @@ extension Grpc_Health_V1_Health.StreamingServiceProtocol { /// RPCs. Its semantics are documented in /// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.StreamingServiceProtocol { +package protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.StreamingServiceProtocol { /// Check gets the health of the specified service. If the requested service /// is unknown, the call will fail with status NOT_FOUND. If the caller does /// not specify a service name, the server should respond with its overall @@ -151,13 +155,160 @@ internal protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.St /// Partial conformance to `Grpc_Health_V1_HealthStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Health_V1_Health.ServiceProtocol { - internal func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + package func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.check(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - internal func watch(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + package func watch(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.watch(request: ServerRequest.Single(stream: request)) return response } +} + +/// Health is gRPC's mechanism for checking whether a server is able to handle +/// RPCs. Its semantics are documented in +/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package protocol Grpc_Health_V1_HealthClientProtocol: Sendable { + /// Check gets the health of the specified service. If the requested service + /// is unknown, the call will fail with status NOT_FOUND. If the caller does + /// not specify a service name, the server should respond with its overall + /// health status. + /// + /// Clients should set a deadline when calling Check, and can declare the + /// server unhealthy if they do not receive a timely response. + /// + /// Check implementations should be idempotent and side effect free. + func check( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + func watch( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Grpc_Health_V1_Health.ClientProtocol { + package func check( + request: ClientRequest.Single, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.check( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } + + package func watch( + request: ClientRequest.Single, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.watch( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + options: options, + body + ) + } +} + +/// Health is gRPC's mechanism for checking whether a server is able to handle +/// RPCs. Its semantics are documented in +/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol { + private let client: GRPCCore.GRPCClient + + package init(client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Check gets the health of the specified service. If the requested service + /// is unknown, the call will fail with status NOT_FOUND. If the caller does + /// not specify a service name, the server should respond with its overall + /// health status. + /// + /// Clients should set a deadline when calling Check, and can declare the + /// server unhealthy if they do not receive a timely response. + /// + /// Check implementations should be idempotent and side effect free. + package func check( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Grpc_Health_V1_Health.Method.Check.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + package func watch( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Grpc_Health_V1_Health.Method.Watch.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } } \ No newline at end of file diff --git a/Sources/Services/Health/Generated/health.pb.swift b/Sources/Services/Health/Generated/health.pb.swift index 883a55443..f95e659f8 100644 --- a/Sources/Services/Health/Generated/health.pb.swift +++ b/Sources/Services/Health/Generated/health.pb.swift @@ -37,29 +37,29 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct Grpc_Health_V1_HealthCheckRequest: Sendable { +package struct Grpc_Health_V1_HealthCheckRequest: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - var service: String = String() + package var service: String = String() - var unknownFields = SwiftProtobuf.UnknownStorage() + package var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + package init() {} } -struct Grpc_Health_V1_HealthCheckResponse: Sendable { +package struct Grpc_Health_V1_HealthCheckResponse: Sendable { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - var status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown + package var status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown - var unknownFields = SwiftProtobuf.UnknownStorage() + package var unknownFields = SwiftProtobuf.UnknownStorage() - enum ServingStatus: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int + package enum ServingStatus: SwiftProtobuf.Enum, Swift.CaseIterable { + package typealias RawValue = Int case unknown // = 0 case serving // = 1 case notServing // = 2 @@ -68,11 +68,11 @@ struct Grpc_Health_V1_HealthCheckResponse: Sendable { case serviceUnknown // = 3 case UNRECOGNIZED(Int) - init() { + package init() { self = .unknown } - init?(rawValue: Int) { + package init?(rawValue: Int) { switch rawValue { case 0: self = .unknown case 1: self = .serving @@ -82,7 +82,7 @@ struct Grpc_Health_V1_HealthCheckResponse: Sendable { } } - var rawValue: Int { + package var rawValue: Int { switch self { case .unknown: return 0 case .serving: return 1 @@ -93,7 +93,7 @@ struct Grpc_Health_V1_HealthCheckResponse: Sendable { } // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ + package static let allCases: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ .unknown, .serving, .notServing, @@ -102,7 +102,7 @@ struct Grpc_Health_V1_HealthCheckResponse: Sendable { } - init() {} + package init() {} } // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -110,12 +110,12 @@ struct Grpc_Health_V1_HealthCheckResponse: Sendable { fileprivate let _protobuf_package = "grpc.health.v1" extension Grpc_Health_V1_HealthCheckRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HealthCheckRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + package static let protoMessageName: String = _protobuf_package + ".HealthCheckRequest" + package static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "service"), ] - mutating func decodeMessage(decoder: inout D) throws { + package mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are @@ -127,14 +127,14 @@ extension Grpc_Health_V1_HealthCheckRequest: SwiftProtobuf.Message, SwiftProtobu } } - func traverse(visitor: inout V) throws { + package func traverse(visitor: inout V) throws { if !self.service.isEmpty { try visitor.visitSingularStringField(value: self.service, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_Health_V1_HealthCheckRequest, rhs: Grpc_Health_V1_HealthCheckRequest) -> Bool { + package static func ==(lhs: Grpc_Health_V1_HealthCheckRequest, rhs: Grpc_Health_V1_HealthCheckRequest) -> Bool { if lhs.service != rhs.service {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -142,12 +142,12 @@ extension Grpc_Health_V1_HealthCheckRequest: SwiftProtobuf.Message, SwiftProtobu } extension Grpc_Health_V1_HealthCheckResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HealthCheckResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + package static let protoMessageName: String = _protobuf_package + ".HealthCheckResponse" + package static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "status"), ] - mutating func decodeMessage(decoder: inout D) throws { + package mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { // The use of inline closures is to circumvent an issue where the compiler // allocates stack space for every case branch when no optimizations are @@ -159,14 +159,14 @@ extension Grpc_Health_V1_HealthCheckResponse: SwiftProtobuf.Message, SwiftProtob } } - func traverse(visitor: inout V) throws { + package func traverse(visitor: inout V) throws { if self.status != .unknown { try visitor.visitSingularEnumField(value: self.status, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: Grpc_Health_V1_HealthCheckResponse, rhs: Grpc_Health_V1_HealthCheckResponse) -> Bool { + package static func ==(lhs: Grpc_Health_V1_HealthCheckResponse, rhs: Grpc_Health_V1_HealthCheckResponse) -> Bool { if lhs.status != rhs.status {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -174,7 +174,7 @@ extension Grpc_Health_V1_HealthCheckResponse: SwiftProtobuf.Message, SwiftProtob } extension Grpc_Health_V1_HealthCheckResponse.ServingStatus: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + package static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 0: .same(proto: "UNKNOWN"), 1: .same(proto: "SERVING"), 2: .same(proto: "NOT_SERVING"), diff --git a/Sources/Services/Health/Health.swift b/Sources/Services/Health/Health.swift new file mode 100644 index 000000000..f0cdb1a34 --- /dev/null +++ b/Sources/Services/Health/Health.swift @@ -0,0 +1,127 @@ +/* + * 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 + +/// ``Health`` is gRPC’s mechanism for checking whether a server is able to handle RPCs. Its semantics are documented in +/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. +/// +/// `Health` initializes a new ``Health/Service-swift.struct`` and ``Health/Provider-swift.struct``. +/// - `Health.Service` implements the Health service from the `grpc.health.v1` package and can be registered with a server +/// like any other service. +/// - `Health.Provider` provides status updates to `Health.Service`. `Health.Service` doesn't know about the other +/// services running on a server so it must be provided with status updates via `Health.Provider`. To make specifying the service +/// being updated easier, the generated code for services includes an extension to `ServiceDescriptor`. +/// +/// The following shows an example of initializing a Health service and updating the status of the `Foo` service in the `bar` package. +/// +/// ```swift +/// let health = Health() +/// let server = GRPCServer( +/// transport: transport, +/// services: [health.service, FooService()] +/// ) +/// +/// health.provider.updateStatus( +/// .serving, +/// forService: .bar_Foo +/// ) +/// ``` +@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +public struct Health: Sendable { + /// An implementation of the `grpc.health.v1.Health` service. + public let service: Health.Service + + /// Provides status updates to the Health service. + public let provider: Health.Provider + + /// Constructs a new ``Health``, initializing a ``Health/Service-swift.struct`` and a + /// ``Health/Provider-swift.struct``. + public init() { + let healthService = HealthService() + + self.service = Health.Service(healthService: healthService) + self.provider = Health.Provider(healthService: healthService) + } +} + +@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Health { + /// An implementation of the `grpc.health.v1.Health` service. + public struct Service: RegistrableRPCService, Sendable { + private let healthService: HealthService + + public func registerMethods(with router: inout RPCRouter) { + self.healthService.registerMethods(with: &router) + } + + fileprivate init(healthService: HealthService) { + self.healthService = healthService + } + } + + /// Provides status updates to ``Health/Service-swift.struct``. + public struct Provider: Sendable { + private let healthService: HealthService + + /// Updates the status of a service. + /// + /// - Parameters: + /// - status: The status of the service. + /// - service: The description of the service. + public func updateStatus( + _ status: ServingStatus, + forService service: ServiceDescriptor + ) { + self.healthService.updateStatus( + Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), + forService: service.fullyQualifiedService + ) + } + + /// Updates the status of a service. + /// + /// - Parameters: + /// - status: The status of the service. + /// - service: The fully qualified service name in the format: + /// - "package.service": if the service is part of a package. For example, "helloworld.Greeter". + /// - "service": if the service is not part of a package. For example, "Greeter". + public func updateStatus( + _ status: ServingStatus, + forService service: String + ) { + self.healthService.updateStatus( + Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), + forService: service + ) + } + + fileprivate init(healthService: HealthService) { + self.healthService = healthService + } + } +} + +extension Grpc_Health_V1_HealthCheckResponse.ServingStatus { + package init(_ status: ServingStatus) { + switch status.value { + case .serving: + self = .serving + case .notServing: + self = .notServing + } + } +} diff --git a/Sources/Services/Health/HealthService.swift b/Sources/Services/Health/HealthService.swift new file mode 100644 index 000000000..b8b54e80c --- /dev/null +++ b/Sources/Services/Health/HealthService.swift @@ -0,0 +1,132 @@ +/* + * 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 + +@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { + private let state = HealthService.State() + + func check( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let service = request.message.service + + guard let status = self.state.currentStatus(ofService: service) else { + throw RPCError(code: .notFound, message: "Requested service unknown.") + } + + var response = Grpc_Health_V1_HealthCheckResponse() + response.status = status + + return ServerResponse.Single(message: response) + } + + func watch( + request: ServerRequest.Single + ) async -> ServerResponse.Stream { + let service = request.message.service + let statuses = AsyncStream.makeStream(of: Grpc_Health_V1_HealthCheckResponse.ServingStatus.self) + + self.state.addContinuation(statuses.continuation, forService: service) + + return ServerResponse.Stream( + of: Grpc_Health_V1_HealthCheckResponse.self + ) { writer in + var response = Grpc_Health_V1_HealthCheckResponse() + + for await status in statuses.stream { + response.status = status + try await writer.write(response) + } + + return [:] + } + } + + func updateStatus( + _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, + forService service: String + ) { + self.state.updateStatus(status, forService: service) + } +} + +@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension HealthService { + private struct State: Sendable { + // The state of each service keyed by the fully qualified service name. + private let lockedStorage = LockedValueBox([String: ServiceState]()) + + fileprivate func currentStatus( + ofService service: String + ) -> Grpc_Health_V1_HealthCheckResponse.ServingStatus? { + return self.lockedStorage.withLockedValue { $0[service]?.currentStatus } + } + + fileprivate func updateStatus( + _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, + forService service: String + ) { + self.lockedStorage.withLockedValue { storage in + storage[service, default: ServiceState(status: status)].updateStatus(status) + } + } + + fileprivate func addContinuation( + _ continuation: AsyncStream.Continuation, + forService service: String + ) { + self.lockedStorage.withLockedValue { storage in + storage[service, default: ServiceState(status: .serviceUnknown)] + .addContinuation(continuation) + } + } + } + + // Encapsulates the current status of a service and the continuations of its watch streams. + private struct ServiceState: Sendable { + private(set) var currentStatus: Grpc_Health_V1_HealthCheckResponse.ServingStatus + private var continuations: + [AsyncStream.Continuation] + + fileprivate mutating func updateStatus( + _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus + ) { + guard status != self.currentStatus else { + return + } + + self.currentStatus = status + + for continuation in self.continuations { + continuation.yield(status) + } + } + + fileprivate mutating func addContinuation( + _ continuation: AsyncStream.Continuation + ) { + self.continuations.append(continuation) + continuation.yield(self.currentStatus) + } + + fileprivate init(status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown) { + self.currentStatus = status + self.continuations = [] + } + } +} diff --git a/Sources/Services/Health/ServingStatus.swift b/Sources/Services/Health/ServingStatus.swift new file mode 100644 index 000000000..cc0fd5b15 --- /dev/null +++ b/Sources/Services/Health/ServingStatus.swift @@ -0,0 +1,38 @@ +/* + * 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. + */ + +/// The status of a service. +/// +/// - ``ServingStatus/serving`` indicates that a service is healthy. +/// - ``ServingStatus/notServing`` indicates that a service is unhealthy. +public struct ServingStatus: Sendable, Hashable { + internal enum Value: Sendable, Hashable { + case serving + case notServing + } + + /// A status indicating that a service is healthy. + public static let serving = ServingStatus(.serving) + + /// A status indicating that a service unhealthy. + public static let notServing = ServingStatus(.notServing) + + internal var value: Value + + private init(_ value: Value) { + self.value = value + } +} diff --git a/Tests/Services/HealthTests/HealthTests.swift b/Tests/Services/HealthTests/HealthTests.swift new file mode 100644 index 000000000..8e8517b0c --- /dev/null +++ b/Tests/Services/HealthTests/HealthTests.swift @@ -0,0 +1,287 @@ +/* + * 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 GRPCHealth +import GRPCInProcessTransport +import XCTest + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class HealthTests: XCTestCase { + private func withHealthClient( + _ body: @Sendable (Grpc_Health_V1_HealthClient, Health.Provider) async throws -> Void + ) async throws { + let health = Health() + let inProcess = InProcessTransport.makePair() + let server = GRPCServer(transport: inProcess.server, services: [health.service]) + let client = GRPCClient(transport: inProcess.client) + let healthClient = Grpc_Health_V1_HealthClient(client: client) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await server.run() + } + + group.addTask { + try await client.run() + } + + do { + try await body(healthClient, health.provider) + } catch { + XCTFail("Unexpected error: \(error)") + } + + group.cancelAll() + } + } + + func testCheckOnKnownService() async throws { + try await withHealthClient { (healthClient, healthProvider) in + let testServiceDescriptor = ServiceDescriptor.testService + + healthProvider.updateStatus(.serving, forService: testServiceDescriptor) + + let message = Grpc_Health_V1_HealthCheckRequest.with { + $0.service = testServiceDescriptor.fullyQualifiedService + } + + try await healthClient.check(request: ClientRequest.Single(message: message)) { response in + try XCTAssertEqual(response.message.status, .serving) + } + } + } + + func testCheckOnUnknownService() async throws { + try await withHealthClient { (healthClient, healthProvider) in + let message = Grpc_Health_V1_HealthCheckRequest.with { + $0.service = "does.not.Exist" + } + + try await healthClient.check(request: ClientRequest.Single(message: message)) { response in + try XCTAssertThrowsError(ofType: RPCError.self, response.message) { error in + XCTAssertEqual(error.code, .notFound) + } + } + } + } + + func testCheckOnServer() async throws { + try await withHealthClient { (healthClient, healthProvider) in + // An unspecified service refers to the server. + healthProvider.updateStatus(.notServing, forService: "") + + let message = Grpc_Health_V1_HealthCheckRequest() + + try await healthClient.check(request: ClientRequest.Single(message: message)) { response in + try XCTAssertEqual(response.message.status, .notServing) + } + } + } + + func testWatchOnKnownService() async throws { + try await withHealthClient { (healthClient, healthProvider) in + let testServiceDescriptor = ServiceDescriptor.testService + + let statusesToBeSent: [ServingStatus] = [.serving, .notServing, .serving] + + // Before watching the service, make the status of the service known to the Health service. + healthProvider.updateStatus(statusesToBeSent[0], forService: testServiceDescriptor) + + let message = Grpc_Health_V1_HealthCheckRequest.with { + $0.service = testServiceDescriptor.fullyQualifiedService + } + + try await healthClient.watch(request: ClientRequest.Single(message: message)) { response in + var responseStreamIterator = response.messages.makeAsyncIterator() + + for i in 0 ..< statusesToBeSent.count { + let next = try await responseStreamIterator.next() + let message = try XCTUnwrap(next) + let expectedStatus = Grpc_Health_V1_HealthCheckResponse.ServingStatus(statusesToBeSent[i]) + + XCTAssertEqual(message.status, expectedStatus) + + if i < statusesToBeSent.count - 1 { + healthProvider.updateStatus(statusesToBeSent[i + 1], forService: testServiceDescriptor) + } + } + } + } + } + + func testWatchOnUnknownServiceDoesNotTerminateTheRPC() async throws { + try await withHealthClient { (healthClient, healthProvider) in + let testServiceDescriptor = ServiceDescriptor.testService + + let message = Grpc_Health_V1_HealthCheckRequest.with { + $0.service = testServiceDescriptor.fullyQualifiedService + } + + try await healthClient.watch(request: ClientRequest.Single(message: message)) { response in + var responseStreamIterator = response.messages.makeAsyncIterator() + var next = try await responseStreamIterator.next() + var message = try XCTUnwrap(next) + + // As the service was watched before being updated, the first status received should be + // .serviceUnknown. + XCTAssertEqual(message.status, .serviceUnknown) + + healthProvider.updateStatus(.notServing, forService: testServiceDescriptor) + + next = try await responseStreamIterator.next() + message = try XCTUnwrap(next) + + // The RPC was not terminated and a status update was received successfully. + XCTAssertEqual(message.status, .notServing) + } + } + } + + func testMultipleWatchOnTheSameService() async throws { + try await withHealthClient { (healthClient, healthProvider) in + let testServiceDescriptor = ServiceDescriptor.testService + + let statusesToBeSent: [ServingStatus] = [.serving, .notServing, .serving] + + try await withThrowingTaskGroup( + of: [Grpc_Health_V1_HealthCheckResponse.ServingStatus].self + ) { group in + let message = Grpc_Health_V1_HealthCheckRequest.with { + $0.service = testServiceDescriptor.fullyQualifiedService + } + + // The continuation of this stream will be used to signal when the watch response streams + // are up and ready. + let signal = AsyncStream.makeStream(of: Void.self) + let numberOfWatches = 2 + + for _ in 0 ..< numberOfWatches { + group.addTask { + return try await healthClient.watch( + request: ClientRequest.Single(message: message) + ) { response in + signal.continuation.yield() // Make signal + + var statuses = [Grpc_Health_V1_HealthCheckResponse.ServingStatus]() + var responseStreamIterator = response.messages.makeAsyncIterator() + + // Since responseStreamIterator.next() will never be nil (ideally, as the response + // stream is always open), the iteration cannot be based on when + // responseStreamIterator.next() is nil. Else, the iteration infinitely awaits and the + // test never finishes. Hence, it is based on the expected number of statuses to be + // received. + for _ in 0 ..< statusesToBeSent.count + 1 { + // As the service will be watched before being updated, the first status received + // should be .serviceUnknown. Hence, the range of this iteration is increased by 1. + + let next = try await responseStreamIterator.next() + let message = try XCTUnwrap(next) + statuses.append(message.status) + } + + return statuses + } + } + } + + // Wait until all the watch streams are up and ready. + for await _ in signal.stream.prefix(numberOfWatches) {} + + for status in statusesToBeSent { + healthProvider.updateStatus(status, forService: testServiceDescriptor) + } + + for try await receivedStatuses in group { + XCTAssertEqual(receivedStatuses[0], .serviceUnknown) + + for i in 0 ..< statusesToBeSent.count { + let sentStatus = Grpc_Health_V1_HealthCheckResponse.ServingStatus(statusesToBeSent[i]) + XCTAssertEqual(sentStatus, receivedStatuses[i + 1]) + } + } + } + } + } + + func testWatchWithUnchangingStatusUpdates() async throws { + try await withHealthClient { (healthClient, healthProvider) in + let testServiceDescriptor = ServiceDescriptor.testService + + let statusesToBeSent: [ServingStatus] = [.notServing, .notServing, .notServing, .serving] + + // The repeated .notServing updates should be received only once. Also, as the service will + // be watched before being updated, the first status received should be .serviceUnknown. + let expectedStatuses: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ + .serviceUnknown, + .notServing, + .serving, + ] + + let message = Grpc_Health_V1_HealthCheckRequest.with { + $0.service = testServiceDescriptor.fullyQualifiedService + } + + try await healthClient.watch( + request: ClientRequest.Single(message: message) + ) { response in + // Send all status updates. + for status in statusesToBeSent { + healthProvider.updateStatus(status, forService: testServiceDescriptor) + } + + var responseStreamIterator = response.messages.makeAsyncIterator() + + for i in 0 ..< expectedStatuses.count { + let next = try await responseStreamIterator.next() + let message = try XCTUnwrap(next) + + XCTAssertEqual(message.status, expectedStatuses[i]) + } + } + } + } + + func testWatchOnServer() async throws { + try await withHealthClient { (healthClient, healthProvider) in + let statusesToBeSent: [ServingStatus] = [.serving, .notServing, .serving] + + // An unspecified service refers to the server. + healthProvider.updateStatus(statusesToBeSent[0], forService: "") + + let message = Grpc_Health_V1_HealthCheckRequest() + + try await healthClient.watch(request: ClientRequest.Single(message: message)) { response in + var responseStreamIterator = response.messages.makeAsyncIterator() + + for i in 0 ..< statusesToBeSent.count { + let next = try await responseStreamIterator.next() + let message = try XCTUnwrap(next) + let expectedStatus = Grpc_Health_V1_HealthCheckResponse.ServingStatus(statusesToBeSent[i]) + + XCTAssertEqual(message.status, expectedStatus) + + if i < statusesToBeSent.count - 1 { + healthProvider.updateStatus(statusesToBeSent[i + 1], forService: "") + } + } + } + } + } +} + +extension ServiceDescriptor { + fileprivate static let testService = ServiceDescriptor(package: "test", service: "Service") +} diff --git a/Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift b/Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift new file mode 100644 index 000000000..3848388bc --- /dev/null +++ b/Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift @@ -0,0 +1,30 @@ +/* + * 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 XCTest + +func XCTAssertThrowsError( + ofType: E.Type, + _ expression: @autoclosure () throws -> T, + _ errorHandler: (E) -> Void +) { + XCTAssertThrowsError(try expression()) { error in + guard let error = error as? E else { + return XCTFail("Error had unexpected type '\(type(of: error))'") + } + errorHandler(error) + } +} From 426785c8954942bd666f400842e2595c8aa19f3d Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:22:25 +0100 Subject: [PATCH 419/580] Update generated code to use service descriptors (#2004) Motivation: The generated code refers to services literally using strings. With the addition of service descriptors, the generated code can now utilize them to refer to services. Modifications: - Update the generated code to use service descriptors. - For each service in the generated code, extend `ServiceDescriptor` to include a `static` property that holds the service descriptor for that service. (I.e. for a service `Foo` in the `bar` package, the service descriptor for `Foo` can be accessed using `ServiceDescriptor.bar_Foo`.) Result: The generated code will utilize and provide easy access to service descriptors. --- .../v2/Echo/Generated/echo.grpc.swift | 18 ++- .../GRPCCodeGen/CodeGenerationRequest.swift | 9 ++ .../Translator/TypealiasTranslator.swift | 151 +++++++++++++++--- .../Generated/empty_service.grpc.swift | 8 + .../Generated/test.grpc.swift | 46 ++++-- .../Health/Generated/health.grpc.swift | 12 +- .../grpc_testing_benchmark_service.grpc.swift | 18 ++- .../grpc_testing_worker_service.grpc.swift | 16 +- ...uredSwiftTranslatorSnippetBasedTests.swift | 8 + ...TypealiasTranslatorSnippetBasedTests.swift | 120 +++++++++++++- .../Generated/control.grpc.swift | 16 +- .../ProtobufCodeGeneratorTests.swift | 30 +++- 12 files changed, 389 insertions(+), 63 deletions(-) diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index 175f6f5a3..55e282dcf 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -25,12 +25,13 @@ import GRPCCore import GRPCProtobuf internal enum Echo_Echo { + internal static let descriptor = ServiceDescriptor.echo_Echo internal enum Method { internal enum Get { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse internal static let descriptor = MethodDescriptor( - service: "echo.Echo", + service: Echo_Echo.descriptor.fullyQualifiedService, method: "Get" ) } @@ -38,7 +39,7 @@ internal enum Echo_Echo { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse internal static let descriptor = MethodDescriptor( - service: "echo.Echo", + service: Echo_Echo.descriptor.fullyQualifiedService, method: "Expand" ) } @@ -46,7 +47,7 @@ internal enum Echo_Echo { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse internal static let descriptor = MethodDescriptor( - service: "echo.Echo", + service: Echo_Echo.descriptor.fullyQualifiedService, method: "Collect" ) } @@ -54,7 +55,7 @@ internal enum Echo_Echo { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse internal static let descriptor = MethodDescriptor( - service: "echo.Echo", + service: Echo_Echo.descriptor.fullyQualifiedService, method: "Update" ) } @@ -75,6 +76,13 @@ internal enum Echo_Echo { internal typealias Client = Echo_EchoClient } +extension ServiceDescriptor { + internal static let echo_Echo = Self( + package: "echo", + service: "Echo" + ) +} + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Immediately returns an echo of a request. @@ -341,4 +349,4 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { handler: body ) } -} +} \ No newline at end of file diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 0c0abb1b9..1eb24780a 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -321,3 +321,12 @@ public struct CodeGenerationRequest { } } } + +extension CodeGenerationRequest.Name { + /// The base name replacing occurrences of "." with "_". + /// + /// For example, if `base` is "Foo.Bar", then `normalizedBase` is "Foo_Bar". + public var normalizedBase: String { + return self.base.replacingOccurrences(of: ".", with: "_") + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 8c689aebc..26d261717 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -21,35 +21,48 @@ /// For example, in the case of the ``Echo`` service, the ``TypealiasTranslator`` will create /// a representation for the following generated code: /// ```swift -/// public enum Echo { -/// public enum Echo { -/// public enum Method { -/// public enum Get { -/// public typealias Input = Echo_EchoRequest -/// public typealias Output = Echo_EchoResponse -/// public static let descriptor = MethodDescriptor(service: "echo.Echo", method: "Get") -/// } +/// public enum Echo_Echo { +/// public static let descriptor = ServiceDescriptor.echo_Echo /// -/// public enum Collect { -/// public typealias Input = Echo_EchoRequest -/// public typealias Output = Echo_EchoResponse -/// public static let descriptor = MethodDescriptor(service: "echo.Echo", method: "Collect") -/// } -/// // ... -/// -/// public static let descriptors: [MethodDescriptor] = [ -/// Get.descriptor, -/// Collect.descriptor, -/// // ... -/// ] +/// public enum Method { +/// public enum Get { +/// public typealias Input = Echo_EchoRequest +/// public typealias Output = Echo_EchoResponse +/// public static let descriptor = MethodDescriptor( +/// service: Echo_Echo.descriptor.fullyQualifiedService, +/// method: "Get" +/// ) /// } /// -/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// public typealias StreamingServiceProtocol = echo_EchoServiceStreamingProtocol -/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// public typealias ServiceProtocol = echo_EchoServiceProtocol +/// public enum Collect { +/// public typealias Input = Echo_EchoRequest +/// public typealias Output = Echo_EchoResponse +/// public static let descriptor = MethodDescriptor( +/// service: Echo_Echo.descriptor.fullyQualifiedService, +/// method: "Collect" +/// ) +/// } +/// // ... /// +/// public static let descriptors: [MethodDescriptor] = [ +/// Get.descriptor, +/// Collect.descriptor, +/// // ... +/// ] /// } +/// +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +/// public typealias StreamingServiceProtocol = Echo_EchoServiceStreamingProtocol +/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +/// public typealias ServiceProtocol = Echo_EchoServiceProtocol +/// +/// } +/// +/// extension ServiceDescriptor { +/// public static let echo_Echo = Self( +/// package: "echo", +/// service: "Echo" +/// ) /// } /// ``` /// @@ -82,6 +95,10 @@ struct TypealiasTranslator: SpecializedTranslator { item: .declaration(try self.makeServiceEnum(from: service, named: generatedEnumName)) ) ) + + codeBlocks.append( + CodeBlock(item: .declaration(self.makeServiceDescriptorExtension(for: service))) + ) } } @@ -111,6 +128,10 @@ extension TypealiasTranslator { let methodDescriptorsDeclaration = self.makeMethodDescriptors(for: service) methodsEnum.members.append(methodDescriptorsDeclaration) + // Create the static service descriptor property. + let staticServiceDescriptorProperty = self.makeStaticServiceDescriptorProperty(for: service) + + serviceEnum.members.append(.variable(staticServiceDescriptorProperty)) serviceEnum.members.append(.enum(methodsEnum)) if self.server { @@ -165,6 +186,16 @@ extension TypealiasTranslator { from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor ) -> Declaration { + let fullyQualifiedService = MemberAccessDescription( + left: .memberAccess( + MemberAccessDescription( + left: .identifierType(.member([service.namespacedGeneratedName])), + right: "descriptor" + ) + ), + right: "fullyQualifiedService" + ) + let descriptorDeclarationLeft = Expression.identifier(.pattern("descriptor")) let descriptorDeclarationRight = Expression.functionCall( FunctionCallDescription( @@ -172,7 +203,7 @@ extension TypealiasTranslator { arguments: [ FunctionArgumentDescription( label: "service", - expression: .literal(service.fullyQualifiedName) + expression: .memberAccess(fullyQualifiedService) ), FunctionArgumentDescription( label: "method", @@ -270,4 +301,74 @@ extension TypealiasTranslator { ) ) } + + private func makeServiceIdentifier(_ service: CodeGenerationRequest.ServiceDescriptor) -> String { + let prefix: String + + if service.namespace.normalizedBase.isEmpty { + prefix = "" + } else { + prefix = service.namespace.normalizedBase + "_" + } + + return prefix + service.name.normalizedBase + } + + private func makeStaticServiceDescriptorProperty( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> VariableDescription { + let serviceIdentifier = makeServiceIdentifier(service) + + return VariableDescription( + accessModifier: self.accessModifier, + isStatic: true, + kind: .let, + left: .identifierPattern("descriptor"), + right: .memberAccess( + MemberAccessDescription( + left: .identifierPattern("ServiceDescriptor"), + right: serviceIdentifier + ) + ) + ) + } + + private func makeServiceDescriptorExtension( + for service: CodeGenerationRequest.ServiceDescriptor + ) -> Declaration { + let serviceIdentifier = makeServiceIdentifier(service) + + let serviceDescriptorInitialization = Expression.functionCall( + FunctionCallDescription( + calledExpression: .identifierType(.member("Self")), + arguments: [ + FunctionArgumentDescription( + label: "package", + expression: .literal(service.namespace.base) + ), + FunctionArgumentDescription( + label: "service", + expression: .literal(service.name.base) + ), + ] + ) + ) + + return .extension( + ExtensionDescription( + onType: "ServiceDescriptor", + declarations: [ + .variable( + VariableDescription( + accessModifier: self.accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern(serviceIdentifier)), + right: serviceDescriptorInitialization + ) + ) + ] + ) + ) + } } diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift index 81c4c40e5..cabc1fbeb 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -25,6 +25,7 @@ import GRPCCore import GRPCProtobuf public enum Grpc_Testing_EmptyService { + public static let descriptor = ServiceDescriptor.grpc_testing_EmptyService public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -38,6 +39,13 @@ public enum Grpc_Testing_EmptyService { public typealias Client = Grpc_Testing_EmptyServiceClient } +extension ServiceDescriptor { + public static let grpc_testing_EmptyService = Self( + package: "grpc.testing", + service: "EmptyService" + ) +} + /// A service that has zero methods. /// See https://github.com/grpc/grpc/issues/15574 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index 8cbff1728..365ea5c8d 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -28,12 +28,13 @@ import GRPCCore import GRPCProtobuf public enum Grpc_Testing_ReconnectService { + public static let descriptor = ServiceDescriptor.grpc_testing_ReconnectService public enum Method { public enum Start { public typealias Input = Grpc_Testing_ReconnectParams public typealias Output = Grpc_Testing_Empty public static let descriptor = MethodDescriptor( - service: "grpc.testing.ReconnectService", + service: Grpc_Testing_ReconnectService.descriptor.fullyQualifiedService, method: "Start" ) } @@ -41,7 +42,7 @@ public enum Grpc_Testing_ReconnectService { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_ReconnectInfo public static let descriptor = MethodDescriptor( - service: "grpc.testing.ReconnectService", + service: Grpc_Testing_ReconnectService.descriptor.fullyQualifiedService, method: "Stop" ) } @@ -60,13 +61,21 @@ public enum Grpc_Testing_ReconnectService { public typealias Client = Grpc_Testing_ReconnectServiceClient } +extension ServiceDescriptor { + public static let grpc_testing_ReconnectService = Self( + package: "grpc.testing", + service: "ReconnectService" + ) +} + public enum Grpc_Testing_TestService { + public static let descriptor = ServiceDescriptor.grpc_testing_TestService public enum Method { public enum EmptyCall { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_Empty public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "EmptyCall" ) } @@ -74,7 +83,7 @@ public enum Grpc_Testing_TestService { public typealias Input = Grpc_Testing_SimpleRequest public typealias Output = Grpc_Testing_SimpleResponse public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "UnaryCall" ) } @@ -82,7 +91,7 @@ public enum Grpc_Testing_TestService { public typealias Input = Grpc_Testing_SimpleRequest public typealias Output = Grpc_Testing_SimpleResponse public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "CacheableUnaryCall" ) } @@ -90,7 +99,7 @@ public enum Grpc_Testing_TestService { public typealias Input = Grpc_Testing_StreamingOutputCallRequest public typealias Output = Grpc_Testing_StreamingOutputCallResponse public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "StreamingOutputCall" ) } @@ -98,7 +107,7 @@ public enum Grpc_Testing_TestService { public typealias Input = Grpc_Testing_StreamingInputCallRequest public typealias Output = Grpc_Testing_StreamingInputCallResponse public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "StreamingInputCall" ) } @@ -106,7 +115,7 @@ public enum Grpc_Testing_TestService { public typealias Input = Grpc_Testing_StreamingOutputCallRequest public typealias Output = Grpc_Testing_StreamingOutputCallResponse public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "FullDuplexCall" ) } @@ -114,7 +123,7 @@ public enum Grpc_Testing_TestService { public typealias Input = Grpc_Testing_StreamingOutputCallRequest public typealias Output = Grpc_Testing_StreamingOutputCallResponse public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "HalfDuplexCall" ) } @@ -122,7 +131,7 @@ public enum Grpc_Testing_TestService { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_Empty public static let descriptor = MethodDescriptor( - service: "grpc.testing.TestService", + service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "UnimplementedCall" ) } @@ -147,13 +156,21 @@ public enum Grpc_Testing_TestService { public typealias Client = Grpc_Testing_TestServiceClient } +extension ServiceDescriptor { + public static let grpc_testing_TestService = Self( + package: "grpc.testing", + service: "TestService" + ) +} + public enum Grpc_Testing_UnimplementedService { + public static let descriptor = ServiceDescriptor.grpc_testing_UnimplementedService public enum Method { public enum UnimplementedCall { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_Empty public static let descriptor = MethodDescriptor( - service: "grpc.testing.UnimplementedService", + service: Grpc_Testing_UnimplementedService.descriptor.fullyQualifiedService, method: "UnimplementedCall" ) } @@ -171,6 +188,13 @@ public enum Grpc_Testing_UnimplementedService { public typealias Client = Grpc_Testing_UnimplementedServiceClient } +extension ServiceDescriptor { + public static let grpc_testing_UnimplementedService = Self( + package: "grpc.testing", + service: "UnimplementedService" + ) +} + /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 05c4387b4..530620577 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -28,12 +28,13 @@ import GRPCCore import GRPCProtobuf package enum Grpc_Health_V1_Health { + package static let descriptor = ServiceDescriptor.grpc_health_v1_Health package enum Method { package enum Check { package typealias Input = Grpc_Health_V1_HealthCheckRequest package typealias Output = Grpc_Health_V1_HealthCheckResponse package static let descriptor = MethodDescriptor( - service: "grpc.health.v1.Health", + service: Grpc_Health_V1_Health.descriptor.fullyQualifiedService, method: "Check" ) } @@ -41,7 +42,7 @@ package enum Grpc_Health_V1_Health { package typealias Input = Grpc_Health_V1_HealthCheckRequest package typealias Output = Grpc_Health_V1_HealthCheckResponse package static let descriptor = MethodDescriptor( - service: "grpc.health.v1.Health", + service: Grpc_Health_V1_Health.descriptor.fullyQualifiedService, method: "Watch" ) } @@ -60,6 +61,13 @@ package enum Grpc_Health_V1_Health { package typealias Client = Grpc_Health_V1_HealthClient } +extension ServiceDescriptor { + package static let grpc_health_v1_Health = Self( + package: "grpc.health.v1", + service: "Health" + ) +} + /// Health is gRPC's mechanism for checking whether a server is able to handle /// RPCs. Its semantics are documented in /// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 315c57233..50f361264 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -28,12 +28,13 @@ import GRPCCore import GRPCProtobuf internal enum Grpc_Testing_BenchmarkService { + internal static let descriptor = ServiceDescriptor.grpc_testing_BenchmarkService internal enum Method { internal enum UnaryCall { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse internal static let descriptor = MethodDescriptor( - service: "grpc.testing.BenchmarkService", + service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "UnaryCall" ) } @@ -41,7 +42,7 @@ internal enum Grpc_Testing_BenchmarkService { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse internal static let descriptor = MethodDescriptor( - service: "grpc.testing.BenchmarkService", + service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingCall" ) } @@ -49,7 +50,7 @@ internal enum Grpc_Testing_BenchmarkService { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse internal static let descriptor = MethodDescriptor( - service: "grpc.testing.BenchmarkService", + service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingFromClient" ) } @@ -57,7 +58,7 @@ internal enum Grpc_Testing_BenchmarkService { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse internal static let descriptor = MethodDescriptor( - service: "grpc.testing.BenchmarkService", + service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingFromServer" ) } @@ -65,7 +66,7 @@ internal enum Grpc_Testing_BenchmarkService { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse internal static let descriptor = MethodDescriptor( - service: "grpc.testing.BenchmarkService", + service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingBothWays" ) } @@ -87,6 +88,13 @@ internal enum Grpc_Testing_BenchmarkService { internal typealias Client = Grpc_Testing_BenchmarkServiceClient } +extension ServiceDescriptor { + internal static let grpc_testing_BenchmarkService = Self( + package: "grpc.testing", + service: "BenchmarkService" + ) +} + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One request followed by one response. diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index 3cfdba799..5b8a1de2b 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -28,12 +28,13 @@ import GRPCCore import GRPCProtobuf internal enum Grpc_Testing_WorkerService { + internal static let descriptor = ServiceDescriptor.grpc_testing_WorkerService internal enum Method { internal enum RunServer { internal typealias Input = Grpc_Testing_ServerArgs internal typealias Output = Grpc_Testing_ServerStatus internal static let descriptor = MethodDescriptor( - service: "grpc.testing.WorkerService", + service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "RunServer" ) } @@ -41,7 +42,7 @@ internal enum Grpc_Testing_WorkerService { internal typealias Input = Grpc_Testing_ClientArgs internal typealias Output = Grpc_Testing_ClientStatus internal static let descriptor = MethodDescriptor( - service: "grpc.testing.WorkerService", + service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "RunClient" ) } @@ -49,7 +50,7 @@ internal enum Grpc_Testing_WorkerService { internal typealias Input = Grpc_Testing_CoreRequest internal typealias Output = Grpc_Testing_CoreResponse internal static let descriptor = MethodDescriptor( - service: "grpc.testing.WorkerService", + service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "CoreCount" ) } @@ -57,7 +58,7 @@ internal enum Grpc_Testing_WorkerService { internal typealias Input = Grpc_Testing_Void internal typealias Output = Grpc_Testing_Void internal static let descriptor = MethodDescriptor( - service: "grpc.testing.WorkerService", + service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "QuitWorker" ) } @@ -74,6 +75,13 @@ internal enum Grpc_Testing_WorkerService { internal typealias ServiceProtocol = Grpc_Testing_WorkerServiceServiceProtocol } +extension ServiceDescriptor { + internal static let grpc_testing_WorkerService = Self( + package: "grpc.testing", + service: "WorkerService" + ) +} + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Start server with specified workload. diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index f66778bbf..a023c6ddc 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -171,6 +171,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { @_spi(Secret) import enum Foo.Bar public enum NamespaceA_ServiceA { + public static let descriptor = ServiceDescriptor.namespaceA_ServiceA public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -180,6 +181,13 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } + extension ServiceDescriptor { + public static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } + /// Documentation for AService @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 1c73deb9a..5040e2454 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -47,12 +47,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { + public static let descriptor = ServiceDescriptor.namespaceA_ServiceA public enum Method { public enum MethodA { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse public static let descriptor = MethodDescriptor( - service: "namespaceA.ServiceA", + service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, method: "MethodA" ) } @@ -69,6 +70,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } + extension ServiceDescriptor { + public static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -94,6 +101,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { + public static let descriptor = ServiceDescriptor.namespaceA_ServiceA public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -106,6 +114,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } + extension ServiceDescriptor { + public static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -131,6 +145,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { + public static let descriptor = ServiceDescriptor.namespaceA_ServiceA public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -139,6 +154,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } + extension ServiceDescriptor { + public static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -164,6 +185,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { + public static let descriptor = ServiceDescriptor.namespaceA_ServiceA public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -172,6 +194,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } + extension ServiceDescriptor { + public static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -197,10 +225,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { + public static let descriptor = ServiceDescriptor.namespaceA_ServiceA public enum Method { public static let descriptors: [MethodDescriptor] = [] } } + extension ServiceDescriptor { + public static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -230,12 +265,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum ServiceA { + public static let descriptor = ServiceDescriptor.ServiceA public enum Method { public enum MethodA { public typealias Input = ServiceARequest public typealias Output = ServiceAResponse public static let descriptor = MethodDescriptor( - service: "ServiceA", + service: ServiceA.descriptor.fullyQualifiedService, method: "MethodA" ) } @@ -252,6 +288,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = ServiceAClient } + extension ServiceDescriptor { + public static let ServiceA = Self( + package: "", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -293,12 +335,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { + public static let descriptor = ServiceDescriptor.namespaceA_ServiceA public enum Method { public enum MethodA { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse public static let descriptor = MethodDescriptor( - service: "namespaceA.ServiceA", + service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, method: "MethodA" ) } @@ -306,7 +349,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse public static let descriptor = MethodDescriptor( - service: "namespaceA.ServiceA", + service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, method: "MethodB" ) } @@ -324,6 +367,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } + extension ServiceDescriptor { + public static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -349,6 +398,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ package enum NamespaceA_ServiceA { + package static let descriptor = ServiceDescriptor.namespaceA_ServiceA package enum Method { package static let descriptors: [MethodDescriptor] = [] } @@ -361,6 +411,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = NamespaceA_ServiceAClient } + extension ServiceDescriptor { + package static let namespaceA_ServiceA = Self( + package: "namespaceA", + service: "ServiceA" + ) + } """ try self.assertTypealiasTranslation( @@ -398,6 +454,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_Aservice { + public static let descriptor = ServiceDescriptor.namespaceA_AService public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -410,7 +467,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_AserviceClient } + extension ServiceDescriptor { + public static let namespaceA_AService = Self( + package: "namespaceA", + service: "AService" + ) + } public enum NamespaceA_Bservice { + public static let descriptor = ServiceDescriptor.namespaceA_BService public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -423,6 +487,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_BserviceClient } + extension ServiceDescriptor { + public static let namespaceA_BService = Self( + package: "namespaceA", + service: "BService" + ) + } """ try self.assertTypealiasTranslation( @@ -452,6 +522,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ package enum AService { + package static let descriptor = ServiceDescriptor.AService package enum Method { package static let descriptors: [MethodDescriptor] = [] } @@ -464,7 +535,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = AServiceClient } + extension ServiceDescriptor { + package static let AService = Self( + package: "", + service: "AService" + ) + } package enum BService { + package static let descriptor = ServiceDescriptor.BService package enum Method { package static let descriptors: [MethodDescriptor] = [] } @@ -477,6 +555,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = BServiceClient } + extension ServiceDescriptor { + package static let BService = Self( + package: "", + service: "BService" + ) + } """ try self.assertTypealiasTranslation( @@ -514,6 +598,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ internal enum Anamespace_AService { + internal static let descriptor = ServiceDescriptor.anamespace_AService internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } @@ -526,7 +611,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Anamespace_AServiceClient } + extension ServiceDescriptor { + internal static let anamespace_AService = Self( + package: "anamespace", + service: "AService" + ) + } internal enum Bnamespace_BService { + internal static let descriptor = ServiceDescriptor.bnamespace_BService internal enum Method { internal static let descriptors: [MethodDescriptor] = [] } @@ -539,6 +631,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Bnamespace_BServiceClient } + extension ServiceDescriptor { + internal static let bnamespace_BService = Self( + package: "bnamespace", + service: "BService" + ) + } """ try self.assertTypealiasTranslation( @@ -570,6 +668,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum Anamespace_AService { + public static let descriptor = ServiceDescriptor.anamespace_AService public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -582,7 +681,14 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = Anamespace_AServiceClient } + extension ServiceDescriptor { + public static let anamespace_AService = Self( + package: "anamespace", + service: "AService" + ) + } public enum BService { + public static let descriptor = ServiceDescriptor.BService public enum Method { public static let descriptors: [MethodDescriptor] = [] } @@ -595,6 +701,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = BServiceClient } + extension ServiceDescriptor { + public static let BService = Self( + package: "", + service: "BService" + ) + } """ try self.assertTypealiasTranslation( diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index 61e0f85b5..ceaf7d656 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -26,12 +26,13 @@ import GRPCCore import GRPCProtobuf internal enum Control { + internal static let descriptor = ServiceDescriptor.Control internal enum Method { internal enum Unary { internal typealias Input = ControlInput internal typealias Output = ControlOutput internal static let descriptor = MethodDescriptor( - service: "Control", + service: Control.descriptor.fullyQualifiedService, method: "Unary" ) } @@ -39,7 +40,7 @@ internal enum Control { internal typealias Input = ControlInput internal typealias Output = ControlOutput internal static let descriptor = MethodDescriptor( - service: "Control", + service: Control.descriptor.fullyQualifiedService, method: "ServerStream" ) } @@ -47,7 +48,7 @@ internal enum Control { internal typealias Input = ControlInput internal typealias Output = ControlOutput internal static let descriptor = MethodDescriptor( - service: "Control", + service: Control.descriptor.fullyQualifiedService, method: "ClientStream" ) } @@ -55,7 +56,7 @@ internal enum Control { internal typealias Input = ControlInput internal typealias Output = ControlOutput internal static let descriptor = MethodDescriptor( - service: "Control", + service: Control.descriptor.fullyQualifiedService, method: "BidiStream" ) } @@ -76,6 +77,13 @@ internal enum Control { internal typealias Client = ControlClient } +extension ServiceDescriptor { + internal static let Control = Self( + package: "", + service: "Control" + ) +} + /// A controllable service for testing. /// /// The control service has one RPC of each kind, the input to each RPC controls diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 9d847a6fa..fc86ae140 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -60,12 +60,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import ExtraModule internal enum Hello_World_Greeter { + internal static let descriptor = ServiceDescriptor.hello_world_Greeter internal enum Method { internal enum SayHello { internal typealias Input = Hello_World_HelloRequest internal typealias Output = Hello_World_HelloReply internal static let descriptor = MethodDescriptor( - service: "hello.world.Greeter", + service: Hello_World_Greeter.descriptor.fullyQualifiedService, method: "SayHello" ) } @@ -79,6 +80,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal typealias Client = Hello_World_GreeterClient } + extension ServiceDescriptor { + internal static let hello_world_Greeter = Self( + package: "hello.world", + service: "Greeter" + ) + } + /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Hello_World_GreeterClientProtocol: Sendable { @@ -175,12 +183,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import ExtraModule public enum Helloworld_Greeter { + public static let descriptor = ServiceDescriptor.helloworld_Greeter public enum Method { public enum SayHello { public typealias Input = Helloworld_HelloRequest public typealias Output = Helloworld_HelloReply public static let descriptor = MethodDescriptor( - service: "helloworld.Greeter", + service: Helloworld_Greeter.descriptor.fullyQualifiedService, method: "SayHello" ) } @@ -194,6 +203,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol } + extension ServiceDescriptor { + public static let helloworld_Greeter = Self( + package: "helloworld", + service: "Greeter" + ) + } + /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { @@ -270,12 +286,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import ExtraModule package enum Greeter { + package static let descriptor = ServiceDescriptor.Greeter package enum Method { package enum SayHello { package typealias Input = HelloRequest package typealias Output = HelloReply package static let descriptor = MethodDescriptor( - service: "Greeter", + service: Greeter.descriptor.fullyQualifiedService, method: "SayHello" ) } @@ -293,6 +310,13 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package typealias Client = GreeterClient } + extension ServiceDescriptor { + package static let Greeter = Self( + package: "", + service: "Greeter" + ) + } + /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { From cc563a3dbe64e9c52c5093d1553cc5ad70273715 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:31:53 +0100 Subject: [PATCH 420/580] Change label in init of generated client (#2005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: In v2 a generated client stub for the echo service will have the following init: ```swift init(client: GRPCClient) ``` As client stubs are named `_Client`, the call site for the echo service looks like: ```swift let client: GRPCClient = … let echo = Echo_EchoClient(client: client) ``` This reads badly. Modifications: - Change the label from `client:` to `wrapping client:` (since the generated stub wraps the underlying `GRPCClient` to provide the type-safe APIs). Result: The call site will look like: ```swift let client: GRPCClient = … let echo = Echo_EchoClient(wrapping: client) ``` --------- Co-authored-by: George Barnett --- .../v2/Echo/Generated/echo.grpc.swift | 2 +- .../v2/Echo/Subcommands/Collect.swift | 2 +- .../Examples/v2/Echo/Subcommands/Expand.swift | 2 +- .../Examples/v2/Echo/Subcommands/Get.swift | 2 +- .../Examples/v2/Echo/Subcommands/Update.swift | 2 +- .../Translator/ClientCodeTranslator.swift | 6 ++-- .../Generated/empty_service.grpc.swift | 2 +- .../Generated/test.grpc.swift | 6 ++-- .../InteroperabilityTestCases.swift | 28 +++++++++---------- .../performance-worker/BenchmarkClient.swift | 2 +- .../grpc_testing_benchmark_service.grpc.swift | 2 +- ...lientCodeTranslatorSnippetBasedTests.swift | 16 +++++------ .../Generated/control.grpc.swift | 2 +- .../HTTP2TransportTests.swift | 4 +-- .../ProtobufCodeGeneratorTests.swift | 4 +-- 15 files changed, 42 insertions(+), 40 deletions(-) diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index 55e282dcf..d21a7a327 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -274,7 +274,7 @@ extension Echo_Echo.ClientProtocol { internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { private let client: GRPCCore.GRPCClient - internal init(client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Sources/Examples/v2/Echo/Subcommands/Collect.swift b/Sources/Examples/v2/Echo/Subcommands/Collect.swift index 1b799171f..333777a55 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Collect.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Collect.swift @@ -37,7 +37,7 @@ struct Collect: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(client: client) + let echo = Echo_EchoClient(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in diff --git a/Sources/Examples/v2/Echo/Subcommands/Expand.swift b/Sources/Examples/v2/Echo/Subcommands/Expand.swift index 5430bac25..ea17f741f 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Expand.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Expand.swift @@ -37,7 +37,7 @@ struct Expand: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(client: client) + let echo = Echo_EchoClient(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } diff --git a/Sources/Examples/v2/Echo/Subcommands/Get.swift b/Sources/Examples/v2/Echo/Subcommands/Get.swift index 85d56ad50..c37983b8f 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Get.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Get.swift @@ -35,7 +35,7 @@ struct Get: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(client: client) + let echo = Echo_EchoClient(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } diff --git a/Sources/Examples/v2/Echo/Subcommands/Update.swift b/Sources/Examples/v2/Echo/Subcommands/Update.swift index 7ce4b2394..e62fa7ff2 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Update.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Update.swift @@ -37,7 +37,7 @@ struct Update: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(client: client) + let echo = Echo_EchoClient(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index a9ccf4d1a..f7f9cfd08 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -50,7 +50,7 @@ /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public struct Foo_BarClient: Foo_Bar.ClientProtocol { /// private let client: GRPCCore.GRPCClient -/// public init(client: GRPCCore.GRPCClient) { +/// public init(wrapping client: GRPCCore.GRPCClient) { /// self.client = client /// } /// public func methodA( @@ -369,7 +369,9 @@ extension ClientCodeTranslator { signature: .init( accessModifier: self.accessModifier, kind: .initializer, - parameters: [.init(label: "client", type: .member(["GRPCCore", "GRPCClient"]))] + parameters: [ + .init(label: "wrapping", name: "client", type: .member(["GRPCCore", "GRPCClient"])) + ] ), body: [CodeBlock(item: .expression(initializerBody))] ) diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift index cabc1fbeb..c277d42c3 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -83,7 +83,7 @@ extension Grpc_Testing_EmptyService.ClientProtocol { public struct Grpc_Testing_EmptyServiceClient: Grpc_Testing_EmptyService.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } } \ No newline at end of file diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index 365ea5c8d..2ef5fc243 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -682,7 +682,7 @@ extension Grpc_Testing_TestService.ClientProtocol { public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -878,7 +878,7 @@ extension Grpc_Testing_UnimplementedService.ClientProtocol { public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_UnimplementedService.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -957,7 +957,7 @@ extension Grpc_Testing_ReconnectService.ClientProtocol { public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index 4d5902b26..d43e58590 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -34,7 +34,7 @@ import struct Foundation.Data @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EmptyUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) try await testServiceClient.emptyCall( request: ClientRequest.Single(message: Grpc_Testing_Empty()) ) { response in @@ -68,7 +68,7 @@ struct EmptyUnary: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct LargeUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let request = Grpc_Testing_SimpleRequest.with { request in request.responseSize = 314_159 request.payload = Grpc_Testing_Payload.with { @@ -144,7 +144,7 @@ struct LargeUnary: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) class ClientCompressedUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let compressedRequest = Grpc_Testing_SimpleRequest.with { request in request.expectCompressed = .with { $0.value = true } request.responseSize = 314_159 @@ -253,7 +253,7 @@ class ClientCompressedUnary: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) class ServerCompressedUnary: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let compressedRequest = Grpc_Testing_SimpleRequest.with { request in request.responseCompressed = .with { $0.value = true } @@ -343,7 +343,7 @@ class ServerCompressedUnary: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ClientStreaming: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let request = ClientRequest.Stream { writer in for bytes in [27182, 8, 1828, 45904] { let message = Grpc_Testing_StreamingInputCallRequest.with { @@ -394,7 +394,7 @@ struct ClientStreaming: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ServerStreaming: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let responseSizes = [31415, 9, 2653, 58979] let request = Grpc_Testing_StreamingOutputCallRequest.with { request in request.responseParameters = responseSizes.map { @@ -470,7 +470,7 @@ struct ServerStreaming: InteroperabilityTest { class ServerCompressedStreaming: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let request: Grpc_Testing_StreamingOutputCallRequest = .with { request in request.responseParameters = [ .with { @@ -583,7 +583,7 @@ class ServerCompressedStreaming: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct PingPong: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let ids = AsyncStream.makeStream(of: Int.self) let request = ClientRequest.Stream { writer in @@ -650,7 +650,7 @@ struct PingPong: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EmptyStream: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let request = ClientRequest.Stream { _ in } try await testServiceClient.fullDuplexCall(request: request) { response in @@ -722,7 +722,7 @@ struct CustomMetadata: InteroperabilityTest { } func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let unaryRequest = Grpc_Testing_SimpleRequest.with { request in request.responseSize = 314_159 @@ -830,7 +830,7 @@ struct StatusCodeAndMessage: InteroperabilityTest { let expectedMessage = "test status message" func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let message = Grpc_Testing_SimpleRequest.with { $0.responseStatus = Grpc_Testing_EchoStatus.with { @@ -905,7 +905,7 @@ struct StatusCodeAndMessage: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct SpecialStatusMessage: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) let responseMessage = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" let message = Grpc_Testing_SimpleRequest.with { @@ -948,7 +948,7 @@ struct SpecialStatusMessage: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct UnimplementedMethod: InteroperabilityTest { func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(client: client) + let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) try await testServiceClient.unimplementedCall( request: ClientRequest.Single(message: Grpc_Testing_Empty()) ) { response in @@ -981,7 +981,7 @@ struct UnimplementedMethod: InteroperabilityTest { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct UnimplementedService: InteroperabilityTest { func run(client: GRPCClient) async throws { - let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(client: client) + let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(wrapping: client) try await unimplementedServiceClient.unimplementedCall( request: ClientRequest.Single(message: Grpc_Testing_Empty()) ) { response in diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 588055363..1098244d1 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -93,7 +93,7 @@ struct BenchmarkClient { } internal func run() async throws { - let benchmarkClient = Grpc_Testing_BenchmarkServiceClient(client: self.client) + let benchmarkClient = Grpc_Testing_BenchmarkServiceClient(wrapping: self.client) return try await withThrowingTaskGroup(of: Void.self) { clientGroup in // Start the client. clientGroup.addTask { diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 50f361264..73ddaeabf 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -341,7 +341,7 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkService.ClientProtocol { private let client: GRPCCore.GRPCClient - internal init(client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index bd4e5d268..5abb474d4 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -75,7 +75,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -156,7 +156,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -237,7 +237,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -318,7 +318,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -430,7 +430,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - package init(client: GRPCCore.GRPCClient) { + package init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -529,7 +529,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { internal struct ServiceAClient: ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - internal init(client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -594,7 +594,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } } @@ -613,7 +613,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public struct ServiceBClient: ServiceB.ClientProtocol { private let client: GRPCCore.GRPCClient - public init(client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } } diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index ceaf7d656..dbc205faf 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -279,7 +279,7 @@ extension Control.ClientProtocol { internal struct ControlClient: Control.ClientProtocol { private let client: GRPCCore.GRPCClient - internal init(client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 7ffeee209..9b81d7dbe 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -88,7 +88,7 @@ final class HTTP2TransportTests: XCTestCase { } do { - let control = ControlClient(client: client) + let control = ControlClient(wrapping: client) try await execute(control, pair) } catch { XCTFail("Unexpected error: '\(error)' (\(pair))") @@ -123,7 +123,7 @@ final class HTTP2TransportTests: XCTestCase { } do { - let control = ControlClient(client: client) + let control = ControlClient(wrapping: client) try await execute(control, clientKind) } catch { XCTFail("Unexpected error: '\(error)' (\(clientKind))") diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index fc86ae140..6e9ff2bee 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -122,7 +122,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal struct Hello_World_GreeterClient: Hello_World_Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient - internal init(client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -391,7 +391,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package struct GreeterClient: Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient - package init(client: GRPCCore.GRPCClient) { + package init(wrapping client: GRPCCore.GRPCClient) { self.client = client } From 0ce24ad7f9e0b03408de05aefd7312e5876be9e0 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 8 Aug 2024 15:03:05 +0100 Subject: [PATCH 421/580] Update generated health tests (#2008) Motivation: The health tests didn't get updated in cc563a3d. Modifications: Regenerate and fix label Result: Generated code is up-to-date --- Sources/Services/Health/Generated/health.grpc.swift | 2 +- Tests/Services/HealthTests/HealthTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 530620577..6316b5b55 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -258,7 +258,7 @@ extension Grpc_Health_V1_Health.ClientProtocol { package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol { private let client: GRPCCore.GRPCClient - package init(client: GRPCCore.GRPCClient) { + package init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Tests/Services/HealthTests/HealthTests.swift b/Tests/Services/HealthTests/HealthTests.swift index 8e8517b0c..01ccb313c 100644 --- a/Tests/Services/HealthTests/HealthTests.swift +++ b/Tests/Services/HealthTests/HealthTests.swift @@ -27,7 +27,7 @@ final class HealthTests: XCTestCase { let inProcess = InProcessTransport.makePair() let server = GRPCServer(transport: inProcess.server, services: [health.service]) let client = GRPCClient(transport: inProcess.client) - let healthClient = Grpc_Health_V1_HealthClient(client: client) + let healthClient = Grpc_Health_V1_HealthClient(wrapping: client) try await withThrowingDiscardingTaskGroup { group in group.addTask { From 4d0fc0718990ae118f8fabb5d97a45726fd3b67b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 12 Aug 2024 09:49:40 +0100 Subject: [PATCH 422/580] Remove swift lang mode workaround (#2011) Motivation: Xcode 16 Beta 5 has been released which now aligns with swift-6-nightly Linux toolchains in deprecating 'swift language version' in favour of 'swift language mode'. Modifications: - Remove temporary shim - Use 'swiftLanguageMode' Result: - Fewer warnings - Xcode 16 Beta 5 is now required --- Package@swift-6.swift | 106 ++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 60 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 5cbb769bd..8a1a62aa3 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -27,20 +27,6 @@ let cgrpcZlibTargetName = cgrpcZlibProductName let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil -// Temporary shim: nightly toolchains renamed 'swiftLanguageVersion' to 'swiftLanguageMode'. This -// isn't yet available in a beta Xcode. -// -// See also: https://github.com/swiftlang/swift-package-manager/issues/7823 -extension SwiftSetting { - static func _swiftLanguageMode(_ version: SwiftVersion) -> SwiftSetting { - #if os(Linux) - return .swiftLanguageMode(version) - #else - return .swiftLanguageVersion(version) - #endif - } -} - // MARK: - Package Dependencies let packageDependencies: [Package.Dependency] = [ @@ -196,7 +182,7 @@ extension Target { .nioSSL, if: includeNIOSSL ), path: "Sources/GRPC", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -209,7 +195,7 @@ extension Target { ], path: "Sources/GRPCCore", swiftSettings: [ - ._swiftLanguageMode(.v5), + .swiftLanguageMode(.v5), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -223,7 +209,7 @@ extension Target { .grpcCore ], swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -238,7 +224,7 @@ extension Target { .tracing ], swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -257,7 +243,7 @@ extension Target { .atomics ], swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -274,7 +260,7 @@ extension Target { .nioExtras ], swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -292,7 +278,7 @@ extension Target { .nioTransportServices ], swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -309,7 +295,7 @@ extension Target { .grpcHTTP2TransportNIOTransportServices, ], swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -338,7 +324,7 @@ extension Target { exclude: [ "README.md", ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -354,7 +340,7 @@ extension Target { .nioFileSystem, .argumentParser ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -395,7 +381,7 @@ extension Target { exclude: [ "Codegen/Serialization/echo.grpc.reflection" ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -409,7 +395,7 @@ extension Target { .atomics, .protobuf, ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -420,7 +406,7 @@ extension Target { .grpcCore, .grpcInProcessTransport ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -433,7 +419,7 @@ extension Target { .nioCore, .grpcInterceptors ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -447,7 +433,7 @@ extension Target { .nioEmbedded, .nioTestUtils, ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -460,7 +446,7 @@ extension Target { .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -470,7 +456,7 @@ extension Target { dependencies: [ .grpcCodeGen ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -482,7 +468,7 @@ extension Target { .grpcCore, .protobuf ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -495,7 +481,7 @@ extension Target { .protobuf, .protobufPluginLibrary ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -507,7 +493,7 @@ extension Target { .interoperabilityTests, .grpcCore ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -520,7 +506,7 @@ extension Target { .grpcInProcessTransport ], path: "Tests/Services/HealthTests", - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -541,7 +527,7 @@ extension Target { "src/proto/grpc/testing/test.proto", "unimplemented_call.patch", ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -552,7 +538,7 @@ extension Target { .grpcCore, .grpcProtobuf ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -566,7 +552,7 @@ extension Target { .interoperabilityTests, .argumentParser ], - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -583,7 +569,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -598,7 +584,7 @@ extension Target { .logging, .argumentParser, ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -616,7 +602,7 @@ extension Target { exclude: [ "README.md", ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -632,7 +618,7 @@ extension Target { .nioHTTP2, .argumentParser, ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -643,7 +629,7 @@ extension Target { exclude: [ "bundle.p12", ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -656,7 +642,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/Echo/Model", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -671,7 +657,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/Echo/Implementation", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -691,7 +677,7 @@ extension Target { .nioSSL, if: includeNIOSSL ), path: "Sources/Examples/v1/Echo/Runtime", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -708,7 +694,7 @@ extension Target { .nioSSL, if: includeNIOSSL ), path: "Sources/Examples/v2/Echo", - swiftSettings: [._swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } @@ -721,7 +707,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/HelloWorld/Model", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -736,7 +722,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/HelloWorld/Client", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -751,7 +737,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/HelloWorld/Server", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -764,7 +750,7 @@ extension Target { .protobuf, ], path: "Sources/Examples/v1/RouteGuide/Model", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -779,7 +765,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/RouteGuide/Client", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -795,7 +781,7 @@ extension Target { .argumentParser, ], path: "Sources/Examples/v1/RouteGuide/Server", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -814,7 +800,7 @@ extension Target { exclude: [ "README.md", ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -827,7 +813,7 @@ extension Target { .protobuf, ], path: "Sources/GRPCReflectionService", - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -848,7 +834,7 @@ extension Target { resources: [ .copy("Generated") ], - swiftSettings: [._swiftLanguageMode(.v5)] + swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -857,7 +843,7 @@ extension Target { name: "GRPCCodeGen", path: "Sources/GRPCCodeGen", swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -873,7 +859,7 @@ extension Target { ], path: "Sources/GRPCProtobuf", swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -890,7 +876,7 @@ extension Target { ], path: "Sources/GRPCProtobufCodeGen", swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] @@ -906,7 +892,7 @@ extension Target { ], path: "Sources/Services/Health", swiftSettings: [ - ._swiftLanguageMode(.v6), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny") ] ) From 9457258dd637b94a2da645404eee4c1a281f5b8a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 12 Aug 2024 11:06:56 +0100 Subject: [PATCH 423/580] Add check that generated code is up-to-date (#2013) Motivation: It's easy for checked in generated code to not be up-to-date. Modifications: - Add a script to check it's up-to-date and run it in CI Result: More guarantess that checked in code is up-to-date --- .github/workflows/ci.yaml | 19 ++-- Protos/generate.sh | 2 +- .../v2/Echo/Generated/echo.grpc.swift | 38 +++---- .../Generated/empty_service.grpc.swift | 2 +- .../Generated/test.grpc.swift | 104 +++++++++--------- .../Health/Generated/health.grpc.swift | 16 +-- .../grpc_testing_benchmark_service.grpc.swift | 48 ++++---- .../Generated/grpc_testing_control.pb.swift | 8 +- .../grpc_testing_worker_service.grpc.swift | 14 +-- .../Generated/control.grpc.swift | 38 +++---- scripts/check-generated-code.sh | 32 ++++++ scripts/format.sh | 65 +++-------- scripts/sanity.sh | 11 +- 13 files changed, 199 insertions(+), 198 deletions(-) create mode 100755 scripts/check-generated-code.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 26d4e0447..1b348667d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,20 +9,17 @@ jobs: name: License Header and Formatting Checks runs-on: ubuntu-latest container: - image: swift + image: swiftlang/swift:nightly-6.0-jammy steps: - - uses: actions/checkout@v4 - - uses: actions/cache/restore@v4 - with: - path: scripts/.swift-format-source - key: ${{ runner.os }}-swift-format - - name: "Formatting and License Headers check" + - name: "Checkout repository" + uses: actions/checkout@v4 + - name: Mark the workspace as safe + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: "Install protoc" + run: apt update && apt install -y protobuf-compiler + - name: "Formatting, License Headers, and Generated Code check" run: | ./scripts/sanity.sh - - uses: actions/cache/save@v4 - with: - path: scripts/.swift-format-source - key: ${{ runner.os }}-swift-format unit-tests: strategy: fail-fast: false diff --git a/Protos/generate.sh b/Protos/generate.sh index 3e5c05631..17c803049 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -119,7 +119,7 @@ function generate_reflection_service { mv "$output_v1/reflection.grpc.swift" "$output_v1/reflection-v1.grpc.swift" local proto_v1alpha="$here/upstream/grpc/reflection/v1alpha/reflection.proto" - local output_v1alpha="$root/Sources/GRPCReflectionService/v1alpha" + local output_v1alpha="$root/Sources/GRPCReflectionService/v1Alpha" # Messages were accidentally leaked into public API, they shouldn't be but we # can't undo that change until the next major version. diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index d21a7a327..797ab5ca5 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -87,13 +87,13 @@ extension ServiceDescriptor { internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Immediately returns an echo of a request. func get(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Splits a request into words and returns each word in a stream of messages. func expand(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Collects a stream of messages and returns them concatenated when the caller closes. func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Streams back messages as they are received in an input stream. func update(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -142,13 +142,13 @@ extension Echo_Echo.StreamingServiceProtocol { internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { /// Immediately returns an echo of a request. func get(request: ServerRequest.Single) async throws -> ServerResponse.Single - + /// Splits a request into words and returns each word in a stream of messages. func expand(request: ServerRequest.Single) async throws -> ServerResponse.Stream - + /// Collects a stream of messages and returns them concatenated when the caller closes. func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Single - + /// Streams back messages as they are received in an input stream. func update(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -160,12 +160,12 @@ extension Echo_Echo.ServiceProtocol { let response = try await self.get(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + internal func expand(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.expand(request: ServerRequest.Single(stream: request)) return response } - + internal func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.collect(request: request) return ServerResponse.Stream(single: response) @@ -182,7 +182,7 @@ internal protocol Echo_EchoClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// Splits a request into words and returns each word in a stream of messages. func expand( request: ClientRequest.Single, @@ -191,7 +191,7 @@ internal protocol Echo_EchoClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable - + /// Collects a stream of messages and returns them concatenated when the caller closes. func collect( request: ClientRequest.Stream, @@ -200,7 +200,7 @@ internal protocol Echo_EchoClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// Streams back messages as they are received in an input stream. func update( request: ClientRequest.Stream, @@ -226,7 +226,7 @@ extension Echo_Echo.ClientProtocol { body ) } - + internal func expand( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -240,7 +240,7 @@ extension Echo_Echo.ClientProtocol { body ) } - + internal func collect( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -254,7 +254,7 @@ extension Echo_Echo.ClientProtocol { body ) } - + internal func update( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -273,11 +273,11 @@ extension Echo_Echo.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { private let client: GRPCCore.GRPCClient - + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Immediately returns an echo of a request. internal func get( request: ClientRequest.Single, @@ -295,7 +295,7 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { handler: body ) } - + /// Splits a request into words and returns each word in a stream of messages. internal func expand( request: ClientRequest.Single, @@ -313,7 +313,7 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { handler: body ) } - + /// Collects a stream of messages and returns them concatenated when the caller closes. internal func collect( request: ClientRequest.Stream, @@ -331,7 +331,7 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { handler: body ) } - + /// Streams back messages as they are received in an input stream. internal func update( request: ClientRequest.Stream, diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift index c277d42c3..a28e39ff5 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -82,7 +82,7 @@ extension Grpc_Testing_EmptyService.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_EmptyServiceClient: Grpc_Testing_EmptyService.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index 2ef5fc243..b42989ccc 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -201,34 +201,34 @@ extension ServiceDescriptor { public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One empty request followed by one empty response. func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// One request followed by one response. func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -312,34 +312,34 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestService.StreamingServiceProtocol { /// One empty request followed by one empty response. func emptyCall(request: ServerRequest.Single) async throws -> ServerResponse.Single - + /// One request followed by one response. func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single - + /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. func cacheableUnaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single - + /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. func streamingOutputCall(request: ServerRequest.Single) async throws -> ServerResponse.Stream - + /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Single - + /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single @@ -352,27 +352,27 @@ extension Grpc_Testing_TestService.ServiceProtocol { let response = try await self.emptyCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + public func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + public func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.cacheableUnaryCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + public func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingOutputCall(request: ServerRequest.Single(stream: request)) return response } - + public func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingInputCall(request: request) return ServerResponse.Stream(single: response) } - + public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) @@ -424,7 +424,7 @@ extension Grpc_Testing_UnimplementedService.ServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -456,7 +456,7 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_ReconnectService.StreamingServiceProtocol { func start(request: ServerRequest.Single) async throws -> ServerResponse.Single - + func stop(request: ServerRequest.Single) async throws -> ServerResponse.Single } @@ -467,7 +467,7 @@ extension Grpc_Testing_ReconnectService.ServiceProtocol { let response = try await self.start(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + public func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.stop(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) @@ -486,7 +486,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// One request followed by one response. func unaryCall( request: ClientRequest.Single, @@ -495,7 +495,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. @@ -506,7 +506,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. func streamingOutputCall( @@ -516,7 +516,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable - + /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. func streamingInputCall( @@ -526,7 +526,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. @@ -537,7 +537,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable - + /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with @@ -549,7 +549,7 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable - + /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. func unimplementedCall( @@ -576,7 +576,7 @@ extension Grpc_Testing_TestService.ClientProtocol { body ) } - + public func unaryCall( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -590,7 +590,7 @@ extension Grpc_Testing_TestService.ClientProtocol { body ) } - + public func cacheableUnaryCall( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -604,7 +604,7 @@ extension Grpc_Testing_TestService.ClientProtocol { body ) } - + public func streamingOutputCall( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -618,7 +618,7 @@ extension Grpc_Testing_TestService.ClientProtocol { body ) } - + public func streamingInputCall( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -632,7 +632,7 @@ extension Grpc_Testing_TestService.ClientProtocol { body ) } - + public func fullDuplexCall( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -646,7 +646,7 @@ extension Grpc_Testing_TestService.ClientProtocol { body ) } - + public func halfDuplexCall( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -660,7 +660,7 @@ extension Grpc_Testing_TestService.ClientProtocol { body ) } - + public func unimplementedCall( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -681,11 +681,11 @@ extension Grpc_Testing_TestService.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// One empty request followed by one empty response. public func emptyCall( request: ClientRequest.Single, @@ -703,7 +703,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro handler: body ) } - + /// One request followed by one response. public func unaryCall( request: ClientRequest.Single, @@ -721,7 +721,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro handler: body ) } - + /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. @@ -741,7 +741,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro handler: body ) } - + /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. public func streamingOutputCall( @@ -760,7 +760,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro handler: body ) } - + /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. public func streamingInputCall( @@ -779,7 +779,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro handler: body ) } - + /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. @@ -799,7 +799,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro handler: body ) } - + /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with @@ -820,7 +820,7 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro handler: body ) } - + /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. public func unimplementedCall( @@ -877,11 +877,11 @@ extension Grpc_Testing_UnimplementedService.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_UnimplementedService.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// A call that no server should implement public func unimplementedCall( request: ClientRequest.Single, @@ -911,7 +911,7 @@ public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + func stop( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -936,7 +936,7 @@ extension Grpc_Testing_ReconnectService.ClientProtocol { body ) } - + public func stop( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -956,11 +956,11 @@ extension Grpc_Testing_ReconnectService.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + public func start( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -977,7 +977,7 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService handler: body ) } - + public func stop( request: ClientRequest.Single, serializer: some MessageSerializer, diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 6316b5b55..504dd871e 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -83,7 +83,7 @@ package protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.Registr /// /// Check implementations should be idempotent and side effect free. func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current /// serving status. It will then subsequently send a new message whenever @@ -141,7 +141,7 @@ package protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.Str /// /// Check implementations should be idempotent and side effect free. func check(request: ServerRequest.Single) async throws -> ServerResponse.Single - + /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current /// serving status. It will then subsequently send a new message whenever @@ -167,7 +167,7 @@ extension Grpc_Health_V1_Health.ServiceProtocol { let response = try await self.check(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + package func watch(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.watch(request: ServerRequest.Single(stream: request)) return response @@ -195,7 +195,7 @@ package protocol Grpc_Health_V1_HealthClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current /// serving status. It will then subsequently send a new message whenever @@ -235,7 +235,7 @@ extension Grpc_Health_V1_Health.ClientProtocol { body ) } - + package func watch( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -257,11 +257,11 @@ extension Grpc_Health_V1_Health.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol { private let client: GRPCCore.GRPCClient - + package init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Check gets the health of the specified service. If the requested service /// is unknown, the call will fail with status NOT_FOUND. If the caller does /// not specify a service name, the server should respond with its overall @@ -287,7 +287,7 @@ package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol handler: body ) } - + /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current /// serving status. It will then subsequently send a new message whenever diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 73ddaeabf..7e3c95084 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -100,20 +100,20 @@ internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCor /// One request followed by one response. /// The server returns the client payload as-is. func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -172,20 +172,20 @@ internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_Ben /// One request followed by one response. /// The server returns the client payload as-is. func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single - + /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Single - + /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is func streamingFromServer(request: ServerRequest.Single) async throws -> ServerResponse.Stream - + /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream @@ -198,12 +198,12 @@ extension Grpc_Testing_BenchmarkService.ServiceProtocol { let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + internal func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingFromClient(request: request) return ServerResponse.Stream(single: response) } - + internal func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.streamingFromServer(request: ServerRequest.Single(stream: request)) return response @@ -221,7 +221,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response @@ -232,7 +232,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable - + /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone func streamingFromClient( @@ -242,7 +242,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is func streamingFromServer( @@ -252,7 +252,7 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable - + /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other func streamingBothWays( @@ -279,7 +279,7 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { body ) } - + internal func streamingCall( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -293,7 +293,7 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { body ) } - + internal func streamingFromClient( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -307,7 +307,7 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { body ) } - + internal func streamingFromServer( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -321,7 +321,7 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { body ) } - + internal func streamingBothWays( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -340,11 +340,11 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkService.ClientProtocol { private let client: GRPCCore.GRPCClient - + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// One request followed by one response. /// The server returns the client payload as-is. internal func unaryCall( @@ -363,7 +363,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi handler: body ) } - + /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response @@ -383,7 +383,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi handler: body ) } - + /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone internal func streamingFromClient( @@ -402,7 +402,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi handler: body ) } - + /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is internal func streamingFromServer( @@ -421,7 +421,7 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi handler: body ) } - + /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other internal func streamingBothWays( diff --git a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift index 44d5a5b61..969f5b561 100644 --- a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift @@ -743,8 +743,8 @@ struct Grpc_Testing_ScenarioResultSummary: @unchecked Sendable { // methods supported on all messages. /// Total number of operations per second over all clients. What is counted as 1 'operation' depends on the benchmark scenarios: - /// For unary benchmarks, an operation is processing of a single unary RPC. - /// For streaming benchmarks, an operation is processing of a single ping pong of request and response. + /// For unary benchmarks, an operation is processing of a single unary RPC. + /// For streaming benchmarks, an operation is processing of a single ping pong of request and response. var qps: Double { get {return _storage._qps} set {_uniqueStorage()._qps = newValue} @@ -757,8 +757,8 @@ struct Grpc_Testing_ScenarioResultSummary: @unchecked Sendable { } /// The total server cpu load based on system time across all server processes, expressed as percentage of a single cpu core. - /// For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server - /// processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. + /// For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server + /// processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. /// Same explanation for the total client cpu load below. var serverSystemTime: Double { get {return _storage._serverSystemTime} diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index 5b8a1de2b..2a702944f 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -91,7 +91,7 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus /// response. After that, a "Mark" can be sent anytime to request the latest @@ -99,10 +99,10 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Just return the core count - unary call func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Quit this worker func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -156,7 +156,7 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus /// response. After that, a "Mark" can be sent anytime to request the latest @@ -164,10 +164,10 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + /// Just return the core count - unary call func coreCount(request: ServerRequest.Single) async throws -> ServerResponse.Single - + /// Quit this worker func quitWorker(request: ServerRequest.Single) async throws -> ServerResponse.Single } @@ -179,7 +179,7 @@ extension Grpc_Testing_WorkerService.ServiceProtocol { let response = try await self.coreCount(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + internal func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.quitWorker(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index dbc205faf..9f4bdf534 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -91,11 +91,11 @@ extension ServiceDescriptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCService { func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream - + func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -146,11 +146,11 @@ extension Control.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single - + func serverStream(request: ServerRequest.Single) async throws -> ServerResponse.Stream - + func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Single - + func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -161,12 +161,12 @@ extension Control.ServiceProtocol { let response = try await self.unary(request: ServerRequest.Single(stream: request)) return ServerResponse.Stream(single: response) } - + internal func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.serverStream(request: ServerRequest.Single(stream: request)) return response } - + internal func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { let response = try await self.clientStream(request: request) return ServerResponse.Stream(single: response) @@ -186,7 +186,7 @@ internal protocol ControlClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + func serverStream( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -194,7 +194,7 @@ internal protocol ControlClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable - + func clientStream( request: ClientRequest.Stream, serializer: some MessageSerializer, @@ -202,7 +202,7 @@ internal protocol ControlClientProtocol: Sendable { options: CallOptions, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable - + func bidiStream( request: ClientRequest.Stream, serializer: some MessageSerializer, @@ -227,7 +227,7 @@ extension Control.ClientProtocol { body ) } - + internal func serverStream( request: ClientRequest.Single, options: CallOptions = .defaults, @@ -241,7 +241,7 @@ extension Control.ClientProtocol { body ) } - + internal func clientStream( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -255,7 +255,7 @@ extension Control.ClientProtocol { body ) } - + internal func bidiStream( request: ClientRequest.Stream, options: CallOptions = .defaults, @@ -278,11 +278,11 @@ extension Control.ClientProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct ControlClient: Control.ClientProtocol { private let client: GRPCCore.GRPCClient - + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + internal func unary( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -299,7 +299,7 @@ internal struct ControlClient: Control.ClientProtocol { handler: body ) } - + internal func serverStream( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -316,7 +316,7 @@ internal struct ControlClient: Control.ClientProtocol { handler: body ) } - + internal func clientStream( request: ClientRequest.Stream, serializer: some MessageSerializer, @@ -333,7 +333,7 @@ internal struct ControlClient: Control.ClientProtocol { handler: body ) } - + internal func bidiStream( request: ClientRequest.Stream, serializer: some MessageSerializer, diff --git a/scripts/check-generated-code.sh b/scripts/check-generated-code.sh new file mode 100755 index 000000000..3444d9d88 --- /dev/null +++ b/scripts/check-generated-code.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# 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. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Re-generate everything. +log "Regenerating protos..." +"$here"/../Protos/generate.sh + +# Check for changes. +GIT_PAGER='' git diff --exit-code '*.swift' + +log "Generated code is up-to-date" diff --git a/scripts/format.sh b/scripts/format.sh index 1e3d7801b..f9ded9164 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -52,53 +52,20 @@ while getopts ":flh" opt; do esac done -THIS_SCRIPT=$0 -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REPO="$HERE/.." -SWIFTFORMAT_DIR="$HERE/.swift-format-source" -SWIFTFORMAT_VERSION=510.0.0 - -# Clone SwiftFormat if we don't already have it. -if [ ! -d "$SWIFTFORMAT_DIR" ]; then - echo "- Cloning swift-format @ $SWIFTFORMAT_VERSION" - git clone \ - --depth 1 \ - --branch "$SWIFTFORMAT_VERSION" \ - https://github.com/apple/swift-format.git \ - "$SWIFTFORMAT_DIR" -fi - -cd "$SWIFTFORMAT_DIR" - -# Figure out the path for the binary. -SWIFTFORMAT_BIN="$(swift build --show-bin-path -c release)/swift-format-$SWIFTFORMAT_VERSION" - -# Build it if we don't already have it. -if [ ! -f "$SWIFTFORMAT_BIN" ]; then - # We're not on the right tag, fetch and checkout the right one. - echo "- Fetching swift-format @ $SWIFTFORMAT_VERSION" - git fetch --depth 1 origin "refs/tags/$SWIFTFORMAT_VERSION:refs/tags/$SWIFTFORMAT_VERSION" - git checkout "$SWIFTFORMAT_VERSION" - - # Now build and name the bin appropriately. - echo "- Building swift-format @ $SWIFTFORMAT_VERSION" - swift build -c release --product swift-format - mv "$(swift build --show-bin-path -c release)/swift-format" "$SWIFTFORMAT_BIN" - - echo "- OK" -fi +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +repo="$here/.." if "$lint"; then - "${SWIFTFORMAT_BIN}" lint \ + swift format lint \ --parallel --recursive --strict \ - "${REPO}/Sources" \ - "${REPO}/Tests" \ - "${REPO}/Plugins" \ - "${REPO}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/Sources" \ + "${repo}/Tests" \ + "${repo}/Plugins" \ + "${repo}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then - fatal "Running swift-format produced errors. + fatal "Running swift format produced errors. To fix, run the following command: @@ -107,21 +74,21 @@ if "$lint"; then exit "${SWIFT_FORMAT_RC}" fi - log "Ran swift-format lint with no errors." + log "Ran swift format lint with no errors." elif "$format"; then - "${SWIFTFORMAT_BIN}" format \ + swift format \ --parallel --recursive --in-place \ - "${REPO}/Sources" \ - "${REPO}/Tests" \ - "${REPO}/Plugins" \ - "${REPO}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/Sources" \ + "${repo}/Tests" \ + "${repo}/Plugins" \ + "${repo}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then - fatal "Running swift-format produced errors." "${SWIFT_FORMAT_RC}" + fatal "Running swift format produced errors." "${SWIFT_FORMAT_RC}" fi - log "Ran swift-format with no errors." + log "Ran swift format with no errors." else fatal "No actions taken." fi diff --git a/scripts/sanity.sh b/scripts/sanity.sh index a290eb9f7..de039b514 100755 --- a/scripts/sanity.sh +++ b/scripts/sanity.sh @@ -16,7 +16,7 @@ set -eu -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function run_logged() { local message=$1 @@ -38,14 +38,19 @@ function run_logged() { } function check_license_headers() { - run_logged "Checking license headers" "$HERE/license-check.sh" + run_logged "Checking license headers" "$here/license-check.sh" } function check_formatting() { - run_logged "Checking formatting" "$HERE/format.sh -l" + run_logged "Checking formatting" "$here/format.sh -l" +} + +function check_generated_code_is_up_to_date() { + run_logged "Checking generated code is up-to-date" "$here/check-generated-code.sh" } errors=0 check_license_headers check_formatting +check_generated_code_is_up_to_date exit $errors From edaa545c218deddcd5bafbb480c0cc1d494398fc Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:26:51 +0100 Subject: [PATCH 424/580] Generate default response handlers (#2007) Motivation: The client stub for all RPCs accepts a response handler and can return the result of that response. For unary and client-streaming RPCs, the most common handler will just return the response message. This handler should be generated as the default for those RPC types. Modifications: - Default the response handler parameter (`body`) to `{ try $0.message }`. Result: As an example, part of the generated code for the Echo service which looks like: ```swift extension Echo_Echo.ClientProtocol { internal func get( request: ClientRequest.Single, options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable { try await self.get(...) } ``` Becomes: ```swift extension Echo_Echo.ClientProtocol { internal func get( request: ClientRequest.Single, options: CallOptions = .defaults, _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.get(...) } ``` --------- Co-authored-by: George Barnett --- .../v2/Echo/Generated/echo.grpc.swift | 16 +++-- .../Translator/ClientCodeTranslator.swift | 45 ++++++++++--- .../Generated/test.grpc.swift | 64 ++++++++++++++----- .../Health/Generated/health.grpc.swift | 8 ++- .../grpc_testing_benchmark_service.grpc.swift | 16 +++-- ...lientCodeTranslatorSnippetBasedTests.swift | 32 +++++++--- .../Generated/control.grpc.swift | 16 +++-- .../ProtobufCodeGeneratorTests.swift | 16 +++-- 8 files changed, 163 insertions(+), 50 deletions(-) diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index 797ab5ca5..0c829064b 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -216,7 +216,9 @@ extension Echo_Echo.ClientProtocol { internal func get( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.get( request: request, @@ -244,7 +246,9 @@ extension Echo_Echo.ClientProtocol { internal func collect( request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.collect( request: request, @@ -284,7 +288,9 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -320,7 +326,9 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index f7f9cfd08..4b2684870 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -37,7 +37,9 @@ /// public func baz( /// request: ClientRequest.Single, /// options: CallOptions = .defaults, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { +/// try $0.message +/// } /// ) async throws -> R where R: Sendable { /// try await self.baz( /// request: request, @@ -58,7 +60,9 @@ /// serializer: some MessageSerializer, /// deserializer: some MessageDeserializer, /// options: CallOptions = .defaults, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { +/// try $0.message +/// } /// ) async throws -> R where R: Sendable { /// try await self.client.unary( /// request: request, @@ -173,7 +177,8 @@ extension ClientCodeTranslator { from: codeGenerationRequest, // The serializer/deserializer for the protocol extension method will be auto-generated. includeSerializationParameters: !isProtocolExtension, - includeDefaultCallOptions: includeDefaultCallOptions + includeDefaultCallOptions: includeDefaultCallOptions, + includeDefaultResponseHandler: isProtocolExtension && !method.isOutputStreaming ) let functionSignature = FunctionSignatureDescription( accessModifier: accessModifier, @@ -242,7 +247,8 @@ extension ClientCodeTranslator { in service: CodeGenerationRequest.ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest, includeSerializationParameters: Bool, - includeDefaultCallOptions: Bool + includeDefaultCallOptions: Bool, + includeDefaultResponseHandler: Bool ) -> [ParameterDescription] { var parameters = [ParameterDescription]() @@ -259,7 +265,13 @@ extension ClientCodeTranslator { ? .memberAccess(MemberAccessDescription(right: "defaults")) : nil ) ) - parameters.append(self.bodyParameter(for: method, in: service)) + parameters.append( + self.bodyParameter( + for: method, + in: service, + includeDefaultResponseHandler: includeDefaultResponseHandler + ) + ) return parameters } private func clientRequestParameter( @@ -309,7 +321,8 @@ extension ClientCodeTranslator { private func bodyParameter( for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + in service: CodeGenerationRequest.ServiceDescriptor, + includeDefaultResponseHandler: Bool ) -> ParameterDescription { let clientStreaming = method.isOutputStreaming ? "Stream" : "Single" let closureParameterType = ExistingTypeDescription.generic( @@ -324,7 +337,22 @@ extension ClientCodeTranslator { sendable: true, escaping: true ) - return ParameterDescription(name: "body", type: .closure(bodyClosure)) + + var defaultResponseHandler: Expression? = nil + + if includeDefaultResponseHandler { + defaultResponseHandler = .closureInvocation( + ClosureInvocationDescription( + body: [.expression(.try(.identifierPattern("$0").dot("message")))] + ) + ) + } + + return ParameterDescription( + name: "body", + type: .closure(bodyClosure), + defaultValue: defaultResponseHandler + ) } private func makeClientStruct( @@ -403,7 +431,8 @@ extension ClientCodeTranslator { in: service, from: codeGenerationRequest, includeSerializationParameters: true, - includeDefaultCallOptions: true + includeDefaultCallOptions: true, + includeDefaultResponseHandler: !method.isOutputStreaming ) let grpcMethodName = self.clientMethod( isInputStreaming: method.isInputStreaming, diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index b42989ccc..e31c5a835 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -566,7 +566,9 @@ extension Grpc_Testing_TestService.ClientProtocol { public func emptyCall( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.emptyCall( request: request, @@ -580,7 +582,9 @@ extension Grpc_Testing_TestService.ClientProtocol { public func unaryCall( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, @@ -594,7 +598,9 @@ extension Grpc_Testing_TestService.ClientProtocol { public func cacheableUnaryCall( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.cacheableUnaryCall( request: request, @@ -622,7 +628,9 @@ extension Grpc_Testing_TestService.ClientProtocol { public func streamingInputCall( request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.streamingInputCall( request: request, @@ -664,7 +672,9 @@ extension Grpc_Testing_TestService.ClientProtocol { public func unimplementedCall( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, @@ -692,7 +702,9 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -710,7 +722,9 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -730,7 +744,9 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -768,7 +784,9 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -828,7 +846,9 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -860,7 +880,9 @@ extension Grpc_Testing_UnimplementedService.ClientProtocol { public func unimplementedCall( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, @@ -888,7 +910,9 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -926,7 +950,9 @@ extension Grpc_Testing_ReconnectService.ClientProtocol { public func start( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.start( request: request, @@ -940,7 +966,9 @@ extension Grpc_Testing_ReconnectService.ClientProtocol { public func stop( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.stop( request: request, @@ -966,7 +994,9 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -983,7 +1013,9 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 504dd871e..4407d9e76 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -225,7 +225,9 @@ extension Grpc_Health_V1_Health.ClientProtocol { package func check( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.check( request: request, @@ -276,7 +278,9 @@ package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 7e3c95084..951901e83 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -269,7 +269,9 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { internal func unaryCall( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, @@ -297,7 +299,9 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { internal func streamingFromClient( request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.streamingFromClient( request: request, @@ -352,7 +356,9 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -391,7 +397,9 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 5abb474d4..10b4c770f 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -59,7 +59,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public func methodA( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.methodA( request: request, @@ -85,7 +87,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -140,7 +144,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public func methodA( request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.methodA( request: request, @@ -166,7 +172,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -400,7 +408,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package func methodA( request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.methodA( request: request, @@ -440,7 +450,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, @@ -513,7 +525,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { internal func methodA( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.methodA( request: request, @@ -539,7 +553,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index 9f4bdf534..2249463f8 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -217,7 +217,9 @@ extension Control.ClientProtocol { internal func unary( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.unary( request: request, @@ -245,7 +247,9 @@ extension Control.ClientProtocol { internal func clientStream( request: ClientRequest.Stream, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.clientStream( request: request, @@ -288,7 +292,9 @@ internal struct ControlClient: Control.ClientProtocol { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -322,7 +328,9 @@ internal struct ControlClient: Control.ClientProtocol { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.clientStreaming( request: request, diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 6e9ff2bee..d72a5190e 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -105,7 +105,9 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal func sayHello( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.sayHello( request: request, @@ -132,7 +134,9 @@ final class ProtobufCodeGeneratorTests: XCTestCase { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, @@ -374,7 +378,9 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package func sayHello( request: ClientRequest.Single, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.sayHello( request: request, @@ -401,7 +407,9 @@ final class ProtobufCodeGeneratorTests: XCTestCase { serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + try $0.message + } ) async throws -> R where R: Sendable { try await self.client.unary( request: request, From 0665c3f343b1a9a14340a6f78377901972521933 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 12 Aug 2024 14:40:23 +0100 Subject: [PATCH 425/580] Generate sugared client APIs (#2009) Motivation: The generated client API uses the full expressive request/response types which are used by the interceptors. This gives clients full control but at the cost of usability. In many cases we can simplify this for them: - For single requests we can push the message and metadata into the stub signature and construct the request on behalf of the user - For streaming requests we can push the metadata and request writing closure into the signature of the stub and construct the request for the user - For single responses we can default the response handler to returning the message Modifications: - Add sugared API - This, required that the struct swift representation and renderer be updated to support dictionary literls - Regenrate the code - Update the echo example to use the convenience API Result: Easier to use client APIs --- .../v2/Echo/Generated/echo.grpc.swift | 79 ++++++ .../v2/Echo/Subcommands/Collect.swift | 8 +- .../Examples/v2/Echo/Subcommands/Expand.swift | 4 +- .../Examples/v2/Echo/Subcommands/Get.swift | 6 +- .../Examples/v2/Echo/Subcommands/Update.swift | 6 +- .../Internal/Renderer/TextBasedRenderer.swift | 23 ++ .../StructuredSwiftRepresentation.swift | 10 + .../Translator/ClientCodeTranslator.swift | 262 ++++++++++++++++-- .../Generated/empty_service.grpc.swift | 4 + .../Generated/test.grpc.swift | 231 +++++++++++++++ .../Health/Generated/health.grpc.swift | 63 +++++ .../grpc_testing_benchmark_service.grpc.swift | 103 +++++++ .../Renderer/TextBasedRendererTests.swift | 29 ++ ...lientCodeTranslatorSnippetBasedTests.swift | 158 ++++++++++- .../Generated/control.grpc.swift | 75 +++++ .../ProtobufCodeGeneratorTests.swift | 54 +++- 16 files changed, 1070 insertions(+), 45 deletions(-) diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index 0c829064b..28cbec207 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -274,6 +274,85 @@ extension Echo_Echo.ClientProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Echo_Echo.ClientProtocol { + /// Immediately returns an echo of a request. + internal func get( + _ message: Echo_EchoRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.get( + request: request, + options: options, + handleResponse + ) + } + + /// Splits a request into words and returns each word in a stream of messages. + internal func expand( + _ message: Echo_EchoRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.expand( + request: request, + options: options, + handleResponse + ) + } + + /// Collects a stream of messages and returns them concatenated when the caller closes. + internal func collect( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.collect( + request: request, + options: options, + handleResponse + ) + } + + /// Streams back messages as they are received in an input stream. + internal func update( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.update( + request: request, + options: options, + handleResponse + ) + } +} + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Sources/Examples/v2/Echo/Subcommands/Collect.swift b/Sources/Examples/v2/Echo/Subcommands/Collect.swift index 333777a55..467e9c876 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Collect.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Collect.swift @@ -40,17 +40,13 @@ struct Collect: AsyncParsableCommand { let echo = Echo_EchoClient(wrapping: client) for _ in 0 ..< self.arguments.repetitions { - let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in + let message = try await echo.collect { writer in for part in self.arguments.message.split(separator: " ") { print("collect → \(part)") try await writer.write(.with { $0.text = String(part) }) } } - - try await echo.collect(request: request) { response in - let message = try response.message - print("collect ← \(message.text)") - } + print("collect ← \(message.text)") } client.close() diff --git a/Sources/Examples/v2/Echo/Subcommands/Expand.swift b/Sources/Examples/v2/Echo/Subcommands/Expand.swift index ea17f741f..c3135d544 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Expand.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Expand.swift @@ -42,9 +42,9 @@ struct Expand: AsyncParsableCommand { for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } print("expand → \(message.text)") - try await echo.expand(request: ClientRequest.Single(message: message)) { response in + try await echo.expand(message) { response in for try await message in response.messages { - print("get ← \(message.text)") + print("expand ← \(message.text)") } } } diff --git a/Sources/Examples/v2/Echo/Subcommands/Get.swift b/Sources/Examples/v2/Echo/Subcommands/Get.swift index c37983b8f..e22d0523c 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Get.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Get.swift @@ -40,10 +40,8 @@ struct Get: AsyncParsableCommand { for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } print("get → \(message.text)") - try await echo.get(request: ClientRequest.Single(message: message)) { response in - let message = try response.message - print("get ← \(message.text)") - } + let response = try await echo.get(message) + print("get ← \(message.text)") } client.close() diff --git a/Sources/Examples/v2/Echo/Subcommands/Update.swift b/Sources/Examples/v2/Echo/Subcommands/Update.swift index e62fa7ff2..a580c25fb 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Update.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Update.swift @@ -40,14 +40,12 @@ struct Update: AsyncParsableCommand { let echo = Echo_EchoClient(wrapping: client) for _ in 0 ..< self.arguments.repetitions { - let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in + try await echo.update { writer in for part in self.arguments.message.split(separator: " ") { print("update → \(part)") try await writer.write(.with { $0.text = String(part) }) } - } - - try await echo.update(request: request) { response in + } onResponse: { response in for try await message in response.messages { print("update ← \(message.text)") } diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 35c7173e0..2cf326ab9 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -528,6 +528,29 @@ struct TextBasedRenderer: RendererProtocol { writer.nextLineAppendsToLastLine() } writer.writeLine("]") + + case .dictionary(let items): + writer.writeLine("[") + if items.isEmpty { + writer.nextLineAppendsToLastLine() + writer.writeLine(":") + writer.nextLineAppendsToLastLine() + } else { + writer.withNestedLevel { + for (item, isLast) in items.enumeratedWithLastMarker() { + renderExpression(item.key) + writer.nextLineAppendsToLastLine() + writer.writeLine(": ") + writer.nextLineAppendsToLastLine() + renderExpression(item.value) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(",") + } + } + } + } + writer.writeLine("]") } } diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index b96601db7..2b6d3ebff 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -167,6 +167,16 @@ enum LiteralDescription: Equatable, Codable { /// /// For example `["hello", 42]`. case array([Expression]) + + /// A dictionary literal. + /// + /// For example: `["hello": "42"]` + case dictionary([KeyValue]) + + struct KeyValue: Codable, Equatable { + var key: Expression + var value: Expression + } } /// A description of an identifier, such as a variable name. diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 4b2684870..9ba625e47 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -82,31 +82,24 @@ struct ClientCodeTranslator: SpecializedTranslator { self.accessLevel = accessLevel } - func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { - var codeBlocks = [CodeBlock]() - - for service in codeGenerationRequest.services { - codeBlocks.append( - .declaration( - .commentable( - .preFormatted(service.documentation), - self.makeClientProtocol(for: service, in: codeGenerationRequest) - ) - ) - ) - codeBlocks.append( - .declaration(self.makeExtensionProtocol(for: service, in: codeGenerationRequest)) - ) - codeBlocks.append( - .declaration( - .commentable( - .preFormatted(service.documentation), - self.makeClientStruct(for: service, in: codeGenerationRequest) - ) - ) - ) + func translate(from request: CodeGenerationRequest) throws -> [CodeBlock] { + var blocks = [CodeBlock]() + + for service in request.services { + let `protocol` = self.makeClientProtocol(for: service, in: request) + blocks.append(.declaration(.commentable(.preFormatted(service.documentation), `protocol`))) + + let defaultImplementation = self.makeDefaultImplementation(for: service, in: request) + blocks.append(.declaration(defaultImplementation)) + + let sugaredAPI = self.makeSugaredAPI(forService: service, request: request) + blocks.append(.declaration(sugaredAPI)) + + let clientStruct = self.makeClientStruct(for: service, in: request) + blocks.append(.declaration(.commentable(.preFormatted(service.documentation), clientStruct))) } - return codeBlocks + + return blocks } } @@ -136,7 +129,7 @@ extension ClientCodeTranslator { return .guarded(self.availabilityGuard, clientProtocol) } - private func makeExtensionProtocol( + private func makeDefaultImplementation( for service: CodeGenerationRequest.ServiceDescriptor, in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { @@ -162,6 +155,225 @@ extension ClientCodeTranslator { ) } + private func makeSugaredAPI( + forService service: CodeGenerationRequest.ServiceDescriptor, + request: CodeGenerationRequest + ) -> Declaration { + let sugaredAPIExtension = Declaration.extension( + ExtensionDescription( + onType: "\(service.namespacedGeneratedName).ClientProtocol", + declarations: service.methods.map { method in + self.makeSugaredMethodDeclaration( + method: method, + accessModifier: self.accessModifier + ) + } + ) + ) + + return .guarded(self.availabilityGuard, sugaredAPIExtension) + } + + private func makeSugaredMethodDeclaration( + method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + accessModifier: AccessModifier? + ) -> Declaration { + let signature = FunctionSignatureDescription( + accessModifier: accessModifier, + kind: .function(name: method.name.generatedLowerCase), + generics: [.member("Result")], + parameters: self.makeParametersForSugaredMethodDeclaration(method: method), + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + whereClause: WhereClause( + requirements: [ + .conformance("Result", "Sendable") + ] + ) + ) + + let functionDescription = FunctionDescription( + signature: signature, + body: self.makeFunctionBodyForSugaredMethodDeclaration(method: method) + ) + + if method.documentation.isEmpty { + return .function(functionDescription) + } else { + return .commentable(.preFormatted(method.documentation), .function(functionDescription)) + } + } + + private func makeParametersForSugaredMethodDeclaration( + method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + ) -> [ParameterDescription] { + var parameters = [ParameterDescription]() + + // Unary inputs have a 'message' parameter + if !method.isInputStreaming { + parameters.append( + ParameterDescription( + label: "_", + name: "message", + type: .member([method.inputType]) + ) + ) + } + + parameters.append( + ParameterDescription( + label: "metadata", + type: .member(["GRPCCore", "Metadata"]), + defaultValue: .literal(.dictionary([])) + ) + ) + + parameters.append( + ParameterDescription( + label: "options", + type: .member(["GRPCCore", "CallOptions"]), + defaultValue: .memberAccess(.dot("defaults")) + ) + ) + + // Streaming inputs have a writer callback + if method.isInputStreaming { + parameters.append( + ParameterDescription( + label: "requestProducer", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .generic( + wrapper: .member(["GRPCCore", "RPCWriter"]), + wrapped: .member(method.inputType) + ) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Void"), + sendable: true, + escaping: true + ) + ) + ) + ) + } + + // All methods have a response handler. + var responseHandler = ParameterDescription(label: "onResponse", name: "handleResponse") + let responseKind = method.isOutputStreaming ? "Stream" : "Single" + responseHandler.type = .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .generic( + wrapper: .member(["GRPCCore", "ClientResponse", responseKind]), + wrapped: .member(method.outputType) + ) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + sendable: true, + escaping: true + ) + ) + + if !method.isOutputStreaming { + responseHandler.defaultValue = .closureInvocation( + ClosureInvocationDescription( + body: [.expression(.try(.identifierPattern("$0").dot("message")))] + ) + ) + } + + parameters.append(responseHandler) + + return parameters + } + + private func makeFunctionBodyForSugaredMethodDeclaration( + method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + ) -> [CodeBlock] { + // Produces the following: + // + // let request = GRPCCore.ClientRequest.Single(message: message, metadata: metadata) + // return try await method(request: request, options: options, responseHandler: responseHandler) + // + // or: + // + // let request = GRPCCore.ClientRequest.Stream(metadata: metadata, producer: writer) + // return try await method(request: request, options: options, responseHandler: responseHandler) + + // First, make the init for the ClientRequest + let requestType = method.isInputStreaming ? "Stream" : "Single" + var requestInit = FunctionCallDescription( + calledExpression: .identifier( + .type( + .generic( + wrapper: .member(["GRPCCore", "ClientRequest", requestType]), + wrapped: .member(method.inputType) + ) + ) + ) + ) + + if method.isInputStreaming { + requestInit.arguments.append( + FunctionArgumentDescription( + label: "metadata", + expression: .identifierPattern("metadata") + ) + ) + requestInit.arguments.append( + FunctionArgumentDescription( + label: "producer", + expression: .identifierPattern("requestProducer") + ) + ) + } else { + requestInit.arguments.append( + FunctionArgumentDescription( + label: "message", + expression: .identifierPattern("message") + ) + ) + requestInit.arguments.append( + FunctionArgumentDescription( + label: "metadata", + expression: .identifierPattern("metadata") + ) + ) + } + + // Now declare the request: + // + // let request = + let request = VariableDescription( + kind: .let, + left: .identifier(.pattern("request")), + right: .functionCall(requestInit) + ) + + var blocks = [CodeBlock]() + blocks.append(.declaration(.variable(request))) + + // Finally, call the underlying method. + let methodCall = FunctionCallDescription( + calledExpression: .identifierPattern("self").dot(method.name.generatedLowerCase), + arguments: [ + FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), + FunctionArgumentDescription(label: "options", expression: .identifierPattern("options")), + FunctionArgumentDescription(expression: .identifierPattern("handleResponse")), + ] + ) + + blocks.append(.expression(.return(.try(.await(.functionCall(methodCall)))))) + return blocks + } + private func makeClientProtocolMethod( for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor, diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift index a28e39ff5..b6f51da07 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -77,6 +77,10 @@ public protocol Grpc_Testing_EmptyServiceClientProtocol: Sendable {} extension Grpc_Testing_EmptyService.ClientProtocol { } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Grpc_Testing_EmptyService.ClientProtocol { +} + /// A service that has zero methods. /// See https://github.com/grpc/grpc/issues/15574 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index e31c5a835..c526584d8 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -686,6 +686,173 @@ extension Grpc_Testing_TestService.ClientProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Grpc_Testing_TestService.ClientProtocol { + /// One empty request followed by one empty response. + public func emptyCall( + _ message: Grpc_Testing_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.emptyCall( + request: request, + options: options, + handleResponse + ) + } + + /// One request followed by one response. + public func unaryCall( + _ message: Grpc_Testing_SimpleRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.unaryCall( + request: request, + options: options, + handleResponse + ) + } + + /// One request followed by one response. Response has cache control + /// headers set such that a caching HTTP proxy (such as GFE) can + /// satisfy subsequent requests. + public func cacheableUnaryCall( + _ message: Grpc_Testing_SimpleRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.cacheableUnaryCall( + request: request, + options: options, + handleResponse + ) + } + + /// One request followed by a sequence of responses (streamed download). + /// The server returns the payload with client desired type and sizes. + public func streamingOutputCall( + _ message: Grpc_Testing_StreamingOutputCallRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.streamingOutputCall( + request: request, + options: options, + handleResponse + ) + } + + /// A sequence of requests followed by one response (streamed upload). + /// The server returns the aggregated size of client payload as the result. + public func streamingInputCall( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.streamingInputCall( + request: request, + options: options, + handleResponse + ) + } + + /// A sequence of requests with each request served by the server immediately. + /// As one request could lead to multiple responses, this interface + /// demonstrates the idea of full duplexing. + public func fullDuplexCall( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.fullDuplexCall( + request: request, + options: options, + handleResponse + ) + } + + /// A sequence of requests followed by a sequence of responses. + /// The server buffers all the client requests and then serves them in order. A + /// stream of responses are returned to the client when the server starts with + /// first request. + public func halfDuplexCall( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.halfDuplexCall( + request: request, + options: options, + handleResponse + ) + } + + /// The test server will not implement this method. It will be used + /// to test the behavior when clients call unimplemented methods. + public func unimplementedCall( + _ message: Grpc_Testing_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.unimplementedCall( + request: request, + options: options, + handleResponse + ) + } +} + /// A simple service to test the various types of RPCs and experiment with /// performance with various types of payload. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -894,6 +1061,29 @@ extension Grpc_Testing_UnimplementedService.ClientProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Grpc_Testing_UnimplementedService.ClientProtocol { + /// A call that no server should implement + public func unimplementedCall( + _ message: Grpc_Testing_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.unimplementedCall( + request: request, + options: options, + handleResponse + ) + } +} + /// A simple service NOT implemented at servers so clients can test for /// that case. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -980,6 +1170,47 @@ extension Grpc_Testing_ReconnectService.ClientProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Grpc_Testing_ReconnectService.ClientProtocol { + public func start( + _ message: Grpc_Testing_ReconnectParams, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.start( + request: request, + options: options, + handleResponse + ) + } + + public func stop( + _ message: Grpc_Testing_Empty, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.stop( + request: request, + options: options, + handleResponse + ) + } +} + /// A service used to control reconnect server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService.ClientProtocol { diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 4407d9e76..b9a97ac10 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -253,6 +253,69 @@ extension Grpc_Health_V1_Health.ClientProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Grpc_Health_V1_Health.ClientProtocol { + /// Check gets the health of the specified service. If the requested service + /// is unknown, the call will fail with status NOT_FOUND. If the caller does + /// not specify a service name, the server should respond with its overall + /// health status. + /// + /// Clients should set a deadline when calling Check, and can declare the + /// server unhealthy if they do not receive a timely response. + /// + /// Check implementations should be idempotent and side effect free. + package func check( + _ message: Grpc_Health_V1_HealthCheckRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.check( + request: request, + options: options, + handleResponse + ) + } + + /// Performs a watch for the serving status of the requested service. + /// The server will immediately send back a message indicating the current + /// serving status. It will then subsequently send a new message whenever + /// the service's serving status changes. + /// + /// If the requested service is unknown when the call is received, the + /// server will send a message setting the serving status to + /// SERVICE_UNKNOWN but will *not* terminate the call. If at some + /// future point, the serving status of the service becomes known, the + /// server will send a new message with the service's serving status. + /// + /// If the call terminates with status UNIMPLEMENTED, then clients + /// should assume this method is not supported and should not retry the + /// call. If the call terminates with any other status (including OK), + /// clients should retry the call with appropriate exponential backoff. + package func watch( + _ message: Grpc_Health_V1_HealthCheckRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.watch( + request: request, + options: options, + handleResponse + ) + } +} + /// Health is gRPC's mechanism for checking whether a server is able to handle /// RPCs. Its semantics are documented in /// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 951901e83..73c12b54b 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -341,6 +341,109 @@ extension Grpc_Testing_BenchmarkService.ClientProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Grpc_Testing_BenchmarkService.ClientProtocol { + /// One request followed by one response. + /// The server returns the client payload as-is. + internal func unaryCall( + _ message: Grpc_Testing_SimpleRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.unaryCall( + request: request, + options: options, + handleResponse + ) + } + + /// Repeated sequence of one request followed by one response. + /// Should be called streaming ping-pong + /// The server returns the client payload as-is on each response + internal func streamingCall( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.streamingCall( + request: request, + options: options, + handleResponse + ) + } + + /// Single-sided unbounded streaming from client to server + /// The server returns the client payload as-is once the client does WritesDone + internal func streamingFromClient( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.streamingFromClient( + request: request, + options: options, + handleResponse + ) + } + + /// Single-sided unbounded streaming from server to client + /// The server repeatedly returns the client payload as-is + internal func streamingFromServer( + _ message: Grpc_Testing_SimpleRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.streamingFromServer( + request: request, + options: options, + handleResponse + ) + } + + /// Two-sided unbounded streaming between server to client + /// Both sides send the content of their own choice to the other + internal func streamingBothWays( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.streamingBothWays( + request: request, + options: options, + handleResponse + ) + } +} + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkService.ClientProtocol { private let client: GRPCCore.GRPCClient diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 5e7227bbf..da2a4ab66 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -281,6 +281,35 @@ final class Test_TextBasedRenderer: XCTestCase { ] """# ) + try _test( + .dictionary([]), + renderedBy: { $0.renderLiteral(_:) }, + rendersAs: #""" + [:] + """# + ) + try _test( + .dictionary([.init(key: .literal("foo"), value: .literal("bar"))]), + renderedBy: { $0.renderLiteral(_:) }, + rendersAs: #""" + [ + "foo": "bar" + ] + """# + ) + try _test( + .dictionary([ + .init(key: .literal("foo"), value: .literal("bar")), + .init(key: .literal("bar"), value: .literal("baz")), + ]), + renderedBy: { $0.renderLiteral(_:) }, + rendersAs: #""" + [ + "foo": "bar", + "bar": "baz" + ] + """# + ) } func testExpression() throws { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 10b4c770f..62ecf95c9 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -72,6 +72,28 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension NamespaceA_ServiceA.ClientProtocol { + /// Documentation for MethodA + public func methodA( + _ message: NamespaceA_ServiceARequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.methodA( + request: request, + options: options, + handleResponse + ) + } + } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { @@ -157,6 +179,28 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension NamespaceA_ServiceA.ClientProtocol { + /// Documentation for MethodA + public func methodA( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.methodA( + request: request, + options: options, + handleResponse + ) + } + } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { @@ -240,6 +284,26 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension NamespaceA_ServiceA.ClientProtocol { + /// Documentation for MethodA + public func methodA( + _ message: NamespaceA_ServiceARequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.methodA( + request: request, + options: options, + handleResponse + ) + } + } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { @@ -321,6 +385,26 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension NamespaceA_ServiceA.ClientProtocol { + /// Documentation for MethodA + public func methodA( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.methodA( + request: request, + options: options, + handleResponse + ) + } + } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { @@ -435,6 +519,46 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension NamespaceA_ServiceA.ClientProtocol { + /// Documentation for MethodA + package func methodA( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.methodA( + request: request, + options: options, + handleResponse + ) + } + + /// Documentation for MethodB + package func methodB( + _ message: NamespaceA_ServiceARequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.methodB( + request: request, + options: options, + handleResponse + ) + } + } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { @@ -538,6 +662,28 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { ) } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension ServiceA.ClientProtocol { + /// Documentation for MethodA + internal func methodA( + _ message: ServiceARequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.methodA( + request: request, + options: options, + handleResponse + ) + } + } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct ServiceAClient: ServiceA.ClientProtocol { @@ -605,6 +751,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension NamespaceA_ServiceA.ClientProtocol { + } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { @@ -622,6 +771,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceB.ClientProtocol { } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension ServiceB.ClientProtocol { + } /// Documentation for ServiceB /// /// Line 2 @@ -645,14 +797,16 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { private func assertClientCodeTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, - accessLevel: SourceGenerator.Configuration.AccessLevel + accessLevel: SourceGenerator.Configuration.AccessLevel, + file: StaticString = #filePath, + line: UInt = #line ) throws { let translator = ClientCodeTranslator(accessLevel: accessLevel) let codeBlocks = try translator.translate(from: codeGenerationRequest) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) let contents = renderer.renderedContents() - try XCTAssertEqualWithDiff(contents, expectedSwift) + try XCTAssertEqualWithDiff(contents, expectedSwift, file: file, line: line) } } diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index 2249463f8..7df9f4e41 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -275,6 +275,81 @@ extension Control.ClientProtocol { } } +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Control.ClientProtocol { + internal func unary( + _ message: ControlInput, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.unary( + request: request, + options: options, + handleResponse + ) + } + + internal func serverStream( + _ message: ControlInput, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.serverStream( + request: request, + options: options, + handleResponse + ) + } + + internal func clientStream( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.clientStream( + request: request, + options: options, + handleResponse + ) + } + + internal func bidiStream( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.bidiStream( + request: request, + options: options, + handleResponse + ) + } +} + /// A controllable service for testing. /// /// The control service has one RPC of each kind, the input to each RPC controls diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index d72a5190e..822b0d54a 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -119,6 +119,29 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension Hello_World_Greeter.ClientProtocol { + /// Sends a greeting. + internal func sayHello( + _ message: Hello_World_HelloRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.sayHello( + request: request, + options: options, + handleResponse + ) + } + } + /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct Hello_World_GreeterClient: Hello_World_Greeter.ClientProtocol { @@ -392,6 +415,29 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + extension Greeter.ClientProtocol { + /// Sends a greeting. + package func sayHello( + _ message: HelloRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.sayHello( + request: request, + options: options, + handleResponse + ) + } + } + /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct GreeterClient: Greeter.ClientProtocol { @@ -431,7 +477,9 @@ final class ProtobufCodeGeneratorTests: XCTestCase { visibility: SourceGenerator.Configuration.AccessLevel, client: Bool, server: Bool, - expectedCode: String + expectedCode: String, + file: StaticString = #filePath, + line: UInt = #line ) throws { let configs = SourceGenerator.Configuration( accessLevel: visibility, @@ -471,7 +519,9 @@ final class ProtobufCodeGeneratorTests: XCTestCase { protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), extraModuleImports: ["ExtraModule"] ), - expectedCode + expectedCode, + file: file, + line: line ) } } From 3dce993af91aa80338b44cbc0cfb7c72e0130adc Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Mon, 12 Aug 2024 15:35:40 +0100 Subject: [PATCH 426/580] Add explicit access-level modifier support for imports in code generator (#2010) ## Motivation This is a follow-up of https://github.com/grpc/grpc-swift/pull/2003. As of 5.9, Swift supports access-level modifiers on imports. In an upcoming Swift 6.x release, the default modifier will become `internal`. ## Modifications This PR adds explicit access-level modifiers to the generated code and a few other modules that use this generated code. ## Result Explicit access level imports present in more places. --- Package@swift-6.swift | 17 ++- .../v2/Echo/Generated/echo.grpc.swift | 4 +- .../v2/Echo/Subcommands/Collect.swift | 8 +- .../GRPCCodeGen/CodeGenerationRequest.swift | 9 +- .../Internal/Renderer/TextBasedRenderer.swift | 9 +- .../StructuredSwiftRepresentation.swift | 7 + .../IDLToStructuredSwiftTranslator.swift | 18 ++- .../ProtobufCodeGenParser.swift | 14 +- .../ProtobufCodeGenerator.swift | 3 +- .../Generated/empty_service.grpc.swift | 4 +- .../Generated/test.grpc.swift | 4 +- .../Health/Generated/health.grpc.swift | 4 +- Sources/Services/Health/Health.swift | 2 +- Sources/Services/Health/HealthService.swift | 2 +- .../performance-worker/BenchmarkClient.swift | 8 +- .../performance-worker/BenchmarkService.swift | 4 +- .../grpc_testing_benchmark_service.grpc.swift | 4 +- .../grpc_testing_worker_service.grpc.swift | 4 +- .../PerformanceWorker.swift | 10 +- Sources/performance-worker/RPCStats.swift | 6 +- .../Renderer/TextBasedRendererTests.swift | 39 +++++- ...uredSwiftTranslatorSnippetBasedTests.swift | 120 ++++++++++++------ .../ControlService.swift | 2 +- .../Generated/control.grpc.swift | 4 +- .../HTTP2TransportNIOPosixTests.swift | 8 +- ...P2TransportNIOTransportServicesTests.swift | 8 +- .../HTTP2TransportTests.swift | 12 +- .../HTTP2StatusCodeServer.swift | 10 +- .../Test Utilities/XCTest+Utilities.swift | 4 +- .../XCTestCase+Vsock.swift | 4 +- .../ProtobufCodeGenParserTests.swift | 9 +- .../ProtobufCodeGeneratorTests.swift | 24 ++-- 32 files changed, 253 insertions(+), 132 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 8a1a62aa3..6bf77a6f1 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -340,7 +340,11 @@ extension Target { .nioFileSystem, .argumentParser ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } @@ -538,7 +542,10 @@ extension Target { .grpcCore, .grpcProtobuf ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny") + ] ) } @@ -694,7 +701,11 @@ extension Target { .nioSSL, if: includeNIOSSL ), path: "Sources/Examples/v2/Echo", - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] ) } diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index 28cbec207..96e335ddb 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -21,8 +21,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -import GRPCCore -import GRPCProtobuf +internal import GRPCCore +internal import GRPCProtobuf internal enum Echo_Echo { internal static let descriptor = ServiceDescriptor.echo_Echo diff --git a/Sources/Examples/v2/Echo/Subcommands/Collect.swift b/Sources/Examples/v2/Echo/Subcommands/Collect.swift index 467e9c876..f723978a3 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Collect.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Collect.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import ArgumentParser -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix +internal import ArgumentParser +private import GRPCCore +private import GRPCHTTP2Core +private import GRPCHTTP2TransportNIOPosix @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Collect: AsyncParsableCommand { diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 1eb24780a..2175e88bf 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -84,7 +84,10 @@ public struct CodeGenerationRequest { public struct Dependency: Equatable { /// If the dependency is an item, the property's value is the item representation. /// If the dependency is a module, this property is nil. - public var item: Item? = nil + public var item: Item? + + /// The access level to be included in imports of this dependency. + public var accessLevel: SourceGenerator.Configuration.AccessLevel /// The name of the imported module or of the module an item is imported from. public var module: String @@ -102,12 +105,14 @@ public struct CodeGenerationRequest { item: Item? = nil, module: String, spi: String? = nil, - preconcurrency: PreconcurrencyRequirement = .notRequired + preconcurrency: PreconcurrencyRequirement = .notRequired, + accessLevel: SourceGenerator.Configuration.AccessLevel ) { self.item = item self.module = module self.spi = spi self.preconcurrency = preconcurrency + self.accessLevel = accessLevel } /// Represents an item imported from a module. diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 2cf326ab9..fe7f038a6 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -198,17 +198,20 @@ struct TextBasedRenderer: RendererProtocol { func render(preconcurrency: Bool) { let spiPrefix = description.spi.map { "@_spi(\($0)) " } ?? "" let preconcurrencyPrefix = preconcurrency ? "@preconcurrency " : "" + let accessLevel = description.accessLevel.map { "\($0) " } ?? "" if let item = description.item { writer.writeLine( - "\(preconcurrencyPrefix)\(spiPrefix)import \(item.kind) \(description.moduleName).\(item.name)" + "\(preconcurrencyPrefix)\(spiPrefix)\(accessLevel)import \(item.kind) \(description.moduleName).\(item.name)" ) } else if let moduleTypes = description.moduleTypes { for type in moduleTypes { - writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(type)") + writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)\(accessLevel)import \(type)") } } else { - writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(description.moduleName)") + writer.writeLine( + "\(preconcurrencyPrefix)\(spiPrefix)\(accessLevel)import \(description.moduleName)" + ) } } diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 2b6d3ebff..c6171f72f 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -31,6 +31,13 @@ /// /// For example: `import Foo`. struct ImportDescription: Equatable, Codable { + /// The access level of the imported module. + /// + /// For example, the `public` in `public import Foo`. + /// + /// - Note: This is optional, as explicit access-level modifiers are not required on `import` statements. + var accessLevel: AccessModifier? = nil + /// The name of the imported module. /// /// For example, the `Foo` in `import Foo`. diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 79dc6d2f2..8adfc7868 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -30,10 +30,9 @@ struct IDLToStructuredSwiftTranslator: Translator { server: server, accessLevel: accessLevel ) - let topComment = Comment.preFormatted(codeGenerationRequest.leadingTrivia) let imports = try codeGenerationRequest.dependencies.reduce( - into: [ImportDescription(moduleName: "GRPCCore")] + into: [ImportDescription(accessLevel: AccessModifier(accessLevel), moduleName: "GRPCCore")] ) { partialResult, newDependency in try partialResult.append(translateImport(dependency: newDependency)) } @@ -68,11 +67,24 @@ struct IDLToStructuredSwiftTranslator: Translator { } } +extension AccessModifier { + fileprivate init(_ accessLevel: SourceGenerator.Configuration.AccessLevel) { + switch accessLevel.level { + case .internal: self = .internal + case .package: self = .package + case .public: self = .public + } + } +} + extension IDLToStructuredSwiftTranslator { private func translateImport( dependency: CodeGenerationRequest.Dependency ) throws -> ImportDescription { - var importDescription = ImportDescription(moduleName: dependency.module) + var importDescription = ImportDescription( + accessLevel: AccessModifier(dependency.accessLevel), + moduleName: dependency.module + ) if let item = dependency.item { if let matchedKind = ImportDescription.Kind(rawValue: item.kind.value.rawValue) { importDescription.item = ImportDescription.Item(kind: matchedKind, name: item.name) diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index 824da05a6..2bbf29447 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -19,6 +19,7 @@ internal import SwiftProtobuf internal import SwiftProtobufPluginLibrary internal import struct GRPCCodeGen.CodeGenerationRequest +internal import struct GRPCCodeGen.SourceGenerator /// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. internal struct ProtobufCodeGenParser { @@ -26,11 +27,13 @@ internal struct ProtobufCodeGenParser { let namer: SwiftProtobufNamer let extraModuleImports: [String] let protoToModuleMappings: ProtoFileToModuleMappings + let accessLevel: SourceGenerator.Configuration.AccessLevel internal init( input: FileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings, - extraModuleImports: [String] + extraModuleImports: [String], + accessLevel: SourceGenerator.Configuration.AccessLevel ) { self.input = input self.extraModuleImports = extraModuleImports @@ -39,6 +42,7 @@ internal struct ProtobufCodeGenParser { currentFile: input, protoFileToModuleMappings: protoFileModuleMappings ) + self.accessLevel = accessLevel } internal func parse() throws -> CodeGenerationRequest { @@ -86,18 +90,20 @@ internal struct ProtobufCodeGenParser { extension ProtobufCodeGenParser { fileprivate var codeDependencies: [CodeGenerationRequest.Dependency] { - var codeDependencies: [CodeGenerationRequest.Dependency] = [.init(module: "GRPCProtobuf")] + var codeDependencies: [CodeGenerationRequest.Dependency] = [ + .init(module: "GRPCProtobuf", accessLevel: .internal) + ] // Adding as dependencies the modules containing generated code or types for // '.proto' files imported in the '.proto' file we are parsing. codeDependencies.append( contentsOf: (self.protoToModuleMappings.neededModules(forFile: self.input) ?? []).map { - CodeGenerationRequest.Dependency(module: $0) + CodeGenerationRequest.Dependency(module: $0, accessLevel: self.accessLevel) } ) // Adding extra imports passed in as an option to the plugin. codeDependencies.append( contentsOf: self.extraModuleImports.sorted().map { - CodeGenerationRequest.Dependency(module: $0) + CodeGenerationRequest.Dependency(module: $0, accessLevel: self.accessLevel) } ) return codeDependencies diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index 0fd4e7727..b612fb578 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -34,7 +34,8 @@ public struct ProtobufCodeGenerator { let parser = ProtobufCodeGenParser( input: fileDescriptor, protoFileModuleMappings: protoFileModuleMappings, - extraModuleImports: extraModuleImports + extraModuleImports: extraModuleImports, + accessLevel: self.configuration.accessLevel ) let sourceGenerator = SourceGenerator(configuration: self.configuration) diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift index b6f51da07..bd9f3b45d 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -21,8 +21,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -import GRPCCore -import GRPCProtobuf +public import GRPCCore +internal import GRPCProtobuf public enum Grpc_Testing_EmptyService { public static let descriptor = ServiceDescriptor.grpc_testing_EmptyService diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index c526584d8..78371b818 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -24,8 +24,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -import GRPCCore -import GRPCProtobuf +public import GRPCCore +internal import GRPCProtobuf public enum Grpc_Testing_ReconnectService { public static let descriptor = ServiceDescriptor.grpc_testing_ReconnectService diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index b9a97ac10..64f632007 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -24,8 +24,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -import GRPCCore -import GRPCProtobuf +package import GRPCCore +internal import GRPCProtobuf package enum Grpc_Health_V1_Health { package static let descriptor = ServiceDescriptor.grpc_health_v1_Health diff --git a/Sources/Services/Health/Health.swift b/Sources/Services/Health/Health.swift index f0cdb1a34..e2d5b33a5 100644 --- a/Sources/Services/Health/Health.swift +++ b/Sources/Services/Health/Health.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +public import GRPCCore /// ``Health`` is gRPC’s mechanism for checking whether a server is able to handle RPCs. Its semantics are documented in /// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. diff --git a/Sources/Services/Health/HealthService.swift b/Sources/Services/Health/HealthService.swift index b8b54e80c..e7081b889 100644 --- a/Sources/Services/Health/HealthService.swift +++ b/Sources/Services/Health/HealthService.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore @available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 1098244d1..0541cc4cb 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import Atomics -import Foundation -import GRPCCore -import NIOConcurrencyHelpers +private import Atomics +private import Foundation +internal import GRPCCore +private import NIOConcurrencyHelpers @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct BenchmarkClient { diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift index 45dd1a972..694ad51b7 100644 --- a/Sources/performance-worker/BenchmarkService.swift +++ b/Sources/performance-worker/BenchmarkService.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import Atomics -import GRPCCore +private import Atomics +internal import GRPCCore import struct Foundation.Data diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 73c12b54b..20d7b9e16 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -24,8 +24,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -import GRPCCore -import GRPCProtobuf +internal import GRPCCore +internal import GRPCProtobuf internal enum Grpc_Testing_BenchmarkService { internal static let descriptor = ServiceDescriptor.grpc_testing_BenchmarkService diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index 2a702944f..9eafeccfd 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -24,8 +24,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -import GRPCCore -import GRPCProtobuf +internal import GRPCCore +internal import GRPCProtobuf internal enum Grpc_Testing_WorkerService { internal static let descriptor = ServiceDescriptor.grpc_testing_WorkerService diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift index ca8c5005b..7f1493579 100644 --- a/Sources/performance-worker/PerformanceWorker.swift +++ b/Sources/performance-worker/PerformanceWorker.swift @@ -14,11 +14,11 @@ * limitations under the License. */ -import ArgumentParser -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import NIOPosix +internal import ArgumentParser +private import GRPCCore +private import GRPCHTTP2Core +private import GRPCHTTP2TransportNIOPosix +private import NIOPosix @main @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Sources/performance-worker/RPCStats.swift b/Sources/performance-worker/RPCStats.swift index bc2bba74b..ef0703d9e 100644 --- a/Sources/performance-worker/RPCStats.swift +++ b/Sources/performance-worker/RPCStats.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import Foundation -import GRPCCore -import NIOConcurrencyHelpers +private import Foundation +internal import GRPCCore +internal import NIOConcurrencyHelpers /// Stores the real time latency histogram and error code count dictionary, /// for the RPCs made by a particular GRPCClient. It gets updated after diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index da2a4ab66..65b5eb79b 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -110,11 +110,24 @@ final class Test_TextBasedRenderer: XCTestCase { func testImports() throws { try _test(nil, renderedBy: { $0.renderImports(_:) }, rendersAs: "") try _test( - [ImportDescription(moduleName: "Foo"), ImportDescription(moduleName: "Bar")], + [ + ImportDescription(moduleName: "Foo"), + ImportDescription(moduleName: "Bar"), + ImportDescription(accessLevel: .fileprivate, moduleName: "BazFileprivate"), + ImportDescription(accessLevel: .private, moduleName: "BazPrivate"), + ImportDescription(accessLevel: .internal, moduleName: "BazInternal"), + ImportDescription(accessLevel: .package, moduleName: "BazPackage"), + ImportDescription(accessLevel: .public, moduleName: "BazPublic"), + ], renderedBy: { $0.renderImports(_:) }, rendersAs: #""" import Foo import Bar + fileprivate import BazFileprivate + private import BazPrivate + internal import BazInternal + package import BazPackage + public import BazPublic """# ) try _test( @@ -125,7 +138,12 @@ final class Test_TextBasedRenderer: XCTestCase { """# ) try _test( - [ImportDescription(moduleName: "Foo", preconcurrency: .onOS(["Bar", "Baz"]))], + [ + ImportDescription( + moduleName: "Foo", + preconcurrency: .onOS(["Bar", "Baz"]) + ) + ], renderedBy: { $0.renderImports(_:) }, rendersAs: #""" #if os(Bar) || os(Baz) @@ -138,7 +156,11 @@ final class Test_TextBasedRenderer: XCTestCase { try _test( [ ImportDescription(moduleName: "Foo", preconcurrency: .always), - ImportDescription(moduleName: "Bar", spi: "Secret", preconcurrency: .always), + ImportDescription( + moduleName: "Bar", + spi: "Secret", + preconcurrency: .always + ), ], renderedBy: { $0.renderImports(_:) }, rendersAs: #""" @@ -169,8 +191,14 @@ final class Test_TextBasedRenderer: XCTestCase { moduleName: "Foo", item: ImportDescription.Item(kind: .protocol, name: "Bat") ), - ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .let, name: "Bam")), - ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .var, name: "Bag")), + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .let, name: "Bam") + ), + ImportDescription( + moduleName: "Foo", + item: ImportDescription.Item(kind: .var, name: "Bag") + ), ImportDescription( moduleName: "Foo", item: ImportDescription.Item(kind: .func, name: "Bak") @@ -952,7 +980,6 @@ final class Test_TextBasedRenderer: XCTestCase { } extension Test_TextBasedRenderer { - func _test( _ input: Input, renderedBy renderClosure: (TextBasedRenderer) -> ((Input) -> String), diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index a023c6ddc..8b174d439 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -27,46 +27,78 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testImports() throws { var dependencies = [CodeGenerationRequest.Dependency]() - dependencies.append(CodeGenerationRequest.Dependency(module: "Foo")) + dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", accessLevel: .public)) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .typealias, name: "Bar"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .typealias, name: "Bar"), + module: "Foo", + accessLevel: .internal + ) ) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .struct, name: "Baz"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .struct, name: "Baz"), + module: "Foo", + accessLevel: .package + ) ) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .class, name: "Bac"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .class, name: "Bac"), + module: "Foo", + accessLevel: .package + ) ) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .enum, name: "Bap"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .enum, name: "Bap"), + module: "Foo", + accessLevel: .package + ) ) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .protocol, name: "Bat"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .protocol, name: "Bat"), + module: "Foo", + accessLevel: .package + ) ) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .let, name: "Baq"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .let, name: "Baq"), + module: "Foo", + accessLevel: .package + ) ) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .var, name: "Bag"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .var, name: "Bag"), + module: "Foo", + accessLevel: .package + ) ) dependencies.append( - CodeGenerationRequest.Dependency(item: .init(kind: .func, name: "Bak"), module: "Foo") + CodeGenerationRequest.Dependency( + item: .init(kind: .func, name: "Bak"), + module: "Foo", + accessLevel: .package + ) ) let expectedSwift = """ /// Some really exciting license header 2023. - import GRPCCore - import Foo - import typealias Foo.Bar - import struct Foo.Baz - import class Foo.Bac - import enum Foo.Bap - import protocol Foo.Bat - import let Foo.Baq - import var Foo.Bag - import func Foo.Bak + public import GRPCCore + public import Foo + internal import typealias Foo.Bar + package import struct Foo.Baz + package import class Foo.Bac + package import enum Foo.Bap + package import protocol Foo.Bat + package import let Foo.Baq + package import var Foo.Bag + package import func Foo.Bak """ try self.assertIDLToStructuredSwiftTranslation( @@ -78,31 +110,39 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testPreconcurrencyImports() throws { var dependencies = [CodeGenerationRequest.Dependency]() - dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", preconcurrency: .required)) + dependencies.append( + CodeGenerationRequest.Dependency( + module: "Foo", + preconcurrency: .required, + accessLevel: .internal + ) + ) dependencies.append( CodeGenerationRequest.Dependency( item: .init(kind: .enum, name: "Bar"), module: "Foo", - preconcurrency: .required + preconcurrency: .required, + accessLevel: .internal ) ) dependencies.append( CodeGenerationRequest.Dependency( module: "Baz", - preconcurrency: .requiredOnOS(["Deq", "Der"]) + preconcurrency: .requiredOnOS(["Deq", "Der"]), + accessLevel: .internal ) ) let expectedSwift = """ /// Some really exciting license header 2023. - import GRPCCore - @preconcurrency import Foo - @preconcurrency import enum Foo.Bar + public import GRPCCore + @preconcurrency internal import Foo + @preconcurrency internal import enum Foo.Bar #if os(Deq) || os(Der) - @preconcurrency import Baz + @preconcurrency internal import Baz #else - import Baz + internal import Baz #endif """ @@ -115,12 +155,15 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSPIImports() throws { var dependencies = [CodeGenerationRequest.Dependency]() - dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret")) + dependencies.append( + CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) + ) dependencies.append( CodeGenerationRequest.Dependency( item: .init(kind: .enum, name: "Bar"), module: "Foo", - spi: "Secret" + spi: "Secret", + accessLevel: .internal ) ) @@ -128,9 +171,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { """ /// Some really exciting license header 2023. - import GRPCCore - @_spi(Secret) import Foo - @_spi(Secret) import enum Foo.Bar + public import GRPCCore + @_spi(Secret) internal import Foo + @_spi(Secret) internal import enum Foo.Bar """ try self.assertIDLToStructuredSwiftTranslation( @@ -142,12 +185,15 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testGeneration() throws { var dependencies = [CodeGenerationRequest.Dependency]() - dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret")) + dependencies.append( + CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) + ) dependencies.append( CodeGenerationRequest.Dependency( item: .init(kind: .enum, name: "Bar"), module: "Foo", - spi: "Secret" + spi: "Secret", + accessLevel: .internal ) ) @@ -166,9 +212,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { """ /// Some really exciting license header 2023. - import GRPCCore - @_spi(Secret) import Foo - @_spi(Secret) import enum Foo.Bar + public import GRPCCore + @_spi(Secret) internal import Foo + @_spi(Secret) internal import enum Foo.Bar public enum NamespaceA_ServiceA { public static let descriptor = ServiceDescriptor.namespaceA_ServiceA diff --git a/Tests/GRPCHTTP2TransportTests/ControlService.swift b/Tests/GRPCHTTP2TransportTests/ControlService.swift index dfb83b132..e9d5b322f 100644 --- a/Tests/GRPCHTTP2TransportTests/ControlService.swift +++ b/Tests/GRPCHTTP2TransportTests/ControlService.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore import struct Foundation.Data diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index 7df9f4e41..b12c0ccb0 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -22,8 +22,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -import GRPCCore -import GRPCProtobuf +internal import GRPCCore +internal import GRPCProtobuf internal enum Control { internal static let descriptor = ServiceDescriptor.Control diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index 252fbc29f..0b53135d8 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import XCTest +private import GRPCCore +private import GRPCHTTP2Core +private import GRPCHTTP2TransportNIOPosix +internal import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportNIOPosixTests: XCTestCase { diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index cca04cf05..421a419f3 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -15,10 +15,10 @@ */ #if canImport(Network) -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOTransportServices -import XCTest +private import GRPCCore +private import GRPCHTTP2Core +internal import GRPCHTTP2TransportNIOTransportServices +internal import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportNIOTransportServicesTests: XCTestCase { diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 9b81d7dbe..ddca6d01b 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -14,12 +14,12 @@ * limitations under the License. */ -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import GRPCHTTP2TransportNIOTransportServices -import GRPCProtobuf -import XCTest +internal import GRPCCore +private import GRPCHTTP2Core +private import GRPCHTTP2TransportNIOPosix +private import GRPCHTTP2TransportNIOTransportServices +private import GRPCProtobuf +internal import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportTests: XCTestCase { diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift index d4f6e00ee..3e1df197e 100644 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift @@ -14,11 +14,11 @@ * limitations under the License. */ -import GRPCHTTP2Core -import NIOCore -import NIOHPACK -import NIOHTTP2 -import NIOPosix +internal import GRPCHTTP2Core +private import NIOCore +private import NIOHPACK +private import NIOHTTP2 +private import NIOPosix /// An HTTP/2 test server which only responds to request headers by sending response headers and /// then closing. Each stream will be closed with the ":status" set to the value of the diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift index 4cef512d0..053065da4 100644 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import GRPCCore -import XCTest + +private import XCTest func XCTAssertThrowsError( ofType: E.Type, diff --git a/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift b/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift index cb613fb63..d66676c99 100644 --- a/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift +++ b/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import NIOPosix -import XCTest +private import NIOPosix +internal import XCTest extension XCTestCase { func vsockAvailable() -> Bool { diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 12241b53e..8cfc72fe7 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -55,7 +55,8 @@ final class ProtobufCodeGenParserTests: XCTestCase { let parsedCodeGenRequest = try ProtobufCodeGenParser( input: fileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"] + extraModuleImports: ["ExtraModule"], + accessLevel: .internal ).parse() self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) @@ -136,7 +137,8 @@ final class ProtobufCodeGenParserTests: XCTestCase { let parsedCodeGenRequest = try ProtobufCodeGenParser( input: fileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"] + extraModuleImports: ["ExtraModule"], + accessLevel: .internal ).parse() self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) @@ -217,7 +219,8 @@ final class ProtobufCodeGenParserTests: XCTestCase { let parsedCodeGenRequest = try ProtobufCodeGenParser( input: fileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"] + extraModuleImports: ["ExtraModule"], + accessLevel: .internal ).parse() self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 822b0d54a..03cd693e3 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -54,10 +54,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift - import GRPCCore - import GRPCProtobuf - import DifferentModule - import ExtraModule + internal import GRPCCore + internal import GRPCProtobuf + internal import DifferentModule + internal import ExtraModule internal enum Hello_World_Greeter { internal static let descriptor = ServiceDescriptor.hello_world_Greeter @@ -204,10 +204,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift - import GRPCCore - import GRPCProtobuf - import DifferentModule - import ExtraModule + public import GRPCCore + internal import GRPCProtobuf + public import DifferentModule + public import ExtraModule public enum Helloworld_Greeter { public static let descriptor = ServiceDescriptor.helloworld_Greeter @@ -307,10 +307,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift - import GRPCCore - import GRPCProtobuf - import DifferentModule - import ExtraModule + package import GRPCCore + internal import GRPCProtobuf + package import DifferentModule + package import ExtraModule package enum Greeter { package static let descriptor = ServiceDescriptor.Greeter From a2e6784cfbbd22df0e8116b869bca3cfb61e150b Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:33:23 +0100 Subject: [PATCH 427/580] Use fully qualified types in generated code (#2012) Motivation: The generated code has possible namespace collisions between user-defined types and gRPC types. Modifications: - Adjust `GRPCCodeGen` and `GRPCProtobufCodeGen` to use the fully qualified names of gRPC types when generating code. Result: This will help avoid namespace collision between user-defined types and gRPC types. --------- Co-authored-by: George Barnett --- .../v2/Echo/Generated/echo.grpc.swift | 180 +++---- .../Translator/ClientCodeTranslator.swift | 42 +- .../Translator/ServerCodeTranslator.swift | 32 +- .../Translator/TypealiasTranslator.swift | 18 +- .../ProtobufCodeGenParser.swift | 4 +- .../Generated/empty_service.grpc.swift | 6 +- .../Generated/test.grpc.swift | 508 +++++++++--------- .../Health/Generated/health.grpc.swift | 96 ++-- .../grpc_testing_benchmark_service.grpc.swift | 220 ++++---- .../grpc_testing_worker_service.grpc.swift | 58 +- ...lientCodeTranslatorSnippetBasedTests.swift | 210 ++++---- ...uredSwiftTranslatorSnippetBasedTests.swift | 6 +- ...erverCodeTranslatorSnippetBasedTests.swift | 84 +-- .../Internal/Translator/TestFunctions.swift | 4 +- ...TypealiasTranslatorSnippetBasedTests.swift | 104 ++-- .../Generated/control.grpc.swift | 180 +++---- .../ProtobufCodeGenParserTests.swift | 12 +- .../ProtobufCodeGeneratorTests.swift | 112 ++-- 18 files changed, 939 insertions(+), 937 deletions(-) diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift index 96e335ddb..6c5f82d29 100644 --- a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift +++ b/Sources/Examples/v2/Echo/Generated/echo.grpc.swift @@ -25,12 +25,12 @@ internal import GRPCCore internal import GRPCProtobuf internal enum Echo_Echo { - internal static let descriptor = ServiceDescriptor.echo_Echo + internal static let descriptor = GRPCCore.ServiceDescriptor.echo_Echo internal enum Method { internal enum Get { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Echo_Echo.descriptor.fullyQualifiedService, method: "Get" ) @@ -38,7 +38,7 @@ internal enum Echo_Echo { internal enum Expand { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Echo_Echo.descriptor.fullyQualifiedService, method: "Expand" ) @@ -46,7 +46,7 @@ internal enum Echo_Echo { internal enum Collect { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Echo_Echo.descriptor.fullyQualifiedService, method: "Collect" ) @@ -54,12 +54,12 @@ internal enum Echo_Echo { internal enum Update { internal typealias Input = Echo_EchoRequest internal typealias Output = Echo_EchoResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Echo_Echo.descriptor.fullyQualifiedService, method: "Update" ) } - internal static let descriptors: [MethodDescriptor] = [ + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ Get.descriptor, Expand.descriptor, Collect.descriptor, @@ -76,7 +76,7 @@ internal enum Echo_Echo { internal typealias Client = Echo_EchoClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { internal static let echo_Echo = Self( package: "echo", service: "Echo" @@ -86,16 +86,16 @@ extension ServiceDescriptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Immediately returns an echo of a request. - func get(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func get(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Splits a request into words and returns each word in a stream of messages. - func expand(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func expand(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func collect(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Streams back messages as they are received in an input stream. - func update(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func update(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -105,32 +105,32 @@ extension Echo_Echo.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Echo_Echo.Method.Get.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.get(request: request) } ) router.registerHandler( forMethod: Echo_Echo.Method.Expand.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.expand(request: request) } ) router.registerHandler( forMethod: Echo_Echo.Method.Collect.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.collect(request: request) } ) router.registerHandler( forMethod: Echo_Echo.Method.Update.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.update(request: request) } @@ -141,34 +141,34 @@ extension Echo_Echo.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { /// Immediately returns an echo of a request. - func get(request: ServerRequest.Single) async throws -> ServerResponse.Single + func get(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single /// Splits a request into words and returns each word in a stream of messages. - func expand(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func expand(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func collect(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single /// Streams back messages as they are received in an input stream. - func update(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func update(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `Echo_EchoStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ServiceProtocol { - internal func get(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.get(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + internal func get(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.get(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - internal func expand(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.expand(request: ServerRequest.Single(stream: request)) + internal func expand(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.expand(request: GRPCCore.ServerRequest.Single(stream: request)) return response } - internal func collect(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func collect(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { let response = try await self.collect(request: request) - return ServerResponse.Stream(single: response) + return GRPCCore.ServerResponse.Stream(single: response) } } @@ -176,98 +176,98 @@ extension Echo_Echo.ServiceProtocol { internal protocol Echo_EchoClientProtocol: Sendable { /// Immediately returns an echo of a request. func get( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Splits a request into words and returns each word in a stream of messages. func expand( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// Collects a stream of messages and returns them concatenated when the caller closes. func collect( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Streams back messages as they are received in an input stream. func update( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ClientProtocol { internal func get( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.get( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func expand( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.expand( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func collect( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.collect( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func update( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.update( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -363,11 +363,11 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { /// Immediately returns an echo of a request. internal func get( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -383,11 +383,11 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { /// Splits a request into words and returns each word in a stream of messages. internal func expand( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -401,11 +401,11 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { /// Collects a stream of messages and returns them concatenated when the caller closes. internal func collect( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -421,11 +421,11 @@ internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { /// Streams back messages as they are received in an input stream. internal func update( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 9ba625e47..b8f123e18 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -25,26 +25,26 @@ /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarClientProtocol: Sendable { /// func baz( -/// request: ClientRequest.Single, -/// serializer: some MessageSerializer, -/// deserializer: some MessageDeserializer, -/// options: CallOptions = .defaults, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R +/// request: GRPCCore.ClientRequest.Single, +/// serializer: some GRPCCore.MessageSerializer, +/// deserializer: some GRPCCore.MessageDeserializer, +/// options: GRPCCore.CallOptions = .defaults, +/// _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R /// ) async throws -> R where R: Sendable /// } /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// extension Foo_Bar.ClientProtocol { /// public func baz( -/// request: ClientRequest.Single, -/// options: CallOptions = .defaults, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { +/// request: GRPCCore.ClientRequest.Single, +/// options: GRPCCore.CallOptions = .defaults, +/// _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { /// try $0.message /// } /// ) async throws -> R where R: Sendable { /// try await self.baz( /// request: request, -/// serializer: ProtobufSerializer(), -/// deserializer: ProtobufDeserializer(), +/// serializer: GRPCProtobuf.ProtobufSerializer(), +/// deserializer: GRPCProtobuf.ProtobufDeserializer(), /// options: options, /// body /// ) @@ -56,11 +56,11 @@ /// self.client = client /// } /// public func methodA( -/// request: ClientRequest.Stream, -/// serializer: some MessageSerializer, -/// deserializer: some MessageDeserializer, -/// options: CallOptions = .defaults, -/// _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { +/// request: GRPCCore.ClientRequest.Stream, +/// serializer: some GRPCCore.MessageSerializer, +/// deserializer: some GRPCCore.MessageDeserializer, +/// options: GRPCCore.CallOptions = .defaults, +/// _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { /// try $0.message /// } /// ) async throws -> R where R: Sendable { @@ -472,7 +472,7 @@ extension ClientCodeTranslator { parameters.append( ParameterDescription( label: "options", - type: .member("CallOptions"), + type: .member(["GRPCCore", "CallOptions"]), defaultValue: includeDefaultCallOptions ? .memberAccess(MemberAccessDescription(right: "defaults")) : nil ) @@ -491,7 +491,9 @@ extension ClientCodeTranslator { in service: CodeGenerationRequest.ServiceDescriptor ) -> ParameterDescription { let requestType = method.isInputStreaming ? "Stream" : "Single" - let clientRequestType = ExistingTypeDescription.member(["ClientRequest", requestType]) + let clientRequestType = ExistingTypeDescription.member([ + "GRPCCore", "ClientRequest", requestType, + ]) return ParameterDescription( label: "request", type: .generic( @@ -509,7 +511,7 @@ extension ClientCodeTranslator { label: "serializer", type: ExistingTypeDescription.some( .generic( - wrapper: .member("MessageSerializer"), + wrapper: .member(["GRPCCore", "MessageSerializer"]), wrapped: .member(method.inputType) ) ) @@ -524,7 +526,7 @@ extension ClientCodeTranslator { label: "deserializer", type: ExistingTypeDescription.some( .generic( - wrapper: .member("MessageDeserializer"), + wrapper: .member(["GRPCCore", "MessageDeserializer"]), wrapped: .member(method.outputType) ) ) @@ -538,7 +540,7 @@ extension ClientCodeTranslator { ) -> ParameterDescription { let clientStreaming = method.isOutputStreaming ? "Stream" : "Single" let closureParameterType = ExistingTypeDescription.generic( - wrapper: .member(["ClientResponse", clientStreaming]), + wrapper: .member(["GRPCCore", "ClientResponse", clientStreaming]), wrapped: .member(method.outputType) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 391a8d002..313c3410e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -25,8 +25,8 @@ /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// func baz( -/// request: ServerRequest.Stream -/// ) async throws -> ServerResponse.Stream +/// request: GRPCCore.ServerRequest.Stream +/// ) async throws -> GRPCCore.ServerResponse.Stream /// } /// // Conformance to `GRPCCore.RegistrableRPCService`. /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -34,8 +34,8 @@ /// public func registerMethods(with router: inout GRPCCore.RPCRouter) { /// router.registerHandler( /// forMethod: Foo_Bar.Method.baz.descriptor, -/// deserializer: ProtobufDeserializer(), -/// serializer: ProtobufSerializer(), +/// deserializer: GRPCProtobuf.ProtobufDeserializer(), +/// serializer: GRPCProtobuf.ProtobufSerializer(), /// handler: { request in try await self.baz(request: request) } /// ) /// } @@ -43,17 +43,17 @@ /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarServiceProtocol: Foo_Bar.StreamingServiceProtocol { /// func baz( -/// request: ServerRequest.Single -/// ) async throws -> ServerResponse.Single +/// request: GRPCCore.ServerRequest.Single +/// ) async throws -> GRPCCore.ServerResponse.Single /// } /// // Partial conformance to `Foo_BarStreamingServiceProtocol`. /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// extension Foo_Bar.ServiceProtocol { /// public func baz( -/// request: ServerRequest.Stream -/// ) async throws -> ServerResponse.Stream { -/// let response = try await self.baz(request: ServerRequest.Single(stream: request)) -/// return ServerResponse.Stream(single: response) +/// request: GRPCCore.ServerRequest.Stream +/// ) async throws -> GRPCCore.ServerResponse.Stream { +/// let response = try await self.baz(request: GRPCCore.ServerRequest.Single(stream: request)) +/// return GRPCCore.ServerResponse.Stream(single: response) /// } /// } ///``` @@ -147,7 +147,7 @@ extension ServerCodeTranslator { .init( label: "request", type: .generic( - wrapper: .member(["ServerRequest", "Stream"]), + wrapper: .member(["GRPCCore", "ServerRequest", "Stream"]), wrapped: .member(method.inputType) ) ) @@ -155,7 +155,7 @@ extension ServerCodeTranslator { keywords: [.async, .throws], returnType: .identifierType( .generic( - wrapper: .member(["ServerResponse", "Stream"]), + wrapper: .member(["GRPCCore", "ServerResponse", "Stream"]), wrapped: .member(method.outputType) ) ) @@ -322,7 +322,7 @@ extension ServerCodeTranslator { label: "request", type: .generic( - wrapper: .member(["ServerRequest", inputStreaming]), + wrapper: .member(["GRPCCore", "ServerRequest", inputStreaming]), wrapped: .member(method.inputType) ) ) @@ -330,7 +330,7 @@ extension ServerCodeTranslator { keywords: [.async, .throws], returnType: .identifierType( .generic( - wrapper: .member(["ServerResponse", outputStreaming]), + wrapper: .member(["GRPCCore", "ServerResponse", outputStreaming]), wrapped: .member(method.outputType) ) ) @@ -390,7 +390,7 @@ extension ServerCodeTranslator { serverRequest = Expression.functionCall( calledExpression: .memberAccess( MemberAccessDescription( - left: .identifierPattern("ServerRequest"), + left: .identifierPattern("GRPCCore.ServerRequest"), right: "Single" ) ), @@ -429,7 +429,7 @@ extension ServerCodeTranslator { returnValue = .functionCall( calledExpression: .memberAccess( MemberAccessDescription( - left: .identifierType(.member(["ServerResponse"])), + left: .identifierType(.member(["GRPCCore", "ServerResponse"])), right: "Stream" ) ), diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 26d261717..7e5c7b559 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -22,13 +22,13 @@ /// a representation for the following generated code: /// ```swift /// public enum Echo_Echo { -/// public static let descriptor = ServiceDescriptor.echo_Echo +/// public static let descriptor = GRPCCore.ServiceDescriptor.echo_Echo /// /// public enum Method { /// public enum Get { /// public typealias Input = Echo_EchoRequest /// public typealias Output = Echo_EchoResponse -/// public static let descriptor = MethodDescriptor( +/// public static let descriptor = GRPCCore.MethodDescriptor( /// service: Echo_Echo.descriptor.fullyQualifiedService, /// method: "Get" /// ) @@ -37,14 +37,14 @@ /// public enum Collect { /// public typealias Input = Echo_EchoRequest /// public typealias Output = Echo_EchoResponse -/// public static let descriptor = MethodDescriptor( +/// public static let descriptor = GRPCCore.MethodDescriptor( /// service: Echo_Echo.descriptor.fullyQualifiedService, /// method: "Collect" /// ) /// } /// // ... /// -/// public static let descriptors: [MethodDescriptor] = [ +/// public static let descriptors: [GRPCCore.MethodDescriptor] = [ /// Get.descriptor, /// Collect.descriptor, /// // ... @@ -58,7 +58,7 @@ /// /// } /// -/// extension ServiceDescriptor { +/// extension GRPCCore.ServiceDescriptor { /// public static let echo_Echo = Self( /// package: "echo", /// service: "Echo" @@ -199,7 +199,7 @@ extension TypealiasTranslator { let descriptorDeclarationLeft = Expression.identifier(.pattern("descriptor")) let descriptorDeclarationRight = Expression.functionCall( FunctionCallDescription( - calledExpression: .identifierType(.member("MethodDescriptor")), + calledExpression: .identifierType(.member(["GRPCCore", "MethodDescriptor"])), arguments: [ FunctionArgumentDescription( label: "service", @@ -245,7 +245,7 @@ extension TypealiasTranslator { isStatic: true, kind: .let, left: .identifier(.pattern("descriptors")), - type: .array(.member("MethodDescriptor")), + type: .array(.member(["GRPCCore", "MethodDescriptor"])), right: .literal(.array(methodDescriptors)) ) } @@ -326,7 +326,7 @@ extension TypealiasTranslator { left: .identifierPattern("descriptor"), right: .memberAccess( MemberAccessDescription( - left: .identifierPattern("ServiceDescriptor"), + left: .identifierPattern("GRPCCore.ServiceDescriptor"), right: serviceIdentifier ) ) @@ -356,7 +356,7 @@ extension TypealiasTranslator { return .extension( ExtensionDescription( - onType: "ServiceDescriptor", + onType: "GRPCCore.ServiceDescriptor", declarations: [ .variable( VariableDescription( diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index 2bbf29447..de36e0151 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -63,10 +63,10 @@ internal struct ProtobufCodeGenParser { """ let lookupSerializer: (String) -> String = { messageType in - "ProtobufSerializer<\(messageType)>()" + "GRPCProtobuf.ProtobufSerializer<\(messageType)>()" } let lookupDeserializer: (String) -> String = { messageType in - "ProtobufDeserializer<\(messageType)>()" + "GRPCProtobuf.ProtobufDeserializer<\(messageType)>()" } let services = self.input.services.map { CodeGenerationRequest.ServiceDescriptor( diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift index bd9f3b45d..ede7a37ea 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift @@ -25,9 +25,9 @@ public import GRPCCore internal import GRPCProtobuf public enum Grpc_Testing_EmptyService { - public static let descriptor = ServiceDescriptor.grpc_testing_EmptyService + public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_EmptyService public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Grpc_Testing_EmptyServiceStreamingServiceProtocol @@ -39,7 +39,7 @@ public enum Grpc_Testing_EmptyService { public typealias Client = Grpc_Testing_EmptyServiceClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { public static let grpc_testing_EmptyService = Self( package: "grpc.testing", service: "EmptyService" diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index 78371b818..5fed75d22 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -28,12 +28,12 @@ public import GRPCCore internal import GRPCProtobuf public enum Grpc_Testing_ReconnectService { - public static let descriptor = ServiceDescriptor.grpc_testing_ReconnectService + public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_ReconnectService public enum Method { public enum Start { public typealias Input = Grpc_Testing_ReconnectParams public typealias Output = Grpc_Testing_Empty - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_ReconnectService.descriptor.fullyQualifiedService, method: "Start" ) @@ -41,12 +41,12 @@ public enum Grpc_Testing_ReconnectService { public enum Stop { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_ReconnectInfo - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_ReconnectService.descriptor.fullyQualifiedService, method: "Stop" ) } - public static let descriptors: [MethodDescriptor] = [ + public static let descriptors: [GRPCCore.MethodDescriptor] = [ Start.descriptor, Stop.descriptor ] @@ -61,7 +61,7 @@ public enum Grpc_Testing_ReconnectService { public typealias Client = Grpc_Testing_ReconnectServiceClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { public static let grpc_testing_ReconnectService = Self( package: "grpc.testing", service: "ReconnectService" @@ -69,12 +69,12 @@ extension ServiceDescriptor { } public enum Grpc_Testing_TestService { - public static let descriptor = ServiceDescriptor.grpc_testing_TestService + public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_TestService public enum Method { public enum EmptyCall { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_Empty - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "EmptyCall" ) @@ -82,7 +82,7 @@ public enum Grpc_Testing_TestService { public enum UnaryCall { public typealias Input = Grpc_Testing_SimpleRequest public typealias Output = Grpc_Testing_SimpleResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "UnaryCall" ) @@ -90,7 +90,7 @@ public enum Grpc_Testing_TestService { public enum CacheableUnaryCall { public typealias Input = Grpc_Testing_SimpleRequest public typealias Output = Grpc_Testing_SimpleResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "CacheableUnaryCall" ) @@ -98,7 +98,7 @@ public enum Grpc_Testing_TestService { public enum StreamingOutputCall { public typealias Input = Grpc_Testing_StreamingOutputCallRequest public typealias Output = Grpc_Testing_StreamingOutputCallResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "StreamingOutputCall" ) @@ -106,7 +106,7 @@ public enum Grpc_Testing_TestService { public enum StreamingInputCall { public typealias Input = Grpc_Testing_StreamingInputCallRequest public typealias Output = Grpc_Testing_StreamingInputCallResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "StreamingInputCall" ) @@ -114,7 +114,7 @@ public enum Grpc_Testing_TestService { public enum FullDuplexCall { public typealias Input = Grpc_Testing_StreamingOutputCallRequest public typealias Output = Grpc_Testing_StreamingOutputCallResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "FullDuplexCall" ) @@ -122,7 +122,7 @@ public enum Grpc_Testing_TestService { public enum HalfDuplexCall { public typealias Input = Grpc_Testing_StreamingOutputCallRequest public typealias Output = Grpc_Testing_StreamingOutputCallResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "HalfDuplexCall" ) @@ -130,12 +130,12 @@ public enum Grpc_Testing_TestService { public enum UnimplementedCall { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_Empty - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, method: "UnimplementedCall" ) } - public static let descriptors: [MethodDescriptor] = [ + public static let descriptors: [GRPCCore.MethodDescriptor] = [ EmptyCall.descriptor, UnaryCall.descriptor, CacheableUnaryCall.descriptor, @@ -156,7 +156,7 @@ public enum Grpc_Testing_TestService { public typealias Client = Grpc_Testing_TestServiceClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { public static let grpc_testing_TestService = Self( package: "grpc.testing", service: "TestService" @@ -164,17 +164,17 @@ extension ServiceDescriptor { } public enum Grpc_Testing_UnimplementedService { - public static let descriptor = ServiceDescriptor.grpc_testing_UnimplementedService + public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_UnimplementedService public enum Method { public enum UnimplementedCall { public typealias Input = Grpc_Testing_Empty public typealias Output = Grpc_Testing_Empty - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_UnimplementedService.descriptor.fullyQualifiedService, method: "UnimplementedCall" ) } - public static let descriptors: [MethodDescriptor] = [ + public static let descriptors: [GRPCCore.MethodDescriptor] = [ UnimplementedCall.descriptor ] } @@ -188,7 +188,7 @@ public enum Grpc_Testing_UnimplementedService { public typealias Client = Grpc_Testing_UnimplementedServiceClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { public static let grpc_testing_UnimplementedService = Self( package: "grpc.testing", service: "UnimplementedService" @@ -200,38 +200,38 @@ extension ServiceDescriptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One empty request followed by one empty response. - func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func emptyCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// One request followed by one response. - func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. - func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func cacheableUnaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. - func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingOutputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. - func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingInputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. - func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func fullDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. - func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func halfDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. - func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -241,64 +241,64 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_TestService.Method.EmptyCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.emptyCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.UnaryCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.unaryCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.cacheableUnaryCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.streamingOutputCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.streamingInputCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.fullDuplexCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.halfDuplexCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.unimplementedCall(request: request) } @@ -311,71 +311,71 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestService.StreamingServiceProtocol { /// One empty request followed by one empty response. - func emptyCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func emptyCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single /// One request followed by one response. - func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unaryCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. - func cacheableUnaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func cacheableUnaryCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. - func streamingOutputCall(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func streamingOutputCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. - func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func streamingInputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. - func fullDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func fullDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. - func halfDuplexCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func halfDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. - func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unimplementedCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_TestServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_TestService.ServiceProtocol { - public func emptyCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.emptyCall(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func emptyCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.emptyCall(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - public func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unaryCall(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - public func cacheableUnaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.cacheableUnaryCall(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func cacheableUnaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.cacheableUnaryCall(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - public func streamingOutputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.streamingOutputCall(request: ServerRequest.Single(stream: request)) + public func streamingOutputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.streamingOutputCall(request: GRPCCore.ServerRequest.Single(stream: request)) return response } - public func streamingInputCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + public func streamingInputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { let response = try await self.streamingInputCall(request: request) - return ServerResponse.Stream(single: response) + return GRPCCore.ServerResponse.Stream(single: response) } - public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unimplementedCall(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } @@ -384,7 +384,7 @@ extension Grpc_Testing_TestService.ServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_UnimplementedServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// A call that no server should implement - func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -394,8 +394,8 @@ extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.unimplementedCall(request: request) } @@ -408,24 +408,24 @@ extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_UnimplementedServiceServiceProtocol: Grpc_Testing_UnimplementedService.StreamingServiceProtocol { /// A call that no server should implement - func unimplementedCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unimplementedCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_UnimplementedServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_UnimplementedService.ServiceProtocol { - public func unimplementedCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.unimplementedCall(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unimplementedCall(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } /// A service used to control reconnect server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func start(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream - func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func stop(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -435,16 +435,16 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_ReconnectService.Method.Start.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.start(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_ReconnectService.Method.Stop.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.stop(request: request) } @@ -455,22 +455,22 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { /// A service used to control reconnect server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_ReconnectService.StreamingServiceProtocol { - func start(request: ServerRequest.Single) async throws -> ServerResponse.Single + func start(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single - func stop(request: ServerRequest.Single) async throws -> ServerResponse.Single + func stop(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_ReconnectServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_ReconnectService.ServiceProtocol { - public func start(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.start(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func start(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.start(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - public func stop(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.stop(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func stop(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.stop(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } @@ -480,62 +480,62 @@ extension Grpc_Testing_ReconnectService.ServiceProtocol { public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { /// One empty request followed by one empty response. func emptyCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// One request followed by one response. func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. func cacheableUnaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. func streamingOutputCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. func streamingInputCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. func fullDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// A sequence of requests followed by a sequence of responses. @@ -543,143 +543,143 @@ public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { /// stream of responses are returned to the client when the server starts with /// first request. func halfDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_TestService.ClientProtocol { public func emptyCall( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.emptyCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func unaryCall( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func cacheableUnaryCall( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.cacheableUnaryCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func streamingOutputCall( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingOutputCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func streamingInputCall( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.streamingInputCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func fullDuplexCall( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.fullDuplexCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func halfDuplexCall( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.halfDuplexCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func unimplementedCall( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -865,11 +865,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// One empty request followed by one empty response. public func emptyCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -885,11 +885,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// One request followed by one response. public func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -907,11 +907,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. public func cacheableUnaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -928,11 +928,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. public func streamingOutputCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -947,11 +947,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. public func streamingInputCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -969,11 +969,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. public func fullDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -990,11 +990,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// stream of responses are returned to the client when the server starts with /// first request. public func halfDuplexCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -1009,11 +1009,11 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. public func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -1034,27 +1034,27 @@ public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientPro public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { /// A call that no server should implement func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_UnimplementedService.ClientProtocol { public func unimplementedCall( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.unimplementedCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -1096,11 +1096,11 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente /// A call that no server should implement public func unimplementedCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -1119,51 +1119,51 @@ public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_Unimplemente @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { func start( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable func stop( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_ReconnectService.ClientProtocol { public func start( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.start( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } public func stop( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.stop( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -1221,11 +1221,11 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService } public func start( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -1240,11 +1240,11 @@ public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService } public func stop( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 64f632007..16f73d5b3 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -28,12 +28,12 @@ package import GRPCCore internal import GRPCProtobuf package enum Grpc_Health_V1_Health { - package static let descriptor = ServiceDescriptor.grpc_health_v1_Health + package static let descriptor = GRPCCore.ServiceDescriptor.grpc_health_v1_Health package enum Method { package enum Check { package typealias Input = Grpc_Health_V1_HealthCheckRequest package typealias Output = Grpc_Health_V1_HealthCheckResponse - package static let descriptor = MethodDescriptor( + package static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Health_V1_Health.descriptor.fullyQualifiedService, method: "Check" ) @@ -41,12 +41,12 @@ package enum Grpc_Health_V1_Health { package enum Watch { package typealias Input = Grpc_Health_V1_HealthCheckRequest package typealias Output = Grpc_Health_V1_HealthCheckResponse - package static let descriptor = MethodDescriptor( + package static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Health_V1_Health.descriptor.fullyQualifiedService, method: "Watch" ) } - package static let descriptors: [MethodDescriptor] = [ + package static let descriptors: [GRPCCore.MethodDescriptor] = [ Check.descriptor, Watch.descriptor ] @@ -61,7 +61,7 @@ package enum Grpc_Health_V1_Health { package typealias Client = Grpc_Health_V1_HealthClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { package static let grpc_health_v1_Health = Self( package: "grpc.health.v1", service: "Health" @@ -82,7 +82,7 @@ package protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.Registr /// server unhealthy if they do not receive a timely response. /// /// Check implementations should be idempotent and side effect free. - func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func check(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current @@ -99,7 +99,7 @@ package protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.Registr /// should assume this method is not supported and should not retry the /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. - func watch(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func watch(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -109,16 +109,16 @@ extension Grpc_Health_V1_Health.StreamingServiceProtocol { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Health_V1_Health.Method.Check.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.check(request: request) } ) router.registerHandler( forMethod: Grpc_Health_V1_Health.Method.Watch.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.watch(request: request) } @@ -140,7 +140,7 @@ package protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.Str /// server unhealthy if they do not receive a timely response. /// /// Check implementations should be idempotent and side effect free. - func check(request: ServerRequest.Single) async throws -> ServerResponse.Single + func check(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current @@ -157,19 +157,19 @@ package protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.Str /// should assume this method is not supported and should not retry the /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. - func watch(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func watch(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `Grpc_Health_V1_HealthStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Health_V1_Health.ServiceProtocol { - package func check(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.check(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + package func check(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.check(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - package func watch(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.watch(request: ServerRequest.Single(stream: request)) + package func watch(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.watch(request: GRPCCore.ServerRequest.Single(stream: request)) return response } } @@ -189,11 +189,11 @@ package protocol Grpc_Health_V1_HealthClientProtocol: Sendable { /// /// Check implementations should be idempotent and side effect free. func check( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Performs a watch for the serving status of the requested service. @@ -212,41 +212,41 @@ package protocol Grpc_Health_V1_HealthClientProtocol: Sendable { /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. func watch( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Health_V1_Health.ClientProtocol { package func check( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.check( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } package func watch( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.watch( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -337,11 +337,11 @@ package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol /// /// Check implementations should be idempotent and side effect free. package func check( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -371,11 +371,11 @@ package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. package func watch( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 20d7b9e16..47cbbeccb 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -28,12 +28,12 @@ internal import GRPCCore internal import GRPCProtobuf internal enum Grpc_Testing_BenchmarkService { - internal static let descriptor = ServiceDescriptor.grpc_testing_BenchmarkService + internal static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_BenchmarkService internal enum Method { internal enum UnaryCall { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "UnaryCall" ) @@ -41,7 +41,7 @@ internal enum Grpc_Testing_BenchmarkService { internal enum StreamingCall { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingCall" ) @@ -49,7 +49,7 @@ internal enum Grpc_Testing_BenchmarkService { internal enum StreamingFromClient { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingFromClient" ) @@ -57,7 +57,7 @@ internal enum Grpc_Testing_BenchmarkService { internal enum StreamingFromServer { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingFromServer" ) @@ -65,12 +65,12 @@ internal enum Grpc_Testing_BenchmarkService { internal enum StreamingBothWays { internal typealias Input = Grpc_Testing_SimpleRequest internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, method: "StreamingBothWays" ) } - internal static let descriptors: [MethodDescriptor] = [ + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ UnaryCall.descriptor, StreamingCall.descriptor, StreamingFromClient.descriptor, @@ -88,7 +88,7 @@ internal enum Grpc_Testing_BenchmarkService { internal typealias Client = Grpc_Testing_BenchmarkServiceClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { internal static let grpc_testing_BenchmarkService = Self( package: "grpc.testing", service: "BenchmarkService" @@ -99,24 +99,24 @@ extension ServiceDescriptor { internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One request followed by one response. /// The server returns the client payload as-is. - func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response - func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingFromClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is - func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingFromServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other - func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingBothWays(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -126,40 +126,40 @@ extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.unaryCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.streamingCall(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.streamingFromClient(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.streamingFromServer(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.streamingBothWays(request: request) } @@ -171,41 +171,41 @@ extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_BenchmarkService.StreamingServiceProtocol { /// One request followed by one response. /// The server returns the client payload as-is. - func unaryCall(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unaryCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response - func streamingCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func streamingFromClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is - func streamingFromServer(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func streamingFromServer(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other - func streamingBothWays(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func streamingBothWays(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `Grpc_Testing_BenchmarkServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_BenchmarkService.ServiceProtocol { - internal func unaryCall(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.unaryCall(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + internal func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unaryCall(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - internal func streamingFromClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func streamingFromClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { let response = try await self.streamingFromClient(request: request) - return ServerResponse.Stream(single: response) + return GRPCCore.ServerResponse.Stream(single: response) } - internal func streamingFromServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.streamingFromServer(request: ServerRequest.Single(stream: request)) + internal func streamingFromServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.streamingFromServer(request: GRPCCore.ServerRequest.Single(stream: request)) return response } } @@ -215,126 +215,126 @@ internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { /// One request followed by one response. /// The server returns the client payload as-is. func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response func streamingCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone func streamingFromClient( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is func streamingFromServer( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other func streamingBothWays( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_BenchmarkService.ClientProtocol { internal func unaryCall( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.unaryCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func streamingCall( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingCall( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func streamingFromClient( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.streamingFromClient( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func streamingFromServer( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingFromServer( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func streamingBothWays( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.streamingBothWays( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -455,11 +455,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// One request followed by one response. /// The server returns the client payload as-is. internal func unaryCall( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -477,11 +477,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response internal func streamingCall( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -496,11 +496,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone internal func streamingFromClient( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -517,11 +517,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is internal func streamingFromServer( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -536,11 +536,11 @@ internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkServi /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other internal func streamingBothWays( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index 9eafeccfd..467e2cdac 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -28,12 +28,12 @@ internal import GRPCCore internal import GRPCProtobuf internal enum Grpc_Testing_WorkerService { - internal static let descriptor = ServiceDescriptor.grpc_testing_WorkerService + internal static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_WorkerService internal enum Method { internal enum RunServer { internal typealias Input = Grpc_Testing_ServerArgs internal typealias Output = Grpc_Testing_ServerStatus - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "RunServer" ) @@ -41,7 +41,7 @@ internal enum Grpc_Testing_WorkerService { internal enum RunClient { internal typealias Input = Grpc_Testing_ClientArgs internal typealias Output = Grpc_Testing_ClientStatus - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "RunClient" ) @@ -49,7 +49,7 @@ internal enum Grpc_Testing_WorkerService { internal enum CoreCount { internal typealias Input = Grpc_Testing_CoreRequest internal typealias Output = Grpc_Testing_CoreResponse - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "CoreCount" ) @@ -57,12 +57,12 @@ internal enum Grpc_Testing_WorkerService { internal enum QuitWorker { internal typealias Input = Grpc_Testing_Void internal typealias Output = Grpc_Testing_Void - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, method: "QuitWorker" ) } - internal static let descriptors: [MethodDescriptor] = [ + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ RunServer.descriptor, RunClient.descriptor, CoreCount.descriptor, @@ -75,7 +75,7 @@ internal enum Grpc_Testing_WorkerService { internal typealias ServiceProtocol = Grpc_Testing_WorkerServiceServiceProtocol } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { internal static let grpc_testing_WorkerService = Self( package: "grpc.testing", service: "WorkerService" @@ -90,7 +90,7 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// stats. Closing the stream will initiate shutdown of the test server /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus @@ -98,13 +98,13 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// stats. Closing the stream will initiate shutdown of the test client /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Just return the core count - unary call - func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func coreCount(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Quit this worker - func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func quitWorker(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -114,32 +114,32 @@ extension Grpc_Testing_WorkerService.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.RunServer.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.runServer(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.RunClient.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.runClient(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.CoreCount.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.coreCount(request: request) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.QuitWorker.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.quitWorker(request: request) } @@ -155,7 +155,7 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// stats. Closing the stream will initiate shutdown of the test server /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runServer(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus @@ -163,25 +163,25 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// stats. Closing the stream will initiate shutdown of the test client /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runClient(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func runClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Just return the core count - unary call - func coreCount(request: ServerRequest.Single) async throws -> ServerResponse.Single + func coreCount(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single /// Quit this worker - func quitWorker(request: ServerRequest.Single) async throws -> ServerResponse.Single + func quitWorker(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_WorkerServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_WorkerService.ServiceProtocol { - internal func coreCount(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.coreCount(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + internal func coreCount(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.coreCount(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - internal func quitWorker(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.quitWorker(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + internal func quitWorker(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.quitWorker(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } \ No newline at end of file diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 62ecf95c9..5f26bbe3a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -47,26 +47,26 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -105,11 +105,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -154,26 +154,26 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -212,11 +212,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -261,24 +261,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -315,11 +315,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -362,24 +362,24 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -416,11 +416,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -471,49 +471,49 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable /// Documentation for MethodB func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { package func methodA( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } package func methodB( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.methodB( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -570,11 +570,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA package func methodA( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -590,11 +590,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodB package func methodB( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -637,26 +637,26 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol ServiceAClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ClientProtocol { internal func methodA( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.methodA( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -695,11 +695,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA internal func methodA( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 8b174d439..539189941 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -217,9 +217,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { @_spi(Secret) internal import enum Foo.Bar public enum NamespaceA_ServiceA { - public static let descriptor = ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol @@ -227,7 +227,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index ebff94eda..ea8ff3186 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -54,7 +54,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod - func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -63,8 +63,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.unary(request: request) } @@ -75,14 +75,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod - func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unary(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - public func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.unary(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unary(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } """ @@ -123,7 +123,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -132,8 +132,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } @@ -144,14 +144,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - package func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + package func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { let response = try await self.inputStreaming(request: request) - return ServerResponse.Stream(single: response) + return GRPCCore.ServerResponse.Stream(single: response) } } """ @@ -196,7 +196,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -205,8 +205,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -217,13 +217,13 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - public func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) + public func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.outputStreaming(request: GRPCCore.ServerRequest.Single(stream: request)) return response } } @@ -269,7 +269,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -278,8 +278,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.BidirectionalStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.bidirectionalStreaming(request: request) } @@ -290,7 +290,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidirectionalStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -350,10 +350,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -362,16 +362,16 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.inputStreaming(request: request) } ) router.registerHandler( forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.outputStreaming(request: request) } @@ -382,21 +382,21 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single /// Documentation for outputStreamingMethod - func outputStreaming(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func outputStreaming(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - internal func inputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { let response = try await self.inputStreaming(request: request) - return ServerResponse.Stream(single: response) + return GRPCCore.ServerResponse.Stream(single: response) } - internal func outputStreaming(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.outputStreaming(request: ServerRequest.Single(stream: request)) + internal func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.outputStreaming(request: GRPCCore.ServerRequest.Single(stream: request)) return response } } @@ -434,7 +434,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA - func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func methodA(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -443,8 +443,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: ServiceA.Method.MethodA.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.methodA(request: request) } @@ -455,14 +455,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA - func methodA(request: ServerRequest.Single) async throws -> ServerResponse.Single + func methodA(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ServiceProtocol { - internal func methodA(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.methodA(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + internal func methodA(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.methodA(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index fd5808816..52ab821a1 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -81,10 +81,10 @@ internal func makeCodeGenerationRequest( dependencies: dependencies, services: services, lookupSerializer: { - "ProtobufSerializer<\($0)>()" + "GRPCProtobuf.ProtobufSerializer<\($0)>()" }, lookupDeserializer: { - "ProtobufDeserializer<\($0)>()" + "GRPCProtobuf.ProtobufDeserializer<\($0)>()" } ) } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 5040e2454..ce9d0ab80 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -47,17 +47,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { - public static let descriptor = ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { public enum MethodA { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, method: "MethodA" ) } - public static let descriptors: [MethodDescriptor] = [ + public static let descriptors: [GRPCCore.MethodDescriptor] = [ MethodA.descriptor ] } @@ -70,7 +70,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" @@ -101,9 +101,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { - public static let descriptor = ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol @@ -114,7 +114,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" @@ -145,16 +145,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { - public static let descriptor = ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" @@ -185,16 +185,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { - public static let descriptor = ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" @@ -225,12 +225,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { - public static let descriptor = ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" @@ -265,17 +265,17 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum ServiceA { - public static let descriptor = ServiceDescriptor.ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.ServiceA public enum Method { public enum MethodA { public typealias Input = ServiceARequest public typealias Output = ServiceAResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: ServiceA.descriptor.fullyQualifiedService, method: "MethodA" ) } - public static let descriptors: [MethodDescriptor] = [ + public static let descriptors: [GRPCCore.MethodDescriptor] = [ MethodA.descriptor ] } @@ -288,7 +288,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = ServiceAClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let ServiceA = Self( package: "", service: "ServiceA" @@ -335,12 +335,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_ServiceA { - public static let descriptor = ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { public enum MethodA { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, method: "MethodA" ) @@ -348,12 +348,12 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum MethodB { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, method: "MethodB" ) } - public static let descriptors: [MethodDescriptor] = [ + public static let descriptors: [GRPCCore.MethodDescriptor] = [ MethodA.descriptor, MethodB.descriptor ] @@ -367,7 +367,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceAClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" @@ -398,9 +398,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ package enum NamespaceA_ServiceA { - package static let descriptor = ServiceDescriptor.namespaceA_ServiceA + package static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA package enum Method { - package static let descriptors: [MethodDescriptor] = [] + package static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol @@ -411,7 +411,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = NamespaceA_ServiceAClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { package static let namespaceA_ServiceA = Self( package: "namespaceA", service: "ServiceA" @@ -454,9 +454,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum NamespaceA_Aservice { - public static let descriptor = ServiceDescriptor.namespaceA_AService + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_AService public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_AserviceStreamingServiceProtocol @@ -467,16 +467,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_AserviceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_AService = Self( package: "namespaceA", service: "AService" ) } public enum NamespaceA_Bservice { - public static let descriptor = ServiceDescriptor.namespaceA_BService + public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_BService public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_BserviceStreamingServiceProtocol @@ -487,7 +487,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_BserviceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let namespaceA_BService = Self( package: "namespaceA", service: "BService" @@ -522,9 +522,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ package enum AService { - package static let descriptor = ServiceDescriptor.AService + package static let descriptor = GRPCCore.ServiceDescriptor.AService package enum Method { - package static let descriptors: [MethodDescriptor] = [] + package static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = AServiceStreamingServiceProtocol @@ -535,16 +535,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = AServiceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { package static let AService = Self( package: "", service: "AService" ) } package enum BService { - package static let descriptor = ServiceDescriptor.BService + package static let descriptor = GRPCCore.ServiceDescriptor.BService package enum Method { - package static let descriptors: [MethodDescriptor] = [] + package static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol @@ -555,7 +555,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = BServiceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { package static let BService = Self( package: "", service: "BService" @@ -598,9 +598,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ internal enum Anamespace_AService { - internal static let descriptor = ServiceDescriptor.anamespace_AService + internal static let descriptor = GRPCCore.ServiceDescriptor.anamespace_AService internal enum Method { - internal static let descriptors: [MethodDescriptor] = [] + internal static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol @@ -611,16 +611,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Anamespace_AServiceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { internal static let anamespace_AService = Self( package: "anamespace", service: "AService" ) } internal enum Bnamespace_BService { - internal static let descriptor = ServiceDescriptor.bnamespace_BService + internal static let descriptor = GRPCCore.ServiceDescriptor.bnamespace_BService internal enum Method { - internal static let descriptors: [MethodDescriptor] = [] + internal static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias StreamingServiceProtocol = Bnamespace_BServiceStreamingServiceProtocol @@ -631,7 +631,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal typealias Client = Bnamespace_BServiceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { internal static let bnamespace_BService = Self( package: "bnamespace", service: "BService" @@ -668,9 +668,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ public enum Anamespace_AService { - public static let descriptor = ServiceDescriptor.anamespace_AService + public static let descriptor = GRPCCore.ServiceDescriptor.anamespace_AService public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol @@ -681,16 +681,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = Anamespace_AServiceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let anamespace_AService = Self( package: "anamespace", service: "AService" ) } public enum BService { - public static let descriptor = ServiceDescriptor.BService + public static let descriptor = GRPCCore.ServiceDescriptor.BService public enum Method { - public static let descriptors: [MethodDescriptor] = [] + public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol @@ -701,7 +701,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = BServiceClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let BService = Self( package: "", service: "BService" diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index b12c0ccb0..6ec4497cf 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -26,12 +26,12 @@ internal import GRPCCore internal import GRPCProtobuf internal enum Control { - internal static let descriptor = ServiceDescriptor.Control + internal static let descriptor = GRPCCore.ServiceDescriptor.Control internal enum Method { internal enum Unary { internal typealias Input = ControlInput internal typealias Output = ControlOutput - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Control.descriptor.fullyQualifiedService, method: "Unary" ) @@ -39,7 +39,7 @@ internal enum Control { internal enum ServerStream { internal typealias Input = ControlInput internal typealias Output = ControlOutput - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Control.descriptor.fullyQualifiedService, method: "ServerStream" ) @@ -47,7 +47,7 @@ internal enum Control { internal enum ClientStream { internal typealias Input = ControlInput internal typealias Output = ControlOutput - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Control.descriptor.fullyQualifiedService, method: "ClientStream" ) @@ -55,12 +55,12 @@ internal enum Control { internal enum BidiStream { internal typealias Input = ControlInput internal typealias Output = ControlOutput - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Control.descriptor.fullyQualifiedService, method: "BidiStream" ) } - internal static let descriptors: [MethodDescriptor] = [ + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ Unary.descriptor, ServerStream.descriptor, ClientStream.descriptor, @@ -77,7 +77,7 @@ internal enum Control { internal typealias Client = ControlClient } -extension ServiceDescriptor { +extension GRPCCore.ServiceDescriptor { internal static let Control = Self( package: "", service: "Control" @@ -90,13 +90,13 @@ extension ServiceDescriptor { /// the output. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream - func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func serverStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream - func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func clientStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream - func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidiStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -106,32 +106,32 @@ extension Control.StreamingServiceProtocol { internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Control.Method.Unary.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.unary(request: request) } ) router.registerHandler( forMethod: Control.Method.ServerStream.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.serverStream(request: request) } ) router.registerHandler( forMethod: Control.Method.ClientStream.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.clientStream(request: request) } ) router.registerHandler( forMethod: Control.Method.BidiStream.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.bidiStream(request: request) } @@ -145,31 +145,31 @@ extension Control.StreamingServiceProtocol { /// the output. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { - func unary(request: ServerRequest.Single) async throws -> ServerResponse.Single + func unary(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single - func serverStream(request: ServerRequest.Single) async throws -> ServerResponse.Stream + func serverStream(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream - func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Single + func clientStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single - func bidiStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func bidiStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `ControlStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Control.ServiceProtocol { - internal func unary(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.unary(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + internal func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unary(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } - internal func serverStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.serverStream(request: ServerRequest.Single(stream: request)) + internal func serverStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.serverStream(request: GRPCCore.ServerRequest.Single(stream: request)) return response } - internal func clientStream(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + internal func clientStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { let response = try await self.clientStream(request: request) - return ServerResponse.Stream(single: response) + return GRPCCore.ServerResponse.Stream(single: response) } } @@ -180,95 +180,95 @@ extension Control.ServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlClientProtocol: Sendable { func unary( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable func serverStream( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable func clientStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable func bidiStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Control.ClientProtocol { internal func unary( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.unary( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func serverStream( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.serverStream( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func clientStream( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.clientStream( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) } internal func bidiStream( - request: ClientRequest.Stream, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.bidiStream( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -363,11 +363,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func unary( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -382,11 +382,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func serverStream( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -399,11 +399,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func clientStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -418,11 +418,11 @@ internal struct ControlClient: Control.ClientProtocol { } internal func bidiStream( - request: ClientRequest.Stream, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 8cfc72fe7..6e814bb8a 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -96,11 +96,11 @@ final class ProtobufCodeGenParserTests: XCTestCase { XCTAssertEqual( parsedCodeGenRequest.lookupSerializer("Helloworld_HelloRequest"), - "ProtobufSerializer()" + "GRPCProtobuf.ProtobufSerializer()" ) XCTAssertEqual( parsedCodeGenRequest.lookupDeserializer("Helloworld_HelloRequest"), - "ProtobufDeserializer()" + "GRPCProtobuf.ProtobufDeserializer()" ) } @@ -178,11 +178,11 @@ final class ProtobufCodeGenParserTests: XCTestCase { XCTAssertEqual( parsedCodeGenRequest.lookupSerializer("Hello_World_HelloRequest"), - "ProtobufSerializer()" + "GRPCProtobuf.ProtobufSerializer()" ) XCTAssertEqual( parsedCodeGenRequest.lookupDeserializer("Hello_World_HelloRequest"), - "ProtobufDeserializer()" + "GRPCProtobuf.ProtobufDeserializer()" ) } @@ -260,11 +260,11 @@ final class ProtobufCodeGenParserTests: XCTestCase { XCTAssertEqual( parsedCodeGenRequest.lookupSerializer("HelloRequest"), - "ProtobufSerializer()" + "GRPCProtobuf.ProtobufSerializer()" ) XCTAssertEqual( parsedCodeGenRequest.lookupDeserializer("HelloRequest"), - "ProtobufDeserializer()" + "GRPCProtobuf.ProtobufDeserializer()" ) } } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 03cd693e3..8f541e1c6 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -60,17 +60,17 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal import ExtraModule internal enum Hello_World_Greeter { - internal static let descriptor = ServiceDescriptor.hello_world_Greeter + internal static let descriptor = GRPCCore.ServiceDescriptor.hello_world_Greeter internal enum Method { internal enum SayHello { internal typealias Input = Hello_World_HelloRequest internal typealias Output = Hello_World_HelloReply - internal static let descriptor = MethodDescriptor( + internal static let descriptor = GRPCCore.MethodDescriptor( service: Hello_World_Greeter.descriptor.fullyQualifiedService, method: "SayHello" ) } - internal static let descriptors: [MethodDescriptor] = [ + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ SayHello.descriptor ] } @@ -80,7 +80,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal typealias Client = Hello_World_GreeterClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { internal static let hello_world_Greeter = Self( package: "hello.world", service: "Greeter" @@ -92,27 +92,27 @@ final class ProtobufCodeGeneratorTests: XCTestCase { internal protocol Hello_World_GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Hello_World_Greeter.ClientProtocol { internal func sayHello( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -153,11 +153,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. internal func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -210,17 +210,17 @@ final class ProtobufCodeGeneratorTests: XCTestCase { public import ExtraModule public enum Helloworld_Greeter { - public static let descriptor = ServiceDescriptor.helloworld_Greeter + public static let descriptor = GRPCCore.ServiceDescriptor.helloworld_Greeter public enum Method { public enum SayHello { public typealias Input = Helloworld_HelloRequest public typealias Output = Helloworld_HelloReply - public static let descriptor = MethodDescriptor( + public static let descriptor = GRPCCore.MethodDescriptor( service: Helloworld_Greeter.descriptor.fullyQualifiedService, method: "SayHello" ) } - public static let descriptors: [MethodDescriptor] = [ + public static let descriptors: [GRPCCore.MethodDescriptor] = [ SayHello.descriptor ] } @@ -230,7 +230,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { public static let helloworld_Greeter = Self( package: "helloworld", service: "Greeter" @@ -241,7 +241,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -251,8 +251,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Helloworld_Greeter.Method.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -264,15 +264,15 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.ServiceProtocol { - public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + public func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.sayHello(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } """ @@ -313,17 +313,17 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package import ExtraModule package enum Greeter { - package static let descriptor = ServiceDescriptor.Greeter + package static let descriptor = GRPCCore.ServiceDescriptor.Greeter package enum Method { package enum SayHello { package typealias Input = HelloRequest package typealias Output = HelloReply - package static let descriptor = MethodDescriptor( + package static let descriptor = GRPCCore.MethodDescriptor( service: Greeter.descriptor.fullyQualifiedService, method: "SayHello" ) } - package static let descriptors: [MethodDescriptor] = [ + package static let descriptors: [GRPCCore.MethodDescriptor] = [ SayHello.descriptor ] } @@ -337,7 +337,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package typealias Client = GreeterClient } - extension ServiceDescriptor { + extension GRPCCore.ServiceDescriptor { package static let Greeter = Self( package: "", service: "Greeter" @@ -348,7 +348,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream + func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -358,8 +358,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Greeter.Method.SayHello.descriptor, - deserializer: ProtobufDeserializer(), - serializer: ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), handler: { request in try await self.sayHello(request: request) } @@ -371,15 +371,15 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterServiceProtocol: Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single + func sayHello(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `GreeterStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Greeter.ServiceProtocol { - package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { - let response = try await self.sayHello(request: ServerRequest.Single(stream: request)) - return ServerResponse.Stream(single: response) + package func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.sayHello(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) } } @@ -388,27 +388,27 @@ final class ProtobufCodeGeneratorTests: XCTestCase { package protocol GreeterClientProtocol: Sendable { /// Sends a greeting. func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Greeter.ClientProtocol { package func sayHello( - request: ClientRequest.Single, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { try await self.sayHello( request: request, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, body ) @@ -449,11 +449,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { /// Sends a greeting. package func sayHello( - request: ClientRequest.Single, - serializer: some MessageSerializer, - deserializer: some MessageDeserializer, - options: CallOptions = .defaults, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R = { + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { From 209ffc560b53fb787aad828dc18030eb7cc9918f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 13 Aug 2024 10:52:57 +0100 Subject: [PATCH 428/580] Fixup various doc issues (#2015) --- Sources/GRPCCore/Call/Server/RPCRouter.swift | 4 ++-- Sources/GRPCCore/Call/Server/ServerRequest.swift | 2 +- Sources/GRPCCore/Configuration/ServiceConfig.swift | 7 ++++--- Sources/GRPCCore/GRPCClient.swift | 10 +++++----- Sources/GRPCCore/Transport/ClientTransport.swift | 5 +++-- Sources/GRPCCore/Transport/ServerTransport.swift | 2 +- .../GRPCHTTP2Core/Client/HTTP2ClientTransport.swift | 4 ++-- .../GRPCHTTP2Core/Client/Resolver/NameResolver.swift | 4 ++-- .../GRPCHTTP2Core/Server/HTTP2ServerTransport.swift | 2 +- .../HTTP2ServerTransport+Posix.swift | 2 +- .../HTTP2ServerTransport+TransportServices.swift | 2 +- .../InProcessClientTransport.swift | 8 +++----- .../InProcessServerTransport.swift | 5 +---- Sources/GRPCProtobuf/Coding.swift | 4 ++-- 14 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index 93c125c7b..e6670d523 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -24,8 +24,8 @@ /// given method by calling ``removeHandler(forMethod:)``. /// /// In most cases you won't need to interact with the router directly. Instead you should register -/// your services with ``GRPCServer/Services-swift.struct/register(_:)`` which will in turn register -/// each method with the router. +/// your services with ``GRPCServer/init(transport:services:interceptors:)`` which will in turn +/// register each method with the router. /// /// You may wish to not serve all methods from your service in which case you can either: /// diff --git a/Sources/GRPCCore/Call/Server/ServerRequest.swift b/Sources/GRPCCore/Call/Server/ServerRequest.swift index 962d5f049..90618bdfe 100644 --- a/Sources/GRPCCore/Call/Server/ServerRequest.swift +++ b/Sources/GRPCCore/Call/Server/ServerRequest.swift @@ -33,7 +33,7 @@ extension ServerRequest { /// /// - Parameters: /// - metadata: Metadata received from the client. - /// - messages: The message received from the client. + /// - message: The message received from the client. public init(metadata: Metadata, message: Message) { self.metadata = metadata self.message = message diff --git a/Sources/GRPCCore/Configuration/ServiceConfig.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift index 2f711b0ba..f0e41c4bc 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -34,11 +34,12 @@ public struct ServiceConfig: Hashable, Sendable { /// and hedged RPCs when the client's ratio of failures to successes exceeds a threshold. /// /// For each server name, the gRPC client will maintain a `token_count` which is initially set - /// to ``maxTokens``. Every outgoing RPC (regardless of service or method invoked) will change - /// `token_count` as follows: + /// to ``RetryThrottling-swift.struct/maxTokens``. Every outgoing RPC (regardless of service or + /// method invoked) will change `token_count` as follows: /// /// - Every failed RPC will decrement the `token_count` by 1. - /// - Every successful RPC will increment the `token_count` by ``tokenRatio``. + /// - Every successful RPC will increment the `token_count` by + /// ``RetryThrottling-swift.struct/tokenRatio``. /// /// If `token_count` is less than or equal to `max_tokens / 2`, then RPCs will not be retried /// and hedged RPCs will not be sent. diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 1ba012640..e65699932 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -21,10 +21,10 @@ internal import Atomics /// A ``GRPCClient`` communicates to a server via a ``ClientTransport``. /// /// You can start RPCs to the server by calling the corresponding method: -/// - ``unary(request:descriptor:serializer:deserializer:handler:)`` -/// - ``clientStreaming(request:descriptor:serializer:deserializer:handler:)`` -/// - ``serverStreaming(request:descriptor:serializer:deserializer:handler:)`` -/// - ``bidirectionalStreaming(request:descriptor:serializer:deserializer:handler:)`` +/// - ``unary(request:descriptor:serializer:deserializer:options:handler:)`` +/// - ``clientStreaming(request:descriptor:serializer:deserializer:options:handler:)`` +/// - ``serverStreaming(request:descriptor:serializer:deserializer:options:handler:)`` +/// - ``bidirectionalStreaming(request:descriptor:serializer:deserializer:options:handler:)`` /// /// However, in most cases you should prefer wrapping the ``GRPCClient`` with a generated stub. /// @@ -163,7 +163,7 @@ public struct GRPCClient: Sendable { /// If you need to abruptly stop all work you should cancel the task executing this method. /// /// The client, and by extension this function, can only be run once. If the client is already - /// running or has already been closed then a ``ClientError`` is thrown. + /// running or has already been closed then a ``RuntimeError`` is thrown. public func run() async throws { let (wasNotStarted, original) = self.state.compareExchange( expected: .notStarted, diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 9b91ca5f1..c678cac61 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -40,8 +40,9 @@ public protocol ClientTransport: Sendable { /// Signal to the transport that no new streams may be created. /// - /// Existing streams may run to completion naturally but calling ``withStream(descriptor:_:)`` - /// should result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. + /// Existing streams may run to completion naturally but calling + /// ``ClientTransport/withStream(descriptor:options:_:)`` should result in an ``RPCError`` with + /// code ``RPCError/Code/failedPrecondition`` being thrown. /// /// If you want to forcefully cancel all active streams then cancel the task /// running ``connect()``. diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index 6d4d4851f..c402c735a 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -29,7 +29,7 @@ public protocol ServerTransport: Sendable { /// You can call ``stopListening()`` to stop the transport from accepting new streams. Existing /// streams must be allowed to complete naturally. However, transports may also enforce a grace /// period after which any open streams may be cancelled. You can also cancel the task running - /// ``listen()`` to abruptly close connections and streams. + /// ``listen(_:)`` to abruptly close connections and streams. func listen( _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws diff --git a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift index e4cd97784..03bc39f1c 100644 --- a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift +++ b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift @@ -28,12 +28,12 @@ extension HTTP2ClientTransport.Config { public struct Compression: Sendable { /// The default algorithm used for compressing outbound messages. /// - /// This can be overridden on a per-call basis via ``CallOptions``. + /// This can be overridden on a per-call basis via `CallOptions`. public var algorithm: CompressionAlgorithm /// Compression algorithms enabled for inbound messages. /// - /// - Note: ``CompressionAlgorithm/none`` is always supported, even if it isn't set here. + /// - Note: `CompressionAlgorithm.none` is always supported, even if it isn't set here. public var enabledAlgorithms: CompressionAlgorithmSet /// Creates a new compression configuration. diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift index ad1436fe2..822ddb815 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift @@ -21,11 +21,11 @@ public import GRPCCore public struct NameResolver: Sendable { /// A sequence of name resolution results. /// - /// Resolvers may be push or pull based. Resolvers with the ``UpdateMode-swift.enum/push`` + /// Resolvers may be push or pull based. Resolvers with the ``UpdateMode-swift.struct/push`` /// update mode have addresses pushed to them by an external source and you should subscribe /// to changes in addresses by awaiting for new values in a loop. /// - /// Resolvers with the ``UpdateMode-swift.enum/pull`` update mode shouldn't be subscribed to, + /// Resolvers with the ``UpdateMode-swift.struct/pull`` update mode shouldn't be subscribed to, /// instead you should create an iterator and ask for new results as and when necessary. public var names: RPCAsyncSequence diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift index c0aef9094..07eb6b696 100644 --- a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift +++ b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift @@ -29,7 +29,7 @@ extension HTTP2ServerTransport.Config { public struct Compression: Sendable { /// Compression algorithms enabled for inbound messages. /// - /// - Note: ``CompressionAlgorithm/none`` is always supported, even if it isn't set here. + /// - Note: `CompressionAlgorithm.none` is always supported, even if it isn't set here. public var enabledAlgorithms: CompressionAlgorithmSet /// Creates a new compression configuration. diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 0d806dc73..18074fa0c 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -294,7 +294,7 @@ extension HTTP2ServerTransport { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ServerTransport.Posix { - /// Configuration for the ``GRPCHTTP2TransportNIOPosix/GRPCHTTP2Core/HTTP2ServerTransport/Posix``. + /// Config for the `Posix` transport. public struct Config: Sendable { /// Compression configuration. public var compression: HTTP2ServerTransport.Config.Compression diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 93d6f3fd8..e381068ab 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -286,7 +286,7 @@ extension HTTP2ServerTransport { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ServerTransport.TransportServices { - /// Configuration for the ``GRPCHTTP2TransportNIOTransportServices/GRPCHTTP2Core/HTTP2ServerTransport/TransportServices``. + /// Configuration for the `TransportServices` transport. public struct Config: Sendable { /// Compression configuration. public var compression: HTTP2ServerTransport.Config.Compression diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index c8f9412ec..04ff14a98 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -22,15 +22,13 @@ public import GRPCCore /// involved, as the client and server will communicate directly with each other via in-process streams. /// /// To use this client, you'll have to provide an ``InProcessServerTransport`` upon creation, as well -/// as a ``ClientRPCExecutionConfigurationCollection``, containing a set of -/// ``ClientRPCExecutionConfiguration``s which are specific, per-method configurations for your -/// transport. +/// as a ``ServiceConfig``. /// /// Once you have a client, you must keep a long-running task executing ``connect()``, which /// will return only once all streams have been finished and ``close()`` has been called on this client; or /// when the containing task is cancelled. /// -/// To execute requests using this client, use ``withStream(descriptor:_:)``. If this function is +/// To execute requests using this client, use ``withStream(descriptor:options:_:)``. If this function is /// called before ``connect()`` is called, then any streams will remain pending and the call will /// block until ``connect()`` is called or the task is cancelled. /// @@ -187,7 +185,7 @@ public struct InProcessClientTransport: ClientTransport { /// Signal to the transport that no new streams may be created. /// - /// Existing streams may run to completion naturally but calling ``withStream(descriptor:_:)`` + /// Existing streams may run to completion naturally but calling ``withStream(descriptor:options:_:)`` /// will result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. /// /// If you want to forcefully cancel all active streams then cancel the task running ``connect()``. diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 412d1bd58..519300f16 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -21,7 +21,7 @@ public import GRPCCore /// This is useful when you're interested in testing your application without any actual networking layers /// involved, as the client and server will communicate directly with each other via in-process streams. /// -/// To use this server, you call ``listen()`` and iterate over the returned `AsyncSequence` to get all +/// To use this server, you call ``listen(_:)`` and iterate over the returned `AsyncSequence` to get all /// RPC requests made from clients (as ``RPCStream``s). /// To stop listening to new requests, call ``stopListening()``. /// @@ -69,9 +69,6 @@ public struct InProcessServerTransport: ServerTransport, Sendable { /// Stop listening to any new ``RPCStream`` publications. /// - /// All further calls to ``acceptStream(_:)`` will not produce any new elements on the - /// ``RPCAsyncSequence`` returned by ``listen()``. - /// /// - SeeAlso: ``ServerTransport`` public func stopListening() { self.newStreamsContinuation.finish() diff --git a/Sources/GRPCProtobuf/Coding.swift b/Sources/GRPCProtobuf/Coding.swift index 4aa0718cf..df2e10f45 100644 --- a/Sources/GRPCProtobuf/Coding.swift +++ b/Sources/GRPCProtobuf/Coding.swift @@ -21,7 +21,7 @@ public import SwiftProtobuf public struct ProtobufSerializer: GRPCCore.MessageSerializer { public init() {} - /// Serializes a ``Message`` into a sequence of bytes. + /// Serializes a `Message` into a sequence of bytes. /// /// - Parameter message: The message to serialize. /// - Returns: An array of serialized bytes representing the message. @@ -42,7 +42,7 @@ public struct ProtobufSerializer: GRPCCore.Messa public struct ProtobufDeserializer: GRPCCore.MessageDeserializer { public init() {} - /// Deserializes a sequence of bytes into a ``Message``. + /// Deserializes a sequence of bytes into a `Message`. /// /// - Parameter serializedMessageBytes: The array of bytes to deserialize. /// - Returns: The deserialized message. From f3917796e94cb213b08c8434331af89e6aee3c84 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 14 Aug 2024 13:23:55 +0100 Subject: [PATCH 429/580] Add docc docs for grpc core (#2016) Motivation: As we move towards v2 we need to make a clearer separation between v1 and v2 docs to avoid confusion. Modifications: - Remove v1 specific content out of README.md (it is duplicated in the v1 DocC docs) and provide a clearer update on the state of the project. This also points users to the v1 docs on Swift Package Index where the duplicated v1 content lives. - Add a doc catalog to gRPC core with a very high level description of the package products - Add a "Benchmarks" tutorial to the v2 doc catalog (it was content removed from the README) Result: - DocC catalog in place for v2 - README is clearer --- .spi.yml | 2 +- README.md | 181 ++---------------- .../Development/Benchmarks.md | 35 ++++ .../Documentation.docc/Documentation.md | 28 +++ 4 files changed, 75 insertions(+), 171 deletions(-) create mode 100644 Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md create mode 100644 Sources/GRPCCore/Documentation.docc/Documentation.md diff --git a/.spi.yml b/.spi.yml index 260eb791f..477270f7e 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [GRPC, GRPCReflectionService, protoc-gen-grpc-swift] + - documentation_targets: [GRPC, GRPCReflectionService, protoc-gen-grpc-swift, _GRPCCore] diff --git a/README.md b/README.md index 0b892bcca..92d0bb405 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,16 @@ -[![CI](https://img.shields.io/github/workflow/status/grpc/grpc-swift/CI?event=push)](https://github.com/grpc/grpc-swift/actions/workflows/ci.yaml) -[![Latest Version](https://img.shields.io/github/v/release/grpc/grpc-swift?include_prereleases&sort=semver)](https://img.shields.io/github/v/release/grpc/grpc-swift?include_prereleases&sort=semver) -[![sswg:graduated|104x20](https://img.shields.io/badge/sswg-graduated-green.svg)](https://github.com/swift-server/sswg/blob/main/process/incubation.md#graduated-level) - # gRPC Swift -This repository contains a gRPC Swift API and code generator. - -It is intended for use with Apple's [SwiftProtobuf][swift-protobuf] support for -Protocol Buffers. Both projects contain code generation plugins for `protoc`, -Google's Protocol Buffer compiler, and both contain libraries of supporting code -that is needed to build and run the generated code. - -APIs and generated code is provided for both gRPC clients and servers, and can -be built either with Xcode or the Swift Package Manager. Support is provided for -all four gRPC API styles (Unary, Server Streaming, Client Streaming, and -Bidirectional Streaming) and connections can be made either over secure (TLS) or -insecure channels. +This repository contains a gRPC code generator and runtime libraries for Swift. +You can read more about gRPC on the [gRPC project's website][grpcio]. ## Versions -gRPC Swift has recently been rewritten on top of [SwiftNIO][swift-nio] as -opposed to the core library provided by the [gRPC project][grpc]. - -Version | Implementation | Branch | `protoc` Plugin | Support ---------|----------------|------------------------|-------------------------|----------------------------------------- -1.x | SwiftNIO | [`main`][branch-new] | `protoc-gen-grpc-swift` | Actively developed and supported -0.x | gRPC C library | [`cgrpc`][branch-old] | `protoc-gen-swiftgrpc` | No longer developed; security fixes only - -The remainder of this README refers to the 1.x version of gRPC Swift. - - -## Supported Platforms - -gRPC Swift's platform support is identical to the [platform support of Swift -NIO][swift-nio-platforms]. - -The earliest supported version of Swift for gRPC Swift releases are as follows: - -gRPC Swift Version | Earliest Swift Version --------------------|----------------------- -`1.0.0 ..< 1.8.0` | 5.2 -`1.8.0 ..< 1.11.0` | 5.4 -`1.11.0..< 1.16.0`.| 5.5 -`1.16.0..< 1.20.0` | 5.6 -`1.20.0..< 1.22.0` | 5.7 -`1.22.0...` | 5.8 - -Versions of clients and services which are use Swift's Concurrency support -are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. - -## Getting gRPC Swift - -There are two parts to gRPC Swift: the gRPC library and an API code generator. - -### Getting the gRPC library - -#### Swift Package Manager - -The Swift Package Manager is the preferred way to get gRPC Swift. Simply add the -package dependency to your `Package.swift`: - -```swift -dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.21.0") -] -``` - -...and depend on `"GRPC"` in the necessary targets: - -```swift -.target( - name: ..., - dependencies: [.product(name: "GRPC", package: "grpc-swift")] -] -``` - -### Getting the `protoc` Plugins - -Binary releases of `protoc`, the Protocol Buffer Compiler, are available on -[GitHub][protobuf-releases]. - -To build the plugins, run: -- `swift build -c release --product protoc-gen-swift` to build the `protoc` - plugin which generates Protocol Buffer support code, and -- `swift build -c release --product protoc-gen-grpc-swift` to build the `protoc` plugin - which generates gRPC interface code. - -To install these plugins, just copy the two executables (`protoc-gen-swift` and -`protoc-gen-grpc-swift`) from the build directory (`.build/release`) into a directory -that is part of your `PATH` environment variable. Alternatively the full path to -the plugins can be specified when using `protoc`. - -### Using the Swift Package Manager plugin - -You can also use the Swift Package Manager build plugin to generate messages and -gRPC code at build time rather than using `protoc` to generate them ahead of -time. Using this method Swift Package Manager takes care of building -`protoc-gen-swift` and `protoc-gen-grpc-swift` for you. - -One important distinction between using the Swift Package Manager build plugin -and generating the code ahead of time is that the build plugin has an implicit -dependency on `protoc`. It's therefore unsuitable for _libraries_ as they can't -guarantee that end users will have `protoc` available at compile time. - -You can find more documentation about the Swift Package Manager build plugin in -[Using the Swift Package Manager plugin][spm-plugin-grpc]. - -## Examples - -gRPC Swift has a number of tutorials and examples available. They are split -across two directories: - -- [`/Sources/Examples`][examples-in-source] contains examples which do not - require additional dependencies and may be built using the Swift Package - Manager. -- [`/Examples`][examples-out-of-source] contains examples which rely on - external dependencies or may not be built by the Swift Package Manager (such - as an iOS app). - -Some of the examples are accompanied by tutorials, including: -- A [quick start guide][docs-quickstart] for creating and running your first - gRPC service. -- A [basic tutorial][docs-tutorial] covering the creation and implementation of - a gRPC service using all four call types as well as the code required to setup - and run a server and make calls to it using a generated client. -- An [interceptors][docs-interceptors-tutorial] tutorial covering how to create - and use interceptors with gRPC Swift. - -## Documentation - -The `docs` directory contains documentation, including: - -- Options for the `protoc` plugin in [`docs/plugin.md`][docs-plugin] -- How to configure TLS in [`docs/tls.md`][docs-tls] -- How to configure keepalive in [`docs/keepalive.md`][docs-keepalive] -- Support for Apple Platforms and NIO Transport Services in - [`docs/apple-platforms.md`][docs-apple] - -## Benchmarks - -Benchmarks for `grpc-swift` are in a separate Swift Package in the `Performance/Benchmarks` subfolder of this repository. -They use the [`package-benchmark`](https://github.com/ordo-one/package-benchmark) plugin. -Benchmarks depends on the [`jemalloc`](https://jemalloc.net) memory allocation library, which is used by `package-benchmark` to capture memory allocation statistics. -An installation guide can be found in the [Getting Started article](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted#Installing-Prerequisites-and-Platform-Support) of `package-benchmark`. -Afterwards you can run the benchmarks from CLI by going to the `Performance/Benchmarks` subfolder (e.g. `cd Performance/Benchmarks`) and invoking: -``` -swift package benchmark -``` - -Profiling benchmarks or building the benchmarks in release mode in Xcode with `jemalloc` is currently not supported and requires disabling `jemalloc`. -Make sure Xcode is closed and then open it from the CLI with the `BENCHMARK_DISABLE_JEMALLOC=true` environment variable set e.g.: -``` -BENCHMARK_DISABLE_JEMALLOC=true xed . -``` +gRPC Swift is currently undergoing active development to take full advantage of +Swift's native concurrency features. The culmination of this work will be a new +major version, v2.x. Pre-release versions will be available in the near future. -For more information please refer to `swift package benchmark --help` or the [documentation of `package-benchmark`](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark). +In the meantime, v1.x is available and still supported. You can read more about +it on the [Swift Package Index][spi-grpc-swift-main]. ## Security @@ -162,28 +18,13 @@ Please see [SECURITY.md](SECURITY.md). ## License -gRPC Swift is released under the same license as [gRPC][grpc], repeated in +gRPC Swift is released under the same license as [gRPC][gh-grpc], repeated in [LICENSE](LICENSE). ## Contributing Please get involved! See our [guidelines for contributing](CONTRIBUTING.md). -[docs-apple]: ./docs/apple-platforms.md -[docs-plugin]: ./docs/plugin.md -[docs-quickstart]: ./docs/quick-start.md -[docs-tls]: ./docs/tls.md -[docs-keepalive]: ./docs/keepalive.md -[docs-tutorial]: ./docs/basic-tutorial.md -[docs-interceptors-tutorial]: ./docs/interceptors-tutorial.md -[grpc]: https://github.com/grpc/grpc -[protobuf-releases]: https://github.com/protocolbuffers/protobuf/releases -[swift-nio-platforms]: https://github.com/apple/swift-nio#supported-platforms -[swift-nio]: https://github.com/apple/swift-nio -[swift-protobuf]: https://github.com/apple/swift-protobuf -[xcode-spm]: https://help.apple.com/xcode/mac/current/#/devb83d64851 -[branch-new]: https://github.com/grpc/grpc-swift/tree/main -[branch-old]: https://github.com/grpc/grpc-swift/tree/cgrpc -[examples-out-of-source]: https://github.com/grpc/grpc-swift/tree/main/Examples -[examples-in-source]: https://github.com/grpc/grpc-swift/tree/main/Sources/Examples -[spm-plugin-grpc]: https://swiftpackageindex.com/grpc/grpc-swift/main/documentation/protoc-gen-grpc-swift/spm-plugin +[gh-grpc]: https://github.com/grpc/grpc +[grpcio]: https://grpc.io +[spi-grpc-swift-main]: https://swiftpackageindex.com/grpc/grpc-swift/main/documentation/grpc diff --git a/Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md b/Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md new file mode 100644 index 000000000..4033857f7 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md @@ -0,0 +1,35 @@ +# Benchmarks + +## Overview + +Benchmarks for this package are in a separate Swift Package in the `Performance/Benchmarks` +subdirectory of the repository. + +They use the [`package-benchmark`](https://github.com/ordo-one/package-benchmark) plugin. +Benchmarks depends on the [`jemalloc`](https://jemalloc.net) memory allocation library, which is +used by `package-benchmark` to capture memory allocation statistics. + +An installation guide can be found in the [Getting Started article](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark/gettingstarted) +for `package-benchmark`. + +### Running the benchmarks + +You can run the benchmarks CLI by going to the `Performance/Benchmarks` subdirectory +(e.g. `cd Performance/Benchmarks`) and invoking: + +``` +swift package benchmark +``` + +Profiling benchmarks or building the benchmarks in release mode in Xcode with `jemalloc` isn't +currently supported and requires disabling `jemalloc`. + +Make sure you have quit Xcode and then open it from the command line with the `BENCHMARK_DISABLE_JEMALLOC=true` +environment variable set: + +``` +BENCHMARK_DISABLE_JEMALLOC=true xed . +``` + +For more information please refer to `swift package benchmark --help` or the [documentation +of `package-benchmark`](https://swiftpackageindex.com/ordo-one/package-benchmark/documentation/benchmark). diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md new file mode 100644 index 000000000..26a2618f0 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -0,0 +1,28 @@ +# ``GRPCCore`` + +A gRPC library for Swift written natively in Swift. + +> 🚧 This module is part of gRPC Swift v2 which is under active development and in the pre-release +> stage. + +## Package structure + +gRPC Swift is made up of a number of modules, each of which is documented separately. However this +module – ``GRPCCore`` – includes higher level documentation such as tutorials. The following list +contains products of this package: + +- ``GRPCCore`` contains core types and abstractions and is the 'base' module for the project. +- `GRPCInProcessTransport` contains an implementation of an in-process transport. +- `GRPCHTTP2TransportNIOPosix` provides client and server implementations of HTTP/2 transports built + on top of SwiftNIO's POSIX Sockets abstractions. +- `GRPCHTTP2TransportNIOTransportServices` provides client and server implementations of HTTP/2 + transports built on top of SwiftNIO's Network.framework abstraction, `NIOTransportServices`. +- `GRPCProtobuf` provides serialization and deserialization components for `SwiftProtobuf`. + +## Topics + +### Getting involved + +Resources for developers working on gRPC Swift: + +- From 840352fe97bc75abdde817ef1430191115d0bf29 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 16 Aug 2024 16:07:03 +0100 Subject: [PATCH 430/580] Use Swift 6 to build docs on Swift Package Index (#2020) Motivation: We'd like v2 docs to be generated by the Swift Package Index. By default it uses the latest released Swift version, i.e. 5.10. As v2 requires Swift 6 we need to configure that. Modifications: - Set the swift version in .spi.yml Result: Docs for v2 should be built by Swift Package Index --- .spi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.spi.yml b/.spi.yml index 477270f7e..235846373 100644 --- a/.spi.yml +++ b/.spi.yml @@ -2,3 +2,4 @@ version: 1 builder: configs: - documentation_targets: [GRPC, GRPCReflectionService, protoc-gen-grpc-swift, _GRPCCore] + swift_version: 6.0 From ce6092ea46483291ff371fb4fffa25f79853610c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Aug 2024 09:06:23 +0100 Subject: [PATCH 431/580] Fix availability of health service (#2021) Motivation: A couple of availability annotations for the health service were incorrect for some platforms. Modifications: - Update availability Result: Builds on iOS etc. --- Sources/Services/Health/Health.swift | 4 ++-- Sources/Services/Health/HealthService.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Services/Health/Health.swift b/Sources/Services/Health/Health.swift index e2d5b33a5..641de83dd 100644 --- a/Sources/Services/Health/Health.swift +++ b/Sources/Services/Health/Health.swift @@ -40,7 +40,7 @@ public import GRPCCore /// forService: .bar_Foo /// ) /// ``` -@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Health: Sendable { /// An implementation of the `grpc.health.v1.Health` service. public let service: Health.Service @@ -58,7 +58,7 @@ public struct Health: Sendable { } } -@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Health { /// An implementation of the `grpc.health.v1.Health` service. public struct Service: RegistrableRPCService, Sendable { diff --git a/Sources/Services/Health/HealthService.swift b/Sources/Services/Health/HealthService.swift index e7081b889..8e1cbb92d 100644 --- a/Sources/Services/Health/HealthService.swift +++ b/Sources/Services/Health/HealthService.swift @@ -16,7 +16,7 @@ internal import GRPCCore -@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { private let state = HealthService.State() @@ -65,7 +65,7 @@ internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { } } -@available(macOS 15.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HealthService { private struct State: Sendable { // The state of each service keyed by the fully qualified service name. From a0db85bb2e06e3aa09461ec7864fdf92b60b0f6f Mon Sep 17 00:00:00 2001 From: Gustavo Cairo Date: Mon, 19 Aug 2024 11:07:00 +0100 Subject: [PATCH 432/580] Add support for TLS configuration in H2 Posix server transport (#1999) Motivation: We currently have a NIOPosix server transport implementation in gRPC v2, but it doesn't support TLS. Modifications: This PR adds support for TLS in the NIOPosix-backed HTTP/2 implementation of the server transport for gRPC v2. It also adds support for ALPN, to validate that the negotiated protocol, if required, is HTTP2 or `grpc-exp`. If it's not, an error will be fired/the channel will be closed, since we don't support H1. Result: We now support TLS/ALPN when using the NIOPosix server transport in gRPC V2. --- Package@swift-6.swift | 10 +- .../Examples/v2/Echo/Subcommands/Serve.swift | 5 +- .../Internal/NIOChannelPipeline+GRPC.swift | 8 +- .../ServerConnectionManagementHandler.swift | 23 ++ .../HTTP2ServerTransport+Posix.swift | 61 ++++- .../NIOSSL+GRPC.swift | 148 +++++++++++ .../TLSConfig.swift | 219 ++++++++++++++++ ...TP2ServerTransport+TransportServices.swift | 3 +- .../InteroperabilityTestsExecutable.swift | 4 +- .../PerformanceWorker.swift | 2 +- .../performance-worker/WorkerService.swift | 2 +- ...rverConnectionManagementHandlerTests.swift | 1 + .../HTTP2TransportNIOPosixTests.swift | 241 +++++++++++++++++- .../HTTP2TransportTests.swift | 2 +- 14 files changed, 706 insertions(+), 23 deletions(-) create mode 100644 Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift create mode 100644 Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 6bf77a6f1..f8e7eb4af 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -238,6 +238,7 @@ extension Target { .grpcCore, .nioCore, .nioHTTP2, + .nioTLS, .cgrpcZlib, .dequeModule, .atomics @@ -258,7 +259,10 @@ extension Target { .grpcHTTP2Core, .nioPosix, .nioExtras - ], + ].appending( + .nioSSL, + if: includeNIOSSL + ), swiftSettings: [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), @@ -449,7 +453,9 @@ extension Target { .grpcHTTP2TransportNIOPosix, .grpcHTTP2TransportNIOTransportServices, .grpcProtobuf - ], + ].appending( + .nioSSL, if: includeNIOSSL + ), swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } diff --git a/Sources/Examples/v2/Echo/Subcommands/Serve.swift b/Sources/Examples/v2/Echo/Subcommands/Serve.swift index ceb4149d0..3ce40ae8e 100644 --- a/Sources/Examples/v2/Echo/Subcommands/Serve.swift +++ b/Sources/Examples/v2/Echo/Subcommands/Serve.swift @@ -27,7 +27,10 @@ struct Serve: AsyncParsableCommand { var port: Int = 1234 func run() async throws { - let transport = HTTP2ServerTransport.Posix(address: .ipv4(host: "127.0.0.1", port: self.port)) + let transport = HTTP2ServerTransport.Posix( + address: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults(transportSecurity: .plaintext) + ) let server = GRPCServer(transport: transport, services: [EchoService()]) try await withThrowingDiscardingTaskGroup { group in group.addTask { try await server.run() } diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index dbbd837d2..52a2fa03c 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -32,7 +32,8 @@ extension ChannelPipeline.SynchronousOperations { connectionConfig: HTTP2ServerTransport.Config.Connection, http2Config: HTTP2ServerTransport.Config.HTTP2, rpcConfig: HTTP2ServerTransport.Config.RPC, - useTLS: Bool + requireALPN: Bool, + scheme: Scheme ) throws -> (HTTP2ConnectionChannel, HTTP2StreamMultiplexer) { let serverConnectionHandler = ServerConnectionManagementHandler( eventLoop: self.eventLoop, @@ -44,7 +45,8 @@ extension ChannelPipeline.SynchronousOperations { allowKeepaliveWithoutCalls: connectionConfig.keepalive.clientBehavior.allowWithoutCalls, minPingIntervalWithoutCalls: TimeAmount( connectionConfig.keepalive.clientBehavior.minPingIntervalWithoutCalls - ) + ), + requireALPN: requireALPN ) let flushNotificationHandler = GRPCServerFlushNotificationHandler( serverConnectionManagementHandler: serverConnectionHandler @@ -81,7 +83,7 @@ extension ChannelPipeline.SynchronousOperations { return streamChannel.eventLoop.makeCompletedFuture { let methodDescriptorPromise = streamChannel.eventLoop.makePromise(of: MethodDescriptor.self) let streamHandler = GRPCServerStreamHandler( - scheme: useTLS ? .https : .http, + scheme: scheme, acceptedEncodings: compressionConfig.enabledAlgorithms, maximumPayloadSize: rpcConfig.maxRequestPayloadSize, methodDescriptorPromise: methodDescriptorPromise diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift index 350cf44eb..3ceee927b 100644 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift @@ -14,8 +14,10 @@ * limitations under the License. */ +internal import GRPCCore internal import NIOCore internal import NIOHTTP2 +internal import NIOTLS /// A `ChannelHandler` which manages the lifecycle of a gRPC connection over HTTP/2. /// @@ -71,6 +73,7 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { /// Whether a flush is pending. private var flushPending: Bool + /// Whether `channelRead` has been called and `channelReadComplete` hasn't yet been called. /// Resets once `channelReadComplete` returns. private var inReadLoop: Bool @@ -84,6 +87,11 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { /// The clock. private let clock: Clock + /// Whether ALPN is required. + /// If it is but the TLS handshake finished without negotiating a protocol, an error will be fired down the + /// pipeline and the channel will be closed. + private let requireALPN: Bool + /// A clock providing the current time. /// /// This is necessary for testing where a manual clock can be used and advanced from the test. @@ -209,6 +217,7 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { keepaliveTimeout: TimeAmount?, allowKeepaliveWithoutCalls: Bool, minPingIntervalWithoutCalls: TimeAmount, + requireALPN: Bool, clock: Clock = .nio ) { self.eventLoop = eventLoop @@ -235,6 +244,8 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { self.inReadLoop = false self.clock = clock self.frameStats = FrameStats() + + self.requireALPN = requireALPN } func handlerAdded(context: ChannelHandlerContext) { @@ -284,6 +295,18 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler { case is ChannelShouldQuiesceEvent: self.initiateGracefulShutdown(context: context) + case TLSUserEvent.handshakeCompleted(let negotiatedProtocol): + if negotiatedProtocol == nil, self.requireALPN { + // No ALPN protocol negotiated but it was required: fire an error and close the channel. + context.fireErrorCaught( + RPCError( + code: .internalError, + message: "ALPN resulted in no protocol being negotiated, but it was required." + ) + ) + context.close(mode: .all, promise: nil) + } + default: () } diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 18074fa0c..450865a25 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -21,6 +21,10 @@ internal import NIOExtras internal import NIOHTTP2 public import NIOPosix // has to be public because of default argument value in init +#if canImport(NIOSSL) +import NIOSSL +#endif + extension HTTP2ServerTransport { /// A NIOPosix-backed implementation of a server transport. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -150,7 +154,7 @@ extension HTTP2ServerTransport { /// - eventLoopGroup: The ELG from which to get ELs to run this transport. public init( address: GRPCHTTP2Core.SocketAddress, - config: Config = .defaults, + config: Config, eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup ) { self.address = address @@ -174,6 +178,24 @@ extension HTTP2ServerTransport { } } + #if canImport(NIOSSL) + let nioSSLContext: NIOSSLContext? + switch self.config.transportSecurity.wrapped { + case .plaintext: + nioSSLContext = nil + case .tls(let tlsConfig): + do { + nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig)) + } catch { + throw RuntimeError( + code: .transportError, + message: "Couldn't create SSL context, check your TLS configuration.", + cause: error + ) + } + } + #endif + let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup) .serverChannelOption( ChannelOptions.socketOption(.so_reuseaddr), @@ -187,13 +209,33 @@ extension HTTP2ServerTransport { } .bind(to: self.address) { channel in channel.eventLoop.makeCompletedFuture { + #if canImport(NIOSSL) + if let nioSSLContext { + try channel.pipeline.syncOperations.addHandler( + NIOSSLServerHandler(context: nioSSLContext) + ) + } + #endif + + let requireALPN: Bool + let scheme: Scheme + switch self.config.transportSecurity.wrapped { + case .plaintext: + requireALPN = false + scheme = .http + case .tls(let tlsConfig): + requireALPN = tlsConfig.requireALPN + scheme = .https + } + return try channel.pipeline.syncOperations.configureGRPCServerPipeline( channel: channel, compressionConfig: self.config.compression, connectionConfig: self.config.connection, http2Config: self.config.http2, rpcConfig: self.config.rpc, - useTLS: false + requireALPN: requireALPN, + scheme: scheme ) } } @@ -289,7 +331,6 @@ extension HTTP2ServerTransport { self.serverQuiescingHelper.initiateShutdown(promise: nil) } } - } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -304,6 +345,8 @@ extension HTTP2ServerTransport.Posix { public var http2: HTTP2ServerTransport.Config.HTTP2 /// RPC configuration. public var rpc: HTTP2ServerTransport.Config.RPC + /// The transport's security. + public var transportSecurity: TransportSecurity /// Construct a new `Config`. /// - Parameters: @@ -311,25 +354,31 @@ extension HTTP2ServerTransport.Posix { /// - connection: Connection configuration. /// - http2: HTTP2 configuration. /// - rpc: RPC configuration. + /// - transportSecurity: The transport's security configuration. public init( compression: HTTP2ServerTransport.Config.Compression, connection: HTTP2ServerTransport.Config.Connection, http2: HTTP2ServerTransport.Config.HTTP2, - rpc: HTTP2ServerTransport.Config.RPC + rpc: HTTP2ServerTransport.Config.RPC, + transportSecurity: TransportSecurity ) { self.compression = compression self.connection = connection self.http2 = http2 self.rpc = rpc + self.transportSecurity = transportSecurity } /// Default values for the different configurations. - public static var defaults: Self { + public static func defaults( + transportSecurity: TransportSecurity + ) -> Self { Self( compression: .defaults, connection: .defaults, http2: .defaults, - rpc: .defaults + rpc: .defaults, + transportSecurity: transportSecurity ) } } diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift new file mode 100644 index 000000000..4bc3f143e --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift @@ -0,0 +1,148 @@ +/* + * 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. + */ +#if canImport(NIOSSL) +import NIOSSL + +extension NIOSSLSerializationFormats { + fileprivate init(_ format: TLSConfig.SerializationFormat) { + switch format.wrapped { + case .pem: + self = .pem + case .der: + self = .der + } + } +} + +extension Sequence { + func sslCertificateSources() throws -> [NIOSSLCertificateSource] { + var certificateSources: [NIOSSLCertificateSource] = [] + for source in self { + switch source.wrapped { + case .bytes(let bytes, let serializationFormat): + switch serializationFormat.wrapped { + case .der: + certificateSources.append( + .certificate(try NIOSSLCertificate(bytes: bytes, format: .der)) + ) + + case .pem: + let certificates = try NIOSSLCertificate.fromPEMBytes(bytes).map { + NIOSSLCertificateSource.certificate($0) + } + certificateSources.append(contentsOf: certificates) + } + + case .file(let path, let serializationFormat): + switch serializationFormat.wrapped { + case .der: + certificateSources.append( + .certificate(try NIOSSLCertificate(file: path, format: .der)) + ) + + case .pem: + let certificates = try NIOSSLCertificate.fromPEMFile(path).map { + NIOSSLCertificateSource.certificate($0) + } + certificateSources.append(contentsOf: certificates) + } + } + } + return certificateSources + } +} + +extension NIOSSLPrivateKey { + fileprivate convenience init( + privateKey source: TLSConfig.PrivateKeySource + ) throws { + switch source.wrapped { + case .file(let path, let serializationFormat): + try self.init( + file: path, + format: NIOSSLSerializationFormats(serializationFormat) + ) + case .bytes(let bytes, let serializationFormat): + try self.init( + bytes: bytes, + format: NIOSSLSerializationFormats(serializationFormat) + ) + } + } +} + +extension NIOSSLTrustRoots { + fileprivate init(_ trustRoots: TLSConfig.TrustRootsSource) throws { + switch trustRoots.wrapped { + case .certificates(let certificateSources): + let certificates = try certificateSources.map { source in + switch source.wrapped { + case .bytes(let bytes, let serializationFormat): + return try NIOSSLCertificate( + bytes: bytes, + format: NIOSSLSerializationFormats(serializationFormat) + ) + case .file(let path, let serializationFormat): + return try NIOSSLCertificate( + file: path, + format: NIOSSLSerializationFormats(serializationFormat) + ) + } + } + self = .certificates(certificates) + + case .systemDefault: + self = .default + } + } +} + +extension CertificateVerification { + fileprivate init( + _ verificationMode: TLSConfig.CertificateVerification + ) { + switch verificationMode.wrapped { + case .doNotVerify: + self = .none + case .fullVerification: + self = .fullVerification + case .noHostnameVerification: + self = .noHostnameVerification + } + } +} + +extension TLSConfiguration { + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + package init(_ tlsConfig: HTTP2ServerTransport.Posix.Config.TLS) throws { + let certificateChain = try tlsConfig.certificateChain.sslCertificateSources() + let privateKey = try NIOSSLPrivateKey(privateKey: tlsConfig.privateKey) + + var tlsConfiguration = TLSConfiguration.makeServerConfiguration( + certificateChain: certificateChain, + privateKey: .privateKey(privateKey) + ) + tlsConfiguration.minimumTLSVersion = .tlsv12 + tlsConfiguration.certificateVerification = CertificateVerification( + tlsConfig.clientCertificateVerification + ) + tlsConfiguration.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots) + tlsConfiguration.applicationProtocols = ["grpc-exp", "h2"] + + self = tlsConfiguration + } +} +#endif diff --git a/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift b/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift new file mode 100644 index 000000000..591bafbe7 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift @@ -0,0 +1,219 @@ +/* + * 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. + */ + +public enum TLSConfig: Sendable { + /// The serialization format of the provided certificates and private keys. + public struct SerializationFormat: Sendable, Equatable { + package enum Wrapped { + case pem + case der + } + + package let wrapped: Wrapped + + public static let pem = Self(wrapped: .pem) + public static let der = Self(wrapped: .der) + } + + /// A description of where a certificate is coming from: either a byte array or a file. + /// The serialization format is specified by ``TLSConfig/SerializationFormat``. + public struct CertificateSource: Sendable { + package enum Wrapped { + case file(path: String, format: SerializationFormat) + case bytes(bytes: [UInt8], format: SerializationFormat) + } + + package let wrapped: Wrapped + + /// The certificate's source is a file. + /// - Parameters: + /// - path: The file path containing the certificate. + /// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``. + /// - Returns: A source describing the certificate source is the given file. + public static func file(path: String, format: SerializationFormat) -> Self { + Self(wrapped: .file(path: path, format: format)) + } + + /// The certificate's source is an array of bytes. + /// - Parameters: + /// - bytes: The array of bytes making up the certificate. + /// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``. + /// - Returns: A source describing the certificate source is the given bytes. + public static func bytes(_ bytes: [UInt8], format: SerializationFormat) -> Self { + Self(wrapped: .bytes(bytes: bytes, format: format)) + } + } + + /// A description of where the private key is coming from: either a byte array or a file. + /// The serialization format is specified by ``TLSConfig/SerializationFormat``. + public struct PrivateKeySource: Sendable { + package enum Wrapped { + case file(path: String, format: SerializationFormat) + case bytes(bytes: [UInt8], format: SerializationFormat) + } + + package let wrapped: Wrapped + + /// The private key's source is a file. + /// - Parameters: + /// - path: The file path containing the private key. + /// - format: The private key's format, as a ``TLSConfig/SerializationFormat``. + /// - Returns: A source describing the private key source is the given file. + public static func file(path: String, format: SerializationFormat) -> Self { + Self(wrapped: .file(path: path, format: format)) + } + + /// The private key's source is an array of bytes. + /// - Parameters: + /// - bytes: The array of bytes making up the private key. + /// - format: The private key's format, as a ``TLSConfig/SerializationFormat``. + /// - Returns: A source describing the private key source is the given bytes. + public static func bytes( + _ bytes: [UInt8], + format: SerializationFormat + ) -> Self { + Self(wrapped: .bytes(bytes: bytes, format: format)) + } + } + + /// A description of where the trust roots are coming from: either a custom certificate chain, or the system default trust store. + public struct TrustRootsSource: Sendable { + package enum Wrapped { + case certificates([CertificateSource]) + case systemDefault + } + + package let wrapped: Wrapped + + /// A list of ``TLSConfig/CertificateSource``s making up the + /// chain of trust. + /// - Parameter certificateSources: The sources for the certificates that make up the chain of trust. + /// - Returns: A trust root for the given chain of trust. + public static func certificates( + _ certificateSources: [CertificateSource] + ) -> Self { + Self(wrapped: .certificates(certificateSources)) + } + + /// The system default trust store. + public static let systemDefault: Self = Self(wrapped: .systemDefault) + } + + /// How to verify client certificates. + public struct CertificateVerification: Sendable { + package enum Wrapped { + case doNotVerify + case fullVerification + case noHostnameVerification + } + + package let wrapped: Wrapped + + /// All certificate verification disabled. + public static let noVerification: Self = Self(wrapped: .doNotVerify) + + /// Certificates will be validated against the trust store, but will not be checked to see if they are valid for the given hostname. + public static let noHostnameVerification: Self = Self(wrapped: .noHostnameVerification) + + /// Certificates will be validated against the trust store and checked against the hostname of the service we are contacting. + public static let fullVerification: Self = Self(wrapped: .fullVerification) + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HTTP2ServerTransport.Posix.Config { + /// The security configuration for this connection. + public struct TransportSecurity: Sendable { + package enum Wrapped: Sendable { + case plaintext + case tls(TLS) + } + + package let wrapped: Wrapped + + /// This connection is plaintext: no encryption will take place. + public static let plaintext = Self(wrapped: .plaintext) + + /// This connection will use TLS. + public static func tls(_ tls: TLS) -> Self { + Self(wrapped: .tls(tls)) + } + } + + public struct TLS: Sendable { + /// The certificates the server will offer during negotiation. + public var certificateChain: [TLSConfig.CertificateSource] + + /// The private key associated with the leaf certificate. + public var privateKey: TLSConfig.PrivateKeySource + + /// How to verify the client certificate, if one is presented. + public var clientCertificateVerification: TLSConfig.CertificateVerification + + /// The trust roots to be used when verifying client certificates. + public var trustRoots: TLSConfig.TrustRootsSource + + /// Whether ALPN is required. + /// + /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. + public var requireALPN: Bool + + /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: + /// - `clientCertificateVerificationMode` equals `doNotVerify` + /// - `trustRoots` equals `systemDefault` + /// - `requireALPN` equals `false` + /// + /// - Parameters: + /// - certificateChain: The certificates the server will offer during negotiation. + /// - privateKey: The private key associated with the leaf certificate. + /// - Returns: A new HTTP2 NIO Posix transport TLS config. + public static func defaults( + certificateChain: [TLSConfig.CertificateSource], + privateKey: TLSConfig.PrivateKeySource + ) -> Self { + Self.init( + certificateChain: certificateChain, + privateKey: privateKey, + clientCertificateVerification: .noVerification, + trustRoots: .systemDefault, + requireALPN: false + ) + } + + /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match + /// the requirements of mTLS: + /// - `clientCertificateVerificationMode` equals `noHostnameVerification` + /// - `trustRoots` equals `systemDefault` + /// - `requireALPN` equals `false` + /// + /// - Parameters: + /// - certificateChain: The certificates the server will offer during negotiation. + /// - privateKey: The private key associated with the leaf certificate. + /// - Returns: A new HTTP2 NIO Posix transport TLS config. + public static func mTLS( + certificateChain: [TLSConfig.CertificateSource], + privateKey: TLSConfig.PrivateKeySource + ) -> Self { + Self.init( + certificateChain: certificateChain, + privateKey: privateKey, + clientCertificateVerification: .noHostnameVerification, + trustRoots: .systemDefault, + requireALPN: false + ) + } + } +} diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index e381068ab..1538a6767 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -188,7 +188,8 @@ extension HTTP2ServerTransport { connectionConfig: self.config.connection, http2Config: self.config.http2, rpcConfig: self.config.rpc, - useTLS: false + requireALPN: false, + scheme: .http ) } } diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift index 3694a5827..a47ab8b3f 100644 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -38,7 +38,9 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { var port: Int func run() async throws { - var transportConfig = HTTP2ServerTransport.Posix.Config.defaults + var transportConfig = HTTP2ServerTransport.Posix.Config.defaults( + transportSecurity: .plaintext + ) transportConfig.compression.enabledAlgorithms = .all let transport = HTTP2ServerTransport.Posix( address: .ipv4(host: "0.0.0.0", port: self.port), diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift index 7f1493579..a52fc4d37 100644 --- a/Sources/performance-worker/PerformanceWorker.swift +++ b/Sources/performance-worker/PerformanceWorker.swift @@ -52,7 +52,7 @@ struct PerformanceWorker: AsyncParsableCommand { let transport = HTTP2ServerTransport.Posix( address: .ipv4(host: "127.0.0.1", port: self.driverPort), - config: .defaults + config: .defaults(transportSecurity: .plaintext) ) let server = GRPCServer(transport: transport, services: [WorkerService()]) diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index aa800e20a..8f090dd00 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -388,7 +388,7 @@ extension WorkerService { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads) // Don't restrict the max payload size, the client is always trusted. - var config = HTTP2ServerTransport.Posix.Config.defaults + var config = HTTP2ServerTransport.Posix.Config.defaults(transportSecurity: .plaintext) config.rpc.maxRequestPayloadSize = .max let transport = HTTP2ServerTransport.Posix( diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift index c0e0805a6..b4bfb0066 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift @@ -361,6 +361,7 @@ extension ServerConnectionManagementHandlerTests { keepaliveTimeout: keepaliveTimeout, allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls, minPingIntervalWithoutCalls: minPingIntervalWithoutCalls, + requireALPN: false, clock: self.clock ) diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index 0b53135d8..fae9960cf 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -19,11 +19,16 @@ private import GRPCHTTP2Core private import GRPCHTTP2TransportNIOPosix internal import XCTest +#if canImport(NIOSSL) +private import NIOSSL +#endif + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportNIOPosixTests: XCTestCase { func testGetListeningAddress_IPv4() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .ipv4(host: "0.0.0.0", port: 0) + address: .ipv4(host: "0.0.0.0", port: 0), + config: .defaults(transportSecurity: .plaintext) ) try await withThrowingDiscardingTaskGroup { group in @@ -42,7 +47,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { func testGetListeningAddress_IPv6() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .ipv6(host: "::1", port: 0) + address: .ipv6(host: "::1", port: 0), + config: .defaults(transportSecurity: .plaintext) ) try await withThrowingDiscardingTaskGroup { group in @@ -61,7 +67,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { func testGetListeningAddress_UnixDomainSocket() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .unixDomainSocket(path: "/tmp/posix-uds-test") + address: .unixDomainSocket(path: "/tmp/posix-uds-test"), + config: .defaults(transportSecurity: .plaintext) ) try await withThrowingDiscardingTaskGroup { group in @@ -84,7 +91,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { try XCTSkipUnless(self.vsockAvailable(), "Vsock unavailable") let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .vsock(contextID: .any, port: .any) + address: .vsock(contextID: .any, port: .any), + config: .defaults(transportSecurity: .plaintext) ) try await withThrowingDiscardingTaskGroup { group in @@ -102,7 +110,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { func testGetListeningAddress_InvalidAddress() async { let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .unixDomainSocket(path: "/this/should/be/an/invalid/path") + address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"), + config: .defaults(transportSecurity: .plaintext) ) try? await withThrowingDiscardingTaskGroup { group in @@ -130,7 +139,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { func testGetListeningAddress_StoppedListening() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .ipv4(host: "0.0.0.0", port: 0) + address: .ipv4(host: "0.0.0.0", port: 0), + config: .defaults(transportSecurity: .plaintext) ) try? await withThrowingDiscardingTaskGroup { group in @@ -159,4 +169,223 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } } } + + #if canImport(NIOSSL) + static let samplePemCert = """ + -----BEGIN CERTIFICATE----- + MIIGGzCCBAOgAwIBAgIJAJ/X0Fo0ynmEMA0GCSqGSIb3DQEBCwUAMIGjMQswCQYD + VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5z + b2t5bzEuMCwGA1UECgwlU2FuIEZyYW5zb2t5byBJbnN0aXR1dGUgb2YgVGVjaG5v + bG9neTEVMBMGA1UECwwMUm9ib3RpY3MgTGFiMSAwHgYDVQQDDBdyb2JvdHMuc2Fu + ZnJhbnNva3lvLmVkdTAeFw0xNzEwMTYyMTAxMDJaFw00NzEwMDkyMTAxMDJaMIGj + MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu + IEZyYW5zb2t5bzEuMCwGA1UECgwlU2FuIEZyYW5zb2t5byBJbnN0aXR1dGUgb2Yg + VGVjaG5vbG9neTEVMBMGA1UECwwMUm9ib3RpY3MgTGFiMSAwHgYDVQQDDBdyb2Jv + dHMuc2FuZnJhbnNva3lvLmVkdTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC + ggIBAO9rzJOOE8cmsIqAJMCrHDxkBAMgZhMsJ863MnWtVz5JIJK6CKI/Nu26tEzo + kHy3EI9565RwikvauheMsWaTFA4PD/P+s1DtxRCGIcK5x+SoTN7Drn5ZueoJNZRf + TYuN+gwyhprzrZrYjXpvEVPYuSIeUqK5XGrTyFA2uGj9wY3f9IF4rd7JT0ewRb1U + 8OcR7xQbXKGjkY4iJE1TyfmIsBZboKaG/aYa9KbnWyTkDssaELWUIKrjwwuPgVgS + vlAYmo12MlsGEzkO9z78jvFmhUOsaEldM8Ua2AhOKW0oSYgauVuro/Ap/o5zn8PD + IDapl9g+5vjN2LucqX2a9utoFvxSKXT4NvfpL9fJvzdBNMM4xpqtHIkV0fkiMbWk + EW2FFlOXKnIJV8wT4a9iduuIDMg8O7oc+gt9pG9MHTWthXm4S29DARTqfZ48bW77 + z8RrEURV03o05b/twuAJSRyyOCUi61yMo3YNytebjY2W3Pxqpq+YmT5qhqBZDLlT + LMptuFdISv6SQgg7JoFHGMWRXUavMj/sn5qZD4pQyZToHJ2Vtg5W/MI1pKwc3oKD + 6M3/7Gf35r92V/ox6XT7+fnEsAH8AtQiZJkEbvzJ5lpUihSIaV3a/S+jnk7Lw8Tp + vjtpfjOg+wBblc38Oa9tk2WdXwYDbnvbeL26WmyHwQTUBi1jAgMBAAGjUDBOMB0G + A1UdDgQWBBToPRmTBQEF5F5LcPiUI5qBNPBU+DAfBgNVHSMEGDAWgBToPRmTBQEF + 5F5LcPiUI5qBNPBU+DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCY + gxM5lufF2lTB9sH0s1E1VTERv37qoapNP+aw06oZkAD67QOTXFzbsM3JU1diY6rV + Y0g9CLzRO7gZY+kmi1WWnsYiMMSIGjIfsB8S+ot43LME+AJXPVeDZQnoZ6KQ/9r+ + 71Umi4AKLoZ9dInyUIM3EHg9pg5B0eEINrh4J+OPGtlC3NMiWxdmIkZwzfXa+64Z + 8k5aX5piMTI+9BQSMWw5l7tFT/PISuI8b/Ln4IUBXKA0xkONXVnjPOmS0h7MBoc2 + EipChDKnK+Mtm9GQewOCKdS2nsrCndGkIBnUix4ConUYIoywVzWGMD+9OzKNg76d + O6A7MxdjEdKhf1JDvklxInntDUDTlSFL4iEFELwyRseoTzj8vJE+cL6h6ClasYQ6 + p0EeL3UpICYerfIvPhohftCivCH3k7Q1BSf0fq73cQ55nrFAHrqqYjD7HBeBS9hn + 3L6bz9Eo6U9cuxX42k3l1N44BmgcDPin0+CRTirEmahUMb3gmvoSZqQ3Cz86GkIg + 7cNJosc9NyevQlU9SX3ptEbv33tZtlB5GwgZ2hiGBTY0C3HaVFjLpQiSS5ygZLgI + /+AKtah7sTHIAtpUH1ZZEgKPl1Hg6J4x/dBkuk3wxPommNHaYaHREXF+fHMhBrSi + yH8agBmmECpa21SVnr7vrL+KSqfuF+GxwjSNsSR4SA== + -----END CERTIFICATE----- + """ + + static let samplePemKey = """ + -----BEGIN RSA PRIVATE KEY----- + MIIJKAIBAAKCAgEA72vMk44TxyawioAkwKscPGQEAyBmEywnzrcyda1XPkkgkroI + oj827bq0TOiQfLcQj3nrlHCKS9q6F4yxZpMUDg8P8/6zUO3FEIYhwrnH5KhM3sOu + flm56gk1lF9Ni436DDKGmvOtmtiNem8RU9i5Ih5SorlcatPIUDa4aP3Bjd/0gXit + 3slPR7BFvVTw5xHvFBtcoaORjiIkTVPJ+YiwFlugpob9phr0pudbJOQOyxoQtZQg + quPDC4+BWBK+UBiajXYyWwYTOQ73PvyO8WaFQ6xoSV0zxRrYCE4pbShJiBq5W6uj + 8Cn+jnOfw8MgNqmX2D7m+M3Yu5ypfZr262gW/FIpdPg29+kv18m/N0E0wzjGmq0c + iRXR+SIxtaQRbYUWU5cqcglXzBPhr2J264gMyDw7uhz6C32kb0wdNa2FebhLb0MB + FOp9njxtbvvPxGsRRFXTejTlv+3C4AlJHLI4JSLrXIyjdg3K15uNjZbc/Gqmr5iZ + PmqGoFkMuVMsym24V0hK/pJCCDsmgUcYxZFdRq8yP+yfmpkPilDJlOgcnZW2Dlb8 + wjWkrBzegoPozf/sZ/fmv3ZX+jHpdPv5+cSwAfwC1CJkmQRu/MnmWlSKFIhpXdr9 + L6OeTsvDxOm+O2l+M6D7AFuVzfw5r22TZZ1fBgNue9t4vbpabIfBBNQGLWMCAwEA + AQKCAgArWV9PEBhwpIaubQk6gUC5hnpbfpA8xG/os67FM79qHZ9yMZDCn6N4Y6el + jS4sBpFPCQoodD/2AAJVpTmxksu8x+lhiio5avOVTFPsh+qzce2JH/EGG4TX5Rb4 + aFEIBYrSjotknt49/RuQoW+HuOO8U7UulVUwWmwYae/1wow6/eOtVYZVoilil33p + C+oaTFr3TwT0l0MRcwkTnyogrikDw09RF3vxiUvmtFkCUvCCwZNo7QsFJfv4qeEH + a01d/zZsiowPgwgT+qu1kdDn0GIsoJi5P9DRzUx0JILHqtW1ePE6sdca8t+ON00k + Cr5YZ1iA5NK5Fbw6K+FcRqSSduRCLYXAnI5GH1zWMki5TUdl+psvCnpdZK5wysGe + tYfIbrVHXIlg7J3R4BrbMF4q3HwOppTHMrqsGyRVCCSjDwXjreugInV0CRzlapDs + JNEVyrbt6Ild6ie7c1AJqTpibJ9lVYRVpG35Dni9RJy5Uk5m89uWnF9PCjCRCHOf + 4UATY+qie6wlu0E8y43LcTvDi8ROXQQoCnys2ES8DmS+GKJ1uzG1l8jx3jF9BMAJ + kyzZfSmPwuS2NUk8sftYQ8neJSgk4DOV4h7x5ghaBWYzseomy3uo3gD4IyuiO56K + y7IYZnXSt2s8LfzhVcB5I4IZbSIvP/MAEkGMC09SV+dEcEJSQQKCAQEA/uJex1ef + g+q4gb/C4/biPr+ZRFheVuHu49ES0DXxoxmTbosGRDPRFBLwtPxCLuzHXa1Du2Vc + c0E12zLy8wNczv5bGAxynPo57twJCyeptFNFJkb+0uxRrCi+CZ56Qertg2jr460Q + cg+TMYxauDleLzR7uwL6VnOhTSq3CVTA2TrQ+kjIHgVqmmpwgk5bPBRDj2EuqdyD + dEQmt4z/0fFFBmW6iBcXS9y8Q1rCnAHKjDUEoXKyJYL85szupjUuerOt6iTIe7CJ + pH0REwQO4djwM4Ju/PEGfBs+RqgNXoHmBMcFdf9RdogCuFit7lX0+LlRT/KJitan + LaaFgY1TXTVkcwKCAQEA8HgZuPGVHQTMHCOfNesXxnCY9Dwqa9ZVukqDLMaZ0TVy + PIqXhdNeVCWpP+VXWhj9JRLNuW8VWYMxk+poRmsZgbdwSbq30ljsGlfoupCpXfhd + AIhUeRwLVl4XnaHW+MjAmY/rqO156/LvNbV5e0YsqObzynlTczmhhYwi48x1tdf0 + iuCn8o3+Ikv8xM7MuMnv5QmGp2l8Q3BhwxLN1x4MXfbG+4BGsqavudIkt71RVbSb + Sp7U4Khq3UEnCekrceRLQpJykRFu11/ntPsJ0Q+fLuvuRUMg/wsq8WTuVlwLrw46 + hlRcq6S99jc9j2TbidxHyps6j8SDnEsEFHMHH8THUQKCAQAd03WN1CYZdL0UidEP + hhNhjmAsDD814Yhn5k5SSQ22rUaAWApqrrmXpMPAGgjQnuqRfrX/VtQjtIzN0r91 + Sn5wxnj4bnR3BB0FY4A3avPD4z6jRQmKuxavk7DxRTc/QXN7vipkYRscjdAGq0ru + ZeAsm/Kipq2Oskc81XPHxsAua2CK+TtZr/6ShUQXK34noKNrQs8IF4LWdycksX46 + Hgaawgq65CDYwsLRCuzc/qSqFYYuMlLAavyXMYH3tx9yQlZmoNlJCBaDRhNaa04m + hZFOJcRBGx9MJI/8CqxN09uL0ZJFBZSNz0qqMc5gpnRdKqpmNZZ8xbOYdvUGfPg1 + XwsbAoIBAGdH7iRU/mp8SP48/oC1/HwqmEcuIDo40JE2t6hflGkav3npPLMp2XXi + xxK+egokeXWW4e0nHNBZXM3e+/JixY3FL+E65QDfWGjoIPkgcN3/clJsO3vY47Ww + rAv0GtS3xKEwA1OGy7rfmIZE72xW84+HwmXQPltbAVjOm52jj1sO6eVMIFY5TlGE + uYf+Gkez0+lXchItaEW+2v5h8S7XpRAmkcgrjDHnDcqNy19vXKOm8pvWJDBppZxq + A05qa1J7byekprhP+H9gnbBJsimsv/3zL19oOZ/ROBx98S/+ULZbMh/H1BWUqFI7 + 36Da/L/1cJBAo6JkEPLr9VCjJwgqCEECggEBAI6+35Lf4jDwRPvZV7kE+FQuFp1G + /tKxIJtPOZU3sbOVlsFsOoyEfV6+HbpeWxlWnrOnKRFOLoC3s5MVTjPglu1rC0ZX + 4b0wMetvun5S1MGadB808rvu5EsEB1vznz1vOXV8oDdkdgBiiUcKewSeCrG1IrXy + B9ux859S3JjELzeuNdz+xHqu2AqR22gtqN72tJUEQ95qLGZ8vo+ytY9MDVDqoSWJ + 9pqHXFUVLmwHTM0/pciXN4Kx1IL9FZ3fjXgME0vdYpWYQkcvSKLsswXN+LnYcpoQ + h33H/Kz4yji7jPN6Uk9wMyG7XGqpjYAuKCd6V3HEHUiGJZzho/VBgb3TVnw= + -----END RSA PRIVATE KEY----- + """ + + func testTLSConfig_Defaults() throws { + let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( + certificateChain: [ + .bytes(Array(Self.samplePemCert.utf8), format: .pem) + ], + privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) + ) + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual( + nioSSLTLSConfig.certificateChain, + [ + .certificate( + try NIOSSLCertificate( + bytes: Array(Self.samplePemCert.utf8), + format: .pem + ) + ) + ] + ) + XCTAssertEqual( + nioSSLTLSConfig.privateKey, + .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) + ) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .none) + XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } + + func testTLSConfig_mTLS() throws { + let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.mTLS( + certificateChain: [ + .bytes(Array(Self.samplePemCert.utf8), format: .pem) + ], + privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) + ) + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual( + nioSSLTLSConfig.certificateChain, + [ + .certificate( + try NIOSSLCertificate( + bytes: Array(Self.samplePemCert.utf8), + format: .pem + ) + ) + ] + ) + XCTAssertEqual( + nioSSLTLSConfig.privateKey, + .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) + ) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .noHostnameVerification) + XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } + + func testTLSConfig_FullVerifyClient() throws { + var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( + certificateChain: [ + .bytes(Array(Self.samplePemCert.utf8), format: .pem) + ], + privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) + ) + grpcTLSConfig.clientCertificateVerification = .fullVerification + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual( + nioSSLTLSConfig.certificateChain, + [ + .certificate( + try NIOSSLCertificate( + bytes: Array(Self.samplePemCert.utf8), + format: .pem + ) + ) + ] + ) + XCTAssertEqual( + nioSSLTLSConfig.privateKey, + .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) + ) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) + XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } + + func testTLSConfig_CustomTrustRoots() throws { + var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( + certificateChain: [ + .bytes(Array(Self.samplePemCert.utf8), format: .pem) + ], + privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) + ) + grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.utf8), format: .pem)]) + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual( + nioSSLTLSConfig.certificateChain, + [ + .certificate( + try NIOSSLCertificate( + bytes: Array(Self.samplePemCert.utf8), + format: .pem + ) + ) + ] + ) + XCTAssertEqual( + nioSSLTLSConfig.privateKey, + .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) + ) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .none) + XCTAssertEqual( + nioSSLTLSConfig.trustRoots, + .certificates(try NIOSSLCertificate.fromPEMBytes(Array(Self.samplePemCert.utf8))) + ) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } + #endif } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index ddca6d01b..8a70959bf 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -144,7 +144,7 @@ final class HTTP2TransportTests: XCTestCase { switch kind { case .posix: - var config = HTTP2ServerTransport.Posix.Config.defaults + var config = HTTP2ServerTransport.Posix.Config.defaults(transportSecurity: .plaintext) config.compression.enabledAlgorithms = compression let transport = HTTP2ServerTransport.Posix( address: .ipv4(host: "127.0.0.1", port: 0), From 39f16819302434236b09a4b8da55a1a573b66ba6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Aug 2024 14:01:50 +0100 Subject: [PATCH 433/580] Add hello-world example (#2017) Motivation: Examples are great, we should add more. Modifications: - Add a 'Hello World' example Result: More examples --- Package@swift-6.swift | 21 +- Protos/generate.sh | 15 +- Sources/Examples/v2/{Echo => echo}/Echo.swift | 0 .../{Echo => echo}/Generated/echo.grpc.swift | 0 .../v2/{Echo => echo}/Generated/echo.pb.swift | 0 .../Subcommands/ClientArguments.swift | 0 .../{Echo => echo}/Subcommands/Collect.swift | 0 .../{Echo => echo}/Subcommands/Expand.swift | 0 .../v2/{Echo => echo}/Subcommands/Get.swift | 0 .../v2/{Echo => echo}/Subcommands/Serve.swift | 0 .../{Echo => echo}/Subcommands/Update.swift | 0 .../Generated/helloworld.grpc.swift | 181 ++++++++++++++++++ .../hello-world/Generated/helloworld.pb.swift | 129 +++++++++++++ .../Examples/v2/hello-world/HelloWorld.swift | 27 +++ .../v2/hello-world/Subcommands/Greet.swift | 49 +++++ .../v2/hello-world/Subcommands/Serve.swift | 54 ++++++ 16 files changed, 472 insertions(+), 4 deletions(-) rename Sources/Examples/v2/{Echo => echo}/Echo.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Generated/echo.grpc.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Generated/echo.pb.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Subcommands/ClientArguments.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Subcommands/Collect.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Subcommands/Expand.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Subcommands/Get.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Subcommands/Serve.swift (100%) rename Sources/Examples/v2/{Echo => echo}/Subcommands/Update.swift (100%) create mode 100644 Sources/Examples/v2/hello-world/Generated/helloworld.grpc.swift create mode 100644 Sources/Examples/v2/hello-world/Generated/helloworld.pb.swift create mode 100644 Sources/Examples/v2/hello-world/HelloWorld.swift create mode 100644 Sources/Examples/v2/hello-world/Subcommands/Greet.swift create mode 100644 Sources/Examples/v2/hello-world/Subcommands/Serve.swift diff --git a/Package@swift-6.swift b/Package@swift-6.swift index f8e7eb4af..0b6d0cbb5 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -152,6 +152,7 @@ extension Target.Dependency { static var grpcInProcessTransport: Self { .target(name: "GRPCInProcessTransport") } static var grpcInterceptors: Self { .target(name: "GRPCInterceptors") } static var grpcHTTP2Core: Self { .target(name: "GRPCHTTP2Core") } + static var grpcHTTP2Transport: Self { .target(name: "GRPCHTTP2Transport") } static var grpcHTTP2TransportNIOPosix: Self { .target(name: "GRPCHTTP2TransportNIOPosix") } static var grpcHTTP2TransportNIOTransportServices: Self { .target(name: "GRPCHTTP2TransportNIOTransportServices") } static var grpcHealth: Self { .target(name: "GRPCHealth") } @@ -706,7 +707,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - path: "Sources/Examples/v2/Echo", + path: "Sources/Examples/v2/echo", swiftSettings: [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), @@ -758,6 +759,23 @@ extension Target { ) } + static var helloWorld_v2: Target { + .executableTarget( + name: "hello-world", + dependencies: [ + .grpcProtobuf, + .grpcHTTP2Transport, + .argumentParser, + ], + path: "Sources/Examples/v2/hello-world", + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] + ) + } + static var routeGuideModel: Target { .target( name: "RouteGuideModel", @@ -1069,6 +1087,7 @@ let package = Package( // v2 examples .echo_v2, + .helloWorld_v2, ] ) diff --git a/Protos/generate.sh b/Protos/generate.sh index 17c803049..a1304d5e4 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -82,7 +82,7 @@ function generate_echo_v1_example { function generate_echo_v2_example { local proto="$here/examples/echo/echo.proto" - local output="$root/Sources/Examples/v2/Echo/Generated" + local output="$root/Sources/Examples/v2/echo/Generated" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" @@ -96,7 +96,7 @@ function generate_routeguide_example { generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" } -function generate_helloworld_example { +function generate_helloworld_v1_example { local proto="$here/upstream/grpc/examples/helloworld.proto" local output="$root/Sources/Examples/v1/HelloWorld/Model" @@ -104,6 +104,14 @@ function generate_helloworld_example { generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" } +function generate_helloworld_v2_example { + local proto="$here/upstream/grpc/examples/helloworld.proto" + local output="$root/Sources/Examples/v2/hello-world/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" +} + function generate_reflection_service { local proto_v1="$here/upstream/grpc/reflection/v1/reflection.proto" local output_v1="$root/Sources/GRPCReflectionService/v1" @@ -254,7 +262,8 @@ function generate_health_service { generate_echo_v1_example generate_echo_v2_example generate_routeguide_example -generate_helloworld_example +generate_helloworld_v1_example +generate_helloworld_v2_example generate_reflection_data_example # Reflection service and tests diff --git a/Sources/Examples/v2/Echo/Echo.swift b/Sources/Examples/v2/echo/Echo.swift similarity index 100% rename from Sources/Examples/v2/Echo/Echo.swift rename to Sources/Examples/v2/echo/Echo.swift diff --git a/Sources/Examples/v2/Echo/Generated/echo.grpc.swift b/Sources/Examples/v2/echo/Generated/echo.grpc.swift similarity index 100% rename from Sources/Examples/v2/Echo/Generated/echo.grpc.swift rename to Sources/Examples/v2/echo/Generated/echo.grpc.swift diff --git a/Sources/Examples/v2/Echo/Generated/echo.pb.swift b/Sources/Examples/v2/echo/Generated/echo.pb.swift similarity index 100% rename from Sources/Examples/v2/Echo/Generated/echo.pb.swift rename to Sources/Examples/v2/echo/Generated/echo.pb.swift diff --git a/Sources/Examples/v2/Echo/Subcommands/ClientArguments.swift b/Sources/Examples/v2/echo/Subcommands/ClientArguments.swift similarity index 100% rename from Sources/Examples/v2/Echo/Subcommands/ClientArguments.swift rename to Sources/Examples/v2/echo/Subcommands/ClientArguments.swift diff --git a/Sources/Examples/v2/Echo/Subcommands/Collect.swift b/Sources/Examples/v2/echo/Subcommands/Collect.swift similarity index 100% rename from Sources/Examples/v2/Echo/Subcommands/Collect.swift rename to Sources/Examples/v2/echo/Subcommands/Collect.swift diff --git a/Sources/Examples/v2/Echo/Subcommands/Expand.swift b/Sources/Examples/v2/echo/Subcommands/Expand.swift similarity index 100% rename from Sources/Examples/v2/Echo/Subcommands/Expand.swift rename to Sources/Examples/v2/echo/Subcommands/Expand.swift diff --git a/Sources/Examples/v2/Echo/Subcommands/Get.swift b/Sources/Examples/v2/echo/Subcommands/Get.swift similarity index 100% rename from Sources/Examples/v2/Echo/Subcommands/Get.swift rename to Sources/Examples/v2/echo/Subcommands/Get.swift diff --git a/Sources/Examples/v2/Echo/Subcommands/Serve.swift b/Sources/Examples/v2/echo/Subcommands/Serve.swift similarity index 100% rename from Sources/Examples/v2/Echo/Subcommands/Serve.swift rename to Sources/Examples/v2/echo/Subcommands/Serve.swift diff --git a/Sources/Examples/v2/Echo/Subcommands/Update.swift b/Sources/Examples/v2/echo/Subcommands/Update.swift similarity index 100% rename from Sources/Examples/v2/Echo/Subcommands/Update.swift rename to Sources/Examples/v2/echo/Subcommands/Update.swift diff --git a/Sources/Examples/v2/hello-world/Generated/helloworld.grpc.swift b/Sources/Examples/v2/hello-world/Generated/helloworld.grpc.swift new file mode 100644 index 000000000..319c5e65e --- /dev/null +++ b/Sources/Examples/v2/hello-world/Generated/helloworld.grpc.swift @@ -0,0 +1,181 @@ +/// Copyright 2015 gRPC authors. +/// +/// 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: helloworld.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +internal import GRPCCore +internal import GRPCProtobuf + +internal enum Helloworld_Greeter { + internal static let descriptor = GRPCCore.ServiceDescriptor.helloworld_Greeter + internal enum Method { + internal enum SayHello { + internal typealias Input = Helloworld_HelloRequest + internal typealias Output = Helloworld_HelloReply + internal static let descriptor = GRPCCore.MethodDescriptor( + service: Helloworld_Greeter.descriptor.fullyQualifiedService, + method: "SayHello" + ) + } + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ + SayHello.descriptor + ] + } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias ClientProtocol = Helloworld_GreeterClientProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias Client = Helloworld_GreeterClient +} + +extension GRPCCore.ServiceDescriptor { + internal static let helloworld_Greeter = Self( + package: "helloworld", + service: "Greeter" + ) +} + +/// The greeting service definition. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Sends a greeting + func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Helloworld_Greeter.StreamingServiceProtocol { + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Helloworld_Greeter.Method.SayHello.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request in + try await self.sayHello(request: request) + } + ) + } +} + +/// The greeting service definition. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { + /// Sends a greeting + func sayHello(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single +} + +/// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Helloworld_Greeter.ServiceProtocol { + internal func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.sayHello(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) + } +} + +/// The greeting service definition. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal protocol Helloworld_GreeterClientProtocol: Sendable { + /// Sends a greeting + func sayHello( + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Helloworld_Greeter.ClientProtocol { + internal func sayHello( + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + try $0.message + } + ) async throws -> R where R: Sendable { + try await self.sayHello( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + body + ) + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Helloworld_Greeter.ClientProtocol { + /// Sends a greeting + internal func sayHello( + _ message: Helloworld_HelloRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.sayHello( + request: request, + options: options, + handleResponse + ) + } +} + +/// The greeting service definition. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal struct Helloworld_GreeterClient: Helloworld_Greeter.ClientProtocol { + private let client: GRPCCore.GRPCClient + + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Sends a greeting + internal func sayHello( + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + try $0.message + } + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Helloworld_Greeter.Method.SayHello.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } +} \ No newline at end of file diff --git a/Sources/Examples/v2/hello-world/Generated/helloworld.pb.swift b/Sources/Examples/v2/hello-world/Generated/helloworld.pb.swift new file mode 100644 index 000000000..cceb2c743 --- /dev/null +++ b/Sources/Examples/v2/hello-world/Generated/helloworld.pb.swift @@ -0,0 +1,129 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: helloworld.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +/// Copyright 2015 gRPC authors. +/// +/// 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 Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The request message containing the user's name. +struct Helloworld_HelloRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The response message containing the greetings +struct Helloworld_HelloReply: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "helloworld" + +extension Helloworld_HelloRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HelloRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Helloworld_HelloRequest, rhs: Helloworld_HelloRequest) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Helloworld_HelloReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HelloReply" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Helloworld_HelloReply, rhs: Helloworld_HelloReply) -> Bool { + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/Examples/v2/hello-world/HelloWorld.swift b/Sources/Examples/v2/hello-world/HelloWorld.swift new file mode 100644 index 000000000..1427fecd7 --- /dev/null +++ b/Sources/Examples/v2/hello-world/HelloWorld.swift @@ -0,0 +1,27 @@ +/* + * 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. + */ + +internal import ArgumentParser + +@main +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct HelloWorld: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "hello-world", + abstract: "A multi-tool to run a greeter server and execute RPCs against it.", + subcommands: [Serve.self, Greet.self] + ) +} diff --git a/Sources/Examples/v2/hello-world/Subcommands/Greet.swift b/Sources/Examples/v2/hello-world/Subcommands/Greet.swift new file mode 100644 index 000000000..2e12bda8a --- /dev/null +++ b/Sources/Examples/v2/hello-world/Subcommands/Greet.swift @@ -0,0 +1,49 @@ +/* + * 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. + */ + +internal import ArgumentParser +internal import GRPCHTTP2Transport +internal import GRPCProtobuf + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct Greet: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Sends a request to the greeter server") + + @Option(help: "The port to listen on") + var port: Int = 31415 + + @Option(help: "The person to greet") + var name: String = "" + + func run() async throws { + try await withThrowingDiscardingTaskGroup { group in + let http2 = try HTTP2ClientTransport.Posix(target: .ipv4(host: "127.0.0.1", port: self.port)) + let client = GRPCClient(transport: http2) + + group.addTask { + try await client.run() + } + + defer { + client.close() + } + + let greeter = Helloworld_GreeterClient(wrapping: client) + let reply = try await greeter.sayHello(.with { $0.name = self.name }) + print(reply.message) + } + } +} diff --git a/Sources/Examples/v2/hello-world/Subcommands/Serve.swift b/Sources/Examples/v2/hello-world/Subcommands/Serve.swift new file mode 100644 index 000000000..210989102 --- /dev/null +++ b/Sources/Examples/v2/hello-world/Subcommands/Serve.swift @@ -0,0 +1,54 @@ +/* + * 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. + */ + +internal import ArgumentParser +internal import GRPCHTTP2Transport +internal import GRPCProtobuf + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct Serve: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Starts a greeter server.") + + @Option(help: "The port to listen on") + var port: Int = 31415 + + func run() async throws { + let http2 = HTTP2ServerTransport.Posix( + address: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults(transportSecurity: .plaintext) + ) + + let server = GRPCServer(transport: http2, services: [Greeter()]) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await server.run() } + let address = try await http2.listeningAddress + print("Greeter listening on \(address)") + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct Greeter: Helloworld_GreeterServiceProtocol { + func sayHello( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + var reply = Helloworld_HelloReply() + let recipient = request.message.name.isEmpty ? "stranger" : request.message.name + reply.message = "Hello, \(recipient)" + return ServerResponse.Single(message: reply) + } +} From bef2e5c104f2fa8a377fed0bd9d7affdacaa2521 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 19 Aug 2024 14:30:37 +0100 Subject: [PATCH 434/580] Don't police "te: trailers" at the grpc layer (#2022) Motivation: "te: trailers" is required in the request to detect incompatible proxies. However, "te" is a hop-by-hop header so isn't meant to be forwarded, as such the only validation the server should do is to check whether the value is "trailers" iff "te" is present. This doesn't need to be done at the gRPC layer because NIOHTTP2 already does this validation. Modifications: - Remove the check for "te: trailers" Result: Fewer bugs --- .../GRPCStreamStateMachine.swift | 10 ----- .../GRPCStreamStateMachineTests.swift | 42 ++++--------------- .../Server/GRPCServerStreamHandlerTests.swift | 39 ----------------- 3 files changed, 8 insertions(+), 83 deletions(-) diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index b9a08862e..be43c0240 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -1456,16 +1456,6 @@ extension GRPCStreamStateMachine { ) } - guard let te = headers.firstString(forKey: .te), te == "trailers" else { - self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC_serverOnly( - trailers: .trailersOnly( - code: .invalidArgument, - message: "\"te\" header is expected to be present and have a value of \"trailers\"." - ) - ) - } - // Firstly, find out if we support the client's chosen encoding, and reject // the RPC if we don't. let inboundEncoding: CompressionAlgorithm diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 5bec991a3..61e2354f4 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -1843,40 +1843,14 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { endStream: false ) - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "3", - "grpc-message": - "\"te\" header is expected to be present and have a value of \"trailers\".", - ] - ) - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidTE() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithInvalidTE, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "3", - "grpc-message": - "\"te\" header is expected to be present and have a value of \"trailers\".", - ] - ) - } + let metadata: Metadata = [ + ":path": "/test/test", + ":scheme": "http", + ":method": "POST", + "content-type": "application/grpc", + ] + let descriptor = MethodDescriptor(service: "test", method: "test") + XCTAssertEqual(action, .receivedMetadata(metadata, descriptor)) } func testReceiveMetadataWhenClientIdleAndServerIdle_MissingMethod() throws { diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index e7e3a7ad2..deb610673 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -201,45 +201,6 @@ final class GRPCServerStreamHandlerTests: XCTestCase { XCTAssertTrue(writtenTrailersOnlyResponse.endStream) } - func testClientInitialMetadataWithoutTEResultsInRejectedRPC() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maximumPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata without TE - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we have sent a trailers-only response - let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenTrailersOnlyResponse.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: - "\"te\" header is expected to be present and have a value of \"trailers\".", - ] - ) - XCTAssertTrue(writtenTrailersOnlyResponse.endStream) - } - func testNotAcceptedEncodingResultsInRejectedRPC() throws { let channel = EmbeddedChannel() let handler = GRPCServerStreamHandler( From 431d8078ca1b5122c21343677ca3b10b23815df3 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Aug 2024 09:51:51 +0100 Subject: [PATCH 435/580] Move examples to top-level (#2024) Motivation: There are two examples directories: one at the top-level and one buried in Sources. The top-level directory contains examples which can't be built with SwiftPM and are more or less unmaintained. The harder-to-find examples within Sources are built by SwiftPM and are kept up-to-date. Examples should be easy to find, and the easy to find examples should be the maintained ones. Modifications: - Remove the top-level unmaintained examples - Move the examples from Sources to the top-level - Update docs and various links Results: - Only maintained examples are kept - Examples are easier to find --- .../EchoWeb/Generated/echo_grpc_web_pb.js | 181 - Examples/EchoWeb/Generated/echo_pb.js | 300 -- Examples/EchoWeb/Makefile | 9 - Examples/EchoWeb/README.md | 17 - Examples/EchoWeb/client.js | 33 - Examples/EchoWeb/index.html | 17 - Examples/EchoWeb/package.json | 13 - Examples/Google/NaturalLanguage/Makefile | 9 - .../Google/NaturalLanguage/Package.resolved | 115 - Examples/Google/NaturalLanguage/Package.swift | 37 - Examples/Google/NaturalLanguage/README.md | 17 - Examples/Google/NaturalLanguage/RUNME | 32 - .../Sources/annotations.pb.swift | 84 - .../NaturalLanguage/Sources/http.pb.swift | 730 --- .../Sources/language_service.grpc.swift | 845 ---- .../Sources/language_service.pb.swift | 4261 ----------------- .../Google/NaturalLanguage/Sources/main.swift | 124 - Examples/Google/README.md | 40 - Examples/Google/SpeechToText/.gitignore | 4 - .../Google/SpeechToText/Images/Resized.gif | Bin 399857 -> 0 bytes Examples/Google/SpeechToText/Makefile | 36 - Examples/Google/SpeechToText/README.md | 34 - .../Sources/AudioStreamManager.swift | 218 - .../AppIcon.appiconset/1024.png | Bin 78397 -> 0 bytes .../AppIcon.appiconset/114.png | Bin 6106 -> 0 bytes .../AppIcon.appiconset/120.png | Bin 5304 -> 0 bytes .../AppIcon.appiconset/180.png | Bin 8944 -> 0 bytes .../Assets.xcassets/AppIcon.appiconset/29.png | Bin 1618 -> 0 bytes .../Assets.xcassets/AppIcon.appiconset/40.png | Bin 2049 -> 0 bytes .../Assets.xcassets/AppIcon.appiconset/57.png | Bin 2938 -> 0 bytes .../Assets.xcassets/AppIcon.appiconset/58.png | Bin 2975 -> 0 bytes .../Assets.xcassets/AppIcon.appiconset/60.png | Bin 2887 -> 0 bytes .../Assets.xcassets/AppIcon.appiconset/80.png | Bin 3795 -> 0 bytes .../Assets.xcassets/AppIcon.appiconset/87.png | Bin 4303 -> 0 bytes .../AppIcon.appiconset/Contents.json | 80 - .../Assets.xcassets/Contents.json | 6 - .../Sources/Configuration/Info.plist | 64 - .../SpeechToText/Sources/Constants.swift | 26 - .../Sources/Launch/AppDelegate.swift | 45 - .../Launch/Base.lproj/LaunchScreen.storyboard | 25 - .../Sources/Launch/SceneDelegate.swift | 73 - .../SpeechToText/Sources/SpeechService.swift | 136 - .../SpeechToText/Sources/ViewController.swift | 192 - .../project.pbxproj | 451 -- .../xcschemes/SpeechToText-gRPC-iOS.xcscheme | 78 - .../common/include/google/protobuf/any.proto | 149 - .../common/include/google/protobuf/api.proto | 210 - .../google/protobuf/compiler/plugin.proto | 167 - .../include/google/protobuf/descriptor.proto | 849 ---- .../include/google/protobuf/duration.proto | 117 - .../include/google/protobuf/empty.proto | 52 - .../include/google/protobuf/field_mask.proto | 246 - .../google/protobuf/source_context.proto | 48 - .../include/google/protobuf/struct.proto | 96 - .../include/google/protobuf/timestamp.proto | 133 - .../common/include/google/protobuf/type.proto | 187 - .../include/google/protobuf/wrappers.proto | 118 - Examples/README.md | 16 - .../Implementation/EchoAsyncProvider.swift | 0 .../v1/Echo/Implementation/EchoProvider.swift | 0 .../HPACKHeaders+Prettify.swift | 0 .../v1/Echo/Implementation/Interceptors.swift | 0 .../v1/Echo/Model/echo.grpc.swift | 0 .../v1/Echo/Model/echo.pb.swift | 0 .../Examples => Examples}/v1/Echo/README.md | 0 .../v1/Echo/Runtime/Echo.swift | 0 .../v1/Echo/Runtime/Empty.swift | 0 .../HelloWorld/Client/HelloWorldClient.swift | 0 .../v1/HelloWorld/Model/helloworld.grpc.swift | 0 .../v1/HelloWorld/Model/helloworld.pb.swift | 0 .../v1/HelloWorld/README.md | 0 .../HelloWorld/Server/GreeterProvider.swift | 0 .../HelloWorld/Server/HelloWorldServer.swift | 0 .../v1/PacketCapture/Empty.swift | 0 .../v1/PacketCapture/PacketCapture.swift | 0 .../v1/PacketCapture/README.md | 0 {Sources/Examples => Examples}/v1/README.md | 0 .../Generated/echo.grpc.reflection | 0 .../Generated/helloworld.grpc.reflection | 0 .../ReflectionService/GreeterProvider.swift | 0 .../ReflectionService/ReflectionServer.swift | 0 .../v1/RouteGuide/Client/Empty.swift | 0 .../RouteGuide/Client/RouteGuideClient.swift | 0 .../RouteGuide/Model/route_guide.grpc.swift | 0 .../v1/RouteGuide/Model/route_guide.pb.swift | 0 .../v1/RouteGuide/README.md | 0 .../Server/RouteGuideProvider.swift | 0 .../RouteGuide/Server/RouteGuideServer.swift | 0 .../v1/RouteGuide/route_guide_db.json | 0 .../Examples => Examples}/v2/echo/Echo.swift | 0 .../v2/echo/Generated/echo.grpc.swift | 0 .../v2/echo/Generated/echo.pb.swift | 0 .../v2/echo/Subcommands/ClientArguments.swift | 0 .../v2/echo/Subcommands/Collect.swift | 0 .../v2/echo/Subcommands/Expand.swift | 0 .../v2/echo/Subcommands/Get.swift | 0 .../v2/echo/Subcommands/Serve.swift | 0 .../v2/echo/Subcommands/Update.swift | 0 .../Generated/helloworld.grpc.swift | 0 .../hello-world/Generated/helloworld.pb.swift | 0 .../v2/hello-world/HelloWorld.swift | 0 .../v2/hello-world/Subcommands/Greet.swift | 0 .../v2/hello-world/Subcommands/Serve.swift | 0 FuzzTesting/Sources/EchoImplementation | 2 +- FuzzTesting/Sources/EchoModel | 2 +- Package.swift | 22 +- Package@swift-6.swift | 26 +- Protos/generate.sh | 12 +- Sources/GRPC/Docs.docc/index.md | 15 +- .../Benchmarks/echo.grpc.swift | 2 +- .../Benchmarks/echo.pb.swift | 2 +- .../ReflectionServiceTutorial.md | 18 +- dev/codegen-tests/01-echo/proto/echo.proto | 2 +- docs/basic-tutorial.md | 24 +- docs/client-without-codegen.md | 2 +- docs/interceptors-tutorial.md | 2 +- docs/quick-start.md | 6 +- 117 files changed, 65 insertions(+), 10792 deletions(-) delete mode 100644 Examples/EchoWeb/Generated/echo_grpc_web_pb.js delete mode 100644 Examples/EchoWeb/Generated/echo_pb.js delete mode 100644 Examples/EchoWeb/Makefile delete mode 100644 Examples/EchoWeb/README.md delete mode 100644 Examples/EchoWeb/client.js delete mode 100644 Examples/EchoWeb/index.html delete mode 100644 Examples/EchoWeb/package.json delete mode 100644 Examples/Google/NaturalLanguage/Makefile delete mode 100644 Examples/Google/NaturalLanguage/Package.resolved delete mode 100644 Examples/Google/NaturalLanguage/Package.swift delete mode 100644 Examples/Google/NaturalLanguage/README.md delete mode 100755 Examples/Google/NaturalLanguage/RUNME delete mode 100644 Examples/Google/NaturalLanguage/Sources/annotations.pb.swift delete mode 100644 Examples/Google/NaturalLanguage/Sources/http.pb.swift delete mode 100644 Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift delete mode 100644 Examples/Google/NaturalLanguage/Sources/language_service.pb.swift delete mode 100644 Examples/Google/NaturalLanguage/Sources/main.swift delete mode 100644 Examples/Google/README.md delete mode 100644 Examples/Google/SpeechToText/.gitignore delete mode 100644 Examples/Google/SpeechToText/Images/Resized.gif delete mode 100644 Examples/Google/SpeechToText/Makefile delete mode 100644 Examples/Google/SpeechToText/README.md delete mode 100644 Examples/Google/SpeechToText/Sources/AudioStreamManager.swift delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/1024.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/114.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/120.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/180.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/29.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/40.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/57.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/58.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/60.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/80.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/87.png delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/Contents.json delete mode 100644 Examples/Google/SpeechToText/Sources/Configuration/Info.plist delete mode 100644 Examples/Google/SpeechToText/Sources/Constants.swift delete mode 100644 Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift delete mode 100644 Examples/Google/SpeechToText/Sources/Launch/Base.lproj/LaunchScreen.storyboard delete mode 100644 Examples/Google/SpeechToText/Sources/Launch/SceneDelegate.swift delete mode 100644 Examples/Google/SpeechToText/Sources/SpeechService.swift delete mode 100644 Examples/Google/SpeechToText/Sources/ViewController.swift delete mode 100644 Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/project.pbxproj delete mode 100644 Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/xcshareddata/xcschemes/SpeechToText-gRPC-iOS.xcscheme delete mode 100755 Examples/Google/common/include/google/protobuf/any.proto delete mode 100755 Examples/Google/common/include/google/protobuf/api.proto delete mode 100755 Examples/Google/common/include/google/protobuf/compiler/plugin.proto delete mode 100755 Examples/Google/common/include/google/protobuf/descriptor.proto delete mode 100755 Examples/Google/common/include/google/protobuf/duration.proto delete mode 100755 Examples/Google/common/include/google/protobuf/empty.proto delete mode 100755 Examples/Google/common/include/google/protobuf/field_mask.proto delete mode 100755 Examples/Google/common/include/google/protobuf/source_context.proto delete mode 100755 Examples/Google/common/include/google/protobuf/struct.proto delete mode 100755 Examples/Google/common/include/google/protobuf/timestamp.proto delete mode 100755 Examples/Google/common/include/google/protobuf/type.proto delete mode 100755 Examples/Google/common/include/google/protobuf/wrappers.proto delete mode 100644 Examples/README.md rename {Sources/Examples => Examples}/v1/Echo/Implementation/EchoAsyncProvider.swift (100%) rename {Sources/Examples => Examples}/v1/Echo/Implementation/EchoProvider.swift (100%) rename {Sources/Examples => Examples}/v1/Echo/Implementation/HPACKHeaders+Prettify.swift (100%) rename {Sources/Examples => Examples}/v1/Echo/Implementation/Interceptors.swift (100%) rename {Sources/Examples => Examples}/v1/Echo/Model/echo.grpc.swift (100%) rename {Sources/Examples => Examples}/v1/Echo/Model/echo.pb.swift (100%) rename {Sources/Examples => Examples}/v1/Echo/README.md (100%) rename {Sources/Examples => Examples}/v1/Echo/Runtime/Echo.swift (100%) rename {Sources/Examples => Examples}/v1/Echo/Runtime/Empty.swift (100%) rename {Sources/Examples => Examples}/v1/HelloWorld/Client/HelloWorldClient.swift (100%) rename {Sources/Examples => Examples}/v1/HelloWorld/Model/helloworld.grpc.swift (100%) rename {Sources/Examples => Examples}/v1/HelloWorld/Model/helloworld.pb.swift (100%) rename {Sources/Examples => Examples}/v1/HelloWorld/README.md (100%) rename {Sources/Examples => Examples}/v1/HelloWorld/Server/GreeterProvider.swift (100%) rename {Sources/Examples => Examples}/v1/HelloWorld/Server/HelloWorldServer.swift (100%) rename {Sources/Examples => Examples}/v1/PacketCapture/Empty.swift (100%) rename {Sources/Examples => Examples}/v1/PacketCapture/PacketCapture.swift (100%) rename {Sources/Examples => Examples}/v1/PacketCapture/README.md (100%) rename {Sources/Examples => Examples}/v1/README.md (100%) rename {Sources/Examples => Examples}/v1/ReflectionService/Generated/echo.grpc.reflection (100%) rename {Sources/Examples => Examples}/v1/ReflectionService/Generated/helloworld.grpc.reflection (100%) rename {Sources/Examples => Examples}/v1/ReflectionService/GreeterProvider.swift (100%) rename {Sources/Examples => Examples}/v1/ReflectionService/ReflectionServer.swift (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/Client/Empty.swift (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/Client/RouteGuideClient.swift (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/Model/route_guide.grpc.swift (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/Model/route_guide.pb.swift (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/README.md (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/Server/RouteGuideProvider.swift (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/Server/RouteGuideServer.swift (100%) rename {Sources/Examples => Examples}/v1/RouteGuide/route_guide_db.json (100%) rename {Sources/Examples => Examples}/v2/echo/Echo.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Generated/echo.grpc.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Generated/echo.pb.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Subcommands/ClientArguments.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Subcommands/Collect.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Subcommands/Expand.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Subcommands/Get.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Subcommands/Serve.swift (100%) rename {Sources/Examples => Examples}/v2/echo/Subcommands/Update.swift (100%) rename {Sources/Examples => Examples}/v2/hello-world/Generated/helloworld.grpc.swift (100%) rename {Sources/Examples => Examples}/v2/hello-world/Generated/helloworld.pb.swift (100%) rename {Sources/Examples => Examples}/v2/hello-world/HelloWorld.swift (100%) rename {Sources/Examples => Examples}/v2/hello-world/Subcommands/Greet.swift (100%) rename {Sources/Examples => Examples}/v2/hello-world/Subcommands/Serve.swift (100%) diff --git a/Examples/EchoWeb/Generated/echo_grpc_web_pb.js b/Examples/EchoWeb/Generated/echo_grpc_web_pb.js deleted file mode 100644 index 0b7363b08..000000000 --- a/Examples/EchoWeb/Generated/echo_grpc_web_pb.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * @fileoverview gRPC-Web generated client stub for echo - * @enhanceable - * @public - */ - -// GENERATED CODE -- DO NOT EDIT! - - - -const grpc = {}; -grpc.web = require('grpc-web'); - -const proto = {}; -proto.echo = require('./echo_pb.js'); - -/** - * @param {string} hostname - * @param {?Object} credentials - * @param {?Object} options - * @constructor - * @struct - * @final - */ -proto.echo.EchoClient = - function(hostname, credentials, options) { - if (!options) options = {}; - options['format'] = 'text'; - - /** - * @private @const {!grpc.web.GrpcWebClientBase} The client - */ - this.client_ = new grpc.web.GrpcWebClientBase(options); - - /** - * @private @const {string} The hostname - */ - this.hostname_ = hostname; - - /** - * @private @const {?Object} The credentials to be used to connect - * to the server - */ - this.credentials_ = credentials; - - /** - * @private @const {?Object} Options for the client - */ - this.options_ = options; -}; - - -/** - * @param {string} hostname - * @param {?Object} credentials - * @param {?Object} options - * @constructor - * @struct - * @final - */ -proto.echo.EchoPromiseClient = - function(hostname, credentials, options) { - if (!options) options = {}; - options['format'] = 'text'; - - /** - * @private @const {!proto.echo.EchoClient} The delegate callback based client - */ - this.delegateClient_ = new proto.echo.EchoClient( - hostname, credentials, options); - -}; - - -/** - * @const - * @type {!grpc.web.AbstractClientBase.MethodInfo< - * !proto.echo.EchoRequest, - * !proto.echo.EchoResponse>} - */ -const methodInfo_Echo_Get = new grpc.web.AbstractClientBase.MethodInfo( - proto.echo.EchoResponse, - /** @param {!proto.echo.EchoRequest} request */ - function(request) { - return request.serializeBinary(); - }, - proto.echo.EchoResponse.deserializeBinary -); - - -/** - * @param {!proto.echo.EchoRequest} request The - * request proto - * @param {!Object} metadata User defined - * call metadata - * @param {function(?grpc.web.Error, ?proto.echo.EchoResponse)} - * callback The callback function(error, response) - * @return {!grpc.web.ClientReadableStream|undefined} - * The XHR Node Readable Stream - */ -proto.echo.EchoClient.prototype.get = - function(request, metadata, callback) { - return this.client_.rpcCall(this.hostname_ + - '/echo.Echo/Get', - request, - metadata, - methodInfo_Echo_Get, - callback); -}; - - -/** - * @param {!proto.echo.EchoRequest} request The - * request proto - * @param {!Object} metadata User defined - * call metadata - * @return {!Promise} - * The XHR Node Readable Stream - */ -proto.echo.EchoPromiseClient.prototype.get = - function(request, metadata) { - return new Promise((resolve, reject) => { - this.delegateClient_.get( - request, metadata, (error, response) => { - error ? reject(error) : resolve(response); - }); - }); -}; - - -/** - * @const - * @type {!grpc.web.AbstractClientBase.MethodInfo< - * !proto.echo.EchoRequest, - * !proto.echo.EchoResponse>} - */ -const methodInfo_Echo_Expand = new grpc.web.AbstractClientBase.MethodInfo( - proto.echo.EchoResponse, - /** @param {!proto.echo.EchoRequest} request */ - function(request) { - return request.serializeBinary(); - }, - proto.echo.EchoResponse.deserializeBinary -); - - -/** - * @param {!proto.echo.EchoRequest} request The request proto - * @param {!Object} metadata User defined - * call metadata - * @return {!grpc.web.ClientReadableStream} - * The XHR Node Readable Stream - */ -proto.echo.EchoClient.prototype.expand = - function(request, metadata) { - return this.client_.serverStreaming(this.hostname_ + - '/echo.Echo/Expand', - request, - metadata, - methodInfo_Echo_Expand); -}; - - -/** - * @param {!proto.echo.EchoRequest} request The request proto - * @param {!Object} metadata User defined - * call metadata - * @return {!grpc.web.ClientReadableStream} - * The XHR Node Readable Stream - */ -proto.echo.EchoPromiseClient.prototype.expand = - function(request, metadata) { - return this.delegateClient_.client_.serverStreaming(this.delegateClient_.hostname_ + - '/echo.Echo/Expand', - request, - metadata, - methodInfo_Echo_Expand); -}; - - -module.exports = proto.echo; diff --git a/Examples/EchoWeb/Generated/echo_pb.js b/Examples/EchoWeb/Generated/echo_pb.js deleted file mode 100644 index 24928394f..000000000 --- a/Examples/EchoWeb/Generated/echo_pb.js +++ /dev/null @@ -1,300 +0,0 @@ -/** - * @fileoverview - * @enhanceable - * @suppress {messageConventions} JS Compiler reports an error if a variable or - * field starts with 'MSG_' and isn't a translatable message. - * @public - */ -// GENERATED CODE -- DO NOT EDIT! - -var jspb = require('google-protobuf'); -var goog = jspb; -var global = Function('return this')(); - -goog.exportSymbol('proto.echo.EchoRequest', null, global); -goog.exportSymbol('proto.echo.EchoResponse', null, global); - -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.echo.EchoRequest = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); -}; -goog.inherits(proto.echo.EchoRequest, jspb.Message); -if (goog.DEBUG && !COMPILED) { - proto.echo.EchoRequest.displayName = 'proto.echo.EchoRequest'; -} - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto suitable for use in Soy templates. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. - * @param {boolean=} opt_includeInstance Whether to include the JSPB instance - * for transitional soy proto support: http://goto/soy-param-migration - * @return {!Object} - */ -proto.echo.EchoRequest.prototype.toObject = function(opt_includeInstance) { - return proto.echo.EchoRequest.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Whether to include the JSPB - * instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.echo.EchoRequest} msg The msg instance to transform. - * @return {!Object} - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.echo.EchoRequest.toObject = function(includeInstance, msg) { - var f, obj = { - text: jspb.Message.getFieldWithDefault(msg, 1, "") - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.echo.EchoRequest} - */ -proto.echo.EchoRequest.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.echo.EchoRequest; - return proto.echo.EchoRequest.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.echo.EchoRequest} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.echo.EchoRequest} - */ -proto.echo.EchoRequest.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setText(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.echo.EchoRequest.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.echo.EchoRequest.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.echo.EchoRequest} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.echo.EchoRequest.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getText(); - if (f.length > 0) { - writer.writeString( - 1, - f - ); - } -}; - - -/** - * optional string text = 1; - * @return {string} - */ -proto.echo.EchoRequest.prototype.getText = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** @param {string} value */ -proto.echo.EchoRequest.prototype.setText = function(value) { - jspb.Message.setProto3StringField(this, 1, value); -}; - - - -/** - * Generated by JsPbCodeGenerator. - * @param {Array=} opt_data Optional initial data array, typically from a - * server response, or constructed directly in Javascript. The array is used - * in place and becomes part of the constructed object. It is not cloned. - * If no data is provided, the constructed object will be empty, but still - * valid. - * @extends {jspb.Message} - * @constructor - */ -proto.echo.EchoResponse = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); -}; -goog.inherits(proto.echo.EchoResponse, jspb.Message); -if (goog.DEBUG && !COMPILED) { - proto.echo.EchoResponse.displayName = 'proto.echo.EchoResponse'; -} - - -if (jspb.Message.GENERATE_TO_OBJECT) { -/** - * Creates an object representation of this proto suitable for use in Soy templates. - * Field names that are reserved in JavaScript and will be renamed to pb_name. - * To access a reserved field use, foo.pb_, eg, foo.pb_default. - * For the list of reserved names please see: - * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. - * @param {boolean=} opt_includeInstance Whether to include the JSPB instance - * for transitional soy proto support: http://goto/soy-param-migration - * @return {!Object} - */ -proto.echo.EchoResponse.prototype.toObject = function(opt_includeInstance) { - return proto.echo.EchoResponse.toObject(opt_includeInstance, this); -}; - - -/** - * Static version of the {@see toObject} method. - * @param {boolean|undefined} includeInstance Whether to include the JSPB - * instance for transitional soy proto support: - * http://goto/soy-param-migration - * @param {!proto.echo.EchoResponse} msg The msg instance to transform. - * @return {!Object} - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.echo.EchoResponse.toObject = function(includeInstance, msg) { - var f, obj = { - text: jspb.Message.getFieldWithDefault(msg, 1, "") - }; - - if (includeInstance) { - obj.$jspbMessageInstance = msg; - } - return obj; -}; -} - - -/** - * Deserializes binary data (in protobuf wire format). - * @param {jspb.ByteSource} bytes The bytes to deserialize. - * @return {!proto.echo.EchoResponse} - */ -proto.echo.EchoResponse.deserializeBinary = function(bytes) { - var reader = new jspb.BinaryReader(bytes); - var msg = new proto.echo.EchoResponse; - return proto.echo.EchoResponse.deserializeBinaryFromReader(msg, reader); -}; - - -/** - * Deserializes binary data (in protobuf wire format) from the - * given reader into the given message object. - * @param {!proto.echo.EchoResponse} msg The message object to deserialize into. - * @param {!jspb.BinaryReader} reader The BinaryReader to use. - * @return {!proto.echo.EchoResponse} - */ -proto.echo.EchoResponse.deserializeBinaryFromReader = function(msg, reader) { - while (reader.nextField()) { - if (reader.isEndGroup()) { - break; - } - var field = reader.getFieldNumber(); - switch (field) { - case 1: - var value = /** @type {string} */ (reader.readString()); - msg.setText(value); - break; - default: - reader.skipField(); - break; - } - } - return msg; -}; - - -/** - * Serializes the message to binary data (in protobuf wire format). - * @return {!Uint8Array} - */ -proto.echo.EchoResponse.prototype.serializeBinary = function() { - var writer = new jspb.BinaryWriter(); - proto.echo.EchoResponse.serializeBinaryToWriter(this, writer); - return writer.getResultBuffer(); -}; - - -/** - * Serializes the given message to binary data (in protobuf wire - * format), writing to the given BinaryWriter. - * @param {!proto.echo.EchoResponse} message - * @param {!jspb.BinaryWriter} writer - * @suppress {unusedLocalVariables} f is only used for nested messages - */ -proto.echo.EchoResponse.serializeBinaryToWriter = function(message, writer) { - var f = undefined; - f = message.getText(); - if (f.length > 0) { - writer.writeString( - 1, - f - ); - } -}; - - -/** - * optional string text = 1; - * @return {string} - */ -proto.echo.EchoResponse.prototype.getText = function() { - return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); -}; - - -/** @param {string} value */ -proto.echo.EchoResponse.prototype.setText = function(value) { - jspb.Message.setProto3StringField(this, 1, value); -}; - - -goog.object.extend(exports, proto.echo); diff --git a/Examples/EchoWeb/Makefile b/Examples/EchoWeb/Makefile deleted file mode 100644 index d8c7ce76b..000000000 --- a/Examples/EchoWeb/Makefile +++ /dev/null @@ -1,9 +0,0 @@ - -all: - npm install - npx webpack client.js - -clean: - rm -rf Packages googleapis .build - rm -f Package.pins Echo google.json - rm -rf Package.resolved Echo.xcodeproj Echo diff --git a/Examples/EchoWeb/README.md b/Examples/EchoWeb/README.md deleted file mode 100644 index 68e010463..000000000 --- a/Examples/EchoWeb/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Echo gRPC-Web Sample App - -The Echo gRPC-Web is a node project that creates a website that -connects to a gRPC Swift server to display messages. To build -it, just run `make` inside this directory, and then open the -`index.html` file in a web browser. Remember to start the Echo -service by executing `swift run Echo server 8080` from the package -root before opening `index.html` in the browser. - -The proto files were generated by invoking `protoc` with the -`protoc-gen-grpc-web` plugin as described -[here](https://github.com/grpc/grpc-web/tree/master/net/grpc/gateway/examples/helloworld#generate-protobuf-messages-and-client-service-stub). - -## Dependencies - -You'll need to install `npm` in order to compile the Javascript -code. diff --git a/Examples/EchoWeb/client.js b/Examples/EchoWeb/client.js deleted file mode 100644 index d0fcb0a15..000000000 --- a/Examples/EchoWeb/client.js +++ /dev/null @@ -1,33 +0,0 @@ -const {EchoRequest, EchoResponse} = require('./Generated/echo_pb.js'); -const {EchoClient} = require('./Generated/echo_grpc_web_pb.js'); - -var client = new EchoClient('http://localhost:8080'); - -function sendMessage(message) { - var request = new EchoRequest(); - request.setText(message); - - client.get(request, {}, (err, response) => { - var responseLabel = document.getElementById("response_label") - if (err) { - responseLabel.innerText = "ERROR: Could not connect to the server." - } else { - responseLabel.innerText = "Server reply: " + response.getText() - } - }); - - var expandStream = client.expand(request); - expandStream.on('data', function(response) { - console.log(response.getText()); - }); - expandStream.on('end', function(end) { - console.log("Expand Stream Ended"); - }); - -} - -window.addEventListener("DOMContentLoaded", function() { - document.getElementById("message_button").addEventListener("click", function() { - sendMessage(document.getElementById("input_field").value); - }); -}, false); diff --git a/Examples/EchoWeb/index.html b/Examples/EchoWeb/index.html deleted file mode 100644 index 6354cc9f3..000000000 --- a/Examples/EchoWeb/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Echo gRPC-Web Example - - - -

- - -
-
-

-
- - diff --git a/Examples/EchoWeb/package.json b/Examples/EchoWeb/package.json deleted file mode 100644 index f7a3a7fc0..000000000 --- a/Examples/EchoWeb/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "echo-grpc-web-example", - "version": "0.1.0", - "description": "Echo gRPC-Web Example", - "devDependencies": { - "@grpc/proto-loader": "^0.3.0", - "google-protobuf": "^3.6.1", - "grpc": "^1.15.0", - "grpc-web": "^1.0.0", - "webpack": "^4.16.5", - "webpack-cli": "^3.1.0" - } -} diff --git a/Examples/Google/NaturalLanguage/Makefile b/Examples/Google/NaturalLanguage/Makefile deleted file mode 100644 index 92a049873..000000000 --- a/Examples/Google/NaturalLanguage/Makefile +++ /dev/null @@ -1,9 +0,0 @@ - -all: - swift build -c release - cp .build/release/NaturalLanguage . - -clean: - rm -rf Packages googleapis .build - rm -f Package.pins NaturalLanguage Sources/*.pb.swift Sources/*.grpc.swift google.json - rm -rf Package.resolved diff --git a/Examples/Google/NaturalLanguage/Package.resolved b/Examples/Google/NaturalLanguage/Package.resolved deleted file mode 100644 index 61a38d594..000000000 --- a/Examples/Google/NaturalLanguage/Package.resolved +++ /dev/null @@ -1,115 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "BigInt", - "repositoryURL": "https://github.com/attaswift/BigInt", - "state": { - "branch": null, - "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version": "5.3.0" - } - }, - { - "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", - "state": { - "branch": null, - "revision": "af1b58fc569bfde777462349b9f7314b61762be0", - "version": "1.3.2" - } - }, - { - "package": "Auth", - "repositoryURL": "https://github.com/googleapis/google-auth-library-swift.git", - "state": { - "branch": null, - "revision": "4b510d91fc74f1415eae6dabc9836b8c3e1f44f6", - "version": "0.5.3" - } - }, - { - "package": "grpc-swift", - "repositoryURL": "/Users/filipw/Documents/dev/grpc-swift", - "state": { - "branch": "HEAD", - "revision": "6cab35e471cd8e35df5ef913c749527369f67553", - "version": null - } - }, - { - "package": "swift-atomics", - "repositoryURL": "https://github.com/apple/swift-atomics.git", - "state": { - "branch": null, - "revision": "919eb1d83e02121cdb434c7bfc1f0c66ef17febe", - "version": "1.0.2" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log", - "state": { - "branch": null, - "revision": "6fe203dc33195667ce1759bf0182975e4653ba1c", - "version": "1.4.4" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "b4e0a274f7f34210e97e2f2c50ab02a10b549250", - "version": "2.41.1" - } - }, - { - "package": "swift-nio-extras", - "repositoryURL": "https://github.com/apple/swift-nio-extras.git", - "state": { - "branch": null, - "revision": "6c84d247754ad77487a6f0694273b89b83efd056", - "version": "1.14.0" - } - }, - { - "package": "swift-nio-http2", - "repositoryURL": "https://github.com/apple/swift-nio-http2.git", - "state": { - "branch": null, - "revision": "f9ab1c94c80d568efd762d2a638f25162691d766", - "version": "1.22.1" - } - }, - { - "package": "swift-nio-ssl", - "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", - "state": { - "branch": null, - "revision": "ba7c0d7f82affc518147ea61d240330bf7f7ea9b", - "version": "2.22.1" - } - }, - { - "package": "swift-nio-transport-services", - "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", - "state": { - "branch": null, - "revision": "4e02d9cf35cabfb538c96613272fb027dd0c8692", - "version": "1.13.1" - } - }, - { - "package": "SwiftProtobuf", - "repositoryURL": "https://github.com/apple/swift-protobuf.git", - "state": { - "branch": null, - "revision": "b8230909dedc640294d7324d37f4c91ad3dcf177", - "version": "1.20.1" - } - } - ] - }, - "version": 1 -} diff --git a/Examples/Google/NaturalLanguage/Package.swift b/Examples/Google/NaturalLanguage/Package.swift deleted file mode 100644 index 12e3d35b0..000000000 --- a/Examples/Google/NaturalLanguage/Package.swift +++ /dev/null @@ -1,37 +0,0 @@ -// swift-tools-version:5.1 -/* - * 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 PackageDescription - -let package = Package( - name: "NaturalLanguage", - dependencies: [ - .package(url: "../../..", .branch("HEAD")), - .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.7.0"), - .package(url: "https://github.com/googleapis/google-auth-library-swift.git", from: "0.5.0"), - ], - targets: [ - .target( - name: "NaturalLanguage", - dependencies: [ - "GRPC", - "SwiftProtobuf", - "OAuth2", - ], - path: "Sources" - ), - ] -) diff --git a/Examples/Google/NaturalLanguage/README.md b/Examples/Google/NaturalLanguage/README.md deleted file mode 100644 index 6221ead98..000000000 --- a/Examples/Google/NaturalLanguage/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Calling the Google Cloud Natural Language API - -This directory contains a very simple sample that calls the -[Google Cloud Natural Language API](https://cloud.google.com/natural-language/docs/reference/rpc/). -Calls are made directly to the Cloud Natural Language RPC interface. -In practice, these would be wrapped in idiomatic code. - -Use [RUNME](RUNME) to generate the necessary Protocol Buffer -and gRPC support code. It uses protoc and the Swift Protocol -Buffer and gRPC plugins, so please be sure these are in your -path. The plugins can be built by running `make` in the -top-level Plugins directory. - -## Prerequisites - -Please be sure to perform the preliminary steps in -[Examples/Google/README](../README.md). diff --git a/Examples/Google/NaturalLanguage/RUNME b/Examples/Google/NaturalLanguage/RUNME deleted file mode 100755 index c451e7b82..000000000 --- a/Examples/Google/NaturalLanguage/RUNME +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -# -# Use this script to regenerate the Protocol Buffer and gRPC files -# needed to build the example. -# -# Note that it requires updated protoc, protoc-gen-swift, and -# protoc-gen-grpc-swift binaries and assumes that protoc-gen-swift -# is installed in $HOME/local/bin. - -if [ ! -d "googleapis" ]; then - curl -L -O https://github.com/googleapis/googleapis/archive/master.zip - unzip master.zip - rm -f master.zip - mv googleapis-master googleapis -fi - -protoc \ - google/cloud/language/v1/language_service.proto \ - google/api/annotations.proto \ - google/api/http.proto \ - google/protobuf/descriptor.proto \ - -Igoogleapis \ - -I../common/include \ - --swift_out=googleapis \ - --grpc-swift_out=googleapis - -# Move Swift files to the Sources directory. descriptor.pb.swift is provided -# with SwiftProtobuf (from 1.7.0). -find googleapis \ - -name "*.swift" \ - ! -name "descriptor.pb.swift" \ - -exec mv {} Sources \; diff --git a/Examples/Google/NaturalLanguage/Sources/annotations.pb.swift b/Examples/Google/NaturalLanguage/Sources/annotations.pb.swift deleted file mode 100644 index 249cc2bdd..000000000 --- a/Examples/Google/NaturalLanguage/Sources/annotations.pb.swift +++ /dev/null @@ -1,84 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: google/api/annotations.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 Google LLC -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -// MARK: - Extension support defined in annotations.proto. - -// MARK: - Extension Properties - -// Swift Extensions on the exteneded Messages to add easy access to the declared -// extension fields. The names are based on the extension field name from the proto -// declaration. To avoid naming collisions, the names are prefixed with the name of -// the scope where the extend directive occurs. - -extension SwiftProtobuf.Google_Protobuf_MethodOptions { - - /// See `HttpRule`. - var Google_Api_http: Google_Api_HttpRule { - get {return getExtensionValue(ext: Google_Api_Extensions_http) ?? Google_Api_HttpRule()} - set {setExtensionValue(ext: Google_Api_Extensions_http, value: newValue)} - } - /// Returns true if extension `Google_Api_Extensions_http` - /// has been explicitly set. - var hasGoogle_Api_http: Bool { - return hasExtensionValue(ext: Google_Api_Extensions_http) - } - /// Clears the value of extension `Google_Api_Extensions_http`. - /// Subsequent reads from it will return its default value. - mutating func clearGoogle_Api_http() { - clearExtensionValue(ext: Google_Api_Extensions_http) - } - -} - -// MARK: - File's ExtensionMap: Google_Api_Annotations_Extensions - -/// A `SwiftProtobuf.SimpleExtensionMap` that includes all of the extensions defined by -/// this .proto file. It can be used any place an `SwiftProtobuf.ExtensionMap` is needed -/// in parsing, or it can be combined with other `SwiftProtobuf.SimpleExtensionMap`s to create -/// a larger `SwiftProtobuf.SimpleExtensionMap`. -let Google_Api_Annotations_Extensions: SwiftProtobuf.SimpleExtensionMap = [ - Google_Api_Extensions_http -] - -// Extension Objects - The only reason these might be needed is when manually -// constructing a `SimpleExtensionMap`, otherwise, use the above _Extension Properties_ -// accessors for the extension fields on the messages directly. - -/// See `HttpRule`. -let Google_Api_Extensions_http = SwiftProtobuf.MessageExtension, SwiftProtobuf.Google_Protobuf_MethodOptions>( - _protobuf_fieldNumber: 72295728, - fieldName: "google.api.http" -) diff --git a/Examples/Google/NaturalLanguage/Sources/http.pb.swift b/Examples/Google/NaturalLanguage/Sources/http.pb.swift deleted file mode 100644 index 62b494f53..000000000 --- a/Examples/Google/NaturalLanguage/Sources/http.pb.swift +++ /dev/null @@ -1,730 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: google/api/http.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 Google LLC -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// Defines the HTTP configuration for an API service. It contains a list of -/// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method -/// to one or more HTTP REST API methods. -struct Google_Api_Http { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// A list of HTTP configuration rules that apply to individual API methods. - /// - /// **NOTE:** All service configuration rules follow "last one wins" order. - var rules: [Google_Api_HttpRule] = [] - - /// When set to true, URL path parameters will be fully URI-decoded except in - /// cases of single segment matches in reserved expansion, where "%2F" will be - /// left encoded. - /// - /// The default behavior is to not decode RFC 6570 reserved characters in multi - /// segment matches. - var fullyDecodeReservedExpansion: Bool = false - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// # gRPC Transcoding -/// -/// gRPC Transcoding is a feature for mapping between a gRPC method and one or -/// more HTTP REST endpoints. It allows developers to build a single API service -/// that supports both gRPC APIs and REST APIs. Many systems, including [Google -/// APIs](https://github.com/googleapis/googleapis), -/// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC -/// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), -/// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature -/// and use it for large scale production services. -/// -/// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies -/// how different portions of the gRPC request message are mapped to the URL -/// path, URL query parameters, and HTTP request body. It also controls how the -/// gRPC response message is mapped to the HTTP response body. `HttpRule` is -/// typically specified as an `google.api.http` annotation on the gRPC method. -/// -/// Each mapping specifies a URL path template and an HTTP method. The path -/// template may refer to one or more fields in the gRPC request message, as long -/// as each field is a non-repeated field with a primitive (non-message) type. -/// The path template controls how fields of the request message are mapped to -/// the URL path. -/// -/// Example: -/// -/// service Messaging { -/// rpc GetMessage(GetMessageRequest) returns (Message) { -/// option (google.api.http) = { -/// get: "/v1/{name=messages/*}" -/// }; -/// } -/// } -/// message GetMessageRequest { -/// string name = 1; // Mapped to URL path. -/// } -/// message Message { -/// string text = 1; // The resource content. -/// } -/// -/// This enables an HTTP REST to gRPC mapping as below: -/// -/// HTTP | gRPC -/// -----|----- -/// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` -/// -/// Any fields in the request message which are not bound by the path template -/// automatically become HTTP query parameters if there is no HTTP request body. -/// For example: -/// -/// service Messaging { -/// rpc GetMessage(GetMessageRequest) returns (Message) { -/// option (google.api.http) = { -/// get:"/v1/messages/{message_id}" -/// }; -/// } -/// } -/// message GetMessageRequest { -/// message SubMessage { -/// string subfield = 1; -/// } -/// string message_id = 1; // Mapped to URL path. -/// int64 revision = 2; // Mapped to URL query parameter `revision`. -/// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. -/// } -/// -/// This enables a HTTP JSON to RPC mapping as below: -/// -/// HTTP | gRPC -/// -----|----- -/// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | -/// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: -/// "foo"))` -/// -/// Note that fields which are mapped to URL query parameters must have a -/// primitive type or a repeated primitive type or a non-repeated message type. -/// In the case of a repeated type, the parameter can be repeated in the URL -/// as `...?param=A¶m=B`. In the case of a message type, each field of the -/// message is mapped to a separate parameter, such as -/// `...?foo.a=A&foo.b=B&foo.c=C`. -/// -/// For HTTP methods that allow a request body, the `body` field -/// specifies the mapping. Consider a REST update method on the -/// message resource collection: -/// -/// service Messaging { -/// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { -/// option (google.api.http) = { -/// patch: "/v1/messages/{message_id}" -/// body: "message" -/// }; -/// } -/// } -/// message UpdateMessageRequest { -/// string message_id = 1; // mapped to the URL -/// Message message = 2; // mapped to the body -/// } -/// -/// The following HTTP JSON to RPC mapping is enabled, where the -/// representation of the JSON in the request body is determined by -/// protos JSON encoding: -/// -/// HTTP | gRPC -/// -----|----- -/// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: -/// "123456" message { text: "Hi!" })` -/// -/// The special name `*` can be used in the body mapping to define that -/// every field not bound by the path template should be mapped to the -/// request body. This enables the following alternative definition of -/// the update method: -/// -/// service Messaging { -/// rpc UpdateMessage(Message) returns (Message) { -/// option (google.api.http) = { -/// patch: "/v1/messages/{message_id}" -/// body: "*" -/// }; -/// } -/// } -/// message Message { -/// string message_id = 1; -/// string text = 2; -/// } -/// -/// -/// The following HTTP JSON to RPC mapping is enabled: -/// -/// HTTP | gRPC -/// -----|----- -/// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: -/// "123456" text: "Hi!")` -/// -/// Note that when using `*` in the body mapping, it is not possible to -/// have HTTP parameters, as all fields not bound by the path end in -/// the body. This makes this option more rarely used in practice when -/// defining REST APIs. The common usage of `*` is in custom methods -/// which don't use the URL at all for transferring data. -/// -/// It is possible to define multiple HTTP methods for one RPC by using -/// the `additional_bindings` option. Example: -/// -/// service Messaging { -/// rpc GetMessage(GetMessageRequest) returns (Message) { -/// option (google.api.http) = { -/// get: "/v1/messages/{message_id}" -/// additional_bindings { -/// get: "/v1/users/{user_id}/messages/{message_id}" -/// } -/// }; -/// } -/// } -/// message GetMessageRequest { -/// string message_id = 1; -/// string user_id = 2; -/// } -/// -/// This enables the following two alternative HTTP JSON to RPC mappings: -/// -/// HTTP | gRPC -/// -----|----- -/// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` -/// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: -/// "123456")` -/// -/// ## Rules for HTTP mapping -/// -/// 1. Leaf request fields (recursive expansion nested messages in the request -/// message) are classified into three categories: -/// - Fields referred by the path template. They are passed via the URL path. -/// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP -/// request body. -/// - All other fields are passed via the URL query parameters, and the -/// parameter name is the field path in the request message. A repeated -/// field can be represented as multiple query parameters under the same -/// name. -/// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields -/// are passed via URL path and HTTP request body. -/// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all -/// fields are passed via URL path and URL query parameters. -/// -/// ### Path template syntax -/// -/// Template = "/" Segments [ Verb ] ; -/// Segments = Segment { "/" Segment } ; -/// Segment = "*" | "**" | LITERAL | Variable ; -/// Variable = "{" FieldPath [ "=" Segments ] "}" ; -/// FieldPath = IDENT { "." IDENT } ; -/// Verb = ":" LITERAL ; -/// -/// The syntax `*` matches a single URL path segment. The syntax `**` matches -/// zero or more URL path segments, which must be the last part of the URL path -/// except the `Verb`. -/// -/// The syntax `Variable` matches part of the URL path as specified by its -/// template. A variable template must not contain other variables. If a variable -/// matches a single path segment, its template may be omitted, e.g. `{var}` -/// is equivalent to `{var=*}`. -/// -/// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` -/// contains any reserved character, such characters should be percent-encoded -/// before the matching. -/// -/// If a variable contains exactly one path segment, such as `"{var}"` or -/// `"{var=*}"`, when such a variable is expanded into a URL path on the client -/// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The -/// server side does the reverse decoding. Such variables show up in the -/// [Discovery -/// Document](https://developers.google.com/discovery/v1/reference/apis) as -/// `{var}`. -/// -/// If a variable contains multiple path segments, such as `"{var=foo/*}"` -/// or `"{var=**}"`, when such a variable is expanded into a URL path on the -/// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. -/// The server side does the reverse decoding, except "%2F" and "%2f" are left -/// unchanged. Such variables show up in the -/// [Discovery -/// Document](https://developers.google.com/discovery/v1/reference/apis) as -/// `{+var}`. -/// -/// ## Using gRPC API Service Configuration -/// -/// gRPC API Service Configuration (service config) is a configuration language -/// for configuring a gRPC service to become a user-facing product. The -/// service config is simply the YAML representation of the `google.api.Service` -/// proto message. -/// -/// As an alternative to annotating your proto file, you can configure gRPC -/// transcoding in your service config YAML files. You do this by specifying a -/// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same -/// effect as the proto annotation. This can be particularly useful if you -/// have a proto that is reused in multiple services. Note that any transcoding -/// specified in the service config will override any matching transcoding -/// configuration in the proto. -/// -/// Example: -/// -/// http: -/// rules: -/// # Selects a gRPC method and applies HttpRule to it. -/// - selector: example.v1.Messaging.GetMessage -/// get: /v1/messages/{message_id}/{sub.subfield} -/// -/// ## Special notes -/// -/// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the -/// proto to JSON conversion must follow the [proto3 -/// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). -/// -/// While the single segment variable follows the semantics of -/// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String -/// Expansion, the multi segment variable **does not** follow RFC 6570 Section -/// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion -/// does not expand special characters like `?` and `#`, which would lead -/// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding -/// for multi segment variables. -/// -/// The path variables **must not** refer to any repeated or mapped field, -/// because client libraries are not capable of handling such variable expansion. -/// -/// The path variables **must not** capture the leading "/" character. The reason -/// is that the most common use case "{var}" does not capture the leading "/" -/// character. For consistency, all path variables must share the same behavior. -/// -/// Repeated message fields must not be mapped to URL query parameters, because -/// no client library can support such complicated mapping. -/// -/// If an API needs to use a JSON array for request or response body, it can map -/// the request or response body to a repeated field. However, some gRPC -/// Transcoding implementations may not support this feature. -struct Google_Api_HttpRule { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Selects a method to which this rule applies. - /// - /// Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - var selector: String = String() - - /// Determines the URL pattern is matched by this rules. This pattern can be - /// used with any of the {get|put|post|delete|patch} methods. A custom method - /// can be defined using the 'custom' field. - var pattern: Google_Api_HttpRule.OneOf_Pattern? = nil - - /// Maps to HTTP GET. Used for listing and getting information about - /// resources. - var get: String { - get { - if case .get(let v)? = pattern {return v} - return String() - } - set {pattern = .get(newValue)} - } - - /// Maps to HTTP PUT. Used for replacing a resource. - var put: String { - get { - if case .put(let v)? = pattern {return v} - return String() - } - set {pattern = .put(newValue)} - } - - /// Maps to HTTP POST. Used for creating a resource or performing an action. - var post: String { - get { - if case .post(let v)? = pattern {return v} - return String() - } - set {pattern = .post(newValue)} - } - - /// Maps to HTTP DELETE. Used for deleting a resource. - var delete: String { - get { - if case .delete(let v)? = pattern {return v} - return String() - } - set {pattern = .delete(newValue)} - } - - /// Maps to HTTP PATCH. Used for updating a resource. - var patch: String { - get { - if case .patch(let v)? = pattern {return v} - return String() - } - set {pattern = .patch(newValue)} - } - - /// The custom pattern is used for specifying an HTTP method that is not - /// included in the `pattern` field, such as HEAD, or "*" to leave the - /// HTTP method unspecified for this rule. The wild-card rule is useful - /// for services that provide content to Web (HTML) clients. - var custom: Google_Api_CustomHttpPattern { - get { - if case .custom(let v)? = pattern {return v} - return Google_Api_CustomHttpPattern() - } - set {pattern = .custom(newValue)} - } - - /// The name of the request field whose value is mapped to the HTTP request - /// body, or `*` for mapping all request fields not captured by the path - /// pattern to the HTTP body, or omitted for not having any HTTP request body. - /// - /// NOTE: the referred field must be present at the top-level of the request - /// message type. - var body: String = String() - - /// Optional. The name of the response field whose value is mapped to the HTTP - /// response body. When omitted, the entire response message will be used - /// as the HTTP response body. - /// - /// NOTE: The referred field must be present at the top-level of the response - /// message type. - var responseBody: String = String() - - /// Additional HTTP bindings for the selector. Nested bindings must - /// not contain an `additional_bindings` field themselves (that is, - /// the nesting may only be one level deep). - var additionalBindings: [Google_Api_HttpRule] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// Determines the URL pattern is matched by this rules. This pattern can be - /// used with any of the {get|put|post|delete|patch} methods. A custom method - /// can be defined using the 'custom' field. - enum OneOf_Pattern: Equatable { - /// Maps to HTTP GET. Used for listing and getting information about - /// resources. - case get(String) - /// Maps to HTTP PUT. Used for replacing a resource. - case put(String) - /// Maps to HTTP POST. Used for creating a resource or performing an action. - case post(String) - /// Maps to HTTP DELETE. Used for deleting a resource. - case delete(String) - /// Maps to HTTP PATCH. Used for updating a resource. - case patch(String) - /// The custom pattern is used for specifying an HTTP method that is not - /// included in the `pattern` field, such as HEAD, or "*" to leave the - /// HTTP method unspecified for this rule. The wild-card rule is useful - /// for services that provide content to Web (HTML) clients. - case custom(Google_Api_CustomHttpPattern) - - #if !swift(>=4.1) - static func ==(lhs: Google_Api_HttpRule.OneOf_Pattern, rhs: Google_Api_HttpRule.OneOf_Pattern) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.get, .get): return { - guard case .get(let l) = lhs, case .get(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.put, .put): return { - guard case .put(let l) = lhs, case .put(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.post, .post): return { - guard case .post(let l) = lhs, case .post(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.delete, .delete): return { - guard case .delete(let l) = lhs, case .delete(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.patch, .patch): return { - guard case .patch(let l) = lhs, case .patch(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.custom, .custom): return { - guard case .custom(let l) = lhs, case .custom(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - init() {} -} - -/// A custom pattern is used for defining custom HTTP verb. -struct Google_Api_CustomHttpPattern { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The name of this custom HTTP verb. - var kind: String = String() - - /// The path matched by this custom verb. - var path: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Google_Api_Http: @unchecked Sendable {} -extension Google_Api_HttpRule: @unchecked Sendable {} -extension Google_Api_HttpRule.OneOf_Pattern: @unchecked Sendable {} -extension Google_Api_CustomHttpPattern: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "google.api" - -extension Google_Api_Http: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Http" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "rules"), - 2: .standard(proto: "fully_decode_reserved_expansion"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.rules) }() - case 2: try { try decoder.decodeSingularBoolField(value: &self.fullyDecodeReservedExpansion) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.rules.isEmpty { - try visitor.visitRepeatedMessageField(value: self.rules, fieldNumber: 1) - } - if self.fullyDecodeReservedExpansion != false { - try visitor.visitSingularBoolField(value: self.fullyDecodeReservedExpansion, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Api_Http, rhs: Google_Api_Http) -> Bool { - if lhs.rules != rhs.rules {return false} - if lhs.fullyDecodeReservedExpansion != rhs.fullyDecodeReservedExpansion {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Api_HttpRule: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HttpRule" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "selector"), - 2: .same(proto: "get"), - 3: .same(proto: "put"), - 4: .same(proto: "post"), - 5: .same(proto: "delete"), - 6: .same(proto: "patch"), - 8: .same(proto: "custom"), - 7: .same(proto: "body"), - 12: .standard(proto: "response_body"), - 11: .standard(proto: "additional_bindings"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.selector) }() - case 2: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.pattern != nil {try decoder.handleConflictingOneOf()} - self.pattern = .get(v) - } - }() - case 3: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.pattern != nil {try decoder.handleConflictingOneOf()} - self.pattern = .put(v) - } - }() - case 4: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.pattern != nil {try decoder.handleConflictingOneOf()} - self.pattern = .post(v) - } - }() - case 5: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.pattern != nil {try decoder.handleConflictingOneOf()} - self.pattern = .delete(v) - } - }() - case 6: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.pattern != nil {try decoder.handleConflictingOneOf()} - self.pattern = .patch(v) - } - }() - case 7: try { try decoder.decodeSingularStringField(value: &self.body) }() - case 8: try { - var v: Google_Api_CustomHttpPattern? - var hadOneofValue = false - if let current = self.pattern { - hadOneofValue = true - if case .custom(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.pattern = .custom(v) - } - }() - case 11: try { try decoder.decodeRepeatedMessageField(value: &self.additionalBindings) }() - case 12: try { try decoder.decodeSingularStringField(value: &self.responseBody) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.selector.isEmpty { - try visitor.visitSingularStringField(value: self.selector, fieldNumber: 1) - } - switch self.pattern { - case .get?: try { - guard case .get(let v)? = self.pattern else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - }() - case .put?: try { - guard case .put(let v)? = self.pattern else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - }() - case .post?: try { - guard case .post(let v)? = self.pattern else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - }() - case .delete?: try { - guard case .delete(let v)? = self.pattern else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 5) - }() - case .patch?: try { - guard case .patch(let v)? = self.pattern else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 6) - }() - default: break - } - if !self.body.isEmpty { - try visitor.visitSingularStringField(value: self.body, fieldNumber: 7) - } - try { if case .custom(let v)? = self.pattern { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() - if !self.additionalBindings.isEmpty { - try visitor.visitRepeatedMessageField(value: self.additionalBindings, fieldNumber: 11) - } - if !self.responseBody.isEmpty { - try visitor.visitSingularStringField(value: self.responseBody, fieldNumber: 12) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Api_HttpRule, rhs: Google_Api_HttpRule) -> Bool { - if lhs.selector != rhs.selector {return false} - if lhs.pattern != rhs.pattern {return false} - if lhs.body != rhs.body {return false} - if lhs.responseBody != rhs.responseBody {return false} - if lhs.additionalBindings != rhs.additionalBindings {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Api_CustomHttpPattern: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".CustomHttpPattern" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "kind"), - 2: .same(proto: "path"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.kind) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.path) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.kind.isEmpty { - try visitor.visitSingularStringField(value: self.kind, fieldNumber: 1) - } - if !self.path.isEmpty { - try visitor.visitSingularStringField(value: self.path, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Api_CustomHttpPattern, rhs: Google_Api_CustomHttpPattern) -> Bool { - if lhs.kind != rhs.kind {return false} - if lhs.path != rhs.path {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift b/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift deleted file mode 100644 index 75eb20105..000000000 --- a/Examples/Google/NaturalLanguage/Sources/language_service.grpc.swift +++ /dev/null @@ -1,845 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: google/cloud/language/v1/language_service.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Provides text analysis operations such as sentiment analysis and entity -/// recognition. -/// -/// Usage: instantiate `Google_Cloud_Language_V1_LanguageServiceClient`, then call methods of this protocol to make API calls. -internal protocol Google_Cloud_Language_V1_LanguageServiceClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { get } - - func analyzeSentiment( - _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func analyzeEntities( - _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func analyzeEntitySentiment( - _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func analyzeSyntax( - _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func classifyText( - _ request: Google_Cloud_Language_V1_ClassifyTextRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func annotateText( - _ request: Google_Cloud_Language_V1_AnnotateTextRequest, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Google_Cloud_Language_V1_LanguageServiceClientProtocol { - internal var serviceName: String { - return "google.cloud.language.v1.LanguageService" - } - - /// Analyzes the sentiment of the provided text. - /// - /// - Parameters: - /// - request: Request to send to AnalyzeSentiment. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeSentiment( - _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [] - ) - } - - /// Finds named entities (currently proper names and common nouns) in the text - /// along with entity types, salience, mentions for each entity, and - /// other properties. - /// - /// - Parameters: - /// - request: Request to send to AnalyzeEntities. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeEntities( - _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [] - ) - } - - /// Finds entities, similar to - /// [AnalyzeEntities][google.cloud.language.v1.LanguageService.AnalyzeEntities] - /// in the text and analyzes sentiment associated with each entity and its - /// mentions. - /// - /// - Parameters: - /// - request: Request to send to AnalyzeEntitySentiment. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeEntitySentiment( - _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [] - ) - } - - /// Analyzes the syntax of the text and provides sentence boundaries and - /// tokenization along with part of speech tags, dependency trees, and other - /// properties. - /// - /// - Parameters: - /// - request: Request to send to AnalyzeSyntax. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func analyzeSyntax( - _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [] - ) - } - - /// Classifies a document into categories. - /// - /// - Parameters: - /// - request: Request to send to ClassifyText. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func classifyText( - _ request: Google_Cloud_Language_V1_ClassifyTextRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [] - ) - } - - /// A convenience method that provides all the features that analyzeSentiment, - /// analyzeEntities, and analyzeSyntax provide in one call. - /// - /// - Parameters: - /// - request: Request to send to AnnotateText. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func annotateText( - _ request: Google_Cloud_Language_V1_AnnotateTextRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [] - ) - } -} - -#if compiler(>=5.6) -@available(*, deprecated) -extension Google_Cloud_Language_V1_LanguageServiceClient: @unchecked Sendable {} -#endif // compiler(>=5.6) - -@available(*, deprecated, renamed: "Google_Cloud_Language_V1_LanguageServiceNIOClient") -internal final class Google_Cloud_Language_V1_LanguageServiceClient: Google_Cloud_Language_V1_LanguageServiceClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the google.cloud.language.v1.LanguageService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -internal struct Google_Cloud_Language_V1_LanguageServiceNIOClient: Google_Cloud_Language_V1_LanguageServiceClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? - - /// Creates a client for the google.cloud.language.v1.LanguageService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -#if compiler(>=5.6) -/// Provides text analysis operations such as sentiment analysis and entity -/// recognition. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { get } - - func makeAnalyzeSentimentCall( - _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeAnalyzeEntitiesCall( - _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeAnalyzeEntitySentimentCall( - _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeAnalyzeSyntaxCall( - _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeClassifyTextCall( - _ request: Google_Cloud_Language_V1_ClassifyTextRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeAnnotateTextCall( - _ request: Google_Cloud_Language_V1_AnnotateTextRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Google_Cloud_Language_V1_LanguageServiceClientMetadata.serviceDescriptor - } - - internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? { - return nil - } - - internal func makeAnalyzeSentimentCall( - _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [] - ) - } - - internal func makeAnalyzeEntitiesCall( - _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [] - ) - } - - internal func makeAnalyzeEntitySentimentCall( - _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [] - ) - } - - internal func makeAnalyzeSyntaxCall( - _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [] - ) - } - - internal func makeClassifyTextCall( - _ request: Google_Cloud_Language_V1_ClassifyTextRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [] - ) - } - - internal func makeAnnotateTextCall( - _ request: Google_Cloud_Language_V1_AnnotateTextRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol { - internal func analyzeSentiment( - _ request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, - callOptions: CallOptions? = nil - ) async throws -> Google_Cloud_Language_V1_AnalyzeSentimentResponse { - return try await self.performAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [] - ) - } - - internal func analyzeEntities( - _ request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, - callOptions: CallOptions? = nil - ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitiesResponse { - return try await self.performAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [] - ) - } - - internal func analyzeEntitySentiment( - _ request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, - callOptions: CallOptions? = nil - ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse { - return try await self.performAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [] - ) - } - - internal func analyzeSyntax( - _ request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, - callOptions: CallOptions? = nil - ) async throws -> Google_Cloud_Language_V1_AnalyzeSyntaxResponse { - return try await self.performAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [] - ) - } - - internal func classifyText( - _ request: Google_Cloud_Language_V1_ClassifyTextRequest, - callOptions: CallOptions? = nil - ) async throws -> Google_Cloud_Language_V1_ClassifyTextResponse { - return try await self.performAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [] - ) - } - - internal func annotateText( - _ request: Google_Cloud_Language_V1_AnnotateTextRequest, - callOptions: CallOptions? = nil - ) async throws -> Google_Cloud_Language_V1_AnnotateTextResponse { - return try await self.performAsyncUnaryCall( - path: Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct Google_Cloud_Language_V1_LanguageServiceAsyncClient: Google_Cloud_Language_V1_LanguageServiceAsyncClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? - - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -#endif // compiler(>=5.6) - -internal protocol Google_Cloud_Language_V1_LanguageServiceClientInterceptorFactoryProtocol: GRPCSendable { - - /// - Returns: Interceptors to use when invoking 'analyzeSentiment'. - func makeAnalyzeSentimentInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'analyzeEntities'. - func makeAnalyzeEntitiesInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'analyzeEntitySentiment'. - func makeAnalyzeEntitySentimentInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'analyzeSyntax'. - func makeAnalyzeSyntaxInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'classifyText'. - func makeClassifyTextInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'annotateText'. - func makeAnnotateTextInterceptors() -> [ClientInterceptor] -} - -internal enum Google_Cloud_Language_V1_LanguageServiceClientMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "LanguageService", - fullName: "google.cloud.language.v1.LanguageService", - methods: [ - Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSentiment, - Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntities, - Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeEntitySentiment, - Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.analyzeSyntax, - Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.classifyText, - Google_Cloud_Language_V1_LanguageServiceClientMetadata.Methods.annotateText, - ] - ) - - internal enum Methods { - internal static let analyzeSentiment = GRPCMethodDescriptor( - name: "AnalyzeSentiment", - path: "/google.cloud.language.v1.LanguageService/AnalyzeSentiment", - type: GRPCCallType.unary - ) - - internal static let analyzeEntities = GRPCMethodDescriptor( - name: "AnalyzeEntities", - path: "/google.cloud.language.v1.LanguageService/AnalyzeEntities", - type: GRPCCallType.unary - ) - - internal static let analyzeEntitySentiment = GRPCMethodDescriptor( - name: "AnalyzeEntitySentiment", - path: "/google.cloud.language.v1.LanguageService/AnalyzeEntitySentiment", - type: GRPCCallType.unary - ) - - internal static let analyzeSyntax = GRPCMethodDescriptor( - name: "AnalyzeSyntax", - path: "/google.cloud.language.v1.LanguageService/AnalyzeSyntax", - type: GRPCCallType.unary - ) - - internal static let classifyText = GRPCMethodDescriptor( - name: "ClassifyText", - path: "/google.cloud.language.v1.LanguageService/ClassifyText", - type: GRPCCallType.unary - ) - - internal static let annotateText = GRPCMethodDescriptor( - name: "AnnotateText", - path: "/google.cloud.language.v1.LanguageService/AnnotateText", - type: GRPCCallType.unary - ) - } -} - -/// Provides text analysis operations such as sentiment analysis and entity -/// recognition. -/// -/// To build a server, implement a class that conforms to this protocol. -internal protocol Google_Cloud_Language_V1_LanguageServiceProvider: CallHandlerProvider { - var interceptors: Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol? { get } - - /// Analyzes the sentiment of the provided text. - func analyzeSentiment(request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Finds named entities (currently proper names and common nouns) in the text - /// along with entity types, salience, mentions for each entity, and - /// other properties. - func analyzeEntities(request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Finds entities, similar to - /// [AnalyzeEntities][google.cloud.language.v1.LanguageService.AnalyzeEntities] - /// in the text and analyzes sentiment associated with each entity and its - /// mentions. - func analyzeEntitySentiment(request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Analyzes the syntax of the text and provides sentence boundaries and - /// tokenization along with part of speech tags, dependency trees, and other - /// properties. - func analyzeSyntax(request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Classifies a document into categories. - func classifyText(request: Google_Cloud_Language_V1_ClassifyTextRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// A convenience method that provides all the features that analyzeSentiment, - /// analyzeEntities, and analyzeSyntax provide in one call. - func annotateText(request: Google_Cloud_Language_V1_AnnotateTextRequest, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension Google_Cloud_Language_V1_LanguageServiceProvider { - internal var serviceName: Substring { - return Google_Cloud_Language_V1_LanguageServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "AnalyzeSentiment": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [], - userFunction: self.analyzeSentiment(request:context:) - ) - - case "AnalyzeEntities": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [], - userFunction: self.analyzeEntities(request:context:) - ) - - case "AnalyzeEntitySentiment": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [], - userFunction: self.analyzeEntitySentiment(request:context:) - ) - - case "AnalyzeSyntax": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [], - userFunction: self.analyzeSyntax(request:context:) - ) - - case "ClassifyText": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [], - userFunction: self.classifyText(request:context:) - ) - - case "AnnotateText": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [], - userFunction: self.annotateText(request:context:) - ) - - default: - return nil - } - } -} - -#if compiler(>=5.6) - -/// Provides text analysis operations such as sentiment analysis and entity -/// recognition. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Google_Cloud_Language_V1_LanguageServiceAsyncProvider: CallHandlerProvider { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol? { get } - - /// Analyzes the sentiment of the provided text. - @Sendable func analyzeSentiment( - request: Google_Cloud_Language_V1_AnalyzeSentimentRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Google_Cloud_Language_V1_AnalyzeSentimentResponse - - /// Finds named entities (currently proper names and common nouns) in the text - /// along with entity types, salience, mentions for each entity, and - /// other properties. - @Sendable func analyzeEntities( - request: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitiesResponse - - /// Finds entities, similar to - /// [AnalyzeEntities][google.cloud.language.v1.LanguageService.AnalyzeEntities] - /// in the text and analyzes sentiment associated with each entity and its - /// mentions. - @Sendable func analyzeEntitySentiment( - request: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse - - /// Analyzes the syntax of the text and provides sentence boundaries and - /// tokenization along with part of speech tags, dependency trees, and other - /// properties. - @Sendable func analyzeSyntax( - request: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Google_Cloud_Language_V1_AnalyzeSyntaxResponse - - /// Classifies a document into categories. - @Sendable func classifyText( - request: Google_Cloud_Language_V1_ClassifyTextRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Google_Cloud_Language_V1_ClassifyTextResponse - - /// A convenience method that provides all the features that analyzeSentiment, - /// analyzeEntities, and analyzeSyntax provide in one call. - @Sendable func annotateText( - request: Google_Cloud_Language_V1_AnnotateTextRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Google_Cloud_Language_V1_AnnotateTextResponse -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Google_Cloud_Language_V1_LanguageServiceAsyncProvider { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Google_Cloud_Language_V1_LanguageServiceServerMetadata.serviceDescriptor - } - - internal var serviceName: Substring { - return Google_Cloud_Language_V1_LanguageServiceServerMetadata.serviceDescriptor.fullName[...] - } - - internal var interceptors: Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol? { - return nil - } - - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "AnalyzeSentiment": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeSentimentInterceptors() ?? [], - wrapping: self.analyzeSentiment(request:context:) - ) - - case "AnalyzeEntities": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeEntitiesInterceptors() ?? [], - wrapping: self.analyzeEntities(request:context:) - ) - - case "AnalyzeEntitySentiment": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeEntitySentimentInterceptors() ?? [], - wrapping: self.analyzeEntitySentiment(request:context:) - ) - - case "AnalyzeSyntax": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnalyzeSyntaxInterceptors() ?? [], - wrapping: self.analyzeSyntax(request:context:) - ) - - case "ClassifyText": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeClassifyTextInterceptors() ?? [], - wrapping: self.classifyText(request:context:) - ) - - case "AnnotateText": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeAnnotateTextInterceptors() ?? [], - wrapping: self.annotateText(request:context:) - ) - - default: - return nil - } - } -} - -#endif // compiler(>=5.6) - -internal protocol Google_Cloud_Language_V1_LanguageServiceServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'analyzeSentiment'. - /// Defaults to calling `self.makeInterceptors()`. - func makeAnalyzeSentimentInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'analyzeEntities'. - /// Defaults to calling `self.makeInterceptors()`. - func makeAnalyzeEntitiesInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'analyzeEntitySentiment'. - /// Defaults to calling `self.makeInterceptors()`. - func makeAnalyzeEntitySentimentInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'analyzeSyntax'. - /// Defaults to calling `self.makeInterceptors()`. - func makeAnalyzeSyntaxInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'classifyText'. - /// Defaults to calling `self.makeInterceptors()`. - func makeClassifyTextInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'annotateText'. - /// Defaults to calling `self.makeInterceptors()`. - func makeAnnotateTextInterceptors() -> [ServerInterceptor] -} - -internal enum Google_Cloud_Language_V1_LanguageServiceServerMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "LanguageService", - fullName: "google.cloud.language.v1.LanguageService", - methods: [ - Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeSentiment, - Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeEntities, - Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeEntitySentiment, - Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.analyzeSyntax, - Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.classifyText, - Google_Cloud_Language_V1_LanguageServiceServerMetadata.Methods.annotateText, - ] - ) - - internal enum Methods { - internal static let analyzeSentiment = GRPCMethodDescriptor( - name: "AnalyzeSentiment", - path: "/google.cloud.language.v1.LanguageService/AnalyzeSentiment", - type: GRPCCallType.unary - ) - - internal static let analyzeEntities = GRPCMethodDescriptor( - name: "AnalyzeEntities", - path: "/google.cloud.language.v1.LanguageService/AnalyzeEntities", - type: GRPCCallType.unary - ) - - internal static let analyzeEntitySentiment = GRPCMethodDescriptor( - name: "AnalyzeEntitySentiment", - path: "/google.cloud.language.v1.LanguageService/AnalyzeEntitySentiment", - type: GRPCCallType.unary - ) - - internal static let analyzeSyntax = GRPCMethodDescriptor( - name: "AnalyzeSyntax", - path: "/google.cloud.language.v1.LanguageService/AnalyzeSyntax", - type: GRPCCallType.unary - ) - - internal static let classifyText = GRPCMethodDescriptor( - name: "ClassifyText", - path: "/google.cloud.language.v1.LanguageService/ClassifyText", - type: GRPCCallType.unary - ) - - internal static let annotateText = GRPCMethodDescriptor( - name: "AnnotateText", - path: "/google.cloud.language.v1.LanguageService/AnnotateText", - type: GRPCCallType.unary - ) - } -} diff --git a/Examples/Google/NaturalLanguage/Sources/language_service.pb.swift b/Examples/Google/NaturalLanguage/Sources/language_service.pb.swift deleted file mode 100644 index b4a8c2c70..000000000 --- a/Examples/Google/NaturalLanguage/Sources/language_service.pb.swift +++ /dev/null @@ -1,4261 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: google/cloud/language/v1/language_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2019 Google LLC. -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// Represents the text encoding that the caller uses to process the output. -/// Providing an `EncodingType` is recommended because the API provides the -/// beginning offsets for various outputs, such as tokens and mentions, and -/// languages that natively use different text encodings may access offsets -/// differently. -enum Google_Cloud_Language_V1_EncodingType: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// If `EncodingType` is not specified, encoding-dependent information (such as - /// `begin_offset`) will be set at `-1`. - case none // = 0 - - /// Encoding-dependent information (such as `begin_offset`) is calculated based - /// on the UTF-8 encoding of the input. C++ and Go are examples of languages - /// that use this encoding natively. - case utf8 // = 1 - - /// Encoding-dependent information (such as `begin_offset`) is calculated based - /// on the UTF-16 encoding of the input. Java and JavaScript are examples of - /// languages that use this encoding natively. - case utf16 // = 2 - - /// Encoding-dependent information (such as `begin_offset`) is calculated based - /// on the UTF-32 encoding of the input. Python is an example of a language - /// that uses this encoding natively. - case utf32 // = 3 - case UNRECOGNIZED(Int) - - init() { - self = .none - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .none - case 1: self = .utf8 - case 2: self = .utf16 - case 3: self = .utf32 - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .none: return 0 - case .utf8: return 1 - case .utf16: return 2 - case .utf32: return 3 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension Google_Cloud_Language_V1_EncodingType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_EncodingType] = [ - .none, - .utf8, - .utf16, - .utf32, - ] -} - -#endif // swift(>=4.2) - -/// ################################################################ # -/// -/// Represents the input to API methods. -struct Google_Cloud_Language_V1_Document { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. If the type is not set or is `TYPE_UNSPECIFIED`, - /// returns an `INVALID_ARGUMENT` error. - var type: Google_Cloud_Language_V1_Document.TypeEnum = .unspecified - - /// The source of the document: a string containing the content or a - /// Google Cloud Storage URI. - var source: Google_Cloud_Language_V1_Document.OneOf_Source? = nil - - /// The content of the input in string format. - /// Cloud audit logging exempt since it is based on user data. - var content: String { - get { - if case .content(let v)? = source {return v} - return String() - } - set {source = .content(newValue)} - } - - /// The Google Cloud Storage URI where the file content is located. - /// This URI must be of the form: gs://bucket_name/object_name. For more - /// details, see https://cloud.google.com/storage/docs/reference-uris. - /// NOTE: Cloud Storage object versioning is not supported. - var gcsContentUri: String { - get { - if case .gcsContentUri(let v)? = source {return v} - return String() - } - set {source = .gcsContentUri(newValue)} - } - - /// The language of the document (if not specified, the language is - /// automatically detected). Both ISO and BCP-47 language codes are - /// accepted.
- /// [Language - /// Support](https://cloud.google.com/natural-language/docs/languages) lists - /// currently supported languages for each API method. If the language (either - /// specified by the caller or automatically detected) is not supported by the - /// called API method, an `INVALID_ARGUMENT` error is returned. - var language: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The source of the document: a string containing the content or a - /// Google Cloud Storage URI. - enum OneOf_Source: Equatable { - /// The content of the input in string format. - /// Cloud audit logging exempt since it is based on user data. - case content(String) - /// The Google Cloud Storage URI where the file content is located. - /// This URI must be of the form: gs://bucket_name/object_name. For more - /// details, see https://cloud.google.com/storage/docs/reference-uris. - /// NOTE: Cloud Storage object versioning is not supported. - case gcsContentUri(String) - - #if !swift(>=4.1) - static func ==(lhs: Google_Cloud_Language_V1_Document.OneOf_Source, rhs: Google_Cloud_Language_V1_Document.OneOf_Source) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.content, .content): return { - guard case .content(let l) = lhs, case .content(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.gcsContentUri, .gcsContentUri): return { - guard case .gcsContentUri(let l) = lhs, case .gcsContentUri(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - /// The document types enum. - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// The content type is not specified. - case unspecified // = 0 - - /// Plain text - case plainText // = 1 - - /// HTML - case html // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unspecified - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unspecified - case 1: self = .plainText - case 2: self = .html - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unspecified: return 0 - case .plainText: return 1 - case .html: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - } - - init() {} -} - -#if swift(>=4.2) - -extension Google_Cloud_Language_V1_Document.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_Document.TypeEnum] = [ - .unspecified, - .plainText, - .html, - ] -} - -#endif // swift(>=4.2) - -/// Represents a sentence in the input document. -struct Google_Cloud_Language_V1_Sentence { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The sentence text. - var text: Google_Cloud_Language_V1_TextSpan { - get {return _text ?? Google_Cloud_Language_V1_TextSpan()} - set {_text = newValue} - } - /// Returns true if `text` has been explicitly set. - var hasText: Bool {return self._text != nil} - /// Clears the value of `text`. Subsequent reads from it will return its default value. - mutating func clearText() {self._text = nil} - - /// For calls to [AnalyzeSentiment][] or if - /// [AnnotateTextRequest.Features.extract_document_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_document_sentiment] - /// is set to true, this field will contain the sentiment for the sentence. - var sentiment: Google_Cloud_Language_V1_Sentiment { - get {return _sentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_sentiment = newValue} - } - /// Returns true if `sentiment` has been explicitly set. - var hasSentiment: Bool {return self._sentiment != nil} - /// Clears the value of `sentiment`. Subsequent reads from it will return its default value. - mutating func clearSentiment() {self._sentiment = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _text: Google_Cloud_Language_V1_TextSpan? = nil - fileprivate var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil -} - -/// Represents a phrase in the text that is a known entity, such as -/// a person, an organization, or location. The API associates information, such -/// as salience and mentions, with entities. -struct Google_Cloud_Language_V1_Entity { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The representative name for the entity. - var name: String = String() - - /// The entity type. - var type: Google_Cloud_Language_V1_Entity.TypeEnum = .unknown - - /// Metadata associated with the entity. - /// - /// For most entity types, the metadata is a Wikipedia URL (`wikipedia_url`) - /// and Knowledge Graph MID (`mid`), if they are available. For the metadata - /// associated with other entity types, see the Type table below. - var metadata: Dictionary = [:] - - /// The salience score associated with the entity in the [0, 1.0] range. - /// - /// The salience score for an entity provides information about the - /// importance or centrality of that entity to the entire document text. - /// Scores closer to 0 are less salient, while scores closer to 1.0 are highly - /// salient. - var salience: Float = 0 - - /// The mentions of this entity in the input document. The API currently - /// supports proper noun mentions. - var mentions: [Google_Cloud_Language_V1_EntityMention] = [] - - /// For calls to [AnalyzeEntitySentiment][] or if - /// [AnnotateTextRequest.Features.extract_entity_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entity_sentiment] - /// is set to true, this field will contain the aggregate sentiment expressed - /// for this entity in the provided document. - var sentiment: Google_Cloud_Language_V1_Sentiment { - get {return _sentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_sentiment = newValue} - } - /// Returns true if `sentiment` has been explicitly set. - var hasSentiment: Bool {return self._sentiment != nil} - /// Clears the value of `sentiment`. Subsequent reads from it will return its default value. - mutating func clearSentiment() {self._sentiment = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The type of the entity. For most entity types, the associated metadata is a - /// Wikipedia URL (`wikipedia_url`) and Knowledge Graph MID (`mid`). The table - /// below lists the associated fields for entities that have different - /// metadata. - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Unknown - case unknown // = 0 - - /// Person - case person // = 1 - - /// Location - case location // = 2 - - /// Organization - case organization // = 3 - - /// Event - case event // = 4 - - /// Artwork - case workOfArt // = 5 - - /// Consumer product - case consumerGood // = 6 - - /// Other types of entities - case other // = 7 - - /// Phone number - /// - /// The metadata lists the phone number, formatted according to local - /// convention, plus whichever additional elements appear in the text: - /// - /// * `number` - the actual number, broken down into sections as per local - /// convention - /// * `national_prefix` - country code, if detected - /// * `area_code` - region or area code, if detected - /// * `extension` - phone extension (to be dialed after connection), if - /// detected - case phoneNumber // = 9 - - /// Address - /// - /// The metadata identifies the street number and locality plus whichever - /// additional elements appear in the text: - /// - /// * `street_number` - street number - /// * `locality` - city or town - /// * `street_name` - street/route name, if detected - /// * `postal_code` - postal code, if detected - /// * `country` - country, if detected< - /// * `broad_region` - administrative area, such as the state, if detected - /// * `narrow_region` - smaller administrative area, such as county, if - /// detected - /// * `sublocality` - used in Asian addresses to demark a district within a - /// city, if detected - case address // = 10 - - /// Date - /// - /// The metadata identifies the components of the date: - /// - /// * `year` - four digit year, if detected - /// * `month` - two digit month number, if detected - /// * `day` - two digit day number, if detected - case date // = 11 - - /// Number - /// - /// The metadata is the number itself. - case number // = 12 - - /// Price - /// - /// The metadata identifies the `value` and `currency`. - case price // = 13 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .person - case 2: self = .location - case 3: self = .organization - case 4: self = .event - case 5: self = .workOfArt - case 6: self = .consumerGood - case 7: self = .other - case 9: self = .phoneNumber - case 10: self = .address - case 11: self = .date - case 12: self = .number - case 13: self = .price - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .person: return 1 - case .location: return 2 - case .organization: return 3 - case .event: return 4 - case .workOfArt: return 5 - case .consumerGood: return 6 - case .other: return 7 - case .phoneNumber: return 9 - case .address: return 10 - case .date: return 11 - case .number: return 12 - case .price: return 13 - case .UNRECOGNIZED(let i): return i - } - } - - } - - init() {} - - fileprivate var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil -} - -#if swift(>=4.2) - -extension Google_Cloud_Language_V1_Entity.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_Entity.TypeEnum] = [ - .unknown, - .person, - .location, - .organization, - .event, - .workOfArt, - .consumerGood, - .other, - .phoneNumber, - .address, - .date, - .number, - .price, - ] -} - -#endif // swift(>=4.2) - -/// Represents the smallest syntactic building block of the text. -struct Google_Cloud_Language_V1_Token { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The token text. - var text: Google_Cloud_Language_V1_TextSpan { - get {return _storage._text ?? Google_Cloud_Language_V1_TextSpan()} - set {_uniqueStorage()._text = newValue} - } - /// Returns true if `text` has been explicitly set. - var hasText: Bool {return _storage._text != nil} - /// Clears the value of `text`. Subsequent reads from it will return its default value. - mutating func clearText() {_uniqueStorage()._text = nil} - - /// Parts of speech tag for this token. - var partOfSpeech: Google_Cloud_Language_V1_PartOfSpeech { - get {return _storage._partOfSpeech ?? Google_Cloud_Language_V1_PartOfSpeech()} - set {_uniqueStorage()._partOfSpeech = newValue} - } - /// Returns true if `partOfSpeech` has been explicitly set. - var hasPartOfSpeech: Bool {return _storage._partOfSpeech != nil} - /// Clears the value of `partOfSpeech`. Subsequent reads from it will return its default value. - mutating func clearPartOfSpeech() {_uniqueStorage()._partOfSpeech = nil} - - /// Dependency tree parse for this token. - var dependencyEdge: Google_Cloud_Language_V1_DependencyEdge { - get {return _storage._dependencyEdge ?? Google_Cloud_Language_V1_DependencyEdge()} - set {_uniqueStorage()._dependencyEdge = newValue} - } - /// Returns true if `dependencyEdge` has been explicitly set. - var hasDependencyEdge: Bool {return _storage._dependencyEdge != nil} - /// Clears the value of `dependencyEdge`. Subsequent reads from it will return its default value. - mutating func clearDependencyEdge() {_uniqueStorage()._dependencyEdge = nil} - - /// [Lemma](https://en.wikipedia.org/wiki/Lemma_%28morphology%29) of the token. - var lemma: String { - get {return _storage._lemma} - set {_uniqueStorage()._lemma = newValue} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -/// Represents the feeling associated with the entire text or entities in -/// the text. -struct Google_Cloud_Language_V1_Sentiment { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// A non-negative number in the [0, +inf) range, which represents - /// the absolute magnitude of sentiment regardless of score (positive or - /// negative). - var magnitude: Float = 0 - - /// Sentiment score between -1.0 (negative sentiment) and 1.0 - /// (positive sentiment). - var score: Float = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Represents part of speech information for a token. Parts of speech -/// are as defined in -/// http://www.lrec-conf.org/proceedings/lrec2012/pdf/274_Paper.pdf -struct Google_Cloud_Language_V1_PartOfSpeech { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The part of speech tag. - var tag: Google_Cloud_Language_V1_PartOfSpeech.Tag = .unknown - - /// The grammatical aspect. - var aspect: Google_Cloud_Language_V1_PartOfSpeech.Aspect = .unknown - - /// The grammatical case. - var `case`: Google_Cloud_Language_V1_PartOfSpeech.Case = .unknown - - /// The grammatical form. - var form: Google_Cloud_Language_V1_PartOfSpeech.Form = .unknown - - /// The grammatical gender. - var gender: Google_Cloud_Language_V1_PartOfSpeech.Gender = .unknown - - /// The grammatical mood. - var mood: Google_Cloud_Language_V1_PartOfSpeech.Mood = .unknown - - /// The grammatical number. - var number: Google_Cloud_Language_V1_PartOfSpeech.Number = .unknown - - /// The grammatical person. - var person: Google_Cloud_Language_V1_PartOfSpeech.Person = .unknown - - /// The grammatical properness. - var proper: Google_Cloud_Language_V1_PartOfSpeech.Proper = .unknown - - /// The grammatical reciprocity. - var reciprocity: Google_Cloud_Language_V1_PartOfSpeech.Reciprocity = .unknown - - /// The grammatical tense. - var tense: Google_Cloud_Language_V1_PartOfSpeech.Tense = .unknown - - /// The grammatical voice. - var voice: Google_Cloud_Language_V1_PartOfSpeech.Voice = .unknown - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The part of speech tags enum. - enum Tag: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Unknown - case unknown // = 0 - - /// Adjective - case adj // = 1 - - /// Adposition (preposition and postposition) - case adp // = 2 - - /// Adverb - case adv // = 3 - - /// Conjunction - case conj // = 4 - - /// Determiner - case det // = 5 - - /// Noun (common and proper) - case noun // = 6 - - /// Cardinal number - case num // = 7 - - /// Pronoun - case pron // = 8 - - /// Particle or other function word - case prt // = 9 - - /// Punctuation - case punct // = 10 - - /// Verb (all tenses and modes) - case verb // = 11 - - /// Other: foreign words, typos, abbreviations - case x // = 12 - - /// Affix - case affix // = 13 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .adj - case 2: self = .adp - case 3: self = .adv - case 4: self = .conj - case 5: self = .det - case 6: self = .noun - case 7: self = .num - case 8: self = .pron - case 9: self = .prt - case 10: self = .punct - case 11: self = .verb - case 12: self = .x - case 13: self = .affix - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .adj: return 1 - case .adp: return 2 - case .adv: return 3 - case .conj: return 4 - case .det: return 5 - case .noun: return 6 - case .num: return 7 - case .pron: return 8 - case .prt: return 9 - case .punct: return 10 - case .verb: return 11 - case .x: return 12 - case .affix: return 13 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// The characteristic of a verb that expresses time flow during an event. - enum Aspect: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Aspect is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Perfective - case perfective // = 1 - - /// Imperfective - case imperfective // = 2 - - /// Progressive - case progressive // = 3 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .perfective - case 2: self = .imperfective - case 3: self = .progressive - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .perfective: return 1 - case .imperfective: return 2 - case .progressive: return 3 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// The grammatical function performed by a noun or pronoun in a phrase, - /// clause, or sentence. In some languages, other parts of speech, such as - /// adjective and determiner, take case inflection in agreement with the noun. - enum Case: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Case is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Accusative - case accusative // = 1 - - /// Adverbial - case adverbial // = 2 - - /// Complementive - case complementive // = 3 - - /// Dative - case dative // = 4 - - /// Genitive - case genitive // = 5 - - /// Instrumental - case instrumental // = 6 - - /// Locative - case locative // = 7 - - /// Nominative - case nominative // = 8 - - /// Oblique - case oblique // = 9 - - /// Partitive - case partitive // = 10 - - /// Prepositional - case prepositional // = 11 - - /// Reflexive - case reflexiveCase // = 12 - - /// Relative - case relativeCase // = 13 - - /// Vocative - case vocative // = 14 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .accusative - case 2: self = .adverbial - case 3: self = .complementive - case 4: self = .dative - case 5: self = .genitive - case 6: self = .instrumental - case 7: self = .locative - case 8: self = .nominative - case 9: self = .oblique - case 10: self = .partitive - case 11: self = .prepositional - case 12: self = .reflexiveCase - case 13: self = .relativeCase - case 14: self = .vocative - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .accusative: return 1 - case .adverbial: return 2 - case .complementive: return 3 - case .dative: return 4 - case .genitive: return 5 - case .instrumental: return 6 - case .locative: return 7 - case .nominative: return 8 - case .oblique: return 9 - case .partitive: return 10 - case .prepositional: return 11 - case .reflexiveCase: return 12 - case .relativeCase: return 13 - case .vocative: return 14 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// Depending on the language, Form can be categorizing different forms of - /// verbs, adjectives, adverbs, etc. For example, categorizing inflected - /// endings of verbs and adjectives or distinguishing between short and long - /// forms of adjectives and participles - enum Form: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Form is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Adnomial - case adnomial // = 1 - - /// Auxiliary - case auxiliary // = 2 - - /// Complementizer - case complementizer // = 3 - - /// Final ending - case finalEnding // = 4 - - /// Gerund - case gerund // = 5 - - /// Realis - case realis // = 6 - - /// Irrealis - case irrealis // = 7 - - /// Short form - case short // = 8 - - /// Long form - case long // = 9 - - /// Order form - case order // = 10 - - /// Specific form - case specific // = 11 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .adnomial - case 2: self = .auxiliary - case 3: self = .complementizer - case 4: self = .finalEnding - case 5: self = .gerund - case 6: self = .realis - case 7: self = .irrealis - case 8: self = .short - case 9: self = .long - case 10: self = .order - case 11: self = .specific - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .adnomial: return 1 - case .auxiliary: return 2 - case .complementizer: return 3 - case .finalEnding: return 4 - case .gerund: return 5 - case .realis: return 6 - case .irrealis: return 7 - case .short: return 8 - case .long: return 9 - case .order: return 10 - case .specific: return 11 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// Gender classes of nouns reflected in the behaviour of associated words. - enum Gender: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Gender is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Feminine - case feminine // = 1 - - /// Masculine - case masculine // = 2 - - /// Neuter - case neuter // = 3 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .feminine - case 2: self = .masculine - case 3: self = .neuter - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .feminine: return 1 - case .masculine: return 2 - case .neuter: return 3 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// The grammatical feature of verbs, used for showing modality and attitude. - enum Mood: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Mood is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Conditional - case conditionalMood // = 1 - - /// Imperative - case imperative // = 2 - - /// Indicative - case indicative // = 3 - - /// Interrogative - case interrogative // = 4 - - /// Jussive - case jussive // = 5 - - /// Subjunctive - case subjunctive // = 6 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .conditionalMood - case 2: self = .imperative - case 3: self = .indicative - case 4: self = .interrogative - case 5: self = .jussive - case 6: self = .subjunctive - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .conditionalMood: return 1 - case .imperative: return 2 - case .indicative: return 3 - case .interrogative: return 4 - case .jussive: return 5 - case .subjunctive: return 6 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// Count distinctions. - enum Number: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Number is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Singular - case singular // = 1 - - /// Plural - case plural // = 2 - - /// Dual - case dual // = 3 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .singular - case 2: self = .plural - case 3: self = .dual - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .singular: return 1 - case .plural: return 2 - case .dual: return 3 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// The distinction between the speaker, second person, third person, etc. - enum Person: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Person is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// First - case first // = 1 - - /// Second - case second // = 2 - - /// Third - case third // = 3 - - /// Reflexive - case reflexivePerson // = 4 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .first - case 2: self = .second - case 3: self = .third - case 4: self = .reflexivePerson - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .first: return 1 - case .second: return 2 - case .third: return 3 - case .reflexivePerson: return 4 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// This category shows if the token is part of a proper name. - enum Proper: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Proper is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Proper - case proper // = 1 - - /// Not proper - case notProper // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .proper - case 2: self = .notProper - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .proper: return 1 - case .notProper: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// Reciprocal features of a pronoun. - enum Reciprocity: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Reciprocity is not applicable in the analyzed language or is not - /// predicted. - case unknown // = 0 - - /// Reciprocal - case reciprocal // = 1 - - /// Non-reciprocal - case nonReciprocal // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .reciprocal - case 2: self = .nonReciprocal - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .reciprocal: return 1 - case .nonReciprocal: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// Time reference. - enum Tense: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Tense is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Conditional - case conditionalTense // = 1 - - /// Future - case future // = 2 - - /// Past - case past // = 3 - - /// Present - case present // = 4 - - /// Imperfect - case imperfect // = 5 - - /// Pluperfect - case pluperfect // = 6 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .conditionalTense - case 2: self = .future - case 3: self = .past - case 4: self = .present - case 5: self = .imperfect - case 6: self = .pluperfect - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .conditionalTense: return 1 - case .future: return 2 - case .past: return 3 - case .present: return 4 - case .imperfect: return 5 - case .pluperfect: return 6 - case .UNRECOGNIZED(let i): return i - } - } - - } - - /// The relationship between the action that a verb expresses and the - /// participants identified by its arguments. - enum Voice: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Voice is not applicable in the analyzed language or is not predicted. - case unknown // = 0 - - /// Active - case active // = 1 - - /// Causative - case causative // = 2 - - /// Passive - case passive // = 3 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .active - case 2: self = .causative - case 3: self = .passive - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .active: return 1 - case .causative: return 2 - case .passive: return 3 - case .UNRECOGNIZED(let i): return i - } - } - - } - - init() {} -} - -#if swift(>=4.2) - -extension Google_Cloud_Language_V1_PartOfSpeech.Tag: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Tag] = [ - .unknown, - .adj, - .adp, - .adv, - .conj, - .det, - .noun, - .num, - .pron, - .prt, - .punct, - .verb, - .x, - .affix, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Aspect: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Aspect] = [ - .unknown, - .perfective, - .imperfective, - .progressive, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Case: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Case] = [ - .unknown, - .accusative, - .adverbial, - .complementive, - .dative, - .genitive, - .instrumental, - .locative, - .nominative, - .oblique, - .partitive, - .prepositional, - .reflexiveCase, - .relativeCase, - .vocative, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Form: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Form] = [ - .unknown, - .adnomial, - .auxiliary, - .complementizer, - .finalEnding, - .gerund, - .realis, - .irrealis, - .short, - .long, - .order, - .specific, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Gender: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Gender] = [ - .unknown, - .feminine, - .masculine, - .neuter, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Mood: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Mood] = [ - .unknown, - .conditionalMood, - .imperative, - .indicative, - .interrogative, - .jussive, - .subjunctive, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Number: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Number] = [ - .unknown, - .singular, - .plural, - .dual, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Person: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Person] = [ - .unknown, - .first, - .second, - .third, - .reflexivePerson, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Proper: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Proper] = [ - .unknown, - .proper, - .notProper, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Reciprocity: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Reciprocity] = [ - .unknown, - .reciprocal, - .nonReciprocal, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Tense: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Tense] = [ - .unknown, - .conditionalTense, - .future, - .past, - .present, - .imperfect, - .pluperfect, - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Voice: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_PartOfSpeech.Voice] = [ - .unknown, - .active, - .causative, - .passive, - ] -} - -#endif // swift(>=4.2) - -/// Represents dependency parse tree information for a token. (For more -/// information on dependency labels, see -/// http://www.aclweb.org/anthology/P13-2017 -struct Google_Cloud_Language_V1_DependencyEdge { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Represents the head of this token in the dependency tree. - /// This is the index of the token which has an arc going to this token. - /// The index is the position of the token in the array of tokens returned - /// by the API method. If this token is a root token, then the - /// `head_token_index` is its own index. - var headTokenIndex: Int32 = 0 - - /// The parse label for the token. - var label: Google_Cloud_Language_V1_DependencyEdge.Label = .unknown - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The parse label enum for the token. - enum Label: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Unknown - case unknown // = 0 - - /// Abbreviation modifier - case abbrev // = 1 - - /// Adjectival complement - case acomp // = 2 - - /// Adverbial clause modifier - case advcl // = 3 - - /// Adverbial modifier - case advmod // = 4 - - /// Adjectival modifier of an NP - case amod // = 5 - - /// Appositional modifier of an NP - case appos // = 6 - - /// Attribute dependent of a copular verb - case attr // = 7 - - /// Auxiliary (non-main) verb - case aux // = 8 - - /// Passive auxiliary - case auxpass // = 9 - - /// Coordinating conjunction - case cc // = 10 - - /// Clausal complement of a verb or adjective - case ccomp // = 11 - - /// Conjunct - case conj // = 12 - - /// Clausal subject - case csubj // = 13 - - /// Clausal passive subject - case csubjpass // = 14 - - /// Dependency (unable to determine) - case dep // = 15 - - /// Determiner - case det // = 16 - - /// Discourse - case discourse // = 17 - - /// Direct object - case dobj // = 18 - - /// Expletive - case expl // = 19 - - /// Goes with (part of a word in a text not well edited) - case goeswith // = 20 - - /// Indirect object - case iobj // = 21 - - /// Marker (word introducing a subordinate clause) - case mark // = 22 - - /// Multi-word expression - case mwe // = 23 - - /// Multi-word verbal expression - case mwv // = 24 - - /// Negation modifier - case neg // = 25 - - /// Noun compound modifier - case nn // = 26 - - /// Noun phrase used as an adverbial modifier - case npadvmod // = 27 - - /// Nominal subject - case nsubj // = 28 - - /// Passive nominal subject - case nsubjpass // = 29 - - /// Numeric modifier of a noun - case num // = 30 - - /// Element of compound number - case number // = 31 - - /// Punctuation mark - case p // = 32 - - /// Parataxis relation - case parataxis // = 33 - - /// Participial modifier - case partmod // = 34 - - /// The complement of a preposition is a clause - case pcomp // = 35 - - /// Object of a preposition - case pobj // = 36 - - /// Possession modifier - case poss // = 37 - - /// Postverbal negative particle - case postneg // = 38 - - /// Predicate complement - case precomp // = 39 - - /// Preconjunt - case preconj // = 40 - - /// Predeterminer - case predet // = 41 - - /// Prefix - case pref // = 42 - - /// Prepositional modifier - case prep // = 43 - - /// The relationship between a verb and verbal morpheme - case pronl // = 44 - - /// Particle - case prt // = 45 - - /// Associative or possessive marker - case ps // = 46 - - /// Quantifier phrase modifier - case quantmod // = 47 - - /// Relative clause modifier - case rcmod // = 48 - - /// Complementizer in relative clause - case rcmodrel // = 49 - - /// Ellipsis without a preceding predicate - case rdrop // = 50 - - /// Referent - case ref // = 51 - - /// Remnant - case remnant // = 52 - - /// Reparandum - case reparandum // = 53 - - /// Root - case root // = 54 - - /// Suffix specifying a unit of number - case snum // = 55 - - /// Suffix - case suff // = 56 - - /// Temporal modifier - case tmod // = 57 - - /// Topic marker - case topic // = 58 - - /// Clause headed by an infinite form of the verb that modifies a noun - case vmod // = 59 - - /// Vocative - case vocative // = 60 - - /// Open clausal complement - case xcomp // = 61 - - /// Name suffix - case suffix // = 62 - - /// Name title - case title // = 63 - - /// Adverbial phrase modifier - case advphmod // = 64 - - /// Causative auxiliary - case auxcaus // = 65 - - /// Helper auxiliary - case auxvv // = 66 - - /// Rentaishi (Prenominal modifier) - case dtmod // = 67 - - /// Foreign words - case foreign // = 68 - - /// Keyword - case kw // = 69 - - /// List for chains of comparable items - case list // = 70 - - /// Nominalized clause - case nomc // = 71 - - /// Nominalized clausal subject - case nomcsubj // = 72 - - /// Nominalized clausal passive - case nomcsubjpass // = 73 - - /// Compound of numeric modifier - case numc // = 74 - - /// Copula - case cop // = 75 - - /// Dislocated relation (for fronted/topicalized elements) - case dislocated // = 76 - - /// Aspect marker - case asp // = 77 - - /// Genitive modifier - case gmod // = 78 - - /// Genitive object - case gobj // = 79 - - /// Infinitival modifier - case infmod // = 80 - - /// Measure - case mes // = 81 - - /// Nominal complement of a noun - case ncomp // = 82 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .abbrev - case 2: self = .acomp - case 3: self = .advcl - case 4: self = .advmod - case 5: self = .amod - case 6: self = .appos - case 7: self = .attr - case 8: self = .aux - case 9: self = .auxpass - case 10: self = .cc - case 11: self = .ccomp - case 12: self = .conj - case 13: self = .csubj - case 14: self = .csubjpass - case 15: self = .dep - case 16: self = .det - case 17: self = .discourse - case 18: self = .dobj - case 19: self = .expl - case 20: self = .goeswith - case 21: self = .iobj - case 22: self = .mark - case 23: self = .mwe - case 24: self = .mwv - case 25: self = .neg - case 26: self = .nn - case 27: self = .npadvmod - case 28: self = .nsubj - case 29: self = .nsubjpass - case 30: self = .num - case 31: self = .number - case 32: self = .p - case 33: self = .parataxis - case 34: self = .partmod - case 35: self = .pcomp - case 36: self = .pobj - case 37: self = .poss - case 38: self = .postneg - case 39: self = .precomp - case 40: self = .preconj - case 41: self = .predet - case 42: self = .pref - case 43: self = .prep - case 44: self = .pronl - case 45: self = .prt - case 46: self = .ps - case 47: self = .quantmod - case 48: self = .rcmod - case 49: self = .rcmodrel - case 50: self = .rdrop - case 51: self = .ref - case 52: self = .remnant - case 53: self = .reparandum - case 54: self = .root - case 55: self = .snum - case 56: self = .suff - case 57: self = .tmod - case 58: self = .topic - case 59: self = .vmod - case 60: self = .vocative - case 61: self = .xcomp - case 62: self = .suffix - case 63: self = .title - case 64: self = .advphmod - case 65: self = .auxcaus - case 66: self = .auxvv - case 67: self = .dtmod - case 68: self = .foreign - case 69: self = .kw - case 70: self = .list - case 71: self = .nomc - case 72: self = .nomcsubj - case 73: self = .nomcsubjpass - case 74: self = .numc - case 75: self = .cop - case 76: self = .dislocated - case 77: self = .asp - case 78: self = .gmod - case 79: self = .gobj - case 80: self = .infmod - case 81: self = .mes - case 82: self = .ncomp - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .abbrev: return 1 - case .acomp: return 2 - case .advcl: return 3 - case .advmod: return 4 - case .amod: return 5 - case .appos: return 6 - case .attr: return 7 - case .aux: return 8 - case .auxpass: return 9 - case .cc: return 10 - case .ccomp: return 11 - case .conj: return 12 - case .csubj: return 13 - case .csubjpass: return 14 - case .dep: return 15 - case .det: return 16 - case .discourse: return 17 - case .dobj: return 18 - case .expl: return 19 - case .goeswith: return 20 - case .iobj: return 21 - case .mark: return 22 - case .mwe: return 23 - case .mwv: return 24 - case .neg: return 25 - case .nn: return 26 - case .npadvmod: return 27 - case .nsubj: return 28 - case .nsubjpass: return 29 - case .num: return 30 - case .number: return 31 - case .p: return 32 - case .parataxis: return 33 - case .partmod: return 34 - case .pcomp: return 35 - case .pobj: return 36 - case .poss: return 37 - case .postneg: return 38 - case .precomp: return 39 - case .preconj: return 40 - case .predet: return 41 - case .pref: return 42 - case .prep: return 43 - case .pronl: return 44 - case .prt: return 45 - case .ps: return 46 - case .quantmod: return 47 - case .rcmod: return 48 - case .rcmodrel: return 49 - case .rdrop: return 50 - case .ref: return 51 - case .remnant: return 52 - case .reparandum: return 53 - case .root: return 54 - case .snum: return 55 - case .suff: return 56 - case .tmod: return 57 - case .topic: return 58 - case .vmod: return 59 - case .vocative: return 60 - case .xcomp: return 61 - case .suffix: return 62 - case .title: return 63 - case .advphmod: return 64 - case .auxcaus: return 65 - case .auxvv: return 66 - case .dtmod: return 67 - case .foreign: return 68 - case .kw: return 69 - case .list: return 70 - case .nomc: return 71 - case .nomcsubj: return 72 - case .nomcsubjpass: return 73 - case .numc: return 74 - case .cop: return 75 - case .dislocated: return 76 - case .asp: return 77 - case .gmod: return 78 - case .gobj: return 79 - case .infmod: return 80 - case .mes: return 81 - case .ncomp: return 82 - case .UNRECOGNIZED(let i): return i - } - } - - } - - init() {} -} - -#if swift(>=4.2) - -extension Google_Cloud_Language_V1_DependencyEdge.Label: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_DependencyEdge.Label] = [ - .unknown, - .abbrev, - .acomp, - .advcl, - .advmod, - .amod, - .appos, - .attr, - .aux, - .auxpass, - .cc, - .ccomp, - .conj, - .csubj, - .csubjpass, - .dep, - .det, - .discourse, - .dobj, - .expl, - .goeswith, - .iobj, - .mark, - .mwe, - .mwv, - .neg, - .nn, - .npadvmod, - .nsubj, - .nsubjpass, - .num, - .number, - .p, - .parataxis, - .partmod, - .pcomp, - .pobj, - .poss, - .postneg, - .precomp, - .preconj, - .predet, - .pref, - .prep, - .pronl, - .prt, - .ps, - .quantmod, - .rcmod, - .rcmodrel, - .rdrop, - .ref, - .remnant, - .reparandum, - .root, - .snum, - .suff, - .tmod, - .topic, - .vmod, - .vocative, - .xcomp, - .suffix, - .title, - .advphmod, - .auxcaus, - .auxvv, - .dtmod, - .foreign, - .kw, - .list, - .nomc, - .nomcsubj, - .nomcsubjpass, - .numc, - .cop, - .dislocated, - .asp, - .gmod, - .gobj, - .infmod, - .mes, - .ncomp, - ] -} - -#endif // swift(>=4.2) - -/// Represents a mention for an entity in the text. Currently, proper noun -/// mentions are supported. -struct Google_Cloud_Language_V1_EntityMention { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The mention text. - var text: Google_Cloud_Language_V1_TextSpan { - get {return _text ?? Google_Cloud_Language_V1_TextSpan()} - set {_text = newValue} - } - /// Returns true if `text` has been explicitly set. - var hasText: Bool {return self._text != nil} - /// Clears the value of `text`. Subsequent reads from it will return its default value. - mutating func clearText() {self._text = nil} - - /// The type of the entity mention. - var type: Google_Cloud_Language_V1_EntityMention.TypeEnum = .unknown - - /// For calls to [AnalyzeEntitySentiment][] or if - /// [AnnotateTextRequest.Features.extract_entity_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entity_sentiment] - /// is set to true, this field will contain the sentiment expressed for this - /// mention of the entity in the provided document. - var sentiment: Google_Cloud_Language_V1_Sentiment { - get {return _sentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_sentiment = newValue} - } - /// Returns true if `sentiment` has been explicitly set. - var hasSentiment: Bool {return self._sentiment != nil} - /// Clears the value of `sentiment`. Subsequent reads from it will return its default value. - mutating func clearSentiment() {self._sentiment = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The supported types of mentions. - enum TypeEnum: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// Unknown - case unknown // = 0 - - /// Proper name - case proper // = 1 - - /// Common noun (or noun compound) - case common // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .proper - case 2: self = .common - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .proper: return 1 - case .common: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - } - - init() {} - - fileprivate var _text: Google_Cloud_Language_V1_TextSpan? = nil - fileprivate var _sentiment: Google_Cloud_Language_V1_Sentiment? = nil -} - -#if swift(>=4.2) - -extension Google_Cloud_Language_V1_EntityMention.TypeEnum: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_EntityMention.TypeEnum] = [ - .unknown, - .proper, - .common, - ] -} - -#endif // swift(>=4.2) - -/// Represents an output piece of text. -struct Google_Cloud_Language_V1_TextSpan { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The content of the output text. - var content: String = String() - - /// The API calculates the beginning offset of the content in the original - /// document according to the - /// [EncodingType][google.cloud.language.v1.EncodingType] specified in the API - /// request. - var beginOffset: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Represents a category returned from the text classifier. -struct Google_Cloud_Language_V1_ClassificationCategory { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The name of the category representing the document, from the [predefined - /// taxonomy](https://cloud.google.com/natural-language/docs/categories). - var name: String = String() - - /// The classifier's confidence of the category. Number represents how certain - /// the classifier is that this category represents the given text. - var confidence: Float = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Model options available for classification requests. -struct Google_Cloud_Language_V1_ClassificationModelOptions { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// If this field is not set, then the `v1_model` will be used by default. - var modelType: Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType? = nil - - /// Setting this field will use the V1 model and V1 content categories - /// version. The V1 model is a legacy model; support for this will be - /// discontinued in the future. - var v1Model: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model { - get { - if case .v1Model(let v)? = modelType {return v} - return Google_Cloud_Language_V1_ClassificationModelOptions.V1Model() - } - set {modelType = .v1Model(newValue)} - } - - /// Setting this field will use the V2 model with the appropriate content - /// categories version. The V2 model is a better performing model. - var v2Model: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model { - get { - if case .v2Model(let v)? = modelType {return v} - return Google_Cloud_Language_V1_ClassificationModelOptions.V2Model() - } - set {modelType = .v2Model(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// If this field is not set, then the `v1_model` will be used by default. - enum OneOf_ModelType: Equatable { - /// Setting this field will use the V1 model and V1 content categories - /// version. The V1 model is a legacy model; support for this will be - /// discontinued in the future. - case v1Model(Google_Cloud_Language_V1_ClassificationModelOptions.V1Model) - /// Setting this field will use the V2 model with the appropriate content - /// categories version. The V2 model is a better performing model. - case v2Model(Google_Cloud_Language_V1_ClassificationModelOptions.V2Model) - - #if !swift(>=4.1) - static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType, rhs: Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType) -> Bool { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch (lhs, rhs) { - case (.v1Model, .v1Model): return { - guard case .v1Model(let l) = lhs, case .v1Model(let r) = rhs else { preconditionFailure() } - return l == r - }() - case (.v2Model, .v2Model): return { - guard case .v2Model(let l) = lhs, case .v2Model(let r) = rhs else { preconditionFailure() } - return l == r - }() - default: return false - } - } - #endif - } - - /// Options for the V1 model. - struct V1Model { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - /// Options for the V2 model. - struct V2Model { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The content categories used for classification. - var contentCategoriesVersion: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion = .unspecified - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The content categories used for classification. - enum ContentCategoriesVersion: SwiftProtobuf.Enum { - typealias RawValue = Int - - /// If `ContentCategoriesVersion` is not specified, this option will - /// default to `V1`. - case unspecified // = 0 - - /// Legacy content categories of our initial launch in 2017. - case v1 // = 1 - - /// Updated content categories in 2022. - case v2 // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unspecified - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unspecified - case 1: self = .v1 - case 2: self = .v2 - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unspecified: return 0 - case .v1: return 1 - case .v2: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - } - - init() {} - } - - init() {} -} - -#if swift(>=4.2) - -extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - static var allCases: [Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion] = [ - .unspecified, - .v1, - .v2, - ] -} - -#endif // swift(>=4.2) - -/// The sentiment analysis request message. -struct Google_Cloud_Language_V1_AnalyzeSentimentRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. Input document. - var document: Google_Cloud_Language_V1_Document { - get {return _document ?? Google_Cloud_Language_V1_Document()} - set {_document = newValue} - } - /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return self._document != nil} - /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {self._document = nil} - - /// The encoding type used by the API to calculate sentence offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType = .none - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _document: Google_Cloud_Language_V1_Document? = nil -} - -/// The sentiment analysis response message. -struct Google_Cloud_Language_V1_AnalyzeSentimentResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The overall sentiment of the input document. - var documentSentiment: Google_Cloud_Language_V1_Sentiment { - get {return _documentSentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_documentSentiment = newValue} - } - /// Returns true if `documentSentiment` has been explicitly set. - var hasDocumentSentiment: Bool {return self._documentSentiment != nil} - /// Clears the value of `documentSentiment`. Subsequent reads from it will return its default value. - mutating func clearDocumentSentiment() {self._documentSentiment = nil} - - /// The language of the text, which will be the same as the language specified - /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field - /// for more details. - var language: String = String() - - /// The sentiment for all the sentences in the document. - var sentences: [Google_Cloud_Language_V1_Sentence] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _documentSentiment: Google_Cloud_Language_V1_Sentiment? = nil -} - -/// The entity-level sentiment analysis request message. -struct Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. Input document. - var document: Google_Cloud_Language_V1_Document { - get {return _document ?? Google_Cloud_Language_V1_Document()} - set {_document = newValue} - } - /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return self._document != nil} - /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {self._document = nil} - - /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType = .none - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _document: Google_Cloud_Language_V1_Document? = nil -} - -/// The entity-level sentiment analysis response message. -struct Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The recognized entities in the input document with associated sentiments. - var entities: [Google_Cloud_Language_V1_Entity] = [] - - /// The language of the text, which will be the same as the language specified - /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field - /// for more details. - var language: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The entity analysis request message. -struct Google_Cloud_Language_V1_AnalyzeEntitiesRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. Input document. - var document: Google_Cloud_Language_V1_Document { - get {return _document ?? Google_Cloud_Language_V1_Document()} - set {_document = newValue} - } - /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return self._document != nil} - /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {self._document = nil} - - /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType = .none - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _document: Google_Cloud_Language_V1_Document? = nil -} - -/// The entity analysis response message. -struct Google_Cloud_Language_V1_AnalyzeEntitiesResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The recognized entities in the input document. - var entities: [Google_Cloud_Language_V1_Entity] = [] - - /// The language of the text, which will be the same as the language specified - /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field - /// for more details. - var language: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The syntax analysis request message. -struct Google_Cloud_Language_V1_AnalyzeSyntaxRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. Input document. - var document: Google_Cloud_Language_V1_Document { - get {return _document ?? Google_Cloud_Language_V1_Document()} - set {_document = newValue} - } - /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return self._document != nil} - /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {self._document = nil} - - /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType = .none - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _document: Google_Cloud_Language_V1_Document? = nil -} - -/// The syntax analysis response message. -struct Google_Cloud_Language_V1_AnalyzeSyntaxResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Sentences in the input document. - var sentences: [Google_Cloud_Language_V1_Sentence] = [] - - /// Tokens, along with their syntactic information, in the input document. - var tokens: [Google_Cloud_Language_V1_Token] = [] - - /// The language of the text, which will be the same as the language specified - /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field - /// for more details. - var language: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The document classification request message. -struct Google_Cloud_Language_V1_ClassifyTextRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. Input document. - var document: Google_Cloud_Language_V1_Document { - get {return _document ?? Google_Cloud_Language_V1_Document()} - set {_document = newValue} - } - /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return self._document != nil} - /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {self._document = nil} - - /// Model options to use for classification. Defaults to v1 options if not - /// specified. - var classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions { - get {return _classificationModelOptions ?? Google_Cloud_Language_V1_ClassificationModelOptions()} - set {_classificationModelOptions = newValue} - } - /// Returns true if `classificationModelOptions` has been explicitly set. - var hasClassificationModelOptions: Bool {return self._classificationModelOptions != nil} - /// Clears the value of `classificationModelOptions`. Subsequent reads from it will return its default value. - mutating func clearClassificationModelOptions() {self._classificationModelOptions = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _document: Google_Cloud_Language_V1_Document? = nil - fileprivate var _classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions? = nil -} - -/// The document classification response message. -struct Google_Cloud_Language_V1_ClassifyTextResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Categories representing the input document. - var categories: [Google_Cloud_Language_V1_ClassificationCategory] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The request message for the text annotation API, which can perform multiple -/// analysis types (sentiment, entities, and syntax) in one call. -struct Google_Cloud_Language_V1_AnnotateTextRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Required. Input document. - var document: Google_Cloud_Language_V1_Document { - get {return _document ?? Google_Cloud_Language_V1_Document()} - set {_document = newValue} - } - /// Returns true if `document` has been explicitly set. - var hasDocument: Bool {return self._document != nil} - /// Clears the value of `document`. Subsequent reads from it will return its default value. - mutating func clearDocument() {self._document = nil} - - /// Required. The enabled features. - var features: Google_Cloud_Language_V1_AnnotateTextRequest.Features { - get {return _features ?? Google_Cloud_Language_V1_AnnotateTextRequest.Features()} - set {_features = newValue} - } - /// Returns true if `features` has been explicitly set. - var hasFeatures: Bool {return self._features != nil} - /// Clears the value of `features`. Subsequent reads from it will return its default value. - mutating func clearFeatures() {self._features = nil} - - /// The encoding type used by the API to calculate offsets. - var encodingType: Google_Cloud_Language_V1_EncodingType = .none - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// All available features for sentiment, syntax, and semantic analysis. - /// Setting each one to true will enable that specific analysis for the input. - struct Features { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Extract syntax information. - var extractSyntax: Bool = false - - /// Extract entities. - var extractEntities: Bool = false - - /// Extract document-level sentiment. - var extractDocumentSentiment: Bool = false - - /// Extract entities and their associated sentiment. - var extractEntitySentiment: Bool = false - - /// Classify the full document into categories. - var classifyText: Bool = false - - /// The model options to use for classification. Defaults to v1 options - /// if not specified. Only used if `classify_text` is set to true. - var classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions { - get {return _classificationModelOptions ?? Google_Cloud_Language_V1_ClassificationModelOptions()} - set {_classificationModelOptions = newValue} - } - /// Returns true if `classificationModelOptions` has been explicitly set. - var hasClassificationModelOptions: Bool {return self._classificationModelOptions != nil} - /// Clears the value of `classificationModelOptions`. Subsequent reads from it will return its default value. - mutating func clearClassificationModelOptions() {self._classificationModelOptions = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _classificationModelOptions: Google_Cloud_Language_V1_ClassificationModelOptions? = nil - } - - init() {} - - fileprivate var _document: Google_Cloud_Language_V1_Document? = nil - fileprivate var _features: Google_Cloud_Language_V1_AnnotateTextRequest.Features? = nil -} - -/// The text annotations response message. -struct Google_Cloud_Language_V1_AnnotateTextResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Sentences in the input document. Populated if the user enables - /// [AnnotateTextRequest.Features.extract_syntax][google.cloud.language.v1.AnnotateTextRequest.Features.extract_syntax]. - var sentences: [Google_Cloud_Language_V1_Sentence] = [] - - /// Tokens, along with their syntactic information, in the input document. - /// Populated if the user enables - /// [AnnotateTextRequest.Features.extract_syntax][google.cloud.language.v1.AnnotateTextRequest.Features.extract_syntax]. - var tokens: [Google_Cloud_Language_V1_Token] = [] - - /// Entities, along with their semantic information, in the input document. - /// Populated if the user enables - /// [AnnotateTextRequest.Features.extract_entities][google.cloud.language.v1.AnnotateTextRequest.Features.extract_entities]. - var entities: [Google_Cloud_Language_V1_Entity] = [] - - /// The overall sentiment for the document. Populated if the user enables - /// [AnnotateTextRequest.Features.extract_document_sentiment][google.cloud.language.v1.AnnotateTextRequest.Features.extract_document_sentiment]. - var documentSentiment: Google_Cloud_Language_V1_Sentiment { - get {return _documentSentiment ?? Google_Cloud_Language_V1_Sentiment()} - set {_documentSentiment = newValue} - } - /// Returns true if `documentSentiment` has been explicitly set. - var hasDocumentSentiment: Bool {return self._documentSentiment != nil} - /// Clears the value of `documentSentiment`. Subsequent reads from it will return its default value. - mutating func clearDocumentSentiment() {self._documentSentiment = nil} - - /// The language of the text, which will be the same as the language specified - /// in the request or, if not specified, the automatically-detected language. - /// See [Document.language][google.cloud.language.v1.Document.language] field - /// for more details. - var language: String = String() - - /// Categories identified in the input document. - var categories: [Google_Cloud_Language_V1_ClassificationCategory] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _documentSentiment: Google_Cloud_Language_V1_Sentiment? = nil -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Google_Cloud_Language_V1_EncodingType: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Document: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Document.OneOf_Source: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Document.TypeEnum: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Sentence: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Entity: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Entity.TypeEnum: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Token: @unchecked Sendable {} -extension Google_Cloud_Language_V1_Sentiment: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Tag: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Aspect: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Case: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Form: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Gender: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Mood: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Number: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Person: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Proper: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Reciprocity: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Tense: @unchecked Sendable {} -extension Google_Cloud_Language_V1_PartOfSpeech.Voice: @unchecked Sendable {} -extension Google_Cloud_Language_V1_DependencyEdge: @unchecked Sendable {} -extension Google_Cloud_Language_V1_DependencyEdge.Label: @unchecked Sendable {} -extension Google_Cloud_Language_V1_EntityMention: @unchecked Sendable {} -extension Google_Cloud_Language_V1_EntityMention.TypeEnum: @unchecked Sendable {} -extension Google_Cloud_Language_V1_TextSpan: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassificationCategory: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassificationModelOptions: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassificationModelOptions.OneOf_ModelType: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassificationModelOptions.V1Model: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeSentimentRequest: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeSentimentResponse: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeEntitiesRequest: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeEntitiesResponse: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeSyntaxRequest: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnalyzeSyntaxResponse: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassifyTextRequest: @unchecked Sendable {} -extension Google_Cloud_Language_V1_ClassifyTextResponse: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnnotateTextRequest: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnnotateTextRequest.Features: @unchecked Sendable {} -extension Google_Cloud_Language_V1_AnnotateTextResponse: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "google.cloud.language.v1" - -extension Google_Cloud_Language_V1_EncodingType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NONE"), - 1: .same(proto: "UTF8"), - 2: .same(proto: "UTF16"), - 3: .same(proto: "UTF32"), - ] -} - -extension Google_Cloud_Language_V1_Document: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Document" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "content"), - 3: .standard(proto: "gcs_content_uri"), - 4: .same(proto: "language"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 2: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.source != nil {try decoder.handleConflictingOneOf()} - self.source = .content(v) - } - }() - case 3: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.source != nil {try decoder.handleConflictingOneOf()} - self.source = .gcsContentUri(v) - } - }() - case 4: try { try decoder.decodeSingularStringField(value: &self.language) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.type != .unspecified { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) - } - switch self.source { - case .content?: try { - guard case .content(let v)? = self.source else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - }() - case .gcsContentUri?: try { - guard case .gcsContentUri(let v)? = self.source else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - }() - case nil: break - } - if !self.language.isEmpty { - try visitor.visitSingularStringField(value: self.language, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_Document, rhs: Google_Cloud_Language_V1_Document) -> Bool { - if lhs.type != rhs.type {return false} - if lhs.source != rhs.source {return false} - if lhs.language != rhs.language {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_Document.TypeEnum: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "TYPE_UNSPECIFIED"), - 1: .same(proto: "PLAIN_TEXT"), - 2: .same(proto: "HTML"), - ] -} - -extension Google_Cloud_Language_V1_Sentence: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Sentence" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - 2: .same(proto: "sentiment"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._text) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._sentiment) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._text { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._sentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_Sentence, rhs: Google_Cloud_Language_V1_Sentence) -> Bool { - if lhs._text != rhs._text {return false} - if lhs._sentiment != rhs._sentiment {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_Entity: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Entity" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .same(proto: "type"), - 3: .same(proto: "metadata"), - 4: .same(proto: "salience"), - 5: .same(proto: "mentions"), - 6: .same(proto: "sentiment"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.metadata) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.salience) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &self.mentions) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._sentiment) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - if self.type != .unknown { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 2) - } - if !self.metadata.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.metadata, fieldNumber: 3) - } - if self.salience != 0 { - try visitor.visitSingularFloatField(value: self.salience, fieldNumber: 4) - } - if !self.mentions.isEmpty { - try visitor.visitRepeatedMessageField(value: self.mentions, fieldNumber: 5) - } - try { if let v = self._sentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_Entity, rhs: Google_Cloud_Language_V1_Entity) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.type != rhs.type {return false} - if lhs.metadata != rhs.metadata {return false} - if lhs.salience != rhs.salience {return false} - if lhs.mentions != rhs.mentions {return false} - if lhs._sentiment != rhs._sentiment {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_Entity.TypeEnum: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "PERSON"), - 2: .same(proto: "LOCATION"), - 3: .same(proto: "ORGANIZATION"), - 4: .same(proto: "EVENT"), - 5: .same(proto: "WORK_OF_ART"), - 6: .same(proto: "CONSUMER_GOOD"), - 7: .same(proto: "OTHER"), - 9: .same(proto: "PHONE_NUMBER"), - 10: .same(proto: "ADDRESS"), - 11: .same(proto: "DATE"), - 12: .same(proto: "NUMBER"), - 13: .same(proto: "PRICE"), - ] -} - -extension Google_Cloud_Language_V1_Token: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Token" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - 2: .standard(proto: "part_of_speech"), - 3: .standard(proto: "dependency_edge"), - 4: .same(proto: "lemma"), - ] - - fileprivate class _StorageClass { - var _text: Google_Cloud_Language_V1_TextSpan? = nil - var _partOfSpeech: Google_Cloud_Language_V1_PartOfSpeech? = nil - var _dependencyEdge: Google_Cloud_Language_V1_DependencyEdge? = nil - var _lemma: String = String() - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _text = source._text - _partOfSpeech = source._partOfSpeech - _dependencyEdge = source._dependencyEdge - _lemma = source._lemma - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &_storage._text) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._partOfSpeech) }() - case 3: try { try decoder.decodeSingularMessageField(value: &_storage._dependencyEdge) }() - case 4: try { try decoder.decodeSingularStringField(value: &_storage._lemma) }() - default: break - } - } - } - } - - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = _storage._text { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = _storage._partOfSpeech { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = _storage._dependencyEdge { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if !_storage._lemma.isEmpty { - try visitor.visitSingularStringField(value: _storage._lemma, fieldNumber: 4) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_Token, rhs: Google_Cloud_Language_V1_Token) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._text != rhs_storage._text {return false} - if _storage._partOfSpeech != rhs_storage._partOfSpeech {return false} - if _storage._dependencyEdge != rhs_storage._dependencyEdge {return false} - if _storage._lemma != rhs_storage._lemma {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_Sentiment: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Sentiment" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 2: .same(proto: "magnitude"), - 3: .same(proto: "score"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 2: try { try decoder.decodeSingularFloatField(value: &self.magnitude) }() - case 3: try { try decoder.decodeSingularFloatField(value: &self.score) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.magnitude != 0 { - try visitor.visitSingularFloatField(value: self.magnitude, fieldNumber: 2) - } - if self.score != 0 { - try visitor.visitSingularFloatField(value: self.score, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_Sentiment, rhs: Google_Cloud_Language_V1_Sentiment) -> Bool { - if lhs.magnitude != rhs.magnitude {return false} - if lhs.score != rhs.score {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_PartOfSpeech: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PartOfSpeech" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "tag"), - 2: .same(proto: "aspect"), - 3: .same(proto: "case"), - 4: .same(proto: "form"), - 5: .same(proto: "gender"), - 6: .same(proto: "mood"), - 7: .same(proto: "number"), - 8: .same(proto: "person"), - 9: .same(proto: "proper"), - 10: .same(proto: "reciprocity"), - 11: .same(proto: "tense"), - 12: .same(proto: "voice"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.tag) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.aspect) }() - case 3: try { try decoder.decodeSingularEnumField(value: &self.`case`) }() - case 4: try { try decoder.decodeSingularEnumField(value: &self.form) }() - case 5: try { try decoder.decodeSingularEnumField(value: &self.gender) }() - case 6: try { try decoder.decodeSingularEnumField(value: &self.mood) }() - case 7: try { try decoder.decodeSingularEnumField(value: &self.number) }() - case 8: try { try decoder.decodeSingularEnumField(value: &self.person) }() - case 9: try { try decoder.decodeSingularEnumField(value: &self.proper) }() - case 10: try { try decoder.decodeSingularEnumField(value: &self.reciprocity) }() - case 11: try { try decoder.decodeSingularEnumField(value: &self.tense) }() - case 12: try { try decoder.decodeSingularEnumField(value: &self.voice) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.tag != .unknown { - try visitor.visitSingularEnumField(value: self.tag, fieldNumber: 1) - } - if self.aspect != .unknown { - try visitor.visitSingularEnumField(value: self.aspect, fieldNumber: 2) - } - if self.`case` != .unknown { - try visitor.visitSingularEnumField(value: self.`case`, fieldNumber: 3) - } - if self.form != .unknown { - try visitor.visitSingularEnumField(value: self.form, fieldNumber: 4) - } - if self.gender != .unknown { - try visitor.visitSingularEnumField(value: self.gender, fieldNumber: 5) - } - if self.mood != .unknown { - try visitor.visitSingularEnumField(value: self.mood, fieldNumber: 6) - } - if self.number != .unknown { - try visitor.visitSingularEnumField(value: self.number, fieldNumber: 7) - } - if self.person != .unknown { - try visitor.visitSingularEnumField(value: self.person, fieldNumber: 8) - } - if self.proper != .unknown { - try visitor.visitSingularEnumField(value: self.proper, fieldNumber: 9) - } - if self.reciprocity != .unknown { - try visitor.visitSingularEnumField(value: self.reciprocity, fieldNumber: 10) - } - if self.tense != .unknown { - try visitor.visitSingularEnumField(value: self.tense, fieldNumber: 11) - } - if self.voice != .unknown { - try visitor.visitSingularEnumField(value: self.voice, fieldNumber: 12) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_PartOfSpeech, rhs: Google_Cloud_Language_V1_PartOfSpeech) -> Bool { - if lhs.tag != rhs.tag {return false} - if lhs.aspect != rhs.aspect {return false} - if lhs.`case` != rhs.`case` {return false} - if lhs.form != rhs.form {return false} - if lhs.gender != rhs.gender {return false} - if lhs.mood != rhs.mood {return false} - if lhs.number != rhs.number {return false} - if lhs.person != rhs.person {return false} - if lhs.proper != rhs.proper {return false} - if lhs.reciprocity != rhs.reciprocity {return false} - if lhs.tense != rhs.tense {return false} - if lhs.voice != rhs.voice {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Tag: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "ADJ"), - 2: .same(proto: "ADP"), - 3: .same(proto: "ADV"), - 4: .same(proto: "CONJ"), - 5: .same(proto: "DET"), - 6: .same(proto: "NOUN"), - 7: .same(proto: "NUM"), - 8: .same(proto: "PRON"), - 9: .same(proto: "PRT"), - 10: .same(proto: "PUNCT"), - 11: .same(proto: "VERB"), - 12: .same(proto: "X"), - 13: .same(proto: "AFFIX"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Aspect: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "ASPECT_UNKNOWN"), - 1: .same(proto: "PERFECTIVE"), - 2: .same(proto: "IMPERFECTIVE"), - 3: .same(proto: "PROGRESSIVE"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Case: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "CASE_UNKNOWN"), - 1: .same(proto: "ACCUSATIVE"), - 2: .same(proto: "ADVERBIAL"), - 3: .same(proto: "COMPLEMENTIVE"), - 4: .same(proto: "DATIVE"), - 5: .same(proto: "GENITIVE"), - 6: .same(proto: "INSTRUMENTAL"), - 7: .same(proto: "LOCATIVE"), - 8: .same(proto: "NOMINATIVE"), - 9: .same(proto: "OBLIQUE"), - 10: .same(proto: "PARTITIVE"), - 11: .same(proto: "PREPOSITIONAL"), - 12: .same(proto: "REFLEXIVE_CASE"), - 13: .same(proto: "RELATIVE_CASE"), - 14: .same(proto: "VOCATIVE"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Form: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "FORM_UNKNOWN"), - 1: .same(proto: "ADNOMIAL"), - 2: .same(proto: "AUXILIARY"), - 3: .same(proto: "COMPLEMENTIZER"), - 4: .same(proto: "FINAL_ENDING"), - 5: .same(proto: "GERUND"), - 6: .same(proto: "REALIS"), - 7: .same(proto: "IRREALIS"), - 8: .same(proto: "SHORT"), - 9: .same(proto: "LONG"), - 10: .same(proto: "ORDER"), - 11: .same(proto: "SPECIFIC"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Gender: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "GENDER_UNKNOWN"), - 1: .same(proto: "FEMININE"), - 2: .same(proto: "MASCULINE"), - 3: .same(proto: "NEUTER"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Mood: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "MOOD_UNKNOWN"), - 1: .same(proto: "CONDITIONAL_MOOD"), - 2: .same(proto: "IMPERATIVE"), - 3: .same(proto: "INDICATIVE"), - 4: .same(proto: "INTERROGATIVE"), - 5: .same(proto: "JUSSIVE"), - 6: .same(proto: "SUBJUNCTIVE"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Number: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "NUMBER_UNKNOWN"), - 1: .same(proto: "SINGULAR"), - 2: .same(proto: "PLURAL"), - 3: .same(proto: "DUAL"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Person: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "PERSON_UNKNOWN"), - 1: .same(proto: "FIRST"), - 2: .same(proto: "SECOND"), - 3: .same(proto: "THIRD"), - 4: .same(proto: "REFLEXIVE_PERSON"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Proper: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "PROPER_UNKNOWN"), - 1: .same(proto: "PROPER"), - 2: .same(proto: "NOT_PROPER"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Reciprocity: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "RECIPROCITY_UNKNOWN"), - 1: .same(proto: "RECIPROCAL"), - 2: .same(proto: "NON_RECIPROCAL"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Tense: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "TENSE_UNKNOWN"), - 1: .same(proto: "CONDITIONAL_TENSE"), - 2: .same(proto: "FUTURE"), - 3: .same(proto: "PAST"), - 4: .same(proto: "PRESENT"), - 5: .same(proto: "IMPERFECT"), - 6: .same(proto: "PLUPERFECT"), - ] -} - -extension Google_Cloud_Language_V1_PartOfSpeech.Voice: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "VOICE_UNKNOWN"), - 1: .same(proto: "ACTIVE"), - 2: .same(proto: "CAUSATIVE"), - 3: .same(proto: "PASSIVE"), - ] -} - -extension Google_Cloud_Language_V1_DependencyEdge: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".DependencyEdge" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "head_token_index"), - 2: .same(proto: "label"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.headTokenIndex) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.label) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.headTokenIndex != 0 { - try visitor.visitSingularInt32Field(value: self.headTokenIndex, fieldNumber: 1) - } - if self.label != .unknown { - try visitor.visitSingularEnumField(value: self.label, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_DependencyEdge, rhs: Google_Cloud_Language_V1_DependencyEdge) -> Bool { - if lhs.headTokenIndex != rhs.headTokenIndex {return false} - if lhs.label != rhs.label {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_DependencyEdge.Label: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "ABBREV"), - 2: .same(proto: "ACOMP"), - 3: .same(proto: "ADVCL"), - 4: .same(proto: "ADVMOD"), - 5: .same(proto: "AMOD"), - 6: .same(proto: "APPOS"), - 7: .same(proto: "ATTR"), - 8: .same(proto: "AUX"), - 9: .same(proto: "AUXPASS"), - 10: .same(proto: "CC"), - 11: .same(proto: "CCOMP"), - 12: .same(proto: "CONJ"), - 13: .same(proto: "CSUBJ"), - 14: .same(proto: "CSUBJPASS"), - 15: .same(proto: "DEP"), - 16: .same(proto: "DET"), - 17: .same(proto: "DISCOURSE"), - 18: .same(proto: "DOBJ"), - 19: .same(proto: "EXPL"), - 20: .same(proto: "GOESWITH"), - 21: .same(proto: "IOBJ"), - 22: .same(proto: "MARK"), - 23: .same(proto: "MWE"), - 24: .same(proto: "MWV"), - 25: .same(proto: "NEG"), - 26: .same(proto: "NN"), - 27: .same(proto: "NPADVMOD"), - 28: .same(proto: "NSUBJ"), - 29: .same(proto: "NSUBJPASS"), - 30: .same(proto: "NUM"), - 31: .same(proto: "NUMBER"), - 32: .same(proto: "P"), - 33: .same(proto: "PARATAXIS"), - 34: .same(proto: "PARTMOD"), - 35: .same(proto: "PCOMP"), - 36: .same(proto: "POBJ"), - 37: .same(proto: "POSS"), - 38: .same(proto: "POSTNEG"), - 39: .same(proto: "PRECOMP"), - 40: .same(proto: "PRECONJ"), - 41: .same(proto: "PREDET"), - 42: .same(proto: "PREF"), - 43: .same(proto: "PREP"), - 44: .same(proto: "PRONL"), - 45: .same(proto: "PRT"), - 46: .same(proto: "PS"), - 47: .same(proto: "QUANTMOD"), - 48: .same(proto: "RCMOD"), - 49: .same(proto: "RCMODREL"), - 50: .same(proto: "RDROP"), - 51: .same(proto: "REF"), - 52: .same(proto: "REMNANT"), - 53: .same(proto: "REPARANDUM"), - 54: .same(proto: "ROOT"), - 55: .same(proto: "SNUM"), - 56: .same(proto: "SUFF"), - 57: .same(proto: "TMOD"), - 58: .same(proto: "TOPIC"), - 59: .same(proto: "VMOD"), - 60: .same(proto: "VOCATIVE"), - 61: .same(proto: "XCOMP"), - 62: .same(proto: "SUFFIX"), - 63: .same(proto: "TITLE"), - 64: .same(proto: "ADVPHMOD"), - 65: .same(proto: "AUXCAUS"), - 66: .same(proto: "AUXVV"), - 67: .same(proto: "DTMOD"), - 68: .same(proto: "FOREIGN"), - 69: .same(proto: "KW"), - 70: .same(proto: "LIST"), - 71: .same(proto: "NOMC"), - 72: .same(proto: "NOMCSUBJ"), - 73: .same(proto: "NOMCSUBJPASS"), - 74: .same(proto: "NUMC"), - 75: .same(proto: "COP"), - 76: .same(proto: "DISLOCATED"), - 77: .same(proto: "ASP"), - 78: .same(proto: "GMOD"), - 79: .same(proto: "GOBJ"), - 80: .same(proto: "INFMOD"), - 81: .same(proto: "MES"), - 82: .same(proto: "NCOMP"), - ] -} - -extension Google_Cloud_Language_V1_EntityMention: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".EntityMention" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - 2: .same(proto: "type"), - 3: .same(proto: "sentiment"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._text) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._sentiment) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._text { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.type != .unknown { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 2) - } - try { if let v = self._sentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_EntityMention, rhs: Google_Cloud_Language_V1_EntityMention) -> Bool { - if lhs._text != rhs._text {return false} - if lhs.type != rhs.type {return false} - if lhs._sentiment != rhs._sentiment {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_EntityMention.TypeEnum: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "TYPE_UNKNOWN"), - 1: .same(proto: "PROPER"), - 2: .same(proto: "COMMON"), - ] -} - -extension Google_Cloud_Language_V1_TextSpan: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".TextSpan" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "content"), - 2: .standard(proto: "begin_offset"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.content) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.beginOffset) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.content.isEmpty { - try visitor.visitSingularStringField(value: self.content, fieldNumber: 1) - } - if self.beginOffset != 0 { - try visitor.visitSingularInt32Field(value: self.beginOffset, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_TextSpan, rhs: Google_Cloud_Language_V1_TextSpan) -> Bool { - if lhs.content != rhs.content {return false} - if lhs.beginOffset != rhs.beginOffset {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_ClassificationCategory: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClassificationCategory" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .same(proto: "confidence"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 2: try { try decoder.decodeSingularFloatField(value: &self.confidence) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - if self.confidence != 0 { - try visitor.visitSingularFloatField(value: self.confidence, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_ClassificationCategory, rhs: Google_Cloud_Language_V1_ClassificationCategory) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.confidence != rhs.confidence {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_ClassificationModelOptions: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClassificationModelOptions" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "v1_model"), - 2: .standard(proto: "v2_model"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model? - var hadOneofValue = false - if let current = self.modelType { - hadOneofValue = true - if case .v1Model(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.modelType = .v1Model(v) - } - }() - case 2: try { - var v: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model? - var hadOneofValue = false - if let current = self.modelType { - hadOneofValue = true - if case .v2Model(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.modelType = .v2Model(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.modelType { - case .v1Model?: try { - guard case .v1Model(let v)? = self.modelType else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .v2Model?: try { - guard case .v2Model(let v)? = self.modelType else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions, rhs: Google_Cloud_Language_V1_ClassificationModelOptions) -> Bool { - if lhs.modelType != rhs.modelType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_ClassificationModelOptions.V1Model: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Google_Cloud_Language_V1_ClassificationModelOptions.protoMessageName + ".V1Model" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model, rhs: Google_Cloud_Language_V1_ClassificationModelOptions.V1Model) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Google_Cloud_Language_V1_ClassificationModelOptions.protoMessageName + ".V2Model" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "content_categories_version"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.contentCategoriesVersion) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.contentCategoriesVersion != .unspecified { - try visitor.visitSingularEnumField(value: self.contentCategoriesVersion, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model, rhs: Google_Cloud_Language_V1_ClassificationModelOptions.V2Model) -> Bool { - if lhs.contentCategoriesVersion != rhs.contentCategoriesVersion {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_ClassificationModelOptions.V2Model.ContentCategoriesVersion: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "CONTENT_CATEGORIES_VERSION_UNSPECIFIED"), - 1: .same(proto: "V1"), - 2: .same(proto: "V2"), - ] -} - -extension Google_Cloud_Language_V1_AnalyzeSentimentRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeSentimentRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "document"), - 2: .standard(proto: "encoding_type"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.encodingType != .none { - try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSentimentRequest, rhs: Google_Cloud_Language_V1_AnalyzeSentimentRequest) -> Bool { - if lhs._document != rhs._document {return false} - if lhs.encodingType != rhs.encodingType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnalyzeSentimentResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeSentimentResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "document_sentiment"), - 2: .same(proto: "language"), - 3: .same(proto: "sentences"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._documentSentiment) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.language) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &self.sentences) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._documentSentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.language.isEmpty { - try visitor.visitSingularStringField(value: self.language, fieldNumber: 2) - } - if !self.sentences.isEmpty { - try visitor.visitRepeatedMessageField(value: self.sentences, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSentimentResponse, rhs: Google_Cloud_Language_V1_AnalyzeSentimentResponse) -> Bool { - if lhs._documentSentiment != rhs._documentSentiment {return false} - if lhs.language != rhs.language {return false} - if lhs.sentences != rhs.sentences {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeEntitySentimentRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "document"), - 2: .standard(proto: "encoding_type"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.encodingType != .none { - try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest, rhs: Google_Cloud_Language_V1_AnalyzeEntitySentimentRequest) -> Bool { - if lhs._document != rhs._document {return false} - if lhs.encodingType != rhs.encodingType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeEntitySentimentResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "entities"), - 2: .same(proto: "language"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.entities) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.language) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.entities.isEmpty { - try visitor.visitRepeatedMessageField(value: self.entities, fieldNumber: 1) - } - if !self.language.isEmpty { - try visitor.visitSingularStringField(value: self.language, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse, rhs: Google_Cloud_Language_V1_AnalyzeEntitySentimentResponse) -> Bool { - if lhs.entities != rhs.entities {return false} - if lhs.language != rhs.language {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnalyzeEntitiesRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeEntitiesRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "document"), - 2: .standard(proto: "encoding_type"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.encodingType != .none { - try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeEntitiesRequest, rhs: Google_Cloud_Language_V1_AnalyzeEntitiesRequest) -> Bool { - if lhs._document != rhs._document {return false} - if lhs.encodingType != rhs.encodingType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnalyzeEntitiesResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeEntitiesResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "entities"), - 2: .same(proto: "language"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.entities) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.language) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.entities.isEmpty { - try visitor.visitRepeatedMessageField(value: self.entities, fieldNumber: 1) - } - if !self.language.isEmpty { - try visitor.visitSingularStringField(value: self.language, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeEntitiesResponse, rhs: Google_Cloud_Language_V1_AnalyzeEntitiesResponse) -> Bool { - if lhs.entities != rhs.entities {return false} - if lhs.language != rhs.language {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnalyzeSyntaxRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeSyntaxRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "document"), - 2: .standard(proto: "encoding_type"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() - case 2: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.encodingType != .none { - try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSyntaxRequest, rhs: Google_Cloud_Language_V1_AnalyzeSyntaxRequest) -> Bool { - if lhs._document != rhs._document {return false} - if lhs.encodingType != rhs.encodingType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnalyzeSyntaxResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnalyzeSyntaxResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "sentences"), - 2: .same(proto: "tokens"), - 3: .same(proto: "language"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.sentences) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.tokens) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.language) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.sentences.isEmpty { - try visitor.visitRepeatedMessageField(value: self.sentences, fieldNumber: 1) - } - if !self.tokens.isEmpty { - try visitor.visitRepeatedMessageField(value: self.tokens, fieldNumber: 2) - } - if !self.language.isEmpty { - try visitor.visitSingularStringField(value: self.language, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnalyzeSyntaxResponse, rhs: Google_Cloud_Language_V1_AnalyzeSyntaxResponse) -> Bool { - if lhs.sentences != rhs.sentences {return false} - if lhs.tokens != rhs.tokens {return false} - if lhs.language != rhs.language {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_ClassifyTextRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClassifyTextRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "document"), - 3: .standard(proto: "classification_model_options"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._classificationModelOptions) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._classificationModelOptions { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_ClassifyTextRequest, rhs: Google_Cloud_Language_V1_ClassifyTextRequest) -> Bool { - if lhs._document != rhs._document {return false} - if lhs._classificationModelOptions != rhs._classificationModelOptions {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_ClassifyTextResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClassifyTextResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "categories"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.categories) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.categories.isEmpty { - try visitor.visitRepeatedMessageField(value: self.categories, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_ClassifyTextResponse, rhs: Google_Cloud_Language_V1_ClassifyTextResponse) -> Bool { - if lhs.categories != rhs.categories {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnnotateTextRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnnotateTextRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "document"), - 2: .same(proto: "features"), - 3: .standard(proto: "encoding_type"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._document) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._features) }() - case 3: try { try decoder.decodeSingularEnumField(value: &self.encodingType) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._document { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._features { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if self.encodingType != .none { - try visitor.visitSingularEnumField(value: self.encodingType, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnnotateTextRequest, rhs: Google_Cloud_Language_V1_AnnotateTextRequest) -> Bool { - if lhs._document != rhs._document {return false} - if lhs._features != rhs._features {return false} - if lhs.encodingType != rhs.encodingType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnnotateTextRequest.Features: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Google_Cloud_Language_V1_AnnotateTextRequest.protoMessageName + ".Features" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "extract_syntax"), - 2: .standard(proto: "extract_entities"), - 3: .standard(proto: "extract_document_sentiment"), - 4: .standard(proto: "extract_entity_sentiment"), - 6: .standard(proto: "classify_text"), - 10: .standard(proto: "classification_model_options"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.extractSyntax) }() - case 2: try { try decoder.decodeSingularBoolField(value: &self.extractEntities) }() - case 3: try { try decoder.decodeSingularBoolField(value: &self.extractDocumentSentiment) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.extractEntitySentiment) }() - case 6: try { try decoder.decodeSingularBoolField(value: &self.classifyText) }() - case 10: try { try decoder.decodeSingularMessageField(value: &self._classificationModelOptions) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.extractSyntax != false { - try visitor.visitSingularBoolField(value: self.extractSyntax, fieldNumber: 1) - } - if self.extractEntities != false { - try visitor.visitSingularBoolField(value: self.extractEntities, fieldNumber: 2) - } - if self.extractDocumentSentiment != false { - try visitor.visitSingularBoolField(value: self.extractDocumentSentiment, fieldNumber: 3) - } - if self.extractEntitySentiment != false { - try visitor.visitSingularBoolField(value: self.extractEntitySentiment, fieldNumber: 4) - } - if self.classifyText != false { - try visitor.visitSingularBoolField(value: self.classifyText, fieldNumber: 6) - } - try { if let v = self._classificationModelOptions { - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnnotateTextRequest.Features, rhs: Google_Cloud_Language_V1_AnnotateTextRequest.Features) -> Bool { - if lhs.extractSyntax != rhs.extractSyntax {return false} - if lhs.extractEntities != rhs.extractEntities {return false} - if lhs.extractDocumentSentiment != rhs.extractDocumentSentiment {return false} - if lhs.extractEntitySentiment != rhs.extractEntitySentiment {return false} - if lhs.classifyText != rhs.classifyText {return false} - if lhs._classificationModelOptions != rhs._classificationModelOptions {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Google_Cloud_Language_V1_AnnotateTextResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".AnnotateTextResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "sentences"), - 2: .same(proto: "tokens"), - 3: .same(proto: "entities"), - 4: .standard(proto: "document_sentiment"), - 5: .same(proto: "language"), - 6: .same(proto: "categories"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.sentences) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.tokens) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &self.entities) }() - case 4: try { try decoder.decodeSingularMessageField(value: &self._documentSentiment) }() - case 5: try { try decoder.decodeSingularStringField(value: &self.language) }() - case 6: try { try decoder.decodeRepeatedMessageField(value: &self.categories) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.sentences.isEmpty { - try visitor.visitRepeatedMessageField(value: self.sentences, fieldNumber: 1) - } - if !self.tokens.isEmpty { - try visitor.visitRepeatedMessageField(value: self.tokens, fieldNumber: 2) - } - if !self.entities.isEmpty { - try visitor.visitRepeatedMessageField(value: self.entities, fieldNumber: 3) - } - try { if let v = self._documentSentiment { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - if !self.language.isEmpty { - try visitor.visitSingularStringField(value: self.language, fieldNumber: 5) - } - if !self.categories.isEmpty { - try visitor.visitRepeatedMessageField(value: self.categories, fieldNumber: 6) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Google_Cloud_Language_V1_AnnotateTextResponse, rhs: Google_Cloud_Language_V1_AnnotateTextResponse) -> Bool { - if lhs.sentences != rhs.sentences {return false} - if lhs.tokens != rhs.tokens {return false} - if lhs.entities != rhs.entities {return false} - if lhs._documentSentiment != rhs._documentSentiment {return false} - if lhs.language != rhs.language {return false} - if lhs.categories != rhs.categories {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/Google/NaturalLanguage/Sources/main.swift b/Examples/Google/NaturalLanguage/Sources/main.swift deleted file mode 100644 index a98529d17..000000000 --- a/Examples/Google/NaturalLanguage/Sources/main.swift +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 Dispatch -import Foundation -import GRPC -import NIO -import NIOHPACK -import NIOHTTP1 -import NIOSSL -import OAuth2 - -/// Create a client and return a future to provide its value. -func makeServiceClient( - host: String, - port: Int, - eventLoopGroup: MultiThreadedEventLoopGroup -) -> Google_Cloud_Language_V1_LanguageServiceNIOClient { - let connection = ClientConnection.usingPlatformAppropriateTLS(for: eventLoopGroup) - .connect(host: host, port: port) - return Google_Cloud_Language_V1_LanguageServiceNIOClient(channel: connection) -} - -enum AuthError: Error { - case noTokenProvider - case tokenProviderFailed -} - -/// Get an auth token and return a future to provide its value. -func getAuthToken( - scopes: [String], - eventLoop: EventLoop -) -> EventLoopFuture { - let promise = eventLoop.makePromise(of: String.self) - guard let provider = DefaultTokenProvider(scopes: scopes) else { - promise.fail(AuthError.noTokenProvider) - return promise.futureResult - } - do { - try provider.withToken { token, error in - if let token = token, - let accessToken = token.AccessToken { - promise.succeed(accessToken) - } else if let error = error { - promise.fail(error) - } else { - promise.fail(AuthError.tokenProviderFailed) - } - } - } catch { - promise.fail(error) - } - return promise.futureResult -} - -/// Main program. Make a sample API request. -do { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - // Get an auth token. - let scopes = ["https://www.googleapis.com/auth/cloud-language"] - let authToken = try getAuthToken( - scopes: scopes, - eventLoop: eventLoopGroup.next() - ).wait() - - // Create a service client. - let service = makeServiceClient( - host: "language.googleapis.com", - port: 443, - eventLoopGroup: eventLoopGroup - ) - - // Use CallOptions to send the auth token (necessary) and set a custom timeout (optional). - let headers: HPACKHeaders = ["authorization": "Bearer \(authToken)"] - let callOptions = CallOptions(customMetadata: headers, timeLimit: TimeLimit.timeout(.seconds(30))) - print("CALL OPTIONS\n\(callOptions)\n") - - // Construct the API request. - let request = Google_Cloud_Language_V1_AnnotateTextRequest.with { - $0.document = .with { - $0.type = .plainText - $0 - .content = - "The Caterpillar and Alice looked at each other for some time in silence: at last the Caterpillar took the hookah out of its mouth, and addressed her in a languid, sleepy voice. `Who are you?' said the Caterpillar." - } - - $0.features = .with { - $0.extractSyntax = true - $0.extractEntities = true - $0.extractDocumentSentiment = true - $0.extractEntitySentiment = true - $0.classifyText = true - } - } - print("REQUEST MESSAGE\n\(request)") - - // Create/start the API call. - let call = service.annotateText(request, callOptions: callOptions) - call.response.whenSuccess { response in - print("CALL SUCCEEDED WITH RESPONSE\n\(response)") - } - call.response.whenFailure { error in - print("CALL FAILED WITH ERROR\n\(error)") - } - - // wait() on the status to stop the program from exiting. - let status = try call.status.wait() - print("CALL STATUS\n\(status)") -} catch { - print("EXAMPLE FAILED WITH ERROR\n\(error)") -} diff --git a/Examples/Google/README.md b/Examples/Google/README.md deleted file mode 100644 index 5000450bc..000000000 --- a/Examples/Google/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Calling the Google gRPC APIs - -This directory contains samples that call selected -Google gRPC APIs. Samples are typically quite basic -and demonstrate how to directly call gRPC APIs from -generated client support code. In practice, this code -would be wrapped with higher-level Swift code. - -Each sample uses protoc and the Swift Protocol -Buffer and gRPC plugins, so please be sure these are in your -path. The plugins can be built by running `make` in the -top-level Plugins directory. - -Calls to Google APIs require a Google project ID, -API activation, and service account credentials. - -1. To create a project ID, visit the -[Google Cloud Console](https://cloud.google.com/console). -Your selected project ID should be shown in the top bar just -to the right of the **Google Cloud Platform** label. Click -on this to change projects or create a new one. - -2. To activate an API, visit the Google Cloud Console, -go to the **APIs & Services** section and use its **Library** -subsection to lookup the API and click on the **Enable** -button. If you instead see a button labeled **Manage**, -the API is already activated. - -3. To create service account credentials, again visit -the Google Cloud Console, go to the **APIs & Services** -section and use the **Credentials** subsection. When you -create these credentials, you'll be prompted to download -them. Do that, and then set the `GOOGLE_APPLICATION_CREDENTIALS` -environment variable to point to the file containing -your credentials. - -Service account support is provided by Google's -[Auth Library for Swift](https://github.com/google/auth-library-swift). -To learn more about service accounts, please see -[Understanding Service Accounts](https://cloud.google.com/iam/docs/understanding-service-accounts). \ No newline at end of file diff --git a/Examples/Google/SpeechToText/.gitignore b/Examples/Google/SpeechToText/.gitignore deleted file mode 100644 index c99bab229..000000000 --- a/Examples/Google/SpeechToText/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -Pods -Sources/Generated -google -googleapis diff --git a/Examples/Google/SpeechToText/Images/Resized.gif b/Examples/Google/SpeechToText/Images/Resized.gif deleted file mode 100644 index bb73f70fbece4f2573dec56f08483a31c58a3c17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 399857 zcmeFZ2UJt*x-L9Pok2=KKsupHQ==kX483bqM4A{BX#&QENDVy#LMSRt2#EA1AYG)1 z^e)nbh$x73=^(kRwa(sWpL5pP_w4^4_l|S#-5D7pVlH=SH=-gLfs^Om!dvx|eX%S~5TM;F(dF1K&pzJIUu zehbCj&HbT!h_|PAfL}mRKy66yb8;~GQAl`rX!zr>%8192k&h{qh}dXKbTlP4Hs(n} zd}3lkP4e^Pl;o7ul++igFJ7kSWMxj|WaktVG!_;V78ezlmsQkMzj;$T{r1h<`nsl; z=9bQm&d$#H-k#pUfx)4{q2ZxVpT;I8KTm!A@^xl?|Gk zoa1MU`B5$RNUc)homxhH!lS4+t7nERDm+2W6}~DqekgPZ2dtHmWTC+8%fE zdcnxhO@9QlnlB48@2W%Zqihj*qvc#H8FMk`ozpG`)iD%+j%{)Xd`x%i1R(FShTTKZxe^}S(_%bQt88Y zoH|49-<}$~ky=D!nAH9Fee^Kua5t?Jj|4bo7&1hd31-$X`Ic9pcz~qROtimU&i9NY zx7qKR)L`k^tn{Z&v)Ng9a?~-gW-H|INId-xN<oqvk?yBc}kt z%rEo3U4m>DXAgrue+>6$07KTJ__0N`u%XgO_x#8r983lb9DZ8S`VvpVqh_n-%6=?V zFE!^%l|>XWG;!N;97l-6q({x6jq&xcgZkLsM&bLbJ&gzVFZUg%=+PgJNCD8`_B*cY zvJB+Nfv9FNZ4=ybb1;8&fS8+eUZ%*P0p`8fv{N)nM8K9(|4wn*`Dd5v#@x?t9P-F& zkJcfV)n2{B`K7R13v8|8ae60O&=*YzB{1MXQs6<8IX{%Bht@Kzo>k#?@H=%&f?ubT zJ=JZL$4nL!k@L!HYQCMq*2;1C3*E*KA!Arn#(X#Jc&oUhS@qA+aD|1L2;M7Pi})}vCB;;H%XC4JQgRXm`>iIMJxL2 zi9VCO1``UnMiDFdK9_!5(`Hh1h0XT)ljoEaQ+-XoDMs<|h5Q#N- z2FJ%SgEZjRU}V@eMSzNR!qhnr2vaJktJlDuuoNh1l*)LyrGcwzDF~ZH;$XQP@-_$$ z4eaMEOanjy{H7V{t|b#)pX`T9s%O)Q!?A^N^~!Vi@by8Lri_$aAN0mAAY* z>aiSVS=uO+!>z%jXLI(h$Hjw#EzNS`Y@VhjX`CCTjn zLF%_?@UBqEM(HAlT3dA1dntT{ff zj9yA#Zf!HIx)8-rekp6O-)=Fo^0cDk<mup#;#*LZ)%wHRfbc-Gos zr~EU%Cp<$gQ@_*6;%CB8M}~ZPYp1Kn&qNwirs8}3t~&`olV(=M!NJxpx2m7dm%=ku zr}eu%Mt&x5c4Vq;w9>nMwtuDo%vm_3L61N0Dit)&(m2%C6Rfb=aUc)M7Oa z^!C#^HPUq`um6Q0oIy{wsy8}eHC@s;+u(9r-_xqqm)OVIM)n5%2_teDBA>GK9!U42 zV+S(%nAMs583zspQnPf$XLYXQOrdFV$|Z6_S|q2uQ=B2Hy1| zvT@$EL+wM&%ImMfzDnC3HZ*UErWMEK=h-Q=4|jR2mr$8qKy2c0J7TajS-k%I`RdCb zLI(>Ayz`xHR7U#fBZH;s`SULN?W5z{>*aO7@?Ed<`Ac(`GoCSX;cOv{@eH^jY%8EV zN$sQJHJ@27aXZ^t8d{A-QyByGTphzat-c)fD@=c`{_t`8=m(Ey)%A}Hy`~K(wigC# zH#-Y`rmH{dnur+1Y-)l&Sd>8^iv7}NfoPM~BOO`cl3VDQ)CF|FdX9W~?h(J6$8v3$ zWBk}J(6DybDP)NKHTD=x6Y;nqg5zofyX09SO0L6(w~&ActY{3~X#XO(v-uvkupDlP z4^oh|LD{<3=ejRJHBGLeV0(?MIRHv`0K*n-2EBLqHuy<7jqOxiy^zSVzcl|A^YEKF z*6}68ax{s~Glo(?+Mr==^@xbGx6)avvok`$$(#gdO7!6S2nmD-f}U&iO3Miz=~S|woFYQp%&*L_ zXIeaIBuY{ZW0eZXp zx0{a9WZJ_#6590}RFr`6oVXprJLbo3*y8qXk@RS)cI;yyxrwR8#r0kchCP^E&)y^u zpo6`}7D}#XqKw}nS^k<$E2tPO?^-MH{PlytT0K;5?QiyshR%w3^4jr=8%Kx~d&iq5 zK1tVQqo48U7z&J)J(YZjSj`@3x8Z+iImA%0fsI1d;^gWjqhk|cP8Cf&1~|l-A4zbT z8F>`PXENj4`jwN`45%&ve{mBL_K?CRp<2U7`cqyA3Rdgw`?EVEqkHQW-ES5ibl>ON z#jzKsFtl9!w2@d?KOBRF@w6-ju*<5mhhWqz=@=SMcU{ZZT1|-@3R%ZwlhsYIpS}{m zvvNvy;$x_L;Od6ev?$(s_Gv*P|WvtT>-V0N?HhBz={3F)AFt@zo) zXzZ0JB64~9&aDlk=H=UlxY;}9MTjpAJXr+e)rCAjqaIN`$abX9*8!yE=dTRjtifj@%)?>HV=j`aYwEwMwW3$ND?DA<032KB6o)( z_ckK|9ttEoa)c|O1@YjMB4yJ70<=;#$=8eDLk^gSZIB}ley4mVM{ScMSoEVfTcd=s zDcspn(^(V=DN46{wB+|F>EY<3Tha6u(wQQ!uPxD76h(?oh)}4EQ5}v^+lrBnhiE9p zYNBFwlqh1Yky@3p+7Ba*A4N)$q77POP0XK|O2wXi_{1#!iQyy2`G-$*d7d&>KH1}b zVqFmvGwHP@U#2zk#4QeemOkT(T{V; zeq!?d2{}GaD?83?I4-O;?wnFw1W!Bx6=z!+_oOnO;u!yYD^7bWK6&d&s(AwSd;F7! z3F(gU*-8nQh7$^S5{s1*OU)C@9wt`AC+0nhE6Pr+-Ab(ENor6^YBEo1ewfq}pVU^F z)G?gYwUyMv^Zco!;-8Q3*us0Bk8eGv@gz^um6E5-lfOMoo{3MMtxTR9 zPF~nbUgAkv8Ga7Ir>s9r*^E!wu1wh-PTAW^0eGnpWh%^qitwO<2~@@^D$@v+Wt)oT zO=VY3<*-QQ@<`=QNad|cuzBr-$ zLeAoayvGa0gcr(HFH}cfsBOQ%@uq7ir)ycH>v*K=C8Qfvr5lZ;pWRN!^S->G{L;kY zrK!hDvxJuxRWGeZURrOzB=BZjQ_irp$guOsa7f5-s>*O4$#C7yAo6D3QO>+)k?H1< z>7J13QI+X6lIgRZN#f1&SI!Ew$O`tzA}3^pR%M0LN3tTevnagT(aPDe7THfdvf~o6 z6RNV4MzWK)v#Go}Y05e27C9LnIavugIaN7%BRK`zIYqp=#mc#*7P(~}xfKbyRaLn) zBe}KPxplmG4a#{<7J1Dcc`XTfZB=<4BY9ohc|E-OeaiU*7Wso7`9lf$BUSlhBl+Xo z`83{wN#%kmi-K<+1v3c+vsDFiBLxfF1xvhzE6Rnd7KQ5`g_{Y5+f{|TBZYh0g#cd> zM5PF3S%mN`0uzfEtBaUMi&%Dw(0s4hRbFvezT)zH#hv(yxB3FQ$H(c+^!#aO9Z&l1JN66NX=)zK2Qoe~^hsfJ3amSw4q zXQ^IdsX=wA(P-(}ol-pC>kBHcO)OuVdcHPGd~H$v+G_N*_0DSoU)eR4GF!_sJI^wQ z#4@MqGUw4U*PSvVU-=!C@_Ux$Zl2}tiRB*EgWzuM6@=hg{uPRNYD&4Xw!?P+Yu_~v!DsQx^ zV5h2xuew;Jy413|%(J>8vAU|dx@NSxcBi_IuckqzrpdCV*|Vl4v8JuMren0GYp14% z?@gb|n*qx=gPw1O65otezZn~SGrseNM(3-Y49kMz0Aw97lUO@jT{|~gyRcKc#P@bZ zKJS4n8xZ@cI(jm_3Wzk99H#Q zUiI8b^}NpYrT|=<2!$`D!AMX%6>d#zkeq`Phz&9|4br;}$L1Q2j5Ww6HJo5-l;dwa z<<&@_G#<5TROW9|%WKf|YLx6~R7q+&Zqnc ziRryo(L2dds5!cYoYWFZuW1QSY9UadfmUfqEc~HY(?#dzGfa)Qy;>8UTjM%fsY$Kz zbFIdCt*KtkY2j^D)#mKo)^wxhqj~U)`1j(@mt1z$YS4a2GT$fZHe9|awYPYjW7C|7w zReO3a6v406wJ(MDgc}v7PUaoK8D}8_@22zoOcI^Bx?- zjEF2k60nGSSQHWe;XVOrhih^qAYEw<*J%hh3PL6v;ZTQgqrvU)$h!oDM*e`|uV&pN zX-MXt_3)2}pLfqBeT2BQ${m59?bNX(_M0#x95AT+Bm_Cc-R1}wRR_LOKmTfb|K#!&@LTC(H1h^di zXXspmDIO+R*CFPc0+OH_RG87k>RI|KUTE#YnKhHEYo@+yW+`hHZ`Z6Q)~t@f6o|my z&DFzBt5D*)L(00-+jZxObyxa2QE21NnT>l_H{5(T+*3Ag-b%5JOtYuI^(b5?(Kq}i z)&tJ02SsiMMy`hxZiGy1J}TUd7TSuTZ>|k*NQZ8HsoqMO*h;2vQH8eC&TOY&-Oljc z&Pv(NdAprAv0XslE)v=)KC@GLb*IdCM?eKIpdtNxc53N6bwaxhXLjp^z-HgwmXzJL zx4Rt^yItK#1KcLn02)C-B@llNrTiL6`8A#0_iLQ~YuX<*>5DpMiu!gAHIuS8`*v?` zVsC-Ix6lpBQMVFupwUIYHY4fBp?ed@_P$)*n`Wgi{r>#Uic-eW;~CMgA*_c9`ht1@ z$a&OusE8^`W)qcnnj%kXhx43rof=|$ktPR2fEe?>Dg9WfO9i&WeP0dZj^F4?H}C&u zoTPMrVQRR4`W#g~h(+9DVCHv(CRf`_dvBhx<+Bg(LTc+kN%c6vDM% z{Qw}Gfsxtm^QbB^$NUx(36}Qom zE$Me!=YoFM)#3l?t=XUKAK!TScOCuDC;KYxT^RRCr?Hk-FWyIDG(-9Ii_=?UBroPz zwHCi@e}=u@mZV>j(V3`tcXq6`B(pmO7m#d)KCVteoOvvwC0&}`2{QBI^&Gy~1%S{e zEJ-7Nn4s*ljm0(Bu#X!>&~GDbo$yZTNQeyixo7c@bOZPi@4ULp>0`&?GDgeLM^vjS z@+CLnEt($=#V)8RsK|3Ys0DuNGz&%em3?&3F*rJq>#jyGG)AkVC;qPeU>@Aj z(P4%tb`+qlPfuW=@T@6`RHu*cSn*rd zmk8DMnJY>NoPE*>8v;bIOKM@He};vy5cC3QWd?m1;EIvSuQ7(406^9gxpD`quV z)1)qlzV4fv)s*GdlGbqP!d+xOf>6OQLZog&47(XQ@=@KzR9W3Y1i_^YdSx`%u){R4 zp#mBuGmq;8aT5IwFHd@pldD<=kTE*2ksi6+%>*^aj{#QarF!si-ckdT z!rW3LT32?tiQ}^K@;h$(yya&82Xo8sF(I-mEn?4{S6U@A^H$nq%jZ_wvF~MncF28n z{@JNGo%d%t`f8t%!u)DKUiZk_fXQW-wGU?Y`D=q#59ZfC5<-rw57|C*Ss!-D%wHdI zE}vf?CB8qhF?R2x%f=`7>HLjxuMPVA#smp@bdwf%$aV8G`EbGJzwsE|V~bqM7pKXf=1c37{T(PGJ;yMu~QXsKuMSPH=WxsD#5 z%4DzC$UCwWtk{vt@}Q+raC<2P$CQSqhv+ql@Gg_}jMCVjwKPd6FF(SEr*UNJy_2?B z4mIoetBy`!dcpl(ulYp6a=3%h3*L_{&GJ>tkBQ+g_^1ES(cL>liHszm9)$rE&}2>) zWH3PbOo2$Hi*P!&XvwccJ(5iqW*gSiKfxXqo|i6))M(YTSc!UeZ|bm;RI9$uN=(>5 zy0oEVn_lvT*o=;JSyW%^S^whbR4ef#<|vMH|Cx^d%=q(5!OYfrql^xyID3G44J{}} z|D@_CSR)tM(AjwZpzy>!OJ%q}^2}WEt@x$^T-C!a|G*MgFThBEvAE(~K)B6whMCME z7CE$zKogZsiivm0^jx57gRE^MUc)^IeI&~z32}`qnh`_skS}$HiR;<4JfbKAcCMdP zziaA*QZ(-kIE@&A{1C$VCtNUUq>-lb?a)(U(oLDpI}REoUbfVy#}qccCT6b{Alh%i=3 zz7!!mtbhw)fe2|vsBWbNG9gZ)nyi#wO;du7!@2Ldo!n36@mi$>b<7VS?k;6+>3G70(!UP)~) z201o1gw(J-6~}Qj>vJ%_+sx+>%zrhRFKlX1ID>FgQF;>DQ(8KqLt+nf)LLJ}8g?n_ zt9Ref^2c4cVBYgo@bkMK#*^p^H}u3|6bW@r##K$=iR`k3_DKu}1SQtNLwaWK!viEs z3Pzphj8bLC*~lyp+Z%;bmUVQ3B?dr$RSu3u{R>BLzf?cTf%ujtBwcl0{J;m)?70k< zbaM?`tLSBdHgX}X0ry4XW3(lLC(Rz4xSK9^Ss!g^aUv#SKupTg z+<?>!>Pfwy5KOa8REswB3 z*`~r?LA8eEd(EX_Y^Yw10cmB55_j|q_*V(OEc2dh&eC*e&F|`R7HTp~5kS`*TK9UD z(v!Z`48_juvk%((lebqham+dR5QBj<-nA?}YbO*jT~ERJFRB~c>*S#E#{*aDb;XWguSy2Q9`1NUQ>{wwSDw-xz!tlN$J zJL^Ukf&&kWP+j}1n^C}^G^_W8UX1jWw$kkWO#Gj?S3(60%kqXx9Qt#vwExw;QeN<> z`ro-%ias~~nS15tct>T)bT?HqOmN?{^1VOnVy<;()$9N0URj)|3pgNjwx(kFOG}K* z)vkYWucVxPQ?>SEsO08E*PH5%MOtGRUFckG&DPJE{@knGwQqJdmcF#7oO@gQYiIN4 zkBRQLZ}&M5LXt*;aXHZZK#44xKa>0nEdZ?}`8klo)Zudwx7|OSsQ=2ke@oPV=3aSf z<@WvSGeWS`w>aCUj^E-PGP1uVIG26@mPl-tnohbm=s5k{{af~Qve)|e=@b$|dWIUv zh%3(+@HBuGIC~f^2%msbBmhk(m(P_2c3T8SA5I) zQBbo!`=hW9Av0Ih#C3D-Rf|OKTycl|kGYZ_9hv#k0n?lFuZQe%=gY?2e$1ECf@Kye zrk>thsGP~jUHFT8MP{*Pb)R*=+5Gln2tcrtNQbGgKTXuPo073O5+K1w)0fIEg&4|~ z1pX=OE@^*NQ`{y+MPs5FUY4F{KT=K;`B(SK-?HxWQ11gr3X>*LZv`LTF_2Zi_*d4Q z=-n=xOPY*gxEk_&{SVgdR`O;|2Np+%gpIlX&bpDc>(Wd_=Jr{m=uT=Kq-aA;!YWv%xO)mpXh{lk`9!n{w(7Sv5{{v6UR{PnZ#{T^Df>z42Okpij)Lb|}}*8SOg zmPdSsKW(t=?bvnu8%P(XM>Ecs4o$v2QoK35Nn{AK*nR9{Qmwrkr*LLQxXAHU#Z_6W zS7)g2t{viTqB01Woidwi;NHG4GDrzLGbGQGDfh zhd(%TSt+|2A$@UumCvG;y<|xN?6{vthYv`|sltb0P4be_B9n-l<&QiUv zvU{5+PZzKFU%h7|cC1y-K+X11sR{2EzBklZ=R9YryRdB445QgzH&@2)=!hv#`=Q#t z@G}ALWbZ0zSSn8i4wzWLdUflSrHO~~sPK~qDh`-+Oi~<}2n-?ST|z#TC%Mjt#rmHa zr;DHW>538KyJALHJ4HR_<)QVQsU%Ka!|ixDLhP`{0kbd}ry?$=8tc=KbTk%E!4eOi z8X2)p4Lz0BkfgDby4K{__}o=Fum+haMxoS7{SAp>czBC~%Sw7B@j z;CvoSej3M?fJvu8SyAy5r|Mh~dUj*5%46ZacsR&#Rv1^ky>=UC8FW}$gLFaiXCaw$ zY{V$FBoss7RL*m}cG$4ew8B^vwka?x**?5n-@pMcnmL8SlP&;-C0S3bWzX6ij{r&b zYKHmF@m%tU!V~-%kZ2bz7-je(^-g{X^TLN~z4CnzVb^!10ZF6YSEh;^l|7FO?qwQ& znzh)d8tN=?D{ud_;IUEt=}M3kPF*=%;28#@SLpGveSE!Yz57zKkFa`H|7Q15sJp49{=M=@ib)Lihp%(ovOWgtJ495ZH zeGSYQVE7Y&(jwo)9I=2LKRedKhLiQn6APZxyMU1e%0VAhrKRhwfrTg72R83q??_O* zd-PuR#cS`9QNGbke_5;iBU0Ues$T8mC<;IVAOri~!O?%KUY(RDW3l!1EHEiN_=kFR zM&5k}^SgRAnTMm;Y9uTFhNDEiLus6+1n?d>NM9!Ys)mSJdUhR?-YfP23ktLPf%A=_ z^JcBa4Pwfp&PU8XP=j9)zZ@OF9h$>yB0je4yFib9By(>w3OZj5t&;!?(f`2(`VZo$ zYZd!Gj#@tJ;YQhpPnGDCV%3Tm#b>hl1gL_2?CXn+txWVO7N5m%Xaw)gW5?p)^}2}9 zB@gEJFpLM!z>0^iro2G;$uQYA`2O;15>khEZLl*!m}h9dnB#FVfk=E7)l3%Wp6ViH z;$Ra5d5K9Q!;TLLUlmBMu)zxjS4`fPh=_wQ9?!|5JXW-e{en|582G|)M%ng7m};%V zljj50sbRTl~dP4sz__zKEKrZNi=x`fM}ipHtgeRGF&XT3d$+QsN2NKt!dEF!3h`9v`8xCg85D2nzAUGWQnl% z!>JId3)=IXFs>7U@3<^)1Vd+DVr)(H*7x4OtAC{@4oj@@hgB1Y!+^?Dn(jOgklv8{%~M$Cx*gqa;d zA*GUz@O%Ao19Re<;regYuQmH4O1X}5W#`|rfN|7>KT(mo^G4UHJt?#8x*iNe0w6Qp zm%fKT+E~h#Vo7c|h(wd{gJj(G=#z9{1>r3N1~SeJlx?=jnN>buM*J_FYce<%`Zzs}c-vtj`^}R+nm;p2ed+ ziwoaIzr53w&C50mAeQ=_k3SJ+_)BdD@kdgg1O75q~EN>@1+^?OM{w2vtpukqXBMgn%?o1Fwym)REGR_fTL2Mcj1#Zz+XCf?YNX|Fy03S8N&O+rNI#rRJoZN@Xy7~-(TGP z_f5)+o`!!ZDX>58k#+iVVIA;!vCq^34<_IHn;NL~e({ZCpT7VI430$9ljSwnIS9Oz zQ%)gC++#F0zo;Q|pZwL8!6N_U->{~C9VGO}AY&b_y7c`)0LMK8ZS$AsWltFloXsD$ z|C@s3zmEA|cOr0qBEPe-zXblDs~Z0%@R7V)+;KDl(w{@}FM-dedJp(L@DH0}6iznNcu5x9;%HKMGS`!G7IXv)%ci&>8j&* zcjpt(XDYr*Uq~0@bP^PcA1bh+CAd@Dc#Pd-yLIk~ky)m?bCHBGI4i*`5} z`6$iN*N^`7FwI$tBhDWO*KwoyL6w^@{>;Ho3I36%x&NtZ{2f)}zi;6GR!gK$`u|o- zWdE&}D5_;RT23{_(QdNf@Bbz6|A~+PKk+fa5AjcY{GW?||9!*cZ}5@zZ}5>c@K+AG zuMYh0z^vbej{iL;`X@O46CD3{NdEtQqyE1Djv(+)aQr7Y{=Xe2{}+Jce{|I%0o}hC zP5)@v5cxk>h5nw=)bOZ#MZaygD2iQ`&8#pEx~3b-MJ@tVA$e z`3Yel7h0d}j9w)}86Bj6BOdY^<|Q0m)~9n{AMMKF2%Y6Vfz26CgM=)-zOB{B#z)>^ zzcTkxcqAZD)P#ab)j;~$i?MPzePE!8UWBQY8S-X)l|l6LXOjl~U<0WrX<_csyqQu* z8EJNhCcipH2f>L>XlaVR27UWGF072v(WZRRDXyTR&EjTZ8A1kK z*tBQiT+o83&VALfoiJC_d33)r-mt!3gRQgk?BOlY*h%L=AK8}HImo@_28%>uF zK>TQTm=FwC&V$J1v8Y~I(fpr%MtoWj6f_94Hn4n}`F?+!xyu^cMZ?Zhy^t#cr}PMh z%xfPALaA&0(C2T~2gOsEUww2cIo-qVB&4;WZK6wmzWx!y<^r-_==)lK&Z4AeTK@ks0}UjlmM-kCSk}{E4qSj=xdZzKea;6>t2t=Wz%;!AxG^J!t$UbUp3zjt!Qnlq-N%DkrK>$q-zHXJ znsGBz@iV;FXw+ert3Ud5t+sGFA+(x})XY!HD>j;il)KY=-tQ-1y%%TGFGJQir<}y~ zEI+f`P!uqME@-Xib7FexATRoUYRssH1b6)b4_i3YoB=#1k~$~gXaAT;(*((Co%Lri z34ARguxkzeKq2EPcblQ`!>6;h&N}0v0%&^oeb}$#k(e;t09yn%l$F;ukex@ELsJsX zrnU@l-i3()WS8;xGy#nRaR;2tFi1PshD+! zZf+bTcP|Y#oS`n~0gG@e#q{RQ=pG-*3espwH&a)~eW0KteM&J0PC07XJK_B*I9?)7 zI~bo#xcGc_-*mK`rE|rD;*wLEb^Jz_euxv~xpYVdTY$8B(kz8TRV$}(uEnUF_k3SE zEtl|m7T1(RDRg^+AqarR{nF^njD91=UeTT!wLHh8<-3B8y1XB@@`Zt^8k0AwU2#M z{@0Rs?S;zvcdmw|GU$a2t?IV5f(r=DI63yR_;morfxrO>ejEmt)hMV#41{3z&Cxsn z?=CGs6i`PyVipF#Ed(%xA-o8UY=}C9S{+T?jSS(P8aTqu* zP}G4K2o0ybQCT<|^$U2^W8}x~k10f|;()o}%Wc-=z zg6kF|-tvEi$S|-63}Mx0d%j^~6xvjRHN;kNQ<}1q?^*pIV!K5)$|?yMD84}y=$+Eo zZ_C2E%peBqbj-nb1zV(gHLa z&G@5%PA6+H@zT$4nk~Z}OqF+&nH%gsFs0Y{OUN-WJ$49@Oz34~cf(IbMshtTc!cAppa|MwY9fK?F%a<13YKCk|=Rw1WMG zSZqKQ8c^LRh^vR+VK@n!MEesPBj>~IBO)t0KfxOW8Ewd?!PIK@6!~{Q!r*7P)6n-7 z6&`RhgRlA0zDbxCu*_jW%_Viz&boW~abps4;uA~Daj@ckQOMhF_2#?H44+vUK6__+ z0Wy)wLQFPfRnX=gpZ-S9v7T9RAqI$_qKPoK%_Hr(I;beOmY9RFld!A^dzd0jO!_v+ zP4j^v4(av3lOaOVKeI(D!bC9pO@=_*A>E;E5Wfv7vynq2Ue}-A$m!>3yJuJ>xo#QlF8#cifg$D5mco@z*WC@HQAh8t6K%Iv!9<~tz1_R#m>knn7y&}-wWD4Ac%pzPY~{IwnLYRP(1Qji>)BqHt8w)K?dkfK)Shy1YuAN2xw>zXo>@1 z1b74$#A1QlhkUOCs1N|QVjmnrCtQQ3Iz-`-OhlM65sae3+1*Ij0W!W045L7!)7m+hTiFW-!!z;`mqD`nRmaY4E?coYRW0!KQ$CkbB$NIj8v zbpQ`K)d`mB7)p&$kw>VvMEPLsRk={Nu|N~q$&(b;&1I`B&#rEYyoiNg$097Ka7&ub z2@!pqEXsJz#oSaiw~5(hIoPoNo3EL2-M(ZKotK7cHC|j4)@4@b`c9+ zG6fpmBOI}D8t-GFj?PK|Du`;Y*cuB8I|gIno|VshdhFIDP;S_`fL3IQL%4v{11t_$ z0K*)5;$igHz9x$b@)=VI}g#=565z30)boch0k3dDpcoFhKl>4%QBewpnd3@Q$dio~E|mYzpr;gxhpghGLg-dxf#&%QB))qzN=y#|^DSHJTKoEHrm9pz91Vq{p^^80iiMCm~6hpvA z`w&wB1v@vY71vYfbt=?vgrtZE#-724KE!p&qeH9G!b9OrkFK`ehuRT6q%J#U$^%cN zu4y&fy8u^(>$CH|Wm7$1u0&J-1`4A=_IE1>Q}5ZwKg|yXf(iRs1A?GJ{Ymj^4#?+b zR}oaNH{16d?ZSJfp`;nVPP5o1x(?+{i@gtSe=x)CB1c0qg3%1!larC-2i-%$_c6U} z6MT`AuS-C1%Y=gj1bPHAX@K_dfMJ)i&G2aob@{qrnid2RDp`Uof_Ip?ysB{QZcc+@ zAqMh@CGHu=|5>0OgJc`1Kv2B)>DkfMM(YMsyzzcNdETL}(rSu-@HpoE=k7 zf$(8pH+oT3?+*AFT_81E7T=bo*8=u_^zGfU*4MW^^8@^{1M(>WchM;66*fmAXxL|c z*c|Y|fM%A!(K)V=PK&d?I&o5!Y)N8_Y2J3tBw@Bou zt#>VRa58$MrF-y-YX>?e2|a?is_A}b>D>!XU39Eb+jHkTcJ|gUcZo?%w4oHiAZgq z_d58AzmsP5TGJl9k9Yau+`S)^0f3t`u5-)k&`x;Qf+=8#vcJybqf)l-ZimKIfWbW= zZ&uHN$DDT3pj8}FtTX5_&EvWRc-oyIfA|L1FV|C!$l~pQ7&^IIi20y8*JE^Le_w?y zjNoV@YZF=o208WWefJiuyQM&jm~-nlXS2OcgbU#Z&w%0Sj)|$~Heuw~^AFK#TB3#?c^LbcT@R5EPRL25p00F8Fy^DSmKtbqx!<5jV zE9TREVQa&RV)Uy(!$Y5zo==MOye|`JsMi@ zGrQVn4(rccKA*XhKl9dp<{$qoxc3<&I4PnwDP}z>;WH_jJSkl}DLX!SbZ-(X_~nG! z7dh)M@;+Y_lfNj}eo-C&qPF)1Cpe{{Hl<}frQ+E)|n zuckg<&62-b)PA)Z|7yMWl_2=-n%Xy8>u+{G-yD*^In{o19{=XL_l+nxeMfEjp7pew z&$N5;v`6i<*Z8#0-ZV*Y#$RnF(0V4=XNF8qo(Zj;2_K({*qfmUevelB9&7#msn7Sg z6_C+~fy3hp;xm`%5y-EY{Cl{}kMJDWE?Td+41CP?MJEgk20Sh70ExU zYJb#>|ES&jQ71Upu-~x3dal`Lt|fV{t#+@SQex&V}w>j z&a8-CU6JrzkxW^Ue!C(&v2v8Yf))CCV!vU7+|{4*zCRUHe##yNEFvaY_mbV%4p>DT z&^ouOOWK!QR-vh@VEwA@IrLefHGCoZ!rRr0x7JwBuj%`)S*EO6(O1nT){NjZbZK0_vS%OIPQrd$4+?huw0QsZ$NeRTyrsQ|v2BZA<5s_R zEPmymUUIm;dcF9UTF{cyi|-R_|Gls}as3DX@KW7{UxM0SS~E*4c}q^Gf356VpL1XG z6dqi;ZoHyqymCSE!-daA6s-OFX|t@Cdu>$$lluMH5h=UX$rUxJ zocmKy=7S@K095cf=QJ+$#j?WGw;VTI>W9C6|NYNAT-5!~IlErwT=;DD@BaMzpXMz* zPm&BSkg|EP?D>4zT;(9KP*3FhxX2SnV&7-8bDynFzxZ!o5TpNooeW*`f4BZM`ThUs zIvJ*<&-!bFN=a@$N+9(&x8TqlKfwcvu-eJq1K$BdQ>q;m!?@Y`)bt|^r2m$oOK-`} zj^m;=e*CpGDgLITx6Am^0!k@W6Xb$2RO@he>m6)dOA&?w9A*yb|qy9$AY?o;bL zr}VA&PQF%a!WNs|NKU+nc&Non#XgQ=)#lT7DwXOPvgc`X7PoM@b2s|(R^c0k89gu2 zg(e$0Q+CGF~K;7mgUr(rGd# z5S${*sn=CHO6~n;R$c$TLDdnG5T_iRDWnxd=NE0s4b!9)he<2bwr}cu zNePO8KM9j4=4%E+Ts>b=f>Y>J8R*p=RmH?{h37;mgG&A>?4eGf67G_SyGHo=TlwWE zHcPTf=9iJAMt1LP7=0aCI*q(DP!EiPo$S$}uD6gDsS({>UQcb@p5z{V+-?=FLyr`t z&(lJcTd0l7?y+kj|Kpnok32s*ni3Oh1!_g2eT6EBbC@Q&elw}bsYNxwpd7lxnokgu zi3Cq#vexk{VD+~a$n$z_Jo3GPuwDn9ifH=PuO-2p_F+Yn58coS>>_m|7}rFGX%{V$ zwSaC?FHj9EVt$Hywov7cFE3to6zl$gYgZqQ?nl4eoPQI1`K0-(mW1bp+XlyltrV^M zeoqI~$|vV8*|-JozaM#q4_)(r?+yz$DxF($psoLp+yW7i=mo&=*cSfdbuD|$p6nC> zzR~wzB(^^_MhBuGj!#$je3egQ#qeJ^E|Y|i1Yo3>##M?lr{$R&=Fh@xukl}&SkpUS z#eO?^kaj%n`N4TzTf=^0_TrWfm1Cm>{^NazQ~MFwbPny=p}AAZ^EGm_2WG0TpI@0g zT`krL(mioE{(`}^TtFA824)x7U~3Tu9UWu}$JSYe%r+|{`> zd-HtiOaA6n%l8vA{$?w0+gGpty0hMGb$;18>jhV6`_R@krB?}fEN#K+Ja#|fN)Y~W zgX0z8{D%Y4*8Z!{zuC@T6-$19dvb~Mt^4jn3H3G1_On%oO`(k;x^4q=u3y_6itdr% zG3fXTGOKg=UPA0Tf&41G1x;VRYE zKb&=4HL#O9}ev989POLTIw8p$rS3DUgneJg+20A0lDC;udGQog(wFpTCB z?~qv#7G(`YWdlukKE#iSyR5`g(3|3^iwc17`FqpGuW!(^6;s@lAOLT)|X)|4u6tl^M$&olG4M2XfvP{5W02FA7i!#xh%OELm zkZzsFU!{jf7$;{3&;BaOatVWuCVkkTA#0$(a8X|>54ocyC8t>6Ht-ScJy<8(#z&Cb zhO1d+FqKy%6q(s8fyg?$V6%vJ>X?iP8HD2-Uf*|gFy8;Wv3?7o;aaG}`3h^58M+## zw;Z{|d1CV9b@v!~F>4=?Hpso8McLMXoIcs!wiB!F-gI*dK`OmNW94VKv$Kb1`c~pw zjr{CKnGNyhXg8`{_dVW<4I$#BYaoMf+Nb{QM zF$*#LheQk`GKqht^pL)muKD%O)RyxIR}(1T<= zToqqiVvIiv#pw-1S$(QIZ9ohWQ?$P=dFkA41HGM>=C`&_6de_8sdRK`9Dtqrgyi$! z^5SHcGCojpdq?%AeWA!r`cyQW2`Cb$(DEQtG=@p`JJ`6RT3xQb+(0MfC z92|$ifl3H6X-}E`+iGStVfv#p{+BDY;fB7DrohhMV|TP%4a*iHaJUf0D*L(y8R8Sp zPvzW?f;(wHSvCtadY*SCKga3l^>=e-D{&Pc8&xY;Y+l$GEErWDYOB%S@_Li+kCC!9 z|94%)M#sYw501NHvNaC9V%)Y*jk2q&C)}O(Tupe`l-q)aJezas`=MDEa-O*D*-PIS z#4*FK>q{vMZ!h2NxK7eaXmL08qDz|Jy?w^{;^wB^yB)iT0<#F>)s%&{OOG!Wzf4%O zR`#4M=*XHGi0@?kO#D9Mt0{0e)BN&T?*X~QXO1!vjD zTSu?ly~PacpHpC7N58h~$86u9)AIK_2F&k$%uW6|gO}(WbaeexaQx@2&Z=AIkk`FW zC0##XlJYw5gt~rS8^wQRaliB4fqS1%t^AxLOI#6Vx-L~I{hD`jyE0OGZ|RKnuh)#c zE2C#!zcj4*zuxz+{A>UBrt8<%ysoEnuHUXa`Sm{We%JGF z_r6_Q`SpP%(LITBTkci*y~uOxo|e16JYfC%BR{WuR@?3SknitLW%s*Zncx3@FZuW9 z3W=-pj&46jj{jb&bG!P+>;8{NUBACH=Usgl>h`bv-<9VpzrP72t}SM|{hU@>T^@A1 z_BYG-mG$a(VcxZ+vu?lM_^$pKzkls(`~6?GK}NWfR%* zWo*R`w(>Yzb(xKq=crq7h~6BnM2=1wN3VmUKh7~+=8)vmj4jekywl7Q(=5u;ga;h0 zoTNIK@f&%Zc9!&+=y+O3jN_o&(NeDMIQMG_w?n{nMq$NpTo-e!JR#luY`W{R)FnLF z!~`#}((T)^A{4AmJ4Wmy#*vwUVrPg_GS`^CN-e4v_F)4LCy}T?bZk%>}IjcfGyUHTF#yk5= zVs>3wc0)&Y(|Gpz|h|5k=ZR>=Kha$e-`GYjT`$J!R;89?g)Y-ZuV z<5>TTHT`Y%P*q8^0+C(ChWTZr^EB9O{tjHpp}~PbO-{I!sMGDUQ?*7xman#uP=UnU z7!m0!+3pg|BQfDxhF9cgpJ(QYA>sd-3fX`1%!XFov5GkXg3Nv&CB&ayV+w4|=5NKF zy{}=9lRR18*07BuB1^WNXF*5npjoOD&aD*qgRnN0 z6G@VDx00@!SsEX&m|}Y&s~+A?jEPnDfbF)~Yl0xm-wad3 z72yfZ(rJu$%B#v^9n5{1H@dJ#HBNlTi73E_aPo;q$rpxyHLJ=Wy0Tgv1rQiGxUJwB z*@eXBbKqaRCSg#O96?s<@bh)lCjxjTi*i$BOJ#N@LQy&{V6UThEWN-& zGPvj8V+zVq3YL8QBbBKPgk#FYZYtGhhXytJ{RCQzD?+a!M}}3fvliVg@fGVG9=jJi zGBydrwxxFRr4C`XYn;Mv!3*J4#9Fg@yKUjyybf_k z{R&C^hm5q@FOPyGJl8rg%x&BCV3UvRlD)8&t`wFZf!;iar^4>W7+_|pEz zPk{u{ZL#!Ms80Viy=_wL`$J-9YQLl-KW6EJBO?%uG$qE;8xR!#zH#TwozTSq^Q+d= z5y~cJU$!y!nN9e=FyMci=u(A>1j`3sS;#;CA^C=_+DGcdw3_QT+_gNFr??I8Y(a%$ zYfi$`ubCZ2ZaA3kV~p(bH4T$0{CQvY+dyUq=P7~kzEADr_deIeLrppB1_52)HdSHt1V~ZyER0x*R(;9VuTw7cZ4Egl*OQl*5UdW^M5gz_t zm^H4?ixkM7`1bOA)(fH&&^b6DcW*y_$hZ1+P5o*Qhu?~P(_JP4Hx{xNr8KnU!>Cv* zNxSFZsl;`m6!9c@hGG$3hupKghJ|Z{6(M9*oSd+MP+8@oDy^Nt&IqVS4u)Q>p;<@$ zUhk28Ww1shM%N*)YAez3GhX3tn5(ItXa0MWVy!J<6pj^GW>{2VWeRc0%ZAntNmnBl zbRE(aJX! zb=~8U@~>@mTh2vz-G2P&%GXP3I3Ox?^OMJ`kH22-xO;Bzfxne(Jko+C@&F|G^hIt{ z2hs>e<1(KX;3~5bx_edS0}&JU-|#*5=eGqv@jK#jfUrG`m(X=&y}CZ!5;k={s?&F8 z+R@T(ndI|Sp*8SXoGtV|877qGzrI)WDMYLEu`=5SS14a@0n^`S@^hllOGr4n?#oy5NOIbg7u5PIJ^cKOQ@{6#w+uVtCnYw0ZBmzDO?P;1_7t^;h-P=6<*Y4KYjQ<4pk}QBg-A^TOvneNFbC9k;=f6 z3s35+%swV`93u^)K_*`x6}k#H)cSl_@#v(+YyhxV2MuBIk!m$%co{Qz(QaRD2w2e{ zdwEmZ&6GKA7+b(fMXQXTm~YTmB)m`=BpE=^W0t1xlvEh75z@3`H8p*Bd-M4|WJLQxc7m z<|Py5b#1)U9R--!)xmT)1sk52Ua)MuqfRp2+bGupJnV$clII!k=NaVl6h3Fj^zcyZ z>`V69%bX1C6sDPh6$#*o1z=kkS(<3yW4!g}KpXVO%m#0;NeA>D#8g-uEl|(aXCGZZ zjgcZ?VFB3^gq%Ce>ZN6pUCZiSyd&a-99RHWyZY!l_nZ+5cV-EUbixXCz_!U8R!6qF z6gREx2*n9oP?wQbme-~O(!I0N5;+%(b5>jOdUe3>VSGeS+VWs}3Y!1fDR;ekt~QPf zS{ipbakS>s5_m=)-r>;iMrsEo-;c-2WW^W_F-$PW^YcH~By7X!13R zWKs#Mbc(U^wjxpQqot9Onu(tFBaP;o;WiP?)bxD!&V3!WR&k(CA}OFx`rKDOt0VJ_>M?kTPE&iUlPyYfX|O_pP@OHS zj~0EVVG=$pZxF1L7;X}NyVdBsd;zN)=lZ#N<+&zw}P1gh;{dARV8^ zg__xqfDv=r8(Wq}+-<&xgf7C=WtGIUz4HC|BR4Z23x9mZx49X|erAE4^NQ_bxf85i z*Zx^SEMb|NC6mtjyW30GQ);*8b%<76dtIE(wVg z?KQ_yqIij<`QIEzzlk*dX7LGFsp#Jw$5y4y%nSu7jo|M5{BjY~R#gea4F>-hexjp4 z7kQT@m%kU;;eT9dTRo#B{dfO7IbUpPmK>~4KU^K7MysKQ^)yX3tfxnXPfe z%+@N%DWJ;x6rC1eYc?Y&@9>}(wge4qjBRC zIb0aUfWY!?_;*Za+Egh!mcYRw7%-Biw%^1ybHsMuPl)DsyYLk#t8gPqJgEGdX0Z5Z z1H~@%+14>Ob0iHF)qYYaW-GM3JRE|3Xu4Oa1s6XN-?s{nqwKtS2$T(WG;w@k^6Tyw znumFf*|a(Cgo8ww^pb+Qq}a2xX?|C~c~8!26!=0!3{09c&CmVg_Wm2Uw_5ev;7z+9 zAM;rMw*SuRQ$GJ_4N=?q)+V3-+y1*x#mz_mGyCsc&RUKAkNtO0`-l!a(+=W2nhhB` zDvRh&w{?!ivZ16vq{{q}ue!oRdM3m!d9?ngxop+`DwN1Vk3I-xWJWC=Qrk9Y|H9m( zATBPcetA$o+TG(whJ=h*J;EUZ=dt)u`mmHrmdld^4|s!AymoONt=)W-Jvm(`c4MBB z?ZeMd-9|o9?d74VEF-~I5?d=%^n%+x5+}BkA-AS~Ed;YcCdoDVs(Qj_k*p5GLa8|XQyD_@(XOQZ!XT9O4c5Psxc==r&Hoa_`e#%v z6!8DrOT>j_67aW|h*=mpfVuUQn7Sx&Q*(G`jrTs+k>xgg`gwewxb%~>@JAOd*NYTY zfY~ZMnd$5%VUP>uN-DncS@P!|nwFT_@&bWvFx3mgd38Uo<^-^hC58DMuQYyfTm74I zcLagfYcFH)t=aEb+<<5v!bj3u@#5Q@Qz94)Q7G2eULb937gpK+;l*`{z`!~}%sY-n zs@&nb-GMP!{DJi+@X^pOX-SHE_jm|!a(M^~$_hOU3QSL%{ z$)kT!?zVqX?q4kaM!5@W$R3J2gn8Rk|DxQh#x(SG>tFs0<&F;9DD7G0ykR@#7KJ!p zJ9F$OyF}BmQFlrEw8af)Vf0qnwgKwgmuSt(pJGEg9y#Pqg_W`kS&aM*HcK;3rqYAZ zWF53ggoKuj=Ns}#$u>P|z3ggSL;b~8Qlw+M&zr4Hq8}gkHA2soM>O?SSv84h0gZ24 z?|jYdZM^1DM8yo5MV_mD7-+EvGnQ~9@bdHjfT6rVfjz}mD*O<4f>XbL<)a+GBcFco zhpCB9UdQYXhhR=N z#cnw&U1Kk?=AN_g77wO2sI+|{15{O;5iLsCou6BK;4h7qg298rc`{9ccyYvf!ia^> z&zbx(CNd@L8-*fHYLGqzr!es2sIuj~_bnp4og8x>c`Uijic;tTrfB!G_64~f{jJ$WsF1F#m^a8fb z@<}E(x_~RY5Pa4;C-`2%`TeKx^*6|YMlE*Rmzc7)ytCvmHb2>rF8eqQK?BNdGYa}E zu2}auu01!>#rrD>^oeWx@fwNeLiYf_33miT=OhhWMqUr z{<-#7<%!OBmiYBa;RXN4`1==a*RMTHitC{< zas2*@j@~1C(5-KM;^qGw1^$a41@w*nnWI2-LC)rPVM5VF-0Kk5+e@8mr2B5mdADqF z=?Su%IDLQ`m(;1l%J9hcR4*MF4YTK{*96x%7A)1Xwfb*z$n{^F8))Y+t8G-u1$EE2 z^j5j=ehq&OnB`mfDsYwOZWSEqyj|a@cg|>-`A+(B*GmM8@_IqM1k4p@9G!X;sQ#` z8bp2Erg-@1B*B)e&mi(GYPda!ZTiL@S`tMLRD@3dr`s0i92v5jy?Uz zk)2&)5AGt|PkyC%d#{>gJ~NrJjdu!!r50Y_Q0*=Ztm*6P9_jvYOYbRs2pl|zu zKNTMTzk7=ROBMD8osvHLU!w}wJ%o9?+nQe(+;cAQy;GR_t!D1XxMjCn!kZ0$h5pgM zhyL&H)dhtRWIVZrz7`@e`@s0`j-d#KNI5r*^|JZ$p0tu|B3v6BL9DS z-_xJS|0nYQx&8kW`Ts=z|3A0({g--*|B3wnDJOwHk^fKR{}cKDME*aK|IaP|zb)$i zGw`!k)ct=l@bf3~|B3v6BLC3#EB~njKYwodApih{(*IGagaP!S016O;99mmNTLonP zcji5{YVdz=j6FlFujuVX!Zhkm{N)`WyQJf?F8<9sSeGDQM);S;SY0Q3xT(d-O_v%T zHhN!-cs6vYaqI%K@Biz@*u;5=a(<-e`@(~-AS5P8;;{zM8E|F?*^V>7&Ng65uadmU z$uzkU8NZ4iLFfUPRM%m1=2dl=;%kZX2aI;U1yn4T9EAuwp@0Fgnj{GTlVyNnZ~l(S z+K-WMhKXbJ%Y=aCJjowR_8XHDv&L5_$4aaAS4b=n7)*POWq^y)dp=2H`ToibqQDh9 zKn~{oGTt87>%~vU7>`b65`NItp;D&H6-Vp_v}eSm#iIq{<``aau0JLY0&Wwh6ZqIo zB9&YuVFx1{qhAm3)H(386t^lJ;Ys+p@j!Npp>(x`<~5N%ak-3enKERKb6PB-BN`@d z;zt6@sp4Ef)M65V6=7wZE01F(g}pV#M}=ycs`n;p$VjKvP4%@%+$0B!jMjtY9GH0|*A@P^iUJs%;OGLEKE5VI_rIOM$JOzkV9 z8#;NxAz@^3^J!KX7Ft=)D#skH=*zLhC+(%$Up^Rc)wsWFz1Xi?&S%M@g3X4dPh{vo z=gd%78tPL5nkxzyWu=L~5Hb0f3o}jB>9kvpHjJ|-qCbc^cr*gV&C>v#gNn3{0#CeR zG?9%i;|(Fc&+!!~yOi6~5UY3Kvj@YsgoY50cHu|JFKYOOa^E-Pb?_}pS^S)3iWn_T zUVM@x@l%Y!8Z~JhnvIwGfM0-o`n}G$8@+CI5>je$G*obHAqU`Mlhx0)wUB3p{@3g# zg+?RhM@v;eQq8UDJ1lWVbkaT;WgqovFBo(Zo}zqyR#mP%pog&jiyBC-iAo-#$j?{J z_Gz61_|SThw0St2XCa`BrX~-OnHI4(2Nfn^B{7$;G^2uzGuuQJ#68=`&F^@}7Lva@ zPR|_8I~8ypAqob((`dh-b=MbJ_jHn|X@b!yNN*8}P3Klb#YF4*|Mt9R5 ze;h^N?JXk!EK#~@z^Mlt{+{wq0}au)FeBvZ#lCT#kiR*9@y*=@e{#jc=Lte>`A5R| zh~#3~U$0CF2AUbRaiKvU{x|VP8S%O zHS9E~AVRj^8hQd0aNi|c)Jg4U&*U95Y!Qz zzNGvkYxLv?{dHpl% zktA1nN~XXP>#2Am`unPbC;{zQ3k}HdoNIW?+!ym^0`kpu_9lBC^b{T}md>w(DG0L4 zVeU0I$pa^_i4{Rl;L=dTQM=F)eq0&Mhj3a%zvQws>g@5MD@YSlrTPe9LU;DVyvXfi zm)_-Xcsq7LUdTBu1~1*inaNJwt#|g)XwsI3CywFA%!5yP;w7GK-GG>`l2C_NljDMp z1_NrLnwSF5-te@?+kwJg=MiO3C)%SQufM!-t5)hNrSXLE1GYtWSK}p%`!PYT4`f z=ngx2NBZq%Gc2jAfnmPx^js_lfA>p%RL%N=deI--LF6PpP=*7!QG;kw(m98)7{|kc z$%YDkfF)eMN9@$#9YOxh&eGVPj#$>cwFRZ1)IPRqBkjDB%cVRM@&$5#WDVpf9+l*) zM8&Gt`^94HEL12r>`cLVs>nWrj?-A35L54G0zPz2$e2%8?%uuEr(dN`VrgE+cHtJ@ z_$laM(|tBKOTIq^(~{llNfN*AOwh|H%)VQ*fqSBku-eoT9sR>RF*O}{Ib0uu;h3t; z1BaOTlS^^>Cq8wsx^{hJy*_F+tLOWD$opRFPH64!^S!_=kxrJVz=jVS)h6yaH%qo* z=w_51TXncRlt-_i!F0ynMqTX2z|np(^6&!$?1Q`yRFB(A%KZpQv5Ox(Zr;lz8K7lt zovXdN!WzKZbS4)Lw^Y*Rx|>);&StsCY#W~U1V$JM)kqmwYe&P~U{-16Uyo-f*e zx_A!;Wlj!FJk$3&)V-v$)($w^yT>!a?M=@bF)bFG4)ewmN3WOYT)O%4`ESvcQT3qx zXPKl()AuaDcoQijU(o?8shTGUHu)q%)!`8qTT5&5isgTRHNE6qkKAU!$ zU8qceoxt|W+=f$Wln0V|}M+g#Gx3=Y3C;&JsiRy)hYM&BxRfa2^V zM({~QSI_NV3P8V86jF`{m#2v z)k{Y32(OU*G{Dm-eGPoMOQ+g|If!`!W3!q@7G9{BE#K!%?zO9=hUk6__4?Ku@k8T) z(z3Ff&S>cukmDy+7LfqjgGH6Q;l1HAgd(vzu zie=IflJ{y{Rm?+|#m^?~SPLT%ETA6A!QI7z5KT;g0Jr zl@U(NLWhH+j7$ymTXPgz$tZ=RZnbqpC2#OpxgNcEiz>dGosu>|o7@-gzdH z&nj7*PT`v_LAE%L6y!!wlpy)0~?ST=nRlr-@6I?p z)QTO7(YrAceekQ<&?|EoX5BPmFC-fnhUyQu=-o1oZds%GkXz6%p~0s5#-z<(f}+qv z0S1dzhRFCZo(^wIeFcg5#5`b)q3o$7nU=%wsShiZM9`< zFBu2vm<4)cg0@F!nQf26U)KIw>>)E%iCNFbr6WbH}K=xjJUxX z(HO|LGEh}m?=z)uS_k?n;6Bbl2HE=VgJ4|PzCdGWaDo)EwE>;)8iOX}R~w}5xA3J{ z9&E^yG%~(o8Ua#VL{Nq)QquEiobN?TdT&be8M9l3m7Z+iCIUwj!Bs5rB?Gus>9M*& z$1FY)C5M|@-hfjuok6~Rv4-kyn#0Z@pj%^(Vz=N0#n0KDQYcLNyYoM`@Pnf7nIofn4J;C z-G-!==di|73fR{8)=2srd4$g^Gki>(&7s}sld}OPNKdswmU8I}ukN^qkf(_aq z%+ zx8WREtGd^)Vv>=QkS#FHq)aUwi_`}8aXK~~cX0(Z?p^WTOZnJ_l7*xA3~!dPYD zY4v7)ut3369$)>Nr+NSsQO<-?w^tb&X@BpsdBagjHS)~Oo zBb%K*-N;$l)~+9D5#MWIz}RdDRj=0I>QP+gXReNUri(JqNw!{t9(3+mSBS*r$oAT% zzSZw;0B(o*d`>_1@pgnUJQyW#wgl)ip2id5UQN*ssgwMDC7qu8JZ* zdk3M#4UG|CelF~sfqsL6L!ur!dz1^lalLem8Xt0R&w6+$cmOl`H+_D30aCY$L~hn} z+8gzTY(B9sNAtFrhqi1%KR@N}$14fEMhAayaNE;ibn?LSK?^PQ^_}ow>vwm?@^ehz z1tBO~JgZ?xyYSyupL$@2GA=1wKeu-8Z9P~}cystf^aJzLi79>gcXG@=Tinto8#COa zTg`H6Y4tKJ^`HyR9{C6B9lh5jJxA#e_X}9+Nux8T)y23zSU3Z!{++O+eej-LG0cHl z{NM7GjEqOuREY z{lg!@v=r`F7^4*0J*Ob7r6WFUX<-2P`cOid|X6uz0AR%(cCurdD3gB>?_ zfAjR#{$MU|{r=QAa%C6--5@h32F->^LYqF9VCSyPaB8zNaZ z@AO{uV&OWsjyayD3+w{VAr<^_N(Xb;*E8l7Pf{ogH(OOmCZ@!&uXnBAb^rPF9(F=* zlierPS|r_ZQO@QtJxw6upgcTCmRcHn&MpcERq0=FqZMTNqPZ9L2hnSwP>Sg63)Q<> zp+YWzX~T1fTJ1X=6l>%qk+_C&p*NyLr+PPEmv{rWMO2zeG5Ny@gV7XQ-bQM2$GVxXY+$om=CjWGts&w6HHQWz=Wr|rR zoU+YEnp%;m3elnRKjCpTMdIABs;$KgD(#R8&YALA#j~M;S5VPV(}bb-o`o5#Qs&NZ z0!{}m0MN9Vi4s&XVfq31f2;CpDb1NB&V^|uf$0#2Re!6!JOsaJ4Yr54&`3AaUKX$hx;4yKNx&M~PE zrECqBMnz3+O~*yD3`{A0+A6pS>3}W#C&)cHP)zhLl>?7sd~F#Iw%lJm1isj|OYFe& zBsqWKq`QRpmuY!j@iTo9j^Z8|PdWO*mkh^+cCv2V`_9??nM0N@OViF@o;%&=Z27#n zc(2v*rt5|)@7l)pt}JxSonLw1{q6nA2LWpR&&7VZeLp`As_6)J-oEJo_}@Jug4_ge z>&bv1UovhkVGm9s72%?S8_MRM!2`;M@}s@VVJ1aLCAmZ|N8DDn5lT$zpu6MuH9f}O zo?4ft1hPQQQGyr>?YzYBU{OV(P^3FleflCOFWoBv$CFjWDO?Rvrh2LcTwF9Zjke4t zz|sA9pJ@07e4yyomOcW9#D8f(A}H0U;lzzHvzZVaFM11Q71NBO!_`yYerM7YEzn4M zG)J@rkIZZC5%(Z+HT0cC+&cKu=bO{lADt2{Ugkq)qXGNLN(pvZ<{XX=SXpp!`H7O6 z&)15;Dl{l|IxK5NZdQ^s4<5wu&CMIFA zPxTIo!=7D4hrgR5`ZAHW090Z}bQLvKr|&vggWv#Md~TSaN3HNH_`Z=%QYRwh z`(a{Ggbs#|WLI#6!vWniFzFJ=6Z9E~eCjhM->Sy#9cgOE2%LjsDnk>n+svIRo zu|$1bT0*;0rlTrbjK_q*7I*Zox=S8?@R`i$VXz>x0GJ6@kw>j5XuZn1REs)MWh|U@ z*$uxn%s~6C@F6}px{c*DDJ|ZTnWheO?@fGee_*^13XY3?jAW~T`5F1KiK4v8=Qs@0 zt4KAUmodmDqw%!s5!{9hWgaoomv-#A?8UaC8NZi2^kBccM7k(RsVKRp)=aAVQctyl z^zamyDaT{veiDHwq4UK76mOgcx%amB^_MpBW87ZCB8fF${|#LJJgF+4e+L3GSH*eF zy*R&U3e@;PHhf~y-PW5m)5`*qS(>VjDN7Y;&b=h*hgKlr)g^)kzsQdWCN=cE z@{b;UNp{5sYv0`QsM&u}<$JC_5}(@x*I@T)b?Ze|VRBMz1Q6c=yS) zMukbwX&d;j#g11>@)ck@Dee4S&{Ks}nyO$+W$oMy+A8rtE2Ku|7QHRT!=`R(BS8cf-CsTOJPos}G z&cDvJ_rER6P|AoSb0yKSteK?6+UTnSLlmAS;}-i=ReJwQQ7T_-`+oewHI_2X^$Y*N zVpZJ<*cxIQ%za}+IBClP#qUPj%2xpE(kqaSw*zUxTQxZHt~CAM2^>Gknc?TlWUEu* z;xqt?M%zFo%RDs>@bewy(Bkqb&q}OEB%u5XmEZSRLmL1{7qL+@k90dyjqv0cA*lT| z4|Nk8Tw;1RfrIruuvPK|ej)W64zWH_#C*exragVCn`^xTG?_KN7Un8X4nkbhlEfA719p;e!psIi=H-l_h}A6`MFIkyoU@j z@TfAXalP$TppQ_vY4Ep_W8TdXZPnMRZQj--^yAXKZrb3&fS2yq{UZnj+$4b4taKKE zmTR_ItNUQfY;2NFfRTJcDm-M0qn&0Ux|f)-uW&O|a<9WCB;nBJS#mq^pd$-7T7*E8 z0SywW?`5RDY^oI#&0)aT+Lh>d^j>p-%0#{{OjYl9_Kv4Qg*(IXaiUCQX7pjlii3o& z2OTUBC^BFHQcoR#|I`M5helj1Lcm&Rz406*fkq)aENB6jLs0zP03({>TH{nlM?sl@ zl36-j2!)5FYLJlDUSNP@hC?Dcd=<1qvBFb(m}#t7d_ar@1g#yrRg`k5hX_(gXuX># zPIRQIAT70dcQzdeTHG|^=^4bQ*uxLWk2v`RQ*YB?sZ4q@=*ebn9DRl0z_+t1w(nUC z50;NhiOWc3qfJfIx|Z#cJ$^BD^!;G~+zkD;-#e?$$+}I+kp){c^{nkc$y{RAZU>tl z2CtR;@Fo)qrL#Dipf%pnM_Upug*!2r0&@ovn5ZOF_Rmq9wPqH*hf#{-(1TzAjJ4LZ z7B`ZPVx*&P(Nj|=VaO?DGCu?kfhB{eO(xK=#T}`9WMwTSf#J@WL}fVvR%8^T7PXai z6em!jKQ3tt+7*h8;zw zLZ)D)$l+3ydq|l{X)>|2puhA{O)1(v#JoR{$M-|yY>yL5@AcUtNoA++wAP?R^Remp*n}Y7;BN?4U#w0R` z;eO?N;EjVP29KQ>Qn1|Xu#Kf4nRu*pIE%W;iOy!DPDf>R>BgzPrW&;Ozqwb&rqjWXkAs(~>*Q=!XrCuo&Ul@Kt8k=}=@cqb@{1g_3|E zCdvfdVzmi$|4}icopQAd<;|))7hh|foymQJikhs3vw?JA8=HJWHXf~Xd4C8KwYmeI zNXnSjg)>+v7QVqGfv38yM8}aA0>HVO@)s>pha!-2lMV>{sT&qT2y+42akldT(lX`^xXB5cfT3|deuKD-hTEqDvfW44?$%hc0uP<+Vs$3ncG=p znA$9K{L2L%e;2ZJH(6)biN}#1Nv>DQwwEDL$i=GK>{|UeVDDmXD(IQUv`=lTWiiw5 z52hsp{$ekwVW6#2TRnce_ZCa9utkb0AN6we#}1UQyKyun4ZN61LOjm!;t7#cTY>Hb z%C)fS#td{NlIH+#vVF8&tK~>aGz=~Pq8SZHMpDRP`t-QVM!1~@uqz}w?Zd-uUw`Dj zrvvunoy&!XrHbq{$V|L5umg)SsMvPkEd~AxWx+ZL!#Q%AcRqQD>`HXDDno)~z*yiE z5l}1NxKC@T9a(|U8!JXlHL#*#=>imjNiz&a9J#dr1QvZM9%6g^I6efuwR!h9P4w#^ z1lJU5!9%$u!>!-)5DdVZR)en+_O_L`?gl`|JIFV_g2=m5SKrT*d8j;7@wPGmV>ehaIED+JS-OEe}d35B1ZSF7Rk1 z&31P&IqEozbWYAUkY$<&$oh*AT4itVbJ6aB;)4r`v~#FH+yQqDB+QV_PmNV#Be?AVO7(mplizLFy-oXv&Y!{&0CuUGgfH{ z_FV@VJEu!|#KZwsF#%Z_4zEmGg29Z7f)4Ihv{H;*|Y&0Q3Ta|MsQ%dyc_oxDZ!$2D>-d9?H45It)!C_kg6!S(iG3NfOfm>G5k(V?9 zLly#IKD1DHIGN!)jqMUq3Bn#i@5p8skB3)$@yblehC2+;zHUw)tul!&7iqImWg;#j zs$r}aMAP0BuHdURSbpTbzv&gV{t+lCA)MiDmeTW97Z^_ok2C-cXYZc{S@!+#as{=W!2&!{H4=-tmGlVk!ROsD|@Bs4*hH}sBa z2qGmY0wQXt0wM+l1Vv3qAOR@>>7oWi1Vsg@_5@K;QDes*6cw!4vAi#r|62FnZ+G3b z=KJ|D=j_?@oM%73O$!qizO6s$EBZJi%{M)H^|kWE>{4Mw1J(%$dzr@G^}WI3;20#Y zDHPyL9yLDb%)lZMtdahd!iUQfM_&3MC6vuJP^rJFXPM^WW^DXs)l%qUoEPohulJ7v zr$NX)hM5PLEDl7`PF%1FaDH6seW>n2)HR&rog(q|2DeSQ zebM#TakqcpS@hs}$@g;Z_aLs;1v9~FA=1Hm)ouk=U-)-fHs2S;MDM$I^lDl7HuW-; zEOe^?$D6BM7g^jc+?N{6HWkxbhFji+&Yq=)q<4wOupKw&A2CfTewKK^MS$xajM=8% zN9P7tJ*x4RX^Wm$x| zRE^`x(76EHfrm@>T#~`XSy%(6HU8R|&K(HOn;W!Tj(cZQ^(3wHL^!Y(t}|1Q;J-0f zwZ`n@EQa_we+NHk(17pn5lHayPJP?DfS}r6$o6k+OH)!t4sbeAIO+cBP70uZy6x0L z%h}--nG7_J3Y{L@*Lc_ZkXyV5f5h@stFXAO^V_u6aVwN+kkOiuE|19_8e+`x$iGU0 zru~uy==F^@n7Q4=t-NORf9Xocmy*NVj(@jV@Q-y$E2jPr0`o9wG=KwO9qdU#0Jhns zj7!JrJ~zYCF`Xp{ON~i6$C*P;-t0m1=+E?+Py%BQHXKSgP8o$sd$DlR{nd%)B>&+H zI3~J2NeK_dZ0q_6-CDip5rU|Xe|S|c`hY;m@3732^Mya!4*j^24=oEN`L|}^##tH; zmfmVy-9ChW|0m(|PZ2T`>PLcJm*Pls-)hQS$GU#wxhm~njEHj?g$HUDSuVKQ*mNG* z_k3x=TGF|iB=i~ugEP@RzSB#9-2h-q%R>{UeOaoyy_ftec0z`9%;_Y|f4xZQw-vla z$gf$+YEvv`r98kB_>LL>?!ZSUQ=43Pb(%PAh62m&%H+~_D~qu|Y*v56V_e(-1VEOL z4R*g6A7P3EVgz;tvP)$5loBLRN}qG1xQ{A~*as{K($o??516TC52D<+QG#4( z``J_LhM9{B9#K%M0+%!%j(aSl2rh)hHo2#0ct)&KkgRuUt|$`;Ue7jl>^iP7|I2T< z`r-=h>v8;HtE-mtk5XE@=h=@jgZ4e6Y;M;IbC+~d4{siRV^)GYFOkGN^9k{hV#}1{ z7l;<#R=da=P~qarrj^)1TWC`Ks3-=WkhyFc0n6AZVe>I|3SuQgBd3&zuYBg+z*F1%*g>G(B|||g+h5BH zgg=(ksR+9*WxVKT?U5AXcNM*h<3}V@B`j<=tBCrA0cpJq4N5&|Nd&HHnP!kL$e6wH zhv^o8(ZmT+40*VMQf05uX8U*rTGMYhe^Y%^P0xnhWX4b#hl|P)0LT7oD_AsJt~g4g zBMp{kK%E3WRMTp5m#%q-tPnAWLQA#ou*?J8l(kg(U+)_8DxbeA?hV zDAKo4@)KJu7P z#^!F6NMD|q-)pCeyt3LcI}k1~@$Fpl-15YFX`cr?V!}s#p+k}hg7X^w4}3r%z(@-8 zk=eaHZmwbLllynk6=4;PMvh2JLFEG%<3t?qA^5#hJ!%Gs!*^@$8|`9pW__@c zSrO7XVNAOF9>;T~kNEqu&19H+WzlZS#0iV+5l2(n94H!cCUJ3W@;mA>T}A#2>ka_* zqH4Bze0}W}XzNKDD?koXi(p}-zFcB|Wlni+ZnUJUeG|qW5_!}i5iF-OJObZHvyXkY z)+QF&mNsp&8(e*=cefLH?hamoWl~s+>b=?~2>eToR+<*SgDp0L_mxEG%0-GREd9Zn zismIvQdx6E78S6ku#i-S$PBosWbWun$Sz$62I)n(c+Ar-f3mKM%ro>Z1+_eB2mMQG zWTJlRysd#~6Kt!kv_%ZOB-BJclwnW5SE3>65bdwrH^i)9wl#pKA1U>yo@dq!N7_AI z`Y_6yDc7bQy_P_H&xLOQLmGM{)fJYjv`9+;rXrjb{cmZS_m#Ohh*$!vi&z4syFusr zH)lM!;`(MJ-5X`f+GFD~(Tn7aFsVP}M}43dqI9y@4I?XLab#ffh+WeY#i;nqZLO;< zOw%k~siXXaMXIgXO&cX)rC_j|b3c&-hk!*u3-C4OBa=YK+B}>*bCAL@TT_U*6Na|p zm~5InM{GiP;RVM5v1zX#E?C2Z7V8Lbiy8Y+y;Ph_E*)IlHC9@#0M5|_IC0jCls>g` z;c#~=qC!QqT?8!oR-hNoVZsaoon}L&(hJLLDTBOA{}AIVDyT0G!fJr?W1{TAfPxg8 zBbOTQYe1g5YI!4*n862)_sqC1T{9W!Kt)rcA6_)1n?Yl-T?Q1!dWRw)(P{o^ zpMvOHlG_G-2G{l08ONU}@%wt<=JM2~3mSoy*&%Jd|4w~<;*3b8qDJQSw1{=anx2*X z!>jkEL1RkoRKfcr8@8`6%u{OTbp3N|yH-59{`Z3mHaY$<{bl^TRS&Nv=k)CTH$5d_ z)uTIYIekaBZ%Ajqx`w~t5^`$qhWa(ePu|3J>h!PPU3lgW*AGAwuymCDfO+)p$OuJLa%_c+b*r%XE5P%i<@e1qIFQmmOw4uI^z~$pCz^H+z4y{ zZk<06A^<_hxFsTuvl`jchp1Fy?%=?3DPUg6{0l3tvM=tBr*S6Yw%fg(QQqm26q=Vz zggJU%okNmN?0u88%8PRyE!oT_P@Hy5oxFu{&pM=MI?Pp|=Ik@N_V4S#e~Q@w#%D#2 zGXxthxr^ZDg#DIb4W|f1Mts@PRzsdfbactmF+_KPW$U<^g2l0^l6Ch8wN{QD<5^|E zhyJADQrk~!`()7MUE`xoXDa8a2e8oAn#P$7^q)g4qi3jE8Ph{J!**y6NK}!)cQ$@R z$1GGT2pM$ttLoWoxkq%ze9o2*v6}%uEBF;kF!NHp}W1Ou?umBAtv9~Dvx%PEoefH9tHGD z4>xzv#SfJ2B$2LJVVs@d7r-gU8dMH~0Df2XBS1!aK|3gM* znLLpRbx-s|jjdPE<^u2vU2F=}SdJ}YTv;0xEf^(wZDhfrmT4iT)Pu{Y)xwxYHcu6`nsbh~pO2r;>=?9tqYlc7ZS|DU= zw5Xk20v4DsgGQ$JTaFZHHKNGVHJ{e|Io+yF_-Zt3POWW_8$SAd$-1+;n}lLcw;Zac zu+asT9dKyPysxSPzK+O6I z=>+Z$bqlBcl& zZ{kW>_2=6Jq2C%qp=m*rLgZMFvQ?uwjHR5(8{pp?)2;KOyU~9;mf13fzHx$3BcSt) z&5U%hJj+8nxeVIYfKh? z;J_Lis8FD`FCA^=JCFFvhH+I?J4k}))_#oT)?5_b^y=&_1NUxp+ZVLsws5B@Odwb94Sa1*QAUY zT@%xNJ1q#E%?w{D^Si+63;OX`!|m7zNy@1JeF#dqW^n-AG>(_cLChK9G#PxOnun4d z!7ay^KPun(p1r(mXcnDY;WA=CCPQPz2Eq_VmVLMwx$RnIYl+ren|<3d_ax63^*LBq zK0R=BjC8lP*4|$bsdE&?A5PIflUk_nbWvE!iM9rhDeD8zy8v=^LHa`%-O6^lOCNZ! z^Tw_UozdmuF?)~tl8oJ4=27l`)uqHQCCW+4;#X1DqKMQf{Uo~rX1o4b+vtjp8n?|9}IJ{76@;Z?$@d81DDp^gTL92HuOcGs^9#hyITovtWMQ<>-BlTm-Maz$;F&fKYKlLv~X zUB%ybYt(c~efT#-WboYyEeoJptDvBUWp3}Ymud@%IEKtI(W!+67sd&`xePWN@I%Be zYz2-Zc7Jv+X>8|t1j5#I==v%u9ofg?qs5?>^_UKmUzYwE{Q6mqvlsY~p(|N&Y_*NU z=ZGXn`PX2n(CA9W5%hiz468-o=M)%(wQC4vPa)WL(ZyrYZYYzDD-28(qQi7(*3ZLd zpE_cTB{0VZz0oDoEv-qu;Z}g}DG<4O*%0WY4w z^7?bhtA~;|JWI~_n7r}7WIl0SrrDbX@yT=O@Hi8piTwJ6P$=A9GOrkcWai3~0u#P_ zTh|+ksk^rAB4y^fW5ICtz7>T`fr->x%zZPHr2*Uata`SNi!R$7v1S&^(fFH2wtz{> z29(F=vnim~Hq!x3@_1@c{itnfo4mwBi=z4V$x0G$m$NvLWN7W@ntTh@NEJL{O?^0d zn{7XDP_=f&~+8j1}lNje&hwxk6!emw^}UneFlJv|32Pi?m) zOwFV!NpG7bia*fbigYbsuh(cR(g?qLu>DxEPF`B$;M9uM@RXf%I3qDly|3UKvf%PnfGosc?it!pLj$}5li27)E+)M zSfN@BpT3c%_4yr@ujhYf{aN)pdxLTYa#lf2uY2Y)p4mys(b&8}#2P{0Mk?;-%DSDQ zZA{p-{@6Ap$gD^IKB2*kK5r=l&%wLqnu`~?dcT=*e!>DUl`uZP*hQs9xSHg8WyzfM z`IIj997$?ad@z`!BGSbkiroW6LUcSH67Y{iPamRxEUAA7VI7XZ=dY!G6ho!~y&Gpy zj&I8ij)zu`D+-{nHG8I~hKelyEJf3!J6N{yZS<&H_t*hn&A#8?P2|AKLZv0f;%O7n z5E-iQ0IbW7Os3WD?cT-YiyU(+PIrhLqcvyQT+`&wXlF9|jq2WQts}f)yuF}cPs;eA zl=YU6(9U%9AYIiPhd1onWo{*KHAlN|)Y{|xS~z%8GT z4^qR<%a2F={Dml{&0}d zv$O4fn5K=t(mE@x0Be6yIw|fW!x}XxZR}{jyj4rPOSAGoG#h^ZGo{||3EFRZu;1pk z&!Bzj=#{DPXuB48DI~rYtsn6BP6^lM&!2Car!;C)miq{L{^jZ1I4L{r)^l{lkJ{Ev z70KqwcxX{zZlR?4)p~tH^%eBk`B?_Z|KpBpF`vs&iuyJ`*b3L>eXI8ENC9djJE`QU zs#_HFF7nHyUeX&QbV>l4zKfCjtBJoRY23q(Ca~v13Ua-pU(b>6?Q0M~mHjH5aKxU17Ox@pu69n`0~pnjLLO z={@+_K6%RUa2Joa>Wd4h@kATs~TlldbICN?7Pb955XyZ~>?~u(oC5I0cqvU_k>Qb*R z^xNa;cL8H(|9Kxe-X^joSOvqIt#@Cx6S(co8vdu;tm@t<@73jb6(0ZsnJiRJ3KMQx4pE;?HeyJ|MSS9~ zuFZj$NhgHlS^8*P`wb#HChrBk_sx9vf>2y*rbhFIT}&b2y)WlLoC$|}KR&#=$l1c) z`5|%aAp**!aSwG;9Ft@;3dMIua0}T$tBu~VmC68w@|pmzij%Z2RUot|8`tH?xP7gE!{mCYIrve2W`CQ8nt?1Y#lpn z#p-MZ=<~1s7|D+kA3)p2X(uNXe{c*mmcZRBlImP1o-q#3-EdL9fRMXSNejP8E&$lz;^*6cA9an~Nt@yZ z(vsn3$V@sMF4)J9TloM;9!x1j5&*+GZQ(Gjc{zc^*9%X;pNMAjsLQ10uq?$WIk+Yb zYvK2iS45S-tVZ*;4--XcohNg|VCAD`F_MyNl)z>#<}lK-XJl-HLfFZiy~FGUmc*SA z!X^afxP_i~Vt8%V-i7`Ovh;nX5}aIUA~u?D6o97YjHIn0VGyeTp(5F zX~TUIORuUDc~P~X%4`QYASJ43O+$3cYmEZSY-a8t>isr!a!XRw-P;+J$&xtnMj4XIg=mG{WzYp!khlB zDFxe!xO2v^Pm#P;|90gy*JFVjYp0BT6qJx0~HWm;wqptzSb%cZcRozgq8d3DgKJM^L(PW#gSa3Z%$#=n{k_|q%ecy8>Vr#3R zFvcXyu#wB7gj081dcdIEKne@)Tx%X-?=v_&RkZj?*((0X1v97a;!uvR$yR?z->G3Wf}RFl$W-bJ#TlH=bW)L``Uil z_e*!hj=z>vwA~0(=S1lZqqqR#$`K2%Tb|-g6Z)w=`hm|RRn<+jjX}9re9A#&Gh-q~ zrIu`3czb6y?}E3%Nyh=-QxnMYzchlXU$V??m?esSO$boXZ)Fw&TRp@Md!-A6R$usjii*?aTo;?) z5Pg*$qm>Zn>w5|!?Vnh3X2Sn^cd#N@HRBwxHWErW7u70!V-wicw4?%A61>T5JP96!@O>1lylqUPc#@O zQ}67})oq%8R3Fr!-}ys!sf2ysWOBXEWjX0NFJRd<=j*r_XSOzciD+GuIJ3BpU|p)! zu+-Fkv)FM)neGjU7kgMhi=vepf>~P*bxd)TBp*Lo(8WgWOqO^c<7@#L4d%!s?RS~A zAI2|GAtBBp{USQIeb>u2K{Ruc>HG6a2YbXz%A8>OrW@I^9T3ThBL5S6uA>u*--aGA zL%}9?z{}g}Og~^x0l$Pe#_2jepfW z1N79!ph51@*xNScPm;6?)gm)hq~r*Lx=h%`?!qeNGn~uXdA7r}T#QtCb!jBTxIn|k z;e5$M$bav5XF9(;=y@0P$bVPj20B_dS3}zH_v0@a@4ySK!euk2kQosdH?6ngpMa22 z?#B`pv-N(z&5OF(UDLpB22yS5Clr|yn2(Jw8d|_y?#nsV+N-sy)FGT@qAoBpj4r~) zpJh<7j^?n{uZ^#VV7+4&qBXSFr*i7of?k%g84qh#rK1rV21IMDWpL3nT5IgKHN(SD z;by|Gms1Xu+)akQK2!HD76^3`PdfROe)QU@S-XBcTXv67bQof2BVIbjdukTnfT*lB zq}frrX&D3WG*(=R1}E7Qa-wLBjR~>4x@hpvd5ZEL?aR?m`4tNve@uSAm>}R9pV`zr zpx@I!m@OCw4M}{(^?q{vyR#ABPR*rV97!pItr4nr5uJzzXfX7CYvcB7*MLVk>Y^tX zwc@9O_l%(+d%03;@)o-XBMkX((?7%#0jenkD`~zE7rRQ8Be>jn&?|8q>zfQS>Xrbe zswSgo&*-fiV_&YkaV$vnE| zd*YU?j;l+^y)%br3ZlzS%d`0VN8|E`8{M74HhB6W3QR%QRki5@qwk}_r!HGV!n1%Y$)bU zYLx)L=Mp!TFQpa)cVhJ9%pWep{iY1fIgy4}1fID^HWM1aL!;?p;>6Vz^0^VVid~&1 zL<(R!cAccxysKHbn6~ys2aok?@0MWDcr@%#nIie$$b5lVtLP$8RcH>Q+QMTc; z#m^h=A!cCZt5vZCh7J)hYlqAg)m^FP+o=WmbAcPBU@C(+2~rIT?i*~n-cnsAX9$B_ zVvNceJKJ&K3=r%2aP?dShSbQ6a;?$Bza9@#Q_9SU3K-g@?|~esuK*d$_`c^9-dc96 zyrcdO&{;}N#Gw+w3nM{?lvuMT05}3L2WLSr7BHy?Eq+1It+A}tqeSB&o5;8f#;zAf zF!obT^nJGpv*{Gd7*%*U`Y z$S6-UC5VXYV#SwSF1_juqIC&VCgT!fFB1`gere)yJyv+BzQq_p!)E^)xh0rVQupnu z*Zo>$!w5_+>W>vy@|MtC8W(-Mw22owK}uu~0+G)mOqfcFnn)T2SL6vyWUDplSE+6s zVyWep>&m&;jwqOB)L_jsuSj%;aYQt`RA|!0Ngh_7^eiWKfo5u^F_juNX=yxsH!*+!pcQ3QU>0SPqDSmyn%xEUU>jo(NnCtP?x@hP*Pqy`OlVW$Hb8{2qyRl| z)!{3DS{q(MLX!#5Ob#j=u2F2u=avkmD)jCO&F%{IBV>N4$N;qJNDesm!S!FGge0X|Cnc&BG;fX6$1BYUT7Dlu8OqSy z9s`ZdDla&OYrYr$lUYcd5t<`6Ewls%@0kd9c?xAHHmy8o9M@f+sQZhvW*V}1CNUV) z1tL`hqlZteb-F2+wzl9Z9d3jED|HK}wwF%nH%vvg+T{iZPM+-0RHo8K01La3vC(L| zae<+Wajr*IU`_i=ctyx+|AVM|fx_y8hwo~|&A&X|(IHJ^_iC+3TGp}SaL3-%0QhZr zscm!rR7sm&E_vn7x2ndEX7S?c$fKC#)=8_Hc1!&FjuzV^UN?>#F9=g8eGJB^Gl5<6 zg=W-71$QPWdk?kw|3 zQ-gj}#eTm4V}NCE#?9USK+mdu*SNnt=sGYdIB77{_i?oISP-U+VVpm_`lLbb`$yJp zgnx%a%qUo}wAMigQtMksek+E}8jVJmudXem+S;(62+ZZnuW_eQGue z2CIdT+s#!gLhA@al=rnn0}Xp8Xvfo<%ceumO@;fN7-#MY>-o%D%J8cvc2QCs@t_?E zz?t1uKDno?_DQEk+}NE~X8Iny^CcyKmo9C6dv#bCH>m(7j29Ipd1TLClK6b|pLwHo z#?<^S)AhG7CZc8J6}Z!gb?&{gmBzYD6v#o=Tx16I?iH#8d_oE~qN*mdD6Boa zraPu88i%(u{lfyIAsC;}B3+6u1>n+rzgvQzj@P={J z2;z>t!SN}L=H#~%Z&woa3QX??>ApDhE2*o9F0L%32V@mdCmCEvJm=qWnp{~O9No<+ zTIwZfOM(tU!hYT(gZj}*vOsW~x$e^Aw3J7!KDeDpp={MGgUHuJO$ag(g_bHo=Z&Je zRG|AOjA)y2-39g4wQDkIBE7d!C`dDh$7>{d4tLCR_2@F_U9)vjR5>4Ag&-{BSC| z+Wrr&hpz4HXxQUu?(8(L$LSwuXZIfGMb0h(JuYF+u2DU%Yn|QJ^|){-|2x!>9AP>!kX91`p1Rs-pgL(!U^c*gt_=f_4==MS-h@y@kW<`ExiFdT>>S&ffX)G z>Ux*#cL_Sw8+6=d=|Jz&^De=cdxLMfggodCdF~SWrZ@DHOW4oeuvr%_u8*tj8gAGZ zZtl8lUf;5RT$j7|Ennms5zrSA=DH%PZ^c^I$aQ^@8(pKe^hND-jh6I9SGcaM>sz_s zHRe!X%yHLM1AVK`yT)Gbi@oW(`a$37=dNqs^sV{iy7p(^+F4f~uAisv7H8NWXYLk1 zuRs1Dw*>e8ghg(N0sV<#Zb?!7No(Db*YziFbX&J&wtwADw-iZ#N`+f$U4QC+x3ok3 zX~*5x5A?4;@0NbKKmDfLh6nu{p1Wnd>CgD&w()2G##uK$Zh)`to@qFcY3{yh-oU1R z+&8-qY+mG^6)=z$=AIojkiFJDXWc-~M)xgS2Da>U&y@`1R=DTY4dm^2-+E|Z>v8vO z0|VR6yXRjX$iL~n{lUQY=k7bciUI1ikZV_W7%sCP`)0@0b>MS^=1k#BR)OVAfu)DY zd$53=E1EY`5a3bhJt$r`BMh4nt(_@K7?f=BVDkqHcX@~_2Bmc~#p?%)CAq~%26shy z6i0cKeEPl+2seM4yW{59(8-zdu%8QOJj`wl3AJ76qT62=>gBCo;K_zPt8O|kq$o|& z{e|A11}lFRwQTgn&5#r*HsYDf`$cf{T;4Mj>zy}$t{LRb)R`+C-uKl#zudUxd()ni zvHQWO(Y$~&p3P8hlk;%XgCTpV=d#^OQag)1>B$~p#azy7bms^*|B5_=VviMQ`seL& z?rU9ptL-`NAKUN3fgxi7uI($-yvM6{zt=KcUVwV2?A=V+ZqLi@zrzAN!YDHx*S1ED zc@@mdTjM@cA(9-s{JraDcK4CK4U}y$nhfcEkJZaJbsgBk_&+dE^Z%hfLx2Ie9Q8jb zfQ$bBDS$?XKEc2Vo=Dq5*M_?C#KCKI^aPd<-bq;iX{Bgn$(K_@8i}^k@9lc#QUDz` zTO_8^=Y^MgnbKPs5giN-k_B}x1@N5$*0*hioVfD}HOx1`y_tNNd5CPvNs(t$3$;@) zhi%u>Cs&K~QQJ&|XOf<2SNa9&)W5#){5)v1ao56W=vVYd}Th9pHkf=`Z1+Du04@qCCZb0!9Tk>d-nX!i*PD` zKVwpVLd^Uj)O(7P=-n;CjFVc0jPk$h;&vJY56ZB{r{IiJTyAyH@vau%+5;wtDiQIk_e6h{kpG{3Fe$ zVh)Fwdo?1~SYOr>tKD!!YJ2fqu0%)Wml)Y`w6*sK_L-nR?|3Vq-%1^GX{5>$f04&+ ze{B#^(A?anaf}_H7lG-=96a^5?QDjLt`+*=JJm6B*=Y{mnap|5xNe+<>qXOAK57tz zK{AV$`QI+}{oa(_&HAbVea5}hseW(SPt^OwD5h3BXlwlY`6%OEQNAkPJnAn~N!E@K zF+A4UFK?$dd8^{F_nvU!#g@a*F1#y`>K!$={~{bP@0`vYX=a^p8To0G-goo-zx?Cv zuI&5(n+n!Z+1ZKO>0`{a^VKe6)}#%{h26DXtefcW*?j(4n8cEuHRpbDuvsr2Enw6~ zv|hTO5Oq}bB5Sjr%L8D3FZotv4 znREzO70m(t=x~s@(n2a1BUyonc`z%DkP2C)Obfku7R77qtn_S_jnG4zbqd-8jp;(X zGi3shj)*P4r5yBreT$m^?IjDa3q)m|uqf6mVCFOMfexIi>Rv0G(=AIvjh6xY3D*9% zW%v+EybNQ9vUcgd`XBuZ?O5U~M*I@ldL41C=!`WY9FcK!;_A-oNwxDPuYV6dQB`LT zduqS9kaJ_RQvf!qty*cg7BRz?MG}rpur@sm4RByd&}5K8?1tH`Am~E@=;NNj=F`Fh zBC-f1Y!dpuXW|6y9Yz{_kuQ}+%H=C@WJKyq7f`dw5D|6)LQV+VL?b8=8xJ_^#gimm zbN)07X{F8Xz4Uf;h}JFauK9`8g>tdE1`8cXe?dC(ZHTI3$PC~YoHY%${-4`Jg#52) zKfai4=!g$;AvN{;*nqdG_#lrVQg!{Xv3}K3(R=D%lt4+NgLwbI7di#D7ISv2QSDi( zvrcU;WI6wcq}kv;sUli@fBv2#e@+3ZYJ@{Ob{G~DA*?Kylb~q_FL8N6s;P&J^G75> zfhPYB@5Fm4Y6*ki*jP2yV zz*XO{l0lAfZs4lm-f{FoIo*Un6H>m=ZiQe7&`=Zw(O;lY47N3K&g2p+(5|0LK)C)#n7Bhd3uwN6flJ}R!BhjcWEJmC#j$q3r3r{TcPn(G-qgjVERDDxLo zo!cRS`!ZFJEOt;es{sf#^GE2xYax2IDl<>NSh{1$J!;9>1glGe!Xob{RXY;dHUvkB z=WrY`PYzkY^tW`IWw%sDJyP9bti;_%Gi7$t+UcKeY02qWo$?f+w zV=L1Gdl`U_i@5pUDO`xlYjh$1nA{Lluh(wTiKGTv2nB*bPRk3!{pK<=UsE) z1v zr*9dtbo}Dd*vixS6Szy%DW&!^()y#BIv62-Ap(JA(F9A3EMe+V48W=&?kaOYoFiC) z$8K6MOQvpiRPc1_yZ$n*yRRIZjd_vR^~L%Gb))xAK%*Llz$|Jpo2vM*l`?E4-~v-P zWq|(KcJpyrns)B&pX~QDvq0mv8$wv9BgsAEpgFX(BS|>&{HFp|dpzC}^W~C20o}l` zKJL|SebZUJ6&;=xfJ2ve!36H=ck7bU=4DqS5CPb{5ZU=`AUVU8EANj&Gl3#T&X-#J zyf5pj80Z}NP86`cd<8z2kFHduQZl!|;a;T5kVjkchp-`=i?DkVbG63eH_NuvRO9X9 z*JG8-Fx}`X+=jJofY&gP%*gjj+Ubf*yK05-1UvTGAf7*h`mF{1uW$|gEc?Ydm0Tw* za~sMr8>5clbfufiG0EJ3fr(_!bYN!|TBaz(DTAdKv9mv-oFoMvui+9Iwwj-1?YKo_ zd@0Oyz|U50-zE+>l*1A@z(JO?nGW>2L=3b-RRDfD7nmFBkqCz!X#)D(%fe?Z#QIw*ka9TLxE`SA!K)M{a zi!a#5UyG)~h13z=y3iaoRpuA8poEXG5>u7x61H`g-CW@ zom)UXDihowfVUdKx0rQOsu4#3x48lC3S3zSoUO_*2OtTOgsH?7rmTXfkW^-|(FQI6 z0#Ml$W754pzzWg#u*VR{Y6ZmIZSB-6<=E8W*q8 zo-%v5Ej>mj;}I20I>e_R%J$;r8spcylJXnz5vd5F3vQ&g`?Wwj7zTyla-Qd*-&1UL z2fOrJV(M>D?^N4iW$3@qkq5m;G)Nu%`G+1PBUY)08cuZpZ;|{Ud<}mc`ty?ATs+fA z{@;=#8n^bL!6TD+xvoF(L?awL6&fqV2?2DjTsD{SkT(zTkk9QAxQK@>@xXkVKj+$N z*afVZ8#@$GSDf&|7O7x~8asBfO~8PJ{Hkr!8*Z+^nc$l)y~T>;Xq&9!9{gu^9um!kxjbx< z0;arcc$o{&J6V9r^v?z`*4OIHxpONW&z`Q`;fA$70_XD@wL&=q;d?V4W(0d@7s{n<&S{O$PhwhDec?#J2c zr5o~q0M9vt1TV;ZRpR(kIMZu`Ak*;fE;NUnuqPrgQq3l)Vh?*D zD@FqffDo1p8)>48QpY4k_6oB=ow~>zE-t$ZXZ)b1o@#Xzk9!c2?eP~}7lV7BxcP<~ zus*HGVI`hR$JBG%9^~S~FYjKH#+i@B%rxEPs?sx&fqJ0nemX8!mAm`F$;P*+TL(kB zJB7H+Q|1E9)_YrPko+f<_y<7Q0Yay1hd0OVv9z7o0l)8}jDLes1j{7&YBLES1hB zrO>7>^~^`|)#>J{foa07XgaoWx)CkCrx|$@6W9Gz?@;R1;D-md>Ax6h43Zyr#x&F- z#O=V4^@`kGC-hB^E>uThsp}1{+-l&ZIV{D8OVD|J_je0!8x_H-M4(J{8~ynhRe|yw`X0WcVy@7$OMxwhRBv2Z;)a z4(Ys6cIn1)K0em2`b%(NJ>xzUcmF}j{oQrRN6tPncy*sHPd8Xt z<~?Wc94SVpAaErEpUgY3@YZPS>fW~PV9;EDVuwye7Kff~ld2oi|H6ISA4itmn^}C> zoQf9gLPL$0Z}vT3=6_RD_~buMhdB@^^+6WOu;E)eP~1*g`q%?2xR!?{6+YeW-&X@* z83RLT5c4E@b)nsu*tDVO>_!hltzh2s`3n)*!iRB^=UmxJzr*Zk8FsAW&Uy&5L2lp! zrSYzhvR6M^{FL`;Jpj>BU1PG)rzmnNjt^lf1bB)cu2l{7@RbFV&KQ7gmwXMy{l7^$8rcgYQ~GVE>@yqW`T<1gNSGQ;oN zDU5NBIs{p;stC?}27$K?k*yLjM!Eh~dD*M)M?+)%@@nL;Cp|I}*jN(y+FXtOl)f>G zm+!As|8!la?FR&@-YE4`B`W_=U!XO*|I(M^mArv^1oxo&@gyP0*x*E0E!rmw{d`+& zWgC2$Jk@&@CNEu9we7y*FuIJfd&v4swfH3^9}vh^r(cPYWF<|PK3#fxEQAB}%q1IB zQ)~0Dinec{{tNPvm|cR{eVLZR;bx$D5(HnPVIfTpu$Os z69HJ+ zaHkks#6ON%kR;)qe&{h* z#yLBv+wuw2Yi0s?RM>Ud);2Q z>w3MO&*$TQ#|_NPvmlLB*z*rYuE+m486!PMVH;c5FUmaG;`DDW9nz%5mh*n~?QOgN z-vTf0H@#fcE)7W6SCy4ZI#;B`oiQ{uhGHILpbJ?N3~d1Cm{+vRdbuDjLYg`-)8Zi%i{f_4G=3BlH#SH;S>xwJ|lYZJ1zlys2Id!-b17x4<`_?I=$bRJpBe zF{@)yjux@^9oD``$+RLbEp#N_A#o}WpG7_3G1D>q)-+Qe_ri3#;&5oj677dGy&6oD z5@)?aPX#n@eGQk$4*SF4THiFoBsL5@A;P?Boj>&zwM)rf2z;5`J=I7Co2;UiLup@LDxdk`b2$Ave9~OL(3iY$B?h4RcgD>KF zGdQ{oq3by+3%Rqeu$^3~KtOH@RJOq4JA-1k$ET1$Wj*R(;uUVVlJ>MO6v=g(xjQltsZ_h1A1 ze{%~sP{$Vp`Gy@*=-NN&((>%1ECCzroJ6^T*5+B2hfrD*u)Yu^$m`KuJK$tJb2I@P zYDU^bV%ek}#qQI#>scN8l{r*K zru?k=5LOnf+9O%>U(WBxr6MG-q2NHathDT( zE`9Yla!<%K0*7l&C?!kV#n_-NqZ#FRd=g78+#&0cITc#W^NNB$n@@_^_^h_!P|Da* zDH%{sKuAzAhT%M%f)!THW+r1o%wYaf4{8C3z3@OP!BSO?k!Yf1NL=oJ)q5EG6yhgf zUHLS%gW_d~Jqw2g^bSUlK zQLM}H8z(g$dLvVdLe&}jtfx5qSQ*>fAr5i_addzzO92p(EbCML0{?_KeL|)-!FfeK zx`wpL#y*^6H+@V*XJk5#7WX0B=fzCweWI`zB;sa-e`uc&M;OOe(;im2cv5nI@ZZ z13v!LbSJcr&iKl~p6Nm?4N_5bm5K-Map3#-eoL8=oWW(H=KByfBmIYCUfck*h56B2h|eBAz zPRG1{uvK#$#-_vVQ84lv#q6=NjO7KhXNoBj88NtsR7vkH=J@ruSSWbd61v#5XxD)8 z#|@ayWL?;XRm(&J*n%Z=WU*@54i`!o22gJj~9_# z#v@>6W70Cz_}=%8_G}F&RFg&H;lzxA26n6X{5`!>;F^Ibt+3S-O$<7rOQ5@@k2@AU zs%ifhS**WpNSI&-aC(N4~9 zpa1|D_ldHDC=krHB4wjZi&s*Bg=dY80UPQ>*_^ zp^s;v{702&nY`zoLy86rPlRexLBe-Afx#QrO>*s(CYct5QsK949eX!BINk18TcS*o zgOu;Qe1F45W#GZjpK_x5(KrRVI4zxr-Gt)pn`9QAJOK-!#d&4OSe=>zzm;XI69>HlAn$vZ%N5#_9kx#zl{?_=uOomX5ent$1 z{=mj%)*q!ETq%TMwg?hUcuik~KNMp}km4`rd0NcT2(fyWE7csRp+~-Bc;5eU;r^Mg zzQB*&{|#o?4s4nFy-tm+wq-`Sl1iB6nCEdMuP8| zpV_!T1E}?CE3G7ZiFSYcbCsL&q|DXT4(q#cp6g9R227HsJ~i)U4z){}p<{n$-=g4# zH}?XrQaD}kNt=%Frh}n$jVQWi3SBE(p*7<7eMm}Hp->XVK7}uc7ucqvq;!Kqk@P|@ z43-LW8-%;duPv0IE>Zz|YNcx`;Y&B*qRyRju_CU>ol0^1*#*^CKnAAXNQtMc8kber zJ+wq~THut1G2uf_DMD8O^hPqcGYC3c+*QbQ@$M;~B7|szUZW>+Tj~a<#c-A&VA6~; zdMcD-Xy0NW9)-K{0BS!%U+9GZP?2|veYdMoxgx#Ra21F z6x`5F;fz)XPh#1l)uB9);BK30+Z`+eU#n$HU4qw}aHjH_{(4Cu5hcc{n0!M{YPAo| z6#Cg)KTy?7y@q!8|EyvMk5+T0#CvUZLZ`%mnP8U~9tue8zgDCq_X(*2`*M@mdT{wq zL-q4pL@@uzs`?c3cXV+G&NZi_)HYMHGYh=R1iVL0S{r+IKRt$#<2l5$Nl8dwU1L%t zxKmjb)DC~g>vbXPRl5UTK-GV>px4}gxqOp`r!Z;_(mS%K;^ZYy^%mjib6RfZ+b#B+ zf?2bPgyV?KE8ut<;{5Q@iEn}f5}cnxIB(2DEcsPRYLqYqB8#a59O6D5cS$`8ZqXYW z2w)TNK=WB~2p63h!ZDyR-{jG6b{kI`NqVe8ot} zLol}*i!JrHIYS5{?ryLEWDrGUNHR9VawSurPQ`=Q838;bNGw+sek`@RUc@OuG*;Wp`w+p5=Paq|;Y;mZ8 zbc@UBwB2AJ8BN%XK2Ryia{@zYtxU!>lEhL|DfXUF1XCrSG7AGv=p>}v^3Ps$9&-lv zPvunpHhX{u16IuWs0jE^CAWMpmzN>!Argg5D!ZP{Cfz2}rJz;7*aQPkqI&yAyPY zlN|QUV-V*g#)@h@TN*vOZlUKMx-^>jw<{;rxgZDW^eRIiaz_YT45!}@>t1kXX8*x` z7o09S1PLThkHUz8+EDg>R|WX9z%2M%#g%S=C@%{SgHr{PpveJc6xYB-+$B|B$4lqS z=|FU@2p(p(>M`O^cL^GTzi6RQ1@&2v9lj^~Ms`@Qg@Zv<4qpXcW9ws_BVw(Q>oV*& zos%Sm;gZ&rHb*|pOB2581l=+aG}l2H=KiJtt^{rrN|kK+w3-&cnwx%b8ew~4qQ-X8 z5n8`Dwu(!I`?VtXyiC`2|lv4EGslA*r+8A`8}?&>WaWP z5~S}F&zP3fE-lsSG_kPe>XYF!8^ETb$<>ahQzxvx`KJnc5jBEX@~q<#BEa3z1Jm|p9`-n zjIXdq&p0oaP?X!-Eq_2I;&Od98`MeoRj!BaFK+GdBsQy&+uT07ciMkfNj)>3s~ z_X$0Qm2xy-u6o`{Fzys05yYq$%LCU;Bmy6<;QKw>^em+3a>u^>b?)O4U&_~Bwnomj ztvZzjPLCHG55U~c4EQ486{Y}AW{6rSaBml1+Ag2>fB;u2N&Bk+2z>0$6ENECDwHrb zA5424mliIq%pL6k0)9^#xYS5OBcInD5*^5Rq-y~b)JRG%t>a9QsK!rqrtC;EeFU+{ z@L%-yPq#4NZ3r@bZrjDyrGYGOpm4?Ea=`R2`H<`DK{Pe2B3?$eMB2+GbomQOWZb#m zF_pr)+={rFrt#V9<7sVc9HiiAqU@)IfGGw4x||G2jVbI{XJ|(}rG8I2R=LR~w~R(t zSqRLjmMNBC2z!Fv|1eV`I-bs}T3U2(x8&b-1S3-t;69bgyEu@=$BW0jtXrhKD18G6n(a4{X_T} zpBnM2IMDUVvl?B{w??|)R1qij#%^P?+kcOm>K>XQ!mm)?(}zqB1#kGTzxkjT7DSiy z)rgdF5~nEyM`q&5KTuHy9yS0>GeH(ohHi(SwChIvhCVaFAU+OJgE+8Hy?iiN37nI9 zS=bNfuxSXs_=YBED%0vVh=psh+vSd>wB5ArX3ULT!q7i4=MQIzIv5-``%xg(~i3)b{Nv2W-5JgPcvGC#Jhvc1KmcY(?&O8EO#|PBhB=FnpWjji!W(}`7|PW8;P)u zyl5NMZ5!na3McouH6-*6MD}6SI8fn>16v8b!YNk0{b|3s_egx|qTEy%fzK=M5?Hto z&%dbmvHW46xZUY^1J#K*7q&uDyiN)OygFqm!S|4^?x7zXr9I)Gb8{hvE(xMf@8e1u zlcfKpe<_aabizw}idW=9pHm_vzm649Pp%PB1=ZHQVt`!T4kwQwiB}$qaZ8r6KYrgN ze66D@!U?K#Gjc}}2^`eIF0fSt7a1pYi#ZaMD*5v`J;xEbyc*;#H(Y`K81@87d@Kp9 z@p~AiAF zTi;?VfYat0^MEjZ#rBh5kKLA*a644E8RAf7$d(A#slOjk!{Pi%E+Ct>@`iwd%KV0f*^EyK<6&PZC5^c z@eml0S@8`KcF`Bih;8|&wquPt5&T$V;=^Aj zYj1O;pf6v1KddPic&H|5^oz#5nn4HdZ$i4Hi+MOCrFVEmPP1jd1Ci#@GVaZS;hg}M%sRE=3cbOrB=%)!t1InYa+1ry=)H#VAVZTPFNIhr-o}(J$ zLN=`J?`gLtZjM=oZ15eSvs#x4(K-4z(!|I|a;fvVC_BnG1TmD4cVtcE-6Mw*{N#O& zZ~gZLm8{JMzP3ChF4=YXUU$-LGRv=)67c^0;)m88S8z_rT3`*cuP`J*MCrpybcCG- z^C|yfC5(V$BNm7xOwUSmo{-7lM8m009QB#4a+nOA6QV5PhBW48O(OkT#yJkVCjkzW_bt;$ z4IZ(mQBO~8;#xklK2;(reUE8Ez75+|Cze~F=2C|@I^pK&<0;}S# zLV|U0m&Ow<@?ZdeFB-*VTc@3KAJR(Wg}tkF9-oxPThZWs8t~DwKAdyn4tDGQH^JBN zVq#~nRkb@gF$bNrxP1SgJvDt+r_O70Y%&9VNnCg0v|}B`>`mNLxPB{xQe#;T)i3e^2jo4kJ7(lk0<_DJC5{vY@MYWPg~9x4DD0TRj`t=B-c5vvy)MN-4gEdt6~cF8n&Nf;tTw=>2@|0MSpsY4JI9 zy^ymvD?z1|0n*>~!uThA?7B6k7bq;<-mm?1i9fi+RBr=|TmV7ca+U~dkvYG{j5fOi zc6dgDa-SQ;loOw7&BzOa*A(qp-6wjzrMv^ZCQciqR>goc!K0rWZ5?ILGcC8#s+;uf zPgI}+66ipEJ&cCH3GcRiGj3QabYy$Upc!X@!^r@$Nms$XWiBXx_BeDr>BWJXe5BfY z;l6~V8Vp@cjS(^ulOXf18_n4BRVVzmJ4ur8Dos{`atk6>K-6{G--nFjLrN8-CpRJ0 z=)pD3<^Gzy+}Wr=uMIc=cj{GrH(rH^U`Y~uQ-xCj5*xPyX%+7Ae{|faNTYWh281r^ zI|uNSAgmr03&M!JSnC9cpFS%y%%P*AS`mg_JZT7b+dQxp=KuLUQ{b^mg-H^?@kFNr zKBFEZOqKJdreyvJ0OfccTp88fX6^(WJ-oRyUz17rV+X|uZbrC z4KMaM#$i`)?RZAhB!;9{mdQd|V1q;EVzqgZk|2t6UQ7uCl|thw&sJEwuWFDXYZ6KA z?aNMOQniYx5{pYaT7sCAaxaE}oaw4T{TPca#-X)>zQd?&CU%EFWNIix`Cd--IOv%e z5%x}Ys{R%!J`=QYt}oyq;{w61M8{c`dQzBdu&fv~2gK@;M=}|~J~m4eTD*DPOIj!c zZMJ>uX^XB9ThUn7b*KVYYQVvx^W=>T?b*ftTvcl|Ut)yEEf@6)PF2gnu~ZZWUIfF| zr6vdN+78eMj10+NJ)n!=#3GWhQIu4o{~N-vpq}K!6S%9|(4c z%leIZz84>-DNTxN5oQ|blk2??2C|Ux3dSDt`2AcxKr>$8Z&oQcD=>`JeU+xfeGRuG zr~|+odS7bXpo6ueJYCgxsg|}j#y)hcPD74_PCCPBbOxJZ>c7jVKx2tKR(8-7FjP$W z7eKMP)oDomJ8JGG5ztOay@q4jM>$Hi0va0-_^wy3uJT5x}KwOK}UGtj`8?rZebt~BB?v+-u*GEkU$_lmO$A6a|{l)7XMOW`a zCd^g3erD>sO}Ofy2;@3ZPlSox52Yq;3O7J-PxUo_?5y6Y&9Og%y)8@ctO@^~V7ExR zEm{BbCI$YD$D`VNk)vW-Gw{~)Tlf8(e=c&wLO!F~5WAW7O>WRO8Q^m9LCO9CogFkd z%kSdD{a0O&xwO4oy8Ys#!~a}c{1?7oQE_qLnD64_E8E_O?|gdYq_I&`kRERJxBY{$ zZf6eqeaZ_!HtcX#ipj@*B%7!Mu;-1#vp~kgi76Q7i|+PNMwJn30BsLvi^*T>#QwMg z7lOGxq^tAx295%+qWVGYwysMp{+EA|T7;7BkV!blRaLp#qla?bXCvKWsf`V)L^LC4 zg3-&KU-mJy`SsV?KP9(L{{33)?oagCMAhggH}%=JKLOYO?8OpW;OeZ!8xKBn4Ep&) z1YWHWEvdDsO_k)4!X(FnRbxj6wO6Npl({S&$7(JD+D_Y6zzh|yvK#<}K9r8L65Q4& z(QT7_YQz40vO)4DMD8(Pq}(f;NJmmz^#vIkkLxFvFn;#sT;giH7@8jv-z@Udn6dOt{Yi#+K`kK$)ezu`TZA(=prdd(H?s` zk1-@eEBn`3jq(6%92_fySpU?1t}Da#i(K4#oGe6nz07ea^lIUt$s6Id_3V|5qxN(X+KgQ3aTxvOhm9NDxi2{rD=|*~YPLNYuWCRtgDY=F8>z^v;FJNh zuD8mH4)5ZKtg{WQDr2q>KhT;`?lOd5<%oC;i7ty-X4tc9v}ns~qlgL1BACkBTSThq zfnlP1N!N+1RF*Ay7ANyb*}U=RtYN3a{_>HyD?CySNCBdr?E!A@nHT8ORMDnztC@#_ zCN;=cMVSbBc2;vLQ=nV(VqcolddBQnk_K)LW}RvrXShP2_23YTiOdq@Uana_U2NLe zqc2!_l?Sudg=gRt%9(r}pi2Fh_TeHT4y*4DPem#WLsjGnMS?&{p{a6wSPuw5*|a)#Q1IqRf-J1fv_MPN0K`G*05FcrPyh`-uSwhEm3)4v(HAD{;!*VdUf zXqx8Gs|TJuE8ge-vFsAP2gegBllnKabo2%DjDMMf1-cECguB3>b*U((O%X_3#r&B~ zCq=;(v1F7dxM0UEwk=Xe1(-}0`A+~>jDRU4rQT--u8p;y-LtOpK{%jo9SRRV*0>va z)c(eGbs{^zdkuqLurH>s{u_o@4#-uvfk&Qjx(Kj@|I7APORvl4Htgmaz$vL{!U-!t4NLimez0_mR}fY z4rAR8?Zo3)x-5R~*}k57SJoDi`hHLRmQLa|mYQUvKYm(4ry7V34CX!h5q+E1E!eNO<LMG3-tFDxzepCX6?e z0_NY&8ML|+y1$EIQ%PijvETY`S=T3k&P4e2lm1B6A966|h;N-KC~9B|iIt@~fa-#$M0es~xcV|%dH*x(ei8`(xD=*0CrsF-u$+LE zDPhDvCM!UAqaBQZ6OleDv&yvyL&8V4LIU?Rp(`TjQ_MMM5n%#t@C!Cv1p8|DNQ-v* z+gb)uq6sr({4j$PQl2DCD0**!YJdj%^L0ERq8!#-V+OaJ1U?nPrfvbm9-%BsU<%xM z%e|>Ho$?Z;{I{!$FvJkI9wShbHsVDtSL$MVv@?G( z!>wz!qJAEEAE}!E8sUA|isi2T>=qcY2f8yIVZH%Qwd7}uzML?E+pN;i-l(*2tEC=* zY@ZC|d>NcpFrmp*G2ijL)xw+8Mr0`ew`HTOHEe{CmvKEPL8CeJkqQ zt-1B*deb6UfgNvFeCTrWj)U8+G`7K0VCnPUjO{Ay?LV63JF(8z@0Rgd?ZSA*neaPn zSzS<3(sol5`Z*rtvoZd|F{k~pYnTVtsKLhiNakuH6M18ZL{}}eJh7jqGMs{qxh3fg z!E$I!GsskPrNnB2vDk~%W~s`_g2C(pMKKy4F2MP_rQ{K5Gw6~s1!F?baU-r|Im`=v zCe-j-3{^?YoFNYg^SXJ z7g};bs{v(>k(9{ov7(>$2ZZDShS}&%auF-^+#^ezQ`nHO&|5?_R7#W#Vh9K!MX<#p z%c4{M!6F6!_MN#x)3fsfzo**>jS`Cl)p>mpnQKI3iL@Fh-H>v;N6*kqbT?X1c$xDg zP(<$%fxvqDmKQJnK*8Y*& zC#oWjuA>lT;XDd?5@ualYV{SlArWgS3ID)m>vu$74`8B$nbwtk`VRVJB;3#uzEanE z;_i?5+H<6jce$xAtZ8sXwsBSZsx?KR9!*r>YQE~^hvb2enK*%Bw$9B`<_`&j4h-?V z;L>QtW|qhZ2()1J!+876sYi7vlxVxK&CjrBCv)}b%<$HG?dknHjq?7sg0=)f+R7k( z-ixny=1;?7zebRCXWcwz;YQtT_M&Rgli+ekPhi>Q!r(<&+oiCiJ2&8%+lR2JU2#|M zj68Igpi`FqS$fL~xoK@Ed{F*MIvA)!8(F6Nnv#H0D<-vGRa5Ay$SxQe+XI_==ui>h z9ltWV#|9Jd&Hv!}ub&jLd8Jq?V>_f69k|cGloylAxU1c#PaoI%C;%Lv0Dq*{J;2RD zqWJ6^1P>PGY<0=%;9u9P$rwjQ!fTxF1tK5!>M7$KpuPPi%X_UzeeSS!*oB$7x42k| zi!o{+*}h71c~ZU60a1pn8U5?3*hpj-xSj!DASK?PinmgkRCmKYVnl zF~{4iBHr`Za65YQ6MVM3k<=r^(wdfiaesh;nSpP{z>mR1(fun0F(aBE6TPopIofdz z)SI|`?-7u`SAkDic|%J?TqPn$T~H_IBcH9qkKPEE{-A}axJ5g3a(6Q#MVs)zRA#r5 zcI(mDf%Ia!u|lan2&K&2fo~z25%CzJQbv`Du zfYNVdZ&AG}0MBYz^fp5(5&XY!KJv$0>c z_on=t_}I84jy(!|yx|}Q)$rB-kW1)txp+lyyjH0%?s9iwP zhgN2qI#?nd108cE+*!5OHTQ;wW(iq2H>&l>(xt-ebr+IdQ7hEY#u3?=x42MosXCUXpQ9!k$n!p%Z+bW2ht1-)8cwQj}-MI4#Zyxdq%fk7Q-m}R~XW3@m`3ojEp zvj*4P2AzO?IGpz?&h31k>g2ssq}B;eNIY_=HGyBBOqXiWhY^V<7>B%f|I@8IiY33% z*g0wHLbpA3P*eyDuo0e;BXn1huc9x8HM(*j+l2BVO?pThTC;r|>urcS)!be9TOhbp zh>OJr2KLJUy0hs?nm@F%$VpEt^G-heGgCItTJo`_q~VVHc)XT)gl+4}Nq|1PqUBT6 zAXBgwbQ|Wo@C!q?+4GkK`ci==dMXXbXE=5snqPUlE1-}ecR4^XE2C%?>bg$^n1me# z%gue=J0YAl1jhuczQtcgJ3lWw^XtT%60ew2VVrz5T$4Bk5;O_z@K+qp^ zY;CHmbFdA@t$@Q!xz0U63D_sIKYdcWr~I4aMcP7f;I0JhnTdwmj+cpNHFrGhYBx2ZyO^SOti+axs?WXwU9--(glSFa{Y_4KUWa&a;mk>7&scHZwtH zw=o_OGLn8ES%BudhtFf#>e*J=aowA#!TR{_1MqE;Z6I6P9}F&+$NaZQDkDlZVIz4h`<{TWkS; zcS^(WBXYOI6?9@ju7bu%z-x{!Ct6HVCm{MU_a}DNVA2G}pLaf3(@WQsq6fYoiYK|i zmFMQA^!GG6-%H!62Tj$sDbw={ZD{%RnaZ@1bsR{Jhy3BDM+d*d$H*N%9~FN??BQ!( z=$YtZqmk}uLPF|yYwwS-;L8vMF;z~|OXOfmZgYNFR=1k#DuDP*3xb4urAw{8PMNcW z&j_@>_bAbgwgMv{7I|-TA^D*_JTg#`r{%7WiTcS5%H0wnhfn>!)$~QVHwSP{5)bOi`@iE0gGJPVL2*;-pot%!VTa5H%#d zfaF*BcacF}FEFK91FtN94h8xB!^nh&6GYS+w!bB|tYFmy`IQ*(Qq)Iy&*K>uL#YOV zG1!XDc;QvNv6_wr-&w5Hh|WSu(au9`gPscJ#*%@^O}N?fDY@}$`cy><_4nF}mSPg@NY1 z4*xbtfaHK`dE|;Wezw8vjpZ#Gj2ad0yBsyhZo=fYgO-kOiF;~eb!{!ged*j4)!0sr zUl$dCW((~?EYl!6bAgRxT7f4q5V`7C%@ez{0#3`9CeEk$`A~~A7@YPV+R`q7#a52PW}Ck^b^5OK>4DP76V#H#yg@+;mYLG3G0Qxm1+zowGostcR5R z8s-9(w733k&@nD62-I!hmQTF^q9%|GLpb*DFRFK1KHwLnLW5mOvSWwT{AsAJ#)`XS zLG#+T%N}Uusq48>0f$A6pD|^8mie(!sb^OTWWAX|GIPp>ji~SMLN(xbRqLp)$<-}A z3{18f;#mY}cCAZ^*N6HViEUJ@&U)N6 z)4{1ckC9mPUR)2!cv{MdiZeKZyKWmXUASf>&g4Jb4X6FnMM+Wd<}YzKJqD*svPR+w z^SEAy@mE5JOna>Vp*QpC5q;gJnX{xiLow*T9~cTpR?UYg#|8vJ(n=g3CCdDDKO@r)c5&G#pm zJt&BnX)t}x5Arj6Shjzr(Kb3MWWCv=+QFG)9?z4O?=>5!H=b?bL?^F2V)nQ-V)po& z=gF)8GaKyKKYKDMdegd>W>2~YXHR84-!#8|-fT!|{Jp&(IwhW9KHL}ay|eat%0@r) zrw{gjKie9;dDD9HXTyWv&v!rHymhbn$e8huuD<9k+mDz(e;4uN;_&k=JO49(F}?rC z<#*9rcfT}$`E&5c)t}F|?wvOug_+DLP%)`u0)7m&dQN5fA~oL+KaQ!M>#>bV+qWM7 z%Jj+H4UZRTrF-!cBomJo1BVY06J{p1i=?;oL^_3CQLi1h{=B{3@mtlS>^Dv~f8N@B z@9TlL_&0?9pLf>1*nU9g&>K&}ulrdsGl#79ykjre^{Cl#_CV0zx4wj|^3tnYV1K$l z?9#n-XmiZAh-SRWf|tLHqI32|m-<{w4eEII%75p@O+UJ%c83F&e|}-mw%q;8!ettd z(y%%wygI%}K6~2x^5)v68i;3Uy$e%GKSdvXeD_z+jP90Zi8EgiOYWbYETig#Rajy> zr2z}y-`lzLG7^f2QYHL*N7*4ZkqTGX^(LH&`$FwX z{v)A2UiW0(*%MDBQAUo3?D|_c#}ZGt$p!nF8rp^>TxlwWDbY-~=np?J$XGAWIiacf z=Yo=VEf-~Fe6VYJ>}H|WqP#m@oi7!YMjK;J4yTRX74Fj>S#f^y`AIOp#ghC=TKptv zo8g34nd68pd~JEe%u|Y&NRCdA56yAw z3&Rebd519W9}H^OMFQsEcjO_f%89!5%U&KkD)b)=kx^s)eybxPQ|q*XSlA$@8sPL2 zS9mhtVrLNFxj$z%oLyn68BQO=gyMxgjL>wdnu?S`AKM*-OOt3)Hqf{faPbgzbf&5&mSu#bZ3Huph$8*(LlSgfzF|%^;ibcwz1IoLN-?`a?6}d!&kpj-4N2Y zg^D8eDaYvi(8&f3`uU`CjTGZZDZQ$f)VMFia;#BbW6c`wK_i%16aA+iGgiEzJd<_M zz`>4dWtFTr*03ewDW*xYgUV;&@J_H}*u|bOOZZ*ee#>0QeS*HaeORtp96WEf(V}>5 zvOUK*Dy_#l=%B}gX7j$ncVN$8p_pzw@ZOeTth<}rj9}Q8oekoKY*vo}q%RB@4m_~p z0(|oasOvEnOzs+ROOu@na#e0Qi<9bLI&PC2Khl5fs{7LRC#SOQ@?TAdDFtYfGNnW= zI60-#UGj128WiL(-D9{qW%@d9+O)ByE6kCuSYV5BuH?_P~sUM$P_Jn&V7=;d-IYf+K-EWyhyg6pE!T50?7(Q zhOmOTLyu;QWzR^u(W;>!_JecxWj7YcNI@U=fHs%ieEMH4ee!4LW2?9srm~qW zi(IeK7vuKye}L=#4g8esdUo-re1h^f#v8fHoslqd`5Z%Dp^tq=JANp8DsH*+AB?qN z8FMyhWc77*s|kLH-hrM8VpNReNXReu9`(~BX!TOKcPnf<& z-^6|S5Bz$iep)|FVbJ?&Q06?3n#hDJdoZd6&<|AO_ND$Tb-+ui`MOB7a2?plq+dGl zx|FoQJ6Idxc15x(RuFaK#jgmq%tX?Lkul{Ae40ytI8z(paZPi6QKTEJq-}$G17`2n z^nWd+WEERUHE#x8xx8?*M&Is(Kf#9z@!2V)nrNiWkt;>L(6*449?a-@nT=dXT0ga$c8&{49ZqO4}s^KK0pKX;9Lb0`@2~Sar zy5Yu=->UE4dE?e?dN;XF0rpQE`#YWm?$DPUD$j0q>rR?qzI*NDA=Gp;{aAs)j^J-Z z@Od*`i@99PbUAF(`|=^^c{ks;lK3c{AAnv9?Th%$UM+cCRU}G`2!O^w4F$%N`y#CZGQMATj**(}MYd9TZo3Z=!}$BM(p#9jA~O za2g7;sMiacQU<|eNAd+Q&gNfj^oOa>@^#^mZY*707YizOU=mMm+#IDP2nm2!8BI!{gaJ+Lenq92#3?PQsRt&l{+hp7D7cA>CV#lehZwJa1)-}X0 z(Vys+MY3P|6v(8tY;O>DH2?Ar;Bl@%cT?O{tQVBM@A- z=LVS6!NeBv{#o{@;L3Ie)ms+Yd!L9MLGCnn7MQ%9BU%~v;^aDI7~Dk9nJY?`T#osK z7hVZ+?^iSWUkTY4NS)msbjTV)0v_|^D;B*lO=ge;X?hdWCm{`V$-c%kUPH%9gj(== zE8SllbLCeI@E00n+NJ4|)LgFq=c3B)J3;L#%-li991+p0w<-gOj*XYEu zi98*0KI%&rKJ4p*3F_lS3lW%y-Pqx^}5fd9TrO@cMz0YX`Mduk$lwU3HYi zieih{CEoA5ZW|->bju5vMFN1w#_NL+I7wIDsO?2$W`rn*6zOjg}rt`&Urr;4?# zH%DvR8ZdkHAgavGKfUcnFs_gNA#W%1>AnId>Yp)W2dLWT3MCLg2>s$hiD^SY0h%@H zVWTQL;R8`NgPb)uhw^hC%jYna^rPhlpyvfQcD+W!$h!f?9k@s5 z9*uI;$C3*VwT3U4+1}YuF!_|&SojN5bQ$_t{_E`T2x+t4wigW-4nEfu3hQ@nfjA)( znC^v8@$%yJNL=IDe&tsGx)1RMycfhe_uq7>{mwmGn~52YY$)|)RHMK5`2e&83{vd* z?A55z{CvlJz?*crclyiUCXsWQNbk#!9liP6^6ANjX>0G>KwFKGpPJTwF%oIP zY?Zmt%WzbRLSRs~0c1mNfv7NYvUC7Cu^Ku?hL(1%^IQUXXF|v!XnrSzJC@D(PIhXM z1PMqTMw!&=IKUiMh^)}G{ zdBEzW#wN|OTrf5b6r%D0#N)*p$2GE21~U?ISuX>2<3Mrc#?7Bzry(9}dUuUi{d%`P z19+9O;k0p9xndhiNIz7)adfd%ze8X~N;q9*Q!}v7NT{`70kpkenv|*j7+E6d$$2uL zpivEXO)Udb!q3F`N10$xBqS3dQ#Xp&xhLdB#GKKPSbk5~(!S4Kx?>_%>r5_UH#hgx z1-?|i$Q(|h_%ypy)qJwg=ZfP$G+E){@vzKOVheqQ;vo0OBVZFSjA2)+t#1hmq&L5&}$Fg*xHoKC~HYXWfmajh5PdJ#I5a>K#9ZeaBhN0tK5S9YiblLN`Yj-KCYd6|8wC1hrPL^?VcV9>{C}WG;Q!@H6 zCh}q=4Jz0PF3K;uE+v=$jM?~ZeFV)Sn+c72AZLwt>(4=zykw0Phu~wd?UYQ^SVAd0 zAGu=B@CL2@GR>U!ZHUoCc-i(fHS0&SP_Mhz-^xI7nUGi}(d!px$jL`F}`zudpWG@Le}$NM@*$(0f8N6eSetYJdQt zO0fVU0wU54O{$ncfP@-42qIN!N|#~_RX|igMQjL&iXD`nijB?x+H3E1w2s#4oW9q* zGuQV$&wUq9C!rBp{aZG$&pC)T_PXqobGM64W}XVb(*w1M`GI=z$=Hb@2x9KB-qrklYioYtnqAWBtX-xQ1F1W z-YNh!UYYH_pu6Yq7{xdr?A-eVARfMqQ{3NlQ3cnGGf(<#1^aoxXEIaoKwz)cQP`yf z$XD}-2d35S_-2xXa5edaL<|XGoS9W*o6lck-LM-vnh6`ndNiGc3(2|Yh?0)tcA;}P zBU||0Ovp}pN1_S*&ISAaKktfo*pm)mex}{Dy@Y1+_5A1a*#vfp?~trW0d-pc7$UYo zHj!KPRmlO{w=S9{@la|tUtA-s>k03whoM=!in0$Sh9CC*wb|a)APLp zM?~XJdot#OHLk(Nuo0(}MU<*R9f>rv_W^2xg>ScAcl{Dd?Ne|lf511a@K@nxR7(?fcScMa}3SZuQR8w*gYha z^+cv8y-IOM!E3vdQTAp0OA%vzkR1=>5_}ZnQ=}^~KOo$4 zRzcL8ESul>9Q7KvwWFk3oWK(t%)#q;6)SWM8_y7KYU(}t?SAQ?ck`iJ5}SBU2mDX_ zVy8bN&T^x!W}`coh7UZ~ZvL;x*mqQiiE)1ldxqk|DNQ(?(qpe~;$VP+D`6yEkXFBcu`6qlU&DTr=VLXjVt`1Jr zK)A#Q*PpKETS5jpw?2D@i9YPfsKy>Jfw3|m$_pO2p7o-V)Y83_40zz=Fpm+=A~ER9jNZU^RgRH2Y*cg#d(Cw!zxQ4yzaG}U8VW6~FYTF(?Hye>q)tefom*vw*EIDE9C(&>TQj2YrNa5$&gJ(_aL2hEcDA z>j^O!9ni-_{^R)<*Dk?a!c53#1EyqYxMJf3028LSI5opI^TF>eM^wSnX(YN2RtUc!;H%Nx05-F+9WjW8Vl@=w z*0<2sPEDBPw4>vh`1?`txQ0>B%+ULbae&Yt51^D*znB*(W!&Z1YSu^Sr>?r#sj18CEAd`lem-p&8-h-3KweSDI zrt{jGX4t+eSas{HCiW%VXoAx?>wbs!u<;%4I2VKQX;XGmnb3sos_&qyy~gEUDG8+q ztHBHe=t%QVAjC%6!M<|y{N@^QYcG)};U`+K{$oy2GvAl14=%;jThKk{=NX0+tkgyH zs|LXASkY0#uOTftk?fN_kGyQ(-NL$DXib1|)=fn)RC^RIAVjam)!yNBjlH%k_6p$xK5!9RKuVOFd8KP(fai;=UPO!#gWD@2|-w{NGHjHi0qF~^% zTsa(>KFecrVPPo}o3LG4%=O=1zs^?dU1Z^I|5b1}GYPP!N&zd@MK;j~Mu@g9lgxPM zgb`&IMxJu6u>MGy73G{V8}>7wSOk@AsbqGg4 z01rFyrF!`txXc?w>cq$u*+;q>v?e8&Mo_z6Wi9#@u}fS#bHwO`?*pFH-_E9=V&k}r zBmobL$^xya;^I6rlN3>+7>nQVJt^)TD03;%9!bfi{=jambEXt$} zsdDkSr)eaEh9F)<6Go;nsPAaz5gQAAw+P0FLt})%8W%5gu_;3{!OUyhW%5qyjO9vF zB4M4We@+--D^v51NuHXGa5@G$ymcO8;eXDiP8eldQv!uQ`D6+!>!C)#!5)k&Khe^s zPNR@Q5}e>Ek5#X#^q@rFgL)cI(5f>dsqE{(kD;(V*$UKgL9(zvNti9h;@ryfS^Nkp z7!QH_n!7*E;ML^#D_5r}lT4f1Z1bw?2koE#BxzXk6FQz|`m-9Xib7kbvv?z~8?PGk z;|z3!PyH2A-Sx)t-BFPjA8}919#TOPTP4kQdwMBU5rrPH&tQNd9?&dmu4i5Mx7oAi z2x|Xo%YC3Gm_;0Jm8U^K&vGb2XeqVRg+2XRStHEK`zOe8zrr^9A=uGhD{fez>yjud z-3UI91bYpPVofasxaWjL8|-U`@iQr!>06w@^5T^V?<=Kp?3YFGbK=7**rzoQcxKK$ zwe@$xC!fa)42UZCAgm@nvwdY!KY!TXhppNIokz5~Ul{zMkBIK8+&d~8`zK<%y_6I= z`LaRI@Ve~%KauxLfBcD>wv;>*J!@}xW8?1r!)IdVj>P_rnLl27=JeB$-ZS4Ho_c&H zb|JQu=96uf)fT&aceX8J>Du>*=sm8UBMa(E{o|i#jD3x9BhoUi|2;QMP>5z27HE^B zMNYQv_UW)$-IN_DDUF1Ob?sedhHnQZbxmqZC|z0)&=UQ2U?h1y3ic+?HWCGgW ztltkM4uUHFQN&)=+h@d$y;U8etWe;acaudYEypjFgo8{#Y~FE7F$ZNLEOPL??3XAJ5v3%HdB4=t)NSpcm zH(?0-U_NKJxSg%UN*QM@$G?g+XHTFdwKDK1vSw6a zH4FE+Hq{1HDnys1456Y5UTxjjq2q#^yyS|q5urc@Htqa0iFIlXD?3Dac1U%kZ#!2C zh`LKt%w=MR?%*=i^PTGsd0QxJf-QCysnGHEPOwCrnmhaioRK1G82bA;pQf_YH#Tro zSG-cX14r=>skxnxPB^c0Bxu3SHr;4P(3#e8i!X)6ObVeY)&a8Q*WEMzm$P>RQnHrQ zfqX4gs?pMfGb`QWIhCjW2SFuEey7(|ze2c}v`W!3X?x1nSIrxwst7afyNr}P{ETOC zDmAiu{f7cjFwa(Q{m{2@j>d?I?GZ!NcG-BFpc}+U{hB+a=fR#PM#_&9M_TQWkDR#)%3)v9CuVqnxDdpcJ`Zubbmpty`=c4Zmgp9>=DFaaj z4z7IWsLclv#mbT>vZ@=*ObIZyRJR!T4mNkDMEr4uPbIW!j!6S(X^YFuDKU(eCeFs# z5Md7V5W3~rCb+NcddkQ-@rlMEvx;MOQf1+poH?7SP8%?6ggl|6YJrG4wx_vW$pMGJ zeNBEPCN2Nx23d}Ugw{f?9-yX}$@LH++wm$Bd99eY9>$T>|_&Z1c zMV8(6dZTyY=x~tmwdXM+J$x7BN#|qN&3~G|g^@>AZyq)<(DHkluGtbp3c2=EcObU( zpW}qvtlJ~L&XNt2ypLX^=8Mu{3d$TSDVJi_{Z8_)4gaOoFagYjR+HvGE|$+Iu$b=0cXUACzPIc)EU|H4pL|j;~GsCKgFGV$gU}8jU?D2=qte8vVtYf>Im?gTd?O%Q57&h z1ne&F%Aw~*+Uv2VE#CwJpEdS%(bY9xT`;O_u6Mb7i>1V=Cq+uBxO1p59moT)DQIWc)%OPRV#LOlMG_0dU$%Xps%8#T32At zG)sR?z(ugaaRl_35Zd`(U0=CQnX#KCb=_?R%Ihn_^#x4)%6>$5NW0}@R!FJTo7JEAi!`v4Pird_2? zTchw4N)j-tlBPmvBcfpixUWTnO+a;UtKE)3iyk+;WJ%{DXnP84rhJ40o6xPMDMruP zO_`bRkISFGLuy8hDX)&@%vUuA4%>z1t9O?ER3BAFD5o*3i`Zz#cH2>3D;DYcb^)Bp z711OMwG`6WeF|Ln+pH0*dq}mlmTsE6-a}n+p1Vo5kxjaKN+Ss+PUX~^qH2wSdxc~2 z1NoQ9W~`r<&2bYv25|?!=+9^DEOPGE#PcaqX!}0PE9%=bP<-*={Qb(jaUH_K6XZ?` zuR>Gy(MXj&FZHqid8zA;i!9ET@rS#MT29ZHXfn9tm6ucD9mh9GPOI5Y?<$?%w>o`z z&PE|R|GbjB5iK{@J@O&6az}KU3giMW}`ZXH`Bo;52kV&uPu8gdBn5ub~|mD7Pq1**Db)N zLe4!qL5jS%hdB(sP#Ry%!Vhk1s4SM9h0^<2_U4F|_EOupga1+32tH*q1ZSr^8##?^ zabcZryu_3S=lrg|ltNr4pePeFyZ?Z{%J@zG=Gd`?Rr~g^5oo3z?ZQ`0j=Nb=9OT!} z?I)J>va`3?nFf8ujd6{|YfR>IO}|!>qC7(;O(`ALFQGwqQ-G+M(TGuS!k%-ODIn?T-@GaB|Xca3z5JENZb@tgz?r2qkndr(q?w+blWO+bg0tuj=yyw!5!v>H-1Ot%6@u_(SFsLv=ic@G^?!w|+GUNU&G;S)j@Wt>Nm)t zH!&E=n#O{cB@cBJ3^N{#cHsnWDA}#KD9)qxNNm0nAeyrOIVq)Z?HLmkxnj)w%+B0; zl+`y=Vhl@pm`r~6wk<$2Wc^~ z3y4-_XV^m^CGpIR;--vDq4pCh+uZrEBaoX`BUnTQO%t;0;((M)G?k+;;F3cebk(_Y zQCq#>hLn=&D7zsEvR$Pr9c*_N$UdkFc8X=GX1ZsWWm0NAXBY+d(m3-+$7zkt7QB7G zt}8KpTNccSQszaCM%QGy+Xn!dYj-Q+`?(L1{f*=K)N8&hqbj7&kuCP>+;u6$Sm1@a z^QC1thP0=HX%mQl}i(&@wgdFjKA#y zSa#mu{-(dfJ%7gqf2Ys>`~Uen3!iWic3O}>;gjN!S7AYalvA{=K|h)DIgam0}>?jT$LSYBBHWvyLKR6!7=ipaq&*gu2y#0 z673ptE}_bJG9goTe6DO1Y{qJP18;NDKn^D|*X;H977Th9yytrKyH&U0Pg3f8e zPTLqUN||sodPwQot0JGK+^T+U9Tk$X0TRq5a;A!I-tDb!ylJ6!%@z#rdyn`=>2VY7KZp;mCstRtt65Mh#xbBEq4)oVP78<4D1^=K z3VX0O?4eWGBk!=e@UX|pVe`3RPpZP6UI}}4Gwk`juonwq3!lTbUj7SP6b@ff2w&b6 z{%UXdic|P&@9;O_;ct_}S98PPRfWI568_<4_{V$UpBBPDe-8ihFMLh-)Vji{ue(lt z+k0xm>C}JTr@n`u`jLF{n${WHa}x#^JB(Hjvhr$eo5?weEjC(#N;>JwpD+hb`9LZ@B`IW20fZ zZKpI38|!XG=$?&gOHV7)j5IZTt-rt0sCR8&o}Af*&_fKi2f1=j8=IQZzt^UcB4VtY zmo_q=#x&YcuIBTkK=y_kn`Xi8)zM}18zbWzr6C)`@f#(gG1<=lc}PXGxY0$6>s8h< z+fSTrpu>4z?<+)m7^NJ^V0&di?zbG>4COq(7`g9Hizmh$=CQpWN3)#&EA4W2`1j2! zw!&BP{c#cFgAB-YTNCgy%C`5b;jQoG3*U!lihO@u{_{}) zzlTT^@8b76<=>doKa?Az&Yb-bskq5ijL$z7Uu+X!xPP}F|fe3^E9NnU(~P5imo z&C0yZb8q9%-`cFo*u3x~zIw-I_5NSw$L^652%)I}{T;wG+GcC3f#m>^YWr^;BZ-*~GrQ#QqD318s@dZY5s7pLpYC;>|CK zx3&^__doX+hfTcyHEob|!TV{kDY$ng_`&x=rZ zaQ9m8mzTU$0Y#_lee18r3pJ13yWaop?R1^}1*bjwQp|1EY2&@4jTy`*ni<_0{Ecmj zj>39;@O0-4ji&cRwjtBy=4}bSPqAX~ZS6A`&`DH^&#EztLqT7Ro=CX(_wjj~UDw4H zQ^gJiE~QBYG%lsfonBqaAf%I)*(xQ0%N)|h#^p?{tEgJ z-&bGdQ84N&T&hISNM)2>Oc#+Kieog@n;7p&dKp@G8y!&>I)iOYOIY_LaAMfv(j~(Z9c`ku-d&6sBfG=nyQqa7+;;a2NdH8 zm*b+_hERN&crnK5+6h}U?nvu_Ai*KaQkkIhq1+a1suwBkDDCtv&mQp|d^%FGpN9rw zHG9@g^C`$V5GkNQbB`Fwz8dD%8a?66Of6J$Q`j}F`^GMHM9q0&GxTZdq-dtXngQ}T za8bNg!ku$>@Ck-*dwV(}{H^C9f7#ZsGk%}5O4MvM{H8@3Yzu7hl20D1AaADrG8bn;+{ec*D({fr zpPtphGnP~#g3^Ra45EaPV~KOFr?4AvH6@9}w6c&R5(t2+Chcd7$`Ty$MJc>Vdnm>g z>%NV|8gDb9vPc1kE|g0R@i>4uWmq|P^*>LSan%v5YCd6^rpkWaE|PN%z7OQwyyKfceR8^emtMU@RToIDuE8d4pL08=fg8mwn zP{JDy!RR7BsuDaI`Gsr^9iEKvv|s3?tt@#-CqtD*-^UzM*+S?rbg(5{O}#;-6O4w4JO-ik^~54> z+Ki^A=gKiA58;k(tWacXKz#>PNnO34xf8Ti^Rltiiru#Z;BPOyE=Va^@07__&{3>^ z$izfvx@e!}h{=ziNGqDR6p%Y7c>*sIO(*bBSQLw1zDC%&CSmTS+=2cZfWPeTD|p*d zHd+;789py6jA4)zg;u;!=B^^>nhuE(q`>b#xhPzs|MK#fmqiO)<_2vTz>*We( zp)l3Tphu~42=`A)v?Ii5viO$tp;ka=B-N==5(y&&(gIoj-^Ep)ngljr?Ri8*{_~V^ zLVxP25=$;}PXk3TW;@}fzV`J&#I=IYLI0(XF`1QzIa^yr2(Xm#vSRuL|lo;DZjeO4s4RDhaDRV}K7 zwVhG!Np>fxN=)ee2{8C~aU@|^M~234hKTWTP!5#?Tbz77{dgz4Ryqf^u}pB;u8uYA zPvly8S=>5w@zh=y-8bjvHj?Rmk8;u*7qN$dM@t^N)1lc%hwTUE1%fnGzryl%G}L3k z^!5&`B#m&9i$ixXvldgLT4rXT$Tq$J_q>2%M)1Rxq<2tI9Uyg)i8+m+N~Z1%w3bQ{ zI$(>COeX=`#$78z5=VeF!nOoCgw$~)LBjlGDxrd)HZxo+wEP7)5FUYI?l$M|0Ik-R zD)_fJfbxQ|>xXJ9Sk#F}$kgJhj~pSm_jM5^a1WT>j3xFs`ZAu9N-?{>UwogD#rqo> z9YmGe%Ism{`_cCkgk|=Lc$O-`Jj$B={XIn-ER&JrM3r0N)Ituo`#xeP9JCObla3=Q zAfdveL;8l4kvSX^nogJSyDu?5s@EE!nsEnrShAGDLwu!)jEKa$P?H73KkW_DQkFJe zR!}p;+LMaO^9nRslb){^&9CsKSC1?aT&^|GtfFHF4+_1qxn#YUa$ssq{*r8K2(CRhiFg&Mg2!UVzwAjQN(vGl?Xax z3!TBqrNp0Nry*ysh#qjr5DLKnnS&;KBvtd$E%um0=8b$sJm#}lC@}!S=q48#!qJBC z#tt|KhK?5L+)0HqPAe9!r5y{@E1o9r8}@M{Ln#|)iINt^Y&`iA+^J?skVvo3e3dRV2?|Yksg)|SHvVKQsXe}=EH$- z8Fx;~T=o}zLP{%$GFqL%6s^&}Z5crMG3XTkFmyC@t;hBQ7*SHq_ngQzJOEpN3rHhC zy;s3-3%SFO^H9DL)$Xcy^-!J@XHRq;HF_6u??L8}0W7SpnkmYv`Om^iqsaz^~dGxh`bw^KNHBv;^L&r5CsGp8ZMkef^1)mVh~*Squ3a?b8`pi z&Ojg4*Fh1BP=nydaUD-*k9-0;uDrOw9j=@|@LQ*tPe+n`f~L>lCh8)}^MfHWrf3no zOoLL?;1lJDLj;K1PC?VvxpZ_l-b_%+hRtUiYdggsQBh_h=8ftoaR z$uiAZ@e3At^IPmN6oT*Zwlwf(&L=9IACHU|J%aYpH}`-mp@`yYJ`5vu_X$4G78dVU zyV2=+q)hzqN%*1;lyX|Sc+roKBVXi$y|)bZs+U{~$|xbIy3xbZA!LP`?Hd%Rz*A>% zad*|>Lq6D@rHDxAe9k)!-F%z~4e_{Ks!tW#W)(b&Vvu~p3X~D-{_-3IWVj>2aH7hD zgILys5AhO2EP!(m>AVMI0c%$H?KYiy?uqK(av3n#dMwtd| z7s>vKz!U1pIF4!-0zwUQ^yx`l@+BYLXw}Ib+r~vr4R|I9pG}93*2KIws`Cm~9N+q> z@A5M_ajXEfu3gITW(VO<(?eV%VFRdKTOSclP|PG47m+UjE*Cy6m2;Tc-lcHqqWn&J zQcinRZI9yl0NBnCpSTtdEn+0Oxcr_b!%=L8Gq|us4CHWA;^@8aqb7Smvs#}hAMhQ7 zO-Ga-e_#ptS}fZ}A4FZ=EzU{~kmxyFk--i6T&Voc_z+)*=HUVOo2&odO~aIGtWW{s zwWpZ=Nlbe!C{o})S3fD?^o62!l z^-V+kNrQZGJZuyN(Ei<}G=qtEj-Wm3cKY4zEZgI1+H>GY&!NN~_lh3RYdwbp_)cMP zseCdL$m+`A69J1kuy4EAAf~+UD!&e&e~8~*Y^e~EW*mgeU_kE!VIvqSb@p_Aqj`S< z{$Y&cS8o&v!j-OF`Bq=B+o0Dbq&oHpE=p|$p1v*#ZtC-)xPn6hMfq#>t?2g7| zgd{ea;`&YdFm%v0ilZTMZDR==N-0hwspxh{_9R|Q-sLla5DLE11RDxOawY~5z9#I zyO=c~Nya21w4P?cMcYX~62guE&Dtwltl{|v2M)~^?a@^$;eq;C`?o1ly}xc;bpdXe zQi^@NR}%NjYDN{16CPrs+^uR${jo*PSngU{tJ2kbmX?la9W`IwWP)Nb40*O=4Jx~U zh$M9Mu8G6&xf<;}!~sj7W8VqvQ5~q*Iqf(+CWg=dc7PEQW#3402>idhO?D>L6X^l;@Sp23f$}!;TBq;SGsgyF9JyP=}*$y8-PHY zFjS6Z9nSZ`HVVeG+qIndp+GL$uATkOf49sIcpG@0>IrN}tTVP+IGJJ=5``V~_#OVoW5hZhyAZmKbe+SXe!R50dRVi=uW z6sKz~6a1Otv8$0$?$gcYaEu-Uy%ZhO2>P0w;@5&bx3Q;3u*K7&!coT_W!%CdgkG5uO}b9Az{&d4f%;f@kM8W8mm2n`dQm7y6+oXhA0JqB%YoSHBkli&D8l zY+$DM)|MOWbXR^~1n55G^pqb`rZiw@p7a;*&4(cdW$2n)@wxIWbj?%U$A0rWTQP&T zN$j@ZBENkx&(IxKc`)=l?8U}cu;Ih3ZY)%zmKGz&KvOap1gPfC%PY0SEQ-VZ%d#lO zynV5H*$&urX@0+6n~boWzzCQ_pq9?-pqWf20g|2)ZF#m}WeuI~mNTAioOgL?z!7DrA$Krs@KM>Y~j(N_< zk6S1S+oCAIQC|eZAK!Q$;SC?!E9rbELdo!o?9ErP7ebc}e}a}8@W;#S(+|$3Z23Y3 z=sD|iPs^(3vM9y*l*RTv@Iz&UzYF6E>b35VM7^U{S&LvPuAY5|n#)b%_l$W4seD`+ zM-ks_n{`A`Hx-?th`U}?NT=!|548i^iuJb;H#vJcC~GlR?wzuLzftUt@Uy#MqrPFZ z%PNYw*UA9O<6`&dCZ5qnWbcJvHO@Tyv6?y#CNQwGStXO_5t7@xW13vl-YJ}VC3P+F ziXV8o9lQBR)I1mw5h{P0d^Tw@Jo&H_Uq=>c72FU8WWxCzyL#Qkz&yrP!?*_b*~T9- zZb`h{(+sw385UiMaEGB}smFQTlw=qR8R|xD-$WWOMXlA}OC<_!Ib#LIp)?90WVj#5 zU+2R?BC$yI>r8%RkTC(wCsT5qYe*9o3_{ToAA}wORpivN)?{N~$o1x=YD12_5W46}+7H{V)bht%Bd! zZVD6ik8gXv7@tLm7(?Z)d#Molr9WzMP8123eaQqhUFY;sj8{2#Yz5L|ek4T{fsu`e>j_XzM&Xzf_IQjUln z8gx-vEdx-e1*0vGSI;sTt3MeeN|WNgAQ_9A1MvMKq@CecTkaU&;GQB1UfQ_2UY>Hg zs3??lah7P8D4~(^5L6pP@m=bh>EPbwnfXE*e1#b&P>QGjh_Up@8G}l3vf*O(u;4R; zL&{_Q-P5bb8^C8&af1J)t+CeAnKUY3dw1=4!LG!DWaSt-!i8EiJ4 z_YEji;=Hc#nOOoD%6ra@u*WSr(}XYA>rZ^8$g?Na8b`p-NdqdDpY!X_6sQamnn&`i zH5~HCB3dj<{ zZLMcucD}8`li`dPWc9`Zps3F`8?Ms;Q@&tkU*WH6=>s>u^Q#A{HmFJKW$;VBqwPX| zt!vD+6YJPQVKw3Y`rOZg7NRctf}VIaXk2@Q zQ1M2Y-Ay>nNc*4wj%ksuB~=3mrjiO&GLW8lAOly87gL(U8|j*wCCwxAR7Jta9q2AX zU;*|-8ET4sQNK|O#a}y)z(4~k@bl^RdV(sR!m748V9yf-%+o~GGU+|ShFL>e6w=F;`W5_kBYjM!omhB+h*#Fp&FDa)CJ8^lJvo0AuGCyM)kQ(z1+MFod*keMig#FL} z@6kt?W4Hh)sGqrif415mr?+@j;I@VUUI}SM$|oXCc0n4*U2^q%9BoX#Boe4f9ft?8 z2jv_4?OgX?>js|gmHB)^C+NQMTkG9{FPWYob$~hN4JxNqs^7Mnd&26*4^9l#<-1mNm|8I^ml)U3_?0I?^jCBGhQ-C**97@(-FdZ*nF?@uu{W@%D;s4rBtf%O6?4Ju74n8?XA3CuI8`c7!M-CG; zbj)5e_9tO*MToc7ux6|FDU)rZBEf~D8Lp{(QfaYH24BjQ;urjIuDrtfDy%O=&>5E= zR)pe zi1PGznAJCd6G};y<~f_i>`oDppDYf-)Lzaw@Fij7xWYNXYOx*o5xi^t$V`<1SL*M& z$0!5qlmmV#;c-Re?bzP{V2*zhQb}u{;~H(f4--z~Qdx3k?ZsLtIM9bi5GXLu zwngCt32dd^L$v#5x&=@la$XLl(Pq^c!RS!orW`J^pCYOR*kxy63e3{_^fZhZfHFV- z1v%I!;j4w8dFM&U|C~8q{%M0%f-U^jyI#KLmEny zKXF8LOtKg7@g*>)Evyn+l`@%51t_f9?cfTzW(SgLCj1 z!Z!m;sr*8Iu3x;qM@g8SWKU(PT{o-kYOZKDYqQg~6|@jxGCm1dErVt}V&ol}$CKa5 z4e<~Hn=!~ymY6wYKQ4mLJI|^@__|s;pppO`ma@;tdhc~&_h30JDUzRSx>6xB+Z`|!bq4l~(mE#c zXO!u@hs9J>Ij~hjlOt?%>rMnBc)@M8loAFJ=P!7LpHuZO6&y_;DIH8<3+CPyP4&@I z6QV}NO#gY4vQmTZW$G20|96|i-=+4q(!tNG>~K2hc0Wcfx<=^C>IBHc&R9m*$^Q+c z*?7+8q|3%*%-*pDWgb+#iN3VwC@j7D<3XKTOq1i8-~*RDAGHL;v>g2#?0(yGuIpmV z<*+j$UT-}g!{w`2P88Jm$oDJp76vbz70QD7SadgkAr@BGkm$ zI67NtDg;0Cq*pDEe%1Wvs}QC0@oH@Mb;~lhRa$nxvi^iZ{gG$z@Bx3kCyn9q`fk3y zyC3~*=-`$znMn0I-rA=&5lfwRKdI#~bXHKh#d6oW0=j8)CNWUQT;?B_>tJ!@-`s}f ztTV@+T+4^SA!K&psL^%hIDuHl(U25AJBRB8)LQCYNx$?gQFWwD?8Xi0uj~2=M5N}) z7vno3x^o&Oly)epS{jseF#+bg9h!#bxuvN@?GfyuvXo9yLa2;C?l>yWex6h5Zm11l zXTFz+cYorR%c@}A#x0&JI>A~ugg)a3`|nj8;odk^OrYGir0F`r0BkN$`ys|5`zAL} zbP$%^xxeA+=&6DXK*;CZehEi!XfJQ2CMcMz%v#0XqzPeQQrA>*44~@rX z;-;I@J96j?xYG1*uZ2=JF6|(QX5(Z3wFpM zk?>oW+@KN z`fpaaWeueLtY?P;?f!sGA3)S+54BueVb^upBHA|W_AeXc*UKbmkV{*-=47;UXQ-cG zV6t_pjNkaTEL*=12$wCTc!5SLAayF`Edfu&%lRzH)gIS2nnk}GxMEFanS;EFnlZhJ z$!>^0ws~qYl;Y@bo~hZsRi~#80V;gvO1XEW?da%CnDNJ0enD_BgJ6*=CG#9nw>kwP zX;Zh|E*S*(=hksBE8RaORpTDG(X5^ywaq>vu>vxj+^Wer;?ZxZ-NRp%;B{yVJrYj0KY>w9V=tnXT!)15u%7i$B zIcD=k1)AV{{fPtd(j;2e^{@P&Sr))^<2^pS@5-%djg5U8l9}V62+u)48kFLe5uEuo z%UI5#?7MZAI*&>OpdAXIBIxDtB8S0Kjgtc+vDZ_J zjU568_439~<^B}VoGjF_M}2g}3=%^3yqOxm*t_;<4d4j+$vW$)^1%OEA$Y!ZzhK>` z+YM8gTtoGYiz=k@br%V0f4sx8^SvaW!RF9byIA4I{S=$6W?H(mAUbUMfew>TWyF~_ zE_jjdgxy+s`Tx#K*6!^|uc!#BlXKb@fQ81Js1dBEli|RVHt|r_GWIY_6 zJYTeA)-4g#HG}>_gHq*GwFzpU|M|5d5Sl=leTl+v5n895=*%lSPNAHnx^~g|$5X;i z#-aux0~$(a(H{K~F_W+4U0(8-V(ZuqYW8O9blD4I67KSpeLN1-o>^a2zo88X<+Wkf z&?Q6naHZ6kZn33k9jq_~V!vn0q3E?|vtYJ^1J#dER}(Kf&ZzmE?i*_N8eUa0nT9$` zJLD66xtnTGih6qd1%tl!?0lE-18sd=+Ee-#;hoh_#bv@}J62iye_QA5;|aY5PK!R)v<$Kw`H$GyEA_wGjA`=@aq-p76XFK+4ZGj0F=Pa9TQhWf_$!u(l2 zv3v-F^;|3yA1v!HxIW{nc${I|@5{LbVqu$J3SHxW#IW$m&5&ngK28efmOSHviMA8sgzYeC^$$GeiJDR z#nW7X=XA-v$)6S$6QZ?o)!|Sei1Q%lN!z}isG05V_wOlrsMsPJVCC(i7bjfdU=ZB4 z+4?{NGbTrsLeB$YH@MHeo9>GmY$x)n|a7N{2!Md;* z0c@jE8UJG*IMOg!5f9QRfF%`ttOh<;YkZamy5-b7Y8Rekb!%|=$oQ9b5$cIHev9XA ze8s$)20l7Yh##wPZF|X>sSrt6C^ajH2;xmG4-m?Wq&%Eq$;@8@eMKohUhgb_ z)V_^X5UGV5UQ550oX25Z5bwP~Pu{(YmseIdj*>@b7??Bs&WF=|7nedpB= z(NSQKNN_7j7-WSBqzG~hg`?Ego$dS=c)^WBLU+-6e)QG6Yu15y!3KfphJo463GUm) zje|P{hv$ZRm91y}V4-dN?%?+2@kt);lS>uAzk%|+Gzm%Mf$+Pt6>S2%+SaR1UTaZU zbXU+nEy50szf(!V`4~ZuC2D4RgZly3(c4@W32+0*_dyHKi26att9DM&MZhcni7*Cj z$D?}izm8g8DhGmJsM%<-nhrqdz+}u;$FRfqN~TIq+zMe{=n@N@j=SizI5C*jg>uN5 z@Ziu=TExk!KTRm-MpZ}5+=pLAt8_SfpVf3xH1y#XArqq>p@ICMEqemGKx#}^o+xji z2Dka7)6L3$2b4#fU^!*wt!Hg0{hb_$RfTl%OsX3g%_=HtgvNWER`s5Yoo{|Kxqi!A zp0=vx4j4>6?sx&SoX)%42?STj)Q>I6I-2Gmot_iO!8T6k&rmEKYrqeBCpp&W=<$>i zCp^(#mBrmT;=$PimeT?7bL9tjn5By~RORQjWQj0$YCc91Y^QZjVmWn=F=e4*#KWi_&gZQGXXtqD<2NpLYld&)n;p!B zZ;q}nc-QWDrWV;gHZbT3f_pfQSM<#>958ev(|;xEKwQ0d#aDbLH;@Bvx23^!6TwgQ@2ZK2l?J4$5Ed zW%8i3Ue!7eLSrekl*X*w#d^>Mr8eUZPzO71O>i&V&+Hr-ebZCb5vCWsvNVdwF~Nd8 zg5e_ggKYDM9-qFNeUSe-r21~PsG=6u;MG|%FY4>{<16nT#y3!QH}5w+>!<;W2&kBW(x4xEgn+6P%8X70=`%Kk zgTQJTznBM$K?qDKY6FTx1Ys>sc^Oo~e5s)K;H|<1^xLLW<*Psjr*wIBo3@+u%#fOB zHT~1R7?dqakoK{B@g2$Y2l^z-;Aq5wAR7y_4eH*kH8PmsI%dLs3ASW)X6;~l%%MD* z?Y8KDS-K}mzE*7wN;?-b!dr#jc9gpOZTG5>yxHgmY_RO$PoxlpBDL*OmI%WFbpxk# zv0@!G1hXQdD+n8U6olJly<7N1o)D1&_0!2r;99NCrrz{7pSOq9Qg__$O8sJW^O(op z&+p!RS)q{l6l>e720cMgMES-iKWiN$O_ctE{+PFGaV-)hvoy2&a}nxT5K}T(WLSSY2F6vxwL3|E4?{ z?7w7XlB9-t&5Ndg4xhqGLq)7mr4JPE-Xy6U$X56u~^4qxA?-udo{%hpq5oE2^49wi4#{9 z%|T_TrWkO075R&1zwm&Ormi2Ma-q0b13|b7PH$!Y|jYX?NSPE3{>wi?)$wxPF;0S35khNd|>sTphb1Y9F;^dGJZ zC4ZK0DoG8hkr8-wC$$vyvlLE2DGsuk!!mwMSEs$=;O#dlim(JjbL7o***+- z)q;a2oiIIuw+KE0zRCkmbLoP9@}r z+M~#9s-06Xai59L3-V$>9lsZd1d<97Pj*&D2%RgX@Q431)CxWde?4> z)SX^J*r9WC^pmSP8WlB5$qN>HHD7iyilS=XTP8Ht7FyO~{F*TY2g^GhhdrijtP;Pr zVbiV<;S5fZzpnTUtSA0G9eOu2*knzlPl?6(w+`1Vy7j_KHoR54TL2B)an3RWrsos< zmzw=`n*r!k>@{Ln>AAxeSJZOUCLV3%jnlz*|JX4yq7BZmVg969dd)i)r{4RRKt>X| z4DPn97=E_gT<@kUaKM1Q#q@=-B=TB7*QRIHvSE?NH>h8oHlZXSuluhW3g|~5aHesB zRWaI|3S}exi@H!A*3Qq8lqZsdXI4SEBbL~BA37>Qo9GmmwDIZycMy_$^U!q`nqPY^ z<|Dfnbq6w~JMa?kqaC-Fqo-(*0!*1?LMxA4}pLK^c1Q%#6xD831ISaIUT zvSui9ZjDhYgNv96g4{mel=W8_EePUEu)?_%{(w}E_!>ZsAlE`(Hpaprqn6VpHw$w< z6C}DQV`?>WPsbOhQ{_+TO$gjZb+DR0JFuwrS}P3sVSb&Z;KIUW)M(qT&**aWmlO&>l7 z^^<5=)=Cs+`|;yn9Zz9urBC+aBba=Xs9i*#Nz7LT#e@XlYL+E0%{>3{{l=&w7_F1( zTJTWnyqr3gjIP}eN7v`?D6`kNwJPVOONr227`|lR6Auvn{%a%cs^_INe1R4tftlaSvg*amrwc;-HbvuOtR>tO3g)cp=1 zuw@odKed1bTlQ2cmv0`vCIp=joh`OwxLRY~&jGuN6q*AVY`*NEQOb@tS=$>OpU!AG#!{yr8k_E&2KM{)a$Mo9mM* zfTi+&YD8QVV)9p~DyECd1{Bm2f|`K+z&O9Y3Rp9EE)ZK15Nl1mqk$VHKYQ=K%8N*-p^cqceX1zd z>xJ3)^)Yw^X~)GtAIh6NXe<-y=XOzGe(-0Q&$FS+K`poAYRiDlRQpy}+o>9Y*%aSf zr#^BRd(FA-mrs%~n4@LsL>@-5BTJ&yE-%_u|FlZo?%m3pfS38W96@QT`OiFAOg1l> z(xF0$bO?H%3_SL&FV>4`&)IJpRGG#8ez{D5gBn82v+j9@$S%hprd?Xp#tMR?=5i~~ z2xfmcIb5(-PPWH(vjdJlb{K=wuA8HT3~JHk)LXmXv^PJjxmL&T`KwBarE`^+lkvg7Xoz_-wzpigF@WXLnUPk z$DUitG=DHSUOs1u{^Y#P{{=q_(3NANj?vK?e)6aRO~JwA0-$XZELVa&KtL5qP?ZF= zBNDX}1oc*ldN)C1K%y~3K#xh#Hwl{eC7RC&n0X2213~M%MC%U$i)h4Rh&cU5oEcHu zs!`j4sN=fasN+r44QSL2BkFBz)QcnPCpYS65Dju04Gs|TMUD7MqT!K7!xKcK)<&al zqVYhZ@et8utkLAAs@}KJ^cm4?zR~Oh(foU(`5z(y(WIJM5cQjgW+akT6Ul)@c5Nbi zlPm(7EW${Z8=EZSNLI;BRvDxfxlJn$kSIk>luDBIktXXCB%9VIn{JZrK$Gnd$!@I4 z?k36pev|z(lEZwH!w1sJ?@cTJkf?}eDu(Q+-|T2ccCu=Aav(dqHamNhT>_e2!pN&O zHm`~!yCyfgW{}--o81nOX+_PnO0xTrX7>|hkJe_7ZnEb27iQtVvBf{mA|SaX zAj2Xsw9$xq(6V;OA{1fjfF2FKX<5OALx%H+io9QyI0{IKDJ+zY;XElw+%jJZ1SYtZD`^j7X5#N9Ml07$U#j7a)AARAP2qwpWxRBO5gc^f?v(!RRmur zJJSCFav(ivR^FCloc<^6_ocb<#Q%}@dv&;#*&K2_KQ#b1@1HY*UnKUA;3K=v=|OU);yDbDg?NN7`EE4=0uytQ z7+pWCMcrwC0Xn7cAoGh6|7`eAp2U?W&69pPP zR>VYQF5R)v*rRQ8B5{AH+{!}TPg`i3t2+xb<}^hrjL~M!p_RDWZpE}dguOBp_IQT+oqx;I8iB%9=W?X*96MEx$*I8QwCV~JJdMilI#^G6OK znn2c#_W%`JQ=%(#UAg~dzLk91Le*GgH#&R-jd-IkNB!i#Z!BHTWuwwY5Svc-B@izSZzrAz1g2a&Yg zRQNq`R_NL_9=b`F1Cff5_WLUiCg=c%Kg$Xs`7A*sQ&`_B0lg@*o^Y10iPw^B)x*Z( zm`OgmGQu9tw$?6~(KojumPB7bY3@<@8gFB_-0vhL7-5w>6QB|07F(@v0zr|0d*}mFAB+I- zpygR(`7#y6h=uZ$784phKaZJJ+Js~ z)W*VLXT#Sh=XWd+MRPJ5T||AqjT~b1^0!=eNAe0-%UlHNWc4#Og#Ak*N}Vdxl!y?) zgeF-A$5(LS5DR&p0pW3YkN^PbFO>tuVn6*ZC4ypM)2MNS0dX}A@fwr>d)P4Ou-78Y zgb;3WL;rCv6FW2cosfRL1$vuV-1~}wnw&cg-OoYB-;9q8e%N5ECqa?Ph8F^_j+iNA zfnT|DE#aeX!|wFgKLrNrWwcRD=qlc6&6FzJc z5dsfn45HUj?|=|_-tPx$J9bzY<6UewhIxZnO6scsuc0^Ji)nsWswWIAD+w^skG4$ z+MWkAVV!qLgmCc69YC{GLEDa;_G$S^53<$H&vh(O5tUbH?M?)AV`H&Lt{XwvZ7{Q> zbsQ6omIJ9`SaW5Tc?h4WLaUQR)QO4N!Ln0G2Lg{?kj|BP(b+1(>vj zr2@!24gyO6d^pJBjo2Pn>_%!-8yNXwI51y=OeCcHI6@00fQAd!g^fBOvqmW4^*OLq zf;&sL0kH#FiACvVK~h*=FevJ5$2xKgy&e7p}?t}^~IWI!Cj zy+DSHVMCrW06!|ihX6!MkzR?270vKXFOwo=J2I9wwFxX>WEH|taLfsMDJl<3vafQ3 zo6cpXva`((BVv|J)m9;P!Xk5J1%B~nZZgD2F4k+p`cy=!3kjGDx4*%2ne-ut&Dnsn zrsQc`#od~c1A8NXTG-VklWFuy_@@f5Ap2*I6^KT==()siZCHb)l{>R_ z{S<2rGXV#{vU5V`Q;IgohWHUI7NYga5QuzHXb2aUnuvo-Lw79&?}kKt=aV-(7N5(C zNN6mHejwU83yD^`wASc(O5h>v=!ZQxq~Rf#m_x4lhq{(uVs9@bexPWtcCY%eVEU3+ zp+9SL`m0S85n!Cxb)h2OPhj^XmcysbF5$JwN^>L;Ocue=5Lhyu38%v22sK)n)yc|g z#4IF{g*b!IqfrsN-1XMySfi)u*Hf_W93+dS?IPkKxrKY{v7Q`cWfBnMf4T! z%o8jC0+Td&vJjAs)nHQ{RT}y|-*Qb;0dHg#Rz^=|A5JO8o`^Bq^eR}DzG)L8{8=Ra zwVg$&7LUV$&DRjAN_f|sV|x^cT}p&T2sV*U&y^icjug2s9nQ`=;?45Fdoa)vkQXU| zHXdQJVSj>YFohYZBL&6)V#rOAOlMHFu-k#1D_l|}U{bwyKVnN$3o=LH>%WMwqU_r! zqI5qn^<*L~qE<7S_su_Wox6>AV40SmipU|5U-&uSzP&4tj!dN^e~wV>FOYLYR)|^a zzw2!`Z9n!@zqLU1{Ofufue{^<^2f$HlXKy}F}wdp#OyNo|a;YDA}euQj6Pyu@pB9F5}^r$g#cw@RE4$cH~ z6$sMF_T`8~nH84FLHesgIWdXpED~yICFvnOm)UK$1DP!~x7&dT%<@Fl5qme0j^*p86i=G^pn({zFW5g>hahqp~*dTXLEPi zc_}}ktLcKR{jY=i8r1Xf9P0wWtjxug7viJH0;@aD=xx~_1@4Rv=(Os45dmb%vV&lq ztR!;yW}&CHz^DOfL5#X?gLPnnVMnz+n)=pER)t8Au`u}HznxR9r|Xm8W9_VLYRLS9 zj_oc$3_DGo*?&5Bi@ONf`S$GHRQo)3J+{D>#=bmL2E+a|+f;V(nP(5UtUMPm3}g~g zKnPe*X zf73aM!Xg0}XQ3WYbOc!LvU^hxxnGvKbai#?;UL}xXl6di%GbMB zyQfobMn_h)CNyu{iIS2?`wJg?U?l^+yNtxnv^AIW+hUf7F9l6C1*eT@;2J%Co7tlf zXz6(T0BJ7GQ@$_4M>K?r2l8SAMvWNPZGbXc`vEwt>SDv!>Z>paVw;bphZshd0Spmx zpY_-=A7Hx|c1JqU^BN*HNj7*gc8LZ+Rv~(k;L~>PJ+X~>=|It=>t;;wVZk0yG;uv5 z;Jn?XdTV|Gb&dPJXecL(EUilM#HKwSxV>+r0D1PH^-)Ld-d{-@KWmRx7eF{=M_^?; zY*U?^y9WMAfM2Cx+uwy0P~n?<_f>0LhZ3)AppkPOJ@H*O@ZZd^s6WR(dtEDwC3$aIv%Bw_ZvXzBZ6!Ke3S1N|@SY*u*7n`n9bM;4 zk38}~RbSbBNp?@?q=)j{vv$~G6Uo%?l{pgRuf?L+NyOi$*NE)w)0ZwXPfxUJ zAH6;U&7g-h)vo!MdvBrfXgX(&_}3~A{C(Iu-m{h~?Uzg&OOW7Mw{+9%TQ2o@d3wjN zkQZYg4DEjql0P=t1phvQc)An$1vZnhY`1PPDK1}uC{LI%o~52du1;hX<*U26fs$CU z6_if9073*wnU!Y^t}4IGU(28PBSd-r`b;Hc=$2+ zFb%tWj|^hLWI_`=AIqTApb*HiWfK^u`}PQSzBxszz3qE!!|5h(HO8weKPz!P8JdPw z4}Mg7r9AucI7q)IgLMMC*6wdi+fVHrfB7)-&Dokw+Sz2JT(MB9w@15yd%ZQ2WzBhU^WrXW;T*x>w@nhyMg!{)3hN zYTEb3wtvF{{Zr|$$JhbA_ZpeN-Hi7I^tbOEE{|!ZRpL} zblyzEqC}70dXG!@%}gD|4qQFV3j27k8+g!7*`a;*abDM-INd9M?y)3bYnn7f6j&3n zgj{*jj{}ckf=`w+u$wm{@pl_sPXi2A&Wi5E;|ZS~MCJo=STYlg;lRBmuAT%Cu2kXD z7lO!pwn$+H_1+=tl%WeCFS8`FHoyH+?iZ_^~w$ptIl( zhHsI$w%WDWgDJE+C@h3v{qR+c&K2#=P`Ks%x~V9cbJCi}JosDr=iuvs0O*k~LmW7W zm2LppCoE6GuWWleA+W(-QHSTqItr-Bl=H9yJI~of2b_NmXPNL2|5-EANoRgsJ?GTm zB(eva_FE#Jj;AkOc16~FM9^-VGGzZs4!ywd{S9fhiU$=ZEeu4=jzwCW}u8oz-t2w75;_>RJ>J#v#MG||{i zT?1;XlHn_3$&K^ICBZvhWsT}#CF%A^JXFhsVj4Xx1GUjKFH3d#^ zpsAlZjkw!krucx0${s%Xh6xI>QW5M+z7+;de^*WwGD|kgxNt4~^s$b0C7JViOx9Rf zB?P|pZm_RxXfcEO;%{?qK=Tto5`O6m%MKMe$rO1mS-XgE%b6J4mb4Bqe@4{=7Gl13f}aK zS6qKoQ0pJARl zs~Izf`m*G!WqMIAf!uCuh9kfW4203G6lTU9i4%++}n`-4IP@ z+P8?_IcOR6cEE}qQ-vQ8uwO4GJAZy%nr+2R?z3TdW(b$70SGGZI!t|U4az-W`vk@> z)OpopPe0nJ2d?!_F@lK*zUq4oziOm$^m4`p3C1nw5(gN6Y3ozfDU9kKe-_bIe$A`Y zX(9MS7M)Lue8|ig(dggbwf6QD{%n-zk^+Wa8Z`!xZl9&(O2;9-j@CVN%@G7*Q9Kak z^?9jr{W2g3tFqMeAglqw#56zqr5_F}Rw9Hs(fxnY!XgEZufDdf_uWHA35kedBqHLt zR@H{BHMb-xHE$#Jj)Tg6{Xm!Aj2X&#^~N6LOpJs!VK|kx(+&56B_?3%xzvYd4jRk!mOg4I*wi< zY?J0nea9NabG?JvBOc-IA8f1$oq3myB%N>6o48icdl76uXxNcPjLYDiEnJN`I(daH zV$&LFp@ud+5q>G}y_eBk+Z1fl2bSnO`xu)KpjseWsbmg1Ah7RFG)2p_ry82O`3of9g`mmUZ(#>e+r2f zW$U4|SVAE*Kouu70SZ$1rKV4mQ0w5gD?y2TETi|b0u|5ovqqsuUkv@%121{|qUn6t zYirMmQ}B$oC%RH9(hG|`n2M_4ZQQ_dT###Sl==itVR7#SwbRuWvo`w{~CQF>~jyQIUqO|WW%1bxv&IcR!V#0{dxkxMvJ=}C@$ zITzEC#n&##(_yI02i(2Cr}b@Jl&UbTR47`8y{w_T2LU>!#%F`>!(gY9s*8 zE%EAF-!k%VT=YJEG=9VD?Ypxz2v5I{Z5rYd>3-#&+kPjMcK&>|``~~7ZUd0fGME*c ztGS;AuJ~c4GIa1wr(#WL$VdYwbhqPG!Vp!0@<3&@21xrJXosjpr%^Tc%UD=BU2VNi zdFi=-m2%}c=8rBKQ(NCY4KxZ1Ld6(#jJb@~g89diY8YrScr$7}6|r1kQ;tjosN(q3 zf}Tvz%y-?US~tgG5!bhWr6%FGKs4`%M%Sz!OKVLwzs!l*<0%x z0oMJ7z|lEg;kCtFSDqX>0ma?l$uW(NM>E+Z%~~qd2>WYvfeW=>O_xBPu++=4Q_I-_OWQJsW|FRB86v3_T~7ke8jdFi!@r5h1(C}(M*E#dKU9d;U(+8F;T;g zJr!_!iX3K&2>m<82egac><-WBU__yl+R`sP`1g-!xuliZQQUtP4)rP{@5LTO_r2J2DY-X+0|v|S~+5UzZI7Gs-Aj3*lUv> zh5EgYk@jBMrn5h28oPx2#9i;;I$VFwovAX@))L=0gkzcxgSVI-9|-WEaE)yj2Me?c zKG7x!PH-B{tRMqvsnxjwg%7#*35(QtV7zHV66WS-0BMq5e66|>;-96UP?nBGF@Ky5 zAC0KdR}eIfW{u4HMfYSHCC~MCF);g`kamOg^#wuOxSu%)qjCU+yv&9B#u?ix=l52; z?CDs)y~wy9uq;Vvl%7`b^uZIFZR75~E0=4FOv(w!gy4(e`_l_Gth2TSzux;T>vFr- zl>DI!b!vy-i~25o?x`Cj-u0CvI$I7Pg6>oTM)e32m8`Oh`D)iXXDCDkUfTYJVfu)f zcT?FB-dYd+u%o6($a1nO<$EX~4N7CzperVnQv?9lLxDaW$ofsk!DBEI&k ziISHo&XBw-KiPG!+xoGQ8p|*)^nBJ?s1OZPXpFI(3?(Rs@C(#Z?X~SCJPq+9o%rOKNox7({Zyv6_pL z^BS(pUlM#nv&#oz1Mv;UMw~$xpJBy<*{K&oceb6$B%f>yAQbA?eEb&g@CkU%hsdkm zO}f_gd>WPWA_!*KHWo5f^}u7~GM*EV03%Y|)--sYzynCn6owNvi4@3lYF* zgFr!KSxBkGo#JQynfI_w-#4U8L+%mSCG!4^zjzzwMbnGCrn+tE9b>J5Ae0M}XPvDI zt*A}Pk1W%gj_jP~KWnQrae*vOmdwvuB{qXMyZLJ~pqCZxP^p>~GF zcb_oO5}vH31+5045M{&7FGTZ9+Y}c-&2J(iSeP7h?w`a)>o&e12u8sZZ6P zXkPU3@M^MCUr%`Wq#(~#-Zst~EcdxMY@V4U(JNdK=i9>G4{W8OZqlDa%h2cG;S5?`UVK4aaXW#C$Tza=|^N0{>cb z7KK-je$_nY2f(lc!u@MU6poEA@#ZCGn=hSkS%QqeTPyJ$afssWog2LyQD(8$cA3^@ zVu|9J(gF`;!|Ws_iM=&p`$Na?a!WvTf30wz3s7^uIjszHzGdyj79tn&H#{4{_F4@k zbk6-C9;QthU!0iVr*e~X2SuTA46CZ?BW+7}cQbDD^F(9TIs9(p7tmPx$a9iTQ=GQkJIeoWA{kBCZK!;l%Fnh*<(BZ zgLIa%?iH)!SZtIF2f7kvv+CVYqn+;Rim?kZ8=YrdjuDN{zR(yd;afAI11x@2T8oGH z-qkdP&ozu(0GpTGy@Ce}B`}YP2EG$aMNT+oZE%O+xV(Z3L(QDgV8fVM8WE{5!V=W% zWqB#*sE71^zX0YQ2_#zUDI&|HqDw*cr~2@)gTGEx@di8=F4bEe_5k>pk`PQb*BuXg zs?hlxzWT$$33sVGf6!3Og-o?+W_cZPeQ|D5Jno2(v2JYN&)DqA;ge8a_u8&3mQ3d^ z>g?AA7H##E&DUsBE7FphAd9Qy(L88DFU76j_F0?GY#tLueZzBp`SMR;G3P9go?aCt z8=RgS6aIB2O`T4#H3g{Ia+V*H`M{mUB0W31?6ki8Xj|<&>p7M?3m=19*d>Q zm2Y{H=fJTK(7AD=1g*?Q8{@cT88g968uE$&UA=@-u9B99|7~NHP-`TydzN5ehcf;u z`tVpt{T@G0dI5KSf#yO?RV4^|Rt_aZTVIax4A-Oj*#|P&zhm>UR2YoO`m7B+abC?N zawacAw%Gv&6I>-(1pCf05pf5(9AgDUYoe36b^CdDWkJo>4MWeh&L{E-ui~t*b`B^6 z;nv&|U@`&OW`(xj=)nK+qEp-3gmRAlp_8C?49ZM0S$OW?^pX{jgr`#T+4;Q>sW)2v`ZYx-tYAFKS;F^tEqQX&1tTEnp$Ow9f4Cv>a&!{z8M1f8aQ2~ivk zwcD`V9ai#DtTRV><|tih+*z(&@2PKe(!JdLwhZTrGD5IR$p2n{kXr7QBDrgE;>wAH z9}6cP1bTfPm4rdy&Qj{aAM>Xdh%&`opUm*b33sA&#eC>>&1$VF{lu1~)TGHOb+cpB zDSQ_^8@ji|E)#YYG6EG_*VZ=#>R0W!xizb6Zh7Na9qxhTO*x6ed2&EKxklFna`$<+ zwN62OKxS~39q>DoFJ!AZ>T>KPq^$C56|#Gpo7mMsp09|gVkxhEGg}LiEENiW@#;34 zRgkC^;$G~jh$Sc|aR!UFZ!L$KFS$?f`>mr^d{m!Pm1VL3S~U9k;@qu(&lI&C%NT2* zCJW}e8b9 zx3m=3(jCt*U`ct6*T}^*n5*)ef3fDum9<@a%#G?!Kf2a=A}Cwn1WVrW-Eo3;^5zxS z)kfc4OB1${p#;P0AF{)e8CmVVgW+XuD07QKiMfZn3+wwzbmJ>)q1^;**Q>GMdC&eJF>wSGuQ!1LT)>&{H|@)&~ybYW_Wv z$5`fO&7!8p-}5f&39ME=`_?J9@I{DZYVLN~slHs4HUO`Nj7Mu{tf@6KgsqJU&P1An z;jioJPk0d)kokK0JNB+9{gk)z`!q$pU%TAY-2O9;S6$!%iXcup9O~<#VZm{zLVawh zOrIdT(rHNoG@l!lqxKfO4r?${>wdkI^;@C5Cyo9F*?EGR2shj}Y{YlWD{XX7_)Mh# z*T15o&i02#j;-8^e(YywK^ed7YUXn{jlZD~yt>-ONd1-7xy5aXPmNK%j!d6Ui&LA_ z_nXpuV3iTrg6&pxUGW}s%_!~t9@IR$O@JwC%g?3XzO!SGrm)LDJoYsczHI4vJ3&2Z z`6=sKSTp6}3=iU4^>_Vow?reEAw|4>gsoNg9PK)iT}9v}8(Y5?L0sFWN_d11*a2Vd zxr{4)D}Cde@&6beZZdj3Y9fK~fB8(U{B@&hzq$+B84#Mdwcagp_JQ~a;J5F_OMvnd zj!rDycG!6QutvsGp1g-#VP=c^OPSfn52&W?SyZW*@G6zDXG!A7l!cl(LmThb-|=B+ zxF)Mx_=#av^#Yi7!8Kn$xsO*OTPaN_)&${xUqiz=K&3ILo^iJa21klq^1D!Uj@1o$ zQ`YO-d+JXKBLv_RX%t1OhVNFx+i<>Crrs(##sgc31-&WZ zpr#6EH-ag0l)}*d=jj4%M!!H6E*_;kKzR>}M35EMhheG~i>nW+a!e&7n*RC9W8HfV<7u;!h+t~c9GirO zrU%z*cnf|eU;>WBsKOjS&-xz9IxJMBS=f1s2>82pKTLk<8y)~+AGSi#pnQkVfqCiQ zMePSBBpuba^|r@l%O5Rv7R-ut+B$~xPXggh7w!TwCz^_H_-b)yPrv?liyp-7$<8>` zss@{`GiqB5dM@7Yusip(;NR2d+*yvrrZmnzhYN2yE;?L%f8pQmi&gSQ@Y1(O`&JJ9 zT)g-(7TwR0_5W7)qRQdM`>DffwwI_Qnx4O@qqwzRj#pJPWydj0^U+To#s~3s|MFIN$X+ge5DHhgsfO(^1!f5nDc=x#8A~#?n>xLV0=BX1CY3yupFE!I4 zdY!Fq?2T3Q%!-|~!hGl;3;9Sn&qA6-;!A8G$qV7dkkz(!xqfE1a&jMCSI3=wX*6Vd zu47MMgWC@C@272)j+&KYPxqLrl%lvtJ;;~G$#w&#d-B?_#<%stWc?N^{pk?dE&ofU zIeNS8Az8kMy-we_^Xq5N)$5iQ4vhPrA->sXut{=YqWS0rXs2_=<&MDr#ol=ZH5EQ? zIw1ihrz4Sbs2=Kr zIE;j!$-~PbzID+Y9c3a{Dsbdo%p!1FI!FG>8*7Bb(9TN7t0b2TT3)q<5VDl=`94yb z##hBUWv$&XdVfDz0OI6u;BeYOfYy;q#pUSq99lU%loWZJ32C`;kRF=j;r+b}Yt)mqG37L*X($9R^J; z_WU!&DIE+@nnqh&t`Sxuoy%D8*t~eTE4-)$cLqXM0AESCn1=Slhy;4_5iHky8wZzD zYzm|wz{i$Np>}9M5jtTWcl{;?*o7?Tmd<3{@zlP_hu28tz(=mlGMcZJ(TZepuNUbA^`f#?Vs*Xd~R`Ub2Ybg4G$!LZpms!LS zaw=-(y8vr-0e6#*@`pWiDYY^u;#2VW$hoe@b<&mASzVr|i>-qmkX6SOXSmdHov8Gj z&zt-5mxY;_bA_lwB1*N^k{sns8Om{wYi)MO^JTEkaCl-@N5s+qago0NM8b1)p=xyl za6_?4n+DS`Ae;)oCW>J=4%;~HB@zI>7?7t#A^e)yMyeYMLbkO*5zo?8q^A58{g#aq zly80g4i^A1<-wIILLR7lwn3LDO)fc2;df=c^N(bnS+-$@fDCpK#IoDcn5tCj6?b+h z2T?i~wCvdk z%%m`qvsnO?F=kRhL@mo2=#9^P-zgXaN<>iwo9MM-xLaiMA5@lBun_%n#||4 z{y8~gWK)*X{Q(@m${cS1O>Ac5nC=%%%9A0l;!1wnxAj_FtMUth6cA?yGIHN|z9s}+ zSwS65VeQwiGah3~dIqp_(g;S;>m3TI$KjS8foQ%98GCY`T3~m`dhHiAMGP9lJ~PqE zDUPf$D*LWHPHiuz)4QNDqumIraOZ=TJ?tz=$POrh9%{HRXW3}A=8}O$VyJlWQW(mz zborHEg1kn1a$25c@$!g-uGgwY7|_?L)3f`z;bp87p@7)c5&57GqxBI{$YvlINS&ef z8-SFiR3+*HXv?%Gy^4ZI+Fa=4`a^c0nQ_YC%}+Gj4}CNSqeXHCQe@p#ZlxCQ2wtTw z$4-FR{HZ?T{9wKxF$9J*M*z?1PW9_eSD@5v91)x(L1Tn7+q{bCUIk?Mu-_c$%c|)q z!>Tj~@H$DzdQ#3|8Gw8RZEm1J!RI^uPIDYOO=F8mY;(~v;6W*MQ^3conSl&m zWt_Ovd(H_vW(1jDR=;FjL1fhG8j7mJpt$RhKv6 z9UICi9}-fq3H$mb6U;1y1b7!F@emmohGBK)Og+OR(s^M!lR$SY^ArTp#RHP&>pXR2 zLFC`Lv@~%yGvg&c-Zd6}oAfoC`gu%6T>4h86Clh(cdZLWPkWon%k^b`1`Ek@}Z zU7f+3{<6w;48KJ1_9z1IP?Eow7QI*>xul@sX?4xZ&@OPWPPIYHCO*IfzK`#L%ed z>f8!py5_Y7P&V_%yVGZX?Dqt6d+sy_3JgXH+`?5gWV_duRjG^7sO+M-)N`{?tH$1Z zva%S7Z$>9X1{Ln4sWbA^vhA03ZjCs%4$ldXNpjVZ#N&dTPPnn{e4_bl-5aa!XPikE zrBxg|lShFbaa}AF|0Q?*lq7CX*lEL%4DZ3w`8q|(O3;M)AM>5O}z;!CGKyK}FE*z`?3_=^1gjA6|V zIoN=M)AgG#bD98Y(Jfg(3kgT`T|xO%Q?zV|Lurw4q(-tNKcRS^&&2o7a1ol|UX z1dDk(j6$`}cVdyZ)T%ZiMLir9UqC<@>Av$U;m#+Jr&ngC099mTTu*rxyJIp5gfs*| zrztaXa?Vs9ag@DyE5ws4T`z%GhbGX{_aZ*(k_eDqWbdzwXGR4iW9{6%IjbN*JRn%B z%4r=2UClQ2i!n8WN2=;$Y$)&72y7%1`o=!nA~CNlK6}@3xUOTe6C*a?09v1irHkC?u@S3@YV)` z186`()LqV{?a;yT)c=xK)CJ-Cl0I75qK?OA)r_|G!xXd=m;^I#;5widf(Ib*eke@c zH;iaOgGpH&R>#m}RpizlhC7DRGe?3un}-fLCUCGAK(%{Qgoo@%*L#25*Y|CRRO6#V z10S}c?5Lug`jx8dU+x}`1OMvKtYd0oY`meqMc`#l{F|XkmisHF)g~Fm$e00d-G2f^ z$Oh^Veq%8lupVNVhQcx@qdE7`$vdX=>(pghEO)CMrZEJ>teAOQ>9nJvL6$X-;UNNo$ zEriv$krO{~?!Q>LJqaC>_HWFX^0dgc;T{KeNud$sIVM=vI^CPuqI#CT(~{&pewJK| zlJBF?zF6~tPlVzDAh#)&UK%i&tgnUh`!ZJpj_CpjdAZ}dCU+gvp_MHz%=mkZf zRZPo|etc*dHH2RJmPKv0&3hmM#j!|fIsE10Sl)2C_)LEM*XLj8Lh^O{fZ0F4-Q2vg zh*0+w=#^AoyXYY3Jl7HGzIt}R?*ueIwTLl3afRJce+*kT&CVW3YR&9!wy87mX~zQ2 z-uA$1{_IOOoHNE0R8^HzC179*5Gz+U%min zlBScoCuQ-v81~(y?psDIvxtX*WuR7i2UQEqtqsc)0dQ>!{w*{^=PS8>5zC@Ogh26- zy7B#kAHWr8l0q8I#9_x}1~bGSIy>I?ZJ!(pc(m0@c1pp(hKUG?)myvb8@<7d)zPl+z46Y!!v}fsV)Adc#sS)A?M=A(`YkT{;Wt z?-_x3D234vA5Ij?rz9r>%rOA~Uh(!j=(XemUhEP(cP-3De7GhOey+ttAk*FNF0Dm; z_ZgC19!VJ|$LS(d*(NFt#WA!*v>|7@y$*?*-q{%)LpclqeBu?n6C-*RH4DgR{#0`4 z3Vz8Do*T`Kx%>2v7XG+vPo$<{%w2y%@2i;C?6s{rVkj}z_;*lH+;~8jrS$-DMdW+D zNI}*O`{Yws!?0C>eg!T3gK1zvj=}~=){`T|A^ZpmzJ&47Ie0weU|FGLcHkuw0h*$L zAQ2biJpe=SSN;xaGHF*Y5neWGbm>LxYN`Q^3DEQb(Vm5zE}4*AP6&R)8C3*?A#<41 zLXzGFeL4jle*O6^(RC`| z#M&sdTx{agFS;;l@n|>@5kpD0n**z5kMiJm)0F_TD7K3z#%ZRo+j~WhDn=8kQm~H++V_V~mFe0zParzo#B0aT_ z8a>r4;XuEwsEyvUK;$rne)EmcI-%5CRh!3w}EM zJXn6A8EHAKdT~{y3X5Tmi8)iu9M;a|IK$INUedS86Saqk44ALX@b7bRY0V7W#Fj%& z{i&1N_tS<3S|i$QZP}fv={PrLV%|@#VwJlA>jsE9W4q=B_1f>wxyqE>ur%Fdj_4h_ z%yU&O-iDiP3n^e4+A_KM@qykN^o`lpZLKk*!FMb7R z{(ku4_fyTY_!nnsn&){h&Yx*s)V{cA)%?@@;?E1s%kdYNZ#4fdzxex6^WWze|GsOI ze!n37|KIsk#PBP7jfaU@vc@~O;AGSRCuYoCg*kNo#*C2T~I;YUb5ZsM0u<>yC!QG)-6{g>xI zUKmNIA1B2v-K0MG|o1R);U}hdR%q&WieIeo!-@xl$(pqG+!}X#-U#2 z*r2I~#j>7e$2gXjA;FcOqhU$z4uTLpYdNQb=w~25uYRi(l!ASzVfth#(G8mn})Aw>AIkRlX zZP(l9VTH+Sl+^(lmx*Qf;5H*qDaS0x^yc?_;SdHc<@9K(F?^}66(oi#Lcz3d!)$En zRA2kILJXBQkK)cmoHrVhK_!H&axvU!58+F6Xh&^ON{VG~qa_5CXH!65m7&z&_9JWt zgh~{esn4h*psjXYr6|+L$i@S0d0n8IO6Ru94y8y~eRVc%hA5Q!ZpTDLfqPHB;O)C+ zaN!h+8N*sP(Yl;_dZMSRwLT(;{W)b+Fx9ovaP|n{lqXULdzI;WV*6FuH@x?&^Y1q7 z*A#~y?AMmXi5=8c<$50&fi82!>>4SS#GJ{9B$h8oXMi9ENI`VCYJ$U$4$z=eF${qG zYTlqrjyU>4NnoF~Xsu^Nfw(<6jpsO@;I9}I_Ydbr(34W{psnMXjY(^gaP}l}TP~!+WTU3Mb&hjxV-RjX&g=D^j zM{$xtU| zgHLg1o1-I$&(2Mhp@1K1S2f%U2w&t8`UM!%0y;B7@`$sN3EaHC%qH$ZR`3o8-DrBm z)^z(5yFsV4m2wdZ%ZRWh@(B=dGRje~)chEMc@##~-IW3Gh#e`AK^|E>1qjRsBe}67 z3^pV5tSk2HBt96GPVy1V8aja#2*T+j$rv*yo-y$UQR&QL0F-TH0zyGrhUyg#6x;SN zJ{MedJ31C;K@>ogM@P{DoS7n*`S@VqTHqF=t~*$ea}(6>VnPY+!|({SQ$ix~^kPmi z{9?xt!K!cJ@uSOwKdP^#OSbuO%H=Q2d-TqM6ixt|bWboX0uasguLaaN)`l$|EpUX8;+CqMkO?8JvP4%54TxX+poHn2WRSB+iml z3?;NM6aXUSFU)hHE0~cjr>by$>9>& z_^+2GtI}9Mkdv;MB^4GohJ;E?yy5zmGVfU#Uy|Mf(o4`Sf5n76908%z1o~BNYE^7lshDTCqR;2>C z>!JnL%gjPEWr|a;kmy#sWJ>v)wt^{n>|DJdMG@Wx9;B8ay`@D186V>#XZ-vnib@`i zyaAC03t|GB%wKu~1ex>obU$^6y>E&RekDv!u4m-2D&74cdR4a#^)7}qjYbw=3{1#> z5YWYG00�j-EK2!5+pZb5^?@jHc3m&Jv$hH@cLnFd;fk1C$gQLCizr_2nxXjblav zVD`*n>JLXuT`qv9>``As^%16`8>Q_VtJS)_Ezq zDnC&j%11uUFl9147=s5!^?)E7_(91&pa8;UK7Uu5e&H|b0XYDuJT1eRj@f$nl`r8- ze+Z+d_)0h%6lY=)g&d`giF|i1#kzwo%CH0Tcm|r+v`~}qWOp%KNHmq9jy>U%wvLdE zGL8m91SFLar7Jmi`yCd&aK<4A@&`@fx2}AZ6w3>_zq;C1p?B+zHuM|cD#U`L_*St7 z0XIF|bYo1%(F=t0U&sVLUtY#Q=ePIMOk@R($r|sL5&34@qFM0wr=sdQx}gOgU|)NN zOqBLb%8nkHtPD546AbfV8N_70+Si{2f{8xDsz}e#4!=h0u+~9tzLsX-6GsIx9AJxX zZR{(TWj20LOw&cnuP`*~w z0s5y?h#;YqmQQk^^ypa|nGG1viVrW})w>TjO>Hs+DC5V1Siy93E^_^B#9u&>Dj>zU z)o1?7oB`;aj;?mTvZt`y&}2qmY|9@dK^0D;?8Qx_Uoy^}Dw9I<%nvCaca|dfd|Q8G zgY3TXHa|TUU=^xPe=_3mfDIpSxUuOa5BZf%DUlJ-w*m-#K8cu)l0B}8D3~?aV z8_MWRnc=C!K?R645aGKGnm`LQME1$=IoK0!gkGUIaE1uis3c6x%NRUP?+Knd%Ps~( z10dH!ksVSvYqR4&Da<)ox$~SblTHtAKW{QE{7CUgzJ`*IK=d0crE~%C3mr>Ff&YPE z!gwN25D1*l%#eqn@|C(cw!H=VL9BzdGMEUF;!vkHh zf$V{_S<*cU&y%ZN4w?9L3MpyGUMLWQgskyFeWjC5GAXY3a*0s2PbX$k8E)=Zh!uq{A7v${T6E(TJrpc)?wRrlV*N1M z!9z|Owk?%?$^x3D$k@2gECb+2ftjqq)j$oqF0=F?%MktyM;X9Xz4etIj6xbRc#pF9 zEt_3PY&uCsDLIR6wu;hCI<^XX{c~1m8bO?Lk!?^R^J}n}$!fOQc{YwW!%dsX+ABq@ zK+(<+X6pqV4bBPClDb)dwzN)>fJtnwq`B8aKUrPZ7lrL+_D5$X7$>&87Wa@O} zLn@`_;aZ<}Z#C`lB;(V1#oS=gyhy6-g#E$*wufc9Uav!F-1NB1)wc?v`#|lC$T@+A`=vP^_Aqgp#NsjgDnns2onNR4Cyp*ld-` zs-GnvddP2RIU&L7VEHAX$gIwcmt8XJWl?$v`uo-s2*G7GE>NjbzC0w3Kz0puUs;?w z_8`x&&|QM_W(D#>D(!7a!lx=5TnejdsUrl=5 zH@DnsyK3)5el#kN`+;lDDe4zv)#SW5PmiVR#SH}}MWrd5U~9|mUWX_)J#Th&2&vNY z#+1toMnkuVSdg&vnT`WRmN>Y|Y|7Oucc%)nl9HTQe|x}7t2`he9-VhLAuCk5YFD;B z_!ilAC}bz#+RMt-QJ0$PfJ3)Dt8ICFgPB=${d?uWALr`-a!@BLja0yTL5g~cb9F(z z#sow32~K|R#6aw=Cm_#CG|oIJx4biUpu`=tL42gnmbqG)T(ZuqPU#gQD{4$ zSyx&1zR~z()AU-gWT)H;Q`=BSp20xL91b1raP5nJ!D~LLT7|&&gOa(3CccTvU1~3` z%JP-%mUUGlKPIL7)Yp{U+T0_m_wGqR#nP3^TNJCZ2nX^`qw<>GvXc%EyGhcD8jXPk=C(W{jd+W9rA?d3Vr6{-ygN2x>Y$|7RWiJcK7 zyHz(yIWntY=tt3;l_E}J2jq{VOSHzEtd-)I7=G$viv6&1(GJJ8&hh!y@Q5OR;9NALpIV9J>c1I=LS8zgm;gG)DOub~hvi=XmwJOz3eT6=u%z*Bnu6 zScyqnxn-ukD-3v$pPOho8ti&3UK^H_`y#kN$nkl6guVV?C}Xy&-^Go+2DB*$nzc)8 z+HruNyx$~WRiLswzP=km?4G}O8HeJcmp2tygjX>=cg7J^M4X-Dqa9;yU7!knpx5Cni6kG4>szd0>8k@_Z=cl^jkH zZ6HRd*c-?ZP|Vm-b%D$5ELZbgT$zfcP9c)u&M|$_+`Y<@zQ(JGRV~9o8E8me@Vx9W zFL$O-ycU$7Y5npns%1^2c}%PVT}i*{(Mp%_=+lMs?MrIsC}wdrtQUQ`OvY9sJ$qwZE{~82T?Y=XylcJ%FLqE%-hJB zl_vPS3ESTG@Gh(5DhKRcQlB3!!iNU-{xJ7npZsp}>#yr+-_=>fenRv1hhRYC61L*3 zde-Icsf=IH^g@TqL7B1yzlV-2VzyI1ecOK@TF%}9{xpRS`c zWkJ4$))0G1>P7T0ZR;ZG>tkiyPK=d9P<{<5)>|<4TN4nH`}}=NIjw@>702Wi(fN;I zWsoVT3~kxzR__Y`wcX<8D}vY2p-b~tnwF@dC7s<@F{#hVD9c;Q=Lar;sijS-!TJQL z%^{gga2fx)r=JOmp&4#=S%fk%8X3U>8yEEr+@9!UY}-jp#F>4 zyaKv5LAB-AeJBo^N-Cf+!q>rauh+4!t+~KF=%8IGe63)$y0zrVO0>LgqAKO17e!RT zD<|^>r;dz^JuMnN)ZoGGca}dwmu+dJ&2L#2+56a#9M?iXDK{@Wb6>61>joY*Kov%Z z{(bVCl|y?exi%*lVIQQpyA6(tw{QxoXghj6Q<#qqcgUA7a2`mVr1m?{*I11XIMeu8 zm(vR4GBWn*#O5%c4E1+&v?+gG2zQ)QN?!i)@?Gs<-urNSPLC&>d-h(nZaED`yE^_Pl z$RIiIJM%At8#h0^c+?Im-&(b=p773m`mz!rDl2n2sXF_YXeQeh z72S?9Tdcd9#F}hK$Vmi#D=B!sQ63`hshxI^U99HcG#%0RZFtu>X!8qvn0@iNb)9?) z)j(ttv|}dVCF6(0Z!2?l3S85TTM>#Z>q(z<=W`Z%#Ta`-%Bs+Yh%d-!*;|Is!CexVg(tbRZnr^2=U=wi8hQ)h z;@8YRndy6dQ#zC!ZJr_{*!+9*m+gaea);Bqj{|=yiyODsg`09YyWIZG`~F?E;f`&Q6)Li?w~%0e+(~e)c1u#`aTt#kE61hODR9gDZE&3z z{YZ0rGW~9*NNznsqM6mJF`#zE=vn=%%ub%}N-;nDG2d>HY2|@_U5jc-Ld|&Fw_hTm z<7ZFmozbYnj-JDjt!CL{5hZD#q(_Mi)@dKpkNx^!TneJ^6n7`Lp6RAB(4Bt?ivu&Y z#ey`Fc-nqx1$dmy3V2zbAQVfVrF(NJP9=P8C`e2D-Y-IFx7@-y(!_}W`1zmgW%!)f zL~9Oj=Vh(s&)ZK$gEVKG<|K|KMEAA-HqXuHkTTO=Kby&IpV`p-w_`3GQ0Yi_@1jnN zW9vJM_wdwB$=CXiU$;%49Rx2Xw7-VK+HQfMU`Qm=ZgvA5L&f|bqi;Eyj21~7d3FWr z1~D_RIHR2LV<0kl?qnREUkk_yOLkxFs0M&pMFw{A+g*TQa*Dp4)y~gX%VeQ9lEW}} z^^+jUKa#IjUq+H>Z(LM4Fn^w{RAk$duvq%C+^U;K4_@sQolk*QC4%3*Z5}D4?|&{> z*6EKW1I0g)cSZ)Fu{<;gBoc+X^&*yroBMLPjW8-qel(`WRAEgBUwx9FeD>Wel^gB< zyoj}Nq=I%p>pGGd{W%Nc29m-nx0BG%6IQ9=cB@B#p_<oJeEJ%*@QVhDUEqtDeg5?>*;G~Z%h3dl6CFnLhVLefRJ8zYfhNtr zhoh6-pntERqckUSY5hwO{F^~~&GgrLwANPAwG^x)X}xc6&`7rqY%N%7U>$rck#s7f zlS4wf?sTJt>J46<`vQDThE-CR!A6p57Y6k(f)8WV8cSw+<&rYT_lkOI48BILTg-E) zxXjmV>{G-S|JuxS;`dj_2z64rf!tT|x^8TTN!#=0!guu(AEXyx2HfUiiI(SxD7bXB zEC;FzIFz(Fzi;HXRA(czW1_V!c8SCnP;Afh3DVf4ET`)*I=>d2v=S4wXUGyWz)R?! zEZI>#4qZ^@vHiLT%HZ&=b7^0M+mG$(v}m~`8NFUJ5M~U0SM-FuMbSnaY}RdNS#r`o z$Gt}1Xv4fq+he)u`&K0xvuld1_5S%*-Is~+)f{f1p>Kxa^$gV!nJPuOEKst5pyGPbU(c;y$?|5DMy!8Iw|OwB ze9LUD&ir0Yu{$KAGu7*wz>}J&zadwL~a52-ukFpE@(e1u#fpAx%VSEEA(Pn>aSksrM>8j z))1`Cx9qAY%1@E9^%{k&zlz%V`YR8P8}wA#ZJSuO)jkUfgcTDw3_@kuoX+b4wmGsv zyjrj()B%55B^MeIO=89xPK2~4!qfX6mK~m%*wMkBElU5$P}67=Z9H&VyspF+JR>Gd z{l|d*qkjL+Eg^m+0on2^UaP&EO1V05x`pqP7N?-FKS-NLiv>5jiM{86$|^e$f$+|c z3E@)ta1^W}im#mNr1Qn6iNQ6QjO^5s`ef6zIuFpPruk;<%DoSNvEQrI*4W$K!~-?B^oY9=4or z^IbP4OkV4`nr@`^FOe}Dh*FCt>Tgh$ejnN!Np7Om zm9ttwS;?BVlDcI@8;>nli{GUM=lQ_;{iaJEfnp^gMu3zArKDtijpshMKOgAx z8XecEEgr5mJHvB+mFWb;G4U@7j|riuWY{+W zFh>=~h0j*W;@z24*4c*LwxK8E+@Djjbr73Y zmSP55svl&=I1X&p-@mIN)f2;QUu7hsiU*Dhf_;=mf^Iu@vW7p2NA)%@GwQCo)kz>^ zt4Yj7N|oCVl__$XRDQ2wMd8z+_5HfNFGnR9FM~TBk~z5r3uTCR%>#3jsbQA$q>kQ_ zH9Sk!_VIRHQ@z1Zm)@AlTXv5$YI~sE1~%j+&Cd{dB@K?dM&Y?!@4CcmDby0?9)eru z!m@|Io%INh2G6AFs6gpO(K2c5~NM})tLWfE#{Dm07ZH68Z zHMq3_Egjl`>FCM|uE|o4>*#Uoa7S8}i4NLpltw4T0{dOH?`qb{XGPN)KP1G3GWoRD zrCi_U{@o$Kd1tyLanZ)-^V3RUi-;TvdEWa4{9i6482VY>4=_zV2JAJqguHx(ESe?_ z9sJT{7o%uN`Fk8<6UQkuQg1Nlj^!BSKW)lS$KH;SxSMstErK%Pq%pI)`V|;9n17VJ$6xZ8G$sFnxB@x{qwV3 zph~SdJh>f;W?Fob@+>$lazy)0^N&4Wed3?$!ugX_hiU(2>&FoO;lukl_DFG?o?FMR zZ|$>-7g^PQRi{{R_Xj?M++LRb4f;X$Fw~$~-LK{LN}p58_o(reow0u&^c0Vlt4KXf zWgsX2xZVA@#{Q;yi(i=_qlaIQ4fendGxu8FW`X2zo0)eo%d6}+ANwisqHFTomd{tc3hq{lwYc*`oc7vQB5tGrCg`yApi=u=rP&k-lsVI`3y* zHKBhxmmY&kpFR?}&zD95p*oPYELjRngfixH`e9&-{kHrHO^4!wqW4Lj=%SKdRgEyU z6i8j0Oh)05U4k^lBS6UAE^8YTp*w_*jTK5!7A2VN&;~{)KNo8qh)O-PAHySfY^tOM zChHuEbEb|&t!V2fr0M$C#(sql4B}dUp%WZAbtiFGHfC<0gU0FfG;n3&xSdo!q5iuA zF2_qgH5lSzUw-RYB$-|G-F(4tTbiGZRA3A(gO!GVSA9T;c$)$AYo&hV^mxNa=kUD# z(@TB77!-!nAWFg@THPSV#30tmAkNnyKGMK1#$aL(m0f1geK1Z+I~ezCf+WG~6P5D- zj`gYX*PnOr#~M(*z#FuXQ44GvsIAreFdF2w!9G$`A2Ahbnl$8MwC;5AANh!x2Qxg^ zR_N+L=6Px-tqD9UyRLcI5?NqaMV%@1&Y=27m4;jGYpurYv&k%{q&U>|Yx1HoZuI|L zsK;0PR}RG3)aY<0jN!!w+W=i+;Y8tpUbbslYH@PL>BQI>U8<4s&^m2fe2ahWguLoXKLxeVSS`uZrG?v%VgkUmai+;N_~EKdVpOsyQ01KWk>2`n4Pbf2}_qDXCGb}4Dzy|!bW9XOZVGrS2j03sowZ(ittY5&LOu*ybSZhQ|)+lxxt{#pfL;mep*{i9SId z0`6;m#(IoEm?hN|sCkt0)$jB-x9h8FXWM?QH|>Pi6|T@T7q|TzoMs5;{Cv)2T2=6B zf{Gzr>!Y~niC2~uBV7@&Cv7jsSB>SzpTZ1A!2P6#Gb@1%v{nmF{mzBO@6(*wg`t^* z9zheP4$oRzot#3v$UwhZeE}1A2F1X~vJXz0HQW$K z!yCdo4R31Y6Gagc51{$7ZO&cQ$zjg|ISV24ojuwOKMyin&L^c~>pzD3}4C`@ch#S3ma4#$y+=0uIotRPj@=yL37O^-W`D{d%O z!ovy6!>7zB4r4m46=mJ>ijBZ1aUQ+&$1+Da>!yq( ze-&?T|3uF&WbSSf-AV`Tej5YVOvhFklXq;UnqiiAd8(1UT~YbS{#lZ5iKO$v+rWeB zoeAp=FW82Y7$~~g{?+TpiH%<(*p(MD$%CqAoNcU!Z|d(@sU6m9_~phi)_5Ahy+&SG z9`}ySDn1L>+L^X3BS#j@<%;j{P%)-hJYp3dm_4=NtI;4J1jQ6~yJ}~x?HA-T=y+R` zlI-@^vP-T*lhX8p6%^dqe;=#wt$&U{eLTJlDtnlY7a zNm`H!t=cLbF*(?e=GZ{9wyjOB^=^6K0~5cOOWX&(Sdi4L(#UE9;~CGHVFfEF-Oa&^YNG zrxE|zDVDPc@kckV4~teRG){QxCP-|=ZqS4>Z%_uoK>p5sh;-5ev6mLtC_|kOHm0aw zJJWq|rvK*5@Xr}a?*hBq7_(c)_^-7GG5PfG)S>cbT-fy3+Q*hmmnK(3_P8o5-W$ouW{ex;t2|BBtMuCUkZ)^huLm%0+g%BE;(Wss zc;&pKLH#d;jW`i?^1>~r5^d6UlvqN(B8G1}jZ@5S#~^<|Z(@S2Pr@YAriJ-+<738kD@!k*?~-)2l#ezNQxG&gRBkX>6P3$F`CApgxvVa> zhAje{Go_{G=gNM3t-4M$9A^8pfOUQ|opioqF;|tcc`0ecxqI^o+?L(K+1p&#Qh?+O zBc2Gp;d--|UHDFcrFyUW&yorcmB!kb`YHjoIy0hCMVORNw+Fc#v2oB>QwPQA=*Uvq^(;&Q<`1}2*0JU#^^X)W$Th3 z4$BC)@8Rc#r`^BT`*o{MtSM=x?H&~!6(9VSsRfxWO)$rsFXu`w)Kyo7p1%c*1z=XV z&+|%uZe|~o4HwS8K{^A+Y_49^C3Y1)UZtIMaMZpGs6FcErjPdS_;hVJ zlv5{Av;XuRIUhQsa(~nfoliBlt++J;@*8xs4xB0Ubt_yAaRvPFzROJYfjZ;fE>mz) zRnt3r=i8Y)KI3<={&2d;c+%xto{v)h=h`zPw{Gp>+ z`Ahw$mVWM=tfZY`M#&(UY18|?>pYJl65F)DaV{A?vz6uv;4nRgRy^L3?dGiJoVvM* zf0&g2<5%#0YOoOF@|PR!hovIH`(+I5ZeNhZI!oavqCrO+G^ZalISSF$dK=O2SpWQF z`!(O$M^|IIEdA$$s|=ysr+r2(X+#s~xt!=_DSX%XKDS>+iJCBTG>dG{3V*6KI;g1=@h@W!9x z6>59w-m=En@LD0|7p1=)zEoM1IiVMMYSbxe<;BHW{noqQB_m-BRrjWPNiWF>IsF+x z;^JTW-not=Egm~Q(?;8VcyFz^~kc~iLV7r)6(vU8a4zSB<&)Ee-mg>dLM!92u)`L_U0&P0D|9PUdz}9dsu_ znD~p%usU%gErO=U#C845NL;+3Nw^^d9)R1rv#jo1Xjb3+`(qd7_Rd)Wm*Sr%eEc0e zJlCFXcJge-Mm~!hdGvf-Ecf6M{$m3NedNfs$WhHmqWL8)cjTCUWP1E$zhC4er9M6c zF<%-v-CGwBl=bRO38DeJO%IDFWMsJ^9&Z=6j&<0p$oZ% zlw|GX2m*R`gvJ2Kz+i;$-VrYm$9QG*^*rW#nar;AAEU3E4jF`Ag3|&J1&o0}ML3a} zXel4E1V*%T zAKNC*I(1opDzVn(z1*Q-05~wpq-0>RODF?iK{5q3^2&ICAyB9&6<%m@ZL}wml_cli z6o#5DrhoLQNsJ52s=p4`dhdV#_E+ufXlp#{TEIW` zi_?SU&VcX#G%nA6d|qe`AYK1=dGY(?`*)Hi21u=p#FDdzA)~0o`jF9ds%OX;n2|DX zEVDxxZycLW=feqMPV(BKUL+Rk3UW0 z;*38XO|2r3q01gFkZCB^FOX%bdM=P{X`~{UW9twum}~FdFPL{L;#@G_B~gVWRNzq< zE>!5%&@WWvH*hXg95kgOT=HNoT(~s+pkMge%Kgh)k#*nmi7it2?( zRfdtOXmyT5glJ8H_kd_^NyLR{U3sFaSbcS2gjhp;!+=<0^T35zQ~Q*vcysq!gm_E; z!GL({@Wq9A8-ZF)qJ510kwnL&*q}t`jOrhWt~nz$$?heGN0L3O-h-0=0XsNo z_3sw`cd)~^Mg=+|VCZ;C?b`7F1Up=N@%Q4-H9YY5D@Ls@)7WT63p*_NDDAc6u~B*@ z+IAwHzDfHS)4ieNF)s5%nU~BrX*(u(-AsN=@-z+|Pl)$UlR73@M=zzPxU|c`^y*`J z0D5cBP4ZbZjfVW3F2`f}c|-AG`2|z8zw(Qg#$2ax<@BXOpjIR=1;YB#UxgKyBn`z? z^XQCN=;LNR4Aee`S8p}wm4?#$2Wic*(3_P)D0cw2Dj|{nK9bGzTv-{l> zwQt58sePVck@E0w1RBlAb)zdL#1@`Ov-igbuHFQ{ob#l7XB3QkFFERZ1qrC~$6{&(IT7&@oohPDs0Gsiu$>qLo zS8}$#aYF5_S$XzimsR2s!NO@1A^=`PAU7ep(prr1o|}75^iY4pU1&3%LG<54@$>2C z36`{utf4E~+f(AvpNbewBs)pVn^bP^3Qbc+u^se-d(gc(uO$7wG?0wFeGiUDXIeX5Y zyVl&j_?PvqkL2IW^FHrWVv}#?nSj72HLU)Q5uByhxIn-VDFzNI3(+o)9z?JlpFde# zg&e~O9xA!VPGtoRV9m0ED&v+(0vrPwTj5aEkvRX3$hqqesE1yv?hwCWSmr}`4k1)I zsNzE*EX{jRMTQP|cqrr=n^>@D^Ds#WQeK!dhe!@#VjHcTPd~EPr8h=xv(KW=ew(;P zL}DZaA!JSO-xngbiK4QCZ?c~u#_w{ul0JuaPO+UziXu&NLn?|8n1WIF0d$`1?;@+W zRue@k#x%TS3S#Ndu)AvIlp!PjsBU<$%yxU48#I6s9vkfNs)~H2w1}Q~6lx9f&t$oH z_I6hh+2`4jXw4S*jaS|&P|&q3?paCaWm6pC!|=xxCJgdiG(ebq!sGWutPloLCuolB z3Z?u)(e15piM(K*ytHR?v}=Xh{07D(@o(fvd9_0%3?5T><5t&}Dmv)=jY-}2Bk8lX>@nuk~VQKQcvC<>UxD#V9@|T7UXZ+cE7t&bFPOhrHqV_8Nz7$fAKbm78ue z$m87z!T6^!-_e$Gj)s!Oqh0Y>ASBI+brmS6T-T~t<4+`7&j1qr(E5j#hvq1$p5v?z zuc0a1Zl??uWU6nUe&hFw@dT*H0I-&Mk~0FWg6e8mxv_euc)(A(>TBEe$Oww6K6%8EB(vA|V6oe36dMw`C`%|f$U}kj1?lY2&5qH>(nF>AxU!7SdD@d znZM5{$D}Py&)2(b{fBDK-C!eb1L>l!;Pz<)_3;AVeaelwXZGKhw`$tSrNP|F7jQnq zv+b{&<0a2OwzmBKwYNH${ivpp{oJQgxy(9pbp$D)1W0K@kOgjHQyw$A z+{@@#Xu>QCK$XyibmyWmRscJL*mxE4dcq)YX}K7!Gt4eydKkNAP~p^5GmnFzFuwMn zhtP=tWYiuznuT@Hso?fa49yMN zH&#v|)(E%E0tpX<5oS1C{)=_1|PQ&jqnyNp+k+NpaPR z%~pA1`CQ4{azc{rvR0dL>jFsZDVDI;M54V4Y7HoG@$eFW6N1$-!n`vM`sk`*S%?T~ z`LTpUEn}u2x`Db_-@a+BCqjt#wUN>F+K>0p%!5kS?&9;eyznZo84N|NiHMgeV`UqhkA`H`^BAfIIIHW7Jc|+87yq9EPInw z0=)g6Q&V=qC0t<~ zDjuq-5R{=)=lZMZzF`B$l6Kh-DnYJ^KeDgku1YfxEi-R^cKGf4`uz0Qi?+{y4nO}P zgyK&r_g-D|X5)Q@!|w2#5x@fxC?3j|C15e(9R?_OAG+FKYDD3|8~Kgu;4tnx?}%?8 z(3;k8+f0B3BA~gVZdX)EI{>?+trUQ69{V=8(f5D+pyphjSPMc`KA# z7tLd-4b+tP+;yy;^vrA~6I~0M3@> z+wZ2mwzuI7q5L zGZT5C0yo~JirKlnuB>L{A6InXX|g6y%GC#>uE3>svOpzum--(~IY-|MOcQvoq81<# zpD}rqFY2c|>@Pab{(XpBab%Uo&#!uAc>Bk2qHE4r?1VxZMMN|WI`UU8{BNFZ`m z1@1*GpG|z8E6c#7;m>iD0WIT^rVR%L8&;{kk0`kAgNxy|>9!L-%CyO7d42*_bDR zBaUdxJP5XA{h74$WY*rjKHs<$L(eeX{8@mF5>kc;&JO7PtV|g$wYW7n`@!pH6^^XT zO6kGeSlZ9(BK0zx2ZDgfnxE`tzQW385ma**59Mp$aU(6w;h_-_KWaNT1}Mq_m?zGw z?|ue#j!8EN7pg;ieCps#u;hO?&yO063uOhNz`-*>xTut}D|_X_z9OWd$K1h-mm@I9 z70O}5qU>I^1wV8KwTvmeaCy=6*>7eVaZHP$7g>fvN3=P@Qy}Ja{`Xiz>)I+ZMFRVO zfBvmi&-uN(&+m|AiKOIKEp>E-)7|aQUu*G=Gx^H?zIgc2m!;kVMrA~!TRAHN)=v>`7 ztnALt%}$066}ERZf^nt~CHyHVoHS9TI4nO>fGXOfi<`|jSXrl#Dgo08SjWTeit55s z83JA{Vqhrcj<})^4$M4aFh!}{O9z%el6N62txkurV8IERWu*Sp{mSK z^tDc#grZkX@!g#$*YqinUsnVP)$@mUVyTH3`k=1fa2FR2a)>B^O%w&YFNy(J7ePo} z5Mc@)xOx#xpxaONpAv8+z#v7Q%SseODG#DTj|j7fIE8{qm*A;75OP9fpps^X3>bQy zXKe?lsJ7AwN&#_kybm~tZdQkScr3y2!K#3j)(7;JQ~?M8pX0$p=%(;By?LXI^sK2T zIC;)cQ2=rXWHS0R$>NFWenpBw`5E3Bi~3SawE4aR3-6 z9!6RS=}RO^!xEyWIQ)u$#KApzaJu1ursK{yzB4m6R| zA8ZVr)rR@;!G+;4QB4D_&>&H~GARH&N*zp5WCmUeB31%8>_HQdj`0|ngNH{vBIL4E zJyg;<_40nh9YG)l3qTtp5wQ77T>%`HjD|&a1x6|BCpZCp(ClNI94S_?R7WV=rHyC_ zD#8&p#$ZrT?^814syr3mS?F_%kJ8k`_VWf19%iHYONtW!!(zN<%zYotxHU?_TaF_P zJc7C;0k4Z7rf8TcgV7nGpCTPJ3FkQV3u=voh>n2hPJ*2-l%LO88E4>NpZbVWv4otW z3G&vi0epVLHwk@~Vo*s2vRrU-C~pQ1j6Kjx6eaSRG=`{vL`W(Ee@aKm4E*BZ>)mF+UH-rjI%90D?18>iX>6GXAt}5_YW@#?wbo9Ypwo*X&J0KoP^Uc?;|qPk<^frP&m_IQ%X_PKqY)Wzvm2 zff2kWEEy~N+#43DZDkBMa>N=z8)<`6@p1PqdLqFUAwF3DTWDx9Ix3$hWL6>dGXoSn z0$Q~staj&3K!=b2@XT=JjR8RJ6+k`(>yy*Lo~;ljA#<5kS|+2oMNxCfXnm(oWIJ~@WG)N zzlpHeYu3iT4F;(rI9{8SW@|TH1VA1R!#YD_@Oci}kz2nm7e3P8EEb7Do^OhsW)j^NIlL7`Zqcs!v( z0VDwvq^cCatC#!TDmOelfaYS^%&EZ?z+;}_=!NKo#i+ui(d@NPIsV)P1p_Qq>7XI_ zm)&2$2>_S`y2umYHi8ltT1b3@j`GQm^};Mhc*2(mMVCqqQo>g$e z($?KUgz0?6r1PeaBrDV{B8b>uTPi*$N;gzo2g2c*zrMUj)!G)_0*rblQ^8DP2MqYO z49wk=B-jBi7f9He1*I?=!orIq&k3X#q$FXa5+W8;NT>c*NAyC6k_hjZh~T>JQtiu8 zgPJvC_5NZ{_X>9hBGsbYXr3wbDg^_rI&48;Y?kKe!c%-JvT}}+Cv{QAIe~jtxzzw zyIt+mRC~%)|HG|8ndh0gI~A-rNwE+Mg=OTIU<;aTQ`5b;-!{HZ_j&NL@g+$U_I8G8 zYIwC^M$3c7eB1EC^rQ~&B7wI}noadZ4|012>slh3wJefK_?nvpo0PoY1rawFdxy8F zw0vT2p1eq}Z5s4}^|wTNVSf%ZAKU1Ev&~(%Z65vI^ku7o-neC_sbN>JDWb74EZx<# zp{?JtsBS)CxFi>R2M@}?^V>9$MkWzvQ2qSfVmsBgxYgL4-ZIV8-lcyT!d1urEj_}f z!MY$okg2_7tM+3_OMOH$nGl}bu6=ACFX>*%+}tV_*%~6#%o2&8k0`p+(4p|V{q+_l zk6K50N?U_>SLtSBXGA9*X&bpu*W3CQz;@l}ROjQ2u8aA1r6ld#c3u14-S;Bf&TNae z%%6WJsq(RHf%&yiGiwsY?`YrK&)umT zGZ;~gYygst)(2UTmer354l53hDld$X4}ZKgm3&Pfb&MR^7W^pO+*$42w^8C#FyzDN z)Bfh7xlfLzOW?fodi(JBZ+yu@f~eg%f6E|{r8OsV@c!_)!cHH>`IwS+x0ptA(RR-V z{W0R^(Te7YRsG@C-%V(tc8%>xx6-8A=J9pre#AoGsNG~}c+U*;RC!U`!tIHmJ0>J* zlMJKEy}`EHmt3*L;Z1T-7a>+Z|o+n+s%m@B)vattP+~dw(k#H=nhez^R(}c zy8LWEvMSW$^Q=#Hr>%>nUF^I=O?#?ocy_?f-iCGR8tY<<`n+h_T=a2k_1y)T#mN^J zLUYkOvvo~9BRgXsS~|K|x_#`XBZVi})T?jaog~=q*vcFVW$i5`9jqbiPPH2vC7Y6s zYE)GmM4}jw}tzi;FiG zmpJaF6%X}m&~?a6HS*n?aNYjI@O44gVBr3nZmED;X7(`=&MDmFgv{z{2Ep3Skofnz zYkct|idk!_w^2Be?Ln!8%bHYYEl zHrR)nHs(LkmQS*0Hj;@=&ppGNl((jQn52%LhLlcQk74e=+cC`z!aOeUXoSGg(`V6a%}=JJe@W@h#sOS!Y%5 z?{a=VAiw^7BC7w|^?}6gAC?9`gnWK5J(||L{zLB3*S_e_HXjdv3hxK(PBOQB>-5}} z{P_J`!#o5GosX@!guFiqt6XR{_yyoX#MHp`8ZMKeef}2(#Q8AZTEAA;qMhP zI0!<#Br$?OJ-l>qu+*cCJN=0Wcmj1aIAlW@Z#qPV?i+VTP~VQ;=8H8|iKUS<2|O0d zQ;iqA-f<^o6fuy*BAmwJNo_e0CH?FVO+$Q9Z@dcQK*r`cUtf|e>p>SRetjYnk|`N= z>MV#Zc@{gjHc-j;@ttY1xUlCBYt?MkFKjKk)8$i@wv%H+#5JY8^<3#xtaCp_X0qtw z=iE+i2@b@oUC8c|XntG3g;MkmrQZ;pO1n>HTkDflHd!IqzmorUTwX;vH{KJ%9UVX6}VA~GTxDnI- zJ<@sb{V!rBY&Gb@TM*xJagFd!?$FEIX4eqZQ< zMnjR=?MQZ;Ez$L{s$k90V>Rv;@h1;&|2TSrmK#>neEi_%4^7P{Pu-qs>AU@Ss$-hy zrln_F^FzzP#f@g9!DI5r^dsMzM>E|_tU_akeo-%^&6B&wwC`uL+KzMVEF{cnzU!); zvY#Hmp|@BmKXu)9Ywn?zeR~bZr@3Pe;v-sUfE zkSj@!l+@gW2+rZI#e46D6DiHy?TGiL63&g!I8mx6_*L0|2;idG4+mct*ykri(H|~pw%hf96`Pf<3b$P9JZIincMVL!?ZSLg8 zQ$PJwvk-IAH~z}kbz*;KU+m)xSErxb`aX>ZjfMAVcbZ1cnp`JQuNoDS>dKk-jt*CK zw)kBl_rASFe^;h!${N$8yFad?_6jMG8cFKGbuJCP%R_?jU2x{xT()r<;FxOB%fGm- zo%v3eGcOT3O(08E;ONVDhG^0}?x1ai2VYhxlG%I6v8FjA0)130%YVa&sPsN)Dbd)=JyG>oU0fRja?R3ZjUAnqjc9rzHPf+Rm77#aZ#= z+ee-UNg156{B^%-@y8Y}rFrRthK%=k42P}si5UYUZ&gvZ^C~zpEC zvJQyvYFtRp-#7; zg;T_{GZThhot_&7PEqzgrYsvey|>q$Vtmg`5k$Iuay-tlQ9fo|yt@4lE-akmGtbQU zlynE46gVfA`Irk?>JI9!J14iCnTv+#4w>?}qz?O7NEGT0+giA!FP>RQ_3D0bDR9Z$ z@v*$Kq5IK$-6i|{%o0hYHxj_(nnUJmCC{rj8e!r3hV|S^SxIj!zQ8qK*w%UxivaOZ{iJ)Td}&Yjb@?VWQm1a$+L4ConF1Essgt%dtY0F4ZZ26b+>oE=e9;f z`ZJw8?(d^~?aX-fX9q3Zt1{2+td#WU#tYnQ%6#qZEcNH-*WK${&g~sT^cPlnUN#K- zI=B|~2B-A%lVP#W!yrKBI%*^EgLkx?Xr3;~f@&yTT-K3|UE zC4!92+eS)j$2pj!dJ?aG11^xi-1TG_rIx-ZSOgXBiw44rKgI2cofToD#BBQf!4 zD*XK+tju=pdn{Y?+&Ml>`cBJZo4TqwxmawwT+6o6wcD%I9mnmTPlz8&!LH!}!7`2@ za+}^p*(E%zG{%_S^pV^qnJ**+3EDG42FNnChbb8t@}pk|a!6b_yA6Im*(cVx%ezp4 zl|61fCI-qev6!3`zcGX-w|V8ZFNG>_as3Way35dmJ^A+0kRAW=kWcCWx-0jIl6*g4 z`~HPzpSLk-tTL3Ni~OWC{`p}eu#;VC-}m7Nc>M`<*Sjh22IQe=z!}N?I{|m2uq{gu zLhtl7D(&5m=4;w!Zi9AJ_3J~yL!njaV0^%xi3d<%-aUEn{l${^n_D@htIf2oG-LWL zRD;dmZBSH)GRy{5`~Yd@aS#J0k!(UnZ?^rRU<0Mk4VH?;Z_KjS!M7Rlw1hM_iaJ3Y zeC#23KZNKFgfbM=mk5N)Y01f66Oy}eM@~Xb?xum9q@A4nEgw0lNIB^YIhj(q+s$%! zhUD%p$lcqPyMHE!B$Y?8$jb`J%iWQeSCj8gyMan+)#e9@vxqY-(Ws$ssiNi8gz7MW zzaG|!E^q38J*h?>!XAN$L zEBUuyzFTbd1b=2FtrvaX?sqQ0Et3dLO+Eyw+xw#*$^MAbM@2~d7|1QAgKFCo9fMO2$hOw=-^YhrpFg&>U0eVN`Iaz{DJ3j-4BQpQ zahOSup&^=$MO!kepi!*YE?^dt$_#;J8S$kB1aQzLz#KI4i&ny6Kz1}Gi5Lpa3TF4~ zLP|<`Q2K&KLpj_aDwjuo(ro1Qk}pyU`J;j* z^s%t)Lpqo-0at&IR<3~v9jxWp>m08)yDkQ&tJoFN|JLBoK6-UhL~pEG93_CIH>3d?@Z+12g*oOkRM{{00mcQl9&TEXYVkHqV>w77SSN z3cZ;dAcFn|YH*O=p-X*rD%-flhWSz&AKmoLdR)XCm8XruZh?nKPLV^L7frhrau;XY z;~^DYZzh#u&c2pT)gt#>gA+4KOL-l;VZk{_Pq6si0t}Hm|%j@Jvl7jH4G;( z6w>^G9;1R(LZk_1e+S{(zQ*GG#=Y5KeS3((M1%*73(9Wz65*c!@V^FGB+jV2zoMe< zNyE}d0dWrWN5k9*+1b}OFC(;CjS>7bP#9&UEIEff87V2|;G(M+PeBavwG8v6wPP z@hRP!+K+b6c*C|FswC5@!Go~lP{u=9*=PbOp|i$h?0hhf^PC8ft}F$aF7{@TQ!voV z7-i!a!ug6a5_k-xjoA#9&l4ehfL=w?0sH-+V}TmFC<>)8_yr7-;1CD}z*a)(E5!kV zQrTMxzZB>o)YQ51h%FJPK_l`m=fvaZWMG_v&b=@Km_`?gOe&TevZQfxXhab(tD;XX z9}gDWKsBdgX^r9$q+W&+FHRJx&H#qCxSTC3>x<#kW*3C4e+I|#*RDXv8-Salp%)33 zJpj!WawH9ud!(TUfL@6$hpgX!Mufj5q3M+c;bFM%DQWOwIQ5fV7n16D08p`%4-rG4 zGWy|AMUwyt0t7>lriNE=B|>00pNJ&4uep5!0z$o=$9@}P)!WS$G53QdsA6BnjX?OI z0OBGhIX|QuLN%!PL5a*HAdt2cdLp3RA~5z+^+$Px1YMP0rm!u^%1kd$R~L5ef6TS zYywH*C(tj>#;44N@q-IduC?#5!e#a`g78^!NWI|SjMDpLQ18q^;(diARQztgb=mKjzON|`-{-`wBzH$wT(4>CyZE>3lVy{=q5 zvIV{$xOd#CeBo8mdERUhxHka?wYNvGudEB{OR!&x=kHv9b&K{Eh(vcpcBBT5xi@VL zz@%u)LDba)k3893-Tf5zjNpPe&k@4W&M*usfQymq6IulNUih(#e_;9wm_HCHvbX&F z18U4m-y@#Li6cZaog*OhG=sVW3MG~Q3>9Fdt@cQj!o2kj@5e*I10pf zEzz$8L^RFL5`^6w@VnDvi5XCqi5oRl!radaaX zeZ6D}_pdNNrw3r@Da)0tN%xF`v0m37J6H3h0J(KAr~(5-z)UUSc2oe- zIJlO)iJFr7^*|qIusw$kTsRakW#I&(bZji3q@r>sYWLz(bXA~x)Sf|jg!Jpy!-2zxB-Wzhaq zD2!jB7k8mM2?_&O@`V9IQl{X|Jn&^nxTOx96B*)jXiwhM^cTDs^E*|AGm2=j&+6NY#dc|i~WFd9HGV$U1mv-s19r^od3x+!N*=%pOsv7|Xz zD~wbufOtPFW7@cRCc+u~vXvpSLn^XMC$h&QvM(iapgwYFCi25^}Hc3PLdoMlbxlLU3HV)J(E3Blf4>}eP)wiog`xzQ~ae<0(Db@ zJySwcQ^FfkB4<;gPg1aqsd3V&3A(9Co~bFRsc8+V8MCQbC#g8bv|Q=5Jl(Vc&$Ob{ zw6_gurL$?}CutRo>6Oyy)w=1mp6T_e>5UEP&9mvPC+T>`j1K9HF5QeC&y2p*jDd!X zq1lWNCmAD*nPbwK6S|pGo|!YLnR5-93$vM@PA-!bvsNyov(|L8);+T}Q?tG`WNpu8 z?Ve=qF=ih~XCLWi|Mbj0PR%}T$Ud9R{&SKIV8Vf9a1cElp%)ICh9hpokI`-IyyA{zlO2&HVwmLWPr=obzvGSFqTNC#wVxz2K^Ia1AEF6TN&2 z<9x7RK1eK|Bqv`}6>d0}ZzNNot6K2brr^0=0r_&i;Wr24fV>N3XM?vl9Q3fts<}6F z2ps3)U1YFaA%&vAqBAJL?Nx&Mm)OX46KR*CR~KNyN8$0&s_;E(15k7K3x%Mijtv2i_9S5RYSXpONhcse>fLUHRh9}0wJi<$RtEKz{|u6xKjUZA=&S@q7PnI5LZdezo(qgD)bEy-~Luf)?^hj_dZguN9LDq%}2U%r|8HZon}&=H711({C*BZY)Y~eB0DmIv5>fOX-IT z(#VAv{%qXv@G~@R;x7g-T6u@Oi6~=k2GGI(x-CO5cV{71fC3;DKyukWAcq0^2LuKM zhlGZOM?^+N$6#aQ;u8{+l2cOC(lau%vT-@N@!&TFg+;}0OL81a-&MS?tSSEGV#LVv!w&#BV&&S2PNU9IWo1Pa7oqq3Q1lrV)C zCLetQ&9U~2kGWv5X=*qOG<~h6T*Q*Y8sYnz=Iz=BB3tEt+Sx_upXO%I;PKa*s(fGA zJ;SL*GqN9jUJjIb-9wRZ9ewBH5y8IpXUrk62#bO66fOCaN^UL%z!mwH18KCZmV+29 zik5>}+&7m)5W##apO$T=NZ z7qQF!;Y)#+CIPpSWLR=Mnp|EnL0wYzOQO!0g=`Y{y-G!tzO(ge!u<-wda6}H@p_tF z?br2m#~%KT4A&{^jZBaA;*BhypIZgDDP9x_8X4fLRkA$z9|ZM;DS5pIWJ^aa_=($)$KeKFOh!s|`_aHA10E z3JhF;5<0O1;|lU9+$RdsH7#^0b+jIwxr@pSgUV4&kFRxnIuEa8yxg4V?gKcm`Rl~# zBhLtOmXoSG8v&15Xu@pa_2Bj#I0VeNwMpS(1uav|(UT8A4r^62KlMQezLQHynlv`& z9D475TkgJ?-#&r+%aS_ippA-QVBwMAy&uy69h=?f3D%J3AP<_5JK{MCtnZ z(S-Jk^B*&o@6LZNy!?LtYboUVpX0UU7k^GR3*Y@Y-LCuo=l5Rk^^3Ek=@%F0#~bf1 z{+#{#en}pY;s8X-co5Sb2C9Yw(f8pAW%m5wkvIsiayzl!o;%=ndPizTSJM`!N{OJOq(SpDfb>HdHN-gTB8{QRW~P8=1$& ztJ1HkcMz8{l*e_qQHYD`{s`zGFIC2L*O|jaw2yY}*PyM} zVfqqjp;Rwy;JKyb0|99e0jd=C&ao1mDn0c+Xi-q9QD?Zi4lgn< zn-yfmpd&vvQ|GuUnJ3|hMm0WO`?fgvYdP?;K88dw6xg}c`H$Ae1l-TcxfQs#N*G09 zAPzg(SN-sY#DMd>{>`_UstARgOjj;(f`*qD@17~(!uJ11^U?o{u%w%Lu0iO)gY?gt zZC?r?(UL%-AeuDBYSe^iV+%%EcXowWo1!CuK9CA!dER?7RIbqXn`j=E(H~d}DqWKi z%{bQFSSmuYksJknFS;%{Hi!`?2y{~k8^Okt&=Uip9a%*(6aL6CkucHBH`Rl{Ly?T9 zl$2bAsqZHefuw){C70#|Zj=L`Ydexxh5^Zv!kTw*iQR_V5sTyS;WO zZ|PnK>8m++gz4mI zMYuvef%EXFf17$H=OEC(g1*ax%0G^?{>GOO|HnZ@`#*5OS^sv6IQ0Nw$PPy4Kowzxa>2OTI0W*r2<^gV3>63Tqu3}#q1Or z=`u3*4Gm)-8kl!(#TZGvxWh8C5m|<6u#iT zwNrOBi2g5zUzH@J;&SXmnwvoZ)gz|*I(-?2JOu$PATz9$G8YXlDD?-{L`;HRo~FwP zE9j~oB>Qjh%jp&H-|nrELVu-3b9-vmj67r;c9jI&k3U|#L{qC z1B{c4U{i0b{yJXtq{MKzv1V(!!gMf8y{UHRzYlo1;#jUYmj8+<>ELR-M702>VzB*kYmMg?^brrh03jKYz@PGI!^uLE#{>IyX6Uba4 zmMg?^g;=f-%hgqAxT)^@XZ)3pI{I+iOP%axAhO2=}Q$MO#ZNdHr;HvgY;EdMJlE%-9(2?n&h zjMlpV{2d7^2)W15f9;tkW+}#XbPz%~i~gl&p15&op}(r|Q6%dtVpi4aqS4EQ6=zL( zbii04`psp)UCrCc%cv)t#etfV>C1$baMlO4rL$F5<8Pl0{v~0>adjZ`L0$P`(`CZS z;$Yoh{K{YwHnsl)2`hL{61)`Vrr#X7!JRqLU{xz-wyzE zVt@pJtof=#kwk$TXf|Smc&{e`rKo|w8owEltPEE@I)w0CX(WUg4*L?i)3TA_J;4~d zl>AvEIgvA7f&d_SOki34pwJf?XDsoAIE?*~CXOA;uk{q!dgEg{XVY%7`pt zv~)g*U{4Ir_40ISDa23HBny=$pn(H&kn^G4KPRT)l1kTxdF^gs!`Qr+9gM!3)~Yn`gDYWRb=qSK7T>h$TWnPe{#R zh*`qhBY+T15J50Au~{hh5OIPEidhiT-O0ouu|x1-zj&mU_;|^#7Dh~|>2F9s9Z+y64}D+=kE_i> z4T6wWsBDB7kc(_5((-{15Pr?El#?c2+bG-<_{G-tJ&lhi!06!OEB6=#U*Vccbko_7 z#4gSNoB4>&=I11FwFaCgR>CrL>JCR&4JwF6!XA}&u z1e!+p-l`tCtb~Oh`H3PsLj4c|j{VUuuD=Rn?It5q=GG3#0c;UG+bu@w|FRlkBVy+- zk3S z6A{s$ae;DBALWSMc@gN%7jrs;&b~7lgZMR=)2!9~mhYbB_A zT-3{olhxT!B;ZL(Ag`En;XZ0DbJAx@k;V05h}&X2d+l~Xe_%B`ShXP=tfV>?mp1WyXDH`xb|r6Uo&dwn|emY;u1YqSxbD$rFX0L+p#3Dc~Q9Vl0^5zuGN%|6#D zdM6n+{g}wLF5#zrjJtlJiy2Y%-X;}B$kfi6N`5Ap!>lN=ET)yN>3YY&LW^YrrGP4h%`pkhAyLwV zlGJ;%Va_Ax$dsI-C`!t^Ipkc8B&EYhrBZM8*16ukeSg2-=lAF5@9TEE{aoK2>RqY&Y{0y>yNUD{$X>>|JHV-n99xkGC^joJ!M+^!yO*A{LxSv9Bh9FJ=` ze9XlGnV`AH{@;)LmHeO2PNsC`{e7UB^g9y1bNJ%nFKx{()lXHFE1QG8ldE3ZM$r!~ zj&bZFpkUT9At&p)a*16JN3pk+a=uI)LEgTsUHjDaz}=Mc6MK&RMZ6R1an#Qz(Cx5B zS})Zz<+grcGo~@S`8;%I&GVNsCjL{L`62w;fAvp{@<;AvrGL3|ZZWiOn-~3mxIWeY z1KCmge>^Mg6-aLFl0E@S)ih+Jy>1}u|MLjGolE@xoqC;eNdK>pakU}NO8A0utl|co zogZhS!sL%r@2-vwJF@>glTB~6nmnwRezbbL38mU!t&ON&`Ctv4X zzESC>6~&2R1V5M7$;R0l8Qxa5+Bqmo|2zU#etmYgW`3Wy7glR+_#$4Fu`EJd#k)O~ zi@U+i;(xrKzkzwHzB)u*_x$V`K_=_V9DTb1m5={yD0-asDM(|Z+#8!G1R*za@JeUdFcqZok4 zS1T4N7}xA5qHi!n_G!2?p6g66RiGg)2u5@<#n{yl=doB*PFo$ol-R5yjJQ9VV?D|? ztfn*bi`$tlx$0|4t|(8(es_bF1g6f}`ZDnwmL;K6#5i|mfAOX2+5K$&D96>ePOhZY z0p>|9U)g$ihuF|r^eNWd{nlZX5AJ1{MG-R}SJHe;r*zJ(2buiipz8(>tCDdK>mOHL zZQj>Ls532T=2kD7t=zPKZ~%u=-gR8otq30<)j~LXis(17{_cHmBVhkrN4*|Qzlzh0 zD{9-YgYa$uW_Xr8hAAR_a<@wKwKg)w7$o*8>esg^Y)Op!*U45j$$CN!{###0F|A?u z+OH>cO~c^tsZMO`nt!LUp%&tPzvD_O4+A$&F3YSNLsb-`(1TYQCWAWH9&c@s`^>IfFEosR1ht5?%?9i2hra5xlkj0aCbs{wEVoO{_xoeH=E+KIgR z9tLB=G7G!DW1|W-*Dd1qw}uP1WI#F_I>dy@bltVI>*h0&hy(^{qWxw%nXDAUTSs^c z2%*t2X_)U}I*+9ef{Hrq>qL1xj>~v9dQW%t%KHCShqvW_Q|7_(WQfLLDQrU_cI)gN zB;1OJ4p}_@N&q#2QFtG(^RNka>mh3h2_t=io8{)Uz+fL8Oh-wwVT_s;|DA*U>3>dA z38G&pcGhn-SrRU8L;p+ES$;%B`HINCZ0+pnS z>h@=m8@Ll0d6pS_2`I3Hf}o~^BM-46KN0P48eV;vXc-hco1$Wl5#n`6`33|Zk=t~U zk^^c%pPaCUa$2G*6xgx8!mJSPD|`q{H(g)71La0xs+1F97?CK$jlB?BQUTKrxrX#0 zFjewJunn+}r(NiJyPVq%_02z`oZZq@jx@cZ2eU=4OXpF!6@z+MeD?2-E99Na^F;1T zlq*|L8<~l!u3+jL&!b$t`Wt6{?Ezj>5D$+@F^O&h&Bv51lWy;7He285 zur$+-jKe0dnQHFoJ+2n@C{Wz2uB>;@&8to+nwPHgmIK$5g<~Am`cW{5!rx@6a`8&~ za*iD338WvT*$o<0!~*42d;mrqFwp9|@!XM_WkrW=(3=cUhUuv$P^Dx}Dg?3yW8hD< zGZ``a??b1YL{1goB&nU znSsmw$6o+QLK=Cpg6YM(;>K+BP^?9k)&`#}C`pDHCIMhb59qxN zQB5W<082iisI z$D6#5(5gxmA+Qoj8c&M*Q_l2=Biu|z>FLZ&yt2owtU-CDCfHYUICSJ|##SOPypcYt z^Miv8sxWhh_K96vy~yr?5sbr>Y$u!?kswXip5lRaOYJ99K11}emOj=&-}1q_W!(_z zVEPCCn7+(Y>#ZE+4lsb@O)zV}ScDfwq`EbRYPA2vC3~)(@+mrtc$&2BhVp*`)%~@#}%=KIMIl+2d+x>?=J%ezvz2~5(t(i<@Y?afL+AWk?|YT zZ{|v1ws!yCjlFY8{gr$VKZU21y>%2&w5tRym z{Jp?Xa)pK~F#;C6nr^TPY_YD--C4dSW?>v?nf@QNR8xt2E-G8{u9H~!feoJkGB4Aa zY^ovLp%Bxq4)l4)A5dJf9qB0{_MdI?}Ft{vGF*EmUoU z4&~`$pGJm4rZ|awG*tJd!ASd$P;r^r4{6UUA#RzBvhA$9P~DVb+KbDkMRntHm)nOK z8*vo3g=vx15HX^MY!FWFMjex8q~b4F|)EI>*qSx)Q-yhWgB zQ{9TlRmxDHwjb_#L(l&EUpwTMUlwa@)F+Veu?jZ%W8YDBAZWMteca`ORxd8=BpQGM zJ4Qbta6;a`4zjkx$eQZ+$4k0a5~TvvAn6EYFz3bxgvgHNHz)NTI~kH&uq|0@V-ZrT zU-BSy(&x&T!)J_eboNP>ECxf_dJ}{XM5AnO17Q?|zRc<3W`$jJoGly04WY6+f&y72ABaO5%TR>lqhz9nf+QFxZ^jNWr zr9UiLo&>D}6L^m4U+^-)Rz!#6{65^hQe?KRAisgOi>y${7P9;knqKV2-bM{Y;=T;0 zREz&25q#8Up(p7I7dHT@bOmtsSW66v4H51Mu@!xCFUVC=zu*1RV)kWypI5#aU(mu zLms3gBzqwNc$p2riV-f~K4K39Z{#5!XzXzY(j0s;6d|a2b$#MmRRtmoLf8Ir>-r*A$c-n#)yqGlkN!$%48$bu}PsuVvhMfhrYm{4^4KBUx0< zffq_6GsPQu@_d*Kq1lD%-m8!-r#bLYCBP2Ic~ni)Ue+Qgk_N*mh(d{<2vA_S?5D_6 z4Pm*#rOYcM3O?${7}<6ZM!zB22kih4a*89>kk}E0l`q*QkWDIUeR>0vbm@@t``l5%9o;eJTR4udN;T&4IfY~MYIUO zED9NH2TxMWbJ$4lK-@YBQXm1hz~Je?4v`#j==x5tQom{$DxMwB;+5vfk@g&9;v=st z3H${J&y*uK(g6<~YQ^BMb#fY#g3M&YJ_=ZY6a>o9;k{bfIshKSt@0oX5OR1B*NeqN z{CD1C7q==>0=~E(CJs`Gw95c>9XS4f5UZejH!EP3b<8bfB?I^OcgDE8lV23eHii=@ zO14>|VOao<3z3vL#|e`SyPdXu@oZo710-;VEe0oBuQ*y$S+eYH%bL!bXJN@)P$L0C zBbOx8IZYph^@+H2!co;fnv$)MC9eLh@P}jP9glNFY8n1|^gocDRGQXw3FDKHm`WRK zuA{D=nebz2KPEZ=2$F3E>klc`fBm%zUu7B&!GT*hXlMxaa+Zi8Ph1jDN z9MOW`yGG0SP}+d%pj*s94_j+g}pCY;ObYz%#3&@6r?Yz9} zM~<)XL^}=Gf({CQn`0hzXl;M%4hLkMY>U#nCb}}Xh~p*zO)SLj1J=qA59&SUBd_Ro(CsMj1ZjA+ z6tJHYSlD*m^ao-nEl3+Amkk?t0psNDw$gy|Y~am#C!~KMl9v+?AS1aDP!7E|?m&?t zVgOJIgKhoh(&BX0X))}YAZPx{)%LNlFEdc-VCsjp`X&nw;0GZ6IBp}S5qB>{a@lT* zy0t$%!WNVF^y!1|-}UwpkUXR2xSnI3*2q8}8zl}5;&2je;C~%Cn|S&8CxMDM+RqGED;lrH330z@liJy*zr^9tfheF2i9KWM?q~(KB*nogEO( zJ3W;n_0b}d|O^)B&R~_%6b^ zKp#@KHi@&96Sp-~!_n~qZM;69We9f<95M}P9QE7?AQw(uKM)WI z4F_Vx2%t*!tafl5$LZqk(sgTbN9}OPo>E7>n=!zX(+7S;uA_rn*|0ZVJ-avbUVeM{ zZq8O)4E*0fSR@M+6($9!6405KD{5)NF2k)hBdnTX9^^wD zI+8VaJIwXECM$$|jk%PfFBxcmNBJ5qs^Ig=A1SRi@Z9nfa*NC20yZN)2W1H*uZZ!AL&Hoh=i#Wou zz2b!7UbJq?c-L(+#>8$XvKyY4p;*(o zjJ%v{R{fzR}g(haA>99=P*wm@OwZ z%^8^%*oqN$M~Xpx9xy&W`hB9kHcpnzIsF;|BH1(1O530vW4HVPI|+JbWhAoLa1TYw?d8RN?es+&q;ECR*b@FXPrH^A* z`eAaY<;floP7gTwEdS}2#At-tx0pD`J(&-0u~@h723!L$!5;E?$lZ3V2GZ|@ym`9o z4?SnQZco^D0e1jm9+vB$wX# z;adep zvYW1dfE$^;WLoArFFkuLhGZ2FVwl zD&gPinhj!O6}XFi`E5PTLcY?k|Gq=R znN2}#B7Khw+={#u~%|S{ylr-9-a!oS>$Ejw(li-zL)*=y<*4rZEJchtUGE9kR^HV0|RxI zzo^YaSkl{|E?Bq207eH_1Aj1>=M%_Zl)_srJ8^F^zNjbkTZiKO=t#{q{Xc|A-nzyl zF+vGuU`ay=FnH$apTV_ZOIN;IqhgTwUju9U+G}wLdjRy1CwF`7JZO0qkN=J7_^Aas zbZf``xkJBuN55Wr*Ad(GRj&wKJ_H z^6$*C6q&rWf+{d*_f|C{@jNt z+*!Ehgv3cfm{irDku9f3nh(#i&%#Mno-bU86HQ=_Bu6xW(Q=v9RkFp*wOxB2Ruv&c zi+GWFgi|lZFt#U!;W@OMA`#8|Vx$w|)DRC;B-I6720q#e=Di)mr4iVhVOma^_@tEW zS@+*c$FuC2q_w!_vJ%ms6jU&(U&+Ri$i>>WO6oE|g79pH`XcFtnNT7Tpz@+pN_gu2 z?@$P{C1Rwx?h+2K-{>_k0?rT7B{$Us&K@p9T;F7WD2c{1fD%^SvT3cdJ4o7uBoWXm zGXVJ1sqQ8NsP5@vB@h8bVu|SsFhk#>PgYwDxmt3l&D6J2n%3I~1*)V4X?ne+y-C0x zEvXVVngLn`an339Jjd;TOR7Bk2Q-|`LY!Llq(b1wC0Zq)N`sqiU?dn20ub$G9J@D7^%r`gr~Mx$R<#6?EZldO^hzH_}6za6~m&i(z#xUw~TrVoYTZ z#{ewnbp9DS}=1>=$F7mB=RPJw_R%0 zQD{tcliK^$=o>%V!Zt`KSLt^T-cb4Xv51AB^zPa_-mylKP6EGZA+BH*Vj60&`B@_ajSMa6MS`9Q1F zo2#!j6p~Gm9$W#Mlm*x5?#s}$FV{@%1}W2Zi5euH##=T_hvi!wSCXymV%T^h8NJu8 z53&~2WE|2bHp4P@DaEGHp1*7dYF43RRhzq*3Om$)O7`rOcZ&R!#CnOI)U zbJ;${NN%vYj4rgB%`&v$3N&HXfEn>_RHuNs^h|Z)VA-eSFpd*?T^7#vrP4tvvaPnFDeed?MxQ4rvIAn`2ZvxAsiV(B^>6j zvc=FDAK`N#6z~X?piXiWEOaCC*BsL{O>T)I;pyz0V(+*&Kw1vMkN`pNRxy5@N$8{T z_y))K4iHM@cdxfC8a9kFw5R0!s5c|J@t2Ku$^ay(-2h46>%R5lSZ)Fq1fm-@Qq9=kiT%aNAJx3A#zCyd?O7%?3I2bEe*@=CUxmcjvb3=2W zpt?!;kgbS*djdyzj6-_y8NDnK`+k&c{#^Zr?hq5EwuBDcW{oI0XjTG z3(JJ!XDN}p&q5-8Z_$G0mEjR1_W+6Ma|oI8e8?FN{h<(j=I8)UxkO$6$jlu8CxdE; z%&kBo1DgbZeA;8UzC`t0?7#aMYl^xkA|Fl9$8Wom4+v&zAO`vPZs(p*7-e>f~Ccv83|MgEA+#)!B@=A(&6_dnuG70!Wt z+iC=^xo#*UpOCTguw>Wcm2k}Em6M8=rfjISFG9Bd=M#Cv=GFkm7qd$hGrCH53+%4R z$%)}z1%pM$6vFgq0IB|}NzUN)Pb2;R62kkIY(;NB2-#UNZ{lH93TC&9-d@o1==EHG z%6O*hnTpkS`P2XIet&54l~$X(WofVL<45l}brdJhH=8cmO)VCXdOWG`%my7Esy#cp zSFpuc-?)EIJTMFqZRw7>bl%1ItT5^Q?`6fyk0*7h*V;{m#V|5+^u>q*3^k8HuJB*q zTI=t+yCWR=bfRx45r9u8?ltA<286JoFdwty$nPIs(w|rjjI!K%Q7EWKdV8_&*yW&&O_oIhYDHgJisSV@f~^Fkii=w!&%{q+k0)ZKTFkMEn{#Nx zm&V2LIssM6{|bP1DCHj_fu>*0`TJK-rb9_YC#yk7`zgN8I7C?jnHQC>$zJqR!%L!y zN;i!1UjgZ(b`DuFl}$u}4W+5QMf^zGXkTIZ6jg5lDbWGe|Fq*>$EW+wRhyu4KF~oD z8@=ELt|vmQVH+Q>5TgB{Iepu%WOodH1U}3a!xv%ie-_)Z|8x#zL$!H&H~I15s;b9M z;fWBRRm$iH&Dz+>%B#M@?vgE}RByHeZ33maUF}xH7ZV|NGE)l6;lRxy+BXODCzZvL zLY-qy&?rUaCXLUJ&8TknTYi~6=vVlo3?39qlNIVdPZ^Y<4A2mwRfm@H2sDZ*!p8^_ z(+sOj6kHcfqR-;%j-SgTS8iCyx3N>SpqIDRi2_P)fZVn<=XdIl7F0LaLA--Y^T?;7 za`RdTh7PonaX-rQC~5|=uCbL^xxK45P-hG}?b0OmRg#PB`yC;t)PZ5UD8=)}b!{fi zD|E#zLgn)ciZ$)bl(=C_xq-_5Z2DocdXJ=7)TKS!@pWaCoo-BCsVt{&aZD&-93COQ z6O)1zj@9K2k9#zQTiJArYI>aWmW=-HQ`P%cy~m$cZ9ux{*HAO5M3kvNL!BAHJ9~Ci zH;>=ct3MY->~S#80OGz~{?i7;DN^#aV;B;KUhAn{el@LY)2HHWod( zKI%0<>c01LkiSP$Dp?fH-qbmln>Q&G>)_Mvjng^l!dDPgm1%^@ zpNovVrTXM3g+`eJ)Ks~sc$6d(=&;jmxR3{v>FOz}@uzm&QmuTp!gHDH65)F7R>_xJ zD%zUFXMTz%SIxKDGdvmF|Hjplc;K4DloaJGQc%O!$Q=HHxhW;rtlU?7#yUNN&T+k9 z(^(=?=jkiQU^DikO2$#kE11GM9oK4~3QEz=L4{jy?nCT=u+y0J&UJ0!w|Zken?6CS ziNnc?XWqC8deX1Notm2Xd83?{ejtI5Fx9YlIq})lWNx74(3EJ1_UV0fVB}HCjP2Z+83wO99K0Y`G;g)P%oOBGbFjJ! zwF@S#itBr-L8j&gTX{B}DR@CQ)q9b%JvMv$?X%T#T0q^-Y98-HP)o67dqTF^IDOl2 z^}V0pZfMsNzj=_Wn;tYNTCLfRK#jSlUO~}0vDuU63g%7gByvUgON|+~K-=vzd$&x@ zjLoV#x~dNPNv8a2-OHB0qh&!zcEngGk6)3gh^bx~~ zHZK7_=;3{6{9VGDG z&#GuAX!3}aWb7|@?-7knF$(KZr`kr#54W^D>nViiU<6jGc|k0#x?bGBP>t?uU%zeo zY-MSe_#}^GxiWc~^10)0Q>c#L#pZKVnHvE`UVO(3THTq7dhy^IheZ~rHgi62UQAg@YGZ6 z!>b+>bf13@ey*k0H5AgR9yFhI<%-f(o7rsf1QA^4Wg4Kf+1@XxOa^T>OAQrpZ*Q^Cs zKpxa4Ncm`|uPRkA_qo852w5+EN^PehBugv&^l}F;&Opo>+r%#OnvRTNNKO~A1m|W+ ziUKIn*x#-_{lj>SsQ4To-?Bit$Y;`T$3TPEu zN#*zJ-5-e-`p^zf!8w-V$YIsbdcdak=7vXDhdX)~JW)C~F@*g5Lq)3A?N*w*T#Y;4 zg~eK&#;!NB0E#u&Pb%TBCR9@;s>U*VN>Fg%73GnM~~nH;0Qowg_YVy@TGj#lF8$ z4{nmriw{bUALn*-X~g%v`DFvJhtAon7v(W^%|1lt@yyXi`+TPPP{2({7Vp+|5EU#* z0yM0g)lNyfzII=CKs-_{jNkis>hNe4_^Q3wtgaR(awFx_05 zu9@cR^Vx*yqhkX+`@2mFEd)K7+>4yX4*`i=39nOiyOu|x7Aw%$hv_Em1jnj%8)sIx z3I$-MxyJ=MR8p3LPYS6tL6^Ii8Be^`;!fG{M12hhO@A`acSqr}Jo^I&9BI4<jo4@qwh=`?+scq#Iov3*UyO@dIu$KiWY!wT$zzCt)I z7y9@XkcO5j0lMyMaeI0dZZyA)V^D1E5W4h4Nm z0nFsVlX(I?vDs6O`%2U%yJ-c-e>AKM6$>I??5({Tou$0PTmeA@ty>Z|hTV80ykK!% z(fLK<$&cl+7{$Z;nx#Hk6+gch;(kUkgQptIRq))MwOd{nF*hdME%= z?2esxlqU|D*vd~oiW1PohcgTHv%lc!WzB2zaoGv0w+d?8XDVJ{pdc-Lm!24+%Pa1& zy>}e#xA`BL^MNBLn=hB1j;qF{;p=W&K89$y4cU-eZrKXX_@$xh1omPhD6p=aC$Mo_ zNNT+J@FVfUqYK5E5JvVT_e*`LNRhpjoszu1-W%_YoEYplEI<%!V>WVIf;#A0HgH5r zr8OLZ(Nj?lGLQXaFjivf^MT{H4@;u3-vaX`d$IZHu*CXGrz|YV0^_UFZiU=!e1JzP_Cfug zIG#Th?b?Trf71O2$t}9Amn2>bG>{H?xogBPO|ujt3)Ig$zcMh8M13N)au$Tn11<-h zEfNrcjum1*bmq3#75uvUdM^3ZE|hB3eU|S^`?FN9*+Ef7+VJ2dv7$cmZ4QWAG}aR$ z_@y38atnJofYp0=Ww!xY;D%eKa>Yg|FI;|}x@S`LC#l=(tW3*+B~1Qz9a0@4b_#;w zW$DVSC5nIKKTl@@64fomAm-&f?g@aYFUs^c)}hZI)L*19BPl363i&piSfglmSy?3_x&s@RrY1~7Ks{A5+an0;fJz1PQpjHI{MjcQL+O{Z%g z`LVwmR=rCFa_a0ZGoPsb$HpbM?|3W%U5{J#Ln5Q_1 zOrXiYAo&_ko8RA`_O@MndZDDXGsk4P^wCT6&99KV9EFy7hYRkog*}yw}j^_pL*R-jC4!psWjAC!cUmpiSxbl%?ikP z60=j-hEsn-x#&!(_!#UCCxpAWL@u_!Bt*)j-3KWtMMauSmI+MDffbD)JnG_PJYiFo zpi9?Yax#@YnEYe79iFe5>Y8D zer+o$zU2Jop23&t+U*c*&J?z{& zUWqd;DY2O3B`l=THuGv8-{5mP#;9VjTQPGevYH2sx{TC!q{$p8ZXk(^v&oKQDI~D_ zT`&Nms{Ws28%I!BNffJ_n+{m>`&-?P&?zYpOjz2fI|vGVlMXq^g#Q5c0=;$sW#@yr)d7b{XBrj8YzG@NIEdIc0Fc= zld>lhrnFE5Y~_JR?K%u>zmXk~lriLb{p_5J=g0Iw7Oh!<^&oy%QTRKF0^DBpJiuKe@_lS$@Rv+F^dlurh^eW-#*>T?}1>of3)@~`u&|SH4$N3 zA<(FI5?4x2Da9D*LOuuqLn$BOGKVI~^m?fndOnx^%u5D+Obvp#C&n3AkE0kN3*&W( z+&;$Zo}l2m17~3$wZh0v_GkI32;M=bPE;~ zK5B-1BP+2ee&~jBmq9JyaPFOTeNV`Phc5TkbS<8L>XvJpdmV4yn=W!F zMER3vUtj{~n9DH9^q;3bewOpB>Y$Jp8`B2}DO=Vk zKzf+5=%1CH#Xz}D9#Ydu>`E!ylzjV2ELi~}Wz$2=_U7qoOTVyYcXN-70FhjQ3XTp; z-&yqul$dM9ubqPzrf}v?m@~D{yTU-C6j_#>fCUii5nhmz6cd2>4GvM;Y{)IUn-Ff_ z0rFn*Am2Z`Z$L|g;)w7D7zZM(k)2Zh9+_a(G$919^KaVQgl`^N#Z{wUM-#(yt!D`%`)bYe z!WB0XYl86~P+4}io*jBDo52qM1ai(2uWZ$0x?$b(|KrhD!_we9fAkHT)LVwDSk z4?6_cc=LO+Gx7IAo%AU9lzic%j&;0x*v7!mEdf0qeq9Yof(ne;&w-Esm841Fsy436H*wdK5Gwfh^#7N zL2g3-LeR73JRkXVR_ruBpg9xw8{HM9U^&T9uM%o~&-i^^5enmUR>-wY)&)E&d_CyS z;zK-$x93Yed`st#bwD)qM!QdhJ4qBQA28}G2Z3MX5%qPS=cQPbX7SB=?nqrVrBvq( zY+cLPI;}hEQ)7QA(aD$HNSrrE2f(qww~ zW+N)6v(Q@k27gAw>K{x|GdH#f4fRIrEO$?bxXV(86;(zz%(%M9hT{^H6Eggb|3lBN z-2bQD>xlx=au4F1YyVMvJTg!?kQR^>kwBcPD|4FGG%Zk3gW9G z#LbpysNs4lb-y;jAszf6_j|kdyHl9rojryn>l81*_P*;&=hpHsv zVX#U}d|F9xp7Ti+A)13!^oW4S=={|+b=%lQEIT8XnSJn6P37|awCEYm>OIax-|xX< zC{-^Sw8{QLky1Nnj+T^+`~{%S;L5%;g5-^n9GqcTaP*n(6s4cLZ_Cs77;t!}RW) ziOKag`;XIGJog90YkHA_*7kGUk?Cg$M$;N$d8hj*r~2x3LaN?6n(W?O$LMxh5C-B2 z1lhiWI?fX?olc1MtPJ4r_XNG`ikM6*Gou#xyrT#gbb;|GWbNer^yTNfMgnRb-xc{6 zRX=->H9EGx3~sCVr0H3n6Q7XvBYNldr;;4w+~qy!_CCWZ;f-L}&D8uwIvrA-(R(DI z@BzQ!qnVN0l>S}wkgE`F_bkzpvc}^DAWG=JD6M@8fJA!y#9M`zJXXqs2Qz%^#PSVs z)SkWjPZnfWT?I(pXd@XwBGs4Ek0%M$7Nn1YV590KE>&JLkEUBq11vV<+~udXFtRape$^S6-}$orpY?SXjsE3~ z9x)cX2H>=A`)bebIP!MXE6rf@RFQPMGRE;B9#*ow6_INms+M1*UUGWo{1joOZsbjX zn)?ps!E6~Vp!aN_5~5MN-Q4IKFYL}BA%y4$$--)t1q z9_lj`zjoH`n& z?LXa1i)&Kv?7+C>nFA%N5$;jd6T(-l3)*a!PyG)zP@?9}G zR4IIL)kTMD2+=;ovAa$1&qhmkh~B96#f9EIIO6i;z;XoFIfly^q3Pyl+zHdI_$FMgau`@t|g|BEoo>o9^pWkd@WcOHJxtg0au%tvt|Kzo-57 zJ)`c>mSis`?m%`SXFAy$T|VnK`xsf7{WvyPVQYsfP4|@}mG0w|86gc#y59hGW)_WOrw)=N;vuccx~aQt0wNdMzAXy?M&_QkwPAt3_V22x0Yyb%UdcIrjJxlyCF#^&VadS5Pf0b+T@cvEmZO-E zh(|@~r(xk{ti8n_L;VMZ*Z%PfsHtPb zn%?%nQrjE_BjTZn{k6VcWv9c}*C|1l=(@_cznq6)$x`_2JzYJb(IW2!LXEsN07}H}`qg#_l(A(%Fnt&fI27(2 z%;ZkrI3r`EU@wFYy7tXu>F}%8Jmp!!m3vk6zZD{?opFT#7>5||=3SjD@$UwL9glZ6 zHi+Sjz@0)Lz(qZ(fuExLFQ_0Fx!`Ln9r z;EWZ5GiSc50KeyXRA^S^%=wkxmc}jCPWnubUE@t*n-n7P~iB?W|#m!VLq+?GuogYOYBN+n|(-#d#%s zajonwS$h;XsVS<>r94A-g4#rs+Gtxzv-Hql8GUSsm0FQgq71_embZayUuA8|_BxxTZ)afW zu2c9i))Pj;28q*3F-@jSxV^pA@Z^WXJ72unQ#>Uy=jBQ0!6z@5Ot9@w(A&E2cwD3t zHC$-;55HOB?*F_KL*lNz;YXrefmNNREu6>PlKiv#bOryRq%#G-Us|q|i*)0Hd znw4mY9pepnhdGiqmtLZiDQDDOrF~XfLnt+K%{9Z3l-5{S1}vIv_FK3jaGYR1yRY^{ z%g!IKDm|2e^9W6Q&=q20=~+99)ip(ZO-2Suq?0SOW==YmeM833ToerRo|6=c6Xi+@ zONvEe6Z!~i-Oua^j198e#WS4-doB>6=jjU6)Dl#Y(}YsTou6q3A~Hu6{X6Kqgm-}J zbOLG%QWT0RcCxkWf2BVmQzN*5I;ma5*XA1>E4|%-V$X^6$0e}Zp>dyGhaEX>m?A zH#?N(c+EC@j%+z{Td>z>O|@d7;PHykOyjM`dOTTkEuPh+taCGS9!P z;8y5hWy;w%^B;EN33FQ2#Tqkd^F2Mr^Sy_#cGtjzex`FA_doY6O%48<+nNU}VK(t2AN3)X@0t*oig~E~K%YX|aV<2~aCNOYJXPgUu8y)F z`o$KIzXqc<+SlD6x#~22Tps``i^S@ayz}>!u!iTY^+rc<%Ur4T7{^+f;Ujl*M8n+P zgU$>hTVO)wEZ%{p11S5zWk_BZj9~ys1&Qb}ipU_^0H_K2v1Oy9+qPZOLO@)eb8m8K z;#tE!aRYIVc+1=o>X)l2&*&aV0I&=4$q<5vfq(BP0L`06f60_tlksJ|+D7X?_ih;0 zNOhd)8;lPNhg}{emi~I(Xk+)o`X|%BB;I7p-OPu}HHpB+tDj!b3f9~bC}GV62@^r! z`Mb9%sz)ndx-O(4XoGz#@7{gs)`Kr6wyA6KA8v8K(Ifqr*67IPq9%tQjwH1@Xru!5 zUa!~OY0gv)bI1_~=C-fogvAj!JVC|JHHx^xVf6rNDbp}yWS!gHqt8XN{x)kzcj=F9 z)W0_y=nr3GWiIzhk@^Dc)5TnZ4%uwO0*^UYt!EoPU7pfcq4sX$AIR?1Glr7~*tet-4s zC0j$MWg_)hj@pecF@AADw=QWGo|^o9(&K7Ka8dfX1{BqP?LPdY9)_CrsT$!*4K*}% zau1pfHfyInI(E``2B{iq(QVd==`a(6krFn35u`dycDheG!l zO~w*AxJYp;vl3eGKE+XUfcr_}!2O&r-n8NwL&tm^11{=E)K>`m4IQ^ zZvzd6?yJ#B4q?~@9_mjVb0t*XrwbwxR9fiyH9)`hYZt8Dq%787RW(gh3S2bcgf<5% z>BvJpHw%{|+he4=>{hjAURzAgyE*4^T}C=_n;e?T6GGUp_kytpJtJC$hxAjH9*p|R zNSs(d`UtxFgX*#0^QBYa8%9@TaHGc9VPm7{-|wN;&!Yiy2rFlm*qLmtXXVsrg&C%W-WYTtHXF(?!7 z#N$7Rybbt^cXoY4+oDJS$c1z^-rRF(T9>yeWZqnJx@xjH9!Cg}d~N&pdMzs9z>TjqTL1^odT?vR=6rohnD1jk+rK-`(L*Pyx--t{@rz z*_*BT9P3ek&^?D+hokbKN_+G6>ORb~{|fZHr?-9X?OS6;a2H2C^?SZqYPTo}9l%TSG!0I(i%V_T*c3is#-JGu3gL zk=MA(O~Lla1h$sRB{{ZyinE+Z=C!GhSOqy;Q0^_D3u5;PJ-|1#L{eXzk^wc)qFtYt zibp9CBF4TZ%OeE^QdqmfjXwO;L5wn{Jdsubmpx1$xh(r8y|vF*6{S zkgC!^iUhp8Vw~IQ&Em87x?mA(a^(5a;Zl8p5(R$;J~89NmQyy!zt@!y=9NFZxoDH) z)s$I)ra%yXCrRhdcDS#_^lQfaiz>WI++)&}%mU@Sc-t4YTVILpKE8D60FSS^!FH81 zo@T#kb&}?0^3%zjcWr*_fJL*tz4qr|0U_zf#%lV zw1Gb{=G!_8bPie492o($9morVZZBIy_AaIKFDa65ND6**GFv_Os>m_sx}raww-olI=HTlzfcxv0-v(V|szM zu@lju)!$7tNDVj0 zC^yI%HTZ`}5{8rV%SlC}q%xvmMYv&Ax#8ha!=pqa)x_Y5a-)-@MhmBi#%IEfFO(Z! z95w!zXfhCPa=qN-=BUXXqUm_J>4S39$D^jth-S~j&0dw8y&X0CKwR`We9^b^MZZQD zEf8T;1dNly>SM5$Hd!ZvY#=2YkC7K?Q_NKtd@03ljIvnU+$F-?U25(%X1+|@!Y{%i zNNTZs%wnasWn_fqYN_SAG0TnGR+}TNwn?paj#=&2woZ+(&X8K?j9LGqO%+B^^QF|H zF>0B%O+|!FmDJ|&n9Wgb+hY;7C#1F~$81k&+ntH9yCAi@IA-^+w*5ea{dKAR%`y8s z+79Cp4iBUbkH;LIX*)iTaC{|od^_g&L3{D%h{fNei+_zRUeKm3pdx9w3Yz*jO-si~ zC(_BF!pV5tX_1bzd8D&-g|pqb^I{zrmq-`)3Ky?&mt{Jxevz(06|T$2U03QXiHuyb zx?;(?@g*B|+%`wLZL4tGIqtSw$2~RDJ)^=sXWadtR5FI+un2TG$HDS-JTgAg$0Ho? zp7z)lK_5?~YmS2=UyCVWVR#fkIwb_>D_}^!rsJqPEN+X*ftlFc8?Zm=5_M~gdpBkIJ!hS7&hY<($LjtU9s>Yjl-9{#_ADt);lqgu{cS-#W&d{-%B3L65Ab)ycc;IRs?e?q+1I`I{l zK#bKcM*T@)SV)m()Nq>e!HI&|FFqD@*WyZ+?Hrgb+1XVu)c zuNw%|vVPjy-8XejVz@Xk)Pp-STxz{@VMbI5204_y_&u zl~M==Ql~_^(Fs$zq^-?Uc_u#15_2Tk8I94K&p5)&qMlV2=6tjy3Xw+Wd~&Z-MX8i$!>+BR`QCJa$*ljZ3jqShBX zHqX|V>Z@&UEO89k-Y5?9Y&l-*Q<%_D6Y^&J@gsRVY+H_M4j*Ie1x3NuLU2KsTHTR; zI!G(kYzO-=^5dJAhYe#S%w(g%?IMhkg_mUSi#-i}jNSbYZ=6KqAMdmt4a{*kKbdma zf%$N4QG55JboIG&&$1ue^~?y0-k*PrHBRdNcxdP1vu{V#PhEaCwzH$V?)RGaj1=@_ zJ0lcq6q=&<^*lfNPyf!oLEY7{U42-+uAnS2^~=k_5u?#ybL7&UlGB#QYTItu6>UF# zX7Mq?nVa?}?LXgkdKH>F?0I7D(;aV|M(Q1xUx{B7z8Choj%@NYc48WZD%%*83~48G zwSn$eMuhp@ujutUu6;K#di^`BJ=0U3qW`Xe&QI*PS`z)BD)h0Qi9fHEPP_! zt7B=TTf-qGd~%+s1c9wnlA-64#b!UI>sUTNUL080k@d22-H6A_BRj8T%}W1C@_gNp zvBC3=>_o@Ux9tOeduC5mIi7>dTt>d`y`+fL|7PHa2%=fBB}4}ZQX^B4d9K2x>(?~mCNr~dw&yYS)fulehX7k+z-BD8keA}n6O5Jjm7 z2{Hm3B$h*oT(Wh%7{rbBX{$8~k$tMyQ?OiTOp#+g_f$0iy-Fxa5pm%yAUAeQW3S)smX$Of}1tn4%!-m}^228k(I zYnBG@Ic#BdpCJhDfa~jG{Pk`JJ4F456YdQKjh4eJD-K3j=$E`W;`uS3W;TPX=-v$8 zZdoY}`JC&s4@CZxRbW1!rqNt$eahaPliP0?JvV{OHRg~ydEwncH;F# zvGCbH!t$aIkVQjdOzl3F@YdaS)YN9nhG2T$!L!Sd$^K460kcZYzk|DH!`AlU@MFWb z(Nxk<{G#qVr!@!BvpsW6d*e0Hn*N=rHSzer*^(~{x5gWm&(EsGa3AO1)h;n^-}%k$ zCh}dqth>?`u_K)KC`@*~=S09Otr%eaN$0z0jV%euCm-WydhcC)^sa-!UcO%7opA|; z*zN0FH2L4B^fQx@9jPA3_3yV1UICeiRE>V*pFb;ikHzWpmFh?Iv!pH+(A5`!wCe`#y{x z`B;XJcwwvMe@*LUigbYRQk3a&Yok@FZ-i`%YmWcIkgq!lZ@z0AD^fI8`SNpvL;LGv*}WE7TfdwL zTJa|0@Xx!=Vdq*`-+mK)aV?C>JlD2u#oM)yf8OsNIoFYK`|XCep7%8nZk#E~_{-vgKCDdQ=t)%}#uPGt7_(otah}MP11c?SySx6odE}Wkev)8Gs6B(LFM1IO@ ze;|F{Po3b4N_Rmcv*akWr&At-8fRtmh}Cv%sRZRM(C%^;0}5I}Fx&Wk9h5#u_Tlj( zO{P&E=Lg-xl8aWxPNT^T*triUa6vzS<3qiWJ-9wcWf zag6Uh%N{@Pw8}jF?frMYVm-X>L+{i2Cn<-`{edWV@q|W1`ki%VAM03NcRngV3q*e@ zn?qT094yMw`2)B6?qfnwL7@FK(Ul4yNE>+!wlAc)8v*|({~V(OYYOHOgLnpxaaN`& zW{dufa{x@w77}wAs0Ad&CzlzD(8y;PYpT)mV)O*+`HT3*o_(NS;}kLgC;aival!BI zQ4KRWl+MGi$7MTnaIZ5!fSyseIuXS{*U53_G~mzbf4*IZj;)4Vw`HrKms}nw;9zmQ zbk7Uf?FPxdow$4+$e%Z{A*X33=bVp#E;d302wzI$C2|2Q1Hf`s#3-1f8eoH-!;FDkg2ZIMXS*2{efDNd};bJdjILpDRc`$i3p3g;WT-cVXH494R z=`;>_5Wv&i^Ua$fLl(q$|OV8@$#gMT@Ey`2F7#Q0EUOF=7LxnuvdcT$y1%@^Afp~j?81=9jl@L=s2WGRpH`u0EEi_Dxu*c=f!9?S~`@D?!+Bblp(pOJT870 zH(ksv%$b+c@Sto0?_7ZUUWIa42;S8?l%1H5DuM8c*Heliyy<4Zy%E3=M#WPs05*xXJG%u}1xe~~Qiex{<2)Ej%0xow3Lf~P8Y-Mm+h28fe-Pd+2qocx zBpWCa#&@5oXs^OnNOF*I*a`?-RF8?3V`i-&UT0yQvX-aVb0{D3H{jJNkx;U*WB~@E zVf=o%Fb2lwz(6(!zhyg+_+vK>z$g4TSi#QGdxelFKnEz(07l;pRNcYxYv!v6E*SxA ze3Lt}d>&ksg)f%X)vxnnodfy8Xt_&1Rku+wZF{Otj- z8PNxLs&B|6L?(u?@wq@E;f?IhR1_+%9?7q_Tey+3G811R0~3_TaUW|^t>U zi5hBf_y%@#=jB+R+8TMPB?D2p7NVVKjmH(fM_x?b&CG^ z!jZ!n&?PDqmXAU6y$lw1K%6%q{}pn)73XsyqiM~-?AN$mYl`!0+8=uYd)~B1!p$Wa zfLMM4>j}N{gmNF@X*}WWAi#`+UBgZ)9!}XVL0t_55_tF#56BDxbCpO8p`Ovi`O6%23!?6CsY={^4#H!RKts{ZdIsqPI2}FNKRy4k)tNwu1NGI%| zN5-itB4YJ&bI`aB_=nvRjdE&3Ibl=bVl<-ZwkvQOEGoNkB>qN+UkHBX8VEfvdP7QH z{umj;z!x#94A{5|_}ng8N0eQ2wE8I&1Xjs$pI;&}rRRUFm7wI~V0@{0kB1VJ09YJbb#znr+x*jDr%kJlM3%E!mqY%z#KzXhHT`EsVxTrJ z$XD5dv%RSG0FRM@H2|K@g?ha37jsT^JVau-U^PG~o(FR|mxgZghJSTvwkD}qM%dT6 z1m2fkjkm+23fcG~Hu!f6eB7E53*!&SBc$>&Wb%cJ=+u#4S8_Wu2mpAWrIN3}P;ci0 z^Mg7)st4^VbaMCgXgP8URY=1Zalsf9QtmJK^{Y)XsKj4Gl!()Tcq9>?D-;XSXu|rYawRWV8SPm+Xbu^N*U=>G&n& z;+Uus`QdvgjC3A6r$Kb&9_-r(9{B zL5T~7_&o_aek}Z6pbHF^7T^BvPCIHxCx+n!9U zAUd;nvGdU%tfe+fvgc1>w zh!)Bl(BWtS4I2rAg&c4f@06JYE0lp^8BX>Vv7d)y0^rIdEJlXf&jZCATqAxU5(1;& z<9xYO=AAFj;YXwQ?;#+b4HN>Kl`~=`4x&Pzm)=T;!8;!ne8$T}v+VyCcByd3X&M0= z_Wn*MG1L1Y-bXR5qDYfC;K~h(ZMf&>jl^gKqL`*owxb1|rv~P5`*{y->Ys0A7ZVhj zw=d=~pLU%dDO{?Ft)PKl@ToUE_7WhlVjdKCmKOoWj>?>31z4!e*xm`2^1vB8(3}M% zDO(?Ydz@sL64?qqcgN}#(#rvS2xCurX;!ZY6#fQ7*opt_N9>33yw2T;ymz<%f{z0; z%{NRJmQG_hxL%7P9)Umq-)!5 z+iV(MK)b)F{`p$j0e_#P`Lgbe%DK3jlZBjCO#R#P1vwT}xu^4BIe@MMp69_R-C5l4 zBXeG6ytTa2JO*mfYcM1~_sfsnSRS^V2cBCBJlPKvIpPBzUUfSRy(&d@+3o`wmt@}0 z2+#)}#PjEzcoq#L!1(CPQz+(iK2VXixZs^ntr++(dH#7^_cU&0CPNFPF#reIb3XSh zw(H4$d95vO;QD2h(Lact1tL@*j{QK};=|*6Hju>;EFGFb!xgSOOQjHatLg1`(y;SW zyeALKHc49cw=NPq;(YPIi1l1740x7ei%%RpUwFSx@ovfDclS(oZl(QSR7^mc^L7{rS)NTO)R9YQ<;T*9oD}X z+$vpES~nN!e5^}HgPJfoUmn$5Q50ALab0!sV9>IPHi%(yW5h(3Mp)siBLzx=3b)=b zzdNLtZvqz_V}l@(@k5@F;l%-7W2J%2%?yDhtm8y4$4|!6WPY;~Q5o zZXY)K!xBtqL|DvEk$d>XQQP}HOD$Gp>&{+#dvZ`q$^T;VyRoxQE&Pz^XrTX*E9zR_ zK}tw=u6M4vt?7yhk?kF%?blXul|TlHf`J-4ijR25J7-B+CYnMr3LkV=gy}G3Hl7bV z7Ef%d>~em%!TBSPLBVgjkWbN;l?I>XZE7Db_M$b+6!veH`ko!?!&t68-Q~Gx=O<>z zyIwyFZF1oJr<TQLe zIUA-~E)p-k{AB^)qudELeXSsT@>Bp^PaY+DgQ@*$#yR$l469$znXnWKN=fjL9ema0 z*p~reAmqdeT)SP5rCxH-A0_o2oL=E*N|*g;Vs1O-1i~_L0JWxrXnUtE+UxzB zs|$tVmO}0f+&UChh)v@|44$#c_QqVvH3_=hSar>TLZXe~s860Z_?w zcC*COgR?yNclDciJq*`RTcLe4E8{+Tn03r|-k98Q#JupJw71Al-{XZWH;m{HPnkci}cPB^683=sDzz3KVnB!cA8$+A&XQ}-;)`%%34fi}%}9P?6;c_P(c1Fy>9!AK&XhrH}NCSc6PMQL$pGCPg@AcMIT$*htuC)8rl= zz&9}*gZraSYB(vk>^`*oR;dNxlBb0L#amgm!MyDYj%S=v^MHmz-v%wdTBByt6s5_y zz0ntY_?Mi$y}i}90u^8oWM6kNXWJIt1i*x}!_CEZ+o~gLI`>n~aDIJx z#aHW97+UH&6ZYH=G+S$^m1d>&r7-Zv{q48tpmfM3@gp9eh;fO!IZ4xaCX|5A- z1j;Jv&OaPmo33hEzQr~YCm(GJT-6$Re8;Nx z>{V@BSDjq$!d&CwmUS}4L>r`W5(Diuiy&Yi zp5`R9;b6Kie$TwnIPDcRw4tvMA`O`R$IIKT_Q21W_`vMD%6+Fto!g8`ETVZ~m;QE_ zx7#GGqTVb=GWb`x6Xm;aaq?Dga@P1XBN%Lr0PGYQn!ZYw<`|5&{Jnbf*7;NEC!JLm zN}L}YFC@s^rbQ zn8gUvTEq3PUP^zeK104W^198<9EtGvrZ~`GbqmjCu^pbo35glZi0VD2_u8HkIk7e) zjaK;!7F_LUp{;V9Eb#HY?jaHTA=D(NO#LlzWsEp$QkQrtcMwH*>})YcJK(gpQ7C{^ z(@7Px-l_5Ee+=t!x*x%t+v|z@&!7IC;8I|g?hA98)zEo@yw^$e6| zG<{&-FmxP`v07iPl~sL_=2qmMB-3I(QPA{m>n*t8 zdVoK!@6xATq>@DvpzGEn*w-#ZJV?eWkbAd=0*i3? z44qH!!!G55K)>JV*wnQKK!au^5g^>UFVO4LS1Y4_52UGyLq>f9Fq} zb`Yb@zj!qI4Y{WqfAo6^kq^4XN^Fzog#;r#+f64PK?DV>)$%4W@6SjbL*7&78TnAt zkV*kF)wf6%%(RaWe0t0SE=4e(zYY5h5h2A8pa!EBQTa44WW=48zuBh)ki@U88G{Gd zA{)lEKRMam!fNXa`*{1nzNI>-B;ff#KmeQyd+WYDjSK0Fn@*IWdu)mqEd?2fyC}Do zwuG*WVNfZn8-b2NcRj9-E)*?5A{P7KRvF)vDq!0bM6J`%tVdf^bbDl){6?0P+VBE6 zXaf@OrO_#umXdqlmzIJ)6FX~~d>8E#@PlG}E=~ypfHPmK(QgimT2cu&jErBr6*(!j zW)plvxad~uzh^neX{jP#7D_9#P1!&X1bn}j|NBZN@=M^CR2zpR_*(B>dSBeuRbRMJ z+uY_U6D16ED&x9d%J~$D$d!$3=mv0Y#aC6-=S5l6=YJ6-k&FMp*JiL4yo@{~eh%}3 z5=5&#C1j!qA?cgoIS}6Zs(ag-%z&n`SBNxSziN~#N%;CEfl{k}o#T&S^+y?tv`z-N zD3BL&ygnC+QPrn>XW_x|Ghqt8vpU+Hb+sKa;N+{1nh-N&A`hC-i6qoAMv>O|F}DwF z>+S4q7D^^8gNM#07z#Z*3-q}n1|VR~|GPWBIw}O^0--~w1qpFMSt8WB-!xKz4r|Ec z;H)|lroa5vQid0WqR&fap-n|%q+ICDrQ0m8a(dXO{+-0+@fJ#A4g_6*DC{yc z#>)5{nOCYfj-8rmPKW5l4d#~n&RZEqNOjlt(EO92^aAaoJ@}!qFyP7q9+fQc~@BY1E*m( zx)X|(;8iN|t?@IdcH(7z94=wjYZs_qC3%*7RrMF}@>E7tVs`b&arn z(E4bxSe1tTHd`Dpgz|!m{8&W~*4!3+L`qX#pIC&(;VptWDtc4;Dxr3%M^Q$EIG7~z zoDc@W#i!%1BO$T&(r_!XZqRmqNfFwlT3w4&=qK+8oX3|?140<0-c%7X(MgcXJZ^*z zWG^Gdi0&B!b`#Qt*RCiZMr{}u#ZnxUKNPy54?nFoQOIjji)w`HMB^MDor9iFD7&I| zR9#l~x7pp{r-iSB7nMUf+a(P8UPnJSjN}0H??o)GXsifCeXl`XH9^<*1x+lcO+>OF zQBQ?h&v5NH_X?A?{ zu;7pc9n^_(%S7k@aKe9bYBE9?sCJ_!@LQBkrW5?Bj{pfOQs*Juod6QG9rfeBgu9Hw zVQ{2H`m@n%9wR6+#GykcOk~$6+=>%BRE`pVy!$~5ZFRaWZ@0z+q5?stKiKO0fV-q% z0hRDT;QXMT5134>uG>7c+rbY(LhQLr#SS50;6Yj)9?j!77K`&sCc4y`G!Z0@0I^8H zpBfzrWAIEMK{IQ0`Wumcb!WTnz|-E=#slr;+DMrpSt!tGj$GT+zP4!?;m6kcJE@ve zS$pcwTEiO%e|dd9X5E>kkB?J$nZE1J?^<_Z-@5LSbv#|v?{#dlxSJ(8gV8w*W%=n%>Jo11`TRP?dq2Oj5^JXzuRtX)m{?$L3hr#dbh zW?Bnp4yfufPyfo7#37}@vJHy0k6I_5KA3&F@#hA2-KD40o_spMy2wW@%T)WXQ2b^6 zhE1nY%c@arEhit*D(RU|Mq|)|aT9rnbGz{R1ItIQ)krPX(t}RnPK@N@v~W)C96}b0 zB?+NuK}olx4@pRy>QI+;p>A$?CW~#4i9;<%HSf~+wXeW+0=0~R`Z}=jM~cvX0{q+S zx#+3Q9}T?nz!GD(>7Mz=hS9!m&Hgl&ko}hW>3AWMw(*hjY;8f^S%|lQ+OV+!gPt8S zlyhTSn>D;?#Eez_!E)is&%6Q>I%`wSaRQz!7O64{sC&1N3ny=VsLf(m4rm<%Qy1zmcFgr6m@f5*E211hW?e*_-V+C)EE*6yjRL(q@ zf08nc=h-0L>f-}h4c(tZr(es68_;ZH8$PGjCg$aq^T;4p6=93WsT)DrQPUFhyfA^~ z#Yd{PaDC`{ILz<_*Y>XY$`4eStlR8SDk0E4rOa!lX~J8RM!X#7bE?x@#x}athcsB{ z5yTLhD3D*d)#|Xi_XOXBqvrp8)5<(i@Nu+{gs;!;S@6beVr7=?EfSi}^EI8U5EJVm ziYjtzFB<2YR1074UDNuVWU*=M)(*POrz74&Yv{94mYV@wiH~(EsK2RPXw* zs)^L*72_sLu7O|M#m2CAKjoswc8x}Jr0-vw^4}Lyeg56hYaPfQ2_wa`f;=uH@OgITRcu(@~2@gh=%WI6gF=KoxdZAj24)H-JD6mHWMGR;;i+ zPFCSiPt{|z0_MuD$X|xZ-*b$CGo-23@0X(@p?@O_pB-F}YQ}SK7lb`hGCO7y5WA7p zf};qEq~g6VUu4q!enK1YbUfhi?-#-zxRiJHB}dDTgF5B&qR477GXZV3t?av|pa1u! zo~%+$1Cea)>+AE4>PI*E8J=1|ZAbV7zn{3fIyeDEfqk)D`Ti#-XnzBrFXc6X1iSFP#WB4^W!(X2`BHKQuV?W^Lj6xepJ$^F zHn^tWr53eF5Gx%-ZNtFH$zT$b_Bi%CsUyZORYQM3c^ z*4*UJ8>DkTyvrG^XVnV(#dy>2D(7uw+x(KZb>ly#9^ac9J#ciI9FE?2%!R#JrB`mhn%|uB zk2vJc^95b~7m%B1oz5@#XWE%Nn@x71edMUq+}`=W@j8VYy;QApCsF-Jo|1`P8uM$w z;$qcF6q8z#*0+($69qz)Jiou*vaz%9#m!+q&QP0eRQ-!hX!03F(F|B?Pbgs&o&i8) z9ah^uVIJ-mrc-J>Vt{aSl4{pdwyuqo;)?#`eP|3nd~7KSAL^tVtZl*c#R_!O6of_( z6ozLX#bH>Z#L#*;K~`>20`8-y1ak?Eo^@)S2%}(#7$DY6v%L%AM#9qJE#w3%=~x(U z!rt?TxXfP&5|-@fs%%@sa6UaU=)@Xp3@&?PGPI*9vC%I?%}E-A({ulU)pDw{9YI95?QUp!lRV8}eMMjb4yv?9Ns?9DhlQhv~V zmgwOP4Hf|Oeg(>qp5kxV(}$w3HF(ut3nNG#r6_BzIZia`bBo7!g5Ul?(qm(B9d%wz zG)89Kn7rBN&+nh#K7ZJ#;2BrUF{}Q;=(A*eylpou)NriEQlzmj;M|@m5hvGhM>jeD zOBbI7`=`+<1(t_08-(xbyD3Euz27OtPQ!Hbk|mEa%}YJrbeoqg{rTNo!aPq$LLsv} zAg@j?^V^Dj!4vN69hC_Yb(x()_21_@2Q-l6voAD!HD?z3cW=7J;u#j!>IRcT;6a>4 zjLBUArSPmIo++R=2my}&Cfq-}{7(;Q*rrbhS>%a>PtR?i?|Foa==FkS*KdEeIys_;xfVULeKN6d-@l-oTmS)D)!WE)blBQbynHKYm zJkK31Cz^U~SK?ZXPS(&Wvz`MqYIy*$MuQ>L8rwo11#n;-M)#Ugyj;_RH0Y4*@s5Vk zbq0xH#rq7s!8%ZVZJK>&m=}uodhWj(jB&~=pLC!FM!s0xV4xy z9nLhTeiB=p?uXEOP=-oWG=|)XB4FlZ3$$j;6p*X;M7vVhOAoF0UwO~h38V2j%=M}+ zo8k|QY-kNS+w1I>yLu=@IT&tK8agI{8bT;HD870Rkp3$uY_YlB6bxFdJ9YmN!1wkQ zRiWV1;A89$r-D!{W;u5qotZooETtl`h@&30iG!iry@NSyQi!Bg9g|Y0FP;A=ZcF4H zH?;VpA2n#6HCKLKX_DbAFoT0^#e}1#0@l5C#!{3~k^82=X!F>Pbh8!Og8-$_B!gkr znje)?w3U^BGrsI7-Eyjw(10U4Qz>{jVM2A6>o1+bSsvGz@#A2Nc|#Vtolf}_V6~0d zB0#lR*nFsHNxo=>f$qYPyl#I84>BU+ePnU261HGUg6ZOSGPO%P{fvMq25~BI)ppvA z-3dB)#f=>}3EQ9f{=P&&?c-}D3O|oY^MhfeW|C^pDLNcLG`A3QIDTZSWH1O2Xz;ll z742o?*V(5rTNJ~D05 z0lF4su{x$%qmDOV0rc@cD;O)-v*_P5LC{a~Da^9iK~pVlFoZEp%=4Zxw+e4NzXGX& zWJVHjDczVcIZ*=jZv!D2W9FJ*>%1P*J4QubzIVw;j&VU16l^RH zz|aBYcLu)Xx1%~gR`p^(+Sz)dZM3@4$4lwHy!~qqHJLWr{QM)IFyY5s!{*zox2!R}|E-1@{_NV`V-a z{(BfV$XFs6kh$c12cI8+BtKz*z7SurnVBXFL=KW$=-a~%U3&Ks0!Z)_lVSS)oX6i}XGnJI9L5XSG%+w?;Cbyy9a5`AbF7JgRVLL z_Q$EJwLe}lygRQV=IhTQ4FTs5ArSZF*4=;Yq7iCjynSXi#B0{x^zkd0-LNK02!W?G zkPpM+$!F^wY6Vr1*8aO5-O)WFGXAr$J^v1Y0b0MF_tr6;<~I*FA0-T~NL0i5$v$O_ z#>-(e(*%L`+V7aczc$Gh+0S*BZ`7CpI4Ha)HBRmFqgm#q=b+@?IxOQMO1BdZV4C0} z$~Y+9F$Kt)?bUIl4U&(HKmFTxAx34=TbT4=EqRDMUhk-w_T z7jyel+eY0B7*BiP>sunFAql#Ss{hQL-2I4u`2!kR5nj&s&uBqXY zxfUQIni{5+niVdYwwRWhm3aoleP1eV#ig>c!n8iy0nIHdGCSFN(5%d?tZY4Y{P+jB z;F`IH-@Nbh+&2OStmxGy%gb39&Mrqz^81mn3=`t>6mO11w#S{fPvwT~1dVGB3MR*# z&waSs)NKAeY#KgvHhyfWeoYqn?PgMTIInJ93({R4rwgf-WK|@}g&GMs1O8^i+Eb+& z%9eNI*O(uytBCND@4bB-$1&xZXLJYroRhYUR6k@d^OW-<`|V{AeQL?pf3 zgaUt*a~kGC7Kk_0mto27)GOx4^xO*{Wa7xYf7lZM**z3A;!s+|bL!X?>M~G+0&$`t z`1pTRg;5J=CIa^^K&@`Tqe4J4x!8nSSIQj7*YMM?%=i9cBTf#Xl0)L^sswwF1g6IJ zZJMbSZ;j*RtW*GjhR$O~JI2PoJ<5!HaE=AxMmZ;rJfZ9m9G_M)*j!{U&60%r#o}vT z)AFf-BV-q$Zox@mz>r3h@kg z#HX_<+oK-kCO@od-+D;Z~pC}}OA@i-a>Ni6E= zY0m*GI|PG@uyBm8CbNafV*k;UAsf$~U^7Bg zuER%Zv$P6!v-=c(@%m4lOs4% zkME8`z?Z~fwYcD=eiYOaNtFo&K*;|&XWDb{iBYP#0))zksqjGp;Aue@@Sp$HU${dp z;s_GClXbzNh`A>t!5{fubrT%N77xFbpt*`pz=X}|^?ZQ1y^Nbbm4{p;R_^*sh7*Wn zE=@WBjVk*bB~}@0xv`0y#0jHe*wWXU${RCM+zY3wE*OFIdgDE1I&}9!T9lYghFAY7 zXLJ>lCb4gRlJ?+G(t2eeNlBBB06zgLOU1B5SdIo0%q12>rN&fANf6Gm&2)PD6`LTY zvQE-R=Q-CjbSoUn$_Npl9;EdLX~Ae#ZBJ-2&}jC^uC3Itj5R4=`d5CJev_6_J1gme zp2(E}L`MWOTMQ76aTXKbl#`4PBB@MqUCq%5}%n4d^*1@j*PS-{E*6_xS!8e2{@g z@Zc^7{J}K&XMtpGT60qdLc{K%@*pN#xmy9?TvYjd$;pYkZ#}upxmRvBEr6!dLYR-i zN!El~BaGfMm<52k&MFp%kUp?3NdpOHRC(a)q~iAbyCX( z2xd#bY66+=+jk460ssSRNCn}5Q69pW{ZyX^sZKnu;RJQ%lwB1Xnhj~7=lW+*t+xo1 zoT|TRozdFchbctJ;85XEA2eICGVK*DifK*eHZA}{btDJ#iCUl&a;$vEY;%ADT>cXB zT@M2&@d9BV&;nX(lo{ImD9eVdk@k6FIN30R#eKe@hgS45U}P%5=-7bM;VZIQrYJb~ zY7pfShe_Uls;4e{ouRQJ!dA_8Cjm18go8e%=V+g;zF@(y<=G>Jgw}A7z;WfY;3n%a z>UXel@>22s;VwHFwN@M&$FG1JV_KMr;GqxmY*SM?3v~c=jewMp!H<^ z3lG)txWd*6ILs4~-o+WE%u9`k917NM*jT%fE#cIe$_T3{(%wxI!9>DnR>>e2~gGDqta=4U@UVP5_|*m_v>;qtk4)7HIz|N7QfIF8*IZ zV=KKH5N!K^PUhgwUCGs5r#6x)9$bJFAXViT)cPnj#(JBReh>N!b$=0_jh|+p94rE4 zN_uDNhF>t=6hQKt!?Pro?iUu*+4vT;2@Y7nkzCB;5-&G#CO>$LH|KGgU%RjEIZzC_^)D(2EB!7M}?LM3JjCbWtg!5IBVe>T`sS_-5qXD)&- zoVMch6iM!q#f#N6lz716AQkG-|EcUINex!mF3m!OnyP{)6bAXUo1TO==iAhlySA06 zl^?)#=CKn|=@0u4T}TW{^xn!vahXMt!QX?|WG%#95d*P`&Fx}LHb7AXi?a|dER_)% zz)f?GBXnmIUYQFbGyK!c?~5@T63_&i|E1Y`GcQB2dPk`Ven5r>%*+a09dvwzp_ zEx|#D6N*tMbA8c2*yXedc~0p#o-5vm2|}E-GMXLw%QkzD)mh1suDoTQ(!eh1LeJdV zB_J85t~O0`p8)#l`(nE`=5;k>NB@=Vxn0yH@#J%5o)vQC=F4kZk{Zo$lEX$yTIXPH z2#1E_9>__oi&dBF4utQu|0^+bhBD8i7;(qHV2DO4a}i`t<;J{#R^DCDVIi1hRPJ2W zk?98KkAV%Lh$C1_?{IFoZm}Mazy3krp$D1VHlgF!KBFE&L6NW`@`}Ic$^pG$d8Xvx z8car=skJ$xMtd|1Q#D|)LG|K|pZHpHFQC{hi4y;#d+g{d=$IJ~B-?IO=a+n{WTsUg z+z<%nTurTe5_IJB<87hg!_2G+bdg?DLj8om6TPSIDrJvhsa1Ip1unGh0djEb-d|M< zez*7DT{*ocRrs4?(Q~2_b^bo{-15EoiQa^%D_hLZ(}%WG6_yA8_4=;uuv9*$M}9lq zNBjtFis*4PxH9Ak+3W@I<0*{b7{|PU1>VR>F@+fT^3#oD0#)tW z1s!?OQ2zX!Sx|4q&ij_4u1zOa5qXGV)5T14?w|>@lBS|wUpj3dc^QFD0l9iP5pd*> z;mY4S6D4MZMN!v`Qd>XXDr&{lt#V$l@fNMhI7oFHFTE0kFz+wau1ZZIMdZsqaOQNV zF!^8w3~Ja<{^^4zs`$^@JEu4Nui}W)A-?xUtX4qVG6oAcznkB#WK;!%isu55cgnYR z$Vknw7nX)CG51=zQU(+F)ma%Avx&|7gDTLOv!1O)4#=Y~UKy`|4FYP~GAM4vVYXl@ zGl1kSdsr8=oArJ6m*M1Xzq-wQ*F6~|CIv;{45oipH3hnR>(ofCPchku5}vnKJZX5( zxZLszRFYI7HCo4a9jrX+Y^@?fK^aOf3Vu zZ|vm<>+vM0{uQ~)%9#sJ54xT`ryj>Pb8LJTI*DzR5;#n^m{)jZYD$W}jGL5}5C1Od zsX~-p!yA<1>=5PN-{mF5ethwSeZ$ZCMBH7qjLBPOW6E2$Ot(QgZ*W94g%Xc=x2syY z&xI$#hN8H@(bCpP9Peti^Bo&|70j)BD))bHgZ+$HR<=W09l#87aXpo`Hy!s%t((3$ z)+*De(j~>P2DV4jwi*!Y(06LFSFiYaB2&D3U7`90kh*E3V`y7_dF}h2>qU13 zb6?##m}sL;>qw_Fkp>o)#1`eTxsq?^qFs)(5UgWG3LQcAy7t>)@QviCqjA!oai4N} z>;-~YR+tZKYo^qpZ10)xL{skvTvEVEcuayk(6X88*IATEw7A^)09O>gBkD>}>KE*K z5Z#bi;<8V3)^O!3P2RgOyjRPxN1gT06?w#u6T%a&t5to7(fU^sux<0{;d2@CCyurj zd4TeIYz@!g@^jLaT0Nt1X?gC#?&OW1V({UF11JKnWZkO7&rL_2rk=z%>c9wtp2bMl z#i;St?sHCFx06fwKf7AsY+Haph&w4iBETnB7hkSpFeUsy)>1nRO^;qVLl3OQgJj{iL_D{)zad%NT?!XpE1Nhy%-QyS-WhZ-vuXI9R0Azf2z1Qk4Rf+FTCs9 zm}AJ&Y!UHI^INNjZfG_;uF8Z4TYR7KRfQ@I@(5D1Ryx`JXCvKvCeO6Y4dIT8C&1)x z(2Qi1~xw#WerM$r7dSxePyHes9*!yRRLuVK6Z;} z)Y0&0`Z!943^JGaHf(&Iw0B1|yZk`sp~c%aM?Z^sj7SCCVby+NFiT0sT;jZ&_>}AJ zo>+2V@0HA_TmE|XO#I@zt;+i2j^fh708r90wEttq>9yK3^SE|peG1S+{mIgKRIbD?fxD?{ zHwQndk$!r>8)!;{{!CQh8dA$GP(-L?v(2N>+G4YD*~Zf6+D15lqHHGLp9cvtC_8zv znTQJChCzexU&TLQ7-KG0O0~}pD|92KVT{IKF=&d+p1i+wMN=>Qu^u4H+=Ew)aWfei zmHPXLbh$TSYt>?5?^efU>rm3|Yimtst2&f#;&6i`u4Nrt+V}<3vvG$PwBFb`hGnB2Po|2^fib+pTp@H>I5KGv=C-8-}J zSaT#d+Ec7Y=lZ-__(OVUH<}mvlTjRCpt&=BK9+Lv($}Ebd2^41Bh#mqnf?ysjR>QyCzRE+y!FeWPyg~uJX{O9Kw z+PPj*_vgwREmst6(rUzzu@L5M1#EHyGC8NEmYrrzswC&>iG+n`E{P&iulCcWFAY(# z0|uVSAlckyc}L_RF)A6ZYDr!4vf4N`tbPgAzfYB@=-+~UKGg0+Ts=bxkBK--up7G ztl5RrOW&SG&(h_*$R6sWynEmchLjE2&YrLS-5rQi+PkOC;3gIetO;4m_1k+Ybwz=5 zNL>f#jdH-JGo=Do?6G>hI`_jib+^c&wa-ohxislW0bKBv|}@7 z_b#I;jzk0mrx5utFmD8sTHests|=p=e>pbc_R{yE%DLu^j;Z$)Nj!tdfGHKTaRw&2 z-z%8=;P%p>=FA(lyo@+oX?2acs6^9IUv2ujReSkftn3D^+goiI&w;s!e}YK&3L)sO z;R?adBWDjfSCRg1&$1K$hrW-@0(9lneQ2eQ3GIchhCrnidp9mW$p#2ClA2A;C-MOu z48R#Q*6}V%FGEuY*KZAX73j^Qkyti^+X7G!<$~M9+XRM+n9$15cW5>AW#4ORBM#rC z%V1I}7r(njI8C)QRhZZI&|@E1ZfXe(DMc&v!I6zNB%ZCb#@32fh96FLhU*8L13DMN7Yg@6A00O^bBV!RItPZVlsgQ(HVzIy9E; zYLQ0!n>}|*rdk77CtCf?IYdeeJ(Is7zL2561t3(K7fSSu2f-H#-8zkR$i*+lO1Cwx z#VEGBRSL1^weKc3t2_x4&&f2m^|nhdgA)Vh1_E>7RXGy9#V3jQG4A~J^cWcikDFxN z!IBOcFiwW`Uv>Q%s{x))EH?HRhAuRR z+;wD`b|IKLjh{;$dM%sPr8$&CYD`1)NszX6ilsqQ+Z}E@L=9QXC(smWxK$j6kMtpn zw0Y#*Tf?0%-+85c3RR^0=v(>p-3sCdgAjY>2nuG1KMF5c+;qNuz?Tuuh zMnW3_+bkGZFiV`QOPd~{(>vqQ$*ZtbO#Ov5549-iv|-D!ZMaL#SxjFvZSD7<2uOj1 zSfoQqlr_!7Hr+!Z{s1idP0&(F#Ln#fn>K5M2jI%_cL;@)h#KQ0k!(emT-$0r6ca=o z$7T(3+F7;aBGPWIt#`upgSWP0{EC2R3^mu1GQt+!MS$ehi5(kHIqY00`4$5X=Qz`s z`NaL8wss}$54|J;vhEWR90KM09`BAYPK1qS0l2j#?l#gL{RRyv&53ciVVDRz7#PLj z)&T}BDk5&o6?30AFwAsOA?G!spo|WxbNtt$0fG$PG@mikt05GnXhAImJcovF#v>jR z;LR?s4`WZk#5balQJC0Cy+5vqHvskBRhD;Q;&ag@5!DyZ{!1;&T!etmRMaDA=Sory zpi}XGdXNr})B30z6A$Xo_7Th<2oaoAodh?oNoJYqAC`?Cr7?RE9iRNk!ENWd_acl@ z+%9c#@b`f<1W05P3_?_NDv-8Yow09~A77y}tnUgHMlLS}MKT@c*5sSpN!{@PL7@{v zebL(k#EMYYU&TpOK#T^i-oLQe27b^Ayg3Nq+4834&@jBpycVd+GskKW3PT1SUbt5! zaZ!op<1~I)U|ZNm=n4i7lu5DLxvZge*W0+>M9E%y7Mi1SY?ADamqehYaWDwUB&*&Q zq%#T^#TU7tVV$V!#(4liTbJMvhtxM6HWFn3!WYnUv_DMM_s|8UIK}W-u~R8D9Tr96*@-%5@M( z|7@wDnHDX4QuD|^$gQE)=Sl4{(>nj?y1-wTJa%^p?$iZcxdiPNt@OIo##TTM`m?f_ zzv$gFnpYn;={js{8wCvBgznFaM@>`ZBi)#3nYCLf`nkdYpy4||UoCSkpZ{dLykp8% zS3#gMFHOQfLb3ylyz2Ljqnq5XijXUIt?4)^`~7iv^M&QZ@NUr=FF$!hmS78B_tkd1 z=5V){U#}@~(92H7Cba#^<%_H*6@K`5qI2EV*yzUWjAI>MdtBZhdrv+7!z)Tzy8qmA zV9rz~RykaGn|re?nVF|Lsi!{+JD{nUSs4MRdKzm9rSwX;w4RQ*Pt##GU_R1J# zC;njf!Vhb8`!K`U+aDVr~Tr*0a8&+Jp3)CbQ(2Muyyecdq;T7mhl zIWfi_Va2y?ISaf|;8*H2;b??mz^PAm*2PBui|C#NZUbq0^S+d#c7ULv=qGPp@BoRP zRp3$;cwO3gO#Q;2G6JB~zTD$S6=i**GM~oKu$Zf88tK^PZ{!mVHY~r1E2(EH>+4^M zCV$!E0IBZS?j3AmQt`5vt@j+B+E(pJsAC^U$_Q^cZP?m;kRba#D>%MEhfxAtI=kJ* zN#Y)3OLC;F8Bl?>R}4FMOwNrjBxtCeuf6z-3VcZk7aQ)g5SUHanD6ir|33b8v*n${ zzL0JVvyouaRIi%gx=wp#vz#3WS$g?^T*f3Hx`N|D7c(LaG;VXUP8#K~wOHR5Km=D2 zokgb%sQ*+MfQlYG9{@*w5nZ3zevBrJDthKu9MRA2U#9rqL<|giKE`$+j%GG(u}|E% zuPKzCqf+u|f>*>LMRze=e^R@-kKVSFf<1aUYEgb~9RP+*@vpxM;F%N+n4NMQ(B~_V zJMhm1x_x*KbnN9K zCn*g$`~;Wes|f6Oeb|ojD4{$G%szgNx!q|T$ZiGDKwldW{1rx9HU{G3h@Fo0Oh%rK z*lw;FZx?r5S1~#7YN6Vv@{MQi>zs7j`O>q(Z;^At=sPLB>U5}}?8LwoUyM>Kr1NUA ze~Kx+>4I>YqPk8!p}%>fyRg}}x%Har#>qtvOvG5B)o*sPqUMW2SE@l<3Z^bh@J8Np z(eUB-RLq`#evC!`n0a!z_P49&pt4`(BgD3UJF&`$HaOkh;uvHghoLl0@U-QpMXG`# z#gBa((m@g-^5U;VxHtIcM@Er>UECp`Ti2Z=Z)<(R5_93n0bkcwU*7H={xrVzSt1TZ zlra4<-lo^Vz7l0V&dfUpB92poxcm_Ni^78DKxgtB0JSL&45-e@aCI9n0P{=;p{j6& zkZHl1Cj@c*MX*T_jKfck#1B>+C%hG&@y{3&`&YA?8e4WWl7b3XSNNh@9~6acqc>J3 z_RVD!J$4YqFBX+7)z+8q7bl=nBpV$~A^nTY^mqbHqC%5jM zZm03x9<>Bh*;qdjR~@3)-OzKg)*vXK+*JHL<$jA7j%FBZCp-7NsB_E1V}gl-X1+U4 z)&E>V(-&0Mkvr+9s|#!EP_)TJSw+i1mrYp?7sCetlTvz|DGP?Jf*P@D;48J=@U;+^ z&CP`@ ziQ=`PWi9HWIk>@I#kM_Vaq$sQ7u{F&S4Hv?m{1;>MYXawRb#;vVJ64+c)mwWir!Hh zQ=@8Mgif|LVLL$6xwM!|2^dORyMOh#2as3>ARtEdxt^-cmAI7xW^K>sTD|p_q<&ht zx$A)bqt_xex!UfcsY$iax`-Z{Rp-yFCOoLx6J#o7@AYb;wbJs2$o)If=`I7zEtwLS zUKjaVsJEUzoWv*)NDiFPGu8AlD{!v;j2bnVumO8X?a?c zfcq_j;y$-?bn4^!=>;ug{>*~W--bp-qsr8=PH`UrWRz7!@SIS@o*ja{)ZJr*PB>va z@R{+KeI{jqd<>`0{&ls)E$kWC>-|PJ4>}3Fy}`P?C%^X5E7LyiE2PZq$=0(d@f_aX z5f_lXjOgj%ud~&8&)0j)4ED_qS6c47UIVRUe|Y$Bi_f=Pe{I-EsAR{Zkr;M+5tFms z;KujqTLyp4O-r;1)ZNASxv`f!4gOp4(Y69Dm>pbI)9OFD)%F(3Ax(^PG`~GdmCi3F z>_MqH7AP9zYb4%nI4#30gH(i)G%3@u5xb&DWppMj-*IcB-Ug_fJeXGC-qxre*P3TP zi;fP4xgY7{dG@G2dYhpI?xYN*pv~C2v&X@0^?B4ZwO04Oj<jEZli{L4%wRtXY#FPo|YzPIc?)xfjldH_Shc;ep9vEG@9>jA#;Rh`eu*8roZtZ_=&y zA!5U8BYhQM5c|;MOyUW1{qaHh5GnmtTd0#x`kgiW%0qYGZ}iulxwAF}#3!pcZPK8S zgf|LY8X2k2%%&_8PiwJ+YKEWzxlo-#L$Svn_yYMn3iDe%xCb0^INMwmSZ@(1%D+Iq@WBeDpo zyT<(_(QA|UqWsebZbt4v!3d*|0f2P~C<>@YL_B#L_WJftV6{-n{61UcCD1+Y3sz#L z*op~_)Z^5HZU#QQDc^C}pqy$=W)h2{OoIr4dI%;W{QNs#y1rhFig3Gs5KlODWi%YShfy$D==b3?LGQ%fFl%>*eoB1f`HZ!;QcWOB>owjY zb=-3IbmP6TVX9|6lxVmMygP`ftX4y!XJSq!MDvNRU| z@1#?x`XZ}~KmjMGf3?33B4~&W5H~E+iy+_sC)q6@v2D$@Q5~tbKHG{@#?gDlIdM_c zkpGTw-q5wn(ns$UrN3mi_e(#Qu-qmSULADf+BOsu+DLE`zCLuSJ&%px7+J6-)PF)>CG5o5pScvtE)NKI@|VOYrgZNkC?1@EaK2Dl;u zB=ypCGePYtX+bZvBijWt(w2tZQ|LdS+lF#ZFup#>pR*>!jELbTGxq*E0TR&G9A-^Q`W($Lo6*Vp;J~=Mx{Ix&X#Vo45Zojda-b ze)8^Ut0`qi4WxO@sL0L8H7&f0`R5DQ12t7RvKyE`m7X_ktSlm4KU{aT3+UvtfHnMm zG@YdvIl`J5EhxaCP{hlz1=Ga;TfO-5VxsB=x0pdY8Z_H`vehR&Z`46$5F_~PpY;@L zo>Q z*9;0vwkEUn#)r4mJW%#OL(GbQA6kdkCz8(VlnYS^VLT@C#}&i$?AMTy@>@Zjg(&&f zwm%u`{}uEcwcmN1_u_cX4Dny(_seg&{>+yLT>uGl=w*^^Rxq-|Qeu!*-_74siwQ0Y zCS?IfYWr5?W(s#1j71*ZWRGMy_>UQ~(&lY1$D<2+(9|rT0)WKBTfcN*N_v;+sMkGv zv!VH<&&}q&HB~{|E(s28T9P{wgLlb93b#cFZ7R}%J0^A#D&gju?YaK@YOB)D5Ha9m zYeui1QsX-qG0xUh=W}la$Kbv6V zI6xLm{^cJw=N5A0F`@f);+4*=d2&+6MU3_(L?VYyJ=j7fXM7qFAFXoO2H0q_pgYPW zC$)vrmfk|NWX-IyY1;ljYv*d%?Gk>ZD6}V(^gczafK4Rup<7$!Y=LF|B8BwZ?HoC!;t#gl@RPOAYOfLw`~IwNSj?OKDdj z#3aji-gXH|P)VvsvBAwt7p52Q-$}_$$gcTW{Ad^D!(wzUJLpswA>&{P_7p3$2Az}^ zUjj%<;vAiY1+^k5sY)j+&35ky!r29~du<)kA*j%x6GWKQx9j{D14(JNEEy}3SpBLU zYJP*9C52u#EPux%*lKn$ z<#iMQgB3avX~=d7GC*!_DMZ0MZHQWDtqj!`9pS3B52)f}2uE=XP;A0Nof#82BbPU` z(@AkO%Mi65&~ah|Tx!5knf9Z6mIzTigM89a z&r=8JiIEO_{PpXM64=;*oy~T$8y7!pcFXXU0N5%5ic``OwAp`I++Rha++f`k;lH^! z*n8#X6KhINM4vgacJRcysS_J?PsTZ%Ob9-yz4|z6=-I!0(EyT+B`!9p(n$kvn-Wm~ zShM5gvfvn#v`{Dw+#GY#u?LWd$}e#+C4`*&c|dxa$E+)Q1qxY4g_b1}lGTtxV8)X} zd#SDx7*a-i7Nngfh>=BVXa%L!WrSoNXXb5F@o!#8`xGne^1MU7V$tfy1b3ln!9iM> z2{D2OA%(zSkA2Ho!PuucQ*~#~Y>v_VBzUsWZ|TdGI|HA)x$O257>q+20kQ^$t_Ndp z>Jk+Wu00OkTJUQvfIj>+=RT@XH_~U%RcDBb^pS7Vgd1xYwLU^GXF1Z7eG0c2QP)Q> zfxYw|AiQB@$VPc%hv(B*pjVozjM;>CM0H@zX$P=*K103ib_i&bD-u{FN zs+juXg@tL4wmG*KMbp){4}(Dz(;u)7AIETcujg zhz}MfxH=0r!vy)&-{;Pmokxo;9w%UC8=D!AIfrLFR-WlXZ%uiM%k?@^ zTUNwoT_qujjos9#blybeB(KhwVGKbm`2nJ#-6~fD)utWHS736RN-?bXk}Ax}AA;2v z`LUYS*N6EG52*F4X|5t4HA5{RxRQs;uNP8I#XhX6ies8@Isu(-8$5YtyAF%E zb0hGGKkvEtIniLvR!wzn6|0%RVOheNsAt&x9+|4t{=Wr$R+`ZMWY6t(lDh(`(S|M? zF(qR+n${p>&T1~;sikuhpi0#|bvrR< z&2)L2%3-Z<7RnYIMUYkrp*vqq_I3*9XlgV^&Bszgn}eOxZdWKGFZGxh+`WB{cAcPJ zx8NO>F+?E878^V`SEN4atU+wkMe;eGXa469zs{yo0XUQc4d4*6DskUig6mP__G=`c zb$172Q1CZH#hv0!yLR!a#nEb$Bp3s;9CZ*zn9N052yPWChV7@5#G}OfLC!zwh(z|8 zCaCSY@%nvM{c~1D)E7k>*KhAtLnr&TaDWD)cSJZP{ZDR1n!>qNY`uqH@dv74p9<73 znu;0tYg?V_4zR799VHsQ{B=RL%+CRL9(yqHf$NLmbr2*Qwyc_enLK4UZ9zoUGPXCO z8pv4vyoX-gXm^bk*jyJcPE#5_``_yw%}boS%I}uSpWXQLOn6uPlA;+2y-BFPsyu3# zqkgpNLw)L!mzUhyF1|UKHxc-zn3O{;sS)Krb9$-&+4SZaqPYv=#m2#^`W?CY6Q8%C z9p58^#;!8weC`+$=kBVcwNG~xT~J0`^EKHUlukKvRZ~vz96E_QZ|XMq6pP)8wxb0F z&0R$WVxxtCS<`B-#yu}2cUv|svAIc5_yIr-GA``grS=pa2o|zlDgXEOV`xOC98=lf zFB*H}<>Z1+2bxD;BJ;Nd+#{U!>+Y@FZVv9;S7fgRWeV~0qwJHEKR*mazFpR? z59W@ryqX;0^UvG^Ced+|k36<{jiYllp~ObBDuoVl%bt_t&ws?-EN7veU}SzIWZtrA zJt=b>iA`Z6z2un4_60f(0TF3VlUo6AV~7hNlZDILHZv<>G-C^+V8&eT{2Kfkm&ry6 z)eb-Y7)Hsf*WP@O8@np?2Y_wR<7kw3fsw;Li@?4#=)%lVg-(Hb{My*EUvJLoq(7B2 z+jv*sQNlc59D7S#v7tgA@VBA_6Z^U8}P zzT;|9Btu>@{9LDi_PYTI@`h~&n;5c$O__c@r0;1F`{%|n>XjDWI>Fb#bAayZ-S=XFB;gTlc31e7aw zJwOaTlA{-djS-Ec+S`@hc4Ei5{NUcd`R8HGCu-Mrn^IQ8rSMgIHJB6@LMXxzQRqxf zC=y0yYA`t>^lNh>pSI-hsc(-f(R(w%WDRC7>vggQ4ZZ^>Zed~+i_@zPplQhTv_A=i zh$sDR)^!g_z*9epZMH6_*nBk2b`&!z5pL zmObXl<{x!A3dtIFg5v-=ZEnWpm9h!%!}HFH?@oFHxb2LMG9o9w=@FFi=cMEwd>u&g z32kpi##eafPE90-_l4M^QilofPuDQL(u8S^T+m%^wJhZr_x9m67gpUk>TF&T+44!G z%GbY-%1f>Qiy$C0cxUyexWWDQD+;`+cbR_4dI=TK(=U?`nif@%y8~;e6)o!wJ#Jj8 zX0nsC|B+I`TlM1`>-vsi^UIq9m*UnQEqngsi*k^Isdj%?wffDYgX8DUOjI$0>zMja zm-(a2S|M>48YsL&%YQIA?v<&IA5TR>2 z?jz^iA!o=tA=@?_S9ehl%U^%Vgxud?8Onu6wCGIh`5m&Ao6%4Xja&V~MDY%%OY zDCUs@zl)Y%5FWRVq$7ugElGiMGLXL1{7Kw(j(G!Di{M#3)r5Eo#njZr!TsfdLu?)? zIIzDtA;^fsP}9jo=JPW#o+#qwENFrlMCT;p?oVeYh8;x2F$<6L70n3(RJbI3J`gj* zAqd_q?a{q|o|i!9e;4Hud%ZsN=q#JCe2xh~?Dp*DFh8HJ2qK(rt_Wc6cDqo`DDfxp ze`*q{g8&-*IxJ(-+qJ3fJJNt>M`W(Ko@IGy6Jor6f>=wa{%G%E6X#E-&>IpBikw9) z#L`8Q1QLI4v^$IKc`=W~HzM}n&^-k`9)hc15ua#|}5 za+7&#+*m7KzbO(0#XRsH%L_yyJTLew?!Z(t!Q7T8FIjZIXkHEl^0ox#SMipA8Mh6j za*1nt;xUbNd4cXOv5f2xb8k>8fVlMJY$~1nDo=n?3DDUM>E7ZzzAwW0oj*$dOnkwk z>61{k5ViK_u%l<>iJ+Q(6<}VEL1|+e5y>TCxFSX8WU*NAD{2@GBLj}y144fr211Jb z=L!d@Pg}za?=dkqB;GNBd>BXnvMhK=?eD=vfLA^8(})0O3E|1f*e`doP2)*RW+I6- zqm-!4_vtOOdG~CyfKhg_pL{dGm6?HL8N`ny5VZ6HCM4*%RiILnxk6|I^13kIu&!iqE*L46DbCI#99%b$<>E*vx5X-wt zm&-;wBPxVM*Z5;(3$kfx$o&J09aq_^g>JlQrS4q%QI_79YBp}1P*sn@e_>vm&RZn+c#Qlp)JyA)z?56_iW5N!fEk0Pe_fz~g!u1aj z<=s1sPo@NI>Rw}J^9YOF+}QZn4Z+cnOA7?-Pv?`OlKafnfM&)aftZb}r zS0XTPd}8JpuBtEyr3&Z4J+v=pr0na(-OD&H0|c*YyDjN!7QO3uh-)lraz!7|ZUdHf z+zBn!$PzpXAWNGE1t^tP>>Q2<5n6mkaHnBf9je}XSb|mE&PYB0GSyp%?oC*_PMWBh z%ytXYm+55A9^QA&_eSTc+dy7FRo`u`gF%MAzDXBsI+=7mj3vt4BRBvWD9Ls$1YJuz zgy>7#0t5EiCN#W(n#Gbhw%xn#XvDkcSd(0eayfxZK5>0*RY4%C*P*?Zcl5^D_k8kC zdiO@DmPdqA2oNJjN5u@PvcOm&!(Vyuze8QDVm`Ym_$YjWF~pM4+0P7;z2>L?j*&^d zKTzn5C=099{b3GgBT3~z9$eml>D+qd&Jy`it2#yWk&5VymiH(lR_m6UEx+l}+0tE2 z4V)H^el}+tLk%IsP1LEfRjJ zPV--28UcHXK7EB*J0Ri@i<`1`yk+~*mu8LIP9^l~?Nl7v^+b*tJa>h`?f0kYR_`yR z0yr(}#+0BjrZmJk8QXJbtr4KpXq>^$nc)i$TQRa)w< z&hr*ZkZE}r8?dx`B>D&Ce|F3P=<{IJ}avW_lOjMIY&bHau9BLv_ zsyQS|B9!Q`6Xw{Q4=HmhN`+J^)tq&v!#kCCZ$u%z-xShYpSRz>zklGmUANn|>)Px2 zd_3-VI|y{u%Qak;i|7>4&HiB(C9_tVgNHz?85Q+ml*=e&O~Ak8qv*0rPa9D5AZJrC z!mXidhypmxi59!FQF0cwEl&kTqOV9#9$iC;>)2RvZl*IwhV#hQ-MI0qiY`RylWKv^ zIe|yTk!{dA1y4|7x)CqpEH~ht0p>u}gLZ(G3=*31x{>0SgubC=mGOgum_r+g-2AP) zzN+NDPHErK8Bo&d(apD;`ITu*?mjZog=-dB!J4+2xkts|hMw%i5YJW0Z5qGd=PRUw zMY8!G5_ZF`4#}xm$?2Rv4}dKo=LSB`{ig-uXQ`z*1R?d#_{A-oK zccfnQfWd+qi91(Jr=iL!jmM(R4JDo+jm4sYu0aJKfU7<6A{-y-W41rA>-hHa>uq zw=K0+ddy|UB^qZ-u38G%Y=tS6i@TWx9wxiAGJ%&V;?h+IL*Di${E$|OF6vJ8W3?>Q zsIK8AO6@KpU$N;5%DNbN=YC4o%{<-(#4d4RzW7z5=nh@{<^*_(RLp1~Rkot&yrS0( zajh<3F;(^};${_JZFxp))2v_pukHq|HJJ+r;OIt=6BevFgAqJpL}9Fhyf{$X8$aJ`4-FwcoWs8C5_V@@L@K`jG-I0^5+qB-^SxRXUtlwMDL8d2 z{av2#NmV=c@Zppl9V4P$<(Km8-=L^1mGgWXIkM{Iql5*79fZ7M-_+B})|2NtbIZDK zID#_0EX$nmZ{lASqF1Yse#%$ts_nx*2I>w>l)c=B#dqKg4s3;OH?>2$6pGO|h4>f_fmvC`pJCI9DssMO+9JOCGu?hGV9m)~?IU75Ah;oapm?r~Z%1%$D!nlj`Z; zJF=B8aDmUUnnlmAIt3ddTq*X*zdc_0_8&C_)uj-IeHCf{&m!!(@&TJxf zX>?%PL2N}(AUw?`_B3%uk=Nb_P0aDFnjhvBeT@C{z2aSfmkLOBVW6~+C4E~${g6%D zee>B)&Y<$?R{HCg#{Bb{-I~abr+?l*%*wRyxHwkJqM}Fx0+!VXogR`sx34cL=?@=E;!v^U z*>N_t9Z|dUBW~X94U_-YRD0PgIxpUozL_&x-_L9H*&2=y|MW9QvFG!<_a18#q>*i} z2u%;SPmxlG@_;?{!Y+NY8#|}%|4gbw9LkmqwLCNrxND_pA0Ato0^C4 z4Vpg~qbu(b?5T6JvU8#q=)fs1iU-Uey|J}Hq4Ur+(cP&U=y{7G^o-k3RQtw|0HDf{ zb&=TxEeE?~w(+yUjRxo3r^;x5o_Lu{hXd zX5fYQHaTDXm(jHt%N)70n8B!gQrBj~H#cneVFDm2O2k;Dsv+gb9%r!~UqDNC$d220 zP6Qe1f==+h@A~4lU00>C&X>k}`_!tM^IYW; zCrHAdVdD8Hxa=2v=?{6%*!N49**BT^LihLz!vDU5U3sWxKVz$R50(pNA78i+s}I`$ z*rHx|KMJ?yjFzXy8~sQ9TltDnmSAwzoL{juXb4CIn}?b=C(aJyJVt(WMIoGv!HxVK zEA1Tm&o!@t+|LHSaS`_rz{0Ix61%uJE9ONVLM<0JHTTdS1s={NAM_LCJ{eq`lqDKE zOG~*B>d>NZ|033kZ#RIVN^gDUc6(U%9O_?+dAoG@%Tnymr6Wo|<8*$;XB6&NXEr$x zxBy}#`FM7_fF@x$H;VMHsu7(PP#*DL8`ls9Q8fyLjSWshGH>MU_;l#FWa_%lKVt1c z+qs-Ydd70b0$>7I1f3Z`=%O4&1Gx_u=gW|H?ljzqN2T?{9KGaxO0uTa@we>cd=6?0 z1rGWvX)&(C?{xpN{@LxT8CiWh+6U!oR*iP!-vL9j(L)$q;VSlzG*INW?6Y*~lhh*b z1?Nb?6~yvTRllm;m+8Nrow&rAwABM>H#$>T9gl49np=xx{20N6y71DOlAA0nnjwW^X zDv0nPsWB-A4$3yss(o9*-;fk`OMxvn*rjWE~tPJx)ox~Mg+Z3#>UeMqjuA&jwe>94$Fzm z94&O$4$?GE3kmi)SJ(_V>~w4NTwcQ*1$@dw3~kLub%zACg(tTx7)MY2X8)=m|9guW&YDvZkf!>bk#Z9O6Znf^ODk=NgI9z9hyF z`wl0fk9<;$TJp<3xp6`oW}=_Xq=F}Su@gW zqiGV^^$rqmu>TIZYuu5-epUC)a9}R22IT=)wu=xgCrpQGSN9|xmsXEyCKgiweq2U-jiHUjo zdr%`(w0>N2)Br1&+C|W^c$WVSuVohogg`c}5}!bJt7ieJbFx%#pT3!CNXs|4o+-;s z+;CJETaP~fQIM67kkEu8wKGK7mBS+KNu!hJ2V_APWBdxzmGbP5k_+OvDMJt|r_;j_ zJ46MdRK60i+N>d3OHFv|`^_0C%p7t$ni@8YK6^FEPrGm#w=+<6bRJSW%w~XhVn=L| z>!fj79CDFv*shz7qM))jw~gcxMXBuZ@^!04Nnx8pl6;U#_d-9etVM+EI_ZbYP3 zs2pcw^;e$TWUl?sNCT_J;?`JtRFQJ>yEMLy8b3+!*`A?tTc1A$L~_0Odf7J57Rbca z-Bze<%L0qDafLmtPcHp4`F3~QP)raafAsPp{i22UnTGkxdCykIV+|Tl-&r|yQt1~7pMcgfH`U)BIw*M3B*MePLdmkLWSHiq9{bA3? zRPXn<<%M|Ue_3fSRz6DAbc!?n)ZFP4sr%!x8fji)19YO5B+qgOZlNh`r1}i%x$buH zU6t>wq`L*|uTi|3Ym*m&Tk}FDVJcmb|LdekC}N_vAGLkq+qUxx+r1g%RP_GW>m zek;`lB!uC@so6{qnc2OlX=4XngmJSNXQOEB#|^oEqB4OpU$gyoU63Y`i0T+3Z?7o$ ztA|7fb4KL2Sz-Wz{M~^~ognEDyw!}}=&AZXwpf|ZV(w~|JUdm-=MSTXsZs15(Na!) zBWL@|0#|>B<3aqvC8u16j=ZjYm>@v{d7ODIbRKXM|V<8WnCsggO1KV*J-{5B@FP zFeTLif!W(2tx4V0n$q)x4}@46A6IAYmKPv~NhcUy_4ROLv>*leRm`xjQH&6Y9=o_1 zRz7o5q;G_uV?*P%Kf#1NINU4-m7mQ|PfgZy4~F9-vI8N3{m}43|P`J&gx?*r_0y7jDSX}X4L^%2eqSe=0;O{qQ1rHzS8Dh%c6+aO5!B%IwI9iPoh+(co4>X z5tl#Exu-oiBcpfiwTlTR2wfhZe>VpB`6hc>IS#Qro`#`i4(?HO7=bGi#B#3+0&eE1 zZI%JoCZ1ed0>Ce-e7}NWzrB_I>lN~zO=xcOo}?WB_7mDhD9Y{_|2nSIt-EQ%AmZ&T zbxe1ihMGkSLro&PKo{%wpU4-$TN5`EjU66sN_Qtul?Iut`;HV(tPZ58b?&8<&TQVs zpqAX5om8dM)njP?lv)J=q}QS9$C{DL9X^1~M5ro#qxJc(F<+cMIlM^uXkE23`^63A zIH*94RjVAHayNDy%G%qOT`W!izCS`WGAd!OjGx{;2&~bL*rE24m+qj0hN5**rAoel zNzhoQKC(O9IsmjGzmb-IwDG{7nsn3DGDgSE6wYfD+|Qj&%wyt2XnH0qNOfpq6mxFt zmjLoi{e}y@vbE1eIu)|9zT?krl}?Sec&B$6=>XyR*BCa^7nF>{P!qc>$0s_maYBd&tkC5>)T=;^jUHT}ah31X88+Un&2zR3e z!fwg&Fx**Mjmn@x6E>i29I!A8;9b(PlWD3)&N}V0%sY~!E<=Z8tmIxu*rJw4ToYUf zX%9AcFCRVmHvN~fevQ92XSp-XbX_v;pUF8U;-UBM!mnFV`Rn#JtcnU%_u(A+ArzopU4QyNTtQHe-Y?QT z`G4`u$E5=!-os`i6Z}TsK<4Fzuz#i}S1g!woNsz#dPpsjMrSWQk;LM&0>1WMt3a-< zxS@CXsqZPHtI;|+@=jKa(McKhe?gsUIf&0s)A!oHe8WLvC;JFY2HfxwYS_~>-ACpl zLI(Dd@m*Q~LSMq#pTjq1A`JjR!hB9bC1{)@fGgqNDJYQ-M{tBjkU$-HK@14=vsp)G zN({NebrXQ9eYyt=-|;Jl{2GDNLinmO{@~)ZxH$)?z_)J*95B{MlkW{UM_k^w`mS$l zvZ6}jje`(r_DD5VejaOd74{Y0No1(883ft+C&>e3AdG++pV*VG&Jn9=J<2B&y1710 zXacE7|LMEV4x9R9l3oxALr#>{~ca|D-$U~&|+DK4U^ zuWgKV6B}md_4+i}HI4&}0&uLjmp)pVqzNU1;Wasa`GjG*)7@G_XGZ2kp@A~prJy&@ zGHe$-0xbBRndk*)wv!?Cw?3VSi17~;l{>Uds9+Q^@C3XJA(Hv3Q+y3xZYC6wk*uk8 z0=>3{zUuNvt$`2Y8@1|364G!`U0cp~KRB?c*Nf(oan$W*ZD>QTPnIh1w3!YAbQtWM zokQrYXF81|jNtlhv*N>1bU#=S7Sq3V4_~B;oEsR6uqRF2VF~8-!KMsJQ$IPNk5psK z@e*_8>U(JsyM_@61#pmpMCp>dH^AgJ{)uIh`?!*aJ?A@dJpb8gYt_z_}T=cno3#kB$g!K=``HQ7?b456!}NnT_671Gmtq_ z5kMC@%y^JzY5sQ+xc1&JG*S-B=(?d?AQR{Zzp&Y3SPMgFryuEh&b#(@RM*rR- zT|-qyVo3Gk)jeiYfQm8$zi{zFoT&+=>!8+OI|1akhD%5E5BOECJE#VHi(cz7$lp2H z50}&NWqt@b8*x)3Hqzwl-24B9J}PUcT}tdCF}OtMrv{7fHFGC+N}uttzV#7dX3ZQT zPe2|-6lkH3x#kp>nfcar?wX+aXC4S8sd^qmeyZ%P8I+@&b2e%KYot+#3BaKBi`whX ze7#;{=dL4DWrh)1IEVbUT;SXJUZ4kIA{YEi;HxH+LM$8dK0jMcJL&Z>38`JFgrGh# zUZxWX!b1SVcvi_hwEq0D4-5aFVp6htkj@xtozU2*jT7f3z&NZ13iyNKH?XCdfC61XK41nGS_ z{7!{|_kdO)LNYON6YVzz7&C=yRr#|U7)^%qm$M)Oq<17SDz#bvVlC9BqE&j*9}G#} z-+Vbi@i-sXPDwIA7z1fV0^n8 zpkNRuc8s9k2-38I0y{WrpfKgc4t>S>=Cws^l!BGaUyBz8(HKN61k-^LZTHBSxfAQ6 zp#Ba&1U7i!`TDHUJ+q4P0?C7~*dBJKcF~OkpbxtOPN*V{?McoD8};C8Oag$9>J=;_ zIH$ac6IF=z&Pi7WA!L#mMj*HfYd&N2qtJmgddqJQLuceQ4ZvV7dgG~Uuk0D0y~OHM zg=P#yRV=(4tTK3pXnV<8HInM#oW;Eom|RC+ci}l&&QDtMv!2#p19z&aj_piV?9raY zH5edA0o({p%9=h$0#qe=e(n6bH|6H1&`9pntiCiAZsfmNdDd8jn%2`5?fk{AOk^nd zpLQ=a1>oeg##{h!V#PeBH4=e-zTkNmO9iB>O*}>xiWfHS4UyCKcw{0p=~t&7EB%#* zkOTX}m_NT4`Y+R9AkL#5NO{x0z}Qqf`F7Sd^Fpj-Vk&BS`Caa>W{Wi~B)xd`L%!)+ zEl=@QYW-^s;xdg#jrxqh){aQ+R`W&4cA-M2} zZ#?^aT9vQeO!Ndwi4W(|&XC|6nxL9}a;($p?$b3RJvFy-n)#rSR?ow*qXxUvhz-=e zIec|y*ptD5tgFhZcqHNV_r0=AlBzONQhM*!Q7e@IVz=&QgCCm{tT5&Ye$Lj`-al;o z6Kr??unkYJJM_aYAz@4Ek1e7E`|Ka~g$WMlemK-7I9~bTcq75--Vdkl1m`C|oL?ol zeE8w=DZ%yY57!?FZYw|BP>D2^1)63e-C%)koXD_PU|1)*J1w}oCo;VknEr{Z-3zSn zM2|xYs~!o7o~a9-qC|G~0=qDgb8dlCo9K0A!Rtn%_q_%0?nIv_3qG$BeLpPteoEZ> zbz$p|#BD1J+fYedl|`7!^5p^qE<(f83 z|AT3rgFWWwf6Q<7-S+iK7l^h8?=2k?wcVb*X;fjJt?=jZALtW0XfpfOac?Gh4t*(%X`HBN;%qtf6@B8v zPg#5C$kGOnW&!_461PUN2Hzs6eLm8~dcpg-_lGTZ{`LWb-j__qtO% zo~(4dO6_>)Sb#;2d@kSq*S*_6@7=y`>RCv#Els&^@VnEvg9(BF@fWCo3y}az06+rT z2puEJ|g1D4o%CW0jw(InRR@Y5Vb)vMmNYB3>6X5DdQ4yNz`~IUK(stLZ*pL7<&R2>Y;Fb ztNwjS9?8n}aD*iu#(qUlF?3zO%( zwF2BrBj%JZZ>>}#KcXGfx9Mi&;k0JyH*3h37&Rq?nd{oV7ShWka_V5qp#Fm7f~p>) ziRPBr*2VC)yfSzPeg4VDo8lc|g97BxlR=XM)QE3){!mNAlII+^?7fK1tawCg@>sZN za$sR)Q2C!Mf*udyWOjsZ8Bz3=rlT`KWo5qJ9q(<*23sJ#Z0|v3y?-JYPhJ%a9;N3M ztsKrasVcnzO{%^A{+yyI7QR5TEC-)j#-%RMErG*~M-E$jxqT+;_icudtXwO1>ug}b zJJ4+HEf($U{lT|N<++2e_U`oMKKD?uX?xFj`0eIM^tuS)o2VzNhD(2;hQ7gn3e6(y zlqJS_TUe*g&Ms$~Yq|O>pFTtWm022IpcZS2ao_Al2~{_gns_sTzKRd^bRFKcM|bA7MKCz(&Gw1^B|9XLP;BupR) zLWLH4znO%>S=2M~uRwL6Zq)Q#+$1pLq>QV8cX5dsmfmn^cE&36F zP7IGBQCJFX<})?4h*)$%hpJsf7RHaLqBG9WeJo8!>lt%8?4=A>KgF=yo!p-c;OvDs z5?`R307<;G!X(BpVx2Rej)TSo$TCrAfHAUr&JT>UlBgl_y_puL`N6714!R;T-QkvQlc_MkIJ;pj73lt}xy1nPN<-8%Y4!dC*)#DRi@Kjiy#K9t6_7 zD5P{F`~BM5VM>wz9#Jo!>qeL8l$EK`#FhH|c`8n#tvXs>UiN&eIRbKPU%Dxf_sGHc zOqW%f+*ZeXMjQQI!imEvpH(Qs$k90OfG!QNQDkB$$$U;>7OKCy3@H;N&y}~~@}m&J zEIx{DLpt4aPkdP8pu|UFl0lU+-KXu}pD6iq9of=D@>(xEaQYCq#ej`VFAnmhnW~fn zD&QvqBkgzm3B$#8M%xYiNj70YJ=tT%QOzBO?3{BI)L4_eLnGc1VG2F2m}F-mwG2?W zj0QKXK@Vblfbo*3b}>O>f%Ax2J?iIS+zs=vb1(>NINCUbyD?*i;KbGnoxp5a)w&Hl)=7Y-ar#76F9| zv4p1DCg^G%9gj-3X(sZ=NtBEGwy4*gd^i4?{IvNK{-0s~Co7aGY?a~lqEoGq2Z8}P z7{p-7Q@334eUF0Aj)TjD4f0EY#)9p$FZqC|OuTgKUdQhshI(4eA$n(1)BgQkPtCOi z#FE>e5j3Lazy^uByW@eeHB`$KJBcB+^FfDpPb>PC*d)qa_%DJx)T|{@?k`ODX^PjF z^7tvR-!SWyyqloI7hm=an;BLU7dJ)FJQvElt+#ncmNY}8A5oq0$|HPOZuJwl3zofoj)<_~a^RxJk$e=u&HpL98QZy|#Qj_x)^!m>lvE z@)DmtId~KDUAui-ZB5}kj+ug*PrTv@@7|w+4qS)w zP}GMPck;|)yF6k@eD2Qu@b$KMV-w^?K2$QuEt-S=u!Czn;ZgyIFU3A0VQr_5`;n37 zC&EZ^sKAUAnvCbfhO>XV*_gq!L8xRDl8i#7RI6DJAc}x3YKE9Qu=4{X$LE}Lno=i z#0X3_57UQ2DN~UART;{0ApzNvHW#TRItKZ}F1Db~X$taj9oS_Yl*5&%+Mu(VeUr9O zb&60~(sd(~O4*}OwlorD1)b@|WWl1Fuc5p+h*{+B5C@%K#&rd7n#IoBv<_z;_qHcP z#nLl(UwbYUB@>S0-n3`n{D8i~L#qk7!w{o43f9rhRj$qrn{ey6t>-Y2t4?PYC^;J+ z&0C9LkqB64%vl#99AB*$Dj2D`q4=?+CY^XZ-rFy|fY`!1OqjPq~Ax|zeS1Z+^F=4q*-7mR(; z=4R}PZ8g~i?`OxG=B{Z85uw=tn+?iy_M5{89>LL#Zc2Vdht1(5caqS!QYN)De*zAU z!xqlv3`>+sX;@dSyp2PpLOwQv19KH6N1zxb$D~vD{^t&+%MT-~!8CbPx*W=o?b>qb zT&@f{O%?E)m6!ym&m49FprA4(tSKBwg>sr9K0jXZ%M+|ND?PDBkKBQ#3gKdNq$&@S zHh5mZ1^fJgQgb+&3pqz#n48|O(pjREU-U5&2%o!fb>LVRs-gxPkueC-%V7T#=ZJd8 z^9b1ma<4Q%57h|>0O;Q~jZ?vC5&)4FuqT{<&J?{5DOV~VO+i^<%6N)_Y=CPNSH#Ee zL1W8Cp_ZSFvvW`m4XbF`<-yo&0v55fV?q?0H3${MSbrI9Gp)`c0#gXY(ao{y^>A@8 zkgC|HVbb!sZVTo`eg0l&CNW-*>$bl4LRXisHWn35)n@@|N0Maad`y)|#b!QacNwz3 z3#BK+^;jr_fL$?zBc<3BT0qmP7WOPJ!k_1L{nw#4^)Xq%iBI#1QrMjYAabZjHhN*N zDWUTTMI;{DLX7GOKd!tCmBU2dTmTuwwweN7a4j+(h^Ao+`rZ8QLR~*fdaN-8m+K2? zn;dA^oH;L5zEYW#5WZePIQJ4ry;c^XkA;}RX6SJQla&cwUwf@{hg)D=nn{pSF<~Q; zFGFeWJ~N4IZiM}@kd;R^(yzf)-az8kc1$ZlHjO+gx)6qbTPu|fVjKt+$1;u~=~(MP zc;jQn6Jw}c#&MNmXxs}$-1xHuY~mOFqPaRGy`^H}dYHh|H~_mc<@RvBbGF>vApnSy zCPtoUa$B7YIeTd@ntG8rs#HAZoDzqfM0lpbt-mi}OPfQz?uC{Q1`B!EUrLB9*mBnE zKt520vO?w&uy7RoV}^6_XHh!9bnQ98`3(K))i|Z+B9U##9@WJCHZ>u0q0Loesczo1m=-r4pGYH66b zW~IV7!U6|tt9)FsJ< zRt+S+l3}+}DS$!K0#Ne|6Oa9yT5g5LV=rPb9%2w>31rwvE|51`I$aX0t9LL*c7()JSU<6L6iBe^#)1$yNx)t?@j#Xe zv9qzVBIj(v!yiuA$Hnk}(eROAKsqY_vD*muC+D`u1{ZpKzFN^-P6A{X$k?Qk ztrsZpjwkmXq+;dX@V?#Nt$mmhsuGHJt3TkJ+Y*#LSbZ?{UgGLWY%~G8KD;~G94i=A zYVGU(=ncDBS{1_J6U}hXkoJpN?SFrj%z47mb1i;&tPDg&OV4a*z8P-`N6qCi<-N$z z)7`dk(J16}uEaPFDwezM&$+>sV&a?nVrKh9Tx>z|<@5RXsvV5{97DmDn|lm8Ow8bX zW$19~jtFl!!4Xp;3(08I7+gu-u?ve8AICZ}@-9QmDlMhXaJlMJCK>4kKdf?wzh3F7 zwL})q1$LUB+3@D}<)$OMcMBax8$8po{DVGPE8!Q?v6735mlU=Gf3>~mXb3N+AkG@3 zfoB!amC9Ct7jc9-ld>!H{g;L;DOXQ}N8;>q^ zWE?wHI49f?|GX=*v{Ly|YbCav`w$%JF2mo60_y(E6xDFrf8!r}{d{qZ|7Owb^2v!S zN-jXGyh6f<0wtBdE3q?4ol-cJOTd;iR~8T;#-$-75t|3+D4N3;hum#)#3<7<913ikyh@@@JkYen)P_!LVz;`{tg^VdcY#j6trXU;M))5Q~ zN0mNJjZ7BxS5GMIR>7w6<9$~Hp=>_%iU={&mC|LGTTeog&uG!y$ZKog{%ANn4svnH zOrH}*@}L)|^!kjQ%LevzxhQB$N)kCF;pS6;e2FEJ6$c##64OSN<~x*dDUczg*<(u& zlwJd)x8(%Wzelmxyz_ix@A23s4=s3@&<1m3wrL=arbOhB#u&0M8ifw9wlCJe76-j^ zh9^DL<3FhjT4CbE|Gt$u#S8kLC>gtIX2fCNSxOCuPL+SlxBCx{hV#p?jIpKMr=M_) z2b0>m}e2)3}IrhKL?cL2<2y{#{s)zN(K!A#Y|3q1#A#dY2#cd<8 z8}Yc)gCRYulh+N#YT|YtSDhlk08}W~w}ZOYk0XTq_v-OeU+Ur0(ipwYDJ9(QpT9{- z^4>@pSavJM68Cx2HJIs&t9%YuJ(zmoI8)A@aX&qYpXX_tI-hEsBq8<$HY=T0U?&03 zu5e5$0LAe>c#SGP957{6Y49d00fu6~ag$*vkt!aJ0wPC4baSy%L*dzExEFriVH27An}SYYqmKcmwa7JxoYJ!gJ>3l(H$UCK37g8=Rlvvca3H;R zF?(QkRSfu#T)9`V z3HBHa?iz)P0PJ5_v_y(MDNRm{V>$9M-<7h!F@&dECS~8g=ncoP^9xzP4u*nuW5&zc zi1a5M*_MKkKUGpBeN*IDRK{x3W#CJ<|8-%Hp}2izQ>e&MXlAwY;W{!hiia&9&2S}z zzg(&z#kCa9@xN;>mgeuQzoJAZfP1naNyjyd_s+M>kqS_IKKU88hAgdjCo$_f>pZI& zZly+BZ4#%s*_nE_u@l37;u@bA5aLF)@FCaJMn+s_bOz*94rUU{O$$jFZL9&l1fa8`uy@~M%sFTpZ#`4 z!#-ncOUGcyV*TM-FPdW+k;!Jr)_Nb`}29fp}JD#BkWym@8!7!61<;; zwDvz&>rV~U7PxB|5+FRYG&^zbOJ&UTK2y#SMVyk$kov%d zJ|=0vkT`d>sKL047~B2-8Td15f=kpV1y3L) zc$}o*c13pd!`X|eQSIYT2PQPUx>T(-*%3@Ha~w}MqQINvP?w847JkIs1e1SM!)tT( zk^4U110rhZ|7sN$c-c5yF}|(#Y?T7v4QmzMa;HXbmFgGEde-2YzRPhek8@DEvwr#a z4VqSZT%x;;UeTZr^4&p_JvClL_WC{S)s)j_&EymI7fumTb(1?tGfmxF#XhP-P)@$7{x&fJ=1;?%jec#l4yX7L1@zBx+66BE8;HoKQ|CVc zO*Sp4HP*vfip|)IQxd;hGlR!TNVRGZ!liHSR^y0ifCgP$Ht{+~xrK>y8iuQ-8f!P( zTKfjL`xxtmN|`op6hx4stt0qOv|R6S<*n8p)UmTJM@dUx^h|;`H;uQ;>tHZ%*T(nJ z`%?zq|6|IDvegfnT3xC>61_Y}_AoUk8QW7&HSE2GKM#HUmlSrz#BSt{$sz$xuvi&P7*`sNGL20p%J(lR~TcmR^ixbxZ!AljHE zxz*xIb}6k|dqtSQXL;bau<@3Rk*#3&of2&_F_Shb*#0k0y<#Q~>oJT_6E%RO9$TDK z+avWB-r&d_H z4u+)OpURxy9+YY644z3F9UxhTLQczLID+|`|41(-mZUC{x>cb{taWB)&L~{U?J-&i z!aC0$aL5Hgvi7`1+AP7%<^5>0>KLRy(16R}Z`Xb>>}In}Pau_v$XiX33U54%Bo!E| z&9A2pMrm01hjIFL=sLu0}Q8e`W@~DIG9+j|EpS|phf-Z z+@qxLoCSEy5pxHN*6R#(zRg=UXDoRrF-F;cp1zA(v)ss-2jMbB%+}D5Hx;MEYx_T6| z-7^;;ota$o z$m8Xcb4w3O9lKjIO_5`-GN))774joTHWBP!|L^z~)Uk~-ZE>Tyl=hxR+v}n?AMzm! z@PXdYVb2&LRzH$#T0RwbgfTsDOln{*|Op9;(0Mk znn4>LJLDE9@rC27O!}L;dAcioPGmw#r-sd@1T{bP*T=QCMAMgXB-V5X@1{e zVaGxa#y?vIM8fA7eR50=5co(hbTi^5oax0T`J66q6)S0*ast9M@)sn&OEkvSU7v@S zua+bGqpo>q(lyfOLhevnGM?ukG)kpCWa_+Gn(7Pndp%}U))(Ohu)XlB^tV+ znc1$+c}?_*Kp+FV!MO+LvUH_mfuCk6!>a@hBFS9`bgV}K<$On84h#6}D)XNT2VHmL z_4zZo!=U;}K!rtGf_|^xq~X24=wGcadoOOJQj`QFamJ707ARcx?Vu?hk#5*b*Z9Th zR%`RcZlpwHr_Kq~X1Q+rUhdv9iC}VH5^OhGkOwyS<-*(>w?)Q%%2xiBN^tzQTWA=_f!B@}>0qslc|AA(yqwIQq5%m5%+#mcOdna4(%iFHv;WDkuP?bSmec%+6h!*Y zX@W+cE+Vz@_c)7Ml$p=`#?R}(RaFfsZ^a#kJsO~Gr z;?BqIBi$ur=K^6GoK0o>ZP0Mfv?&sC=ZvbR>=laOJo(T0$X~>blHgP6mL+?`IyI^y zUuT&e4SVB%-v_qcBE@bSLg`k^IC52*1oUW@BEh>43Q8H0+>Akyju3A6qCo)rCU>8n2K|1MLUbFB_bj?bQ6kA99^4Jke>P5nSx-r#13eSX)kD zkb4lc&CJp3nQ;j78Z2$F(m^aDwS#tizDFEB_)xQnrasu5Z58EtT&#+SV?G@ayTF8k zI2ZFLP_?E&Mb`nx6Z!BMy0cbx(zUvymA&~DnNe`eIpH28Xvb3_{FdyTI2%;STD{pI zIv(SxRB?M7SW0L>xl6=8lwRI~;IgLpv%YB7%ELDZ^q9}SI#`;EN1X_7B84{{tZUyu zvdHRsV09U^;dWdr!_Xx30cl|k)>Z{P2T9|)2NBm^S(_8g? zH%-hQZqyASZqY+|*~>l`>Te5WbK8^=FPj7F*x%Q8Wk^{usW?G^L>orBYhGGV5st^7 zH3$MrJL#^Hjkp2zwB&r}uO%hSOmAAwhRZ1Aa7V!-sC7pB!FEZdPqUQ1=7K&d0a0o| zxzvf!p(le|q>>JA!xQscL{6*%g3u+a@N#7)E-G{9T_P*^^1a_)tGEG#c~|ad;+?0} zTIn425UiZyqjCkdF-qvYYg2Wvk`q(xoQSMaLf-e8XZgF)N%G+7l zNp2PXyV#Jn?)kv;+sNKIfpB5~+F-9g!e~{RF{&SOO=Ii+MCkmdcOginDcJq`Tw{m( zlr_5nY{~chIVy}b09|>t6S^V}SN#5c|H=64wG{8P0r9s{XnP1dMT`HU(8O7{GP5p! zrh`p+E%Htl6aU>@xUf}+0NA(JIG2g&631M7@rg0i9In5XZR|_|>)Z?FPgFR`Vxvxx zmwd=&dGq52m4Y8WS}5NhMHo9(?66y!@c$S(_kSk)KaTHk*{%(<%`gl*n;aUchG?5% zPBn)_sYWVFMM)}kZ!>Jpa|$VQOlL`@?(W(&g;Xe|QX!Q}rL+20zWW2NU#`dXxvuN; z{=8n#XKGi_3=>)cohwaSr0L?$MaTnKcRL2o{|(FRLJMp=2Zxua;`#DwI0L7F2aegmuwFdP!9|>5%^r{}0R)MW z*@_jT6Y!l+iaj_hgbL%jd9p|AukcIcQ8Z`70Mshc{n-F?qMaWvmCXNb2|(^ZeDPM~ zGvE3YHYriJNwg!Et(Ltj%qTq8_Lgo5VZU)Z1s_||T<(%)6P;~*YSC=N@--uWgwcw_ z(LFohbADKPQB!o`)99l2(ZzqFON?VwHZi5nF=gH{<)JYZYhp(7Op4Xgy-z`#l$AxD z(!Zaj`@QuomFTA9Mbg_bl~48NK6(Bs%c3d&w>6;GWFb!>Kd7L_(<(U{))5bHBVGOt1n(s7G~hOZK79fb=JR>*dmw-QD7;qB5_S>xDe`0ORcx$ z!Y$BpCJ+Z%<6H%k`V|!nNXM}9h8YLRFq(mQv(N~MvEjs7){o3UcSGK3r&Igi?8N#A;*r3$CYMp+8tlTU`oCFZB z;QRcR`uCuKu`Ij4_p)v~xvS*?8u;QAqu-yeFLDKZL^A76<2jz5aMJ48eNy}`5~P&K zmnKR5G;&`F+$#|X$tZad23vEuuAmh@zjEbC^%{-$lgH5({IV!P_`iYJ@#}?IK@z!l zO4fIoykJ;yF)pj)c2%HAX7L@WJ~`HK2xPS;1Q%uH%?!8BUhq_Bz2vgL?qP8hgtG*I zg9Q-J9L{rr#Z_5e&+M{0C3P&4y#P3nRJfPq421vz5?SL2^$rm*gkKMtTAk}2P*Wuv zYEpVc^Kls}J*d%g$|^F>R%M!yodUgCB`zeZ&fYTFTLgq)V_{<()K9LR0>_`*U@cQF zb)Yvn-aBhXIee9|A$)IK3nv7}mArh>5m^b$4Y-Cs^N?TK3O%?pC`Nk1zegV70Vzp3 zIv~DcLwOqS^9stjG8QiPB|jW^j0VJzJ(tk7pP7EyVsNH*>}`X8l^qm&%LaTaDjJDS zRV>9MK)tMpw*ajOXf4`2|Ei-f5jgZVK-g;Xe8ZD^brIyR@}@z9nWZ#9vdOy@SfDqL zxF{p4VaP#6V^jJ5;+vk zb9Tg%P&e!jwal-PNbR-({poU>vvN+YBI{ODAh+%3OtoqN*eVp=dtR0 z*BVQSz@KN^c~gsVBhVmDKH04hCb7s=;erM#mu|EV5WV;10^+Z$Lq-%M51@!=+y7$n zp4wV|lf(ILknBeZXTMjG1|sF=kV#wOFDvNnXNt%pZJeqPi>Lj5Jy`rcM{JAV!Ym;8 zYEu4GYQ8*ZC28PRasP2LLT?WJ7oZ1rkR^vh;7gmPC5;0obks-R+a?U~xozS=>=}L|5O6)!vK@+9_y;3amHwzqVor&3U0i(4%ps=U| z@2uyu!NRZPg38X+D^}rWqYAlGTlk~JKT521&+<-zd1t?z8kli_ezch3ED=R-i zcq#Xij_ZrZ09WD&=q_x#IJau|d84jiiG z-V$oc7xmcArRP8eoN>3s*OCQ0aRadRGPoX8^Kt-QN=(NoYz>cPS%i};^rgb7zKs@A zzghgti7$@-yXV_;I@0GL(V{0%0yOgB4yYf7O1U5M+eJ&?muAToFuv zvL7hfG}v%?e&EdK&X2hgYCuN5kEOhpf#MASr-o7foxuG@IbQ>%HUanlCHje9zsn~Q z?|n-q(xF%5*PyXZbD)kl++5^DMr52AT4<60hQw`Q^B6aG%?|k&#@sLeO zC-Ht|X@mWbegf34)4#J*?l<%C=w0a+pe#61{`$^I=!n!EYi*~v^pTi#Em`U+fiHsC z%$27Wg@n-ADe&T-$8)iNb9-bzXzPv>v)X(l62`D~-eGyj0I=jkhWyQ7gRmO2YnRi$=B=YaxD^vNgNtUyl5$Uc$RHP1G)dX- zajoldOpfYIzEdA6QGUKL-{0 zUSL8tHF-(G4Qd;zNk`q;@f8c($-)QHa}%XuPzlMcyMko)c}_^r>-_Z4f18smXAPB5 zFWd*iQiS=1(_uEa6u$cnYLciUm$o4aOho>1K{LM3Kirtsj(prFtT*w zLJPRaht9RB=4?Kk$8iturDnU!dZ{K)FVy0li(Dm!ehb5Us0Z0YjedWimd}g}#6t>7 ze)MNyq>pfiLJANX0wo#Z@F;;~$tK0NQujy+wcWcxh>I4up{Uth8OrV-l)C{Vx2g%r z+{A~Xj^}V>N~*g8lFi62uTqvZY=9{#d3xG^YJfjqN&Oy=PdYk@miN%}mxP~Wl!j0w z5O-E-Z#Tja)+Pwzx&`u@(2)U*mxbavA@jh5#TL4;Qhbvk;gUtRRRybX)GDGIIOt~) z9+)9j^xcm&pQ9IZmGb3#tN^efMsj{guqG}*B zd%ZQ#Z6a9b^A`^&k9NU9Rrk^4#L{i)em$Gjphq7BM=$Ca*upHE{rm67Y6#MoUeYN( zh)nxP*4S2`3`W_O=_pvbBDq6l7%NY|zH2mm2yaoL?ioxu&~a%8GbJTfaBUmLRz)wK zzO`Ff1B~LIS}z6W&TWnrO?>m!n4axZO1#8lRgm^oWD%~&3wKUh)*j5zsW&&sU7 zLO?-ceB`H{0!AwXW+SBHrJOi21d0j->sMA_p*XodG@-xd8cvG8obIzU0cH#zlQE{r z5U-Ffq|Q^w&~sp@O!LGf6`|YOaSMUR#aH2KXb5lT!9HJRi@nUr_4o>jLT)%c^Pdu+&XslV z(=jIPWe(`3J;jrgOatD(xsmTTBclQO1oft8tw~&1ecf}azOOfMl#wJZ2EUe|7`#br z*_iQ5X8OW^d(_1iJyJSRx{xDi!>J@ZJs+(;w_|Lv1xt*7u77I^!U0CYea2h1`pF2A zIjZd2y)9ZEM1r{mEy&^;K~Xk>2Jf|M9TD?MvFC#PaPG`q+UxX%g*_?^(1)@bS_uR} zV6=aNA4-iXv;KpM$SG}gf;=R=tS4+?nnDfL>*j4{nZ>mI3XhdP6;Hk8xj*jl67gk- zfE3f5YR+wg)7M>bv-Ncsaf*{b3RDH~xG9{7GyI=*GlJVf>$#*s-&ZM0j;ISwbfwUN zaXgf}4Q&}EMUEPXf<;A?#QsCxbQ%eYE?zKE87QT3cG0nNNbWOwb2E9Rv~j1?2OXfDL~H`{ZV3yC|>X;VJ0zUjXC zOe;+M-us>iC6s7GbBIs>OqX!F$=v+)s%3MuWIBW+ipLja0Q4qCY!+5xTuvsF-TYgE zonYW*!Vy8|0vr^bfIX+@0$T-|oWSIa?93jW#I7qhs5TnEuT!u`2B`}e+7n6e2P*tZ zA`z^rf7kOiX!J+1H(d}IMFv3oJ?0=S;9+wifH538Xg)ih26NSrU{Hf)`z~X5PW3%% zH&jJ#x1vgjZI(;C!707~s~qN$KO5ofxM)Ay0pC4u$s48p_1M}^umIYG%*@JxnoI;X zicgd}Wvs$RKxAakxh1M)))OoND8J2~KqMn&yF#03HV1N4-kMirWE+eOgymZI#}#35 zsR=%OGeO=>=3M^wh9GLisqnBXgg{@)4Kdy~l0Ry2ZB~4;trLBx+Xc7?5=oXwRx|~B zkW9W2amRueDZ?L_65X8t(sWUDHQsrtAacd&otHbbXDsX`M*Y~_;|DTd`!D^~oOe2< zX~BPQmi^;-ztQE}F~?VXBa!GX`-ZxW``^Fa*?Tz#)NoHNKw$F6W_{bXA+@#CK4!Nu390HvpoeHk0_4RztfdfZ|b)jLR9YP%ym*RRvsb;-%&lCjHpmTh1r z_op?JY@(X;OXT}CNwqXq*w(s^)&~~j*BO)6SDtm%*uuR8X*gTnU)u==$TU51x(cP#(Q zTv)d~0g{#I@o8oF6QyY5A7p#ar{Utvm%jAJ{mcLS@2vTpA^~ce)Uj)NxbE9l)Axnt z5FK7MX@+YB2z%E1<(fXF0x>l zq!7^6q?XRZlqIE&k|$`JuH)VUfA4H+j7QNMxbK5KfnMy~iGePHV} z2U{%VVW0V16|C{&uA>O;roH9RDkyb|Y(0{Dl&2$i1O)?Kl&Sm`vFikyifBt2gDoRZ zA=*I!{6HO-%bPmvf7(g*CT2Brl4rr!VPp7KOJGWtp(R>CO5pEV0^o%FwDMcWSzV8z zT?`KF!p}RKb{W!|`u=M}Fwyz6jJ$CHpJLS2Rb?gMN`(3k8LF#9B+D!Z{+uEq^Lp}R!&Kvl4%7`J9QL2I&vrxDJpD{a{w1IC80`O6U zS-63Tvy4(e^LRBO?A9{WpnuOWd4R%by8|z}ONs6xksA$4Dx#p} zi=UDkMf@0^e1pM#C`nEY3D&V-m?erFkI)gK0K@5G7+EwXtUXF72K5+u0&le+;8=9O zrwUs0QbPlT`n$_SX(3pMc?gVFp!rjgEj!-`PD1Eh7(s1`>k!`YlbefS+VU41AO5tN zHku2CiVbKYS#uS1tRGtUtVhWox2B zSJS6n*bQld_&Uaz779%IBsFu@rqmG7=n~QF0d(1U4PiNQolsg| z4A6^PuTALkM@&YRokwNfzAG*JdxzN;~310mn)-WUliOg z?0ddVguF2>9a2IWMbl@Dn6t4~Ev2F*I%7d0JrHJYz@Pf$zX~mX+00`$-O`11ZTb~w zi-p*UW#%OLu!6gG?^-*QJYj7MEnOAZbU&&EYCEeX(~;{jDtMCIf zD1liM$1}PxO3l!+!+<|;v`su2#ix-wBV|U)qN1Vj&9KUvq-T+F?_5a%3F9s+VDUqy z`+J56vE`PX8v3Nv@N3J^A9;SwOQFM%`>uH###s8FsXU{A;Pa^N*<1m5jEkwTiJ=D|wF%KdMA701qV2^;7q$a&n2&u$U{7=tS- zyO2O1m5i>CdDc^`pIO=!Nb@gMfQkHvTDjS(!~W#WlvvqiK>A<`ndGpVK3YU2!*=w~ zX`6ouT4ck+SV(s!lQaU|I@$U%&`Oysh~jj4hv|IKa?8jrTd~zv-MorLe466UChs?= z(7?W;Bu0ufsO?r|>4pg034evdu?xKf&HZxsEjEy8|Jrr&N{GIBq|Vcvd5gcI9)<|5 zZtoNw2i$%$#wD|q_2I8KE`%|}ux&xfi(D(B~aIX2zWH%c0QNJPkfi zv$rYVkaRke^&9N&`3PDIPIIT4_Z8e}_n2rO+q!QG;{R4bpXGh% zKic+rV1KxM4aVHX?Cyf^E@lf);N`Y0fyUeT0-KT1PndrDOI%;sBeERz=J-2b79X_H z{j_>Puc|SN?wZ;=I(TIHQ26cs3o-OaK7FPJ6vG%DC18Oj`OoW>RZbSpJdh$KQc5K3y01X;|kf(XFHXa#!^x;_@kFD3)-}ukXpRx3FSFeUZ zFaR435X+U9KCE3+l|8)1jR`k+8`g!cb8}pL7@xS-@n*ZzT{q{4?at5LTwb@kd~}=t zV@EjT%>0LL){yBSF3$#BE1KroKxB5Bc5Z>hma|~6Ce?2gVw?GZjqD~(Yc0-%P>>7O zC;+zz9VG#__b>1vZ{~?xm(@RFM&iwrp-BPUIVj_6q4=kcmdKBLkJ=h#$H~e@D!v4% zwfdy4gRd?=P?k9bnI!W&dL<0a&e+~_ego&0J=?i_@(Vq)bl8l~{3@#b`y~c+U)7{# z+&>AMRaFjSLRe@UDXef^3 zN-lr~gi7s*{L31REtimmb_CcisHK45VzWT=^$5Vf#4z{%5gPihGSWn5yAmk4J@wdI zXUmS9uGgOA1K1gDw>jF5JIBM37m_pV1PR($Ls_X_dp82L9)s8tWp*qrM57@Y$;fIR z&+h0%utJf>gh!;??_-_#@*|nYpks4k0QjdUAuM6Z$^i>x}b*enBS4Vtps1xM^|j=a;(5I zLtr*$n)&|k=u1LkBj?f5g6U#Dovfs-;NLO=t;iqIpGKar*6dEtF966^zb)e@9}c9v%Zd z&Xqt;B1?*-;1(+*wU+&sOJDl4k%U1!>u2kOemz_UM`qn2wFdU8i$f-`rId08Et>C3 z>YR}LBydV^k3cW*XKAc_6Ab!DZ&=Jo&VD6#lr?p!n6W390gf7@GbFxdvJQECQ>&!D9!)>8U{`0_V9CVdlC zb(zHDp0LWLx@R4I{%YQhG(U=1cA4u!Ja!3e^AN8Gsy9QS1qGP{n4D!OSk+wZg@1v% zO)%!D*VXl10rr@JHGL1=E=%~!Vp`>@pXYeV)#S`FU-DyfBeeRzX7*kn`83&NrIaRtY?$9@XRpyK3(4RWD}tJhn}QS&<+ra7+0H z7~-#g?uR|n^HA$r@5iQ|7+0e^#n2CZCYr20>Vo}2^)rHzVbd+F(G>4T#k zbX_$!ManSS!X$OC`TZ?;`7yJRe+yRr6TOx@dR>B)69ily^ZvXKJti;~jGpFqBA&M^ zEFW1FA!@F>u^AS%dqvYlVZqw2*KP+S+^ck<&r7%*45*%G=A=NCS>p^X7bPpE!t31) z;TwV$h(*l~FhHRI0_AuWZFLGkuMWr+<_Um?$b|7mA?9496}=0~U>Ms;)2i$vYRl}` za7qDRfG)JPD(WS)GMLWP8{*IR*u_kDuwW>$iaA%N&bFzT*1BmZzLu2)SD}~vJzHFa zn~jx2I1s`rM?1&Tlj?_X#^>arw^B5X0X9#>va++{{6j^DELohMtBo1`tWa!@uv)j*QOO6{A1`~1d6!$jyW|q}JkF?&vVn^GlixU&4 zOF2CRN@1Yx@w*lisHxOA7^{LV^Vn+oEi{+li^2w#^U?l&R;!wD0p!X41p|A3;}$sX zkEWMBSew7K{?X=toiFxdKLr6cR#G}L!qO=X$1_suN}LWQv}Wt`tqE9PdUZmBn}?ND z6->V*r2v3-a!$p^sN|(L1!QU|(lBfi7ciTqC=J9o=u`n#9`l-EHenghm58ys*N)FK z8xO~kiS#ldHcarq#e#nJ!FAEDTa-?w(}XGrYYE_jsDp-~9f`g{@+{M}RrtjN=uoFP zJIi2HkQ+4NB+AfYf2` zjQ?zOVo6W>?ll`190`7b%eD4Q7~F=8OE_AFK{isTukKMgUAxxptoPh#F%I*xz4#RA zzsOgIeG*tol`&-A92v!8pthEv%6Z@F7{snBf>^j6?ih36u-<_GB$S)C*dP$=+%Q>~ zG1%Q0EXNLrU6rTFIUFklcJ%Z_PB>%2(Cq z?3%*DNtz(0Olp{y=c*=RNxK5M1b%2RIExP_>6fZ3}`VxX3t@P|Jla$W`{tr zIc%Nn(5ArUr&Az%SOS}n*}#YkIN8x9P1qHWf>vG~Dms!+bxMi}Ui}Pz@Y4O4E_)sY zultLy(z$swTd@G=U7&Iw!oKc*k&QO)<9Bl)gaxpz44yH%)4^z>VeJ(NjTM(`-6Jun z;Ep+ju2_~!>S~bOV%d1S4a<>4Hqo3DJ+`7dF=h}(o*I}l52?<1=bV7d9I3Lo+7P%mE8W^)jrVP-`5Be@Q zrMpm$2G4AvczJVe=Y{#$uwka9;3{G0{fJ#>7cz5X0tPEC8Xn(3QT6CZm;DZPu%P&= z6qmHXMkQY#rF0iur_bYchPYFO7^QBQI5*)VU`wC}$RBBXyW1p;?`}ll`oKrA3xxcu zs)2q{1PlxzZoyY`1#Jnl#2+>j%T=xN8DdvXU_zCDDOY|Oq0SCmB8xrpdn*nhxo3Wk zb7`e>4d2Bb6~JaRf<17&o~Xr-mE{<<$!gQAYkLVW%(fmkQhV*q><%ujqAd%xB|zyk za~@xDS-_ZJ!Yz+(=rMBr%%4#!t2j0SIKEeEOnmWB`AxC_ zy?Cl%a}Ycy*fSUDEI^nYvpN{k|NJ;(!`eNs4kG;_NTa(Wlx-z)`he)Z$A@UZ?s|!h z3qE0(EXHILps@0wwmHcJberA%Dm4d>3>m`W!b@!w~!BJP#QD!uAfL2QE;G#KJN;avt&c% zz8*O*V8%85pMnUtNClRe>MC_i7;uEknWEbz46m-GN9E^p%?1(S!`P!gNd3eijUwcV zLmk=&Fyc@p{}{rIT?2geTf#M=Al2C8;U23iPB8pgiVszUqhDy8Z+oZTs9dav+xOiz zXF-h^XMn>CwTPU70%Ny;EQ(7iG5_pMWErSNvQnqA!O^$Bo;hE$`J51Rr$>FA^0bH@ zW|@6xE@HiLT-HUu6veTFedyse0~nu%u;5qny$`ek2JzG4TrPXkzMAi2(mp-q@~?}= z(w*rt_%oO+-<9;{_3kgn&yWwUX-Dqq_NNo&0vxG-KEmKr?RbQ*@1bFG;o4PT& zFWQF-eZQwD;i-ZC)FaK{M2T{VlV{hKj~BsdU+cG( zg5R6VHnyDcYEe_l#rU8+d>?WNTl+R>I_`53MOU1Y_v)m8n`jOor^dV_|FH4c6 zEDFZ&DCZh<|1%HnQ!b6xXp6j+_3z z!1GHcqLq}%KpaB9X@%ncVgc)}>ttS8&MmYJ)qa5#X9YuGp{67)wmb-b0fh)Da$3F# zaSvuP^A7vf0LPg|5GbY(+VP30|GQ$M%t`qz&`{@grr?-ySoV6}D~KuoW7#Yhm;3@U zLY{Z_eFsqnogTxn;tF*3^U*w4bUpZ04YMeeh~hC;+6;|+LARV1z%5(kXlxay_en!w zJlwC_*u4|(tl%Up&*qaFcc1PoA6sNc+4WhBlgW*(#@}bO8dFViux#E$HzvB*U`B!? zYjME%Nr`5wZCuz_uB!#rxM2u)`ZbmypS{EIay=12&3CNSnD_xJ z+lDrm(pL0FUdZw;QG)U@z z-=vddP=4AD;eFp-5KO0-H}{y1jdgBa8(A1z&Kni~VcyL5{Q4>WC}ne>?@A%~!Ni!X zfwX9FtnvEqeH94=|9_H-1K{pLK@s_#WARH_Y-jjr#m4FA_S~DRg zk8{c{6GHDrhCVD0eR4naIbqq0$Yrm~m%Y2c>?2`$V*>J|q`?Y?HgXN(Y+!4a6((xX zGc+@=3B)s;|1QZ>QXulA_}u*|BfH@cxkC#x0zhkaMG3L|&1mMh@v-ZIAr1ZrI{-e8 zH@9!~@wfS}XIJMZcGU-NcwmsFprB~T?K3tY)xkaf%h7_a2nQ_!ZUR38I^l!X0OIfN zIgD{FW+mH(rT7+dWG3dYS>nH=XUj747^LT>tN49XStd84GCt5 zd>Ejtd3c_(@s80icaDZCmfZ23qvEkbB$!G;kBJI}7hjQ$N6t}Bk};!WNA&cAd_sNo_?6gRt_bNc5?N1pKo8$u; z)j+k9LDd@L)(4aWQERrBg#A&(9lRNPPL1nvoc~@UV_k^8RYD_cFu!u}PEfr2FhwIX ziISb0^U1|S>f>8wc=n8<{d*ie4(n5aZx4Q`Cik!vvNYlMqw5OEyGTzJS0in#*Yn>3cOodJG0gJ~IC=%`H?i(2!t5;DPU8G#tSCF7ZK z9`|5QpEZmF-y`b9DZzL=Yo0r^42&{fq}5ET#f4&f7NR@za_>nuo|(^MDym;=iL$y4$qa+sW-@m zu=>jlbi?Q@#KveKad!3MVK`E2B+R$n)4p5F-Et>qzRvxSJ0-)_RB4<|E#1LNX|p}V zT%*EzTlW9DA;EX;1P?C<(2MiZxp)i|im(`L+XeA#h_LC}&wk-Cuc3r6p4<_JSWs_# zxmk39jW^yhKLcw&uRhs%HVO?jjeL=Qem>{-umJ`di4cDW`{ldG)+Z#)C&}Iw-KTVZ zyIw+6?ly_Yk<+kh>WCrgVu?wIAzu?e+n+v$=2fh`gndP0+ES1kM;sHcBL^04>y9^a z;;od*t27PZc0WkabtB@Z@8C}VqJ>~^--&*NEAeK&jWncYeZbt_-4y!dl@V@b5;^Se zy$voSk~9^#M6%AhYn>SZv285%%EYR14eQElx&(G;L-qDhotPr6Shb;nbbpD({ zsqLPRjVphsWB6YzzGKdjo57e`ysh7|$bNie*1m@o#tqFBw}#|v{QT}>%s>|otEdWG zyqryGT%OG9+sMZT<7|<^>gwNg=q2o_Ib8&Q*6vrZ&1;-st}{;+-6oww4_gIcu%f=`D`!vj63-kp9}It3{cDm z1lC*aKTjNV;u(HrXmruQl};?*?9k(*ZI-ZRoU!R!v3wgBZ)M$b)T8|+-z2k*Ho%7< z)CJbCb1x22s;I@D$7myvAcQ34N|*749GTswCVSbld2r{+jL^rs#1>s*+(9ZU3&Ihe zde!i%eKr2Kgc$&#ktoJ4(o&OE_*)fJQX^%Kz{4SYFAd&{vz*Abr0=)r+Mk&7r|sX5 zi61>N*PFEaRv;(?B3baj|ITi*iaNPx{*h+L3O{WW{fJ4Nh?`*FL* zmP4S%qhg%ME3l!{sIHOniER8^JRG^c544Qh2<|k}Dk?q+5dZFcjVWAsqwOXCH%V&T z55X4jdyf7*e@|1hX@Tn#vN5X?x5)5Z%njxs#Ars0CsKI-4q%7K7+dK_HyZwx#^}4+ zK4{-bl=zzdt7S%ccM>iIxM17uopYYl978St$r6v`A>$FX7yCc=g6$MAC~Thc&DZpu zL>M~n6}~OfMBVEdNsD|xn=)@4-&aJu3Hd{U8L!HtOs~O-h2+`2lt?PYlMrtd=jQ-@ zMA-k#LTX5op7FJB*(yKM`TJ)q92}=Iuj)5G#@VNwR;aT#*^w0bo z8gvSTfgmG2-<4(~bJCQZmK%ySRpPH^kJz>qgU>Hc=}r9b%L$g3SDlEE4(NpoRym7u>SFY{^IU ztc}`DHq=NW?l*%9uEJ+qt@oQR|6_f%>@rI8}Ipp&F0q4=0Be;t_%J1v>Zded9~-H4gaU3Ql^@#U~bHw zmaNb!pyuF{wfpuEleXJI%R`$$lGbE^y07$E9#$&Jq9OO*{C;dsh0wsc^dN+q`#@K< zO;7iNnbLi8nPD{xxN?5>Vi$+_Hqp4mqx!o*zogL3JeYu03cwqMD!6UZ=1E?{;gITq zf=twcmi}t@xh;>cD0e1w`H?r(xTb_j22?gQv?N?P$X`%Kj!SM8(*5#X7h=L^T@#k! zn68DFs!CKXm8(-*jqTW9UHo56#nXcPT=*5M2G4<3xKr`(FSYqStuAJU&LgCSGz$~fwG{Lm zJOkRfqs8)ZTEKc7Ks}AAC9XHrE%uV}2*=*2N&KJguycu}mN@qBjv{$t-=l0rgZV;o zPuM2hK2l(52kSO}Y0(3hCGteNwSOtqMiyShcg|R>)F(Zgu(m~9io~o~6L)j>N+tRJ zoUxSa)=SI8jw@`bu(&{H`9xZ2rMR1QCJHz zHxxNx{8JBcFN%GGmOj-iDow&ZDktAS$?EBf=(8o%Eb)W#+>JBzUdngnm{j)Bp?ytg z^c8!BU0#B0f?T1()won|^La@|(Tvz)A+r#I7t$+6>Psl)v6G4j z;-xqxJ!@7A(o57iLT3IfMbVf-CxFbgWF?BG0$~bTHHkWQsIX)+gDESx`ILOw+(M;f zt!-)mgSh-a%05y&ik3<-cv3)FBLAX8URrvL?%U(E|VU{NRyB>SC}qHL*6(q2lclB=VYrZF9wEByt&Y0-JytxWGz zgslOzzZGxIscNTQLh>bzkbw@PFUY12Fq?-1TxCj1(;B)jqs*zRhr035A7LUwn71C9 znb2?(;+UZ7Fv7RC6y3RsO9)PH;8va1BU&m5}Zy!0jb?M8oQSN zTY>_rp{BN*4>F(GLwE^YNiH*C0iHW99^lCiSRT@ShcuV)l9Tt8h93z*l)yQ&aoAkp zYO5Xg3$wp&!RNB&xYd#R$~}OsfMoHzno4wQ0KLp4l<(_ZB>60oQ#+(X< zijUzcuuV8SDT>rfH!kf2=)`)^4$sZWKP0CXDMS25QYb;K0;?w_!8n2@!eX%J%4y<% zhn?jnHP|(R*>gAb*7C*ZYCiLH>G1{eT_&3oXe$w&*zyghXh~{dg#dpcS1j<0?h(*S z_k^sn{Jgoit!CH_=UmE3fyibLgBk8s!FvI`n*^F6m^i&%>SnsmflqS@WBIAWD^!WP z6ztr1P;_57P`Mi54!WN`n28SFdg-aja79f3XTy$UX&^f;MIp>AT-qBZya;iH_(sCe zxcXOqQ;>^_QL%uMdiNor6mhpAEOUJi;APS7?@lCp@j17G($8aIkS+AOM@Q`ohdr-& z_;0IFJP07Zu(hmZ7}w0c6_Q%9n>KFaZ+GlzsQV5@L+n`SXYCt4A{9k*n1rOyoCtf%MY#(n_7j_~^RbXe@0T6SfwCrgPwnluquT} zd*s&ChRCgEkq}U%mnMBW7i_gvj}0XGFC4EoeYVZWA`w{fM;JbiXE)cZaj;{j&ik() zip^4?F#hg2xG6|{HES!@AtBpnt37g`8aO4G&^H_13=Jt=`yP{FHZ%2jX7F_eDK5Lj zY2pCUgL@t^SnVDe==eufYyzpb&!%t9yfZA%%p)rU$Lhg|L>%qZ|41mGmjZQto()?H zw#=(vVEN#7^Rq{)F({ph#+Oe#W8fgQ(oV*txESRU#cG<2=?kk-EST zC$S9Lgbmv|KP*1DlA3wT+=fjxEw2(VVB>2~&EQRsMG4k)-8@dcl?!Qa-o+qk+t-+N zXQmSZ(`n&OGmq|)E3ty01cDmcQ~9u5biQO^+$JdGd&#R%VQupjUI^#Y)#o+EECY77 zaAq%F*`mARWR=)|{OhKTC5w+*2+HIkS`!KP6QS~oyUwKP{ zAVP9yc?S&xvu>;@&EXUz0JcA_6EP(@o=!Ow%O6>mbPVDkN`uTGf8w{4Ag{XWjQ0t% z3>Q`TpLjzyd18vP<($4z=K!GhfjZlkfhy|}O6Z)!!CB)~AMpQ8b4~m<8Ita7S?GQr zT9kq9nzQ{hm5-&EOvHfZGbJ%LoKcKtinPm;jht+W&G$3o-MCkkuED;{vE}OM9EVxm zLm)6u)VXniG*;wz;Io_Rx1ajbrSA0!tn!angidz21?*Jp^-ovEcy@k4Aj~*P^pNT= z+I;`ek+|xIB;(`Vt2F)$%zTOC->2w{rXSfk9Wm$ zzu)~c_xt0uxj7RFlqG@w*yp-%4}!Dj$6|?c<=lQljwyn>bTuW_)m5sr$awNv^1s(m zbd}T<3Kpn$dZ=t~n3}#B+?_axP!0GIUBMzQ@B*5pwawP1A^H^j^|XH%vm4p}2Xx(=_+y_iMpk#a#(j+pG3NcfV9a`DY>$z9?PnhIMp zf;;IJpXB2Yv!89s&ckNc+QXvVvgeS{HVYg_&*deWuFEjTVl6EwIk>azO@m^$!6nP4 zypUKX_29xM1$cHCNxwPT#)2K;kcd0= zs!nB}6z5}t*-0_x zILR0bZ~zK4xc}KZ9ES4p#mIEA$5I9E3Jaq84qDke#?W%SN^EXrA-=v!%a`auZn?z4 zC0D{BIk}2m*aE2rnTJi-m4y-?^f=OJQ8WX#w_?*$P|iyBFBfcYLU^Rul*1TxS8rU#-H zLD<{koxN9p)ngzKU@su#9@zxC&|$yNVtEj@n>ODz%+4@w0a_~eWS0&4Qh@9g}y1l0fZp@QpN0UMd2xjgDvB`z)_)x$tf9^B|rouaq0WTT1}BlMKq>*w&V z8CB&a%zZ6b`gQQb!T5IkDzZbh)p`Si0h~vhg7zuJwfK}Kc57YKLfaUf1_B2fPeuYGRnTds-R zOYyx$hmFfXzfWMb6mmb!Tp1YuG|;vTd+_4=)iw{4VC)qJ-SnR8J|8WsW;i5AE%%r# z*7|ZI`Iu+5FDh>g8LLsw4^sypMYhPptH!D)j;*j4xD-ji)K#_zXX0o)mkbDW&+((S zsTpHss3&0r+c8^0mq0lV=0Ln&8@N)#Afggv;^ScU<^)6B!W~veJLz^Wv^1Y~VI~{m zBH3_dQ!|{~bR??|Z8;$}t=H0PUtS(NHr z4YDNlRgm>=VJ(@WEqadZfC_cw80sVLP}DiUl+DK!W2=@NL#2!&k33BvJwBK9Wvlx`0 zMPM=uRw)K;j$@l{L`>e8cj+LosSUhKxEy4TIVG97B<|vhV1*2VHfTR^ z$nW{DCTx*bydl6*T*n58Bj6-E19MwA2$Zi1u532-X0;VI?ww$pYvi8BXTw>8fhb}# z7LWKhK3W-+Nyj7-A!vvJo1R2p&F!}UlD?A)Q?X8lRUm`Uf*?!>Q93Mc`XMKO z{5pJU4Nz7im8}B7-?8O935)}wqktAfqF2#5FTrRGhbB+M zdmOm609`DOcpJ3C^#P0qZm#-K@%HKfcB!H`_M~71jAv>>^Uco|9UHGUf z!G3Wdrd+JS;#G3mKpT2R|A=up2~&x~m_KzorGa(qy-BJ9Z5hDmZcHV=z}*>9N_2U5 zd^;edoVk#)T!(;*Y&W1$r`<54w*b+I5vvt5WCV*Bd5u;W-?xFJi^jem2S^GP&;_89 zjwx|NuP2;K-)(oeJte<#1SS|r9XnK-hdF}6Z0$Y&+Zq+OnrALXrSsM=M&Ckb#Q_6e z?x_Ow0VpaVPV=1xHc?8?DjrvFM|Q2zrb^?-+dWAW-=36j;MlkUf>>a@xC&;SEFP;A*8hTvJI?Q3em@ zE8s`sB=F%YQO$taYg7ylknqGkY#vyAIQ_IcDqr`34H3BB6_q7Qw3MCxJ%jZl-qy*7 z`w@{#s?%PR(>@=jcgQ~SHF@Oc^Jv$RN4xVM;V*&+-uk_qnMc-Zk4Bk~PK`{%#V8i; zp$!y~N&=aXzUS(VT=MxHr+8w3VNzNu2C?kdJX>6D*i#Mef*1hOrGqikW-2# zd~^g&sW>a;vWa3Yi0L|#!mtEH`B(%Eu$WG@4iCMW65RXsnOp{T*hI0$1a^L(%BIP4 zG((sP&rZkTVW7dO&a4+^wJ)B(IGGG?E~KpkFB{+~49R~Eh{EOC_?Omq!h0qm60CT> zgpn=Kj-m^lX+UBPhxd&=Xrgd6VNWC-$Y%h_jPr^MPl6^?JRv~11<-vas|*LcA2ymk zemH&8%>!*^eh5v2d*UeKeJ=|jToLfzs>H&<(*yt)L`#TfNqis=0#ucO6bP`}6gYbE zA>9F`S^%W2s$Sq?*Z-OuUINnSX;(~eX$;daIq%dsmL)mYc?3wtK|JZUJEcdMBqy>6 zkQ~e7>|C@^8w-eK|AnF|`avm6v}`1@Sga&VomUH)SM?vh`3{stz50A48q1!$Z0_0GtHvJ`}l+qJ(ki$oSUyqEZ0h@sMDKafBbv z>F0RTH0o>SMs`&OVeJ)@s5vyfxf1@?gl{_Yi&RR-^}huAb}Qyu0=Y$IWi)jB9Ne&< zm8}jm6v2T$Gka1OX%J$<>)Q!~+a>OCn92%(qdzf=&&;7y2hn})6D^-ZblHF%17Z?K zSI}4vIs;exe5}*~^N)*Nh9f{_>^uSH7#o@^2+-RKQ*^=Jd=1Sl!Z)nZ3n7k)ahNX; zu;VRYmi{@Kyov&lb67cz6cZpB3eL4rekMKdbvC`Q=Icj5jM_r zeUpC?GAt}fXSE`OmSP#Q{fhz2ER05KQ?i%X9+M3;OW^d~N)XP~R;2yX1FT>fm`G5; zgkdTmKN;&kt{m%HsrmYS*IEP-yZ@1jbo}aN_G^qc;?}-?R#P+65hjZ|VoX`hmrI_su1ORf^6gdWPG>R7&KffJOVp`_ zvF4jr-7+hqyM*asJWUi2t_z-NU}b3eCJgh4S6F2DRz%$NbC<3~&g-X97{ zQK(=(GKhyIW_+ieZYZb0E7jyv4Po9l z1e<1Fg^vULunjzdp-Oynu{<`#I3GLsMqxvSjO&K{QqZkoG|Xab=%+89Wykyaam@d- zu3NcUNQ833yUxWL)zp7^QY{L4?y0l`d!Ke8`2*z^z6VRQ74+9#bd;RAoT%UXEneN) zKBP;n)$+04VjdW-fE^d!*TaHP4}=ZTcQlW14CkzH^Lu}|jDf-JWdhWVsAJKIpx^Ci zz$wnLsM@#ZP|CagBd@^i{)=Id&DT~59yU5&S1+XTMnRa8?Zxh13Xa6{;F92qPsfMD z3&aLYXS}BdnQggYdxO_tZnfW1VYb-ZNGgm1>c!|q6 z=*%%BDezl8XVn%IJ{6?sDm+$FScCmnJS=a5_kZqbU3nXA*(^;qR0w$z0? zV;MXOxQ*}asW;}lp{-c>yqbc^>msqClN(M>U0MJAMyTtOad$8rgvDxlU3EbhA!I!> z_cq&Mi5>7D4dHJeeC{@uGlOK>a&%`g_mTPL*exmBYrTICMrK)ZNy~N_v(51j-F0^X zQk1@(ksK<^KU`s>zjJ-#Huda$ni&!?CVKbzw5oYC5?T>}+Kq8mmThaYn>WezU+PF(}1m zF}sF~JUV&p1!kFJRQHSlzOI6Q#;CB!jbKRWD82ZNyW{?VAGNEvKG#)w6nMB-SUT?MPGoXW^*AbnDiZ zm<-*^;YBXdORQ=2ItNty@oC~OhH zVHi6LEDUv1%>z9igHxBo9~m7c1pDKae9=&l1f&O2bmWFmKlX;BiF-M?97vW6eLG?= z5h1ZzI!h)_;EF2|VDWk<+=W6hV+_jeOp?I9=^WWC`X~X0H^L^9Kv{K&rs#(O*_egR zB#z8ecx)GOErfb|1b;`DWr>?lakfG1Ar5O5<&VjPN!(!NhD;rmS=bx_Tn15l9=d|h zvCqBhtHk13?L|l(EDh1X@szBRJ{^i0BM8| zJn_d#l%;>Fo@9y#o@l_?81Y@p<3^EjG3Ab^J<)q!79usFNV2T>rG?gaEq<`!o-enl z7^d+4&`ZRooji;LJ-C3sp^@7vs8ZS*ptyar`din`s~%I0Uyfe$RgzgcIGIWqtLe@; zc2~1kj4_aYZoo#Kn*urArMXNI%JtFxm5(jr-bv!$%Og}~~S4Wq;`;tP4 zLeZ}RX&$wrFnILx>Hpe2-5|*GOv|Qo*SYGJb2XxzX+CVwS=X3s@98}p7Je!weAD58 zVbAqPot1y(G-Gogq?bRu62Pap2;M!(Dn58sBQ)x;JHyO6j{%JmN#k!-KWm_tx92*TIUp)uz@^7hV@<*vHO>0*FS8=SF(<4i3{-%0(clp*uctSbx(qlq`y3@;<5YdbkkZVULJPOShj4jP=BE8C`O1VAL@#sq5Jb z=4`HuX)U0q&5-r8D=Er}E6t|YZG+8@=uvy!DU4N^EoNCqDxs7tzl_T2L7Xny$=y29 zyDe2n!C%m0G1eQVo33!UvZU@UDU7^cJuPF z#!6ItijaekrDGieVOL^6Dg1j9#&k<4iy&g@;^v^si_Hn#?5jYbMr`@HgUf$Wi*vhxpzOQ+6wP&>D=$>#4=Rv zg#;3Rlrx(muEI$Wjeu;fm$x)n31Pc_r$k|u=kU<$k-Y>{G@cVOd(+e|IuhOKp0kNkkgeq7B;HS|Ky7m+Hp8K2t>_Ytw7 zG>je_?vTv!=0TyPOONN+f8sQnld0Lesgk5=87IL@N_*NA z-VNcn#}&H8A>R3MC8l$A>rzJR%Nk3$6xrHsT~F8^Ak393vw_dC6SCJlq#Ok$CQEgi zan4_^aOoY&D88%L07&X#xYH0cTL-HFCqK;#hgu!-#7?rmoE+zzC4_O9<)FOs$0*NVX&OCf34k1O|84hL?q7WTUk$x`ZU!)A`7+9o+)qxGH2 z5Esy<)~4@CiESn4i~;07Th6L2Pj?t7(NVewGK``Sd1_2cTI)?>S4NM?j7zNwz|)Fk zR`hc%g*NEVd6#&)+e928;9xVry?=O|%y!G5k(je46ec2OVMH22%w2`J@(~;3q?vx% za2w9s^-#E}A7Bl|LwmM4MQYxuyH%3r;YH&N^jqJwky^H;m_B7+k1Nk5BD{DudXZ;c zY=k#4IgXJ~SR@otoZS&Y?)u?}P-Hr>r8 zE$Ng$EG$iPvx}LKc0k~I7+RG;%m7HCl|l#M-@zgWqTR_oh$OQ{k7QE|Vy_$w;fkp3 zvcSnYD9-*cH`c`z)SbPR8N9S#WN>}FdB)o)!62a8B|%aoXQ>)x`zc*gEF zRx|#QqrjTEP(u~^qk}gB%wzi@<1n)&nJpt(^E!S&N)!d?-poT5Hw!+A&-v0RrQZ$8e1J zd7zfSfC-I$uo3+wO%8g}VZlnSChZO{{9tsr6ApG{J;%{g;ut8~1O=zFrTjsMEs4(l zoy~}z;!StVcXh5pHqf&jXCDfx;ixH?x&iCRJJs4j@loVVobpV(-psMB zGsj(L5_Zib9^CDBkR3J#Sdic`uKAWrI@u!zS_aoTK$sP^J|Y8VLicBCu<0(cdfdkH zVrVkPz<|zv;A}Lz;Z6l5zfdS+M6b!47w+)Ld9Fn5vjaRCyO}V-C&1?H9hQ$~?%7p4 zt7kK_UGllg(7b&kDf3hxp=X8=>LKD3t|zuhgPh=tP4}w1 zRXZE@8|dIaF^w`P(CzN{>Ad=+b@8x4G9k!=YTZc;Wlh=--b)U$W9_ipX=}L(cW8FR4l=2WaEr<| z)^&!N7;Z1FGaQ6FslihYHad#g|LNqK@oaJj_ih`U`Gk9VVBW@w3O6u$cCfV}m#&EX zub{za4>8*}${ap*szdeJZerlRbF60--t3<|&Ij7Yfs4Uq(wxv5gts{7lUV3T;x-5n zD<(3gYP<4&ULmVB0cME)D#W*SahsXJP-Q4DG7mBDaKPi|3-Kt&3qZ&}gSzrxzB|a# zzYYL#b{Tx8OQpq~IK<6ed5@1&Z}S^<6W2X?RC4Neo|f40_denC7hTc8d!Poag@yy} zKPL)p6>9PeL5cgzYEVHP#&Hm4EP$zwvy9`er_61z6fq5xb8VOW-agv1%b#Nmfmt+9 z&bvw#jIx#Y>bdX`gk-0E$GylSh{jIii6Iz)3fKLtH{;@N&@+xdY3WXc6S(l9Y{Y;C z{36rs5zn6>f={&I^8mU8savyJWu>;|vGShs(iQ@ZMII@5QUiOKEWgzhh96|p)Gu^U zpSVQMsiub*_qgp_4@JoMvZ87b)VK+|$dLmQ%Tr%KtC>TNeB=tX_a-j-SuI}CDJJtd zV)zCRwSoR4ivy1ei__-aTa=`Y=e>n+y-JR$F-zN)@#7iXn`gr`$WD6K_j^wUJ(ztSx~+)l`#pT9IXOO7LvlVuwqVoBjX#zR_%AYFHaNs8MhVH@A;L0B?i60TR? z;sl?#r-Ay^o3!Y+O(d$_MyBb!e#rSqzFLDTG~UUzB1SaVVO&J)tjwjh=NA=yVFbZa z8PqlCp3I49`M5nJ=~I^wM{dB09FNExC@;r0%}D~&%Mio7h;N}lge!kb!9aw^Fmxk> zrMa$N{lQkrYNZ@XWxq8Kw(f~E8|NG`Isf9fGer#3Fko~Of;~PnG-sKF>;Wn%5IO5ptph)UL9GK6ZAe{3iX&TvZ8frOS zkn`ut5!}Y?_v+CBHCV5o@4ZBvtuk+Oi3-k|1eD`jhk~=tagHm~9Nb!-r;y_+>RST) zZ$9R{xXx`*e$|-o+s_QNz>eCKA~y0%Z4FpnjCMC(#09(k68!5?GJ8$`XwkZQjHjq@ z`@m#2t$$wdI199I4@^!A&9%=|X@nvUU;6ABSOR0mIw7NDf4)GsW+m+8Nhu#cn z^dzzE44fy`I3rZAPmkYQYu=&Skn<{4@*gXHEib0-1gaAT$ce!I@rc-7er~c;2AAV9 zSbZsuukNR8xf%ogdHvMlJGlFYnmr##(F%DzjLj}NHY!j9N=%PH z8fo{_)ZjfZdbT^Q!;LO4|L}`9j+7BMw3N5`d;i<7qgXAT{dGFR`V6Kd?bTw0*L5zz zh{2K6)Zk}W22Cy$9;G+irW0WGi~vV8&yk+yX#2uG_yR7%@3}&_hiI23^fu|3#t;TE z5W!&l$+e^Yp@N(}H-L>coOf4uy!C=NG5Lm&KYzK10VZ6gPtSknAM=(}QjU0;R)$+q z0T_S8X^7{_Oai0I+7#gt$M!9#>^V?M_!(7_9LDbPK?#|JLFOq1Ej6|D)5V%p$m0g0 z|GU$?4x+yrO@B=Aapx@nwRHkVvKXU7^wv{_^*GjpXlVb$zn^&gfF4mg9#!%Gsq2dFmybeDhSa0yd;fM?9$8e%_5j zf^2@&8gMyuvNh3Y=FVRv3KIKnq-Zl#-joDpBQEMC%9@n8-4igeE*zRuQWyA;>KRed9hbUj4p zTq<$M7GwMO$h+0HQdNBx#X3{PDpl*{wu3Z13ncR3Q;YxJ0R~|?OWGuzEa--?SWs6S zCZi-TFS{@p3y#ZuLL6sw96N){+c1QADXe`~n`ivuuzT>9P;{kXg7h#S4kzpt!1p~f zWDb&%N>Dctp(bFb6M6(}32~A!B7xX3Tv9A;D3H^h{_fRK%^WdPA&f^A0rov{pqwoc zN5y}sW!}b3D36Wnl+w+!2_#vNsvAX-_?HlV(uKMew0U7Mx_3~h16N33;ETRyjNpp* z?)1<-S-FX;OHu3M>gd=cFtm#T6&lsMu?=iIy)Qfqiui@Utwdnpv$dF1g#{i`nJFwO zQG#XYrf&$prMq5*xGiLuSE@(orV0#)A*SJ;*#y5zDovvR;&Gcm;}TtUtyL06baIr4 zgK31HMH_M)-h`v{3TLOraTS&duW^^vJF^t5EuFJ*Q484Hx>@S}4f^9)1Sea^fOQ)m zH$4RCr>_J7dL`HeKzChP;=*%zac;c4HOdXHQ?4|hsa3GI^Oir8KA!=E(LZx_l1AsA z5?XKGsxEJ7C=++H zPvopq%-sajtTZ?T!j23WBQD!RKLbjdDsfc3{W3(VK9kO%-2lvS2dFVRl z4w~q3|&=`a%ItVYN@7=>7lnR1*(~27@j73CDGN5-_Wg(RZqv$`U z^UNeg@6^vE>7VYr@eDUiiOp!{d?xpojCfxbZU3hPXNbYiA&}cswaVR%S%>uD6iOoz z(m{Z}sZF?i1+hR*%@qP8N#?YOLZ!{Vz=w{DxEB^eZK+5e)`s9@$U&3?EaaVwm*W^j zsZ0I7_@*de!k{}1qZRgmQ?I!@{mg;%m=x~B|WhFl}VkSmcK^`!8CTd5!!!XfJh=y2VgJChJ=%qu8bmbzqA)i$jV;FB;ekfKyx z@|mU_gML;X16vicprho^GmPAhfY@sf;dX)7YnjlXaK#)T(}}(G=@?z9{v_axuuu?# zRS*_Aflh$+gz$2R4TJ~oU`Hdqs#uI8%Ye5qf7g92ix?aYG-+^7`Eok-t~{}$T#G`Q zN96>nl8GaCSq&k32?i9s<%-+!upNjvA;;wYL#kS`IO4fdvwkU@NL0GSCo~f@KH6kI zt^HK-VLm_hjkPtaQo^s`EnZDd&VJB^%X0ssq`gc9HK_AoJbRZ-&HQZ_r3TMzhmfR| za-Rj{FgRujXl}+%8Y(n!{}H(8KWG=&c!&!e0@rgp3F5uyvF5@8En<;63>3)q%{^WZ zR~K-1PUXNES~wOP?jGq}|0H`<=XWNWr6Q5h6072mB>=`(O_>ruC8K=|;IM+^!n>-` zEfWga{diiRm^`X0rg4lnz!cW)zcnfN`=Rv$LjsSP0CEIu_Wh#r&Qu0WMWRJ@SrN&E zdcu{q>tWh78X!V4;>4;*cvs@_`Cu5XG$;|S{FPSZv>gntXn<2~CS+m)gH+{NR@Meg z$k9g5#=u;A%0eO{caVc?CnBA*??9+$Ia;Meq)VigB|@EDiNn_GNYKhfH5d&k*K7|p z=jf%CL;d5cT;eX40vj^r0^k52%ew5EaEjh$>?W551`b?>^ETr#+wNNM34Ey2h_2*j zeH;=f-mS;i1yD{(7q4~IHC-6wdXX?#yfWg`C0mTQO(=;L>5*#~4`?Y-fOA(yV|3!} zEOfIK+*H=jq+u4g*Vkep*1U4vA8p0Z6^3m5APmN$qZSQ^WPX@XrSB5a)}-2_AbF#9 ziUZ4R6ngL2Jzb@F5z0V`E)_O=oGeE~+6Za^rZznM)1Nmg*ajd1QY3Si#@UB~%G%%{ z@bo66r8g96TO3K2;v+2c@Rt$uunl5xdCy6@%yB9_@wbICY(WBm6~SP~=$WaksnZa| z8>OB&!j|)m$V#&V2LU>Y3K@3LAwDS`*onZCRi4B$$yzcY7*da->4PAS9vG74kg#%j zzb{LxIwMp2u)T>hZIX4e0j}dSe=waMw_6-%W*=w%GTMv;#)5HAlprYQRi^a%hNUtU z1Qa!=Vl!qGyOtZ~@LJ=Q%`N+nYlUow1;R$#+g`DsOX?jKHy^N_*z@so#fZZj^YgZo zhhx8-dF}Al?v?HR)Q?}z!yT8LH`-0*#(u5WaeU`_!0zFhk6)Wy9G7>Vw|mqU`>id^ z@qO?syP5uv-!8KqS3)=1KbeUAex=^=L-YarXU{%88@?Eq3;{vS6Wsx&rh!D-$>?f{Ih1sY+f)r`m40kZMEW+!(01}33qZy zU(Pv6FG)YhnEJ8|$oq~GwqQ3N(n1^gj4ptyOjP*6LP z1?qU7rFPVf<*HdkKNoh?-zvnpzB^ql51VN9o;99VTOYLrUWtBj=W5%`W%@{tmREQClPi%A zTlY?l~?nKf0JO*Hv4?(uALJ9ed3`#czjE zwO)^g>~LyF=QG^$sDjg@8NW5u`BQSmfF*M)4TF*iW&CPJJ)IT6heJHZUULo=X3vRwQx$okHc%o=tch^|mJYO-oZq09fkkidZpR;&W7@muk8CtxI)zR?AEEJh#p78cGAA-!)c7 zw7$D=Hfi}?6F+nFa&tp@^m0q{h1TWP_U`57Ho^RQOLk52Xmm(P*yEG@iv!=KUgq3D z8LbFLZ!~ZSWBPk`mtYWmhe{;$Lrg8b{HjspJ*(-a-BZ82kJP++p#Jgf>&=JvbTLTfv=AbA9cI(v}#KtZTgu>V#45mpBkf{A6^{-Hnpz~%UG|hj(pt? zT^m)}?LB+by)z0u-rJYmF^0=B{wz6l#(chQ(A56^Hb6{jKAEqoQe4|Y4f)I z%8r@w+tehahGm?O&v$Fen5Hfxuvt|8u?npI;Ls z3QlE+3MI>FTiI2S{GzmB;Y7bO7TxxjEx95?_Or=(Kj3~R`;kN zwm3-UlqQOO5T4cH7hE#WsC(Y=M+~w^(%WRCMKnP^vJSP$U{rvDGcdG7&zfi=?tgE- z=Sa!y?2eQj*cV~V=}(xY0>%Whm!ZF|sVxx1|pn7bb%Imj=?JF61FxmHAe+_Z)z?msw77F@ zPO_3)(@*V0vdzpdrTr%z6GTkxAz^$50&QmSiVY@Fo+ZZQsp3=K+a}Xa!_5rH$*_Z> zA@EQ;{+5PSxQLBV?^>!OopDssp;|Zx>iAn*Ro0SHEqki4a6L(((~-gyr5Hs6zUX&a z;rtwA%PcvW7R+bLy0qgnkb9%WjYt^tYr^7pK6uY{YUOS5o`T@?Zm0Xe|jsY ze!Nq7_wu){zU>!GL%M7bl#DXqE$ljqxtu%t6&$wZ8pPo~=m_yWhwPtuTtZujp8iLr zv61EiHautuDc?E#OfUQ-F>HwRxe0vwjn0sv2tNUxTDirb3qhhromtEBc&*^OG*F%;jD% z!klwfo&46-++#8#^GgNHtO;`|>Ylbu5#pQ7A=`?R>-Sh9i*w6gH#w)g^E$JncnJNM zDi!JBTGAk{1v4UttE*rv%GWx1Why0A;9C&kU$6Qt($#(QR6&kXy@9MD)$h)O^45e9 zWml?qm|aKJG5>m%$#3jO^Qu1Wm z{vUT{ZamlpNtbP>qrGN_HNFeDKJB5Z6SL!{;Sf2darj{ZWYF|(r_zwIi)CBUZMksB zIUj1IVmYcH;caMrzQnS@8C{t79VL4O?z&UDNOoOgw6ii&?TD=ev5lZVvCHERp$g$C%m-xlUxD33>Jz`-j=6ypDkoH!ta}Pal%Ijch;oLonoMhLB+| zF{_p4ne50~bRaHPepW2b<9p&3J}^NkR;z1_U|4;~SO>v^$Qp%@>c?MpQ!=~B`a>i< zc?2&|SGE05PWOg&z-!X6_dFGVQZ8f2kk@;AfICRS-8Z6gF`)}A?_>gHa5N1QI{rfr zqe~9lVD%?tI zg@~>DhZSyW*2H+d=ZWPOrd8KS@~ODDthC+v)nmpHyOB&LfA&68A5cmWZE|u6g($ln zPg;M-mi{4zj6SlYh-r)33n93X3^NU!rZ{Kd_w#MKK-~!wo-x7}Ulq4TBHUg6C&iHt z9=vw-13k!2I$X|?dyO1Z!y}!w#s@5QrlUpgna2I-jTB)VUUN-b>AzP}_|-@Z4&l{4 zi;r7^$|f6!o^)Rr#;@O}G-Xb3cm3*}i08UA5$0H#k#lcsj$R^3IieZT4bw=&kA$s+ z*B`%=y2CT6`G)g{PFn1Jn;sZflL|pFz3uw_RHzR%i!p9G;qTGdpU#=muIHsGD@gTU z&JM?k>YsMwI=_7-!S;S+vP}nzmXhRWpM&?uhtM$F!H2TL5bfiSI~ms0-qg_~9|{L@ z-RC2)bR&`_IE8zdL~XKj<;iak5GnLJkh`A4EhDYB#kx~9iBoYLh~ssIsTIAukf)E2 z2H>)HT*L#AAo$=p9AX=F5*hmBcl8t8zKudTRA@?xbokHei(hY{SxIl(RG-}_00<~P zJoHfdSGix?)+y!oQE%ttaT(UaGqQyJAGcE=T2{6vx4eY^Wu@a8OU#X#uRtsRL%5uB zkko`gk|D_|aS>VjU}pOq2T!l7Mx)Gv_62}H4uQWHgTWBYuR|x`b4THLFP9Ub&)ekr zgZtpVQSx(uTLeHEk5%?%%9z=uc19zLknb|V-m?7|DtPm?dIcDd{c!}WjrV^G*iXY9 z{nH(eDNb60O?qHg1o({%MuG;e;zKvB!9D0?-mc&s+@JyL#JV30$3ZkK{-FE}+_w^r z_616i@LyB#hqpkxfOu30o16v4WFA&t3qjyd0CPuqW9W?)3F!E!FDePV3?Kqf_F1nZ+Q+uIy`#L7rUj+`;U*#MH{hY`+sZ*tP7FmBjhCftQ*Ymu+@) zRdSQ{a=Mapdn$ALdU6MT5-+Oli%U}EEg3|K=~8ZKaO`Pdh@FoE=sK41%SXW^PZ3pMBR z0xCdOQgHv)LPSRz5fv1HDDD24RA~(=QbD(qhf(jq{p#2s=%NrbyZ2jMSsb&}5PV0) z`T=R|0He%a3H*RdjwrM<1g&{xFnkqeDj$I_lEtzVr>YzwXHVMf%i^WUFwY>NXFD5t zPm@mn$-sOIL6hI0?JY4fQHhUH_{&w`BK52sl#5)4mdcPJKI7o0lJrj(a`>t&idC-E z&{1Vy9ytiCev`MJtW}%H2)@QlaXU|&ssW&ZWs;$oqKt)KpoxGl9bJCF3M9>dC9Co2 z24EN!WbNw3AC@ini%1^s!#OPk@o)B_nm3!Lc*y}v-1pjuc7=C9Vm zLbC2QRUoE9N|X!Mu5R-9VecudNG?UrAd{MH!7$qsFlYseTnEH6o&@QqU4x(0%~lUd zsW0Pem>v717+;RW$f?;Gxj{(QK^HU5eai~cHsoM^dGp^mwPgjY%uwuj<<3elS*2j# z_uQ>>dAC}Qf9pM@7$4o=UA+T!4p^T%lT`)A3N|CI?O8!JQmL6v5$6#i!~83uG6q;0 zBru(Z3H{!J>#KryNEREj|0&$^j!jcy9c(D`1-Ec5BxoHL# zZeAcPs8NgDz;nIW@;PmTuOPj+@Z2K5_EGW1adcNixo3a*R%$a@^(s81V=d(hzP9Ai z3E*Z}11zK(1+@)02}UGTL9(x)Izlh19Thj8fVQ}NT?Ui(B&torTTX!ajBwZwmzX_Z zYFi=*Pm20=9DtrzTWjmO)miW!dlQ~qNN)|$%tv9f-g4o!8E2eVdrVc1JvReePV_oL zYG8PQJv4jw^*tH64jO zV}BTw01#VH@DkUXie`hKYvG#7NzuLF!yhhl05(z+TPR3)YZp{4z80U|Nl>%%dyJA8 zrLn}>NT_RxTCz%Q{dW9W`61NGSeB0gzw!hmO4$GRXR_Qty^mVnt3b?_V2~0QA951p zf>|i4_Nd zq6U?aYf?}?STx|s7(;aI1xR~t<&Nx_8}4swi|*YMGZ3Bfy%ioTjB>xx76>xeTaK3u zHY$?KPW@?@>xiNDVi|Ny5-}*y9#ZC&_OJB@*tvPff6aG1z}{#uE(XkS>qPtBaiFFp zzrU_Abb>Wll+44F^V;ByK?Hd~ZVqT9OrXg5=PST&w?l4GE%qJc{hkB=#(Gd43Civx zQC0^|Rs}~i-$Je(Q3c{;_kyaeLF(mTXIEmTU`z=cTAv1}@QWels(u^*;-3?+%qCMD^^e zs@Ki`^iOI3ox*87AZX!#FPrN*KIn!`s+d8nX_CY7WY+^!AThXP{VM!dj8S_#cDw^J(Y ziy^s0JN_3ywL8Bu5fgDrQT_W<^w48QC=0j6tZ)m?k|Hy1RC$c&&5ADrfPmOZIg)ec&xvtOM`>T=UfBhlwAAn#)o zs{}9KUj);xO?+`F?fzS!wim1T9DbbC;t~Nm_asIjf+v?}$_!s1e2*uuo}wN(l5z*E z6!X|VV5ijMV@patso}7>`%e0hn>oVOZ`c>9R~~N-=JtrC=a|w(`6bHQTsQI4XX>_h zz%>3b#N3#}p1FN(`nUTS@j;3)#79RDPXlwKv5X`*|Ipo2pw+)CweVb#YtP(|K+1=Q z<~jG4yTO4fY~aO120#!-qhNU8mhK@?X5S^5B6P_&0Q#mPT{w(z%-CguJ^HeczX2Nu zzIDU_?^GA%YNw-t18-fQwP_r9X7RT0NMcOW9Ah2hTE6u_LRx$1^IlosSEi_u_foTm zt6PwG@eUL0F)K*snZkMpQPDutbj3JdIryPfM&)2z7%ZqjaQO4&6WH2f_Fl&nxvPq1 zcJr>coKAsHmL_%wfE1PpZSXGrZf~PCxOoz6bVSm^A2!DxV=1f1`S6?G9Kl6Tb z9{I3vE6O%M8ZJ?+ar=2&ufrrlf^ET8u`ljbEFjn3rM+a9=Ymat3>3YEj%sMO$auVf_nf<5y>= zuaxt|iHehQKKU1ydOt72PUwA?W`2j^e?WAW!<^4;i~V%^185DVCF?)qU0s2j6~5XE(fA8K zI^4NzAJm3i0E%8CncpGndpwgCzRBDPCNV1Yo|SLChlpj{G3Hj|ezfc6pDS8$V$ScAf&Arh(#sC)c)=qiX-d#-F1lXq`9R zsBD@Z#alQpd@AwY*{Z03;6TWtSEZt3!U{Ab!~>?CLcIeWYVu@(@@^UiU1+GQD_-iw zanaJC3i^y(^-|jB^H)c~pj^%SB!tZ8UVSkHeUB<~?K^o`uDx*U2T_?XS z*O-f3<;x4&gOSfbANg)U9)je4T^sF9m+ro~w&jb7*n-Q}Y= zx8nYb0wx3-%_tNN=khlc>)bEk4rn&_vA2g(9ax+3m$U8;mb~Xfez7k*Ds7qq7TM&6!5+1oHKvqQ zW^fnvb^2PBvT)S~7Up0^ho=SSkuIxZjz?7RKJTVm-Akubr$RRT&*BVk{I+iw_z=IB z);VTq z9WtHYHO?nRJZ5L{!iHcTnK5)j_{2u`^2f#WmlpnrG}ZeocRd(Z+gNR;Ii2sPx1t$?(sq#@O0pr0`0EqaQ06S^SMjnJqm7q7e{%4)5ULQA@_vMlv^ zp4^l%CLN*0pMV)zw5UGvujITLcZfD4b+pPk!t)oO)z&q-8og)v2~YQx1RYtgUJ0eQ zg$wMsibzCN4`OqxNL5xN*Rvro<)dYD08QxyagBlNGG>Z#U9gTGd#0Uj>6Jj?-_%C< zR#(;oap@aUOq|mDd-uGCzZdE_T$Hg8jlYF}4oeSzC;ru5I93n0>5pS@V9hdwJ`z1G zVt$5UJ8Ygxz{77PQ+idWZ%u~xp!4Dd72=@=bNGQ+rfuh^NbXHS1Nz6FQKQK_7lB_3 zPgGKw&$)5vHbPpR&Rg+n0L!I@DfDO1S>k*!YcbA8650`_bgywoB`;Fjdrj$bZhwSM zBbFe7)n(yPN{>AlZ{x|rFo_^4R|MUxEVsO{j7NcWi=35Mcs~gRAt`bSFvH}`Q$wv{ zO_ZE#eh>BIzGFkruZ#Ap9U`@8BEmkq~$486wHASA`^Lt`#mGXgtGi=|GN<; zWv<^DGIs=Mfnt@^&X!EP+z$pEOvVjbNmfdy&uUlvu|1n~QUGgqyOTM)UTa`RkSlHS z=l_Bkh_y-@34Cc@H@I>ZyHh%bMADL*P&N7I%yW)Jc1mmSeQrf`Y!WoiBP=ok$I%6pCl{fRE<}ZRbVQ886>D4Ho4XAyCRHnCe2qt?JuKx(#au^ zIThriGT#KgV6wh~&hxE{d_g;E^Z+z2IgWwc_?8hyrj94~@@wzkL4Ad2uFOx@avdw< zU1DVN>UgTJV%21hHl?^UEo)D^Kfb1->UBAeM;vb%7UkOcu)}1Ab^64Ic(B zwmH}Dn(|?dIdqrpVpVX2R|msoV*JSwF6~z{L5@*5VIALn&XnTJ9OsJP)mY#8vb{NA z?|+p#Ti{vv>`AzIjNT;UVFt|l0|C%HQKYX?&fVp}&ufj=(lGBbdX;t(z5O(8E8HAv zmlHXcH0M}Q5US}wk9`&W#M@;X`yhZX52B0NEW6o>QtrcXPDylsJ$t;8IP$FsDSQ5p zwU>D-bdYr2Qz+Z{nsu~r_-kMHlM}F#YaNq^T$C`ID~s_5EJkrl`RMfEcYHa>xeXP= z9Hu4iio`r;r?<9rCBp*bofzTW@~p_}Cpq-k9(DNCowqAe&Mi&QqNzB_MqYg$zswVP zhWvKROcJ)S%^T;vF6-FAnAd0VTlQ3i@FAX}vv=cODLCN}&uY#EK=nfRFo|!)^c2W%~iX!owT8xIgz5Zs@ zBKh8rwzG+!r$X{jh0m~MK+Q%hdSc=HwN!aFV*rr?K7!+Kc&$s+V3}$7MatNd3*^r} z(hx-z%;2(t#)-bkl$}b4%dTTOCoMNpm~IE}*{#Qj$|5BIGRo@Nh^3jjW5}B@)7izS z98nqvfA@y^wIefWq|iLC*N(E^B!>51NBV6f3D9O2vPoE>uZL+bJ}Tc818}$>h0j{D ztt$#pM^Sw3khJ7Z6as*7Jw7V)g zW9X5dkI~K#G3ETlt7fSC%hgr%W8X@g<9TF0KJeu@<(o6a({%{dz`S4k@>z?@#5#KONlFrCx4CxUHnqMmq;Cv+Kqf= zD|!E(dUYKbe&;a216-^bt7&`jYaWlZ=n^gxNIg^|*@%zo$whD)>e=CH!iof^gdBWiQ5eDe$H1Z_5A9xqnEE zvFpbu)XqP?DEazDEji*qUYvkbUw`NX>|AwC1jm4Z6istqCm&hvuGFpf?a^ zyE~+Y?{ySt=9}G zuv*V_oNpsHUG;`Mqz2zE>+Z0nKp%rZmIpSLnN$A&R{ zvj|XcftiY+_fF9{>t~bhoK}e41gCp4{O44dMGOmePcoa9-bUx*>t5}=U&Jo58HS7i z+$TdCrme9wynu5K1LkjJWAk7+zT?Vz(b`V{@6}>ZqQGS=L656|VXXT}`DJDqS3-Cy zF`SU$LB@FcZd`$QPUbSU?%gn~=NYVora%km1VT0GST?Vpbi#HpytSksHl zGVUr@;>&EYa(~yVAu9?N$kKD0`m`PE;*CeF>Z|Y8xRNq}o((D=UwSW*OEs=j-XGwY z;}af18D#=T8jMqB_3|krg~Q@vq)J{sTRTz`sJLG>j{zyG{mDKI^I~L@OOlN(6h0^I zedFgW^<+Hiv4sG&$&5Qm`l!_x;Oh+Iv78}uv93Z*er&O1B^%aMR2mvtmb+$E@wI>& z_n2l31d2UoblORI0$STPY$X`hhawRvnMQdkeUK9^4%o4cER@aAEIr0Jo%=LDoQ^cl z$wB&-TZi%%;_0 zf3`&HQBGPMo&3Tg&j{f1KPp^t#Ad$l_m{$|U#2cymT7qFZ8f661X6bJN)pKILM5`! zuZY{}@!h2(dqk0EO_S*crCJRR^kUmCS>n?2{e;h!b4I2NcQv(g;T~J3J-stdhb}y0 zP@Q5hP6pifg~pRq2TfHs!RtCjnQD3jpQCHy_LA$h(ZvV9?>OD6a!T@P_@?0VNFJuGYs*PTK18^V&TQp&HY9MkK@{s1?lW`6{6L+DDuP!l~WkbvMHejP8jjR0r?ML6k#?6 z@NDBso0-f;&DBxSd_K|K3=yyrn(vKhvz|z^hZkV}g8D?00w2+-94@W5V2Fp3em=@m zH3GJamDD<6H16qyN_w-TZKwWt70B_-uIe%wSZd_a)HF(?XiJ?CFTjZYhcYBs$z6T68gat=c;K7p!U?G zBqgusA0(^oOXk>gVj_=)3Bxa-aQalobjJ{N3v|qjgheuylVsZ~V6;D?DfSxG45Mod z*EA6BckyNSQ;^K+8ExSq+Wk`pR6pe{0(MuC8bFH3Gthradl z*pI)x*XVkS>+Z^x8cj}T^Nfw8q>S;CE~xa}CFlEVUZ_>Qgmp$n1(}9>T}~4GWKVAi z0KVVrO=0BA2iSHI$Av62&Rsy7H;B@-7AtBNuXkF}gKLRJmp^%On=>;}u7&RycdS9# z$EYW0Tg`?IymXN4DV@1OeID})**%Tu+mlO2+U6$bKbizSAC_T|CZoO6k|=LH^BEL%&H_vQn&aC?1m$G&|$dqM);KDsKk z0M)=J)yzT}c>0r~B)@5>MsOFjBHgUW%)cN#CJbmPGLzKBONt_)`f1G{!6Tv+k}tBy z>1|PrXW^06LJGI(NAMgCR22p|b@{2_04m^Z`US+GP(F0DkDF1{{I(7otgq8A$UynG?RbGd(cIRfecS$tQp6ZfX3_of7m!$T^SKbVm zE8(^740P!QV!gImIzfuJ%1Q!QDOVWAEw@t4y60PFR?LFtD;(R& z^e0LZB+V=PxN)U}B72J(CTjeM>2uzpX*5Dj)twuQIWclQGt*p2@z&Y2SyZ0qkvm~x zkc9^=SGdu*vsKYtpU1Ns&dn%S2E~`JtXr&jP8HHWN`GS5(#F-rDE?%-CL^ zUltT@a9Myjx5<*C2`ytixI<&dlm-2$mC}Zy6hcEWH$Uj$#enYd(fXE^3VA^060(*6 z)5Amews#B6erA4MJFdUL$*4G)ZM2v)EWR4%k*7I+q4h%wC|geyw1wS2Hr%jLVwUo7 z9;p)wKFZs%#b*i3=7~{+0OS!RRI2=^G%|gxl6ge46-<_706NDmGxW_1e~O&D-9(>x zEF8?oncii&x|yH7UQpL}Hf2_&%e4x#7@?<8;{xa2I6heqD8olGSi>&~k<>GRmjhDY z`9CZOd~@B4*A(0J)u(wf_(9B>H!)$KV$SZxg!9HmNXJqeO`sI8jO58jGhFcD;9|1B z_m7x?to`er4C3=ZNv%_nyo7coQERwxAzrQkFZ})YkHVtQ_r|mD&lH5*Ii0fpU@0}&^*59gHE6A0<_7&V##ACX%?K9n)d5Ym3n5ef;7-u z&U+7LvKF{SG9TBgGXe~Q9;lbmeu5Z8lX!R0;P8rC zsy2^y=-O7LlwAbeSYoYm2$T-x5QSCeR9;DesA&Hl)?74W70Z6pUzxu~-hHaAq<+w_+u_%Kgo}zYL?+l52tWFB1lN~qv ztU|bvB9bC6%J8Xe7SJtGW1I`TTwK11SxwrGX2dqcL_~D1sU;e=B&2p!obuW#*tQD;L#FSB& zDt&e8#PgVjZUi(}AnjKu!|js?Lc%vq^ojC2w72%cOc|j?l-}DMs*4e68s17V)*d-gthb%QEor|!7}7GoXU{&sOJ^r)8>uxtHjp;I z^#q+1c&;64CH^cZdi(eIQ{K3k=<2Yz%7uXrr& z8$RL(Gmj|Cw>o}9d-1)!-I1flAn^Nakwo6RelL>Zpu=KmeBZul#yRP#SGn;L;D2*R zFa3-!!r9*$e2HIk8+dvAzWB=S$>#S0*OI6nYs|*p2ladR2~DEXT4nIaBtxyoG80FR zWvu$@ZR(%J611n+8)hC*7n%?O?~n1~o{I@c(k|tQV2R#3SEjUJhzHk5fA7j`rzfAq zmiCpOs+%slIx7=0yd&L|28FNXm zmZ_VPtYNfdf6kP}3VG2Yo6J8-ge!}@Y59Jupzz3~ zgIF`rL!nQw#YJM)`{@Yd)}tB|Hqcm9pV2{i`krj9aizOsl1*yOQ6(vGAz`aR-Aali zmv?^o*h^VTf3R7u(DD_veZ5o^wN(I*m?kP23`j{Fy&RoB)AN$qKn?z4sjuP>4@%?L zi3v#Q4o#2x>y{I1nk?h)wT!>-zF(PsR?PM6-7y}-IJL*3^arf&>u?BjQq-D-Y7KO2 zi4}TmJizxOPr_W7q@HkX<-prKUki=5=EZ|lv$jZS6z@{9VH}+2aP@F))RM3dhm59r z(=%U3p~3xeEUVd224jpu(PFm9Xpe_H+&P4nj#8%7vsUpuiYy1Qm5ZYiAYoBQx<&`N z#(zT@bVd9{GU3aWz(qfm=A znE`yMrs;?2^h#YPSB)iNnSO{dVz3~hr5c}-Mb>g0X(Aef>Xx|&a=e$0pBJOOOqvo( zf{NZq4pTjt>ps)9A(nBxUVgsALp2})E$2BpgC-8xhW_k(5iB_=d3FhR&TDHqKw>GG z%8Urm*(AijTM*X!InFzt2uRxY0b1FJk~YEO*L5zvJvuNz;{7~>$3V)|*|%&*^0~XR zW6kn>OhsT6Ls9p5{o?n9y8a{0djwo)9hB=6mgfU?du1|7R%#{EC4nJhs-ox0u^Mp> zCmAz;sGR*+^By;0BW~q9dc$@<_`Vn;dM*}iyP~Y-bh>#`U%>bD37AzCEX*1X;&^_o zQ|4|JgV#G$q*0(sQTjr#!GVamtKZoimPhkJBFEJZ)Y@oMMbc!leSMRd_8rjP$fIz+ z-Xyi1%h@rUvb-Du>9fMc^Z{gad{rPh-kR$`q)OIb-?chCZ9wk#G%53!qJFre-00gW zD@HZaFgp2gb>ML7$#-H^?r~|P- zp-R7ZqES%s=NaaerIjIx_sbVmwkxe#-cQeb02|fY9FUW)X)yh~;htZDJi6V6n7%R- zc*{n%T%(3+gO(dCWx+>ndvWF4Ec-~iRL-pF#X?5oepdL{AIpg)o`_CHzV@v;36@g4 zUj%WS5qf;t|Fv|XYbWq|Bo9tIXa;WNbHi2AFQ`7>t} zQ8!H_KkpW}dY+jQP(j+!9Xy+rKeX^>v@sn;Dnk0}yG;di1^BgeSD6crLkeLP?3*1s>G{Fx@pO@HpO)I@Sp{%PGU@GP!p zY1;!$5~V~cS&`_KgSG^yB**DQ(yBm}VI%Kpwd2I_>JAgw+# z;2x)E#KRcU^f3L6kAS@lH+uotFDN@nG$9Yir%4~@4%Cz+hF-aQL*}TzVCWQLx1~Vz zJ9~%lrphBl?&uZbnBUm}i+%bAh2g+49Em+AM-s0{+0%ul2f*`qA3TnOn=ZV9_waXS z?*M1Z26rMz->hq@K+AXIMocVuWZm-_Exo_BU}79dNa9tn7+q#+`PkSHo>QENT0AB& zY%ah}jFJ&FDRpQajoTC!JAt>`q=5HhzZ#E8bJM+Z;99FFa1G%cri?3d;AyDX!|>Yg zt?7|tL!Od!hW_OGh0@5cVwstYPdn-NM9t+}{4JOwlFT&X>}wAtnUnJ7Y+#R#^@n^i zmnNF;_p->|5HYvIy(Q+2Gzbc+>e4?(**==!w&FJTLxVpuMJzI5uE$Z!;D@1Npi{@+ zd=Xp{*ri+JM8bMb=CLUrx_zt~FG(dcIlQ;iS~>&N$5B4WQIje3LFg#OI@1`LY^&hW zc(rg1cDe^!?{Nf(1Wqa;9HKIhUoe-atRQ#2tFlrdktaDyu%ljmvJy|u^Nr1^tx#r0 zq+X(;8WiuL9+u#OlRmzY(p-D6MHOYNBwtiwq%<(HmC_0t(yl0_*C|!+R1_xyVsQ-w zz-t7z!Y~f#>&f*hhGj$!D?OAR*P=7+GK9zhMjxj^n+<ymgo6b+u}`>R&0Y;~l-zdG zk%XP9n<_ZvZ+fFf;z}Vgu{uY@oMJtKCsQ5%noD(t5L-OOzu_(a4!qOi%`&j()j>?2 zU&VL!zHx#5y#a^{4ybNlBSgKw&pq&t+twXFByW73cX0V5?5b&YMqDy%Q%sic8*^MY|ieXji&;*+#qbk~zj~q?r(qebeFG;e z9NybDO?yq0ZLPB!xGY4xG+h}3W0){^!i*VtE1DxaX5e<-hYNNS%~IwzC<=J$QHG!z zah%{eg_v8i8~n2#r}lvz0J9)%5K-^?NFQ(f+?ysf6kWB+h~TCKeGet#I0SuD(P&D5 zpe2|2$ZEhVGg5P}8Qj8MX~D&O$JtD7h0W83R9LB+Jyp+EQ<+Iu&y*Bvf}PAeF6S^a zqA!yhkmu;P7Isp?qD^y=SW$f)Jt}9r2~vBw0BiWDaXo&07&Ndyl7WwHunKn$LFrmN z^*tbLKeY4h)whL#MarlX2UT1h`gZpdDJ&cc7 zpl=kKLw098oQmv~_TjMx|ISVH^xpxV;TaZ-UOTZK$9OF3ItEsm0Sw2yntVN;wB&Xj z$z9y?*Xp}@O$iMx_zl;>2)AOSwfGeGTKNuW@gLG6e{SilX=z`%&VQ*P+}O_d zp|EJJzC6vN^VbL~K0-$_U~`rON)QpZ@+y%Jr(cbpZC*V`cz8|E{KkT%Wu7QEY4$QF zTG+N15O^)1zRu$XI#x51wLT!iY$dcfkyqG4Ml@1Di$MsYsq@ov; z2spfU=Y@&gOMkmfwFFDkMVzFr8V-oyPdWLUxqhJF)XV@wg?HYhz34QD&c!=L^oiGs zodaKu9;gsIPoEuokFTM|MT`6B(H;KY;CZlr#Rd)wwkkL7YZXD4`63=X1Ya5x{&Z30 z?aU1VwhuQu!4oksiqx&xO4quqWI^GvaIOp+Ie5v1L!syq$lcOeQ7VQGdv=AFzhl}$ zC*+w|$_>Z#8cLp-?_|(etOZ#)> zhk27vo@c)!w36jB&=mxpS^3np-j9#3LhO1aYBE#4<0Za2M^{F6@4N;4siRvdJ;w*? zLV*(`iy2AE>JtKJP{c*{9p<3M$M>VRwrP-p|0G+26^twh|dGBT_=wp)E9zE*X&I>@lXbGOdVuzJs+Ha5QD&@O&~ zedvwBwPOXs?j8MOZV6X^J`zHmt~b2FKh|V4cK!L?=0mQ{qtiDI8C?G)D1u7hWe|AL zznXt!Jc#aXRqSp%NEgsCI?fCphkw3l)cs&AUg<07R()#XH+EC^x5N`hB4dVapYEo_ z_tpU(58IyWZs^?J+e?FEJ$ic>r_TDsjCVp$HLn#|p~H}}9~ ze(2u(QpX5CgiD z4n==E8JS@p$gqLRn$s@|5Hv9g7FybKQrCN_6Ee^fX7*G+Tpkt-b=W>msn1bjC5b}r zV9kVq37Oz?z*L`wAGd%XL23Iol$giR7ek2@Gv>1qCRG$ENY+9z!j>jLBG@_VQqUng z7Au5gn3BJHC}j|oghP4^nMWvLhpkMH4l#04vjj(S{Ty)v1=24#0xkJCVJVPc+m%!Z zl13kL;|O;HG0Y~E?JHvc4J>IqmsvH{SYOK1UPa*0`x-(5l*q?L7P@Q+H2~3+A_*pbG-5ShZ0Qbp%*>~8+3`z8a`2p zZ9&ZM%=_S(4O$Bg=Myp<2VD~C2xtq_(ggL~Z8Q>S=P+3pmP2*o(f2zV6Plf`nnj9m zZ+tJPyEmeR$BTz->y6-1{5bW91isYZ2KK~gD@)>6fc5Z22a>iI=<5FhOSVU!BPmr1 zP(iMLz>>^~&gQI47Fz#v;?<{=UG}SYYlCa(NhJV14r`CPD8^(S2 z8z1b4pRrcezTCNC*B=;^VBdN0Z$cduap2eIVa5LjmRu?>&!}^5ekFQ~zBT9KtMm(W z2r$xp9jN&9E5S2K=l2M!|6=Pm*n~#Hm62zJ2hN{9ecIatp(;QK5*s46MFDKdL{tj@ zApqyLL?t^@&;pe?K_H6Xt3s&`HMr;}b-`akk!`ncgFIvsCtrY#SN}gB|L4x~Q67H7hkv+``KuUJl@QghOX(hWKUxW$z{L9UTr1Rim=qCew z=;IrScUZzNtdN{+DQl6UL9DLBVe*(Q&Odgr&IJ=UbdZ91M;bDG`8hjotXe8sZfNCO z#N|wf#T>J5miyAA8E=}%hz_j(vL+rLzdSE+&~bRG{W8`ia!g2OdiZ@_#D0|Tvh?NW z){`$}>}#j;U;#mg@prfLTQY0af~-f6zuYe994Hp%)PA%=&5}p7FfO6Td;3i=fmczE zH#OQU$CCH+BW(gT)}5Ntz>Tp@xq%A>jWBrdxsnA`z5bB(lgN@UWotEA*rc{6yJ!tH zUo~zfEOM;heaoQxc);nI*gcH71DD!ymElAE%Vxt8!KU4EBopa_9Qdvv5Hc&1X^C#2 zzSh7C-Lo|p+_D|sa;o_BqG3Zxt}DAL_2>uKm`U~agspP(8J8pLQO(Ji(EIY$smr`a zf8Kr;wfnVxadUmiF~qQ{su`4$E9sbf$9Y9J`f`zd$v1zMJIS0djI-e5R#L9ob#FS2 z(|tX(?UBe$+KS~U0nf7DhsTMd4Wkyz-LSBA#o2LxV> z8Wfz?FHH+9n>cjZ-}gR{sXRS<`FHv;S(%&!#cv)@9vV1?t26SPQvKdiJ03HRwpR&1 zUb8gXonU6S*C^Vp+j#Cy7CmXNJM?({Xu|HZ3Z9MzE4%lOrMpuNM?0>$J$~=ny*u5K z-EsEroxA#P-DcEdV1IFv=>O9w;@@X#?t|RH|1dTGvr)9a|Hg!3Td3B#Fv$jif55N; zjI0jRS{syH3YP8J2g8Ca%D{%sfaX;FJ{ZR3s$^JWhn3^2vLV@YsEk%R-JI<`_mDTc zLb54caj4Evw=-;Fm)>E#ub*W z-ul0RVO}xoZwtvO%Bw~G1^*cgdv_)A`}(`96u8R&cQEXsxynXU`^>408=b4yHky0B zzu&mY{udY~bb7OOSmF9++nCAx>5wIWa@l*%ouQAXNYWCH?9`r+dXGW+?Ahg}=c6~sbiX~H!D zB^e60K`NG8w#)Ht9zFwhf}oQU@Hm-o#g-KX9HS7y+P|G=C*S6)1`9~q+={B@kh?4p zGES~k8AMv*ri(T0lb$DwY|a7{0`|r2AdbGQ|0dD>zs9fre_2>c|M_6|uZ4A~$6%mt z#QVSGuzvwp|BzJzAg+C~%Jg4k6_X4_aPW&s%wlPMQ~|)UXS-#;DU6{a)P!YDurB?O%`S{|*e4!2VyrK#O!~ zlQ0P^o-&-PB>nO;l*5P1D3MW9X24_%95&PBZ0ifRg6K)`Zhk)4x55USQZ@YT>v@u3 zUcQrMyW?zG5(Qmy{ws+Kd`*cLxa`Z-L2+Ka77FB+7l+LUnU)9dj{8Dqz&VTWuC2V6u1 z6U%zt##a^x2*rEC$9B6oL#QTNi`^;h}q!mYDDNtoi4~b}6%4Fi$&+zJF zT4=EqL36eU4gcAND_o+DBoh+Hwm%ypzyEih2mDE5!l3%%R2y6c!drbn2AzUd^Y&u{=b#vp85)h0@Ew*?C!|gSEFBs`K71V157yinU`XfqFH&W5YLw92 zNXghSb|~7;u0sqNf0Iui{T#5FH6J< z2oyROK+RyAFU#50zzhes;!8%Xfu(+VQm@N;TOaf=qfG+e$u>1A~-Bp(t!DE6%{a zIJZb^yqSUBIs-X&&mAf7B@hCTj(gEaV1*AV^;}H-J(?S2#IEgHABvxxau*KZ@tClk z7N!(SP}5TEk}w-lEu;Eb=p=Nr2SDMt$LnM`a4Q74B6k+$J@lvYa9w=N2N{AU$`As+ z*y;5_ZsUX@{L_)xsZgrDJ_~`;5IC6%N^tLc&XG)V8~`2KGFB;0CDE>c5>;u>K?cx{ zVRK=`@uQOy6KG#Bh`XhKkOxMs*?xx0GXtqhjA66%WyW6ns?Oqj z`Dkc7QaYl^&)UXOj_C~1SU`|6WsEPkK&sFb2JmuA=9Dda3r2&Jrl>$BL-316#7%LN zCA5@?Ic_}Zo_qEk32Ub1QXG*q!kL508m|@?g zfZNHK+yx>_#8Sm-UX>3~0s%N8dp$nqSax2sRrtL?jVw5bID};bBA)@_g}2E%?mp}L zcCPdZJ~;s&N9tDX9s^=RqaYWOHAucbSW)$ENFgNT zP1X`jpBl_a@--*>O;Vug0u*j#xnTgN#n1wWsRfhp7}z1^&E5!g*aDYaVw@)&G1m=f9=WdD3P z^+_hOA3f!3*n7Z4s8I{CmFUY7u6`gzlgN~ z>FiAth#JG3QhfG>`4jj9JX z*7c~coL|6ly~_8x7fW$bcEp~);iU2F(GiP}@A)7(Q%AY_HQ%dO@qOkW-mi|M?w$vOCpkd|ho|bWD#r91#`XYB zegWYS3zimHWLRuc_xAmWBzb;K zDOj2+IGI8dMT6mx6`a*UH9m|XViodEW+>WxxpS+@iZ(ppX|R=q7w|!tX!D`98)mRL z9U4Z7%A2Zhj%C{$f*0`Uy2c0stYZIqTTB*Uyr%L&_?CL$lx|UWEnJpj>q^(dywD{8 zWR|z8|8v2OhMNbMPBQ^mQ>uCQ>zyZe?*DlF>Ef?nZ$P}AY{cJB2nGN79R;3VUfF&( zu5WXw=dEY%8p848ge)ljA8I2Eo{`WH754&ctoby>i_9Cnr*wYcMGNAEwt2a{?6BQe zKc?Om_z7e&wBCwMDch~I)7-apIN4va`CwqVh8RNPLaNCyWvzO92b4Gd6*@nMeLuis z#%PYt|N5bWg9@H#D+z;ig5b+DWwKSTA!6+}WYm>}E`~dSps*@TQz8In#+K+9_Z>ax z_pD`3N>^l@3_BO+1BpUEI(_LTR;r@++e%-4g~KRu_y}H3`1ALat+g%9(t=4*Ujzx? z$MfhVCvKQ0>{j)O%1}}@o+rFJb>J2HoE5PB@k(s!<|~d1d%p-UnYdYV?|G;ah3n$= zxQ9cWUt80DuNeFma`t|`sWIH^qJJqGVaEkmdHQp2JB0ZwWtPfhgf|8=_83iOUNRl7 zBY80$}LIdIrDgm=Vb1)EtkAzS`@fC>bA-Nwz6u=C6#?4D^;Z%up z0*s0%V8cDnL8sbIYb65+5S*n6XpTchhY-y0fEJ7sZ9y(&0Z1`B7Cfnv6OK&B9?)kW zOr-$J`bc;U0D~YehJ&2>aI6*}6OXLvBReiZ2v}SjizA15m^QBBiq>8JaR`c4&%ULd z^MjIU52S7>J87ynFF~I;i#j?Ym~0@4nfg8QQmPe{jYt~}Lgcp~QmsG_6w}PZLE4%w zV3dw5t-y>P{BoKE&7z1*8 z2V86sH=cy-*ZVl|nUnhsD{hgHVD!a4M0N!%CX4ALMIcR0ppYzPK#q^cOKIvOX=q{73Lpy&)7VNk2-*J?k!ueiusZ7YhMFyh zs4m*>NO<9Xv~0bkgPf|_xl38$!kQH@)|MYK935Sp!J49zsT7QvMLe35X_lQ^KbAkIWoH9`hS!K)J#aoT&DDhvSHwxk=l zMJA(>iFm05VFiO0m8lt$Jw6i_e$ktqKfqw9SBOj=D#$R0G9tmnf#~yCq-%x2d8|6X zgy*u;9A|QJ#`&D?6hB?%xc2ODqX5zg?}o%4fP5BBYEh7%y%+R8Nk!4eRKI> zAe*A}3V>CZYC#4q@F%FqpXXIRf9HRwvyU^-JZz(>VlQ@%a_&H4 zi+qQfq3#m8YF&!#6>Ey(KLwtUTN$XEu(n;c;s~6dT@? zjxhIw2gNGuL;+Jva8`J{a%9}yRR`9)cuu8QRCt_*Jv-b_qX3|{INNRy zOrvZ=B-DHvNXFr^m~aOuUw?cYih{^NBWVtR(U#ccyQahG`|$?|xI#w_p^0g(oCQ)4 zAh2@Afm5;_Bee5>kZEUv5XEf7Gl*@DV(lT2OY#(8wgd!Lm;V-1U1&oL=%M#}(K(LL zaKZ4V>bhWAbQTr_Fi%1vF507`2H#!K63{(~X_B2gY*sDvZ05WH0=Vc4($K8d2LX}W zxh3hw;ah-Hg+a7tm@7qa=MY47gJjYk?-UNzNLEN|Y^U_MLvyZqQ&QvHjyg9$J3{V4 zayq22ASnvees2MOot6Lv*8*hv!gEL#8Gy3|o;Pq9jK^fMZ&SB`&;`VM*1_eH9;tAs z=|cV={(?X2wo^-)=fV@O7hT`+mGSmCnu19CnO+B3lH_Pbu<$LiM+!7wh+GB*wmz&| za1<&(f{yG$ymSMq3(pMujs66zi%|?AEDTX1INM|Iz0d3YdOcq|_=I&X?j9@E-etIA`PvdGyh1#Y=dGv?XP0_I_<4ysB{A)8 zX%T+|3|xiEkH@34i{|h32JmjZ>(_;fHGp%fc$ETA^gkb#Awy)CJ4uV%E}z6L3kt8; z8Bw)&I^VloOQX>Uc6YD;Sj*^n4Dkyqmf?w+n0w23mgq`XSgF8+X=0KrU=_X3B}jM9 zxF2I}M$EL+KJhgv$EF~Be1(jVxY5||TQCVJl27CZyWm8V?J|Oaw=rWNj37rUGp3gGOiItht=qefC2J*?5z1sH z?gK{)25_A>Ew>For+!-TF_(WgAZ2`$v&ySmy-?ObaXwJuXAH~H0emU4&Jmp`pv^~p zHV~>;y`1>Y>18p16s>S_!sP-dFIoGT$oFLdIETw&MSS8sDPX%2ak_uBRF2qSz^_aD zy?-)G+kH1tk!cxV`e@i2Vpa0EQDc#fE6~kSA6t2XgIg*L&aA*8*mTD9#rJBrHU(~; z}VYHlv{4(`Ir5(J5$uMtfFN;%cOGKK**O(;1Yl+UVv=V5f=MF&kqM}Yq#0g&DxTT z*2m;SMR0sxo89!EIL8;K$4?s|tbYksRFzyBOEKdh_rzJLxb4b@#W)?1*F1UCs9nCy zQ6aD1)HJ4a@q^*3z#Yq2J%V86HS)EDzs2?S{opKa$O=;QrU%v9H@dm+c%}n@}2VR^9UB z2p=3TG2V23vat*0w5JEmOQ+_c)kNHi8w(zvaJ+Qn5%a~{18>1iiWyA(g7%Zt@Bce$ zO09|&z4_++q;k5`fhiXEQV+hX!q>$h$|cl?&QAHOsi}NIJpc7$64mEOB_H2x{Ww&L2AAPi z$?(hfeH@*&W{HYWBHN@&^8I|^)0Yr=p4>#F!#g0)e$5y?o{DS({=!^_;p?NjDy{Kl z_-EBX`^84m($9-#2k_#Qj+ zd)(6R^Zx!GfAIUl>)#X3d})04{pmb7O}Dhn&vnVjr5hntQaOHtiO-kgA# z1K_yYDwo*6Gj>#*48O(lU)J5Q$5p6VHPbWYM9(ruesu=MI(vYiKK}F;Q#ket= zn#ks!M=u=w-@=Wkd3?mKI-ANf;rem^(P=>I8UOa};qMm6&OTQhrTu{$<(U+e2vZ#6 z8wLWexHpTJ1fiHjgBF&Zo*R5&qB@H9(tL}T)RO{n~DI#fjU73 zH3Y=7igZ0*q2UmOn2u&v>VV2HMIaq36Lc_wR#)aUOJ#r(hqJK|%n2{A^MlW`hiBBdSbN#rg^R=zj25u5iH`Ei zK>GCcZN-`}AoBr?TuJf0#}O_ovhkY7N^GOByrxLked9=VYS# zzaye?i*-B%Yy1XphOqoo+Dll+Rsl}5m^uxdA+-o|XCC6UyfF3r=*=em%q$N@6`~2R zNn#2?v06Gv<>3PcQS-B2n(cqI((zt1ukLE7JVcoxBocbCIcQjanI2a=+6h_AK;`=F zVQ`ER@wQetHhuzVX%ik{zRF_8NRFWhM89BVI1@WPPK>!EsUc+akcBrtN(<0=*(oT$ zlAc^1*49oe5AK#iMbOubn2OMDqrUhY%@$V>$|P2TLPiaj!06AYD-#i2*HZ7jlp82^ zf&4>Mp?pXEjbc-}f$F=~sGldOebvt88RA61dgd|QGhc>>TH)g`aW^ir`u?g8#`uOL zW=7Z{YH$XMmCUud+fp7nj3NXo*@=QiaTIVxD5t~G@*tP}5}3)@t(05Kbc%4MUp7pl zbNa`*Ak#5Zf``-SHAo1_Lx(GXXlW0El(M?SQDvIJ9JgA{2B%LUP6o}<7ztsSIbBZ( zvA!sbGau0yO06?Sabz_~S3?z71rJtuALpVK{^{$u_sT*zz@0y<-G`Rk12(Fo;$OAj z7||fJWa@gn9Y~c#ZT5?jY#nBNRD8qyn?tu+Z_TOb0q%c+-^d+HU6~gHz(ATyUD>b^ zng5rPgs3fMz8pPJ8uYtfRSv5$0aa3z(Om?af^oxRye%^ng3%+O5{4h0t0^!QO$P-G z4W86sMI*^Ia8sK2%$^qR2!LBgG!RPY67bRkyRacp+;|D3<^tm;Aq;3u?_Iqhy){o! z4X#_9WJXP>&JRtwy(CXJAL?u1xOJ}rAp5?`rV1UC2QFIoN@MTnTNWUw&70D6fEkdg z@iQtY*<~*o{RI`}uT!xZ8EUL}t)|3dU_$LwaXl-VE+)>#5X9ivz%AR20SciExb*;6 zXl_C#jjmMuPOm2}?HQN|x!Tc?(Sr`ovU$0q#WIWrVa6 zBT59umB|?x$=eY**h8`?7`5Neb3v#3L1r!n2K)XwR>BM@#_MDa;aizgFw){=7ZVNP z2XHfGC#a=z14l!z4W+kuAANDdJ+q_RcRX0)Cn&U%r2FK9V?#TynbvrGHfk6dRPqdD zezd7RK+M#Ztm)o~1tT!d8=}seIG)o}O!y=6?OSPx!?1sQol6Zc;L*-et^I$=ZWAd& z=Sav<1cX6kcRvZ6GN2@TYFA7a0dUBIa~GF|qss@BR-zoh!?!lLA=lt`=e%e}Acy>W z@R*enwq{)JJz&5*VWtp<3J#Z;G^Y|p8;BsGs zxVn)oVD*71s#ltH%3!p!PzAVh<#-AKUu48#ruxPpM$4uMjm6?seH~`)vNPK;CE?8| z5}={sr_qykpHO7k4F#(D8*$awU5 z6BedpImf)+rPB#;LcosCE);r`m2g@3IV=XT8Onxb0DrzK7&HxedFEu6p2P5%bE)uS zx(95krY05yG)1D{IX)RCva4bpn;dQl$C|7Koos6f4rDM0S(ijstOAyf=WW18dzjPdIlm zjd;+OTo8YU^P?<8*)N~ZUMR6|dTDajt0iu2{z-dl{R~B2k$I2p|2!wQFz+`2RjG3j z-aS>I=}n~aqI6)>wJ3=}@rXDIE;L=^7NUJI04HW|AzGZ!4geo8TOp;`AHBJ88s`9E zi-e9H>9LWxS!9}1oIm|6LP&UjfE<=KUWO};mJpH~l*9LC+}i24DikJOvm;{b23IZl zb(^p}Dk-n5W(*=ZF!T-y7`vj&Z1!h+Sq;3JZ zUqBfVnEw`_Bq5a{9MK0^1Pak_0FwtXhOR0z6pZ$3O*r48j6Ey6z`?H4yN5u_-2IH| z>z3&XhD48#@)yTDham774l=#^k_tGqX$K;Th41ZE*4{Yajj%CCBq&V8VSr-3n&;SJ z(xj}rhYOHeNa#lueYkPObmW`u4Gp5!M9AGR-~An@LXCl{GBCgv=A?so%%E7i5}Yy0V%nDHE%-H!LoRZ9Qp0Zm=y)W51BfVaVfe2yA#t7fiV)5}+GFT(iO@ z>=3R`;Ua}Z-bDlBs9aIXs8Pd41!Um zi(tP(E17N(Ytz8k39!K-O+V+dfQ`r&z^06*?VF#Y&Oqu$-6b|6gz&;$8s}TVKW|l~h(N&cuQK5#v9KD3?7VUZ~MMK;4 zeJ5a9*l%>A)<)AmL0g}!F`mT*0}^i;?C0kd!f#Wc%HVpHa~9n0;plfVl1pxn zf?*1B>q>a=Y>0dP?kg+1{Q0|bg`?4kY44tF6P=f>Y3 z**|l-pZX99R&*T;oKP*Kl1ox3gr{Z(oGYgz^dyviw>(d-;5zPbT+|}w-HNb_dYUK+ z=iv$#Uj?}>|Di|cJyI7;NZyqy2U?Z6%1 z|0YWvEI55;Gt9nmK5pBTzajvC4^>_>;D1at%$Jwj;vfy$Vu{%B)d5X5|2oRodt80! z0t=;KU&Rv^otg015g0S&$*snB?6!b&?hT8MD*1AXM@b{6F=K_gr-`+f8a+^L<|ns568Np@)73~6YxgHj1)Q31*m=;e(ycnG`UWZhI6t57Ii?O*{UE0^IylZP?^eF=Ht0c z=hCVY@FGLxu-jVrX`|yL+e+cZ@(f4KxTKJgv$xY|U$+p6_>2S5XeAJ*)W!Oy7#6*# zp=xf}4@9i=fj?H>3%|u!e3%EbC)bf|zyZyI3a<8Cj3AdO@ly}C!#vBOd=9Ewy5W>3zT|JWAX z7X#~!kM_Mbw>V{peahIfxN3b&k0z%FE(59Vayps(DsI;D?*PAOhC+7MI$9397nc){ z>|0f_?bL-5rl$1pL1ov1)8BEG*rUI1fPOjUPPR{e9|FJiD30(bjiw2^>+d}cm|Ds} zgveoGdpP&=-Ng6T>Rg~+40xP)zEits{i(;Q=ErCGFf!yk??hX`$>_ZvI08huI9EO1 z$3-T}=V|vHY@3T>f~NtNM8DA)mz^|M$nHYv)hXGN1MM}s*M`NxcYI316Wtep8VfpK z6+Bw=XVF{@Ag<_m29GkGtPp%3PB0Xr$-k!77<{v-z@naqh)~@z9k4kW zp19d?!U_08^IQt5gX$?uz(oPP9bd9NLF_QqM~*vR^FCZ|e2fdfR}%A2q}Xp#c1qvn zTZ`xwHQT&9u5YpW!A#(%#Vf*abAB%IG=XO}UtzqSDNKjY%awN)KhLv5FsZtBGPGLg zrb6Z4Yj>DVj|pv1t@2?09SL~t3v-I^Iw$kBL17{C%s2gE-kw@nRp~K=Zu@76E-1-9mUAV> zB1o=0D&H5i^H7D;yg(Fk&_sw9=sd=Dpdc(`;y~F_%ZnBa@XP`?UKk&?aZ~O9?Wl(< za2h9V$8(_+PVG_z$HQJ?S!(g=eSuvAnPGwPGXm7ffr0nzLAwiAY|jl?zW5`%vIXA0 z@Q}qUIkVj&YLTb??bKV7fy4c;1H$&ueUI2y4IU1lGkQ{Q)=!OoMzu||NefV$y}r)i z#i}YhP?TzagOF1BvlqpfuKI?L*F9`5oj{< z>=0i&UjhE@sbZ!Tbz-e_6*!sxCNmu8-mYKefhcCZh}bjns1}jW+=cLx$6t>%om1NP znZ|jj*leYX#v_=Gup5+{EQjXX$#J?N?`r6nNzsud+!OgLK5Z5#j-9Ep3bo@bemT0N z>A~(Zt4k~6k^XHJjSr9u&)j1p=I$?iFUk_wXmC!AxGT5ctLy>yw3aG3{l<}=-~rWq z-;vK39B1H&RyhuvXojvg+QH>;F8setpJ= z{yByBXAxK46Cfx&V`xArJC)Mw6-ZZSN6W|g2OJ(eKzKa3g}%}T;|3i91GRmk4AuY6 z3Cxk2x4wv7yn5AxbL)xF6FOoiI5F^_#(H{*Po~BHS`?ENbu+2z)_mR24E4Jyn&E4! z|F>e-GxeHJ|9vb%J{8HH6xK|BzFS4#0s6nVVV3^se>?6EZChirFX;1Wk239=;gA!* z)@IyJ4!XS;>8;SjzY97!9l(zx56bT=$5wcp@-&|f{J9-ZIjy<#E~BvQekW5?paOaa z7Du)UaMx`k7OBNX93x&c%z}+5Bt+E3qw;`e4ePDGsQn|deczFZ*DyZJdWf6KTn#;h z#LZI_2qc#G+kbp5TPuTP^PGm3jo-wSDHW_+UjxTf>7A3+7w;;@M7IM*Pc!N2Uro@< zQVlh1?K?$GAPvC6oCo2I$yV^Bm-huaMUc^6XslH{`8@IG=fs06jZ=@@au1lC@opAi z=eJt~s3&{C0P)L7RJkpF4;7d#9>x9Z0edm0mjUA|R?93NerL*nC58fhvohkLy(%UW*(i#AJpNwURn+EfkWvvwg)<&XCJ^w;_O?`Tlbz2u5B4)D;j+j4+bQh6{z9ko+Y5XJr9 zvbiJQR3h+3NX9^`IwfCQvpYb3y|zvzR1J*+olUzlP5upbMBdvbYa5=w-nf5U>lWeyt zz|rUD_#3IkRtL>>2~4=HW0;e90+pW*7nNB%d}RMITZG|f7TAsptlvswrmE6xe%M;z z)cfAp_S^$x6@_JBM>T`aT{HF99W$jz25vfL3KzNW_B?WnsvbW2-#%!3Q#w4)MB}%; z843H!o27(tct>=Q7fKD?YT@jS>mawPan|fhWj(Te;A+-kYFQ(Ch~a5RjPTP?DLaU1 zJWp8B#t|c)rRm}rxtM2LVBx;v8}IHR7M+`6}zTc zp!K*y?xSu?FHe$TC`2LB1UjaD)!e!R%CVnPj>nHHF zX7F9Ju~V zBb3Z5;ctCm(Ry|r`!{$WLup2AZPk2>0B~d~EN=tK^GOI<@9z!8&6Gte- zCg3L5#}reslF@(ON`d?_4K^K>;~cgR31cY9{>QZQ{-Xo~toa-H0&0F>#k+!oPFV`^ z>OkUsu7%}41g^&WXt{%_{iJbtA|IKd;ilzeC~OyYRK-{)3qS8E zv}(BPNsHptW%1L8$g02=>dm^g(}#dhYRU@|@llPT*j8B$>85tIoXc}C6)Xq`p{nCkG; zN7ELhkBYG${Jd|r%V14d$Ludjc(f=Z-cten{-%YQng-s-uPm0PK@}9Q;J6ELW}(&S z*B4IU@&@qeekk6bL*x2e`(O4W(Sa!4s zYC99&$y?QXoBo@h2cnoN-hQX;*ZP}5UwVrDGCjtKOmi26a^F@nmiPG5N@(#b8^rE5XJm+_LO zD^63^9qU^BI*(fEy48wy!@;a@QLZ_lFCy{o8r|X zOwCI}B!aďZM0l8A2JvM;UG^d&`y-Z$Hqy)sjOUe!D3TKnU?t)ckc#u}bH8ceC z0+YPN{$(907mfKdylX9AR|fyHj2cGzi|6*VI<$~z!q2jtE`R9|TXi(n{cP`H+YX## zl3iEc0~fB(mY5jFUq^~M4N-Rtw^v#BAU9GZz}EP^4`&os);fRPvu`|HFzv~&;AHEa zyijJR$1+htsM|lsZOh-EU$~G~A7Ry?ts6F+4uGaf3DnJkNReYiOz2=7FvEz`QWCxY zc0Ebm-qlW~D}p?i)u3||?qJrwPC{LxX_!duFu}?p=sI!aG z*+fq$v4V~TH-<6FylqPwYElZYIh5hAxV!!qU z1$F@V-R*!~_gp;8iCOwtP&qHF@`>(+@6pS;g6Oa#u=gc|#FmRwBB>5W{|QXEV)zRG z;4h=R^a5K7ZF)`E-tdk(p(y}?z`Ae*iMckQ%-2V zdjYXkdM})}-1wUhfkp|njj5wN9(Nq}1ZI z)c6R3`@#^{lCf#J`Ggq%!Z?7$w?Aza{Rl?c@ksqaK;T*(J`BBoX_!z#41cWE8VTPlvzTpM7?FrE>C=YKZ3 zY}V+P0K#-2iWZgU+uM)vlwjkwUGlnCmZ$aAc%?R zp8np;$?a<+bz!7MqGDEX^^&gmSwERA*OtM?4gw8mdE^KA*)Fc22^expvY%*tLEAjO zNbteZF--O|u{B1bakxU=6Wp3l?y2Uf?P(!unI7>IF@}OM?4z)#Ju)Ve z7kH>C@U~ou0sr9zhKSVrtmTgqs2`sJ?fs5+G4_?jU!QMRXQ4JMDCp|aRwtMqb+;lX z+S(a_=}LBYq>|JMl-U9KHGs3=c?W!?zGf)63-_xN@T-YJq>S|MFDx)ohN_>kh6(_hka=?P?w@+Qn(^I2Id@r|ViQT% zJ!W$9qMp;O7pGH`6b_Q_#cWgo8m*s_wn=P5f}PZxKkLbKImOT&mEz!b+9AbQTj?OQc;IY@rO!RdjH z%eKlvem93Nvc?2v+GnZ#ZPTp;6p2Z|aT-*7OEu&;dy^bA)Fle`OcJ$oDS4ZJnuTRM z;bi)(5ArC-5H2m|#(mh9Ik+vsit&5V-1K5|zC`k}_pC0o6E@r{jg+NU9w2KhdUNY%}Bk-7lorhWOPD^_x+$Mk~oK(Y`vRP ztdH=V{X9pt0n5-!9|f^d=>eUb88#Q9 z3CV{|Ss5KiTNU=jw-0)^Y+w~Iqgd1-Mf4kh2|x}hdN84Ee+C+yp?t|uh1@i2PNgXy+W}eb{wWu@~~b--jr=j`99G}^Pt!{wnTXf1^SS9Y**|K_oo&l z9+=zrcGm%yJ+8E`BjAC<$M#-xVN(?@dYAnV-f@XF``;Wp@X_Vqk7Eb_bJ60DYbnR~ zPJwG!u39_5u0goV+JxEwMx>fi>OfYX!d7yeU{J`HNvPdmk+lg~2in9*?7fe#uhTPf z7RDs#tUdq-ILDnlbY+rZsqW_B@<{8ny!b#RTk`HK-Aq33snIc5(i;Y8;ZsQjjzHOH z(ds~9p1d2oFD^6|(a~Yi^IUQPU-{&Ppo;{>=ybc__uA29A=9KS3>IVWAG)pN_NRVm z>gLLYO%dZO*}T8FPw~t4NGE!x&$QhNje43bF{ECRoLv%nsk~1*qT^&SuO2u?s1~vS z7h2sNARS~xQL*N|fo*T;gL6WP1@<{F=|)A^grQ*!bf^Q#?J(T{ZtCocKhjC~-i34@ z4?*>eF+Ho9862f&oP5jR=_zjY%y@-oP|pr)J<_dc8q(2w2SUETmue>KOeeP!0Z1m? z_*mHL4C`atp!HJJ0IClBRUJElX^IL#Q!uyM-d-U^772f^4H=PpLqaC7+|T6$i&hqn z0ru4j(v^DVK$=+{c&8MU&j{}PqW^s|#^5j6tTAD<-(0YNCsz)Fyv}&zxGm-wBQg|n z1Ig=u)bytXuU%D~xqBzq*nf}*wa3D!cSxsSi`wNcJL99_A1nSaG}3rImo&fJUt~`| zZV#J(bW;ikX@5nP%!+ewP*P_q`(F%}IQz5!&)#xNSBbRXrxmSin(%Sb3pQf7ar;5R zBa^FNl9};Drl94N5_B98uH7GIGH^`<5APr(Cnx~A!cBP?vhX)m; zVjXSYkqY-VnwBrLhH!M`rIUO#pF(Z|=#>pxNjoQJTxez#Fug_dNqOMl0H=NLx79?u z1P^O7X8Q{>42ihQOYUp4`^QxuCj(7SGz?joESP{B$+{9RDKy5(at0t~0NLr=0pl5N zH`ot8;xIaCqsPr*o2CpR@V1NWxcb>>v{{OfI6L3G`ynmi`Ij-Fm%fb=SQxHl(W`e3 z+PY5rFLiDXjSNQ~fJ?5_uei99YrZADNooSXCeqJ_wWm{r5HJ;e zVi#n4P+A-v-rI-;dIZzuo2^7t6rdNbd=TyBV|J3rhBjToE7YI+oELUftHbxr{bg;k zz;Pj0=b7p4((epBK=rDfuhn1THb}=!9S7#fIEPaRHMu%^K? zqIhozszb&AnoQVLtax8Bx5-^(9j^#pB#djy5Wy{%iw4;VQPGWSt$D?Oj2vJlpCCRI zd|*siA(~u{hSwD^5&33fFc!H`)R%XCB&93(z}IJ!attKz5VBEJN+t(JJZn{uh}Yq7%lk=!-%Bp|< zDbNY`j;y%W*9FE(Ks76P)CY7N@&5P6DHhN)rl<21sCG%xp$J2a33c$oRx_d9tCy=h z*3n|OjZiwZI?N+LgvQ@D`wEAv-Ly^eB|L~54LT0#0qyzGB%f>iDT-x=#Q&PmoZ%Q9 zK)Cx^TD>wMP=pO#*g7dE9T5{P58P&{Ky~*nkww3H?}#1z7GxJ+9|pt}Yc2|M ziu<+RkFEB=*P_<94D_UNMRsOu&OzDP&HuiiWwvM?aRuRQ{`L7&Epwpak`>Dcg{qbR zA?ay(1cNO=)tXH6Qf3RbCDMn#bf0`YBR|KcW!*{7JGUkWBmNOJ>x%4tuAi;%E!txv z_?feYP%)Cd(_zdiB$b*IhstlB&%DDS?N7377LE$fi0R$sl#fY|v{!y%qqC`w5BvAD z1#WErlC!COMeNiTpo#uu9ip6TDW5pmywWZiJ?@)Bd7AizoKh?t{oQ0H7n1YGCljtE zT{LxzdC)zd29!NYrayVH3W4rX1=OYjt!`pY(5x43HpZE39~;veIts{;_v~#Vla6}$LLaKZ6x2jRq&`X4%88pCM|muNnIzOBD#WYKj(4NSA^uq z+q|JoMJ35&wnG_(z=-ZqUmq4<%K7$hFDa~O3;iFF;4}lma@uaqaePUZTh)hdCov)V za}EMU6yKUX)qF8MulaH4A)B~~B?TK(h?_$%t;Szoe>FMYcBl>%OFCvxf=ef<X$QxP<-cJ=HHT!Ip53`++@$@E`%be~u$Y{_e1JPL&IoSTvAQ2(X+r1k zQYHgBenH2mzO^cE&SL)Lg9vbfLfjH`k}lM*|Bz=fDx4hyi!=1=*$_6SW06D_7Zt;1 zkk!|Ic6{e-eBl+Wr9KC+*Mw|nq|J)F#m>0Yb&IfbU^Iu#%=>a z115m+UtAoMvHjv;m?KLLE3Xx%TAYs>sL4~naEdcedd^8{%{4_bc1!PY`R)WqzlL)H z(Ls~-&S=@UqkOdEkYH~T25`w836F^w<$W?X^Bf4h~y^CBYM_QJ)zRg z(_56a;{FMHab{K+QE4^@^?9tf+pzj+tywcL*xtGN#f`#AhD=S&p_O?scsHtFfUOv$q7Y#S|(Y7Y9Jx2f@<9W$+$61iV07cZDT z+8(nl!?680%$K2|DgC!e(`BQ?f&l0XBA`DB0_p zMN1NoZp;MeP0hY11J>OCctKDTKlqQhZrwE3Buie)1}+IHYb4Tp?aN5jqY= zJ0dX$J1PvqBN_@dgykW53XjSS(TckM%T7~i6M=}(5#-(^gte=b8itRV?yg=RPxAl7 z>N(`>#Ynt&Il!7~IIOf28t#7Bic69QoNDeUzW=G|9^ld=2Naj69@N=5Ee`$%2VA^6 zuEO0-Fch?%{Dv#}cs2A@SqSu9i-z{o^_vbZ%eocykO4|Cdg_K_x4ox?T`L!d&;d%A z_2=46f+0H^X#n)geLI=hr{Z$n=7-iL0{Y1kDrEqg9tdqMRjI88y)IVi1k{oS2V8f9 zCKPiem(mRs3ZR+@<(r4QEQV&i)dU#K;yJW;kMXZfXdFlvEP zd%KU5Q+btxBUy59?F(SDt${mFJ!#sNHA#{*IJr3I@CA~VT$W?7d(ex z{LGrLrFov9R(f$9ze#sC6ixwc1e9BG7d1>omLHIz<|N3Ojcj;zky|Ig3NS?YjAHgtrF=5<9}l{(W5O9@S}( z{8wrF60;vh%^@a4aW1OZAL4p=sG)q%O0{Ghn0JMrsFRw^*uBomqi&Ijh^4lH^OO<_BiyufJXHs<>poR-6TJ@PI(#h4CG z^Rx;>?K}_=P4vkA9L`{k>n*DAEr(N2&e?^prB`-heZgH4)a`N z*kgPOU z3Jl8UcFz>HbEt9vzVl|JqIJp?wtLwJHV|_nxHtgZvDLta^h^GCX;5B_NU+qS8d2F% zbmM#fy+s2%;-@&X_eAR$te)pzKGz(^`JdY~EJYaIous_#+Y#zKz%SO8|2rl%~L_Oo|_X0)grH)=9ZZum#V{ zcyzr4M=)ZYO+fySHKMjVk*qqvI52(2Xoodeh-v|??yvam@c1IW=p?p|rqCxes8e0m zbsq%+jV2p1W)$hnhXoZ2Gk~=^@^78_kPN?3O59aO+-oErL`@G#O^=qDo-mr8LCwyM z;xYkKdF0JX0W|)4UXmNZZ*-CjFf9W9zySGcA)JYAo>hlbV%r2NGn+MjNp8s3!)CWK zX3#sFHl5rqN>q(R&yK2Dn&jSBh(+=PDa2Y$-~te7L6YX^UdPBa*}L#nD}%D@NIH(laBVcNXgsgWeToim7CcYOx6mnPf%5~^1Mon z+`L`tI#Q(3QVQ@Sm6ka_1-XThFBxV|xUh{Fxl;FKVJ%V{g7L6q zkBo#8Rf1s3AugJ6+{utB{USRv60_RVYcnE$S1-9dBEd*6P~Y@v5-M3Hn6_cg<-7P= ztb|-SaM1K5Pa1=rkL}C9P10gyQCJJ@Gj%{<9_zu@okomj@5tXP+hl@V!AnY_|5JJY z>Q^xg1w{*x&ZklQYUiPBvQ{3u)tyr9-(Nguzi#O8k^dExeted8&xODA?=@FeJbqY9;%EN10 z$60#W*l}wA7vKAc$6wyh(^CQ-h674DwUW$YzVKV60TyB$-Yj5YI%?{aGazG~NCK=Q z(M!Wr%K>Gx)^Tgee{W7xn_FGJ2@Y8R1iW5WB+59j`rL015UPo?en)kDKNnVjxf6UO zZYSgAOv`UnjgJ3A#-)6_&*z}=Hzx@YYQmuXmQ&Q`b!oGPp-@Fx~iAR`73}bK1be^uek!uG!GZ(+RpIi)Ey{-(n8q&{_A9 zo$%6hGqbFbx`+|fZ5SX4(P1ssZCSviufAp^<~=<4#?vX56A)HA?}Ek6H!{Is>a259 z0=(WUU>%O~0u7YMAF2fVCQmgur(Ctv<2jdx1-o6H|7Y>n&{<>^@92M6|BC}w1Irkl>fAXrap=V0$b&y5^|&`}vY3Z&tsV_N z-+$P|QH1@FQ&(&Mr<`*bZ$uICv^}2eO~pxn9V0*1k;fm)$-su-vla`y%q$jGVuGsq zq3dC^2Bm1*2edrty!vmZq(HgvTu=5D%fq$X7HyCR7+T4F*xxzpskskg$PZ=dX6Dd^ zHIL+E$Ef9#+Heh+Io3)&9kuwU>k7P{@={L5R?Ro0zOFqtr5!f^@BErS)@dCN1m9aS z!UbBXju*@qF5LpV7iBK<>QRBxSMypJtDE&zM%T^{}^47HhRi$EF#nI zQanG2IrVhzx_}R+c>Wp~o_H@jwFPpBJX^Hel7aDRefG zkhGv948!|@G}pD?jxXF?rI;R3yn*C{AMi(OHvZW`rSMV83w#5YWgnF{CBlOw#F{DT zMJ_&x*ji?O`}%er~1<9iy@zj_e|4VXVP`%MKNgEZ1Z0?NK3M)#tQ|L z9)zsc`bOZ%ri$RbQMTgjssSvR&c1sW4hgT?YT0yZBHr?CK2WISV_&Tc~w2_R6LFlP=Cp4S!h1Bfb*OL?tLKw2w^$x@fZW{EQm`S-x2+^ zeTQ|D^MxyhS%Np?$Xb&YS1+6^uBEDc)|%8jqa_W%s9KhYCM>xb`btT}7N|YKs7>d5 zL7Jgd?ExZsmbCa~$E#^&KJo-{81&WIReO(R?=En^bU||YgZ;T#C^?c15py~rBG$pzsb0x9K4_(mlXu$ zQsHdLiQ7joaxH0>iQWMKCTRI_er`R8zFcJg$U$FA^)_bV4sh_9b~wOqsa$&t;lRZl zP8OhdlnT4d&K3=(nm!5o-uyRL*?r{U92uvxX_B|%R>sx=PpIn}?Qlpoval6&}Pt``0UgHbgqkbNv=UtbpPvF6gtrAq9 zhDlZ$8fnkO26=K)`c3`)5w#I^EaIPWyBWi1uSk%&1f=|Uh5Y7#L+XGaG;BCVhm~Uy z%jf=do1F^$n6er;jRu!H_ZcT|Dkgiu+ERr3>iu9Q3B=BhSW~({#e1=oH$SGz-{bfP zq&4on4T_|8^P?1d4HxsumiP0d*dBFO{EMYW9i2{T3p8HWu6ocqik&$xu3_lc3sctE zp!CVmhWq@HiWjIG6fZ*#HjlGVg$6AbH$qBGt`_T543XKiDI@z(styIP|1yULOgXzy zaj9y+hS_+(=8gqrrlA%1oO0)W8fQ5b=Q*-+5b&3hwt9G%v_EBnj;iBxrjB{aJOA{d*R0@B0bG_k%jXZ4{PfYr zp9Pb?nG$@1MRqTN8|~|__Dat9cco!>IuaS26@Bg>_i@=PP8RkC^E*Jw!bUb8 zi(0qbXvS@I&%`Xe^=#nQ{&(MXe~3BPmk*#J6v-*s$ZaUII^*6riw>_0-)q8H@@Vis zVZohU)C5k;H>$X`7-=`#p160N5VOj}C#vS(5JYs{Dfxd7O*{^?4I>Zdue>L=#K-;# zYI$WsF#Cu24KSL}LmEJAqaE^A>t)t5KS7)P12DRfQiW=n`t#PwP~A;vF`O2yPVlEa zH4AWRiwSk!ib7?Bjz$eU-~#nMskPzC!|e=yt-soyvxE32kOs_1dFl569sqF^W~cdz zSM~eQAPI;%#R>!c>y&n?&+__)GG~I%Y#y~mNyL*(;-QR1SGR>227nXEFC)mcGh~Z% z6?YLjKZj%jPo$5CAn2|C+xUU{eGO8wbtIg=$}qU9VE95aKx~D|kri>_L-jj6>tr?> zOLwk*NExymvsg7OAeU3d`0T-`J=?NuKCE~*Jffjg{U!;PdC39x90L!#JCh3BQIA}T zOUA)Wa9-Wf#$b&}SWxSNjODA8+fqSJb+H|AICz#7KQqRUL_hg1b!VcM+1AKYD5H$}T^qZoJ~nK8syACRqIOZ&VB zCG{9j2;Ut)QO@W*2FWXa{VOW7-mDYOrZKbHx6`WuNeY!6Q?7#}b(koj3g)Q+Ijc_; zDHzy6&vJZIU`nY|)&qGAO0ov7q&7^X3)(%RLO}1;<&oJOw?R~#&8Y^ny7V)o(5RXM z(*>n!4R7zJuAC*H#cWARZ4E1z6Fr})S)z~2{Bb`*;NNS~nmSuF?I+*wp_OGEK*J zwS$6Yz_teAa;P=K3emxkSycz=lJ|vc)cB5OwV{i1@-VfGL%N?(Hu`q)agxE6uoH6c zX!*eHg=egT%?#L_G_2b$5$;^4qeE0FFnj+jTVY~;yETMfnDpv|I8Bb8D@!cq{-0$ap~hbI&B3A|3Pb(pzg-Iwk|Xky?p9e zGZyiyE_R#)!&r^Qx1+-}$Faj}!;3lK6`+)z z8=G^5=(3ntBTOKnyA~d61)%ISOdeL9R6gjozn;oKlD9vPzyY6Ob7Rb4G$Ui?l|0pP z-H~B#Na3&eEqh(h6h6oQu315WOdd*jXA~yZj;?A0~QP~-S@FiCOr=4l6ENy&ENr7X_v57)6 zS3S#6cag^gY86&CY~mu42AHJ9&z_ApA&ccs)f_CW2R&%-f_KCnOw+ZFEQQ%A2-?ybSZ3WzR;ujbu1v9i-Gz$Qk z*|V>jK(1w`+hwz)!bLAgca)dlR@epxe`tYyLE#E`g@;r>TieEsd{v*Qu8fIk^$jER z0nAVLBBCb)!d(YT@xvn~C7)rh2?qn$zCE;pgZ&MBR*|1bZj+?Dbvx2F*JcmUDM_$z z_FBNb0dGl;+8XMgjF28JLcx9e`^7Enn(P4vfk-z}+#-IcHnlq822jZ3#Ob_X8*GxI zp-H^wquuK8&l*SW=PCu7ZDx0PqkJ7PlkU0X=_X{77cgbKyH<;acp04yN52q16t9+l zo)ON8eVf%#*+g3DakAl=KgefGp~J~IJEqB?qKYkL05o79?PMXS9TRkMG+~tS%wJIw z>nRb|h($9z-Yc<+WNfwSK^YZHfoQw9p$4$>YaIe(FRonXvjf(Xp#sFHTXPLBK3i2Yjv)zKbEmF@3`xB z8#F=Ijev(`4_6;t)xQDYfZ*EE4@(8@nZm96#*rrv-TJotdelq7>X-WP zg8?tyAm^z_YIX|6f%*NJpZdx&5|RuH9V+y%RpQ*0*EbwL-}?GnKQP1fNbY*>d}8CR zs%NtoBq68Q4x{Yy9!*~>yfhs$2eqV7_494v<=!9ga|z=|Oj-gJqJhuXEXGal>J(WS)@4fxDbsF% z94OqM{19XN@ z74s#^HAr}#(IeLF=AXW*y8a~2BI5iq4zT=0J8f|_J`XGWxNW)Gx%ZM+OejZ6q@<07 zK0wp$B!Cx7vbVe_t1~R70o^`^d{`p09&LQnpfj(ocDiath|2X-Kvz%Z^qU4OS-Y|_1&Z++pE^H{RcE-^&O z`rAUK%%ahbO5YC26%KQ&h$R^?c&_y#HL&kXL3Tw1R?v_7(W?(@BWaV0@|vL?tTG`~ ziQ6u+>Ip{jwP^2(Qrx4fvPW^ZS)Xvwv4;2Bba6+2hhIP21Z z-YzDhHq2R^+VI-){%i0`3%a^Im6}MEb*l@WLT`m?@Wuu32_(F-daB8u+ zfWlKPyBHI+elD_0;j4{7fgc~~P z^{8R)NmR&0i|MpSd^&(HP`dGu{dc=Y_jy)n`GPy(#DpjQPU(yr(PfPk!1IE8ej6ilas9 z4418w;)t=qS~w~n2EWxh%Bb@lV5=SLqIph`z}zHboA5{A7=yP1i&ji+Ek#FM>>*wD zZt6$H?=SF+bGP~#wRIlT_{lg{p4;HqDf!1D1!B3|2%x}dIKKe!L(*qeSfypaT-_vN zs(g}fJ8yh(VKnLRU2F)?q(6?-uw=up+a??~utB-mz21*GhN@EGGO`=YR?9jD1Iwt; z!Eo!GaDL=%IQd0FP$w_TJyfT>H8!_c>1r7r6lF#Bj*Ih%SK4(gaj#ZZnBPI*`w?>= z!LAis>%#+?6~yg_lFe4PU?#rRx)Lqmwy&vJqJ4jax9?R9717N{1lo4k0nnu^ziwsc;+H z!W*sJ>WOK~VcrhsWwR|krkvu^y}%d}q{Jx{Yhh}BCyEo6YyJ_xPq7m?`~tT_w@IF~ z&!KD|bsS}rghjGZ8>8UIZL{h)I5vQ0G2!k8+WdS}2;2Yg&J1)mYGNx^6XQ`LLff!0 z`6xSc5%1z)2KlWeJQZ4^Kr^(3)#`Gz+P*e>F%JyM?{*z;fy}88kZ*<7v@+|_o())? zXLt*3!LKpO5^gYBh#A)@$N*?PWFH}1AY{*g!2;W@JFSX?&@OD8cDi(<7=9K9b>*@J zo4lhV%-8cEW4(p(&B;IBuwDD$3+3>USM16u!ABWf90$-Fi8Z>$L#Oio{n9CWz-21* zcO!9qDf+JE8a%rr?h|dGe#_Pa#|5j?`~(Z^qum@zO(9L#p8;IRh797NMAzsA|C>Ip zR|uP8+DgY3Mnsj(G3mVQHal+8-X&^5^;BL#7?7#nOgxV}-^-rsdA_ zYWSvQ@?r%xb6htt$K?wn8Ya;BN=6R54JX9pvvGOMlUrLb8CsNv2xST}m%K1JLR_Y* zG~}G{SMU4_Vr(}Cb$2N)ojQYFp9Z9}mw^B#Q`NBC2b0Ia#Rvg6E8`F~*54=Gv+AVr zd0S9z(VlPWqretW(Q$c)7DO(7S#l;oL^8~F6w!;?VPLU(IU?Lr4rlN&F9YQuksRN zMR8W={6ME!aPoEBIBM;=stb{WKD8>GXWCj1E$%{Cv_!M|>e`Fp55NcoICFVT5wBW| zZ(y1^;8JS({S#bWlxXm&H%^@T+tGMwY{2P5=bqmmSoC`ZValcZ%1R4x;~UJ}giPVf z)94^x+f%neOv2#hEWdI7jKT<9&afYA{%VvmM4snGT$&3!D0fs(USp)EjFGy#<_>SXWGm z5=unias*BhoDYn;<<*IceA%%H(0#EtGO_^|0O#vIQvL#6z`6OVI<`cOZToafXW2Q>SZZ z^8n^y8{MXNPcO!ZZhT>5uYk=`%>wR?1deO~0zp@oD46x)#+v2Kxa%0`s1Tj`PNsSsd zJ$xd{Rd;q+w8w$&=WHok0Q#xBuE}t+`<_ri5z(=&eHyg;tIiS`3a`mi#j#ia#!+nfo3#Dn2=MENY|; zV^xX9vcb@L9d~+vXSJ|Ki9tj_hzPEs-Y!R*9CE%Ideo#V!P8f?S8vEHxi-z_;XZh8OP$4O4VV1>wmc^6*2!M~hy#lIMS9=VtB;-)6*Ua=~b zBS!E3avl^>7gLY4T(GPYp@VvTV7<1`742WgEM0`y2X0`ziN~zNQL=*3ZKDE~ugU5= z>bWnPu+%w+b5Lo@oALd(eMMM*ha*wM>zX6x7$IeM4jnB3`rB@oe5MAuS|whrU=JN! zbBVMy&Dlfw{|aj6xq^NtVl%YFVD`US zf8g@TcwSfZ>r8WV71y?OuuKR{tYB{$uapnk8!W%@07UPv#b&9Nl`|7P0PDb3>Z(R- zh)Ub2dY7p=qY!ukZ<}ae;Oz&Yf_FZYW4Qu0cFQlgCvnR}3hs23Pqp%6hU%U7TOa#C zcg585Lk?klC*yyY^yS?PDeWAXTkL{U&3zK`g#GT`vaqW*>dnS;K1XCWoGZRS(f!p?sU0&=4Bn6tRb%il1vv2VqJPy<8g&ubk$mccy&uu;?cltacy6J52 z^viIy=!*Vcz(t6&dKkDtaK*8A=C+Lxck9kHD=8%Uw_wgEQ?v*u9h+WpYH-1-dw=UG zZoL&XQ*X*IG_P*+m8vnyM-8UW%@-?7Qu0yDsT1aRK5eu7c9wzmP=2p8LgV)g?i_jS z;TE3z_kKY=;FHaO0G?|-t%z;%>KW{(_Aw`%m=obKn=Y&CMsMt4q6O16z{e(8zixVpjjGq`TF~55k-dHc6`@gBC zy}i}!KA@f>1U>8S5F*txL-h@)%{CbpR1J<1j5mbW1T&IBzuHp-3Of82;9mEZI*^2g z`yf9GDuRQqX@H1HPP`?64u#C}+$3lp4v}zcaz(`^asUdd3{|ry(jyme^f9mruxbR} z?o{DT$`h-_yxKcOd=W#@3t<3m*IoL7ePOkgb{^?MbuG7!U_e3mw_A8{TrEFkzauZG zIz8`?C0`iM^@cVjrA~_UgUbna9bZ`U6GEyFP*|J^L7M~kDDIIrTfw-FR(dC-#wtl+ zMTm`W`|z!CD1#7M-JwWI_l2=qWO`B&Qf-MTkg?e9dm%RB+?7jRmv;qT(KpgLsi~Q1 z4zV;%u8`K#nHa>xByXu=$(|5HrnLwaWM;tyXc7_~23RK5U1aNuNHT%Cnt^GtlOv(UH_H%ivqRTA9Sxm7a zPH6ne^^xk32@+8KpOB?&V^k$T)!(Xr6KTxfMk8!J&0xC95gI>>lRSQjwMSatw{1j%(m zROfht#)^@b3aZ zkE%=BRB7uIh_ys4Qoda}CIEHI@4fZ*)i|C8N! zS^VkL8yCMXr@}^)=I#DBG$8+e?COh{`#NSj?^gW5iv>^C4+Rw7|B*gG{FsLe{CG%RrA%N-B*wEGTu3T=TK{W7w!w({Nc>&OPl|988@%^(~$k;;xD(t=56`< zaP{Ra|2;3CS90U!>gQ`8y?JOo{dowGy$JtLOZ=-2LkFtHO3)eo6kl`yKl>K~IuF{B zEHA+N9u1L)8|OSMmD%<8d;FFoJs`rYYqenyQL0~_xI1{u})DPNa$ zGt&IqQhtFxYh+E*?`7l@xt=FlK4c1tAd`5vK~D*al-IE$SOgp%)r3TkeWB0S0|s$o zZoE|0e=1#DZiNSMCEgVr7j^kI=bMCUvvs|i4W@qgxOg#nK#-uu>ZnR2jT1i%xU%!! zpp0&Y1D0S8%{o5IMyh!ZZ1(`qn88EYkb`xVBeZe(@Nu* z0MbfZX>^&@H`A>hBX0AR)lY3iX{YrJ>e7Qx9MH=7h7-WXA{jl}ZcoM-p6H&7GWf~6 zZgnk(>LDI=RmCuuKS0bpkFq3gtENqxS zIEM>WM3$$><%EH}_PWm~CEVLjqjthUM>C-r|2q}6`M?of9Yx-9&@?y5#e6>(NGO7A zkvl=>7U;}ollC|o5e}H78I{}!vN7Xo8VJb=X;VmS(U}J5doR(5I83n-FNzmqu(7>p zySE6j>0=8fwm03HfR>(a5@*finWG5J7?f<#$OTbjcxdCBnmnrKhbQQ5vRkOsnIz(7 z(_x$7zHP+m3~wC&E#OuKGhvZkh-sY-b*#K(>HAKD6g=g>>_Kl1i2OMRP}Foqxg`$= zvN@JEzueItp}OoMBAu$W3!Z{e3%H|-ry6<8wp)gZjw(aU?>M5N5Yg{U2%fyt+)$)& zcl-Ho%jaDpN)s1nbrqFuJhjKl+Z-#U)pI25L`p!PpW=)vcSTn>B?}lPU7BddeX(_1fLj&)x_ZA$HrR@dWppPt`cmo{W$Q&xV}o zY)#sI2kPA6!&{)6dIC7~sg6a7rmp}z74}0iZbW-Oqi8=IOgQFUNRi{@0M?^tY~G(u ztsCBH`gEYR7?1DH@Nv`648qv#3*KCx>e8)mt=@h;%FDTvh?3t z`I!smz3{EP{fzGe(iK?6n(aL(QU>t#xs3YaIK_K_XmKF*08kOI+rV*)rSwQ?rpC!! zh@3VAMQ8yV(>A;?dvrVnb zJ*8b9Zo#hGWhqxDdsbR;2kN#q z1GPp2FJ6bbF=|bXn?9cVYH!=nID_jGb;Vg+U+S**dd(r-*tG7)+&cK>e;Oiv_=AXI z(tQP>qsZu9KQj;hQMsT~=DWjp^vv_5ihn-SXlz;MBlhtxmgODj<5MCZZ@+QDiMpUm z!<(SD5h7x?vv{=2i3Zqqs4c!UOQN7_cO)TXV=Ht?QeGa-}weBGC(ng ztU02vO-JWG!mCv7-bE0nh+ri5-CyJqBx=$Z%t_r!Tj$d&y6bOh*Uc5}PhKN!NIALA z3P$K1Alx&;@307l{p#`83tl4lkWva|bXP}LE&ST9({6FIce4X>T#h|>>MKV}6r zZFdktZ^+2JIB4F?@NUTz5;Wxyia=tecv1lIoMu}hz{KosXv&Xv^fZ($o@A3tPR{<~ z*K6n_^YrGbNLPZqPvCN3?@Wl&-!{9UHhT6>_U?u-Z|@<83Tq$RoI~ASD9FYEVDp=U zlkqN`1IeGIoMFer4Le)KB94-lF9uFoh&_k5CE%4;H8o(y3?2H669@kfjkHox6C5FfHv6AJ{;Y`+j!`Z(wqjXv^<&v`-LDhpAWyMiDYcFoxy*n8G-wa2F zKhhi#1n-L^k65%7TQCmgxVWp(*6Q6fAC<$dwjaG$J$@={vX39v*gjJQ)4=(%9-z{r zLS+KKU_mUA*qYtJp+ zBRjX|Sn0$xEh3aV`QrUUYQvsYj9K1F`y9dF$UPkc*AS)^3R8Xl(=K|R3^a{;@X2?&82Y;W_E3&sLcDOG zej$aA_RDtfk_z0#YLF!vj6_qBh1b%(vV9=`+ly!%DOHGf7xF?VXck%Qs)=Hy??Ne2 z2alqiVWk5Zjn%o&n35w=lEG7wC7%JO4(V%R1h-S->Mc5X7I5r9`;pP3xmKhFA~Uko z&l*jZU(0Y@B&LEM%A>xRB{qy*W*;h{r2|gscl~F+TDm9}4_pP^l@wGHn0VWIj-Hk0 zg+O-|*E~n&-_dNdZBfWQ=}_fBsJD&oZGAXU(5S#s$ejd#qh{+*PPL^uWhsnJwHTZEkrYu>OA5!(6D5v zvfTkm|RGv87C}`?xLpGWwlqD_@2}5T8i4+OH z(8_o+gB#2L_H_q_OHn`Xb)Vzh0l`4EpEncml`r1Rv&#n0l`qD+V3q;QKjMqQCT@tG6=K`YB z)eoAT(ELXbEaCdiTLD%lSwg)IYKI6>=ZKgtsJr#O=gt;glAC#|zJW*2b8FR;_Fv7< zURHVvmYJ(5e<$wJTErCHU4=FXOGhW4uPXEJA>G{7b+#r*DgAHD^GjrDpFVH9Md!lc z*_`up_Fa|B(xLy3?y)pAq4GpSma3smvR99vFTLXK&C#8Rs?{cs!s0A?5-jXpWv6d1 zHBFTf$z9g9@e5nDVQR*cj;*s%PlJ#;$4^30@5vuu(0~K{?e8=oRH9Alr3A|7U^cW%bqQ_ zrYu{z8=|AL8(p<86aSu)DLrlT15>|P4JF*X%6X9k)@HlH)0W!)Ps#? ze`iI0(=Q)Jf1(0o5_I0HmnnhsCapx#&x~Bo>5+%4pKPtv*`{q-BJx#ns7U*ic&-&< zysD}(n=}W%C*xll?C-F|DsCPp$BRY0If#rOBfNP(Is}$gbX{5V$|Q4UFy&*)EvN^T zJxed=nLLn~>XMS=byg2666?cO2Vd%u;Z`e_e*R6AmwG1(u54k~`cOqj*nwnLpZQnO zACB~h>WP>LK*+80(!hOtD-DwwsSeylOJx3lj3;jn@Rl{OQ=W7tN7Y>N(cn?T;*6N5 z!o>S#8z3h+46U&1ZKwa9C_$$NZj?1x zO^m!eGsFrB5XwGlV?eGb=&b6H>#J*SkWJDVo(3;HPaFRa+RhtAi|>OWc7 z;1oRBJ_%P{IZHart_>1NJ+hB-Oz8gPL!r!Ji`X{85 z)PCr@URwLdu=|O%3PV3&JIiS+YFNR54N;jX;tWE`5ED4iGMu!V ziw;qv_wdoSyXr#BVQcd2+ol_qe5llgQrwPP8@w+jc$6apKqQC~v3%vzvw(S=GP#*e z20-gkfzjx8FkNgAAhH5LOV!AM#A>ripLEx5VqDmbP-ymGfo!yT?Rpfo2lXi(WfN7P zhQDEQ#cRXPQjsO&WbwMQl|&7SHdUY|f^(X{2y+o}3Pq7OZ+tj>l`G#cQ$YbFMYb@R z6NmQhx)N6?Q~hUMbaz7-yvnxV^CQ^kPq%hshq1o3_KiRr--h3#`4+zM-V$pMxn4;6 z+U+}jy!rI~C0ELul5Uz=u&dc<-?n`}_IXkE&irVx|JmyGvn}jrd+g8stA8G_w`wWe z^o3S*&-L7NZ_(4v3hHQYhdC9?ulM?J>QT(T!4(&tbV@ojH1253@qcWiP@83=($J2? zttX@rhpijIkCqKyflBFVj_g+2uWv*or07?w^x8yJ-4pI1e77~Ksm2+ByT-d(3Tckn z(I;|Hlx$ze^h=aU|H3f26*WXQTueG!Y1IU>hEI%~iL`9Wx*`_e{37ZNBZoz7@v)Jc zAw?)5K+)<^nn3z!k?r?K-D*^50Cyk+qNohT3h}+Lzsu(?81dYq*m&gl&jT`*I@{kDj__KI~ z`w6x++po)!hZ=iuV7kQ}ISJgoy*cE_=`^B2l{Zw6>L1(Aq10(xqCq!G->@N`@VddY z4uNlDO8x2EtrWTx`&1w5XE^^jQ#DL8YxyL%ML1ADwN}1rH_e4JA*L!aw~MUhJ&i=$ zI*}`2t!N?=sWk}g9=xdtx>BM?Z|-Q#Tfvq9zb>A(-ZGQ#Cg7Kt8&Ks8pW#(%8l7t= zr+X9Fu+q#xkeuy$c(8f-pLO_p!)k3z24i=k2h)mA`mdp><1tF-ZrJVc02oRp9}y&m zwjUzdelx$fES++$eKX#pS>1+Lp{^e1d6VE!FQ=~Uf~?}u{?>P0yC#8%QIrk@HL-JO zMoz2iX}><6hjYq#!R4z!ts171X|3{7E8p?*jE>U@j9_x~+Irl+Zp9qBz7)pihyP5l z49@0sS<|aVSJ&rUOBiqp4vBal)Psk>tMyI!J@EPT0qxYlyq8-wM!pf%4cvC z50Bqn^qk4D95n!kZQ33=#(*xYKG=3Xv=GqG8o&7EKvCMtD=atz-LwUob$8q zQHu3ma`_gH5;=`D##_vc-Y>5KiTAYsSpZgXkn;S~~U@+`W8c^~}hcl|7p(2#ep z@S2bpJyh;B`k)W<{m}K|>`ng3A;Uao;3-B*n9Sw+)qn7}cXEU60P}ICs$9h@?Rfxq z)1H@|S>*!4XARFkyF8QYkd#KYKs_Mmp18VXkf5)-lD`4+j^R~65>LwbEF=>Cge^1I z+%qZKhFy1YU=C@%$nf{LWagx5j2O_@|HtN~n8#89v zJy697esaL1@hTNS6g?y4z^x{JuJ$M|)wRCRsRJ6O+UU_nnfYX2=ZrWi%7qimxC20} zsgK0&T_cOS7DFsJ&FE-Q)jAvTg;U(IQ3ePh?4WuRS01<|6p7Kro#F-PVrDTQXEn!RIndXV2b8^igwsnAfS_8l=p2$nh>7EaWT14PB_JeBy&lE5J z`i8%0S8Hje{%Ce{e@zV6gZ$};pg%Yd*>G%7yCp&I8cp16y zBNT@ghz;o8Kt!W((P8`%n}4t6E2X8pV{4ce_r>uNqVG~~{x;Tw6PUXNd#sGp6RbRa z0aL!1C)?}eYY^2jMtxeDi+LRP;}$(uMo7XxA%n>?E5=n^{VaZN#j=z4v)Ta!*__o%zDzb+lCJo{|wpbv!69^nTD>yH?; z_wE}pjft~2&U0hs%Oy+FTcAhUgFA4KZdVxZ8TLW29<6v{n~lo=jv1-v(c7`xvU&25 zQ)v;ag&3ancHfcc8IQM|P$3n;_K@1SEv@;Iwa3vn*1Y3g{+aOpL^u6NX4jDCF2}CH z!1G5>jzp|ocY4&t%DMYVfuHl~XA=DnXU4IP8s+ZCn;6I7vkAzd{SA3eho}2I-tYhV z>d5~t{r>juf0qHA)07%A&z$Oo?T=3N5xm|{^&??Up9V}~Ge2D+uRi)|kW%>m(-75& z5;SbLBlGiB)^!t*>v+N+!JC|2j^I7+J`IPx`<;-RaH;Ui4fNQ^ms?!@b&3%jex1iH zhkX-ncL>~o4cx#dM^D~_lKz~$1AY>EJTNo3_~mYhJM`^Q{J}q6yz~Lq&Gq*`wF%$jXA&^~oY5&>=tD+hK)A2po!^|xIWB@X@WtC#r?I#HZnVE(ehNEo z^y!1osEi`>=Q5or(6H^={98E*=vD`w;P)V z>`dRK2bgtyZrF7pc~-2${1sVU)y+rwtX1Pf%UlMU!YpR-Dj!t@(o-8-GY{C7zllga zoZEOW_yF%`M?{)o@GgTlvm6b;YZBIm-8Me2-Ruu6tmiDQcr$Ro|H+%38yc?#AJmGuQ4P-}Lne$qI-Yig?h~{Pk%1 zf+GnJuRZ9dM*=HNMa%tA59{Tpy^@c;3!biQIIkx=QS4kvbs@Z>uYn{;yhU#1J0>kf00R+K655Es9wtiaz7H~l%oABpihw;LF%p{HK%Id} zw0ya|j-~6=7q!48OgZ6#0z*zMa1724wyPA>^90Fyd%sHUx|BqBexDJR#YX|Ict>_S zAQ1IsOG96o4La5)H76uN@}y54S>2;%1Qx5ZKS4^s^h4bCf*)X zBD}D$b+Dl)#~x9XEvyy;+Xqh_*rpnnO*RdoA7vHdTBQL_=WZXg_OKmr1XvWhp~@5> z0J=Fb(1LL}J}mKSO`W>EfW~Wr=!U2YF5G97`AwQ@F@_@-(MS^IG0tOlg|B}Cb+LSa zMnP4C^2bTy>`eQk^D9EVC8Vt!Y1~fNDJM&Xl6hjTomkU}S zgB!iNKMa;SuzCs4LeYj7*w|1G9p~abzzBX)p--TbB3Q#FR4(36(+y+GUC;zW>}YEe zsY>Zg^&Nxh1OQjr1KC~hlW>LfPS_h_YNu8*y?G=7i$7=~Mx(PIo`O_;3i zE0i&m9008rzjEfBHbdB7cu$q5t3(52J)y{zlT2I|CeJsmpOQF@4>95f?D)?sZ(% zCBQPY2(yg}g96A!wl+h6->lqZS&qh0;RZIwxd4?S#D3U@>eLZYX}$0f|IVWFCt= zxSt-EUHf_zU#eQWNf_rXLVJnOAtH)-Hu`g#j%M80uVaI-hQeTDE0zBg zVsT{vHW!O=^(5t=-fH{^Z^Ldnl7>`qBzpk7f+PObjnuLb9eiD`#>EJr|6}Of9qxz4p&3hfce~%HWJo)qPm8PK}n&dZ_x}I zazeMXiNJQAz`D8+JY0Yb3AVaROppoupnU-33K@KT z!Ez59Q$vNXM<&)NP&G;sgQAS~IPqCB3b`l|wnqXe8el7{!pQ5YrO0eD%$6Vp3N)V! zC;Lgz6U1dVk_yZc39@y(q`VdFv39lD>^Zn#G+B1IIDc=Ykbv=s-i&Onwh4&taK@l% zn7y)^CNi>8u}?NpeZHZzmI)&P)TM=t6+ATGk1!}!`U%i%F35d@nCK~Lk{}CZiHJ;~ zgp3`jV|7{(n$pVd*P)CRm>LPjmF z9aUT@%8`f3CuC{sudD9R{F|DM`C^cJ4ueg}_F-3bTVgBO?1tf;Xaz>1fJb){wEl~> ziviMkTbE#w(~FoErIV*o-h7eMveg-z&P{QovMikm%lGZ90?#gefV(3xweq$m9#Y7}`rh2QiVMF=1&D897Ek#cp&}qhDS}OuqrYHklCTFajUoK@ zq6wq1Ek$Sq6X+ho2mn;dCbWROmC9{2=IzLg0-VVk_3VL=UxcC+lWvIAg&ysX&?7Hoq#z&;49 z^IL^2lc3Q+G|2@PlwejVFnkGn|DzU&ijHtXG%te>Z$`u9C|xN=NIqq>6V9MkKm<%B zE`dQ6H}T+6l}$c!bPgN6N($NXfJ`RFWswF=9)?CN2zV*9HLFt%yoB5*;YX&Obpi^1 z|BrO9xQLKy$nR{d>r}0ZvDQf#xKzhCYu;WEZ&P^KS$WS&lcR}ow$sre@5*$?68=2Z zjv>Q;V~EBwq_6;upN&clT8WO%%D?7( z;M#Xev_^0OBP@+Pc`eoY9GuyxnjhyCOpu*Ru=o+}7&WkDc3?(jfSes2@Qfg9C&ZKy z9huOn@9_w(^04f64g*poXfymKR~~Z5VJJ=3f5HrE=uvu_uV82c&?J~9E!eyc;{J)A zEWK#bf#NbT8InCz8q~d50Qg9djtcZcZ5C63UdSaF(;y!u)=9blQvr^m&z1(Ml86}! zbW&Mn`Y76wM|4x5jCT<*0P1S87A(Q=4Ll^13U|U_ z{+x-A0@aHrqR4R8LLb*0u1hDLjEl!)BDKa^-1tBaw+KxKT*%11rRd>fgl69n#w;dJ z3Ju#3N0j;~mD{TBx3v~xgc7u!bTpp|b7)B~4ez`z>qN!h`4vN`pFvH&xwCum24)Wc zMo3YCoVyEn_p7u=yN;uQ#n^z0gQGO-$-O7e?vS93705u|m5IZndD$=zKo^qX zk305pr5H>|HzFQgz(X?ubhZ@o0Wf(?ObpP|M7Wi$fQuI8m$_6FRrVwhfFcMO0a@8% z!`;bgd>+h~pj}51Q$|qD0n85a3!a39xQ9p~z!Ead;-kgxuuS zEGlJ#?55>q^lS<#6}N2A?Pd^JaONH1mT-Up;c_S0)YQG!-YI_I4c?@ z;ifPpOvwT9Xj{r0T+8dxj>b6t#~WQQVFB18Dhic`+CYOK8pN06e{(u^=3*U(3g14_ zCLzaC*>If%9Vq4JlE-ovd3Oza>ZbF1n#gcoCniRb&6J?>6qt9aMN((d#jM_h+O|w-zDgc6z~(8yK(_Mz zlVcNk$}c3!?k%J#^x0jfjysLM7utFx3bU7e zL4D>TGM>B69QgX|>$MQ1Z4^}GhF(g=2xM%i`0ivfTsXQXkhXin;!U)W-I&4Mv0**X ztISUs0#J?Ll6H&REeWnvxKz6L&{YhJaE&ebegp+EDv`Tkf?&n3VZn`R6WgfRIlAL@ zhPjww9MBa`E7H|(vSV4kY9$;FD1gchO^d5dESlu_n+RfyQE(c5yei2zk?hYPY;O^@ zYT|8a(=Y{r3IzAs;ovTBcBr(diL@{*B$rE#nv$Sn-qHs=SAK@6Sa0J!hOb57pvBJCNdN(s+9iRwpt^9<2*uvAcfTZ|p z7sT~)BbF3+6#?O@{3_T$^ok=@hYa_S%7-aPiduVMeAyQV%v}@5r5q*f0#&L(=bf0M zl`mzQdc=_w6n$viAG<^?ZJ^9ZKb?#BAp}^e(MLyvPPwlbZq;<6HhKdvp%_AwjCUm+ zL6r3;5FAd-HJ=Er?`o}iah=C;v30#*WsP`GfC@w16@d<!M4F&Y?*MSJ}LmYbTweoud-Yw>n{&3L&le@16{mZPKrRm6{m#WpqSbdIRJ?K&4{Pa`)R7Iim2Cj$VS0ZsvR^R#~th=~v%k+W#iTTPxsb1>IN0lt9NfiYi09r4Ru$&PoFG$Z62g(EOo<68h{=Ka!MOmZ|RP zPL`|DVn>O`4KQGwTFSC}*tPHc4!9o4jzI)>}CgZA2iIz+(8jwu=5V z$p_XiFx^2=QWB;Cp_2t35zF*4R}hN5;n`Kk_lNE{!3CXs64aeZav+y3FrK>$Co=Z^p=ldzQkn72SXIP6R=Zt*i|G=~F5`=Fcm<%BzOhMJ8F(+m zs~Gd>#%jc3dlH}`7((J<9?zY_g_ss1lVhlY06bztSA^(8$=oW387^L_S;HJ|ftYnz z72R+2-UgxOg6wCkq(nR zH!n3hTBegvYzfnIwEa^M>i>sH1CAl^fhUM~21 zM1vBaWU+f+f}3D>0lZ5K+WV}bwnZDEUPkmWpBXE3)8VNKl`|lv2f^ViQQ|4PM$V5O zGNy}&6`V*TeSf51FoKW^3>a(O^E}uG9CFzPnZir4fCixYZ9=$Y*%#OZR-lWdKKixn zqM#Yjxyu?dkh4*%{Slg#3-x+x@#0M7t#Aa-%Rr?MW=J>TUu7xBbRx_<8b9u=(wt|^ zFX|a4#8omy&AiCte(e6mN=TxcC8rh#*qqSxa%b940>KwU`*4AhX3h~Riu;kS&Uf}9 zZ|F93k_4_D+xKeyH)ShC>sJkAnbZzx`2PGAcqVl_7Ww`{QB(dyGq*Y9k~w*y;rM{> zhK6gZV)k{d-A9RANI@~aOh~VcNo5d7KEctqx=HOo47=25RLNt9TY8&%__YVFqiNk^ zxO~~Q&#=VFU136qgYo(bWWhGIpWAl8{c457Cq3HwFpXK>VYOT#0`9}uH5{9H6$5!X zTB^Eo>0wgdYG*~WT1rE|@rXhj;+WVJ$^?X&QQBTSLId+#1z*C}TK%vO$!q4XA;wlzy#E&8>lB^dXio46Ggfc`k| zRGSuA&519Le8#g9vrN?VGK*!`k3B2iP-(MbM*vRFdrcWU!&>;QLPCCh8l&;V3@Lu< z74O6AS9Lu&2OU%&m7CozL*o$(t!H&yqXL7zJ#3OYy1Ol;2J5~<1j)e z#np*UFI{qXgNMse8zw z4sO+-k>=nH=ytyj%ye!4^X5{9V-IQ+VJ+>$GkFek+yBiQN#j+ny8U-Y(^Dx=9e+n1 zdNb0rXUB=u%QlzCavxv*w?kI6em2oOZ{qfz?Cv8TicP6`PoMwG?zyr4)3zg*PTdWi zzxa0jXYNnI;KW?ZnVp8=dFsCbUmntEi2YQQ+G}7-{88<6W~G?v(k2Z^U%Ai z8b01@v*=S7{si>XS8bd}VoI+q4>w?yBlX;gn3!?!^Ulz}6gsq2&uKO3a#sTsI3o%I zy1iNQ(41bb_m#GV%WX;}$wF%=3wqNJJ*sgM39IIDt9}Y$6;qrV9#QzqAz&12?2>bT zFSqG&IeiWC_g7IfJ@*T#H2oiR?V&5)^|nd+IM?K%79#%`k2jh<-sAE%uE^=#nFE^6 zAPuP!+}6hK<|2DT0rH!yKG(z^iljsKl}`GCe1fDaSP)w7(PVvbF}&58n`dJo)3M{x z4E|j7--hiKh+QoD+B$@OXLN#P#Y7){g!GJGsl56t@Eb)aJ# zkdX;)S9g6dFrfGVeKzni8a;9zciphL=M%-Rka%#B9Fjh}}Nf%X;IdS)<`DXUn@!;!kWuMLONhM{iHk`I?SZr4IdBBvN{+8Hl# zj;Kq91*tT#WbkEqtP)MH(vnII(zmYzFM^#L zA^PhNw7p~i<~c10y@!YC`Oo_0Y^S$EBGUiPZKN9qy8AtU%|h9lU)$aG?qBn}_wH>2 z_dn0p{Hwhb_hAj7$^?y=2wNu7g9(K&|G2xBj$A@(_h5`hi9KN8GrlDUBuV)j@fsE! zzD^JSp$ni-y~Ro78H_SxE`bqN8i)mZ1%8!ogTP)cFdQG<{o4g>?!li8d(_9*)pmv? zd>x4;*f2Uha%op}-|+?e{`dx>2BKg_MC>>%(ggHL!skP)D06~Vi)sYW@KM@hDXp#| z)GT>^eqso5{n`1&zA%@<_$$)39-SVwd@W$tuozaoWExU(+@fTet%iwAs9Add(PY0t z9e>R}gjXWZ5~w@m6AG3-;5-Gl??)Ih1-aF?M3=g#Z9PtZEsTZ`0opw-wt;)+KwCop zg{KjFXCCy0t=Xsvn8*=)%ZFB%L0@s*%D*LsLkK_H>PA3B)pphk#BLtu(?^AVD=)3t z(aJP>w0kq(hWcnYIP&1s^%GA)Y6HT7hO}@7qC=ddu!92`p{)2wrQc91h=bYiS z2cR;P*1=zB-w|d(bCl=6{$hxp;~;ki%o>h)kc6$yy;8mLtncda<(ftf$e;R1$~b>} zBx3Ot0f2h=jxyn{M~HkrXyPIAo&(qZGBKJ1BXTJSDPQ?RpSh-w78WJs*YA#bvuTVJ zAG0^|LTb|u1B>MdY0O?*AipMxGV0-WcC5GsTrvmhjo&b?iRtSBDVd-?E2`95aHF}> zJ+?buKcar0D3qgTGsk~p5q)R^8ORn@jT+dH>-{?*#zMWlLg>EWVB!n~v+Dh*c>U43 zXIzorc#{rnn$fIYwNg}Q1}rB*ft+K3&LVpNIf$wzp@hXW7L=Bpe>5jcBoBmb)N`-Fx(aLxsa z*XC`*t@!d?XsFTV-68JdBfL9=lsUwb8FFrws7C@$>__Ocd89YZIkC?T*#LToe}UjV ze)(YO9A8O9)bFXR{O{r7O@twvhoe2BunfaKigcW9`S0S5l(R26I3){9+pVYG^h$@_ zQ|3?ncZs*G{_@K0mk~sOG=xyEf06dc6-`q8$pzD@f4e920@2Nyd94~#SplBhz~k%*lgJmQxK~E{@DjT zgSEd@jbt||zaHYZxrDj4q6n5gE@rBC?4#Aug&pn zhK}VlBFtp0|8ncojShae$u}GY=`4}`jN9rt-b*78>3}qoBfV))lj@=XeI&*v3S=Ur z!yug-u$MK(n6tOGh??FEE+R-sf3mJahlnPte{EayN)J$Ph%+RaIZHt5f{rdh$xcDi zNc^Rt6_#R%!2^fsj~Z+E(+)x*x&qqLS}9dg7UH)>&o;4d!jvIF?ZF!t#OAFm3kwUu3>^TSYg%5o{PPg~DXn3A z6$B`oBe}pP=c&6~L?1I=Q#*}nRwAbAU8opbCP z^TI;7$m#GKrE70IE&NO>2p}8!`N>W|%M&&Fy!9as(+6V+3LtW45&aqv8oORG>B4cl z1jLb#w%_74VlqiUzn*UkPR?KPRbaC0)5EY4{fIDK&I2yG&J~Net!PB0WR4?^b$iq( zmm~Bq=w`a{%>NaWwkQ10<#f4h(}`9-g9mM_R`DdOSZhG~qytSXOEVgfdvEKa4M4V* zmX)MbX`L1zS!#Qs3dBnr8WZwp5HxW=&b<>+EJP=rA1^=XuUl84nN%~JT0jr}3_X52 zPr3lG3Q}BcaUWBze0maXtLU-WhcK}~l-5OB^zdys{MUy4bUF0CR60QBT4d47VB z*rSEoG9!hUsHn$ZeRzciAzE#iCOjP)a-~P4=3^DDtH8zZpv9Z}1cuK;*@f%#e^G6u z9x4}&saMob4S}}QR&eG~5-!yS(11zpMZ6pf=9Oag2Sjqf(kna8$i^&p|GMpbpfELj z$wGO_--weALO%i0L5%F>{jis$yUht7v7p1(1a-7+07Y>AmDq&`1vo>_IjRAYk$_hI zrGBIXhhN@O=14=j=a}wo5Il1hc>tvA<7xq4kaVuyl9=sA&cM7dP$qS0Jx{ zJ0JKq^4&N;XiyM(IHcL>$BFCwQ&Dd}COKP-gX9K;4b?CE41zoaY9_uvxbl^F`LBVi z4{eyK*_U2;o%yx-bQCDAxV4k2dX;q~t*3)ryJ6$jMP`9;>xDc$l!!bNXVk3W#}OTx zTw(@`?45C)B)(;iIa%2fb~xG23736AUe;T8=}fo-0BwJrX5AoMP>z*4V0{7NpqI#< zfLNtKlH+&MC6FIYIOp8{7uGrXu9gD8>dab#|LYDlL8!`(;LK(wpla>niY0TACEC^z z`G(Ay)0%t=Q-wDGqDp;Du&c8Zm{3r?O+*%94u0LHZMM|Mm>_ALyk-y~BFMzVTh}X^ z?1D{(x(2U?<{QkA?H6ixAW_#kH1@St9d2gc7Umb`j<9X#b}rJ&)B2L4B*K& zy33E*C+D|){`A*v$dV7e>3i)bF+e}rYSD*Cev_fM1BgLyUG)6-=f97d<5kx~jkool zH_G$LQ9M=6eh7~3QFUD)i-_4j_ldM)b4&{LHuabk8RagRh)qhIDaB^{awsL1zW&W6 zwikQWfx2a6{q0R8-b4(7DyNkzP2cno6pc2%brnTU3G%(NPz46sT2fT~V!{Ss&M$UB z;u39O(Eni7VPm{Qe9@v%8`)wuP`hS+(5z}xHtekv1)mmb#L~dN7%xtIWOaIO8tNdG zeHxkV!l^>3RfbF!Xl%-GVeHSy7&kry5he$HY_L_RL&5*0MH8cVE}Z6A&ZJh5OHSso zeF(?K7O-fSWE{+*`;tK29;#sTiH}3!C*z268qStXm2W-_{vsmqYfVM!>N~Q8I`paG zckA|C@#SGI>Btj0_|?Y&y#aargzHr$GAxd}%+KhiOikJcyrI(V-WExtKIK z7?FN>Q9_7_dt0S}SCPL0ymG2K+Vl1WkHdj1`xRPEg|1f33G0(GLnAwrJJg~GebqSD z!sx?p4KS5KKy06Wq~;b(@5X8Akez}fI@?4VUhCr^D9kvv+Yc3I^gzokseOXB^7|^3 z68>FoqWb9_*PEC<#t`X5Xq^^f-~*GzDIPYeB|{8G1gNYp+zmwgyRu8vn0sc7>|SjsRoTjAekcHr=GpV1v1uv>zGpM&KoBXwIMsZIU)wR;&wdh2}Jl z5ET*YtFW4yZzU27KDF{atkuO8J{DzQ_9VvPmvo2_+-cw>&cuIy74L^)*f+4lq=@$R zY|8>UugQNsF2ffv3#L0VCYDM)Yp%cDA*g&$+=pRp7upb1M2GYy{jwD(3SKQGJHcSt z#I!Mzq_9RWBx;%*_6fsQu2q(y1X$kK*L`Qh%zXj=T#|!PoZ4csic9i+Uj>SMpdfE$ zZ7AVX%gxm)As#0q0`I8J5WUSdVGbaYsY-S7^*8bjNDGx}swsVmR&z>(Eb;w;v0)qT z(F2LMs{OX}Y+--Z-JQSvaWd*kzg(#L#D~^cr%1Pg?e`1tv;db2wM6&llC{VB!Bocw z#*TYT8{T&ZYWgJvUCBSZ%yUTa!1EQjYtNJ*1;pY-a;Gv$*N!+MZGP;qL}+y{6kPxC z?*dv!sZLWVw??ShaUIkK?aUcuLC9I%&^@8_O+L<6;STSsaNKW#{P1Qga|cAl{RopE z5ca7R)*;<&xQ0Me39mX<9jaB!9QiY19Qk-@WFrcUSft1a<}2=nvQMG9+@sB%Tvt}+ z)Gq(L`R1C^O(T|Evqr0rP|(Kt2Qm$M-u)BB6|rkM>Sx~ zAgMe`S+QBg+;V&s9^HHG!OsU3N=yqD><6QsXy$sCxQixFGSgm+`Lv z(Sxc-=iB;EG@@zVN44JA+&dBbO(fNO(@s>0PWZg2`SX}$x}hL8uUQ-&cx-(Mcjw_3 z+FE-qM|mEu@3CNzRc0I zW_TrWZHzCss^m$Xc=v(;~cFP+W)+NjC4BIUrSmnIc3!IcErOtNGD$bs>6qa3{)N* zlQBhxrn>-dv&SL9Z%{{f{paB7Hj#Cv1GT&(*9@8ib$Y3rT_^PsjHZdkp7)lzC5IEc z#@4fL9&5C2ycz1)flWEQ*XmX0!rJ)8%^PACqq@BPb`F*nHfi?~ju=6A+xA6F2QEKJ zfR*T}Dd=P3I>g(miFY!64FF2w3U%-}H1-RCe==o7dZs{c*`?o}5CP~uqoLK8mo?aW z@~&<71qQ20{hLF02?Y$LVyXx4l|!l~lkJQ_J=F{ecRr&Rb&=2O+wxZ6Q@uI*{(IA` z2jKw8W-EZ_@vqH=fAKMHUuOC_u-`-Qbt1A2rMi>8#f%g$VXdEzy-WZOf6o$CtvAW9 zpU7(WB6j_bNxCtT=X%m5cXxhJh?u$f&7AaFRPn;pnE8M+v=20pFmfxFe1Bp0;FFk_S}Jq{mqUmbK13{Z z*OC3JgGqG04sdI}u6zLlkrB^mDxpOhNe4AOF&p4Mt@l9u6hloW#yvENC2aW6tqM$Q z8*XB-NWA$lXq7!+Xholp+yYJGjm`mDqcpi}Z}UKHF;zM>QmNcYn-xojy6@bWYt*HI zM0ulzbUbFQ)KBvAuVtTA1x3qH{MNC=EAHknDG+ah=etyl0w{83u|^KCHW9HIdQYm(?zk2_X(E4ov7y@nuLx*Dc%l^|Q1w*b@K22-yt<<~u5YIcNMPE-X8ce!*ZhM(FS?h0A zW2a=`_SDpd{Ef8vVeOMmHoMoGq|tp(yor5MT;4&bYOBi?uc-@;R7*AvL!Il8q=Fe4HeEpap?SKu?D8Q|*%J4%88h zW9+YA8bPx>qqBhOi#I45=>H_SLLR3!OZ!Ia+lX#jb3kS5`|004udfCduN7BD&P|mL zGwtciN)KoxtH`61e76O^jM@OnlD;b@o zQ&qtkEA}ZyY~Y=Lw6V)SoUd~9cRV(#d3~4TfxjP@l#V7?oUPjDQQ4w>_u}?ypm=<3 zZ{>sZ19?M7y=Y58<`8O1?y=|Hu|5NYBqG7WLODTM072C*Xm5Ol*SnW zmf502q+}Isze!so`ljU_S-g7eJgXFz0tsta)f|o$I3TW4+ZFwP?P3?Fv8q0URJU-xb zCrtivBxSScM+n2YCb$-RT)XNmJYn8-gEmW>P~+iXI&ndc%2}il+Fq$~k^>-m9YH?* z@1ZIo!0gFnN#tbAf!jXmLH>WWRA*J^4Rw$J|3@oo8+l?4}%JTNLVs< zbg&+}ze^iXwK+wAKc{vBZ&m0x8jY}E?r*!CP{zXm^m-ES_a9PUW0U$EUTr4iIbrLm zsOM_1(oaF#HdjXXYTK_Ui%p7{b{_~0QG%+iuElNA|Frg2?&sIPmdp}yhdYXK98p#L z=RbdJX3rixh!6bND8?UvbfhAUnTM##xd{9-^nUShAE3s1hOLp|uENNxGW=bt>SLMe zOKw4fj@s)RD=(}sszPI;eDKGmGw%-W$A6jqxVS+jrRZ_bdWPsjL2c#Fb1kT#G5!=o z@4rGEUKDlOZ|<-0su)=;SMyC1O4Wic`{z3<$?$7FUAzF0prt|G)v4UQS@+*>9pcF@ z*uCWOk6sp^r|jQqSd;3$cBh4TffdC6VlVcuHvAuC^t8Z8-6VU;v{ms4I!Q_DM~h>Dg8WfxgV)RH#Mek;w)kjZjv_a-ZsrrS5%(o5g4^vV6e&Y%rmGqL zZA# z)62w!Xs5u(gQ$=VVH{?dd~roAAEwNvZi#yXl-vHE-7V?zs9<*!@*PNh*Mb^IX~7CC%X$# z9G?}jyu*4;Wztcff3yM57aK!o&|Mc%EowBdn>OKjEYME<$JBA-HKM9)^g3J2lFJg^ zq!-*3JJ0E(y9&bBxN9B3g3%r3rE{jqS*rqjf4+Ef&+(uxo&ypS#(qh(t?y{9SB`EB z0dw#I2>moq6J;P4!RkW;{P9&DubZ~{H?n$T|99rR>&eo_613;rQC>tTB75*HH}OCw zsVyXRhpVL|a8)#xoQI)5rnqd~cig;l2Rj7{RmG?C=7F~B+pm}Th7bN(EAwNoA@*Dd zS=#Z^^2pJ|gw2dC0zA7zO)Pwk_cZ~W$@pdXgrE7}OaJu1iUN|x)(pWQiqrByiN zqbSyuAHOUth^#4p4!*2#pmDk0f-3RWGWEWL5bMOf#>(!R&nRVOL#w-(d8zP*-ibU5 zS^Lg`IXljO+o94uGh^?HWqWJP!$Wqg8}2*jm-#c|R0ijfoCeM&g3fnVeMM)@+gGd6 z7^<2U{QoYB7jw(~f>KGk$Ik35_-mK=bG&m!^=ZcRt&qxfdo-5Zl}tRx+dW_x zIJj2tly4_dU$=3O$l>3&3z#noRq+pN>za<#qe4q4LiW|{ zDPNTMmC4n(7VeQhZ$1s%Mm9~IOA2W@?th?iO%Glt{pU`({n7{a2TYgFkj~v!tFzGr zesn-1xi$!Bk-j%**_Bmc7n4;UtcGk|s(khKc|FXzwm)k8JMqZg?9ZX6V(GnND`q<=Y23$_cX%5i0eT5nZec zCJHHlV78NHroccQ3+>ZW&f{r-nspQ>m;b6+3YuxAX-wp>}x% zcQ0U3E-psxIVbmmrq?Wqjz8D1BC{m$1X9ImAgh@Tp7fmg_wBdt+(wd@?}Cve5fjsB zo3CXec+I2h(Mj6{0JM37zE26vr%M&!l1BB4xpV?uV!-CV9|A8*d9?+L#^a}-NHKS} zFQ4W9*;S;5Kx8e*Vxr-2X}fnyl0lDJ#l_y=7lp@PE5@0MmrfHI0(%@TKM(~v5{ zS!1VwDEe`mdpDHBtI>kor3!eaT_KXsO%j1~HRBU?* z*YGQ!%`JydFcD5_48y7c4b{L~16q-BRf9Sq?ragIn|ymv_3v?_MOD_wmqp((?jNEu zpYH@0vs?yH22ASWs)v^~<#%-(^a`tQTAl3G2)fFgwZCO|`H#j3b?P6&o_f193c z!%hKWgHe~n-jt-2d=I{0)53T-oA2^fC%sMOsFxxM{qFZ9t|Pq-FQ=#Ldz~=2_ziwE zYE`oKdjLQ=L2fbA7bwNNC1akQ%b90${)^v7AQd03V;wGsZECGKM_8Fpj!{fFU1?u! zqV5M=IBSvaFRo*~w+wxCV$>+sRz#A04ImC7d|Xh9sKugQ5y#TZI{$s!=JIxVXyN(n zBFdGkBX{Dj3%>~vyHBL-9wHm{E?C)u{`2%4TbWp|!^ruRKaN2T0^6mca`7n?GO^fu zCSi5`D3oh`O|q891CTv$HZ)0fwAefzqM;xk#g>0p+*rOm{j+Hd%<^gnkjMi^wk4%K z|KLizbV#fQ5U&i|hBQE9U3l*YpaUf&Ms*8eoyvVc6L4 z$=vj`K&@e1dw&m*YiuD$)pzM5Izx(_7n#Fh+2h&}EJs>nTecva4e`S?3<~F^!=?fw zSlFUrGd&hh)0_GXTY0WR^uc+ouz-QZ(xtr=>SriL?=S+*3M6S<8-UH zC|x`a{#vRECaq=c#K!N)ycng^EkSMFQ?viMm62AQ6QTgmE`Rwzs>>0%9BbHW-lP8Vxu=Ie-o%yjw%r*Vjnn+wdIWImezg!f*G<&%ENz4@PQMSKC%X z0V&=BF5#DDC#CJfz@lo$Oi&hLdRJU6sM--2nI3f)IkCbrFbsu4w4DJQj$%vLQdDl; z7IOskR?KoM-`TVhOXDgiUzF+2ihKIUp?1N^{1>g+BC3SxWqHq&$LU4I4lxB@hEA0Ws z6$iv}X>v!5^JaRMJoNN3Z1dQxkMYjqgUSDra$lwMbuJbcYz>_{etv!f2!9wBh|SoX z0zoj>33yp$@30(oJ5(h*nmV%M)Ez?r%wkr(PV2)72gUJ6cq~=Q>FALipH`g)o~ndJ z6gYDFV8J4HqE3x{E`ro2&^zI8{N^wZ0tDJB4~|=!4%>j%2#=WkrmrM{yU!p0M2}Mx zD7~S!G3ox9d8KQYv=^XbEsoKBWx~7qJ2#$gHvTzwU4d?mxR|cB8SJ(MM0ZFBpK9~) zT|i^gC~Egw`f2|yZ%&O%l8!L4K)pJi5ZoJi<)labZtcyJ;O`u0+n*q>AB>Zd`{=cL zj-zmrj%XX5j&-?j-YRc7cM9%1^L^iP?^i)a+r~`q>@6u@%jLbY>0q75Oe}rM6or^3 z&g^or8>%1@;^%UJZlR5Ua5uVw3OX`-7Qaga?z#Ey7Sxc>fP(& zcd{%UdNW6@?;!3a1*LZ&U zz+?(;Cz1cNpd(GoL@Qo}d@?OxMW$uJO;L#&H9YIruKWB#8Ne&-&lbFx*)Q|KGREPM z1ctELf)A%AazN9I-&7M#94vKc-ha;eAw;Kn27T?^odBA`#VYpfTH=)@*ijd{2wG0# z$G2ym?fb}8TeXB}H*2-0Ci0E8jlS-}*}PgmBk9c8aFbws&!?!mYtt+|(4LUF^5&t} ze?;vUqvlj#p#fK>>Bfn?u!B}}|HIJo%YW-CRd~Xq*3Gr20~CR^qX-j@YpCUVEYY0+ zxlA9v+`Bg`Z+U@6y70}nJt(~!b!6my1X#)B=5E3~Y0M-{I7RCG+0dUSQ=ZZZuEx4hMPwXc>YSgU@A%lk_h z^i`1qw8YCk{lDb`08c)FK(aOscLv%bSB?5{8Ayi~1h^Dh{R!&xMlN1WysU5W^4QX) z8D402@=MRo^P|U`gBi<`e3!*B5yk0A354k04vnlKqdvNoeG|mgO5H zSaZU%19RxUrHc8G&=p+7nIwCQmfmquw^WLn-!k#3KmOk>!1O)xDc6URV8Iy($)ZJa z5V$x&#FQ+nU2(3BskRaVOUQ7>YzSJx(eSy44hzagV;+tWwE7m3nE~uC%jeS$*S;}M^rkROmuwX;X@>N#LS$9%ASy%4MdR#RSd(ss+Y{#Rn}DfC zA@YUr{FJ8A3eP)EF}A`RWGT*mCZR z`vEW4gQFOC1-gXnK$hBC3w{k?6y|5M}Z*2N9AK9{2LD+3~sl7;!at&EmhKKvZ zGq-=XRApm0Y_Xv?x*D=A1>kebFwhjR$ObHV0f&Rb{l67UUnQ7tPc^8**`Er^FHE)j zRCRgmdLo%-ZP8+uB-Q3;y>`ZzH+V%J2;I{m0NMe(?BjZ-^@>$B=QB2|@9uxkdSfdk zx8{`Kw+T1FG%Gu`+=J6gJ#Xp1DKn`kvbO7wg~@PPn}rtlK{tD)9nHjwVpofCtRJ}$ zg}`kmzwI7CiM>pOK8JdhJPym34V&h=MaG%Q?1!%v?I8veK_+6nd|m{+_~`UM#P0jA zOWExdN&n1j&0=AB_nl1_Y}8kIbh>^U_Bd%bkpdb;Kqa+~Mz^H;pk z#|rKcs^JxaQ|D})(7t-Rtjrb@rbDG_jy())`Wzqv@bZu-+KH2EXMii4oDPQ0%&THv zv>5iiMqg}c*q3z8PTtQgy6k+$i>(pOrFr0SxX>F1J3F4&zdqoT5Y=nHY&7+Z^DW#A zwz);35-*=0d;3}}hVwZWdNVy1iY~?dpP}=PO6qUm@MS6D0>!-m_Xah$28ervX|Bw0 zW@V+OX12hQxN&bo9I2V%$kePCSGg)@Y3qlhQnRwMvgzx8{CN)t?m72+KhN{L8grW7+15o=bocPPrRm89(g{!JB1s$cpT^&44)7FcVAxC9_Wrt#K<56I+}dXKI52AfR5Iv-(#TH0s4`Q6P{1h^9VCmf zzH(4FmQ7ub^71HlaB9KG0>T@yoF<`Djr^Hivg%sYBt(oaZ=73wdL!_u=_xt3$Qkfb zZa6)2O|wDU+z{b;h0GaGe{}zZtLC|Vncw%XersF1Em>dkKFZig2#4x!nxWYIBIYw}lr@D!t$n;uSE4s1ed~Sl1N* z8bUsuS}9S1BqIJ~M*r3|-8%LKxoo%F$}hMK7Fa%ME@pR#TWT^jXR({2(H)cI9l3zu zIHdws^>?@hrhn(8&2R#J=xSQU~G7(BLF6dL~zAt9UQmvShN1hWO1P1e+ zT@C>cMBz21iI>TO8q1&BL^US$^_ug}Jxse83|PNf*?3SFfP*Bow_ozlR8d;9i#Dh& zYi^x!ymXJ|B+e;rdGli{``9`{#7T8ME`5Ei*lRmJ%g#Vx5iL*(~{9j(&$jwfXwOGmzi~aM?2}B_syZ_9^o@`DXd45u>E6>sz z^Zv5OvC)i-lZi;545eeT%Gy_}9YuHhOC?>{-HAD=dzEb$^;bqk(#Fo#9%61{(tpN>3**pWJWi zxN|Gux7?F3Ua>e>J#NaG#p4K$-H3lyA6@`WzUcFgUH;Tb9g^Zgg$c^9@+v`ub~O)lWibw}$sOeO^~H z`Hvb>GQ6sKZO-{>K;uN%V%)sr*6#1;w|-WD0*J{D5d$T^zyUOR0A~NbRX||Vfer&HR(cp< z+Ris{V+h>y`E~}8?N~PK`FGba(Xx}wTD`Cu#8B10lA{f!=8h47^_k|e)OvYiJMsv5 zBh!8oXS0%1^thh$N4J~!0^{*JJ;BPZz^5t@h7?|K-|^|uPt~Zg`X;YokJP?9m&}@= z=(P3X&|j>e^q%;;6qW+QM^<)#r)AxUZGY~fJWt{LCI@v}zCCQ`^!~%2aKM3c{_;wlqM{a*doSYkA~wD?Svnt+1lu#=DgI zxdmkMV{10gR{*>l$<8WVy+P9uRhWSUDEiia5g;q}!7h&6Yo+OIRO1d?XrUt_t?{kD z9lpFc;=s;ARz4srR%^Z0peZ_LhIK7j>3*lGug2AAQTid#iI&F zSm5~zxceT5ct1Uw!J6bAowpfh(aGJSgMGWnqr&YSwoq-{#xw72>`C}3dc70p4aVy) zz`qdOh!J!k24o5K3z-a_rrG%uH;m&rlzrp0`Pua>ET*w z^@df1->$dIZ^XuO9G#cahA%Lvbwf;Xrf`>>xDLc)5*v~Yft)S}>!Ngn=-*%+^4YFx zcp&e`OPB4|-+&14-`BrPE;)|LZ@-8+tav&vTSRzROZ(TlV`hD)ciPfT8V(1U%H0mb ziS*wDCL{xa0HII#zjg`LLB_3#+xrk>u7HkCf8e_2){}UvTAoW9sEBAz3pwlfK^=ZN z=+|=gr9q#rw2Fkpl?US&U!@t3UwUu3Vrg}AZeBnD@3q#Zdl>9^?PdOOzt9xuRqu@8 zB>ofgeh4(qHP8V3#5PwgpxYr6`hHm8!waI+2-Eq8Sg znB<0V2YLV4I3r{}kX-{7py=UIj?(h5+@NiI!zhqxeKLujY(JJ;-~&fcjnY8&Dq_ti znw`~zS%vDsermu|-Y^)SOncN}M|8+k5s$N^AIiv6G;gGDb`1%m)sJ0onKLT8`Qn~I z2eP*59Bwh!g-dKoq|rz<_|txJB$^9Sz=ythN%ZdWgIt;4VzOFyX1!na`F3BF`x>4< zDOu}B;+7s01A!B0F`i-L$Lgr8gb?KIv|=UMT4`y3P9N`O1^e)CYfn3v#|y&chy<97 zD}pF~S{wHWuT&F#HA52FQNR|JQDf@%-JN+VgwHELeAT} zc~DZNeYO^bC_013(j@r)tFOA2d*LIZ_?<1!^5FQU#@#GS_}(v~@--3CMqMpA8VFwF zr%B*@*mk}y2?yyCL1+?0j2gK#o9D#`?5eIRO$GWxcyt(A@M5BYC@C%t?>!SxAKV~) zs_|W~5xTv(z#{CkGT1Au7J9j5un8jFHj7YL9w@PQc9>cuK5Y%1;@4GLOHtWm=|&Y zh*90Hcv0;q(ihtzn9VbYkV)L?68wteR_+2K*>rD^GRAT3fPN2PWe=IWoDWhSeYv?= zF2o;z4a=ob9iWL@a`v!ndZ4FF&lDLne1R z5a1F0ewPu5_l{ndNhN-a%bha)`{uyiyT%scN$)1E6+i*TizCGFfgxr4Am6^JU?xMN zl?_c7gc!mZ$U(Q=8&^ls%y-C|O}Nd86H@)AKI8XcU? z3JjX7mL|kKh$!|v6i{?GJ6eIMH7YMu$`kH@cU4K0$wM z&PSy&w^qAT^p1L&E^$ za++4*CiGmiizb%11m5Wjy%hje<{T`>#RY~o+e%0Q-(~>Hhhm9HR=JgY(@Asg#+%1+ z^pv=8K^c1T0fd=8L+uQ4tjkINBu7DQ)2TK!bsgO!8ZcG*lWCQ6pbrAL24W|f$d_W zb%S*0Wzc&L)uu=+{GRL672I>*Ot7?$A0}B$wvID&zr)|2nk}AKlID3|19u23b}|#+ zcr}6=syjZ&8Vr58m0RjcCoPKIN9$dbdmH;Q+0EPXBH=*zId#cp_b8u1&Di)x^)a2~ z!0&4?4E^B=N23@`liLFslf#I z;y^Mt8n}vE)AJ0X-OBjn1Lenv3m?f~maB4L9}~yJ`xi%hwzG-?PMp{ANL~xd1;64l zvFuij zF^furE{=rCf%d47JkGjr4ZcVTRXSTfs5U$J?5 zN5J*#X-=B7?1+Ak2{@eW-K;;44!O|_ly-(v9VuBcp}nh~Dx><%WrcD*->dDQ=yJl1QM|Fiz3 zIqQhV^d|`A$$=sq=YMy-%R*I}jwyJ1>OuryNz)p(T#HidF3?r#V#3E=I*01JqZ7mj zpWE{sryUPCcSh{c#nizwM^VxbB~?#>qoP5V;tQSjOy(9`h;&YDP*C~TXSv_f36Jt& zxlofm#^}YJ<5y!CBGE|g1#?jd^A)p(B?Gri}LbS8Ew%q$u}ZZWf=*m`PE4m3V1kF8pT!%oU_S(*zd7y_w;h_HelR4|4=>?EJqnBQ z^I<8-kL5oo-edR#M0^zI;AfB5rL34neQ+=;m^*#&;ROQ_>t2Y?+p=_(9$XOrh*^S> zL-)RTK?TSZ*owb*44z?$!NBa8_|jd7EgvEdqBKLVluhT7_z<@xQ;E(J(omF)Ctw6X zZKD}~jr8Fx$cdjwmpVwu63AkU=BtefpkOSynRdbXk|0oEugpz#$Vn5hC0=c}9kN$P z{9p_SvLL5Dbu5qvlKe~Eg3pWa%cNr#$r^RZAm z`-l7hMTSU|Gt{I!4@2DoOCp()S=;328FuZ&YO zURCG>N4tfiJI--W<>({o2uU1WOO6J@vDOO+K`Ur}vrkAyS^VM{wYXVWl3HXOAv}HM zt-~!H#$Y*xgaJ#Hu*~#Q6}?zCS;x|aUT6ieck*RNaduB=dgv#6rk?kr!9nze% zf*lNBv~B4+UZ3Jy9KJ!iDPVEDrBQdN@z7$%sI&e4RjMG(qaE9LepN+q&(zp*N`z0< zexp(fZ_@nHc&1%hjG~!2-UQeyo+?t&{w7_4s7f4he~JZQ#3qMd&E90q%7DGUN!{%f z-|D{IX5LPV)AaSnkQ)%2fxWdsdixkF#EEqxaWGEgDg#) zOxv8Y2u6n{pX0|0v&A>B2;k2rHOJv@sxCn;Pb*Tl;-_mRoi^%EuNcm5K<1Mm6(h2? z!RFT&@Y#n@8G|&Bc`XP4T5r(%(xG5EWn4CZ1DnYB7l!qIQ^UoA*7qH8;e(Srbt>YO z-tE(s($$OhwJ7!SJ^b8G*38I-DiQH4OD3kN_RhYLpN0xyH^I@M_ap^181%=R*jtS2 zB`s$NvO!&GMo&92?6ZIm^8Djv`-!-qmD?oICRr*GDxR+wz%cdUc02&F4cJ+btzDf; z&JiEFH!YG;HZ3QJAW#7)C0wz`F)P9sYQ+ljif*r_y5`m+RS1Vi1k7rdkG||sKJ7|* zbenjIl%aM~k{yOQ(@DgqW1eQ-^x3K;b&w}XESix+=ohuKcVuf*Z5Gw*SpK`dV=r-(}8OZ%Kkh{ z+7jqJqgrI;(p>_&tzAaS2stHvaG~?u`QUTYWP_=Aqy-J;tq$od+hwIjcC4cOeFW%c z`MOdA@~2;*=kw*$vA91D-NP5U<(FXQY?(B0#!!RWp#ohO#vN^!9tb@+_8F-q~+ z9nU)CO-;$iF9yn&K#pFNB`ZC>!JGEs_p|mwS+%ORt;#8lS_;=?$M2NMPIsO*ZhLE_ zDSq3&;vmL{U3^d&MR=)ai9f%)L-*{G8oMBdt~K-7}GHp0GDDF zwVMg3CqE5B%u#CusT>XH$4F~y$ikoE@XB5VNP=vHjxNl*RKcf?vX z>1pA>9@Llb7_cP3{%?0PLmd#J7Z9AeedKPKhHCt2EH`5ali;{o>~dcLwaCfBdylm54i#OnE3Rnjp$>eA1~LFm->EWbpTP;AK~+3>9#YirEt% zcb`Q0yfQ$hG060VZxz9*Mn-Y&<$0#p?bMo&C6s+u?g84Ht` zE01?qOL2@_hWO!+bbQnPQ8bhExo_pY_w$>tI3`l9ghmD5=CY_`3oftCQhX#P3O zGB0S)!X(e!8BY!I?hboJ%1O+5=X<=)^jq;R=JhB`=cjY`DYM-UQx3YF{3iE&9qd~g zI?QHL;>hK5p~kTiQlunuL%xFGUOtx#F+-9@FCMkuDm}ddwqiqM*1+Z%2R!XL_96&s zr@3t;`y0*>F#(GV%>5lSfD#Qgl^cFyApwi#<^cZ@*^qF0)trteU1qDDUas$zIUZvN zK3tV7^vJU&)_}Kq&udu(Wdc_!OPL05mq5S=0n6M;5l=L&%imV$pUjh9={jpm@@!qv5wj~lndcHg%fyzlgQU;KK55c~d}|N9^LI+$EL^XLVoI8avu zl)M%F+6k(Ce>%O*+IDLX**Ywh3b6-H=RGovi35n-4gwkQ)r>e27ysXLQU`>C2M-(& zQ#}_dJ8R!?(euQdMB=325rxm=gqfok!cmANSaG2J&3mIz$XCx#c(BpHo`(@EXJ7PwM`Gps(%MgrttGcD%_2hY{WJXQ zZn7Z+seI{$^^d4eSK2|c62ap#d0333emL|Q3pJ=-bPacasylSUbl_c5q@>2~^yM1J z+e1)#WCdr?Feo)6s~sXj+w;E>A=(@$k)?iQmnK7}J?TuBWZb*``J{nR+*Dh%&hYaPz6&`&%u9Fh#$A^SSY~qr$?!lGxx4zdD#s17Eqd-s zz}EJ=dF%cpkS#`s4D1!gq3lk4(0|L^+U=d0Z+?c-pqH*!?VtiifB_Gub@<~U?L#N; zDEhYVqcLD4K-o7qkhbI;`iprWBthtg?bp_E+};$HlA8WP-SEyEm6pV@JtD<0E5e^Mk7Q`2&H2a zgU6&7=TilOrpmlD8PfhtB+#PfDh7dKw5DTMR8fYcOq^E1$T%D%QX7b&($ZzHV3GR3 zj2V}ORA}^6Di))O*0W9MPZO>WCd&pC5l8z}v+_VkYmsa9s>0bGaJC=Alg?uf>Bs5l zCBGq)pb$&ZEn34Uv%A_L+xvG6q6jR1CbFGuM-a&pL?~>tq|%?{+4pM&6L~2_t#&z# z{f-o2N?PE^v2FYvuki)wY5HRD&(9BYs{>v-5ZMi-idu)HF&ukA+Fme|ug|iZc(S`b z>$ro)&{B_pYmM#Xc)XCkphTmazKEds^4Cd(x&V?VXc(BdW|NLC7!%e~oE1VCc1!Swj;HFf8sR`KQi%5W?FH6l5_#A7Q z!<5zi!Y+h_MZ4VuJKWIEk|qlarB8XhAkq6c7a{?rejcfAH4{k(Qg&2{0x|-u)7Li) zdIlit0lYyh_#hc$T_zEm(UNZ70j~QVw=p*A5z9YaHv~`gWSUxeg0oN(5g$s-AV zo#S{hQ{$$R@o8ds`1OYi$pbK%0O~wUeq-A}P%(NGo9<*|PT=kiK-Y{_4}PvZ?I5vx zuz)>c`M%yk$v}8|F8lQ8pi3H)<&AYE%nxv$w-PZX_afmr0)`zg$J6~!9zw!%4Csdd z?B;lIDmbJnw@%x-FPkBapPzVKgiF!B4-&g^n^r5UXmXuA58gMK&C($Tsu(Rx5y5dg zd5zj`2j6?lnB(;8ghlFo>xPh{V-UY<=rK&`^+@b9G)#+}r6!dWo9Vc(!jrDZrS35> z3EuB@)w>jqzw_`oTlVC{qwxhcqx*v?Rz<+eX;jJbyzfe>jg*{E$;C3T@YguZd%^`9 z9ePxT$d}|vtJ^$I?aVlOExgp;-Mmiv=>_=oM?D3Ay6bRz%>@h6sXzy))3yM{@A?U> zuu}ViWgVDSw%yL?b?@~6)%3$bvj?7uAws-1)%vBM|2cGgRP8-9IRjHYxR0YEF^G`A z9rTZ^wcU@xPGMh-|a3N$=@B2z^Q@3j2CymzUFNv zFM*+JMV0z;fitx$MVQgoalBvOFWlRCYZXvChF!?J{m-s=G|mHvOD0F!s-`M=3tvH4 z_^)jao3F!phnf#7J&k6k>1o#+TuqwHRwk#WU`{$B$`6P+B`VGWhN^QFb9ZM7Sih>@d6)8J(gb%J==@Itnk^>px7foYYxgss$AeYH$^KWcWG-gzdUD zGkV%m$BPU?$Y?7v^Ff|MlAsga5QqroKwPDFVl+@TR#u;EFYCT*k`9q6(o;pD3m3xn z#-T+*0(I+I8&q_Q2Ogu22Qmd02HD`P_As0^#+?#JhW~R-m?|g%wiU!++MGn{-~SXu`R~lFONt>$i1mg`07@G zCdG>jp@eTeFw1bi6$R5*C}qtW3=~P`HEWlPfA^6;Hm6B9NJj(*gX+YB;FV2rmb1* zHb!~sT{14Q3%y{!CI;Foteg-71YL;CC~ZzECN5WIs0n@6$y?Lqq*pv>R$~I=dGCX^ms^ij zlX9qV)VgH!TEWS!w;RC1042fLQ}#fftq~#LLI5 z9_)SOAJz0-9QSWh2%RMx~3B#~jX9%=br_-dTL)WBR4?;mcLQ ziN(jeDvhL5(>GEuZr>gUK9-ae+)K}7KOBpBJPOaGdWxIH3&NzpX>m?rp-n+DOan3(Tl(t3Pg&R;CDVRwh(c5>17Nxkuv*6Kfdx>@z4?n0!-9@!HDUq8 z{9^BpIx}{xCi*7YMGAo10@*`C1t}|Crw*JLE9??zm6lEHy8WUg_d%(=v|@0-87B8$lsAv;SH?>cj z4sYDlmJ?efoI}K+q~H z7GRp8_wJ7K-L=c7+~a-Ll*$n)Mfq+ZtJk3Hz~{~ddNfi}ZqWsovV+qg?|I?6^<2=7 zheTnS4*$fR@K6JiePPMP|(7~68P>|@%X!g1|tyH0v$DT+U2 z>n8MUia27C6aG^~^*Cd0ON-s3RBcYblwqV8n9IsHKc!QqxC^J)3&~lINes*~{anfN z0fRddh>J@_+8nzip$u)Wdc8StYHhCe-a#WqZTGo#+D{FZs+!OXgdDz5yG-XB@iv&( zt1t?xjLz{m&!orNs}uIf+JX1SZ|zqDz5?WUkQ{gc*QVC@iqC* z`SQD7@O#nOm@>pqIwg%N{scj}P5s};%%b15j*p0%ZBy`aoyJ35%FdlKV;KsdPLXnn zp~J1!J=5@kpTnt0nH~rob&5E9+z zh9X=`j;g)K3FJbkxiS6O|NAM#o*Mf#nxWKN2-#hSdQ8F(^bbE8Ot$of*XZ7;K^h94 zFY+E9q^BasS@t%4*#UEKnk&4b%jI*OnQ^ZbXaPg6&rs+AMu?d!WjTk=3q>s}nw{-d z`fVpS3`VTySCkJ$wu93>!I{K|scXzBtviD4W6xN_cR7r36FaSFjpH%6V`}+C?BHer zi09l&X}1geRa_ihp!?FPxgRwijl%@gKqkk}bllO>$;uqfF_|-{XI>-qt4Q=Z-IkRT zh{cTLcN4N)?R&fAJ(GpCs{X0wJnqV<+@sOq;Qg7IAsFd(HoEu@&J z;isl35GVS6TZqlg`OM2MAi7YuHQBr7YGE$OHa{WALgw$^Zu%?}IlZBqa1XG#svfe5 zazWz+xk#X2SD;FnEaB0~e-d7<`Q^s`e`@zrd8(ncGLTV5d8jdKu5a&Ly8UX9jAYVb!$suW z${Nb%RSyJcL+Dy0vp|l!^PmaY>Xql_W>Vnq<_4I-{E-hr&h8olq{we0Y%!|Nt@D=8SBtQ6akHxo-? zD)rE(_=E_ES+KF8Yxw^xRg3DPxUX9Gnd$-pQ^Lc)o_dsts=) zUZrEIieAY4>+^GW;imX(L0P=G9PgQ=9U#y3&rp4CHes0`cH`lwRzF0+dWBV=LlFAh zxnJRiUgDzAP+_W2(+x=bm$K*C+?u3Hv#uk#t`KgvC1wsvz%ufyv>q9cn6RkHV@d4& z&=)BC_AllKZZI5rOuX~MEI#U#zm5^H6uQ~-fn^jmDx6O_bE#vB{qj3aa^f5Q{=EBl zN#H4=U0fGkz4b6k9q&8e(RjOyyJw+fb|jK;a)$%;%ASu<{Xn&NEGd)!IrsRW4(1nJ ze$;kKw4!kIZkkp*eFVP7CtPcaNWhRE<=pk#I6k)YaW*~KILsV@aDq8Y+; zP&c=6ohp5=Nz1yR9_uD_Z1SMP@9bkIK;mo$sPD!QQ_>y+xz8dM(yKc0OmmC95O!|q z?$(~~GXwv;T~1`LJP#JY)Bh>-Z^euDO@hQ_LBJfp92@e`yjy9DE?k!JKeanox0e0I z@>V;9d>U7$0SQF_qF1452-k&jC-<&Tj1Up}TC?S>&>&MN5G-Qi@W3zDf#8$;olSEu z6#MqIYzCRl61f|{bm#`C&;<(~b%6uF_F~Sqj9`SfSKdc0G)UNdqBpFVU#z#Kt3)j& zExp(ibas8ooc7B=FE!#WA5QpC)|vL7n8n+x4sba*uZ$}~4aPn8^H>|JOYLC{s=ozV zDN4Mc6sAMd&KzT@H|cI zCA3$$qPBVh&mmT6x)JVMVUzPcwL=>J^FLX)zp8EexGL}d>BqqM{-wZbHlGKQA17w8 zn$fQ;wW`ZDx0N#=SKb?(A<7kZyON-AhBh%Tg?+?mqFlU^O zA0P~FxiUoq_D=#wzqKt-bCcyOKi|zD3|E4$Jo;SvF?VaX3>!7KIl>71tPuPAeqNnK z8`9)dF}E}_jjHY31q>dyvP6F@YZG1GIMPdhz+HbaJ2lsnJxU@Hu9wZ&vs(y*@dC|7 z3-bB|R%p)I+xN_LPcW65biR1NVG)6SWrVesa{F`nfpbiGxgNcnuGpv`nI4vV)~$21 zF|cB|3bpml4e`e{X9_yb7|Jp4AQ7$~}z|`{ul(=8$Z=dIpz9PdKQlwRUukk?g$G*m;(b z%+7e^#9W5Iv^ybxniG}F9k*J#UquJBFv+IDKW%rP+}snq|JrJ*`yzMc3tM|F*Jyw- z#Lh@LX_yxv)JgoMjZEg)sKTW)6!&DmvKeW=o9&k3m}SW_3BNM4f2Hn-Bs$h=!*2{1 zG~{YO$o-Ge{5)q!4QqVPNbQ-Bq(!XAP}YJmipRY`w*$VBI4&7I|GUC8m{UBWFZ|zK z!(l;lnAM*>ZEHy)Rk9f~WcKH79}xH%*MG}=<+0f90Sg%#y&C1vKXq5q?9crk+eg>> z94+Xr^J0MMpJ@i5cG2*mq44N6V`Hz1oTuxNCkb;(+}|x76W^*<9Q_UNg1YG=Z_@v* z9gyXyg4Nr6)*t@M(^p)5^6%r1f1d<*w`6ud?`rOTG27jDc+qO*-`AtYlBL~)sHcQk zD5(#tunQE{0BMJma!rkv`h&wQZvB1oSP=4OG(h02EQ2`t}s2bJlT9!^GS{E z6|lV)i}n~SqbN|VMMI$T_B)n{ShAg@Wd($gpgL9UXKg_-(z2kT6#Kp83x@7F#fQnN zBG?oHvz{344^W~|Kt_<10$Ye z0&7OJae(0nf(LeKY4v;wZ@MDH+*Lxg@8QVW;tT5JVVtNT`94_uaj`&=UQnx+TsvwM zE$7wmmqs1Vl+mNM;uL8#{(GF4bsU)(<|3`<6^mZbTD2mpQ{75(TT}YuWg*Xu%5q1N z9^_cHW=~}Zzk`vKVnLTz84`Yx!S*Osza=}e1?w6ALL(~J{=C;F;CuAFQmLZz7V04; zRzGfL5N(f11<)=c{34QVVtaA4*)mg7-D5K+tWa=Nz{H#PqF=Pqf^d#xqV&)Q6oKvMO`EEha7Cf7&wvr zIcmWz6GU=SFFgnh97eYMD3$_(o*diTVA)>`|QP+BAvFeZJwEct1j#m=Qd< z8UzzDDJ2=rl_5c9sV-i*oimoI=y4mNh(byv2ns6MVB0&WG{aTN;4aq4X@OU4m>7i5 zhM~^&n%mQ7Gmaz6%hDb;3M!!ZDbi3OR*H=6%Ml)tztnt8+G|P;s2?x4`d}wRT!L{P z8o=x+W{Cv-U_YIAgL&8DV;dSZegd#emS?7f zP=3~KbP2DfzI+KEB7cJjza_!|3?aoI|)%;eTn6m?m6eT_PLt}}kAUEZT_Udfz&@0FuU=gNkl>m_~JG*U^X!Q7% zHPt+|g@U3Ccb%J8QNpLWlA{kme-w7UDFCKJ9`2BGJGCsx#VRK4($dE+U;B@!Fb7^! zefQI3UEmGdYy&Qr9seQJB_br9B}^j;b+%rw<$?;k|m^QYI`$xQhV3SbMRQ% zyMFPFFtyF{{MT*?_Z%PT;iOO^6rZcWk2vai(!`*c0M9HFpLQ5{!SJFaws>Qqw8H~l zh2Og%soS68gbzL@O{x=fdmdwiD-=bD&oVHPAggb#?Baz~w#W8?#Of`DmyO)kNjg4< z6fBhW3#|DbHi{+fMHw(;ItI-+tkMgTrrrMEzjd0aU+lnWRXj3fI#2(t{{-;sStDpF z(og6xGLzY~Uv1+QaPX@X?aRKdr=XmWwLTXV4?~a-TldXhI^>MxHxqQQiRE(QKh?;9 zO$(t^@_eDx>AjmmYip|6#ZkEIzGUgGOt|u<^!fX1IK54NneF)Q%525nK7}}d$Vg=( z``OStRHpki?Fy;bJ+eLw5nIh&UBmku!C8)QJd1?i-+Mo(V8s4tVT3e?gU8{oS{#yq zOUG_a;CRm2^HE-8MD(4Wbt8^ME$C8YDMj)D3in-GTsklrpRq6{oS_Ipb9tUUM@9rS zK4`ghFJ?gK&caI7Xge4mY#v@~O(Du!9bbF=tGLVhzu^jsw7O&=tIOI*9}E$TWxzfh zL!fgIB43^s;3%%O;Axq!4k55DXOq^w*@I~G6d2qUZV>V_+W`SU6kpPyEtK)p$h@m>V7J5eRatEiYBW12@`Iz_$FV_ zPF5XNgHO|qS`rR5ACiBKPq*4!65k7J;gks(u2IX9b><^FL4>Tso69mo<`0Z&2stOC zRuopuA6mX9qj)?JUL*))*Dvh8GwVHd223EN*y+$PDMLONI_O=r|}QE;NiUG~e*7QBvYRCu+1;H>3b>35T6 z{hPq z>@_Rww8NdR-8n3R6I86HaftGgD`~ttW!3*#%skC5zmPmv|1`VINnQgiKtjGeUsndd zkCI5w*jYl&x81&X$|!^Bh?WpoOd(mm7Sf+Q{Jgp;QB#bxS(MW6b8s|+d*{*B$~i0+ zlGppSe(Qrrq$2j;KcV*XH$SI+t@6?MM0qTF@blSgR&UO7os|pPwquV4zr)=J=SKg1 z-*9vh5pc&cHP8KfwOjDQv3HpgPd|or-aUewyL#c}d$DlcRhi9%TZVNHAD@jiob83R zAA5PN=f{w$?dQy+)+-91b{^WAEk|wMAO7^~*^uq`j%zj_PM-PwV#QYfN8f)opDuj*{U2oi&Y+6z=d3e-R#bN% zF@E@Az@%4iH1|J2%wLoRKV9;}b7O+v^_YVeH7+svY%Xg<4)MsSCjk6Qy|jfK2Te#G^{{ zvo4DDo!6gSBVr@qE?rI2rN(*kRqU>2udFLIF4Zo#Tjp>0b)I-Ne*4C=TeP7}CD-oO z#m>mtmLn70H(%U|UKy`;z0>yUZsOOCR}*(`{r5jQL|ECahr6`*z^fZH+0(xAP`H$M z+H!0~1jf=3%#G{FBsv>9M=IO2L)Y2Z$hM5KB0eIGb3jl%_1g^D1?E3`e`0almI#fE6&wy(*@smYN*Z3(&2d$-4^i_4b&)x_j z|EkUrCNf>*{8|Yhm+8j>v?{cUg)i2V048b?1s4@rtOZ2m%L$`s9ch*$Mm4pJ16FHs zDgg||XlnToB)9$`BrG^X73p0+sz%cDrsMM0*wf18J+;fAY%SsnSx%>!Sa=|ytTA7! zFmwT9|IMoB4EaK+UknX2A~*15|j&DE7D_o~MXF zW0%n>Y!W~k^&kkeU9AtM6XiXyOd^9gfNkFk)iJ?m6Y=171eBA)wA#e7FkX6xk)n)+ zrFyW>P9|K%vMPXe9gL+5v?2I6R-3@**#>Btr+1m)3srZiEsP51o5;%-b(t~~-54jO zuWvC-Wf)rVQhWf3lytmy9UH+GZA#r z>MIIP2O5M=o6f|-JdInPC0uG?|FmSBPBE#Sat(=K8nI(k4AksXayKsb>@ zix5CT*w~S3x3eID1%{CNUVIynFETL#ixuF(Jz#y}m(fkgU+au4L>R$Y%_IrwtOnQR zKTuCsWE1DIR@ro9t~ZC?jmK~RMQ|c_^dt5Fpdn{<8c|?2(G6PhOHVGj+*tF z`2z?71wty2>(p#U?IjfWQpVOa*3;rFc>^U!i6}4 z(P9XhmpX|+86g53Fy08gZ!d#1p)nMR3*-H{Nx}#4J1Vgaj0U<~v zLt(drjRp4r5hTl0^#{?lpdFHABA+U61cJk8pvVrRBT9p2-QYw%DxU|+ci>&E(Oqwj^xII_{TnpIvvDCny?bO@sk)Ng50G&6C+uRf2M>0=EqM0IW`c>1l zT8XfUN-!Svf5y%#sLd{H*C8Y!p-6%|A-I;}uE8y6ad&Bf0&TGd_YxquG`P38w>T|D ziWVr+lp;kxTBuXl{4@W|KH4+;@I8IkGqYx%b>G(oh1%BvZncy4(qWSg3(AcmdpH@w z`Eeow&fZe+A*6)*HJTSeguI-O;ZcGm=(P;PpQ@?dLS?D>X5n&HH0Cu}x%7cJvvl!5 z5H}MsR>$8$OSMsRc1OVAv*|r#!Q3u@YS`e-zhI=>jS{lq5l$m8C=!Sz;Y8iZ(K#Ll zQ}ogx=m0UwhDIcX&{~uP*GtVuL=DKJ67^NW3+z8VErmFCIi`Mg;Hxs(4d1FSHgj|v^33TY#mUxk2Oo~ob3{VdEdSM%4Q*Ka&{3bP+3X(Y_O-2tEa3P+85&$BgJtVw`YHD_7emUl* z*;zz|DQ*RnUMRiWT&(v%z@)Wzc5lP6RdY9umzkPN)=eag;4 zhY)p^mNOKP2%yOl2tZJklRdnH4VGg9pac*g>I@QE{gdolbm}QDY+7hM>7aBASQ}sZ zoo-YgypV?>$?(^(15lil4`K=+v5uh$EI`wd5t22Tw!;V`VwRW4t(c)3#kq&YBk%}d zQx^X0&PP$L`=cppNbc1@xEK(sYXr+mOgLl*J4f zY|uzLYaJ6AP#2HV@Ju#9V{Y1tXfuy>ULZUs7=r{b?j(IUFg>k@1d_7IDdHYAq$0_@ z;|p&-bxWA1wB|0Dl0k7gazI(5g*YA_(@8p?eBG13FmJYTfO@VqQitJ<#-LqW8Wo7T z5Ce#$&UREZw8_&*M)NZKNY<2dZT+6`OzkucHA!Pm;s%%*LAt=y?Va0I8bCZ0S#1}& zU4V$5c%D}Y#Se?jeBP&_k$ecdScEeLU@V5@7q<9*Okx8$26%&#LM1orFG|h9SvpF4 z_0QBF^L*InMxLQMT}QO^g-mI+4)AP&+FF|9qiRadP<0W5+#7@V5|sc6OoQH?B2_Ie z34|fja>oGllUYWktf15sY7UA_Q1MQpn1FyrRjDOVa`ER$ERhKBs^(-Vc{w182}e>C zjMme)Ks|<+!czlDDG1NGRGCnIz?3)?Q4K(980DSdo&Wq6BDp5Q2w^CaOacr_Hl$HI zD7x`l#aHV|2~-R-qkeOJtNxlzpAsM*)0!p7^(_wC+YrlNYexzEO>4u!&1W{Bea45I zN%Gwz6!Gq_;Q!ohkJ7Je-juEN<(`1r-oxCyrk$^l{F8O$60w*M?5oiCN zNX8*`mvMBhqQ-}JlWzgDk{TrkDB!$t;2Hc!J{$x9>jMO7NEB1& zqE;IL!Vb+myK`cwV*q$@Lnw(q9uULb zBTx+%(hH^&sEgf7aCLNf3USHY7A>#~BwrkcheogElSX2pPXnR%&Mbg%vN$zR%p4T`5a8(y z%?NxXd-g zf`V#U!PSQ(?D=7ZNGKV>ooY?}3TgeAY;qsHT3Jd@6)UmWPDJ-_)d$8bCO-Kn=~vkk7&!FBJ-i!DPfs!Wx*N z_ay-LhM;+BuyhO*iUHgNwbGH2*#wca;D}~!fw-1?i`k^G0$37JOg~#d| z15*MC8dZ?idP35(Q*^wcdKE!&tGd}p)tgJtq%!->D)8+W5>OzC;%HIGITZfu5!fZ+ zXE4lJ4X%aEk7-F@Y*C&)&uNm6ByBAY!+ENlL7Aigf5)I&a0zdK@$3VV13swGHsq(K z#1a%r8tC>E6Bo-5%S37y!_YzB3cu+BxLcr+i~!1$+uG4bIsX>;m6bTFJVE%@3Q0&tg4EJ)8( zix0%QC^}b%s@*^Xjb>JplQAGMm`sEk%x#VU_a-ZBF-}^#3ylGL6CW74s!Hh8GLhvo zSx9i4X{c$zrADAe{7}3zSm6PTLkj+A8A6V!B%Bv+=>Tk{F7m|UV6Z+9jt&7<0dsY2fMXHYiiCeDnC~c|;r0YqD4}Li0_^RK zXO1N+8oDFgC%}kJe!nE(Owbrc@oOi;3NSD@YilmCCC!4YUb9OJTh0gyL0JpaOp%%xi8S{&k!c~I!g&+1 z$XFxh=`0mMW(CG&W9FpaD!}jdH-Ejpkk;EK@c@!e{e^BOp)dqk6>qWf1D9wu_^Wsb z{7lURENBam68tUjpFV)P%D#mGL;uw~@P`)+ukkd=0X(+5T4ccaL*3E0 z6bMCe6hkgw(?~^~$*8VHT=4*?NI$fJJJVVma@F2!;nU0Fk7C>C?O6eqT}~ExBxgz0 zs~}++xL2fet}i-nX2~Gi4l{JnEHf}L7pmtJ3D;MGV1`Gu@Gx* zLC_Fcjx(5IHzCd$Ch?F9gwewBK=XNVl z2=!ipi{yHw z!;T&7K_CDGs4c+i0pS(tL|(A7jBZ1fQe1!-5$Q5!GGh6D`(+B?dg)D7o+6EF|}pb2$w zU4WXir$%79U&S*utNLV|bXHCM`<>Mxy9I`&Y!$&Ndx4w3taL*rSZTXwY+!iSss5o& zgv@!S)m$zMVt!+RH;V`RFXu9~apXygFIcThe136iOdrf-Y13(CiVkQpe zB1D>v852kK=g5w&x{j`K+de&uCc6U+BS3pAKxyu@q0Z2Um4Xmtb{YYSRRh)LbJZQ{ zz4);h)01h`#up^DgN2v(XiAXqLsJRRNHx+3B$PMrshM&6J$RtxBdDSX+}x`cK54wR z4NmSDG|DE8c2-^>hs25R#1ce}+5{BpaSw>V*da2m^Y_Uu&;$V3<~k7S^&yr~CxSVc z1dD_gUhpMqfRcvp;8g$tfzUBa(KzhqH~=JF0z9;B69b2S6#%G5Lp4vyAd!G*B(z@v zPUZ}q;DO^*pzWuS#52JqSJlX&sG?oCRVu(TK~`2&V|exUTT(wlGC;vvaL!nxd<0-^ zB&dmsHYbvC$pFH!qc5xkL0CXMnI9^kZkPUuUH~i28yjR3s>?Lr5;>Bb({B02+`66U^#5kUnE<0A6f1e~UIqyfqDHqNRSo zdIy|?gW-rVET4RHfus@6WPzU11ZzRrIY?v+nU7lHZ4z(<5z=i)cc%v--$E9OBSl;@ zo~uN8KKbGO?1%5*5C8kox*vadAU;|^f8KL_qMXdIs>}88$>dbVSRg zY&`#68Sz_q7#sBh(R9uDq5GIl{kLk?>5JP8_#@hwT0qUSzjWn)hNv0%R4*rq$9t)v>6K?oFU>Loa``k z##3Mjt%a=l&l*5~ltV>`j~M~MX(>Uc7C5AzDT;{p|4p-}Uq~y;Q1W9n_y*-HrD5y5O!r>W}%f=}5gRK`-+!o#SO^3&Uw zOe?4BedH-DGk;rsj5|San+2E7ZEU3#{k;S=1sY=*|q)5?R&4@N{Z!iCk2& zUMD>2k98J+R4V$1yE-A+ZI}do2-2o_S2iweMO4v&VLn5v-S;Jzb&ECN)2BmUu& zA4A`U2&h%*^XR>SGeeO8O2cnVfFxb`9d{Z%v3d}09a^w0g5<3qMQAe>xmFosMKnk_ z;q{<52FN>~D@cOn;;ZOLO{-dghd3T>eLA>1h?*tEJ&78DLDHN)LJeURB$#Q(p!-W1mH%a zJ2$7EqArdY!?MoPV0H8vhBcDzAj{n;Qkak2`B)o@+b9(5l zNu!qvCj!*P@1SAqb90@IB=}$V)7fgQce16RtE9BDz6%vJ>QBSz?wKTkN+6OnIwE%tA2Ur;Hk&_aG`n;+gEU8tu3#aH zK}qv-_C$@qwL69COb?G^5VhdY#OsbTbL`F8yX#X^>WeHkBLB^eP`0BAT;mzEO&q&8 ztNyFk4TxNFs@>6^o_*Zfx4t?>Ab=2J;MLR}22{`(c;I9R^fce?9@;Sx&0pRfR>`k% z*q!eM(%D+*$nFCTixKkW{^27yW5w{!5Gt-`j1E-;pq};tguOwtL0!2?3u?I`x^XD~ zo7$)$(};FpPdqCW2iE_kZYuhig5_$b8!9;MLchHp4@GNs86|%)k(MEK0|>i%A)RYt(X`fLlA5Jn6ZvrEORD9R{b^18J_pZ$S4e%LWtps zVL*)W4q=(%I*14yMy>3S$PQ$~B8a|myd1-1G6gY|FIl0P0}!bGF`tXPIbcTOod4Y$ zOeERGT2nVVvCKx~DF3r-WCSj;`189THUJRby4qoix86?t5$I*UtL^_UFx* zT8p9lJpxX_xk7-{ni0tqh;LCsp?{C=j*eJ^^wfalGT%tJu~vPW)*3K~9xqRFARa-t z9ZXJ$;pQs`v))mI7kX1ng-HUKk*Ii^eh`w+G^v~NMk>>TZJjM_!JeEvTe9;SC9aj1 zw20QHOH>OU>B;skwM2xhe=_u3m8lPdkY^L(xqdlA^#m#efIY(~kr=27RKv6BGfHgO zIzIi2L zO29O^$X=}?8#*%V=nB`mZVU3m?Kkm|_H>#TvtD#sgCmAh}6c<2W^RH`BoyG;U`?Je%1_ zG4{FS^X+DPAJmVib)NT1loRZaIN@FMfBLNs7aE~Obm_>0*XJ{8qWsp ze)9-COV&+SHf5UoB_(LVD-6?Z3$w zbh_)^`;S+P%c|lZym_W-PXE-`h6{0Ba%f-_u^8D<+gV6itg4EOj^gW1RATpYdGevO zal8`|dSWN|&hDA=aB|0Dmu25Y_13|^>1$K6(=2+Hy~h5n&X?8ymL47cB~lGy#yHCk zR1OTfpN_D;Qr|fq4H*cyRQYL79&$=nBN+*e`P3JuAjf|@JDZBs6(iUFL8tn%i}UpC zBS;LL(f%Of(_irkY34~@4)%RH9oN~U*^4KI0}(V6KLRQlQD4^Tq-yAS9mI6+2Ltg3 zac1Gw?F23_^dBh~ zmmEpWga+1x4H-LQj#rOEC;5pIe2@FPn-w4YmTe7iZS5~*5(aCwwYNSOUogmg*8SXN z0;|{+XV2pr&A_C+)cl>)#_T`zlAVIXbz66{AN^*?wbCYM!MhH@tdt#6y79c1J}0oc zo*WB9iP@J&_4l~GDmU#uyR-4;1sUVf=N|!I9zV2RyO|np2wk>|}< zKV&7#8Wx|3p8wT~R`F3<;+79L7914|Gt~q1{`&4|C zRQ3GZTu&1%2!N7NKj*RTZ3tZ#yf1Eh-S~la92l= zOn{`4y5Jx=9;egF_2IBjTVDQ$q|h}bn;0C%TVKSnd&HCOS9>-;#CM!eNahL;CTEU^ ze`xPdqb(APB!tDvH}$iQR&o@z%RXH1A@+9?xE1wzsuSc@$WumRhq*p~)97c0BT9I% zuF6khx&+WA=AdEnb=gd&=Ti2A0`9%Bqf8VV9M_T>>)Cw`w09^D)KvY3nKVc9+eM2< z$7#k?d;AKb&wFl&q#1>>p>LHFX}c2R#-iHgxMSt$M9@NiC#n}l`TM2o{=BSN&~~2E z2BpYFXUP&UwJJ*m-UF!x1f3pXTvxJAotnDpK#HolwiIo*eI=(it*+QDd92hM%N(YJ zl6EP-8~=lLGEbdfiEi3}?sfXKZpMc0(_`JtOI;j=UKS|zQH1(JPj{Auk2SnI|}lWl|J`h@)=Eci0N)m?uirl(6}$n8j0 z(?>rat@3^JmGD@(|KGR9+XJfUqlFjw$0Q}@>3wC;YH%weeeG35WjShRbVI0n%kPyM zOiji@`dGNSTvMC;f#N{jE7g=&h6|#!Jc=8rnW?ev_lvK{kNQa?n*I9`>DSMe7;CxJw~s`1HWXG}Cn)@08KL^03q3;@8oIhG_qpSp?F+(v z@ri;Z7mBqM{crh?xqy#h==7H!55)VGl!v%g>&d$>MBY7&)954}7W(^&9bZOiQ%p0( zr8_)pG+%Zo#rBi6!g&^?X3+fLoYV9=~$wxL652s6_Z-scjJ5k8*N zLG`{Hm?OsaA$W#!gmulsx}WCe)Vl@wv3EJ513V^W z6e<_Qd30_SlN5&6784cpvN>KH*;dwNK5Jd|%+yGuWW$K- z#hfr+X}g?%v_w186Q(I+X|I^!f{{*FaowdJ?43PKSAx*@1}t01P0dSn6!R@J_2CWS z&Bh;GUrxa8oV)4sZ3}TX4>r5=7fn~WJG|~lE8ilW=7}8KTEs@Sy zG>Aj5TtBTUw=FK%Ow-4WS=%1XI`3L%G)+0lF4=1|4>tx9{VN0QGyBQOQW>D*Pf2+EU8>GI?r z-R^Oen;_Ym+&i_D^9W`Xm6UW93;qTBQZp|TOYZ9qW$ES5UG4M}{VntRg|>A@&tA02 z7r3i7o-JZW*8@E?f-mm+SG`VC)Op$2g8!%4F$Q~OuJJ0);I$TfA%ZmwWl&UV*9smR zDQTV>U1}tKX2SF4RCptw8y(j>YpFE%m)1zdQuXY>-oR&AbkSsR89!6)FtNBIytCYY z0>7Slpwb_t@`_zPEk>U$s>-{MXDWfeAErj5Il*?)0^P9QWU02Hm|4`ae6H|JGMeFxs^bqUe4`muD)ifz}8Geh(dv*j{+u(xh-RM=@#YVjI8 zctMZ8+~g;@*C(?%|50I1bh*uaMt(1}_G^~oVtUH(9jBO-!3gDvxX!Z9W33Qd&7bW~ z;#GJB_Vu2)cNe0sYsZPce&(a0@0>)pa{Spg6-O{UnDn~QYID2)-nblp=DDUb>^w)` zMw_+>$x?C;*=O{xR%_fds*y8N`-Hcpwjbf(VKbaw5#XJa7IohGQ+Cnx_Pp}blGf^V zLTMj$jjC|jU78w|FUM84t%mg{hoFAqzi2mCif5yjR@aoPh4(f#jmEaMR9lvZsV6^& zbt-6rH-EB^Q3+~TD?bagcsD0KsiiW@n9z$~ta?3g!m5@RxRp#h=PKwu%HiWUMawhpTKwi~ z>F?7^J98FNRx2-^|8fs=c34)Ksj0RvSN^eRjGv7{eEQ37D6T3g?3HuMnk*qwb1Z4b~f;RY$BTP>q`))r+?AKkjqCl{_Fa3q$8#*AoLO z$`spMLaPUpAB^#gTyMMai?;o_3U@DFswhuA;OGqky`svyyC3D^YcHc5ruARwh;D*| zIS*(1XN*Y4&dBAE+j`IYF^his?t6_kRaDYSLcZB}Ni;$~x+10rTLc*2Lk7_jZNm&%6{3GuF+AilZ{O=A8arF zy4IAu95naq3+sMDckDHFcf_-{2h0=_tur(McVj!QlsZr`qlie0d!0HQcC$)5$k%F%F06GORLghi5e!G zaXq$OIQri`hwn>f(*d#NYMe^utWOT9qR~Eom0u29OHdj5EaROrzFzqqzY9>1&aL9N zltWa1Wtq7z^YmUQdGOb~`}aki|Ip4eb8441%_Qx;2Ic;@#f=u989TQdzeY&=l)O6e zGB#`$#t_0zeZJ5Kv8i+gBMydIfmf$_|?;eRscSz1bmArsQ(1_w*S%fs$ou5igv zh5xo6o2B#4sPhE`YBGK^kf`Q7QQ-+&NljzA!#SV?`c!g7jQQT8v~hIpesbTz(N&6f z@K#!llhUJ`EX;z%%WKgt^7lWftt_G={C!`vjkbQfga5&u>g;#%-|)G(`i0DX)XDFO z$BIA8HavGZg<9K0tLc(HDSx~_9=W+~uBssN7(2R2*7-`BXC{DQU#t9g$CkPi+ir1lR*hqrBh#(A67G`S1|h+!=V?u)DSZBM!d) zS2q>)6#vhDaS6zOIZe3Cg?@|CZYRC?hm(p9p`W1&iWFVkzYK_W>;9WI(Z)^{<-t0A zdfg+J&_HhJAjr4Xo4C#HrJEb3UapAiSz9IInc5?Rg!=s!05_2jdMMmG5DNg4GfG$p zh;t3cQFGJiTF()HDYUFIBG9aI01!$~-Xjo0U6JVT+@u+E%|YLH?y3JgDq~@Dy|w!N zbfKMXm+#_%-E`b7D2`a!kwZ4ffZDYXVgS|7%*byKP^Yb45qJ{ zJ^ET}liL7EzvDGJQB?b4mi)mVyRE0o8q&TP-VfC}xzvpMk1jvGqhcrh-n@AKxK*0D zhX2`W&8Nb5k8>DHRuzK!wRq@4!C_K3vs{lncGK6@Nh`syZ`E5GrIg(au}|dpt)0ri z)de*Yc@sT{W@PE@9DBj< zUF@sn+(X&Q_ZP9>)_93e_CA4EG`J2g%0n!SJ2-uQfvv9#e5vjvBJq~HE8He(eZN`t zy2sb-vy|rPJ$>8eSNJT2!{}K2<%McJlgPHwMad1DZ`=yyf7=MyoAD*C8eAN#xP^zf zJbo7Csd2zpcf(5dNoe6^s($RZEXQh)X&JiaRlqL0`b0*9<)%Y)Plz$|5Mkxv@Acdp zUmhmYb@#4|q62=UZ(4T!SQnQ#!(lHpdUfS@Z8z2O=MT5--Wp$ksg$`6A?M_q z{pPePSTii4chyEJx@_wsGB{ZT%W)8c6f*D6;6efqM;e_l(`MN#u4mlyvH z8ZPhLIU0Wx@v=D*#LqLI<93@^jp|)Jgr_aTlFPe)i9Z}1EgkTz;*kM?yO8ir`lcKRnEFUQj#e) zL@H3OP!v=aw``yKD1MK@I7e9AzgFN*?$<&wjwR53^(g4=9WPj|GwyRhPr<5=ALUw<~x&f^}=u6ncv9R zpc&*_rd;t{2CAN&^_L4W47`5#p`wG#FNNFKE!K>BLrRmf!xtNKB?rWh##!$|Fi*k4 zSl_lPJ}RTMug8t|_qmxI)~Y8B2fi@whS)@Kgoq7_hPEmR~1zBtqQjd@d}rQ zdXPFNShhvqamInG<(v3}w^>~=k2!ulZfajKJkEVSB5^%y9z(XDOuub~_^b@R$BOKT>%YQ)}Jvw~~1h`HnKj>9}G<{d!S;2Cb zpHnussEHr1u-KNoizLE>HakBGp}(bd7%_%Rz~QmRXM+mr5$a-uY2i2 zwto(E74AI;JLxug*B))rHryF-=ob)5AWIazTnJ9OTW`z7=xn!{H|27r%<;OtO75`N zaZc=Wt+zuBY@jYAOHA*&XA6=4^qrgj-38Rx+qQ?_zwMh+9-RhO2j)plny!3g^t`4^ zsAH|h*`Zbo$_eVw=KQKZP%uYqcJlpaE^RukP0!k?lwOjaXV#JP z6Q|W+U7QJ?IHjL@XPF)>Nbg(10puv9YtFgCfM;EZn+{atrc1YC?FvJ525lG#>`pI! zG5)j3tN6tF9`{an{yN{=xoky=c~zt^CT8A2B>S_o>2+@g-N=ecb;b|6)q|IJeM`o6 zf)>)$Kdp$kFzQooP0l`6wl4iwz!YoEy1>P;SI%)b!+u)M2{Gop;$l=;6Q`d^0B0bz zb+VbacQz(3dhYIbd@?ir!~xt)ez`K(3*!mf`m@C3%v-@r!}ir;mG>!;T_NWS*p+_U znsS88DXSu_h5qX6TCM4aU#%OLW0PefhI5Yy;={&oI@q}-R`_NkW z9OV=p{c;3*K2% zHpQ({RfsGLzKW@i*6sB#Q=cBgRgB5y%j2MP8a=h) zjj2iJp+cvM$h%zs+&-#tAm`ty&EnlH;vL`QKAWcqV4iHWVaeWcvr2cu8)mtCOzP&1 zd)ZHZ-=Ooz%^!3y)IB!)JipNXHAyl7l@Q9R#PzPpVan`%>cwUOb%Q0NGMyuu*QUo- zS~{Uz;2PVwtYuY}T_E-Txd8LhKSDFX^P|PwKXIgGnS+;*i$DFG%(lU3`4|(d=-kD9 zlh@={_QDZP_a-GJoi)KG=;)YAP3-0SIG{^V%Cx^iM@+j1EKCHjBzbIG+4h>OS2-=* zVX0&*3yCes31!xV&^;n&r;vhchYNYsw6iT?)#(GdMrJ*^;2574VmiB!1{7E z*8+dp4id2Z;{`5}4jZ_)C0Tie`IOFV_YnQ1#v$Kn-1xR*xV&}*4%||G=(BwayTDiF z^|3}pw9yHweLh&1l_&&v>06hwQ^99H>YrT+5+H}1*%eDl8ErE&(Ry5hG`<^wtI$}+ z(s@fsSFSw^#%dX3uJBB;netU}U(a5bB#w7Hz-mj!h7-#DQegcAPhrt^`ne&Q?(pH0 zu7fxK_6B=WrF^xMM0Ny68p?}&XLfu|ppASApII6XUyvj@7^7N8KBH(na*gJ#$&DLX zc$cT~d!05Oq1?qjM<3;pzM^%%R}D3y5@w~6mo4k61-CoToC=Jf4UdcQ%S3$ZZ&SxdN}<+q#8NNUmRTQIVs?A^$rLr5&dmn zN=G0UT=pTDSL-IvZCjOP>AkUf*8AbuF5uSpC^z@WlEuDx@-m+~22*cdytQ=g7)v8< z=cc)_mEr0#_^`=(e6u}-JmloV<5!tv38v4=Q~Z9>^m!cCu(6GAHC`ikHB|&Vd~Gyk znz}AZti&AM*_Y;kn|GuIwQPBwSfrBM*ma3j)U4LoRpB3|2S_nz?kXSfkX$=<2D)3y zh(5k*k}204?zv0-d*1|+z3i|^$qw3sP{Su!;@&47->V4xQu5OK^c9|(=D<5G<9!;# zr*|&p(h@~A4)bpC`-iUO9tBx08C)M4@U>AoRrQ=@p~oNaW)q;s)H86TtFZN$@IFHo z#m)ZY1o3BZq>Oy~#ubtmADasytVBB0Zot2mJFMTLhkN9JbbY`5H z_eR|66}?b%>CEw?=Y`R$|60$M-%D{@{W2&{%bIOU*4d?;&-AS(-9uzY6`KeN+_L=K9Vi})A*b}D=a#-c21=aT7#Yq( z%%Y>}27a9#p1MOFb^0<2Q2##iyDYraI-^qDObV@p4BG4q2o|azq|m#EYt3D)YJnPq zc-rpH@_f!s6e#X%g*at_UQW+drL$3EXi-A6%x@l*6Mn+mxbkWKIYK%f7XZ0~qb5Bu z;zprbLc+giiUVwaQJ~srn6d#4LSdQT>OK`qycRg}iK&ukYz=<$vXn5VVxZy}Z|Y|+dU1CAo&oVR3kzN>erjaEg< zQzvO4-?~oOXwPRr%m<>U&M4;J2A6%9eNQLs%o#MLX&Ph@Y^3~jU-P8&c@SexnIvOZ z&hc|{8T}K)v%rfzD)3uDI+`=zQ};Rg`)x z$E&;6PkFSqz1kw}&w?r{GD7ULnsPZ_pD8&QX-$1>^}lIh1BIJ)0?4a&1pexljNxye zCVu26<1OAxXc$=d>t3DmV|8+kY`K-~#TU=Y7D!m^`ncMr&$sJ&U*)i?u=_O1i~Qv4 zqz{^?_S?B4LacjOEuETf{Xv!_*FeD{Tf5n}OZn7;L;l*Ef3sS5e$qAaR*E;lk-@;$ zdF4O%DznIuD&H3pUP3(YMQbg%ZQS?ujj8z+9l{vC?(yhtd>A@7jYvQ@W{(l_#8 z=<%)J7t+F#U7{fabP3WQby8Hs*fbkLcIs-IS%oUNWm9W9L`(LAEQhqaYd;ld7EPQg z$Vqserv=p{7<|0R;pu3bc~Z+g^|ddsT-fNDAn_ElpUDP4xy}h)7o!Stb{9i@{vE*b z!}3{S^Z$C2TG8RgKtyO70t^Bdd9A3Y=(N!zs88H7l zu}Q&CzGNrR_e()T`9A!B<*7Wak*eivf}I!7&p$4jy*0r!MX$r<4Sos#HuOn77uD#C z)@w@mE_|lZ|3Wu{mrSsewDPivT%6iE2xbz~wax4mgnn|JU%&b_j3AVsCxUNV&YKA$ z9S)C}EmMFvgT(U z?JMIU@aMe(K{#v3^ZB@;c^LP9a%Ou2&rdc>=RQ4G`n@OjW1RKs&(q2Mgz^AWx^-d} z!&&AZVNSA$-)LI;c}jKpU1{JS_mCn-(LZ)+G<2clsujD8VN`MnzNK0K3+z;-1J+;86;$TMgFwqwrbv-Pf( zv`M<=BG#uNc990RVri#kTM>Ov#J+Xyc}v}Y8&NyMer*9Z;C*ut|H4uR0-j`8oFL<3 z*Ij(!kjHBBdI`ZCHNlG6IpGiDe9X#-w{GFTZCrYoRj29ba0N7sEpl$&K)TCj<9A^{ zKh20&v+F!@L!Tp;hZ$uL62xoteYW_f_Xqv0$J_`N!;rC8PzyxY2__s zNe>VGvRZa;G8OM9TeFqR;CJ}Fu!}%^F6&e@lWgfSo|f{l4ZpGKyEmOvYnFHv*z7ec z#Q6GnLt@WkDED_)#!ZKRX_CHrWLJ(^(kV{M;QXI(Z{_r6-4R1>T2ZLg(4)#99fDH5 zo(J8nwj0(V-Q$~6t?TjAn}0*gONafA?{#~REbrz!{VXdxeB8Ys^xWj*rtF0niovPM3it3%xtugQNYTZ8}b=h-M zQv%B+#BB#{G=kXFGIwnZ~X0?IzP5WA(;W z<$Tyvn_8c*>Ax-e!k(x3R$l@0h03dBnm^X0JB0Jr8A24kGz1nKR9H<(9Jk(~H5U`k zKQDSX-_>R$U+H&3DKGZuXyW~0VeZxYy=*S6Z%av$W#1dK?ujXW*7hQwu_0Q8FdQ_m zgoWFV7KR(1w*ZZ8j0(o`@?=K-JNzI&Nf!6p?G1f;hX~l#C~uwjVb~W9YgpdH_fkiT zZ*MPCks7$=IR3MD>HGcu{o*_gga5TtnS!OY*BS}iqpm`>J*y@;kMDPDO4cW4ME1va zt#+lI>=u?L#C=sZ2A{vOv2VTjsmnFvw-OzZDoL?9^pmwcMaZY*TS05fH<2$PLIwi; z*28Z@+e!moV;RF4FaSo`V-Iqy6=Jq@qKfT{vsSpKeHWJ>nLs^G9NE}=J>s;qk4tF#n9~_ zVK%y&95h)e7CR{BHC-JQn(VAv{0@(xjh=xcO-|v`4xjv*o=G$fzMO7XK+i_syo@Hd z-eOnCW=;R@E1JAk{H}1E(vRf?>Kn@e~_FHxAmuNsB#+!ZkxL z722YC{GL?3jgg-tZL!EV4H;<<02mun{E2Q~Zqmm1K^bl7fd&6WSFbT!z$aux3yyu` zpj>CkTu0{@T<+@u0dql1Aj#OH2iaocfH{Qc6wv==m%#pqT_OUYA;FXU54(i+|G_TN zisNJR9C*{}_Bw-uw+-HdBDsZQ(P6P*ZsXOVBQxa_=US|u!Lj;vp-FOFjOqBo&8DH| z`!kiaaVUqk9rcT~n9BcUmvEUo->^%L%$jfR#sAAL5f1tPWS8K$4ay9A+jgf4BxQMU zYFI$pGwI6jN{=#~l<742e>8nS9`xCY!Tew5nZ(KvoCY0Vu1g2>*2pW)e43Xjj?3UU zyfS(;gxvz!Jj|0rs((+MeL)@he;9kKsHoofZ+nUfa_Ghxy2K%*#TmLpLQ?QYhlF$r z4&5+xAS=snQ^zqM{Ni5C7+Vp0(bicfI@YKH3NOy7#?4-)n!aI`(3~mVUSK zwY(dqbLjnBu5d<2`~~(KjBMK8KYWuU3?D;vw$+gIv&IBjdP`-WK0;looF4Mty1F%U z!W*GCNMLx!p4yL2MRLP&2`1^PDp+p1VrrMb7TUf#;#iYEQW6D~`3gWKNX`}F15 zC~XbobC|0Do(y4$0|3_obl+!QBW%^Oi!QQe18q2lKL*Y8wttLepO7T*c=Nj3fv6Z3 z4}GCa{OphL(s7S`Y>KTWuo04bPtqaGfwOrxZCrf*Zo6|OPb1kRG(aBx*Q6;f@emL1e{P4?i55}UHKoXDj$U25=N3O zus4pZid!?kOtWF!r`69+yCUq>BiaC;6cyRht#KP75%pMBu5?cV!bJ4T6l{-wDB+6} z3|dH)Q(-pAIK*a=5P{#*CBc6$0qMrjB@Ad4Rpl zyB0s5LV(dDPS>H-dTr|G&#yx4RlfTi=lW_?MC&e_-MRC;-?P&^rL@@azjr`}CrxcX zH_{9}es4zK`FFmX_3YopVMV=y-P;9{PID>!)(gcvMr-t<;EfMYk(Lz#cmDC1beFd* zll+^Tz_Q*GHTP=6w>NX82O;39i9g72(OBk!^4A(=lxm0dC= zj7#812P;EqCoEM8n?;%0#?jz&^Q)8Af&$zf&Vil(ysz`c8)u0c05=oEe(sKn9R%P3 z97R+@HV`;|B4ZFMg^EaQT_m8`9|0)2CK?t*AT+JvdJ!#5R{h6JoYu1oVURcDXp(?a z`BI>XB2y;b#<1#c?*#{op0A4B8d4z{KMa;SXbXjVqQiB^W0Ll0_D^P>fC-z_{}~8r zu?cxl4h&{jIwDuq55!5thPkdf3b3~!Nu`g9d^*Cp(^EY}hOg6`ugVC1?dr_J=-@6ISnjC4Nl;RGxSEe3jtxLvLH z8iLh)5{N1bFE7tu@bYF{R%oIgJq@(@qlqn6EaCO1IQAL>zx&2XGlAwo(VSc|tHC_G zo>Oy&L~wI+u1XR#T?d}rEiU!Jw=0@5K-}2g9QgL@&TA~C729e(0F76Tqx_poAm}}x z)6)V-wm!Po7S5CX?kym&Rh$I>iR|&vaMQ2?{pP{6K*%Z@xruc!{ zMKFyal8CiDgOKvXqOBdAtu!I<8V3QsuQH^A#TLd6H$6+D=VId*<5{z8<9QXq@#Owv zlTdj>RLCjM(`Rc+S?t>lNYOe6pR>30U+P~hh;C*>h;arwYcPK<<;X7>Rpb1r~z_Iqx&HNlpk4>*=qzMv^GB5N3vdyNuOe!Rh`9U71hQi;7)JdcD^eGJwVk}OY z`F(;{M$xTHexF@}uGTjx+dIY{+*G)A3$82p{g%jg^D(9>{=(tsyw3h?l8_{G_Nv`+ zvFhuOZ`~jFrlk6B|M(gjUB3x^*fGdBa~dVLAq;xaG0dm)E$;E3ZKk&!qf#^9l7jy1 ze2>y@7Z+KNR@-=^cr8EmOwgm*il>b;m^2M&`#eR`)^Wrk{8t)l*y&yw+qT|*XL`!_ zzMOUCr)<-mvr2>g4!4jqm6C!g{yYm2Oz#T?$G8@xNIigbn<|T9%ar@bvR<7tsX5oT z4&SgQg|v`IJtFEOSQ8|sM3@%%RIZ z&xM1AXRb>%Z&HrV9P0yZJO9ya+J2LtAq59Kjk@8#Kj?@vt6azm z?@}Q5%hSl7vEJ)DW#fK*;Z-wHkOfkpfH2(p?lyB^F~Hnx`PxwpvAjCud~EonO(p9> z*Uv~_N`BJ*K|3EZn+O+K@_ThRsD3QO?SMsP#Elag>Yy50oM9O$83gi!pX$Bnl0bS8 zbPy=SS8LnX)=J__%-Lcnt~3YIl&~p7^^e=Zk;);gJk}=M1_z75J0K;d@nGFiJo7kR zm@a}&%$jCNg@<2T@2(cl7}5yIUxY@qY-yHP7;S88y1UDPsCn$ML1BKH!O{Mo8H4N( z@p}ishwVWGG3TeM{(o&k7Rr$@teq3;ZoQ$SkRtLAP%W7RlkJIb-hvkW7TeTQ2BRUT zQU18=b8nAxzs|AH^l>*isR$)n4AUXatTEC}DTKiJgfbL?_eTa;1~IpR(+KGI9_;^R z0Ir72=C}TPemClH9w%!Y;s6iX`2CC=63sGh?6neQB83e4B_@FZEH*_eh;x(kY8NoD zM5~L}hViMygttoL?arh4VIc?6w$AiPM}Dy>Op2@g32M|vWL!^wq@W?QUIn6#^f-+# zb4E?Sy;vP%VT12Wuz=Ikqk%zx+Xg1 zHS+6=rfFAr@1=pOjWiA;Q&0{JSq=BucGJ{M>BmGp2UgMp8^iQXGmHW5RW)_Lws41X6D+$E1WS6t|*YcX+!XsP!1_>{mTJbHWv2NmbwC z13oeF3M7S37^seW`kn0y?+IJkJ2Z`COJA~AK9g5 zRArM)WEW2aI>4WVzDthF&diZ{maJj-?>zbAa*oMBl;}h=p*m@~Wl=?f5fkyVOIpJJRe5)RdZF4gmAsl7Ixf+|gOl{;Q0W+(rUAtE|` znpSh)YRHjIMnhu@CI_;hRe6fz1q1wfu`-#k&X8BjFQYW9>5g*RKNa!IgtUSaIwy#d z8ic2+1=Y-j@tt`?=VFm+B`inDiY7S+Vo%`{#rI+fLM#QZokCe-irjLF7{*_!Pv)my z6#G|&PIV?K9wlBd7gRFQXTYB=1_YUR6>m!yK|bZvNuYLgaQyy^Q`< zXhfY~Tb28IB3voA%(^ohXjUX|5<#rWKh7zA^C=4@SO7DNZ7TP&zSc~#iDg1OtuVXH z^hU5~Q)hn&t$L!7kefo><*hn?mr*QRU20Zc9#~ze$();8J#|J`qsjC+DyL-2vr)FV zATXY6x5%w1?99FXvvV5c_QF+!G_)pc&UnCsYs};CP3nf;sS| zW_Js{S>=7rI&rg>a;M^1=9mAiCCbTF+H1BbXNUZhA#ljmCaOoV3FRkGwqm9Vff}um zzni++Go^By1A^N0oGa0bFaJGtK?+swlh&(~6{#GzQy+UpPBnpIvvvhLemd1zT#7R0 zHXE)L=QEe1y6e+r@UG6ypi6~egg8vF`Q_v0dsC?$Rc#c;1RWzGe*vUT1}^-F<0 z(E`ezW|^a_ui!Nmp8PN2@y=iPYHY~d`N$Z(rm7E8d)cn%3O`)+`iu7L1Qi^xGHvGd zovinLx#~MzPdgv49ain$%OiALrk!ar^j&)Wtf})Gc$Fmkx)a@K2W&|_Zx3cJ+avR8 zsU})Yyxy6RRO)VG_PX(;hHkDEue18Qy{mz2)PW-UX@g4r*RpD3+jeqZO4roio$7BF zD&pyBdOF!}?fiPyx#|m|H0mnx7I|kc|Io8>;NnJm7doz$hAtCmk3VQ zx?CmOZVV`$_{B~Y(rb-VnguDCw78$S;x#&?}R9W@cI`I7sZqPph(Iu^U$;lw|= zHeSbT-jkou)R$kDIyuGd)TG8X%17S*)V#d@YN*4Z0+)~*A)jTE7urYedYLmW_4rM0 zd}RUIoAKHZ5ZiRy ziwLaE4%M%f9}gEg&rk}DoKJKNId(+LO`&D;Zn4%)n>TsK#GjE?{4%fo=ad7<8*t9|z#O;_DNZwG|N?U$m&*`Hzg2X6&8MKgw;lVwXGAR{`2l&k zvGLxuaf-Gblf{1RY=lMYYvIZ$mZjO|tgoLV;3CndvZ3V#r91c5sk`3s1aBq_wzKEu zosrM;hqMl_57@i5Kbl?ygcJ?sH(?{Djq@6AKN)GeET0e_Uar~FOlUkjE_>nHmLHe; z^iNLx-<@UF*(&CRrQFfB)rz>fx4f(ap_+Ru56a&B@pB`e6YR^Gt6S;}?jcxwc;dFd z#@?_Sm;bzaR(hiWb-ebN#Xo&x{y(<@f$O6Zt_KG^&QXHGhRm)z>`zv;zHBponfNg^ zX12CeNHg3s@W}(XEyy>g^!JdwX^A)DNUZqiBkxi0z){^lrUsG4mM6z;A;(@ROBqiU z{dL@%T&h4i>6AUkTdae9eV+zIdZ@H#GhbEbUXFomcRYFduzVXeCWuz?j1b(J9hMZ)ONByTIvVi{gSTE(MOX*pzfod zpI>!nrWq4=rmwtHm|Nb7_#4ITV;$Gk`qrb!XIB6AZVP^%$SpwB?w?Wi1d$$!7ML`+ zoa%pUW1~9Dp4xl&`O`1cPq@j$eaao$7mY2qJE$#o`QL8q=~NNMgC)pOxTelWDaL3yEj7iG{Is-^^aLtv%5=zTM*Vw#oU)#01#_KqPS@%;A1k?ER=@h-6Oee^bnW|R;1 z{kwk`&Bl(GMS6~JZEHCVCE{vYWv>svrkea+oDBL~Lw)`E;h)22wcQifw;qqpu2r8s zJej>&dD!7p7rQF|^KW4L(~m`S!t3>>W2LVezE^jDc=#vg_Lb1vIVHX{CDu1RZh>e$8SZ-1or!Gd&&ddcK!NJjad2Cgn8ss@fN#)pOa8t}CkL1+(fN$Iyi~-J7C0nMm|8mRw?Hax)*$ z_`%{9pMFO(*KXoFWNpd8uIH|uti8WXv&EDtdzQAGq3Cpbc)`ATFcY8gKtb}8)<1^d zcZZvtuQFUE4@Zi7!t=_ybAM{@#;DLr6-qm)Z+2xJy~@o!{50lT_tE}UmxuY5-vDLi zhrFX-<&WR$9Ju5j2aM+yDMhmsc7Hrb9QjYX^RL3jTYs@+p6gRVSBbiDgX-QM$C^91 zKciaDdb%MJ^#h>bAC#|mFQ-mlk3Z3W^_sMTb{0#@CG0+Uc%G&y)K4n+wqa2*jkMu! z&-|2Rw}kTNIs|S}Sc(?$9N9|ud)r;|`XEzVi?55##Z5iG_D8kM8fCnW1|-Nfv4bm; z`(fXNzjxIbK;Ri-t3 zG|tZGbEc=xmEk>q4J|v<*A`#$Hqep#d1j!iMDO!RU+eDAM~@5)eGH8r`}{OCvB~f; zGIMSMWSKkh-6+fsQWRQ{EGL3hF2?0 zQ}yFapL+bp@6>pAm~r@d)vN9iyJ$XM$JgviNhUp*_SAsWb2G{O*SILANq*Z!n!os5 z>sB*jaW{J8aYo^RI2!BsGgbx%Av`F&6FKOfsfTehCZ7%peCR~|2wTr2~J zo=7(n8^f9%;`c_~e$6+Pt=<8CFl zPu{cGG1uvzNAIgXevD{8{ar>Ia?j~{&daxZEDSyCVYT8WGkM6f)y%X17gkxQpqocdPjJ z8Us?q^VVa^Yf-V&;c3H`Ddr|E*D>Foh6H(8mviP6QACYB3vT45TNY>V*E`X~a+&je zT9&NuowqKWn*1@lEHL+6*CV6o)-<$i>T?0FjMaSZLLV0ZFjGr7oiSNsC?Pb1M_pKe>qBC6T?-~N*_7t`=@UzFi~ zd_j^8@iV1HFKg`Hol?mD1Tm`YKgE^|BKMv^d?iOU`?rYcVUyGbm4|;U>N2+$%m0FM zCW9298BM#n$LI6H|9wd<1uZQ}c2;4nGUMHxPOQ%P+cV#}XgKPMf0CfT%!Q=B^`zP0 zQ=zylKQDtRk?nmf_m&lN;k{lgXUVXMg*Z+pz9IKL2t3&E91|C8KuFO_OjoO;aTwPs*9^Wo!Xz%2y z<61Hidf(^qkaVa*LpN5W{(Yf)9Kxw67jO=$YtoaS&RP){|3rz6io&r0Z~eprOM z*}EsxC#_~0O^Ud)u9$M*X+nVb{{A&tuw9iZ{_Uc;}+Uec1eS&pnp zf*?h|PyDTmQ{(iBaviPfqO-&YqLP|HEN`NEO$*qm+f7C}oEzyjK7{sEjr$n6yZQcf z+n_t1v-7kMmle}ju#>Wc_8wOI*_8eog*{r&&aBd-J+fbs;<}K?u>G;b7?vQfl)?GP zb>D`$-7iOwA*nSH^PYb%&81&qmh-vbNf6 zZP{rf)D}NNe#-P_G#Wm2tSYxXU?WO?y%?g<_V%IA95@j;uL__3Ftq}m6mwYh{NgMI zpn$&tDbZ$r2(HR*6jnM6%Pg|mDfRt)ATw)n@!F}mA&9j@F|Sd4;2_)E>`pX*a?ISWe@!77g$62aDzxQC!J-I1m?BUjbV>5|cJPSM`;Ie%|- z7xky+y^0<9yo^~UzpUH=-{J`X~)`7ReO4OR>hm|oZ517;i7&tkz{0tT6SC^_WOviWhN zGHFo*NWs2PaxL_FON*0krqwr?;b$ehzTMF#vH$topNo}+4>ONee0_Wiw`e||xI7kr zdylX9fMTtf;_Y`Eo1m|I14n117suaYU$`v)(Qvz}UHf*I?AJZH=UQ`HY(aiBg5jbT zgHJv_*;t)fdq@3Ngg==zwO*ljb?S( zz7Ggsuse!d{EQbmyqYtU{zQMNtSluwtn=r1LP*|Dv*hnk1(QwNUJCPUm}Dl+%K*_< zLXPmf&^@Ydp$pD}hgYos<aCgd^{3YUOUm3W$*Xmt zfOWPyp;twNuk?ek*O%^6g2%agX}3Lb$!enYOw;wM+d>&aipe_-pYpKp260BQ+?Jk- zxp~FeT8e2UiozR;A}5M>{wQL|l|&U8zlbS{Y~RN*-!Agv`jVQ3N#(XQ9m-Ern)=aM z!F z94JjIG3-tdN*^Glkz}M5w$>6ZJLdm9SijkQtC*BAOQ!i_T;+0Qn;iI#t#j4==aORq zc^)8@&kdqn!U~@>1%l^a*Md=>ap^KUU7I+S>j_Hc%hwV>G1_!mejKhow&@qiOp5)^Uo%${()s&MpM0 zg!6Q{S(fMcy=5gTE{9q5g;n#Eg*?R4tw*JmTkcM&Mj5cPao2}EzjGztI1L_kOA}o3 z8as{4aXKRpUuUNktnB{QC1jxRNGnCXc?{q+!8t7+#M=BgY`8pL!S6)%o@XZQfI9wB zi!7JW8=HwV@^*92cJgY`H|rC{)9sR>D)d)%fKH6m5F_KXCQ2V8SKVk0QXkxu5FlrN zqL~ZjYB81=Mi%#y<&VDvV0yaNP&ilmTD#ACr(Ch>)xWqY(M2Uu z`JoYQnd=c3?Zq(diG1zJTJ5PG?dfUlHyhgbZM&=jv@^nobVEBdcD0q|-vn%P5hUJB z9SxNT$(9~-eUiJ+FELD$FT!uz8{UjtEY^AmYAAcET$Y!WZOf9oJ)X46b10`dKAf44cP5$q;;LmQRh(~>AI}e1bvo&k;61yY=<_QKv zd{&;`7QzftdP_O zQnOY?WzbHn;?x!bn=;NO=zmn7@v7T)UkP3T%yo0D~+L0Grny6MZL#Jhl&TO%5bVGjehSc5hy zNbR_AE<*UG>U$5Sv7c!}zAxbH7L?dUJ~ehL+dWX*OTq*GH<|(m{12CM^MG!=b^s74 z;=lhxtlTWq1C2pI0Pug7m4W|nki{540aOS4e}OFjSHiP10_0GNW7GH_2@jlvs0{b~ z9|?~Q%LaAK8=N>k9rFkT*9Jj-PuMDNs=uitr2hcs(akp?3r!Igv^WmXFptN@a&Pi6wS?C^AD5QZpAnN2;%ko z%OV*H6c3}MZvvZanJC3kilR(Vgc5Aov-3tJ0+#*0*dynUGlaVXKyQ-VYqaYjvQ*Uw za`cXsA@rqs5@L`fT)l7&$Ky!X2}T-iq*N z+BO5p3OB*&5J*fESM+Hs$1_kVbu3!ud~}p6ir*0#q9}i0!k%mi+|=_@P94Saj?Qf+ zZ)jw^<5E_fXXZ}mEaJk2UNNH$}EZ{r*wU><-g zoDHSU)y;YAW-!Sxd=lw_mF@3EAg!PjpxURmgBVQ?hwLoAIMiUJB{;`9o@PAG#e&bb zVrUZ1zkL}I9U*(lZO^?4=8CH=19K+r&fcjxpC zR5o9sBk1Pxj4Hn@6~(=g%!E2cvnkrL3SO*d5c_&z#hfX#S^c3p9}eoUy3&K+u=~6i zCQcECI$w1;_;+Soa1k0F{aX>u>Z+uK9%lk!uN%fVB;T@axR0>oqi@}&TQp}Uh!SAi z^3cIWtiQ5#bg;97NE#5X9)ShsMi!bXL&>*eTyrZznD+V5SQk|F*ay@`&4pq1NykBw z61&SdHt0)#&A*`2Va8{Fn#TC*|9qdWkbM4Q)}VBFmR2BCHIB@bidR*P2qf8m$H>@w zgyH`Vr=U0d@J-Pl8y2Z*gtczO1u1VYoL4LLN28qX?-MS`(AcXFzk6ba-&+5w`uO*h zk>*_}FZ#BJr84O`QZ6Ae_>0^F6-s|338!k9@D?S*C>FrKI6!H$IfZ2cK;;h66x+0l zEN-X>TI)~%VjRcv3lC+YQ%1P2D6w2OI3O{#cYsI$nB&AUYFKpm#xxVke1eCGcUYo) zE9dOuHt&U+TarC;;Z%}Vzkfwh3eb%i;#fgKGW^HMCA9!dvv{RYlL2x;+ZnoTB1{qx zigH9Mf=(=JA?B9kT5L+)@NJ;DFw(_|IMC~ekCI;*AS0awvczDKpL$T5vbYE_lH!-Y2kfX5Dj!lNXmC4GYNKJpj5v8F-l)&y3NEvYT@BIUD%e2@dnIKK zlI>95f(7cx#Yqkp>TATxr+&qg!g|a>EI#!DjVa8 z%uZ(ZCPpYJgD6p3VD4YY2=@N(NTfPGm1PUct_`G4)>Y1pYb#MdC~%beK?Z`OjFGhvl3u4b5vT za;sKRT8J6|jJ;YyJ8F&tnUEeCK>jKbzgT5T#FX!9MRj>Eph4%@*&G0s%d8(=QVxet)t77>U?_^opu!f#uk zF7opRpk>qe8W$aC9~gl;s=S54k3A%a_7}{H&&?}r7Yhj}PmnLdm4pvONeBa!ahMo& za_u&6^Jh1d-7TQH%8l_skva|#=|bU`4JHmvmTxM7Htjocy`fa;ge9T2w-gDpf($-% zJIr~Nl$+Z7G~tSS0vEQ1f$ro$N=+~XHHd_D060Nbup7=3X)l5z_tu3dm>*t?WYMZu z&SG|$hf<4goMBO9%6M~&Z^Uf_`a(#e!-91SwE{OUmwY~eZ4*eH&l4X@i{H-Vw;BP- z5YP}nlsYO_y~A7?7ZJC^04NrZXM(Wo$07myQ&a>FJ8q_}FyNyk;TsJN;DZgfZkk;P z9KrW@TIjLVVx#Ej1HdhoQ1JWmA~+juHJWlhOqR`>x+z4w5G#hq`mZgki)rCj@Ma!+ zK<@cOF8eBdt4SPah+hCw3yrm8BUK}3?_`DXC`(f7VeLw+`h)3A^a-)O8_ROjHKGuJ zD@$OBF?KA#|N48cB=Ql>#{-@4f7Hr*JGSI>mlNs~pO2sk4FKH^thq^46p9Bxm;h4Mqpg5o34a{Y_}>H5r7aX-@IywsG$ zFh($78W$0ay9R*i)R59d5*rjmf;mw7)XGyWEjLw{ivU*G1dTE9QVAeoo1ofEBoeD6 zfi(r=0GJ6RV+>ffL8$(~U1avYRA)L2C>7w2BiRDU5x@qq0BOt2YNqsYkRpdwR?0V; zZEl*N*er51Kmld(SVoo{!V+Sd%@hg_!+ZRV$xgbUleUD1TSC1rvNI91NjO|qXHNcV zP9ZaG&_zzM3=PCRJGUgKBuCUyH=f6OfMU2+dJu>Yodu(vKh$5gB_LG*Ar5BMB%uF)vvZAl(lQ(uEn| zGi1H#7%ibD0HBSfhLNRs2M{bL0W*+-o@Y2YVU67hP@Q2#UzGA6dS*CY-PfIX%FL{Q zB|^;rpiQ8rG1363@njZSp$x!mM&8bOiS_{QFTzQA^-aT|Z{ejlFu3Czzpg5{3d&vr zK?GpvKt#ywBSEqNcng@M9dauXrM@n~Qc?H*g1QTB04XAp|j` z0p@W#keLI^6?dxkT#&|kAO1isG#Nkwap$wdvkGoWxGq*oxNBhn%rYc)erULasZ72Z zl9_}T%aw9pRd|<%t4j=7Ghd*Kgqv*E7UM+d_)}2Sg1P{yEI1+(r9LD`v&{(?+)}M4 zA>qO&a{;6rQ=sBmf(wE0@x3^%3_#J6C;=Dxp45+-glS?#v{#V$Bjd(8C6lpS5Ec!- zOf?1L0Z+md!cvm6zcq%rr(R7`gR!a`y!yH~`@h=|X%-4Y99KqdGU;MO5CC>IYt)aQ zawn#2E{65UApjv0Ad?F9iBKBO0gKo`BOBmN%9@YR0L@k|tPRXv37}zeUAIp4NedA-Agr##I<@R9JO&TAjb^$++jBk^+4o`el5_34Y|t!@N!I5l&b5O)>I#O% z5m-y~ieqF3CrdfxzN+QRFnj4!yR2gxOjb?(vD44$TY;87$V#}#>>bbJ<|LrRo|(x{ zQGm>*UagRj#fSz7AQX4{fa99rU9B$m>XAsZy_i3Y21C?*IY^>=36FF}=>lR0rl{%C zOno=m+Q!fl7@mtp7lI|U9oID*)D?sRSV<>`VB&)Em>Im`zIX*UR8pWNY#*Jn-6;Ot z5=P{%z=tx#=)&IF2YI3a@P^n#j0|MXFwyii^Oki(CEOkWOXx>~+`&^uf>FAR`J?a` zAaLy=6CGC@*d4$c_au-AF7Zc7G>F7wFeEpoX`TrGSx}M{)PTGSkB3X27W=wuCE{T! zL^y~K;DmrbSVdnwiW8Zp79kc$ftgUd*xQyQKLhPc;4SP+>A( z!)rGzSK8}nF(7Rrj07Ql7nO}VoGDRPqU5=x-O^9JM=Rkk*ibnas><&8KDVXDINTtf zQ$jX9>>Zp^=bt&I6-81+_hED|eM#83NRd@K1}{0d{95!vQn%42Cay5QMZZd~#s_m3L}`{C?R56vCsnh(Q;T1wO=mvzJQwTQts>26=ub+Ci+FnbW^ zGo3CuvIvRO*&R~)=`J_@2KZn*<+i5ZU!=@{8i;3?At^)Ab2ERJ{94;tH|*la;X=)S zZ~J<$exL44y?pq(@9!B{h{Bs2NqxKt-U>wq(b@GQ*oWy$2eIyO-lS?XEwaZiBG!aXi&y5@5fPU++f?;)RL$PG`YK}$H>f2UD(1M~Ev=hkQ zN&BIvehfApI}u9~ty8W`L&uOVpqxf$8`rV(~XMBT%TUktWztBsl z{&K2oZkj^S!cuY}RiIUDO*hT^saQPt0!=_OwA?h*S8*yIV|_ORKMi8GgkE7uAJss^nTk*A@>g_`BlOM*ViWZBC!SNn6&p>H5^u@Hr|(YHdQHoJ zJn@{lSMywa_WpWL^IP@D(;A~p5)`6PWAgzvJ_D&;?@_eXo6~pyrLd(8fdk(BT(Qk^ z{k7^?QTJ=jrTz1-b@#E`zc;*>U4Os#KdAft;rZ|D&%Zz7VIt?75sYp(K?mQf^XgeTUtITm2U)#+{u7+|A%HWVj;-s6()DYQ#(OflKHSU?)GQS^7RLoqycd>^4@^A0 zh0C1ndsp-YHNZ7a4b=}LrGY%m2N3_6RA8Z=)-S}zZ}Dj&2Rfgmk`uG~E-nZ_-N!?vDps4d3OPbUWbP>);_flF> z&`dYZu&0>5qH;vU`dd7Lx=3ZXBP=w1j{AaZdk3^SiXBcgt-Ys-IkgPG94!Cf+>epHu~h#S^dU%K=ow5rGUSoaGS=+zdab= zhMt!<)sTH4D1o*>ah!@xC5%4*G>t^`7LG}Sz;bw$rUk#na}*OB=1mp;`gt+jr)(g+ z^a!u|XkWv`668R`P17W+&ipv%x?A_1J9^E0Pl~KKii)%}zsNRJh(b&QyRV5#LT%rs z9R%5X78f(UsZ^DkoJ$HkD?$BqTo;~=}dfIc;A=^uxu4Sf;~}DW!BL z+=ra8eW9~-0@6M)D&bw!bVNL@>b<8LCY$=9+5M=8d&34Ln#cr&j8gGazC&%xFru#^ zir%pLq0ppZ1_mV!l~yj z_Wd)gwayUKA7EJst<6C3pC4lj0~g0KeX5@s*4nZQ8J2KFyvjDLRiw$InU^iCe>zpm z9NU#b*Wk+|K&U9w$3E7%Ui{V8l30iJoHAYC|J4@%^S1syEy72Z;f}r$S66h<#>x>x zXV?4N&t9-vgl1=QXWSOnkq>%5|0NT>wC9ZLT3`FFs9cr7?&?k(WU-LCj5?`P_rLf3 zN!;*n?jQGVdj(AyFIF?6e?AxwuIv6UmR!1{=>bL`B;4^wYhu|97U)glU9=I#9P}o zNt>(OIFu(Y;lv@_))a-j`DVMFAa7xm$MbJostD`INIK#oSZ@$}h$6_}+>0UKvf>d9 z;i^+I=nMvRJCW3IX|6R?<;;QNb9uWcWGHRiEPXZgV0Eg$DcUk=PjNCKMQt&a_Rkii zHf+bL|t3Lwt`2X^QVB~f!PK&plT2zNkC|Jo8n1B?{f84pV!qTDdzLsZ+=_`TWZ4Fa-pF$=-r3uH#V#xUV&Yqpdbq>H}b zzyZ6QRS}C7NtiHlS+v?%=iqHI0JFnm1L!x#aVo`UWyb-iJ8#7a;-S&aCHb^wo3ean zM7yYyKPV-s80J@n1XbtvG&etY7Sg}6M%4tZ@tDfu=w8^}9}Y$Bu}QQZgg8e7WVYa< zN<1v=@Cg5_NfFh%9~rl|+i7>dkqVdq0eUu3yG(DSm|{3)HIUfd$BJNFZw*^mq~{%W z^2Z0mO(PCYVH+XbDNkYmk|Ttgg+H9i^4(ZVq@GSco|cy7E^d;7w$UK$zW0mq>uAw zDaoAUodY1psfUvh#$n9qY8iU1rEM$LuTzmml^P+DUpoz^DCWKF9|dU zvS|n-!9i?JlMxsoP+Yu>;fm&K-`ITh*Fb1*dCD7@4(5E26@Zj3j z_{58BCBB^WSY&2wjuA134hJZxB4H%hlVC`SC$c$!4)#W=bVaEaWBHQ;>3R&wbrp{D ztF{QM+;>h%*8ASz&;Zw2Pm--{!nFpn%_*Ue$-Z*}k)r{is45`e$}6~tIDVI7gA57y zod(;op>1^yF!G4*zh&ELlfMbGXG$+Ff`haD#3b?X0r)=;FuOxSBq3OF0`YPJp_`Su z;)E=?8FBEt)W+r-RUn}#FOVoq%dybwcQ#C~(D5Be1w&$>3vtRSJidsOB0>xRBnkkJ z4k%O?Rl=_U&;&p=F;Ff5Pz4Fm+Xt)YrcnaJ$<2_p6G(&%n=NDdecnWWUxb)<8Ji~Z z`kf&67$OXn_o_1XQWRUuq}9$1fZzlF<3qwQfUgzl7~7-`DSI*Amr!)!ZP}b?IMfDJ zVzWuIJcf|$%5%6@1*o`J;>;?!EHfa0QY;1wsClNQQBm;aZ`2fH{ zvf@TiU9erD?}*Pv62V3O)lsRSO?hh>dt=oia7wVDV0pfKqU$8WG{&n&v{qjoFk<3v z)#jzk|9xcuUpZC=%OkaXM;qD))Kmv677{n#1t6}R2P{KA%L+@tL>w1 z%Be_p0;(iXIkzgnHVw|{R#1h89FRvy+&jU!QtLY~uwG`>j ztCNjEVt@HQV6;~|$R!6R03Rd8qCDx1pUU&v{}J=9j`Q2ncqu&9BxYH0A?T1iX(K61 zvUhz{WwQyys+MM7mgr(jiGs)qqw4%g1P7or@^@8q)pPi4(ltz3Y&05#ekvf7n%Bev z=tgQ1>`Nyq-e^3}2i*9P^J~Y5noRJ=Rj_hnFTv493Uo2Z3ISd%gbk2<<4xa@a&+wX z+g=5QT(x6lp%T8OSRz=42$pp({i^l~vI#W9kQ_(fZ2p!zv9E3h<7q<3OjBkRIly|Z z6V$Twyef?0I65B-mf@!MX($5EwbF^!fr=vDt0Nr&Kz$)3oilY#L#VHj#}2b^KIVV5 z_ntvbhikiUItlepuVUy(6A=&)Frfwn1XQFOdPlk-pb5SAPC$CEg3^&rC?Y5-T|k-? z1wlnb*}QALYuO!9PMgj}@ReLG)*Pv2Nd6_8M2^RZB=`*~xaD#kWn&?c+4lPf&_A#lkNlo?R-H?YwW1NtIT z4FiWMQ`kReax9JmVkI#IbS_^`gP(A3a&~8o_Ic2vX|+0cy(&I&8y-aCYzd;d9^<2xmJ2Es+@`f z6qL^lhMXTb0xf68^d6`Y(OQ^~kd1^s3|j@u-iBxcfiRES$9_;OB^>J!2m@g14RMs^ zFy&=yI0mM!Un*Y!yLl64-!M$y+cS_0q$R6qwQ zUBq&k&c{Pt8;COS%MJJ)&uACCEQy6SZbGma*for|xDiMi1XD|b4qt=F_kj2spei0q z+(}UNbD>_yk;8vg;dctKWZ$R@Q}KZ6TSVKSi%b+(uIZQlyx=&I=fO6SDLnz8mbgx8 z;w1{8V)+izXsDbbNXnxd_)-T0L9gq>6g;w6lb}3ouxT7r1p|}P|D+NNl?EIn&wm}G zKyv6$(&$J5%Y=ywOA=tXiR4gNOdazz5OqUso)f|(siLqF8t}MFl5kFTqlIV7g6=M6 zhgHF^;T`Z5aOp_UJSD;sRL|@Tq(tZNNy6DR~Z^iuovyFafKZ^AJ z=VHK@T@nF$s-3UI$vE!}msV>r-!d&2SDzKDG`>kUu>3J%4C z%JWzCC%)NljA{gxT)qanj*muNSrRjbT?eHuDZv+AmVgJ7?}rclg4QUAZ8pHR_m--Gb5TE` z2B2oT17rR2vX{9ZtKwDg2y=;$&67t+V%zPrp3J4!nFCrj8Coz;SvZ~mg6;2YD8jV~ zo!y*!c2OYKhJAY=aAhvjap;{~!@)5npe#qjj~t{5CEhOM0wf2=!og6~G8><5Py7`K zih&&(x~!SRqn^Mo&p^orgVEa(RS0k4yhhCg_S|&R8zEbYpz-R8 zv{OPaB&CN*91dgCH@q6lzy;T)U5g5ROQ6@bl^FCQFb?PFH`B5e3{V@E*b`F14sP?) zUzL_yXKM-->aDsK%f(gb9hS{`t56LVfFEWOyE(kD>?2LS-{Rpi3cp0)O#n-Xc=0X| z#@q~1PWR}mNOl>nZM1u>ob9!IHktu1h$E(aiilgh=Kbh`@S{5Y%eO@m4ZfKTd9zEh z-3+TrKZ?4b-z=%AvL{I`ZH3G>Ln=zk{IRw5c5Q@Sbe138|yVa_TlSr zl?2|6n!^Xw4}~-c_k~^wK~{<{`Sq+s{183|e1MS^A`~b29rafvjXqVt!3Z*hYMR#z zTNitB62+HjpXxc3Nc}9HaRCYFq9FdfXgQLP_NzK#gh*AxAd+_(1SM7JfnaRBG>~r^ z#}x!FkF7eTF0VNrVQ39`l>k0|>lVk=J=Ys2ovzl?p1f_iTF;7FvmO3S3EUP%bfO_K zmmV?2*3;dv)~~Hc2%kY3*9pr`1R3lnSU+TmS_>pV+sr#j^b<`>m3cXUj7pq~P1d+$ z*JL1&flOK{=cY&^mgVwZ_$J3Ee~F;T@wdH~>bw-C2(J5%FQ45u=F{@w;nb_bz&igj~UNq3;qn zdk7FJxtFU0 z8@`XC=Jf;4ZtA$NznhIo3t_W~K?mVXtIPY~mu@7ta%~Cc5B@z7d{GsyOHj@Z%~aU% z0U%Vu0OPW-;Y(gn=cOQWLK^`kMNpIkI4TGw!87yf77s%qNZy9!@3s@_hSoeGv{}5) zYPQY`zldtCK1+eNVl|O)yhEvW)wrug+quPRLGFBmNXB45Eoly5ge0y z#1qR#LB>+?;y&dGibXZ(s){LhHGS@o2SZK@93_P>350MvhKZFm=&6d&!BArJwPc=1 zu}9%Nv6s_3^DphitDP4r_wOPhrQ-s2l=52>NWXrji#lKL$HQ3IOG zh2y8R=M5ifQ?ZU*7`99P!?VpU8Y}(9Ae;VSWlA8)wGbUa>#s?N@1=C=OmjVA!{Iax zWh2sT1;hhJY9%o!6AYuE(l0Ij2*b2uCKRjy1Q$;>Os}@J6SWAaya4%@-c({I?iO98 zxn!8xv1WHU*aT`4MZu+u38gC|a(2DG>6qMiUc=+8r2FI+FrX(A%8L%af^_e*v!dxHU&L>iD8kvK_WmgSA>dQGJkB=8n1P?XP6Dy$p(% zjt%KUN)STHCEj?O1_-rw2WS1An~#U&OTe8G;WlmciC`^cOCgTLGwFKNJ{QfvJl<8#qr2%FuQ6 zASIvWT$ItfcU$h&L$h%bmFjWt6TUb1b;1et#I?IDX9nVsPCVw}Y^jKZF>iwOPuy?4 zDy3D!6jjV%SyURX64nZ4Qh1Rq|2#|gc3$nBvGek`VG})x8f}N@H!3m6k`b{&#<}fI zHLy8hkS9R+)_``Bg$Y>5zSSrbJjyBx16Q%-GC@vD@f3$H*v$AtmCFTiz4T;7iSQT- ziBXh9?>#hf62f%inn{w|?qKV6i@vA;0`IG%B~bA%Gi*#k&Yg*L^n`N7agvsmGc|Qq z$Q!l3&w8)DRk0%|*D3+nbZd_^QvbWNg)Yz>kD)T@S#0T!Sib^$1vP0d=tySr^cl8L$l89QE zz&_1%=C`?ZHc0|W`V~cni(%|zhOksQHH@;P>Pr3#&c~^U7ZhIFccX6a?=7VY8uj$j zHe=RvOX~!03$xQa^ALx{CFspXTyXNEj3^U$_MT#Tt+iH~e$p^8xkmnw3E>J4W5R6V zfj(qS!PBo<;28Y2ml}D~ z-#lgbURACHw=oV}*tGoFu^Id|`0Oljnd1I&ztH1@_$NVYLic}-39TDtp9O7c-amO; z{`h7pE#uQdOu?z22#+IIgO+psKv9dgD|TL#>A8Qd8;dP*GG<(@|45 zSJQA+(+pC>#i?nL)U<2VZg#5ajH~G$73=P*-8xm%r&c%MQa2P+zpbc#M@Rjxxw?_7 z`n@1^<2ZE_lDcV)x>={X`MA2pg1Y6d`u$UNJhg@umxi^NhK-_zt&WDBxrV)~hC`5s zW1NN)NyE8D!=+QhbzH-3LBoAl2&4)pn{&AWC zB+W-Pnt`2~LF1ai3!0C2HJ_Yn5~y)RE?kHhE>sa0rh^MN$3?i}B7<;Iakyv_E~e&x zZuY%Rx}LbJB)J@xT%vy2zc;nBbevDM2{*z9gJvqK3s?6(4=fdo9tjcE+MQRpHyT3X z*SajHbs2zOX;8MIf0?=vk~kAmq|={=icX#$(N596P%@fQ0KM87qJNC64jWDE8L3|z z6;K?}T!v-(gr+rz5*xI0*rKFC(F(Y}S|(Hj7gDa`W{NQ^DGMed9hO`$Dvi^UT@KUq zAH%XCJDF6fNTCD#BPHmlD=`!EYz?M_M%_pn&zsV^%r=6X(NUWmshjQ< z6-JG4AvGk!lBaLhFT^B^!3JP`L*_Tn_0vB7C`FHv@i^T)A6TP0sIN|EL_SL=WF|C` zEp*mjTY6H5mnj^hpqp|!Ue5J$o=K{@W^6!gB&%6(n_qRG`qq`W(bxQB6y?Z0?Gb+| zRY}Jfi=wz)>E7Au(okM4CNZsVyQRMddXg2j)V=%5MCrea=|h$DVY>Qo3w?x}K1HxT zGF~5*r%zd{Pt~PQ{Ysx^QJ;2CpYD%7Kx06E!GJ-03Q}8dKnY`vFZpYr)IBZ=afRv` zzhbVPGNt?%dGH!B?9}zCP9m*l?dd@f%!z$aqt+H_OfXuged>YNW*;2RI;eWWjIM!1C@g)ID z#eL3pUg?iw6tx_0ZTvL+-x#MZOXF=#o39I0W4U$Pn?Jv;xLsYggTqI?YoJMq((QP$ zH{a^Lv-GN?Hz$j4KGo}L|GqQRbRG@q>iGF}xj**o z;;rt^|8Q~KqAuayC;YGpb{*nvpzW2XTt$vE1Q4Me4 zT&QPk{Apdoso`1jS=kW~KO|EGrN`Dv6n}=}YP4`!#cGV0wvaXUvLKU9tRzaZCr)<4 zcryO$EYeyXg2WI)uWVF&ic+C<0>IkJ(mkm>D$xchhF@@)bYa?_HLNMO{A8+8*M}3%L#_uTMMOv! z5xAW$on$6muPoTZ|9a|P;OPKIdh0SjisgHEwzRGF0J5LZ?Lh0tEKPcC{j`iCbo&}yF5#)%C3pC zeO$Gd{+>ldznrsDYq2m=&`hLaX(&Ljee;dkZu-#|Q}b>Op;lEmxPf${t50D~%#fp-xdyC0S|Db#-I?0hZ@0GGyT^C`%2Hg+!4d%)zTmdTV^o8FrfxYv4Fb=IluxU8zWO}Xyu(qsodBHm!0 zf#+MPsrZ<$%t#i8==ux&{B5~eJz!R zsbyd03%GwES3tD__khpN!i{;oVDkiZZ~KuIjZ4Lzy%qj*G=1;E zpSD-?Iqh-Mi2>KQXB~zgX*gXxa&N!izi9dK;Wzik16%jN*U*^;u`)3`6;y!nq=Ru;!ZP(WK z%(lKQz{1x{^VzjbKE*7TDys|7nQA`>q<)9$$B8u)X&yjpnEsAZq;@zZnXBV1byzu?Imnkx3tnx>u1)e}- z)26dRpDz6j#qC4%Tq~L?nSHZY+n~4Jj28Tjlon6r*2&euf;E?gol^Q|H;A;AwfU-~ z!;$Lf>WJe%NlQE%IK8`RQ?h zL`F=sjceM}o8uw5i35>CwN=?ya=Z_l4<76byuX8OFWHX30& zDb*&gmoReeyR^1^a7w`#){NoyOf$8&VTEG5^I4VASQUr=nPzQpChA?YN>iy!>ODbP z>aN!J$tP6VH#ahwqLC{uhAFRiXjnJ8cgighv8L4n=HA9XZ`<*vr1*T|7?;)>4xO9o zLWQ&L$PV6o_C<;)Ja*{OjXL)zvpSWL3g}(hI#?gy^g4#UE6P^5g6*%f)ko0AtoUMv zmZlTTbT=(s_GXU^XyC>%DYQXS4=xtUPKi)zh!*BcVlt4Z!+O1H&Yco zSYl&&i%-7g33FrA_3TPDUr7@3jzNXlN$OQoy5#M>Xy{$Bjk>Fsdq4bsn(#9}odEjJ zlq&e2hEgi<(|?R2(0`30j3VabhW~XGAvYEeC!+tAQvII|rA=k8a?cH={~Sdyp_CW2 zoBuJ2i2N_O_485W9JgNmp&s8K^Iy32vGbJb{|lpt<*WARJ0BWs#`6CkjUwE}7?rQo zU7=^Jpl<75p_{R(noqfTejV)bbSK{UcpUhH<|~EqgYL)JGp+zljMK><&8NiuJl3ay zhov8uae;9|Pl-qo^2e~>;EKsmm?t^;B3R9uBLd2tRrusYcLp83o9(~)h^;+}GY%5{ z@P4rDd;gULQHiycSjj4Kk(($>fORBR5Lpm`_+G)6pz^~ApCW5$vYxuib8L|!V3}=| z*-aB*lWY!Q|CEG(T)C09AFlN@U7FjeEW^xtgfGi+Nl-BTzMx}yXh793>r562xlg%a zKeDW|Z^)YDN4t(@&F3d7v{j_gJwrMb;t;0u`FSZ7))lm?nPmw+Ugas}Rgcqbs)e=A zD64T16V5xets7O+~sOWY+9x@F3%KvT$}nrL@qSnaxU3zeDv$H zaDi=iwnN}F1BY|@g)(QcBBqQTzR=%o4(|SIe}6J$&l}Cv#_E;L35WjBFx`9FUt}Vj zi(@hWT8g|G=-eH`d(*HbS|#9eua42p`a~74){0i<>!z!HpK3=gReetBEJ@}r4>%}m z%#U6#D2fOY8ZjN(+AWreITaaS(F+oJeR;EUZ%nAgtZ|ItD%Tf{E2$i|oq~r*iKldK zF+~x84av8km%?H<3;MOAv0wPi%y2v_0ppxep{M1ELzuGMcJQ!t(2!zQ_n^Os-_RoKJh}7R%vHm2H zSqx=1usjx>ZOxCYCtW~>(2VP>BP*I_DD>(kxQ*r( zy@KJgKm5^+<8ztY>MpPS+C}F=m1YexVR^pCb1TRz%_g?$=k-^|d){p~i&p+tsPVm# zhy}e}23_$ctNx((Zq2oMTjTd~FueY1v*;Y};%3Xbn^clC10Ms{rVXx9nN~Xg@uGY{ zOKWXP`Vq&+gw^nCO-}V0PTrrmUPei&SB?jRM4wIzKfk(|ED@#%vwkd_nQVHgDd|{SwP+#FarzV*MU7-0k}&*MRi zT5EDi8BhbQbu})pH)Ko@wG_Y`g`7sD$M|G{(;2Cok+d9+EW$!LDCvsAaE6?%!U%@6 ziX!M0kM5m1PC898TrA5ek>ei!-#7m`DxIp@cimXD>Fehw{X* z1UB8tO0moM$aA0t}=j z^at0&S8|&*hZBlFcMo4FM{jhl?H#?<*A)LgednIr_jkq)wcp>H zKg^q=f)FKrsVTzozzl8b-uH9s{-OcWo=&ZA!N<(WZvojG-!iYbb5?sz$mcbU916Ne z)2i(|W+AsgVv|Vv=*c+oaMXiBLLQqy$4&n!)8u0+7Lx5MA# z&3abaz)zHWZohZyT-<#&`@H(B7P>MgqWMyn@9BR*EM5p%wx{C0j&k;RriKz2kkH=` zle}{d*Q1+O5)vKPZ$C`E_)cDB{tPo2Bg5Y}QV&hu3!+)|4w3yyl|1=}GcnabfAuCB z_opQCT3_V@`Wwh)j5E;oEgNUNNer9Web_zaxr(f>&fkw{DPq4fc@2?0fZd0_R7CM3 zQp~X5CFm8xEA~CJ84EFAdQgw1zC0;oT-uf{Z|_HaiBzOsr91OR4)FfikCL833w4*L zFxL3sU>;zf{HLz!-crh6`p+;98EW}Rp=QeR$Q9=QyXYl&+|Y@4VeE? z&H@B9)I$9hR#^^P2Luf15~)^-Cl~yaM35opegV&KpMk^+%0 z#;7=`^jR~13( zTC*eu_lmwst(NLNhZcTntn|~9*PnvtBhS4`29_kkxt}?VuMye45)NdLDu_Hw!zc&k z6vo*=K`a>)EueUiaw*8+V#_t)Kt34`@!ZIj>pgoJUIThc!hJ=i;^m@LOv&8m}-0bQ@O5p(B+p0Q%ORIFA| ziy{TbGH`?zZoMRA-u^Z>qu{Y;f4Lr2-|Z0UcV$-w%XOhK0F&fh9ufG~P5G7SeEH@| zM5S|jb34G|k&eC}?qonwbeEADs8qDfvweAnc=9f->wDi!_c|_h^Smn3} z8q%%v${btRQ25z48DHm9esgh+_b2*;1=5S6CmQVW&}Ft1=RYe54KvTAqNayncYC5) z9Np-8+i(G~*6>^uI0wo@!0;adjE}F`o!M!fhnbd&T6?>xRvxHq&*Q?V<(%mvqUfSl zDfGBn?csV+AruT&Ywk^GdOe&!99WI;&t#n6tHa_>lfYY<=o6(zl~yzBNF9GP1n(K^ z;+jNxKZ{2~V=|bTx9}@(U)W`PNg;^_T?oO7*M!YMBdI@H=&A}Dv0oQkX%2wLvFH!P zH;>_7mzn7>6cEofe^kn%;vVsg*Pe__40Z*eY$ZMCX?_-_ZyOo5Y&^cp5}C8Ez?oOb zy`?l<08-_a-sB2T8$a%AC+GbTTks$xSxSgS3DCi%Fzz@ttEnAlMZk!Rh>8YGQEO-@ zQf2yY05XsD8QhnWKLJ@t#7Kyz@VLyNq07&)Sz=DHTp&6ss{tejZw&-Ck3KfJDkNo< zBz&&c-q=v%kUOv*Wm=nFu*YC$KrZ1-g}nlnFEDJAZtmXJ{j4VRLX>EJBAy5e5k?BQ z`tMoN4KRRS!pb+B+=yg0p82(h1Q*vndMewe^vT~sS3DksC$Y8VG(f>6;s9hTvkL)! zm?_}pt~Y>1%b&fFaY8`Act#inWu=FN{HtgUDiCe5ASUTb@&j6-cC-Xii}yu6~`Ql;FiWVHn@(<&s) z*Y1c8&!JiWKIl%aOX8s^*`WX{VHutR%C0$UBtjsTHhJ+IS08jiiJ(4*2WDBrkRNFp zz)S~%KVIZ8vTro1|0N~mU!I4Oe-b`$20`ga<@H$F~{ZLmPk6i<<^`Kg10lUl)lxIZwH6Rn@i%f=| z*tu2^I0%s__!Bcx6bTSlv2KGWF2g8#e zsFxF{l2PzTz^(#-N%Bf;0_S`dUp7k#7KdI2bz=?(< z4Ho)Azz?1rNs@%9Y^Gyyol1(fz2EixY=y zn#5nn2|A47@6>f)45ON|QY3z*nA)UB%BM=3q{;@Q%AHk8U;GR(x24L<15qykuwmS% z!USe|OBH0G@~>2#&$3cZfMo@JpfSClEd62z6w$zq#K~|+`U+^+FnA`@SS9ghBz-`- zvPJv8`GG&!y8ffplXfVXb;@mf3&A%dyA=+CdHS9s7eYWtW_e(x1yEtbgDZ)#1;|Gr zZ#aESY?hOVV3;X7ENb5oksLW&25hAR>cgl=Oqj7CAV+Y0@XhgKt37f*5PIbzaX(G% zs#PYSE}v-+C4zn$`Xychj>?s|j1nBlfU|HElt&EL@{m;EgrG;7X?a6d5E%{uiy65s z8-j2v&>LB#=x1LO464K-u>~h%u;%Z0){{6JZtj+Z63@O*dtmXwF048mNJA&^li^SR z9r@ECpCSh_q)1S6--+=g| zwQMUk^14Q>cz#$kJ7iE6qPj;4qi_Ig1MlP}oW>w|(a#1#U*tQ4f*l74Kg>cHfch(B z+GgUwFI@Tb{XdmvLa(5B&oX_vP#Nn&)-8^1P%DK0D2ijQ494@6&eKj#5+B-Qv-;j~ zEaSY`%f@qMMyhb-DaW)fT=!V+9m3GGeriUw}a z#$Df{_>gWPAW{ZzRB|aR(&B^i43)y@)5p*M1NgbafEd)3!pGKLNUIpNOThNAtxZx1 ztN`8{T1;yc`Bp2K{yQQ02#EX{8IvPJa<1+Bby0TBA2R7Ry#bj`r%0c?e#GDaJ$J`C z)31X)O?xbUrc_dR63yfR+0jgj6@*lCWMa0EWxI6{E6MfnKB6a<9hG=(3&1;gGP9Mz zrm_oWs`fVEhG#!QAz-jigFmz}H|Q{({=gy~^OV6V56x369Fg*S1U2hqDdA#%ogL5b za&NJ72IHZ!;m_?_c49Is04+bGPXiS|Bs=WOx1-r{4{ z;uqNBpWE`Nqa|p*<=nbY;A{=K-Wq1s8WGqUmD?KA(Hb}3n((_diL))`dRv-VTSj18 zR&HBPM;mFrE&q300cU&B_4X38_OihCirn_9j`o`Q_PXEg4V)d%u6Hz>b-W1dXwB_t z@95~9@96&B(Zku`&GoKnv#$4nT{F2|a~)j^ z^IhkS7RzUx-K*ET*UY*%0=u_zySF?3Rcyie^4s;7M`ka71in1ZeR?-`_7m zTs;uQ9+-I#BB%#R>Y?oHpH> ze)B%Tpgv(zpGaq)*g~JgX`dulzm#IXw0Xa5P`@0hU!k*KaiL%Nv>(GYprSaSWNTUl_DJ9mI1DSt}0Nnh)6r4LOpA zoI8hH7lzzVhdj83y%dLi%!mDghW$yyk2;5g7KR_64imV@A&TTMb8Y5H`Dj_tXa#Ass&lkvVYKda zwBeiwpg7iSKK3GLtd%s@P9jtPc+y>y2s;>q9z5yi8t*Y5CkKrWi;a)Dj*ryTPK=LF zD!v-kdG-FB9=$OBTI|)l`KyJRa+@eAQxIa!H8gt%jz^aZ-=0`M?Yi}T;zO?A#%bH3 z4)1rx_H9yc{&4$g4(GFnuNPwBAaQ9~VZ;_`;$=9z?GY1{M*8Q%IiKK?y)e7B4;kVem_F@cB*dS2wIpRC%b+CQtJObq?nSCwT z4?{E6@cXUjzU`anhu=?a#m7+WN^e_4AKX()f7C;<**InDF=YutIk~;Pwm98#9pSBn z0LqcKG$^nmu~cA0)zJHDs!7R3(h4QL^$WMe24qUfOl9mO0{ot__5+2%^J`tx_jNm- z_Ry=on$@YDtr!zg+yi81CV^KI6^mx2du9!_=5R^J{&y3N!B_uqad+&s1#0*H321NH zn|~SHW{DX`dM#v8wTSRQM{C=Djeij8Ux=Q6bBpEi#2&*h%66zep|pc*X;o=y&0=XI zcxfwdX}fD_cX8>!okaK4N7)9^4wX#J6>=oz#Uyk|O%5EPi|T&4S#aI28VK!M#wTlr{d`S)f@>8+m*|=Xh(WEByb=S@7@V z{NL!3sN_dfH`uIOJ);tpwH3Ng3{2y%X4MOpG}5EeeWB7R6A#0Cbf`bbol&pkb^JOk zGGP;Y?AbQ9vqh0BS)u%Q=NE;+S__@ojk7ZlLU@?~V{u$2A_Xg!L#So9mO}vz;gv8Z zBgd6+Hv5W|h)`5dp(8$soE^yq?&9N8L2 z{Mlgh{ak8)O6iC?DjMD-Pz*wU!mOdvac9lUJ>9WS6fAmTAW<7oo<)}Q=Mv@!kVMKNs8EH z$FIlyHuX$0BZJzkbOU7a7(z0#miEuKX-tmT8j)LG=&sh7_6Pu??*h9KGGXy(hDk81 zZ4b|4%Eo~^m#W5#hzA#^zZh^2DCN$0Ch$iWf4Oot{Q~PPn!YL!UgHM5t%_&I%!^Ho zPd;_34;%a_5->ij>PM$0Z|^&WndCj@4%@|;-{=3KI=>Xw>Vvy_=Xq1^MK#QetS7_z zYg%?oxm|Xb`N`^vOKOB7OP{r4qS%n@WU~u%G5E<|eL{we3{6jI(J_p!9^hfBs4 zQaj-KIp+2w^gRw2%iS9v#Husf*c$aYIr$2XT%%#T*~VL$d7$@g8x-T${LIg}H+S|M6n(pX6cX zlY>M8bqSh6i>$)&Eh$Q;gqL-MtfBBNIVrA$Ur1|2+w@yX)_95FwUH6MN8eHls7r-4 zwMGqdzNJ;@l#1LN88vGEmfjFoD(0YdUeEI_qjkJg;^D}c<;l0q9_lj5NUd>ej>9an zPMK8Z$hf`2VfJKPnRL0U%s(D4SN=H?{`&FB;ZqPz1%^UR|P#8;>Z zX-~zN9{o>z2)OovpMPhP_5h7)4GX0%3n7`7U(Hda9~JHeTyDJ+Uc-yix)mju*6304 zjw)Ig)p|R0GL299`Db05iaRh%D+%z7%FQ%|JM4zOl@hKZMly{5?PmVJEF6}5zzC`$ z_6i{@kPhvIFFgN#;qaf^iDk_B;T-lXAuGa7+6&wNb@+e7AO5!w|8KVw|MoNm=>PMF z|Nj^G?{-K3_G7RG{bPA=1YrR~&dJlvafy{ AudioComponentDescription { - var description = AudioComponentDescription() - description.componentType = kAudioUnitType_Output - description.componentSubType = kAudioUnitSubType_RemoteIO - description.componentManufacturer = kAudioUnitManufacturer_Apple - description.componentFlags = 0 - description.componentFlagsMask = 0 - return description - } - - private func configureMicrophoneForInput() throws { - guard let microphoneUnit = self.microphoneUnit else { - throw AudioStreamError.failedToFindMicrophoneUnit - } - - var oneFlag: UInt32 = 1 - - let status = AudioUnitSetProperty( - microphoneUnit, - kAudioOutputUnitProperty_EnableIO, - kAudioUnitScope_Input, - self.bus1, - &oneFlag, - UInt32(MemoryLayout.size) - ) - if status != noErr { - throw AudioStreamError.failedToConfigure - } - } - - private func setFormatForMicrophone() throws { - guard let microphoneUnit = self.microphoneUnit else { - throw AudioStreamError.failedToFindMicrophoneUnit - } - - /* - Configure Audio format to match initial message sent - over bidirectional stream. Config and below must match. - */ - var asbd = AudioStreamBasicDescription() - asbd.mSampleRate = Double(Constants.sampleRate) - asbd.mFormatID = kAudioFormatLinearPCM - asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked - asbd.mBytesPerPacket = 2 - asbd.mFramesPerPacket = 1 - asbd.mBytesPerFrame = 2 - asbd.mChannelsPerFrame = 1 - asbd.mBitsPerChannel = 16 - let status = AudioUnitSetProperty( - microphoneUnit, - kAudioUnitProperty_StreamFormat, - kAudioUnitScope_Output, - self.bus1, - &asbd, - UInt32(MemoryLayout.size) - ) - if status != noErr { - throw AudioStreamError.failedToConfigure - } - } - - private func setCallback() throws { - guard let microphoneUnit = self.microphoneUnit else { - throw AudioStreamError.failedToFindMicrophoneUnit - } - - var callbackStruct = AURenderCallbackStruct() - callbackStruct.inputProc = recordingCallback - callbackStruct.inputProcRefCon = nil - let status = AudioUnitSetProperty( - microphoneUnit, - kAudioOutputUnitProperty_SetInputCallback, - kAudioUnitScope_Global, - self.bus1, - &callbackStruct, - UInt32(MemoryLayout.size) - ) - if status != noErr { - throw AudioStreamError.failedToConfigure - } - } -} - -func recordingCallback( - inRefCon: UnsafeMutableRawPointer, - ioActionFlags: UnsafeMutablePointer, - inTimeStamp: UnsafePointer, - inBusNumber: UInt32, - inNumberFrames: UInt32, - ioData: UnsafeMutablePointer? -) -> OSStatus { - var status = noErr - - let channelCount: UInt32 = 1 - - var bufferList = AudioBufferList() - bufferList.mNumberBuffers = channelCount - let buffers = UnsafeMutableBufferPointer( - start: &bufferList.mBuffers, - count: Int(bufferList.mNumberBuffers) - ) - buffers[0].mNumberChannels = 1 - buffers[0].mDataByteSize = inNumberFrames * 2 - buffers[0].mData = nil - - // get the recorded samples - guard let remoteIOUnit = AudioStreamManager.shared.microphoneUnit else { fatalError() } - status = AudioUnitRender( - remoteIOUnit, - ioActionFlags, - inTimeStamp, - inBusNumber, - inNumberFrames, - UnsafeMutablePointer(&bufferList) - ) - if status != noErr { - return status - } - - guard let bytes = buffers[0].mData else { - fatalError("Unable to find pointer to the buffer audio data") - } - - let data = Data(bytes: bytes, count: Int(buffers[0].mDataByteSize)) - DispatchQueue.main.async { - AudioStreamManager.shared.delegate?.processAudio(data) - } - - return noErr -} diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/1024.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/1024.png deleted file mode 100644 index efe4c7884e514a1476d66b256bdb021cda1565b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 78397 zcmeEv2_Tev+xKnXvXmwJlqF;?Oy~}l%YxXXew<*f)f>ib!7}2iX4ZMDNFaFD0jwYdy_HZbRcFc$A z(Ij!dH>DWb%t#CFMnfe6QE#TA*-S+#hfv^|bX32*ez^?%prWRsrK4wHWMXCkC**QK z)KoMy)U-5obZd{H@&eyOw43R+Y~QU-&uMm+LF5XT!tICzM$!G5CEQ2q@nVYSuKF@D z^K9ki;}_o{At|*}Y0q9|6;-tZ2X%Dy^bHOjJ$C$rxy4CKoAVcJ?d%;cUc2sk!_D2} zrr({r{sDnO!I4o99!AGJdK{aW^gQ`RO6tp3S=l+cdHL_&7nGKjS5#Jgs;+5hY-(<4 zZENr79~d0^K0GoyHcptGn_pO5A}+72^$U#WU%CZ;{-tL>^lLNd7d0&{4K2f3zo@9) z);hkKmTvoQ`YqaK3}>%!iYVM>OYKz5OC5-ZY?PcYuV zWRn90;vixAJEJoRcHw(cBGI%&zswlz(pLO#I<5qjukxP+|`MW{2) zCybD|X$De4(DpMN&YVS0V0dppzo9{h{++QEo=5l!w(i3BR~y*Qce&Xuq%1m-V-Xgl z8bz-p-#)SEfuG91Te!Xdo%%g#mQxW?bLtJB%Bz)T-^Hj^ z63&b3b@`<*MON0>}@5?6LH1ZBRGqe|FAbMW<=MfpJ(9h)*h-Y#&w!RJLNuI{urK=RGn-~7e z9b!;(bAv7p43Mi1Kpci`Q?s-zG+$Rd8JwptpL71*k zAhhOQV3!zz#t&YH%S};u!|>&0LOvk06bdAeG1p(!IGcnrsqx7|f4hZ<_yECl3a+aQ z#o;5H(Zp^Pfs28(Wf1Z9bXv3LUQ(dS2@>%Ncnr8MUMBp%&^{JXG6QZvlAS1CMIsS9 z0lmr|r9h$e6lk*+Tq1`a8L8|g8eGJZoN;vlpk*PTU(_3Y+L)&e+WRTLZ}8KGjM$Ln z8|%~W=>!`xVnar3$cX=qGD73cfAo618%q+L1ExdPyMnNurK5zqk@_W`6X8r3Yx$#9 z_@p^e4Y)ch7oga0gM4m`groxVVWcsg`n%pr2~tCW@^O>5R-Nth`DZ562<*CqoD+|f zca6&NCdxRcVr&r_ewKzo!B*AlVZ`JeuA^ zG_P3Uio5Y7ee9aRtxh+{FNn+bpsTo8u#K4W-H7Igff@5^zzjhFd&}A!VOGTVnh{SD z5}a}wDFkTbZ!Rjud0I;Or?7do{nipJ5|wv`H_^*&?Et^cD9d1|n5}r@srU9|_$rU3ZPE>&@Bxg%9<(Vd%=C(pS3=@8 zHktlgo43l>T~hRirb|`^!@ovL%UT^R%BjCDz_*}bXjb6+wX&vRZ{+a!^sr8FPY*3` zuS|p5>x+!F915y?$l>0#z4(jh+qu1Wqhl%1R0ct@JWo>zcQDRNy5#Cu{mHT0F~hmH z()ZUOhSgkyo}acEwI4rWD(L52MJQG%6Nzzzu@vYy?202W#Z2;d0b{g@_!d3Ay`MmV z-Z7gH@02aFQlP{%f(-?t*94RPoep|TlA3Y>ggTz|x2v%PrhrM{jngq7s6sY*qA5@f zGkSoNy-gVj&Zr0{@e*MQ^ni=5uHWP@mk3yqD9}pU>T6&>A4Z?)UD=7_LE~-uDG*`` z+;$v~yi0KN$Gv6XzAE79BgMzd!OXXQxhA!vcZ32pW4i4~`_Hb@Q=t0e#7qk0hhnEd zv%cX!YcSO{;?cicrP~5{26=@{{G@Sk`3}@u7cw{I9*VddPk|_qq$CTrH102#?9E@M zKnvzeCx6noBxL(8vL8J9iev#uI6Qy?Eo*C?Lbw)xxyCFP2SB1%xPrQ$wGH)?k)s~L zr~Kuj&~yYQA1Al`tZhFTIn%A^yBcek9CP|*eln)Bf6_F)pADSP`;AHc4aIFt>ISF& zM(k~H>YB*j5Wjz28aBl5#+v#c%l*chx}hj-XutofwcqXA-EG3><};@Af1-0ALiaVI z+LYig$A}rkap7O=C}5VD)m>&2wB{_Cc|ax4EpBncIZ86L=g3a55XN;(OZ-!zf#u*25<`kk<&X*^v4e*}gIA$|Kb$@Tfr-`t~z=%q>9d9x19donv ztl@J$8@XloSFt0{*{{XrY@$Fm6D(;O`@7}mmm3Cp`cLRT4=Ssyf7ccAT4mcyS`Ph6 z)pU{Iu2`ZS!W1)TWH_Kq;>_q%4$F0R&cy^2@xOl*_d>7nOixvJ;1{oY?T~ylrGX5* zq<%RnR(^EOb^!j@G{@HdUvySU3yQ@$dS%2U}f8EjQjM*80Yk)Y> z8f&y2c9nMzJ9+m-S0wS}l3v5@+Oth&I`qnCb+}BoZv6P2F*wJ;ZaIq$ML52ZZs8p( z5JtlBENh3GEdi+(*4jG`8M<6^lDS&hhIW1UNoC49<5YM!h{@G!F(IpfaD&m9_;w&X4^&#q3fROIXqVYDDy3}KHLym@i;;Ndb~s;cj&ux0TKWp(VgDUenz zn#}RAxeI>jl7JlJM~zzn7|joYOQdmj+t8y=+yQ!F^tN^Mi}y7kqqd{JgIMHjEIG<$ z6Fip3Pv(s;!BQIkLS*nzgExQ@+I2Bxg+HRPWHeqEwQN{5xd4csK7j(gkTu3N9oOtz zF<*9tOL*b$@~G@qatMC_@+cR8W@g7${l>I<%2E@9H&F*m#{aK4-lG6(ehLLhPwU+} zBu z%!={fXIKC=ej3d19RMnsBVXY~qr%-_@Y@{c&*4-lP~Uy*Y%rL@I;1+^A3fj6>K$Tw zrjF9IF~R>M1h;M`xcT2@f~6EBsqCMtqA-W92#r!nCI&&D{J7l3Wz9Y%YbLKnz9zI6ArY0tI?LcKUSHO7sfqyLKj$+>6&Y zS=NCkM<~$w&N9@BlRr*ZdJc;)F#!}dgZva+^miFvLBEO8@^3KHu_yJ82KyQYCVjG< zW}s0KTdKT5Yy}-8afu?#2CI9(LZni(IXnm@IP<)q*;T0C-kQHLuR6_9nK`-egFOpt zW-4Fo!2Y-8IFI}+X~?T}K*LQJUwO}Kz-4o<)mVY;6BpU;?(bbMH}T;2-yMo}5WK5j zuWff@no)9X_%bGkZHjD1Ui?qUZ%PNne<5x9pM#(IQ$9(6hKor@eN zYuC(#DcugF6Fb270H zhF-@$_*BAYn3^BHiBQU$W_RVX|&4sRV_0wVP^wK@4(6 zL`X__`9LK<xxX6l)Sp zJp)#0s2L}8JORK@yGqVcr(5+xs^hkhQwy0;ZT=r=%3h9z^nY*M6vqIpC35WXYmH4cFhi86+EevCqc0 zfiq@^ViKf8+#*o1vYh(E$$QH%N!-i8_wloJ+K(Qlw613^mI~>2pV4AZwS91q-kkz% z^Y&1uYHIcnzuIX^(;6|=n>yGUCrw-htnvYHas^Mp=4RZ5aN>^Pbk^alkQU3Oa!F_EWa|o;kZ4pVck4pL%dFsHC_pL<}|3N=O=1t~BYe8mN3p7+ee_2-V&5 zv`H5{HLmP#6yY_LmwOv_b8_ITi=nkRrrO04IWtU*8yFrr(zGLbdV0m~eZl*Jh>>^i z;*P%m9_zpqdGd6awE>f@dZt8_Ed47UsyOmBZ22`{=a?QfHl(lGHYB`UiC$?M*Ij+# zgU*sCD&4BE-iZY?kb^Gskb7LH@#12UV6&6#<-Iy^s^#etk8S8%*Y8`j`(a}kUu)Xl zM5;KaTW22v_u&&y^gUy8l((Fx)pcp*z`|SP*gHPTR%4b%(T#D$xZ!?u-yZTQQyYy^ zpc~&$43>n}^YKOxNS0YqNAyBJp!h`Vg7G0w7(|Ef&89%sThS}xqdNt#gC756)jO5) zbA^|_Y9$8#iG`B!75)i(aq%irzO7d-+PNcE{PgEGdY??GW%oTJXzn5@k}6RYziLj* zCAgi=!q@pLvE2C3`2utJ8y2ktQLeaR79ci_GPzS;-)3QbPN^`)RW zL|VP&RDdk=ng0;w8c!6dxe;0O1#c!dbDny4>P0HXPQ}}Td{>~;pDpAVSCbItR&Z?} zF=f@=B-N9d9P_M8mP1@IA(>`jo+iGbF}z`W=s%lx2kpn_!>3mX?-0 zLPtksrp}g~m4P3)k3rD!(IlGqmu0vUGA0?`H-{e{H+3?*Ju10}=WljjER_fC+03VQ zVLl@yR<*Yaqq+N@@ekK&pI*89jVJnE)R!-fGP^dpu}aGeeX4-q=U9?JGQP$n0E5@B zyW8=siQUC@aBFknQH6IjSI1YLRaAVy@9Xpsb1R`y$>_Q}Mr;lB{@Tl^6y849$8g)5 zYE)vcCIfytCf*8bk<+O^bsv%D&9IHH)CQccZ>GZqt>?pBbPzbzH|Jtk@b7`gU;eb~ zRZTQV{uKbLbp{KTyk(x7wT@8u&z7e*NBdhzQFXwqZwj#GAeMyof#ya@IH1Dr=4z0alu7)0Z$ArThYOrJMV#rR>qS29*V4Qz0S(c9TEAW~GCtt?qEWQ$ zQ$m2JcM%2fFDy>n(jgpFBhRYP@z<4&t~|f(clr2Gey|97EJd)jWU_4x(^yC1e~H&9 zJiCe^nUE7^WXto481NN&evZ<8cR#kiDO^6OJ@VPm5#;fd>|&sur9l^77>XjTG>!#* z`l9K_x9=@zg4lYu=hl%e_g8Rt{ornnd8+diNWHZhlV>vZa#aH`FT@e3D3&33@hjDHFS@~SA);Y_-Cb9dkmrHs$$23b zz1!0n3qrc~Q%+7!NqinJ;>?c)?}5HQ3)p|t9B%?>6O@Cl3QIUgO)4!vpndM2-{$dXU_6Rcn$D-k=CJtR~Z{S8UVu)^^%B!R7X4R1c64J%H0%k=2tE)g>ZG|bDbYHmxkr4R9+_T55%aubiW zdv&owrjR3P(e>TJtxyF+6kFBEMf1C|GD!%zyOB{grgb>>AVcRKyeZ%wmr$CS~PbN^)BdJoiqSrc`z1fKaIPnI||K8zEn6JiYI((6N!5(`@}yd(Xu>g54>V?o)P*y z_CD-{ScD%H;)ct$n{rw`=-mbL%Xw=cjVpaExm9!9v$FEd0}j?)kQFKlO9EG#+zbGqkOdCNC@i+E}`&!}q;Xx#YzCfCUP$fLSj9{XG)PnyU;%cfA` z9)ZpKLz5JD_?`E^bNrJT06fq`4fQ%S%#w*dzd0OlAP>fE@RBq$;Y4xe5`S_64D?#+ zqODyzBbHSIfu7no??`XY#m^tP_*%%yH(a*R?ClJW8%d3b8?sTq@AcJ@2x1xith_ht z4TcjXW9}i+)|Nr(B-Xwln_$l~;?vo;1P#KM($iA}&*8rD>bB8_UD1BftCbjasR%nV zm-Nfec$vE!pZ`~mM>4g^G?KHWG?@2+Kwwoa%98evFs^llF|7-yk5r_7=GCg!Wm_NN zFPR1!(y@fCK}zz>_FH>)5{@tA#XKLoG27kK)BX9aRYzxWx>&Z6^!KhS`YMj4lLxjsoiRSjp~_9s6;GapRXza=azyXm*!U z>n#HWzT_ybFB2mS>|nL79w>~D#E!S^>9?*ejW7(#6{Vr~pE&Ivw&P5R2PtU?b))-a(qlPvO3}o6o8O3K>hLTrmRsEqjl)h;B=(N*ZBzupJue zzMs!YfoeH7*?f*#c6-}6r8IFoJ1f*CNYRJkrC8*x>@9}7tHmPABaKd-;vNYzsIoSw z)_5;AS6pZPJaQZ;9&}u0+CN>h^^H|4uY6^PYoddp4=)_8C4n6u*TdK1Dl0m5=%s37 z#+yr)7xtf|#a@Wi=;wV(FVV`K4ic7I%Rk#7oa~%%-&$7q(5n&2c(x}9XI(IAUez57 zjGB%Z0Am7NfoY_IeMPVks9=6mhQMp#_&Cu=&4p;9DG7i3J^+(Y7crzLJ(2{L3q5+3 zfJWGJm@vEgu=&pf>kD=7Nm(2HJfnDJKo1i(!2m^ z*>eK{WHxIFFdd7ac2%_!T7yM>zdI}%{G zn;aj>E14!awL2R2%T2NwU-nQ%2C47%nd9@pg|TpTv*$HXkHe&_cja=E<7#qz<)M;4 zf*aQl)c0qH6GUxujeokSNx@Kf{|I*6p32&yG*g1NkL;yc*gDKW;`)FvXRHDQphU@& zfqSV!sux3u%b+occSjq$S0<&ZWgdUnj#wFLi`-wj8v2T6`JCSAfH~m(V_pEr!6?yf zl}j^aRTZh#ZLGxdJv^)|SM{aAbN5GM%#p~w)VE5?bW%iJC{T1%j@eW%iHoF77%7*( z*Q43uE$exvHN(?OnJx=bQ5CG?o3!&s!OL!b(3muKVZWY)_vvuy{O2O z&%T~_vx&ZO)!>}5!O+kGZhlM)x-1!AzwOu1_IiGwETkhYIW9@F0uoaFjL<{lMZk&D zmeM~%R=K!`+|Y{8{rTcha|`gg_U{6B>j*Ua6N49+vuZQntHZ`4APAue1UjnK5mgS9 zVDLu_**l&A7`c;EE&=~#=6ODzuJVpJW|M{3<93>)3wpddd*55VHHH)A8X0$X&Yu49mpg7l+ib82szSR*pPP^Uy3d#!V@{uW6)c zX|x)%(VxVkMP&@eueC;Xt*$O4Gi)=Trva;q4GA2lT~Gsz*C-oii5B_Kze;5kQHvFz&XIUv;iJWc6{&!tYB@9dPM|6^&B<;H{YMyx@3qc{pkwf zCp8UF724lj=YH^bF0}rrDQTN_ftq1;{{$KS{!fB>Qum^pKKwk;?stNv|Ky+p#^f$Y zQeSlnf$I{+FrTVO+%pJkcfIDeP?N<(K!exDPIQ>u3yPkz!iy-Bq&Tt}k6;aLrG!EW z0@+Lz>2-n!wmFC)1)UMWcl+ep8E9tXlciCEk8fRgIogpv6W}_rFJ19i!}NvBqD^bs&kjqrrxlNn(>A7I~Kf>VF#rH|6 z&%s&%!ACK8hZNE?^D6Wz7fFKxrLgMa3M>>wkNfvRXzMyeLJyMS+a@L*eT=d?uH{ z;K>5ifzd-jr z7FMz{3BJP4;i=i=99k4GxQ}=5$*$;4=kh)V?&jH=tVJ!cL+bMI)~k%DXH-EdqOJ2P z8H+T;hxpcFc&j?gE7ERaQeqqNURlA^hmLW6=OpG+R;P-|M=YLlpY5_sfWkab#VI82 zB!qD*L3vT`Lo@KQRGo+o57lI=%IgM-<;FAGRT>5l&Fmftm}xch|F?wAQv?bfvyw zAN0$gng}op8t?F!VexOP2t1E7&6tk;w_7gOiI1NKT6mStbXeWXJS9WJ*w|yJ`IRL9 z4RjQWC>=l&$V?pub!&50uBlpF+QV~cNKrf; zI1P8bol7Hzf5>c}7qT(MX9VS}enFk6e#hRORD%W_X~j`YDEH_ z8gN_Bz-EQM1tE=W1QremCV}i1-xOeZa2J70CB5q}y|+0`$FUIK+guWDEIjVScj28y z%+ig5*UZOGTg_7+4(cy`)XS3T+f+AaFTs^N)ti$)SxcB%R3g^iT19&|YNDdb;>5Ge z)Gc-x3h92vMduedwS1FvdiIXa{Bvkll$bMfIP1w{sb}1zM$3575ocTT421CR@%3BG z^cmHIE$N$|`2vsr4@mr-K3kuiT^K9Ns72pmIps@wR~0{`nqDa$l!)`%`D$V>?bTDl zgSAn96v(r6z8PrtneM}lM&tr-QIcpA>E(8U-ff$rVAW;;&ko&WUgjsa#U2#hINQj` zeiqZSl_a9c3qKl*#uC}LE?ClbhZCBSwzmjCDSF?diLvNB8FV29TADUNcHr7l5RMrZ z_}kt_3Pd$Pfd)X{-<^G>z4NR&6sXu8M~tQW8ct~Wzqp>}8EP<{oOzZER5sjIn4kn- zvVc7(&`cMWWCUlLkhb^mP@s6H6ojUN0+j;?0;Ahq+^7Js_eVw(D?M{$fSI5*8s>uRY>dt=R(*1$=>XoZMY~9r``_@`X)F^frMP9?y=9H$*WOo5BPO!9Ld3OKN~)uOyh*c3(F%r!hdK^ zylOq7*4xR0>Va36B-S4AtERNBa)ISUsNZFUz60)4$B=-><2S9*x|T$8#r!TMfC20o2JM9)}$g!|X?;VDIi78YPOI!Nm;f4jP z6Bc`nM7|dvbxrA>lBK;vH7vioCh1b(1PFNBhayVJleSbX0#(KL9P!K60Z+SfEc>2) z8QJzlO4)(QuPboPofkLroVD>hc11urN{V%8cZm*2HIvYBevdM_*oTOFs^ZxzV5*Ba zmLduJJnQZF;rAC8-BxUqm`xn7ZSwIGjL>J^CBaL(Fm&d&_MyWR2vy3QP1V_$tcFX# z5v7$$9P$I^lV|s4tZI9I;%V*KMbu`S_&^qaJnibTDPG|D2>s~;Mj=MRmE(4zfyw5fo2b+#J`cXGT}+=!AVD|P0YCfIiW=a>Is5DQQ|&ru@F(
" where + /// "" indicates the underlying network transport (such as "ipv4", "unix", or + /// "in-process"). This is a guideline for how descriptions should be formatted; different + /// implementations may not follow this format so you shouldn't make assumptions based on it. + /// + /// Some examples include: + /// - "ipv4:127.0.0.1:31415", + /// - "ipv6:[::1]:443", + /// - "in-process:27182". + public var peer: String + /// A handle for checking the cancellation status of an RPC. public var cancellation: RPCCancellationHandle @@ -26,10 +39,16 @@ public struct ServerContext: Sendable { /// /// - Parameters: /// - descriptor: A description of the method being called. + /// - peer: A description of the remote peer. /// - cancellation: A cancellation handle. You can create a cancellation handle /// using ``withServerContextRPCCancellationHandle(_:)``. - public init(descriptor: MethodDescriptor, cancellation: RPCCancellationHandle) { + public init( + descriptor: MethodDescriptor, + peer: String, + cancellation: RPCCancellationHandle + ) { self.descriptor = descriptor + self.peer = peer self.cancellation = cancellation } } diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift index 11828108c..b24ec07e1 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -109,7 +109,7 @@ extension InProcessTransport { /// - Parameters: /// - server: The in-process server transport to connect to. /// - serviceConfig: Service configuration. - public init( + package init( server: InProcessTransport.Server, serviceConfig: ServiceConfig = ServiceConfig() ) { diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 953d2ffdb..90e291b6e 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -34,6 +34,7 @@ extension InProcessTransport { private let newStreams: AsyncStream> private let newStreamsContinuation: AsyncStream>.Continuation + private let peer: String private struct State: Sendable { private var _nextID: UInt64 @@ -73,9 +74,10 @@ extension InProcessTransport { private let handles: Mutex /// Creates a new instance of ``Server``. - public init() { + package init(peer: String) { (self.newStreams, self.newStreamsContinuation) = AsyncStream.makeStream() self.handles = Mutex(State()) + self.peer = peer } /// Publish a new ``RPCStream``, which will be returned by the transport's ``events`` @@ -115,7 +117,11 @@ extension InProcessTransport { handle.cancel() } - let context = ServerContext(descriptor: stream.descriptor, cancellation: handle) + let context = ServerContext( + descriptor: stream.descriptor, + peer: self.peer, + cancellation: handle + ) await streamHandler(stream, context) } } diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index 1a563f079..cd891a64c 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -25,7 +25,8 @@ public struct InProcessTransport: Sendable { /// - Parameters: /// - serviceConfig: Configuration describing how methods should be executed. public init(serviceConfig: ServiceConfig = ServiceConfig()) { - self.server = Self.Server() + let peer = "in-process:\(System.pid())" + self.server = Self.Server(peer: peer) self.client = Self.Client(server: self.server, serviceConfig: serviceConfig) } } diff --git a/Sources/GRPCInProcessTransport/Syscalls.swift b/Sources/GRPCInProcessTransport/Syscalls.swift new file mode 100644 index 000000000..96737a791 --- /dev/null +++ b/Sources/GRPCInProcessTransport/Syscalls.swift @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif + +enum System { + static func pid() -> Int { + #if canImport(Darwin) + let pid = Darwin.getpid() + return Int(pid) + #elseif canImport(Glibc) + let pid = Glibc.getpid() + return Int(pid) + #elseif canImport(Musl) + let pid = Musl.getpid() + return Int(pid) + #endif + } +} diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 57892399e..0a74ba96f 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -47,13 +47,13 @@ struct ClientRPCExecutorTestHarness { switch transport { case .inProcess: - let server = InProcessTransport.Server() + let server = InProcessTransport.Server(peer: "in-process:1234") let client = server.spawnClientTransport() self.serverTransport = StreamCountingServerTransport(wrapping: server) self.clientTransport = StreamCountingClientTransport(wrapping: client) case .throwsOnStreamCreation(let code): - let server = InProcessTransport.Server() // Will never be called. + let server = InProcessTransport.Server(peer: "in-process:1234") // Will never be called. let client = ThrowOnStreamCreationTransport(code: code) self.serverTransport = StreamCountingServerTransport(wrapping: server) self.clientTransport = StreamCountingClientTransport(wrapping: client) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index 07251e593..6634d9e9f 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -102,6 +102,7 @@ struct ServerRPCExecutorTestHarness { await withServerContextRPCCancellationHandle { cancellation in let context = ServerContext( descriptor: MethodDescriptor(fullyQualifiedService: "foo", method: "bar"), + peer: "tests", cancellation: cancellation ) diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index c7856d7c0..388940e83 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -334,7 +334,10 @@ final class GRPCServerTests: XCTestCase { } func testTestRunStoppedServer() async throws { - let server = GRPCServer(transport: InProcessTransport.Server(), services: []) + let server = GRPCServer( + transport: InProcessTransport.Server(peer: "in-process:1234"), + services: [] + ) // Run the server. let task = Task { try await server.serve() } task.cancel() diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 36ef7b84a..8d4e3a2ae 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -142,7 +142,7 @@ final class InProcessClientTransportTests: XCTestCase { } func testOpenStreamSuccessfullyAndThenClose() async throws { - let server = InProcessTransport.Server() + let server = InProcessTransport.Server(peer: "in-process:1234") let client = makeClient(server: server) try await withThrowingTaskGroup(of: Void.self) { group in @@ -199,7 +199,7 @@ final class InProcessClientTransportTests: XCTestCase { ) var client = InProcessTransport.Client( - server: InProcessTransport.Server(), + server: InProcessTransport.Server(peer: "in-process:1234"), serviceConfig: serviceConfig ) @@ -223,7 +223,7 @@ final class InProcessClientTransportTests: XCTestCase { ) serviceConfig.methodConfig.append(overrideConfiguration) client = InProcessTransport.Client( - server: InProcessTransport.Server(), + server: InProcessTransport.Server(peer: "in-process:1234"), serviceConfig: serviceConfig ) @@ -239,7 +239,7 @@ final class InProcessClientTransportTests: XCTestCase { } func testOpenMultipleStreamsThenClose() async throws { - let server = InProcessTransport.Server() + let server = InProcessTransport.Server(peer: "in-process:1234") let client = makeClient(server: server) try await withThrowingTaskGroup(of: Void.self) { group in @@ -269,7 +269,7 @@ final class InProcessClientTransportTests: XCTestCase { } func makeClient( - server: InProcessTransport.Server = InProcessTransport.Server() + server: InProcessTransport.Server = InProcessTransport.Server(peer: "in-process:1234") ) -> InProcessTransport.Client { let defaultPolicy = RetryPolicy( maxAttempts: 10, diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 9ecfc4a14..487dfc953 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -21,7 +21,7 @@ import XCTest final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { - let transport = InProcessTransport.Server() + let transport = InProcessTransport.Server(peer: "in-process:1234") let outbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let stream = RPCStream< @@ -53,7 +53,7 @@ final class InProcessServerTransportTests: XCTestCase { } func testStopListening() async throws { - let transport = InProcessTransport.Server() + let transport = InProcessTransport.Server(peer: "in-process:1234") let firstStreamOutbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let firstStream = RPCStream< diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 11edbfd6c..786f6fe99 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -64,6 +64,29 @@ struct InProcessTransportTests { client.beginGracefulShutdown() } } + + @Test("Peer info") + func peerInfo() async throws { + try await self.withTestServerAndClient { server, client in + defer { + client.beginGracefulShutdown() + server.beginGracefulShutdown() + } + + let peerInfo = try await client.unary( + request: ClientRequest(message: ()), + descriptor: .peerInfo, + serializer: VoidSerializer(), + deserializer: UTF8Deserializer(), + options: .defaults + ) { + try $0.message + } + + let match = peerInfo.wholeMatch(of: /in-process:\d+/) + #expect(match != nil) + } + } } private struct TestService: RegistrableRPCService { @@ -96,6 +119,13 @@ private struct TestService: RegistrableRPCService { } } + func peerInfo( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + return ServerResponse(message: context.peer) + } + func registerMethods(with router: inout RPCRouter) { router.registerHandler( forMethod: .testCancellation, @@ -105,6 +135,19 @@ private struct TestService: RegistrableRPCService { try await self.cancellation(request: ServerRequest(stream: $0), context: $1) } ) + + router.registerHandler( + forMethod: .peerInfo, + deserializer: VoidDeserializer(), + serializer: UTF8Serializer(), + handler: { + let response = try await self.peerInfo( + request: ServerRequest(stream: $0), + context: $1 + ) + return StreamingServerResponse(single: response) + } + ) } } @@ -113,6 +156,11 @@ extension MethodDescriptor { fullyQualifiedService: "test", method: "cancellation" ) + + fileprivate static let peerInfo = Self( + fullyQualifiedService: "test", + method: "peerInfo" + ) } private struct UTF8Serializer: MessageSerializer { @@ -126,3 +174,14 @@ private struct UTF8Deserializer: MessageDeserializer { String(decoding: serializedMessageBytes, as: UTF8.self) } } + +private struct VoidSerializer: MessageSerializer { + func serialize(_ message: Void) throws -> [UInt8] { + [] + } +} + +private struct VoidDeserializer: MessageDeserializer { + func deserialize(_ serializedMessageBytes: [UInt8]) throws { + } +} From 962d63471438b5d40406d96a6dca4086b37f471b Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 4 Dec 2024 10:59:56 +0000 Subject: [PATCH 514/580] Update parameter labels for GRPCClient (#2137) Motivation: The updated generated code added parameter labels to the handler passed to each method of the GRPCClient. This was done purposefully to improve readability. We haven't as yet actually updated the client to use the new labels. Modifications: - Update param labels in GRPCClient Result: New generated code compiles --- .../Documentation.docc/Development/Design.md | 8 ++-- Sources/GRPCCore/GRPCClient.swift | 48 +++++++++++-------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/Sources/GRPCCore/Documentation.docc/Development/Design.md b/Sources/GRPCCore/Documentation.docc/Development/Design.md index 1861d3ae0..cecccbafa 100644 --- a/Sources/GRPCCore/Documentation.docc/Development/Design.md +++ b/Sources/GRPCCore/Documentation.docc/Development/Design.md @@ -159,10 +159,10 @@ can either be applied to all RPCs or to specific services. The call layer includes a concrete ``GRPCClient`` which provides API to execute all four types of RPC against a ``ClientTransport``. These methods are: -- ``GRPCClient/unary(request:descriptor:serializer:deserializer:options:handler:)``, -- ``GRPCClient/clientStreaming(request:descriptor:serializer:deserializer:options:handler:)``, -- ``GRPCClient/serverStreaming(request:descriptor:serializer:deserializer:options:handler:)``, and -- ``GRPCClient/bidirectionalStreaming(request:descriptor:serializer:deserializer:options:handler:)``. +- ``GRPCClient/unary(request:descriptor:serializer:deserializer:options:onResponse:)``, +- ``GRPCClient/clientStreaming(request:descriptor:serializer:deserializer:options:onResponse:)``, +- ``GRPCClient/serverStreaming(request:descriptor:serializer:deserializer:options:onResponse:)``, and +- ``GRPCClient/bidirectionalStreaming(request:descriptor:serializer:deserializer:options:onResponse:)``. As lower level methods they require you to pass in a serializer and deserializer, as well as the descriptor of the method being called. Each method diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 0ac39c8e0..b9d3234b1 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -21,10 +21,10 @@ private import Synchronization /// A ``GRPCClient`` communicates to a server via a ``ClientTransport``. /// /// You can start RPCs to the server by calling the corresponding method: -/// - ``unary(request:descriptor:serializer:deserializer:options:handler:)`` -/// - ``clientStreaming(request:descriptor:serializer:deserializer:options:handler:)`` -/// - ``serverStreaming(request:descriptor:serializer:deserializer:options:handler:)`` -/// - ``bidirectionalStreaming(request:descriptor:serializer:deserializer:options:handler:)`` +/// - ``unary(request:descriptor:serializer:deserializer:options:onResponse:)`` +/// - ``clientStreaming(request:descriptor:serializer:deserializer:options:onResponse:)`` +/// - ``serverStreaming(request:descriptor:serializer:deserializer:options:onResponse:)`` +/// - ``bidirectionalStreaming(request:descriptor:serializer:deserializer:options:onResponse:)`` /// /// However, in most cases you should prefer wrapping the ``GRPCClient`` with a generated stub. /// @@ -247,16 +247,18 @@ public final class GRPCClient: Sendable { /// - serializer: A request serializer. /// - deserializer: A response deserializer. /// - options: Call specific options. - /// - handler: A unary response handler. + /// - handleResponse: A unary response handler. /// - /// - Returns: The return value from the `handler`. + /// - Returns: The return value from the `handleResponse`. public func unary( request: ClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: ClientResponse + ) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( request: StreamingClientRequest(single: request), @@ -266,7 +268,7 @@ public final class GRPCClient: Sendable { options: options ) { stream in let singleResponse = await ClientResponse(stream: stream) - return try await handler(singleResponse) + return try await handleResponse(singleResponse) } } @@ -278,16 +280,18 @@ public final class GRPCClient: Sendable { /// - serializer: A request serializer. /// - deserializer: A response deserializer. /// - options: Call specific options. - /// - handler: A unary response handler. + /// - handleResponse: A unary response handler. /// - /// - Returns: The return value from the `handler`. + /// - Returns: The return value from the `handleResponse`. public func clientStreaming( request: StreamingClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: ClientResponse + ) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( request: request, @@ -297,7 +301,7 @@ public final class GRPCClient: Sendable { options: options ) { stream in let singleResponse = await ClientResponse(stream: stream) - return try await handler(singleResponse) + return try await handleResponse(singleResponse) } } @@ -309,16 +313,18 @@ public final class GRPCClient: Sendable { /// - serializer: A request serializer. /// - deserializer: A response deserializer. /// - options: Call specific options. - /// - handler: A response stream handler. + /// - handleResponse: A response stream handler. /// - /// - Returns: The return value from the `handler`. + /// - Returns: The return value from the `handleResponse`. public func serverStreaming( request: ClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (StreamingClientResponse) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: StreamingClientResponse + ) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( request: StreamingClientRequest(single: request), @@ -326,7 +332,7 @@ public final class GRPCClient: Sendable { serializer: serializer, deserializer: deserializer, options: options, - handler: handler + onResponse: handleResponse ) } @@ -341,16 +347,18 @@ public final class GRPCClient: Sendable { /// - serializer: A request serializer. /// - deserializer: A response deserializer. /// - options: Call specific options. - /// - handler: A response stream handler. + /// - handleResponse: A response stream handler. /// - /// - Returns: The return value from the `handler`. + /// - Returns: The return value from the `handleResponse`. public func bidirectionalStreaming( request: StreamingClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (StreamingClientResponse) async throws -> ReturnValue + onResponse handleResponse: @Sendable @escaping ( + _ response: StreamingClientResponse + ) async throws -> ReturnValue ) async throws -> ReturnValue { let applicableInterceptors = try self.stateMachine.withLock { try $0.checkExecutableAndGetApplicableInterceptors(for: descriptor) @@ -367,7 +375,7 @@ public final class GRPCClient: Sendable { deserializer: deserializer, transport: self.transport, interceptors: applicableInterceptors, - handler: handler + handler: handleResponse ) } } From 1f2e78b067d1c8b2b1e9586db5e7094ed3e8fc18 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 4 Dec 2024 15:17:24 +0000 Subject: [PATCH 515/580] Remove guarded declarations case in generated code (#2138) ## Motivation https://github.com/grpc/grpc-swift/pull/2131 removed all usages of availability guards. However, the `guarded` Declaration case was not removed even though it's unused. ## Modifications This PR removes the `guarded` case since it's not used anywhere anymore. ## Result Cleaner codebase. --- .../GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift | 2 -- .../Internal/StructuredSwiftRepresentation.swift | 7 ------- 2 files changed, 9 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 888d91fd3..c7c6f3850 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -857,8 +857,6 @@ struct TextBasedRenderer: RendererProtocol { renderCommentableDeclaration(comment: comment, declaration: nestedDeclaration) case let .deprecated(deprecation, nestedDeclaration): renderDeprecatedDeclaration(deprecation: deprecation, declaration: nestedDeclaration) - case let .guarded(availability, nestedDeclaration): - renderGuardedDeclaration(availability: availability, declaration: nestedDeclaration) case .variable(let variableDescription): renderVariable(variableDescription) case .extension(let extensionDescription): renderExtension(extensionDescription) case .struct(let structDescription): renderStruct(structDescription) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index c39be20d6..3a0f1ee59 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -791,9 +791,6 @@ indirect enum Declaration: Equatable, Codable, Sendable { /// A declaration that adds a comment on top of the provided declaration. case deprecated(DeprecationDescription, Declaration) - /// A declaration that adds an availability guard on top of the provided declaration. - case guarded(AvailabilityDescription, Declaration) - /// A variable declaration. case variable(VariableDescription) @@ -1870,7 +1867,6 @@ extension Declaration { switch self { case .commentable(_, let declaration): return declaration.accessModifier case .deprecated(_, let declaration): return declaration.accessModifier - case .guarded(_, let declaration): return declaration.accessModifier case .variable(let variableDescription): return variableDescription.accessModifier case .extension(let extensionDescription): return extensionDescription.accessModifier case .struct(let structDescription): return structDescription.accessModifier @@ -1889,9 +1885,6 @@ extension Declaration { case .deprecated(let deprecationDescription, var declaration): declaration.accessModifier = newValue self = .deprecated(deprecationDescription, declaration) - case .guarded(let availability, var declaration): - declaration.accessModifier = newValue - self = .guarded(availability, declaration) case .variable(var variableDescription): variableDescription.accessModifier = newValue self = .variable(variableDescription) From b281cf1aa4a7771fcd1baeba9565781bd45b8667 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 4 Dec 2024 15:52:13 +0000 Subject: [PATCH 516/580] Update examples and tutorials (#2139) Motivation: We're going to tag beta.1 soon, so the examples and tutorials need to be updated to reflect this. Modifications: - Regenerate example code - Update example code to use new APIs - Update example manifests to use the as-yet-unreleased beta.1 tag of each dependency Result: Examples are up-to-date --- Examples/echo/Package.swift | 8 +- .../echo/Sources/Generated/echo.grpc.swift | 982 ++++++++++---- .../echo/Sources/Subcommands/Collect.swift | 4 +- .../echo/Sources/Subcommands/Expand.swift | 4 +- Examples/echo/Sources/Subcommands/Get.swift | 4 +- Examples/echo/Sources/Subcommands/Serve.swift | 44 +- .../echo/Sources/Subcommands/Update.swift | 4 +- Examples/hello-world/Package.swift | 8 +- .../Sources/Generated/helloworld.grpc.swift | 345 +++-- .../Sources/Subcommands/Greet.swift | 4 +- .../Sources/Subcommands/Serve.swift | 12 +- Examples/route-guide/Package.swift | 8 +- .../Sources/Generated/route_guide.grpc.swift | 1170 ++++++++++++----- .../Sources/Subcommands/GetFeature.swift | 4 +- .../Sources/Subcommands/ListFeatures.swift | 4 +- .../Sources/Subcommands/RecordRoute.swift | 4 +- .../Sources/Subcommands/RouteChat.swift | 4 +- .../Sources/Subcommands/Serve.swift | 58 +- .../Hello-World/Hello-World.tutorial | 2 +- .../Resources/hello-world-sec04-step01.swift | 8 +- .../Resources/hello-world-sec04-step02.swift | 14 +- .../Resources/hello-world-sec04-step03.swift | 2 +- .../Resources/hello-world-sec04-step04.swift | 2 +- ...route-guide-sec01-step07-description.swift | 5 +- ...route-guide-sec01-step08-description.swift | 5 +- .../route-guide-sec04-step01-struct.swift | 2 +- ...ute-guide-sec04-step02-unimplemented.swift | 20 +- .../route-guide-sec04-step03-features.swift | 20 +- .../route-guide-sec04-step04-unary.swift | 20 +- .../route-guide-sec04-step05-unary.swift | 24 +- ...-guide-sec04-step06-server-streaming.swift | 32 +- ...-guide-sec04-step07-server-streaming.swift | 79 -- ...-guide-sec04-step08-client-streaming.swift | 38 +- ...te-guide-sec04-step09-bidi-streaming.swift | 68 +- ...te-guide-sec04-step10-bidi-streaming.swift | 77 +- .../route-guide-sec05-step00-package.swift | 4 +- .../route-guide-sec05-step01-package.swift | 5 +- .../route-guide-sec05-step02-package.swift | 5 +- .../route-guide-sec05-step07-server.swift | 2 +- .../route-guide-sec05-step08-run.swift | 2 +- ...ute-guide-sec06-step03-create-client.swift | 2 +- .../route-guide-sec06-step04-run-client.swift | 2 +- .../route-guide-sec06-step05-stub.swift | 4 +- ...route-guide-sec06-step06-get-feature.swift | 6 +- ...ute-guide-sec06-step07-list-features.swift | 8 +- ...oute-guide-sec06-step08-record-route.swift | 10 +- .../route-guide-sec06-step09-route-chat.swift | 12 +- .../Route-Guide/Route-Guide.tutorial | 46 +- 48 files changed, 2129 insertions(+), 1068 deletions(-) delete mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index 3792fcb04..8029be33f 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", exact: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "1.0.0-alpha.1"), - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "1.0.0-beta.1"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ .executableTarget( diff --git a/Examples/echo/Sources/Generated/echo.grpc.swift b/Examples/echo/Sources/Generated/echo.grpc.swift index 88b929b9a..7ad44d1ce 100644 --- a/Examples/echo/Sources/Generated/echo.grpc.swift +++ b/Examples/echo/Sources/Generated/echo.grpc.swift @@ -23,42 +23,65 @@ import GRPCCore import GRPCProtobuf +import SwiftProtobuf +// MARK: - echo.Echo + +/// Namespace containing generated types for the "echo.Echo" service. internal enum Echo_Echo { - internal static let descriptor = GRPCCore.ServiceDescriptor.echo_Echo + /// Service descriptor for the "echo.Echo" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") + /// Namespace for method metadata. internal enum Method { + /// Namespace for "Get" metadata. internal enum Get { + /// Request type for "Get". internal typealias Input = Echo_EchoRequest + /// Response type for "Get". internal typealias Output = Echo_EchoResponse + /// Descriptor for "Get". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), method: "Get" ) } + /// Namespace for "Expand" metadata. internal enum Expand { + /// Request type for "Expand". internal typealias Input = Echo_EchoRequest + /// Response type for "Expand". internal typealias Output = Echo_EchoResponse + /// Descriptor for "Expand". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), method: "Expand" ) } + /// Namespace for "Collect" metadata. internal enum Collect { + /// Request type for "Collect". internal typealias Input = Echo_EchoRequest + /// Response type for "Collect". internal typealias Output = Echo_EchoResponse + /// Descriptor for "Collect". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), method: "Collect" ) } + /// Namespace for "Update" metadata. internal enum Update { + /// Request type for "Update". internal typealias Input = Echo_EchoRequest + /// Response type for "Update". internal typealias Output = Echo_EchoResponse + /// Descriptor for "Update". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Echo_Echo.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), method: "Update" ) } + /// Descriptors for all methods in the "echo.Echo" service. internal static let descriptors: [GRPCCore.MethodDescriptor] = [ Get.descriptor, Expand.descriptor, @@ -66,54 +89,265 @@ internal enum Echo_Echo { Update.descriptor ] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Echo_Echo_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Echo_Echo_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Echo_Echo_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Echo_Echo_Client } extension GRPCCore.ServiceDescriptor { - internal static let echo_Echo = Self( - package: "echo", - service: "Echo" - ) + /// Service descriptor for the "echo.Echo" service. + internal static let echo_Echo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Echo_Echo_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Immediately returns an echo of a request. - func get( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Splits a request into words and returns each word in a stream of messages. - func expand( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Streams back messages as they are received in an input stream. - func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse +// MARK: echo.Echo (server) + +extension Echo_Echo { + /// Streaming variant of the service protocol for the "echo.Echo" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func get( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func expand( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func collect( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func update( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "echo.Echo" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + internal protocol ServiceProtocol: Echo_Echo.StreamingServiceProtocol { + /// Handle the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Echo_EchoResponse` message. + func get( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func expand( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Echo_EchoResponse` message. + func collect( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func update( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Simple service protocol for the "echo.Echo" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + internal protocol SimpleServiceProtocol: Echo_Echo.ServiceProtocol { + /// Handle the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A `Echo_EchoRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Echo_EchoResponse` to respond with. + func get( + request: Echo_EchoRequest, + context: GRPCCore.ServerContext + ) async throws -> Echo_EchoResponse + + /// Handle the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A `Echo_EchoRequest` message. + /// - response: A response stream of `Echo_EchoResponse` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func expand( + request: Echo_EchoRequest, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A stream of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Echo_EchoResponse` to respond with. + func collect( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> Echo_EchoResponse + + /// Handle the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A stream of `Echo_EchoRequest` messages. + /// - response: A response stream of `Echo_EchoResponse` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func update( + request: GRPCCore.RPCAsyncSequence, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + } } -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Default implementation of 'registerMethods(with:)'. extension Echo_Echo.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Echo_Echo.Method.Get.descriptor, @@ -162,35 +396,7 @@ extension Echo_Echo.StreamingServiceProtocol { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Echo_Echo_ServiceProtocol: Echo_Echo.StreamingServiceProtocol { - /// Immediately returns an echo of a request. - func get( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Splits a request into words and returns each word in a stream of messages. - func expand( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Streams back messages as they are received in an input stream. - func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse -} - -/// Partial conformance to `Echo_Echo_StreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Default implementation of streaming methods from 'StreamingServiceProtocol'. extension Echo_Echo.ServiceProtocol { internal func get( request: GRPCCore.StreamingServerRequest, @@ -202,7 +408,7 @@ extension Echo_Echo.ServiceProtocol { ) return GRPCCore.StreamingServerResponse(single: response) } - + internal func expand( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext @@ -213,7 +419,7 @@ extension Echo_Echo.ServiceProtocol { ) return response } - + internal func collect( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext @@ -226,117 +432,457 @@ extension Echo_Echo.ServiceProtocol { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Echo_Echo_ClientProtocol: Sendable { - /// Immediately returns an echo of a request. - func get( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// Splits a request into words and returns each word in a stream of messages. - func expand( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// Streams back messages as they are received in an input stream. - func update( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable +// Default implementation of methods from 'ServiceProtocol'. +extension Echo_Echo.SimpleServiceProtocol { + internal func get( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.get( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func expand( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.expand( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func collect( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.collect( + request: request.messages, + context: context + ), + metadata: [:] + ) + } + + internal func update( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.update( + request: request.messages, + response: writer, + context: context + ) + return [:] + } + ) + } +} + +// MARK: echo.Echo (client) + +extension Echo_Echo { + /// Generated client protocol for the "echo.Echo" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + internal protocol ClientProtocol: Sendable { + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func get( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func expand( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func collect( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func update( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "echo.Echo" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + internal struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func get( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Echo_Echo.Method.Get.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func expand( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Echo_Echo.Method.Expand.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func collect( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Echo_Echo.Method.Collect.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func update( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Echo_Echo.Method.Update.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Helpers providing default arguments to 'ClientProtocol' methods. extension Echo_Echo.ClientProtocol { - internal func get( + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func get( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.get( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } - - internal func expand( + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func expand( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.expand( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } - - internal func collect( + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func collect( request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.collect( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } - - internal func update( + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func update( request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.update( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Helpers providing sugared APIs for 'ClientProtocol' methods. extension Echo_Echo.ClientProtocol { - /// Immediately returns an echo of a request. + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func get( _ message: Echo_EchoRequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.ClientRequest( @@ -346,11 +892,24 @@ extension Echo_Echo.ClientProtocol { return try await self.get( request: request, options: options, - handleResponse + onResponse: handleResponse ) } - - /// Splits a request into words and returns each word in a stream of messages. + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func expand( _ message: Echo_EchoRequest, metadata: GRPCCore.Metadata = [:], @@ -364,130 +923,73 @@ extension Echo_Echo.ClientProtocol { return try await self.expand( request: request, options: options, - handleResponse + onResponse: handleResponse ) } - - /// Collects a stream of messages and returns them concatenated when the caller closes. + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func collect( metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.StreamingClientRequest( metadata: metadata, - producer: requestProducer + producer: producer ) return try await self.collect( request: request, options: options, - handleResponse + onResponse: handleResponse ) } - - /// Streams back messages as they are received in an input stream. + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func update( metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { let request = GRPCCore.StreamingClientRequest( metadata: metadata, - producer: requestProducer + producer: producer ) return try await self.update( request: request, options: options, - handleResponse - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Echo_Echo_Client: Echo_Echo.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Immediately returns an echo of a request. - internal func get( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Echo_Echo.Method.Get.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Splits a request into words and returns each word in a stream of messages. - internal func expand( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Echo_Echo.Method.Expand.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Collects a stream of messages and returns them concatenated when the caller closes. - internal func collect( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Echo_Echo.Method.Collect.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Streams back messages as they are received in an input stream. - internal func update( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Echo_Echo.Method.Update.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body + onResponse: handleResponse ) } } \ No newline at end of file diff --git a/Examples/echo/Sources/Subcommands/Collect.swift b/Examples/echo/Sources/Subcommands/Collect.swift index d105237c3..281ae9d35 100644 --- a/Examples/echo/Sources/Subcommands/Collect.swift +++ b/Examples/echo/Sources/Subcommands/Collect.swift @@ -30,7 +30,7 @@ struct Collect: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) @@ -39,7 +39,7 @@ struct Collect: AsyncParsableCommand { try await client.run() } - let echo = Echo_Echo_Client(wrapping: client) + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = try await echo.collect { writer in diff --git a/Examples/echo/Sources/Subcommands/Expand.swift b/Examples/echo/Sources/Subcommands/Expand.swift index 72285dc08..c30b481ef 100644 --- a/Examples/echo/Sources/Subcommands/Expand.swift +++ b/Examples/echo/Sources/Subcommands/Expand.swift @@ -30,7 +30,7 @@ struct Expand: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) @@ -39,7 +39,7 @@ struct Expand: AsyncParsableCommand { try await client.run() } - let echo = Echo_Echo_Client(wrapping: client) + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } diff --git a/Examples/echo/Sources/Subcommands/Get.swift b/Examples/echo/Sources/Subcommands/Get.swift index 242e4b0a1..9a1953628 100644 --- a/Examples/echo/Sources/Subcommands/Get.swift +++ b/Examples/echo/Sources/Subcommands/Get.swift @@ -28,7 +28,7 @@ struct Get: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) @@ -37,7 +37,7 @@ struct Get: AsyncParsableCommand { try await client.run() } - let echo = Echo_Echo_Client(wrapping: client) + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } diff --git a/Examples/echo/Sources/Subcommands/Serve.swift b/Examples/echo/Sources/Subcommands/Serve.swift index 4e12baa5d..6bbb9ea82 100644 --- a/Examples/echo/Sources/Subcommands/Serve.swift +++ b/Examples/echo/Sources/Subcommands/Serve.swift @@ -28,7 +28,7 @@ struct Serve: AsyncParsableCommand { let server = GRPCServer( transport: .http2NIOPosix( address: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ), services: [EchoService()] ) @@ -42,44 +42,40 @@ struct Serve: AsyncParsableCommand { } } -struct EchoService: Echo_Echo_ServiceProtocol { +struct EchoService: Echo_Echo.SimpleServiceProtocol { func get( - request: ServerRequest, + request: Echo_EchoRequest, context: ServerContext - ) async throws -> ServerResponse { - return ServerResponse(message: .with { $0.text = request.message.text }) + ) async throws -> Echo_EchoResponse { + return .with { $0.text = request.text } } func collect( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { - let messages = try await request.messages.reduce(into: []) { $0.append($1.text) } + ) async throws -> Echo_EchoResponse { + let messages = try await request.reduce(into: []) { $0.append($1.text) } let joined = messages.joined(separator: " ") - return ServerResponse(message: .with { $0.text = joined }) + return .with { $0.text = joined } } func expand( - request: ServerRequest, + request: Echo_EchoRequest, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - let parts = request.message.text.split(separator: " ") - let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } } - try await writer.write(contentsOf: messages) - return [:] - } + ) async throws { + let parts = request.text.split(separator: " ") + let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } } + try await response.write(contentsOf: messages) } func update( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for try await message in request.messages { - try await writer.write(.with { $0.text = message.text }) - } - return [:] + ) async throws { + for try await message in request { + try await response.write(.with { $0.text = message.text }) } } } diff --git a/Examples/echo/Sources/Subcommands/Update.swift b/Examples/echo/Sources/Subcommands/Update.swift index 2166a6aa1..8e8cb664f 100644 --- a/Examples/echo/Sources/Subcommands/Update.swift +++ b/Examples/echo/Sources/Subcommands/Update.swift @@ -30,7 +30,7 @@ struct Update: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) @@ -39,7 +39,7 @@ struct Update: AsyncParsableCommand { try await client.run() } - let echo = Echo_Echo_Client(wrapping: client) + let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { try await echo.update { writer in diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 5437aa7dc..2dd86b73e 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", exact: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ .executableTarget( diff --git a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift index bc729483a..e64bc3e95 100644 --- a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift +++ b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift @@ -23,53 +23,140 @@ import GRPCCore import GRPCProtobuf +import SwiftProtobuf +// MARK: - helloworld.Greeter + +/// Namespace containing generated types for the "helloworld.Greeter" service. internal enum Helloworld_Greeter { - internal static let descriptor = GRPCCore.ServiceDescriptor.helloworld_Greeter + /// Service descriptor for the "helloworld.Greeter" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") + /// Namespace for method metadata. internal enum Method { + /// Namespace for "SayHello" metadata. internal enum SayHello { + /// Request type for "SayHello". internal typealias Input = Helloworld_HelloRequest + /// Response type for "SayHello". internal typealias Output = Helloworld_HelloReply + /// Descriptor for "SayHello". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Helloworld_Greeter.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter"), method: "SayHello" ) } + /// Descriptors for all methods in the "helloworld.Greeter" service. internal static let descriptors: [GRPCCore.MethodDescriptor] = [ SayHello.descriptor ] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Helloworld_Greeter_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Helloworld_Greeter_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Helloworld_Greeter_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Helloworld_Greeter_Client } extension GRPCCore.ServiceDescriptor { - internal static let helloworld_Greeter = Self( - package: "helloworld", - service: "Greeter" - ) + /// Service descriptor for the "helloworld.Greeter" service. + internal static let helloworld_Greeter = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") } -/// The greeting service definition. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Helloworld_Greeter_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Sends a greeting - func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse +// MARK: helloworld.Greeter (server) + +extension Helloworld_Greeter { + /// Streaming variant of the service protocol for the "helloworld.Greeter" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A streaming request of `Helloworld_HelloRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Helloworld_HelloReply` messages. + func sayHello( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "helloworld.Greeter" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Helloworld_HelloReply` message. + func sayHello( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + } + + /// Simple service protocol for the "helloworld.Greeter" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol SimpleServiceProtocol: Helloworld_Greeter.ServiceProtocol { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A `Helloworld_HelloRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Helloworld_HelloReply` to respond with. + func sayHello( + request: Helloworld_HelloRequest, + context: GRPCCore.ServerContext + ) async throws -> Helloworld_HelloReply + } } -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Default implementation of 'registerMethods(with:)'. extension Helloworld_Greeter.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Helloworld_Greeter.Method.SayHello.descriptor, @@ -85,18 +172,7 @@ extension Helloworld_Greeter.StreamingServiceProtocol { } } -/// The greeting service definition. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Helloworld_Greeter_ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { - /// Sends a greeting - func sayHello( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse -} - -/// Partial conformance to `Helloworld_Greeter_StreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Default implementation of streaming methods from 'StreamingServiceProtocol'. extension Helloworld_Greeter.ServiceProtocol { internal func sayHello( request: GRPCCore.StreamingServerRequest, @@ -110,47 +186,168 @@ extension Helloworld_Greeter.ServiceProtocol { } } -/// The greeting service definition. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Helloworld_Greeter_ClientProtocol: Sendable { - /// Sends a greeting - func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable +// Default implementation of methods from 'ServiceProtocol'. +extension Helloworld_Greeter.SimpleServiceProtocol { + internal func sayHello( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sayHello( + request: request.message, + context: context + ), + metadata: [:] + ) + } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// MARK: helloworld.Greeter (client) + +extension Helloworld_Greeter { + /// Generated client protocol for the "helloworld.Greeter" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol ClientProtocol: Sendable { + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - serializer: A serializer for `Helloworld_HelloRequest` messages. + /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sayHello( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "helloworld.Greeter" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - serializer: A serializer for `Helloworld_HelloRequest` messages. + /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Helloworld_Greeter.Method.SayHello.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. extension Helloworld_Greeter.ClientProtocol { - internal func sayHello( + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.sayHello( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Helpers providing sugared APIs for 'ClientProtocol' methods. extension Helloworld_Greeter.ClientProtocol { - /// Sends a greeting + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func sayHello( _ message: Helloworld_HelloRequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.ClientRequest( @@ -160,37 +357,7 @@ extension Helloworld_Greeter.ClientProtocol { return try await self.sayHello( request: request, options: options, - handleResponse - ) - } -} - -/// The greeting service definition. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Helloworld_Greeter_Client: Helloworld_Greeter.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Sends a greeting - internal func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Helloworld_Greeter.Method.SayHello.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body + onResponse: handleResponse ) } } \ No newline at end of file diff --git a/Examples/hello-world/Sources/Subcommands/Greet.swift b/Examples/hello-world/Sources/Subcommands/Greet.swift index cd005534a..cf5d73a8d 100644 --- a/Examples/hello-world/Sources/Subcommands/Greet.swift +++ b/Examples/hello-world/Sources/Subcommands/Greet.swift @@ -33,7 +33,7 @@ struct Greet: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) @@ -45,7 +45,7 @@ struct Greet: AsyncParsableCommand { client.beginGracefulShutdown() } - let greeter = Helloworld_Greeter_Client(wrapping: client) + let greeter = Helloworld_Greeter.Client(wrapping: client) let reply = try await greeter.sayHello(.with { $0.name = self.name }) print(reply.message) } diff --git a/Examples/hello-world/Sources/Subcommands/Serve.swift b/Examples/hello-world/Sources/Subcommands/Serve.swift index d65483865..caf801ae4 100644 --- a/Examples/hello-world/Sources/Subcommands/Serve.swift +++ b/Examples/hello-world/Sources/Subcommands/Serve.swift @@ -29,7 +29,7 @@ struct Serve: AsyncParsableCommand { let server = GRPCServer( transport: .http2NIOPosix( address: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ), services: [Greeter()] ) @@ -43,14 +43,14 @@ struct Serve: AsyncParsableCommand { } } -struct Greeter: Helloworld_Greeter_ServiceProtocol { +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { func sayHello( - request: ServerRequest, + request: Helloworld_HelloRequest, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Helloworld_HelloReply { var reply = Helloworld_HelloReply() - let recipient = request.message.name.isEmpty ? "stranger" : request.message.name + let recipient = request.name.isEmpty ? "stranger" : request.name reply.message = "Hello, \(recipient)" - return ServerResponse(message: reply) + return reply } } diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index 7aba9a836..fa2554ed7 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", exact: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), - .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ .executableTarget( diff --git a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift index 2468fd7d7..7971c54e1 100644 --- a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift +++ b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift @@ -23,42 +23,65 @@ import GRPCCore import GRPCProtobuf +import SwiftProtobuf +// MARK: - routeguide.RouteGuide + +/// Namespace containing generated types for the "routeguide.RouteGuide" service. internal enum Routeguide_RouteGuide { - internal static let descriptor = GRPCCore.ServiceDescriptor.routeguide_RouteGuide + /// Service descriptor for the "routeguide.RouteGuide" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide") + /// Namespace for method metadata. internal enum Method { + /// Namespace for "GetFeature" metadata. internal enum GetFeature { + /// Request type for "GetFeature". internal typealias Input = Routeguide_Point + /// Response type for "GetFeature". internal typealias Output = Routeguide_Feature + /// Descriptor for "GetFeature". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), method: "GetFeature" ) } + /// Namespace for "ListFeatures" metadata. internal enum ListFeatures { + /// Request type for "ListFeatures". internal typealias Input = Routeguide_Rectangle + /// Response type for "ListFeatures". internal typealias Output = Routeguide_Feature + /// Descriptor for "ListFeatures". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), method: "ListFeatures" ) } + /// Namespace for "RecordRoute" metadata. internal enum RecordRoute { + /// Request type for "RecordRoute". internal typealias Input = Routeguide_Point + /// Response type for "RecordRoute". internal typealias Output = Routeguide_RouteSummary + /// Descriptor for "RecordRoute". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), method: "RecordRoute" ) } + /// Namespace for "RouteChat" metadata. internal enum RouteChat { + /// Request type for "RouteChat". internal typealias Input = Routeguide_RouteNote + /// Response type for "RouteChat". internal typealias Output = Routeguide_RouteNote + /// Descriptor for "RouteChat". internal static let descriptor = GRPCCore.MethodDescriptor( - service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), method: "RouteChat" ) } + /// Descriptors for all methods in the "routeguide.RouteGuide" service. internal static let descriptors: [GRPCCore.MethodDescriptor] = [ GetFeature.descriptor, ListFeatures.descriptor, @@ -66,71 +89,325 @@ internal enum Routeguide_RouteGuide { RouteChat.descriptor ] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Routeguide_RouteGuide_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Routeguide_RouteGuide_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Routeguide_RouteGuide_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Routeguide_RouteGuide_Client } extension GRPCCore.ServiceDescriptor { - internal static let routeguide_RouteGuide = Self( - package: "routeguide", - service: "RouteGuide" - ) + /// Service descriptor for the "routeguide.RouteGuide" service. + internal static let routeguide_RouteGuide = GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide") } -/// Interface exported by the server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Routeguide_RouteGuide_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// A simple RPC. +// MARK: routeguide.RouteGuide (server) + +extension Routeguide_RouteGuide { + /// Streaming variant of the service protocol for the "routeguide.RouteGuide" service. /// - /// Obtains the feature at a given position. + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. - func getFeature( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// A server-to-client streaming RPC. + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. - func listFeatures( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// A client-to-server streaming RPC. + /// > Source IDL Documentation: + /// > + /// > Interface exported by the server. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "GetFeature" method. + /// + /// > Source IDL Documentation: + /// > + /// > A simple RPC. + /// > + /// > Obtains the feature at a given position. + /// > + /// > A feature with an empty name is returned if there's no feature at the given + /// > position. + /// + /// - Parameters: + /// - request: A streaming request of `Routeguide_Point` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Routeguide_Feature` messages. + func getFeature( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "ListFeatures" method. + /// + /// > Source IDL Documentation: + /// > + /// > A server-to-client streaming RPC. + /// > + /// > Obtains the Features available within the given Rectangle. Results are + /// > streamed rather than returned at once (e.g. in a response message with a + /// > repeated field), as the rectangle may cover a large area and contain a + /// > huge number of features. + /// + /// - Parameters: + /// - request: A streaming request of `Routeguide_Rectangle` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Routeguide_Feature` messages. + func listFeatures( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "RecordRoute" method. + /// + /// > Source IDL Documentation: + /// > + /// > A client-to-server streaming RPC. + /// > + /// > Accepts a stream of Points on a route being traversed, returning a + /// > RouteSummary when traversal is completed. + /// + /// - Parameters: + /// - request: A streaming request of `Routeguide_Point` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Routeguide_RouteSummary` messages. + func recordRoute( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "RouteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > A Bidirectional streaming RPC. + /// > + /// > Accepts a stream of RouteNotes sent while a route is being traversed, + /// > while receiving other RouteNotes (e.g. from other users). + /// + /// - Parameters: + /// - request: A streaming request of `Routeguide_RouteNote` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Routeguide_RouteNote` messages. + func routeChat( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "routeguide.RouteGuide" service. /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. - func recordRoute( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// A Bidirectional streaming RPC. + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). - func routeChat( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse + /// > Source IDL Documentation: + /// > + /// > Interface exported by the server. + internal protocol ServiceProtocol: Routeguide_RouteGuide.StreamingServiceProtocol { + /// Handle the "GetFeature" method. + /// + /// > Source IDL Documentation: + /// > + /// > A simple RPC. + /// > + /// > Obtains the feature at a given position. + /// > + /// > A feature with an empty name is returned if there's no feature at the given + /// > position. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Point` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Routeguide_Feature` message. + func getFeature( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "ListFeatures" method. + /// + /// > Source IDL Documentation: + /// > + /// > A server-to-client streaming RPC. + /// > + /// > Obtains the Features available within the given Rectangle. Results are + /// > streamed rather than returned at once (e.g. in a response message with a + /// > repeated field), as the rectangle may cover a large area and contain a + /// > huge number of features. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Rectangle` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Routeguide_Feature` messages. + func listFeatures( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "RecordRoute" method. + /// + /// > Source IDL Documentation: + /// > + /// > A client-to-server streaming RPC. + /// > + /// > Accepts a stream of Points on a route being traversed, returning a + /// > RouteSummary when traversal is completed. + /// + /// - Parameters: + /// - request: A streaming request of `Routeguide_Point` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Routeguide_RouteSummary` message. + func recordRoute( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "RouteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > A Bidirectional streaming RPC. + /// > + /// > Accepts a stream of RouteNotes sent while a route is being traversed, + /// > while receiving other RouteNotes (e.g. from other users). + /// + /// - Parameters: + /// - request: A streaming request of `Routeguide_RouteNote` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Routeguide_RouteNote` messages. + func routeChat( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Simple service protocol for the "routeguide.RouteGuide" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Interface exported by the server. + internal protocol SimpleServiceProtocol: Routeguide_RouteGuide.ServiceProtocol { + /// Handle the "GetFeature" method. + /// + /// > Source IDL Documentation: + /// > + /// > A simple RPC. + /// > + /// > Obtains the feature at a given position. + /// > + /// > A feature with an empty name is returned if there's no feature at the given + /// > position. + /// + /// - Parameters: + /// - request: A `Routeguide_Point` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Routeguide_Feature` to respond with. + func getFeature( + request: Routeguide_Point, + context: GRPCCore.ServerContext + ) async throws -> Routeguide_Feature + + /// Handle the "ListFeatures" method. + /// + /// > Source IDL Documentation: + /// > + /// > A server-to-client streaming RPC. + /// > + /// > Obtains the Features available within the given Rectangle. Results are + /// > streamed rather than returned at once (e.g. in a response message with a + /// > repeated field), as the rectangle may cover a large area and contain a + /// > huge number of features. + /// + /// - Parameters: + /// - request: A `Routeguide_Rectangle` message. + /// - response: A response stream of `Routeguide_Feature` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func listFeatures( + request: Routeguide_Rectangle, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "RecordRoute" method. + /// + /// > Source IDL Documentation: + /// > + /// > A client-to-server streaming RPC. + /// > + /// > Accepts a stream of Points on a route being traversed, returning a + /// > RouteSummary when traversal is completed. + /// + /// - Parameters: + /// - request: A stream of `Routeguide_Point` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Routeguide_RouteSummary` to respond with. + func recordRoute( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> Routeguide_RouteSummary + + /// Handle the "RouteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > A Bidirectional streaming RPC. + /// > + /// > Accepts a stream of RouteNotes sent while a route is being traversed, + /// > while receiving other RouteNotes (e.g. from other users). + /// + /// - Parameters: + /// - request: A stream of `Routeguide_RouteNote` messages. + /// - response: A response stream of `Routeguide_RouteNote` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func routeChat( + request: GRPCCore.RPCAsyncSequence, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + } } -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Default implementation of 'registerMethods(with:)'. extension Routeguide_RouteGuide.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: Routeguide_RouteGuide.Method.GetFeature.descriptor, @@ -179,52 +456,7 @@ extension Routeguide_RouteGuide.StreamingServiceProtocol { } } -/// Interface exported by the server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Routeguide_RouteGuide_ServiceProtocol: Routeguide_RouteGuide.StreamingServiceProtocol { - /// A simple RPC. - /// - /// Obtains the feature at a given position. - /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. - func getFeature( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// A server-to-client streaming RPC. - /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. - func listFeatures( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// A client-to-server streaming RPC. - /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. - func recordRoute( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// A Bidirectional streaming RPC. - /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). - func routeChat( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse -} - -/// Partial conformance to `Routeguide_RouteGuide_StreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Default implementation of streaming methods from 'StreamingServiceProtocol'. extension Routeguide_RouteGuide.ServiceProtocol { internal func getFeature( request: GRPCCore.StreamingServerRequest, @@ -236,7 +468,7 @@ extension Routeguide_RouteGuide.ServiceProtocol { ) return GRPCCore.StreamingServerResponse(single: response) } - + internal func listFeatures( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext @@ -247,7 +479,7 @@ extension Routeguide_RouteGuide.ServiceProtocol { ) return response } - + internal func recordRoute( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext @@ -260,139 +492,518 @@ extension Routeguide_RouteGuide.ServiceProtocol { } } -/// Interface exported by the server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Routeguide_RouteGuide_ClientProtocol: Sendable { - /// A simple RPC. - /// - /// Obtains the feature at a given position. +// Default implementation of methods from 'ServiceProtocol'. +extension Routeguide_RouteGuide.SimpleServiceProtocol { + internal func getFeature( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.getFeature( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func listFeatures( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.listFeatures( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func recordRoute( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.recordRoute( + request: request.messages, + context: context + ), + metadata: [:] + ) + } + + internal func routeChat( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.routeChat( + request: request.messages, + response: writer, + context: context + ) + return [:] + } + ) + } +} + +// MARK: routeguide.RouteGuide (client) + +extension Routeguide_RouteGuide { + /// Generated client protocol for the "routeguide.RouteGuide" service. /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. - func getFeature( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// A server-to-client streaming RPC. + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. - func listFeatures( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// A client-to-server streaming RPC. + /// > Source IDL Documentation: + /// > + /// > Interface exported by the server. + internal protocol ClientProtocol: Sendable { + /// Call the "GetFeature" method. + /// + /// > Source IDL Documentation: + /// > + /// > A simple RPC. + /// > + /// > Obtains the feature at a given position. + /// > + /// > A feature with an empty name is returned if there's no feature at the given + /// > position. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Point` message. + /// - serializer: A serializer for `Routeguide_Point` messages. + /// - deserializer: A deserializer for `Routeguide_Feature` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func getFeature( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "ListFeatures" method. + /// + /// > Source IDL Documentation: + /// > + /// > A server-to-client streaming RPC. + /// > + /// > Obtains the Features available within the given Rectangle. Results are + /// > streamed rather than returned at once (e.g. in a response message with a + /// > repeated field), as the rectangle may cover a large area and contain a + /// > huge number of features. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Rectangle` message. + /// - serializer: A serializer for `Routeguide_Rectangle` messages. + /// - deserializer: A deserializer for `Routeguide_Feature` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func listFeatures( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "RecordRoute" method. + /// + /// > Source IDL Documentation: + /// > + /// > A client-to-server streaming RPC. + /// > + /// > Accepts a stream of Points on a route being traversed, returning a + /// > RouteSummary when traversal is completed. + /// + /// - Parameters: + /// - request: A streaming request producing `Routeguide_Point` messages. + /// - serializer: A serializer for `Routeguide_Point` messages. + /// - deserializer: A deserializer for `Routeguide_RouteSummary` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func recordRoute( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "RouteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > A Bidirectional streaming RPC. + /// > + /// > Accepts a stream of RouteNotes sent while a route is being traversed, + /// > while receiving other RouteNotes (e.g. from other users). + /// + /// - Parameters: + /// - request: A streaming request producing `Routeguide_RouteNote` messages. + /// - serializer: A serializer for `Routeguide_RouteNote` messages. + /// - deserializer: A deserializer for `Routeguide_RouteNote` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func routeChat( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "routeguide.RouteGuide" service. /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. - func recordRoute( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - - /// A Bidirectional streaming RPC. + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). - func routeChat( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable + /// > Source IDL Documentation: + /// > + /// > Interface exported by the server. + internal struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "GetFeature" method. + /// + /// > Source IDL Documentation: + /// > + /// > A simple RPC. + /// > + /// > Obtains the feature at a given position. + /// > + /// > A feature with an empty name is returned if there's no feature at the given + /// > position. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Point` message. + /// - serializer: A serializer for `Routeguide_Point` messages. + /// - deserializer: A deserializer for `Routeguide_Feature` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getFeature( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Routeguide_RouteGuide.Method.GetFeature.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "ListFeatures" method. + /// + /// > Source IDL Documentation: + /// > + /// > A server-to-client streaming RPC. + /// > + /// > Obtains the Features available within the given Rectangle. Results are + /// > streamed rather than returned at once (e.g. in a response message with a + /// > repeated field), as the rectangle may cover a large area and contain a + /// > huge number of features. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Rectangle` message. + /// - serializer: A serializer for `Routeguide_Rectangle` messages. + /// - deserializer: A deserializer for `Routeguide_Feature` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func listFeatures( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Routeguide_RouteGuide.Method.ListFeatures.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "RecordRoute" method. + /// + /// > Source IDL Documentation: + /// > + /// > A client-to-server streaming RPC. + /// > + /// > Accepts a stream of Points on a route being traversed, returning a + /// > RouteSummary when traversal is completed. + /// + /// - Parameters: + /// - request: A streaming request producing `Routeguide_Point` messages. + /// - serializer: A serializer for `Routeguide_Point` messages. + /// - deserializer: A deserializer for `Routeguide_RouteSummary` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func recordRoute( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Routeguide_RouteGuide.Method.RecordRoute.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "RouteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > A Bidirectional streaming RPC. + /// > + /// > Accepts a stream of RouteNotes sent while a route is being traversed, + /// > while receiving other RouteNotes (e.g. from other users). + /// + /// - Parameters: + /// - request: A streaming request producing `Routeguide_RouteNote` messages. + /// - serializer: A serializer for `Routeguide_RouteNote` messages. + /// - deserializer: A deserializer for `Routeguide_RouteNote` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func routeChat( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Routeguide_RouteGuide.Method.RouteChat.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Helpers providing default arguments to 'ClientProtocol' methods. extension Routeguide_RouteGuide.ClientProtocol { - internal func getFeature( + /// Call the "GetFeature" method. + /// + /// > Source IDL Documentation: + /// > + /// > A simple RPC. + /// > + /// > Obtains the feature at a given position. + /// > + /// > A feature with an empty name is returned if there's no feature at the given + /// > position. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Point` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func getFeature( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.getFeature( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } - - internal func listFeatures( + + /// Call the "ListFeatures" method. + /// + /// > Source IDL Documentation: + /// > + /// > A server-to-client streaming RPC. + /// > + /// > Obtains the Features available within the given Rectangle. Results are + /// > streamed rather than returned at once (e.g. in a response message with a + /// > repeated field), as the rectangle may cover a large area and contain a + /// > huge number of features. + /// + /// - Parameters: + /// - request: A request containing a single `Routeguide_Rectangle` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func listFeatures( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.listFeatures( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } - - internal func recordRoute( + + /// Call the "RecordRoute" method. + /// + /// > Source IDL Documentation: + /// > + /// > A client-to-server streaming RPC. + /// > + /// > Accepts a stream of Points on a route being traversed, returning a + /// > RouteSummary when traversal is completed. + /// + /// - Parameters: + /// - request: A streaming request producing `Routeguide_Point` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func recordRoute( request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.recordRoute( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } - - internal func routeChat( + + /// Call the "RouteChat" method. + /// + /// > Source IDL Documentation: + /// > + /// > A Bidirectional streaming RPC. + /// > + /// > Accepts a stream of RouteNotes sent while a route is being traversed, + /// > while receiving other RouteNotes (e.g. from other users). + /// + /// - Parameters: + /// - request: A streaming request producing `Routeguide_RouteNote` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func routeChat( request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.routeChat( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +// Helpers providing sugared APIs for 'ClientProtocol' methods. extension Routeguide_RouteGuide.ClientProtocol { - /// A simple RPC. + /// Call the "GetFeature" method. /// - /// Obtains the feature at a given position. + /// > Source IDL Documentation: + /// > + /// > A simple RPC. + /// > + /// > Obtains the feature at a given position. + /// > + /// > A feature with an empty name is returned if there's no feature at the given + /// > position. /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func getFeature( _ message: Routeguide_Point, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.ClientRequest( @@ -402,16 +1013,29 @@ extension Routeguide_RouteGuide.ClientProtocol { return try await self.getFeature( request: request, options: options, - handleResponse + onResponse: handleResponse ) } - - /// A server-to-client streaming RPC. + + /// Call the "ListFeatures" method. + /// + /// > Source IDL Documentation: + /// > + /// > A server-to-client streaming RPC. + /// > + /// > Obtains the Features available within the given Rectangle. Results are + /// > streamed rather than returned at once (e.g. in a response message with a + /// > repeated field), as the rectangle may cover a large area and contain a + /// > huge number of features. /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func listFeatures( _ message: Routeguide_Rectangle, metadata: GRPCCore.Metadata = [:], @@ -425,153 +1049,79 @@ extension Routeguide_RouteGuide.ClientProtocol { return try await self.listFeatures( request: request, options: options, - handleResponse + onResponse: handleResponse ) } - - /// A client-to-server streaming RPC. + + /// Call the "RecordRoute" method. + /// + /// > Source IDL Documentation: + /// > + /// > A client-to-server streaming RPC. + /// > + /// > Accepts a stream of Points on a route being traversed, returning a + /// > RouteSummary when traversal is completed. /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func recordRoute( metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.StreamingClientRequest( metadata: metadata, - producer: requestProducer + producer: producer ) return try await self.recordRoute( request: request, options: options, - handleResponse + onResponse: handleResponse ) } - - /// A Bidirectional streaming RPC. + + /// Call the "RouteChat" method. /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). + /// > Source IDL Documentation: + /// > + /// > A Bidirectional streaming RPC. + /// > + /// > Accepts a stream of RouteNotes sent while a route is being traversed, + /// > while receiving other RouteNotes (e.g. from other users). + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. internal func routeChat( metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { let request = GRPCCore.StreamingClientRequest( metadata: metadata, - producer: requestProducer + producer: producer ) return try await self.routeChat( request: request, options: options, - handleResponse - ) - } -} - -/// Interface exported by the server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Routeguide_RouteGuide_Client: Routeguide_RouteGuide.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// A simple RPC. - /// - /// Obtains the feature at a given position. - /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. - internal func getFeature( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Routeguide_RouteGuide.Method.GetFeature.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// A server-to-client streaming RPC. - /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. - internal func listFeatures( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Routeguide_RouteGuide.Method.ListFeatures.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// A client-to-server streaming RPC. - /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. - internal func recordRoute( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Routeguide_RouteGuide.Method.RecordRoute.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// A Bidirectional streaming RPC. - /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). - internal func routeChat( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Routeguide_RouteGuide.Method.RouteChat.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body + onResponse: handleResponse ) } } \ No newline at end of file diff --git a/Examples/route-guide/Sources/Subcommands/GetFeature.swift b/Examples/route-guide/Sources/Subcommands/GetFeature.swift index 890d432cd..eb58eeff3 100644 --- a/Examples/route-guide/Sources/Subcommands/GetFeature.swift +++ b/Examples/route-guide/Sources/Subcommands/GetFeature.swift @@ -39,7 +39,7 @@ struct GetFeature: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) let client = GRPCClient(transport: transport) @@ -48,7 +48,7 @@ struct GetFeature: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) let point = Routeguide_Point.with { $0.latitude = self.latitude diff --git a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift index e2d1a5f5c..32db2d0c1 100644 --- a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift +++ b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift @@ -53,7 +53,7 @@ struct ListFeatures: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) let client = GRPCClient(transport: transport) @@ -62,7 +62,7 @@ struct ListFeatures: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) let boundingRectangle = Routeguide_Rectangle.with { $0.lo.latitude = self.minLatitude $0.hi.latitude = self.maxLatitude diff --git a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift index e6d1611f1..bc3f4f9dc 100644 --- a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift +++ b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift @@ -32,7 +32,7 @@ struct RecordRoute: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) let client = GRPCClient(transport: transport) @@ -41,7 +41,7 @@ struct RecordRoute: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) // Get all features. let rectangle = Routeguide_Rectangle.with { diff --git a/Examples/route-guide/Sources/Subcommands/RouteChat.swift b/Examples/route-guide/Sources/Subcommands/RouteChat.swift index d25352662..2a4c23b16 100644 --- a/Examples/route-guide/Sources/Subcommands/RouteChat.swift +++ b/Examples/route-guide/Sources/Subcommands/RouteChat.swift @@ -32,7 +32,7 @@ struct RouteChat: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) let client = GRPCClient(transport: transport) @@ -41,7 +41,7 @@ struct RouteChat: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) try await routeGuide.routeChat { writer in let notes: [(String, (Int32, Int32))] = [ diff --git a/Examples/route-guide/Sources/Subcommands/Serve.swift b/Examples/route-guide/Sources/Subcommands/Serve.swift index 8651346d4..066d8e396 100644 --- a/Examples/route-guide/Sources/Subcommands/Serve.swift +++ b/Examples/route-guide/Sources/Subcommands/Serve.swift @@ -40,7 +40,7 @@ struct Serve: AsyncParsableCommand { let features = try self.loadFeatures() let transport = HTTP2ServerTransport.Posix( address: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) let server = GRPCServer(transport: transport, services: [RouteGuideService(features: features)]) @@ -99,56 +99,54 @@ struct RouteGuideService { } } -extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +extension RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { let feature = self.findFeature( - latitude: request.message.latitude, - longitude: request.message.longitude + latitude: request.latitude, + longitude: request.longitude ) if let feature { - return ServerResponse(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { $0.name = "" $0.location = .with { - $0.latitude = request.message.latitude - $0.longitude = request.message.longitude + $0.latitude = request.latitude + $0.longitude = request.longitude } } - return ServerResponse(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - let featuresWithinBounds = self.features.filter { feature in - !feature.name.isEmpty && feature.isContained(by: request.message) - } - - try await writer.write(contentsOf: featuresWithinBounds) - return [:] + ) async throws { + let featuresWithinBounds = self.features.filter { feature in + !feature.name.isEmpty && feature.isContained(by: request) } + + try await response.write(contentsOf: featuresWithinBounds) } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 var distanceTravelled = 0.0 var previousPoint: Routeguide_Point? = nil - for try await point in request.messages { + for try await point in request { pointsVisited += 1 if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { @@ -170,19 +168,17 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse(message: summary) + return summary } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for try await note in request.messages { - let notes = self.receivedNotes.recordNote(note) - try await writer.write(contentsOf: notes) - } - return [:] + ) async throws { + for try await note in request { + let notes = self.receivedNotes.recordNote(note) + try await response.write(contentsOf: notes) } } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index e3e4241a4..6a056d47f 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -18,7 +18,7 @@ repository by running the following command in a terminal: ```console - git clone https://github.com/grpc/grpc-swift + git clone --branch 2.0.0-beta.1 https://github.com/grpc/grpc-swift ``` You then need to change directory to the `Examples/hello-world` directory of the cloned diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift index b4b20841a..c7ab21adc 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift @@ -1,11 +1,11 @@ -struct Greeter: Helloworld_GreeterServiceProtocol { +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { func sayHello( - request: ServerRequest, + request: Helloworld_HelloRequest, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Helloworld_HelloReply { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello, \(recipient)" - return ServerResponse(message: reply) + return reply } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift index 9ecccd975..a2e4fba28 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift @@ -1,21 +1,21 @@ -struct Greeter: Helloworld_GreeterServiceProtocol { +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { func sayHello( - request: ServerRequest, + request: Helloworld_HelloRequest, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Helloworld_HelloReply { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello, \(recipient)" - return ServerResponse(message: reply) + return reply } func sayHelloAgain( - request: ServerRequest, + request: Helloworld_HelloRequest, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Helloworld_HelloReply { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello again, \(recipient)" - return ServerResponse(message: reply) + return reply } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift index 4e10d4adc..23b644f0c 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift @@ -1,3 +1,3 @@ -let greeter = Helloworld_GreeterClient(wrapping: client) +let greeter = Helloworld_Greeter.Client(wrapping: client) let reply = try await greeter.sayHello(.with { $0.name = self.name }) print(reply.message) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift index 534a4fb8a..34fb040f1 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift @@ -1,4 +1,4 @@ -let greeter = Helloworld_GreeterClient(wrapping: client) +let greeter = Helloworld_Greeter.Client(wrapping: client) let reply = try await greeter.sayHello(.with { $0.name = self.name }) print(reply.message) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift index f475a700c..d7116586e 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift @@ -5,8 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), ], targets: [] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift index 65a51d4b9..462edb8d6 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -5,8 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift index 65aa33cb2..141e47735 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift @@ -1,4 +1,4 @@ import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift index 283c9a8fb..bd5d7724c 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift @@ -1,27 +1,29 @@ import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift index 84ce31e89..f66682fb0 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift @@ -1,6 +1,6 @@ import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { /// Known features. private let features: [Routeguide_Feature] @@ -11,26 +11,28 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift index cf1b88e21..75a19a7ec 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift @@ -1,6 +1,6 @@ import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { /// Known features. private let features: [Routeguide_Feature] @@ -18,9 +18,9 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude @@ -28,20 +28,22 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift index dcbde29fa..d23cc14e2 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift @@ -1,6 +1,6 @@ import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { /// Known features. private let features: [Routeguide_Feature] @@ -18,16 +18,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -37,25 +37,27 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift index 779857268..c912c5f58 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift @@ -1,6 +1,6 @@ import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { /// Known features. private let features: [Routeguide_Feature] @@ -18,16 +18,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -37,33 +37,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for feature in self.features { - if !feature.name.isEmpty, feature.isContained(by: request.message) { - try await writer.write(feature) - } + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } } } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift deleted file mode 100644 index 8fe80e2dd..000000000 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift +++ /dev/null @@ -1,79 +0,0 @@ -import GRPCCore - -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { - /// Known features. - private let features: [Routeguide_Feature] - - /// Creates a new route guide service. - /// - Parameter features: Known features. - init(features: [Routeguide_Feature]) { - self.features = features - } - - /// Returns the first feature found at the given location, if one exists. - private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { - self.features.first { - $0.location.latitude == latitude && $0.location.longitude == longitude - } - } - - func getFeature( - request: ServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - let feature = self.findFeature( - latitude: request.message.latitude, - longitude: request.message.longitude - ) - - if let feature { - return ServerResponse(message: feature) - } else { - // No feature: return a feature with an empty name. - let unknownFeature = Routeguide_Feature.with { - $0.name = "" - $0.location = .with { - $0.latitude = request.message.latitude - $0.longitude = request.message.longitude - } - } - return ServerResponse(message: unknownFeature) - } - } - - func listFeatures( - request: ServerRequest, - context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for feature in self.features { - if !feature.name.isEmpty, feature.isContained(by: request.message) { - try await writer.write(feature) - } - } - - return [:] - } - } - - func recordRoute( - request: StreamingServerRequest, - context: ServerContext - ) async throws -> ServerResponse { - } - - func routeChat( - request: StreamingServerRequest, - context: ServerContext - ) async throws -> StreamingServerResponse { - } -} - -extension Routeguide_Feature { - func isContained(by rectangle: Routeguide_Rectangle) -> Bool { - return rectangle.lo.latitude <= self.location.latitude - && self.location.latitude <= rectangle.hi.latitude - && rectangle.lo.longitude <= self.location.longitude - && self.location.longitude <= rectangle.hi.longitude - } -} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift index 2004a8ddc..72032cfbb 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift @@ -1,7 +1,7 @@ import Foundation import GRPCCore -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { /// Known features. private let features: [Routeguide_Feature] @@ -19,16 +19,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -38,36 +38,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for feature in self.features { - if !feature.name.isEmpty, feature.isContained(by: request.message) { - try await writer.write(feature) - } + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } - - return [:] } } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 var distanceTravelled = 0.0 var previousPoint: Routeguide_Point? = nil - for try await point in request.messages { + for try await point in request { pointsVisited += 1 if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { @@ -89,13 +86,14 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse(message: summary) + return summary } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift index d48385753..96ecea323 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift @@ -2,44 +2,14 @@ import Foundation import GRPCCore import Synchronization -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { /// Known features. private let features: [Routeguide_Feature] - /// Notes recorded by clients. - private let receivedNotes: Notes - - /// A thread-safe store for notes sent by clients. - private final class Notes: Sendable { - private let notes: Mutex<[Routeguide_RouteNote]> - - init() { - self.notes = Mutex([]) - } - - /// Records a note and returns all other notes recorded at the same location. - /// - /// - Parameter receivedNote: A note to record. - /// - Returns: Other notes recorded at the same location. - func recordNote(_ receivedNote: Routeguide_RouteNote) -> [Routeguide_RouteNote] { - return self.notes.withLock { notes in - var notesFromSameLocation: [Routeguide_RouteNote] = [] - for note in notes { - if note.location == receivedNote.location { - notesFromSameLocation.append(note) - } - } - notes.append(receivedNote) - return notesFromSameLocation - } - } - } - /// Creates a new route guide service. /// - Parameter features: Known features. init(features: [Routeguide_Feature]) { self.features = features - self.receivedNotes = Notes() } /// Returns the first feature found at the given location, if one exists. @@ -50,16 +20,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -69,36 +39,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for feature in self.features { - if !feature.name.isEmpty, feature.isContained(by: request.message) { - try await writer.write(feature) - } + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } - - return [:] } } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 var distanceTravelled = 0.0 var previousPoint: Routeguide_Point? = nil - for try await point in request.messages { + for try await point in request { pointsVisited += 1 if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { @@ -120,13 +87,14 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse(message: summary) + return summary } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { + ) async throws { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift index eb2b88a78..452e1ad7f 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift @@ -2,44 +2,14 @@ import Foundation import GRPCCore import Synchronization -struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +struct RouteGuideService: Routeguide_RouteGuide.SimpleServiceProtocol { /// Known features. private let features: [Routeguide_Feature] - /// Notes recorded by clients. - private let receivedNotes: Notes - - /// A thread-safe store for notes sent by clients. - private final class Notes: Sendable { - private let notes: Mutex<[Routeguide_RouteNote]> - - init() { - self.notes = Mutex([]) - } - - /// Records a note and returns all other notes recorded at the same location. - /// - /// - Parameter receivedNote: A note to record. - /// - Returns: Other notes recorded at the same location. - func recordNote(_ receivedNote: Routeguide_RouteNote) -> [Routeguide_RouteNote] { - return self.notes.withLock { notes in - var notesFromSameLocation: [Routeguide_RouteNote] = [] - for note in notes { - if note.location == receivedNote.location { - notesFromSameLocation.append(note) - } - } - notes.append(receivedNote) - return notesFromSameLocation - } - } - } - /// Creates a new route guide service. /// - Parameter features: Known features. init(features: [Routeguide_Feature]) { self.features = features - self.receivedNotes = Notes() } /// Returns the first feature found at the given location, if one exists. @@ -50,16 +20,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest, + request: Routeguide_Point, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_Feature { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse(message: feature) + return feature } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -69,36 +39,33 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse(message: unknownFeature) + return unknownFeature } } func listFeatures( - request: ServerRequest, + request: Routeguide_Rectangle, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for feature in self.features { - if !feature.name.isEmpty, feature.isContained(by: request.message) { - try await writer.write(feature) - } + ) async throws { + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request) { + try await response.write(feature) } - - return [:] } } func recordRoute( - request: StreamingServerRequest, + request: RPCAsyncSequence, context: ServerContext - ) async throws -> ServerResponse { + ) async throws -> Routeguide_RouteSummary { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 var distanceTravelled = 0.0 var previousPoint: Routeguide_Point? = nil - for try await point in request.messages { + for try await point in request { pointsVisited += 1 if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { @@ -120,19 +87,17 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse(message: summary) + return summary } func routeChat( - request: StreamingServerRequest, + request: RPCAsyncSequence, + response: RPCWriter, context: ServerContext - ) async throws -> StreamingServerResponse { - return StreamingServerResponse { writer in - for try await note in request.messages { - let notes = self.receivedNotes.recordNote(note) - try await writer.write(contentsOf: notes) - } - return [:] + ) async throws { + for try await note in request { + let notes = self.receivedNotes.recordNote(note) + try await response.write(contentsOf: notes) } } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index 65a51d4b9..d5526b86f 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -5,8 +5,8 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index e5f9b1088..8b4629476 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -5,8 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index 1e8ef47e1..bb1937d02 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -5,8 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift index a0ba287f4..c6b0359fc 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift @@ -8,7 +8,7 @@ extension RouteGuide { let server = GRPCServer( transport: .http2NIOPosix( address: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ), services: [routeGuide] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift index 6502dee78..c5401b149 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift @@ -8,7 +8,7 @@ extension RouteGuide { let server = GRPCServer( transport: .http2NIOPosix( address: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ), services: [routeGuide] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift index ef8bc14ae..bf1dbaebc 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift @@ -6,7 +6,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift index f87f8cc80..52c4c79e5 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift @@ -6,7 +6,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift index 15322b30f..9b8b5ee3b 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift @@ -6,12 +6,12 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) async let _ = client.run() - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift index 04f75c699..b2b103b20 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift @@ -6,17 +6,17 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) async let _ = client.run() - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) try await self.getFeature(using: routeGuide) } - private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'GetFeature'") let point = Routeguide_Point.with { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift index adc6a25f4..609ce0ead 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift @@ -6,18 +6,18 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) async let _ = client.run() - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) try await self.getFeature(using: routeGuide) try await self.listFeatures(using: routeGuide) } - private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'GetFeature'") let point = Routeguide_Point.with { @@ -29,7 +29,7 @@ extension RouteGuide { print("Got feature '\(feature.name)'") } - private func listFeatures(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func listFeatures(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'ListFeatures'") let boundingRectangle = Routeguide_Rectangle.with { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift index 448eaa0ac..9fb6490c8 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift @@ -6,19 +6,19 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) async let _ = client.run() - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) try await self.getFeature(using: routeGuide) try await self.listFeatures(using: routeGuide) try await self.recordRoute(using: routeGuide) } - private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'GetFeature'") let point = Routeguide_Point.with { @@ -30,7 +30,7 @@ extension RouteGuide { print("Got feature '\(feature.name)'") } - private func listFeatures(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func listFeatures(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'ListFeatures'") let boundingRectangle = Routeguide_Rectangle.with { @@ -53,7 +53,7 @@ extension RouteGuide { } } - private func recordRoute(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func recordRoute(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'RecordRoute'") let features = try self.loadFeatures() diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift index 12f75d83d..9f1afac60 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift @@ -6,20 +6,20 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults(transportSecurity: .plaintext) + transportSecurity: .plaintext ) ) async let _ = client.run() - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) try await self.getFeature(using: routeGuide) try await self.listFeatures(using: routeGuide) try await self.recordRoute(using: routeGuide) try await self.routeChat(using: routeGuide) } - private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'GetFeature'") let point = Routeguide_Point.with { @@ -31,7 +31,7 @@ extension RouteGuide { print("Got feature '\(feature.name)'") } - private func listFeatures(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func listFeatures(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'ListFeatures'") let boundingRectangle = Routeguide_Rectangle.with { @@ -54,7 +54,7 @@ extension RouteGuide { } } - private func recordRoute(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func recordRoute(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'RecordRoute'") let features = try self.loadFeatures() @@ -76,7 +76,7 @@ extension RouteGuide { ) } - private func routeChat(using routeGuide: Routeguide_RouteGuideClient) async throws { + private func routeChat(using routeGuide: Routeguide_RouteGuide.Client) async throws { print("→ Calling 'RouteChat'") try await routeGuide.routeChat { writer in diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial index 2d9192efc..3491b521a 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -65,7 +65,7 @@ @Step { We need to add a dependency on the gRPC Swift and Swift Protobuf packages. As gRPC Swift v2 - hasn't yet been released the dependencies must use the `-alpha` tags. + hasn't yet been released the dependencies must use the `-beta` tags. Note that we also add `.macOS(.v15)` to platforms, this is the earliest macOS version supported by gRPC Swift v2. @@ -192,9 +192,6 @@ Run `protoc` again to generate the service code. This will create `Sources/Generated/route_guide.grpc.swift`. - > `protoc-gen-grpc-swift` is currently shared with v1 so the `_V2=true` option is required. - > This will be removed when v2 is released. - @Code(name: "Console", file: "route-guide-sec03-step04-gen-grpc.txt") } } @@ -216,11 +213,11 @@ @Step { Create a new empty file in `Sources` called `RouteGuideService.swift`. To implement the service we need to conform a type to the generated service protocol. The name - of the protocol will be `_.ServiceProtocol` where `` and + of the protocol will be `_.SimpleServiceProtocol` where `` and `` are both taken from the `.proto` file. Create a `struct` called `RouteGuideService` which conforms to - the `Routeguide_RouteGuide.ServiceProtocol`. + the `Routeguide_RouteGuide.SimpleServiceProtocol`. @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step01-struct.swift") } @@ -243,47 +240,38 @@ @Step { `GetFeature` is a unary RPC which takes a single point as input and returns a single feature back to the client. Its generated method, `getFeature`, has one parameter: - `ServerRequest` describing the request. To return our response to + `Routeguide_Point`, the request message. To return our response to the client and complete the call we must first lookup a feature at the given point. @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step04-unary.swift") } @Step { - Then create and return an appropriate `ServerResponse` to the - client. + Then create and return an appropriate `Routeguide_Feature` to the client. @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step05-unary.swift") } @Step { Next, let's look at one of our streaming RPCs. Like the unary RPC, this method gets a - request object, `ServerRequest`, which has a message describing - the area in which the client wants to list features. As this is a server-side streaming RPC - we can send back multiple `Routeguide_Feature` messages to our client. + request message, `Routeguide_Rectangle`, describing the area in which the client wants to + list features. As this is a server-side streaming RPC we can send back + multiple `Routeguide_Feature` messages to our client. - To implement the method we must return a `StreamingServerResponse` which is initialized with - a closure to produce messages. The closure is passed a writer allowing you to write back - messages. We can write back a message for each feature we find in the rectangle. + To implement the method we must write messages back to the client using `response`, + an `RPCWriter` for `Routeguide_Feature` messages. @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step06-server-streaming.swift") } - @Step { - You can also send metadata to the client once the RPC has finished, in this case we don't - have any to send back so return the empty `Metadata` collection. - - @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step07-server-streaming.swift") - } - @Step { Now let's look at something a little more complicated: the client-side streaming method `RecordRoute`, where we get a stream of `Routeguide_Point`s from the client and return a single `Routeguide_RouteSummary` with information about their trip. - As you can see our method gets a `StreamingServerRequest` parameter and - returns a `ServerResponse`. In the method we iterate over - the asynchronous stream of points sent by the client. For each point we check if there's + As you can see our method gets a `RPCAsyncSequence` parameter + and returns a `Routeguide_RouteSummary`. In the method we iterate over the asynchronous + stream of points sent by the client. For each point we check if there's a feature at that point and calculate the distance between that and the last point we saw. After the *client* has finished sending points we populate a `Routeguide_RouteSummary` which we return in the response. @@ -301,11 +289,9 @@ } @Step { - To implement the RPC we return a `StreamingServerResponse`. Like in the - server-side streaming RPC it's initialized with a closure for writing back messages. In - the body of the closure we iterate the request messages and for each one call our helper - class to record the note and get all other notes recorded in the same location. We then - write each of those notes back to the client. + To implement the RPC we iterate the request notes, call our helper class to record each + note and get all other notes recorded in the same location. We then write each of those + notes back to the client. @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step10-bidi-streaming.swift") } From e390c85ad880004513221555884ae77b08714f1e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Dec 2024 10:35:22 +0000 Subject: [PATCH 517/580] Update generated code (#2141) --- Examples/echo/Sources/Generated/echo.grpc.swift | 1 - Examples/hello-world/Sources/Generated/helloworld.grpc.swift | 1 - Examples/route-guide/Sources/Generated/route_guide.grpc.swift | 1 - 3 files changed, 3 deletions(-) diff --git a/Examples/echo/Sources/Generated/echo.grpc.swift b/Examples/echo/Sources/Generated/echo.grpc.swift index 7ad44d1ce..26c075247 100644 --- a/Examples/echo/Sources/Generated/echo.grpc.swift +++ b/Examples/echo/Sources/Generated/echo.grpc.swift @@ -23,7 +23,6 @@ import GRPCCore import GRPCProtobuf -import SwiftProtobuf // MARK: - echo.Echo diff --git a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift index e64bc3e95..737d5fa0f 100644 --- a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift +++ b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift @@ -23,7 +23,6 @@ import GRPCCore import GRPCProtobuf -import SwiftProtobuf // MARK: - helloworld.Greeter diff --git a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift index 7971c54e1..05b5e3c48 100644 --- a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift +++ b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift @@ -23,7 +23,6 @@ import GRPCCore import GRPCProtobuf -import SwiftProtobuf // MARK: - routeguide.RouteGuide From 7ed6f7d194290b1c7403a1e35fb5f489e86d2211 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Wed, 11 Dec 2024 14:35:10 +0000 Subject: [PATCH 518/580] Enable MemberImportVisibility check on all targets (#2142) Enable MemberImportVisibility check on all targets. Use a standard string header and footer to bracket the new block for ease of updating in the future with scripts. --------- Co-authored-by: George Barnett --- Package.swift | 1 + Sources/GRPCCodeGen/CodeGenerationRequest.swift | 2 ++ Sources/GRPCCodeGen/Internal/Translator/Docs.swift | 2 ++ .../GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift | 1 + Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift | 1 + 5 files changed, 7 insertions(+) diff --git a/Package.swift b/Package.swift index e73076761..24f8cc8df 100644 --- a/Package.swift +++ b/Package.swift @@ -49,6 +49,7 @@ let defaultSwiftSettings: [SwiftSetting] = [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), ] let targets: [Target] = [ diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 5106b3c58..fbe545dbe 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +import Foundation + /// Describes the services, dependencies and trivia from an IDL file, /// and the IDL itself through its specific serializer and deserializer. public struct CodeGenerationRequest { diff --git a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift index 1d4b1e1fd..c0017e293 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +import Foundation + package enum Docs { package static func suffix(_ header: String, withDocs footer: String) -> String { if footer.isEmpty { diff --git a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift index cb5e38b7a..787d87e0f 100644 --- a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift @@ -16,6 +16,7 @@ import Foundation import GRPCCore +import SwiftProtobuf import XCTest final class ServiceConfigCodingTests: XCTestCase { diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift index a464cec02..253fbca1c 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import Foundation import GRPCCore struct HelloWorld: RegistrableRPCService { From 13150bde884d3f7261080a3fbfae6ac8862cfffa Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 12 Dec 2024 13:14:20 +0000 Subject: [PATCH 519/580] Add RPCErrorConvertible (#2143) Motivation: If an error is thrown from a server RPC then the status sent to the client will always have the unknown error code unless an `RPCError` is thrown. Moreover, there are various extensions to gRPC which rely on additional information being stuffed into the metadata. This is difficult and a bit error prone for users to do directly. We should provide a mechanism whereby errors can be converted to an `RPCError` such that the appropriate code, message and metadata are sent to the client. Modifications: - Add the `RPCErrorConvertible` protocol. Conforming types provide appropriate properties to populate an `RPCError`. - Add handling for this in the server executor such that convertible errors are converted into an `RPCError`. Result: Easier for users to propagate an appropriate status --- .../Server/Internal/ServerRPCExecutor.swift | 10 +++- Sources/GRPCCore/RPCError.swift | 57 +++++++++++++++++++ .../Internal/ServerRPCExecutorTests.swift | 23 ++++++++ Tests/GRPCCoreTests/RPCErrorTests.swift | 45 +++++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index aa2163424..50ff0b3bd 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -188,7 +188,15 @@ struct ServerRPCExecutor { try await handler(request, context) } }.castError(to: RPCError.self) { error in - RPCError(code: .unknown, message: "Service method threw an unknown error.", cause: error) + if let convertible = error as? (any RPCErrorConvertible) { + return RPCError(convertible) + } else { + return RPCError( + code: .unknown, + message: "Service method threw an unknown error.", + cause: error + ) + } }.flatMap { response in response.accepted } diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 810298e3a..80157034c 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -277,3 +277,60 @@ extension RPCError.Code { /// operation. public static let unauthenticated = Self(code: .unauthenticated) } + +/// A value that can be converted to an ``RPCError``. +/// +/// You can conform types to this protocol to have more control over the status codes and +/// error information provided to clients when a service throws an error. +public protocol RPCErrorConvertible { + /// The error code to terminate the RPC with. + var rpcErrorCode: RPCError.Code { get } + + /// A message providing additional context about the error. + var rpcErrorMessage: String { get } + + /// Metadata associated with the error. + /// + /// Any metadata included in the error thrown from a service will be sent back to the client and + /// conversely any ``RPCError`` received by the client may include metadata sent by a service. + /// + /// Note that clients and servers may synthesise errors which may not include metadata. + var rpcErrorMetadata: Metadata { get } + + /// The original error which led to this error being thrown. + var rpcErrorCause: (any Error)? { get } +} + +extension RPCErrorConvertible { + /// Metadata associated with the error. + /// + /// Any metadata included in the error thrown from a service will be sent back to the client and + /// conversely any ``RPCError`` received by the client may include metadata sent by a service. + /// + /// Note that clients and servers may synthesise errors which may not include metadata. + public var rpcErrorMetadata: Metadata { + [:] + } + + /// The original error which led to this error being thrown. + public var rpcErrorCause: (any Error)? { + nil + } +} + +extension RPCErrorConvertible where Self: Error { + /// The original error which led to this error being thrown. + public var rpcErrorCause: (any Error)? { + self + } +} + +extension RPCError { + /// Create a new error by converting the given value. + public init(_ convertible: some RPCErrorConvertible) { + self.code = convertible.rpcErrorCode + self.message = convertible.rpcErrorMessage + self.metadata = convertible.rpcErrorMetadata + self.cause = convertible.rpcErrorCause + } +} diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index fe8d301aa..b807d02cd 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -346,4 +346,27 @@ final class ServerRPCExecutorTests: XCTestCase { XCTAssertEqual(parts, [.status(Status(code: .unavailable, message: "Unavailable"), [:])]) } } + + func testErrorConversion() async throws { + struct CustomError: RPCErrorConvertible, Error { + var rpcErrorCode: RPCError.Code { .alreadyExists } + var rpcErrorMessage: String { "foobar" } + var rpcErrorMetadata: Metadata { ["error": "yes"] } + } + + let harness = ServerRPCExecutorTestHarness() + try await harness.execute(handler: .throwing(CustomError())) { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + try await inbound.write(.message([0])) + await inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + XCTAssertEqual( + parts, + [ + .status(Status(code: .alreadyExists, message: "foobar"), ["error": "yes"]) + ] + ) + } + } } diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift index 7f87e697a..b4eba43d0 100644 --- a/Tests/GRPCCoreTests/RPCErrorTests.swift +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -189,4 +189,49 @@ struct RPCErrorTests { #expect(wrappedError1.message == "Error 1.") #expect(wrappedError1.cause == nil) } + + @Test("Convert type to RPCError") + func convertTypeUsingRPCErrorConvertible() { + struct Cause: Error {} + struct ConvertibleError: RPCErrorConvertible { + var rpcErrorCode: RPCError.Code { .unknown } + var rpcErrorMessage: String { "uhoh" } + var rpcErrorMetadata: Metadata { ["k": "v"] } + var rpcErrorCause: (any Error)? { Cause() } + } + + let error = RPCError(ConvertibleError()) + #expect(error.code == .unknown) + #expect(error.message == "uhoh") + #expect(error.metadata == ["k": "v"]) + #expect(error.cause is Cause) + } + + @Test("Convert type to RPCError with defaults") + func convertTypeUsingRPCErrorConvertibleDefaults() { + struct ConvertibleType: RPCErrorConvertible { + var rpcErrorCode: RPCError.Code { .unknown } + var rpcErrorMessage: String { "uhoh" } + } + + let error = RPCError(ConvertibleType()) + #expect(error.code == .unknown) + #expect(error.message == "uhoh") + #expect(error.metadata == [:]) + #expect(error.cause == nil) + } + + @Test("Convert error to RPCError with defaults") + func convertErrorUsingRPCErrorConvertibleDefaults() { + struct ConvertibleType: RPCErrorConvertible, Error { + var rpcErrorCode: RPCError.Code { .unknown } + var rpcErrorMessage: String { "uhoh" } + } + + let error = RPCError(ConvertibleType()) + #expect(error.code == .unknown) + #expect(error.message == "uhoh") + #expect(error.metadata == [:]) + #expect(error.cause is ConvertibleType) + } } From e431ed6ab8a405cc019bce0ff9fc28a3e75d0794 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 17 Dec 2024 16:12:10 +0000 Subject: [PATCH 520/580] Add detailed error example and error article (#2145) Motivation: We should document how service authors and clients should deal with errors and what options are available to them. Modifications: - Add an article explaining what the error mechanisms available - Add an example Result: Better docs --------- Co-authored-by: Gus Cairo --- Examples/error-details/.gitignore | 8 + Examples/error-details/Package.swift | 37 ++ Examples/error-details/README.md | 26 ++ .../Sources/DetailedErrorExample.swift | 85 ++++ .../Sources/Generated/helloworld.grpc.swift | 362 ++++++++++++++++++ .../Sources/Generated/helloworld.pb.swift | 129 +++++++ .../Documentation.docc/Articles/Errors.md | 83 ++++ .../Documentation.docc/Documentation.md | 1 + dev/protos/generate.sh | 9 + 9 files changed, 740 insertions(+) create mode 100644 Examples/error-details/.gitignore create mode 100644 Examples/error-details/Package.swift create mode 100644 Examples/error-details/README.md create mode 100644 Examples/error-details/Sources/DetailedErrorExample.swift create mode 100644 Examples/error-details/Sources/Generated/helloworld.grpc.swift create mode 100644 Examples/error-details/Sources/Generated/helloworld.pb.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Articles/Errors.md diff --git a/Examples/error-details/.gitignore b/Examples/error-details/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/error-details/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift new file mode 100644 index 000000000..2598c8d80 --- /dev/null +++ b/Examples/error-details/Package.swift @@ -0,0 +1,37 @@ +// swift-tools-version:6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "error-details", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + ], + targets: [ + .executableTarget( + name: "error-details", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ] + ) + ] +) diff --git a/Examples/error-details/README.md b/Examples/error-details/README.md new file mode 100644 index 000000000..6eeaa35e4 --- /dev/null +++ b/Examples/error-details/README.md @@ -0,0 +1,26 @@ +# Detailed Error + +This example demonstrates how to create and unpack detailed errors. + +## Overview + +A command line tool that demonstrates how a detailed error can be thrown by a +service and unpacked and inspected by a client. The detailed error model is +described in more detailed in the [gRPC Error +Guide](https://grpc.io/docs/guides/error/) and is made available via the +[grpc-swift-protobuf](https://github.com/grpc-swift-protobuf) package. + +## Usage + +Build and run the example using the CLI: + +```console +$ swift run +Error code: resourceExhausted +Error message: The greeter has temporarily run out of greetings. +Error details: +- Localized message (en-GB): Out of enthusiasm. The greeter is having a cup of tea, try again after that. +- Localized message (en-US): Out of enthusiasm. The greeter is taking a coffee break, try again later. +- Help links: + - https://en.wikipedia.org/wiki/Caffeine (A Wikipedia page about caffeine including its properties and effects.) +``` diff --git a/Examples/error-details/Sources/DetailedErrorExample.swift b/Examples/error-details/Sources/DetailedErrorExample.swift new file mode 100644 index 000000000..ddd669877 --- /dev/null +++ b/Examples/error-details/Sources/DetailedErrorExample.swift @@ -0,0 +1,85 @@ +/* + * 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 GRPCInProcessTransport +import GRPCProtobuf + +@main +struct DetailedErrorExample { + static func main() async throws { + let inProcess = InProcessTransport() + try await withGRPCServer(transport: inProcess.server, services: [Greeter()]) { server in + try await withGRPCClient(transport: inProcess.client) { client in + try await Self.doRPC(Helloworld_Greeter.Client(wrapping: client)) + } + } + } + + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + do { + let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) + print("Unexpected reply: \(reply.message)") + } catch let error as RPCError { + // Unpack the detailed from the standard 'RPCError'. + guard let status = try error.unpackGoogleRPCStatus() else { return } + print("Error code: \(status.code)") + print("Error message: \(status.message)") + print("Error details:") + for detail in status.details { + if let localizedMessage = detail.localizedMessage { + print("- Localized message (\(localizedMessage.locale)): \(localizedMessage.message)") + } else if let help = detail.help { + print("- Help links:") + for link in help.links { + print(" - \(link.url) (\(link.linkDescription))") + } + } + } + } + } +} + +struct Greeter: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + // Always throw a detailed error. + throw GoogleRPCStatus( + code: .resourceExhausted, + message: "The greeter has temporarily run out of greetings.", + details: [ + .localizedMessage( + locale: "en-GB", + message: "Out of enthusiasm. The greeter is having a cup of tea, try again after that." + ), + .localizedMessage( + locale: "en-US", + message: "Out of enthusiasm. The greeter is taking a coffee break, try again later." + ), + .help( + links: [ + ErrorDetails.Help.Link( + url: "https://en.wikipedia.org/wiki/Caffeine", + description: "A Wikipedia page about caffeine including its properties and effects." + ) + ] + ), + ] + ) + } +} diff --git a/Examples/error-details/Sources/Generated/helloworld.grpc.swift b/Examples/error-details/Sources/Generated/helloworld.grpc.swift new file mode 100644 index 000000000..737d5fa0f --- /dev/null +++ b/Examples/error-details/Sources/Generated/helloworld.grpc.swift @@ -0,0 +1,362 @@ +/// Copyright 2015 gRPC authors. +/// +/// 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: helloworld.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +// MARK: - helloworld.Greeter + +/// Namespace containing generated types for the "helloworld.Greeter" service. +internal enum Helloworld_Greeter { + /// Service descriptor for the "helloworld.Greeter" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") + /// Namespace for method metadata. + internal enum Method { + /// Namespace for "SayHello" metadata. + internal enum SayHello { + /// Request type for "SayHello". + internal typealias Input = Helloworld_HelloRequest + /// Response type for "SayHello". + internal typealias Output = Helloworld_HelloReply + /// Descriptor for "SayHello". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter"), + method: "SayHello" + ) + } + /// Descriptors for all methods in the "helloworld.Greeter" service. + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ + SayHello.descriptor + ] + } +} + +extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "helloworld.Greeter" service. + internal static let helloworld_Greeter = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") +} + +// MARK: helloworld.Greeter (server) + +extension Helloworld_Greeter { + /// Streaming variant of the service protocol for the "helloworld.Greeter" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A streaming request of `Helloworld_HelloRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Helloworld_HelloReply` messages. + func sayHello( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "helloworld.Greeter" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Helloworld_HelloReply` message. + func sayHello( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + } + + /// Simple service protocol for the "helloworld.Greeter" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol SimpleServiceProtocol: Helloworld_Greeter.ServiceProtocol { + /// Handle the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A `Helloworld_HelloRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Helloworld_HelloReply` to respond with. + func sayHello( + request: Helloworld_HelloRequest, + context: GRPCCore.ServerContext + ) async throws -> Helloworld_HelloReply + } +} + +// Default implementation of 'registerMethods(with:)'. +extension Helloworld_Greeter.StreamingServiceProtocol { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Helloworld_Greeter.Method.SayHello.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.sayHello( + request: request, + context: context + ) + } + ) + } +} + +// Default implementation of streaming methods from 'StreamingServiceProtocol'. +extension Helloworld_Greeter.ServiceProtocol { + internal func sayHello( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.sayHello( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } +} + +// Default implementation of methods from 'ServiceProtocol'. +extension Helloworld_Greeter.SimpleServiceProtocol { + internal func sayHello( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.sayHello( + request: request.message, + context: context + ), + metadata: [:] + ) + } +} + +// MARK: helloworld.Greeter (client) + +extension Helloworld_Greeter { + /// Generated client protocol for the "helloworld.Greeter" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal protocol ClientProtocol: Sendable { + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - serializer: A serializer for `Helloworld_HelloRequest` messages. + /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func sayHello( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "helloworld.Greeter" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > The greeting service definition. + internal struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - serializer: A serializer for `Helloworld_HelloRequest` messages. + /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Helloworld_Greeter.Method.SayHello.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. +extension Helloworld_Greeter.ClientProtocol { + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - request: A request containing a single `Helloworld_HelloRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.sayHello( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } +} + +// Helpers providing sugared APIs for 'ClientProtocol' methods. +extension Helloworld_Greeter.ClientProtocol { + /// Call the "SayHello" method. + /// + /// > Source IDL Documentation: + /// > + /// > Sends a greeting + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func sayHello( + _ message: Helloworld_HelloRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.sayHello( + request: request, + options: options, + onResponse: handleResponse + ) + } +} \ No newline at end of file diff --git a/Examples/error-details/Sources/Generated/helloworld.pb.swift b/Examples/error-details/Sources/Generated/helloworld.pb.swift new file mode 100644 index 000000000..20b4f36df --- /dev/null +++ b/Examples/error-details/Sources/Generated/helloworld.pb.swift @@ -0,0 +1,129 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: helloworld.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +/// Copyright 2015 gRPC authors. +/// +/// 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 SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// The request message containing the user's name. +struct Helloworld_HelloRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var name: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// The response message containing the greetings +struct Helloworld_HelloReply: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "helloworld" + +extension Helloworld_HelloRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HelloRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Helloworld_HelloRequest, rhs: Helloworld_HelloRequest) -> Bool { + if lhs.name != rhs.name {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Helloworld_HelloReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".HelloReply" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Helloworld_HelloReply, rhs: Helloworld_HelloReply) -> Bool { + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Errors.md b/Sources/GRPCCore/Documentation.docc/Articles/Errors.md new file mode 100644 index 000000000..5addfc7b4 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Errors.md @@ -0,0 +1,83 @@ +# Errors + +Learn about the different error mechanisms in gRPC and how to use them. + +## Overview + +gRPC has a well defined error model for RPCs and a common extension to provide +richer errors when using Protocol Buffers. This article explains both mechanisms +and offers advice on using and handling RPC errors for service authors and +clients. + +### Error models + +gRPC has two widely used error models: + +1. A 'standard' error model supported by all client/server gRPC libraries. +2. A 'rich' error model providing more detailed error information via serialized + Protocol Buffers messages. + +#### Standard error model + +In gRPC the outcome of every RPC is represented by a status made up of a code +and a message. The status is propagated from the server to the client in the +metadata as the final part of an RPC indicating the outcome of the RPC. + +You can find more information about the error codes in ``RPCError/Code`` and in +the status codes guide on the +[gRPC website](https://grpc.io/docs/guides/status-codes/). + +This mechanism is part of the gRPC protocol and is supported by all client/server +gRPC libraries regardless of the data format (e.g. Protocol Buffers) being used +for messages. + +#### Rich error model + +The standard error model is quite limited and doesn't include the ability to +communicate details about the error. If you're using the Protocol Buffers data +format for messages then you may wish to use the "rich" error model. + +The model was developed and used by Google and is described in more detail +in the [gRPC error guide](https://grpc.io/docs/guides/error/) and +[Google AIP-193](https://google.aip.dev/193). + +While not officially part of gRPC it's a widely used convention with support in +various client/server gRPC libraries, including gRPC Swift. + +It specifies a standard set of error message types covering the most common +situations. The error details are encoded as protobuf messages in the trailing +metadata of an RPC. Clients are able to deserialize and access the details as +type-safe structured messages should they need to. + +### User guide + +Learn how to use both models in gRPC Swift. + +#### Service authors + +Errors thrown from an RPC handler are caught by the gRPC runtime and turned into +a status. You have a two options to ensure that an appropriate status is sent to +the client if your RPC handler throws an error: + +1. Throw an ``RPCError`` which explicitly sets the desired status code and + message. +2. Throw an error conforming to ``RPCErrorConvertible`` which the gRPC runtime + will use to create an ``RPCError``. + +Any errors thrown which don't fall into these categories will result in a status +code of `unknown` being sent to the client. + +Generally speaking expected failure scenarios should be considered as part of +the API contract and each RPC should be documented accordingly. + +#### Clients + +Clients should catch ``RPCError`` if they are interested in the failures from an +RPC. This is a manifestation of the error sent by the server but in some cases +it may be synthesized locally. + +For clients using the rich error model, the ``RPCError`` can be caught and a +detailed error can be extracted from it using `unpackGoogleRPCStatus()`. + +See [`error-details`](https://github.com/grpc/grpc-swift/tree/main/Examples/error-details) for +an example. diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 258a45a9e..f70913cea 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -52,6 +52,7 @@ as tutorials. ### Essentials - +- ### Project Information diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh index 5efb7018c..294145be4 100755 --- a/dev/protos/generate.sh +++ b/dev/protos/generate.sh @@ -97,6 +97,14 @@ function generate_routeguide_example { generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" } +function generate_error_details_example { + local proto="$here/upstream/grpc/examples/helloworld.proto" + local output="$root/Examples/error-details/Sources/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" +} + #- TESTS ---------------------------------------------------------------------- function generate_service_config_for_tests { @@ -119,6 +127,7 @@ function generate_service_config_for_tests { generate_echo_example generate_helloworld_example generate_routeguide_example +generate_error_details_example # Tests generate_service_config_for_tests From ec8db6ab0f6d970035b88f58dcd8aa1884e9a239 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 17 Dec 2024 16:57:42 +0000 Subject: [PATCH 521/580] Remove unused Foundation imports (#2146) Motivation: A few files unnecesarily import Foundation Modifications: Remove unused imports Result: Fewer dependencies --- Sources/GRPCCodeGen/CodeGenerationRequest.swift | 4 +--- .../GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift | 1 - Sources/GRPCCodeGen/Internal/Translator/Docs.swift | 6 +++--- Sources/GRPCCodeGen/Internal/TypeName.swift | 1 - 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index fbe545dbe..d6d0f73d1 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -14,8 +14,6 @@ * limitations under the License. */ -import Foundation - /// Describes the services, dependencies and trivia from an IDL file, /// and the IDL itself through its specific serializer and deserializer. public struct CodeGenerationRequest { @@ -334,7 +332,7 @@ extension Name { /// /// For example, if `base` is "Foo.Bar", then `normalizedBase` is "Foo_Bar". public var normalizedBase: String { - return self.base.replacingOccurrences(of: ".", with: "_") + return self.base.replacing(".", with: "_") } } diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index c7c6f3850..0900169c0 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -26,7 +26,6 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation /// An object for building up a generated file line-by-line. /// diff --git a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift index c0017e293..f8c6def00 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift @@ -14,8 +14,6 @@ * limitations under the License. */ -import Foundation - package enum Docs { package static func suffix(_ header: String, withDocs footer: String) -> String { if footer.isEmpty { @@ -58,7 +56,9 @@ package enum Docs { """ let body = docs.split(separator: "\n").map { line in - "/// > " + line.dropFirst(4).trimmingCharacters(in: .whitespaces) + var line = "/// > " + line.dropFirst(4) + line.trimPrefix(while: { $0.isWhitespace }) + return String(line.drop(while: { $0.isWhitespace })) }.joined(separator: "\n") return header + "\n" + body diff --git a/Sources/GRPCCodeGen/Internal/TypeName.swift b/Sources/GRPCCodeGen/Internal/TypeName.swift index 0152de6a0..35d5eb77a 100644 --- a/Sources/GRPCCodeGen/Internal/TypeName.swift +++ b/Sources/GRPCCodeGen/Internal/TypeName.swift @@ -26,7 +26,6 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import Foundation /// A fully-qualified type name that contains the components of the Swift /// type name. From dee1f1a7c0485fe2b5ef15ad07a7e30807040522 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 18 Dec 2024 14:05:00 +0100 Subject: [PATCH 522/580] Update release.yml (#2148) Update the release.yml file with the latest label changes --- .github/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/release.yml b/.github/release.yml index f96b51492..e29eb8464 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -5,10 +5,10 @@ changelog: - ⚠️ semver/major - title: SemVer Minor labels: - - semver/minor + - 🆕 semver/minor - title: SemVer Patch labels: - - semver/patch + - 🔨 semver/patch - title: Other Changes labels: - semver/none From 5be11cd9386bfd9b29ed72549fe7530157a37bf2 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 20 Dec 2024 15:16:18 +0000 Subject: [PATCH 523/580] Add reflection server example (#2149) Motivation: We've added back the reflection server; we should have an example of how to use it. Modifications: - Add a reflection server example - Split the echo service into a separate file in the echo example so that it can be symlinked from the reflection server Result: More examples --- .licenseignore | 1 + .../Sources/Subcommands/EchoService.swift | 55 + Examples/echo/Sources/Subcommands/Serve.swift | 38 - Examples/reflection-server/.gitignore | 8 + Examples/reflection-server/Package.swift | 45 + Examples/reflection-server/README.md | 61 ++ .../Sources/DescriptorSets/echo.pb | Bin 0 -> 1727 bytes .../Sources/Echo/EchoService.swift | 1 + .../Sources/Generated/echo.grpc.swift | 994 ++++++++++++++++++ .../Sources/Generated/echo.pb.swift | 129 +++ .../Sources/ReflectionServer.swift | 71 ++ dev/protos/generate.sh | 13 + 12 files changed, 1378 insertions(+), 38 deletions(-) create mode 100644 Examples/echo/Sources/Subcommands/EchoService.swift create mode 100644 Examples/reflection-server/.gitignore create mode 100644 Examples/reflection-server/Package.swift create mode 100644 Examples/reflection-server/README.md create mode 100644 Examples/reflection-server/Sources/DescriptorSets/echo.pb create mode 120000 Examples/reflection-server/Sources/Echo/EchoService.swift create mode 100644 Examples/reflection-server/Sources/Generated/echo.grpc.swift create mode 100644 Examples/reflection-server/Sources/Generated/echo.pb.swift create mode 100644 Examples/reflection-server/Sources/ReflectionServer.swift diff --git a/.licenseignore b/.licenseignore index 314cb7efe..6a74dfa65 100644 --- a/.licenseignore +++ b/.licenseignore @@ -40,3 +40,4 @@ LICENSE **/*.swift dev/protos/**/*.proto Examples/hello-world/Protos/HelloWorld.proto +**/*.pb diff --git a/Examples/echo/Sources/Subcommands/EchoService.swift b/Examples/echo/Sources/Subcommands/EchoService.swift new file mode 100644 index 000000000..e752af5b6 --- /dev/null +++ b/Examples/echo/Sources/Subcommands/EchoService.swift @@ -0,0 +1,55 @@ +/* + * 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 + +struct EchoService: Echo_Echo.SimpleServiceProtocol { + func get( + request: Echo_EchoRequest, + context: ServerContext + ) async throws -> Echo_EchoResponse { + return .with { $0.text = request.text } + } + + func collect( + request: RPCAsyncSequence, + context: ServerContext + ) async throws -> Echo_EchoResponse { + let messages = try await request.reduce(into: []) { $0.append($1.text) } + let joined = messages.joined(separator: " ") + return .with { $0.text = joined } + } + + func expand( + request: Echo_EchoRequest, + response: RPCWriter, + context: ServerContext + ) async throws { + let parts = request.text.split(separator: " ") + let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } } + try await response.write(contentsOf: messages) + } + + func update( + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) async throws { + for try await message in request { + try await response.write(.with { $0.text = message.text }) + } + } +} diff --git a/Examples/echo/Sources/Subcommands/Serve.swift b/Examples/echo/Sources/Subcommands/Serve.swift index 6bbb9ea82..17f38aec7 100644 --- a/Examples/echo/Sources/Subcommands/Serve.swift +++ b/Examples/echo/Sources/Subcommands/Serve.swift @@ -41,41 +41,3 @@ struct Serve: AsyncParsableCommand { } } } - -struct EchoService: Echo_Echo.SimpleServiceProtocol { - func get( - request: Echo_EchoRequest, - context: ServerContext - ) async throws -> Echo_EchoResponse { - return .with { $0.text = request.text } - } - - func collect( - request: RPCAsyncSequence, - context: ServerContext - ) async throws -> Echo_EchoResponse { - let messages = try await request.reduce(into: []) { $0.append($1.text) } - let joined = messages.joined(separator: " ") - return .with { $0.text = joined } - } - - func expand( - request: Echo_EchoRequest, - response: RPCWriter, - context: ServerContext - ) async throws { - let parts = request.text.split(separator: " ") - let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } } - try await response.write(contentsOf: messages) - } - - func update( - request: RPCAsyncSequence, - response: RPCWriter, - context: ServerContext - ) async throws { - for try await message in request { - try await response.write(.with { $0.text = message.text }) - } - } -} diff --git a/Examples/reflection-server/.gitignore b/Examples/reflection-server/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/reflection-server/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/reflection-server/Package.swift b/Examples/reflection-server/Package.swift new file mode 100644 index 000000000..37a3942b2 --- /dev/null +++ b/Examples/reflection-server/Package.swift @@ -0,0 +1,45 @@ +// swift-tools-version:6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "reflection-server", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-extras.git", branch: "main"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "reflection-server", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "GRPCReflectionService", package: "grpc-swift-extras"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + resources: [ + .copy("DescriptorSets") + ] + ) + ] +) diff --git a/Examples/reflection-server/README.md b/Examples/reflection-server/README.md new file mode 100644 index 000000000..1743a8a08 --- /dev/null +++ b/Examples/reflection-server/README.md @@ -0,0 +1,61 @@ +# Reflection Server + +This example demonstrates the gRPC Reflection service which is described in more +detail in the [gRPC documentation](https://github.com/grpc/grpc/blob/6fa8043bf9befb070b846993b59a3348248e6566/doc/server-reflection.md). + +## Overview + +A 'reflection-server' command line tool that uses the reflection service implementation +from [grpc/grpc-swift-extras](https://github.com/grpc/grpc-swift-extras) and the +Echo service (see the 'echo' example). + +The reflection service requires you to initialize it with a set of Protobuf file +descriptors for the services you're offering. You can use `protoc` to create a +descriptor set including dependencies and source information for each service. + +The following command will generate a descriptor set at `path/to/output.pb` from +the `path/to/input.proto` file with source information and any imports used in +`input.proto`: + +```console +protoc --descriptor_set_out=path/to/output.pb path/to/input.proto \ + --include_source_info \ + --include_imports +``` + +## Usage + +Build and run the server using the CLI: + +```console +$ swift run reflection-server +Reflection server listening on [ipv4]127.0.0.1:31415 +``` + +You can use 'grpcurl' to query the reflection service. If you don't already have +it installed follow the instructions in the 'grpcurl' project's +[README](https://github.com/fullstorydev/grpcurl). + +You can list all services with: + +```console +$ grpcurl -plaintext 127.0.0.1:31415 list +echo.Echo +``` + +And describe the 'Get' method in the 'echo.Echo' service: + +```console +$ grpcurl -plaintext 127.0.0.1:31415 describe echo.Echo.Get +echo.Echo.Get is a method: +// Immediately returns an echo of a request. +rpc Get ( .echo.EchoRequest ) returns ( .echo.EchoResponse ); +``` + +You can also call the 'echo.Echo.Get' method: +```console +$ grpcurl -plaintext -d '{ "text": "Hello" }' 127.0.0.1:31415 echo.Echo.Get +{ + "text": "Hello" +} +``` diff --git a/Examples/reflection-server/Sources/DescriptorSets/echo.pb b/Examples/reflection-server/Sources/DescriptorSets/echo.pb new file mode 100644 index 0000000000000000000000000000000000000000..dce4d22e3354b88fd5e154954ec8f81fbf646493 GIT binary patch literal 1727 zcma)7(T>_i6vf7I*(tl)b%-`XqPcanS~ejpAx&E)s#Ln6De0D#fNivogBf5H@VNF6 zmM`l|pZXR3nMyrl3?*tG776U*xpU9C=iV{wkA22OFje&^R$6(Ex%a&HiFTMADR$PZ9kqQ&>e=*I!EasRXug>ysh4zl>CVPD+b=li}v`*#c zycf)E-$pzf-FZvH^1fo9U8vbi1a~K@THZ_MUPmLY#ht0DR>OMtH)F2n9OSAN`_o}o z(aRs?yyou?U8rak%gIzzH8`VYCMVv?#QnTKmstNG3CI9R-qm^3FoUbTUnih#b2AR-Ufnfg{&(>um=a-#} zcDLVt2C>+19nM6OP>j)$F^U|nh)0kT@F4`w_>xpid=d++X=S7?W2t30sZpZFddXwK zXe1LI%i%(Q;!>ucQ0;C2eBvSbtv+@7KK;<@cltF(?>mE^uC534zSZlsx`R%;Pggy< zxaz*?3_4d`pdYE#y`i5w-8VH7@CdySw^3}=L!vbPi4n}|3uE!NJ2b|s!AT?nIhFy6 z3MUIb5j0UBMI53QibOn@iRobiaU(`EIhUI2^ufP<-*$u*m}PtU!yHZ{v)!EMc)2eu z+~?h#<>kFXk$uY?H)rR(e93Nl+dL^XDkav<>7epSAtJ}l5EV3P~oLd%va9;H=nc?G67eQesv3B411 zZdA-+CX7k8A}o{JEoQW)W&FShio6uW`K66K+KPm&RDV z6@?bfG1p-Lk3xJ$w491C?Oec-B&J}dk{xX}w3XfcjNiTobWqwLBj5pT5VVz_e#3rA zwEBJMmiBk{@XbXh=2r0&=5WmqvXLW2X%0otFYf&CDD*N{{ z;yh$jN{==OhE<+4Hpu9B{+*d02lGR@$Lahyrteg^&zj6}()saiuY5uSOt5*WuZJaT zlsS<@yhboYX7o*Fvj-Y{yRbpX=kKL9nKiKbhes)^AM{(V5dZ)H literal 0 HcmV?d00001 diff --git a/Examples/reflection-server/Sources/Echo/EchoService.swift b/Examples/reflection-server/Sources/Echo/EchoService.swift new file mode 120000 index 000000000..499718660 --- /dev/null +++ b/Examples/reflection-server/Sources/Echo/EchoService.swift @@ -0,0 +1 @@ +../../../echo/Sources/Subcommands/EchoService.swift \ No newline at end of file diff --git a/Examples/reflection-server/Sources/Generated/echo.grpc.swift b/Examples/reflection-server/Sources/Generated/echo.grpc.swift new file mode 100644 index 000000000..26c075247 --- /dev/null +++ b/Examples/reflection-server/Sources/Generated/echo.grpc.swift @@ -0,0 +1,994 @@ +// Copyright (c) 2015, Google Inc. +// +// 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: echo.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +import GRPCCore +import GRPCProtobuf + +// MARK: - echo.Echo + +/// Namespace containing generated types for the "echo.Echo" service. +internal enum Echo_Echo { + /// Service descriptor for the "echo.Echo" service. + internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") + /// Namespace for method metadata. + internal enum Method { + /// Namespace for "Get" metadata. + internal enum Get { + /// Request type for "Get". + internal typealias Input = Echo_EchoRequest + /// Response type for "Get". + internal typealias Output = Echo_EchoResponse + /// Descriptor for "Get". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), + method: "Get" + ) + } + /// Namespace for "Expand" metadata. + internal enum Expand { + /// Request type for "Expand". + internal typealias Input = Echo_EchoRequest + /// Response type for "Expand". + internal typealias Output = Echo_EchoResponse + /// Descriptor for "Expand". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), + method: "Expand" + ) + } + /// Namespace for "Collect" metadata. + internal enum Collect { + /// Request type for "Collect". + internal typealias Input = Echo_EchoRequest + /// Response type for "Collect". + internal typealias Output = Echo_EchoResponse + /// Descriptor for "Collect". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), + method: "Collect" + ) + } + /// Namespace for "Update" metadata. + internal enum Update { + /// Request type for "Update". + internal typealias Input = Echo_EchoRequest + /// Response type for "Update". + internal typealias Output = Echo_EchoResponse + /// Descriptor for "Update". + internal static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), + method: "Update" + ) + } + /// Descriptors for all methods in the "echo.Echo" service. + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ + Get.descriptor, + Expand.descriptor, + Collect.descriptor, + Update.descriptor + ] + } +} + +extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "echo.Echo" service. + internal static let echo_Echo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") +} + +// MARK: echo.Echo (server) + +extension Echo_Echo { + /// Streaming variant of the service protocol for the "echo.Echo" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func get( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func expand( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func collect( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func update( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Service protocol for the "echo.Echo" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + internal protocol ServiceProtocol: Echo_Echo.StreamingServiceProtocol { + /// Handle the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Echo_EchoResponse` message. + func get( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func expand( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Echo_EchoResponse` message. + func collect( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Echo_EchoResponse` messages. + func update( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Simple service protocol for the "echo.Echo" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + internal protocol SimpleServiceProtocol: Echo_Echo.ServiceProtocol { + /// Handle the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A `Echo_EchoRequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Echo_EchoResponse` to respond with. + func get( + request: Echo_EchoRequest, + context: GRPCCore.ServerContext + ) async throws -> Echo_EchoResponse + + /// Handle the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A `Echo_EchoRequest` message. + /// - response: A response stream of `Echo_EchoResponse` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func expand( + request: Echo_EchoRequest, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A stream of `Echo_EchoRequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Echo_EchoResponse` to respond with. + func collect( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> Echo_EchoResponse + + /// Handle the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A stream of `Echo_EchoRequest` messages. + /// - response: A response stream of `Echo_EchoResponse` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func update( + request: GRPCCore.RPCAsyncSequence, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + } +} + +// Default implementation of 'registerMethods(with:)'. +extension Echo_Echo.StreamingServiceProtocol { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Echo_Echo.Method.Get.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.get( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Echo_Echo.Method.Expand.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.expand( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Echo_Echo.Method.Collect.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.collect( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Echo_Echo.Method.Update.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.update( + request: request, + context: context + ) + } + ) + } +} + +// Default implementation of streaming methods from 'StreamingServiceProtocol'. +extension Echo_Echo.ServiceProtocol { + internal func get( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.get( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + + internal func expand( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.expand( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + + internal func collect( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.collect( + request: request, + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } +} + +// Default implementation of methods from 'ServiceProtocol'. +extension Echo_Echo.SimpleServiceProtocol { + internal func get( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.get( + request: request.message, + context: context + ), + metadata: [:] + ) + } + + internal func expand( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.expand( + request: request.message, + response: writer, + context: context + ) + return [:] + } + ) + } + + internal func collect( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.collect( + request: request.messages, + context: context + ), + metadata: [:] + ) + } + + internal func update( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.update( + request: request.messages, + response: writer, + context: context + ) + return [:] + } + ) + } +} + +// MARK: echo.Echo (client) + +extension Echo_Echo { + /// Generated client protocol for the "echo.Echo" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + internal protocol ClientProtocol: Sendable { + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func get( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func expand( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func collect( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func update( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Generated client for the "echo.Echo" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + internal struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func get( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: Echo_Echo.Method.Get.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func expand( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Echo_Echo.Method.Expand.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func collect( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Echo_Echo.Method.Collect.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - serializer: A serializer for `Echo_EchoRequest` messages. + /// - deserializer: A deserializer for `Echo_EchoResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func update( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Echo_Echo.Method.Update.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } +} + +// Helpers providing default arguments to 'ClientProtocol' methods. +extension Echo_Echo.ClientProtocol { + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func get( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.get( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - request: A request containing a single `Echo_EchoRequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func expand( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.expand( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func collect( + request: GRPCCore.StreamingClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.collect( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - request: A streaming request producing `Echo_EchoRequest` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func update( + request: GRPCCore.StreamingClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.update( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } +} + +// Helpers providing sugared APIs for 'ClientProtocol' methods. +extension Echo_Echo.ClientProtocol { + /// Call the "Get" method. + /// + /// > Source IDL Documentation: + /// > + /// > Immediately returns an echo of a request. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func get( + _ message: Echo_EchoRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.get( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Expand" method. + /// + /// > Source IDL Documentation: + /// > + /// > Splits a request into words and returns each word in a stream of messages. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func expand( + _ message: Echo_EchoRequest, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.expand( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Collect" method. + /// + /// > Source IDL Documentation: + /// > + /// > Collects a stream of messages and returns them concatenated when the caller closes. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func collect( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.collect( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "Update" method. + /// + /// > Source IDL Documentation: + /// > + /// > Streams back messages as they are received in an input stream. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + internal func update( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.update( + request: request, + options: options, + onResponse: handleResponse + ) + } +} \ No newline at end of file diff --git a/Examples/reflection-server/Sources/Generated/echo.pb.swift b/Examples/reflection-server/Sources/Generated/echo.pb.swift new file mode 100644 index 000000000..88ef21ca9 --- /dev/null +++ b/Examples/reflection-server/Sources/Generated/echo.pb.swift @@ -0,0 +1,129 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: echo.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright (c) 2015, Google Inc. +// +// 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 SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct Echo_EchoRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The text of a message to be echoed. + var text: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct Echo_EchoResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The text of an echo response. + var text: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "echo" + +extension Echo_EchoRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EchoRequest" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "text"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.text.isEmpty { + try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Echo_EchoRequest, rhs: Echo_EchoRequest) -> Bool { + if lhs.text != rhs.text {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Echo_EchoResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".EchoResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "text"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.text.isEmpty { + try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Echo_EchoResponse, rhs: Echo_EchoResponse) -> Bool { + if lhs.text != rhs.text {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Examples/reflection-server/Sources/ReflectionServer.swift b/Examples/reflection-server/Sources/ReflectionServer.swift new file mode 100644 index 000000000..1e70abe2c --- /dev/null +++ b/Examples/reflection-server/Sources/ReflectionServer.swift @@ -0,0 +1,71 @@ +/* + * 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 ArgumentParser +import Foundation +import GRPCCore +import GRPCNIOTransportHTTP2 +import GRPCProtobuf +import GRPCReflectionService + +@main +struct ReflectionServer: AsyncParsableCommand { + @Option(help: "The port to listen on") + var port: Int = 31415 + + func run() async throws { + // Find descriptor sets ('*.pb') bundled with this example. + let paths = Bundle.module.paths(forResourcesOfType: "pb", inDirectory: "DescriptorSets") + + // Start the server with the reflection service and the echo service. + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: self.port), + transportSecurity: .plaintext + ), + services: [ + try ReflectionService(descriptorSetFilePaths: paths), + EchoService(), + ] + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await server.serve() } + if let address = try await server.listeningAddress?.ipv4 { + print("Reflection server listening on \(address)") + print(String(repeating: "-", count: 80)) + + let example = """ + If you have grpcurl installed you can query the service to discover services + and make calls against them. You can install grpcurl by following the + instruction in its repository: https://github.com/fullstorydev/grpcurl + + Here are some example commands: + + List all services: + $ grpcurl -plaintext \(address.host):\(address.port) list + + Describe the 'Get' method in the 'echo.Echo' service: + $ grpcurl -plaintext \(address.host):\(address.port) describe echo.Echo.Get + + Call the 'echo.Echo.Get' method: + $ grpcurl -plaintext -d '{ "text": "Hello" }' \(address.host):\(address.port) echo.Echo.Get + """ + print(example) + } + } + } +} diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh index 294145be4..0dc5cabb4 100755 --- a/dev/protos/generate.sh +++ b/dev/protos/generate.sh @@ -105,6 +105,18 @@ function generate_error_details_example { generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" } +function generate_reflection_server_example { + local proto="$here/examples/echo/echo.proto" + local output="$root/Examples/reflection-server/Sources/Generated" + local pb_output="$root/Examples/reflection-server/Sources/DescriptorSets/echo.pb" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + invoke_protoc --descriptor_set_out="$pb_output" "$proto" -I "$(dirname "$proto")" \ + --include_source_info \ + --include_imports +} + #- TESTS ---------------------------------------------------------------------- function generate_service_config_for_tests { @@ -128,6 +140,7 @@ generate_echo_example generate_helloworld_example generate_routeguide_example generate_error_details_example +generate_reflection_server_example # Tests generate_service_config_for_tests From 0696e0adf7e081ba7a757a4c87242a1aab524078 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 20 Dec 2024 15:23:23 +0000 Subject: [PATCH 524/580] Bump versions (#2150) --- Examples/echo/Package.swift | 6 +++--- Examples/error-details/Package.swift | 4 ++-- Examples/hello-world/Package.swift | 6 +++--- Examples/reflection-server/Package.swift | 8 ++++---- Examples/route-guide/Package.swift | 6 +++--- README.md | 6 +++--- .../Tutorials/Hello-World/Hello-World.tutorial | 2 +- .../Resources/route-guide-sec01-step07-description.swift | 6 +++--- .../Resources/route-guide-sec01-step08-description.swift | 6 +++--- .../Resources/route-guide-sec05-step00-package.swift | 4 ++-- .../Resources/route-guide-sec05-step01-package.swift | 6 +++--- .../Resources/route-guide-sec05-step02-package.swift | 6 +++--- 12 files changed, 33 insertions(+), 33 deletions(-) diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index 8029be33f..bdbf47403 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift index 2598c8d80..ffd3f85a0 100644 --- a/Examples/error-details/Package.swift +++ b/Examples/error-details/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "error-details", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), ], targets: [ .executableTarget( diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 2dd86b73e..c73f1433d 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/reflection-server/Package.swift b/Examples/reflection-server/Package.swift index 37a3942b2..8a8041fcf 100644 --- a/Examples/reflection-server/Package.swift +++ b/Examples/reflection-server/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "reflection-server", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-extras.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-extras.git", exact: "1.0.0-beta.2"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index fa2554ed7..d9ee1b592 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/README.md b/README.md index ea764d8e0..82e23d403 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ let package = Package( name: "foo-package", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-alpha.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 6a056d47f..31273ed0b 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -18,7 +18,7 @@ repository by running the following command in a terminal: ```console - git clone --branch 2.0.0-beta.1 https://github.com/grpc/grpc-swift + git clone --branch 2.0.0-beta.2 https://github.com/grpc/grpc-swift ``` You then need to change directory to the `Examples/hello-world` directory of the cloned diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift index d7116586e..662795346 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), ], targets: [] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift index 462edb8d6..55dab65b4 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index d5526b86f..7deea7e81 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -5,8 +5,8 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index 8b4629476..67cf80691 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index bb1937d02..8462eada5 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ From 5e92f645c59923f535e9660fb917822dc8d9f48f Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 6 Jan 2025 16:06:40 +0000 Subject: [PATCH 525/580] Make empty generated source files descriptive (#2151) ### Motivation: Sometimes protobuf definition files contain no gRPC services and result in an empty file. This is intentional but could be confusing. ### Modifications: Empty files now contain a comment indicating they are intentional. ``` // This file contained no services. ``` This is analogous to the behavior of swift-protobuf which adds: ``` // This file contained no messages, enums, or extensions. ``` ### Result: More descriptive empty source files. --- .../StructuredSwiftRepresentation.swift | 2 +- .../IDLToStructuredSwiftTranslator.swift | 24 ++- Sources/GRPCCodeGen/SourceGenerator.swift | 5 +- .../StructuredSwift+ClientTests.swift | 2 +- .../StructuredSwift+ImportTests.swift | 201 ++++++++++++++++++ .../StructuredSwift+MetadataTests.swift | 2 +- .../StructuredSwift+ServerTests.swift | 2 +- .../Internal/StructuredSwiftTestHelpers.swift | 10 +- ...uredSwiftTranslatorSnippetBasedTests.swift | 177 ++------------- 9 files changed, 253 insertions(+), 172 deletions(-) create mode 100644 Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 3a0f1ee59..ddb340c66 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -30,7 +30,7 @@ /// A description of an import declaration. /// /// For example: `import Foo`. -struct ImportDescription: Equatable, Codable, Sendable { +package struct ImportDescription: Equatable, Codable, Sendable { /// The access level of the imported module. /// /// For example, the `public` in `public import Foo`. diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 839ef0fa1..612768cdd 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -17,7 +17,9 @@ /// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties. /// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications, /// using types from ``StructuredSwiftRepresentation``. -struct IDLToStructuredSwiftTranslator: Translator { +package struct IDLToStructuredSwiftTranslator: Translator { + package init() {} + func translate( codeGenerationRequest: CodeGenerationRequest, accessLevel: SourceGenerator.Config.AccessLevel, @@ -72,13 +74,23 @@ struct IDLToStructuredSwiftTranslator: Translator { } } - let fileDescription = FileDescription( - topComment: .preFormatted(codeGenerationRequest.leadingTrivia), - imports: try self.makeImports( + let imports: [ImportDescription] + if codeGenerationRequest.services.isEmpty { + imports = [] + codeBlocks.append( + CodeBlock(comment: .inline("This file contained no services.")) + ) + } else { + imports = try self.makeImports( dependencies: codeGenerationRequest.dependencies, accessLevel: accessLevel, accessLevelOnImports: accessLevelOnImports - ), + ) + } + + let fileDescription = FileDescription( + topComment: .preFormatted(codeGenerationRequest.leadingTrivia), + imports: imports, codeBlocks: codeBlocks ) @@ -87,7 +99,7 @@ struct IDLToStructuredSwiftTranslator: Translator { return StructuredSwiftRepresentation(file: file) } - private func makeImports( + package func makeImports( dependencies: [Dependency], accessLevel: SourceGenerator.Config.AccessLevel, accessLevelOnImports: Bool diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift index 454258bc6..e8a92ec5f 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -61,8 +61,8 @@ public struct SourceGenerator: Sendable { /// The possible access levels for the generated code. public struct AccessLevel: Sendable, Hashable { - internal var level: Level - internal enum Level { + package var level: Level + package enum Level { case `internal` case `public` case `package` @@ -94,6 +94,7 @@ public struct SourceGenerator: Sendable { client: self.config.client, server: self.config.server ) + let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) return sourceFile diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift index 50d6f7ada..caa8c9bc5 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -18,7 +18,7 @@ import Testing @testable import GRPCCodeGen -extension StructuedSwiftTests { +extension StructuredSwiftTests { @Suite("Client") struct Client { @Test( diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift new file mode 100644 index 000000000..ff1d34dbe --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift @@ -0,0 +1,201 @@ +/* + * Copyright 2025, 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 GRPCCodeGen +import Testing + +extension StructuredSwiftTests { + @Suite("Import") + struct Import { + static let translator = IDLToStructuredSwiftTranslator() + + static let allAccessLevels: [SourceGenerator.Config.AccessLevel] = [ + .internal, .public, .package, + ] + + @Test( + "import rendering", + arguments: allAccessLevels + ) + func imports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + var dependencies = [Dependency]() + dependencies.append(Dependency(module: "Foo", accessLevel: .public)) + dependencies.append( + Dependency( + item: .init(kind: .typealias, name: "Bar"), + module: "Foo", + accessLevel: .internal + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .struct, name: "Baz"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .class, name: "Bac"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .enum, name: "Bap"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .protocol, name: "Bat"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .let, name: "Baq"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .var, name: "Bag"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .func, name: "Bak"), + module: "Foo", + accessLevel: .package + ) + ) + + let expected = + """ + \(accessLevel.level) import GRPCCore + public import Foo + internal import typealias Foo.Bar + package import struct Foo.Baz + package import class Foo.Bac + package import enum Foo.Bap + package import protocol Foo.Bat + package import let Foo.Baq + package import var Foo.Bag + package import func Foo.Bak + """ + + let imports = try StructuredSwiftTests.Import.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true + ) + + #expect(render(imports) == expected) + } + + @Test( + "preconcurrency import rendering", + arguments: allAccessLevels + ) + func preconcurrencyImports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + var dependencies = [Dependency]() + dependencies.append( + Dependency( + module: "Foo", + preconcurrency: .required, + accessLevel: .internal + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + preconcurrency: .required, + accessLevel: .internal + ) + ) + dependencies.append( + Dependency( + module: "Baz", + preconcurrency: .requiredOnOS(["Deq", "Der"]), + accessLevel: .internal + ) + ) + + let expected = + """ + \(accessLevel.level) import GRPCCore + @preconcurrency internal import Foo + @preconcurrency internal import enum Foo.Bar + #if os(Deq) || os(Der) + @preconcurrency internal import Baz + #else + internal import Baz + #endif + """ + + let imports = try StructuredSwiftTests.Import.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true + ) + + #expect(render(imports) == expected) + } + + @Test( + "SPI import rendering", + arguments: allAccessLevels + ) + func spiImports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + var dependencies = [Dependency]() + dependencies.append( + Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) + ) + dependencies.append( + Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + spi: "Secret", + accessLevel: .internal + ) + ) + + let expected = + """ + \(accessLevel.level) import GRPCCore + @_spi(Secret) internal import Foo + @_spi(Secret) internal import enum Foo.Bar + """ + + let imports = try StructuredSwiftTests.Import.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true + ) + + #expect(render(imports) == expected) + } + + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index 8107b159c..1b8d9afd2 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -18,7 +18,7 @@ import Testing @testable import GRPCCodeGen -extension StructuedSwiftTests { +extension StructuredSwiftTests { @Suite("Metadata") struct Metadata { @Test("typealias Input = ", arguments: AccessModifier.allCases) diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index 45567bafd..a415307a6 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -18,7 +18,7 @@ import Testing @testable import GRPCCodeGen -extension StructuedSwiftTests { +extension StructuredSwiftTests { @Suite("Server") struct Server { @Test( diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift index d364e843f..b2c0d2847 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift @@ -19,8 +19,8 @@ import Testing @testable import GRPCCodeGen // Used as a namespace for organising other structured swift tests. -@Suite("Structued Swift") -struct StructuedSwiftTests {} +@Suite("Structured Swift") +struct StructuredSwiftTests {} func render(_ declaration: Declaration) -> String { let renderer = TextBasedRenderer(indentation: 2) @@ -40,6 +40,12 @@ func render(_ blocks: [CodeBlock]) -> String { return renderer.renderedContents() } +func render(_ imports: [ImportDescription]) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderImports(imports) + return renderer.renderedContents() +} + enum RPCKind: Hashable, Sendable, CaseIterable { case unary case clientStreaming diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index a64fe0451..d30fe8954 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -21,164 +21,6 @@ import XCTest @testable import GRPCCodeGen final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { - func testImports() throws { - var dependencies = [Dependency]() - dependencies.append(Dependency(module: "Foo", accessLevel: .public)) - dependencies.append( - Dependency( - item: .init(kind: .typealias, name: "Bar"), - module: "Foo", - accessLevel: .internal - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .struct, name: "Baz"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .class, name: "Bac"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .enum, name: "Bap"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .protocol, name: "Bat"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .let, name: "Baq"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .var, name: "Bag"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .func, name: "Bak"), - module: "Foo", - accessLevel: .package - ) - ) - - let expectedSwift = - """ - /// Some really exciting license header 2023. - - public import GRPCCore - public import Foo - internal import typealias Foo.Bar - package import struct Foo.Baz - package import class Foo.Bac - package import enum Foo.Bap - package import protocol Foo.Bat - package import let Foo.Baq - package import var Foo.Bag - package import func Foo.Bak - - """ - try self.assertIDLToStructuredSwiftTranslation( - codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testPreconcurrencyImports() throws { - var dependencies = [Dependency]() - dependencies.append( - Dependency( - module: "Foo", - preconcurrency: .required, - accessLevel: .internal - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .enum, name: "Bar"), - module: "Foo", - preconcurrency: .required, - accessLevel: .internal - ) - ) - dependencies.append( - Dependency( - module: "Baz", - preconcurrency: .requiredOnOS(["Deq", "Der"]), - accessLevel: .internal - ) - ) - let expectedSwift = - """ - /// Some really exciting license header 2023. - - public import GRPCCore - @preconcurrency internal import Foo - @preconcurrency internal import enum Foo.Bar - #if os(Deq) || os(Der) - @preconcurrency internal import Baz - #else - internal import Baz - #endif - - """ - try self.assertIDLToStructuredSwiftTranslation( - codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testSPIImports() throws { - var dependencies = [Dependency]() - dependencies.append( - Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) - ) - dependencies.append( - Dependency( - item: .init(kind: .enum, name: "Bar"), - module: "Foo", - spi: "Secret", - accessLevel: .internal - ) - ) - - let expectedSwift = - """ - /// Some really exciting license header 2023. - - public import GRPCCore - @_spi(Secret) internal import Foo - @_spi(Secret) internal import enum Foo.Bar - - """ - try self.assertIDLToStructuredSwiftTranslation( - codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - func testGeneration() throws { var dependencies = [Dependency]() dependencies.append( @@ -298,6 +140,25 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) } + func testEmptyFileGeneration() throws { + let expectedSwift = + """ + /// Some really exciting license header 2023. + + + // This file contained no services. + """ + try self.assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: makeCodeGenerationRequest( + services: [], + dependencies: [] + ), + expectedSwift: expectedSwift, + accessLevel: .public, + server: true + ) + } + private func assertIDLToStructuredSwiftTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, From 560c6c7bf5bb2217db4c2d39b888661dc242252c Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 7 Jan 2025 13:03:45 +0000 Subject: [PATCH 526/580] Use a nil Descriptor list for empty proto files (#2152) ### Motivation: If a proto file contains no services we include a comment indicating this is expected. At the moment this has two empty lines above it. ### Modifications: Store `nil` instead of an empty array of `Descriptor`s into the `FileDescription`. ### Result: Only one empty line above the comment. --- .../Internal/Translator/IDLToStructuredSwiftTranslator.swift | 4 ++-- .../IDLToStructuredSwiftTranslatorSnippetBasedTests.swift | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 612768cdd..eccc65ba3 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -74,9 +74,9 @@ package struct IDLToStructuredSwiftTranslator: Translator { } } - let imports: [ImportDescription] + let imports: [ImportDescription]? if codeGenerationRequest.services.isEmpty { - imports = [] + imports = nil codeBlocks.append( CodeBlock(comment: .inline("This file contained no services.")) ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index d30fe8954..8f4509b50 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -145,7 +145,6 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { """ /// Some really exciting license header 2023. - // This file contained no services. """ try self.assertIDLToStructuredSwiftTranslation( From 76073a2c1a2dcae35df4f0ab83ffd07b23e4cec9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 13 Jan 2025 14:02:47 +0000 Subject: [PATCH 527/580] Extend structured swift to support generic structs (#2154) Motivation: The structured swift representation we're using doesn't support generic struct at the moment. We'll need this for an upcoming change. Modifications: - Allow for generic structs with where clauses to be represented and rendered. Result: Generic structs can be rendered --- .../Internal/Renderer/TextBasedRenderer.swift | 21 ++++++++ .../StructuredSwiftRepresentation.swift | 6 +++ .../Renderer/TextBasedRendererTests.swift | 52 ++++++++++++++++++- 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 0900169c0..e1fac9546 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -740,10 +740,31 @@ struct TextBasedRenderer: RendererProtocol { } writer.writeLine("struct \(structDesc.name)") writer.nextLineAppendsToLastLine() + let generics = structDesc.generics + if !generics.isEmpty { + writer.nextLineAppendsToLastLine() + writer.writeLine("<") + for (genericType, isLast) in generics.enumeratedWithLastMarker() { + writer.nextLineAppendsToLastLine() + renderExistingTypeDescription(genericType) + if !isLast { + writer.nextLineAppendsToLastLine() + writer.writeLine(", ") + } + } + writer.nextLineAppendsToLastLine() + writer.writeLine(">") + writer.nextLineAppendsToLastLine() + } if !structDesc.conformances.isEmpty { writer.writeLine(": \(structDesc.conformances.joined(separator: ", "))") writer.nextLineAppendsToLastLine() } + if let whereClause = structDesc.whereClause { + writer.nextLineAppendsToLastLine() + writer.writeLine(" " + renderedWhereClause(whereClause)) + writer.nextLineAppendsToLastLine() + } writer.writeLine(" {") if !structDesc.members.isEmpty { writer.withNestedLevel { diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index ddb340c66..8db1cca87 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -401,11 +401,17 @@ struct StructDescription: Equatable, Codable, Sendable { /// For example, in `struct Foo {`, `name` is `Foo`. var name: String + /// The generic types of the struct. + var generics: [ExistingTypeDescription] = [] + /// The type names that the struct conforms to. /// /// For example: `["Sendable", "Codable"]`. var conformances: [String] = [] + /// A where clause constraining the struct declaration. + var whereClause: WhereClause? = nil + /// The declarations that make up the main struct body. var members: [Declaration] = [] } diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 65b5eb79b..036eb41e9 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -820,12 +820,62 @@ final class Test_TextBasedRenderer: XCTestCase { func testStruct() throws { try _test( - .init(name: "Structy"), + StructDescription(name: "Structy"), renderedBy: { $0.renderStruct(_:) }, rendersAs: #""" struct Structy {} """# ) + try _test( + StructDescription( + name: "Structy", + conformances: ["Foo"] + ), + renderedBy: { $0.renderStruct(_:) }, + rendersAs: #""" + struct Structy: Foo {} + """# + ) + try _test( + StructDescription( + name: "Structy", + generics: [.member("T")] + ), + renderedBy: { $0.renderStruct(_:) }, + rendersAs: #""" + struct Structy {} + """# + ) + try _test( + StructDescription( + name: "Structy", + generics: [.member("T")], + whereClause: WhereClause( + requirements: [ + .conformance("T", "Foo"), + .conformance("T", "Sendable"), + ] + ) + ), + renderedBy: { + $0.renderStruct(_:) + }, + rendersAs: #""" + struct Structy where T: Foo, T: Sendable {} + """# + ) + try _test( + StructDescription( + name: "Structy", + generics: [.member("T")], + conformances: ["Hashable"], + whereClause: WhereClause(requirements: [.conformance("T", "Foo")]) + ), + renderedBy: { $0.renderStruct(_:) }, + rendersAs: #""" + struct Structy: Hashable where T: Foo {} + """# + ) } func testProtocol() throws { From 1fb16264905b39cc3c08c179f36e5ad3901b4e25 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 16 Jan 2025 13:09:45 +0000 Subject: [PATCH 528/580] Add more properties to `ClientContext` and have the `ClientTransport` provide it (#2158) The `ServerTransport` provides the `ServerContext`, as it contains information that only the transport knows about (such as the remote peer's address). For consistency and to allow the `ClientContext` to also hold some additional information (such as remote and local peer descriptions), this PR changes the `ClientTransport` protocol so that implementations also provide the corresponding `ClientContext`. This PR also adds additional information to the context (which will be used by the tracing interceptor but can be useful for users in general): remote and local peer addresses, server hostname, and network transport. --- .../StructuredSwift+ServiceMetadata.swift | 1 + .../GRPCCore/Call/Client/ClientContext.swift | 34 ++++++++++++++++++- .../ClientRPCExecutor+HedgingExecutor.swift | 4 +-- .../ClientRPCExecutor+OneShotExecutor.swift | 7 ++-- .../ClientRPCExecutor+RetryExecutor.swift | 6 ++-- .../Client/Internal/ClientRPCExecutor.swift | 6 ++-- .../GRPCCore/Transport/ClientTransport.swift | 6 ++-- .../InProcessTransport+Client.swift | 18 +++++++--- .../InProcessTransport+Server.swift | 5 ++- .../InProcessTransport.swift | 2 +- ...ientRPCExecutorTestHarness+Transport.swift | 4 +-- Tests/GRPCCoreTests/GRPCServerTests.swift | 30 ++++++++-------- .../Transport/AnyTransport.swift | 8 ++--- .../Transport/StreamCountingTransport.swift | 6 ++-- .../Transport/ThrowingTransport.swift | 2 +- .../InProcessClientTransportTests.swift | 26 ++++++++------ dev/license-check.sh | 2 +- 17 files changed, 111 insertions(+), 56 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index ad7109a9d..2804eaab0 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -45,6 +45,7 @@ extension VariableDescription { /// static let descriptor = GRPCCore.MethodDescriptor( /// service: GRPCCore.ServiceDescriptor(fullyQualifiedServiceName: ""), /// method: "" + /// ) /// ``` package static func methodDescriptor( accessModifier: AccessModifier? = nil, diff --git a/Sources/GRPCCore/Call/Client/ClientContext.swift b/Sources/GRPCCore/Call/Client/ClientContext.swift index 51eaa1a21..613cf0c36 100644 --- a/Sources/GRPCCore/Call/Client/ClientContext.swift +++ b/Sources/GRPCCore/Call/Client/ClientContext.swift @@ -19,8 +19,40 @@ public struct ClientContext: Sendable { /// A description of the method being called. public var descriptor: MethodDescriptor + /// A description of the remote peer. + /// + /// The format of the description should follow the pattern ":
" where + /// "" indicates the underlying network transport (such as "ipv4", "unix", or + /// "in-process"). This is a guideline for how descriptions should be formatted; different + /// implementations may not follow this format so you shouldn't make assumptions based on it. + /// + /// Some examples include: + /// - "ipv4:127.0.0.1:31415", + /// - "ipv6:[::1]:443", + /// - "in-process:27182". + public var remotePeer: String + + /// A description of the local peer. + /// + /// The format of the description should follow the pattern ":
" where + /// "" indicates the underlying network transport (such as "ipv4", "unix", or + /// "in-process"). This is a guideline for how descriptions should be formatted; different + /// implementations may not follow this format so you shouldn't make assumptions based on it. + /// + /// Some examples include: + /// - "ipv4:127.0.0.1:31415", + /// - "ipv6:[::1]:443", + /// - "in-process:27182". + public var localPeer: String + /// Create a new client interceptor context. - public init(descriptor: MethodDescriptor) { + public init( + descriptor: MethodDescriptor, + remotePeer: String, + localPeer: String + ) { self.descriptor = descriptor + self.remotePeer = remotePeer + self.localPeer = localPeer } } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index cb44fefff..480b23817 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -322,7 +322,7 @@ extension ClientRPCExecutor.HedgingExecutor { return try await self.transport.withStream( descriptor: method, options: options - ) { stream -> _HedgingAttemptTaskResult.AttemptResult in + ) { stream, context -> _HedgingAttemptTaskResult.AttemptResult in return await withTaskGroup(of: _HedgingAttemptTaskResult.self) { group in group.addTask { do { @@ -348,8 +348,8 @@ extension ClientRPCExecutor.HedgingExecutor { let response = await ClientRPCExecutor._execute( in: &group, + context: context, request: request, - method: method, attempt: attempt, serializer: self.serializer, deserializer: self.deserializer, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index 32dde4a66..cc21ad4fc 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -98,11 +98,14 @@ extension ClientRPCExecutor.OneShotExecutor { ) async -> Result { return await withTaskGroup(of: Void.self, returning: Result.self) { group in do { - return try await self.transport.withStream(descriptor: method, options: options) { stream in + return try await self.transport.withStream( + descriptor: method, + options: options + ) { stream, context in let response = await ClientRPCExecutor._execute( in: &group, + context: context, request: request, - method: method, attempt: 1, serializer: self.serializer, deserializer: self.deserializer, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 6e7da3433..03acd0543 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -118,7 +118,7 @@ extension ClientRPCExecutor.RetryExecutor { let attemptResult = try await self.transport.withStream( descriptor: method, options: options - ) { stream in + ) { stream, context in group.addTask { var metadata = request.metadata // Work out the timeout from the deadline. @@ -127,6 +127,7 @@ extension ClientRPCExecutor.RetryExecutor { } return await self.executeAttempt( + context: context, stream: stream, metadata: metadata, retryStream: retry.stream, @@ -194,6 +195,7 @@ extension ClientRPCExecutor.RetryExecutor { @inlinable func executeAttempt( + context: ClientContext, stream: RPCStream, metadata: Metadata, retryStream: BroadcastAsyncSequence, @@ -211,8 +213,8 @@ extension ClientRPCExecutor.RetryExecutor { let response = await ClientRPCExecutor._execute( in: &group, + context: context, request: request, - method: method, attempt: attempt, serializer: self.serializer, deserializer: self.deserializer, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index ade536d65..0f94d817e 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -104,25 +104,25 @@ extension ClientRPCExecutor { /// /// - Parameters: /// - request: The request to execute. - /// - method: A description of the method to execute the request against. + /// - context: The ``ClientContext`` related to this request. /// - attempt: The attempt number of the request. /// - serializer: A serializer to convert input messages to bytes. /// - deserializer: A deserializer to convert bytes to output messages. /// - interceptors: An array of interceptors which the request and response pass through. The /// interceptors will be called in the order of the array. + /// - stream: The stream to excecute the RPC on. /// - Returns: The deserialized response. @inlinable // would be private static func _execute( in group: inout TaskGroup, + context: ClientContext, request: StreamingClientRequest, - method: MethodDescriptor, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, interceptors: [any ClientInterceptor], stream: RPCStream ) async -> StreamingClientResponse { - let context = ClientContext(descriptor: method) if interceptors.isEmpty { return await ClientStreamExecutor.execute( diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index a86a79fea..659244e11 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -47,7 +47,7 @@ public protocol ClientTransport: Sendable { /// running ``connect()``. func beginGracefulShutdown() - /// Opens a stream using the transport, and uses it as input into a user-provided closure. + /// Opens a stream using the transport, and uses it as input into a user-provided closure alongisde the given context. /// /// - Important: The opened stream is closed after the closure is finished. /// @@ -59,12 +59,12 @@ public protocol ClientTransport: Sendable { /// - Parameters: /// - descriptor: A description of the method to open a stream for. /// - options: Options specific to the stream. - /// - closure: A closure that takes the opened stream as parameter. + /// - closure: A closure that takes the opened stream and the client context as its parameters. /// - Returns: Whatever value was returned from `closure`. func withStream( descriptor: MethodDescriptor, options: CallOptions, - _ closure: (_ stream: RPCStream) async throws -> T + _ closure: (_ stream: RPCStream, _ context: ClientContext) async throws -> T ) async throws -> T /// Returns the configuration for a given method. diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift index b24ec07e1..aa28fbac2 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -103,19 +103,23 @@ extension InProcessTransport { private let methodConfig: MethodConfigs private let state: Mutex + private let peer: String /// Creates a new in-process client transport. /// /// - Parameters: /// - server: The in-process server transport to connect to. /// - serviceConfig: Service configuration. + /// - peer: The system's PID for the running client and server. package init( server: InProcessTransport.Server, - serviceConfig: ServiceConfig = ServiceConfig() + serviceConfig: ServiceConfig = ServiceConfig(), + peer: String ) { self.retryThrottle = serviceConfig.retryThrottling.map { RetryThrottle(policy: $0) } self.methodConfig = MethodConfigs(serviceConfig: serviceConfig) self.state = Mutex(.unconnected(.init(serverTransport: server))) + self.peer = peer } /// Establish and maintain a connection to the remote destination. @@ -225,12 +229,12 @@ extension InProcessTransport { /// - Parameters: /// - descriptor: A description of the method to open a stream for. /// - options: Options specific to the stream. - /// - closure: A closure that takes the opened stream as parameter. + /// - closure: A closure that takes the opened stream and the client context as its parameters. /// - Returns: Whatever value was returned from `closure`. public func withStream( descriptor: MethodDescriptor, options: CallOptions, - _ closure: (RPCStream) async throws -> T + _ closure: (RPCStream, ClientContext) async throws -> T ) async throws -> T { let request = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) let response = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) @@ -297,11 +301,17 @@ extension InProcessTransport { } } + let clientContext = ClientContext( + descriptor: descriptor, + remotePeer: self.peer, + localPeer: self.peer + ) + switch acceptStream { case .success(let streamID): let streamHandlingResult: Result do { - let result = try await closure(clientStream) + let result = try await closure(clientStream, clientContext) streamHandlingResult = .success(result) } catch { streamHandlingResult = .failure(error) diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 90e291b6e..659c53465 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -34,7 +34,7 @@ extension InProcessTransport { private let newStreams: AsyncStream> private let newStreamsContinuation: AsyncStream>.Continuation - private let peer: String + package let peer: String private struct State: Sendable { private var _nextID: UInt64 @@ -74,6 +74,9 @@ extension InProcessTransport { private let handles: Mutex /// Creates a new instance of ``Server``. + /// + /// - Parameters: + /// - peer: The system's PID for the running client and server. package init(peer: String) { (self.newStreams, self.newStreamsContinuation) = AsyncStream.makeStream() self.handles = Mutex(State()) diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index cd891a64c..e73beee91 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -27,6 +27,6 @@ public struct InProcessTransport: Sendable { public init(serviceConfig: ServiceConfig = ServiceConfig()) { let peer = "in-process:\(System.pid())" self.server = Self.Server(peer: peer) - self.client = Self.Client(server: self.server, serviceConfig: serviceConfig) + self.client = Self.Client(server: self.server, serviceConfig: serviceConfig, peer: peer) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index aa5a21332..c80a63b7b 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023, gRPC Authors All rights reserved. + * Copyright 2023-2025, 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. @@ -21,6 +21,6 @@ extension InProcessTransport.Server { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) ) -> InProcessTransport.Client { - return InProcessTransport.Client(server: self) + return InProcessTransport.Client(server: self, peer: self.peer) } } diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 388940e83..02a0672e1 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -48,7 +48,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3, 1, 4, 1, 5])) await stream.outbound.finish() @@ -75,7 +75,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.collect, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3])) try await stream.outbound.write(.message([1])) @@ -106,7 +106,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.expand, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3, 1, 4, 1, 5])) await stream.outbound.finish() @@ -135,7 +135,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.update, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) for byte in [3, 1, 4, 1, 5] as [UInt8] { try await stream.outbound.write(.message([byte])) @@ -166,7 +166,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) await stream.outbound.finish() @@ -187,7 +187,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([i])) await stream.outbound.finish() @@ -225,7 +225,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) await stream.outbound.finish() @@ -250,7 +250,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) await stream.outbound.finish() @@ -277,7 +277,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in XCTFail("Stream shouldn't be opened") } } errorHandler: { error in @@ -291,7 +291,7 @@ final class GRPCServerTests: XCTestCase { try await client.withStream( descriptor: BinaryEcho.Methods.update, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) var iterator = stream.inbound.makeAsyncIterator() // Don't need to validate the response, just that the server is running. @@ -364,7 +364,7 @@ final class GRPCServerTests: XCTestCase { try await transport.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([0])) await stream.outbound.finish() @@ -407,7 +407,7 @@ struct ServerTests { try await client.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message(Array("hello".utf8))) await stream.outbound.finish() @@ -437,7 +437,7 @@ struct ServerTests { try await client.withStream( descriptor: HelloWorld.Methods.sayHello, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message(Array("Swift".utf8))) await stream.outbound.finish() @@ -494,7 +494,7 @@ struct ServerTests { try await client.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message(Array("hello".utf8))) await stream.outbound.finish() @@ -524,7 +524,7 @@ struct ServerTests { try await client.withStream( descriptor: BinaryEcho.Methods.collect, options: .defaults - ) { stream in + ) { stream, _ in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message(Array("hello".utf8))) await stream.outbound.finish() diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index 7ca178fef..e63b5e5f1 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -24,7 +24,7 @@ struct AnyClientTransport: ClientTransport, Sendable { @Sendable ( _ method: MethodDescriptor, _ options: CallOptions, - _ body: (RPCStream) async throws -> (any Sendable) + _ body: (RPCStream, ClientContext) async throws -> (any Sendable) ) async throws -> Any private let _connect: @Sendable () async throws -> Void private let _close: @Sendable () -> Void @@ -34,8 +34,8 @@ struct AnyClientTransport: ClientTransport, Sendable { where Transport.Inbound == Inbound, Transport.Outbound == Outbound { self._retryThrottle = { transport.retryThrottle } self._withStream = { descriptor, options, closure in - try await transport.withStream(descriptor: descriptor, options: options) { stream in - try await closure(stream) as (any Sendable) + try await transport.withStream(descriptor: descriptor, options: options) { stream, context in + try await closure(stream, context) as (any Sendable) } } @@ -67,7 +67,7 @@ struct AnyClientTransport: ClientTransport, Sendable { func withStream( descriptor: MethodDescriptor, options: CallOptions, - _ closure: (RPCStream) async throws -> T + _ closure: (RPCStream, ClientContext) async throws -> T ) async throws -> T { let result = try await self._withStream(descriptor, options, closure) return result as! T diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 970109286..abd052e7c 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -54,15 +54,15 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { func withStream( descriptor: MethodDescriptor, options: CallOptions, - _ closure: (RPCStream) async throws -> T + _ closure: (RPCStream, ClientContext) async throws -> T ) async throws -> T { do { return try await self.transport.withStream( descriptor: descriptor, options: options - ) { stream in + ) { stream, context in self._streamsOpened.increment() - return try await closure(stream) + return try await closure(stream, context) } } catch { self._streamFailures.increment() diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index e73bdbdf1..7f7649eb1 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -44,7 +44,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { func withStream( descriptor: MethodDescriptor, options: CallOptions, - _ closure: (RPCStream) async throws -> T + _ closure: (RPCStream, ClientContext) async throws -> T ) async throws -> T { throw RPCError(code: self.code, message: "") } diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 8d4e3a2ae..c1e5dfc9b 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -1,5 +1,5 @@ /* - * Copyright 2023, gRPC Authors All rights reserved. + * Copyright 2023-2025, 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. @@ -110,7 +110,7 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.withStream(descriptor: .testTest, options: .defaults) { _ in + try await client.withStream(descriptor: .testTest, options: .defaults) { _, _ in // Once the pending stream is opened, close the client to new connections, // so that, once this closure is executed and this stream is closed, // the client will return from `connect()`. @@ -135,7 +135,7 @@ final class InProcessClientTransportTests: XCTestCase { client.beginGracefulShutdown() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await client.withStream(descriptor: .testTest, options: .defaults) { _ in } + try await client.withStream(descriptor: .testTest, options: .defaults) { _, _ in } } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) } @@ -151,7 +151,7 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream(descriptor: .testTest, options: .defaults) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream, _ in try await stream.outbound.write(.message([1])) await stream.outbound.finish() let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } @@ -198,9 +198,11 @@ final class InProcessClientTransportTests: XCTestCase { ] ) + let peer = "in-process:1234" var client = InProcessTransport.Client( - server: InProcessTransport.Server(peer: "in-process:1234"), - serviceConfig: serviceConfig + server: InProcessTransport.Server(peer: peer), + serviceConfig: serviceConfig, + peer: peer ) let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "first") @@ -223,8 +225,9 @@ final class InProcessClientTransportTests: XCTestCase { ) serviceConfig.methodConfig.append(overrideConfiguration) client = InProcessTransport.Client( - server: InProcessTransport.Server(peer: "in-process:1234"), - serviceConfig: serviceConfig + server: InProcessTransport.Server(peer: peer), + serviceConfig: serviceConfig, + peer: peer ) let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "second") @@ -248,13 +251,13 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream(descriptor: .testTest, options: .defaults) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream, _ in try await Task.sleep(for: .milliseconds(100)) } } group.addTask { - try await client.withStream(descriptor: .testTest, options: .defaults) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream, _ in try await Task.sleep(for: .milliseconds(100)) } } @@ -290,7 +293,8 @@ final class InProcessClientTransportTests: XCTestCase { return InProcessTransport.Client( server: server, - serviceConfig: serviceConfig + serviceConfig: serviceConfig, + peer: server.peer ) } } diff --git a/dev/license-check.sh b/dev/license-check.sh index 889478b31..be92bcf85 100755 --- a/dev/license-check.sh +++ b/dev/license-check.sh @@ -88,7 +88,7 @@ check_copyright_headers() { actual_sha=$(head -n "$((drop_first + expected_lines))" "$filename" \ | tail -n "$expected_lines" \ - | sed -e 's/201[56789]-20[12][0-9]/YEARS/' -e 's/20[12][0-9]/YEARS/' \ + | sed -e 's/20[12][0-9]-20[12][0-9]/YEARS/' -e 's/20[12][0-9]/YEARS/' \ | shasum \ | awk '{print $1}') From 421450417a7a3d97e012c2a2826e2215d88f80c9 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 17 Jan 2025 08:05:49 +0000 Subject: [PATCH 529/580] Add remote and local peers to `ServerContext` (#2161) This PR adds a `localPeer` property to the `ServerContext`, and renames `peer` to `remotePeer`. This is related to https://github.com/grpc/grpc-swift/pull/2158 on the client side and will be used to improve the server tracing interceptor. --- .../GRPCCore/Call/Client/ClientContext.swift | 5 +++ .../Call/Client/ClientInterceptor.swift | 6 +-- .../GRPCCore/Call/Server/ServerContext.swift | 41 +++++++++++++++++-- .../InProcessTransport+Server.swift | 3 +- .../ServerRPCExecutorTestHarness.swift | 3 +- .../InProcessTransportTests.swift | 33 +++++++++++---- 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/ClientContext.swift b/Sources/GRPCCore/Call/Client/ClientContext.swift index 613cf0c36..679496ed4 100644 --- a/Sources/GRPCCore/Call/Client/ClientContext.swift +++ b/Sources/GRPCCore/Call/Client/ClientContext.swift @@ -46,6 +46,11 @@ public struct ClientContext: Sendable { public var localPeer: String /// Create a new client interceptor context. + /// + /// - Parameters: + /// - descriptor: A description of the method being called. + /// - remotePeer: A description of the remote peer. + /// - localPeer: A description of the local peer. public init( descriptor: MethodDescriptor, remotePeer: String, diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 68a1fcf45..890a49f0c 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +// - FIXME: Update example and documentation to show how to register an interceptor. + /// A type that intercepts requests and response for clients. /// /// Interceptors allow you to inspect and modify requests and responses. Requests are intercepted @@ -21,14 +23,12 @@ /// received from the transport. They are typically used for cross-cutting concerns like injecting /// metadata, validating messages, logging additional data, and tracing. /// -/// Interceptors are registered with the server via ``ClientInterceptorPipelineOperation``s. +/// Interceptors are registered with the client via ``ClientInterceptorPipelineOperation``s. /// You may register them for all services registered with a server, for RPCs directed to specific services, or /// for RPCs directed to specific methods. If you need to modify the behavior of an interceptor on a /// per-RPC basis in more detail, then you can use the ``ClientContext/descriptor`` to determine /// which RPC is being called and conditionalise behavior accordingly. /// -/// - TODO: Update example and documentation to show how to register an interceptor. -/// /// Some examples of simple interceptors follow. /// /// ## Metadata injection diff --git a/Sources/GRPCCore/Call/Server/ServerContext.swift b/Sources/GRPCCore/Call/Server/ServerContext.swift index 504e9563a..812a22886 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -30,7 +30,37 @@ public struct ServerContext: Sendable { /// - "ipv4:127.0.0.1:31415", /// - "ipv6:[::1]:443", /// - "in-process:27182". - public var peer: String + @available(*, deprecated, renamed: "remotePeer") + public var peer: String { + get { remotePeer } + set { remotePeer = newValue } + } + + /// A description of the remote peer. + /// + /// The format of the description should follow the pattern ":
" where + /// "" indicates the underlying network transport (such as "ipv4", "unix", or + /// "in-process"). This is a guideline for how descriptions should be formatted; different + /// implementations may not follow this format so you shouldn't make assumptions based on it. + /// + /// Some examples include: + /// - "ipv4:127.0.0.1:31415", + /// - "ipv6:[::1]:443", + /// - "in-process:27182". + public var remotePeer: String + + /// A description of the local peer. + /// + /// The format of the description should follow the pattern ":
" where + /// "" indicates the underlying network transport (such as "ipv4", "unix", or + /// "in-process"). This is a guideline for how descriptions should be formatted; different + /// implementations may not follow this format so you shouldn't make assumptions based on it. + /// + /// Some examples include: + /// - "ipv4:127.0.0.1:31415", + /// - "ipv6:[::1]:443", + /// - "in-process:27182". + public var localPeer: String /// A handle for checking the cancellation status of an RPC. public var cancellation: RPCCancellationHandle @@ -39,16 +69,19 @@ public struct ServerContext: Sendable { /// /// - Parameters: /// - descriptor: A description of the method being called. - /// - peer: A description of the remote peer. + /// - remotePeer: A description of the remote peer. + /// - localPeer: A description of the local peer. /// - cancellation: A cancellation handle. You can create a cancellation handle /// using ``withServerContextRPCCancellationHandle(_:)``. public init( descriptor: MethodDescriptor, - peer: String, + remotePeer: String, + localPeer: String, cancellation: RPCCancellationHandle ) { self.descriptor = descriptor - self.peer = peer + self.remotePeer = remotePeer + self.localPeer = localPeer self.cancellation = cancellation } } diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 659c53465..d8b385ce3 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -122,7 +122,8 @@ extension InProcessTransport { let context = ServerContext( descriptor: stream.descriptor, - peer: self.peer, + remotePeer: self.peer, + localPeer: self.peer, cancellation: handle ) await streamHandler(stream, context) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index 6634d9e9f..d9beb0d6a 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -102,7 +102,8 @@ struct ServerRPCExecutorTestHarness { await withServerContextRPCCancellationHandle { cancellation in let context = ServerContext( descriptor: MethodDescriptor(fullyQualifiedService: "foo", method: "bar"), - peer: "tests", + remotePeer: "remote", + localPeer: "local", cancellation: cancellation ) diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 786f6fe99..04b8265a0 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -1,5 +1,5 @@ /* - * Copyright 2024, gRPC Authors All rights reserved. + * Copyright 2024-2025, 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. @@ -77,14 +77,13 @@ struct InProcessTransportTests { request: ClientRequest(message: ()), descriptor: .peerInfo, serializer: VoidSerializer(), - deserializer: UTF8Deserializer(), + deserializer: PeerInfoDeserializer(), options: .defaults ) { try $0.message } - let match = peerInfo.wholeMatch(of: /in-process:\d+/) - #expect(match != nil) + #expect(peerInfo.local == peerInfo.remote) } } } @@ -122,8 +121,9 @@ private struct TestService: RegistrableRPCService { func peerInfo( request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse { - return ServerResponse(message: context.peer) + ) async throws -> ServerResponse { + let peerInfo = PeerInfo(local: context.localPeer, remote: context.remotePeer) + return ServerResponse(message: peerInfo) } func registerMethods(with router: inout RPCRouter) { @@ -139,7 +139,7 @@ private struct TestService: RegistrableRPCService { router.registerHandler( forMethod: .peerInfo, deserializer: VoidDeserializer(), - serializer: UTF8Serializer(), + serializer: PeerInfoSerializer(), handler: { let response = try await self.peerInfo( request: ServerRequest(stream: $0), @@ -163,6 +163,25 @@ extension MethodDescriptor { ) } +private struct PeerInfo: Codable { + var local: String + var remote: String +} + +private struct PeerInfoSerializer: MessageSerializer { + func serialize(_ message: PeerInfo) throws -> [UInt8] { + Array("\(message.local) \(message.remote)".utf8) + } +} + +private struct PeerInfoDeserializer: MessageDeserializer { + func deserialize(_ serializedMessageBytes: [UInt8]) throws -> PeerInfo { + let stringPeerInfo = String(decoding: serializedMessageBytes, as: UTF8.self) + let peerInfoComponents = stringPeerInfo.split(separator: " ") + return PeerInfo(local: String(peerInfoComponents[0]), remote: String(peerInfoComponents[1])) + } +} + private struct UTF8Serializer: MessageSerializer { func serialize(_ message: String) throws -> [UInt8] { Array(message.utf8) From a92a58b250744f8314e1faab1461e0d6bf4d76dc Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 17 Jan 2025 09:42:33 +0000 Subject: [PATCH 530/580] Group DocC docs in the index page (#2160) Motivation: Grouping DocC docs by their area makes it easier to find things. Modifications: - Group docs in the index page - Update various docs Result: Better docs --- .../GRPCCore/Configuration/MethodConfig.swift | 2 + .../Configuration/ServiceConfig.swift | 7 +- .../Articles/{Errors.md => Error-handling.md} | 0 .../Documentation.docc/Documentation.md | 82 ++++++++++++++++++- .../Streaming/RPCWriterProtocol.swift | 3 +- .../GRPCCore/Transport/ClientTransport.swift | 11 +++ .../GRPCCore/Transport/ServerTransport.swift | 9 +- 7 files changed, 110 insertions(+), 4 deletions(-) rename Sources/GRPCCore/Documentation.docc/Articles/{Errors.md => Error-handling.md} (100%) diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index 23a867926..2de35e5a4 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -18,6 +18,7 @@ /// /// See also: https://github.com/grpc/grpc-proto/blob/0b30c8c05277ab78ec72e77c9cbf66a26684673d/grpc/service_config/service_config.proto public struct MethodConfig: Hashable, Sendable { + /// The name of a method to which the method config applies. public struct Name: Sendable, Hashable { /// The name of the service, including the namespace. /// @@ -143,6 +144,7 @@ public struct MethodConfig: Hashable, Sendable { } } +/// Whether an RPC should be retried or hedged. public struct RPCExecutionPolicy: Hashable, Sendable { @usableFromInline enum Wrapped: Hashable, Sendable { diff --git a/Sources/GRPCCore/Configuration/ServiceConfig.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift index 316ad8b61..805e5ea59 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -16,7 +16,12 @@ /// Service configuration values. /// -/// See also: https://github.com/grpc/grpc-proto/blob/0b30c8c05277ab78ec72e77c9cbf66a26684673d/grpc/service_config/service_config.proto +/// A service config mostly contains parameters describing how clients connecting to a service +/// should behave (for example, the load balancing policy to use). +/// +/// The schema is described by [`grpc/service_config/service_config.proto`](https://github.com/grpc/grpc-proto/blob/0b30c8c05277ab78ec72e77c9cbf66a26684673d/grpc/service_config/service_config.proto) +/// in the `grpc/grpc-proto` GitHub repository although gRPC uses it in its JSON form rather than +/// the Protobuf form. public struct ServiceConfig: Hashable, Sendable { /// Per-method configuration. public var methodConfig: [MethodConfig] diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Errors.md b/Sources/GRPCCore/Documentation.docc/Articles/Error-handling.md similarity index 100% rename from Sources/GRPCCore/Documentation.docc/Articles/Errors.md rename to Sources/GRPCCore/Documentation.docc/Articles/Error-handling.md diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index f70913cea..e14f03669 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -52,7 +52,7 @@ as tutorials. ### Essentials - -- +- ### Project Information @@ -64,3 +64,83 @@ Resources for developers working on gRPC Swift: - - + +### Client and Server + +- ``GRPCClient`` +- ``GRPCServer`` +- ``withGRPCClient(transport:interceptors:isolation:handleClient:)`` +- ``withGRPCClient(transport:interceptorPipeline:isolation:handleClient:)`` +- ``withGRPCServer(transport:services:interceptors:isolation:handleServer:)`` +- ``withGRPCServer(transport:services:interceptorPipeline:isolation:handleServer:)`` + +### Request and response types + +- ``ClientRequest`` +- ``StreamingClientRequest`` +- ``ClientResponse`` +- ``StreamingClientResponse`` +- ``ServerRequest`` +- ``StreamingServerRequest`` +- ``ServerResponse`` +- ``StreamingServerResponse`` + +### Service definition and routing + +- ``RegistrableRPCService`` +- ``RPCRouter`` + +### Interceptors + +- ``ClientInterceptor`` +- ``ServerInterceptor`` +- ``ClientContext`` +- ``ServerContext`` +- ``ClientInterceptorPipelineOperation`` +- ``ServerInterceptorPipelineOperation`` + +### RPC descriptors + +- ``MethodDescriptor`` +- ``ServiceDescriptor`` + +### Service config + +- ``ServiceConfig`` +- ``MethodConfig`` +- ``HedgingPolicy`` +- ``RetryPolicy`` +- ``RPCExecutionPolicy`` + +### Serialization + +- ``MessageSerializer`` +- ``MessageDeserializer`` +- ``CompressionAlgorithm`` +- ``CompressionAlgorithmSet`` + +### Transport protocols and supporting types + +- ``ClientTransport`` +- ``ServerTransport`` +- ``RPCRequestPart`` +- ``RPCResponsePart`` +- ``Status`` +- ``Metadata`` +- ``RetryThrottle`` +- ``RPCStream`` +- ``RPCWriterProtocol`` +- ``ClosableRPCWriterProtocol`` +- ``RPCWriter`` +- ``RPCAsyncSequence`` + +### Cancellation + +- ``withServerContextRPCCancellationHandle(_:)`` +- ``withRPCCancellationHandler(operation:onCancelRPC:)`` + +### Errors + +- ``RPCError`` +- ``RPCErrorConvertible`` +- ``RuntimeError`` diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index 076043e30..2f1c706d5 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -/// A sink for values which are produced over time. +/// A type into which values can be written indefinitely. public protocol RPCWriterProtocol: Sendable { /// The type of value written. associatedtype Element: Sendable @@ -49,6 +49,7 @@ extension RPCWriterProtocol { } } +/// A type into which values can be written until it is finished. public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// Indicate to the writer that no more writes are to be accepted. /// diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 659244e11..52003ef6a 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -14,6 +14,17 @@ * limitations under the License. */ +/// A type that provides a long-lived bidirectional communication channel to a server. +/// +/// The client transport is responsible for providing streams to a backend on top of which an +/// RPC can be executed. A typical transport implementation will establish and maintain connections +/// to a server (or servers) and manage these over time, potentially closing idle connections and +/// creating new ones on demand. As such transports can be expensive to create and as such are +/// intended to be used as long-lived objects which exist for the lifetime of your application. +/// +/// gRPC provides an in-process transport in the `GRPCInProcessTransport` module and HTTP/2 +/// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport +/// package. public protocol ClientTransport: Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index 15148c78e..a7b926019 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -14,7 +14,14 @@ * limitations under the License. */ -/// A protocol server transport implementations must conform to. +/// A type that provides a bidirectional communication channel with a client. +/// +/// The server transport is responsible for handling connections created by a client and +/// the multiplexing of those connections into streams corresponding to RPCs. +/// +/// gRPC provides an in-process transport in the `GRPCInProcessTransport` module and HTTP/2 +/// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport +/// package. public protocol ServerTransport: Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable From eafb3341cc2d5099ce1ccfa6a95f516d8759d644 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 17 Jan 2025 09:50:57 +0000 Subject: [PATCH 531/580] Rename 'GRPCClient.run()' (#2156) Motivation: To support swift-service-lifecycle the grpc client and server types will conform to its 'Service' protocol. This has a requirement for a method called 'run()'. Ideally this would respect graceful shutdown. We can't do this with the current client as we already have a run method. To do this we'd need to wrap the type and redeclare all of it's methods. Using a wrapper isn't a good user experience. Modifications: - Rename run to 'maintainConnections()' - Deprecate 'run()', we'll remove this later Result: - run is deprecated --- Sources/GRPCCore/GRPCClient.swift | 17 +++++++++++------ Tests/GRPCCoreTests/GRPCClientTests.swift | 12 ++++++------ .../InProcessTransportTests.swift | 4 ++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index b9d3234b1..a19a4941b 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -45,10 +45,10 @@ private import Synchronization /// ## Creating a client manually /// /// If the `with`-style methods for creating clients isn't suitable for your application then you -/// can create and run a client manually. This requires you to call the ``run()`` method in a task +/// can create and run a client manually. This requires you to call the ``runConnections()`` method in a task /// which instructs the client to start connecting to the server. /// -/// The ``run()`` method won't return until the client has finished handling all requests. You can +/// The ``runConnections()`` method won't return until the client has finished handling all requests. You can /// signal to the client that it should stop creating new request streams by calling ``beginGracefulShutdown()``. /// This gives the client enough time to drain any requests already in flight. To stop the client /// more abruptly you can cancel the task running your client. If your application requires @@ -114,7 +114,7 @@ public final class GRPCClient: Sendable { func checkExecutable() throws { switch self { case .notStarted, .running: - // Allow .notStarted as making a request can race with 'run()'. Transports should tolerate + // Allow .notStarted as making a request can race with 'runConnections()'. Transports should tolerate // queuing the request if not yet started. () case .stopping, .stopped: @@ -208,7 +208,7 @@ public final class GRPCClient: Sendable { /// /// The client, and by extension this function, can only be run once. If the client is already /// running or has already been closed then a ``RuntimeError`` is thrown. - public func run() async throws { + public func runConnections() async throws { try self.stateMachine.withLock { try $0.state.run() } // When this function exits the client must have stopped. @@ -227,6 +227,11 @@ public final class GRPCClient: Sendable { } } + @available(*, deprecated, renamed: "runConnections", message: "It'll be removed before v2.") + public func run() async throws { + try await self.runConnections() + } + /// Close the client. /// /// The transport will be closed: this means that it will be given enough time to wait for @@ -338,7 +343,7 @@ public final class GRPCClient: Sendable { /// Start a bidirectional streaming RPC. /// - /// - Note: ``run()`` must have been called and still executing, and ``beginGracefulShutdown()`` mustn't + /// - Note: ``runConnections()`` must have been called and still executing, and ``beginGracefulShutdown()`` mustn't /// have been called. /// /// - Parameters: @@ -430,7 +435,7 @@ public func withGRPCClient( try await withThrowingDiscardingTaskGroup { group in let client = GRPCClient(transport: transport, interceptorPipeline: interceptorPipeline) group.addTask { - try await client.run() + try await client.runConnections() } let result = try await handleClient(client) diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 0152a9c81..9300bf44e 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -336,7 +336,7 @@ final class GRPCClientTests: XCTestCase { } group.addTask { - try await client.run() + try await client.runConnections() } // Wait for client and server to be running. @@ -377,13 +377,13 @@ final class GRPCClientTests: XCTestCase { let inProcess = InProcessTransport() let client = GRPCClient(transport: inProcess.client) // Run the client. - let task = Task { try await client.run() } + let task = Task { try await client.runConnections() } task.cancel() try await task.value // Client is stopped, should throw an error. await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { - try await client.run() + try await client.runConnections() } errorHandler: { error in XCTAssertEqual(error.code, .clientIsStopped) } @@ -393,13 +393,13 @@ final class GRPCClientTests: XCTestCase { let inProcess = InProcessTransport() let client = GRPCClient(transport: inProcess.client) // Run the client. - let task = Task { try await client.run() } + let task = Task { try await client.runConnections() } // Make sure the client is run for the first time here. try await Task.sleep(for: .milliseconds(10)) // Client is already running, should throw an error. await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { - try await client.run() + try await client.runConnections() } errorHandler: { error in XCTAssertEqual(error.code, .clientIsAlreadyRunning) } @@ -551,7 +551,7 @@ struct ClientTests { } group.addTask { - try await client.run() + try await client.runConnections() } // Make sure both server and client are running diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 04b8265a0..2a90aee5e 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -35,7 +35,7 @@ struct InProcessTransportTests { let client = GRPCClient(transport: inProcess.client) group.addTask { - try await client.run() + try await client.runConnections() } try await execute(server, client) @@ -60,7 +60,7 @@ struct InProcessTransportTests { #expect(messages == ["isCancelled=true"]) } - // Finally, shutdown the client so its run() method returns. + // Finally, shutdown the client so its runConnections() method returns. client.beginGracefulShutdown() } } From eb7ed6fc3573bb4d57bd31057a5dba45eb4d3f3c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 17 Jan 2025 10:05:42 +0000 Subject: [PATCH 532/580] Deduplicate interceptor pipeline operation (#2157) Motivation: ClientInterceptorPipelineOperation is the same as ServerInterceptorPipelineOperation apart from the type of interceptor being used. The duplication here is unnecessary. Modifications: - Add a `ConditionalInterceptor` which is roughly the same as `*InterceptorPipelineOperation` but is generic. - The init is private and can only be initialized via static methods when the generic type is appropriate (i.e. client/server interceptor). Result: Less duplication, less code --------- Co-authored-by: Gus Cairo --- .../Call/Client/ClientInterceptor.swift | 2 +- ...ion.swift => ConditionalInterceptor.swift} | 59 ++++++----- Sources/GRPCCore/Call/Server/RPCRouter.swift | 5 +- .../Call/Server/ServerInterceptor.swift | 2 +- .../ServerInterceptorPipelineOperation.swift | 99 ------------------- .../Documentation.docc/Documentation.md | 3 +- Sources/GRPCCore/GRPCClient.swift | 12 +-- Sources/GRPCCore/GRPCServer.swift | 4 +- ...wift => ConditionalInterceptorTests.swift} | 20 ++-- .../ServerInterceptorPipelineOperation.swift | 68 ------------- Tests/GRPCCoreTests/GRPCClientTests.swift | 4 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 4 +- 12 files changed, 61 insertions(+), 221 deletions(-) rename Sources/GRPCCore/Call/{Client/ClientInterceptorPipelineOperation.swift => ConditionalInterceptor.swift} (69%) delete mode 100644 Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift rename Tests/GRPCCoreTests/Call/{Client/ClientInterceptorPipelineOperationTests.swift => ConditionalInterceptorTests.swift} (75%) delete mode 100644 Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 890a49f0c..d09d654b5 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -23,7 +23,7 @@ /// received from the transport. They are typically used for cross-cutting concerns like injecting /// metadata, validating messages, logging additional data, and tracing. /// -/// Interceptors are registered with the client via ``ClientInterceptorPipelineOperation``s. +/// Interceptors are registered with the client via ``ConditionalInterceptor``s. /// You may register them for all services registered with a server, for RPCs directed to specific services, or /// for RPCs directed to specific methods. If you need to modify the behavior of an interceptor on a /// per-RPC basis in more detail, then you can use the ``ClientContext/descriptor`` to determine diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/ConditionalInterceptor.swift similarity index 69% rename from Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift rename to Sources/GRPCCore/Call/ConditionalInterceptor.swift index d67047bc7..1a61755ac 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift +++ b/Sources/GRPCCore/Call/ConditionalInterceptor.swift @@ -14,18 +14,16 @@ * limitations under the License. */ -/// A `ClientInterceptorPipelineOperation` describes to which RPCs a client interceptor should be applied. +/// Describes the conditions under which an interceptor should be applied. /// -/// You can configure a client interceptor to be applied to: +/// You can configure interceptors to be applied to: /// - all RPCs and services; /// - requests directed only to specific services; or /// - requests directed only to specific methods (of a specific service). /// -/// - SeeAlso: ``ClientInterceptor`` for more information on client interceptors, and -/// ``ServerInterceptorPipelineOperation`` for the server-side version of this type. -public struct ClientInterceptorPipelineOperation: Sendable { - /// The subject of a ``ClientInterceptorPipelineOperation``. - /// The subject of an interceptor can either be all services and methods, only specific services, or only specific methods. +/// - SeeAlso: ``ClientInterceptor`` and ``ServerInterceptor`` for more information on client and +/// server interceptors, respectively. +public struct ConditionalInterceptor: Sendable { public struct Subject: Sendable { internal enum Wrapped: Sendable { case all @@ -41,7 +39,6 @@ public struct ClientInterceptorPipelineOperation: Sendable { /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services. /// - Parameters: /// - services: The list of service names for which this interceptor should intercept RPCs. - /// - Returns: A ``ClientInterceptorPipelineOperation``. public static func services(_ services: Set) -> Self { Self(wrapped: .services(services)) } @@ -49,13 +46,12 @@ public struct ClientInterceptorPipelineOperation: Sendable { /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified service methods. /// - Parameters: /// - methods: The list of method descriptors for which this interceptor should intercept RPCs. - /// - Returns: A ``ClientInterceptorPipelineOperation``. public static func methods(_ methods: Set) -> Self { Self(wrapped: .methods(methods)) } @usableFromInline - internal func applies(to descriptor: MethodDescriptor) -> Bool { + package func applies(to descriptor: MethodDescriptor) -> Bool { switch self.wrapped { case .all: return true @@ -69,24 +65,15 @@ public struct ClientInterceptorPipelineOperation: Sendable { } } - /// The interceptor specified for this operation. - public let interceptor: any ClientInterceptor + /// The interceptor. + public let interceptor: Interceptor @usableFromInline internal let subject: Subject - private init(interceptor: any ClientInterceptor, appliesTo: Subject) { + fileprivate init(interceptor: Interceptor, subject: Subject) { self.interceptor = interceptor - self.subject = appliesTo - } - - /// Create an operation, specifying which ``ClientInterceptor`` to apply and to which ``Subject``. - /// - Parameters: - /// - interceptor: The ``ClientInterceptor`` to register with the client. - /// - subject: The ``Subject`` to which the `interceptor` applies. - /// - Returns: A ``ClientInterceptorPipelineOperation``. - public static func apply(_ interceptor: any ClientInterceptor, to subject: Subject) -> Self { - Self(interceptor: interceptor, appliesTo: subject) + self.subject = subject } /// Returns whether this ``ClientInterceptorPipelineOperation`` applies to the given `descriptor`. @@ -97,3 +84,29 @@ public struct ClientInterceptorPipelineOperation: Sendable { self.subject.applies(to: descriptor) } } + +extension ConditionalInterceptor where Interceptor == any ClientInterceptor { + /// Create an operation, specifying which ``ClientInterceptor`` to apply and to which ``Subject``. + /// - Parameters: + /// - interceptor: The ``ClientInterceptor`` to register with the client. + /// - subject: The ``Subject`` to which the `interceptor` applies. + public static func apply( + _ interceptor: any ClientInterceptor, + to subject: Subject + ) -> Self { + Self(interceptor: interceptor, subject: subject) + } +} + +extension ConditionalInterceptor where Interceptor == any ServerInterceptor { + /// Create an operation, specifying which ``ServerInterceptor`` to apply and to which ``Subject``. + /// - Parameters: + /// - interceptor: The ``ServerInterceptor`` to register with the server. + /// - subject: The ``Subject`` to which the `interceptor` applies. + public static func apply( + _ interceptor: any ServerInterceptor, + to subject: Subject + ) -> Self { + Self(interceptor: interceptor, subject: subject) + } +} diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index d40bd71c4..9cab254af 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -155,9 +155,10 @@ public struct RPCRouter: Sendable { /// only call this method _after_ you have registered all handlers. /// - Parameter pipeline: The interceptor pipeline operations to register to all currently-registered handlers. The order of the /// interceptors matters. - /// - SeeAlso: ``ServerInterceptorPipelineOperation``. @inlinable - public mutating func registerInterceptors(pipeline: [ServerInterceptorPipelineOperation]) { + public mutating func registerInterceptors( + pipeline: [ConditionalInterceptor] + ) { for descriptor in self.handlers.keys { let applicableOperations = pipeline.filter { $0.applies(to: descriptor) } if !applicableOperations.isEmpty { diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index e90266862..c6310edc3 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -21,7 +21,7 @@ /// been returned from a service. They are typically used for cross-cutting concerns like filtering /// requests, validating messages, logging additional data, and tracing. /// -/// Interceptors can be registered with the server either directly or via ``ServerInterceptorPipelineOperation``s. +/// Interceptors can be registered with the server either directly or via ``ConditionalInterceptor``s. /// You may register them for all services registered with a server, for RPCs directed to specific services, or /// for RPCs directed to specific methods. If you need to modify the behavior of an interceptor on a /// per-RPC basis in more detail, then you can use the ``ServerContext/descriptor`` to determine diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift deleted file mode 100644 index e657fdf1c..000000000 --- a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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. - */ - -/// A `ServerInterceptorPipelineOperation` describes to which RPCs a server interceptor should be applied. -/// -/// You can configure a server interceptor to be applied to: -/// - all RPCs and services; -/// - requests directed only to specific services registered with your server; or -/// - requests directed only to specific methods (of a specific service). -/// -/// - SeeAlso: ``ServerInterceptor`` for more information on server interceptors, and -/// ``ClientInterceptorPipelineOperation`` for the client-side version of this type. -public struct ServerInterceptorPipelineOperation: Sendable { - /// The subject of a ``ServerInterceptorPipelineOperation``. - /// The subject of an interceptor can either be all services and methods, only specific services, or only specific methods. - public struct Subject: Sendable { - internal enum Wrapped: Sendable { - case all - case services(Set) - case methods(Set) - } - - private let wrapped: Wrapped - - /// An operation subject specifying an interceptor that applies to all RPCs across all services will be registered with this server. - public static var all: Self { .init(wrapped: .all) } - - /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services. - /// - Parameters: - /// - services: The list of service names for which this interceptor should intercept RPCs. - /// - Returns: A ``ServerInterceptorPipelineOperation``. - public static func services(_ services: Set) -> Self { - Self(wrapped: .services(services)) - } - - /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified service methods. - /// - Parameters: - /// - methods: The list of method descriptors for which this interceptor should intercept RPCs. - /// - Returns: A ``ServerInterceptorPipelineOperation``. - public static func methods(_ methods: Set) -> Self { - Self(wrapped: .methods(methods)) - } - - @usableFromInline - internal func applies(to descriptor: MethodDescriptor) -> Bool { - switch self.wrapped { - case .all: - return true - - case .services(let services): - return services.contains(descriptor.service) - - case .methods(let methods): - return methods.contains(descriptor) - } - } - } - - /// The interceptor specified for this operation. - public let interceptor: any ServerInterceptor - - @usableFromInline - internal let subject: Subject - - private init(interceptor: any ServerInterceptor, appliesTo: Subject) { - self.interceptor = interceptor - self.subject = appliesTo - } - - /// Create an operation, specifying which ``ServerInterceptor`` to apply and to which ``Subject``. - /// - Parameters: - /// - interceptor: The ``ServerInterceptor`` to register with the server. - /// - subject: The ``Subject`` to which the `interceptor` applies. - /// - Returns: A ``ServerInterceptorPipelineOperation``. - public static func apply(_ interceptor: any ServerInterceptor, to subject: Subject) -> Self { - Self(interceptor: interceptor, appliesTo: subject) - } - - /// Returns whether this ``ServerInterceptorPipelineOperation`` applies to the given `descriptor`. - /// - Parameter descriptor: A ``MethodDescriptor`` for which to test whether this interceptor applies. - /// - Returns: `true` if this interceptor applies to the given `descriptor`, or `false` otherwise. - @inlinable - internal func applies(to descriptor: MethodDescriptor) -> Bool { - self.subject.applies(to: descriptor) - } -} diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index e14f03669..8a35bf9d9 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -96,8 +96,7 @@ Resources for developers working on gRPC Swift: - ``ServerInterceptor`` - ``ClientContext`` - ``ServerContext`` -- ``ClientInterceptorPipelineOperation`` -- ``ServerInterceptorPipelineOperation`` +- ``ConditionalInterceptor`` ### RPC descriptors diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index a19a4941b..8e084b6c0 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -129,7 +129,7 @@ public final class GRPCClient: Sendable { private struct StateMachine { var state: State - private let interceptorPipeline: [ClientInterceptorPipelineOperation] + private let interceptorPipeline: [ConditionalInterceptor] /// A collection of interceptors providing cross-cutting functionality to each accepted RPC, keyed by the method to which they apply. /// @@ -142,7 +142,7 @@ public final class GRPCClient: Sendable { /// the appropriate handler. var interceptorsPerMethod: [MethodDescriptor: [any ClientInterceptor]] - init(interceptorPipeline: [ClientInterceptorPipelineOperation]) { + init(interceptorPipeline: [ConditionalInterceptor]) { self.state = .notStarted self.interceptorPipeline = interceptorPipeline self.interceptorsPerMethod = [:] @@ -188,14 +188,14 @@ public final class GRPCClient: Sendable { /// /// - Parameters: /// - transport: The transport used to establish a communication channel with a server. - /// - interceptorPipeline: A collection of ``ClientInterceptorPipelineOperation`` providing cross-cutting + /// - interceptorPipeline: A collection of ``ConditionalInterceptor``s providing cross-cutting /// functionality to each accepted RPC. Only applicable interceptors from the pipeline will be applied to each RPC. /// The order in which interceptors are added reflects the order in which they are called. /// The first interceptor added will be the first interceptor to intercept each request. /// The last interceptor added will be the final interceptor to intercept each request before calling the appropriate handler. public init( transport: some ClientTransport, - interceptorPipeline: [ClientInterceptorPipelineOperation] + interceptorPipeline: [ConditionalInterceptor] ) { self.transport = transport self.stateMachine = Mutex(StateMachine(interceptorPipeline: interceptorPipeline)) @@ -416,7 +416,7 @@ public func withGRPCClient( /// /// - Parameters: /// - transport: The transport used to establish a communication channel with a server. -/// - interceptorPipeline: A collection of ``ClientInterceptorPipelineOperation`` providing cross-cutting +/// - interceptorPipeline: A collection of ``ConditionalInterceptor``s providing cross-cutting /// functionality to each accepted RPC. Only applicable interceptors from the pipeline will be applied to each RPC. /// The order in which interceptors are added reflects the order in which they are called. /// The first interceptor added will be the first interceptor to intercept each request. @@ -428,7 +428,7 @@ public func withGRPCClient( /// - Returns: The result of the `handleClient` closure. public func withGRPCClient( transport: some ClientTransport, - interceptorPipeline: [ClientInterceptorPipelineOperation], + interceptorPipeline: [ConditionalInterceptor], isolation: isolated (any Actor)? = #isolation, handleClient: (GRPCClient) async throws -> Result ) async throws -> Result { diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index f8f576e65..1b930b8d8 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -171,7 +171,7 @@ public final class GRPCServer: Sendable { public convenience init( transport: any ServerTransport, services: [any RegistrableRPCService], - interceptorPipeline: [ServerInterceptorPipelineOperation] + interceptorPipeline: [ConditionalInterceptor] ) { var router = RPCRouter() for service in services { @@ -290,7 +290,7 @@ public func withGRPCServer( public func withGRPCServer( transport: any ServerTransport, services: [any RegistrableRPCService], - interceptorPipeline: [ServerInterceptorPipelineOperation], + interceptorPipeline: [ConditionalInterceptor], isolation: isolated (any Actor)? = #isolation, handleServer: (GRPCServer) async throws -> Result ) async throws -> Result { diff --git a/Tests/GRPCCoreTests/Call/Client/ClientInterceptorPipelineOperationTests.swift b/Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift similarity index 75% rename from Tests/GRPCCoreTests/Call/Client/ClientInterceptorPipelineOperationTests.swift rename to Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift index 43d70bec5..cd688f3c6 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientInterceptorPipelineOperationTests.swift +++ b/Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift @@ -14,12 +14,11 @@ * limitations under the License. */ +import GRPCCore import Testing -@testable import GRPCCore - -@Suite("ClientInterceptorPipelineOperation") -struct ClientInterceptorPipelineOperationTests { +@Suite("ConditionalInterceptor") +struct ConditionalInterceptorTests { @Test( "Applies to", arguments: [ @@ -38,24 +37,19 @@ struct ClientInterceptorPipelineOperationTests { [.barFoo], [.fooBar, .fooBaz, .barBaz] ), - ] as [(ClientInterceptorPipelineOperation.Subject, [MethodDescriptor], [MethodDescriptor])] + ] as [(ConditionalInterceptor.Subject, [MethodDescriptor], [MethodDescriptor])] ) func appliesTo( - operationSubject: ClientInterceptorPipelineOperation.Subject, + target: ConditionalInterceptor.Subject, applicableMethods: [MethodDescriptor], notApplicableMethods: [MethodDescriptor] ) { - let operation = ClientInterceptorPipelineOperation.apply( - .requestCounter(.init()), - to: operationSubject - ) - for applicableMethod in applicableMethods { - #expect(operation.applies(to: applicableMethod)) + #expect(target.applies(to: applicableMethod)) } for notApplicableMethod in notApplicableMethods { - #expect(!operation.applies(to: notApplicableMethod)) + #expect(!target.applies(to: notApplicableMethod)) } } } diff --git a/Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift b/Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift deleted file mode 100644 index d907276c5..000000000 --- a/Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 Testing - -@testable import GRPCCore - -@Suite("ServerInterceptorPipelineOperation") -struct ServerInterceptorPipelineOperationTests { - @Test( - "Applies to", - arguments: [ - ( - .all, - [.fooBar, .fooBaz, .barFoo, .barBaz], - [] - ), - ( - .services([ServiceDescriptor(package: "pkg", service: "foo")]), - [.fooBar, .fooBaz], - [.barFoo, .barBaz] - ), - ( - .methods([.barFoo]), - [.barFoo], - [.fooBar, .fooBaz, .barBaz] - ), - ] as [(ServerInterceptorPipelineOperation.Subject, [MethodDescriptor], [MethodDescriptor])] - ) - func appliesTo( - operationSubject: ServerInterceptorPipelineOperation.Subject, - applicableMethods: [MethodDescriptor], - notApplicableMethods: [MethodDescriptor] - ) { - let operation = ServerInterceptorPipelineOperation.apply( - .requestCounter(.init()), - to: operationSubject - ) - - for applicableMethod in applicableMethods { - #expect(operation.applies(to: applicableMethod)) - } - - for notApplicableMethod in notApplicableMethods { - #expect(!operation.applies(to: notApplicableMethod)) - } - } -} - -extension MethodDescriptor { - fileprivate static let fooBar = Self(fullyQualifiedService: "pkg.foo", method: "bar") - fileprivate static let fooBaz = Self(fullyQualifiedService: "pkg.foo", method: "baz") - fileprivate static let barFoo = Self(fullyQualifiedService: "pkg.bar", method: "foo") - fileprivate static let barBaz = Self(fullyQualifiedService: "pkg.bar", method: "Baz") -} diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 9300bf44e..878e4a365 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -22,7 +22,7 @@ import XCTest final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], - interceptorPipeline: [ClientInterceptorPipelineOperation] = [], + interceptorPipeline: [ConditionalInterceptor] = [], _ body: (GRPCClient, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() @@ -538,7 +538,7 @@ struct ClientTests { func withInProcessConnectedClient( services: [any RegistrableRPCService], - interceptorPipeline: [ClientInterceptorPipelineOperation] = [], + interceptorPipeline: [ConditionalInterceptor] = [], _ body: (GRPCClient, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 02a0672e1..8e17ca174 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -22,7 +22,7 @@ import XCTest final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], - interceptorPipeline: [ServerInterceptorPipelineOperation] = [], + interceptorPipeline: [ConditionalInterceptor] = [], _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() @@ -553,7 +553,7 @@ struct ServerTests { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], - interceptorPipeline: [ServerInterceptorPipelineOperation] = [], + interceptorPipeline: [ConditionalInterceptor] = [], _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() From 93d0536d4eaded276281ad7b2d7fe40ed35bb907 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 17 Jan 2025 11:02:03 +0000 Subject: [PATCH 533/580] Make transport generic over its bag-of-bytes type (#2155) Motivation: The transport protocols deal in request/response part types. The bag-of-bytes message type used in `[UInt8]`. This means that transports might have to copy to and from the bag-of-bytes they use which is inefficient. Modifications: - Add a `GRPCContiguousBytes` protocol defining a basic bag-of-bytes type. - Make the transport protocols have an associated `Bytes` type which conforms to `GRPCContiguousBytes`. - Propagate this requirement throughout the codebase; this affects the generated code. - Update the code generator to generate the appropriate code. - Update tests Result: - Transports can use a bag-of-bytes type of their choosing. --- .../Internal/StructuredSwift+Client.swift | 25 ++++++-- .../Internal/StructuredSwift+Server.swift | 8 ++- .../Internal/StructuredSwift+Types.swift | 11 +++- .../ClientRPCExecutor+RetryExecutor.swift | 7 ++- .../Client/Internal/ClientRPCExecutor.swift | 7 ++- .../Internal/ClientStreamExecutor.swift | 27 +++++---- .../Server/Internal/ServerRPCExecutor.swift | 36 ++++++------ Sources/GRPCCore/Call/Server/RPCRouter.swift | 14 ++--- .../Call/Server/RegistrableRPCService.swift | 2 +- Sources/GRPCCore/Coding/Coding.swift | 4 +- .../GRPCCore/Coding/GRPCContiguousBytes.swift | 58 +++++++++++++++++++ Sources/GRPCCore/GRPCClient.swift | 20 +++---- Sources/GRPCCore/GRPCServer.swift | 28 ++++----- .../RPCWriter+MessageToRPCResponsePart.swift | 13 +++-- .../Internal/RPCWriter+Serialize.swift | 5 +- .../GRPCCore/Transport/ClientTransport.swift | 9 ++- Sources/GRPCCore/Transport/RPCParts.swift | 16 +++-- .../GRPCCore/Transport/ServerTransport.swift | 9 ++- .../InProcessTransport+Client.swift | 15 ++--- .../InProcessTransport+Server.swift | 6 +- .../StructuredSwift+ClientTests.swift | 6 +- .../StructuredSwift+ServerTests.swift | 2 +- ...lientCodeTranslatorSnippetBasedTests.swift | 6 +- ...uredSwiftTranslatorSnippetBasedTests.swift | 2 +- ...erverCodeTranslatorSnippetBasedTests.swift | 2 +- ...PCExecutorTestHarness+ServerBehavior.swift | 14 ++--- .../ServerRPCExecutorTestHarness.swift | 21 +++---- .../Internal/ServerRPCExecutorTests.swift | 5 ++ .../Call/Server/RPCRouterTests.swift | 21 ++++++- Tests/GRPCCoreTests/Coding/CodingTests.swift | 2 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 26 +++------ Tests/GRPCCoreTests/GRPCServerTests.swift | 20 +++---- Tests/GRPCCoreTests/RPCPartsTests.swift | 4 +- .../Test Utilities/Coding+Identity.swift | 10 ++-- .../Test Utilities/Coding+JSON.swift | 10 ++-- .../Test Utilities/Services/BinaryEcho.swift | 2 +- .../Test Utilities/Services/HelloWorld.swift | 2 +- .../Transport/AnyTransport.swift | 11 ++-- .../Transport/StreamCountingTransport.swift | 11 ++-- .../Transport/ThrowingTransport.swift | 7 ++- .../Test Utilities/XCTest+Utilities.swift | 24 ++++---- .../InProcessServerTransportTests.swift | 18 +++--- .../InProcessTransportTests.swift | 33 ++++++----- 43 files changed, 360 insertions(+), 219 deletions(-) create mode 100644 Sources/GRPCCore/Coding/GRPCContiguousBytes.swift diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift index 6c026f2d3..4e9cc905b 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -645,10 +645,10 @@ extension FunctionDescription { extension StructDescription { /// ``` - /// struct : { - /// private let client: GRPCCore.GRPCClient + /// struct : where Transport: GRPCCore.ClientTransport { + /// private let client: GRPCCore.GRPCClient /// - /// init(wrapping client: GRPCCore.GRPCClient) { + /// init(wrapping client: GRPCCore.GRPCClient) { /// self.client = client /// } /// @@ -665,9 +665,18 @@ extension StructDescription { StructDescription( accessModifier: accessLevel, name: name, + generics: [.member("Transport")], conformances: [clientProtocol], + whereClause: WhereClause( + requirements: [.conformance("Transport", "GRPCCore.ClientTransport")] + ), members: [ - .variable(accessModifier: .private, kind: .let, left: "client", type: .grpcClient), + .variable( + accessModifier: .private, + kind: .let, + left: "client", + type: .grpcClient(genericOver: "Transport") + ), .commentable( .preFormatted( """ @@ -681,7 +690,13 @@ extension StructDescription { accessModifier: accessLevel, kind: .initializer, parameters: [ - ParameterDescription(label: "wrapping", name: "client", type: .grpcClient) + ParameterDescription( + label: "wrapping", + name: "client", + type: .grpcClient( + genericOver: "Transport" + ) + ) ], whereClause: nil, body: [ diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index 95643d4d0..7af446a5e 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -311,14 +311,20 @@ extension FunctionDescription { return FunctionDescription( accessModifier: accessLevel, kind: .function(name: "registerMethods"), + generics: [.member("Transport")], parameters: [ ParameterDescription( label: "with", name: "router", - type: .rpcRouter, + type: .rpcRouter(genericOver: "Transport"), `inout`: true ) ], + whereClause: WhereClause( + requirements: [ + .conformance("Transport", "GRPCCore.ServerTransport") + ] + ), body: methods.map { method in .functionCall( .registerWithRouter( diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift index 4706e9888..86abaaf71 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift @@ -54,7 +54,11 @@ extension ExistingTypeDescription { } package static let serverContext: Self = .grpcCore("ServerContext") - package static let rpcRouter: Self = .grpcCore("RPCRouter") + + package static func rpcRouter(genericOver type: String) -> Self { + .generic(wrapper: .grpcCore("RPCRouter"), wrapped: .member(type)) + } + package static let serviceDescriptor: Self = .grpcCore("ServiceDescriptor") package static let methodDescriptor: Self = .grpcCore("MethodDescriptor") @@ -80,5 +84,8 @@ extension ExistingTypeDescription { package static let callOptions: Self = .grpcCore("CallOptions") package static let metadata: Self = .grpcCore("Metadata") - package static let grpcClient: Self = .grpcCore("GRPCClient") + + package static func grpcClient(genericOver transport: String) -> Self { + .generic(wrapper: .grpcCore("GRPCClient"), wrapped: [.member(transport)]) + } } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 03acd0543..bfc62203f 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -194,9 +194,12 @@ extension ClientRPCExecutor.RetryExecutor { } @inlinable - func executeAttempt( + func executeAttempt( context: ClientContext, - stream: RPCStream, + stream: RPCStream< + RPCAsyncSequence, any Error>, + RPCWriter>.Closable + >, metadata: Metadata, retryStream: BroadcastAsyncSequence, method: MethodDescriptor, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 0f94d817e..9a1da1f7d 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -113,7 +113,7 @@ extension ClientRPCExecutor { /// - stream: The stream to excecute the RPC on. /// - Returns: The deserialized response. @inlinable // would be private - static func _execute( + static func _execute( in group: inout TaskGroup, context: ClientContext, request: StreamingClientRequest, @@ -121,7 +121,10 @@ extension ClientRPCExecutor { serializer: some MessageSerializer, deserializer: some MessageDeserializer, interceptors: [any ClientInterceptor], - stream: RPCStream + stream: RPCStream< + RPCAsyncSequence, any Error>, + RPCWriter>.Closable + > ) async -> StreamingClientResponse { if interceptors.isEmpty { diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index f4cf1c482..1b2cac862 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -28,14 +28,17 @@ internal enum ClientStreamExecutor { /// - stream: The stream to excecute the RPC on. /// - Returns: A streamed response. @inlinable - static func execute( + static func execute( in group: inout TaskGroup, request: StreamingClientRequest, context: ClientContext, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, - stream: RPCStream + stream: RPCStream< + RPCAsyncSequence, any Error>, + RPCWriter>.Closable + > ) async -> StreamingClientResponse { // Let the server know this is a retry. var metadata = request.metadata @@ -83,8 +86,8 @@ internal enum ClientStreamExecutor { } @inlinable // would be private - static func _processRequest( - on stream: some ClosableRPCWriterProtocol, + static func _processRequest( + on stream: some ClosableRPCWriterProtocol>, request: StreamingClientRequest, serializer: some MessageSerializer ) async { @@ -104,16 +107,19 @@ internal enum ClientStreamExecutor { } @usableFromInline - enum OnFirstResponsePart: Sendable { - case metadata(Metadata, UnsafeTransfer) + enum OnFirstResponsePart: Sendable { + case metadata( + Metadata, + UnsafeTransfer, any Error>.AsyncIterator> + ) case status(Status, Metadata) case failed(RPCError) } @inlinable // would be private - static func _waitForFirstResponsePart( - on stream: ClientTransport.Inbound - ) async -> OnFirstResponsePart { + static func _waitForFirstResponsePart( + on stream: RPCAsyncSequence, any Error> + ) async -> OnFirstResponsePart { var iterator = stream.makeAsyncIterator() let result = await Result { switch try await iterator.next() { @@ -165,7 +171,8 @@ internal enum ClientStreamExecutor { @usableFromInline struct RawBodyPartToMessageSequence< - Base: AsyncSequence, + Base: AsyncSequence, Failure>, + Bytes: GRPCContiguousBytes, Message: Sendable, Deserializer: MessageDeserializer, Failure: Error diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 50ff0b3bd..e2184de14 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -27,11 +27,11 @@ struct ServerRPCExecutor { /// interceptors will be called in the order of the array. /// - handler: A handler which turns the request into a response. @inlinable - static func execute( + static func execute( context: ServerContext, stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >, deserializer: some MessageDeserializer, serializer: some MessageSerializer, @@ -66,11 +66,11 @@ struct ServerRPCExecutor { } @inlinable - static func _execute( + static func _execute( context: ServerContext, metadata: Metadata, - inbound: UnsafeTransfer.AsyncIterator>, - outbound: RPCWriter.Closable, + inbound: UnsafeTransfer, any Error>.AsyncIterator>, + outbound: RPCWriter>.Closable, deserializer: some MessageDeserializer, serializer: some MessageSerializer, interceptors: [any ServerInterceptor], @@ -106,12 +106,12 @@ struct ServerRPCExecutor { } @inlinable - static func _processRPCWithTimeout( + static func _processRPCWithTimeout( timeout: Duration, context: ServerContext, metadata: Metadata, - inbound: UnsafeTransfer.AsyncIterator>, - outbound: RPCWriter.Closable, + inbound: UnsafeTransfer, any Error>.AsyncIterator>, + outbound: RPCWriter>.Closable, deserializer: some MessageDeserializer, serializer: some MessageSerializer, interceptors: [any ServerInterceptor], @@ -147,11 +147,11 @@ struct ServerRPCExecutor { } @inlinable - static func _processRPC( + static func _processRPC( context: ServerContext, metadata: Metadata, - inbound: UnsafeTransfer.AsyncIterator>, - outbound: RPCWriter.Closable, + inbound: UnsafeTransfer, any Error>.AsyncIterator>, + outbound: RPCWriter>.Closable, deserializer: some MessageDeserializer, serializer: some MessageSerializer, interceptors: [any ServerInterceptor], @@ -235,12 +235,12 @@ struct ServerRPCExecutor { } @inlinable - static func _waitForFirstRequestPart( - inbound: RPCAsyncSequence - ) async -> OnFirstRequestPart { + static func _waitForFirstRequestPart( + inbound: RPCAsyncSequence, any Error> + ) async -> OnFirstRequestPart { var iterator = inbound.makeAsyncIterator() let part = await Result { try await iterator.next() } - let onFirstRequestPart: OnFirstRequestPart + let onFirstRequestPart: OnFirstRequestPart switch part { case .success(.metadata(let metadata)): @@ -275,10 +275,10 @@ struct ServerRPCExecutor { } @usableFromInline - enum OnFirstRequestPart { + enum OnFirstRequestPart { case process( Metadata, - UnsafeTransfer.AsyncIterator> + UnsafeTransfer, any Error>.AsyncIterator> ) case reject(RPCError) } diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index 9cab254af..5f43639f7 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -34,15 +34,15 @@ /// 1. Remove individual methods by calling ``removeHandler(forMethod:)``, or /// 2. Implement ``RegistrableRPCService/registerMethods(with:)`` to register only the methods you /// want to be served. -public struct RPCRouter: Sendable { +public struct RPCRouter: Sendable { @usableFromInline struct RPCHandler: Sendable { @usableFromInline let _fn: @Sendable ( _ stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >, _ context: ServerContext, _ interceptors: [any ServerInterceptor] @@ -73,8 +73,8 @@ public struct RPCRouter: Sendable { @inlinable func handle( stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >, context: ServerContext, interceptors: [any ServerInterceptor] @@ -171,8 +171,8 @@ public struct RPCRouter: Sendable { extension RPCRouter { internal func handle( stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >, context: ServerContext ) async { diff --git a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift index f2a9bee03..b7f3e241b 100644 --- a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift +++ b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift @@ -26,5 +26,5 @@ public protocol RegistrableRPCService: Sendable { /// Registers methods to server with the provided ``RPCRouter``. /// /// - Parameter router: The router to register methods with. - func registerMethods(with router: inout RPCRouter) + func registerMethods(with router: inout RPCRouter) } diff --git a/Sources/GRPCCore/Coding/Coding.swift b/Sources/GRPCCore/Coding/Coding.swift index 29569d3c0..97e236db3 100644 --- a/Sources/GRPCCore/Coding/Coding.swift +++ b/Sources/GRPCCore/Coding/Coding.swift @@ -30,7 +30,7 @@ public protocol MessageSerializer: Sendable { /// /// - Parameter message: The message to serialize. /// - Returns: The serialized bytes of a message. - func serialize(_ message: Message) throws -> [UInt8] + func serialize(_ message: Message) throws -> Bytes } /// Deserializes a sequence of bytes into a message. @@ -49,5 +49,5 @@ public protocol MessageDeserializer: Sendable { /// /// - Parameter serializedMessageBytes: The bytes to deserialize. /// - Returns: The deserialized message. - func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message + func deserialize(_ serializedMessageBytes: Bytes) throws -> Message } diff --git a/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift b/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift new file mode 100644 index 000000000..93adb45fb --- /dev/null +++ b/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift @@ -0,0 +1,58 @@ +/* + * Copyright 2025, 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. + */ + +/// A bag-of-bytes type. +/// +/// This protocol is used by the transport protocols (``ClientTransport`` and ``ServerTransport``) +/// with the serialization protocols (``MessageSerializer`` and ``MessageDeserializer``) so that +/// messages don't have to be copied to a fixed intermediate bag-of-bytes types. +public protocol GRPCContiguousBytes { + /// Initialize the bytes to a repeated value. + /// + /// - Parameters: + /// - byte: The value to be repeated. + /// - count: The number of times to repeat the byte value. + init(repeating byte: UInt8, count: Int) + + /// Initialize the bag of bytes from a sequence of bytes. + /// + /// - Parameters: + /// - sequence: a sequence of `UInt8` from which the bag of bytes should be constructed. + init(_ sequence: Bytes) where Bytes.Element == UInt8 + + /// The number of bytes in the bag of bytes. + var count: Int { get } + + /// Calls the given closure with the contents of underlying storage. + /// + /// - Note: Calling `withUnsafeBytes` multiple times does not guarantee that + /// the same buffer pointer will be passed in every time. + /// - Warning: The buffer argument to the body should not be stored or used + /// outside of the lifetime of the call to the closure. + func withUnsafeBytes(_ body: (_ buffer: UnsafeRawBufferPointer) throws -> R) rethrows -> R + + /// Calls the given closure with the contents of underlying storage. + /// + /// - Note: Calling `withUnsafeBytes` multiple times does not guarantee that + /// the same buffer pointer will be passed in every time. + /// - Warning: The buffer argument to the body should not be stored or used + /// outside of the lifetime of the call to the closure. + mutating func withUnsafeMutableBytes( + _ body: (_ buffer: UnsafeMutableRawBufferPointer) throws -> R + ) rethrows -> R +} + +extension [UInt8]: GRPCContiguousBytes {} diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 8e084b6c0..b1e4ed65e 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -54,9 +54,9 @@ private import Synchronization /// more abruptly you can cancel the task running your client. If your application requires /// additional resources that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). -public final class GRPCClient: Sendable { +public final class GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. - private let transport: any ClientTransport + private let transport: Transport /// The current state of the client. private let stateMachine: Mutex @@ -175,7 +175,7 @@ public final class GRPCClient: Sendable { /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. convenience public init( - transport: some ClientTransport, + transport: Transport, interceptors: [any ClientInterceptor] = [] ) { self.init( @@ -194,7 +194,7 @@ public final class GRPCClient: Sendable { /// The first interceptor added will be the first interceptor to intercept each request. /// The last interceptor added will be the final interceptor to intercept each request before calling the appropriate handler. public init( - transport: some ClientTransport, + transport: Transport, interceptorPipeline: [ConditionalInterceptor] ) { self.transport = transport @@ -398,11 +398,11 @@ public final class GRPCClient: Sendable { /// code is nonisolated. /// - handleClient: A closure which is called with the client. When the closure returns, the /// client is shutdown gracefully. -public func withGRPCClient( - transport: some ClientTransport, +public func withGRPCClient( + transport: Transport, interceptors: [any ClientInterceptor] = [], isolation: isolated (any Actor)? = #isolation, - handleClient: (GRPCClient) async throws -> Result + handleClient: (GRPCClient) async throws -> Result ) async throws -> Result { try await withGRPCClient( transport: transport, @@ -426,11 +426,11 @@ public func withGRPCClient( /// - handleClient: A closure which is called with the client. When the closure returns, the /// client is shutdown gracefully. /// - Returns: The result of the `handleClient` closure. -public func withGRPCClient( - transport: some ClientTransport, +public func withGRPCClient( + transport: Transport, interceptorPipeline: [ConditionalInterceptor], isolation: isolated (any Actor)? = #isolation, - handleClient: (GRPCClient) async throws -> Result + handleClient: (GRPCClient) async throws -> Result ) async throws -> Result { try await withThrowingDiscardingTaskGroup { group in let client = GRPCClient(transport: transport, interceptorPipeline: interceptorPipeline) diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 1b930b8d8..807e8d345 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -74,14 +74,14 @@ private import Synchronization /// you can cancel the task running your server. If your application requires additional resources /// that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). -public final class GRPCServer: Sendable { - typealias Stream = RPCStream +public final class GRPCServer: Sendable { + typealias Stream = RPCStream /// The ``ServerTransport`` implementation that the server uses to listen for new requests. - public let transport: any ServerTransport + public let transport: Transport /// The services registered which the server is serving. - private let router: RPCRouter + private let router: RPCRouter /// The state of the server. private let state: Mutex @@ -147,7 +147,7 @@ public final class GRPCServer: Sendable { /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. public convenience init( - transport: any ServerTransport, + transport: Transport, services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [] ) { @@ -169,11 +169,11 @@ public final class GRPCServer: Sendable { /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. public convenience init( - transport: any ServerTransport, + transport: Transport, services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor] ) { - var router = RPCRouter() + var router = RPCRouter() for service in services { service.registerMethods(with: &router) } @@ -187,7 +187,7 @@ public final class GRPCServer: Sendable { /// - Parameters: /// - transport: The transport the server should listen on. /// - router: A ``RPCRouter`` used by the server to route accepted streams to method handlers. - public init(transport: any ServerTransport, router: RPCRouter) { + public init(transport: Transport, router: RPCRouter) { self.state = Mutex(.notStarted) self.transport = transport self.router = router @@ -256,12 +256,12 @@ public final class GRPCServer: Sendable { /// - handleServer: A closure which is called with the server. When the closure returns, the /// server is shutdown gracefully. /// - Returns: The result of the `handleServer` closure. -public func withGRPCServer( - transport: any ServerTransport, +public func withGRPCServer( + transport: Transport, services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [], isolation: isolated (any Actor)? = #isolation, - handleServer: (GRPCServer) async throws -> Result + handleServer: (GRPCServer) async throws -> Result ) async throws -> Result { try await withGRPCServer( transport: transport, @@ -287,12 +287,12 @@ public func withGRPCServer( /// - handleServer: A closure which is called with the server. When the closure returns, the /// server is shutdown gracefully. /// - Returns: The result of the `handleServer` closure. -public func withGRPCServer( - transport: any ServerTransport, +public func withGRPCServer( + transport: Transport, services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor], isolation: isolated (any Actor)? = #isolation, - handleServer: (GRPCServer) async throws -> Result + handleServer: (GRPCServer) async throws -> Result ) async throws -> Result { return try await withThrowingDiscardingTaskGroup { group in let server = GRPCServer( diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift index 15dd4b6cd..84b2f70f3 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift @@ -16,18 +16,19 @@ @usableFromInline struct MessageToRPCResponsePartWriter< - Serializer: MessageSerializer + Serializer: MessageSerializer, + Bytes: GRPCContiguousBytes & Sendable >: RPCWriterProtocol where Serializer.Message: Sendable { @usableFromInline typealias Element = Serializer.Message @usableFromInline - let base: RPCWriter + let base: RPCWriter> @usableFromInline let serializer: Serializer @inlinable - init(serializer: Serializer, base: some RPCWriterProtocol) { + init(serializer: Serializer, base: some RPCWriterProtocol>) { self.serializer = serializer self.base = RPCWriter(wrapping: base) } @@ -39,7 +40,7 @@ struct MessageToRPCResponsePartWriter< @inlinable func write(contentsOf elements: some Sequence) async throws { - let requestParts = try elements.map { message -> RPCResponsePart in + let requestParts = try elements.map { message -> RPCResponsePart in .message(try self.serializer.serialize(message)) } @@ -49,8 +50,8 @@ struct MessageToRPCResponsePartWriter< extension RPCWriter { @inlinable - static func serializingToRPCResponsePart( - into writer: some RPCWriterProtocol, + static func serializingToRPCResponsePart( + into writer: some RPCWriterProtocol>, with serializer: some MessageSerializer ) -> Self { return RPCWriter(wrapping: MessageToRPCResponsePartWriter(serializer: serializer, base: writer)) diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift index d3d93d74d..1eff0c09a 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -16,7 +16,8 @@ @usableFromInline struct SerializingRPCWriter< - Base: RPCWriterProtocol<[UInt8]>, + Base: RPCWriterProtocol, + Bytes: GRPCContiguousBytes, Serializer: MessageSerializer >: RPCWriterProtocol where Serializer.Message: Sendable { @usableFromInline @@ -41,7 +42,7 @@ struct SerializingRPCWriter< @inlinable func write(contentsOf elements: some Sequence) async throws { let requestParts = try elements.map { message in - try self.serializer.serialize(message) + try self.serializer.serialize(message) as Bytes } try await self.base.write(contentsOf: requestParts) diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 52003ef6a..8c2e8a07e 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -25,9 +25,12 @@ /// gRPC provides an in-process transport in the `GRPCInProcessTransport` module and HTTP/2 /// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport /// package. -public protocol ClientTransport: Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable +public protocol ClientTransport: Sendable { + /// The bag-of-bytes type used by the transport. + associatedtype Bytes: GRPCContiguousBytes & Sendable + + typealias Inbound = RPCAsyncSequence, any Error> + typealias Outbound = RPCWriter>.Closable /// Returns a throttle which gRPC uses to determine whether retries can be executed. /// diff --git a/Sources/GRPCCore/Transport/RPCParts.swift b/Sources/GRPCCore/Transport/RPCParts.swift index cd72f7efb..ba19d58b4 100644 --- a/Sources/GRPCCore/Transport/RPCParts.swift +++ b/Sources/GRPCCore/Transport/RPCParts.swift @@ -15,7 +15,7 @@ */ /// Part of a request sent from a client to a server in a stream. -public enum RPCRequestPart: Hashable, Sendable { +public enum RPCRequestPart { /// Key-value pairs sent at the start of a request stream. Only one ``metadata(_:)`` value may /// be sent to the server. case metadata(Metadata) @@ -23,11 +23,15 @@ public enum RPCRequestPart: Hashable, Sendable { /// The bytes of a serialized message to send to the server. A stream may have any number of /// messages sent on it. Restrictions for unary request or response streams are imposed at a /// higher level. - case message([UInt8]) + case message(Bytes) } +extension RPCRequestPart: Sendable where Bytes: Sendable {} +extension RPCRequestPart: Hashable where Bytes: Hashable {} +extension RPCRequestPart: Equatable where Bytes: Equatable {} + /// Part of a response sent from a server to a client in a stream. -public enum RPCResponsePart: Hashable, Sendable { +public enum RPCResponsePart { /// Key-value pairs sent at the start of the response stream. At most one ``metadata(_:)`` value /// may be sent to the client. If the server sends ``metadata(_:)`` it must be the first part in /// the response stream. @@ -36,10 +40,14 @@ public enum RPCResponsePart: Hashable, Sendable { /// The bytes of a serialized message to send to the client. A stream may have any number of /// messages sent on it. Restrictions for unary request or response streams are imposed at a /// higher level. - case message([UInt8]) + case message(Bytes) /// A status and key-value pairs sent to the client at the end of the response stream. Every /// response stream must have exactly one ``status(_:_:)`` as the final part of the request /// stream. case status(Status, Metadata) } + +extension RPCResponsePart: Sendable where Bytes: Sendable {} +extension RPCResponsePart: Hashable where Bytes: Hashable {} +extension RPCResponsePart: Equatable where Bytes: Equatable {} diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index a7b926019..c3f2c7df7 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -22,9 +22,12 @@ /// gRPC provides an in-process transport in the `GRPCInProcessTransport` module and HTTP/2 /// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport /// package. -public protocol ServerTransport: Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable +public protocol ServerTransport: Sendable { + /// The bag-of-bytes type used by the transport. + associatedtype Bytes: GRPCContiguousBytes & Sendable + + typealias Inbound = RPCAsyncSequence, any Error> + typealias Outbound = RPCWriter>.Closable /// Starts the transport. /// diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift index aa28fbac2..070f08b67 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -36,6 +36,8 @@ extension InProcessTransport { /// /// - SeeAlso: `ClientTransport` public final class Client: ClientTransport { + public typealias Bytes = [UInt8] + private enum State: Sendable { struct UnconnectedState { var serverTransport: InProcessTransport.Server @@ -54,7 +56,8 @@ extension InProcessTransport { [Int: ( RPCStream, RPCStream< - RPCAsyncSequence, RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable > )] var signalEndContinuation: AsyncStream.Continuation @@ -75,7 +78,8 @@ extension InProcessTransport { [Int: ( RPCStream, RPCStream< - RPCAsyncSequence, RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable > )] var signalEndContinuation: AsyncStream.Continuation? @@ -96,9 +100,6 @@ extension InProcessTransport { case closed(ClosedState) } - public typealias Inbound = RPCAsyncSequence - public typealias Outbound = RPCWriter.Closable - public let retryThrottle: RetryThrottle? private let methodConfig: MethodConfigs @@ -236,8 +237,8 @@ extension InProcessTransport { options: CallOptions, _ closure: (RPCStream, ClientContext) async throws -> T ) async throws -> T { - let request = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) - let response = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let request = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) + let response = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let clientStream = RPCStream( descriptor: descriptor, diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index d8b385ce3..11ce7a792 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -29,8 +29,10 @@ extension InProcessTransport { /// /// - SeeAlso: `ClientTransport` public final class Server: ServerTransport, Sendable { - public typealias Inbound = RPCAsyncSequence - public typealias Outbound = RPCWriter.Closable + public typealias Bytes = [UInt8] + + public typealias Inbound = RPCAsyncSequence, any Error> + public typealias Outbound = RPCWriter>.Closable private let newStreams: AsyncStream> private let newStreamsContinuation: AsyncStream>.Continuation diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift index caa8c9bc5..6404a751a 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -489,14 +489,14 @@ extension StructuredSwiftTests { ) let expected = """ - \(access) struct FooClient: Foo_ClientProtocol { - private let client: GRPCCore.GRPCClient + \(access) struct FooClient: Foo_ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. /// /// - Parameters: /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - \(access) init(wrapping client: GRPCCore.GRPCClient) { + \(access) init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index a415307a6..ba44a6896 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -220,7 +220,7 @@ extension StructuredSwiftTests { } let expected = """ - \(access) func registerMethods(with router: inout GRPCCore.RPCRouter) { + \(access) func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( forMethod: FooService.Method.Bar.descriptor, deserializer: Deserialize(), diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 8c40eb7f4..7d1d7504d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -82,14 +82,14 @@ struct ClientCodeTranslatorSnippetBasedTests { /// > Source IDL Documentation: /// > /// > Documentation for ServiceA - public struct Client: ClientProtocol { - private let client: GRPCCore.GRPCClient + public struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. /// /// - Parameters: /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - public init(wrapping client: GRPCCore.GRPCClient) { + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 8f4509b50..e765f52fe 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -118,7 +118,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { - public func registerMethods(with router: inout GRPCCore.RPCRouter) {} + public func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport {} } // Default implementation of streaming methods from 'StreamingServiceProtocol'. diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 1b48ddadb..b0475ce50 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -144,7 +144,7 @@ final class ServerCodeTranslatorSnippetBasedTests { } // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { - public func registerMethods(with router: inout GRPCCore.RPCRouter) { + public func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index 4a6e17ed2..2b0f2b90f 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -23,16 +23,16 @@ extension ClientRPCExecutorTestHarness { private let handler: @Sendable ( _ stream: RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable > ) async throws -> Void init( _ handler: @escaping @Sendable ( RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable > ) async throws -> Void ) { @@ -43,9 +43,9 @@ extension ClientRPCExecutorTestHarness { stream: RPCStream ) async throws where - Inbound.Element == RPCRequestPart, + Inbound.Element == RPCRequestPart<[UInt8]>, Inbound.Failure == any Error, - Outbound.Element == RPCResponsePart + Outbound.Element == RPCResponsePart<[UInt8]> { let erased = RPCStream( descriptor: stream.descriptor, @@ -61,7 +61,7 @@ extension ClientRPCExecutorTestHarness { extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { return Self { stream in - let response = stream.inbound.map { part -> RPCResponsePart in + let response = stream.inbound.map { part -> RPCResponsePart<[UInt8]> in switch part { case .metadata(let metadata): return .metadata(metadata) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index d9beb0d6a..d7f976ba3 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -52,7 +52,8 @@ struct ServerRPCExecutorTestHarness { self.interceptors = interceptors } - func execute( + func execute( + bytes: Bytes.Type = Bytes.self, deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @escaping @Sendable ( @@ -60,10 +61,10 @@ struct ServerRPCExecutorTestHarness { ServerContext ) async throws -> StreamingServerResponse, producer: @escaping @Sendable ( - RPCWriter.Closable + RPCWriter>.Closable ) async throws -> Void, consumer: @escaping @Sendable ( - RPCAsyncSequence + RPCAsyncSequence, any Error> ) async throws -> Void ) async throws { try await self.execute( @@ -75,19 +76,19 @@ struct ServerRPCExecutorTestHarness { ) } - func execute( + func execute( deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: ServerHandler, producer: @escaping @Sendable ( - RPCWriter.Closable + RPCWriter>.Closable ) async throws -> Void, consumer: @escaping @Sendable ( - RPCAsyncSequence + RPCAsyncSequence, any Error> ) async throws -> Void ) async throws { - let input = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) - let output = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let input = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) + let output = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { @@ -131,10 +132,10 @@ struct ServerRPCExecutorTestHarness { func execute( handler: ServerHandler<[UInt8], [UInt8]> = .echo, producer: @escaping @Sendable ( - RPCWriter.Closable + RPCWriter>.Closable ) async throws -> Void, consumer: @escaping @Sendable ( - RPCAsyncSequence + RPCAsyncSequence, any Error> ) async throws -> Void ) async throws { try await self.execute( diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index b807d02cd..09982b20d 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -81,6 +81,7 @@ final class ServerRPCExecutorTests: XCTestCase { func testEchoSingleJSONMessage() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, deserializer: JSONDeserializer(), serializer: JSONSerializer() ) { request, _ in @@ -110,6 +111,7 @@ final class ServerRPCExecutorTests: XCTestCase { func testEchoMultipleJSONMessages() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, deserializer: JSONDeserializer(), serializer: JSONSerializer() ) { request, _ in @@ -142,6 +144,7 @@ final class ServerRPCExecutorTests: XCTestCase { func testReturnTrailingMetadata() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, deserializer: IdentityDeserializer(), serializer: IdentitySerializer() ) { request, _ in @@ -233,6 +236,7 @@ final class ServerRPCExecutorTests: XCTestCase { func testHandlerRespectsTimeout() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute( + bytes: [UInt8].self, deserializer: IdentityDeserializer(), serializer: IdentitySerializer() ) { request, context in @@ -260,6 +264,7 @@ final class ServerRPCExecutorTests: XCTestCase { // The interceptor skips the handler altogether. let harness = ServerRPCExecutorTestHarness(interceptors: [.rejectAll(with: error)]) try await harness.execute( + bytes: [UInt8].self, deserializer: IdentityDeserializer(), serializer: IdentitySerializer() ) { request, _ in diff --git a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift index 36d8e5580..7ae55b2da 100644 --- a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift @@ -19,7 +19,7 @@ import XCTest final class RPCRouterTests: XCTestCase { func testEmptyRouter() async throws { - var router = RPCRouter() + var router = RPCRouter() XCTAssertEqual(router.count, 0) XCTAssertEqual(router.methods, []) XCTAssertFalse( @@ -31,7 +31,7 @@ final class RPCRouterTests: XCTestCase { } func testRegisterMethod() async throws { - var router = RPCRouter() + var router = RPCRouter() let method = MethodDescriptor(fullyQualifiedService: "foo", method: "bar") router.registerHandler( forMethod: method, @@ -47,7 +47,7 @@ final class RPCRouterTests: XCTestCase { } func testRemoveMethod() async throws { - var router = RPCRouter() + var router = RPCRouter() let method = MethodDescriptor(fullyQualifiedService: "foo", method: "bar") router.registerHandler( forMethod: method, @@ -63,3 +63,18 @@ final class RPCRouterTests: XCTestCase { XCTAssertEqual(router.methods, []) } } + +struct NoServerTransport: ServerTransport { + typealias Bytes = [UInt8] + + func listen( + streamHandler: @escaping @Sendable ( + GRPCCore.RPCStream, + GRPCCore.ServerContext + ) async -> Void + ) async throws { + } + + func beginGracefulShutdown() { + } +} diff --git a/Tests/GRPCCoreTests/Coding/CodingTests.swift b/Tests/GRPCCoreTests/Coding/CodingTests.swift index efb57f94f..dab5ecece 100644 --- a/Tests/GRPCCoreTests/Coding/CodingTests.swift +++ b/Tests/GRPCCoreTests/Coding/CodingTests.swift @@ -35,7 +35,7 @@ final class CodingTests: XCTestCase { let serializer = JSONSerializer() let deserializer = JSONDeserializer() - let bytes = try serializer.serialize(message) + let bytes = try serializer.serialize(message) as [UInt8] let roundTrip = try deserializer.deserialize(bytes) XCTAssertEqual(roundTrip, message) } diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 878e4a365..477a43588 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -23,7 +23,10 @@ final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor] = [], - _ body: (GRPCClient, GRPCServer) async throws -> Void + _ body: ( + GRPCClient, + GRPCServer + ) async throws -> Void ) async throws { let inProcess = InProcessTransport() _ = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) @@ -43,22 +46,6 @@ final class GRPCClientTests: XCTestCase { } } - struct IdentitySerializer: MessageSerializer { - typealias Message = [UInt8] - - func serialize(_ message: [UInt8]) throws -> [UInt8] { - return message - } - } - - struct IdentityDeserializer: MessageDeserializer { - typealias Message = [UInt8] - - func deserialize(_ serializedMessageBytes: [UInt8]) throws -> [UInt8] { - return serializedMessageBytes - } - } - func testUnary() async throws { try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in try await client.unary( @@ -539,7 +526,10 @@ struct ClientTests { func withInProcessConnectedClient( services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor] = [], - _ body: (GRPCClient, GRPCServer) async throws -> Void + _ body: ( + GRPCClient, + GRPCServer + ) async throws -> Void ) async throws { let inProcess = InProcessTransport() let client = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 8e17ca174..acf29b8e5 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -23,7 +23,7 @@ final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor] = [], - _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void + _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() @@ -360,7 +360,7 @@ final class GRPCServerTests: XCTestCase { } } - private func doEchoGet(using transport: some ClientTransport) async throws { + private func doEchoGet(using transport: some ClientTransport<[UInt8]>) async throws { try await transport.withStream( descriptor: BinaryEcho.Methods.get, options: .defaults @@ -554,7 +554,7 @@ struct ServerTests { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor] = [], - _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void + _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() let server = GRPCServer( @@ -578,8 +578,8 @@ struct ServerTests { } } - func assertMetadata( - _ part: RPCResponsePart?, + func assertMetadata( + _ part: RPCResponsePart?, metadataHandler: (Metadata) -> Void = { _ in } ) { switch part { @@ -590,9 +590,9 @@ struct ServerTests { } } - func assertMessage( - _ part: RPCResponsePart?, - messageHandler: ([UInt8]) -> Void = { _ in } + func assertMessage( + _ part: RPCResponsePart?, + messageHandler: (Bytes) -> Void = { _ in } ) { switch part { case .some(.message(let message)): @@ -602,8 +602,8 @@ struct ServerTests { } } - func assertStatus( - _ part: RPCResponsePart?, + func assertStatus( + _ part: RPCResponsePart?, statusHandler: (Status, Metadata) -> Void = { _, _ in } ) { switch part { diff --git a/Tests/GRPCCoreTests/RPCPartsTests.swift b/Tests/GRPCCoreTests/RPCPartsTests.swift index e950a8e97..3bf72e85e 100644 --- a/Tests/GRPCCoreTests/RPCPartsTests.swift +++ b/Tests/GRPCCoreTests/RPCPartsTests.swift @@ -18,7 +18,7 @@ import XCTest final class RPCPartsTests: XCTestCase { func testPartsFitInExistentialContainer() { - XCTAssertLessThanOrEqual(MemoryLayout.size, 24) - XCTAssertLessThanOrEqual(MemoryLayout.size, 24) + XCTAssertLessThanOrEqual(MemoryLayout>.size, 24) + XCTAssertLessThanOrEqual(MemoryLayout>.size, 24) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift index 335426fad..35eca49c8 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift @@ -16,13 +16,15 @@ import GRPCCore struct IdentitySerializer: MessageSerializer { - func serialize(_ message: [UInt8]) throws -> [UInt8] { - return message + func serialize(_ message: [UInt8]) throws -> Bytes { + return Bytes(message) } } struct IdentityDeserializer: MessageDeserializer { - func deserialize(_ serializedMessageBytes: [UInt8]) throws -> [UInt8] { - return serializedMessageBytes + func deserialize(_ serializedMessageBytes: Bytes) throws -> [UInt8] { + return serializedMessageBytes.withUnsafeBytes { + Array($0) + } } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift index 3008cf3ef..3eb025783 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift @@ -20,10 +20,11 @@ import class Foundation.JSONDecoder import class Foundation.JSONEncoder struct JSONSerializer: MessageSerializer { - func serialize(_ message: Message) throws -> [UInt8] { + func serialize(_ message: Message) throws -> Bytes { do { let jsonEncoder = JSONEncoder() - return try Array(jsonEncoder.encode(message)) + let data = try jsonEncoder.encode(message) + return Bytes(data) } catch { throw RPCError(code: .internalError, message: "Can't serialize message to JSON. \(error)") } @@ -31,10 +32,11 @@ struct JSONSerializer: MessageSerializer { } struct JSONDeserializer: MessageDeserializer { - func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message { + func deserialize(_ serializedMessageBytes: Bytes) throws -> Message { do { let jsonDecoder = JSONDecoder() - return try jsonDecoder.decode(Message.self, from: Data(serializedMessageBytes)) + let data = serializedMessageBytes.withUnsafeBytes { Data($0) } + return try jsonDecoder.decode(Message.self, from: data) } catch { throw RPCError(code: .internalError, message: "Can't deserialze message from JSON. \(error)") } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index a0043eda3..4783d03e2 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -53,7 +53,7 @@ struct BinaryEcho: RegistrableRPCService { } } - func registerMethods(with router: inout RPCRouter) { + func registerMethods(with router: inout RPCRouter) { let serializer = IdentitySerializer() let deserializer = IdentityDeserializer() diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift index 253fbca1c..a543defc8 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift @@ -26,7 +26,7 @@ struct HelloWorld: RegistrableRPCService { return ServerResponse(message: Array("Hello, \(name)!".utf8), metadata: []) } - func registerMethods(with router: inout RPCRouter) { + func registerMethods(with router: inout RPCRouter) { let serializer = IdentitySerializer() let deserializer = IdentityDeserializer() diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index e63b5e5f1..d8f4fb053 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -16,8 +16,7 @@ @testable import GRPCCore struct AnyClientTransport: ClientTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let _retryThrottle: @Sendable () -> RetryThrottle? private let _withStream: @@ -30,8 +29,7 @@ struct AnyClientTransport: ClientTransport, Sendable { private let _close: @Sendable () -> Void private let _configuration: @Sendable (MethodDescriptor) -> MethodConfig? - init(wrapping transport: Transport) - where Transport.Inbound == Inbound, Transport.Outbound == Outbound { + init(wrapping transport: Transport) where Transport.Bytes == [UInt8] { self._retryThrottle = { transport.retryThrottle } self._withStream = { descriptor, options, closure in try await transport.withStream(descriptor: descriptor, options: options) { stream, context in @@ -81,8 +79,7 @@ struct AnyClientTransport: ClientTransport, Sendable { } struct AnyServerTransport: ServerTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let _listen: @Sendable ( @@ -93,7 +90,7 @@ struct AnyServerTransport: ServerTransport, Sendable { ) async throws -> Void private let _stopListening: @Sendable () -> Void - init(wrapping transport: Transport) { + init(wrapping transport: Transport) where Transport.Bytes == [UInt8] { self._listen = { streamHandler in try await transport.listen(streamHandler: streamHandler) } self._stopListening = { transport.beginGracefulShutdown() } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index abd052e7c..50fe696e3 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -17,8 +17,7 @@ @testable import GRPCCore struct StreamCountingClientTransport: ClientTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let transport: AnyClientTransport private let _streamsOpened: AtomicCounter @@ -32,8 +31,7 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { self._streamFailures.value } - init(wrapping transport: Transport) - where Transport.Inbound == Inbound, Transport.Outbound == Outbound { + init(wrapping transport: Transport) where Transport.Bytes == [UInt8] { self.transport = AnyClientTransport(wrapping: transport) self._streamsOpened = AtomicCounter() self._streamFailures = AtomicCounter() @@ -78,8 +76,7 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } struct StreamCountingServerTransport: ServerTransport, Sendable { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let transport: AnyServerTransport private let _acceptedStreams: AtomicCounter @@ -88,7 +85,7 @@ struct StreamCountingServerTransport: ServerTransport, Sendable { self._acceptedStreams.value } - init(wrapping transport: Transport) { + init(wrapping transport: Transport) where Transport.Bytes == [UInt8] { self.transport = AnyServerTransport(wrapping: transport) self._acceptedStreams = AtomicCounter() } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 7f7649eb1..e8ee489f7 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -16,8 +16,7 @@ @testable import GRPCCore struct ThrowOnStreamCreationTransport: ClientTransport { - typealias Inbound = RPCAsyncSequence - typealias Outbound = RPCWriter.Closable + typealias Bytes = [UInt8] private let code: RPCError.Code @@ -51,6 +50,8 @@ struct ThrowOnStreamCreationTransport: ClientTransport { } struct ThrowOnRunServerTransport: ServerTransport { + typealias Bytes = [UInt8] + func listen( streamHandler: ( _ stream: RPCStream, @@ -69,6 +70,8 @@ struct ThrowOnRunServerTransport: ServerTransport { } struct ThrowOnSignalServerTransport: ServerTransport { + typealias Bytes = [UInt8] + let signal: AsyncStream init(signal: AsyncStream) { diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index cc50b14b9..07acf664e 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -116,8 +116,8 @@ func XCTAssertRejected( } } -func XCTAssertMetadata( - _ part: RPCResponsePart?, +func XCTAssertMetadata( + _ part: RPCResponsePart?, metadataHandler: (Metadata) -> Void = { _ in } ) { switch part { @@ -128,8 +128,8 @@ func XCTAssertMetadata( } } -func XCTAssertMetadata( - _ part: RPCRequestPart?, +func XCTAssertMetadata( + _ part: RPCRequestPart?, metadataHandler: (Metadata) async throws -> Void = { _ in } ) async throws { switch part { @@ -140,9 +140,9 @@ func XCTAssertMetadata( } } -func XCTAssertMessage( - _ part: RPCResponsePart?, - messageHandler: ([UInt8]) -> Void = { _ in } +func XCTAssertMessage( + _ part: RPCResponsePart?, + messageHandler: (Bytes) -> Void = { _ in } ) { switch part { case .some(.message(let message)): @@ -152,9 +152,9 @@ func XCTAssertMessage( } } -func XCTAssertMessage( - _ part: RPCRequestPart?, - messageHandler: ([UInt8]) async throws -> Void = { _ in } +func XCTAssertMessage( + _ part: RPCRequestPart?, + messageHandler: (Bytes) async throws -> Void = { _ in } ) async throws { switch part { case .some(.message(let message)): @@ -164,8 +164,8 @@ func XCTAssertMessage( } } -func XCTAssertStatus( - _ part: RPCResponsePart?, +func XCTAssertStatus( + _ part: RPCResponsePart?, statusHandler: (Status, Metadata) -> Void = { _, _ in } ) { switch part { diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 487dfc953..92c3c5c8e 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -23,10 +23,10 @@ final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessTransport.Server(peer: "in-process:1234") - let outbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let outbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart<[UInt8]>.self) let stream = RPCStream< - RPCAsyncSequence, - RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >( descriptor: .testTest, inbound: RPCAsyncSequence( @@ -55,9 +55,10 @@ final class InProcessServerTransportTests: XCTestCase { func testStopListening() async throws { let transport = InProcessTransport.Server(peer: "in-process:1234") - let firstStreamOutbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let firstStreamOutbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart<[UInt8]>.self) let firstStream = RPCStream< - RPCAsyncSequence, RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >( descriptor: .testTest, inbound: RPCAsyncSequence( @@ -79,9 +80,12 @@ final class InProcessServerTransportTests: XCTestCase { transport.beginGracefulShutdown() - let secondStreamOutbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let secondStreamOutbound = GRPCAsyncThrowingStream.makeStream( + of: RPCResponsePart<[UInt8]>.self + ) let secondStream = RPCStream< - RPCAsyncSequence, RPCWriter.Closable + RPCAsyncSequence, any Error>, + RPCWriter>.Closable >( descriptor: .testTest, inbound: RPCAsyncSequence( diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 2a90aee5e..1de0a12bd 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -23,7 +23,10 @@ struct InProcessTransportTests { private static let cancellationModes = ["await-cancelled", "with-cancellation-handler"] private func withTestServerAndClient( - execute: (GRPCServer, GRPCClient) async throws -> Void + execute: ( + GRPCServer, + GRPCClient + ) async throws -> Void ) async throws { try await withThrowingDiscardingTaskGroup { group in let inProcess = InProcessTransport() @@ -126,7 +129,7 @@ private struct TestService: RegistrableRPCService { return ServerResponse(message: peerInfo) } - func registerMethods(with router: inout RPCRouter) { + func registerMethods(with router: inout RPCRouter) { router.registerHandler( forMethod: .testCancellation, deserializer: UTF8Deserializer(), @@ -169,38 +172,42 @@ private struct PeerInfo: Codable { } private struct PeerInfoSerializer: MessageSerializer { - func serialize(_ message: PeerInfo) throws -> [UInt8] { - Array("\(message.local) \(message.remote)".utf8) + func serialize(_ message: PeerInfo) throws -> Bytes { + Bytes("\(message.local) \(message.remote)".utf8) } } private struct PeerInfoDeserializer: MessageDeserializer { - func deserialize(_ serializedMessageBytes: [UInt8]) throws -> PeerInfo { - let stringPeerInfo = String(decoding: serializedMessageBytes, as: UTF8.self) + func deserialize(_ serializedMessageBytes: Bytes) throws -> PeerInfo { + let stringPeerInfo = serializedMessageBytes.withUnsafeBytes { + String(decoding: $0, as: UTF8.self) + } let peerInfoComponents = stringPeerInfo.split(separator: " ") return PeerInfo(local: String(peerInfoComponents[0]), remote: String(peerInfoComponents[1])) } } private struct UTF8Serializer: MessageSerializer { - func serialize(_ message: String) throws -> [UInt8] { - Array(message.utf8) + func serialize(_ message: String) throws -> Bytes { + Bytes(message.utf8) } } private struct UTF8Deserializer: MessageDeserializer { - func deserialize(_ serializedMessageBytes: [UInt8]) throws -> String { - String(decoding: serializedMessageBytes, as: UTF8.self) + func deserialize(_ serializedMessageBytes: Bytes) throws -> String { + serializedMessageBytes.withUnsafeBytes { + String(decoding: $0, as: UTF8.self) + } } } private struct VoidSerializer: MessageSerializer { - func serialize(_ message: Void) throws -> [UInt8] { - [] + func serialize(_ message: Void) throws -> Bytes { + Bytes(repeating: 0, count: 0) } } private struct VoidDeserializer: MessageDeserializer { - func deserialize(_ serializedMessageBytes: [UInt8]) throws { + func deserialize(_ serializedMessageBytes: Bytes) throws { } } From b143369c7ba74c666d4adb7dc37333e9ad26abc3 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 17 Jan 2025 14:04:52 +0000 Subject: [PATCH 534/580] Update examples (#2163) Motivation: We changed a few API across the packages so the examples need to be brought up-to-date. Modifications: - Update generated code - Update dependencies - Fix warnings Result: Examples are up-to-date --- Examples/echo/Package.swift | 6 +++--- .../echo/Sources/Generated/echo.grpc.swift | 8 ++++---- .../echo/Sources/Subcommands/Collect.swift | 14 +++---------- .../echo/Sources/Subcommands/Expand.swift | 14 +++---------- Examples/echo/Sources/Subcommands/Get.swift | 14 +++---------- .../echo/Sources/Subcommands/Update.swift | 14 +++---------- Examples/error-details/Package.swift | 4 ++-- .../Sources/DetailedErrorExample.swift | 2 +- .../Sources/Generated/helloworld.grpc.swift | 8 ++++---- Examples/hello-world/Package.swift | 6 +++--- .../Sources/Generated/helloworld.grpc.swift | 8 ++++---- .../Sources/Subcommands/Greet.swift | 20 +++++-------------- Examples/reflection-server/Package.swift | 8 ++++---- .../Sources/Generated/echo.grpc.swift | 8 ++++---- Examples/route-guide/Package.swift | 6 +++--- .../Sources/Generated/route_guide.grpc.swift | 8 ++++---- .../Sources/Subcommands/GetFeature.swift | 19 ++++++------------ .../Sources/Subcommands/ListFeatures.swift | 20 ++++++------------- .../Sources/Subcommands/RecordRoute.swift | 19 ++++++------------ .../Sources/Subcommands/RouteChat.swift | 19 ++++++------------ 20 files changed, 77 insertions(+), 148 deletions(-) diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index bdbf47403..224fff79c 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/echo/Sources/Generated/echo.grpc.swift b/Examples/echo/Sources/Generated/echo.grpc.swift index 26c075247..3a0e70694 100644 --- a/Examples/echo/Sources/Generated/echo.grpc.swift +++ b/Examples/echo/Sources/Generated/echo.grpc.swift @@ -347,7 +347,7 @@ extension Echo_Echo { // Default implementation of 'registerMethods(with:)'. extension Echo_Echo.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( forMethod: Echo_Echo.Method.Get.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), @@ -600,14 +600,14 @@ extension Echo_Echo { /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived /// means of communication with the remote peer. - internal struct Client: ClientProtocol { - private let client: GRPCCore.GRPCClient + internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. /// /// - Parameters: /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Examples/echo/Sources/Subcommands/Collect.swift b/Examples/echo/Sources/Subcommands/Collect.swift index 281ae9d35..27350774a 100644 --- a/Examples/echo/Sources/Subcommands/Collect.swift +++ b/Examples/echo/Sources/Subcommands/Collect.swift @@ -27,18 +27,12 @@ struct Collect: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let client = GRPCClient( - transport: try .http2NIOPosix( + try await withGRPCClient( + transport: .http2NIOPosix( target: self.arguments.target, transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + ) { client in let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { @@ -50,8 +44,6 @@ struct Collect: AsyncParsableCommand { } print("collect ← \(message.text)") } - - client.beginGracefulShutdown() } } } diff --git a/Examples/echo/Sources/Subcommands/Expand.swift b/Examples/echo/Sources/Subcommands/Expand.swift index c30b481ef..b488c9977 100644 --- a/Examples/echo/Sources/Subcommands/Expand.swift +++ b/Examples/echo/Sources/Subcommands/Expand.swift @@ -27,18 +27,12 @@ struct Expand: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let client = GRPCClient( - transport: try .http2NIOPosix( + try await withGRPCClient( + transport: .http2NIOPosix( target: self.arguments.target, transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + ) { client in let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { @@ -50,8 +44,6 @@ struct Expand: AsyncParsableCommand { } } } - - client.beginGracefulShutdown() } } } diff --git a/Examples/echo/Sources/Subcommands/Get.swift b/Examples/echo/Sources/Subcommands/Get.swift index 9a1953628..4c429910c 100644 --- a/Examples/echo/Sources/Subcommands/Get.swift +++ b/Examples/echo/Sources/Subcommands/Get.swift @@ -25,18 +25,12 @@ struct Get: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let client = GRPCClient( - transport: try .http2NIOPosix( + try await withGRPCClient( + transport: .http2NIOPosix( target: self.arguments.target, transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + ) { client in let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { @@ -45,8 +39,6 @@ struct Get: AsyncParsableCommand { let response = try await echo.get(message) print("get ← \(response.text)") } - - client.beginGracefulShutdown() } } } diff --git a/Examples/echo/Sources/Subcommands/Update.swift b/Examples/echo/Sources/Subcommands/Update.swift index 8e8cb664f..726e8b83d 100644 --- a/Examples/echo/Sources/Subcommands/Update.swift +++ b/Examples/echo/Sources/Subcommands/Update.swift @@ -27,18 +27,12 @@ struct Update: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let client = GRPCClient( - transport: try .http2NIOPosix( + try await withGRPCClient( + transport: .http2NIOPosix( target: self.arguments.target, transportSecurity: .plaintext ) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + ) { client in let echo = Echo_Echo.Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { @@ -53,8 +47,6 @@ struct Update: AsyncParsableCommand { } } } - - client.beginGracefulShutdown() } } } diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift index ffd3f85a0..2598c8d80 100644 --- a/Examples/error-details/Package.swift +++ b/Examples/error-details/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "error-details", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), ], targets: [ .executableTarget( diff --git a/Examples/error-details/Sources/DetailedErrorExample.swift b/Examples/error-details/Sources/DetailedErrorExample.swift index ddd669877..f4bfa464c 100644 --- a/Examples/error-details/Sources/DetailedErrorExample.swift +++ b/Examples/error-details/Sources/DetailedErrorExample.swift @@ -29,7 +29,7 @@ struct DetailedErrorExample { } } - static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { + static func doRPC(_ greeter: Helloworld_Greeter.Client) async throws { do { let reply = try await greeter.sayHello(.with { $0.name = "(ignored)" }) print("Unexpected reply: \(reply.message)") diff --git a/Examples/error-details/Sources/Generated/helloworld.grpc.swift b/Examples/error-details/Sources/Generated/helloworld.grpc.swift index 737d5fa0f..329c0e816 100644 --- a/Examples/error-details/Sources/Generated/helloworld.grpc.swift +++ b/Examples/error-details/Sources/Generated/helloworld.grpc.swift @@ -156,7 +156,7 @@ extension Helloworld_Greeter { // Default implementation of 'registerMethods(with:)'. extension Helloworld_Greeter.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( forMethod: Helloworld_Greeter.Method.SayHello.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), @@ -246,14 +246,14 @@ extension Helloworld_Greeter { /// > Source IDL Documentation: /// > /// > The greeting service definition. - internal struct Client: ClientProtocol { - private let client: GRPCCore.GRPCClient + internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. /// /// - Parameters: /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index c73f1433d..f4f32f991 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift index 737d5fa0f..329c0e816 100644 --- a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift +++ b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift @@ -156,7 +156,7 @@ extension Helloworld_Greeter { // Default implementation of 'registerMethods(with:)'. extension Helloworld_Greeter.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( forMethod: Helloworld_Greeter.Method.SayHello.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), @@ -246,14 +246,14 @@ extension Helloworld_Greeter { /// > Source IDL Documentation: /// > /// > The greeting service definition. - internal struct Client: ClientProtocol { - private let client: GRPCCore.GRPCClient + internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. /// /// - Parameters: /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Examples/hello-world/Sources/Subcommands/Greet.swift b/Examples/hello-world/Sources/Subcommands/Greet.swift index cf5d73a8d..643c90798 100644 --- a/Examples/hello-world/Sources/Subcommands/Greet.swift +++ b/Examples/hello-world/Sources/Subcommands/Greet.swift @@ -29,22 +29,12 @@ struct Greet: AsyncParsableCommand { var name: String = "" func run() async throws { - try await withThrowingDiscardingTaskGroup { group in - let client = GRPCClient( - transport: try .http2NIOPosix( - target: .ipv4(host: "127.0.0.1", port: self.port), - transportSecurity: .plaintext - ) + try await withGRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: self.port), + transportSecurity: .plaintext ) - - group.addTask { - try await client.run() - } - - defer { - client.beginGracefulShutdown() - } - + ) { client in let greeter = Helloworld_Greeter.Client(wrapping: client) let reply = try await greeter.sayHello(.with { $0.name = self.name }) print(reply.message) diff --git a/Examples/reflection-server/Package.swift b/Examples/reflection-server/Package.swift index 8a8041fcf..37a3942b2 100644 --- a/Examples/reflection-server/Package.swift +++ b/Examples/reflection-server/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "reflection-server", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-extras.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-extras.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/reflection-server/Sources/Generated/echo.grpc.swift b/Examples/reflection-server/Sources/Generated/echo.grpc.swift index 26c075247..3a0e70694 100644 --- a/Examples/reflection-server/Sources/Generated/echo.grpc.swift +++ b/Examples/reflection-server/Sources/Generated/echo.grpc.swift @@ -347,7 +347,7 @@ extension Echo_Echo { // Default implementation of 'registerMethods(with:)'. extension Echo_Echo.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( forMethod: Echo_Echo.Method.Get.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), @@ -600,14 +600,14 @@ extension Echo_Echo { /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived /// means of communication with the remote peer. - internal struct Client: ClientProtocol { - private let client: GRPCCore.GRPCClient + internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. /// /// - Parameters: /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index d9ee1b592..bf26e8ffe 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift index 05b5e3c48..87ef9ba8b 100644 --- a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift +++ b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift @@ -407,7 +407,7 @@ extension Routeguide_RouteGuide { // Default implementation of 'registerMethods(with:)'. extension Routeguide_RouteGuide.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( forMethod: Routeguide_RouteGuide.Method.GetFeature.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), @@ -684,14 +684,14 @@ extension Routeguide_RouteGuide { /// > Source IDL Documentation: /// > /// > Interface exported by the server. - internal struct Client: ClientProtocol { - private let client: GRPCCore.GRPCClient + internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { + private let client: GRPCCore.GRPCClient /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. /// /// - Parameters: /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Examples/route-guide/Sources/Subcommands/GetFeature.swift b/Examples/route-guide/Sources/Subcommands/GetFeature.swift index eb58eeff3..6ea6a12bd 100644 --- a/Examples/route-guide/Sources/Subcommands/GetFeature.swift +++ b/Examples/route-guide/Sources/Subcommands/GetFeature.swift @@ -37,17 +37,12 @@ struct GetFeature: AsyncParsableCommand { var longitude: Int32 = -746_143_763 func run() async throws { - let transport = try HTTP2ClientTransport.Posix( - target: .ipv4(host: "127.0.0.1", port: self.port), - transportSecurity: .plaintext - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + try await withGRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: self.port), + transportSecurity: .plaintext + ) + ) { client in let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) let point = Routeguide_Point.with { @@ -62,8 +57,6 @@ struct GetFeature: AsyncParsableCommand { } else { print("Found '\(feature.name)' at (\(self.latitude), \(self.longitude))") } - - client.beginGracefulShutdown() } } } diff --git a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift index 32db2d0c1..be6d754dc 100644 --- a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift +++ b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift @@ -51,17 +51,12 @@ struct ListFeatures: AsyncParsableCommand { var maxLongitude: Int32 = -730_000_000 func run() async throws { - let transport = try HTTP2ClientTransport.Posix( - target: .ipv4(host: "127.0.0.1", port: self.port), - transportSecurity: .plaintext - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + try await withGRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: self.port), + transportSecurity: .plaintext + ) + ) { client in let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) let boundingRectangle = Routeguide_Rectangle.with { $0.lo.latitude = self.minLatitude @@ -78,9 +73,6 @@ struct ListFeatures: AsyncParsableCommand { print("(\(count)) \(feature.name) at (\(lat), \(lon))") } } - - client.beginGracefulShutdown() } - } } diff --git a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift index bc3f4f9dc..7a652206c 100644 --- a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift +++ b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift @@ -30,17 +30,12 @@ struct RecordRoute: AsyncParsableCommand { var points: Int = 10 func run() async throws { - let transport = try HTTP2ClientTransport.Posix( - target: .ipv4(host: "127.0.0.1", port: self.port), - transportSecurity: .plaintext - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + try await withGRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: self.port), + transportSecurity: .plaintext + ) + ) { client in let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) // Get all features. @@ -67,8 +62,6 @@ struct RecordRoute: AsyncParsableCommand { a distance \(summary.distance) metres. """ print(text) - - client.beginGracefulShutdown() } } } diff --git a/Examples/route-guide/Sources/Subcommands/RouteChat.swift b/Examples/route-guide/Sources/Subcommands/RouteChat.swift index 2a4c23b16..ba6d92424 100644 --- a/Examples/route-guide/Sources/Subcommands/RouteChat.swift +++ b/Examples/route-guide/Sources/Subcommands/RouteChat.swift @@ -30,17 +30,12 @@ struct RouteChat: AsyncParsableCommand { var port: Int = 31415 func run() async throws { - let transport = try HTTP2ClientTransport.Posix( - target: .ipv4(host: "127.0.0.1", port: self.port), - transportSecurity: .plaintext - ) - let client = GRPCClient(transport: transport) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - + try await withGRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: self.port), + transportSecurity: .plaintext + ) + ) { client in let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) try await routeGuide.routeChat { writer in @@ -67,8 +62,6 @@ struct RouteChat: AsyncParsableCommand { print("Received note: '\(note.message) at (\(lat), \(lon))'") } } - - client.beginGracefulShutdown() } } } From 3e75bbb6a08df369c72d0af43eaa7d680197e230 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 17 Jan 2025 15:25:34 +0000 Subject: [PATCH 535/580] Update example dependencies to point to the new beta tag (#2164) --- Examples/echo/Package.swift | 6 +++--- Examples/error-details/Package.swift | 4 ++-- Examples/hello-world/Package.swift | 6 +++--- Examples/reflection-server/Package.swift | 8 ++++---- Examples/route-guide/Package.swift | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index 224fff79c..ca473e981 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift index 2598c8d80..c7faca691 100644 --- a/Examples/error-details/Package.swift +++ b/Examples/error-details/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "error-details", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), ], targets: [ .executableTarget( diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index f4f32f991..18eb5c1f6 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/reflection-server/Package.swift b/Examples/reflection-server/Package.swift index 37a3942b2..03aed7629 100644 --- a/Examples/reflection-server/Package.swift +++ b/Examples/reflection-server/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "reflection-server", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-extras.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-extras.git", exact: "1.0.0-beta.3"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index bf26e8ffe..86df4fa62 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ From ebcac536a47d9b14c92d1c89808328f0aea9e689 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 17 Jan 2025 15:34:56 +0000 Subject: [PATCH 536/580] Update tutorials for beta 3 (#2165) Motivation: We're going to tag beta 3 soon, so ensure the tutorials are up to date. Modifications: - Update tutorials Result: More up-to-date docs --------- Co-authored-by: Gus Cairo --- README.md | 6 +++--- .../Tutorials/Hello-World/Hello-World.tutorial | 4 ++-- .../route-guide-sec01-step07-description.swift | 6 +++--- .../route-guide-sec01-step08-description.swift | 6 +++--- .../route-guide-sec05-step00-package.swift | 4 ++-- .../route-guide-sec05-step01-package.swift | 6 +++--- .../route-guide-sec05-step02-package.swift | 6 +++--- ...oute-guide-sec06-step03-create-client.swift | 6 ++++-- .../route-guide-sec06-step04-run-client.swift | 15 --------------- .../route-guide-sec06-step05-stub.swift | 10 ++++------ .../route-guide-sec06-step06-get-feature.swift | 12 +++++------- ...oute-guide-sec06-step07-list-features.swift | 14 ++++++-------- ...route-guide-sec06-step08-record-route.swift | 16 +++++++--------- .../route-guide-sec06-step09-route-chat.swift | 18 ++++++++---------- .../Tutorials/Route-Guide/Route-Guide.tutorial | 17 ++++++----------- 15 files changed, 59 insertions(+), 87 deletions(-) delete mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift diff --git a/README.md b/README.md index 82e23d403..d641ed824 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ let package = Package( name: "foo-package", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 31273ed0b..4818c18ae 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -1,6 +1,6 @@ @Tutorial(time: 10) { @XcodeRequirement( - title: "Xcode 16 Beta 5+", + title: "Xcode 16", destination: "https://developer.apple.com/download/" ) @@ -18,7 +18,7 @@ repository by running the following command in a terminal: ```console - git clone --branch 2.0.0-beta.2 https://github.com/grpc/grpc-swift + git clone --branch 2.0.0-beta.3 https://github.com/grpc/grpc-swift ``` You then need to change directory to the `Examples/hello-world` directory of the cloned diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift index 662795346..2f1fb0bd3 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), ], targets: [] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift index 55dab65b4..dcb3ced86 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index 7deea7e81..6aaa5f7a7 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -5,8 +5,8 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index 67cf80691..a450a82e4 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index 8462eada5..b250e8d82 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.2"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.2"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift index bf1dbaebc..55f110792 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift @@ -3,11 +3,13 @@ import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { - let client = try GRPCClient( + try await withGRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), transportSecurity: .plaintext ) - ) + ) { client in + // ... + } } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift deleted file mode 100644 index 52c4c79e5..000000000 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift +++ /dev/null @@ -1,15 +0,0 @@ -import GRPCCore -import GRPCNIOTransportHTTP2 - -extension RouteGuide { - func runClient() async throws { - let client = try GRPCClient( - transport: .http2NIOPosix( - target: .ipv4(host: "127.0.0.1", port: 31415), - transportSecurity: .plaintext - ) - ) - - async let _ = client.run() - } -} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift index 9b8b5ee3b..94bca0e04 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift @@ -3,15 +3,13 @@ import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { - let client = try GRPCClient( + try await withGRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) + ) { client in + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) + } } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift index b2b103b20..e985f8189 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift @@ -3,17 +3,15 @@ import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { - let client = try GRPCClient( + try await withGRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) - try await self.getFeature(using: routeGuide) + ) { client in + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) + try await self.getFeature(using: routeGuide) + } } private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift index 609ce0ead..ba990eede 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift @@ -3,18 +3,16 @@ import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { - let client = try GRPCClient( + try await withGRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) - try await self.getFeature(using: routeGuide) - try await self.listFeatures(using: routeGuide) + ) { client in + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) + try await self.getFeature(using: routeGuide) + try await self.listFeatures(using: routeGuide) + } } private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift index 9fb6490c8..8bf4f71ee 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift @@ -3,19 +3,17 @@ import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { - let client = try GRPCClient( + try await withGRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) - try await self.getFeature(using: routeGuide) - try await self.listFeatures(using: routeGuide) - try await self.recordRoute(using: routeGuide) + ) { client in + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) + try await self.getFeature(using: routeGuide) + try await self.listFeatures(using: routeGuide) + try await self.recordRoute(using: routeGuide) + } } private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift index 9f1afac60..81d7cfac9 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift @@ -3,20 +3,18 @@ import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { - let client = try GRPCClient( + try await withGRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), transportSecurity: .plaintext ) - ) - - async let _ = client.run() - - let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) - try await self.getFeature(using: routeGuide) - try await self.listFeatures(using: routeGuide) - try await self.recordRoute(using: routeGuide) - try await self.routeChat(using: routeGuide) + ) { client in + let routeGuide = Routeguide_RouteGuide.Client(wrapping: client) + try await self.getFeature(using: routeGuide) + try await self.listFeatures(using: routeGuide) + try await self.recordRoute(using: routeGuide) + try await self.routeChat(using: routeGuide) + } } private func getFeature(using routeGuide: Routeguide_RouteGuide.Client) async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial index 3491b521a..d5746c1b2 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -1,6 +1,6 @@ @Tutorial(time: 30) { @XcodeRequirement( - title: "Xcode 16 Beta 6+", + title: "Xcode 16", destination: "https://developer.apple.com/download/" ) @@ -401,20 +401,15 @@ } @Step { - First we need to create a gRPC client for our stub, we're not using TLS so we - use the `.plaintext` security transport and specify the server address and port - we want to connect to. + We need to create a gRPC client for our stub. gRPC provides a with-style helper to + manage the lifecycle of the client and its transport so let's use that. We specify the + transport to use and configure it appropriately. We're not using TLS so we use + the `.plaintext` security transport and specify the server address and port we want to + connect to. @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step03-create-client.swift") } - @Step { - To make RPCs using the client it needs to be running. Running the client will resolve the - target address, start a load balancer and then maintain connections to the server. - - @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step04-run-client.swift") - } - @Step { We can now instantiate the stub with our `client` and call service methods. From a1dbd1506f9975d7faa79f0d857b5f958e904cb6 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 17 Jan 2025 16:25:28 +0000 Subject: [PATCH 537/580] Use JSON (de)serializers instead of custom ones for tests (#2162) In some tests, we were using a custom serializer/deserializer to deal with the `PeerInfo` type. We can just reuse the JSON (de)serialiser we have. Had to copy it over as it was in a different target. --- .../Test Utilities/Coding+JSON.swift | 1 + .../InProcessTransportTests.swift | 20 +-------- .../Test Utilities/JSONSerializing.swift | 45 +++++++++++++++++++ 3 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift index 3eb025783..d2c6ef452 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import GRPCCore import struct Foundation.Data diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 1de0a12bd..5751d74a1 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -80,7 +80,7 @@ struct InProcessTransportTests { request: ClientRequest(message: ()), descriptor: .peerInfo, serializer: VoidSerializer(), - deserializer: PeerInfoDeserializer(), + deserializer: JSONDeserializer(), options: .defaults ) { try $0.message @@ -142,7 +142,7 @@ private struct TestService: RegistrableRPCService { router.registerHandler( forMethod: .peerInfo, deserializer: VoidDeserializer(), - serializer: PeerInfoSerializer(), + serializer: JSONSerializer(), handler: { let response = try await self.peerInfo( request: ServerRequest(stream: $0), @@ -171,22 +171,6 @@ private struct PeerInfo: Codable { var remote: String } -private struct PeerInfoSerializer: MessageSerializer { - func serialize(_ message: PeerInfo) throws -> Bytes { - Bytes("\(message.local) \(message.remote)".utf8) - } -} - -private struct PeerInfoDeserializer: MessageDeserializer { - func deserialize(_ serializedMessageBytes: Bytes) throws -> PeerInfo { - let stringPeerInfo = serializedMessageBytes.withUnsafeBytes { - String(decoding: $0, as: UTF8.self) - } - let peerInfoComponents = stringPeerInfo.split(separator: " ") - return PeerInfo(local: String(peerInfoComponents[0]), remote: String(peerInfoComponents[1])) - } -} - private struct UTF8Serializer: MessageSerializer { func serialize(_ message: String) throws -> Bytes { Bytes(message.utf8) diff --git a/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift b/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift new file mode 100644 index 000000000..905e90525 --- /dev/null +++ b/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift @@ -0,0 +1,45 @@ +/* + * Copyright 2025, 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 struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder + +struct JSONSerializer: MessageSerializer { + func serialize(_ message: Message) throws -> Bytes { + do { + let jsonEncoder = JSONEncoder() + let data = try jsonEncoder.encode(message) + return Bytes(data) + } catch { + throw RPCError(code: .internalError, message: "Can't serialize message to JSON. \(error)") + } + } +} + +struct JSONDeserializer: MessageDeserializer { + func deserialize(_ serializedMessageBytes: Bytes) throws -> Message { + do { + let jsonDecoder = JSONDecoder() + let data = serializedMessageBytes.withUnsafeBytes { Data($0) } + return try jsonDecoder.decode(Message.self, from: data) + } catch { + throw RPCError(code: .internalError, message: "Can't deserialze message from JSON. \(error)") + } + } +} From e4da2bbb0d50bd741d8c1d7ca9a99aae672f08df Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 17 Jan 2025 17:28:24 +0000 Subject: [PATCH 538/580] Remove deprecated API (#2166) --- .../GRPCCore/Call/Server/ServerContext.swift | 17 ----------------- .../Documentation.docc/Development/Design.md | 4 ++-- Sources/GRPCCore/GRPCClient.swift | 7 +------ Sources/GRPCCore/MethodDescriptor.swift | 11 ----------- 4 files changed, 3 insertions(+), 36 deletions(-) diff --git a/Sources/GRPCCore/Call/Server/ServerContext.swift b/Sources/GRPCCore/Call/Server/ServerContext.swift index 812a22886..d2518adf9 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -19,23 +19,6 @@ public struct ServerContext: Sendable { /// A description of the method being called. public var descriptor: MethodDescriptor - /// A description of the remote peer. - /// - /// The format of the description should follow the pattern ":
" where - /// "" indicates the underlying network transport (such as "ipv4", "unix", or - /// "in-process"). This is a guideline for how descriptions should be formatted; different - /// implementations may not follow this format so you shouldn't make assumptions based on it. - /// - /// Some examples include: - /// - "ipv4:127.0.0.1:31415", - /// - "ipv6:[::1]:443", - /// - "in-process:27182". - @available(*, deprecated, renamed: "remotePeer") - public var peer: String { - get { remotePeer } - set { remotePeer = newValue } - } - /// A description of the remote peer. /// /// The format of the description should follow the pattern ":
" where diff --git a/Sources/GRPCCore/Documentation.docc/Development/Design.md b/Sources/GRPCCore/Documentation.docc/Development/Design.md index cecccbafa..8175f036f 100644 --- a/Sources/GRPCCore/Documentation.docc/Development/Design.md +++ b/Sources/GRPCCore/Documentation.docc/Development/Design.md @@ -173,12 +173,12 @@ concurrency. Most users won't use ``GRPCClient`` to execute RPCs directly, instead they will use the generated client stubs which wrap the ``GRPCClient``. Users are responsible for creating the client and running it (which starts and runs the -underlying transport). This is done by calling ``GRPCClient/run()``. The client +underlying transport). This is done by calling ``GRPCClient/runConnections()``. The client can be shutdown gracefully by calling ``GRPCClient/beginGracefulShutdown()`` which will stop new RPCs from starting (by failing them with ``RPCError/Code-swift.struct/unavailable``) but allow existing ones to continue. Existing work can be stopped more abruptly by cancelling the task where -``GRPCClient/run()`` is executing. +``GRPCClient/runConnections()`` is executing. #### Server diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index b1e4ed65e..b3093f8b1 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -227,16 +227,11 @@ public final class GRPCClient: Sendable { } } - @available(*, deprecated, renamed: "runConnections", message: "It'll be removed before v2.") - public func run() async throws { - try await self.runConnections() - } - /// Close the client. /// /// The transport will be closed: this means that it will be given enough time to wait for /// in-flight RPCs to finish executing, but no new RPCs will be accepted. You can cancel the task - /// executing ``run()`` if you want to abruptly stop in-flight RPCs. + /// executing ``runConnections()`` if you want to abruptly stop in-flight RPCs. public func beginGracefulShutdown() { let wasRunning = self.stateMachine.withLock { $0.state.beginGracefulShutdown() } if wasRunning { diff --git a/Sources/GRPCCore/MethodDescriptor.swift b/Sources/GRPCCore/MethodDescriptor.swift index 3cfc500c1..20a1f3f50 100644 --- a/Sources/GRPCCore/MethodDescriptor.swift +++ b/Sources/GRPCCore/MethodDescriptor.swift @@ -51,17 +51,6 @@ public struct MethodDescriptor: Sendable, Hashable { self.service = ServiceDescriptor(fullyQualifiedService: fullyQualifiedService) self.method = method } - - @available(*, deprecated, renamed: "init(fullyQualifiedService:method:)") - /// Creates a new method descriptor. - /// - /// - Parameters: - /// - service: The fully qualified name of the service, including the package - /// name. For example, "helloworld.Greeter". - /// - method: The name of the method. For example, "SayHello". - public init(service: String, method: String) { - self.init(fullyQualifiedService: service, method: method) - } } extension MethodDescriptor: CustomStringConvertible { From 32d17fb9f5e171e9038b687f516439a2e6650ac1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 21 Jan 2025 08:10:38 +0000 Subject: [PATCH 539/580] Fix a few docs (#2168) --- Sources/GRPCCore/GRPCServer.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 807e8d345..832e3cdec 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -34,8 +34,8 @@ private import Synchronization /// The following example demonstrates how to create and run a server. /// /// ```swift -/// // Create an transport -/// let transport: any ServerTransport = ... +/// // Create a transport +/// let transport = SomeServerTransport() /// /// // Create the 'Greeter' and 'Echo' services. /// let greeter = GreeterService() @@ -73,7 +73,8 @@ private import Synchronization /// This allows the server to drain existing requests gracefully. To stop the server more abruptly /// you can cancel the task running your server. If your application requires additional resources /// that need their lifecycles managed you should consider using [Swift Service -/// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). +/// Lifecycle](https://github.com/swift-server/swift-service-lifecycle) and the +/// `GRPCServiceLifecycle` module provided by [gRPC Swift Extras](https://github.com/grpc/grpc-swift-extras). public final class GRPCServer: Sendable { typealias Stream = RPCStream @@ -136,7 +137,7 @@ public final class GRPCServer: Sendable { } } - /// Creates a new server with no resources. + /// Creates a new server. /// /// - Parameters: /// - transport: The transport the server should listen on. @@ -158,7 +159,7 @@ public final class GRPCServer: Sendable { ) } - /// Creates a new server with no resources. + /// Creates a new server. /// /// - Parameters: /// - transport: The transport the server should listen on. @@ -182,7 +183,7 @@ public final class GRPCServer: Sendable { self.init(transport: transport, router: router) } - /// Creates a new server with no resources. + /// Creates a new server with a pre-configured router. /// /// - Parameters: /// - transport: The transport the server should listen on. From 0db5cc0957bca76d7c434cb6fe3c45e990c7423a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 22 Jan 2025 10:56:53 +0000 Subject: [PATCH 540/580] Update thresholds for nightly-6.1 (#2172) Motivation: Nightly 6.1 CI has been enabled and can't find appropriate benchmark thresholds. Modifications: - Rename nightly-6.0 thresholds to nightly-6.1 Result: CI passes --- .../GRPCSwiftBenchmark.Metadata_Add_binary.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Add_string.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json | 0 ..._Iterate_binary_values_when_only_binary_values_stored.p90.json | 0 ...tadata_Iterate_binary_values_when_only_strings_stored.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.0 => nightly-6.1}/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.0 => nightly-6.1}/GRPCSwiftBenchmark.Metadata_Add_string.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.0 => nightly-6.1}/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.0 => nightly-6.1}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.0 => nightly-6.1}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.0 => nightly-6.1}/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.0 => nightly-6.1}/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json (100%) diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json From 067938bed65d25fe4af6ffd486581e97ed402bca Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 22 Jan 2025 15:40:29 +0000 Subject: [PATCH 541/580] Allow a Status to be created from an HTTP status code (#2170) Motivation: gRPC has a well defined mapping of HTTP status codes to gRPC status codes for when a response doesn't explicitly include a gRPC status. Modifications: - Add an init to status to allow for a status to be created from an HTTP status code. Result: Can create a status from an HTTP status code --- Sources/GRPCCore/Status.swift | 29 +++++++++++++++++++++++++++ Tests/GRPCCoreTests/StatusTests.swift | 19 ++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift index 7a78c12e1..eba8d54f0 100644 --- a/Sources/GRPCCore/Status.swift +++ b/Sources/GRPCCore/Status.swift @@ -296,3 +296,32 @@ extension Status.Code { /// operation. public static let unauthenticated = Self(code: .unauthenticated) } + +extension Status { + /// Create a status from an HTTP status code for a response which didn't include a gRPC status. + /// + /// - Parameter httpStatusCode: The HTTP status code to map to a status. + public init(httpStatusCode: Int) { + // See the "http-grpc-status-mapping.md" doc in grpc/grpc GitHub repo. + switch httpStatusCode { + case 400: + self = Status(code: .internalError, message: "HTTP 400: Bad Request") + case 401: + self = Status(code: .unauthenticated, message: "HTTP 401: Unauthorized") + case 403: + self = Status(code: .permissionDenied, message: "HTTP 403: Forbidden") + case 404: + self = Status(code: .unimplemented, message: "HTTP 404: Not Found") + case 429: + self = Status(code: .unavailable, message: "HTTP 429: Too Many Requests") + case 502: + self = Status(code: .unavailable, message: "HTTP 502: Bad Gateway") + case 503: + self = Status(code: .unavailable, message: "HTTP 503: Service Unavailable") + case 504: + self = Status(code: .unavailable, message: "HTTP 504: Gateway Timeout") + default: + self = Status(code: .unknown, message: "HTTP \(httpStatusCode)") + } + } +} diff --git a/Tests/GRPCCoreTests/StatusTests.swift b/Tests/GRPCCoreTests/StatusTests.swift index 936ff8e41..fd442586a 100644 --- a/Tests/GRPCCoreTests/StatusTests.swift +++ b/Tests/GRPCCoreTests/StatusTests.swift @@ -69,4 +69,23 @@ struct StatusTests { func fitsInExistentialContainer() { #expect(MemoryLayout.size <= 24) } + + @Test( + "From HTTP status code", + arguments: [ + (400, Status(code: .internalError, message: "HTTP 400: Bad Request")), + (401, Status(code: .unauthenticated, message: "HTTP 401: Unauthorized")), + (403, Status(code: .permissionDenied, message: "HTTP 403: Forbidden")), + (404, Status(code: .unimplemented, message: "HTTP 404: Not Found")), + (429, Status(code: .unavailable, message: "HTTP 429: Too Many Requests")), + (502, Status(code: .unavailable, message: "HTTP 502: Bad Gateway")), + (503, Status(code: .unavailable, message: "HTTP 503: Service Unavailable")), + (504, Status(code: .unavailable, message: "HTTP 504: Gateway Timeout")), + (418, Status(code: .unknown, message: "HTTP 418")), + ] + ) + func convertFromHTTPStatusCode(code: Int, expected: Status) { + let status = Status(httpStatusCode: code) + #expect(status == expected) + } } From 12b2ee2eee9f16b3ec17732691499d36e8e95c25 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 22 Jan 2025 16:21:35 +0000 Subject: [PATCH 542/580] Update examples CI to use swift-test-matrix (#2174) --- .github/workflows/pull_request.yml | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e7ae36d8b..6bbf64854 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -26,14 +26,31 @@ jobs: linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - examples: + construct-examples-matrix: + name: Construct Examples matrix + runs-on: ubuntu-latest + outputs: + examples-matrix: '${{ steps.generate-matrix.outputs.examples-matrix }}' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - id: generate-matrix + run: echo "examples-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT" + env: + MATRIX_LINUX_5_9_ENABLED: false + MATRIX_LINUX_5_10_ENABLED: false + MATRIX_LINUX_COMMAND: "./dev/build-examples.sh" + MATRIX_LINUX_SETUP_COMMAND: "" + + examples-matrix: name: Examples - uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main + needs: construct-examples-matrix + uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main with: name: "Examples" - matrix_linux_5_9_enabled: false - matrix_linux_5_10_enabled: false - matrix_linux_command: "./dev/build-examples.sh" + matrix_string: '${{ needs.construct-examples-matrix.outputs.examples-matrix }}' benchmarks: name: Benchmarks From 9639fe95bd995b1e7a114ab62b3c7703c5660ed1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 22 Jan 2025 17:15:29 +0000 Subject: [PATCH 543/580] Use the protoc build plugin in examples (#2171) Motivation: We now have a SwiftPM build plugin to generate code as part of the build process, the examples should make use of it. Modifications: - Remove generated code for examples and update the generate script to stop genearting example code. - Symlink protos from examples to the checked out location in the repo; this avoids some duplication and makes it easier to keep things up-to-date. - Add a basic config to each example. - Update CI to install protoc before building examples. Result: Better examples, less code. --- .github/workflows/pull_request.yml | 2 +- Examples/echo/Package.swift | 9 +- .../echo/Sources/Generated/echo.grpc.swift | 994 --------------- Examples/echo/Sources/Generated/echo.pb.swift | 129 -- Examples/echo/Sources/Protos/echo | 1 + .../grpc-swift-proto-generator-config.json | 7 + Examples/error-details/Package.swift | 7 +- .../Sources/Generated/helloworld.grpc.swift | 362 ------ .../Sources/Generated/helloworld.pb.swift | 129 -- .../grpc-swift-proto-generator-config.json | 7 + .../Sources/Protos/helloworld.proto | 1 + Examples/hello-world/Package.swift | 9 +- Examples/hello-world/Protos/HelloWorld.proto | 1 - .../Sources/Generated/helloworld.grpc.swift | 362 ------ .../Sources/Generated/helloworld.pb.swift | 129 -- .../grpc-swift-proto-generator-config.json | 7 + .../Sources/Protos/helloworld.proto | 1 + Examples/reflection-server/Package.swift | 9 +- .../Sources/Generated/echo.grpc.swift | 994 --------------- .../Sources/Generated/echo.pb.swift | 129 -- .../reflection-server/Sources/Protos/echo | 1 + .../grpc-swift-proto-generator-config.json | 7 + Examples/route-guide/Package.swift | 9 +- .../Sources/Generated/route_guide.grpc.swift | 1126 ----------------- .../Sources/Generated/route_guide.pb.swift | 387 ------ .../grpc-swift-proto-generator-config.json | 7 + .../route-guide/Sources/Protos/route_guide | 1 + dev/protos/generate.sh | 53 - 28 files changed, 70 insertions(+), 4810 deletions(-) delete mode 100644 Examples/echo/Sources/Generated/echo.grpc.swift delete mode 100644 Examples/echo/Sources/Generated/echo.pb.swift create mode 120000 Examples/echo/Sources/Protos/echo create mode 100644 Examples/echo/Sources/Protos/grpc-swift-proto-generator-config.json delete mode 100644 Examples/error-details/Sources/Generated/helloworld.grpc.swift delete mode 100644 Examples/error-details/Sources/Generated/helloworld.pb.swift create mode 100644 Examples/error-details/Sources/Protos/grpc-swift-proto-generator-config.json create mode 120000 Examples/error-details/Sources/Protos/helloworld.proto delete mode 120000 Examples/hello-world/Protos/HelloWorld.proto delete mode 100644 Examples/hello-world/Sources/Generated/helloworld.grpc.swift delete mode 100644 Examples/hello-world/Sources/Generated/helloworld.pb.swift create mode 100644 Examples/hello-world/Sources/Protos/grpc-swift-proto-generator-config.json create mode 120000 Examples/hello-world/Sources/Protos/helloworld.proto delete mode 100644 Examples/reflection-server/Sources/Generated/echo.grpc.swift delete mode 100644 Examples/reflection-server/Sources/Generated/echo.pb.swift create mode 120000 Examples/reflection-server/Sources/Protos/echo create mode 100644 Examples/reflection-server/Sources/Protos/grpc-swift-proto-generator-config.json delete mode 100644 Examples/route-guide/Sources/Generated/route_guide.grpc.swift delete mode 100644 Examples/route-guide/Sources/Generated/route_guide.pb.swift create mode 100644 Examples/route-guide/Sources/Protos/grpc-swift-proto-generator-config.json create mode 120000 Examples/route-guide/Sources/Protos/route_guide diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 6bbf64854..8a60ba1f8 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -42,7 +42,7 @@ jobs: MATRIX_LINUX_5_9_ENABLED: false MATRIX_LINUX_5_10_ENABLED: false MATRIX_LINUX_COMMAND: "./dev/build-examples.sh" - MATRIX_LINUX_SETUP_COMMAND: "" + MATRIX_LINUX_SETUP_COMMAND: "apt update && apt install -y protobuf-compiler && ./dev/build-examples.sh" examples-matrix: name: Examples diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index ca473e981..01608d9a5 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ @@ -34,6 +34,9 @@ let package = Package( .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Examples/echo/Sources/Generated/echo.grpc.swift b/Examples/echo/Sources/Generated/echo.grpc.swift deleted file mode 100644 index 3a0e70694..000000000 --- a/Examples/echo/Sources/Generated/echo.grpc.swift +++ /dev/null @@ -1,994 +0,0 @@ -// Copyright (c) 2015, Google Inc. -// -// 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. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: echo.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -// MARK: - echo.Echo - -/// Namespace containing generated types for the "echo.Echo" service. -internal enum Echo_Echo { - /// Service descriptor for the "echo.Echo" service. - internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") - /// Namespace for method metadata. - internal enum Method { - /// Namespace for "Get" metadata. - internal enum Get { - /// Request type for "Get". - internal typealias Input = Echo_EchoRequest - /// Response type for "Get". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Get". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Get" - ) - } - /// Namespace for "Expand" metadata. - internal enum Expand { - /// Request type for "Expand". - internal typealias Input = Echo_EchoRequest - /// Response type for "Expand". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Expand". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Expand" - ) - } - /// Namespace for "Collect" metadata. - internal enum Collect { - /// Request type for "Collect". - internal typealias Input = Echo_EchoRequest - /// Response type for "Collect". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Collect". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Collect" - ) - } - /// Namespace for "Update" metadata. - internal enum Update { - /// Request type for "Update". - internal typealias Input = Echo_EchoRequest - /// Response type for "Update". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Update". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Update" - ) - } - /// Descriptors for all methods in the "echo.Echo" service. - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - Get.descriptor, - Expand.descriptor, - Collect.descriptor, - Update.descriptor - ] - } -} - -extension GRPCCore.ServiceDescriptor { - /// Service descriptor for the "echo.Echo" service. - internal static let echo_Echo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") -} - -// MARK: echo.Echo (server) - -extension Echo_Echo { - /// Streaming variant of the service protocol for the "echo.Echo" service. - /// - /// This protocol is the lowest-level of the service protocols generated for this service - /// giving you the most flexibility over the implementation of your service. This comes at - /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in - /// terms of a request stream and response stream. Where only a single request or response - /// message is expected, you are responsible for enforcing this invariant is maintained. - /// - /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` - /// or ``SimpleServiceProtocol`` instead. - internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Handle the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func get( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func expand( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Service protocol for the "echo.Echo" service. - /// - /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than - /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and - /// trailing response metadata. If you don't need these then consider using - /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then - /// use ``StreamingServiceProtocol``. - internal protocol ServiceProtocol: Echo_Echo.StreamingServiceProtocol { - /// Handle the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Echo_EchoResponse` message. - func get( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Handle the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func expand( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Echo_EchoResponse` message. - func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Handle the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Simple service protocol for the "echo.Echo" service. - /// - /// This is the highest level protocol for the service. The API is the easiest to use but - /// doesn't provide access to request or response metadata. If you need access to these - /// then use ``ServiceProtocol`` instead. - internal protocol SimpleServiceProtocol: Echo_Echo.ServiceProtocol { - /// Handle the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A `Echo_EchoRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Echo_EchoResponse` to respond with. - func get( - request: Echo_EchoRequest, - context: GRPCCore.ServerContext - ) async throws -> Echo_EchoResponse - - /// Handle the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A `Echo_EchoRequest` message. - /// - response: A response stream of `Echo_EchoResponse` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - func expand( - request: Echo_EchoRequest, - response: GRPCCore.RPCWriter, - context: GRPCCore.ServerContext - ) async throws - - /// Handle the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A stream of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Echo_EchoResponse` to respond with. - func collect( - request: GRPCCore.RPCAsyncSequence, - context: GRPCCore.ServerContext - ) async throws -> Echo_EchoResponse - - /// Handle the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A stream of `Echo_EchoRequest` messages. - /// - response: A response stream of `Echo_EchoResponse` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - func update( - request: GRPCCore.RPCAsyncSequence, - response: GRPCCore.RPCWriter, - context: GRPCCore.ServerContext - ) async throws - } -} - -// Default implementation of 'registerMethods(with:)'. -extension Echo_Echo.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { - router.registerHandler( - forMethod: Echo_Echo.Method.Get.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.get( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Echo_Echo.Method.Expand.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.expand( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Echo_Echo.Method.Collect.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.collect( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Echo_Echo.Method.Update.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.update( - request: request, - context: context - ) - } - ) - } -} - -// Default implementation of streaming methods from 'StreamingServiceProtocol'. -extension Echo_Echo.ServiceProtocol { - internal func get( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.get( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - - internal func expand( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.expand( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return response - } - - internal func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.collect( - request: request, - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } -} - -// Default implementation of methods from 'ServiceProtocol'. -extension Echo_Echo.SimpleServiceProtocol { - internal func get( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.get( - request: request.message, - context: context - ), - metadata: [:] - ) - } - - internal func expand( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - return GRPCCore.StreamingServerResponse( - metadata: [:], - producer: { writer in - try await self.expand( - request: request.message, - response: writer, - context: context - ) - return [:] - } - ) - } - - internal func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.collect( - request: request.messages, - context: context - ), - metadata: [:] - ) - } - - internal func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - return GRPCCore.StreamingServerResponse( - metadata: [:], - producer: { writer in - try await self.update( - request: request.messages, - response: writer, - context: context - ) - return [:] - } - ) - } -} - -// MARK: echo.Echo (client) - -extension Echo_Echo { - /// Generated client protocol for the "echo.Echo" service. - /// - /// You don't need to implement this protocol directly, use the generated - /// implementation, ``Client``. - internal protocol ClientProtocol: Sendable { - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func get( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func expand( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func collect( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func update( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - - /// Generated client for the "echo.Echo" service. - /// - /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps - /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived - /// means of communication with the remote peer. - internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { - private let client: GRPCCore.GRPCClient - - /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. - /// - /// - Parameters: - /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func get( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.unary( - request: request, - descriptor: Echo_Echo.Method.Get.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func expand( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Echo_Echo.Method.Expand.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func collect( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Echo_Echo.Method.Collect.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func update( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Echo_Echo.Method.Update.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } -} - -// Helpers providing default arguments to 'ClientProtocol' methods. -extension Echo_Echo.ClientProtocol { - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func get( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.get( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func expand( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.expand( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func collect( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.collect( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func update( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.update( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } -} - -// Helpers providing sugared APIs for 'ClientProtocol' methods. -extension Echo_Echo.ClientProtocol { - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func get( - _ message: Echo_EchoRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.get( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func expand( - _ message: Echo_EchoRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.expand( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - producer: A closure producing request messages to send to the server. The request - /// stream is closed when the closure returns. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func collect( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.collect( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - producer: A closure producing request messages to send to the server. The request - /// stream is closed when the closure returns. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func update( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.update( - request: request, - options: options, - onResponse: handleResponse - ) - } -} \ No newline at end of file diff --git a/Examples/echo/Sources/Generated/echo.pb.swift b/Examples/echo/Sources/Generated/echo.pb.swift deleted file mode 100644 index 88ef21ca9..000000000 --- a/Examples/echo/Sources/Generated/echo.pb.swift +++ /dev/null @@ -1,129 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: echo.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright (c) 2015, Google Inc. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct Echo_EchoRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The text of a message to be echoed. - var text: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Echo_EchoResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The text of an echo response. - var text: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "echo" - -extension Echo_EchoRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".EchoRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.text.isEmpty { - try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Echo_EchoRequest, rhs: Echo_EchoRequest) -> Bool { - if lhs.text != rhs.text {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Echo_EchoResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".EchoResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.text.isEmpty { - try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Echo_EchoResponse, rhs: Echo_EchoResponse) -> Bool { - if lhs.text != rhs.text {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/echo/Sources/Protos/echo b/Examples/echo/Sources/Protos/echo new file mode 120000 index 000000000..66fa3f5f5 --- /dev/null +++ b/Examples/echo/Sources/Protos/echo @@ -0,0 +1 @@ +../../../../dev/protos/examples/echo/ \ No newline at end of file diff --git a/Examples/echo/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/echo/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/echo/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift index c7faca691..e9c868719 100644 --- a/Examples/error-details/Package.swift +++ b/Examples/error-details/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "error-details", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), ], targets: [ .executableTarget( @@ -31,6 +31,9 @@ let package = Package( .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCInProcessTransport", package: "grpc-swift"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Examples/error-details/Sources/Generated/helloworld.grpc.swift b/Examples/error-details/Sources/Generated/helloworld.grpc.swift deleted file mode 100644 index 329c0e816..000000000 --- a/Examples/error-details/Sources/Generated/helloworld.grpc.swift +++ /dev/null @@ -1,362 +0,0 @@ -/// Copyright 2015 gRPC authors. -/// -/// 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. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: helloworld.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -// MARK: - helloworld.Greeter - -/// Namespace containing generated types for the "helloworld.Greeter" service. -internal enum Helloworld_Greeter { - /// Service descriptor for the "helloworld.Greeter" service. - internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") - /// Namespace for method metadata. - internal enum Method { - /// Namespace for "SayHello" metadata. - internal enum SayHello { - /// Request type for "SayHello". - internal typealias Input = Helloworld_HelloRequest - /// Response type for "SayHello". - internal typealias Output = Helloworld_HelloReply - /// Descriptor for "SayHello". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter"), - method: "SayHello" - ) - } - /// Descriptors for all methods in the "helloworld.Greeter" service. - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } -} - -extension GRPCCore.ServiceDescriptor { - /// Service descriptor for the "helloworld.Greeter" service. - internal static let helloworld_Greeter = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") -} - -// MARK: helloworld.Greeter (server) - -extension Helloworld_Greeter { - /// Streaming variant of the service protocol for the "helloworld.Greeter" service. - /// - /// This protocol is the lowest-level of the service protocols generated for this service - /// giving you the most flexibility over the implementation of your service. This comes at - /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in - /// terms of a request stream and response stream. Where only a single request or response - /// message is expected, you are responsible for enforcing this invariant is maintained. - /// - /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` - /// or ``SimpleServiceProtocol`` instead. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Handle the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A streaming request of `Helloworld_HelloRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Helloworld_HelloReply` messages. - func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Service protocol for the "helloworld.Greeter" service. - /// - /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than - /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and - /// trailing response metadata. If you don't need these then consider using - /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then - /// use ``StreamingServiceProtocol``. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { - /// Handle the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Helloworld_HelloReply` message. - func sayHello( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - } - - /// Simple service protocol for the "helloworld.Greeter" service. - /// - /// This is the highest level protocol for the service. The API is the easiest to use but - /// doesn't provide access to request or response metadata. If you need access to these - /// then use ``ServiceProtocol`` instead. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol SimpleServiceProtocol: Helloworld_Greeter.ServiceProtocol { - /// Handle the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A `Helloworld_HelloRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Helloworld_HelloReply` to respond with. - func sayHello( - request: Helloworld_HelloRequest, - context: GRPCCore.ServerContext - ) async throws -> Helloworld_HelloReply - } -} - -// Default implementation of 'registerMethods(with:)'. -extension Helloworld_Greeter.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { - router.registerHandler( - forMethod: Helloworld_Greeter.Method.SayHello.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.sayHello( - request: request, - context: context - ) - } - ) - } -} - -// Default implementation of streaming methods from 'StreamingServiceProtocol'. -extension Helloworld_Greeter.ServiceProtocol { - internal func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.sayHello( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } -} - -// Default implementation of methods from 'ServiceProtocol'. -extension Helloworld_Greeter.SimpleServiceProtocol { - internal func sayHello( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.sayHello( - request: request.message, - context: context - ), - metadata: [:] - ) - } -} - -// MARK: helloworld.Greeter (client) - -extension Helloworld_Greeter { - /// Generated client protocol for the "helloworld.Greeter" service. - /// - /// You don't need to implement this protocol directly, use the generated - /// implementation, ``Client``. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol ClientProtocol: Sendable { - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - serializer: A serializer for `Helloworld_HelloRequest` messages. - /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - - /// Generated client for the "helloworld.Greeter" service. - /// - /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps - /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived - /// means of communication with the remote peer. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { - private let client: GRPCCore.GRPCClient - - /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. - /// - /// - Parameters: - /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - serializer: A serializer for `Helloworld_HelloRequest` messages. - /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.unary( - request: request, - descriptor: Helloworld_Greeter.Method.SayHello.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } -} - -// Helpers providing default arguments to 'ClientProtocol' methods. -extension Helloworld_Greeter.ClientProtocol { - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func sayHello( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.sayHello( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } -} - -// Helpers providing sugared APIs for 'ClientProtocol' methods. -extension Helloworld_Greeter.ClientProtocol { - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func sayHello( - _ message: Helloworld_HelloRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.sayHello( - request: request, - options: options, - onResponse: handleResponse - ) - } -} \ No newline at end of file diff --git a/Examples/error-details/Sources/Generated/helloworld.pb.swift b/Examples/error-details/Sources/Generated/helloworld.pb.swift deleted file mode 100644 index 20b4f36df..000000000 --- a/Examples/error-details/Sources/Generated/helloworld.pb.swift +++ /dev/null @@ -1,129 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: helloworld.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -/// Copyright 2015 gRPC authors. -/// -/// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The request message containing the user's name. -struct Helloworld_HelloRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var name: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The response message containing the greetings -struct Helloworld_HelloReply: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var message: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "helloworld" - -extension Helloworld_HelloRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HelloRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Helloworld_HelloRequest, rhs: Helloworld_HelloRequest) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Helloworld_HelloReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HelloReply" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "message"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Helloworld_HelloReply, rhs: Helloworld_HelloReply) -> Bool { - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/error-details/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/error-details/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/error-details/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/error-details/Sources/Protos/helloworld.proto b/Examples/error-details/Sources/Protos/helloworld.proto new file mode 120000 index 000000000..f4684af4f --- /dev/null +++ b/Examples/error-details/Sources/Protos/helloworld.proto @@ -0,0 +1 @@ +../../../../dev/protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 18eb5c1f6..696daffc2 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ @@ -34,6 +34,9 @@ let package = Package( .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Examples/hello-world/Protos/HelloWorld.proto b/Examples/hello-world/Protos/HelloWorld.proto deleted file mode 120000 index b5e1142c1..000000000 --- a/Examples/hello-world/Protos/HelloWorld.proto +++ /dev/null @@ -1 +0,0 @@ -../../..//dev/protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file diff --git a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift deleted file mode 100644 index 329c0e816..000000000 --- a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift +++ /dev/null @@ -1,362 +0,0 @@ -/// Copyright 2015 gRPC authors. -/// -/// 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. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: helloworld.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -// MARK: - helloworld.Greeter - -/// Namespace containing generated types for the "helloworld.Greeter" service. -internal enum Helloworld_Greeter { - /// Service descriptor for the "helloworld.Greeter" service. - internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") - /// Namespace for method metadata. - internal enum Method { - /// Namespace for "SayHello" metadata. - internal enum SayHello { - /// Request type for "SayHello". - internal typealias Input = Helloworld_HelloRequest - /// Response type for "SayHello". - internal typealias Output = Helloworld_HelloReply - /// Descriptor for "SayHello". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter"), - method: "SayHello" - ) - } - /// Descriptors for all methods in the "helloworld.Greeter" service. - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } -} - -extension GRPCCore.ServiceDescriptor { - /// Service descriptor for the "helloworld.Greeter" service. - internal static let helloworld_Greeter = GRPCCore.ServiceDescriptor(fullyQualifiedService: "helloworld.Greeter") -} - -// MARK: helloworld.Greeter (server) - -extension Helloworld_Greeter { - /// Streaming variant of the service protocol for the "helloworld.Greeter" service. - /// - /// This protocol is the lowest-level of the service protocols generated for this service - /// giving you the most flexibility over the implementation of your service. This comes at - /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in - /// terms of a request stream and response stream. Where only a single request or response - /// message is expected, you are responsible for enforcing this invariant is maintained. - /// - /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` - /// or ``SimpleServiceProtocol`` instead. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Handle the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A streaming request of `Helloworld_HelloRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Helloworld_HelloReply` messages. - func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Service protocol for the "helloworld.Greeter" service. - /// - /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than - /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and - /// trailing response metadata. If you don't need these then consider using - /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then - /// use ``StreamingServiceProtocol``. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { - /// Handle the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Helloworld_HelloReply` message. - func sayHello( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - } - - /// Simple service protocol for the "helloworld.Greeter" service. - /// - /// This is the highest level protocol for the service. The API is the easiest to use but - /// doesn't provide access to request or response metadata. If you need access to these - /// then use ``ServiceProtocol`` instead. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol SimpleServiceProtocol: Helloworld_Greeter.ServiceProtocol { - /// Handle the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A `Helloworld_HelloRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Helloworld_HelloReply` to respond with. - func sayHello( - request: Helloworld_HelloRequest, - context: GRPCCore.ServerContext - ) async throws -> Helloworld_HelloReply - } -} - -// Default implementation of 'registerMethods(with:)'. -extension Helloworld_Greeter.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { - router.registerHandler( - forMethod: Helloworld_Greeter.Method.SayHello.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.sayHello( - request: request, - context: context - ) - } - ) - } -} - -// Default implementation of streaming methods from 'StreamingServiceProtocol'. -extension Helloworld_Greeter.ServiceProtocol { - internal func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.sayHello( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } -} - -// Default implementation of methods from 'ServiceProtocol'. -extension Helloworld_Greeter.SimpleServiceProtocol { - internal func sayHello( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.sayHello( - request: request.message, - context: context - ), - metadata: [:] - ) - } -} - -// MARK: helloworld.Greeter (client) - -extension Helloworld_Greeter { - /// Generated client protocol for the "helloworld.Greeter" service. - /// - /// You don't need to implement this protocol directly, use the generated - /// implementation, ``Client``. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal protocol ClientProtocol: Sendable { - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - serializer: A serializer for `Helloworld_HelloRequest` messages. - /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - - /// Generated client for the "helloworld.Greeter" service. - /// - /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps - /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived - /// means of communication with the remote peer. - /// - /// > Source IDL Documentation: - /// > - /// > The greeting service definition. - internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { - private let client: GRPCCore.GRPCClient - - /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. - /// - /// - Parameters: - /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - serializer: A serializer for `Helloworld_HelloRequest` messages. - /// - deserializer: A deserializer for `Helloworld_HelloReply` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.unary( - request: request, - descriptor: Helloworld_Greeter.Method.SayHello.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } -} - -// Helpers providing default arguments to 'ClientProtocol' methods. -extension Helloworld_Greeter.ClientProtocol { - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - request: A request containing a single `Helloworld_HelloRequest` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func sayHello( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.sayHello( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } -} - -// Helpers providing sugared APIs for 'ClientProtocol' methods. -extension Helloworld_Greeter.ClientProtocol { - /// Call the "SayHello" method. - /// - /// > Source IDL Documentation: - /// > - /// > Sends a greeting - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func sayHello( - _ message: Helloworld_HelloRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.sayHello( - request: request, - options: options, - onResponse: handleResponse - ) - } -} \ No newline at end of file diff --git a/Examples/hello-world/Sources/Generated/helloworld.pb.swift b/Examples/hello-world/Sources/Generated/helloworld.pb.swift deleted file mode 100644 index 20b4f36df..000000000 --- a/Examples/hello-world/Sources/Generated/helloworld.pb.swift +++ /dev/null @@ -1,129 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: helloworld.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -/// Copyright 2015 gRPC authors. -/// -/// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The request message containing the user's name. -struct Helloworld_HelloRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var name: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The response message containing the greetings -struct Helloworld_HelloReply: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var message: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "helloworld" - -extension Helloworld_HelloRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HelloRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Helloworld_HelloRequest, rhs: Helloworld_HelloRequest) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Helloworld_HelloReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HelloReply" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "message"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Helloworld_HelloReply, rhs: Helloworld_HelloReply) -> Bool { - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/hello-world/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/hello-world/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/hello-world/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/hello-world/Sources/Protos/helloworld.proto b/Examples/hello-world/Sources/Protos/helloworld.proto new file mode 120000 index 000000000..f4684af4f --- /dev/null +++ b/Examples/hello-world/Sources/Protos/helloworld.proto @@ -0,0 +1 @@ +../../../../dev/protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file diff --git a/Examples/reflection-server/Package.swift b/Examples/reflection-server/Package.swift index 03aed7629..684b10c1c 100644 --- a/Examples/reflection-server/Package.swift +++ b/Examples/reflection-server/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "reflection-server", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), .package(url: "https://github.com/grpc/grpc-swift-extras.git", exact: "1.0.0-beta.3"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], @@ -39,6 +39,9 @@ let package = Package( ], resources: [ .copy("DescriptorSets") + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Examples/reflection-server/Sources/Generated/echo.grpc.swift b/Examples/reflection-server/Sources/Generated/echo.grpc.swift deleted file mode 100644 index 3a0e70694..000000000 --- a/Examples/reflection-server/Sources/Generated/echo.grpc.swift +++ /dev/null @@ -1,994 +0,0 @@ -// Copyright (c) 2015, Google Inc. -// -// 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. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: echo.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -// MARK: - echo.Echo - -/// Namespace containing generated types for the "echo.Echo" service. -internal enum Echo_Echo { - /// Service descriptor for the "echo.Echo" service. - internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") - /// Namespace for method metadata. - internal enum Method { - /// Namespace for "Get" metadata. - internal enum Get { - /// Request type for "Get". - internal typealias Input = Echo_EchoRequest - /// Response type for "Get". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Get". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Get" - ) - } - /// Namespace for "Expand" metadata. - internal enum Expand { - /// Request type for "Expand". - internal typealias Input = Echo_EchoRequest - /// Response type for "Expand". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Expand". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Expand" - ) - } - /// Namespace for "Collect" metadata. - internal enum Collect { - /// Request type for "Collect". - internal typealias Input = Echo_EchoRequest - /// Response type for "Collect". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Collect". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Collect" - ) - } - /// Namespace for "Update" metadata. - internal enum Update { - /// Request type for "Update". - internal typealias Input = Echo_EchoRequest - /// Response type for "Update". - internal typealias Output = Echo_EchoResponse - /// Descriptor for "Update". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo"), - method: "Update" - ) - } - /// Descriptors for all methods in the "echo.Echo" service. - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - Get.descriptor, - Expand.descriptor, - Collect.descriptor, - Update.descriptor - ] - } -} - -extension GRPCCore.ServiceDescriptor { - /// Service descriptor for the "echo.Echo" service. - internal static let echo_Echo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.Echo") -} - -// MARK: echo.Echo (server) - -extension Echo_Echo { - /// Streaming variant of the service protocol for the "echo.Echo" service. - /// - /// This protocol is the lowest-level of the service protocols generated for this service - /// giving you the most flexibility over the implementation of your service. This comes at - /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in - /// terms of a request stream and response stream. Where only a single request or response - /// message is expected, you are responsible for enforcing this invariant is maintained. - /// - /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` - /// or ``SimpleServiceProtocol`` instead. - internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Handle the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func get( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func expand( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Service protocol for the "echo.Echo" service. - /// - /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than - /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and - /// trailing response metadata. If you don't need these then consider using - /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then - /// use ``StreamingServiceProtocol``. - internal protocol ServiceProtocol: Echo_Echo.StreamingServiceProtocol { - /// Handle the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Echo_EchoResponse` message. - func get( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Handle the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func expand( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Echo_EchoResponse` message. - func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Handle the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Echo_EchoResponse` messages. - func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Simple service protocol for the "echo.Echo" service. - /// - /// This is the highest level protocol for the service. The API is the easiest to use but - /// doesn't provide access to request or response metadata. If you need access to these - /// then use ``ServiceProtocol`` instead. - internal protocol SimpleServiceProtocol: Echo_Echo.ServiceProtocol { - /// Handle the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A `Echo_EchoRequest` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Echo_EchoResponse` to respond with. - func get( - request: Echo_EchoRequest, - context: GRPCCore.ServerContext - ) async throws -> Echo_EchoResponse - - /// Handle the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A `Echo_EchoRequest` message. - /// - response: A response stream of `Echo_EchoResponse` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - func expand( - request: Echo_EchoRequest, - response: GRPCCore.RPCWriter, - context: GRPCCore.ServerContext - ) async throws - - /// Handle the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A stream of `Echo_EchoRequest` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Echo_EchoResponse` to respond with. - func collect( - request: GRPCCore.RPCAsyncSequence, - context: GRPCCore.ServerContext - ) async throws -> Echo_EchoResponse - - /// Handle the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A stream of `Echo_EchoRequest` messages. - /// - response: A response stream of `Echo_EchoResponse` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - func update( - request: GRPCCore.RPCAsyncSequence, - response: GRPCCore.RPCWriter, - context: GRPCCore.ServerContext - ) async throws - } -} - -// Default implementation of 'registerMethods(with:)'. -extension Echo_Echo.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { - router.registerHandler( - forMethod: Echo_Echo.Method.Get.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.get( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Echo_Echo.Method.Expand.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.expand( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Echo_Echo.Method.Collect.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.collect( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Echo_Echo.Method.Update.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.update( - request: request, - context: context - ) - } - ) - } -} - -// Default implementation of streaming methods from 'StreamingServiceProtocol'. -extension Echo_Echo.ServiceProtocol { - internal func get( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.get( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - - internal func expand( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.expand( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return response - } - - internal func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.collect( - request: request, - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } -} - -// Default implementation of methods from 'ServiceProtocol'. -extension Echo_Echo.SimpleServiceProtocol { - internal func get( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.get( - request: request.message, - context: context - ), - metadata: [:] - ) - } - - internal func expand( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - return GRPCCore.StreamingServerResponse( - metadata: [:], - producer: { writer in - try await self.expand( - request: request.message, - response: writer, - context: context - ) - return [:] - } - ) - } - - internal func collect( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.collect( - request: request.messages, - context: context - ), - metadata: [:] - ) - } - - internal func update( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - return GRPCCore.StreamingServerResponse( - metadata: [:], - producer: { writer in - try await self.update( - request: request.messages, - response: writer, - context: context - ) - return [:] - } - ) - } -} - -// MARK: echo.Echo (client) - -extension Echo_Echo { - /// Generated client protocol for the "echo.Echo" service. - /// - /// You don't need to implement this protocol directly, use the generated - /// implementation, ``Client``. - internal protocol ClientProtocol: Sendable { - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func get( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func expand( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func collect( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func update( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - - /// Generated client for the "echo.Echo" service. - /// - /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps - /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived - /// means of communication with the remote peer. - internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { - private let client: GRPCCore.GRPCClient - - /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. - /// - /// - Parameters: - /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func get( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.unary( - request: request, - descriptor: Echo_Echo.Method.Get.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func expand( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Echo_Echo.Method.Expand.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func collect( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Echo_Echo.Method.Collect.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - serializer: A serializer for `Echo_EchoRequest` messages. - /// - deserializer: A deserializer for `Echo_EchoResponse` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func update( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Echo_Echo.Method.Update.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } -} - -// Helpers providing default arguments to 'ClientProtocol' methods. -extension Echo_Echo.ClientProtocol { - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func get( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.get( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: A request containing a single `Echo_EchoRequest` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func expand( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.expand( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func collect( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.collect( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - request: A streaming request producing `Echo_EchoRequest` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func update( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.update( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } -} - -// Helpers providing sugared APIs for 'ClientProtocol' methods. -extension Echo_Echo.ClientProtocol { - /// Call the "Get" method. - /// - /// > Source IDL Documentation: - /// > - /// > Immediately returns an echo of a request. - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func get( - _ message: Echo_EchoRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.get( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Expand" method. - /// - /// > Source IDL Documentation: - /// > - /// > Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func expand( - _ message: Echo_EchoRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.expand( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Collect" method. - /// - /// > Source IDL Documentation: - /// > - /// > Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// - Parameters: - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - producer: A closure producing request messages to send to the server. The request - /// stream is closed when the closure returns. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func collect( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.collect( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "Update" method. - /// - /// > Source IDL Documentation: - /// > - /// > Streams back messages as they are received in an input stream. - /// - /// - Parameters: - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - producer: A closure producing request messages to send to the server. The request - /// stream is closed when the closure returns. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func update( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.update( - request: request, - options: options, - onResponse: handleResponse - ) - } -} \ No newline at end of file diff --git a/Examples/reflection-server/Sources/Generated/echo.pb.swift b/Examples/reflection-server/Sources/Generated/echo.pb.swift deleted file mode 100644 index 88ef21ca9..000000000 --- a/Examples/reflection-server/Sources/Generated/echo.pb.swift +++ /dev/null @@ -1,129 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: echo.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright (c) 2015, Google Inc. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct Echo_EchoRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The text of a message to be echoed. - var text: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Echo_EchoResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The text of an echo response. - var text: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "echo" - -extension Echo_EchoRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".EchoRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.text.isEmpty { - try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Echo_EchoRequest, rhs: Echo_EchoRequest) -> Bool { - if lhs.text != rhs.text {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Echo_EchoResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".EchoResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.text.isEmpty { - try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Echo_EchoResponse, rhs: Echo_EchoResponse) -> Bool { - if lhs.text != rhs.text {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/reflection-server/Sources/Protos/echo b/Examples/reflection-server/Sources/Protos/echo new file mode 120000 index 000000000..d28b5425a --- /dev/null +++ b/Examples/reflection-server/Sources/Protos/echo @@ -0,0 +1 @@ +../../../../dev/protos/examples/echo \ No newline at end of file diff --git a/Examples/reflection-server/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/reflection-server/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/reflection-server/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index 86df4fa62..b2d38d69f 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ @@ -37,6 +37,9 @@ let package = Package( ], resources: [ .copy("route_guide_db.json") + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift deleted file mode 100644 index 87ef9ba8b..000000000 --- a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift +++ /dev/null @@ -1,1126 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: route_guide.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -// MARK: - routeguide.RouteGuide - -/// Namespace containing generated types for the "routeguide.RouteGuide" service. -internal enum Routeguide_RouteGuide { - /// Service descriptor for the "routeguide.RouteGuide" service. - internal static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide") - /// Namespace for method metadata. - internal enum Method { - /// Namespace for "GetFeature" metadata. - internal enum GetFeature { - /// Request type for "GetFeature". - internal typealias Input = Routeguide_Point - /// Response type for "GetFeature". - internal typealias Output = Routeguide_Feature - /// Descriptor for "GetFeature". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), - method: "GetFeature" - ) - } - /// Namespace for "ListFeatures" metadata. - internal enum ListFeatures { - /// Request type for "ListFeatures". - internal typealias Input = Routeguide_Rectangle - /// Response type for "ListFeatures". - internal typealias Output = Routeguide_Feature - /// Descriptor for "ListFeatures". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), - method: "ListFeatures" - ) - } - /// Namespace for "RecordRoute" metadata. - internal enum RecordRoute { - /// Request type for "RecordRoute". - internal typealias Input = Routeguide_Point - /// Response type for "RecordRoute". - internal typealias Output = Routeguide_RouteSummary - /// Descriptor for "RecordRoute". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), - method: "RecordRoute" - ) - } - /// Namespace for "RouteChat" metadata. - internal enum RouteChat { - /// Request type for "RouteChat". - internal typealias Input = Routeguide_RouteNote - /// Response type for "RouteChat". - internal typealias Output = Routeguide_RouteNote - /// Descriptor for "RouteChat". - internal static let descriptor = GRPCCore.MethodDescriptor( - service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide"), - method: "RouteChat" - ) - } - /// Descriptors for all methods in the "routeguide.RouteGuide" service. - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - GetFeature.descriptor, - ListFeatures.descriptor, - RecordRoute.descriptor, - RouteChat.descriptor - ] - } -} - -extension GRPCCore.ServiceDescriptor { - /// Service descriptor for the "routeguide.RouteGuide" service. - internal static let routeguide_RouteGuide = GRPCCore.ServiceDescriptor(fullyQualifiedService: "routeguide.RouteGuide") -} - -// MARK: routeguide.RouteGuide (server) - -extension Routeguide_RouteGuide { - /// Streaming variant of the service protocol for the "routeguide.RouteGuide" service. - /// - /// This protocol is the lowest-level of the service protocols generated for this service - /// giving you the most flexibility over the implementation of your service. This comes at - /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in - /// terms of a request stream and response stream. Where only a single request or response - /// message is expected, you are responsible for enforcing this invariant is maintained. - /// - /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` - /// or ``SimpleServiceProtocol`` instead. - /// - /// > Source IDL Documentation: - /// > - /// > Interface exported by the server. - internal protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Handle the "GetFeature" method. - /// - /// > Source IDL Documentation: - /// > - /// > A simple RPC. - /// > - /// > Obtains the feature at a given position. - /// > - /// > A feature with an empty name is returned if there's no feature at the given - /// > position. - /// - /// - Parameters: - /// - request: A streaming request of `Routeguide_Point` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Routeguide_Feature` messages. - func getFeature( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "ListFeatures" method. - /// - /// > Source IDL Documentation: - /// > - /// > A server-to-client streaming RPC. - /// > - /// > Obtains the Features available within the given Rectangle. Results are - /// > streamed rather than returned at once (e.g. in a response message with a - /// > repeated field), as the rectangle may cover a large area and contain a - /// > huge number of features. - /// - /// - Parameters: - /// - request: A streaming request of `Routeguide_Rectangle` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Routeguide_Feature` messages. - func listFeatures( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "RecordRoute" method. - /// - /// > Source IDL Documentation: - /// > - /// > A client-to-server streaming RPC. - /// > - /// > Accepts a stream of Points on a route being traversed, returning a - /// > RouteSummary when traversal is completed. - /// - /// - Parameters: - /// - request: A streaming request of `Routeguide_Point` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Routeguide_RouteSummary` messages. - func recordRoute( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "RouteChat" method. - /// - /// > Source IDL Documentation: - /// > - /// > A Bidirectional streaming RPC. - /// > - /// > Accepts a stream of RouteNotes sent while a route is being traversed, - /// > while receiving other RouteNotes (e.g. from other users). - /// - /// - Parameters: - /// - request: A streaming request of `Routeguide_RouteNote` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Routeguide_RouteNote` messages. - func routeChat( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Service protocol for the "routeguide.RouteGuide" service. - /// - /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than - /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and - /// trailing response metadata. If you don't need these then consider using - /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then - /// use ``StreamingServiceProtocol``. - /// - /// > Source IDL Documentation: - /// > - /// > Interface exported by the server. - internal protocol ServiceProtocol: Routeguide_RouteGuide.StreamingServiceProtocol { - /// Handle the "GetFeature" method. - /// - /// > Source IDL Documentation: - /// > - /// > A simple RPC. - /// > - /// > Obtains the feature at a given position. - /// > - /// > A feature with an empty name is returned if there's no feature at the given - /// > position. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Point` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Routeguide_Feature` message. - func getFeature( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Handle the "ListFeatures" method. - /// - /// > Source IDL Documentation: - /// > - /// > A server-to-client streaming RPC. - /// > - /// > Obtains the Features available within the given Rectangle. Results are - /// > streamed rather than returned at once (e.g. in a response message with a - /// > repeated field), as the rectangle may cover a large area and contain a - /// > huge number of features. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Rectangle` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Routeguide_Feature` messages. - func listFeatures( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Handle the "RecordRoute" method. - /// - /// > Source IDL Documentation: - /// > - /// > A client-to-server streaming RPC. - /// > - /// > Accepts a stream of Points on a route being traversed, returning a - /// > RouteSummary when traversal is completed. - /// - /// - Parameters: - /// - request: A streaming request of `Routeguide_Point` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A response containing a single `Routeguide_RouteSummary` message. - func recordRoute( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Handle the "RouteChat" method. - /// - /// > Source IDL Documentation: - /// > - /// > A Bidirectional streaming RPC. - /// > - /// > Accepts a stream of RouteNotes sent while a route is being traversed, - /// > while receiving other RouteNotes (e.g. from other users). - /// - /// - Parameters: - /// - request: A streaming request of `Routeguide_RouteNote` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A streaming response of `Routeguide_RouteNote` messages. - func routeChat( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - - /// Simple service protocol for the "routeguide.RouteGuide" service. - /// - /// This is the highest level protocol for the service. The API is the easiest to use but - /// doesn't provide access to request or response metadata. If you need access to these - /// then use ``ServiceProtocol`` instead. - /// - /// > Source IDL Documentation: - /// > - /// > Interface exported by the server. - internal protocol SimpleServiceProtocol: Routeguide_RouteGuide.ServiceProtocol { - /// Handle the "GetFeature" method. - /// - /// > Source IDL Documentation: - /// > - /// > A simple RPC. - /// > - /// > Obtains the feature at a given position. - /// > - /// > A feature with an empty name is returned if there's no feature at the given - /// > position. - /// - /// - Parameters: - /// - request: A `Routeguide_Point` message. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Routeguide_Feature` to respond with. - func getFeature( - request: Routeguide_Point, - context: GRPCCore.ServerContext - ) async throws -> Routeguide_Feature - - /// Handle the "ListFeatures" method. - /// - /// > Source IDL Documentation: - /// > - /// > A server-to-client streaming RPC. - /// > - /// > Obtains the Features available within the given Rectangle. Results are - /// > streamed rather than returned at once (e.g. in a response message with a - /// > repeated field), as the rectangle may cover a large area and contain a - /// > huge number of features. - /// - /// - Parameters: - /// - request: A `Routeguide_Rectangle` message. - /// - response: A response stream of `Routeguide_Feature` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - func listFeatures( - request: Routeguide_Rectangle, - response: GRPCCore.RPCWriter, - context: GRPCCore.ServerContext - ) async throws - - /// Handle the "RecordRoute" method. - /// - /// > Source IDL Documentation: - /// > - /// > A client-to-server streaming RPC. - /// > - /// > Accepts a stream of Points on a route being traversed, returning a - /// > RouteSummary when traversal is completed. - /// - /// - Parameters: - /// - request: A stream of `Routeguide_Point` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - /// - Returns: A `Routeguide_RouteSummary` to respond with. - func recordRoute( - request: GRPCCore.RPCAsyncSequence, - context: GRPCCore.ServerContext - ) async throws -> Routeguide_RouteSummary - - /// Handle the "RouteChat" method. - /// - /// > Source IDL Documentation: - /// > - /// > A Bidirectional streaming RPC. - /// > - /// > Accepts a stream of RouteNotes sent while a route is being traversed, - /// > while receiving other RouteNotes (e.g. from other users). - /// - /// - Parameters: - /// - request: A stream of `Routeguide_RouteNote` messages. - /// - response: A response stream of `Routeguide_RouteNote` messages. - /// - context: Context providing information about the RPC. - /// - Throws: Any error which occurred during the processing of the request. Thrown errors - /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted - /// to an internal error. - func routeChat( - request: GRPCCore.RPCAsyncSequence, - response: GRPCCore.RPCWriter, - context: GRPCCore.ServerContext - ) async throws - } -} - -// Default implementation of 'registerMethods(with:)'. -extension Routeguide_RouteGuide.StreamingServiceProtocol { - internal func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { - router.registerHandler( - forMethod: Routeguide_RouteGuide.Method.GetFeature.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.getFeature( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Routeguide_RouteGuide.Method.ListFeatures.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.listFeatures( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Routeguide_RouteGuide.Method.RecordRoute.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.recordRoute( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Routeguide_RouteGuide.Method.RouteChat.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.routeChat( - request: request, - context: context - ) - } - ) - } -} - -// Default implementation of streaming methods from 'StreamingServiceProtocol'. -extension Routeguide_RouteGuide.ServiceProtocol { - internal func getFeature( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.getFeature( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - - internal func listFeatures( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.listFeatures( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return response - } - - internal func recordRoute( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.recordRoute( - request: request, - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } -} - -// Default implementation of methods from 'ServiceProtocol'. -extension Routeguide_RouteGuide.SimpleServiceProtocol { - internal func getFeature( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.getFeature( - request: request.message, - context: context - ), - metadata: [:] - ) - } - - internal func listFeatures( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - return GRPCCore.StreamingServerResponse( - metadata: [:], - producer: { writer in - try await self.listFeatures( - request: request.message, - response: writer, - context: context - ) - return [:] - } - ) - } - - internal func recordRoute( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse { - return GRPCCore.ServerResponse( - message: try await self.recordRoute( - request: request.messages, - context: context - ), - metadata: [:] - ) - } - - internal func routeChat( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - return GRPCCore.StreamingServerResponse( - metadata: [:], - producer: { writer in - try await self.routeChat( - request: request.messages, - response: writer, - context: context - ) - return [:] - } - ) - } -} - -// MARK: routeguide.RouteGuide (client) - -extension Routeguide_RouteGuide { - /// Generated client protocol for the "routeguide.RouteGuide" service. - /// - /// You don't need to implement this protocol directly, use the generated - /// implementation, ``Client``. - /// - /// > Source IDL Documentation: - /// > - /// > Interface exported by the server. - internal protocol ClientProtocol: Sendable { - /// Call the "GetFeature" method. - /// - /// > Source IDL Documentation: - /// > - /// > A simple RPC. - /// > - /// > Obtains the feature at a given position. - /// > - /// > A feature with an empty name is returned if there's no feature at the given - /// > position. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Point` message. - /// - serializer: A serializer for `Routeguide_Point` messages. - /// - deserializer: A deserializer for `Routeguide_Feature` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func getFeature( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "ListFeatures" method. - /// - /// > Source IDL Documentation: - /// > - /// > A server-to-client streaming RPC. - /// > - /// > Obtains the Features available within the given Rectangle. Results are - /// > streamed rather than returned at once (e.g. in a response message with a - /// > repeated field), as the rectangle may cover a large area and contain a - /// > huge number of features. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Rectangle` message. - /// - serializer: A serializer for `Routeguide_Rectangle` messages. - /// - deserializer: A deserializer for `Routeguide_Feature` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func listFeatures( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "RecordRoute" method. - /// - /// > Source IDL Documentation: - /// > - /// > A client-to-server streaming RPC. - /// > - /// > Accepts a stream of Points on a route being traversed, returning a - /// > RouteSummary when traversal is completed. - /// - /// - Parameters: - /// - request: A streaming request producing `Routeguide_Point` messages. - /// - serializer: A serializer for `Routeguide_Point` messages. - /// - deserializer: A deserializer for `Routeguide_RouteSummary` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func recordRoute( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Call the "RouteChat" method. - /// - /// > Source IDL Documentation: - /// > - /// > A Bidirectional streaming RPC. - /// > - /// > Accepts a stream of RouteNotes sent while a route is being traversed, - /// > while receiving other RouteNotes (e.g. from other users). - /// - /// - Parameters: - /// - request: A streaming request producing `Routeguide_RouteNote` messages. - /// - serializer: A serializer for `Routeguide_RouteNote` messages. - /// - deserializer: A deserializer for `Routeguide_RouteNote` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - func routeChat( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - - /// Generated client for the "routeguide.RouteGuide" service. - /// - /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps - /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived - /// means of communication with the remote peer. - /// - /// > Source IDL Documentation: - /// > - /// > Interface exported by the server. - internal struct Client: ClientProtocol where Transport: GRPCCore.ClientTransport { - private let client: GRPCCore.GRPCClient - - /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. - /// - /// - Parameters: - /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Call the "GetFeature" method. - /// - /// > Source IDL Documentation: - /// > - /// > A simple RPC. - /// > - /// > Obtains the feature at a given position. - /// > - /// > A feature with an empty name is returned if there's no feature at the given - /// > position. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Point` message. - /// - serializer: A serializer for `Routeguide_Point` messages. - /// - deserializer: A deserializer for `Routeguide_Feature` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func getFeature( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.unary( - request: request, - descriptor: Routeguide_RouteGuide.Method.GetFeature.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "ListFeatures" method. - /// - /// > Source IDL Documentation: - /// > - /// > A server-to-client streaming RPC. - /// > - /// > Obtains the Features available within the given Rectangle. Results are - /// > streamed rather than returned at once (e.g. in a response message with a - /// > repeated field), as the rectangle may cover a large area and contain a - /// > huge number of features. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Rectangle` message. - /// - serializer: A serializer for `Routeguide_Rectangle` messages. - /// - deserializer: A deserializer for `Routeguide_Feature` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func listFeatures( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Routeguide_RouteGuide.Method.ListFeatures.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "RecordRoute" method. - /// - /// > Source IDL Documentation: - /// > - /// > A client-to-server streaming RPC. - /// > - /// > Accepts a stream of Points on a route being traversed, returning a - /// > RouteSummary when traversal is completed. - /// - /// - Parameters: - /// - request: A streaming request producing `Routeguide_Point` messages. - /// - serializer: A serializer for `Routeguide_Point` messages. - /// - deserializer: A deserializer for `Routeguide_RouteSummary` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func recordRoute( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Routeguide_RouteGuide.Method.RecordRoute.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "RouteChat" method. - /// - /// > Source IDL Documentation: - /// > - /// > A Bidirectional streaming RPC. - /// > - /// > Accepts a stream of RouteNotes sent while a route is being traversed, - /// > while receiving other RouteNotes (e.g. from other users). - /// - /// - Parameters: - /// - request: A streaming request producing `Routeguide_RouteNote` messages. - /// - serializer: A serializer for `Routeguide_RouteNote` messages. - /// - deserializer: A deserializer for `Routeguide_RouteNote` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func routeChat( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Routeguide_RouteGuide.Method.RouteChat.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } -} - -// Helpers providing default arguments to 'ClientProtocol' methods. -extension Routeguide_RouteGuide.ClientProtocol { - /// Call the "GetFeature" method. - /// - /// > Source IDL Documentation: - /// > - /// > A simple RPC. - /// > - /// > Obtains the feature at a given position. - /// > - /// > A feature with an empty name is returned if there's no feature at the given - /// > position. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Point` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func getFeature( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.getFeature( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "ListFeatures" method. - /// - /// > Source IDL Documentation: - /// > - /// > A server-to-client streaming RPC. - /// > - /// > Obtains the Features available within the given Rectangle. Results are - /// > streamed rather than returned at once (e.g. in a response message with a - /// > repeated field), as the rectangle may cover a large area and contain a - /// > huge number of features. - /// - /// - Parameters: - /// - request: A request containing a single `Routeguide_Rectangle` message. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func listFeatures( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.listFeatures( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "RecordRoute" method. - /// - /// > Source IDL Documentation: - /// > - /// > A client-to-server streaming RPC. - /// > - /// > Accepts a stream of Points on a route being traversed, returning a - /// > RouteSummary when traversal is completed. - /// - /// - Parameters: - /// - request: A streaming request producing `Routeguide_Point` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func recordRoute( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.recordRoute( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - /// Call the "RouteChat" method. - /// - /// > Source IDL Documentation: - /// > - /// > A Bidirectional streaming RPC. - /// > - /// > Accepts a stream of RouteNotes sent while a route is being traversed, - /// > while receiving other RouteNotes (e.g. from other users). - /// - /// - Parameters: - /// - request: A streaming request producing `Routeguide_RouteNote` messages. - /// - options: Options to apply to this RPC. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func routeChat( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.routeChat( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } -} - -// Helpers providing sugared APIs for 'ClientProtocol' methods. -extension Routeguide_RouteGuide.ClientProtocol { - /// Call the "GetFeature" method. - /// - /// > Source IDL Documentation: - /// > - /// > A simple RPC. - /// > - /// > Obtains the feature at a given position. - /// > - /// > A feature with an empty name is returned if there's no feature at the given - /// > position. - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func getFeature( - _ message: Routeguide_Point, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.getFeature( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "ListFeatures" method. - /// - /// > Source IDL Documentation: - /// > - /// > A server-to-client streaming RPC. - /// > - /// > Obtains the Features available within the given Rectangle. Results are - /// > streamed rather than returned at once (e.g. in a response message with a - /// > repeated field), as the rectangle may cover a large area and contain a - /// > huge number of features. - /// - /// - Parameters: - /// - message: request message to send. - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func listFeatures( - _ message: Routeguide_Rectangle, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.listFeatures( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "RecordRoute" method. - /// - /// > Source IDL Documentation: - /// > - /// > A client-to-server streaming RPC. - /// > - /// > Accepts a stream of Points on a route being traversed, returning a - /// > RouteSummary when traversal is completed. - /// - /// - Parameters: - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - producer: A closure producing request messages to send to the server. The request - /// stream is closed when the closure returns. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func recordRoute( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.recordRoute( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Call the "RouteChat" method. - /// - /// > Source IDL Documentation: - /// > - /// > A Bidirectional streaming RPC. - /// > - /// > Accepts a stream of RouteNotes sent while a route is being traversed, - /// > while receiving other RouteNotes (e.g. from other users). - /// - /// - Parameters: - /// - metadata: Additional metadata to send, defaults to empty. - /// - options: Options to apply to this RPC, defaults to `.defaults`. - /// - producer: A closure producing request messages to send to the server. The request - /// stream is closed when the closure returns. - /// - handleResponse: A closure which handles the response, the result of which is - /// returned to the caller. Returning from the closure will cancel the RPC if it - /// hasn't already finished. - /// - Returns: The result of `handleResponse`. - internal func routeChat( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.routeChat( - request: request, - options: options, - onResponse: handleResponse - ) - } -} \ No newline at end of file diff --git a/Examples/route-guide/Sources/Generated/route_guide.pb.swift b/Examples/route-guide/Sources/Generated/route_guide.pb.swift deleted file mode 100644 index 09892ef83..000000000 --- a/Examples/route-guide/Sources/Generated/route_guide.pb.swift +++ /dev/null @@ -1,387 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: route_guide.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// Points are represented as latitude-longitude pairs in the E7 representation -/// (degrees multiplied by 10**7 and rounded to the nearest integer). -/// Latitudes should be in the range +/- 90 degrees and longitude should be in -/// the range +/- 180 degrees (inclusive). -struct Routeguide_Point: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var latitude: Int32 = 0 - - var longitude: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A latitude-longitude rectangle, represented as two diagonally opposite -/// points "lo" and "hi". -struct Routeguide_Rectangle: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// One corner of the rectangle. - var lo: Routeguide_Point { - get {return _lo ?? Routeguide_Point()} - set {_lo = newValue} - } - /// Returns true if `lo` has been explicitly set. - var hasLo: Bool {return self._lo != nil} - /// Clears the value of `lo`. Subsequent reads from it will return its default value. - mutating func clearLo() {self._lo = nil} - - /// The other corner of the rectangle. - var hi: Routeguide_Point { - get {return _hi ?? Routeguide_Point()} - set {_hi = newValue} - } - /// Returns true if `hi` has been explicitly set. - var hasHi: Bool {return self._hi != nil} - /// Clears the value of `hi`. Subsequent reads from it will return its default value. - mutating func clearHi() {self._hi = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _lo: Routeguide_Point? = nil - fileprivate var _hi: Routeguide_Point? = nil -} - -/// A feature names something at a given point. -/// -/// If a feature could not be named, the name is empty. -struct Routeguide_Feature: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The name of the feature. - var name: String = String() - - /// The point where the feature is detected. - var location: Routeguide_Point { - get {return _location ?? Routeguide_Point()} - set {_location = newValue} - } - /// Returns true if `location` has been explicitly set. - var hasLocation: Bool {return self._location != nil} - /// Clears the value of `location`. Subsequent reads from it will return its default value. - mutating func clearLocation() {self._location = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _location: Routeguide_Point? = nil -} - -/// A RouteNote is a message sent while at a given point. -struct Routeguide_RouteNote: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The location from which the message is sent. - var location: Routeguide_Point { - get {return _location ?? Routeguide_Point()} - set {_location = newValue} - } - /// Returns true if `location` has been explicitly set. - var hasLocation: Bool {return self._location != nil} - /// Clears the value of `location`. Subsequent reads from it will return its default value. - mutating func clearLocation() {self._location = nil} - - /// The message to be sent. - var message: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _location: Routeguide_Point? = nil -} - -/// A RouteSummary is received in response to a RecordRoute rpc. -/// -/// It contains the number of individual points received, the number of -/// detected features, and the total distance covered as the cumulative sum of -/// the distance between each point. -struct Routeguide_RouteSummary: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of points received. - var pointCount: Int32 = 0 - - /// The number of known features passed while traversing the route. - var featureCount: Int32 = 0 - - /// The distance covered in metres. - var distance: Int32 = 0 - - /// The duration of the traversal in seconds. - var elapsedTime: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "routeguide" - -extension Routeguide_Point: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Point" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "latitude"), - 2: .same(proto: "longitude"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.latitude) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.longitude) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.latitude != 0 { - try visitor.visitSingularInt32Field(value: self.latitude, fieldNumber: 1) - } - if self.longitude != 0 { - try visitor.visitSingularInt32Field(value: self.longitude, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Routeguide_Point, rhs: Routeguide_Point) -> Bool { - if lhs.latitude != rhs.latitude {return false} - if lhs.longitude != rhs.longitude {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_Rectangle: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Rectangle" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "lo"), - 2: .same(proto: "hi"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._lo) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._hi) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._lo { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._hi { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Routeguide_Rectangle, rhs: Routeguide_Rectangle) -> Bool { - if lhs._lo != rhs._lo {return false} - if lhs._hi != rhs._hi {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_Feature: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Feature" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .same(proto: "location"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._location) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try { if let v = self._location { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Routeguide_Feature, rhs: Routeguide_Feature) -> Bool { - if lhs.name != rhs.name {return false} - if lhs._location != rhs._location {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_RouteNote: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".RouteNote" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "location"), - 2: .same(proto: "message"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._location) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._location { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Routeguide_RouteNote, rhs: Routeguide_RouteNote) -> Bool { - if lhs._location != rhs._location {return false} - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_RouteSummary: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".RouteSummary" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "point_count"), - 2: .standard(proto: "feature_count"), - 3: .same(proto: "distance"), - 4: .standard(proto: "elapsed_time"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.pointCount) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.featureCount) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &self.distance) }() - case 4: try { try decoder.decodeSingularInt32Field(value: &self.elapsedTime) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.pointCount != 0 { - try visitor.visitSingularInt32Field(value: self.pointCount, fieldNumber: 1) - } - if self.featureCount != 0 { - try visitor.visitSingularInt32Field(value: self.featureCount, fieldNumber: 2) - } - if self.distance != 0 { - try visitor.visitSingularInt32Field(value: self.distance, fieldNumber: 3) - } - if self.elapsedTime != 0 { - try visitor.visitSingularInt32Field(value: self.elapsedTime, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Routeguide_RouteSummary, rhs: Routeguide_RouteSummary) -> Bool { - if lhs.pointCount != rhs.pointCount {return false} - if lhs.featureCount != rhs.featureCount {return false} - if lhs.distance != rhs.distance {return false} - if lhs.elapsedTime != rhs.elapsedTime {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/route-guide/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/route-guide/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/route-guide/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/route-guide/Sources/Protos/route_guide b/Examples/route-guide/Sources/Protos/route_guide new file mode 120000 index 000000000..fb9dc04e8 --- /dev/null +++ b/Examples/route-guide/Sources/Protos/route_guide @@ -0,0 +1 @@ +../../../../dev/protos/examples/route_guide \ No newline at end of file diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh index 0dc5cabb4..a4409f1d2 100755 --- a/dev/protos/generate.sh +++ b/dev/protos/generate.sh @@ -71,52 +71,6 @@ function invoke_protoc { "$protoc" "$@" } -#- EXAMPLES ------------------------------------------------------------------- - -function generate_echo_example { - local proto="$here/examples/echo/echo.proto" - local output="$root/Examples/echo/Sources/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" -} - -function generate_helloworld_example { - local proto="$here/upstream/grpc/examples/helloworld.proto" - local output="$root/Examples/hello-world/Sources/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" -} - -function generate_routeguide_example { - local proto="$here/examples/route_guide/route_guide.proto" - local output="$root/Examples/route-guide/Sources/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" -} - -function generate_error_details_example { - local proto="$here/upstream/grpc/examples/helloworld.proto" - local output="$root/Examples/error-details/Sources/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" -} - -function generate_reflection_server_example { - local proto="$here/examples/echo/echo.proto" - local output="$root/Examples/reflection-server/Sources/Generated" - local pb_output="$root/Examples/reflection-server/Sources/DescriptorSets/echo.pb" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - invoke_protoc --descriptor_set_out="$pb_output" "$proto" -I "$(dirname "$proto")" \ - --include_source_info \ - --include_imports -} - #- TESTS ---------------------------------------------------------------------- function generate_service_config_for_tests { @@ -135,12 +89,5 @@ function generate_service_config_for_tests { #------------------------------------------------------------------------------ -# Generate examples -generate_echo_example -generate_helloworld_example -generate_routeguide_example -generate_error_details_example -generate_reflection_server_example - # Tests generate_service_config_for_tests From e4f69cfd26c70366c52be57caf3a285d68376403 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 Jan 2025 10:33:46 +0000 Subject: [PATCH 544/580] Simplify code gen lib interface (#2169) Motivation: When reviewing APIs I noticed a few inconsistencies and some abstractions which are more complicated than they need to be. We should simplify this before we commit to a stable API. Modifications: - Replace the 'Name' type with a more concrete 'ServiceName' and 'MethodName'. They carry approximately equivalent information but the information is expressed in terms of what the names are used for (i.e. a function vs. what the expected format of the name is, e.g. lowercase). This makes it easier for users to understand how the names will be used and leaves room for more customisation in the future. - The service descriptor used two names: a namespace name and a service name. These have now been compressed into a single object (not all namespace name values were used). The namespace name was never used in isolation so this was adding unnecessary complexity. - The top-level 'SourceGenerator' type has been renamed 'CodeGenerator'. This is consistent with being a 'CodeGen' module and having a 'CodeGenerationRequest'. - The closures for returning code snippets to create a serializer/deserializer were renamed to make their user clearer. - Accidental public API was removed. Result: Slightly smaller and more consistent API. --- .../GRPCCodeGen/CodeGenerationRequest.swift | 210 ++++++++++++++---- ...rceGenerator.swift => CodeGenerator.swift} | 12 +- .../Internal/StructuredSwift+Client.swift | 14 +- .../Internal/StructuredSwift+Server.swift | 20 +- .../StructuredSwift+ServiceMetadata.swift | 18 +- .../Translator/ClientCodeTranslator.swift | 12 +- .../IDLToStructuredSwiftTranslator.swift | 56 ++--- .../Translator/ServerCodeTranslator.swift | 20 +- .../Translator/SpecializedTranslator.swift | 2 +- .../Internal/Translator/Translator.swift | 2 +- .../StructuredSwift+ClientTests.swift | 39 ++-- .../StructuredSwift+ImportTests.swift | 8 +- .../StructuredSwift+MetadataTests.swift | 4 +- .../StructuredSwift+ServerTests.swift | 12 +- ...lientCodeTranslatorSnippetBasedTests.swift | 9 +- ...uredSwiftTranslatorSnippetBasedTests.swift | 139 ++++++------ ...erverCodeTranslatorSnippetBasedTests.swift | 15 +- .../Internal/Translator/TestFunctions.swift | 4 +- ...TypealiasTranslatorSnippetBasedTests.swift | 13 +- 19 files changed, 365 insertions(+), 244 deletions(-) rename Sources/GRPCCodeGen/{SourceGenerator.swift => CodeGenerator.swift} (89%) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index d6d0f73d1..fc69c1bcf 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -44,11 +44,11 @@ public struct CodeGenerationRequest { /// /// For example, to serialize Protobuf messages you could specify a serializer as: /// ```swift - /// request.lookupSerializer = { messageType in + /// request.makeSerializerCodeSnippet = { messageType in /// "ProtobufSerializer<\(messageType)>()" /// } /// ``` - public var lookupSerializer: (_ messageType: String) -> String + public var makeSerializerCodeSnippet: (_ messageType: String) -> String /// Closure that receives a message type as a `String` and returns a code snippet to /// initialize a `MessageDeserializer` for that type as a `String`. @@ -58,26 +58,64 @@ public struct CodeGenerationRequest { /// /// For example, to serialize Protobuf messages you could specify a serializer as: /// ```swift - /// request.lookupDeserializer = { messageType in + /// request.makeDeserializerCodeSnippet = { messageType in /// "ProtobufDeserializer<\(messageType)>()" /// } /// ``` - public var lookupDeserializer: (_ messageType: String) -> String + public var makeDeserializerCodeSnippet: (_ messageType: String) -> String public init( fileName: String, leadingTrivia: String, dependencies: [Dependency], services: [ServiceDescriptor], - lookupSerializer: @escaping (String) -> String, - lookupDeserializer: @escaping (String) -> String + makeSerializerCodeSnippet: @escaping (_ messageType: String) -> String, + makeDeserializerCodeSnippet: @escaping (_ messageType: String) -> String ) { self.fileName = fileName self.leadingTrivia = leadingTrivia self.dependencies = dependencies self.services = services - self.lookupSerializer = lookupSerializer - self.lookupDeserializer = lookupDeserializer + self.makeSerializerCodeSnippet = makeSerializerCodeSnippet + self.makeDeserializerCodeSnippet = makeDeserializerCodeSnippet + } +} + +extension CodeGenerationRequest { + @available(*, deprecated, renamed: "makeSerializerSnippet") + public var lookupSerializer: (_ messageType: String) -> String { + get { self.makeSerializerCodeSnippet } + set { self.makeSerializerCodeSnippet = newValue } + } + + @available(*, deprecated, renamed: "makeDeserializerSnippet") + public var lookupDeserializer: (_ messageType: String) -> String { + get { self.makeDeserializerCodeSnippet } + set { self.makeDeserializerCodeSnippet = newValue } + } + + @available( + *, + deprecated, + renamed: + "init(fileName:leadingTrivia:dependencies:services:lookupSerializer:lookupDeserializer:)" + ) + public init( + fileName: String, + leadingTrivia: String, + dependencies: [Dependency], + services: [ServiceDescriptor], + lookupSerializer: @escaping (String) -> String, + lookupDeserializer: @escaping (String) -> String + ) { + self.init( + fileName: fileName, + leadingTrivia: leadingTrivia, + dependencies: dependencies, + services: services, + makeSerializerCodeSnippet: lookupSerializer, + makeDeserializerCodeSnippet: lookupDeserializer + ) } } @@ -88,7 +126,7 @@ public struct Dependency: Equatable { public var item: Item? /// The access level to be included in imports of this dependency. - public var accessLevel: SourceGenerator.Config.AccessLevel + public var accessLevel: CodeGenerator.Config.AccessLevel /// The name of the imported module or of the module an item is imported from. public var module: String @@ -107,7 +145,7 @@ public struct Dependency: Equatable { module: String, spi: String? = nil, preconcurrency: PreconcurrencyRequirement = .notRequired, - accessLevel: SourceGenerator.Config.AccessLevel + accessLevel: CodeGenerator.Config.AccessLevel ) { self.item = item self.module = module @@ -228,23 +266,27 @@ public struct ServiceDescriptor: Hashable { /// It is already formatted, meaning it contains "///" and new lines. public var documentation: String - /// The service name in different formats. - /// - /// All properties of this object must be unique for each service from within a namespace. - public var name: Name - - /// The service namespace in different formats. - /// - /// All different services from within the same namespace must have - /// the same ``Name`` object as this property. - /// For `.proto` files the base name of this object is the package name. - public var namespace: Name + /// The name of the service. + public var name: ServiceName /// A description of each method of a service. /// /// - SeeAlso: ``MethodDescriptor``. public var methods: [MethodDescriptor] + public init( + documentation: String, + name: ServiceName, + methods: [MethodDescriptor] + ) { + self.documentation = documentation + self.name = name + self.methods = methods + } +} + +extension ServiceDescriptor { + @available(*, deprecated, renamed: "init(documentation:name:methods:)") public init( documentation: String, name: Name, @@ -252,9 +294,25 @@ public struct ServiceDescriptor: Hashable { methods: [MethodDescriptor] ) { self.documentation = documentation - self.name = name - self.namespace = namespace self.methods = methods + + let identifier = namespace.base.isEmpty ? name.base : namespace.base + "." + name.base + + let typeName = + namespace.generatedUpperCase.isEmpty + ? name.generatedUpperCase + : namespace.generatedUpperCase + "_" + name.generatedUpperCase + + let propertyName = + namespace.generatedLowerCase.isEmpty + ? name.generatedUpperCase + : namespace.generatedLowerCase + "_" + name.generatedUpperCase + + self.name = ServiceName( + identifyingName: identifier, + typeName: typeName, + propertyName: propertyName + ) } } @@ -268,7 +326,7 @@ public struct MethodDescriptor: Hashable { /// /// All properties of this object must be unique for each method /// from within a service. - public var name: Name + public var name: MethodName /// Identifies if the method is input streaming. public var isInputStreaming: Bool @@ -284,7 +342,7 @@ public struct MethodDescriptor: Hashable { public init( documentation: String, - name: Name, + name: MethodName, isInputStreaming: Bool, isOutputStreaming: Bool, inputType: String, @@ -299,7 +357,94 @@ public struct MethodDescriptor: Hashable { } } +extension MethodDescriptor { + @available(*, deprecated, message: "Use MethodName instead of Name") + public init( + documentation: String, + name: Name, + isInputStreaming: Bool, + isOutputStreaming: Bool, + inputType: String, + outputType: String + ) { + self.documentation = documentation + self.name = MethodName( + identifyingName: name.base, + typeName: name.generatedUpperCase, + functionName: name.generatedLowerCase + ) + self.isInputStreaming = isInputStreaming + self.isOutputStreaming = isOutputStreaming + self.inputType = inputType + self.outputType = outputType + } +} + +public struct ServiceName: Hashable { + /// The identifying name as used in the service/method descriptors including any namespace. + /// + /// This value is also used to identify the service to the remote peer, usually as part of the + /// ":path" pseudoheader if doing gRPC over HTTP/2. + /// + /// If the service is declared in package "foo.bar" and the service is called "Baz" then this + /// value should be "foo.bar.Baz". + public var identifyingName: String + + /// The name as used on types including any namespace. + /// + /// This is used to generate a namespace for each service which contains a number of client and + /// server protocols and concrete types. + /// + /// If the service is declared in package "foo.bar" and the service is called "Baz" then this + /// value should be "Foo\_Bar\_Baz". + public var typeName: String + + /// The name as used as a property. + /// + /// This is used to provide a convenience getter for a descriptor of the service. + /// + /// If the service is declared in package "foo.bar" and the service is called "Baz" then this + /// value should be "foo\_bar\_Baz". + public var propertyName: String + + public init(identifyingName: String, typeName: String, propertyName: String) { + self.identifyingName = identifyingName + self.typeName = typeName + self.propertyName = propertyName + } +} + +public struct MethodName: Hashable { + /// The identifying name as used in the service/method descriptors. + /// + /// This value is also used to identify the method to the remote peer, usually as part of the + /// ":path" pseudoheader if doing gRPC over HTTP/2. + /// + /// This value typically starts with an uppercase character, for example "Get". + public var identifyingName: String + + /// The name as used on types including any namespace. + /// + /// This is used to generate a namespace for each method which contains information about + /// the method. + /// + /// This value typically starts with an uppercase character, for example "Get". + public var typeName: String + + /// The name as used as a property. + /// + /// This value typically starts with an lowercase character, for example "get". + public var functionName: String + + public init(identifyingName: String, typeName: String, functionName: String) { + self.identifyingName = identifyingName + self.typeName = typeName + self.functionName = functionName + } +} + /// Represents the name associated with a namespace, service or a method, in three different formats. +@available(*, deprecated, message: "Use ServiceName/MethodName instead.") public struct Name: Hashable { /// The base name is the name used for the namespace/service/method in the IDL file, so it should follow /// the specific casing of the IDL. @@ -327,6 +472,7 @@ public struct Name: Hashable { } } +@available(*, deprecated, message: "Use ServiceName/MethodName instead.") extension Name { /// The base name replacing occurrences of "." with "_". /// @@ -335,17 +481,3 @@ extension Name { return self.base.replacing(".", with: "_") } } - -extension ServiceDescriptor { - var namespacedServicePropertyName: String { - let prefix: String - - if self.namespace.normalizedBase.isEmpty { - prefix = "" - } else { - prefix = self.namespace.normalizedBase + "_" - } - - return prefix + self.name.normalizedBase - } -} diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/CodeGenerator.swift similarity index 89% rename from Sources/GRPCCodeGen/SourceGenerator.swift rename to Sources/GRPCCodeGen/CodeGenerator.swift index e8a92ec5f..adac1a6c9 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/CodeGenerator.swift @@ -14,8 +14,12 @@ * limitations under the License. */ -/// Creates a ``SourceFile`` containing the generated code for the RPCs represented in a ``CodeGenerationRequest`` object. -public struct SourceGenerator: Sendable { +@available(*, deprecated, renamed: "CodeGenerator") +public typealias SourceGenerator = CodeGenerator + +/// Generates ``SourceFile`` objects containing generated code for the RPCs represented +/// in a ``CodeGenerationRequest`` object. +public struct CodeGenerator: Sendable { /// The options regarding the access level, indentation for the generated code /// and whether to generate server and client code. public var config: Config @@ -79,8 +83,8 @@ public struct SourceGenerator: Sendable { } } - /// The function that transforms a ``CodeGenerationRequest`` object into a ``SourceFile`` object containing - /// the generated code, in accordance to the configurations set by the user for the ``SourceGenerator``. + /// Transforms a ``CodeGenerationRequest`` object into a ``SourceFile`` object containing + /// the generated code. public func generate( _ request: CodeGenerationRequest ) throws -> SourceFile { diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift index 4e9cc905b..511724702 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -213,7 +213,7 @@ extension ProtocolDescription { .preFormatted(docs(for: method)), .function( signature: .clientMethod( - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, @@ -256,7 +256,7 @@ extension ExtensionDescription { .function( .clientMethodWithDefaults( accessLevel: accessLevel, - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, @@ -506,7 +506,7 @@ extension ExtensionDescription { .function( .clientMethodExploded( accessLevel: accessLevel, - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, @@ -716,11 +716,11 @@ extension StructDescription { .function( .clientMethod( accessLevel: accessLevel, - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, serviceEnum: serviceEnum, - methodEnum: method.name.generatedUpperCase, + methodEnum: method.name.typeName, streamingInput: method.isInputStreaming, streamingOutput: method.isOutputStreaming ) @@ -735,7 +735,7 @@ private func docs( for method: MethodDescriptor, serializers includeSerializers: Bool = true ) -> String { - let summary = "/// Call the \"\(method.name.base)\" method." + let summary = "/// Call the \"\(method.name.identifyingName)\" method." let request: String if method.isInputStreaming { @@ -773,7 +773,7 @@ private func docs( } private func explodedDocs(for method: MethodDescriptor) -> String { - let summary = "/// Call the \"\(method.name.base)\" method." + let summary = "/// Call the \"\(method.name.identifyingName)\" method." var parameters = """ /// - Parameters: """ diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index 7af446a5e..a3525c13e 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -58,7 +58,7 @@ extension ProtocolDescription { ) -> Self { func docs(for method: MethodDescriptor) -> String { let summary = """ - /// Handle the "\(method.name.normalizedBase)" method. + /// Handle the "\(method.name.identifyingName)" method. """ let parameters = """ @@ -83,7 +83,7 @@ extension ProtocolDescription { .preFormatted(docs(for: method)), .function( signature: .serverMethod( - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: true, @@ -143,7 +143,7 @@ extension ProtocolDescription { ) -> Self { func docs(for method: MethodDescriptor) -> String { let summary = """ - /// Handle the "\(method.name.normalizedBase)" method. + /// Handle the "\(method.name.identifyingName)" method. """ let request: String @@ -182,7 +182,7 @@ extension ProtocolDescription { .preFormatted(docs(for: method)), .function( signature: .serverMethod( - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, @@ -329,8 +329,8 @@ extension FunctionDescription { .functionCall( .registerWithRouter( serviceNamespace: serviceNamespace, - methodNamespace: method.name.generatedUpperCase, - methodName: method.name.generatedLowerCase, + methodNamespace: method.name.typeName, + methodName: method.name.functionName, inputDeserializer: deserializer(method.inputType), outputSerializer: serializer(method.outputType) ) @@ -468,7 +468,7 @@ extension ExtensionDescription { return .function( .serverStreamingMethodsCallingMethod( accessLevel: accessModifier, - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, @@ -540,7 +540,7 @@ extension ProtocolDescription { ) -> Self { func docs(for method: MethodDescriptor) -> String { let summary = """ - /// Handle the "\(method.name.normalizedBase)" method. + /// Handle the "\(method.name.identifyingName)" method. """ let requestText = @@ -587,7 +587,7 @@ extension ProtocolDescription { .preFormatted(docs(for: method)), .function( signature: .simpleServerMethod( - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, @@ -754,7 +754,7 @@ extension ExtensionDescription { .function( .serviceProtocolDefaultImplementation( accessModifier: accessModifier, - name: method.name.generatedLowerCase, + name: method.name.functionName, input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index 2804eaab0..18502832a 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -236,12 +236,12 @@ extension EnumDescription { // Add a namespace for each method. let methodNamespaces: [Declaration] = methods.map { method in return .commentable( - .doc("Namespace for \"\(method.name.base)\" metadata."), + .doc("Namespace for \"\(method.name.typeName)\" metadata."), .enum( .methodNamespace( accessModifier: accessModifier, - name: method.name.base, - literalMethod: method.name.base, + name: method.name.typeName, + literalMethod: method.name.identifyingName, literalFullyQualifiedService: literalFullyQualifiedService, inputType: method.inputType, outputType: method.outputType @@ -254,7 +254,7 @@ extension EnumDescription { // Add an array of method descriptors let methodDescriptorsArray: VariableDescription = .methodDescriptorsArray( accessModifier: accessModifier, - methodNamespaceNames: methods.map { $0.name.base } + methodNamespaceNames: methods.map { $0.name.typeName } ) description.members.append( .commentable( @@ -329,14 +329,14 @@ extension [CodeBlock] { let serviceNamespace: EnumDescription = .serviceNamespace( accessModifier: accessModifier, - name: service.namespacedGeneratedName, - literalFullyQualifiedService: service.fullyQualifiedName, + name: service.name.typeName, + literalFullyQualifiedService: service.name.identifyingName, methods: service.methods ) blocks.append( CodeBlock( comment: .doc( - "Namespace containing generated types for the \"\(service.fullyQualifiedName)\" service." + "Namespace containing generated types for the \"\(service.name.identifyingName)\" service." ), item: .declaration(.enum(serviceNamespace)) ) @@ -344,8 +344,8 @@ extension [CodeBlock] { let descriptorExtension: ExtensionDescription = .serviceDescriptor( accessModifier: accessModifier, - propertyName: service.namespacedServicePropertyName, - literalFullyQualifiedService: service.fullyQualifiedName + propertyName: service.name.propertyName, + literalFullyQualifiedService: service.name.identifyingName ) blocks.append(CodeBlock(item: .declaration(.extension(descriptorExtension)))) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index b77cddbb9..611c09078 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -87,13 +87,13 @@ struct ClientCodeTranslator { var blocks = [CodeBlock]() let `extension` = ExtensionDescription( - onType: service.namespacedGeneratedName, + onType: service.name.typeName, declarations: [ // protocol ClientProtocol { ... } .commentable( .preFormatted( Docs.suffix( - self.clientProtocolDocs(serviceName: service.fullyQualifiedName), + self.clientProtocolDocs(serviceName: service.name.identifyingName), withDocs: service.documentation ) ), @@ -110,7 +110,7 @@ struct ClientCodeTranslator { .commentable( .preFormatted( Docs.suffix( - self.clientDocs(serviceName: service.fullyQualifiedName), + self.clientDocs(serviceName: service.name.identifyingName), withDocs: service.documentation ) ), @@ -118,7 +118,7 @@ struct ClientCodeTranslator { .client( accessLevel: accessModifier, name: "Client", - serviceEnum: service.namespacedGeneratedName, + serviceEnum: service.name.typeName, clientProtocol: "ClientProtocol", methods: service.methods ) @@ -130,7 +130,7 @@ struct ClientCodeTranslator { let extensionWithDefaults: ExtensionDescription = .clientMethodSignatureWithDefaults( accessLevel: accessModifier, - name: "\(service.namespacedGeneratedName).ClientProtocol", + name: "\(service.name.typeName).ClientProtocol", methods: service.methods, serializer: serializer, deserializer: deserializer @@ -144,7 +144,7 @@ struct ClientCodeTranslator { let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods( accessLevel: accessModifier, - on: "\(service.namespacedGeneratedName).ClientProtocol", + on: "\(service.name.typeName).ClientProtocol", methods: service.methods ) blocks.append( diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index eccc65ba3..2d3a7746e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -22,7 +22,7 @@ package struct IDLToStructuredSwiftTranslator: Translator { func translate( codeGenerationRequest: CodeGenerationRequest, - accessLevel: SourceGenerator.Config.AccessLevel, + accessLevel: CodeGenerator.Config.AccessLevel, accessLevelOnImports: Bool, client: Bool, server: Bool @@ -37,7 +37,7 @@ package struct IDLToStructuredSwiftTranslator: Translator { for service in codeGenerationRequest.services { codeBlocks.append( - CodeBlock(comment: .mark("\(service.fullyQualifiedName)", sectionBreak: true)) + CodeBlock(comment: .mark("\(service.name.identifyingName)", sectionBreak: true)) ) let metadata = metadataTranslator.translate( @@ -48,27 +48,27 @@ package struct IDLToStructuredSwiftTranslator: Translator { if server { codeBlocks.append( - CodeBlock(comment: .mark("\(service.fullyQualifiedName) (server)", sectionBreak: false)) + CodeBlock(comment: .mark("\(service.name.identifyingName) (server)", sectionBreak: false)) ) let blocks = serverTranslator.translate( accessModifier: accessModifier, service: service, - serializer: codeGenerationRequest.lookupSerializer, - deserializer: codeGenerationRequest.lookupDeserializer + serializer: codeGenerationRequest.makeSerializerCodeSnippet, + deserializer: codeGenerationRequest.makeDeserializerCodeSnippet ) codeBlocks.append(contentsOf: blocks) } if client { codeBlocks.append( - CodeBlock(comment: .mark("\(service.fullyQualifiedName) (client)", sectionBreak: false)) + CodeBlock(comment: .mark("\(service.name.identifyingName) (client)", sectionBreak: false)) ) let blocks = clientTranslator.translate( accessModifier: accessModifier, service: service, - serializer: codeGenerationRequest.lookupSerializer, - deserializer: codeGenerationRequest.lookupDeserializer + serializer: codeGenerationRequest.makeSerializerCodeSnippet, + deserializer: codeGenerationRequest.makeDeserializerCodeSnippet ) codeBlocks.append(contentsOf: blocks) } @@ -101,7 +101,7 @@ package struct IDLToStructuredSwiftTranslator: Translator { package func makeImports( dependencies: [Dependency], - accessLevel: SourceGenerator.Config.AccessLevel, + accessLevel: CodeGenerator.Config.AccessLevel, accessLevelOnImports: Bool ) throws -> [ImportDescription] { var imports: [ImportDescription] = [] @@ -125,7 +125,7 @@ package struct IDLToStructuredSwiftTranslator: Translator { } extension AccessModifier { - init(_ accessLevel: SourceGenerator.Config.AccessLevel) { + init(_ accessLevel: CodeGenerator.Config.AccessLevel) { switch accessLevel.level { case .internal: self = .internal case .package: self = .package @@ -173,7 +173,7 @@ extension IDLToStructuredSwiftTranslator { let servicesByGeneratedEnumName = Dictionary( grouping: codeGenerationRequest.services, - by: { $0.namespacedGeneratedName } + by: { $0.name.typeName } ) try self.checkServiceEnumNamesAreUnique(for: servicesByGeneratedEnumName) @@ -205,39 +205,39 @@ extension IDLToStructuredSwiftTranslator { ) throws { // Check that the method descriptors are unique, by checking that the base names // of the methods of a specific service are unique. - let baseNames = service.methods.map { $0.name.base } + let baseNames = service.methods.map { $0.name.identifyingName } if let duplicatedBase = baseNames.getFirstDuplicate() { throw CodeGenError( code: .nonUniqueMethodName, message: """ Methods of a service must have unique base names. \ - \(duplicatedBase) is used as a base name for multiple methods of the \(service.name.base) service. + \(duplicatedBase) is used as a base name for multiple methods of the \(service.name.identifyingName) service. """ ) } // Check that generated upper case names for methods are unique within a service, to ensure that // the enums containing type aliases for each method of a service. - let upperCaseNames = service.methods.map { $0.name.generatedUpperCase } + let upperCaseNames = service.methods.map { $0.name.typeName } if let duplicatedGeneratedUpperCase = upperCaseNames.getFirstDuplicate() { throw CodeGenError( code: .nonUniqueMethodName, message: """ Methods of a service must have unique generated upper case names. \ - \(duplicatedGeneratedUpperCase) is used as a generated upper case name for multiple methods of the \(service.name.base) service. + \(duplicatedGeneratedUpperCase) is used as a generated upper case name for multiple methods of the \(service.name.identifyingName) service. """ ) } // Check that generated lower case names for methods are unique within a service, to ensure that // the function declarations and definitions from the same protocols and extensions have unique names. - let lowerCaseNames = service.methods.map { $0.name.generatedLowerCase } + let lowerCaseNames = service.methods.map { $0.name.functionName } if let duplicatedLowerCase = lowerCaseNames.getFirstDuplicate() { throw CodeGenError( code: .nonUniqueMethodName, message: """ Methods of a service must have unique lower case names. \ - \(duplicatedLowerCase) is used as a signature name for multiple methods of the \(service.name.base) service. + \(duplicatedLowerCase) is used as a signature name for multiple methods of the \(service.name.identifyingName) service. """ ) } @@ -248,9 +248,7 @@ extension IDLToStructuredSwiftTranslator { ) throws { var descriptors: Set = [] for service in services { - let name = - service.namespace.base.isEmpty - ? service.name.base : "\(service.namespace.base).\(service.name.base)" + let name = service.name.identifyingName let (inserted, _) = descriptors.insert(name) if !inserted { throw CodeGenError( @@ -265,24 +263,6 @@ extension IDLToStructuredSwiftTranslator { } } -extension ServiceDescriptor { - var namespacedGeneratedName: String { - if self.namespace.generatedUpperCase.isEmpty { - return self.name.generatedUpperCase - } else { - return "\(self.namespace.generatedUpperCase)_\(self.name.generatedUpperCase)" - } - } - - var fullyQualifiedName: String { - if self.namespace.base.isEmpty { - return self.name.base - } else { - return "\(self.namespace.base).\(self.name.base)" - } - } -} - extension [String] { internal func getFirstDuplicate() -> String? { var seen = Set() diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index a2e91a83e..e563981e6 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -69,13 +69,13 @@ struct ServerCodeTranslator { var blocks = [CodeBlock]() let `extension` = ExtensionDescription( - onType: service.namespacedGeneratedName, + onType: service.name.typeName, declarations: [ // protocol StreamingServiceProtocol { ... } .commentable( .preFormatted( Docs.suffix( - self.streamingServiceDocs(serviceName: service.fullyQualifiedName), + self.streamingServiceDocs(serviceName: service.name.identifyingName), withDocs: service.documentation ) ), @@ -92,7 +92,7 @@ struct ServerCodeTranslator { .commentable( .preFormatted( Docs.suffix( - self.serviceDocs(serviceName: service.fullyQualifiedName), + self.serviceDocs(serviceName: service.name.identifyingName), withDocs: service.documentation ) ), @@ -100,7 +100,7 @@ struct ServerCodeTranslator { .service( accessLevel: accessModifier, name: "ServiceProtocol", - streamingProtocol: "\(service.namespacedGeneratedName).StreamingServiceProtocol", + streamingProtocol: "\(service.name.typeName).StreamingServiceProtocol", methods: service.methods ) ) @@ -110,7 +110,7 @@ struct ServerCodeTranslator { .commentable( .preFormatted( Docs.suffix( - self.simpleServiceDocs(serviceName: service.fullyQualifiedName), + self.simpleServiceDocs(serviceName: service.name.identifyingName), withDocs: service.documentation ) ), @@ -118,7 +118,7 @@ struct ServerCodeTranslator { .simpleServiceProtocol( accessModifier: accessModifier, name: "SimpleServiceProtocol", - serviceProtocol: "\(service.namespacedGeneratedName).ServiceProtocol", + serviceProtocol: "\(service.name.typeName).ServiceProtocol", methods: service.methods ) ) @@ -130,8 +130,8 @@ struct ServerCodeTranslator { // extension .StreamingServiceProtocol> { ... } let registerExtension: ExtensionDescription = .registrableRPCServiceDefaultImplementation( accessLevel: accessModifier, - on: "\(service.namespacedGeneratedName).StreamingServiceProtocol", - serviceNamespace: service.namespacedGeneratedName, + on: "\(service.name.typeName).StreamingServiceProtocol", + serviceNamespace: service.name.typeName, methods: service.methods, serializer: serializer, deserializer: deserializer @@ -147,7 +147,7 @@ struct ServerCodeTranslator { let streamingServiceDefaultImplExtension: ExtensionDescription = .streamingServiceProtocolDefaultImplementation( accessModifier: accessModifier, - on: "\(service.namespacedGeneratedName).ServiceProtocol", + on: "\(service.name.typeName).ServiceProtocol", methods: service.methods ) blocks.append( @@ -162,7 +162,7 @@ struct ServerCodeTranslator { // extension _SimpleServiceProtocol { ... } let serviceDefaultImplExtension: ExtensionDescription = .serviceProtocolDefaultImplementation( accessModifier: accessModifier, - on: "\(service.namespacedGeneratedName).SimpleServiceProtocol", + on: "\(service.name.typeName).SimpleServiceProtocol", methods: service.methods ) blocks.append( diff --git a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift index 1db3fce02..1ac3c4bee 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift @@ -19,7 +19,7 @@ protocol SpecializedTranslator { /// The ``SourceGenerator.Config.AccessLevel`` object used to represent the visibility level used in the generated code. - var accessLevel: SourceGenerator.Config.AccessLevel { get } + var accessLevel: CodeGenerator.Config.AccessLevel { get } /// Generates an array of ``CodeBlock`` elements that will be part of the ``StructuredSwiftRepresentation`` object /// created by the ``Translator``. diff --git a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift index 36b1c665f..9d0a043b0 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift @@ -29,7 +29,7 @@ protocol Translator { /// - Throws: An error if there are issues translating the codeGenerationRequest. func translate( codeGenerationRequest: CodeGenerationRequest, - accessLevel: SourceGenerator.Config.AccessLevel, + accessLevel: CodeGenerator.Config.AccessLevel, accessLevelOnImports: Bool, client: Bool, server: Bool diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift index 6404a751a..700a8b895 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -103,7 +103,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "/// Some docs", - name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), isInputStreaming: false, isOutputStreaming: false, inputType: "BarInput", @@ -187,7 +187,7 @@ extension StructuredSwiftTests { methods: [ MethodDescriptor( documentation: "", - name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), isInputStreaming: false, isOutputStreaming: false, inputType: "BarInput", @@ -342,7 +342,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "/// Some docs", - name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), isInputStreaming: false, isOutputStreaming: true, inputType: "Input", @@ -439,10 +439,10 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "/// Unary docs", - name: .init( - base: "Unary", - generatedUpperCase: "Unary", - generatedLowerCase: "unary" + name: MethodName( + identifyingName: "Unary", + typeName: "Unary", + functionName: "unary" ), isInputStreaming: false, isOutputStreaming: false, @@ -451,11 +451,12 @@ extension StructuredSwiftTests { ), .init( documentation: "/// ClientStreaming docs", - name: .init( - base: "ClientStreaming", - generatedUpperCase: "ClientStreaming", - generatedLowerCase: "clientStreaming" + name: MethodName( + identifyingName: "ClientStreaming", + typeName: "ClientStreaming", + functionName: "clientStreaming" ), + isInputStreaming: true, isOutputStreaming: false, inputType: "Input", @@ -463,10 +464,10 @@ extension StructuredSwiftTests { ), .init( documentation: "/// ServerStreaming docs", - name: .init( - base: "ServerStreaming", - generatedUpperCase: "ServerStreaming", - generatedLowerCase: "serverStreaming" + name: MethodName( + identifyingName: "ServerStreaming", + typeName: "ServerStreaming", + functionName: "serverStreaming" ), isInputStreaming: false, isOutputStreaming: true, @@ -475,10 +476,10 @@ extension StructuredSwiftTests { ), .init( documentation: "/// BidiStreaming docs", - name: .init( - base: "BidiStreaming", - generatedUpperCase: "BidiStreaming", - generatedLowerCase: "bidiStreaming" + name: MethodName( + identifyingName: "BidiStreaming", + typeName: "BidiStreaming", + functionName: "bidiStreaming" ), isInputStreaming: true, isOutputStreaming: true, diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift index ff1d34dbe..c079d5f5c 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift @@ -22,7 +22,7 @@ extension StructuredSwiftTests { struct Import { static let translator = IDLToStructuredSwiftTranslator() - static let allAccessLevels: [SourceGenerator.Config.AccessLevel] = [ + static let allAccessLevels: [CodeGenerator.Config.AccessLevel] = [ .internal, .public, .package, ] @@ -30,7 +30,7 @@ extension StructuredSwiftTests { "import rendering", arguments: allAccessLevels ) - func imports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + func imports(accessLevel: CodeGenerator.Config.AccessLevel) throws { var dependencies = [Dependency]() dependencies.append(Dependency(module: "Foo", accessLevel: .public)) dependencies.append( @@ -117,7 +117,7 @@ extension StructuredSwiftTests { "preconcurrency import rendering", arguments: allAccessLevels ) - func preconcurrencyImports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + func preconcurrencyImports(accessLevel: CodeGenerator.Config.AccessLevel) throws { var dependencies = [Dependency]() dependencies.append( Dependency( @@ -167,7 +167,7 @@ extension StructuredSwiftTests { "SPI import rendering", arguments: allAccessLevels ) - func spiImports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + func spiImports(accessLevel: CodeGenerator.Config.AccessLevel) throws { var dependencies = [Dependency]() dependencies.append( Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index 1b8d9afd2..06c87be43 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -143,7 +143,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "", - name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), isInputStreaming: false, isOutputStreaming: false, inputType: "FooInput", @@ -201,7 +201,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "", - name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), isInputStreaming: false, isOutputStreaming: false, inputType: "BarInput", diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index ba44a6896..7db009ce4 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -80,7 +80,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "/// Some docs", - name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), isInputStreaming: false, isOutputStreaming: false, inputType: "FooInput", @@ -123,7 +123,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "/// Some docs", - name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), isInputStreaming: false, isOutputStreaming: false, inputType: "FooInput", @@ -206,7 +206,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "", - name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), isInputStreaming: false, isOutputStreaming: false, inputType: "BarInput", @@ -321,7 +321,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "", - name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), isInputStreaming: false, isOutputStreaming: false, inputType: "FooInput", @@ -330,7 +330,7 @@ extension StructuredSwiftTests { // Will be ignored as a bidirectional streaming method. .init( documentation: "", - name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + name: MethodName(identifyingName: "Bar", typeName: "Bar", functionName: "bar"), isInputStreaming: true, isOutputStreaming: true, inputType: "BarInput", @@ -421,7 +421,7 @@ extension StructuredSwiftTests { methods: [ .init( documentation: "", - name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + name: MethodName(identifyingName: "Foo", typeName: "Foo", functionName: "foo"), isInputStreaming: false, isOutputStreaming: false, inputType: "Input", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 7d1d7504d..fd380c806 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -24,7 +24,7 @@ struct ClientCodeTranslatorSnippetBasedTests { func translate() { let method = MethodDescriptor( documentation: "/// Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + name: MethodName(identifyingName: "MethodA", typeName: "MethodA", functionName: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -33,8 +33,11 @@ struct ClientCodeTranslatorSnippetBasedTests { let service = ServiceDescriptor( documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), - namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), + name: ServiceName( + identifyingName: "namespaceA.ServiceA", + typeName: "NamespaceA_ServiceA", + propertyName: "" + ), methods: [method] ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index e765f52fe..1848df575 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -37,11 +37,10 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let serviceA = ServiceDescriptor( documentation: "/// Documentation for AService\n", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" + name: ServiceName( + identifyingName: "namespaceA.ServiceA", + typeName: "NamespaceA_ServiceA", + propertyName: "namespaceA_ServiceA" ), methods: [] ) @@ -161,7 +160,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { private func assertIDLToStructuredSwiftTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, - accessLevel: SourceGenerator.Config.AccessLevel, + accessLevel: CodeGenerator.Config.AccessLevel, server: Bool = false ) throws { let translator = IDLToStructuredSwiftTranslator() @@ -181,8 +180,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameNameServicesNoNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), + name: ServiceName( + identifyingName: "AService", + typeName: "AService", + propertyName: "aService" + ), methods: [] ) @@ -215,15 +217,21 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameDescriptorsServicesNoNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), + name: ServiceName( + identifyingName: "AService", + typeName: "AService", + propertyName: "aService" + ), methods: [] ) let serviceB = ServiceDescriptor( documentation: "Documentation for BService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), + name: ServiceName( + identifyingName: "AService", + typeName: "AService", + propertyName: "aService" + ), methods: [] ) @@ -254,11 +262,10 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameDescriptorsSameNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name( - base: "namespacea", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespacea" + name: ServiceName( + identifyingName: "namespacea.AService", + typeName: "NamespaceA_AService", + propertyName: "namespacea_aService" ), methods: [] ) @@ -292,21 +299,19 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameGeneratedNameServicesSameNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "/// Documentation for AService\n", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name( - base: "namespacea", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespacea" + name: ServiceName( + identifyingName: "namespacea.AService", + typeName: "NamespaceA_AService", + propertyName: "namespacea_aService" ), methods: [] ) let serviceB = ServiceDescriptor( documentation: "/// Documentation for BService\n", - name: Name(base: "BService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name( - base: "namespacea", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespacea" + name: ServiceName( + identifyingName: "namespacea.BService", + typeName: "NamespaceA_AService", + propertyName: "namespacea_aService" ), methods: [] ) @@ -322,8 +327,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { client: true, server: true ) - ) { - error in + ) { error in XCTAssertEqual( error as CodeGenError, CodeGenError( @@ -340,7 +344,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameBaseNameMethodsSameServiceError() throws { let methodA = MethodDescriptor( documentation: "Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + name: MethodName(identifyingName: "MethodA", typeName: "MethodA", functionName: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -348,11 +352,10 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name( - base: "namespacea", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespacea" + name: ServiceName( + identifyingName: "namespacea.AService", + typeName: "NamespaceA_AService", + propertyName: "namespacea_aService" ), methods: [methodA, methodA] ) @@ -368,15 +371,14 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { client: true, server: true ) - ) { - error in + ) { error in XCTAssertEqual( error as CodeGenError, CodeGenError( code: .nonUniqueMethodName, message: """ Methods of a service must have unique base names. \ - MethodA is used as a base name for multiple methods of the AService service. + MethodA is used as a base name for multiple methods of the namespacea.AService service. """ ) ) @@ -386,7 +388,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameGeneratedUpperCaseNameMethodsSameServiceError() throws { let methodA = MethodDescriptor( documentation: "Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + name: MethodName( + identifyingName: "MethodA", + typeName: "MethodA", + functionName: "methodA" + ), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -394,7 +400,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) let methodB = MethodDescriptor( documentation: "Documentation for MethodA", - name: Name(base: "MethodB", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + name: MethodName( + identifyingName: "MethodB", + typeName: "MethodA", + functionName: "methodA" + ), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -402,11 +412,10 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name( - base: "namespacea", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespacea" + name: ServiceName( + identifyingName: "namespacea.AService", + typeName: "NamespaceA_AService", + propertyName: "namespacea_AService" ), methods: [methodA, methodB] ) @@ -422,15 +431,15 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { client: true, server: true ) - ) { - error in + ) { error in XCTAssertEqual( error as CodeGenError, CodeGenError( code: .nonUniqueMethodName, message: """ Methods of a service must have unique generated upper case names. \ - MethodA is used as a generated upper case name for multiple methods of the AService service. + MethodA is used as a generated upper case name for multiple methods of the \ + namespacea.AService service. """ ) ) @@ -440,7 +449,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameLowerCaseNameMethodsSameServiceError() throws { let methodA = MethodDescriptor( documentation: "Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + name: MethodName(identifyingName: "MethodA", typeName: "MethodA", functionName: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -448,7 +457,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) let methodB = MethodDescriptor( documentation: "Documentation for MethodA", - name: Name(base: "MethodB", generatedUpperCase: "MethodB", generatedLowerCase: "methodA"), + name: MethodName(identifyingName: "MethodB", typeName: "MethodB", functionName: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -456,11 +465,10 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) let service = ServiceDescriptor( documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name( - base: "namespacea", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespacea" + name: ServiceName( + identifyingName: "namespacea.AService", + typeName: "NamespaceA_AService", + propertyName: "namespacea_aService" ), methods: [methodA, methodB] ) @@ -484,7 +492,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { code: .nonUniqueMethodName, message: """ Methods of a service must have unique lower case names. \ - methodA is used as a signature name for multiple methods of the AService service. + methodA is used as a signature name for multiple methods of the \ + namespacea.AService service. """ ) ) @@ -494,21 +503,19 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameGeneratedNameNoNamespaceServiceAndNamespaceError() throws { let serviceA = ServiceDescriptor( documentation: "Documentation for SameName service with no namespace", - name: Name( - base: "SameName", - generatedUpperCase: "SameName_BService", - generatedLowerCase: "sameName" + name: ServiceName( + identifyingName: "SameName", + typeName: "SameName_BService", + propertyName: "sameName" ), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), methods: [] ) let serviceB = ServiceDescriptor( documentation: "Documentation for BService", - name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bService"), - namespace: Name( - base: "sameName", - generatedUpperCase: "SameName", - generatedLowerCase: "sameName" + name: ServiceName( + identifyingName: "sameName.BService", + typeName: "SameName_BService", + propertyName: "sameName" ), methods: [] ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index b0475ce50..1b41acc1c 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -24,7 +24,7 @@ final class ServerCodeTranslatorSnippetBasedTests { func translate() { let method = MethodDescriptor( documentation: "/// Documentation for unaryMethod", - name: Name(base: "UnaryMethod", generatedUpperCase: "Unary", generatedLowerCase: "unary"), + name: MethodName(identifyingName: "UnaryMethod", typeName: "Unary", functionName: "unary"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -33,15 +33,10 @@ final class ServerCodeTranslatorSnippetBasedTests { let service = ServiceDescriptor( documentation: "/// Documentation for ServiceA", - name: Name( - base: "AlongNameForServiceA", - generatedUpperCase: "ServiceA", - generatedLowerCase: "serviceA" - ), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" + name: ServiceName( + identifyingName: "namespaceA.AlongNameForServiceA", + typeName: "NamespaceA_ServiceA", + propertyName: "namespaceA_serviceA" ), methods: [method] ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 24c7dd330..afd957638 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -80,10 +80,10 @@ internal func makeCodeGenerationRequest( leadingTrivia: "/// Some really exciting license header 2023.\n", dependencies: dependencies, services: services, - lookupSerializer: { + makeSerializerCodeSnippet: { "GRPCProtobuf.ProtobufSerializer<\($0)>()" }, - lookupDeserializer: { + makeDeserializerCodeSnippet: { "GRPCProtobuf.ProtobufDeserializer<\($0)>()" } ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 8713b784a..ba6d4d249 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -24,7 +24,7 @@ struct TypealiasTranslatorSnippetBasedTests { func testTypealiasTranslator() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), + name: MethodName(identifyingName: "MethodA", typeName: "MethodA", functionName: "methodA"), isInputStreaming: false, isOutputStreaming: false, inputType: "NamespaceA_ServiceARequest", @@ -32,11 +32,10 @@ struct TypealiasTranslatorSnippetBasedTests { ) let service = ServiceDescriptor( documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" + name: ServiceName( + identifyingName: "namespaceA.ServiceA", + typeName: "NamespaceA_ServiceA", + propertyName: "namespaceA_ServiceA" ), methods: [method] ) @@ -78,7 +77,7 @@ struct TypealiasTranslatorSnippetBasedTests { extension TypealiasTranslatorSnippetBasedTests { func render( - accessLevel: SourceGenerator.Config.AccessLevel, + accessLevel: CodeGenerator.Config.AccessLevel, service: ServiceDescriptor ) -> String { let translator = MetadataTranslator() From fa335250ff39dd0c8751d1658ddfd1dbc3b54af7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 23 Jan 2025 17:13:35 +0000 Subject: [PATCH 545/580] Document what constitutes public API (#2175) Motivation: SemVer packages should descibe what constitutes their API. Modifications: - Add a doc explaining what the public API is Result: More docs --- .../Documentation.docc/Articles/Public-API.md | 93 +++++++++++++++++++ .../Documentation.docc/Documentation.md | 1 + 2 files changed, 94 insertions(+) create mode 100644 Sources/GRPCCore/Documentation.docc/Articles/Public-API.md diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md b/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md new file mode 100644 index 000000000..6da3217ee --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md @@ -0,0 +1,93 @@ +# Public API + +Understand what constitutes the public API of gRPC Swift and the commitments +made by the maintainers. + +## Overview + +The gRPC Swift project uses [Semantic Versioning 2.0.0][0] which requires +projects to declare their public API. This document describes what is and isn't +part of the public API; commitments the maintainers make relating to the API; +and guidelines for users. + +For clarity the project is comprised of the following Swift packages: + +- [grpc/grpc-swift][1], +- [grpc/grpc-swift-nio-transport][2], +- [grpc/grpc-swift-protobuf][3], and +- [grpc/grpc-swift-extras][4]. + +## What _is_ and _isn't_ public API + +### Library targets + +All library targets made available as package products as considered to be +public API. Examples of these include `GRPCCore` and `GRPCProtobuf`. + +> Exceptions: +> Targets with names starting with an underscore (`_`) aren't public API. + +### Symbols + +All publicly exposed symbols (i.e. symbols which are declared as `public`) +within public library targets or those which are re-exported from non-public +targets are part of the public API. Examples include `Metadata`, +`ServiceConfig`, and `GRPCServer`. + +> Exceptions: +> - Symbols starting with an underscore (`_`), for example `_someFunction()` and +> `_AnotherType` aren't public API. +> - Initializers where the first character of the first parameter label is an +> underscore, for example `init(_foo:)` aren't public API. + +### Configuration and inputs + +Any configuration, input, and interfaces to executable products which have +inputs (such as command line arguments, or configuration files) are considered +to be public API. Examples of these include the configuration file passed to the +Swift Package Manager build plugin for generating stubs provided by +[grpc-swift-protobuf][3]. + +> Exceptions: +> - Executable _targets_ which aren't exposed as executable _products_. + +## Commitments made by the maintainers + +Without releasing a new major version, the gRPC Swift maintainers commit to not +adding any new types to the global namespace without a "GRPC" prefix. + +To illustrate this, the maintainers may: +1. Add a new type to an existing module called `GRPCPanCakes` but will not add a + new type called `PanCakes` to an existing module. +2. Add a new top-level function to an existing module called `grpcRun()` but + won't add a new top-level function called `run()`. +3. Add a new module called `GRPCFoo`. Any symbols added to the new module at the + point the module becomes API aren't required to have a "GRPC" prefix; symbols + added after that point will be prefixed as required by (1) and (2). + +This allows the project to follow Semantic versioning without breaking adopter +code in minor and patch releases. + +## Guidelines for users + +In order to not have your code broken by a gRPC Swift update you should only use +the public API as described above. There are a number of other guidelines you +should follow as well: + +1. You _may_ conform your own types to protocols provided by gRPC Swift. +2. You _may_ conform types provided by gRPC Swift to your own protocols. +3. You _mustn't_ conform types provided by gRPC Swift to protocols that you + don't own, and you mustn't conform types you don't own to protocols provided + by gRPC Swift. +4. You _may_ extend types provided by gRPC Swift at `package`, `internal`, + `private` or `fileprivate` level. +5. You _may_ extend types provided by gRPC Swift at `public` access level if + doing so means that a symbol clash is impossible (such as including a type + you own in the signature, or prefixing the method with the namespace of your + package in much the same way that gRPC Swift will prefix new symbols). + +[0]: https://semver.org +[1]: https://github.com/grpc/grpc-swift +[2]: https://github.com/grpc/grpc-swift-nio-transport +[3]: https://github.com/grpc/grpc-swift-protobuf +[4]: https://github.com/grpc/grpc-swift-extras diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 8a35bf9d9..b885c72e8 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -57,6 +57,7 @@ as tutorials. ### Project Information - +- ### Getting involved From f7261adb6a7ed32b0d17d8ea8aa6dde87266037e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Jan 2025 07:06:38 +0000 Subject: [PATCH 546/580] Fix typos/punctuation (#2177) Motivation: Some comments on #2175 were missed becuase automerge was enabled and the PR got approved. Modification: Fix typos/punctuation. Result: Better docs. --- Sources/GRPCCore/Documentation.docc/Articles/Public-API.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md b/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md index 6da3217ee..5a24ad28a 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Public-API.md @@ -7,10 +7,10 @@ made by the maintainers. The gRPC Swift project uses [Semantic Versioning 2.0.0][0] which requires projects to declare their public API. This document describes what is and isn't -part of the public API; commitments the maintainers make relating to the API; +part of the public API; commitments the maintainers make relating to the API, and guidelines for users. -For clarity the project is comprised of the following Swift packages: +For clarity, the project is comprised of the following Swift packages: - [grpc/grpc-swift][1], - [grpc/grpc-swift-nio-transport][2], @@ -21,7 +21,7 @@ For clarity the project is comprised of the following Swift packages: ### Library targets -All library targets made available as package products as considered to be +All library targets made available as package products are considered to be public API. Examples of these include `GRPCCore` and `GRPCProtobuf`. > Exceptions: From 4c6357d83d9cfb1f4d993570b5cba45438ebcbbe Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Jan 2025 10:56:14 +0000 Subject: [PATCH 547/580] Update tutorials to use the build plugin (#2178) Motivation: The examples have been updated to use the build plugin; so too should the tutorials. Modifications: Update tutorials to use the build plugin. Result: Better tutorials. --- .../Hello-World/Hello-World.tutorial | 44 +++++---- .../route-guide-sec01-step09-plugin.swift | 25 +++++ ...oute-guide-sec01-step10-plugin-config.json | 7 ++ .../route-guide-sec05-step00-package.swift | 4 + .../route-guide-sec05-step01-package.swift | 3 + .../route-guide-sec05-step02-package.swift | 3 + .../Route-Guide/Route-Guide.tutorial | 92 +++++++++++-------- 7 files changed, 124 insertions(+), 54 deletions(-) create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step10-plugin-config.json diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 4818c18ae..3a2c60697 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -28,17 +28,28 @@ @Section(title: "Run a gRPC application") { Let's start by running the existing Greeter application. + As a prerequisite you must have the Protocol Buffers compiler (`protoc`) installed. You can + find the instructions for doing this in the [gRPC Swift Protobuf + documentation](https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc). + The remainder of this tutorial assumes you installed `protoc` and it's available in + your `$PATH`. + + You may notice that the `swift` commands are all prefixed with `PROTOC_PATH=$(which protoc)`, + this is to let the build system know where `protoc` is located so that it can generate stubs + for you. You can read more about it in the [gRPC Swift Protobuf + documentation](https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs). + @Steps { @Step { - In a terminal run `swift run hello-world serve` to start the server. By default it'll start - listening on port 31415. + In a terminal run `PROTOC_PATH=$(which protoc) swift run hello-world serve` to start the + server. By default it'll start listening on port 31415. @Code(name: "Console.txt", file: "hello-world-sec02-step01.txt") } @Step { - In another terminal run `swift run hello-world greet` to create a client, connect - to the server you started and send it a request and print the response. + In another terminal run `PROTOC_PATH=$(which protoc) swift run hello-world greet` to create + a client, connect to the server you started and send it a request and print the response. @Code(name: "Console.txt", file: "hello-world-sec02-step02.txt") } @@ -75,19 +86,19 @@ } @Section(title: "Update and run the application") { - You need to regenerate the stubs as the service definition has changed. To do this run the - following command from the _root of the checked out repository_: - - ```console - dev/protos/generate.sh - ``` + You need to regenerate the stubs as the service definition has changed. As you're using + the Swift Package Manager Build Plugin for gRPC Swift the gRPC code is automatically + generated if necessary when you build the example. You can learn more about generating stubs in + the article. - To learn how to generate stubs check out the article. - - Now that the stubs have been updated you need to implement and call the new method in the - human-written parts of your application. @Steps { + @Step { + Run `PROTOC_PATH=$(which protoc) swift build` to build the example. This will fail + because the service no longer implements all of the methods declared in the `.proto` file. + Let's fix that! + } + @Step { Open `Serve.swift` in the `Subcommands` directory. @@ -114,13 +125,14 @@ @Step { Just like we did before, open a terminal and start the server by - running `swift run hello-world serve` + running `PROTOC_PATH=$(which protoc) swift run hello-world serve` @Code(name: "Console.txt", file: "hello-world-sec04-step05.txt") } @Step { - In a separate terminal run `swift run hello-world greet` to call the server. + In a separate terminal run `PROTOC_PATH=$(which protoc) swift run hello-world greet` to + call the server. @Code(name: "Console.txt", file: "hello-world-sec04-step06.txt") } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift new file mode 100644 index 000000000..6db99da7f --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "RouteGuide", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + ], + targets: [ + .executableTarget( + name: "RouteGuide", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step10-plugin-config.json b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step10-plugin-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step10-plugin-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index 6aaa5f7a7..6db99da7f 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -7,6 +7,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), ], targets: [ .executableTarget( @@ -15,6 +16,9 @@ let package = Package( .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index a450a82e4..27ee4f7f3 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -19,6 +19,9 @@ let package = Package( ], resources: [ .copy("route_guide_db.json") + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index b250e8d82..49f69af0c 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -21,6 +21,9 @@ let package = Package( ], resources: [ .copy("route_guide_db.json") + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") ] ) ] diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial index d5746c1b2..1e62f5120 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -20,6 +20,17 @@ Before we can write any code we need to create a new Swift Package and configure it to depend on gRPC Swift. + As a prerequisite you must have the Protocol Buffers compiler (`protoc`) installed. You can + find the instructions for doing this in the [gRPC Swift Protobuf + documentation](https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc). + The remainder of this tutorial assumes you installed `protoc` and it's available in + your `$PATH`. + + You may notice that the `swift` commands are all prefixed with `PROTOC_PATH=$(which protoc)`, + this is to let the build system know where `protoc` is located so that it can generate stubs + for you. You can read more about it in the [gRPC Swift Protobuf + documentation](https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs). + @Steps { @Step { Create a new directory called for the package called `RouteGuide`. @@ -84,6 +95,24 @@ @Code(name: "Package.swift", file: "route-guide-sec01-step08-description.swift") } + + @Step { + We'll also add a build plugin. This allows the build system to generate gRPC code at build + time rather than having to generate it with separate tooling. + + @Code(name: "Package.swift", file: "route-guide-sec01-step09-plugin.swift") + } + + @Step { + A configuration file is required so that the plugin knows what to generate. Create + a JSON file in the `Sources/Protos` directory called `grpc-swift-proto-generator-config.json` + with this content. + + The name of the file (`grpc-swift-proto-generator-config.json`) is important: the plugin + looks for files matching this name in the source directory of your target. + + @Code(name: "Sources/Protos/grpc-swift-proto-generator-config.json", file: "route-guide-sec01-step10-plugin-config.json") + } } } @@ -93,16 +122,26 @@ @Steps { @Step { - Create a new empty file in the `Protos` directory called `route_guide.proto`. We'll use - the "proto3" syntax and our service will be part of the "routeguide" package. + Create a new directory in the `Sources/Protos` directory called `routeguide` + using `mkdir Sources/Protos/routeguide`. + } + + @Step { + Create a new empty file in the `Sources/Protos/routeguide` directory + called `route_guide.proto`. We'll use the "proto3" syntax and our service will be part of + the "routeguide" package. - @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step01-import.proto") + It's good practice to organize your `.proto` files according to the package they are + declared in, that's why we created the `routeguide` directory to match the "routeguide" + package name. + + @Code(name: "Sources/Protos/routeguide/route_guide.proto", file: "route-guide-sec02-step01-import.proto") } @Step { To define a service we create a named `service` in the `.proto` file. - @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step02-service.proto") + @Code(name: "Sources/Protos/routeguide/route_guide.proto", file: "route-guide-sec02-step02-service.proto") } @Step { @@ -113,7 +152,7 @@ A *unary RPC* where the client sends a request to the server using the stub and waits for a response to come back, just like a normal function call. - @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step03-unary.proto") + @Code(name: "Sources/Protos/routeguide/route_guide.proto", file: "route-guide-sec02-step03-unary.proto") } @Step { @@ -123,7 +162,7 @@ example, you specify a server-side streaming method by placing the `stream` keyword before the *response* type. - @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step04-server-streaming.proto") + @Code(name: "Sources/Protos/routeguide/route_guide.proto", file: "route-guide-sec02-step04-server-streaming.proto") } @Step { @@ -133,7 +172,7 @@ and return its response. You specify a client-side streaming method by placing the `stream` keyword before the *request* type. - @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step05-client-streaming.proto") + @Code(name: "Sources/Protos/routeguide/route_guide.proto", file: "route-guide-sec02-step05-client-streaming.proto") } @Step { @@ -146,53 +185,30 @@ stream is preserved. You specify this type of method by placing the `stream` keyword before both the request and the response. - @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step06-bidi-streaming.proto") + @Code(name: "Sources/Protos/routeguide/route_guide.proto", file: "route-guide-sec02-step06-bidi-streaming.proto") } @Step { The `.proto` file also contains the Protocol Buffers message type definitions for all request and response messages used by the service. - @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step07-messages.proto") + @Code(name: "Sources/Protos/routeguide/route_guide.proto", file: "route-guide-sec02-step07-messages.proto") } } } @Section(title: "Generating client and server code") { Next we need to generate the gRPC client and server interfaces from our `.proto` - service definition. We do this using the Protocol Buffer compiler, `protoc`, with - two plugins: one with support for Swift (via [Swift Protobuf](https://github.com/apple/swift-protobuf)) - and the other for gRPC. This section assumes you already have `protoc` installed. + service definition. As we're using the build plugin we just need to build our project. To learn more about generating code check out the article. @Steps { @Step { - First we need to build the two plugins for `protoc`, `protoc-gen-swift` and - `protoc-gen-grpc-swift`. - - @Code(name: "Console", file: "route-guide-sec03-step01-protoc-plugins.txt") - } - - @Step { - We'll generate the code into a separate directory within `Sources` called `Generated` which - we need to create first. - - @Code(name: "Console", file: "route-guide-sec03-step02-mkdir.txt") - } - - @Step { - Now run `protoc` to generate the messages. This will create - `Sources/Generated/route_guide.pb.swift`. - - @Code(name: "Console", file: "route-guide-sec03-step03-gen-messages.txt") - } - - @Step { - Run `protoc` again to generate the service code. This will create - `Sources/Generated/route_guide.grpc.swift`. + Build the project using `PROTOC_PATH=$(which protoc) swift build`. - @Code(name: "Console", file: "route-guide-sec03-step04-gen-grpc.txt") + If you are using Xcode or another IDE then you'll need to set the environment variable + appropriately. } } } @@ -464,13 +480,13 @@ @Steps { @Step { - In one terminal run `swift run RouteGuide --server` to start the server. + In one terminal run `PROTOC_PATH=$(which protoc) swift run RouteGuide --server` to start the server. @Code(name: "Console", file: "route-guide-sec07-step01-server.txt") } @Step { - In another terminal run `swift run RouteGuide` to run the client program. + In another terminal run `PROTOC_PATH=$(which protoc) swift run RouteGuide` to run the client program. @Code(name: "Console", file: "route-guide-sec07-step02-client.txt") } From bfe033fbdd1436dd91b1d231f3687e701205bb28 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Jan 2025 11:58:28 +0000 Subject: [PATCH 548/580] Update Example READMEs (#2179) Motivation: The examples now use the Swift PM build plugin but the READMEs weren't updated to include the PROTOC_PATH. Modifications: - update READMEs Result: Better examples --- Examples/echo/README.md | 20 ++++++++++++++++---- Examples/error-details/README.md | 14 +++++++++++++- Examples/hello-world/README.md | 18 +++++++++++++++--- Examples/reflection-server/README.md | 12 ++++++++++++ Examples/route-guide/README.md | 16 ++++++++++++++-- 5 files changed, 70 insertions(+), 10 deletions(-) diff --git a/Examples/echo/README.md b/Examples/echo/README.md index bea921190..da5049731 100644 --- a/Examples/echo/README.md +++ b/Examples/echo/README.md @@ -12,19 +12,28 @@ the four RPC types. The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) HTTP/2 transport. +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + ## Usage Build and run the server using the CLI: ```console -$ swift run echo serve +$ PROTOC_PATH=$(which protoc) swift run echo serve Echo listening on [ipv4]127.0.0.1:1234 ``` Use the CLI to make a unary 'Get' request against it: ```console -$ swift run echo get --message "Hello" +$ PROTOC_PATH=$(which protoc) swift run echo get --message "Hello" get → Hello get ← Hello ``` @@ -32,7 +41,7 @@ get ← Hello Use the CLI to make a bidirectional streaming 'Update' request: ```console -$ swift run echo update --message "Hello World" +$ PROTOC_PATH=$(which protoc) swift run echo update --message "Hello World" update → Hello update → World update ← Hello @@ -42,5 +51,8 @@ update ← World Get help with the CLI by running: ```console -$ swift run echo --help +$ PROTOC_PATH=$(which protoc) swift run echo --help ``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Examples/error-details/README.md b/Examples/error-details/README.md index 6eeaa35e4..6dc09059e 100644 --- a/Examples/error-details/README.md +++ b/Examples/error-details/README.md @@ -10,12 +10,21 @@ described in more detailed in the [gRPC Error Guide](https://grpc.io/docs/guides/error/) and is made available via the [grpc-swift-protobuf](https://github.com/grpc-swift-protobuf) package. +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + ## Usage Build and run the example using the CLI: ```console -$ swift run +$ PROTOC_PATH=$(which protoc) swift run Error code: resourceExhausted Error message: The greeter has temporarily run out of greetings. Error details: @@ -24,3 +33,6 @@ Error details: - Help links: - https://en.wikipedia.org/wiki/Caffeine (A Wikipedia page about caffeine including its properties and effects.) ``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Examples/hello-world/README.md b/Examples/hello-world/README.md index 9a1ff32ce..32fa4ca5a 100644 --- a/Examples/hello-world/README.md +++ b/Examples/hello-world/README.md @@ -10,25 +10,37 @@ service which allows you to start a server and to make requests against it. The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) HTTP/2 transport. +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + ## Usage Build and run the server using the CLI: ```console -$ swift run hello-world serve +$ PROTOC_PATH=$(which protoc) swift run hello-world serve Greeter listening on [ipv4]127.0.0.1:31415 ``` Use the CLI to send a request to the service: ```console -$ swift run hello-world greet +$ PROTOC_PATH=$(which protoc) swift run hello-world greet Hello, stranger ``` Send the name of the greetee in the request by specifying a `--name`: ```console -$ swift run hello-world greet --name "PanCakes 🐶" +$ PROTOC_PATH=$(which protoc) swift run hello-world greet --name "PanCakes 🐶" Hello, PanCakes 🐶 ``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Examples/reflection-server/README.md b/Examples/reflection-server/README.md index 1743a8a08..0954d844d 100644 --- a/Examples/reflection-server/README.md +++ b/Examples/reflection-server/README.md @@ -23,6 +23,15 @@ protoc --descriptor_set_out=path/to/output.pb path/to/input.proto \ --include_imports ``` +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + ## Usage Build and run the server using the CLI: @@ -59,3 +68,6 @@ $ grpcurl -plaintext -d '{ "text": "Hello" }' 127.0.0.1:31415 echo.Echo.Get "text": "Hello" } ``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Examples/route-guide/README.md b/Examples/route-guide/README.md index d19a53c87..8a06ed735 100644 --- a/Examples/route-guide/README.md +++ b/Examples/route-guide/README.md @@ -15,19 +15,28 @@ HTTP/2 transport. This example has an accompanying tutorial hosted on the [Swift Package Index](https://swiftpackageindex.com/grpc/grpc-swift/main/tutorials/grpccore/route-guide). +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + ## Usage Build and run the server using the CLI: ```console -$ swift run route-guide serve +$ PROTOC_PATH=$(which protoc) swift run route-guide serve server listening on [ipv4]127.0.0.1:31415 ``` Use the CLI to interrogate the different RPCs you can call: ```console -$ swift run route-guide --help +$ PROTOC_PATH=$(which protoc) swift run route-guide --help USAGE: route-guide OPTIONS: @@ -42,3 +51,6 @@ SUBCOMMANDS: See 'route-guide help ' for detailed help. ``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs From 5f21fc9882856511b7566117de9eea2e69efea61 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 24 Jan 2025 13:55:03 +0000 Subject: [PATCH 549/580] Document use of the build plugin (#2176) ### Motivation: To ease use of the `grpc-swift-protobuf` Swift Package Manager build plugin. ### Modifications: Documentation added. ### Result: Greater understanding? --------- Co-authored-by: George Barnett --- .../Articles/Generating-stubs.md | 100 ++++++++++++++++-- 1 file changed, 92 insertions(+), 8 deletions(-) diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md index 80c983184..88c2108a9 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md @@ -4,25 +4,109 @@ Learn how to generate stubs for gRPC Swift from a service defined using the Prot ## Overview -### Using protoc - If you've used Protocol Buffers before then generating gRPC Swift stubs should be simple. If you're unfamiliar with Protocol Buffers then you should get comfortable with the concepts before continuing; the [Protocol Buffers website](https://protobuf.dev/) is a great place to start. -The [`grpc-swift-protobuf`](https://github.com/grpc/grpc-swift-protobuf) package provides -`protoc-gen-grpc-swift`, a program which is a plugin for the Protocol Buffers compiler, `protoc`. +You can use the `protoc` plugin from the command line directly, or you can make use of a + [Swift Package Manager build plugin](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) +convenience which adds the stub generation to the Swift build graph. +You may use the build plugin either from the command line or from Xcode. + +## Using the build plugin + +The build plugin (`GRPCProtobufGenerator`) is a great choice for convenient dynamic code generation, however it does come with some limitations. +Because it generates the gRPC Swift stubs as part of the build it has the requirement that `protoc` must be available +at compile time. This requirement means it is not a good fit for library authors who do not have +direct control over this. + +The build plugin detects `.proto` files in the source tree and invokes `protoc` once for each file +(caching results and performing the generation as necessary). + +### Adoption +You must adopt Swift Package Manager build plugins on a per-target basis by modifying your package manifest +(`Package.swift` file). To do this, declare the grpc-swift-protobuf package as a dependency and add the plugin +to your desired targets. + +For example, to make use of the plugin for generating gRPC Swift stubs as part of the +`echo-server` target: +```swift +targets: [ + .executableTarget( + name: "echo-server", + dependencies: [ + // ... + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +``` +Once this is done you need to ensure that the `.proto` files to be used for generation +are included in the target's source directory and that you have defined at least one configuration file. + +### Configuration + +You must provide a configuration file in the directory which encloses all `.proto` files (in the same directory or a parent). +Configuration files, written in JSON, tell the build plugin about the options used for `protoc` invocations. +You must name the file `grpc-swift-proto-generator-config.json` and structure it in the following format: +```json +{ + "generate": { + "clients": true, + "servers": true, + "messages": true, + }, + "generatedSource": { + "accessLevelOnImports": false, + "accessLevel": "internal", + } + "protoc": { + "executablePath": "/opt/homebrew/bin/protoc" + "importPaths": [ + "../directory_1", + ], + }, +} +``` -> `protoc-gen-grpc-swift` only generates gRPC stubs, it doesn't generate messages. You must use -> `protoc-gen-swift` to generate messages in addition to gRPC Stubs. +The options do not need to be specified and each have default values. + +| Name | Possible Values | Default | Description | +|----------------------------------------|--------------------------------------------|--------------|-----------------------------------------------------| +| `generate.servers` | `true`, `false` | `true` | Generate server stubs | +| `generate.clients` | `true`, `false` | `true` | Generate client stubs | +| `generate.messages` | `true`, `false` | `true` | Generate message stubs | +| `generatedSource.accessLevelOnImports` | `true`, `false` | `false` | Whether imports should have explicit access levels | +| `generatedSource.accessLevel` | `"public"`, `"package"`, `"internal"` | `"internal"` | Access level for generated stubs | +| `protoc.executablePath` | N/A | `null`† | Path to the `protoc` executable | +| `protoc.importPaths` | N/A | `null`‡ | Import paths passed to `protoc` | + +† The Swift Package Manager build plugin infrastructure will attempt to discover the executable's location if you don't provide one. -To generate gRPC stubs for your `.proto` files you must run the `protoc` command with +‡ If you don't provide any import paths then the path to the configuration file will be used on a per-source-file basis. + +Many of these options map to `protoc-gen-grpc-swift` and `protoc-gen-swift` options. + +If you require greater flexibility you may specify more than one configuration file. +Configuration files apply to all `.proto` files equal to or below it in the file hierarchy. A configuration file +lower in the file hierarchy supersedes one above it. + +### Using protoc + +The [`grpc-swift-protobuf`](https://github.com/grpc/grpc-swift-protobuf) package provides +`protoc-gen-grpc-swift`, a program which is a plugin for the Protocol Buffers compiler, `protoc`. +To generate gRPC stubs for your `.proto` files directly you must run the `protoc` command with the `--grpc-swift_out=` option: ```console protoc --grpc-swift_out=. my-service.proto ``` +> `protoc-gen-grpc-swift` only generates gRPC stubs, it doesn't generate messages. You must use +> `protoc-gen-swift` to generate messages in addition to gRPC Stubs. + The presence of `--grpc-swift_out` tells `protoc` to use the `protoc-gen-grpc-swift` plugin. By default it'll look for the plugin in your `PATH`. You can also specify the path to the plugin explicitly: @@ -69,7 +153,7 @@ allows you to specify a mapping from `.proto` files to the Swift module they are allows the code generator to add appropriate imports to your generated stubs. This is described in more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md). -#### Building the plugin +#### Building the protoc plugin > The version of `protoc-gen-grpc-swift` you use mustn't be newer than the version of > the `grpc-swift-protobuf` you're using. From 6197e77adae3aae68debdc8db07c3ffb3279932e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Jan 2025 14:20:04 +0000 Subject: [PATCH 550/580] Move the Generating-stubs article to grpc-swift-protobuf (#2180) Motivation: It makes more sense for it to live there. Modifications: - Delete the contents and point to its new home on the Swift Package Index. Result: Docs live in a more sensible place. --- .../Articles/Generating-stubs.md | 167 +----------------- 1 file changed, 3 insertions(+), 164 deletions(-) diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md index 88c2108a9..d47ab1997 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md @@ -1,169 +1,8 @@ -# Generating stubs +# Generating stubs with gRPC Swift Protobuf Learn how to generate stubs for gRPC Swift from a service defined using the Protocol Buffers IDL. ## Overview -If you've used Protocol Buffers before then generating gRPC Swift stubs should be simple. If you're -unfamiliar with Protocol Buffers then you should get comfortable with the concepts before -continuing; the [Protocol Buffers website](https://protobuf.dev/) is a great place to start. - -You can use the `protoc` plugin from the command line directly, or you can make use of a - [Swift Package Manager build plugin](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) -convenience which adds the stub generation to the Swift build graph. -You may use the build plugin either from the command line or from Xcode. - -## Using the build plugin - -The build plugin (`GRPCProtobufGenerator`) is a great choice for convenient dynamic code generation, however it does come with some limitations. -Because it generates the gRPC Swift stubs as part of the build it has the requirement that `protoc` must be available -at compile time. This requirement means it is not a good fit for library authors who do not have -direct control over this. - -The build plugin detects `.proto` files in the source tree and invokes `protoc` once for each file -(caching results and performing the generation as necessary). - -### Adoption -You must adopt Swift Package Manager build plugins on a per-target basis by modifying your package manifest -(`Package.swift` file). To do this, declare the grpc-swift-protobuf package as a dependency and add the plugin -to your desired targets. - -For example, to make use of the plugin for generating gRPC Swift stubs as part of the -`echo-server` target: -```swift -targets: [ - .executableTarget( - name: "echo-server", - dependencies: [ - // ... - ], - plugins: [ - .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") - ] - ) - ] -``` -Once this is done you need to ensure that the `.proto` files to be used for generation -are included in the target's source directory and that you have defined at least one configuration file. - -### Configuration - -You must provide a configuration file in the directory which encloses all `.proto` files (in the same directory or a parent). -Configuration files, written in JSON, tell the build plugin about the options used for `protoc` invocations. -You must name the file `grpc-swift-proto-generator-config.json` and structure it in the following format: -```json -{ - "generate": { - "clients": true, - "servers": true, - "messages": true, - }, - "generatedSource": { - "accessLevelOnImports": false, - "accessLevel": "internal", - } - "protoc": { - "executablePath": "/opt/homebrew/bin/protoc" - "importPaths": [ - "../directory_1", - ], - }, -} -``` - -The options do not need to be specified and each have default values. - -| Name | Possible Values | Default | Description | -|----------------------------------------|--------------------------------------------|--------------|-----------------------------------------------------| -| `generate.servers` | `true`, `false` | `true` | Generate server stubs | -| `generate.clients` | `true`, `false` | `true` | Generate client stubs | -| `generate.messages` | `true`, `false` | `true` | Generate message stubs | -| `generatedSource.accessLevelOnImports` | `true`, `false` | `false` | Whether imports should have explicit access levels | -| `generatedSource.accessLevel` | `"public"`, `"package"`, `"internal"` | `"internal"` | Access level for generated stubs | -| `protoc.executablePath` | N/A | `null`† | Path to the `protoc` executable | -| `protoc.importPaths` | N/A | `null`‡ | Import paths passed to `protoc` | - -† The Swift Package Manager build plugin infrastructure will attempt to discover the executable's location if you don't provide one. - -‡ If you don't provide any import paths then the path to the configuration file will be used on a per-source-file basis. - -Many of these options map to `protoc-gen-grpc-swift` and `protoc-gen-swift` options. - -If you require greater flexibility you may specify more than one configuration file. -Configuration files apply to all `.proto` files equal to or below it in the file hierarchy. A configuration file -lower in the file hierarchy supersedes one above it. - -### Using protoc - -The [`grpc-swift-protobuf`](https://github.com/grpc/grpc-swift-protobuf) package provides -`protoc-gen-grpc-swift`, a program which is a plugin for the Protocol Buffers compiler, `protoc`. -To generate gRPC stubs for your `.proto` files directly you must run the `protoc` command with -the `--grpc-swift_out=` option: - -```console -protoc --grpc-swift_out=. my-service.proto -``` - -> `protoc-gen-grpc-swift` only generates gRPC stubs, it doesn't generate messages. You must use -> `protoc-gen-swift` to generate messages in addition to gRPC Stubs. - -The presence of `--grpc-swift_out` tells `protoc` to use the `protoc-gen-grpc-swift` plugin. By -default it'll look for the plugin in your `PATH`. You can also specify the path to the plugin -explicitly: - -```console -protoc --plugin=/path/to/protoc-gen-grpc-swift --grpc-swift_out=. my-service.proto -``` - -You can also specify various option the `protoc-gen-grpc-swift` via `protoc` using -the `--grpc-swift_opt` argument: - -```console -protoc --grpc-swift_opt== --grpc-swift_out=. -``` - -You can specify multiple options by passing the `--grpc-swift_opt` argument multiple times: - -```console -protoc \ - --grpc-swift_opt== \ - --grpc-swift_opt== \ - --grpc-swift_out=. -``` - -#### Generator options - -| Name | Possible Values | Default | Description | -|---------------------------|--------------------------------------------|------------|----------------------------------------------------------| -| `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs | -| `Server` | `True`, `False` | `True` | Generate server stubs | -| `Client` | `True`, `False` | `True` | Generate client stubs | -| `FileNaming` | `FullPath`, `PathToUnderscore`, `DropPath` | `FullPath` | How generated source files should be named. (See below.) | -| `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. (See below.) | -| `UseAccessLevelOnImports` | `True`, `False` | `False` | Whether imports should have explicit access levels. | - -The `FileNaming` option has three possible values, for an input of `foo/bar/baz.proto` the following -output file will be generated: -- `FullPath`: `foo/bar/baz.grpc.swift`. -- `PathToUnderscore`: `foo_bar_baz.grpc.swift` -- `DropPath`: `baz.grpc.swift` - -The code generator assumes all inputs are generated into the same module, `ProtoPathModuleMappings` -allows you to specify a mapping from `.proto` files to the Swift module they are generated in. This -allows the code generator to add appropriate imports to your generated stubs. This is described in -more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md). - -#### Building the protoc plugin - -> The version of `protoc-gen-grpc-swift` you use mustn't be newer than the version of -> the `grpc-swift-protobuf` you're using. - -If your package depends on `grpc-swift-protobuf` then you can get a copy of `protoc-gen-grpc-swift` -by building it directly: - -```console -swift build --product protoc-gen-grpc-swift -``` - -This command will build the plugin into `.build/debug` directory. You can get the full path using -`swift build --show-bin-path`. +You can learn about generating gRPC stubs from the [gRPC Swift Protobuf +documentation](https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs). From 968f4dc624370326d41a9a45a6e7daf4b6824ee3 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 24 Jan 2025 15:50:59 +0000 Subject: [PATCH 551/580] Update dependencies for rc (#2181) Motivation: We're going to tag an RC soon so bump tutorial/example dependencies. Modifications: - Bump dependencies to `-rc.1`. Result: Docs will be correct for the tagged release. --- Examples/echo/Package.swift | 6 +++--- Examples/error-details/Package.swift | 4 ++-- Examples/hello-world/Package.swift | 6 +++--- Examples/reflection-server/Package.swift | 8 ++++---- Examples/route-guide/Package.swift | 6 +++--- README.md | 6 +++--- .../Tutorials/Hello-World/Hello-World.tutorial | 2 +- .../Resources/route-guide-sec01-step07-description.swift | 6 +++--- .../Resources/route-guide-sec01-step08-description.swift | 6 +++--- .../Resources/route-guide-sec01-step09-plugin.swift | 6 +++--- .../Resources/route-guide-sec05-step00-package.swift | 6 +++--- .../Resources/route-guide-sec05-step01-package.swift | 6 +++--- .../Resources/route-guide-sec05-step02-package.swift | 6 +++--- 13 files changed, 37 insertions(+), 37 deletions(-) diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index 01608d9a5..ca43e1957 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift index e9c868719..1810e76b0 100644 --- a/Examples/error-details/Package.swift +++ b/Examples/error-details/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "error-details", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), ], targets: [ .executableTarget( diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 696daffc2..2fe6d8f3f 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/reflection-server/Package.swift b/Examples/reflection-server/Package.swift index 684b10c1c..8630885dd 100644 --- a/Examples/reflection-server/Package.swift +++ b/Examples/reflection-server/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "reflection-server", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-extras.git", exact: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-extras.git", exact: "1.0.0-rc.1"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index b2d38d69f..c6730cfb2 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/README.md b/README.md index d641ed824..b5abda907 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ let package = Package( name: "foo-package", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 3a2c60697..93788cf4b 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -18,7 +18,7 @@ repository by running the following command in a terminal: ```console - git clone --branch 2.0.0-beta.3 https://github.com/grpc/grpc-swift + git clone --branch 2.0.0-rc.1 https://github.com/grpc/grpc-swift ``` You then need to change directory to the `Examples/hello-world` directory of the cloned diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift index 2f1fb0bd3..1d964173c 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), ], targets: [] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift index dcb3ced86..401ba2054 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift index 6db99da7f..81793e013 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index 6db99da7f..81793e013 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index 27ee4f7f3..3f149e5d0 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index 49f69af0c..7163d45f1 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-beta.3"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-beta.3"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ From f1a8497647d05bfcc3f57904ba5ea819206687db Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 29 Jan 2025 06:51:34 +0000 Subject: [PATCH 552/580] Fix grpc-swift-protobuf version in hello-world example (#2186) CI is currently failing to build because `grpc-swift-protobuf`'s dependency on the `hello-world` example points to no a non-existing tag. --- Examples/hello-world/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 2fe6d8f3f..237b86d8f 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -22,7 +22,7 @@ let package = Package( platforms: [.macOS("15.0")], dependencies: [ .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], From 0e94c29f1164d07ac1fa571dbe649df6c9dff847 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 30 Jan 2025 10:24:38 +0000 Subject: [PATCH 553/580] CI use 6.1 nightlies (#2187) CI use 6.1 nightlies now that Swift development is happening in the 6.1 branch --- .github/workflows/main.yml | 2 +- .github/workflows/pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aea5da49f..536b48b28 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" benchmarks: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8a60ba1f8..29a9bd5e3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,7 +23,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" construct-examples-matrix: From f20f91669b65f31a024af0e9a609a111a427522d Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 31 Jan 2025 13:38:26 +0000 Subject: [PATCH 554/580] Add `echo-metadata` example (#2182) This PR adds an example showcasing how to set/read request and response metadata. --------- Co-authored-by: George Barnett --- Examples/echo-metadata/.gitignore | 8 ++ Examples/echo-metadata/Package.swift | 43 +++++++++++ Examples/echo-metadata/README.md | 58 +++++++++++++++ .../Sources/ClientArguments.swift | 35 +++++++++ .../echo-metadata/Sources/EchoMetadata.swift | 27 +++++++ .../echo-metadata/Sources/EchoService.swift | 73 +++++++++++++++++++ Examples/echo-metadata/Sources/Protos/echo | 1 + .../grpc-swift-proto-generator-config.json | 7 ++ .../Sources/Subcommands/Collect.swift | 58 +++++++++++++++ .../Sources/Subcommands/Expand.swift | 65 +++++++++++++++++ .../Sources/Subcommands/Get.swift | 53 ++++++++++++++ .../Sources/Subcommands/Serve.swift | 43 +++++++++++ .../Sources/Subcommands/Update.swift | 67 +++++++++++++++++ Examples/echo/README.md | 2 +- 14 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 Examples/echo-metadata/.gitignore create mode 100644 Examples/echo-metadata/Package.swift create mode 100644 Examples/echo-metadata/README.md create mode 100644 Examples/echo-metadata/Sources/ClientArguments.swift create mode 100644 Examples/echo-metadata/Sources/EchoMetadata.swift create mode 100644 Examples/echo-metadata/Sources/EchoService.swift create mode 120000 Examples/echo-metadata/Sources/Protos/echo create mode 100644 Examples/echo-metadata/Sources/Protos/grpc-swift-proto-generator-config.json create mode 100644 Examples/echo-metadata/Sources/Subcommands/Collect.swift create mode 100644 Examples/echo-metadata/Sources/Subcommands/Expand.swift create mode 100644 Examples/echo-metadata/Sources/Subcommands/Get.swift create mode 100644 Examples/echo-metadata/Sources/Subcommands/Serve.swift create mode 100644 Examples/echo-metadata/Sources/Subcommands/Update.swift diff --git a/Examples/echo-metadata/.gitignore b/Examples/echo-metadata/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/echo-metadata/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/echo-metadata/Package.swift b/Examples/echo-metadata/Package.swift new file mode 100644 index 000000000..9cad1a0ff --- /dev/null +++ b/Examples/echo-metadata/Package.swift @@ -0,0 +1,43 @@ +// swift-tools-version:6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "echo-metadata", + platforms: [.macOS("15.0")], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "echo-metadata", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/echo-metadata/README.md b/Examples/echo-metadata/README.md new file mode 100644 index 000000000..fc9f17fc8 --- /dev/null +++ b/Examples/echo-metadata/README.md @@ -0,0 +1,58 @@ +# Echo-Metadata + +This example demonstrates how to interact with `Metadata` on RPCs: how to set and read it on unary +and streaming requests, as well as how to set and read both initial and trailing metadata on unary +and streaming responses. This is done using a simple 'echo' server and client and the SwiftNIO +based HTTP/2 transport. + +## Overview + +An `echo-metadata` command line tool that uses generated stubs for an 'echo-metadata' service +which allows you to start a server and to make requests against it. + +You can use any of the client's subcommands (`get`, `collect`, `expand` and `update`) to send the +provided `message` as both the request's message, and as the value for the `echo-message` key in +the request's metadata. + +The server will then echo back the message and the metadata's `echo-message` key-value pair sent +by the client. The request's metadata will be echoed both in the initial and the trailing metadata. + +The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) HTTP/2 transport. + +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + +## Usage + +Build and run the server using the CLI: + +```console +$ PROTOC_PATH=$(which protoc) swift run echo-metadata serve +Echo-Metadata listening on [ipv4]127.0.0.1:1234 +``` + +Use the CLI to run the client and make a `get` (unary) request: + +```console +$ PROTOC_PATH=$(which protoc) swift run echo-metadata get --message "hello" +get → metadata: [("echo-message", "hello")] +get → message: hello +get ← initial metadata: [("echo-message", "hello")] +get ← message: hello +get ← trailing metadata: [("echo-message", "hello")] +``` + +Get help with the CLI by running: + +```console +$ PROTOC_PATH=$(which protoc) swift run echo-metadata --help +``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Examples/echo-metadata/Sources/ClientArguments.swift b/Examples/echo-metadata/Sources/ClientArguments.swift new file mode 100644 index 000000000..9aa237f67 --- /dev/null +++ b/Examples/echo-metadata/Sources/ClientArguments.swift @@ -0,0 +1,35 @@ +/* + * Copyright 2025, 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 ArgumentParser +import GRPCNIOTransportHTTP2 + +struct ClientArguments: ParsableArguments { + @Option(help: "The server's listening port") + var port: Int = 1234 + + @Option( + help: + "Message to send to the server. It will also be sent in the request's metadata as the value for `echo-message`." + ) + var message: String +} + +extension ClientArguments { + var target: any ResolvableTarget { + return .ipv4(host: "127.0.0.1", port: self.port) + } +} diff --git a/Examples/echo-metadata/Sources/EchoMetadata.swift b/Examples/echo-metadata/Sources/EchoMetadata.swift new file mode 100644 index 000000000..4624f16c7 --- /dev/null +++ b/Examples/echo-metadata/Sources/EchoMetadata.swift @@ -0,0 +1,27 @@ +/* + * Copyright 2025, 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 ArgumentParser +import GRPCCore + +@main +struct EchoMetadata: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "echo-metadata", + abstract: "A multi-tool to run an echo-metadata server and execute RPCs against it.", + subcommands: [Serve.self, Get.self, Collect.self, Update.self, Expand.self] + ) +} diff --git a/Examples/echo-metadata/Sources/EchoService.swift b/Examples/echo-metadata/Sources/EchoService.swift new file mode 100644 index 000000000..ebfe56de2 --- /dev/null +++ b/Examples/echo-metadata/Sources/EchoService.swift @@ -0,0 +1,73 @@ +/* + * Copyright 2025, 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 + +struct EchoService: Echo_Echo.ServiceProtocol { + func get( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") })) + return ServerResponse( + message: .with { $0.text = request.message.text }, + metadata: responseMetadata, + trailingMetadata: responseMetadata + ) + } + + func collect( + request: StreamingServerRequest, + context: ServerContext + ) async throws -> ServerResponse { + let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") })) + let messages = try await request.messages.reduce(into: []) { $0.append($1.text) } + let joined = messages.joined(separator: " ") + + return ServerResponse( + message: .with { $0.text = joined }, + metadata: responseMetadata, + trailingMetadata: responseMetadata + ) + } + + func expand( + request: ServerRequest, + context: ServerContext + ) async throws -> StreamingServerResponse { + let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") })) + let parts = request.message.text.split(separator: " ") + let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } } + + return StreamingServerResponse(metadata: responseMetadata) { writer in + try await writer.write(contentsOf: messages) + return responseMetadata + } + } + + func update( + request: StreamingServerRequest, + context: ServerContext + ) async throws -> StreamingServerResponse { + let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") })) + return StreamingServerResponse(metadata: responseMetadata) { writer in + for try await message in request.messages { + try await writer.write(.with { $0.text = message.text }) + } + return responseMetadata + } + } +} diff --git a/Examples/echo-metadata/Sources/Protos/echo b/Examples/echo-metadata/Sources/Protos/echo new file mode 120000 index 000000000..66fa3f5f5 --- /dev/null +++ b/Examples/echo-metadata/Sources/Protos/echo @@ -0,0 +1 @@ +../../../../dev/protos/examples/echo/ \ No newline at end of file diff --git a/Examples/echo-metadata/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/echo-metadata/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/echo-metadata/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/echo-metadata/Sources/Subcommands/Collect.swift b/Examples/echo-metadata/Sources/Subcommands/Collect.swift new file mode 100644 index 000000000..a523a809e --- /dev/null +++ b/Examples/echo-metadata/Sources/Subcommands/Collect.swift @@ -0,0 +1,58 @@ +/* + * Copyright 2025, 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 ArgumentParser +import GRPCCore +import GRPCNIOTransportHTTP2 + +struct Collect: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Makes a client streaming RPC to the echo-metadata server." + ) + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + try await withGRPCClient( + transport: .http2NIOPosix( + target: self.arguments.target, + transportSecurity: .plaintext + ) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) + let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"] + + print("collect → metadata: \(requestMetadata)") + try await echo.collect(metadata: requestMetadata) { writer in + for part in self.arguments.message.split(separator: " ") { + print("collect → \(part)") + try await writer.write(.with { $0.text = String(part) }) + } + } onResponse: { response in + let initialMetadata = Metadata(response.metadata.filter({ $0.key.starts(with: "echo-") })) + print("collect ← initial metadata: \(initialMetadata)") + + print("collect ← message: \(try response.message.text)") + + let trailingMetadata = Metadata( + response.trailingMetadata.filter({ $0.key.starts(with: "echo-") }) + ) + print("collect ← trailing metadata: \(trailingMetadata)") + } + } + } +} diff --git a/Examples/echo-metadata/Sources/Subcommands/Expand.swift b/Examples/echo-metadata/Sources/Subcommands/Expand.swift new file mode 100644 index 000000000..134a2ac66 --- /dev/null +++ b/Examples/echo-metadata/Sources/Subcommands/Expand.swift @@ -0,0 +1,65 @@ +/* + * Copyright 2025, 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 ArgumentParser +import GRPCCore +import GRPCNIOTransportHTTP2 + +struct Expand: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Makes a server streaming RPC to the echo-metadata server." + ) + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + try await withGRPCClient( + transport: .http2NIOPosix( + target: self.arguments.target, + transportSecurity: .plaintext + ) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) + let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"] + let message = Echo_EchoRequest.with { $0.text = self.arguments.message } + + print("expand → metadata: \(requestMetadata)") + print("expand → message: \(message.text)") + + try await echo.expand(message, metadata: requestMetadata) { response in + let responseContents = try response.accepted.get() + + let initialMetadata = Metadata( + responseContents.metadata.filter({ $0.key.starts(with: "echo-") }) + ) + print("expand ← initial metadata: \(initialMetadata)") + for try await part in responseContents.bodyParts { + switch part { + case .message(let message): + print("expand ← message: \(message.text)") + + case .trailingMetadata(let trailingMetadata): + let trailingMetadata = Metadata( + trailingMetadata.filter({ $0.key.starts(with: "echo-") }) + ) + print("expand ← trailing metadata: \(trailingMetadata)") + } + } + } + } + } +} diff --git a/Examples/echo-metadata/Sources/Subcommands/Get.swift b/Examples/echo-metadata/Sources/Subcommands/Get.swift new file mode 100644 index 000000000..443da6914 --- /dev/null +++ b/Examples/echo-metadata/Sources/Subcommands/Get.swift @@ -0,0 +1,53 @@ +/* + * Copyright 2025, 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 ArgumentParser +import GRPCCore +import GRPCNIOTransportHTTP2 + +struct Get: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Makes a unary RPC to the echo-metadata server." + ) + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + try await withGRPCClient( + transport: .http2NIOPosix( + target: self.arguments.target, + transportSecurity: .plaintext + ) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) + let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"] + let message = Echo_EchoRequest.with { $0.text = self.arguments.message } + + print("get → metadata: \(requestMetadata)") + print("get → message: \(message.text)") + try await echo.get(message, metadata: requestMetadata) { response in + let initialMetadata = Metadata(response.metadata.filter({ $0.key.starts(with: "echo-") })) + print("get ← initial metadata: \(initialMetadata)") + print("get ← message: \(try response.message.text)") + let trailingMetadata = Metadata( + response.trailingMetadata.filter({ $0.key.starts(with: "echo-") }) + ) + print("get ← trailing metadata: \(trailingMetadata)") + } + } + } +} diff --git a/Examples/echo-metadata/Sources/Subcommands/Serve.swift b/Examples/echo-metadata/Sources/Subcommands/Serve.swift new file mode 100644 index 000000000..36f4616d0 --- /dev/null +++ b/Examples/echo-metadata/Sources/Subcommands/Serve.swift @@ -0,0 +1,43 @@ +/* + * Copyright 2025, 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 ArgumentParser +import GRPCCore +import GRPCNIOTransportHTTP2 + +struct Serve: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Starts an echo-metadata server.") + + @Option(help: "The port to listen on") + var port: Int = 1234 + + func run() async throws { + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: self.port), + transportSecurity: .plaintext + ), + services: [EchoService()] + ) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await server.serve() } + if let address = try await server.listeningAddress { + print("Echo-Metadata listening on \(address)") + } + } + } +} diff --git a/Examples/echo-metadata/Sources/Subcommands/Update.swift b/Examples/echo-metadata/Sources/Subcommands/Update.swift new file mode 100644 index 000000000..f357fa901 --- /dev/null +++ b/Examples/echo-metadata/Sources/Subcommands/Update.swift @@ -0,0 +1,67 @@ +/* + * Copyright 2025, 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 ArgumentParser +import GRPCCore +import GRPCNIOTransportHTTP2 + +struct Update: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Makes a bidirectional server streaming RPC to the echo-metadata server." + ) + + @OptionGroup + var arguments: ClientArguments + + func run() async throws { + try await withGRPCClient( + transport: .http2NIOPosix( + target: self.arguments.target, + transportSecurity: .plaintext + ) + ) { client in + let echo = Echo_Echo.Client(wrapping: client) + let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"] + + print("update → metadata: \(requestMetadata)") + try await echo.update(metadata: requestMetadata) { writer in + for part in self.arguments.message.split(separator: " ") { + print("update → message: \(part)") + try await writer.write(.with { $0.text = String(part) }) + } + } onResponse: { response in + let responseContents = try response.accepted.get() + + let initialMetadata = Metadata( + responseContents.metadata.filter({ $0.key.starts(with: "echo-") }) + ) + print("update ← initial metadata: \(initialMetadata)") + for try await part in responseContents.bodyParts { + switch part { + case .message(let message): + print("update ← message: \(message.text)") + + case .trailingMetadata(let trailingMetadata): + let trailingMetadata = Metadata( + trailingMetadata.filter({ $0.key.starts(with: "echo-") }) + ) + print("update ← trailing metadata: \(trailingMetadata)") + } + } + } + } + } +} diff --git a/Examples/echo/README.md b/Examples/echo/README.md index da5049731..7753d3e1e 100644 --- a/Examples/echo/README.md +++ b/Examples/echo/README.md @@ -1,7 +1,7 @@ # Echo This example demonstrates all four RPC types using a simple 'echo' service and -client and the Swift NIO based HTTP/2 transport. +client and the SwiftNIO based HTTP/2 transport. ## Overview From 2ed95d8573dadb593e9ae339ee9b796268599593 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 31 Jan 2025 13:46:42 +0000 Subject: [PATCH 555/580] Update dependency requirements and docs (#2188) --- Examples/echo-metadata/Package.swift | 6 +++--- Examples/echo/Package.swift | 6 +++--- Examples/error-details/Package.swift | 4 ++-- Examples/hello-world/Package.swift | 6 +++--- Examples/reflection-server/Package.swift | 8 ++++---- Examples/route-guide/Package.swift | 6 +++--- README.md | 15 +++++---------- .../GRPCCore/Documentation.docc/Documentation.md | 3 --- .../Tutorials/Hello-World/Hello-World.tutorial | 2 +- .../route-guide-sec01-step07-description.swift | 6 +++--- .../route-guide-sec01-step08-description.swift | 6 +++--- .../route-guide-sec01-step09-plugin.swift | 6 +++--- .../route-guide-sec05-step00-package.swift | 6 +++--- .../route-guide-sec05-step01-package.swift | 6 +++--- .../route-guide-sec05-step02-package.swift | 6 +++--- .../Tutorials/Route-Guide/Route-Guide.tutorial | 3 +-- 16 files changed, 43 insertions(+), 52 deletions(-) diff --git a/Examples/echo-metadata/Package.swift b/Examples/echo-metadata/Package.swift index 9cad1a0ff..e19ab6ef0 100644 --- a/Examples/echo-metadata/Package.swift +++ b/Examples/echo-metadata/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "echo-metadata", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index ca43e1957..6705a8b1f 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/error-details/Package.swift b/Examples/error-details/Package.swift index 1810e76b0..b2f15c721 100644 --- a/Examples/error-details/Package.swift +++ b/Examples/error-details/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "error-details", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 237b86d8f..daa71a571 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/reflection-server/Package.swift b/Examples/reflection-server/Package.swift index 8630885dd..4361c689c 100644 --- a/Examples/reflection-server/Package.swift +++ b/Examples/reflection-server/Package.swift @@ -21,10 +21,10 @@ let package = Package( name: "reflection-server", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-extras.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-extras.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index c6730cfb2..b4a9589f1 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,9 +21,9 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), ], targets: [ diff --git a/README.md b/README.md index b5abda907..a6ea9c736 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,6 @@ This repository contains a gRPC implementation for Swift. You can read more about gRPC on the [gRPC project's website][grpcio]. -> gRPC Swift v2.x is under active development on the `main` branch and takes -> full advantage of Swift's native concurrency features. -> -> v1.x is still supported and maintained on the `release/1.x` branch. - - 📚 **Documentation** and **tutorials** are available on the [Swift Package Index][spi-grpc-swift] - 💻 **Examples** are available in the [Examples](Examples) directory - 🚀 **Contributions** are welcome, please see [CONTRIBUTING.md](CONTRIBUTING.md) @@ -29,16 +24,16 @@ the SwiftNIO based transport and SwiftProtobuf serialization: import PackageDescription let package = Package( - name: "foo-package", + name: "Application", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), ], targets: [ .executableTarget( - name: "bar-target", + name: "Server", dependencies: [ .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index b885c72e8..7902e601c 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -2,9 +2,6 @@ A gRPC library for Swift written natively in Swift. -> 🚧 This module is part of gRPC Swift v2 which is under active development and in the pre-release -> stage. - ## Overview ### Package structure diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 93788cf4b..a16c4edb2 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -18,7 +18,7 @@ repository by running the following command in a terminal: ```console - git clone --branch 2.0.0-rc.1 https://github.com/grpc/grpc-swift + git clone --branch 2.0.0 https://github.com/grpc/grpc-swift ``` You then need to change directory to the `Examples/hello-world` directory of the cloned diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift index 1d964173c..e30951efb 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), ], targets: [] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift index 401ba2054..0fe74355e 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift index 81793e013..427915609 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step09-plugin.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index 81793e013..427915609 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index 3f149e5d0..757d99104 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index 7163d45f1..89ace64df 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -5,9 +5,9 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-rc.1"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-rc.1"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial index 1e62f5120..c1d8e42b2 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -75,8 +75,7 @@ } @Step { - We need to add a dependency on the gRPC Swift and Swift Protobuf packages. As gRPC Swift v2 - hasn't yet been released the dependencies must use the `-beta` tags. + We need to add a dependency on the gRPC Swift and Swift Protobuf packages. Note that we also add `.macOS(.v15)` to platforms, this is the earliest macOS version supported by gRPC Swift v2. From 2b17298b1bfe0931c1e4d247ab943f4083fa3213 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 31 Jan 2025 14:57:44 +0000 Subject: [PATCH 556/580] Add a computed property to get the `bodyParts` from a `StreamingClientResponse` (#2184) This PR resolves https://github.com/grpc/grpc-swift/issues/2183 --------- Co-authored-by: George Barnett --- .../GRPCCore/Call/Client/ClientResponse.swift | 17 ++++++++++- .../Call/Client/ClientResponseTests.swift | 30 ++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index b99f6f3ae..73fcde0d5 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -363,7 +363,7 @@ extension StreamingClientResponse { /// Returns the messages received from the server. /// - /// For rejected RPCs the `RPCAsyncSequence` throws a `RPCError``. + /// For rejected RPCs (in other words, where ``accepted`` is `failure`), the `RPCAsyncSequence` throws a ``RPCError``. public var messages: RPCAsyncSequence { switch self.accepted { case let .success(contents): @@ -382,4 +382,19 @@ extension StreamingClientResponse { return RPCAsyncSequence.throwing(error) } } + + /// Returns the body parts (i.e. `messages` and `trailingMetadata`) returned from the server. + /// + /// For rejected RPCs (in other words, where ``accepted`` is `failure`), the `RPCAsyncSequence` throws a ``RPCError``. + public var bodyParts: RPCAsyncSequence { + switch self.accepted { + case let .success(contents): + return contents.bodyParts + + case let .failure(error): + return RPCAsyncSequence.throwing(error) + } + } } + +extension StreamingClientResponse.Contents.BodyPart: Equatable where Message: Equatable {} diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index 01557d02c..46b0f19ae 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -53,7 +53,7 @@ final class ClientResponseTests: XCTestCase { XCTAssertEqual(response.trailingMetadata, ["bar": "baz"]) } - func testAcceptedStreamResponseConvenienceMethods() async throws { + func testAcceptedStreamResponseConvenienceMethods_Messages() async throws { let response = StreamingClientResponse( of: String.self, metadata: ["foo": "bar"], @@ -73,6 +73,29 @@ final class ClientResponseTests: XCTestCase { XCTAssertEqual(messages, ["foo", "bar", "baz"]) } + func testAcceptedStreamResponseConvenienceMethods_BodyParts() async throws { + let response = StreamingClientResponse( + of: String.self, + metadata: ["foo": "bar"], + bodyParts: RPCAsyncSequence( + wrapping: AsyncThrowingStream { + $0.yield(.message("foo")) + $0.yield(.message("bar")) + $0.yield(.message("baz")) + $0.yield(.trailingMetadata(["baz": "baz"])) + $0.finish() + } + ) + ) + + XCTAssertEqual(response.metadata, ["foo": "bar"]) + let bodyParts = try await response.bodyParts.collect() + XCTAssertEqual( + bodyParts, + [.message("foo"), .message("bar"), .message("baz"), .trailingMetadata(["baz": "baz"])] + ) + } + func testRejectedStreamResponseConvenienceMethods() async throws { let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"]) let response = StreamingClientResponse(of: String.self, error: error) @@ -83,6 +106,11 @@ final class ClientResponseTests: XCTestCase { } errorHandler: { XCTAssertEqual($0, error) } + await XCTAssertThrowsRPCErrorAsync { + try await response.bodyParts.collect() + } errorHandler: { + XCTAssertEqual($0, error) + } } func testStreamToSingleConversionForValidStream() async throws { From f16339283883b400113c6f7d55e62ec6b0724b14 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 31 Jan 2025 15:18:21 +0000 Subject: [PATCH 557/580] Add a dev-tool subpackage (#2167) Motivation: A number of test in this package and others rely on ad-hoc services using Codable. This is less overhead than using protobuf as you it's not always available. It also means the messages are defined in Swift so they're easy to change without needing to regenerate. However, the service glue code is hand rolled. We can avoid this by having a little adapter sit on top of the code gen lib. Modifications: - Add a grpc-dev-tool package to dev. We can use this as a place to add tooling and other helpers without worrying about worsening the experience for end users (because of additional dependencies, more public API and so on). - For now this has a single executable for generating code from a JSON config file. The schema for the services is limited, but that's fine, it's not a general purpose tool. Result: - We have a tool which can generate grpc code from a JSON definition which uses Codable message types. --- dev/format.sh | 2 + dev/grpc-dev-tool/.gitignore | 8 ++ dev/grpc-dev-tool/Package.swift | 36 +++++ .../Sources/grpc-dev-tool/GRPCDevTool.swift | 25 ++++ .../GRPCCodeGen+Conversions.swift | 75 ++++++++++ .../GRPCDevUtils+GenerateJSON.swift | 83 +++++++++++ .../GenerateJSON/JSONCodeGenerator.swift | 119 +++++++++++++++ .../JSONCodeGeneratorRequest.swift | 135 ++++++++++++++++++ 8 files changed, 483 insertions(+) create mode 100644 dev/grpc-dev-tool/.gitignore create mode 100644 dev/grpc-dev-tool/Package.swift create mode 100644 dev/grpc-dev-tool/Sources/grpc-dev-tool/GRPCDevTool.swift create mode 100644 dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCCodeGen+Conversions.swift create mode 100644 dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCDevUtils+GenerateJSON.swift create mode 100644 dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGenerator.swift create mode 100644 dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGeneratorRequest.swift diff --git a/dev/format.sh b/dev/format.sh index 7e4e46519..1201d6861 100755 --- a/dev/format.sh +++ b/dev/format.sh @@ -61,6 +61,7 @@ if "$lint"; then "${repo}/Tests" \ "${repo}/Examples" \ "${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/dev" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then @@ -80,6 +81,7 @@ elif "$format"; then "${repo}/Tests" \ "${repo}/Examples" \ "${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/dev" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then diff --git a/dev/grpc-dev-tool/.gitignore b/dev/grpc-dev-tool/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/dev/grpc-dev-tool/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/dev/grpc-dev-tool/Package.swift b/dev/grpc-dev-tool/Package.swift new file mode 100644 index 000000000..8f8d3f892 --- /dev/null +++ b/dev/grpc-dev-tool/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version:6.0 +/* + * Copyright 2025, 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 PackageDescription + +let package = Package( + name: "grpc-dev-tool", + platforms: [.macOS(.v15)], + dependencies: [ + .package(path: "../.."), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "grpc-dev-tool", + dependencies: [ + .product(name: "GRPCCodeGen", package: "grpc-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ) + ] +) diff --git a/dev/grpc-dev-tool/Sources/grpc-dev-tool/GRPCDevTool.swift b/dev/grpc-dev-tool/Sources/grpc-dev-tool/GRPCDevTool.swift new file mode 100644 index 000000000..5cf08ea27 --- /dev/null +++ b/dev/grpc-dev-tool/Sources/grpc-dev-tool/GRPCDevTool.swift @@ -0,0 +1,25 @@ +/* + * Copyright 2025, 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 ArgumentParser + +@main +struct GRPCDevTool: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "grpc-dev-tool", + subcommands: [GenerateJSON.self] + ) +} diff --git a/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCCodeGen+Conversions.swift b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCCodeGen+Conversions.swift new file mode 100644 index 000000000..1888aa866 --- /dev/null +++ b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCCodeGen+Conversions.swift @@ -0,0 +1,75 @@ +/* + * Copyright 2025, 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 GRPCCodeGen + +/// Creates a `ServiceDescriptor` from a JSON `ServiceSchema`. +extension ServiceDescriptor { + init(_ service: ServiceSchema) { + self.init( + documentation: "", + name: .init( + identifyingName: service.name, + typeName: service.name, + propertyName: service.name + ), + methods: service.methods.map { + MethodDescriptor($0) + } + ) + } +} + +extension MethodDescriptor { + /// Creates a `MethodDescriptor` from a JSON `ServiceSchema.Method`. + init(_ method: ServiceSchema.Method) { + self.init( + documentation: "", + name: .init( + identifyingName: method.name, + typeName: method.name, + functionName: method.name + ), + isInputStreaming: method.kind.streamsInput, + isOutputStreaming: method.kind.streamsOutput, + inputType: method.input, + outputType: method.output + ) + } +} + +extension CodeGenerator.Config.AccessLevel { + init(_ level: GeneratorConfig.AccessLevel) { + switch level { + case .internal: + self = .internal + case .package: + self = .package + } + } +} + +extension CodeGenerator.Config { + init(_ config: GeneratorConfig) { + self.init( + accessLevel: CodeGenerator.Config.AccessLevel(config.accessLevel), + accessLevelOnImports: config.accessLevelOnImports, + client: config.generateClient, + server: config.generateServer, + indentation: 2 + ) + } +} diff --git a/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCDevUtils+GenerateJSON.swift b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCDevUtils+GenerateJSON.swift new file mode 100644 index 000000000..d7821d0ca --- /dev/null +++ b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/GRPCDevUtils+GenerateJSON.swift @@ -0,0 +1,83 @@ +/* + * Copyright 2025, 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 ArgumentParser +import Foundation + +struct GenerateJSON: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "generate-json", + subcommands: [Generate.self, DumpConfig.self], + defaultSubcommand: Generate.self + ) +} + +extension GenerateJSON { + struct Generate: ParsableCommand { + @Argument(help: "The path to a JSON input file.") + var input: String + + func run() throws { + // Decode the input file. + let url = URL(filePath: self.input) + let data = try Data(contentsOf: url) + let json = JSONDecoder() + let config = try json.decode(JSONCodeGeneratorRequest.self, from: data) + + // Generate the output and dump it to stdout. + let generator = JSONCodeGenerator() + let sourceFile = try generator.generate(request: config) + print(sourceFile.contents) + } + } +} + +extension GenerateJSON { + struct DumpConfig: ParsableCommand { + func run() throws { + // Create a request for the code generator using all four RPC kinds. + var request = JSONCodeGeneratorRequest( + service: ServiceSchema(name: "Echo", methods: []), + config: .defaults + ) + + let methodNames = ["get", "collect", "expand", "update"] + let methodKinds: [ServiceSchema.Method.Kind] = [ + .unary, + .clientStreaming, + .serverStreaming, + .bidiStreaming, + ] + + for (name, kind) in zip(methodNames, methodKinds) { + let method = ServiceSchema.Method( + name: name, + input: "EchoRequest", + output: "EchoResponse", + kind: kind + ) + request.service.methods.append(method) + } + + // Encoding the config to JSON and dump it to stdout. + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted] + let data = try encoder.encode(request) + let json = String(decoding: data, as: UTF8.self) + print(json) + } + } +} diff --git a/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGenerator.swift b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGenerator.swift new file mode 100644 index 000000000..3c827b93d --- /dev/null +++ b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGenerator.swift @@ -0,0 +1,119 @@ +/* + * Copyright 2025, 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 Foundation +import GRPCCodeGen + +struct JSONCodeGenerator { + private static let currentYear: Int = { + let now = Date() + let year = Calendar.current.component(.year, from: Date()) + return year + }() + + private static let header = """ + /* + * Copyright \(Self.currentYear), 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. + */ + """ + + private static let jsonSerializers: String = """ + fileprivate struct JSONSerializer: MessageSerializer { + fileprivate func serialize( + _ message: Message + ) throws -> Bytes { + do { + let jsonEncoder = JSONEncoder() + let data = try jsonEncoder.encode(message) + return Bytes(data) + } catch { + throw RPCError( + code: .internalError, + message: "Can't serialize message to JSON.", + cause: error + ) + } + } + } + + fileprivate struct JSONDeserializer: MessageDeserializer { + fileprivate func deserialize( + _ serializedMessageBytes: Bytes + ) throws -> Message { + do { + let jsonDecoder = JSONDecoder() + let data = serializedMessageBytes.withUnsafeBytes { Data($0) } + return try jsonDecoder.decode(Message.self, from: data) + } catch { + throw RPCError( + code: .internalError, + message: "Can't deserialize message from JSON.", + cause: error + ) + } + } + } + """ + + func generate(request: JSONCodeGeneratorRequest) throws -> SourceFile { + let generator = CodeGenerator(config: CodeGenerator.Config(request.config)) + + let codeGenRequest = CodeGenerationRequest( + fileName: request.service.name + ".swift", + leadingTrivia: Self.header, + dependencies: [ + Dependency( + item: Dependency.Item(kind: .struct, name: "Data"), + module: "Foundation", + accessLevel: .internal + ), + Dependency( + item: Dependency.Item(kind: .class, name: "JSONEncoder"), + module: "Foundation", + accessLevel: .internal + ), + Dependency( + item: Dependency.Item(kind: .class, name: "JSONDecoder"), + module: "Foundation", + accessLevel: .internal + ), + ], + services: [ServiceDescriptor(request.service)], + makeSerializerCodeSnippet: { type in "JSONSerializer<\(type)>()" }, + makeDeserializerCodeSnippet: { type in "JSONDeserializer<\(type)>()" } + ) + + var sourceFile = try generator.generate(codeGenRequest) + + // Insert a fileprivate serializer/deserializer for JSON at the bottom of each file. + sourceFile.contents += "\n\n" + sourceFile.contents += Self.jsonSerializers + + return sourceFile + } +} diff --git a/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGeneratorRequest.swift b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGeneratorRequest.swift new file mode 100644 index 000000000..5f26c4050 --- /dev/null +++ b/dev/grpc-dev-tool/Sources/grpc-dev-tool/Subcommands/GenerateJSON/JSONCodeGeneratorRequest.swift @@ -0,0 +1,135 @@ +/* + * Copyright 2025, 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 Foundation + +struct JSONCodeGeneratorRequest: Codable { + /// The service to generate. + var service: ServiceSchema + + /// Configuration for the generation. + var config: GeneratorConfig + + init(service: ServiceSchema, config: GeneratorConfig) { + self.service = service + self.config = config + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.service = try container.decode(ServiceSchema.self, forKey: .service) + self.config = try container.decodeIfPresent(GeneratorConfig.self, forKey: .config) ?? .defaults + } +} + +struct ServiceSchema: Codable { + var name: String + var methods: [Method] + + struct Method: Codable { + var name: String + var input: String + var output: String + var kind: Kind + + enum Kind: String, Codable { + case unary = "unary" + case clientStreaming = "client_streaming" + case serverStreaming = "server_streaming" + case bidiStreaming = "bidi_streaming" + + var streamsInput: Bool { + switch self { + case .unary, .serverStreaming: + return false + case .clientStreaming, .bidiStreaming: + return true + } + } + + var streamsOutput: Bool { + switch self { + case .unary, .clientStreaming: + return false + case .serverStreaming, .bidiStreaming: + return true + } + } + } + } +} + +struct GeneratorConfig: Codable { + enum AccessLevel: String, Codable { + case `internal` + case `package` + + var capitalized: String { + switch self { + case .internal: + return "Internal" + case .package: + return "Package" + } + } + } + + var generateClient: Bool + var generateServer: Bool + var accessLevel: AccessLevel + var accessLevelOnImports: Bool + + static var defaults: Self { + GeneratorConfig( + generateClient: true, + generateServer: true, + accessLevel: .internal, + accessLevelOnImports: false + ) + } + + init( + generateClient: Bool, + generateServer: Bool, + accessLevel: AccessLevel, + accessLevelOnImports: Bool + ) { + self.generateClient = generateClient + self.generateServer = generateServer + self.accessLevel = accessLevel + self.accessLevelOnImports = accessLevelOnImports + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let defaults = Self.defaults + + let generateClient = try container.decodeIfPresent(Bool.self, forKey: .generateClient) + self.generateClient = generateClient ?? defaults.generateClient + + let generateServer = try container.decodeIfPresent(Bool.self, forKey: .generateServer) + self.generateServer = generateServer ?? defaults.generateServer + + let accessLevel = try container.decodeIfPresent(AccessLevel.self, forKey: .accessLevel) + self.accessLevel = accessLevel ?? defaults.accessLevel + + let accessLevelOnImports = try container.decodeIfPresent( + Bool.self, + forKey: .accessLevelOnImports + ) + self.accessLevelOnImports = accessLevelOnImports ?? defaults.accessLevelOnImports + } +} From eea6b49e08f66a33711b0b957a2f404bb6397fb7 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 31 Jan 2025 15:35:19 +0000 Subject: [PATCH 558/580] Fix Metadata's description for prettier printing (#2185) This PR changes `Metadata`'s description to include quotes around string values. From this: ``` [("some-key", some-value)] ``` to this: ``` [("some-key", "some-value")] ``` --------- Co-authored-by: George Barnett --- Sources/GRPCCore/Metadata.swift | 19 +++++++++++++++- Tests/GRPCCoreTests/MetadataTests.swift | 30 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index dfc095e1e..23218b4a1 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -494,7 +494,13 @@ extension Metadata.Value: ExpressibleByArrayLiteral { extension Metadata: CustomStringConvertible { public var description: String { - String(describing: self.map({ ($0.key, $0.value) })) + if self.isEmpty { + return "[:]" + } else { + let elements = self.map { "\(String(reflecting: $0.key)): \(String(reflecting: $0.value))" } + .joined(separator: ", ") + return "[\(elements)]" + } } } @@ -508,3 +514,14 @@ extension Metadata.Value: CustomStringConvertible { } } } + +extension Metadata.Value: CustomDebugStringConvertible { + public var debugDescription: String { + switch self { + case .string(let stringValue): + return String(reflecting: stringValue) + case .binary(let binaryValue): + return String(reflecting: binaryValue) + } + } +} diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index 68c3df85c..617d2263d 100644 --- a/Tests/GRPCCoreTests/MetadataTests.swift +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -314,4 +314,34 @@ struct MetadataTests { ) } } + + @Suite("Description") + struct Description { + let metadata: Metadata = [ + "key1": "value1", + "key2": "value2", + "key-bin": .binary([1, 2, 3]), + ] + + @Test("Metadata") + func describeMetadata() async throws { + #expect("\(self.metadata)" == #"["key1": "value1", "key2": "value2", "key-bin": [1, 2, 3]]"#) + } + + @Test("Metadata.Value") + func describeMetadataValue() async throws { + for (key, value) in self.metadata { + switch key { + case "key1": + #expect("\(value)" == "value1") + case "key2": + #expect("\(value)" == "value2") + case "key-bin": + #expect("\(value)" == "[1, 2, 3]") + default: + Issue.record("Should not have reached this point") + } + } + } + } } From c51d91bc677469fa1816ec321f693215130c82a9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 31 Jan 2025 16:09:29 +0000 Subject: [PATCH 559/580] Don't build examples twice (#2189) Motivation: The CI for examples builds them in the setup stage and the actual run stages. That's just a silly oversight. Modifications: - Don't build them during set. Result: Examples don't build twice in CI --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 29a9bd5e3..34fbceeea 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -42,7 +42,7 @@ jobs: MATRIX_LINUX_5_9_ENABLED: false MATRIX_LINUX_5_10_ENABLED: false MATRIX_LINUX_COMMAND: "./dev/build-examples.sh" - MATRIX_LINUX_SETUP_COMMAND: "apt update && apt install -y protobuf-compiler && ./dev/build-examples.sh" + MATRIX_LINUX_SETUP_COMMAND: "apt update && apt install -y protobuf-compiler" examples-matrix: name: Examples From e31dd430e34cb82428751cdf7d2c014fad956796 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 4 Feb 2025 12:59:11 +0000 Subject: [PATCH 560/580] Add note about minimum deployment versions for Apple platforms (#2190) Motivation: The compatibility doc doesn't mention minimum deployment versions for Apple platforms. It should. Modifications: - Add minimum deployment versions. Result: Better docs. --- .../Documentation.docc/Articles/Compatibility.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md b/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md index 9748c0d0e..5862c17ca 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md @@ -20,3 +20,14 @@ gRPC Swift aims to support the same platforms as Swift project. These are listed on [swift.org](https://www.swift.org/platform-support/). The only known unsupported platform from that list is currently Windows. + +Apple platforms have a minimum deployment version. These are as follows for gRPC +Swift: + +| OS | Minimum Deployment Version | +|----------|----------------------------| +| macOS | 15.0 | +| iOS | 18.0 | +| tvOS | 18.0 | +| watchOS | 11.0 | +| visionOS | 2.0 | From 362efe542ca7f4f1eca60c598ab2308020194eac Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 10 Feb 2025 10:14:10 +0000 Subject: [PATCH 561/580] Ensure imports have explicit access levels (#2192) Motivation: The imports should all have access level set explicitly. Some were missing. Modifications: - Add a script and CI to check for this - Add a few missing access levels Result: Better consistency --- .github/workflows/soundness.yml | 14 +++++++++ .../ServerContext+RPCCancellationHandle.swift | 2 +- Sources/GRPCInProcessTransport/Syscalls.swift | 6 ++-- dev/check-imports.sh | 29 +++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100755 dev/check-imports.sh diff --git a/.github/workflows/soundness.yml b/.github/workflows/soundness.yml index 35a50d70a..cd3f196aa 100644 --- a/.github/workflows/soundness.yml +++ b/.github/workflows/soundness.yml @@ -35,3 +35,17 @@ jobs: - name: Run soundness checks run: | ./dev/check-generated-code.sh + + check-imports: + name: Check imports have access level + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Mark the workspace as safe + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: Check import access level + run: | + ./dev/check-imports.sh diff --git a/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift index 5e0f63367..f958978b8 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -import Synchronization +private import Synchronization extension ServerContext { @TaskLocal diff --git a/Sources/GRPCInProcessTransport/Syscalls.swift b/Sources/GRPCInProcessTransport/Syscalls.swift index 96737a791..df49a49f2 100644 --- a/Sources/GRPCInProcessTransport/Syscalls.swift +++ b/Sources/GRPCInProcessTransport/Syscalls.swift @@ -15,11 +15,11 @@ */ #if canImport(Darwin) -import Darwin +private import Darwin #elseif canImport(Glibc) -import Glibc +private import Glibc #elseif canImport(Musl) -import Musl +private import Musl #endif enum System { diff --git a/dev/check-imports.sh b/dev/check-imports.sh new file mode 100755 index 000000000..1ff7fd982 --- /dev/null +++ b/dev/check-imports.sh @@ -0,0 +1,29 @@ +#!/bin/bash +## Copyright 2025, 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. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +root="${here}/.." + +log "Checking all imports have an access level" +if grep -r "^import " --exclude-dir="Documentation.docc" "${root}/Sources"; then + # Matches are bad! + exit 1 +else + exit 0 +fi From 85f0fc74c953b5e6bcff37a92a425d5137346797 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 17 Feb 2025 10:03:02 +0000 Subject: [PATCH 562/580] Add an example using swift-service-lifecycle (#2195) --- Examples/service-lifecycle/Package.swift | 42 ++++++++++ Examples/service-lifecycle/README.md | 41 ++++++++++ .../Sources/GreetingService.swift | 80 +++++++++++++++++++ .../Sources/LifecycleExample.swift | 74 +++++++++++++++++ .../grpc-swift-proto-generator-config.json | 7 ++ .../Sources/Protos/helloworld.proto | 1 + 6 files changed, 245 insertions(+) create mode 100644 Examples/service-lifecycle/Package.swift create mode 100644 Examples/service-lifecycle/README.md create mode 100644 Examples/service-lifecycle/Sources/GreetingService.swift create mode 100644 Examples/service-lifecycle/Sources/LifecycleExample.swift create mode 100644 Examples/service-lifecycle/Sources/Protos/grpc-swift-proto-generator-config.json create mode 120000 Examples/service-lifecycle/Sources/Protos/helloworld.proto diff --git a/Examples/service-lifecycle/Package.swift b/Examples/service-lifecycle/Package.swift new file mode 100644 index 000000000..c2d239c21 --- /dev/null +++ b/Examples/service-lifecycle/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version:6.0 +/* + * Copyright 2025, 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 PackageDescription + +let package = Package( + name: "service-lifecycle", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), + .package(url: "https://github.com/grpc/grpc-swift-extras", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "service-lifecycle", + dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), + .product(name: "GRPCInProcessTransport", package: "grpc-swift"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "GRPCServiceLifecycle", package: "grpc-swift-extras"), + ], + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] + ) + ] +) diff --git a/Examples/service-lifecycle/README.md b/Examples/service-lifecycle/README.md new file mode 100644 index 000000000..db1b12b27 --- /dev/null +++ b/Examples/service-lifecycle/README.md @@ -0,0 +1,41 @@ +# Service Lifecycle + +This example demonstrates gRPC Swift's integration with Swift Service Lifecycle +which is provided by the gRPC Swift Extras package. + +## Overview + +A "service-lifecycle" command line tool that uses generated stubs for a +'greeter' service starts an in-process client and server orchestrated using +Swift Service Lifecycle. The client makes requests against the server which +periodically changes its greeting. + +## Prerequisites + +You must have the Protocol Buffers compiler (`protoc`) installed. You can find +the instructions for doing this in the [gRPC Swift Protobuf documentation][0]. +The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`, +this is to let the build system know where `protoc` is located so that it can +generate stubs for you. You can read more about it in the [gRPC Swift Protobuf +documentation][1]. + +## Usage + +Build and run the server using the CLI: + +```console +$ PROTOC_PATH=$(which protoc) swift run service-lifecycle +Здравствуйте, request-1! +नमस्ते, request-2! +你好, request-3! +French, request-4! +Olá, request-5! +Hola, request-6! +Hello, request-7! +Hello, request-8! +नमस्ते, request-9! +Hello, request-10! +``` + +[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc +[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Examples/service-lifecycle/Sources/GreetingService.swift b/Examples/service-lifecycle/Sources/GreetingService.swift new file mode 100644 index 000000000..8ca4332fb --- /dev/null +++ b/Examples/service-lifecycle/Sources/GreetingService.swift @@ -0,0 +1,80 @@ +/* + * Copyright 2025, 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 ServiceLifecycle +import Synchronization + +/// Implements the "Hello World" gRPC service but modifies the greeting on a timer. +/// +/// The service conforms to the 'ServiceLifecycle.Service' and uses its 'run()' method +/// to execute the run loop which updates the greeting. +final class GreetingService { + private let updateInterval: Duration + private let currentGreetingIndex: Mutex + private let greetings: [String] = [ + "Hello", + "你好", + "नमस्ते", + "Hola", + "French", + "Olá", + "Здравствуйте", + "こんにちは", + "Ciao", + ] + + private func personalizedGreeting(forName name: String) -> String { + let index = self.currentGreetingIndex.withLock { $0 } + return "\(self.greetings[index]), \(name)!" + } + + private func periodicallyUpdateGreeting() async throws { + while !Task.isShuttingDownGracefully { + try await Task.sleep(for: self.updateInterval) + + // Increment the greeting index. + self.currentGreetingIndex.withLock { index in + // '!' is fine; greetings is non-empty. + index = self.greetings.indices.randomElement()! + } + } + } + + init(updateInterval: Duration) { + // '!' is fine; greetings is non-empty. + let index = self.greetings.indices.randomElement()! + self.currentGreetingIndex = Mutex(index) + self.updateInterval = updateInterval + } +} + +extension GreetingService: Helloworld_Greeter.SimpleServiceProtocol { + func sayHello( + request: Helloworld_HelloRequest, + context: ServerContext + ) async throws -> Helloworld_HelloReply { + return .with { + $0.message = self.personalizedGreeting(forName: request.name) + } + } +} + +extension GreetingService: Service { + func run() async throws { + try await self.periodicallyUpdateGreeting() + } +} diff --git a/Examples/service-lifecycle/Sources/LifecycleExample.swift b/Examples/service-lifecycle/Sources/LifecycleExample.swift new file mode 100644 index 000000000..75a8573a0 --- /dev/null +++ b/Examples/service-lifecycle/Sources/LifecycleExample.swift @@ -0,0 +1,74 @@ +/* + * Copyright 2025, 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 GRPCInProcessTransport +import GRPCServiceLifecycle +import Logging +import ServiceLifecycle + +@main +struct LifecycleExample { + static func main() async throws { + // Create the gRPC service. It periodically changes the greeting returned to the client. + // It also conforms to 'ServiceLifecycle.Service' and uses the 'run()' method to perform + // the updates. + // + // A more realistic service may use the run method to maintain a connection to an upstream + // service or database. + let greetingService = GreetingService(updateInterval: .microseconds(250)) + + // Create the client and server using the in-process transport (which is used here for + // simplicity.) + let inProcess = InProcessTransport() + let server = GRPCServer(transport: inProcess.server, services: [greetingService]) + let client = GRPCClient(transport: inProcess.client) + + // Configure the service group with the services. They're started in the order they're listed + // and shutdown in reverse order. + let serviceGroup = ServiceGroup( + services: [ + greetingService, + server, + client, + ], + logger: Logger(label: "io.grpc.examples.service-lifecycle") + ) + + try await withThrowingDiscardingTaskGroup { group in + // Run the service group in a task group. This isn't typically required but is here in + // order to make requests using the client while the service group is running. + group.addTask { + try await serviceGroup.run() + } + + // Make some requests, pausing between each to give the server a chance to update + // the greeting. + let greeter = Helloworld_Greeter.Client(wrapping: client) + for request in 1 ... 10 { + let reply = try await greeter.sayHello(.with { $0.name = "request-\(request)" }) + print(reply.message) + + // Sleep for a moment. + let waitTime = Duration.milliseconds((50 ... 400).randomElement()!) + try await Task.sleep(for: waitTime) + } + + // Finally, shutdown the service group gracefully. + await serviceGroup.triggerGracefulShutdown() + } + } +} diff --git a/Examples/service-lifecycle/Sources/Protos/grpc-swift-proto-generator-config.json b/Examples/service-lifecycle/Sources/Protos/grpc-swift-proto-generator-config.json new file mode 100644 index 000000000..e6dda31fb --- /dev/null +++ b/Examples/service-lifecycle/Sources/Protos/grpc-swift-proto-generator-config.json @@ -0,0 +1,7 @@ +{ + "generate": { + "clients": true, + "servers": true, + "messages": true + } +} diff --git a/Examples/service-lifecycle/Sources/Protos/helloworld.proto b/Examples/service-lifecycle/Sources/Protos/helloworld.proto new file mode 120000 index 000000000..f4684af4f --- /dev/null +++ b/Examples/service-lifecycle/Sources/Protos/helloworld.proto @@ -0,0 +1 @@ +../../../../dev/protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file From d41212860db01eb604ed2b3cb1a9ded9176c8356 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 17 Feb 2025 11:00:20 +0000 Subject: [PATCH 563/580] Fix greeting in service-lifecycle example (#2197) --- Examples/service-lifecycle/README.md | 2 +- Examples/service-lifecycle/Sources/GreetingService.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/service-lifecycle/README.md b/Examples/service-lifecycle/README.md index db1b12b27..1f69fc4db 100644 --- a/Examples/service-lifecycle/README.md +++ b/Examples/service-lifecycle/README.md @@ -28,7 +28,7 @@ $ PROTOC_PATH=$(which protoc) swift run service-lifecycle Здравствуйте, request-1! नमस्ते, request-2! 你好, request-3! -French, request-4! +Bonjour, request-4! Olá, request-5! Hola, request-6! Hello, request-7! diff --git a/Examples/service-lifecycle/Sources/GreetingService.swift b/Examples/service-lifecycle/Sources/GreetingService.swift index 8ca4332fb..56f96c0f4 100644 --- a/Examples/service-lifecycle/Sources/GreetingService.swift +++ b/Examples/service-lifecycle/Sources/GreetingService.swift @@ -30,7 +30,7 @@ final class GreetingService { "你好", "नमस्ते", "Hola", - "French", + "Bonjour", "Olá", "Здравствуйте", "こんにちは", From f9fbd683afa0431603cb69c042fc2492f0ab0f1a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 28 Feb 2025 13:46:29 +0000 Subject: [PATCH 564/580] Add code gen config for the module name (#2201) Modifications: - Add config to the code generator allowing a different module name to be specified. - Remove unused code Result: The grpc module name can be specified in generated code --- Sources/GRPCCodeGen/CodeGenerator.swift | 6 +- Sources/GRPCCodeGen/Internal/Namer.swift | 133 ++++++++++++++++++ .../Internal/StructuredSwift+Client.swift | 95 ++++++++----- .../Internal/StructuredSwift+Server.swift | 84 +++++++---- .../StructuredSwift+ServiceMetadata.swift | 66 ++++++--- .../Internal/StructuredSwift+Types.swift | 91 ------------ .../Translator/ClientCodeTranslator.swift | 20 ++- .../IDLToStructuredSwiftTranslator.swift | 20 ++- .../Translator/MetadataTranslator.swift | 5 +- .../Translator/ServerCodeTranslator.swift | 17 ++- .../Translator/SpecializedTranslator.swift | 59 -------- .../Internal/Translator/Translator.swift | 37 ----- .../StructuredSwift+ImportTests.swift | 26 +++- ...uredSwiftTranslatorSnippetBasedTests.swift | 107 ++++++++++++-- 14 files changed, 458 insertions(+), 308 deletions(-) create mode 100644 Sources/GRPCCodeGen/Internal/Namer.swift delete mode 100644 Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift delete mode 100644 Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift delete mode 100644 Sources/GRPCCodeGen/Internal/Translator/Translator.swift diff --git a/Sources/GRPCCodeGen/CodeGenerator.swift b/Sources/GRPCCodeGen/CodeGenerator.swift index adac1a6c9..25d17a6cf 100644 --- a/Sources/GRPCCodeGen/CodeGenerator.swift +++ b/Sources/GRPCCodeGen/CodeGenerator.swift @@ -40,6 +40,8 @@ public struct CodeGenerator: Sendable { public var client: Bool /// Whether or not server code should be generated. public var server: Bool + /// The name of the core gRPC module. + public var grpcCoreModuleName: String /// Creates a new configuration. /// @@ -61,6 +63,7 @@ public struct CodeGenerator: Sendable { self.indentation = indentation self.client = client self.server = server + self.grpcCoreModuleName = "GRPCCore" } /// The possible access levels for the generated code. @@ -96,7 +99,8 @@ public struct CodeGenerator: Sendable { accessLevel: self.config.accessLevel, accessLevelOnImports: self.config.accessLevelOnImports, client: self.config.client, - server: self.config.server + server: self.config.server, + grpcCoreModuleName: self.config.grpcCoreModuleName ) let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) diff --git a/Sources/GRPCCodeGen/Internal/Namer.swift b/Sources/GRPCCodeGen/Internal/Namer.swift new file mode 100644 index 000000000..560d0b579 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Namer.swift @@ -0,0 +1,133 @@ +/* + * 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. + */ + +package struct Namer: Sendable, Hashable { + let grpcCore: String + + package init(grpcCore: String = "GRPCCore") { + self.grpcCore = grpcCore + } + + private func grpcCore(_ typeName: String) -> ExistingTypeDescription { + return .member([self.grpcCore, typeName]) + } + + private func requestResponse( + for type: String?, + isRequest: Bool, + isStreaming: Bool, + isClient: Bool + ) -> ExistingTypeDescription { + let prefix = isStreaming ? "Streaming" : "" + let peer = isClient ? "Client" : "Server" + let kind = isRequest ? "Request" : "Response" + let baseType = self.grpcCore(prefix + peer + kind) + + if let type = type { + return .generic(wrapper: baseType, wrapped: .member(type)) + } else { + return baseType + } + } + + func literalNamespacedType(_ type: String) -> String { + return self.grpcCore + "." + type + } + + func serverRequest(forType type: String?, isStreaming: Bool) -> ExistingTypeDescription { + return self.requestResponse( + for: type, + isRequest: true, + isStreaming: isStreaming, + isClient: false + ) + } + + func serverResponse(forType type: String?, isStreaming: Bool) -> ExistingTypeDescription { + return self.requestResponse( + for: type, + isRequest: false, + isStreaming: isStreaming, + isClient: false + ) + } + + func clientRequest(forType type: String?, isStreaming: Bool) -> ExistingTypeDescription { + return self.requestResponse( + for: type, + isRequest: true, + isStreaming: isStreaming, + isClient: true + ) + } + + func clientResponse(forType type: String?, isStreaming: Bool) -> ExistingTypeDescription { + return self.requestResponse( + for: type, + isRequest: false, + isStreaming: isStreaming, + isClient: true + ) + } + + var serverContext: ExistingTypeDescription { + self.grpcCore("ServerContext") + } + + func rpcRouter(genericOver type: String) -> ExistingTypeDescription { + .generic(wrapper: self.grpcCore("RPCRouter"), wrapped: .member(type)) + } + + var serviceDescriptor: ExistingTypeDescription { + self.grpcCore("ServiceDescriptor") + } + + var methodDescriptor: ExistingTypeDescription { + self.grpcCore("MethodDescriptor") + } + + func serializer(forType type: String) -> ExistingTypeDescription { + .generic(wrapper: self.grpcCore("MessageSerializer"), wrapped: .member(type)) + } + + func deserializer(forType type: String) -> ExistingTypeDescription { + .generic(wrapper: self.grpcCore("MessageDeserializer"), wrapped: .member(type)) + } + + func rpcWriter(forType type: String) -> ExistingTypeDescription { + .generic(wrapper: self.grpcCore("RPCWriter"), wrapped: .member(type)) + } + + func rpcAsyncSequence(forType type: String) -> ExistingTypeDescription { + .generic( + wrapper: self.grpcCore("RPCAsyncSequence"), + wrapped: .member(type), + .any(.member(["Swift", "Error"])) + ) + } + + var callOptions: ExistingTypeDescription { + self.grpcCore("CallOptions") + } + + var metadata: ExistingTypeDescription { + self.grpcCore("Metadata") + } + + func grpcClient(genericOver transport: String) -> ExistingTypeDescription { + .generic(wrapper: self.grpcCore("GRPCClient"), wrapped: [.member(transport)]) + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift index 511724702..99fddbede 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -46,7 +46,8 @@ extension FunctionSignatureDescription { streamingInput: Bool, streamingOutput: Bool, includeDefaults: Bool, - includeSerializers: Bool + includeSerializers: Bool, + namer: Namer = Namer() ) -> Self { var signature = FunctionSignatureDescription( accessModifier: accessLevel, @@ -61,7 +62,7 @@ extension FunctionSignatureDescription { signature.parameters.append( ParameterDescription( label: "request", - type: .clientRequest(forType: input, streaming: streamingInput) + type: namer.clientRequest(forType: input, isStreaming: streamingInput) ) ) @@ -70,14 +71,14 @@ extension FunctionSignatureDescription { ParameterDescription( label: "serializer", // Type is optional, so be explicit about which 'some' to use - type: ExistingTypeDescription.some(.serializer(forType: input)) + type: ExistingTypeDescription.some(namer.serializer(forType: input)) ) ) signature.parameters.append( ParameterDescription( label: "deserializer", // Type is optional, so be explicit about which 'some' to use - type: ExistingTypeDescription.some(.deserializer(forType: output)) + type: ExistingTypeDescription.some(namer.deserializer(forType: output)) ) ) } @@ -85,7 +86,7 @@ extension FunctionSignatureDescription { signature.parameters.append( ParameterDescription( label: "options", - type: .callOptions, + type: namer.callOptions, defaultValue: includeDefaults ? .memberAccess(.dot("defaults")) : nil ) ) @@ -98,7 +99,7 @@ extension FunctionSignatureDescription { ClosureSignatureDescription( parameters: [ ParameterDescription( - type: .clientResponse(forType: output, streaming: streamingOutput) + type: namer.clientResponse(forType: output, isStreaming: streamingOutput) ) ], keywords: [.async, .throws], @@ -141,7 +142,8 @@ extension FunctionDescription { streamingInput: Bool, streamingOutput: Bool, serializer: Expression, - deserializer: Expression + deserializer: Expression, + namer: Namer = Namer() ) -> Self { FunctionDescription( signature: .clientMethod( @@ -152,7 +154,8 @@ extension FunctionDescription { streamingInput: streamingInput, streamingOutput: streamingOutput, includeDefaults: true, - includeSerializers: false + includeSerializers: false, + namer: namer ), body: [ .expression( @@ -202,7 +205,8 @@ extension ProtocolDescription { static func clientProtocol( accessLevel: AccessModifier? = nil, name: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> Self { ProtocolDescription( accessModifier: accessLevel, @@ -219,7 +223,8 @@ extension ProtocolDescription { streamingInput: method.isInputStreaming, streamingOutput: method.isOutputStreaming, includeDefaults: false, - includeSerializers: true + includeSerializers: true, + namer: namer ) ) ) @@ -245,6 +250,7 @@ extension ExtensionDescription { accessLevel: AccessModifier? = nil, name: String, methods: [MethodDescriptor], + namer: Namer = Namer(), serializer: (String) -> String, deserializer: (String) -> String ) -> Self { @@ -262,7 +268,8 @@ extension ExtensionDescription { streamingInput: method.isInputStreaming, streamingOutput: method.isOutputStreaming, serializer: .identifierPattern(serializer(method.inputType)), - deserializer: .identifierPattern(deserializer(method.outputType)) + deserializer: .identifierPattern(deserializer(method.outputType)), + namer: namer ) ) ) @@ -288,7 +295,8 @@ extension FunctionSignatureDescription { input: String, output: String, streamingInput: Bool, - streamingOutput: Bool + streamingOutput: Bool, + namer: Namer = Namer() ) -> Self { var signature = FunctionSignatureDescription( accessModifier: accessLevel, @@ -310,7 +318,7 @@ extension FunctionSignatureDescription { signature.parameters.append( ParameterDescription( label: "metadata", - type: .metadata, + type: namer.metadata, defaultValue: .literal(.dictionary([])) ) ) @@ -319,7 +327,7 @@ extension FunctionSignatureDescription { signature.parameters.append( ParameterDescription( label: "options", - type: .callOptions, + type: namer.callOptions, defaultValue: .dot("defaults") ) ) @@ -331,7 +339,7 @@ extension FunctionSignatureDescription { name: "producer", type: .closure( ClosureSignatureDescription( - parameters: [ParameterDescription(type: .rpcWriter(forType: input))], + parameters: [ParameterDescription(type: namer.rpcWriter(forType: input))], keywords: [.async, .throws], returnType: .identifierPattern("Void"), sendable: true, @@ -350,7 +358,7 @@ extension FunctionSignatureDescription { ClosureSignatureDescription( parameters: [ ParameterDescription( - type: .clientResponse(forType: output, streaming: streamingOutput) + type: namer.clientResponse(forType: output, isStreaming: streamingOutput) ) ], keywords: [.async, .throws], @@ -382,7 +390,8 @@ extension [CodeBlock] { static func clientMethodExploded( name: String, input: String, - streamingInput: Bool + streamingInput: Bool, + namer: Namer = Namer() ) -> Self { func arguments(streaming: Bool) -> [FunctionArgumentDescription] { let metadata = FunctionArgumentDescription( @@ -414,7 +423,7 @@ extension [CodeBlock] { left: .identifierPattern("request"), right: .functionCall( calledExpression: .identifierType( - .clientRequest(forType: input, streaming: streamingInput) + namer.clientRequest(forType: input, isStreaming: streamingInput) ), arguments: arguments(streaming: streamingInput) ) @@ -471,7 +480,8 @@ extension FunctionDescription { input: String, output: String, streamingInput: Bool, - streamingOutput: Bool + streamingOutput: Bool, + namer: Namer = Namer() ) -> Self { FunctionDescription( signature: .clientMethodExploded( @@ -480,9 +490,15 @@ extension FunctionDescription { input: input, output: output, streamingInput: streamingInput, - streamingOutput: streamingOutput + streamingOutput: streamingOutput, + namer: namer ), - body: .clientMethodExploded(name: name, input: input, streamingInput: streamingInput) + body: .clientMethodExploded( + name: name, + input: input, + streamingInput: streamingInput, + namer: namer + ) ) } } @@ -496,7 +512,8 @@ extension ExtensionDescription { static func explodedClientMethods( accessLevel: AccessModifier? = nil, on extensionName: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> ExtensionDescription { return ExtensionDescription( onType: extensionName, @@ -510,7 +527,8 @@ extension ExtensionDescription { input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, - streamingOutput: method.isOutputStreaming + streamingOutput: method.isOutputStreaming, + namer: namer ) ) ) @@ -539,7 +557,8 @@ extension FunctionDescription { serviceEnum: String, methodEnum: String, streamingInput: Bool, - streamingOutput: Bool + streamingOutput: Bool, + namer: Namer = Namer() ) -> Self { let underlyingMethod: String switch (streamingInput, streamingOutput) { @@ -560,21 +579,21 @@ extension FunctionDescription { parameters: [ ParameterDescription( label: "request", - type: .clientRequest(forType: input, streaming: streamingInput) + type: namer.clientRequest(forType: input, isStreaming: streamingInput) ), ParameterDescription( label: "serializer", // Be explicit: 'type' is optional and '.some' resolves to Optional.some by default. - type: ExistingTypeDescription.some(.serializer(forType: input)) + type: ExistingTypeDescription.some(namer.serializer(forType: input)) ), ParameterDescription( label: "deserializer", // Be explicit: 'type' is optional and '.some' resolves to Optional.some by default. - type: ExistingTypeDescription.some(.deserializer(forType: output)) + type: ExistingTypeDescription.some(namer.deserializer(forType: output)) ), ParameterDescription( label: "options", - type: .callOptions, + type: namer.callOptions, defaultValue: .dot("defaults") ), ParameterDescription( @@ -584,7 +603,7 @@ extension FunctionDescription { ClosureSignatureDescription( parameters: [ ParameterDescription( - type: .clientResponse(forType: output, streaming: streamingOutput) + type: namer.clientResponse(forType: output, isStreaming: streamingOutput) ) ], keywords: [.async, .throws], @@ -660,30 +679,31 @@ extension StructDescription { name: String, serviceEnum: String, clientProtocol: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> Self { - StructDescription( + return StructDescription( accessModifier: accessLevel, name: name, generics: [.member("Transport")], conformances: [clientProtocol], whereClause: WhereClause( - requirements: [.conformance("Transport", "GRPCCore.ClientTransport")] + requirements: [.conformance("Transport", namer.literalNamespacedType("ClientTransport"))] ), members: [ .variable( accessModifier: .private, kind: .let, left: "client", - type: .grpcClient(genericOver: "Transport") + type: namer.grpcClient(genericOver: "Transport") ), .commentable( .preFormatted( """ - /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// Creates a new client wrapping the provided `\(namer.literalNamespacedType("GRPCClient"))`. /// /// - Parameters: - /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + /// - client: A `\(namer.literalNamespacedType("GRPCClient"))` providing a communication channel to the service. """ ), .function( @@ -693,7 +713,7 @@ extension StructDescription { ParameterDescription( label: "wrapping", name: "client", - type: .grpcClient( + type: namer.grpcClient( genericOver: "Transport" ) ) @@ -722,7 +742,8 @@ extension StructDescription { serviceEnum: serviceEnum, methodEnum: method.name.typeName, streamingInput: method.isInputStreaming, - streamingOutput: method.isOutputStreaming + streamingOutput: method.isOutputStreaming, + namer: namer ) ) ) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index a3525c13e..a012f0b67 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -27,7 +27,8 @@ extension FunctionSignatureDescription { input: String, output: String, streamingInput: Bool, - streamingOutput: Bool + streamingOutput: Bool, + namer: Namer = Namer() ) -> Self { return FunctionSignatureDescription( accessModifier: accessLevel, @@ -35,12 +36,14 @@ extension FunctionSignatureDescription { parameters: [ ParameterDescription( label: "request", - type: .serverRequest(forType: input, streaming: streamingInput) + type: namer.serverRequest(forType: input, isStreaming: streamingInput) ), - ParameterDescription(label: "context", type: .serverContext), + ParameterDescription(label: "context", type: namer.serverContext), ], keywords: [.async, .throws], - returnType: .identifierType(.serverResponse(forType: output, streaming: streamingOutput)) + returnType: .identifierType( + namer.serverResponse(forType: output, isStreaming: streamingOutput) + ) ) } } @@ -54,7 +57,8 @@ extension ProtocolDescription { static func streamingService( accessLevel: AccessModifier? = nil, name: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> Self { func docs(for method: MethodDescriptor) -> String { let summary = """ @@ -77,7 +81,7 @@ extension ProtocolDescription { return ProtocolDescription( accessModifier: accessLevel, name: name, - conformances: ["GRPCCore.RegistrableRPCService"], + conformances: [namer.literalNamespacedType("RegistrableRPCService")], members: methods.map { method in .commentable( .preFormatted(docs(for: method)), @@ -87,7 +91,8 @@ extension ProtocolDescription { input: method.inputType, output: method.outputType, streamingInput: true, - streamingOutput: true + streamingOutput: true, + namer: namer ) ) ) @@ -109,6 +114,7 @@ extension ExtensionDescription { on extensionName: String, serviceNamespace: String, methods: [MethodDescriptor], + namer: Namer = Namer(), serializer: (String) -> String, deserializer: (String) -> String ) -> Self { @@ -120,6 +126,7 @@ extension ExtensionDescription { accessLevel: accessLevel, serviceNamespace: serviceNamespace, methods: methods, + namer: namer, serializer: serializer, deserializer: deserializer ) @@ -139,7 +146,8 @@ extension ProtocolDescription { accessLevel: AccessModifier? = nil, name: String, streamingProtocol: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> Self { func docs(for method: MethodDescriptor) -> String { let summary = """ @@ -186,7 +194,8 @@ extension ProtocolDescription { input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, - streamingOutput: method.isOutputStreaming + streamingOutput: method.isOutputStreaming, + namer: namer ) ) ) @@ -305,6 +314,7 @@ extension FunctionDescription { accessLevel: AccessModifier? = nil, serviceNamespace: String, methods: [MethodDescriptor], + namer: Namer = Namer(), serializer: (String) -> String, deserializer: (String) -> String ) -> Self { @@ -316,13 +326,13 @@ extension FunctionDescription { ParameterDescription( label: "with", name: "router", - type: .rpcRouter(genericOver: "Transport"), + type: namer.rpcRouter(genericOver: "Transport"), `inout`: true ) ], whereClause: WhereClause( requirements: [ - .conformance("Transport", "GRPCCore.ServerTransport") + .conformance("Transport", namer.literalNamespacedType("ServerTransport")) ] ), body: methods.map { method in @@ -359,7 +369,8 @@ extension FunctionDescription { input: String, output: String, streamingInput: Bool, - streamingOutput: Bool + streamingOutput: Bool, + namer: Namer = Namer() ) -> FunctionDescription { let signature: FunctionSignatureDescription = .serverMethod( accessLevel: accessLevel, @@ -368,7 +379,8 @@ extension FunctionDescription { output: output, // This method converts from the fully streamed version to the specified version. streamingInput: true, - streamingOutput: true + streamingOutput: true, + namer: namer ) // Call the underlying function. @@ -385,7 +397,9 @@ extension FunctionDescription { expression: streamingInput ? .identifierPattern("request") : .functionCall( - calledExpression: .identifierType(.serverRequest(forType: nil, streaming: false)), + calledExpression: .identifierType( + namer.serverRequest(forType: nil, isStreaming: false) + ), arguments: [ FunctionArgumentDescription( label: "stream", @@ -420,7 +434,7 @@ extension FunctionDescription { expression: streamingOutput ? .identifierPattern("response") : .functionCall( - calledExpression: .identifierType(.serverResponse(forType: nil, streaming: true)), + calledExpression: .identifierType(namer.serverResponse(forType: nil, isStreaming: true)), arguments: [ FunctionArgumentDescription( label: "single", @@ -456,7 +470,8 @@ extension ExtensionDescription { static func streamingServiceProtocolDefaultImplementation( accessModifier: AccessModifier? = nil, on extensionName: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> Self { return ExtensionDescription( onType: extensionName, @@ -472,7 +487,8 @@ extension ExtensionDescription { input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, - streamingOutput: method.isOutputStreaming + streamingOutput: method.isOutputStreaming, + namer: namer ) ) } @@ -501,20 +517,26 @@ extension FunctionSignatureDescription { input: String, output: String, streamingInput: Bool, - streamingOutput: Bool + streamingOutput: Bool, + namer: Namer = Namer() ) -> Self { var parameters: [ParameterDescription] = [ ParameterDescription( label: "request", - type: streamingInput ? .rpcAsyncSequence(forType: input) : .member(input) + type: streamingInput ? namer.rpcAsyncSequence(forType: input) : .member(input) ) ] if streamingOutput { - parameters.append(ParameterDescription(label: "response", type: .rpcWriter(forType: output))) + parameters.append( + ParameterDescription( + label: "response", + type: namer.rpcWriter(forType: output) + ) + ) } - parameters.append(ParameterDescription(label: "context", type: .serverContext)) + parameters.append(ParameterDescription(label: "context", type: namer.serverContext)) return FunctionSignatureDescription( accessModifier: accessLevel, @@ -536,7 +558,8 @@ extension ProtocolDescription { accessModifier: AccessModifier? = nil, name: String, serviceProtocol: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> Self { func docs(for method: MethodDescriptor) -> String { let summary = """ @@ -591,7 +614,8 @@ extension ProtocolDescription { input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, - streamingOutput: method.isOutputStreaming + streamingOutput: method.isOutputStreaming, + namer: namer ) ) ) @@ -666,7 +690,8 @@ extension FunctionDescription { input: String, output: String, streamingInput: Bool, - streamingOutput: Bool + streamingOutput: Bool, + namer: Namer = Namer() ) -> Self { func makeUnaryOutputArguments() -> [FunctionArgumentDescription] { return [ @@ -719,14 +744,15 @@ extension FunctionDescription { input: input, output: output, streamingInput: streamingInput, - streamingOutput: streamingOutput + streamingOutput: streamingOutput, + namer: namer ), body: [ .expression( .functionCall( calledExpression: .return( .identifierType( - .serverResponse(forType: output, streaming: streamingOutput) + namer.serverResponse(forType: output, isStreaming: streamingOutput) ) ), arguments: streamingOutput ? makeStreamingOutputArguments() : makeUnaryOutputArguments() @@ -746,7 +772,8 @@ extension ExtensionDescription { static func serviceProtocolDefaultImplementation( accessModifier: AccessModifier? = nil, on extensionName: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> Self { ExtensionDescription( onType: extensionName, @@ -758,7 +785,8 @@ extension ExtensionDescription { input: method.inputType, output: method.outputType, streamingInput: method.isInputStreaming, - streamingOutput: method.isOutputStreaming + streamingOutput: method.isOutputStreaming, + namer: namer ) ) } diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index 18502832a..dd51eb496 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -50,7 +50,8 @@ extension VariableDescription { package static func methodDescriptor( accessModifier: AccessModifier? = nil, literalFullyQualifiedService: String, - literalMethodName: String + literalMethodName: String, + namer: Namer = Namer() ) -> Self { return VariableDescription( accessModifier: accessModifier, @@ -59,13 +60,14 @@ extension VariableDescription { left: .identifier(.pattern("descriptor")), right: .functionCall( FunctionCallDescription( - calledExpression: .identifierType(.methodDescriptor), + calledExpression: .identifierType(namer.methodDescriptor), arguments: [ FunctionArgumentDescription( label: "service", expression: .functionCall( .serviceDescriptor( - literalFullyQualifiedService: literalFullyQualifiedService + literalFullyQualifiedService: literalFullyQualifiedService, + namer: namer ) ) ), @@ -84,24 +86,26 @@ extension VariableDescription { /// ``` package static func serviceDescriptor( accessModifier: AccessModifier? = nil, - literalFullyQualifiedService name: String + literalFullyQualifiedService name: String, + namer: Namer = Namer() ) -> Self { return VariableDescription( accessModifier: accessModifier, isStatic: true, kind: .let, left: .identifierPattern("descriptor"), - right: .functionCall(.serviceDescriptor(literalFullyQualifiedService: name)) + right: .functionCall(.serviceDescriptor(literalFullyQualifiedService: name, namer: namer)) ) } } extension FunctionCallDescription { package static func serviceDescriptor( - literalFullyQualifiedService: String + literalFullyQualifiedService: String, + namer: Namer = Namer() ) -> Self { FunctionCallDescription( - calledExpression: .identifier(.type(.serviceDescriptor)), + calledExpression: .identifier(.type(namer.serviceDescriptor)), arguments: [ FunctionArgumentDescription( label: "fullyQualifiedService", @@ -123,10 +127,11 @@ extension ExtensionDescription { package static func serviceDescriptor( accessModifier: AccessModifier? = nil, propertyName: String, - literalFullyQualifiedService: String + literalFullyQualifiedService: String, + namer: Namer = Namer() ) -> ExtensionDescription { return ExtensionDescription( - onType: "GRPCCore.ServiceDescriptor", + onType: namer.literalNamespacedType("ServiceDescriptor"), declarations: [ .commentable( .doc("Service descriptor for the \"\(literalFullyQualifiedService)\" service."), @@ -136,7 +141,10 @@ extension ExtensionDescription { kind: .let, left: .identifier(.pattern(propertyName)), right: .functionCall( - .serviceDescriptor(literalFullyQualifiedService: literalFullyQualifiedService) + .serviceDescriptor( + literalFullyQualifiedService: literalFullyQualifiedService, + namer: namer + ) ) ) ) @@ -151,14 +159,15 @@ extension VariableDescription { /// ``` package static func methodDescriptorsArray( accessModifier: AccessModifier? = nil, - methodNamespaceNames names: [String] + methodNamespaceNames names: [String], + namer: Namer = Namer() ) -> Self { return VariableDescription( accessModifier: accessModifier, isStatic: true, kind: .let, left: .identifier(.pattern("descriptors")), - type: .array(.methodDescriptor), + type: .array(namer.methodDescriptor), right: .literal(.array(names.map { name in .identifierPattern(name).dot("descriptor") })) ) } @@ -181,7 +190,8 @@ extension EnumDescription { literalMethod: String, literalFullyQualifiedService: String, inputType: String, - outputType: String + outputType: String, + namer: Namer = Namer() ) -> Self { return EnumDescription( accessModifier: accessModifier, @@ -201,7 +211,8 @@ extension EnumDescription { .methodDescriptor( accessModifier: accessModifier, literalFullyQualifiedService: literalFullyQualifiedService, - literalMethodName: literalMethod + literalMethodName: literalMethod, + namer: namer ) ) ), @@ -229,7 +240,8 @@ extension EnumDescription { package static func methodsNamespace( accessModifier: AccessModifier? = nil, literalFullyQualifiedService: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> EnumDescription { var description = EnumDescription(accessModifier: accessModifier, name: "Method") @@ -244,7 +256,8 @@ extension EnumDescription { literalMethod: method.name.identifyingName, literalFullyQualifiedService: literalFullyQualifiedService, inputType: method.inputType, - outputType: method.outputType + outputType: method.outputType, + namer: namer ) ) ) @@ -254,7 +267,8 @@ extension EnumDescription { // Add an array of method descriptors let methodDescriptorsArray: VariableDescription = .methodDescriptorsArray( accessModifier: accessModifier, - methodNamespaceNames: methods.map { $0.name.typeName } + methodNamespaceNames: methods.map { $0.name.typeName }, + namer: namer ) description.members.append( .commentable( @@ -278,14 +292,16 @@ extension EnumDescription { accessModifier: AccessModifier? = nil, name: String, literalFullyQualifiedService: String, - methods: [MethodDescriptor] + methods: [MethodDescriptor], + namer: Namer = Namer() ) -> EnumDescription { var description = EnumDescription(accessModifier: accessModifier, name: name) // static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "...") let descriptor = VariableDescription.serviceDescriptor( accessModifier: accessModifier, - literalFullyQualifiedService: literalFullyQualifiedService + literalFullyQualifiedService: literalFullyQualifiedService, + namer: namer ) description.members.append( .commentable( @@ -298,7 +314,8 @@ extension EnumDescription { let methodsNamespace: EnumDescription = .methodsNamespace( accessModifier: accessModifier, literalFullyQualifiedService: literalFullyQualifiedService, - methods: methods + methods: methods, + namer: namer ) description.members.append( .commentable( @@ -323,7 +340,8 @@ extension [CodeBlock] { /// ``` package static func serviceMetadata( accessModifier: AccessModifier? = nil, - service: ServiceDescriptor + service: ServiceDescriptor, + namer: Namer = Namer() ) -> Self { var blocks: [CodeBlock] = [] @@ -331,7 +349,8 @@ extension [CodeBlock] { accessModifier: accessModifier, name: service.name.typeName, literalFullyQualifiedService: service.name.identifyingName, - methods: service.methods + methods: service.methods, + namer: namer ) blocks.append( CodeBlock( @@ -345,7 +364,8 @@ extension [CodeBlock] { let descriptorExtension: ExtensionDescription = .serviceDescriptor( accessModifier: accessModifier, propertyName: service.name.propertyName, - literalFullyQualifiedService: service.name.identifyingName + literalFullyQualifiedService: service.name.identifyingName, + namer: namer ) blocks.append(CodeBlock(item: .declaration(.extension(descriptorExtension)))) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift deleted file mode 100644 index 86abaaf71..000000000 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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. - */ - -extension ExistingTypeDescription { - fileprivate static func grpcCore(_ typeName: String) -> Self { - return .member(["GRPCCore", typeName]) - } - - fileprivate static func requestResponse( - for type: String?, - isRequest: Bool, - isStreaming: Bool, - isClient: Bool - ) -> Self { - let prefix = isStreaming ? "Streaming" : "" - let peer = isClient ? "Client" : "Server" - let kind = isRequest ? "Request" : "Response" - let baseType: Self = .grpcCore(prefix + peer + kind) - - if let type = type { - return .generic(wrapper: baseType, wrapped: .member(type)) - } else { - return baseType - } - } - - package static func serverRequest(forType type: String?, streaming: Bool) -> Self { - return .requestResponse(for: type, isRequest: true, isStreaming: streaming, isClient: false) - } - - package static func serverResponse(forType type: String?, streaming: Bool) -> Self { - return .requestResponse(for: type, isRequest: false, isStreaming: streaming, isClient: false) - } - - package static func clientRequest(forType type: String?, streaming: Bool) -> Self { - return .requestResponse(for: type, isRequest: true, isStreaming: streaming, isClient: true) - } - - package static func clientResponse(forType type: String?, streaming: Bool) -> Self { - return .requestResponse(for: type, isRequest: false, isStreaming: streaming, isClient: true) - } - - package static let serverContext: Self = .grpcCore("ServerContext") - - package static func rpcRouter(genericOver type: String) -> Self { - .generic(wrapper: .grpcCore("RPCRouter"), wrapped: .member(type)) - } - - package static let serviceDescriptor: Self = .grpcCore("ServiceDescriptor") - package static let methodDescriptor: Self = .grpcCore("MethodDescriptor") - - package static func serializer(forType type: String) -> Self { - .generic(wrapper: .grpcCore("MessageSerializer"), wrapped: .member(type)) - } - - package static func deserializer(forType type: String) -> Self { - .generic(wrapper: .grpcCore("MessageDeserializer"), wrapped: .member(type)) - } - - package static func rpcWriter(forType type: String) -> Self { - .generic(wrapper: .grpcCore("RPCWriter"), wrapped: .member(type)) - } - - package static func rpcAsyncSequence(forType type: String) -> Self { - .generic( - wrapper: .grpcCore("RPCAsyncSequence"), - wrapped: .member(type), - .any(.member(["Swift", "Error"])) - ) - } - - package static let callOptions: Self = .grpcCore("CallOptions") - package static let metadata: Self = .grpcCore("Metadata") - - package static func grpcClient(genericOver transport: String) -> Self { - .generic(wrapper: .grpcCore("GRPCClient"), wrapped: [.member(transport)]) - } -} diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 611c09078..d4d9f87ad 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -81,6 +81,7 @@ struct ClientCodeTranslator { func translate( accessModifier: AccessModifier, service: ServiceDescriptor, + namer: Namer = Namer(), serializer: (String) -> String, deserializer: (String) -> String ) -> [CodeBlock] { @@ -101,7 +102,8 @@ struct ClientCodeTranslator { .clientProtocol( accessLevel: accessModifier, name: "ClientProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) ) ), @@ -110,7 +112,10 @@ struct ClientCodeTranslator { .commentable( .preFormatted( Docs.suffix( - self.clientDocs(serviceName: service.name.identifyingName), + self.clientDocs( + serviceName: service.name.identifyingName, + moduleName: namer.grpcCore + ), withDocs: service.documentation ) ), @@ -120,7 +125,8 @@ struct ClientCodeTranslator { name: "Client", serviceEnum: service.name.typeName, clientProtocol: "ClientProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) ) ), @@ -132,6 +138,7 @@ struct ClientCodeTranslator { accessLevel: accessModifier, name: "\(service.name.typeName).ClientProtocol", methods: service.methods, + namer: namer, serializer: serializer, deserializer: deserializer ) @@ -145,7 +152,8 @@ struct ClientCodeTranslator { let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods( accessLevel: accessModifier, on: "\(service.name.typeName).ClientProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) blocks.append( CodeBlock( @@ -166,12 +174,12 @@ struct ClientCodeTranslator { """ } - private func clientDocs(serviceName: String) -> String { + private func clientDocs(serviceName: String, moduleName: String) -> String { return """ /// Generated client for the "\(serviceName)" service. /// /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps - /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// a `\(moduleName).GRPCCClient`. The underlying `GRPCClient` provides the long-lived /// means of communication with the remote peer. """ } diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 2d3a7746e..e83fa34c7 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -17,7 +17,7 @@ /// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties. /// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications, /// using types from ``StructuredSwiftRepresentation``. -package struct IDLToStructuredSwiftTranslator: Translator { +package struct IDLToStructuredSwiftTranslator { package init() {} func translate( @@ -25,7 +25,8 @@ package struct IDLToStructuredSwiftTranslator: Translator { accessLevel: CodeGenerator.Config.AccessLevel, accessLevelOnImports: Bool, client: Bool, - server: Bool + server: Bool, + grpcCoreModuleName: String ) throws -> StructuredSwiftRepresentation { try self.validateInput(codeGenerationRequest) let accessModifier = AccessModifier(accessLevel) @@ -35,6 +36,8 @@ package struct IDLToStructuredSwiftTranslator: Translator { let serverTranslator = ServerCodeTranslator() let clientTranslator = ClientCodeTranslator() + let namer = Namer(grpcCore: grpcCoreModuleName) + for service in codeGenerationRequest.services { codeBlocks.append( CodeBlock(comment: .mark("\(service.name.identifyingName)", sectionBreak: true)) @@ -42,7 +45,8 @@ package struct IDLToStructuredSwiftTranslator: Translator { let metadata = metadataTranslator.translate( accessModifier: accessModifier, - service: service + service: service, + namer: namer ) codeBlocks.append(contentsOf: metadata) @@ -54,6 +58,7 @@ package struct IDLToStructuredSwiftTranslator: Translator { let blocks = serverTranslator.translate( accessModifier: accessModifier, service: service, + namer: namer, serializer: codeGenerationRequest.makeSerializerCodeSnippet, deserializer: codeGenerationRequest.makeDeserializerCodeSnippet ) @@ -67,6 +72,7 @@ package struct IDLToStructuredSwiftTranslator: Translator { let blocks = clientTranslator.translate( accessModifier: accessModifier, service: service, + namer: namer, serializer: codeGenerationRequest.makeSerializerCodeSnippet, deserializer: codeGenerationRequest.makeDeserializerCodeSnippet ) @@ -84,7 +90,8 @@ package struct IDLToStructuredSwiftTranslator: Translator { imports = try self.makeImports( dependencies: codeGenerationRequest.dependencies, accessLevel: accessLevel, - accessLevelOnImports: accessLevelOnImports + accessLevelOnImports: accessLevelOnImports, + grpcCoreModuleName: grpcCoreModuleName ) } @@ -102,13 +109,14 @@ package struct IDLToStructuredSwiftTranslator: Translator { package func makeImports( dependencies: [Dependency], accessLevel: CodeGenerator.Config.AccessLevel, - accessLevelOnImports: Bool + accessLevelOnImports: Bool, + grpcCoreModuleName: String ) throws -> [ImportDescription] { var imports: [ImportDescription] = [] imports.append( ImportDescription( accessLevel: accessLevelOnImports ? AccessModifier(accessLevel) : nil, - moduleName: "GRPCCore" + moduleName: grpcCoreModuleName ) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift index a7b91132e..41252995e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift @@ -19,8 +19,9 @@ struct MetadataTranslator { func translate( accessModifier: AccessModifier, - service: ServiceDescriptor + service: ServiceDescriptor, + namer: Namer = Namer() ) -> [CodeBlock] { - .serviceMetadata(accessModifier: accessModifier, service: service) + .serviceMetadata(accessModifier: accessModifier, service: service, namer: namer) } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index e563981e6..2bf06757f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -63,6 +63,7 @@ struct ServerCodeTranslator { func translate( accessModifier: AccessModifier, service: ServiceDescriptor, + namer: Namer = Namer(), serializer: (String) -> String, deserializer: (String) -> String ) -> [CodeBlock] { @@ -83,7 +84,8 @@ struct ServerCodeTranslator { .streamingService( accessLevel: accessModifier, name: "StreamingServiceProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) ) ), @@ -101,7 +103,8 @@ struct ServerCodeTranslator { accessLevel: accessModifier, name: "ServiceProtocol", streamingProtocol: "\(service.name.typeName).StreamingServiceProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) ) ), @@ -119,7 +122,8 @@ struct ServerCodeTranslator { accessModifier: accessModifier, name: "SimpleServiceProtocol", serviceProtocol: "\(service.name.typeName).ServiceProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) ) ), @@ -133,6 +137,7 @@ struct ServerCodeTranslator { on: "\(service.name.typeName).StreamingServiceProtocol", serviceNamespace: service.name.typeName, methods: service.methods, + namer: namer, serializer: serializer, deserializer: deserializer ) @@ -148,7 +153,8 @@ struct ServerCodeTranslator { .streamingServiceProtocolDefaultImplementation( accessModifier: accessModifier, on: "\(service.name.typeName).ServiceProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) blocks.append( CodeBlock( @@ -163,7 +169,8 @@ struct ServerCodeTranslator { let serviceDefaultImplExtension: ExtensionDescription = .serviceProtocolDefaultImplementation( accessModifier: accessModifier, on: "\(service.name.typeName).SimpleServiceProtocol", - methods: service.methods + methods: service.methods, + namer: namer ) blocks.append( CodeBlock( diff --git a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift deleted file mode 100644 index 1ac3c4bee..000000000 --- a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -/// Represents one responsibility of the ``Translator``: either the type aliases translation, -/// the server code translation or the client code translation. -protocol SpecializedTranslator { - - /// The ``SourceGenerator.Config.AccessLevel`` object used to represent the visibility level used in the generated code. - var accessLevel: CodeGenerator.Config.AccessLevel { get } - - /// Generates an array of ``CodeBlock`` elements that will be part of the ``StructuredSwiftRepresentation`` object - /// created by the ``Translator``. - /// - /// - Parameters: - /// - codeGenerationRequest: The ``CodeGenerationRequest`` object used to represent a Source IDL description of RPCs. - /// - Returns: An array of ``CodeBlock`` elements. - /// - /// - SeeAlso: ``CodeGenerationRequest``, ``Translator``, ``CodeBlock``. - func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] -} - -extension SpecializedTranslator { - /// The access modifier that corresponds with the access level from ``SourceGenerator.Configuration``. - internal var accessModifier: AccessModifier { - get { - switch accessLevel.level { - case .internal: - return AccessModifier.internal - case .package: - return AccessModifier.package - case .public: - return AccessModifier.public - } - } - } - - internal var availabilityGuard: AvailabilityDescription { - AvailabilityDescription(osVersions: [ - .init(os: .macOS, version: "15.0"), - .init(os: .iOS, version: "18.0"), - .init(os: .watchOS, version: "11.0"), - .init(os: .tvOS, version: "18.0"), - .init(os: .visionOS, version: "2.0"), - ]) - } -} diff --git a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift deleted file mode 100644 index 9d0a043b0..000000000 --- a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -/// Transforms ``CodeGenerationRequest`` objects into ``StructuredSwiftRepresentation`` objects. -/// -/// It represents the first step of the code generation process for IDL described RPCs. -protocol Translator { - /// Translates the provided ``CodeGenerationRequest`` object, into Swift code representation. - /// - Parameters: - /// - codeGenerationRequest: The IDL described RPCs representation. - /// - accessLevel: The access level that will restrict the protocols, extensions and methods in the generated code. - /// - accessLevelOnImports: Whether access levels should be included on imports. - /// - client: Whether or not client code should be generated from the IDL described RPCs representation. - /// - server: Whether or not server code should be generated from the IDL described RPCs representation. - /// - Returns: A structured Swift representation of the generated code. - /// - Throws: An error if there are issues translating the codeGenerationRequest. - func translate( - codeGenerationRequest: CodeGenerationRequest, - accessLevel: CodeGenerator.Config.AccessLevel, - accessLevelOnImports: Bool, - client: Bool, - server: Bool - ) throws -> StructuredSwiftRepresentation -} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift index c079d5f5c..88a6bc075 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift @@ -107,7 +107,8 @@ extension StructuredSwiftTests { let imports = try StructuredSwiftTests.Import.translator.makeImports( dependencies: dependencies, accessLevel: accessLevel, - accessLevelOnImports: true + accessLevelOnImports: true, + grpcCoreModuleName: "GRPCCore" ) #expect(render(imports) == expected) @@ -157,7 +158,8 @@ extension StructuredSwiftTests { let imports = try StructuredSwiftTests.Import.translator.makeImports( dependencies: dependencies, accessLevel: accessLevel, - accessLevelOnImports: true + accessLevelOnImports: true, + grpcCoreModuleName: "GRPCCore" ) #expect(render(imports) == expected) @@ -191,11 +193,29 @@ extension StructuredSwiftTests { let imports = try StructuredSwiftTests.Import.translator.makeImports( dependencies: dependencies, accessLevel: accessLevel, - accessLevelOnImports: true + accessLevelOnImports: true, + grpcCoreModuleName: "GRPCCore" ) #expect(render(imports) == expected) } + @Test("gRPC module name") + func grpcModuleName() throws { + let translator = IDLToStructuredSwiftTranslator() + let imports = try translator.makeImports( + dependencies: [], + accessLevel: .public, + accessLevelOnImports: true, + grpcCoreModuleName: "GRPCCoreFoo" + ) + + let expected = + """ + public import GRPCCoreFoo + """ + + #expect(render(imports) == expected) + } } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 1848df575..05375ae4f 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -139,6 +139,83 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) } + func testGenerateWithDifferentModuleName() throws { + let service = ServiceDescriptor( + documentation: "/// Documentation for FooService\n", + name: ServiceName( + identifyingName: "foo.FooService", + typeName: "Foo_FooService", + propertyName: "foo_FooService" + ), + methods: [ + MethodDescriptor( + documentation: "", + name: MethodName( + identifyingName: "Unary", + typeName: "Unary", + functionName: "unary" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "Foo", + outputType: "Bar" + ), + MethodDescriptor( + documentation: "", + name: MethodName( + identifyingName: "ClientStreaming", + typeName: "ClientStreaming", + functionName: "clientStreaming" + ), + isInputStreaming: true, + isOutputStreaming: false, + inputType: "Foo", + outputType: "Bar" + ), + MethodDescriptor( + documentation: "", + name: MethodName( + identifyingName: "ServerStreaming", + typeName: "ServerStreaming", + functionName: "serverStreaming" + ), + isInputStreaming: false, + isOutputStreaming: true, + inputType: "Foo", + outputType: "Bar" + ), + MethodDescriptor( + documentation: "", + name: MethodName( + identifyingName: "BidiStreaming", + typeName: "BidiStreaming", + functionName: "bidiStreaming" + ), + isInputStreaming: true, + isOutputStreaming: true, + inputType: "Foo", + outputType: "Bar" + ), + ] + ) + + let request = makeCodeGenerationRequest(services: [service]) + let translator = IDLToStructuredSwiftTranslator() + let structuredSwift = try translator.translate( + codeGenerationRequest: request, + accessLevel: .internal, + accessLevelOnImports: false, + client: true, + server: true, + grpcCoreModuleName: String("GRPCCore".reversed()) + ) + let renderer = TextBasedRenderer.default + let sourceFile = try renderer.render(structured: structuredSwift) + let contents = sourceFile.contents + + XCTAssertFalse(contents.contains("GRPCCore")) + } + func testEmptyFileGeneration() throws { let expectedSwift = """ @@ -161,7 +238,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, accessLevel: CodeGenerator.Config.AccessLevel, - server: Bool = false + server: Bool = false, + grpcCoreModuleName: String = "GRPCCore" ) throws { let translator = IDLToStructuredSwiftTranslator() let structuredSwift = try translator.translate( @@ -169,7 +247,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: accessLevel, accessLevelOnImports: true, client: false, - server: server + server: server, + grpcCoreModuleName: grpcCoreModuleName ) let renderer = TextBasedRenderer.default let sourceFile = try renderer.render(structured: structuredSwift) @@ -197,7 +276,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in @@ -244,7 +324,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in @@ -279,7 +360,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in @@ -325,7 +407,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .internal, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in XCTAssertEqual( @@ -369,7 +452,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in XCTAssertEqual( @@ -429,7 +513,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in XCTAssertEqual( @@ -482,7 +567,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in @@ -528,7 +614,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevel: .public, accessLevelOnImports: true, client: true, - server: true + server: true, + grpcCoreModuleName: "GRPCCore" ) ) { error in From 87384f1937cbfbca1cd4746543a753771ddf8afc Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 3 Mar 2025 14:00:42 +0000 Subject: [PATCH 565/580] Add a migration guide (#2199) Motivation: Moving from 1.x to 2.x isn't trivial. We should provide a guide for doing this. Modifications: - Add a guide for migrating client code and services - Add a script which can automate parts of this Result: Some guidance on migrating. --------- Co-authored-by: Gus Cairo --- .../Articles/Migration-guide.md | 498 ++++++++++++++++++ .../Documentation.docc/Documentation.md | 1 + dev/v1-to-v2/v1_to_v2.sh | 128 +++++ 3 files changed, 627 insertions(+) create mode 100644 Sources/GRPCCore/Documentation.docc/Articles/Migration-guide.md create mode 100755 dev/v1-to-v2/v1_to_v2.sh diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Migration-guide.md b/Sources/GRPCCore/Documentation.docc/Articles/Migration-guide.md new file mode 100644 index 000000000..493061ef8 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Migration-guide.md @@ -0,0 +1,498 @@ +# gRPC Swift 1.x to 2.x migration guide + +Learn how to migrate an app from gRPC Swift 1.x to 2.x. + +## Overview + +The intended audience for this guide is users of the `async` variants of clients +and services from 1.x, not the versions using the older `EventLoopFuture` API. + +The guide takes you through a number of steps to migrate your gRPC app +from 1.x to 2.x. You'll use the following strategy: + +1. Setup your package so it depends on a local copy of gRPC Swift 1.x and the + upstream version of 2.x. +2. Generate code for 2.x alongside generated 1.x code. +3. Incrementally migrate targets to 2.x. +4. Remove the code generated for, and the dependency on, 1.x. + +You'll do this migration incrementally by staging in a local copy of gRPC Swift +1.x and migrating client and service code on a per service basis. This approach +aims to minimise the number of errors and changes required to get the package +building again. As a practical note, you should commit changes regularly as you +work through the migration, especially when your package is in a compiling +state. + +## Requirements + +gRPC Swift 2.x has stricter requirements than 1.x. These include: + +- Swift 6 or newer. +- Deployment targets of macOS 15+, iOS 18+, tvOS 18+, watchOS 11+ and visionOS 2+. + +To make the migration easier a script is available to automate a number of +steps. You should download it now using: + +```sh +curl https://raw.githubusercontent.com/grpc/grpc-swift/refs/heads/main/dev/v1-to-v2/v1_to_v2.sh -o v1_to_v2 +``` + +You'll also need to make the `v1_to_v2` script executable: + +```sh +chmod +x v1_to_v2 +``` + +## Depending on 1.x and 2.x + +The first step in the migration is to modify your package so that it can +temporarily depend on 1.x and 2.x. + +### Getting a local copy of 1.x + +The exact version of 1.x you need to depend on must be local as Swift packages +can't depend on two different major versions of the same package. Create a +directory in your package called "LocalPackages" and then call `v1_to_v2`: + +```sh +mkdir LocalPackages && ./v1_to_v2 clone-v1 LocalPackages +``` + +This command checks out a copy of 1.x into `LocalPackages` and applies a few +patches to it which are necessary for the migration. You can remove it once +you've finished the migration. + +### Using the local copy of 1.x + +Now you need to update your package manifest (`Package.swift`) to use the local +copy rather than the copy from GitHub. Replace your package dependency on +"grpc-swift" with the local dependency, and update any target dependencies to +use "grpc-swift-v1" instead of "grpc-swift": + +```swift +let package = Package( + ... + dependencies: [ + .package(path: "LocalPackages/grpc-swift-v1") + ], + targets [ + .executableTarget( + name: "Application", + dependencies [ + ... + .product(name: "GRPC", package: "grpc-swift-v1"), + ... + ] + ) + ] + ... +) +``` + +Check your package still builds by running `swift build`. Now's a good time to +commit the changes you've made so far. + +### Adding a dependency on 2.x + +Next you need to add a dependency on 2.x. In order to do this you'll need to +raise the tools version at the top of the manifest to 6.0 or higher: + +```swift +// swift-tools-version: 6.0 +``` + +You also need to set the `platforms` to the following or higher: + +```swift +let package = Package( + name: "...", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + ... +) +``` + +Note that setting or increasing the platforms is an API breaking change. + +Check that your package still builds with `swift build`. If you weren't +previously using tools version 6.0 then you're likely to have new warnings or +errors relating to concurrency. You should fix these in the fullness of time +but for now add the `.swiftLanguageMode(.v5)` setting to the `settings` for each +target. + +If there are any other build issues fix them up now and commit the changes. + +Now add the following package dependencies for gRPC Swift 2.x: + +``` +.package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0"), +.package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0"), +.package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0"), +``` + +For each target which was previously importing the `GRPC` module add the +following target dependencies: + +``` +.product(name: "GRPCCore", package: "grpc-swift"), +.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), +.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), +``` + +Run `swift build` again to verify your package still builds. Now is another +great time to commit your changes. + +## Code generation + +Now that you've built your package with dependencies on a lightly modified +version of 1.x and 2.x you need to consider the generated code. The approach you +take here depends on how you're currently generating your gRPC code: + +1. Using `protoc` directly, or +2. Using the build plugin. + +### Using protoc directly + +> If you generated your gRPC code with the build plugin then skip this section. + +Because the names of the files containing generated gRPC code will be the same +for 1.x and 2.x (and the Swift compiler requires file names to be unique) we need +to rename all of the gRPC code generated by 1.x. + +You can use the `v1_to_v2` script to rename all `*.grpc.swift` files to +`*.grpc.v1.swift` by using the `rename-generated-files` subcommand with the +directory containing your generated code, for example: + +```sh +./v1_to_v2 rename-generated-files Sources/ +``` + +One of the patches applied to the local copy of 1.x was to rename +`protoc-gen-grpc-swift` to `protoc-gen-grpc-swift-v1`. If you previously used a +script to generate your code, then run it again, ensuring that the copy of +`protoc-gen-grpc-swift` comes from this package (as it will now be for 2.x). + +If you didn't use a script to generate your code then refer to the +[documentation][3] to learn how to generate gRPC Swift code. + +Check that your package still builds and commit any changes. + +### Using the build plugin + +> If you generated your gRPC code using `protoc` directly then skip this +> section. + +Because you don't have direct control over the names of files generated by the +build plugin you can't rename them directly. Instead our strategy is to locate +the generated gRPC code from the build directory and copy it into the source +directory and then replace the 1.x plugin with the 2.x plugin. + +As you've been building your package regularly the generated files should +already be in the `.build` directory. You can find them using: + +```sh +find .build/plugins/outputs -name '*.grpc.swift' +``` + +Move the files for their appropriate directory in `Sources`. Once you've done +that you can use the `v1_to_v2` script to rename all `*.grpc.swift` files to +`*.grpc.v1.swift` by using the `rename-generated-files` subcommand with the +directory containing your generated code, for example: + +```sh +./v1_to_v2 rename-generated-files Sources/ +``` + +The next step is to use the new build plugin. The build plugin for 2.x can +generate gRPC code and Protobuf messages, so remove the gRPC Swift 1.x _and_ +SwiftProtobuf build plugins from your manifest and replace them with the plugin +for 2.x: + +```swift +.target( + ... + plugins: [ + .plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf") + ] +) +``` + +Finally you need to add a configuration file for the plugin. Take a look at the +[build plugin documentation][3] for instructions on how to do this. + +At this point you should run `swift build` again to check your package still +compiles and commit any changes. + +## Service code migration + +> If you only need to migrate clients then skip this section. + +By now your package should be set up to depend on a patched version of 1.x and 2.x +and have both sets of generated code and still compile. It's time to make some +code changes, so let's start by migrating a service. + + +A number of these steps can be automated, and the `v1_to_v2` script can do just +this. However, it might not be sufficient and you should read through the +steps below to understand what transformations are done. + +Find the service you wish to migrate. The first step is to update any imports +from `GRPC` to `GRPCCore`, which is the base module containing abstractions and +runtime components for 2.x. + +Next let's update the service protocol that your type conforms to. In 2.x each +service has three protocols generated for it, each offering a different level of +granularity. You can read more about each version in the [gRPC Swift Protobuf +documentation][2]. The variant most like 1.x is the `SimpleServiceProtocol`. +However, it doesn't allow you access metadata. If you need access to metadata +skip to the section called `ServiceProtocol`. + +### SimpleServiceProtocol + +The requirements for each methods are also slightly different; in 2.x the context +type is called `ServerContext` as opposed to `GRPCAsyncServerCallContext` in 1.x. +It also has different functionality but that will be covered later. The types +for streaming requests and responses are also different: +- `GRPCAsyncRequestStream` became `RPCAsyncSequence`, and +- `GRPCAsyncResponseStreamWriter` became `RPCWriter`. + +The `v1_to_v2` script has a subcommand to apply all of these transformations to +an input file. Run it now. Here's an example invocation: + +```sh +./v1_to_v2 patch-service Sources/Server/Service.swift +``` + +If the service was contained to that file then that might be the extent of +changes you need to make for that service. However, it's likely that types leak +into other files. If that's the case you should continue applying these +transformations until your app compiles again. You'll also need to stop +passing this service to your 1.x server. + +Once you've gotten to a point where the package builds, commit your changes. +Repeat this until you've done all services in your package. + +### ServiceProtocol + +> If the `SimpleServiceProtocol` worked then you can skip this section. + +If you're reading this section then you're likely relying on metadata in your +service. This means you need to implement the `ServiceProtocol` instead of the +`SimpleServiceProtocol` and the transformations you need to apply are +aren't well suited for automation. The best approach is to conform your +service to the 1.x protocol and the 2.x protocol. Add conformance to the +`{Service}.ServiceProtocol` where `{Service}` is the namespaced name of your +service (if your service is called `Baz` and declared in the `foo.bar` Protocol +Buffers package then this would be `Foo_Bar_Baz.ServiceProtocol`). + +Let Xcode generate stubs for the methods which haven't been implemented yet and +fill each one with a `fatalError` so that you app builds. Each method +should take a `ServerRequest` or `StreamingServerRequest` and context as input +and return a `ServerResponse` or `StreamingServerResponse`. Request metadata is +available on the request object. For single responses you can set initial and +trailing metadata when you create the response. For streaming responses you can +set initial metadata in the initializer and return trailing metadata from the +closure you provide to the initializer. This is demonstrated in the +['echo-metadata'](https://github.com/grpc/grpc-swift/tree/main/Examples/echo-metadata) +example. + +One important difference between this approach and the `SimpleServiceProtocol` +(and 1.x) is that responses aren't completed until the body of the response has +completed as opposed to when the function returns. This means that much of your +logic likely lives within the body of the `StreamingServerResponse`. + +## Server migration + +With all services updated to use gRPC Swift 2.x you now need to update the +server. Find where you create the server in your app. In this file +you'll need to add imports for `GRPCCore` (which provides the server type) and +`GRPCNIOTransportHTTP2` (which provides HTTP/2 transports built on top of +SwiftNIO). + +The server object is called `GRPCServer` and you initialize it with a transport, +any configuration, and a list of services. Importantly you must call `serve()` to start +the server. This blocks indefinitely so it often makes sense to start it in a +task group if you need to run other code concurrently. Here's an example of a +server configured to use the HTTP/2 transport: + +```swift +let server = GRPCServer( + transport: .http2NIOPosix( + // Configure the host and port to listen on. + address: .ipv4(host: "127.0.0.1", port: 1234), + // Configure TLS here, if your're using it. + transportSecurity: .plaintext, + config: .defaults { config in + // Change any of the default config in here. + } + ), + // List your services here: + services: [] +) + +// Start the server. +try await server.serve() +``` + +You can get the listening address using the `listeningAddress` property: + +```swift +try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await server.serve() } + if let address = try await server.listeningAddress { + print("Listening on \(address)") + } +} +``` + +With any luck your app should build and your server should run. Yes, you guessed +it, it's time to commit any changes you've made. + +## Client code migration + +> You can skip this section if you only needed to migrate services. + +Migrating client code is more difficult as you typically use client code +throughout a wider part of your app. Our approach is to migrate from client +calls first and then work upwards through your app to where the client is +created. + +Start by finding a place within the target being migrated where a generated +client is being used. + +Note that the generated client in 2.x is generic over a transport type, any +types or functions using it will either need to choose a concrete type or +also become generic. The most similar replacements to 1.x are: + +- `HTTP2ClientTransport.Posix`, and +- `HTTP2ClientTransport.TransportServices`. + +Changing the type of the client will cause numerous build errors. To keep the +number of errors manageable you'll migrate one function at a time. How this +is done depends on whether the generated client is passed in to the function +or stored on a property. + +If the function is passed a generated client then duplicate it, changing the +signature to use a 2.x generated client. The new client is +named `{Service}.Client` where `{Service}` is the namespaced name of your +service (if your service is named `Baz` and declared in the `foo.bar` +Protocol Buffers package then this would be `Foo_Bar_Baz.Client`). +Change the body of the function using the 1.x client to just `fatalError()`. +Later you'll remove this function altogether. + +If the generated client is a stored type then add a new computed property +returning an instance of it. The body can just call `fatalError()` for now: + +```swift +var client: Foo_Bar_Baz.Client { + fatalError("TODO") +} +``` + +Now you need to update the function to use the new client. For unary calls the API +is very similar, so you may not have to change any code. An important change to +highlight is that for RPCs which stream their responses you must handle the +response stream _within_ the closure passed to the client. By way of example, +imagine the following server streaming RPC from 1.x: + +```swift +func serverStreamingEcho(text: String, client: Echo_EchoAsyncClient) async throws { + for try await reply in client.expand(.with { $0.text = text }) { + print(reply.text) + } +} +``` + +In 2.x this becomes: + +```swift +func serverStreamingEcho(text: String, client: Echo_Echo.Client) async throws { + try await client.expand(.with { $0.text = text }) { response in + for try await reply in response.messages { + print(reply.text) + } + } +} +``` + +Similarly for client streaming RPCs you must provide any messages within a +closure. Here's an example of 1.x: + +```swift +func clientStreamingEcho(text: String, client: Echo_EchoAsyncClient) async throws { + let messages = makeAsyncSequenceOfMessages(text) + let reply = try await client.collect(messages) + print(reply.text) +} +``` + +The equivalent code in 2.x is: + +```swift +func clientStreamingEcho(text: String, client: Echo_Echo.Client) async throws { + let reply = try await client.collect { request in + for try await message in makeAsyncSequenceOfMessages(text) { + request.write(message) + } + } + print(reply.text) +} +``` + +Bidirectional streaming is just a combination of the previous two examples. + +Once the new version compiles you can work upwards, updating functions which +pass in the generated client to use the new one instead. You can also remove +any of the unused functions. + +## Client migration + +Once all client call sites have been updates you'll need to update how you +create the client. Find where you create the client in your app. In this file +you'll need to add imports for `GRPCCore` (which provides the client type) and +`GRPCNIOTransportHTTP2` (which provides HTTP/2 transports built on top of +SwiftNIO). + +The client object is called `GRPCClient` and you initialize it with a transport, +and any configuration. Importantly you must call `runConnections()` to start the +client. This runs indefinitely and maintains the connections for the client so +it makes sense to start it in a task group. Alternatively you can use the +`withGRPCClient(transport:interceptors:handleClient:)` helper which provides you +with scoped access to a running client. + +Here's an example of a client configured to use the HTTP/2 transport: + +```swift +try await withGRPCClient( + transport: .http2NIOPosix( + target: .dns(host: "example.com"), + transportSecurity: .tls, + ) +) { client in + // ... +} +``` + +With any luck your app should build and your server should run. Yes, you guessed +it, it's time to commit any changes you've made. + +## Cleaning up + +Once you've migrated you package you can remove the local checkout of gRPC Swift +1.x and remove it from your package manifest. + +## What's missing? + +If there were any parts of this guide you felt were unclear or didn't cover enough +of the migration then please file an issue on GitHub so that we can work on improving +it. + +[0]: https://github.com/grpc/grpc-swift/tree/main +[1]: https://github.com/grpc/grpc-swift/tree/release/1.x +[2]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation +[3]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 7902e601c..3c5f6b08d 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -55,6 +55,7 @@ as tutorials. - - +- ### Getting involved diff --git a/dev/v1-to-v2/v1_to_v2.sh b/dev/v1-to-v2/v1_to_v2.sh new file mode 100755 index 000000000..c1901ed2c --- /dev/null +++ b/dev/v1-to-v2/v1_to_v2.sh @@ -0,0 +1,128 @@ +#!/bin/bash +## Copyright 2025, 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. + +set -eou pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +# Clones v1 into the given directory and applies a number of patches to rename +# the package from 'grpc-swift' to 'grpc-swift-v1' and 'protoc-gen-grpc-swift' +# to 'protoc-gen-grpc-swift-v1'. +function checkout_v1 { + # The directory to clone grpc-swift into. + grpc_checkout_dir="$(realpath "$1")" + # The path of the checkout. + grpc_checkout_path="${grpc_checkout_dir}/grpc-swift-v1" + + # Clone the repo. + log "Cloning grpc-swift to ${grpc_checkout_path}" + git clone \ + --quiet \ + https://github.com/grpc/grpc-swift.git \ + "${grpc_checkout_path}" + + # Get the latest version of 1.x.y. + local -r version=$(git -C "${grpc_checkout_path}" tag --list | grep '1.\([0-9]\+\).\([0-9]\+\)$' | sort -V | tail -n 1) + + log "Checking out $version" + git -C "${grpc_checkout_path}" checkout --quiet "$version" + + # Remove the git bits. + log "Removing ${grpc_checkout_path}/.git" + rm -rf "${grpc_checkout_path}/.git" + + # Update the manifest to rename the package and the protoc plugin. + package_manifest="${grpc_checkout_path}/Package.swift" + log "Updating ${package_manifest}" + sed -i '' \ + -e 's/let grpcPackageName = "grpc-swift"/let grpcPackageName = "grpc-swift-v1"/g' \ + -e 's/protoc-gen-grpc-swift/protoc-gen-grpc-swift-v1/g' \ + "${package_manifest}" + + # Update all references to protoc-gen-grpc-swift. + log "Updating references to protoc-gen-grpc-swift" + find \ + "${grpc_checkout_path}/Sources" \ + "${grpc_checkout_path}/Tests" \ + "${grpc_checkout_path}/Plugins" \ + -type f \ + -name '*.swift' \ + -exec sed -i '' 's/protoc-gen-grpc-swift/protoc-gen-grpc-swift-v1/g' {} + + + # Update the path of the protoc plugin so it aligns with the target name. + log "Updating directory name for protoc-gen-grpc-swift-v1" + mv "${grpc_checkout_path}/Sources/protoc-gen-grpc-swift" "${grpc_checkout_path}/Sources/protoc-gen-grpc-swift-v1" + + log "Cloned and patched v1 to: ${grpc_checkout_path}" +} + + +# Recursively finds '*.grpc.swift' files in the given directory and renames them +# to '*grpc.v1.swift'. +function rename_generated_grpc_code { + local directory=$1 + + find "$directory" -type f -name "*.grpc.swift" \ + -exec bash -c 'mv "$0" "${0%.grpc.swift}.grpc.v1.swift"' {} \; +} + +# Applies a number of textual replacements to migrate a service implementation +# on the given file. +function patch_service_code { + local filename=$1 + + sed -E -i '' \ + -e 's/import GRPC/import GRPCCore/g' \ + -e 's/GRPCAsyncServerCallContext/ServerContext/g' \ + -e 's/: ([A-Za-z_][A-Za-z0-9_]*)AsyncProvider/: \1.SimpleServiceProtocol/g' \ + -e 's/GRPCAsyncResponseStreamWriter/RPCWriter/g' \ + -e 's/GRPCAsyncRequestStream<([A-Za-z_][A-Za-z0-9_]*)>/RPCAsyncSequence<\1, any Error>/g' \ + -e 's/responseStream.send/responseStream.write/g' \ + -e 's/responseStream:/response responseStream:/g' \ + -e 's/requestStream:/request requestStream:/g' \ + "$filename" +} + +function usage { + echo "Usage:" + echo " $0 clone-v1 DIRECTORY" + echo " $0 rename-generated-code DIRECTORY" + echo " $0 patch-service FILE" + exit 1 +} + +if [[ $# -lt 2 ]]; then + usage +fi + +subcommand="$1" +argument="$2" + +case "$subcommand" in + "clone-v1") + checkout_v1 "$argument" + ;; + "rename-generated-code") + rename_generated_grpc_code "$argument" + ;; + "patch-service") + patch_service_code "$argument" + ;; + *) + usage + ;; +esac From 384a8dbb494f14e86d8b9d1a38d96b01a35933c7 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 3 Mar 2025 16:23:57 +0000 Subject: [PATCH 566/580] Rename nightly_6_1 params to nightly_next (#2203) Rename nightly_6_1 params to nightly_next; see https://github.com/apple/swift-nio/pull/3122 --- .github/workflows/main.yml | 2 +- .github/workflows/pull_request.yml | 2 +- IntegrationTests/Benchmarks/Thresholds/nightly-6.1 | 1 + .../GRPCSwiftBenchmark.Metadata_Add_binary.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Add_string.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json | 0 ...terate_binary_values_when_only_binary_values_stored.p90.json | 0 ...data_Iterate_binary_values_when_only_strings_stored.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json | 0 .../GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json | 0 10 files changed, 3 insertions(+), 2 deletions(-) create mode 120000 IntegrationTests/Benchmarks/Thresholds/nightly-6.1 rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.1 => nightly-next}/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.1 => nightly-next}/GRPCSwiftBenchmark.Metadata_Add_string.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.1 => nightly-next}/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.1 => nightly-next}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.1 => nightly-next}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.1 => nightly-next}/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{nightly-6.1 => nightly-next}/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 536b48b28..7d43f44dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,7 +14,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" benchmarks: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 34fbceeea..5e0e7b4ba 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,7 +23,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" construct-examples-matrix: diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1 b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1 new file mode 120000 index 000000000..11e4d1b5c --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1 @@ -0,0 +1 @@ +./nightly-next \ No newline at end of file diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Add_string.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/nightly-6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json From 433c0d1337e539650c3ae7928db0d594b2b3d3e7 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 14 Mar 2025 13:08:07 +0000 Subject: [PATCH 567/580] Fix build issues with static Linux SDK (#2206) This PR fixes some imports so that grpc-swift can be built using the static Linux SDK. --- .../GRPCCore/Call/Client/Internal/RetryDelaySequence.swift | 6 +++++- Sources/GRPCInProcessTransport/Syscalls.swift | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift index f07606f5c..46b3e197d 100644 --- a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -15,8 +15,12 @@ */ #if canImport(Darwin) public import Darwin // should be @usableFromInline -#else +#elseif canImport(Glibc) public import Glibc // should be @usableFromInline +#elseif canImport(Musl) +public import Musl // should be @usableFromInline +#else +#error("Unsupported OS") #endif @usableFromInline diff --git a/Sources/GRPCInProcessTransport/Syscalls.swift b/Sources/GRPCInProcessTransport/Syscalls.swift index df49a49f2..538eb3a78 100644 --- a/Sources/GRPCInProcessTransport/Syscalls.swift +++ b/Sources/GRPCInProcessTransport/Syscalls.swift @@ -17,9 +17,11 @@ #if canImport(Darwin) private import Darwin #elseif canImport(Glibc) -private import Glibc +private import Glibc // should be @usableFromInline #elseif canImport(Musl) -private import Musl +private import Musl // should be @usableFromInline +#else +#error("Unsupported OS") #endif enum System { From 634ab229ca11e3b0a9095a4361370eae61851ca5 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 17 Mar 2025 11:25:57 +0000 Subject: [PATCH 568/580] Add static SDK CI workflow (#2205) Add static SDK CI workflow which runs on commits to PRs, merges to main and daily on main. --------- Co-authored-by: Gus Cairo Co-authored-by: George Barnett --- .github/workflows/main.yml | 4 ++++ .github/workflows/pull_request.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7d43f44dd..ccfd80f58 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,3 +24,7 @@ jobs: benchmark_package_path: "IntegrationTests/Benchmarks" linux_5_9_enabled: false linux_5_10_enabled: false + + static-sdk: + name: Static SDK + uses: apple/swift-nio/.github/workflows/static_sdk.yml@main diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5e0e7b4ba..d923455d6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -66,3 +66,7 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false + + static-sdk: + name: Static SDK + uses: apple/swift-nio/.github/workflows/static_sdk.yml@main From 147045139d1bccf48925881c6332c0a7667283f8 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Mon, 17 Mar 2025 09:58:25 -0400 Subject: [PATCH 569/580] Fix Android build (#2207) This PR follows-up #2206 and fixes the Android build I noticed at https://swift-everywhere.org. Hopefully we will soon have CI support for Android (https://github.com/swiftlang/github-workflows/pull/106) to catch this sort of breakage sooner. --- .../GRPCCore/Call/Client/Internal/RetryDelaySequence.swift | 2 ++ Sources/GRPCInProcessTransport/Syscalls.swift | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift index 46b3e197d..4f87682b2 100644 --- a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -15,6 +15,8 @@ */ #if canImport(Darwin) public import Darwin // should be @usableFromInline +#elseif canImport(Android) +public import Android // should be @usableFromInline #elseif canImport(Glibc) public import Glibc // should be @usableFromInline #elseif canImport(Musl) diff --git a/Sources/GRPCInProcessTransport/Syscalls.swift b/Sources/GRPCInProcessTransport/Syscalls.swift index 538eb3a78..c96becbd5 100644 --- a/Sources/GRPCInProcessTransport/Syscalls.swift +++ b/Sources/GRPCInProcessTransport/Syscalls.swift @@ -16,6 +16,8 @@ #if canImport(Darwin) private import Darwin +#elseif canImport(Android) +private import Android // should be @usableFromInline #elseif canImport(Glibc) private import Glibc // should be @usableFromInline #elseif canImport(Musl) @@ -29,6 +31,9 @@ enum System { #if canImport(Darwin) let pid = Darwin.getpid() return Int(pid) + #elseif canImport(Android) + let pid = Android.getpid() + return Int(pid) #elseif canImport(Glibc) let pid = Glibc.getpid() return Int(pid) From c4d6281784f50bf2e60d3af45e83be1194056062 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 18 Mar 2025 11:24:26 +0100 Subject: [PATCH 570/580] Convert errors thrown from interceptors (#2209) Motivation: gRPC checks whether errors thrown from interceptors are `RPCError` and otherwise treats them as `unknown` (to avoid leaking internal information). There is a third possibility: the error is explicitly marked as being convertible to an `RPCError`. This check is currently missing when thrown from client/server interceptors. Modifications: - Catch `RPCErrorConvertible` in the client/server executors when thrown from interceptors - Add tests Result: Error information isn't dropped --- .../Client/Internal/ClientRPCExecutor.swift | 2 + .../Server/Internal/ServerRPCExecutor.swift | 2 + .../ClientRPCExecutorTestHarness.swift | 10 ++++- .../Internal/ClientRPCExecutorTests.swift | 21 +++++++++++ .../Internal/ServerRPCExecutorTests.swift | 19 ++++++++++ .../Call/Client/ClientInterceptors.swift | 37 +++++++++++-------- .../Call/Server/ServerInterceptors.swift | 37 +++++++++++-------- 7 files changed, 96 insertions(+), 32 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 9a1da1f7d..ff0b30cdb 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -186,6 +186,8 @@ extension ClientRPCExecutor { } } catch let error as RPCError { return StreamingClientResponse(error: error) + } catch let error as RPCErrorConvertible { + return StreamingClientResponse(error: RPCError(error)) } catch let other { let error = RPCError(code: .unknown, message: "", cause: other) return StreamingClientResponse(error: error) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index e2184de14..dee3303d5 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -330,6 +330,8 @@ extension ServerRPCExecutor { } } catch let error as RPCError { return StreamingServerResponse(error: error) + } catch let error as RPCErrorConvertible { + return StreamingServerResponse(error: RPCError(error)) } catch let other { let error = RPCError(code: .unknown, message: "", cause: other) return StreamingServerResponse(error: error) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 0a74ba96f..2b09af76b 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -29,6 +29,7 @@ struct ClientRPCExecutorTestHarness { private let server: ServerStreamHandler private let clientTransport: StreamCountingClientTransport private let serverTransport: StreamCountingServerTransport + private let interceptors: [any ClientInterceptor] var clientStreamsOpened: Int { self.clientTransport.streamsOpened @@ -42,8 +43,13 @@ struct ClientRPCExecutorTestHarness { self.serverTransport.acceptedStreamsCount } - init(transport: Transport = .inProcess, server: ServerStreamHandler) { + init( + transport: Transport = .inProcess, + server: ServerStreamHandler, + interceptors: [any ClientInterceptor] = [] + ) { self.server = server + self.interceptors = interceptors switch transport { case .inProcess: @@ -141,7 +147,7 @@ struct ClientRPCExecutorTestHarness { serializer: serializer, deserializer: deserializer, transport: self.clientTransport, - interceptors: [], + interceptors: self.interceptors, handler: handler ) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 1d3e78d69..4639dedb0 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -268,4 +268,25 @@ final class ClientRPCExecutorTests: XCTestCase { } } } + + func testInterceptorErrorConversion() async throws { + struct CustomError: RPCErrorConvertible, Error { + var rpcErrorCode: RPCError.Code { .alreadyExists } + var rpcErrorMessage: String { "foobar" } + var rpcErrorMetadata: Metadata { ["error": "yes"] } + } + + let tester = ClientRPCExecutorTestHarness( + server: .echo, + interceptors: [.throwError(CustomError())] + ) + + try await tester.unary(request: ClientRequest(message: [])) { response in + XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in + XCTAssertEqual(error.code, .alreadyExists) + XCTAssertEqual(error.message, "foobar") + XCTAssertEqual(error.metadata, ["error": "yes"]) + } + } + } } diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 09982b20d..215584ebf 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -374,4 +374,23 @@ final class ServerRPCExecutorTests: XCTestCase { ) } } + + func testInterceptorErrorConversion() async throws { + struct CustomError: RPCErrorConvertible, Error { + var rpcErrorCode: RPCError.Code { .alreadyExists } + var rpcErrorMessage: String { "foobar" } + var rpcErrorMetadata: Metadata { ["error": "yes"] } + } + + let harness = ServerRPCExecutorTestHarness(interceptors: [.throwError(CustomError())]) + try await harness.execute(handler: .throwing(CustomError())) { inbound in + try await inbound.write(.metadata(["foo": "bar"])) + await inbound.finish() + } consumer: { outbound in + let parts = try await outbound.collect() + let status = Status(code: .alreadyExists, message: "foobar") + let metadata: Metadata = ["error": "yes"] + XCTAssertEqual(parts, [.status(status, metadata)]) + } + } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index e13228b3e..ba6c1abf1 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -18,11 +18,11 @@ import GRPCCore extension ClientInterceptor where Self == RejectAllClientInterceptor { static func rejectAll(with error: RPCError) -> Self { - return RejectAllClientInterceptor(error: error, throw: false) + return RejectAllClientInterceptor(reject: error) } - static func throwError(_ error: RPCError) -> Self { - return RejectAllClientInterceptor(error: error, throw: true) + static func throwError(_ error: any Error) -> Self { + return RejectAllClientInterceptor(throw: error) } } @@ -35,15 +35,21 @@ extension ClientInterceptor where Self == RequestCountingClientInterceptor { /// Rejects all RPCs with the provided error. struct RejectAllClientInterceptor: ClientInterceptor { - /// The error to reject all RPCs with. - let error: RPCError - /// Whether the error should be thrown. If `false` then the request is rejected with the error - /// instead. - let `throw`: Bool + enum Mode: Sendable { + /// Throw the error rather. + case `throw`(any Error) + /// Reject the RPC with a given error. + case reject(RPCError) + } + + let mode: Mode + + init(throw error: any Error) { + self.mode = .throw(error) + } - init(error: RPCError, throw: Bool = false) { - self.error = error - self.`throw` = `throw` + init(reject error: RPCError) { + self.mode = .reject(error) } func intercept( @@ -54,10 +60,11 @@ struct RejectAllClientInterceptor: ClientInterceptor { ClientContext ) async throws -> StreamingClientResponse ) async throws -> StreamingClientResponse { - if self.throw { - throw self.error - } else { - return StreamingClientResponse(error: self.error) + switch self.mode { + case .throw(let error): + throw error + case .reject(let error): + return StreamingClientResponse(error: error) } } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index fdb869d1c..8340aa130 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -18,11 +18,11 @@ import GRPCCore extension ServerInterceptor where Self == RejectAllServerInterceptor { static func rejectAll(with error: RPCError) -> Self { - return RejectAllServerInterceptor(error: error, throw: false) + return RejectAllServerInterceptor(reject: error) } - static func throwError(_ error: RPCError) -> Self { - RejectAllServerInterceptor(error: error, throw: true) + static func throwError(_ error: any Error) -> Self { + RejectAllServerInterceptor(throw: error) } } @@ -34,15 +34,21 @@ extension ServerInterceptor where Self == RequestCountingServerInterceptor { /// Rejects all RPCs with the provided error. struct RejectAllServerInterceptor: ServerInterceptor { - /// The error to reject all RPCs with. - let error: RPCError - /// Whether the error should be thrown. If `false` then the request is rejected with the error - /// instead. - let `throw`: Bool + enum Mode: Sendable { + /// Throw the error rather. + case `throw`(any Error) + /// Reject the RPC with a given error. + case reject(RPCError) + } + + let mode: Mode + + init(throw error: any Error) { + self.mode = .throw(error) + } - init(error: RPCError, throw: Bool = false) { - self.error = error - self.`throw` = `throw` + init(reject error: RPCError) { + self.mode = .reject(error) } func intercept( @@ -53,10 +59,11 @@ struct RejectAllServerInterceptor: ServerInterceptor { ServerContext ) async throws -> StreamingServerResponse ) async throws -> StreamingServerResponse { - if self.throw { - throw self.error - } else { - return StreamingServerResponse(error: self.error) + switch self.mode { + case .throw(let error): + throw error + case .reject(let error): + return StreamingServerResponse(error: error) } } } From edb1ec6a5f5e3d3bd1f1a9353b18edc170df4689 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 27 Mar 2025 16:33:09 +0000 Subject: [PATCH 571/580] Improve error message when client is run when shutdown (#2215) Motivation: GRPCClient can throw an error when run is called more than once or if run is called after it has been shutdown. Normally this would happen if a user caller 'run()' more than once, but can also happen if 'withGRPCClient' is called and the client is never used and the body returns quickly. Modifications: - Improve the error message Result: Better error message. --- Sources/GRPCCore/GRPCClient.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index b3093f8b1..ffb6911ae 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -89,7 +89,11 @@ public final class GRPCClient: Sendable { case .stopping, .stopped: throw RuntimeError( code: .clientIsStopped, - message: "The client has stopped and can only be started once." + message: """ + Can't call 'runConnections()' as the client is stopped (or is stopping). \ + This can happen if the you call 'runConnections()' after shutting the \ + client down or if you used 'withGRPCClient' with an empty body. + """ ) } } From e0ba0ed6f5fa38e56865d2ab6979e8193645a68f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 14 Apr 2025 11:35:10 +0100 Subject: [PATCH 572/580] Add Swift 6.1 CI (#2221) Motivation: Swift 6.1 has been released, we should add it to our CI coverage. Modifications: - Add additional Swift 6.1 jobs where appropriate in main.yml, pull_request.yml - Add thresholds for Swift 6.1 Result: Improved test coverage. --- .github/workflows/main.yml | 1 + .github/workflows/pull_request.yml | 1 + .../6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json | 7 +++++++ .../6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json | 7 +++++++ ...GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json | 7 +++++++ ...e_binary_values_when_only_binary_values_stored.p90.json | 7 +++++++ ...Iterate_binary_values_when_only_strings_stored.p90.json | 7 +++++++ ...CSwiftBenchmark.Metadata_Iterate_string_values.p90.json | 7 +++++++ ...CSwiftBenchmark.Metadata_Remove_values_for_key.p90.json | 7 +++++++ IntegrationTests/Benchmarks/Thresholds/nightly-6.1 | 1 - 10 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json delete mode 120000 IntegrationTests/Benchmarks/Thresholds/nightly-6.1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ccfd80f58..571ee9bd2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,6 +14,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d923455d6..efb4dc4b9 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,6 +23,7 @@ jobs: linux_5_9_enabled: false linux_5_10_enabled: false linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json new file mode 100644 index 000000000..b642696c1 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 3012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json new file mode 100644 index 000000000..7fde30a69 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Add_string.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 4012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json new file mode 100644 index 000000000..b59f05063 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 2000, + "memoryLeaked" : 0, + "releaseCount" : 6001, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json new file mode 100644 index 000000000..5750750bc --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 2002001, + "retainCount" : 1999000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1 b/IntegrationTests/Benchmarks/Thresholds/nightly-6.1 deleted file mode 120000 index 11e4d1b5c..000000000 --- a/IntegrationTests/Benchmarks/Thresholds/nightly-6.1 +++ /dev/null @@ -1 +0,0 @@ -./nightly-next \ No newline at end of file From c295efd55e8e0565467054a7500eb4989f02a110 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 25 Apr 2025 16:52:38 +0100 Subject: [PATCH 573/580] Set tolerance to zero when using `Task.sleep` (#2225) `Task.sleep` will by default try and coalesce multiple timers into one, mostly for client-specific reasons such as performance, power consumption, etc. However, this is undesirable on servers, as it can increase latency, memory usage, and (in the case of gRPC) may result in timeouts not firing when they should. We can avoid this by setting the sleep `tolerance` to zero. --- .../ClientRPCExecutor+HedgingExecutor.swift | 4 ++-- .../ClientRPCExecutor+OneShotExecutor.swift | 2 +- .../Internal/ClientRPCExecutor+RetryExecutor.swift | 9 +++++++-- .../Call/Server/Internal/ServerRPCExecutor.swift | 2 +- ...ientRPCExecutorTestHarness+ServerBehavior.swift | 2 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 8 ++++---- .../Internal/Result+CatchingTests.swift | 2 +- .../InProcessClientTransportTests.swift | 14 +++++++------- 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 480b23817..e0743bee3 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -83,7 +83,7 @@ extension ClientRPCExecutor.HedgingExecutor { if let deadline = self.deadline { group.addTask { let result = await Result { - try await Task.sleep(until: deadline, clock: .continuous) + try await Task.sleep(until: deadline, tolerance: .zero, clock: .continuous) } return .timedOut(result) } @@ -533,7 +533,7 @@ extension ClientRPCExecutor.HedgingExecutor { self._isPushback = pushback self._handle = group.addCancellableTask { do { - try await Task.sleep(for: delay, clock: .continuous) + try await Task.sleep(for: delay, tolerance: .zero, clock: .continuous) return .scheduledAttemptFired(.ran) } catch { return .scheduledAttemptFired(.cancelled) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index cc21ad4fc..a4de402af 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -137,7 +137,7 @@ func withDeadline( return await withTaskGroup(of: _DeadlineChildTaskResult.self) { group in group.addTask { do { - try await Task.sleep(until: deadline) + try await Task.sleep(until: deadline, tolerance: .zero) return .deadlinePassed } catch { return .timeoutCancelled diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index bfc62203f..e5964baee 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -92,7 +92,7 @@ extension ClientRPCExecutor.RetryExecutor { if let deadline = self.deadline { group.addTask { let result = await Result { - try await Task.sleep(until: deadline, clock: .continuous) + try await Task.sleep(until: deadline, tolerance: .zero, clock: .continuous) } return .timedOut(result) } @@ -155,11 +155,16 @@ extension ClientRPCExecutor.RetryExecutor { // If the delay is overridden with server pushback then reset the iterator for the // next retry. delayIterator = delaySequence.makeIterator() - try? await Task.sleep(until: .now.advanced(by: delayOverride), clock: .continuous) + try? await Task.sleep( + until: .now.advanced(by: delayOverride), + tolerance: .zero, + clock: .continuous + ) } else { // The delay iterator never terminates. try? await Task.sleep( until: .now.advanced(by: delayIterator.next()!), + tolerance: .zero, clock: .continuous ) } diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index dee3303d5..7d5f42f05 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -123,7 +123,7 @@ struct ServerRPCExecutor { await withTaskGroup(of: Void.self) { group in group.addTask { do { - try await Task.sleep(for: timeout, clock: .continuous) + try await Task.sleep(for: timeout, tolerance: .zero, clock: .continuous) context.cancellation.cancel() } catch { () // Only cancel the RPC if the timeout completes. diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index 2b0f2b90f..cf4a4ccc1 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -112,7 +112,7 @@ extension ClientRPCExecutorTestHarness.ServerStreamHandler { static func sleepFor(duration: Duration, then handler: Self) -> Self { return Self { stream in - try await Task.sleep(until: .now.advanced(by: duration), clock: .continuous) + try await Task.sleep(until: .now.advanced(by: duration), tolerance: .zero, clock: .continuous) try await handler.handle(stream: stream) } } diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 477a43588..61fb3b6c6 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -40,7 +40,7 @@ final class GRPCClientTests: XCTestCase { transport: inProcess.client, interceptorPipeline: interceptorPipeline ) { client in - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) try await body(client, server) } } @@ -341,7 +341,7 @@ final class GRPCClientTests: XCTestCase { let task = Task { try await client.clientStreaming( request: StreamingClientRequest { writer in - try await Task.sleep(for: .seconds(5)) + try await Task.sleep(for: .seconds(5), tolerance: .zero) }, descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), @@ -382,7 +382,7 @@ final class GRPCClientTests: XCTestCase { // Run the client. let task = Task { try await client.runConnections() } // Make sure the client is run for the first time here. - try await Task.sleep(for: .milliseconds(10)) + try await Task.sleep(for: .milliseconds(10), tolerance: .zero) // Client is already running, should throw an error. await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { @@ -545,7 +545,7 @@ struct ClientTests { } // Make sure both server and client are running - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) try await body(client, server) client.beginGracefulShutdown() server.beginGracefulShutdown() diff --git a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift index cbe5ac742..d5bc65cd1 100644 --- a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift +++ b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift @@ -21,7 +21,7 @@ import XCTest final class ResultCatchingTests: XCTestCase { func testResultCatching() async { let result = await Result { - try? await Task.sleep(nanoseconds: 1) + try? await Task.sleep(for: .nanoseconds(1), tolerance: .zero) throw RPCError(code: .unknown, message: "foo") } diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index c1e5dfc9b..c64f97646 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -62,7 +62,7 @@ final class InProcessClientTransportTests: XCTestCase { try await client.connect() } group.addTask { - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) } try await group.next() @@ -97,7 +97,7 @@ final class InProcessClientTransportTests: XCTestCase { try await client.connect() } group.addTask { - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) } try await group.next() @@ -121,7 +121,7 @@ final class InProcessClientTransportTests: XCTestCase { group.addTask { // Add a sleep to make sure connection happens after `withStream` has been called, // to test pending streams are handled correctly. - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) try await client.connect() } @@ -171,7 +171,7 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) client.beginGracefulShutdown() } @@ -252,18 +252,18 @@ final class InProcessClientTransportTests: XCTestCase { group.addTask { try await client.withStream(descriptor: .testTest, options: .defaults) { stream, _ in - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) } } group.addTask { try await client.withStream(descriptor: .testTest, options: .defaults) { stream, _ in - try await Task.sleep(for: .milliseconds(100)) + try await Task.sleep(for: .milliseconds(100), tolerance: .zero) } } group.addTask { - try await Task.sleep(for: .milliseconds(50)) + try await Task.sleep(for: .milliseconds(50), tolerance: .zero) client.beginGracefulShutdown() } From d8185e7a77e7623be5617c625c06ffe154598020 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 1 May 2025 08:03:56 +0100 Subject: [PATCH 574/580] Fix a few warnings (#2229) Motivation: We should aim to keep warnings at zero. There were a few littered around as a result of Swift 6.1. Modifications: - Fix the warnings - Enabled warnings-as-errors in CI to avoid regressions Result: No warnings --- .github/workflows/main.yml | 4 ++-- .github/workflows/pull_request.yml | 4 ++-- .../GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift | 2 +- .../GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift | 2 +- Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift | 4 ++-- Tests/GRPCCoreTests/GRPCClientTests.swift | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 571ee9bd2..7bbb50f13 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,8 +13,8 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false - linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index efb4dc4b9..a7aa1ae96 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,8 +22,8 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false - linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index ff0b30cdb..0cbe1490a 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -186,7 +186,7 @@ extension ClientRPCExecutor { } } catch let error as RPCError { return StreamingClientResponse(error: error) - } catch let error as RPCErrorConvertible { + } catch let error as any RPCErrorConvertible { return StreamingClientResponse(error: RPCError(error)) } catch let other { let error = RPCError(code: .unknown, message: "", cause: other) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 7d5f42f05..416a2c4c7 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -330,7 +330,7 @@ extension ServerRPCExecutor { } } catch let error as RPCError { return StreamingServerResponse(error: error) - } catch let error as RPCErrorConvertible { + } catch let error as any RPCErrorConvertible { return StreamingServerResponse(error: RPCError(error)) } catch let other { let error = RPCError(code: .unknown, message: "", cause: other) diff --git a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift index 4461620d0..2152143c4 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift @@ -27,7 +27,7 @@ struct ServerResponseTests { trailingMetadata: ["metadata": "trailing"] ) - let contents = try #require(try response.accepted.get()) + let contents = try response.accepted.get() #expect(contents.message == "message") #expect(contents.metadata == ["metadata": "initial"]) #expect(contents.trailingMetadata == ["metadata": "trailing"]) @@ -55,7 +55,7 @@ struct ServerResponseTests { return ["metadata": "trailing"] } - let contents = try #require(try response.accepted.get()) + let contents = try response.accepted.get() #expect(contents.metadata == ["metadata": "initial"]) let trailingMetadata = try await contents.producer(.failTestOnWrite()) #expect(trailingMetadata == ["metadata": "trailing"]) diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 61fb3b6c6..f038760d8 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -431,7 +431,7 @@ struct ClientTests { deserializer: IdentityDeserializer(), options: .defaults ) { response in - let message = try #require(try response.message) + let message = try response.message #expect(message == Array("hello".utf8)) } @@ -449,7 +449,7 @@ struct ClientTests { deserializer: IdentityDeserializer(), options: .defaults ) { response in - let message = try #require(try response.message) + let message = try response.message #expect(message == Array("Hello, Swift!".utf8)) } @@ -494,7 +494,7 @@ struct ClientTests { deserializer: IdentityDeserializer(), options: .defaults ) { response in - let message = try #require(try response.message) + let message = try response.message #expect(message == Array("hello".utf8)) } @@ -512,7 +512,7 @@ struct ClientTests { deserializer: IdentityDeserializer(), options: .defaults ) { response in - let message = try #require(try response.message) + let message = try response.message #expect(message == Array("hello".utf8)) } From 0d850d6d666fac316cc2512dff755cc9b211c312 Mon Sep 17 00:00:00 2001 From: Jeff Davey <86268978+jtdavey@users.noreply.github.com> Date: Fri, 2 May 2025 01:25:23 -0600 Subject: [PATCH 575/580] Add a new field, transportSpecific to ServerContext (#2228) Motivation: Currently there's no way to plumb through details from the transport level to a request handler. Adding this field allows transports, such as the nio transport, to add the peer certificate to the server context when using mTLS. From there there it's easy for an interceptor to take this data and propogate it forward to a request handler. Modifications: This PR adds a single field to the ServerContext that transports can use Result: A new field will be accessible to transports and consumers of the API --------- Co-authored-by: George Barnett --- Sources/GRPCCore/Call/Server/ServerContext.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sources/GRPCCore/Call/Server/ServerContext.swift b/Sources/GRPCCore/Call/Server/ServerContext.swift index d2518adf9..f027ad058 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -16,6 +16,10 @@ /// Additional information about an RPC handled by a server. public struct ServerContext: Sendable { + + /// Protocol used to help identify transport specific context fields + public protocol TransportSpecific: Sendable {} + /// A description of the method being called. public var descriptor: MethodDescriptor @@ -45,6 +49,15 @@ public struct ServerContext: Sendable { /// - "in-process:27182". public var localPeer: String + /// An optional field for transports to store specific data + /// + /// Refer to the transport documentation to understand what type of + /// value this field will contain, if any. + /// + /// An example of what this field can be used for, would be to store + /// things like a peer certificate from a mTLS connection + public var transportSpecific: (any TransportSpecific)? + /// A handle for checking the cancellation status of an RPC. public var cancellation: RPCCancellationHandle From b18eb73601e39e185014bd0695fe57f28c603623 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 8 May 2025 08:06:44 +0100 Subject: [PATCH 576/580] Generate availability annotations (#2232) Motivation: gRPC is moving from specifying the platforms in the package manifest to annotating code with availability annotations. In order to do this the generateed code must also be annotated. Modifications: - Generate appropriate annotations Result: Generated code has availability annotations --------- Co-authored-by: Gus Cairo --- Sources/GRPCCodeGen/CodeGenerator.swift | 62 ++++++++++++++++++- .../Internal/Renderer/TextBasedRenderer.swift | 2 + .../StructuredSwift+ServiceMetadata.swift | 19 +++++- .../StructuredSwiftRepresentation.swift | 37 ++++++----- .../Translator/ClientCodeTranslator.swift | 7 ++- .../IDLToStructuredSwiftTranslator.swift | 6 +- .../Translator/MetadataTranslator.swift | 8 ++- .../Translator/ServerCodeTranslator.swift | 9 +-- ...lientCodeTranslatorSnippetBasedTests.swift | 9 ++- ...uredSwiftTranslatorSnippetBasedTests.swift | 36 ++++++++--- ...erverCodeTranslatorSnippetBasedTests.swift | 10 ++- ...TypealiasTranslatorSnippetBasedTests.swift | 5 +- 12 files changed, 170 insertions(+), 40 deletions(-) diff --git a/Sources/GRPCCodeGen/CodeGenerator.swift b/Sources/GRPCCodeGen/CodeGenerator.swift index 25d17a6cf..07ffd9848 100644 --- a/Sources/GRPCCodeGen/CodeGenerator.swift +++ b/Sources/GRPCCodeGen/CodeGenerator.swift @@ -42,6 +42,8 @@ public struct CodeGenerator: Sendable { public var server: Bool /// The name of the core gRPC module. public var grpcCoreModuleName: String + /// The availability annotations to use on the generated code. + public var availability: AvailabilityAnnotations = .default /// Creates a new configuration. /// @@ -84,6 +86,50 @@ public struct CodeGenerator: Sendable { /// The generated code will have `package` access level. public static var `package`: Self { Self(level: .`package`) } } + + // The availability that generated code is annotated with. + public struct AvailabilityAnnotations: Sendable, Hashable { + public struct Platform: Sendable, Hashable { + /// The name of the OS, e.g. 'macOS'. + public var os: String + /// The version of the OS, e.g. '15.0'. + public var version: String + + public init(os: String, version: String) { + self.os = os + self.version = version + } + } + + fileprivate enum Wrapped: Sendable, Hashable { + case macOS15Aligned + case custom([Platform]) + } + + fileprivate var wrapped: Wrapped + + private init(_ wrapped: Wrapped) { + self.wrapped = wrapped + } + + /// Use the default availability. + /// + /// The default platform availability is: + /// - macOS 15.0 + /// - iOS 18.0 + /// - tvOS 18.0 + /// - watchOS 11.0 + /// - visionOS 2.0 + public static var `default`: Self { + Self(.macOS15Aligned) + } + + /// Use a custom set of availability attributes. + /// - Parameter platforms: Availability requirements. + public static func custom(_ platforms: [Platform]) -> Self { + Self(.custom(platforms)) + } + } } /// Transforms a ``CodeGenerationRequest`` object into a ``SourceFile`` object containing @@ -100,7 +146,8 @@ public struct CodeGenerator: Sendable { accessLevelOnImports: self.config.accessLevelOnImports, client: self.config.client, server: self.config.server, - grpcCoreModuleName: self.config.grpcCoreModuleName + grpcCoreModuleName: self.config.grpcCoreModuleName, + availability: AvailabilityDescription(self.config.availability) ) let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) @@ -108,3 +155,16 @@ public struct CodeGenerator: Sendable { return sourceFile } } + +extension AvailabilityDescription { + init(_ availability: CodeGenerator.Config.AvailabilityAnnotations) throws { + switch availability.wrapped { + case .macOS15Aligned: + self = .macOS15Aligned + case .custom(let platforms): + self.osVersions = platforms.map { + .init(os: .init(name: $0.os), version: $0.version) + } + } + } +} diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index e1fac9546..e4cb173af 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -877,6 +877,8 @@ struct TextBasedRenderer: RendererProtocol { renderCommentableDeclaration(comment: comment, declaration: nestedDeclaration) case let .deprecated(deprecation, nestedDeclaration): renderDeprecatedDeclaration(deprecation: deprecation, declaration: nestedDeclaration) + case let .guarded(availability, nestedDeclaration): + renderGuardedDeclaration(availability: availability, declaration: nestedDeclaration) case .variable(let variableDescription): renderVariable(variableDescription) case .extension(let extensionDescription): renderExtension(extensionDescription) case .struct(let structDescription): renderStruct(structDescription) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index dd51eb496..7e06ec07a 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -14,6 +14,18 @@ * limitations under the License. */ +extension AvailabilityDescription { + package static let macOS15Aligned = AvailabilityDescription( + osVersions: [ + OSVersion(os: .macOS, version: "15.0"), + OSVersion(os: .iOS, version: "18.0"), + OSVersion(os: .watchOS, version: "11.0"), + OSVersion(os: .tvOS, version: "18.0"), + OSVersion(os: .visionOS, version: "2.0"), + ] + ) +} + extension TypealiasDescription { /// `typealias Input = ` package static func methodInput( @@ -341,6 +353,7 @@ extension [CodeBlock] { package static func serviceMetadata( accessModifier: AccessModifier? = nil, service: ServiceDescriptor, + availability: AvailabilityDescription, namer: Namer = Namer() ) -> Self { var blocks: [CodeBlock] = [] @@ -357,7 +370,7 @@ extension [CodeBlock] { comment: .doc( "Namespace containing generated types for the \"\(service.name.identifyingName)\" service." ), - item: .declaration(.enum(serviceNamespace)) + item: .declaration(.guarded(availability, .enum(serviceNamespace))) ) ) @@ -367,7 +380,9 @@ extension [CodeBlock] { literalFullyQualifiedService: service.name.identifyingName, namer: namer ) - blocks.append(CodeBlock(item: .declaration(.extension(descriptorExtension)))) + blocks.append( + CodeBlock(item: .declaration(.guarded(availability, .extension(descriptorExtension)))) + ) return blocks } diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 8db1cca87..52ad2ffae 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -797,6 +797,9 @@ indirect enum Declaration: Equatable, Codable, Sendable { /// A declaration that adds a comment on top of the provided declaration. case deprecated(DeprecationDescription, Declaration) + /// A declaration that adds an availability guard on top of the provided declaration. + case guarded(AvailabilityDescription, Declaration) + /// A variable declaration. case variable(VariableDescription) @@ -837,18 +840,18 @@ struct DeprecationDescription: Equatable, Codable, Sendable { /// A description of an availability guard. /// /// For example: `@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)` -struct AvailabilityDescription: Equatable, Codable, Sendable { +package struct AvailabilityDescription: Equatable, Codable, Sendable { /// The array of OSes and versions which are specified in the availability guard. - var osVersions: [OSVersion] - init(osVersions: [OSVersion]) { + package var osVersions: [OSVersion] + package init(osVersions: [OSVersion]) { self.osVersions = osVersions } /// An OS and its version. - struct OSVersion: Equatable, Codable, Sendable { - var os: OS - var version: String - init(os: OS, version: String) { + package struct OSVersion: Equatable, Codable, Sendable { + package var os: OS + package var version: String + package init(os: OS, version: String) { self.os = os self.version = version } @@ -856,18 +859,18 @@ struct AvailabilityDescription: Equatable, Codable, Sendable { /// One of the possible OSes. // swift-format-ignore: DontRepeatTypeInStaticProperties - struct OS: Equatable, Codable, Sendable { - var name: String + package struct OS: Equatable, Codable, Sendable { + package var name: String - init(name: String) { + package init(name: String) { self.name = name } - static let macOS = Self(name: "macOS") - static let iOS = Self(name: "iOS") - static let watchOS = Self(name: "watchOS") - static let tvOS = Self(name: "tvOS") - static let visionOS = Self(name: "visionOS") + package static let macOS = Self(name: "macOS") + package static let iOS = Self(name: "iOS") + package static let watchOS = Self(name: "watchOS") + package static let tvOS = Self(name: "tvOS") + package static let visionOS = Self(name: "visionOS") } } @@ -1873,6 +1876,7 @@ extension Declaration { switch self { case .commentable(_, let declaration): return declaration.accessModifier case .deprecated(_, let declaration): return declaration.accessModifier + case .guarded(_, let declaration): return declaration.accessModifier case .variable(let variableDescription): return variableDescription.accessModifier case .extension(let extensionDescription): return extensionDescription.accessModifier case .struct(let structDescription): return structDescription.accessModifier @@ -1891,6 +1895,9 @@ extension Declaration { case .deprecated(let deprecationDescription, var declaration): declaration.accessModifier = newValue self = .deprecated(deprecationDescription, declaration) + case .guarded(let availability, var declaration): + declaration.accessModifier = newValue + self = .guarded(availability, declaration) case .variable(var variableDescription): variableDescription.accessModifier = newValue self = .variable(variableDescription) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index d4d9f87ad..443bf9c17 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -81,6 +81,7 @@ struct ClientCodeTranslator { func translate( accessModifier: AccessModifier, service: ServiceDescriptor, + availability: AvailabilityDescription, namer: Namer = Namer(), serializer: (String) -> String, deserializer: (String) -> String @@ -132,7 +133,7 @@ struct ClientCodeTranslator { ), ] ) - blocks.append(.declaration(.extension(`extension`))) + blocks.append(.declaration(.guarded(availability, .extension(`extension`)))) let extensionWithDefaults: ExtensionDescription = .clientMethodSignatureWithDefaults( accessLevel: accessModifier, @@ -145,7 +146,7 @@ struct ClientCodeTranslator { blocks.append( CodeBlock( comment: .inline("Helpers providing default arguments to 'ClientProtocol' methods."), - item: .declaration(.extension(extensionWithDefaults)) + item: .declaration(.guarded(availability, .extension(extensionWithDefaults))) ) ) @@ -158,7 +159,7 @@ struct ClientCodeTranslator { blocks.append( CodeBlock( comment: .inline("Helpers providing sugared APIs for 'ClientProtocol' methods."), - item: .declaration(.extension(extensionWithExplodedAPI)) + item: .declaration(.guarded(availability, .extension(extensionWithExplodedAPI))) ) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index e83fa34c7..c496980b7 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -26,7 +26,8 @@ package struct IDLToStructuredSwiftTranslator { accessLevelOnImports: Bool, client: Bool, server: Bool, - grpcCoreModuleName: String + grpcCoreModuleName: String, + availability: AvailabilityDescription ) throws -> StructuredSwiftRepresentation { try self.validateInput(codeGenerationRequest) let accessModifier = AccessModifier(accessLevel) @@ -46,6 +47,7 @@ package struct IDLToStructuredSwiftTranslator { let metadata = metadataTranslator.translate( accessModifier: accessModifier, service: service, + availability: availability, namer: namer ) codeBlocks.append(contentsOf: metadata) @@ -58,6 +60,7 @@ package struct IDLToStructuredSwiftTranslator { let blocks = serverTranslator.translate( accessModifier: accessModifier, service: service, + availability: availability, namer: namer, serializer: codeGenerationRequest.makeSerializerCodeSnippet, deserializer: codeGenerationRequest.makeDeserializerCodeSnippet @@ -72,6 +75,7 @@ package struct IDLToStructuredSwiftTranslator { let blocks = clientTranslator.translate( accessModifier: accessModifier, service: service, + availability: availability, namer: namer, serializer: codeGenerationRequest.makeSerializerCodeSnippet, deserializer: codeGenerationRequest.makeDeserializerCodeSnippet diff --git a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift index 41252995e..2fc64821c 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift @@ -20,8 +20,14 @@ struct MetadataTranslator { func translate( accessModifier: AccessModifier, service: ServiceDescriptor, + availability: AvailabilityDescription, namer: Namer = Namer() ) -> [CodeBlock] { - .serviceMetadata(accessModifier: accessModifier, service: service, namer: namer) + .serviceMetadata( + accessModifier: accessModifier, + service: service, + availability: availability, + namer: namer + ) } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 2bf06757f..caf981742 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -63,6 +63,7 @@ struct ServerCodeTranslator { func translate( accessModifier: AccessModifier, service: ServiceDescriptor, + availability: AvailabilityDescription, namer: Namer = Namer(), serializer: (String) -> String, deserializer: (String) -> String @@ -129,7 +130,7 @@ struct ServerCodeTranslator { ), ] ) - blocks.append(.declaration(.extension(`extension`))) + blocks.append(.declaration(.guarded(availability, .extension(`extension`)))) // extension .StreamingServiceProtocol> { ... } let registerExtension: ExtensionDescription = .registrableRPCServiceDefaultImplementation( @@ -144,7 +145,7 @@ struct ServerCodeTranslator { blocks.append( CodeBlock( comment: .inline("Default implementation of 'registerMethods(with:)'."), - item: .declaration(.extension(registerExtension)) + item: .declaration(.guarded(availability, .extension(registerExtension))) ) ) @@ -161,7 +162,7 @@ struct ServerCodeTranslator { comment: .inline( "Default implementation of streaming methods from 'StreamingServiceProtocol'." ), - item: .declaration(.extension(streamingServiceDefaultImplExtension)) + item: .declaration(.guarded(availability, .extension(streamingServiceDefaultImplExtension))) ) ) @@ -175,7 +176,7 @@ struct ServerCodeTranslator { blocks.append( CodeBlock( comment: .inline("Default implementation of methods from 'ServiceProtocol'."), - item: .declaration(.extension(serviceDefaultImplExtension)) + item: .declaration(.guarded(availability, .extension(serviceDefaultImplExtension))) ) ) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index fd380c806..064791b78 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -42,6 +42,7 @@ struct ClientCodeTranslatorSnippetBasedTests { ) let expectedSwift = """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA { /// Generated client protocol for the "namespaceA.ServiceA" service. /// @@ -132,6 +133,7 @@ struct ClientCodeTranslatorSnippetBasedTests { } } // Helpers providing default arguments to 'ClientProtocol' methods. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { /// Call the "MethodA" method. /// @@ -163,6 +165,7 @@ struct ClientCodeTranslatorSnippetBasedTests { } } // Helpers providing sugared APIs for 'ClientProtocol' methods. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { /// Call the "MethodA" method. /// @@ -208,7 +211,11 @@ struct ClientCodeTranslatorSnippetBasedTests { service: ServiceDescriptor ) -> String { let translator = ClientCodeTranslator() - let codeBlocks = translator.translate(accessModifier: accessLevel, service: service) { + let codeBlocks = translator.translate( + accessModifier: accessLevel, + service: service, + availability: .macOS15Aligned + ) { "GRPCProtobuf.ProtobufSerializer<\($0)>()" } deserializer: { "GRPCProtobuf.ProtobufDeserializer<\($0)>()" diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 05375ae4f..eb9c85e5a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -56,6 +56,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // MARK: - namespaceA.ServiceA /// Namespace containing generated types for the "namespaceA.ServiceA" service. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public enum NamespaceA_ServiceA { /// Service descriptor for the "namespaceA.ServiceA" service. public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") @@ -66,6 +67,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCCore.ServiceDescriptor { /// Service descriptor for the "namespaceA.ServiceA" service. public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") @@ -73,6 +75,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // MARK: namespaceA.ServiceA (server) + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA { /// Streaming variant of the service protocol for the "namespaceA.ServiceA" service. /// @@ -116,15 +119,18 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { } // Default implementation of 'registerMethods(with:)'. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport {} } // Default implementation of streaming methods from 'StreamingServiceProtocol'. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } // Default implementation of methods from 'ServiceProtocol'. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.SimpleServiceProtocol { } """ @@ -207,7 +213,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: false, client: true, server: true, - grpcCoreModuleName: String("GRPCCore".reversed()) + grpcCoreModuleName: String("GRPCCore".reversed()), + availability: .macOS15Aligned ) let renderer = TextBasedRenderer.default let sourceFile = try renderer.render(structured: structuredSwift) @@ -248,7 +255,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: false, server: server, - grpcCoreModuleName: grpcCoreModuleName + grpcCoreModuleName: grpcCoreModuleName, + availability: .macOS15Aligned ) let renderer = TextBasedRenderer.default let sourceFile = try renderer.render(structured: structuredSwift) @@ -277,7 +285,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -325,7 +334,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -361,7 +371,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -408,7 +419,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in XCTAssertEqual( @@ -453,7 +465,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in XCTAssertEqual( @@ -514,7 +527,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in XCTAssertEqual( @@ -568,7 +582,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in @@ -615,7 +630,8 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { accessLevelOnImports: true, client: true, server: true, - grpcCoreModuleName: "GRPCCore" + grpcCoreModuleName: "GRPCCore", + availability: .macOS15Aligned ) ) { error in diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 1b41acc1c..bdbfcba9d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -42,6 +42,7 @@ final class ServerCodeTranslatorSnippetBasedTests { ) let expectedSwift = """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA { /// Streaming variant of the service protocol for the "namespaceA.AlongNameForServiceA" service. /// @@ -138,6 +139,7 @@ final class ServerCodeTranslatorSnippetBasedTests { } } // Default implementation of 'registerMethods(with:)'. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) where Transport: GRPCCore.ServerTransport { router.registerHandler( @@ -154,6 +156,7 @@ final class ServerCodeTranslatorSnippetBasedTests { } } // Default implementation of streaming methods from 'StreamingServiceProtocol'. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func unary( request: GRPCCore.StreamingServerRequest, @@ -167,6 +170,7 @@ final class ServerCodeTranslatorSnippetBasedTests { } } // Default implementation of methods from 'ServiceProtocol'. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.SimpleServiceProtocol { public func unary( request: GRPCCore.ServerRequest, @@ -192,7 +196,11 @@ final class ServerCodeTranslatorSnippetBasedTests { service: ServiceDescriptor ) -> String { let translator = ServerCodeTranslator() - let codeBlocks = translator.translate(accessModifier: accessLevel, service: service) { + let codeBlocks = translator.translate( + accessModifier: accessLevel, + service: service, + availability: .macOS15Aligned + ) { "GRPCProtobuf.ProtobufSerializer<\($0)>()" } deserializer: { "GRPCProtobuf.ProtobufDeserializer<\($0)>()" diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index ba6d4d249..4f6168c4b 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -42,6 +42,7 @@ struct TypealiasTranslatorSnippetBasedTests { let expectedSwift = """ /// Namespace containing generated types for the "namespaceA.ServiceA" service. + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public enum NamespaceA_ServiceA { /// Service descriptor for the "namespaceA.ServiceA" service. public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") @@ -65,6 +66,7 @@ struct TypealiasTranslatorSnippetBasedTests { ] } } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCCore.ServiceDescriptor { /// Service descriptor for the "namespaceA.ServiceA" service. public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") @@ -83,7 +85,8 @@ extension TypealiasTranslatorSnippetBasedTests { let translator = MetadataTranslator() let codeBlocks = translator.translate( accessModifier: AccessModifier(accessLevel), - service: service + service: service, + availability: .macOS15Aligned ) let renderer = TextBasedRenderer.default From 90d909422a1304fcf50f01075f8cd602b2a51eea Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 13 May 2025 11:59:23 +0100 Subject: [PATCH 577/580] Add explicit availability annotations (#2234) Motivation: gRPC is moving from specifying the platforms in the package manifest to annotating code with availability annotations. Modifications: - Explicitly annotate public API - Add '-require-explicit-availability' to CI - Drop platforms Result: - gRPC can be used by packages which don't specify their platforms in the manifest --- .github/workflows/main.yml | 4 +- .github/workflows/pull_request.yml | 4 +- Package.swift | 36 +++-- Sources/GRPCCodeGen/CodeGenError.swift | 3 + .../GRPCCodeGen/CodeGenerationRequest.swift | 11 ++ Sources/GRPCCodeGen/CodeGenerator.swift | 6 + .../Internal/Renderer/RendererProtocol.swift | 1 + .../Internal/Renderer/TextBasedRenderer.swift | 2 + .../Internal/StructuredSwift+Client.swift | 6 + .../Internal/StructuredSwift+Server.swift | 7 + .../StructuredSwift+ServiceMetadata.swift | 2 + .../Translator/ClientCodeTranslator.swift | 1 + .../Internal/Translator/Docs.swift | 1 + .../IDLToStructuredSwiftTranslator.swift | 3 + .../Translator/MetadataTranslator.swift | 1 + .../Translator/ServerCodeTranslator.swift | 1 + Sources/GRPCCodeGen/SourceFile.swift | 1 + .../GRPCCore/Call/Client/CallOptions.swift | 3 + .../GRPCCore/Call/Client/ClientContext.swift | 1 + .../Call/Client/ClientInterceptor.swift | 1 + .../GRPCCore/Call/Client/ClientRequest.swift | 2 + .../GRPCCore/Call/Client/ClientResponse.swift | 6 + .../ClientRPCExecutor+HedgingExecutor.swift | 4 + .../ClientRPCExecutor+OneShotExecutor.swift | 5 + .../ClientRPCExecutor+RetryExecutor.swift | 3 + .../Client/Internal/ClientRPCExecutor.swift | 2 + .../Internal/ClientRequest+Convenience.swift | 1 + .../Internal/ClientResponse+Convenience.swift | 3 + .../Internal/ClientStreamExecutor.swift | 1 + .../Client/Internal/RetryDelaySequence.swift | 1 + .../Call/ConditionalInterceptor.swift | 3 + .../Internal/ServerCancellationManager.swift | 2 + .../Server/Internal/ServerRPCExecutor.swift | 2 + Sources/GRPCCore/Call/Server/RPCRouter.swift | 3 + .../Call/Server/RegistrableRPCService.swift | 1 + .../ServerContext+RPCCancellationHandle.swift | 3 + .../GRPCCore/Call/Server/ServerContext.swift | 3 + .../Call/Server/ServerInterceptor.swift | 1 + .../GRPCCore/Call/Server/ServerRequest.swift | 4 + .../GRPCCore/Call/Server/ServerResponse.swift | 5 + Sources/GRPCCore/Coding/Coding.swift | 2 + .../Coding/CompressionAlgorithm.swift | 3 + .../GRPCCore/Coding/GRPCContiguousBytes.swift | 2 + .../GRPCCore/Configuration/MethodConfig.swift | 12 ++ .../Configuration/ServiceConfig.swift | 6 + Sources/GRPCCore/GRPCClient.swift | 3 + Sources/GRPCCore/GRPCServer.swift | 3 + .../UnsafeTransfer.swift | 2 + Sources/GRPCCore/Internal/Metadata+GRPC.swift | 3 + Sources/GRPCCore/Internal/MethodConfigs.swift | 1 + .../GRPCCore/Internal/Result+Catching.swift | 2 + .../GRPCCore/Internal/String+Extensions.swift | 4 + .../Internal/TaskGroup+CancellableTask.swift | 2 + Sources/GRPCCore/Metadata.swift | 14 +- Sources/GRPCCore/MethodDescriptor.swift | 2 + Sources/GRPCCore/RPCError.swift | 8 + Sources/GRPCCore/RuntimeError.swift | 4 + Sources/GRPCCore/ServiceDescriptor.swift | 2 + Sources/GRPCCore/Status.swift | 6 + .../Internal/AsyncSequenceOfOne.swift | 2 + .../BroadcastAsyncSequence+RPCWriter.swift | 1 + .../Internal/BroadcastAsyncSequence.swift | 9 ++ .../Internal/GRPCAsyncThrowingStream.swift | 3 + .../Streaming/Internal/RPCWriter+Map.swift | 2 + .../RPCWriter+MessageToRPCResponsePart.swift | 2 + .../Internal/RPCWriter+Serialize.swift | 2 + .../UncheckedAsyncIteratorSequence.swift | 3 +- .../GRPCCore/Streaming/RPCAsyncSequence.swift | 1 + .../Streaming/RPCWriter+Closable.swift | 1 + Sources/GRPCCore/Streaming/RPCWriter.swift | 1 + .../Streaming/RPCWriterProtocol.swift | 3 + Sources/GRPCCore/Timeout.swift | 2 + .../GRPCCore/Transport/ClientTransport.swift | 1 + Sources/GRPCCore/Transport/RPCParts.swift | 8 + Sources/GRPCCore/Transport/RPCStream.swift | 1 + .../GRPCCore/Transport/RetryThrottle.swift | 1 + .../GRPCCore/Transport/ServerTransport.swift | 1 + .../InProcessTransport+Client.swift | 1 + .../InProcessTransport+Server.swift | 1 + .../InProcessTransport.swift | 1 + .../Renderer/TextBasedRendererTests.swift | 3 +- .../StructuredSwift+ClientTests.swift | 10 ++ .../StructuredSwift+ImportTests.swift | 28 ++-- .../StructuredSwift+MetadataTests.swift | 11 ++ .../StructuredSwift+ServerTests.swift | 10 ++ .../Internal/StructuredSwiftTestHelpers.swift | 4 + ...lientCodeTranslatorSnippetBasedTests.swift | 2 + .../Internal/Translator/DocsTests.swift | 4 + ...uredSwiftTranslatorSnippetBasedTests.swift | 1 + ...erverCodeTranslatorSnippetBasedTests.swift | 2 + .../Internal/Translator/TestFunctions.swift | 1 + ...TypealiasTranslatorSnippetBasedTests.swift | 2 + .../Call/Client/ClientRequestTests.swift | 1 + .../Call/Client/ClientResponseTests.swift | 1 + ...PCExecutorTestHarness+ServerBehavior.swift | 2 + ...ientRPCExecutorTestHarness+Transport.swift | 1 + .../ClientRPCExecutorTestHarness.swift | 1 + .../ClientRPCExecutorTests+Hedging.swift | 2 + .../ClientRPCExecutorTests+Retries.swift | 2 + .../Internal/ClientRPCExecutorTests.swift | 1 + .../Call/Client/RetryDelaySequenceTests.swift | 1 + .../Call/ConditionalInterceptorTests.swift | 2 + .../ServerCancellationManagerTests.swift | 6 + .../ServerRPCExecutorTestHarness.swift | 2 + .../Internal/ServerRPCExecutorTests.swift | 1 + .../Call/Server/RPCRouterTests.swift | 2 + .../Call/Server/ServerContextTests.swift | 3 + .../Call/Server/ServerRequestTests.swift | 1 + .../Call/Server/ServerResponseTests.swift | 8 + Tests/GRPCCoreTests/Coding/CodingTests.swift | 1 + .../Coding/CompressionAlgorithmTests.swift | 1 + .../MethodConfigCodingTests.swift | 18 +++ .../Configuration/MethodConfigTests.swift | 2 + .../ServiceConfigCodingTests.swift | 1 + Tests/GRPCCoreTests/GRPCClientTests.swift | 4 + Tests/GRPCCoreTests/GRPCServerTests.swift | 7 + .../Internal/Metadata+GRPCTests.swift | 1 + .../Internal/MethodConfigsTests.swift | 1 + .../Internal/Result+CatchingTests.swift | 1 + Tests/GRPCCoreTests/MetadataTests.swift | 137 +++++++++++------- .../GRPCCoreTests/MethodDescriptorTests.swift | 2 + Tests/GRPCCoreTests/RPCErrorTests.swift | 10 ++ Tests/GRPCCoreTests/RPCPartsTests.swift | 1 + Tests/GRPCCoreTests/RuntimeErrorTests.swift | 1 + .../ServiceDescriptorTests.swift | 2 + Tests/GRPCCoreTests/StatusTests.swift | 8 + .../Internal/AsyncSequenceOfOne.swift | 1 + .../BroadcastAsyncSequenceTests.swift | 1 + .../Test Utilities/AtomicCounter.swift | 1 + .../Call/Client/ClientInterceptors.swift | 4 + .../Call/Server/ServerInterceptors.swift | 4 + .../Test Utilities/Coding+Identity.swift | 2 + .../Test Utilities/Coding+JSON.swift | 2 + .../RPCAsyncSequence+Utilities.swift | 1 + .../Test Utilities/RPCWriter+Utilities.swift | 1 + .../Test Utilities/Services/BinaryEcho.swift | 1 + .../Test Utilities/Services/HelloWorld.swift | 1 + .../Transport/AnyTransport.swift | 2 + .../Transport/StreamCountingTransport.swift | 2 + .../Transport/ThrowingTransport.swift | 3 + .../Test Utilities/XCTest+Utilities.swift | 9 ++ Tests/GRPCCoreTests/TimeoutTests.swift | 4 + .../Transport/RetryThrottleTests.swift | 1 + .../ClientServerWithMethods.swift | 2 + .../InProcessClientTransportTests.swift | 3 + .../InProcessServerTransportTests.swift | 1 + .../InProcessTransportTests.swift | 9 ++ .../Test Utilities/JSONSerializing.swift | 2 + 148 files changed, 562 insertions(+), 86 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7bbb50f13..b00e62a07 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,8 +13,8 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false - linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a7aa1ae96..cada4efcc 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,8 +22,8 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false - linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" diff --git a/Package.swift b/Package.swift index 24f8cc8df..0b1839e24 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,7 @@ * limitations under the License. */ +import CompilerPluginSupport import PackageDescription let products: [Product] = [ @@ -45,12 +46,28 @@ let dependencies: [Package.Dependency] = [ ), ] -let defaultSwiftSettings: [SwiftSetting] = [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault"), - .enableUpcomingFeature("MemberImportVisibility"), -] +// ------------------------------------------------------------------------------------------------- + +// This adds some build settings which allow us to map "@available(gRPCSwift 2.x, *)" to +// the appropriate OS platforms. +let nextMinorVersion = 2 +let availabilitySettings: [SwiftSetting] = (0 ... nextMinorVersion).map { minor in + let name = "gRPCSwift" + let version = "2.\(minor)" + let platforms = "macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0" + let setting = "AvailabilityMacro=\(name) \(version):\(platforms)" + return .enableExperimentalFeature(setting) +} + +let defaultSwiftSettings: [SwiftSetting] = + availabilitySettings + [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + ] + +// ------------------------------------------------------------------------------------------------- let targets: [Target] = [ // Runtime serialization components @@ -107,13 +124,6 @@ let targets: [Target] = [ let package = Package( name: "grpc-swift", - platforms: [ - .macOS(.v15), - .iOS(.v18), - .tvOS(.v18), - .watchOS(.v11), - .visionOS(.v2), - ], products: products, dependencies: dependencies, targets: targets diff --git a/Sources/GRPCCodeGen/CodeGenError.swift b/Sources/GRPCCodeGen/CodeGenError.swift index 6cfffa32c..e5a0508a9 100644 --- a/Sources/GRPCCodeGen/CodeGenError.swift +++ b/Sources/GRPCCodeGen/CodeGenError.swift @@ -15,6 +15,7 @@ */ /// A error thrown by the ``SourceGenerator`` to signal errors in the ``CodeGenerationRequest`` object. +@available(gRPCSwift 2.0, *) public struct CodeGenError: Error, Hashable, Sendable { /// The code indicating the domain of the error. public var code: Code @@ -33,6 +34,7 @@ public struct CodeGenError: Error, Hashable, Sendable { } } +@available(gRPCSwift 2.0, *) extension CodeGenError { public struct Code: Hashable, Sendable { private enum Value { @@ -63,6 +65,7 @@ extension CodeGenError { } } +@available(gRPCSwift 2.0, *) extension CodeGenError: CustomStringConvertible { public var description: String { return "\(self.code): \"\(self.message)\"" diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index fc69c1bcf..55cdb679a 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -16,6 +16,7 @@ /// Describes the services, dependencies and trivia from an IDL file, /// and the IDL itself through its specific serializer and deserializer. +@available(gRPCSwift 2.0, *) public struct CodeGenerationRequest { /// The name of the source file containing the IDL, including the extension if applicable. public var fileName: String @@ -81,6 +82,7 @@ public struct CodeGenerationRequest { } } +@available(gRPCSwift 2.0, *) extension CodeGenerationRequest { @available(*, deprecated, renamed: "makeSerializerSnippet") public var lookupSerializer: (_ messageType: String) -> String { @@ -120,6 +122,7 @@ extension CodeGenerationRequest { } /// Represents an import: a module or a specific item from a module. +@available(gRPCSwift 2.0, *) public struct Dependency: Equatable { /// If the dependency is an item, the property's value is the item representation. /// If the dependency is a module, this property is nil. @@ -261,6 +264,7 @@ public struct Dependency: Equatable { } /// Represents a service described in an IDL file. +@available(gRPCSwift 2.0, *) public struct ServiceDescriptor: Hashable { /// Documentation from comments above the IDL service description. /// It is already formatted, meaning it contains "///" and new lines. @@ -285,6 +289,7 @@ public struct ServiceDescriptor: Hashable { } } +@available(gRPCSwift 2.0, *) extension ServiceDescriptor { @available(*, deprecated, renamed: "init(documentation:name:methods:)") public init( @@ -317,6 +322,7 @@ extension ServiceDescriptor { } /// Represents a method described in an IDL file. +@available(gRPCSwift 2.0, *) public struct MethodDescriptor: Hashable { /// Documentation from comments above the IDL method description. /// It is already formatted, meaning it contains "///" and new lines. @@ -357,6 +363,7 @@ public struct MethodDescriptor: Hashable { } } +@available(gRPCSwift 2.0, *) extension MethodDescriptor { @available(*, deprecated, message: "Use MethodName instead of Name") public init( @@ -380,6 +387,7 @@ extension MethodDescriptor { } } +@available(gRPCSwift 2.0, *) public struct ServiceName: Hashable { /// The identifying name as used in the service/method descriptors including any namespace. /// @@ -414,6 +422,7 @@ public struct ServiceName: Hashable { } } +@available(gRPCSwift 2.0, *) public struct MethodName: Hashable { /// The identifying name as used in the service/method descriptors. /// @@ -445,6 +454,7 @@ public struct MethodName: Hashable { /// Represents the name associated with a namespace, service or a method, in three different formats. @available(*, deprecated, message: "Use ServiceName/MethodName instead.") +@available(gRPCSwift 2.0, *) public struct Name: Hashable { /// The base name is the name used for the namespace/service/method in the IDL file, so it should follow /// the specific casing of the IDL. @@ -473,6 +483,7 @@ public struct Name: Hashable { } @available(*, deprecated, message: "Use ServiceName/MethodName instead.") +@available(gRPCSwift 2.0, *) extension Name { /// The base name replacing occurrences of "." with "_". /// diff --git a/Sources/GRPCCodeGen/CodeGenerator.swift b/Sources/GRPCCodeGen/CodeGenerator.swift index 07ffd9848..56de6efc6 100644 --- a/Sources/GRPCCodeGen/CodeGenerator.swift +++ b/Sources/GRPCCodeGen/CodeGenerator.swift @@ -15,10 +15,12 @@ */ @available(*, deprecated, renamed: "CodeGenerator") +@available(gRPCSwift 2.0, *) public typealias SourceGenerator = CodeGenerator /// Generates ``SourceFile`` objects containing generated code for the RPCs represented /// in a ``CodeGenerationRequest`` object. +@available(gRPCSwift 2.0, *) public struct CodeGenerator: Sendable { /// The options regarding the access level, indentation for the generated code /// and whether to generate server and client code. @@ -41,8 +43,10 @@ public struct CodeGenerator: Sendable { /// Whether or not server code should be generated. public var server: Bool /// The name of the core gRPC module. + @available(gRPCSwift 2.1, *) public var grpcCoreModuleName: String /// The availability annotations to use on the generated code. + @available(gRPCSwift 2.2, *) public var availability: AvailabilityAnnotations = .default /// Creates a new configuration. @@ -88,6 +92,7 @@ public struct CodeGenerator: Sendable { } // The availability that generated code is annotated with. + @available(gRPCSwift 2.2, *) public struct AvailabilityAnnotations: Sendable, Hashable { public struct Platform: Sendable, Hashable { /// The name of the OS, e.g. 'macOS'. @@ -156,6 +161,7 @@ public struct CodeGenerator: Sendable { } } +@available(gRPCSwift 2.0, *) extension AvailabilityDescription { init(_ availability: CodeGenerator.Config.AvailabilityAnnotations) throws { switch availability.wrapped { diff --git a/Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift b/Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift index a08700e65..8495a1884 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/RendererProtocol.swift @@ -31,6 +31,7 @@ /// into Swift files. /// /// Rendering is the last phase of the generator pipeline. +@available(gRPCSwift 2.0, *) protocol RendererProtocol { /// Renders the specified structured code into a raw Swift file. diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index e4cb173af..3cfb4a2dd 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -111,6 +111,7 @@ extension TextBasedRenderer: Sendable {} /// A renderer that uses string interpolation and concatenation /// to convert the provided structure code into raw string form. +@available(gRPCSwift 2.0, *) struct TextBasedRenderer: RendererProtocol { func render( @@ -1207,6 +1208,7 @@ extension String { } } +@available(gRPCSwift 2.0, *) extension TextBasedRenderer { /// Returns the provided expression rendered as a string. diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift index 99fddbede..c9c7d9545 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -194,6 +194,7 @@ extension FunctionDescription { } } +@available(gRPCSwift 2.0, *) extension ProtocolDescription { /// ``` /// protocol : Sendable { @@ -233,6 +234,7 @@ extension ProtocolDescription { } } +@available(gRPCSwift 2.0, *) extension ExtensionDescription { /// ``` /// extension { @@ -503,6 +505,7 @@ extension FunctionDescription { } } +@available(gRPCSwift 2.0, *) extension ExtensionDescription { /// ``` /// extension { @@ -662,6 +665,7 @@ extension FunctionDescription { } } +@available(gRPCSwift 2.0, *) extension StructDescription { /// ``` /// struct : where Transport: GRPCCore.ClientTransport { @@ -752,6 +756,7 @@ extension StructDescription { } } +@available(gRPCSwift 2.0, *) private func docs( for method: MethodDescriptor, serializers includeSerializers: Bool = true @@ -793,6 +798,7 @@ private func docs( return Docs.interposeDocs(method.documentation, between: summary, and: allParameters) } +@available(gRPCSwift 2.0, *) private func explodedDocs(for method: MethodDescriptor) -> String { let summary = "/// Call the \"\(method.name.identifyingName)\" method." var parameters = """ diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index a012f0b67..5cd883caa 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -48,6 +48,7 @@ extension FunctionSignatureDescription { } } +@available(gRPCSwift 2.0, *) extension ProtocolDescription { /// ``` /// protocol : GRPCCore.RegistrableRPCService { @@ -101,6 +102,7 @@ extension ProtocolDescription { } } +@available(gRPCSwift 2.0, *) extension ExtensionDescription { /// ``` /// extension { @@ -136,6 +138,7 @@ extension ExtensionDescription { } } +@available(gRPCSwift 2.0, *) extension ProtocolDescription { /// ``` /// protocol : { @@ -304,6 +307,7 @@ extension FunctionCallDescription { } } +@available(gRPCSwift 2.0, *) extension FunctionDescription { /// ``` /// func registerMethods(with router: inout GRPCCore.RPCRouter) { @@ -451,6 +455,7 @@ extension FunctionDescription { } } +@available(gRPCSwift 2.0, *) extension ExtensionDescription { /// ``` /// extension { @@ -548,6 +553,7 @@ extension FunctionSignatureDescription { } } +@available(gRPCSwift 2.0, *) extension ProtocolDescription { /// ``` /// protocol SimpleServiceProtocol: { @@ -763,6 +769,7 @@ extension FunctionDescription { } } +@available(gRPCSwift 2.0, *) extension ExtensionDescription { /// ``` /// extension ServiceProtocol { diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index 7e06ec07a..8ed3cc885 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -185,6 +185,7 @@ extension VariableDescription { } } +@available(gRPCSwift 2.0, *) extension EnumDescription { /// ``` /// enum { @@ -340,6 +341,7 @@ extension EnumDescription { } } +@available(gRPCSwift 2.0, *) extension [CodeBlock] { /// ``` /// enum { diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 443bf9c17..1f38bea37 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -75,6 +75,7 @@ /// } /// } ///``` +@available(gRPCSwift 2.0, *) struct ClientCodeTranslator { init() {} diff --git a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift index f8c6def00..2e6505406 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) package enum Docs { package static func suffix(_ header: String, withDocs footer: String) -> String { if footer.isEmpty { diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index c496980b7..038254cf3 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -17,6 +17,7 @@ /// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties. /// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications, /// using types from ``StructuredSwiftRepresentation``. +@available(gRPCSwift 2.0, *) package struct IDLToStructuredSwiftTranslator { package init() {} @@ -136,6 +137,7 @@ package struct IDLToStructuredSwiftTranslator { } } +@available(gRPCSwift 2.0, *) extension AccessModifier { init(_ accessLevel: CodeGenerator.Config.AccessLevel) { switch accessLevel.level { @@ -146,6 +148,7 @@ extension AccessModifier { } } +@available(gRPCSwift 2.0, *) extension IDLToStructuredSwiftTranslator { private func translateImport( dependency: Dependency, diff --git a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift index 2fc64821c..6015a1057 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) struct MetadataTranslator { init() {} diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index caf981742..3de95fdc1 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -57,6 +57,7 @@ /// } /// } ///``` +@available(gRPCSwift 2.0, *) struct ServerCodeTranslator { init() {} diff --git a/Sources/GRPCCodeGen/SourceFile.swift b/Sources/GRPCCodeGen/SourceFile.swift index c435fb100..ac511536f 100644 --- a/Sources/GRPCCodeGen/SourceFile.swift +++ b/Sources/GRPCCodeGen/SourceFile.swift @@ -16,6 +16,7 @@ /// Representation of the file to be created by the code generator, that contains the /// generated Swift source code. +@available(gRPCSwift 2.0, *) public struct SourceFile: Sendable, Hashable { /// The base name of the file. public var name: String diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index 2e6f8d939..68a36a9e6 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -21,6 +21,7 @@ /// /// You can create the default set of options, which defers all possible /// configuration to the transport, by using ``CallOptions/defaults``. +@available(gRPCSwift 2.0, *) public struct CallOptions: Sendable { /// The default timeout for the RPC. /// @@ -107,6 +108,7 @@ public struct CallOptions: Sendable { } } +@available(gRPCSwift 2.0, *) extension CallOptions { /// Default call options. /// @@ -123,6 +125,7 @@ extension CallOptions { } } +@available(gRPCSwift 2.0, *) extension CallOptions { package mutating func formUnion(with methodConfig: MethodConfig?) { guard let methodConfig = methodConfig else { return } diff --git a/Sources/GRPCCore/Call/Client/ClientContext.swift b/Sources/GRPCCore/Call/Client/ClientContext.swift index 679496ed4..b53983795 100644 --- a/Sources/GRPCCore/Call/Client/ClientContext.swift +++ b/Sources/GRPCCore/Call/Client/ClientContext.swift @@ -15,6 +15,7 @@ */ /// A context passed to the client containing additional information about the RPC. +@available(gRPCSwift 2.0, *) public struct ClientContext: Sendable { /// A description of the method being called. public var descriptor: MethodDescriptor diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index d09d654b5..a2e22ba87 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -89,6 +89,7 @@ /// ``` /// /// For server-side interceptors see ``ServerInterceptor``. +@available(gRPCSwift 2.0, *) public protocol ClientInterceptor: Sendable { /// Intercept a request object. /// diff --git a/Sources/GRPCCore/Call/Client/ClientRequest.swift b/Sources/GRPCCore/Call/Client/ClientRequest.swift index c36df63db..3e52a741c 100644 --- a/Sources/GRPCCore/Call/Client/ClientRequest.swift +++ b/Sources/GRPCCore/Call/Client/ClientRequest.swift @@ -28,6 +28,7 @@ /// print(request.metadata) // prints '[:]' /// print(request.message) // prints 'Hello, gRPC!' /// ``` +@available(gRPCSwift 2.0, *) public struct ClientRequest: Sendable { /// Caller-specified metadata to send to the server at the start of the RPC. /// @@ -61,6 +62,7 @@ public struct ClientRequest: Sendable { /// /// See ``ClientRequest`` for single-message requests and ``StreamingServerRequest`` for the /// servers representation of a streaming-message request. +@available(gRPCSwift 2.0, *) public struct StreamingClientRequest: Sendable { /// Caller-specified metadata sent to the server at the start of the RPC. /// diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index 73fcde0d5..23ec38546 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -67,6 +67,7 @@ /// print("RPC failed with code '\(error.code)'") /// } /// ``` +@available(gRPCSwift 2.0, *) public struct ClientResponse: Sendable { /// The contents of an accepted response with a single message. public struct Contents: Sendable { @@ -198,6 +199,7 @@ public struct ClientResponse: Sendable { /// print("RPC failed with code '\(error.code)'") /// } /// ``` +@available(gRPCSwift 2.0, *) public struct StreamingClientResponse: Sendable { public struct Contents: Sendable { /// Metadata received from the server at the beginning of the response. @@ -254,6 +256,7 @@ public struct StreamingClientResponse: Sendable { // MARK: - Convenience API +@available(gRPCSwift 2.0, *) extension ClientResponse { /// Creates a new accepted response. /// @@ -324,6 +327,7 @@ extension ClientResponse { } } +@available(gRPCSwift 2.0, *) extension StreamingClientResponse { /// Creates a new accepted response. /// @@ -386,6 +390,7 @@ extension StreamingClientResponse { /// Returns the body parts (i.e. `messages` and `trailingMetadata`) returned from the server. /// /// For rejected RPCs (in other words, where ``accepted`` is `failure`), the `RPCAsyncSequence` throws a ``RPCError``. + @available(gRPCSwift 2.1, *) public var bodyParts: RPCAsyncSequence { switch self.accepted { case let .success(contents): @@ -397,4 +402,5 @@ extension StreamingClientResponse { } } +@available(gRPCSwift 2.0, *) extension StreamingClientResponse.Contents.BodyPart: Equatable where Message: Equatable {} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index e0743bee3..08bdb62d7 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -16,6 +16,7 @@ public import Synchronization // would be internal but for usableFromInline +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor { @usableFromInline struct HedgingExecutor< @@ -61,6 +62,7 @@ extension ClientRPCExecutor { } } +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor.HedgingExecutor { @inlinable func execute( @@ -543,6 +545,7 @@ extension ClientRPCExecutor.HedgingExecutor { } } +@available(gRPCSwift 2.0, *) @usableFromInline enum _HedgingTaskResult: Sendable { case rpcHandled(Result) @@ -550,6 +553,7 @@ enum _HedgingTaskResult: Sendable { case timedOut(Result) } +@available(gRPCSwift 2.0, *) @usableFromInline enum _HedgingAttemptTaskResult: Sendable { case attemptPicked(Bool) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index a4de402af..97be8b77a 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor { /// An executor for requests which doesn't apply retries or hedging. The request has just one /// attempt at execution. @@ -53,6 +54,7 @@ extension ClientRPCExecutor { } } +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor.OneShotExecutor { @inlinable func execute( @@ -88,6 +90,7 @@ extension ClientRPCExecutor.OneShotExecutor { } } +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor.OneShotExecutor { @inlinable func _execute( @@ -129,6 +132,7 @@ extension ClientRPCExecutor.OneShotExecutor { } } +@available(gRPCSwift 2.0, *) @inlinable func withDeadline( _ deadline: ContinuousClock.Instant, @@ -169,6 +173,7 @@ func withDeadline( } } +@available(gRPCSwift 2.0, *) @usableFromInline enum _DeadlineChildTaskResult: Sendable { case deadlinePassed diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index e5964baee..bb4eb4bc7 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor { @usableFromInline struct RetryExecutor< @@ -59,6 +60,7 @@ extension ClientRPCExecutor { } } +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor.RetryExecutor { @inlinable func execute( @@ -310,6 +312,7 @@ extension ClientRPCExecutor.RetryExecutor { } } +@available(gRPCSwift 2.0, *) @usableFromInline enum _RetryExecutorTask: Sendable { case timedOut(Result) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 0cbe1490a..f7e41fad9 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) @usableFromInline enum ClientRPCExecutor { /// Execute the request and handle its response. @@ -99,6 +100,7 @@ enum ClientRPCExecutor { } } +@available(gRPCSwift 2.0, *) extension ClientRPCExecutor { /// Executes a request on a given stream processor. /// diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift index 477378599..3a6b15969 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension StreamingClientRequest { internal init(single request: ClientRequest) { self.init(metadata: request.metadata) { diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift index 30b381e72..41c3d0244 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension ClientResponse { /// Converts a streaming response into a single response. /// @@ -81,6 +82,7 @@ extension ClientResponse { } } +@available(gRPCSwift 2.0, *) extension StreamingClientResponse { /// Creates a streaming response from the given status and metadata. /// @@ -101,6 +103,7 @@ extension StreamingClientResponse { } } +@available(gRPCSwift 2.0, *) extension StreamingClientResponse { /// Returns a new response which maps the messages of this response. /// diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 1b2cac862..74aac103f 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) @usableFromInline internal enum ClientStreamExecutor { /// Execute a request on the stream executor. diff --git a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift index 4f87682b2..b9f54a916 100644 --- a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -25,6 +25,7 @@ public import Musl // should be @usableFromInline #error("Unsupported OS") #endif +@available(gRPCSwift 2.0, *) @usableFromInline struct RetryDelaySequence: Sequence { @usableFromInline diff --git a/Sources/GRPCCore/Call/ConditionalInterceptor.swift b/Sources/GRPCCore/Call/ConditionalInterceptor.swift index 1a61755ac..c5302260c 100644 --- a/Sources/GRPCCore/Call/ConditionalInterceptor.swift +++ b/Sources/GRPCCore/Call/ConditionalInterceptor.swift @@ -23,6 +23,7 @@ /// /// - SeeAlso: ``ClientInterceptor`` and ``ServerInterceptor`` for more information on client and /// server interceptors, respectively. +@available(gRPCSwift 2.0, *) public struct ConditionalInterceptor: Sendable { public struct Subject: Sendable { internal enum Wrapped: Sendable { @@ -85,6 +86,7 @@ public struct ConditionalInterceptor: Sendable { } } +@available(gRPCSwift 2.0, *) extension ConditionalInterceptor where Interceptor == any ClientInterceptor { /// Create an operation, specifying which ``ClientInterceptor`` to apply and to which ``Subject``. /// - Parameters: @@ -98,6 +100,7 @@ extension ConditionalInterceptor where Interceptor == any ClientInterceptor { } } +@available(gRPCSwift 2.0, *) extension ConditionalInterceptor where Interceptor == any ServerInterceptor { /// Create an operation, specifying which ``ServerInterceptor`` to apply and to which ``Subject``. /// - Parameters: diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift b/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift index 471f9d007..d4b8021fc 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift @@ -17,6 +17,7 @@ private import Synchronization /// Stores cancellation state for an RPC on the server . +@available(gRPCSwift 2.0, *) package final class ServerCancellationManager: Sendable { private let state: Mutex @@ -104,6 +105,7 @@ package final class ServerCancellationManager: Sendable { } } +@available(gRPCSwift 2.0, *) extension ServerCancellationManager { enum CancellationSource { case rpc diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 416a2c4c7..8df70f86d 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) @usableFromInline struct ServerRPCExecutor { /// Executes an RPC using the provided handler. @@ -290,6 +291,7 @@ struct ServerRPCExecutor { } } +@available(gRPCSwift 2.0, *) extension ServerRPCExecutor { @inlinable static func _intercept( diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index 5f43639f7..88c89c635 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -34,6 +34,7 @@ /// 1. Remove individual methods by calling ``removeHandler(forMethod:)``, or /// 2. Implement ``RegistrableRPCService/registerMethods(with:)`` to register only the methods you /// want to be served. +@available(gRPCSwift 2.0, *) public struct RPCRouter: Sendable { @usableFromInline struct RPCHandler: Sendable { @@ -168,6 +169,7 @@ public struct RPCRouter: Sendable { } } +@available(gRPCSwift 2.0, *) extension RPCRouter { internal func handle( stream: RPCStream< @@ -187,6 +189,7 @@ extension RPCRouter { } } +@available(gRPCSwift 2.0, *) extension Status { fileprivate static let rpcNotImplemented = Status( code: .unimplemented, diff --git a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift index b7f3e241b..7bd5819be 100644 --- a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift +++ b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift @@ -22,6 +22,7 @@ /// generated conformance by implementing ``registerMethods(with:)`` manually by calling /// ``RPCRouter/registerHandler(forMethod:deserializer:serializer:handler:)`` for each method /// you want to register with the router. +@available(gRPCSwift 2.0, *) public protocol RegistrableRPCService: Sendable { /// Registers methods to server with the provided ``RPCRouter``. /// diff --git a/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift index f958978b8..607c478c6 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift @@ -16,6 +16,7 @@ private import Synchronization +@available(gRPCSwift 2.0, *) extension ServerContext { @TaskLocal internal static var rpcCancellation: RPCCancellationHandle? @@ -73,6 +74,7 @@ extension ServerContext { /// - handler: The handler which is invoked when the RPC is cancelled. /// - Throws: Any error thrown by the `operation` closure. /// - Returns: The result of the `operation` closure. +@available(gRPCSwift 2.0, *) public func withRPCCancellationHandler( operation: () async throws(Failure) -> Result, onCancelRPC handler: @Sendable @escaping () -> Void @@ -102,6 +104,7 @@ public func withRPCCancellationHandler( /// use ``withRPCCancellationHandler(operation:onCancelRPC:)``. /// /// - Parameter operation: The operation to execute with the handle. +@available(gRPCSwift 2.0, *) public func withServerContextRPCCancellationHandle( _ operation: (ServerContext.RPCCancellationHandle) async throws(Failure) -> Success ) async throws(Failure) -> Success { diff --git a/Sources/GRPCCore/Call/Server/ServerContext.swift b/Sources/GRPCCore/Call/Server/ServerContext.swift index f027ad058..6df188f1c 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -15,9 +15,11 @@ */ /// Additional information about an RPC handled by a server. +@available(gRPCSwift 2.0, *) public struct ServerContext: Sendable { /// Protocol used to help identify transport specific context fields + @available(gRPCSwift 2.2, *) public protocol TransportSpecific: Sendable {} /// A description of the method being called. @@ -56,6 +58,7 @@ public struct ServerContext: Sendable { /// /// An example of what this field can be used for, would be to store /// things like a peer certificate from a mTLS connection + @available(gRPCSwift 2.2, *) public var transportSpecific: (any TransportSpecific)? /// A handle for checking the cancellation status of an RPC. diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index c6310edc3..8192fc21e 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -60,6 +60,7 @@ /// ``` /// /// For client-side interceptors see ``ClientInterceptor``. +@available(gRPCSwift 2.0, *) public protocol ServerInterceptor: Sendable { /// Intercept a request object. /// diff --git a/Sources/GRPCCore/Call/Server/ServerRequest.swift b/Sources/GRPCCore/Call/Server/ServerRequest.swift index 40ec6b568..6dd23063b 100644 --- a/Sources/GRPCCore/Call/Server/ServerRequest.swift +++ b/Sources/GRPCCore/Call/Server/ServerRequest.swift @@ -15,6 +15,7 @@ */ /// A request received at the server containing a single message. +@available(gRPCSwift 2.0, *) public struct ServerRequest: Sendable { /// Metadata received from the client at the start of the RPC. /// @@ -37,6 +38,7 @@ public struct ServerRequest: Sendable { } /// A request received at the server containing a stream of messages. +@available(gRPCSwift 2.0, *) public struct StreamingServerRequest: Sendable { /// Metadata received from the client at the start of the RPC. /// @@ -62,12 +64,14 @@ public struct StreamingServerRequest: Sendable { // MARK: - Conversion +@available(gRPCSwift 2.0, *) extension StreamingServerRequest { public init(single request: ServerRequest) { self.init(metadata: request.metadata, messages: .one(request.message)) } } +@available(gRPCSwift 2.0, *) extension ServerRequest { public init(stream request: StreamingServerRequest) async throws { var iterator = request.messages.makeAsyncIterator() diff --git a/Sources/GRPCCore/Call/Server/ServerResponse.swift b/Sources/GRPCCore/Call/Server/ServerResponse.swift index f71fe4204..0b439355d 100644 --- a/Sources/GRPCCore/Call/Server/ServerResponse.swift +++ b/Sources/GRPCCore/Call/Server/ServerResponse.swift @@ -66,6 +66,7 @@ /// print("RPC failed with code '\(error.code)'") /// } /// ``` +@available(gRPCSwift 2.0, *) public struct ServerResponse: Sendable { /// An accepted RPC with a successful outcome. public struct Contents: Sendable { @@ -168,6 +169,7 @@ public struct ServerResponse: Sendable { /// return ["goodbye": "trailing metadata"] /// } /// ``` +@available(gRPCSwift 2.0, *) public struct StreamingServerResponse: Sendable { /// The contents of a response to a request which has been accepted for processing. public struct Contents: Sendable { @@ -219,6 +221,7 @@ public struct StreamingServerResponse: Sendable { } } +@available(gRPCSwift 2.0, *) extension ServerResponse { /// Creates a new accepted response. /// @@ -288,6 +291,7 @@ extension ServerResponse { } } +@available(gRPCSwift 2.0, *) extension StreamingServerResponse { /// Creates a new accepted response. /// @@ -336,6 +340,7 @@ extension StreamingServerResponse { } } +@available(gRPCSwift 2.0, *) extension StreamingServerResponse { public init(single response: ServerResponse) { switch response.accepted { diff --git a/Sources/GRPCCore/Coding/Coding.swift b/Sources/GRPCCore/Coding/Coding.swift index 97e236db3..156fd6638 100644 --- a/Sources/GRPCCore/Coding/Coding.swift +++ b/Sources/GRPCCore/Coding/Coding.swift @@ -22,6 +22,7 @@ /// /// Serializers are used frequently and implementations should take care to ensure that /// serialization is as cheap as possible. +@available(gRPCSwift 2.0, *) public protocol MessageSerializer: Sendable { /// The type of message this serializer can serialize. associatedtype Message @@ -41,6 +42,7 @@ public protocol MessageSerializer: Sendable { /// /// Deserializers are used frequently and implementations should take care to ensure that /// deserialization is as cheap as possible. +@available(gRPCSwift 2.0, *) public protocol MessageDeserializer: Sendable { /// The type of message this deserializer can deserialize. associatedtype Message diff --git a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift index 7b54e6636..9c162f6a2 100644 --- a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift +++ b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift @@ -15,6 +15,7 @@ */ /// Message compression algorithms. +@available(gRPCSwift 2.0, *) public struct CompressionAlgorithm: Hashable, Sendable { package enum Value: UInt8, Hashable, Sendable, CaseIterable { case none = 0 @@ -45,6 +46,7 @@ public struct CompressionAlgorithm: Hashable, Sendable { } /// A set of compression algorithms. +@available(gRPCSwift 2.0, *) public struct CompressionAlgorithmSet: OptionSet, Hashable, Sendable { public var rawValue: UInt32 @@ -84,6 +86,7 @@ public struct CompressionAlgorithmSet: OptionSet, Hashable, Sendable { } } +@available(gRPCSwift 2.0, *) extension CompressionAlgorithmSet { /// A sequence of ``CompressionAlgorithm`` values present in the set. public var elements: Elements { diff --git a/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift b/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift index 93adb45fb..79930ab01 100644 --- a/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift +++ b/Sources/GRPCCore/Coding/GRPCContiguousBytes.swift @@ -19,6 +19,7 @@ /// This protocol is used by the transport protocols (``ClientTransport`` and ``ServerTransport``) /// with the serialization protocols (``MessageSerializer`` and ``MessageDeserializer``) so that /// messages don't have to be copied to a fixed intermediate bag-of-bytes types. +@available(gRPCSwift 2.0, *) public protocol GRPCContiguousBytes { /// Initialize the bytes to a repeated value. /// @@ -55,4 +56,5 @@ public protocol GRPCContiguousBytes { ) rethrows -> R } +@available(gRPCSwift 2.0, *) extension [UInt8]: GRPCContiguousBytes {} diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index 2de35e5a4..3decbfe58 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -17,6 +17,7 @@ /// Configuration values for executing an RPC. /// /// See also: https://github.com/grpc/grpc-proto/blob/0b30c8c05277ab78ec72e77c9cbf66a26684673d/grpc/service_config/service_config.proto +@available(gRPCSwift 2.0, *) public struct MethodConfig: Hashable, Sendable { /// The name of a method to which the method config applies. public struct Name: Sendable, Hashable { @@ -145,6 +146,7 @@ public struct MethodConfig: Hashable, Sendable { } /// Whether an RPC should be retried or hedged. +@available(gRPCSwift 2.0, *) public struct RPCExecutionPolicy: Hashable, Sendable { @usableFromInline enum Wrapped: Hashable, Sendable { @@ -214,6 +216,7 @@ public struct RPCExecutionPolicy: Hashable, Sendable { /// /// For more information see [gRFC A6 Client /// Retries](https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A6-client-retries.md). +@available(gRPCSwift 2.0, *) public struct RetryPolicy: Hashable, Sendable { /// The maximum number of RPC attempts, including the original attempt. /// @@ -331,6 +334,7 @@ public struct RetryPolicy: Hashable, Sendable { /// /// For more information see [gRFC A6 Client /// Retries](https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A6-client-retries.md). +@available(gRPCSwift 2.0, *) public struct HedgingPolicy: Hashable, Sendable { /// The maximum number of RPC attempts, including the original attempt. /// @@ -384,6 +388,7 @@ public struct HedgingPolicy: Hashable, Sendable { } } +@available(gRPCSwift 2.0, *) private func validateMaxAttempts(_ value: Int) throws -> Int { guard value > 1 else { throw RuntimeError( @@ -395,6 +400,7 @@ private func validateMaxAttempts(_ value: Int) throws -> Int { return min(value, 5) } +@available(gRPCSwift 2.0, *) extension MethodConfig: Codable { private enum CodingKeys: String, CodingKey { case name @@ -453,6 +459,7 @@ extension MethodConfig: Codable { } } +@available(gRPCSwift 2.0, *) extension MethodConfig.Name: Codable { private enum CodingKeys: String, CodingKey { case service @@ -478,6 +485,7 @@ extension MethodConfig.Name: Codable { } } +@available(gRPCSwift 2.0, *) extension RetryPolicy: Codable { private enum CodingKeys: String, CodingKey { case maxAttempts @@ -525,6 +533,7 @@ extension RetryPolicy: Codable { } } +@available(gRPCSwift 2.0, *) extension HedgingPolicy: Codable { private enum CodingKeys: String, CodingKey { case maxAttempts @@ -556,6 +565,7 @@ extension HedgingPolicy: Codable { } } +@available(gRPCSwift 2.0, *) struct GoogleProtobufDuration: Codable { var duration: Duration @@ -593,6 +603,7 @@ struct GoogleProtobufDuration: Codable { } } +@available(gRPCSwift 2.0, *) struct GoogleRPCCode: Codable { var code: Status.Code @@ -625,6 +636,7 @@ struct GoogleRPCCode: Codable { } } +@available(gRPCSwift 2.0, *) extension Status.Code { fileprivate init?(googleRPCCode code: String) { switch code { diff --git a/Sources/GRPCCore/Configuration/ServiceConfig.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift index 805e5ea59..a96119f1e 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -22,6 +22,7 @@ /// The schema is described by [`grpc/service_config/service_config.proto`](https://github.com/grpc/grpc-proto/blob/0b30c8c05277ab78ec72e77c9cbf66a26684673d/grpc/service_config/service_config.proto) /// in the `grpc/grpc-proto` GitHub repository although gRPC uses it in its JSON form rather than /// the Protobuf form. +@available(gRPCSwift 2.0, *) public struct ServiceConfig: Hashable, Sendable { /// Per-method configuration. public var methodConfig: [MethodConfig] @@ -67,6 +68,7 @@ public struct ServiceConfig: Hashable, Sendable { } } +@available(gRPCSwift 2.0, *) extension ServiceConfig: Codable { private enum CodingKeys: String, CodingKey { case methodConfig @@ -103,6 +105,7 @@ extension ServiceConfig: Codable { } } +@available(gRPCSwift 2.0, *) extension ServiceConfig { /// Configuration used by clients for load-balancing. public struct LoadBalancingConfig: Hashable, Sendable { @@ -168,6 +171,7 @@ extension ServiceConfig { } } +@available(gRPCSwift 2.0, *) extension ServiceConfig.LoadBalancingConfig { /// Configuration for the pick-first load balancing policy. public struct PickFirst: Hashable, Sendable, Codable { @@ -195,6 +199,7 @@ extension ServiceConfig.LoadBalancingConfig { } } +@available(gRPCSwift 2.0, *) extension ServiceConfig.LoadBalancingConfig: Codable { private enum CodingKeys: String, CodingKey { case roundRobin = "round_robin" @@ -225,6 +230,7 @@ extension ServiceConfig.LoadBalancingConfig: Codable { } } +@available(gRPCSwift 2.0, *) extension ServiceConfig { public struct RetryThrottling: Hashable, Sendable, Codable { /// The initial, and maximum number of tokens. diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index ffb6911ae..a41c8b261 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -54,6 +54,7 @@ private import Synchronization /// more abruptly you can cancel the task running your client. If your application requires /// additional resources that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). +@available(gRPCSwift 2.0, *) public final class GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. private let transport: Transport @@ -397,6 +398,7 @@ public final class GRPCClient: Sendable { /// code is nonisolated. /// - handleClient: A closure which is called with the client. When the closure returns, the /// client is shutdown gracefully. +@available(gRPCSwift 2.0, *) public func withGRPCClient( transport: Transport, interceptors: [any ClientInterceptor] = [], @@ -425,6 +427,7 @@ public func withGRPCClient( /// - handleClient: A closure which is called with the client. When the closure returns, the /// client is shutdown gracefully. /// - Returns: The result of the `handleClient` closure. +@available(gRPCSwift 2.0, *) public func withGRPCClient( transport: Transport, interceptorPipeline: [ConditionalInterceptor], diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 832e3cdec..d2ca1ed77 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -75,6 +75,7 @@ private import Synchronization /// that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle) and the /// `GRPCServiceLifecycle` module provided by [gRPC Swift Extras](https://github.com/grpc/grpc-swift-extras). +@available(gRPCSwift 2.0, *) public final class GRPCServer: Sendable { typealias Stream = RPCStream @@ -257,6 +258,7 @@ public final class GRPCServer: Sendable { /// - handleServer: A closure which is called with the server. When the closure returns, the /// server is shutdown gracefully. /// - Returns: The result of the `handleServer` closure. +@available(gRPCSwift 2.0, *) public func withGRPCServer( transport: Transport, services: [any RegistrableRPCService], @@ -288,6 +290,7 @@ public func withGRPCServer( /// - handleServer: A closure which is called with the server. When the closure returns, the /// server is shutdown gracefully. /// - Returns: The result of the `handleServer` closure. +@available(gRPCSwift 2.0, *) public func withGRPCServer( transport: Transport, services: [any RegistrableRPCService], diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift index bc82d0255..a9b4ea090 100644 --- a/Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift +++ b/Sources/GRPCCore/Internal/Concurrency Primitives/UnsafeTransfer.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) @usableFromInline struct UnsafeTransfer { @usableFromInline @@ -25,4 +26,5 @@ struct UnsafeTransfer { } } +@available(gRPCSwift 2.0, *) extension UnsafeTransfer: @unchecked Sendable {} diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index bb1c74a0d..76c7e459d 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension Metadata { @inlinable var previousRPCAttempts: Int? { @@ -51,6 +52,7 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata { @usableFromInline enum GRPCKey: String, Sendable, Hashable { @@ -75,6 +77,7 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata { @usableFromInline enum RetryPushback: Hashable, Sendable { diff --git a/Sources/GRPCCore/Internal/MethodConfigs.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift index 6aab8858d..326237def 100644 --- a/Sources/GRPCCore/Internal/MethodConfigs.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -22,6 +22,7 @@ /// service, or set a default configuration for all methods by calling ``setDefaultConfig(_:)``. /// /// Use the subscript to get and set configurations for specific methods. +@available(gRPCSwift 2.0, *) package struct MethodConfigs: Sendable, Hashable { private var elements: [MethodConfig.Name: MethodConfig] diff --git a/Sources/GRPCCore/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift index 8f9cbe59c..07258ca58 100644 --- a/Sources/GRPCCore/Internal/Result+Catching.swift +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -19,6 +19,7 @@ extension Result { /// /// - Parameter body: An `async` closure to catch the result of. @inlinable + @available(gRPCSwift 2.0, *) init(catching body: () async throws(Failure) -> Success) async { do { self = .success(try await body()) @@ -35,6 +36,7 @@ extension Result { /// - errorType: The type of error to cast to. /// - buildError: A closure which constructs the desired error if the cast fails. @inlinable + @available(gRPCSwift 2.0, *) func castError( to errorType: NewError.Type = NewError.self, or buildError: (any Error) -> NewError diff --git a/Sources/GRPCCore/Internal/String+Extensions.swift b/Sources/GRPCCore/Internal/String+Extensions.swift index f230c1ffe..dd4d68397 100644 --- a/Sources/GRPCCore/Internal/String+Extensions.swift +++ b/Sources/GRPCCore/Internal/String+Extensions.swift @@ -29,6 +29,7 @@ extension UInt8 { @inlinable + @available(gRPCSwift 2.0, *) var isASCII: Bool { return self <= 127 } @@ -40,6 +41,7 @@ extension String.UTF8View { /// - Parameter bytes: The string constant in the form of a collection of `UInt8` /// - Returns: Whether the collection contains **EXACTLY** this array or no, but by ignoring case. @inlinable + @available(gRPCSwift 2.0, *) func compareCaseInsensitiveASCIIBytes(to other: String.UTF8View) -> Bool { // fast path: we can get the underlying bytes of both let maybeMaybeResult = self.withContiguousStorageIfAvailable { lhsBuffer -> Bool? in @@ -67,6 +69,7 @@ extension String.UTF8View { @inlinable @inline(never) + @available(gRPCSwift 2.0, *) func _compareCaseInsensitiveASCIIBytesSlowPath(to other: String.UTF8View) -> Bool { return self.elementsEqual(other, by: { return (($0 & 0xdf) == ($1 & 0xdf) && $0.isASCII) }) } @@ -74,6 +77,7 @@ extension String.UTF8View { extension String { @inlinable + @available(gRPCSwift 2.0, *) func isEqualCaseInsensitiveASCIIBytes(to: String) -> Bool { return self.utf8.compareCaseInsensitiveASCIIBytes(to: to.utf8) } diff --git a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift index d0f38d6d3..4b6fd5ca8 100644 --- a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift +++ b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension TaskGroup { /// Adds a child task to the group which is individually cancellable. /// @@ -63,6 +64,7 @@ extension TaskGroup { } } +@available(gRPCSwift 2.0, *) @usableFromInline struct CancellableTaskHandle: Sendable { @usableFromInline diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 23218b4a1..c9e736eae 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -79,6 +79,7 @@ /// - Note: Binary values are encoded as base64 strings when they are sent over the wire, so keys with /// the "-bin" suffix may have string values (rather than binary). These are deserialized automatically when /// using ``subscript(binaryValues:)``. +@available(gRPCSwift 2.0, *) public struct Metadata: Sendable, Hashable { /// A metadata value. It can either be a simple string, or binary data. @@ -262,6 +263,7 @@ public struct Metadata: Sendable, Hashable { } } +@available(gRPCSwift 2.0, *) extension Metadata: RandomAccessCollection { public typealias Element = (key: String, value: Value) @@ -302,6 +304,7 @@ extension Metadata: RandomAccessCollection { } } +@available(gRPCSwift 2.0, *) extension Metadata { /// A sequence of metadata values for a given key. public struct Values: Sequence, Sendable { @@ -349,8 +352,8 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata { - /// A sequence of metadata string values for a given key. public struct StringValues: Sequence, Sendable { /// An iterator for all string values associated with a given key. @@ -399,6 +402,7 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata { /// A sequence of metadata binary values for a given key. public struct BinaryValues: Sequence, Sendable { @@ -460,30 +464,35 @@ extension Metadata { } } +@available(gRPCSwift 2.0, *) extension Metadata: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, Value)...) { self.elements = elements.map { KeyValuePair(key: $0, value: $1) } } } +@available(gRPCSwift 2.0, *) extension Metadata: ExpressibleByArrayLiteral { public init(arrayLiteral elements: (String, Value)...) { self.elements = elements.map { KeyValuePair(key: $0, value: $1) } } } +@available(gRPCSwift 2.0, *) extension Metadata.Value: ExpressibleByStringLiteral { public init(stringLiteral value: StringLiteralType) { self = .string(value) } } +@available(gRPCSwift 2.0, *) extension Metadata.Value: ExpressibleByStringInterpolation { public init(stringInterpolation: DefaultStringInterpolation) { self = .string(String(stringInterpolation: stringInterpolation)) } } +@available(gRPCSwift 2.0, *) extension Metadata.Value: ExpressibleByArrayLiteral { public typealias ArrayLiteralElement = UInt8 @@ -492,6 +501,7 @@ extension Metadata.Value: ExpressibleByArrayLiteral { } } +@available(gRPCSwift 2.0, *) extension Metadata: CustomStringConvertible { public var description: String { if self.isEmpty { @@ -504,6 +514,7 @@ extension Metadata: CustomStringConvertible { } } +@available(gRPCSwift 2.0, *) extension Metadata.Value: CustomStringConvertible { public var description: String { switch self { @@ -515,6 +526,7 @@ extension Metadata.Value: CustomStringConvertible { } } +@available(gRPCSwift 2.0, *) extension Metadata.Value: CustomDebugStringConvertible { public var debugDescription: String { switch self { diff --git a/Sources/GRPCCore/MethodDescriptor.swift b/Sources/GRPCCore/MethodDescriptor.swift index 20a1f3f50..a677ff982 100644 --- a/Sources/GRPCCore/MethodDescriptor.swift +++ b/Sources/GRPCCore/MethodDescriptor.swift @@ -15,6 +15,7 @@ */ /// A description of a method on a service. +@available(gRPCSwift 2.0, *) public struct MethodDescriptor: Sendable, Hashable { /// A description of the service, including its package name. public var service: ServiceDescriptor @@ -53,6 +54,7 @@ public struct MethodDescriptor: Sendable, Hashable { } } +@available(gRPCSwift 2.0, *) extension MethodDescriptor: CustomStringConvertible { public var description: String { self.fullyQualifiedMethod diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 80157034c..e6baf1d2b 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -17,6 +17,7 @@ /// An error representing the outcome of an RPC. /// /// See also ``Status``. +@available(gRPCSwift 2.0, *) public struct RPCError: Sendable, Hashable, Error { /// A code representing the high-level domain of the error. public var code: Code @@ -116,6 +117,7 @@ public struct RPCError: Sendable, Hashable, Error { } } +@available(gRPCSwift 2.0, *) extension RPCError: CustomStringConvertible { public var description: String { if let cause = self.cause { @@ -126,6 +128,7 @@ extension RPCError: CustomStringConvertible { } } +@available(gRPCSwift 2.0, *) extension RPCError { public struct Code: Hashable, Sendable, CustomStringConvertible { /// The numeric value of the error code. @@ -173,6 +176,7 @@ extension RPCError { } } +@available(gRPCSwift 2.0, *) extension RPCError.Code { /// The operation was cancelled (typically by the caller). public static let cancelled = Self(code: .cancelled) @@ -282,6 +286,7 @@ extension RPCError.Code { /// /// You can conform types to this protocol to have more control over the status codes and /// error information provided to clients when a service throws an error. +@available(gRPCSwift 2.0, *) public protocol RPCErrorConvertible { /// The error code to terminate the RPC with. var rpcErrorCode: RPCError.Code { get } @@ -301,6 +306,7 @@ public protocol RPCErrorConvertible { var rpcErrorCause: (any Error)? { get } } +@available(gRPCSwift 2.0, *) extension RPCErrorConvertible { /// Metadata associated with the error. /// @@ -318,6 +324,7 @@ extension RPCErrorConvertible { } } +@available(gRPCSwift 2.0, *) extension RPCErrorConvertible where Self: Error { /// The original error which led to this error being thrown. public var rpcErrorCause: (any Error)? { @@ -325,6 +332,7 @@ extension RPCErrorConvertible where Self: Error { } } +@available(gRPCSwift 2.0, *) extension RPCError { /// Create a new error by converting the given value. public init(_ convertible: some RPCErrorConvertible) { diff --git a/Sources/GRPCCore/RuntimeError.swift b/Sources/GRPCCore/RuntimeError.swift index 357aa59f7..62d82f8ed 100644 --- a/Sources/GRPCCore/RuntimeError.swift +++ b/Sources/GRPCCore/RuntimeError.swift @@ -18,6 +18,7 @@ /// /// In contrast to ``RPCError``, the ``RuntimeError`` represents errors which happen at a scope /// wider than an individual RPC. For example, passing invalid configuration values. +@available(gRPCSwift 2.0, *) public struct RuntimeError: Error, Hashable, Sendable { /// The code indicating the domain of the error. public var code: Code @@ -51,6 +52,7 @@ public struct RuntimeError: Error, Hashable, Sendable { } } +@available(gRPCSwift 2.0, *) extension RuntimeError: CustomStringConvertible { public var description: String { if let cause = self.cause { @@ -61,6 +63,7 @@ extension RuntimeError: CustomStringConvertible { } } +@available(gRPCSwift 2.0, *) extension RuntimeError { public struct Code: Hashable, Sendable { private enum Value { @@ -109,6 +112,7 @@ extension RuntimeError { } } +@available(gRPCSwift 2.0, *) extension RuntimeError.Code: CustomStringConvertible { public var description: String { String(describing: self.value) diff --git a/Sources/GRPCCore/ServiceDescriptor.swift b/Sources/GRPCCore/ServiceDescriptor.swift index 3a9709d83..a8b7d3abd 100644 --- a/Sources/GRPCCore/ServiceDescriptor.swift +++ b/Sources/GRPCCore/ServiceDescriptor.swift @@ -15,6 +15,7 @@ */ /// A description of a service. +@available(gRPCSwift 2.0, *) public struct ServiceDescriptor: Sendable, Hashable { /// The name of the package the service belongs to. For example, "helloworld". /// An empty string means that the service does not belong to any package. @@ -60,6 +61,7 @@ public struct ServiceDescriptor: Sendable, Hashable { } } +@available(gRPCSwift 2.0, *) extension ServiceDescriptor: CustomStringConvertible { public var description: String { self.fullyQualifiedService diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift index eba8d54f0..2b85d038f 100644 --- a/Sources/GRPCCore/Status.swift +++ b/Sources/GRPCCore/Status.swift @@ -24,6 +24,7 @@ /// ``Status`` represents the raw outcome of an RPC whether it was successful or not; ``RPCError`` /// is similar to ``Status`` but only represents error cases, in other words represents all status /// codes apart from ``Code-swift.struct/ok``. +@available(gRPCSwift 2.0, *) public struct Status: @unchecked Sendable, Hashable { // @unchecked because it relies on heap allocated storage and 'isKnownUniquelyReferenced' @@ -75,12 +76,14 @@ public struct Status: @unchecked Sendable, Hashable { internal static let ok = Status(storage: Storage(code: .ok, message: "")) } +@available(gRPCSwift 2.0, *) extension Status: CustomStringConvertible { public var description: String { "\(self.code): \"\(self.message)\"" } } +@available(gRPCSwift 2.0, *) extension Status { private final class Storage: Hashable { var code: Status.Code @@ -106,6 +109,7 @@ extension Status { } } +@available(gRPCSwift 2.0, *) extension Status { /// Status codes for gRPC operations. /// @@ -189,6 +193,7 @@ extension Status { } } +@available(gRPCSwift 2.0, *) extension Status.Code { /// The operation completed successfully. public static let ok = Self(code: .ok) @@ -297,6 +302,7 @@ extension Status.Code { public static let unauthenticated = Self(code: .unauthenticated) } +@available(gRPCSwift 2.0, *) extension Status { /// Create a status from an HTTP status code for a response which didn't include a gRPC status. /// diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift index 40ea25e71..f0a078c1e 100644 --- a/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension RPCAsyncSequence { /// Returns an ``RPCAsyncSequence`` containing just the given element. @inlinable @@ -32,6 +33,7 @@ extension RPCAsyncSequence { /// An `AsyncSequence` of a single value. @usableFromInline +@available(gRPCSwift 2.0, *) struct AsyncSequenceOfOne: AsyncSequence, Sendable { @usableFromInline let result: Result diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift index f7e4f72ff..afd8e6661 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension BroadcastAsyncSequence.Source: ClosableRPCWriterProtocol { @inlinable func write(contentsOf elements: some Sequence) async throws { diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift index 4e4860508..c40e01585 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -32,6 +32,7 @@ public import Synchronization // should be @usableFromInline /// The expectation is that the number of subscribers will be low; for retries there will be at most /// one subscriber at a time, for hedging there may be at most five subscribers at a time. @usableFromInline +@available(gRPCSwift 2.0, *) struct BroadcastAsyncSequence: Sendable, AsyncSequence { @usableFromInline let _storage: _BroadcastSequenceStorage @@ -85,6 +86,7 @@ struct BroadcastAsyncSequence: Sendable, AsyncSequence { // MARK: - AsyncIterator +@available(gRPCSwift 2.0, *) extension BroadcastAsyncSequence { @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { @@ -111,6 +113,7 @@ extension BroadcastAsyncSequence { // MARK: - Continuation +@available(gRPCSwift 2.0, *) extension BroadcastAsyncSequence { @usableFromInline struct Source: Sendable { @@ -145,6 +148,7 @@ extension BroadcastAsyncSequence { } @usableFromInline +@available(gRPCSwift 2.0, *) enum BroadcastAsyncSequenceError: Error { /// The consumer was too slow. case consumingTooSlow @@ -155,6 +159,7 @@ enum BroadcastAsyncSequenceError: Error { // MARK: - Storage @usableFromInline +@available(gRPCSwift 2.0, *) final class _BroadcastSequenceStorage: Sendable { @usableFromInline let _state: Mutex<_BroadcastSequenceStateMachine> @@ -326,6 +331,7 @@ final class _BroadcastSequenceStorage: Sendable { // MARK: - State machine @usableFromInline +@available(gRPCSwift 2.0, *) struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline typealias ConsumerContinuation = CheckedContinuation @@ -1376,6 +1382,7 @@ struct _BroadcastSequenceStateMachine: Sendable { } } +@available(gRPCSwift 2.0, *) extension _BroadcastSequenceStateMachine { /// A collection of elements tagged with an identifier. /// @@ -1530,6 +1537,7 @@ extension _BroadcastSequenceStateMachine { } } +@available(gRPCSwift 2.0, *) extension _BroadcastSequenceStateMachine { /// A collection of subscriptions. @usableFromInline @@ -1793,6 +1801,7 @@ extension _BroadcastSequenceStateMachine { } } +@available(gRPCSwift 2.0, *) extension _BroadcastSequenceStateMachine { // TODO: tiny array @usableFromInline diff --git a/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift b/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift index 3eadd1d85..f0ff95878 100644 --- a/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift +++ b/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift @@ -19,6 +19,7 @@ // 'RPCWriterProtocol'. (Adding a constrained conformance to 'RPCWriterProtocol' on // 'AsyncThrowingStream.Continuation' isn't possible because 'Sendable' is a marker protocol.) +@available(gRPCSwift 2.0, *) package struct GRPCAsyncThrowingStream: AsyncSequence, Sendable { package typealias Element = Element package typealias Failure = any Error @@ -77,6 +78,7 @@ package struct GRPCAsyncThrowingStream: AsyncSequence, Sendab } } +@available(gRPCSwift 2.0, *) extension GRPCAsyncThrowingStream.Continuation: RPCWriterProtocol { package func write(_ element: Element) async throws { self.yield(element) @@ -89,6 +91,7 @@ extension GRPCAsyncThrowingStream.Continuation: RPCWriterProtocol { } } +@available(gRPCSwift 2.0, *) extension GRPCAsyncThrowingStream.Continuation: ClosableRPCWriterProtocol { package func finish() async { self.finish(throwing: nil) diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift index 41332f1f2..d5349d2bf 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -15,6 +15,7 @@ */ @usableFromInline +@available(gRPCSwift 2.0, *) struct MapRPCWriter< Value: Sendable, Mapped: Sendable, @@ -46,6 +47,7 @@ struct MapRPCWriter< } } +@available(gRPCSwift 2.0, *) extension RPCWriter { @inlinable static func map( diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift index 84b2f70f3..c1a0a9b51 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift @@ -15,6 +15,7 @@ */ @usableFromInline +@available(gRPCSwift 2.0, *) struct MessageToRPCResponsePartWriter< Serializer: MessageSerializer, Bytes: GRPCContiguousBytes & Sendable @@ -48,6 +49,7 @@ struct MessageToRPCResponsePartWriter< } } +@available(gRPCSwift 2.0, *) extension RPCWriter { @inlinable static func serializingToRPCResponsePart( diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift index 1eff0c09a..b2c714fee 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -15,6 +15,7 @@ */ @usableFromInline +@available(gRPCSwift 2.0, *) struct SerializingRPCWriter< Base: RPCWriterProtocol, Bytes: GRPCContiguousBytes, @@ -49,6 +50,7 @@ struct SerializingRPCWriter< } } +@available(gRPCSwift 2.0, *) extension RPCWriter { @inlinable static func serializing( diff --git a/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift b/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift index 5b963e3bd..4beae5841 100644 --- a/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift @@ -16,8 +16,9 @@ public import Synchronization // should be @usableFromInline -@usableFromInline /// An `AsyncSequence` which wraps an existing async iterator. +@available(gRPCSwift 2.0, *) +@usableFromInline final class UncheckedAsyncIteratorSequence< Base: AsyncIteratorProtocol >: AsyncSequence, @unchecked Sendable { diff --git a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift index edf451179..de44eb5aa 100644 --- a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift @@ -15,6 +15,7 @@ */ /// A type-erasing `AsyncSequence`. +@available(gRPCSwift 2.0, *) public struct RPCAsyncSequence< Element: Sendable, Failure: Error diff --git a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift index 075c05ad8..51d2824d1 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +@available(gRPCSwift 2.0, *) extension RPCWriter { public struct Closable: ClosableRPCWriterProtocol { @usableFromInline diff --git a/Sources/GRPCCore/Streaming/RPCWriter.swift b/Sources/GRPCCore/Streaming/RPCWriter.swift index cc680b01d..866aa974e 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter.swift @@ -15,6 +15,7 @@ */ /// A type-erasing ``RPCWriterProtocol``. +@available(gRPCSwift 2.0, *) public struct RPCWriter: Sendable, RPCWriterProtocol { private let writer: any RPCWriterProtocol diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index 2f1c706d5..bde8369ec 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -15,6 +15,7 @@ */ /// A type into which values can be written indefinitely. +@available(gRPCSwift 2.0, *) public protocol RPCWriterProtocol: Sendable { /// The type of value written. associatedtype Element: Sendable @@ -36,6 +37,7 @@ public protocol RPCWriterProtocol: Sendable { func write(contentsOf elements: some Sequence) async throws } +@available(gRPCSwift 2.0, *) extension RPCWriterProtocol { /// Writes an `AsyncSequence` of values into the sink. /// @@ -50,6 +52,7 @@ extension RPCWriterProtocol { } /// A type into which values can be written until it is finished. +@available(gRPCSwift 2.0, *) public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// Indicate to the writer that no more writes are to be accepted. /// diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index 68fa49779..c7150b99c 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -23,6 +23,7 @@ internal import Dispatch /// /// Timeouts must be positive and at most 8-digits long. @usableFromInline +@available(gRPCSwift 2.0, *) struct Timeout: CustomStringConvertible, Hashable, Sendable { /// Possible units for a ``Timeout``. internal enum Unit: Character { @@ -181,6 +182,7 @@ extension Int64 { } } +@available(gRPCSwift 2.0, *) extension Duration { /// Construct a `Duration` given a number of minutes represented as an `Int64`. /// diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 8c2e8a07e..4b3cf6545 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -25,6 +25,7 @@ /// gRPC provides an in-process transport in the `GRPCInProcessTransport` module and HTTP/2 /// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport /// package. +@available(gRPCSwift 2.0, *) public protocol ClientTransport: Sendable { /// The bag-of-bytes type used by the transport. associatedtype Bytes: GRPCContiguousBytes & Sendable diff --git a/Sources/GRPCCore/Transport/RPCParts.swift b/Sources/GRPCCore/Transport/RPCParts.swift index ba19d58b4..8ebf12787 100644 --- a/Sources/GRPCCore/Transport/RPCParts.swift +++ b/Sources/GRPCCore/Transport/RPCParts.swift @@ -15,6 +15,7 @@ */ /// Part of a request sent from a client to a server in a stream. +@available(gRPCSwift 2.0, *) public enum RPCRequestPart { /// Key-value pairs sent at the start of a request stream. Only one ``metadata(_:)`` value may /// be sent to the server. @@ -26,11 +27,15 @@ public enum RPCRequestPart { case message(Bytes) } +@available(gRPCSwift 2.0, *) extension RPCRequestPart: Sendable where Bytes: Sendable {} +@available(gRPCSwift 2.0, *) extension RPCRequestPart: Hashable where Bytes: Hashable {} +@available(gRPCSwift 2.0, *) extension RPCRequestPart: Equatable where Bytes: Equatable {} /// Part of a response sent from a server to a client in a stream. +@available(gRPCSwift 2.0, *) public enum RPCResponsePart { /// Key-value pairs sent at the start of the response stream. At most one ``metadata(_:)`` value /// may be sent to the client. If the server sends ``metadata(_:)`` it must be the first part in @@ -48,6 +53,9 @@ public enum RPCResponsePart { case status(Status, Metadata) } +@available(gRPCSwift 2.0, *) extension RPCResponsePart: Sendable where Bytes: Sendable {} +@available(gRPCSwift 2.0, *) extension RPCResponsePart: Hashable where Bytes: Hashable {} +@available(gRPCSwift 2.0, *) extension RPCResponsePart: Equatable where Bytes: Equatable {} diff --git a/Sources/GRPCCore/Transport/RPCStream.swift b/Sources/GRPCCore/Transport/RPCStream.swift index 445d27f45..0c976265f 100644 --- a/Sources/GRPCCore/Transport/RPCStream.swift +++ b/Sources/GRPCCore/Transport/RPCStream.swift @@ -15,6 +15,7 @@ */ /// A bidirectional communication channel between a client and server for a given method. +@available(gRPCSwift 2.0, *) public struct RPCStream< Inbound: AsyncSequence & Sendable, Outbound: ClosableRPCWriterProtocol & Sendable diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index 6b52739d9..7c6b05baa 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -30,6 +30,7 @@ private import Synchronization /// the server. /// /// See also [gRFC A6: client retries](https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A6-client-retries.md). +@available(gRPCSwift 2.0, *) public final class RetryThrottle: Sendable { // Note: only three figures after the decimal point from the original token ratio are used so // all computation is done a scaled number of tokens (tokens * 1000). This allows us to do all diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index c3f2c7df7..d3891147d 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -22,6 +22,7 @@ /// gRPC provides an in-process transport in the `GRPCInProcessTransport` module and HTTP/2 /// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport /// package. +@available(gRPCSwift 2.0, *) public protocol ServerTransport: Sendable { /// The bag-of-bytes type used by the transport. associatedtype Bytes: GRPCContiguousBytes & Sendable diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift index 070f08b67..703b9f62c 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -17,6 +17,7 @@ public import GRPCCore private import Synchronization +@available(gRPCSwift 2.0, *) extension InProcessTransport { /// An in-process implementation of a `ClientTransport`. /// diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 11ce7a792..cc01e2046 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -17,6 +17,7 @@ public import GRPCCore private import Synchronization +@available(gRPCSwift 2.0, *) extension InProcessTransport { /// An in-process implementation of a `ServerTransport`. /// diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index e73beee91..4dc5d3f83 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -16,6 +16,7 @@ public import GRPCCore +@available(gRPCSwift 2.0, *) public struct InProcessTransport: Sendable { public let server: Self.Server public let client: Self.Client diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index 036eb41e9..080204962 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -30,8 +30,8 @@ import XCTest @testable import GRPCCodeGen +@available(gRPCSwift 2.0, *) final class Test_TextBasedRenderer: XCTestCase { - func testComment() throws { try _test( .inline( @@ -1029,6 +1029,7 @@ final class Test_TextBasedRenderer: XCTestCase { } } +@available(gRPCSwift 2.0, *) extension Test_TextBasedRenderer { func _test( _ input: Input, diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift index 700a8b895..4e697a87e 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -26,6 +26,7 @@ extension StructuredSwiftTests { arguments: AccessModifier.allCases, RPCKind.allCases ) + @available(gRPCSwift 2.0, *) func clientMethodSignature(access: AccessModifier, kind: RPCKind) { let decl: FunctionSignatureDescription = .clientMethod( accessLevel: access, @@ -59,6 +60,7 @@ extension StructuredSwiftTests { arguments: AccessModifier.allCases, [true, false] ) + @available(gRPCSwift 2.0, *) func clientMethodSignatureWithDefaults(access: AccessModifier, streamsOutput: Bool) { let decl: FunctionSignatureDescription = .clientMethod( accessLevel: access, @@ -96,6 +98,7 @@ extension StructuredSwiftTests { } @Test("protocol Foo_ClientProtocol: Sendable { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func clientProtocol(access: AccessModifier) { let decl: ProtocolDescription = .clientProtocol( accessLevel: access, @@ -143,6 +146,7 @@ extension StructuredSwiftTests { } @Test("func foo(...) { try await self.foo(...) }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func clientMethodFunctionWithDefaults(access: AccessModifier) { let decl: FunctionDescription = .clientMethodWithDefaults( accessLevel: access, @@ -180,6 +184,7 @@ extension StructuredSwiftTests { "extension Foo_ClientProtocol { ... } (methods with defaults)", arguments: AccessModifier.allCases ) + @available(gRPCSwift 2.0, *) func extensionWithDefaultClientMethods(access: AccessModifier) { let decl: ExtensionDescription = .clientMethodSignatureWithDefaults( accessLevel: access, @@ -235,6 +240,7 @@ extension StructuredSwiftTests { arguments: AccessModifier.allCases, RPCKind.allCases ) + @available(gRPCSwift 2.0, *) func explodedClientMethodSignature(access: AccessModifier, kind: RPCKind) { let decl: FunctionSignatureDescription = .clientMethodExploded( accessLevel: access, @@ -296,6 +302,7 @@ extension StructuredSwiftTests { "func foo(_:metadata:options:onResponse:) -> Result (exploded body)", arguments: [true, false] ) + @available(gRPCSwift 2.0, *) func explodedClientMethodBody(streamingInput: Bool) { let blocks: [CodeBlock] = .clientMethodExploded( name: "foo", @@ -335,6 +342,7 @@ extension StructuredSwiftTests { } @Test("extension Foo_ClientProtocol { ... } (exploded)", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func explodedClientMethodExtension(access: AccessModifier) { let decl: ExtensionDescription = .explodedClientMethods( accessLevel: access, @@ -393,6 +401,7 @@ extension StructuredSwiftTests { "func foo(request:serializer:deserializer:options:onResponse:) (client method impl.)", arguments: AccessModifier.allCases ) + @available(gRPCSwift 2.0, *) func clientMethodImplementation(access: AccessModifier) { let decl: FunctionDescription = .clientMethod( accessLevel: access, @@ -430,6 +439,7 @@ extension StructuredSwiftTests { } @Test("struct FooClient: Foo_ClientProtocol { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func client(access: AccessModifier) { let decl: StructDescription = .client( accessLevel: access, diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift index 88a6bc075..fdb8b6813 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift @@ -18,18 +18,21 @@ import GRPCCodeGen import Testing extension StructuredSwiftTests { - @Suite("Import") - struct Import { - static let translator = IDLToStructuredSwiftTranslator() + @available(gRPCSwift 2.0, *) + static let translator = IDLToStructuredSwiftTranslator() - static let allAccessLevels: [CodeGenerator.Config.AccessLevel] = [ - .internal, .public, .package, - ] + @available(gRPCSwift 2.0, *) + static let allAccessLevels: [CodeGenerator.Config.AccessLevel] = [ + .internal, .public, .package, + ] + @Suite("Import") + struct Import { @Test( "import rendering", arguments: allAccessLevels ) + @available(gRPCSwift 2.0, *) func imports(accessLevel: CodeGenerator.Config.AccessLevel) throws { var dependencies = [Dependency]() dependencies.append(Dependency(module: "Foo", accessLevel: .public)) @@ -104,7 +107,7 @@ extension StructuredSwiftTests { package import func Foo.Bak """ - let imports = try StructuredSwiftTests.Import.translator.makeImports( + let imports = try StructuredSwiftTests.translator.makeImports( dependencies: dependencies, accessLevel: accessLevel, accessLevelOnImports: true, @@ -116,8 +119,9 @@ extension StructuredSwiftTests { @Test( "preconcurrency import rendering", - arguments: allAccessLevels + arguments: StructuredSwiftTests.allAccessLevels ) + @available(gRPCSwift 2.0, *) func preconcurrencyImports(accessLevel: CodeGenerator.Config.AccessLevel) throws { var dependencies = [Dependency]() dependencies.append( @@ -155,7 +159,7 @@ extension StructuredSwiftTests { #endif """ - let imports = try StructuredSwiftTests.Import.translator.makeImports( + let imports = try StructuredSwiftTests.translator.makeImports( dependencies: dependencies, accessLevel: accessLevel, accessLevelOnImports: true, @@ -167,8 +171,9 @@ extension StructuredSwiftTests { @Test( "SPI import rendering", - arguments: allAccessLevels + arguments: StructuredSwiftTests.allAccessLevels ) + @available(gRPCSwift 2.0, *) func spiImports(accessLevel: CodeGenerator.Config.AccessLevel) throws { var dependencies = [Dependency]() dependencies.append( @@ -190,7 +195,7 @@ extension StructuredSwiftTests { @_spi(Secret) internal import enum Foo.Bar """ - let imports = try StructuredSwiftTests.Import.translator.makeImports( + let imports = try StructuredSwiftTests.translator.makeImports( dependencies: dependencies, accessLevel: accessLevel, accessLevelOnImports: true, @@ -201,6 +206,7 @@ extension StructuredSwiftTests { } @Test("gRPC module name") + @available(gRPCSwift 2.0, *) func grpcModuleName() throws { let translator = IDLToStructuredSwiftTranslator() let imports = try translator.makeImports( diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index 06c87be43..1f297e66f 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -22,6 +22,7 @@ extension StructuredSwiftTests { @Suite("Metadata") struct Metadata { @Test("typealias Input = ", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func methodInputTypealias(access: AccessModifier) { let decl: TypealiasDescription = .methodInput(accessModifier: access, name: "Foo") let expected = "\(access) typealias Input = Foo" @@ -29,6 +30,7 @@ extension StructuredSwiftTests { } @Test("typealias Output = ", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func methodOutputTypealias(access: AccessModifier) { let decl: TypealiasDescription = .methodOutput(accessModifier: access, name: "Foo") let expected = "\(access) typealias Output = Foo" @@ -39,6 +41,7 @@ extension StructuredSwiftTests { "static let descriptor = GRPCCore.MethodDescriptor(...)", arguments: AccessModifier.allCases ) + @available(gRPCSwift 2.0, *) func staticMethodDescriptorProperty(access: AccessModifier) { let decl: VariableDescription = .methodDescriptor( accessModifier: access, @@ -59,6 +62,7 @@ extension StructuredSwiftTests { "static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService:)", arguments: AccessModifier.allCases ) + @available(gRPCSwift 2.0, *) func staticServiceDescriptorProperty(access: AccessModifier) { let decl: VariableDescription = .serviceDescriptor( accessModifier: access, @@ -72,6 +76,7 @@ extension StructuredSwiftTests { } @Test("extension GRPCCore.ServiceDescriptor { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func staticServiceDescriptorPropertyExtension(access: AccessModifier) { let decl: ExtensionDescription = .serviceDescriptor( accessModifier: access, @@ -92,6 +97,7 @@ extension StructuredSwiftTests { "static let descriptors: [GRPCCore.MethodDescriptor] = [...]", arguments: AccessModifier.allCases ) + @available(gRPCSwift 2.0, *) func staticMethodDescriptorsArray(access: AccessModifier) { let decl: VariableDescription = .methodDescriptorsArray( accessModifier: access, @@ -109,6 +115,7 @@ extension StructuredSwiftTests { } @Test("enum { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func methodNamespaceEnum(access: AccessModifier) { let decl: EnumDescription = .methodNamespace( accessModifier: access, @@ -136,6 +143,7 @@ extension StructuredSwiftTests { } @Test("enum Method { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func methodsNamespaceEnum(access: AccessModifier) { let decl: EnumDescription = .methodsNamespace( accessModifier: access, @@ -176,6 +184,7 @@ extension StructuredSwiftTests { } @Test("enum Method { ... } (no methods)", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func methodsNamespaceEnumNoMethods(access: AccessModifier) { let decl: EnumDescription = .methodsNamespace( accessModifier: access, @@ -193,6 +202,7 @@ extension StructuredSwiftTests { } @Test("enum { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func serviceNamespaceEnum(access: AccessModifier) { let decl: EnumDescription = .serviceNamespace( accessModifier: access, @@ -239,6 +249,7 @@ extension StructuredSwiftTests { } @Test("enum { ... } (no methods)", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func serviceNamespaceEnumNoMethods(access: AccessModifier) { let decl: EnumDescription = .serviceNamespace( accessModifier: access, diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index 7db009ce4..50bfed96f 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -26,6 +26,7 @@ extension StructuredSwiftTests { arguments: AccessModifier.allCases, RPCKind.allCases ) + @available(gRPCSwift 2.0, *) func serverMethodSignature(access: AccessModifier, kind: RPCKind) { let decl: FunctionSignatureDescription = .serverMethod( accessLevel: access, @@ -73,6 +74,7 @@ extension StructuredSwiftTests { } @Test("protocol StreamingServiceProtocol { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func serverStreamingServiceProtocol(access: AccessModifier) { let decl: ProtocolDescription = .streamingService( accessLevel: access, @@ -115,6 +117,7 @@ extension StructuredSwiftTests { } @Test("protocol ServiceProtocol { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func serverServiceProtocol(access: AccessModifier) { let decl: ProtocolDescription = .service( accessLevel: access, @@ -158,6 +161,7 @@ extension StructuredSwiftTests { } @Test("{ router, context in try await self.(...) }") + @available(gRPCSwift 2.0, *) func routerHandlerInvokingRPC() { let expression: ClosureInvocationDescription = .routerHandlerInvokingRPC(method: "foo") let expected = """ @@ -172,6 +176,7 @@ extension StructuredSwiftTests { } @Test("router.registerHandler(...) { ... }") + @available(gRPCSwift 2.0, *) func registerMethodsWithRouter() { let expression: FunctionCallDescription = .registerWithRouter( serviceNamespace: "FooService", @@ -199,6 +204,7 @@ extension StructuredSwiftTests { } @Test("func registerMethods(router:)", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func registerMethods(access: AccessModifier) { let expression: FunctionDescription = .registerMethods( accessLevel: access, @@ -243,6 +249,7 @@ extension StructuredSwiftTests { arguments: AccessModifier.allCases, RPCKind.allCases ) + @available(gRPCSwift 2.0, *) func serverStreamingMethodsCallingMethod(access: AccessModifier, kind: RPCKind) { let expression: FunctionDescription = .serverStreamingMethodsCallingMethod( accessLevel: access, @@ -314,6 +321,7 @@ extension StructuredSwiftTests { } @Test("extension FooService_ServiceProtocol { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func streamingServiceProtocolDefaultImplementation(access: AccessModifier) { let decl: ExtensionDescription = .streamingServiceProtocolDefaultImplementation( accessModifier: access, @@ -362,6 +370,7 @@ extension StructuredSwiftTests { arguments: AccessModifier.allCases, RPCKind.allCases ) + @available(gRPCSwift 2.0, *) func simpleServerMethod(access: AccessModifier, kind: RPCKind) { let decl: FunctionSignatureDescription = .simpleServerMethod( accessLevel: access, @@ -413,6 +422,7 @@ extension StructuredSwiftTests { } @Test("protocol SimpleServiceProtocol { ... }", arguments: AccessModifier.allCases) + @available(gRPCSwift 2.0, *) func simpleServiceProtocol(access: AccessModifier) { let decl: ProtocolDescription = .simpleServiceProtocol( accessModifier: access, diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift index b2c0d2847..5146aaaa1 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift @@ -22,24 +22,28 @@ import Testing @Suite("Structured Swift") struct StructuredSwiftTests {} +@available(gRPCSwift 2.0, *) func render(_ declaration: Declaration) -> String { let renderer = TextBasedRenderer(indentation: 2) renderer.renderDeclaration(declaration) return renderer.renderedContents() } +@available(gRPCSwift 2.0, *) func render(_ expression: Expression) -> String { let renderer = TextBasedRenderer(indentation: 2) renderer.renderExpression(expression) return renderer.renderedContents() } +@available(gRPCSwift 2.0, *) func render(_ blocks: [CodeBlock]) -> String { let renderer = TextBasedRenderer(indentation: 2) renderer.renderCodeBlocks(blocks) return renderer.renderedContents() } +@available(gRPCSwift 2.0, *) func render(_ imports: [ImportDescription]) -> String { let renderer = TextBasedRenderer(indentation: 2) renderer.renderImports(imports) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 064791b78..5a449e29e 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -21,6 +21,7 @@ import Testing @Suite struct ClientCodeTranslatorSnippetBasedTests { @Test + @available(gRPCSwift 2.0, *) func translate() { let method = MethodDescriptor( documentation: "/// Documentation for MethodA", @@ -206,6 +207,7 @@ struct ClientCodeTranslatorSnippetBasedTests { #expect(rendered == expectedSwift) } + @available(gRPCSwift 2.0, *) private func render( accessLevel: AccessModifier, service: ServiceDescriptor diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift index 69426663b..c100582a6 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift @@ -20,6 +20,7 @@ import Testing @Suite("Docs tests") struct DocsTests { @Test("Suffix with additional docs") + @available(gRPCSwift 2.0, *) func suffixWithAdditional() { let foo = """ /// Foo @@ -42,6 +43,7 @@ struct DocsTests { } @Test("Suffix with empty additional docs") + @available(gRPCSwift 2.0, *) func suffixWithEmptyAdditional() { let foo = """ /// Foo @@ -52,6 +54,7 @@ struct DocsTests { } @Test("Interpose additional docs") + @available(gRPCSwift 2.0, *) func interposeDocs() { let header = """ /// Header @@ -81,6 +84,7 @@ struct DocsTests { } @Test("Interpose empty additional docs") + @available(gRPCSwift 2.0, *) func interposeEmpty() { let header = """ /// Header diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index eb9c85e5a..33f4b0d87 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -20,6 +20,7 @@ import XCTest @testable import GRPCCodeGen +@available(gRPCSwift 2.0, *) final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testGeneration() throws { var dependencies = [Dependency]() diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index bdbfcba9d..9db99776f 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -21,6 +21,7 @@ import Testing @Suite final class ServerCodeTranslatorSnippetBasedTests { @Test + @available(gRPCSwift 2.0, *) func translate() { let method = MethodDescriptor( documentation: "/// Documentation for unaryMethod", @@ -191,6 +192,7 @@ final class ServerCodeTranslatorSnippetBasedTests { #expect(rendered == expectedSwift) } + @available(gRPCSwift 2.0, *) private func render( accessLevel: AccessModifier, service: ServiceDescriptor diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index afd957638..ee516e0e3 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -71,6 +71,7 @@ internal func XCTAssertEqualWithDiff( ) } +@available(gRPCSwift 2.0, *) internal func makeCodeGenerationRequest( services: [ServiceDescriptor] = [], dependencies: [Dependency] = [] diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 4f6168c4b..59250a2e4 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -21,6 +21,7 @@ import Testing @Suite struct TypealiasTranslatorSnippetBasedTests { @Test + @available(gRPCSwift 2.0, *) func testTypealiasTranslator() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", @@ -77,6 +78,7 @@ struct TypealiasTranslatorSnippetBasedTests { } } +@available(gRPCSwift 2.0, *) extension TypealiasTranslatorSnippetBasedTests { func render( accessLevel: CodeGenerator.Config.AccessLevel, diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift index c5f0dbb9f..677228131 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class ClientRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let (messages, continuation) = AsyncStream.makeStream(of: String.self) diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index 46b0f19ae..30df1465c 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class ClientResponseTests: XCTestCase { func testAcceptedSingleResponseConvenienceMethods() { let response = ClientResponse( diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index cf4a4ccc1..11d661dde 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) extension ClientRPCExecutorTestHarness { struct ServerStreamHandler: Sendable { private let handler: @@ -58,6 +59,7 @@ extension ClientRPCExecutorTestHarness { } } +@available(gRPCSwift 2.0, *) extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { return Self { stream in diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index c80a63b7b..c1ae7869a 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -17,6 +17,7 @@ import GRPCCore import GRPCInProcessTransport +@available(gRPCSwift 2.0, *) extension InProcessTransport.Server { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 2b09af76b..18a5d06c2 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -25,6 +25,7 @@ import XCTest /// of the server to allow for flexible testing scenarios with minimal boilerplate. The harness /// also tracks how many streams the client has opened, how many streams the server accepted, and /// how many streams the client failed to open. +@available(gRPCSwift 2.0, *) struct ClientRPCExecutorTestHarness { private let server: ServerStreamHandler private let clientTransport: StreamCountingClientTransport diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index 14b31b0c5..77a64e9e9 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) extension ClientRPCExecutorTests { func testHedgingWhenAllAttemptsResultInNonFatalCodes() async throws { let harness = ClientRPCExecutorTestHarness( @@ -185,6 +186,7 @@ extension ClientRPCExecutorTests { } } +@available(gRPCSwift 2.0, *) extension CallOptions { fileprivate static func hedge( maxAttempts: Int = 5, diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index d4e1e210c..e8abaa857 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) extension ClientRPCExecutorTests { fileprivate func makeHarnessForRetries( rejectUntilAttempt firstSuccessfulAttempt: Int, @@ -288,6 +289,7 @@ extension ClientRPCExecutorTests { } } +@available(gRPCSwift 2.0, *) extension CallOptions { fileprivate static func retry( _ policy: RetryPolicy diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 4639dedb0..474640f95 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) diff --git a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift index 71fa0a495..4811700df 100644 --- a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -17,6 +17,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class RetryDelaySequenceTests: XCTestCase { func testSequence() { let policy = RetryPolicy( diff --git a/Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift b/Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift index cd688f3c6..bbaa05d05 100644 --- a/Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift +++ b/Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift @@ -39,6 +39,7 @@ struct ConditionalInterceptorTests { ), ] as [(ConditionalInterceptor.Subject, [MethodDescriptor], [MethodDescriptor])] ) + @available(gRPCSwift 2.0, *) func appliesTo( target: ConditionalInterceptor.Subject, applicableMethods: [MethodDescriptor], @@ -54,6 +55,7 @@ struct ConditionalInterceptorTests { } } +@available(gRPCSwift 2.0, *) extension MethodDescriptor { fileprivate static let fooBar = Self(fullyQualifiedService: "pkg.foo", method: "bar") fileprivate static let fooBaz = Self(fullyQualifiedService: "pkg.foo", method: "baz") diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift index 528ab88c3..87f8b200a 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift @@ -20,12 +20,14 @@ import Testing @Suite struct ServerCancellationManagerTests { @Test("Isn't cancelled after init") + @available(gRPCSwift 2.0, *) func isNotCancelled() { let manager = ServerCancellationManager() #expect(!manager.isRPCCancelled) } @Test("Is cancelled") + @available(gRPCSwift 2.0, *) func isCancelled() { let manager = ServerCancellationManager() manager.cancelRPC() @@ -33,6 +35,7 @@ struct ServerCancellationManagerTests { } @Test("Cancellation handler runs") + @available(gRPCSwift 2.0, *) func addCancellationHandler() async throws { let manager = ServerCancellationManager() let signal = AsyncStream.makeStream(of: Void.self) @@ -48,6 +51,7 @@ struct ServerCancellationManagerTests { } @Test("Cancellation handler runs immediately when already cancelled") + @available(gRPCSwift 2.0, *) func addCancellationHandlerAfterCancelled() async throws { let manager = ServerCancellationManager() let signal = AsyncStream.makeStream(of: Void.self) @@ -63,6 +67,7 @@ struct ServerCancellationManagerTests { } @Test("Remove cancellation handler") + @available(gRPCSwift 2.0, *) func removeCancellationHandler() async throws { let manager = ServerCancellationManager() @@ -76,6 +81,7 @@ struct ServerCancellationManagerTests { } @Test("Wait for cancellation") + @available(gRPCSwift 2.0, *) func waitForCancellation() async throws { let manager = ServerCancellationManager() try await withThrowingTaskGroup(of: Void.self) { group in diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index d7f976ba3..f0f156c2a 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -17,6 +17,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) struct ServerRPCExecutorTestHarness { struct ServerHandler: Sendable { let fn: @@ -148,6 +149,7 @@ struct ServerRPCExecutorTestHarness { } } +@available(gRPCSwift 2.0, *) extension ServerRPCExecutorTestHarness.ServerHandler where Input == Output { static var echo: Self { return Self { request, context in diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 215584ebf..73f9ba82f 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class ServerRPCExecutorTests: XCTestCase { func testEchoNoMessages() async throws { let harness = ServerRPCExecutorTestHarness() diff --git a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift index 7ae55b2da..5ec97ddbf 100644 --- a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift @@ -17,6 +17,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) final class RPCRouterTests: XCTestCase { func testEmptyRouter() async throws { var router = RPCRouter() @@ -64,6 +65,7 @@ final class RPCRouterTests: XCTestCase { } } +@available(gRPCSwift 2.0, *) struct NoServerTransport: ServerTransport { typealias Bytes = [UInt8] diff --git a/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift index c524519ff..f575d8f30 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift @@ -22,6 +22,7 @@ struct ServerContextTests { @Suite("CancellationHandle") struct CancellationHandle { @Test("Is cancelled") + @available(gRPCSwift 2.0, *) func isCancelled() async throws { await withServerContextRPCCancellationHandle { handle in #expect(!handle.isCancelled) @@ -31,6 +32,7 @@ struct ServerContextTests { } @Test("Wait for cancellation") + @available(gRPCSwift 2.0, *) func waitForCancellation() async throws { await withServerContextRPCCancellationHandle { handle in await withTaskGroup(of: Void.self) { group in @@ -44,6 +46,7 @@ struct ServerContextTests { } @Test("Binds task local") + @available(gRPCSwift 2.0, *) func bindsTaskLocal() async throws { await withServerContextRPCCancellationHandle { handle in let signal = AsyncStream.makeStream(of: Void.self) diff --git a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift index c341bc992..c83aa3fc7 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift @@ -16,6 +16,7 @@ @_spi(Testing) import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) final class ServerRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let single = ServerRequest(metadata: ["bar": "baz"], message: "foo") diff --git a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift index 2152143c4..0267405c3 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift @@ -20,6 +20,7 @@ import Testing @Suite("ServerResponse") struct ServerResponseTests { @Test("ServerResponse(message:metadata:trailingMetadata:)") + @available(gRPCSwift 2.0, *) func responseInitSuccess() throws { let response = ServerResponse( message: "message", @@ -34,6 +35,7 @@ struct ServerResponseTests { } @Test("ServerResponse(of:error:)") + @available(gRPCSwift 2.0, *) func responseInitError() throws { let error = RPCError(code: .aborted, message: "Aborted") let response = ServerResponse(of: String.self, error: error) @@ -46,6 +48,7 @@ struct ServerResponseTests { } @Test("StreamingServerResponse(of:metadata:producer:)") + @available(gRPCSwift 2.0, *) func streamingResponseInitSuccess() async throws { let response = StreamingServerResponse( of: String.self, @@ -62,6 +65,7 @@ struct ServerResponseTests { } @Test("StreamingServerResponse(of:error:)") + @available(gRPCSwift 2.0, *) func streamingResponseInitError() async throws { let error = RPCError(code: .aborted, message: "Aborted") let response = StreamingServerResponse(of: String.self, error: error) @@ -74,6 +78,7 @@ struct ServerResponseTests { } @Test("StreamingServerResponse(single:) (accepted)") + @available(gRPCSwift 2.0, *) func singleToStreamConversionForSuccessfulResponse() async throws { let single = ServerResponse( message: "foo", @@ -100,6 +105,7 @@ struct ServerResponseTests { } @Test("StreamingServerResponse(single:) (rejected)") + @available(gRPCSwift 2.0, *) func singleToStreamConversionForFailedResponse() async throws { let error = RPCError(code: .aborted, message: "aborted") let single = ServerResponse(of: String.self, error: error) @@ -114,6 +120,7 @@ struct ServerResponseTests { } @Test("Mutate metadata on response", arguments: [true, false]) + @available(gRPCSwift 2.0, *) func mutateMetadataOnResponse(accepted: Bool) { var response: ServerResponse if accepted { @@ -127,6 +134,7 @@ struct ServerResponseTests { } @Test("Mutate metadata on streaming response", arguments: [true, false]) + @available(gRPCSwift 2.0, *) func mutateMetadataOnStreamingResponse(accepted: Bool) { var response: StreamingServerResponse if accepted { diff --git a/Tests/GRPCCoreTests/Coding/CodingTests.swift b/Tests/GRPCCoreTests/Coding/CodingTests.swift index dab5ecece..d50fd93f2 100644 --- a/Tests/GRPCCoreTests/Coding/CodingTests.swift +++ b/Tests/GRPCCoreTests/Coding/CodingTests.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) final class CodingTests: XCTestCase { func testJSONRoundtrip() throws { // This test just demonstrates that the API is suitable. diff --git a/Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift b/Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift index 351538816..69d2e2c3e 100644 --- a/Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift +++ b/Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift @@ -17,6 +17,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) final class CompressionAlgorithmTests: XCTestCase { func testCompressionAlgorithmSetContains() { var algorithms = CompressionAlgorithmSet() diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift index 634ef3399..9a3d1f6a7 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift @@ -43,6 +43,7 @@ struct MethodConfigCodingTests { (MethodConfig.Name(service: "", method: ""), #"{"method":"","service":""}"#), ] as [(MethodConfig.Name, String)] ) + @available(gRPCSwift 2.0, *) func methodConfigName(name: MethodConfig.Name, expected: String) throws { let json = try self.encodeToJSON(name) #expect(json == expected) @@ -56,6 +57,7 @@ struct MethodConfigCodingTests { (.milliseconds(100_123), #""100.123s""#), ] as [(Duration, String)] ) + @available(gRPCSwift 2.0, *) func protobufDuration(duration: Duration, expected: String) throws { let json = try self.encodeToJSON(GoogleProtobufDuration(duration: duration)) #expect(json == expected) @@ -83,12 +85,14 @@ struct MethodConfigCodingTests { (.unauthenticated, #""UNAUTHENTICATED""#), ] as [(Status.Code, String)] ) + @available(gRPCSwift 2.0, *) func rpcCode(code: Status.Code, expected: String) throws { let json = try self.encodeToJSON(GoogleRPCCode(code: code)) #expect(json == expected) } @Test("RetryPolicy") + @available(gRPCSwift 2.0, *) func retryPolicy() throws { let policy = RetryPolicy( maxAttempts: 3, @@ -105,6 +109,7 @@ struct MethodConfigCodingTests { } @Test("HedgingPolicy") + @available(gRPCSwift 2.0, *) func hedgingPolicy() throws { let policy = HedgingPolicy( maxAttempts: 3, @@ -174,6 +179,7 @@ struct MethodConfigCodingTests { ("method_config.name.empty", MethodConfig.Name(service: "", method: "")), ] as [(String, MethodConfig.Name)] ) + @available(gRPCSwift 2.0, *) func name(_ fileName: String, expected: MethodConfig.Name) throws { let decoded = try self.decodeFromFile(fileName, as: MethodConfig.Name.self) #expect(decoded == expected) @@ -190,6 +196,7 @@ struct MethodConfigCodingTests { ("100.123s", .milliseconds(100_123)), ] as [(String, Duration)] ) + @available(gRPCSwift 2.0, *) func googleProtobufDuration(duration: String, expectedDuration: Duration) throws { let json = "\"\(duration)\"" let decoded = try self.decodeFromJSONString(json, as: GoogleProtobufDuration.self) @@ -206,6 +213,7 @@ struct MethodConfigCodingTests { } @Test("Invalid GoogleProtobufDuration", arguments: ["1", "1ss", "1S", "1.0S"]) + @available(gRPCSwift 2.0, *) func googleProtobufDuration(invalidDuration: String) throws { let json = "\"\(invalidDuration)\"" #expect { @@ -217,6 +225,7 @@ struct MethodConfigCodingTests { } @Test("GoogleRPCCode from case name", arguments: zip(Self.codeNames, Status.Code.all)) + @available(gRPCSwift 2.0, *) func rpcCode(name: String, expected: Status.Code) throws { let json = "\"\(name)\"" let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self) @@ -224,6 +233,7 @@ struct MethodConfigCodingTests { } @Test("GoogleRPCCode from rawValue", arguments: zip(0 ... 16, Status.Code.all)) + @available(gRPCSwift 2.0, *) func rpcCode(rawValue: Int, expected: Status.Code) throws { let json = "\(rawValue)" let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self) @@ -231,6 +241,7 @@ struct MethodConfigCodingTests { } @Test("RetryPolicy") + @available(gRPCSwift 2.0, *) func retryPolicy() throws { let decoded = try self.decodeFromFile("method_config.retry_policy", as: RetryPolicy.self) let expected = RetryPolicy( @@ -253,6 +264,7 @@ struct MethodConfigCodingTests { "method_config.retry_policy.invalid.retryable_status_codes", ] ) + @available(gRPCSwift 2.0, *) func invalidRetryPolicy(fileName: String) throws { #expect(throws: RuntimeError.self) { try self.decodeFromFile(fileName, as: RetryPolicy.self) @@ -260,6 +272,7 @@ struct MethodConfigCodingTests { } @Test("HedgingPolicy") + @available(gRPCSwift 2.0, *) func hedgingPolicy() throws { let decoded = try self.decodeFromFile("method_config.hedging_policy", as: HedgingPolicy.self) let expected = HedgingPolicy( @@ -276,6 +289,7 @@ struct MethodConfigCodingTests { "method_config.hedging_policy.invalid.max_attempts" ] ) + @available(gRPCSwift 2.0, *) func invalidHedgingPolicy(fileName: String) throws { #expect(throws: RuntimeError.self) { try self.decodeFromFile(fileName, as: HedgingPolicy.self) @@ -283,6 +297,7 @@ struct MethodConfigCodingTests { } @Test("MethodConfig") + @available(gRPCSwift 2.0, *) func methodConfig() throws { let expected = MethodConfig( names: [ @@ -302,6 +317,7 @@ struct MethodConfigCodingTests { } @Test("MethodConfig with hedging") + @available(gRPCSwift 2.0, *) func methodConfigWithHedging() throws { let expected = MethodConfig( names: [ @@ -328,6 +344,7 @@ struct MethodConfigCodingTests { } @Test("MethodConfig with retries") + @available(gRPCSwift 2.0, *) func methodConfigWithRetries() throws { let expected = MethodConfig( names: [ @@ -406,6 +423,7 @@ struct MethodConfigCodingTests { "method_config.with_hedging", ] ) + @available(gRPCSwift 2.0, *) func roundTripCodingAndDecoding(fileName: String) throws { try self.roundTrip(type: MethodConfig.self, fileName: fileName) } diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift index 8d01bcfd6..f549de2cb 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift @@ -19,6 +19,7 @@ import Testing struct MethodConfigTests { @Test("RetryPolicy clamps max attempts") + @available(gRPCSwift 2.0, *) func retryPolicyClampsMaxAttempts() { var policy = RetryPolicy( maxAttempts: 10, @@ -36,6 +37,7 @@ struct MethodConfigTests { } @Test("HedgingPolicy clamps max attempts") + @available(gRPCSwift 2.0, *) func hedgingPolicyClampsMaxAttempts() { var policy = HedgingPolicy( maxAttempts: 10, diff --git a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift index 787d87e0f..bdf5671c0 100644 --- a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift @@ -19,6 +19,7 @@ import GRPCCore import SwiftProtobuf import XCTest +@available(gRPCSwift 2.0, *) final class ServiceConfigCodingTests: XCTestCase { private let encoder = JSONEncoder() private let decoder = JSONDecoder() diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index f038760d8..9e6db0cdb 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -19,6 +19,7 @@ import GRPCInProcessTransport import Testing import XCTest +@available(gRPCSwift 2.0, *) final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], @@ -398,6 +399,7 @@ final class GRPCClientTests: XCTestCase { @Suite("GRPC Client Tests") struct ClientTests { @Test("Interceptors are applied only to specified services") + @available(gRPCSwift 2.0, *) func testInterceptorsAreAppliedToSpecifiedServices() async throws { let onlyBinaryEchoCounter = AtomicCounter() let allServicesCounter = AtomicCounter() @@ -461,6 +463,7 @@ struct ClientTests { } @Test("Interceptors are applied only to specified methods") + @available(gRPCSwift 2.0, *) func testInterceptorsAreAppliedToSpecifiedMethods() async throws { let onlyBinaryEchoGetCounter = AtomicCounter() let onlyBinaryEchoCollectCounter = AtomicCounter() @@ -523,6 +526,7 @@ struct ClientTests { } } + @available(gRPCSwift 2.0, *) func withInProcessConnectedClient( services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor] = [], diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index acf29b8e5..9e35bded6 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -19,6 +19,7 @@ import GRPCInProcessTransport import Testing import XCTest +@available(gRPCSwift 2.0, *) final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], @@ -378,6 +379,7 @@ final class GRPCServerTests: XCTestCase { @Suite("GRPC Server Tests") struct ServerTests { @Test("Interceptors are applied only to specified services") + @available(gRPCSwift 2.0, *) func testInterceptorsAreAppliedToSpecifiedServices() async throws { let onlyBinaryEchoCounter = AtomicCounter() let allServicesCounter = AtomicCounter() @@ -465,6 +467,7 @@ struct ServerTests { } @Test("Interceptors are applied only to specified methods") + @available(gRPCSwift 2.0, *) func testInterceptorsAreAppliedToSpecifiedMethods() async throws { let onlyBinaryEchoGetCounter = AtomicCounter() let onlyBinaryEchoCollectCounter = AtomicCounter() @@ -551,6 +554,7 @@ struct ServerTests { } } + @available(gRPCSwift 2.0, *) func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], interceptorPipeline: [ConditionalInterceptor] = [], @@ -578,6 +582,7 @@ struct ServerTests { } } + @available(gRPCSwift 2.0, *) func assertMetadata( _ part: RPCResponsePart?, metadataHandler: (Metadata) -> Void = { _ in } @@ -590,6 +595,7 @@ struct ServerTests { } } + @available(gRPCSwift 2.0, *) func assertMessage( _ part: RPCResponsePart?, messageHandler: (Bytes) -> Void = { _ in } @@ -602,6 +608,7 @@ struct ServerTests { } } + @available(gRPCSwift 2.0, *) func assertStatus( _ part: RPCResponsePart?, statusHandler: (Status, Metadata) -> Void = { _, _ in } diff --git a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift index a6bb11a84..b4b59bcef 100644 --- a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift +++ b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class MetadataGRPCTests: XCTestCase { func testPreviousRPCAttemptsValidValues() { let testData = [("0", 0), ("1", 1), ("-1", -1)] diff --git a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift index bac916763..1f9cf7bdf 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) final class MethodConfigsTests: XCTestCase { func testGetConfigurationForKnownMethod() async throws { let policy = HedgingPolicy( diff --git a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift index d5bc65cd1..644bc72dd 100644 --- a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift +++ b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class ResultCatchingTests: XCTestCase { func testResultCatching() async { let result = await Result { diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index 617d2263d..715869fdb 100644 --- a/Tests/GRPCCoreTests/MetadataTests.swift +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -20,6 +20,7 @@ import Testing @Suite("Metadata") struct MetadataTests { @Test("Initialize from Sequence") + @available(gRPCSwift 2.0, *) func initFromSequence() { let elements: [Metadata.Element] = [ (key: "key1", value: "value1"), @@ -33,6 +34,7 @@ struct MetadataTests { } @Test("Add string Value") + @available(gRPCSwift 2.0, *) func addStringValue() { var metadata = Metadata() #expect(metadata.isEmpty) @@ -47,6 +49,7 @@ struct MetadataTests { } @Test("Add binary value") + @available(gRPCSwift 2.0, *) func addBinaryValue() { var metadata = Metadata() #expect(metadata.isEmpty) @@ -61,6 +64,7 @@ struct MetadataTests { } @Test("Initialize from dictionary literal") + @available(gRPCSwift 2.0, *) func initFromDictionaryLiteral() { let metadata: Metadata = [ "testKey": "stringValue", @@ -83,52 +87,52 @@ struct MetadataTests { struct ReplaceOrAdd { @Suite("String") struct StringValues { - var metadata: Metadata = [ - "key1": "value1", - "key1": "value2", - ] - @Test("Add different key") + @available(gRPCSwift 2.0, *) mutating func addNewKey() async throws { - self.metadata.replaceOrAddString("value3", forKey: "key2") - #expect(Array(self.metadata[stringValues: "key1"]) == ["value1", "value2"]) - #expect(Array(self.metadata[stringValues: "key2"]) == ["value3"]) - #expect(self.metadata.count == 3) + var metadata: Metadata = ["key1": "value1", "key1": "value2"] + metadata.replaceOrAddString("value3", forKey: "key2") + #expect(Array(metadata[stringValues: "key1"]) == ["value1", "value2"]) + #expect(Array(metadata[stringValues: "key2"]) == ["value3"]) + #expect(metadata.count == 3) } @Test("Replace values for existing key") + @available(gRPCSwift 2.0, *) mutating func replaceValues() async throws { - self.metadata.replaceOrAddString("value3", forKey: "key1") - #expect(Array(self.metadata[stringValues: "key1"]) == ["value3"]) - #expect(self.metadata.count == 1) + var metadata: Metadata = ["key1": "value1", "key1": "value2"] + metadata.replaceOrAddString("value3", forKey: "key1") + #expect(Array(metadata[stringValues: "key1"]) == ["value3"]) + #expect(metadata.count == 1) } } @Suite("Binary") struct BinaryValues { - var metadata: Metadata = [ - "key1-bin": [0], - "key1-bin": [1], - ] @Test("Add different key") + @available(gRPCSwift 2.0, *) mutating func addNewKey() async throws { - self.metadata.replaceOrAddBinary([2], forKey: "key2-bin") - #expect(Array(self.metadata[binaryValues: "key1-bin"]) == [[0], [1]]) - #expect(Array(self.metadata[binaryValues: "key2-bin"]) == [[2]]) - #expect(self.metadata.count == 3) + var metadata: Metadata = ["key1-bin": [0], "key1-bin": [1]] + metadata.replaceOrAddBinary([2], forKey: "key2-bin") + #expect(Array(metadata[binaryValues: "key1-bin"]) == [[0], [1]]) + #expect(Array(metadata[binaryValues: "key2-bin"]) == [[2]]) + #expect(metadata.count == 3) } @Test("Replace values for existing key") + @available(gRPCSwift 2.0, *) mutating func replaceValues() async throws { - self.metadata.replaceOrAddBinary([2], forKey: "key1-bin") - #expect(Array(self.metadata[binaryValues: "key1-bin"]) == [[2]]) - #expect(self.metadata.count == 1) + var metadata: Metadata = ["key1-bin": [0], "key1-bin": [1]] + metadata.replaceOrAddBinary([2], forKey: "key1-bin") + #expect(Array(metadata[binaryValues: "key1-bin"]) == [[2]]) + #expect(metadata.count == 1) } } } @Test("Reserve more capacity increases capacity") + @available(gRPCSwift 2.0, *) func reserveMoreCapacity() { var metadata = Metadata() #expect(metadata.capacity == 0) @@ -138,6 +142,7 @@ struct MetadataTests { } @Test("Reserve less capacity doesn't reduce capacity") + @available(gRPCSwift 2.0, *) func reserveCapacity() { var metadata = Metadata() #expect(metadata.capacity == 0) @@ -148,6 +153,7 @@ struct MetadataTests { } @Test("Iterate over all values for a key") + @available(gRPCSwift 2.0, *) func iterateOverValuesForKey() { let metadata: Metadata = [ "key-bin": "1", @@ -162,6 +168,7 @@ struct MetadataTests { } @Test("Iterate over string values for a key") + @available(gRPCSwift 2.0, *) func iterateOverStringsForKey() { let metadata: Metadata = [ "key-bin": "1", @@ -176,6 +183,7 @@ struct MetadataTests { } @Test("Iterate over binary values for a key") + @available(gRPCSwift 2.0, *) func iterateOverBinaryForKey() { let metadata: Metadata = [ "key-bin": "1", @@ -190,6 +198,7 @@ struct MetadataTests { } @Test("Iterate over base64 encoded binary values for a key") + @available(gRPCSwift 2.0, *) func iterateOverBase64BinaryEncodedValuesForKey() { let metadata: Metadata = [ "key-bin": "c3RyaW5nMQ==", @@ -213,6 +222,7 @@ struct MetadataTests { } @Test("Subscripts are case-insensitive") + @available(gRPCSwift 2.0, *) func subscriptIsCaseInsensitive() { let metadata: Metadata = [ "key1": "value1", @@ -228,50 +238,57 @@ struct MetadataTests { @Suite("Remove all") struct RemoveAll { - var metadata: Metadata = [ - "key1": "value1", - "key2": "value2", - "key3": "value1", - ] - @Test("Where value matches") + @available(gRPCSwift 2.0, *) mutating func removeAllWhereValueMatches() async throws { - self.metadata.removeAll { _, value in + var metadata: Metadata = ["key1": "value1", "key2": "value2", "key3": "value1"] + metadata.removeAll { _, value in value == "value1" } - #expect(self.metadata == ["key2": "value2"]) + #expect(metadata == ["key2": "value2"]) } @Test("Where key matches") + @available(gRPCSwift 2.0, *) mutating func removeAllWhereKeyMatches() async throws { - self.metadata.removeAll { key, _ in + var metadata: Metadata = ["key1": "value1", "key2": "value2", "key3": "value1"] + metadata.removeAll { key, _ in key == "key2" } - #expect(self.metadata == ["key1": "value1", "key3": "value1"]) + #expect(metadata == ["key1": "value1", "key3": "value1"]) } } @Suite("Merge") struct Merge { - var metadata: Metadata = [ - "key1": "value1-1", - "key2": "value2", - "key3": "value3", - ] - var otherMetadata: Metadata = [ - "key4": "value4", - "key5": "value5", - ] + @available(gRPCSwift 2.0, *) + var metadata: Metadata { + [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + ] + } + @available(gRPCSwift 2.0, *) + var otherMetadata: Metadata { + [ + "key4": "value4", + "key5": "value5", + ] + } @Test("Where key is already present with a different value") + @available(gRPCSwift 2.0, *) mutating func mergeWhereKeyIsAlreadyPresentWithDifferentValue() async throws { - self.otherMetadata.addString("value1-2", forKey: "key1") - self.metadata.add(contentsOf: self.otherMetadata) + var otherMetadata = self.otherMetadata + otherMetadata.addString("value1-2", forKey: "key1") + var metadata = metadata + metadata.add(contentsOf: otherMetadata) #expect( - self.metadata == [ + metadata == [ "key1": "value1-1", "key2": "value2", "key3": "value3", @@ -283,12 +300,15 @@ struct MetadataTests { } @Test("Where key is already present with same value") + @available(gRPCSwift 2.0, *) mutating func mergeWhereKeyIsAlreadyPresentWithSameValue() async throws { - self.otherMetadata.addString("value1-1", forKey: "key1") - self.metadata.add(contentsOf: self.otherMetadata) + var otherMetadata = otherMetadata + otherMetadata.addString("value1-1", forKey: "key1") + var metadata = metadata + metadata.add(contentsOf: otherMetadata) #expect( - self.metadata == [ + metadata == [ "key1": "value1-1", "key2": "value2", "key3": "value3", @@ -300,11 +320,13 @@ struct MetadataTests { } @Test("Where key is not already present") + @available(gRPCSwift 2.0, *) mutating func mergeWhereKeyIsNotAlreadyPresent() async throws { - self.metadata.add(contentsOf: self.otherMetadata) + var metadata = self.metadata + metadata.add(contentsOf: self.otherMetadata) #expect( - self.metadata == [ + metadata == [ "key1": "value1-1", "key2": "value2", "key3": "value3", @@ -317,18 +339,23 @@ struct MetadataTests { @Suite("Description") struct Description { - let metadata: Metadata = [ - "key1": "value1", - "key2": "value2", - "key-bin": .binary([1, 2, 3]), - ] + @available(gRPCSwift 2.0, *) + var metadata: Metadata { + [ + "key1": "value1", + "key2": "value2", + "key-bin": .binary([1, 2, 3]), + ] + } @Test("Metadata") + @available(gRPCSwift 2.0, *) func describeMetadata() async throws { #expect("\(self.metadata)" == #"["key1": "value1", "key2": "value2", "key-bin": [1, 2, 3]]"#) } @Test("Metadata.Value") + @available(gRPCSwift 2.0, *) func describeMetadataValue() async throws { for (key, value) in self.metadata { switch key { diff --git a/Tests/GRPCCoreTests/MethodDescriptorTests.swift b/Tests/GRPCCoreTests/MethodDescriptorTests.swift index 889a0c878..12329ffc0 100644 --- a/Tests/GRPCCoreTests/MethodDescriptorTests.swift +++ b/Tests/GRPCCoreTests/MethodDescriptorTests.swift @@ -19,6 +19,7 @@ import Testing @Suite struct MethodDescriptorTests { @Test("Fully qualified name") + @available(gRPCSwift 2.0, *) func testFullyQualifiedName() { let descriptor = MethodDescriptor(fullyQualifiedService: "foo.bar", method: "Baz") #expect(descriptor.service == ServiceDescriptor(fullyQualifiedService: "foo.bar")) @@ -27,6 +28,7 @@ struct MethodDescriptorTests { } @Test("CustomStringConvertible") + @available(gRPCSwift 2.0, *) func description() { let descriptor = MethodDescriptor( service: ServiceDescriptor(fullyQualifiedService: "foo.Foo"), diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift index b4eba43d0..7c9968423 100644 --- a/Tests/GRPCCoreTests/RPCErrorTests.swift +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -19,6 +19,7 @@ import Testing @Suite("RPCError Tests") struct RPCErrorTests { @Test("Custom String Convertible") + @available(gRPCSwift 2.0, *) func testCustomStringConvertible() { #expect(String(describing: RPCError(code: .dataLoss, message: "")) == #"dataLoss: """#) #expect( @@ -36,6 +37,7 @@ struct RPCErrorTests { } @Test("Error from Status") + @available(gRPCSwift 2.0, *) func testErrorFromStatus() throws { var status = Status(code: .ok, message: "") // ok isn't an error @@ -77,11 +79,13 @@ struct RPCErrorTests { (Status.Code.unauthenticated, RPCError.Code.unauthenticated), ] ) + @available(gRPCSwift 2.0, *) func testErrorCodeFromStatusCode(statusCode: Status.Code, rpcErrorCode: RPCError.Code?) throws { #expect(RPCError.Code(statusCode) == rpcErrorCode) } @Test("Equatable Conformance") + @available(gRPCSwift 2.0, *) func testEquatableConformance() { #expect( RPCError(code: .cancelled, message: "") @@ -135,11 +139,13 @@ struct RPCErrorTests { (.unauthenticated, 16), ] ) + @available(gRPCSwift 2.0, *) func testStatusCodeRawValues(statusCode: RPCError.Code, rawValue: Int) { #expect(statusCode.rawValue == rawValue, "\(statusCode) had unexpected raw value") } @Test("Flatten causes with same status code") + @available(gRPCSwift 2.0, *) func testFlattenCausesWithSameStatusCode() { let error1 = RPCError(code: .unknown, message: "Error 1.") let error2 = RPCError(code: .unknown, message: "Error 2.", cause: error1) @@ -162,6 +168,7 @@ struct RPCErrorTests { } @Test("Causes of errors with different status codes aren't flattened") + @available(gRPCSwift 2.0, *) func testDifferentStatusCodeAreNotFlattened() throws { let error1 = RPCError(code: .unknown, message: "Error 1.") let error2 = RPCError(code: .dataLoss, message: "Error 2.", cause: error1) @@ -191,6 +198,7 @@ struct RPCErrorTests { } @Test("Convert type to RPCError") + @available(gRPCSwift 2.0, *) func convertTypeUsingRPCErrorConvertible() { struct Cause: Error {} struct ConvertibleError: RPCErrorConvertible { @@ -208,6 +216,7 @@ struct RPCErrorTests { } @Test("Convert type to RPCError with defaults") + @available(gRPCSwift 2.0, *) func convertTypeUsingRPCErrorConvertibleDefaults() { struct ConvertibleType: RPCErrorConvertible { var rpcErrorCode: RPCError.Code { .unknown } @@ -222,6 +231,7 @@ struct RPCErrorTests { } @Test("Convert error to RPCError with defaults") + @available(gRPCSwift 2.0, *) func convertErrorUsingRPCErrorConvertibleDefaults() { struct ConvertibleType: RPCErrorConvertible, Error { var rpcErrorCode: RPCError.Code { .unknown } diff --git a/Tests/GRPCCoreTests/RPCPartsTests.swift b/Tests/GRPCCoreTests/RPCPartsTests.swift index 3bf72e85e..605821fb0 100644 --- a/Tests/GRPCCoreTests/RPCPartsTests.swift +++ b/Tests/GRPCCoreTests/RPCPartsTests.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) final class RPCPartsTests: XCTestCase { func testPartsFitInExistentialContainer() { XCTAssertLessThanOrEqual(MemoryLayout>.size, 24) diff --git a/Tests/GRPCCoreTests/RuntimeErrorTests.swift b/Tests/GRPCCoreTests/RuntimeErrorTests.swift index fb8411687..9881a60e5 100644 --- a/Tests/GRPCCoreTests/RuntimeErrorTests.swift +++ b/Tests/GRPCCoreTests/RuntimeErrorTests.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) final class RuntimeErrorTests: XCTestCase { func testCopyOnWrite() { // RuntimeError has a heap based storage, so check CoW semantics are correctly implemented. diff --git a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift index ef4ec8988..20c5897cd 100644 --- a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift +++ b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift @@ -29,6 +29,7 @@ struct ServiceDescriptorTests { ("", "", ""), ] ) + @available(gRPCSwift 2.0, *) func packageAndService(fullyQualified: String, package: String, service: String) { let descriptor = ServiceDescriptor(fullyQualifiedService: fullyQualified) #expect(descriptor.fullyQualifiedService == fullyQualified) @@ -37,6 +38,7 @@ struct ServiceDescriptorTests { } @Test("CustomStringConvertible") + @available(gRPCSwift 2.0, *) func description() { let descriptor = ServiceDescriptor(fullyQualifiedService: "foo.Foo") #expect(String(describing: descriptor) == "foo.Foo") diff --git a/Tests/GRPCCoreTests/StatusTests.swift b/Tests/GRPCCoreTests/StatusTests.swift index fd442586a..345ba664f 100644 --- a/Tests/GRPCCoreTests/StatusTests.swift +++ b/Tests/GRPCCoreTests/StatusTests.swift @@ -22,6 +22,7 @@ struct StatusTests { @Suite("Code") struct Code { @Test("rawValue", arguments: zip(Status.Code.all, 0 ... 16)) + @available(gRPCSwift 2.0, *) func rawValueOfStatusCodes(code: Status.Code, expected: Int) { #expect(code.rawValue == expected) } @@ -33,28 +34,33 @@ struct StatusTests { Status.Code.all.dropFirst() // Drop '.ok', there is no '.ok' error code. ) ) + @available(gRPCSwift 2.0, *) func initFromRPCErrorCode(errorCode: RPCError.Code, expected: Status.Code) { #expect(Status.Code(errorCode) == expected) } @Test("Initialize from rawValue", arguments: zip(0 ... 16, Status.Code.all)) + @available(gRPCSwift 2.0, *) func initFromRawValue(rawValue: Int, expected: Status.Code) { #expect(Status.Code(rawValue: rawValue) == expected) } @Test("Initialize from invalid rawValue", arguments: [-1, 17, 100, .max]) + @available(gRPCSwift 2.0, *) func initFromInvalidRawValue(rawValue: Int) { #expect(Status.Code(rawValue: rawValue) == nil) } } @Test("CustomStringConvertible conformance") + @available(gRPCSwift 2.0, *) func customStringConvertible() { #expect("\(Status(code: .ok, message: ""))" == #"ok: """#) #expect("\(Status(code: .dataLoss, message: "oh no"))" == #"dataLoss: "oh no""#) } @Test("Equatable conformance") + @available(gRPCSwift 2.0, *) func equatable() { let ok = Status(code: .ok, message: "") let okWithMessage = Status(code: .ok, message: "message") @@ -66,6 +72,7 @@ struct StatusTests { } @Test("Fits in existential container") + @available(gRPCSwift 2.0, *) func fitsInExistentialContainer() { #expect(MemoryLayout.size <= 24) } @@ -84,6 +91,7 @@ struct StatusTests { (418, Status(code: .unknown, message: "HTTP 418")), ] ) + @available(gRPCSwift 2.0, *) func convertFromHTTPStatusCode(code: Int, expected: Status) { let status = Status(httpStatusCode: code) #expect(status == expected) diff --git a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift index 79cfef19d..a612649cf 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) internal final class AsyncSequenceOfOneTests: XCTestCase { func testSuccessPath() async throws { let sequence = RPCAsyncSequence.one("foo") diff --git a/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift b/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift index 55479d38c..969025063 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class BroadcastAsyncSequenceTests: XCTestCase { func testSingleSubscriberToEmptyStream() async throws { let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) diff --git a/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift b/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift index cf9c4f679..dc38b25d9 100644 --- a/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift +++ b/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift @@ -16,6 +16,7 @@ import Synchronization +@available(gRPCSwift 2.0, *) final class AtomicCounter: Sendable { private let counter: Atomic diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index ba6c1abf1..3c46a35d5 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -16,6 +16,7 @@ import GRPCCore +@available(gRPCSwift 2.0, *) extension ClientInterceptor where Self == RejectAllClientInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllClientInterceptor(reject: error) @@ -27,6 +28,7 @@ extension ClientInterceptor where Self == RejectAllClientInterceptor { } +@available(gRPCSwift 2.0, *) extension ClientInterceptor where Self == RequestCountingClientInterceptor { static func requestCounter(_ counter: AtomicCounter) -> Self { return RequestCountingClientInterceptor(counter: counter) @@ -34,6 +36,7 @@ extension ClientInterceptor where Self == RequestCountingClientInterceptor { } /// Rejects all RPCs with the provided error. +@available(gRPCSwift 2.0, *) struct RejectAllClientInterceptor: ClientInterceptor { enum Mode: Sendable { /// Throw the error rather. @@ -69,6 +72,7 @@ struct RejectAllClientInterceptor: ClientInterceptor { } } +@available(gRPCSwift 2.0, *) struct RequestCountingClientInterceptor: ClientInterceptor { /// The number of requests made. let counter: AtomicCounter diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 8340aa130..5918102db 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -16,6 +16,7 @@ import GRPCCore +@available(gRPCSwift 2.0, *) extension ServerInterceptor where Self == RejectAllServerInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllServerInterceptor(reject: error) @@ -26,6 +27,7 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { } } +@available(gRPCSwift 2.0, *) extension ServerInterceptor where Self == RequestCountingServerInterceptor { static func requestCounter(_ counter: AtomicCounter) -> Self { RequestCountingServerInterceptor(counter: counter) @@ -33,6 +35,7 @@ extension ServerInterceptor where Self == RequestCountingServerInterceptor { } /// Rejects all RPCs with the provided error. +@available(gRPCSwift 2.0, *) struct RejectAllServerInterceptor: ServerInterceptor { enum Mode: Sendable { /// Throw the error rather. @@ -68,6 +71,7 @@ struct RejectAllServerInterceptor: ServerInterceptor { } } +@available(gRPCSwift 2.0, *) struct RequestCountingServerInterceptor: ServerInterceptor { /// The number of requests made. let counter: AtomicCounter diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift index 35eca49c8..cc27182b5 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+Identity.swift @@ -15,12 +15,14 @@ */ import GRPCCore +@available(gRPCSwift 2.0, *) struct IdentitySerializer: MessageSerializer { func serialize(_ message: [UInt8]) throws -> Bytes { return Bytes(message) } } +@available(gRPCSwift 2.0, *) struct IdentityDeserializer: MessageDeserializer { func deserialize(_ serializedMessageBytes: Bytes) throws -> [UInt8] { return serializedMessageBytes.withUnsafeBytes { diff --git a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift index d2c6ef452..bb734479f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Coding+JSON.swift @@ -20,6 +20,7 @@ import struct Foundation.Data import class Foundation.JSONDecoder import class Foundation.JSONEncoder +@available(gRPCSwift 2.0, *) struct JSONSerializer: MessageSerializer { func serialize(_ message: Message) throws -> Bytes { do { @@ -32,6 +33,7 @@ struct JSONSerializer: MessageSerializer { } } +@available(gRPCSwift 2.0, *) struct JSONDeserializer: MessageDeserializer { func deserialize(_ serializedMessageBytes: Bytes) throws -> Message { do { diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift index 950e2c61a..64c7c85dd 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift @@ -15,6 +15,7 @@ */ import GRPCCore +@available(gRPCSwift 2.0, *) extension RPCAsyncSequence where Failure == any Error { static func elements(_ elements: Element...) -> Self { return .elements(elements) diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift index eafd569ab..d4426c4e9 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -16,6 +16,7 @@ import GRPCCore import XCTest +@available(gRPCSwift 2.0, *) extension RPCWriter { /// Returns a writer which calls `XCTFail(_:)` on every write. static func failTestOnWrite(elementType: Element.Type = Element.self) -> Self { diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index 4783d03e2..1bd5bbbc4 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -15,6 +15,7 @@ */ import GRPCCore +@available(gRPCSwift 2.0, *) struct BinaryEcho: RegistrableRPCService { static let serviceDescriptor = ServiceDescriptor(package: "echo", service: "Echo") diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift index a543defc8..12777517f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift @@ -16,6 +16,7 @@ import Foundation import GRPCCore +@available(gRPCSwift 2.0, *) struct HelloWorld: RegistrableRPCService { static let serviceDescriptor = ServiceDescriptor(package: "helloworld", service: "HelloWorld") diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index d8f4fb053..2e97f8f0b 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -15,6 +15,7 @@ */ @testable import GRPCCore +@available(gRPCSwift 2.0, *) struct AnyClientTransport: ClientTransport, Sendable { typealias Bytes = [UInt8] @@ -78,6 +79,7 @@ struct AnyClientTransport: ClientTransport, Sendable { } } +@available(gRPCSwift 2.0, *) struct AnyServerTransport: ServerTransport, Sendable { typealias Bytes = [UInt8] diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 50fe696e3..5b1ef428f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -16,6 +16,7 @@ @testable import GRPCCore +@available(gRPCSwift 2.0, *) struct StreamCountingClientTransport: ClientTransport, Sendable { typealias Bytes = [UInt8] @@ -75,6 +76,7 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } } +@available(gRPCSwift 2.0, *) struct StreamCountingServerTransport: ServerTransport, Sendable { typealias Bytes = [UInt8] diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index e8ee489f7..c635dc249 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -15,6 +15,7 @@ */ @testable import GRPCCore +@available(gRPCSwift 2.0, *) struct ThrowOnStreamCreationTransport: ClientTransport { typealias Bytes = [UInt8] @@ -49,6 +50,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { } } +@available(gRPCSwift 2.0, *) struct ThrowOnRunServerTransport: ServerTransport { typealias Bytes = [UInt8] @@ -69,6 +71,7 @@ struct ThrowOnRunServerTransport: ServerTransport { } } +@available(gRPCSwift 2.0, *) struct ThrowOnSignalServerTransport: ServerTransport { typealias Bytes = [UInt8] diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 07acf664e..0e4b5e960 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -65,6 +65,7 @@ func XCTAssertThrowsErrorAsync( } } +@available(gRPCSwift 2.0, *) func XCTAssertThrowsRPCError( _ expression: @autoclosure () throws -> T, _ errorHandler: (RPCError) -> Void @@ -78,6 +79,7 @@ func XCTAssertThrowsRPCError( } } +@available(gRPCSwift 2.0, *) func XCTAssertThrowsRPCErrorAsync( _ expression: () async throws -> T, errorHandler: (RPCError) -> Void @@ -92,6 +94,7 @@ func XCTAssertThrowsRPCErrorAsync( } } +@available(gRPCSwift 2.0, *) func XCTAssertRejected( _ response: StreamingClientResponse, errorHandler: (RPCError) -> Void @@ -104,6 +107,7 @@ func XCTAssertRejected( } } +@available(gRPCSwift 2.0, *) func XCTAssertRejected( _ response: ClientResponse, errorHandler: (RPCError) -> Void @@ -116,6 +120,7 @@ func XCTAssertRejected( } } +@available(gRPCSwift 2.0, *) func XCTAssertMetadata( _ part: RPCResponsePart?, metadataHandler: (Metadata) -> Void = { _ in } @@ -128,6 +133,7 @@ func XCTAssertMetadata( } } +@available(gRPCSwift 2.0, *) func XCTAssertMetadata( _ part: RPCRequestPart?, metadataHandler: (Metadata) async throws -> Void = { _ in } @@ -140,6 +146,7 @@ func XCTAssertMetadata( } } +@available(gRPCSwift 2.0, *) func XCTAssertMessage( _ part: RPCResponsePart?, messageHandler: (Bytes) -> Void = { _ in } @@ -152,6 +159,7 @@ func XCTAssertMessage( } } +@available(gRPCSwift 2.0, *) func XCTAssertMessage( _ part: RPCRequestPart?, messageHandler: (Bytes) async throws -> Void = { _ in } @@ -164,6 +172,7 @@ func XCTAssertMessage( } } +@available(gRPCSwift 2.0, *) func XCTAssertStatus( _ part: RPCResponsePart?, statusHandler: (Status, Metadata) -> Void = { _, _ in } diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index a22bb32be..de0caaef5 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -20,6 +20,7 @@ import Testing struct TimeoutTests { @Test("Initialize from invalid String value", arguments: ["", "H", "123", "100000000S", "123j"]) + @available(gRPCSwift 2.0, *) func initFromStringWithInvalidValue(_ value: String) throws { #expect(Timeout(decoding: value) == nil) } @@ -35,6 +36,7 @@ struct TimeoutTests { ("123n", .nanoseconds(123)), ] as [(String, Duration)] ) + @available(gRPCSwift 2.0, *) func initFromString(_ value: String, expected: Duration) throws { let timeout = try #require(Timeout(decoding: value)) #expect(timeout.duration == expected) @@ -51,6 +53,7 @@ struct TimeoutTests { .nanoseconds(100), ] as [Duration] ) + @available(gRPCSwift 2.0, *) func initFromDuration(_ value: Duration) { let timeout = Timeout(duration: value) #expect(timeout.duration == value) @@ -77,6 +80,7 @@ struct TimeoutTests { (Duration(secondsComponent: 1, attosecondsComponent: Int64(1e11)), .seconds(1)), ] as [(Duration, Duration)] ) + @available(gRPCSwift 2.0, *) func initFromDurationWithLossOfPrecision(original: Duration, rounded: Duration) { let timeout = Timeout(duration: original) #expect(timeout.duration == rounded) diff --git a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift index 9e89e032d..123ca966e 100644 --- a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift +++ b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(gRPCSwift 2.0, *) final class RetryThrottleTests: XCTestCase { func testThrottleOnInit() { let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift b/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift index 930b18183..16391c1d9 100644 --- a/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift +++ b/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift @@ -21,6 +21,7 @@ import Testing @Suite("withGRPCServer / withGRPCClient") struct WithMethods { @Test("Actor isolation") + @available(gRPCSwift 2.0, *) func actorIsolation() async throws { let testActor = TestActor() #expect(await !testActor.hasRun) @@ -29,6 +30,7 @@ struct WithMethods { } } +@available(gRPCSwift 2.0, *) fileprivate actor TestActor { private(set) var hasRun = false diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index c64f97646..9dd66feb9 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -18,6 +18,7 @@ import GRPCCore import GRPCInProcessTransport import XCTest +@available(gRPCSwift 2.0, *) final class InProcessClientTransportTests: XCTestCase { struct FailTest: Error {} @@ -271,6 +272,7 @@ final class InProcessClientTransportTests: XCTestCase { } } + @available(gRPCSwift 2.0, *) func makeClient( server: InProcessTransport.Server = InProcessTransport.Server(peer: "in-process:1234") ) -> InProcessTransport.Client { @@ -299,6 +301,7 @@ final class InProcessClientTransportTests: XCTestCase { } } +@available(gRPCSwift 2.0, *) extension MethodDescriptor { static let testTest = Self(fullyQualifiedService: "test", method: "test") } diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 92c3c5c8e..7d9a70093 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -19,6 +19,7 @@ import XCTest @testable import GRPCCore @testable import GRPCInProcessTransport +@available(gRPCSwift 2.0, *) final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessTransport.Server(peer: "in-process:1234") diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 5751d74a1..40a2e0b46 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -22,6 +22,7 @@ import Testing struct InProcessTransportTests { private static let cancellationModes = ["await-cancelled", "with-cancellation-handler"] + @available(gRPCSwift 2.0, *) private func withTestServerAndClient( execute: ( GRPCServer, @@ -46,6 +47,7 @@ struct InProcessTransportTests { } @Test("RPC cancelled by graceful shutdown", arguments: Self.cancellationModes) + @available(gRPCSwift 2.0, *) func cancelledByGracefulShutdown(mode: String) async throws { try await self.withTestServerAndClient { server, client in try await client.serverStreaming( @@ -69,6 +71,7 @@ struct InProcessTransportTests { } @Test("Peer info") + @available(gRPCSwift 2.0, *) func peerInfo() async throws { try await self.withTestServerAndClient { server, client in defer { @@ -91,6 +94,7 @@ struct InProcessTransportTests { } } +@available(gRPCSwift 2.0, *) private struct TestService: RegistrableRPCService { func cancellation( request: ServerRequest, @@ -154,6 +158,7 @@ private struct TestService: RegistrableRPCService { } } +@available(gRPCSwift 2.0, *) extension MethodDescriptor { fileprivate static let testCancellation = Self( fullyQualifiedService: "test", @@ -171,12 +176,14 @@ private struct PeerInfo: Codable { var remote: String } +@available(gRPCSwift 2.0, *) private struct UTF8Serializer: MessageSerializer { func serialize(_ message: String) throws -> Bytes { Bytes(message.utf8) } } +@available(gRPCSwift 2.0, *) private struct UTF8Deserializer: MessageDeserializer { func deserialize(_ serializedMessageBytes: Bytes) throws -> String { serializedMessageBytes.withUnsafeBytes { @@ -185,12 +192,14 @@ private struct UTF8Deserializer: MessageDeserializer { } } +@available(gRPCSwift 2.0, *) private struct VoidSerializer: MessageSerializer { func serialize(_ message: Void) throws -> Bytes { Bytes(repeating: 0, count: 0) } } +@available(gRPCSwift 2.0, *) private struct VoidDeserializer: MessageDeserializer { func deserialize(_ serializedMessageBytes: Bytes) throws { } diff --git a/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift b/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift index 905e90525..ed44053c9 100644 --- a/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift +++ b/Tests/GRPCInProcessTransportTests/Test Utilities/JSONSerializing.swift @@ -20,6 +20,7 @@ import struct Foundation.Data import class Foundation.JSONDecoder import class Foundation.JSONEncoder +@available(gRPCSwift 2.0, *) struct JSONSerializer: MessageSerializer { func serialize(_ message: Message) throws -> Bytes { do { @@ -32,6 +33,7 @@ struct JSONSerializer: MessageSerializer { } } +@available(gRPCSwift 2.0, *) struct JSONDeserializer: MessageDeserializer { func deserialize(_ serializedMessageBytes: Bytes) throws -> Message { do { From 369172abf87d3c83ce4c66e1a23c95dfe021a4c6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 15 May 2025 14:08:07 +0100 Subject: [PATCH 578/580] Normalise CI flags (#2240) Motivation: `-require-explicit-sendable` is ineffective without `-Xfrontend` but combining it with `-warnings-as-errors` surfaces a warning from SwiftPM. Modifications: - remove `-require-explicit-sendable`, it's not doing anything - add `-require-explicit-availability` to nightly jobs Result: More consistent CI flags --- .github/workflows/main.yml | 8 ++++---- .github/workflows/pull_request.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b00e62a07..843f67aa4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,10 +13,10 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false - linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" - linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" benchmarks: name: Benchmarks diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index cada4efcc..ed4bc8afe 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,10 +22,10 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false - linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" - linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" - linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" + linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" construct-examples-matrix: name: Construct Examples matrix From 36e1ad1d861a111f191c71363cc97531c4f61838 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 27 May 2025 14:47:05 +0100 Subject: [PATCH 579/580] Allow padding to be omitted from binary metadata values (#2243) Motivation: Binary metadata values are encoded as base64 strings. The gRPC spec doesn't require that the values are padded. Currently gRPC Swift requires values to be padded otherwise decoding will fail. Modifications: - Allow padding characters to be omitted when decoding base64 Result: Can decode unpadded binary metadata values --- ...e_binary_values_when_only_strings_stored.p90.json | 6 +----- ...e_binary_values_when_only_strings_stored.p90.json | 6 +----- ...e_binary_values_when_only_strings_stored.p90.json | 6 +----- ...e_binary_values_when_only_strings_stored.p90.json | 6 +----- Sources/GRPCCore/Metadata.swift | 2 +- Tests/GRPCCoreTests/MetadataTests.swift | 12 ++++++++++++ 6 files changed, 17 insertions(+), 21 deletions(-) diff --git a/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json index b59f05063..9c403f41f 100644 --- a/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -1,7 +1,3 @@ { - "mallocCountTotal" : 2000, - "memoryLeaked" : 0, - "releaseCount" : 6001, - "retainCount" : 2000, - "syscalls" : 0 + "mallocCountTotal": 1000 } diff --git a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json index b59f05063..9c403f41f 100644 --- a/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ b/IntegrationTests/Benchmarks/Thresholds/6.1/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -1,7 +1,3 @@ { - "mallocCountTotal" : 2000, - "memoryLeaked" : 0, - "releaseCount" : 6001, - "retainCount" : 2000, - "syscalls" : 0 + "mallocCountTotal": 1000 } diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json index b4aba1c3f..9c403f41f 100644 --- a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -1,7 +1,3 @@ { - "mallocCountTotal" : 2000, - "memoryLeaked" : 0, - "releaseCount" : 7001, - "retainCount" : 3000, - "syscalls" : 0 + "mallocCountTotal": 1000 } diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json index b4aba1c3f..9c403f41f 100644 --- a/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-next/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -1,7 +1,3 @@ { - "mallocCountTotal" : 2000, - "memoryLeaked" : 0, - "releaseCount" : 7001, - "retainCount" : 3000, - "syscalls" : 0 + "mallocCountTotal": 1000 } diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index c9e736eae..6c8263037 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -423,7 +423,7 @@ extension Metadata { switch value { case .string(let stringValue): do { - return try Base64.decode(string: stringValue) + return try Base64.decode(string: stringValue, options: [.omitPaddingCharacter]) } catch { continue } diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index 715869fdb..647a34179 100644 --- a/Tests/GRPCCoreTests/MetadataTests.swift +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -221,6 +221,18 @@ struct MetadataTests { #expect(Array(metadata[binaryValues: "key-bin"]) == expected) } + @Test("Iterate over unpadded base64 encoded binary values for a key") + @available(gRPCSwift 2.0, *) + func iterateOverUnpaddedBase64BinaryEncodedValuesForKey() { + let metadata: Metadata = [ + "key-bin": "YQ==", + "key-bin": "YQ", + ] + + let expected: [[UInt8]] = [[UInt8(ascii: "a")], [UInt8(ascii: "a")]] + #expect(Array(metadata[binaryValues: "key-bin"]) == expected) + } + @Test("Subscripts are case-insensitive") @available(gRPCSwift 2.0, *) func subscriptIsCaseInsensitive() { From adc18c3e1c55027d0ce43893897ac448e3f27ebe Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 3 Jun 2025 19:43:06 +0100 Subject: [PATCH 580/580] Add deprecation warnings (#2248) Motivation: v2 has moved to a new repo, grpc-swift-2. That's not all that discoverable. Modifications: - Deprecate commonly used high-level types with a link to a forums post explaining the move. Result: Users are notified about the move --- .github/workflows/pull_request.yml | 4 +-- .../Call/Server/RegistrableRPCService.swift | 1 + Sources/GRPCCore/GRPCClient.swift | 3 ++ Sources/GRPCCore/GRPCServer.swift | 3 ++ Sources/GRPCCore/Metadata.swift | 1 + .../GRPCCore/Transport/ClientTransport.swift | 1 + .../GRPCCore/Transport/ServerTransport.swift | 1 + .../InProcessTransport.swift | 1 + .../Configuration/Generated/rls.pb.swift | 30 +++++++++++-------- .../Generated/rls_config.pb.swift | 25 +++++++++------- .../Generated/service_config.pb.swift | 8 ----- dev/protos/generate.sh | 3 +- 12 files changed, 47 insertions(+), 34 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ed4bc8afe..2adc5ec34 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -22,8 +22,8 @@ jobs: with: linux_5_9_enabled: false linux_5_10_enabled: false - linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" - linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -warnings-as-errors -Xswiftc -require-explicit-availability" + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" + linux_6_1_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-availability" diff --git a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift index 7bd5819be..7eed9c647 100644 --- a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift +++ b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift @@ -23,6 +23,7 @@ /// ``RPCRouter/registerHandler(forMethod:deserializer:serializer:handler:)`` for each method /// you want to register with the router. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public protocol RegistrableRPCService: Sendable { /// Registers methods to server with the provided ``RPCRouter``. /// diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index a41c8b261..97e540ae5 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -55,6 +55,7 @@ private import Synchronization /// additional resources that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public final class GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. private let transport: Transport @@ -399,6 +400,7 @@ public final class GRPCClient: Sendable { /// - handleClient: A closure which is called with the client. When the closure returns, the /// client is shutdown gracefully. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public func withGRPCClient( transport: Transport, interceptors: [any ClientInterceptor] = [], @@ -428,6 +430,7 @@ public func withGRPCClient( /// client is shutdown gracefully. /// - Returns: The result of the `handleClient` closure. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public func withGRPCClient( transport: Transport, interceptorPipeline: [ConditionalInterceptor], diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index d2ca1ed77..3aa55d3c4 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -76,6 +76,7 @@ private import Synchronization /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle) and the /// `GRPCServiceLifecycle` module provided by [gRPC Swift Extras](https://github.com/grpc/grpc-swift-extras). @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public final class GRPCServer: Sendable { typealias Stream = RPCStream @@ -259,6 +260,7 @@ public final class GRPCServer: Sendable { /// server is shutdown gracefully. /// - Returns: The result of the `handleServer` closure. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public func withGRPCServer( transport: Transport, services: [any RegistrableRPCService], @@ -291,6 +293,7 @@ public func withGRPCServer( /// server is shutdown gracefully. /// - Returns: The result of the `handleServer` closure. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public func withGRPCServer( transport: Transport, services: [any RegistrableRPCService], diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 6c8263037..b3ea93810 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -80,6 +80,7 @@ /// the "-bin" suffix may have string values (rather than binary). These are deserialized automatically when /// using ``subscript(binaryValues:)``. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public struct Metadata: Sendable, Hashable { /// A metadata value. It can either be a simple string, or binary data. diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 4b3cf6545..89d21e1c3 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -26,6 +26,7 @@ /// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport /// package. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public protocol ClientTransport: Sendable { /// The bag-of-bytes type used by the transport. associatedtype Bytes: GRPCContiguousBytes & Sendable diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index d3891147d..62488a3bb 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -23,6 +23,7 @@ /// transport built on top of SwiftNIO in the https://github.com/grpc/grpc-swift-nio-transport /// package. @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public protocol ServerTransport: Sendable { /// The bag-of-bytes type used by the transport. associatedtype Bytes: GRPCContiguousBytes & Sendable diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index 4dc5d3f83..3ceab8156 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -17,6 +17,7 @@ public import GRPCCore @available(gRPCSwift 2.0, *) +@available(*, deprecated, message: "See https://forums.swift.org/t/80177") public struct InProcessTransport: Sendable { public let server: Self.Server public let client: Self.Client diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift index 36f8887af..003f6ff54 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift @@ -134,13 +134,16 @@ fileprivate let _protobuf_package = "grpc.lookup.v1" extension Grpc_Lookup_V1_RouteLookupRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".RouteLookupRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 3: .standard(proto: "target_type"), - 5: .same(proto: "reason"), - 6: .standard(proto: "stale_header_data"), - 4: .standard(proto: "key_map"), - 7: .same(proto: "extensions"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap( + reservedNames: ["server", "path"], + reservedRanges: [1..<3], + numberNameMappings: [ + 3: .standard(proto: "target_type"), + 5: .same(proto: "reason"), + 6: .standard(proto: "stale_header_data"), + 4: .standard(proto: "key_map"), + 7: .same(proto: "extensions"), + ]) mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -198,11 +201,14 @@ extension Grpc_Lookup_V1_RouteLookupRequest.Reason: SwiftProtobuf._ProtoNameProv extension Grpc_Lookup_V1_RouteLookupResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".RouteLookupResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 3: .same(proto: "targets"), - 2: .standard(proto: "header_data"), - 4: .same(proto: "extensions"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap( + reservedNames: ["target"], + reservedRanges: [1..<2], + numberNameMappings: [ + 3: .same(proto: "targets"), + 2: .standard(proto: "header_data"), + 4: .same(proto: "extensions"), + ]) mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift index bc6047eda..6ee5aa88f 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift @@ -587,17 +587,20 @@ extension Grpc_Lookup_V1_HttpKeyBuilder: SwiftProtobuf.Message, SwiftProtobuf._M extension Grpc_Lookup_V1_RouteLookupConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".RouteLookupConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "http_keybuilders"), - 2: .standard(proto: "grpc_keybuilders"), - 3: .standard(proto: "lookup_service"), - 4: .standard(proto: "lookup_service_timeout"), - 5: .standard(proto: "max_age"), - 6: .standard(proto: "stale_age"), - 7: .standard(proto: "cache_size_bytes"), - 8: .standard(proto: "valid_targets"), - 9: .standard(proto: "default_target"), - ] + static let _protobuf_nameMap = SwiftProtobuf._NameMap( + reservedNames: ["request_processing_strategy"], + reservedRanges: [10..<11], + numberNameMappings: [ + 1: .standard(proto: "http_keybuilders"), + 2: .standard(proto: "grpc_keybuilders"), + 3: .standard(proto: "lookup_service"), + 4: .standard(proto: "lookup_service_timeout"), + 5: .standard(proto: "max_age"), + 6: .standard(proto: "stale_age"), + 7: .standard(proto: "cache_size_bytes"), + 8: .standard(proto: "valid_targets"), + 9: .standard(proto: "default_target"), + ]) mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { diff --git a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift index c25062a78..2cefad51d 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift @@ -2549,15 +2549,11 @@ extension Grpc_ServiceConfig_RlsLoadBalancingPolicyConfig: SwiftProtobuf.Message var _childPolicy: [Grpc_ServiceConfig_LoadBalancingConfig] = [] var _childPolicyConfigTargetFieldName: String = String() - #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. // The type itself is protecting the reference to its storage via CoW semantics. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} @@ -3692,15 +3688,11 @@ extension Grpc_ServiceConfig_XdsClusterResolverLoadBalancingPolicyConfig.Discove var _overrideHostStatus: [Grpc_ServiceConfig_OverrideHostLoadBalancingPolicyConfig.HealthStatus] = [] var _telemetryLabels: Dictionary = [:] - #if swift(>=5.10) // This property is used as the initial default value for new instances of the type. // The type itself is protecting the reference to its storage via CoW semantics. // This will force a copy to be made of this reference when the first mutation occurs; // hence, it is safe to mark this as `nonisolated(unsafe)`. static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif private init() {} diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh index a4409f1d2..dec5de14c 100755 --- a/dev/protos/generate.sh +++ b/dev/protos/generate.sh @@ -21,7 +21,8 @@ protoc=$(which protoc) # Checkout and build the plugins. build_dir=$(mktemp -d) -git clone https://github.com/grpc/grpc-swift-protobuf --depth 1 "$build_dir" +git clone -b 1.3.0 https://github.com/grpc/grpc-swift-protobuf --depth 1 "$build_dir" + swift build --package-path "$build_dir" --product protoc-gen-swift swift build --package-path "$build_dir" --product protoc-gen-grpc-swift

zpHmf*vX5u}8h7dv{!NQnrSvkuntk8FC56I5#G;jysWl8|rcsuJ9z4l!_|dT>wc7E8 zqm!@YX|#vMrCbhn<_cLk3uLawsnLAlrCDv^8%`{KX2o<5KtV%O+Z@$|^WlCBr$x5| ze`WYJ@#ut<-qOX^XgAZ{22Ru49-0kA@N!g(_??rX{j*DJ;41XN_S-!pq4n{iJ`K%T2DEt3L;KNEI& z+a1_C?ThcWis}l?Qla-f{|3H(?1&WYm6apk&aDgF^$+rp*HwJxpB-fJLn;IHUq939 zC&$=l1f!kh+qNz`1KRfW6prd=pW;jMdLwneKf^MyYLK^>{R2u zn>3~ETf9Z_!ylX%WBs3`mgn=dnpQ7G17P(M2jk$j+rGOP5gK$KsJVjI#su|da!;ob zRu>QFHL2U+uPAr*I3&c)S>khKSoAZNUKBOP2t*EXUZM3^)q1e~sek5U(n|etC;4qf z`Gy~qPoBF^Z@!;TUxV>%2}34r^DCCW2P~|PAWV9MPJ}NTVYb5?4!q8i;V~B$yH^%_ z?~{IrrMv&$M!s%;>ep9mH({0e4!0IJ-^)KcDWsOQmCvfc~4k z5nuTXr$ke?g*DF{w`*q~NUdJ^wXm|3SX+Gg+vm9uW;W82ec2aud zRIi2lWJls&LRkA6i?$WLTr~r{r8=LgRL4~qd3r*VPA{y}xY_Xg^LGt~Q^7UlCo=fR zyd^$~-3ITUBeHVoGxJ}JXZE)`$tiu*GkzESb+HoJnp=?j{?1giK%h7?zE;39UBIU= z>>*Ow;LA{Zds^2Kzltyep-*c-1ZW!_Z^N{vq`R&=iG|?X&?TM{I2e+fm$>cJ0ms*+ zk5&(Ao!xR}m8f+jxmo=MtElG4a{0BM;DwCv-t3u5kVA?dq)%GaMCyx#-{Gt7UB53h zWqVe}{KRDExx%b>T3g0jtbNa~W;_}?_JB)R_cbs5*waD$MBM@MzHjzwYceUqqOxSA z&o8_(I<{MJOba!-7DUmu7I6%UAR$cReQ4I2N&^zh%ZVc1hmW%=F6Bz~bV@hF^%nwI z+RlVAX7mIjYm%q|8;Eu!ZYEUq_4Jh;B;55c@F9OcjXC7oin`|z(tE_~8m5(3HNMj7 z-X8_(t%rQ!A6?*oUS9B@7!LBcHS2eO$43hj}H+ z7Z_=pq+KnVEF}AHuM3U(F&v$_FA`p0%Ej~J6zHiK9-iN(pvLXqeZ1aJQi)$1JG9eK za@n7a{^~C8GrRA5Fx=Ol2=;;vu=u0BMCAhmGn4sFUWEKbPPLeF7f7e*R>r2UMpc11 zfq1t)X>?yx?4PnGYqs-t7@Ex0V80mUe$V}UcyVgj!};80s!drEn?jynuu!HIbW#_# zmp%0TmE*1s{p#XL#Y0w4*)**sba|GqaTI-0|l&tH((%n+fCZ4M{#yWsa##PJga2=HPI@Qb)65tmNC zt-l^v^YqJB|Hy%TPjcV_f=2UX^thgGzUKlr*{N#j%9ypps-`Cc(P34A#Dm98F5_*> z9pX6g{!i#1zv%Rfo{iTLJ|6lPPVn2Awj47Z>c6keKL!%6 zm(S}apz%+RVEJ8U{hLzitFT*#i|XW5bEijtz<#y)s@aSh3e?GhFaqgMJ57G4e&7ly zjBu}qiQna(9AShQP?o>NLG8g1YS}%qEpVFc@-+qhs~Rd9%^($JIRRfq;$K_OM(Yc; z_P|{3F#`;NmyT!+8Wl88rBBuP-47QJeVAJ-r=t2q%yrLdBQVE41790_0ps6-V}$MG znH_pq@!eP{o0R>cC$2&YH6sc}QpKMm6;+FC1(}yl#pRgp%aTmgujHZvdARGJ7}uC$ z-4NtD2qpQKi1Kg5`S0@k$0BuqTK&m&5(M%8-6XG@30_b5{J#r#{wwWVHxvA)BEhq8 zSTB!~3n+v^r*1>2D!-Ah+~MXsFROzR={ zPr@bbxJ=eBPn8+^!^^QfA~@1b`>7=#4fCTV6JPgzvka{p_YX!aaRV}#HW&%QmWPvQ z8qmuQU>oCmtL&iIk|Hr;t%9N+1(KCSWusqOO0U6Mftm1GupP(_cnccOt`D|_N=6ZQ z+(fo{F|98yMs z<_{xcaGx+}ZS5iSkKNI_rMU7i5f`07ytj1`1?sig5kx5vG#`#wn*HnsH0{`?bZ$~E zTyBAT3o0RUqJvOGhUf5XL-;@5!BqG&l5C&xJq;15AP0al){jYGNQyPU6QUpd;~jh! z?G~hUU|GyRj9AjoZou%@K}X|n{4l_8?v8(vAU;q0=4F(E=ldvF8%_BdF*R&C~sb$dxrU#5QK-wg0HKH{n#7iX_H!&Ja%GkRP9;L)viOY z$N3!+Eb1@ci7dDINMmL<<;vA87Iw>Zr7`sns$<=My6GErJw_SL?6RZiKXj_bDp8=a<2QiC=Cp6uS_2){!{t7M~N7ZSdxcw}`p% z#19pg56n3`9SW{zsW<33@d5nh-6VQ*7z>#96#rmH)nNe*4{W!ydNRL7ICHe!bJ>B}ZH&LK(-lc_!AI=v3ncxSdjjZoLcBmfoSFDjW1v*L^YvSFN7%a*0&+zFZ!Vhvx83r*VL0+Is zl?ZC80(Oo+9e_NOnIJX2HNlkP8p;qhX0W$_s^^up4GI4G)jsyQ9zTmqJLq`1a!f5A zah*9*uf=p*mrj-o`WHXkdCP0LnvF1>`VRM{qOxn8D`x~x5-89mImiA(tN93%RX!qo zcU7H-j{*hv0P$z=#-4_JoRKD3Tx#4wQnmCCv}N`oKZLEd3rpqA@Tl#Lr=6!hsC+nJ zb`4WLFuiSyF?uJ`4);xkFK>fs(UaFd0$R}c0(08L$9He{SU^7$T)}<~mgK!Q4y&nx zkD!QJ-AmHwNE341Op8G?WiAPObh%y!T01Sw%VY%94E;}>dS{DTEIA%zgt{$Jpt62M zaIU0Y{V*fgww5lVl9`+UzHvu?-_^?oPqGZSkhfXVE}|PeF9QI~^xV>Kw3Brell3Qh z(b&WOh-szCKr8w~#bGW$v0u^$oR1&j}7_6p1bYVmvGV-W0X$C|WeZz{ODf+P=$ zJj|71R=hLg*%!wC%7x7{3+S)UwEccFcRxwwM}_v{FPoDRY6rtXy>ayXm>p&~p$(2( zKpp~3<7eN@^Ov0g94_;FIbv(yNrP3KC~il34(gSIF#gPeI>1rWj{VH5IE3jW1(HIO zeOj>Nr)^n3b8cSiJaKjWi}sVIc44HOb$z#l-W4139nU9E(@ zYRqi6?f->69;6oofO8Xwoia~VAa>+~aPzkv2{&=}a9?$=8C@=avw>84& zv0wytuVqYY`Tk8RtQ$M|PjrTDFvTBG+W%infi45ux+#Ve9;5yyk%>gjgH5{R*Ahto zzWlEr7xACyar#%d!Vz}HG7*!idNbuK(Nsi{%F{=!T1cz|(yW>_UCyz?<0D4zf_ZHO zFAPOIJZASYV@|&T?t6g4A9M6l=;(22pQ{Wel<~ja8*!c^NkhEqyLOIsK-s)`4+=jH z%4BRM3(SB(4*V80vQLksmVq$er9ir?gJ=?MI0Xv(W`_6TUJCffFX|}}+nVb+0j@71 z0{KlP3T&|)s*SV%@5kh3MzQ>|3J%P}qNckc1u|*bGvE7N#p^Zyg{ZDF+pKEtg~HG3%kAfkGAqACpJChG)QVs1#k*ihJX{}k z5szM|<)lEO!eRReRUIZSDGh%58thff(u@0$?Q`I+WpMpYu&+sJxE_Eyg3$x0wP%vI zq{)*c=V3%Y{zDR>GMF3&;3ZF>SH10>KlB=hPXZ*mED}x(s6$v-0sQ%@mI5(r0EjVC zl4axk|Mu}IngSDbA`iW0KotX6>KCQ%jpI1#Cleq&;nGHfFnR&VT%Rbq>P8YIwi3+y zD_%VA$1=IU>O9gcH`sdpgTH@^iml%L141Pc_^{;CC!;^5oUJ}@tM2DsTEoh(7HuIBl0Vu#fQn1g) z`G05J|E}?IT=YX&d$z4j4I(&#_{0Guv4Q(FkQnrTov#P&aKFJX^X2^voW=+Dg@bA$ zb?wRsGrF(0gMBd&&-&tazmR}ItJ9mAq?YTettVZaPDpZ}&3l%-?&KOb)GJ+x?eoOjU3N1X_T-Ut&0t!^rMMjL zxfZMr^xyMt*YcwYvugH7yGVrI*tlTm{6(|N9!e{}%H8;zsrJKf+HITtzQ_TrgRLLn z9sl|Uvj(}^p8y#3%LTu|K7j@d{cuq2lCn>{-MwZpp8l7AY=;(-6Ma7XgAKo~Fqn{V?4{%W%IU zrp|Zw4&fIHUv!L^2f@v~j-kAprZjnuWp|zRmrWg?<2lFv02HPX0_DHhR~hZ&b_qkx z-bzHC-jjB-NCnumb>XQ&JF`bSD^gvOk*@UY=;4-2BF)#I1WuHmF@N4w z+(J0s4>Rb<9x%_FVC633RKI^}+rg(a)m1bz!u!aqUhQjDWJ+}4OD;qSY+yf#-LVbl zDzM+TWbJNZ?jWy@zJAB|=wv(Vr4eVkicEjga;aY7?`M)}$X5Aj*G2Iu=7S)Q&KzMm zunL@Zpk~sjCo0KUor!Qrk~SY7x=Nh<6CF?g@5JAKLNwpUD>yiWDHJ`RB6-0j&A7b1*$M9G zVi<4I%g~jx*}Vh>wxeTtI0tG0y9O9%+bmfM-22v8^U&ndt(h-M_`!V(lrQJi}S1_OTG)DIZf0Gi} zXH0K$X+Am8H*o9J-u%Uzd5^DXJe;~)db<5|hd%za<*7zel5KF`0HdeTwPd6gi$B78LRQ)8e)5|j0`=+pCoeLHsHEp6fH=R9E8nd>K`z^| zsM^PR&q%i1d|%Ap=lPEJ7&^XdaDRRzT+l~B;cdy0=N#IC9J;&PzU_DeXO17L&kWWn zsWW-AZ)pgT_(nKUvPoHfTkA3?Oa;Q4`abq@;?5>F!gjqKU!4wAp%~6=?sQnWZCBXh zsklW>bn}QJW~}~IU#Y>n+gcotxlf3a7fCY2Z2aJ$xzBZ%U>xJZ2lN(qyKs>;xtH`i*Hu=p$=c7?|+(H){WlzC;GAe(}VWkups^6Gw=6> z@LTGF?88rPB%us^<)9f+8!tb2GkP`$&3Q3S5bFZT<)=K!y?X1Mk<>9e!J>Vwgfq`0 zqFQkE9}qSr;sJ>Y42ZKU_p+>EC}>h=(R?_tdfqxXR%c$}^at1F6V-26Zh^Q9Rl%LC zudXs2=!FKcuqA8KTaejAN4I_G9M}fN{)p5`w#x0}})jf-Qx1|L2% zZOuNSr39yiPnxaX@csg#5di#ak0^M5;h$Mdk5nj0Z963yaHQeQr|P^WSwkLwQ&omx z1Fc?x0&bhdt3qW7!to$uR0osoHYE*8@u?6iE)bR$&k;7o@e?#+Jrq<~W#bYYmqMeH zFEMVtkr6T}`Z+XHOja;B9W&|*g3?}t6}uV%ehvH)ENjXg_Nczzdj&?g$FJQr$DO{2 zIr_AAoF9_!lzFtUjVVF(WW90a=){2v_&%L~PzKR*2R`7QsJ*M#7Kfa4ZNak@!&p&EV~ZP_=ZzvJFq4m^r%F~} zO=%Ji?#o3}TP?Z+noILMhky1|mIpgvMO43V_91fy&j-fX?@_2y`G)Uaohm~1N`C0w zsfod?e;Xv)?YBna+9!I)TIt`6cI4suT{e$-E93o>ToWd43Uy!{V#A(DTXwf~=`Bg* zJ!7;VI{MV+yM~a(We1SC-8n6K%kZ@`sRQi9!rKL2SR+djDXy%0xmE1kGuXoRlVB=)gI zUHwVm=AJx$Bi#}zqt_qa^oLB9mvC$8iJ)<@(t&u1aua%z3Enw|z}esP(k^{et^1X= z#l^jf&lmG|=~EeqJY?GRnsHm0#%1?6ZGyp@{`IuW4J`IQq0art7XQPa8HbMVH1twY z$rykUwn`$CTnvGx4>x7=qVv5!(gO&}@JKTvQ?F;p|L0Yx_aSU0r)h`EEzcV~@fX+lzxKkWs--zI6GiAbHxcsQXvnK<@2> z7p@`3PW<^Yied216`IA9Z@uhp88~ z?oxGTJ7Hn5{Pt6r!hUfU>_mW$_dd8pe$8JBXU~whaHV!-E9yc0^1=K@1qQE)n$wr( zUJ2UH89y$><+T;Di67r`(EDUkXduHP&kniUb>8-h_K{%rwST>S{p|fZTGQ4YAbsa+ zLSEl|UCLZanw$#QX*3WVxEphWY!Bf3&A+ zI(jfO|Bctn_9{zA@2;|w;M zn9yQNj6JQj5Wn{pIPZCv6u*vjB50tl_|b-EAH6_Rj%+{c44tsiqi<5J4OTg6sOV zwwN96$@$Ql45_FmI(d4BKj`+eT$`Qv?_pZ9tFQB&7_-`8@kbFS-L=X}n&Do3YA zx9#3a?z<7n2=(bXOU*%4S?kWVAgi(o!RMKH?0|>Nfy3AP9v?OnoT=YMcnmQeDVJ{3 z`5?wKNTE#cv1+G@mt6sC=W74usM>Aaqc$P_wWDWc8GTw_6YAztLZUH$0*vN@cC?l4 zXV9gF9^jc`ITzF5xb63yhf*Hj)pt79{;BMW(p^gp8{O((FF~I9wwV<`a|K}_k(UB- z9Xf}b@TaJqAEq0s$&xxCSvEm()XG||CO}VkRaxAvSlXe)K39U4frT*F5xL{IYJ)fX z%ky5fjQnUmCve;A;w9JX23(0b=R~M)$wwow+qW+B9AK}Vy)66q0xngfZ>0khtadzU zoW@Wk^dr>@34)7-DbyD?+cQS?R)1ijIKSaXAp*bDFk=xsptlr=cym)=Bzjav9Os`IaCB0jslK04Ne&Y~K#i*`@ zqk5c%CD*~!E)#b1;Qte*)y3o;iLm^>-;y5qnV&@fpoeAjRkIiG&~Y0`GS#@1xr&h)gpotYUhFl{33ct|DSZTj!GJ-})3TVWbBE|3jwiylCQ z*BBAqFV7S80S8$bA<+-R$CanXP#nOZ^CotcqB2_e9X+|ixH*dy94gEPrAcQkYAt2H z277I%{d6ZJ-rTnmd=AX6C>Vz;S6|<%;ss2x*A4&>+LH)XR^XV2Su|w5!oewKD^OS8 z6~Z8vhwEL!S1f#)9~A~Lpj{DdAU-Wu2E?a%`~+|iE>MvRjSx5O{<9~QQyN7w-EZcv zJdH!w@B@O;bZq0!i%u~=C6MPOYyc7=0$k!sowPrLXWnh%M;kZ6qrV%Vz?XGU;g$IQ zXC8ET?+_mG%)~K$Aw+s3c2=VV1ZNhz?*UjjJGhVYdOMKW$V(6-FBI4NAej?z2mY!Z zvPD*cI11h?qb#ev;N%?}Fny~PeWK>uZ-=7YHgU+`+j|B`( zy$=-wn3sqeF{(ImD-53m0VSg+2ys%J$;~CeTXIlg!GwD>RH0!$cAAbPfoMS|(4VMd zVEt$gCF-2TPxn%#o}?K{u#Z!jenE&d7A_0wAy-$6L*u)2kewWEkK{)!C?XPINHsX* z2H{Ezr}lgt(+{9^!~*~iP@||gk!o-S?3xFlk}O18IGaU{B9WPKZ$aTns^n=-1AZX% z5J2FLhXK}TuDS%S&dLK^qXUP%#To{hJvUo)i6-?1LU|0g)zvy&}r6vv-M7 zq;eT(DBGbYic@`{1+wxks48d`aTg40v}7`k4u2wb*lP0h2714J20=NbZrO z{Aqd&vVdvH4+0|yCqx8trg;L#3@|`eBtmrq)qPXUFW{T?9zeJn03BRG{xV9;YY>3o zIRmQN1j}&5i=G5(+z4ts%+){8GsI~KEI-MIvQRNEK-U+66X+^3C7J>jz92$a0cZpb z1BC4X6&?j^Q8fioEySY7!0OnmrUPrV&6ER?>e=)JkYbuj=EwqMMrJKfKvo395`ah< z(ovx_g*Eh^EOdDYc>$x7GduAx?7PLJDxNVwhczN19=7$6!86d^jZ#pJEtv^U;Ko)=Il?fyl2cwLa-6w z{D&1>Czus3=%acd%E^;p5S+U&Gxbpjw_qqEXwT^hrb%=|gE(Su$IKgIr zrYCU4E38grT!VBbT292A)STi*1p)ex*JD1N%19>ZBwI6YYWLxyO5W1Qxxt$+?2-L` z(ztG3(QT`@kEPsgW%O#mrytj>UuN~M&`s0WHfEMmjOJ1EW=1LOEoc{kKbN!xtv|LV z!MRDbCTL&LFG>OX{<7AhR!U4nprNeKt(cGTgFA4~8mr?IvVIW0hqJZNFXRHXkzK3@ zZPVfNccwrbk2Pskb|~*hpY^HFoVX{Bc)sf0=@jR$Ul{#9U5&WGyc$U1P71ts2CP)m zfZHRIgFUU6SH~LTSo*tP9hsu=6Kq}{%{(GJT_^aVR_ED~PjnLrX9Z#7{f3xm|EmdY zu^KNw7#wF_92iH5cH~F@{{3c)3Qn|`zzB(x8w+C6)4T$ZVJf4aFuSJ{EXS?quKEHr#W{Vhz(0vyWlxo~o2w^kDp z%CNSds76AsNn~9HJpjmzXy3Ykn zY_n!3DXil1=4XXYhFY7}T|caL;Ck(_+z}h2X#U4rCXM42z2=+&&jYonvI-K1PTgvP z+}sNG{E@$Qrg?o2?ZdGD@o0;aFi zl0X#8-wi>3GYTy#=kvcXr1@|5=)eE|4;B%Q2&<>nDO`~vC#QpK``8x62dUXDC&Td8 z;C42MX^-vDM+M{hxKA*ql56q8n~uc&Bng(>)Y|i@{rZH(t!r9iPFhVsbyI9RdS##G zdt<_2I$MF%NzI2wyO{pXeL*A5y<4X0RS)DHb1J!WPSj{HzVLavC%vpFRcxEc<`=Vl zZt*#|@!)ltW5Zu7Hi39bpni{9u(zRasdCh~Vlq=Pp3ai^jTSRL|z*~FE@A?dQ zqGF}@jiY3^C>!73S<~GvZCnzrrE=!MM(N&n-?4$n>Vu~vZ@tr~{(=97MXSKORb~Kh%?*!bzw}^Do={WSr_#X<4+e&7cD_{qWwvEt zZesRJtf%|&PXX8FJzB*7wo`C^7u9aDm|ztD|A8gqB zAdLG4qBIdEtkBkmu;&~=LL|wbr~pW#u^p$}0+;rq}R-v2CJrm zuX|^L@{`Exd3erhq?O*kbd~G6#ZKOs8DJ0O0j9f1h`55Se$)eBKpiEoAjg?%@5~80 zk)hgo6<0N~fZ1@tHB6@vW8PnOZoKz~=9x5e)fXz)yHZ>cB)=jAn#ch87fe ze+IFICOWdXxj~qa>p$)gfBW~M+9?)O4&0;?c}Fq5dJL2bQU$%!2dkY^zyjtF8yHvT z9I}HbmB5H*P5zyXnJq}_BFh8gR2u$tsEl2UvDJLCSH|6Eg%|5&(K@&B_u}Jer<^+< z6CW_x4j+N#ldlfw!0YlRf(bR+zlavu5UWG9xDgEw?D|O<1XlWX2v+IoLnXW~lIQLK z8{Aeiz)NlrjR|j8Nw{oi!n^Agh!l zJfnjQt3>#g^L}ZV{}@7Dz^wh<*0Eh&;X{PMJP@M#1*%lh1IhK)gDRsGLCO0=z%72Vnhu*CYQoQLfw_XiV0EIlUI6e4ccHRt2wGe!HP7T@N z0!pL0CWuBYAi}pd6XfZe-2m@)?o5(c#V#xX5c(-4ig1Vr6<2e2f|2O&GIfB^-rgsL6`V{uh-4Iar$JJyIB z5VVR2ahNknfDL3&f$!LQ=4}X3C+?cxq;UsS1g6kJB!C%h4?1?~UQOhz=SdC`s4o2c zvA{xasRPF-qXJ)@)EG$d5Qu))xq_c{-vcqekzwWIY&w4?p0GSP8Dy#q1ozk0kH?3% zQk!I<%3=c3Jfi_o2l^@K3KhwJ4E#TaWTrV~PPqx2b%nSGKci+gn=o-v79XU>d{jx9 z1U`k!{#3?QwP@I6^&EV!CI-0fZjxDI9Mu{>Zh-^SK#+vSKZOHQ(8c&eH0PBX5QQW# z>eKj%-;Zr2vGX7H(V1rnh{O}}tny2e;#!sx>X-^{c%2BXG*K5&rr;dGr~n`F^?`|$ zx5YEF{$BCTztF=xEEwJbSn6wCFCIPeS+Nn<5A#{T2Tp}73z*!5CQ?~q7NASi%C_Hw zBZI+_VFNW(p!pgJYe_0sWvPNZL}nLvLic*MhJ4mF45={rBYxlp(~ru`WPy%o4Ps9xOays3_=g#Ksv|mz;3h_5+(w;sv9W~>0#Jc(F$oc$L6sBE_=f#yM+X-$nH9!=ofLDA?OogjK6>kEeU)vuMV1pZ|KyL;&qmbH{>lHIQEXCpDu$5E2H}40o zp*CHDD&KH~t(lQqgok$bi{rcV(f@s;EHOloc@ZYgvDc;f>80*l!v11Npbdyw6l7t2Yj=>=FmIOQnD`b)kLJ~NuIj2ee-eIu~_~o&J7}t~q z%w8<)>yAibd`Fc+PNl@~B)!D{K7fJlm9?=j9^vv|{*22Brh7$9{k54$G7(sf_y z6^!D*Bk1vAKtCbCHmhZ_D(V3jF4LjTswRPcM&g;4fd>vU+^*c9=@W4C#c7@n_>bQ< zp5ZpHjC5bMHN@W^x}{Bu1m8NMibOGvV~2MgoXvEZ_We9swuvz&Q2)rozT;f_`Ka~D zdy9%oeVx1F%JLKazbfd31&|wjb=R!}w;+c7Syl(}M5AUonrws8#Am;$K5{_E(_xub z$8-6|2p?8QnNGaY)vuT_p2RGFP)h|4M-?if(AlTcam-nN`r3T?A2|2Xt-pere^bC>Q2*w^~#AWA(eR0ly%-tJFsBX^Z)Uatx^k$2&seLhFqr zPdkd-BdNj+UQ#U7XAvQ0g15YHX zMTCytXubYN@6FMaJ%2&Za;yFo3d{T6#N)3_EW+LT*FtU#XzefV$O@Cwppm0VO#nUl zXZ}e}85ighX!pc{PicPd@W~tJV0To=g}tq4BoC|{^YDAtT8IKR z%hPRb;Z@&~)E6)z;TZ^(dYT%CV^Z-R)L2Myb=QyJhT0S~SIp*or}YyX?VCm0rFpI# zx|Zfvc29OiQksX~pyS%xk;2kW3c`jqiq7)W@8 zF)cLK@1aYUGy0eV6j(MTKMaR_EOCrkg&skDt#?6U!E4?G? z&=@*%FAi4G837w2Pct)T_!v;?V~cui@|IOUgKOtjSY<1}Q*HfVT=aFMZk>yc_o-$mrtJ&f4?*Lhv`XTOUk zPHHhB(Ef~2|B5o|9b4GwM&CnNVOokwsaR$DofgX4`nJ{YTSaE%`djJB!$FEQp-(lp zyfevc@I)V|Xvm28J-CIEfIc5+LBqj^3Cw-ALWt;#5|3x7T(I@p2g}+I7wU)~mHV+} z5ZkVudo5H!N^C3nlg;k)-#V|Tr8utpC&%wf7TkQ~7T@77UY&6lx5)R-UfTb-X$0Z@ zc0QYSz)Cz#yuGMo8NFs8M}U`gl@Ioi5~`oKAc?aqDb}>*pTub0G&rtk&|>;?XfA*;5_ROPyFGDMLZW{G6GRecLeU6s&nUj~0e`dB zq_fQKaP4Pjw2oivXl)g!I20{#($2DP7e?c0*jB%Ip5j5`3|o;z!{xV)7&0B}7#1V# zX%-Jp-+t~^e2aSV!V~9mk+zbkI-R=#=hWGsXhP{O@hYb;Q|+H>T;s)OfpoHeGkyG@ zeixNlWHBKU{uQ$Hhc?v(E9x;8s=^vUE(r~kA)tz?=>TLzUjnkRVRV9()%Q_Gb&lrD zBBk@)Gqq8$X}aTae%Cg+j?a#o@iQ)Op)9r0de$(u{UW$_IE%g0xJtcDp~u?E?$vCV zduha%j@GqP9`0YSpv_otG+3)6Iy5oZt(ey|CR;YmT(bvB9e0^YxNZ|<>cmQG66gdQ z>Bhh7l4f6zfRe&1lb4>K--_PdnSF^OQ6) zY(-i-Y+t*IP%D$9an$&LcbE4GajjSO$Dj6ykKKHD*Y(Cp@A-4F?Q0BIK@FZF_kXGQ zS!T^#ExjHvH9e2NZDjYX|9Y&z-zoD~W#!-f|DuVHT1@DMKd}D4AciXCYe#_1V`^7_ z82$l((GB#*ck_e8+u`AIOn60dFFv*aE9#OVd%KTq|9zPNZO)Xe8v1f;+w9mcvAuB> zCv8`Xg_b%@1C5dYEV83S8r+Cl`O_mNDd1u+PP}Wopw`Rwj?N>F?~BrBb)Nf?f2E*C zmG2sGQGAVG!6_{wB6eNh8;n#N&w4R`OW9Svq_COkEl-P*U(FFWICs5EqRH|UTrD(H z!)@l;q7t4I?_T^CvLY~d<6(nN8oAKX16L3;T7Tj~PeSzak&@;zHPxTaE;aj#J|+-X zT)HhE5TP%t8Q@#`I(7|Zi<&0x0oeZ%V3n*~)A%z;p7zDEk#K?@+&XPHS___9t+W(hxV^tpQ6*A7mPk&l$RC zPd^GYlRP#ov)8!p-rdddrK_I4zNVX}v2ISYk|fHcgc0>lFep48w@Wr(&8)cnN?Ka? z(VN0&5~9F4wo`iJRSegPLl{hN$<1BcqUJuks~_d@6i_ZSja{eo+Xeo&(>KC`dMFJKs7e@6{ki&{)8Dnx`KsxbC>ok8Uo=T^D_7-?43kiu&9yghi|!YsBa4Vp3)j+O4K{&!qeV%w0q zT3Z#>;d=ryAr3J+v|E#UvW+szHzapl$K84?7p?kDsrwKJvS=yE{^SIBG$}>-@-+ez z$vYSyD{}9(7#~09z9Yn9sLf33c0;CMP-?7l=jZ@_8jhB4tq%;=cT72AkrhHVXY)OD?67h*zuDll{f zq2#A_C8RifbNFd7asrqOx=7y$1**C3y*eE}?v)V#*$FinQQn!$?(y#5BR*}u-rW@^ zBDa3!P0`|$*QuZD`hVF*wpll`CCS^v8y=l@1~i)MoV(Kbw;djfzjkF9N} zF)8SG;OZMGRmfhzxImXW9@g%ah9r6>G6y^_q^y6EbK@6n88x~}D9>DWP2}k_q+3uW z@Dcu4PtR{I;zlJ!Er9pY@eWE14W>+q$C?de=16v^8D79t3vQT(;J1vi1iF~CK^ua7 ze#Gmx#K$8i9ysmh-c&YHd_uJ5V$aF6(FudZ?Y-1y{Io6D>x)ShP+?nDC*jI55jZX* zn8Zrrdz1#pqMduc1eV^$nY{T@)+BP%P*~+uPa=l(Zg7XV3Mr+LuqmNab9x&$j#-ER zdue1BegTsJjQ`cA(0BMryMHVDyeE7zb;X1`vY9Xf9GANYl~mZI87TMjIA%Na5<9;F z;sf-U0e$J4wk_#O~2;EJ2mKt+K<|BDPjNrPCoP_%iQttg(J%2(UaO3O}nU~1C#LY{K#?qp(v^G!vargSXv;{6{8Yq5|YZ~t1 zDSf7^KPClZkGgHO3;g-m5&dDI8};o-b(xjQqr>5n zmczogKZ1;4gcv!T7-A~;Uj7sry}TX2=jT~`OA)|gYFzu!+ELZCRaj-55x&zeZSYBk zP{vm=JT2E~c$)}T6IyoLq(tPiCzKt7NG?bB6W9Qb*S?uaL+_oSG{z$1K)bdYDk?GH zWUHG#-!#S3z7a24w*L7Wy#)jq$Xh9RG31gZ+Bs&g}Hhq5vzYyP`@B@3|GfYkd74xarmt z_+oWOqea_Bwo{*MNA$2jzh=W$Vnrt;;{G}}Og5hJ0EI_s$1a9UtqfeZ(U1U}Apjs` zCaSrB!FW^Q#tc*(&jl_v<_0~gPypAJJKITdeE?WMeg=LH<3WJAy+MYaCjiuqt$-#m zO;jcyaB*LCu-k?4r!vD~EE*>7fMPl!)>C{BHxoCH*+@aSfFG*Q?#q6&l@vsY#0@Ss z=DW3k3ENCZJQF|%`2V2;U(Oxc#}7IqfC~ezAg)IcL$qiCBfr$GC2B44){-Pr;iUy) zX*pfmeE$F46XSxpEB5+t6xD{hUcSOnZXr1tFJI&}Ye!wXD`Njyeap4(wb6PHsB2F6 zP3{Zz>{?YdV~h&#K@chFVz0#MGGLa4Si^Im3`f&io~Tk*>>J+aqj9VUkcy7?h_2Z*D*ZPxXfC)NH0Q z58^s-mdQ2~&)gz=T9eN|HhlZ$yW7^QA+AargzpG%J5IUZ`Cf~0CPfvaKZn3R4DaPV zF<}L^(GL{toeio>LZ2tQJ;QYRxC?EQoi)Rq<3BN4t6BPF&Ay!1VOzMb8QEiFiMDC5 zPIX93e3{+(+w#Y5cx@W}@$tR|@0*g1*WV4ie5igh^ocIeRllI@qkc)1BVHfMbrA*l z6x)orsg)#(cz!;E*!I|pJ1JQ2*wf;&yz^*{NQZrp7t?hAjV6wo= zD!ay(4KEXSEZZQUY{;WX?Xy4h-7P!=9-DteWwhvsc!s?BRLYhk+TN>f_20T%EV8%G zt%VVOXW)#{&GpHeLX$gF)W371j^`eQ&+h+GEHj*pG{`vXR2OP33QE7=amIUbE zmXRfSSdxb&d03K%CDma`U;6(E+y|l-uEMFkCP6*?QNW5$rt<&#Q3S`at%vE% z=pzbe0h*+>Sk@cR-q;9~YJ#FeFktSFk! zX*_GZwb%BtBQZp#dQ8Yu_*Gw3H~+OA8?AiP5>syHsdkoD-aegN7r$XyW7;+l3*f>e z)cSPLP=U8@9g5jmUIazRwNvk^(HOvH+px%Cx#6?kXCI2#dVE2>mRm_yJvr})XZQ=8IUYgO zX!UxtVMu4P{d@Wd`xj5cQg_{xk5}Ee23%iX{h=v@r)pCD>(D)%v}pkr(S5| z=<0)PU*Z{VV`2y17U4-F#-o@0f1k7(@&NoT3NpnpN5~zUV#C2}-m}+C(FAat+qpD4 z(~8nz;e(L*YrBIR?q;w+Wc(q1Wju-dZ( zazC5k(%$oFyUK4dfm=>ubAE5!&+KKhdfHKxJFVTvec#QPtN04Uzk_#-3qzGFpBta- zwjJeSA0S>}P2XE{D^w4$DKUw!44*xfus6DUH`jNJ?9}%_N)g_b0ILm*IIx90qMN4= zz+!eNPLT)2gWaco(Q6avFr2yiFs0 z#WU^djp)^L{4qLwSxhz~^%?ohizc;wdyObp+;_D@PovgmT*9h;6Sou_Hz${U-qUP& v>;2jUe|>u7wGH}_vw^)YT&`gBMimH{t7-0vWv-}+W?Eqaw`v4-q5XdWXF1~= diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/114.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/114.png deleted file mode 100644 index fcc4ba755e284824118d76cdde091c562a9bc054..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6106 zcmdT|dpuNI-(T+JGVVf5MJP$-mTnWu<&;AzaywJHxr9)mG+W6fDoF^{5K;*zxkP0j zA(c7^$tW{!soZ9yv6-2@pQY#hoagg&dOn}$egAuZvuDkkwbx#2eShowyR1#TBUnKG z7h4BgAS48UJ^TQo4{X~K;dc-KM@O&>03Zp3;{AXqtidio1AYMz$q@n~@KL_MSBmMvik{}d>iU=(R!U{qn3PMB!Aj3Jugucu7$6z5OEFvlaI5h4ox5cZ$}1|X9#+>pt#4?2*7W?v%Qx*Eoo~C| zbu;?<2L^{e4uAUmg){ke>Kk_&o0*-%1@-w)Sg`yjvLA3MKwQG2q9URabGU?r!{>l2 zh>9&*C9Y_-U1D#D(&E*pC6%pHa_`qk>6o~%ReVC*=BetM_ATMeLHmyEzXNvW|Ag!> zVE@9^18g9%=0q4?BErIOt3;rPLI_aA#Kpdg`2UE+cafYEslSAPgM1%BSXdNZQsQFb zf8G19E5rv#OG$zOWJH7@nM4!-1qkf0b^XcMiukeWkb&eRW|`B4jq?0;SmdpUu7=;# z+e~GhtZ(#x>FwFnv7|kwd3nf7ZHH|e_g%8i@Y$tfeY>`q05Y!$AYn{^Hh)Zq0B47o z1kmZ~K-tz4Gp0;Uq}u~aBEX#`L$sFGT`eCcaP@3lj!J-=ooqV-NHwFlUEWXHNGauAWjJ5Y(h3TY!!gYEc|@=hQ7V59NnD+Fc`B$@Fcf7GGBiM0cNyoLj@(rNZ6WiE?JQM*puhMcf(TH`x<*S*eX{$ zk8Mz7)^{1q#@-8E+#Fb)mn6L9u%fQ*v*VXSsBbUOtdP1W0u0F^<@$oeFamt~#YFqjBB3&doCRMX~mE^Ky;=;9AHx@C=^Se7v0i zG42#BYpOJ!Il0Ie;gTvLX|{Tw60Rm%@hb=`mO1HHYfgZ&3XU29T(-Ux&_BPXtwidi z(ZTkpKmr^H=r_jmGtx2%Al4=*p#+oO5rFHWy+Lq}PZs3KJe|N55w*nyqrFPMpOY`1 zI+witdB)fxP8GKRdr*YGp_Z4RVf{>1@1CoS zzOKWrZ;zy>EjJz6wZte;;oMSDD$RO-T0!Ow+sViZ%@4=%uZfWvSH#$9>+1op*5#m=96em14TXO{916TxcX`g87=5RDbaK zUa_*b<)yM7A4XKaV#!NS`xfh*TQJJao& zF-GGaTj*r!wil~k9ZVJ7pE@zwj`XRqn*<_8l!*p}?nyOThb008ORS^$~nh+LlFFUIE+Am>1#081voxjb9}kwIElx8r=c+J;u` z*W{5c1R#kB&LE$vFcAXG^M(%MvM&MRPvdozDNnu-0b*BV5a5Lwz7=IBVf*mY>GbJ- z3a*!oZQ-vXz@}y=9*0Z-rA;SIkzzIedJDT9^-kVNAspVa*b zK!XmeKVTM47qi>6*w7KGIuE=0`R(sL8Q89ppvkD+1F=IMEw}pF_0QY}I1X{A9MlGGeKd?NMOhN~(TL~~qhE%?p zUNb9-5N-c~_rp~xSRDaGq4B0Va;gMrr3vWl<=@;!1tNzy97z8Vxa82$inpks0AXdV z-KlZ>Zi~^|DmK?d>&XS~r1!X~o==LqchHNI=7s?q!_+HNcDL~N_*XI-^RsTnEP2eJ zJ-GEcWN+A^Q!ODBSk zcBUM5@pf{l?(pdCcvWOr9~87c|MC{sBabR=CASC^bG$}zKcruUg6VY$WKal+jX-C! zcsl{)?+~EQ9Q|})j?*z1I-`gd+%=Fpj+mmHd33%~03Jt8AOJ>z;Vi{~00}z@z{;jh zt)3%&9iPfSM}5<&EU}J~Mg7UTx%5$4xN;l8B?{{rT??9nuTwMwO$z?@!Op6hJ$iqH zaV@;^>6`J7Z^muy-}aijM>VgB4#XI{RV+nm^=+FhSN?=(wA!vgHNIL4?mW!1M&1=t z@QZP$|6B6C`6>CnpsbrrwmSj3<8i4Bj($B^nzl9~tdY^1+y7+suxoj0eY$J7bG~Y7 zQ_#rL>L(RPh6Y`Pi)4;1WNEwMk}x>wWe)`N45!q?9*y*t+^Bu$s3cUPfO@8y<67po5nrq!A1+lsoS~bgX>z7D<{+1Z1vu^~* zmF|bDnL=?-w>EN_9e=?to+{prl%w$e0VIw_;&8M3NgD6HJ$JwVG8;q}J0I+~FYLDc z!OKR!_$kjX_FD1$ea13Lk>wN>Xz@xEMmU~?#-i-2C=6`Hr!M?+mY7hQCH@SloT#TW z9z8=;r$~x^xcDQqCe^X5wPf@beJpF&_%-3zM*L5Y9DBcB84mua{DxAzGSuWZH%`xh zL@WUg(a&g`V1j{!pb}H6h_6R-!ZFEv``m-_E830nYcls-$W$5f9oUoTZFN}K&g0vb z`QU`x8CoY@JQ(q1c8B%5jZWYCqVQR5<9c?aWEQv5r#M$A@@1e6USQ_$)m41h4!2a!wE;5rL}C`O#}7THQT7n zK59_43BB-8vge2aST7ve^b+N$9O5hO!(*9qX6n=gqsy`9&@`P87Skz_S~WeESxoEP zD34X;W41l6Z%m6#c;;U>J?z-;G}@6CKGWuJem6YR;n|`LPjoL68 z+D;Q7w>q6XkuNbj!`&fMUJsf~Et3T+gM&Iaegq z6jy~#Zyys{b$&1=`WV8hMJD4Z3?;k~Mxd#qW*jxXVmK~o%rU{GUhuq}c-tb{`%5wg z)^Da`Bb7An7H~?(+VH7HPwckF+e3 zJ!VpIvKus~E0nJxbP~#r3?M*btIiaj$bU*^dn9AJ_Xltdi@ph*R7}BYA|OlOz&%>h z;>57%8^%eaaO-M-VC_wGwH;qI2A48LJ`U7s!IatR00Eko4&c)3;PBZSD83W%RLmLX zN}QKKU{b0j$i*!%8}1=YdVtC>>=(5N*HDX|GO}sVIP0n)RdS=tvFc=Ix@32Xhpqi{ z=kasbf)057jx`{42N70kAs)1@uLW0jrSL-^QWBZ$oxzw6+C+-&U4j|H@r?UkWf`A0 zE&8D3Y}N0$A>8AO{D$)8>ViLB+DERJn-U7grLMzmi4~-ytXmB4@mMzdJ+`B1cDeA# zPIg=+`-1g4AL~KiW}Kw`80JlCHrF*2<| zO?QiF!(dCf0O{P?b%9Pr?w6YKiU-@jaL;CZ{xzk@?Hv0yfGBpK!$egW4-xbA}L z;WBnpk^fni41hc*c> zsz4nV_V~};W!b%$Zr6bt%0Omn>N~90L}i8&oRLW>!Jwm=b8(1 zTqF^l3EXsYGjffZsXv`OZ1=sSej2B+-jgsL1c_A}#y=7OhNVeHHC+>_t3}zH(KuYT z6JLZi^_?2Jk&@HxwQp>Pn{d~y7#Wyva*iD1;2OPbQl|Q zDYIKBbN)%aNS(^{f|2|FSw>X}zrAOyD5&%`QhJp(pypH~y_0NO_B{8E?tDMpJsZE? zKuVdIZaiNz<4!~iPlgo}AZSC>wd7eEzHyf3=8*JWeRk3R)P}wO1#KC833i7wy zTc-5RY%!aMZ#E*~bzJ7`(ty$un|~*&l|Iq=pNKiw!`%&onm-lOOQSa+-PC8yGSV|+ zt@qu0I}YuBU@2y2rxBNYd-sUPJw|Lkw@UvYt8H6OwA`DOy#1sF2+SGlx%fJyEs+M( zdd!i8uQ*d3glz6``}zQ$etNJm0@yC)*+AcA3SAwgkB4ZXpBgE?2wyLr0BV~;O=k6O zqSxsdjoB1DyUK|H&pT3(HH~dQd0Zrg-e#u4hlc;-q{)AKzIDNQ{9F+B9(+v*S)#gX!DsHJ8;yfw94NT~HvJ?9WkuHy9I z=XY@7DxdlyoTJE(Q$`OSWbzfma7lIcAui}-VPl+HyuIJ8@oL6_i@99am|t4=>B!VL z=C>FcHN>1VzGYCA>5z5dI%T_ z{u|t`XLPnRiY>JmbLU0kjdXc*A2klWZ`|al(?C`<9csO2eC)bZpeZl1q%c7$s6?Gx zuei@hr%yR_G%3UR7bAbfo%-Y9jBP|7I8@gn_F1)7jzdnIx@xASr(9N(JZ50-D4a0p zWloaG4;A{Rl1Bj9C2VOtURIv~!tkGl6oq&zsOrR5MR~QEc|Os!<+$GW^YdGOsZP|Q zOQA{Gw{g3I%G0~I=U#9g5jzM~%*B}SjFVmseTOC;xAbyLLlxhVi*9M#|DSa`PFsMraT3|{OZe{hUKU5& zFyF6SIIu8gNz{b*#918UQ?BMb3_=`gY)|L<&F(6WjlCjoCF-ysq)FrM;$_iy??Uyh h1gv?j_^WngxYwA+LSh}E=hwcEJVamKbG9P7{{bpEF5UnD diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/120.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/120.png deleted file mode 100644 index 35ec349398ba0ce1aa37b596b711a028dfbc0b24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5304 zcmd5=dpJ~G+h1dxg)mf9Qz8mU6d{`=QIbk3)zl-0ND?{ZuoZGP4@IZ9MoBs$X;Pv| zl%$eFaw?}nB6}RBVfO6(Et6eJuiyP!_ZqU9>;yCI z*W0ZJ6bb+v;0uslV3SSAArAl?9KcclfFz*A9Ri}T1-k%M_yK^LMFCX!OPRWsCGzLp zkSyxI+EX3LAz-q_%P-I`z{~IG0>fp?fyr7s2hk~S(0;Wwe>G<|&P#4#g8QQ1qNL6b z^4CU^&wzp?2mv8fiaHQcpimVkWF?@(ImIZy?AI{(MG>Kjiit}|N=eJW4(ypgghHi? zh*HJGrlz4p!23W{K}>O>ft9$D(*X%}f7*(ZX;&o|tt~89-dcxg82)zbl$5l}tl4u^ zH5Y4XFIj4|a+R^kYE$cVHtTIS*x7H}?!3cg=dRsu2i-k9y$*Q?1Rf6x4hcOG9TR&x zED#Cx2pc3Kxv$AF$x}AISa7x0JY;_@DRw z;|BQ&(o&La1v97=NG7TRK!DJM@5FT>WQqhn-#Ighg+!$>M>JdpYpB>~!j*_IiqNXO z7$F#tnI9d(JAG((y;^+Bjn;Xo+xqm_BEEWq2$oHP&i;Z)I_inwD&G-2nz4xl<>Kg< zRzeqT@!`jjAorFJZ}Mc(#7z>!(n%m;gT5CCR>N_{yGZaw73s2tBkCAq7Q{Q0uh)c5 zh;wpI^p<+|I7)<@V3|2RB#7V5&Wyh5)l|RFTQxJ=e_mcrUa9?VyWA@tJG)b8ith%M zEvtcj_ z5->31v(h<%H;tSglkbUSPG>YS;)|*vgKXTI1PzTDf(S-Df?2U9R&kQ(yxqh?5_}u5 zpFBfIpM=jf8}q}Jba>vEdO8xr4Q_|r@9q6&^l8=0>iKHcp~y@UczLfkKJ~o)A+2VC zXVAihiH9WZ#cKnFg(zIamJXgAUo(t5%FFneT^G2~Q-yagYqWr^(?XqnI8fkh;r_Pq z$4KdZ za&2wxQEpa5iZ@Wud9B*(n1*=Qwzlz-g^wZ)+QUPHH^)gZR2EwL>TaHIT~_^A*e=P% zH5(szqUs8VC#_ z=9xoJ)rwVWG!92%#QR4a>(mu7o~x;o`JV>SMT9b8wqu_*_eO@-#h|3_HNl(N$wiuF zsRcK8Tjo@I>d2N=`rq=cz9bZfWMWp{I z*Os8E7L!1;ZW>n-MM!f%a3KzJ@JTaai|gPaBP>ob4hbmu@Dm#=hgD+J`H2bgASP>}ww|_R~*r{-Ljx*&8g??cgvg zyvXoX^A^xv1t{;Oh51>hBUPcA*l@n;q4#D9ljkEOXxwKntS@-p!PhRva|(DOBq)rA zz$cTc8713QE8NJsX^FbJn*@>}j@|OHEA>S}er_yk8e8Mh>Z)hGxp6`2K>d%_<4bgv z%9lPk_Cj4TKQ(8Uo#;#-E|ZWnB*Eg3TsNYIQHcoG*fEM5Wm)BPT=@GO5J-un5&<5ed}hKG#pyuZ`RuwIQGRzl>PPokoFNb-37$Ky9I~GbvCeN zEw*2gI%|Gxy|vDgACjGkBsiFLG=Nd#i7WpkWCt+L5Iwkn_&~pxQg-h2Q?1WM9Rq|~ zkn3K0PG!M&4WB&LOk;fdUj|%X_{7EqLT4WnCw$ogO1g9>$kRV+fZURn-FKbeF zK2)0W%}~Xvm4!WZmSx#UG?O2z#A_kJwen5|P8;ChX}0*<1Oe29y8W0wA$^=dY$zC> zpGa*r+7lapTL}0 z{CNclo(JrO%-Kx>O9p=>9wru3557vSC`HoLC=$QWNpD5akS*Cv1EtEO9<^J}tmy&*x0T10Ix`tq`kh_v>#C{-7&)yM@H{Z)V5G))AS2JM7LW}-GIUsPu7_ZY9` zf5cz}B#;5dd_oQuPmxlQ?;`%^J$pI~PMNYk!q#y6?_xl}Y2ogXZfN zwEMf?s;(QnoVbba&Zy_P8YH>F<9+Kp_SHeS=#B*Ldz zhw&{ighOyuEKx6gzLxP|UgvN!QWS1Fx3!nk6Nbpu3OSx@#_zW}-tcxwtx+!fkey&& zv*DSaF=eA{?gHl5PPDw1jyXG#U>Rpdt+3`|E(!GS{A?iP!uiJB+#bI{D{QoaMKxFM zmY?SxgeZ;)zF(rt9V#}>XyZQUeC>tb z1Z%>fqZI16A9{CV#zrY%%MrBuW8`%;o{vP z8exsTwLm^(TLtUEyR^b=H%hs&31o4+7Xb$HYs6PNgHa7<`Rle6CYB z2~KX?6$#nlba}^UWu&C^@89;BUy5kTy*~6j{y3*hZ|KTuS8hqKB2vHOpvR*mK|0nE zkFV*W%Qc5^o*VO8^UvIFvk7$%^IYoa8{lUurG9aan{K+T`tpOvHFs@T@VgKG0!HzC z2WB_i3OB*fp3{%8ZN@Q?v%eY69%re3vVOW~>8klvwh8x&iz`?L%*mBEMc)i-Z#;z7 zF?ps!CLvBdrx(@6k-$T~rNNQ487q$4%FsNLuTk>oVpxKz&!vQ%v+4DEmpmv`yPT9u0%(up*yKRaKvEW#?zRKR$!w(IY6$k0+T^I>s zF4Z&b7YVcXdCTccXJLMEOw^~%!HkzEe5|LB)3Jx1c4Hkg3^Hq6xpS#AG@mRw*m~rW zvCWpPysyUeF|R~(c=i~6so!kKGZtn&H4ic4UE;T3eKuSq6{o7%T=kxenSc1_#WeI-)py zD*PWn-N}Y>l&FL&lmHWYI8p?Xbq0zj*iJK6J8K#J-)GgHW~>cOtU{nf$U$+)F5|=E zF$#*)h~{6%UWI`j)PFQ#I(5ZH24I|=3R&S6NR6^Br2ciM`yb}!9K>7iXGfBDcIH&__HB7 zV8sg&8<6T~kbA6}o=Fv5AL=p*A2~LM8%88t$$^29HH4L~&WL~q4_oQqucD8k_Ff-m LJfg{MB3u3m6Ypt^ diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/180.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/180.png deleted file mode 100644 index 2d18c8855b12c1f473e2c9951e7b7fa2fa15c20d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8944 zcmeHsdpK0>*Z+1tmYnj?VG1FVN+`wXksL}xg`}DajfhDnD%lPta!QiuFrp%o%K4B< zNO+tpIgTNxa+s0EX7=p;-t@hm@B2P^uj}o--rs+}+w48t?AiCd*1FectcXN^i{(U|BMSK+6WaCCXOFiR8J+(r2?K)`FpO)5wv$KIQcfD%va?yun5pB`X zE5YFau{$HkpZ@q8NLm!ifpP@+lp%g;J^^VyyaFPCHHG-*#@u7z#K$inC?qT*Dkd%g zzL2{J;^z|(;1?7S5}I9x?=rX#2}%petX#iMSl0Hah_bibhQOGIqAGvBER)~U#8TBg zb~;E*e6hkGib|{0)HPOXZq(B^*tFTubi3IOa|=tWz5DF!9rimOaCUJ$PH{Woe&+1C z^FF>8E(Tw@8geZ(?E1~vTXDDJ?<6E9r#yQ6BsJ}6`m5}m+`RmP!lJk36_xL*-dEQ& zw|w~csr7SPJF~a%OaH*&(C}CG#P`V`oGD~_W>zm?&!1|6^H0tGp_eq!i(gPsKu}~> zFFt;sS;eIVg;uT?mf2=2a@1Q^c|)M6+@CQIUzUlf=iH zXNm>=uQdBxv488;0ht2FnjQS$CBV-Qwn_jDL7)N{Lc&6GL-=1qWNwJg4za%n4knqK zfuCOxyu^fsg#W(xKfb`bfLlu9?U1AZA8;lCX^04M`#3Zl3J%#S#@&Fm`0MfFU>AB@ zp}%UIuZT@S^AFFxqxq*gJKfLUy!?Py_xed>`az4BgBd1M@f(B-5)Mv4K5P;a*3ySV zJjLw{L@^VGzON!5#Gx`eB!y^1qjDsHKaeLq(yfG!QngJD2VEW*p1y1i+by}=FaW;` zdLDz4;ELTe0uBw;sNv9?Iw}q&XcBwXNzpho=_(8N9M8w0zvAGNTjrjKEFJt#>@zuZ zo)|X%%fja9_F*;-VG+qxRPqxJP2bEKy+$oW8<3{_PK<)ONj$G&8{v?&p&yJxDH?Zi zNI;K{xuoGx=zU`%4mI7f#-YGO1`cho_zTaI45JuoQ)b44n_A~ z`b93=U+tB_J^E8f&z~0J|Mv3#avSY*;GQ9R*a20oOKe$=zLI~$*o)3u?{xRYWw-a{ z*P55IDs!`~rYpRwH;66y(y1H6%lnrgs4sBl0BI5`-O~4*S?m>gvfQUGMCs~NLCU5T z0o%Tcz8?s%K5sP06hJb12j3FJP%mDnpR;okx{2K#>DsHUL;5f|P^91Vb=|YtrJ~tM zxuGi$wnseMFdcYmUB(cZ6^wNNx3opK;82h{4t+_Sp29?Pz%~Y1`PT}h4(-@`i8(lP&HH7*T9h5g62|Mig z$Teys#dYrl4xw^7n7B1}BLJTsCbx|(+xISMN-6J!OK^Gjk`|6-hKIp=jkUL5WNYky zDEXv!V9yPcXQj>|z8`U@p2C>|I)q@&IK26EDWH(K=C6sJt8X21#?hca{ zGF9t&@^!kAW;|oJ@XM=;8>XQqdm?Lpf)!Da;+5Cf67L-QDuKn6MJWWNkfHvA{KXKtn?^kAh z<9oZ<8#UW_uR)m0!J*^PGs^HHc$^c6L$!vOvPM$89PEWd?8UT^CLG#M>XqVQjl^k+ zIck>5-3T|yBEdKW=SX3Wt!!yCzW33i3{1I&mq;86L$~1&ybMt#JA0{!IWi-wrqd0WL3Fd7&&^lt|zZEV68L)pLZ5e$;0;Jqj zi35CIJ_ME-eR_M=$S@8aTNsH$kG_$g{FsixAwiJXZdqaVG|q}4#?0>A8LEj<1K#}m zV3^2?A_$a9W6iL*9zBq@e5;XbZ8e zo?eb}ai}Mg$THQ5AhJkK86X*h#C?EUMHdVznNsk2;^|zz^0ec*b-@Pq;w8_M12&q- z7n@0N65yuWHv+ba(B=5!qS>#bECwQvUky$PsHr(Es21V2Q;DtRmoDGv*!6s8oj0@Bs>*ZS$>I}rdEGXowssL)5b&y;o;7xL z4!3&m^gQS6t^N38ZUt46Gqq^FWzqvh{%X0VO!cuaN%IMZnSEI^{RCEg$6JH5P z0-J4sLt>GpK=sx!5bBj;*dKgNIa$a{t{&sXem`$!hK{VGf*;?xq=R?2CseUoRDtRl0^lOR|2% z)Y{%oI^2juYU97&PQjM(KrmYb=#wvU7l*FAdx%5tzVTd6e{fBca)V=i=93w~cOigG zP$=9~;%ALRZ=G#$sP7#1YP0)>4f(8)1=zg?!)4ovL*2%J95UCZ!R#U&a$K6L z=q}Js_|i4+v}fURI>@YqNh(1WX59g~;6JT&kA&#!^rXIH1d?9>^h-(l`LgU^_gLF= zdv~Y6p3c2X#B#nRWD?AqdY!IZKWFcjXz|FTFX(-FdLkB;R&C^#6!P{rGR)#Z@A=R} zPM8mBlLDQ1%kN`qjx->EjR=8Y8KmF`!cEYXOyHoT;P36evVAV+GnBq9SOQZ#g1YoO zIy}6$Kkjf3lyhjxIN0%|DXe#!7ko)0>BMJDh~5*%U|9p+4wq38EyC5U7uey&EtA(1HO5UbkHS${P7~1$F%F?DlGpOq= zNXy&I{260M;o}J1IH8j9t zGyU(j4w$3TRLoY3YY2Z1{>isqLsLZj!|&S;bAo%4i7fA|NcbqV!=Z}s$~c_0qxRh* zU!${mhYnNas-Nh+`EYhfC(6^oyjZM1Ap-t(_moX1#SN*&l>258u%q<}aLqFSpSmBX zbHOfjB3kU03P7YBXc7MEtmyJ_1&_o-?Y`beN}EfJGRW;U&W+FF(Bg0>$77c><2;|A zdh@j3f)Gn)HPCXhcrvra5K|=!o>(B9u37a1B!6jd6o4khqXlLaf)x|KzNFxg2o*Ie z_;7N>3qH;>UR7M2o6}vYpHOo*p)Ww6F(OpoJgstSm|ROmbq7baK>D1mQV@%83^dq2 zXS}V06HhzXZ09fU%RcX3bTXpZmvqofY}v@dFWHVlkY;T4sL8Y~RwB91+j2McwRO@P zt?4Vkudc&o@0PFR1;ZzCNCA64Lw$pwu7sQ^ zOKU|`Z+-LrJ1NUDLpB}RHzCSr>p&Tzpk_^dIy3sAL>8cR;|mCE1@j2{P&;t=w+tLA z(L>AO@>SSpI>@=JfDPq)bug0@Oi_nZIhVC{a7Z*u8lDgmz88w!8vu;67g8uMmV5KA=tj0x`8jaX%0=Wcvv|fU_0hOfH_R2vbiy1J?4R%S)vXZc ze_V00;`U4E2>->_nXkm|q_B(y$=5YjV8`e^4{0oK0Kk!z+~Y_UXGbow0$WEGGksjk zd$HAW`Gd8@oM$N|Rc%qU&CxaA^(l3ppS#|9la)X`I;2#^TXK+w>^BFbG!X>>E@;%h zo}5z^MeNCBEH0t+5U!RQu$PBl@?=)#f5@Rqep8COSGO?oc>jkK?Sef?n|xnd?I@4*=?5@}R8_UPotD!+AP@CWXO>nQu4_-O!S)AipAv#-B^6l_UuI zue#l(SjHlUnoB6hR==p)X3{B*m6!*aKla}O$`SY8{klZu_Ws8)k9?b)SKjY~czaONfLqmUccOKGgv$D0;;*?naM`Y{@ZELz1gND`czp(%Q{*YFs~ zg@O_|XqNKTEHmB$e_jlLv3qeYS8Bev8<<(&hlv6#X~gmF6Gyf+(I@YFOm@>Z5PQPm zi7;|o3s$jg2@Y9)qFWD%6yl$L-_{z*B~!hO=p{_*+bmf>YbWBU0JBSNY9Ci8q5lF7 zIp<$Mv({ytWMq4drh1(Tj#y;=(3V^o1zb$T?`z__aLP0Fk&num{Uv(?wjPy!6DQE8 zZYkJjbjmm`jo34GpF|&NHNfOqpx%2PRH(?iZ%cl+2{?1nx#c*N?E)g61E>UQicZab zro+YR;t7a@6Zm#sz!wU~(+pN5vlYw+;kH+UI3(TyWA-iA&}B_$7#vR!u-@hY>&1N# zQ6XjD1rHa0-)}_U_ z?#Z?M2pwoTF3>TmaQk_8pe}j26tX38Ezry#u&%4tV4jr@pf{E0gD#C>k~_>fpc%(q zjbv~x)0;C(E+lmii}`n}i$Cq-j{n^Rozf6_w>$Skex3j;!eN$pZjp=B9UdG zgXqx9$2$xU5dp8ww2ludUH8)IvT-l5bEsQg)i3i%^LqVOlwd%L3-emuHb7o~6obkw z%Fh<8xp4AqgP+~bZxTkkn?|U8G8IV8y{LU5dqm6Z?fK#Yh4%A{u27QjF2t%YyJay4 zF)3-F(B*fok9=g&fbp~(!ec)&ThitFHtSP-@-u1**>_#2g#Nj&Dd~so6Z&q7J zRks4G@=^TE?Z?bI`%jL{A#N?w}9_ut#z6eg*P^LfG1QvviZrj7KGB_}`% zlqo9W*1l!1%mKSoi(_8av7~W)*dCT16sm-@+6_LpUb{_QX-~gy3Ak+TJ|u$5h+yJx z;eoCafEYN3iaFX)&wS6j&XXsw%o&K5A`aa>&!&KSpb4I)?2$rL8c_K=lf3&S4xo#w z1k!Tnef}ci=CrJ)mw*L>HbqblVLBg{hkNY9W^4S}PzBIhc7TL-(67TR9Z=u&KNcvQ zE;sYP5sj!&QTb;P(QSQ|1Vr&6Jkk7!z}k_8Xq_15ZuPwjO2D%O>_DSd%6IqcJaupj zMzk)S(xvdd%@0r_}$(DT&G$d%WTYLCL1$`bT2?++rkc-GHQwFj+e)KMP4N7ITj$B{` zmlx40$N78{C*xRpDXUAHdSj~EsgoYz*DThICLjOqr9Jle8fne`XL-#v4LWb=FC0)= z1}6^a5RH-n=AF34`&Z?~rCOy_9M<|ET_5wX!sOVh)ggti&#AnBdG$(^#fihBx*Nb| zPoc8em>6xYAGi^dtLq^IaE)K8AK1_r+H~xmwwvzn z!Qp?|8P%ftECeRKifVO@KzSf1JD0lR!!l?QG1jaGt?!yxrY9Ai$%ry4U@+iJ^x`yK96L)ie zUzdhsN#&kUL^z)qn4n77_-c`eYV4b3b^d_im8lbZLdgJ3J&zO?`5i$XYmW!`i>0i# zb`*(AM?%lJP1>DP*0R}j*~(1QQo{Y_{qLqTfxxi=0)naIKQ2Y8b7Fvh+#O#3Y*9^q zBB|W*@a8_}CC3#@b~MF@9I=pDo3Nx>SU)`fR(xJKmzbZImz^I~U#}PP$3A6cC_O!e zZ=WOt_3$+Z^&3~kqQZy}i^4gASf}_&@Mx%kE0ZLK_!E??cE7NWNvc0=er#v$#%H@? zNm^U8E$URG>i?>0O-l}5_gfC5)HyZ}$N2VLWga>baEbQc_gz^L{>s3F`{14^m}N_B zKVygJa*e;jayjJ4<=O>oy}XAWDIP^v!`rRX$c(lUg(9QJHEYrmBW@h(*p)u4YW3AG zPndKLG#Wq{EAt=@)%>8M>L~E{nu`){Sd&Ip?>I)70#5%7WPYuWbQX;b2-E7>*2J$* z2sv}NC)2~}2+TkiXJZjr_JAqC?-*!m%Dj_E5ZYJCqla*)9CSx|a%q#U7xOV$Odo_n zBLW~bbVQrNQ#_mr`YfRHL!Ty@?EhVn_^UW!X0QT0-)!b6aU7Fy1U0Nf2FxV(QL(*^ zi-|c_T1fG{>JgbB53mk2%xriKGJ+WB7Qqt_0$%cxZ6jLQJ~$-B1%CJmZi@TLKuAaj z2ojUF+Gkt!daJ$s3dU6KRY|{Y$sA4I@JEp1lN+j5jt#p4P7pr8^N@j4{*~Scfg{2T z#U!vAdX7){#z=)qOqR7fnv&L0cSb4DTaJG-`lMbpOx;q&QA8^HyznVw0OgRkyg1^} z!yz)FKtLLI67gp27N&-^;LtLELEjoU#Bd>9Ez9-j(j7}84`@(|KED?=PmobjD|>Un zz^co|=h%K0%mU0c#{uPi27aXpN1h_&j-&DSNmv2cm7(0Mgdu}h#;sr}@WX8~eQ>M_ zhxqoQZvXtiqxZUogvuz4fEIDP7Un+!v*d~IqW|l^r5ecr|9`T-jrebyjkm91bBb5u zkk?a8Z>W12@1?(yZ;(q)!N--?>d!@=J^y0tP=#dV`*M}7F4A8Y_0J!2w z|CR&KEJ_N=C7vqT%$NuQhT#4G?GZ&tEh{@M=Rn(^!-Ykn+Cbzyy pMl&F-WH1F#gjuL#0ZJ5%uMA`|hZ7*U5l-{Wu^>$)$p*abKL8RpZxR3i diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/29.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/29.png deleted file mode 100644 index a60fc0ee7a369671a8e657e9858ae1f2e779fea6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1618 zcmbV~do+}39LJw|XT}&Q29bLiku27bkP;Q9ajD$O9@MxEXA>r7beSnF;+STotM*84 zsdj7Cj5cgzT+-IDB*M6i(M8&^UL~f&yYEbUbk6?O+3)kb&-*>^@Av$k^Ld{i(ua%y z?d?8PAArFC;D-)?$bkPgc49mL0Rdnw06-mJauWd#b!ZAOLLUIIr5J!k-x%e$QfM)1 zT8jPRm5Im%aNWT;aPR<=aUf--ll`~A)tefCQ&vNz@~c%=yW6y&pAT;1#*Yy%$Q9lk zq!EzRfhjP>Vk`he!eB`lqy?CxXW}s`skWgD2EyX-Y6Nwn#u79^s0|h|D}(6I2`dm>_Dfv@Ib-0yME0w3)CZ;0fj# z^Fq(>xeDvCIo*L7FU8SY>`CzsHUgeI@EXixZX|9p&d=oa$oY(%NLui{Out&edR%Ua zcUQ-%xV*TOPnjY-0>shWk)fRn^d@54ej1PeM8Hz zuVI$yi=!bxU7DXWZk5h6uxiAd+e`7odi-cB5sh43(C8G!6B|el^6BITBgyC+S~WK| zDl%feBEmR^P)t6P{B~~KWvuGmU(wVfbU0{EXJEv)agxY+QerWGNiZcLYV z1!J8leVA6CAUgiHU?6hiq@6!a-;&}}WmsFjKMj9`i2%I2qDRJ;ILI8tqJm+~Uwfv1 zUmp>z?d(!Fb1A+24e5ifq;WX?tUR?tKZ+83XzADx#ye z>M0iGbMK6a?G8VGcBfjxX8K;<3{RhT9F$0cDm*5u-D+z@OuZmzYh%ley12(Dj!A#= zgb@&=usLDEZNA5`;uyGBBVZ^cTr#XPU$be$5o7q~C72b+_iDHc^L~!7WNF1^@Fo+uOn zBiBn@r>t8<*&^KimF%{rkGWat0rckJ`npbGcowaF)1I)_Vq&p`T4tZiKl?EzCG63u z$1PH!L4HHx)P^RhQA4Cmi;%>zg+1I0%*=a|1&=Q#3tD?lubJdk@DKSrq?*{6oVvg? zdof5F`YxuGt974@wz+>Y-ORmLAl1B;74iXU_AiNX>*jZe+C?MQ{o%<3W>F{3q&5NR F{|Cf6oy!0K diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/40.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/40.png deleted file mode 100644 index d00506cfd51a19090b85cd47bf4fedc657f83c71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2049 zcmbW1c~lek7RP_tfB*^sBns6O(1Nx=fq;rOQIsGmh*+_r@8z%)6av}^)W|#*2~?J+ zsI6626@&*wWJk*qHea88EKjzugn)nxO9C23NoG2s@7QzRUp?X8{}zfCJb8P$@X<8p8<&z{3M<0|00NoPYy}n1w9?CfEr8ArA)#*cGR~ zmxur8waO#>VXGISb6|&8DEC|L`A}};rw%q>f*mgI9z=CCOsn>mRa5`@#8 zZ{#0mVV5|x8Bnx<6|f@U%mJQ)BT#T?8=zr1Nw`(5K88&=Jb_5k(9|Mp>tGA2^#LA7 zAmE7v5=or~cM02O)%?|$O63E)d)b$%o zwtQx3wRIca(P{gRozDCAyRzI4xF0LpQ-<=YOt8+juiwx<#%=TP6E*UV}pRlW2GWsB4P-bNE)P7Y5Z53tI|@7{81iwoOXKPJ*VSPo$ z{NiixhN{T6`$I3z64N#JlmHqtuFWJ@X(&Ln*U5Dz1Og~HrqMiXYy%t+Tf`}wDCVD; z7_2JuS17iI+@e1ccipD@Jh&Q>-DALZ^rhJwTzf*R5-1HTJD4)>1=u1)#EkzdQLb7} zsNTiah~awGRF8G;>kk$jt_dxz9yG^uX0Pa`XYBW$&Cah|O!81RBkgP%cVSLvg$<)% zBDw_y>4;?mOq*n0uKn(8^iRS0c_XhC;>$I~M!#~QASk?qqgWDkh10`&QrGHJ?JDdZ zu%97QkaUDysV7WBf$Xt78U;31C=f)&J!3RFpg^?HE>YGAr)m*1AfHivlKul?K$R$a z`>YQkx=Zr@yG5rty>Y!4!fGk*6blh`2=~04#Xr!xds*|<9)8!$D~cRd21A#x4{s3i zePPbUNu4!N>l^MxzevPU?jdU5eZtNtIQf##fgrEAx92+Kb-z5#xO^Si4YdX#{j|47 z9h+8EG!&Q$M{~t`(l&`EG=BS-(xU!nlRG)avc?RW<4jx3q&^%^*jb|WdgY+FRTzBsOpUFoLWMLi00i%Mb)JyDQ!8!(1W~bxw%v@gp3P|`y*rU)D z1;p`snPX9Uay7XLO3{fE`}H!qB0r<7*|~{ZrOF$={*?UCJ?l=$drvm(d{^;r=F(Z8 znOCtBOvLNN%!#>NEK;MYf*DFnr|Ci*i5BcRGj5yKXwp*Z>l@}X;C!RKyey#DbZ7Y& z!f@VK43W;2(6Qc~o6~MN|6`h(vGwbHX2LYZP&I$!qS#1;v1thJNccQ@R%@PU} zm9kpw|Dc$Fhh>sU%ek+lnWH#CH%7ZAU9 z`%>Q%7B_{y%;Vo=VhK4CajUeeQ!l<@L3AW}Y$F_hQ5X{+K3nRS+q}u9BDL_^v~^>6 z-+UISijpDxVzJXGOlQ}&gfoi-Wc?Y%&nPH>iXq9rjv!2S9|J2TtpWwjO>YhVvIQR@ Iwm^q|1HrzK{Qv*} diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/57.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/57.png deleted file mode 100644 index 931d0b0c9e77135c3b61fcb15385c35fd0fb2264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2938 zcmbVOdpuNWAAjbCkxR-YwM>Ov%NFIjCb`8V6cuV`ky|RY@=`GeS&CGjra|lV7UlxhV-Em?dpJPA-niwtd-$)b zCie(`*~^aTBrtXk2tF4a5)gc5qrtXsf$<)DN8)lc3@f(Ain+Q+CFvCtloH3H$TudC zJq+|Ikdpu=z=VL?4DfO|f*cOz01B3qgj>PNHf+G*2}F{ZxCB{p73NUB8sKpR0-i`9 zk(SfoBC&Nqlq0R#wB1r{t&5lV=CkrUqEoUY)b^CrD;(&A)eXFVj3G-ZDy>squdzi_ zOIycq=Pn~-6H_Z|8(X`*_6`SK-3}c-;_l&d+}F=PAn-)UxzK-wg`dB0>2hpbe8QEh zscAo@XJp>^`DXSn_j7Xd@(X?~eOy*vQCan*y5Sk8@p)5oOKVqmPwz`^->d#%9)INB z`wydIA7?(zex95Ef-Ed9^TO)+CoOFJliC09lEZl6i9`ZXe3=&xAGSwVQWDOUUm@$ttNQs~H@C6}*4!lvGqV9M+gwrnbWDpAn1sznFa`_7|@KV1)^5 z8F=g_;PKc|5ik%j3K&RYq!oz$2jVM`SO)nkP%Oww1U#OI-DEM6*w?jxY@ly2SrXBH zAVt7oVj{=^DiFm|kIVLb-!z{k%#0u{*$SMImYI}V8oy`__gJrEJVzziscgx2abY$w zNLTw_d3lssiqvZBl*VtEBebE_SX zl~6Q!DrRyTr({!hyudzh;6%0W9Gf$}rp)M2Sc=}uSzE_G4fW*SpN5XBrgKs7EZ^cAsM7}- zL_xd(hb_pYw_%gAh$(^3tnY|pe1llQD41u!jSnvkJLxs7Osl0TNdOx<-#Vf`Tj~dlYnq7C)Uv^nwK&G6QEW zNQca)U>i?*c!q~OSGs_k#{@|y(v$rx7NA#?}XBD z*cIw2CK;-ivQQpp!A{3yxE#8IH(VJTm+2E;Q7m=6_GMedk?)VWH#m4GA31%;gdlF* z)IbFcV-(c)+{|m524j$F-P%!8DC%J??Y}Y{=fui$3!&giID#w4$0$JPW%Rcv6^|FGu*B-&hZGFS4oRZbIa(` z{H^kJbZ>1zTdq~b**opGxsNc(WKf4v2S1;NWFgop5CxpWqy8Li0o{M-yVDaXtmh-O4ym z4`+)c$$UDpS!q(`0aQ;j*Ut;5@r+^mH6Bex&m%$R#!W*M_@)VEP~egn@@{tN)3bGI zQSFNxb~DjVQ5|(C;~oXk)UVe=!HZk>Q7~r8Wh3nQ9ATzN70I5ix3EB%!>43Dx_|c` zck0=hs2h|oRq~)@;d+}+^DTa8=xZ^zOEMCgPq9F?n0!nu$g+!R=U75b579|X{RM;Y z=?{}dOPF16#n{4lNs*jptcMy*_$G#7rA&@d|7Fu8AE(X-Wee6VV##6aet-E#OU1uh z6*L1|qaNnX9ipD1_w`ms9EIIcP&I!C1x=%r3TWcYg^0tj*MFzR5f2I+h;`M@`y%$(`w%@jH&&#{ zbjMF%Se)@=(QJ?vynf*fV zlI~io^*I&E#c!h@dD1%4eF~bN{@PgOd8`EmsyV9G<@DzT?4j-sL~G{*$I$Z-?5E{J zdNbhCyW7nr)!r2Q6KPTBj@V83E-6PuVriHHOBBRvE^Ql?3wrp%YFlCc+c>6mm)^t2 zBdKe3)O zUp?bcO6>%zJ9ylQSsqh0h`y${)&Ndx%;*|lT+%)E+Gzf}0fnQdLo1U~GiKWLJ~c}0 zeS6+vntI6D4??>{QB-d_qRpNNTQX)e56W2J1pd9|wCwPj`Z`K!XL*sSlUR7c+do9D z+ojuU$}Z4s+vR_4c2`a*d7^y1kdlvr;m_Cy5e19N0vi-uKAZ?GIMH+J#8HrwAkev_x={+U&+B$=T^fP4^2Qj(8jCim6zN z)jG`|6jrl`l2|25^+nVn*@>!~?91l5c?J#@8Fl2Di{{-`E*g!$e+ty!+4wBhxqIK# zJGaATuDGQp8(bDbS~`ab$`u9P#mR#un$PuP*sFF1uMJYwnqkJ6TV@5klnd8yq-ng( zDhTZE@Au4#)HodRcyB_@4XH;hc(w~xjbq{CqUd52KtG^>pFS7cB)Hfj8WXv}rvQY$ F`ZszVYrFse diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/58.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index 258eb99575e1b532518b3c060b2f4a7881b297c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2975 zcmbW1c~leE9>;GMAS?>VVi6EzRS{H(ptd>%lto2c3Mo>^vn+xE1E_$A8I~eS1s+&X zuo9L6E)`V31+9^#@?0^1KtN5xrXazI(m29o<_-9cJ*V&e(eu8uIMur36;{%og02qLh842iQL{0$<$O`~kDFx8TU&_q0 zQtIc?wWYK_V`^R*!_w_alfe!lV&(LGT$;Lu;ruU* z7FgTZE?%;fxyspP^_sQr8#a1)dU)U``A)#U6dmCHd&&ogfQ$gXW(?!MQ6~9zgRi8V5;c8ud!?o)-ZZ+b0l z^+^W?hlWR9y&fHtzk5IVhvEbJacYK(%;zU8^8ShJFI>7L7nM$@(bZ%B)oXBehvEW~BaE1i8rU3REhcJk?cIR6mdX?SObe z+LBK60WBJZG!so1Kmbo8KnMYPcSC#GBKR)99&RN-Hs44fwUT$|a%;x;B7EJm`Zp^N z@wDD+a_n4=_I1Aq%4(sn{596YK;QJ{dg(wrYQt*E1!&+7d3z2HVc|li&w}p2ex(}$ z29Dm0=5V5^M%KlLhHrj5kj32bWWw!2nm6!!+xvF#LyJJf#uaI$qT@6L$TnELQc+vN%-t`U5$FNbBZ0iE+csLr?B7o#9L|N~MtrPO> zlI@hGxYHZlL~igiYieIbnA|0}Q1eV=7cKcfG`RjBtWJun*c*ukhJp-jR55$ zc^d)dZWR#Vz5zB(fYv~^A@sU$QjFQxKp#tL#8MbDWy@{JSq8T)k-;6xv*XOy$-V<~ zY~4BMu#m50YKltC&~3Z!({k%iE05w@$)0kwp}XO&PU8HQ#=^>T!TnaJToMwJ!ftoAJ7t$u z7JBZ9*Kln8d2Yc}kkx9@f5#JR-iycQpP3g zx0%U3N8u?e*85ikP)Q{~dYpZ75a$9cp51M@oR-1--RRS5VS^TZ^P@O`C$U&0+h` zU3w4{yesgfUi6-I_3cA_-psqdUeK=^d61?(Qrj#*my&fg;S8{|?&u+HxdaG1yw0U% zs_IsyXz%s(=3V3A-OVG4FZR8f=Uit|c2w;U?W&{0i$@yiu1R9|39^M+aK;gm`5AhC z+5%gFW>0V6J#O5WI{1Rq-01V_SpBs8Nc4Wy^Y*`l%fnKhPaHqdfBGJp=hV=ABg||I z+zOGvp4wC9joM25&P?@{nco#QnLdUM`&=*sF+O)+W9OwkBxI-^qdHIj!9{6wluAes zrQuQPAFsMoZA8-<1kfSKWH*^WuhU*bAErpFI?UiUxGoQ{3G-Yl4 z?OIntyIvs46Ev_eeU_pMVP*fpM;F7*r+Efr#m<+aqss%`t6q+ZId%EM zuwwuDrxL>5PUfGf2-~*e7ejhC7t`~_w?9N^_?fI0-$y-N}a&%1laFPR_}fn>0Q;=a8tlkZi+&H?tBenXTBcKom4P9R9`|GcmyOV zhLM<6x%b2;iZjRAWXsI!VDdA>^P@2L^NDc}CYpwGTly-M*^}LT(b$I_o2}2{D0ZW~ zn3W?>d1v)L)bJs8tr|0~Am5B^$NqVXB)ZApfS)S(_^c{f9X&h9JyEULjO1pF$XPiP z2M>$qRA<%;9F!Bw>qm#K|F)sv+x~{%Zner*_5aJhB6~~6rYl(2oQ-DVb1SspvU3E8 z5A@M<`CaX48{B{Zof|JDiLrShioMFqWIrtz(Y#UL>8NB60%T+koZGjM6RaifZ9H7g zVCKqqz`a{8i6--P}e@o-8=w)#8Y|1v_lGfZ^g+2jZ0XR=+k!M)nj+;MB<6q&bH zIESyzoO9R{xpp=Zt{Pit+YwzObFetJ?DBW6_8s@NgqN3C?JE;D`BPG9GHojjUN7Ojf)Esyd8D0{(jhXLlc`x8a zZ~mj!{H@-PqX^LAj6V@fnkVij6Y9^$_NE7K=6^*#ekza@i6>)WEeNR*3O+jI&hGam zp0iwS2ES3O^K5s= zH8M&2JyC-Y;pZgy& z81wvLn+(hAp4`GOEOMN8k9aK%KV+=8-M3wT%ruQBsFd&a;b6)GyytmL+>9U+%(%n2LO%F3naSEFJ_ zN|Ic<&SH$+Y(z4QTn3eEW(srCG3R&K{nYQXyMOHG_dV}<&+|FY^FGh}eV*sM2kk-! zfU>KTixa@%0C2`GfC|A^j&WhZ0JysYV*r30z@>%(A|_!bKnuG8AQa*N0ej*W-WB3M zX>AG#pXCKZbP{aw45dfYqeAHsIu>Rdz!rNKcjAIK42$xbMNzq9)sNko;0|&0r2K^m z*q(|00?2Z}2G|gAdH_$x5y&`{3n*Aj8QdZkmthkQPaw*WWaZ=)mS6@|$^efe5b#8T zjLbq9Tmq&CM6!&E?nXP3s#k!l-m&GI60@$z>DyP-sqJnP8CV>SOp;en*U(&{wPvm1 zIwNDt%~sZ1wr+FS>FD&Ovy1DVz1}|i_8;&KJQ5Tf5*qeRRP^zf*tq!blYcmsl6pEV z{akiVZr=F|`2|<66&3%?F1cQMr}A!9bxrNP`}GanMqbmezddPx-tnT7-_2@OUCNWwA zB;c@SB9H+U%+JkdastZs%$Cnzc&Pf(6>*1W#8)X~c--yG!H%KlHb-OG`-@+UTecS$ z=N29F6ZBlO>iRkj@2I(VMP1;ue=DMrT5o05eyE@M4AK@Rsf8J2Np;6uOVT&}&K&@< z2HA&lHcbhhjR-7w3 z$M-PmR>Z}0NNa7?p*kB|A|q0?Dz(u(iB)CDqiDv3?0wH{x-r61EvHYI*`ytkXI#7( z6Ib-^RmM=l&6xXlF58vwKtZY-93?#?X`&-rd|B|m_p%>lQBc>&RGm*kK_ChM^Vi49 zunbh@2OGNixy+cu&XeJdGt(^+YyQz?T^G1D8j;P$y!|;=Tx2^LRr&=0=n~*g=mr|fNypxsGsAY%>t>FR+&V6=ZJz*_;O8aH? zdU#Z%&t>YqKp-~qg&5KD$ta0ynXD@FS--wtA@0P16S0pks{E$a_l>o=W@MwMNn3)w?R3vP?K)_j!n6Nz z@UiWDJ~aFq`iAq-XpSzufdbiQ?ycxE={;y5*RvrC1w;Y7IXv==L1dh3oxdi@XU__v za-7cDLjHTKH)JAEkQ^B#8usN+eu3Gxwl(-ootO+1mX+-Fycz3ZosuKe5BNRR_3-W6 zFHHn%<-^U#`ja@{KA3_H`w?aHI#$)|i$W_gS4tMKQmaSOVrJKlf3%nOcu+^lJ%={M z+Ydb*?3H}m^DeoT=|F?(`unWzoNPsb;ynvtR)o!-6RiAt6g;cx*;@EudoRTitNI3L zAQck1w+i0R5@Fg$J4PfNFzBz(E9g6Sn1AJ-n3876&)$8zHjg(pd|>_-j=rkn_GYfv zP3Ifh>B|F&^RG~#{2cS62aW9Hb40hrhA1#ODk(zP^xD;rj>2>B3oJ`}VZM;UYhB6E zt(woD+8l~d22E)@+@{4!0@JScsh=w~aH>0UJ>7>tBoZ@gVtSia=-ld_+xkzw!^%-W zYKQ7{haFbJs{Xo(M5vxNxGPmUNP5geL6Q%)nH^PB*l6EGZU0?n`M?3@>aXsD2>j`b5Dy6q36CETA9{ji+z7d)z~unaTLI7rg-J@4Bd8HrCx@!hw4bw zxM>3E#^#gN8+w<}b(+-k6CPH_zU`|Wt@+7Wi&f2%XKKV*48+z$iVQV4>f2f7Y7sLl z=*nn>i%IrFA76%i_;(p5#YI_p>pC=IOt?X51_ki;*&q~T=g*g-V1hl$nJY&qG!{qX zMu*MWr#S;fP}NRO<*pkd`*Gofi?G+hU)ssqe|g5G;&MAh$IdMGh=rk`hOUA<*m(Et z(DahOd)rY!McbtJIRiO6GhxGJbqwekn~j1I62}f{IaYv3Fveaec(R4L1O;7xs&c1b zGtQ&~)~8mmKQX^Cd;(_g(U!VUq_!9t%N1 z#PqTp6nLG_er4ieSAc|ytPyq2?Ds6Gg2hldytb*y-E2tMls>H#%y$;`u}cr`oQpjj z^q`{pQIbKf{h93#5+iM!=^OQi!^yvf1mYoOw~zFNDsD69QqHoa1KZA{z-U}po_WX^ zejyGSRKGq53F_Y*>|_L#`1yT#-F>jx`ff*Y^;y&E{?McoCxW~52+hH#zdNYxR|X29 z@}9gp5@!=9nD${SOHeL-!n}wAtYURD9}=(@8rwTmg#jcYE1`Qx~|f4m1VSX{^rQGJ;G)4YMiqaI2!RoL9-S~LiIu_rTo_rFVh4C zRUcU5^5#r9nq5zsTMubdMWb*H3i2$WhGJV~6twxlLhKOtXyhOjg{pNjV?F0cA4-+3qN=0d9q%8uqGT~c614R3I=8Ra%+n08 z(>}(#;!So^^L7Ft0u1bXeSYG1>d@N%J()Xc$Tp9^FrTI+Lpa-2OrFnuNupFU!_5y; x9)o%cs>=o+?ei2bElXg`=5@LmNCdgu=Nr=7+Xa1=isZmkJ66_M;HhZ$e*vauSTFzp diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/80.png b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index 59cc71809fe3cf139b2adc932eb0a4abe19124ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3795 zcmbVOd0Z1&@_)HFRc;X?Mv%kh6hzPw%zzw&fZ&MY1(^YN5P{{8OGR1`1O-H7kP+5I z1Vj-H0b;l`gq!i;lv4)+3dn#Ys2EA7dmCmyv-_EuKlb;lbUKyyUe~K{)wio2I*5(| z#XU|gP5_4k;2Z1%P(E;Tj1Bw&0C#s_2mp`)xRgLZ#58OP(8Rs~5N_cB0sF-*kKMxm zKI?gl@E?77Avy=Pd!7i53_W!sG;HHm(=A}Ty^A|>*%~G*y55Sa*r$~~lnE+`@6OBq zG%K=?M;m~W4DbY=1e^}QE8z%AIJ6m1usun*6H}Y;578TzqxqGj)qOywph*Mot+t}3H(%RPE@pDga-^>1i!J%RP z_{5vHlT*|0W`qkLK7LyKEc&vvj0=nBZ?LfYZ^-_DO9{h;ClU!n>1A9ve9SU%B_c`N zTuRw)pY-?PDmoSkGOG4j1uUNIrmf!!)chj4<<{z2^7Vwv&{mNBZ@?1&U&ww3_8(j$ zU^hmrWx- zYmAm8bQr86;4m@~lmHb--bqrBwa75-QRnPXaeQyyz?}0$rj+}*yN9n=SG{df>|R@= zy`1v?&|s60vYvUznrbD-&`{0E!*8QE)>$1&_>>E8F_=4z0_SYuZWNGw}*;cG~wMUiGSLNs0!w4YWt$x?f0#IT>o`4 z`KM0-(Owy<>RChk)NY*DKGxp_FQVWgQNpB7FT`(##_sbVxN{#0R>_Keq3$=ZCklAp zi75C&XTG&YmA<~k#G88ofxb#3ovatP%|6bKp^>?z3xj}`@3E!GSLH)g2( z1Zq&{k`3cBMPw+Tp$Ed^ei(C5Qm11W; zEM^Kbo6l`+p{dqukG($nwa_YupY?sz9Z`zsQKM|Gdr#RvZqql8AJV@PUg4l|kT|h~ z+oTn=pn}M+Mu85sfg&~=UB&$PfTpj}82cPy2*kUo#aY0#sEwv>G`2abez4Rl)+ zB&G*(*?sYNNnMCk##pMEK35cTXL`ZK`l9J~sTT5g-)nfj{#CKH`Iw}1dkJSv>n}2M z#merIG%N#uO7{EtVWY($k&5_8oPHl7g>*mBs1ybyv%j+u8DWVd~%1-#pQ4|PkSFi6z zc8m(DY_w6ZyNV(5JO(Wsp#0%K#7cn;A&hDtO#>kjDHH{*(|&$b7*fs8)5;8Gg2cXcpUBj=_f{@_94_x8ewu zzaPd-6`XQKL4y&5Sbu%iAr5nj7*L;!y#R@x$cur>XT2R}O>-o=Q^f3x;NEa!7v1+S zB7&Y%$I3?+Ho;iJDByh8pb|#OVJ1c2>)(&8*hcLb=jX{9VF% z(7^1Vh>Wda%w+)0a^oye&^zm;QHXXUMJO0cJ%(j`D=cLjMOA}lTuZ&D9%RB=SP39@ z_n9a!j5aZpt#*aq#A|nDp@2$%xuk|o*>A`E42WHYg1%+mU7N{SNan!g<*A&igb==< z?NTGtEtYe1gM`#Yxq9od*}eJEavH3zuxNulnFhzyO6a=??{=A%4}@6m*bfD;jITQNrMCtqp(Wd7`Ful>adD1 zI(On;R3`D)+RPNTbXAvI|6EM*tH))N8V(cB(mTk(!K)swk>2wW8rq7ik(DGgpZ zo|=?$4RTC0vza=~Mb_XY7pVRo!`3a-im6t4NZn-|lCdp{nR4xVThD$zm-86*J=UR5 z{%mdjzMW|0B*UX}@||9@MJN!vVWIxb1@=H2Lka1jt+z2R8;#|(cm(Meh6j$@`g>e< zs;o+ERW_M3y?rU1RDUv>xxJ&~mePm$Mo6^71Cf0y%pV_3ikFYwHPYHqCK@=_TW?#E z&sA_IRoA~X^{?W5C_ZhzKK^)G&6O~Vurrj~E6mo7!>=&AbA{P-*c=5HVxh5Iip(wp zk^h8rLcQKY-b2yuL*)aJ9RWfoPSpCC+K@({qdR%FBP|;b`+vKy@BSK@mNV+`S~B90 zd7idNd!m7YcZ-J7(-7LTQZb?bs+bsamktOO#yy&tNxKy4L@B}mT5p^gUt-g2%C_~L zAscO9)njaanxj(TL_hv8>&_(kW{#5!p1TbiOZ|hPsZl{#Zi0=E&fbP?`#h+1G0xxQ6fKt`S9}HNCp_-tq`<1yi@@yI<_a@i^~GrGC~4;=&Ja?bwIVQJ?|^hYs!@PicY_ zHSApiIWp{)me+rgEkdh9kijl3ia+czW9|o zd7R3Up4%O+(x|1b!AbbC zbh(PFa#!s@Iw)3!9uCZ6^xhq(Dkv6N+5_lkJXTFIcYsjlis7q%PB-Ib!DO zwpPZ38fT}bi4~7-`@z2Hsn6dxSSDYvzau;t6ousdo9_Qr$UPm)I?>+0WN7{Ab<9bX z#e_b|DC(bwNdBLjLTlhoQ8(um5&#DYsK|!1SR_19P-swvf+jYfDk%Jmv0KLR zBXRqjU%L&_3lA;eBFx1MSah9Vfxx-eyR(#*27fnTihm3$6+?2uPXpW_*WP!yH5GL zmUCV4pZr)uatJKj>Kzaq5abY`OKi^|p5Q8yq&exNdXfY~SI&Z~uXVUfw>wLBYq4hlGax8hIuv znimrrcj4ltJMa7RwN-L|XpVrj=`RsW^V^ecWYg>DV zxTp88zIX3G^na9m`8qrz{f2#)jp2gp`4bjQeL zFI3iAr>^&)>LTK{K&*Ms@cdt*7gmE*6_f#-RN`_TK?J{XZf58`wX% zI>9ahc5C^%o_G%nHJ_<; zH0MoXvQNf4$g!oZQBr;@Ol1b@-x313YJ z6#wuueU!^xj#SZ*3K!M~8SX%WGmJ>qcS_Q~yWtKtt0eXT90$wP4$W?Ze8ex+@Z;~CZ|g( zn2(s~9b8-9`58BOO9F`uzbh{!L7zo;J_$wz+N|L!f6s#9Q+)C@kwyYhl;BH(1s!P< z_IArRjb!6vvqO=JLx|5j?Ov4_M8xM8BwowG=R9oC$ctI&5I(tWz`a__(vIJ@-l#>k z^SAAYPkLD6BX7|c>cJgIIP$L^Whw++3GzoQholl^@acQ7fYH@HxmTmlH3+YV^4PQW za?*Fcys`g6!*RFCX7rokVY;H<&A-Py)wsiP!L#NLKVHFZbg^#~JBDv>e6b;YvaYKX z>@)gxM7YXtII;qADgZOLuVPa_@YT6S_|cYNPey0}yRaenu}!BVN`hp+0|)4xovWjU zGmmfYvEBOW<8+RCZVS)Jc-$GxJN`@-r;$ zZW;dG-km*rQJbTQ)wZj&nK6bc!}lQ~5$vmtR9LVMGuznWfFX8vJNCyskEf;T<)zif z-_B?51>2lYmoHToRS0zG$|qgU;EEjJT?m8h&!)QGO1W7BQOykpYP4DhZT{ zxuGt-OC$ZucgYJybphbB$IAYZA*J z`R@2_kVSm17RncNat7tx1>6o2d`bD-hRC~P`LaDldF?W)kgOk%*Ycn@gNO;C`yJ{T zb^e%?VQl8`;?S=oa9QOmxERpkNzj(SRce>;hL%fMHC!qhlJKdVm4gYZ-4oBx<5$m_ zJd3%xta-lIgN2<;jkSAR4oP~f5)6OUrB0$3Wqzxdl!i||1vwbE(gFB}58f6ZJp}bmw z)=HyZKen2;jWzl9DoOBwf$Q+3nXO#4o!lo^y<(0{W-}^!4oLK+j5uSo-M-v^R$JKZ zU3O2pDAP(ii(hJfy;c^DTw`LvN%#~N3D(wDvvF%H0bakA#_XRu(L3)&H)g`F5Of5I zRB-LgCww~VkmIhk!XCZUV|g~W^s=(;@4cDJv2GrzOG!PUTi5Y|;F&I~e&rgGsiw-@ z%I&gYU^$WrQgf_I#5^@SaAx8XAr;C7`lVynyt6~uUHt`5_fIbko9+>&UNz&{)%BW1 z2BoLYsmdZl?&C?Ab7WfWmnFoJg-C@t)SuoPTD;eU+2IbT#`@0Px$r2YCJH=Z4UeDS zjq9mM!{jBoHq(jDQY3z8r82>2zNhH-){0l{_TJ^{XLk6mqS(8hiv-EKHKv-tT8|n> zzb|VXh3E3Qr(h2WzHw(D{m~r)uK`J<6dtQf0&W!vKH>0^X;d%d3SZAa$cFI=YMz;& z_oomu4#3kLEBR7O?AuRnt|x&vKc0c=OHW9;Yd1XnJQR&`c*iz}Za4fxC{53FcMP8P zwaB3_EN!>Y^2LdgS1mQFOG^fgU4wNBaaSJ!8p4lXVr1RD)wBJkj=rX886viNwe-#* zuTe&k?`Par*dvr#T@j##Og!Uy#95*tpW(h~9-;>?4}JT%GNNuF)D&OVcZV$`h$+FL z^4K-;F?R;xymiW1qJH3uA3=|TO!UJbe(J*}8v~1{7Ak&evl@x%z9B6a-*~@WW9^@N zjpDetX7(=kcY6;cy(mgjliYhXIl^a(AhKJc&x(b^I6`f^2@mvT!6Jbuiv*~L0sMO^ zmajrXJg`M8RZfWcnw?nmi|w8ox9>cu^$+q|Rl*y!YA#}{zo4wYGV#t+4{sgy)sTbC zQ;3lbgWJfW1+x`iuGuDff8 z{XNmdWWrUDuaE0+q$yAZ{UhAqlz1pzV)4*ui}-+qc_yGQxS{Y4s@au1@zCdH9o2xF zb_dMwE)LO1G8fb%1P#iGl9@bJAj;^bzr&(XZKu#6Vy1YKVuE zAS8z6hSkyj(zJ>(rzE!bC<@eyu>aHM+;gX(CLSDn|Z6t^R z7S8kReypRif_3sf94#B(Xm(}z8VsJN69Y^<))R(g6{``^S)>q(2S@ZSSyJ5l?Q8_| zo{#KpnZI~o#XRaq^$@e>m-_lE1P#9=E;wK1KR4;sk@BFENTaWSArKWH=thfJIG$^X zjfS_48g|N0l)$L4Zn+VbYxeX~%7r?mk?bcaR=%d&iPpoqy$Lg?I(hKU$9}1HK6xAA zcMn#;K=n1VD5WNz(M|&CesnZ6vO46q10@&i7WB)urTXU}*}H}U{e7=2`@Y-fO4xCU zqcP3%1H8bgcOckh5mcD&3qAVz>E$4XL4=z`{bq+>+We9;b|g6X){J=7_)AxhkiBDd zkio&rVU%ZqrcoxEa{j13QOzny8l<7lLJ?=M{d#Wl?e*(hTMLT$S-!<*_By_?8#)>O zTP&-*Z9~&sC(|Ptfq5+t>CPos{Wzrg4|3Eb55^4lLE_GkpTj2vqLZZ_*fo^ngoS4g z{Q55aMqQ6^VrcGWzd>2c-DBAgb{9r<>D}ItQ5O1DXV1p$G>0XFOthYfx?9L*u?jK? zZJVVqYKih}6!g|u+#n9-w#2D+o;N_oX)|8G!%pn>>wnoX@x}JIUOW-U80?2Zw=jVO z-}l&$SLkucY4G(?69zHKgqUDU0$UYnFbOKXYDj?Rr;l5j^L&=nd$}f$1nQa)Hx!+l zk)zg#*s>QPwnKeW*Z#Lsw?5+!-ea9o&~Wj$p%O`_RD|sq-N!DF6!2+eVEesK>VL_Gn)2^0@Th5+dHKUjT%+AU{sQ9Zu0@7tjNLlhl1O06 ddCwYz^fyadM}ngRBv`+STj+M^m2WNC{(q6-PoV$+ diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index af727e0ca..000000000 --- a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "images" : [ - { - "filename" : "40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "29.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "57.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "57x57" - }, - { - "filename" : "114.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "57x57" - }, - { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "1024.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/Contents.json b/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/Contents.json deleted file mode 100644 index da4a164c9..000000000 --- a/Examples/Google/SpeechToText/Sources/Configuration/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Examples/Google/SpeechToText/Sources/Configuration/Info.plist b/Examples/Google/SpeechToText/Sources/Configuration/Info.plist deleted file mode 100644 index cdeb96813..000000000 --- a/Examples/Google/SpeechToText/Sources/Configuration/Info.plist +++ /dev/null @@ -1,64 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSMicrophoneUsageDescription - Microphone usage is required to stream and convert speech to text. - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - - - - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIUserInterfaceStyle - Dark - - diff --git a/Examples/Google/SpeechToText/Sources/Constants.swift b/Examples/Google/SpeechToText/Sources/Constants.swift deleted file mode 100644 index ed720a150..000000000 --- a/Examples/Google/SpeechToText/Sources/Constants.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020, 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 Foundation - -struct Constants { - static let sampleRate: Double = 16000 - - #warning( - "Please enter your API key below. Please refer to the README.md for instructions on how to configure the project." - ) - static let apiKey = "" -} diff --git a/Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift b/Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift deleted file mode 100644 index 4d4a6a931..000000000 --- a/Examples/Google/SpeechToText/Sources/Launch/AppDelegate.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020, 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 UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - return true - } - - // MARK: UISceneSession Lifecycle - - func application( - _ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions - ) -> UISceneConfiguration { - return UISceneConfiguration( - name: "Default Configuration", - sessionRole: connectingSceneSession.role - ) - } - - func application( - _ application: UIApplication, - didDiscardSceneSessions sceneSessions: Set - ) {} -} diff --git a/Examples/Google/SpeechToText/Sources/Launch/Base.lproj/LaunchScreen.storyboard b/Examples/Google/SpeechToText/Sources/Launch/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329f..000000000 --- a/Examples/Google/SpeechToText/Sources/Launch/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/Google/SpeechToText/Sources/Launch/SceneDelegate.swift b/Examples/Google/SpeechToText/Sources/Launch/SceneDelegate.swift deleted file mode 100644 index da4d3367c..000000000 --- a/Examples/Google/SpeechToText/Sources/Launch/SceneDelegate.swift +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020, 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 UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - var navController: UINavigationController? - - func scene( - _ scene: UIScene, - willConnectTo session: UISceneSession, - options connectionOptions: UIScene.ConnectionOptions - ) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let windowScene = (scene as? UIWindowScene) else { return } - - self.navController = UINavigationController() - self.navController?.navigationBar.backgroundColor = .blue - self.navController?.navigationBar.prefersLargeTitles = false - - let viewController: UIViewController = ViewController() - self.navController?.pushViewController(viewController, animated: false) - - self.window = UIWindow(windowScene: windowScene) - self.window!.rootViewController = self.navController - self.window!.backgroundColor = .gray - self.window!.makeKeyAndVisible() - } - - func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } -} diff --git a/Examples/Google/SpeechToText/Sources/SpeechService.swift b/Examples/Google/SpeechToText/Sources/SpeechService.swift deleted file mode 100644 index a78861c75..000000000 --- a/Examples/Google/SpeechToText/Sources/SpeechService.swift +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2020, 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 Foundation -import GRPC -import Logging - -typealias Request = Google_Cloud_Speech_V1_StreamingRecognizeRequest -typealias Response = Google_Cloud_Speech_V1_StreamingRecognizeResponse -typealias StreamingRecognizeCall = BidirectionalStreamingCall - -final class SpeechService { - // Track whether we are currently streaming or not - enum State { - case idle - case streaming(StreamingRecognizeCall) - } - - // Generated SpeechClient for making calls - private var client: Google_Cloud_Speech_V1_SpeechClient - - // Track if we are streaming or not - private var state: State = .idle - - init() { - precondition( - !Constants.apiKey.isEmpty, - "Please refer to the README on how to configure your API Key properly." - ) - - // Make EventLoopGroup for the specific platform (NIOTSEventLoopGroup for iOS) - // see https://github.com/grpc/grpc-swift/blob/main/docs/apple-platforms.md for more details - let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) - - // Setup a logger for debugging. - var logger = Logger(label: "gRPC", factory: StreamLogHandler.standardOutput(label:)) - logger.logLevel = .debug - - // Create a connection secured with TLS to Google's speech service running on our `EventLoopGroup` - let channel = ClientConnection - .usingPlatformAppropriateTLS(for: group) - .withBackgroundActivityLogger(logger) - .connect(host: "speech.googleapis.com", port: 443) - - // Specify call options to be used for gRPC calls - let callOptions = CallOptions(customMetadata: [ - "x-goog-api-key": Constants.apiKey, - ], logger: logger) - - // Now we have a client! - self.client = Google_Cloud_Speech_V1_SpeechClient( - channel: channel, - defaultCallOptions: callOptions - ) - } - - func stream( - _ data: Data, - completion: ((Google_Cloud_Speech_V1_StreamingRecognizeResponse) -> Void)? = nil - ) { - switch self.state { - case .idle: - // Initialize the bidirectional stream - let call = self.client.streamingRecognize { response in - // Message received from Server, execute provided closure from caller - completion?(response) - } - - self.state = .streaming(call) - - // Specify audio details - let config = Google_Cloud_Speech_V1_RecognitionConfig.with { - $0.encoding = .linear16 - $0.sampleRateHertz = Int32(Constants.sampleRate) - $0.languageCode = "en-US" - $0.enableAutomaticPunctuation = true - $0.metadata = Google_Cloud_Speech_V1_RecognitionMetadata.with { - $0.interactionType = .dictation - $0.microphoneDistance = .nearfield - $0.recordingDeviceType = .smartphone - } - } - - // Create streaming request - let request = Google_Cloud_Speech_V1_StreamingRecognizeRequest.with { - $0.streamingConfig = Google_Cloud_Speech_V1_StreamingRecognitionConfig.with { - $0.config = config - } - } - - // Send first message consisting of the streaming request details - call.sendMessage(request, promise: nil) - - // Stream request to send that contains the audio details - let streamAudioDataRequest = Google_Cloud_Speech_V1_StreamingRecognizeRequest.with { - $0.audioContent = data - } - - // Send audio data - call.sendMessage(streamAudioDataRequest, promise: nil) - - case let .streaming(call): - // Stream request to send that contains the audio details - let streamAudioDataRequest = Google_Cloud_Speech_V1_StreamingRecognizeRequest.with { - $0.audioContent = data - } - - // Send audio data - call.sendMessage(streamAudioDataRequest, promise: nil) - } - } - - func stopStreaming() { - // Send end message to the stream - switch self.state { - case .idle: - return - case let .streaming(stream): - stream.sendEnd(promise: nil) - self.state = .idle - } - } -} diff --git a/Examples/Google/SpeechToText/Sources/ViewController.swift b/Examples/Google/SpeechToText/Sources/ViewController.swift deleted file mode 100644 index f1c4451ef..000000000 --- a/Examples/Google/SpeechToText/Sources/ViewController.swift +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2020, 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 AVFoundation -import GRPC -import SnapKit -import UIKit - -final class ViewController: UIViewController { - private lazy var recordButton: UIButton = { - var button = UIButton() - button.setTitle("Record", for: .normal) - button.setImage(UIImage(systemName: "mic"), for: .normal) - button.backgroundColor = .darkGray - button.layer.cornerRadius = 15 - button.clipsToBounds = true - button.addTarget(self, action: #selector(self.recordTapped), for: .touchUpInside) - return button - }() - - private lazy var textView: UITextView = { - var textView = UITextView() - textView.isEditable = false - textView.isSelectable = false - textView.textColor = .white - textView.textAlignment = .left - textView.font = UIFont.systemFont(ofSize: 30) - return textView - }() - - private var isRecording: Bool = false { - didSet { - if self.isRecording { - self.startRecording() - } else { - self.stopRecording() - } - } - } - - private var audioData = Data() - - private let speechService: SpeechService - private let audioStreamManager: AudioStreamManager - - init( - speechService: SpeechService, - audioStreamManager: AudioStreamManager - ) { - self.speechService = speechService - self.audioStreamManager = audioStreamManager - - super.init(nibName: nil, bundle: nil) - } - - convenience init() { - self.init( - speechService: SpeechService(), - audioStreamManager: AudioStreamManager.shared - ) - } - - required init?(coder: NSCoder) { - return nil - } - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .black - title = "gRPC Speech To Text" - - self.audioStreamManager.delegate = self - - let recordingSession = AVAudioSession.sharedInstance() - - recordingSession.requestRecordPermission { [weak self] allowed in - DispatchQueue.main.async { - if allowed { - do { - try self?.audioStreamManager.configure() - self?.setupRecordingLayout() - } catch { - self?.setupConfigurationFailedLayout() - } - } else { - self?.setupErrorLayout() - } - } - } - } - - func setupRecordingLayout() { - view.addSubview(self.textView) - view.addSubview(self.recordButton) - - self.textView.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide.snp.topMargin) - make.left.right.equalToSuperview() - make.bottom.equalTo(self.recordButton.snp.top) - } - - self.recordButton.snp.makeConstraints { make in - make.height.equalTo(50) - make.left.equalTo(40) - make.right.equalTo(-40) - make.bottom.equalToSuperview().inset(100) - make.centerX.equalToSuperview() - } - } - - func setupErrorLayout() { - self.textView.text = "Microphone Permissions are required in order to use this App." - self.recordButton.isEnabled = false - } - - func setupConfigurationFailedLayout() { - self.textView.text = "An error occured while configuring your device for audio streaming." - self.recordButton.isEnabled = false - } - - @objc - func recordTapped() { - self.isRecording.toggle() - } - - func startRecording() { - self.audioData = Data() - self.audioStreamManager.start() - - UIView.animate(withDuration: 0.02) { [weak self] in - self?.recordButton.backgroundColor = .red - } - } - - func stopRecording() { - self.audioStreamManager.stop() - self.speechService.stopStreaming() - - UIView.animate(withDuration: 0.02) { [weak self] in - self?.recordButton.backgroundColor = .darkGray - } - } -} - -extension ViewController: StreamDelegate { - func processAudio(_ data: Data) { - self.audioData.append(data) - - // 100 ms chunk size - let chunkSize = Int(0.1 * Constants.sampleRate * 2) - - // When the audio data gets big enough - if self.audioData.count > chunkSize { - // Send to server - self.speechService.stream(self.audioData) { [weak self] response in - guard let self = self else { return } - - DispatchQueue.main.async { - UIView.transition( - with: self.textView, - duration: 0.25, - options: .transitionCrossDissolve, - animations: { - guard - let results = response.results.first, - let text = results.alternatives.first?.transcript else { return } - - if self.textView.text != text { - self.textView.text = text - } - }, - completion: nil - ) - } - } - } - } -} diff --git a/Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/project.pbxproj b/Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/project.pbxproj deleted file mode 100644 index ca8aea8dd..000000000 --- a/Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/project.pbxproj +++ /dev/null @@ -1,451 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 52; - objects = { - -/* Begin PBXBuildFile section */ - 45600AC42445365000180DCE /* AudioStreamManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45600AC32445365000180DCE /* AudioStreamManager.swift */; }; - 458EDDB32444997700BEBFD3 /* SpeechService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 458EDDB22444997700BEBFD3 /* SpeechService.swift */; }; - 45A75D542446506000ACB73B /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A75D532446506000ACB73B /* Constants.swift */; }; - 45EA4F3024439C2300622606 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45EA4F2F24439C2300622606 /* AppDelegate.swift */; }; - 45EA4F3224439C2300622606 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45EA4F3124439C2300622606 /* SceneDelegate.swift */; }; - 45EA4F3424439C2300622606 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45EA4F3324439C2300622606 /* ViewController.swift */; }; - 45EA4F3924439C2400622606 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 45EA4F3824439C2400622606 /* Assets.xcassets */; }; - 45EA4F3C24439C2400622606 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 45EA4F3A24439C2400622606 /* LaunchScreen.storyboard */; }; - E7726D7B282887B3006686BE /* cloud_speech.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D73282887B3006686BE /* cloud_speech.grpc.swift */; }; - E7726D7C282887B3006686BE /* status.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D74282887B3006686BE /* status.pb.swift */; }; - E7726D7D282887B3006686BE /* cloud_speech.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D75282887B3006686BE /* cloud_speech.pb.swift */; }; - E7726D7E282887B3006686BE /* annotations.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D76282887B3006686BE /* annotations.pb.swift */; }; - E7726D7F282887B3006686BE /* operations.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D77282887B3006686BE /* operations.grpc.swift */; }; - E7726D80282887B3006686BE /* http.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D78282887B3006686BE /* http.pb.swift */; }; - E7726D81282887B3006686BE /* operations.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D79282887B3006686BE /* operations.pb.swift */; }; - E7726D82282887B3006686BE /* client.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7726D7A282887B3006686BE /* client.pb.swift */; }; - E7726D8528288825006686BE /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = E7726D8428288825006686BE /* GRPC */; }; - E7726D8828288850006686BE /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = E7726D8728288850006686BE /* SnapKit */; }; - E7DB07D128288C160049B966 /* resource.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7DB07D028288C160049B966 /* resource.pb.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - 45600AC32445365000180DCE /* AudioStreamManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStreamManager.swift; sourceTree = ""; }; - 458EDDB22444997700BEBFD3 /* SpeechService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechService.swift; sourceTree = ""; }; - 45A75D532446506000ACB73B /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 45EA4F2C24439C2300622606 /* gRPC Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gRPC Example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 45EA4F2F24439C2300622606 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 45EA4F3124439C2300622606 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 45EA4F3324439C2300622606 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 45EA4F3824439C2400622606 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 45EA4F3B24439C2400622606 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 45EA4F3D24439C2400622606 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E7726D73282887B3006686BE /* cloud_speech.grpc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = cloud_speech.grpc.swift; sourceTree = ""; }; - E7726D74282887B3006686BE /* status.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = status.pb.swift; sourceTree = ""; }; - E7726D75282887B3006686BE /* cloud_speech.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = cloud_speech.pb.swift; sourceTree = ""; }; - E7726D76282887B3006686BE /* annotations.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = annotations.pb.swift; sourceTree = ""; }; - E7726D77282887B3006686BE /* operations.grpc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = operations.grpc.swift; sourceTree = ""; }; - E7726D78282887B3006686BE /* http.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = http.pb.swift; sourceTree = ""; }; - E7726D79282887B3006686BE /* operations.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = operations.pb.swift; sourceTree = ""; }; - E7726D7A282887B3006686BE /* client.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = client.pb.swift; sourceTree = ""; }; - E7DB07D028288C160049B966 /* resource.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = resource.pb.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 45EA4F2924439C2300622606 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - E7726D8528288825006686BE /* GRPC in Frameworks */, - E7726D8828288850006686BE /* SnapKit in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 4524351E244495D800EF8317 /* Configuration */ = { - isa = PBXGroup; - children = ( - 45EA4F3D24439C2400622606 /* Info.plist */, - 45EA4F3824439C2400622606 /* Assets.xcassets */, - ); - path = Configuration; - sourceTree = ""; - }; - 4524351F244495EC00EF8317 /* Launch */ = { - isa = PBXGroup; - children = ( - 45EA4F3A24439C2400622606 /* LaunchScreen.storyboard */, - 45EA4F2F24439C2300622606 /* AppDelegate.swift */, - 45EA4F3124439C2300622606 /* SceneDelegate.swift */, - ); - path = Launch; - sourceTree = ""; - }; - 45EA4F2324439C2300622606 = { - isa = PBXGroup; - children = ( - 45EA4F2E24439C2300622606 /* Sources */, - 45EA4F2D24439C2300622606 /* Products */, - ); - sourceTree = ""; - }; - 45EA4F2D24439C2300622606 /* Products */ = { - isa = PBXGroup; - children = ( - 45EA4F2C24439C2300622606 /* gRPC Example.app */, - ); - name = Products; - sourceTree = ""; - }; - 45EA4F2E24439C2300622606 /* Sources */ = { - isa = PBXGroup; - children = ( - E7726D72282887B3006686BE /* Generated */, - 4524351F244495EC00EF8317 /* Launch */, - 4524351E244495D800EF8317 /* Configuration */, - 45EA4F3324439C2300622606 /* ViewController.swift */, - 458EDDB22444997700BEBFD3 /* SpeechService.swift */, - 45600AC32445365000180DCE /* AudioStreamManager.swift */, - 45A75D532446506000ACB73B /* Constants.swift */, - ); - path = Sources; - sourceTree = ""; - }; - E7726D72282887B3006686BE /* Generated */ = { - isa = PBXGroup; - children = ( - E7726D76282887B3006686BE /* annotations.pb.swift */, - E7726D7A282887B3006686BE /* client.pb.swift */, - E7726D73282887B3006686BE /* cloud_speech.grpc.swift */, - E7726D75282887B3006686BE /* cloud_speech.pb.swift */, - E7726D78282887B3006686BE /* http.pb.swift */, - E7726D77282887B3006686BE /* operations.grpc.swift */, - E7726D79282887B3006686BE /* operations.pb.swift */, - E7DB07D028288C160049B966 /* resource.pb.swift */, - E7726D74282887B3006686BE /* status.pb.swift */, - ); - path = Generated; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 45EA4F2B24439C2300622606 /* SpeechToText-gRPC-iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 45EA4F4024439C2400622606 /* Build configuration list for PBXNativeTarget "SpeechToText-gRPC-iOS" */; - buildPhases = ( - 45EA4F2824439C2300622606 /* Sources */, - 45EA4F2924439C2300622606 /* Frameworks */, - 45EA4F2A24439C2300622606 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "SpeechToText-gRPC-iOS"; - packageProductDependencies = ( - E7726D8428288825006686BE /* GRPC */, - E7726D8728288850006686BE /* SnapKit */, - ); - productName = "SpeechToText-gRPC-iOS"; - productReference = 45EA4F2C24439C2300622606 /* gRPC Example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 45EA4F2424439C2300622606 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1330; - ORGANIZATIONNAME = "Prickett, Jacob (J.A.)"; - TargetAttributes = { - 45EA4F2B24439C2300622606 = { - CreatedOnToolsVersion = 11.3; - }; - }; - }; - buildConfigurationList = 45EA4F2724439C2300622606 /* Build configuration list for PBXProject "SpeechToText-gRPC-iOS" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 45EA4F2324439C2300622606; - packageReferences = ( - E7726D8328288825006686BE /* XCRemoteSwiftPackageReference "grpc-swift" */, - E7726D8628288850006686BE /* XCRemoteSwiftPackageReference "SnapKit" */, - ); - productRefGroup = 45EA4F2D24439C2300622606 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 45EA4F2B24439C2300622606 /* SpeechToText-gRPC-iOS */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 45EA4F2A24439C2300622606 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 45EA4F3C24439C2400622606 /* LaunchScreen.storyboard in Resources */, - 45EA4F3924439C2400622606 /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 45EA4F2824439C2300622606 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 45600AC42445365000180DCE /* AudioStreamManager.swift in Sources */, - 45EA4F3424439C2300622606 /* ViewController.swift in Sources */, - E7DB07D128288C160049B966 /* resource.pb.swift in Sources */, - E7726D7D282887B3006686BE /* cloud_speech.pb.swift in Sources */, - E7726D81282887B3006686BE /* operations.pb.swift in Sources */, - 45EA4F3024439C2300622606 /* AppDelegate.swift in Sources */, - E7726D80282887B3006686BE /* http.pb.swift in Sources */, - E7726D7F282887B3006686BE /* operations.grpc.swift in Sources */, - E7726D82282887B3006686BE /* client.pb.swift in Sources */, - 458EDDB32444997700BEBFD3 /* SpeechService.swift in Sources */, - 45EA4F3224439C2300622606 /* SceneDelegate.swift in Sources */, - E7726D7B282887B3006686BE /* cloud_speech.grpc.swift in Sources */, - E7726D7C282887B3006686BE /* status.pb.swift in Sources */, - 45A75D542446506000ACB73B /* Constants.swift in Sources */, - E7726D7E282887B3006686BE /* annotations.pb.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 45EA4F3A24439C2400622606 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 45EA4F3B24439C2400622606 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 45EA4F3E24439C2400622606 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 45EA4F3F24439C2400622606 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 45EA4F4124439C2400622606 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 24DLJQTAR9; - INFOPLIST_FILE = "$(SRCROOT)/Sources/Configuration/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.jakeprickett.SpeechToText-gRPC-iOS"; - PRODUCT_NAME = "gRPC Example"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Debug; - }; - 45EA4F4224439C2400622606 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 24DLJQTAR9; - INFOPLIST_FILE = "$(SRCROOT)/Sources/Configuration/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.jakeprickett.SpeechToText-gRPC-iOS"; - PRODUCT_NAME = "gRPC Example"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 45EA4F2724439C2300622606 /* Build configuration list for PBXProject "SpeechToText-gRPC-iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 45EA4F3E24439C2400622606 /* Debug */, - 45EA4F3F24439C2400622606 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Debug; - }; - 45EA4F4024439C2400622606 /* Build configuration list for PBXNativeTarget "SpeechToText-gRPC-iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 45EA4F4124439C2400622606 /* Debug */, - 45EA4F4224439C2400622606 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Debug; - }; -/* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - E7726D8328288825006686BE /* XCRemoteSwiftPackageReference "grpc-swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/grpc/grpc-swift.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; - E7726D8628288850006686BE /* XCRemoteSwiftPackageReference "SnapKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SnapKit/SnapKit"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.0.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - E7726D8428288825006686BE /* GRPC */ = { - isa = XCSwiftPackageProductDependency; - package = E7726D8328288825006686BE /* XCRemoteSwiftPackageReference "grpc-swift" */; - productName = GRPC; - }; - E7726D8728288850006686BE /* SnapKit */ = { - isa = XCSwiftPackageProductDependency; - package = E7726D8628288850006686BE /* XCRemoteSwiftPackageReference "SnapKit" */; - productName = SnapKit; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 45EA4F2424439C2300622606 /* Project object */; -} diff --git a/Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/xcshareddata/xcschemes/SpeechToText-gRPC-iOS.xcscheme b/Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/xcshareddata/xcschemes/SpeechToText-gRPC-iOS.xcscheme deleted file mode 100644 index 288db6262..000000000 --- a/Examples/Google/SpeechToText/SpeechToText-gRPC-iOS.xcodeproj/xcshareddata/xcschemes/SpeechToText-gRPC-iOS.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/Google/common/include/google/protobuf/any.proto b/Examples/Google/common/include/google/protobuf/any.proto deleted file mode 100755 index c74866762..000000000 --- a/Examples/Google/common/include/google/protobuf/any.proto +++ /dev/null @@ -1,149 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option go_package = "github.com/golang/protobuf/ptypes/any"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "AnyProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// `Any` contains an arbitrary serialized protocol buffer message along with a -// URL that describes the type of the serialized message. -// -// Protobuf library provides support to pack/unpack Any values in the form -// of utility functions or additional generated methods of the Any type. -// -// Example 1: Pack and unpack a message in C++. -// -// Foo foo = ...; -// Any any; -// any.PackFrom(foo); -// ... -// if (any.UnpackTo(&foo)) { -// ... -// } -// -// Example 2: Pack and unpack a message in Java. -// -// Foo foo = ...; -// Any any = Any.pack(foo); -// ... -// if (any.is(Foo.class)) { -// foo = any.unpack(Foo.class); -// } -// -// Example 3: Pack and unpack a message in Python. -// -// foo = Foo(...) -// any = Any() -// any.Pack(foo) -// ... -// if any.Is(Foo.DESCRIPTOR): -// any.Unpack(foo) -// ... -// -// Example 4: Pack and unpack a message in Go -// -// foo := &pb.Foo{...} -// any, err := ptypes.MarshalAny(foo) -// ... -// foo := &pb.Foo{} -// if err := ptypes.UnmarshalAny(any, foo); err != nil { -// ... -// } -// -// The pack methods provided by protobuf library will by default use -// 'type.googleapis.com/full.type.name' as the type URL and the unpack -// methods only use the fully qualified type name after the last '/' -// in the type URL, for example "foo.bar.com/x/y.z" will yield type -// name "y.z". -// -// -// JSON -// ==== -// The JSON representation of an `Any` value uses the regular -// representation of the deserialized, embedded message, with an -// additional field `@type` which contains the type URL. Example: -// -// package google.profile; -// message Person { -// string first_name = 1; -// string last_name = 2; -// } -// -// { -// "@type": "type.googleapis.com/google.profile.Person", -// "firstName": , -// "lastName": -// } -// -// If the embedded message type is well-known and has a custom JSON -// representation, that representation will be embedded adding a field -// `value` which holds the custom JSON in addition to the `@type` -// field. Example (for message [google.protobuf.Duration][]): -// -// { -// "@type": "type.googleapis.com/google.protobuf.Duration", -// "value": "1.212s" -// } -// -message Any { - // A URL/resource name whose content describes the type of the - // serialized protocol buffer message. - // - // For URLs which use the scheme `http`, `https`, or no scheme, the - // following restrictions and interpretations apply: - // - // * If no scheme is provided, `https` is assumed. - // * The last segment of the URL's path must represent the fully - // qualified name of the type (as in `path/google.protobuf.Duration`). - // The name should be in a canonical form (e.g., leading "." is - // not accepted). - // * An HTTP GET on the URL must yield a [google.protobuf.Type][] - // value in binary format, or produce an error. - // * Applications are allowed to cache lookup results based on the - // URL, or have them precompiled into a binary to avoid any - // lookup. Therefore, binary compatibility needs to be preserved - // on changes to types. (Use versioned type names to manage - // breaking changes.) - // - // Schemes other than `http`, `https` (or the empty scheme) might be - // used with implementation specific semantics. - // - string type_url = 1; - - // Must be a valid serialized protocol buffer of the above specified type. - bytes value = 2; -} diff --git a/Examples/Google/common/include/google/protobuf/api.proto b/Examples/Google/common/include/google/protobuf/api.proto deleted file mode 100755 index f37ee2fa4..000000000 --- a/Examples/Google/common/include/google/protobuf/api.proto +++ /dev/null @@ -1,210 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -import "google/protobuf/source_context.proto"; -import "google/protobuf/type.proto"; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "ApiProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/genproto/protobuf/api;api"; - -// Api is a light-weight descriptor for an API Interface. -// -// Interfaces are also described as "protocol buffer services" in some contexts, -// such as by the "service" keyword in a .proto file, but they are different -// from API Services, which represent a concrete implementation of an interface -// as opposed to simply a description of methods and bindings. They are also -// sometimes simply referred to as "APIs" in other contexts, such as the name of -// this message itself. See https://cloud.google.com/apis/design/glossary for -// detailed terminology. -message Api { - - // The fully qualified name of this interface, including package name - // followed by the interface's simple name. - string name = 1; - - // The methods of this interface, in unspecified order. - repeated Method methods = 2; - - // Any metadata attached to the interface. - repeated Option options = 3; - - // A version string for this interface. If specified, must have the form - // `major-version.minor-version`, as in `1.10`. If the minor version is - // omitted, it defaults to zero. If the entire version field is empty, the - // major version is derived from the package name, as outlined below. If the - // field is not empty, the version in the package name will be verified to be - // consistent with what is provided here. - // - // The versioning schema uses [semantic - // versioning](http://semver.org) where the major version number - // indicates a breaking change and the minor version an additive, - // non-breaking change. Both version numbers are signals to users - // what to expect from different versions, and should be carefully - // chosen based on the product plan. - // - // The major version is also reflected in the package name of the - // interface, which must end in `v`, as in - // `google.feature.v1`. For major versions 0 and 1, the suffix can - // be omitted. Zero major versions must only be used for - // experimental, non-GA interfaces. - // - // - string version = 4; - - // Source context for the protocol buffer service represented by this - // message. - SourceContext source_context = 5; - - // Included interfaces. See [Mixin][]. - repeated Mixin mixins = 6; - - // The source syntax of the service. - Syntax syntax = 7; -} - -// Method represents a method of an API interface. -message Method { - - // The simple name of this method. - string name = 1; - - // A URL of the input message type. - string request_type_url = 2; - - // If true, the request is streamed. - bool request_streaming = 3; - - // The URL of the output message type. - string response_type_url = 4; - - // If true, the response is streamed. - bool response_streaming = 5; - - // Any metadata attached to the method. - repeated Option options = 6; - - // The source syntax of this method. - Syntax syntax = 7; -} - -// Declares an API Interface to be included in this interface. The including -// interface must redeclare all the methods from the included interface, but -// documentation and options are inherited as follows: -// -// - If after comment and whitespace stripping, the documentation -// string of the redeclared method is empty, it will be inherited -// from the original method. -// -// - Each annotation belonging to the service config (http, -// visibility) which is not set in the redeclared method will be -// inherited. -// -// - If an http annotation is inherited, the path pattern will be -// modified as follows. Any version prefix will be replaced by the -// version of the including interface plus the [root][] path if -// specified. -// -// Example of a simple mixin: -// -// package google.acl.v1; -// service AccessControl { -// // Get the underlying ACL object. -// rpc GetAcl(GetAclRequest) returns (Acl) { -// option (google.api.http).get = "/v1/{resource=**}:getAcl"; -// } -// } -// -// package google.storage.v2; -// service Storage { -// rpc GetAcl(GetAclRequest) returns (Acl); -// -// // Get a data record. -// rpc GetData(GetDataRequest) returns (Data) { -// option (google.api.http).get = "/v2/{resource=**}"; -// } -// } -// -// Example of a mixin configuration: -// -// apis: -// - name: google.storage.v2.Storage -// mixins: -// - name: google.acl.v1.AccessControl -// -// The mixin construct implies that all methods in `AccessControl` are -// also declared with same name and request/response types in -// `Storage`. A documentation generator or annotation processor will -// see the effective `Storage.GetAcl` method after inherting -// documentation and annotations as follows: -// -// service Storage { -// // Get the underlying ACL object. -// rpc GetAcl(GetAclRequest) returns (Acl) { -// option (google.api.http).get = "/v2/{resource=**}:getAcl"; -// } -// ... -// } -// -// Note how the version in the path pattern changed from `v1` to `v2`. -// -// If the `root` field in the mixin is specified, it should be a -// relative path under which inherited HTTP paths are placed. Example: -// -// apis: -// - name: google.storage.v2.Storage -// mixins: -// - name: google.acl.v1.AccessControl -// root: acls -// -// This implies the following inherited HTTP annotation: -// -// service Storage { -// // Get the underlying ACL object. -// rpc GetAcl(GetAclRequest) returns (Acl) { -// option (google.api.http).get = "/v2/acls/{resource=**}:getAcl"; -// } -// ... -// } -message Mixin { - // The fully qualified name of the interface which is included. - string name = 1; - - // If non-empty specifies a path under which inherited HTTP paths - // are rooted. - string root = 2; -} diff --git a/Examples/Google/common/include/google/protobuf/compiler/plugin.proto b/Examples/Google/common/include/google/protobuf/compiler/plugin.proto deleted file mode 100755 index 5b5574529..000000000 --- a/Examples/Google/common/include/google/protobuf/compiler/plugin.proto +++ /dev/null @@ -1,167 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// Author: kenton@google.com (Kenton Varda) -// -// WARNING: The plugin interface is currently EXPERIMENTAL and is subject to -// change. -// -// protoc (aka the Protocol Compiler) can be extended via plugins. A plugin is -// just a program that reads a CodeGeneratorRequest from stdin and writes a -// CodeGeneratorResponse to stdout. -// -// Plugins written using C++ can use google/protobuf/compiler/plugin.h instead -// of dealing with the raw protocol defined here. -// -// A plugin executable needs only to be placed somewhere in the path. The -// plugin should be named "protoc-gen-$NAME", and will then be used when the -// flag "--${NAME}_out" is passed to protoc. - -syntax = "proto2"; -package google.protobuf.compiler; -option java_package = "com.google.protobuf.compiler"; -option java_outer_classname = "PluginProtos"; - -option go_package = "github.com/golang/protobuf/protoc-gen-go/plugin;plugin_go"; - -import "google/protobuf/descriptor.proto"; - -// The version number of protocol compiler. -message Version { - optional int32 major = 1; - optional int32 minor = 2; - optional int32 patch = 3; - // A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should - // be empty for mainline stable releases. - optional string suffix = 4; -} - -// An encoded CodeGeneratorRequest is written to the plugin's stdin. -message CodeGeneratorRequest { - // The .proto files that were explicitly listed on the command-line. The - // code generator should generate code only for these files. Each file's - // descriptor will be included in proto_file, below. - repeated string file_to_generate = 1; - - // The generator parameter passed on the command-line. - optional string parameter = 2; - - // FileDescriptorProtos for all files in files_to_generate and everything - // they import. The files will appear in topological order, so each file - // appears before any file that imports it. - // - // protoc guarantees that all proto_files will be written after - // the fields above, even though this is not technically guaranteed by the - // protobuf wire format. This theoretically could allow a plugin to stream - // in the FileDescriptorProtos and handle them one by one rather than read - // the entire set into memory at once. However, as of this writing, this - // is not similarly optimized on protoc's end -- it will store all fields in - // memory at once before sending them to the plugin. - // - // Type names of fields and extensions in the FileDescriptorProto are always - // fully qualified. - repeated FileDescriptorProto proto_file = 15; - - // The version number of protocol compiler. - optional Version compiler_version = 3; - -} - -// The plugin writes an encoded CodeGeneratorResponse to stdout. -message CodeGeneratorResponse { - // Error message. If non-empty, code generation failed. The plugin process - // should exit with status code zero even if it reports an error in this way. - // - // This should be used to indicate errors in .proto files which prevent the - // code generator from generating correct code. Errors which indicate a - // problem in protoc itself -- such as the input CodeGeneratorRequest being - // unparseable -- should be reported by writing a message to stderr and - // exiting with a non-zero status code. - optional string error = 1; - - // Represents a single generated file. - message File { - // The file name, relative to the output directory. The name must not - // contain "." or ".." components and must be relative, not be absolute (so, - // the file cannot lie outside the output directory). "/" must be used as - // the path separator, not "\". - // - // If the name is omitted, the content will be appended to the previous - // file. This allows the generator to break large files into small chunks, - // and allows the generated text to be streamed back to protoc so that large - // files need not reside completely in memory at one time. Note that as of - // this writing protoc does not optimize for this -- it will read the entire - // CodeGeneratorResponse before writing files to disk. - optional string name = 1; - - // If non-empty, indicates that the named file should already exist, and the - // content here is to be inserted into that file at a defined insertion - // point. This feature allows a code generator to extend the output - // produced by another code generator. The original generator may provide - // insertion points by placing special annotations in the file that look - // like: - // @@protoc_insertion_point(NAME) - // The annotation can have arbitrary text before and after it on the line, - // which allows it to be placed in a comment. NAME should be replaced with - // an identifier naming the point -- this is what other generators will use - // as the insertion_point. Code inserted at this point will be placed - // immediately above the line containing the insertion point (thus multiple - // insertions to the same point will come out in the order they were added). - // The double-@ is intended to make it unlikely that the generated code - // could contain things that look like insertion points by accident. - // - // For example, the C++ code generator places the following line in the - // .pb.h files that it generates: - // // @@protoc_insertion_point(namespace_scope) - // This line appears within the scope of the file's package namespace, but - // outside of any particular class. Another plugin can then specify the - // insertion_point "namespace_scope" to generate additional classes or - // other declarations that should be placed in this scope. - // - // Note that if the line containing the insertion point begins with - // whitespace, the same whitespace will be added to every line of the - // inserted text. This is useful for languages like Python, where - // indentation matters. In these languages, the insertion point comment - // should be indented the same amount as any inserted code will need to be - // in order to work correctly in that context. - // - // The code generator that generates the initial file and the one which - // inserts into it must both run as part of a single invocation of protoc. - // Code generators are executed in the order in which they appear on the - // command line. - // - // If |insertion_point| is present, |name| must also be present. - optional string insertion_point = 2; - - // The file contents. - optional string content = 15; - } - repeated File file = 15; -} diff --git a/Examples/Google/common/include/google/protobuf/descriptor.proto b/Examples/Google/common/include/google/protobuf/descriptor.proto deleted file mode 100755 index f1ec5735d..000000000 --- a/Examples/Google/common/include/google/protobuf/descriptor.proto +++ /dev/null @@ -1,849 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// Author: kenton@google.com (Kenton Varda) -// Based on original Protocol Buffers design by -// Sanjay Ghemawat, Jeff Dean, and others. -// -// The messages in this file describe the definitions found in .proto files. -// A valid .proto file can be translated directly to a FileDescriptorProto -// without any other information (e.g. without reading its imports). - - -syntax = "proto2"; - -package google.protobuf; -option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "DescriptorProtos"; -option csharp_namespace = "Google.Protobuf.Reflection"; -option objc_class_prefix = "GPB"; - -// descriptor.proto must be optimized for speed because reflection-based -// algorithms don't work during bootstrapping. -option optimize_for = SPEED; - -// The protocol compiler can output a FileDescriptorSet containing the .proto -// files it parses. -message FileDescriptorSet { - repeated FileDescriptorProto file = 1; -} - -// Describes a complete .proto file. -message FileDescriptorProto { - optional string name = 1; // file name, relative to root of source tree - optional string package = 2; // e.g. "foo", "foo.bar", etc. - - // Names of files imported by this file. - repeated string dependency = 3; - // Indexes of the public imported files in the dependency list above. - repeated int32 public_dependency = 10; - // Indexes of the weak imported files in the dependency list. - // For Google-internal migration only. Do not use. - repeated int32 weak_dependency = 11; - - // All top-level definitions in this file. - repeated DescriptorProto message_type = 4; - repeated EnumDescriptorProto enum_type = 5; - repeated ServiceDescriptorProto service = 6; - repeated FieldDescriptorProto extension = 7; - - optional FileOptions options = 8; - - // This field contains optional information about the original source code. - // You may safely remove this entire field without harming runtime - // functionality of the descriptors -- the information is needed only by - // development tools. - optional SourceCodeInfo source_code_info = 9; - - // The syntax of the proto file. - // The supported values are "proto2" and "proto3". - optional string syntax = 12; -} - -// Describes a message type. -message DescriptorProto { - optional string name = 1; - - repeated FieldDescriptorProto field = 2; - repeated FieldDescriptorProto extension = 6; - - repeated DescriptorProto nested_type = 3; - repeated EnumDescriptorProto enum_type = 4; - - message ExtensionRange { - optional int32 start = 1; - optional int32 end = 2; - - optional ExtensionRangeOptions options = 3; - } - repeated ExtensionRange extension_range = 5; - - repeated OneofDescriptorProto oneof_decl = 8; - - optional MessageOptions options = 7; - - // Range of reserved tag numbers. Reserved tag numbers may not be used by - // fields or extension ranges in the same message. Reserved ranges may - // not overlap. - message ReservedRange { - optional int32 start = 1; // Inclusive. - optional int32 end = 2; // Exclusive. - } - repeated ReservedRange reserved_range = 9; - // Reserved field names, which may not be used by fields in the same message. - // A given name may only be reserved once. - repeated string reserved_name = 10; -} - -message ExtensionRangeOptions { - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -// Describes a field within a message. -message FieldDescriptorProto { - enum Type { - // 0 is reserved for errors. - // Order is weird for historical reasons. - TYPE_DOUBLE = 1; - TYPE_FLOAT = 2; - // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if - // negative values are likely. - TYPE_INT64 = 3; - TYPE_UINT64 = 4; - // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if - // negative values are likely. - TYPE_INT32 = 5; - TYPE_FIXED64 = 6; - TYPE_FIXED32 = 7; - TYPE_BOOL = 8; - TYPE_STRING = 9; - // Tag-delimited aggregate. - // Group type is deprecated and not supported in proto3. However, Proto3 - // implementations should still be able to parse the group wire format and - // treat group fields as unknown fields. - TYPE_GROUP = 10; - TYPE_MESSAGE = 11; // Length-delimited aggregate. - - // New in version 2. - TYPE_BYTES = 12; - TYPE_UINT32 = 13; - TYPE_ENUM = 14; - TYPE_SFIXED32 = 15; - TYPE_SFIXED64 = 16; - TYPE_SINT32 = 17; // Uses ZigZag encoding. - TYPE_SINT64 = 18; // Uses ZigZag encoding. - }; - - enum Label { - // 0 is reserved for errors - LABEL_OPTIONAL = 1; - LABEL_REQUIRED = 2; - LABEL_REPEATED = 3; - }; - - optional string name = 1; - optional int32 number = 3; - optional Label label = 4; - - // If type_name is set, this need not be set. If both this and type_name - // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. - optional Type type = 5; - - // For message and enum types, this is the name of the type. If the name - // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping - // rules are used to find the type (i.e. first the nested types within this - // message are searched, then within the parent, on up to the root - // namespace). - optional string type_name = 6; - - // For extensions, this is the name of the type being extended. It is - // resolved in the same manner as type_name. - optional string extendee = 2; - - // For numeric types, contains the original text representation of the value. - // For booleans, "true" or "false". - // For strings, contains the default text contents (not escaped in any way). - // For bytes, contains the C escaped value. All bytes >= 128 are escaped. - // TODO(kenton): Base-64 encode? - optional string default_value = 7; - - // If set, gives the index of a oneof in the containing type's oneof_decl - // list. This field is a member of that oneof. - optional int32 oneof_index = 9; - - // JSON name of this field. The value is set by protocol compiler. If the - // user has set a "json_name" option on this field, that option's value - // will be used. Otherwise, it's deduced from the field's name by converting - // it to camelCase. - optional string json_name = 10; - - optional FieldOptions options = 8; -} - -// Describes a oneof. -message OneofDescriptorProto { - optional string name = 1; - optional OneofOptions options = 2; -} - -// Describes an enum type. -message EnumDescriptorProto { - optional string name = 1; - - repeated EnumValueDescriptorProto value = 2; - - optional EnumOptions options = 3; -} - -// Describes a value within an enum. -message EnumValueDescriptorProto { - optional string name = 1; - optional int32 number = 2; - - optional EnumValueOptions options = 3; -} - -// Describes a service. -message ServiceDescriptorProto { - optional string name = 1; - repeated MethodDescriptorProto method = 2; - - optional ServiceOptions options = 3; -} - -// Describes a method of a service. -message MethodDescriptorProto { - optional string name = 1; - - // Input and output type names. These are resolved in the same way as - // FieldDescriptorProto.type_name, but must refer to a message type. - optional string input_type = 2; - optional string output_type = 3; - - optional MethodOptions options = 4; - - // Identifies if client streams multiple client messages - optional bool client_streaming = 5 [default=false]; - // Identifies if server streams multiple server messages - optional bool server_streaming = 6 [default=false]; -} - - -// =================================================================== -// Options - -// Each of the definitions above may have "options" attached. These are -// just annotations which may cause code to be generated slightly differently -// or may contain hints for code that manipulates protocol messages. -// -// Clients may define custom options as extensions of the *Options messages. -// These extensions may not yet be known at parsing time, so the parser cannot -// store the values in them. Instead it stores them in a field in the *Options -// message called uninterpreted_option. This field must have the same name -// across all *Options messages. We then use this field to populate the -// extensions when we build a descriptor, at which point all protos have been -// parsed and so all extensions are known. -// -// Extension numbers for custom options may be chosen as follows: -// * For options which will only be used within a single application or -// organization, or for experimental options, use field numbers 50000 -// through 99999. It is up to you to ensure that you do not use the -// same number for multiple options. -// * For options which will be published and used publicly by multiple -// independent entities, e-mail protobuf-global-extension-registry@google.com -// to reserve extension numbers. Simply provide your project name (e.g. -// Objective-C plugin) and your project website (if available) -- there's no -// need to explain how you intend to use them. Usually you only need one -// extension number. You can declare multiple options with only one extension -// number by putting them in a sub-message. See the Custom Options section of -// the docs for examples: -// https://developers.google.com/protocol-buffers/docs/proto#options -// If this turns out to be popular, a web service will be set up -// to automatically assign option numbers. - - -message FileOptions { - - // Sets the Java package where classes generated from this .proto will be - // placed. By default, the proto package is used, but this is often - // inappropriate because proto packages do not normally start with backwards - // domain names. - optional string java_package = 1; - - - // If set, all the classes from the .proto file are wrapped in a single - // outer class with the given name. This applies to both Proto1 - // (equivalent to the old "--one_java_file" option) and Proto2 (where - // a .proto always translates to a single class, but you may want to - // explicitly choose the class name). - optional string java_outer_classname = 8; - - // If set true, then the Java code generator will generate a separate .java - // file for each top-level message, enum, and service defined in the .proto - // file. Thus, these types will *not* be nested inside the outer class - // named by java_outer_classname. However, the outer class will still be - // generated to contain the file's getDescriptor() method as well as any - // top-level extensions defined in the file. - optional bool java_multiple_files = 10 [default=false]; - - // This option does nothing. - optional bool java_generate_equals_and_hash = 20 [deprecated=true]; - - // If set true, then the Java2 code generator will generate code that - // throws an exception whenever an attempt is made to assign a non-UTF-8 - // byte sequence to a string field. - // Message reflection will do the same. - // However, an extension field still accepts non-UTF-8 byte sequences. - // This option has no effect on when used with the lite runtime. - optional bool java_string_check_utf8 = 27 [default=false]; - - - // Generated classes can be optimized for speed or code size. - enum OptimizeMode { - SPEED = 1; // Generate complete code for parsing, serialization, - // etc. - CODE_SIZE = 2; // Use ReflectionOps to implement these methods. - LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. - } - optional OptimizeMode optimize_for = 9 [default=SPEED]; - - // Sets the Go package where structs generated from this .proto will be - // placed. If omitted, the Go package will be derived from the following: - // - The basename of the package import path, if provided. - // - Otherwise, the package statement in the .proto file, if present. - // - Otherwise, the basename of the .proto file, without extension. - optional string go_package = 11; - - - - // Should generic services be generated in each language? "Generic" services - // are not specific to any particular RPC system. They are generated by the - // main code generators in each language (without additional plugins). - // Generic services were the only kind of service generation supported by - // early versions of google.protobuf. - // - // Generic services are now considered deprecated in favor of using plugins - // that generate code specific to your particular RPC system. Therefore, - // these default to false. Old code which depends on generic services should - // explicitly set them to true. - optional bool cc_generic_services = 16 [default=false]; - optional bool java_generic_services = 17 [default=false]; - optional bool py_generic_services = 18 [default=false]; - optional bool php_generic_services = 19 [default=false]; - - // Is this file deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for everything in the file, or it will be completely ignored; in the very - // least, this is a formalization for deprecating files. - optional bool deprecated = 23 [default=false]; - - // Enables the use of arenas for the proto messages in this file. This applies - // only to generated classes for C++. - optional bool cc_enable_arenas = 31 [default=false]; - - - // Sets the objective c class prefix which is prepended to all objective c - // generated classes from this .proto. There is no default. - optional string objc_class_prefix = 36; - - // Namespace for generated classes; defaults to the package. - optional string csharp_namespace = 37; - - // By default Swift generators will take the proto package and CamelCase it - // replacing '.' with underscore and use that to prefix the types/symbols - // defined. When this options is provided, they will use this value instead - // to prefix the types/symbols defined. - optional string swift_prefix = 39; - - // Sets the php class prefix which is prepended to all php generated classes - // from this .proto. Default is empty. - optional string php_class_prefix = 40; - - // Use this option to change the namespace of php generated classes. Default - // is empty. When this option is empty, the package name will be used for - // determining the namespace. - optional string php_namespace = 41; - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; - - reserved 38; -} - -message MessageOptions { - // Set true to use the old proto1 MessageSet wire format for extensions. - // This is provided for backwards-compatibility with the MessageSet wire - // format. You should not use this for any other reason: It's less - // efficient, has fewer features, and is more complicated. - // - // The message must be defined exactly as follows: - // message Foo { - // option message_set_wire_format = true; - // extensions 4 to max; - // } - // Note that the message cannot have any defined fields; MessageSets only - // have extensions. - // - // All extensions of your type must be singular messages; e.g. they cannot - // be int32s, enums, or repeated messages. - // - // Because this is an option, the above two restrictions are not enforced by - // the protocol compiler. - optional bool message_set_wire_format = 1 [default=false]; - - // Disables the generation of the standard "descriptor()" accessor, which can - // conflict with a field of the same name. This is meant to make migration - // from proto1 easier; new code should avoid fields named "descriptor". - optional bool no_standard_descriptor_accessor = 2 [default=false]; - - // Is this message deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the message, or it will be completely ignored; in the very least, - // this is a formalization for deprecating messages. - optional bool deprecated = 3 [default=false]; - - // Whether the message is an automatically generated map entry type for the - // maps field. - // - // For maps fields: - // map map_field = 1; - // The parsed descriptor looks like: - // message MapFieldEntry { - // option map_entry = true; - // optional KeyType key = 1; - // optional ValueType value = 2; - // } - // repeated MapFieldEntry map_field = 1; - // - // Implementations may choose not to generate the map_entry=true message, but - // use a native map in the target language to hold the keys and values. - // The reflection APIs in such implementions still need to work as - // if the field is a repeated message field. - // - // NOTE: Do not set the option in .proto files. Always use the maps syntax - // instead. The option should only be implicitly set by the proto compiler - // parser. - optional bool map_entry = 7; - - reserved 8; // javalite_serializable - reserved 9; // javanano_as_lite - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message FieldOptions { - // The ctype option instructs the C++ code generator to use a different - // representation of the field than it normally would. See the specific - // options below. This option is not yet implemented in the open source - // release -- sorry, we'll try to include it in a future version! - optional CType ctype = 1 [default = STRING]; - enum CType { - // Default mode. - STRING = 0; - - CORD = 1; - - STRING_PIECE = 2; - } - // The packed option can be enabled for repeated primitive fields to enable - // a more efficient representation on the wire. Rather than repeatedly - // writing the tag and type for each element, the entire array is encoded as - // a single length-delimited blob. In proto3, only explicit setting it to - // false will avoid using packed encoding. - optional bool packed = 2; - - // The jstype option determines the JavaScript type used for values of the - // field. The option is permitted only for 64 bit integral and fixed types - // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING - // is represented as JavaScript string, which avoids loss of precision that - // can happen when a large value is converted to a floating point JavaScript. - // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to - // use the JavaScript "number" type. The behavior of the default option - // JS_NORMAL is implementation dependent. - // - // This option is an enum to permit additional types to be added, e.g. - // goog.math.Integer. - optional JSType jstype = 6 [default = JS_NORMAL]; - enum JSType { - // Use the default type. - JS_NORMAL = 0; - - // Use JavaScript strings. - JS_STRING = 1; - - // Use JavaScript numbers. - JS_NUMBER = 2; - } - - // Should this field be parsed lazily? Lazy applies only to message-type - // fields. It means that when the outer message is initially parsed, the - // inner message's contents will not be parsed but instead stored in encoded - // form. The inner message will actually be parsed when it is first accessed. - // - // This is only a hint. Implementations are free to choose whether to use - // eager or lazy parsing regardless of the value of this option. However, - // setting this option true suggests that the protocol author believes that - // using lazy parsing on this field is worth the additional bookkeeping - // overhead typically needed to implement it. - // - // This option does not affect the public interface of any generated code; - // all method signatures remain the same. Furthermore, thread-safety of the - // interface is not affected by this option; const methods remain safe to - // call from multiple threads concurrently, while non-const methods continue - // to require exclusive access. - // - // - // Note that implementations may choose not to check required fields within - // a lazy sub-message. That is, calling IsInitialized() on the outer message - // may return true even if the inner message has missing required fields. - // This is necessary because otherwise the inner message would have to be - // parsed in order to perform the check, defeating the purpose of lazy - // parsing. An implementation which chooses not to check required fields - // must be consistent about it. That is, for any particular sub-message, the - // implementation must either *always* check its required fields, or *never* - // check its required fields, regardless of whether or not the message has - // been parsed. - optional bool lazy = 5 [default=false]; - - // Is this field deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for accessors, or it will be completely ignored; in the very least, this - // is a formalization for deprecating fields. - optional bool deprecated = 3 [default=false]; - - // For Google-internal migration only. Do not use. - optional bool weak = 10 [default=false]; - - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; - - reserved 4; // removed jtype -} - -message OneofOptions { - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message EnumOptions { - - // Set this option to true to allow mapping different tag names to the same - // value. - optional bool allow_alias = 2; - - // Is this enum deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the enum, or it will be completely ignored; in the very least, this - // is a formalization for deprecating enums. - optional bool deprecated = 3 [default=false]; - - reserved 5; // javanano_as_lite - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message EnumValueOptions { - // Is this enum value deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the enum value, or it will be completely ignored; in the very least, - // this is a formalization for deprecating enum values. - optional bool deprecated = 1 [default=false]; - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message ServiceOptions { - - // Note: Field numbers 1 through 32 are reserved for Google's internal RPC - // framework. We apologize for hoarding these numbers to ourselves, but - // we were already using them long before we decided to release Protocol - // Buffers. - - // Is this service deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the service, or it will be completely ignored; in the very least, - // this is a formalization for deprecating services. - optional bool deprecated = 33 [default=false]; - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - -message MethodOptions { - - // Note: Field numbers 1 through 32 are reserved for Google's internal RPC - // framework. We apologize for hoarding these numbers to ourselves, but - // we were already using them long before we decided to release Protocol - // Buffers. - - // Is this method deprecated? - // Depending on the target platform, this can emit Deprecated annotations - // for the method, or it will be completely ignored; in the very least, - // this is a formalization for deprecating methods. - optional bool deprecated = 33 [default=false]; - - // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, - // or neither? HTTP based RPC implementation may choose GET verb for safe - // methods, and PUT verb for idempotent methods instead of the default POST. - enum IdempotencyLevel { - IDEMPOTENCY_UNKNOWN = 0; - NO_SIDE_EFFECTS = 1; // implies idempotent - IDEMPOTENT = 2; // idempotent, but may have side effects - } - optional IdempotencyLevel idempotency_level = - 34 [default=IDEMPOTENCY_UNKNOWN]; - - // The parser stores options it doesn't recognize here. See above. - repeated UninterpretedOption uninterpreted_option = 999; - - // Clients can define custom options in extensions of this message. See above. - extensions 1000 to max; -} - - -// A message representing a option the parser does not recognize. This only -// appears in options protos created by the compiler::Parser class. -// DescriptorPool resolves these when building Descriptor objects. Therefore, -// options protos in descriptor objects (e.g. returned by Descriptor::options(), -// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions -// in them. -message UninterpretedOption { - // The name of the uninterpreted option. Each string represents a segment in - // a dot-separated name. is_extension is true iff a segment represents an - // extension (denoted with parentheses in options specs in .proto files). - // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents - // "foo.(bar.baz).qux". - message NamePart { - required string name_part = 1; - required bool is_extension = 2; - } - repeated NamePart name = 2; - - // The value of the uninterpreted option, in whatever type the tokenizer - // identified it as during parsing. Exactly one of these should be set. - optional string identifier_value = 3; - optional uint64 positive_int_value = 4; - optional int64 negative_int_value = 5; - optional double double_value = 6; - optional bytes string_value = 7; - optional string aggregate_value = 8; -} - -// =================================================================== -// Optional source code info - -// Encapsulates information about the original source file from which a -// FileDescriptorProto was generated. -message SourceCodeInfo { - // A Location identifies a piece of source code in a .proto file which - // corresponds to a particular definition. This information is intended - // to be useful to IDEs, code indexers, documentation generators, and similar - // tools. - // - // For example, say we have a file like: - // message Foo { - // optional string foo = 1; - // } - // Let's look at just the field definition: - // optional string foo = 1; - // ^ ^^ ^^ ^ ^^^ - // a bc de f ghi - // We have the following locations: - // span path represents - // [a,i) [ 4, 0, 2, 0 ] The whole field definition. - // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). - // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). - // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). - // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). - // - // Notes: - // - A location may refer to a repeated field itself (i.e. not to any - // particular index within it). This is used whenever a set of elements are - // logically enclosed in a single code segment. For example, an entire - // extend block (possibly containing multiple extension definitions) will - // have an outer location whose path refers to the "extensions" repeated - // field without an index. - // - Multiple locations may have the same path. This happens when a single - // logical declaration is spread out across multiple places. The most - // obvious example is the "extend" block again -- there may be multiple - // extend blocks in the same scope, each of which will have the same path. - // - A location's span is not always a subset of its parent's span. For - // example, the "extendee" of an extension declaration appears at the - // beginning of the "extend" block and is shared by all extensions within - // the block. - // - Just because a location's span is a subset of some other location's span - // does not mean that it is a descendent. For example, a "group" defines - // both a type and a field in a single declaration. Thus, the locations - // corresponding to the type and field and their components will overlap. - // - Code which tries to interpret locations should probably be designed to - // ignore those that it doesn't understand, as more types of locations could - // be recorded in the future. - repeated Location location = 1; - message Location { - // Identifies which part of the FileDescriptorProto was defined at this - // location. - // - // Each element is a field number or an index. They form a path from - // the root FileDescriptorProto to the place where the definition. For - // example, this path: - // [ 4, 3, 2, 7, 1 ] - // refers to: - // file.message_type(3) // 4, 3 - // .field(7) // 2, 7 - // .name() // 1 - // This is because FileDescriptorProto.message_type has field number 4: - // repeated DescriptorProto message_type = 4; - // and DescriptorProto.field has field number 2: - // repeated FieldDescriptorProto field = 2; - // and FieldDescriptorProto.name has field number 1: - // optional string name = 1; - // - // Thus, the above path gives the location of a field name. If we removed - // the last element: - // [ 4, 3, 2, 7 ] - // this path refers to the whole field declaration (from the beginning - // of the label to the terminating semicolon). - repeated int32 path = 1 [packed=true]; - - // Always has exactly three or four elements: start line, start column, - // end line (optional, otherwise assumed same as start line), end column. - // These are packed into a single field for efficiency. Note that line - // and column numbers are zero-based -- typically you will want to add - // 1 to each before displaying to a user. - repeated int32 span = 2 [packed=true]; - - // If this SourceCodeInfo represents a complete declaration, these are any - // comments appearing before and after the declaration which appear to be - // attached to the declaration. - // - // A series of line comments appearing on consecutive lines, with no other - // tokens appearing on those lines, will be treated as a single comment. - // - // leading_detached_comments will keep paragraphs of comments that appear - // before (but not connected to) the current element. Each paragraph, - // separated by empty lines, will be one comment element in the repeated - // field. - // - // Only the comment content is provided; comment markers (e.g. //) are - // stripped out. For block comments, leading whitespace and an asterisk - // will be stripped from the beginning of each line other than the first. - // Newlines are included in the output. - // - // Examples: - // - // optional int32 foo = 1; // Comment attached to foo. - // // Comment attached to bar. - // optional int32 bar = 2; - // - // optional string baz = 3; - // // Comment attached to baz. - // // Another line attached to baz. - // - // // Comment attached to qux. - // // - // // Another line attached to qux. - // optional double qux = 4; - // - // // Detached comment for corge. This is not leading or trailing comments - // // to qux or corge because there are blank lines separating it from - // // both. - // - // // Detached comment for corge paragraph 2. - // - // optional string corge = 5; - // /* Block comment attached - // * to corge. Leading asterisks - // * will be removed. */ - // /* Block comment attached to - // * grault. */ - // optional int32 grault = 6; - // - // // ignored detached comments. - optional string leading_comments = 3; - optional string trailing_comments = 4; - repeated string leading_detached_comments = 6; - } -} - -// Describes the relationship between generated code and its original source -// file. A GeneratedCodeInfo message is associated with only one generated -// source file, but may contain references to different source .proto files. -message GeneratedCodeInfo { - // An Annotation connects some span of text in generated code to an element - // of its generating .proto file. - repeated Annotation annotation = 1; - message Annotation { - // Identifies the element in the original source .proto file. This field - // is formatted the same as SourceCodeInfo.Location.path. - repeated int32 path = 1 [packed=true]; - - // Identifies the filesystem path to the original source .proto. - optional string source_file = 2; - - // Identifies the starting offset in bytes in the generated code - // that relates to the identified object. - optional int32 begin = 3; - - // Identifies the ending offset in bytes in the generated code that - // relates to the identified offset. The end offset should be one past - // the last relevant byte (so the length of the text = end - begin). - optional int32 end = 4; - } -} diff --git a/Examples/Google/common/include/google/protobuf/duration.proto b/Examples/Google/common/include/google/protobuf/duration.proto deleted file mode 100755 index 975fce41a..000000000 --- a/Examples/Google/common/include/google/protobuf/duration.proto +++ /dev/null @@ -1,117 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "github.com/golang/protobuf/ptypes/duration"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "DurationProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// A Duration represents a signed, fixed-length span of time represented -// as a count of seconds and fractions of seconds at nanosecond -// resolution. It is independent of any calendar and concepts like "day" -// or "month". It is related to Timestamp in that the difference between -// two Timestamp values is a Duration and it can be added or subtracted -// from a Timestamp. Range is approximately +-10,000 years. -// -// # Examples -// -// Example 1: Compute Duration from two Timestamps in pseudo code. -// -// Timestamp start = ...; -// Timestamp end = ...; -// Duration duration = ...; -// -// duration.seconds = end.seconds - start.seconds; -// duration.nanos = end.nanos - start.nanos; -// -// if (duration.seconds < 0 && duration.nanos > 0) { -// duration.seconds += 1; -// duration.nanos -= 1000000000; -// } else if (durations.seconds > 0 && duration.nanos < 0) { -// duration.seconds -= 1; -// duration.nanos += 1000000000; -// } -// -// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code. -// -// Timestamp start = ...; -// Duration duration = ...; -// Timestamp end = ...; -// -// end.seconds = start.seconds + duration.seconds; -// end.nanos = start.nanos + duration.nanos; -// -// if (end.nanos < 0) { -// end.seconds -= 1; -// end.nanos += 1000000000; -// } else if (end.nanos >= 1000000000) { -// end.seconds += 1; -// end.nanos -= 1000000000; -// } -// -// Example 3: Compute Duration from datetime.timedelta in Python. -// -// td = datetime.timedelta(days=3, minutes=10) -// duration = Duration() -// duration.FromTimedelta(td) -// -// # JSON Mapping -// -// In JSON format, the Duration type is encoded as a string rather than an -// object, where the string ends in the suffix "s" (indicating seconds) and -// is preceded by the number of seconds, with nanoseconds expressed as -// fractional seconds. For example, 3 seconds with 0 nanoseconds should be -// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should -// be expressed in JSON format as "3.000000001s", and 3 seconds and 1 -// microsecond should be expressed in JSON format as "3.000001s". -// -// -message Duration { - - // Signed seconds of the span of time. Must be from -315,576,000,000 - // to +315,576,000,000 inclusive. Note: these bounds are computed from: - // 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years - int64 seconds = 1; - - // Signed fractions of a second at nanosecond resolution of the span - // of time. Durations less than one second are represented with a 0 - // `seconds` field and a positive or negative `nanos` field. For durations - // of one second or more, a non-zero value for the `nanos` field must be - // of the same sign as the `seconds` field. Must be from -999,999,999 - // to +999,999,999 inclusive. - int32 nanos = 2; -} diff --git a/Examples/Google/common/include/google/protobuf/empty.proto b/Examples/Google/common/include/google/protobuf/empty.proto deleted file mode 100755 index 03cacd233..000000000 --- a/Examples/Google/common/include/google/protobuf/empty.proto +++ /dev/null @@ -1,52 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option go_package = "github.com/golang/protobuf/ptypes/empty"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "EmptyProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option cc_enable_arenas = true; - -// A generic empty message that you can re-use to avoid defining duplicated -// empty messages in your APIs. A typical example is to use it as the request -// or the response type of an API method. For instance: -// -// service Foo { -// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); -// } -// -// The JSON representation for `Empty` is empty JSON object `{}`. -message Empty {} diff --git a/Examples/Google/common/include/google/protobuf/field_mask.proto b/Examples/Google/common/include/google/protobuf/field_mask.proto deleted file mode 100755 index c68d247c8..000000000 --- a/Examples/Google/common/include/google/protobuf/field_mask.proto +++ /dev/null @@ -1,246 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "FieldMaskProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/genproto/protobuf/field_mask;field_mask"; - -// `FieldMask` represents a set of symbolic field paths, for example: -// -// paths: "f.a" -// paths: "f.b.d" -// -// Here `f` represents a field in some root message, `a` and `b` -// fields in the message found in `f`, and `d` a field found in the -// message in `f.b`. -// -// Field masks are used to specify a subset of fields that should be -// returned by a get operation or modified by an update operation. -// Field masks also have a custom JSON encoding (see below). -// -// # Field Masks in Projections -// -// When used in the context of a projection, a response message or -// sub-message is filtered by the API to only contain those fields as -// specified in the mask. For example, if the mask in the previous -// example is applied to a response message as follows: -// -// f { -// a : 22 -// b { -// d : 1 -// x : 2 -// } -// y : 13 -// } -// z: 8 -// -// The result will not contain specific values for fields x,y and z -// (their value will be set to the default, and omitted in proto text -// output): -// -// -// f { -// a : 22 -// b { -// d : 1 -// } -// } -// -// A repeated field is not allowed except at the last position of a -// paths string. -// -// If a FieldMask object is not present in a get operation, the -// operation applies to all fields (as if a FieldMask of all fields -// had been specified). -// -// Note that a field mask does not necessarily apply to the -// top-level response message. In case of a REST get operation, the -// field mask applies directly to the response, but in case of a REST -// list operation, the mask instead applies to each individual message -// in the returned resource list. In case of a REST custom method, -// other definitions may be used. Where the mask applies will be -// clearly documented together with its declaration in the API. In -// any case, the effect on the returned resource/resources is required -// behavior for APIs. -// -// # Field Masks in Update Operations -// -// A field mask in update operations specifies which fields of the -// targeted resource are going to be updated. The API is required -// to only change the values of the fields as specified in the mask -// and leave the others untouched. If a resource is passed in to -// describe the updated values, the API ignores the values of all -// fields not covered by the mask. -// -// If a repeated field is specified for an update operation, the existing -// repeated values in the target resource will be overwritten by the new values. -// Note that a repeated field is only allowed in the last position of a `paths` -// string. -// -// If a sub-message is specified in the last position of the field mask for an -// update operation, then the existing sub-message in the target resource is -// overwritten. Given the target message: -// -// f { -// b { -// d : 1 -// x : 2 -// } -// c : 1 -// } -// -// And an update message: -// -// f { -// b { -// d : 10 -// } -// } -// -// then if the field mask is: -// -// paths: "f.b" -// -// then the result will be: -// -// f { -// b { -// d : 10 -// } -// c : 1 -// } -// -// However, if the update mask was: -// -// paths: "f.b.d" -// -// then the result would be: -// -// f { -// b { -// d : 10 -// x : 2 -// } -// c : 1 -// } -// -// In order to reset a field's value to the default, the field must -// be in the mask and set to the default value in the provided resource. -// Hence, in order to reset all fields of a resource, provide a default -// instance of the resource and set all fields in the mask, or do -// not provide a mask as described below. -// -// If a field mask is not present on update, the operation applies to -// all fields (as if a field mask of all fields has been specified). -// Note that in the presence of schema evolution, this may mean that -// fields the client does not know and has therefore not filled into -// the request will be reset to their default. If this is unwanted -// behavior, a specific service may require a client to always specify -// a field mask, producing an error if not. -// -// As with get operations, the location of the resource which -// describes the updated values in the request message depends on the -// operation kind. In any case, the effect of the field mask is -// required to be honored by the API. -// -// ## Considerations for HTTP REST -// -// The HTTP kind of an update operation which uses a field mask must -// be set to PATCH instead of PUT in order to satisfy HTTP semantics -// (PUT must only be used for full updates). -// -// # JSON Encoding of Field Masks -// -// In JSON, a field mask is encoded as a single string where paths are -// separated by a comma. Fields name in each path are converted -// to/from lower-camel naming conventions. -// -// As an example, consider the following message declarations: -// -// message Profile { -// User user = 1; -// Photo photo = 2; -// } -// message User { -// string display_name = 1; -// string address = 2; -// } -// -// In proto a field mask for `Profile` may look as such: -// -// mask { -// paths: "user.display_name" -// paths: "photo" -// } -// -// In JSON, the same mask is represented as below: -// -// { -// mask: "user.displayName,photo" -// } -// -// # Field Masks and Oneof Fields -// -// Field masks treat fields in oneofs just as regular fields. Consider the -// following message: -// -// message SampleMessage { -// oneof test_oneof { -// string name = 4; -// SubMessage sub_message = 9; -// } -// } -// -// The field mask can be: -// -// mask { -// paths: "name" -// } -// -// Or: -// -// mask { -// paths: "sub_message" -// } -// -// Note that oneof type names ("test_oneof" in this case) cannot be used in -// paths. -message FieldMask { - // The set of field mask paths. - repeated string paths = 1; -} diff --git a/Examples/Google/common/include/google/protobuf/source_context.proto b/Examples/Google/common/include/google/protobuf/source_context.proto deleted file mode 100755 index f3b2c9668..000000000 --- a/Examples/Google/common/include/google/protobuf/source_context.proto +++ /dev/null @@ -1,48 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "SourceContextProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/genproto/protobuf/source_context;source_context"; - -// `SourceContext` represents information about the source of a -// protobuf element, like the file in which it is defined. -message SourceContext { - // The path-qualified name of the .proto file that contained the associated - // protobuf element. For example: `"google/protobuf/source_context.proto"`. - string file_name = 1; -} diff --git a/Examples/Google/common/include/google/protobuf/struct.proto b/Examples/Google/common/include/google/protobuf/struct.proto deleted file mode 100755 index 7d7808e7f..000000000 --- a/Examples/Google/common/include/google/protobuf/struct.proto +++ /dev/null @@ -1,96 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "github.com/golang/protobuf/ptypes/struct;structpb"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "StructProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - - -// `Struct` represents a structured data value, consisting of fields -// which map to dynamically typed values. In some languages, `Struct` -// might be supported by a native representation. For example, in -// scripting languages like JS a struct is represented as an -// object. The details of that representation are described together -// with the proto support for the language. -// -// The JSON representation for `Struct` is JSON object. -message Struct { - // Unordered map of dynamically typed values. - map fields = 1; -} - -// `Value` represents a dynamically typed value which can be either -// null, a number, a string, a boolean, a recursive struct value, or a -// list of values. A producer of value is expected to set one of that -// variants, absence of any variant indicates an error. -// -// The JSON representation for `Value` is JSON value. -message Value { - // The kind of value. - oneof kind { - // Represents a null value. - NullValue null_value = 1; - // Represents a double value. - double number_value = 2; - // Represents a string value. - string string_value = 3; - // Represents a boolean value. - bool bool_value = 4; - // Represents a structured value. - Struct struct_value = 5; - // Represents a repeated `Value`. - ListValue list_value = 6; - } -} - -// `NullValue` is a singleton enumeration to represent the null value for the -// `Value` type union. -// -// The JSON representation for `NullValue` is JSON `null`. -enum NullValue { - // Null value. - NULL_VALUE = 0; -} - -// `ListValue` is a wrapper around a repeated field of values. -// -// The JSON representation for `ListValue` is JSON array. -message ListValue { - // Repeated field of dynamically typed values. - repeated Value values = 1; -} diff --git a/Examples/Google/common/include/google/protobuf/timestamp.proto b/Examples/Google/common/include/google/protobuf/timestamp.proto deleted file mode 100755 index b7cbd1750..000000000 --- a/Examples/Google/common/include/google/protobuf/timestamp.proto +++ /dev/null @@ -1,133 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "github.com/golang/protobuf/ptypes/timestamp"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "TimestampProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// A Timestamp represents a point in time independent of any time zone -// or calendar, represented as seconds and fractions of seconds at -// nanosecond resolution in UTC Epoch time. It is encoded using the -// Proleptic Gregorian Calendar which extends the Gregorian calendar -// backwards to year one. It is encoded assuming all minutes are 60 -// seconds long, i.e. leap seconds are "smeared" so that no leap second -// table is needed for interpretation. Range is from -// 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. -// By restricting to that range, we ensure that we can convert to -// and from RFC 3339 date strings. -// See [https://www.ietf.org/rfc/rfc3339.txt](https://www.ietf.org/rfc/rfc3339.txt). -// -// # Examples -// -// Example 1: Compute Timestamp from POSIX `time()`. -// -// Timestamp timestamp; -// timestamp.set_seconds(time(NULL)); -// timestamp.set_nanos(0); -// -// Example 2: Compute Timestamp from POSIX `gettimeofday()`. -// -// struct timeval tv; -// gettimeofday(&tv, NULL); -// -// Timestamp timestamp; -// timestamp.set_seconds(tv.tv_sec); -// timestamp.set_nanos(tv.tv_usec * 1000); -// -// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. -// -// FILETIME ft; -// GetSystemTimeAsFileTime(&ft); -// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; -// -// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z -// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. -// Timestamp timestamp; -// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); -// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); -// -// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. -// -// long millis = System.currentTimeMillis(); -// -// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) -// .setNanos((int) ((millis % 1000) * 1000000)).build(); -// -// -// Example 5: Compute Timestamp from current time in Python. -// -// timestamp = Timestamp() -// timestamp.GetCurrentTime() -// -// # JSON Mapping -// -// In JSON format, the Timestamp type is encoded as a string in the -// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the -// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" -// where {year} is always expressed using four digits while {month}, {day}, -// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional -// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), -// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone -// is required, though only UTC (as indicated by "Z") is presently supported. -// -// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past -// 01:30 UTC on January 15, 2017. -// -// In JavaScript, one can convert a Date object to this format using the -// standard [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString] -// method. In Python, a standard `datetime.datetime` object can be converted -// to this format using [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) -// with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one -// can use the Joda Time's [`ISODateTimeFormat.dateTime()`]( -// http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()) -// to obtain a formatter capable of generating timestamps in this format. -// -// -message Timestamp { - - // Represents seconds of UTC time since Unix epoch - // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to - // 9999-12-31T23:59:59Z inclusive. - int64 seconds = 1; - - // Non-negative fractions of a second at nanosecond resolution. Negative - // second values with fractions must still have non-negative nanos values - // that count forward in time. Must be from 0 to 999,999,999 - // inclusive. - int32 nanos = 2; -} diff --git a/Examples/Google/common/include/google/protobuf/type.proto b/Examples/Google/common/include/google/protobuf/type.proto deleted file mode 100755 index 624c15ee6..000000000 --- a/Examples/Google/common/include/google/protobuf/type.proto +++ /dev/null @@ -1,187 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto3"; - -package google.protobuf; - -import "google/protobuf/any.proto"; -import "google/protobuf/source_context.proto"; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option java_package = "com.google.protobuf"; -option java_outer_classname = "TypeProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; -option go_package = "google.golang.org/genproto/protobuf/ptype;ptype"; - -// A protocol buffer message type. -message Type { - // The fully qualified message name. - string name = 1; - // The list of fields. - repeated Field fields = 2; - // The list of types appearing in `oneof` definitions in this type. - repeated string oneofs = 3; - // The protocol buffer options. - repeated Option options = 4; - // The source context. - SourceContext source_context = 5; - // The source syntax. - Syntax syntax = 6; -} - -// A single field of a message type. -message Field { - // Basic field types. - enum Kind { - // Field type unknown. - TYPE_UNKNOWN = 0; - // Field type double. - TYPE_DOUBLE = 1; - // Field type float. - TYPE_FLOAT = 2; - // Field type int64. - TYPE_INT64 = 3; - // Field type uint64. - TYPE_UINT64 = 4; - // Field type int32. - TYPE_INT32 = 5; - // Field type fixed64. - TYPE_FIXED64 = 6; - // Field type fixed32. - TYPE_FIXED32 = 7; - // Field type bool. - TYPE_BOOL = 8; - // Field type string. - TYPE_STRING = 9; - // Field type group. Proto2 syntax only, and deprecated. - TYPE_GROUP = 10; - // Field type message. - TYPE_MESSAGE = 11; - // Field type bytes. - TYPE_BYTES = 12; - // Field type uint32. - TYPE_UINT32 = 13; - // Field type enum. - TYPE_ENUM = 14; - // Field type sfixed32. - TYPE_SFIXED32 = 15; - // Field type sfixed64. - TYPE_SFIXED64 = 16; - // Field type sint32. - TYPE_SINT32 = 17; - // Field type sint64. - TYPE_SINT64 = 18; - }; - - // Whether a field is optional, required, or repeated. - enum Cardinality { - // For fields with unknown cardinality. - CARDINALITY_UNKNOWN = 0; - // For optional fields. - CARDINALITY_OPTIONAL = 1; - // For required fields. Proto2 syntax only. - CARDINALITY_REQUIRED = 2; - // For repeated fields. - CARDINALITY_REPEATED = 3; - }; - - // The field type. - Kind kind = 1; - // The field cardinality. - Cardinality cardinality = 2; - // The field number. - int32 number = 3; - // The field name. - string name = 4; - // The field type URL, without the scheme, for message or enumeration - // types. Example: `"type.googleapis.com/google.protobuf.Timestamp"`. - string type_url = 6; - // The index of the field type in `Type.oneofs`, for message or enumeration - // types. The first type has index 1; zero means the type is not in the list. - int32 oneof_index = 7; - // Whether to use alternative packed wire representation. - bool packed = 8; - // The protocol buffer options. - repeated Option options = 9; - // The field JSON name. - string json_name = 10; - // The string value of the default value of this field. Proto2 syntax only. - string default_value = 11; -} - -// Enum type definition. -message Enum { - // Enum type name. - string name = 1; - // Enum value definitions. - repeated EnumValue enumvalue = 2; - // Protocol buffer options. - repeated Option options = 3; - // The source context. - SourceContext source_context = 4; - // The source syntax. - Syntax syntax = 5; -} - -// Enum value definition. -message EnumValue { - // Enum value name. - string name = 1; - // Enum value number. - int32 number = 2; - // Protocol buffer options. - repeated Option options = 3; -} - -// A protocol buffer option, which can be attached to a message, field, -// enumeration, etc. -message Option { - // The option's name. For protobuf built-in options (options defined in - // descriptor.proto), this is the short name. For example, `"map_entry"`. - // For custom options, it should be the fully-qualified name. For example, - // `"google.api.http"`. - string name = 1; - // The option's value packed in an Any message. If the value is a primitive, - // the corresponding wrapper type defined in google/protobuf/wrappers.proto - // should be used. If the value is an enum, it should be stored as an int32 - // value using the google.protobuf.Int32Value type. - Any value = 2; -} - -// The syntax in which a protocol buffer element is defined. -enum Syntax { - // Syntax `proto2`. - SYNTAX_PROTO2 = 0; - // Syntax `proto3`. - SYNTAX_PROTO3 = 1; -} diff --git a/Examples/Google/common/include/google/protobuf/wrappers.proto b/Examples/Google/common/include/google/protobuf/wrappers.proto deleted file mode 100755 index 01947639a..000000000 --- a/Examples/Google/common/include/google/protobuf/wrappers.proto +++ /dev/null @@ -1,118 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2008 Google Inc. All rights reserved. -// https://developers.google.com/protocol-buffers/ -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// Wrappers for primitive (non-message) types. These types are useful -// for embedding primitives in the `google.protobuf.Any` type and for places -// where we need to distinguish between the absence of a primitive -// typed field and its default value. - -syntax = "proto3"; - -package google.protobuf; - -option csharp_namespace = "Google.Protobuf.WellKnownTypes"; -option cc_enable_arenas = true; -option go_package = "github.com/golang/protobuf/ptypes/wrappers"; -option java_package = "com.google.protobuf"; -option java_outer_classname = "WrappersProto"; -option java_multiple_files = true; -option objc_class_prefix = "GPB"; - -// Wrapper message for `double`. -// -// The JSON representation for `DoubleValue` is JSON number. -message DoubleValue { - // The double value. - double value = 1; -} - -// Wrapper message for `float`. -// -// The JSON representation for `FloatValue` is JSON number. -message FloatValue { - // The float value. - float value = 1; -} - -// Wrapper message for `int64`. -// -// The JSON representation for `Int64Value` is JSON string. -message Int64Value { - // The int64 value. - int64 value = 1; -} - -// Wrapper message for `uint64`. -// -// The JSON representation for `UInt64Value` is JSON string. -message UInt64Value { - // The uint64 value. - uint64 value = 1; -} - -// Wrapper message for `int32`. -// -// The JSON representation for `Int32Value` is JSON number. -message Int32Value { - // The int32 value. - int32 value = 1; -} - -// Wrapper message for `uint32`. -// -// The JSON representation for `UInt32Value` is JSON number. -message UInt32Value { - // The uint32 value. - uint32 value = 1; -} - -// Wrapper message for `bool`. -// -// The JSON representation for `BoolValue` is JSON `true` and `false`. -message BoolValue { - // The bool value. - bool value = 1; -} - -// Wrapper message for `string`. -// -// The JSON representation for `StringValue` is JSON string. -message StringValue { - // The string value. - string value = 1; -} - -// Wrapper message for `bytes`. -// -// The JSON representation for `BytesValue` is JSON string. -message BytesValue { - // The bytes value. - bytes value = 1; -} diff --git a/Examples/README.md b/Examples/README.md deleted file mode 100644 index 27a9a941e..000000000 --- a/Examples/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Examples - -## Google API examples - -Samples that call Google gRPC APIs are in the [Google](Google) directory. - -## gRPC Web - -[EchoWeb](EchoWeb) is an example which uses a JavaScript client to communicate -with a gRPC Server using the gRPC-Web protocol. - -## Other Examples - -See also: -- [Quick Start Guide](../docs/quick-start.md), and -- [Basic Tutorial](../docs/basic-tutorial.md) diff --git a/Sources/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift b/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift similarity index 100% rename from Sources/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift rename to Examples/v1/Echo/Implementation/EchoAsyncProvider.swift diff --git a/Sources/Examples/v1/Echo/Implementation/EchoProvider.swift b/Examples/v1/Echo/Implementation/EchoProvider.swift similarity index 100% rename from Sources/Examples/v1/Echo/Implementation/EchoProvider.swift rename to Examples/v1/Echo/Implementation/EchoProvider.swift diff --git a/Sources/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift b/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift similarity index 100% rename from Sources/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift rename to Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift diff --git a/Sources/Examples/v1/Echo/Implementation/Interceptors.swift b/Examples/v1/Echo/Implementation/Interceptors.swift similarity index 100% rename from Sources/Examples/v1/Echo/Implementation/Interceptors.swift rename to Examples/v1/Echo/Implementation/Interceptors.swift diff --git a/Sources/Examples/v1/Echo/Model/echo.grpc.swift b/Examples/v1/Echo/Model/echo.grpc.swift similarity index 100% rename from Sources/Examples/v1/Echo/Model/echo.grpc.swift rename to Examples/v1/Echo/Model/echo.grpc.swift diff --git a/Sources/Examples/v1/Echo/Model/echo.pb.swift b/Examples/v1/Echo/Model/echo.pb.swift similarity index 100% rename from Sources/Examples/v1/Echo/Model/echo.pb.swift rename to Examples/v1/Echo/Model/echo.pb.swift diff --git a/Sources/Examples/v1/Echo/README.md b/Examples/v1/Echo/README.md similarity index 100% rename from Sources/Examples/v1/Echo/README.md rename to Examples/v1/Echo/README.md diff --git a/Sources/Examples/v1/Echo/Runtime/Echo.swift b/Examples/v1/Echo/Runtime/Echo.swift similarity index 100% rename from Sources/Examples/v1/Echo/Runtime/Echo.swift rename to Examples/v1/Echo/Runtime/Echo.swift diff --git a/Sources/Examples/v1/Echo/Runtime/Empty.swift b/Examples/v1/Echo/Runtime/Empty.swift similarity index 100% rename from Sources/Examples/v1/Echo/Runtime/Empty.swift rename to Examples/v1/Echo/Runtime/Empty.swift diff --git a/Sources/Examples/v1/HelloWorld/Client/HelloWorldClient.swift b/Examples/v1/HelloWorld/Client/HelloWorldClient.swift similarity index 100% rename from Sources/Examples/v1/HelloWorld/Client/HelloWorldClient.swift rename to Examples/v1/HelloWorld/Client/HelloWorldClient.swift diff --git a/Sources/Examples/v1/HelloWorld/Model/helloworld.grpc.swift b/Examples/v1/HelloWorld/Model/helloworld.grpc.swift similarity index 100% rename from Sources/Examples/v1/HelloWorld/Model/helloworld.grpc.swift rename to Examples/v1/HelloWorld/Model/helloworld.grpc.swift diff --git a/Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift b/Examples/v1/HelloWorld/Model/helloworld.pb.swift similarity index 100% rename from Sources/Examples/v1/HelloWorld/Model/helloworld.pb.swift rename to Examples/v1/HelloWorld/Model/helloworld.pb.swift diff --git a/Sources/Examples/v1/HelloWorld/README.md b/Examples/v1/HelloWorld/README.md similarity index 100% rename from Sources/Examples/v1/HelloWorld/README.md rename to Examples/v1/HelloWorld/README.md diff --git a/Sources/Examples/v1/HelloWorld/Server/GreeterProvider.swift b/Examples/v1/HelloWorld/Server/GreeterProvider.swift similarity index 100% rename from Sources/Examples/v1/HelloWorld/Server/GreeterProvider.swift rename to Examples/v1/HelloWorld/Server/GreeterProvider.swift diff --git a/Sources/Examples/v1/HelloWorld/Server/HelloWorldServer.swift b/Examples/v1/HelloWorld/Server/HelloWorldServer.swift similarity index 100% rename from Sources/Examples/v1/HelloWorld/Server/HelloWorldServer.swift rename to Examples/v1/HelloWorld/Server/HelloWorldServer.swift diff --git a/Sources/Examples/v1/PacketCapture/Empty.swift b/Examples/v1/PacketCapture/Empty.swift similarity index 100% rename from Sources/Examples/v1/PacketCapture/Empty.swift rename to Examples/v1/PacketCapture/Empty.swift diff --git a/Sources/Examples/v1/PacketCapture/PacketCapture.swift b/Examples/v1/PacketCapture/PacketCapture.swift similarity index 100% rename from Sources/Examples/v1/PacketCapture/PacketCapture.swift rename to Examples/v1/PacketCapture/PacketCapture.swift diff --git a/Sources/Examples/v1/PacketCapture/README.md b/Examples/v1/PacketCapture/README.md similarity index 100% rename from Sources/Examples/v1/PacketCapture/README.md rename to Examples/v1/PacketCapture/README.md diff --git a/Sources/Examples/v1/README.md b/Examples/v1/README.md similarity index 100% rename from Sources/Examples/v1/README.md rename to Examples/v1/README.md diff --git a/Sources/Examples/v1/ReflectionService/Generated/echo.grpc.reflection b/Examples/v1/ReflectionService/Generated/echo.grpc.reflection similarity index 100% rename from Sources/Examples/v1/ReflectionService/Generated/echo.grpc.reflection rename to Examples/v1/ReflectionService/Generated/echo.grpc.reflection diff --git a/Sources/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection b/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection similarity index 100% rename from Sources/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection rename to Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection diff --git a/Sources/Examples/v1/ReflectionService/GreeterProvider.swift b/Examples/v1/ReflectionService/GreeterProvider.swift similarity index 100% rename from Sources/Examples/v1/ReflectionService/GreeterProvider.swift rename to Examples/v1/ReflectionService/GreeterProvider.swift diff --git a/Sources/Examples/v1/ReflectionService/ReflectionServer.swift b/Examples/v1/ReflectionService/ReflectionServer.swift similarity index 100% rename from Sources/Examples/v1/ReflectionService/ReflectionServer.swift rename to Examples/v1/ReflectionService/ReflectionServer.swift diff --git a/Sources/Examples/v1/RouteGuide/Client/Empty.swift b/Examples/v1/RouteGuide/Client/Empty.swift similarity index 100% rename from Sources/Examples/v1/RouteGuide/Client/Empty.swift rename to Examples/v1/RouteGuide/Client/Empty.swift diff --git a/Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift b/Examples/v1/RouteGuide/Client/RouteGuideClient.swift similarity index 100% rename from Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift rename to Examples/v1/RouteGuide/Client/RouteGuideClient.swift diff --git a/Sources/Examples/v1/RouteGuide/Model/route_guide.grpc.swift b/Examples/v1/RouteGuide/Model/route_guide.grpc.swift similarity index 100% rename from Sources/Examples/v1/RouteGuide/Model/route_guide.grpc.swift rename to Examples/v1/RouteGuide/Model/route_guide.grpc.swift diff --git a/Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift b/Examples/v1/RouteGuide/Model/route_guide.pb.swift similarity index 100% rename from Sources/Examples/v1/RouteGuide/Model/route_guide.pb.swift rename to Examples/v1/RouteGuide/Model/route_guide.pb.swift diff --git a/Sources/Examples/v1/RouteGuide/README.md b/Examples/v1/RouteGuide/README.md similarity index 100% rename from Sources/Examples/v1/RouteGuide/README.md rename to Examples/v1/RouteGuide/README.md diff --git a/Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift b/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift similarity index 100% rename from Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift rename to Examples/v1/RouteGuide/Server/RouteGuideProvider.swift diff --git a/Sources/Examples/v1/RouteGuide/Server/RouteGuideServer.swift b/Examples/v1/RouteGuide/Server/RouteGuideServer.swift similarity index 100% rename from Sources/Examples/v1/RouteGuide/Server/RouteGuideServer.swift rename to Examples/v1/RouteGuide/Server/RouteGuideServer.swift diff --git a/Sources/Examples/v1/RouteGuide/route_guide_db.json b/Examples/v1/RouteGuide/route_guide_db.json similarity index 100% rename from Sources/Examples/v1/RouteGuide/route_guide_db.json rename to Examples/v1/RouteGuide/route_guide_db.json diff --git a/Sources/Examples/v2/echo/Echo.swift b/Examples/v2/echo/Echo.swift similarity index 100% rename from Sources/Examples/v2/echo/Echo.swift rename to Examples/v2/echo/Echo.swift diff --git a/Sources/Examples/v2/echo/Generated/echo.grpc.swift b/Examples/v2/echo/Generated/echo.grpc.swift similarity index 100% rename from Sources/Examples/v2/echo/Generated/echo.grpc.swift rename to Examples/v2/echo/Generated/echo.grpc.swift diff --git a/Sources/Examples/v2/echo/Generated/echo.pb.swift b/Examples/v2/echo/Generated/echo.pb.swift similarity index 100% rename from Sources/Examples/v2/echo/Generated/echo.pb.swift rename to Examples/v2/echo/Generated/echo.pb.swift diff --git a/Sources/Examples/v2/echo/Subcommands/ClientArguments.swift b/Examples/v2/echo/Subcommands/ClientArguments.swift similarity index 100% rename from Sources/Examples/v2/echo/Subcommands/ClientArguments.swift rename to Examples/v2/echo/Subcommands/ClientArguments.swift diff --git a/Sources/Examples/v2/echo/Subcommands/Collect.swift b/Examples/v2/echo/Subcommands/Collect.swift similarity index 100% rename from Sources/Examples/v2/echo/Subcommands/Collect.swift rename to Examples/v2/echo/Subcommands/Collect.swift diff --git a/Sources/Examples/v2/echo/Subcommands/Expand.swift b/Examples/v2/echo/Subcommands/Expand.swift similarity index 100% rename from Sources/Examples/v2/echo/Subcommands/Expand.swift rename to Examples/v2/echo/Subcommands/Expand.swift diff --git a/Sources/Examples/v2/echo/Subcommands/Get.swift b/Examples/v2/echo/Subcommands/Get.swift similarity index 100% rename from Sources/Examples/v2/echo/Subcommands/Get.swift rename to Examples/v2/echo/Subcommands/Get.swift diff --git a/Sources/Examples/v2/echo/Subcommands/Serve.swift b/Examples/v2/echo/Subcommands/Serve.swift similarity index 100% rename from Sources/Examples/v2/echo/Subcommands/Serve.swift rename to Examples/v2/echo/Subcommands/Serve.swift diff --git a/Sources/Examples/v2/echo/Subcommands/Update.swift b/Examples/v2/echo/Subcommands/Update.swift similarity index 100% rename from Sources/Examples/v2/echo/Subcommands/Update.swift rename to Examples/v2/echo/Subcommands/Update.swift diff --git a/Sources/Examples/v2/hello-world/Generated/helloworld.grpc.swift b/Examples/v2/hello-world/Generated/helloworld.grpc.swift similarity index 100% rename from Sources/Examples/v2/hello-world/Generated/helloworld.grpc.swift rename to Examples/v2/hello-world/Generated/helloworld.grpc.swift diff --git a/Sources/Examples/v2/hello-world/Generated/helloworld.pb.swift b/Examples/v2/hello-world/Generated/helloworld.pb.swift similarity index 100% rename from Sources/Examples/v2/hello-world/Generated/helloworld.pb.swift rename to Examples/v2/hello-world/Generated/helloworld.pb.swift diff --git a/Sources/Examples/v2/hello-world/HelloWorld.swift b/Examples/v2/hello-world/HelloWorld.swift similarity index 100% rename from Sources/Examples/v2/hello-world/HelloWorld.swift rename to Examples/v2/hello-world/HelloWorld.swift diff --git a/Sources/Examples/v2/hello-world/Subcommands/Greet.swift b/Examples/v2/hello-world/Subcommands/Greet.swift similarity index 100% rename from Sources/Examples/v2/hello-world/Subcommands/Greet.swift rename to Examples/v2/hello-world/Subcommands/Greet.swift diff --git a/Sources/Examples/v2/hello-world/Subcommands/Serve.swift b/Examples/v2/hello-world/Subcommands/Serve.swift similarity index 100% rename from Sources/Examples/v2/hello-world/Subcommands/Serve.swift rename to Examples/v2/hello-world/Subcommands/Serve.swift diff --git a/FuzzTesting/Sources/EchoImplementation b/FuzzTesting/Sources/EchoImplementation index eaa26a3be..aeaf586b8 120000 --- a/FuzzTesting/Sources/EchoImplementation +++ b/FuzzTesting/Sources/EchoImplementation @@ -1 +1 @@ -../../Sources/Examples/v1/Echo/Implementation \ No newline at end of file +../../Examples/v1/Echo/Implementation \ No newline at end of file diff --git a/FuzzTesting/Sources/EchoModel b/FuzzTesting/Sources/EchoModel index d0ff42971..5561e573a 120000 --- a/FuzzTesting/Sources/EchoModel +++ b/FuzzTesting/Sources/EchoModel @@ -1 +1 @@ -../../Sources/Examples/v1/Echo/Model \ No newline at end of file +../../Examples/v1/Echo/Model \ No newline at end of file diff --git a/Package.swift b/Package.swift index 4599df9ec..658d97e83 100644 --- a/Package.swift +++ b/Package.swift @@ -294,7 +294,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/v1/Echo/Model" + path: "Examples/v1/Echo/Model" ) static let echoImplementation: Target = .target( @@ -306,7 +306,7 @@ extension Target { .nioHTTP2, .protobuf, ], - path: "Sources/Examples/v1/Echo/Implementation" + path: "Examples/v1/Echo/Implementation" ) static let echo: Target = .executableTarget( @@ -323,7 +323,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - path: "Sources/Examples/v1/Echo/Runtime" + path: "Examples/v1/Echo/Runtime" ) static let helloWorldModel: Target = .target( @@ -333,7 +333,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/v1/HelloWorld/Model" + path: "Examples/v1/HelloWorld/Model" ) static let helloWorldClient: Target = .executableTarget( @@ -345,7 +345,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/HelloWorld/Client" + path: "Examples/v1/HelloWorld/Client" ) static let helloWorldServer: Target = .executableTarget( @@ -357,7 +357,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/HelloWorld/Server" + path: "Examples/v1/HelloWorld/Server" ) static let routeGuideModel: Target = .target( @@ -367,7 +367,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/v1/RouteGuide/Model" + path: "Examples/v1/RouteGuide/Model" ) static let routeGuideClient: Target = .executableTarget( @@ -379,7 +379,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/RouteGuide/Client" + path: "Examples/v1/RouteGuide/Client" ) static let routeGuideServer: Target = .executableTarget( @@ -392,7 +392,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/RouteGuide/Server" + path: "Examples/v1/RouteGuide/Server" ) static let packetCapture: Target = .executableTarget( @@ -405,7 +405,7 @@ extension Target { .nioExtras, .argumentParser, ], - path: "Sources/Examples/v1/PacketCapture", + path: "Examples/v1/PacketCapture", exclude: [ "README.md", ] @@ -433,7 +433,7 @@ extension Target { .echoModel, .echoImplementation ], - path: "Sources/Examples/v1/ReflectionService", + path: "Examples/v1/ReflectionService", resources: [ .copy("Generated") ] diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 0b6d0cbb5..118db3950 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -655,7 +655,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/v1/Echo/Model", + path: "Examples/v1/Echo/Model", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -670,7 +670,7 @@ extension Target { .nioHTTP2, .protobuf, ], - path: "Sources/Examples/v1/Echo/Implementation", + path: "Examples/v1/Echo/Implementation", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -690,7 +690,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - path: "Sources/Examples/v1/Echo/Runtime", + path: "Examples/v1/Echo/Runtime", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -707,7 +707,7 @@ extension Target { ].appending( .nioSSL, if: includeNIOSSL ), - path: "Sources/Examples/v2/echo", + path: "Examples/v2/echo", swiftSettings: [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), @@ -724,7 +724,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/v1/HelloWorld/Model", + path: "Examples/v1/HelloWorld/Model", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -739,7 +739,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/HelloWorld/Client", + path: "Examples/v1/HelloWorld/Client", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -754,7 +754,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/HelloWorld/Server", + path: "Examples/v1/HelloWorld/Server", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -767,7 +767,7 @@ extension Target { .grpcHTTP2Transport, .argumentParser, ], - path: "Sources/Examples/v2/hello-world", + path: "Examples/v2/hello-world", swiftSettings: [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), @@ -784,7 +784,7 @@ extension Target { .nio, .protobuf, ], - path: "Sources/Examples/v1/RouteGuide/Model", + path: "Examples/v1/RouteGuide/Model", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -799,7 +799,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/RouteGuide/Client", + path: "Examples/v1/RouteGuide/Client", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -815,7 +815,7 @@ extension Target { .nioPosix, .argumentParser, ], - path: "Sources/Examples/v1/RouteGuide/Server", + path: "Examples/v1/RouteGuide/Server", swiftSettings: [.swiftLanguageMode(.v5)] ) } @@ -831,7 +831,7 @@ extension Target { .nioExtras, .argumentParser, ], - path: "Sources/Examples/v1/PacketCapture", + path: "Examples/v1/PacketCapture", exclude: [ "README.md", ], @@ -865,7 +865,7 @@ extension Target { .echoModel, .echoImplementation ], - path: "Sources/Examples/v1/ReflectionService", + path: "Examples/v1/ReflectionService", resources: [ .copy("Generated") ], diff --git a/Protos/generate.sh b/Protos/generate.sh index a1304d5e4..e35f7e042 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -74,7 +74,7 @@ function invoke_protoc { function generate_echo_v1_example { local proto="$here/examples/echo/echo.proto" - local output="$root/Sources/Examples/v1/Echo/Model" + local output="$root/Examples/v1/Echo/Model" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" "TestClient=true" @@ -82,7 +82,7 @@ function generate_echo_v1_example { function generate_echo_v2_example { local proto="$here/examples/echo/echo.proto" - local output="$root/Sources/Examples/v2/echo/Generated" + local output="$root/Examples/v2/echo/Generated" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" @@ -90,7 +90,7 @@ function generate_echo_v2_example { function generate_routeguide_example { local proto="$here/examples/route_guide/route_guide.proto" - local output="$root/Sources/Examples/v1/RouteGuide/Model" + local output="$root/Examples/v1/RouteGuide/Model" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" @@ -98,7 +98,7 @@ function generate_routeguide_example { function generate_helloworld_v1_example { local proto="$here/upstream/grpc/examples/helloworld.proto" - local output="$root/Sources/Examples/v1/HelloWorld/Model" + local output="$root/Examples/v1/HelloWorld/Model" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" @@ -106,7 +106,7 @@ function generate_helloworld_v1_example { function generate_helloworld_v2_example { local proto="$here/upstream/grpc/examples/helloworld.proto" - local output="$root/Sources/Examples/v2/hello-world/Generated" + local output="$root/Examples/v2/hello-world/Generated" generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" @@ -181,7 +181,7 @@ function generate_echo_reflection_data_for_tests { function generate_reflection_data_example { local protos=("$here/examples/echo/echo.proto" "$here/upstream/grpc/examples/helloworld.proto") - local output="$root/Sources/Examples/v1/ReflectionService/Generated" + local output="$root/Examples/v1/ReflectionService/Generated" for proto in "${protos[@]}"; do generate_grpc "$proto" "$(dirname "$proto")" "$output" "Client=false" "Server=false" "ReflectionData=true" diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md index e9657a9c1..58eb3ba8c 100644 --- a/Sources/GRPC/Docs.docc/index.md +++ b/Sources/GRPC/Docs.docc/index.md @@ -88,15 +88,9 @@ The plugins are available from [homebrew](https://brew.sh) and can be installed ## Examples -gRPC Swift has a number of tutorials and examples available. They are split -across two directories: - -- [`/Sources/Examples`][examples-in-source] contains examples which do not - require additional dependencies and may be built using the Swift Package - Manager. -- [`/Examples`][examples-out-of-source] contains examples which rely on - external dependencies or may not be built by the Swift Package Manager (such - as an iOS app). +gRPC Swift has a number of tutorials and examples available. The +[`Examples/v1`][examples] directory contains examples which do not require +additional dependencies and may be built using the Swift Package Manager. Some of the examples are accompanied by tutorials, including: - A [quick start guide][docs-quickstart] for creating and running your first @@ -130,5 +124,4 @@ Some of the examples are accompanied by tutorials, including: [xcode-spm]: https://help.apple.com/xcode/mac/current/#/devb83d64851 [branch-new]: https://github.com/grpc/grpc-swift/tree/main [branch-old]: https://github.com/grpc/grpc-swift/tree/cgrpc -[examples-out-of-source]: https://github.com/grpc/grpc-swift/tree/main/Examples -[examples-in-source]: https://github.com/grpc/grpc-swift/tree/main/Sources/Examples +[examples]: https://github.com/grpc/grpc-swift/tree/main/Examples/v1 diff --git a/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift b/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift index d7bd05ba6..2dd5aef99 120000 --- a/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift @@ -1 +1 @@ -../../Examples/v1/Echo/Model/echo.grpc.swift \ No newline at end of file +../../../Examples/v1/Echo/Model/echo.grpc.swift \ No newline at end of file diff --git a/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift b/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift index f6d794789..611b0845f 120000 --- a/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift +++ b/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift @@ -1 +1 @@ -../../Examples/v1/Echo/Model/echo.pb.swift \ No newline at end of file +../../../Examples/v1/Echo/Model/echo.pb.swift \ No newline at end of file diff --git a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md index c5205c75f..d43f80a7d 100644 --- a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md +++ b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md @@ -4,7 +4,7 @@ This tutorial goes through the steps of adding Reflection service to a server, running it and testing it using gRPCurl. The server used in this example is implemented at - [Sources/Examples/v1/ReflectionService/ReflectionServer.swift][reflection-server] + [Examples/v1/ReflectionService/ReflectionServer.swift][reflection-server] and it supports the "Greeter", "Echo", and "Reflection" services. @@ -38,27 +38,27 @@ describing the services of the server and the version of the reflection service. The server from this example uses the `GreeterProvider` and the `EchoProvider`, besides the `ReflectionService`. -The associated proto files are located at `Sources/Examples/v1/HelloWorld/Model/helloworld.proto`, and -`Sources/Examples/v1/Echo/Model/echo.proto` respectively. +The associated proto files are located at `Examples/v1/HelloWorld/Model/helloworld.proto`, and +`Examples/v1/Echo/Model/echo.proto` respectively. In order to generate the reflection data for the `helloworld.proto`, you can run the following command: ```sh -$ protoc Sources/Examples/v1/HelloWorld/Model/helloworld.proto \ - --proto_path=Sources/Examples/v1/HelloWorld/Model \ +$ protoc Examples/v1/HelloWorld/Model/helloworld.proto \ + --proto_path=Examples/v1/HelloWorld/Model \ --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ - --grpc-swift_out=Sources/Examples/v1/ReflectionService/Generated + --grpc-swift_out=Examples/v1/ReflectionService/Generated ``` Let's break the command down: - The first argument passed to `protoc` is the path to the `.proto` file to generate reflection data - for: [`Sources/Examples/v1/HelloWorld/Model/helloworld.proto`][helloworld-proto]. -- The `proto_path` flag is the path to search for imports: `Sources/Examples/v1/HelloWorld/Model`. + for: [`Examples/v1/HelloWorld/Model/helloworld.proto`][helloworld-proto]. +- The `proto_path` flag is the path to search for imports: `Examples/v1/HelloWorld/Model`. - The 'grpc-swift_opt' flag allows us to list options for the Swift generator. To generate only the reflection data set: `Client=false,Server=false,ReflectionData=true`. - The `grpc-swift_out` flag is used to set the path of the directory - where the generated file will be located: `Sources/Examples/v1/ReflectionService/Generated`. + where the generated file will be located: `Examples/v1/ReflectionService/Generated`. This command assumes that the `protoc-gen-grpc-swift` plugin is in your `$PATH` environment variable. You can learn how to get the plugin from this section of the `grpc-swift` README: diff --git a/dev/codegen-tests/01-echo/proto/echo.proto b/dev/codegen-tests/01-echo/proto/echo.proto index 08e5e63ed..1af81ed41 120000 --- a/dev/codegen-tests/01-echo/proto/echo.proto +++ b/dev/codegen-tests/01-echo/proto/echo.proto @@ -1 +1 @@ -../../../../Sources/Examples/Echo/Model/echo.proto \ No newline at end of file +../../../../Protos/examples/echo/echo.proto \ No newline at end of file diff --git a/docs/basic-tutorial.md b/docs/basic-tutorial.md index a48f7736c..2d63d4887 100644 --- a/docs/basic-tutorial.md +++ b/docs/basic-tutorial.md @@ -34,7 +34,7 @@ updating. ### Example code and setup The example code for our tutorial is in -[grpc/grpc-swift/Sources/Examples/v1/RouteGuide][routeguide-source]. +[grpc/grpc-swift/Examples/v1/RouteGuide][routeguide-source]. To download the example, clone the latest release in `grpc-swift` repository by running the following command (replacing `x.y.z` with the latest release, for example `1.7.0`): @@ -43,10 +43,10 @@ example `1.7.0`): $ git clone -b x.y.z https://github.com/grpc/grpc-swift ``` -Then change your current directory to `grpc-swift/Sources/Examples/v1/RouteGuide`: +Then change your current directory to `grpc-swift/Examples/v1/RouteGuide`: ```sh -$ cd grpc-swift/Sources/Examples/v1/RouteGuide +$ cd grpc-swift/Examples/v1/RouteGuide ``` @@ -151,7 +151,7 @@ $ Protos/generate.sh ``` Running this command generates the following files in the -`Sources/Examples/v1/RouteGuide/Model` directory: +`Examples/v1/RouteGuide/Model` directory: - `route_guide.pb.swift`, which contains the implementation of your generated message classes @@ -165,10 +165,10 @@ $ protoc Protos/examples/route_guide/route_guide.proto \ --proto_path=Protos/examples/route_guide \ --plugin=./.build/debug/protoc-gen-swift \ --swift_opt=Visibility=Public \ - --swift_out=Sources/Examples/v1/RouteGuide/Model \ + --swift_out=Examples/v1/RouteGuide/Model \ --plugin=./.build/debug/protoc-gen-grpc-swift \ --grpc-swift_opt=Visibility=Public \ - --grpc-swift_out=Sources/Examples/v1/RouteGuide/Model + --grpc-swift_out=Examples/v1/RouteGuide/Model ``` We invoke the protocol buffer compiler `protoc` with the path to our service @@ -197,7 +197,7 @@ There are two parts to making our `RouteGuide` service do its job: service responses. You can find our example `RouteGuide` provider in -[grpc-swift/Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift][routeguide-provider]. +[grpc-swift/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift][routeguide-provider]. Let's take a closer look at how it works. #### Implementing RouteGuide @@ -455,7 +455,7 @@ program from exiting (since `close()` is never called on the server). In this section, we'll look at creating a Swift client for our `RouteGuide` service. You can see our complete example client code in -[grpc-swift/Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift][routeguide-client]. +[grpc-swift/Examples/v1/RouteGuide/Client/RouteGuideClient.swift][routeguide-client]. #### Creating a stub @@ -604,11 +604,11 @@ Follow the instructions in the Route Guide example directory [protobuf-docs]: https://developers.google.com/protocol-buffers/docs/proto3 [protobuf-releases]: https://github.com/google/protobuf/releases [protocol-buffers]: https://developers.google.com/protocol-buffers/docs/overview -[routeguide-client]: ../Sources/Examples/v1/RouteGuide/Client/RouteGuideClient.swift +[routeguide-client]: ../Examples/v1/RouteGuide/Client/RouteGuideClient.swift [routeguide-proto]: ../Protos/examples/route_guide/route_guide.proto -[routeguide-provider]: ../Sources/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift -[routeguide-readme]: ../Sources/Examples/v1/RouteGuide/README.md -[routeguide-source]: ../Sources/Examples/v1/RouteGuide +[routeguide-provider]: ../Examples/v1/RouteGuide/Server/RouteGuideProvider.swift +[routeguide-readme]: ../Examples/v1/RouteGuide/README.md +[routeguide-source]: ../Examples/v1/RouteGuide [run-protoc]: ../Protos/generate.sh [swift-protobuf-guide]: https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md [swift-protobuf]: https://github.com/apple/swift-protobuf diff --git a/docs/client-without-codegen.md b/docs/client-without-codegen.md index ddb07132c..924c22cde 100644 --- a/docs/client-without-codegen.md +++ b/docs/client-without-codegen.md @@ -27,4 +27,4 @@ using `makeClientStreamingCall`, `makeServerStreamingCall`, and These methods are also available on generated clients, allowing you to call methods which have been added to the service since the client was generated. -[helloworld-source]: ../Sources/Examples/v1/HelloWorld +[helloworld-source]: ../Examples/v1/HelloWorld diff --git a/docs/interceptors-tutorial.md b/docs/interceptors-tutorial.md index aee221c7a..4882aa4f5 100644 --- a/docs/interceptors-tutorial.md +++ b/docs/interceptors-tutorial.md @@ -293,4 +293,4 @@ by invoking methods on the `context` from that `EventLoop`. [quick-start]: ./quick-start.md [basic-tutorial]: ./basic-tutorial.md -[echo-example]: ../Sources/Examples/v1/Echo +[echo-example]: ../Examples/v1/Echo diff --git a/docs/quick-start.md b/docs/quick-start.md index 476928503..e66b91b89 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -109,7 +109,7 @@ message HelloReply { ### Update and run the application We need to regenerate -`Sources/Examples/v1/HelloWorld/Model/helloworld.grpc.swift`, which +`Examples/v1/HelloWorld/Model/helloworld.grpc.swift`, which contains our generated gRPC client and server classes. From the `grpc-swift` directory run @@ -127,7 +127,7 @@ parts of our example application. #### Update the server In the same directory, open -`Sources/Examples/v1/HelloWorld/Server/GreeterProvider.swift`. Implement the new +`Examples/v1/HelloWorld/Server/GreeterProvider.swift`. Implement the new method like this: ```swift @@ -159,7 +159,7 @@ final class GreeterProvider: Helloworld_GreeterAsyncProvider { #### Update the client In the same directory, open -`Sources/Examples/v1/HelloWorld/Client/HelloWorldClient.swift`. Call the new method like this: +`Examples/v1/HelloWorld/Client/HelloWorldClient.swift`. Call the new method like this: ```swift func run() async throws { From ad52f944b2f08d8c5464623fd18f2eac1b664e8e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Aug 2024 10:50:40 +0100 Subject: [PATCH 436/580] Add a 'Hello, World' tutorial (#2023) Motivation: Tutorials are a great way to introduce new users to the library. Modifications: - Add a 'hello, world' tutorial Result: Easier to get started --- .spi.yml | 2 +- Examples/v2/hello-world/HelloWorld.proto | 1 + .../Documentation.docc/Documentation.md | 4 + .../Hello-World/Hello-World.tutorial | 129 ++++++++++++++++++ .../Resources/hello-world-sec02-step01.txt | 1 + .../Resources/hello-world-sec02-step02.txt | 1 + .../Resources/hello-world-sec03-step01.proto | 15 ++ .../Resources/hello-world-sec03-step02.proto | 17 +++ .../Resources/hello-world-sec04-step01.swift | 10 ++ .../Resources/hello-world-sec04-step02.swift | 19 +++ .../Resources/hello-world-sec04-step03.swift | 3 + .../Resources/hello-world-sec04-step04.swift | 6 + .../Resources/hello-world-sec04-step05.txt | 1 + .../Resources/hello-world-sec04-step06.txt | 2 + .../Tutorials/Resources/image.png | Bin 0 -> 68 bytes .../Tutorials/Table-of-Contents.tutorial | 13 ++ scripts/license-check.sh | 1 + 17 files changed, 224 insertions(+), 1 deletion(-) create mode 120000 Examples/v2/hello-world/HelloWorld.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step01.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step02.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step01.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step02.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step05.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step06.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Resources/image.png create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial diff --git a/.spi.yml b/.spi.yml index 235846373..6db16665f 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,5 +1,5 @@ version: 1 builder: configs: - - documentation_targets: [GRPC, GRPCReflectionService, protoc-gen-grpc-swift, _GRPCCore] + - documentation_targets: [GRPC, GRPCReflectionService, protoc-gen-grpc-swift, GRPCCore] swift_version: 6.0 diff --git a/Examples/v2/hello-world/HelloWorld.proto b/Examples/v2/hello-world/HelloWorld.proto new file mode 120000 index 000000000..f746c2066 --- /dev/null +++ b/Examples/v2/hello-world/HelloWorld.proto @@ -0,0 +1 @@ +../../../Protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 26a2618f0..817356072 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -21,6 +21,10 @@ contains products of this package: ## Topics +### Tutorials + +- + ### Getting involved Resources for developers working on gRPC Swift: diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial new file mode 100644 index 000000000..7035fe836 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -0,0 +1,129 @@ +@Tutorial(time: 10) { + @XcodeRequirement( + title: "Xcode 16 Beta 5+", + destination: "https://developer.apple.com/download/" + ) + + @Intro(title: "Quick Start: Hello, World!") { + This tutorial walks you through the canonical "Hello, World!" program for gRPC Swift. You'll + learn how to implement a service using generated stubs, then you'll learn to configure and use + a gRPC server. You'll also see how to create a client and use it to call the server. + + The tutorial assumes you are comfortable with both Swift and the basic concepts of gRPC. If you + aren't then check out the Swift [Getting Started](https://www.swift.org/getting-started/) guide + and the [gRPC website](https://grpc.io) for more information. + + You'll need a local copy of the example code to work through this tutorial. Download the example + code from our GitHub repository if you haven't done so already. You can do this by cloning the + repository by running the following command in a terminal: + + ```console + git clone https://github.com/grpc/grpc-swift + ``` + + The rest of the tutorial assumes that your current working directory is the cloned `grpc-swift` + directory. + } + + @Section(title: "Run a gRPC application") { + Let's start by running the existing Greeter application. + + @Steps { + @Step { + In a terminal run `swift run hello-world serve` to start the server. By default it'll start + listening on port 31415. + + @Code(name: "Console.txt", file: "hello-world-sec02-step01.txt") + } + + @Step { + In another terminal run `swift run hello-world greet` to create a client, connect + to the server you started and send it a request and print the response. + + @Code(name: "Console.txt", file: "hello-world-sec02-step02.txt") + } + + @Step { + Congratulations! You've just run a client-server application with gRPC Swift. You can now + cancel the two running processes. + } + } + } + + @Section(title: "Update a gRPC service") { + Now let's look at how to update the application with an extra method on the server for the + client to call. Our gRPC service is defined using protocol buffers; you can find out lots more + about how to define a service in a `.proto` file in [What is gRPC?](https://grpc.io/docs/what-is-grpc/). + For now all you need to know is that both the server and client "stub" have a `SayHello` RPC + method that takes a `HelloRequest` parameter from the client and returns a `HelloReply` from + the server. + + @Steps { + @Step { + Open `HelloWorld.proto` in the `Examples/v2/hello-world` directory to see how the + service is defined. + + @Code(name: "HelloWorld.proto", file: "hello-world-sec03-step01.proto") + } + + @Step { + Let's update it so that the `Greeter` service has two methods. Add a new `SayHelloAgain` + method, with the same request and response types. + + @Code(name: "HelloWorld.proto", file: "hello-world-sec03-step02.proto") + } + } + } + + @Section(title: "Update and run the application") { + You need to regenerate the stubs as the service definition has changed. To do this run the + following command from the root of the checked out repository: + + ```console + Protos/generate.sh + ``` + + You'll learn how to do this manually in another tutorial. Now that the stubs have been updated + you need to implement and call the new method in the human-written parts of your application. + + @Steps { + @Step { + Open `Serve.swift` in the `Examples/v2/hello-world/Subcommands` directory. + + @Code(name: "Serve.swift", file: "hello-world-sec04-step01.swift") + } + + @Step { + Implement the new method like this: + + @Code(name: "Serve.swift", file: "hello-world-sec04-step02.swift") + } + + @Step { + Let's update the client now. Open `Greet.swift` in the + `Examples/v2/hello-world/Subcommands` directory. + + @Code(name: "Greet.swift", file: "hello-world-sec04-step03.swift") + } + + @Step { + Add a call to the `sayHelloAgain` method: + + @Code(name: "Greet.swift", file: "hello-world-sec04-step04.swift") + } + + @Step { + Just like we did before, open a terminal and start the server by + running `swift run hello-world serve` + + @Code(name: "Console.txt", file: "hello-world-sec04-step05.txt") + } + + @Step { + In a separate terminal run `swift run hello-world greet` to call the server. + + @Code(name: "Console.txt", file: "hello-world-sec04-step06.txt") + } + } + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step01.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step01.txt new file mode 100644 index 000000000..4d6dd6c92 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step01.txt @@ -0,0 +1 @@ +Greeter listening on [ipv4]127.0.0.1:31415 diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step02.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step02.txt new file mode 100644 index 000000000..627d37bbc --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec02-step02.txt @@ -0,0 +1 @@ +Hello, stranger diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step01.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step01.proto new file mode 100644 index 000000000..cdeb0ec5a --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step01.proto @@ -0,0 +1,15 @@ +// The greeting service definition. +service Greeter { + // Sends a greeting. + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings. +message HelloReply { + string message = 1; +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step02.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step02.proto new file mode 100644 index 000000000..a6dbcfd13 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec03-step02.proto @@ -0,0 +1,17 @@ +// The greeting service definition. +service Greeter { + // Sends a greeting. + rpc SayHello (HelloRequest) returns (HelloReply) {} + // Sends another greeting. + rpc SayHelloAgain (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings. +message HelloReply { + string message = 1; +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift new file mode 100644 index 000000000..2c89bab8d --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift @@ -0,0 +1,10 @@ +struct Greeter: Helloworld_GreeterServiceProtocol { + func sayHello( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + var reply = Helloworld_HelloReply() + let recipient = request.message.name.isEmpty ? "stranger" : request.message.name + reply.message = "Hello, \(recipient)" + return ServerResponse.Single(message: reply) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift new file mode 100644 index 000000000..e9dde27f9 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift @@ -0,0 +1,19 @@ +struct Greeter: Helloworld_GreeterServiceProtocol { + func sayHello( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + var reply = Helloworld_HelloReply() + let recipient = request.message.name.isEmpty ? "stranger" : request.message.name + reply.message = "Hello, \(recipient)" + return ServerResponse.Single(message: reply) + } + + func sayHelloAgain( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + var reply = Helloworld_HelloReply() + let recipient = request.message.name.isEmpty ? "stranger" : request.message.name + reply.message = "Hello again, \(recipient)" + return ServerResponse.Single(message: reply) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift new file mode 100644 index 000000000..4e10d4adc --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step03.swift @@ -0,0 +1,3 @@ +let greeter = Helloworld_GreeterClient(wrapping: client) +let reply = try await greeter.sayHello(.with { $0.name = self.name }) +print(reply.message) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift new file mode 100644 index 000000000..534a4fb8a --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step04.swift @@ -0,0 +1,6 @@ +let greeter = Helloworld_GreeterClient(wrapping: client) +let reply = try await greeter.sayHello(.with { $0.name = self.name }) +print(reply.message) + +let replyAgain = try await greeter.sayHelloAgain(.with { $0.name = self.name }) +print(replyAgain.message) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step05.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step05.txt new file mode 100644 index 000000000..4d6dd6c92 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step05.txt @@ -0,0 +1 @@ +Greeter listening on [ipv4]127.0.0.1:31415 diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step06.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step06.txt new file mode 100644 index 000000000..5e45eab71 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step06.txt @@ -0,0 +1,2 @@ +Hello, stranger +Hello again, stranger diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Resources/image.png b/Sources/GRPCCore/Documentation.docc/Tutorials/Resources/image.png new file mode 100644 index 0000000000000000000000000000000000000000..909c66db1740b7c1b41eb4db6c414a7ab5bb6a23 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$DG5Lh8v~O;;{|;n Oi^0>?&t;ucLK6U5DhwL{ literal 0 HcmV?d00001 diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial new file mode 100644 index 000000000..fecc3c270 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial @@ -0,0 +1,13 @@ +@Tutorials(name: "gRPC Swift") { + @Intro(title: "Working with gRPC Swift") { + Learn how to use gRPC Swift to implement gRPC services and call them using generated clients. + } + + @Chapter(name: "Quick Start") { + Follow this "Hello World" tutorial for a quick start with gRPC Swift. + + @Image(source: "image.png") + + @TutorialReference(tutorial: "doc:Hello-World") + } +} diff --git a/scripts/license-check.sh b/scripts/license-check.sh index 8bcdc9769..e006af8bc 100755 --- a/scripts/license-check.sh +++ b/scripts/license-check.sh @@ -107,6 +107,7 @@ check_copyright_headers() { ! -name '*.grpc.swift' \ ! -name 'LinuxMain.swift' \ ! -name 'XCTestManifests.swift' \ + ! -path './Sources/GRPCCore/Documentation.docc/*' \ ! -path './FuzzTesting/.build/*' \ ! -path './Performance/QPSBenchmark/.build/*' \ ! -path './Performance/Benchmarks/.build/*' \ From e3258e93577ecbdf448e6e1af86a3f744bd3777f Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Aug 2024 13:29:11 +0100 Subject: [PATCH 437/580] Fix echo example (#2025) Motivation: The v2 echo example accidentally ignored the response message and printed the text from the input message. Modifications: - Use the response message Result: Fewer warnings --- Examples/v2/echo/Subcommands/Get.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/v2/echo/Subcommands/Get.swift b/Examples/v2/echo/Subcommands/Get.swift index e22d0523c..eb80396dc 100644 --- a/Examples/v2/echo/Subcommands/Get.swift +++ b/Examples/v2/echo/Subcommands/Get.swift @@ -41,7 +41,7 @@ struct Get: AsyncParsableCommand { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } print("get → \(message.text)") let response = try await echo.get(message) - print("get ← \(message.text)") + print("get ← \(response.text)") } client.close() From a1c333b7352284d1b9d8ccab04e5936de490eb27 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 21 Aug 2024 16:16:44 +0100 Subject: [PATCH 438/580] Add a doc on generating stubs (#2027) Motivation: It's not always easy to know how to control the code generator or the SwiftPM build plugin. Modifications: - Add a narrative doc on generating stubs using both approaches Results: Better docs --- .../Articles/Generating-stubs.md | 203 ++++++++++++++++++ .../Documentation.docc/Documentation.md | 4 + .../Hello-World/Hello-World.tutorial | 6 +- 3 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md new file mode 100644 index 000000000..991f173f9 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md @@ -0,0 +1,203 @@ +# Generating stubs + +Learn how to generate stubs for gRPC Swift from a service defined using the Protocol Buffers IDL. + +## Overview + +There are two approaches to generating stubs from Protocol Buffers: + +1. With the Swift Package Manager build plugin, or +2. With the Protocol Buffers compiler (`protoc`). + +The following sections describe how and when to use each. + +### Using the Swift Package Manager build plugin + +You can generate stubs at build time by using `GRPCSwiftPlugin` which is a build plugin for the +Swift Package Manager. Using it means that you don't have to manage the generation of +stubs with separate tooling, or check the generated stubs into your source repository. + +The build plugin will generate gRPC stubs for you by building `protoc-gen-grpc-swift` (more details +in the following section) for you and invoking `protoc`. Because of the implicit +dependency on `protoc` being made available by the system `GRPCSwiftPlugin` isn't suitable for use +in: + +- Library packages, or +- Environments where `protoc` isn't available. + +> `GRPCSwiftPlugin` _only_ generates gRPC stubs, it doesn't generate messages. You must generate +> messages in addition to the gRPC Stubs. The [Swift Protobuf](https://github.com/apple/swift-protobuf) +> project provides an equivalent build plugin, `SwiftProtobufPlugin`, for this. + +#### Configuring the build plugin + +You can configure which stubs `GRPCSwiftPlugin` generates and how via a configuration file. This +must be called `grpc-swift-config.json` and can be placed anywhere in the source directory for your +target. + +A config file for the plugin is made up of a number of `protoc` invocations. Each invocation +describes the inputs to `protoc` as well as any options. + +The following is a list of options which can be applied to each invocation object: +- `protoFiles`, an array of strings where each string is the path to an input `.proto` file + _relative to `grpc-swift-config.json`_. +- `visibility`, a string describing the access level of the generated stub (must be one + of `"public"`, `"internal"`, or `"package"`). If not specified then stubs are generated as + `internal`. +- `server`, a boolean indicating whether server stubs should be generated. Defaults to `true` if + not specified. +- `client`, a boolean indicating whether client stubs should be generated. Defaults to `true` if + not specified. +- `_V2`, a boolean indicated whether the generated stubs should be for v2.x. Defaults to `false` if + not specified. + +> The `GRPCSwiftPlugin` build plugin is currently shared between gRPC Swift v1.x and v2.x. To +> generate stubs for v2.x you _must_ set `_V2` to `true` in your config. +> +> This option will be deprecated and removed once v2.x has been released. + +#### Finding protoc + +The build plugin requires a copy of the `protoc` binary to be available. To resolve which copy of +the binary to use, `GRPCSwiftPlugin` will look at the following in order: + +1. The exact path specified in the `protocPath` property in `grpc-swift-config.json`, if present. +2. The exact path specified in the `PROTOC_PATH` environment variable, if set. +3. The first `protoc` binary found in your `PATH` environment variable. + +#### Using the build plugin from Xcode + +Xcode doesn't have access to your `PATH` so in order to use `GRPCSwiftPlugin` with Xcode you must +either set `protocPath` in your `grpc-swift-config.json` or explicitly set `PROTOC_PATH` when +opening Xcode. + +You can do this by running: + +```sh +env PROTOC_PATH=/path/to/protoc xed /path/to/your-project +``` + +Note that Xcode must _not_ be open before running this command. + +#### Example configuration + +We recommend putting your config and `.proto` files in a directory called `Protos` within your +target. Here's an example package structure: + +``` +MyPackage +├── Package.swift +└── Sources + └── MyTarget + └── Protos + ├── foo + │   └── bar + │   ├── baz.proto + │   └── buzz.proto + └── grpc-swift-config.json +``` + +If you wanted the generated stubs from `baz.proto` to be `public`, and to only generate a client +for `buzz.proto` then the `grpc-swift-config` could look like this: + +```json +{ + "invocations": [ + { + "_V2": true, + "protoFiles": ["foo/bar/baz.proto"], + "visibility": "public" + }, + { + "_V2": true, + "protoFiles": ["foo/bar/buzz.proto"], + "server": false + } + ] +} +``` + +### Using protoc + +If you've used Protocol Buffers before then generating gRPC Swift stubs should be simple. If you're +unfamiliar with Protocol Buffers then you should get comfortable with the concepts before +continuing; the [Protocol Buffers website](https://protobuf.dev/) is a great place to start. + +gRPC Swift provides `protoc-gen-grpc-swift`, a program which is a plugin for the Protocol Buffers +compiler, `protoc`. + +> `protoc-gen-grpc-swift` only generates gRPC stubs, it doesn't generate messages. You must use +> `protoc-gen-swift` to generate messages in addition to gRPC Stubs. + +To generate gRPC stubs for your `.proto` files you must run the `protoc` command with +the `--grpc-swift_out=` option: + +```console +protoc --grpc-swift_out=. my-service.proto +``` + +The presence of `--grpc-swift_out` tells `protoc` to use the `protoc-gen-grpc-swift` plugin. By +default it'll look for the plugin in your `PATH`. You can also specify the path to the plugin +explicitly: + +```console +protoc --plugin=/path/to/protoc-gen-grpc-swift --grpc-swift_out=. my-service.proto +``` + +You can also specify various option the `protoc-gen-grpc-swift` via `protoc` using +the `--grpc-swift_opt` argument: + +```console +protoc --grpc-swift_opt== --grpc-swift_out=. +``` + +You can specify multiple options by passing the `--grpc-swift_opt` argument multiple times: + +```console +protoc \ + --grpc-swift_opt== \ + --grpc-swift_opt== \ + --grpc-swift_out=. +``` + +#### Generator options + +| Name | Possible Values | Default | Description | +|---------------------------|--------------------------------------------|------------|----------------------------------------------------------| +| `_V2` | `True`, `False` | `False` | Whether stubs are generated for gRPC Swift v2.x | +| `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs | +| `Server` | `True`, `False` | `True` | Generate server stubs | +| `Client` | `True`, `False` | `True` | Generate client stubs | +| `FileNaming` | `FullPath`, `PathToUnderscore`, `DropPath` | `FullPath` | How generated source files should be named. (See below.) | +| `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. (See below.) | + +> The `protoc-gen-grpc-swift` binary is currently shared between gRPC Swift v1.x and v2.x. To +> generate stubs for v2.x you _must_ specify `_V2=True`. +> +> This option will be deprecated and removed once v2.x has been released. + +The `FileNaming` option has three possible values, for an input of `foo/bar/baz.proto` the following +output file will be generated: +- `FullPath`: `foo/bar/baz.grpc.swift`. +- `PathToUnderscore`: `foo_bar_baz.grpc.swift` +- `DropPath`: `baz.grpc.swift` + +The code generator assumes all inputs are generated into the same module, `ProtoPathModuleMappings` +allows you to specify a mapping from `.proto` files to the Swift module they are generated in. This +allows the code generator to add appropriate imports to your generated stubs. This is described in +more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md). + +#### Building the plugin + +> The version of `protoc-gen-grpc-swift` you use mustn't be newer than the version of +> the `grpc-swift` you're using. + +If your package depends on `grpc-swift` then you can get a copy of `protoc-gen-grpc-swift` +by building it directly: + +```console +swift build --product protoc-gen-grpc-swift +``` + +This command will build the plugin into `.build/debug` directory. You can get the full path using +`swift build --show-bin-path`. diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 817356072..8cd8f1198 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -25,6 +25,10 @@ contains products of this package: - +### Essentials + +- + ### Getting involved Resources for developers working on gRPC Swift: diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 7035fe836..32ecd4847 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -83,8 +83,10 @@ Protos/generate.sh ``` - You'll learn how to do this manually in another tutorial. Now that the stubs have been updated - you need to implement and call the new method in the human-written parts of your application. + To learn how to generate stubs check out the article. + + Now that the stubs have been updated you need to implement and call the new method in the + human-written parts of your application. @Steps { @Step { From 82a2e2337090a80d210c8f6a9ab935be60829966 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 23 Aug 2024 10:21:05 +0100 Subject: [PATCH 439/580] Allow access level on imports to be disabled (#2032) Motivation: The v2 code generator defaults to generating access levels on imports. SwiftProtobuf also has but hasn't yet been released. This causes problems as explicit 'internal' imports in gRPC are ambiguous with the implicit imports from Protobuf. Modifications: - Allow an opt-out of explicit imports for v2 code generation Result: Can build against current version of Protobuf without `InternalImportsByDefault`. --- .../IDLToStructuredSwiftTranslator.swift | 26 ++++++++++---- .../Internal/Translator/Translator.swift | 2 ++ Sources/GRPCCodeGen/SourceGenerator.swift | 28 ++++++++++++--- .../Articles/Generating-stubs.md | 1 + Sources/protoc-gen-grpc-swift/main.swift | 1 + Sources/protoc-gen-grpc-swift/options.swift | 8 +++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 9 +++++ .../ProtobufCodeGeneratorTests.swift | 34 +++++++++++++++++-- 8 files changed, 96 insertions(+), 13 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 8adfc7868..99c9acfaa 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -21,6 +21,7 @@ struct IDLToStructuredSwiftTranslator: Translator { func translate( codeGenerationRequest: CodeGenerationRequest, accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevelOnImports: Bool, client: Bool, server: Bool ) throws -> StructuredSwiftRepresentation { @@ -30,11 +31,23 @@ struct IDLToStructuredSwiftTranslator: Translator { server: server, accessLevel: accessLevel ) + let topComment = Comment.preFormatted(codeGenerationRequest.leadingTrivia) - let imports = try codeGenerationRequest.dependencies.reduce( - into: [ImportDescription(accessLevel: AccessModifier(accessLevel), moduleName: "GRPCCore")] - ) { partialResult, newDependency in - try partialResult.append(translateImport(dependency: newDependency)) + + var imports: [ImportDescription] = [] + imports.append( + ImportDescription( + accessLevel: accessLevelOnImports ? AccessModifier(accessLevel) : nil, + moduleName: "GRPCCore" + ) + ) + + for dependency in codeGenerationRequest.dependencies { + let importDescription = try self.translateImport( + dependency: dependency, + accessLevelOnImports: accessLevelOnImports + ) + imports.append(importDescription) } var codeBlocks = [CodeBlock]() @@ -79,10 +92,11 @@ extension AccessModifier { extension IDLToStructuredSwiftTranslator { private func translateImport( - dependency: CodeGenerationRequest.Dependency + dependency: CodeGenerationRequest.Dependency, + accessLevelOnImports: Bool ) throws -> ImportDescription { var importDescription = ImportDescription( - accessLevel: AccessModifier(dependency.accessLevel), + accessLevel: accessLevelOnImports ? AccessModifier(dependency.accessLevel) : nil, moduleName: dependency.module ) if let item = dependency.item { diff --git a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift index 14c0c7042..d8ad3210e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift @@ -22,6 +22,7 @@ protocol Translator { /// - Parameters: /// - codeGenerationRequest: The IDL described RPCs representation. /// - accessLevel: The access level that will restrict the protocols, extensions and methods in the generated code. + /// - accessLevelOnImports: Whether access levels should be included on imports. /// - client: Whether or not client code should be generated from the IDL described RPCs representation. /// - server: Whether or not server code should be generated from the IDL described RPCs representation. /// - Returns: A structured Swift representation of the generated code. @@ -29,6 +30,7 @@ protocol Translator { func translate( codeGenerationRequest: CodeGenerationRequest, accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevelOnImports: Bool, client: Bool, server: Bool ) throws -> StructuredSwiftRepresentation diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift index 55b1fc54e..3130e15be 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -28,6 +28,8 @@ public struct SourceGenerator: Sendable { public struct Configuration: Sendable { /// The access level the generated code will have. public var accessLevel: AccessLevel + /// Whether imports have explicit access levels. + public var accessLevelOnImports: Bool /// The indentation of the generated code as the number of spaces. public var indentation: Int /// Whether or not client code should be generated. @@ -35,8 +37,23 @@ public struct SourceGenerator: Sendable { /// Whether or not server code should be generated. public var server: Bool - public init(accessLevel: AccessLevel, client: Bool, server: Bool, indentation: Int = 4) { + /// Creates a new configuration. + /// + /// - Parameters: + /// - accessLevel: The access level the generated code will have. + /// - accessLevelOnImports: Whether imports have explicit access levels. + /// - client: Whether or not client code should be generated. + /// - server: Whether or not server code should be generated. + /// - indentation: The indentation of the generated code as the number of spaces. + public init( + accessLevel: AccessLevel, + accessLevelOnImports: Bool, + client: Bool, + server: Bool, + indentation: Int = 4 + ) { self.accessLevel = accessLevel + self.accessLevelOnImports = accessLevelOnImports self.indentation = indentation self.client = client self.server = server @@ -65,16 +82,17 @@ public struct SourceGenerator: Sendable { /// The function that transforms a ``CodeGenerationRequest`` object into a ``SourceFile`` object containing /// the generated code, in accordance to the configurations set by the user for the ``SourceGenerator``. public func generate( - _ serviceRepresentation: CodeGenerationRequest + _ request: CodeGenerationRequest ) throws -> SourceFile { let translator = IDLToStructuredSwiftTranslator() let textRenderer = TextBasedRenderer(indentation: self.configuration.indentation) let structuredSwiftRepresentation = try translator.translate( - codeGenerationRequest: serviceRepresentation, + codeGenerationRequest: request, accessLevel: self.configuration.accessLevel, - client: configuration.client, - server: configuration.server + accessLevelOnImports: self.configuration.accessLevelOnImports, + client: self.configuration.client, + server: self.configuration.server ) let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md index 991f173f9..34b57f8d2 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md @@ -170,6 +170,7 @@ protoc \ | `Client` | `True`, `False` | `True` | Generate client stubs | | `FileNaming` | `FullPath`, `PathToUnderscore`, `DropPath` | `FullPath` | How generated source files should be named. (See below.) | | `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. (See below.) | +| `AccessLevelOnImports` | `True`, `False` | `True` | Whether imports should have explicit access levels. | > The `protoc-gen-grpc-swift` binary is currently shared between gRPC Swift v1.x and v2.x. To > generate stubs for v2.x you _must_ specify `_V2=True`. diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 36dee4503..b53d2f2eb 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -221,6 +221,7 @@ extension SourceGenerator.Configuration { } self.init( accessLevel: accessLevel, + accessLevelOnImports: options.useAccessLevelOnImports, client: options.generateClient, server: options.generateServer ) diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/options.swift index ec1f9e8e0..ead277108 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/options.swift @@ -71,6 +71,7 @@ final class GeneratorOptions { #if compiler(>=6.0) private(set) var v2 = false #endif + private(set) var useAccessLevelOnImports = true init(parameter: String?) throws { for pair in GeneratorOptions.parseParameter(string: parameter) { @@ -166,6 +167,13 @@ final class GeneratorOptions { } #endif + case "UseAccessLevelOnImports": + if let value = Bool(pair.value) { + self.useAccessLevelOnImports = value + } else { + throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) + } + default: throw GenerationError.unknownParameter(name: pair.key) } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 539189941..2ae6c9ceb 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -275,6 +275,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let structuredSwift = try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: accessLevel, + accessLevelOnImports: true, client: false, server: server ) @@ -299,6 +300,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .public, + accessLevelOnImports: true, client: true, server: true ) @@ -339,6 +341,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .public, + accessLevelOnImports: true, client: true, server: true ) @@ -374,6 +377,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .public, + accessLevelOnImports: true, client: true, server: true ) @@ -421,6 +425,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .internal, + accessLevelOnImports: true, client: true, server: true ) @@ -466,6 +471,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .public, + accessLevelOnImports: true, client: true, server: true ) @@ -519,6 +525,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .public, + accessLevelOnImports: true, client: true, server: true ) @@ -572,6 +579,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .public, + accessLevelOnImports: true, client: true, server: true ) @@ -618,6 +626,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: .public, + accessLevelOnImports: true, client: true, server: true ) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 8f541e1c6..a18fb275c 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -471,22 +471,52 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) } + func testNoAccessLevelOnImports() throws { + let proto = Google_Protobuf_FileDescriptorProto(name: "helloworld.proto", package: "") + try testCodeGeneration( + proto: proto, + indentation: 2, + visibility: .package, + client: true, + server: true, + accessLevelOnImports: false, + expectedCode: """ + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: helloworld.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + import GRPCCore + import GRPCProtobuf + import ExtraModule + + """ + ) + } + func testCodeGeneration( proto: Google_Protobuf_FileDescriptorProto, indentation: Int, visibility: SourceGenerator.Configuration.AccessLevel, client: Bool, server: Bool, + accessLevelOnImports: Bool = true, expectedCode: String, file: StaticString = #filePath, line: UInt = #line ) throws { - let configs = SourceGenerator.Configuration( + let config = SourceGenerator.Configuration( accessLevel: visibility, + accessLevelOnImports: accessLevelOnImports, client: client, server: server, indentation: indentation ) + let descriptorSet = DescriptorSet( protos: [ Google_Protobuf_FileDescriptorProto(name: "same-module.proto", package: "same-package"), @@ -512,7 +542,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } ] } - let generator = ProtobufCodeGenerator(configuration: configs) + let generator = ProtobufCodeGenerator(configuration: config) try XCTAssertEqualWithDiff( try generator.generateCode( from: fileDescriptor, From 1699a6fd99030b0b8973752660001e34bbdda860 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 23 Aug 2024 12:44:00 +0100 Subject: [PATCH 440/580] Add helpers for creating transports (#2029) Motivation: Creating an HTTP/2 transport requires quite a bit of typing and knowing what types to use. We can make use of type inference to make this a bit easier for users. Modifications: - Add constrained extensions to `ClientTransport` and `ServerTransport` allowing for `http2NIO` and `http2NIOTS` transports to be created via static methods. This makes it possible for transports to be created inline in the `init` of a `GRPCClient` or `GRPCServer` without the users need the full type. - Extend the `defaults` functions so that they can take an optional trailing closure to further modify the defaults. This allows for configs to be created and modified inline when making a transport. - A config is now required to create client transports and the NIOTS server transport as these will soon require a transport security config. Results: Easier to create clients/servers --- Examples/v2/echo/Subcommands/Collect.swift | 8 +++- Examples/v2/echo/Subcommands/Expand.swift | 8 +++- Examples/v2/echo/Subcommands/Get.swift | 8 +++- Examples/v2/echo/Subcommands/Serve.swift | 16 ++++--- Examples/v2/echo/Subcommands/Update.swift | 8 +++- .../v2/hello-world/Subcommands/Greet.swift | 8 +++- .../v2/hello-world/Subcommands/Serve.swift | 16 ++++--- Sources/GRPCCore/GRPCServer.swift | 2 +- .../ListeningServerTransport.swift | 41 +++++++++++++++++ .../HTTP2ClientTransport+Posix.swift | 44 +++++++++++++++++-- .../HTTP2ServerTransport+Posix.swift | 36 +++++++++++++-- ...TP2ServerTransport+TransportServices.swift | 36 ++++++++++++--- .../InteroperabilityTestsExecutable.swift | 31 ++++++------- .../PerformanceWorker.swift | 11 ++--- .../performance-worker/WorkerService.swift | 8 +++- ...P2TransportNIOTransportServicesTests.swift | 15 ++++--- .../HTTP2TransportTests.swift | 40 +++++++++-------- 17 files changed, 255 insertions(+), 81 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/ListeningServerTransport.swift diff --git a/Examples/v2/echo/Subcommands/Collect.swift b/Examples/v2/echo/Subcommands/Collect.swift index f723978a3..ede689925 100644 --- a/Examples/v2/echo/Subcommands/Collect.swift +++ b/Examples/v2/echo/Subcommands/Collect.swift @@ -29,8 +29,12 @@ struct Collect: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) - let client = GRPCClient(transport: transport) + let client = GRPCClient( + transport: try .http2NIOPosix( + target: self.arguments.target, + config: .defaults() + ) + ) try await withThrowingDiscardingTaskGroup { group in group.addTask { diff --git a/Examples/v2/echo/Subcommands/Expand.swift b/Examples/v2/echo/Subcommands/Expand.swift index c3135d544..9eb9c8821 100644 --- a/Examples/v2/echo/Subcommands/Expand.swift +++ b/Examples/v2/echo/Subcommands/Expand.swift @@ -29,8 +29,12 @@ struct Expand: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) - let client = GRPCClient(transport: transport) + let client = GRPCClient( + transport: try .http2NIOPosix( + target: self.arguments.target, + config: .defaults() + ) + ) try await withThrowingDiscardingTaskGroup { group in group.addTask { diff --git a/Examples/v2/echo/Subcommands/Get.swift b/Examples/v2/echo/Subcommands/Get.swift index eb80396dc..0e686a906 100644 --- a/Examples/v2/echo/Subcommands/Get.swift +++ b/Examples/v2/echo/Subcommands/Get.swift @@ -27,8 +27,12 @@ struct Get: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) - let client = GRPCClient(transport: transport) + let client = GRPCClient( + transport: try .http2NIOPosix( + target: self.arguments.target, + config: .defaults() + ) + ) try await withThrowingDiscardingTaskGroup { group in group.addTask { diff --git a/Examples/v2/echo/Subcommands/Serve.swift b/Examples/v2/echo/Subcommands/Serve.swift index 3ce40ae8e..e9c50bb46 100644 --- a/Examples/v2/echo/Subcommands/Serve.swift +++ b/Examples/v2/echo/Subcommands/Serve.swift @@ -27,15 +27,19 @@ struct Serve: AsyncParsableCommand { var port: Int = 1234 func run() async throws { - let transport = HTTP2ServerTransport.Posix( - address: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults(transportSecurity: .plaintext) + ), + services: [EchoService()] ) - let server = GRPCServer(transport: transport, services: [EchoService()]) + try await withThrowingDiscardingTaskGroup { group in group.addTask { try await server.run() } - let address = try await transport.listeningAddress - print("server listening on \(address)") + if let address = try await server.listeningAddress { + print("Echo listening on \(address)") + } } } } diff --git a/Examples/v2/echo/Subcommands/Update.swift b/Examples/v2/echo/Subcommands/Update.swift index a580c25fb..2e5b6ed52 100644 --- a/Examples/v2/echo/Subcommands/Update.swift +++ b/Examples/v2/echo/Subcommands/Update.swift @@ -29,8 +29,12 @@ struct Update: AsyncParsableCommand { var arguments: ClientArguments func run() async throws { - let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target) - let client = GRPCClient(transport: transport) + let client = GRPCClient( + transport: try .http2NIOPosix( + target: self.arguments.target, + config: .defaults() + ) + ) try await withThrowingDiscardingTaskGroup { group in group.addTask { diff --git a/Examples/v2/hello-world/Subcommands/Greet.swift b/Examples/v2/hello-world/Subcommands/Greet.swift index 2e12bda8a..3b8edae99 100644 --- a/Examples/v2/hello-world/Subcommands/Greet.swift +++ b/Examples/v2/hello-world/Subcommands/Greet.swift @@ -30,8 +30,12 @@ struct Greet: AsyncParsableCommand { func run() async throws { try await withThrowingDiscardingTaskGroup { group in - let http2 = try HTTP2ClientTransport.Posix(target: .ipv4(host: "127.0.0.1", port: self.port)) - let client = GRPCClient(transport: http2) + let client = GRPCClient( + transport: try .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults() + ) + ) group.addTask { try await client.run() diff --git a/Examples/v2/hello-world/Subcommands/Serve.swift b/Examples/v2/hello-world/Subcommands/Serve.swift index 210989102..56f80c3fa 100644 --- a/Examples/v2/hello-world/Subcommands/Serve.swift +++ b/Examples/v2/hello-world/Subcommands/Serve.swift @@ -26,17 +26,19 @@ struct Serve: AsyncParsableCommand { var port: Int = 31415 func run() async throws { - let http2 = HTTP2ServerTransport.Posix( - address: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults(transportSecurity: .plaintext) + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults(transportSecurity: .plaintext) + ), + services: [Greeter()] ) - let server = GRPCServer(transport: http2, services: [Greeter()]) - try await withThrowingDiscardingTaskGroup { group in group.addTask { try await server.run() } - let address = try await http2.listeningAddress - print("Greeter listening on \(address)") + if let address = try await server.listeningAddress { + print("Greeter listening on \(address)") + } } } } diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index e0700f380..c4e2534a5 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -74,7 +74,7 @@ public struct GRPCServer: Sendable { typealias Stream = RPCStream /// The ``ServerTransport`` implementation that the server uses to listen for new requests. - private let transport: any ServerTransport + public let transport: any ServerTransport /// The services registered which the server is serving. private let router: RPCRouter diff --git a/Sources/GRPCHTTP2Core/ListeningServerTransport.swift b/Sources/GRPCHTTP2Core/ListeningServerTransport.swift new file mode 100644 index 000000000..20150d360 --- /dev/null +++ b/Sources/GRPCHTTP2Core/ListeningServerTransport.swift @@ -0,0 +1,41 @@ +/* + * 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. + */ + +public import GRPCCore + +/// A transport which refines `ServerTransport` to provide the socket address of a listening +/// server. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public protocol ListeningServerTransport: ServerTransport { + /// Returns the listening address of the server transport once it has started. + var listeningAddress: SocketAddress { get async throws } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension GRPCServer { + /// Returns the listening address of the server transport once it has started. + /// + /// This will be `nil` if the transport doesn't conform to ``ListeningServerTransport``. + public var listeningAddress: SocketAddress? { + get async throws { + if let listener = self.transport as? (any ListeningServerTransport) { + return try await listener.listeningAddress + } else { + return nil + } + } + } +} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index 1d7cff13e..ad6bfdc7e 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -59,8 +59,8 @@ extension HTTP2ClientTransport { /// /// - Parameters: /// - target: A target to resolve. - /// - resolverRegistry: A registry of resolver factories. /// - config: Configuration for the transport. + /// - resolverRegistry: A registry of resolver factories. /// - serviceConfig: Service config controlling how the transport should establish and /// load-balance connections. /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must @@ -69,8 +69,8 @@ extension HTTP2ClientTransport { /// - Throws: When no suitable resolver could be found for the `target`. public init( target: any ResolvableTarget, + config: Config, resolverRegistry: NameResolverRegistry = .defaults, - config: Config = .defaults, serviceConfig: ServiceConfig = ServiceConfig(), eventLoopGroup: any EventLoopGroup = .singletonMultiThreadedEventLoopGroup ) throws { @@ -180,13 +180,18 @@ extension HTTP2ClientTransport.Posix { } /// Default values. - public static var defaults: Self { - Self( + /// + /// - Parameters: + /// - configure: A closure which allows you to modify the defaults before returning them. + public static func defaults(_ configure: (_ config: inout Self) -> Void = { _ in }) -> Self { + var config = Self( http2: .defaults, backoff: .defaults, connection: .defaults, compression: .defaults ) + configure(&config) + return config } } } @@ -202,3 +207,34 @@ extension GRPCChannel.Config { ) } } + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension ClientTransport where Self == HTTP2ClientTransport.Posix { + /// Creates a new Posix based HTTP/2 client transport. + /// + /// - Parameters: + /// - target: A target to resolve. + /// - config: Configuration for the transport. + /// - resolverRegistry: A registry of resolver factories. + /// - serviceConfig: Service config controlling how the transport should establish and + /// load-balance connections. + /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must + /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from + /// a `MultiThreadedEventLoopGroup`. + /// - Throws: When no suitable resolver could be found for the `target`. + public static func http2NIOPosix( + target: any ResolvableTarget, + config: HTTP2ClientTransport.Posix.Config, + resolverRegistry: NameResolverRegistry = .defaults, + serviceConfig: ServiceConfig = ServiceConfig(), + eventLoopGroup: any EventLoopGroup = .singletonMultiThreadedEventLoopGroup + ) throws -> Self { + return try HTTP2ClientTransport.Posix( + target: target, + config: config, + resolverRegistry: resolverRegistry, + serviceConfig: serviceConfig, + eventLoopGroup: eventLoopGroup + ) + } +} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 450865a25..5d29fe2aa 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -28,7 +28,7 @@ import NIOSSL extension HTTP2ServerTransport { /// A NIOPosix-backed implementation of a server transport. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct Posix: ServerTransport { + public struct Posix: ServerTransport, ListeningServerTransport { private let address: GRPCHTTP2Core.SocketAddress private let config: Config private let eventLoopGroup: MultiThreadedEventLoopGroup @@ -370,16 +370,23 @@ extension HTTP2ServerTransport.Posix { } /// Default values for the different configurations. + /// + /// - Parameters: + /// - transportSecurity: The security settings applied to the transport. + /// - configure: A closure which allows you to modify the defaults before returning them. public static func defaults( - transportSecurity: TransportSecurity + transportSecurity: TransportSecurity, + configure: (_ config: inout Self) -> Void = { _ in } ) -> Self { - Self( + var config = Self( compression: .defaults, connection: .defaults, http2: .defaults, rpc: .defaults, transportSecurity: transportSecurity ) + configure(&config) + return config } } } @@ -421,3 +428,26 @@ extension ServerBootstrap { } } } + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension ServerTransport where Self == HTTP2ServerTransport.Posix { + /// Create a new `Posix` based HTTP/2 server transport. + /// + /// - Parameters: + /// - address: The address to which the server should be bound. + /// - config: The transport configuration. + /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to the server on. This must + /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from + /// a `MultiThreadedEventLoopGroup`. + public static func http2NIOPosix( + address: GRPCHTTP2Core.SocketAddress, + config: HTTP2ServerTransport.Posix.Config, + eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup + ) -> Self { + return HTTP2ServerTransport.Posix( + address: address, + config: config, + eventLoopGroup: eventLoopGroup + ) + } +} diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 1538a6767..f2b4c44c3 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -26,7 +26,7 @@ internal import NIOHTTP2 extension HTTP2ServerTransport { /// A NIO Transport Services-backed implementation of a server transport. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct TransportServices: ServerTransport { + public struct TransportServices: ServerTransport, ListeningServerTransport { private let address: GRPCHTTP2Core.SocketAddress private let config: Config private let eventLoopGroup: NIOTSEventLoopGroup @@ -145,7 +145,7 @@ extension HTTP2ServerTransport { /// - eventLoopGroup: The ELG from which to get ELs to run this transport. public init( address: GRPCHTTP2Core.SocketAddress, - config: Config = .defaults, + config: Config, eventLoopGroup: NIOTSEventLoopGroup = .singletonNIOTSEventLoopGroup ) { self.address = address @@ -282,7 +282,6 @@ extension HTTP2ServerTransport { self.serverQuiescingHelper.initiateShutdown(promise: nil) } } - } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -317,13 +316,18 @@ extension HTTP2ServerTransport.TransportServices { } /// Default values for the different configurations. - public static var defaults: Self { - Self( + /// + /// - Parameter configure: A closure which allows you to modify the defaults before + /// returning them. + public static func defaults(configure: (_ config: inout Self) -> Void = { _ in }) -> Self { + var config = Self( compression: .defaults, connection: .defaults, http2: .defaults, rpc: .defaults ) + configure(&config) + return config } } } @@ -368,4 +372,26 @@ extension NIOTSListenerBootstrap { } } } + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension ServerTransport where Self == HTTP2ServerTransport.TransportServices { + /// Create a new `TransportServices` based HTTP/2 server transport. + /// + /// - Parameters: + /// - address: The address to which the server should be bound. + /// - config: The transport configuration. + /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to the server on. This must + /// be a `NIOTSEventLoopGroup` or an `EventLoop` from a `NIOTSEventLoopGroup`. + public static func http2NIOTS( + address: GRPCHTTP2Core.SocketAddress, + config: HTTP2ServerTransport.TransportServices.Config, + eventLoopGroup: NIOTSEventLoopGroup = .singletonNIOTSEventLoopGroup + ) -> Self { + return HTTP2ServerTransport.TransportServices( + address: address, + config: config, + eventLoopGroup: eventLoopGroup + ) + } +} #endif diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift index a47ab8b3f..cd92cba3c 100644 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -38,15 +38,15 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { var port: Int func run() async throws { - var transportConfig = HTTP2ServerTransport.Posix.Config.defaults( - transportSecurity: .plaintext - ) - transportConfig.compression.enabledAlgorithms = .all - let transport = HTTP2ServerTransport.Posix( - address: .ipv4(host: "0.0.0.0", port: self.port), - config: transportConfig + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "0.0.0.0", port: self.port), + config: .defaults(transportSecurity: .plaintext) { + $0.compression.enabledAlgorithms = .all + } + ), + services: [TestService()] ) - let server = GRPCServer(transport: transport, services: [TestService()]) try await server.run() } } @@ -102,15 +102,16 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { } private func buildClient(host: String, port: Int) throws -> GRPCClient { - var transportConfig = HTTP2ClientTransport.Posix.Config.defaults - transportConfig.compression.enabledAlgorithms = .all let serviceConfig = ServiceConfig(loadBalancingConfig: [.roundRobin]) - let transport = try HTTP2ClientTransport.Posix( - target: .ipv4(host: host, port: port), - config: transportConfig, - serviceConfig: serviceConfig + return GRPCClient( + transport: try .http2NIOPosix( + target: .ipv4(host: host, port: port), + config: .defaults { + $0.compression.enabledAlgorithms = .all + }, + serviceConfig: serviceConfig + ) ) - return GRPCClient(transport: transport) } private func runTest( diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift index a52fc4d37..5b1ec8a20 100644 --- a/Sources/performance-worker/PerformanceWorker.swift +++ b/Sources/performance-worker/PerformanceWorker.swift @@ -50,12 +50,13 @@ struct PerformanceWorker: AsyncParsableCommand { print("[WARNING] performance-worker built in DEBUG mode, results won't be representative.") } - let transport = HTTP2ServerTransport.Posix( - address: .ipv4(host: "127.0.0.1", port: self.driverPort), - config: .defaults(transportSecurity: .plaintext) + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: self.driverPort), + config: .defaults(transportSecurity: .plaintext) + ), + services: [WorkerService()] ) - - let server = GRPCServer(transport: transport, services: [WorkerService()]) try await server.run() } } diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 8f090dd00..413314ff0 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -453,9 +453,13 @@ extension WorkerService { var clients = [BenchmarkClient]() for _ in 0 ..< config.clientChannels { - let transport = try HTTP2ClientTransport.Posix(target: target) let client = BenchmarkClient( - client: GRPCClient(transport: transport), + client: GRPCClient( + transport: try .http2NIOPosix( + target: target, + config: .defaults() + ) + ), concurrentRPCs: Int(config.outstandingRpcsPerChannel), rpcType: rpcType, messagesPerStream: Int(config.messagesPerStream), diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index 421a419f3..ce21a0ad9 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -24,7 +24,8 @@ internal import XCTest final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_IPv4() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .ipv4(host: "0.0.0.0", port: 0) + address: .ipv4(host: "0.0.0.0", port: 0), + config: .defaults() ) try await withThrowingDiscardingTaskGroup { group in @@ -43,7 +44,8 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_IPv6() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .ipv6(host: "::1", port: 0) + address: .ipv6(host: "::1", port: 0), + config: .defaults() ) try await withThrowingDiscardingTaskGroup { group in @@ -62,7 +64,8 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_UnixDomainSocket() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .unixDomainSocket(path: "/tmp/niots-uds-test") + address: .unixDomainSocket(path: "/tmp/niots-uds-test"), + config: .defaults() ) defer { // NIOTS does not unlink the UDS on close. @@ -87,7 +90,8 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_InvalidAddress() async { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .unixDomainSocket(path: "/this/should/be/an/invalid/path") + address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"), + config: .defaults() ) try? await withThrowingDiscardingTaskGroup { group in @@ -115,7 +119,8 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_StoppedListening() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .ipv4(host: "0.0.0.0", port: 0) + address: .ipv4(host: "0.0.0.0", port: 0), + config: .defaults() ) try? await withThrowingDiscardingTaskGroup { group in diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 8a70959bf..673289b29 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -144,36 +144,40 @@ final class HTTP2TransportTests: XCTestCase { switch kind { case .posix: - var config = HTTP2ServerTransport.Posix.Config.defaults(transportSecurity: .plaintext) - config.compression.enabledAlgorithms = compression - let transport = HTTP2ServerTransport.Posix( - address: .ipv4(host: "127.0.0.1", port: 0), - config: config + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: 0), + config: .defaults(transportSecurity: .plaintext) { + $0.compression.enabledAlgorithms = compression + } + ), + services: services ) - let server = GRPCServer(transport: transport, services: services) group.addTask { try await server.run() } - let address = try await transport.listeningAddress + let address = try await server.listeningAddress! return (server, address) case .niots: #if canImport(Network) - var config = HTTP2ServerTransport.TransportServices.Config.defaults - config.compression.enabledAlgorithms = compression - let transport = HTTP2ServerTransport.TransportServices( - address: .ipv4(host: "127.0.0.1", port: 0), - config: config + let server = GRPCServer( + transport: .http2NIOTS( + address: .ipv4(host: "127.0.0.1", port: 0), + config: .defaults { + $0.compression.enabledAlgorithms = compression + } + ), + services: services ) - let server = GRPCServer(transport: transport, services: services) group.addTask { try await server.run() } - let address = try await transport.listeningAddress + let address = try await server.listeningAddress! return (server, address) #else throw XCTSkip("Transport not supported on this platform") @@ -191,14 +195,14 @@ final class HTTP2TransportTests: XCTestCase { switch kind { case .posix: - var config = HTTP2ClientTransport.Posix.Config.defaults - config.compression.algorithm = compression - config.compression.enabledAlgorithms = enabledCompression var serviceConfig = ServiceConfig() serviceConfig.loadBalancingConfig = [.roundRobin] transport = try HTTP2ClientTransport.Posix( target: target, - config: config, + config: .defaults { + $0.compression.algorithm = compression + $0.compression.enabledAlgorithms = enabledCompression + }, serviceConfig: serviceConfig ) From 7e6f4cfac2febd67c3b7def5000bd7fab9f5a263 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 26 Aug 2024 08:32:57 +0100 Subject: [PATCH 441/580] Point to correct docs from README (#2034) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92d0bb405..df7eced27 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,4 @@ Please get involved! See our [guidelines for contributing](CONTRIBUTING.md). [gh-grpc]: https://github.com/grpc/grpc [grpcio]: https://grpc.io -[spi-grpc-swift-main]: https://swiftpackageindex.com/grpc/grpc-swift/main/documentation/grpc +[spi-grpc-swift-main]: https://swiftpackageindex.com/grpc/grpc-swift/main/documentation/grpccore From 397dcf7ba97d4f17d5589c398454264a392ed1cd Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 26 Aug 2024 08:43:27 +0100 Subject: [PATCH 442/580] Add a route guide tutorial (#2033) Motivation: The Hello World tutorial is very basic, only covering unary RPCs. It doesn't cover writing the service definition, generating code, creating client/server. Modifications: - Add the basic tutorial covering the route guide example Result: More tutorials --------- Co-authored-by: Gus Cairo --- .../Documentation.docc/Documentation.md | 1 + .../route-guide-sec01-step01-mkdir.txt | 1 + .../Resources/route-guide-sec01-step02-cd.txt | 1 + .../route-guide-sec01-step03-mkdir.txt | 1 + ...ute-guide-sec01-step04-tools-version.swift | 1 + .../route-guide-sec01-step05-import.swift | 2 + ...route-guide-sec01-step06-description.swift | 8 + ...route-guide-sec01-step07-description.swift | 12 + ...route-guide-sec01-step08-description.swift | 21 + .../route-guide-sec02-step01-import.proto | 3 + .../route-guide-sec02-step02-service.proto | 7 + .../route-guide-sec02-step03-unary.proto | 8 + ...-guide-sec02-step04-server-streaming.proto | 14 + ...-guide-sec02-step05-client-streaming.proto | 18 + ...te-guide-sec02-step06-bidi-streaming.proto | 22 + .../route-guide-sec02-step07-messages.proto | 80 +++ ...oute-guide-sec03-step01-protoc-plugins.txt | 2 + .../route-guide-sec03-step02-mkdir.txt | 1 + .../route-guide-sec03-step03-gen-messages.txt | 4 + .../route-guide-sec03-step04-gen-grpc.txt | 6 + .../route-guide-sec04-step01-struct.swift | 4 + ...ute-guide-sec04-step02-unimplemented.swift | 23 + .../route-guide-sec04-step03-features.swift | 32 ++ .../route-guide-sec04-step04-unary.swift | 43 ++ .../route-guide-sec04-step05-unary.swift | 57 ++ ...-guide-sec04-step06-server-streaming.swift | 73 +++ ...-guide-sec04-step07-server-streaming.swift | 75 +++ ...-guide-sec04-step08-client-streaming.swift | 154 ++++++ ...te-guide-sec04-step09-bidi-streaming.swift | 185 +++++++ ...te-guide-sec04-step10-bidi-streaming.swift | 192 +++++++ .../route-guide-sec05-step00-package.swift | 21 + .../route-guide-sec05-step01-package.swift | 24 + .../route-guide-sec05-step02-package.swift | 26 + .../route-guide-sec05-step03-main.swift | 10 + ...ute-guide-sec05-step04-load-features.swift | 23 + .../route-guide-sec05-step05-run-server.swift | 4 + ...oute-guide-sec05-step06-init-service.swift | 6 + .../route-guide-sec05-step07-server.swift | 15 + .../route-guide-sec05-step08-run.swift | 25 + ...e-guide-sec06-step01-call-run-client.swift | 25 + ...te-guide-sec06-step02-add-run-client.swift | 6 + ...ute-guide-sec06-step03-create-client.swift | 12 + .../route-guide-sec06-step04-run-client.swift | 14 + .../route-guide-sec06-step05-stub.swift | 16 + ...route-guide-sec06-step06-get-feature.swift | 29 + ...ute-guide-sec06-step07-list-features.swift | 53 ++ ...oute-guide-sec06-step08-record-route.swift | 76 +++ .../route-guide-sec06-step09-route-chat.swift | 107 ++++ .../route-guide-sec07-step01-server.txt | 1 + .../route-guide-sec07-step02-client.txt | 76 +++ .../Route-Guide/Route-Guide.tutorial | 500 ++++++++++++++++++ .../Tutorials/Table-of-Contents.tutorial | 8 + 52 files changed, 2128 insertions(+) create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step01-mkdir.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step02-cd.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step03-mkdir.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step04-tools-version.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step05-import.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step06-description.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step01-import.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step02-service.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step03-unary.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step04-server-streaming.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step05-client-streaming.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step06-bidi-streaming.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step07-messages.proto create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step01-protoc-plugins.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step02-mkdir.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step03-gen-messages.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step03-main.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step04-load-features.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step05-run-server.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step06-init-service.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step01-call-run-client.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step01-server.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step02-client.txt create mode 100644 Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 8cd8f1198..de33f0bb1 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -24,6 +24,7 @@ contains products of this package: ### Tutorials - +- ### Essentials diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step01-mkdir.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step01-mkdir.txt new file mode 100644 index 000000000..f20a12d5a --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step01-mkdir.txt @@ -0,0 +1 @@ +$ mkdir RouteGuide diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step02-cd.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step02-cd.txt new file mode 100644 index 000000000..f92e41733 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step02-cd.txt @@ -0,0 +1 @@ +$ cd RouteGuide diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step03-mkdir.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step03-mkdir.txt new file mode 100644 index 000000000..bb89324f6 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step03-mkdir.txt @@ -0,0 +1 @@ +$ mkdir Sources Protos diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step04-tools-version.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step04-tools-version.swift new file mode 100644 index 000000000..d99076027 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step04-tools-version.swift @@ -0,0 +1 @@ +// swift-tools-version: 6.0 diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step05-import.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step05-import.swift new file mode 100644 index 000000000..f41662016 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step05-import.swift @@ -0,0 +1,2 @@ +// swift-tools-version: 6.0 +import PackageDescription diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step06-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step06-description.swift new file mode 100644 index 000000000..0219d2c28 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step06-description.swift @@ -0,0 +1,8 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "RouteGuide", + dependencies: [], + targets: [] +) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift new file mode 100644 index 000000000..baaffab21 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift @@ -0,0 +1,12 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "RouteGuide", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), + .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + ], + targets: [] +) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift new file mode 100644 index 000000000..c9f71095f --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "RouteGuide", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), + .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + ], + targets: [ + .executableTarget( + name: "RouteGuide", + dependencies: [ + .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), + .product(name: "_GRPCProtobuf", package: "grpc-swift"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + ] + ) + ] +) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step01-import.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step01-import.proto new file mode 100644 index 000000000..676449318 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step01-import.proto @@ -0,0 +1,3 @@ +syntax = "proto3"; + +package routeguide; diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step02-service.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step02-service.proto new file mode 100644 index 000000000..cd37f3372 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step02-service.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package routeguide; + +service RouteGuide { + // ... +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step03-unary.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step03-unary.proto new file mode 100644 index 000000000..a34c123c5 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step03-unary.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package routeguide; + +service RouteGuide { + // Obtains the feature at a given position. + rpc GetFeature(Point) returns (Feature) {} +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step04-server-streaming.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step04-server-streaming.proto new file mode 100644 index 000000000..ce0a5a66d --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step04-server-streaming.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package routeguide; + +service RouteGuide { + // Obtains the feature at a given position. + rpc GetFeature(Point) returns (Feature) {} + + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step05-client-streaming.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step05-client-streaming.proto new file mode 100644 index 000000000..1e8d494e3 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step05-client-streaming.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package routeguide; + +service RouteGuide { + // Obtains the feature at a given position. + rpc GetFeature(Point) returns (Feature) {} + + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} + + // Accepts a stream of Points on a route being traversed, returning a + // RouteSummary when traversal is completed. + rpc RecordRoute(stream Point) returns (RouteSummary) {} +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step06-bidi-streaming.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step06-bidi-streaming.proto new file mode 100644 index 000000000..01a0b3932 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step06-bidi-streaming.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package routeguide; + +service RouteGuide { + // Obtains the feature at a given position. + rpc GetFeature(Point) returns (Feature) {} + + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} + + // Accepts a stream of Points on a route being traversed, returning a + // RouteSummary when traversal is completed. + rpc RecordRoute(stream Point) returns (RouteSummary) {} + + // Accepts a stream of RouteNotes sent while a route is being traversed, + // while receiving other RouteNotes (e.g. from other users). + rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step07-messages.proto b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step07-messages.proto new file mode 100644 index 000000000..435d7878f --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec02-step07-messages.proto @@ -0,0 +1,80 @@ +syntax = "proto3"; + +package routeguide; + +service RouteGuide { + // Obtains the feature at a given position. + rpc GetFeature(Point) returns (Feature) {} + + // Obtains the Features available within the given Rectangle. Results are + // streamed rather than returned at once (e.g. in a response message with a + // repeated field), as the rectangle may cover a large area and contain a + // huge number of features. + rpc ListFeatures(Rectangle) returns (stream Feature) {} + + // Accepts a stream of Points on a route being traversed, returning a + // RouteSummary when traversal is completed. + rpc RecordRoute(stream Point) returns (RouteSummary) {} + + // Accepts a stream of RouteNotes sent while a route is being traversed, + // while receiving other RouteNotes (e.g. from other users). + rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} +} + +// Points are represented as latitude-longitude pairs in the E7 representation +// (degrees multiplied by 10**7 and rounded to the nearest integer). +// Latitudes should be in the range +/- 90 degrees and longitude should be in +// the range +/- 180 degrees (inclusive). +message Point { + int32 latitude = 1; + int32 longitude = 2; +} + +// A latitude-longitude rectangle, represented as two diagonally opposite +// points "lo" and "hi". +message Rectangle { + // One corner of the rectangle. + Point lo = 1; + + // The other corner of the rectangle. + Point hi = 2; +} + +// A feature names something at a given point. +// +// If a feature could not be named, the name is empty. +message Feature { + // The name of the feature. + string name = 1; + + // The point where the feature is detected. + Point location = 2; +} + +// A RouteNote is a message sent while at a given point. +message RouteNote { + // The location from which the message is sent. + Point location = 1; + + // The message to be sent. + string message = 2; +} + +// A RouteSummary is received in response to a RecordRoute rpc. +// +// It contains the number of individual points received, the number of +// detected features, and the total distance covered as the cumulative sum of +// the distance between each point. +message RouteSummary { + // The number of points received. + int32 point_count = 1; + + // The number of known features passed while traversing the route. + int32 feature_count = 2; + + // The distance covered in metres. + int32 distance = 3; + + // The duration of the traversal in seconds. + int32 elapsed_time = 4; +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step01-protoc-plugins.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step01-protoc-plugins.txt new file mode 100644 index 000000000..4eabab72c --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step01-protoc-plugins.txt @@ -0,0 +1,2 @@ +$ swift build --product protoc-gen-swift +$ swift build --product protoc-gen-grpc-swift diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step02-mkdir.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step02-mkdir.txt new file mode 100644 index 000000000..fa5470b47 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step02-mkdir.txt @@ -0,0 +1 @@ +$ mkdir Sources/Generated diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step03-gen-messages.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step03-gen-messages.txt new file mode 100644 index 000000000..5060b92b4 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step03-gen-messages.txt @@ -0,0 +1,4 @@ +$ protoc --plugin=.build/debug/protoc-gen-swift \ + -I Protos \ + --swift_out=Sources/Generated \ + Protos/route_guide.proto diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt new file mode 100644 index 000000000..1c084165e --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt @@ -0,0 +1,6 @@ +$ protoc --plugin=.build/debug/protoc-gen-grpc-swift \ + -I Protos \ + --grpc-swift_out=Sources/Generated \ + --grpc-swift_opt=_V2=true \ + --grpc-swift_opt=UseAccessLevelOnImports=false \ + Protos/route_guide.proto diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift new file mode 100644 index 000000000..65aa33cb2 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step01-struct.swift @@ -0,0 +1,4 @@ +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift new file mode 100644 index 000000000..de6f415cd --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift @@ -0,0 +1,23 @@ +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift new file mode 100644 index 000000000..99bb69ad7 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift @@ -0,0 +1,32 @@ +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift new file mode 100644 index 000000000..cf791d6f8 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift @@ -0,0 +1,43 @@ +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift new file mode 100644 index 000000000..452c74a85 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift @@ -0,0 +1,57 @@ +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + + if let feature { + return ServerResponse.Single(message: feature) + } else { + // No feature: return a feature with an empty name. + let unknownFeature = Routeguide_Feature.with { + $0.name = "" + $0.location = .with { + $0.latitude = request.message.latitude + $0.longitude = request.message.longitude + } + } + return ServerResponse.Single(message: unknownFeature) + } + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift new file mode 100644 index 000000000..b52288fa3 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift @@ -0,0 +1,73 @@ +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + + if let feature { + return ServerResponse.Single(message: feature) + } else { + // No feature: return a feature with an empty name. + let unknownFeature = Routeguide_Feature.with { + $0.name = "" + $0.location = .with { + $0.latitude = request.message.latitude + $0.longitude = request.message.longitude + } + } + return ServerResponse.Single(message: unknownFeature) + } + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request.message) { + try await writer.write(feature) + } + } + } + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} + +extension Routeguide_Feature { + func isContained(by rectangle: Routeguide_Rectangle) -> Bool { + return rectangle.lo.latitude <= self.location.latitude + && self.location.latitude <= rectangle.hi.latitude + && rectangle.lo.longitude <= self.location.longitude + && self.location.longitude <= rectangle.hi.longitude + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift new file mode 100644 index 000000000..780c63a14 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift @@ -0,0 +1,75 @@ +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + + if let feature { + return ServerResponse.Single(message: feature) + } else { + // No feature: return a feature with an empty name. + let unknownFeature = Routeguide_Feature.with { + $0.name = "" + $0.location = .with { + $0.latitude = request.message.latitude + $0.longitude = request.message.longitude + } + } + return ServerResponse.Single(message: unknownFeature) + } + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request.message) { + try await writer.write(feature) + } + } + + return [:] + } + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} + +extension Routeguide_Feature { + func isContained(by rectangle: Routeguide_Rectangle) -> Bool { + return rectangle.lo.latitude <= self.location.latitude + && self.location.latitude <= rectangle.hi.latitude + && rectangle.lo.longitude <= self.location.longitude + && self.location.longitude <= rectangle.hi.longitude + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift new file mode 100644 index 000000000..76f399c50 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift @@ -0,0 +1,154 @@ +import Foundation +import GRPCCore + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + + if let feature { + return ServerResponse.Single(message: feature) + } else { + // No feature: return a feature with an empty name. + let unknownFeature = Routeguide_Feature.with { + $0.name = "" + $0.location = .with { + $0.latitude = request.message.latitude + $0.longitude = request.message.longitude + } + } + return ServerResponse.Single(message: unknownFeature) + } + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request.message) { + try await writer.write(feature) + } + } + + return [:] + } + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + let startTime = ContinuousClock.now + var pointsVisited = 0 + var featuresVisited = 0 + var distanceTravelled = 0.0 + var previousPoint: Routeguide_Point? = nil + + for try await point in request.messages { + pointsVisited += 1 + + if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { + featuresVisited += 1 + } + + if let previousPoint { + distanceTravelled += greatCircleDistance(from: previousPoint, to: point) + } + + previousPoint = point + } + + let duration = startTime.duration(to: .now) + let summary = Routeguide_RouteSummary.with { + $0.pointCount = Int32(pointsVisited) + $0.featureCount = Int32(featuresVisited) + $0.elapsedTime = Int32(duration.components.seconds) + $0.distance = Int32(distanceTravelled) + } + + return ServerResponse.Single(message: summary) + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} + +extension Routeguide_Feature { + func isContained(by rectangle: Routeguide_Rectangle) -> Bool { + return rectangle.lo.latitude <= self.location.latitude + && self.location.latitude <= rectangle.hi.latitude + && rectangle.lo.longitude <= self.location.longitude + && self.location.longitude <= rectangle.hi.longitude + } +} + +private func greatCircleDistance( + from point1: Routeguide_Point, + to point2: Routeguide_Point +) -> Double { + // See https://en.wikipedia.org/wiki/Great-circle_distance + // + // Let λ1 (lambda1) and φ1 (phi1) be the longitude and latitude of point 1. + // Let λ2 (lambda2) and φ2 (phi2) be the longitude and latitude of point 2. + // + // Let Δλ = λ2 - λ1, and Δφ = φ2 - φ1. + // + // The central angle between them, σc (sigmaC) can be computed as: + // + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + // + // The unit distance (d) between point1 and point2 can then be computed as: + // + // d = 2 ⨯ atan(sqrt(σc), sqrt(1 - σc)) + + let lambda1 = radians(degreesInE7: point1.longitude) + let phi1 = radians(degreesInE7: point1.latitude) + let lambda2 = radians(degreesInE7: point2.longitude) + let phi2 = radians(degreesInE7: point2.latitude) + + // Δλ = λ2 - λ1 + let deltaLambda = lambda2 - lambda1 + // Δφ = φ2 - φ1 + let deltaPhi = phi2 - phi1 + + // sin²(Δφ/2) + let sinHalfDeltaPhiSquared = sin(deltaPhi / 2) * sin(deltaPhi / 2) + // sin²(Δλ/2) + let sinHalfDeltaLambdaSquared = sin(deltaLambda / 2) * sin(deltaLambda / 2) + + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + let sigmaC = 2 * sqrt(sinHalfDeltaPhiSquared + cos(phi1) * cos(phi2) * sinHalfDeltaLambdaSquared) + + // This is the unit distance, i.e. assumes the circle has a radius of 1. + let unitDistance = 2 * atan2(sqrt(sigmaC), sqrt(1 - sigmaC)) + + // Scale it by the radius of the Earth in meters. + let earthRadius = 6_371_000.0 + return unitDistance * earthRadius +} + +private func radians(degreesInE7 degrees: Int32) -> Double { + return (Double(degrees) / 1e7) * .pi / 180.0 +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift new file mode 100644 index 000000000..5c8ad560d --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift @@ -0,0 +1,185 @@ +import Foundation +import GRPCCore +import Synchronization + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Notes recorded by clients. + private let receivedNotes: Notes + + /// A thread-safe store for notes sent by clients. + private final class Notes: Sendable { + private let notes: Mutex<[Routeguide_RouteNote]> + + init() { + self.notes = Mutex([]) + } + + /// Records a note and returns all other notes recorded at the same location. + /// + /// - Parameter receivedNote: A note to record. + /// - Returns: Other notes recorded at the same location. + func recordNote(_ receivedNote: Routeguide_RouteNote) -> [Routeguide_RouteNote] { + return self.notes.withLock { notes in + var notesFromSameLocation: [Routeguide_RouteNote] = [] + for note in notes { + if note.location == receivedNote.location { + notesFromSameLocation.append(note) + } + } + notes.append(receivedNote) + return notesFromSameLocation + } + } + } + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + self.receivedNotes = Notes() + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + + if let feature { + return ServerResponse.Single(message: feature) + } else { + // No feature: return a feature with an empty name. + let unknownFeature = Routeguide_Feature.with { + $0.name = "" + $0.location = .with { + $0.latitude = request.message.latitude + $0.longitude = request.message.longitude + } + } + return ServerResponse.Single(message: unknownFeature) + } + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request.message) { + try await writer.write(feature) + } + } + + return [:] + } + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + let startTime = ContinuousClock.now + var pointsVisited = 0 + var featuresVisited = 0 + var distanceTravelled = 0.0 + var previousPoint: Routeguide_Point? = nil + + for try await point in request.messages { + pointsVisited += 1 + + if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { + featuresVisited += 1 + } + + if let previousPoint { + distanceTravelled += greatCircleDistance(from: previousPoint, to: point) + } + + previousPoint = point + } + + let duration = startTime.duration(to: .now) + let summary = Routeguide_RouteSummary.with { + $0.pointCount = Int32(pointsVisited) + $0.featureCount = Int32(featuresVisited) + $0.elapsedTime = Int32(duration.components.seconds) + $0.distance = Int32(distanceTravelled) + } + + return ServerResponse.Single(message: summary) + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + } +} + +extension Routeguide_Feature { + func isContained(by rectangle: Routeguide_Rectangle) -> Bool { + return rectangle.lo.latitude <= self.location.latitude + && self.location.latitude <= rectangle.hi.latitude + && rectangle.lo.longitude <= self.location.longitude + && self.location.longitude <= rectangle.hi.longitude + } +} + +private func greatCircleDistance( + from point1: Routeguide_Point, + to point2: Routeguide_Point +) -> Double { + // See https://en.wikipedia.org/wiki/Great-circle_distance + // + // Let λ1 (lambda1) and φ1 (phi1) be the longitude and latitude of point 1. + // Let λ2 (lambda2) and φ2 (phi2) be the longitude and latitude of point 2. + // + // Let Δλ = λ2 - λ1, and Δφ = φ2 - φ1. + // + // The central angle between them, σc (sigmaC) can be computed as: + // + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + // + // The unit distance (d) between point1 and point2 can then be computed as: + // + // d = 2 ⨯ atan(sqrt(σc), sqrt(1 - σc)) + + let lambda1 = radians(degreesInE7: point1.longitude) + let phi1 = radians(degreesInE7: point1.latitude) + let lambda2 = radians(degreesInE7: point2.longitude) + let phi2 = radians(degreesInE7: point2.latitude) + + // Δλ = λ2 - λ1 + let deltaLambda = lambda2 - lambda1 + // Δφ = φ2 - φ1 + let deltaPhi = phi2 - phi1 + + // sin²(Δφ/2) + let sinHalfDeltaPhiSquared = sin(deltaPhi / 2) * sin(deltaPhi / 2) + // sin²(Δλ/2) + let sinHalfDeltaLambdaSquared = sin(deltaLambda / 2) * sin(deltaLambda / 2) + + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + let sigmaC = 2 * sqrt(sinHalfDeltaPhiSquared + cos(phi1) * cos(phi2) * sinHalfDeltaLambdaSquared) + + // This is the unit distance, i.e. assumes the circle has a radius of 1. + let unitDistance = 2 * atan2(sqrt(sigmaC), sqrt(1 - sigmaC)) + + // Scale it by the radius of the Earth in meters. + let earthRadius = 6_371_000.0 + return unitDistance * earthRadius +} + +private func radians(degreesInE7 degrees: Int32) -> Double { + return (Double(degrees) / 1e7) * .pi / 180.0 +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift new file mode 100644 index 000000000..3913b0c36 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift @@ -0,0 +1,192 @@ +import Foundation +import GRPCCore +import Synchronization + +struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + /// Known features. + private let features: [Routeguide_Feature] + + /// Notes recorded by clients. + private let receivedNotes: Notes + + /// A thread-safe store for notes sent by clients. + private final class Notes: Sendable { + private let notes: Mutex<[Routeguide_RouteNote]> + + init() { + self.notes = Mutex([]) + } + + /// Records a note and returns all other notes recorded at the same location. + /// + /// - Parameter receivedNote: A note to record. + /// - Returns: Other notes recorded at the same location. + func recordNote(_ receivedNote: Routeguide_RouteNote) -> [Routeguide_RouteNote] { + return self.notes.withLock { notes in + var notesFromSameLocation: [Routeguide_RouteNote] = [] + for note in notes { + if note.location == receivedNote.location { + notesFromSameLocation.append(note) + } + } + notes.append(receivedNote) + return notesFromSameLocation + } + } + } + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + self.receivedNotes = Notes() + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } + + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + + if let feature { + return ServerResponse.Single(message: feature) + } else { + // No feature: return a feature with an empty name. + let unknownFeature = Routeguide_Feature.with { + $0.name = "" + $0.location = .with { + $0.latitude = request.message.latitude + $0.longitude = request.message.longitude + } + } + return ServerResponse.Single(message: unknownFeature) + } + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for feature in self.features { + if !feature.name.isEmpty, feature.isContained(by: request.message) { + try await writer.write(feature) + } + } + + return [:] + } + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + let startTime = ContinuousClock.now + var pointsVisited = 0 + var featuresVisited = 0 + var distanceTravelled = 0.0 + var previousPoint: Routeguide_Point? = nil + + for try await point in request.messages { + pointsVisited += 1 + + if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { + featuresVisited += 1 + } + + if let previousPoint { + distanceTravelled += greatCircleDistance(from: previousPoint, to: point) + } + + previousPoint = point + } + + let duration = startTime.duration(to: .now) + let summary = Routeguide_RouteSummary.with { + $0.pointCount = Int32(pointsVisited) + $0.featureCount = Int32(featuresVisited) + $0.elapsedTime = Int32(duration.components.seconds) + $0.distance = Int32(distanceTravelled) + } + + return ServerResponse.Single(message: summary) + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for try await note in request.messages { + let notes = self.receivedNotes.recordNote(note) + try await writer.write(contentsOf: notes) + } + return [:] + } + } +} + +extension Routeguide_Feature { + func isContained(by rectangle: Routeguide_Rectangle) -> Bool { + return rectangle.lo.latitude <= self.location.latitude + && self.location.latitude <= rectangle.hi.latitude + && rectangle.lo.longitude <= self.location.longitude + && self.location.longitude <= rectangle.hi.longitude + } +} + +private func greatCircleDistance( + from point1: Routeguide_Point, + to point2: Routeguide_Point +) -> Double { + // See https://en.wikipedia.org/wiki/Great-circle_distance + // + // Let λ1 (lambda1) and φ1 (phi1) be the longitude and latitude of point 1. + // Let λ2 (lambda2) and φ2 (phi2) be the longitude and latitude of point 2. + // + // Let Δλ = λ2 - λ1, and Δφ = φ2 - φ1. + // + // The central angle between them, σc (sigmaC) can be computed as: + // + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + // + // The unit distance (d) between point1 and point2 can then be computed as: + // + // d = 2 ⨯ atan(sqrt(σc), sqrt(1 - σc)) + + let lambda1 = radians(degreesInE7: point1.longitude) + let phi1 = radians(degreesInE7: point1.latitude) + let lambda2 = radians(degreesInE7: point2.longitude) + let phi2 = radians(degreesInE7: point2.latitude) + + // Δλ = λ2 - λ1 + let deltaLambda = lambda2 - lambda1 + // Δφ = φ2 - φ1 + let deltaPhi = phi2 - phi1 + + // sin²(Δφ/2) + let sinHalfDeltaPhiSquared = sin(deltaPhi / 2) * sin(deltaPhi / 2) + // sin²(Δλ/2) + let sinHalfDeltaLambdaSquared = sin(deltaLambda / 2) * sin(deltaLambda / 2) + + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + let sigmaC = 2 * sqrt(sinHalfDeltaPhiSquared + cos(phi1) * cos(phi2) * sinHalfDeltaLambdaSquared) + + // This is the unit distance, i.e. assumes the circle has a radius of 1. + let unitDistance = 2 * atan2(sqrt(sigmaC), sqrt(1 - sigmaC)) + + // Scale it by the radius of the Earth in meters. + let earthRadius = 6_371_000.0 + return unitDistance * earthRadius +} + +private func radians(degreesInE7 degrees: Int32) -> Double { + return (Double(degrees) / 1e7) * .pi / 180.0 +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift new file mode 100644 index 000000000..c9f71095f --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "RouteGuide", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), + .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + ], + targets: [ + .executableTarget( + name: "RouteGuide", + dependencies: [ + .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), + .product(name: "_GRPCProtobuf", package: "grpc-swift"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + ] + ) + ] +) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift new file mode 100644 index 000000000..05447cfb9 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "RouteGuide", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), + .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + ], + targets: [ + .executableTarget( + name: "RouteGuide", + dependencies: [ + .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), + .product(name: "_GRPCProtobuf", package: "grpc-swift"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + ], + resources: [ + .copy("route_guide_db.json") + ] + ) + ] +) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift new file mode 100644 index 000000000..1a49f098d --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "RouteGuide", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), + .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "RouteGuide", + dependencies: [ + .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), + .product(name: "_GRPCProtobuf", package: "grpc-swift"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + resources: [ + .copy("route_guide_db.json") + ] + ) + ] +) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step03-main.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step03-main.swift new file mode 100644 index 000000000..07db34cc8 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step03-main.swift @@ -0,0 +1,10 @@ +import ArgumentParser + +@main +struct RouteGuide: AsyncParsableCommand { + @Flag + var server: Bool = false + + func run() async throws { + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step04-load-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step04-load-features.swift new file mode 100644 index 000000000..82dc23b97 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step04-load-features.swift @@ -0,0 +1,23 @@ +import ArgumentParser +import Foundation + +@main +struct RouteGuide: AsyncParsableCommand { + @Flag + var server: Bool = false + + func run() async throws { + if self.server { + try await self.runServer() + } + } + + private static func loadFeatures() throws -> [Routeguide_Feature] { + guard let url = Bundle.module.url(forResource: "route_guide_db", withExtension: "json") else { + throw ExitCode.failure + } + + let data = try Data(contentsOf: url) + return try Routeguide_Feature.array(fromJSONUTF8Bytes: data) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step05-run-server.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step05-run-server.swift new file mode 100644 index 000000000..6acac0cf8 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step05-run-server.swift @@ -0,0 +1,4 @@ +extension RouteGuide { + func runServer() async throws { + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step06-init-service.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step06-init-service.swift new file mode 100644 index 000000000..0aa8f2b54 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step06-init-service.swift @@ -0,0 +1,6 @@ +extension RouteGuide { + func runServer() async throws { + let features = try self.loadFeatures() + let routeGuide = RouteGuideService(features: features) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift new file mode 100644 index 000000000..b3ffcaf33 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift @@ -0,0 +1,15 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runServer() async throws { + let features = try self.loadFeatures() + let routeGuide = RouteGuideService(features: features) + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults(transportSecurity: .plaintext) + ), + services: [routeGuide] + ) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift new file mode 100644 index 000000000..630b408b7 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift @@ -0,0 +1,25 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runServer() async throws { + let features = try self.loadFeatures() + let routeGuide = RouteGuideService(features: features) + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults(transportSecurity: .plaintext) + ), + services: [routeGuide] + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.run() + } + + if let address = try await server.listeningAddress { + print("RouteGuide server listening on \(address)") + } + } + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step01-call-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step01-call-run-client.swift new file mode 100644 index 000000000..96c6111db --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step01-call-run-client.swift @@ -0,0 +1,25 @@ +import ArgumentParser +import Foundation + +@main +struct RouteGuide: AsyncParsableCommand { + @Flag + var server: Bool = false + + func run() async throws { + if self.server { + try await self.runServer() + } else { + try await self.runClient() + } + } + + private static func loadFeatures() throws -> [Routeguide_Feature] { + guard let url = Bundle.module.url(forResource: "route_guide_db", withExtension: "json") else { + throw ExitCode.failure + } + + let data = try Data(contentsOf: url) + return try Routeguide_Feature.array(fromJSONUTF8Bytes: data) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift new file mode 100644 index 000000000..eee2b6838 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift @@ -0,0 +1,6 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift new file mode 100644 index 000000000..ac97861b9 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift @@ -0,0 +1,12 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + let client = try GRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults() + ) + ) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift new file mode 100644 index 000000000..76332bada --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift @@ -0,0 +1,14 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + let client = try GRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults() + ) + ) + + async let _ = client.run() + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift new file mode 100644 index 000000000..e231432bc --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift @@ -0,0 +1,16 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + let client = try GRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults() + ) + ) + + async let _ = client.run() + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift new file mode 100644 index 000000000..5d4e5e725 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift @@ -0,0 +1,29 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + let client = try GRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults() + ) + ) + + async let _ = client.run() + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + try await self.getFeature(using: routeGuide) + } + + private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'GetFeature'") + + let point = Routeguide_Point.with { + $0.latitude = 407_838_351 + $0.longitude = -746_143_763 + } + + let feature = try await routeGuide.getFeature(point) + print("Got feature '\(feature.name)'") + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift new file mode 100644 index 000000000..d22daa343 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift @@ -0,0 +1,53 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + let client = try GRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults() + ) + ) + + async let _ = client.run() + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + try await self.getFeature(using: routeGuide) + try await self.listFeatures(using: routeGuide) + } + + private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'GetFeature'") + + let point = Routeguide_Point.with { + $0.latitude = 407_838_351 + $0.longitude = -746_143_763 + } + + let feature = try await routeGuide.getFeature(point) + print("Got feature '\(feature.name)'") + } + + private func listFeatures(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'ListFeatures'") + + let boundingRectangle = Routeguide_Rectangle.with { + $0.lo = .with { + $0.latitude = 400_000_000 + $0.longitude = -750_000_000 + } + $0.hi = .with { + $0.latitude = 420_000_000 + $0.longitude = -730_000_000 + } + } + + try await routeGuide.listFeatures(boundingRectangle) { response in + for try await feature in response.messages { + print( + "Got feature '\(feature.name)' at (\(feature.location.latitude), \(feature.location.longitude))" + ) + } + } + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift new file mode 100644 index 000000000..6b5ecaba7 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift @@ -0,0 +1,76 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + let client = try GRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults() + ) + ) + + async let _ = client.run() + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + try await self.getFeature(using: routeGuide) + try await self.listFeatures(using: routeGuide) + try await self.recordRoute(using: routeGuide) + } + + private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'GetFeature'") + + let point = Routeguide_Point.with { + $0.latitude = 407_838_351 + $0.longitude = -746_143_763 + } + + let feature = try await routeGuide.getFeature(point) + print("Got feature '\(feature.name)'") + } + + private func listFeatures(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'ListFeatures'") + + let boundingRectangle = Routeguide_Rectangle.with { + $0.lo = .with { + $0.latitude = 400_000_000 + $0.longitude = -750_000_000 + } + $0.hi = .with { + $0.latitude = 420_000_000 + $0.longitude = -730_000_000 + } + } + + try await routeGuide.listFeatures(boundingRectangle) { response in + for try await feature in response.messages { + print( + "Got feature '\(feature.name)' at (\(feature.location.latitude), \(feature.location.longitude))" + ) + } + } + } + + private func recordRoute(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'RecordRoute'") + + let features = try self.loadFeatures() + let pointsToVisit = 10 + + let summary = try await routeGuide.recordRoute { writer in + for _ in 0 ..< pointsToVisit { + if let feature = features.randomElement() { + try await writer.write(feature.location) + } + } + } + + print( + """ + Finished trip with \(summary.pointCount) points. Passed \(summary.featureCount) \ + features. Travelled \(summary.distance) meters. It took \(summary.elapsedTime) seconds. + """ + ) + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift new file mode 100644 index 000000000..a0fa49c55 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift @@ -0,0 +1,107 @@ +import GRPCHTTP2Transport + +extension RouteGuide { + func runClient() async throws { + let client = try GRPCClient( + transport: .http2NIOPosix( + target: .ipv4(host: "127.0.0.1", port: 31415), + config: .defaults() + ) + ) + + async let _ = client.run() + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + try await self.getFeature(using: routeGuide) + try await self.listFeatures(using: routeGuide) + try await self.recordRoute(using: routeGuide) + try await self.routeChat(using: routeGuide) + } + + private func getFeature(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'GetFeature'") + + let point = Routeguide_Point.with { + $0.latitude = 407_838_351 + $0.longitude = -746_143_763 + } + + let feature = try await routeGuide.getFeature(point) + print("Got feature '\(feature.name)'") + } + + private func listFeatures(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'ListFeatures'") + + let boundingRectangle = Routeguide_Rectangle.with { + $0.lo = .with { + $0.latitude = 400_000_000 + $0.longitude = -750_000_000 + } + $0.hi = .with { + $0.latitude = 420_000_000 + $0.longitude = -730_000_000 + } + } + + try await routeGuide.listFeatures(boundingRectangle) { response in + for try await feature in response.messages { + print( + "Got feature '\(feature.name)' at (\(feature.location.latitude), \(feature.location.longitude))" + ) + } + } + } + + private func recordRoute(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'RecordRoute'") + + let features = try self.loadFeatures() + let pointsToVisit = 10 + + let summary = try await routeGuide.recordRoute { writer in + for _ in 0 ..< pointsToVisit { + if let feature = features.randomElement() { + try await writer.write(feature.location) + } + } + } + + print( + """ + Finished trip with \(summary.pointCount) points. Passed \(summary.featureCount) \ + features. Travelled \(summary.distance) meters. It took \(summary.elapsedTime) seconds. + """ + ) + } + + private func routeChat(using routeGuide: Routeguide_RouteGuideClient) async throws { + print("→ Calling 'RouteChat'") + + try await routeGuide.routeChat { writer in + let notes: [(String, (Int32, Int32))] = [ + ("First message", (0, 0)), + ("Second message", (0, 1)), + ("Third message", (1, 0)), + ("Fourth message", (1, 1)), + ("Fifth message", (0, 0)), + ] + + for (message, (lat, lon)) in notes { + let note = Routeguide_RouteNote.with { + $0.message = message + $0.location.latitude = lat + $0.location.longitude = lon + } + print("Sending message '\(message)' at (\(lat), \(lon))") + try await writer.write(note) + } + } onResponse: { response in + for try await note in response.messages { + print( + "Got message '\(note.message)' at (\(note.location.latitude), \(note.location.longitude))" + ) + } + } + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step01-server.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step01-server.txt new file mode 100644 index 000000000..4056a8e5c --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step01-server.txt @@ -0,0 +1 @@ +RouteGuide server listening on [ipv4]127.0.0.1:31415 diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step02-client.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step02-client.txt new file mode 100644 index 000000000..def58639d --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec07-step02-client.txt @@ -0,0 +1,76 @@ +→ Calling 'GetFeature' +Got feature 'Patriots Path, Mendham, NJ 07945, USA' +→ Calling 'ListFeatures' +Got feature 'Patriots Path, Mendham, NJ 07945, USA' at (407838351, -746143763) +Got feature '101 New Jersey 10, Whippany, NJ 07981, USA' at (408122808, -743999179) +Got feature 'U.S. 6, Shohola, PA 18458, USA' at (413628156, -749015468) +Got feature '5 Conners Road, Kingston, NY 12401, USA' at (419999544, -740371136) +Got feature 'Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA' at (414008389, -743951297) +Got feature '287 Flugertown Road, Livingston Manor, NY 12758, USA' at (419611318, -746524769) +Got feature '4001 Tremley Point Road, Linden, NJ 07036, USA' at (406109563, -742186778) +Got feature '352 South Mountain Road, Wallkill, NY 12589, USA' at (416802456, -742370183) +Got feature 'Bailey Turn Road, Harriman, NY 10926, USA' at (412950425, -741077389) +Got feature '193-199 Wawayanda Road, Hewitt, NJ 07421, USA' at (412144655, -743949739) +Got feature '406-496 Ward Avenue, Pine Bush, NY 12566, USA' at (415736605, -742847522) +Got feature '162 Merrill Road, Highland Mills, NY 10930, USA' at (413843930, -740501726) +Got feature 'Clinton Road, West Milford, NJ 07480, USA' at (410873075, -744459023) +Got feature '16 Old Brook Lane, Warwick, NY 10990, USA' at (412346009, -744026814) +Got feature '3 Drake Lane, Pennington, NJ 08534, USA' at (402948455, -747903913) +Got feature '6324 8th Avenue, Brooklyn, NY 11220, USA' at (406337092, -740122226) +Got feature '1 Merck Access Road, Whitehouse Station, NJ 08889, USA' at (406421967, -747727624) +Got feature '78-98 Schalck Road, Narrowsburg, NY 12764, USA' at (416318082, -749677716) +Got feature '282 Lakeview Drive Road, Highland Lake, NY 12743, USA' at (415301720, -748416257) +Got feature '330 Evelyn Avenue, Hamilton Township, NJ 08619, USA' at (402647019, -747071791) +Got feature 'New York State Reference Route 987E, Southfields, NY 10975, USA' at (412567807, -741058078) +Got feature '103-271 Tempaloni Road, Ellenville, NY 12428, USA' at (416855156, -744420597) +Got feature '1300 Airport Road, North Brunswick Township, NJ 08902, USA' at (404663628, -744820157) +Got feature '211-225 Plains Road, Augusta, NJ 07822, USA' at (411633782, -746784970) +Got feature '165 Pedersen Ridge Road, Milford, PA 18337, USA' at (413447164, -748712898) +Got feature '100-122 Locktown Road, Frenchtown, NJ 08825, USA' at (405047245, -749800722) +Got feature '650-652 Willi Hill Road, Swan Lake, NY 12783, USA' at (417951888, -748484944) +Got feature '26 East 3rd Street, New Providence, NJ 07974, USA' at (407033786, -743977337) +Got feature '611 Lawrence Avenue, Westfield, NJ 07090, USA' at (406589790, -743560121) +Got feature '18 Lannis Avenue, New Windsor, NY 12553, USA' at (414653148, -740477477) +Got feature '82-104 Amherst Avenue, Colonia, NJ 07067, USA' at (405957808, -743255336) +Got feature '170 Seven Lakes Drive, Sloatsburg, NY 10974, USA' at (411733589, -741648093) +Got feature '1270 Lakes Road, Monroe, NY 10950, USA' at (412676291, -742606606) +Got feature '509-535 Alphano Road, Great Meadows, NJ 07838, USA' at (409224445, -748286738) +Got feature '652 Garden Street, Elizabeth, NJ 07202, USA' at (406523420, -742135517) +Got feature '349 Sea Spray Court, Neptune City, NJ 07753, USA' at (401827388, -740294537) +Got feature '13-17 Stanley Street, West Milford, NJ 07480, USA' at (410564152, -743685054) +Got feature '47 Industrial Avenue, Teterboro, NJ 07608, USA' at (408472324, -740726046) +Got feature '5 White Oak Lane, Stony Point, NY 10980, USA' at (412452168, -740214052) +Got feature 'Berkshire Valley Management Area Trail, Jefferson, NJ, USA' at (409146138, -746188906) +Got feature '1007 Jersey Avenue, New Brunswick, NJ 08901, USA' at (404701380, -744781745) +Got feature '6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA' at (409642566, -746017679) +Got feature '1358-1474 New Jersey 57, Port Murray, NJ 07865, USA' at (408031728, -748645385) +Got feature '367 Prospect Road, Chester, NY 10918, USA' at (413700272, -742135189) +Got feature '10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA' at (404310607, -740282632) +Got feature '11 Ward Street, Mount Arlington, NJ 07856, USA' at (409319800, -746201391) +Got feature '300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA' at (406685311, -742108603) +Got feature '43 Dreher Road, Roscoe, NY 12776, USA' at (419018117, -749142781) +Got feature 'Swan Street, Pine Island, NY 10969, USA' at (412856162, -745148837) +Got feature '66 Pleasantview Avenue, Monticello, NY 12701, USA' at (416560744, -746721964) +Got feature '565 Winding Hills Road, Montgomery, NY 12549, USA' at (415534177, -742900616) +Got feature '231 Rocky Run Road, Glen Gardner, NJ 08826, USA' at (406898530, -749127080) +Got feature '100 Mount Pleasant Avenue, Newark, NJ 07104, USA' at (407586880, -741670168) +Got feature '517-521 Huntington Drive, Manchester Township, NJ 08759, USA' at (400106455, -742870190) +Got feature '40 Mountain Road, Napanoch, NY 12458, USA' at (418803880, -744102673) +Got feature '48 North Road, Forestburgh, NY 12777, USA' at (415464475, -747175374) +Got feature '9 Thompson Avenue, Leonardo, NJ 07737, USA' at (404226644, -740517141) +Got feature '213 Bush Road, Stone Ridge, NY 12484, USA' at (418811433, -741718005) +Got feature '1-17 Bergen Court, New Brunswick, NJ 08901, USA' at (404839914, -744759616) +Got feature '35 Oakland Valley Road, Cuddebackville, NY 12729, USA' at (414638017, -745957854) +Got feature '42-102 Main Street, Belford, NJ 07718, USA' at (404318328, -740835638) +Got feature '3387 Richmond Terrace, Staten Island, NY 10303, USA' at (406411633, -741722051) +Got feature '261 Van Sickle Road, Goshen, NY 10924, USA' at (413069058, -744597778) +Got feature '3 Hasta Way, Newton, NJ 07860, USA' at (410248224, -747127767) +→ Calling 'RecordRoute' +Finished trip with 10 points. Passed "10 features. Travelled 10314218 meters. It took 0 seconds. +→ Calling 'RouteChat' +Sending message 'First message' at (0, 0) +Sending message 'Second message' at (0, 1) +Sending message 'Third message' at (1, 0) +Sending message 'Fourth message' at (1, 1) +Sending message 'Fifth message' at (0, 0) +Got message 'First message' at (0, 0) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial new file mode 100644 index 000000000..52b404049 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -0,0 +1,500 @@ +@Tutorial(time: 30) { + @XcodeRequirement( + title: "Xcode 16 Beta 6+", + destination: "https://developer.apple.com/download/" + ) + + @Intro(title: "The Basics: Route Guide") { + Follow this tutorial to learn how to create a gRPC service and client from scratch. You'll + learn how to define a service in a `.proto` file, generate server and client code using the + Protocol Buffer compiler, and use gRPC Swift to write a simple client and server for your + service. + + This tutorial assumes that you have read the [Overview](https://grpc.io/docs) and are familiar + with [Protocol Buffers](https://developers.google.com/protocol-buffers/docs/proto3). + + If you're looking for a simpler starting point, try the tutorial. + } + + @Section(title: "Setup") { + Before we can write any code we need to create a new Swift Package and configure it + to depend on gRPC Swift. + + @Steps { + @Step { + Create a new directory called for the package called `RouteGuide`. + + @Code(name: "Console.txt", file: "route-guide-sec01-step01-mkdir.txt") + } + + @Step { + All subsequent commands used in this tutorial assume that you're working from the directory + you just created so change to it now. + + @Code(name: "Console.txt", file: "route-guide-sec01-step02-cd.txt") + } + + @Step { + We need to create a few more directories, one for our source code and another for the + Protocol Buffers files. + + @Code(name: "Console.txt", file: "route-guide-sec01-step03-mkdir.txt") + } + + @Step { + Now we'll create a manifest for our Swift Package. Create a new empty file + called `Package.swift`. + + The first line in the file is a directive indicating what tools version is required to build + the package. For gRPC Swift v2, the tools version must be 6.0 or newer. + + @Code(name: "Package.swift", file: "route-guide-sec01-step04-tools-version.swift") + } + + @Step { + Import the `PackageDescription` module so we can describe our package. + + @Code(name: "Package.swift", file: "route-guide-sec01-step05-import.swift") + } + + @Step { + Create an empty package object called `RouteGuide`. + + @Code(name: "Package.swift", file: "route-guide-sec01-step06-description.swift") + } + + @Step { + We need to add a dependency on the gRPC Swift and Swift Protobuf packages. As gRPC Swift v2 + hasn't yet been released the dependency must be on the `main` branch. + + Note that we also add `.macOS(.v15)` to platforms, this is the earliest macOS version supported by + gRPC Swift v2. + + @Code(name: "Package.swift", file: "route-guide-sec01-step07-description.swift") + } + + @Step { + Next we can add a target. In this tutorial we'll create a single executable target which + can act as both a client and a server. + + We require two gRPC dependencies: `_GRPCHTTP2Transport"` provides an implementation of an HTTP/2 + client and server, while `_GRPCProtobuf` provides the necessary components to serialize + and deserialize `SwiftProtobuf` messages. + + > Because gRPC Swift v2 hasn't been released yet the product names are prefixed + > with an `"_"`; this indicates that they aren't yet considered as stable public API. + + @Code(name: "Package.swift", file: "route-guide-sec01-step08-description.swift") + } + } + } + + @Section(title: "Defining the service") { + Our next step is to define our gRPC *service* and its *request* and *response* types using + Protocol Buffers. + + @Steps { + @Step { + Create a new empty file in the `Protos` directory called `route_guide.proto`. We'll use + the "proto3" syntax and our service will be part of the "routeguide" package. + + @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step01-import.proto") + } + + @Step { + To define a service we create a named `service` in the `.proto` file. + + @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step02-service.proto") + } + + @Step { + Then we define `rpc` methods inside our service definition, specifying their + request and response types. gRPC lets you define four kinds of service methods, + all of which are used in the `RouteGuide` service. + + A *unary RPC* where the client sends a request to the server using the stub + and waits for a response to come back, just like a normal function call. + + @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step03-unary.proto") + } + + @Step { + A *server-side streaming RPC* where the client sends a request to the server + and gets a stream to read a sequence of messages back. The client reads from + the returned stream until there are no more messages. As you can see in our + example, you specify a server-side streaming method by placing the `stream` + keyword before the *response* type. + + @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step04-server-streaming.proto") + } + + @Step { + A *client-side streaming RPC* where the client writes a sequence of messages + and sends them to the server, again using a provided stream. Once the client + has finished writing the messages, it waits for the server to read them all + and return its response. You specify a client-side streaming method by placing + the `stream` keyword before the *request* type. + + @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step05-client-streaming.proto") + } + + @Step { + A *bidirectional streaming RPC* where both sides send a sequence of messages + using a read-write stream. The two streams operate independently, so clients + and servers can read and write in whatever order they like: for example, the + server could wait to receive all the client messages before writing its + responses, or it could alternately read a message then write a message, or + some other combination of reads and writes. The order of messages in each + stream is preserved. You specify this type of method by placing the `stream` + keyword before both the request and the response. + + @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step06-bidi-streaming.proto") + } + + @Step { + The `.proto` file also contains the Protocol Buffers message type definitions for all + request and response messages used by the service. + + @Code(name: "Protos/route_guide.proto", file: "route-guide-sec02-step07-messages.proto") + } + } + } + + @Section(title: "Generating client and server code") { + Next we need to generate the gRPC client and server interfaces from our `.proto` + service definition. We do this using the Protocol Buffer compiler, `protoc`, with + two plugins: one with support for Swift (via [Swift Protobuf](https://github.com/apple/swift-protobuf)) + and the other for gRPC. This section assumes you already have `protoc` installed. + + To learn more about generating code check out the article. + + @Steps { + @Step { + First we need to build the two plugins for `protoc`, `protoc-gen-swift` and + `protoc-gen-grpc-swift`. + + @Code(name: "Console", file: "route-guide-sec03-step01-protoc-plugins.txt") + } + + @Step { + We'll generate the code into a separate directory within `Sources` called `Generated` which + we need to create first. + + @Code(name: "Console", file: "route-guide-sec03-step02-mkdir.txt") + } + + @Step { + Now run `protoc` to generate the messages. This will create + `Sources/Generated/route_guide.pb.swift`. + + @Code(name: "Console", file: "route-guide-sec03-step03-gen-messages.txt") + } + + @Step { + Run `protoc` again to generate the service code. This will create + `Sources/Generated/route_guide.grpc.swift`. + + > `protoc-gen-grpc-swift` is currently shared with v1 so the `_V2=true` option is required. + > This will be removed when v2 is released. + + @Code(name: "Console", file: "route-guide-sec03-step04-gen-grpc.txt") + } + } + } + + @Section(title: "Creating the service") { + Now that we've generated the service stubs and messages we can create a server. If you're only + interested in creating gRPC clients you can skip this section (although you might find it + interesting anyway!) + + There are two parts to making our service do its job: + 1. Implementing the service protocol generated from our service definition, doing the + actual "work" of our service. + 2. Running a gRPC server to listen for requests from clients and return the service responses. + + This section explains how to implement the service protocol. + + @Steps { + @Step { + Create a new empty file in `Sources` called `RouteGuideService.swift`. To implement + the service we need to conform a type to the generated service protocol. The name + of the protocol will be `_.ServiceProtocol` where `` and + `` are both taken from the `.proto` file. + + Create a `struct` called `RouteGuideService` which conforms to + the `Routeguide_RouteGuide.ServiceProtocol`. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step01-struct.swift") + } + + @Step { + The compiler will produce an error because the requirements of the protocol haven't yet been + met. It should also suggest a fix-it to generate the methods for you. Apply it so that you + get empty function definitions. They should look like this: + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step02-unimplemented.swift") + } + + @Step { + To implement the unary `getFeature` method the service needs a way to get features. We'll + just store the features in an array which is passed to the service when it's initialized. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step03-features.swift") + } + + @Step { + `GetFeature` is a unary RPC which takes a single point as input and returns a single + feature back to the client. Its generated method, `getFeature`, has one parameter: + `ServerRequest.Single` describing the request. To return our response to + the client and complete the call we must first lookup a feature at the given point. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step04-unary.swift") + } + + @Step { + Then create and return an appropriate `ServerResponse.Single` to the + client. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step05-unary.swift") + } + + @Step { + Next, let's look at one of our streaming RPCs. Like the unary RPC, this method gets a + request object, `ServerRequest.Single`, which has a message describing + the area in which the client wants to list features. As this is a server-side streaming RPC + we can send back multiple `Routeguide_Feature` messages to our client. + + To implement the method we must return a `ServerResponse.Stream` which is initialized with + a closure to produce messages. The closure is passed a writer allowing you to write back + messages. We can write back a message for each feature we find in the rectangle. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step06-server-streaming.swift") + } + + @Step { + You can also send metadata to the client once the RPC has finished, in this case we don't + have any to send back so return the empty `Metadata` collection. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step07-server-streaming.swift") + } + + @Step { + Now let's look at something a little more complicated: the client-side streaming + method `RecordRoute`, where we get a stream of `Routeguide_Point`s from the client and + return a single `Routeguide_RouteSummary` with information about their trip. + + As you can see our method gets a `ServerRequest.Stream` parameter and + returns a `ServerResponse.Single`. In the method we iterate over + the asynchronous stream of points sent by the client. For each point we check if there's + a feature at that point and calculate the distance between that and the last point we saw. + After the *client* has finished sending points we populate a `Routeguide_RouteSummary` which + we return in the response. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step08-client-streaming.swift") + } + + @Step { + Finally, let's look at our bidirectional streaming RPC `RouteChat`. Here we receive a stream + of `Routeguide_RouteNote`s and return a stream of `Routeguide_RouteNote`s. For this RPC we + also need to modify the state of our service to store notes sent by the client. We'll do + this with a helper class to store the notes. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step09-bidi-streaming.swift") + } + + @Step { + To implement the RPC we return a `ServerResponse.Stream`. Like in the + server-side streaming RPC it's initialized with a closure for writing back messages. In + the body of the closure we iterate the request messages and for each one call our helper + class to record the note and get all other notes recorded in the same location. We then + write each of those notes back to the client. + + @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step10-bidi-streaming.swift") + } + } + } + + @Section(title: "Creating the server") { + Now that we've created the service we can create a server. + + @Steps { + @Step { + First we need to download a database of known features for the service. Copy + `route_guide_db.json` from the [gRPC Swift repository](https://github.com/grpc/grpc-swift/tree/main/Examples/v2/route-guide) + and place it in the `Sources` directory. + } + + @Step { + Next, we need to add it as a resource to the target. Open `Package.swift`. + + @Code(name: "Package.swift", file: "route-guide-sec05-step00-package.swift") + } + + @Step { + Now add a `resouces` argument to copy the `route_guide_db.json` database. + + @Code(name: "Package.swift", file: "route-guide-sec05-step01-package.swift") + } + + @Step { + We'll also need to add a dependency on Swift Argument Parser to the package and target. + + @Code(name: "Package.swift", file: "route-guide-sec05-step02-package.swift") + } + + @Step { + With that done we can now create a new file in `Sources` called `RouteGuide.swift` with the + following code in. + + `@main` indicates that the type contains the entry point to the program. In this case, + because `RouteGuide` conforms to `AsyncParseableCommand`, the entry point to our program + is the `run()` method. The `@Flag` annotation marks `server` as a flag to the argument + parser so that you can specify `--server` when running the program. + + @Code(name: "Sources/RouteGuide.swift", file: "route-guide-sec05-step03-main.swift") + } + + @Step { + We'll also add a helper to load the features from the resource we added in the package + manifest. Let's call `runServer` if the `server` flag is set. We'll define + the `runServer` method next. + + @Code(name: "Sources/RouteGuide.swift", file: "route-guide-sec05-step04-load-features.swift") + } + + @Step { + Create a new file in `Sources` called `RouteGuide+Server.swift`, we'll use this for all of + the server specific code. + + @Code(name: "Sources/RouteGuide+Server.swift", file: "route-guide-sec05-step05-run-server.swift") + } + + @Step { + Next, let's load the features and instantiate our service. + + @Code(name: "Sources/RouteGuide+Server.swift", file: "route-guide-sec05-step06-init-service.swift") + } + + @Step { + The server is instantiated with a transport which provides the communication with a remote + peer. + + We'll use an HTTP/2 transport provided by the `GRPCHTTP2Transport` module which is + implemented on top of SwiftNIO's Posix sockets abstraction. The server is configured to + bind to "127.0.0.1:31415" and use the default configuration. The `.plaintext` transport + security means that the connections won't use TLS and will be unencrypted. + + @Code(name: "Sources/RouteGuide+Server.swift", file: "route-guide-sec05-step07-server.swift") + } + + @Step { + Finally, we start the server and then print out its listening address when it's started. + + @Code(name: "Sources/RouteGuide+Server.swift", file: "route-guide-sec05-step08-run.swift") + } + } + } + + @Section(title: "Creating the client") { + In this section, we'll look at creating a Swift client for our `RouteGuide` service. + + To call service methods, we first need to create a *stub*. All generated Swift stubs + are *asynchronous*. + + @Steps { + @Step { + Like for the server we'll add a method to run the client RPCs. Open `RouteGuide.swift` + again and add a call to `runClient`. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step01-call-run-client.swift") + } + + @Step { + Create a new file in `Sources` called `RouteGuide+Client.swift` and define the + `runClient` function. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step02-add-run-client.swift") + } + + @Step { + First we need to create a gRPC client for our stub, we're not using TLS so we + use the `.plaintext` security transport and specify the server address and port + we want to connect to. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step03-create-client.swift") + } + + @Step { + To make RPCs using the client it needs to be running. Running the client will resolve the + target address, start a load balancer and then maintain connections to the server. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step04-run-client.swift") + } + + @Step { + We can now instantiate the stub with our `client` and call service methods. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step05-stub.swift") + } + + @Step { + Calling the unary RPC `GetFeature` is straightforward, we create a + `Routeguide_Point` and pass it to the `getFeature` method and it returns + a `Routeguide_Feature`. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step06-get-feature.swift") + } + + @Step { + Next, let's look at a server-side streaming call to `ListFeatures` which returns a stream of + features within a bounding rectangle. It's very similar to the unary RPC we just looked at + except that we must pass a response handling closure to `listFeatures`. Inside the closure + we can iterate the stream of features returned from the server. + + The response handling closure makes the lifetime of the RPC clear: only once the closure + returns does the `listFeature` return, at which point any resources used by gRPC to execute + the RPC will have been cleaned up. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step07-list-features.swift") + } + + @Step { + Now for something a little more complicated: the client-side streaming method `RecordRoute`, + where we send a stream of `Routeguide_Point`s to the server and get back a + single `Routeguide_RouteSummary`. The `recordRoute` method takes a closure which is passed + a writer which we can use to send messages to the server. Once the closure returns the + server knows that the client is done sending messages and can return a summary to the + client. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step08-record-route.swift") + } + + @Step { + Finally, let's look at our bidirectional stream `RouteChat` RPC. As with our client-side + streaming example, we have a closure for writing notes, and like the server-side streaming + example another closure to handle the response. + + @Code(name: "Sources/RouteGuide+Client.swift", file: "route-guide-sec06-step09-route-chat.swift") + } + } + } + + @Section(title: "Try it out!") { + Now that you've implemented the service and created a client to call the various RPCs + we can try running it. + + @Steps { + @Step { + In one terminal run `swift run RouteGuide --server` to start the server. + + @Code(name: "Console", file: "route-guide-sec07-step01-server.txt") + } + + @Step { + In another terminal run `swift run RouteGuide` to run the client program. + + @Code(name: "Console", file: "route-guide-sec07-step02-client.txt") + } + } + } +} diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial index fecc3c270..076e13c14 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Table-of-Contents.tutorial @@ -10,4 +10,12 @@ @TutorialReference(tutorial: "doc:Hello-World") } + + @Chapter(name: "The Basics") { + Follow the basic tutorial to learn how to create a gRPC service and client from scratch. + + @Image(source: "image.png") + + @TutorialReference(tutorial: "doc:Route-Guide") + } } From 962608aafa25000371b1129f1e4bf1d6204dc924 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 27 Aug 2024 16:35:08 +0100 Subject: [PATCH 443/580] Regenerate protos after swift-protobuf update to 1.28.0 (#2035) `swift-protobuf` [released 1.28.0](https://github.com/apple/swift-protobuf/releases/tag/1.28.0) yesterday, and it changed the headers in the generated files to disable `swiftlint` (see https://github.com/apple/swift-protobuf/pull/1627). This means that our CI is now failing the sanity step when it checks the generated files match the expected output of the `generate.sh` script. This PR regenerates the protos with swift-protobuf 1.28.0 to include the updated headers. --- Examples/v1/Echo/Model/echo.pb.swift | 1 + Examples/v1/HelloWorld/Model/helloworld.pb.swift | 1 + Examples/v1/RouteGuide/Model/route_guide.pb.swift | 1 + Examples/v2/echo/Generated/echo.pb.swift | 1 + Examples/v2/hello-world/Generated/helloworld.pb.swift | 1 + Sources/GRPCReflectionService/v1/reflection-v1.pb.swift | 1 + .../GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift | 1 + Sources/InteroperabilityTests/Generated/empty.pb.swift | 1 + Sources/InteroperabilityTests/Generated/empty_service.pb.swift | 1 + Sources/InteroperabilityTests/Generated/messages.pb.swift | 1 + Sources/InteroperabilityTests/Generated/test.pb.swift | 1 + Sources/Services/Health/Generated/health.pb.swift | 1 + Sources/performance-worker/Generated/grpc_core_stats.pb.swift | 1 + .../Generated/grpc_testing_benchmark_service.pb.swift | 1 + .../performance-worker/Generated/grpc_testing_control.pb.swift | 1 + .../performance-worker/Generated/grpc_testing_messages.pb.swift | 1 + .../performance-worker/Generated/grpc_testing_payloads.pb.swift | 1 + Sources/performance-worker/Generated/grpc_testing_stats.pb.swift | 1 + .../Generated/grpc_testing_worker_service.pb.swift | 1 + Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift | 1 + Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift | 1 + Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift | 1 + .../Configuration/Generated/service_config.pb.swift | 1 + Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift | 1 + Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift | 1 + .../Generated/v1/reflection-v1.pb.swift | 1 + .../Generated/v1Alpha/reflection-v1alpha.pb.swift | 1 + 27 files changed, 27 insertions(+) diff --git a/Examples/v1/Echo/Model/echo.pb.swift b/Examples/v1/Echo/Model/echo.pb.swift index bff6995e1..7ddf8cca9 100644 --- a/Examples/v1/Echo/Model/echo.pb.swift +++ b/Examples/v1/Echo/Model/echo.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: echo.proto diff --git a/Examples/v1/HelloWorld/Model/helloworld.pb.swift b/Examples/v1/HelloWorld/Model/helloworld.pb.swift index a1ded002d..d0425b18f 100644 --- a/Examples/v1/HelloWorld/Model/helloworld.pb.swift +++ b/Examples/v1/HelloWorld/Model/helloworld.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: helloworld.proto diff --git a/Examples/v1/RouteGuide/Model/route_guide.pb.swift b/Examples/v1/RouteGuide/Model/route_guide.pb.swift index 0fe32db43..80c5ff7b6 100644 --- a/Examples/v1/RouteGuide/Model/route_guide.pb.swift +++ b/Examples/v1/RouteGuide/Model/route_guide.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: route_guide.proto diff --git a/Examples/v2/echo/Generated/echo.pb.swift b/Examples/v2/echo/Generated/echo.pb.swift index 47249e093..178a32b5e 100644 --- a/Examples/v2/echo/Generated/echo.pb.swift +++ b/Examples/v2/echo/Generated/echo.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: echo.proto diff --git a/Examples/v2/hello-world/Generated/helloworld.pb.swift b/Examples/v2/hello-world/Generated/helloworld.pb.swift index cceb2c743..f3dba4167 100644 --- a/Examples/v2/hello-world/Generated/helloworld.pb.swift +++ b/Examples/v2/hello-world/Generated/helloworld.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: helloworld.proto diff --git a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift index ca5442979..0d15778aa 100644 --- a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift +++ b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: reflection.proto diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift index f4569ce38..5560e7d6e 100644 --- a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift +++ b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: reflection.proto diff --git a/Sources/InteroperabilityTests/Generated/empty.pb.swift b/Sources/InteroperabilityTests/Generated/empty.pb.swift index e6965f03e..b946bc9bc 100644 --- a/Sources/InteroperabilityTests/Generated/empty.pb.swift +++ b/Sources/InteroperabilityTests/Generated/empty.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: src/proto/grpc/testing/empty.proto diff --git a/Sources/InteroperabilityTests/Generated/empty_service.pb.swift b/Sources/InteroperabilityTests/Generated/empty_service.pb.swift index c6a096809..ac2f51edf 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.pb.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: src/proto/grpc/testing/empty_service.proto diff --git a/Sources/InteroperabilityTests/Generated/messages.pb.swift b/Sources/InteroperabilityTests/Generated/messages.pb.swift index 34ad924a9..fa2318afd 100644 --- a/Sources/InteroperabilityTests/Generated/messages.pb.swift +++ b/Sources/InteroperabilityTests/Generated/messages.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: src/proto/grpc/testing/messages.proto diff --git a/Sources/InteroperabilityTests/Generated/test.pb.swift b/Sources/InteroperabilityTests/Generated/test.pb.swift index 8fb71aea8..b2c02d793 100644 --- a/Sources/InteroperabilityTests/Generated/test.pb.swift +++ b/Sources/InteroperabilityTests/Generated/test.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: src/proto/grpc/testing/test.proto diff --git a/Sources/Services/Health/Generated/health.pb.swift b/Sources/Services/Health/Generated/health.pb.swift index f95e659f8..c1e0ff837 100644 --- a/Sources/Services/Health/Generated/health.pb.swift +++ b/Sources/Services/Health/Generated/health.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: health.proto diff --git a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift index 8ae7f8aeb..5daa789c0 100644 --- a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift +++ b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/core/stats.proto diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift index 745e5b757..19b09bc03 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/testing/benchmark_service.proto diff --git a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift index 969f5b561..390894079 100644 --- a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/testing/control.proto diff --git a/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift b/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift index 5d0680338..0665c8f0c 100644 --- a/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/testing/messages.proto diff --git a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift index 7f5023fbe..88102d044 100644 --- a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/testing/payloads.proto diff --git a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift index 79f3c2664..44234bddf 100644 --- a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/testing/stats.proto diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift index 6cacd66fa..7328357d3 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/testing/worker_service.proto diff --git a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift index 4810fb7f7..f451901d4 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: google/rpc/code.proto diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift index cb3f67f7e..53cd64eac 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/lookup/v1/rls.proto diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift index 1be022821..602b2d7ea 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/lookup/v1/rls_config.proto diff --git a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift index 532eeeed1..108e1994a 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: grpc/service_config/service_config.proto diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift index a9eff91b7..435bd944c 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: control.proto diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift index 784909215..c791f6aa9 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: normalization.proto diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift index e5b316a72..1c7a59122 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: reflection.proto diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift index fdf2554d5..bcb7d31a2 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift @@ -1,5 +1,6 @@ // DO NOT EDIT. // swift-format-ignore-file +// swiftlint:disable all // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: reflection.proto From 3713967b74a76c97fbb25b219c6bc68c75725c28 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Sep 2024 13:08:32 +0100 Subject: [PATCH 444/580] Regenerate protos (#2039) swift-protobuf 1.28.1 includes generated code changes, our. CI fails without regenerating. --- Examples/v1/Echo/Model/echo.pb.swift | 1 - Examples/v1/HelloWorld/Model/helloworld.pb.swift | 1 - Examples/v1/RouteGuide/Model/route_guide.pb.swift | 1 - Examples/v2/echo/Generated/echo.pb.swift | 1 - .../v2/hello-world/Generated/helloworld.pb.swift | 1 - .../InteroperabilityTests/Generated/empty.pb.swift | 1 - .../Generated/empty_service.pb.swift | 13 +------------ .../InteroperabilityTests/Generated/test.pb.swift | 13 +------------ Sources/Services/Health/Generated/health.pb.swift | 1 - .../Generated/grpc_core_stats.pb.swift | 1 - .../grpc_testing_benchmark_service.pb.swift | 13 +------------ .../Generated/grpc_testing_control.pb.swift | 1 - .../Generated/grpc_testing_payloads.pb.swift | 1 - .../Generated/grpc_testing_stats.pb.swift | 1 - .../Generated/grpc_testing_worker_service.pb.swift | 13 +------------ .../Configuration/Generated/code.pb.swift | 1 - .../Configuration/Generated/rls.pb.swift | 1 - .../Configuration/Generated/rls_config.pb.swift | 1 - .../Configuration/Generated/service_config.pb.swift | 1 - .../Codegen/Normalization/normalization.pb.swift | 1 - 20 files changed, 4 insertions(+), 64 deletions(-) diff --git a/Examples/v1/Echo/Model/echo.pb.swift b/Examples/v1/Echo/Model/echo.pb.swift index 7ddf8cca9..a2b496bdd 100644 --- a/Examples/v1/Echo/Model/echo.pb.swift +++ b/Examples/v1/Echo/Model/echo.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Examples/v1/HelloWorld/Model/helloworld.pb.swift b/Examples/v1/HelloWorld/Model/helloworld.pb.swift index d0425b18f..f53870911 100644 --- a/Examples/v1/HelloWorld/Model/helloworld.pb.swift +++ b/Examples/v1/HelloWorld/Model/helloworld.pb.swift @@ -22,7 +22,6 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Examples/v1/RouteGuide/Model/route_guide.pb.swift b/Examples/v1/RouteGuide/Model/route_guide.pb.swift index 80c5ff7b6..04669f032 100644 --- a/Examples/v1/RouteGuide/Model/route_guide.pb.swift +++ b/Examples/v1/RouteGuide/Model/route_guide.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Examples/v2/echo/Generated/echo.pb.swift b/Examples/v2/echo/Generated/echo.pb.swift index 178a32b5e..88ef21ca9 100644 --- a/Examples/v2/echo/Generated/echo.pb.swift +++ b/Examples/v2/echo/Generated/echo.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Examples/v2/hello-world/Generated/helloworld.pb.swift b/Examples/v2/hello-world/Generated/helloworld.pb.swift index f3dba4167..20b4f36df 100644 --- a/Examples/v2/hello-world/Generated/helloworld.pb.swift +++ b/Examples/v2/hello-world/Generated/helloworld.pb.swift @@ -22,7 +22,6 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Sources/InteroperabilityTests/Generated/empty.pb.swift b/Sources/InteroperabilityTests/Generated/empty.pb.swift index b946bc9bc..840c3367f 100644 --- a/Sources/InteroperabilityTests/Generated/empty.pb.swift +++ b/Sources/InteroperabilityTests/Generated/empty.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Sources/InteroperabilityTests/Generated/empty_service.pb.swift b/Sources/InteroperabilityTests/Generated/empty_service.pb.swift index ac2f51edf..81eecc29e 100644 --- a/Sources/InteroperabilityTests/Generated/empty_service.pb.swift +++ b/Sources/InteroperabilityTests/Generated/empty_service.pb.swift @@ -22,15 +22,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} +// This file contained no messages, enums, or extensions. diff --git a/Sources/InteroperabilityTests/Generated/test.pb.swift b/Sources/InteroperabilityTests/Generated/test.pb.swift index b2c02d793..8947a84cb 100644 --- a/Sources/InteroperabilityTests/Generated/test.pb.swift +++ b/Sources/InteroperabilityTests/Generated/test.pb.swift @@ -25,15 +25,4 @@ // An integration test service that covers all the method signature permutations // of unary/streaming requests/responses. -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} +// This file contained no messages, enums, or extensions. diff --git a/Sources/Services/Health/Generated/health.pb.swift b/Sources/Services/Health/Generated/health.pb.swift index c1e0ff837..35bec2020 100644 --- a/Sources/Services/Health/Generated/health.pb.swift +++ b/Sources/Services/Health/Generated/health.pb.swift @@ -25,7 +25,6 @@ // The canonical version of this proto can be found at // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift index 5daa789c0..e68cf193f 100644 --- a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift +++ b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift index 19b09bc03..268a0f868 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift @@ -25,15 +25,4 @@ /// An integration test service that covers all the method signature permutations /// of unary/streaming requests/responses. -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} +// This file contained no messages, enums, or extensions. diff --git a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift index 390894079..777fff519 100644 --- a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift index 88102d044..8624160c0 100644 --- a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift index 44234bddf..2b45d0bd0 100644 --- a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift index 7328357d3..73f9c0029 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift @@ -25,15 +25,4 @@ /// An integration test service that covers all the method signature permutations /// of unary/streaming requests/responses. -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} +// This file contained no messages, enums, or extensions. diff --git a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift index f451901d4..75e9e725a 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/code.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift index 53cd64eac..36f8887af 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift index 602b2d7ea..879269999 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift index 108e1994a..c25062a78 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/service_config.pb.swift @@ -36,7 +36,6 @@ // form. This proto definition is intended to help document the schema but // will not actually be used directly by gRPC. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift index c791f6aa9..0a3f36e95 100644 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift +++ b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift @@ -22,7 +22,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file From d13926b0225defef5b38d4054bfac3167398ada8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Sep 2024 13:21:10 +0100 Subject: [PATCH 445/580] Improve error message for missing reflection data (#2038) Motivation: The message for missing reflection data isn't very helpful as it doesn't indicate what reflection data is missing. Modifications: - Improve the error message - Add note about well-known-types Result: Clearer error message --- .../Server/ReflectionService.swift | 18 ++++++++++++------ .../ReflectionServiceIntegrationTests.swift | 2 +- .../ReflectionServiceUnitTests.swift | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index b452d59b4..c55529adf 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -40,6 +40,8 @@ public final class ReflectionService: CallHandlerProvider, Sendable { /// - Parameter fileURLs: The URLs of the files containing serialized reflection data. /// - Parameter version: The version of the reflection service to create. /// + /// - Note: Reflection data for well-known-types must be provided if any of your reflection data depends + /// on them. /// - Throws: When a file can't be read from disk or parsed. public convenience init(reflectionDataFileURLs fileURLs: [URL], version: Version) throws { let filePaths: [String] @@ -64,6 +66,8 @@ public final class ReflectionService: CallHandlerProvider, Sendable { /// - Parameter filePaths: The paths to files containing serialized reflection data. /// - Parameter version: The version of the reflection service to create. /// + /// - Note: Reflection data for well-known-types must be provided if any of your reflection data depends + /// on them. /// - Throws: When a file can't be read from disk or parsed. public init(reflectionDataFilePaths filePaths: [String], version: Version) throws { let fileDescriptorProtos = try ReflectionService.readSerializedFileDescriptorProtos( @@ -218,12 +222,14 @@ internal struct ReflectionServiceData: Sendable { let serializedFileDescriptorProto = protoData.serializedFileDescriptorProto serializedFileDescriptorProtos.append(serializedFileDescriptorProto) } else { - return .failure( - GRPCStatus( - code: .notFound, - message: "The provided file or a dependency of the provided file could not be found." - ) - ) + let base = "No reflection data for '\(currentFileName)'" + let message: String + if fileName == currentFileName { + message = base + "." + } else { + message = base + " which is a dependency of '\(fileName)'." + } + return .failure(GRPCStatus(code: .notFound, message: message)) } visited.insert(currentFileName) } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index f0eb4bab4..1fdfca6e5 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -287,7 +287,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) XCTAssertEqual( message.errorResponse.errorMessage, - "The provided file or a dependency of the provided file could not be found." + "No reflection data for 'invalidFileName.proto'." ) } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index 519c3dc1a..76c51f2db 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -349,7 +349,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { status, GRPCStatus( code: .notFound, - message: "The provided file or a dependency of the provided file could not be found." + message: "No reflection data for 'invalid.proto'." ) ) } @@ -368,7 +368,8 @@ final class ReflectionServiceUnitTests: GRPCTestCase { status, GRPCStatus( code: .notFound, - message: "The provided file or a dependency of the provided file could not be found." + message: + "No reflection data for 'invalidDependency' which is a dependency of 'bar1.proto'." ) ) } From c450c1a78a5d648b2dfb0e7750020f50677b1bab Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Sep 2024 15:15:00 +0100 Subject: [PATCH 446/580] Remove 'BufferedStream' (#2018) Motivation: We use buffered stream in a couple of places which don't require backpressure. Modifications: - Replace occurrences with `AsyncStream` Result: Less code to maintain --------- Co-authored-by: Gus Cairo --- .../Internal/ClientStreamExecutor.swift | 20 +- .../Streaming/Internal/BufferedStream.swift | 1934 ----------------- .../Internal/RPCAsyncSequence+Buffered.swift | 31 - .../Streaming/RPCWriterProtocol.swift | 24 + .../InProcessClientTransport.swift | 16 +- .../ServerRPCExecutorTestHarness.swift | 19 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 27 +- .../Internal/BufferedStreamTests.swift | 1104 ---------- .../InProcessServerTransportTests.swift | 23 +- 9 files changed, 68 insertions(+), 3130 deletions(-) delete mode 100644 Sources/GRPCCore/Streaming/Internal/BufferedStream.swift delete mode 100644 Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift delete mode 100644 Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 32172b6d3..749969b2d 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -132,14 +132,18 @@ internal enum ClientStreamExecutor { return .failed(error) case .none: - let error = RPCError( - code: .internalError, - message: """ - Invalid stream. The transport returned an empty stream. This is likely to be \ - a transport-specific bug. - """ - ) - return .failed(error) + if Task.isCancelled { + throw CancellationError() + } else { + let error = RPCError( + code: .internalError, + message: """ + Invalid stream. The transport returned an empty stream. This is likely to be \ + a transport-specific bug. + """ + ) + return .failed(error) + } } }.castError(to: RPCError.self) { error in RPCError( diff --git a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift b/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift deleted file mode 100644 index f2692c2a6..000000000 --- a/Sources/GRPCCore/Streaming/Internal/BufferedStream.swift +++ /dev/null @@ -1,1934 +0,0 @@ -/* - * 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. - */ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2020-2021 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -public import DequeModule // should be @usableFromInline - -/// An asynchronous sequence generated from an error-throwing closure that -/// calls a continuation to produce new elements. -/// -/// `BufferedStream` conforms to `AsyncSequence`, providing a convenient -/// way to create an asynchronous sequence without manually implementing an -/// asynchronous iterator. In particular, an asynchronous stream is well-suited -/// to adapt callback- or delegation-based APIs to participate with -/// `async`-`await`. -/// -/// In contrast to `AsyncStream`, this type can throw an error from the awaited -/// `next()`, which terminates the stream with the thrown error. -/// -/// You initialize an `BufferedStream` with a closure that receives an -/// `BufferedStream.Continuation`. Produce elements in this closure, then -/// provide them to the stream by calling the continuation's `yield(_:)` method. -/// When there are no further elements to produce, call the continuation's -/// `finish()` method. This causes the sequence iterator to produce a `nil`, -/// which terminates the sequence. If an error occurs, call the continuation's -/// `finish(throwing:)` method, which causes the iterator's `next()` method to -/// throw the error to the awaiting call point. The continuation is `Sendable`, -/// which permits calling it from concurrent contexts external to the iteration -/// of the `BufferedStream`. -/// -/// An arbitrary source of elements can produce elements faster than they are -/// consumed by a caller iterating over them. Because of this, `BufferedStream` -/// defines a buffering behavior, allowing the stream to buffer a specific -/// number of oldest or newest elements. By default, the buffer limit is -/// `Int.max`, which means it's unbounded. -/// -/// ### Adapting Existing Code to Use Streams -/// -/// To adapt existing callback code to use `async`-`await`, use the callbacks -/// to provide values to the stream, by using the continuation's `yield(_:)` -/// method. -/// -/// Consider a hypothetical `QuakeMonitor` type that provides callers with -/// `Quake` instances every time it detects an earthquake. To receive callbacks, -/// callers set a custom closure as the value of the monitor's -/// `quakeHandler` property, which the monitor calls back as necessary. Callers -/// can also set an `errorHandler` to receive asynchronous error notifications, -/// such as the monitor service suddenly becoming unavailable. -/// -/// class QuakeMonitor { -/// var quakeHandler: ((Quake) -> Void)? -/// var errorHandler: ((Error) -> Void)? -/// -/// func startMonitoring() {…} -/// func stopMonitoring() {…} -/// } -/// -/// To adapt this to use `async`-`await`, extend the `QuakeMonitor` to add a -/// `quakes` property, of type `BufferedStream`. In the getter for -/// this property, return an `BufferedStream`, whose `build` closure -- -/// called at runtime to create the stream -- uses the continuation to -/// perform the following steps: -/// -/// 1. Creates a `QuakeMonitor` instance. -/// 2. Sets the monitor's `quakeHandler` property to a closure that receives -/// each `Quake` instance and forwards it to the stream by calling the -/// continuation's `yield(_:)` method. -/// 3. Sets the monitor's `errorHandler` property to a closure that receives -/// any error from the monitor and forwards it to the stream by calling the -/// continuation's `finish(throwing:)` method. This causes the stream's -/// iterator to throw the error and terminate the stream. -/// 4. Sets the continuation's `onTermination` property to a closure that -/// calls `stopMonitoring()` on the monitor. -/// 5. Calls `startMonitoring` on the `QuakeMonitor`. -/// -/// ``` -/// extension QuakeMonitor { -/// -/// static var throwingQuakes: BufferedStream { -/// BufferedStream { continuation in -/// let monitor = QuakeMonitor() -/// monitor.quakeHandler = { quake in -/// continuation.yield(quake) -/// } -/// monitor.errorHandler = { error in -/// continuation.finish(throwing: error) -/// } -/// continuation.onTermination = { @Sendable _ in -/// monitor.stopMonitoring() -/// } -/// monitor.startMonitoring() -/// } -/// } -/// } -/// ``` -/// -/// -/// Because the stream is an `AsyncSequence`, the call point uses the -/// `for`-`await`-`in` syntax to process each `Quake` instance as produced by the stream: -/// -/// do { -/// for try await quake in quakeStream { -/// print("Quake: \(quake.date)") -/// } -/// print("Stream done.") -/// } catch { -/// print("Error: \(error)") -/// } -/// -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal struct BufferedStream { - @usableFromInline - final class _Backing: Sendable { - @usableFromInline - let storage: _BackPressuredStorage - - @inlinable - init(storage: _BackPressuredStorage) { - self.storage = storage - } - - deinit { - self.storage.sequenceDeinitialized() - } - } - - @usableFromInline - enum _Implementation: Sendable { - /// This is the implementation with backpressure based on the Source - case backpressured(_Backing) - } - - @usableFromInline - let implementation: _Implementation -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream: AsyncSequence { - /// The asynchronous iterator for iterating an asynchronous stream. - /// - /// This type is not `Sendable`. Don't use it from multiple - /// concurrent contexts. It is a programmer error to invoke `next()` from a - /// concurrent context that contends with another such call, which - /// results in a call to `fatalError()`. - @usableFromInline - internal struct Iterator: AsyncIteratorProtocol { - @usableFromInline - final class _Backing { - @usableFromInline - let storage: _BackPressuredStorage - - @inlinable - init(storage: _BackPressuredStorage) { - self.storage = storage - self.storage.iteratorInitialized() - } - - deinit { - self.storage.iteratorDeinitialized() - } - } - - @usableFromInline - enum _Implementation { - /// This is the implementation with backpressure based on the Source - case backpressured(_Backing) - } - - @usableFromInline - var implementation: _Implementation - - @inlinable - init(implementation: _Implementation) { - self.implementation = implementation - } - - /// The next value from the asynchronous stream. - /// - /// When `next()` returns `nil`, this signifies the end of the - /// `BufferedStream`. - /// - /// It is a programmer error to invoke `next()` from a concurrent context - /// that contends with another such call, which results in a call to - /// `fatalError()`. - /// - /// If you cancel the task this iterator is running in while `next()` is - /// awaiting a value, the `BufferedStream` terminates. In this case, - /// `next()` may return `nil` immediately, or else return `nil` on - /// subsequent calls. - @inlinable - internal mutating func next() async throws -> Element? { - switch self.implementation { - case .backpressured(let backing): - return try await backing.storage.next() - } - } - } - - /// Creates the asynchronous iterator that produces elements of this - /// asynchronous sequence. - @inlinable - internal func makeAsyncIterator() -> Iterator { - switch self.implementation { - case .backpressured(let backing): - return Iterator(implementation: .backpressured(.init(storage: backing.storage))) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream: Sendable where Element: Sendable {} - -@usableFromInline -internal struct _ManagedCriticalState: @unchecked Sendable { - @usableFromInline - let lock: LockedValueBox - - @inlinable - internal init(_ initial: State) { - self.lock = .init(initial) - } - - @inlinable - internal func withCriticalRegion( - _ critical: (inout State) throws -> R - ) rethrows -> R { - try self.lock.withLockedValue(critical) - } -} - -@usableFromInline -internal struct AlreadyFinishedError: Error { - @inlinable - init() {} -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream { - /// A mechanism to interface between producer code and an asynchronous stream. - /// - /// Use this source to provide elements to the stream by calling one of the `write` methods, then terminate the stream normally - /// by calling the `finish()` method. You can also use the source's `finish(throwing:)` method to terminate the stream by - /// throwing an error. - @usableFromInline - internal struct Source: Sendable { - /// A strategy that handles the backpressure of the asynchronous stream. - @usableFromInline - internal struct BackPressureStrategy: Sendable { - /// When the high watermark is reached producers will be suspended. All producers will be resumed again once - /// the low watermark is reached. - @inlinable - internal static func watermark(low: Int, high: Int) -> BackPressureStrategy { - BackPressureStrategy( - internalBackPressureStrategy: .watermark(.init(low: low, high: high)) - ) - } - - @inlinable - init(internalBackPressureStrategy: _InternalBackPressureStrategy) { - self._internalBackPressureStrategy = internalBackPressureStrategy - } - - @usableFromInline - let _internalBackPressureStrategy: _InternalBackPressureStrategy - } - - /// A type that indicates the result of writing elements to the source. - @frozen - @usableFromInline - internal enum WriteResult: Sendable { - /// A token that is returned when the asynchronous stream's backpressure strategy indicated that production should - /// be suspended. Use this token to enqueue a callback by calling the ``enqueueCallback(_:)`` method. - @usableFromInline - internal struct CallbackToken: Sendable { - @usableFromInline - let id: UInt - @usableFromInline - init(id: UInt) { - self.id = id - } - } - - /// Indicates that more elements should be produced and written to the source. - case produceMore - - /// Indicates that a callback should be enqueued. - /// - /// The associated token should be passed to the ``enqueueCallback(_:)`` method. - case enqueueCallback(CallbackToken) - } - - /// Backing class for the source used to hook a deinit. - @usableFromInline - final class _Backing: Sendable { - @usableFromInline - let storage: _BackPressuredStorage - - @inlinable - init(storage: _BackPressuredStorage) { - self.storage = storage - } - - deinit { - self.storage.sourceDeinitialized() - } - } - - /// A callback to invoke when the stream finished. - /// - /// The stream finishes and calls this closure in the following cases: - /// - No iterator was created and the sequence was deinited - /// - An iterator was created and deinited - /// - After ``finish(throwing:)`` was called and all elements have been consumed - /// - The consuming task got cancelled - @inlinable - internal var onTermination: (@Sendable () -> Void)? { - set { - self._backing.storage.onTermination = newValue - } - get { - self._backing.storage.onTermination - } - } - - @usableFromInline - var _backing: _Backing - - @inlinable - internal init(storage: _BackPressuredStorage) { - self._backing = .init(storage: storage) - } - - /// Writes new elements to the asynchronous stream. - /// - /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the - /// first element of the provided sequence. If the asynchronous stream already terminated then this method will throw an error - /// indicating the failure. - /// - /// - Parameter sequence: The elements to write to the asynchronous stream. - /// - Returns: The result that indicates if more elements should be produced at this time. - @inlinable - internal func write(contentsOf sequence: S) throws -> WriteResult - where Element == S.Element, S: Sequence { - try self._backing.storage.write(contentsOf: sequence) - } - - /// Write the element to the asynchronous stream. - /// - /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the - /// provided element. If the asynchronous stream already terminated then this method will throw an error - /// indicating the failure. - /// - /// - Parameter element: The element to write to the asynchronous stream. - /// - Returns: The result that indicates if more elements should be produced at this time. - @inlinable - internal func write(_ element: Element) throws -> WriteResult { - try self._backing.storage.write(contentsOf: CollectionOfOne(element)) - } - - /// Enqueues a callback that will be invoked once more elements should be produced. - /// - /// Call this method after ``write(contentsOf:)`` or ``write(:)`` returned ``WriteResult/enqueueCallback(_:)``. - /// - /// - Important: Enqueueing the same token multiple times is not allowed. - /// - /// - Parameters: - /// - callbackToken: The callback token. - /// - onProduceMore: The callback which gets invoked once more elements should be produced. - @inlinable - internal func enqueueCallback( - callbackToken: WriteResult.CallbackToken, - onProduceMore: @escaping @Sendable (Result) -> Void - ) { - self._backing.storage.enqueueProducer( - callbackToken: callbackToken, - onProduceMore: onProduceMore - ) - } - - /// Cancel an enqueued callback. - /// - /// Call this method to cancel a callback enqueued by the ``enqueueCallback(callbackToken:onProduceMore:)`` method. - /// - /// - Note: This methods supports being called before ``enqueueCallback(callbackToken:onProduceMore:)`` is called and - /// will mark the passed `callbackToken` as cancelled. - /// - /// - Parameter callbackToken: The callback token. - @inlinable - internal func cancelCallback(callbackToken: WriteResult.CallbackToken) { - self._backing.storage.cancelProducer(callbackToken: callbackToken) - } - - /// Write new elements to the asynchronous stream and provide a callback which will be invoked once more elements should be produced. - /// - /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the - /// first element of the provided sequence. If the asynchronous stream already terminated then `onProduceMore` will be invoked with - /// a `Result.failure`. - /// - /// - Parameters: - /// - sequence: The elements to write to the asynchronous stream. - /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be - /// invoked during the call to ``write(contentsOf:onProduceMore:)``. - @inlinable - internal func write( - contentsOf sequence: S, - onProduceMore: @escaping @Sendable (Result) -> Void - ) where Element == S.Element, S: Sequence { - do { - let writeResult = try self.write(contentsOf: sequence) - - switch writeResult { - case .produceMore: - onProduceMore(Result.success(())) - - case .enqueueCallback(let callbackToken): - self.enqueueCallback(callbackToken: callbackToken, onProduceMore: onProduceMore) - } - } catch { - onProduceMore(.failure(error)) - } - } - - /// Writes the element to the asynchronous stream. - /// - /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the - /// provided element. If the asynchronous stream already terminated then `onProduceMore` will be invoked with - /// a `Result.failure`. - /// - /// - Parameters: - /// - sequence: The element to write to the asynchronous stream. - /// - onProduceMore: The callback which gets invoked once more elements should be produced. This callback might be - /// invoked during the call to ``write(_:onProduceMore:)``. - @inlinable - internal func write( - _ element: Element, - onProduceMore: @escaping @Sendable (Result) -> Void - ) { - self.write(contentsOf: CollectionOfOne(element), onProduceMore: onProduceMore) - } - - /// Write new elements to the asynchronous stream. - /// - /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the - /// first element of the provided sequence. If the asynchronous stream already terminated then this method will throw an error - /// indicating the failure. - /// - /// This method returns once more elements should be produced. - /// - /// - Parameters: - /// - sequence: The elements to write to the asynchronous stream. - @inlinable - internal func write(contentsOf sequence: S) async throws - where Element == S.Element, S: Sequence { - let writeResult = try { try self.write(contentsOf: sequence) }() - - switch writeResult { - case .produceMore: - return - - case .enqueueCallback(let callbackToken): - try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - self.enqueueCallback( - callbackToken: callbackToken, - onProduceMore: { result in - switch result { - case .success(): - continuation.resume(returning: ()) - case .failure(let error): - continuation.resume(throwing: error) - } - } - ) - } - } onCancel: { - self.cancelCallback(callbackToken: callbackToken) - } - } - } - - /// Write new element to the asynchronous stream. - /// - /// If there is a task consuming the stream and awaiting the next element then the task will get resumed with the - /// provided element. If the asynchronous stream already terminated then this method will throw an error - /// indicating the failure. - /// - /// This method returns once more elements should be produced. - /// - /// - Parameters: - /// - sequence: The element to write to the asynchronous stream. - @inlinable - internal func write(_ element: Element) async throws { - try await self.write(contentsOf: CollectionOfOne(element)) - } - - /// Write the elements of the asynchronous sequence to the asynchronous stream. - /// - /// This method returns once the provided asynchronous sequence or the the asynchronous stream finished. - /// - /// - Important: This method does not finish the source if consuming the upstream sequence terminated. - /// - /// - Parameters: - /// - sequence: The elements to write to the asynchronous stream. - @inlinable - internal func write(contentsOf sequence: S) async throws - where Element == S.Element, S: AsyncSequence { - for try await element in sequence { - try await self.write(contentsOf: CollectionOfOne(element)) - } - } - - /// Indicates that the production terminated. - /// - /// After all buffered elements are consumed the next iteration point will return `nil` or throw an error. - /// - /// Calling this function more than once has no effect. After calling finish, the stream enters a terminal state and doesn't accept - /// new elements. - /// - /// - Parameters: - /// - error: The error to throw, or `nil`, to finish normally. - @inlinable - internal func finish(throwing error: (any Error)?) { - self._backing.storage.finish(error) - } - } - - /// Initializes a new ``BufferedStream`` and an ``BufferedStream/Source``. - /// - /// - Parameters: - /// - elementType: The element type of the stream. - /// - failureType: The failure type of the stream. - /// - backPressureStrategy: The backpressure strategy that the stream should use. - /// - Returns: A tuple containing the stream and its source. The source should be passed to the - /// producer while the stream should be passed to the consumer. - @inlinable - internal static func makeStream( - of elementType: Element.Type = Element.self, - throwing failureType: (any Error).Type = (any Error).self, - backPressureStrategy: Source.BackPressureStrategy - ) -> (`Self`, Source) where any Error == any Error { - let storage = _BackPressuredStorage( - backPressureStrategy: backPressureStrategy._internalBackPressureStrategy - ) - let source = Source(storage: storage) - - return (.init(storage: storage), source) - } - - @inlinable - init(storage: _BackPressuredStorage) { - self.implementation = .backpressured(.init(storage: storage)) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream { - @usableFromInline - struct _WatermarkBackPressureStrategy: Sendable { - /// The low watermark where demand should start. - @usableFromInline - let _low: Int - /// The high watermark where demand should be stopped. - @usableFromInline - let _high: Int - - /// Initializes a new ``_WatermarkBackPressureStrategy``. - /// - /// - Parameters: - /// - low: The low watermark where demand should start. - /// - high: The high watermark where demand should be stopped. - @inlinable - init(low: Int, high: Int) { - precondition(low <= high) - self._low = low - self._high = high - } - - @inlinable - func didYield(bufferDepth: Int) -> Bool { - // We are demanding more until we reach the high watermark - return bufferDepth < self._high - } - - @inlinable - func didConsume(bufferDepth: Int) -> Bool { - // We start demanding again once we are below the low watermark - return bufferDepth < self._low - } - } - - @usableFromInline - enum _InternalBackPressureStrategy: Sendable { - case watermark(_WatermarkBackPressureStrategy) - - @inlinable - mutating func didYield(bufferDepth: Int) -> Bool { - switch self { - case .watermark(let strategy): - return strategy.didYield(bufferDepth: bufferDepth) - } - } - - @inlinable - mutating func didConsume(bufferDepth: Int) -> Bool { - switch self { - case .watermark(let strategy): - return strategy.didConsume(bufferDepth: bufferDepth) - } - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream { - @usableFromInline - struct _BackPressuredStorage: Sendable { - @usableFromInline - let _stateMachine: _ManagedCriticalState<_StateMachine> - - @usableFromInline - var onTermination: (@Sendable () -> Void)? { - nonmutating set { - self._stateMachine.withCriticalRegion { - $0._onTermination = newValue - } - } - get { - self._stateMachine.withCriticalRegion { - $0._onTermination - } - } - } - - @inlinable - init( - backPressureStrategy: _InternalBackPressureStrategy - ) { - self._stateMachine = .init(.init(backPressureStrategy: backPressureStrategy)) - } - - @inlinable - func sequenceDeinitialized() { - let action = self._stateMachine.withCriticalRegion { - $0.sequenceDeinitialized() - } - - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): - for producerContinuation in producerContinuations { - producerContinuation(.failure(AlreadyFinishedError())) - } - onTermination?() - - case .none: - break - } - } - - @inlinable - func iteratorInitialized() { - self._stateMachine.withCriticalRegion { - $0.iteratorInitialized() - } - } - - @inlinable - func iteratorDeinitialized() { - let action = self._stateMachine.withCriticalRegion { - $0.iteratorDeinitialized() - } - - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .failProducersAndCallOnTermination(let producerContinuations, let onTermination): - for producerContinuation in producerContinuations { - producerContinuation(.failure(AlreadyFinishedError())) - } - onTermination?() - - case .none: - break - } - } - - @inlinable - func sourceDeinitialized() { - let action = self._stateMachine.withCriticalRegion { - $0.sourceDeinitialized() - } - - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .failProducersAndCallOnTermination( - let consumer, - let producerContinuations, - let onTermination - ): - consumer?.resume(returning: nil) - for producerContinuation in producerContinuations { - producerContinuation(.failure(AlreadyFinishedError())) - } - onTermination?() - - case .failProducers(let producerContinuations): - for producerContinuation in producerContinuations { - producerContinuation(.failure(AlreadyFinishedError())) - } - - case .none: - break - } - } - - @inlinable - func write( - contentsOf sequence: some Sequence - ) throws -> Source.WriteResult { - let action = self._stateMachine.withCriticalRegion { - return $0.write(sequence) - } - - switch action { - case .returnProduceMore: - return .produceMore - - case .returnEnqueue(let callbackToken): - return .enqueueCallback(callbackToken) - - case .resumeConsumerAndReturnProduceMore(let continuation, let element): - continuation.resume(returning: element) - return .produceMore - - case .resumeConsumerAndReturnEnqueue(let continuation, let element, let callbackToken): - continuation.resume(returning: element) - return .enqueueCallback(callbackToken) - - case .throwFinishedError: - throw AlreadyFinishedError() - } - } - - @inlinable - func enqueueProducer( - callbackToken: Source.WriteResult.CallbackToken, - onProduceMore: @escaping @Sendable (Result) -> Void - ) { - let action = self._stateMachine.withCriticalRegion { - $0.enqueueProducer(callbackToken: callbackToken, onProduceMore: onProduceMore) - } - - switch action { - case .resumeProducer(let onProduceMore): - onProduceMore(Result.success(())) - - case .resumeProducerWithError(let onProduceMore, let error): - onProduceMore(Result.failure(error)) - - case .none: - break - } - } - - @inlinable - func cancelProducer(callbackToken: Source.WriteResult.CallbackToken) { - let action = self._stateMachine.withCriticalRegion { - $0.cancelProducer(callbackToken: callbackToken) - } - - switch action { - case .resumeProducerWithCancellationError(let onProduceMore): - onProduceMore(Result.failure(CancellationError())) - - case .none: - break - } - } - - @inlinable - func finish(_ failure: (any Error)?) { - let action = self._stateMachine.withCriticalRegion { - $0.finish(failure) - } - - switch action { - case .callOnTermination(let onTermination): - onTermination?() - - case .resumeConsumerAndCallOnTermination( - let consumerContinuation, - let failure, - let onTermination - ): - switch failure { - case .some(let error): - consumerContinuation.resume(throwing: error) - case .none: - consumerContinuation.resume(returning: nil) - } - - onTermination?() - - case .resumeProducers(let producerContinuations): - for producerContinuation in producerContinuations { - producerContinuation(.failure(AlreadyFinishedError())) - } - - case .none: - break - } - } - - @inlinable - func next() async throws -> Element? { - let action = self._stateMachine.withCriticalRegion { - $0.next() - } - - switch action { - case .returnElement(let element): - return element - - case .returnElementAndResumeProducers(let element, let producerContinuations): - for producerContinuation in producerContinuations { - producerContinuation(Result.success(())) - } - - return element - - case .returnErrorAndCallOnTermination(let failure, let onTermination): - onTermination?() - switch failure { - case .some(let error): - throw error - - case .none: - return nil - } - - case .returnNil: - return nil - - case .suspendTask: - return try await self.suspendNext() - } - } - - @inlinable - func suspendNext() async throws -> Element? { - return try await withTaskCancellationHandler { - return try await withCheckedThrowingContinuation { continuation in - let action = self._stateMachine.withCriticalRegion { - $0.suspendNext(continuation: continuation) - } - - switch action { - case .resumeConsumerWithElement(let continuation, let element): - continuation.resume(returning: element) - - case .resumeConsumerWithElementAndProducers( - let continuation, - let element, - let producerContinuations - ): - continuation.resume(returning: element) - for producerContinuation in producerContinuations { - producerContinuation(Result.success(())) - } - - case .resumeConsumerWithErrorAndCallOnTermination( - let continuation, - let failure, - let onTermination - ): - switch failure { - case .some(let error): - continuation.resume(throwing: error) - - case .none: - continuation.resume(returning: nil) - } - onTermination?() - - case .resumeConsumerWithNil(let continuation): - continuation.resume(returning: nil) - - case .none: - break - } - } - } onCancel: { - let action = self._stateMachine.withCriticalRegion { - $0.cancelNext() - } - - switch action { - case .resumeConsumerWithCancellationErrorAndCallOnTermination( - let continuation, - let onTermination - ): - continuation.resume(throwing: CancellationError()) - onTermination?() - - case .failProducersAndCallOnTermination( - let producerContinuations, - let onTermination - ): - for producerContinuation in producerContinuations { - producerContinuation(.failure(AlreadyFinishedError())) - } - onTermination?() - - case .none: - break - } - } - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream { - /// The state machine of the backpressured async stream. - @usableFromInline - struct _StateMachine { - @usableFromInline - enum _State { - @usableFromInline - struct Initial { - /// The backpressure strategy. - @usableFromInline - var backPressureStrategy: _InternalBackPressureStrategy - /// Indicates if the iterator was initialized. - @usableFromInline - var iteratorInitialized: Bool - /// The onTermination callback. - @usableFromInline - var onTermination: (@Sendable () -> Void)? - - @inlinable - init( - backPressureStrategy: _InternalBackPressureStrategy, - iteratorInitialized: Bool, - onTermination: (@Sendable () -> Void)? = nil - ) { - self.backPressureStrategy = backPressureStrategy - self.iteratorInitialized = iteratorInitialized - self.onTermination = onTermination - } - } - - @usableFromInline - struct Streaming { - /// The backpressure strategy. - @usableFromInline - var backPressureStrategy: _InternalBackPressureStrategy - /// Indicates if the iterator was initialized. - @usableFromInline - var iteratorInitialized: Bool - /// The onTermination callback. - @usableFromInline - var onTermination: (@Sendable () -> Void)? - /// The buffer of elements. - @usableFromInline - var buffer: Deque - /// The optional consumer continuation. - @usableFromInline - var consumerContinuation: CheckedContinuation? - /// The producer continuations. - @usableFromInline - var producerContinuations: Deque<(UInt, (Result) -> Void)> - /// The producers that have been cancelled. - @usableFromInline - var cancelledAsyncProducers: Deque - /// Indicates if we currently have outstanding demand. - @usableFromInline - var hasOutstandingDemand: Bool - - @inlinable - init( - backPressureStrategy: _InternalBackPressureStrategy, - iteratorInitialized: Bool, - onTermination: (@Sendable () -> Void)? = nil, - buffer: Deque, - consumerContinuation: CheckedContinuation? = nil, - producerContinuations: Deque<(UInt, (Result) -> Void)>, - cancelledAsyncProducers: Deque, - hasOutstandingDemand: Bool - ) { - self.backPressureStrategy = backPressureStrategy - self.iteratorInitialized = iteratorInitialized - self.onTermination = onTermination - self.buffer = buffer - self.consumerContinuation = consumerContinuation - self.producerContinuations = producerContinuations - self.cancelledAsyncProducers = cancelledAsyncProducers - self.hasOutstandingDemand = hasOutstandingDemand - } - } - - @usableFromInline - struct SourceFinished { - /// Indicates if the iterator was initialized. - @usableFromInline - var iteratorInitialized: Bool - /// The buffer of elements. - @usableFromInline - var buffer: Deque - /// The failure that should be thrown after the last element has been consumed. - @usableFromInline - var failure: (any Error)? - /// The onTermination callback. - @usableFromInline - var onTermination: (@Sendable () -> Void)? - - @inlinable - init( - iteratorInitialized: Bool, - buffer: Deque, - failure: (any Error)? = nil, - onTermination: (@Sendable () -> Void)? - ) { - self.iteratorInitialized = iteratorInitialized - self.buffer = buffer - self.failure = failure - self.onTermination = onTermination - } - } - - case initial(Initial) - /// The state once either any element was yielded or `next()` was called. - case streaming(Streaming) - /// The state once the underlying source signalled that it is finished. - case sourceFinished(SourceFinished) - - /// The state once there can be no outstanding demand. This can happen if: - /// 1. The iterator was deinited - /// 2. The underlying source finished and all buffered elements have been consumed - case finished(iteratorInitialized: Bool) - - /// An intermediate state to avoid CoWs. - case modify - } - - /// The state machine's current state. - @usableFromInline - var _state: _State - - // The ID used for the next CallbackToken. - @usableFromInline - var nextCallbackTokenID: UInt = 0 - - @inlinable - var _onTermination: (@Sendable () -> Void)? { - set { - switch self._state { - case .initial(var initial): - initial.onTermination = newValue - self._state = .initial(initial) - - case .streaming(var streaming): - streaming.onTermination = newValue - self._state = .streaming(streaming) - - case .sourceFinished(var sourceFinished): - sourceFinished.onTermination = newValue - self._state = .sourceFinished(sourceFinished) - - case .finished: - break - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - get { - switch self._state { - case .initial(let initial): - return initial.onTermination - - case .streaming(let streaming): - return streaming.onTermination - - case .sourceFinished(let sourceFinished): - return sourceFinished.onTermination - - case .finished: - return nil - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - } - - /// Initializes a new `StateMachine`. - /// - /// We are passing and holding the back-pressure strategy here because - /// it is a customizable extension of the state machine. - /// - /// - Parameter backPressureStrategy: The back-pressure strategy. - @inlinable - init( - backPressureStrategy: _InternalBackPressureStrategy - ) { - self._state = .initial( - .init( - backPressureStrategy: backPressureStrategy, - iteratorInitialized: false - ) - ) - } - - /// Generates the next callback token. - @inlinable - mutating func nextCallbackToken() -> Source.WriteResult.CallbackToken { - let id = self.nextCallbackTokenID - self.nextCallbackTokenID += 1 - return .init(id: id) - } - - /// Actions returned by `sequenceDeinitialized()`. - @usableFromInline - enum SequenceDeinitializedAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((@Sendable () -> Void)?) - /// Indicates that all producers should be failed and `onTermination` should be called. - case failProducersAndCallOnTermination( - [(Result) -> Void], - (@Sendable () -> Void)? - ) - } - - @inlinable - mutating func sequenceDeinitialized() -> SequenceDeinitializedAction? { - switch self._state { - case .initial(let initial): - if initial.iteratorInitialized { - // An iterator was created and we deinited the sequence. - // This is an expected pattern and we just continue on normal. - return .none - } else { - // No iterator was created so we can transition to finished right away. - self._state = .finished(iteratorInitialized: false) - - return .callOnTermination(initial.onTermination) - } - - case .streaming(let streaming): - if streaming.iteratorInitialized { - // An iterator was created and we deinited the sequence. - // This is an expected pattern and we just continue on normal. - return .none - } else { - // No iterator was created so we can transition to finished right away. - self._state = .finished(iteratorInitialized: false) - - return .failProducersAndCallOnTermination( - Array(streaming.producerContinuations.map { $0.1 }), - streaming.onTermination - ) - } - - case .sourceFinished(let sourceFinished): - if sourceFinished.iteratorInitialized { - // An iterator was created and we deinited the sequence. - // This is an expected pattern and we just continue on normal. - return .none - } else { - // No iterator was created so we can transition to finished right away. - self._state = .finished(iteratorInitialized: false) - - return .callOnTermination(sourceFinished.onTermination) - } - - case .finished: - // We are already finished so there is nothing left to clean up. - // This is just the references dropping afterwards. - return .none - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - @inlinable - mutating func iteratorInitialized() { - switch self._state { - case .initial(var initial): - if initial.iteratorInitialized { - // Our sequence is a unicast sequence and does not support multiple AsyncIterator's - fatalError("Only a single AsyncIterator can be created") - } else { - // The first and only iterator was initialized. - initial.iteratorInitialized = true - self._state = .initial(initial) - } - - case .streaming(var streaming): - if streaming.iteratorInitialized { - // Our sequence is a unicast sequence and does not support multiple AsyncIterator's - fatalError("Only a single AsyncIterator can be created") - } else { - // The first and only iterator was initialized. - streaming.iteratorInitialized = true - self._state = .streaming(streaming) - } - - case .sourceFinished(var sourceFinished): - if sourceFinished.iteratorInitialized { - // Our sequence is a unicast sequence and does not support multiple AsyncIterator's - fatalError("Only a single AsyncIterator can be created") - } else { - // The first and only iterator was initialized. - sourceFinished.iteratorInitialized = true - self._state = .sourceFinished(sourceFinished) - } - - case .finished(iteratorInitialized: true): - // Our sequence is a unicast sequence and does not support multiple AsyncIterator's - fatalError("Only a single AsyncIterator can be created") - - case .finished(iteratorInitialized: false): - // It is strange that an iterator is created after we are finished - // but it can definitely happen, e.g. - // Sequence.init -> source.finish -> sequence.makeAsyncIterator - self._state = .finished(iteratorInitialized: true) - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `iteratorDeinitialized()`. - @usableFromInline - enum IteratorDeinitializedAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((@Sendable () -> Void)?) - /// Indicates that all producers should be failed and `onTermination` should be called. - case failProducersAndCallOnTermination( - [(Result) -> Void], - (@Sendable () -> Void)? - ) - } - - @inlinable - mutating func iteratorDeinitialized() -> IteratorDeinitializedAction? { - switch self._state { - case .initial(let initial): - if initial.iteratorInitialized { - // An iterator was created and deinited. Since we only support - // a single iterator we can now transition to finish. - self._state = .finished(iteratorInitialized: true) - return .callOnTermination(initial.onTermination) - } else { - // An iterator needs to be initialized before it can be deinitialized. - fatalError("AsyncStream internal inconsistency") - } - - case .streaming(let streaming): - if streaming.iteratorInitialized { - // An iterator was created and deinited. Since we only support - // a single iterator we can now transition to finish. - self._state = .finished(iteratorInitialized: true) - - return .failProducersAndCallOnTermination( - Array(streaming.producerContinuations.map { $0.1 }), - streaming.onTermination - ) - } else { - // An iterator needs to be initialized before it can be deinitialized. - fatalError("AsyncStream internal inconsistency") - } - - case .sourceFinished(let sourceFinished): - if sourceFinished.iteratorInitialized { - // An iterator was created and deinited. Since we only support - // a single iterator we can now transition to finish. - self._state = .finished(iteratorInitialized: true) - return .callOnTermination(sourceFinished.onTermination) - } else { - // An iterator needs to be initialized before it can be deinitialized. - fatalError("AsyncStream internal inconsistency") - } - - case .finished: - // We are already finished so there is nothing left to clean up. - // This is just the references dropping afterwards. - return .none - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `sourceDeinitialized()`. - @usableFromInline - enum SourceDeinitializedAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((() -> Void)?) - /// Indicates that all producers should be failed and `onTermination` should be called. - case failProducersAndCallOnTermination( - CheckedContinuation?, - [(Result) -> Void], - (@Sendable () -> Void)? - ) - /// Indicates that all producers should be failed. - case failProducers([(Result) -> Void]) - } - - @inlinable - mutating func sourceDeinitialized() -> SourceDeinitializedAction? { - switch self._state { - case .initial(let initial): - // The source got deinited before anything was written - self._state = .finished(iteratorInitialized: initial.iteratorInitialized) - return .callOnTermination(initial.onTermination) - - case .streaming(let streaming): - if streaming.buffer.isEmpty { - // We can transition to finished right away since the buffer is empty now - self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) - - return .failProducersAndCallOnTermination( - streaming.consumerContinuation, - Array(streaming.producerContinuations.map { $0.1 }), - streaming.onTermination - ) - } else { - // The continuation must be `nil` if the buffer has elements - precondition(streaming.consumerContinuation == nil) - - self._state = .sourceFinished( - .init( - iteratorInitialized: streaming.iteratorInitialized, - buffer: streaming.buffer, - failure: nil, - onTermination: streaming.onTermination - ) - ) - - return .failProducers( - Array(streaming.producerContinuations.map { $0.1 }) - ) - } - - case .sourceFinished, .finished: - // This is normal and we just have to tolerate it - return .none - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `write()`. - @usableFromInline - enum WriteAction { - /// Indicates that the producer should be notified to produce more. - case returnProduceMore - /// Indicates that the producer should be suspended to stop producing. - case returnEnqueue( - callbackToken: Source.WriteResult.CallbackToken - ) - /// Indicates that the consumer should be resumed and the producer should be notified to produce more. - case resumeConsumerAndReturnProduceMore( - continuation: CheckedContinuation, - element: Element - ) - /// Indicates that the consumer should be resumed and the producer should be suspended. - case resumeConsumerAndReturnEnqueue( - continuation: CheckedContinuation, - element: Element, - callbackToken: Source.WriteResult.CallbackToken - ) - /// Indicates that the producer has been finished. - case throwFinishedError - - @inlinable - init( - callbackToken: Source.WriteResult.CallbackToken?, - continuationAndElement: (CheckedContinuation, Element)? = nil - ) { - switch (callbackToken, continuationAndElement) { - case (.none, .none): - self = .returnProduceMore - - case (.some(let callbackToken), .none): - self = .returnEnqueue(callbackToken: callbackToken) - - case (.none, .some((let continuation, let element))): - self = .resumeConsumerAndReturnProduceMore( - continuation: continuation, - element: element - ) - - case (.some(let callbackToken), .some((let continuation, let element))): - self = .resumeConsumerAndReturnEnqueue( - continuation: continuation, - element: element, - callbackToken: callbackToken - ) - } - } - } - - @inlinable - mutating func write(_ sequence: some Sequence) -> WriteAction { - switch self._state { - case .initial(var initial): - var buffer = Deque() - buffer.append(contentsOf: sequence) - - let shouldProduceMore = initial.backPressureStrategy.didYield( - bufferDepth: buffer.count - ) - let callbackToken = shouldProduceMore ? nil : self.nextCallbackToken() - - self._state = .streaming( - .init( - backPressureStrategy: initial.backPressureStrategy, - iteratorInitialized: initial.iteratorInitialized, - onTermination: initial.onTermination, - buffer: buffer, - consumerContinuation: nil, - producerContinuations: .init(), - cancelledAsyncProducers: .init(), - hasOutstandingDemand: shouldProduceMore - ) - ) - - return .init(callbackToken: callbackToken) - - case .streaming(var streaming): - self._state = .modify - - streaming.buffer.append(contentsOf: sequence) - - // We have an element and can resume the continuation - let shouldProduceMore = streaming.backPressureStrategy.didYield( - bufferDepth: streaming.buffer.count - ) - streaming.hasOutstandingDemand = shouldProduceMore - let callbackToken = shouldProduceMore ? nil : self.nextCallbackToken() - - if let consumerContinuation = streaming.consumerContinuation { - guard let element = streaming.buffer.popFirst() else { - // We got a yield of an empty sequence. We just tolerate this. - self._state = .streaming(streaming) - - return .init(callbackToken: callbackToken) - } - - // We got a consumer continuation and an element. We can resume the consumer now - streaming.consumerContinuation = nil - self._state = .streaming(streaming) - return .init( - callbackToken: callbackToken, - continuationAndElement: (consumerContinuation, element) - ) - } else { - // We don't have a suspended consumer so we just buffer the elements - self._state = .streaming(streaming) - return .init( - callbackToken: callbackToken - ) - } - - case .sourceFinished, .finished: - // If the source has finished we are dropping the elements. - return .throwFinishedError - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `enqueueProducer()`. - @usableFromInline - enum EnqueueProducerAction { - /// Indicates that the producer should be notified to produce more. - case resumeProducer((Result) -> Void) - /// Indicates that the producer should be notified about an error. - case resumeProducerWithError((Result) -> Void, any Error) - } - - @inlinable - mutating func enqueueProducer( - callbackToken: Source.WriteResult.CallbackToken, - onProduceMore: @Sendable @escaping (Result) -> Void - ) -> EnqueueProducerAction? { - switch self._state { - case .initial: - // We need to transition to streaming before we can suspend - // This is enforced because the CallbackToken has no internal init so - // one must create it by calling `write` first. - fatalError("AsyncStream internal inconsistency") - - case .streaming(var streaming): - if let index = streaming.cancelledAsyncProducers.firstIndex(of: callbackToken.id) { - // Our producer got marked as cancelled. - self._state = .modify - streaming.cancelledAsyncProducers.remove(at: index) - self._state = .streaming(streaming) - - return .resumeProducerWithError(onProduceMore, CancellationError()) - } else if streaming.hasOutstandingDemand { - // We hit an edge case here where we wrote but the consuming thread got interleaved - return .resumeProducer(onProduceMore) - } else { - self._state = .modify - streaming.producerContinuations.append((callbackToken.id, onProduceMore)) - - self._state = .streaming(streaming) - return .none - } - - case .sourceFinished, .finished: - // Since we are unlocking between yielding and suspending the yield - // It can happen that the source got finished or the consumption fully finishes. - return .resumeProducerWithError(onProduceMore, AlreadyFinishedError()) - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `cancelProducer()`. - @usableFromInline - enum CancelProducerAction { - /// Indicates that the producer should be notified about cancellation. - case resumeProducerWithCancellationError((Result) -> Void) - } - - @inlinable - mutating func cancelProducer( - callbackToken: Source.WriteResult.CallbackToken - ) -> CancelProducerAction? { - switch self._state { - case .initial: - // We need to transition to streaming before we can suspend - fatalError("AsyncStream internal inconsistency") - - case .streaming(var streaming): - if let index = streaming.producerContinuations.firstIndex(where: { - $0.0 == callbackToken.id - }) { - // We have an enqueued producer that we need to resume now - self._state = .modify - let continuation = streaming.producerContinuations.remove(at: index).1 - self._state = .streaming(streaming) - - return .resumeProducerWithCancellationError(continuation) - } else { - // The task that yields was cancelled before yielding so the cancellation handler - // got invoked right away - self._state = .modify - streaming.cancelledAsyncProducers.append(callbackToken.id) - self._state = .streaming(streaming) - - return .none - } - - case .sourceFinished, .finished: - // Since we are unlocking between yielding and suspending the yield - // It can happen that the source got finished or the consumption fully finishes. - return .none - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `finish()`. - @usableFromInline - enum FinishAction { - /// Indicates that `onTermination` should be called. - case callOnTermination((() -> Void)?) - /// Indicates that the consumer should be resumed with the failure, the producers - /// should be resumed with an error and `onTermination` should be called. - case resumeConsumerAndCallOnTermination( - consumerContinuation: CheckedContinuation, - failure: (any Error)?, - onTermination: (() -> Void)? - ) - /// Indicates that the producers should be resumed with an error. - case resumeProducers( - producerContinuations: [(Result) -> Void] - ) - } - - @inlinable - mutating func finish(_ failure: (any Error)?) -> FinishAction? { - switch self._state { - case .initial(let initial): - // Nothing was yielded nor did anybody call next - // This means we can transition to sourceFinished and store the failure - self._state = .sourceFinished( - .init( - iteratorInitialized: initial.iteratorInitialized, - buffer: .init(), - failure: failure, - onTermination: initial.onTermination - ) - ) - - return .callOnTermination(initial.onTermination) - - case .streaming(let streaming): - if let consumerContinuation = streaming.consumerContinuation { - // We have a continuation, this means our buffer must be empty - // Furthermore, we can now transition to finished - // and resume the continuation with the failure - precondition(streaming.buffer.isEmpty, "Expected an empty buffer") - precondition( - streaming.producerContinuations.isEmpty, - "Expected no suspended producers" - ) - - self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) - - return .resumeConsumerAndCallOnTermination( - consumerContinuation: consumerContinuation, - failure: failure, - onTermination: streaming.onTermination - ) - } else { - self._state = .sourceFinished( - .init( - iteratorInitialized: streaming.iteratorInitialized, - buffer: streaming.buffer, - failure: failure, - onTermination: streaming.onTermination - ) - ) - - return .resumeProducers( - producerContinuations: Array(streaming.producerContinuations.map { $0.1 }) - ) - } - - case .sourceFinished, .finished: - // If the source has finished, finishing again has no effect. - return .none - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `next()`. - @usableFromInline - enum NextAction { - /// Indicates that the element should be returned to the caller. - case returnElement(Element) - /// Indicates that the element should be returned to the caller and that all producers should be called. - case returnElementAndResumeProducers(Element, [(Result) -> Void]) - /// Indicates that the `Error` should be returned to the caller and that `onTermination` should be called. - case returnErrorAndCallOnTermination((any Error)?, (() -> Void)?) - /// Indicates that the `nil` should be returned to the caller. - case returnNil - /// Indicates that the `Task` of the caller should be suspended. - case suspendTask - } - - @inlinable - mutating func next() -> NextAction { - switch self._state { - case .initial(let initial): - // We are not interacting with the back-pressure strategy here because - // we are doing this inside `next(:)` - self._state = .streaming( - .init( - backPressureStrategy: initial.backPressureStrategy, - iteratorInitialized: initial.iteratorInitialized, - onTermination: initial.onTermination, - buffer: Deque(), - consumerContinuation: nil, - producerContinuations: .init(), - cancelledAsyncProducers: .init(), - hasOutstandingDemand: false - ) - ) - - return .suspendTask - case .streaming(var streaming): - guard streaming.consumerContinuation == nil else { - // We have multiple AsyncIterators iterating the sequence - fatalError("AsyncStream internal inconsistency") - } - - self._state = .modify - - if let element = streaming.buffer.popFirst() { - // We have an element to fulfil the demand right away. - let shouldProduceMore = streaming.backPressureStrategy.didConsume( - bufferDepth: streaming.buffer.count - ) - streaming.hasOutstandingDemand = shouldProduceMore - - if shouldProduceMore { - // There is demand and we have to resume our producers - let producers = Array(streaming.producerContinuations.map { $0.1 }) - streaming.producerContinuations.removeAll() - self._state = .streaming(streaming) - return .returnElementAndResumeProducers(element, producers) - } else { - // We don't have any new demand, so we can just return the element. - self._state = .streaming(streaming) - return .returnElement(element) - } - } else { - // There is nothing in the buffer to fulfil the demand so we need to suspend. - // We are not interacting with the back-pressure strategy here because - // we are doing this inside `suspendNext` - self._state = .streaming(streaming) - - return .suspendTask - } - - case .sourceFinished(var sourceFinished): - // Check if we have an element left in the buffer and return it - self._state = .modify - - if let element = sourceFinished.buffer.popFirst() { - self._state = .sourceFinished(sourceFinished) - - return .returnElement(element) - } else { - // We are returning the queued failure now and can transition to finished - self._state = .finished(iteratorInitialized: sourceFinished.iteratorInitialized) - - return .returnErrorAndCallOnTermination( - sourceFinished.failure, - sourceFinished.onTermination - ) - } - - case .finished: - return .returnNil - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `suspendNext()`. - @usableFromInline - enum SuspendNextAction { - /// Indicates that the consumer should be resumed. - case resumeConsumerWithElement(CheckedContinuation, Element) - /// Indicates that the consumer and all producers should be resumed. - case resumeConsumerWithElementAndProducers( - CheckedContinuation, - Element, - [(Result) -> Void] - ) - /// Indicates that the consumer should be resumed with the failure and that `onTermination` should be called. - case resumeConsumerWithErrorAndCallOnTermination( - CheckedContinuation, - (any Error)?, - (() -> Void)? - ) - /// Indicates that the consumer should be resumed with `nil`. - case resumeConsumerWithNil(CheckedContinuation) - } - - @inlinable - mutating func suspendNext( - continuation: CheckedContinuation - ) -> SuspendNextAction? { - switch self._state { - case .initial: - // We need to transition to streaming before we can suspend - preconditionFailure("AsyncStream internal inconsistency") - - case .streaming(var streaming): - guard streaming.consumerContinuation == nil else { - // We have multiple AsyncIterators iterating the sequence - fatalError( - "This should never happen since we only allow a single Iterator to be created" - ) - } - - self._state = .modify - - // We have to check here again since we might have a producer interleave next and suspendNext - if let element = streaming.buffer.popFirst() { - // We have an element to fulfil the demand right away. - - let shouldProduceMore = streaming.backPressureStrategy.didConsume( - bufferDepth: streaming.buffer.count - ) - streaming.hasOutstandingDemand = shouldProduceMore - - if shouldProduceMore { - // There is demand and we have to resume our producers - let producers = Array(streaming.producerContinuations.map { $0.1 }) - streaming.producerContinuations.removeAll() - self._state = .streaming(streaming) - return .resumeConsumerWithElementAndProducers( - continuation, - element, - producers - ) - } else { - // We don't have any new demand, so we can just return the element. - self._state = .streaming(streaming) - return .resumeConsumerWithElement(continuation, element) - } - } else { - // There is nothing in the buffer to fulfil the demand so we to store the continuation. - streaming.consumerContinuation = continuation - self._state = .streaming(streaming) - - return .none - } - - case .sourceFinished(var sourceFinished): - // Check if we have an element left in the buffer and return it - self._state = .modify - - if let element = sourceFinished.buffer.popFirst() { - self._state = .sourceFinished(sourceFinished) - - return .resumeConsumerWithElement(continuation, element) - } else { - // We are returning the queued failure now and can transition to finished - self._state = .finished(iteratorInitialized: sourceFinished.iteratorInitialized) - - return .resumeConsumerWithErrorAndCallOnTermination( - continuation, - sourceFinished.failure, - sourceFinished.onTermination - ) - } - - case .finished: - return .resumeConsumerWithNil(continuation) - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - - /// Actions returned by `cancelNext()`. - @usableFromInline - enum CancelNextAction { - /// Indicates that the continuation should be resumed with a cancellation error, the producers should be finished and call onTermination. - case resumeConsumerWithCancellationErrorAndCallOnTermination( - CheckedContinuation, - (() -> Void)? - ) - /// Indicates that the producers should be finished and call onTermination. - case failProducersAndCallOnTermination([(Result) -> Void], (() -> Void)?) - } - - @inlinable - mutating func cancelNext() -> CancelNextAction? { - switch self._state { - case .initial: - // We need to transition to streaming before we can suspend - fatalError("AsyncStream internal inconsistency") - - case .streaming(let streaming): - self._state = .finished(iteratorInitialized: streaming.iteratorInitialized) - - if let consumerContinuation = streaming.consumerContinuation { - precondition( - streaming.producerContinuations.isEmpty, - "Internal inconsistency. Unexpected producer continuations." - ) - return .resumeConsumerWithCancellationErrorAndCallOnTermination( - consumerContinuation, - streaming.onTermination - ) - } else { - return .failProducersAndCallOnTermination( - Array(streaming.producerContinuations.map { $0.1 }), - streaming.onTermination - ) - } - - case .sourceFinished, .finished: - return .none - - case .modify: - fatalError("AsyncStream internal inconsistency") - } - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream.Source: ClosableRPCWriterProtocol { - @inlinable - func finish() { - self.finish(throwing: nil) - } - - @inlinable - func finish(throwing error: any Error) { - self.finish(throwing: error as (any Error)?) - } -} diff --git a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift b/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift deleted file mode 100644 index 3a0c75aa6..000000000 --- a/Sources/GRPCCore/Streaming/Internal/RPCAsyncSequence+Buffered.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension RPCAsyncSequence where Failure == any Error { - @inlinable - package static func makeBackpressuredStream( - of elementType: Element.Type = Element.self, - watermarks: (low: Int, high: Int) - ) -> (stream: Self, writer: RPCWriter.Closable) { - let (stream, continuation) = BufferedStream.makeStream( - of: Element.self, - backPressureStrategy: .watermark(low: watermarks.low, high: watermarks.high) - ) - - return (RPCAsyncSequence(wrapping: stream), RPCWriter.Closable(wrapping: continuation)) - } -} diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index fc10e8a63..b2607f733 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -65,3 +65,27 @@ public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// being thrown. func finish(throwing error: any Error) } + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncThrowingStream.Continuation: RPCWriterProtocol { + public func write(_ element: Element) async throws { + self.yield(element) + } + + public func write(contentsOf elements: some Sequence) async throws { + for element in elements { + self.yield(element) + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncThrowingStream.Continuation: ClosableRPCWriterProtocol where Failure == any Error { + public func finish() { + self.finish(throwing: nil) + } + + public func finish(throwing error: any Error) { + self.finish(throwing: .some(error)) + } +} diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index 04ff14a98..4759ec449 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -231,23 +231,19 @@ public struct InProcessClientTransport: ClientTransport { options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { - let request = RPCAsyncSequence.makeBackpressuredStream( - watermarks: (16, 32) - ) - let response = RPCAsyncSequence.makeBackpressuredStream( - watermarks: (16, 32) - ) + let request = AsyncThrowingStream.makeStream(of: RPCRequestPart.self) + let response = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) let clientStream = RPCStream( descriptor: descriptor, - inbound: response.stream, - outbound: request.writer + inbound: RPCAsyncSequence(wrapping: response.stream), + outbound: RPCWriter.Closable(wrapping: request.continuation) ) let serverStream = RPCStream( descriptor: descriptor, - inbound: request.stream, - outbound: response.writer + inbound: RPCAsyncSequence(wrapping: request.stream), + outbound: RPCWriter.Closable(wrapping: response.continuation) ) let waitForConnectionStream: AsyncStream? = self.state.withLockedValue { state in diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index d924808b6..ca23d02a6 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -80,31 +80,24 @@ struct ServerRPCExecutorTestHarness { RPCAsyncSequence ) async throws -> Void ) async throws { - let input = RPCAsyncSequence.makeBackpressuredStream( - of: RPCRequestPart.self, - watermarks: (16, 32) - ) - - let output = RPCAsyncSequence.makeBackpressuredStream( - of: RPCResponsePart.self, - watermarks: (16, 32) - ) + let input = AsyncThrowingStream.makeStream(of: RPCRequestPart.self) + let output = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await producer(input.writer) + try await producer(RPCWriter.Closable(wrapping: input.continuation)) } group.addTask { - try await consumer(output.stream) + try await consumer(RPCAsyncSequence(wrapping: output.stream)) } group.addTask { await ServerRPCExecutor.execute( stream: RPCStream( descriptor: MethodDescriptor(service: "foo", method: "bar"), - inbound: input.stream, - outbound: output.writer + inbound: RPCAsyncSequence(wrapping: input.stream), + outbound: RPCWriter.Closable(wrapping: output.continuation) ), deserializer: deserializer, serializer: serializer, diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 55ba15505..f360936b4 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -343,13 +343,22 @@ final class GRPCClientTests: XCTestCase { } // Wait for client and server to be running. - try await Task.sleep(for: .milliseconds(10)) + try await client.unary( + request: .init(message: [3, 1, 4, 1, 5]), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer(), + options: .defaults + ) { response in + let message = try response.message + XCTAssertEqual(message, [3, 1, 4, 1, 5]) + } let task = Task { try await client.clientStreaming( - request: .init(producer: { writer in + request: ClientRequest.Stream { writer in try await Task.sleep(for: .seconds(5)) - }), + }, descriptor: BinaryEcho.Methods.collect, serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), @@ -361,18 +370,6 @@ final class GRPCClientTests: XCTestCase { } } - // Check requests are getting through. - try await client.unary( - request: .init(message: [3, 1, 4, 1, 5]), - descriptor: BinaryEcho.Methods.collect, - serializer: IdentitySerializer(), - deserializer: IdentityDeserializer(), - options: .defaults - ) { response in - let message = try response.message - XCTAssertEqual(message, [3, 1, 4, 1, 5]) - } - task.cancel() try await task.value group.cancelAll() diff --git a/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift b/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift deleted file mode 100644 index 041019f1d..000000000 --- a/Tests/GRPCCoreTests/Streaming/Internal/BufferedStreamTests.swift +++ /dev/null @@ -1,1104 +0,0 @@ -/* - * Copyright 2023, 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 XCTest - -@testable import GRPCCore - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -final class BufferedStreamTests: XCTestCase { - // MARK: - sequenceDeinitialized - - func testSequenceDeinitialized_whenNoIterator() async throws { - var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - withExtendedLifetime(stream) {} - stream = nil - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - do { - _ = try { try source.write(2) }() - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is AlreadyFinishedError) - } - - group.cancelAll() - } - } - - func testSequenceDeinitialized_whenIterator() async throws { - var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - var iterator = stream?.makeAsyncIterator() - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - try withExtendedLifetime(stream) { - let writeResult = try source.write(1) - writeResult.assertIsProducerMore() - } - - stream = nil - - do { - let writeResult = try { try source.write(2) }() - writeResult.assertIsProducerMore() - } catch { - XCTFail("Expected no error to be thrown") - } - - let element1 = try await iterator?.next() - XCTAssertEqual(element1, 1) - let element2 = try await iterator?.next() - XCTAssertEqual(element2, 2) - - group.cancelAll() - } - } - - func testSequenceDeinitialized_whenFinished() async throws { - var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - withExtendedLifetime(stream) { - source.finish(throwing: nil) - } - - stream = nil - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - do { - _ = try { try source.write(1) }() - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is AlreadyFinishedError) - } - - group.cancelAll() - } - } - - func testSequenceDeinitialized_whenStreaming_andSuspendedProducer() async throws { - var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 2) - ) - - _ = try { try source.write(1) }() - - do { - try await withCheckedThrowingContinuation { continuation in - source.write(1) { result in - continuation.resume(with: result) - } - - stream = nil - _ = stream?.makeAsyncIterator() - } - } catch { - XCTAssertTrue(error is AlreadyFinishedError) - } - } - - // MARK: - iteratorInitialized - - func testIteratorInitialized_whenInitial() async throws { - let (stream, _) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - _ = stream.makeAsyncIterator() - } - - func testIteratorInitialized_whenStreaming() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - try await source.write(1) - - var iterator = stream.makeAsyncIterator() - let element = try await iterator.next() - XCTAssertEqual(element, 1) - } - - func testIteratorInitialized_whenSourceFinished() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - try await source.write(1) - source.finish(throwing: nil) - - var iterator = stream.makeAsyncIterator() - let element1 = try await iterator.next() - XCTAssertEqual(element1, 1) - let element2 = try await iterator.next() - XCTAssertNil(element2) - } - - func testIteratorInitialized_whenFinished() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - source.finish(throwing: nil) - - var iterator = stream.makeAsyncIterator() - let element = try await iterator.next() - XCTAssertNil(element) - } - - // MARK: - iteratorDeinitialized - - func testIteratorDeinitialized_whenInitial() async throws { - var (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() - iterator = nil - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testIteratorDeinitialized_whenStreaming() async throws { - var (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - try await source.write(1) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() - iterator = nil - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testIteratorDeinitialized_whenSourceFinished() async throws { - var (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - try await source.write(1) - source.finish(throwing: nil) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() - iterator = nil - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testIteratorDeinitialized_whenFinished() async throws { - var (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source.onTermination = { - onTerminationContinuation.finish() - } - - source.finish(throwing: nil) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() - iterator = nil - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testIteratorDeinitialized_whenStreaming_andSuspendedProducer() async throws { - var (stream, source): (BufferedStream?, BufferedStream.Source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 2) - ) - - var iterator: BufferedStream.AsyncIterator? = stream?.makeAsyncIterator() - stream = nil - - _ = try { try source.write(1) }() - - do { - try await withCheckedThrowingContinuation { continuation in - source.write(1) { result in - continuation.resume(with: result) - } - - iterator = nil - } - } catch { - XCTAssertTrue(error is AlreadyFinishedError) - } - - _ = try await iterator?.next() - } - - // MARK: - sourceDeinitialized - - func testSourceDeinitialized_whenInitial() async throws { - var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.onTermination = { - onTerminationContinuation.finish() - } - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - source = nil - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - - withExtendedLifetime(stream) {} - } - - func testSourceDeinitialized_whenStreaming_andEmptyBuffer() async throws { - var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.onTermination = { - onTerminationContinuation.finish() - } - - try await source?.write(1) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() - _ = try await iterator?.next() - - source = nil - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testSourceDeinitialized_whenStreaming_andNotEmptyBuffer() async throws { - var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.onTermination = { - onTerminationContinuation.finish() - } - - try await source?.write(1) - try await source?.write(2) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() - _ = try await iterator?.next() - - source = nil - - _ = await onTerminationIterator.next() - - _ = try await iterator?.next() - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testSourceDeinitialized_whenSourceFinished() async throws { - var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.onTermination = { - onTerminationContinuation.finish() - } - - try await source?.write(1) - try await source?.write(2) - source?.finish(throwing: nil) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - var iterator: BufferedStream.AsyncIterator? = stream.makeAsyncIterator() - _ = try await iterator?.next() - - source = nil - - _ = await onTerminationIterator.next() - - _ = try await iterator?.next() - _ = try await iterator?.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testSourceDeinitialized_whenFinished() async throws { - var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 5, high: 10) - ) - - let (onTerminationStream, onTerminationContinuation) = AsyncStream.makeStream() - source?.onTermination = { - onTerminationContinuation.finish() - } - - source?.finish(throwing: nil) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while !Task.isCancelled { - onTerminationContinuation.yield() - try await Task.sleep(nanoseconds: 200_000_000) - } - } - - var onTerminationIterator = onTerminationStream.makeAsyncIterator() - _ = await onTerminationIterator.next() - - _ = stream.makeAsyncIterator() - - source = nil - - _ = await onTerminationIterator.next() - - let terminationResult: Void? = await onTerminationIterator.next() - XCTAssertNil(terminationResult) - - group.cancelAll() - } - } - - func testSourceDeinitialized_whenStreaming_andSuspendedProducer() async throws { - var (stream, source): (BufferedStream, BufferedStream.Source?) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 0, high: 0) - ) - let (producerStream, producerContinuation) = AsyncThrowingStream.makeStream() - var iterator = stream.makeAsyncIterator() - - source?.write(1) { - producerContinuation.yield(with: $0) - } - - _ = try await iterator.next() - source = nil - - do { - try await producerStream.first { _ in true } - XCTFail("We expected to throw here") - } catch { - XCTAssertTrue(error is AlreadyFinishedError) - } - } - - // MARK: - write - - func testWrite_whenInitial() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 5) - ) - - try await source.write(1) - - var iterator = stream.makeAsyncIterator() - let element = try await iterator.next() - XCTAssertEqual(element, 1) - } - - func testWrite_whenStreaming_andNoConsumer() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 5) - ) - - try await source.write(1) - try await source.write(2) - - var iterator = stream.makeAsyncIterator() - let element1 = try await iterator.next() - XCTAssertEqual(element1, 1) - let element2 = try await iterator.next() - XCTAssertEqual(element2, 2) - } - - func testWrite_whenStreaming_andSuspendedConsumer() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 5) - ) - - try await withThrowingTaskGroup(of: Int?.self) { group in - group.addTask { - return try await stream.first { _ in true } - } - - // This is always going to be a bit racy since we need the call to next() suspend - try await Task.sleep(nanoseconds: 500_000_000) - - try await source.write(1) - let element = try await group.next() - XCTAssertEqual(element, 1) - } - } - - func testWrite_whenStreaming_andSuspendedConsumer_andEmptySequence() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 5) - ) - - try await withThrowingTaskGroup(of: Int?.self) { group in - group.addTask { - return try await stream.first { _ in true } - } - - // This is always going to be a bit racy since we need the call to next() suspend - try await Task.sleep(nanoseconds: 500_000_000) - - try await source.write(contentsOf: []) - try await source.write(contentsOf: [1]) - let element = try await group.next() - XCTAssertEqual(element, 1) - } - } - - // MARK: - enqueueProducer - - func testEnqueueProducer_whenStreaming_andAndCancelled() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 2) - ) - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - - try await source.write(1) - - let writeResult = try { try source.write(2) }() - - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.cancelCallback(callbackToken: callbackToken) - - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - } - - do { - _ = try await producerStream.first { _ in true } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - } - - let element = try await stream.first { _ in true } - XCTAssertEqual(element, 1) - } - - func testEnqueueProducer_whenStreaming_andAndCancelled_andAsync() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 2) - ) - - try await source.write(1) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await source.write(2) - } - - group.cancelAll() - do { - try await group.next() - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - } - } - - let element = try await stream.first { _ in true } - XCTAssertEqual(element, 1) - } - - func testEnqueueProducer_whenStreaming_andInterleaving() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 1) - ) - var iterator = stream.makeAsyncIterator() - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - - let writeResult = try { try source.write(1) }() - - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - let element = try await iterator.next() - XCTAssertEqual(element, 1) - - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - } - - do { - _ = try await producerStream.first { _ in true } - } catch { - XCTFail("Expected no error to be thrown") - } - } - - func testEnqueueProducer_whenStreaming_andSuspending() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 1) - ) - var iterator = stream.makeAsyncIterator() - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - - let writeResult = try { try source.write(1) }() - - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - } - - let element = try await iterator.next() - XCTAssertEqual(element, 1) - - do { - _ = try await producerStream.first { _ in true } - } catch { - XCTFail("Expected no error to be thrown") - } - } - - func testEnqueueProducer_whenFinished() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 1) - ) - var iterator = stream.makeAsyncIterator() - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - - let writeResult = try { try source.write(1) }() - - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.finish(throwing: nil) - - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - } - - let element = try await iterator.next() - XCTAssertEqual(element, 1) - - do { - _ = try await producerStream.first { _ in true } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is AlreadyFinishedError) - } - } - - // MARK: - cancelProducer - - func testCancelProducer_whenStreaming() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 2) - ) - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - - try await source.write(1) - - let writeResult = try { try source.write(2) }() - - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - - source.cancelCallback(callbackToken: callbackToken) - } - - do { - _ = try await producerStream.first { _ in true } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - } - - let element = try await stream.first { _ in true } - XCTAssertEqual(element, 1) - } - - func testCancelProducer_whenSourceFinished() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 2) - ) - - let (producerStream, producerSource) = AsyncThrowingStream.makeStream() - - try await source.write(1) - - let writeResult = try { try source.write(2) }() - - switch writeResult { - case .produceMore: - preconditionFailure() - case .enqueueCallback(let callbackToken): - source.enqueueCallback(callbackToken: callbackToken) { result in - producerSource.yield(with: result) - } - - source.finish(throwing: nil) - - source.cancelCallback(callbackToken: callbackToken) - } - - do { - _ = try await producerStream.first { _ in true } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is AlreadyFinishedError) - } - - let element = try await stream.first { _ in true } - XCTAssertEqual(element, 1) - } - - // MARK: - finish - - func testFinish_whenStreaming_andConsumerSuspended() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 1) - ) - - try await withThrowingTaskGroup(of: Int?.self) { group in - group.addTask { - return try await stream.first { $0 == 2 } - } - - // This is always going to be a bit racy since we need the call to next() suspend - try await Task.sleep(nanoseconds: 500_000_000) - - source.finish(throwing: nil) - let element = try await group.next() - XCTAssertEqual(element, .some(nil)) - } - } - - func testFinish_whenInitial() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 1, high: 1) - ) - - source.finish(throwing: CancellationError()) - - do { - for try await _ in stream {} - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - } - - } - - // MARK: - Backpressure - - func testBackPressure() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 4) - ) - - let (backPressureEventStream, backPressureEventContinuation) = AsyncStream.makeStream( - of: Void.self - ) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - while true { - backPressureEventContinuation.yield(()) - try await source.write(contentsOf: [1]) - } - } - - var backPressureEventIterator = backPressureEventStream.makeAsyncIterator() - var iterator = stream.makeAsyncIterator() - - await backPressureEventIterator.next() - await backPressureEventIterator.next() - await backPressureEventIterator.next() - await backPressureEventIterator.next() - - _ = try await iterator.next() - _ = try await iterator.next() - _ = try await iterator.next() - - await backPressureEventIterator.next() - await backPressureEventIterator.next() - await backPressureEventIterator.next() - - group.cancelAll() - } - } - - func testBackPressureSync() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 4) - ) - - let (backPressureEventStream, backPressureEventContinuation) = AsyncStream.makeStream( - of: Void.self - ) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - @Sendable func yield() { - backPressureEventContinuation.yield(()) - source.write(contentsOf: [1]) { result in - switch result { - case .success: - yield() - - case .failure: - break - } - } - } - - yield() - } - - var backPressureEventIterator = backPressureEventStream.makeAsyncIterator() - var iterator = stream.makeAsyncIterator() - - await backPressureEventIterator.next() - await backPressureEventIterator.next() - await backPressureEventIterator.next() - await backPressureEventIterator.next() - - _ = try await iterator.next() - _ = try await iterator.next() - _ = try await iterator.next() - - await backPressureEventIterator.next() - await backPressureEventIterator.next() - await backPressureEventIterator.next() - - group.cancelAll() - } - } - - func testThrowsError() async throws { - let (stream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 4) - ) - - try await source.write(1) - try await source.write(2) - source.finish(throwing: CancellationError()) - - var elements = [Int]() - var iterator = stream.makeAsyncIterator() - - do { - while let element = try await iterator.next() { - elements.append(element) - } - XCTFail("Expected an error to be thrown") - } catch { - XCTAssertTrue(error is CancellationError) - XCTAssertEqual(elements, [1, 2]) - } - - let element = try await iterator.next() - XCTAssertNil(element) - } - - func testAsyncSequenceWrite() async throws { - let (stream, continuation) = AsyncStream.makeStream() - let (backpressuredStream, source) = BufferedStream.makeStream( - of: Int.self, - backPressureStrategy: .watermark(low: 2, high: 4) - ) - - continuation.yield(1) - continuation.yield(2) - continuation.finish() - - try await source.write(contentsOf: stream) - source.finish(throwing: nil) - - let elements = try await backpressuredStream.collect() - XCTAssertEqual(elements, [1, 2]) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension BufferedStream.Source.WriteResult { - func assertIsProducerMore() { - switch self { - case .produceMore: - return - - case .enqueueCallback: - XCTFail("Expected produceMore") - } - } - - func assertIsEnqueueCallback() { - switch self { - case .produceMore: - XCTFail("Expected enqueueCallback") - - case .enqueueCallback: - return - } - } -} diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 4d9c997d9..cd4475137 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -23,6 +23,8 @@ import XCTest final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessServerTransport() + + let outbound = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) let stream = RPCStream< RPCAsyncSequence, RPCWriter.Closable @@ -34,11 +36,7 @@ final class InProcessServerTransportTests: XCTestCase { $0.finish() } ), - outbound: .init( - wrapping: BufferedStream.Source( - storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) - ) - ) + outbound: RPCWriter.Closable(wrapping: outbound.continuation) ) let messages = LockedValueBox<[RPCRequestPart]?>(nil) @@ -59,6 +57,8 @@ final class InProcessServerTransportTests: XCTestCase { func testStopListening() async throws { let transport = InProcessServerTransport() + + let firstStreamOutbound = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) let firstStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( @@ -69,11 +69,7 @@ final class InProcessServerTransportTests: XCTestCase { $0.finish() } ), - outbound: .init( - wrapping: BufferedStream.Source( - storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) - ) - ) + outbound: RPCWriter.Closable(wrapping: firstStreamOutbound.continuation) ) try transport.acceptStream(firstStream) @@ -86,6 +82,7 @@ final class InProcessServerTransportTests: XCTestCase { transport.stopListening() + let secondStreamOutbound = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) let secondStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( @@ -96,11 +93,7 @@ final class InProcessServerTransportTests: XCTestCase { $0.finish() } ), - outbound: .init( - wrapping: BufferedStream.Source( - storage: .init(backPressureStrategy: .watermark(.init(low: 1, high: 1))) - ) - ) + outbound: RPCWriter.Closable(wrapping: secondStreamOutbound.continuation) ) XCTAssertThrowsError(ofType: RPCError.self) { From 493dbefc0a39d3e7c8cbeba474de3eb404d5ebc1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Sep 2024 15:28:24 +0100 Subject: [PATCH 447/580] Adopt `Mutex` where possible (#2026) Motivation: Swift 6 has a `Mutex` which we can use in place of `LockedValueBox` or `NIOLockedValueBox` in a bunch of places. Modifications: - Use `Mutex` where possible - This causes a number of other changes too as `Mutex` doesn't allocate: the allocations move to the type holding it instead. This makes sense as the objects which were previously structs didn't have value semantics. Result: Fewer uses of our own lock, no uses of NIOs lock --- .../ClientRPCExecutor+HedgingExecutor.swift | 34 ++++++++++--- .../Concurrency Primitives/Lock.swift | 16 +++--- .../GRPCCore/Transport/RetryThrottle.swift | 20 ++++---- .../Client/Connection/Connection.swift | 30 +++++------ .../Client/Connection/GRPCChannel.swift | 41 +++++++-------- .../LoadBalancers/LoadBalancer.swift | 4 +- .../LoadBalancers/PickFirstLoadBalancer.swift | 31 +++++------ .../RoundRobinLoadBalancer.swift | 14 ++--- .../Connection/LoadBalancers/Subchannel.swift | 32 ++++++------ .../Client/Connection/RequestQueue.swift | 2 +- .../HTTP2ServerTransport+Posix.swift | 13 ++--- ...TP2ServerTransport+TransportServices.swift | 14 ++--- .../InProcessClientTransport.swift | 19 +++---- Sources/Services/Health/HealthService.swift | 11 ++-- .../Transport/RetryThrottleTests.swift | 1 + .../Connection/Connection+Equatable.swift | 8 +-- .../Client/Connection/ConnectionTests.swift | 3 +- .../LoadBalancers/LoadBalancerTest.swift | 4 +- .../PickFirstLoadBalancerTests.swift | 2 +- .../RoundRobinLoadBalancerTests.swift | 2 +- .../LoadBalancers/SubchannelTests.swift | 2 +- .../Client/Connection/RequestQueueTests.swift | 51 ++++++++++++------- .../Connection/Utilities/ConnectionTest.swift | 6 +-- .../Connection/Utilities/TestServer.swift | 26 +++++----- .../Internal/TimerTests.swift | 30 +++++------ .../InProcessServerTransportTests.swift | 5 +- 26 files changed, 232 insertions(+), 189 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 5bb27190f..d21b2d3bf 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +public import Synchronization // would be internal but for usableFromInline + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { @usableFromInline @@ -176,10 +178,10 @@ extension ClientRPCExecutor.HedgingExecutor { // of this. To avoid this each attempt goes via a state check before yielding to the sequence // ensuring that only one response is used. (If this wasn't the case the response handler // could be invoked more than once.) - let state = LockedValueBox(State(policy: self.policy)) + let state = SharedState(policy: self.policy) // There's always a first attempt, safe to '!'. - let (attempt, scheduleNext) = state.withLockedValue({ $0.nextAttemptNumber() })! + let (attempt, scheduleNext) = state.withState({ $0.nextAttemptNumber() })! group.addTask { let result = await self._startAttempt( @@ -210,7 +212,7 @@ extension ClientRPCExecutor.HedgingExecutor { switch outcome { case .ran: // Start a new attempt and possibly schedule the next. - if let (attempt, scheduleNext) = state.withLockedValue({ $0.nextAttemptNumber() }) { + if let (attempt, scheduleNext) = state.withState({ $0.nextAttemptNumber() }) { group.addTask { let result = await self._startAttempt( request: request, @@ -263,7 +265,7 @@ extension ClientRPCExecutor.HedgingExecutor { nextScheduledAttempt.cancel() - if let (attempt, scheduleNext) = state.withLockedValue({ $0.nextAttemptNumber() }) { + if let (attempt, scheduleNext) = state.withState({ $0.nextAttemptNumber() }) { group.addTask { let result = await self._startAttempt( request: request, @@ -317,7 +319,7 @@ extension ClientRPCExecutor.HedgingExecutor { method: MethodDescriptor, options: CallOptions, attempt: Int, - state: LockedValueBox, + state: SharedState, picker: (stream: BroadcastAsyncSequence, continuation: BroadcastAsyncSequence.Source), responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R ) async -> _HedgingAttemptTaskResult.AttemptResult { @@ -364,7 +366,7 @@ extension ClientRPCExecutor.HedgingExecutor { case .success: self.transport.retryThrottle?.recordSuccess() - if state.withLockedValue({ $0.receivedUsableResponse() }) { + if state.withState({ $0.receivedUsableResponse() }) { try? await picker.continuation.write(attempt) picker.continuation.finish() let result = await Result { try await responseHandler(response) } @@ -385,7 +387,7 @@ extension ClientRPCExecutor.HedgingExecutor { // A fatal error code counts as a success to the throttle. self.transport.retryThrottle?.recordSuccess() - if state.withLockedValue({ $0.receivedUsableResponse() }) { + if state.withState({ $0.receivedUsableResponse() }) { try! await picker.continuation.write(attempt) picker.continuation.finish() let result = await Result { try await responseHandler(response) } @@ -428,6 +430,24 @@ extension ClientRPCExecutor.HedgingExecutor { } } + @usableFromInline + final class SharedState { + @usableFromInline + let state: Mutex + + @inlinable + init(policy: HedgingPolicy) { + self.state = Mutex(State(policy: policy)) + } + + @inlinable + func withState(_ body: @Sendable (inout State) -> ReturnType) -> ReturnType { + self.state.withLock { + body(&$0) + } + } + } + @usableFromInline struct State { @usableFromInline diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift index 7bb9e4ca2..2867e9982 100644 --- a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift +++ b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift @@ -237,17 +237,17 @@ extension UnsafeMutablePointer { } @usableFromInline -package struct LockedValueBox { +struct LockedValueBox { @usableFromInline let storage: LockStorage @inlinable - package init(_ value: Value) { + init(_ value: Value) { self.storage = .create(value: value) } @inlinable - package func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { return try self.storage.withLockedValue(mutate) } @@ -255,30 +255,30 @@ package struct LockedValueBox { /// /// Prefer ``withLockedValue(_:)`` where possible. @usableFromInline - package var unsafe: Unsafe { + var unsafe: Unsafe { Unsafe(storage: self.storage) } @usableFromInline - package struct Unsafe { + struct Unsafe { @usableFromInline let storage: LockStorage /// Manually acquire the lock. @inlinable - package func lock() { + func lock() { self.storage.lock() } /// Manually release the lock. @inlinable - package func unlock() { + func unlock() { self.storage.unlock() } /// Mutate the value, assuming the lock has been acquired manually. @inlinable - package func withValueAssumingLockIsAcquired( + func withValueAssumingLockIsAcquired( _ mutate: (inout Value) throws -> T ) rethrows -> T { return try self.storage.withUnsafeMutablePointerToHeader { value in diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index 0e94d85d6..5d6b2cf93 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +private import Synchronization + /// A throttle used to rate-limit retries and hedging attempts. /// /// gRPC prevents servers from being overloaded by retries and hedging by using a token-based @@ -28,13 +30,14 @@ /// the server. /// /// See also [gRFC A6: client retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). -public struct RetryThrottle: Sendable { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public final class RetryThrottle: Sendable { // Note: only three figures after the decimal point from the original token ratio are used so // all computation is done a scaled number of tokens (tokens * 1000). This allows us to do all // computation in integer space. /// The number of tokens available, multiplied by 1000. - private let scaledTokensAvailable: LockedValueBox + private let scaledTokensAvailable: Mutex /// The number of tokens, multiplied by 1000. private let scaledTokenRatio: Int /// The maximum number of tokens, multiplied by 1000. @@ -66,14 +69,14 @@ public struct RetryThrottle: Sendable { /// If this value is less than or equal to the retry threshold (defined as `maximumTokens / 2`) /// then RPCs will not be retried and hedging will be disabled. public var tokens: Double { - self.scaledTokensAvailable.withLockedValue { + self.scaledTokensAvailable.withLock { Double($0) / 1000 } } /// Returns whether retries and hedging are permitted at this time. public var isRetryPermitted: Bool { - self.scaledTokensAvailable.withLockedValue { + self.scaledTokensAvailable.withLock { $0 > self.scaledRetryThreshold } } @@ -100,21 +103,20 @@ public struct RetryThrottle: Sendable { self.scaledMaximumTokens = scaledTokens self.scaledRetryThreshold = scaledTokens / 2 self.scaledTokenRatio = scaledTokenRatio - self.scaledTokensAvailable = LockedValueBox(scaledTokens) + self.scaledTokensAvailable = Mutex(scaledTokens) } /// Create a new throttle. /// /// - Parameter policy: The policy to use to configure the throttle. - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public init(policy: ServiceConfig.RetryThrottling) { + public convenience init(policy: ServiceConfig.RetryThrottling) { self.init(maximumTokens: policy.maxTokens, tokenRatio: policy.tokenRatio) } /// Records a success, adding a token to the throttle. @usableFromInline func recordSuccess() { - self.scaledTokensAvailable.withLockedValue { value in + self.scaledTokensAvailable.withLock { value in value = min(self.scaledMaximumTokens, value &+ self.scaledTokenRatio) } } @@ -124,7 +126,7 @@ public struct RetryThrottle: Sendable { @usableFromInline @discardableResult func recordFailure() -> Bool { - self.scaledTokensAvailable.withLockedValue { value in + self.scaledTokensAvailable.withLock { value in value = max(0, value &- 1000) return value <= self.scaledRetryThreshold } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index 9f28ed6b3..2dbcf8996 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -15,9 +15,9 @@ */ package import GRPCCore -internal import NIOConcurrencyHelpers package import NIOCore package import NIOHTTP2 +private import Synchronization /// A `Connection` provides communication to a single remote peer. /// @@ -43,8 +43,8 @@ package import NIOHTTP2 /// } /// } /// ``` -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -package struct Connection: Sendable { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package final class Connection: Sendable { /// Events which can happen over the lifetime of the connection. package enum Event: Sendable { /// The connect attempt succeeded and the connection is ready to use. @@ -96,7 +96,7 @@ package struct Connection: Sendable { private let http2Connector: any HTTP2Connector /// The state of the connection. - private let state: NIOLockedValueBox + private let state: Mutex /// The default max request message size in bytes, 4 MiB. private static var defaultMaxRequestMessageSizeBytes: Int { @@ -120,7 +120,7 @@ package struct Connection: Sendable { self.http2Connector = http2Connector self.event = AsyncStream.makeStream(of: Event.self) self.input = AsyncStream.makeStream(of: Input.self) - self.state = NIOLockedValueBox(.notConnected) + self.state = Mutex(.notConnected) } /// Connect and run the connection. @@ -135,7 +135,7 @@ package struct Connection: Sendable { switch connectResult { case .success(let connected): // Connected successfully, update state and report the event. - self.state.withLockedValue { state in + self.state.withLock { state in state.connected(connected) } @@ -151,7 +151,7 @@ package struct Connection: Sendable { for await input in self.input.stream { switch input { case .close: - let asyncChannel = self.state.withLockedValue { $0.beginClosing() } + let asyncChannel = self.state.withLock { $0.beginClosing() } if let channel = asyncChannel?.channel { let event = ClientConnectionHandler.OutboundEvent.closeGracefully channel.triggerUserOutboundEvent(event, promise: nil) @@ -162,7 +162,7 @@ package struct Connection: Sendable { case .failure(let error): // Connect failed, this connection is no longer useful. - self.state.withLockedValue { $0.closed() } + self.state.withLock { $0.closed() } self.finishStreams(withEvent: .connectFailed(error)) } } @@ -180,7 +180,7 @@ package struct Connection: Sendable { descriptor: MethodDescriptor, options: CallOptions ) async throws -> Stream { - let (multiplexer, scheme) = try self.state.withLockedValue { state in + let (multiplexer, scheme) = try self.state.withLock { state in switch state { case .connected(let connected): return (connected.multiplexer, connected.scheme) @@ -259,7 +259,7 @@ package struct Connection: Sendable { self.event.continuation.yield(.connectSucceeded) case .closing(let reason): - self.state.withLockedValue { $0.closing() } + self.state.withLock { $0.closing() } switch reason { case .goAway(let errorCode, let reason): @@ -282,7 +282,7 @@ package struct Connection: Sendable { let finalEvent: Event if isReady { - let connectionCloseReason: Self.CloseReason + let connectionCloseReason: CloseReason switch channelCloseReason { case .keepaliveExpired: connectionCloseReason = .keepaliveTimeout @@ -323,7 +323,7 @@ package struct Connection: Sendable { } // The connection events sequence has finished: the connection is now closed. - self.state.withLockedValue { $0.closed() } + self.state.withLock { $0.closed() } self.finishStreams(withEvent: finalEvent) } catch { let finalEvent: Event @@ -338,7 +338,7 @@ package struct Connection: Sendable { finalEvent = .connectFailed(makeNeverReadyError(cause: error)) } - self.state.withLockedValue { $0.closed() } + self.state.withLock { $0.closed() } self.finishStreams(withEvent: finalEvent) } } @@ -350,7 +350,7 @@ package struct Connection: Sendable { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Connection { package struct Stream { package typealias Inbound = NIOAsyncChannelInboundStream @@ -412,7 +412,7 @@ extension Connection { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Connection { private enum State: Sendable { /// The connection is idle or connecting. diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 50122c11c..00fde9312 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -17,9 +17,10 @@ internal import Atomics internal import DequeModule package import GRPCCore +private import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package struct GRPCChannel: ClientTransport { +package final class GRPCChannel: ClientTransport { private enum Input: Sendable { /// Close the channel, if possible. case close @@ -43,7 +44,7 @@ package struct GRPCChannel: ClientTransport { private let resolver: NameResolver /// The state of the channel. - private let state: LockedValueBox + private let state: Mutex /// The maximum number of times to attempt to create a stream per RPC. /// @@ -68,8 +69,8 @@ package struct GRPCChannel: ClientTransport { private let defaultServiceConfig: ServiceConfig // These are both read frequently and updated infrequently so may be a bottleneck. - private let _methodConfig: LockedValueBox - private let _retryThrottle: LockedValueBox + private let _methodConfig: Mutex + private let _retryThrottle: Mutex package init( resolver: NameResolver, @@ -78,7 +79,7 @@ package struct GRPCChannel: ClientTransport { defaultServiceConfig: ServiceConfig ) { self.resolver = resolver - self.state = LockedValueBox(StateMachine()) + self.state = Mutex(StateMachine()) self._connectivityState = AsyncStream.makeStream() self.input = AsyncStream.makeStream() self.connector = connector @@ -94,10 +95,10 @@ package struct GRPCChannel: ClientTransport { self.defaultServiceConfig = defaultServiceConfig let throttle = defaultServiceConfig.retryThrottling.map { RetryThrottle(policy: $0) } - self._retryThrottle = LockedValueBox(throttle) + self._retryThrottle = Mutex(throttle) let methodConfig = MethodConfigs(serviceConfig: defaultServiceConfig) - self._methodConfig = LockedValueBox(methodConfig) + self._methodConfig = Mutex(methodConfig) } /// The connectivity state of the channel. @@ -107,7 +108,7 @@ package struct GRPCChannel: ClientTransport { /// Returns a throttle which gRPC uses to determine whether retries can be executed. package var retryThrottle: RetryThrottle? { - self._retryThrottle.withLockedValue { $0 } + self._retryThrottle.withLock { $0 } } /// Returns the configuration for a given method. @@ -115,12 +116,12 @@ package struct GRPCChannel: ClientTransport { /// - Parameter descriptor: The method to lookup configuration for. /// - Returns: Configuration for the method, if it exists. package func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { - self._methodConfig.withLockedValue { $0[descriptor] } + self._methodConfig.withLock { $0[descriptor] } } /// Establishes and maintains a connection to the remote destination. package func connect() async { - self.state.withLockedValue { $0.start() } + self.state.withLock { $0.start() } self._connectivityState.continuation.yield(.idle) await withDiscardingTaskGroup { group in @@ -146,7 +147,7 @@ package struct GRPCChannel: ClientTransport { // When the channel is closed gracefully, the task group running the load balancer mustn't // be cancelled (otherwise in-flight RPCs would fail), but the push based resolver will // continue indefinitely. Store its handle and cancel it on close when closing the channel. - self.state.withLockedValue { state in + self.state.withLock { state in state.setNameResolverTaskHandle(handle) } @@ -270,7 +271,7 @@ extension GRPCChannel { options: CallOptions ) async -> MakeStreamResult { let waitForReady = options.waitForReady ?? true - switch self.state.withLockedValue({ $0.makeStream(waitForReady: waitForReady) }) { + switch self.state.withLock({ $0.makeStream(waitForReady: waitForReady) }) { case .useLoadBalancer(let loadBalancer): return await self.makeStream( descriptor: descriptor, @@ -327,7 +328,7 @@ extension GRPCChannel { return } - let enqueued = self.state.withLockedValue { state in + let enqueued = self.state.withLock { state in state.enqueue(continuation: continuation, waitForReady: waitForReady, id: id) } @@ -338,7 +339,7 @@ extension GRPCChannel { } } } onCancel: { - let continuation = self.state.withLockedValue { state in + let continuation = self.state.withLock { state in state.dequeueContinuation(id: id) } @@ -350,7 +351,7 @@ extension GRPCChannel { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCChannel { private func handleClose(in group: inout DiscardingTaskGroup) { - switch self.state.withLockedValue({ $0.close() }) { + switch self.state.withLock({ $0.close() }) { case .close(let current, let next, let resolver, let continuations): resolver?.cancel() current.close() @@ -383,10 +384,10 @@ extension GRPCChannel { case .success(let config): // Update per RPC configuration. let methodConfig = MethodConfigs(serviceConfig: config) - self._methodConfig.withLockedValue { $0 = methodConfig } + self._methodConfig.withLock { $0 = methodConfig } let retryThrottle = config.retryThrottling.map { RetryThrottle(policy: $0) } - self._retryThrottle.withLockedValue { $0 = retryThrottle } + self._retryThrottle.withLock { $0 = retryThrottle } // Update the load balancer. self.updateLoadBalancer(serviceConfig: config, endpoints: result.endpoints, in: &group) @@ -446,7 +447,7 @@ extension GRPCChannel { let loadBalancerConfig = configFromServiceConfig ?? .pickFirst(.init(shuffleAddressList: false)) switch loadBalancerConfig { case .roundRobin: - onUpdatePolicy = self.state.withLockedValue { state in + onUpdatePolicy = self.state.withLock { state in state.changeLoadBalancerKind(to: loadBalancerConfig) { let loadBalancer = RoundRobinLoadBalancer( connector: self.connector, @@ -463,7 +464,7 @@ extension GRPCChannel { endpoints[0].addresses.shuffle() } - onUpdatePolicy = self.state.withLockedValue { state in + onUpdatePolicy = self.state.withLock { state in state.changeLoadBalancerKind(to: loadBalancerConfig) { let loadBalancer = PickFirstLoadBalancer( connector: self.connector, @@ -527,7 +528,7 @@ extension GRPCChannel { ) async { switch event { case .connectivityStateChanged(let connectivityState): - let actions = self.state.withLockedValue { state in + let actions = self.state.withLock { state in state.loadBalancerStateChanged(to: connectivityState, id: loadBalancerID) } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift index f6916e3c7..419094aba 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift @@ -14,13 +14,13 @@ * limitations under the License. */ -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package enum LoadBalancer: Sendable { case roundRobin(RoundRobinLoadBalancer) case pickFirst(PickFirstLoadBalancer) } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension LoadBalancer { package init(_ loadBalancer: RoundRobinLoadBalancer) { self = .roundRobin(loadBalancer) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift index d0e66a5a3..31c6d4382 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift @@ -15,6 +15,7 @@ */ package import GRPCCore +private import Synchronization /// A load-balancer which has a single subchannel. /// @@ -55,8 +56,8 @@ package import GRPCCore /// } /// } /// ``` -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -package struct PickFirstLoadBalancer { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package final class PickFirstLoadBalancer: Sendable { enum Input: Sendable, Hashable { /// Update the addresses used by the load balancer to the following endpoints. case updateEndpoint(Endpoint) @@ -87,7 +88,7 @@ package struct PickFirstLoadBalancer { private let enabledCompression: CompressionAlgorithmSet /// The state of the load-balancer. - private let state: LockedValueBox + private let state: Mutex /// The ID of this load balancer. internal let id: LoadBalancerID @@ -103,7 +104,7 @@ package struct PickFirstLoadBalancer { self.defaultCompression = defaultCompression self.enabledCompression = enabledCompression self.id = LoadBalancerID() - self.state = LockedValueBox(State()) + self.state = Mutex(State()) self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) self.input = AsyncStream.makeStream(of: Input.self) @@ -153,7 +154,7 @@ package struct PickFirstLoadBalancer { /// /// - Returns: A subchannel, or `nil` if there aren't any ready subchannels. package func pickSubchannel() -> Subchannel? { - let onPickSubchannel = self.state.withLockedValue { $0.pickSubchannel() } + let onPickSubchannel = self.state.withLock { $0.pickSubchannel() } switch onPickSubchannel { case .picked(let subchannel): return subchannel @@ -164,12 +165,12 @@ package struct PickFirstLoadBalancer { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension PickFirstLoadBalancer { private func handleUpdateEndpoint(_ endpoint: Endpoint, in group: inout DiscardingTaskGroup) { if endpoint.addresses.isEmpty { return } - let onUpdate = self.state.withLockedValue { state in + let onUpdate = self.state.withLock { state in state.updateEndpoint(endpoint) { endpoint, id in Subchannel( endpoint: endpoint, @@ -220,7 +221,7 @@ extension PickFirstLoadBalancer { _ connectivityState: ConnectivityState, id: SubchannelID ) { - let onUpdateState = self.state.withLockedValue { + let onUpdateState = self.state.withLock { $0.updateSubchannelConnectivityState(connectivityState, id: id) } @@ -241,13 +242,13 @@ extension PickFirstLoadBalancer { } private func handleGoAway(id: SubchannelID) { - self.state.withLockedValue { state in + self.state.withLock { state in state.receivedGoAway(id: id) } } private func handleCloseInput() { - let onClose = self.state.withLockedValue { $0.close() } + let onClose = self.state.withLock { $0.close() } switch onClose { case .closeSubchannels(let subchannel1, let subchannel2): self.event.continuation.yield(.connectivityStateChanged(.shutdown)) @@ -265,7 +266,7 @@ extension PickFirstLoadBalancer { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension PickFirstLoadBalancer { enum State: Sendable { case active(Active) @@ -278,7 +279,7 @@ extension PickFirstLoadBalancer { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension PickFirstLoadBalancer.State { struct Active: Sendable { var endpoint: Endpoint? @@ -307,7 +308,7 @@ extension PickFirstLoadBalancer.State { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension PickFirstLoadBalancer.State.Active { mutating func updateEndpoint( _ endpoint: Endpoint, @@ -470,7 +471,7 @@ extension PickFirstLoadBalancer.State.Active { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension PickFirstLoadBalancer.State.Closing { mutating func updateSubchannelConnectivityState( _ connectivityState: ConnectivityState, @@ -511,7 +512,7 @@ extension PickFirstLoadBalancer.State.Closing { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension PickFirstLoadBalancer.State { enum OnUpdateEndpoint { case connect(Subchannel, close: Subchannel?) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift index a6a8418b0..5c0709175 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift @@ -15,6 +15,7 @@ */ package import GRPCCore +private import NIOConcurrencyHelpers /// A load-balancer which maintains to a set of subchannels and uses round-robin to pick a /// subchannel when picking a subchannel to use. @@ -57,8 +58,8 @@ package import GRPCCore /// } /// } /// ``` -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -package struct RoundRobinLoadBalancer { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package final class RoundRobinLoadBalancer: Sendable { enum Input: Sendable, Hashable { /// Update the addresses used by the load balancer to the following endpoints. case updateAddresses([Endpoint]) @@ -102,8 +103,9 @@ package struct RoundRobinLoadBalancer { /// Inputs which this load balancer should react to. private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) + // Uses NIOLockedValueBox to workaround: https://github.com/swiftlang/swift/issues/76007 /// The state of the load balancer. - private let state: LockedValueBox + private let state: NIOLockedValueBox /// A connector, capable of creating connections. private let connector: any HTTP2Connector @@ -134,7 +136,7 @@ package struct RoundRobinLoadBalancer { self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) self.input = AsyncStream.makeStream(of: Input.self) - self.state = LockedValueBox(.active(State.Active())) + self.state = NIOLockedValueBox(.active(State.Active())) // The load balancer starts in the idle state. self.event.continuation.yield(.connectivityStateChanged(.idle)) @@ -196,7 +198,7 @@ package struct RoundRobinLoadBalancer { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RoundRobinLoadBalancer { /// Handles an update in endpoints. /// @@ -338,7 +340,7 @@ extension RoundRobinLoadBalancer { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RoundRobinLoadBalancer { private enum State { case active(Active) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift index cd1b197b6..64f53e305 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift @@ -15,7 +15,7 @@ */ package import GRPCCore -internal import NIOConcurrencyHelpers +private import Synchronization /// A ``Subchannel`` provides communication to a single ``Endpoint``. /// @@ -43,8 +43,8 @@ internal import NIOConcurrencyHelpers /// } /// } /// ``` -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -package struct Subchannel { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package final class Subchannel: Sendable { package enum Event: Sendable, Hashable { /// The connection received a GOAWAY and will close soon. No new streams /// should be opened on this connection. @@ -73,7 +73,7 @@ package struct Subchannel { private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) /// The state of the subchannel. - private let state: NIOLockedValueBox + private let state: Mutex /// The endpoint this subchannel is targeting. let endpoint: Endpoint @@ -103,7 +103,7 @@ package struct Subchannel { ) { assert(!endpoint.addresses.isEmpty, "endpoint.addresses mustn't be empty") - self.state = NIOLockedValueBox(.notConnected(.initial)) + self.state = Mutex(.notConnected(.initial)) self.endpoint = endpoint self.id = id self.connector = connector @@ -117,7 +117,7 @@ package struct Subchannel { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Subchannel { /// A stream of events which can happen to the subchannel. package var events: AsyncStream { @@ -173,7 +173,7 @@ extension Subchannel { descriptor: MethodDescriptor, options: CallOptions ) async throws -> Connection.Stream { - let connection: Connection? = self.state.withLockedValue { state in + let connection: Connection? = self.state.withLock { state in switch state { case .notConnected, .connecting, .goingAway, .shuttingDown, .shutDown: return nil @@ -190,10 +190,10 @@ extension Subchannel { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Subchannel { private func handleConnectInput(in group: inout DiscardingTaskGroup) { - let connection = self.state.withLockedValue { state in + let connection = self.state.withLock { state in state.makeConnection( to: self.endpoint.addresses, using: self.connector, @@ -214,7 +214,7 @@ extension Subchannel { } private func handleBackedOffInput(in group: inout DiscardingTaskGroup) { - switch self.state.withLockedValue({ $0.backedOff() }) { + switch self.state.withLock({ $0.backedOff() }) { case .none: () @@ -230,7 +230,7 @@ extension Subchannel { } private func handleShutDownInput(in group: inout DiscardingTaskGroup) { - switch self.state.withLockedValue({ $0.shutDown() }) { + switch self.state.withLock({ $0.shutDown() }) { case .none: () @@ -269,7 +269,7 @@ extension Subchannel { } private func handleConnectSucceededEvent() { - switch self.state.withLockedValue({ $0.connectSucceeded() }) { + switch self.state.withLock({ $0.connectSucceeded() }) { case .updateStateToReady: // Emit a connectivity state change: the load balancer can now use this subchannel. self.event.continuation.yield(.connectivityStateChanged(.ready)) @@ -286,7 +286,7 @@ extension Subchannel { } private func handleConnectFailedEvent(in group: inout DiscardingTaskGroup) { - let onConnectFailed = self.state.withLockedValue { $0.connectFailed(connector: self.connector) } + let onConnectFailed = self.state.withLock { $0.connectFailed(connector: self.connector) } switch onConnectFailed { case .connect(let connection): // Try the next address. @@ -316,7 +316,7 @@ extension Subchannel { } private func handleGoingAwayEvent() { - let isGoingAway = self.state.withLockedValue { $0.goingAway() } + let isGoingAway = self.state.withLock { $0.goingAway() } guard isGoingAway else { return } // Notify the load balancer that the subchannel is going away to stop it from being used. @@ -330,7 +330,7 @@ extension Subchannel { _ reason: Connection.CloseReason, in group: inout DiscardingTaskGroup ) { - switch self.state.withLockedValue({ $0.closed(reason: reason) }) { + switch self.state.withLock({ $0.closed(reason: reason) }) { case .nothing: () @@ -368,7 +368,7 @@ extension Subchannel { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Subchannel { /// ┌───────────────┐ /// ┌───────▶│ NOT CONNECTED │───────────shutDown─────────────┐ diff --git a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift index 7d4876eea..b935e4a05 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift @@ -16,7 +16,7 @@ internal import DequeModule -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RequestQueue { typealias Continuation = CheckedContinuation diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 5d29fe2aa..006434bbe 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -20,6 +20,7 @@ internal import NIOCore internal import NIOExtras internal import NIOHTTP2 public import NIOPosix // has to be public because of default argument value in init +private import Synchronization #if canImport(NIOSSL) import NIOSSL @@ -28,7 +29,7 @@ import NIOSSL extension HTTP2ServerTransport { /// A NIOPosix-backed implementation of a server transport. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct Posix: ServerTransport, ListeningServerTransport { + public final class Posix: ServerTransport, ListeningServerTransport { private let address: GRPCHTTP2Core.SocketAddress private let config: Config private let eventLoopGroup: MultiThreadedEventLoopGroup @@ -129,7 +130,7 @@ extension HTTP2ServerTransport { } } - private let listeningAddressState: LockedValueBox + private let listeningAddressState: Mutex /// The listening address for this server transport. /// @@ -141,7 +142,7 @@ extension HTTP2ServerTransport { public var listeningAddress: GRPCHTTP2Core.SocketAddress { get async throws { try await self.listeningAddressState - .withLockedValue { try $0.listeningAddressFuture } + .withLock { try $0.listeningAddressFuture } .get() } } @@ -163,14 +164,14 @@ extension HTTP2ServerTransport { self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) let eventLoop = eventLoopGroup.any() - self.listeningAddressState = LockedValueBox(.idle(eventLoop.makePromise())) + self.listeningAddressState = Mutex(.idle(eventLoop.makePromise())) } public func listen( _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { defer { - switch self.listeningAddressState.withLockedValue({ $0.close() }) { + switch self.listeningAddressState.withLock({ $0.close() }) { case .failPromise(let promise, let error): promise.fail(error) case .doNothing: @@ -240,7 +241,7 @@ extension HTTP2ServerTransport { } } - let action = self.listeningAddressState.withLockedValue { + let action = self.listeningAddressState.withLock { $0.addressBound( serverChannel.channel.localAddress, userProvidedAddress: self.address diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index f2b4c44c3..77374f54b 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -23,10 +23,12 @@ internal import NIOCore internal import NIOExtras internal import NIOHTTP2 +private import Synchronization + extension HTTP2ServerTransport { /// A NIO Transport Services-backed implementation of a server transport. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct TransportServices: ServerTransport, ListeningServerTransport { + public final class TransportServices: ServerTransport, ListeningServerTransport { private let address: GRPCHTTP2Core.SocketAddress private let config: Config private let eventLoopGroup: NIOTSEventLoopGroup @@ -120,7 +122,7 @@ extension HTTP2ServerTransport { } } - private let listeningAddressState: LockedValueBox + private let listeningAddressState: Mutex /// The listening address for this server transport. /// @@ -132,7 +134,7 @@ extension HTTP2ServerTransport { public var listeningAddress: GRPCHTTP2Core.SocketAddress { get async throws { try await self.listeningAddressState - .withLockedValue { try $0.listeningAddressFuture } + .withLock { try $0.listeningAddressFuture } .get() } } @@ -154,14 +156,14 @@ extension HTTP2ServerTransport { self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) let eventLoop = eventLoopGroup.any() - self.listeningAddressState = LockedValueBox(.idle(eventLoop.makePromise())) + self.listeningAddressState = Mutex(.idle(eventLoop.makePromise())) } public func listen( _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { defer { - switch self.listeningAddressState.withLockedValue({ $0.close() }) { + switch self.listeningAddressState.withLock({ $0.close() }) { case .failPromise(let promise, let error): promise.fail(error) case .doNothing: @@ -194,7 +196,7 @@ extension HTTP2ServerTransport { } } - let action = self.listeningAddressState.withLockedValue { + let action = self.listeningAddressState.withLock { $0.addressBound(serverChannel.channel.localAddress) } switch action { diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index 4759ec449..03aa409ba 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -15,6 +15,7 @@ */ public import GRPCCore +private import Synchronization /// An in-process implementation of a ``ClientTransport``. /// @@ -34,7 +35,7 @@ public import GRPCCore /// /// - SeeAlso: ``ClientTransport`` @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct InProcessClientTransport: ClientTransport { +public final class InProcessClientTransport: ClientTransport { private enum State: Sendable { struct UnconnectedState { var serverTransport: InProcessServerTransport @@ -101,7 +102,7 @@ public struct InProcessClientTransport: ClientTransport { public let retryThrottle: RetryThrottle? private let methodConfig: MethodConfigs - private let state: LockedValueBox + private let state: Mutex /// Creates a new in-process client transport. /// @@ -114,7 +115,7 @@ public struct InProcessClientTransport: ClientTransport { ) { self.retryThrottle = serviceConfig.retryThrottling.map { RetryThrottle(policy: $0) } self.methodConfig = MethodConfigs(serviceConfig: serviceConfig) - self.state = LockedValueBox(.unconnected(.init(serverTransport: server))) + self.state = Mutex(.unconnected(.init(serverTransport: server))) } /// Establish and maintain a connection to the remote destination. @@ -129,7 +130,7 @@ public struct InProcessClientTransport: ClientTransport { /// task this function runs in. public func connect() async throws { let (stream, continuation) = AsyncStream.makeStream() - try self.state.withLockedValue { state in + try self.state.withLock { state in switch state { case .unconnected(let unconnectedState): state = .connected( @@ -164,7 +165,7 @@ public struct InProcessClientTransport: ClientTransport { // If at this point there are any open streams, it's because Cancellation // occurred and all open streams must now be closed. - let openStreams = self.state.withLockedValue { state in + let openStreams = self.state.withLock { state in switch state { case .unconnected: // We have transitioned to connected, and we can't transition back. @@ -190,7 +191,7 @@ public struct InProcessClientTransport: ClientTransport { /// /// If you want to forcefully cancel all active streams then cancel the task running ``connect()``. public func close() { - let maybeContinuation: AsyncStream.Continuation? = self.state.withLockedValue { state in + let maybeContinuation: AsyncStream.Continuation? = self.state.withLock { state in switch state { case .unconnected: state = .closed(.init()) @@ -246,7 +247,7 @@ public struct InProcessClientTransport: ClientTransport { outbound: RPCWriter.Closable(wrapping: response.continuation) ) - let waitForConnectionStream: AsyncStream? = self.state.withLockedValue { state in + let waitForConnectionStream: AsyncStream? = self.state.withLock { state in if case .unconnected(var unconnectedState) = state { let (stream, continuation) = AsyncStream.makeStream() unconnectedState.pendingStreams.append(continuation) @@ -264,7 +265,7 @@ public struct InProcessClientTransport: ClientTransport { try Task.checkCancellation() } - let streamID = try self.state.withLockedValue { state in + let streamID = try self.state.withLock { state in switch state { case .unconnected: // The state cannot be unconnected because if it was, then the above @@ -305,7 +306,7 @@ public struct InProcessClientTransport: ClientTransport { defer { clientStream.outbound.finish() - let maybeEndContinuation = self.state.withLockedValue { state in + let maybeEndContinuation = self.state.withLock { state in switch state { case .unconnected: // The state cannot be unconnected at this point, because if we made diff --git a/Sources/Services/Health/HealthService.swift b/Sources/Services/Health/HealthService.swift index 8e1cbb92d..72f70e064 100644 --- a/Sources/Services/Health/HealthService.swift +++ b/Sources/Services/Health/HealthService.swift @@ -15,6 +15,7 @@ */ internal import GRPCCore +private import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { @@ -67,21 +68,21 @@ internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HealthService { - private struct State: Sendable { + private final class State: Sendable { // The state of each service keyed by the fully qualified service name. - private let lockedStorage = LockedValueBox([String: ServiceState]()) + private let lockedStorage = Mutex([String: ServiceState]()) fileprivate func currentStatus( ofService service: String ) -> Grpc_Health_V1_HealthCheckResponse.ServingStatus? { - return self.lockedStorage.withLockedValue { $0[service]?.currentStatus } + return self.lockedStorage.withLock { $0[service]?.currentStatus } } fileprivate func updateStatus( _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, forService service: String ) { - self.lockedStorage.withLockedValue { storage in + self.lockedStorage.withLock { storage in storage[service, default: ServiceState(status: status)].updateStatus(status) } } @@ -90,7 +91,7 @@ extension HealthService { _ continuation: AsyncStream.Continuation, forService service: String ) { - self.lockedStorage.withLockedValue { storage in + self.lockedStorage.withLock { storage in storage[service, default: ServiceState(status: .serviceUnknown)] .addContinuation(continuation) } diff --git a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift index 2d6ea9f19..34f283345 100644 --- a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift +++ b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCCore +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class RetryThrottleTests: XCTestCase { func testThrottleOnInit() { let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift index 4a72424aa..171e886ac 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift @@ -19,15 +19,15 @@ import GRPCHTTP2Core // Equatable conformance for these types is 'best effort', this is sufficient for testing but not // for general use. -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Connection.Event: Equatable {} -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Connection.CloseReason: Equatable {} extension ClientConnectionEvent: Equatable {} extension ClientConnectionEvent.CloseReason: Equatable {} -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Connection.Event { package static func == (lhs: Connection.Event, rhs: Connection.Event) -> Bool { switch (lhs, rhs) { @@ -47,7 +47,7 @@ extension Connection.Event { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Connection.CloseReason { package static func == (lhs: Connection.CloseReason, rhs: Connection.CloseReason) -> Bool { switch (lhs, rhs) { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift index 1f38b5634..bcc6d86ed 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift @@ -17,14 +17,13 @@ import DequeModule import GRPCCore import GRPCHTTP2Core -import NIOConcurrencyHelpers import NIOCore import NIOHPACK import NIOHTTP2 import NIOPosix import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ConnectionTests: XCTestCase { func testConnectThenClose() async throws { try await ConnectionTest.run(connector: .posix()) { context, event in diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift index 9971e92de..571f38436 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift @@ -17,7 +17,7 @@ import GRPCHTTP2Core import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) enum LoadBalancerTest { struct Context { let servers: [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)] @@ -163,7 +163,7 @@ enum LoadBalancerTest { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension LoadBalancerTest.Context { var roundRobin: RoundRobinLoadBalancer? { switch self.loadBalancer { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift index 48b8e9e5c..cc26898dd 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift @@ -21,7 +21,7 @@ import NIOHTTP2 import NIOPosix import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class PickFirstLoadBalancerTests: XCTestCase { func testPickFirstConnectsToServer() async throws { try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift index 9b155ef95..db15b52c8 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift @@ -21,7 +21,7 @@ import NIOHTTP2 import NIOPosix import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class RoundRobinLoadBalancerTests: XCTestCase { func testMultipleConnectionsAreEstablished() async throws { try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift index d8c2bf41c..b0456aee0 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift @@ -21,7 +21,7 @@ import NIOHTTP2 import NIOPosix import XCTest -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class SubchannelTests: XCTestCase { func testMakeStreamOnIdleSubchannel() async throws { let subchannel = self.makeSubchannel( diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift index cacf22708..31b046571 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift @@ -15,11 +15,12 @@ */ import GRPCCore +import Synchronization import XCTest @testable import GRPCHTTP2Core -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class RequestQueueTests: XCTestCase { struct AnErrorToAvoidALeak: Error {} @@ -45,7 +46,7 @@ final class RequestQueueTests: XCTestCase { func testPopFirstMultiple() async { await withTaskGroup(of: QueueEntryID.self) { group in - let queue = LockedValueBox(RequestQueue()) + let queue = SharedRequestQueue() let signal1 = AsyncStream.makeStream(of: Void.self) let signal2 = AsyncStream.makeStream(of: Void.self) @@ -54,7 +55,7 @@ final class RequestQueueTests: XCTestCase { group.addTask { _ = try? await withCheckedThrowingContinuation { continuation in - queue.withLockedValue { + queue.withQueue { $0.append(continuation: continuation, waitForReady: false, id: id1) } @@ -70,7 +71,7 @@ final class RequestQueueTests: XCTestCase { for await _ in signal1.stream {} _ = try? await withCheckedThrowingContinuation { continuation in - queue.withLockedValue { + queue.withQueue { $0.append(continuation: continuation, waitForReady: false, id: id2) } @@ -85,7 +86,7 @@ final class RequestQueueTests: XCTestCase { for await _ in signal2.stream {} for id in [id1, id2] { - let continuation = queue.withLockedValue { $0.popFirst() } + let continuation = queue.withQueue { $0.popFirst() } continuation?.resume(throwing: AnErrorToAvoidALeak()) let actual = await group.next() XCTAssertEqual(id, actual) @@ -110,7 +111,7 @@ final class RequestQueueTests: XCTestCase { func testRemoveEntryByIDMultiple() async { await withTaskGroup(of: QueueEntryID.self) { group in - let queue = LockedValueBox(RequestQueue()) + let queue = SharedRequestQueue() let signal1 = AsyncStream.makeStream(of: Void.self) let signal2 = AsyncStream.makeStream(of: Void.self) @@ -119,7 +120,7 @@ final class RequestQueueTests: XCTestCase { group.addTask { _ = try? await withCheckedThrowingContinuation { continuation in - queue.withLockedValue { + queue.withQueue { $0.append(continuation: continuation, waitForReady: false, id: id1) } @@ -135,7 +136,7 @@ final class RequestQueueTests: XCTestCase { for await _ in signal1.stream {} _ = try? await withCheckedThrowingContinuation { continuation in - queue.withLockedValue { + queue.withQueue { $0.append(continuation: continuation, waitForReady: false, id: id2) } @@ -150,7 +151,7 @@ final class RequestQueueTests: XCTestCase { for await _ in signal2.stream {} for id in [id1, id2] { - let continuation = queue.withLockedValue { $0.removeEntry(withID: id) } + let continuation = queue.withQueue { $0.removeEntry(withID: id) } continuation?.resume(throwing: AnErrorToAvoidALeak()) let actual = await group.next() XCTAssertEqual(id, actual) @@ -159,7 +160,7 @@ final class RequestQueueTests: XCTestCase { } func testRemoveFastFailingEntries() async throws { - let queue = LockedValueBox(RequestQueue()) + let queue = SharedRequestQueue() let enqueued = AsyncStream.makeStream(of: Void.self) try await withThrowingTaskGroup(of: Void.self) { group in @@ -177,7 +178,7 @@ final class RequestQueueTests: XCTestCase { group.addTask { do { _ = try await withCheckedThrowingContinuation { continuation in - queue.withLockedValue { + queue.withQueue { $0.append(continuation: continuation, waitForReady: waitForReady, id: id) } enqueued.continuation.yield() @@ -199,7 +200,7 @@ final class RequestQueueTests: XCTestCase { } // Remove all fast-failing continuations. - let continuations = queue.withLockedValue { + let continuations = queue.withQueue { $0.removeFastFailingEntries() } @@ -208,13 +209,13 @@ final class RequestQueueTests: XCTestCase { } for id in failFastIDs { - queue.withLockedValue { + queue.withQueue { XCTAssertNil($0.removeEntry(withID: id)) } } for id in waitForReadyIDs { - let maybeContinuation = queue.withLockedValue { $0.removeEntry(withID: id) } + let maybeContinuation = queue.withQueue { $0.removeEntry(withID: id) } let continuation = try XCTUnwrap(maybeContinuation) continuation.resume(throwing: AnErrorToAvoidALeak()) } @@ -222,14 +223,14 @@ final class RequestQueueTests: XCTestCase { } func testRemoveAll() async throws { - let queue = LockedValueBox(RequestQueue()) + let queue = SharedRequestQueue() let enqueued = AsyncStream.makeStream(of: Void.self) await withThrowingTaskGroup(of: Void.self) { group in for _ in 0 ..< 10 { group.addTask { _ = try await withCheckedThrowingContinuation { continuation in - queue.withLockedValue { + queue.withQueue { $0.append(continuation: continuation, waitForReady: false, id: QueueEntryID()) } @@ -247,13 +248,27 @@ final class RequestQueueTests: XCTestCase { } } - let continuations = queue.withLockedValue { $0.removeAll() } + let continuations = queue.withQueue { $0.removeAll() } XCTAssertEqual(continuations.count, 10) - XCTAssertNil(queue.withLockedValue { $0.popFirst() }) + XCTAssertNil(queue.withQueue { $0.popFirst() }) for continuation in continuations { continuation.resume(throwing: AnErrorToAvoidALeak()) } } } + + final class SharedRequestQueue: Sendable { + private let protectedQueue: Mutex + + init() { + self.protectedQueue = Mutex(RequestQueue()) + } + + func withQueue(_ body: @Sendable (inout RequestQueue) throws -> T) rethrows -> T { + try self.protectedQueue.withLock { + try body(&$0) + } + } + } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift index d42fccdb2..6a237db40 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -21,7 +21,7 @@ import NIOCore import NIOHTTP2 import NIOPosix -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) enum ConnectionTest { struct Context { var server: Server @@ -61,7 +61,7 @@ enum ConnectionTest { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ConnectionTest { /// A server which only expected to accept a single connection. final class Server { @@ -142,7 +142,7 @@ extension ConnectionTest { } } -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ConnectionTest { /// Succeeds a promise when a SETTINGS frame ack has been read. private final class SucceedOnSettingsAck: ChannelInboundHandler { diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift index b59c4be5e..4063a06e9 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -15,31 +15,31 @@ */ import GRPCCore -import NIOConcurrencyHelpers import NIOCore import NIOHTTP2 import NIOPosix +import Synchronization import XCTest @testable import GRPCHTTP2Core -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class TestServer: Sendable { private let eventLoopGroup: any EventLoopGroup private typealias Stream = NIOAsyncChannel private typealias Multiplexer = NIOHTTP2AsyncSequence - private let connected: NIOLockedValueBox<[any Channel]> + private let connected: Mutex<[any Channel]> typealias Inbound = NIOAsyncChannelInboundStream typealias Outbound = NIOAsyncChannelOutboundWriter - private let server: NIOLockedValueBox?> + private let server: Mutex?> init(eventLoopGroup: any EventLoopGroup) { self.eventLoopGroup = eventLoopGroup - self.server = NIOLockedValueBox(nil) - self.connected = NIOLockedValueBox([]) + self.server = Mutex(nil) + self.connected = Mutex([]) } enum Target { @@ -48,20 +48,20 @@ final class TestServer: Sendable { } var clients: [any Channel] { - return self.connected.withLockedValue { $0 } + return self.connected.withLock { $0 } } func bind(to target: Target = .localhost) async throws -> GRPCHTTP2Core.SocketAddress { - precondition(self.server.withLockedValue { $0 } == nil) + precondition(self.server.withLock { $0 } == nil) @Sendable func configure(_ channel: any Channel) -> EventLoopFuture { - self.connected.withLockedValue { + self.connected.withLock { $0.append(channel) } channel.closeFuture.whenSuccess { - self.connected.withLockedValue { connected in + self.connected.withLock { connected in guard let index = connected.firstIndex(where: { $0 === channel }) else { return } connected.remove(at: index) } @@ -112,12 +112,12 @@ final class TestServer: Sendable { address = .unixDomainSocket(path: server.channel.localAddress!.pathname!) } - self.server.withLockedValue { $0 = server } + self.server.withLock { $0 = server } return address } func run(_ handle: @Sendable @escaping (Inbound, Outbound) async throws -> Void) async throws { - guard let server = self.server.withLockedValue({ $0 }) else { + guard let server = self.server.withLock({ $0 }) else { fatalError("bind() must be called first") } @@ -145,7 +145,7 @@ final class TestServer: Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension TestServer { enum RunHandler { case echo diff --git a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift index 77726347d..65f9013c5 100644 --- a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift @@ -18,30 +18,30 @@ import Atomics import GRPCCore import GRPCHTTP2Core import NIOEmbedded +import Synchronization import XCTest +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal final class TimerTests: XCTestCase { func testScheduleOneOffTimer() { let loop = EmbeddedEventLoop() defer { try! loop.close() } - let value = LockedValueBox(0) + let value = Atomic(0) var timer = Timer(delay: .seconds(1), repeat: false) timer.schedule(on: loop) { - value.withLockedValue { - XCTAssertEqual($0, 0) - $0 += 1 - } + let (old, _) = value.add(1, ordering: .releasing) + XCTAssertEqual(old, 0) } loop.advanceTime(by: .milliseconds(999)) - XCTAssertEqual(value.withLockedValue { $0 }, 0) + XCTAssertEqual(value.load(ordering: .acquiring), 0) loop.advanceTime(by: .milliseconds(1)) - XCTAssertEqual(value.withLockedValue { $0 }, 1) + XCTAssertEqual(value.load(ordering: .acquiring), 1) // Run again to make sure the task wasn't repeated. loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(value.withLockedValue { $0 }, 1) + XCTAssertEqual(value.load(ordering: .acquiring), 1) } func testCancelOneOffTimer() { @@ -62,25 +62,25 @@ internal final class TimerTests: XCTestCase { let loop = EmbeddedEventLoop() defer { try! loop.close() } - let values = LockedValueBox([Int]()) + let value = Atomic(0) var timer = Timer(delay: .seconds(1), repeat: true) timer.schedule(on: loop) { - values.withLockedValue { $0.append($0.count) } + value.add(1, ordering: .releasing) } loop.advanceTime(by: .milliseconds(999)) - XCTAssertEqual(values.withLockedValue { $0 }, []) + XCTAssertEqual(value.load(ordering: .acquiring), 0) loop.advanceTime(by: .milliseconds(1)) - XCTAssertEqual(values.withLockedValue { $0 }, [0]) + XCTAssertEqual(value.load(ordering: .acquiring), 1) loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(values.withLockedValue { $0 }, [0, 1]) + XCTAssertEqual(value.load(ordering: .acquiring), 2) loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(values.withLockedValue { $0 }, [0, 1, 2]) + XCTAssertEqual(value.load(ordering: .acquiring), 3) timer.cancel() loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(values.withLockedValue { $0 }, [0, 1, 2]) + XCTAssertEqual(value.load(ordering: .acquiring), 3) } func testCancelRepeatedTimer() { diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index cd4475137..c9af595c1 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -39,20 +39,17 @@ final class InProcessServerTransportTests: XCTestCase { outbound: RPCWriter.Closable(wrapping: outbound.continuation) ) - let messages = LockedValueBox<[RPCRequestPart]?>(nil) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await transport.listen { stream in let partValue = try? await stream.inbound.reduce(into: []) { $0.append($1) } - messages.withLockedValue { $0 = partValue } + XCTAssertEqual(partValue, [.message([42])]) transport.stopListening() } } try transport.acceptStream(stream) } - - XCTAssertEqual(messages.withLockedValue { $0 }, [.message([42])]) } func testStopListening() async throws { From 63168d007b52d8d5d7493af3f5feb4cf8cb7c801 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 2 Sep 2024 15:55:20 +0100 Subject: [PATCH 448/580] Add the route-guide example for v2 (#2028) Motivation: Examples help illustrate how to use gRPC. The route guide example is also the base for one of the "standard" gRPC tutorials. Modifications: - Add a route-guide example for v2 Result: More examples. --- .../Generated/route_guide.grpc.swift | 523 +++++++++++++ .../Generated/route_guide.pb.swift | 387 ++++++++++ Examples/v2/route-guide/RouteGuide.swift | 26 + .../route-guide/Subcommands/GetFeature.swift | 69 ++ .../Subcommands/ListFeatures.swift | 86 +++ .../route-guide/Subcommands/RecordRoute.swift | 74 ++ .../route-guide/Subcommands/RouteChat.swift | 74 ++ .../v2/route-guide/Subcommands/Serve.swift | 246 ++++++ Examples/v2/route-guide/route_guide_db.json | 702 ++++++++++++++++++ Package@swift-6.swift | 24 + Protos/generate.sh | 13 +- 11 files changed, 2222 insertions(+), 2 deletions(-) create mode 100644 Examples/v2/route-guide/Generated/route_guide.grpc.swift create mode 100644 Examples/v2/route-guide/Generated/route_guide.pb.swift create mode 100644 Examples/v2/route-guide/RouteGuide.swift create mode 100644 Examples/v2/route-guide/Subcommands/GetFeature.swift create mode 100644 Examples/v2/route-guide/Subcommands/ListFeatures.swift create mode 100644 Examples/v2/route-guide/Subcommands/RecordRoute.swift create mode 100644 Examples/v2/route-guide/Subcommands/RouteChat.swift create mode 100644 Examples/v2/route-guide/Subcommands/Serve.swift create mode 100644 Examples/v2/route-guide/route_guide_db.json diff --git a/Examples/v2/route-guide/Generated/route_guide.grpc.swift b/Examples/v2/route-guide/Generated/route_guide.grpc.swift new file mode 100644 index 000000000..e2fd21a5f --- /dev/null +++ b/Examples/v2/route-guide/Generated/route_guide.grpc.swift @@ -0,0 +1,523 @@ +// Copyright 2015 gRPC authors. +// +// 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. + +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. +// Source: route_guide.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/grpc/grpc-swift + +internal import GRPCCore +internal import GRPCProtobuf + +internal enum Routeguide_RouteGuide { + internal static let descriptor = GRPCCore.ServiceDescriptor.routeguide_RouteGuide + internal enum Method { + internal enum GetFeature { + internal typealias Input = Routeguide_Point + internal typealias Output = Routeguide_Feature + internal static let descriptor = GRPCCore.MethodDescriptor( + service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + method: "GetFeature" + ) + } + internal enum ListFeatures { + internal typealias Input = Routeguide_Rectangle + internal typealias Output = Routeguide_Feature + internal static let descriptor = GRPCCore.MethodDescriptor( + service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + method: "ListFeatures" + ) + } + internal enum RecordRoute { + internal typealias Input = Routeguide_Point + internal typealias Output = Routeguide_RouteSummary + internal static let descriptor = GRPCCore.MethodDescriptor( + service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + method: "RecordRoute" + ) + } + internal enum RouteChat { + internal typealias Input = Routeguide_RouteNote + internal typealias Output = Routeguide_RouteNote + internal static let descriptor = GRPCCore.MethodDescriptor( + service: Routeguide_RouteGuide.descriptor.fullyQualifiedService, + method: "RouteChat" + ) + } + internal static let descriptors: [GRPCCore.MethodDescriptor] = [ + GetFeature.descriptor, + ListFeatures.descriptor, + RecordRoute.descriptor, + RouteChat.descriptor + ] + } + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias StreamingServiceProtocol = Routeguide_RouteGuideStreamingServiceProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias ServiceProtocol = Routeguide_RouteGuideServiceProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias ClientProtocol = Routeguide_RouteGuideClientProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal typealias Client = Routeguide_RouteGuideClient +} + +extension GRPCCore.ServiceDescriptor { + internal static let routeguide_RouteGuide = Self( + package: "routeguide", + service: "RouteGuide" + ) +} + +/// Interface exported by the server. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal protocol Routeguide_RouteGuideStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// A simple RPC. + /// + /// Obtains the feature at a given position. + /// + /// A feature with an empty name is returned if there's no feature at the given + /// position. + func getFeature(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + + /// A server-to-client streaming RPC. + /// + /// Obtains the Features available within the given Rectangle. Results are + /// streamed rather than returned at once (e.g. in a response message with a + /// repeated field), as the rectangle may cover a large area and contain a + /// huge number of features. + func listFeatures(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + + /// A client-to-server streaming RPC. + /// + /// Accepts a stream of Points on a route being traversed, returning a + /// RouteSummary when traversal is completed. + func recordRoute(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + + /// A Bidirectional streaming RPC. + /// + /// Accepts a stream of RouteNotes sent while a route is being traversed, + /// while receiving other RouteNotes (e.g. from other users). + func routeChat(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream +} + +/// Conformance to `GRPCCore.RegistrableRPCService`. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Routeguide_RouteGuide.StreamingServiceProtocol { + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + internal func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Routeguide_RouteGuide.Method.GetFeature.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request in + try await self.getFeature(request: request) + } + ) + router.registerHandler( + forMethod: Routeguide_RouteGuide.Method.ListFeatures.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request in + try await self.listFeatures(request: request) + } + ) + router.registerHandler( + forMethod: Routeguide_RouteGuide.Method.RecordRoute.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request in + try await self.recordRoute(request: request) + } + ) + router.registerHandler( + forMethod: Routeguide_RouteGuide.Method.RouteChat.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request in + try await self.routeChat(request: request) + } + ) + } +} + +/// Interface exported by the server. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal protocol Routeguide_RouteGuideServiceProtocol: Routeguide_RouteGuide.StreamingServiceProtocol { + /// A simple RPC. + /// + /// Obtains the feature at a given position. + /// + /// A feature with an empty name is returned if there's no feature at the given + /// position. + func getFeature(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + + /// A server-to-client streaming RPC. + /// + /// Obtains the Features available within the given Rectangle. Results are + /// streamed rather than returned at once (e.g. in a response message with a + /// repeated field), as the rectangle may cover a large area and contain a + /// huge number of features. + func listFeatures(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + + /// A client-to-server streaming RPC. + /// + /// Accepts a stream of Points on a route being traversed, returning a + /// RouteSummary when traversal is completed. + func recordRoute(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + + /// A Bidirectional streaming RPC. + /// + /// Accepts a stream of RouteNotes sent while a route is being traversed, + /// while receiving other RouteNotes (e.g. from other users). + func routeChat(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream +} + +/// Partial conformance to `Routeguide_RouteGuideStreamingServiceProtocol`. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Routeguide_RouteGuide.ServiceProtocol { + internal func getFeature(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.getFeature(request: GRPCCore.ServerRequest.Single(stream: request)) + return GRPCCore.ServerResponse.Stream(single: response) + } + + internal func listFeatures(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.listFeatures(request: GRPCCore.ServerRequest.Single(stream: request)) + return response + } + + internal func recordRoute(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.recordRoute(request: request) + return GRPCCore.ServerResponse.Stream(single: response) + } +} + +/// Interface exported by the server. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal protocol Routeguide_RouteGuideClientProtocol: Sendable { + /// A simple RPC. + /// + /// Obtains the feature at a given position. + /// + /// A feature with an empty name is returned if there's no feature at the given + /// position. + func getFeature( + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// A server-to-client streaming RPC. + /// + /// Obtains the Features available within the given Rectangle. Results are + /// streamed rather than returned at once (e.g. in a response message with a + /// repeated field), as the rectangle may cover a large area and contain a + /// huge number of features. + func listFeatures( + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable + + /// A client-to-server streaming RPC. + /// + /// Accepts a stream of Points on a route being traversed, returning a + /// RouteSummary when traversal is completed. + func recordRoute( + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + + /// A Bidirectional streaming RPC. + /// + /// Accepts a stream of RouteNotes sent while a route is being traversed, + /// while receiving other RouteNotes (e.g. from other users). + func routeChat( + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Routeguide_RouteGuide.ClientProtocol { + internal func getFeature( + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + try $0.message + } + ) async throws -> R where R: Sendable { + try await self.getFeature( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + body + ) + } + + internal func listFeatures( + request: GRPCCore.ClientRequest.Single, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.listFeatures( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + body + ) + } + + internal func recordRoute( + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + try $0.message + } + ) async throws -> R where R: Sendable { + try await self.recordRoute( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + body + ) + } + + internal func routeChat( + request: GRPCCore.ClientRequest.Stream, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.routeChat( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + body + ) + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Routeguide_RouteGuide.ClientProtocol { + /// A simple RPC. + /// + /// Obtains the feature at a given position. + /// + /// A feature with an empty name is returned if there's no feature at the given + /// position. + internal func getFeature( + _ message: Routeguide_Point, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.getFeature( + request: request, + options: options, + handleResponse + ) + } + + /// A server-to-client streaming RPC. + /// + /// Obtains the Features available within the given Rectangle. Results are + /// streamed rather than returned at once (e.g. in a response message with a + /// repeated field), as the rectangle may cover a large area and contain a + /// huge number of features. + internal func listFeatures( + _ message: Routeguide_Rectangle, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Single( + message: message, + metadata: metadata + ) + return try await self.listFeatures( + request: request, + options: options, + handleResponse + ) + } + + /// A client-to-server streaming RPC. + /// + /// Accepts a stream of Points on a route being traversed, returning a + /// RouteSummary when traversal is completed. + internal func recordRoute( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + try $0.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.recordRoute( + request: request, + options: options, + handleResponse + ) + } + + /// A Bidirectional streaming RPC. + /// + /// Accepts a stream of RouteNotes sent while a route is being traversed, + /// while receiving other RouteNotes (e.g. from other users). + internal func routeChat( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest.Stream( + metadata: metadata, + producer: requestProducer + ) + return try await self.routeChat( + request: request, + options: options, + handleResponse + ) + } +} + +/// Interface exported by the server. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +internal struct Routeguide_RouteGuideClient: Routeguide_RouteGuide.ClientProtocol { + private let client: GRPCCore.GRPCClient + + internal init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// A simple RPC. + /// + /// Obtains the feature at a given position. + /// + /// A feature with an empty name is returned if there's no feature at the given + /// position. + internal func getFeature( + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + try $0.message + } + ) async throws -> R where R: Sendable { + try await self.client.unary( + request: request, + descriptor: Routeguide_RouteGuide.Method.GetFeature.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + /// A server-to-client streaming RPC. + /// + /// Obtains the Features available within the given Rectangle. Results are + /// streamed rather than returned at once (e.g. in a response message with a + /// repeated field), as the rectangle may cover a large area and contain a + /// huge number of features. + internal func listFeatures( + request: GRPCCore.ClientRequest.Single, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Routeguide_RouteGuide.Method.ListFeatures.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + /// A client-to-server streaming RPC. + /// + /// Accepts a stream of Points on a route being traversed, returning a + /// RouteSummary when traversal is completed. + internal func recordRoute( + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + try $0.message + } + ) async throws -> R where R: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: Routeguide_RouteGuide.Method.RecordRoute.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } + + /// A Bidirectional streaming RPC. + /// + /// Accepts a stream of RouteNotes sent while a route is being traversed, + /// while receiving other RouteNotes (e.g. from other users). + internal func routeChat( + request: GRPCCore.ClientRequest.Stream, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + ) async throws -> R where R: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: Routeguide_RouteGuide.Method.RouteChat.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + handler: body + ) + } +} \ No newline at end of file diff --git a/Examples/v2/route-guide/Generated/route_guide.pb.swift b/Examples/v2/route-guide/Generated/route_guide.pb.swift new file mode 100644 index 000000000..09892ef83 --- /dev/null +++ b/Examples/v2/route-guide/Generated/route_guide.pb.swift @@ -0,0 +1,387 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: route_guide.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +// Copyright 2015 gRPC authors. +// +// 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 SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// Points are represented as latitude-longitude pairs in the E7 representation +/// (degrees multiplied by 10**7 and rounded to the nearest integer). +/// Latitudes should be in the range +/- 90 degrees and longitude should be in +/// the range +/- 180 degrees (inclusive). +struct Routeguide_Point: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var latitude: Int32 = 0 + + var longitude: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// A latitude-longitude rectangle, represented as two diagonally opposite +/// points "lo" and "hi". +struct Routeguide_Rectangle: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// One corner of the rectangle. + var lo: Routeguide_Point { + get {return _lo ?? Routeguide_Point()} + set {_lo = newValue} + } + /// Returns true if `lo` has been explicitly set. + var hasLo: Bool {return self._lo != nil} + /// Clears the value of `lo`. Subsequent reads from it will return its default value. + mutating func clearLo() {self._lo = nil} + + /// The other corner of the rectangle. + var hi: Routeguide_Point { + get {return _hi ?? Routeguide_Point()} + set {_hi = newValue} + } + /// Returns true if `hi` has been explicitly set. + var hasHi: Bool {return self._hi != nil} + /// Clears the value of `hi`. Subsequent reads from it will return its default value. + mutating func clearHi() {self._hi = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _lo: Routeguide_Point? = nil + fileprivate var _hi: Routeguide_Point? = nil +} + +/// A feature names something at a given point. +/// +/// If a feature could not be named, the name is empty. +struct Routeguide_Feature: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The name of the feature. + var name: String = String() + + /// The point where the feature is detected. + var location: Routeguide_Point { + get {return _location ?? Routeguide_Point()} + set {_location = newValue} + } + /// Returns true if `location` has been explicitly set. + var hasLocation: Bool {return self._location != nil} + /// Clears the value of `location`. Subsequent reads from it will return its default value. + mutating func clearLocation() {self._location = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _location: Routeguide_Point? = nil +} + +/// A RouteNote is a message sent while at a given point. +struct Routeguide_RouteNote: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The location from which the message is sent. + var location: Routeguide_Point { + get {return _location ?? Routeguide_Point()} + set {_location = newValue} + } + /// Returns true if `location` has been explicitly set. + var hasLocation: Bool {return self._location != nil} + /// Clears the value of `location`. Subsequent reads from it will return its default value. + mutating func clearLocation() {self._location = nil} + + /// The message to be sent. + var message: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _location: Routeguide_Point? = nil +} + +/// A RouteSummary is received in response to a RecordRoute rpc. +/// +/// It contains the number of individual points received, the number of +/// detected features, and the total distance covered as the cumulative sum of +/// the distance between each point. +struct Routeguide_RouteSummary: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// The number of points received. + var pointCount: Int32 = 0 + + /// The number of known features passed while traversing the route. + var featureCount: Int32 = 0 + + /// The distance covered in metres. + var distance: Int32 = 0 + + /// The duration of the traversal in seconds. + var elapsedTime: Int32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "routeguide" + +extension Routeguide_Point: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Point" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "latitude"), + 2: .same(proto: "longitude"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.latitude) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.longitude) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.latitude != 0 { + try visitor.visitSingularInt32Field(value: self.latitude, fieldNumber: 1) + } + if self.longitude != 0 { + try visitor.visitSingularInt32Field(value: self.longitude, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Routeguide_Point, rhs: Routeguide_Point) -> Bool { + if lhs.latitude != rhs.latitude {return false} + if lhs.longitude != rhs.longitude {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Routeguide_Rectangle: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Rectangle" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "lo"), + 2: .same(proto: "hi"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._lo) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._hi) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._lo { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + try { if let v = self._hi { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Routeguide_Rectangle, rhs: Routeguide_Rectangle) -> Bool { + if lhs._lo != rhs._lo {return false} + if lhs._hi != rhs._hi {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Routeguide_Feature: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Feature" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 2: .same(proto: "location"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._location) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) + } + try { if let v = self._location { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Routeguide_Feature, rhs: Routeguide_Feature) -> Bool { + if lhs.name != rhs.name {return false} + if lhs._location != rhs._location {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Routeguide_RouteNote: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RouteNote" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "location"), + 2: .same(proto: "message"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._location) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._location { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + if !self.message.isEmpty { + try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Routeguide_RouteNote, rhs: Routeguide_RouteNote) -> Bool { + if lhs._location != rhs._location {return false} + if lhs.message != rhs.message {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Routeguide_RouteSummary: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RouteSummary" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "point_count"), + 2: .standard(proto: "feature_count"), + 3: .same(proto: "distance"), + 4: .standard(proto: "elapsed_time"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.pointCount) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.featureCount) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.distance) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.elapsedTime) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.pointCount != 0 { + try visitor.visitSingularInt32Field(value: self.pointCount, fieldNumber: 1) + } + if self.featureCount != 0 { + try visitor.visitSingularInt32Field(value: self.featureCount, fieldNumber: 2) + } + if self.distance != 0 { + try visitor.visitSingularInt32Field(value: self.distance, fieldNumber: 3) + } + if self.elapsedTime != 0 { + try visitor.visitSingularInt32Field(value: self.elapsedTime, fieldNumber: 4) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Routeguide_RouteSummary, rhs: Routeguide_RouteSummary) -> Bool { + if lhs.pointCount != rhs.pointCount {return false} + if lhs.featureCount != rhs.featureCount {return false} + if lhs.distance != rhs.distance {return false} + if lhs.elapsedTime != rhs.elapsedTime {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Examples/v2/route-guide/RouteGuide.swift b/Examples/v2/route-guide/RouteGuide.swift new file mode 100644 index 000000000..d53882726 --- /dev/null +++ b/Examples/v2/route-guide/RouteGuide.swift @@ -0,0 +1,26 @@ +/* + * 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 ArgumentParser + +@main +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct RouteGuide: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "route-guide", + subcommands: [Serve.self, GetFeature.self, ListFeatures.self, RecordRoute.self, RouteChat.self] + ) +} diff --git a/Examples/v2/route-guide/Subcommands/GetFeature.swift b/Examples/v2/route-guide/Subcommands/GetFeature.swift new file mode 100644 index 000000000..a8e56191a --- /dev/null +++ b/Examples/v2/route-guide/Subcommands/GetFeature.swift @@ -0,0 +1,69 @@ +/* + * 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 ArgumentParser +import GRPCHTTP2Transport + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct GetFeature: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Gets a feature at a given location.") + + @Option(help: "The server's listening port") + var port: Int = 31415 + + @Option( + name: [.customLong("latitude"), .customLong("lat")], + help: "Latitude of the feature to get in E7 format (degrees ⨯ 1e7)" + ) + var latitude: Int32 = 407_838_351 + + @Option( + name: [.customLong("longitude"), .customLong("lon")], + help: "Longitude of the feature to get in E7 format (degrees ⨯ 1e7)" + ) + var longitude: Int32 = -746_143_763 + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix( + target: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults() + ) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + + let point = Routeguide_Point.with { + $0.latitude = self.latitude + $0.longitude = self.longitude + } + + let feature = try await routeGuide.getFeature(point) + + if feature.name.isEmpty { + print("No feature found at (\(self.latitude), \(self.longitude))") + } else { + print("Found '\(feature.name)' at (\(self.latitude), \(self.longitude))") + } + + client.close() + } + } +} diff --git a/Examples/v2/route-guide/Subcommands/ListFeatures.swift b/Examples/v2/route-guide/Subcommands/ListFeatures.swift new file mode 100644 index 000000000..4bb33c2c3 --- /dev/null +++ b/Examples/v2/route-guide/Subcommands/ListFeatures.swift @@ -0,0 +1,86 @@ +/* + * 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 ArgumentParser +import GRPCHTTP2Transport + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct ListFeatures: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "List all features within a bounding rectangle." + ) + + @Option(help: "The server's listening port") + var port: Int = 31415 + + @Option( + name: [.customLong("minimum-latitude"), .customLong("min-lat")], + help: "Minimum latitude of the bounding rectangle to search in E7 format." + ) + var minLatitude: Int32 = 400_000_000 + + @Option( + name: [.customLong("maximum-latitude"), .customLong("max-lat")], + help: "Maximum latitude of the bounding rectangle to search in E7 format." + ) + var maxLatitude: Int32 = 420_000_000 + + @Option( + name: [.customLong("minimum-longitude"), .customLong("min-lon")], + help: "Minimum longitude of the bounding rectangle to search in E7 format." + ) + var minLongitude: Int32 = -750_000_000 + + @Option( + name: [.customLong("maximum-longitude"), .customLong("max-lon")], + help: "Maximum longitude of the bounding rectangle to search in E7 format." + ) + var maxLongitude: Int32 = -730_000_000 + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix( + target: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults() + ) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let boundingRectangle = Routeguide_Rectangle.with { + $0.lo.latitude = self.minLatitude + $0.hi.latitude = self.maxLatitude + $0.lo.longitude = self.minLongitude + $0.hi.longitude = self.maxLongitude + } + + try await routeGuide.listFeatures(boundingRectangle) { response in + var count = 0 + for try await feature in response.messages { + count += 1 + let (lat, lon) = (feature.location.latitude, feature.location.longitude) + print("(\(count)) \(feature.name) at (\(lat), \(lon))") + } + } + + client.close() + } + + } +} diff --git a/Examples/v2/route-guide/Subcommands/RecordRoute.swift b/Examples/v2/route-guide/Subcommands/RecordRoute.swift new file mode 100644 index 000000000..2482e842a --- /dev/null +++ b/Examples/v2/route-guide/Subcommands/RecordRoute.swift @@ -0,0 +1,74 @@ +/* + * 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 ArgumentParser +import GRPCHTTP2Transport + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct RecordRoute: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Records a route by visiting N randomly selected points and prints a summary of it." + ) + + @Option(help: "The server's listening port") + var port: Int = 31415 + + @Option(help: "The number of places to visit.") + var points: Int = 10 + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix( + target: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults() + ) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + + // Get all features. + let rectangle = Routeguide_Rectangle.with { + $0.lo.latitude = 400_000_000 + $0.hi.latitude = 420_000_000 + $0.lo.longitude = -750_000_000 + $0.hi.longitude = -730_000_000 + } + let features = try await routeGuide.listFeatures(rectangle) { response in + try await response.messages.reduce(into: []) { $0.append($1) } + } + + // Pick 'N' locations to visit. + let placesToVisit = features.shuffled().map { $0.location }.prefix(self.points) + + // Record a route. + let summary = try await routeGuide.recordRoute { writer in + try await writer.write(contentsOf: placesToVisit) + } + + let text = """ + Visited \(summary.pointCount) points and \(summary.featureCount) features covering \ + a distance \(summary.distance) metres. + """ + print(text) + + client.close() + } + } +} diff --git a/Examples/v2/route-guide/Subcommands/RouteChat.swift b/Examples/v2/route-guide/Subcommands/RouteChat.swift new file mode 100644 index 000000000..55e3be6f9 --- /dev/null +++ b/Examples/v2/route-guide/Subcommands/RouteChat.swift @@ -0,0 +1,74 @@ +/* + * 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 ArgumentParser +import GRPCHTTP2Transport + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct RouteChat: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: """ + Visits a few points and records a note at each, and prints all notes previously recorded at \ + each point. + """ + ) + + @Option(help: "The server's listening port") + var port: Int = 31415 + + func run() async throws { + let transport = try HTTP2ClientTransport.Posix( + target: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults() + ) + let client = GRPCClient(transport: transport) + + try await withThrowingDiscardingTaskGroup { group in + group.addTask { + try await client.run() + } + + let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + + try await routeGuide.routeChat { writer in + let notes: [(String, (Int32, Int32))] = [ + ("First message", (0, 0)), + ("Second message", (0, 1)), + ("Third message", (1, 0)), + ("Fourth message", (0, 0)), + ("Fifth message", (1, 0)), + ] + + for (message, (lat, lon)) in notes { + let note = Routeguide_RouteNote.with { + $0.message = message + $0.location.latitude = lat + $0.location.longitude = lon + } + print("Sending note: '\(message) at (\(lat), \(lon))'") + try await writer.write(note) + } + } onResponse: { response in + for try await note in response.messages { + let (lat, lon) = (note.location.latitude, note.location.longitude) + print("Received note: '\(note.message) at (\(lat), \(lon))'") + } + } + + client.close() + } + } +} diff --git a/Examples/v2/route-guide/Subcommands/Serve.swift b/Examples/v2/route-guide/Subcommands/Serve.swift new file mode 100644 index 000000000..9af509502 --- /dev/null +++ b/Examples/v2/route-guide/Subcommands/Serve.swift @@ -0,0 +1,246 @@ +/* + * 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 ArgumentParser +import Foundation +import GRPCHTTP2Transport +import GRPCProtobuf +import Synchronization + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct Serve: AsyncParsableCommand { + static let configuration = CommandConfiguration(abstract: "Starts a route-guide server.") + + @Option(help: "The port to listen on") + var port: Int = 31415 + + private func loadFeatures() throws -> [Routeguide_Feature] { + guard let url = Bundle.module.url(forResource: "route_guide_db", withExtension: "json") else { + throw RPCError(code: .internalError, message: "Couldn't find 'route_guide_db.json") + } + + let data = try Data(contentsOf: url) + return try Routeguide_Feature.array(fromJSONUTF8Bytes: data) + } + + func run() async throws { + let features = try self.loadFeatures() + let transport = HTTP2ServerTransport.Posix( + address: .ipv4(host: "127.0.0.1", port: self.port), + config: .defaults(transportSecurity: .plaintext) + ) + + let server = GRPCServer(transport: transport, services: [RouteGuideService(features: features)]) + try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await server.run() } + let address = try await transport.listeningAddress + print("server listening on \(address)") + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +struct RouteGuideService { + /// Known features. + private let features: [Routeguide_Feature] + /// Notes recorded by clients. + private let receivedNotes: Notes + + /// A thread-safe store for notes sent by clients. + private final class Notes: Sendable { + private let notes: Mutex<[Routeguide_RouteNote]> + + init() { + self.notes = Mutex([]) + } + + /// Records a note and returns all other notes recorded at the same location. + /// + /// - Parameter receivedNote: A note to record. + /// - Returns: Other notes recorded at the same location. + func recordNote(_ receivedNote: Routeguide_RouteNote) -> [Routeguide_RouteNote] { + return self.notes.withLock { notes in + var notesFromSameLocation: [Routeguide_RouteNote] = [] + for note in notes { + if note.location == receivedNote.location { + notesFromSameLocation.append(note) + } + } + notes.append(receivedNote) + return notesFromSameLocation + } + } + } + + /// Creates a new route guide service. + /// - Parameter features: Known features. + init(features: [Routeguide_Feature]) { + self.features = features + self.receivedNotes = Notes() + } + + /// Returns the first feature found at the given location, if one exists. + private func findFeature(latitude: Int32, longitude: Int32) -> Routeguide_Feature? { + self.features.first { + $0.location.latitude == latitude && $0.location.longitude == longitude + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { + func getFeature( + request: ServerRequest.Single + ) async throws -> ServerResponse.Single { + let feature = self.findFeature( + latitude: request.message.latitude, + longitude: request.message.longitude + ) + + if let feature { + return ServerResponse.Single(message: feature) + } else { + // No feature: return a feature with an empty name. + let unknownFeature = Routeguide_Feature.with { + $0.name = "" + $0.location = .with { + $0.latitude = request.message.latitude + $0.longitude = request.message.longitude + } + } + return ServerResponse.Single(message: unknownFeature) + } + } + + func listFeatures( + request: ServerRequest.Single + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + let featuresWithinBounds = self.features.filter { feature in + !feature.name.isEmpty && feature.isContained(by: request.message) + } + + try await writer.write(contentsOf: featuresWithinBounds) + return [:] + } + } + + func recordRoute( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Single { + let startTime = ContinuousClock.now + var pointsVisited = 0 + var featuresVisited = 0 + var distanceTravelled = 0.0 + var previousPoint: Routeguide_Point? = nil + + for try await point in request.messages { + pointsVisited += 1 + + if self.findFeature(latitude: point.latitude, longitude: point.longitude) != nil { + featuresVisited += 1 + } + + if let previousPoint { + distanceTravelled += greatCircleDistance(from: previousPoint, to: point) + } + + previousPoint = point + } + + let duration = startTime.duration(to: .now) + let summary = Routeguide_RouteSummary.with { + $0.pointCount = Int32(pointsVisited) + $0.featureCount = Int32(featuresVisited) + $0.elapsedTime = Int32(duration.components.seconds) + $0.distance = Int32(distanceTravelled) + } + + return ServerResponse.Single(message: summary) + } + + func routeChat( + request: ServerRequest.Stream + ) async throws -> ServerResponse.Stream { + return ServerResponse.Stream { writer in + for try await note in request.messages { + let notes = self.receivedNotes.recordNote(note) + try await writer.write(contentsOf: notes) + } + return [:] + } + } +} + +extension Routeguide_Feature { + func isContained( + by rectangle: Routeguide_Rectangle + ) -> Bool { + return rectangle.lo.latitude <= self.location.latitude + && self.location.latitude <= rectangle.hi.latitude + && rectangle.lo.longitude <= self.location.longitude + && self.location.longitude <= rectangle.hi.longitude + } +} + +private func greatCircleDistance( + from point1: Routeguide_Point, + to point2: Routeguide_Point +) -> Double { + // See https://en.wikipedia.org/wiki/Great-circle_distance + // + // Let λ1 (lambda1) and φ1 (phi1) be the longitude and latitude of point 1. + // Let λ2 (lambda2) and φ2 (phi2) be the longitude and latitude of point 2. + // + // Let Δλ = λ2 - λ1, and Δφ = φ2 - φ1. + // + // The central angle between them, σc (sigmaC) can be computed as: + // + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + // + // The unit distance (d) between point1 and point2 can then be computed as: + // + // d = 2 ⨯ atan(sqrt(σc), sqrt(1 - σc)) + + let lambda1 = radians(degreesInE7: point1.longitude) + let phi1 = radians(degreesInE7: point1.latitude) + let lambda2 = radians(degreesInE7: point2.longitude) + let phi2 = radians(degreesInE7: point2.latitude) + + // Δλ = λ2 - λ1 + let deltaLambda = lambda2 - lambda1 + // Δφ = φ2 - φ1 + let deltaPhi = phi2 - phi1 + + // sin²(Δφ/2) + let sinHalfDeltaPhiSquared = sin(deltaPhi / 2) * sin(deltaPhi / 2) + // sin²(Δλ/2) + let sinHalfDeltaLambdaSquared = sin(deltaLambda / 2) * sin(deltaLambda / 2) + + // σc = 2 ⨯ sqrt(sin²(Δφ/2) + cos(φ1) ⨯ cos(φ2) ⨯ sin²(Δλ/2)) + let sigmaC = 2 * sqrt(sinHalfDeltaPhiSquared + cos(phi1) * cos(phi2) * sinHalfDeltaLambdaSquared) + + // This is the unit distance, i.e. assumes the circle has a radius of 1. + let unitDistance = 2 * atan2(sqrt(sigmaC), sqrt(1 - sigmaC)) + + // Scale it by the radius of the Earth in meters. + let earthRadius = 6_371_000.0 + return unitDistance * earthRadius +} + +private func radians(degreesInE7 degrees: Int32) -> Double { + return (Double(degrees) / 1e7) * .pi / 180.0 +} diff --git a/Examples/v2/route-guide/route_guide_db.json b/Examples/v2/route-guide/route_guide_db.json new file mode 100644 index 000000000..22e93e313 --- /dev/null +++ b/Examples/v2/route-guide/route_guide_db.json @@ -0,0 +1,702 @@ +[ + { + "location": { + "latitude": 407838351, + "longitude": -746143763 + }, + "name": "Patriots Path, Mendham, NJ 07945, USA" + }, + { + "location": { + "latitude": 408122808, + "longitude": -743999179 + }, + "name": "101 New Jersey 10, Whippany, NJ 07981, USA" + }, + { + "location": { + "latitude": 413628156, + "longitude": -749015468 + }, + "name": "U.S. 6, Shohola, PA 18458, USA" + }, + { + "location": { + "latitude": 419999544, + "longitude": -740371136 + }, + "name": "5 Conners Road, Kingston, NY 12401, USA" + }, + { + "location": { + "latitude": 414008389, + "longitude": -743951297 + }, + "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" + }, + { + "location": { + "latitude": 419611318, + "longitude": -746524769 + }, + "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" + }, + { + "location": { + "latitude": 406109563, + "longitude": -742186778 + }, + "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" + }, + { + "location": { + "latitude": 416802456, + "longitude": -742370183 + }, + "name": "352 South Mountain Road, Wallkill, NY 12589, USA" + }, + { + "location": { + "latitude": 412950425, + "longitude": -741077389 + }, + "name": "Bailey Turn Road, Harriman, NY 10926, USA" + }, + { + "location": { + "latitude": 412144655, + "longitude": -743949739 + }, + "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" + }, + { + "location": { + "latitude": 415736605, + "longitude": -742847522 + }, + "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" + }, + { + "location": { + "latitude": 413843930, + "longitude": -740501726 + }, + "name": "162 Merrill Road, Highland Mills, NY 10930, USA" + }, + { + "location": { + "latitude": 410873075, + "longitude": -744459023 + }, + "name": "Clinton Road, West Milford, NJ 07480, USA" + }, + { + "location": { + "latitude": 412346009, + "longitude": -744026814 + }, + "name": "16 Old Brook Lane, Warwick, NY 10990, USA" + }, + { + "location": { + "latitude": 402948455, + "longitude": -747903913 + }, + "name": "3 Drake Lane, Pennington, NJ 08534, USA" + }, + { + "location": { + "latitude": 406337092, + "longitude": -740122226 + }, + "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" + }, + { + "location": { + "latitude": 406421967, + "longitude": -747727624 + }, + "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" + }, + { + "location": { + "latitude": 416318082, + "longitude": -749677716 + }, + "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" + }, + { + "location": { + "latitude": 415301720, + "longitude": -748416257 + }, + "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" + }, + { + "location": { + "latitude": 402647019, + "longitude": -747071791 + }, + "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" + }, + { + "location": { + "latitude": 412567807, + "longitude": -741058078 + }, + "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" + }, + { + "location": { + "latitude": 416855156, + "longitude": -744420597 + }, + "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" + }, + { + "location": { + "latitude": 404663628, + "longitude": -744820157 + }, + "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" + }, + { + "location": { + "latitude": 407113723, + "longitude": -749746483 + }, + "name": "" + }, + { + "location": { + "latitude": 402133926, + "longitude": -743613249 + }, + "name": "" + }, + { + "location": { + "latitude": 400273442, + "longitude": -741220915 + }, + "name": "" + }, + { + "location": { + "latitude": 411236786, + "longitude": -744070769 + }, + "name": "" + }, + { + "location": { + "latitude": 411633782, + "longitude": -746784970 + }, + "name": "211-225 Plains Road, Augusta, NJ 07822, USA" + }, + { + "location": { + "latitude": 415830701, + "longitude": -742952812 + }, + "name": "" + }, + { + "location": { + "latitude": 413447164, + "longitude": -748712898 + }, + "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" + }, + { + "location": { + "latitude": 405047245, + "longitude": -749800722 + }, + "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" + }, + { + "location": { + "latitude": 418858923, + "longitude": -746156790 + }, + "name": "" + }, + { + "location": { + "latitude": 417951888, + "longitude": -748484944 + }, + "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" + }, + { + "location": { + "latitude": 407033786, + "longitude": -743977337 + }, + "name": "26 East 3rd Street, New Providence, NJ 07974, USA" + }, + { + "location": { + "latitude": 417548014, + "longitude": -740075041 + }, + "name": "" + }, + { + "location": { + "latitude": 410395868, + "longitude": -744972325 + }, + "name": "" + }, + { + "location": { + "latitude": 404615353, + "longitude": -745129803 + }, + "name": "" + }, + { + "location": { + "latitude": 406589790, + "longitude": -743560121 + }, + "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" + }, + { + "location": { + "latitude": 414653148, + "longitude": -740477477 + }, + "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" + }, + { + "location": { + "latitude": 405957808, + "longitude": -743255336 + }, + "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" + }, + { + "location": { + "latitude": 411733589, + "longitude": -741648093 + }, + "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" + }, + { + "location": { + "latitude": 412676291, + "longitude": -742606606 + }, + "name": "1270 Lakes Road, Monroe, NY 10950, USA" + }, + { + "location": { + "latitude": 409224445, + "longitude": -748286738 + }, + "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" + }, + { + "location": { + "latitude": 406523420, + "longitude": -742135517 + }, + "name": "652 Garden Street, Elizabeth, NJ 07202, USA" + }, + { + "location": { + "latitude": 401827388, + "longitude": -740294537 + }, + "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" + }, + { + "location": { + "latitude": 410564152, + "longitude": -743685054 + }, + "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" + }, + { + "location": { + "latitude": 408472324, + "longitude": -740726046 + }, + "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" + }, + { + "location": { + "latitude": 412452168, + "longitude": -740214052 + }, + "name": "5 White Oak Lane, Stony Point, NY 10980, USA" + }, + { + "location": { + "latitude": 409146138, + "longitude": -746188906 + }, + "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" + }, + { + "location": { + "latitude": 404701380, + "longitude": -744781745 + }, + "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" + }, + { + "location": { + "latitude": 409642566, + "longitude": -746017679 + }, + "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" + }, + { + "location": { + "latitude": 408031728, + "longitude": -748645385 + }, + "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" + }, + { + "location": { + "latitude": 413700272, + "longitude": -742135189 + }, + "name": "367 Prospect Road, Chester, NY 10918, USA" + }, + { + "location": { + "latitude": 404310607, + "longitude": -740282632 + }, + "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" + }, + { + "location": { + "latitude": 409319800, + "longitude": -746201391 + }, + "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" + }, + { + "location": { + "latitude": 406685311, + "longitude": -742108603 + }, + "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" + }, + { + "location": { + "latitude": 419018117, + "longitude": -749142781 + }, + "name": "43 Dreher Road, Roscoe, NY 12776, USA" + }, + { + "location": { + "latitude": 412856162, + "longitude": -745148837 + }, + "name": "Swan Street, Pine Island, NY 10969, USA" + }, + { + "location": { + "latitude": 416560744, + "longitude": -746721964 + }, + "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" + }, + { + "location": { + "latitude": 405314270, + "longitude": -749836354 + }, + "name": "" + }, + { + "location": { + "latitude": 414219548, + "longitude": -743327440 + }, + "name": "" + }, + { + "location": { + "latitude": 415534177, + "longitude": -742900616 + }, + "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" + }, + { + "location": { + "latitude": 406898530, + "longitude": -749127080 + }, + "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" + }, + { + "location": { + "latitude": 407586880, + "longitude": -741670168 + }, + "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" + }, + { + "location": { + "latitude": 400106455, + "longitude": -742870190 + }, + "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" + }, + { + "location": { + "latitude": 400066188, + "longitude": -746793294 + }, + "name": "" + }, + { + "location": { + "latitude": 418803880, + "longitude": -744102673 + }, + "name": "40 Mountain Road, Napanoch, NY 12458, USA" + }, + { + "location": { + "latitude": 414204288, + "longitude": -747895140 + }, + "name": "" + }, + { + "location": { + "latitude": 414777405, + "longitude": -740615601 + }, + "name": "" + }, + { + "location": { + "latitude": 415464475, + "longitude": -747175374 + }, + "name": "48 North Road, Forestburgh, NY 12777, USA" + }, + { + "location": { + "latitude": 404062378, + "longitude": -746376177 + }, + "name": "" + }, + { + "location": { + "latitude": 405688272, + "longitude": -749285130 + }, + "name": "" + }, + { + "location": { + "latitude": 400342070, + "longitude": -748788996 + }, + "name": "" + }, + { + "location": { + "latitude": 401809022, + "longitude": -744157964 + }, + "name": "" + }, + { + "location": { + "latitude": 404226644, + "longitude": -740517141 + }, + "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" + }, + { + "location": { + "latitude": 410322033, + "longitude": -747871659 + }, + "name": "" + }, + { + "location": { + "latitude": 407100674, + "longitude": -747742727 + }, + "name": "" + }, + { + "location": { + "latitude": 418811433, + "longitude": -741718005 + }, + "name": "213 Bush Road, Stone Ridge, NY 12484, USA" + }, + { + "location": { + "latitude": 415034302, + "longitude": -743850945 + }, + "name": "" + }, + { + "location": { + "latitude": 411349992, + "longitude": -743694161 + }, + "name": "" + }, + { + "location": { + "latitude": 404839914, + "longitude": -744759616 + }, + "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" + }, + { + "location": { + "latitude": 414638017, + "longitude": -745957854 + }, + "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" + }, + { + "location": { + "latitude": 412127800, + "longitude": -740173578 + }, + "name": "" + }, + { + "location": { + "latitude": 401263460, + "longitude": -747964303 + }, + "name": "" + }, + { + "location": { + "latitude": 412843391, + "longitude": -749086026 + }, + "name": "" + }, + { + "location": { + "latitude": 418512773, + "longitude": -743067823 + }, + "name": "" + }, + { + "location": { + "latitude": 404318328, + "longitude": -740835638 + }, + "name": "42-102 Main Street, Belford, NJ 07718, USA" + }, + { + "location": { + "latitude": 419020746, + "longitude": -741172328 + }, + "name": "" + }, + { + "location": { + "latitude": 404080723, + "longitude": -746119569 + }, + "name": "" + }, + { + "location": { + "latitude": 401012643, + "longitude": -744035134 + }, + "name": "" + }, + { + "location": { + "latitude": 404306372, + "longitude": -741079661 + }, + "name": "" + }, + { + "location": { + "latitude": 403966326, + "longitude": -748519297 + }, + "name": "" + }, + { + "location": { + "latitude": 405002031, + "longitude": -748407866 + }, + "name": "" + }, + { + "location": { + "latitude": 409532885, + "longitude": -742200683 + }, + "name": "" + }, + { + "location": { + "latitude": 416851321, + "longitude": -742674555 + }, + "name": "" + }, + { + "location": { + "latitude": 406411633, + "longitude": -741722051 + }, + "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" + }, + { + "location": { + "latitude": 413069058, + "longitude": -744597778 + }, + "name": "261 Van Sickle Road, Goshen, NY 10924, USA" + }, + { + "location": { + "latitude": 418465462, + "longitude": -746859398 + }, + "name": "" + }, + { + "location": { + "latitude": 411733222, + "longitude": -744228360 + }, + "name": "" + }, + { + "location": { + "latitude": 410248224, + "longitude": -747127767 + }, + "name": "3 Hasta Way, Newton, NJ 07860, USA" + } +] diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 118db3950..99206ce42 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -768,6 +768,9 @@ extension Target { .argumentParser, ], path: "Examples/v2/hello-world", + exclude: [ + "HelloWorld.proto" + ], swiftSettings: [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), @@ -820,6 +823,26 @@ extension Target { ) } + static var routeGuide_v2: Target { + .executableTarget( + name: "route-guide", + dependencies: [ + .grpcProtobuf, + .grpcHTTP2Transport, + .argumentParser, + ], + path: "Examples/v2/route-guide", + resources: [ + .copy("route_guide_db.json") + ], + swiftSettings: [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") + ] + ) + } + static var packetCapture: Target { .executableTarget( name: "PacketCapture", @@ -1088,6 +1111,7 @@ let package = Package( // v2 examples .echo_v2, .helloWorld_v2, + .routeGuide_v2, ] ) diff --git a/Protos/generate.sh b/Protos/generate.sh index e35f7e042..6346105b2 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -88,7 +88,7 @@ function generate_echo_v2_example { generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" } -function generate_routeguide_example { +function generate_routeguide_v1_example { local proto="$here/examples/route_guide/route_guide.proto" local output="$root/Examples/v1/RouteGuide/Model" @@ -96,6 +96,14 @@ function generate_routeguide_example { generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" } +function generate_routeguide_v2_example { + local proto="$here/examples/route_guide/route_guide.proto" + local output="$root/Examples/v2/route-guide/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" +} + function generate_helloworld_v1_example { local proto="$here/upstream/grpc/examples/helloworld.proto" local output="$root/Examples/v1/HelloWorld/Model" @@ -261,7 +269,8 @@ function generate_health_service { # Examples generate_echo_v1_example generate_echo_v2_example -generate_routeguide_example +generate_routeguide_v1_example +generate_routeguide_v2_example generate_helloworld_v1_example generate_helloworld_v2_example generate_reflection_data_example From a06ade33b22b51ac1137bfb10f69fd9582983bd7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 4 Sep 2024 10:08:48 +0100 Subject: [PATCH 449/580] Improve the names of a few commonly used APIs (#2014) Motivation: Naming is important; it should be clear and concise. Modifications: The follow renames all offer more precise names: - Rename `server.run()` to `server.serve()` - Rename `server.stopListening()` to `server.beginGracefulShutdown()` - Rename `client.close()` to `client.beginGracefulShutdown()` Result: Clearer APIs --- Examples/v2/echo/Subcommands/Collect.swift | 2 +- Examples/v2/echo/Subcommands/Expand.swift | 2 +- Examples/v2/echo/Subcommands/Get.swift | 2 +- Examples/v2/echo/Subcommands/Serve.swift | 2 +- Examples/v2/echo/Subcommands/Update.swift | 2 +- .../v2/hello-world/Subcommands/Greet.swift | 2 +- .../v2/hello-world/Subcommands/Serve.swift | 2 +- .../route-guide/Subcommands/GetFeature.swift | 2 +- .../route-guide/Subcommands/ListFeatures.swift | 2 +- .../route-guide/Subcommands/RecordRoute.swift | 2 +- .../v2/route-guide/Subcommands/RouteChat.swift | 2 +- .../v2/route-guide/Subcommands/Serve.swift | 2 +- .../route-guide-sec05-step08-run.swift | 2 +- Sources/GRPCCore/GRPCClient.swift | 12 ++++++------ Sources/GRPCCore/GRPCServer.swift | 14 +++++++------- .../GRPCCore/Transport/ClientTransport.swift | 4 ++-- .../GRPCCore/Transport/ServerTransport.swift | 4 ++-- .../Client/Connection/GRPCChannel.swift | 12 ++++++------ .../HTTP2ClientTransport+Posix.swift | 4 ++-- .../HTTP2ServerTransport+Posix.swift | 2 +- ...TTP2ServerTransport+TransportServices.swift | 2 +- .../InProcessClientTransport.swift | 2 +- .../InProcessServerTransport.swift | 6 +++--- .../InteroperabilityTestsExecutable.swift | 4 ++-- .../performance-worker/BenchmarkClient.swift | 4 ++-- .../performance-worker/PerformanceWorker.swift | 2 +- Sources/performance-worker/WorkerService.swift | 8 ++++---- .../ClientRPCExecutorTestHarness.swift | 4 ++-- Tests/GRPCCoreTests/GRPCClientTests.swift | 12 ++++++------ Tests/GRPCCoreTests/GRPCServerTests.swift | 18 +++++++++--------- .../Transport/AnyTransport.swift | 8 ++++---- .../Transport/StreamCountingTransport.swift | 8 ++++---- .../Transport/ThrowingTransport.swift | 6 +++--- .../Client/Connection/GRPCChannelTests.swift | 18 +++++++++--------- .../HTTP2TransportNIOPosixTests.swift | 10 +++++----- ...TP2TransportNIOTransportServicesTests.swift | 8 ++++---- .../HTTP2TransportTests.swift | 8 ++++---- .../InProcessClientTransportTests.swift | 18 +++++++++--------- .../InProcessServerTransportTests.swift | 4 ++-- .../InProcessInteroperabilityTests.swift | 2 +- Tests/Services/HealthTests/HealthTests.swift | 2 +- 41 files changed, 116 insertions(+), 116 deletions(-) diff --git a/Examples/v2/echo/Subcommands/Collect.swift b/Examples/v2/echo/Subcommands/Collect.swift index ede689925..a617e83b9 100644 --- a/Examples/v2/echo/Subcommands/Collect.swift +++ b/Examples/v2/echo/Subcommands/Collect.swift @@ -53,7 +53,7 @@ struct Collect: AsyncParsableCommand { print("collect ← \(message.text)") } - client.close() + client.beginGracefulShutdown() } } } diff --git a/Examples/v2/echo/Subcommands/Expand.swift b/Examples/v2/echo/Subcommands/Expand.swift index 9eb9c8821..33f89895d 100644 --- a/Examples/v2/echo/Subcommands/Expand.swift +++ b/Examples/v2/echo/Subcommands/Expand.swift @@ -53,7 +53,7 @@ struct Expand: AsyncParsableCommand { } } - client.close() + client.beginGracefulShutdown() } } } diff --git a/Examples/v2/echo/Subcommands/Get.swift b/Examples/v2/echo/Subcommands/Get.swift index 0e686a906..400ba24a8 100644 --- a/Examples/v2/echo/Subcommands/Get.swift +++ b/Examples/v2/echo/Subcommands/Get.swift @@ -48,7 +48,7 @@ struct Get: AsyncParsableCommand { print("get ← \(response.text)") } - client.close() + client.beginGracefulShutdown() } } } diff --git a/Examples/v2/echo/Subcommands/Serve.swift b/Examples/v2/echo/Subcommands/Serve.swift index e9c50bb46..d03adbbe8 100644 --- a/Examples/v2/echo/Subcommands/Serve.swift +++ b/Examples/v2/echo/Subcommands/Serve.swift @@ -36,7 +36,7 @@ struct Serve: AsyncParsableCommand { ) try await withThrowingDiscardingTaskGroup { group in - group.addTask { try await server.run() } + group.addTask { try await server.serve() } if let address = try await server.listeningAddress { print("Echo listening on \(address)") } diff --git a/Examples/v2/echo/Subcommands/Update.swift b/Examples/v2/echo/Subcommands/Update.swift index 2e5b6ed52..04fdc1335 100644 --- a/Examples/v2/echo/Subcommands/Update.swift +++ b/Examples/v2/echo/Subcommands/Update.swift @@ -56,7 +56,7 @@ struct Update: AsyncParsableCommand { } } - client.close() + client.beginGracefulShutdown() } } } diff --git a/Examples/v2/hello-world/Subcommands/Greet.swift b/Examples/v2/hello-world/Subcommands/Greet.swift index 3b8edae99..9d97bb517 100644 --- a/Examples/v2/hello-world/Subcommands/Greet.swift +++ b/Examples/v2/hello-world/Subcommands/Greet.swift @@ -42,7 +42,7 @@ struct Greet: AsyncParsableCommand { } defer { - client.close() + client.beginGracefulShutdown() } let greeter = Helloworld_GreeterClient(wrapping: client) diff --git a/Examples/v2/hello-world/Subcommands/Serve.swift b/Examples/v2/hello-world/Subcommands/Serve.swift index 56f80c3fa..1d60c82d7 100644 --- a/Examples/v2/hello-world/Subcommands/Serve.swift +++ b/Examples/v2/hello-world/Subcommands/Serve.swift @@ -35,7 +35,7 @@ struct Serve: AsyncParsableCommand { ) try await withThrowingDiscardingTaskGroup { group in - group.addTask { try await server.run() } + group.addTask { try await server.serve() } if let address = try await server.listeningAddress { print("Greeter listening on \(address)") } diff --git a/Examples/v2/route-guide/Subcommands/GetFeature.swift b/Examples/v2/route-guide/Subcommands/GetFeature.swift index a8e56191a..6e51f2427 100644 --- a/Examples/v2/route-guide/Subcommands/GetFeature.swift +++ b/Examples/v2/route-guide/Subcommands/GetFeature.swift @@ -63,7 +63,7 @@ struct GetFeature: AsyncParsableCommand { print("Found '\(feature.name)' at (\(self.latitude), \(self.longitude))") } - client.close() + client.beginGracefulShutdown() } } } diff --git a/Examples/v2/route-guide/Subcommands/ListFeatures.swift b/Examples/v2/route-guide/Subcommands/ListFeatures.swift index 4bb33c2c3..ea95cb593 100644 --- a/Examples/v2/route-guide/Subcommands/ListFeatures.swift +++ b/Examples/v2/route-guide/Subcommands/ListFeatures.swift @@ -79,7 +79,7 @@ struct ListFeatures: AsyncParsableCommand { } } - client.close() + client.beginGracefulShutdown() } } diff --git a/Examples/v2/route-guide/Subcommands/RecordRoute.swift b/Examples/v2/route-guide/Subcommands/RecordRoute.swift index 2482e842a..829f5ee06 100644 --- a/Examples/v2/route-guide/Subcommands/RecordRoute.swift +++ b/Examples/v2/route-guide/Subcommands/RecordRoute.swift @@ -68,7 +68,7 @@ struct RecordRoute: AsyncParsableCommand { """ print(text) - client.close() + client.beginGracefulShutdown() } } } diff --git a/Examples/v2/route-guide/Subcommands/RouteChat.swift b/Examples/v2/route-guide/Subcommands/RouteChat.swift index 55e3be6f9..7fbf673c2 100644 --- a/Examples/v2/route-guide/Subcommands/RouteChat.swift +++ b/Examples/v2/route-guide/Subcommands/RouteChat.swift @@ -68,7 +68,7 @@ struct RouteChat: AsyncParsableCommand { } } - client.close() + client.beginGracefulShutdown() } } } diff --git a/Examples/v2/route-guide/Subcommands/Serve.swift b/Examples/v2/route-guide/Subcommands/Serve.swift index 9af509502..8de97671e 100644 --- a/Examples/v2/route-guide/Subcommands/Serve.swift +++ b/Examples/v2/route-guide/Subcommands/Serve.swift @@ -45,7 +45,7 @@ struct Serve: AsyncParsableCommand { let server = GRPCServer(transport: transport, services: [RouteGuideService(features: features)]) try await withThrowingDiscardingTaskGroup { group in - group.addTask { try await server.run() } + group.addTask { try await server.serve() } let address = try await transport.listeningAddress print("server listening on \(address)") } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift index 630b408b7..b99885335 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift @@ -14,7 +14,7 @@ extension RouteGuide { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await server.run() + try await server.serve() } if let address = try await server.listeningAddress { diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index e65699932..011226d7a 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -99,12 +99,12 @@ internal import Atomics /// } /// /// // The RPC has completed, close the client. -/// client.close() +/// client.beginGracefulShutdown() /// } /// ``` /// /// The ``run()`` method won't return until the client has finished handling all requests. You can -/// signal to the client that it should stop creating new request streams by calling ``close()``. +/// signal to the client that it should stop creating new request streams by calling ``beginGracefulShutdown()``. /// This gives the client enough time to drain any requests already in flight. To stop the client /// more abruptly you can cancel the task running your client. If your application requires /// additional resources that need their lifecycles managed you should consider using [Swift Service @@ -159,7 +159,7 @@ public struct GRPCClient: Sendable { /// Start the client. /// - /// This returns once ``close()`` has been called and all in-flight RPCs have finished executing. + /// This returns once ``beginGracefulShutdown()`` has been called and all in-flight RPCs have finished executing. /// If you need to abruptly stop all work you should cancel the task executing this method. /// /// The client, and by extension this function, can only be run once. If the client is already @@ -210,7 +210,7 @@ public struct GRPCClient: Sendable { /// The transport will be closed: this means that it will be given enough time to wait for /// in-flight RPCs to finish executing, but no new RPCs will be accepted. You can cancel the task /// executing ``run()`` if you want to abruptly stop in-flight RPCs. - public func close() { + public func beginGracefulShutdown() { while true { let (wasRunning, actualState) = self.state.compareExchange( expected: .running, @@ -220,7 +220,7 @@ public struct GRPCClient: Sendable { // Transition from running to stopping: close the transport. if wasRunning { - self.transport.close() + self.transport.beginGracefulShutdown() return } @@ -351,7 +351,7 @@ public struct GRPCClient: Sendable { /// Start a bidirectional streaming RPC. /// - /// - Note: ``run()`` must have been called and still executing, and ``close()`` mustn't + /// - Note: ``run()`` must have been called and still executing, and ``beginGracefulShutdown()`` mustn't /// have been called. /// /// - Parameters: diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index c4e2534a5..fe6207c79 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -60,11 +60,11 @@ internal import Atomics /// /// ```swift /// // Start running the server. -/// try await server.run() +/// try await server.serve() /// ``` /// /// The ``run()`` method won't return until the server has finished handling all requests. You can -/// signal to the server that it should stop accepting new requests by calling ``stopListening()``. +/// signal to the server that it should stop accepting new requests by calling ``beginGracefulShutdown()``. /// This allows the server to drain existing requests gracefully. To stop the server more abruptly /// you can cancel the task running your server. If your application requires additional resources /// that need their lifecycles managed you should consider using [Swift Service @@ -154,13 +154,13 @@ public struct GRPCServer: Sendable { /// /// This function returns when the configured transport has stopped listening and all requests have been /// handled. You can signal to the transport that it should stop listening by calling - /// ``stopListening()``. The server will continue to process existing requests. + /// ``beginGracefulShutdown()``. The server will continue to process existing requests. /// /// To stop the server more abruptly you can cancel the task that this function is running in. /// /// - Note: You can only call this function once, repeated calls will result in a /// ``RuntimeError`` being thrown. - public func run() async throws { + public func serve() async throws { let (wasNotStarted, actualState) = self.state.compareExchange( expected: .notStarted, desired: .running, @@ -209,7 +209,7 @@ public struct GRPCServer: Sendable { /// against this server. Once the server has processed all requests the ``run()`` method returns. /// /// Calling this on a server which is already stopping or has stopped has no effect. - public func stopListening() { + public func beginGracefulShutdown() { let (wasRunning, actual) = self.state.compareExchange( expected: .running, desired: .stopping, @@ -217,7 +217,7 @@ public struct GRPCServer: Sendable { ) if wasRunning { - self.transport.stopListening() + self.transport.beginGracefulShutdown() } else { switch actual { case .notStarted: @@ -229,7 +229,7 @@ public struct GRPCServer: Sendable { // Lost a race with 'run()', try again. if !exchanged { - self.stopListening() + self.beginGracefulShutdown() } case .running: diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index c678cac61..89d61464a 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -34,7 +34,7 @@ public protocol ClientTransport: Sendable { /// /// Implementations of this function will typically create a long-lived task group which /// maintains connections. The function exits when all open streams have been closed and new connections - /// are no longer required by the caller who signals this by calling ``close()``, or by cancelling the + /// are no longer required by the caller who signals this by calling ``beginGracefulShutdown()``, or by cancelling the /// task this function runs in. func connect() async throws @@ -46,7 +46,7 @@ public protocol ClientTransport: Sendable { /// /// If you want to forcefully cancel all active streams then cancel the task /// running ``connect()``. - func close() + func beginGracefulShutdown() /// Opens a stream using the transport, and uses it as input into a user-provided closure. /// diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index c402c735a..abb4b8c90 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -26,7 +26,7 @@ public protocol ServerTransport: Sendable { /// and start accepting new connections. Each accepted inbound RPC stream will be handed over to /// the provided `streamHandler` to handle accordingly. /// - /// You can call ``stopListening()`` to stop the transport from accepting new streams. Existing + /// You can call ``beginGracefulShutdown()`` to stop the transport from accepting new streams. Existing /// streams must be allowed to complete naturally. However, transports may also enforce a grace /// period after which any open streams may be cancelled. You can also cancel the task running /// ``listen(_:)`` to abruptly close connections and streams. @@ -38,5 +38,5 @@ public protocol ServerTransport: Sendable { /// /// Existing streams are permitted to run to completion. However, the transport may also enforce /// a grace period, after which remaining streams are cancelled. - func stopListening() + func beginGracefulShutdown() } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index 00fde9312..bd6718174 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -138,9 +138,9 @@ package final class GRPCChannel: ClientTransport { for try await result in self.resolver.names { self.input.continuation.yield(.handleResolutionResult(result)) } - self.close() + self.beginGracefulShutdown() } catch { - self.close() + self.beginGracefulShutdown() } } @@ -183,7 +183,7 @@ package final class GRPCChannel: ClientTransport { /// Signal to the transport that no new streams may be created and that connections should be /// closed when all streams are closed. - package func close() { + package func beginGracefulShutdown() { self.input.continuation.yield(.close) } @@ -393,7 +393,7 @@ extension GRPCChannel { self.updateLoadBalancer(serviceConfig: config, endpoints: result.endpoints, in: &group) case .failure: - self.close() + self.beginGracefulShutdown() } } @@ -567,10 +567,10 @@ extension GRPCChannel { if let result = try await iterator.next() { self.handleNameResolutionResult(result, in: &group) } else { - self.close() + self.beginGracefulShutdown() } } catch { - self.close() + self.beginGracefulShutdown() } } } diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index ad6bfdc7e..995f8948f 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -105,8 +105,8 @@ extension HTTP2ClientTransport { self.channel.configuration(forMethod: descriptor) } - public func close() { - self.channel.close() + public func beginGracefulShutdown() { + self.channel.beginGracefulShutdown() } public func withStream( diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 006434bbe..49d15c2d5 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -328,7 +328,7 @@ extension HTTP2ServerTransport { } } - public func stopListening() { + public func beginGracefulShutdown() { self.serverQuiescingHelper.initiateShutdown(promise: nil) } } diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 77374f54b..a3fb426d1 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -280,7 +280,7 @@ extension HTTP2ServerTransport { } } - public func stopListening() { + public func beginGracefulShutdown() { self.serverQuiescingHelper.initiateShutdown(promise: nil) } } diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index 03aa409ba..aded232a2 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -190,7 +190,7 @@ public final class InProcessClientTransport: ClientTransport { /// will result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. /// /// If you want to forcefully cancel all active streams then cancel the task running ``connect()``. - public func close() { + public func beginGracefulShutdown() { let maybeContinuation: AsyncStream.Continuation? = self.state.withLock { state in switch state { case .unconnected: diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 519300f16..4c627037f 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -23,7 +23,7 @@ public import GRPCCore /// /// To use this server, you call ``listen(_:)`` and iterate over the returned `AsyncSequence` to get all /// RPC requests made from clients (as ``RPCStream``s). -/// To stop listening to new requests, call ``stopListening()``. +/// To stop listening to new requests, call ``beginGracefulShutdown()``. /// /// - SeeAlso: ``ClientTransport`` @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -44,7 +44,7 @@ public struct InProcessServerTransport: ServerTransport, Sendable { /// /// - Parameter stream: The new ``RPCStream`` to publish. /// - Throws: ``RPCError`` with code ``RPCError/Code-swift.struct/failedPrecondition`` - /// if the server transport stopped listening to new streams (i.e., if ``stopListening()`` has been called). + /// if the server transport stopped listening to new streams (i.e., if ``beginGracefulShutdown()`` has been called). internal func acceptStream(_ stream: RPCStream) throws { let yieldResult = self.newStreamsContinuation.yield(stream) if case .terminated = yieldResult { @@ -70,7 +70,7 @@ public struct InProcessServerTransport: ServerTransport, Sendable { /// Stop listening to any new ``RPCStream`` publications. /// /// - SeeAlso: ``ServerTransport`` - public func stopListening() { + public func beginGracefulShutdown() { self.newStreamsContinuation.finish() } } diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift index cd92cba3c..9b60f6031 100644 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -47,7 +47,7 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { ), services: [TestService()] ) - try await server.run() + try await server.serve() } } @@ -97,7 +97,7 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { await self.runTest(testCase, using: client) } - client.close() + client.beginGracefulShutdown() } } diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 0541cc4cb..d0dd72ba0 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -120,7 +120,7 @@ struct BenchmarkClient { try await rpcsGroup.waitForAll() } - self.client.close() + self.client.beginGracefulShutdown() try await clientGroup.next() } } @@ -237,6 +237,6 @@ struct BenchmarkClient { internal func shutdown() { self._isShuttingDown.store(true, ordering: .relaxed) - self.client.close() + self.client.beginGracefulShutdown() } } diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift index 5b1ec8a20..e93bfca60 100644 --- a/Sources/performance-worker/PerformanceWorker.swift +++ b/Sources/performance-worker/PerformanceWorker.swift @@ -57,7 +57,7 @@ struct PerformanceWorker: AsyncParsableCommand { ), services: [WorkerService()] ) - try await server.run() + try await server.serve() } } diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 413314ff0..f029e146b 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -239,7 +239,7 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { } case .shutDownServer(let server): - server.stopListening() + server.beginGracefulShutdown() } return ServerResponse.Single(message: Grpc_Testing_Void()) @@ -269,7 +269,7 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { let result: Result do { - try await server.run() + try await server.serve() result = .success(()) } catch { result = .failure(error) @@ -317,7 +317,7 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { // shutdown its ELG. switch self.state.withLockedValue({ $0.stopListening() }) { case .stopListening(let server): - server.stopListening() + server.beginGracefulShutdown() case .nothing: () } @@ -409,7 +409,7 @@ extension WorkerService { case .runServer: return (server, transport) case .invalidState(let error): - server.stopListening() + server.beginGracefulShutdown() try await eventLoopGroup.shutdownGracefully() throw error } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 9a23448ac..0d16fdf4d 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -143,8 +143,8 @@ struct ClientRPCExecutorTestHarness { ) // Close the client so the server can finish. - self.clientTransport.close() - self.serverTransport.stopListening() + self.clientTransport.beginGracefulShutdown() + self.serverTransport.beginGracefulShutdown() group.cancelAll() } } diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index f360936b4..71762ba15 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -31,7 +31,7 @@ final class GRPCClientTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await server.run() + try await server.serve() } group.addTask { @@ -41,8 +41,8 @@ final class GRPCClientTests: XCTestCase { // Make sure both server and client are running try await Task.sleep(for: .milliseconds(100)) try await body(client, server) - client.close() - server.stopListening() + client.beginGracefulShutdown() + server.beginGracefulShutdown() } } @@ -273,7 +273,7 @@ final class GRPCClientTests: XCTestCase { } // New RPCs should fail immediately after this. - client.close() + client.beginGracefulShutdown() // RPC should fail now. await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { @@ -296,7 +296,7 @@ final class GRPCClientTests: XCTestCase { request: .init(producer: { writer in // Close the client once this RCP has been started. - client.close() + client.beginGracefulShutdown() // Attempts to start a new RPC should fail. await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { @@ -335,7 +335,7 @@ final class GRPCClientTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { let server = GRPCServer(transport: inProcess.server, services: [BinaryEcho()]) - try await server.run() + try await server.serve() } group.addTask { diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 6c66d7ca0..354131865 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -34,7 +34,7 @@ final class GRPCServerTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await server.run() + try await server.serve() } group.addTask { @@ -42,8 +42,8 @@ final class GRPCServerTests: XCTestCase { } try await body(inProcess.client, server) - inProcess.client.close() - server.stopListening() + inProcess.client.beginGracefulShutdown() + server.beginGracefulShutdown() } } @@ -274,7 +274,7 @@ final class GRPCServerTests: XCTestCase { try await self.doEchoGet(using: client) // New streams should fail immediately after this. - server.stopListening() + server.beginGracefulShutdown() // RPC should fail now. await XCTAssertThrowsRPCErrorAsync { @@ -303,7 +303,7 @@ final class GRPCServerTests: XCTestCase { XCTAssertMetadata(metadata) // New streams should fail immediately after this. - server.stopListening() + server.beginGracefulShutdown() try await stream.outbound.write(.message([0])) stream.outbound.finish() @@ -320,7 +320,7 @@ final class GRPCServerTests: XCTestCase { let inProcess = InProcessTransport.makePair() let task = Task { let server = GRPCServer(transport: inProcess.server, services: [BinaryEcho()]) - try await server.run() + try await server.serve() } try await withThrowingTaskGroup(of: Void.self) { group in @@ -340,13 +340,13 @@ final class GRPCServerTests: XCTestCase { func testTestRunStoppedServer() async throws { let server = GRPCServer(transport: InProcessServerTransport(), services: []) // Run the server. - let task = Task { try await server.run() } + let task = Task { try await server.serve() } task.cancel() try await task.value // Server is stopped, should throw an error. await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { - try await server.run() + try await server.serve() } errorHandler: { error in XCTAssertEqual(error.code, .serverIsStopped) } @@ -355,7 +355,7 @@ final class GRPCServerTests: XCTestCase { func testRunServerWhenTransportThrows() async throws { let server = GRPCServer(transport: ThrowOnRunServerTransport(), services: []) await XCTAssertThrowsErrorAsync(ofType: RuntimeError.self) { - try await server.run() + try await server.serve() } errorHandler: { error in XCTAssertEqual(error.code, .transportError) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index b09293117..b5953dd3a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -45,7 +45,7 @@ struct AnyClientTransport: ClientTransport, Sendable { } self._close = { - transport.close() + transport.beginGracefulShutdown() } self._configuration = { descriptor in @@ -61,7 +61,7 @@ struct AnyClientTransport: ClientTransport, Sendable { try await self._connect() } - func close() { + func beginGracefulShutdown() { self._close() } @@ -94,7 +94,7 @@ struct AnyServerTransport: ServerTransport, Sendable { init(wrapping transport: Transport) { self._listen = { streamHandler in try await transport.listen(streamHandler) } - self._stopListening = { transport.stopListening() } + self._stopListening = { transport.beginGracefulShutdown() } } func listen( @@ -103,7 +103,7 @@ struct AnyServerTransport: ServerTransport, Sendable { try await self._listen(streamHandler) } - func stopListening() { + func beginGracefulShutdown() { self._stopListening() } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 28111bd15..31a1de086 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -47,8 +47,8 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { try await self.transport.connect() } - func close() { - self.transport.close() + func beginGracefulShutdown() { + self.transport.beginGracefulShutdown() } func withStream( @@ -102,7 +102,7 @@ struct StreamCountingServerTransport: ServerTransport, Sendable { } } - func stopListening() { - self.transport.stopListening() + func beginGracefulShutdown() { + self.transport.beginGracefulShutdown() } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 4e5dca3ab..b9b1b6d80 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -32,7 +32,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { // no-op } - func close() { + func beginGracefulShutdown() { // no-op } @@ -60,7 +60,7 @@ struct ThrowOnRunServerTransport: ServerTransport { ) } - func stopListening() { + func beginGracefulShutdown() { // no-op } } @@ -84,7 +84,7 @@ struct ThrowOnSignalServerTransport: ServerTransport { ) } - func stopListening() { + func beginGracefulShutdown() { // no-op } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index 8757445f1..391e1e624 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -96,7 +96,7 @@ final class GRPCChannelTests: XCTestCase { XCTAssertEqual(throttle.tokenRatio, 0.1) // Now close. - channel.close() + channel.beginGracefulShutdown() default: () @@ -176,7 +176,7 @@ final class GRPCChannelTests: XCTestCase { return noConfigForGet && configForUpdate && noThrottle } - channel.close() + channel.beginGracefulShutdown() default: () @@ -362,7 +362,7 @@ final class GRPCChannelTests: XCTestCase { switch part1 { case .metadata: // Got metadata, close the channel. - channel.close() + channel.beginGracefulShutdown() case .message, .status, .none: XCTFail("Expected metadata, got \(String(describing: part1))") } @@ -472,7 +472,7 @@ final class GRPCChannelTests: XCTestCase { // All RPCs done, close the channel and cancel the group to stop the server. if outstandingRPCs == 0 { - channel.close() + channel.beginGracefulShutdown() group.cancelAll() } @@ -537,7 +537,7 @@ final class GRPCChannelTests: XCTestCase { // All RPCs done, close the channel and cancel the group to stop the server. if outstandingRPCs == 0 { - channel.close() + channel.beginGracefulShutdown() group.cancelAll() } @@ -616,7 +616,7 @@ final class GRPCChannelTests: XCTestCase { server1.clients.count == 0 && server2.clients.count == 0 && server3.clients.count == 1 } - channel.close() + channel.beginGracefulShutdown() case .shutdown: group.cancelAll() @@ -683,7 +683,7 @@ final class GRPCChannelTests: XCTestCase { break } } - channel.close() + channel.beginGracefulShutdown() default: () } @@ -737,7 +737,7 @@ final class GRPCChannelTests: XCTestCase { server1.clients.count == 1 && server2.clients.count == 0 } - channel.close() + channel.beginGracefulShutdown() default: () @@ -776,7 +776,7 @@ final class GRPCChannelTests: XCTestCase { // Sleep a little to increase the chances of the stream being queued before the channel // reacts to the close. try await Task.sleep(for: .milliseconds(10)) - channel.close() + channel.beginGracefulShutdown() } // Try to open a new stream. diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index fae9960cf..28bc49967 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -40,7 +40,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { let address = try await transport.listeningAddress let ipv4Address = try XCTUnwrap(address.ipv4) XCTAssertNotEqual(ipv4Address.port, 0) - transport.stopListening() + transport.beginGracefulShutdown() } } } @@ -60,7 +60,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { let address = try await transport.listeningAddress let ipv6Address = try XCTUnwrap(address.ipv6) XCTAssertNotEqual(ipv6Address.port, 0) - transport.stopListening() + transport.beginGracefulShutdown() } } } @@ -82,7 +82,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { address.unixDomainSocket, GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/posix-uds-test") ) - transport.stopListening() + transport.beginGracefulShutdown() } } } @@ -103,7 +103,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { group.addTask { let address = try await transport.listeningAddress XCTAssertNotNil(address.virtualSocket) - transport.stopListening() + transport.beginGracefulShutdown() } } } @@ -165,7 +165,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { group.addTask { let address = try await transport.listeningAddress XCTAssertNotNil(address.ipv4) - transport.stopListening() + transport.beginGracefulShutdown() } } } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index ce21a0ad9..be54c0890 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -37,7 +37,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { let address = try await transport.listeningAddress let ipv4Address = try XCTUnwrap(address.ipv4) XCTAssertNotEqual(ipv4Address.port, 0) - transport.stopListening() + transport.beginGracefulShutdown() } } } @@ -57,7 +57,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { let address = try await transport.listeningAddress let ipv6Address = try XCTUnwrap(address.ipv6) XCTAssertNotEqual(ipv6Address.port, 0) - transport.stopListening() + transport.beginGracefulShutdown() } } } @@ -83,7 +83,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { address.unixDomainSocket, GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/niots-uds-test") ) - transport.stopListening() + transport.beginGracefulShutdown() } } } @@ -145,7 +145,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { group.addTask { let address = try await transport.listeningAddress XCTAssertNotNil(address.ipv4) - transport.stopListening() + transport.beginGracefulShutdown() } } } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 673289b29..b0652c2fe 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -94,8 +94,8 @@ final class HTTP2TransportTests: XCTestCase { XCTFail("Unexpected error: '\(error)' (\(pair))") } - server.stopListening() - client.close() + server.beginGracefulShutdown() + client.beginGracefulShutdown() } } } @@ -155,7 +155,7 @@ final class HTTP2TransportTests: XCTestCase { ) group.addTask { - try await server.run() + try await server.serve() } let address = try await server.listeningAddress! @@ -174,7 +174,7 @@ final class HTTP2TransportTests: XCTestCase { ) group.addTask { - try await server.run() + try await server.serve() } let address = try await server.listeningAddress! diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 90de7c27a..1209bc9d1 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -46,7 +46,7 @@ final class InProcessClientTransportTests: XCTestCase { func testConnectWhenClosed() async { let client = makeClient() - client.close() + client.beginGracefulShutdown() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { try await client.connect() @@ -80,14 +80,14 @@ final class InProcessClientTransportTests: XCTestCase { func testCloseWhenUnconnected() { let client = makeClient() - XCTAssertNoThrow(client.close()) + XCTAssertNoThrow(client.beginGracefulShutdown()) } func testCloseWhenClosed() { let client = makeClient() - client.close() + client.beginGracefulShutdown() - XCTAssertNoThrow(client.close()) + XCTAssertNoThrow(client.beginGracefulShutdown()) } func testConnectSuccessfullyAndThenClose() async throws { @@ -102,7 +102,7 @@ final class InProcessClientTransportTests: XCTestCase { } try await group.next() - client.close() + client.beginGracefulShutdown() } } @@ -118,7 +118,7 @@ final class InProcessClientTransportTests: XCTestCase { // Once the pending stream is opened, close the client to new connections, // so that, once this closure is executed and this stream is closed, // the client will return from `connect()`. - client.close() + client.beginGracefulShutdown() } } @@ -136,7 +136,7 @@ final class InProcessClientTransportTests: XCTestCase { func testOpenStreamWhenClosed() async { let client = makeClient() - client.close() + client.beginGracefulShutdown() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { try await client.withStream( @@ -182,7 +182,7 @@ final class InProcessClientTransportTests: XCTestCase { group.addTask { try await Task.sleep(for: .milliseconds(100)) - client.close() + client.beginGracefulShutdown() } try await group.next() @@ -277,7 +277,7 @@ final class InProcessClientTransportTests: XCTestCase { group.addTask { try await Task.sleep(for: .milliseconds(50)) - client.close() + client.beginGracefulShutdown() } try await group.next() diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index c9af595c1..9890d6c4b 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -44,7 +44,7 @@ final class InProcessServerTransportTests: XCTestCase { try await transport.listen { stream in let partValue = try? await stream.inbound.reduce(into: []) { $0.append($1) } XCTAssertEqual(partValue, [.message([42])]) - transport.stopListening() + transport.beginGracefulShutdown() } } @@ -77,7 +77,7 @@ final class InProcessServerTransportTests: XCTestCase { } XCTAssertEqual(firstStreamMessages, [.message([42])]) - transport.stopListening() + transport.beginGracefulShutdown() let secondStreamOutbound = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) let secondStream = RPCStream< diff --git a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift index 6c4be3353..437d31916 100644 --- a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift +++ b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift @@ -29,7 +29,7 @@ final class InProcessInteroperabilityTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { let server = GRPCServer(transport: inProcess.server, services: [TestService()]) - try await server.run() + try await server.serve() } group.addTask { diff --git a/Tests/Services/HealthTests/HealthTests.swift b/Tests/Services/HealthTests/HealthTests.swift index 01ccb313c..cd762f4ab 100644 --- a/Tests/Services/HealthTests/HealthTests.swift +++ b/Tests/Services/HealthTests/HealthTests.swift @@ -31,7 +31,7 @@ final class HealthTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await server.run() + try await server.serve() } group.addTask { From 3be62d5ac7e4a9f1f43edd5774c947c3d30ceb93 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Sep 2024 13:16:26 +0100 Subject: [PATCH 450/580] Stop using deprecated protobuf API (#2045) Motivation: swift-protobuf 1.27.0 deprecated the `init` from `serializedData` in favor of the `serializedBytes` variant. Modifications: - Stop using `serializedData` Result: Fewer warnings --- Sources/GRPC/Serialization.swift | 2 +- .../GRPCReflectionService/Server/ReflectionService.swift | 2 +- .../Codegen/Serialization/SerializationTests.swift | 4 +--- .../ReflectionServiceIntegrationTests.swift | 7 +++---- .../ReflectionServiceUnitTests.swift | 6 +++--- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Sources/GRPC/Serialization.swift b/Sources/GRPC/Serialization.swift index a6c590d59..fff0e13ec 100644 --- a/Sources/GRPC/Serialization.swift +++ b/Sources/GRPC/Serialization.swift @@ -76,7 +76,7 @@ public struct ProtobufDeserializer: MessageDeser var buffer = byteBuffer // '!' is okay; we can always read 'readableBytes'. let data = buffer.readData(length: buffer.readableBytes)! - return try Message(serializedData: data) + return try Message(serializedBytes: data) } } diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index c55529adf..af9dc2529 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -366,7 +366,7 @@ extension ReflectionService { """ ) } - return try Google_Protobuf_FileDescriptorProto(serializedData: serializedData) + return try Google_Protobuf_FileDescriptorProto(serializedBytes: serializedData) } static func readSerializedFileDescriptorProtos( diff --git a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift index e9584273c..a4bcd28e8 100644 --- a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift +++ b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift @@ -28,9 +28,7 @@ final class SerializationTests: GRPCTestCase { .deletingLastPathComponent().appendingPathComponent("echo.grpc.reflection") let base64EncodedData = try! Data(contentsOf: binaryFileURL) let binaryData = Data(base64Encoded: base64EncodedData)! - self - .fileDescriptorProto = - try! Google_Protobuf_FileDescriptorProto(serializedData: binaryData) + self.fileDescriptorProto = try! Google_Protobuf_FileDescriptorProto(serializedBytes: binaryData) } func testFileDescriptorMetadata() throws { diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index 1fdfca6e5..1836a1f68 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -128,8 +128,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { // response can't be nil as we just checked it. let receivedFileDescriptorProto = try Google_Protobuf_FileDescriptorProto( - serializedData: (message.fileDescriptorResponse - .fileDescriptorProto[0]) + serializedBytes: message.fileDescriptorResponse.fileDescriptorProto[0] ) XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto") @@ -177,7 +176,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { let receivedData: [Google_Protobuf_FileDescriptorProto] do { receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) + try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) } } catch { return XCTFail("Could not serialize data received as a message.") @@ -221,7 +220,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { let receivedData: [Google_Protobuf_FileDescriptorProto] do { receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) + try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) } } catch { return XCTFail("Could not serialize data received as a message.") diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index 76c51f2db..69f680311 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -178,7 +178,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { switch serializedFileDescriptorProtosResult { case .success(let serializedFileDescriptorProtos): let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) + try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) } // Tests that the functions returns all the transitive dependencies, with their services and // methods, together with the initial proto, as serialized data. @@ -233,7 +233,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { switch serializedFileDescriptorProtosResult { case .success(let serializedFileDescriptorProtos): let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) + try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) } // Tests that the functions returns all the tranzitive dependencies, with their services and // methods, together with the initial proto, as serialized data. @@ -292,7 +292,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { switch serializedFileDescriptorProtosResult { case .success(let serializedFileDescriptorProtos): let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedData: $0) + try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) } // Test that we get only 4 serialized File Descriptor Protos as response. XCTAssertEqual(fileDescriptorProtos.count, 4) From 0a6b49f4e93ab5461d9d1697644bb0273fcb95da Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Sep 2024 13:34:21 +0100 Subject: [PATCH 451/580] Remove swift-atomics from v2 (#2041) Motivation: Swift 6 includes built-in support for atomics in the Synchronization module. Currently we depend on the swift-atomics package which is no longer necessary. Modifications: Remove usages of swift-atomics from v2 Result: Fewer dependencies --- Package.swift | 9 +- Package@swift-6.swift | 7 +- Sources/GRPCCore/GRPCClient.swift | 148 ++++++++---------- Sources/GRPCCore/GRPCServer.swift | 106 ++++++------- .../Internal/AsyncIteratorSequence.swift | 8 +- .../Client/Connection/GRPCChannel.swift | 3 +- .../Internal/ProcessUniqueID.swift | 11 +- .../performance-worker/BenchmarkClient.swift | 16 +- .../performance-worker/BenchmarkService.swift | 30 +++- ...PCExecutorTestHarness+ServerBehavior.swift | 8 +- ...ientRPCExecutorTestHarness+Transport.swift | 2 +- .../ClientRPCExecutorTestHarness.swift | 2 +- .../Internal/ServerRPCExecutorTests.swift | 17 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 10 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 14 +- .../Test Utilities/AtomicCounter.swift | 35 +++++ .../Call/Client/ClientInterceptors.swift | 10 +- .../Call/Server/ServerInterceptors.swift | 10 +- .../Transport/StreamCountingTransport.swift | 22 +-- .../PickFirstLoadBalancerTests.swift | 17 +- .../RoundRobinLoadBalancerTests.swift | 13 +- .../Internal/ProcessUniqueIDTests.swift | 1 + .../Internal/TimerTests.swift | 15 +- .../Test Utilities/AtomicCounter.swift | 35 +++++ 24 files changed, 304 insertions(+), 245 deletions(-) create mode 100644 Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift diff --git a/Package.swift b/Package.swift index 658d97e83..43d1b987c 100644 --- a/Package.swift +++ b/Package.swift @@ -50,6 +50,10 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-collections.git", from: "1.0.5" ), + .package( + url: "https://github.com/apple/swift-atomics.git", + from: "1.2.0" + ), .package( url: "https://github.com/apple/swift-protobuf.git", from: "1.27.0" @@ -123,6 +127,7 @@ extension Target.Dependency { name: "SwiftProtobufPluginLibrary", package: "swift-protobuf" ) + static let atomics: Self = .product(name: "Atomics", package: "swift-atomics") static let dequeModule: Self = .product(name: "DequeModule", package: "swift-collections") } @@ -146,6 +151,7 @@ extension Target { .logging, .protobuf, .dequeModule, + .atomics ].appending( .nioSSL, if: includeNIOSSL ), @@ -198,7 +204,8 @@ extension Target { .nioEmbedded, .nioTransportServices, .logging, - .reflectionService + .reflectionService, + .atomics ].appending( .nioSSL, if: includeNIOSSL ), diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 99206ce42..f42e48fa5 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -179,6 +179,7 @@ extension Target { .logging, .protobuf, .dequeModule, + .atomics ].appending( .nioSSL, if: includeNIOSSL ), @@ -192,7 +193,6 @@ extension Target { name: "GRPCCore", dependencies: [ .dequeModule, - .atomics ], path: "Sources/GRPCCore", swiftSettings: [ @@ -242,7 +242,6 @@ extension Target { .nioTLS, .cgrpcZlib, .dequeModule, - .atomics ], swiftSettings: [ .swiftLanguageMode(.v6), @@ -383,7 +382,8 @@ extension Target { .nioEmbedded, .nioTransportServices, .logging, - .reflectionService + .reflectionService, + .atomics ].appending( .nioSSL, if: includeNIOSSL ), @@ -401,7 +401,6 @@ extension Target { .grpcCore, .grpcInProcessTransport, .dequeModule, - .atomics, .protobuf, ], swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 011226d7a..8b0027a74 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -internal import Atomics +private import Synchronization /// A gRPC client. /// @@ -110,7 +110,7 @@ internal import Atomics /// additional resources that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct GRPCClient: Sendable { +public final class GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. private let transport: any ClientTransport @@ -123,10 +123,10 @@ public struct GRPCClient: Sendable { private let interceptors: [any ClientInterceptor] /// The current state of the client. - private let state: ManagedAtomic + private let state: Mutex /// The state of the client. - private enum State: UInt8, AtomicValue { + private enum State: Sendable { /// The client hasn't been started yet. Can transition to `running` or `stopped`. case notStarted /// The client is running and can send RPCs. Can transition to `stopping`. @@ -137,6 +137,56 @@ public struct GRPCClient: Sendable { /// The client has stopped, no RPCs are in flight and no more will be accepted. This state /// is terminal. case stopped + + mutating func run() throws { + switch self { + case .notStarted: + self = .running + + case .running: + throw RuntimeError( + code: .clientIsAlreadyRunning, + message: "The client is already running and can only be started once." + ) + + case .stopping, .stopped: + throw RuntimeError( + code: .clientIsStopped, + message: "The client has stopped and can only be started once." + ) + } + } + + mutating func stopped() { + self = .stopped + } + + mutating func beginGracefulShutdown() -> Bool { + switch self { + case .notStarted: + self = .stopped + return false + case .running: + self = .stopping + return true + case .stopping, .stopped: + return false + } + } + + func checkExecutable() throws { + switch self { + case .notStarted, .running: + // Allow .notStarted as making a request can race with 'run()'. Transports should tolerate + // queuing the request if not yet started. + () + case .stopping, .stopped: + throw RuntimeError( + code: .clientIsStopped, + message: "Client has been stopped. Can't make any more RPCs." + ) + } + } } /// Creates a new client with the given transport, interceptors and configuration. @@ -154,7 +204,7 @@ public struct GRPCClient: Sendable { ) { self.transport = transport self.interceptors = interceptors - self.state = ManagedAtomic(.notStarted) + self.state = Mutex(.notStarted) } /// Start the client. @@ -165,33 +215,11 @@ public struct GRPCClient: Sendable { /// The client, and by extension this function, can only be run once. If the client is already /// running or has already been closed then a ``RuntimeError`` is thrown. public func run() async throws { - let (wasNotStarted, original) = self.state.compareExchange( - expected: .notStarted, - desired: .running, - ordering: .sequentiallyConsistent - ) - - guard wasNotStarted else { - switch original { - case .notStarted: - // The value wasn't exchanged so the original value can't be 'notStarted'. - fatalError() - case .running: - throw RuntimeError( - code: .clientIsAlreadyRunning, - message: "The client is already running and can only be started once." - ) - case .stopping, .stopped: - throw RuntimeError( - code: .clientIsStopped, - message: "The client has stopped and can only be started once." - ) - } - } + try self.state.withLock { try $0.run() } - // When we exit this function we must have stopped. + // When this function exits the client must have stopped. defer { - self.state.store(.stopped, ordering: .sequentiallyConsistent) + self.state.withLock { $0.stopped() } } do { @@ -211,50 +239,9 @@ public struct GRPCClient: Sendable { /// in-flight RPCs to finish executing, but no new RPCs will be accepted. You can cancel the task /// executing ``run()`` if you want to abruptly stop in-flight RPCs. public func beginGracefulShutdown() { - while true { - let (wasRunning, actualState) = self.state.compareExchange( - expected: .running, - desired: .stopping, - ordering: .sequentiallyConsistent - ) - - // Transition from running to stopping: close the transport. - if wasRunning { - self.transport.beginGracefulShutdown() - return - } - - // The expected state wasn't 'running'. There are two options: - // 1. The client isn't running yet. - // 2. The client is already stopping or stopped. - switch actualState { - case .notStarted: - // Not started: try going straight to stopped. - let (wasNotStarted, _) = self.state.compareExchange( - expected: .notStarted, - desired: .stopped, - ordering: .sequentiallyConsistent - ) - - // If the exchange happened then just return: the client wasn't started so there's no - // transport to start. - // - // If the exchange didn't happen then continue looping: the client must've been started by - // another thread. - if wasNotStarted { - return - } else { - continue - } - - case .running: - // Unreachable: the value was exchanged and this was the expected value. - fatalError() - - case .stopping, .stopped: - // No exchange happened but the client is already stopping. - return - } + let wasRunning = self.state.withLock { $0.beginGracefulShutdown() } + if wasRunning { + self.transport.beginGracefulShutdown() } } @@ -371,18 +358,7 @@ public struct GRPCClient: Sendable { options: CallOptions, handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { - switch self.state.load(ordering: .sequentiallyConsistent) { - case .notStarted, .running: - // Allow .notStarted as making a request can race with 'run()'. Transports should tolerate - // queuing the request if not yet started. - () - case .stopping, .stopped: - throw RuntimeError( - code: .clientIsStopped, - message: "Client has been stopped. Can't make any more RPCs." - ) - } - + try self.state.withLock { try $0.checkExecutable() } let methodConfig = self.transport.configuration(forMethod: descriptor) var options = options options.formUnion(with: methodConfig) diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index fe6207c79..e5d7fa8ee 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -internal import Atomics +private import Synchronization /// A gRPC server. /// @@ -70,7 +70,7 @@ internal import Atomics /// that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct GRPCServer: Sendable { +public final class GRPCServer: Sendable { typealias Stream = RPCStream /// The ``ServerTransport`` implementation that the server uses to listen for new requests. @@ -88,9 +88,9 @@ public struct GRPCServer: Sendable { private let interceptors: [any ServerInterceptor] /// The state of the server. - private let state: ManagedAtomic + private let state: Mutex - private enum State: UInt8, AtomicValue { + private enum State: Sendable { /// The server hasn't been started yet. Can transition to `running` or `stopped`. case notStarted /// The server is running and accepting RPCs. Can transition to `stopping`. @@ -101,6 +101,43 @@ public struct GRPCServer: Sendable { /// The server has stopped, no RPCs are in flight and no more will be accepted. This state /// is terminal. case stopped + + mutating func startServing() throws { + switch self { + case .notStarted: + self = .running + + case .running: + throw RuntimeError( + code: .serverIsAlreadyRunning, + message: "The server is already running and can only be started once." + ) + + case .stopping, .stopped: + throw RuntimeError( + code: .serverIsStopped, + message: "The server has stopped and can only be started once." + ) + } + } + + mutating func beginGracefulShutdown() -> Bool { + switch self { + case .notStarted: + self = .stopped + return false + case .running: + self = .stopping + return true + case .stopping, .stopped: + // Already stopping/stopped, ignore. + return false + } + } + + mutating func stopped() { + self = .stopped + } } /// Creates a new server with no resources. @@ -113,7 +150,7 @@ public struct GRPCServer: Sendable { /// are called. The first interceptor added will be the first interceptor to intercept each /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. - public init( + public convenience init( transport: any ServerTransport, services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [] @@ -141,7 +178,7 @@ public struct GRPCServer: Sendable { router: RPCRouter, interceptors: [any ServerInterceptor] = [] ) { - self.state = ManagedAtomic(.notStarted) + self.state = Mutex(.notStarted) self.transport = transport self.router = router self.interceptors = interceptors @@ -161,33 +198,11 @@ public struct GRPCServer: Sendable { /// - Note: You can only call this function once, repeated calls will result in a /// ``RuntimeError`` being thrown. public func serve() async throws { - let (wasNotStarted, actualState) = self.state.compareExchange( - expected: .notStarted, - desired: .running, - ordering: .sequentiallyConsistent - ) - - guard wasNotStarted else { - switch actualState { - case .notStarted: - fatalError() - case .running: - throw RuntimeError( - code: .serverIsAlreadyRunning, - message: "The server is already running and can only be started once." - ) + try self.state.withLock { try $0.startServing() } - case .stopping, .stopped: - throw RuntimeError( - code: .serverIsStopped, - message: "The server has stopped and can only be started once." - ) - } - } - - // When we exit this function we must have stopped. + // When we exit this function the server must have stopped. defer { - self.state.store(.stopped, ordering: .sequentiallyConsistent) + self.state.withLock { $0.stopped() } } do { @@ -210,36 +225,9 @@ public struct GRPCServer: Sendable { /// /// Calling this on a server which is already stopping or has stopped has no effect. public func beginGracefulShutdown() { - let (wasRunning, actual) = self.state.compareExchange( - expected: .running, - desired: .stopping, - ordering: .sequentiallyConsistent - ) - + let wasRunning = self.state.withLock { $0.beginGracefulShutdown() } if wasRunning { self.transport.beginGracefulShutdown() - } else { - switch actual { - case .notStarted: - let (exchanged, _) = self.state.compareExchange( - expected: .notStarted, - desired: .stopped, - ordering: .sequentiallyConsistent - ) - - // Lost a race with 'run()', try again. - if !exchanged { - self.beginGracefulShutdown() - } - - case .running: - // Unreachable, this branch only happens when the initial exchange didn't take place. - fatalError() - - case .stopping, .stopped: - // Already stopping/stopped, ignore. - () - } } } } diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift b/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift index 05596c6d4..f75421dd6 100644 --- a/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift @@ -14,12 +14,12 @@ * limitations under the License. */ -public import Atomics // should be @usableFromInline +public import Synchronization // should be @usableFromInline -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline /// An `AsyncSequence` which wraps an existing async iterator. -struct AsyncIteratorSequence: AsyncSequence { +final class AsyncIteratorSequence: AsyncSequence { @usableFromInline typealias Element = Base.Element @@ -29,7 +29,7 @@ struct AsyncIteratorSequence: AsyncSequence { /// Set to `true` when an iterator has been made. @usableFromInline - let _hasMadeIterator = ManagedAtomic(false) + let _hasMadeIterator = Atomic(false) @inlinable init(_ base: Base) { diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index bd6718174..c98e94914 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -14,8 +14,7 @@ * limitations under the License. */ -internal import Atomics -internal import DequeModule +private import DequeModule package import GRPCCore private import Synchronization diff --git a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift index d80658e00..5f6f32f7e 100644 --- a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift +++ b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift @@ -14,15 +14,17 @@ * limitations under the License. */ -internal import Atomics +private import Synchronization /// An ID which is unique within this process. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ProcessUniqueID: Hashable, Sendable, CustomStringConvertible { - private static let source = ManagedAtomic(UInt64(0)) + private static let source = Atomic(UInt64(0)) private let rawValue: UInt64 init() { - self.rawValue = Self.source.loadThenWrappingIncrement(ordering: .relaxed) + let (_, newValue) = Self.source.add(1, ordering: .relaxed) + self.rawValue = newValue } var description: String { @@ -31,6 +33,7 @@ struct ProcessUniqueID: Hashable, Sendable, CustomStringConvertible { } /// A process-unique ID for a subchannel. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct SubchannelID: Hashable, Sendable, CustomStringConvertible { private let id = ProcessUniqueID() package init() {} @@ -40,6 +43,7 @@ package struct SubchannelID: Hashable, Sendable, CustomStringConvertible { } /// A process-unique ID for a load-balancer. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct LoadBalancerID: Hashable, Sendable, CustomStringConvertible { private let id = ProcessUniqueID() var description: String { @@ -48,6 +52,7 @@ struct LoadBalancerID: Hashable, Sendable, CustomStringConvertible { } /// A process-unique ID for an entry in a queue. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct QueueEntryID: Hashable, Sendable, CustomStringConvertible { private let id = ProcessUniqueID() var description: String { diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index d0dd72ba0..3e1318b12 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -14,14 +14,14 @@ * limitations under the License. */ -private import Atomics private import Foundation internal import GRPCCore private import NIOConcurrencyHelpers +private import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct BenchmarkClient { - private let _isShuttingDown = ManagedAtomic(false) +final class BenchmarkClient: Sendable { + private let _isShuttingDown = Atomic(false) /// Whether the benchmark client is shutting down. Used to control when to stop sending messages /// or creating new RPCs. @@ -30,17 +30,17 @@ struct BenchmarkClient { } /// The underlying client. - private var client: GRPCClient + private let client: GRPCClient /// The number of concurrent RPCs to run. - private var concurrentRPCs: Int + private let concurrentRPCs: Int /// The type of RPC to make against the server. - private var rpcType: RPCType + private let rpcType: RPCType /// The max number of messages to send on a stream before replacing the RPC with a new one. A /// value of zero means there is no limit. - private var messagesPerStream: Int + private let messagesPerStream: Int private var noMessageLimit: Bool { self.messagesPerStream == 0 } /// The message to send for all RPC types to the server. @@ -206,7 +206,7 @@ struct BenchmarkClient { let nanos = now.uptimeNanoseconds - lastMessageSendTime.uptimeNanoseconds lastMessageSendTime = now self.record(latencyNanos: Double(nanos), errorCode: nil) - try await writer.write(message) + try await writer.write(self.message) } else { break } diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift index 694ad51b7..112b1f1f0 100644 --- a/Sources/performance-worker/BenchmarkService.swift +++ b/Sources/performance-worker/BenchmarkService.swift @@ -14,15 +14,15 @@ * limitations under the License. */ -private import Atomics internal import GRPCCore +private import Synchronization import struct Foundation.Data @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { +final class BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Used to check if the server can be streaming responses. - private let working = ManagedAtomic(true) + private let working = Atomic(true) /// One request followed by one response. /// The server returns a client payload with the size requested by the client. @@ -127,8 +127,24 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { } } + final class InboundStreamingSignal: Sendable { + private let _isStreaming: Atomic + + init() { + self._isStreaming = Atomic(true) + } + + var isStreaming: Bool { + self._isStreaming.load(ordering: .relaxed) + } + + func stop() { + self._isStreaming.store(false, ordering: .relaxed) + } + } + // Marks if the inbound streaming is ongoing or finished. - let inboundStreaming = ManagedAtomic(true) + let inbound = InboundStreamingSignal() return ServerResponse.Stream { writer in try await withThrowingTaskGroup(of: Void.self) { group in @@ -138,13 +154,11 @@ struct BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { try self.checkOkStatus(message.responseStatus) } } - inboundStreaming.store(false, ordering: .relaxed) + inbound.stop() } group.addTask { - while inboundStreaming.load(ordering: .relaxed) - && self.working.load(ordering: .acquiring) - { + while inbound.isStreaming && self.working.load(ordering: .acquiring) { try await writer.write(response) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index a2bed0d7d..4f0ab1b14 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + import XCTest @testable import GRPCCore @@ -104,10 +104,10 @@ extension ClientRPCExecutorTestHarness.ServerStreamHandler { } static func attemptBased(_ onAttempt: @Sendable @escaping (_ attempt: Int) -> Self) -> Self { - let attempts = ManagedAtomic(1) + let attempts = AtomicCounter(1) return Self { stream in - let attempt = attempts.loadThenWrappingIncrement(ordering: .sequentiallyConsistent) - let handler = onAttempt(attempt) + let (oldAttemptCount, _) = attempts.increment() + let handler = onAttempt(oldAttemptCount) try await handler.handle(stream: stream) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 30f0e1000..850b006d5 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + import GRPCCore import GRPCInProcessTransport diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 0d16fdf4d..d9bf10f23 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + import GRPCInProcessTransport import XCTest diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index e552991fa..0393de2c9 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -import Atomics import XCTest @testable import GRPCCore @@ -290,8 +289,8 @@ final class ServerRPCExecutorTests: XCTestCase { } func testMultipleInterceptorsAreCalled() async throws { - let counter1 = ManagedAtomic(0) - let counter2 = ManagedAtomic(0) + let counter1 = AtomicCounter() + let counter2 = AtomicCounter() // The interceptor skips the handler altogether. let harness = ServerRPCExecutorTestHarness( @@ -309,13 +308,13 @@ final class ServerRPCExecutorTests: XCTestCase { XCTAssertEqual(parts, [.metadata([:]), .status(.ok, [:])]) } - XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) - XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 1) + XCTAssertEqual(counter1.value, 1) + XCTAssertEqual(counter2.value, 1) } func testInterceptorsAreCalledInOrder() async throws { - let counter1 = ManagedAtomic(0) - let counter2 = ManagedAtomic(0) + let counter1 = AtomicCounter() + let counter2 = AtomicCounter() // The interceptor skips the handler altogether. let harness = ServerRPCExecutorTestHarness( @@ -334,9 +333,9 @@ final class ServerRPCExecutorTests: XCTestCase { XCTAssertEqual(parts, [.status(Status(code: .unavailable, message: ""), [:])]) } - XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) + XCTAssertEqual(counter1.value, 1) // Zero because the RPC should've been rejected by the second interceptor. - XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 0) + XCTAssertEqual(counter2.value, 0) } func testThrowingInterceptor() async throws { diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 71762ba15..af566279d 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + import GRPCCore import GRPCInProcessTransport import XCTest @@ -230,8 +230,8 @@ final class GRPCClientTests: XCTestCase { } func testInterceptorsAreAppliedInOrder() async throws { - let counter1 = ManagedAtomic(0) - let counter2 = ManagedAtomic(0) + let counter1 = AtomicCounter() + let counter2 = AtomicCounter() try await self.withInProcessConnectedClient( services: [BinaryEcho()], @@ -254,8 +254,8 @@ final class GRPCClientTests: XCTestCase { } } - XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) - XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 0) + XCTAssertEqual(counter1.value, 1) + XCTAssertEqual(counter2.value, 0) } func testNoNewRPCsAfterClientClose() async throws { diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 354131865..f5afcb2a1 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + import GRPCCore import GRPCInProcessTransport import XCTest @@ -215,8 +215,8 @@ final class GRPCServerTests: XCTestCase { } func testInterceptorsAreAppliedInOrder() async throws { - let counter1 = ManagedAtomic(0) - let counter2 = ManagedAtomic(0) + let counter1 = AtomicCounter() + let counter2 = AtomicCounter() try await self.withInProcessClientConnectedToServer( services: [BinaryEcho()], @@ -240,12 +240,12 @@ final class GRPCServerTests: XCTestCase { } } - XCTAssertEqual(counter1.load(ordering: .sequentiallyConsistent), 1) - XCTAssertEqual(counter2.load(ordering: .sequentiallyConsistent), 0) + XCTAssertEqual(counter1.value, 1) + XCTAssertEqual(counter2.value, 0) } func testInterceptorsAreNotAppliedToUnimplementedMethods() async throws { - let counter = ManagedAtomic(0) + let counter = AtomicCounter() try await self.withInProcessClientConnectedToServer( services: [BinaryEcho()], @@ -265,7 +265,7 @@ final class GRPCServerTests: XCTestCase { } } - XCTAssertEqual(counter.load(ordering: .sequentiallyConsistent), 0) + XCTAssertEqual(counter.value, 0) } func testNoNewRPCsAfterServerStopListening() async throws { diff --git a/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift b/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift new file mode 100644 index 000000000..b9e9fb5b8 --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift @@ -0,0 +1,35 @@ +/* + * 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 Synchronization + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class AtomicCounter: Sendable { + private let counter: Atomic + + init(_ initialValue: Int = 0) { + self.counter = Atomic(initialValue) + } + + var value: Int { + self.counter.load(ordering: .sequentiallyConsistent) + } + + @discardableResult + func increment() -> (oldValue: Int, newValue: Int) { + self.counter.add(1, ordering: .sequentiallyConsistent) + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index 1b35a0704..d8d355969 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + import GRPCCore @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -30,7 +30,7 @@ extension ClientInterceptor where Self == RejectAllClientInterceptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientInterceptor where Self == RequestCountingClientInterceptor { - static func requestCounter(_ counter: ManagedAtomic) -> Self { + static func requestCounter(_ counter: AtomicCounter) -> Self { return RequestCountingClientInterceptor(counter: counter) } } @@ -68,9 +68,9 @@ struct RejectAllClientInterceptor: ClientInterceptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RequestCountingClientInterceptor: ClientInterceptor { /// The number of requests made. - let counter: ManagedAtomic + let counter: AtomicCounter - init(counter: ManagedAtomic) { + init(counter: AtomicCounter) { self.counter = counter } @@ -82,7 +82,7 @@ struct RequestCountingClientInterceptor: ClientInterceptor { ClientInterceptorContext ) async throws -> ClientResponse.Stream ) async throws -> ClientResponse.Stream { - self.counter.wrappingIncrement(ordering: .sequentiallyConsistent) + self.counter.increment() return try await next(request, context) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index c70970eed..97c68aba9 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics + import GRPCCore @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -29,7 +29,7 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerInterceptor where Self == RequestCountingServerInterceptor { - static func requestCounter(_ counter: ManagedAtomic) -> Self { + static func requestCounter(_ counter: AtomicCounter) -> Self { return RequestCountingServerInterceptor(counter: counter) } } @@ -67,9 +67,9 @@ struct RejectAllServerInterceptor: ServerInterceptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RequestCountingServerInterceptor: ServerInterceptor { /// The number of requests made. - let counter: ManagedAtomic + let counter: AtomicCounter - init(counter: ManagedAtomic) { + init(counter: AtomicCounter) { self.counter = counter } @@ -81,7 +81,7 @@ struct RequestCountingServerInterceptor: ServerInterceptor { ServerInterceptorContext ) async throws -> ServerResponse.Stream ) async throws -> ServerResponse.Stream { - self.counter.wrappingIncrement(ordering: .sequentiallyConsistent) + self.counter.increment() return try await next(request, context) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 31a1de086..cd3df5af5 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import Atomics @testable import GRPCCore @@ -23,20 +22,22 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { typealias Outbound = RPCWriter.Closable private let transport: AnyClientTransport - private let _streamsOpened = ManagedAtomic(0) - private let _streamFailures = ManagedAtomic(0) + private let _streamsOpened: AtomicCounter + private let _streamFailures: AtomicCounter var streamsOpened: Int { - self._streamsOpened.load(ordering: .sequentiallyConsistent) + self._streamsOpened.value } var streamFailures: Int { - self._streamFailures.load(ordering: .sequentiallyConsistent) + self._streamFailures.value } init(wrapping transport: Transport) where Transport.Inbound == Inbound, Transport.Outbound == Outbound { self.transport = AnyClientTransport(wrapping: transport) + self._streamsOpened = AtomicCounter() + self._streamFailures = AtomicCounter() } var retryThrottle: RetryThrottle? { @@ -61,11 +62,11 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { descriptor: descriptor, options: options ) { stream in - self._streamsOpened.wrappingIncrement(ordering: .sequentiallyConsistent) + self._streamsOpened.increment() return try await closure(stream) } } catch { - self._streamFailures.wrappingIncrement(ordering: .sequentiallyConsistent) + self._streamFailures.increment() throw error } } @@ -83,21 +84,22 @@ struct StreamCountingServerTransport: ServerTransport, Sendable { typealias Outbound = RPCWriter.Closable private let transport: AnyServerTransport - private let _acceptedStreams = ManagedAtomic(0) + private let _acceptedStreams: AtomicCounter var acceptedStreamsCount: Int { - self._acceptedStreams.load(ordering: .sequentiallyConsistent) + self._acceptedStreams.value } init(wrapping transport: Transport) { self.transport = AnyServerTransport(wrapping: transport) + self._acceptedStreams = AtomicCounter() } func listen( _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { try await self.transport.listen { stream in - self._acceptedStreams.wrappingIncrement(ordering: .sequentiallyConsistent) + self._acceptedStreams.increment() await streamHandler(stream) } } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift index cc26898dd..29764adb5 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -import Atomics import GRPCCore import GRPCHTTP2Core import NIOHTTP2 @@ -194,7 +193,7 @@ final class PickFirstLoadBalancerTests: XCTestCase { func testPickOnIdleTriggersConnect() async throws { // Tests that picking a subchannel when the load balancer is idle triggers a reconnect and // becomes ready again. Uses a very short idle time to re-enter the idle state. - let idle = ManagedAtomic(0) + let idle = AtomicCounter() try await LoadBalancerTest.pickFirst( servers: 1, @@ -202,7 +201,7 @@ final class PickFirstLoadBalancerTests: XCTestCase { ) { context, event in switch event { case .connectivityStateChanged(.idle): - let idleCount = idle.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) + let (_, idleCount) = idle.increment() switch idleCount { case 1: @@ -242,12 +241,13 @@ final class PickFirstLoadBalancerTests: XCTestCase { func testPickFirstConnectionDropReturnsToIdle() async throws { // Checks that when the load balancers connection is unexpectedly dropped when there are no // open streams that it returns to the idle state. - let idleCount = ManagedAtomic(0) + let idleCount = AtomicCounter() try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): - switch idleCount.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) { + let (_, newIdleCount) = idleCount.increment() + switch newIdleCount { case 1: let endpoint = Endpoint(addresses: context.servers.map { $0.address }) context.pickFirst!.updateEndpoint(endpoint) @@ -277,11 +277,12 @@ final class PickFirstLoadBalancerTests: XCTestCase { } func testPickFirstReceivesGoAway() async throws { - let idleCount = ManagedAtomic(0) + let idleCount = AtomicCounter() try await LoadBalancerTest.pickFirst(servers: 2, connector: .posix()) { context, event in switch event { case .connectivityStateChanged(.idle): - switch idleCount.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) { + let (_, newIdleCount) = idleCount.increment() + switch newIdleCount { case 1: // Provide the address of the first server. context.pickFirst!.updateEndpoint(Endpoint(context.servers[0].address)) @@ -293,7 +294,7 @@ final class PickFirstLoadBalancerTests: XCTestCase { } case .connectivityStateChanged(.ready): - switch idleCount.load(ordering: .sequentiallyConsistent) { + switch idleCount.value { case 1: // Must be connected to server 1, send a GOAWAY frame. let channel = context.servers[0].server.clients.first! diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift index db15b52c8..b53e038b7 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -import Atomics import GRPCCore import GRPCHTTP2Core import NIOHTTP2 @@ -322,8 +321,8 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } func testPickOnIdleLoadBalancerTriggersConnect() async throws { - let idle = ManagedAtomic(0) - let ready = ManagedAtomic(0) + let idle = AtomicCounter() + let ready = AtomicCounter() try await LoadBalancerTest.roundRobin( servers: 1, @@ -331,9 +330,9 @@ final class RoundRobinLoadBalancerTests: XCTestCase { ) { context, event in switch event { case .connectivityStateChanged(.idle): - let idleCount = idle.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) + let (_, newIdleCount) = idle.increment() - switch idleCount { + switch newIdleCount { case 1: // The first idle happens when the load balancer in started, give it a set of addresses // which it will connect to. Wait for it to be ready and then idle again. @@ -354,9 +353,9 @@ final class RoundRobinLoadBalancerTests: XCTestCase { } case .connectivityStateChanged(.ready): - let readyCount = ready.wrappingIncrementThenLoad(ordering: .sequentiallyConsistent) + let (_, newReadyCount) = ready.increment() - if readyCount == 2 { + if newReadyCount == 2 { XCTAssertNotNil(context.loadBalancer.pickSubchannel()) } diff --git a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift index d510031e3..180bb030f 100644 --- a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import GRPCHTTP2Core +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ProcessUniqueIDTests: XCTestCase { func testProcessUniqueIDIsUnique() { var ids: Set = [] diff --git a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift index 65f9013c5..eb920976c 100644 --- a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -import Atomics import GRPCCore import GRPCHTTP2Core import NIOEmbedded @@ -62,25 +61,25 @@ internal final class TimerTests: XCTestCase { let loop = EmbeddedEventLoop() defer { try! loop.close() } - let value = Atomic(0) + let counter = AtomicCounter() var timer = Timer(delay: .seconds(1), repeat: true) timer.schedule(on: loop) { - value.add(1, ordering: .releasing) + counter.increment() } loop.advanceTime(by: .milliseconds(999)) - XCTAssertEqual(value.load(ordering: .acquiring), 0) + XCTAssertEqual(counter.value, 0) loop.advanceTime(by: .milliseconds(1)) - XCTAssertEqual(value.load(ordering: .acquiring), 1) + XCTAssertEqual(counter.value, 1) loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(value.load(ordering: .acquiring), 2) + XCTAssertEqual(counter.value, 2) loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(value.load(ordering: .acquiring), 3) + XCTAssertEqual(counter.value, 3) timer.cancel() loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(value.load(ordering: .acquiring), 3) + XCTAssertEqual(counter.value, 3) } func testCancelRepeatedTimer() { diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift new file mode 100644 index 000000000..b9e9fb5b8 --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift @@ -0,0 +1,35 @@ +/* + * 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 Synchronization + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class AtomicCounter: Sendable { + private let counter: Atomic + + init(_ initialValue: Int = 0) { + self.counter = Atomic(initialValue) + } + + var value: Int { + self.counter.load(ordering: .sequentiallyConsistent) + } + + @discardableResult + func increment() -> (oldValue: Int, newValue: Int) { + self.counter.add(1, ordering: .sequentiallyConsistent) + } +} From 6f396ca3aed37ecb4f9c7b4d6fbb0b98df883860 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Sep 2024 14:31:47 +0100 Subject: [PATCH 452/580] Use SwiftProtobuf's new CodeGenerator interface (#2043) Motivation: SwiftProtobuf 1.27.0 added a new `CodeGenerator` interface in 1.27.0 and deprecated the old API. This didn't include (non-deprecated) access to the source proto which is required for reflection data, however, this was added in 1.28.0. Modification: - Rename `options.swift` to `Options.swift` - Rewrite `main` as `GenerateGRPC`, the functionality is unchanged but did require a bit of code shuffling. As part of this some global methods became private methods on the new `GenerateGRPC` `struct`. - Add support for protobuf editions. Result: - Fewer warnings - Can use protobuf editions --- Package.swift | 2 +- Package@swift-6.swift | 2 +- .../protoc-gen-grpc-swift/GenerateGRPC.swift | 235 ++++++++++++++++++ .../{options.swift => Options.swift} | 18 +- Sources/protoc-gen-grpc-swift/main.swift | 230 ----------------- 5 files changed, 251 insertions(+), 236 deletions(-) create mode 100644 Sources/protoc-gen-grpc-swift/GenerateGRPC.swift rename Sources/protoc-gen-grpc-swift/{options.swift => Options.swift} (94%) delete mode 100644 Sources/protoc-gen-grpc-swift/main.swift diff --git a/Package.swift b/Package.swift index 43d1b987c..1e2da3eba 100644 --- a/Package.swift +++ b/Package.swift @@ -56,7 +56,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.27.0" + from: "1.28.1" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Package@swift-6.swift b/Package@swift-6.swift index f42e48fa5..6b41375d6 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -56,7 +56,7 @@ let packageDependencies: [Package.Dependency] = [ ), .package( url: "https://github.com/apple/swift-protobuf.git", - from: "1.27.0" + from: "1.28.1" ), .package( url: "https://github.com/apple/swift-log.git", diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift new file mode 100644 index 000000000..d700c2761 --- /dev/null +++ b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift @@ -0,0 +1,235 @@ +/* + * 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 Foundation +import SwiftProtobuf +import SwiftProtobufPluginLibrary + +#if compiler(>=6.0) +import GRPCCodeGen +import GRPCProtobufCodeGen +#endif + +@main +final class GenerateGRPC: CodeGenerator { + var version: String? { + Version.versionString + } + + var projectURL: String { + "https://github.com/grpc/grpc-swift" + } + + var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { + [.proto3Optional, .supportsEditions] + } + + var supportedEditionRange: ClosedRange { + Google_Protobuf_Edition.proto2 ... Google_Protobuf_Edition.edition2023 + } + + // A count of generated files by desired name (actual name may differ to avoid collisions). + private var generatedFileNames: [String: Int] = [:] + + func generate( + files fileDescriptors: [FileDescriptor], + parameter: any CodeGeneratorParameter, + protoCompilerContext: any ProtoCompilerContext, + generatorOutputs outputs: any GeneratorOutputs + ) throws { + let options = try GeneratorOptions(parameter: parameter) + + for descriptor in fileDescriptors { + if options.generateReflectionData { + try self.generateReflectionData( + descriptor, + options: options, + outputs: outputs + ) + } + + if descriptor.services.isEmpty { + continue + } + + if options.generateClient || options.generateServer || options.generateTestClient { + #if compiler(>=6.0) + if options.v2 { + try self.generateV2Stubs(descriptor, options: options, outputs: outputs) + } else { + try self.generateV1Stubs(descriptor, options: options, outputs: outputs) + } + #else + try self.generateV1Stubs(descriptor, options: options, outputs: outputs) + #endif + } + } + } + + private func generateReflectionData( + _ descriptor: FileDescriptor, + options: GeneratorOptions, + outputs: any GeneratorOutputs + ) throws { + let fileName = self.uniqueOutputFileName( + fileDescriptor: descriptor, + fileNamingOption: options.fileNaming, + extension: "reflection" + ) + + var options = ExtractProtoOptions() + options.includeSourceCodeInfo = true + let proto = descriptor.extractProto(options: options) + let serializedProto = try proto.serializedData() + let reflectionData = serializedProto.base64EncodedString() + try outputs.add(fileName: fileName, contents: reflectionData) + } + + private func generateV1Stubs( + _ descriptor: FileDescriptor, + options: GeneratorOptions, + outputs: any GeneratorOutputs + ) throws { + let fileName = self.uniqueOutputFileName( + fileDescriptor: descriptor, + fileNamingOption: options.fileNaming + ) + + let fileGenerator = Generator(descriptor, options: options) + try outputs.add(fileName: fileName, contents: fileGenerator.code) + } + + #if compiler(>=6.0) + private func generateV2Stubs( + _ descriptor: FileDescriptor, + options: GeneratorOptions, + outputs: any GeneratorOutputs + ) throws { + let fileName = self.uniqueOutputFileName( + fileDescriptor: descriptor, + fileNamingOption: options.fileNaming + ) + + let config = SourceGenerator.Configuration(options: options) + let fileGenerator = ProtobufCodeGenerator(configuration: config) + let contents = try fileGenerator.generateCode( + from: descriptor, + protoFileModuleMappings: options.protoToModuleMappings, + extraModuleImports: options.extraModuleImports + ) + + try outputs.add(fileName: fileName, contents: contents) + } + #endif +} + +extension GenerateGRPC { + private func uniqueOutputFileName( + fileDescriptor: FileDescriptor, + fileNamingOption: FileNaming, + component: String = "grpc", + extension: String = "swift" + ) -> String { + let defaultName = outputFileName( + component: component, + fileDescriptor: fileDescriptor, + fileNamingOption: fileNamingOption, + extension: `extension` + ) + if let count = self.generatedFileNames[defaultName] { + self.generatedFileNames[defaultName] = count + 1 + return outputFileName( + component: "\(count)." + component, + fileDescriptor: fileDescriptor, + fileNamingOption: fileNamingOption, + extension: `extension` + ) + } else { + self.generatedFileNames[defaultName] = 1 + return defaultName + } + } + + private func outputFileName( + component: String, + fileDescriptor: FileDescriptor, + fileNamingOption: FileNaming, + extension: String + ) -> String { + let ext = "." + component + "." + `extension` + let pathParts = splitPath(pathname: fileDescriptor.name) + switch fileNamingOption { + case .fullPath: + return pathParts.dir + pathParts.base + ext + case .pathToUnderscores: + let dirWithUnderscores = + pathParts.dir.replacingOccurrences(of: "/", with: "_") + return dirWithUnderscores + pathParts.base + ext + case .dropPath: + return pathParts.base + ext + } + } +} + +// from apple/swift-protobuf/Sources/protoc-gen-swift/StringUtils.swift +private func splitPath(pathname: String) -> (dir: String, base: String, suffix: String) { + var dir = "" + var base = "" + var suffix = "" + + for character in pathname { + if character == "/" { + dir += base + suffix + String(character) + base = "" + suffix = "" + } else if character == "." { + base += suffix + suffix = String(character) + } else { + suffix += String(character) + } + } + + let validSuffix = suffix.isEmpty || suffix.first == "." + if !validSuffix { + base += suffix + suffix = "" + } + return (dir: dir, base: base, suffix: suffix) +} + +#if compiler(>=6.0) +extension SourceGenerator.Configuration { + init(options: GeneratorOptions) { + let accessLevel: SourceGenerator.Configuration.AccessLevel + switch options.visibility { + case .internal: + accessLevel = .internal + case .package: + accessLevel = .package + case .public: + accessLevel = .public + } + + self.init( + accessLevel: accessLevel, + accessLevelOnImports: options.useAccessLevelOnImports, + client: options.generateClient, + server: options.generateServer + ) + } +} +#endif diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/Options.swift similarity index 94% rename from Sources/protoc-gen-grpc-swift/options.swift rename to Sources/protoc-gen-grpc-swift/Options.swift index ead277108..278864dcf 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/Options.swift @@ -36,7 +36,13 @@ enum GenerationError: Error { } } -final class GeneratorOptions { +enum FileNaming: String { + case fullPath = "FullPath" + case pathToUnderscores = "PathToUnderscores" + case dropPath = "DropPath" +} + +struct GeneratorOptions { enum Visibility: String { case `internal` = "Internal" case `public` = "Public" @@ -63,7 +69,7 @@ final class GeneratorOptions { private(set) var keepMethodCasing = false private(set) var protoToModuleMappings = ProtoFileToModuleMappings() - private(set) var fileNaming = FileNaming.FullPath + private(set) var fileNaming = FileNaming.fullPath private(set) var extraModuleImports: [String] = [] private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" @@ -73,8 +79,12 @@ final class GeneratorOptions { #endif private(set) var useAccessLevelOnImports = true - init(parameter: String?) throws { - for pair in GeneratorOptions.parseParameter(string: parameter) { + init(parameter: any CodeGeneratorParameter) throws { + try self.init(pairs: parameter.parsedPairs) + } + + init(pairs: [(key: String, value: String)]) throws { + for pair in pairs { switch pair.key { case "Visibility": if let value = Visibility(rawValue: pair.value) { diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift deleted file mode 100644 index b53d2f2eb..000000000 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2017, 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 Foundation -import SwiftProtobuf -import SwiftProtobufPluginLibrary - -#if compiler(>=6.0) -import GRPCCodeGen -import GRPCProtobufCodeGen -#endif - -func Log(_ message: String) { - FileHandle.standardError.write((message + "\n").data(using: .utf8)!) -} - -// from apple/swift-protobuf/Sources/protoc-gen-swift/StringUtils.swift -func splitPath(pathname: String) -> (dir: String, base: String, suffix: String) { - var dir = "" - var base = "" - var suffix = "" - #if swift(>=3.2) - let pathnameChars = pathname - #else - let pathnameChars = pathname.characters - #endif - for c in pathnameChars { - if c == "/" { - dir += base + suffix + String(c) - base = "" - suffix = "" - } else if c == "." { - base += suffix - suffix = String(c) - } else { - suffix += String(c) - } - } - #if swift(>=3.2) - let validSuffix = suffix.isEmpty || suffix.first == "." - #else - let validSuffix = suffix.isEmpty || suffix.characters.first == "." - #endif - if !validSuffix { - base += suffix - suffix = "" - } - return (dir: dir, base: base, suffix: suffix) -} - -enum FileNaming: String { - case FullPath - case PathToUnderscores - case DropPath -} - -func outputFileName( - component: String, - fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming, - extension: String -) -> String { - let ext = "." + component + "." + `extension` - let pathParts = splitPath(pathname: fileDescriptor.name) - switch fileNamingOption { - case .FullPath: - return pathParts.dir + pathParts.base + ext - case .PathToUnderscores: - let dirWithUnderscores = - pathParts.dir.replacingOccurrences(of: "/", with: "_") - return dirWithUnderscores + pathParts.base + ext - case .DropPath: - return pathParts.base + ext - } -} - -func uniqueOutputFileName( - component: String, - fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming, - generatedFiles: inout [String: Int], - extension: String = "swift" -) -> String { - let defaultName = outputFileName( - component: component, - fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption, - extension: `extension` - ) - if let count = generatedFiles[defaultName] { - generatedFiles[defaultName] = count + 1 - return outputFileName( - component: "\(count)." + component, - fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption, - extension: `extension` - ) - } else { - generatedFiles[defaultName] = 1 - return defaultName - } -} - -func printVersion(args: [String]) { - // Stip off the file path - let program = args.first?.split(separator: "/").last ?? "protoc-gen-grpc-swift" - print("\(program) \(Version.versionString)") -} - -func main(args: [String]) throws { - if args.dropFirst().contains("--version") { - printVersion(args: args) - return - } - - // initialize responses - var response = Google_Protobuf_Compiler_CodeGeneratorResponse( - files: [], - supportedFeatures: [.proto3Optional] - ) - - // read plugin input - let rawRequest = FileHandle.standardInput.readDataToEndOfFile() - let request = try Google_Protobuf_Compiler_CodeGeneratorRequest(serializedData: rawRequest) - - let options = try GeneratorOptions(parameter: request.parameter) - - // Build the SwiftProtobufPluginLibrary model of the plugin input - let descriptorSet = DescriptorSet(protos: request.protoFile) - - // A count of generated files by desired name (actual name may differ to avoid collisions). - var generatedFiles: [String: Int] = [:] - - // Only generate output for services. - for name in request.fileToGenerate { - if let fileDescriptor = descriptorSet.fileDescriptor(named: name) { - if options.generateReflectionData { - var binaryFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() - let binaryFileName = uniqueOutputFileName( - component: "grpc", - fileDescriptor: fileDescriptor, - fileNamingOption: options.fileNaming, - generatedFiles: &generatedFiles, - extension: "reflection" - ) - let serializedFileDescriptorProto = try fileDescriptor.proto.serializedData() - .base64EncodedString() - binaryFile.name = binaryFileName - binaryFile.content = serializedFileDescriptorProto - response.file.append(binaryFile) - } - if !fileDescriptor.services.isEmpty - && (options.generateClient || options.generateServer || options.generateTestClient) - { - var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() - let grpcFileName = uniqueOutputFileName( - component: "grpc", - fileDescriptor: fileDescriptor, - fileNamingOption: options.fileNaming, - generatedFiles: &generatedFiles - ) - - #if compiler(>=6.0) - if options.v2 { - let grpcGenerator = ProtobufCodeGenerator( - configuration: SourceGenerator.Configuration(options: options) - ) - grpcFile.content = try grpcGenerator.generateCode( - from: fileDescriptor, - protoFileModuleMappings: options.protoToModuleMappings, - extraModuleImports: options.extraModuleImports - ) - } else { - let grpcGenerator = Generator(fileDescriptor, options: options) - grpcFile.content = grpcGenerator.code - } - #else - let grpcGenerator = Generator(fileDescriptor, options: options) - grpcFile.content = grpcGenerator.code - #endif - grpcFile.name = grpcFileName - response.file.append(grpcFile) - } - } - } - - // return everything to the caller - let serializedResponse = try response.serializedData() - FileHandle.standardOutput.write(serializedResponse) -} - -do { - try main(args: CommandLine.arguments) -} catch { - Log("ERROR: \(error)") -} - -#if compiler(>=6.0) -extension SourceGenerator.Configuration { - init(options: GeneratorOptions) { - let accessLevel: SourceGenerator.Configuration.AccessLevel - switch options.visibility { - case .internal: - accessLevel = .internal - case .package: - accessLevel = .package - case .public: - accessLevel = .public - } - self.init( - accessLevel: accessLevel, - accessLevelOnImports: options.useAccessLevelOnImports, - client: options.generateClient, - server: options.generateServer - ) - } -} -#endif From d4d1a2ef1daec8ffe3093033e4ac6d1dffa44269 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 5 Sep 2024 14:44:53 +0100 Subject: [PATCH 453/580] Make 'finish()' 'async' (#2044) Motivation: Finishing writes should be `async` as the underlying writer may need to flush and write out any buffered data. Modifications: - Mark `finish()` as `async` - Refactor the in-proc client transport slightly to avoid async calls while holding a lock Result: `finish` is `async` --- .../Internal/ClientStreamExecutor.swift | 4 +- .../Server/Internal/ServerRPCExecutor.swift | 4 +- Sources/GRPCCore/Call/Server/RPCRouter.swift | 2 +- .../Streaming/RPCWriter+Closable.swift | 8 +- .../Streaming/RPCWriterProtocol.swift | 4 +- .../InProcessClientTransport.swift | 92 ++++++++++--------- ...PCExecutorTestHarness+ServerBehavior.swift | 6 +- .../Internal/ServerRPCExecutorTests.swift | 32 +++---- Tests/GRPCCoreTests/GRPCServerTests.swift | 20 ++-- .../Client/Connection/GRPCChannelTests.swift | 6 +- .../InProcessClientTransportTests.swift | 4 +- 11 files changed, 95 insertions(+), 87 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 749969b2d..472639835 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -95,9 +95,9 @@ internal enum ClientStreamExecutor { switch result { case .success: - stream.finish() + await stream.finish() case .failure(let error): - stream.finish(throwing: error) + await stream.finish(throwing: error) } } diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index f2261f1fc..49e3b713e 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -58,7 +58,7 @@ struct ServerRPCExecutor { // Stream can't be handled; write an error status and close. let status = Status(code: Status.Code(error.code), message: error.message) try? await stream.outbound.write(.status(status, error.metadata)) - stream.outbound.finish() + await stream.outbound.finish() } } @@ -231,7 +231,7 @@ struct ServerRPCExecutor { } try? await outbound.write(.status(status, metadata)) - outbound.finish() + await outbound.finish() } @inlinable diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index e6670d523..b6ae3f074 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -155,7 +155,7 @@ extension RPCRouter { // If this throws then the stream must be closed which we can't do anything about, so ignore // any error. try? await stream.outbound.write(.status(.rpcNotImplemented, [:])) - stream.outbound.finish() + await stream.outbound.finish() } } } diff --git a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift index 7462766b7..dda689464 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -55,8 +55,8 @@ extension RPCWriter { /// All writes after ``finish()`` has been called should result in an error /// being thrown. @inlinable - public func finish() { - self.writer.finish() + public func finish() async { + await self.writer.finish() } /// Indicate to the writer that no more writes are to be accepted because an error occurred. @@ -64,8 +64,8 @@ extension RPCWriter { /// All writes after ``finish(throwing:)`` has been called should result in an error /// being thrown. @inlinable - public func finish(throwing error: any Error) { - self.writer.finish(throwing: error) + public func finish(throwing error: any Error) async { + await self.writer.finish(throwing: error) } } } diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index b2607f733..5841f5802 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -57,13 +57,13 @@ public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// /// All writes after ``finish()`` has been called should result in an error /// being thrown. - func finish() + func finish() async /// Indicate to the writer that no more writes are to be accepted because an error occurred. /// /// All writes after ``finish(throwing:)`` has been called should result in an error /// being thrown. - func finish(throwing error: any Error) + func finish(throwing error: any Error) async } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index aded232a2..ddbb4a8bd 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -179,8 +179,8 @@ public final class InProcessClientTransport: ClientTransport { } for (clientStream, serverStream) in openStreams { - clientStream.outbound.finish(throwing: CancellationError()) - serverStream.outbound.finish(throwing: CancellationError()) + await clientStream.outbound.finish(throwing: CancellationError()) + await serverStream.outbound.finish(throwing: CancellationError()) } } @@ -265,7 +265,7 @@ public final class InProcessClientTransport: ClientTransport { try Task.checkCancellation() } - let streamID = try self.state.withLock { state in + let acceptStream: Result = self.state.withLock { state in switch state { case .unconnected: // The state cannot be unconnected because if it was, then the above @@ -281,56 +281,64 @@ public final class InProcessClientTransport: ClientTransport { connectedState.openStreams[streamID] = (clientStream, serverStream) connectedState.nextStreamID += 1 state = .connected(connectedState) + return .success(streamID) } catch let acceptStreamError as RPCError { - serverStream.outbound.finish(throwing: acceptStreamError) - clientStream.outbound.finish(throwing: acceptStreamError) - throw acceptStreamError + return .failure(acceptStreamError) } catch { - serverStream.outbound.finish(throwing: error) - clientStream.outbound.finish(throwing: error) - throw RPCError(code: .unknown, message: "Unknown error: \(error).") + return .failure(RPCError(code: .unknown, message: "Unknown error: \(error).")) } - return streamID case .closed: - let error = RPCError( - code: .failedPrecondition, - message: "The client transport is closed." - ) - serverStream.outbound.finish(throwing: error) - clientStream.outbound.finish(throwing: error) - throw error + let error = RPCError(code: .failedPrecondition, message: "The client transport is closed.") + return .failure(error) } } - defer { - clientStream.outbound.finish() - - let maybeEndContinuation = self.state.withLock { state in - switch state { - case .unconnected: - // The state cannot be unconnected at this point, because if we made - // it this far, it's because the transport was connected. - // Once connected, it's impossible to transition back to unconnected, - // so this is an invalid state. - fatalError("Invalid state") - case .connected(var connectedState): - connectedState.openStreams.removeValue(forKey: streamID) - state = .connected(connectedState) - case .closed(var closedState): - closedState.openStreams.removeValue(forKey: streamID) - state = .closed(closedState) - if closedState.openStreams.isEmpty { - // This was the last open stream: signal the closure of the client. - return closedState.signalEndContinuation - } - } - return nil + switch acceptStream { + case .success(let streamID): + let streamHandlingResult: Result + do { + let result = try await closure(clientStream) + streamHandlingResult = .success(result) + } catch { + streamHandlingResult = .failure(error) } - maybeEndContinuation?.finish() + + await clientStream.outbound.finish() + self.removeStream(id: streamID) + + return try streamHandlingResult.get() + + case .failure(let error): + await serverStream.outbound.finish(throwing: error) + await clientStream.outbound.finish(throwing: error) + throw error } + } - return try await closure(clientStream) + private func removeStream(id streamID: Int) { + let maybeEndContinuation = self.state.withLock { state in + switch state { + case .unconnected: + // The state cannot be unconnected at this point, because if we made + // it this far, it's because the transport was connected. + // Once connected, it's impossible to transition back to unconnected, + // so this is an invalid state. + fatalError("Invalid state") + case .connected(var connectedState): + connectedState.openStreams.removeValue(forKey: streamID) + state = .connected(connectedState) + case .closed(var closedState): + closedState.openStreams.removeValue(forKey: streamID) + state = .closed(closedState) + if closedState.openStreams.isEmpty { + // This was the last open stream: signal the closure of the client. + return closedState.signalEndContinuation + } + } + return nil + } + maybeEndContinuation?.finish() } /// Returns the execution configuration for a given method. diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index 4f0ab1b14..0c2ab936f 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -74,7 +74,7 @@ extension ClientRPCExecutorTestHarness.ServerStreamHandler { try await stream.outbound.write(contentsOf: response) try await stream.outbound.write(.status(Status(code: .ok, message: ""), [:])) - stream.outbound.finish() + await stream.outbound.finish() } } @@ -90,7 +90,7 @@ extension ClientRPCExecutorTestHarness.ServerStreamHandler { // All error codes are valid status codes, '!' is safe. let status = Status(code: Status.Code(error.code), message: error.message) try await stream.outbound.write(.status(status, error.metadata)) - stream.outbound.finish() + await stream.outbound.finish() } } @@ -99,7 +99,7 @@ extension ClientRPCExecutorTestHarness.ServerStreamHandler { XCTFail("Server accepted unexpected stream") let status = Status(code: .unknown, message: "Unexpected stream") try await stream.outbound.write(.status(status, [:])) - stream.outbound.finish() + await stream.outbound.finish() } } diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 0393de2c9..5d2aa0029 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -24,7 +24,7 @@ final class ServerRPCExecutorTests: XCTestCase { let harness = ServerRPCExecutorTestHarness() try await harness.execute(handler: .echo) { inbound in try await inbound.write(.metadata(["foo": "bar"])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual( @@ -42,7 +42,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute(handler: .echo) { inbound in try await inbound.write(.metadata(["foo": "bar"])) try await inbound.write(.message([0])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual( @@ -63,7 +63,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await inbound.write(.message([0])) try await inbound.write(.message([1])) try await inbound.write(.message([2])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual( @@ -94,7 +94,7 @@ final class ServerRPCExecutorTests: XCTestCase { } producer: { inbound in try await inbound.write(.metadata(["foo": "bar"])) try await inbound.write(.message(Array("\"hello\"".utf8))) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual( @@ -125,7 +125,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await inbound.write(.metadata(["foo": "bar"])) try await inbound.write(.message(Array("\"hello\"".utf8))) try await inbound.write(.message(Array("\"world\"".utf8))) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual( @@ -151,7 +151,7 @@ final class ServerRPCExecutorTests: XCTestCase { } } producer: { inbound in try await inbound.write(.metadata(["foo": "bar"])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual( @@ -167,7 +167,7 @@ final class ServerRPCExecutorTests: XCTestCase { func testEmptyInbound() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute(handler: .echo) { inbound in - inbound.finish() + await inbound.finish() } consumer: { outbound in let part = try await outbound.collect().first XCTAssertStatus(part) { status, _ in @@ -180,7 +180,7 @@ final class ServerRPCExecutorTests: XCTestCase { let harness = ServerRPCExecutorTestHarness() try await harness.execute(handler: .echo) { inbound in try await inbound.write(.message([0])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let part = try await outbound.collect().first XCTAssertStatus(part) { status, _ in @@ -192,7 +192,7 @@ final class ServerRPCExecutorTests: XCTestCase { func testInboundStreamThrows() async throws { let harness = ServerRPCExecutorTestHarness() try await harness.execute(handler: .echo) { inbound in - inbound.finish(throwing: RPCError(code: .aborted, message: "")) + await inbound.finish(throwing: RPCError(code: .aborted, message: "")) } consumer: { outbound in let part = try await outbound.collect().first XCTAssertStatus(part) { status, _ in @@ -206,7 +206,7 @@ final class ServerRPCExecutorTests: XCTestCase { let harness = ServerRPCExecutorTestHarness() try await harness.execute(handler: .throwing(SomeError())) { inbound in try await inbound.write(.metadata([:])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let part = try await outbound.collect().first XCTAssertStatus(part) { status, _ in @@ -220,7 +220,7 @@ final class ServerRPCExecutorTests: XCTestCase { let harness = ServerRPCExecutorTestHarness() try await harness.execute(handler: .throwing(error)) { inbound in try await inbound.write(.metadata([:])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let part = try await outbound.collect().first XCTAssertStatus(part) { status, metadata in @@ -247,7 +247,7 @@ final class ServerRPCExecutorTests: XCTestCase { return ServerResponse.Stream(error: RPCError(code: .failedPrecondition, message: "")) } producer: { inbound in try await inbound.write(.metadata(["grpc-timeout": "1000n"])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let part = try await outbound.collect().first XCTAssertStatus(part) { status, _ in @@ -277,7 +277,7 @@ final class ServerRPCExecutorTests: XCTestCase { ) } producer: { inbound in try await inbound.write(.metadata([:])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let part = try await outbound.collect().first XCTAssertStatus(part) { status, metadata in @@ -302,7 +302,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute(handler: .echo) { inbound in try await inbound.write(.metadata([:])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual(parts, [.metadata([:]), .status(.ok, [:])]) @@ -327,7 +327,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute(handler: .echo) { inbound in try await inbound.write(.metadata([:])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual(parts, [.status(Status(code: .unavailable, message: ""), [:])]) @@ -345,7 +345,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute(handler: .echo) { inbound in try await inbound.write(.metadata([:])) - inbound.finish() + await inbound.finish() } consumer: { outbound in let parts = try await outbound.collect() XCTAssertEqual(parts, [.status(Status(code: .unavailable, message: "Unavailable"), [:])]) diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index f5afcb2a1..d8771d81e 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -55,7 +55,7 @@ final class GRPCServerTests: XCTestCase { ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3, 1, 4, 1, 5])) - stream.outbound.finish() + await stream.outbound.finish() var responseParts = stream.inbound.makeAsyncIterator() let metadata = try await responseParts.next() @@ -86,7 +86,7 @@ final class GRPCServerTests: XCTestCase { try await stream.outbound.write(.message([4])) try await stream.outbound.write(.message([1])) try await stream.outbound.write(.message([5])) - stream.outbound.finish() + await stream.outbound.finish() var responseParts = stream.inbound.makeAsyncIterator() let metadata = try await responseParts.next() @@ -113,7 +113,7 @@ final class GRPCServerTests: XCTestCase { ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([3, 1, 4, 1, 5])) - stream.outbound.finish() + await stream.outbound.finish() var responseParts = stream.inbound.makeAsyncIterator() let metadata = try await responseParts.next() @@ -144,7 +144,7 @@ final class GRPCServerTests: XCTestCase { for byte in [3, 1, 4, 1, 5] as [UInt8] { try await stream.outbound.write(.message([byte])) } - stream.outbound.finish() + await stream.outbound.finish() var responseParts = stream.inbound.makeAsyncIterator() let metadata = try await responseParts.next() @@ -172,7 +172,7 @@ final class GRPCServerTests: XCTestCase { options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) - stream.outbound.finish() + await stream.outbound.finish() var responseParts = stream.inbound.makeAsyncIterator() let status = try await responseParts.next() @@ -194,7 +194,7 @@ final class GRPCServerTests: XCTestCase { ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([i])) - stream.outbound.finish() + await stream.outbound.finish() var responseParts = stream.inbound.makeAsyncIterator() let metadata = try await responseParts.next() @@ -231,7 +231,7 @@ final class GRPCServerTests: XCTestCase { options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) - stream.outbound.finish() + await stream.outbound.finish() let parts = try await stream.inbound.collect() XCTAssertStatus(parts.first) { status, _ in @@ -256,7 +256,7 @@ final class GRPCServerTests: XCTestCase { options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) - stream.outbound.finish() + await stream.outbound.finish() let parts = try await stream.inbound.collect() XCTAssertStatus(parts.first) { status, _ in @@ -306,7 +306,7 @@ final class GRPCServerTests: XCTestCase { server.beginGracefulShutdown() try await stream.outbound.write(.message([0])) - stream.outbound.finish() + await stream.outbound.finish() let message = try await iterator.next() XCTAssertMessage(message) { XCTAssertEqual($0, [0]) } @@ -368,7 +368,7 @@ final class GRPCServerTests: XCTestCase { ) { stream in try await stream.outbound.write(.metadata([:])) try await stream.outbound.write(.message([0])) - stream.outbound.finish() + await stream.outbound.finish() // Don't need to validate the response, just that the server is running. let parts = try await stream.inbound.collect() XCTAssertEqual(parts.count, 3) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index 391e1e624..96a172311 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -371,7 +371,7 @@ final class GRPCChannelTests: XCTestCase { switch state { case .shutdown: // Happens when shutting-down has been initiated, so finish the RPC. - stream.outbound.finish() + await stream.outbound.finish() let part2 = try await iterator.next() switch part2 { @@ -444,7 +444,7 @@ final class GRPCChannelTests: XCTestCase { group.addTask { try await channel.withStream(descriptor: .echoGet, options: .defaults) { stream in try await stream.outbound.write(.metadata([:])) - stream.outbound.finish() + await stream.outbound.finish() for try await part in stream.inbound { switch part { @@ -824,7 +824,7 @@ extension GRPCChannel { options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) - stream.outbound.finish() + await stream.outbound.finish() for try await part in stream.inbound { switch part { diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 1209bc9d1..d579ca532 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -163,7 +163,7 @@ final class InProcessClientTransportTests: XCTestCase { options: .defaults ) { stream in try await stream.outbound.write(.message([1])) - stream.outbound.finish() + await stream.outbound.finish() let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } XCTAssertEqual(receivedMessages, [.message([42])]) @@ -174,7 +174,7 @@ final class InProcessClientTransportTests: XCTestCase { try await server.listen { stream in let receivedMessages = try? await stream.inbound.reduce(into: []) { $0.append($1) } try? await stream.outbound.write(RPCResponsePart.message([42])) - stream.outbound.finish() + await stream.outbound.finish() XCTAssertEqual(receivedMessages, [.message([1])]) } From 1b060a32100d125a31e554d38c78b5b556fc1784 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 6 Sep 2024 11:57:20 +0100 Subject: [PATCH 454/580] Enable `InternalImportsByDefault` in remaining modules (#2042) This PR enables `InternalImportsByDefault` on `GRPCHealth` and `InteroperabilityTests`. It also regenerates the protos with the latest version of swift-protobuf (v1.28.1), and increases its min version to 1.28.1 to make sure that we don't get build failures when using earlier generators. --- Package@swift-6.swift | 6 ++++-- Protos/generate.sh | 12 ++++++------ .../InteroperabilityTests/Generated/empty.pb.swift | 2 +- .../Generated/messages.pb.swift | 4 ++-- .../InteroperabilityTestCase.swift | 2 +- .../InteroperabilityTestCases.swift | 4 ++-- Sources/InteroperabilityTests/TestService.swift | 4 ++-- Sources/Services/Health/Generated/health.pb.swift | 2 +- Sources/protoc-gen-grpc-swift/Options.swift | 6 ++++++ 9 files changed, 25 insertions(+), 17 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 6b41375d6..7b5b81394 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -550,7 +550,8 @@ extension Target { ], swiftSettings: [ .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny") + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") ] ) } @@ -950,7 +951,8 @@ extension Target { path: "Sources/Services/Health", swiftSettings: [ .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny") + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") ] ) } diff --git a/Protos/generate.sh b/Protos/generate.sh index 6346105b2..5eb6aa313 100755 --- a/Protos/generate.sh +++ b/Protos/generate.sh @@ -29,7 +29,7 @@ bin_path=$(swift build -c release --show-bin-path) protoc_gen_swift="$bin_path/protoc-gen-swift" protoc_generate_grpc_swift="$bin_path/protoc-gen-grpc-swift" -# Genreates gRPC by invoking protoc with the gRPC Swift plugin. +# Generates gRPC by invoking protoc with the gRPC Swift plugin. # Parameters: # - $1: .proto file # - $2: proto path @@ -46,7 +46,7 @@ function generate_grpc { invoke_protoc "${args[@]}" "$proto" } -# Genreates messages by invoking protoc with the Swift plugin. +# Generates messages by invoking protoc with the Swift plugin. # Parameters: # - $1: .proto file # - $2: proto path @@ -228,8 +228,8 @@ function generate_service_messages_interop_tests { local output="$root/Sources/InteroperabilityTests/Generated" for proto in "${protos[@]}"; do - generate_message "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "FileNaming=DropPath" - generate_grpc "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "Server=true" "_V2=true" "FileNaming=DropPath" + generate_message "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "FileNaming=DropPath" "UseAccessLevelOnImports=true" + generate_grpc "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "Server=true" "_V2=true" "FileNaming=DropPath" "UseAccessLevelOnImports=true" done } @@ -260,8 +260,8 @@ function generate_health_service { local proto="$here/upstream/grpc/health/v1/health.proto" local output="$root/Sources/Services/Health/Generated" - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" "Client=true" "Server=true" "_V2=true" + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" "UseAccessLevelOnImports=true" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" "Client=true" "Server=true" "_V2=true" "UseAccessLevelOnImports=true" } #------------------------------------------------------------------------------ diff --git a/Sources/InteroperabilityTests/Generated/empty.pb.swift b/Sources/InteroperabilityTests/Generated/empty.pb.swift index 840c3367f..7e246bf7b 100644 --- a/Sources/InteroperabilityTests/Generated/empty.pb.swift +++ b/Sources/InteroperabilityTests/Generated/empty.pb.swift @@ -22,7 +22,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import SwiftProtobuf +public import SwiftProtobuf // If the compiler emits an error on this type, it is because this file // was generated by a version of the `protoc` Swift plug-in that is diff --git a/Sources/InteroperabilityTests/Generated/messages.pb.swift b/Sources/InteroperabilityTests/Generated/messages.pb.swift index fa2318afd..cd0a3dd15 100644 --- a/Sources/InteroperabilityTests/Generated/messages.pb.swift +++ b/Sources/InteroperabilityTests/Generated/messages.pb.swift @@ -24,8 +24,8 @@ // Message definitions to be used by integration test service definitions. -import Foundation -import SwiftProtobuf +public import Foundation +public import SwiftProtobuf // If the compiler emits an error on this type, it is because this file // was generated by a version of the `protoc` Swift plug-in that is diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift index 74fdcd35b..1c60f1401 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import GRPCCore +public import GRPCCore @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol InteroperabilityTest { diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift index d43e58590..b1be0be50 100644 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -import GRPCCore +internal import GRPCCore -import struct Foundation.Data +private import struct Foundation.Data /// This test verifies that implementations support zero-size messages. Ideally, client /// implementations would verify that the request and response were zero bytes serialized, but diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift index 669cf5c41..02c27c1f3 100644 --- a/Sources/InteroperabilityTests/TestService.swift +++ b/Sources/InteroperabilityTests/TestService.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -import Foundation -import GRPCCore +private import Foundation +public import GRPCCore @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct TestService: Grpc_Testing_TestService.ServiceProtocol { diff --git a/Sources/Services/Health/Generated/health.pb.swift b/Sources/Services/Health/Generated/health.pb.swift index 35bec2020..ea2cde5c6 100644 --- a/Sources/Services/Health/Generated/health.pb.swift +++ b/Sources/Services/Health/Generated/health.pb.swift @@ -25,7 +25,7 @@ // The canonical version of this proto can be found at // https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto -import SwiftProtobuf +package import SwiftProtobuf // If the compiler emits an error on this type, it is because this file // was generated by a version of the `protoc` Swift plug-in that is diff --git a/Sources/protoc-gen-grpc-swift/Options.swift b/Sources/protoc-gen-grpc-swift/Options.swift index 278864dcf..7c7077eff 100644 --- a/Sources/protoc-gen-grpc-swift/Options.swift +++ b/Sources/protoc-gen-grpc-swift/Options.swift @@ -74,10 +74,16 @@ struct GeneratorOptions { private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" private(set) var generateReflectionData = false + #if compiler(>=6.0) private(set) var v2 = false #endif + + #if compiler(>=6.0) private(set) var useAccessLevelOnImports = true + #else + private(set) var useAccessLevelOnImports = false + #endif init(parameter: any CodeGeneratorParameter) throws { try self.init(pairs: parameter.parsedPairs) From d3ef09d71e75c8260f9dcc9a1180dd625b057f75 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 9 Sep 2024 12:22:32 +0100 Subject: [PATCH 455/580] Default `UseAccessLevelOnImports` to `false` (#2047) ## Motivation After the discussion on https://github.com/grpc/grpc-swift/pull/2042#discussion_r1746796122, I played a bit more with `InternalImportsByDefault` and the different generator options, got to the following conclusions: - If you add `import Foo` to some file, where `Foo` is also being imported with an explicit `internal` access-level modifier _in the generated code_, it will work as long as `InternalImportsByDefault` is enabled. - If you disable `InternalImportsByDefault` for the corresponding target in `Package.swift`, you get an `"Ambiguous implicit access level for import of 'Foo'; it is imported as 'internal' elsewhere"` **error** (not a warning). This means that if the code generator plugin(s) begin adding the access level modifiers by default based on how they're built, they could cause source-breakages for users unintentionally. - _This isn't any different between language mode 5 or 6_ - I tried changing the target's language mode and the behaviour is the same as described above in either case. Given all this, defaulting `UseAccessLevelOnImports` to `false` **always** for now may be the easiest (and least surprising, from a users' perspective) thing to do, until `InternalImportsByDefault` are enabled by default in a future Swift >6.0 version (as the proposal states), where we can default to `true` again: ``` #if compiler(>=6.x) // where x is the version where internal imports by default is enabled // default to true #else // default to false #endif ``` The rationale behind this is that adding access levels to imports on your code is currently totally optional. If you choose to start adding them explicitly, then it's okay to also have to tell your tooling/generators that they should also add them explicitly. If you don't, they'll keep generating things the exact same way they've been doing it, which is what users of the generator would expect. ## Modifications - Default `UseAccessLevelOnImports` to `false` always. - Regenerate protos - Remove `InternalImportsByDefault` from test and executable targets, since it doesn't make a lot of sense to have access level modifiers on imports here anyways as these targets cannot be imported. --- Examples/v2/echo/Generated/echo.grpc.swift | 4 ++-- Examples/v2/echo/Subcommands/Collect.swift | 8 ++++---- .../v2/hello-world/Generated/helloworld.grpc.swift | 4 ++-- Examples/v2/hello-world/HelloWorld.swift | 2 +- Examples/v2/hello-world/Subcommands/Greet.swift | 6 +++--- Examples/v2/hello-world/Subcommands/Serve.swift | 6 +++--- .../v2/route-guide/Generated/route_guide.grpc.swift | 4 ++-- Package@swift-6.swift | 12 ++++-------- .../Resources/route-guide-sec03-step04-gen-grpc.txt | 1 - Sources/performance-worker/BenchmarkClient.swift | 8 ++++---- Sources/performance-worker/BenchmarkService.swift | 4 ++-- .../grpc_testing_benchmark_service.grpc.swift | 4 ++-- .../Generated/grpc_testing_worker_service.grpc.swift | 4 ++-- Sources/performance-worker/PerformanceWorker.swift | 10 +++++----- Sources/performance-worker/RPCStats.swift | 6 +++--- Sources/protoc-gen-grpc-swift/Options.swift | 6 ------ Tests/GRPCHTTP2TransportTests/ControlService.swift | 2 +- .../Generated/control.grpc.swift | 4 ++-- .../HTTP2TransportNIOPosixTests.swift | 10 +++++----- .../HTTP2TransportNIOTransportServicesTests.swift | 8 ++++---- .../HTTP2TransportTests.swift | 12 ++++++------ .../Test Utilities/HTTP2StatusCodeServer.swift | 10 +++++----- .../Test Utilities/XCTest+Utilities.swift | 2 +- Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift | 4 ++-- 24 files changed, 65 insertions(+), 76 deletions(-) diff --git a/Examples/v2/echo/Generated/echo.grpc.swift b/Examples/v2/echo/Generated/echo.grpc.swift index 6c5f82d29..856ccc074 100644 --- a/Examples/v2/echo/Generated/echo.grpc.swift +++ b/Examples/v2/echo/Generated/echo.grpc.swift @@ -21,8 +21,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -internal import GRPCCore -internal import GRPCProtobuf +import GRPCCore +import GRPCProtobuf internal enum Echo_Echo { internal static let descriptor = GRPCCore.ServiceDescriptor.echo_Echo diff --git a/Examples/v2/echo/Subcommands/Collect.swift b/Examples/v2/echo/Subcommands/Collect.swift index a617e83b9..63402e0c6 100644 --- a/Examples/v2/echo/Subcommands/Collect.swift +++ b/Examples/v2/echo/Subcommands/Collect.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -internal import ArgumentParser -private import GRPCCore -private import GRPCHTTP2Core -private import GRPCHTTP2TransportNIOPosix +import ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Collect: AsyncParsableCommand { diff --git a/Examples/v2/hello-world/Generated/helloworld.grpc.swift b/Examples/v2/hello-world/Generated/helloworld.grpc.swift index 319c5e65e..0336b83ca 100644 --- a/Examples/v2/hello-world/Generated/helloworld.grpc.swift +++ b/Examples/v2/hello-world/Generated/helloworld.grpc.swift @@ -21,8 +21,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -internal import GRPCCore -internal import GRPCProtobuf +import GRPCCore +import GRPCProtobuf internal enum Helloworld_Greeter { internal static let descriptor = GRPCCore.ServiceDescriptor.helloworld_Greeter diff --git a/Examples/v2/hello-world/HelloWorld.swift b/Examples/v2/hello-world/HelloWorld.swift index 1427fecd7..8d467670a 100644 --- a/Examples/v2/hello-world/HelloWorld.swift +++ b/Examples/v2/hello-world/HelloWorld.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -internal import ArgumentParser +import ArgumentParser @main @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Examples/v2/hello-world/Subcommands/Greet.swift b/Examples/v2/hello-world/Subcommands/Greet.swift index 9d97bb517..aca973814 100644 --- a/Examples/v2/hello-world/Subcommands/Greet.swift +++ b/Examples/v2/hello-world/Subcommands/Greet.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -internal import ArgumentParser -internal import GRPCHTTP2Transport -internal import GRPCProtobuf +import ArgumentParser +import GRPCHTTP2Transport +import GRPCProtobuf @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Greet: AsyncParsableCommand { diff --git a/Examples/v2/hello-world/Subcommands/Serve.swift b/Examples/v2/hello-world/Subcommands/Serve.swift index 1d60c82d7..fe4e462ea 100644 --- a/Examples/v2/hello-world/Subcommands/Serve.swift +++ b/Examples/v2/hello-world/Subcommands/Serve.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -internal import ArgumentParser -internal import GRPCHTTP2Transport -internal import GRPCProtobuf +import ArgumentParser +import GRPCHTTP2Transport +import GRPCProtobuf @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Serve: AsyncParsableCommand { diff --git a/Examples/v2/route-guide/Generated/route_guide.grpc.swift b/Examples/v2/route-guide/Generated/route_guide.grpc.swift index e2fd21a5f..cf7cb7182 100644 --- a/Examples/v2/route-guide/Generated/route_guide.grpc.swift +++ b/Examples/v2/route-guide/Generated/route_guide.grpc.swift @@ -21,8 +21,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -internal import GRPCCore -internal import GRPCProtobuf +import GRPCCore +import GRPCProtobuf internal enum Routeguide_RouteGuide { internal static let descriptor = GRPCCore.ServiceDescriptor.routeguide_RouteGuide diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 7b5b81394..04dee5de7 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -346,8 +346,7 @@ extension Target { ], swiftSettings: [ .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") + .enableUpcomingFeature("ExistentialAny") ] ) } @@ -710,8 +709,7 @@ extension Target { path: "Examples/v2/echo", swiftSettings: [ .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") + .enableUpcomingFeature("ExistentialAny") ] ) } @@ -773,8 +771,7 @@ extension Target { ], swiftSettings: [ .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") + .enableUpcomingFeature("ExistentialAny") ] ) } @@ -837,8 +834,7 @@ extension Target { ], swiftSettings: [ .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") + .enableUpcomingFeature("ExistentialAny") ] ) } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt index 1c084165e..ab2c22fd8 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt @@ -2,5 +2,4 @@ $ protoc --plugin=.build/debug/protoc-gen-grpc-swift \ -I Protos \ --grpc-swift_out=Sources/Generated \ --grpc-swift_opt=_V2=true \ - --grpc-swift_opt=UseAccessLevelOnImports=false \ Protos/route_guide.proto diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift index 3e1318b12..57afa894f 100644 --- a/Sources/performance-worker/BenchmarkClient.swift +++ b/Sources/performance-worker/BenchmarkClient.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -private import Foundation -internal import GRPCCore -private import NIOConcurrencyHelpers -private import Synchronization +import Foundation +import GRPCCore +import NIOConcurrencyHelpers +import Synchronization @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class BenchmarkClient: Sendable { diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift index 112b1f1f0..11631eaaf 100644 --- a/Sources/performance-worker/BenchmarkService.swift +++ b/Sources/performance-worker/BenchmarkService.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -internal import GRPCCore -private import Synchronization +import GRPCCore +import Synchronization import struct Foundation.Data diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 47cbbeccb..663922d0b 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -24,8 +24,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -internal import GRPCCore -internal import GRPCProtobuf +import GRPCCore +import GRPCProtobuf internal enum Grpc_Testing_BenchmarkService { internal static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_BenchmarkService diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index 467e2cdac..f1637253f 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -24,8 +24,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -internal import GRPCCore -internal import GRPCProtobuf +import GRPCCore +import GRPCProtobuf internal enum Grpc_Testing_WorkerService { internal static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_WorkerService diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift index e93bfca60..83c9f7e82 100644 --- a/Sources/performance-worker/PerformanceWorker.swift +++ b/Sources/performance-worker/PerformanceWorker.swift @@ -14,11 +14,11 @@ * limitations under the License. */ -internal import ArgumentParser -private import GRPCCore -private import GRPCHTTP2Core -private import GRPCHTTP2TransportNIOPosix -private import NIOPosix +import ArgumentParser +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix +import NIOPosix @main @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Sources/performance-worker/RPCStats.swift b/Sources/performance-worker/RPCStats.swift index ef0703d9e..bc2bba74b 100644 --- a/Sources/performance-worker/RPCStats.swift +++ b/Sources/performance-worker/RPCStats.swift @@ -14,9 +14,9 @@ * limitations under the License. */ -private import Foundation -internal import GRPCCore -internal import NIOConcurrencyHelpers +import Foundation +import GRPCCore +import NIOConcurrencyHelpers /// Stores the real time latency histogram and error code count dictionary, /// for the RPCs made by a particular GRPCClient. It gets updated after diff --git a/Sources/protoc-gen-grpc-swift/Options.swift b/Sources/protoc-gen-grpc-swift/Options.swift index 7c7077eff..2b0f96d46 100644 --- a/Sources/protoc-gen-grpc-swift/Options.swift +++ b/Sources/protoc-gen-grpc-swift/Options.swift @@ -74,16 +74,10 @@ struct GeneratorOptions { private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" private(set) var generateReflectionData = false - #if compiler(>=6.0) private(set) var v2 = false #endif - - #if compiler(>=6.0) - private(set) var useAccessLevelOnImports = true - #else private(set) var useAccessLevelOnImports = false - #endif init(parameter: any CodeGeneratorParameter) throws { try self.init(pairs: parameter.parsedPairs) diff --git a/Tests/GRPCHTTP2TransportTests/ControlService.swift b/Tests/GRPCHTTP2TransportTests/ControlService.swift index e9d5b322f..dfb83b132 100644 --- a/Tests/GRPCHTTP2TransportTests/ControlService.swift +++ b/Tests/GRPCHTTP2TransportTests/ControlService.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -internal import GRPCCore +import GRPCCore import struct Foundation.Data diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index 6ec4497cf..ebd5c8e76 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -22,8 +22,8 @@ // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift -internal import GRPCCore -internal import GRPCProtobuf +import GRPCCore +import GRPCProtobuf internal enum Control { internal static let descriptor = GRPCCore.ServiceDescriptor.Control diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index 28bc49967..4f6993dde 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -14,13 +14,13 @@ * limitations under the License. */ -private import GRPCCore -private import GRPCHTTP2Core -private import GRPCHTTP2TransportNIOPosix -internal import XCTest +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix +import XCTest #if canImport(NIOSSL) -private import NIOSSL +import NIOSSL #endif @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index be54c0890..8219540ec 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -15,10 +15,10 @@ */ #if canImport(Network) -private import GRPCCore -private import GRPCHTTP2Core -internal import GRPCHTTP2TransportNIOTransportServices -internal import XCTest +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOTransportServices +import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportNIOTransportServicesTests: XCTestCase { diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index b0652c2fe..2de224ffc 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -14,12 +14,12 @@ * limitations under the License. */ -internal import GRPCCore -private import GRPCHTTP2Core -private import GRPCHTTP2TransportNIOPosix -private import GRPCHTTP2TransportNIOTransportServices -private import GRPCProtobuf -internal import XCTest +import GRPCCore +import GRPCHTTP2Core +import GRPCHTTP2TransportNIOPosix +import GRPCHTTP2TransportNIOTransportServices +import GRPCProtobuf +import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportTests: XCTestCase { diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift index 3e1df197e..d4f6e00ee 100644 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift @@ -14,11 +14,11 @@ * limitations under the License. */ -internal import GRPCHTTP2Core -private import NIOCore -private import NIOHPACK -private import NIOHTTP2 -private import NIOPosix +import GRPCHTTP2Core +import NIOCore +import NIOHPACK +import NIOHTTP2 +import NIOPosix /// An HTTP/2 test server which only responds to request headers by sending response headers and /// then closing. Each stream will be closed with the ":status" set to the value of the diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift index 053065da4..3848388bc 100644 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift @@ -14,7 +14,7 @@ * limitations under the License. */ -private import XCTest +import XCTest func XCTAssertThrowsError( ofType: E.Type, diff --git a/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift b/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift index d66676c99..cb613fb63 100644 --- a/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift +++ b/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift @@ -14,8 +14,8 @@ * limitations under the License. */ -private import NIOPosix -internal import XCTest +import NIOPosix +import XCTest extension XCTestCase { func vsockAvailable() -> Bool { From 17b2054451d61c52f4d87e40e5507a149237fdbe Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 9 Sep 2024 13:04:36 +0100 Subject: [PATCH 456/580] Adopt swift-testing for status tests (#2049) Motivation: swift-testing has a number of advantages over XCTest (parameterisation, organisation, failure messages etc.), we should start using it instead of XCTest. Modifications: - Convert the Status tests - Add a dependency on swift-testing on Linux (this can be removed when it's included as part of the toolchain) Results: Better tests --- Package@swift-6.swift | 12 +++ Sources/GRPCCore/RPCError.swift | 19 ++++ Sources/GRPCCore/Status.swift | 20 +++++ Tests/GRPCCoreTests/StatusTests.swift | 125 +++++++++----------------- 4 files changed, 91 insertions(+), 85 deletions(-) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 04dee5de7..1a84f988d 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -72,6 +72,10 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0" ), + .package( + url: "https://github.com/swiftlang/swift-testing.git", + branch: "release/6.0" + ), ].appending( .package( url: "https://github.com/apple/swift-nio-ssl.git", @@ -147,6 +151,13 @@ extension Target.Dependency { static var dequeModule: Self { .product(name: "DequeModule", package: "swift-collections") } static var atomics: Self { .product(name: "Atomics", package: "swift-atomics") } static var tracing: Self { .product(name: "Tracing", package: "swift-distributed-tracing") } + static var testing: Self { + .product( + name: "Testing", + package: "swift-testing", + condition: .when(platforms: [.linux]) // Already included in the toolchain on Darwin + ) + } static var grpcCore: Self { .target(name: "GRPCCore") } static var grpcInProcessTransport: Self { .target(name: "GRPCInProcessTransport") } @@ -401,6 +412,7 @@ extension Target { .grpcInProcessTransport, .dequeModule, .protobuf, + .testing, ], swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index b9147007d..7354a7b83 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -107,6 +107,25 @@ extension RPCError { public var description: String { String(describing: self.wrapped) } + + package static let all: [Self] = [ + .cancelled, + .unknown, + .invalidArgument, + .deadlineExceeded, + .notFound, + .alreadyExists, + .permissionDenied, + .resourceExhausted, + .failedPrecondition, + .aborted, + .outOfRange, + .unimplemented, + .internalError, + .unavailable, + .dataLoss, + .unauthenticated, + ] } } diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift index 738969c0c..d1c9e087b 100644 --- a/Sources/GRPCCore/Status.swift +++ b/Sources/GRPCCore/Status.swift @@ -166,6 +166,26 @@ extension Status { public var description: String { String(describing: self.wrapped) } + + package static let all: [Self] = [ + .ok, + .cancelled, + .unknown, + .invalidArgument, + .deadlineExceeded, + .notFound, + .alreadyExists, + .permissionDenied, + .resourceExhausted, + .failedPrecondition, + .aborted, + .outOfRange, + .unimplemented, + .internalError, + .unavailable, + .dataLoss, + .unauthenticated, + ] } } diff --git a/Tests/GRPCCoreTests/StatusTests.swift b/Tests/GRPCCoreTests/StatusTests.swift index 98d114934..936ff8e41 100644 --- a/Tests/GRPCCoreTests/StatusTests.swift +++ b/Tests/GRPCCoreTests/StatusTests.swift @@ -13,105 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import GRPCCore -import XCTest - -final class StatusTests: XCTestCase { - private static let statusCodeRawValue: [(Status.Code, Int)] = [ - (.ok, 0), - (.cancelled, 1), - (.unknown, 2), - (.invalidArgument, 3), - (.deadlineExceeded, 4), - (.notFound, 5), - (.alreadyExists, 6), - (.permissionDenied, 7), - (.resourceExhausted, 8), - (.failedPrecondition, 9), - (.aborted, 10), - (.outOfRange, 11), - (.unimplemented, 12), - (.internalError, 13), - (.unavailable, 14), - (.dataLoss, 15), - (.unauthenticated, 16), - ] - func testCustomStringConvertible() { - XCTAssertDescription(Status(code: .ok, message: ""), #"ok: """#) - XCTAssertDescription(Status(code: .dataLoss, message: "message"), #"dataLoss: "message""#) - XCTAssertDescription(Status(code: .unknown, message: "message"), #"unknown: "message""#) - XCTAssertDescription(Status(code: .aborted, message: "message"), #"aborted: "message""#) - } +import GRPCCore +import Testing - func testStatusCodeRawValues() { - for (code, expected) in Self.statusCodeRawValue { - XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value") +@Suite("Status") +struct StatusTests { + @Suite("Code") + struct Code { + @Test("rawValue", arguments: zip(Status.Code.all, 0 ... 16)) + func rawValueOfStatusCodes(code: Status.Code, expected: Int) { + #expect(code.rawValue == expected) } - } - - func testStatusCodeFromErrorCode() throws { - XCTAssertEqual(Status.Code(RPCError.Code.cancelled), .cancelled) - XCTAssertEqual(Status.Code(RPCError.Code.unknown), .unknown) - XCTAssertEqual(Status.Code(RPCError.Code.invalidArgument), .invalidArgument) - XCTAssertEqual(Status.Code(RPCError.Code.deadlineExceeded), .deadlineExceeded) - XCTAssertEqual(Status.Code(RPCError.Code.notFound), .notFound) - XCTAssertEqual(Status.Code(RPCError.Code.alreadyExists), .alreadyExists) - XCTAssertEqual(Status.Code(RPCError.Code.permissionDenied), .permissionDenied) - XCTAssertEqual(Status.Code(RPCError.Code.resourceExhausted), .resourceExhausted) - XCTAssertEqual(Status.Code(RPCError.Code.failedPrecondition), .failedPrecondition) - XCTAssertEqual(Status.Code(RPCError.Code.aborted), .aborted) - XCTAssertEqual(Status.Code(RPCError.Code.outOfRange), .outOfRange) - XCTAssertEqual(Status.Code(RPCError.Code.unimplemented), .unimplemented) - XCTAssertEqual(Status.Code(RPCError.Code.internalError), .internalError) - XCTAssertEqual(Status.Code(RPCError.Code.unavailable), .unavailable) - XCTAssertEqual(Status.Code(RPCError.Code.dataLoss), .dataLoss) - XCTAssertEqual(Status.Code(RPCError.Code.unauthenticated), .unauthenticated) - } - func testStatusCodeFromValidRawValue() { - for (expected, rawValue) in Self.statusCodeRawValue { - XCTAssertEqual( - Status.Code(rawValue: rawValue), - expected, - "\(rawValue) didn't convert to expected code \(expected)" + @Test( + "Initialize from RPCError.Code", + arguments: zip( + RPCError.Code.all, + Status.Code.all.dropFirst() // Drop '.ok', there is no '.ok' error code. ) + ) + func initFromRPCErrorCode(errorCode: RPCError.Code, expected: Status.Code) { + #expect(Status.Code(errorCode) == expected) } - } - func testStatusCodeFromInvalidRawValue() { - // Internally represented as a `UInt8`; try all other values. - for rawValue in UInt8(17) ... UInt8.max { - XCTAssertNil(Status.Code(rawValue: Int(rawValue))) + @Test("Initialize from rawValue", arguments: zip(0 ... 16, Status.Code.all)) + func initFromRawValue(rawValue: Int, expected: Status.Code) { + #expect(Status.Code(rawValue: rawValue) == expected) } - // API accepts `Int` so try invalid `Int` values too. - XCTAssertNil(Status.Code(rawValue: -1)) - XCTAssertNil(Status.Code(rawValue: 1000)) - XCTAssertNil(Status.Code(rawValue: .max)) + @Test("Initialize from invalid rawValue", arguments: [-1, 17, 100, .max]) + func initFromInvalidRawValue(rawValue: Int) { + #expect(Status.Code(rawValue: rawValue) == nil) + } } - func testEquatableConformance() { - XCTAssertEqual(Status(code: .ok, message: ""), Status(code: .ok, message: "")) - XCTAssertEqual(Status(code: .ok, message: "message"), Status(code: .ok, message: "message")) - - XCTAssertNotEqual( - Status(code: .ok, message: ""), - Status(code: .ok, message: "message") - ) + @Test("CustomStringConvertible conformance") + func customStringConvertible() { + #expect("\(Status(code: .ok, message: ""))" == #"ok: """#) + #expect("\(Status(code: .dataLoss, message: "oh no"))" == #"dataLoss: "oh no""#) + } - XCTAssertNotEqual( - Status(code: .ok, message: "message"), - Status(code: .internalError, message: "message") - ) + @Test("Equatable conformance") + func equatable() { + let ok = Status(code: .ok, message: "") + let okWithMessage = Status(code: .ok, message: "message") + let internalError = Status(code: .internalError, message: "") - XCTAssertNotEqual( - Status(code: .ok, message: "message"), - Status(code: .ok, message: "different message") - ) + #expect(ok == ok) + #expect(ok != okWithMessage) + #expect(ok != internalError) } - func testFitsInExistentialContainer() { - XCTAssertLessThanOrEqual(MemoryLayout.size, 24) + @Test("Fits in existential container") + func fitsInExistentialContainer() { + #expect(MemoryLayout.size <= 24) } } From 62b7f850a6698f10d4f67d418b752243d8141649 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 9 Sep 2024 14:44:10 +0100 Subject: [PATCH 457/580] Add TLS support for NIOPosix H2 client (#2036) ## Motivation We currently have a `NIOPosix` client transport implementation in gRPC v2, but it doesn't support TLS. ## Modifications This PR adds support for TLS in the NIOPosix-backed HTTP/2 implementation of the client transport for gRPC v2. ## Result We now support TLS when using the NIOPosix client transport in gRPC V2. --- Examples/v2/echo/Subcommands/Collect.swift | 2 +- Examples/v2/echo/Subcommands/Expand.swift | 2 +- Examples/v2/echo/Subcommands/Get.swift | 2 +- Examples/v2/echo/Subcommands/Update.swift | 2 +- .../v2/hello-world/Subcommands/Greet.swift | 2 +- .../route-guide/Subcommands/GetFeature.swift | 2 +- .../Subcommands/ListFeatures.swift | 2 +- .../route-guide/Subcommands/RecordRoute.swift | 2 +- .../route-guide/Subcommands/RouteChat.swift | 2 +- .../HTTP2ClientTransport+Posix.swift | 85 ++++++++++++++++--- .../HTTP2ServerTransport+Posix.swift | 49 +++++++++-- .../NIOSSL+GRPC.swift | 28 ++++-- .../TLSConfig.swift | 84 +++++++++++++++++- .../InteroperabilityTestsExecutable.swift | 2 +- .../performance-worker/WorkerService.swift | 2 +- .../HTTP2TransportNIOPosixTests.swift | 78 ++++++++++++++++- .../HTTP2TransportTests.swift | 2 +- 17 files changed, 303 insertions(+), 45 deletions(-) diff --git a/Examples/v2/echo/Subcommands/Collect.swift b/Examples/v2/echo/Subcommands/Collect.swift index 63402e0c6..3a61915df 100644 --- a/Examples/v2/echo/Subcommands/Collect.swift +++ b/Examples/v2/echo/Subcommands/Collect.swift @@ -32,7 +32,7 @@ struct Collect: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Examples/v2/echo/Subcommands/Expand.swift b/Examples/v2/echo/Subcommands/Expand.swift index 33f89895d..1d06bdd99 100644 --- a/Examples/v2/echo/Subcommands/Expand.swift +++ b/Examples/v2/echo/Subcommands/Expand.swift @@ -32,7 +32,7 @@ struct Expand: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Examples/v2/echo/Subcommands/Get.swift b/Examples/v2/echo/Subcommands/Get.swift index 400ba24a8..0dd551002 100644 --- a/Examples/v2/echo/Subcommands/Get.swift +++ b/Examples/v2/echo/Subcommands/Get.swift @@ -30,7 +30,7 @@ struct Get: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Examples/v2/echo/Subcommands/Update.swift b/Examples/v2/echo/Subcommands/Update.swift index 04fdc1335..1c189caa8 100644 --- a/Examples/v2/echo/Subcommands/Update.swift +++ b/Examples/v2/echo/Subcommands/Update.swift @@ -32,7 +32,7 @@ struct Update: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: self.arguments.target, - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Examples/v2/hello-world/Subcommands/Greet.swift b/Examples/v2/hello-world/Subcommands/Greet.swift index aca973814..069b8faee 100644 --- a/Examples/v2/hello-world/Subcommands/Greet.swift +++ b/Examples/v2/hello-world/Subcommands/Greet.swift @@ -33,7 +33,7 @@ struct Greet: AsyncParsableCommand { let client = GRPCClient( transport: try .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Examples/v2/route-guide/Subcommands/GetFeature.swift b/Examples/v2/route-guide/Subcommands/GetFeature.swift index 6e51f2427..1c42ec6da 100644 --- a/Examples/v2/route-guide/Subcommands/GetFeature.swift +++ b/Examples/v2/route-guide/Subcommands/GetFeature.swift @@ -39,7 +39,7 @@ struct GetFeature: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) let client = GRPCClient(transport: transport) diff --git a/Examples/v2/route-guide/Subcommands/ListFeatures.swift b/Examples/v2/route-guide/Subcommands/ListFeatures.swift index ea95cb593..887a944e2 100644 --- a/Examples/v2/route-guide/Subcommands/ListFeatures.swift +++ b/Examples/v2/route-guide/Subcommands/ListFeatures.swift @@ -53,7 +53,7 @@ struct ListFeatures: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) let client = GRPCClient(transport: transport) diff --git a/Examples/v2/route-guide/Subcommands/RecordRoute.swift b/Examples/v2/route-guide/Subcommands/RecordRoute.swift index 829f5ee06..cd443230b 100644 --- a/Examples/v2/route-guide/Subcommands/RecordRoute.swift +++ b/Examples/v2/route-guide/Subcommands/RecordRoute.swift @@ -32,7 +32,7 @@ struct RecordRoute: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) let client = GRPCClient(transport: transport) diff --git a/Examples/v2/route-guide/Subcommands/RouteChat.swift b/Examples/v2/route-guide/Subcommands/RouteChat.swift index 7fbf673c2..81cb5c3e2 100644 --- a/Examples/v2/route-guide/Subcommands/RouteChat.swift +++ b/Examples/v2/route-guide/Subcommands/RouteChat.swift @@ -32,7 +32,7 @@ struct RouteChat: AsyncParsableCommand { func run() async throws { let transport = try HTTP2ClientTransport.Posix( target: .ipv4(host: "127.0.0.1", port: self.port), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) let client = GRPCClient(transport: transport) diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index 995f8948f..57e969e3a 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -19,14 +19,18 @@ public import GRPCHTTP2Core // should be @usableFromInline public import NIOCore public import NIOPosix // has to be public because of default argument value in init +#if canImport(NIOSSL) +import NIOSSL +#endif + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ClientTransport { - /// A `ClientTransport` using HTTP/2 built on top of `NIOPosix`. + /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOPosix`. /// /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of - /// the `HTTP2ClientTransport`. + /// the ``GRPCHTTP2Core/HTTP2ClientTransport``. /// /// To use this transport you need to provide a 'target' to connect to which will be resolved /// by an appropriate resolver from the resolver registry. By default the resolver registry can @@ -34,16 +38,19 @@ extension HTTP2ClientTransport { /// targets. If you use a custom target you must also provide an appropriately configured /// registry. /// - /// You can control various aspects of connection creation, management and RPC behavior via the - /// ``Config``. Load balancing policies and other RPC specific behavior can be configured via + /// You can control various aspects of connection creation, management, security and RPC behavior via + /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via /// the ``ServiceConfig`` (if it isn't provided by a resolver). /// /// Beyond creating the transport you don't need to interact with it directly, instead, pass it /// to a `GRPCClient`: /// /// ```swift - /// try await withThrowingDiscardingTaskGroup { - /// let transport = try HTTP2ClientTransport.Posix(target: .dns(host: "example.com")) + /// try await withThrowingDiscardingTaskGroup { group in + /// let transport = try HTTP2ClientTransport.Posix( + /// target: .ipv4(host: "example.com"), + /// config: .defaults(transportSecurity: .plaintext) + /// ) /// let client = GRPCClient(transport: transport) /// group.addTask { /// try await client.run() @@ -87,7 +94,7 @@ extension HTTP2ClientTransport { // Configure a connector. self.channel = GRPCChannel( resolver: resolver, - connector: Connector(eventLoopGroup: eventLoopGroup, config: config), + connector: try Connector(eventLoopGroup: eventLoopGroup, config: config), config: GRPCChannel.Config(posix: config), defaultServiceConfig: serviceConfig ) @@ -125,9 +132,33 @@ extension HTTP2ClientTransport.Posix { private let config: HTTP2ClientTransport.Posix.Config private let eventLoopGroup: any EventLoopGroup - init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) { + #if canImport(NIOSSL) + private let nioSSLContext: NIOSSLContext? + private let serverHostname: String? + #endif + + init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) throws { self.eventLoopGroup = eventLoopGroup self.config = config + + #if canImport(NIOSSL) + switch self.config.transportSecurity.wrapped { + case .plaintext: + self.nioSSLContext = nil + self.serverHostname = nil + case .tls(let tlsConfig): + do { + self.nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig)) + self.serverHostname = tlsConfig.serverHostname + } catch { + throw RuntimeError( + code: .transportError, + message: "Couldn't create SSL context, check your TLS configuration.", + cause: error + ) + } + } + #endif } func establishConnection( @@ -137,7 +168,18 @@ extension HTTP2ClientTransport.Posix { group: self.eventLoopGroup ).connect(to: address) { channel in channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.configureGRPCClientPipeline( + #if canImport(NIOSSL) + if let nioSSLContext = self.nioSSLContext { + try channel.pipeline.syncOperations.addHandler( + NIOSSLClientHandler( + context: nioSSLContext, + serverHostname: self.serverHostname + ) + ) + } + #endif + + return try channel.pipeline.syncOperations.configureGRPCClientPipeline( channel: channel, config: GRPCChannel.Config(posix: self.config) ) @@ -164,31 +206,48 @@ extension HTTP2ClientTransport.Posix { /// Compression configuration. public var compression: HTTP2ClientTransport.Config.Compression + /// The transport's security. + public var transportSecurity: TransportSecurity + /// Creates a new connection configuration. /// - /// See also ``defaults``. + /// - Parameters: + /// - http2: HTTP2 configuration. + /// - backoff: Backoff configuration. + /// - connection: Connection configuration. + /// - compression: Compression configuration. + /// - transportSecurity: The transport's security configuration. + /// + /// - SeeAlso: ``defaults(_:)``. public init( http2: HTTP2ClientTransport.Config.HTTP2, backoff: HTTP2ClientTransport.Config.Backoff, connection: HTTP2ClientTransport.Config.Connection, - compression: HTTP2ClientTransport.Config.Compression + compression: HTTP2ClientTransport.Config.Compression, + transportSecurity: TransportSecurity ) { self.http2 = http2 self.connection = connection self.backoff = backoff self.compression = compression + self.transportSecurity = transportSecurity } /// Default values. /// /// - Parameters: + /// - transportSecurity: The security settings applied to the transport. /// - configure: A closure which allows you to modify the defaults before returning them. - public static func defaults(_ configure: (_ config: inout Self) -> Void = { _ in }) -> Self { + public static func defaults( + transportSecurity: TransportSecurity, + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { var config = Self( http2: .defaults, backoff: .defaults, connection: .defaults, - compression: .defaults + compression: .defaults, + transportSecurity: transportSecurity ) configure(&config) return config diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 49d15c2d5..0717f0f35 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -15,7 +15,7 @@ */ public import GRPCCore -public import GRPCHTTP2Core +public import GRPCHTTP2Core // should be @usableFromInline internal import NIOCore internal import NIOExtras internal import NIOHTTP2 @@ -27,7 +27,33 @@ import NIOSSL #endif extension HTTP2ServerTransport { - /// A NIOPosix-backed implementation of a server transport. + /// A ``GRPCCore/ServerTransport`` using HTTP/2 built on top of `NIOPosix`. + /// + /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use + /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended + /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of + /// the ``GRPCHTTP2Core/HTTP2ServerTransport``. + /// + /// You can control various aspects of connection creation, management, security and RPC behavior via + /// the ``Config``. + /// + /// Beyond creating the transport you don't need to interact with it directly, instead, pass it + /// to a `GRPCServer`: + /// + /// ```swift + /// try await withThrowingDiscardingTaskGroup { group in + /// let transport = HTTP2ServerTransport.Posix( + /// address: .ipv4(host: "127.0.0.1", port: 0), + /// config: .defaults(transportSecurity: .plaintext) + /// ) + /// let server = GRPCServer(transport: transport, services: someServices) + /// group.addTask { + /// try await server.serve() + /// } + /// + /// // ... + /// } + /// ``` @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public final class Posix: ServerTransport, ListeningServerTransport { private let address: GRPCHTTP2Core.SocketAddress @@ -340,27 +366,34 @@ extension HTTP2ServerTransport.Posix { public struct Config: Sendable { /// Compression configuration. public var compression: HTTP2ServerTransport.Config.Compression + /// Connection configuration. public var connection: HTTP2ServerTransport.Config.Connection + /// HTTP2 configuration. public var http2: HTTP2ServerTransport.Config.HTTP2 + /// RPC configuration. public var rpc: HTTP2ServerTransport.Config.RPC + /// The transport's security. public var transportSecurity: TransportSecurity /// Construct a new `Config`. + /// /// - Parameters: - /// - compression: Compression configuration. - /// - connection: Connection configuration. /// - http2: HTTP2 configuration. /// - rpc: RPC configuration. + /// - connection: Connection configuration. + /// - compression: Compression configuration. /// - transportSecurity: The transport's security configuration. + /// + /// - SeeAlso: ``defaults(transportSecurity:configure:)`` public init( - compression: HTTP2ServerTransport.Config.Compression, - connection: HTTP2ServerTransport.Config.Connection, http2: HTTP2ServerTransport.Config.HTTP2, rpc: HTTP2ServerTransport.Config.RPC, + connection: HTTP2ServerTransport.Config.Connection, + compression: HTTP2ServerTransport.Config.Compression, transportSecurity: TransportSecurity ) { self.compression = compression @@ -380,10 +413,10 @@ extension HTTP2ServerTransport.Posix { configure: (_ config: inout Self) -> Void = { _ in } ) -> Self { var config = Self( - compression: .defaults, - connection: .defaults, http2: .defaults, rpc: .defaults, + connection: .defaults, + compression: .defaults, transportSecurity: transportSecurity ) configure(&config) diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift index 4bc3f143e..94436f56e 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift @@ -131,18 +131,34 @@ extension TLSConfiguration { let certificateChain = try tlsConfig.certificateChain.sslCertificateSources() let privateKey = try NIOSSLPrivateKey(privateKey: tlsConfig.privateKey) - var tlsConfiguration = TLSConfiguration.makeServerConfiguration( + self = TLSConfiguration.makeServerConfiguration( certificateChain: certificateChain, privateKey: .privateKey(privateKey) ) - tlsConfiguration.minimumTLSVersion = .tlsv12 - tlsConfiguration.certificateVerification = CertificateVerification( + self.minimumTLSVersion = .tlsv12 + self.certificateVerification = CertificateVerification( tlsConfig.clientCertificateVerification ) - tlsConfiguration.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots) - tlsConfiguration.applicationProtocols = ["grpc-exp", "h2"] + self.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots) + self.applicationProtocols = ["grpc-exp", "h2"] + } + + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + package init(_ tlsConfig: HTTP2ClientTransport.Posix.Config.TLS) throws { + self = TLSConfiguration.makeClientConfiguration() + self.certificateChain = try tlsConfig.certificateChain.sslCertificateSources() - self = tlsConfiguration + if let privateKey = tlsConfig.privateKey { + let privateKeySource = try NIOSSLPrivateKey(privateKey: privateKey) + self.privateKey = .privateKey(privateKeySource) + } + + self.minimumTLSVersion = .tlsv12 + self.certificateVerification = CertificateVerification( + tlsConfig.serverCertificateVerification + ) + self.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots) + self.applicationProtocols = ["grpc-exp", "h2"] } } #endif diff --git a/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift b/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift index 591bafbe7..2e42d58c1 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift @@ -147,10 +147,12 @@ extension HTTP2ServerTransport.Posix.Config { /// This connection is plaintext: no encryption will take place. public static let plaintext = Self(wrapped: .plaintext) + #if canImport(NIOSSL) /// This connection will use TLS. public static func tls(_ tls: TLS) -> Self { Self(wrapped: .tls(tls)) } + #endif } public struct TLS: Sendable { @@ -184,7 +186,7 @@ extension HTTP2ServerTransport.Posix.Config { certificateChain: [TLSConfig.CertificateSource], privateKey: TLSConfig.PrivateKeySource ) -> Self { - Self.init( + Self( certificateChain: certificateChain, privateKey: privateKey, clientCertificateVerification: .noVerification, @@ -207,7 +209,7 @@ extension HTTP2ServerTransport.Posix.Config { certificateChain: [TLSConfig.CertificateSource], privateKey: TLSConfig.PrivateKeySource ) -> Self { - Self.init( + Self( certificateChain: certificateChain, privateKey: privateKey, clientCertificateVerification: .noHostnameVerification, @@ -217,3 +219,81 @@ extension HTTP2ServerTransport.Posix.Config { } } } + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HTTP2ClientTransport.Posix.Config { + /// The security configuration for this connection. + public struct TransportSecurity: Sendable { + package enum Wrapped: Sendable { + case plaintext + case tls(TLS) + } + + package let wrapped: Wrapped + + /// This connection is plaintext: no encryption will take place. + public static let plaintext = Self(wrapped: .plaintext) + + #if canImport(NIOSSL) + /// This connection will use TLS. + public static func tls(_ tls: TLS) -> Self { + Self(wrapped: .tls(tls)) + } + #endif + } + + public struct TLS: Sendable { + /// The certificates the client will offer during negotiation. + public var certificateChain: [TLSConfig.CertificateSource] + + /// The private key associated with the leaf certificate. + public var privateKey: TLSConfig.PrivateKeySource? + + /// How to verify the server certificate, if one is presented. + public var serverCertificateVerification: TLSConfig.CertificateVerification + + /// The trust roots to be used when verifying server certificates. + public var trustRoots: TLSConfig.TrustRootsSource + + /// An optional server hostname to use when verifying certificates. + public var serverHostname: String? + + /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: + /// - `certificateChain` equals `[]` + /// - `privateKey` equals `nil` + /// - `serverCertificateVerification` equals `fullVerification` + /// - `trustRoots` equals `systemDefault` + /// - `serverHostname` equals `nil` + /// + /// - Returns: A new HTTP2 NIO Posix transport TLS config. + public static var defaults: Self { + Self( + certificateChain: [], + privateKey: nil, + serverCertificateVerification: .fullVerification, + trustRoots: .systemDefault, + serverHostname: nil + ) + } + + /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match + /// the requirements of mTLS: + /// - `trustRoots` equals `systemDefault` + /// + /// - Parameters: + /// - certificateChain: The certificates the client will offer during negotiation. + /// - privateKey: The private key associated with the leaf certificate. + /// - Returns: A new HTTP2 NIO Posix transport TLS config. + public static func mTLS( + certificateChain: [TLSConfig.CertificateSource], + privateKey: TLSConfig.PrivateKeySource + ) -> Self { + Self( + certificateChain: certificateChain, + privateKey: privateKey, + serverCertificateVerification: .fullVerification, + trustRoots: .systemDefault + ) + } + } +} diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift index 9b60f6031..15e1ba0fa 100644 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift @@ -106,7 +106,7 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand { return GRPCClient( transport: try .http2NIOPosix( target: .ipv4(host: host, port: port), - config: .defaults { + config: .defaults(transportSecurity: .plaintext) { $0.compression.enabledAlgorithms = .all }, serviceConfig: serviceConfig diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index f029e146b..2864fc405 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -457,7 +457,7 @@ extension WorkerService { client: GRPCClient( transport: try .http2NIOPosix( target: target, - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ), concurrentRPCs: Int(config.outstandingRpcsPerChannel), diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index 4f6993dde..aa6af81c9 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -263,7 +263,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { -----END RSA PRIVATE KEY----- """ - func testTLSConfig_Defaults() throws { + func testServerTLSConfig_Defaults() throws { let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( certificateChain: [ .bytes(Array(Self.samplePemCert.utf8), format: .pem) @@ -293,7 +293,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) } - func testTLSConfig_mTLS() throws { + func testServerTLSConfig_mTLS() throws { let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.mTLS( certificateChain: [ .bytes(Array(Self.samplePemCert.utf8), format: .pem) @@ -323,7 +323,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) } - func testTLSConfig_FullVerifyClient() throws { + func testServerTLSConfig_FullVerifyClient() throws { var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( certificateChain: [ .bytes(Array(Self.samplePemCert.utf8), format: .pem) @@ -354,7 +354,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) } - func testTLSConfig_CustomTrustRoots() throws { + func testServerTLSConfig_CustomTrustRoots() throws { var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( certificateChain: [ .bytes(Array(Self.samplePemCert.utf8), format: .pem) @@ -387,5 +387,75 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { ) XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) } + + func testClientTLSConfig_Defaults() throws { + let grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) + XCTAssertNil(nioSSLTLSConfig.privateKey) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) + XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } + + func testClientTLSConfig_CustomCertificateChainAndPrivateKey() throws { + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + grpcTLSConfig.certificateChain = [ + .bytes(Array(Self.samplePemCert.utf8), format: .pem) + ] + grpcTLSConfig.privateKey = .bytes(Array(Self.samplePemKey.utf8), format: .pem) + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual( + nioSSLTLSConfig.certificateChain, + [ + .certificate( + try NIOSSLCertificate( + bytes: Array(Self.samplePemCert.utf8), + format: .pem + ) + ) + ] + ) + XCTAssertEqual( + nioSSLTLSConfig.privateKey, + .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) + ) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) + XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } + + func testClientTLSConfig_CustomTrustRoots() throws { + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.utf8), format: .pem)]) + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) + XCTAssertNil(nioSSLTLSConfig.privateKey) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) + XCTAssertEqual( + nioSSLTLSConfig.trustRoots, + .certificates(try NIOSSLCertificate.fromPEMBytes(Array(Self.samplePemCert.utf8))) + ) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } + + func testClientTLSConfig_CustomCertificateVerification() throws { + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + grpcTLSConfig.serverCertificateVerification = .noHostnameVerification + let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) + + XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) + XCTAssertNil(nioSSLTLSConfig.privateKey) + XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) + XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .noHostnameVerification) + XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) + XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) + } #endif } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 2de224ffc..a78751874 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -199,7 +199,7 @@ final class HTTP2TransportTests: XCTestCase { serviceConfig.loadBalancingConfig = [.roundRobin] transport = try HTTP2ClientTransport.Posix( target: target, - config: .defaults { + config: .defaults(transportSecurity: .plaintext) { $0.compression.algorithm = compression $0.compression.enabledAlgorithms = enabledCompression }, From f8184b871593f86d19124f5675922481b2da3824 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 9 Sep 2024 17:05:18 +0100 Subject: [PATCH 458/580] Add support for TLS on H2 NIOTS server transport (#2040) ## Motivation We currently have a NIOTS server transport implementation in gRPC v2, but it doesn't support TLS. ## Modifications This PR adds support for TLS in the NIOTS-backed HTTP/2 implementation of the server transport for gRPC v2. It also adds support for ALPN, to validate that the negotiated protocol, if required, is HTTP2 or `grpc-exp`. If it's not, an error will be fired/the channel will be closed, since we don't support H1. ## Result We now support TLS/ALPN when using the NIOTS server transport in gRPC V2. --- ...TP2ServerTransport+TransportServices.swift | 89 ++++++++++++++++--- .../TLSConfig.swift | 63 +++++++++++++ ...P2TransportNIOTransportServicesTests.swift | 68 ++++++++++++-- .../HTTP2TransportTests.swift | 2 +- 4 files changed, 205 insertions(+), 17 deletions(-) create mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index a3fb426d1..7b37bcc4d 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -19,9 +19,10 @@ public import GRPCCore public import NIOTransportServices // has to be public because of default argument value in init public import GRPCHTTP2Core -internal import NIOCore -internal import NIOExtras -internal import NIOHTTP2 +private import NIOCore +private import NIOExtras +private import NIOHTTP2 +private import Network private import Synchronization @@ -171,7 +172,25 @@ extension HTTP2ServerTransport { } } - let serverChannel = try await NIOTSListenerBootstrap(group: self.eventLoopGroup) + let bootstrap: NIOTSListenerBootstrap + + let requireALPN: Bool + let scheme: Scheme + switch self.config.transportSecurity.wrapped { + case .plaintext: + requireALPN = false + scheme = .http + bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup) + + case .tls(let tlsConfig): + requireALPN = tlsConfig.requireALPN + scheme = .https + bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup) + .tlsOptions(try NWProtocolTLS.Options(tlsConfig)) + } + + let serverChannel = + try await bootstrap .serverChannelOption( ChannelOptions.socketOption(.so_reuseaddr), value: 1 @@ -190,8 +209,8 @@ extension HTTP2ServerTransport { connectionConfig: self.config.connection, http2Config: self.config.http2, rpcConfig: self.config.rpc, - requireALPN: false, - scheme: .http + requireALPN: requireALPN, + scheme: scheme ) } } @@ -292,41 +311,55 @@ extension HTTP2ServerTransport.TransportServices { public struct Config: Sendable { /// Compression configuration. public var compression: HTTP2ServerTransport.Config.Compression + /// Connection configuration. public var connection: HTTP2ServerTransport.Config.Connection + /// HTTP2 configuration. public var http2: HTTP2ServerTransport.Config.HTTP2 + /// RPC configuration. public var rpc: HTTP2ServerTransport.Config.RPC + /// The transport's security. + public var transportSecurity: TransportSecurity + /// Construct a new `Config`. /// - Parameters: /// - compression: Compression configuration. /// - connection: Connection configuration. /// - http2: HTTP2 configuration. /// - rpc: RPC configuration. + /// - transportSecurity: The transport's security configuration. public init( compression: HTTP2ServerTransport.Config.Compression, connection: HTTP2ServerTransport.Config.Connection, http2: HTTP2ServerTransport.Config.HTTP2, - rpc: HTTP2ServerTransport.Config.RPC + rpc: HTTP2ServerTransport.Config.RPC, + transportSecurity: TransportSecurity ) { self.compression = compression self.connection = connection self.http2 = http2 self.rpc = rpc + self.transportSecurity = transportSecurity } /// Default values for the different configurations. /// - /// - Parameter configure: A closure which allows you to modify the defaults before - /// returning them. - public static func defaults(configure: (_ config: inout Self) -> Void = { _ in }) -> Self { + /// - Parameters: + /// - transportSecurity: The transport's security configuration. + /// - configure: A closure which allows you to modify the defaults before returning them. + public static func defaults( + transportSecurity: TransportSecurity, + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { var config = Self( compression: .defaults, connection: .defaults, http2: .defaults, - rpc: .defaults + rpc: .defaults, + transportSecurity: transportSecurity ) configure(&config) return config @@ -396,4 +429,38 @@ extension ServerTransport where Self == HTTP2ServerTransport.TransportServices { ) } } + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension NWProtocolTLS.Options { + convenience init(_ tlsConfig: HTTP2ServerTransport.TransportServices.Config.TLS) throws { + self.init() + + guard let sec_identity = sec_identity_create(try tlsConfig.identityProvider()) else { + throw RuntimeError( + code: .transportError, + message: """ + There was an issue creating the SecIdentity required to set up TLS. \ + Please check your TLS configuration. + """ + ) + } + + sec_protocol_options_set_local_identity( + self.securityProtocolOptions, + sec_identity + ) + + sec_protocol_options_set_min_tls_protocol_version( + self.securityProtocolOptions, + .TLSv12 + ) + + for `protocol` in ["grpc-exp", "h2"] { + sec_protocol_options_add_tls_application_protocol( + self.securityProtocolOptions, + `protocol` + ) + } + } +} #endif diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift new file mode 100644 index 000000000..05840a42d --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift @@ -0,0 +1,63 @@ +/* + * 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. + */ + +#if canImport(Network) +public import Network + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HTTP2ServerTransport.TransportServices.Config { + /// The security configuration for this connection. + public struct TransportSecurity: Sendable { + package enum Wrapped: Sendable { + case plaintext + case tls(TLS) + } + + package let wrapped: Wrapped + + /// This connection is plaintext: no encryption will take place. + public static let plaintext = Self(wrapped: .plaintext) + + /// This connection will use TLS. + public static func tls(_ tls: TLS) -> Self { + Self(wrapped: .tls(tls)) + } + } + + public struct TLS: Sendable { + /// A provider for the `SecIdentity` to be used when setting up TLS. + public var identityProvider: @Sendable () throws -> SecIdentity + + /// Whether ALPN is required. + /// + /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. + public var requireALPN: Bool + + /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: + /// - `requireALPN` equals `false` + /// + /// - Returns: A new HTTP2 NIO Transport Services transport TLS config. + public static func defaults( + identityProvider: @Sendable @escaping () throws -> SecIdentity + ) -> Self { + Self( + identityProvider: identityProvider, + requireALPN: false + ) + } + } +} +#endif diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index 8219540ec..8611bdb16 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -22,10 +22,59 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class HTTP2TransportNIOTransportServicesTests: XCTestCase { + private static let p12bundleURL = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() // (this file) + .deletingLastPathComponent() // GRPCHTTP2TransportTests + .deletingLastPathComponent() // Tests + .appendingPathComponent("Sources") + .appendingPathComponent("GRPCSampleData") + .appendingPathComponent("bundle") + .appendingPathExtension("p12") + + @Sendable private static func loadIdentity() throws -> SecIdentity { + let data = try Data(contentsOf: Self.p12bundleURL) + + var externalFormat = SecExternalFormat.formatUnknown + var externalItemType = SecExternalItemType.itemTypeUnknown + let passphrase = "password" as CFTypeRef + var exportKeyParams = SecItemImportExportKeyParameters() + exportKeyParams.passphrase = Unmanaged.passUnretained(passphrase) + var items: CFArray? + + let status = SecItemImport( + data as CFData, + "bundle.p12" as CFString, + &externalFormat, + &externalItemType, + SecItemImportExportFlags(rawValue: 0), + &exportKeyParams, + nil, + &items + ) + + if status != errSecSuccess { + XCTFail( + """ + Unable to load identity from '\(Self.p12bundleURL)'. \ + SecItemImport failed with status \(status) + """ + ) + } else if items == nil { + XCTFail( + """ + Unable to load identity from '\(Self.p12bundleURL)'. \ + SecItemImport failed. + """ + ) + } + + return ((items! as NSArray)[0] as! SecIdentity) + } + func testGetListeningAddress_IPv4() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( address: .ipv4(host: "0.0.0.0", port: 0), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) try await withThrowingDiscardingTaskGroup { group in @@ -45,7 +94,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_IPv6() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( address: .ipv6(host: "::1", port: 0), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) try await withThrowingDiscardingTaskGroup { group in @@ -65,7 +114,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_UnixDomainSocket() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( address: .unixDomainSocket(path: "/tmp/niots-uds-test"), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) defer { // NIOTS does not unlink the UDS on close. @@ -91,7 +140,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_InvalidAddress() async { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) try? await withThrowingDiscardingTaskGroup { group in @@ -120,7 +169,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { func testGetListeningAddress_StoppedListening() async throws { let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( address: .ipv4(host: "0.0.0.0", port: 0), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) try? await withThrowingDiscardingTaskGroup { group in @@ -149,5 +198,14 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { } } } + + func testTLSConfig_Defaults() throws { + let identityProvider = Self.loadIdentity + let grpcTLSConfig = HTTP2ServerTransport.TransportServices.Config.TLS.defaults( + identityProvider: identityProvider + ) + XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider()) + XCTAssertEqual(grpcTLSConfig.requireALPN, false) + } } #endif diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index a78751874..14a9d69c3 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -166,7 +166,7 @@ final class HTTP2TransportTests: XCTestCase { let server = GRPCServer( transport: .http2NIOTS( address: .ipv4(host: "127.0.0.1", port: 0), - config: .defaults { + config: .defaults(transportSecurity: .plaintext) { $0.compression.enabledAlgorithms = compression } ), From 40f316eeace7011bab5ba237b04bfb13e1c4c470 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 9 Sep 2024 17:26:19 +0100 Subject: [PATCH 459/580] Adopt swift-testing for metadata tests (#2050) Motivation: swift-testing has a number of advantages over XCTest (parameterisation, organisation, failure messages etc.), we should start using it instead of XCTest. Modifications: - Convert the Metadata tests Results: Better tests --- Tests/GRPCCoreTests/MetadataTests.swift | 304 ++++++++++++------------ 1 file changed, 158 insertions(+), 146 deletions(-) diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index 40fb2a47b..f0b29df04 100644 --- a/Tests/GRPCCoreTests/MetadataTests.swift +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import GRPCCore -import XCTest +import Testing -final class MetadataTests: XCTestCase { - func testInitFromSequence() { +@Suite("Metadata") +struct MetadataTests { + @Test("Initialize from Sequence") + func initFromSequence() { let elements: [Metadata.Element] = [ (key: "key1", value: "value1"), (key: "key2", value: "value2"), @@ -26,218 +29,227 @@ final class MetadataTests: XCTestCase { let metadata = Metadata(elements) let expected: Metadata = ["key1": "value1", "key2": "value2", "key3": "value3"] - - XCTAssertEqual(metadata, expected) + #expect(metadata == expected) } - func testAddStringValue() { + @Test("Add string Value") + func addStringValue() { var metadata = Metadata() - XCTAssertTrue(metadata.isEmpty) + #expect(metadata.isEmpty) metadata.addString("testValue", forKey: "testString") - XCTAssertEqual(metadata.count, 1) + #expect(metadata.count == 1) let sequence = metadata[stringValues: "testString"] var iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), "testValue") - XCTAssertNil(iterator.next()) + #expect(iterator.next() == "testValue") + #expect(iterator.next() == nil) } - func testAddBinaryValue() { + @Test("Add binary value") + func addBinaryValue() { var metadata = Metadata() - XCTAssertTrue(metadata.isEmpty) + #expect(metadata.isEmpty) metadata.addBinary(Array("base64encodedString".utf8), forKey: "testBinary-bin") - XCTAssertEqual(metadata.count, 1) + #expect(metadata.count == 1) let sequence = metadata[binaryValues: "testBinary-bin"] var iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), Array("base64encodedString".utf8)) - XCTAssertNil(iterator.next()) + #expect(iterator.next() == Array("base64encodedString".utf8)) + #expect(iterator.next() == nil) } - func testCreateFromDictionaryLiteral() { + @Test("Initialize from dictionary literal") + func initFromDictionaryLiteral() { let metadata: Metadata = [ "testKey": "stringValue", "testKey-bin": .binary(Array("base64encodedString".utf8)), ] - XCTAssertEqual(metadata.count, 2) + #expect(metadata.count == 2) let stringSequence = metadata[stringValues: "testKey"] var stringIterator = stringSequence.makeIterator() - XCTAssertEqual(stringIterator.next(), "stringValue") - XCTAssertNil(stringIterator.next()) + #expect(stringIterator.next() == "stringValue") + #expect(stringIterator.next() == nil) let binarySequence = metadata[binaryValues: "testKey-bin"] var binaryIterator = binarySequence.makeIterator() - XCTAssertEqual(binaryIterator.next(), Array("base64encodedString".utf8)) - XCTAssertNil(binaryIterator.next()) + #expect(binaryIterator.next() == Array("base64encodedString".utf8)) + #expect(binaryIterator.next() == nil) } - func testReplaceOrAddValue() { - var metadata: Metadata = [ - "testKey": "value1", - "testKey": "value2", - ] - XCTAssertEqual(metadata.count, 2) + @Suite("Replace or add value") + struct ReplaceOrAdd { + @Suite("String") + struct StringValues { + var metadata: Metadata = [ + "key1": "value1", + "key1": "value2", + ] + + @Test("Add different key") + mutating func addNewKey() async throws { + self.metadata.replaceOrAddString("value3", forKey: "key2") + #expect(Array(self.metadata[stringValues: "key1"]) == ["value1", "value2"]) + #expect(Array(self.metadata[stringValues: "key2"]) == ["value3"]) + #expect(self.metadata.count == 3) + } + + @Test("Replace values for existing key") + mutating func replaceValues() async throws { + self.metadata.replaceOrAddString("value3", forKey: "key1") + #expect(Array(self.metadata[stringValues: "key1"]) == ["value3"]) + #expect(self.metadata.count == 1) + } + } - var sequence = metadata[stringValues: "testKey"] - var iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), "value1") - XCTAssertEqual(iterator.next(), "value2") - XCTAssertNil(iterator.next()) - - metadata.replaceOrAddString("anotherValue", forKey: "testKey2") - XCTAssertEqual(metadata.count, 3) - sequence = metadata[stringValues: "testKey"] - iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), "value1") - XCTAssertEqual(iterator.next(), "value2") - XCTAssertNil(iterator.next()) - sequence = metadata[stringValues: "testKey2"] - iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), "anotherValue") - XCTAssertNil(iterator.next()) - - metadata.replaceOrAddString("newValue", forKey: "testKey") - XCTAssertEqual(metadata.count, 2) - sequence = metadata[stringValues: "testKey"] - iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), "newValue") - XCTAssertNil(iterator.next()) - sequence = metadata[stringValues: "testKey2"] - iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), "anotherValue") - XCTAssertNil(iterator.next()) + @Suite("Binary") + struct BinaryValues { + var metadata: Metadata = [ + "key1-bin": [0], + "key1-bin": [1], + ] + + @Test("Add different key") + mutating func addNewKey() async throws { + self.metadata.replaceOrAddBinary([2], forKey: "key2-bin") + #expect(Array(self.metadata[binaryValues: "key1-bin"]) == [[0], [1]]) + #expect(Array(self.metadata[binaryValues: "key2-bin"]) == [[2]]) + #expect(self.metadata.count == 3) + } + + @Test("Replace values for existing key") + mutating func replaceValues() async throws { + self.metadata.replaceOrAddBinary([2], forKey: "key1-bin") + #expect(Array(self.metadata[binaryValues: "key1-bin"]) == [[2]]) + #expect(self.metadata.count == 1) + } + } } - func testReserveCapacity() { + @Test("Reserve more capacity increases capacity") + func reserveMoreCapacity() { var metadata = Metadata() - XCTAssertEqual(metadata.capacity, 0) + #expect(metadata.capacity == 0) + + metadata.reserveCapacity(10) + #expect(metadata.capacity == 10) + } + @Test("Reserve less capacity doesn't reduce capacity") + func reserveCapacity() { + var metadata = Metadata() + #expect(metadata.capacity == 0) metadata.reserveCapacity(10) - XCTAssertEqual(metadata.capacity, 10) + #expect(metadata.capacity == 10) + metadata.reserveCapacity(0) + #expect(metadata.capacity == 10) } - func testValuesIteration() { + @Test("Iterate over all values for a key") + func iterateOverValuesForKey() { let metadata: Metadata = [ - "testKey-bin": "string1", - "testKey-bin": .binary(.init("data1".utf8)), - "testKey-bin": "string2", - "testKey-bin": .binary(.init("data2".utf8)), - "testKey-bin": "string3", - "testKey-bin": .binary(.init("data3".utf8)), + "key-bin": "1", + "key-bin": [1], + "key-bin": "2", + "key-bin": [2], + "key-bin": "3", + "key-bin": [3], ] - XCTAssertEqual(metadata.count, 6) - let sequence = metadata["testKey-bin"] - var iterator = sequence.makeIterator() - XCTAssertEqual(iterator.next(), .string("string1")) - XCTAssertEqual(iterator.next(), .binary(.init("data1".utf8))) - XCTAssertEqual(iterator.next(), .string("string2")) - XCTAssertEqual(iterator.next(), .binary(.init("data2".utf8))) - XCTAssertEqual(iterator.next(), .string("string3")) - XCTAssertEqual(iterator.next(), .binary(.init("data3".utf8))) - XCTAssertNil(iterator.next()) + #expect(Array(metadata["key-bin"]) == ["1", [1], "2", [2], "3", [3]]) } - func testStringValuesIteration() { + @Test("Iterate over string values for a key") + func iterateOverStringsForKey() { let metadata: Metadata = [ - "testKey-bin": "string1", - "testKey-bin": .binary(.init("data1".utf8)), - "testKey-bin": "string2", - "testKey-bin": .binary(.init("data2".utf8)), - "testKey-bin": "string3", - "testKey-bin": .binary(.init("data3".utf8)), + "key-bin": "1", + "key-bin": [1], + "key-bin": "2", + "key-bin": [2], + "key-bin": "3", + "key-bin": [3], ] - XCTAssertEqual(metadata.count, 6) - let stringSequence = metadata[stringValues: "testKey-bin"] - var stringIterator = stringSequence.makeIterator() - XCTAssertEqual(stringIterator.next(), "string1") - XCTAssertEqual(stringIterator.next(), "string2") - XCTAssertEqual(stringIterator.next(), "string3") - XCTAssertNil(stringIterator.next()) + #expect(Array(metadata[stringValues: "key-bin"]) == ["1", "2", "3"]) } - func testBinaryValuesIteration_InvalidBase64EncodedStrings() { + @Test("Iterate over binary values for a key") + func iterateOverBinaryForKey() { let metadata: Metadata = [ - "testKey-bin": "invalidBase64-1", - "testKey-bin": .binary(.init("data1".utf8)), - "testKey-bin": "invalidBase64-2", - "testKey-bin": .binary(.init("data2".utf8)), - "testKey-bin": "invalidBase64-3", - "testKey-bin": .binary(.init("data3".utf8)), + "key-bin": "1", + "key-bin": [1], + "key-bin": "2", + "key-bin": [2], + "key-bin": "3", + "key-bin": [3], ] - XCTAssertEqual(metadata.count, 6) - let binarySequence = metadata[binaryValues: "testKey-bin"] - var binaryIterator = binarySequence.makeIterator() - XCTAssertEqual(binaryIterator.next(), Array("data1".utf8)) - XCTAssertEqual(binaryIterator.next(), Array("data2".utf8)) - XCTAssertEqual(binaryIterator.next(), Array("data3".utf8)) - XCTAssertNil(binaryIterator.next()) + #expect(Array(metadata[binaryValues: "key-bin"]) == [[1], [2], [3]]) } - func testBinaryValuesIteration_ValidBase64EncodedStrings() { + @Test("Iterate over base64 encoded binary values for a key") + func iterateOverBase64BinaryEncodedValuesForKey() { let metadata: Metadata = [ - "testKey-bin": "c3RyaW5nMQ==", - "testKey-bin": .binary(.init("data1".utf8)), - "testKey-bin": "c3RyaW5nMg==", - "testKey-bin": .binary(.init("data2".utf8)), - "testKey-bin": "c3RyaW5nMw==", - "testKey-bin": .binary(.init("data3".utf8)), + "key-bin": "c3RyaW5nMQ==", + "key-bin": .binary(.init("data1".utf8)), + "key-bin": "c3RyaW5nMg==", + "key-bin": .binary(.init("data2".utf8)), + "key-bin": "c3RyaW5nMw==", + "key-bin": .binary(.init("data3".utf8)), ] - XCTAssertEqual(metadata.count, 6) - let binarySequence = metadata[binaryValues: "testKey-bin"] - var binaryIterator = binarySequence.makeIterator() - XCTAssertEqual(binaryIterator.next(), Array("string1".utf8)) - XCTAssertEqual(binaryIterator.next(), Array("data1".utf8)) - XCTAssertEqual(binaryIterator.next(), Array("string2".utf8)) - XCTAssertEqual(binaryIterator.next(), Array("data2".utf8)) - XCTAssertEqual(binaryIterator.next(), Array("string3".utf8)) - XCTAssertEqual(binaryIterator.next(), Array("data3".utf8)) - XCTAssertNil(binaryIterator.next()) + let expected: [[UInt8]] = [ + Array("string1".utf8), + Array("data1".utf8), + Array("string2".utf8), + Array("data2".utf8), + Array("string3".utf8), + Array("data3".utf8), + ] + + #expect(Array(metadata[binaryValues: "key-bin"]) == expected) } - func testKeysAreCaseInsensitive() { + @Test("Subscripts are case-insensitive") + func subscriptIsCaseInsensitive() { let metadata: Metadata = [ - "testkey1": "value1", - "TESTKEY2": "value2", + "key1": "value1", + "KEY2": "value2", ] - XCTAssertEqual(metadata.count, 2) - var stringSequence = metadata[stringValues: "TESTKEY1"] - var stringIterator = stringSequence.makeIterator() - XCTAssertEqual(stringIterator.next(), "value1") - XCTAssertNil(stringIterator.next()) + #expect(Array(metadata[stringValues: "key1"]) == ["value1"]) + #expect(Array(metadata[stringValues: "KEY1"]) == ["value1"]) - stringSequence = metadata[stringValues: "testkey2"] - stringIterator = stringSequence.makeIterator() - XCTAssertEqual(stringIterator.next(), "value2") - XCTAssertNil(stringIterator.next()) + #expect(Array(metadata[stringValues: "key2"]) == ["value2"]) + #expect(Array(metadata[stringValues: "KEY2"]) == ["value2"]) } - func testRemoveAllWhere() { - let metadata: Metadata = [ - "testKey1": "value1", - "testKey2": "value2", - "testKey3": "value1", + @Suite("Remove all") + struct RemoveAll { + var metadata: Metadata = [ + "key1": "value1", + "key2": "value2", + "key3": "value1", ] - var metadata1 = metadata - metadata1.removeAll { _, value in - value == "value1" + @Test("Where value matches") + mutating func removeAllWhereValueMatches() async throws { + self.metadata.removeAll { _, value in + value == "value1" + } + + #expect(self.metadata == ["key2": "value2"]) } - XCTAssertEqual(metadata1, ["testKey2": "value2"]) + @Test("Where key matches") + mutating func removeAllWhereKeyMatches() async throws { + self.metadata.removeAll { key, _ in + key == "key2" + } - var metadata2 = metadata - metadata2.removeAll { key, _ in - key == "testKey2" + #expect(self.metadata == ["key1": "value1", "key3": "value1"]) } - - XCTAssertEqual(metadata2, ["testKey1": "value1", "testKey3": "value1"]) } } From cd019f66f9a2dc3c2f35e67b7be6748eaca16bbe Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 10 Sep 2024 10:57:50 +0100 Subject: [PATCH 460/580] Enable Swift 6 language mode in GRPCCore (#2046) Motivation: v2 should use Swift 6 language mode to take full advantage of the compilers data race checking. Modifications: - Enable Swift 6 language more for the code module - Add a bunch of missing explicit 'Sendable's Result: - Compiles with Swift 6 lang mode --- Package@swift-6.swift | 2 +- .../ClientRPCExecutor+HedgingExecutor.swift | 64 ++++++----- .../ClientRPCExecutor+OneShotExecutor.swift | 22 ++-- .../ClientRPCExecutor+RetryExecutor.swift | 13 +-- .../Internal/ClientStreamExecutor.swift | 4 +- .../Server/Internal/ServerRPCExecutor.swift | 4 +- Sources/GRPCCore/GRPCClient.swift | 8 +- .../Internal/TaskGroup+CancellableTask.swift | 2 +- Sources/GRPCCore/Status.swift | 14 +-- .../Internal/GRPCAsyncThrowingStream.swift | 103 ++++++++++++++++++ .../Streaming/Internal/RPCWriter+Map.swift | 6 +- .../RPCWriter+MessageToRPCResponsePart.swift | 4 +- .../Internal/RPCWriter+Serialize.swift | 2 +- ...t => UncheckedAsyncIteratorSequence.swift} | 14 ++- .../Streaming/RPCWriterProtocol.swift | 26 +---- .../InProcessClientTransport.swift | 4 +- Sources/GRPCInterceptors/HookedWriter.swift | 2 +- .../ServerRPCExecutorTestHarness.swift | 4 +- .../Test Utilities/RPCWriter+Utilities.swift | 2 +- .../InProcessServerTransportTests.swift | 6 +- 20 files changed, 206 insertions(+), 100 deletions(-) create mode 100644 Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift rename Sources/GRPCCore/Streaming/Internal/{AsyncIteratorSequence.swift => UncheckedAsyncIteratorSequence.swift} (68%) diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 1a84f988d..e7ee68e33 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -207,7 +207,7 @@ extension Target { ], path: "Sources/GRPCCore", swiftSettings: [ - .swiftLanguageMode(.v5), + .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), .enableUpcomingFeature("InternalImportsByDefault") ] diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index d21b2d3bf..36364d22c 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -21,14 +21,11 @@ extension ClientRPCExecutor { @usableFromInline struct HedgingExecutor< Transport: ClientTransport, + Input: Sendable, + Output: Sendable, Serializer: MessageSerializer, Deserializer: MessageDeserializer - > { - @usableFromInline - typealias Input = Serializer.Message - @usableFromInline - typealias Output = Deserializer.Message - + >: Sendable where Serializer.Message == Input, Deserializer.Message == Output { @usableFromInline let transport: Transport @usableFromInline @@ -181,14 +178,14 @@ extension ClientRPCExecutor.HedgingExecutor { let state = SharedState(policy: self.policy) // There's always a first attempt, safe to '!'. - let (attempt, scheduleNext) = state.withState({ $0.nextAttemptNumber() })! + let result = state.withState { $0.nextAttemptNumber()! } group.addTask { let result = await self._startAttempt( request: request, method: method, options: options, - attempt: attempt, + attempt: result.nextAttempt, state: state, picker: picker, responseHandler: responseHandler @@ -199,7 +196,7 @@ extension ClientRPCExecutor.HedgingExecutor { // Schedule the second attempt. var nextScheduledAttempt = ScheduledState() - if scheduleNext { + if result.scheduleNext { nextScheduledAttempt.schedule(in: &group, pushback: false, delay: self.policy.hedgingDelay) } @@ -212,13 +209,13 @@ extension ClientRPCExecutor.HedgingExecutor { switch outcome { case .ran: // Start a new attempt and possibly schedule the next. - if let (attempt, scheduleNext) = state.withState({ $0.nextAttemptNumber() }) { + if let result = state.withState({ $0.nextAttemptNumber() }) { group.addTask { let result = await self._startAttempt( request: request, method: method, options: options, - attempt: attempt, + attempt: result.nextAttempt, state: state, picker: picker, responseHandler: responseHandler @@ -227,7 +224,7 @@ extension ClientRPCExecutor.HedgingExecutor { } // Schedule the next attempt. - if scheduleNext { + if result.scheduleNext { nextScheduledAttempt.schedule( in: &group, pushback: false, @@ -265,13 +262,13 @@ extension ClientRPCExecutor.HedgingExecutor { nextScheduledAttempt.cancel() - if let (attempt, scheduleNext) = state.withState({ $0.nextAttemptNumber() }) { + if let result = state.withState({ $0.nextAttemptNumber() }) { group.addTask { let result = await self._startAttempt( request: request, method: method, options: options, - attempt: attempt, + attempt: result.nextAttempt, state: state, picker: picker, responseHandler: responseHandler @@ -280,7 +277,7 @@ extension ClientRPCExecutor.HedgingExecutor { } // Schedule the next retry. - if scheduleNext { + if result.scheduleNext { nextScheduledAttempt.schedule( in: &group, pushback: true, @@ -314,7 +311,7 @@ extension ClientRPCExecutor.HedgingExecutor { } @inlinable - func _startAttempt( + func _startAttempt( request: ClientRequest.Stream, method: MethodDescriptor, options: CallOptions, @@ -431,7 +428,7 @@ extension ClientRPCExecutor.HedgingExecutor { } @usableFromInline - final class SharedState { + final class SharedState: Sendable { @usableFromInline let state: Mutex @@ -441,7 +438,7 @@ extension ClientRPCExecutor.HedgingExecutor { } @inlinable - func withState(_ body: @Sendable (inout State) -> ReturnType) -> ReturnType { + func withState(_ body: (inout State) -> ReturnType) -> ReturnType { self.state.withLock { body(&$0) } @@ -449,7 +446,7 @@ extension ClientRPCExecutor.HedgingExecutor { } @usableFromInline - struct State { + struct State: Sendable { @usableFromInline let _maximumAttempts: Int @usableFromInline @@ -474,14 +471,31 @@ extension ClientRPCExecutor.HedgingExecutor { } } + @usableFromInline + struct NextAttemptResult: Sendable { + @usableFromInline + var nextAttempt: Int + @usableFromInline + var scheduleNext: Bool + + @inlinable + init(nextAttempt: Int, scheduleNext: Bool) { + self.nextAttempt = nextAttempt + self.scheduleNext = scheduleNext + } + } + @inlinable - mutating func nextAttemptNumber() -> (Int, Bool)? { + mutating func nextAttemptNumber() -> NextAttemptResult? { if self.hasUsableResponse || self.attempt > self._maximumAttempts { return nil } else { let attempt = self.attempt self.attempt += 1 - return (attempt, self.attempt <= self._maximumAttempts) + return NextAttemptResult( + nextAttempt: attempt, + scheduleNext: self.attempt <= self._maximumAttempts + ) } } } @@ -533,7 +547,7 @@ extension ClientRPCExecutor.HedgingExecutor { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline -enum _HedgingTaskResult { +enum _HedgingTaskResult: Sendable { case rpcHandled(Result) case finishedRequest(Result) case timedOut(Result) @@ -541,20 +555,20 @@ enum _HedgingTaskResult { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline -enum _HedgingAttemptTaskResult { +enum _HedgingAttemptTaskResult: Sendable { case attemptPicked(Bool) case attemptCompleted(AttemptResult) case scheduledAttemptFired(ScheduleEvent) @usableFromInline - enum AttemptResult { + enum AttemptResult: Sendable { case unusableResponse(ClientResponse.Stream, Metadata.RetryPushback?) case usableResponse(Result) case noStreamAvailable(any Error) } @usableFromInline - enum ScheduleEvent { + enum ScheduleEvent: Sendable { case ran case cancelled } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index 47668a8f9..a1288999c 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -21,14 +21,11 @@ extension ClientRPCExecutor { @usableFromInline struct OneShotExecutor< Transport: ClientTransport, + Input: Sendable, + Output: Sendable, Serializer: MessageSerializer, Deserializer: MessageDeserializer - > { - @usableFromInline - typealias Input = Serializer.Message - @usableFromInline - typealias Output = Deserializer.Message - + >: Sendable where Serializer.Message == Input, Deserializer.Message == Output { @usableFromInline let transport: Transport @usableFromInline @@ -60,7 +57,7 @@ extension ClientRPCExecutor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.OneShotExecutor { @inlinable - func execute( + func execute( request: ClientRequest.Stream, method: MethodDescriptor, options: CallOptions, @@ -71,9 +68,10 @@ extension ClientRPCExecutor.OneShotExecutor { if let deadline = self.deadline { var request = request request.metadata.timeout = ContinuousClock.now.duration(to: deadline) + let immutableRequest = request result = await withDeadline(deadline) { await self._execute( - request: request, + request: immutableRequest, method: method, options: options, responseHandler: responseHandler @@ -95,7 +93,7 @@ extension ClientRPCExecutor.OneShotExecutor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.OneShotExecutor { @inlinable - func _execute( + func _execute( request: ClientRequest.Stream, method: MethodDescriptor, options: CallOptions, @@ -133,9 +131,9 @@ extension ClientRPCExecutor.OneShotExecutor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @inlinable -func withDeadline( +func withDeadline( _ deadline: ContinuousClock.Instant, - execute: @escaping () async -> Result + execute: @Sendable @escaping () async -> Result ) async -> Result { return await withTaskGroup(of: _DeadlineChildTaskResult.self) { group in group.addTask { @@ -173,7 +171,7 @@ func withDeadline( } @usableFromInline -enum _DeadlineChildTaskResult { +enum _DeadlineChildTaskResult: Sendable { case deadlinePassed case timeoutCancelled case taskCompleted(Value) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 7c29a6c05..2cce865e1 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -19,14 +19,11 @@ extension ClientRPCExecutor { @usableFromInline struct RetryExecutor< Transport: ClientTransport, + Input: Sendable, + Output: Sendable, Serializer: MessageSerializer, Deserializer: MessageDeserializer - > { - @usableFromInline - typealias Input = Serializer.Message - @usableFromInline - typealias Output = Deserializer.Message - + >: Sendable where Serializer.Message == Input, Deserializer.Message == Output { @usableFromInline let transport: Transport @usableFromInline @@ -198,7 +195,7 @@ extension ClientRPCExecutor.RetryExecutor { } @inlinable - func executeAttempt( + func executeAttempt( stream: RPCStream, metadata: Metadata, retryStream: BroadcastAsyncSequence, @@ -307,7 +304,7 @@ extension ClientRPCExecutor.RetryExecutor { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline -enum _RetryExecutorTask { +enum _RetryExecutorTask: Sendable { case timedOut(Result) case handledResponse(Result) case retry(Duration?) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 472639835..c0f4b166a 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -55,7 +55,7 @@ internal enum ClientStreamExecutor { } let bodyParts = RawBodyPartToMessageSequence( - base: AsyncIteratorSequence(iterator.wrappedValue), + base: UncheckedAsyncIteratorSequence(iterator.wrappedValue), deserializer: deserializer ) @@ -168,7 +168,7 @@ internal enum ClientStreamExecutor { Message: Sendable, Deserializer: MessageDeserializer, Failure: Error - >: AsyncSequence { + >: AsyncSequence, Sendable where Base: Sendable { @usableFromInline typealias Element = AsyncIterator.Element diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 49e3b713e..74f657884 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -168,7 +168,7 @@ struct ServerRPCExecutor { ServerRequest.Stream ) async throws -> ServerResponse.Stream ) async { - let messages = AsyncIteratorSequence(inbound.wrappedValue).map { part throws -> Input in + let messages = UncheckedAsyncIteratorSequence(inbound.wrappedValue).map { part in switch part { case .message(let bytes): return try deserializer.deserialize(bytes) @@ -284,7 +284,7 @@ struct ServerRPCExecutor { } @usableFromInline - enum ServerExecutorTask { + enum ServerExecutorTask: Sendable { case timedOut(Result) case executed } diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 8b0027a74..c11d32caf 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -256,7 +256,7 @@ public final class GRPCClient: Sendable { /// - handler: A unary response handler. /// /// - Returns: The return value from the `handler`. - public func unary( + public func unary( request: ClientRequest.Single, descriptor: MethodDescriptor, serializer: some MessageSerializer, @@ -287,7 +287,7 @@ public final class GRPCClient: Sendable { /// - handler: A unary response handler. /// /// - Returns: The return value from the `handler`. - public func clientStreaming( + public func clientStreaming( request: ClientRequest.Stream, descriptor: MethodDescriptor, serializer: some MessageSerializer, @@ -318,7 +318,7 @@ public final class GRPCClient: Sendable { /// - handler: A response stream handler. /// /// - Returns: The return value from the `handler`. - public func serverStreaming( + public func serverStreaming( request: ClientRequest.Single, descriptor: MethodDescriptor, serializer: some MessageSerializer, @@ -350,7 +350,7 @@ public final class GRPCClient: Sendable { /// - handler: A response stream handler. /// /// - Returns: The return value from the `handler`. - public func bidirectionalStreaming( + public func bidirectionalStreaming( request: ClientRequest.Stream, descriptor: MethodDescriptor, serializer: some MessageSerializer, diff --git a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift index 1800f32dc..454a85b85 100644 --- a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift +++ b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift @@ -58,7 +58,7 @@ extension TaskGroup { } @usableFromInline - enum _ResultOrCancelled { + enum _ResultOrCancelled: Sendable { case result(ChildTaskResult) case cancelled } diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift index d1c9e087b..ed6636b7f 100644 --- a/Sources/GRPCCore/Status.swift +++ b/Sources/GRPCCore/Status.swift @@ -60,17 +60,19 @@ public struct Status: @unchecked Sendable, Hashable { public init(code: Code, message: String) { if code == .ok, message.isEmpty { // Avoid a heap allocation for the common case. - self.storage = Storage.okWithNoMessage + self = .ok } else { self.storage = Storage(code: code, message: message) } } - /// A status with code ``Code-swift.struct/ok`` and an empty message. - @inlinable - internal static var ok: Self { - Status(code: .ok, message: "") + private init(storage: Storage) { + self.storage = storage } + + /// A status with code ``Code-swift.struct/ok`` and an empty message. + @usableFromInline + internal static let ok = Status(storage: Storage(code: .ok, message: "")) } extension Status: CustomStringConvertible { @@ -81,8 +83,6 @@ extension Status: CustomStringConvertible { extension Status { private final class Storage: Hashable { - static let okWithNoMessage = Storage(code: .ok, message: "") - var code: Status.Code var message: String diff --git a/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift b/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift new file mode 100644 index 000000000..5b4bcb58b --- /dev/null +++ b/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift @@ -0,0 +1,103 @@ +/* + * 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. + */ + +// This exists to provide a version of 'AsyncThrowingStream' which is constrained to 'Sendable' +// elements. This is required in order for the continuation to be compatible with +// 'RPCWriterProtocol'. (Adding a constrained conformance to 'RPCWriterProtocol' on +// 'AsyncThrowingStream.Continuation' isn't possible because 'Sendable' is a marker protocol.) + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package struct GRPCAsyncThrowingStream: AsyncSequence, Sendable { + package typealias Element = Element + package typealias Failure = any Error + + private let base: AsyncThrowingStream + + package static func makeStream( + of: Element.Type = Element.self + ) -> (stream: Self, continuation: Self.Continuation) { + let base = AsyncThrowingStream.makeStream(of: Element.self) + let stream = GRPCAsyncThrowingStream(base: base.stream) + let continuation = GRPCAsyncThrowingStream.Continuation(base: base.continuation) + return (stream, continuation) + } + + fileprivate init(base: AsyncThrowingStream) { + self.base = base + } + + package struct Continuation: Sendable { + private let base: AsyncThrowingStream.Continuation + + fileprivate init(base: AsyncThrowingStream.Continuation) { + self.base = base + } + + func yield(_ value: Element) { + self.base.yield(value) + } + + func finish(throwing error: (any Error)? = nil) { + self.base.finish(throwing: error) + } + } + + package func makeAsyncIterator() -> AsyncIterator { + AsyncIterator(base: self.base.makeAsyncIterator()) + } + + package struct AsyncIterator: AsyncIteratorProtocol { + private var base: AsyncThrowingStream.AsyncIterator + + fileprivate init(base: AsyncThrowingStream.AsyncIterator) { + self.base = base + } + + package mutating func next() async throws(any Error) -> Element? { + try await self.next(isolation: nil) + } + + package mutating func next( + isolation actor: isolated (any Actor)? + ) async throws(any Error) -> Element? { + try await self.base.next(isolation: `actor`) + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension GRPCAsyncThrowingStream.Continuation: RPCWriterProtocol { + package func write(_ element: Element) async throws { + self.yield(element) + } + + package func write(contentsOf elements: some Sequence) async throws { + for element in elements { + self.yield(element) + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension GRPCAsyncThrowingStream.Continuation: ClosableRPCWriterProtocol { + package func finish() async { + self.finish(throwing: nil) + } + + package func finish(throwing error: any Error) async { + self.finish(throwing: .some(error)) + } +} diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift index 3afe49425..7755b46e1 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -16,7 +16,11 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline -struct MapRPCWriter>: RPCWriterProtocol { +struct MapRPCWriter< + Value: Sendable, + Mapped: Sendable, + Base: RPCWriterProtocol +>: RPCWriterProtocol { @usableFromInline typealias Element = Value diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift index 24ff04c64..c3dc290e9 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift @@ -16,7 +16,9 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline -struct MessageToRPCResponsePartWriter: RPCWriterProtocol { +struct MessageToRPCResponsePartWriter< + Serializer: MessageSerializer +>: RPCWriterProtocol where Serializer.Message: Sendable { @usableFromInline typealias Element = Serializer.Message diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift index f72df4ee6..f2a9acd22 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -19,7 +19,7 @@ struct SerializingRPCWriter< Base: RPCWriterProtocol<[UInt8]>, Serializer: MessageSerializer ->: RPCWriterProtocol { +>: RPCWriterProtocol where Serializer.Message: Sendable { @usableFromInline typealias Element = Serializer.Message diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift b/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift similarity index 68% rename from Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift rename to Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift index f75421dd6..e3bdcafee 100644 --- a/Sources/GRPCCore/Streaming/Internal/AsyncIteratorSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift @@ -19,7 +19,19 @@ public import Synchronization // should be @usableFromInline @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline /// An `AsyncSequence` which wraps an existing async iterator. -final class AsyncIteratorSequence: AsyncSequence { +final class UncheckedAsyncIteratorSequence< + Base: AsyncIteratorProtocol +>: AsyncSequence, @unchecked Sendable { + // This is '@unchecked Sendable' because iterators are typically marked as not being Sendable + // to avoid multiple iterators being created. This is done to avoid multiple concurrent consumers + // of a single async sequence. + // + // However, gRPC needs to read the first message in a sequence of inbound request/response parts + // to check how the RPC should be handled. To do this it creates an async iterator and waits for + // the first value and then decides what to do. If it continues processing the RPC it uses this + // wrapper type to turn the iterator back into an async sequence and then drops the iterator on + // the floor so that there is only a single consumer of the original source. + @usableFromInline typealias Element = Base.Element diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index 5841f5802..63e3ee26d 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -18,7 +18,7 @@ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol RPCWriterProtocol: Sendable { /// The type of value written. - associatedtype Element + associatedtype Element: Sendable /// Writes a single element. /// @@ -65,27 +65,3 @@ public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// being thrown. func finish(throwing error: any Error) async } - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncThrowingStream.Continuation: RPCWriterProtocol { - public func write(_ element: Element) async throws { - self.yield(element) - } - - public func write(contentsOf elements: some Sequence) async throws { - for element in elements { - self.yield(element) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncThrowingStream.Continuation: ClosableRPCWriterProtocol where Failure == any Error { - public func finish() { - self.finish(throwing: nil) - } - - public func finish(throwing error: any Error) { - self.finish(throwing: .some(error)) - } -} diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index ddbb4a8bd..2e29bdd82 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -232,8 +232,8 @@ public final class InProcessClientTransport: ClientTransport { options: CallOptions, _ closure: (RPCStream) async throws -> T ) async throws -> T { - let request = AsyncThrowingStream.makeStream(of: RPCRequestPart.self) - let response = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let request = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) + let response = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let clientStream = RPCStream( descriptor: descriptor, diff --git a/Sources/GRPCInterceptors/HookedWriter.swift b/Sources/GRPCInterceptors/HookedWriter.swift index 8a9994033..9d85df044 100644 --- a/Sources/GRPCInterceptors/HookedWriter.swift +++ b/Sources/GRPCInterceptors/HookedWriter.swift @@ -17,7 +17,7 @@ internal import GRPCCore internal import Tracing @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct HookedWriter: RPCWriterProtocol { +struct HookedWriter: RPCWriterProtocol { private let writer: any RPCWriterProtocol private let beforeEachWrite: @Sendable () -> Void private let afterEachWrite: @Sendable () -> Void diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index ca23d02a6..274913ab8 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -80,8 +80,8 @@ struct ServerRPCExecutorTestHarness { RPCAsyncSequence ) async throws -> Void ) async throws { - let input = AsyncThrowingStream.makeStream(of: RPCRequestPart.self) - let output = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let input = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) + let output = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift index b5a7b0771..923ab267d 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -30,7 +30,7 @@ extension RPCWriter { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -private struct FailOnWrite: RPCWriterProtocol { +private struct FailOnWrite: RPCWriterProtocol { func write(_ element: Element) async throws { XCTFail("Unexpected write") } diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 9890d6c4b..7816bc79a 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -24,7 +24,7 @@ final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessServerTransport() - let outbound = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let outbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let stream = RPCStream< RPCAsyncSequence, RPCWriter.Closable @@ -55,7 +55,7 @@ final class InProcessServerTransportTests: XCTestCase { func testStopListening() async throws { let transport = InProcessServerTransport() - let firstStreamOutbound = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let firstStreamOutbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let firstStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( @@ -79,7 +79,7 @@ final class InProcessServerTransportTests: XCTestCase { transport.beginGracefulShutdown() - let secondStreamOutbound = AsyncThrowingStream.makeStream(of: RPCResponsePart.self) + let secondStreamOutbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let secondStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( From a98b0f327eccad2076585b9d45befe6aff4b948a Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 11 Sep 2024 10:48:34 +0100 Subject: [PATCH 461/580] Update plugin bundling script (#2053) Motivation: The plugin bundling script which is used when we do a release doesn't update the dependencies so will just use whatever version is currently resolved. This means protobuf might not be up-to-date and an older version of `protoc-gen-swift` might get build. Modifications: - Run package update before building Result: The bundled `protoc-gen-swift` should be up-to-date --- scripts/bundle-plugins-for-release.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/bundle-plugins-for-release.sh b/scripts/bundle-plugins-for-release.sh index bb49b2fe9..baec8643b 100755 --- a/scripts/bundle-plugins-for-release.sh +++ b/scripts/bundle-plugins-for-release.sh @@ -44,6 +44,8 @@ stage=$(mktemp -d) stage_bin="${stage}/bin" mkdir -p "${stage_bin}" +# Make sure dependencies are up-to-date +swift package update # Make the plugins. swift build -c release --arch arm64 --arch x86_64 --product protoc-gen-grpc-swift swift build -c release --arch arm64 --arch x86_64 --product protoc-gen-swift From b036fb7c7f0a1faea61eb33520549f0dc475eb7a Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 11 Sep 2024 15:38:02 +0100 Subject: [PATCH 462/580] Add `NIOTransportServices` H2 client transport (#2054) Add a `NIOTransportServices` H2 client transport to gRPC v2. --- .../Client/HTTP2ClientTransport.swift | 10 +- .../NIOSocketAddress+GRPCSocketAddress.swift | 17 + .../Server/HTTP2ServerTransport.swift | 12 +- .../HTTP2ClientTransport+Posix.swift | 9 +- .../HTTP2ServerTransport+Posix.swift | 18 - ...TP2ClientTransport+TransportServices.swift | 339 ++++++++++++++++++ ...TP2ServerTransport+TransportServices.swift | 18 - .../TLSConfig.swift | 31 ++ .../HTTP2TransportNIOPosixTests.swift | 22 ++ ...P2TransportNIOTransportServicesTests.swift | 26 +- .../HTTP2TransportTests.swift | 19 +- 11 files changed, 466 insertions(+), 55 deletions(-) create mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift diff --git a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift index 03bc39f1c..03ad634e3 100644 --- a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift +++ b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift @@ -25,7 +25,7 @@ extension HTTP2ClientTransport { } extension HTTP2ClientTransport.Config { - public struct Compression: Sendable { + public struct Compression: Sendable, Hashable { /// The default algorithm used for compressing outbound messages. /// /// This can be overridden on a per-call basis via `CallOptions`. @@ -51,7 +51,7 @@ extension HTTP2ClientTransport.Config { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Keepalive: Sendable { + public struct Keepalive: Sendable, Hashable { /// The amount of time to wait after reading data before sending a keepalive ping. /// /// - Note: The transport may choose to increase this value if it is less than 10 seconds. @@ -73,7 +73,7 @@ extension HTTP2ClientTransport.Config { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Connection: Sendable { + public struct Connection: Sendable, Hashable { /// The maximum amount of time a connection may be idle before it's closed. /// /// Connections are considered idle when there are no open streams on them. Idle connections @@ -103,7 +103,7 @@ extension HTTP2ClientTransport.Config { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Backoff: Sendable { + public struct Backoff: Sendable, Hashable { /// The initial duration to wait before reattempting to establish a connection. public var initial: Duration @@ -135,7 +135,7 @@ extension HTTP2ClientTransport.Config { } } - public struct HTTP2: Sendable { + public struct HTTP2: Sendable, Hashable { /// The max frame size, in bytes. /// /// The actual value used is clamped to `(1 << 14) ... (1 << 24) - 1` (the min and max values diff --git a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift index db7eb537d..e27b07659 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift @@ -14,6 +14,7 @@ * limitations under the License. */ +private import GRPCCore package import NIOCore extension GRPCHTTP2Core.SocketAddress { @@ -38,6 +39,22 @@ extension GRPCHTTP2Core.SocketAddress { } extension NIOCore.SocketAddress { + package init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws { + if let ipv4 = socketAddress.ipv4 { + self = try Self(ipv4) + } else if let ipv6 = socketAddress.ipv6 { + self = try Self(ipv6) + } else if let unixDomainSocket = socketAddress.unixDomainSocket { + self = try Self(unixDomainSocket) + } else { + throw RPCError( + code: .internalError, + message: + "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)." + ) + } + } + package init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { try self.init(ipAddress: address.host, port: address.port) } diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift index 07eb6b696..e93b09c26 100644 --- a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift +++ b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift @@ -26,7 +26,7 @@ extension HTTP2ServerTransport { } extension HTTP2ServerTransport.Config { - public struct Compression: Sendable { + public struct Compression: Sendable, Hashable { /// Compression algorithms enabled for inbound messages. /// /// - Note: `CompressionAlgorithm.none` is always supported, even if it isn't set here. @@ -46,7 +46,7 @@ extension HTTP2ServerTransport.Config { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Keepalive: Sendable { + public struct Keepalive: Sendable, Hashable { /// The amount of time to wait after reading data before sending a keepalive ping. public var time: Duration @@ -80,7 +80,7 @@ extension HTTP2ServerTransport.Config { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct ClientKeepaliveBehavior: Sendable { + public struct ClientKeepaliveBehavior: Sendable, Hashable { /// The minimum allowed interval the client is allowed to send keep-alive pings. /// Pings more frequent than this interval count as 'strikes' and the connection is closed if there are /// too many strikes. @@ -107,7 +107,7 @@ extension HTTP2ServerTransport.Config { } @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Connection: Sendable { + public struct Connection: Sendable, Hashable { /// The maximum amount of time a connection may exist before being gracefully closed. public var maxAge: Duration? @@ -142,7 +142,7 @@ extension HTTP2ServerTransport.Config { } } - public struct HTTP2: Sendable { + public struct HTTP2: Sendable, Hashable { /// The maximum frame size to be used in an HTTP/2 connection. public var maxFrameSize: Int @@ -175,7 +175,7 @@ extension HTTP2ServerTransport.Config { } } - public struct RPC: Sendable { + public struct RPC: Sendable, Hashable { /// The maximum request payload size. public var maxRequestPayloadSize: Int diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index 57e969e3a..f63adf96a 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -16,11 +16,11 @@ public import GRPCCore public import GRPCHTTP2Core // should be @usableFromInline -public import NIOCore +public import NIOCore // has to be public because of EventLoopGroup param in init public import NIOPosix // has to be public because of default argument value in init #if canImport(NIOSSL) -import NIOSSL +private import NIOSSL #endif @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -28,7 +28,7 @@ extension HTTP2ClientTransport { /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOPosix`. /// /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use - /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended + /// on Linux and Darwin based platforms (macOS, iOS, etc.). However, it's *strongly* recommended /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of /// the ``GRPCHTTP2Core/HTTP2ClientTransport``. /// @@ -62,7 +62,7 @@ extension HTTP2ClientTransport { public struct Posix: ClientTransport { private let channel: GRPCChannel - /// Creates a new Posix based HTTP/2 client transport. + /// Creates a new NIOPosix-based HTTP/2 client transport. /// /// - Parameters: /// - target: A target to resolve. @@ -91,7 +91,6 @@ extension HTTP2ClientTransport { ) } - // Configure a connector. self.channel = GRPCChannel( resolver: resolver, connector: try Connector(eventLoopGroup: eventLoopGroup, config: config), diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 0717f0f35..43da51ae0 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -425,24 +425,6 @@ extension HTTP2ServerTransport.Posix { } } -extension NIOCore.SocketAddress { - fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws { - if let ipv4 = socketAddress.ipv4 { - self = try Self(ipv4) - } else if let ipv6 = socketAddress.ipv6 { - self = try Self(ipv6) - } else if let unixDomainSocket = socketAddress.unixDomainSocket { - self = try Self(unixDomainSocket) - } else { - throw RPCError( - code: .internalError, - message: - "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)." - ) - } - } -} - extension ServerBootstrap { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) fileprivate func bind( diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift new file mode 100644 index 000000000..3c10615d6 --- /dev/null +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift @@ -0,0 +1,339 @@ +/* + * 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. + */ + +#if canImport(Network) +public import GRPCCore +public import GRPCHTTP2Core +public import NIOTransportServices // has to be public because of default argument value in init +public import NIOCore // has to be public because of EventLoopGroup param in init + +private import Network + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HTTP2ClientTransport { + /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOTransportServices`. + /// + /// This transport builds on top of SwiftNIO's Transport Services networking layer and is the recommended + /// variant for use on Darwin-based platforms (macOS, iOS, etc.). + /// If you are targeting Linux platforms then you should use the `NIOPosix` variant of + /// the ``GRPCHTTP2Core/HTTP2ClientTransport``. + /// + /// To use this transport you need to provide a 'target' to connect to which will be resolved + /// by an appropriate resolver from the resolver registry. By default the resolver registry can + /// resolve DNS targets, IPv4 and IPv6 targets, and Unix domain socket targets. Virtual Socket + /// targets are not supported with this transport. If you use a custom target you must also provide an + /// appropriately configured registry. + /// + /// You can control various aspects of connection creation, management, security and RPC behavior via + /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via + /// the ``ServiceConfig`` (if it isn't provided by a resolver). + /// + /// Beyond creating the transport you don't need to interact with it directly, instead, pass it + /// to a `GRPCClient`: + /// + /// ```swift + /// try await withThrowingDiscardingTaskGroup { group in + /// let transport = try HTTP2ClientTransport.TransportServices( + /// target: .ipv4(host: "example.com"), + /// config: .defaults(transportSecurity: .plaintext) + /// ) + /// let client = GRPCClient(transport: transport) + /// group.addTask { + /// try await client.run() + /// } + /// + /// // ... + /// } + /// ``` + public struct TransportServices: ClientTransport { + private let channel: GRPCChannel + + public var retryThrottle: RetryThrottle? { + self.channel.retryThrottle + } + + /// Creates a new NIOTransportServices-based HTTP/2 client transport. + /// + /// - Parameters: + /// - target: A target to resolve. + /// - config: Configuration for the transport. + /// - resolverRegistry: A registry of resolver factories. + /// - serviceConfig: Service config controlling how the transport should establish and + /// load-balance connections. + /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must + /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from + /// a `MultiThreadedEventLoopGroup`. + /// - Throws: When no suitable resolver could be found for the `target`. + public init( + target: any ResolvableTarget, + config: Config, + resolverRegistry: NameResolverRegistry = .defaults, + serviceConfig: ServiceConfig = ServiceConfig(), + eventLoopGroup: any EventLoopGroup = .singletonNIOTSEventLoopGroup + ) throws { + guard let resolver = resolverRegistry.makeResolver(for: target) else { + throw RuntimeError( + code: .transportError, + message: """ + No suitable resolvers to resolve '\(target)'. You must make sure that the resolver \ + registry has a suitable name resolver factory registered for the given target. + """ + ) + } + + self.channel = GRPCChannel( + resolver: resolver, + connector: Connector(eventLoopGroup: eventLoopGroup, config: config), + config: GRPCChannel.Config(transportServices: config), + defaultServiceConfig: serviceConfig + ) + } + + public func connect() async throws { + await self.channel.connect() + } + + public func beginGracefulShutdown() { + self.channel.beginGracefulShutdown() + } + + public func withStream( + descriptor: MethodDescriptor, + options: CallOptions, + _ closure: (RPCStream) async throws -> T + ) async throws -> T { + try await self.channel.withStream(descriptor: descriptor, options: options, closure) + } + + public func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { + self.channel.configuration(forMethod: descriptor) + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HTTP2ClientTransport.TransportServices { + struct Connector: HTTP2Connector { + private let config: HTTP2ClientTransport.TransportServices.Config + private let eventLoopGroup: any EventLoopGroup + + init( + eventLoopGroup: any EventLoopGroup, + config: HTTP2ClientTransport.TransportServices.Config + ) { + self.eventLoopGroup = eventLoopGroup + self.config = config + } + + func establishConnection( + to address: GRPCHTTP2Core.SocketAddress + ) async throws -> HTTP2Connection { + let bootstrap: NIOTSConnectionBootstrap + let isPlainText: Bool + switch self.config.transportSecurity.wrapped { + case .plaintext: + isPlainText = true + bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) + + case .tls(let tlsConfig): + isPlainText = false + bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) + .tlsOptions(try NWProtocolTLS.Options(tlsConfig)) + } + + let (channel, multiplexer) = try await bootstrap.connect(to: address) { channel in + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.configureGRPCClientPipeline( + channel: channel, + config: GRPCChannel.Config(transportServices: self.config) + ) + } + } + + return HTTP2Connection( + channel: channel, + multiplexer: multiplexer, + isPlaintext: isPlainText + ) + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HTTP2ClientTransport.TransportServices { + /// Configuration for the `TransportServices` transport. + public struct Config: Sendable { + /// Configuration for HTTP/2 connections. + public var http2: HTTP2ClientTransport.Config.HTTP2 + + /// Configuration for backoff used when establishing a connection. + public var backoff: HTTP2ClientTransport.Config.Backoff + + /// Configuration for connection management. + public var connection: HTTP2ClientTransport.Config.Connection + + /// Compression configuration. + public var compression: HTTP2ClientTransport.Config.Compression + + /// The transport's security. + public var transportSecurity: TransportSecurity + + /// Creates a new connection configuration. + /// + /// - Parameters: + /// - http2: HTTP2 configuration. + /// - backoff: Backoff configuration. + /// - connection: Connection configuration. + /// - compression: Compression configuration. + /// - transportSecurity: The transport's security configuration. + /// + /// - SeeAlso: ``defaults(_:)``. + public init( + http2: HTTP2ClientTransport.Config.HTTP2, + backoff: HTTP2ClientTransport.Config.Backoff, + connection: HTTP2ClientTransport.Config.Connection, + compression: HTTP2ClientTransport.Config.Compression, + transportSecurity: TransportSecurity + ) { + self.http2 = http2 + self.connection = connection + self.backoff = backoff + self.compression = compression + self.transportSecurity = transportSecurity + } + + /// Default values. + /// + /// - Parameters: + /// - transportSecurity: The security settings applied to the transport. + /// - configure: A closure which allows you to modify the defaults before returning them. + public static func defaults( + transportSecurity: TransportSecurity, + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { + var config = Self( + http2: .defaults, + backoff: .defaults, + connection: .defaults, + compression: .defaults, + transportSecurity: transportSecurity + ) + configure(&config) + return config + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension GRPCChannel.Config { + init(transportServices config: HTTP2ClientTransport.TransportServices.Config) { + self.init( + http2: config.http2, + backoff: config.backoff, + connection: config.connection, + compression: config.compression + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension NIOTSConnectionBootstrap { + fileprivate func connect( + to address: GRPCHTTP2Core.SocketAddress, + childChannelInitializer: @escaping @Sendable (any Channel) -> EventLoopFuture + ) async throws -> Output { + if address.virtualSocket != nil { + throw RuntimeError( + code: .transportError, + message: """ + Virtual sockets are not supported by 'HTTP2ClientTransport.TransportServices'. \ + Please use the 'HTTP2ClientTransport.Posix' transport. + """ + ) + } else { + return try await self.connect( + to: NIOCore.SocketAddress(address), + channelInitializer: childChannelInitializer + ) + } + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension ClientTransport where Self == HTTP2ClientTransport.TransportServices { + /// Create a new `TransportServices` based HTTP/2 client transport. + /// + /// - Parameters: + /// - target: A target to resolve. + /// - config: Configuration for the transport. + /// - resolverRegistry: A registry of resolver factories. + /// - serviceConfig: Service config controlling how the transport should establish and + /// load-balance connections. + /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must + /// be a `NIOTSEventLoopGroup` or an `EventLoop` from + /// a `NIOTSEventLoopGroup`. + /// - Throws: When no suitable resolver could be found for the `target`. + public static func http2NIOTS( + target: any ResolvableTarget, + config: HTTP2ClientTransport.TransportServices.Config, + resolverRegistry: NameResolverRegistry = .defaults, + serviceConfig: ServiceConfig = ServiceConfig(), + eventLoopGroup: any EventLoopGroup = .singletonNIOTSEventLoopGroup + ) throws -> Self { + try HTTP2ClientTransport.TransportServices( + target: target, + config: config, + resolverRegistry: resolverRegistry, + serviceConfig: serviceConfig, + eventLoopGroup: eventLoopGroup + ) + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension NWProtocolTLS.Options { + convenience init(_ tlsConfig: HTTP2ClientTransport.TransportServices.Config.TLS) throws { + self.init() + + guard let sec_identity = sec_identity_create(try tlsConfig.identityProvider()) else { + throw RuntimeError( + code: .transportError, + message: """ + There was an issue creating the SecIdentity required to set up TLS. \ + Please check your TLS configuration. + """ + ) + } + + sec_protocol_options_set_local_identity( + self.securityProtocolOptions, + sec_identity + ) + + sec_protocol_options_set_min_tls_protocol_version( + self.securityProtocolOptions, + .TLSv12 + ) + + for `protocol` in ["grpc-exp", "h2"] { + sec_protocol_options_add_tls_application_protocol( + self.securityProtocolOptions, + `protocol` + ) + } + } +} +#endif diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 7b37bcc4d..12001861d 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -367,24 +367,6 @@ extension HTTP2ServerTransport.TransportServices { } } -extension NIOCore.SocketAddress { - fileprivate init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws { - if let ipv4 = socketAddress.ipv4 { - self = try Self(ipv4) - } else if let ipv6 = socketAddress.ipv6 { - self = try Self(ipv6) - } else if let unixDomainSocket = socketAddress.unixDomainSocket { - self = try Self(unixDomainSocket) - } else { - throw RPCError( - code: .internalError, - message: - "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)." - ) - } - } -} - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension NIOTSListenerBootstrap { fileprivate func bind( diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift index 05840a42d..94bc7dcb2 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift @@ -60,4 +60,35 @@ extension HTTP2ServerTransport.TransportServices.Config { } } } + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension HTTP2ClientTransport.TransportServices.Config { + /// The security configuration for this connection. + public struct TransportSecurity: Sendable { + package enum Wrapped: Sendable { + case plaintext + case tls(TLS) + } + + package let wrapped: Wrapped + + /// This connection is plaintext: no encryption will take place. + public static let plaintext = Self(wrapped: .plaintext) + + /// This connection will use TLS. + public static func tls(_ tls: TLS) -> Self { + Self(wrapped: .tls(tls)) + } + } + + public struct TLS: Sendable { + /// A provider for the `SecIdentity` to be used when setting up TLS. + public var identityProvider: @Sendable () throws -> SecIdentity + + /// Create a new HTTP2 NIO Transport Services transport TLS config. + public init(identityProvider: @Sendable @escaping () throws -> SecIdentity) { + self.identityProvider = identityProvider + } + } +} #endif diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index aa6af81c9..2d94ce482 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -170,6 +170,28 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } } + func testServerConfig_Defaults() throws { + let grpcConfig = HTTP2ServerTransport.Posix.Config.defaults( + transportSecurity: .plaintext + ) + + XCTAssertEqual(grpcConfig.compression, HTTP2ServerTransport.Config.Compression.defaults) + XCTAssertEqual(grpcConfig.connection, HTTP2ServerTransport.Config.Connection.defaults) + XCTAssertEqual(grpcConfig.http2, HTTP2ServerTransport.Config.HTTP2.defaults) + XCTAssertEqual(grpcConfig.rpc, HTTP2ServerTransport.Config.RPC.defaults) + } + + func testClientConfig_Defaults() throws { + let grpcConfig = HTTP2ClientTransport.Posix.Config.defaults( + transportSecurity: .plaintext + ) + + XCTAssertEqual(grpcConfig.compression, HTTP2ClientTransport.Config.Compression.defaults) + XCTAssertEqual(grpcConfig.connection, HTTP2ClientTransport.Config.Connection.defaults) + XCTAssertEqual(grpcConfig.http2, HTTP2ClientTransport.Config.HTTP2.defaults) + XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults) + } + #if canImport(NIOSSL) static let samplePemCert = """ -----BEGIN CERTIFICATE----- diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index 8611bdb16..a74d8374e 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -199,13 +199,37 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { } } - func testTLSConfig_Defaults() throws { + func testServerConfig_Defaults() throws { let identityProvider = Self.loadIdentity let grpcTLSConfig = HTTP2ServerTransport.TransportServices.Config.TLS.defaults( identityProvider: identityProvider ) + let grpcConfig = HTTP2ServerTransport.TransportServices.Config.defaults( + transportSecurity: .tls(grpcTLSConfig) + ) + + XCTAssertEqual(grpcConfig.compression, HTTP2ServerTransport.Config.Compression.defaults) + XCTAssertEqual(grpcConfig.connection, HTTP2ServerTransport.Config.Connection.defaults) + XCTAssertEqual(grpcConfig.http2, HTTP2ServerTransport.Config.HTTP2.defaults) + XCTAssertEqual(grpcConfig.rpc, HTTP2ServerTransport.Config.RPC.defaults) XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider()) XCTAssertEqual(grpcTLSConfig.requireALPN, false) } + + func testClientConfig_Defaults() throws { + let identityProvider = Self.loadIdentity + let grpcTLSConfig = HTTP2ClientTransport.TransportServices.Config.TLS( + identityProvider: identityProvider + ) + let grpcConfig = HTTP2ClientTransport.TransportServices.Config.defaults( + transportSecurity: .tls(grpcTLSConfig) + ) + + XCTAssertEqual(grpcConfig.compression, HTTP2ClientTransport.Config.Compression.defaults) + XCTAssertEqual(grpcConfig.connection, HTTP2ClientTransport.Config.Connection.defaults) + XCTAssertEqual(grpcConfig.http2, HTTP2ClientTransport.Config.HTTP2.defaults) + XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults) + XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider()) + } } #endif diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift index 14a9d69c3..108cc709a 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift @@ -101,7 +101,7 @@ final class HTTP2TransportTests: XCTestCase { } func forEachClientAndHTTPStatusCodeServer( - _ kind: [Transport.Kind] = [.posix], + _ kind: [Transport.Kind] = [.posix, .niots], _ execute: (ControlClient, Transport.Kind) async throws -> Void ) async throws { for clientKind in kind { @@ -207,7 +207,20 @@ final class HTTP2TransportTests: XCTestCase { ) case .niots: - fatalError("NIOTS isn't supported yet") + #if canImport(Network) + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + transport = try HTTP2ClientTransport.TransportServices( + target: target, + config: .defaults(transportSecurity: .plaintext) { + $0.compression.algorithm = compression + $0.compression.enabledAlgorithms = enabledCompression + }, + serviceConfig: serviceConfig + ) + #else + throw XCTSkip("Transport not supported on this platform") + #endif } return GRPCClient(transport: transport) @@ -1421,7 +1434,9 @@ final class HTTP2TransportTests: XCTestCase { extension [HTTP2TransportTests.Transport] { static let supported = [ HTTP2TransportTests.Transport(server: .posix, client: .posix), + HTTP2TransportTests.Transport(server: .niots, client: .niots), HTTP2TransportTests.Transport(server: .niots, client: .posix), + HTTP2TransportTests.Transport(server: .posix, client: .niots), ] } From c51784ed350eba463cbe6dc1606264ca69ec8a84 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 11 Sep 2024 15:50:02 +0100 Subject: [PATCH 463/580] Adopt swift-testing for Timeout tests (#2051) Motivation: swift-testing has a number of advantages over XCTest (parameterisation, organisation, failure messages etc.), we should start using it instead of XCTest. Modifications: - Convert the Timeout tests Results: Better tests --- Tests/GRPCCoreTests/TimeoutTests.swift | 249 +++++++------------------ 1 file changed, 64 insertions(+), 185 deletions(-) diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index 80be5ea1f..a22bb32be 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -13,193 +13,72 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import XCTest -@testable import GRPCCore - -@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) -final class TimeoutTests: XCTestCase { - func testDecodeInvalidTimeout_Empty() { - let timeoutHeader = "" - XCTAssertNil(Timeout(decoding: timeoutHeader)) - } - - func testDecodeInvalidTimeout_NoAmount() { - let timeoutHeader = "H" - XCTAssertNil(Timeout(decoding: timeoutHeader)) - } - - func testDecodeInvalidTimeout_NoUnit() { - let timeoutHeader = "123" - XCTAssertNil(Timeout(decoding: timeoutHeader)) - } - - func testDecodeInvalidTimeout_TooLongAmount() { - let timeoutHeader = "100000000S" - XCTAssertNil(Timeout(decoding: timeoutHeader)) - } - - func testDecodeInvalidTimeout_InvalidUnit() { - let timeoutHeader = "123j" - XCTAssertNil(Timeout(decoding: timeoutHeader)) - } - - func testDecodeValidTimeout_Hours() throws { - let timeoutHeader = "123H" - let timeout = Timeout(decoding: timeoutHeader) - let unwrappedTimeout = try XCTUnwrap(timeout) - XCTAssertEqual(unwrappedTimeout.duration, Duration.hours(123)) - XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) - } - - func testDecodeValidTimeout_Minutes() throws { - let timeoutHeader = "123M" - let timeout = Timeout(decoding: timeoutHeader) - let unwrappedTimeout = try XCTUnwrap(timeout) - XCTAssertEqual(unwrappedTimeout.duration, Duration.minutes(123)) - XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) - } - - func testDecodeValidTimeout_Seconds() throws { - let timeoutHeader = "123S" - let timeout = Timeout(decoding: timeoutHeader) - let unwrappedTimeout = try XCTUnwrap(timeout) - XCTAssertEqual(unwrappedTimeout.duration, Duration.seconds(123)) - XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) - } - - func testDecodeValidTimeout_Milliseconds() throws { - let timeoutHeader = "123m" - let timeout = Timeout(decoding: timeoutHeader) - let unwrappedTimeout = try XCTUnwrap(timeout) - XCTAssertEqual(unwrappedTimeout.duration, Duration.milliseconds(123)) - XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) - } - - func testDecodeValidTimeout_Microseconds() throws { - let timeoutHeader = "123u" - let timeout = Timeout(decoding: timeoutHeader) - let unwrappedTimeout = try XCTUnwrap(timeout) - XCTAssertEqual(unwrappedTimeout.duration, Duration.microseconds(123)) - XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) - } - - func testDecodeValidTimeout_Nanoseconds() throws { - let timeoutHeader = "123n" - let timeout = Timeout(decoding: timeoutHeader) - let unwrappedTimeout = try XCTUnwrap(timeout) - XCTAssertEqual(unwrappedTimeout.duration, Duration.nanoseconds(123)) - XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) - } +import Testing - func testEncodeValidTimeout_Hours() { - let duration = Duration.hours(123) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) - } - - func testEncodeValidTimeout_Minutes() { - let duration = Duration.minutes(43) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) - } - - func testEncodeValidTimeout_Seconds() { - let duration = Duration.seconds(12345) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) - } - - func testEncodeValidTimeout_Seconds_TooLong_Minutes() { - let duration = Duration.seconds(111_111_111) - let timeout = Timeout(duration: duration) - // The conversion from seconds to minutes results in a loss of precision. - // 111,111,111 seconds / 60 = 1,851,851.85 minutes -rounding up-> 1,851,852 minutes * 60 = 111,111,120 seconds - let expectedRoundedDuration = Duration.minutes(1_851_852) - XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual( - timeout.duration.components.attoseconds, - expectedRoundedDuration.components.attoseconds - ) - } - - func testEncodeValidTimeout_Seconds_TooLong_Hours() { - let duration = Duration.seconds(9_999_999_999 as Int64) - let timeout = Timeout(duration: duration) - // The conversion from seconds to hours results in a loss of precision. - // 9,999,999,999 seconds / 60 = 166,666,666.65 minutes -rounding up-> - // 166,666,667 minutes / 60 = 2,777,777.78 hours -rounding up-> - // 2,777,778 hours * 60 -> 166,666,680 minutes * 60 = 10,000,000,800 seconds - let expectedRoundedDuration = Duration.hours(2_777_778) - XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual( - timeout.duration.components.attoseconds, - expectedRoundedDuration.components.attoseconds - ) - } - - func testEncodeValidTimeout_Seconds_TooLong_MaxAmount() { - let duration = Duration.seconds(999_999_999_999 as Int64) - let timeout = Timeout(duration: duration) - // The conversion from seconds to hours results in a number that still has - // more than the maximum allowed 8 digits, so we must clamp it. - // Make sure that `Timeout.maxAmount` is the amount used for the resulting timeout. - let expectedRoundedDuration = Duration.hours(Timeout.maxAmount) - XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual( - timeout.duration.components.attoseconds, - expectedRoundedDuration.components.attoseconds - ) - } - - func testEncodeValidTimeout_SecondsAndMilliseconds() { - let duration = Duration(secondsComponent: 100, attosecondsComponent: Int64(1e+17)) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) - } - - func testEncodeValidTimeout_SecondsAndMicroseconds() { - let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+14)) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) - } - - func testEncodeValidTimeout_SecondsAndNanoseconds() { - let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+11)) - let timeout = Timeout(duration: duration) - // We can't convert seconds to nanoseconds because that would require at least - // 9 digits, and the maximum allowed is 8: we expect to simply drop the nanoseconds. - let expectedRoundedDuration = Duration.seconds(1) - XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual( - timeout.duration.components.attoseconds, - expectedRoundedDuration.components.attoseconds - ) - } - - func testEncodeValidTimeout_Milliseconds() { - let duration = Duration.milliseconds(100) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) - } - - func testEncodeValidTimeout_Microseconds() { - let duration = Duration.microseconds(100) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) - } +@testable import GRPCCore - func testEncodeValidTimeout_Nanoseconds() { - let duration = Duration.nanoseconds(100) - let timeout = Timeout(duration: duration) - XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) +struct TimeoutTests { + @Test("Initialize from invalid String value", arguments: ["", "H", "123", "100000000S", "123j"]) + func initFromStringWithInvalidValue(_ value: String) throws { + #expect(Timeout(decoding: value) == nil) + } + + @Test( + "Initialize from String", + arguments: [ + ("123H", .hours(123)), + ("123M", .minutes(123)), + ("123S", .seconds(123)), + ("123m", .milliseconds(123)), + ("123u", .microseconds(123)), + ("123n", .nanoseconds(123)), + ] as [(String, Duration)] + ) + func initFromString(_ value: String, expected: Duration) throws { + let timeout = try #require(Timeout(decoding: value)) + #expect(timeout.duration == expected) + } + + @Test( + "Initialize from Duration", + arguments: [ + .hours(123), + .minutes(43), + .seconds(12345), + .milliseconds(100), + .microseconds(100), + .nanoseconds(100), + ] as [Duration] + ) + func initFromDuration(_ value: Duration) { + let timeout = Timeout(duration: value) + #expect(timeout.duration == value) + } + + @Test( + "Initialize from Duration with loss of precision", + arguments: [ + // 111,111,111 seconds / 60 = 1,851,851.85 minutes -rounding up-> 1,851,852 minutes * 60 = 111,111,120 seconds + (.seconds(111_111_111), .minutes(1_851_852)), + + // 9,999,999,999 seconds / 60 = 166,666,666.65 minutes -rounding up-> + // 166,666,667 minutes / 60 = 2,777,777.78 hours -rounding up-> + // 2,777,778 hours * 60 -> 166,666,680 minutes * 60 = 10,000,000,800 seconds + (.seconds(9_999_999_999 as Int64), .hours(2_777_778)), + + // The conversion from seconds to hours results in a number that still has + // more than the maximum allowed 8 digits, so we must clamp it. + // Make sure that `Timeout.maxAmount` is the amount used for the resulting timeout. + (.seconds(999_999_999_999 as Int64), .hours(Timeout.maxAmount)), + + // We can't convert seconds to nanoseconds because that would require at least + // 9 digits, and the maximum allowed is 8: we expect to simply drop the nanoseconds. + (Duration(secondsComponent: 1, attosecondsComponent: Int64(1e11)), .seconds(1)), + ] as [(Duration, Duration)] + ) + func initFromDurationWithLossOfPrecision(original: Duration, rounded: Duration) { + let timeout = Timeout(duration: original) + #expect(timeout.duration == rounded) } } From f3380d217ebd7f5a939115736bcc19fe1752fee9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 16 Sep 2024 17:08:21 +0100 Subject: [PATCH 464/580] Factor out shared http2 server transport code (#2058) Motivation: The NIOPosix and NIOTS HTTP/2 server transports have quite a lot of shared code. The only meaningfull difference between them is how they create their channels. Modification: - Factor out the shared parts to a `CommonHTTP2ServerTransport` - Allow each tranport to inject a factory for creating a listener Result: Less duplication --- Package@swift-6.swift | 1 + .../Server/CommonHTTP2ServerTransport.swift | 254 ++++++++++++++ .../Server/HTTP2ListenerFactory.swift | 35 ++ .../HTTP2ServerTransport+Posix.swift | 332 ++++-------------- ...TP2ServerTransport+TransportServices.swift | 289 +++------------ 5 files changed, 419 insertions(+), 492 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift create mode 100644 Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift diff --git a/Package@swift-6.swift b/Package@swift-6.swift index e7ee68e33..e7b1c268a 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -251,6 +251,7 @@ extension Target { .nioCore, .nioHTTP2, .nioTLS, + .nioExtras, .cgrpcZlib, .dequeModule, ], diff --git a/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift new file mode 100644 index 000000000..a900b5402 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift @@ -0,0 +1,254 @@ +/* + * 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. + */ + +package import GRPCCore +package import NIOCore +package import NIOExtras +private import NIOHTTP2 +private import Synchronization + +/// Provides the common functionality for a `NIO`-based server transport. +/// +/// - SeeAlso: ``HTTP2ListenerFactory``. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package final class CommonHTTP2ServerTransport< + ListenerFactory: HTTP2ListenerFactory +>: ServerTransport, ListeningServerTransport { + private let eventLoopGroup: any EventLoopGroup + private let address: SocketAddress + private let listeningAddressState: Mutex + private let serverQuiescingHelper: ServerQuiescingHelper + private let factory: ListenerFactory + + private enum State { + case idle(EventLoopPromise) + case listening(EventLoopFuture) + case closedOrInvalidAddress(RuntimeError) + + var listeningAddressFuture: EventLoopFuture { + get throws { + switch self { + case .idle(let eventLoopPromise): + return eventLoopPromise.futureResult + case .listening(let eventLoopFuture): + return eventLoopFuture + case .closedOrInvalidAddress(let runtimeError): + throw runtimeError + } + } + } + + enum OnBound { + case succeedPromise(_ promise: EventLoopPromise, address: SocketAddress) + case failPromise(_ promise: EventLoopPromise, error: RuntimeError) + } + + mutating func addressBound( + _ address: NIOCore.SocketAddress?, + userProvidedAddress: SocketAddress + ) -> OnBound { + switch self { + case .idle(let listeningAddressPromise): + if let address { + self = .listening(listeningAddressPromise.futureResult) + return .succeedPromise(listeningAddressPromise, address: SocketAddress(address)) + } else if userProvidedAddress.virtualSocket != nil { + self = .listening(listeningAddressPromise.futureResult) + return .succeedPromise(listeningAddressPromise, address: userProvidedAddress) + } else { + assertionFailure("Unknown address type") + let invalidAddressError = RuntimeError( + code: .transportError, + message: "Unknown address type returned by transport." + ) + self = .closedOrInvalidAddress(invalidAddressError) + return .failPromise(listeningAddressPromise, error: invalidAddressError) + } + + case .listening, .closedOrInvalidAddress: + fatalError("Invalid state: addressBound should only be called once and when in idle state") + } + } + + enum OnClose { + case failPromise(EventLoopPromise, error: RuntimeError) + case doNothing + } + + mutating func close() -> OnClose { + let serverStoppedError = RuntimeError( + code: .serverIsStopped, + message: """ + There is no listening address bound for this server: there may have been \ + an error which caused the transport to close, or it may have shut down. + """ + ) + + switch self { + case .idle(let listeningAddressPromise): + self = .closedOrInvalidAddress(serverStoppedError) + return .failPromise(listeningAddressPromise, error: serverStoppedError) + + case .listening: + self = .closedOrInvalidAddress(serverStoppedError) + return .doNothing + + case .closedOrInvalidAddress: + return .doNothing + } + } + } + + /// The listening address for this server transport. + /// + /// It is an `async` property because it will only return once the address has been successfully bound. + /// + /// - Throws: A runtime error will be thrown if the address could not be bound or is not bound any + /// longer, because the transport isn't listening anymore. It can also throw if the transport returned an + /// invalid address. + package var listeningAddress: SocketAddress { + get async throws { + try await self.listeningAddressState + .withLock { try $0.listeningAddressFuture } + .get() + } + } + + package init( + address: SocketAddress, + eventLoopGroup: any EventLoopGroup, + quiescingHelper: ServerQuiescingHelper, + listenerFactory: ListenerFactory + ) { + self.eventLoopGroup = eventLoopGroup + self.address = address + + let eventLoop = eventLoopGroup.any() + self.listeningAddressState = Mutex(.idle(eventLoop.makePromise())) + + self.factory = listenerFactory + self.serverQuiescingHelper = quiescingHelper + } + + package func listen( + _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + ) async throws { + defer { + switch self.listeningAddressState.withLock({ $0.close() }) { + case .failPromise(let promise, let error): + promise.fail(error) + case .doNothing: + () + } + } + + let serverChannel = try await self.factory.makeListeningChannel( + eventLoopGroup: self.eventLoopGroup, + address: self.address, + serverQuiescingHelper: self.serverQuiescingHelper + ) + + let action = self.listeningAddressState.withLock { + $0.addressBound( + serverChannel.channel.localAddress, + userProvidedAddress: self.address + ) + } + switch action { + case .succeedPromise(let promise, let address): + promise.succeed(address) + case .failPromise(let promise, let error): + promise.fail(error) + } + + try await serverChannel.executeThenClose { inbound in + try await withThrowingDiscardingTaskGroup { group in + for try await (connectionChannel, streamMultiplexer) in inbound { + group.addTask { + try await self.handleConnection( + connectionChannel, + multiplexer: streamMultiplexer, + streamHandler: streamHandler + ) + } + } + } + } + } + + private func handleConnection( + _ connection: NIOAsyncChannel, + multiplexer: ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer, + streamHandler: @escaping @Sendable (RPCStream) async -> Void + ) async throws { + try await connection.executeThenClose { inbound, _ in + await withDiscardingTaskGroup { group in + group.addTask { + do { + for try await _ in inbound {} + } catch { + // We don't want to close the channel if one connection throws. + return + } + } + + do { + for try await (stream, descriptor) in multiplexer.inbound { + group.addTask { + await self.handleStream(stream, handler: streamHandler, descriptor: descriptor) + } + } + } catch { + return + } + } + } + } + + private func handleStream( + _ stream: NIOAsyncChannel, + handler streamHandler: @escaping @Sendable (RPCStream) async -> Void, + descriptor: EventLoopFuture + ) async { + // It's okay to ignore these errors: + // - If we get an error because the http2Stream failed to close, then there's nothing we can do + // - If we get an error because the inner closure threw, then the only possible scenario in which + // that could happen is if methodDescriptor.get() throws - in which case, it means we never got + // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. + try? await stream.executeThenClose { inbound, outbound in + guard let descriptor = try? await descriptor.get() else { + return + } + + let rpcStream = RPCStream( + descriptor: descriptor, + inbound: RPCAsyncSequence(wrapping: inbound), + outbound: RPCWriter.Closable( + wrapping: ServerConnection.Stream.Outbound( + responseWriter: outbound, + http2Stream: stream + ) + ) + ) + + await streamHandler(rpcStream) + } + } + + package func beginGracefulShutdown() { + self.serverQuiescingHelper.initiateShutdown(promise: nil) + } +} diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift new file mode 100644 index 000000000..900799a61 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift @@ -0,0 +1,35 @@ +/* + * 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. + */ + +package import NIOCore +package import NIOExtras + +/// A factory to produce `NIOAsyncChannel`s to listen for new HTTP/2 connections. +/// +/// - SeeAlso: ``CommonHTTP2ServerTransport`` +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +package protocol HTTP2ListenerFactory: Sendable { + typealias AcceptedChannel = ( + ChannelPipeline.SynchronousOperations.HTTP2ConnectionChannel, + ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer + ) + + func makeListeningChannel( + eventLoopGroup: any EventLoopGroup, + address: SocketAddress, + serverQuiescingHelper: ServerQuiescingHelper + ) async throws -> NIOAsyncChannel +} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 43da51ae0..d7bda6350 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -55,108 +55,78 @@ extension HTTP2ServerTransport { /// } /// ``` @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public final class Posix: ServerTransport, ListeningServerTransport { - private let address: GRPCHTTP2Core.SocketAddress - private let config: Config - private let eventLoopGroup: MultiThreadedEventLoopGroup - private let serverQuiescingHelper: ServerQuiescingHelper - - private enum State { - case idle(EventLoopPromise) - case listening(EventLoopFuture) - case closedOrInvalidAddress(RuntimeError) - - var listeningAddressFuture: EventLoopFuture { - get throws { - switch self { - case .idle(let eventLoopPromise): - return eventLoopPromise.futureResult - case .listening(let eventLoopFuture): - return eventLoopFuture - case .closedOrInvalidAddress(let runtimeError): - throw runtimeError - } - } - } - - enum OnBound { - case succeedPromise( - _ promise: EventLoopPromise, - address: GRPCHTTP2Core.SocketAddress - ) - case failPromise( - _ promise: EventLoopPromise, - error: RuntimeError - ) - } - - mutating func addressBound( - _ address: NIOCore.SocketAddress?, - userProvidedAddress: GRPCHTTP2Core.SocketAddress - ) -> OnBound { - switch self { - case .idle(let listeningAddressPromise): - if let address { - self = .listening(listeningAddressPromise.futureResult) - return .succeedPromise( - listeningAddressPromise, - address: GRPCHTTP2Core.SocketAddress(address) - ) - - } else if userProvidedAddress.virtualSocket != nil { - self = .listening(listeningAddressPromise.futureResult) - return .succeedPromise(listeningAddressPromise, address: userProvidedAddress) - - } else { - assertionFailure("Unknown address type") - let invalidAddressError = RuntimeError( + public struct Posix: ServerTransport, ListeningServerTransport { + private struct ListenerFactory: HTTP2ListenerFactory { + let config: Config + + func makeListeningChannel( + eventLoopGroup: any EventLoopGroup, + address: GRPCHTTP2Core.SocketAddress, + serverQuiescingHelper: ServerQuiescingHelper + ) async throws -> NIOAsyncChannel { + #if canImport(NIOSSL) + let sslContext: NIOSSLContext? + + switch self.config.transportSecurity.wrapped { + case .plaintext: + sslContext = nil + case .tls(let tlsConfig): + do { + sslContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig)) + } catch { + throw RuntimeError( code: .transportError, - message: "Unknown address type returned by transport." + message: "Couldn't create SSL context, check your TLS configuration.", + cause: error ) - self = .closedOrInvalidAddress(invalidAddressError) - return .failPromise(listeningAddressPromise, error: invalidAddressError) } - - case .listening, .closedOrInvalidAddress: - fatalError( - "Invalid state: addressBound should only be called once and when in idle state" - ) } - } - - enum OnClose { - case failPromise( - EventLoopPromise, - error: RuntimeError - ) - case doNothing - } - - mutating func close() -> OnClose { - let serverStoppedError = RuntimeError( - code: .serverIsStopped, - message: """ - There is no listening address bound for this server: there may have been \ - an error which caused the transport to close, or it may have shut down. - """ - ) + #endif - switch self { - case .idle(let listeningAddressPromise): - self = .closedOrInvalidAddress(serverStoppedError) - return .failPromise(listeningAddressPromise, error: serverStoppedError) + let serverChannel = try await ServerBootstrap(group: eventLoopGroup) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .serverChannelInitializer { channel in + let quiescingHandler = serverQuiescingHelper.makeServerChannelHandler(channel: channel) + return channel.pipeline.addHandler(quiescingHandler) + } + .bind(to: address) { channel in + channel.eventLoop.makeCompletedFuture { + #if canImport(NIOSSL) + if let sslContext { + try channel.pipeline.syncOperations.addHandler( + NIOSSLServerHandler(context: sslContext) + ) + } + #endif + + let requireALPN: Bool + let scheme: Scheme + switch self.config.transportSecurity.wrapped { + case .plaintext: + requireALPN = false + scheme = .http + case .tls(let tlsConfig): + requireALPN = tlsConfig.requireALPN + scheme = .https + } - case .listening: - self = .closedOrInvalidAddress(serverStoppedError) - return .doNothing + return try channel.pipeline.syncOperations.configureGRPCServerPipeline( + channel: channel, + compressionConfig: self.config.compression, + connectionConfig: self.config.connection, + http2Config: self.config.http2, + rpcConfig: self.config.rpc, + requireALPN: requireALPN, + scheme: scheme + ) + } + } - case .closedOrInvalidAddress: - return .doNothing - } + return serverChannel } } - private let listeningAddressState: Mutex + private let underlyingTransport: CommonHTTP2ServerTransport /// The listening address for this server transport. /// @@ -167,9 +137,7 @@ extension HTTP2ServerTransport { /// invalid address. public var listeningAddress: GRPCHTTP2Core.SocketAddress { get async throws { - try await self.listeningAddressState - .withLock { try $0.listeningAddressFuture } - .get() + try await self.underlyingTransport.listeningAddress } } @@ -184,178 +152,24 @@ extension HTTP2ServerTransport { config: Config, eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup ) { - self.address = address - self.config = config - self.eventLoopGroup = eventLoopGroup - self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) - - let eventLoop = eventLoopGroup.any() - self.listeningAddressState = Mutex(.idle(eventLoop.makePromise())) + let factory = ListenerFactory(config: config) + let helper = ServerQuiescingHelper(group: eventLoopGroup) + self.underlyingTransport = CommonHTTP2ServerTransport( + address: address, + eventLoopGroup: eventLoopGroup, + quiescingHelper: helper, + listenerFactory: factory + ) } public func listen( _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { - defer { - switch self.listeningAddressState.withLock({ $0.close() }) { - case .failPromise(let promise, let error): - promise.fail(error) - case .doNothing: - () - } - } - - #if canImport(NIOSSL) - let nioSSLContext: NIOSSLContext? - switch self.config.transportSecurity.wrapped { - case .plaintext: - nioSSLContext = nil - case .tls(let tlsConfig): - do { - nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig)) - } catch { - throw RuntimeError( - code: .transportError, - message: "Couldn't create SSL context, check your TLS configuration.", - cause: error - ) - } - } - #endif - - let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup) - .serverChannelOption( - ChannelOptions.socketOption(.so_reuseaddr), - value: 1 - ) - .serverChannelInitializer { channel in - let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( - channel: channel - ) - return channel.pipeline.addHandler(quiescingHandler) - } - .bind(to: self.address) { channel in - channel.eventLoop.makeCompletedFuture { - #if canImport(NIOSSL) - if let nioSSLContext { - try channel.pipeline.syncOperations.addHandler( - NIOSSLServerHandler(context: nioSSLContext) - ) - } - #endif - - let requireALPN: Bool - let scheme: Scheme - switch self.config.transportSecurity.wrapped { - case .plaintext: - requireALPN = false - scheme = .http - case .tls(let tlsConfig): - requireALPN = tlsConfig.requireALPN - scheme = .https - } - - return try channel.pipeline.syncOperations.configureGRPCServerPipeline( - channel: channel, - compressionConfig: self.config.compression, - connectionConfig: self.config.connection, - http2Config: self.config.http2, - rpcConfig: self.config.rpc, - requireALPN: requireALPN, - scheme: scheme - ) - } - } - - let action = self.listeningAddressState.withLock { - $0.addressBound( - serverChannel.channel.localAddress, - userProvidedAddress: self.address - ) - } - switch action { - case .succeedPromise(let promise, let address): - promise.succeed(address) - case .failPromise(let promise, let error): - promise.fail(error) - } - - try await serverChannel.executeThenClose { inbound in - try await withThrowingDiscardingTaskGroup { group in - for try await (connectionChannel, streamMultiplexer) in inbound { - group.addTask { - try await self.handleConnection( - connectionChannel, - multiplexer: streamMultiplexer, - streamHandler: streamHandler - ) - } - } - } - } - } - - private func handleConnection( - _ connection: NIOAsyncChannel, - multiplexer: ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer, - streamHandler: @escaping @Sendable (RPCStream) async -> Void - ) async throws { - try await connection.executeThenClose { inbound, _ in - await withDiscardingTaskGroup { group in - group.addTask { - do { - for try await _ in inbound {} - } catch { - // We don't want to close the channel if one connection throws. - return - } - } - - do { - for try await (stream, descriptor) in multiplexer.inbound { - group.addTask { - await self.handleStream(stream, handler: streamHandler, descriptor: descriptor) - } - } - } catch { - return - } - } - } - } - - private func handleStream( - _ stream: NIOAsyncChannel, - handler streamHandler: @escaping @Sendable (RPCStream) async -> Void, - descriptor: EventLoopFuture - ) async { - // It's okay to ignore these errors: - // - If we get an error because the http2Stream failed to close, then there's nothing we can do - // - If we get an error because the inner closure threw, then the only possible scenario in which - // that could happen is if methodDescriptor.get() throws - in which case, it means we never got - // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. - try? await stream.executeThenClose { inbound, outbound in - guard let descriptor = try? await descriptor.get() else { - return - } - - let rpcStream = RPCStream( - descriptor: descriptor, - inbound: RPCAsyncSequence(wrapping: inbound), - outbound: RPCWriter.Closable( - wrapping: ServerConnection.Stream.Outbound( - responseWriter: outbound, - http2Stream: stream - ) - ) - ) - - await streamHandler(rpcStream) - } + try await self.underlyingTransport.listen(streamHandler) } public func beginGracefulShutdown() { - self.serverQuiescingHelper.initiateShutdown(promise: nil) + self.underlyingTransport.beginGracefulShutdown() } } } diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 12001861d..94628e215 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -29,101 +29,58 @@ private import Synchronization extension HTTP2ServerTransport { /// A NIO Transport Services-backed implementation of a server transport. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public final class TransportServices: ServerTransport, ListeningServerTransport { - private let address: GRPCHTTP2Core.SocketAddress - private let config: Config - private let eventLoopGroup: NIOTSEventLoopGroup - private let serverQuiescingHelper: ServerQuiescingHelper - - private enum State { - case idle(EventLoopPromise) - case listening(EventLoopFuture) - case closedOrInvalidAddress(RuntimeError) - - var listeningAddressFuture: EventLoopFuture { - get throws { - switch self { - case .idle(let eventLoopPromise): - return eventLoopPromise.futureResult - case .listening(let eventLoopFuture): - return eventLoopFuture - case .closedOrInvalidAddress(let runtimeError): - throw runtimeError - } + public struct TransportServices: ServerTransport, ListeningServerTransport { + private struct ListenerFactory: HTTP2ListenerFactory { + let config: Config + + func makeListeningChannel( + eventLoopGroup: any EventLoopGroup, + address: GRPCHTTP2Core.SocketAddress, + serverQuiescingHelper: ServerQuiescingHelper + ) async throws -> NIOAsyncChannel { + let bootstrap: NIOTSListenerBootstrap + + let requireALPN: Bool + let scheme: Scheme + switch self.config.transportSecurity.wrapped { + case .plaintext: + requireALPN = false + scheme = .http + bootstrap = NIOTSListenerBootstrap(group: eventLoopGroup) + + case .tls(let tlsConfig): + requireALPN = tlsConfig.requireALPN + scheme = .https + bootstrap = NIOTSListenerBootstrap(group: eventLoopGroup) + .tlsOptions(try NWProtocolTLS.Options(tlsConfig)) } - } - - enum OnBound { - case succeedPromise( - _ promise: EventLoopPromise, - address: GRPCHTTP2Core.SocketAddress - ) - case failPromise( - _ promise: EventLoopPromise, - error: RuntimeError - ) - } - - mutating func addressBound(_ address: NIOCore.SocketAddress?) -> OnBound { - switch self { - case .idle(let listeningAddressPromise): - if let address { - self = .listening(listeningAddressPromise.futureResult) - return .succeedPromise( - listeningAddressPromise, - address: GRPCHTTP2Core.SocketAddress(address) - ) - } else { - assertionFailure("Unknown address type") - let invalidAddressError = RuntimeError( - code: .transportError, - message: "Unknown address type returned by transport." - ) - self = .closedOrInvalidAddress(invalidAddressError) - return .failPromise(listeningAddressPromise, error: invalidAddressError) + let serverChannel = + try await bootstrap + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .serverChannelInitializer { channel in + let quiescingHandler = serverQuiescingHelper.makeServerChannelHandler(channel: channel) + return channel.pipeline.addHandler(quiescingHandler) + } + .bind(to: address) { channel in + channel.eventLoop.makeCompletedFuture { + return try channel.pipeline.syncOperations.configureGRPCServerPipeline( + channel: channel, + compressionConfig: self.config.compression, + connectionConfig: self.config.connection, + http2Config: self.config.http2, + rpcConfig: self.config.rpc, + requireALPN: requireALPN, + scheme: scheme + ) + } } - case .listening, .closedOrInvalidAddress: - fatalError( - "Invalid state: addressBound should only be called once and when in idle state" - ) - } - } - - enum OnClose { - case failPromise( - EventLoopPromise, - error: RuntimeError - ) - case doNothing - } - - mutating func close() -> OnClose { - let serverStoppedError = RuntimeError( - code: .serverIsStopped, - message: """ - There is no listening address bound for this server: there may have been \ - an error which caused the transport to close, or it may have shut down. - """ - ) - - switch self { - case .idle(let listeningAddressPromise): - self = .closedOrInvalidAddress(serverStoppedError) - return .failPromise(listeningAddressPromise, error: serverStoppedError) - - case .listening: - self = .closedOrInvalidAddress(serverStoppedError) - return .doNothing - - case .closedOrInvalidAddress: - return .doNothing - } + return serverChannel } } - private let listeningAddressState: Mutex + private let underlyingTransport: CommonHTTP2ServerTransport /// The listening address for this server transport. /// @@ -134,9 +91,7 @@ extension HTTP2ServerTransport { /// invalid address. public var listeningAddress: GRPCHTTP2Core.SocketAddress { get async throws { - try await self.listeningAddressState - .withLock { try $0.listeningAddressFuture } - .get() + try await self.underlyingTransport.listeningAddress } } @@ -151,156 +106,24 @@ extension HTTP2ServerTransport { config: Config, eventLoopGroup: NIOTSEventLoopGroup = .singletonNIOTSEventLoopGroup ) { - self.address = address - self.config = config - self.eventLoopGroup = eventLoopGroup - self.serverQuiescingHelper = ServerQuiescingHelper(group: self.eventLoopGroup) - - let eventLoop = eventLoopGroup.any() - self.listeningAddressState = Mutex(.idle(eventLoop.makePromise())) + let factory = ListenerFactory(config: config) + let helper = ServerQuiescingHelper(group: eventLoopGroup) + self.underlyingTransport = CommonHTTP2ServerTransport( + address: address, + eventLoopGroup: eventLoopGroup, + quiescingHelper: helper, + listenerFactory: factory + ) } public func listen( _ streamHandler: @escaping @Sendable (RPCStream) async -> Void ) async throws { - defer { - switch self.listeningAddressState.withLock({ $0.close() }) { - case .failPromise(let promise, let error): - promise.fail(error) - case .doNothing: - () - } - } - - let bootstrap: NIOTSListenerBootstrap - - let requireALPN: Bool - let scheme: Scheme - switch self.config.transportSecurity.wrapped { - case .plaintext: - requireALPN = false - scheme = .http - bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup) - - case .tls(let tlsConfig): - requireALPN = tlsConfig.requireALPN - scheme = .https - bootstrap = NIOTSListenerBootstrap(group: self.eventLoopGroup) - .tlsOptions(try NWProtocolTLS.Options(tlsConfig)) - } - - let serverChannel = - try await bootstrap - .serverChannelOption( - ChannelOptions.socketOption(.so_reuseaddr), - value: 1 - ) - .serverChannelInitializer { channel in - let quiescingHandler = self.serverQuiescingHelper.makeServerChannelHandler( - channel: channel - ) - return channel.pipeline.addHandler(quiescingHandler) - } - .bind(to: self.address) { channel in - channel.eventLoop.makeCompletedFuture { - return try channel.pipeline.syncOperations.configureGRPCServerPipeline( - channel: channel, - compressionConfig: self.config.compression, - connectionConfig: self.config.connection, - http2Config: self.config.http2, - rpcConfig: self.config.rpc, - requireALPN: requireALPN, - scheme: scheme - ) - } - } - - let action = self.listeningAddressState.withLock { - $0.addressBound(serverChannel.channel.localAddress) - } - switch action { - case .succeedPromise(let promise, let address): - promise.succeed(address) - case .failPromise(let promise, let error): - promise.fail(error) - } - - try await serverChannel.executeThenClose { inbound in - try await withThrowingDiscardingTaskGroup { group in - for try await (connectionChannel, streamMultiplexer) in inbound { - group.addTask { - try await self.handleConnection( - connectionChannel, - multiplexer: streamMultiplexer, - streamHandler: streamHandler - ) - } - } - } - } - } - - private func handleConnection( - _ connection: NIOAsyncChannel, - multiplexer: ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer, - streamHandler: @escaping @Sendable (RPCStream) async -> Void - ) async throws { - try await connection.executeThenClose { inbound, _ in - await withDiscardingTaskGroup { group in - group.addTask { - do { - for try await _ in inbound {} - } catch { - // We don't want to close the channel if one connection throws. - return - } - } - - do { - for try await (stream, descriptor) in multiplexer.inbound { - group.addTask { - await self.handleStream(stream, handler: streamHandler, descriptor: descriptor) - } - } - } catch { - return - } - } - } - } - - private func handleStream( - _ stream: NIOAsyncChannel, - handler streamHandler: @escaping @Sendable (RPCStream) async -> Void, - descriptor: EventLoopFuture - ) async { - // It's okay to ignore these errors: - // - If we get an error because the http2Stream failed to close, then there's nothing we can do - // - If we get an error because the inner closure threw, then the only possible scenario in which - // that could happen is if methodDescriptor.get() throws - in which case, it means we never got - // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. - try? await stream.executeThenClose { inbound, outbound in - guard let descriptor = try? await descriptor.get() else { - return - } - - let rpcStream = RPCStream( - descriptor: descriptor, - inbound: RPCAsyncSequence(wrapping: inbound), - outbound: RPCWriter.Closable( - wrapping: ServerConnection.Stream.Outbound( - responseWriter: outbound, - http2Stream: stream - ) - ) - ) - - await streamHandler(rpcStream) - } + try await self.underlyingTransport.listen(streamHandler) } public func beginGracefulShutdown() { - self.serverQuiescingHelper.initiateShutdown(promise: nil) + self.underlyingTransport.beginGracefulShutdown() } } } From 573d430cc985312fd65d2a54b21ab083f8f7fb50 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 18 Sep 2024 12:55:21 +0100 Subject: [PATCH 465/580] Bump version number to 1.23.1 (#2062) Motivation: We plan on tagging a release soon. Modifications: - Bump the version to 1.23.1 Result: The version in the default user-agent string will match the released version. --- Sources/GRPC/Version.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift index 1808c5bf9..3fa958c6f 100644 --- a/Sources/GRPC/Version.swift +++ b/Sources/GRPC/Version.swift @@ -22,7 +22,7 @@ internal enum Version { internal static let minor = 23 /// The patch version. - internal static let patch = 0 + internal static let patch = 1 /// The version string. internal static let versionString = "\(major).\(minor).\(patch)" From 971b53e4de9f40547574c9c3c0f0ec5d7dc6b881 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 18 Sep 2024 13:03:23 +0100 Subject: [PATCH 466/580] Rename client interceptor context (#2061) Motivation: The `ServerInterceptorContext` was renamed to `ServerContext`. The same should be done for the client. Modifications: - Rename `ClientInterceptorContext` to `ClientContext` Result: More consistent naming --- .../GRPCCore/Call/Client/ClientContext.swift | 26 +++++++++++++++++++ .../Call/Client/ClientInterceptor.swift | 25 +++++------------- .../Client/Internal/ClientRPCExecutor.swift | 6 ++--- .../Internal/ClientStreamExecutor.swift | 2 +- .../ClientTracingInterceptor.swift | 4 +-- .../Call/Client/ClientInterceptors.swift | 8 +++--- 6 files changed, 43 insertions(+), 28 deletions(-) create mode 100644 Sources/GRPCCore/Call/Client/ClientContext.swift diff --git a/Sources/GRPCCore/Call/Client/ClientContext.swift b/Sources/GRPCCore/Call/Client/ClientContext.swift new file mode 100644 index 000000000..51eaa1a21 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/ClientContext.swift @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/// A context passed to the client containing additional information about the RPC. +public struct ClientContext: Sendable { + /// A description of the method being called. + public var descriptor: MethodDescriptor + + /// Create a new client interceptor context. + public init(descriptor: MethodDescriptor) { + self.descriptor = descriptor + } +} diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 2876fbe76..93ddf9cf5 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -23,7 +23,7 @@ /// /// Interceptors are registered with a client and apply to all RPCs. If you need to modify the /// behavior of an interceptor on a per-RPC basis then you can use the -/// ``ClientInterceptorContext/descriptor`` to determine which RPC is being called and +/// ``ClientContext/descriptor`` to determine which RPC is being called and /// conditionalise behavior accordingly. /// /// - TODO: Update example and documentation to show how to register an interceptor. @@ -41,10 +41,10 @@ /// /// func intercept( /// request: ClientRequest.Stream, -/// context: ClientInterceptorContext, +/// context: ClientContext, /// next: @Sendable ( /// _ request: ClientRequest.Stream, -/// _ context: ClientInterceptorContext +/// _ context: ClientContext /// ) async throws -> ClientResponse.Stream /// ) async throws -> ClientResponse.Stream { /// // Fetch the metadata value and attach it. @@ -66,10 +66,10 @@ /// struct LoggingClientInterceptor: ClientInterceptor { /// func intercept( /// request: ClientRequest.Stream, -/// context: ClientInterceptorContext, +/// context: ClientContext, /// next: @Sendable ( /// _ request: ClientRequest.Stream, -/// _ context: ClientInterceptorContext +/// _ context: ClientContext /// ) async throws -> ClientResponse.Stream /// ) async throws -> ClientResponse.Stream { /// print("Invoking method '\(context.descriptor)'") @@ -101,21 +101,10 @@ public protocol ClientInterceptor: Sendable { /// - Returns: A response object. func intercept( request: ClientRequest.Stream, - context: ClientInterceptorContext, + context: ClientContext, next: ( _ request: ClientRequest.Stream, - _ context: ClientInterceptorContext + _ context: ClientContext ) async throws -> ClientResponse.Stream ) async throws -> ClientResponse.Stream } - -/// A context passed to client interceptors containing additional information about the RPC. -public struct ClientInterceptorContext: Sendable { - /// A description of the method being called. - public var descriptor: MethodDescriptor - - /// Create a new client interceptor context. - public init(descriptor: MethodDescriptor) { - self.descriptor = descriptor - } -} diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 5811d20b1..4a7c958c4 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -124,7 +124,7 @@ extension ClientRPCExecutor { interceptors: [any ClientInterceptor], stream: RPCStream ) async -> ClientResponse.Stream { - let context = ClientInterceptorContext(descriptor: method) + let context = ClientContext(descriptor: method) if interceptors.isEmpty { return await ClientStreamExecutor.execute( @@ -160,12 +160,12 @@ extension ClientRPCExecutor { static func _intercept( in group: inout TaskGroup, request: ClientRequest.Stream, - context: ClientInterceptorContext, + context: ClientContext, iterator: Array.Iterator, finally: ( _ group: inout TaskGroup, _ request: ClientRequest.Stream, - _ context: ClientInterceptorContext + _ context: ClientContext ) async -> ClientResponse.Stream ) async -> ClientResponse.Stream { var iterator = iterator diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index c0f4b166a..433232d7c 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -29,7 +29,7 @@ internal enum ClientStreamExecutor { static func execute( in group: inout TaskGroup, request: ClientRequest.Stream, - context: ClientInterceptorContext, + context: ClientContext, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, diff --git a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift index 065d9936e..9da8a1f26 100644 --- a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift +++ b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift @@ -45,10 +45,10 @@ public struct ClientTracingInterceptor: ClientInterceptor { /// that has been configured when bootstrapping `swift-distributed-tracing` in your application. public func intercept( request: ClientRequest.Stream, - context: ClientInterceptorContext, + context: ClientContext, next: ( ClientRequest.Stream, - ClientInterceptorContext + ClientContext ) async throws -> ClientResponse.Stream ) async throws -> ClientResponse.Stream where Input: Sendable, Output: Sendable { var request = request diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index d8d355969..a45d64fd5 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -51,10 +51,10 @@ struct RejectAllClientInterceptor: ClientInterceptor { func intercept( request: ClientRequest.Stream, - context: ClientInterceptorContext, + context: ClientContext, next: ( ClientRequest.Stream, - ClientInterceptorContext + ClientContext ) async throws -> ClientResponse.Stream ) async throws -> ClientResponse.Stream { if self.throw { @@ -76,10 +76,10 @@ struct RequestCountingClientInterceptor: ClientInterceptor { func intercept( request: ClientRequest.Stream, - context: ClientInterceptorContext, + context: ClientContext, next: ( ClientRequest.Stream, - ClientInterceptorContext + ClientContext ) async throws -> ClientResponse.Stream ) async throws -> ClientResponse.Stream { self.counter.increment() From 07123ed731671e800ab8d641006613612e954746 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 18 Sep 2024 15:04:34 +0100 Subject: [PATCH 467/580] Adopt swift-testing for MethodConfig tests (#2055) Motivation: swift-testing has a number of advantages over XCTest (parameterisation, organisation, failure messages etc.), we should start using it instead of XCTest. Modifications: - Convert the MethodConfig coding tests - Fixed a couple of bugs found by additional testing Results: Fewer bugs, better tests --- Package@swift-6.swift | 3 + .../GRPCCore/Configuration/MethodConfig.swift | 1 + ...g.hedging_policy.invalid.max_attempts.json | 5 + .../Inputs/method_config.hedging_policy.json | 5 + .../Configuration/Inputs/method_config.json | 12 + .../Inputs/method_config.name.empty.json | 1 + .../Inputs/method_config.name.full.json | 4 + .../method_config.name.service_only.json | 3 + ...try_policy.invalid.backoff_multiplier.json | 7 + ....retry_policy.invalid.initial_backoff.json | 7 + ...fig.retry_policy.invalid.max_attempts.json | 7 + ...nfig.retry_policy.invalid.max_backoff.json | 7 + ...policy.invalid.retryable_status_codes.json | 7 + .../Inputs/method_config.retry_policy.json | 7 + .../Inputs/method_config.with_hedging.json | 20 + .../Inputs/method_config.with_retries.json | 22 + .../MethodConfigCodingTests.swift | 682 +++++++++--------- .../Configuration/MethodConfigTests.swift | 20 +- 18 files changed, 461 insertions(+), 359 deletions(-) create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.invalid.max_attempts.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.empty.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.full.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.service_only.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.backoff_multiplier.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.initial_backoff.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_attempts.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_backoff.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.retryable_status_codes.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_hedging.json create mode 100644 Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_retries.json diff --git a/Package@swift-6.swift b/Package@swift-6.swift index e7b1c268a..89b509d2d 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -415,6 +415,9 @@ extension Target { .protobuf, .testing, ], + resources: [ + .copy("Configuration/Inputs") + ], swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] ) } diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index b02d25f0e..d99c892df 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -453,6 +453,7 @@ extension MethodConfig: Codable { public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.names, forKey: .name) + try container.encodeIfPresent(self.waitForReady, forKey: .waitForReady) try container.encodeIfPresent( self.timeout.map { GoogleProtobufDuration(duration: $0) }, forKey: .timeout diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.invalid.max_attempts.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.invalid.max_attempts.json new file mode 100644 index 000000000..9436d324b --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.invalid.max_attempts.json @@ -0,0 +1,5 @@ +{ + "maxAttempts": 1, + "hedgingDelay": "1s", + "nonFatalStatusCodes": ["ABORTED"] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.json new file mode 100644 index 000000000..8dd9ed8e9 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.hedging_policy.json @@ -0,0 +1,5 @@ +{ + "maxAttempts": 3, + "hedgingDelay": "1s", + "nonFatalStatusCodes": ["ABORTED"] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.json new file mode 100644 index 000000000..71c9c2cab --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.json @@ -0,0 +1,12 @@ +{ + "name": [ + { + "service": "echo.Echo", + "method": "Get" + } + ], + "waitForReady": true, + "timeout": "1s", + "maxRequestMessageBytes": 1024, + "maxResponseMessageBytes": 2048 +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.empty.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.empty.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.empty.json @@ -0,0 +1 @@ +{} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.full.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.full.json new file mode 100644 index 000000000..ed21cc360 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.full.json @@ -0,0 +1,4 @@ +{ + "service": "foo.bar", + "method": "baz" +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.service_only.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.service_only.json new file mode 100644 index 000000000..beb50d5e3 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.name.service_only.json @@ -0,0 +1,3 @@ +{ + "service": "foo.bar" +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.backoff_multiplier.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.backoff_multiplier.json new file mode 100644 index 000000000..a43451a94 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.backoff_multiplier.json @@ -0,0 +1,7 @@ +{ + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": -1.6, + "retryableStatusCodes": ["ABORTED", "UNAVAILABLE"] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.initial_backoff.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.initial_backoff.json new file mode 100644 index 000000000..bb9691bbb --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.initial_backoff.json @@ -0,0 +1,7 @@ +{ + "maxAttempts": 3, + "initialBackoff": "0s", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED", "UNAVAILABLE"] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_attempts.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_attempts.json new file mode 100644 index 000000000..454ba94e9 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_attempts.json @@ -0,0 +1,7 @@ +{ + "maxAttempts": 1, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED", "UNAVAILABLE"] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_backoff.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_backoff.json new file mode 100644 index 000000000..6059280be --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.max_backoff.json @@ -0,0 +1,7 @@ +{ + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "0s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED", "UNAVAILABLE"] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.retryable_status_codes.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.retryable_status_codes.json new file mode 100644 index 000000000..d437878f0 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.invalid.retryable_status_codes.json @@ -0,0 +1,7 @@ +{ + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": [] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.json new file mode 100644 index 000000000..ef8744c2e --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.retry_policy.json @@ -0,0 +1,7 @@ +{ + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": ["ABORTED", "UNAVAILABLE"] +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_hedging.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_hedging.json new file mode 100644 index 000000000..1d9ecc6b7 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_hedging.json @@ -0,0 +1,20 @@ +{ + "name": [ + { + "service": "echo.Echo", + "method": "Get" + } + ], + "waitForReady": true, + "timeout": "1s", + "maxRequestMessageBytes": 1024, + "maxResponseMessageBytes": 2048, + "hedgingPolicy": { + "maxAttempts": 3, + "hedgingDelay": "42s", + "nonFatalStatusCodes": [ + "ABORTED", + "UNIMPLEMENTED" + ] + } +} diff --git a/Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_retries.json b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_retries.json new file mode 100644 index 000000000..41556e185 --- /dev/null +++ b/Tests/GRPCCoreTests/Configuration/Inputs/method_config.with_retries.json @@ -0,0 +1,22 @@ +{ + "name": [ + { + "service": "echo.Echo", + "method": "Get" + } + ], + "waitForReady": true, + "timeout": "1s", + "maxRequestMessageBytes": 1024, + "maxResponseMessageBytes": 2048, + "retryPolicy": { + "maxAttempts": 3, + "initialBackoff": "1s", + "maxBackoff": "3s", + "backoffMultiplier": 1.6, + "retryableStatusCodes": [ + "ABORTED", + "UNIMPLEMENTED" + ] + } +} diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift index a0d309c25..c65fe1c54 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift @@ -16,415 +16,397 @@ import Foundation import SwiftProtobuf -import XCTest +import Testing @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -internal final class MethodConfigCodingTests: XCTestCase { - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private func testDecodeThrowsRuntimeError(json: String, as: D.Type) throws { - XCTAssertThrowsError( - ofType: RuntimeError.self, - try self.decoder.decode(D.self, from: Data(json.utf8)) - ) { error in - XCTAssertEqual(error.code, .invalidArgument) +@Suite("MethodConfig coding tests") +struct MethodConfigCodingTests { + @Suite("Encoding") + struct Encoding { + private func encodeToJSON(_ value: some Encodable) throws -> String { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + let encoded = try encoder.encode(value) + let json = String(decoding: encoded, as: UTF8.self) + return json } - } - - func testDecodeMethodConfigName() throws { - let inputs: [(String, MethodConfig.Name)] = [ - (#"{"service": "foo.bar", "method": "baz"}"#, .init(service: "foo.bar", method: "baz")), - (#"{"service": "foo.bar"}"#, .init(service: "foo.bar", method: "")), - (#"{}"#, .init(service: "", method: "")), - ] - for (json, expected) in inputs { - let decoded = try self.decoder.decode(MethodConfig.Name.self, from: Data(json.utf8)) - XCTAssertEqual(decoded, expected) + @Test( + "Name", + arguments: [ + ( + MethodConfig.Name(service: "foo.bar", method: "baz"), + #"{"method":"baz","service":"foo.bar"}"# + ), + (MethodConfig.Name(service: "foo.bar", method: ""), #"{"method":"","service":"foo.bar"}"#), + (MethodConfig.Name(service: "", method: ""), #"{"method":"","service":""}"#), + ] as [(MethodConfig.Name, String)] + ) + func methodConfigName(name: MethodConfig.Name, expected: String) throws { + let json = try self.encodeToJSON(name) + #expect(json == expected) } - } - func testEncodeDecodeMethodConfigName() throws { - let inputs: [MethodConfig.Name] = [ - MethodConfig.Name(service: "foo.bar", method: "baz"), - MethodConfig.Name(service: "foo.bar", method: ""), - MethodConfig.Name(service: "", method: ""), - ] - - // We can't do encode-only tests as the output is non-deterministic (the ordering of - // service/method in the JSON object) - for name in inputs { - let encoded = try self.encoder.encode(name) - let decoded = try self.decoder.decode(MethodConfig.Name.self, from: encoded) - XCTAssertEqual(decoded, name) + @Test( + "GoogleProtobufDuration", + arguments: [ + (.seconds(1), #""1.0s""#), + (.zero, #""0.0s""#), + (.milliseconds(100_123), #""100.123s""#), + ] as [(Duration, String)] + ) + func protobufDuration(duration: Duration, expected: String) throws { + let json = try self.encodeToJSON(GoogleProtobufDuration(duration: duration)) + #expect(json == expected) } - } - func testDecodeProtobufDuration() throws { - let inputs: [(String, Duration)] = [ - ("1.0s", .seconds(1)), - ("1s", .seconds(1)), - ("1.000000s", .seconds(1)), - ("0s", .zero), - ("100.123s", .milliseconds(100_123)), - ] + @Test( + "GoogleRPCCode", + arguments: [ + (.ok, #""OK""#), + (.cancelled, #""CANCELLED""#), + (.unknown, #""UNKNOWN""#), + (.invalidArgument, #""INVALID_ARGUMENT""#), + (.deadlineExceeded, #""DEADLINE_EXCEEDED""#), + (.notFound, #""NOT_FOUND""#), + (.alreadyExists, #""ALREADY_EXISTS""#), + (.permissionDenied, #""PERMISSION_DENIED""#), + (.resourceExhausted, #""RESOURCE_EXHAUSTED""#), + (.failedPrecondition, #""FAILED_PRECONDITION""#), + (.aborted, #""ABORTED""#), + (.outOfRange, #""OUT_OF_RANGE""#), + (.unimplemented, #""UNIMPLEMENTED""#), + (.internalError, #""INTERNAL""#), + (.unavailable, #""UNAVAILABLE""#), + (.dataLoss, #""DATA_LOSS""#), + (.unauthenticated, #""UNAUTHENTICATED""#), + ] as [(Status.Code, String)] + ) + func rpcCode(code: Status.Code, expected: String) throws { + let json = try self.encodeToJSON(GoogleRPCCode(code: code)) + #expect(json == expected) + } - for (input, expected) in inputs { - let json = "\"\(input)\"" - let protoDuration = try self.decoder.decode( - GoogleProtobufDuration.self, - from: Data(json.utf8) + @Test("RetryPolicy") + func retryPolicy() throws { + let policy = RetryPolicy( + maximumAttempts: 3, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(3), + backoffMultiplier: 1.6, + retryableStatusCodes: [.aborted] ) - let components = protoDuration.duration.components - - // Conversion is lossy as we go from floating point seconds to integer seconds and - // attoseconds. Allow for millisecond precision. - let divisor: Int64 = 1_000_000_000_000_000 - XCTAssertEqual(components.seconds, expected.components.seconds) - XCTAssertEqual(components.attoseconds / divisor, expected.components.attoseconds / divisor) + let json = try self.encodeToJSON(policy) + let expected = + #"{"backoffMultiplier":1.6,"initialBackoff":"1.0s","maxAttempts":3,"maxBackoff":"3.0s","retryableStatusCodes":["ABORTED"]}"# + #expect(json == expected) } - } - func testEncodeProtobufDuration() throws { - let inputs: [(Duration, String)] = [ - (.seconds(1), "\"1.0s\""), - (.zero, "\"0.0s\""), - (.milliseconds(100_123), "\"100.123s\""), - ] + @Test("HedgingPolicy") + func hedgingPolicy() throws { + let policy = HedgingPolicy( + maximumAttempts: 3, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [.aborted] + ) - for (input, expected) in inputs { - let duration = GoogleProtobufDuration(duration: input) - let encoded = try self.encoder.encode(duration) - let json = String(decoding: encoded, as: UTF8.self) - XCTAssertEqual(json, expected) + let json = try self.encodeToJSON(policy) + let expected = #"{"hedgingDelay":"1.0s","maxAttempts":3,"nonFatalStatusCodes":["ABORTED"]}"# + #expect(json == expected) } } - func testDecodeInvalidProtobufDuration() throws { - for timestamp in ["1", "1ss", "1S", "1.0S"] { - let json = "\"\(timestamp)\"" - try self.testDecodeThrowsRuntimeError(json: json, as: GoogleProtobufDuration.self) - } - } + @Suite("Decoding") + struct Decoding { + private func decodeFromFile( + _ name: String, + as: Decoded.Type + ) throws -> Decoded { + let input = Bundle.module.url( + forResource: name, + withExtension: "json", + subdirectory: "Inputs" + ) - func testDecodeRPCCodeFromCaseName() throws { - let inputs: [(String, Status.Code)] = [ - ("OK", .ok), - ("CANCELLED", .cancelled), - ("UNKNOWN", .unknown), - ("INVALID_ARGUMENT", .invalidArgument), - ("DEADLINE_EXCEEDED", .deadlineExceeded), - ("NOT_FOUND", .notFound), - ("ALREADY_EXISTS", .alreadyExists), - ("PERMISSION_DENIED", .permissionDenied), - ("RESOURCE_EXHAUSTED", .resourceExhausted), - ("FAILED_PRECONDITION", .failedPrecondition), - ("ABORTED", .aborted), - ("OUT_OF_RANGE", .outOfRange), - ("UNIMPLEMENTED", .unimplemented), - ("INTERNAL", .internalError), - ("UNAVAILABLE", .unavailable), - ("DATA_LOSS", .dataLoss), - ("UNAUTHENTICATED", .unauthenticated), - ] + let url = try #require(input) + let data = try Data(contentsOf: url) - for (name, expected) in inputs { - let json = "\"\(name)\"" - let code = try self.decoder.decode(GoogleRPCCode.self, from: Data(json.utf8)) - XCTAssertEqual(code.code, expected) + let decoder = JSONDecoder() + return try decoder.decode(Decoded.self, from: data) } - } - func testDecodeRPCCodeFromRawValue() throws { - let inputs: [(Int, Status.Code)] = [ - (0, .ok), - (1, .cancelled), - (2, .unknown), - (3, .invalidArgument), - (4, .deadlineExceeded), - (5, .notFound), - (6, .alreadyExists), - (7, .permissionDenied), - (8, .resourceExhausted), - (9, .failedPrecondition), - (10, .aborted), - (11, .outOfRange), - (12, .unimplemented), - (13, .internalError), - (14, .unavailable), - (15, .dataLoss), - (16, .unauthenticated), - ] - - for (rawValue, expected) in inputs { - let json = "\(rawValue)" - let code = try self.decoder.decode(GoogleRPCCode.self, from: Data(json.utf8)) - XCTAssertEqual(code.code, expected) + private func decodeFromJSONString( + _ json: String, + as: Decoded.Type + ) throws -> Decoded { + let data = Data(json.utf8) + let decoder = JSONDecoder() + return try decoder.decode(Decoded.self, from: data) } - } - func testEncodeDecodeRPCCode() throws { - let codes: [Status.Code] = [ - .ok, - .cancelled, - .unknown, - .invalidArgument, - .deadlineExceeded, - .notFound, - .alreadyExists, - .permissionDenied, - .resourceExhausted, - .failedPrecondition, - .aborted, - .outOfRange, - .unimplemented, - .internalError, - .unavailable, - .dataLoss, - .unauthenticated, + private static let codeNames: [String] = [ + "OK", + "CANCELLED", + "UNKNOWN", + "INVALID_ARGUMENT", + "DEADLINE_EXCEEDED", + "NOT_FOUND", + "ALREADY_EXISTS", + "PERMISSION_DENIED", + "RESOURCE_EXHAUSTED", + "FAILED_PRECONDITION", + "ABORTED", + "OUT_OF_RANGE", + "UNIMPLEMENTED", + "INTERNAL", + "UNAVAILABLE", + "DATA_LOSS", + "UNAUTHENTICATED", ] - for code in codes { - let encoded = try self.encoder.encode(GoogleRPCCode(code: code)) - let decoded = try self.decoder.decode(GoogleRPCCode.self, from: encoded) - XCTAssertEqual(decoded.code, code) + @Test( + "Name", + arguments: [ + ("method_config.name.full", MethodConfig.Name(service: "foo.bar", method: "baz")), + ("method_config.name.service_only", MethodConfig.Name(service: "foo.bar", method: "")), + ("method_config.name.empty", MethodConfig.Name(service: "", method: "")), + ] as [(String, MethodConfig.Name)] + ) + func name(_ fileName: String, expected: MethodConfig.Name) throws { + let decoded = try self.decodeFromFile(fileName, as: MethodConfig.Name.self) + #expect(decoded == expected) } - } - func testDecodeRetryPolicy() throws { - let json = """ - { - "maxAttempts": 3, - "initialBackoff": "1s", - "maxBackoff": "3s", - "backoffMultiplier": 1.6, - "retryableStatusCodes": ["ABORTED", "UNAVAILABLE"] - } - """ - - let expected = RetryPolicy( - maximumAttempts: 3, - initialBackoff: .seconds(1), - maximumBackoff: .seconds(3), - backoffMultiplier: 1.6, - retryableStatusCodes: [.aborted, .unavailable] + @Test( + "GoogleProtobufDuration", + arguments: [ + ("1.0s", .seconds(1)), + ("1s", .seconds(1)), + ("1.000000s", .seconds(1)), + ("0s", .zero), + ("100.123s", .milliseconds(100_123)), + ] as [(String, Duration)] ) + func googleProtobufDuration(duration: String, expectedDuration: Duration) throws { + let json = "\"\(duration)\"" + let decoded = try self.decodeFromJSONString(json, as: GoogleProtobufDuration.self) - let decoded = try self.decoder.decode(RetryPolicy.self, from: Data(json.utf8)) - XCTAssertEqual(decoded, expected) - } + // Conversion is lossy as we go from floating point seconds to integer seconds and + // attoseconds. Allow for millisecond precision. + let divisor: Int64 = 1_000_000_000_000_000 - func testEncodeDecodeRetryPolicy() throws { - let policy = RetryPolicy( - maximumAttempts: 3, - initialBackoff: .seconds(1), - maximumBackoff: .seconds(3), - backoffMultiplier: 1.6, - retryableStatusCodes: [.aborted] - ) + let duration = decoded.duration.components + let expected = expectedDuration.components - let encoded = try self.encoder.encode(policy) - let decoded = try self.decoder.decode(RetryPolicy.self, from: encoded) - XCTAssertEqual(decoded, policy) - } + #expect(duration.seconds == expected.seconds) + #expect(duration.attoseconds / divisor == expected.attoseconds / divisor) + } - func testDecodeRetryPolicyWithInvalidRetryMaxAttempts() throws { - let cases = ["-1", "0", "1"] - for maxAttempts in cases { - let json = """ - { - "maxAttempts": \(maxAttempts), - "initialBackoff": "1s", - "maxBackoff": "3s", - "backoffMultiplier": 1.6, - "retryableStatusCodes": ["ABORTED"] - } - """ - - try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + @Test("Invalid GoogleProtobufDuration", arguments: ["1", "1ss", "1S", "1.0S"]) + func googleProtobufDuration(invalidDuration: String) throws { + let json = "\"\(invalidDuration)\"" + #expect { + try self.decodeFromJSONString(json, as: GoogleProtobufDuration.self) + } throws: { error in + guard let error = error as? RuntimeError else { return false } + return error.code == .invalidArgument + } } - } - func testDecodeRetryPolicyWithInvalidInitialBackoff() throws { - let cases = ["0s", "-1s"] - for backoff in cases { - let json = """ - { - "maxAttempts": 3, - "initialBackoff": "\(backoff)", - "maxBackoff": "3s", - "backoffMultiplier": 1.6, - "retryableStatusCodes": ["ABORTED"] - } - """ - try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + @Test("GoogleRPCCode from case name", arguments: zip(Self.codeNames, Status.Code.all)) + func rpcCode(name: String, expected: Status.Code) throws { + let json = "\"\(name)\"" + let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self) + #expect(decoded.code == expected) } - } - func testDecodeRetryPolicyWithInvalidMaxBackoff() throws { - let cases = ["0s", "-1s"] - for backoff in cases { - let json = """ - { - "maxAttempts": 3, - "initialBackoff": "1s", - "maxBackoff": "\(backoff)", - "backoffMultiplier": 1.6, - "retryableStatusCodes": ["ABORTED"] - } - """ - try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + @Test("GoogleRPCCode from rawValue", arguments: zip(0 ... 16, Status.Code.all)) + func rpcCode(rawValue: Int, expected: Status.Code) throws { + let json = "\(rawValue)" + let decoded = try self.decodeFromJSONString(json, as: GoogleRPCCode.self) + #expect(decoded.code == expected) } - } - func testDecodeRetryPolicyWithInvalidBackoffMultiplier() throws { - let cases = ["0", "-1.5"] - for multiplier in cases { - let json = """ - { - "maxAttempts": 3, - "initialBackoff": "1s", - "maxBackoff": "3s", - "backoffMultiplier": \(multiplier), - "retryableStatusCodes": ["ABORTED"] - } - """ - try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) + @Test("RetryPolicy") + func retryPolicy() throws { + let decoded = try self.decodeFromFile("method_config.retry_policy", as: RetryPolicy.self) + let expected = RetryPolicy( + maximumAttempts: 3, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(3), + backoffMultiplier: 1.6, + retryableStatusCodes: [.aborted, .unavailable] + ) + #expect(decoded == expected) } - } - func testDecodeRetryPolicyWithEmptyRetryableStatusCodes() throws { - let json = """ - { - "maxAttempts": 3, - "initialBackoff": "1s", - "maxBackoff": "3s", - "backoffMultiplier": 1, - "retryableStatusCodes": [] + @Test( + "RetryPolicy with invalid values", + arguments: [ + "method_config.retry_policy.invalid.backoff_multiplier", + "method_config.retry_policy.invalid.initial_backoff", + "method_config.retry_policy.invalid.max_backoff", + "method_config.retry_policy.invalid.max_attempts", + "method_config.retry_policy.invalid.retryable_status_codes", + ] + ) + func invalidRetryPolicy(fileName: String) throws { + #expect(throws: RuntimeError.self) { + try self.decodeFromFile(fileName, as: RetryPolicy.self) } - """ - try self.testDecodeThrowsRuntimeError(json: json, as: RetryPolicy.self) - } + } - func testDecodeHedgingPolicy() throws { - let json = """ - { - "maxAttempts": 3, - "hedgingDelay": "1s", - "nonFatalStatusCodes": ["ABORTED"] - } - """ + @Test("HedgingPolicy") + func hedgingPolicy() throws { + let decoded = try self.decodeFromFile("method_config.hedging_policy", as: HedgingPolicy.self) + let expected = HedgingPolicy( + maximumAttempts: 3, + hedgingDelay: .seconds(1), + nonFatalStatusCodes: [.aborted] + ) + #expect(decoded == expected) + } - let expected = HedgingPolicy( - maximumAttempts: 3, - hedgingDelay: .seconds(1), - nonFatalStatusCodes: [.aborted] + @Test( + "HedgingPolicy with invalid values", + arguments: [ + "method_config.hedging_policy.invalid.max_attempts" + ] ) + func invalidHedgingPolicy(fileName: String) throws { + #expect(throws: RuntimeError.self) { + try self.decodeFromFile(fileName, as: HedgingPolicy.self) + } + } - let decoded = try self.decoder.decode(HedgingPolicy.self, from: Data(json.utf8)) - XCTAssertEqual(decoded, expected) - } - - func testEncodeDecodeHedgingPolicy() throws { - let policy = HedgingPolicy( - maximumAttempts: 3, - hedgingDelay: .seconds(1), - nonFatalStatusCodes: [.aborted] - ) + @Test("MethodConfig") + func methodConfig() throws { + let expected = MethodConfig( + names: [ + MethodConfig.Name( + service: "echo.Echo", + method: "Get" + ) + ], + waitForReady: true, + timeout: .seconds(1), + maxRequestMessageBytes: 1024, + maxResponseMessageBytes: 2048 + ) - let encoded = try self.encoder.encode(policy) - let decoded = try self.decoder.decode(HedgingPolicy.self, from: encoded) - XCTAssertEqual(decoded, policy) - } + let decoded = try self.decodeFromFile("method_config", as: MethodConfig.self) + #expect(decoded == expected) + } - func testMethodConfigDecodeFromJSON() throws { - let config = Grpc_ServiceConfig_MethodConfig.with { - $0.name = [ - .with { - $0.service = "echo.Echo" - $0.method = "Get" - } - ] + @Test("MethodConfig with hedging") + func methodConfigWithHedging() throws { + let expected = MethodConfig( + names: [ + MethodConfig.Name( + service: "echo.Echo", + method: "Get" + ) + ], + waitForReady: true, + timeout: .seconds(1), + maxRequestMessageBytes: 1024, + maxResponseMessageBytes: 2048, + executionPolicy: .hedge( + HedgingPolicy( + maximumAttempts: 3, + hedgingDelay: .seconds(42), + nonFatalStatusCodes: [.aborted, .unimplemented] + ) + ) + ) - $0.waitForReady = true + let decoded = try self.decodeFromFile("method_config.with_hedging", as: MethodConfig.self) + #expect(decoded == expected) + } - $0.timeout = .with { - $0.seconds = 1 - $0.nanos = 0 - } + @Test("MethodConfig with retries") + func methodConfigWithRetries() throws { + let expected = MethodConfig( + names: [ + MethodConfig.Name( + service: "echo.Echo", + method: "Get" + ) + ], + waitForReady: true, + timeout: .seconds(1), + maxRequestMessageBytes: 1024, + maxResponseMessageBytes: 2048, + executionPolicy: .retry( + RetryPolicy( + maximumAttempts: 3, + initialBackoff: .seconds(1), + maximumBackoff: .seconds(3), + backoffMultiplier: 1.6, + retryableStatusCodes: [.aborted, .unimplemented] + ) + ) + ) - $0.maxRequestMessageBytes = 1024 - $0.maxResponseMessageBytes = 2048 + let decoded = try self.decodeFromFile("method_config.with_retries", as: MethodConfig.self) + #expect(decoded == expected) } + } - // Test the 'regular' config. - do { - let jsonConfig = try config.jsonUTF8Data() - let decoded = try self.decoder.decode(MethodConfig.self, from: jsonConfig) - XCTAssertEqual(decoded.names, [MethodConfig.Name(service: "echo.Echo", method: "Get")]) - XCTAssertEqual(decoded.waitForReady, true) - XCTAssertEqual(decoded.timeout, Duration(secondsComponent: 1, attosecondsComponent: 0)) - XCTAssertEqual(decoded.maxRequestMessageBytes, 1024) - XCTAssertEqual(decoded.maxResponseMessageBytes, 2048) - XCTAssertNil(decoded.executionPolicy) - } + @Suite("Round-trip tests") + struct RoundTrip { + private func decodeFromFile( + _ name: String, + as: Decoded.Type + ) throws -> Decoded { + let input = Bundle.module.url( + forResource: name, + withExtension: "json", + subdirectory: "Inputs" + ) - // Test the hedging policy. - do { - var config = config - config.hedgingPolicy = .with { - $0.maxAttempts = 3 - $0.hedgingDelay = .with { $0.seconds = 42 } - $0.nonFatalStatusCodes = [ - .aborted, - .unimplemented, - ] - } + let url = try #require(input) + let data = try Data(contentsOf: url) - let jsonConfig = try config.jsonUTF8Data() - let decoded = try self.decoder.decode(MethodConfig.self, from: jsonConfig) + let decoder = JSONDecoder() + return try decoder.decode(Decoded.self, from: data) + } - switch decoded.executionPolicy?.wrapped { - case let .some(.hedge(policy)): - XCTAssertEqual(policy.maximumAttempts, 3) - XCTAssertEqual(policy.hedgingDelay, .seconds(42)) - XCTAssertEqual(policy.nonFatalStatusCodes, [.aborted, .unimplemented]) - default: - XCTFail("Expected hedging policy") - } + private func decodeFromJSONString( + _ json: String, + as: Decoded.Type + ) throws -> Decoded { + let data = Data(json.utf8) + let decoder = JSONDecoder() + return try decoder.decode(Decoded.self, from: data) } - // Test the retry policy. - do { - var config = config - config.retryPolicy = .with { - $0.maxAttempts = 3 - $0.initialBackoff = .with { $0.seconds = 1 } - $0.maxBackoff = .with { $0.seconds = 3 } - $0.backoffMultiplier = 1.6 - $0.retryableStatusCodes = [ - .aborted, - .unimplemented, - ] - } + private func encodeToJSON(_ value: some Encodable) throws -> String { + let encoder = JSONEncoder() + let encoded = try encoder.encode(value) + let json = String(decoding: encoded, as: UTF8.self) + return json + } - let jsonConfig = try config.jsonUTF8Data() - let decoded = try self.decoder.decode(MethodConfig.self, from: jsonConfig) - - switch decoded.executionPolicy?.wrapped { - case let .some(.retry(policy)): - XCTAssertEqual(policy.maximumAttempts, 3) - XCTAssertEqual(policy.initialBackoff, .seconds(1)) - XCTAssertEqual(policy.maximumBackoff, .seconds(3)) - XCTAssertEqual(policy.backoffMultiplier, 1.6) - XCTAssertEqual(policy.retryableStatusCodes, [.aborted, .unimplemented]) - default: - XCTFail("Expected hedging policy") - } + private func roundTrip(type: T.Type = T.self, fileName: String) throws { + let decoded = try self.decodeFromFile(fileName, as: T.self) + let encoded = try self.encodeToJSON(decoded) + let decodedAgain = try self.decodeFromJSONString(encoded, as: T.self) + #expect(decoded == decodedAgain) + } + + @Test( + "MethodConfig", + arguments: [ + "method_config", + "method_config.with_retries", + "method_config.with_hedging", + ] + ) + func roundTripCodingAndDecoding(fileName: String) throws { + try self.roundTrip(type: MethodConfig.self, fileName: fileName) } } } diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift index 0b3406efb..eca712cf5 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import GRPCCore -import XCTest +import Testing -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class MethodConfigTests: XCTestCase { - func testRetryPolicyClampsMaxAttempts() { +struct MethodConfigTests { + @Test("RetryPolicy clamps max attempts") + func retryPolicyClampsMaxAttempts() { var policy = RetryPolicy( maximumAttempts: 10, initialBackoff: .seconds(1), @@ -28,13 +29,14 @@ final class MethodConfigTests: XCTestCase { ) // Should be clamped on init - XCTAssertEqual(policy.maximumAttempts, 5) + #expect(policy.maximumAttempts == 5) // and when modifying policy.maximumAttempts = 10 - XCTAssertEqual(policy.maximumAttempts, 5) + #expect(policy.maximumAttempts == 5) } - func testHedgingPolicyClampsMaxAttempts() { + @Test("HedgingPolicy clamps max attempts") + func hedgingPolicyClampsMaxAttempts() { var policy = HedgingPolicy( maximumAttempts: 10, hedgingDelay: .seconds(1), @@ -42,9 +44,9 @@ final class MethodConfigTests: XCTestCase { ) // Should be clamped on init - XCTAssertEqual(policy.maximumAttempts, 5) + #expect(policy.maximumAttempts == 5) // and when modifying policy.maximumAttempts = 10 - XCTAssertEqual(policy.maximumAttempts, 5) + #expect(policy.maximumAttempts == 5) } } From 78da46d6182e81c2a6742db40acc1fcd3ed6ba85 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 18 Sep 2024 15:21:33 +0100 Subject: [PATCH 468/580] Raise minimum Swift version to 5.9 (#2064) Motivation: We support the last three released versions of Swift. Now that Swift 6.0 has been released that's 6.0, 5.10, and 5.9. We no longer need to support Swift 5.8. Modifications: - Update CI - Update docs - Remove swift-testing package dependency for Swift 6 as it's now included in the toolchain Result: Swift 5.8 is no longer supported --- .github/workflows/ci.yaml | 25 ++++--------------------- Package.swift | 2 +- Package@swift-6.swift | 12 ------------ Sources/GRPC/Docs.docc/index.md | 3 ++- 4 files changed, 7 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1b348667d..56a7e4fa6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: name: License Header and Formatting Checks runs-on: ubuntu-latest container: - image: swiftlang/swift:nightly-6.0-jammy + image: swift:6.0-jammy steps: - name: "Checkout repository" uses: actions/checkout@v4 @@ -28,7 +28,7 @@ jobs: - image: swiftlang/swift:nightly-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swiftlang/swift:nightly-6.0-jammy + - image: swift:6.0-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - image: swift:5.10.1-noble @@ -37,9 +37,6 @@ jobs: - image: swift:5.9-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.8-focal - # No TSAN because of: https://github.com/apple/swift/issues/59068 - # swift-test-flags: "--sanitize=thread" name: Build and Test on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -68,7 +65,7 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swiftlang/swift:nightly-6.0-jammy + - image: swift:6.0-jammy swift-version: '6.0' env: MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 @@ -101,17 +98,6 @@ jobs: MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.8-focal - swift-version: 5.8 - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -139,7 +125,7 @@ jobs: - image: swiftlang/swift:nightly-jammy swift-tools-version: '6.0' supports-v2: true - - image: swiftlang/swift:nightly-6.0-jammy + - image: swift:6.0-jammy swift-tools-version: '6.0' supports-v2: true - image: swift:5.10.1-noble @@ -148,9 +134,6 @@ jobs: - image: swift:5.9-jammy swift-tools-version: '5.9' supports-v2: false - - image: swift:5.8-focal - swift-tools-version: '5.8' - supports-v2: false name: Integration Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: diff --git a/Package.swift b/Package.swift index 1e2da3eba..db5881f30 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.8 +// swift-tools-version:5.9 /* * Copyright 2017, gRPC Authors All rights reserved. * diff --git a/Package@swift-6.swift b/Package@swift-6.swift index 89b509d2d..f8922e440 100644 --- a/Package@swift-6.swift +++ b/Package@swift-6.swift @@ -72,10 +72,6 @@ let packageDependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0" ), - .package( - url: "https://github.com/swiftlang/swift-testing.git", - branch: "release/6.0" - ), ].appending( .package( url: "https://github.com/apple/swift-nio-ssl.git", @@ -151,13 +147,6 @@ extension Target.Dependency { static var dequeModule: Self { .product(name: "DequeModule", package: "swift-collections") } static var atomics: Self { .product(name: "Atomics", package: "swift-atomics") } static var tracing: Self { .product(name: "Tracing", package: "swift-distributed-tracing") } - static var testing: Self { - .product( - name: "Testing", - package: "swift-testing", - condition: .when(platforms: [.linux]) // Already included in the toolchain on Darwin - ) - } static var grpcCore: Self { .target(name: "GRPCCore") } static var grpcInProcessTransport: Self { .target(name: "GRPCInProcessTransport") } @@ -413,7 +402,6 @@ extension Target { .grpcInProcessTransport, .dequeModule, .protobuf, - .testing, ], resources: [ .copy("Configuration/Inputs") diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md index 58eb3ba8c..2b6cc1087 100644 --- a/Sources/GRPC/Docs.docc/index.md +++ b/Sources/GRPC/Docs.docc/index.md @@ -29,7 +29,8 @@ gRPC Swift Version | Earliest Swift Version `1.11.0..< 1.16.0`.| 5.5 `1.16.0..< 1.20.0` | 5.6 `1.20.0..< 1.22.0` | 5.7 -`1.22.0...` | 5.8 +`1.22.0..< 1.24.0` | 5.8 +`1.24.0...` | 5.9 Versions of clients and services which are use Swift's Concurrency support are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. From 7789f1ef69c5b714a7fb22707e78548e35745cb9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 19 Sep 2024 16:24:18 +0100 Subject: [PATCH 469/580] Propagate context to server RPC handlers (#2059) Motivation: The interceptors API has a context which, at the moment, only includes the name of the RPC. This information is generally useful so should be propagated to the server handler too. Information on the context can also be extended later to include things like the identity of the remote peer, or info about the state of the RPC. Modifications: - Rename 'ServerInterceptorContext' to 'ServerContext' - Make the transport the source of the context and have that provide it to the 'listen' method. Propagate this through the server stack to the generated stubs. - Update code generator to include the context - Update generated code - Update services Results: RPC handlers have a context provided by a transport --- Examples/v2/echo/Generated/echo.grpc.swift | 98 +++++-- Examples/v2/echo/Subcommands/Serve.swift | 12 +- .../Generated/helloworld.grpc.swift | 27 +- .../v2/hello-world/Subcommands/Serve.swift | 3 +- .../Generated/route_guide.grpc.swift | 98 +++++-- .../v2/route-guide/Subcommands/Serve.swift | 12 +- .../IDLToStructuredSwiftTranslator.swift | 64 ++-- .../Translator/ServerCodeTranslator.swift | 38 ++- .../Server/Internal/ServerRPCExecutor.swift | 42 +-- Sources/GRPCCore/Call/Server/RPCRouter.swift | 16 +- .../GRPCCore/Call/Server/ServerContext.swift | 26 ++ .../Call/Server/ServerInterceptor.swift | 17 +- Sources/GRPCCore/GRPCServer.swift | 4 +- .../GRPCCore/Transport/ServerTransport.swift | 7 +- .../Server/CommonHTTP2ServerTransport.swift | 18 +- .../HTTP2ServerTransport+Posix.swift | 7 +- ...TP2ServerTransport+TransportServices.swift | 7 +- .../InProcessServerTransport.swift | 8 +- .../ServerTracingInterceptor.swift | 4 +- .../Generated/test.grpc.swift | 277 ++++++++++++++---- .../InteroperabilityTests/TestService.swift | 50 ++-- .../Health/Generated/health.grpc.swift | 54 +++- Sources/Services/Health/HealthService.swift | 10 +- .../performance-worker/BenchmarkService.swift | 15 +- .../grpc_testing_benchmark_service.grpc.swift | 115 ++++++-- .../grpc_testing_worker_service.grpc.swift | 88 ++++-- .../performance-worker/WorkerService.swift | 12 +- ...erverCodeTranslatorSnippetBasedTests.swift | 179 ++++++++--- .../ClientRPCExecutorTestHarness.swift | 2 +- .../ServerRPCExecutorTestHarness.swift | 8 +- .../Call/Server/RPCRouterTests.swift | 4 +- .../Call/Server/ServerInterceptors.swift | 8 +- .../Test Utilities/Services/BinaryEcho.swift | 8 +- .../Transport/AnyTransport.swift | 12 +- .../Transport/StreamCountingTransport.swift | 9 +- .../Transport/ThrowingTransport.swift | 12 +- .../ControlService.swift | 12 +- .../Generated/control.grpc.swift | 98 +++++-- .../HTTP2TransportNIOPosixTests.swift | 12 +- ...P2TransportNIOTransportServicesTests.swift | 10 +- .../InProcessClientTransportTests.swift | 2 +- .../InProcessServerTransportTests.swift | 5 +- .../ProtobufCodeGeneratorTests.swift | 55 +++- 43 files changed, 1133 insertions(+), 432 deletions(-) create mode 100644 Sources/GRPCCore/Call/Server/ServerContext.swift diff --git a/Examples/v2/echo/Generated/echo.grpc.swift b/Examples/v2/echo/Generated/echo.grpc.swift index 856ccc074..63a264205 100644 --- a/Examples/v2/echo/Generated/echo.grpc.swift +++ b/Examples/v2/echo/Generated/echo.grpc.swift @@ -86,16 +86,28 @@ extension GRPCCore.ServiceDescriptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Immediately returns an echo of a request. - func get(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func get( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Splits a request into words and returns each word in a stream of messages. - func expand(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func expand( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func collect( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Streams back messages as they are received in an input stream. - func update(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func update( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -107,32 +119,44 @@ extension Echo_Echo.StreamingServiceProtocol { forMethod: Echo_Echo.Method.Get.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.get(request: request) + handler: { request, context in + try await self.get( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Echo_Echo.Method.Expand.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.expand(request: request) + handler: { request, context in + try await self.expand( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Echo_Echo.Method.Collect.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.collect(request: request) + handler: { request, context in + try await self.collect( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Echo_Echo.Method.Update.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.update(request: request) + handler: { request, context in + try await self.update( + request: request, + context: context + ) } ) } @@ -141,33 +165,63 @@ extension Echo_Echo.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { /// Immediately returns an echo of a request. - func get(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func get( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// Splits a request into words and returns each word in a stream of messages. - func expand(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func expand( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + func collect( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// Streams back messages as they are received in an input stream. - func update(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func update( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `Echo_EchoStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ServiceProtocol { - internal func get(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.get(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func get( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.get( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - internal func expand(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.expand(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func expand( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.expand( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } - internal func collect(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.collect(request: request) + internal func collect( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.collect( + request: request, + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } diff --git a/Examples/v2/echo/Subcommands/Serve.swift b/Examples/v2/echo/Subcommands/Serve.swift index d03adbbe8..5bfa1772f 100644 --- a/Examples/v2/echo/Subcommands/Serve.swift +++ b/Examples/v2/echo/Subcommands/Serve.swift @@ -47,13 +47,15 @@ struct Serve: AsyncParsableCommand { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EchoService: Echo_EchoServiceProtocol { func get( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { return ServerResponse.Single(message: .with { $0.text = request.message.text }) } func collect( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { let messages = try await request.messages.reduce(into: []) { $0.append($1.text) } let joined = messages.joined(separator: " ") @@ -61,7 +63,8 @@ struct EchoService: Echo_EchoServiceProtocol { } func expand( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in let parts = request.message.text.split(separator: " ") @@ -72,7 +75,8 @@ struct EchoService: Echo_EchoServiceProtocol { } func update( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for try await message in request.messages { diff --git a/Examples/v2/hello-world/Generated/helloworld.grpc.swift b/Examples/v2/hello-world/Generated/helloworld.grpc.swift index 0336b83ca..8044411e5 100644 --- a/Examples/v2/hello-world/Generated/helloworld.grpc.swift +++ b/Examples/v2/hello-world/Generated/helloworld.grpc.swift @@ -60,7 +60,10 @@ extension GRPCCore.ServiceDescriptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting - func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func sayHello( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -72,8 +75,11 @@ extension Helloworld_Greeter.StreamingServiceProtocol { forMethod: Helloworld_Greeter.Method.SayHello.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.sayHello(request: request) + handler: { request, context in + try await self.sayHello( + request: request, + context: context + ) } ) } @@ -83,14 +89,23 @@ extension Helloworld_Greeter.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting - func sayHello(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func sayHello( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.ServiceProtocol { - internal func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.sayHello(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func sayHello( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.sayHello( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } diff --git a/Examples/v2/hello-world/Subcommands/Serve.swift b/Examples/v2/hello-world/Subcommands/Serve.swift index fe4e462ea..a9dd178ec 100644 --- a/Examples/v2/hello-world/Subcommands/Serve.swift +++ b/Examples/v2/hello-world/Subcommands/Serve.swift @@ -46,7 +46,8 @@ struct Serve: AsyncParsableCommand { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Greeter: Helloworld_GreeterServiceProtocol { func sayHello( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name diff --git a/Examples/v2/route-guide/Generated/route_guide.grpc.swift b/Examples/v2/route-guide/Generated/route_guide.grpc.swift index cf7cb7182..6a89ed2a3 100644 --- a/Examples/v2/route-guide/Generated/route_guide.grpc.swift +++ b/Examples/v2/route-guide/Generated/route_guide.grpc.swift @@ -92,7 +92,10 @@ internal protocol Routeguide_RouteGuideStreamingServiceProtocol: GRPCCore.Regist /// /// A feature with an empty name is returned if there's no feature at the given /// position. - func getFeature(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func getFeature( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A server-to-client streaming RPC. /// @@ -100,19 +103,28 @@ internal protocol Routeguide_RouteGuideStreamingServiceProtocol: GRPCCore.Regist /// streamed rather than returned at once (e.g. in a response message with a /// repeated field), as the rectangle may cover a large area and contain a /// huge number of features. - func listFeatures(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func listFeatures( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A client-to-server streaming RPC. /// /// Accepts a stream of Points on a route being traversed, returning a /// RouteSummary when traversal is completed. - func recordRoute(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func recordRoute( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A Bidirectional streaming RPC. /// /// Accepts a stream of RouteNotes sent while a route is being traversed, /// while receiving other RouteNotes (e.g. from other users). - func routeChat(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func routeChat( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -124,32 +136,44 @@ extension Routeguide_RouteGuide.StreamingServiceProtocol { forMethod: Routeguide_RouteGuide.Method.GetFeature.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.getFeature(request: request) + handler: { request, context in + try await self.getFeature( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Routeguide_RouteGuide.Method.ListFeatures.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.listFeatures(request: request) + handler: { request, context in + try await self.listFeatures( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Routeguide_RouteGuide.Method.RecordRoute.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.recordRoute(request: request) + handler: { request, context in + try await self.recordRoute( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Routeguide_RouteGuide.Method.RouteChat.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.routeChat(request: request) + handler: { request, context in + try await self.routeChat( + request: request, + context: context + ) } ) } @@ -164,7 +188,10 @@ internal protocol Routeguide_RouteGuideServiceProtocol: Routeguide_RouteGuide.St /// /// A feature with an empty name is returned if there's no feature at the given /// position. - func getFeature(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func getFeature( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// A server-to-client streaming RPC. /// @@ -172,36 +199,63 @@ internal protocol Routeguide_RouteGuideServiceProtocol: Routeguide_RouteGuide.St /// streamed rather than returned at once (e.g. in a response message with a /// repeated field), as the rectangle may cover a large area and contain a /// huge number of features. - func listFeatures(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func listFeatures( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A client-to-server streaming RPC. /// /// Accepts a stream of Points on a route being traversed, returning a /// RouteSummary when traversal is completed. - func recordRoute(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + func recordRoute( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// A Bidirectional streaming RPC. /// /// Accepts a stream of RouteNotes sent while a route is being traversed, /// while receiving other RouteNotes (e.g. from other users). - func routeChat(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func routeChat( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `Routeguide_RouteGuideStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Routeguide_RouteGuide.ServiceProtocol { - internal func getFeature(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.getFeature(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func getFeature( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.getFeature( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - internal func listFeatures(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.listFeatures(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func listFeatures( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.listFeatures( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } - internal func recordRoute(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.recordRoute(request: request) + internal func recordRoute( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.recordRoute( + request: request, + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } diff --git a/Examples/v2/route-guide/Subcommands/Serve.swift b/Examples/v2/route-guide/Subcommands/Serve.swift index 8de97671e..f9b942876 100644 --- a/Examples/v2/route-guide/Subcommands/Serve.swift +++ b/Examples/v2/route-guide/Subcommands/Serve.swift @@ -103,7 +103,8 @@ struct RouteGuideService { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -126,7 +127,8 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in let featuresWithinBounds = self.features.filter { feature in @@ -139,7 +141,8 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { let startTime = ContinuousClock.now var pointsVisited = 0 @@ -173,7 +176,8 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for try await note in request.messages { diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 99c9acfaa..4e49e2828 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -26,14 +26,46 @@ struct IDLToStructuredSwiftTranslator: Translator { server: Bool ) throws -> StructuredSwiftRepresentation { try self.validateInput(codeGenerationRequest) + + var codeBlocks = [CodeBlock]() + let typealiasTranslator = TypealiasTranslator( client: client, server: server, accessLevel: accessLevel ) + codeBlocks.append(contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest)) - let topComment = Comment.preFormatted(codeGenerationRequest.leadingTrivia) + if server { + let serverCodeTranslator = ServerCodeTranslator(accessLevel: accessLevel) + codeBlocks.append(contentsOf: try serverCodeTranslator.translate(from: codeGenerationRequest)) + } + if client { + let clientCodeTranslator = ClientCodeTranslator(accessLevel: accessLevel) + codeBlocks.append(contentsOf: try clientCodeTranslator.translate(from: codeGenerationRequest)) + } + + let fileDescription = FileDescription( + topComment: .preFormatted(codeGenerationRequest.leadingTrivia), + imports: try self.makeImports( + dependencies: codeGenerationRequest.dependencies, + accessLevel: accessLevel, + accessLevelOnImports: accessLevelOnImports + ), + codeBlocks: codeBlocks + ) + + let fileName = String(codeGenerationRequest.fileName.split(separator: ".")[0]) + let file = NamedFileDescription(name: fileName, contents: fileDescription) + return StructuredSwiftRepresentation(file: file) + } + + private func makeImports( + dependencies: [CodeGenerationRequest.Dependency], + accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevelOnImports: Bool + ) throws -> [ImportDescription] { var imports: [ImportDescription] = [] imports.append( ImportDescription( @@ -42,7 +74,7 @@ struct IDLToStructuredSwiftTranslator: Translator { ) ) - for dependency in codeGenerationRequest.dependencies { + for dependency in dependencies { let importDescription = try self.translateImport( dependency: dependency, accessLevelOnImports: accessLevelOnImports @@ -50,33 +82,7 @@ struct IDLToStructuredSwiftTranslator: Translator { imports.append(importDescription) } - var codeBlocks = [CodeBlock]() - codeBlocks.append( - contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest) - ) - - if server { - let serverCodeTranslator = ServerCodeTranslator(accessLevel: accessLevel) - codeBlocks.append( - contentsOf: try serverCodeTranslator.translate(from: codeGenerationRequest) - ) - } - - if client { - let clientCodeTranslator = ClientCodeTranslator(accessLevel: accessLevel) - codeBlocks.append( - contentsOf: try clientCodeTranslator.translate(from: codeGenerationRequest) - ) - } - - let fileDescription = FileDescription( - topComment: topComment, - imports: imports, - codeBlocks: codeBlocks - ) - let fileName = String(codeGenerationRequest.fileName.split(separator: ".")[0]) - let file = NamedFileDescription(name: fileName, contents: fileDescription) - return StructuredSwiftRepresentation(file: file) + return imports } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 313c3410e..f9f88dd95 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -150,7 +150,8 @@ extension ServerCodeTranslator { wrapper: .member(["GRPCCore", "ServerRequest", "Stream"]), wrapped: .member(method.inputType) ) - ) + ), + .init(label: "context", type: .member(["GRPCCore", "ServerContext"])), ], keywords: [.async, .throws], returnType: .identifierType( @@ -229,30 +230,27 @@ extension ServerCodeTranslator { ) -> [FunctionArgumentDescription] { var arguments = [FunctionArgumentDescription]() arguments.append( - .init( + FunctionArgumentDescription( label: "forMethod", - expression: .identifierPattern( - self.methodDescriptorPath(for: method, service: service) - ) + expression: .identifierPattern(self.methodDescriptorPath(for: method, service: service)) ) ) arguments.append( - .init( + FunctionArgumentDescription( label: "deserializer", expression: .identifierPattern(codeGenerationRequest.lookupDeserializer(method.inputType)) ) ) arguments.append( - .init( + FunctionArgumentDescription( label: "serializer", - expression: - .identifierPattern(codeGenerationRequest.lookupSerializer(method.outputType)) + expression: .identifierPattern(codeGenerationRequest.lookupSerializer(method.outputType)) ) ) - let getFunctionCall = Expression.functionCall( + let rpcFunctionCall = Expression.functionCall( calledExpression: .memberAccess( MemberAccessDescription( left: .identifierPattern("self"), @@ -260,20 +258,24 @@ extension ServerCodeTranslator { ) ), arguments: [ - FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")) + FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), + FunctionArgumentDescription(label: "context", expression: .identifierPattern("context")), ] ) let handlerClosureBody = Expression.unaryKeyword( kind: .try, - expression: .unaryKeyword(kind: .await, expression: getFunctionCall) + expression: .unaryKeyword(kind: .await, expression: rpcFunctionCall) ) arguments.append( - .init( + FunctionArgumentDescription( label: "handler", expression: .closureInvocation( - .init(argumentNames: ["request"], body: [.expression(handlerClosureBody)]) + ClosureInvocationDescription( + argumentNames: ["request", "context"], + body: [.expression(handlerClosureBody)] + ) ) ) ) @@ -325,7 +327,8 @@ extension ServerCodeTranslator { wrapper: .member(["GRPCCore", "ServerRequest", inputStreaming]), wrapped: .member(method.inputType) ) - ) + ), + .init(label: "context", type: .member(["GRPCCore", "ServerContext"])), ], keywords: [.async, .throws], returnType: .identifierType( @@ -409,7 +412,10 @@ extension ServerCodeTranslator { right: method.name.generatedLowerCase ) ), - arguments: [FunctionArgumentDescription(label: "request", expression: serverRequest)] + arguments: [ + FunctionArgumentDescription(label: "request", expression: serverRequest), + FunctionArgumentDescription(label: "context", expression: .identifier(.pattern("context"))), + ] ) let responseValue = Expression.unaryKeyword( diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 74f657884..a67bfbe37 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -20,6 +20,7 @@ struct ServerRPCExecutor { /// Executes an RPC using the provided handler. /// /// - Parameters: + /// - context: The context for the RPC. /// - stream: The accepted stream to execute the RPC on. /// - deserializer: A deserializer for messages received from the client. /// - serializer: A serializer for messages to send to the client. @@ -27,6 +28,7 @@ struct ServerRPCExecutor { /// - handler: A handler which turns the request into a response. @inlinable static func execute( + context: ServerContext, stream: RPCStream< RPCAsyncSequence, RPCWriter.Closable @@ -35,7 +37,8 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @Sendable @escaping ( - _ request: ServerRequest.Stream + _ request: ServerRequest.Stream, + _ context: ServerContext ) async throws -> ServerResponse.Stream ) async { // Wait for the first request part from the transport. @@ -44,7 +47,7 @@ struct ServerRPCExecutor { switch firstPart { case .process(let metadata, let inbound): await Self._execute( - method: stream.descriptor, + context: context, metadata: metadata, inbound: inbound, outbound: stream.outbound, @@ -64,7 +67,7 @@ struct ServerRPCExecutor { @inlinable static func _execute( - method: MethodDescriptor, + context: ServerContext, metadata: Metadata, inbound: UnsafeTransfer.AsyncIterator>, outbound: RPCWriter.Closable, @@ -72,13 +75,14 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @escaping @Sendable ( - _ request: ServerRequest.Stream + _ request: ServerRequest.Stream, + _ context: ServerContext ) async throws -> ServerResponse.Stream ) async { if let timeout = metadata.timeout { await Self._processRPCWithTimeout( timeout: timeout, - method: method, + context: context, metadata: metadata, inbound: inbound, outbound: outbound, @@ -89,7 +93,7 @@ struct ServerRPCExecutor { ) } else { await Self._processRPC( - method: method, + context: context, metadata: metadata, inbound: inbound, outbound: outbound, @@ -104,7 +108,7 @@ struct ServerRPCExecutor { @inlinable static func _processRPCWithTimeout( timeout: Duration, - method: MethodDescriptor, + context: ServerContext, metadata: Metadata, inbound: UnsafeTransfer.AsyncIterator>, outbound: RPCWriter.Closable, @@ -112,7 +116,8 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @escaping @Sendable ( - ServerRequest.Stream + _ request: ServerRequest.Stream, + _ context: ServerContext ) async throws -> ServerResponse.Stream ) async { await withTaskGroup(of: ServerExecutorTask.self) { group in @@ -125,7 +130,7 @@ struct ServerRPCExecutor { group.addTask { await Self._processRPC( - method: method, + context: context, metadata: metadata, inbound: inbound, outbound: outbound, @@ -157,7 +162,7 @@ struct ServerRPCExecutor { @inlinable static func _processRPC( - method: MethodDescriptor, + context: ServerContext, metadata: Metadata, inbound: UnsafeTransfer.AsyncIterator>, outbound: RPCWriter.Closable, @@ -165,7 +170,8 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @escaping @Sendable ( - ServerRequest.Stream + _ request: ServerRequest.Stream, + _ context: ServerContext ) async throws -> ServerResponse.Stream ) async { let messages = UncheckedAsyncIteratorSequence(inbound.wrappedValue).map { part in @@ -190,10 +196,10 @@ struct ServerRPCExecutor { metadata: metadata, messages: RPCAsyncSequence(wrapping: messages) ), - context: ServerInterceptorContext(descriptor: method), + context: context, interceptors: interceptors - ) { request, _ in - try await handler(request) + ) { request, context in + try await handler(request, context) } }.castError(to: RPCError.self) { error in RPCError(code: .unknown, message: "Service method threw an unknown error.", cause: error) @@ -295,11 +301,11 @@ extension ServerRPCExecutor { @inlinable static func _intercept( request: ServerRequest.Stream, - context: ServerInterceptorContext, + context: ServerContext, interceptors: [any ServerInterceptor], finally: @escaping @Sendable ( _ request: ServerRequest.Stream, - _ context: ServerInterceptorContext + _ context: ServerContext ) async throws -> ServerResponse.Stream ) async throws -> ServerResponse.Stream { return try await self._intercept( @@ -313,11 +319,11 @@ extension ServerRPCExecutor { @inlinable static func _intercept( request: ServerRequest.Stream, - context: ServerInterceptorContext, + context: ServerContext, iterator: Array.Iterator, finally: @escaping @Sendable ( _ request: ServerRequest.Stream, - _ context: ServerInterceptorContext + _ context: ServerContext ) async throws -> ServerResponse.Stream ) async throws -> ServerResponse.Stream { var iterator = iterator diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index b6ae3f074..bc2f58fef 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -43,6 +43,7 @@ public struct RPCRouter: Sendable { RPCAsyncSequence, RPCWriter.Closable >, + _ context: ServerContext, _ interceptors: [any ServerInterceptor] ) async -> Void @@ -52,11 +53,13 @@ public struct RPCRouter: Sendable { deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @Sendable @escaping ( - _ request: ServerRequest.Stream + _ request: ServerRequest.Stream, + _ context: ServerContext ) async throws -> ServerResponse.Stream ) { - self._fn = { stream, interceptors in + self._fn = { stream, context, interceptors in await ServerRPCExecutor.execute( + context: context, stream: stream, deserializer: deserializer, serializer: serializer, @@ -72,9 +75,10 @@ public struct RPCRouter: Sendable { RPCAsyncSequence, RPCWriter.Closable >, + context: ServerContext, interceptors: [any ServerInterceptor] ) async { - await self._fn(stream, interceptors) + await self._fn(stream, context, interceptors) } } @@ -119,7 +123,8 @@ public struct RPCRouter: Sendable { deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @Sendable @escaping ( - _ request: ServerRequest.Stream + _ request: ServerRequest.Stream, + _ context: ServerContext ) async throws -> ServerResponse.Stream ) { self.handlers[descriptor] = RPCHandler( @@ -147,10 +152,11 @@ extension RPCRouter { RPCAsyncSequence, RPCWriter.Closable >, + context: ServerContext, interceptors: [any ServerInterceptor] ) async { if let handler = self.handlers[stream.descriptor] { - await handler.handle(stream: stream, interceptors: interceptors) + await handler.handle(stream: stream, context: context, interceptors: interceptors) } else { // If this throws then the stream must be closed which we can't do anything about, so ignore // any error. diff --git a/Sources/GRPCCore/Call/Server/ServerContext.swift b/Sources/GRPCCore/Call/Server/ServerContext.swift new file mode 100644 index 000000000..a11f09acb --- /dev/null +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/// Additional information about an RPC handled by a server. +public struct ServerContext: Sendable { + /// A description of the method being called. + public var descriptor: MethodDescriptor + + /// Create a new server context. + public init(descriptor: MethodDescriptor) { + self.descriptor = descriptor + } +} diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index 69981b529..2243fe8f2 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -60,7 +60,7 @@ /// } /// ``` /// -/// For server-side interceptors see ``ClientInterceptor``. +/// For client-side interceptors see ``ClientInterceptor``. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ServerInterceptor: Sendable { /// Intercept a request object. @@ -74,21 +74,10 @@ public protocol ServerInterceptor: Sendable { /// - Returns: A response object. func intercept( request: ServerRequest.Stream, - context: ServerInterceptorContext, + context: ServerContext, next: @Sendable ( _ request: ServerRequest.Stream, - _ context: ServerInterceptorContext + _ context: ServerContext ) async throws -> ServerResponse.Stream ) async throws -> ServerResponse.Stream } - -/// A context passed to client interceptors containing additional information about the RPC. -public struct ServerInterceptorContext: Sendable { - /// A description of the method being called. - public var descriptor: MethodDescriptor - - /// Create a new client interceptor context. - public init(descriptor: MethodDescriptor) { - self.descriptor = descriptor - } -} diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index e5d7fa8ee..da46e2db8 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -206,8 +206,8 @@ public final class GRPCServer: Sendable { } do { - try await transport.listen { stream in - await self.router.handle(stream: stream, interceptors: self.interceptors) + try await transport.listen { stream, context in + await self.router.handle(stream: stream, context: context, interceptors: self.interceptors) } } catch { throw RuntimeError( diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index abb4b8c90..79c5c0fc6 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -29,9 +29,12 @@ public protocol ServerTransport: Sendable { /// You can call ``beginGracefulShutdown()`` to stop the transport from accepting new streams. Existing /// streams must be allowed to complete naturally. However, transports may also enforce a grace /// period after which any open streams may be cancelled. You can also cancel the task running - /// ``listen(_:)`` to abruptly close connections and streams. + /// ``listen(streamHandler:)`` to abruptly close connections and streams. func listen( - _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws /// Indicates to the transport that no new streams should be accepted. diff --git a/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift index a900b5402..769db9bf7 100644 --- a/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift +++ b/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift @@ -144,7 +144,10 @@ package final class CommonHTTP2ServerTransport< } package func listen( - _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { defer { switch self.listeningAddressState.withLock({ $0.close() }) { @@ -192,7 +195,10 @@ package final class CommonHTTP2ServerTransport< private func handleConnection( _ connection: NIOAsyncChannel, multiplexer: ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer, - streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { try await connection.executeThenClose { inbound, _ in await withDiscardingTaskGroup { group in @@ -220,7 +226,10 @@ package final class CommonHTTP2ServerTransport< private func handleStream( _ stream: NIOAsyncChannel, - handler streamHandler: @escaping @Sendable (RPCStream) async -> Void, + handler streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void, descriptor: EventLoopFuture ) async { // It's okay to ignore these errors: @@ -244,7 +253,8 @@ package final class CommonHTTP2ServerTransport< ) ) - await streamHandler(rpcStream) + let context = ServerContext(descriptor: descriptor) + await streamHandler(rpcStream, context) } } diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index d7bda6350..0aa14aefa 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -163,9 +163,12 @@ extension HTTP2ServerTransport { } public func listen( - _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { - try await self.underlyingTransport.listen(streamHandler) + try await self.underlyingTransport.listen(streamHandler: streamHandler) } public func beginGracefulShutdown() { diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift index 94628e215..31fd3a312 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift @@ -117,9 +117,12 @@ extension HTTP2ServerTransport { } public func listen( - _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { - try await self.underlyingTransport.listen(streamHandler) + try await self.underlyingTransport.listen(streamHandler: streamHandler) } public func beginGracefulShutdown() { diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift index 4c627037f..2bb2ed57d 100644 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift @@ -56,12 +56,16 @@ public struct InProcessServerTransport: ServerTransport, Sendable { } public func listen( - _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { await withDiscardingTaskGroup { group in for await stream in self.newStreams { group.addTask { - await streamHandler(stream) + let context = ServerContext(descriptor: stream.descriptor) + await streamHandler(stream, context) } } } diff --git a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift index 5a568e425..a2ebe456c 100644 --- a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift +++ b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift @@ -44,8 +44,8 @@ public struct ServerTracingInterceptor: ServerInterceptor { /// that has been configured when bootstrapping `swift-distributed-tracing` in your application. public func intercept( request: ServerRequest.Stream, - context: ServerInterceptorContext, - next: @Sendable (ServerRequest.Stream, ServerInterceptorContext) async throws -> + context: ServerContext, + next: @Sendable (ServerRequest.Stream, ServerContext) async throws -> ServerResponse.Stream ) async throws -> ServerResponse.Stream where Input: Sendable, Output: Sendable { var serviceContext = ServiceContext.topLevel diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift index 5fed75d22..bbdbf3e49 100644 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ b/Sources/InteroperabilityTests/Generated/test.grpc.swift @@ -200,38 +200,62 @@ extension GRPCCore.ServiceDescriptor { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One empty request followed by one empty response. - func emptyCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func emptyCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// One request followed by one response. - func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func unaryCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. - func cacheableUnaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func cacheableUnaryCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. - func streamingOutputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingOutputCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. - func streamingInputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingInputCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. - func fullDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func fullDuplexCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. - func halfDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func halfDuplexCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. - func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func unimplementedCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -243,64 +267,88 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { forMethod: Grpc_Testing_TestService.Method.EmptyCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.emptyCall(request: request) + handler: { request, context in + try await self.emptyCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.UnaryCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.unaryCall(request: request) + handler: { request, context in + try await self.unaryCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.cacheableUnaryCall(request: request) + handler: { request, context in + try await self.cacheableUnaryCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.streamingOutputCall(request: request) + handler: { request, context in + try await self.streamingOutputCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.streamingInputCall(request: request) + handler: { request, context in + try await self.streamingInputCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.fullDuplexCall(request: request) + handler: { request, context in + try await self.fullDuplexCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.halfDuplexCall(request: request) + handler: { request, context in + try await self.halfDuplexCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.unimplementedCall(request: request) + handler: { request, context in + try await self.unimplementedCall( + request: request, + context: context + ) } ) } @@ -311,70 +359,130 @@ extension Grpc_Testing_TestService.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestService.StreamingServiceProtocol { /// One empty request followed by one empty response. - func emptyCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func emptyCall( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// One request followed by one response. - func unaryCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func unaryCall( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// One request followed by one response. Response has cache control /// headers set such that a caching HTTP proxy (such as GFE) can /// satisfy subsequent requests. - func cacheableUnaryCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func cacheableUnaryCall( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// One request followed by a sequence of responses (streamed download). /// The server returns the payload with client desired type and sizes. - func streamingOutputCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func streamingOutputCall( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by one response (streamed upload). /// The server returns the aggregated size of client payload as the result. - func streamingInputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + func streamingInputCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// A sequence of requests with each request served by the server immediately. /// As one request could lead to multiple responses, this interface /// demonstrates the idea of full duplexing. - func fullDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func fullDuplexCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// A sequence of requests followed by a sequence of responses. /// The server buffers all the client requests and then serves them in order. A /// stream of responses are returned to the client when the server starts with /// first request. - func halfDuplexCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func halfDuplexCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// The test server will not implement this method. It will be used /// to test the behavior when clients call unimplemented methods. - func unimplementedCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func unimplementedCall( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_TestServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_TestService.ServiceProtocol { - public func emptyCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.emptyCall(request: GRPCCore.ServerRequest.Single(stream: request)) + public func emptyCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.emptyCall( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - public func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unaryCall(request: GRPCCore.ServerRequest.Single(stream: request)) + public func unaryCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unaryCall( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - public func cacheableUnaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.cacheableUnaryCall(request: GRPCCore.ServerRequest.Single(stream: request)) + public func cacheableUnaryCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.cacheableUnaryCall( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - public func streamingOutputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingOutputCall(request: GRPCCore.ServerRequest.Single(stream: request)) + public func streamingOutputCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.streamingOutputCall( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } - public func streamingInputCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingInputCall(request: request) + public func streamingInputCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.streamingInputCall( + request: request, + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - public func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unimplementedCall(request: GRPCCore.ServerRequest.Single(stream: request)) + public func unimplementedCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unimplementedCall( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } @@ -384,7 +492,10 @@ extension Grpc_Testing_TestService.ServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_UnimplementedServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// A call that no server should implement - func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func unimplementedCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -396,8 +507,11 @@ extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { forMethod: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.unimplementedCall(request: request) + handler: { request, context in + try await self.unimplementedCall( + request: request, + context: context + ) } ) } @@ -408,14 +522,23 @@ extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_UnimplementedServiceServiceProtocol: Grpc_Testing_UnimplementedService.StreamingServiceProtocol { /// A call that no server should implement - func unimplementedCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func unimplementedCall( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_UnimplementedServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_UnimplementedService.ServiceProtocol { - public func unimplementedCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unimplementedCall(request: GRPCCore.ServerRequest.Single(stream: request)) + public func unimplementedCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unimplementedCall( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } @@ -423,9 +546,15 @@ extension Grpc_Testing_UnimplementedService.ServiceProtocol { /// A service used to control reconnect server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func start(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func start( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream - func stop(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func stop( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -437,16 +566,22 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { forMethod: Grpc_Testing_ReconnectService.Method.Start.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.start(request: request) + handler: { request, context in + try await self.start( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_ReconnectService.Method.Stop.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.stop(request: request) + handler: { request, context in + try await self.stop( + request: request, + context: context + ) } ) } @@ -455,21 +590,39 @@ extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { /// A service used to control reconnect server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_ReconnectService.StreamingServiceProtocol { - func start(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func start( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single - func stop(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func stop( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_ReconnectServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_ReconnectService.ServiceProtocol { - public func start(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.start(request: GRPCCore.ServerRequest.Single(stream: request)) + public func start( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.start( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - public func stop(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.stop(request: GRPCCore.ServerRequest.Single(stream: request)) + public func stop( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.stop( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift index 02c27c1f3..f4c79b784 100644 --- a/Sources/InteroperabilityTests/TestService.swift +++ b/Sources/InteroperabilityTests/TestService.swift @@ -22,16 +22,16 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { public init() {} public func unimplementedCall( - request: ServerRequest.Single - ) async throws - -> ServerResponse.Single - { + request: ServerRequest.Single, + context: ServerContext + ) async throws -> ServerResponse.Single { throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") } /// Server implements `emptyCall` which immediately returns the empty message. public func emptyCall( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let message = Grpc_Testing_Empty() let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() @@ -49,7 +49,8 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// If the server does not support the `responseType`, then it should fail the RPC with /// `INVALID_ARGUMENT`. public func unaryCall( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is // set), so we have to check via the encoding header. Note that it is possible for the header @@ -108,10 +109,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// headers such that the response can be cached by proxies in the response path. Server should /// be behind a caching proxy for this test to pass. Currently we set the max-age to 60 seconds. public func cacheableUnaryCall( - request: ServerRequest.Single - ) async throws - -> ServerResponse.Single - { + request: ServerRequest.Single, + context: ServerContext + ) async throws -> ServerResponse.Single { throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") } @@ -121,12 +121,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// bytes, as specified by its respective `ResponseParameter`. After sending all responses, it /// closes with OK. public func streamingOutputCall( - request: ServerRequest.Single< - Grpc_Testing_StreamingOutputCallRequest - > - ) async throws - -> ServerResponse.Stream - { + request: ServerRequest.Single, + context: ServerContext + ) async throws -> ServerResponse.Stream { let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() return ServerResponse.Stream(metadata: initialMetadata) { writer in for responseParameter in request.message.responseParameters { @@ -147,10 +144,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// `StreamingInputCallResponse` where `aggregatedPayloadSize` is the sum of all request payload /// bodies received. public func streamingInputCall( - request: ServerRequest.Stream - ) async throws - -> ServerResponse.Single - { + request: ServerRequest.Stream, + context: ServerContext + ) async throws -> ServerResponse.Single { let isRequestCompressed = request.metadata["grpc-encoding"].filter({ $0 != "identity" }).count > 0 var aggregatedPayloadSize = 0 @@ -187,10 +183,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// of size `ResponseParameter.size` bytes, as specified by its respective `ResponseParameter`s. /// After receiving half close and sending all responses, it closes with OK. public func fullDuplexCall( - request: ServerRequest.Stream - ) async throws - -> ServerResponse.Stream - { + request: ServerRequest.Stream, + context: ServerContext + ) async throws -> ServerResponse.Stream { let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() return ServerResponse.Stream(metadata: initialMetadata) { writer in for try await message in request.messages { @@ -226,10 +221,9 @@ public struct TestService: Grpc_Testing_TestService.ServiceProtocol { /// /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md public func halfDuplexCall( - request: ServerRequest.Stream - ) async throws - -> ServerResponse.Stream - { + request: ServerRequest.Stream, + context: ServerContext + ) async throws -> ServerResponse.Stream { throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") } } diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift index 16f73d5b3..a2f625c74 100644 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ b/Sources/Services/Health/Generated/health.grpc.swift @@ -82,7 +82,10 @@ package protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.Registr /// server unhealthy if they do not receive a timely response. /// /// Check implementations should be idempotent and side effect free. - func check(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func check( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current @@ -99,7 +102,10 @@ package protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.Registr /// should assume this method is not supported and should not retry the /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. - func watch(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func watch( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -111,16 +117,22 @@ extension Grpc_Health_V1_Health.StreamingServiceProtocol { forMethod: Grpc_Health_V1_Health.Method.Check.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.check(request: request) + handler: { request, context in + try await self.check( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Health_V1_Health.Method.Watch.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.watch(request: request) + handler: { request, context in + try await self.watch( + request: request, + context: context + ) } ) } @@ -140,7 +152,10 @@ package protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.Str /// server unhealthy if they do not receive a timely response. /// /// Check implementations should be idempotent and side effect free. - func check(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func check( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// Performs a watch for the serving status of the requested service. /// The server will immediately send back a message indicating the current @@ -157,19 +172,34 @@ package protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.Str /// should assume this method is not supported and should not retry the /// call. If the call terminates with any other status (including OK), /// clients should retry the call with appropriate exponential backoff. - func watch(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func watch( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `Grpc_Health_V1_HealthStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Health_V1_Health.ServiceProtocol { - package func check(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.check(request: GRPCCore.ServerRequest.Single(stream: request)) + package func check( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.check( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - package func watch(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.watch(request: GRPCCore.ServerRequest.Single(stream: request)) + package func watch( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.watch( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } } diff --git a/Sources/Services/Health/HealthService.swift b/Sources/Services/Health/HealthService.swift index 72f70e064..362e707f2 100644 --- a/Sources/Services/Health/HealthService.swift +++ b/Sources/Services/Health/HealthService.swift @@ -22,7 +22,8 @@ internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { private let state = HealthService.State() func check( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let service = request.message.service @@ -37,16 +38,15 @@ internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { } func watch( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async -> ServerResponse.Stream { let service = request.message.service let statuses = AsyncStream.makeStream(of: Grpc_Health_V1_HealthCheckResponse.ServingStatus.self) self.state.addContinuation(statuses.continuation, forService: service) - return ServerResponse.Stream( - of: Grpc_Health_V1_HealthCheckResponse.self - ) { writer in + return ServerResponse.Stream(of: Grpc_Health_V1_HealthCheckResponse.self) { writer in var response = Grpc_Health_V1_HealthCheckResponse() for await status in statuses.stream { diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift index 11631eaaf..b73d46534 100644 --- a/Sources/performance-worker/BenchmarkService.swift +++ b/Sources/performance-worker/BenchmarkService.swift @@ -27,7 +27,8 @@ final class BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// One request followed by one response. /// The server returns a client payload with the size requested by the client. func unaryCall( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { // Throw an error if the status is not `ok`. Otherwise, an `ok` status is automatically sent // if the request is successful. @@ -47,7 +48,8 @@ final class BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Repeated sequence of one request followed by one response. /// The server returns a payload with the size requested by the client for each received message. func streamingCall( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for try await message in request.messages { @@ -71,7 +73,8 @@ final class BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Single-sided unbounded streaming from client to server. /// The server returns a payload with the size requested by the client once the client does WritesDone. func streamingFromClient( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { var responseSize = 0 for try await message in request.messages { @@ -93,7 +96,8 @@ final class BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Single-sided unbounded streaming from server to client. /// The server repeatedly returns a payload with the size requested by the client. func streamingFromServer( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { if request.message.responseStatus.isInitialized { try self.checkOkStatus(request.message.responseStatus) @@ -116,7 +120,8 @@ final class BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { /// Two-sided unbounded streaming between server to client. /// Both sides send the content of their own choice to the other. func streamingBothWays( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { // The 100 size is used by the other implementations as well. // We are using the same canned response size for all responses diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift index 663922d0b..d8b4cdc6b 100644 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift @@ -99,24 +99,39 @@ extension GRPCCore.ServiceDescriptor { internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// One request followed by one response. /// The server returns the client payload as-is. - func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func unaryCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response - func streamingCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingFromClient( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is - func streamingFromServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingFromServer( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other - func streamingBothWays(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingBothWays( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -128,40 +143,55 @@ extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { forMethod: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.unaryCall(request: request) + handler: { request, context in + try await self.unaryCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.streamingCall(request: request) + handler: { request, context in + try await self.streamingCall( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.streamingFromClient(request: request) + handler: { request, context in + try await self.streamingFromClient( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.streamingFromServer(request: request) + handler: { request, context in + try await self.streamingFromServer( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.streamingBothWays(request: request) + handler: { request, context in + try await self.streamingBothWays( + request: request, + context: context + ) } ) } @@ -171,41 +201,74 @@ extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_BenchmarkService.StreamingServiceProtocol { /// One request followed by one response. /// The server returns the client payload as-is. - func unaryCall(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func unaryCall( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// Repeated sequence of one request followed by one response. /// Should be called streaming ping-pong /// The server returns the client payload as-is on each response - func streamingCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Single-sided unbounded streaming from client to server /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + func streamingFromClient( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// Single-sided unbounded streaming from server to client /// The server repeatedly returns the client payload as-is - func streamingFromServer(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func streamingFromServer( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Two-sided unbounded streaming between server to client /// Both sides send the content of their own choice to the other - func streamingBothWays(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func streamingBothWays( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `Grpc_Testing_BenchmarkServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_BenchmarkService.ServiceProtocol { - internal func unaryCall(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unaryCall(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func unaryCall( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unaryCall( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - internal func streamingFromClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingFromClient(request: request) + internal func streamingFromClient( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.streamingFromClient( + request: request, + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - internal func streamingFromServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingFromServer(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func streamingFromServer( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.streamingFromServer( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } } diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift index f1637253f..58bad0ad0 100644 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift @@ -90,7 +90,10 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// stats. Closing the stream will initiate shutdown of the test server /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func runServer( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus @@ -98,13 +101,22 @@ internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.R /// stats. Closing the stream will initiate shutdown of the test client /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func runClient( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Just return the core count - unary call - func coreCount(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func coreCount( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Quit this worker - func quitWorker(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func quitWorker( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -116,32 +128,44 @@ extension Grpc_Testing_WorkerService.StreamingServiceProtocol { forMethod: Grpc_Testing_WorkerService.Method.RunServer.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.runServer(request: request) + handler: { request, context in + try await self.runServer( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.RunClient.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.runClient(request: request) + handler: { request, context in + try await self.runClient( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.CoreCount.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.coreCount(request: request) + handler: { request, context in + try await self.coreCount( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Grpc_Testing_WorkerService.Method.QuitWorker.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.quitWorker(request: request) + handler: { request, context in + try await self.quitWorker( + request: request, + context: context + ) } ) } @@ -155,7 +179,10 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// stats. Closing the stream will initiate shutdown of the test server /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runServer(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func runServer( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Start client with specified workload. /// First request sent specifies the ClientConfig followed by ClientStatus @@ -163,25 +190,46 @@ internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_Worker /// stats. Closing the stream will initiate shutdown of the test client /// and once the shutdown has finished, the OK status is sent to terminate /// this RPC. - func runClient(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func runClient( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Just return the core count - unary call - func coreCount(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func coreCount( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// Quit this worker - func quitWorker(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func quitWorker( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Grpc_Testing_WorkerServiceStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Grpc_Testing_WorkerService.ServiceProtocol { - internal func coreCount(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.coreCount(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func coreCount( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.coreCount( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - internal func quitWorker(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.quitWorker(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func quitWorker( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.quitWorker( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } \ No newline at end of file diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift index 2864fc405..945dca3a7 100644 --- a/Sources/performance-worker/WorkerService.swift +++ b/Sources/performance-worker/WorkerService.swift @@ -225,7 +225,8 @@ final class WorkerService: Sendable { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { func quitWorker( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let onQuit = self.state.withLockedValue { $0.quit() } @@ -246,7 +247,8 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { } func coreCount( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let coreCount = System.coreCount return ServerResponse.Single( @@ -257,7 +259,8 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { } func runServer( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in try await withThrowingTaskGroup(of: Void.self) { group in @@ -328,7 +331,8 @@ extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { } func runClient( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in try await withThrowingTaskGroup(of: Void.self) { group in diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index ea8ff3186..8ec1c8015 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -54,7 +54,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod - func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func unary( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -65,8 +68,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.unary(request: request) + handler: { request, context in + try await self.unary( + request: request, + context: context + ) } ) } @@ -75,13 +81,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod - func unary(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func unary( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - public func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unary(request: GRPCCore.ServerRequest.Single(stream: request)) + public func unary( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unary( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } @@ -123,7 +138,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func inputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -134,8 +152,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.inputStreaming(request: request) + handler: { request, context in + try await self.inputStreaming( + request: request, + context: context + ) } ) } @@ -144,13 +165,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + func inputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - package func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.inputStreaming(request: request) + package func inputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.inputStreaming( + request: request, + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } @@ -196,7 +226,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod - func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func outputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -207,8 +240,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.outputStreaming(request: request) + handler: { request, context in + try await self.outputStreaming( + request: request, + context: context + ) } ) } @@ -217,13 +253,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod - func outputStreaming(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func outputStreaming( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - public func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.outputStreaming(request: GRPCCore.ServerRequest.Single(stream: request)) + public func outputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.outputStreaming( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } } @@ -269,7 +314,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func bidirectionalStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -280,8 +328,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { forMethod: NamespaceA_ServiceA.Method.BidirectionalStreaming.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.bidirectionalStreaming(request: request) + handler: { request, context in + try await self.bidirectionalStreaming( + request: request, + context: context + ) } ) } @@ -290,7 +341,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func bidirectionalStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -350,10 +404,16 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod - func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func inputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream /// Documentation for outputStreamingMethod - func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func outputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -364,16 +424,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.inputStreaming(request: request) + handler: { request, context in + try await self.inputStreaming( + request: request, + context: context + ) } ) router.registerHandler( forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.outputStreaming(request: request) + handler: { request, context in + try await self.outputStreaming( + request: request, + context: context + ) } ) } @@ -382,21 +448,39 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod - func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + func inputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single /// Documentation for outputStreamingMethod - func outputStreaming(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func outputStreaming( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { - internal func inputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.inputStreaming(request: request) + internal func inputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.inputStreaming( + request: request, + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - internal func outputStreaming(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.outputStreaming(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func outputStreaming( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.outputStreaming( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } } @@ -434,7 +518,10 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA - func methodA(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func methodA( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -445,8 +532,11 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { forMethod: ServiceA.Method.MethodA.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.methodA(request: request) + handler: { request, context in + try await self.methodA( + request: request, + context: context + ) } ) } @@ -455,13 +545,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA - func methodA(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func methodA( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `ServiceAStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ServiceProtocol { - internal func methodA(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.methodA(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func methodA( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.methodA( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index d9bf10f23..06f200ae6 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -121,7 +121,7 @@ struct ClientRPCExecutorTestHarness { ) async throws { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await self.serverTransport.listen { stream in + try await self.serverTransport.listen { stream, context in try? await self.server.handle(stream: stream) } } diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index 274913ab8..8d7e0a543 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -93,16 +93,20 @@ struct ServerRPCExecutorTestHarness { } group.addTask { + let context = ServerContext(descriptor: MethodDescriptor(service: "foo", method: "bar")) await ServerRPCExecutor.execute( + context: context, stream: RPCStream( - descriptor: MethodDescriptor(service: "foo", method: "bar"), + descriptor: context.descriptor, inbound: RPCAsyncSequence(wrapping: input.stream), outbound: RPCWriter.Closable(wrapping: output.continuation) ), deserializer: deserializer, serializer: serializer, interceptors: self.interceptors, - handler: { try await handler.handle($0) } + handler: { stream, context in + try await handler.handle(stream) + } ) } diff --git a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift index c1d9a023f..86fe1bd1e 100644 --- a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift @@ -34,7 +34,7 @@ final class RPCRouterTests: XCTestCase { forMethod: method, deserializer: IdentityDeserializer(), serializer: IdentitySerializer() - ) { _ in + ) { _, _ in throw RPCError(code: .failedPrecondition, message: "Shouldn't be called") } @@ -50,7 +50,7 @@ final class RPCRouterTests: XCTestCase { forMethod: method, deserializer: IdentityDeserializer(), serializer: IdentitySerializer() - ) { _ in + ) { _, _ in throw RPCError(code: .failedPrecondition, message: "Shouldn't be called") } diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 97c68aba9..9a3dc96c7 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -50,10 +50,10 @@ struct RejectAllServerInterceptor: ServerInterceptor { func intercept( request: ServerRequest.Stream, - context: ServerInterceptorContext, + context: ServerContext, next: @Sendable ( ServerRequest.Stream, - ServerInterceptorContext + ServerContext ) async throws -> ServerResponse.Stream ) async throws -> ServerResponse.Stream { if self.throw { @@ -75,10 +75,10 @@ struct RequestCountingServerInterceptor: ServerInterceptor { func intercept( request: ServerRequest.Stream, - context: ServerInterceptorContext, + context: ServerContext, next: @Sendable ( ServerRequest.Stream, - ServerInterceptorContext + ServerContext ) async throws -> ServerResponse.Stream ) async throws -> ServerResponse.Stream { self.counter.increment() diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index f1b462376..e5438c550 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -61,7 +61,7 @@ struct BinaryEcho: RegistrableRPCService { forMethod: Methods.get, deserializer: deserializer, serializer: serializer - ) { streamRequest in + ) { streamRequest, context in let singleRequest = try await ServerRequest.Single(stream: streamRequest) let singleResponse = try await self.get(singleRequest) return ServerResponse.Stream(single: singleResponse) @@ -71,7 +71,7 @@ struct BinaryEcho: RegistrableRPCService { forMethod: Methods.collect, deserializer: deserializer, serializer: serializer - ) { streamRequest in + ) { streamRequest, context in let singleResponse = try await self.collect(streamRequest) return ServerResponse.Stream(single: singleResponse) } @@ -80,7 +80,7 @@ struct BinaryEcho: RegistrableRPCService { forMethod: Methods.expand, deserializer: deserializer, serializer: serializer - ) { streamRequest in + ) { streamRequest, context in let singleRequest = try await ServerRequest.Single(stream: streamRequest) let streamResponse = try await self.expand(singleRequest) return streamResponse @@ -90,7 +90,7 @@ struct BinaryEcho: RegistrableRPCService { forMethod: Methods.update, deserializer: deserializer, serializer: serializer - ) { streamRequest in + ) { streamRequest, context in let streamResponse = try await self.update(streamRequest) return streamResponse } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index b5953dd3a..086cf2127 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -88,17 +88,23 @@ struct AnyServerTransport: ServerTransport, Sendable { private let _listen: @Sendable ( - @escaping @Sendable (RPCStream) async -> Void + @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws -> Void private let _stopListening: @Sendable () -> Void init(wrapping transport: Transport) { - self._listen = { streamHandler in try await transport.listen(streamHandler) } + self._listen = { streamHandler in try await transport.listen(streamHandler: streamHandler) } self._stopListening = { transport.beginGracefulShutdown() } } func listen( - _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { try await self._listen(streamHandler) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index cd3df5af5..9bbed93a4 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -96,11 +96,14 @@ struct StreamCountingServerTransport: ServerTransport, Sendable { } func listen( - _ streamHandler: @escaping @Sendable (RPCStream) async -> Void + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { - try await self.transport.listen { stream in + try await self.transport.listen { stream, context in self._acceptedStreams.increment() - await streamHandler(stream) + await streamHandler(stream, context) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index b9b1b6d80..b9c9b97e6 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -53,7 +53,12 @@ struct ThrowOnStreamCreationTransport: ClientTransport { @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ThrowOnRunServerTransport: ServerTransport { - func listen(_ streamHandler: (RPCStream) async -> Void) async throws { + func listen( + streamHandler: ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void + ) async throws { throw RPCError( code: .unavailable, message: "The '\(type(of: self))' transport is never available." @@ -74,7 +79,10 @@ struct ThrowOnSignalServerTransport: ServerTransport { } func listen( - _ streamHandler: (GRPCCore.RPCStream) async -> Void + streamHandler: ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void ) async throws { for await _ in self.signal {} diff --git a/Tests/GRPCHTTP2TransportTests/ControlService.swift b/Tests/GRPCHTTP2TransportTests/ControlService.swift index dfb83b132..9139d9a42 100644 --- a/Tests/GRPCHTTP2TransportTests/ControlService.swift +++ b/Tests/GRPCHTTP2TransportTests/ControlService.swift @@ -21,25 +21,29 @@ import struct Foundation.Data @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ControlService: ControlStreamingServiceProtocol { func unary( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { try await self.handle(request: request) } func serverStream( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { try await self.handle(request: request) } func clientStream( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { try await self.handle(request: request) } func bidiStream( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { try await self.handle(request: request) } diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift index ebd5c8e76..8833dc856 100644 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift @@ -90,13 +90,25 @@ extension GRPCCore.ServiceDescriptor { /// the output. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func unary( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream - func serverStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func serverStream( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream - func clientStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func clientStream( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream - func bidiStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func bidiStream( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -108,32 +120,44 @@ extension Control.StreamingServiceProtocol { forMethod: Control.Method.Unary.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.unary(request: request) + handler: { request, context in + try await self.unary( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Control.Method.ServerStream.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.serverStream(request: request) + handler: { request, context in + try await self.serverStream( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Control.Method.ClientStream.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.clientStream(request: request) + handler: { request, context in + try await self.clientStream( + request: request, + context: context + ) } ) router.registerHandler( forMethod: Control.Method.BidiStream.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.bidiStream(request: request) + handler: { request, context in + try await self.bidiStream( + request: request, + context: context + ) } ) } @@ -145,30 +169,60 @@ extension Control.StreamingServiceProtocol { /// the output. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { - func unary(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func unary( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single - func serverStream(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Stream + func serverStream( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream - func clientStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Single + func clientStream( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single - func bidiStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func bidiStream( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Partial conformance to `ControlStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Control.ServiceProtocol { - internal func unary(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unary(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func unary( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.unary( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } - internal func serverStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.serverStream(request: GRPCCore.ServerRequest.Single(stream: request)) + internal func serverStream( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.serverStream( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return response } - internal func clientStream(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.clientStream(request: request) + internal func clientStream( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.clientStream( + request: request, + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift index 2d94ce482..70ae83707 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift @@ -33,7 +33,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -53,7 +53,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -73,7 +73,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -97,7 +97,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -116,7 +116,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { try? await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -145,7 +145,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { try? await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } do { _ = try await transport.listeningAddress diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift index a74d8374e..2afe9e9fa 100644 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift @@ -79,7 +79,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -99,7 +99,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -123,7 +123,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { try await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -145,7 +145,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { try? await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } } group.addTask { @@ -174,7 +174,7 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase { try? await withThrowingDiscardingTaskGroup { group in group.addTask { - try await transport.listen { _ in } + try await transport.listen { _, _ in } do { _ = try await transport.listeningAddress diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index d579ca532..bde6f7bb6 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -171,7 +171,7 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await server.listen { stream in + try await server.listen { stream, context in let receivedMessages = try? await stream.inbound.reduce(into: []) { $0.append($1) } try? await stream.outbound.write(RPCResponsePart.message([42])) await stream.outbound.finish() diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 7816bc79a..7cd8fed20 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -41,7 +41,8 @@ final class InProcessServerTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await transport.listen { stream in + try await transport.listen { stream, context in + XCTAssertEqual(context.descriptor, stream.descriptor) let partValue = try? await stream.inbound.reduce(into: []) { $0.append($1) } XCTAssertEqual(partValue, [.message([42])]) transport.beginGracefulShutdown() @@ -71,7 +72,7 @@ final class InProcessServerTransportTests: XCTestCase { try transport.acceptStream(firstStream) - try await transport.listen { stream in + try await transport.listen { stream, context in let firstStreamMessages = try? await stream.inbound.reduce(into: []) { $0.append($1) } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index a18fb275c..e192b1e5a 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -241,7 +241,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func sayHello( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -253,8 +256,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { forMethod: Helloworld_Greeter.Method.SayHello.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.sayHello(request: request) + handler: { request, context in + try await self.sayHello( + request: request, + context: context + ) } ) } @@ -264,19 +270,29 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func sayHello( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.ServiceProtocol { - public func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.sayHello(request: GRPCCore.ServerRequest.Single(stream: request)) + public func sayHello( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.sayHello( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } """ ) + try testCodeGeneration( proto: Google_Protobuf_FileDescriptorProto.helloWorldEmptyPackage, indentation: 2, @@ -348,7 +364,10 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream + func sayHello( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -360,8 +379,11 @@ final class ProtobufCodeGeneratorTests: XCTestCase { forMethod: Greeter.Method.SayHello.descriptor, deserializer: GRPCProtobuf.ProtobufDeserializer(), serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request in - try await self.sayHello(request: request) + handler: { request, context in + try await self.sayHello( + request: request, + context: context + ) } ) } @@ -371,14 +393,23 @@ final class ProtobufCodeGeneratorTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol GreeterServiceProtocol: Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: GRPCCore.ServerRequest.Single) async throws -> GRPCCore.ServerResponse.Single + func sayHello( + request: GRPCCore.ServerRequest.Single, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Single } /// Partial conformance to `GreeterStreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Greeter.ServiceProtocol { - package func sayHello(request: GRPCCore.ServerRequest.Stream) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.sayHello(request: GRPCCore.ServerRequest.Single(stream: request)) + package func sayHello( + request: GRPCCore.ServerRequest.Stream, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse.Stream { + let response = try await self.sayHello( + request: GRPCCore.ServerRequest.Single(stream: request), + context: context + ) return GRPCCore.ServerResponse.Stream(single: response) } } From 20269e0c4c8258c31b36025d71096df0e2721830 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 19 Sep 2024 17:01:35 +0100 Subject: [PATCH 470/580] Standardise some naming (#2063) Motivation: In v2 we've predominantly used 'max' instead of 'maximum' and 'config' instead of 'configuration'. We should apply these consistently. Modifications: - Replaces occurrences of 'maximum' with 'max', and 'configuration' with 'config'. Result: More consistent naming --- .../GRPCCodeGen/CodeGenerationRequest.swift | 4 +- .../Translator/ClientCodeTranslator.swift | 4 +- .../IDLToStructuredSwiftTranslator.swift | 6 +- .../Translator/ServerCodeTranslator.swift | 4 +- .../Translator/SpecializedTranslator.swift | 4 +- .../Internal/Translator/Translator.swift | 2 +- .../Translator/TypealiasTranslator.swift | 4 +- Sources/GRPCCodeGen/SourceGenerator.swift | 18 +++--- .../ClientRPCExecutor+HedgingExecutor.swift | 8 +-- .../ClientRPCExecutor+RetryExecutor.swift | 6 +- .../Client/Internal/ClientRPCExecutor.swift | 2 +- .../Internal/ClientStreamExecutor.swift | 7 ++- .../Client/Internal/RetryDelaySequence.swift | 8 +-- .../GRPCCore/Configuration/MethodConfig.swift | 56 +++++++++---------- Sources/GRPCCore/GRPCClient.swift | 2 +- Sources/GRPCCore/GRPCServer.swift | 6 +- Sources/GRPCCore/Internal/MethodConfigs.swift | 20 +++---- Sources/GRPCCore/Timeout.swift | 2 +- .../GRPCCore/Transport/ClientTransport.swift | 2 +- .../GRPCCore/Transport/RetryThrottle.swift | 26 ++++----- .../Client/Connection/Connection.swift | 2 +- .../Client/Connection/GRPCChannel.swift | 6 +- .../Client/GRPCClientStreamHandler.swift | 4 +- .../GRPCHTTP2Core/GRPCMessageDecoder.swift | 18 +++--- Sources/GRPCHTTP2Core/GRPCMessageFramer.swift | 6 +- .../GRPCStreamStateMachine.swift | 24 ++++---- .../Internal/NIOChannelPipeline+GRPC.swift | 2 +- .../Server/GRPCServerStreamHandler.swift | 4 +- .../HTTP2ClientTransport+Posix.swift | 12 ++-- .../HTTP2ServerTransport+Posix.swift | 4 +- ...TP2ClientTransport+TransportServices.swift | 12 ++-- .../InProcessClientTransport.swift | 6 +- .../ProtobufCodeGenParser.swift | 4 +- .../ProtobufCodeGenerator.swift | 6 +- .../protoc-gen-grpc-swift/GenerateGRPC.swift | 6 +- ...lientCodeTranslatorSnippetBasedTests.swift | 2 +- ...uredSwiftTranslatorSnippetBasedTests.swift | 2 +- ...erverCodeTranslatorSnippetBasedTests.swift | 2 +- ...TypealiasTranslatorSnippetBasedTests.swift | 2 +- ...ientRPCExecutorTestHarness+Transport.swift | 2 +- .../ClientRPCExecutorTests+Hedging.swift | 8 +-- .../ClientRPCExecutorTests+Retries.swift | 12 ++-- .../Internal/ClientRPCExecutorTests.swift | 6 +- .../Call/Client/RetryDelaySequenceTests.swift | 8 +-- .../MethodConfigCodingTests.swift | 18 +++--- .../Configuration/MethodConfigTests.swift | 18 +++--- .../ServiceConfigCodingTests.swift | 2 +- .../Internal/MethodConfigsTests.swift | 24 ++++---- .../Transport/AnyTransport.swift | 4 +- .../Transport/StreamCountingTransport.swift | 4 +- .../Transport/ThrowingTransport.swift | 4 +- .../Transport/RetryThrottleTests.swift | 14 ++--- .../Client/Connection/GRPCChannelTests.swift | 30 +++++----- .../Connection/Utilities/ConnectionTest.swift | 2 +- .../Connection/Utilities/TestServer.swift | 2 +- .../Client/GRPCClientStreamHandlerTests.swift | 30 +++++----- .../GRPCMessageDecoderTests.swift | 14 ++--- .../GRPCStreamStateMachineTests.swift | 4 +- .../Server/GRPCServerStreamHandlerTests.swift | 38 ++++++------- .../InProcessClientTransportTests.swift | 16 +++--- .../ProtobufCodeGeneratorTests.swift | 4 +- 61 files changed, 291 insertions(+), 288 deletions(-) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 2175e88bf..700f33866 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -87,7 +87,7 @@ public struct CodeGenerationRequest { public var item: Item? /// The access level to be included in imports of this dependency. - public var accessLevel: SourceGenerator.Configuration.AccessLevel + public var accessLevel: SourceGenerator.Config.AccessLevel /// The name of the imported module or of the module an item is imported from. public var module: String @@ -106,7 +106,7 @@ public struct CodeGenerationRequest { module: String, spi: String? = nil, preconcurrency: PreconcurrencyRequirement = .notRequired, - accessLevel: SourceGenerator.Configuration.AccessLevel + accessLevel: SourceGenerator.Config.AccessLevel ) { self.item = item self.module = module diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index b8f123e18..5ead61a67 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -76,9 +76,9 @@ /// } ///``` struct ClientCodeTranslator: SpecializedTranslator { - var accessLevel: SourceGenerator.Configuration.AccessLevel + var accessLevel: SourceGenerator.Config.AccessLevel - init(accessLevel: SourceGenerator.Configuration.AccessLevel) { + init(accessLevel: SourceGenerator.Config.AccessLevel) { self.accessLevel = accessLevel } diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 4e49e2828..e0899291f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -20,7 +20,7 @@ struct IDLToStructuredSwiftTranslator: Translator { func translate( codeGenerationRequest: CodeGenerationRequest, - accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevel: SourceGenerator.Config.AccessLevel, accessLevelOnImports: Bool, client: Bool, server: Bool @@ -63,7 +63,7 @@ struct IDLToStructuredSwiftTranslator: Translator { private func makeImports( dependencies: [CodeGenerationRequest.Dependency], - accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevel: SourceGenerator.Config.AccessLevel, accessLevelOnImports: Bool ) throws -> [ImportDescription] { var imports: [ImportDescription] = [] @@ -87,7 +87,7 @@ struct IDLToStructuredSwiftTranslator: Translator { } extension AccessModifier { - fileprivate init(_ accessLevel: SourceGenerator.Configuration.AccessLevel) { + fileprivate init(_ accessLevel: SourceGenerator.Config.AccessLevel) { switch accessLevel.level { case .internal: self = .internal case .package: self = .package diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index f9f88dd95..c264eea30 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -58,9 +58,9 @@ /// } ///``` struct ServerCodeTranslator: SpecializedTranslator { - var accessLevel: SourceGenerator.Configuration.AccessLevel + var accessLevel: SourceGenerator.Config.AccessLevel - init(accessLevel: SourceGenerator.Configuration.AccessLevel) { + init(accessLevel: SourceGenerator.Config.AccessLevel) { self.accessLevel = accessLevel } diff --git a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift index eb74b30e7..1db3fce02 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/SpecializedTranslator.swift @@ -18,8 +18,8 @@ /// the server code translation or the client code translation. protocol SpecializedTranslator { - /// The ``SourceGenerator.Configuration.AccessLevel`` object used to represent the visibility level used in the generated code. - var accessLevel: SourceGenerator.Configuration.AccessLevel { get } + /// The ``SourceGenerator.Config.AccessLevel`` object used to represent the visibility level used in the generated code. + var accessLevel: SourceGenerator.Config.AccessLevel { get } /// Generates an array of ``CodeBlock`` elements that will be part of the ``StructuredSwiftRepresentation`` object /// created by the ``Translator``. diff --git a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift index d8ad3210e..36b1c665f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Translator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Translator.swift @@ -29,7 +29,7 @@ protocol Translator { /// - Throws: An error if there are issues translating the codeGenerationRequest. func translate( codeGenerationRequest: CodeGenerationRequest, - accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevel: SourceGenerator.Config.AccessLevel, accessLevelOnImports: Bool, client: Bool, server: Bool diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 7e5c7b559..a6bcc55ae 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -71,9 +71,9 @@ struct TypealiasTranslator: SpecializedTranslator { let client: Bool let server: Bool - let accessLevel: SourceGenerator.Configuration.AccessLevel + let accessLevel: SourceGenerator.Config.AccessLevel - init(client: Bool, server: Bool, accessLevel: SourceGenerator.Configuration.AccessLevel) { + init(client: Bool, server: Bool, accessLevel: SourceGenerator.Config.AccessLevel) { self.client = client self.server = server self.accessLevel = accessLevel diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift index 3130e15be..454258bc6 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -18,14 +18,14 @@ public struct SourceGenerator: Sendable { /// The options regarding the access level, indentation for the generated code /// and whether to generate server and client code. - public var configuration: Configuration + public var config: Config - public init(configuration: Configuration) { - self.configuration = configuration + public init(config: Config) { + self.config = config } /// User options for the CodeGeneration. - public struct Configuration: Sendable { + public struct Config: Sendable { /// The access level the generated code will have. public var accessLevel: AccessLevel /// Whether imports have explicit access levels. @@ -85,14 +85,14 @@ public struct SourceGenerator: Sendable { _ request: CodeGenerationRequest ) throws -> SourceFile { let translator = IDLToStructuredSwiftTranslator() - let textRenderer = TextBasedRenderer(indentation: self.configuration.indentation) + let textRenderer = TextBasedRenderer(indentation: self.config.indentation) let structuredSwiftRepresentation = try translator.translate( codeGenerationRequest: request, - accessLevel: self.configuration.accessLevel, - accessLevelOnImports: self.configuration.accessLevelOnImports, - client: self.configuration.client, - server: self.configuration.server + accessLevel: self.config.accessLevel, + accessLevelOnImports: self.config.accessLevelOnImports, + client: self.config.client, + server: self.config.server ) let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index 36364d22c..bf57dae23 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -448,7 +448,7 @@ extension ClientRPCExecutor.HedgingExecutor { @usableFromInline struct State: Sendable { @usableFromInline - let _maximumAttempts: Int + let _maxAttempts: Int @usableFromInline private(set) var attempt: Int @usableFromInline @@ -456,7 +456,7 @@ extension ClientRPCExecutor.HedgingExecutor { @inlinable init(policy: HedgingPolicy) { - self._maximumAttempts = policy.maximumAttempts + self._maxAttempts = policy.maxAttempts self.attempt = 1 self.hasUsableResponse = false } @@ -487,14 +487,14 @@ extension ClientRPCExecutor.HedgingExecutor { @inlinable mutating func nextAttemptNumber() -> NextAttemptResult? { - if self.hasUsableResponse || self.attempt > self._maximumAttempts { + if self.hasUsableResponse || self.attempt > self._maxAttempts { return nil } else { let attempt = self.attempt self.attempt += 1 return NextAttemptResult( nextAttempt: attempt, - scheduleNext: self.attempt <= self._maximumAttempts + scheduleNext: self.attempt <= self._maxAttempts ) } } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 2cce865e1..d808b1f4a 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -115,7 +115,7 @@ extension ClientRPCExecutor.RetryExecutor { let delaySequence = RetryDelaySequence(policy: self.policy) var delayIterator = delaySequence.makeIterator() - for attempt in 1 ... self.policy.maximumAttempts { + for attempt in 1 ... self.policy.maxAttempts { do { let attemptResult = try await self.transport.withStream( descriptor: method, @@ -253,7 +253,7 @@ extension ClientRPCExecutor.RetryExecutor { switch error.metadata.retryPushback { case .retryAfter(let delay): // Pushback: only retry if our config permits it. - shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + shouldRetry = (attempt < self.policy.maxAttempts) && !throttled retryDelayOverride = delay case .stopRetrying: // Server told us to stop trying. @@ -261,7 +261,7 @@ extension ClientRPCExecutor.RetryExecutor { retryDelayOverride = nil case .none: // No pushback: only retry if our config permits it. - shouldRetry = (attempt < self.policy.maximumAttempts) && !throttled + shouldRetry = (attempt < self.policy.maxAttempts) && !throttled retryDelayOverride = nil break } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 4a7c958c4..33e46fd2d 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -22,7 +22,7 @@ enum ClientRPCExecutor { /// - Parameters: /// - request: The request to execute. /// - method: A description of the method to execute the request against. - /// - configuration: The execution configuration. + /// - options: RPC options. /// - serializer: A serializer to convert input messages to bytes. /// - deserializer: A deserializer to convert bytes to output messages. /// - transport: The transport to execute the request on. diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 433232d7c..8b635bca8 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -19,11 +19,14 @@ internal enum ClientStreamExecutor { /// Execute a request on the stream executor. /// - /// The ``run()`` method must be running at the same time as this method. - /// /// - Parameters: /// - request: A streaming request. /// - method: A description of the method to call. + /// - context: The client context. + /// - attempt: The attempt number for the RPC that will be executed. + /// - serializer: A request serializer. + /// - deserializer: A response deserializer. + /// - stream: The stream to excecute the RPC on. /// - Returns: A streamed response. @inlinable static func execute( diff --git a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift index d22c6fd52..d1ef417f3 100644 --- a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -56,8 +56,8 @@ struct RetryDelaySequence: Sequence { } @inlinable - var _maximumBackoffSeconds: Double { - Self._durationToTimeInterval(self.policy.maximumBackoff) + var _maxBackoffSeconds: Double { + Self._durationToTimeInterval(self.policy.maxBackoff) } @inlinable @@ -65,10 +65,10 @@ struct RetryDelaySequence: Sequence { defer { self.n += 1 } /// The nth retry will happen after a randomly chosen delay between zero and - /// `min(initialBackoff * backoffMultiplier^(n-1), maximumBackoff)`. + /// `min(initialBackoff * backoffMultiplier^(n-1), maxBackoff)`. let factor = pow(self.policy.backoffMultiplier, Double(self.n - 1)) let computedBackoff = self._initialBackoffSeconds * factor - let clampedBackoff = Swift.min(computedBackoff, self._maximumBackoffSeconds) + let clampedBackoff = Swift.min(computedBackoff, self._maxBackoffSeconds) let randomisedBackoff = Double.random(in: 0.0 ... clampedBackoff) return Self._timeIntervalToDuration(randomisedBackoff) diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index d99c892df..295d049a3 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -204,13 +204,13 @@ public struct RPCExecutionPolicy: Hashable, Sendable { /// first responds with metadata and later responds with a retryable status code then the RPC /// won't be retried. /// -/// Execution attempts are limited by ``maximumAttempts`` which includes the original attempt. The +/// Execution attempts are limited by ``maxAttempts`` which includes the original attempt. The /// maximum number of attempts is limited to five. /// /// Subsequent attempts are executed after some delay. The first _retry_, or second attempt, will /// be started after a randomly chosen delay between zero and ``initialBackoff``. More generally, /// the nth retry will happen after a randomly chosen delay between zero -/// and `min(initialBackoff * backoffMultiplier^(n-1), maximumBackoff)`. +/// and `min(initialBackoff * backoffMultiplier^(n-1), maxBackoff)`. /// /// For more information see [gRFC A6 Client /// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). @@ -219,8 +219,8 @@ public struct RetryPolicy: Hashable, Sendable { /// The maximum number of RPC attempts, including the original attempt. /// /// Must be greater than one, values greater than five are treated as five. - public var maximumAttempts: Int { - didSet { self.maximumAttempts = try! validateMaxAttempts(self.maximumAttempts) } + public var maxAttempts: Int { + didSet { self.maxAttempts = try! validateMaxAttempts(self.maxAttempts) } } /// The initial backoff duration. @@ -235,7 +235,7 @@ public struct RetryPolicy: Hashable, Sendable { /// The maximum amount of time to backoff for. /// /// - Precondition: Must be greater than zero. - public var maximumBackoff: Duration { + public var maxBackoff: Duration { willSet { try! Self.validateMaxBackoff(newValue) } } @@ -256,30 +256,30 @@ public struct RetryPolicy: Hashable, Sendable { /// Create a new retry policy. /// /// - Parameters: - /// - maximumAttempts: The maximum number of attempts allowed for the RPC. + /// - maxAttempts: The maximum number of attempts allowed for the RPC. /// - initialBackoff: The initial backoff period for the first retry attempt. Must be /// greater than zero. - /// - maximumBackoff: The maximum period of time to wait between attempts. Must be greater than + /// - maxBackoff: The maximum period of time to wait between attempts. Must be greater than /// zero. /// - backoffMultiplier: The exponential backoff multiplier. Must be greater than zero. /// - retryableStatusCodes: The set of status codes which may be retried. Must not be empty. - /// - Precondition: `maximumAttempts`, `initialBackoff`, `maximumBackoff` and `backoffMultiplier` + /// - Precondition: `maxAttempts`, `initialBackoff`, `maxBackoff` and `backoffMultiplier` /// must be greater than zero. /// - Precondition: `retryableStatusCodes` must not be empty. public init( - maximumAttempts: Int, + maxAttempts: Int, initialBackoff: Duration, - maximumBackoff: Duration, + maxBackoff: Duration, backoffMultiplier: Double, retryableStatusCodes: Set ) { - self.maximumAttempts = try! validateMaxAttempts(maximumAttempts) + self.maxAttempts = try! validateMaxAttempts(maxAttempts) try! Self.validateInitialBackoff(initialBackoff) self.initialBackoff = initialBackoff - try! Self.validateMaxBackoff(maximumBackoff) - self.maximumBackoff = maximumBackoff + try! Self.validateMaxBackoff(maxBackoff) + self.maxBackoff = maxBackoff try! Self.validateBackoffMultiplier(backoffMultiplier) self.backoffMultiplier = backoffMultiplier @@ -301,7 +301,7 @@ public struct RetryPolicy: Hashable, Sendable { if value <= .zero { throw RuntimeError( code: .invalidArgument, - message: "maximumBackoff must be greater than zero" + message: "maxBackoff must be greater than zero" ) } } @@ -327,7 +327,7 @@ public struct RetryPolicy: Hashable, Sendable { /// Hedged RPCs may execute more than once on a server so only idempotent methods should /// be hedged. /// -/// gRPC executes the RPC at most ``maximumAttempts`` times, staggering each attempt +/// gRPC executes the RPC at most ``maxAttempts`` times, staggering each attempt /// by ``hedgingDelay``. /// /// For more information see [gRFC A6 Client @@ -339,8 +339,8 @@ public struct HedgingPolicy: Hashable, Sendable { /// Values greater than five are treated as five. /// /// - Precondition: Must be greater than one. - public var maximumAttempts: Int { - didSet { self.maximumAttempts = try! validateMaxAttempts(self.maximumAttempts) } + public var maxAttempts: Int { + didSet { self.maxAttempts = try! validateMaxAttempts(self.maxAttempts) } } /// The first RPC will be sent immediately, but each subsequent RPC will be sent at intervals @@ -359,17 +359,17 @@ public struct HedgingPolicy: Hashable, Sendable { /// Create a new hedging policy. /// /// - Parameters: - /// - maximumAttempts: The maximum number of attempts allowed for the RPC. + /// - maxAttempts: The maximum number of attempts allowed for the RPC. /// - hedgingDelay: The delay between each hedged RPC. /// - nonFatalStatusCodes: The set of status codes which indicate other hedged RPCs may still /// succeed. - /// - Precondition: `maximumAttempts` must be greater than zero. + /// - Precondition: `maxAttempts` must be greater than zero. public init( - maximumAttempts: Int, + maxAttempts: Int, hedgingDelay: Duration, nonFatalStatusCodes: Set ) { - self.maximumAttempts = try! validateMaxAttempts(maximumAttempts) + self.maxAttempts = try! validateMaxAttempts(maxAttempts) try! Self.validateHedgingDelay(hedgingDelay) self.hedgingDelay = hedgingDelay @@ -512,15 +512,15 @@ extension RetryPolicy: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) let maxAttempts = try container.decode(Int.self, forKey: .maxAttempts) - self.maximumAttempts = try validateMaxAttempts(maxAttempts) + self.maxAttempts = try validateMaxAttempts(maxAttempts) let initialBackoff = try container.decode(String.self, forKey: .initialBackoff) self.initialBackoff = try Duration(googleProtobufDuration: initialBackoff) try Self.validateInitialBackoff(self.initialBackoff) let maxBackoff = try container.decode(String.self, forKey: .maxBackoff) - self.maximumBackoff = try Duration(googleProtobufDuration: maxBackoff) - try Self.validateMaxBackoff(self.maximumBackoff) + self.maxBackoff = try Duration(googleProtobufDuration: maxBackoff) + try Self.validateMaxBackoff(self.maxBackoff) self.backoffMultiplier = try container.decode(Double.self, forKey: .backoffMultiplier) try Self.validateBackoffMultiplier(self.backoffMultiplier) @@ -532,12 +532,12 @@ extension RetryPolicy: Codable { public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.maximumAttempts, forKey: .maxAttempts) + try container.encode(self.maxAttempts, forKey: .maxAttempts) try container.encode( GoogleProtobufDuration(duration: self.initialBackoff), forKey: .initialBackoff ) - try container.encode(GoogleProtobufDuration(duration: self.maximumBackoff), forKey: .maxBackoff) + try container.encode(GoogleProtobufDuration(duration: self.maxBackoff), forKey: .maxBackoff) try container.encode(self.backoffMultiplier, forKey: .backoffMultiplier) try container.encode( self.retryableStatusCodes.map { $0.googleRPCCode }, @@ -558,7 +558,7 @@ extension HedgingPolicy: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) let maxAttempts = try container.decode(Int.self, forKey: .maxAttempts) - self.maximumAttempts = try validateMaxAttempts(maxAttempts) + self.maxAttempts = try validateMaxAttempts(maxAttempts) let delay = try container.decode(String.self, forKey: .hedgingDelay) self.hedgingDelay = try Duration(googleProtobufDuration: delay) @@ -569,7 +569,7 @@ extension HedgingPolicy: Codable { public func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(self.maximumAttempts, forKey: .maxAttempts) + try container.encode(self.maxAttempts, forKey: .maxAttempts) try container.encode(GoogleProtobufDuration(duration: self.hedgingDelay), forKey: .hedgingDelay) try container.encode( self.nonFatalStatusCodes.map { $0.googleRPCCode }, diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index c11d32caf..6c2729548 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -359,7 +359,7 @@ public final class GRPCClient: Sendable { handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue ) async throws -> ReturnValue { try self.state.withLock { try $0.checkExecutable() } - let methodConfig = self.transport.configuration(forMethod: descriptor) + let methodConfig = self.transport.config(forMethod: descriptor) var options = options options.formUnion(with: methodConfig) diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index da46e2db8..b3d99b7de 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -54,7 +54,7 @@ private import Synchronization /// /// ## Starting and stopping the server /// -/// Once you have configured the server call ``run()`` to start it. Calling ``run()`` starts the server's +/// Once you have configured the server call ``serve()`` to start it. Calling ``serve()`` starts the server's /// transport too. A ``RuntimeError`` is thrown if the transport can't be started or encounters some other /// runtime error. /// @@ -63,7 +63,7 @@ private import Synchronization /// try await server.serve() /// ``` /// -/// The ``run()`` method won't return until the server has finished handling all requests. You can +/// The ``serve()`` method won't return until the server has finished handling all requests. You can /// signal to the server that it should stop accepting new requests by calling ``beginGracefulShutdown()``. /// This allows the server to drain existing requests gracefully. To stop the server more abruptly /// you can cancel the task running your server. If your application requires additional resources @@ -221,7 +221,7 @@ public final class GRPCServer: Sendable { /// Signal to the server that it should stop listening for new requests. /// /// By calling this function you indicate to clients that they mustn't start new requests - /// against this server. Once the server has processed all requests the ``run()`` method returns. + /// against this server. Once the server has processed all requests the ``serve()`` method returns. /// /// Calling this on a server which is already stopping or has stopped has no effect. public func beginGracefulShutdown() { diff --git a/Sources/GRPCCore/Internal/MethodConfigs.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift index 46ae4ddce..1992b59a8 100644 --- a/Sources/GRPCCore/Internal/MethodConfigs.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -18,8 +18,8 @@ /// /// When creating a new instance, no overrides and no default will be set for using when getting /// a configuration for a method that has not been given a specific override. -/// Use ``setDefaultConfiguration(_:forService:)`` to set a specific override for a whole -/// service, or set a default configuration for all methods by calling ``setDefaultConfiguration(_:)``. +/// Use ``setDefaultConfig(_:forService:)`` to set a specific override for a whole +/// service, or set a default configuration for all methods by calling ``setDefaultConfig(_:)``. /// /// Use the subscript to get and set configurations for specific methods. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -42,7 +42,7 @@ package struct MethodConfigs: Sendable, Hashable { /// Get or set the corresponding ``MethodConfig`` for the given ``MethodDescriptor``. /// /// Configuration is hierarchical and can be set per-method, per-service - /// (``setDefaultConfiguration(_:forService:)``) and globally (``setDefaultConfiguration(_:)``). + /// (``setDefaultConfig(_:forService:)``) and globally (``setDefaultConfig(_:)``). /// This subscript sets the per-method configuration but retrieves a configuration respecting /// the hierarchy. If no per-method configuration is present, the per-service configuration is /// checked and returned if present. If the per-service configuration isn't present then the @@ -78,10 +78,10 @@ package struct MethodConfigs: Sendable, Hashable { /// Set a default configuration for all methods that have no overrides. /// - /// - Parameter configuration: The default configuration. - package mutating func setDefaultConfiguration(_ configuration: MethodConfig?) { + /// - Parameter config: The default configuration. + package mutating func setDefaultConfig(_ config: MethodConfig?) { let name = MethodConfig.Name(service: "", method: "") - self.elements[name] = configuration + self.elements[name] = config } /// Set a default configuration for a service. @@ -91,13 +91,13 @@ package struct MethodConfigs: Sendable, Hashable { /// this instance of ``MethodConfigs``. /// /// - Parameters: - /// - configuration: The default configuration for the service. + /// - config: The default configuration for the service. /// - service: The name of the service for which this override applies. - package mutating func setDefaultConfiguration( - _ configuration: MethodConfig?, + package mutating func setDefaultConfig( + _ config: MethodConfig?, forService service: String ) { let name = MethodConfig.Name(service: "", method: "") - self.elements[name] = configuration + self.elements[name] = config } } diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index e4e174e98..4640a5cca 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -77,7 +77,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { /// - Important: It's not possible to know with what precision the duration was created: that is, /// it's not possible to know whether `Duration.seconds(value)` or `Duration.milliseconds(value)` /// was used. For this reason, the unit chosen for the ``Timeout`` (and thus the wire encoding) may be - /// different from the one originally used to create the ``Duration``. Despite this, we guarantee that + /// different from the one originally used to create the `Duration`. Despite this, we guarantee that /// both durations will be equivalent if there was no loss in precision during the transformation. /// For example, `Duration.hours(123)` will yield a ``Timeout`` with `wireEncoding` equal to /// `"442800S"`, which is in seconds. However, 442800 seconds and 123 hours are equivalent. diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 89d61464a..800aa5b64 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -72,5 +72,5 @@ public protocol ClientTransport: Sendable { /// /// - Parameter descriptor: The method to lookup configuration for. /// - Returns: Configuration for the method, if it exists. - func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? + func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? } diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index 5d6b2cf93..70c934dfc 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -41,7 +41,7 @@ public final class RetryThrottle: Sendable { /// The number of tokens, multiplied by 1000. private let scaledTokenRatio: Int /// The maximum number of tokens, multiplied by 1000. - private let scaledMaximumTokens: Int + private let scaledMaxTokens: Int /// The retry threshold, multiplied by 1000. If ``scaledTokensAvailable`` is above this then /// retries are permitted. private let scaledRetryThreshold: Int @@ -60,13 +60,13 @@ public final class RetryThrottle: Sendable { } /// The maximum number of tokens the throttle may hold. - public var maximumTokens: Int { - self.scaledMaximumTokens / 1000 + public var maxTokens: Int { + self.scaledMaxTokens / 1000 } /// The number of tokens the throttle currently has. /// - /// If this value is less than or equal to the retry threshold (defined as `maximumTokens / 2`) + /// If this value is less than or equal to the retry threshold (defined as `maxTokens / 2`) /// then RPCs will not be retried and hedging will be disabled. public var tokens: Double { self.scaledTokensAvailable.withLock { @@ -84,23 +84,23 @@ public final class RetryThrottle: Sendable { /// Create a new throttle. /// /// - Parameters: - /// - maximumTokens: The maximum number of tokens available. Must be in the range `1...1000`. + /// - maxTokens: The maximum number of tokens available. Must be in the range `1...1000`. /// - tokenRatio: The number of tokens to increment the available tokens by for successful /// responses. See the documentation on this type for a description of what counts as a /// successful response. Note that only three decimal places are used from this value. - /// - Precondition: `maximumTokens` must be in the range `1...1000`. + /// - Precondition: `maxTokens` must be in the range `1...1000`. /// - Precondition: `tokenRatio` must be `>= 0.001`. - public init(maximumTokens: Int, tokenRatio: Double) { + public init(maxTokens: Int, tokenRatio: Double) { precondition( - (1 ... 1000).contains(maximumTokens), - "maximumTokens must be in the range 1...1000 (is \(maximumTokens))" + (1 ... 1000).contains(maxTokens), + "maxTokens must be in the range 1...1000 (is \(maxTokens))" ) let scaledTokenRatio = Int(tokenRatio * 1000) precondition(scaledTokenRatio > 0, "tokenRatio must be >= 0.001 (is \(tokenRatio))") - let scaledTokens = maximumTokens * 1000 - self.scaledMaximumTokens = scaledTokens + let scaledTokens = maxTokens * 1000 + self.scaledMaxTokens = scaledTokens self.scaledRetryThreshold = scaledTokens / 2 self.scaledTokenRatio = scaledTokenRatio self.scaledTokensAvailable = Mutex(scaledTokens) @@ -110,14 +110,14 @@ public final class RetryThrottle: Sendable { /// /// - Parameter policy: The policy to use to configure the throttle. public convenience init(policy: ServiceConfig.RetryThrottling) { - self.init(maximumTokens: policy.maxTokens, tokenRatio: policy.tokenRatio) + self.init(maxTokens: policy.maxTokens, tokenRatio: policy.tokenRatio) } /// Records a success, adding a token to the throttle. @usableFromInline func recordSuccess() { self.scaledTokensAvailable.withLock { value in - value = min(self.scaledMaximumTokens, value &+ self.scaledTokenRatio) + value = min(self.scaledMaxTokens, value &+ self.scaledTokenRatio) } } diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift index 2dbcf8996..a90d2a9e8 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift @@ -206,7 +206,7 @@ package final class Connection: Sendable { scheme: scheme, outboundEncoding: compression, acceptedEncodings: self.enabledCompression, - maximumPayloadSize: maxRequestSize + maxPayloadSize: maxRequestSize ) try channel.pipeline.syncOperations.addHandler(streamHandler) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift index c98e94914..7be28da30 100644 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift @@ -114,7 +114,7 @@ package final class GRPCChannel: ClientTransport { /// /// - Parameter descriptor: The method to lookup configuration for. /// - Returns: Configuration for the method, if it exists. - package func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { + package func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? { self._methodConfig.withLock { $0[descriptor] } } @@ -193,7 +193,7 @@ package final class GRPCChannel: ClientTransport { _ closure: (_ stream: RPCStream) async throws -> T ) async throws -> T { // Merge options from the call with those from the service config. - let methodConfig = self.configuration(forMethod: descriptor) + let methodConfig = self.config(forMethod: descriptor) var options = options options.formUnion(with: methodConfig) @@ -306,7 +306,7 @@ extension GRPCChannel { return .tryAgain(RPCError(code: .unavailable, message: "channel isn't ready")) } - let methodConfig = self.configuration(forMethod: descriptor) + let methodConfig = self.config(forMethod: descriptor) var options = options options.formUnion(with: methodConfig) diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index 3640af268..e4562e8de 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift @@ -36,7 +36,7 @@ final class GRPCClientStreamHandler: ChannelDuplexHandler { scheme: Scheme, outboundEncoding: CompressionAlgorithm, acceptedEncodings: CompressionAlgorithmSet, - maximumPayloadSize: Int, + maxPayloadSize: Int, skipStateMachineAssertions: Bool = false ) { self.stateMachine = .init( @@ -48,7 +48,7 @@ final class GRPCClientStreamHandler: ChannelDuplexHandler { acceptedEncodings: acceptedEncodings ) ), - maximumPayloadSize: maximumPayloadSize, + maxPayloadSize: maxPayloadSize, skipAssertions: skipStateMachineAssertions ) } diff --git a/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift b/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift index e8e4f1c8f..9d557a6bf 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift @@ -28,19 +28,19 @@ struct GRPCMessageDecoder: NIOSingleStepByteToMessageDecoder { typealias InboundOut = [UInt8] private let decompressor: Zlib.Decompressor? - private let maximumPayloadSize: Int + private let maxPayloadSize: Int /// Create a new ``GRPCMessageDeframer``. /// - Parameters: - /// - maximumPayloadSize: The maximum size a message payload can be. + /// - maxPayloadSize: The maximum size a message payload can be. /// - decompressor: A `Zlib.Decompressor` to use when decompressing compressed gRPC messages. /// - Important: You must call `end()` on the `decompressor` when you're done using it, to clean /// up any resources allocated by `Zlib`. init( - maximumPayloadSize: Int, + maxPayloadSize: Int, decompressor: Zlib.Decompressor? = nil ) { - self.maximumPayloadSize = maximumPayloadSize + self.maxPayloadSize = maxPayloadSize self.decompressor = decompressor } @@ -60,12 +60,12 @@ struct GRPCMessageDecoder: NIOSingleStepByteToMessageDecoder { let isMessageCompressed = buffer.readInteger(as: UInt8.self)! == 1 let messageLength = buffer.readInteger(as: UInt32.self)! - if messageLength > self.maximumPayloadSize { + if messageLength > self.maxPayloadSize { throw RPCError( code: .resourceExhausted, message: """ Message has exceeded the configured maximum payload size \ - (max: \(self.maximumPayloadSize), actual: \(messageLength)) + (max: \(self.maxPayloadSize), actual: \(messageLength)) """ ) } @@ -90,7 +90,7 @@ struct GRPCMessageDecoder: NIOSingleStepByteToMessageDecoder { message: "Received a compressed message payload, but no decompressor has been configured." ) } - return try decompressor.decompress(&message, limit: self.maximumPayloadSize) + return try decompressor.decompress(&message, limit: self.maxPayloadSize) } else { return Array(buffer: message) } @@ -111,14 +111,14 @@ package struct GRPCMessageDeframer { init(maxPayloadSize: Int, decompressor: Zlib.Decompressor?) { self.decoder = GRPCMessageDecoder( - maximumPayloadSize: maxPayloadSize, + maxPayloadSize: maxPayloadSize, decompressor: decompressor ) self.buffer = nil } package init(maxPayloadSize: Int) { - self.decoder = GRPCMessageDecoder(maximumPayloadSize: maxPayloadSize, decompressor: nil) + self.decoder = GRPCMessageDecoder(maxPayloadSize: maxPayloadSize, decompressor: nil) self.buffer = nil } diff --git a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift index 059c3e161..509b7ea35 100644 --- a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift +++ b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift @@ -31,7 +31,7 @@ struct GRPCMessageFramer { /// frames with messages over 64KB can still be written. /// - Note: This is expressed as the power of 2 closer to 64KB (i.e., 64KiB) because `ByteBuffer` /// reserves capacity in powers of 2. This way, we can take advantage of the whole buffer. - static let maximumWriteBufferLength = 65_536 + static let maxWriteBufferLength = 65_536 private var pendingMessages: OneOrManyQueue<(bytes: [UInt8], promise: EventLoopPromise?)> @@ -64,8 +64,8 @@ struct GRPCMessageFramer { defer { // To avoid holding an excessively large buffer, if its size is larger than - // our threshold (`maximumWriteBufferLength`), then reset it to a new `ByteBuffer`. - if self.writeBuffer.capacity > Self.maximumWriteBufferLength { + // our threshold (`maxWriteBufferLength`), then reset it to a new `ByteBuffer`. + if self.writeBuffer.capacity > Self.maxWriteBufferLength { self.writeBuffer = ByteBuffer() } } diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index be43c0240..a040a4524 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift @@ -69,11 +69,11 @@ private enum GRPCStreamStateMachineState { case _modifying struct ClientIdleServerIdleState { - let maximumPayloadSize: Int + let maxPayloadSize: Int } struct ClientOpenServerIdleState { - let maximumPayloadSize: Int + let maxPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm @@ -100,7 +100,7 @@ private enum GRPCStreamStateMachineState { deframer: GRPCMessageDeframer?, headers: HPACKHeaders ) { - self.maximumPayloadSize = previousState.maximumPayloadSize + self.maxPayloadSize = previousState.maxPayloadSize self.compressor = compressor self.outboundCompression = outboundCompression self.framer = framer @@ -194,7 +194,7 @@ private enum GRPCStreamStateMachineState { } struct ClientClosedServerIdleState { - let maximumPayloadSize: Int + let maxPayloadSize: Int var framer: GRPCMessageFramer var compressor: Zlib.Compressor? var outboundCompression: CompressionAlgorithm @@ -212,7 +212,7 @@ private enum GRPCStreamStateMachineState { /// It can happen if the request times out before the client outbound can be opened, or if the stream is /// unexpectedly closed for some other reason on the client before it can transition to open. init(previousState: ClientIdleServerIdleState) { - self.maximumPayloadSize = previousState.maximumPayloadSize + self.maxPayloadSize = previousState.maxPayloadSize // We don't need a compressor since we won't be sending any messages. self.framer = GRPCMessageFramer() self.compressor = nil @@ -235,7 +235,7 @@ private enum GRPCStreamStateMachineState { compressionAlgorithm: CompressionAlgorithm, headers: HPACKHeaders ) { - self.maximumPayloadSize = previousState.maximumPayloadSize + self.maxPayloadSize = previousState.maxPayloadSize if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { self.compressor = Zlib.Compressor(method: zlibMethod) @@ -253,7 +253,7 @@ private enum GRPCStreamStateMachineState { } init(previousState: ClientOpenServerIdleState) { - self.maximumPayloadSize = previousState.maximumPayloadSize + self.maxPayloadSize = previousState.maxPayloadSize self.framer = previousState.framer self.compressor = previousState.compressor self.outboundCompression = previousState.outboundCompression @@ -319,7 +319,7 @@ private enum GRPCStreamStateMachineState { } self.deframer = GRPCMessageDeframer( - maxPayloadSize: previousState.maximumPayloadSize, + maxPayloadSize: previousState.maxPayloadSize, decompressor: self.decompressor ) @@ -405,10 +405,10 @@ struct GRPCStreamStateMachine { init( configuration: GRPCStreamStateMachineConfiguration, - maximumPayloadSize: Int, + maxPayloadSize: Int, skipAssertions: Bool = false ) { - self.state = .clientIdleServerIdle(.init(maximumPayloadSize: maximumPayloadSize)) + self.state = .clientIdleServerIdle(.init(maxPayloadSize: maxPayloadSize)) self.configuration = configuration self.skipAssertions = skipAssertions } @@ -981,7 +981,7 @@ extension GRPCStreamStateMachine { .init( previousState: state, deframer: GRPCMessageDeframer( - maxPayloadSize: state.maximumPayloadSize, + maxPayloadSize: state.maxPayloadSize, decompressor: decompressor ), decompressor: decompressor @@ -1539,7 +1539,7 @@ extension GRPCStreamStateMachine { framer: GRPCMessageFramer(), decompressor: decompressor, deframer: GRPCMessageDeframer( - maxPayloadSize: state.maximumPayloadSize, + maxPayloadSize: state.maxPayloadSize, decompressor: decompressor ), headers: headers diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift index 52a2fa03c..cade0f581 100644 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift @@ -85,7 +85,7 @@ extension ChannelPipeline.SynchronousOperations { let streamHandler = GRPCServerStreamHandler( scheme: scheme, acceptedEncodings: compressionConfig.enabledAlgorithms, - maximumPayloadSize: rpcConfig.maxRequestPayloadSize, + maxPayloadSize: rpcConfig.maxRequestPayloadSize, methodDescriptorPromise: methodDescriptorPromise ) try streamChannel.pipeline.syncOperations.addHandler(streamHandler) diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index df4c184eb..54965cf13 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift @@ -49,13 +49,13 @@ package final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChan package init( scheme: Scheme, acceptedEncodings: CompressionAlgorithmSet, - maximumPayloadSize: Int, + maxPayloadSize: Int, methodDescriptorPromise: EventLoopPromise, skipStateMachineAssertions: Bool = false ) { self.stateMachine = .init( configuration: .server(.init(scheme: scheme, acceptedEncodings: acceptedEncodings)), - maximumPayloadSize: maximumPayloadSize, + maxPayloadSize: maxPayloadSize, skipAssertions: skipStateMachineAssertions ) self.methodDescriptorPromise = methodDescriptorPromise diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift index f63adf96a..baf96639a 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift @@ -25,12 +25,12 @@ private import NIOSSL @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ClientTransport { - /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOPosix`. + /// A `ClientTransport` using HTTP/2 built on top of `NIOPosix`. /// /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use /// on Linux and Darwin based platforms (macOS, iOS, etc.). However, it's *strongly* recommended /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of - /// the ``GRPCHTTP2Core/HTTP2ClientTransport``. + /// the `HTTP2ClientTransport`. /// /// To use this transport you need to provide a 'target' to connect to which will be resolved /// by an appropriate resolver from the resolver registry. By default the resolver registry can @@ -40,7 +40,7 @@ extension HTTP2ClientTransport { /// /// You can control various aspects of connection creation, management, security and RPC behavior via /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via - /// the ``ServiceConfig`` (if it isn't provided by a resolver). + /// the `ServiceConfig` (if it isn't provided by a resolver). /// /// Beyond creating the transport you don't need to interact with it directly, instead, pass it /// to a `GRPCClient`: @@ -107,8 +107,8 @@ extension HTTP2ClientTransport { await self.channel.connect() } - public func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { - self.channel.configuration(forMethod: descriptor) + public func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? { + self.channel.config(forMethod: descriptor) } public func beginGracefulShutdown() { @@ -217,7 +217,7 @@ extension HTTP2ClientTransport.Posix { /// - compression: Compression configuration. /// - transportSecurity: The transport's security configuration. /// - /// - SeeAlso: ``defaults(_:)``. + /// - SeeAlso: ``defaults(transportSecurity:configure:)`` public init( http2: HTTP2ClientTransport.Config.HTTP2, backoff: HTTP2ClientTransport.Config.Backoff, diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift index 0aa14aefa..48420178c 100644 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift @@ -27,12 +27,12 @@ import NIOSSL #endif extension HTTP2ServerTransport { - /// A ``GRPCCore/ServerTransport`` using HTTP/2 built on top of `NIOPosix`. + /// A `ServerTransport` using HTTP/2 built on top of `NIOPosix`. /// /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of - /// the ``GRPCHTTP2Core/HTTP2ServerTransport``. + /// the `HTTP2ServerTransport`. /// /// You can control various aspects of connection creation, management, security and RPC behavior via /// the ``Config``. diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift index 3c10615d6..ee86e5eb5 100644 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift +++ b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift @@ -24,12 +24,12 @@ private import Network @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension HTTP2ClientTransport { - /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOTransportServices`. + /// A `ClientTransport` using HTTP/2 built on top of `NIOTransportServices`. /// /// This transport builds on top of SwiftNIO's Transport Services networking layer and is the recommended /// variant for use on Darwin-based platforms (macOS, iOS, etc.). /// If you are targeting Linux platforms then you should use the `NIOPosix` variant of - /// the ``GRPCHTTP2Core/HTTP2ClientTransport``. + /// the `HTTP2ClientTransport`. /// /// To use this transport you need to provide a 'target' to connect to which will be resolved /// by an appropriate resolver from the resolver registry. By default the resolver registry can @@ -39,7 +39,7 @@ extension HTTP2ClientTransport { /// /// You can control various aspects of connection creation, management, security and RPC behavior via /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via - /// the ``ServiceConfig`` (if it isn't provided by a resolver). + /// the `ServiceConfig` (if it isn't provided by a resolver). /// /// Beyond creating the transport you don't need to interact with it directly, instead, pass it /// to a `GRPCClient`: @@ -118,8 +118,8 @@ extension HTTP2ClientTransport { try await self.channel.withStream(descriptor: descriptor, options: options, closure) } - public func configuration(forMethod descriptor: MethodDescriptor) -> MethodConfig? { - self.channel.configuration(forMethod: descriptor) + public func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? { + self.channel.config(forMethod: descriptor) } } } @@ -200,7 +200,7 @@ extension HTTP2ClientTransport.TransportServices { /// - compression: Compression configuration. /// - transportSecurity: The transport's security configuration. /// - /// - SeeAlso: ``defaults(_:)``. + /// - SeeAlso: ``defaults(transportSecurity:configure:)`` public init( http2: HTTP2ClientTransport.Config.HTTP2, backoff: HTTP2ClientTransport.Config.Backoff, diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift index 2e29bdd82..822138f33 100644 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift @@ -26,7 +26,7 @@ private import Synchronization /// as a ``ServiceConfig``. /// /// Once you have a client, you must keep a long-running task executing ``connect()``, which -/// will return only once all streams have been finished and ``close()`` has been called on this client; or +/// will return only once all streams have been finished and ``beginGracefulShutdown()`` has been called on this client; or /// when the containing task is cancelled. /// /// To execute requests using this client, use ``withStream(descriptor:options:_:)``. If this function is @@ -126,7 +126,7 @@ public final class InProcessClientTransport: ClientTransport { /// /// Implementations of this function will typically create a long-lived task group which /// maintains connections. The function exits when all open streams have been closed and new connections - /// are no longer required by the caller who signals this by calling ``close()``, or by cancelling the + /// are no longer required by the caller who signals this by calling ``beginGracefulShutdown()``, or by cancelling the /// task this function runs in. public func connect() async throws { let (stream, continuation) = AsyncStream.makeStream() @@ -345,7 +345,7 @@ public final class InProcessClientTransport: ClientTransport { /// /// - Parameter descriptor: The method to lookup configuration for. /// - Returns: Execution configuration for the method, if it exists. - public func configuration( + public func config( forMethod descriptor: MethodDescriptor ) -> MethodConfig? { self.methodConfig[descriptor] diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index de36e0151..837cb2ce8 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -27,13 +27,13 @@ internal struct ProtobufCodeGenParser { let namer: SwiftProtobufNamer let extraModuleImports: [String] let protoToModuleMappings: ProtoFileToModuleMappings - let accessLevel: SourceGenerator.Configuration.AccessLevel + let accessLevel: SourceGenerator.Config.AccessLevel internal init( input: FileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings, extraModuleImports: [String], - accessLevel: SourceGenerator.Configuration.AccessLevel + accessLevel: SourceGenerator.Config.AccessLevel ) { self.input = input self.extraModuleImports = extraModuleImports diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index b612fb578..ad888319d 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -18,10 +18,10 @@ public import GRPCCodeGen public import SwiftProtobufPluginLibrary public struct ProtobufCodeGenerator { - internal var configuration: SourceGenerator.Configuration + internal var configuration: SourceGenerator.Config public init( - configuration: SourceGenerator.Configuration + configuration: SourceGenerator.Config ) { self.configuration = configuration } @@ -37,7 +37,7 @@ public struct ProtobufCodeGenerator { extraModuleImports: extraModuleImports, accessLevel: self.configuration.accessLevel ) - let sourceGenerator = SourceGenerator(configuration: self.configuration) + let sourceGenerator = SourceGenerator(config: self.configuration) let codeGenerationRequest = try parser.parse() let sourceFile = try sourceGenerator.generate(codeGenerationRequest) diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift index d700c2761..7d75b9d43 100644 --- a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift +++ b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift @@ -123,7 +123,7 @@ final class GenerateGRPC: CodeGenerator { fileNamingOption: options.fileNaming ) - let config = SourceGenerator.Configuration(options: options) + let config = SourceGenerator.Config(options: options) let fileGenerator = ProtobufCodeGenerator(configuration: config) let contents = try fileGenerator.generateCode( from: descriptor, @@ -212,9 +212,9 @@ private func splitPath(pathname: String) -> (dir: String, base: String, suffix: } #if compiler(>=6.0) -extension SourceGenerator.Configuration { +extension SourceGenerator.Config { init(options: GeneratorOptions) { - let accessLevel: SourceGenerator.Configuration.AccessLevel + let accessLevel: SourceGenerator.Config.AccessLevel switch options.visibility { case .internal: accessLevel = .internal diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 5f26bbe3a..88a713679 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -797,7 +797,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { private func assertClientCodeTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, - accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevel: SourceGenerator.Config.AccessLevel, file: StaticString = #filePath, line: UInt = #line ) throws { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 2ae6c9ceb..c11dd3bda 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -268,7 +268,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { private func assertIDLToStructuredSwiftTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, - accessLevel: SourceGenerator.Configuration.AccessLevel, + accessLevel: SourceGenerator.Config.AccessLevel, server: Bool = false ) throws { let translator = IDLToStructuredSwiftTranslator() diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 8ec1c8015..d4d2bc571 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -640,7 +640,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { private func assertServerCodeTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, - accessLevel: SourceGenerator.Configuration.AccessLevel + accessLevel: SourceGenerator.Config.AccessLevel ) throws { let translator = ServerCodeTranslator(accessLevel: accessLevel) let codeBlocks = try translator.translate(from: codeGenerationRequest) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index ce9d0ab80..af0a9017a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -725,7 +725,7 @@ extension TypealiasTranslatorSnippetBasedTests { expectedSwift: String, client: Bool, server: Bool, - accessLevel: SourceGenerator.Configuration.AccessLevel + accessLevel: SourceGenerator.Config.AccessLevel ) throws { let translator = TypealiasTranslator(client: client, server: server, accessLevel: accessLevel) let codeBlocks = try translator.translate(from: codeGenerationRequest) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 850b006d5..0500b23ec 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -20,7 +20,7 @@ import GRPCInProcessTransport @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension InProcessServerTransport { func spawnClientTransport( - throttle: RetryThrottle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + throttle: RetryThrottle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) ) -> InProcessClientTransport { return InProcessClientTransport(server: self) } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index ffeeae724..6dceb9976 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -90,7 +90,7 @@ extension ClientRPCExecutorTests { try await $0.write([2]) }, options: .hedge( - maximumAttempts: 5, + maxAttempts: 5, delay: .milliseconds(10), nonFatalCodes: [.unavailable] ) @@ -134,7 +134,7 @@ extension ClientRPCExecutorTests { try await $0.write([2]) }, options: .hedge( - maximumAttempts: 5, + maxAttempts: 5, delay: .seconds(60), // High delay, server pushback will override this. nonFatalCodes: [.unavailable] ) @@ -189,13 +189,13 @@ extension ClientRPCExecutorTests { @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { fileprivate static func hedge( - maximumAttempts: Int = 5, + maxAttempts: Int = 5, delay: Duration = .milliseconds(25), nonFatalCodes: Set, timeout: Duration? = nil ) -> Self { let policy = HedgingPolicy( - maximumAttempts: maximumAttempts, + maxAttempts: maxAttempts, hedgingDelay: delay, nonFatalStatusCodes: nonFatalCodes ) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index ca094dba9..6c2e6cd1e 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -224,9 +224,9 @@ extension ClientRPCExecutorTests { ) let retryPolicy = RetryPolicy( - maximumAttempts: 5, + maxAttempts: 5, initialBackoff: .seconds(60), - maximumBackoff: .seconds(50), + maxBackoff: .seconds(50), backoffMultiplier: 1, retryableStatusCodes: [.unavailable] ) @@ -261,9 +261,9 @@ extension ClientRPCExecutorTests { ) let retryPolicy = RetryPolicy( - maximumAttempts: 5, + maxAttempts: 5, initialBackoff: .seconds(60), - maximumBackoff: .seconds(50), + maxBackoff: .seconds(50), backoffMultiplier: 1, retryableStatusCodes: [.unavailable] ) @@ -305,9 +305,9 @@ extension CallOptions { timeout: Duration? = nil ) -> Self { let policy = RetryPolicy( - maximumAttempts: maximumAttempts, + maxAttempts: maximumAttempts, initialBackoff: .milliseconds(10), - maximumBackoff: .milliseconds(100), + maxBackoff: .milliseconds(100), backoffMultiplier: 1.6, retryableStatusCodes: codes ) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 43c8a1225..c2396fdef 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -231,16 +231,16 @@ final class ClientRPCExecutorTests: XCTestCase { var policies: [RPCExecutionPolicy?] = [nil] let retryPolicy = RetryPolicy( - maximumAttempts: 5, + maxAttempts: 5, initialBackoff: .seconds(1), - maximumBackoff: .seconds(1), + maxBackoff: .seconds(1), backoffMultiplier: 1.6, retryableStatusCodes: [.unavailable] ) policies.append(.retry(retryPolicy)) let hedgingPolicy = HedgingPolicy( - maximumAttempts: 5, + maxAttempts: 5, hedgingDelay: .seconds(1), nonFatalStatusCodes: [.unavailable] ) diff --git a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift index 2a6930a68..cbe0d7a09 100644 --- a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -21,9 +21,9 @@ import XCTest final class RetryDelaySequenceTests: XCTestCase { func testSequence() { let policy = RetryPolicy( - maximumAttempts: 3, // ignored here + maxAttempts: 3, // ignored here initialBackoff: .seconds(1), - maximumBackoff: .seconds(8), + maxBackoff: .seconds(8), backoffMultiplier: 2.0, retryableStatusCodes: [.aborted] // ignored here ) @@ -41,9 +41,9 @@ final class RetryDelaySequenceTests: XCTestCase { func testSequenceSupportsMultipleIteration() { let policy = RetryPolicy( - maximumAttempts: 3, // ignored here + maxAttempts: 3, // ignored here initialBackoff: .seconds(1), - maximumBackoff: .seconds(8), + maxBackoff: .seconds(8), backoffMultiplier: 2.0, retryableStatusCodes: [.aborted] // ignored here ) diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift index c65fe1c54..d9797343a 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift @@ -91,9 +91,9 @@ struct MethodConfigCodingTests { @Test("RetryPolicy") func retryPolicy() throws { let policy = RetryPolicy( - maximumAttempts: 3, + maxAttempts: 3, initialBackoff: .seconds(1), - maximumBackoff: .seconds(3), + maxBackoff: .seconds(3), backoffMultiplier: 1.6, retryableStatusCodes: [.aborted] ) @@ -107,7 +107,7 @@ struct MethodConfigCodingTests { @Test("HedgingPolicy") func hedgingPolicy() throws { let policy = HedgingPolicy( - maximumAttempts: 3, + maxAttempts: 3, hedgingDelay: .seconds(1), nonFatalStatusCodes: [.aborted] ) @@ -233,9 +233,9 @@ struct MethodConfigCodingTests { func retryPolicy() throws { let decoded = try self.decodeFromFile("method_config.retry_policy", as: RetryPolicy.self) let expected = RetryPolicy( - maximumAttempts: 3, + maxAttempts: 3, initialBackoff: .seconds(1), - maximumBackoff: .seconds(3), + maxBackoff: .seconds(3), backoffMultiplier: 1.6, retryableStatusCodes: [.aborted, .unavailable] ) @@ -262,7 +262,7 @@ struct MethodConfigCodingTests { func hedgingPolicy() throws { let decoded = try self.decodeFromFile("method_config.hedging_policy", as: HedgingPolicy.self) let expected = HedgingPolicy( - maximumAttempts: 3, + maxAttempts: 3, hedgingDelay: .seconds(1), nonFatalStatusCodes: [.aborted] ) @@ -315,7 +315,7 @@ struct MethodConfigCodingTests { maxResponseMessageBytes: 2048, executionPolicy: .hedge( HedgingPolicy( - maximumAttempts: 3, + maxAttempts: 3, hedgingDelay: .seconds(42), nonFatalStatusCodes: [.aborted, .unimplemented] ) @@ -341,9 +341,9 @@ struct MethodConfigCodingTests { maxResponseMessageBytes: 2048, executionPolicy: .retry( RetryPolicy( - maximumAttempts: 3, + maxAttempts: 3, initialBackoff: .seconds(1), - maximumBackoff: .seconds(3), + maxBackoff: .seconds(3), backoffMultiplier: 1.6, retryableStatusCodes: [.aborted, .unimplemented] ) diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift index eca712cf5..8d01bcfd6 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigTests.swift @@ -21,32 +21,32 @@ struct MethodConfigTests { @Test("RetryPolicy clamps max attempts") func retryPolicyClampsMaxAttempts() { var policy = RetryPolicy( - maximumAttempts: 10, + maxAttempts: 10, initialBackoff: .seconds(1), - maximumBackoff: .seconds(1), + maxBackoff: .seconds(1), backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) // Should be clamped on init - #expect(policy.maximumAttempts == 5) + #expect(policy.maxAttempts == 5) // and when modifying - policy.maximumAttempts = 10 - #expect(policy.maximumAttempts == 5) + policy.maxAttempts = 10 + #expect(policy.maxAttempts == 5) } @Test("HedgingPolicy clamps max attempts") func hedgingPolicyClampsMaxAttempts() { var policy = HedgingPolicy( - maximumAttempts: 10, + maxAttempts: 10, hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) // Should be clamped on init - #expect(policy.maximumAttempts == 5) + #expect(policy.maxAttempts == 5) // and when modifying - policy.maximumAttempts = 10 - #expect(policy.maximumAttempts == 5) + policy.maxAttempts = 10 + #expect(policy.maxAttempts == 5) } } diff --git a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift index 03786cc0c..8dbcd7340 100644 --- a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift @@ -229,7 +229,7 @@ final class ServiceConfigCodingTests: XCTestCase { maxResponseMessageBytes: 4096, executionPolicy: .hedge( HedgingPolicy( - maximumAttempts: 3, + maxAttempts: 3, hedgingDelay: .seconds(1), nonFatalStatusCodes: [.aborted] ) diff --git a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift index cbbcb9d77..58ddcde4f 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -20,18 +20,18 @@ import XCTest final class MethodConfigsTests: XCTestCase { func testGetConfigurationForKnownMethod() async throws { let policy = HedgingPolicy( - maximumAttempts: 10, + maxAttempts: 10, hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() - configurations.setDefaultConfiguration(defaultConfiguration) + configurations.setDefaultConfig(defaultConfiguration) let descriptor = MethodDescriptor(service: "test", method: "first") let retryPolicy = RetryPolicy( - maximumAttempts: 10, + maxAttempts: 10, initialBackoff: .seconds(1), - maximumBackoff: .seconds(1), + maxBackoff: .seconds(1), backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) @@ -43,18 +43,18 @@ final class MethodConfigsTests: XCTestCase { func testGetConfigurationForUnknownMethodButServiceOverride() { let policy = HedgingPolicy( - maximumAttempts: 10, + maxAttempts: 10, hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() - configurations.setDefaultConfiguration(defaultConfiguration) + configurations.setDefaultConfig(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test", method: "") let retryPolicy = RetryPolicy( - maximumAttempts: 10, + maxAttempts: 10, initialBackoff: .seconds(1), - maximumBackoff: .seconds(1), + maxBackoff: .seconds(1), backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) @@ -67,18 +67,18 @@ final class MethodConfigsTests: XCTestCase { func testGetConfigurationForUnknownMethodDefaultValue() { let policy = HedgingPolicy( - maximumAttempts: 10, + maxAttempts: 10, hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() - configurations.setDefaultConfiguration(defaultConfiguration) + configurations.setDefaultConfig(defaultConfiguration) let firstDescriptor = MethodDescriptor(service: "test1", method: "first") let retryPolicy = RetryPolicy( - maximumAttempts: 10, + maxAttempts: 10, initialBackoff: .seconds(1), - maximumBackoff: .seconds(1), + maxBackoff: .seconds(1), backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index 086cf2127..eb8208b72 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -49,7 +49,7 @@ struct AnyClientTransport: ClientTransport, Sendable { } self._configuration = { descriptor in - transport.configuration(forMethod: descriptor) + transport.config(forMethod: descriptor) } } @@ -74,7 +74,7 @@ struct AnyClientTransport: ClientTransport, Sendable { return result as! T } - func configuration( + func config( forMethod descriptor: MethodDescriptor ) -> MethodConfig? { self._configuration(descriptor) diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 9bbed93a4..835c81b79 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -71,10 +71,10 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } } - func configuration( + func config( forMethod descriptor: MethodDescriptor ) -> MethodConfig? { - self.transport.configuration(forMethod: descriptor) + self.transport.config(forMethod: descriptor) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index b9c9b97e6..804be7d52 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -26,7 +26,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { self.code = code } - let retryThrottle: RetryThrottle? = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + let retryThrottle: RetryThrottle? = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) func connect() async throws { // no-op @@ -36,7 +36,7 @@ struct ThrowOnStreamCreationTransport: ClientTransport { // no-op } - func configuration( + func config( forMethod descriptor: MethodDescriptor ) -> MethodConfig? { return nil diff --git a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift index 34f283345..ee2ad3742 100644 --- a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift +++ b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift @@ -21,21 +21,21 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class RetryThrottleTests: XCTestCase { func testThrottleOnInit() { - let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) // Start with max tokens, so permitted. XCTAssertTrue(throttle.isRetryPermitted) - XCTAssertEqual(throttle.maximumTokens, 10) + XCTAssertEqual(throttle.maxTokens, 10) XCTAssertEqual(throttle.tokens, 10) XCTAssertEqual(throttle.tokenRatio, 0.1) } func testThrottleIgnoresMoreThanThreeDecimals() { - let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1239) + let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1239) XCTAssertEqual(throttle.tokenRatio, 0.123) } func testFailureReducesTokens() { - let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) XCTAssertEqual(throttle.tokens, 10) XCTAssert(throttle.isRetryPermitted) @@ -62,7 +62,7 @@ final class RetryThrottleTests: XCTestCase { } func testTokensCantDropBelowZero() { - let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) for _ in 0 ..< 1000 { throttle.recordFailure() XCTAssertGreaterThanOrEqual(throttle.tokens, 0) @@ -71,7 +71,7 @@ final class RetryThrottleTests: XCTestCase { } func testSuccessIncreasesTokens() { - let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) // Drop to zero. for _ in 0 ..< 10 { @@ -91,7 +91,7 @@ final class RetryThrottleTests: XCTestCase { } func testTokensCantRiseAboveMax() { - let throttle = RetryThrottle(maximumTokens: 10, tokenRatio: 0.1) + let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) XCTAssertEqual(throttle.tokens, 10) throttle.recordSuccess() XCTAssertEqual(throttle.tokens, 10) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift index 96a172311..fc0365703 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift @@ -38,11 +38,11 @@ final class GRPCChannelTests: XCTestCase { defaultServiceConfig: serviceConfig ) - XCTAssertNotNil(channel.configuration(forMethod: .echoGet)) - XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + XCTAssertNotNil(channel.config(forMethod: .echoGet)) + XCTAssertNil(channel.config(forMethod: .echoUpdate)) let throttle = try XCTUnwrap(channel.retryThrottle) - XCTAssertEqual(throttle.maximumTokens, 100) + XCTAssertEqual(throttle.maxTokens, 100) XCTAssertEqual(throttle.tokenRatio, 0.1) } @@ -71,8 +71,8 @@ final class GRPCChannelTests: XCTestCase { ) // Not resolved yet so the default (empty) service config is used. - XCTAssertNil(channel.configuration(forMethod: .echoGet)) - XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + XCTAssertNil(channel.config(forMethod: .echoGet)) + XCTAssertNil(channel.config(forMethod: .echoUpdate)) XCTAssertNil(channel.retryThrottle) try await withThrowingDiscardingTaskGroup { group in @@ -88,11 +88,11 @@ final class GRPCChannelTests: XCTestCase { switch event { case .ready: // When the channel is ready it must have the service config from the resolver. - XCTAssertNotNil(channel.configuration(forMethod: .echoGet)) - XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + XCTAssertNotNil(channel.config(forMethod: .echoGet)) + XCTAssertNil(channel.config(forMethod: .echoUpdate)) let throttle = try XCTUnwrap(channel.retryThrottle) - XCTAssertEqual(throttle.maximumTokens, 100) + XCTAssertEqual(throttle.maxTokens, 100) XCTAssertEqual(throttle.tokenRatio, 0.1) // Now close. @@ -124,8 +124,8 @@ final class GRPCChannelTests: XCTestCase { ) // Not resolved yet so the default (empty) service config is used. - XCTAssertNil(channel.configuration(forMethod: .echoGet)) - XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + XCTAssertNil(channel.config(forMethod: .echoGet)) + XCTAssertNil(channel.config(forMethod: .echoUpdate)) XCTAssertNil(channel.retryThrottle) // Yield the first address list and service config. @@ -155,10 +155,10 @@ final class GRPCChannelTests: XCTestCase { switch event { case .ready: // When the channel it must have the service config from the resolver. - XCTAssertNotNil(channel.configuration(forMethod: .echoGet)) - XCTAssertNil(channel.configuration(forMethod: .echoUpdate)) + XCTAssertNotNil(channel.config(forMethod: .echoGet)) + XCTAssertNil(channel.config(forMethod: .echoUpdate)) let throttle = try XCTUnwrap(channel.retryThrottle) - XCTAssertEqual(throttle.maximumTokens, 100) + XCTAssertEqual(throttle.maxTokens, 100) XCTAssertEqual(throttle.tokenRatio, 0.1) // Now yield a new service config with the same addresses. @@ -170,8 +170,8 @@ final class GRPCChannelTests: XCTestCase { // This should be propagated quickly. try await XCTPoll(every: .milliseconds(10)) { - let noConfigForGet = channel.configuration(forMethod: .echoGet) == nil - let configForUpdate = channel.configuration(forMethod: .echoUpdate) != nil + let noConfigForGet = channel.config(forMethod: .echoGet) == nil + let configForUpdate = channel.config(forMethod: .echoUpdate) != nil let noThrottle = channel.retryThrottle == nil return noConfigForGet && configForUpdate && noThrottle } diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift index 6a237db40..aecfd2a8e 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift @@ -118,7 +118,7 @@ extension ConnectionTest { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: .none, - maximumPayloadSize: .max, + maxPayloadSize: .max, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift index 4063a06e9..37292e4ea 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift @@ -74,7 +74,7 @@ final class TestServer: Sendable { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: .all, - maximumPayloadSize: .max, + maxPayloadSize: .max, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift index ab6da3f3f..590153380 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift @@ -32,7 +32,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1 + maxPayloadSize: 1 ) let channel = EmbeddedChannel(handler: handler) @@ -63,7 +63,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -99,7 +99,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -130,7 +130,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -167,7 +167,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -203,7 +203,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .deflate, acceptedEncodings: [.deflate], - maximumPayloadSize: 1 + maxPayloadSize: 1 ) let channel = EmbeddedChannel(handler: handler) @@ -261,7 +261,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -328,7 +328,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, skipStateMachineAssertions: true ) @@ -396,7 +396,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -462,7 +462,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, skipStateMachineAssertions: true ) @@ -580,7 +580,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, skipStateMachineAssertions: true ) @@ -685,7 +685,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, skipStateMachineAssertions: true ) @@ -769,7 +769,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -819,7 +819,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) @@ -865,7 +865,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase { scheme: .http, outboundEncoding: .none, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, skipStateMachineAssertions: true ) diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift index c0d9ec811..544825a84 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift @@ -44,12 +44,12 @@ final class GRPCMessageDecoderTests: XCTestCase { (firstMessage, [Array(repeating: UInt8(42), count: 16)]), (secondMessage, [Array(repeating: UInt8(43), count: 8)]), ]) { - GRPCMessageDecoder(maximumPayloadSize: .max) + GRPCMessageDecoder(maxPayloadSize: .max) } } func testReadMessageOverSizeLimitWithoutCompression() throws { - let deframer = GRPCMessageDecoder(maximumPayloadSize: 100) + let deframer = GRPCMessageDecoder(maxPayloadSize: 100) let processor = NIOSingleStepByteToMessageProcessor(deframer) var buffer = ByteBuffer() @@ -72,7 +72,7 @@ final class GRPCMessageDecoderTests: XCTestCase { } func testReadMessageOverSizeLimitButWithoutActualMessageBytes() throws { - let deframer = GRPCMessageDecoder(maximumPayloadSize: 100) + let deframer = GRPCMessageDecoder(maxPayloadSize: 100) let processor = NIOSingleStepByteToMessageProcessor(deframer) var buffer = ByteBuffer() @@ -97,7 +97,7 @@ final class GRPCMessageDecoderTests: XCTestCase { } func testCompressedMessageWithoutConfiguringDecompressor() throws { - let deframer = GRPCMessageDecoder(maximumPayloadSize: 100) + let deframer = GRPCMessageDecoder(maxPayloadSize: 100) let processor = NIOSingleStepByteToMessageProcessor(deframer) var buffer = ByteBuffer() @@ -143,7 +143,7 @@ final class GRPCMessageDecoderTests: XCTestCase { (firstMessage.bytes, [Array(repeating: 42, count: 100)]), (secondMessage.bytes, [Array(repeating: 43, count: 110)]), ]) { - GRPCMessageDecoder(maximumPayloadSize: 1000, decompressor: decompressor) + GRPCMessageDecoder(maxPayloadSize: 1000, decompressor: decompressor) } } @@ -156,7 +156,7 @@ final class GRPCMessageDecoderTests: XCTestCase { } func testReadCompressedMessageOverSizeLimitBeforeDecompressing() throws { - let deframer = GRPCMessageDecoder(maximumPayloadSize: 1) + let deframer = GRPCMessageDecoder(maxPayloadSize: 1) let processor = NIOSingleStepByteToMessageProcessor(deframer) let compressor = Zlib.Compressor(method: .gzip) var framer = GRPCMessageFramer() @@ -186,7 +186,7 @@ final class GRPCMessageDecoderTests: XCTestCase { private func testReadDecompressedMessageOverSizeLimit(method: Zlib.Method) throws { let decompressor = Zlib.Decompressor(method: method) - let deframer = GRPCMessageDecoder(maximumPayloadSize: 100, decompressor: decompressor) + let deframer = GRPCMessageDecoder(maxPayloadSize: 100, decompressor: decompressor) let processor = NIOSingleStepByteToMessageProcessor(deframer) let compressor = Zlib.Compressor(method: method) var framer = GRPCMessageFramer() diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index 61e2354f4..4ca3e608b 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift @@ -150,7 +150,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase { acceptedEncodings: [.deflate] ) ), - maximumPayloadSize: 100, + maxPayloadSize: 100, skipAssertions: true ) @@ -1346,7 +1346,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase { acceptedEncodings: deflateCompressionEnabled ? [.deflate] : [] ) ), - maximumPayloadSize: 100, + maxPayloadSize: 100, skipAssertions: true ) diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index deb610673..72486d92d 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift @@ -30,7 +30,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -60,7 +60,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -90,7 +90,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -129,7 +129,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -168,7 +168,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -206,7 +206,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -248,7 +248,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -317,7 +317,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 1, + maxPayloadSize: 1, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self), skipStateMachineAssertions: true ) @@ -383,7 +383,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 42, + maxPayloadSize: 42, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self), skipStateMachineAssertions: true ) @@ -493,7 +493,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -589,7 +589,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -629,7 +629,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -707,7 +707,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) ) try channel.pipeline.syncOperations.addHandler(handler) @@ -789,7 +789,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: promise, skipStateMachineAssertions: true ) @@ -828,7 +828,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: promise, skipStateMachineAssertions: true ) @@ -851,7 +851,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: promise, skipStateMachineAssertions: true ) @@ -886,7 +886,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: promise, skipStateMachineAssertions: true ) @@ -941,7 +941,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: promise, skipStateMachineAssertions: true ) @@ -996,7 +996,7 @@ final class GRPCServerStreamHandlerTests: XCTestCase { let handler = GRPCServerStreamHandler( scheme: .http, acceptedEncodings: [], - maximumPayloadSize: 100, + maxPayloadSize: 100, methodDescriptorPromise: promise, skipStateMachineAssertions: true ) diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index bde6f7bb6..d33b2774d 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -192,7 +192,7 @@ final class InProcessClientTransportTests: XCTestCase { func testExecutionConfiguration() { let policy = HedgingPolicy( - maximumAttempts: 10, + maxAttempts: 10, hedgingDelay: .seconds(1), nonFatalStatusCodes: [] ) @@ -215,14 +215,14 @@ final class InProcessClientTransportTests: XCTestCase { let firstDescriptor = MethodDescriptor(service: "test", method: "first") XCTAssertEqual( - client.configuration(forMethod: firstDescriptor), + client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first ) let retryPolicy = RetryPolicy( - maximumAttempts: 10, + maxAttempts: 10, initialBackoff: .seconds(1), - maximumBackoff: .seconds(1), + maxBackoff: .seconds(1), backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) @@ -239,11 +239,11 @@ final class InProcessClientTransportTests: XCTestCase { let secondDescriptor = MethodDescriptor(service: "test", method: "second") XCTAssertEqual( - client.configuration(forMethod: firstDescriptor), + client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first ) XCTAssertEqual( - client.configuration(forMethod: secondDescriptor), + client.config(forMethod: secondDescriptor), serviceConfig.methodConfig.last ) } @@ -288,9 +288,9 @@ final class InProcessClientTransportTests: XCTestCase { server: InProcessServerTransport = InProcessServerTransport() ) -> InProcessClientTransport { let defaultPolicy = RetryPolicy( - maximumAttempts: 10, + maxAttempts: 10, initialBackoff: .seconds(1), - maximumBackoff: .seconds(1), + maxBackoff: .seconds(1), backoffMultiplier: 1.0, retryableStatusCodes: [.unavailable] ) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index e192b1e5a..e8c44043f 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -532,7 +532,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { func testCodeGeneration( proto: Google_Protobuf_FileDescriptorProto, indentation: Int, - visibility: SourceGenerator.Configuration.AccessLevel, + visibility: SourceGenerator.Config.AccessLevel, client: Bool, server: Bool, accessLevelOnImports: Bool = true, @@ -540,7 +540,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { file: StaticString = #filePath, line: UInt = #line ) throws { - let config = SourceGenerator.Configuration( + let config = SourceGenerator.Config( accessLevel: visibility, accessLevelOnImports: accessLevelOnImports, client: client, From 9bdb0d1abb36a4601a984af04f4cfaf30862f536 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 20 Sep 2024 15:18:57 +0100 Subject: [PATCH 471/580] Filter down main to v2 (#2066) Motivation: v2 will be split across a number of packages, and v1 will be maintained on the 'release/1.x' branch. In order to do the package split this package needs to be whittled down to only the v2 components which will remain within this package. Modifications: - Remove v1 code - Remove v2 code destined for other packages - Reshuffle a few scripts Result: - Package contains only the intended code --- .github/workflows/ci.yaml | 96 +- .github/workflows/release.yml | 42 - AUTHORS | 1 - .../Implementation/EchoAsyncProvider.swift | 69 - .../v1/Echo/Implementation/EchoProvider.swift | 88 - .../HPACKHeaders+Prettify.swift | 23 - .../v1/Echo/Implementation/Interceptors.swift | 121 - Examples/v1/Echo/Model/echo.grpc.swift | 749 ----- Examples/v1/Echo/Model/echo.pb.swift | 129 - Examples/v1/Echo/README.md | 50 - Examples/v1/Echo/Runtime/Echo.swift | 246 -- Examples/v1/Echo/Runtime/Empty.swift | 17 - .../HelloWorld/Client/HelloWorldClient.swift | 69 - .../v1/HelloWorld/Model/helloworld.grpc.swift | 308 -- .../v1/HelloWorld/Model/helloworld.pb.swift | 129 - Examples/v1/HelloWorld/README.md | 30 - .../HelloWorld/Server/GreeterProvider.swift | 33 - .../HelloWorld/Server/HelloWorldServer.swift | 45 - Examples/v1/PacketCapture/Empty.swift | 17 - Examples/v1/PacketCapture/PacketCapture.swift | 87 - Examples/v1/PacketCapture/README.md | 35 - Examples/v1/README.md | 5 - .../Generated/echo.grpc.reflection | 1 - .../Generated/helloworld.grpc.reflection | 1 - .../ReflectionService/GreeterProvider.swift | 1 - .../ReflectionService/ReflectionServer.swift | 62 - Examples/v1/RouteGuide/Client/Empty.swift | 17 - .../RouteGuide/Client/RouteGuideClient.swift | 242 -- .../RouteGuide/Model/route_guide.grpc.swift | 682 ---- .../v1/RouteGuide/Model/route_guide.pb.swift | 387 --- Examples/v1/RouteGuide/README.md | 33 - .../Server/RouteGuideProvider.swift | 174 - .../RouteGuide/Server/RouteGuideServer.swift | 66 - Examples/v1/RouteGuide/route_guide_db.json | 601 ---- FuzzTesting/.gitignore | 7 - ...ized-ServerFuzzer-release-4739158818553856 | Bin 626 -> 0 bytes ...ized-ServerFuzzer-release-5077460227063808 | Bin 4284 -> 0 bytes ...ized-ServerFuzzer-release-5134158417494016 | Bin 30879 -> 0 bytes ...ized-ServerFuzzer-release-5285159577452544 | 10 - ...ized-ServerFuzzer-release-5448955772141568 | Bin 29991 -> 0 bytes ...zed-grpc-swift-fuzz-debug-4645975625957376 | 4 - ...d-grpc-swift-fuzz-release-5413100925878272 | 4 - FuzzTesting/Package.swift | 57 - FuzzTesting/README.md | 31 - FuzzTesting/Sources/EchoImplementation | 1 - FuzzTesting/Sources/EchoModel | 1 - .../Sources/ServerFuzzer/src/serverfuzzer.c | 9 - .../ServerFuzzerLib/ServerFuzzer.swift | 49 - .../GRPCSwiftBenchmark/Benchmarks.swift | 0 .../Benchmarks/Package.swift | 2 +- ...wiftBenchmark.Metadata_Add_binary.p90.json | 0 ...wiftBenchmark.Metadata_Add_string.p90.json | 0 ...hmark.Metadata_Iterate_all_values.p90.json | 0 ...es_when_only_binary_values_stored.p90.json | 0 ...y_values_when_only_strings_stored.p90.json | 0 ...rk.Metadata_Iterate_string_values.p90.json | 0 ...rk.Metadata_Remove_values_for_key.p90.json | 0 ...wiftBenchmark.Metadata_Add_binary.p90.json | 0 ...wiftBenchmark.Metadata_Add_string.p90.json | 0 ...hmark.Metadata_Iterate_all_values.p90.json | 0 ...es_when_only_binary_values_stored.p90.json | 0 ...y_values_when_only_strings_stored.p90.json | 0 ...rk.Metadata_Iterate_string_values.p90.json | 0 ...rk.Metadata_Remove_values_for_key.p90.json | 0 NOTICES.txt | 24 +- PATENTS | 22 - Package.swift | 545 +--- Package@swift-6.swift | 1127 ------- Performance/QPSBenchmark/.gitignore | 1 - Performance/QPSBenchmark/Package.swift | 75 - Performance/QPSBenchmark/README.md | 67 - .../Sources/BenchmarkUtils/Histogram.swift | 122 - .../Sources/BenchmarkUtils/StatusCounts.swift | 51 - .../Model/benchmark_service.proto | 44 - .../Sources/QPSBenchmark/Model/control.proto | 289 -- .../QPSBenchmark/Model/core_stats.proto | 38 - .../Sources/QPSBenchmark/Model/messages.proto | 214 -- .../Sources/QPSBenchmark/Model/payloads.proto | 40 - .../Sources/QPSBenchmark/Model/stats.proto | 83 - .../QPSBenchmark/Model/worker_service.proto | 45 - .../Async/AsyncBenchmarkServiceImpl.swift | 121 - .../Runtime/Async/AsyncClientProtocol.swift | 36 - .../Async/AsyncPingPongRequestMaker.swift | 85 - .../Runtime/Async/AsyncQPSClientImpl.swift | 293 -- .../Runtime/Async/AsyncQPSServerImpl.swift | 102 - .../Runtime/Async/AsyncRequestMaker.swift | 42 - .../Runtime/Async/AsyncServerProtocol.swift | 36 - .../Async/AsyncUnaryRequestMaker.swift | 66 - .../Async/AsyncWorkerServiceImpl.swift | 335 -- .../Runtime/ClientUtilities.swift | 42 - .../Runtime/HistogramSerialisation.swift | 33 - .../Runtime/NIO/NIOBenchmarkServiceImpl.swift | 129 - .../Runtime/NIO/NIOClientProtocol.swift | 33 - .../Runtime/NIO/NIOPingPongRequestMaker.swift | 97 - .../Runtime/NIO/NIOQPSClientImpl.swift | 294 -- .../Runtime/NIO/NIOQPSServerImpl.swift | 113 - .../Runtime/NIO/NIORequestMaker.swift | 44 - .../Runtime/NIO/NIOServerProtocol.swift | 33 - .../Runtime/NIO/NIOUnaryRequestMaker.swift | 72 - .../Runtime/NIO/NIOWorkerServiceImpl.swift | 339 -- .../QPSBenchmark/Runtime/QPSWorker.swift | 84 - .../QPSBenchmark/Runtime/ResourceUsage.swift | 59 - .../Runtime/ServerTypeExtensions.swift | 35 - .../Runtime/ServerUtilities.swift | 23 - .../Sources/QPSBenchmark/Runtime/Stats.swift | 60 - .../Runtime/StatusCountsSerialisation.swift | 30 - .../QPSBenchmark/Runtime/grpcTime.swift | 50 - .../Sources/QPSBenchmark/Runtime/main.swift | 78 - .../QPSBenchmark/grpc-swift-config.json | 11 - .../QPSBenchmark/swift-protobuf-config.json | 14 - .../BenchmarkUtilsTests/HistogramTests.swift | 103 - .../StatusCountsTests.swift | 123 - .../bidirectional-ping-pong-1-connection.json | 61 - .../scenarios/unary-1-connection.json | 53 - .../scenarios/unary-unconstrained.json | 53 - .../allocations/test-allocation-counts.sh | 77 - Performance/allocations/test-utils.sh | 69 - .../tests/run-allocation-counter-tests.sh | 76 - .../allocations/tests/shared/Benchmark.swift | 1 - .../allocations/tests/shared/Common.swift | 122 - .../tests/shared/EmbeddedServer.swift | 1 - .../tests/shared/MinimalEchoProvider.swift | 1 - .../allocations/tests/shared/echo.grpc.swift | 1 - .../allocations/tests/shared/echo.pb.swift | 1 - .../allocations/tests/test_bidi_1k_rpcs.swift | 90 - ...erver_bidi_1k_rpcs_10_small_requests.swift | 24 - ..._server_bidi_1k_rpcs_1_small_request.swift | 24 - ...server_unary_1k_rpcs_1_small_request.swift | 21 - .../tests/test_unary_1k_ping_pong.swift | 92 - Plugins/GRPCSwiftPlugin/plugin.swift | 406 --- Protos/generate.sh | 295 -- Protos/tests/control/control.proto | 116 - .../src/proto/grpc/testing/empty.proto | 28 - .../proto/grpc/testing/empty_service.proto | 23 - .../src/proto/grpc/testing/messages.proto | 165 - .../src/proto/grpc/testing/test.proto | 79 - .../tests/normalization/normalization.proto | 38 - Sources/CGRPCZlib/empty.c | 19 - Sources/CGRPCZlib/include/CGRPCZlib.h | 62 - Sources/GRPC/Array+BoundsCheck.swift | 25 - .../ServerHandlerStateMachine+Actions.swift | 96 - .../ServerHandlerStateMachine+Draining.swift | 108 - .../ServerHandlerStateMachine+Finished.swift | 79 - .../ServerHandlerStateMachine+Handling.swift | 111 - .../ServerHandlerStateMachine+Idle.swift | 79 - .../ServerHandlerStateMachine.swift | 360 --- ...erverInterceptorStateMachine+Actions.swift | 66 - ...rverInterceptorStateMachine+Finished.swift | 92 - ...InterceptorStateMachine+Intercepting.swift | 139 - .../ServerInterceptorStateMachine.swift | 284 -- .../StreamState.swift | 100 - .../Call+AsyncRequestStreamWriter.swift | 48 - ...llationError+GRPCStatusTransformable.swift | 22 - .../GRPCAsyncBidirectionalStreamingCall.swift | 200 -- .../GRPCAsyncClientStreamingCall.swift | 124 - .../GRPCAsyncRequestStream.swift | 158 - .../GRPCAsyncRequestStreamWriter.swift | 96 - .../GRPCAsyncResponseStream.swift | 59 - .../GRPCAsyncResponseStreamWriter.swift | 168 - .../GRPCAsyncSequenceProducerDelegate.swift | 30 - .../GRPCAsyncServerCallContext.swift | 130 - .../GRPCAsyncServerHandler.swift | 912 ------ .../GRPCAsyncServerStreamingCall.swift | 141 - .../GRPCAsyncUnaryCall.swift | 118 - .../GRPCAsyncWriterSinkDelegate.swift | 45 - .../GRPCChannel+AsyncAwaitSupport.swift | 223 -- .../GRPCClient+AsyncAwaitSupport.swift | 481 --- .../GRPC/AsyncAwaitSupport/GRPCSendable.swift | 25 - .../AsyncAwaitSupport/NIOAsyncWrappers.swift | 51 - .../BidirectionalStreamingServerHandler.swift | 383 --- .../ClientStreamingServerHandler.swift | 369 --- .../CallHandlers/ServerHandlerProtocol.swift | 70 - .../ServerStreamingServerHandler.swift | 355 -- .../CallHandlers/UnaryServerHandler.swift | 338 -- Sources/GRPC/CallOptions.swift | 212 -- .../BidirectionalStreamingCall.swift | 140 - Sources/GRPC/ClientCalls/Call.swift | 445 --- Sources/GRPC/ClientCalls/CallDetails.swift | 34 - Sources/GRPC/ClientCalls/ClientCall.swift | 173 - .../ClientCalls/ClientStreamingCall.swift | 141 - .../ClientCalls/LazyEventLoopPromise.swift | 108 - .../GRPC/ClientCalls/ResponseContainers.swift | 206 -- .../ClientCalls/ResponsePartContainer.swift | 69 - .../ClientCalls/ServerStreamingCall.swift | 88 - Sources/GRPC/ClientCalls/UnaryCall.swift | 92 - Sources/GRPC/ClientConnection.swift | 686 ---- ...ClientConnectionConfiguration+NIOSSL.swift | 133 - Sources/GRPC/ClientErrorDelegate.swift | 61 - ...oalescingLengthPrefixedMessageWriter.swift | 357 -- .../Compression/CompressionAlgorithm.swift | 53 - .../GRPC/Compression/DecompressionLimit.swift | 55 - .../GRPC/Compression/MessageEncoding.swift | 162 - Sources/GRPC/Compression/Zlib.swift | 526 --- Sources/GRPC/ConnectionBackoff.swift | 173 - Sources/GRPC/ConnectionKeepalive.swift | 184 -- .../GRPC/ConnectionManager+Delegates.swift | 100 - Sources/GRPC/ConnectionManager.swift | 1232 ------- .../ConnectionManagerChannelProvider.swift | 227 -- .../ConnectionPool/ConnectionManagerID.swift | 33 - .../ConnectionPool+PerConnectionState.swift | 178 - .../ConnectionPool+Waiter.swift | 117 - .../GRPC/ConnectionPool/ConnectionPool.swift | 1106 ------- .../ConnectionPool/ConnectionPoolIDs.swift | 59 - .../GRPC/ConnectionPool/GRPCChannelPool.swift | 435 --- Sources/GRPC/ConnectionPool/PoolManager.swift | 419 --- ...PoolManagerStateMachine+PerPoolState.swift | 107 - .../PoolManagerStateMachine.swift | 279 -- .../GRPC/ConnectionPool/PooledChannel.swift | 219 -- .../GRPC/ConnectionPool/StreamLender.swift | 24 - Sources/GRPC/ConnectivityState.swift | 160 - Sources/GRPC/DebugOnly.swift | 24 - Sources/GRPC/DelegatingErrorHandler.swift | 63 - .../GRPC/Docs.docc/Proposals/0001-stub-api.md | 1135 ------- Sources/GRPC/Docs.docc/index.md | 128 - Sources/GRPC/Error+NIOSSL.swift | 32 - ...oopFuture+RecoverFromUncleanShutdown.swift | 30 - Sources/GRPC/FakeChannel.swift | 187 -- .../GRPCChannel/ClientConnection+NIOSSL.swift | 101 - .../GRPCChannel/ClientConnection+NWTLS.swift | 70 - Sources/GRPC/GRPCChannel/GRPCChannel.swift | 304 -- .../GRPC/GRPCChannel/GRPCChannelBuilder.swift | 332 -- Sources/GRPC/GRPCClient.swift | 236 -- Sources/GRPC/GRPCClientChannelHandler.swift | 658 ---- Sources/GRPC/GRPCClientStateMachine.swift | 798 ----- Sources/GRPC/GRPCContentType.swift | 59 - Sources/GRPC/GRPCError.swift | 370 --- Sources/GRPC/GRPCHeaderName.swift | 24 - Sources/GRPC/GRPCIdleHandler.swift | 413 --- .../GRPC/GRPCIdleHandlerStateMachine.swift | 725 ----- Sources/GRPC/GRPCKeepaliveHandlers.swift | 256 -- Sources/GRPC/GRPCPayload.swift | 33 - .../GRPC/GRPCServerPipelineConfigurator.swift | 511 --- .../GRPCServerRequestRoutingHandler.swift | 117 - Sources/GRPC/GRPCServiceDescription.swift | 58 - Sources/GRPC/GRPCStatus.swift | 362 --- Sources/GRPC/GRPCStatusAndMetadata.swift | 32 - .../GRPC/GRPCStatusMessageMarshaller.swift | 206 -- Sources/GRPC/GRPCTLSConfiguration.swift | 716 ----- Sources/GRPC/GRPCTimeout.swift | 155 - Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift | 788 ----- Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift | 365 --- Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift | 1271 -------- .../ClientInterceptorContext.swift | 114 - .../ClientInterceptorPipeline.swift | 514 --- .../ClientInterceptorProtocol.swift | 46 - .../GRPC/Interceptor/ClientInterceptors.swift | 99 - .../GRPC/Interceptor/ClientTransport.swift | 1061 ------ .../Interceptor/ClientTransportFactory.swift | 374 --- Sources/GRPC/Interceptor/MessageParts.swift | 106 - .../ServerInterceptorContext.swift | 115 - .../ServerInterceptorPipeline.swift | 298 -- .../GRPC/Interceptor/ServerInterceptors.swift | 71 - Sources/GRPC/InterceptorContextList.swift | 65 - .../GRPC/LengthPrefixedMessageReader.swift | 225 -- Sources/GRPC/Logger.swift | 51 - Sources/GRPC/LoggingServerErrorDelegate.swift | 32 - .../GRPC/MessageEncodingHeaderValidator.swift | 92 - Sources/GRPC/PlatformSupport.swift | 430 --- Sources/GRPC/ReadWriteStates.swift | 233 -- Sources/GRPC/Ref.swift | 26 - Sources/GRPC/Serialization.swift | 158 - Sources/GRPC/Server+NIOSSL.swift | 125 - Sources/GRPC/Server.swift | 602 ---- Sources/GRPC/ServerBuilder+NIOSSL.swift | 81 - Sources/GRPC/ServerBuilder.swift | 260 -- .../ServerCallContext.swift | 182 -- .../StreamingResponseCallContext.swift | 264 -- .../UnaryResponseCallContext.swift | 122 - Sources/GRPC/ServerChannelErrorHandler.swift | 45 - Sources/GRPC/ServerErrorDelegate.swift | 83 - Sources/GRPC/ServerErrorProcessor.swift | 93 - Sources/GRPC/Stopwatch.swift | 38 - Sources/GRPC/StreamEvent.swift | 23 - Sources/GRPC/TLSVerificationHandler.swift | 65 - Sources/GRPC/TLSVersion.swift | 134 - Sources/GRPC/TimeLimit.swift | 121 - Sources/GRPC/UserInfo.swift | 110 - Sources/GRPC/Version.swift | 29 - Sources/GRPC/WebCORSHandler.swift | 210 -- Sources/GRPC/WriteCapturingHandler.swift | 61 - Sources/GRPC/_EmbeddedThroughput.swift | 67 - Sources/GRPC/_FakeResponseStream.swift | 350 -- Sources/GRPC/_GRPCClientCodecHandler.swift | 176 - Sources/GRPC/_MessageContext.swift | 34 - .../README.md | 22 - .../main.swift | 124 - .../Connection/ClientConnectionHandler.swift | 681 ---- .../Client/Connection/Connection.swift | 498 --- .../Client/Connection/ConnectionBackoff.swift | 87 - .../Client/Connection/ConnectionFactory.swift | 46 - .../Client/Connection/ConnectivityState.swift | 45 - .../Client/Connection/GRPCChannel.swift | 957 ------ .../LoadBalancers/LoadBalancer.swift | 73 - .../LoadBalancers/LoadBalancerEvent.swift | 23 - .../LoadBalancers/PickFirstLoadBalancer.swift | 610 ---- .../RoundRobinLoadBalancer.swift | 764 ----- .../Connection/LoadBalancers/Subchannel.swift | 680 ---- .../Client/Connection/RequestQueue.swift | 105 - .../Client/GRPCClientStreamHandler.swift | 302 -- .../Client/HTTP2ClientTransport.swift | 161 - .../Client/Resolver/NameResolver+IPv4.swift | 75 - .../Client/Resolver/NameResolver+IPv6.swift | 75 - .../Client/Resolver/NameResolver+UDS.swift | 60 - .../Client/Resolver/NameResolver+VSOCK.swift | 64 - .../Client/Resolver/NameResolver.swift | 136 - .../Resolver/NameResolverRegistry.swift | 200 -- .../Client/Resolver/SocketAddress.swift | 313 -- .../Compression/CompressionAlgorithm.swift | 53 - Sources/GRPCHTTP2Core/Compression/Zlib.swift | 419 --- .../GRPCHTTP2Core/GRPCMessageDecoder.swift | 153 - Sources/GRPCHTTP2Core/GRPCMessageFramer.swift | 116 - .../GRPCStreamStateMachine.swift | 1928 ----------- .../Internal/ConstantAsyncSequence.swift | 49 - .../GRPCHTTP2Core/Internal/ContentType.swift | 40 - ...iscardingTaskGroup+CancellableHandle.swift | 79 - .../GRPCStatusMessageMarshaller.swift | 205 -- .../Internal/NIOChannelPipeline+GRPC.swift | 185 -- .../NIOSocketAddress+GRPCSocketAddress.swift | 69 - .../Internal/ProcessUniqueID.swift | 61 - .../Internal/Result+Catching.swift | 30 - Sources/GRPCHTTP2Core/Internal/Timer.swift | 70 - .../ListeningServerTransport.swift | 41 - Sources/GRPCHTTP2Core/OneOrManyQueue.swift | 165 - .../Server/CommonHTTP2ServerTransport.swift | 264 -- .../GRPCServerFlushNotificationHandler.swift | 36 - .../Server/Connection/ServerConnection.swift | 55 - ...ectionManagementHandler+StateMachine.swift | 395 --- .../ServerConnectionManagementHandler.swift | 556 ---- .../Server/GRPCServerStreamHandler.swift | 291 -- .../Server/HTTP2ListenerFactory.swift | 35 - .../Server/HTTP2ServerTransport.swift | 191 -- Sources/GRPCHTTP2Transport/Exports.swift | 20 - .../GRPCHTTP2TransportNIOPosix/Exports.swift | 18 - .../HTTP2ClientTransport+Posix.swift | 298 -- .../HTTP2ServerTransport+Posix.swift | 286 -- .../NIOClientBootstrap+SocketAddress.swift | 55 - .../NIOSSL+GRPC.swift | 164 - .../TLSConfig.swift | 299 -- .../Exports.swift | 18 - ...TP2ClientTransport+TransportServices.swift | 339 -- ...TP2ServerTransport+TransportServices.swift | 274 -- .../TLSConfig.swift | 94 - .../ClientTracingInterceptor.swift | 142 - Sources/GRPCInterceptors/HookedWriter.swift | 46 - .../OnFinishAsyncSequence.swift | 57 - .../ServerTracingInterceptor.swift | 148 - .../Generated/empty.pb.swift | 79 - .../Generated/messages.pb.swift | 950 ------ .../Generated/test.grpc.swift | 1742 ---------- .../Generated/test.pb.swift | 38 - .../GRPCInteroperabilityTestModels/README.md | 12 - .../generate.sh | 43 - .../src/proto/grpc/testing/empty.proto | 28 - .../proto/grpc/testing/empty_service.proto | 23 - .../src/proto/grpc/testing/messages.proto | 165 - .../src/proto/grpc/testing/test.proto | 79 - .../unimplemented_call.patch | 59 - Sources/GRPCInteroperabilityTests/main.swift | 171 - .../Assertions.swift | 76 - .../GRPCTestingConvenienceMethods.swift | 120 - .../InteroperabilityTestCase.swift | 169 - .../InteroperabilityTestCases.swift | 1056 ------ ...InteroperabilityTestClientConnection.swift | 44 - .../InteroperabilityTestCredentials.swift | 109 - .../InteroperabilityTestServer.swift | 74 - .../ServerFeatures.swift | 75 - .../TestServiceAsyncProvider.swift | 218 -- .../TestServiceProvider.swift | 249 -- Sources/GRPCPerformanceTests/Benchmark.swift | 30 - .../GRPCPerformanceTests/BenchmarkUtils.swift | 91 - .../Benchmarks/EmbeddedClientThroughput.swift | 157 - .../Benchmarks/EmbeddedServer.swift | 151 - .../Benchmarks/MinimalEchoProvider.swift | 76 - .../Benchmarks/PercentEncoding.swift | 55 - .../Benchmarks/ServerProvidingBenchmark.swift | 77 - .../Benchmarks/UnaryThroughput.swift | 129 - .../Benchmarks/echo.grpc.swift | 1 - .../Benchmarks/echo.pb.swift | 1 - Sources/GRPCPerformanceTests/main.swift | 274 -- Sources/GRPCProtobuf/Coding.swift | 61 - .../ProtobufCodeGenParser.swift | 204 -- .../ProtobufCodeGenerator.swift | 46 - .../ReflectionServiceTutorial.md | 227 -- .../Server/ReflectionService.swift | 382 --- .../Server/ReflectionServiceV1.swift | 207 -- .../Server/ReflectionServiceV1Alpha.swift | 226 -- .../v1/reflection-v1.grpc.swift | 122 - .../v1/reflection-v1.pb.swift | 775 ----- .../v1Alpha/reflection-v1alpha.grpc.swift | 122 - .../v1Alpha/reflection-v1alpha.pb.swift | 788 ----- .../GRPCSampleData/GRPCSwiftCertificate.swift | 364 --- Sources/GRPCSampleData/bundle.p12 | Bin 2405 -> 0 bytes .../AssertionFailure.swift | 57 - .../Generated/empty.pb.swift | 75 - .../Generated/empty_service.grpc.swift | 93 - .../Generated/empty_service.pb.swift | 25 - .../Generated/messages.pb.swift | 929 ------ .../Generated/test.grpc.swift | 1413 -------- .../Generated/test.pb.swift | 28 - .../InteroperabilityTestCase.swift | 107 - .../InteroperabilityTestCases.swift | 996 ------ .../InteroperabilityTests/TestService.swift | 244 -- .../Health/Generated/health.grpc.swift | 419 --- .../Services/Health/Generated/health.pb.swift | 183 -- Sources/Services/Health/Health.swift | 127 - Sources/Services/Health/HealthService.swift | 133 - Sources/Services/Health/ServingStatus.swift | 38 - .../InteroperabilityTestsExecutable.swift | 144 - .../performance-worker/BenchmarkClient.swift | 242 -- .../performance-worker/BenchmarkService.swift | 189 -- .../Generated/grpc_core_stats.pb.swift | 286 -- .../grpc_testing_benchmark_service.grpc.swift | 617 ---- .../grpc_testing_benchmark_service.pb.swift | 28 - .../Generated/grpc_testing_control.pb.swift | 2325 ------------- .../Generated/grpc_testing_messages.pb.swift | 2140 ------------ .../Generated/grpc_testing_payloads.pb.swift | 305 -- .../Generated/grpc_testing_stats.pb.swift | 462 --- .../grpc_testing_worker_service.grpc.swift | 235 -- .../grpc_testing_worker_service.pb.swift | 28 - .../PerformanceWorker.swift | 71 - Sources/performance-worker/RPCStats.swift | 140 - .../performance-worker/ResourceUsage.swift | 176 - .../performance-worker/WorkerService.swift | 584 ---- .../Docs.docc/spm-plugin.md | 152 - .../protoc-gen-grpc-swift/GenerateGRPC.swift | 235 -- .../Generator-Client+AsyncAwait.swift | 246 -- .../Generator-Client.swift | 688 ---- .../Generator-Metadata.swift | 82 - .../Generator-Names.swift | 181 -- .../Generator-Server+AsyncAwait.swift | 183 -- .../Generator-Server.swift | 205 -- Sources/protoc-gen-grpc-swift/Generator.swift | 159 - Sources/protoc-gen-grpc-swift/Options.swift | 217 -- Sources/protoc-gen-grpc-swift/README.md | 17 - .../protoc-gen-grpc-swift/StreamingType.swift | 54 - Sources/protoc-gen-grpc-swift/Types.swift | 68 - Sources/protoc-gen-grpc-swift/Version.swift | 1 - ...ntConnectionHandlerStateMachineTests.swift | 115 - .../ClientConnectionHandlerTests.swift | 405 --- .../Connection/Connection+Equatable.swift | 105 - .../Connection/ConnectionBackoffTests.swift | 71 - .../Client/Connection/ConnectionTests.swift | 255 -- .../Client/Connection/GRPCChannelTests.swift | 842 ----- .../LoadBalancers/LoadBalancerTest.swift | 185 -- .../PickFirstLoadBalancerTests.swift | 334 -- .../RoundRobinLoadBalancerTests.swift | 379 --- .../LoadBalancers/SubchannelTests.swift | 581 ---- .../Client/Connection/RequestQueueTests.swift | 274 -- .../Connection/Utilities/ConnectionTest.swift | 219 -- .../Utilities/HTTP2Connectors.swift | 161 - .../Connection/Utilities/NameResolvers.swift | 74 - .../Connection/Utilities/TestServer.swift | 178 - .../Client/GRPCClientStreamHandlerTests.swift | 942 ------ .../HTTP2ClientTransportConfigTests.swift | 47 - .../Resolver/NameResolverRegistryTests.swift | 271 -- .../Client/Resolver/SocketAddressTests.swift | 80 - .../GRPCMessageDecoderTests.swift | 237 -- .../GRPCMessageDeframerTests.swift | 106 - .../GRPCMessageFramerTests.swift | 129 - .../GRPCStreamStateMachineTests.swift | 2864 ----------------- .../GRPCStatusMessageMarshallerTests.swift | 44 - .../Internal/ProcessUniqueIDTests.swift | 58 - .../Internal/TimerTests.swift | 98 - .../OneOrManyQueueTests.swift | 140 - .../Server/Compression/ZlibTests.swift | 145 - ...nManagementHandler+StateMachineTests.swift | 240 -- ...rverConnectionManagementHandlerTests.swift | 410 --- .../Server/GRPCServerStreamHandlerTests.swift | 1075 ------- .../HTTP2ServerTransportConfigTests.swift | 53 - .../Test Utilities/AtomicCounter.swift | 35 - .../MethodDescriptor+Common.swift | 34 - .../Test Utilities/Task+Poll.swift | 37 - .../Test Utilities/XCTest+Utilities.swift | 81 - .../XCTest+FramePayload.swift | 43 - .../ControlService.swift | 200 -- .../Generated/control.grpc.swift | 490 --- .../Generated/control.pb.swift | 446 --- .../HTTP2TransportNIOPosixTests.swift | 483 --- ...P2TransportNIOTransportServicesTests.swift | 235 -- .../HTTP2TransportTests.swift | 1507 --------- .../HTTP2StatusCodeServer.swift | 113 - .../Test Utilities/XCTest+Utilities.swift | 30 - .../XCTestCase+Vsock.swift | 34 - .../TracingInterceptorTests.swift | 334 -- .../TracingTestsUtilities.swift | 195 -- .../ProtobufCodeGenParserTests.swift | 443 --- .../ProtobufCodeGeneratorTests.swift | 628 ---- .../ProtobufCodingTests.swift | 98 - Tests/GRPCTests/ALPNConfigurationTests.swift | 104 - Tests/GRPCTests/AnyServiceClientTests.swift | 74 - .../GRPCTests/Array+BoundsCheckingTests.swift | 43 - .../AsyncAwaitSupport/AsyncClientTests.swift | 539 ---- .../AsyncIntegrationTests.swift | 205 -- .../AsyncSequence+Helpers.swift | 27 - .../ServerHandlerStateMachineTests.swift | 335 -- ...erceptorStateMachineStreamStateTests.swift | 129 - .../ServerInterceptorStateMachineTests.swift | 188 -- .../GRPCAsyncRequestStreamTests.swift | 31 - .../GRPCAsyncResponseStreamWriterTests.swift | 32 - .../InterceptorsAsyncTests.swift | 170 - .../AsyncAwaitSupport/XCTest+AsyncAwait.swift | 84 - Tests/GRPCTests/BasicEchoTestCase.swift | 223 -- Tests/GRPCTests/CallPathTests.swift | 69 - Tests/GRPCTests/CallStartBehaviorTests.swift | 45 - Tests/GRPCTests/CapturingLogHandler.swift | 172 - Tests/GRPCTests/ClientCallTests.swift | 249 -- Tests/GRPCTests/ClientCancellingTests.swift | 93 - .../GRPCTests/ClientClosedChannelTests.swift | 172 - .../ClientConnectionBackoffTests.swift | 254 -- .../ClientEventLoopPreferenceTests.swift | 142 - .../ClientInterceptorPipelineTests.swift | 402 --- Tests/GRPCTests/ClientQuiescingTests.swift | 507 --- Tests/GRPCTests/ClientTLSFailureTests.swift | 231 -- Tests/GRPCTests/ClientTLSTests.swift | 217 -- Tests/GRPCTests/ClientTimeoutTests.swift | 261 -- Tests/GRPCTests/ClientTransportTests.swift | 384 --- ...cingLengthPrefixedMessageWriterTests.swift | 223 -- .../Normalization/NormalizationProvider.swift | 131 - .../Normalization/NormalizationTests.swift | 109 - .../Normalization/normalization.grpc.swift | 1037 ------ .../Normalization/normalization.pb.swift | 84 - .../Serialization/SerializationTests.swift | 83 - .../Serialization/echo.grpc.reflection | 1 - Tests/GRPCTests/CompressionTests.swift | 413 --- Tests/GRPCTests/ConfigurationTests.swift | 98 - Tests/GRPCTests/ConnectionBackoffTests.swift | 97 - Tests/GRPCTests/ConnectionFailingTests.swift | 64 - Tests/GRPCTests/ConnectionManagerTests.swift | 1573 --------- .../ConnectionPoolDelegates.swift | 207 -- .../ConnectionPool/ConnectionPoolTests.swift | 1338 -------- .../ConnectionPool/GRPCChannelPoolTests.swift | 622 ---- .../PoolManagerStateMachineTests.swift | 372 --- .../ConnectivityStateMonitorTests.swift | 51 - .../DebugChannelInitializerTests.swift | 65 - .../DelegatingErrorHandlerTests.swift | 50 - .../EchoHelpers/EchoMessageHelpers.swift | 24 - .../DelegatingClientInterceptor.swift | 82 - .../EchoInterceptorFactories.swift | 79 - .../DelegatingOnCloseEchoProvider.swift | 67 - .../Providers/FailingEchoProvider.swift | 53 - .../Providers/MetadataEchoProvider.swift | 54 - .../NeverResolvingEchoProvider.swift | 62 - Tests/GRPCTests/EchoMetadataTests.swift | 108 - Tests/GRPCTests/EchoTestClientTests.swift | 214 -- Tests/GRPCTests/ErrorRecordingDelegate.swift | 61 - .../EventLoopFuture+Assertions.swift | 108 - Tests/GRPCTests/FakeChannelTests.swift | 155 - Tests/GRPCTests/FakeResponseStreamTests.swift | 258 -- Tests/GRPCTests/FunctionalTests.swift | 569 ---- .../GRPCTests/GRPCAsyncClientCallTests.swift | 396 --- .../GRPCAsyncServerHandlerTests.swift | 619 ---- .../GRPCClientChannelHandlerTests.swift | 98 - .../GRPCClientStateMachineTests.swift | 1387 -------- Tests/GRPCTests/GRPCCustomPayloadTests.swift | 334 -- .../GRPCIdleHandlerStateMachineTests.swift | 635 ---- Tests/GRPCTests/GRPCIdleTests.swift | 89 - .../GRPCTests/GRPCInteroperabilityTests.swift | 419 --- Tests/GRPCTests/GRPCKeepaliveTests.swift | 83 - .../GRPCMessageLengthLimitTests.swift | 162 - .../GRPCTests/GRPCNetworkFrameworkTests.swift | 271 -- Tests/GRPCTests/GRPCPingHandlerTests.swift | 481 --- .../Generated/v1/reflection-v1.grpc.swift | 208 -- .../Generated/v1/reflection-v1.pb.swift | 775 ----- .../v1Alpha/reflection-v1alpha.grpc.swift | 208 -- .../v1Alpha/reflection-v1alpha.pb.swift | 788 ----- .../ReflectionServiceIntegrationTests.swift | 341 -- .../ReflectionServiceUnitTests.swift | 615 ---- .../GRPCReflectionServiceTests/Utils.swift | 347 -- .../GRPCServerPipelineConfiguratorTests.swift | 273 -- Tests/GRPCTests/GRPCStatusCodeTests.swift | 215 -- .../GRPCStatusMessageMarshallerTests.swift | 44 - Tests/GRPCTests/GRPCStatusTests.swift | 139 - Tests/GRPCTests/GRPCTestCase.swift | 116 - Tests/GRPCTests/GRPCTimeoutTests.swift | 132 - Tests/GRPCTests/GRPCTypeSizeTests.swift | 49 - .../GRPCWebToHTTP2ServerCodecTests.swift | 205 -- .../GRPCWebToHTTP2StateMachineTests.swift | 693 ---- .../HTTP2MaxConcurrentStreamsTests.swift | 93 - .../HTTP2ToRawGRPCStateMachineTests.swift | 795 ----- Tests/GRPCTests/HTTPVersionParserTests.swift | 87 - .../GRPCTests/HeaderNormalizationTests.swift | 177 - .../ImmediateServerFailureTests.swift | 100 - .../InterceptedRPCCancellationTests.swift | 188 -- Tests/GRPCTests/InterceptorsTests.swift | 444 --- .../GRPCTests/LazyEventLoopPromiseTests.swift | 86 - .../LengthPrefixedMessageReaderTests.swift | 283 -- .../MessageEncodingHeaderValidatorTests.swift | 113 - Tests/GRPCTests/MutualTLSTests.swift | 272 -- Tests/GRPCTests/OneOrManyQueueTests.swift | 140 - Tests/GRPCTests/PlatformSupportTests.swift | 264 -- Tests/GRPCTests/RequestIDProviderTests.swift | 39 - Tests/GRPCTests/RequestIDTests.swift | 85 - .../SampleCertificate+Assertions.swift | 32 - .../GRPCTests/ServerErrorDelegateTests.swift | 170 - .../ServerFuzzingRegressionTests.swift | 93 - .../ServerInterceptorPipelineTests.swift | 217 -- Tests/GRPCTests/ServerInterceptorTests.swift | 390 --- Tests/GRPCTests/ServerOnCloseTests.swift | 249 -- Tests/GRPCTests/ServerQuiescingTests.swift | 91 - Tests/GRPCTests/ServerTLSErrorTests.swift | 211 -- Tests/GRPCTests/ServerThrowingTests.swift | 282 -- Tests/GRPCTests/ServerWebTests.swift | 180 -- Tests/GRPCTests/StopwatchTests.swift | 38 - ...treamResponseHandlerRetainCycleTests.swift | 90 - .../StreamingRequestClientCallTests.swift | 58 - Tests/GRPCTests/TestClientExample.swift | 454 --- Tests/GRPCTests/TimeLimitTests.swift | 43 - Tests/GRPCTests/UnaryServerHandlerTests.swift | 1048 ------ Tests/GRPCTests/UserInfoTests.swift | 87 - Tests/GRPCTests/VsockSocketTests.swift | 67 - Tests/GRPCTests/WebCORSHandlerTests.swift | 380 --- .../GRPCTests/WithConnectedSocketTests.swift | 70 - Tests/GRPCTests/XCTestHelpers.swift | 699 ---- Tests/GRPCTests/ZeroLengthWriteTests.swift | 268 -- Tests/GRPCTests/ZlibTests.swift | 210 -- .../InProcessInteroperabilityTests.swift | 98 - Tests/Services/HealthTests/HealthTests.swift | 287 -- .../Test Utilities/XCTest+Utilities.swift | 30 - {Protos => dev/Protos}/README.md | 0 .../Protos}/examples/echo/echo.proto | 0 .../examples/route_guide/route_guide.proto | 0 {Protos => dev/Protos}/fetch.sh | 11 - dev/Protos/generate.sh | 73 + .../Protos}/upstream/google/rpc/code.proto | 0 .../Protos}/upstream/grpc/core/stats.proto | 0 .../upstream/grpc/examples/helloworld.proto | 0 .../upstream/grpc/health/v1/health.proto | 0 .../Protos}/upstream/grpc/lookup/v1/rls.proto | 0 .../upstream/grpc/lookup/v1/rls_config.proto | 0 .../grpc/reflection/v1/reflection.proto | 0 .../grpc/reflection/v1alpha/reflection.proto | 0 .../grpc/service_config/service_config.proto | 0 .../grpc/testing/benchmark_service.proto | 0 .../upstream/grpc/testing/control.proto | 0 .../upstream/grpc/testing/messages.proto | 0 .../upstream/grpc/testing/payloads.proto | 0 .../Protos}/upstream/grpc/testing/stats.proto | 0 .../grpc/testing/worker_service.proto | 0 {scripts => dev}/check-generated-code.sh | 2 +- .../01-echo/generate-and-diff.sh | 36 - .../01-echo/golden/echo.grpc.swift | 246 -- dev/codegen-tests/01-echo/proto/echo.proto | 1 - .../02-multifile/generate-and-diff.sh | 55 - .../02-multifile/golden/a.grpc.swift | 113 - .../02-multifile/golden/b.grpc.swift | 113 - dev/codegen-tests/02-multifile/proto/a.proto | 27 - dev/codegen-tests/02-multifile/proto/b.proto | 26 - .../generate-and-diff.sh | 60 - .../golden/a.grpc.swift | 114 - .../golden/b.grpc.swift | 113 - .../proto/a.proto | 1 - .../proto/b.proto | 1 - .../swift.modulemap | 8 - .../generate-and-diff.sh | 36 - .../golden/service.grpc.swift | 113 - .../proto/message.proto | 18 - .../proto/service.proto | 22 - .../05-service-only/generate-and-diff.sh | 36 - .../05-service-only/golden/test.grpc.swift | 113 - .../05-service-only/proto/test.proto | 22 - .../06-test-client-only/generate-and-diff.sh | 37 - .../golden/test.grpc.swift | 56 - .../06-test-client-only/proto/test.proto | 28 - dev/codegen-tests/README.md | 30 - dev/codegen-tests/run-tests.sh | 21 - dev/codegen-tests/test-boilerplate.sh | 41 - {scripts => dev}/format.sh | 5 +- {scripts => dev}/license-check.sh | 6 - {scripts => dev}/sanity.sh | 0 docs/api.md | 144 - docs/apple-platforms.md | 27 - docs/async-await-proposal.md | 473 --- docs/basic-tutorial.md | 614 ---- docs/client-without-codegen.md | 30 - docs/faqs.md | 159 - docs/interceptors-tutorial.md | 296 -- docs/keepalive.md | 62 - docs/plugin.md | 124 - docs/quick-start.md | 239 -- docs/tls.md | 95 - scripts/alloc-limits.sh | 35 - scripts/bundle-plugins-for-release.sh | 67 - scripts/cg_diff.py | 338 -- scripts/fix-project-settings.rb | 26 - scripts/make-sample-certs.py | 261 -- scripts/makecert | 47 - scripts/run-plugin-tests.sh | 137 - 687 files changed, 142 insertions(+), 138993 deletions(-) delete mode 100644 .github/workflows/release.yml delete mode 100644 AUTHORS delete mode 100644 Examples/v1/Echo/Implementation/EchoAsyncProvider.swift delete mode 100644 Examples/v1/Echo/Implementation/EchoProvider.swift delete mode 100644 Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift delete mode 100644 Examples/v1/Echo/Implementation/Interceptors.swift delete mode 100644 Examples/v1/Echo/Model/echo.grpc.swift delete mode 100644 Examples/v1/Echo/Model/echo.pb.swift delete mode 100644 Examples/v1/Echo/README.md delete mode 100644 Examples/v1/Echo/Runtime/Echo.swift delete mode 100644 Examples/v1/Echo/Runtime/Empty.swift delete mode 100644 Examples/v1/HelloWorld/Client/HelloWorldClient.swift delete mode 100644 Examples/v1/HelloWorld/Model/helloworld.grpc.swift delete mode 100644 Examples/v1/HelloWorld/Model/helloworld.pb.swift delete mode 100644 Examples/v1/HelloWorld/README.md delete mode 100644 Examples/v1/HelloWorld/Server/GreeterProvider.swift delete mode 100644 Examples/v1/HelloWorld/Server/HelloWorldServer.swift delete mode 100644 Examples/v1/PacketCapture/Empty.swift delete mode 100644 Examples/v1/PacketCapture/PacketCapture.swift delete mode 100644 Examples/v1/PacketCapture/README.md delete mode 100644 Examples/v1/README.md delete mode 100644 Examples/v1/ReflectionService/Generated/echo.grpc.reflection delete mode 100644 Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection delete mode 120000 Examples/v1/ReflectionService/GreeterProvider.swift delete mode 100644 Examples/v1/ReflectionService/ReflectionServer.swift delete mode 100644 Examples/v1/RouteGuide/Client/Empty.swift delete mode 100644 Examples/v1/RouteGuide/Client/RouteGuideClient.swift delete mode 100644 Examples/v1/RouteGuide/Model/route_guide.grpc.swift delete mode 100644 Examples/v1/RouteGuide/Model/route_guide.pb.swift delete mode 100644 Examples/v1/RouteGuide/README.md delete mode 100644 Examples/v1/RouteGuide/Server/RouteGuideProvider.swift delete mode 100644 Examples/v1/RouteGuide/Server/RouteGuideServer.swift delete mode 100644 Examples/v1/RouteGuide/route_guide_db.json delete mode 100644 FuzzTesting/.gitignore delete mode 100644 FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-4739158818553856 delete mode 100644 FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5077460227063808 delete mode 100644 FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5134158417494016 delete mode 100644 FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5285159577452544 delete mode 100644 FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5448955772141568 delete mode 100644 FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-debug-4645975625957376 delete mode 100644 FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-release-5413100925878272 delete mode 100644 FuzzTesting/Package.swift delete mode 100644 FuzzTesting/README.md delete mode 120000 FuzzTesting/Sources/EchoImplementation delete mode 120000 FuzzTesting/Sources/EchoModel delete mode 100644 FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c delete mode 100644 FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift rename {Performance => IntegrationTests}/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift (100%) rename {Performance => IntegrationTests}/Benchmarks/Package.swift (95%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json (100%) rename {Performance => IntegrationTests}/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json (100%) delete mode 100644 PATENTS delete mode 100644 Package@swift-6.swift delete mode 100644 Performance/QPSBenchmark/.gitignore delete mode 100644 Performance/QPSBenchmark/Package.swift delete mode 100644 Performance/QPSBenchmark/README.md delete mode 100644 Performance/QPSBenchmark/Sources/BenchmarkUtils/Histogram.swift delete mode 100644 Performance/QPSBenchmark/Sources/BenchmarkUtils/StatusCounts.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.proto delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.proto delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.proto delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ClientUtilities.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/HistogramSerialisation.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOBenchmarkServiceImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOClientProtocol.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOPingPongRequestMaker.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSClientImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSServerImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIORequestMaker.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOServerProtocol.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOUnaryRequestMaker.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOWorkerServiceImpl.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ResourceUsage.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerTypeExtensions.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerUtilities.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/StatusCountsSerialisation.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/grpcTime.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json delete mode 100644 Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json delete mode 100644 Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/HistogramTests.swift delete mode 100644 Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/StatusCountsTests.swift delete mode 100644 Performance/QPSBenchmark/scenarios/bidirectional-ping-pong-1-connection.json delete mode 100644 Performance/QPSBenchmark/scenarios/unary-1-connection.json delete mode 100644 Performance/QPSBenchmark/scenarios/unary-unconstrained.json delete mode 100755 Performance/allocations/test-allocation-counts.sh delete mode 100755 Performance/allocations/test-utils.sh delete mode 100755 Performance/allocations/tests/run-allocation-counter-tests.sh delete mode 120000 Performance/allocations/tests/shared/Benchmark.swift delete mode 100644 Performance/allocations/tests/shared/Common.swift delete mode 120000 Performance/allocations/tests/shared/EmbeddedServer.swift delete mode 120000 Performance/allocations/tests/shared/MinimalEchoProvider.swift delete mode 120000 Performance/allocations/tests/shared/echo.grpc.swift delete mode 120000 Performance/allocations/tests/shared/echo.pb.swift delete mode 100644 Performance/allocations/tests/test_bidi_1k_rpcs.swift delete mode 100644 Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_10_small_requests.swift delete mode 100644 Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_1_small_request.swift delete mode 100644 Performance/allocations/tests/test_embedded_server_unary_1k_rpcs_1_small_request.swift delete mode 100644 Performance/allocations/tests/test_unary_1k_ping_pong.swift delete mode 100644 Plugins/GRPCSwiftPlugin/plugin.swift delete mode 100755 Protos/generate.sh delete mode 100644 Protos/tests/control/control.proto delete mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/empty.proto delete mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto delete mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/messages.proto delete mode 100644 Protos/tests/interoperability/src/proto/grpc/testing/test.proto delete mode 100644 Protos/tests/normalization/normalization.proto delete mode 100644 Sources/CGRPCZlib/empty.c delete mode 100644 Sources/CGRPCZlib/include/CGRPCZlib.h delete mode 100644 Sources/GRPC/Array+BoundsCheck.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift delete mode 100644 Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift delete mode 100644 Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift delete mode 100644 Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift delete mode 100644 Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift delete mode 100644 Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift delete mode 100644 Sources/GRPC/CallHandlers/UnaryServerHandler.swift delete mode 100644 Sources/GRPC/CallOptions.swift delete mode 100644 Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift delete mode 100644 Sources/GRPC/ClientCalls/Call.swift delete mode 100644 Sources/GRPC/ClientCalls/CallDetails.swift delete mode 100644 Sources/GRPC/ClientCalls/ClientCall.swift delete mode 100644 Sources/GRPC/ClientCalls/ClientStreamingCall.swift delete mode 100644 Sources/GRPC/ClientCalls/LazyEventLoopPromise.swift delete mode 100644 Sources/GRPC/ClientCalls/ResponseContainers.swift delete mode 100644 Sources/GRPC/ClientCalls/ResponsePartContainer.swift delete mode 100644 Sources/GRPC/ClientCalls/ServerStreamingCall.swift delete mode 100644 Sources/GRPC/ClientCalls/UnaryCall.swift delete mode 100644 Sources/GRPC/ClientConnection.swift delete mode 100644 Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift delete mode 100644 Sources/GRPC/ClientErrorDelegate.swift delete mode 100644 Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift delete mode 100644 Sources/GRPC/Compression/CompressionAlgorithm.swift delete mode 100644 Sources/GRPC/Compression/DecompressionLimit.swift delete mode 100644 Sources/GRPC/Compression/MessageEncoding.swift delete mode 100644 Sources/GRPC/Compression/Zlib.swift delete mode 100644 Sources/GRPC/ConnectionBackoff.swift delete mode 100644 Sources/GRPC/ConnectionKeepalive.swift delete mode 100644 Sources/GRPC/ConnectionManager+Delegates.swift delete mode 100644 Sources/GRPC/ConnectionManager.swift delete mode 100644 Sources/GRPC/ConnectionManagerChannelProvider.swift delete mode 100644 Sources/GRPC/ConnectionPool/ConnectionManagerID.swift delete mode 100644 Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift delete mode 100644 Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift delete mode 100644 Sources/GRPC/ConnectionPool/ConnectionPool.swift delete mode 100644 Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift delete mode 100644 Sources/GRPC/ConnectionPool/GRPCChannelPool.swift delete mode 100644 Sources/GRPC/ConnectionPool/PoolManager.swift delete mode 100644 Sources/GRPC/ConnectionPool/PoolManagerStateMachine+PerPoolState.swift delete mode 100644 Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift delete mode 100644 Sources/GRPC/ConnectionPool/PooledChannel.swift delete mode 100644 Sources/GRPC/ConnectionPool/StreamLender.swift delete mode 100644 Sources/GRPC/ConnectivityState.swift delete mode 100644 Sources/GRPC/DebugOnly.swift delete mode 100644 Sources/GRPC/DelegatingErrorHandler.swift delete mode 100644 Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md delete mode 100644 Sources/GRPC/Docs.docc/index.md delete mode 100644 Sources/GRPC/Error+NIOSSL.swift delete mode 100644 Sources/GRPC/EventLoopFuture+RecoverFromUncleanShutdown.swift delete mode 100644 Sources/GRPC/FakeChannel.swift delete mode 100644 Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift delete mode 100644 Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift delete mode 100644 Sources/GRPC/GRPCChannel/GRPCChannel.swift delete mode 100644 Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift delete mode 100644 Sources/GRPC/GRPCClient.swift delete mode 100644 Sources/GRPC/GRPCClientChannelHandler.swift delete mode 100644 Sources/GRPC/GRPCClientStateMachine.swift delete mode 100644 Sources/GRPC/GRPCContentType.swift delete mode 100644 Sources/GRPC/GRPCError.swift delete mode 100644 Sources/GRPC/GRPCHeaderName.swift delete mode 100644 Sources/GRPC/GRPCIdleHandler.swift delete mode 100644 Sources/GRPC/GRPCIdleHandlerStateMachine.swift delete mode 100644 Sources/GRPC/GRPCKeepaliveHandlers.swift delete mode 100644 Sources/GRPC/GRPCPayload.swift delete mode 100644 Sources/GRPC/GRPCServerPipelineConfigurator.swift delete mode 100644 Sources/GRPC/GRPCServerRequestRoutingHandler.swift delete mode 100644 Sources/GRPC/GRPCServiceDescription.swift delete mode 100644 Sources/GRPC/GRPCStatus.swift delete mode 100644 Sources/GRPC/GRPCStatusAndMetadata.swift delete mode 100644 Sources/GRPC/GRPCStatusMessageMarshaller.swift delete mode 100644 Sources/GRPC/GRPCTLSConfiguration.swift delete mode 100644 Sources/GRPC/GRPCTimeout.swift delete mode 100644 Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift delete mode 100644 Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift delete mode 100644 Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift delete mode 100644 Sources/GRPC/Interceptor/ClientInterceptorContext.swift delete mode 100644 Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift delete mode 100644 Sources/GRPC/Interceptor/ClientInterceptorProtocol.swift delete mode 100644 Sources/GRPC/Interceptor/ClientInterceptors.swift delete mode 100644 Sources/GRPC/Interceptor/ClientTransport.swift delete mode 100644 Sources/GRPC/Interceptor/ClientTransportFactory.swift delete mode 100644 Sources/GRPC/Interceptor/MessageParts.swift delete mode 100644 Sources/GRPC/Interceptor/ServerInterceptorContext.swift delete mode 100644 Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift delete mode 100644 Sources/GRPC/Interceptor/ServerInterceptors.swift delete mode 100644 Sources/GRPC/InterceptorContextList.swift delete mode 100644 Sources/GRPC/LengthPrefixedMessageReader.swift delete mode 100644 Sources/GRPC/Logger.swift delete mode 100644 Sources/GRPC/LoggingServerErrorDelegate.swift delete mode 100644 Sources/GRPC/MessageEncodingHeaderValidator.swift delete mode 100644 Sources/GRPC/PlatformSupport.swift delete mode 100644 Sources/GRPC/ReadWriteStates.swift delete mode 100644 Sources/GRPC/Ref.swift delete mode 100644 Sources/GRPC/Serialization.swift delete mode 100644 Sources/GRPC/Server+NIOSSL.swift delete mode 100644 Sources/GRPC/Server.swift delete mode 100644 Sources/GRPC/ServerBuilder+NIOSSL.swift delete mode 100644 Sources/GRPC/ServerBuilder.swift delete mode 100644 Sources/GRPC/ServerCallContexts/ServerCallContext.swift delete mode 100644 Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift delete mode 100644 Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift delete mode 100644 Sources/GRPC/ServerChannelErrorHandler.swift delete mode 100644 Sources/GRPC/ServerErrorDelegate.swift delete mode 100644 Sources/GRPC/ServerErrorProcessor.swift delete mode 100644 Sources/GRPC/Stopwatch.swift delete mode 100644 Sources/GRPC/StreamEvent.swift delete mode 100644 Sources/GRPC/TLSVerificationHandler.swift delete mode 100644 Sources/GRPC/TLSVersion.swift delete mode 100644 Sources/GRPC/TimeLimit.swift delete mode 100644 Sources/GRPC/UserInfo.swift delete mode 100644 Sources/GRPC/Version.swift delete mode 100644 Sources/GRPC/WebCORSHandler.swift delete mode 100644 Sources/GRPC/WriteCapturingHandler.swift delete mode 100644 Sources/GRPC/_EmbeddedThroughput.swift delete mode 100644 Sources/GRPC/_FakeResponseStream.swift delete mode 100644 Sources/GRPC/_GRPCClientCodecHandler.swift delete mode 100644 Sources/GRPC/_MessageContext.swift delete mode 100644 Sources/GRPCConnectionBackoffInteropTest/README.md delete mode 100644 Sources/GRPCConnectionBackoffInteropTest/main.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/Connection.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift delete mode 100644 Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift delete mode 100644 Sources/GRPCHTTP2Core/Compression/Zlib.swift delete mode 100644 Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift delete mode 100644 Sources/GRPCHTTP2Core/GRPCMessageFramer.swift delete mode 100644 Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/ContentType.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/Result+Catching.swift delete mode 100644 Sources/GRPCHTTP2Core/Internal/Timer.swift delete mode 100644 Sources/GRPCHTTP2Core/ListeningServerTransport.swift delete mode 100644 Sources/GRPCHTTP2Core/OneOrManyQueue.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift delete mode 100644 Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift delete mode 100644 Sources/GRPCHTTP2Transport/Exports.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOPosix/Exports.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift delete mode 100644 Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift delete mode 100644 Sources/GRPCInterceptors/ClientTracingInterceptor.swift delete mode 100644 Sources/GRPCInterceptors/HookedWriter.swift delete mode 100644 Sources/GRPCInterceptors/OnFinishAsyncSequence.swift delete mode 100644 Sources/GRPCInterceptors/ServerTracingInterceptor.swift delete mode 100644 Sources/GRPCInteroperabilityTestModels/Generated/empty.pb.swift delete mode 100644 Sources/GRPCInteroperabilityTestModels/Generated/messages.pb.swift delete mode 100644 Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift delete mode 100644 Sources/GRPCInteroperabilityTestModels/Generated/test.pb.swift delete mode 100644 Sources/GRPCInteroperabilityTestModels/README.md delete mode 100755 Sources/GRPCInteroperabilityTestModels/generate.sh delete mode 100644 Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty.proto delete mode 100644 Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty_service.proto delete mode 100644 Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/messages.proto delete mode 100644 Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/test.proto delete mode 100644 Sources/GRPCInteroperabilityTestModels/unimplemented_call.patch delete mode 100644 Sources/GRPCInteroperabilityTests/main.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCase.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/ServerFeatures.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift delete mode 100644 Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift delete mode 100644 Sources/GRPCPerformanceTests/Benchmark.swift delete mode 100644 Sources/GRPCPerformanceTests/BenchmarkUtils.swift delete mode 100644 Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift delete mode 100644 Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift delete mode 100644 Sources/GRPCPerformanceTests/Benchmarks/MinimalEchoProvider.swift delete mode 100644 Sources/GRPCPerformanceTests/Benchmarks/PercentEncoding.swift delete mode 100644 Sources/GRPCPerformanceTests/Benchmarks/ServerProvidingBenchmark.swift delete mode 100644 Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift delete mode 120000 Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift delete mode 120000 Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift delete mode 100644 Sources/GRPCPerformanceTests/main.swift delete mode 100644 Sources/GRPCProtobuf/Coding.swift delete mode 100644 Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift delete mode 100644 Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift delete mode 100644 Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md delete mode 100644 Sources/GRPCReflectionService/Server/ReflectionService.swift delete mode 100644 Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift delete mode 100644 Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift delete mode 100644 Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift delete mode 100644 Sources/GRPCReflectionService/v1/reflection-v1.pb.swift delete mode 100644 Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift delete mode 100644 Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift delete mode 100644 Sources/GRPCSampleData/GRPCSwiftCertificate.swift delete mode 100644 Sources/GRPCSampleData/bundle.p12 delete mode 100644 Sources/InteroperabilityTests/AssertionFailure.swift delete mode 100644 Sources/InteroperabilityTests/Generated/empty.pb.swift delete mode 100644 Sources/InteroperabilityTests/Generated/empty_service.grpc.swift delete mode 100644 Sources/InteroperabilityTests/Generated/empty_service.pb.swift delete mode 100644 Sources/InteroperabilityTests/Generated/messages.pb.swift delete mode 100644 Sources/InteroperabilityTests/Generated/test.grpc.swift delete mode 100644 Sources/InteroperabilityTests/Generated/test.pb.swift delete mode 100644 Sources/InteroperabilityTests/InteroperabilityTestCase.swift delete mode 100644 Sources/InteroperabilityTests/InteroperabilityTestCases.swift delete mode 100644 Sources/InteroperabilityTests/TestService.swift delete mode 100644 Sources/Services/Health/Generated/health.grpc.swift delete mode 100644 Sources/Services/Health/Generated/health.pb.swift delete mode 100644 Sources/Services/Health/Health.swift delete mode 100644 Sources/Services/Health/HealthService.swift delete mode 100644 Sources/Services/Health/ServingStatus.swift delete mode 100644 Sources/interoperability-tests/InteroperabilityTestsExecutable.swift delete mode 100644 Sources/performance-worker/BenchmarkClient.swift delete mode 100644 Sources/performance-worker/BenchmarkService.swift delete mode 100644 Sources/performance-worker/Generated/grpc_core_stats.pb.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_control.pb.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_messages.pb.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_stats.pb.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift delete mode 100644 Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift delete mode 100644 Sources/performance-worker/PerformanceWorker.swift delete mode 100644 Sources/performance-worker/RPCStats.swift delete mode 100644 Sources/performance-worker/ResourceUsage.swift delete mode 100644 Sources/performance-worker/WorkerService.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md delete mode 100644 Sources/protoc-gen-grpc-swift/GenerateGRPC.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Generator-Client.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Generator-Metadata.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Generator-Names.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Generator-Server.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Generator.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Options.swift delete mode 100644 Sources/protoc-gen-grpc-swift/README.md delete mode 100644 Sources/protoc-gen-grpc-swift/StreamingType.swift delete mode 100644 Sources/protoc-gen-grpc-swift/Types.swift delete mode 120000 Sources/protoc-gen-grpc-swift/Version.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift delete mode 100644 Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/ControlService.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift delete mode 100644 Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift delete mode 100644 Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift delete mode 100644 Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift delete mode 100644 Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift delete mode 100644 Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift delete mode 100644 Tests/GRPCProtobufTests/ProtobufCodingTests.swift delete mode 100644 Tests/GRPCTests/ALPNConfigurationTests.swift delete mode 100644 Tests/GRPCTests/AnyServiceClientTests.swift delete mode 100644 Tests/GRPCTests/Array+BoundsCheckingTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift delete mode 100644 Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift delete mode 100644 Tests/GRPCTests/BasicEchoTestCase.swift delete mode 100644 Tests/GRPCTests/CallPathTests.swift delete mode 100644 Tests/GRPCTests/CallStartBehaviorTests.swift delete mode 100644 Tests/GRPCTests/CapturingLogHandler.swift delete mode 100644 Tests/GRPCTests/ClientCallTests.swift delete mode 100644 Tests/GRPCTests/ClientCancellingTests.swift delete mode 100644 Tests/GRPCTests/ClientClosedChannelTests.swift delete mode 100644 Tests/GRPCTests/ClientConnectionBackoffTests.swift delete mode 100644 Tests/GRPCTests/ClientEventLoopPreferenceTests.swift delete mode 100644 Tests/GRPCTests/ClientInterceptorPipelineTests.swift delete mode 100644 Tests/GRPCTests/ClientQuiescingTests.swift delete mode 100644 Tests/GRPCTests/ClientTLSFailureTests.swift delete mode 100644 Tests/GRPCTests/ClientTLSTests.swift delete mode 100644 Tests/GRPCTests/ClientTimeoutTests.swift delete mode 100644 Tests/GRPCTests/ClientTransportTests.swift delete mode 100644 Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift delete mode 100644 Tests/GRPCTests/Codegen/Normalization/NormalizationProvider.swift delete mode 100644 Tests/GRPCTests/Codegen/Normalization/NormalizationTests.swift delete mode 100644 Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift delete mode 100644 Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift delete mode 100644 Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift delete mode 100644 Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection delete mode 100644 Tests/GRPCTests/CompressionTests.swift delete mode 100644 Tests/GRPCTests/ConfigurationTests.swift delete mode 100644 Tests/GRPCTests/ConnectionBackoffTests.swift delete mode 100644 Tests/GRPCTests/ConnectionFailingTests.swift delete mode 100644 Tests/GRPCTests/ConnectionManagerTests.swift delete mode 100644 Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift delete mode 100644 Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift delete mode 100644 Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift delete mode 100644 Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift delete mode 100644 Tests/GRPCTests/ConnectivityStateMonitorTests.swift delete mode 100644 Tests/GRPCTests/DebugChannelInitializerTests.swift delete mode 100644 Tests/GRPCTests/DelegatingErrorHandlerTests.swift delete mode 100644 Tests/GRPCTests/EchoHelpers/EchoMessageHelpers.swift delete mode 100644 Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift delete mode 100644 Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift delete mode 100644 Tests/GRPCTests/EchoHelpers/Providers/DelegatingOnCloseEchoProvider.swift delete mode 100644 Tests/GRPCTests/EchoHelpers/Providers/FailingEchoProvider.swift delete mode 100644 Tests/GRPCTests/EchoHelpers/Providers/MetadataEchoProvider.swift delete mode 100644 Tests/GRPCTests/EchoHelpers/Providers/NeverResolvingEchoProvider.swift delete mode 100644 Tests/GRPCTests/EchoMetadataTests.swift delete mode 100644 Tests/GRPCTests/EchoTestClientTests.swift delete mode 100644 Tests/GRPCTests/ErrorRecordingDelegate.swift delete mode 100644 Tests/GRPCTests/EventLoopFuture+Assertions.swift delete mode 100644 Tests/GRPCTests/FakeChannelTests.swift delete mode 100644 Tests/GRPCTests/FakeResponseStreamTests.swift delete mode 100644 Tests/GRPCTests/FunctionalTests.swift delete mode 100644 Tests/GRPCTests/GRPCAsyncClientCallTests.swift delete mode 100644 Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift delete mode 100644 Tests/GRPCTests/GRPCClientChannelHandlerTests.swift delete mode 100644 Tests/GRPCTests/GRPCClientStateMachineTests.swift delete mode 100644 Tests/GRPCTests/GRPCCustomPayloadTests.swift delete mode 100644 Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift delete mode 100644 Tests/GRPCTests/GRPCIdleTests.swift delete mode 100644 Tests/GRPCTests/GRPCInteroperabilityTests.swift delete mode 100644 Tests/GRPCTests/GRPCKeepaliveTests.swift delete mode 100644 Tests/GRPCTests/GRPCMessageLengthLimitTests.swift delete mode 100644 Tests/GRPCTests/GRPCNetworkFrameworkTests.swift delete mode 100644 Tests/GRPCTests/GRPCPingHandlerTests.swift delete mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift delete mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift delete mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift delete mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift delete mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift delete mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift delete mode 100644 Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift delete mode 100644 Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift delete mode 100644 Tests/GRPCTests/GRPCStatusCodeTests.swift delete mode 100644 Tests/GRPCTests/GRPCStatusMessageMarshallerTests.swift delete mode 100644 Tests/GRPCTests/GRPCStatusTests.swift delete mode 100644 Tests/GRPCTests/GRPCTestCase.swift delete mode 100644 Tests/GRPCTests/GRPCTimeoutTests.swift delete mode 100644 Tests/GRPCTests/GRPCTypeSizeTests.swift delete mode 100644 Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift delete mode 100644 Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift delete mode 100644 Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift delete mode 100644 Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift delete mode 100644 Tests/GRPCTests/HTTPVersionParserTests.swift delete mode 100644 Tests/GRPCTests/HeaderNormalizationTests.swift delete mode 100644 Tests/GRPCTests/ImmediateServerFailureTests.swift delete mode 100644 Tests/GRPCTests/InterceptedRPCCancellationTests.swift delete mode 100644 Tests/GRPCTests/InterceptorsTests.swift delete mode 100644 Tests/GRPCTests/LazyEventLoopPromiseTests.swift delete mode 100644 Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift delete mode 100644 Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift delete mode 100644 Tests/GRPCTests/MutualTLSTests.swift delete mode 100644 Tests/GRPCTests/OneOrManyQueueTests.swift delete mode 100644 Tests/GRPCTests/PlatformSupportTests.swift delete mode 100644 Tests/GRPCTests/RequestIDProviderTests.swift delete mode 100644 Tests/GRPCTests/RequestIDTests.swift delete mode 100644 Tests/GRPCTests/SampleCertificate+Assertions.swift delete mode 100644 Tests/GRPCTests/ServerErrorDelegateTests.swift delete mode 100644 Tests/GRPCTests/ServerFuzzingRegressionTests.swift delete mode 100644 Tests/GRPCTests/ServerInterceptorPipelineTests.swift delete mode 100644 Tests/GRPCTests/ServerInterceptorTests.swift delete mode 100644 Tests/GRPCTests/ServerOnCloseTests.swift delete mode 100644 Tests/GRPCTests/ServerQuiescingTests.swift delete mode 100644 Tests/GRPCTests/ServerTLSErrorTests.swift delete mode 100644 Tests/GRPCTests/ServerThrowingTests.swift delete mode 100644 Tests/GRPCTests/ServerWebTests.swift delete mode 100644 Tests/GRPCTests/StopwatchTests.swift delete mode 100644 Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift delete mode 100644 Tests/GRPCTests/StreamingRequestClientCallTests.swift delete mode 100644 Tests/GRPCTests/TestClientExample.swift delete mode 100644 Tests/GRPCTests/TimeLimitTests.swift delete mode 100644 Tests/GRPCTests/UnaryServerHandlerTests.swift delete mode 100644 Tests/GRPCTests/UserInfoTests.swift delete mode 100644 Tests/GRPCTests/VsockSocketTests.swift delete mode 100644 Tests/GRPCTests/WebCORSHandlerTests.swift delete mode 100644 Tests/GRPCTests/WithConnectedSocketTests.swift delete mode 100644 Tests/GRPCTests/XCTestHelpers.swift delete mode 100644 Tests/GRPCTests/ZeroLengthWriteTests.swift delete mode 100644 Tests/GRPCTests/ZlibTests.swift delete mode 100644 Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift delete mode 100644 Tests/Services/HealthTests/HealthTests.swift delete mode 100644 Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift rename {Protos => dev/Protos}/README.md (100%) rename {Protos => dev/Protos}/examples/echo/echo.proto (100%) rename {Protos => dev/Protos}/examples/route_guide/route_guide.proto (100%) rename {Protos => dev/Protos}/fetch.sh (63%) create mode 100755 dev/Protos/generate.sh rename {Protos => dev/Protos}/upstream/google/rpc/code.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/core/stats.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/examples/helloworld.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/health/v1/health.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/lookup/v1/rls.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/lookup/v1/rls_config.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/reflection/v1/reflection.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/reflection/v1alpha/reflection.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/service_config/service_config.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/testing/benchmark_service.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/testing/control.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/testing/messages.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/testing/payloads.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/testing/stats.proto (100%) rename {Protos => dev/Protos}/upstream/grpc/testing/worker_service.proto (100%) rename {scripts => dev}/check-generated-code.sh (96%) delete mode 100755 dev/codegen-tests/01-echo/generate-and-diff.sh delete mode 100644 dev/codegen-tests/01-echo/golden/echo.grpc.swift delete mode 120000 dev/codegen-tests/01-echo/proto/echo.proto delete mode 100755 dev/codegen-tests/02-multifile/generate-and-diff.sh delete mode 100644 dev/codegen-tests/02-multifile/golden/a.grpc.swift delete mode 100644 dev/codegen-tests/02-multifile/golden/b.grpc.swift delete mode 100644 dev/codegen-tests/02-multifile/proto/a.proto delete mode 100644 dev/codegen-tests/02-multifile/proto/b.proto delete mode 100755 dev/codegen-tests/03-multifile-with-module-map/generate-and-diff.sh delete mode 100644 dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift delete mode 100644 dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift delete mode 120000 dev/codegen-tests/03-multifile-with-module-map/proto/a.proto delete mode 120000 dev/codegen-tests/03-multifile-with-module-map/proto/b.proto delete mode 100644 dev/codegen-tests/03-multifile-with-module-map/swift.modulemap delete mode 100755 dev/codegen-tests/04-service-with-message-import/generate-and-diff.sh delete mode 100644 dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift delete mode 100644 dev/codegen-tests/04-service-with-message-import/proto/message.proto delete mode 100644 dev/codegen-tests/04-service-with-message-import/proto/service.proto delete mode 100755 dev/codegen-tests/05-service-only/generate-and-diff.sh delete mode 100644 dev/codegen-tests/05-service-only/golden/test.grpc.swift delete mode 100644 dev/codegen-tests/05-service-only/proto/test.proto delete mode 100755 dev/codegen-tests/06-test-client-only/generate-and-diff.sh delete mode 100644 dev/codegen-tests/06-test-client-only/golden/test.grpc.swift delete mode 100644 dev/codegen-tests/06-test-client-only/proto/test.proto delete mode 100644 dev/codegen-tests/README.md delete mode 100755 dev/codegen-tests/run-tests.sh delete mode 100644 dev/codegen-tests/test-boilerplate.sh rename {scripts => dev}/format.sh (92%) rename {scripts => dev}/license-check.sh (93%) rename {scripts => dev}/sanity.sh (100%) delete mode 100644 docs/api.md delete mode 100644 docs/apple-platforms.md delete mode 100644 docs/async-await-proposal.md delete mode 100644 docs/basic-tutorial.md delete mode 100644 docs/client-without-codegen.md delete mode 100644 docs/faqs.md delete mode 100644 docs/interceptors-tutorial.md delete mode 100644 docs/keepalive.md delete mode 100644 docs/plugin.md delete mode 100644 docs/quick-start.md delete mode 100644 docs/tls.md delete mode 100755 scripts/alloc-limits.sh delete mode 100755 scripts/bundle-plugins-for-release.sh delete mode 100755 scripts/cg_diff.py delete mode 100644 scripts/fix-project-settings.rb delete mode 100755 scripts/make-sample-certs.py delete mode 100755 scripts/makecert delete mode 100755 scripts/run-plugin-tests.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56a7e4fa6..d795e9681 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ jobs: run: apt update && apt install -y protobuf-compiler - name: "Formatting, License Headers, and Generated Code check" run: | - ./scripts/sanity.sh + ./dev/sanity.sh unit-tests: strategy: fail-fast: false @@ -31,12 +31,6 @@ jobs: - image: swift:6.0-jammy # No TSAN because of: https://github.com/apple/swift/issues/59068 # swift-test-flags: "--sanitize=thread" - - image: swift:5.10.1-noble - # No TSAN because of: https://github.com/apple/swift/issues/59068 - # swift-test-flags: "--sanitize=thread" - - image: swift:5.9-jammy - # No TSAN because of: https://github.com/apple/swift/issues/59068 - # swift-test-flags: "--sanitize=thread" name: Build and Test on ${{ matrix.image }} runs-on: ubuntu-latest container: @@ -56,104 +50,18 @@ jobs: include: - image: swiftlang/swift:nightly-jammy swift-version: 'main' - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - image: swift:6.0-jammy swift-version: '6.0' - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.10.1-noble - swift-version: '5.10' - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 - - image: swift:5.9-jammy - swift-version: 5.9 - env: - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_10_requests: 323000 - MAX_ALLOCS_ALLOWED_bidi_1k_rpcs_1_request: 161000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_10_small_requests: 110000 - MAX_ALLOCS_ALLOWED_embedded_server_bidi_1k_rpcs_1_small_request: 65000 - MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request: 61000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong: 163000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_client: 170000 - MAX_ALLOCS_ALLOWED_unary_1k_ping_pong_interceptors_server: 170000 name: Performance Tests on ${{ matrix.image }} runs-on: ubuntu-latest container: image: ${{ matrix.image }} steps: - uses: actions/checkout@v4 - - name: 🧮 Allocation Counting Tests - run: ./Performance/allocations/test-allocation-counts.sh - env: ${{ matrix.env }} - timeout-minutes: 20 - name: Install jemalloc for benchmarking - if: ${{ matrix.swift-version == '6.0' || matrix.swift-version == 'main' }} run: apt update && apt-get install -y libjemalloc-dev timeout-minutes: 20 - name: Run Benchmarks - if: ${{ matrix.swift-version == '6.0' || matrix.swift-version == 'main' }} - working-directory: ./Performance/Benchmarks + working-directory: "./IntegrationTests/Benchmarks" run: swift package benchmark baseline check --no-progress --check-absolute-path Thresholds/${{ matrix.swift-version }}/ timeout-minutes: 20 - integration-tests: - strategy: - fail-fast: false - matrix: - include: - - image: swiftlang/swift:nightly-jammy - swift-tools-version: '6.0' - supports-v2: true - - image: swift:6.0-jammy - swift-tools-version: '6.0' - supports-v2: true - - image: swift:5.10.1-noble - swift-tools-version: '5.10' - supports-v2: false - - image: swift:5.9-jammy - swift-tools-version: '5.9' - supports-v2: false - name: Integration Tests on ${{ matrix.image }} - runs-on: ubuntu-latest - container: - image: ${{ matrix.image }} - steps: - - uses: actions/checkout@v4 - - name: Install protoc - run: apt update && apt install -y protobuf-compiler - - name: SwiftPM plugin test (v1) - run: ./scripts/run-plugin-tests.sh ${{ matrix.swift-tools-version }} "v1" - - name: SwiftPM plugin test (v2) - if: ${{ matrix.supports-v2 }} - run: ./scripts/run-plugin-tests.sh ${{ matrix.swift-tools-version }} "v2" - - name: Build without NIOSSL - run: swift build - env: - GRPC_NO_NIO_SSL: 1 - timeout-minutes: 20 - - name: Test without NIOSSL - run: swift test - env: - GRPC_NO_NIO_SSL: 1 - timeout-minutes: 20 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a82b5b39c..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Release - -on: - workflow_dispatch: - inputs: - releaseVersion: - description: The release version for which to build and upload artifacts - required: true - type: string - release: - types: [ published ] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Extract release version when job started by release being published - run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - if: ${{ github.event_name == 'release' }} - - - name: Extract release version when job manually started - run: echo "RELEASE_VERSION=${{ inputs.releaseVersion }}" >> $GITHUB_ENV - if: ${{ github.event_name == 'workflow_dispatch' }} - - - name: Build the plugins - run: | - swift build --configuration=release --product protoc-gen-swift - cp ./.build/release/protoc-gen-swift . - swift build --configuration=release --product protoc-gen-grpc-swift - cp ./.build/release/protoc-gen-grpc-swift . - - - name: Zip the plugins - run: | - zip protoc-grpc-swift-plugins-linux-x86_64-${{ env.RELEASE_VERSION }}.zip protoc-gen-swift protoc-gen-grpc-swift - - - name: Upload artifacts - uses: softprops/action-gh-release@v1 - with: - files: protoc-grpc-swift-plugins-linux-x86_64-${{ env.RELEASE_VERSION }}.zip diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index e491a9e7f..000000000 --- a/AUTHORS +++ /dev/null @@ -1 +0,0 @@ -Google Inc. diff --git a/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift b/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift deleted file mode 100644 index 0c06abeb5..000000000 --- a/Examples/v1/Echo/Implementation/EchoAsyncProvider.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public final class EchoAsyncProvider: Echo_EchoAsyncProvider { - public let interceptors: Echo_EchoServerInterceptorFactoryProtocol? - - public init(interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil) { - self.interceptors = interceptors - } - - public func get( - request: Echo_EchoRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Echo_EchoResponse { - return .with { - $0.text = "Swift echo get: " + request.text - } - } - - public func expand( - request: Echo_EchoRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for (i, part) in request.text.components(separatedBy: " ").lazy.enumerated() { - try await responseStream.send(.with { $0.text = "Swift echo expand (\(i)): \(part)" }) - } - } - - public func collect( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Echo_EchoResponse { - let text = try await requestStream.reduce(into: "Swift echo collect:") { result, request in - result += " \(request.text)" - } - - return .with { $0.text = text } - } - - public func update( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - var counter = 0 - for try await request in requestStream { - let text = "Swift echo update (\(counter)): \(request.text)" - try await responseStream.send(.with { $0.text = text }) - counter += 1 - } - } -} diff --git a/Examples/v1/Echo/Implementation/EchoProvider.swift b/Examples/v1/Echo/Implementation/EchoProvider.swift deleted file mode 100644 index b0834994f..000000000 --- a/Examples/v1/Echo/Implementation/EchoProvider.swift +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2018, 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 SwiftProtobuf - -public class EchoProvider: Echo_EchoProvider { - public let interceptors: Echo_EchoServerInterceptorFactoryProtocol? - - public init(interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil) { - self.interceptors = interceptors - } - - public func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - let response = Echo_EchoResponse.with { - $0.text = "Swift echo get: " + request.text - } - return context.eventLoop.makeSucceededFuture(response) - } - - public func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - let responses = request.text.components(separatedBy: " ").lazy.enumerated().map { i, part in - Echo_EchoResponse.with { - $0.text = "Swift echo expand (\(i)): \(part)" - } - } - - context.sendResponses(responses, promise: nil) - return context.eventLoop.makeSucceededFuture(.ok) - } - - public func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - var parts: [String] = [] - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(message): - parts.append(message.text) - - case .end: - let response = Echo_EchoResponse.with { - $0.text = "Swift echo collect: " + parts.joined(separator: " ") - } - context.responsePromise.succeed(response) - } - }) - } - - public func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - var count = 0 - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(message): - let response = Echo_EchoResponse.with { - $0.text = "Swift echo update (\(count)): \(message.text)" - } - count += 1 - context.sendResponse(response, promise: nil) - - case .end: - context.statusPromise.succeed(.ok) - } - }) - } -} diff --git a/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift b/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift deleted file mode 100644 index 9b87d8c1c..000000000 --- a/Examples/v1/Echo/Implementation/HPACKHeaders+Prettify.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020, 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 NIOHPACK - -func prettify(_ headers: HPACKHeaders) -> String { - return "[" - + headers.map { name, value, _ in - "'\(name)': '\(value)'" - }.joined(separator: ", ") + "]" -} diff --git a/Examples/v1/Echo/Implementation/Interceptors.swift b/Examples/v1/Echo/Implementation/Interceptors.swift deleted file mode 100644 index 232e3bedc..000000000 --- a/Examples/v1/Echo/Implementation/Interceptors.swift +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020, 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 - -// All client interceptors derive from the 'ClientInterceptor' base class. We know the request and -// response types for all Echo RPCs are the same: so we'll use them concretely here, allowing us -// to access fields on each type as we intercept them. -class LoggingEchoClientInterceptor: ClientInterceptor, - @unchecked Sendable -{ - /// Called when the interceptor has received a request part to handle. - /// - /// - Parameters: - /// - part: The request part to send to the server. - /// - promise: A promise to complete once the request part has been written to the network. - /// - context: An interceptor context which may be used to forward the request part to the next - /// interceptor. - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - switch part { - // The (user-provided) request headers, we send these at the start of each RPC. They will be - // augmented with transport specific headers once the request part reaches the transport. - case let .metadata(headers): - print("> Starting '\(context.path)' RPC, headers:", prettify(headers)) - - // The request message and metadata (ignored here). For unary and server-streaming RPCs we - // expect exactly one message, for client-streaming and bidirectional streaming RPCs any number - // of messages is permitted. - case let .message(request, _): - print("> Sending request with text '\(request.text)'") - - // The end of the request stream: must be sent exactly once, after which no more messages may - // be sent. - case .end: - print("> Closing request stream") - } - - // Forward the request part to the next interceptor. - context.send(part, promise: promise) - } - - /// Called when the interceptor has received a response part to handle. - /// - /// - Parameters: - /// - part: The response part received from the server. - /// - context: An interceptor context which may be used to forward the response part to the next - /// interceptor. - override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - switch part { - // The response headers received from the server. We expect to receive these once at the start - // of a response stream, however, it is also valid to see no 'metadata' parts on the response - // stream if the server rejects the RPC (in which case we expect the 'end' part). - case let .metadata(headers): - print("< Received headers:", prettify(headers)) - - // A response message received from the server. For unary and client-streaming RPCs we expect - // one message. For server-streaming and bidirectional-streaming we expect any number of - // messages (including zero). - case let .message(response): - print("< Received response with text '\(response.text)'") - - // The end of the response stream (and by extension, request stream). We expect one 'end' part, - // after which no more response parts may be received and no more request parts will be sent. - case let .end(status, trailers): - print("< Response stream closed with status: '\(status)' and trailers:", prettify(trailers)) - } - - // Forward the response part to the next interceptor. - context.receive(part) - } -} - -/// This class is an implementation of a *generated* protocol for the client which has one factory -/// method per RPC returning the interceptors to use. The relevant factory method is call when -/// invoking each RPC. An implementation of this protocol can be set on the generated client. -public final class ExampleClientInterceptorFactory: Echo_EchoClientInterceptorFactoryProtocol { - public init() {} - - // Returns an array of interceptors to use for the 'Get' RPC. - public func makeGetInterceptors() -> [ClientInterceptor] { - return [LoggingEchoClientInterceptor()] - } - - // Returns an array of interceptors to use for the 'Expand' RPC. - public func makeExpandInterceptors() -> [ClientInterceptor] { - return [LoggingEchoClientInterceptor()] - } - - // Returns an array of interceptors to use for the 'Collect' RPC. - public func makeCollectInterceptors() - -> [ClientInterceptor] - { - return [LoggingEchoClientInterceptor()] - } - - // Returns an array of interceptors to use for the 'Update' RPC. - public func makeUpdateInterceptors() -> [ClientInterceptor] { - return [LoggingEchoClientInterceptor()] - } -} diff --git a/Examples/v1/Echo/Model/echo.grpc.swift b/Examples/v1/Echo/Model/echo.grpc.swift deleted file mode 100644 index 6763b31cc..000000000 --- a/Examples/v1/Echo/Model/echo.grpc.swift +++ /dev/null @@ -1,749 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: echo.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Usage: instantiate `Echo_EchoClient`, then call methods of this protocol to make API calls. -public protocol Echo_EchoClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Echo_EchoClientInterceptorFactoryProtocol? { get } - - func get( - _ request: Echo_EchoRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func expand( - _ request: Echo_EchoRequest, - callOptions: CallOptions?, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> ServerStreamingCall - - func collect( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func update( - callOptions: CallOptions?, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> BidirectionalStreamingCall -} - -extension Echo_EchoClientProtocol { - public var serviceName: String { - return "echo.Echo" - } - - /// Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: Request to send to Get. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func get( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Echo_EchoClientMetadata.Methods.get.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetInterceptors() ?? [] - ) - } - - /// Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: Request to send to Expand. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - public func expand( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: Echo_EchoClientMetadata.Methods.expand.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeExpandInterceptors() ?? [], - handler: handler - ) - } - - /// Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. - public func collect( - callOptions: CallOptions? = nil - ) -> ClientStreamingCall { - return self.makeClientStreamingCall( - path: Echo_EchoClientMetadata.Methods.collect.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCollectInterceptors() ?? [] - ) - } - - /// Streams back messages as they are received in an input stream. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func update( - callOptions: CallOptions? = nil, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Echo_EchoClientMetadata.Methods.update.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [], - handler: handler - ) - } -} - -@available(*, deprecated) -extension Echo_EchoClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Echo_EchoNIOClient") -public final class Echo_EchoClient: Echo_EchoClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Echo_EchoClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Echo_EchoClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the echo.Echo service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Echo_EchoClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Echo_EchoNIOClient: Echo_EchoClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Echo_EchoClientInterceptorFactoryProtocol? - - /// Creates a client for the echo.Echo service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Echo_EchoClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Echo_EchoAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Echo_EchoClientInterceptorFactoryProtocol? { get } - - func makeGetCall( - _ request: Echo_EchoRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeExpandCall( - _ request: Echo_EchoRequest, - callOptions: CallOptions? - ) -> GRPCAsyncServerStreamingCall - - func makeCollectCall( - callOptions: CallOptions? - ) -> GRPCAsyncClientStreamingCall - - func makeUpdateCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Echo_EchoAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Echo_EchoClientMetadata.serviceDescriptor - } - - public var interceptors: Echo_EchoClientInterceptorFactoryProtocol? { - return nil - } - - public func makeGetCall( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Echo_EchoClientMetadata.Methods.get.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetInterceptors() ?? [] - ) - } - - public func makeExpandCall( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncServerStreamingCall { - return self.makeAsyncServerStreamingCall( - path: Echo_EchoClientMetadata.Methods.expand.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeExpandInterceptors() ?? [] - ) - } - - public func makeCollectCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncClientStreamingCall { - return self.makeAsyncClientStreamingCall( - path: Echo_EchoClientMetadata.Methods.collect.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCollectInterceptors() ?? [] - ) - } - - public func makeUpdateCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Echo_EchoClientMetadata.Methods.update.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Echo_EchoAsyncClientProtocol { - public func get( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) async throws -> Echo_EchoResponse { - return try await self.performAsyncUnaryCall( - path: Echo_EchoClientMetadata.Methods.get.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetInterceptors() ?? [] - ) - } - - public func expand( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream { - return self.performAsyncServerStreamingCall( - path: Echo_EchoClientMetadata.Methods.expand.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeExpandInterceptors() ?? [] - ) - } - - public func collect( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Echo_EchoResponse where RequestStream: Sequence, RequestStream.Element == Echo_EchoRequest { - return try await self.performAsyncClientStreamingCall( - path: Echo_EchoClientMetadata.Methods.collect.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCollectInterceptors() ?? [] - ) - } - - public func collect( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Echo_EchoResponse where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Echo_EchoRequest { - return try await self.performAsyncClientStreamingCall( - path: Echo_EchoClientMetadata.Methods.collect.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCollectInterceptors() ?? [] - ) - } - - public func update( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Echo_EchoRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Echo_EchoClientMetadata.Methods.update.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [] - ) - } - - public func update( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Echo_EchoRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Echo_EchoClientMetadata.Methods.update.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Echo_EchoAsyncClient: Echo_EchoAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Echo_EchoClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Echo_EchoClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -public protocol Echo_EchoClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'get'. - func makeGetInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'expand'. - func makeExpandInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'collect'. - func makeCollectInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'update'. - func makeUpdateInterceptors() -> [ClientInterceptor] -} - -public enum Echo_EchoClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "Echo", - fullName: "echo.Echo", - methods: [ - Echo_EchoClientMetadata.Methods.get, - Echo_EchoClientMetadata.Methods.expand, - Echo_EchoClientMetadata.Methods.collect, - Echo_EchoClientMetadata.Methods.update, - ] - ) - - public enum Methods { - public static let get = GRPCMethodDescriptor( - name: "Get", - path: "/echo.Echo/Get", - type: GRPCCallType.unary - ) - - public static let expand = GRPCMethodDescriptor( - name: "Expand", - path: "/echo.Echo/Expand", - type: GRPCCallType.serverStreaming - ) - - public static let collect = GRPCMethodDescriptor( - name: "Collect", - path: "/echo.Echo/Collect", - type: GRPCCallType.clientStreaming - ) - - public static let update = GRPCMethodDescriptor( - name: "Update", - path: "/echo.Echo/Update", - type: GRPCCallType.bidirectionalStreaming - ) - } -} - -@available(swift, deprecated: 5.6) -extension Echo_EchoTestClient: @unchecked Sendable {} - -@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.") -public final class Echo_EchoTestClient: Echo_EchoClientProtocol { - private let fakeChannel: FakeChannel - public var defaultCallOptions: CallOptions - public var interceptors: Echo_EchoClientInterceptorFactoryProtocol? - - public var channel: GRPCChannel { - return self.fakeChannel - } - - public init( - fakeChannel: FakeChannel = FakeChannel(), - defaultCallOptions callOptions: CallOptions = CallOptions(), - interceptors: Echo_EchoClientInterceptorFactoryProtocol? = nil - ) { - self.fakeChannel = fakeChannel - self.defaultCallOptions = callOptions - self.interceptors = interceptors - } - - /// Make a unary response for the Get RPC. This must be called - /// before calling 'get'. See also 'FakeUnaryResponse'. - /// - /// - Parameter requestHandler: a handler for request parts sent by the RPC. - public func makeGetResponseStream( - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) -> FakeUnaryResponse { - return self.fakeChannel.makeFakeUnaryResponse(path: Echo_EchoClientMetadata.Methods.get.path, requestHandler: requestHandler) - } - - public func enqueueGetResponse( - _ response: Echo_EchoResponse, - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) { - let stream = self.makeGetResponseStream(requestHandler) - // This is the only operation on the stream; try! is fine. - try! stream.sendMessage(response) - } - - /// Returns true if there are response streams enqueued for 'Get' - public var hasGetResponsesRemaining: Bool { - return self.fakeChannel.hasFakeResponseEnqueued(forPath: Echo_EchoClientMetadata.Methods.get.path) - } - - /// Make a streaming response for the Expand RPC. This must be called - /// before calling 'expand'. See also 'FakeStreamingResponse'. - /// - /// - Parameter requestHandler: a handler for request parts sent by the RPC. - public func makeExpandResponseStream( - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) -> FakeStreamingResponse { - return self.fakeChannel.makeFakeStreamingResponse(path: Echo_EchoClientMetadata.Methods.expand.path, requestHandler: requestHandler) - } - - public func enqueueExpandResponses( - _ responses: [Echo_EchoResponse], - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) { - let stream = self.makeExpandResponseStream(requestHandler) - // These are the only operation on the stream; try! is fine. - responses.forEach { try! stream.sendMessage($0) } - try! stream.sendEnd() - } - - /// Returns true if there are response streams enqueued for 'Expand' - public var hasExpandResponsesRemaining: Bool { - return self.fakeChannel.hasFakeResponseEnqueued(forPath: Echo_EchoClientMetadata.Methods.expand.path) - } - - /// Make a unary response for the Collect RPC. This must be called - /// before calling 'collect'. See also 'FakeUnaryResponse'. - /// - /// - Parameter requestHandler: a handler for request parts sent by the RPC. - public func makeCollectResponseStream( - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) -> FakeUnaryResponse { - return self.fakeChannel.makeFakeUnaryResponse(path: Echo_EchoClientMetadata.Methods.collect.path, requestHandler: requestHandler) - } - - public func enqueueCollectResponse( - _ response: Echo_EchoResponse, - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) { - let stream = self.makeCollectResponseStream(requestHandler) - // This is the only operation on the stream; try! is fine. - try! stream.sendMessage(response) - } - - /// Returns true if there are response streams enqueued for 'Collect' - public var hasCollectResponsesRemaining: Bool { - return self.fakeChannel.hasFakeResponseEnqueued(forPath: Echo_EchoClientMetadata.Methods.collect.path) - } - - /// Make a streaming response for the Update RPC. This must be called - /// before calling 'update'. See also 'FakeStreamingResponse'. - /// - /// - Parameter requestHandler: a handler for request parts sent by the RPC. - public func makeUpdateResponseStream( - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) -> FakeStreamingResponse { - return self.fakeChannel.makeFakeStreamingResponse(path: Echo_EchoClientMetadata.Methods.update.path, requestHandler: requestHandler) - } - - public func enqueueUpdateResponses( - _ responses: [Echo_EchoResponse], - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) { - let stream = self.makeUpdateResponseStream(requestHandler) - // These are the only operation on the stream; try! is fine. - responses.forEach { try! stream.sendMessage($0) } - try! stream.sendEnd() - } - - /// Returns true if there are response streams enqueued for 'Update' - public var hasUpdateResponsesRemaining: Bool { - return self.fakeChannel.hasFakeResponseEnqueued(forPath: Echo_EchoClientMetadata.Methods.update.path) - } -} - -/// To build a server, implement a class that conforms to this protocol. -public protocol Echo_EchoProvider: CallHandlerProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get } - - /// Immediately returns an echo of a request. - func get(request: Echo_EchoRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Splits a request into words and returns each word in a stream of messages. - func expand(request: Echo_EchoRequest, context: StreamingResponseCallContext) -> EventLoopFuture - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// Streams back messages as they are received in an input stream. - func update(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Echo_EchoProvider { - public var serviceName: Substring { - return Echo_EchoServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Get": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeGetInterceptors() ?? [], - userFunction: self.get(request:context:) - ) - - case "Expand": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeExpandInterceptors() ?? [], - userFunction: self.expand(request:context:) - ) - - case "Collect": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCollectInterceptors() ?? [], - observerFactory: self.collect(context:) - ) - - case "Update": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [], - observerFactory: self.update(context:) - ) - - default: - return nil - } - } -} - -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Echo_EchoAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get } - - /// Immediately returns an echo of a request. - func get( - request: Echo_EchoRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Echo_EchoResponse - - /// Splits a request into words and returns each word in a stream of messages. - func expand( - request: Echo_EchoRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Echo_EchoResponse - - /// Streams back messages as they are received in an input stream. - func update( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Echo_EchoAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Echo_EchoServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Echo_EchoServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Get": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeGetInterceptors() ?? [], - wrapping: { try await self.get(request: $0, context: $1) } - ) - - case "Expand": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeExpandInterceptors() ?? [], - wrapping: { try await self.expand(request: $0, responseStream: $1, context: $2) } - ) - - case "Collect": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCollectInterceptors() ?? [], - wrapping: { try await self.collect(requestStream: $0, context: $1) } - ) - - case "Update": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [], - wrapping: { try await self.update(requestStream: $0, responseStream: $1, context: $2) } - ) - - default: - return nil - } - } -} - -public protocol Echo_EchoServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'get'. - /// Defaults to calling `self.makeInterceptors()`. - func makeGetInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'expand'. - /// Defaults to calling `self.makeInterceptors()`. - func makeExpandInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'collect'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCollectInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'update'. - /// Defaults to calling `self.makeInterceptors()`. - func makeUpdateInterceptors() -> [ServerInterceptor] -} - -public enum Echo_EchoServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "Echo", - fullName: "echo.Echo", - methods: [ - Echo_EchoServerMetadata.Methods.get, - Echo_EchoServerMetadata.Methods.expand, - Echo_EchoServerMetadata.Methods.collect, - Echo_EchoServerMetadata.Methods.update, - ] - ) - - public enum Methods { - public static let get = GRPCMethodDescriptor( - name: "Get", - path: "/echo.Echo/Get", - type: GRPCCallType.unary - ) - - public static let expand = GRPCMethodDescriptor( - name: "Expand", - path: "/echo.Echo/Expand", - type: GRPCCallType.serverStreaming - ) - - public static let collect = GRPCMethodDescriptor( - name: "Collect", - path: "/echo.Echo/Collect", - type: GRPCCallType.clientStreaming - ) - - public static let update = GRPCMethodDescriptor( - name: "Update", - path: "/echo.Echo/Update", - type: GRPCCallType.bidirectionalStreaming - ) - } -} diff --git a/Examples/v1/Echo/Model/echo.pb.swift b/Examples/v1/Echo/Model/echo.pb.swift deleted file mode 100644 index a2b496bdd..000000000 --- a/Examples/v1/Echo/Model/echo.pb.swift +++ /dev/null @@ -1,129 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: echo.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright (c) 2015, Google Inc. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -public struct Echo_EchoRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The text of a message to be echoed. - public var text: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -public struct Echo_EchoResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The text of an echo response. - public var text: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "echo" - -extension Echo_EchoRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".EchoRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.text.isEmpty { - try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Echo_EchoRequest, rhs: Echo_EchoRequest) -> Bool { - if lhs.text != rhs.text {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Echo_EchoResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".EchoResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "text"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.text) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.text.isEmpty { - try visitor.visitSingularStringField(value: self.text, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Echo_EchoResponse, rhs: Echo_EchoResponse) -> Bool { - if lhs.text != rhs.text {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/v1/Echo/README.md b/Examples/v1/Echo/README.md deleted file mode 100644 index e068265a2..000000000 --- a/Examples/v1/Echo/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Echo, a gRPC Sample App - -This directory contains a simple echo server that demonstrates all four gRPC API -styles (Unary, Server Streaming, Client Streaming, and Bidirectional Streaming) -using the gRPC Swift. - -There are four subdirectories: -* `Model/` containing the service and model definitions and generated code, -* `Implementation/` containing the server implementation of the generated model, -* `Runtime/` containing a CLI for the server and client using the NIO-based APIs. - -### CLI implementation - -### Server - -To start the server run: - -```sh -swift run Echo server -``` - -By default the server listens on port 1234. The port may also be specified by -passing the `--port` option. Other options may be found by running: - -```sh -swift run Echo server --help -``` - -### Client - -To invoke the 'get' (unary) RPC with the message "Hello, World!" against the -server: - -```sh -swift run Echo client "Hello, World!" -``` - -Different RPC types can be called using the `--rpc` flag (which defaults to -'get'): -- 'get': a unary RPC; one request and one response -- 'collect': a client streaming RPC; multiple requests and one response -- 'expand': a server streaming RPC; one request and multiple responses -- 'update': a bidirectional streaming RPC; multiple requests and multiple - responses - -Additional options may be found by running: - -```sh -swift run Echo client --help -``` diff --git a/Examples/v1/Echo/Runtime/Echo.swift b/Examples/v1/Echo/Runtime/Echo.swift deleted file mode 100644 index c9bdee818..000000000 --- a/Examples/v1/Echo/Runtime/Echo.swift +++ /dev/null @@ -1,246 +0,0 @@ -/* - * 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 ArgumentParser -import EchoImplementation -import EchoModel -import GRPC -import GRPCSampleData -import NIOCore -import NIOPosix - -#if canImport(NIOSSL) -import NIOSSL -#endif - -// MARK: - Argument parsing - -enum RPC: String, ExpressibleByArgument { - case get - case collect - case expand - case update -} - -@main -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct Echo: AsyncParsableCommand { - static var configuration = CommandConfiguration( - abstract: "An example to run and call a simple gRPC service for echoing messages.", - subcommands: [Server.self, Client.self] - ) - - struct Server: AsyncParsableCommand { - static var configuration = CommandConfiguration( - abstract: "Start a gRPC server providing the Echo service." - ) - - @Option(help: "The port to listen on for new connections") - var port = 1234 - - @Flag(help: "Whether TLS should be used or not") - var tls = false - - func run() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - do { - try await startEchoServer(group: group, port: self.port, useTLS: self.tls) - } catch { - print("Error running server: \(error)") - } - } - } - - struct Client: AsyncParsableCommand { - static var configuration = CommandConfiguration( - abstract: "Calls an RPC on the Echo server." - ) - - @Option(help: "The port to connect to") - var port = 1234 - - @Flag(help: "Whether TLS should be used or not") - var tls = false - - @Flag(help: "Whether interceptors should be used, see 'docs/interceptors-tutorial.md'.") - var intercept = false - - @Option(help: "RPC to call ('get', 'collect', 'expand', 'update').") - var rpc: RPC = .get - - @Option(help: "How many RPCs to do.") - var iterations: Int = 1 - - @Argument(help: "Message to echo") - var message: String - - func run() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - - let client = makeClient( - group: group, - port: self.port, - useTLS: self.tls, - useInterceptor: self.intercept - ) - defer { - try! client.channel.close().wait() - } - - for _ in 0 ..< self.iterations { - await callRPC(self.rpc, using: client, message: self.message) - } - } - } -} - -// MARK: - Server - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func startEchoServer(group: EventLoopGroup, port: Int, useTLS: Bool) async throws { - let builder: Server.Builder - - if useTLS { - #if canImport(NIOSSL) - // We're using some self-signed certs here: check they aren't expired. - let caCert = SampleCertificate.ca - let serverCert = SampleCertificate.server - precondition( - !caCert.isExpired && !serverCert.isExpired, - "SSL certificates are expired. Please submit an issue at https://github.com/grpc/grpc-swift." - ) - - builder = Server.usingTLSBackedByNIOSSL( - on: group, - certificateChain: [serverCert.certificate], - privateKey: SamplePrivateKey.server - ) - .withTLS(trustRoots: .certificates([caCert.certificate])) - print("starting secure server") - #else - fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) - } else { - print("starting insecure server") - builder = Server.insecure(group: group) - } - - let server = try await builder.withServiceProviders([EchoAsyncProvider()]) - .bind(host: "localhost", port: port) - .get() - - print("started server: \(server.channel.localAddress!)") - - // This blocks to keep the main thread from finishing while the server runs, - // but the server never exits. Kill the process to stop it. - try await server.onClose.get() -} - -// MARK: - Client - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func makeClient( - group: EventLoopGroup, - port: Int, - useTLS: Bool, - useInterceptor: Bool -) -> Echo_EchoAsyncClient { - let builder: ClientConnection.Builder - - if useTLS { - #if canImport(NIOSSL) - // We're using some self-signed certs here: check they aren't expired. - let caCert = SampleCertificate.ca - let clientCert = SampleCertificate.client - precondition( - !caCert.isExpired && !clientCert.isExpired, - "SSL certificates are expired. Please submit an issue at https://github.com/grpc/grpc-swift." - ) - - builder = ClientConnection.usingTLSBackedByNIOSSL(on: group) - .withTLS(certificateChain: [clientCert.certificate]) - .withTLS(privateKey: SamplePrivateKey.client) - .withTLS(trustRoots: .certificates([caCert.certificate])) - #else - fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) - } else { - builder = ClientConnection.insecure(group: group) - } - - // Start the connection and create the client: - let connection = builder.connect(host: "localhost", port: port) - - return Echo_EchoAsyncClient( - channel: connection, - interceptors: useInterceptor ? ExampleClientInterceptorFactory() : nil - ) -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func callRPC(_ rpc: RPC, using client: Echo_EchoAsyncClient, message: String) async { - do { - switch rpc { - case .get: - try await echoGet(client: client, message: message) - case .collect: - try await echoCollect(client: client, message: message) - case .expand: - try await echoExpand(client: client, message: message) - case .update: - try await echoUpdate(client: client, message: message) - } - } catch { - print("\(rpc) RPC failed: \(error)") - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func echoGet(client: Echo_EchoAsyncClient, message: String) async throws { - let response = try await client.get(.with { $0.text = message }) - print("get received: \(response.text)") -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func echoCollect(client: Echo_EchoAsyncClient, message: String) async throws { - let messages = message.components(separatedBy: " ").map { part in - Echo_EchoRequest.with { $0.text = part } - } - let response = try await client.collect(messages) - print("collect received: \(response.text)") -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func echoExpand(client: Echo_EchoAsyncClient, message: String) async throws { - for try await response in client.expand((.with { $0.text = message })) { - print("expand received: \(response.text)") - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func echoUpdate(client: Echo_EchoAsyncClient, message: String) async throws { - let requests = message.components(separatedBy: " ").map { word in - Echo_EchoRequest.with { $0.text = word } - } - for try await response in client.update(requests) { - print("update received: \(response.text)") - } -} diff --git a/Examples/v1/Echo/Runtime/Empty.swift b/Examples/v1/Echo/Runtime/Empty.swift deleted file mode 100644 index f8c631871..000000000 --- a/Examples/v1/Echo/Runtime/Empty.swift +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2022, 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. - */ - -// This file exists to workaround https://github.com/apple/swift/issues/55127. diff --git a/Examples/v1/HelloWorld/Client/HelloWorldClient.swift b/Examples/v1/HelloWorld/Client/HelloWorldClient.swift deleted file mode 100644 index 253e7c19c..000000000 --- a/Examples/v1/HelloWorld/Client/HelloWorldClient.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 ArgumentParser -import GRPC -import HelloWorldModel -import NIOCore -import NIOPosix - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@main -struct HelloWorld: AsyncParsableCommand { - @Option(help: "The port to connect to") - var port: Int = 1234 - - @Argument(help: "The name to greet") - var name: String? - - func run() async throws { - // Setup an `EventLoopGroup` for the connection to run on. - // - // See: https://github.com/apple/swift-nio#eventloops-and-eventloopgroups - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - // Make sure the group is shutdown when we're done with it. - defer { - try! group.syncShutdownGracefully() - } - - // Configure the channel, we're not using TLS so the connection is `insecure`. - let channel = try GRPCChannelPool.with( - target: .host("localhost", port: self.port), - transportSecurity: .plaintext, - eventLoopGroup: group - ) - - // Close the connection when we're done with it. - defer { - try! channel.close().wait() - } - - // Provide the connection to the generated client. - let greeter = Helloworld_GreeterAsyncClient(channel: channel) - - // Form the request with the name, if one was provided. - let request = Helloworld_HelloRequest.with { - $0.name = self.name ?? "" - } - - do { - let greeting = try await greeter.sayHello(request) - print("Greeter received: \(greeting.message)") - } catch { - print("Greeter failed: \(error)") - } - } -} diff --git a/Examples/v1/HelloWorld/Model/helloworld.grpc.swift b/Examples/v1/HelloWorld/Model/helloworld.grpc.swift deleted file mode 100644 index affa2b8a5..000000000 --- a/Examples/v1/HelloWorld/Model/helloworld.grpc.swift +++ /dev/null @@ -1,308 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: helloworld.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// The greeting service definition. -/// -/// Usage: instantiate `Helloworld_GreeterClient`, then call methods of this protocol to make API calls. -public protocol Helloworld_GreeterClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? { get } - - func sayHello( - _ request: Helloworld_HelloRequest, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Helloworld_GreeterClientProtocol { - public var serviceName: String { - return "helloworld.Greeter" - } - - /// Sends a greeting - /// - /// - Parameters: - /// - request: Request to send to SayHello. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func sayHello( - _ request: Helloworld_HelloRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Helloworld_GreeterClientMetadata.Methods.sayHello.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeSayHelloInterceptors() ?? [] - ) - } -} - -@available(*, deprecated) -extension Helloworld_GreeterClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Helloworld_GreeterNIOClient") -public final class Helloworld_GreeterClient: Helloworld_GreeterClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the helloworld.Greeter service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Helloworld_GreeterNIOClient: Helloworld_GreeterClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? - - /// Creates a client for the helloworld.Greeter service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// The greeting service definition. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Helloworld_GreeterAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? { get } - - func makeSayHelloCall( - _ request: Helloworld_HelloRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Helloworld_GreeterAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Helloworld_GreeterClientMetadata.serviceDescriptor - } - - public var interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? { - return nil - } - - public func makeSayHelloCall( - _ request: Helloworld_HelloRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Helloworld_GreeterClientMetadata.Methods.sayHello.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeSayHelloInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Helloworld_GreeterAsyncClientProtocol { - public func sayHello( - _ request: Helloworld_HelloRequest, - callOptions: CallOptions? = nil - ) async throws -> Helloworld_HelloReply { - return try await self.performAsyncUnaryCall( - path: Helloworld_GreeterClientMetadata.Methods.sayHello.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeSayHelloInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Helloworld_GreeterAsyncClient: Helloworld_GreeterAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Helloworld_GreeterClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -public protocol Helloworld_GreeterClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'sayHello'. - func makeSayHelloInterceptors() -> [ClientInterceptor] -} - -public enum Helloworld_GreeterClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "Greeter", - fullName: "helloworld.Greeter", - methods: [ - Helloworld_GreeterClientMetadata.Methods.sayHello, - ] - ) - - public enum Methods { - public static let sayHello = GRPCMethodDescriptor( - name: "SayHello", - path: "/helloworld.Greeter/SayHello", - type: GRPCCallType.unary - ) - } -} - -/// The greeting service definition. -/// -/// To build a server, implement a class that conforms to this protocol. -public protocol Helloworld_GreeterProvider: CallHandlerProvider { - var interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? { get } - - /// Sends a greeting - func sayHello(request: Helloworld_HelloRequest, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension Helloworld_GreeterProvider { - public var serviceName: Substring { - return Helloworld_GreeterServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "SayHello": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeSayHelloInterceptors() ?? [], - userFunction: self.sayHello(request:context:) - ) - - default: - return nil - } - } -} - -/// The greeting service definition. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Helloworld_GreeterAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? { get } - - /// Sends a greeting - func sayHello( - request: Helloworld_HelloRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Helloworld_HelloReply -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Helloworld_GreeterAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Helloworld_GreeterServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Helloworld_GreeterServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "SayHello": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeSayHelloInterceptors() ?? [], - wrapping: { try await self.sayHello(request: $0, context: $1) } - ) - - default: - return nil - } - } -} - -public protocol Helloworld_GreeterServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'sayHello'. - /// Defaults to calling `self.makeInterceptors()`. - func makeSayHelloInterceptors() -> [ServerInterceptor] -} - -public enum Helloworld_GreeterServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "Greeter", - fullName: "helloworld.Greeter", - methods: [ - Helloworld_GreeterServerMetadata.Methods.sayHello, - ] - ) - - public enum Methods { - public static let sayHello = GRPCMethodDescriptor( - name: "SayHello", - path: "/helloworld.Greeter/SayHello", - type: GRPCCallType.unary - ) - } -} diff --git a/Examples/v1/HelloWorld/Model/helloworld.pb.swift b/Examples/v1/HelloWorld/Model/helloworld.pb.swift deleted file mode 100644 index f53870911..000000000 --- a/Examples/v1/HelloWorld/Model/helloworld.pb.swift +++ /dev/null @@ -1,129 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: helloworld.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -/// Copyright 2015 gRPC authors. -/// -/// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The request message containing the user's name. -public struct Helloworld_HelloRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var name: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// The response message containing the greetings -public struct Helloworld_HelloReply: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var message: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "helloworld" - -extension Helloworld_HelloRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".HelloRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Helloworld_HelloRequest, rhs: Helloworld_HelloRequest) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Helloworld_HelloReply: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".HelloReply" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "message"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Helloworld_HelloReply, rhs: Helloworld_HelloReply) -> Bool { - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/v1/HelloWorld/README.md b/Examples/v1/HelloWorld/README.md deleted file mode 100644 index f3c4eff4a..000000000 --- a/Examples/v1/HelloWorld/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Hello World, a quick-start gRPC Example - -This directory contains a 'Hello World' gRPC example, a single service with just -one RPC for saying hello. The quick-start tutorial which accompanies this -example lives in `docs/` directory of this project. - -## Running - -### Server - -To start the server run: - -```sh -swift run HelloWorldServer -``` - -### Client - -To send a message to the server run the following: - -```sh -swift run HelloWorldClient -``` - -You may also greet a particular person (or dog). For example, to greet -[PanCakes](https://grpc.io/blog/hello-pancakes/) run: - -```sh -swift run HelloWorldClient PanCakes -``` diff --git a/Examples/v1/HelloWorld/Server/GreeterProvider.swift b/Examples/v1/HelloWorld/Server/GreeterProvider.swift deleted file mode 100644 index bf6ea4449..000000000 --- a/Examples/v1/HelloWorld/Server/GreeterProvider.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 GRPC -import HelloWorldModel -import NIOCore - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -final class GreeterProvider: Helloworld_GreeterAsyncProvider { - let interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? = nil - - func sayHello( - request: Helloworld_HelloRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Helloworld_HelloReply { - let recipient = request.name.isEmpty ? "stranger" : request.name - return Helloworld_HelloReply.with { - $0.message = "Hello \(recipient)!" - } - } -} diff --git a/Examples/v1/HelloWorld/Server/HelloWorldServer.swift b/Examples/v1/HelloWorld/Server/HelloWorldServer.swift deleted file mode 100644 index 038728a9f..000000000 --- a/Examples/v1/HelloWorld/Server/HelloWorldServer.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 ArgumentParser -import GRPC -import HelloWorldModel -import NIOCore -import NIOPosix - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@main -struct HelloWorld: AsyncParsableCommand { - @Option(help: "The port to listen on for new connections") - var port = 1234 - - func run() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - - // Start the server and print its address once it has started. - let server = try await Server.insecure(group: group) - .withServiceProviders([GreeterProvider()]) - .bind(host: "localhost", port: self.port) - .get() - - print("server started on port \(server.channel.localAddress!.port!)") - - // Wait on the server's `onClose` future to stop the program from exiting. - try await server.onClose.get() - } -} diff --git a/Examples/v1/PacketCapture/Empty.swift b/Examples/v1/PacketCapture/Empty.swift deleted file mode 100644 index f8c631871..000000000 --- a/Examples/v1/PacketCapture/Empty.swift +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2022, 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. - */ - -// This file exists to workaround https://github.com/apple/swift/issues/55127. diff --git a/Examples/v1/PacketCapture/PacketCapture.swift b/Examples/v1/PacketCapture/PacketCapture.swift deleted file mode 100644 index 83d6de958..000000000 --- a/Examples/v1/PacketCapture/PacketCapture.swift +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020, 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 ArgumentParser -import EchoModel -import GRPC -import NIOCore -import NIOExtras -import NIOPosix - -@main -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct PCAP: AsyncParsableCommand { - @Option(help: "The port to connect to") - var port = 1234 - - func run() async throws { - // Create an `EventLoopGroup`. - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - - // The filename for the .pcap file to write to. - let path = "packet-capture-example.pcap" - let fileSink = try NIOWritePCAPHandler.SynchronizedFileSink.fileSinkWritingToFile( - path: path - ) { error in - print("Failed to write with error '\(error)' for path '\(path)'") - } - - // Ensure that we close the file sink when we're done with it. - defer { - try! fileSink.syncClose() - } - - let channel = try GRPCChannelPool.with( - target: .host("localhost", port: self.port), - transportSecurity: .plaintext, - eventLoopGroup: group - ) { - $0.debugChannelInitializer = { channel in - // Create the PCAP handler and add it to the start of the channel pipeline. If this example - // used TLS we would likely want to place the handler in a different position in the - // pipeline so that the captured packets in the trace would not be encrypted. - let writePCAPHandler = NIOWritePCAPHandler(mode: .client, fileSink: fileSink.write(buffer:)) - return channel.eventLoop.makeCompletedFuture( - Result { - try channel.pipeline.syncOperations.addHandler(writePCAPHandler, position: .first) - } - ) - } - } - - // Create a client. - let echo = Echo_EchoAsyncClient(channel: channel) - - let messages = ["foo", "bar", "baz", "thud", "grunt", "gorp"].map { text in - Echo_EchoRequest.with { $0.text = text } - } - - do { - for try await response in echo.update(messages) { - print("Received response '\(response.text)'") - } - print("RPC completed successfully") - } catch { - print("RPC failed with error '\(error)'") - } - - print("Try opening '\(path)' in Wireshark or with 'tcpdump -r \(path)'") - - try await echo.channel.close().get() - } -} diff --git a/Examples/v1/PacketCapture/README.md b/Examples/v1/PacketCapture/README.md deleted file mode 100644 index bd8ad3313..000000000 --- a/Examples/v1/PacketCapture/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# PCAP Debugging Example - -This example demonstrates how to use the `NIOWritePCAPHandler` from -[NIOExtras][swift-nio-extras] with gRPC Swift. - -The example configures a client to use the `NIOWritePCAPHandler` with a file -sink so that all network traffic captured by the handler is written to a -`.pcap` file. The client makes a single bidirectional streaming RPC to an Echo -server provided by gRPC Swift. - -The captured network traffic can be inspected by opening the `.pcap` with -tools like [Wireshark][wireshark] or `tcpdump`. - -## Running the Example - -The example relies on the Echo server from a different example. To start the -server run: - -```sh -$ swift run Echo server -``` - -In a separate shell run: - -```sh -$ swift run PacketCapture -``` - -The pcap file will be written to 'packet-capture-example.pcap'. - -The *.pcap* file can be opened with either: [Wireshark][wireshark] or `tcpdump --r `. - -[swift-nio-extras]: https://github.com/apple/swift-nio-extras -[wireshark]: https://wireshark.org diff --git a/Examples/v1/README.md b/Examples/v1/README.md deleted file mode 100644 index 81a5f1c22..000000000 --- a/Examples/v1/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Examples - -This directory contains a number of gRPC Swift examples. Each example includes -instructions on running them in their README. There are also tutorials which -accompany the examples in the `docs/` directory of this project. diff --git a/Examples/v1/ReflectionService/Generated/echo.grpc.reflection b/Examples/v1/ReflectionService/Generated/echo.grpc.reflection deleted file mode 100644 index af26ef4a7..000000000 --- a/Examples/v1/ReflectionService/Generated/echo.grpc.reflection +++ /dev/null @@ -1 +0,0 @@ -CgplY2hvLnByb3RvEgRlY2hvIiEKC0VjaG9SZXF1ZXN0EhIKBHRleHQYASABKAlSBHRleHQiIgoMRWNob1Jlc3BvbnNlEhIKBHRleHQYASABKAlSBHRleHQy2AEKBEVjaG8SLgoDR2V0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgASMwoGRXhwYW5kEhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAwARI0CgdDb2xsZWN0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAoARI1CgZVcGRhdGUSES5lY2hvLkVjaG9SZXF1ZXN0GhIuZWNoby5FY2hvUmVzcG9uc2UiACgBMAFK/QoKBhIEDgAoAQrCBAoBDBIDDgASMrcEIENvcHlyaWdodCAoYykgMjAxNSwgR29vZ2xlIEluYy4KCiBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCggKAQISAxAADQoKCgIGABIEEgAeAQoKCgMGAAESAxIIDAo4CgQGAAIAEgMUAjAaKyBJbW1lZGlhdGVseSByZXR1cm5zIGFuIGVjaG8gb2YgYSByZXF1ZXN0LgoKDAoFBgACAAESAxQGCQoMCgUGAAIAAhIDFAoVCgwKBQYAAgADEgMUICwKWQoEBgACARIDFwI6GkwgU3BsaXRzIGEgcmVxdWVzdCBpbnRvIHdvcmRzIGFuZCByZXR1cm5zIGVhY2ggd29yZCBpbiBhIHN0cmVhbSBvZiBtZXNzYWdlcy4KCgwKBQYAAgEBEgMXBgwKDAoFBgACAQISAxcNGAoMCgUGAAIBBhIDFyMpCgwKBQYAAgEDEgMXKjYKYgoEBgACAhIDGgI7GlUgQ29sbGVjdHMgYSBzdHJlYW0gb2YgbWVzc2FnZXMgYW5kIHJldHVybnMgdGhlbSBjb25jYXRlbmF0ZWQgd2hlbiB0aGUgY2FsbGVyIGNsb3Nlcy4KCgwKBQYAAgIBEgMaBg0KDAoFBgACAgUSAxoOFAoMCgUGAAICAhIDGhUgCgwKBQYAAgIDEgMaKzcKTQoEBgACAxIDHQJBGkAgU3RyZWFtcyBiYWNrIG1lc3NhZ2VzIGFzIHRoZXkgYXJlIHJlY2VpdmVkIGluIGFuIGlucHV0IHN0cmVhbS4KCgwKBQYAAgMBEgMdBgwKDAoFBgACAwUSAx0NEwoMCgUGAAIDAhIDHRQfCgwKBQYAAgMGEgMdKjAKDAoFBgACAwMSAx0xPQoKCgIEABIEIAAjAQoKCgMEAAESAyAIEwoyCgQEAAIAEgMiAhIaJSBUaGUgdGV4dCBvZiBhIG1lc3NhZ2UgdG8gYmUgZWNob2VkLgoKDAoFBAACAAUSAyICCAoMCgUEAAIAARIDIgkNCgwKBQQAAgADEgMiEBEKCgoCBAESBCUAKAEKCgoDBAEBEgMlCBQKLAoEBAECABIDJwISGh8gVGhlIHRleHQgb2YgYW4gZWNobyByZXNwb25zZS4KCgwKBQQBAgAFEgMnAggKDAoFBAECAAESAycJDQoMCgUEAQIAAxIDJxARYgZwcm90bzM= \ No newline at end of file diff --git a/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection b/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection deleted file mode 100644 index e88e9ac2c..000000000 --- a/Examples/v1/ReflectionService/Generated/helloworld.grpc.reflection +++ /dev/null @@ -1 +0,0 @@ -ChBoZWxsb3dvcmxkLnByb3RvEgpoZWxsb3dvcmxkIiIKDEhlbGxvUmVxdWVzdBISCgRuYW1lGAEgASgJUgRuYW1lIiYKCkhlbGxvUmVwbHkSGAoHbWVzc2FnZRgBIAEoCVIHbWVzc2FnZTJJCgdHcmVldGVyEj4KCFNheUhlbGxvEhguaGVsbG93b3JsZC5IZWxsb1JlcXVlc3QaFi5oZWxsb3dvcmxkLkhlbGxvUmVwbHkiAEI2Chtpby5ncnBjLmV4YW1wbGVzLmhlbGxvd29ybGRCD0hlbGxvV29ybGRQcm90b1ABogIDSExXSrEICgYSBA0AJAEKvwQKAQwSAw0AEhq0BCBDb3B5cmlnaHQgMjAxNSBnUlBDIGF1dGhvcnMuCgogTGljZW5zZWQgdW5kZXIgdGhlIEFwYWNoZSBMaWNlbnNlLCBWZXJzaW9uIDIuMCAodGhlICJMaWNlbnNlIik7CiB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlIHdpdGggdGhlIExpY2Vuc2UuCiBZb3UgbWF5IG9idGFpbiBhIGNvcHkgb2YgdGhlIExpY2Vuc2UgYXQKCiAgICAgaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzL0xJQ0VOU0UtMi4wCgogVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQogZGlzdHJpYnV0ZWQgdW5kZXIgdGhlIExpY2Vuc2UgaXMgZGlzdHJpYnV0ZWQgb24gYW4gIkFTIElTIiBCQVNJUywKIFdJVEhPVVQgV0FSUkFOVElFUyBPUiBDT05ESVRJT05TIE9GIEFOWSBLSU5ELCBlaXRoZXIgZXhwcmVzcyBvciBpbXBsaWVkLgogU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAogbGltaXRhdGlvbnMgdW5kZXIgdGhlIExpY2Vuc2UuCgoICgEIEgMPACIKCQoCCAoSAw8AIgoICgEIEgMQADQKCQoCCAESAxAANAoICgEIEgMRADAKCQoCCAgSAxEAMAoICgEIEgMSACEKCQoCCCQSAxIAIQoICgECEgMUABMKLgoCBgASBBcAGgEaIiBUaGUgZ3JlZXRpbmcgc2VydmljZSBkZWZpbml0aW9uLgoKCgoDBgABEgMXCA8KHwoEBgACABIDGQI1GhIgU2VuZHMgYSBncmVldGluZwoKDAoFBgACAAESAxkGDgoMCgUGAAIAAhIDGRAcCgwKBQYAAgADEgMZJzEKPQoCBAASBB0AHwEaMSBUaGUgcmVxdWVzdCBtZXNzYWdlIGNvbnRhaW5pbmcgdGhlIHVzZXIncyBuYW1lLgoKCgoDBAABEgMdCBQKCwoEBAACABIDHgISCgwKBQQAAgAFEgMeAggKDAoFBAACAAESAx4JDQoMCgUEAAIAAxIDHhARCjsKAgQBEgQiACQBGi8gVGhlIHJlc3BvbnNlIG1lc3NhZ2UgY29udGFpbmluZyB0aGUgZ3JlZXRpbmdzCgoKCgMEAQESAyIIEgoLCgQEAQIAEgMjAhUKDAoFBAECAAUSAyMCCAoMCgUEAQIAARIDIwkQCgwKBQQBAgADEgMjExRiBnByb3RvMw== \ No newline at end of file diff --git a/Examples/v1/ReflectionService/GreeterProvider.swift b/Examples/v1/ReflectionService/GreeterProvider.swift deleted file mode 120000 index 6cd24dda7..000000000 --- a/Examples/v1/ReflectionService/GreeterProvider.swift +++ /dev/null @@ -1 +0,0 @@ -../HelloWorld/Server/GreeterProvider.swift \ No newline at end of file diff --git a/Examples/v1/ReflectionService/ReflectionServer.swift b/Examples/v1/ReflectionService/ReflectionServer.swift deleted file mode 100644 index ba07e2b96..000000000 --- a/Examples/v1/ReflectionService/ReflectionServer.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2023, 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 ArgumentParser -import EchoImplementation -import EchoModel -import Foundation -import GRPC -import GRPCReflectionService -import NIOPosix -import SwiftProtobuf - -@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) -@main -struct ReflectionServer: AsyncParsableCommand { - func run() async throws { - // Getting the URLs of the files containing the reflection data. - guard - let greeterURL = Bundle.module.url( - forResource: "helloworld", - withExtension: "grpc.reflection", - subdirectory: "Generated" - ), - let echoURL = Bundle.module.url( - forResource: "echo", - withExtension: "grpc.reflection", - subdirectory: "Generated" - ) - else { - print("The resource could not be loaded.") - throw ExitCode.failure - } - - let reflectionService = try ReflectionService( - reflectionDataFileURLs: [greeterURL, echoURL], - version: .v1 - ) - - // Start the server and print its address once it has started. - let server = try await Server.insecure(group: MultiThreadedEventLoopGroup.singleton) - .withServiceProviders([reflectionService, GreeterProvider(), EchoProvider()]) - .bind(host: "localhost", port: 1234) - .get() - - print("server started on port \(server.channel.localAddress!.port!)") - // Wait on the server's `onClose` future to stop the program from exiting. - try await server.onClose.get() - } -} diff --git a/Examples/v1/RouteGuide/Client/Empty.swift b/Examples/v1/RouteGuide/Client/Empty.swift deleted file mode 100644 index f8c631871..000000000 --- a/Examples/v1/RouteGuide/Client/Empty.swift +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2022, 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. - */ - -// This file exists to workaround https://github.com/apple/swift/issues/55127. diff --git a/Examples/v1/RouteGuide/Client/RouteGuideClient.swift b/Examples/v1/RouteGuide/Client/RouteGuideClient.swift deleted file mode 100644 index f3171b92f..000000000 --- a/Examples/v1/RouteGuide/Client/RouteGuideClient.swift +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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 ArgumentParser -import Foundation -import GRPC -import NIOCore -import NIOPosix -import RouteGuideModel - -/// Loads the features from `route_guide_db.json`, assumed to be in the directory above this file. -func loadFeatures() throws -> [Routeguide_Feature] { - let url = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // main.swift - .deletingLastPathComponent() // Client/ - .appendingPathComponent("route_guide_db.json") - - let data = try Data(contentsOf: url) - return try Routeguide_Feature.array(fromJSONUTF8Data: data) -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct RouteGuideExample { - private let routeGuide: Routeguide_RouteGuideAsyncClient - private let features: [Routeguide_Feature] - - init(routeGuide: Routeguide_RouteGuideAsyncClient, features: [Routeguide_Feature]) { - self.routeGuide = routeGuide - self.features = features - } - - func run() async { - // Look for a valid feature. - await self.getFeature(latitude: 409_146_138, longitude: -746_188_906) - - // Look for a missing feature. - await self.getFeature(latitude: 0, longitude: 0) - - // Looking for features between 40, -75 and 42, -73. - await self.listFeatures( - lowLatitude: 400_000_000, - lowLongitude: -750_000_000, - highLatitude: 420_000_000, - highLongitude: -730_000_000 - ) - - // Record a few randomly selected points from the features file. - await self.recordRoute(features: self.features, featuresToVisit: 10) - - // Send and receive some notes. - await self.routeChat() - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension RouteGuideExample { - /// Get the feature at the given latitude and longitude, if one exists. - private func getFeature(latitude: Int, longitude: Int) async { - print("\n→ GetFeature: lat=\(latitude) lon=\(longitude)") - - let point: Routeguide_Point = .with { - $0.latitude = numericCast(latitude) - $0.longitude = numericCast(longitude) - } - - do { - let feature = try await self.routeGuide.getFeature(point) - - if !feature.name.isEmpty { - print("Found feature called '\(feature.name)' at \(feature.location.formatted)") - } else { - print("Found no feature at \(feature.location.formatted)") - } - } catch { - print("RPC failed: \(error)") - } - } - - /// List all features in the area bounded by the high and low latitude and longitudes. - private func listFeatures( - lowLatitude: Int, - lowLongitude: Int, - highLatitude: Int, - highLongitude: Int - ) async { - print( - "\n→ ListFeatures: lowLat=\(lowLatitude) lowLon=\(lowLongitude), hiLat=\(highLatitude) hiLon=\(highLongitude)" - ) - - let rectangle: Routeguide_Rectangle = .with { - $0.lo = .with { - $0.latitude = numericCast(lowLatitude) - $0.longitude = numericCast(lowLongitude) - } - $0.hi = .with { - $0.latitude = numericCast(highLatitude) - $0.longitude = numericCast(highLongitude) - } - } - - do { - var resultCount = 1 - for try await feature in self.routeGuide.listFeatures(rectangle) { - print("Result #\(resultCount): \(feature.name) at \(feature.location.formatted)") - resultCount += 1 - } - } catch { - print("RPC failed: \(error)") - } - } - - /// Record a route for `featuresToVisit` features selected randomly from `features` and print a - /// summary of the route. - private func recordRoute( - features: [Routeguide_Feature], - featuresToVisit: Int - ) async { - print("\n→ RecordRoute") - let recordRoute = self.routeGuide.makeRecordRouteCall() - - do { - for i in 1 ... featuresToVisit { - if let feature = features.randomElement() { - let point = feature.location - print("Visiting point #\(i) at \(point.formatted)") - try await recordRoute.requestStream.send(point) - - // Sleep for 0.2s ... 1.0s before sending the next point. - try await Task.sleep(nanoseconds: UInt64.random(in: UInt64(2e8) ... UInt64(1e9))) - } - } - - recordRoute.requestStream.finish() - let summary = try await recordRoute.response - - print( - "Finished trip with \(summary.pointCount) points. Passed \(summary.featureCount) features. " - + "Travelled \(summary.distance) meters. It took \(summary.elapsedTime) seconds." - ) - } catch { - print("RecordRoute Failed: \(error)") - } - } - - /// Record notes at given locations, printing each all other messages which have previously been - /// recorded at the same location. - private func routeChat() async { - print("\n→ RouteChat") - - let notes = [ - ("First message", 0, 0), - ("Second message", 0, 1), - ("Third message", 1, 0), - ("Fourth message", 1, 1), - ].map { message, latitude, longitude in - Routeguide_RouteNote.with { - $0.message = message - $0.location = .with { - $0.latitude = Int32(latitude) - $0.longitude = Int32(longitude) - } - } - } - - do { - try await withThrowingTaskGroup(of: Void.self) { group in - let routeChat = self.routeGuide.makeRouteChatCall() - - // Add a task to send each message adding a small sleep between each. - group.addTask { - for note in notes { - print("Sending message '\(note.message)' at \(note.location.formatted)") - try await routeChat.requestStream.send(note) - // Sleep for 0.2s ... 1.0s before sending the next note. - try await Task.sleep(nanoseconds: UInt64.random(in: UInt64(2e8) ... UInt64(1e9))) - } - - routeChat.requestStream.finish() - } - - // Add a task to print each message received on the response stream. - group.addTask { - for try await note in routeChat.responseStream { - print("Received message '\(note.message)' at \(note.location.formatted)") - } - } - - try await group.waitForAll() - } - } catch { - print("RouteChat Failed: \(error)") - } - } -} - -@main -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct RouteGuide: AsyncParsableCommand { - @Option(help: "The port to connect to") - var port: Int = 1234 - - func run() async throws { - // Load the features. - let features = try loadFeatures() - - let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) - defer { - try? group.syncShutdownGracefully() - } - - let channel = try GRPCChannelPool.with( - target: .host("localhost", port: self.port), - transportSecurity: .plaintext, - eventLoopGroup: group - ) - defer { - try? channel.close().wait() - } - - let routeGuide = Routeguide_RouteGuideAsyncClient(channel: channel) - let example = RouteGuideExample(routeGuide: routeGuide, features: features) - await example.run() - } -} - -extension Routeguide_Point { - var formatted: String { - return "(\(self.latitude), \(self.longitude))" - } -} diff --git a/Examples/v1/RouteGuide/Model/route_guide.grpc.swift b/Examples/v1/RouteGuide/Model/route_guide.grpc.swift deleted file mode 100644 index 71b372d35..000000000 --- a/Examples/v1/RouteGuide/Model/route_guide.grpc.swift +++ /dev/null @@ -1,682 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: route_guide.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Interface exported by the server. -/// -/// Usage: instantiate `Routeguide_RouteGuideClient`, then call methods of this protocol to make API calls. -public protocol Routeguide_RouteGuideClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? { get } - - func getFeature( - _ request: Routeguide_Point, - callOptions: CallOptions? - ) -> UnaryCall - - func listFeatures( - _ request: Routeguide_Rectangle, - callOptions: CallOptions?, - handler: @escaping (Routeguide_Feature) -> Void - ) -> ServerStreamingCall - - func recordRoute( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func routeChat( - callOptions: CallOptions?, - handler: @escaping (Routeguide_RouteNote) -> Void - ) -> BidirectionalStreamingCall -} - -extension Routeguide_RouteGuideClientProtocol { - public var serviceName: String { - return "routeguide.RouteGuide" - } - - /// A simple RPC. - /// - /// Obtains the feature at a given position. - /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. - /// - /// - Parameters: - /// - request: Request to send to GetFeature. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func getFeature( - _ request: Routeguide_Point, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Routeguide_RouteGuideClientMetadata.Methods.getFeature.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetFeatureInterceptors() ?? [] - ) - } - - /// A server-to-client streaming RPC. - /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. - /// - /// - Parameters: - /// - request: Request to send to ListFeatures. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - public func listFeatures( - _ request: Routeguide_Rectangle, - callOptions: CallOptions? = nil, - handler: @escaping (Routeguide_Feature) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.listFeatures.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeListFeaturesInterceptors() ?? [], - handler: handler - ) - } - - /// A client-to-server streaming RPC. - /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. - public func recordRoute( - callOptions: CallOptions? = nil - ) -> ClientStreamingCall { - return self.makeClientStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.recordRoute.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRecordRouteInterceptors() ?? [] - ) - } - - /// A Bidirectional streaming RPC. - /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func routeChat( - callOptions: CallOptions? = nil, - handler: @escaping (Routeguide_RouteNote) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.routeChat.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRouteChatInterceptors() ?? [], - handler: handler - ) - } -} - -@available(*, deprecated) -extension Routeguide_RouteGuideClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Routeguide_RouteGuideNIOClient") -public final class Routeguide_RouteGuideClient: Routeguide_RouteGuideClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the routeguide.RouteGuide service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Routeguide_RouteGuideNIOClient: Routeguide_RouteGuideClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? - - /// Creates a client for the routeguide.RouteGuide service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// Interface exported by the server. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Routeguide_RouteGuideAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? { get } - - func makeGetFeatureCall( - _ request: Routeguide_Point, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeListFeaturesCall( - _ request: Routeguide_Rectangle, - callOptions: CallOptions? - ) -> GRPCAsyncServerStreamingCall - - func makeRecordRouteCall( - callOptions: CallOptions? - ) -> GRPCAsyncClientStreamingCall - - func makeRouteChatCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Routeguide_RouteGuideAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Routeguide_RouteGuideClientMetadata.serviceDescriptor - } - - public var interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? { - return nil - } - - public func makeGetFeatureCall( - _ request: Routeguide_Point, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Routeguide_RouteGuideClientMetadata.Methods.getFeature.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetFeatureInterceptors() ?? [] - ) - } - - public func makeListFeaturesCall( - _ request: Routeguide_Rectangle, - callOptions: CallOptions? = nil - ) -> GRPCAsyncServerStreamingCall { - return self.makeAsyncServerStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.listFeatures.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeListFeaturesInterceptors() ?? [] - ) - } - - public func makeRecordRouteCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncClientStreamingCall { - return self.makeAsyncClientStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.recordRoute.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRecordRouteInterceptors() ?? [] - ) - } - - public func makeRouteChatCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.routeChat.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRouteChatInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Routeguide_RouteGuideAsyncClientProtocol { - public func getFeature( - _ request: Routeguide_Point, - callOptions: CallOptions? = nil - ) async throws -> Routeguide_Feature { - return try await self.performAsyncUnaryCall( - path: Routeguide_RouteGuideClientMetadata.Methods.getFeature.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetFeatureInterceptors() ?? [] - ) - } - - public func listFeatures( - _ request: Routeguide_Rectangle, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream { - return self.performAsyncServerStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.listFeatures.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeListFeaturesInterceptors() ?? [] - ) - } - - public func recordRoute( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Routeguide_RouteSummary where RequestStream: Sequence, RequestStream.Element == Routeguide_Point { - return try await self.performAsyncClientStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.recordRoute.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRecordRouteInterceptors() ?? [] - ) - } - - public func recordRoute( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Routeguide_RouteSummary where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Routeguide_Point { - return try await self.performAsyncClientStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.recordRoute.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRecordRouteInterceptors() ?? [] - ) - } - - public func routeChat( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Routeguide_RouteNote { - return self.performAsyncBidirectionalStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.routeChat.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRouteChatInterceptors() ?? [] - ) - } - - public func routeChat( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Routeguide_RouteNote { - return self.performAsyncBidirectionalStreamingCall( - path: Routeguide_RouteGuideClientMetadata.Methods.routeChat.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeRouteChatInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Routeguide_RouteGuideAsyncClient: Routeguide_RouteGuideAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Routeguide_RouteGuideClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -public protocol Routeguide_RouteGuideClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'getFeature'. - func makeGetFeatureInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'listFeatures'. - func makeListFeaturesInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'recordRoute'. - func makeRecordRouteInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'routeChat'. - func makeRouteChatInterceptors() -> [ClientInterceptor] -} - -public enum Routeguide_RouteGuideClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "RouteGuide", - fullName: "routeguide.RouteGuide", - methods: [ - Routeguide_RouteGuideClientMetadata.Methods.getFeature, - Routeguide_RouteGuideClientMetadata.Methods.listFeatures, - Routeguide_RouteGuideClientMetadata.Methods.recordRoute, - Routeguide_RouteGuideClientMetadata.Methods.routeChat, - ] - ) - - public enum Methods { - public static let getFeature = GRPCMethodDescriptor( - name: "GetFeature", - path: "/routeguide.RouteGuide/GetFeature", - type: GRPCCallType.unary - ) - - public static let listFeatures = GRPCMethodDescriptor( - name: "ListFeatures", - path: "/routeguide.RouteGuide/ListFeatures", - type: GRPCCallType.serverStreaming - ) - - public static let recordRoute = GRPCMethodDescriptor( - name: "RecordRoute", - path: "/routeguide.RouteGuide/RecordRoute", - type: GRPCCallType.clientStreaming - ) - - public static let routeChat = GRPCMethodDescriptor( - name: "RouteChat", - path: "/routeguide.RouteGuide/RouteChat", - type: GRPCCallType.bidirectionalStreaming - ) - } -} - -/// Interface exported by the server. -/// -/// To build a server, implement a class that conforms to this protocol. -public protocol Routeguide_RouteGuideProvider: CallHandlerProvider { - var interceptors: Routeguide_RouteGuideServerInterceptorFactoryProtocol? { get } - - /// A simple RPC. - /// - /// Obtains the feature at a given position. - /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. - func getFeature(request: Routeguide_Point, context: StatusOnlyCallContext) -> EventLoopFuture - - /// A server-to-client streaming RPC. - /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. - func listFeatures(request: Routeguide_Rectangle, context: StreamingResponseCallContext) -> EventLoopFuture - - /// A client-to-server streaming RPC. - /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. - func recordRoute(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// A Bidirectional streaming RPC. - /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). - func routeChat(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Routeguide_RouteGuideProvider { - public var serviceName: Substring { - return Routeguide_RouteGuideServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "GetFeature": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeGetFeatureInterceptors() ?? [], - userFunction: self.getFeature(request:context:) - ) - - case "ListFeatures": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeListFeaturesInterceptors() ?? [], - userFunction: self.listFeatures(request:context:) - ) - - case "RecordRoute": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRecordRouteInterceptors() ?? [], - observerFactory: self.recordRoute(context:) - ) - - case "RouteChat": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRouteChatInterceptors() ?? [], - observerFactory: self.routeChat(context:) - ) - - default: - return nil - } - } -} - -/// Interface exported by the server. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Routeguide_RouteGuideAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Routeguide_RouteGuideServerInterceptorFactoryProtocol? { get } - - /// A simple RPC. - /// - /// Obtains the feature at a given position. - /// - /// A feature with an empty name is returned if there's no feature at the given - /// position. - func getFeature( - request: Routeguide_Point, - context: GRPCAsyncServerCallContext - ) async throws -> Routeguide_Feature - - /// A server-to-client streaming RPC. - /// - /// Obtains the Features available within the given Rectangle. Results are - /// streamed rather than returned at once (e.g. in a response message with a - /// repeated field), as the rectangle may cover a large area and contain a - /// huge number of features. - func listFeatures( - request: Routeguide_Rectangle, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// A client-to-server streaming RPC. - /// - /// Accepts a stream of Points on a route being traversed, returning a - /// RouteSummary when traversal is completed. - func recordRoute( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Routeguide_RouteSummary - - /// A Bidirectional streaming RPC. - /// - /// Accepts a stream of RouteNotes sent while a route is being traversed, - /// while receiving other RouteNotes (e.g. from other users). - func routeChat( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Routeguide_RouteGuideAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Routeguide_RouteGuideServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Routeguide_RouteGuideServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Routeguide_RouteGuideServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "GetFeature": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeGetFeatureInterceptors() ?? [], - wrapping: { try await self.getFeature(request: $0, context: $1) } - ) - - case "ListFeatures": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeListFeaturesInterceptors() ?? [], - wrapping: { try await self.listFeatures(request: $0, responseStream: $1, context: $2) } - ) - - case "RecordRoute": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRecordRouteInterceptors() ?? [], - wrapping: { try await self.recordRoute(requestStream: $0, context: $1) } - ) - - case "RouteChat": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeRouteChatInterceptors() ?? [], - wrapping: { try await self.routeChat(requestStream: $0, responseStream: $1, context: $2) } - ) - - default: - return nil - } - } -} - -public protocol Routeguide_RouteGuideServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'getFeature'. - /// Defaults to calling `self.makeInterceptors()`. - func makeGetFeatureInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'listFeatures'. - /// Defaults to calling `self.makeInterceptors()`. - func makeListFeaturesInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'recordRoute'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRecordRouteInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'routeChat'. - /// Defaults to calling `self.makeInterceptors()`. - func makeRouteChatInterceptors() -> [ServerInterceptor] -} - -public enum Routeguide_RouteGuideServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "RouteGuide", - fullName: "routeguide.RouteGuide", - methods: [ - Routeguide_RouteGuideServerMetadata.Methods.getFeature, - Routeguide_RouteGuideServerMetadata.Methods.listFeatures, - Routeguide_RouteGuideServerMetadata.Methods.recordRoute, - Routeguide_RouteGuideServerMetadata.Methods.routeChat, - ] - ) - - public enum Methods { - public static let getFeature = GRPCMethodDescriptor( - name: "GetFeature", - path: "/routeguide.RouteGuide/GetFeature", - type: GRPCCallType.unary - ) - - public static let listFeatures = GRPCMethodDescriptor( - name: "ListFeatures", - path: "/routeguide.RouteGuide/ListFeatures", - type: GRPCCallType.serverStreaming - ) - - public static let recordRoute = GRPCMethodDescriptor( - name: "RecordRoute", - path: "/routeguide.RouteGuide/RecordRoute", - type: GRPCCallType.clientStreaming - ) - - public static let routeChat = GRPCMethodDescriptor( - name: "RouteChat", - path: "/routeguide.RouteGuide/RouteChat", - type: GRPCCallType.bidirectionalStreaming - ) - } -} diff --git a/Examples/v1/RouteGuide/Model/route_guide.pb.swift b/Examples/v1/RouteGuide/Model/route_guide.pb.swift deleted file mode 100644 index 04669f032..000000000 --- a/Examples/v1/RouteGuide/Model/route_guide.pb.swift +++ /dev/null @@ -1,387 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: route_guide.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// Points are represented as latitude-longitude pairs in the E7 representation -/// (degrees multiplied by 10**7 and rounded to the nearest integer). -/// Latitudes should be in the range +/- 90 degrees and longitude should be in -/// the range +/- 180 degrees (inclusive). -public struct Routeguide_Point: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var latitude: Int32 = 0 - - public var longitude: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A latitude-longitude rectangle, represented as two diagonally opposite -/// points "lo" and "hi". -public struct Routeguide_Rectangle: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// One corner of the rectangle. - public var lo: Routeguide_Point { - get {return _lo ?? Routeguide_Point()} - set {_lo = newValue} - } - /// Returns true if `lo` has been explicitly set. - public var hasLo: Bool {return self._lo != nil} - /// Clears the value of `lo`. Subsequent reads from it will return its default value. - public mutating func clearLo() {self._lo = nil} - - /// The other corner of the rectangle. - public var hi: Routeguide_Point { - get {return _hi ?? Routeguide_Point()} - set {_hi = newValue} - } - /// Returns true if `hi` has been explicitly set. - public var hasHi: Bool {return self._hi != nil} - /// Clears the value of `hi`. Subsequent reads from it will return its default value. - public mutating func clearHi() {self._hi = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _lo: Routeguide_Point? = nil - fileprivate var _hi: Routeguide_Point? = nil -} - -/// A feature names something at a given point. -/// -/// If a feature could not be named, the name is empty. -public struct Routeguide_Feature: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The name of the feature. - public var name: String = String() - - /// The point where the feature is detected. - public var location: Routeguide_Point { - get {return _location ?? Routeguide_Point()} - set {_location = newValue} - } - /// Returns true if `location` has been explicitly set. - public var hasLocation: Bool {return self._location != nil} - /// Clears the value of `location`. Subsequent reads from it will return its default value. - public mutating func clearLocation() {self._location = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _location: Routeguide_Point? = nil -} - -/// A RouteNote is a message sent while at a given point. -public struct Routeguide_RouteNote: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The location from which the message is sent. - public var location: Routeguide_Point { - get {return _location ?? Routeguide_Point()} - set {_location = newValue} - } - /// Returns true if `location` has been explicitly set. - public var hasLocation: Bool {return self._location != nil} - /// Clears the value of `location`. Subsequent reads from it will return its default value. - public mutating func clearLocation() {self._location = nil} - - /// The message to be sent. - public var message: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _location: Routeguide_Point? = nil -} - -/// A RouteSummary is received in response to a RecordRoute rpc. -/// -/// It contains the number of individual points received, the number of -/// detected features, and the total distance covered as the cumulative sum of -/// the distance between each point. -public struct Routeguide_RouteSummary: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of points received. - public var pointCount: Int32 = 0 - - /// The number of known features passed while traversing the route. - public var featureCount: Int32 = 0 - - /// The distance covered in metres. - public var distance: Int32 = 0 - - /// The duration of the traversal in seconds. - public var elapsedTime: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "routeguide" - -extension Routeguide_Point: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Point" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "latitude"), - 2: .same(proto: "longitude"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.latitude) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.longitude) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.latitude != 0 { - try visitor.visitSingularInt32Field(value: self.latitude, fieldNumber: 1) - } - if self.longitude != 0 { - try visitor.visitSingularInt32Field(value: self.longitude, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Routeguide_Point, rhs: Routeguide_Point) -> Bool { - if lhs.latitude != rhs.latitude {return false} - if lhs.longitude != rhs.longitude {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_Rectangle: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Rectangle" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "lo"), - 2: .same(proto: "hi"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._lo) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._hi) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._lo { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._hi { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Routeguide_Rectangle, rhs: Routeguide_Rectangle) -> Bool { - if lhs._lo != rhs._lo {return false} - if lhs._hi != rhs._hi {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_Feature: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Feature" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .same(proto: "location"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._location) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try { if let v = self._location { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Routeguide_Feature, rhs: Routeguide_Feature) -> Bool { - if lhs.name != rhs.name {return false} - if lhs._location != rhs._location {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_RouteNote: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".RouteNote" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "location"), - 2: .same(proto: "message"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._location) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._location { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Routeguide_RouteNote, rhs: Routeguide_RouteNote) -> Bool { - if lhs._location != rhs._location {return false} - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Routeguide_RouteSummary: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".RouteSummary" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "point_count"), - 2: .standard(proto: "feature_count"), - 3: .same(proto: "distance"), - 4: .standard(proto: "elapsed_time"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.pointCount) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.featureCount) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &self.distance) }() - case 4: try { try decoder.decodeSingularInt32Field(value: &self.elapsedTime) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.pointCount != 0 { - try visitor.visitSingularInt32Field(value: self.pointCount, fieldNumber: 1) - } - if self.featureCount != 0 { - try visitor.visitSingularInt32Field(value: self.featureCount, fieldNumber: 2) - } - if self.distance != 0 { - try visitor.visitSingularInt32Field(value: self.distance, fieldNumber: 3) - } - if self.elapsedTime != 0 { - try visitor.visitSingularInt32Field(value: self.elapsedTime, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Routeguide_RouteSummary, rhs: Routeguide_RouteSummary) -> Bool { - if lhs.pointCount != rhs.pointCount {return false} - if lhs.featureCount != rhs.featureCount {return false} - if lhs.distance != rhs.distance {return false} - if lhs.elapsedTime != rhs.elapsedTime {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Examples/v1/RouteGuide/README.md b/Examples/v1/RouteGuide/README.md deleted file mode 100644 index fe553deab..000000000 --- a/Examples/v1/RouteGuide/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Route Guide - A sample gRPC Application - -This directory contains the source and generated code for the gRPC "Route Guide" -example. - -The tutorial relating to this example can be found in -[grpc-swift/docs/basic-tutorial.md][basic-tutorial]. - -## Running - -To start the server, from the root of this package run: - -```sh -$ swift run RouteGuideServer -``` - -From another terminal, run the client: - -```sh -$ swift run RouteGuideClient -``` - -## Regenerating client and server code - -For simplicity, a shell script ([grpc-swift/Protos/generate.sh][run-protoc]) is provided -to generate client and server code: - -```sh -$ Protos/generate.sh -``` - -[basic-tutorial]: ../../../docs/basic-tutorial.md -[run-protoc]: ../../../Protos/generate.sh diff --git a/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift b/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift deleted file mode 100644 index 8b4402515..000000000 --- a/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift +++ /dev/null @@ -1,174 +0,0 @@ -/* - * 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 Foundation -import GRPC -import NIOConcurrencyHelpers -import NIOCore -import RouteGuideModel - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final class RouteGuideProvider: Routeguide_RouteGuideAsyncProvider { - private let features: [Routeguide_Feature] - private let notes: Notes - - internal init(features: [Routeguide_Feature]) { - self.features = features - self.notes = Notes() - } - - internal func getFeature( - request point: Routeguide_Point, - context: GRPCAsyncServerCallContext - ) async throws -> Routeguide_Feature { - return self.lookupFeature(at: point) ?? .unnamedFeature(at: point) - } - - internal func listFeatures( - request: Routeguide_Rectangle, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - let longitudeRange = request.lo.longitude ... request.hi.longitude - let latitudeRange = request.lo.latitude ... request.hi.latitude - - for feature in self.features where !feature.name.isEmpty { - if feature.location.isWithin(latitude: latitudeRange, longitude: longitudeRange) { - try await responseStream.send(feature) - } - } - } - - internal func recordRoute( - requestStream points: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Routeguide_RouteSummary { - var pointCount: Int32 = 0 - var featureCount: Int32 = 0 - var distance = 0.0 - var previousPoint: Routeguide_Point? - let startTimeNanos = DispatchTime.now().uptimeNanoseconds - - for try await point in points { - pointCount += 1 - - if let feature = self.lookupFeature(at: point), !feature.name.isEmpty { - featureCount += 1 - } - - if let previous = previousPoint { - distance += previous.distance(to: point) - } - - previousPoint = point - } - - let durationInNanos = DispatchTime.now().uptimeNanoseconds - startTimeNanos - let durationInSeconds = Double(durationInNanos) / 1e9 - - return .with { - $0.pointCount = pointCount - $0.featureCount = featureCount - $0.elapsedTime = Int32(durationInSeconds) - $0.distance = Int32(distance) - } - } - - internal func routeChat( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await note in requestStream { - let existingNotes = await self.notes.addNote(note, to: note.location) - - // Respond with all existing notes. - for existingNote in existingNotes { - try await responseStream.send(existingNote) - } - } - } - - /// Returns a feature at the given location or an unnamed feature if none exist at that location. - private func lookupFeature(at location: Routeguide_Point) -> Routeguide_Feature? { - return self.features.first(where: { - $0.location.latitude == location.latitude && $0.location.longitude == location.longitude - }) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final actor Notes { - private var recordedNotes: [Routeguide_Point: [Routeguide_RouteNote]] - - internal init() { - self.recordedNotes = [:] - } - - /// Record a note at the given location and return the all notes which were previously recorded - /// at the location. - internal func addNote( - _ note: Routeguide_RouteNote, - to location: Routeguide_Point - ) -> ArraySlice { - self.recordedNotes[location, default: []].append(note) - return self.recordedNotes[location]!.dropLast(1) - } -} - -private func degreesToRadians(_ degrees: Double) -> Double { - return degrees * .pi / 180.0 -} - -extension Routeguide_Point { - fileprivate func distance(to other: Routeguide_Point) -> Double { - // Radius of Earth in meters - let radius = 6_371_000.0 - // Points are in the E7 representation (degrees multiplied by 10**7 and rounded to the nearest - // integer). See also `Routeguide_Point`. - let coordinateFactor = 1.0e7 - - let lat1 = degreesToRadians(Double(self.latitude) / coordinateFactor) - let lat2 = degreesToRadians(Double(other.latitude) / coordinateFactor) - let lon1 = degreesToRadians(Double(self.longitude) / coordinateFactor) - let lon2 = degreesToRadians(Double(other.longitude) / coordinateFactor) - - let deltaLat = lat2 - lat1 - let deltaLon = lon2 - lon1 - - let a = - sin(deltaLat / 2) * sin(deltaLat / 2) - + cos(lat1) * cos(lat2) * sin(deltaLon / 2) * sin(deltaLon / 2) - let c = 2 * atan2(sqrt(a), sqrt(1 - a)) - - return radius * c - } - - func isWithin( - latitude: Range, - longitude: Range - ) -> Bool where Range.Bound == Int32 { - return latitude.contains(self.latitude) && longitude.contains(self.longitude) - } -} - -extension Routeguide_Feature { - static func unnamedFeature(at location: Routeguide_Point) -> Routeguide_Feature { - return .with { - $0.name = "" - $0.location = location - } - } -} diff --git a/Examples/v1/RouteGuide/Server/RouteGuideServer.swift b/Examples/v1/RouteGuide/Server/RouteGuideServer.swift deleted file mode 100644 index 1551b4fce..000000000 --- a/Examples/v1/RouteGuide/Server/RouteGuideServer.swift +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 ArgumentParser -import GRPC -import NIOCore -import NIOPosix -import RouteGuideModel - -import struct Foundation.Data -import struct Foundation.URL - -/// Loads the features from `route_guide_db.json`, assumed to be in the directory above this file. -func loadFeatures() throws -> [Routeguide_Feature] { - let url = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // main.swift - .deletingLastPathComponent() // Server/ - .appendingPathComponent("route_guide_db.json") - - let data = try Data(contentsOf: url) - return try Routeguide_Feature.array(fromJSONUTF8Data: data) -} - -@main -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct RouteGuide: AsyncParsableCommand { - @Option(help: "The port to listen on for new connections") - var port = 1234 - - func run() async throws { - // Create an event loop group for the server to run on. - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - try! group.syncShutdownGracefully() - } - - // Read the feature database. - let features = try loadFeatures() - - // Create a provider using the features we read. - let provider = RouteGuideProvider(features: features) - - // Start the server and print its address once it has started. - let server = try await Server.insecure(group: group) - .withServiceProviders([provider]) - .bind(host: "localhost", port: self.port) - .get() - - print("server started on port \(server.channel.localAddress!.port!)") - - // Wait on the server's `onClose` future to stop the program from exiting. - try await server.onClose.get() - } -} diff --git a/Examples/v1/RouteGuide/route_guide_db.json b/Examples/v1/RouteGuide/route_guide_db.json deleted file mode 100644 index 9342beb57..000000000 --- a/Examples/v1/RouteGuide/route_guide_db.json +++ /dev/null @@ -1,601 +0,0 @@ -[{ - "location": { - "latitude": 407838351, - "longitude": -746143763 - }, - "name": "Patriots Path, Mendham, NJ 07945, USA" -}, { - "location": { - "latitude": 408122808, - "longitude": -743999179 - }, - "name": "101 New Jersey 10, Whippany, NJ 07981, USA" -}, { - "location": { - "latitude": 413628156, - "longitude": -749015468 - }, - "name": "U.S. 6, Shohola, PA 18458, USA" -}, { - "location": { - "latitude": 419999544, - "longitude": -740371136 - }, - "name": "5 Conners Road, Kingston, NY 12401, USA" -}, { - "location": { - "latitude": 414008389, - "longitude": -743951297 - }, - "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA" -}, { - "location": { - "latitude": 419611318, - "longitude": -746524769 - }, - "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA" -}, { - "location": { - "latitude": 406109563, - "longitude": -742186778 - }, - "name": "4001 Tremley Point Road, Linden, NJ 07036, USA" -}, { - "location": { - "latitude": 416802456, - "longitude": -742370183 - }, - "name": "352 South Mountain Road, Wallkill, NY 12589, USA" -}, { - "location": { - "latitude": 412950425, - "longitude": -741077389 - }, - "name": "Bailey Turn Road, Harriman, NY 10926, USA" -}, { - "location": { - "latitude": 412144655, - "longitude": -743949739 - }, - "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA" -}, { - "location": { - "latitude": 415736605, - "longitude": -742847522 - }, - "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA" -}, { - "location": { - "latitude": 413843930, - "longitude": -740501726 - }, - "name": "162 Merrill Road, Highland Mills, NY 10930, USA" -}, { - "location": { - "latitude": 410873075, - "longitude": -744459023 - }, - "name": "Clinton Road, West Milford, NJ 07480, USA" -}, { - "location": { - "latitude": 412346009, - "longitude": -744026814 - }, - "name": "16 Old Brook Lane, Warwick, NY 10990, USA" -}, { - "location": { - "latitude": 402948455, - "longitude": -747903913 - }, - "name": "3 Drake Lane, Pennington, NJ 08534, USA" -}, { - "location": { - "latitude": 406337092, - "longitude": -740122226 - }, - "name": "6324 8th Avenue, Brooklyn, NY 11220, USA" -}, { - "location": { - "latitude": 406421967, - "longitude": -747727624 - }, - "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA" -}, { - "location": { - "latitude": 416318082, - "longitude": -749677716 - }, - "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA" -}, { - "location": { - "latitude": 415301720, - "longitude": -748416257 - }, - "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA" -}, { - "location": { - "latitude": 402647019, - "longitude": -747071791 - }, - "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA" -}, { - "location": { - "latitude": 412567807, - "longitude": -741058078 - }, - "name": "New York State Reference Route 987E, Southfields, NY 10975, USA" -}, { - "location": { - "latitude": 416855156, - "longitude": -744420597 - }, - "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA" -}, { - "location": { - "latitude": 404663628, - "longitude": -744820157 - }, - "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA" -}, { - "location": { - "latitude": 407113723, - "longitude": -749746483 - }, - "name": "" -}, { - "location": { - "latitude": 402133926, - "longitude": -743613249 - }, - "name": "" -}, { - "location": { - "latitude": 400273442, - "longitude": -741220915 - }, - "name": "" -}, { - "location": { - "latitude": 411236786, - "longitude": -744070769 - }, - "name": "" -}, { - "location": { - "latitude": 411633782, - "longitude": -746784970 - }, - "name": "211-225 Plains Road, Augusta, NJ 07822, USA" -}, { - "location": { - "latitude": 415830701, - "longitude": -742952812 - }, - "name": "" -}, { - "location": { - "latitude": 413447164, - "longitude": -748712898 - }, - "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA" -}, { - "location": { - "latitude": 405047245, - "longitude": -749800722 - }, - "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA" -}, { - "location": { - "latitude": 418858923, - "longitude": -746156790 - }, - "name": "" -}, { - "location": { - "latitude": 417951888, - "longitude": -748484944 - }, - "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA" -}, { - "location": { - "latitude": 407033786, - "longitude": -743977337 - }, - "name": "26 East 3rd Street, New Providence, NJ 07974, USA" -}, { - "location": { - "latitude": 417548014, - "longitude": -740075041 - }, - "name": "" -}, { - "location": { - "latitude": 410395868, - "longitude": -744972325 - }, - "name": "" -}, { - "location": { - "latitude": 404615353, - "longitude": -745129803 - }, - "name": "" -}, { - "location": { - "latitude": 406589790, - "longitude": -743560121 - }, - "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA" -}, { - "location": { - "latitude": 414653148, - "longitude": -740477477 - }, - "name": "18 Lannis Avenue, New Windsor, NY 12553, USA" -}, { - "location": { - "latitude": 405957808, - "longitude": -743255336 - }, - "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA" -}, { - "location": { - "latitude": 411733589, - "longitude": -741648093 - }, - "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA" -}, { - "location": { - "latitude": 412676291, - "longitude": -742606606 - }, - "name": "1270 Lakes Road, Monroe, NY 10950, USA" -}, { - "location": { - "latitude": 409224445, - "longitude": -748286738 - }, - "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA" -}, { - "location": { - "latitude": 406523420, - "longitude": -742135517 - }, - "name": "652 Garden Street, Elizabeth, NJ 07202, USA" -}, { - "location": { - "latitude": 401827388, - "longitude": -740294537 - }, - "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA" -}, { - "location": { - "latitude": 410564152, - "longitude": -743685054 - }, - "name": "13-17 Stanley Street, West Milford, NJ 07480, USA" -}, { - "location": { - "latitude": 408472324, - "longitude": -740726046 - }, - "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA" -}, { - "location": { - "latitude": 412452168, - "longitude": -740214052 - }, - "name": "5 White Oak Lane, Stony Point, NY 10980, USA" -}, { - "location": { - "latitude": 409146138, - "longitude": -746188906 - }, - "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA" -}, { - "location": { - "latitude": 404701380, - "longitude": -744781745 - }, - "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA" -}, { - "location": { - "latitude": 409642566, - "longitude": -746017679 - }, - "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA" -}, { - "location": { - "latitude": 408031728, - "longitude": -748645385 - }, - "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA" -}, { - "location": { - "latitude": 413700272, - "longitude": -742135189 - }, - "name": "367 Prospect Road, Chester, NY 10918, USA" -}, { - "location": { - "latitude": 404310607, - "longitude": -740282632 - }, - "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA" -}, { - "location": { - "latitude": 409319800, - "longitude": -746201391 - }, - "name": "11 Ward Street, Mount Arlington, NJ 07856, USA" -}, { - "location": { - "latitude": 406685311, - "longitude": -742108603 - }, - "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA" -}, { - "location": { - "latitude": 419018117, - "longitude": -749142781 - }, - "name": "43 Dreher Road, Roscoe, NY 12776, USA" -}, { - "location": { - "latitude": 412856162, - "longitude": -745148837 - }, - "name": "Swan Street, Pine Island, NY 10969, USA" -}, { - "location": { - "latitude": 416560744, - "longitude": -746721964 - }, - "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA" -}, { - "location": { - "latitude": 405314270, - "longitude": -749836354 - }, - "name": "" -}, { - "location": { - "latitude": 414219548, - "longitude": -743327440 - }, - "name": "" -}, { - "location": { - "latitude": 415534177, - "longitude": -742900616 - }, - "name": "565 Winding Hills Road, Montgomery, NY 12549, USA" -}, { - "location": { - "latitude": 406898530, - "longitude": -749127080 - }, - "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA" -}, { - "location": { - "latitude": 407586880, - "longitude": -741670168 - }, - "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA" -}, { - "location": { - "latitude": 400106455, - "longitude": -742870190 - }, - "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA" -}, { - "location": { - "latitude": 400066188, - "longitude": -746793294 - }, - "name": "" -}, { - "location": { - "latitude": 418803880, - "longitude": -744102673 - }, - "name": "40 Mountain Road, Napanoch, NY 12458, USA" -}, { - "location": { - "latitude": 414204288, - "longitude": -747895140 - }, - "name": "" -}, { - "location": { - "latitude": 414777405, - "longitude": -740615601 - }, - "name": "" -}, { - "location": { - "latitude": 415464475, - "longitude": -747175374 - }, - "name": "48 North Road, Forestburgh, NY 12777, USA" -}, { - "location": { - "latitude": 404062378, - "longitude": -746376177 - }, - "name": "" -}, { - "location": { - "latitude": 405688272, - "longitude": -749285130 - }, - "name": "" -}, { - "location": { - "latitude": 400342070, - "longitude": -748788996 - }, - "name": "" -}, { - "location": { - "latitude": 401809022, - "longitude": -744157964 - }, - "name": "" -}, { - "location": { - "latitude": 404226644, - "longitude": -740517141 - }, - "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA" -}, { - "location": { - "latitude": 410322033, - "longitude": -747871659 - }, - "name": "" -}, { - "location": { - "latitude": 407100674, - "longitude": -747742727 - }, - "name": "" -}, { - "location": { - "latitude": 418811433, - "longitude": -741718005 - }, - "name": "213 Bush Road, Stone Ridge, NY 12484, USA" -}, { - "location": { - "latitude": 415034302, - "longitude": -743850945 - }, - "name": "" -}, { - "location": { - "latitude": 411349992, - "longitude": -743694161 - }, - "name": "" -}, { - "location": { - "latitude": 404839914, - "longitude": -744759616 - }, - "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA" -}, { - "location": { - "latitude": 414638017, - "longitude": -745957854 - }, - "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA" -}, { - "location": { - "latitude": 412127800, - "longitude": -740173578 - }, - "name": "" -}, { - "location": { - "latitude": 401263460, - "longitude": -747964303 - }, - "name": "" -}, { - "location": { - "latitude": 412843391, - "longitude": -749086026 - }, - "name": "" -}, { - "location": { - "latitude": 418512773, - "longitude": -743067823 - }, - "name": "" -}, { - "location": { - "latitude": 404318328, - "longitude": -740835638 - }, - "name": "42-102 Main Street, Belford, NJ 07718, USA" -}, { - "location": { - "latitude": 419020746, - "longitude": -741172328 - }, - "name": "" -}, { - "location": { - "latitude": 404080723, - "longitude": -746119569 - }, - "name": "" -}, { - "location": { - "latitude": 401012643, - "longitude": -744035134 - }, - "name": "" -}, { - "location": { - "latitude": 404306372, - "longitude": -741079661 - }, - "name": "" -}, { - "location": { - "latitude": 403966326, - "longitude": -748519297 - }, - "name": "" -}, { - "location": { - "latitude": 405002031, - "longitude": -748407866 - }, - "name": "" -}, { - "location": { - "latitude": 409532885, - "longitude": -742200683 - }, - "name": "" -}, { - "location": { - "latitude": 416851321, - "longitude": -742674555 - }, - "name": "" -}, { - "location": { - "latitude": 406411633, - "longitude": -741722051 - }, - "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA" -}, { - "location": { - "latitude": 413069058, - "longitude": -744597778 - }, - "name": "261 Van Sickle Road, Goshen, NY 10924, USA" -}, { - "location": { - "latitude": 418465462, - "longitude": -746859398 - }, - "name": "" -}, { - "location": { - "latitude": 411733222, - "longitude": -744228360 - }, - "name": "" -}, { - "location": { - "latitude": 410248224, - "longitude": -747127767 - }, - "name": "3 Hasta Way, Newton, NJ 07860, USA" -}] \ No newline at end of file diff --git a/FuzzTesting/.gitignore b/FuzzTesting/.gitignore deleted file mode 100644 index bb460e7be..000000000 --- a/FuzzTesting/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store -/.build -/Packages -/*.xcodeproj -xcuserdata/ -DerivedData/ -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-4739158818553856 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-4739158818553856 deleted file mode 100644 index e7972e42c4de2818fc54a8101cd90cf8f7c7d65e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 626 zcmWFt@>I}L@CXSB&^OXE;N{}w3ibt&3=9k`EI>2<4Q)c|H^@AA*SV06Eh+t;6 znhnyZV8zR&XcWu^GY8c`Ze#;(AO>=5dgmM44$^e3i;!(L)*33P z!MxpM?Y(F)b|C;Z%=}*Eab4V3Vs7}X9b*j#;>5OYj$dcRmVr|d9 zW)~VRJaCRYkOsRVKw4oC;B#c)e{BkjqtzMEBTx_S88stGj#FY2NJQtl!kY(1@IjCP z4Pf=QHqod@p${S0i-V9*%+w>B9gA$reUoVGZf8GoN#CSm%S9>#9nIDYa5oJ+DERuc zaap`OvE}1+OGy{P-WwiOL{scd+$9O*m z1R86%)a-dA?3p9aW~=#C1hSd`nZs;MR>r4MftYO;1AD}* ze^I4OMw&#tB~k~U?KDc~`EW5^#V(va-WJ$)<{X|p86DOJV!iZr$ylC}_B+|x$xc*Q zOZl{Jk!-Rgv`e>xC{=)<*$7!qn{N>suJgHQcDlOE)m(;I@UAR1L0NwTSkg;>Vxpbf z&Rop>pfbBz>dujgS3Xu}rtX;ebW6_q%W=r4glzV~pvbFX_Sry=1e33c7tpm-l)n~Y BjW+-Q diff --git a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5134158417494016 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-ServerFuzzer-release-5134158417494016 deleted file mode 100644 index 4b351f2d4957eab04fd9c1b5e52574025c285224..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30879 zcmeI*K}y3w6b4{%8SoC>xGCCEmsvIxy3#Jv1B5izqD(@K3q66SwkD#|BDkxFeh((_ zU@~v!yTCu2=Y!#3n&-1{J~}^2j?>TnqVFG?<+@0dWY+BqRe4j7Cfz5@n?=!9p^kCu z-#ksSI<{48PxHs7N(V*L+^x!@UDYux*G-vZ>1_VARsE^Me)ij}c3s5#>uP;k#j;+k z;xY{<<#Zlzt3{G*HskR)>(=;X)7MdQpa>8kK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF{9l1@ zgG9GtNk3Cbm*M|n?f2!QvoFE0`yKcznrw N;_NCucirVAdj!pYqt ziLy2fO&Au%Yu6<6s_UNjbv5kU5bye~j|R7M diff --git a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-debug-4645975625957376 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-debug-4645975625957376 deleted file mode 100644 index bacd8c03e..000000000 --- a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-debug-4645975625957376 +++ /dev/null @@ -1,4 +0,0 @@ -POST * HTTP/1.1 - -POST * HTTP/1.1 - diff --git a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-release-5413100925878272 b/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-release-5413100925878272 deleted file mode 100644 index 042108b7b..000000000 --- a/FuzzTesting/FailCases/clusterfuzz-testcase-minimized-grpc-swift-fuzz-release-5413100925878272 +++ /dev/null @@ -1,4 +0,0 @@ -PUT * HTTP/1.1 - -PUT * HTTP/1.1 - diff --git a/FuzzTesting/Package.swift b/FuzzTesting/Package.swift deleted file mode 100644 index 0f62ecf69..000000000 --- a/FuzzTesting/Package.swift +++ /dev/null @@ -1,57 +0,0 @@ -// swift-tools-version:5.8 -/* - * 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 PackageDescription - -let package = Package( - name: "grpc-swift-fuzzer", - dependencies: [ - .package(name: "grpc-swift", path: ".."), - .package(url: "https://github.com/apple/swift-nio.git", from: "2.27.0"), - ], - targets: [ - .executableTarget( - name: "ServerFuzzer", - dependencies: [ - .target(name: "ServerFuzzerLib"), - ] - ), - .target( - name: "ServerFuzzerLib", - dependencies: [ - .product(name: "GRPC", package: "grpc-swift"), - .product(name: "NIO", package: "swift-nio"), - .target(name: "EchoImplementation"), - ] - ), - .target( - name: "EchoModel", - dependencies: [ - .product(name: "GRPC", package: "grpc-swift"), - ], - exclude: [ - "echo.proto", - ] - ), - .target( - name: "EchoImplementation", - dependencies: [ - .product(name: "GRPC", package: "grpc-swift"), - .target(name: "EchoModel"), - ] - ), - ] -) diff --git a/FuzzTesting/README.md b/FuzzTesting/README.md deleted file mode 100644 index 38b5bd2fc..000000000 --- a/FuzzTesting/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# gRPC Swift: Fuzz Testing - -This package contains binaries for running fuzz testing. - -## Building - -Building the binary requires additional arguments be passed to the Swift -compiler: - -``` -swift build \ - -Xswiftc -sanitize=fuzzer,address \ - -Xswiftc -parse-as-library -``` - -Note also that on macOS the Swift toolchain shipped with Xcode _does not_ -currently include fuzzing support and one must use a toolchain -from [swift.org](https://swift.org/download/). Building on macOS therefore -requires the above command be run via `xcrun`: - -``` -xcrun --toolchain swift \ - swift build \ - -Xswiftc -sanitize=fuzzer,address \ - -Xswiftc -parse-as-library -``` - -## Failures - -The `FailCases` directory contains fuzzing test input which previously caused -failures in gRPC. diff --git a/FuzzTesting/Sources/EchoImplementation b/FuzzTesting/Sources/EchoImplementation deleted file mode 120000 index aeaf586b8..000000000 --- a/FuzzTesting/Sources/EchoImplementation +++ /dev/null @@ -1 +0,0 @@ -../../Examples/v1/Echo/Implementation \ No newline at end of file diff --git a/FuzzTesting/Sources/EchoModel b/FuzzTesting/Sources/EchoModel deleted file mode 120000 index 5561e573a..000000000 --- a/FuzzTesting/Sources/EchoModel +++ /dev/null @@ -1 +0,0 @@ -../../Examples/v1/Echo/Model \ No newline at end of file diff --git a/FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c b/FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c deleted file mode 100644 index d6ce95691..000000000 --- a/FuzzTesting/Sources/ServerFuzzer/src/serverfuzzer.c +++ /dev/null @@ -1,9 +0,0 @@ -#include -#include - -// Provided by ServerFuzzerLib. -int ServerFuzzer(const uint8_t *Data, size_t Size); - -int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { - return ServerFuzzer(data, size); -} diff --git a/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift b/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift deleted file mode 100644 index 0cbbaafa3..000000000 --- a/FuzzTesting/Sources/ServerFuzzerLib/ServerFuzzer.swift +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 EchoImplementation -import GRPC -import NIO - -@_cdecl("ServerFuzzer") -public func test(_ start: UnsafeRawPointer, _ count: Int) -> CInt { - let bytes = UnsafeRawBufferPointer(start: start, count: count) - - let channel = EmbeddedChannel() - try! channel.connect(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() - - defer { - _ = try? channel.finish() - } - - let configuration = Server.Configuration.default( - target: .unixDomainSocket("/ignored"), - eventLoopGroup: channel.eventLoop, - serviceProviders: [EchoProvider()] - ) - - var buffer = channel.allocator.buffer(capacity: count) - buffer.writeBytes(bytes) - - do { - try channel._configureForServerFuzzing(configuration: configuration) - try channel.writeInbound(buffer) - channel.embeddedEventLoop.run() - } catch { - // We're okay with errors. - } - - return 0 -} diff --git a/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift b/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift similarity index 100% rename from Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift rename to IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark/Benchmarks.swift diff --git a/Performance/Benchmarks/Package.swift b/IntegrationTests/Benchmarks/Package.swift similarity index 95% rename from Performance/Benchmarks/Package.swift rename to IntegrationTests/Benchmarks/Package.swift index 2c1fe63ab..6a818aab9 100644 --- a/Performance/Benchmarks/Package.swift +++ b/IntegrationTests/Benchmarks/Package.swift @@ -30,7 +30,7 @@ let package = Package( name: "GRPCSwiftBenchmark", dependencies: [ .product(name: "Benchmark", package: "package-benchmark"), - .product(name: "_GRPCCore", package: "grpc-swift") + .product(name: "GRPCCore", package: "grpc-swift") ], path: "Benchmarks/GRPCSwiftBenchmark", plugins: [ diff --git a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json rename to IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json diff --git a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json rename to IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json diff --git a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json diff --git a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json diff --git a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json diff --git a/Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename to IntegrationTests/Benchmarks/Thresholds/6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json rename to IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json rename to IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json diff --git a/Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json similarity index 100% rename from Performance/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename to IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json diff --git a/NOTICES.txt b/NOTICES.txt index 3a8509e83..11221c3e0 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -33,7 +33,7 @@ It also uses derivations of SwiftNIO's lock 'NIOLock.swift' and locked value box This product uses derivations of SwiftNIOHTTP2's implementation of case insensitive comparison of strings, found in 'HPACKHeader.swift'. - + * LICENSE (Apache License 2.0): * https://github.com/apple/swift-nio-http2/blob/main/LICENSE.txt * HOMEPAGE: @@ -41,31 +41,9 @@ insensitive comparison of strings, found in 'HPACKHeader.swift'. --- -This product contains a derivation of the backpressure aware async stream from -the Swift project. - - * LICENSE (Apache License 2.0): - * https://github.com/apple/swift/blob/main/LICENSE.txt - * HOMEPAGE: - * https://github.com/apple/swift - ---- - This product uses derivations of swift-extras/swift-extras-base64 'Base64.swift'. * LICENSE (Apache License 2.0): * https://github.com/swift-extras/swift-extras-base64/blob/main/LICENSE * HOMEPAGE: * https://github.com/swift-extras/swift-extras-base64 - ---- - -This product uses derivations of apple/swift-openapi-generator 'StructuredSwiftRepresentation.swift', -'TypeName.swift', 'TypeUsage.swift', 'Builtins.swift', 'RendererProtocol.swift', 'TextBasedProtocol', -'Test_TextBasedRenderer', and 'SnippetBasedReferenceTests.swift'. - - * LICENSE (Apache License 2.0): - * https://github.com/apple/swift-openapi-generator/blob/main/LICENSE.txt - * HOMEPAGE: - * https://github.com/apple/swift-openapi-generator - diff --git a/PATENTS b/PATENTS deleted file mode 100644 index 619f9dbfe..000000000 --- a/PATENTS +++ /dev/null @@ -1,22 +0,0 @@ -Additional IP Rights Grant (Patents) - -"This implementation" means the copyrightable works distributed by -Google as part of the GRPC project. - -Google hereby grants to You a perpetual, worldwide, non-exclusive, -no-charge, royalty-free, irrevocable (except as stated in this section) -patent license to make, have made, use, offer to sell, sell, import, -transfer and otherwise run, modify and propagate the contents of this -implementation of GRPC, where such license applies only to those patent -claims, both currently owned or controlled by Google and acquired in -the future, licensable by Google that are necessarily infringed by this -implementation of GRPC. This grant does not include claims that would be -infringed only as a consequence of further modification of this -implementation. If you or your agent or exclusive licensee institute or -order or agree to the institution of patent litigation against any -entity (including a cross-claim or counterclaim in a lawsuit) alleging -that this implementation of GRPC or any code incorporated within this -implementation of GRPC constitutes direct or contributory patent -infringement, or inducement of patent infringement, then any patent -rights granted to you under this License for this implementation of GRPC -shall terminate as of the date such litigation is filed. diff --git a/Package.swift b/Package.swift index db5881f30..8e1b4c19d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,6 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 /* - * Copyright 2017, gRPC Authors All rights reserved. + * 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. @@ -14,518 +14,97 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import PackageDescription -// swiftformat puts the next import before the tools version. -// swiftformat:disable:next sortImports -import class Foundation.ProcessInfo - -let grpcPackageName = "grpc-swift" -let grpcProductName = "GRPC" -let cgrpcZlibProductName = "CGRPCZlib" -let grpcTargetName = grpcProductName -let cgrpcZlibTargetName = cgrpcZlibProductName -let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil -// MARK: - Package Dependencies +import PackageDescription -let packageDependencies: [Package.Dependency] = [ - .package( - url: "https://github.com/apple/swift-nio.git", - from: "2.65.0" +let products: [Product] = [ + .library( + name: "GRPCCore", + targets: ["GRPCCore"] ), - .package( - url: "https://github.com/apple/swift-nio-http2.git", - from: "1.32.0" - ), - .package( - url: "https://github.com/apple/swift-nio-transport-services.git", - from: "1.15.0" + .library( + name: "GRPCCodeGen", + targets: ["GRPCCodeGen"] ), - .package( - url: "https://github.com/apple/swift-nio-extras.git", - from: "1.4.0" + .library( + name: "GRPCInProcessTransport", + targets: ["GRPCInProcessTransport"] ), +] + +let dependencies: [Package.Dependency] = [ .package( url: "https://github.com/apple/swift-collections.git", - from: "1.0.5" - ), - .package( - url: "https://github.com/apple/swift-atomics.git", - from: "1.2.0" + from: "1.1.3" ), + + // Test-only dependencies: .package( url: "https://github.com/apple/swift-protobuf.git", from: "1.28.1" ), - .package( - url: "https://github.com/apple/swift-log.git", - from: "1.4.4" - ), - .package( - url: "https://github.com/apple/swift-argument-parser.git", - // Version is higher than in other Package@swift manifests: 1.1.0 raised the minimum Swift - // version and indluded async support. - from: "1.1.1" - ), -].appending( - .package( - url: "https://github.com/apple/swift-nio-ssl.git", - from: "2.23.0" - ), - if: includeNIOSSL -) - -// MARK: - Target Dependencies - -extension Target.Dependency { - // Target dependencies; external - static let grpc: Self = .target(name: grpcTargetName) - static let cgrpcZlib: Self = .target(name: cgrpcZlibTargetName) - static let protocGenGRPCSwift: Self = .target(name: "protoc-gen-grpc-swift") - static let reflectionService: Self = .target(name: "GRPCReflectionService") - - // Target dependencies; internal - static let grpcSampleData: Self = .target(name: "GRPCSampleData") - static let echoModel: Self = .target(name: "EchoModel") - static let echoImplementation: Self = .target(name: "EchoImplementation") - static let helloWorldModel: Self = .target(name: "HelloWorldModel") - static let routeGuideModel: Self = .target(name: "RouteGuideModel") - static let interopTestModels: Self = .target(name: "GRPCInteroperabilityTestModels") - static let interopTestImplementation: Self = - .target(name: "GRPCInteroperabilityTestsImplementation") - static let interoperabilityTests: Self = .target(name: "InteroperabilityTests") - - // Product dependencies - static let argumentParser: Self = .product( - name: "ArgumentParser", - package: "swift-argument-parser" - ) - static let nio: Self = .product(name: "NIO", package: "swift-nio") - static let nioConcurrencyHelpers: Self = .product( - name: "NIOConcurrencyHelpers", - package: "swift-nio" - ) - static let nioCore: Self = .product(name: "NIOCore", package: "swift-nio") - static let nioEmbedded: Self = .product(name: "NIOEmbedded", package: "swift-nio") - static let nioExtras: Self = .product(name: "NIOExtras", package: "swift-nio-extras") - static let nioFoundationCompat: Self = .product(name: "NIOFoundationCompat", package: "swift-nio") - static let nioHTTP1: Self = .product(name: "NIOHTTP1", package: "swift-nio") - static let nioHTTP2: Self = .product(name: "NIOHTTP2", package: "swift-nio-http2") - static let nioPosix: Self = .product(name: "NIOPosix", package: "swift-nio") - static let nioSSL: Self = .product(name: "NIOSSL", package: "swift-nio-ssl") - static let nioTLS: Self = .product(name: "NIOTLS", package: "swift-nio") - static let nioTransportServices: Self = .product( - name: "NIOTransportServices", - package: "swift-nio-transport-services" - ) - static let nioTestUtils: Self = .product(name: "NIOTestUtils", package: "swift-nio") - static let nioFileSystem: Self = .product(name: "_NIOFileSystem", package: "swift-nio") - static let logging: Self = .product(name: "Logging", package: "swift-log") - static let protobuf: Self = .product(name: "SwiftProtobuf", package: "swift-protobuf") - static let protobufPluginLibrary: Self = .product( - name: "SwiftProtobufPluginLibrary", - package: "swift-protobuf" - ) - static let atomics: Self = .product(name: "Atomics", package: "swift-atomics") - static let dequeModule: Self = .product(name: "DequeModule", package: "swift-collections") -} - -// MARK: - Targets - -extension Target { - static let grpc: Target = .target( - name: grpcTargetName, - dependencies: [ - .cgrpcZlib, - .nio, - .nioCore, - .nioPosix, - .nioEmbedded, - .nioFoundationCompat, - .nioTLS, - .nioTransportServices, - .nioHTTP1, - .nioHTTP2, - .nioExtras, - .logging, - .protobuf, - .dequeModule, - .atomics - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Sources/GRPC" - ) +] - static let cgrpcZlib: Target = .target( - name: cgrpcZlibTargetName, - path: "Sources/CGRPCZlib", - linkerSettings: [ - .linkedLibrary("z"), - ] - ) - - static let protocGenGRPCSwift: Target = .executableTarget( - name: "protoc-gen-grpc-swift", - dependencies: [ - .protobuf, - .protobufPluginLibrary, - ], - exclude: [ - "README.md", - ] - ) - - static let grpcSwiftPlugin: Target = .plugin( - name: "GRPCSwiftPlugin", - capability: .buildTool(), - dependencies: [ - .protocGenGRPCSwift, - ] - ) - - static let grpcTests: Target = .testTarget( - name: "GRPCTests", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .helloWorldModel, - .interopTestModels, - .interopTestImplementation, - .grpcSampleData, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .nioTLS, - .nioHTTP1, - .nioHTTP2, - .nioEmbedded, - .nioTransportServices, - .logging, - .reflectionService, - .atomics - ].appending( - .nioSSL, if: includeNIOSSL - ), - exclude: [ - "Codegen/Serialization/echo.grpc.reflection" - ] - ) +let defaultSwiftSettings: [SwiftSetting] = [ + .swiftLanguageMode(.v6), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault") +] - static let interopTestModels: Target = .target( - name: "GRPCInteroperabilityTestModels", +let targets: [Target] = [ + // Runtime serialization components + .target( + name: "GRPCCore", dependencies: [ - .grpc, - .nio, - .protobuf, + .product(name: "DequeModule", package: "swift-collections"), ], - exclude: [ - "README.md", - "generate.sh", - "src/proto/grpc/testing/empty.proto", - "src/proto/grpc/testing/empty_service.proto", - "src/proto/grpc/testing/messages.proto", - "src/proto/grpc/testing/test.proto", - "unimplemented_call.patch", - ] - ) - - static let interopTestImplementation: Target = .target( - name: "GRPCInteroperabilityTestsImplementation", - dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .nioHTTP1, - .logging, - ].appending( - .nioSSL, if: includeNIOSSL - ) - ) - - static let interopTests: Target = .executableTarget( - name: "GRPCInteroperabilityTests", - dependencies: [ - .grpc, - .interopTestImplementation, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ] - ) - - static let backoffInteropTest: Target = .executableTarget( - name: "GRPCConnectionBackoffInteropTest", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "GRPCCoreTests", dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .logging, - .argumentParser, + .target(name: "GRPCCore"), + .product(name: "SwiftProtobuf", package: "swift-protobuf") ], - exclude: [ - "README.md", - ] - ) - - static let perfTests: Target = .executableTarget( - name: "GRPCPerformanceTests", - dependencies: [ - .grpc, - .grpcSampleData, - .nioCore, - .nioEmbedded, - .nioPosix, - .nioHTTP2, - .argumentParser, - ] - ) - - static let grpcSampleData: Target = .target( - name: "GRPCSampleData", - dependencies: includeNIOSSL ? [.nioSSL] : [], - exclude: [ - "bundle.p12", + resources: [ + .copy("Configuration/Inputs") ] - ) - - static let echoModel: Target = .target( - name: "EchoModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Examples/v1/Echo/Model" - ) - - static let echoImplementation: Target = .target( - name: "EchoImplementation", - dependencies: [ - .echoModel, - .grpc, - .nioCore, - .nioHTTP2, - .protobuf, - ], - path: "Examples/v1/Echo/Implementation" - ) - - static let echo: Target = .executableTarget( - name: "Echo", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .grpcSampleData, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Examples/v1/Echo/Runtime" - ) - - static let helloWorldModel: Target = .target( - name: "HelloWorldModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Examples/v1/HelloWorld/Model" - ) - - static let helloWorldClient: Target = .executableTarget( - name: "HelloWorldClient", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Examples/v1/HelloWorld/Client" - ) - - static let helloWorldServer: Target = .executableTarget( - name: "HelloWorldServer", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Examples/v1/HelloWorld/Server" - ) - - static let routeGuideModel: Target = .target( - name: "RouteGuideModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Examples/v1/RouteGuide/Model" - ) - - static let routeGuideClient: Target = .executableTarget( - name: "RouteGuideClient", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Examples/v1/RouteGuide/Client" - ) + ), - static let routeGuideServer: Target = .executableTarget( - name: "RouteGuideServer", + // In-process client and server transport implementations + .target( + name: "GRPCInProcessTransport", dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .argumentParser, + .target(name: "GRPCCore") ], - path: "Examples/v1/RouteGuide/Server" - ) - - static let packetCapture: Target = .executableTarget( - name: "PacketCapture", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "GRPCInProcessTransportTests", dependencies: [ - .grpc, - .echoModel, - .nioCore, - .nioPosix, - .nioExtras, - .argumentParser, - ], - path: "Examples/v1/PacketCapture", - exclude: [ - "README.md", + .target(name: "GRPCInProcessTransport") ] - ) + ), - static let reflectionService: Target = .target( - name: "GRPCReflectionService", + // Code generator library for protoc-gen-grpc-swift + .target( + name: "GRPCCodeGen", dependencies: [ - .grpc, - .nio, - .protobuf, ], - path: "Sources/GRPCReflectionService" - ) - - static let reflectionServer: Target = .executableTarget( - name: "ReflectionServer", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "GRPCCodeGenTests", dependencies: [ - .grpc, - .reflectionService, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - .echoModel, - .echoImplementation - ], - path: "Examples/v1/ReflectionService", - resources: [ - .copy("Generated") + .target(name: "GRPCCodeGen") ] ) -} - -// MARK: - Products - -extension Product { - static let grpc: Product = .library( - name: grpcProductName, - targets: [grpcTargetName] - ) - - static let cgrpcZlib: Product = .library( - name: cgrpcZlibProductName, - targets: [cgrpcZlibTargetName] - ) - - static let grpcReflectionService: Product = .library( - name: "GRPCReflectionService", - targets: ["GRPCReflectionService"] - ) - - static let protocGenGRPCSwift: Product = .executable( - name: "protoc-gen-grpc-swift", - targets: ["protoc-gen-grpc-swift"] - ) - - static let grpcSwiftPlugin: Product = .plugin( - name: "GRPCSwiftPlugin", - targets: ["GRPCSwiftPlugin"] - ) -} - -// MARK: - Package +] let package = Package( - name: grpcPackageName, - products: [ - .grpc, - .cgrpcZlib, - .grpcReflectionService, - .protocGenGRPCSwift, - .grpcSwiftPlugin, - ], - dependencies: packageDependencies, - targets: [ - // Products - .grpc, - .cgrpcZlib, - .protocGenGRPCSwift, - .grpcSwiftPlugin, - .reflectionService, - - // Tests etc. - .grpcTests, - .interopTestModels, - .interopTestImplementation, - .interopTests, - .backoffInteropTest, - .perfTests, - .grpcSampleData, - - // Examples - .echoModel, - .echoImplementation, - .echo, - .helloWorldModel, - .helloWorldClient, - .helloWorldServer, - .routeGuideModel, - .routeGuideClient, - .routeGuideServer, - .packetCapture, - .reflectionServer, - ] + name: "grpc-swift", + products: products, + dependencies: dependencies, + targets: targets ) - -extension Array { - func appending(_ element: Element, if condition: Bool) -> [Element] { - if condition { - return self + [element] - } else { - return self - } - } -} diff --git a/Package@swift-6.swift b/Package@swift-6.swift deleted file mode 100644 index f8922e440..000000000 --- a/Package@swift-6.swift +++ /dev/null @@ -1,1127 +0,0 @@ -// swift-tools-version:6.0 -/* - * 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 PackageDescription -// swiftformat puts the next import before the tools version. -// swiftformat:disable:next sortImports -import class Foundation.ProcessInfo - -let grpcPackageName = "grpc-swift" -let grpcProductName = "GRPC" -let cgrpcZlibProductName = "CGRPCZlib" -let grpcTargetName = grpcProductName -let cgrpcZlibTargetName = cgrpcZlibProductName - -let includeNIOSSL = ProcessInfo.processInfo.environment["GRPC_NO_NIO_SSL"] == nil - -// MARK: - Package Dependencies - -let packageDependencies: [Package.Dependency] = [ - .package( - url: "https://github.com/apple/swift-nio.git", - from: "2.65.0" - ), - .package( - url: "https://github.com/apple/swift-nio-http2.git", - from: "1.32.0" - ), - .package( - url: "https://github.com/apple/swift-nio-transport-services.git", - from: "1.15.0" - ), - .package( - url: "https://github.com/apple/swift-nio-extras.git", - from: "1.4.0" - ), - .package( - url: "https://github.com/apple/swift-collections.git", - from: "1.0.5" - ), - .package( - url: "https://github.com/apple/swift-atomics.git", - from: "1.2.0" - ), - .package( - url: "https://github.com/apple/swift-protobuf.git", - from: "1.28.1" - ), - .package( - url: "https://github.com/apple/swift-log.git", - from: "1.4.4" - ), - .package( - url: "https://github.com/apple/swift-argument-parser.git", - // Version is higher than in other Package@swift manifests: 1.1.0 raised the minimum Swift - // version and indluded async support. - from: "1.1.1" - ), - .package( - url: "https://github.com/apple/swift-distributed-tracing.git", - from: "1.0.0" - ), -].appending( - .package( - url: "https://github.com/apple/swift-nio-ssl.git", - from: "2.23.0" - ), - if: includeNIOSSL -) - -// MARK: - Target Dependencies - -extension Target.Dependency { - // Target dependencies; external - static var grpc: Self { .target(name: grpcTargetName) } - static var cgrpcZlib: Self { .target(name: cgrpcZlibTargetName) } - static var protocGenGRPCSwift: Self { .target(name: "protoc-gen-grpc-swift") } - static var performanceWorker: Self { .target(name: "performance-worker") } - static var reflectionService: Self { .target(name: "GRPCReflectionService") } - static var grpcCodeGen: Self { .target(name: "GRPCCodeGen") } - static var grpcProtobuf: Self { .target(name: "GRPCProtobuf") } - static var grpcProtobufCodeGen: Self { .target(name: "GRPCProtobufCodeGen") } - - // Target dependencies; internal - static var grpcSampleData: Self { .target(name: "GRPCSampleData") } - static var echoModel: Self { .target(name: "EchoModel") } - static var echoImplementation: Self { .target(name: "EchoImplementation") } - static var helloWorldModel: Self { .target(name: "HelloWorldModel") } - static var routeGuideModel: Self { .target(name: "RouteGuideModel") } - static var interopTestModels: Self { .target(name: "GRPCInteroperabilityTestModels") } - static var interopTestImplementation: Self { - .target(name: "GRPCInteroperabilityTestsImplementation") - } - static var interoperabilityTests: Self { .target(name: "InteroperabilityTests") } - - // Product dependencies - static var argumentParser: Self { - .product( - name: "ArgumentParser", - package: "swift-argument-parser" - ) - } - static var nio: Self { .product(name: "NIO", package: "swift-nio") } - static var nioConcurrencyHelpers: Self { - .product( - name: "NIOConcurrencyHelpers", - package: "swift-nio" - ) - } - static var nioCore: Self { .product(name: "NIOCore", package: "swift-nio") } - static var nioEmbedded: Self { .product(name: "NIOEmbedded", package: "swift-nio") } - static var nioExtras: Self { .product(name: "NIOExtras", package: "swift-nio-extras") } - static var nioFoundationCompat: Self { .product(name: "NIOFoundationCompat", package: "swift-nio") } - static var nioHTTP1: Self { .product(name: "NIOHTTP1", package: "swift-nio") } - static var nioHTTP2: Self { .product(name: "NIOHTTP2", package: "swift-nio-http2") } - static var nioPosix: Self { .product(name: "NIOPosix", package: "swift-nio") } - static var nioSSL: Self { .product(name: "NIOSSL", package: "swift-nio-ssl") } - static var nioTLS: Self { .product(name: "NIOTLS", package: "swift-nio") } - static var nioTransportServices: Self { - .product( - name: "NIOTransportServices", - package: "swift-nio-transport-services" - ) - } - static var nioTestUtils: Self { .product(name: "NIOTestUtils", package: "swift-nio") } - static var nioFileSystem: Self { .product(name: "_NIOFileSystem", package: "swift-nio") } - static var logging: Self { .product(name: "Logging", package: "swift-log") } - static var protobuf: Self { .product(name: "SwiftProtobuf", package: "swift-protobuf") } - static var protobufPluginLibrary: Self { - .product( - name: "SwiftProtobufPluginLibrary", - package: "swift-protobuf" - ) - } - static var dequeModule: Self { .product(name: "DequeModule", package: "swift-collections") } - static var atomics: Self { .product(name: "Atomics", package: "swift-atomics") } - static var tracing: Self { .product(name: "Tracing", package: "swift-distributed-tracing") } - - static var grpcCore: Self { .target(name: "GRPCCore") } - static var grpcInProcessTransport: Self { .target(name: "GRPCInProcessTransport") } - static var grpcInterceptors: Self { .target(name: "GRPCInterceptors") } - static var grpcHTTP2Core: Self { .target(name: "GRPCHTTP2Core") } - static var grpcHTTP2Transport: Self { .target(name: "GRPCHTTP2Transport") } - static var grpcHTTP2TransportNIOPosix: Self { .target(name: "GRPCHTTP2TransportNIOPosix") } - static var grpcHTTP2TransportNIOTransportServices: Self { .target(name: "GRPCHTTP2TransportNIOTransportServices") } - static var grpcHealth: Self { .target(name: "GRPCHealth") } -} - -// MARK: - Targets - -extension Target { - static var grpc: Target { - .target( - name: grpcTargetName, - dependencies: [ - .cgrpcZlib, - .nio, - .nioCore, - .nioPosix, - .nioEmbedded, - .nioFoundationCompat, - .nioTLS, - .nioTransportServices, - .nioHTTP1, - .nioHTTP2, - .nioExtras, - .logging, - .protobuf, - .dequeModule, - .atomics - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Sources/GRPC", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var grpcCore: Target { - .target( - name: "GRPCCore", - dependencies: [ - .dequeModule, - ], - path: "Sources/GRPCCore", - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcInProcessTransport: Target { - .target( - name: "GRPCInProcessTransport", - dependencies: [ - .grpcCore - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcInterceptors: Target { - .target( - name: "GRPCInterceptors", - dependencies: [ - .grpcCore, - .tracing - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcHTTP2Core: Target { - .target( - name: "GRPCHTTP2Core", - dependencies: [ - .grpcCore, - .nioCore, - .nioHTTP2, - .nioTLS, - .nioExtras, - .cgrpcZlib, - .dequeModule, - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcHTTP2TransportNIOPosix: Target { - .target( - name: "GRPCHTTP2TransportNIOPosix", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .nioPosix, - .nioExtras - ].appending( - .nioSSL, - if: includeNIOSSL - ), - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcHTTP2TransportNIOTransportServices: Target { - .target( - name: "GRPCHTTP2TransportNIOTransportServices", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .nioCore, - .nioExtras, - .nioTransportServices - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcHTTP2Transport: Target { - .target( - name: "GRPCHTTP2Transport", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .grpcHTTP2TransportNIOTransportServices, - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var cgrpcZlib: Target { - .target( - name: cgrpcZlibTargetName, - path: "Sources/CGRPCZlib", - linkerSettings: [ - .linkedLibrary("z"), - ] - ) - } - - static var protocGenGRPCSwift: Target { - .executableTarget( - name: "protoc-gen-grpc-swift", - dependencies: [ - .protobuf, - .protobufPluginLibrary, - .grpcCodeGen, - .grpcProtobufCodeGen - ], - exclude: [ - "README.md", - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var performanceWorker: Target { - .executableTarget( - name: "performance-worker", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .grpcProtobuf, - .nioCore, - .nioFileSystem, - .argumentParser - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny") - ] - ) - } - - static var grpcSwiftPlugin: Target { - .plugin( - name: "GRPCSwiftPlugin", - capability: .buildTool(), - dependencies: [ - .protocGenGRPCSwift, - ] - ) - } - - static var grpcTests: Target { - .testTarget( - name: "GRPCTests", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .helloWorldModel, - .interopTestModels, - .interopTestImplementation, - .grpcSampleData, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .nioTLS, - .nioHTTP1, - .nioHTTP2, - .nioEmbedded, - .nioTransportServices, - .logging, - .reflectionService, - .atomics - ].appending( - .nioSSL, if: includeNIOSSL - ), - exclude: [ - "Codegen/Serialization/echo.grpc.reflection" - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var grpcCoreTests: Target { - .testTarget( - name: "GRPCCoreTests", - dependencies: [ - .grpcCore, - .grpcInProcessTransport, - .dequeModule, - .protobuf, - ], - resources: [ - .copy("Configuration/Inputs") - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcInProcessTransportTests: Target { - .testTarget( - name: "GRPCInProcessTransportTests", - dependencies: [ - .grpcCore, - .grpcInProcessTransport - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcInterceptorsTests: Target { - .testTarget( - name: "GRPCInterceptorsTests", - dependencies: [ - .grpcCore, - .tracing, - .nioCore, - .grpcInterceptors - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcHTTP2CoreTests: Target { - .testTarget( - name: "GRPCHTTP2CoreTests", - dependencies: [ - .grpcHTTP2Core, - .nioCore, - .nioHTTP2, - .nioEmbedded, - .nioTestUtils, - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcHTTP2TransportTests: Target { - .testTarget( - name: "GRPCHTTP2TransportTests", - dependencies: [ - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .grpcHTTP2TransportNIOTransportServices, - .grpcProtobuf - ].appending( - .nioSSL, if: includeNIOSSL - ), - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcCodeGenTests: Target { - .testTarget( - name: "GRPCCodeGenTests", - dependencies: [ - .grpcCodeGen - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcProtobufTests: Target { - .testTarget( - name: "GRPCProtobufTests", - dependencies: [ - .grpcProtobuf, - .grpcCore, - .protobuf - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcProtobufCodeGenTests: Target { - .testTarget( - name: "GRPCProtobufCodeGenTests", - dependencies: [ - .grpcCodeGen, - .grpcProtobufCodeGen, - .protobuf, - .protobufPluginLibrary - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var inProcessInteroperabilityTests: Target { - .testTarget( - name: "InProcessInteroperabilityTests", - dependencies: [ - .grpcInProcessTransport, - .interoperabilityTests, - .grpcCore - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var grpcHealthTests: Target { - .testTarget( - name: "GRPCHealthTests", - dependencies: [ - .grpcHealth, - .grpcProtobuf, - .grpcInProcessTransport - ], - path: "Tests/Services/HealthTests", - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var interopTestModels: Target { - .target( - name: "GRPCInteroperabilityTestModels", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - exclude: [ - "README.md", - "generate.sh", - "src/proto/grpc/testing/empty.proto", - "src/proto/grpc/testing/empty_service.proto", - "src/proto/grpc/testing/messages.proto", - "src/proto/grpc/testing/test.proto", - "unimplemented_call.patch", - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var interoperabilityTestImplementation: Target { - .target( - name: "InteroperabilityTests", - dependencies: [ - .grpcCore, - .grpcProtobuf - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var interoperabilityTestsExecutable: Target { - .executableTarget( - name: "interoperability-tests", - dependencies: [ - .grpcCore, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .interoperabilityTests, - .argumentParser - ], - swiftSettings: [.swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny")] - ) - } - - static var interopTestImplementation: Target { - .target( - name: "GRPCInteroperabilityTestsImplementation", - dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .nioHTTP1, - .logging, - ].appending( - .nioSSL, if: includeNIOSSL - ), - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var interopTests: Target { - .executableTarget( - name: "GRPCInteroperabilityTests", - dependencies: [ - .grpc, - .interopTestImplementation, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var backoffInteropTest: Target { - .executableTarget( - name: "GRPCConnectionBackoffInteropTest", - dependencies: [ - .grpc, - .interopTestModels, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ], - exclude: [ - "README.md", - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var perfTests: Target { - .executableTarget( - name: "GRPCPerformanceTests", - dependencies: [ - .grpc, - .grpcSampleData, - .nioCore, - .nioEmbedded, - .nioPosix, - .nioHTTP2, - .argumentParser, - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var grpcSampleData: Target { - .target( - name: "GRPCSampleData", - dependencies: includeNIOSSL ? [.nioSSL] : [], - exclude: [ - "bundle.p12", - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var echoModel: Target { - .target( - name: "EchoModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Examples/v1/Echo/Model", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var echoImplementation: Target { - .target( - name: "EchoImplementation", - dependencies: [ - .echoModel, - .grpc, - .nioCore, - .nioHTTP2, - .protobuf, - ], - path: "Examples/v1/Echo/Implementation", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var echo: Target { - .executableTarget( - name: "Echo", - dependencies: [ - .grpc, - .echoModel, - .echoImplementation, - .grpcSampleData, - .nioCore, - .nioPosix, - .logging, - .argumentParser, - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Examples/v1/Echo/Runtime", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var echo_v2: Target { - .executableTarget( - name: "echo-v2", - dependencies: [ - .grpcCore, - .grpcProtobuf, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .argumentParser, - ].appending( - .nioSSL, if: includeNIOSSL - ), - path: "Examples/v2/echo", - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny") - ] - ) - } - - static var helloWorldModel: Target { - .target( - name: "HelloWorldModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Examples/v1/HelloWorld/Model", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var helloWorldClient: Target { - .executableTarget( - name: "HelloWorldClient", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Examples/v1/HelloWorld/Client", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var helloWorldServer: Target { - .executableTarget( - name: "HelloWorldServer", - dependencies: [ - .grpc, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Examples/v1/HelloWorld/Server", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var helloWorld_v2: Target { - .executableTarget( - name: "hello-world", - dependencies: [ - .grpcProtobuf, - .grpcHTTP2Transport, - .argumentParser, - ], - path: "Examples/v2/hello-world", - exclude: [ - "HelloWorld.proto" - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny") - ] - ) - } - - static var routeGuideModel: Target { - .target( - name: "RouteGuideModel", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Examples/v1/RouteGuide/Model", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var routeGuideClient: Target { - .executableTarget( - name: "RouteGuideClient", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioPosix, - .argumentParser, - ], - path: "Examples/v1/RouteGuide/Client", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var routeGuideServer: Target { - .executableTarget( - name: "RouteGuideServer", - dependencies: [ - .grpc, - .routeGuideModel, - .nioCore, - .nioConcurrencyHelpers, - .nioPosix, - .argumentParser, - ], - path: "Examples/v1/RouteGuide/Server", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var routeGuide_v2: Target { - .executableTarget( - name: "route-guide", - dependencies: [ - .grpcProtobuf, - .grpcHTTP2Transport, - .argumentParser, - ], - path: "Examples/v2/route-guide", - resources: [ - .copy("route_guide_db.json") - ], - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny") - ] - ) - } - - static var packetCapture: Target { - .executableTarget( - name: "PacketCapture", - dependencies: [ - .grpc, - .echoModel, - .nioCore, - .nioPosix, - .nioExtras, - .argumentParser, - ], - path: "Examples/v1/PacketCapture", - exclude: [ - "README.md", - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var reflectionService: Target { - .target( - name: "GRPCReflectionService", - dependencies: [ - .grpc, - .nio, - .protobuf, - ], - path: "Sources/GRPCReflectionService", - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var reflectionServer: Target { - .executableTarget( - name: "ReflectionServer", - dependencies: [ - .grpc, - .reflectionService, - .helloWorldModel, - .nioCore, - .nioPosix, - .argumentParser, - .echoModel, - .echoImplementation - ], - path: "Examples/v1/ReflectionService", - resources: [ - .copy("Generated") - ], - swiftSettings: [.swiftLanguageMode(.v5)] - ) - } - - static var grpcCodeGen: Target { - .target( - name: "GRPCCodeGen", - path: "Sources/GRPCCodeGen", - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcProtobuf: Target { - .target( - name: "GRPCProtobuf", - dependencies: [ - .grpcCore, - .protobuf, - ], - path: "Sources/GRPCProtobuf", - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcProtobufCodeGen: Target { - .target( - name: "GRPCProtobufCodeGen", - dependencies: [ - .protobuf, - .protobufPluginLibrary, - .grpcCodeGen - ], - path: "Sources/GRPCProtobufCodeGen", - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } - - static var grpcHealth: Target { - .target( - name: "GRPCHealth", - dependencies: [ - .grpcCore, - .grpcProtobuf - ], - path: "Sources/Services/Health", - swiftSettings: [ - .swiftLanguageMode(.v6), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") - ] - ) - } -} - -// MARK: - Products - -extension Product { - static var grpc: Product { - .library( - name: grpcProductName, - targets: [grpcTargetName] - ) - } - - static var _grpcCore: Product { - .library( - name: "_GRPCCore", - targets: ["GRPCCore"] - ) - } - - static var _grpcProtobuf: Product { - .library( - name: "_GRPCProtobuf", - targets: ["GRPCProtobuf"] - ) - } - - static var _grpcInProcessTransport: Product { - .library( - name: "_GRPCInProcessTransport", - targets: ["GRPCInProcessTransport"] - ) - } - - static var _grpcHTTP2Transport: Product { - .library( - name: "_GRPCHTTP2Transport", - targets: ["GRPCHTTP2Transport"] - ) - } - - static var cgrpcZlib: Product { - .library( - name: cgrpcZlibProductName, - targets: [cgrpcZlibTargetName] - ) - } - - static var grpcReflectionService: Product { - .library( - name: "GRPCReflectionService", - targets: ["GRPCReflectionService"] - ) - } - - static var protocGenGRPCSwift: Product { - .executable( - name: "protoc-gen-grpc-swift", - targets: ["protoc-gen-grpc-swift"] - ) - } - - static var grpcSwiftPlugin: Product { - .plugin( - name: "GRPCSwiftPlugin", - targets: ["GRPCSwiftPlugin"] - ) - } -} - -// MARK: - Package - -let package = Package( - name: grpcPackageName, - products: [ - // v1 - .grpc, - .cgrpcZlib, - .grpcReflectionService, - .protocGenGRPCSwift, - .grpcSwiftPlugin, - // v2 - ._grpcCore, - ._grpcProtobuf, - ._grpcHTTP2Transport, - ._grpcInProcessTransport, - ], - dependencies: packageDependencies, - targets: [ - // Products - .grpc, - .cgrpcZlib, - .protocGenGRPCSwift, - .grpcSwiftPlugin, - .reflectionService, - - // Tests etc. - .grpcTests, - .interopTestModels, - .interopTestImplementation, - .interopTests, - .backoffInteropTest, - .perfTests, - .grpcSampleData, - - // Examples - .echoModel, - .echoImplementation, - .echo, - .helloWorldModel, - .helloWorldClient, - .helloWorldServer, - .routeGuideModel, - .routeGuideClient, - .routeGuideServer, - .packetCapture, - .reflectionServer, - - // v2 - .grpcCore, - .grpcCodeGen, - - // v2 transports - .grpcInProcessTransport, - .grpcHTTP2Core, - .grpcHTTP2TransportNIOPosix, - .grpcHTTP2TransportNIOTransportServices, - .grpcHTTP2Transport, - - // v2 Protobuf support - .grpcProtobuf, - .grpcProtobufCodeGen, - - // v2 add-ons - .grpcInterceptors, - .grpcHealth, - - // v2 integration testing - .interoperabilityTestImplementation, - .interoperabilityTestsExecutable, - .performanceWorker, - - // v2 unit tests - .grpcCoreTests, - .grpcInProcessTransportTests, - .grpcCodeGenTests, - .grpcInterceptorsTests, - .grpcHTTP2CoreTests, - .grpcHTTP2TransportTests, - .grpcHealthTests, - .grpcProtobufTests, - .grpcProtobufCodeGenTests, - .inProcessInteroperabilityTests, - - // v2 examples - .echo_v2, - .helloWorld_v2, - .routeGuide_v2, - ] -) - -extension Array { - func appending(_ element: Element, if condition: Bool) -> [Element] { - if condition { - return self + [element] - } else { - return self - } - } -} diff --git a/Performance/QPSBenchmark/.gitignore b/Performance/QPSBenchmark/.gitignore deleted file mode 100644 index f4096fee4..000000000 --- a/Performance/QPSBenchmark/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Package.resolved diff --git a/Performance/QPSBenchmark/Package.swift b/Performance/QPSBenchmark/Package.swift deleted file mode 100644 index 5a26bda5f..000000000 --- a/Performance/QPSBenchmark/Package.swift +++ /dev/null @@ -1,75 +0,0 @@ -// swift-tools-version:5.8 -/* - * Copyright 2020, 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 PackageDescription - -let package = Package( - name: "QPSBenchmark", - platforms: [.macOS(.v12)], - products: [ - .executable(name: "QPSBenchmark", targets: ["QPSBenchmark"]), - ], - dependencies: [ - .package(path: "../../"), - .package(url: "https://github.com/apple/swift-nio.git", from: "2.41.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.4.3"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.1.1"), - .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), - .package( - url: "https://github.com/swift-server/swift-service-lifecycle.git", - from: "1.0.0-alpha" - ), - .package( - url: "https://github.com/apple/swift-protobuf.git", - from: "1.20.1" - ), - ], - targets: [ - .executableTarget( - name: "QPSBenchmark", - dependencies: [ - .product(name: "GRPC", package: "grpc-swift"), - .product(name: "Atomics", package: "swift-atomics"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Logging", package: "swift-log"), - .product(name: "Lifecycle", package: "swift-service-lifecycle"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), - .target(name: "BenchmarkUtils"), - ], - plugins: [ - .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf"), - .plugin(name: "GRPCSwiftPlugin", package: "grpc-swift"), - ] - ), - .target( - name: "BenchmarkUtils", - dependencies: [ - .product(name: "GRPC", package: "grpc-swift"), - ] - ), - .testTarget( - name: "BenchmarkUtilsTests", - dependencies: [ - .product(name: "GRPC", package: "grpc-swift"), - .target(name: "BenchmarkUtils"), - ] - ), - ] -) diff --git a/Performance/QPSBenchmark/README.md b/Performance/QPSBenchmark/README.md deleted file mode 100644 index a8e26d1d2..000000000 --- a/Performance/QPSBenchmark/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# QPS Benchmark worker - -An implementation of the QPS worker for benchmarking described in the -[gRPC benchmarking guide](https://grpc.io/docs/guides/benchmarking/) - -## Building - -The benchmarks can be built in the usual Swift Package Manager way but release -mode is strongly recommended: `swift build -c release` - -## Running the benchmarks - -To date the changes to gRPC to run the tests automatically have not been pushed -upstream. You can easily run the tests locally using the C++ driver program. - -This can be built using Bazel from the root of a checkout of the -[grpc/grpc](https://github.com/grpc/grpc) repository with: - -```sh -bazel build test/cpp/qps:qps_json_driver -``` - -The `qps_json_driver` binary will be in `bazel-bin/test/cpp/qps/`. - -For examples of running benchmarking tests proceed as follows. - -> **Note:** the driver may also be built (via CMake) as a side effect of -> running the performance testing script (`./tools/run_tests/run_performance_tests.py`) -> from [grpc/grpc](https://github.com/grpc/grpc). -> -> The script is also the source of the scenarios listed below. - -### Setting Up the Environment - -1. Open a terminal window and run the QPSBenchmark, this will become the server when instructed by the driver. - - ```sh - swift run -c release QPSBenchmark --driver_port 10400 - ``` - - -2. Open another terminal window and run QPSBenchmark, this will become the client when instructed by the driver. - - ```sh - swift run -c release QPSBenchmark --driver_port 10410 - ``` - -3. Configure the environment for the driver: - - ```sh - export QPS_WORKERS="localhost:10400,localhost:10410" - ``` - -4. Invoke the driver with a scenario file, for example: - - ```sh - /path/to/qps_json_driver --scenarios_file=/path/to/scenario.json - ``` - -### Scenarios - -- `scenarios/unary-unconstrained.json`: will run a test with unary RPCs - using all cores on the machine. 64 clients will connect to the server, each - enqueuing up to 100 requests. -- `scenarios/unary-1-connection.json`: as above with a single client. -- `scenarios/bidirectional-ping-pong-1-connection.json`: will run bidirectional - streaming RPCs using a single client. diff --git a/Performance/QPSBenchmark/Sources/BenchmarkUtils/Histogram.swift b/Performance/QPSBenchmark/Sources/BenchmarkUtils/Histogram.swift deleted file mode 100644 index 1de3738e0..000000000 --- a/Performance/QPSBenchmark/Sources/BenchmarkUtils/Histogram.swift +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2020, 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 Foundation - -struct HistorgramShapeMismatch: Error {} - -/// Histograms are stored with exponentially increasing bucket sizes. -/// The first bucket is [0, `multiplier`) where `multiplier` = 1 + resolution -/// Bucket n (n>=1) contains [`multiplier`**n, `multiplier`**(n+1)) -/// There are sufficient buckets to reach max_bucket_start -public struct Histogram { - public private(set) var sum: Double - public private(set) var sumOfSquares: Double - public private(set) var countOfValuesSeen: Double - private var multiplier: Double - private var oneOnLogMultiplier: Double - public private(set) var minSeen: Double - public private(set) var maxSeen: Double - private var maxPossible: Double - public private(set) var buckets: [UInt32] - - /// Initialise a histogram. - /// - parameters: - /// - resolution: Defines the width of the buckets - see the description of this structure. - /// - maxBucketStart: Defines the start of the greatest valued bucket. - public init(resolution: Double = 0.01, maxBucketStart: Double = 60e9) { - precondition(resolution > 0.0) - precondition(maxBucketStart > resolution) - self.sum = 0.0 - self.sumOfSquares = 0.0 - self.multiplier = 1.0 + resolution - self.oneOnLogMultiplier = 1.0 / log(1.0 + resolution) - self.maxPossible = maxBucketStart - self.countOfValuesSeen = 0.0 - self.minSeen = maxBucketStart - self.maxSeen = 0.0 - let numBuckets = Histogram.bucketForUnchecked( - value: maxBucketStart, - oneOnLogMultiplier: self.oneOnLogMultiplier - ) + 1 - precondition(numBuckets > 1) - precondition(numBuckets < 100_000_000) - self.buckets = .init(repeating: 0, count: numBuckets) - } - - /// Determine a bucket index given a value - does no bounds checking - private static func bucketForUnchecked(value: Double, oneOnLogMultiplier: Double) -> Int { - return Int(log(value) * oneOnLogMultiplier) - } - - private func bucketFor(value: Double) -> Int { - let bucket = Histogram.bucketForUnchecked( - value: self.clamp(value: value), - oneOnLogMultiplier: self.oneOnLogMultiplier - ) - assert(bucket < self.buckets.count) - assert(bucket >= 0) - return bucket - } - - /// Force a value to be within the bounds of 0 and `self.maxPossible` - /// - parameters: - /// - value: The value to force within bounds - /// - returns: The value forced into the bounds for buckets. - private func clamp(value: Double) -> Double { - return min(self.maxPossible, max(0, value)) - } - - /// Add a value to this histogram, updating buckets and stats - /// - parameters: - /// - value: The value to add. - public mutating func add(value: Double) { - self.sum += value - self.sumOfSquares += value * value - self.countOfValuesSeen += 1 - if value < self.minSeen { - self.minSeen = value - } - if value > self.maxSeen { - self.maxSeen = value - } - self.buckets[self.bucketFor(value: value)] += 1 - } - - /// Merge two histograms together updating `self` - /// - parameters: - /// - source: the other histogram to merge into this. - public mutating func merge(source: Histogram) throws { - guard (self.buckets.count == source.buckets.count) || - (self.multiplier == source.multiplier) else { - // Fail because these histograms don't match. - throw HistorgramShapeMismatch() - } - - self.sum += source.sum - self.sumOfSquares += source.sumOfSquares - self.countOfValuesSeen += source.countOfValuesSeen - if source.minSeen < self.minSeen { - self.minSeen = source.minSeen - } - if source.maxSeen > self.maxSeen { - self.maxSeen = source.maxSeen - } - for bucket in 0 ..< self.buckets.count { - self.buckets[bucket] += source.buckets[bucket] - } - } -} diff --git a/Performance/QPSBenchmark/Sources/BenchmarkUtils/StatusCounts.swift b/Performance/QPSBenchmark/Sources/BenchmarkUtils/StatusCounts.swift deleted file mode 100644 index a0ac5c39e..000000000 --- a/Performance/QPSBenchmark/Sources/BenchmarkUtils/StatusCounts.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020, 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 GRPC - -/// Count the number seen of each status code. -public struct StatusCounts { - public private(set) var counts: [Int: Int64] = [:] - - public init() {} - - /// Add one to the count of this sort of status code. - /// - parameters: - /// - status: The code to count. - public mutating func add(status: GRPCStatus.Code) { - // Only record failures - if status != .ok { - if let previousCount = self.counts[status.rawValue] { - self.counts[status.rawValue] = previousCount + 1 - } else { - self.counts[status.rawValue] = 1 - } - } - } - - /// Merge another set of counts into this one. - /// - parameters: - /// - source: The other set of counts to merge into this. - public mutating func merge(source: StatusCounts) { - for sourceCount in source.counts { - if let existingCount = self.counts[sourceCount.key] { - self.counts[sourceCount.key] = existingCount + sourceCount.value - } else { - self.counts[sourceCount.key] = sourceCount.value - } - } - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto deleted file mode 100644 index 5308209a1..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/benchmark_service.proto +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. -syntax = "proto3"; - -import "Model/messages.proto"; - -package grpc.testing; - -service BenchmarkService { - // One request followed by one response. - // The server returns the client payload as-is. - rpc UnaryCall(SimpleRequest) returns (SimpleResponse); - - // Repeated sequence of one request followed by one response. - // Should be called streaming ping-pong - // The server returns the client payload as-is on each response - rpc StreamingCall(stream SimpleRequest) returns (stream SimpleResponse); - - // Single-sided unbounded streaming from client to server - // The server returns the client payload as-is once the client does WritesDone - rpc StreamingFromClient(stream SimpleRequest) returns (SimpleResponse); - - // Single-sided unbounded streaming from server to client - // The server repeatedly returns the client payload as-is - rpc StreamingFromServer(SimpleRequest) returns (stream SimpleResponse); - - // Two-sided unbounded streaming between server to client - // Both sides send the content of their own choice to the other - rpc StreamingBothWays(stream SimpleRequest) returns (stream SimpleResponse); -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto deleted file mode 100644 index 1522560c9..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/control.proto +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -import "Model/payloads.proto"; -import "Model/stats.proto"; - -package grpc.testing; - -enum ClientType { - // Many languages support a basic distinction between using - // sync or async client, and this allows the specification - SYNC_CLIENT = 0; - ASYNC_CLIENT = 1; - OTHER_CLIENT = 2; // used for some language-specific variants - CALLBACK_CLIENT = 3; -} - -enum ServerType { - SYNC_SERVER = 0; - ASYNC_SERVER = 1; - ASYNC_GENERIC_SERVER = 2; - OTHER_SERVER = 3; // used for some language-specific variants - CALLBACK_SERVER = 4; -} - -enum RpcType { - UNARY = 0; - STREAMING = 1; - STREAMING_FROM_CLIENT = 2; - STREAMING_FROM_SERVER = 3; - STREAMING_BOTH_WAYS = 4; -} - -// Parameters of poisson process distribution, which is a good representation -// of activity coming in from independent identical stationary sources. -message PoissonParams { - // The rate of arrivals (a.k.a. lambda parameter of the exp distribution). - double offered_load = 1; -} - -// Once an RPC finishes, immediately start a new one. -// No configuration parameters needed. -message ClosedLoopParams {} - -message LoadParams { - oneof load { - ClosedLoopParams closed_loop = 1; - PoissonParams poisson = 2; - }; -} - -// presence of SecurityParams implies use of TLS -message SecurityParams { - bool use_test_ca = 1; - string server_host_override = 2; - string cred_type = 3; -} - -message ChannelArg { - string name = 1; - oneof value { - string str_value = 2; - int32 int_value = 3; - } -} - -message ClientConfig { - // List of targets to connect to. At least one target needs to be specified. - repeated string server_targets = 1; - ClientType client_type = 2; - SecurityParams security_params = 3; - // How many concurrent RPCs to start for each channel. - // For synchronous client, use a separate thread for each outstanding RPC. - int32 outstanding_rpcs_per_channel = 4; - // Number of independent client channels to create. - // i-th channel will connect to server_target[i % server_targets.size()] - int32 client_channels = 5; - // Only for async client. Number of threads to use to start/manage RPCs. - int32 async_client_threads = 7; - RpcType rpc_type = 8; - // The requested load for the entire client (aggregated over all the threads). - LoadParams load_params = 10; - PayloadConfig payload_config = 11; - HistogramParams histogram_params = 12; - - // Specify the cores we should run the client on, if desired - repeated int32 core_list = 13; - int32 core_limit = 14; - - // If we use an OTHER_CLIENT client_type, this string gives more detail - string other_client_api = 15; - - repeated ChannelArg channel_args = 16; - - // Number of threads that share each completion queue - int32 threads_per_cq = 17; - - // Number of messages on a stream before it gets finished/restarted - int32 messages_per_stream = 18; - - // Use coalescing API when possible. - bool use_coalesce_api = 19; - - // If 0, disabled. Else, specifies the period between gathering latency - // medians in milliseconds. - int32 median_latency_collection_interval_millis = 20; - - // Number of client processes. 0 indicates no restriction. - int32 client_processes = 21; -} - -message ClientStatus { ClientStats stats = 1; } - -// Request current stats -message Mark { - // if true, the stats will be reset after taking their snapshot. - bool reset = 1; -} - -message ClientArgs { - oneof argtype { - ClientConfig setup = 1; - Mark mark = 2; - } -} - -message ServerConfig { - ServerType server_type = 1; - SecurityParams security_params = 2; - // Port on which to listen. Zero means pick unused port. - int32 port = 4; - // Only for async server. Number of threads used to serve the requests. - int32 async_server_threads = 7; - // Specify the number of cores to limit server to, if desired - int32 core_limit = 8; - // payload config, used in generic server. - // Note this must NOT be used in proto (non-generic) servers. For proto servers, - // 'response sizes' must be configured from the 'response_size' field of the - // 'SimpleRequest' objects in RPC requests. - PayloadConfig payload_config = 9; - - // Specify the cores we should run the server on, if desired - repeated int32 core_list = 10; - - // If we use an OTHER_SERVER client_type, this string gives more detail - string other_server_api = 11; - - // Number of threads that share each completion queue - int32 threads_per_cq = 12; - - // c++-only options (for now) -------------------------------- - - // Buffer pool size (no buffer pool specified if unset) - int32 resource_quota_size = 1001; - repeated ChannelArg channel_args = 1002; - - // Number of server processes. 0 indicates no restriction. - int32 server_processes = 21; -} - -message ServerArgs { - oneof argtype { - ServerConfig setup = 1; - Mark mark = 2; - } -} - -message ServerStatus { - ServerStats stats = 1; - // the port bound by the server - int32 port = 2; - // Number of cores available to the server - int32 cores = 3; -} - -message CoreRequest { -} - -message CoreResponse { - // Number of cores available on the server - int32 cores = 1; -} - -message Void { -} - -// A single performance scenario: input to qps_json_driver -message Scenario { - // Human readable name for this scenario - string name = 1; - // Client configuration - ClientConfig client_config = 2; - // Number of clients to start for the test - int32 num_clients = 3; - // Server configuration - ServerConfig server_config = 4; - // Number of servers to start for the test - int32 num_servers = 5; - // Warmup period, in seconds - int32 warmup_seconds = 6; - // Benchmark time, in seconds - int32 benchmark_seconds = 7; - // Number of workers to spawn locally (usually zero) - int32 spawn_local_worker_count = 8; -} - -// A set of scenarios to be run with qps_json_driver -message Scenarios { - repeated Scenario scenarios = 1; -} - -// Basic summary that can be computed from ClientStats and ServerStats -// once the scenario has finished. -message ScenarioResultSummary -{ - // Total number of operations per second over all clients. What is counted as 1 'operation' depends on the benchmark scenarios: - // For unary benchmarks, an operation is processing of a single unary RPC. - // For streaming benchmarks, an operation is processing of a single ping pong of request and response. - double qps = 1; - // QPS per server core. - double qps_per_server_core = 2; - // The total server cpu load based on system time across all server processes, expressed as percentage of a single cpu core. - // For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server - // processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. - // Same explanation for the total client cpu load below. - double server_system_time = 3; - // The total server cpu load based on user time across all server processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - double server_user_time = 4; - // The total client cpu load based on system time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - double client_system_time = 5; - // The total client cpu load based on user time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - double client_user_time = 6; - - // X% latency percentiles (in nanoseconds) - double latency_50 = 7; - double latency_90 = 8; - double latency_95 = 9; - double latency_99 = 10; - double latency_999 = 11; - - // server cpu usage percentage - double server_cpu_usage = 12; - - // Number of requests that succeeded/failed - double successful_requests_per_second = 13; - double failed_requests_per_second = 14; - - // Number of polls called inside completion queue per request - double client_polls_per_request = 15; - double server_polls_per_request = 16; - - // Queries per CPU-sec over all servers or clients - double server_queries_per_cpu_sec = 17; - double client_queries_per_cpu_sec = 18; -} - -// Results of a single benchmark scenario. -message ScenarioResult { - // Inputs used to run the scenario. - Scenario scenario = 1; - // Histograms from all clients merged into one histogram. - HistogramData latencies = 2; - // Client stats for each client - repeated ClientStats client_stats = 3; - // Server stats for each server - repeated ServerStats server_stats = 4; - // Number of cores available to each server - repeated int32 server_cores = 5; - // An after-the-fact computed summary - ScenarioResultSummary summary = 6; - // Information on success or failure of each worker - repeated bool client_success = 7; - repeated bool server_success = 8; - // Number of failed requests (one row per status code seen) - repeated RequestResultCount request_results = 9; -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.proto deleted file mode 100644 index ac181b043..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/core_stats.proto +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2017 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.core; - -message Bucket { - double start = 1; - uint64 count = 2; -} - -message Histogram { - repeated Bucket buckets = 1; -} - -message Metric { - string name = 1; - oneof value { - uint64 count = 10; - Histogram histogram = 11; - } -} - -message Stats { - repeated Metric metrics = 1; -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.proto deleted file mode 100644 index 70e342776..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/messages.proto +++ /dev/null @@ -1,214 +0,0 @@ - -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -syntax = "proto3"; - -package grpc.testing; - -// TODO(dgq): Go back to using well-known types once -// https://github.com/grpc/grpc/issues/6980 has been fixed. -// import "google/protobuf/wrappers.proto"; -message BoolValue { - // The bool value. - bool value = 1; -} - -// The type of payload that should be returned. -enum PayloadType { - // Compressable text format. - COMPRESSABLE = 0; -} - -// A block of data, to simply increase gRPC message size. -message Payload { - // The type of data in body. - PayloadType type = 1; - // Primary contents of payload. - bytes body = 2; -} - -// A protobuf representation for grpc status. This is used by test -// clients to specify a status that the server should attempt to return. -message EchoStatus { - int32 code = 1; - string message = 2; -} - -// The type of route that a client took to reach a server w.r.t. gRPCLB. -// The server must fill in "fallback" if it detects that the RPC reached -// the server via the "gRPCLB fallback" path, and "backend" if it detects -// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got -// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly -// how this detection is done is context and server dependent. -enum GrpclbRouteType { - // Server didn't detect the route that a client took to reach it. - GRPCLB_ROUTE_TYPE_UNKNOWN = 0; - // Indicates that a client reached a server via gRPCLB fallback. - GRPCLB_ROUTE_TYPE_FALLBACK = 1; - // Indicates that a client reached a server as a gRPCLB-given backend. - GRPCLB_ROUTE_TYPE_BACKEND = 2; -} - -// Unary request. -message SimpleRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, server randomly chooses one from other formats. - PayloadType response_type = 1; - - // Desired payload size in the response from the server. - int32 response_size = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether SimpleResponse should include username. - bool fill_username = 4; - - // Whether SimpleResponse should include OAuth scope. - bool fill_oauth_scope = 5; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue response_compressed = 6; - - // Whether server should return a given status - EchoStatus response_status = 7; - - // Whether the server should expect this request to be compressed. - BoolValue expect_compressed = 8; - - // Whether SimpleResponse should include server_id. - bool fill_server_id = 9; - - // Whether SimpleResponse should include grpclb_route_type. - bool fill_grpclb_route_type = 10; -} - -// Unary response, as configured by the request. -message SimpleResponse { - // Payload to increase message size. - Payload payload = 1; - // The user the request came from, for verifying authentication was - // successful when the client expected it. - string username = 2; - // OAuth scope. - string oauth_scope = 3; - - // Server ID. This must be unique among different server instances, - // but the same across all RPC's made to a particular server instance. - string server_id = 4; - // gRPCLB Path. - GrpclbRouteType grpclb_route_type = 5; - - // Server hostname. - string hostname = 6; -} - -// Client-streaming request. -message StreamingInputCallRequest { - // Optional input payload sent along with the request. - Payload payload = 1; - - // Whether the server should expect this request to be compressed. This field - // is "nullable" in order to interoperate seamlessly with servers not able to - // implement the full compression tests by introspecting the call to verify - // the request's compression status. - BoolValue expect_compressed = 2; - - // Not expecting any payload from the response. -} - -// Client-streaming response. -message StreamingInputCallResponse { - // Aggregated size of payloads received from the client. - int32 aggregated_payload_size = 1; -} - -// Configuration for a particular response. -message ResponseParameters { - // Desired payload sizes in responses from the server. - int32 size = 1; - - // Desired interval between consecutive responses in the response stream in - // microseconds. - int32 interval_us = 2; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue compressed = 3; -} - -// Server-streaming request. -message StreamingOutputCallRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, the payload from each response in the stream - // might be of different types. This is to simulate a mixed type of payload - // stream. - PayloadType response_type = 1; - - // Configuration for each expected response message. - repeated ResponseParameters response_parameters = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether server should return a given status - EchoStatus response_status = 7; -} - -// Server-streaming response, as configured by the request and parameters. -message StreamingOutputCallResponse { - // Payload to increase response size. - Payload payload = 1; -} - -// For reconnect interop test only. -// Client tells server what reconnection parameters it used. -message ReconnectParams { - int32 max_reconnect_backoff_ms = 1; -} - -// For reconnect interop test only. -// Server tells client whether its reconnects are following the spec and the -// reconnect backoffs it saw. -message ReconnectInfo { - bool passed = 1; - repeated int32 backoff_ms = 2; -} - -message LoadBalancerStatsRequest { - // Request stats for the next num_rpcs sent by client. - int32 num_rpcs = 1; - // If num_rpcs have not completed within timeout_sec, return partial results. - int32 timeout_sec = 2; -} - -message LoadBalancerStatsResponse { - message RpcsByPeer { - // The number of completed RPCs for each peer. - map rpcs_by_peer = 1; - } - // The number of completed RPCs for each peer. - map rpcs_by_peer = 1; - // The number of RPCs that failed to record a remote peer. - int32 num_failures = 2; - map rpcs_by_method = 3; -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.proto deleted file mode 100644 index 4feab92ea..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/payloads.proto +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -message ByteBufferParams { - int32 req_size = 1; - int32 resp_size = 2; -} - -message SimpleProtoParams { - int32 req_size = 1; - int32 resp_size = 2; -} - -message ComplexProtoParams { - // TODO (vpai): Fill this in once the details of complex, representative - // protos are decided -} - -message PayloadConfig { - oneof payload { - ByteBufferParams bytebuf_params = 1; - SimpleProtoParams simple_params = 2; - ComplexProtoParams complex_params = 3; - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto deleted file mode 100644 index e7af31944..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/stats.proto +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -import "Model/core_stats.proto"; - -message ServerStats { - // wall clock time change in seconds since last reset - double time_elapsed = 1; - - // change in user time (in seconds) used by the server since last reset - double time_user = 2; - - // change in server time (in seconds) used by the server process and all - // threads since last reset - double time_system = 3; - - // change in total cpu time of the server (data from proc/stat) - uint64 total_cpu_time = 4; - - // change in idle time of the server (data from proc/stat) - uint64 idle_cpu_time = 5; - - // Number of polls called inside completion queue - uint64 cq_poll_count = 6; - - // Core library stats - grpc.core.Stats core_stats = 7; -} - -// Histogram params based on grpc/support/histogram.c -message HistogramParams { - double resolution = 1; // first bucket is [0, 1 + resolution) - double max_possible = 2; // use enough buckets to allow this value -} - -// Histogram data based on grpc/support/histogram.c -message HistogramData { - repeated uint32 bucket = 1; - double min_seen = 2; - double max_seen = 3; - double sum = 4; - double sum_of_squares = 5; - double count = 6; -} - -message RequestResultCount { - int32 status_code = 1; - int64 count = 2; -} - -message ClientStats { - // Latency histogram. Data points are in nanoseconds. - HistogramData latencies = 1; - - // See ServerStats for details. - double time_elapsed = 2; - double time_user = 3; - double time_system = 4; - - // Number of failed requests (one row per status code seen) - repeated RequestResultCount request_results = 5; - - // Number of polls called inside completion queue - uint64 cq_poll_count = 6; - - // Core library stats - grpc.core.Stats core_stats = 7; -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto b/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto deleted file mode 100644 index 2c0901c36..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Model/worker_service.proto +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. -syntax = "proto3"; - -import "Model/control.proto"; - -package grpc.testing; - -service WorkerService { - // Start server with specified workload. - // First request sent specifies the ServerConfig followed by ServerStatus - // response. After that, a "Mark" can be sent anytime to request the latest - // stats. Closing the stream will initiate shutdown of the test server - // and once the shutdown has finished, the OK status is sent to terminate - // this RPC. - rpc RunServer(stream ServerArgs) returns (stream ServerStatus); - - // Start client with specified workload. - // First request sent specifies the ClientConfig followed by ClientStatus - // response. After that, a "Mark" can be sent anytime to request the latest - // stats. Closing the stream will initiate shutdown of the test client - // and once the shutdown has finished, the OK status is sent to terminate - // this RPC. - rpc RunClient(stream ClientArgs) returns (stream ClientStatus); - - // Just return the core count - unary call - rpc CoreCount(CoreRequest) returns (CoreResponse); - - // Quit this worker - rpc QuitWorker(Void) returns (Void); -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift deleted file mode 100644 index 6a41206b1..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncBenchmarkServiceImpl.swift +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2022, 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 Foundation -import GRPC -import NIOCore - -/// Implementation of asynchronous service for benchmarking. -final class AsyncBenchmarkServiceImpl: Grpc_Testing_BenchmarkServiceAsyncProvider { - let interceptors: Grpc_Testing_BenchmarkServiceServerInterceptorFactoryProtocol? = nil - - /// One request followed by one response. - /// The server returns the client payload as-is. - func unaryCall( - request: Grpc_Testing_SimpleRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse { - return try AsyncBenchmarkServiceImpl.processSimpleRPC(request: request) - } - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - func streamingCall( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await request in requestStream { - let response = try AsyncBenchmarkServiceImpl.processSimpleRPC(request: request) - try await responseStream.send(response) - } - } - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse { - context.request.logger.warning("streamingFromClient not implemented yet") - throw GRPCStatus( - code: .unimplemented, - message: "Not implemented" - ) - } - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - func streamingFromServer( - request: Grpc_Testing_SimpleRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - context.request.logger.warning("streamingFromServer not implemented yet") - throw GRPCStatus( - code: GRPCStatus.Code.unimplemented, - message: "Not implemented" - ) - } - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - func streamingBothWays( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - context.request.logger.warning("streamingBothWays not implemented yet") - throw GRPCStatus( - code: GRPCStatus.Code.unimplemented, - message: "Not implemented" - ) - } - - /// Make a payload for sending back to the client. - private static func makePayload( - type: Grpc_Testing_PayloadType, - size: Int - ) throws -> Grpc_Testing_Payload { - if type != .compressable { - // Making a payload which is not compressable is hard - and not implemented in - // other implementations too. - throw GRPCStatus(code: .internalError, message: "Failed to make payload") - } - var payload = Grpc_Testing_Payload() - payload.body = Data(count: size) - payload.type = type - return payload - } - - /// Process a simple RPC. - /// - parameters: - /// - request: The request from the client. - /// - returns: A response to send back to the client. - private static func processSimpleRPC( - request: Grpc_Testing_SimpleRequest - ) throws -> Grpc_Testing_SimpleResponse { - var response = Grpc_Testing_SimpleResponse() - if request.responseSize > 0 { - response.payload = try self.makePayload( - type: request.responseType, - size: Int(request.responseSize) - ) - } - return response - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift deleted file mode 100644 index 3414e519a..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncClientProtocol.swift +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2022, 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 GRPC -import NIOCore - -/// Protocol which async clients must implement. -protocol AsyncQPSClient { - /// Start the execution of the client. - func startClient() - - /// Send the status of the current test - /// - parameters: - /// - reset: Indicates if the stats collection should be reset after publication or not. - /// - responseStream: the response stream to write the response to. - func sendStatus( - reset: Bool, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws - - /// Shut down the client. - func shutdown() async throws -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift deleted file mode 100644 index c5df08cd5..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncPingPongRequestMaker.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2022, 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 Atomics -import Foundation -import GRPC -import Logging -import NIOCore - -/// Makes streaming requests and listens to responses ping-pong style. -/// Iterations can be limited by config. -/// Class is marked as `@unchecked Sendable` because `ManagedAtomic` doesn't conform -/// to `Sendable`, but we know it's safe. -final class AsyncPingPongRequestMaker: AsyncRequestMaker, @unchecked Sendable { - private let client: Grpc_Testing_BenchmarkServiceAsyncClient - private let requestMessage: Grpc_Testing_SimpleRequest - private let logger: Logger - private let stats: StatsWithLock - - /// If greater than zero gives a limit to how many messages are exchanged before termination. - private let messagesPerStream: Int - /// Stops more requests being made after stop is requested. - private let stopRequested = ManagedAtomic(false) - - /// Initialiser to gather requirements. - /// - Parameters: - /// - config: config from the driver describing what to do. - /// - client: client interface to the server. - /// - requestMessage: Pre-made request message to use possibly repeatedly. - /// - logger: Where to log useful diagnostics. - /// - stats: Where to record statistics on latency. - init( - config: Grpc_Testing_ClientConfig, - client: Grpc_Testing_BenchmarkServiceAsyncClient, - requestMessage: Grpc_Testing_SimpleRequest, - logger: Logger, - stats: StatsWithLock - ) { - self.client = client - self.requestMessage = requestMessage - self.logger = logger - self.stats = stats - - self.messagesPerStream = Int(config.messagesPerStream) - } - - /// Initiate a request sequence to the server - in this case the sequence is streaming requests to the server and waiting - /// to see responses before repeating ping-pong style. The number of iterations can be limited by config. - func makeRequest() async throws { - var startTime = grpcTimeNow() - var messagesSent = 0 - - let streamingCall = self.client.makeStreamingCallCall() - var responseStream = streamingCall.responseStream.makeAsyncIterator() - while !self.stopRequested.load(ordering: .relaxed), - self.messagesPerStream == 0 || messagesSent < self.messagesPerStream { - try await streamingCall.requestStream.send(self.requestMessage) - _ = try await responseStream.next() - let endTime = grpcTimeNow() - self.stats.add(latency: endTime - startTime) - messagesSent += 1 - startTime = endTime - } - } - - /// Request termination of the request-response sequence. - func requestStop() { - self.logger.info("AsyncPingPongRequestMaker stop requested") - // Flag stop as requested - this will prevent any more requests being made. - self.stopRequested.store(true, ordering: .relaxed) - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift deleted file mode 100644 index b764cc770..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSClientImpl.swift +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright 2022, 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 Atomics -import BenchmarkUtils -import Foundation -import GRPC -import Logging -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix - -/// Client to make a series of asynchronous calls. -final class AsyncQPSClientImpl: AsyncQPSClient { - private let logger = Logger(label: "AsyncQPSClientImpl") - - private let eventLoopGroup: MultiThreadedEventLoopGroup - private let threadCount: Int - private let channelRepeaters: [ChannelRepeater] - - private var statsPeriodStart: DispatchTime - private var cpuStatsPeriodStart: CPUTime - - /// Initialise a client to send requests. - /// - parameters: - /// - config: Config from the driver specifying how the client should behave. - init(config: Grpc_Testing_ClientConfig) throws { - // Setup threads - let threadCount = config.threadsToUse() - self.threadCount = threadCount - self.logger.info("Sizing AsyncQPSClientImpl", metadata: ["threads": "\(threadCount)"]) - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) - self.eventLoopGroup = eventLoopGroup - - // Parse possible invalid targets before code with side effects. - let serverTargets = try config.parsedServerTargets() - precondition(serverTargets.count > 0) - - // Start recording stats. - self.statsPeriodStart = grpcTimeNow() - self.cpuStatsPeriodStart = getResourceUsage() - - let requestMessage = try AsyncQPSClientImpl - .makeClientRequest(payloadConfig: config.payloadConfig) - - // Start the requested number of channels. - self.channelRepeaters = (0 ..< Int(config.clientChannels)).map { channelNumber in - ChannelRepeater( - target: serverTargets[channelNumber % serverTargets.count], - requestMessage: requestMessage, - config: config, - eventLoop: eventLoopGroup.next() - ) - } - } - - /// Start the execution of the client. - func startClient() { - Task { - try await withThrowingTaskGroup(of: Void.self) { group in - for repeater in self.channelRepeaters { - group.addTask { - try await repeater.start() - } - } - try await group.waitForAll() - } - } - } - - /// Send current status back to the driver process. - /// - parameters: - /// - reset: Should the stats reset after being sent. - /// - context: Calling context to allow results to be sent back to the driver. - func sendStatus( - reset: Bool, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - let currentTime = grpcTimeNow() - let currentResourceUsage = getResourceUsage() - var result = Grpc_Testing_ClientStatus() - result.stats.timeElapsed = (currentTime - self.statsPeriodStart).asSeconds() - result.stats.timeSystem = currentResourceUsage.systemTime - self.cpuStatsPeriodStart - .systemTime - result.stats.timeUser = currentResourceUsage.userTime - self.cpuStatsPeriodStart.userTime - result.stats.cqPollCount = 0 - - // Collect stats from each of the channels. - var latencyHistogram = Histogram() - var statusCounts = StatusCounts() - for channelRepeater in self.channelRepeaters { - let stats = channelRepeater.getStats(reset: reset) - try! latencyHistogram.merge(source: stats.latencies) - statusCounts.merge(source: stats.statuses) - } - result.stats.latencies = Grpc_Testing_HistogramData(from: latencyHistogram) - result.stats.requestResults = statusCounts.toRequestResultCounts() - self.logger.info("Sending client status") - try await responseStream.send(result) - - if reset { - self.statsPeriodStart = currentTime - self.cpuStatsPeriodStart = currentResourceUsage - } - } - - /// Shut down the service. - func shutdown() async throws { - await withThrowingTaskGroup(of: Void.self) { group in - for repeater in self.channelRepeaters { - group.addTask { - do { - try await repeater.stop() - } catch { - self.logger.warning( - "A channel repeater could not be stopped", - metadata: ["error": "\(error)"] - ) - } - } - } - } - } - - /// Make a request which can be sent to the server. - private static func makeClientRequest( - payloadConfig: Grpc_Testing_PayloadConfig - ) throws -> Grpc_Testing_SimpleRequest { - if let payload = payloadConfig.payload { - switch payload { - case .bytebufParams: - throw GRPCStatus(code: .invalidArgument, message: "Byte buffer not supported.") - case let .simpleParams(simpleParams): - var result = Grpc_Testing_SimpleRequest() - result.responseType = .compressable - result.responseSize = simpleParams.respSize - result.payload.type = .compressable - let size = Int(simpleParams.reqSize) - let body = Data(count: size) - result.payload.body = body - return result - case .complexParams: - throw GRPCStatus( - code: .invalidArgument, - message: "Complex params not supported." - ) - } - } else { - // Default - simple proto without payloads. - var result = Grpc_Testing_SimpleRequest() - result.responseType = .compressable - result.responseSize = 0 - result.payload.type = .compressable - return result - } - } - - /// Class to manage a channel. Repeatedly makes requests on that channel and records what happens. - /// /// Class is marked as `@unchecked Sendable` because `ManagedAtomic` doesn't conform - /// to `Sendable`, but we know it's safe. - private final class ChannelRepeater: @unchecked Sendable { - private let channel: GRPCChannel - private let eventLoop: EventLoop - private let maxPermittedOutstandingRequests: Int - - private let stats: StatsWithLock - - /// Succeeds after a stop has been requested and all outstanding requests have completed. - private let stopComplete: EventLoopPromise - - private let running = ManagedAtomic(false) - - private let requestMaker: RequestMakerType - - init( - target: ConnectionTarget, - requestMessage: Grpc_Testing_SimpleRequest, - config: Grpc_Testing_ClientConfig, - eventLoop: EventLoop - ) { - self.eventLoop = eventLoop - // 'try!' is fine; it'll only throw if we can't make an SSL context - // TODO: Support TLS if requested. - self.channel = try! GRPCChannelPool.with( - target: target, - transportSecurity: .plaintext, - eventLoopGroup: eventLoop - ) - - let logger = Logger(label: "ChannelRepeater") - let client = Grpc_Testing_BenchmarkServiceAsyncClient(channel: self.channel) - self.maxPermittedOutstandingRequests = Int(config.outstandingRpcsPerChannel) - self.stopComplete = eventLoop.makePromise() - self.stats = StatsWithLock() - - self.requestMaker = RequestMakerType( - config: config, - client: client, - requestMessage: requestMessage, - logger: logger, - stats: self.stats - ) - } - - /// Launch as many requests as allowed on the channel. Must only be called once. - private func launchRequests() async throws { - let exchangedRunning = self.running.compareExchange( - expected: false, - desired: true, - ordering: .relaxed - ) - precondition(exchangedRunning.exchanged, "launchRequests should only be called once") - - try await withThrowingTaskGroup(of: Void.self) { group in - for _ in 0 ..< self.maxPermittedOutstandingRequests { - group.addTask { - try await self.requestMaker.makeRequest() - } - } - - /// While `running` is true, we'll keep launching new requests to - /// maintain `maxPermittedOutstandingRequests` running - /// at any given time. - for try await _ in group { - if self.running.load(ordering: .relaxed) { - group.addTask { - try await self.requestMaker.makeRequest() - } - } - } - self.stopIsComplete() - } - } - - /// Get stats for sending to the driver. - /// - parameters: - /// - reset: Should the stats reset after copying. - /// - returns: The statistics for this channel. - func getStats(reset: Bool) -> Stats { - return self.stats.copyData(reset: reset) - } - - /// Start sending requests to the server. - func start() async throws { - try await self.launchRequests() - } - - private func stopIsComplete() { - // Close the connection then signal done. - self.channel.close().cascade(to: self.stopComplete) - } - - /// Stop sending requests to the server. - /// - returns: A future which can be waited on to signal when all activity has ceased. - func stop() async throws { - self.requestMaker.requestStop() - self.running.store(false, ordering: .relaxed) - try await self.stopComplete.futureResult.get() - } - } -} - -/// Create an asynchronous client of the requested type. -/// - parameters: -/// - config: Description of the client required. -/// - returns: The client created. -func makeAsyncClient(config: Grpc_Testing_ClientConfig) throws -> AsyncQPSClient { - switch config.rpcType { - case .unary: - return try AsyncQPSClientImpl(config: config) - case .streaming: - return try AsyncQPSClientImpl(config: config) - case .streamingFromClient, - .streamingFromServer, - .streamingBothWays: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .UNRECOGNIZED: - throw GRPCStatus(code: .invalidArgument, message: "Unrecognised client rpc type") - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift deleted file mode 100644 index a5112aa8b..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncQPSServerImpl.swift +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2022, 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 Foundation -import GRPC -import Logging -import NIOCore -import NIOPosix - -/// Server setup for asynchronous requests. -final class AsyncQPSServerImpl: AsyncQPSServer { - private let logger = Logger(label: "AsyncQPSServerImpl") - - private let eventLoopGroup: MultiThreadedEventLoopGroup - private let server: Server - private let threadCount: Int - - private var statsPeriodStart: DispatchTime - private var cpuStatsPeriodStart: CPUTime - - var serverInfo: ServerInfo { - let port = self.server.channel.localAddress?.port ?? 0 - return ServerInfo(threadCount: self.threadCount, port: port) - } - - /// Initialisation. - /// - parameters: - /// - config: Description of the type of server required. - init(config: Grpc_Testing_ServerConfig) async throws { - // Setup threads as requested. - let threadCount = config.asyncServerThreads > 0 - ? Int(config.asyncServerThreads) - : System.coreCount - self.threadCount = threadCount - self.logger.info("Sizing AsyncQPSServerImpl", metadata: ["threads": "\(threadCount)"]) - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) - - // Start stats gathering. - self.statsPeriodStart = grpcTimeNow() - self.cpuStatsPeriodStart = getResourceUsage() - - let workerService = AsyncBenchmarkServiceImpl() - - // Start the server - self.server = try await Server.insecure(group: self.eventLoopGroup) - .withServiceProviders([workerService]) - .withLogger(self.logger) - .bind(host: "localhost", port: Int(config.port)) - .get() - } - - /// Send the status of the current test - /// - parameters: - /// - reset: Indicates if the stats collection should be reset after publication or not. - /// - responseStream: the response stream to which the status should be sent. - func sendStatus( - reset: Bool, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - let currentTime = grpcTimeNow() - let currentResourceUsage = getResourceUsage() - var result = Grpc_Testing_ServerStatus() - result.stats.timeElapsed = (currentTime - self.statsPeriodStart).asSeconds() - result.stats.timeSystem = currentResourceUsage.systemTime - self.cpuStatsPeriodStart - .systemTime - result.stats.timeUser = currentResourceUsage.userTime - self.cpuStatsPeriodStart.userTime - result.stats.totalCpuTime = 0 - result.stats.idleCpuTime = 0 - result.stats.cqPollCount = 0 - self.logger.info("Sending server status") - try await responseStream.send(result) - if reset { - self.statsPeriodStart = currentTime - self.cpuStatsPeriodStart = currentResourceUsage - } - } - - /// Shut down the service. - func shutdown() async throws { - do { - try await self.server.initiateGracefulShutdown().get() - } catch { - self.logger.error("Error closing server", metadata: ["error": "\(error)"]) - // May as well plough on anyway - - // we will hopefully sort outselves out shutting down the eventloops - } - try await self.eventLoopGroup.shutdownGracefully() - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift deleted file mode 100644 index d0148999a..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncRequestMaker.swift +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2022, 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 GRPC -import Logging - -/// Implement to provide a method of making requests to a server from a client. -protocol AsyncRequestMaker: Sendable { - /// Initialiser to gather requirements. - /// - Parameters: - /// - config: config from the driver describing what to do. - /// - client: client interface to the server. - /// - requestMessage: Pre-made request message to use possibly repeatedly. - /// - logger: Where to log useful diagnostics. - /// - stats: Where to record statistics on latency. - init( - config: Grpc_Testing_ClientConfig, - client: Grpc_Testing_BenchmarkServiceAsyncClient, - requestMessage: Grpc_Testing_SimpleRequest, - logger: Logger, - stats: StatsWithLock - ) - - /// Initiate a request sequence to the server. - func makeRequest() async throws - - /// Request termination of the request-response sequence. - func requestStop() -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift deleted file mode 100644 index 60e1c33b0..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncServerProtocol.swift +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2022, 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 GRPC -import NIOCore - -/// Interface server types must implement when using async APIs. -protocol AsyncQPSServer { - /// The server information for this server. - var serverInfo: ServerInfo { get } - - /// Send the status of the current test - /// - parameters: - /// - reset: Indicates if the stats collection should be reset after publication or not. - /// - responseStream: the response stream to write the response to. - func sendStatus( - reset: Bool, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws - - /// Shut down the service. - func shutdown() async throws -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift deleted file mode 100644 index 5921ef038..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncUnaryRequestMaker.swift +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import Logging -import NIOCore - -/// Makes unary requests to the server and records performance statistics. -final class AsyncUnaryRequestMaker: AsyncRequestMaker { - private let client: Grpc_Testing_BenchmarkServiceAsyncClient - private let requestMessage: Grpc_Testing_SimpleRequest - private let logger: Logger - private let stats: StatsWithLock - - /// Initialiser to gather requirements. - /// - Parameters: - /// - config: config from the driver describing what to do. - /// - client: client interface to the server. - /// - requestMessage: Pre-made request message to use possibly repeatedly. - /// - logger: Where to log useful diagnostics. - /// - stats: Where to record statistics on latency. - init( - config: Grpc_Testing_ClientConfig, - client: Grpc_Testing_BenchmarkServiceAsyncClient, - requestMessage: Grpc_Testing_SimpleRequest, - logger: Logging.Logger, - stats: StatsWithLock - ) { - self.client = client - self.requestMessage = requestMessage - self.logger = logger - self.stats = stats - } - - /// Initiate a request sequence to the server - in this case a single unary requests and wait for a response. - /// - returns: A future which completes when the request-response sequence is complete. - func makeRequest() async throws { - let startTime = grpcTimeNow() - do { - _ = try await self.client.unaryCall(self.requestMessage) - let endTime = grpcTimeNow() - self.stats.add(latency: endTime - startTime) - } catch { - self.logger.error("Error from unary request", metadata: ["error": "\(error)"]) - throw error - } - } - - /// Request termination of the request-response sequence. - func requestStop() { - // No action here - we could potentially try and cancel the request easiest to just wait. - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift deleted file mode 100644 index 2b82d9d60..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Async/AsyncWorkerServiceImpl.swift +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright 2022, 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 GRPC -import NIOCore - -// Implementation of the control service for communication with the driver process. -actor AsyncWorkerServiceImpl: Grpc_Testing_WorkerServiceAsyncProvider { - let interceptors: Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol? = nil - - private let finishedPromise: EventLoopPromise - private let serverPortOverride: Int? - - private var runningServer: AsyncQPSServer? - private var runningClient: AsyncQPSClient? - - /// Initialise. - /// - parameters: - /// - finishedPromise: Promise to complete when the server has finished running. - /// - serverPortOverride: An override to port number requested by the driver process. - init(finishedPromise: EventLoopPromise, serverPortOverride: Int?) { - self.finishedPromise = finishedPromise - self.serverPortOverride = serverPortOverride - } - - /// Start server with specified workload. - /// First request sent specifies the ServerConfig followed by ServerStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test server - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runServer( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - context.request.logger.info("runServer stream started.") - for try await request in requestStream { - try await self.handleServerMessage( - context: context, - args: request, - responseStream: responseStream - ) - } - try await self.handleServerEnd(context: context) - } - - /// Start client with specified workload. - /// First request sent specifies the ClientConfig followed by ClientStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test client - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runClient( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await request in requestStream { - try await self.handleClientMessage( - context: context, - args: request, - responseStream: responseStream - ) - } - try await self.handleClientEnd(context: context) - } - - /// Just return the core count - unary call - func coreCount( - request: Grpc_Testing_CoreRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_CoreResponse { - context.request.logger.notice("coreCount queried") - return Grpc_Testing_CoreResponse.with { $0.cores = Int32(System.coreCount) } - } - - /// Quit this worker - func quitWorker( - request: Grpc_Testing_Void, - context: GRPCAsyncServerCallContext - ) -> Grpc_Testing_Void { - context.request.logger.warning("quitWorker called") - self.finishedPromise.succeed(()) - return Grpc_Testing_Void() - } - - // MARK: Run Server - - /// Handle a message received from the driver about operating as a server. - private func handleServerMessage( - context: GRPCAsyncServerCallContext, - args: Grpc_Testing_ServerArgs, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - switch args.argtype { - case let .some(.setup(serverConfig)): - try await self.handleServerSetup( - context: context, - config: serverConfig, - responseStream: responseStream - ) - case let .some(.mark(mark)): - try await self.handleServerMarkRequested( - context: context, - mark: mark, - responseStream: responseStream - ) - case .none: - () - } - } - - /// Handle a request to setup a server. - /// Makes a new server and sets it running. - private func handleServerSetup( - context: GRPCAsyncServerCallContext, - config: Grpc_Testing_ServerConfig, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - context.request.logger.info("server setup requested") - guard self.runningServer == nil else { - context.request.logger.error("server already running") - throw GRPCStatus( - code: GRPCStatus.Code.resourceExhausted, - message: "Server worker busy" - ) - } - try await self.runServerBody( - context: context, - serverConfig: config, - responseStream: responseStream - ) - } - - /// Gathers stats and returns them to the driver process. - private func handleServerMarkRequested( - context: GRPCAsyncServerCallContext, - mark: Grpc_Testing_Mark, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - context.request.logger.info("server mark requested") - guard let runningServer = self.runningServer else { - context.request.logger.error("server not running") - throw GRPCStatus( - code: GRPCStatus.Code.failedPrecondition, - message: "Server not running" - ) - } - try await runningServer.sendStatus(reset: mark.reset, responseStream: responseStream) - } - - /// Handle a message from the driver asking this server function to stop running. - private func handleServerEnd(context: GRPCAsyncServerCallContext) async throws { - context.request.logger.info("runServer stream ended.") - if let runningServer = self.runningServer { - self.runningServer = nil - try await runningServer.shutdown() - } - } - - // MARK: Create Server - - /// Start a server running of the requested type. - private func runServerBody( - context: GRPCAsyncServerCallContext, - serverConfig: Grpc_Testing_ServerConfig, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - var serverConfig = serverConfig - self.serverPortOverride.map { serverConfig.port = Int32($0) } - - self.runningServer = try await AsyncWorkerServiceImpl.createServer( - context: context, - config: serverConfig, - responseStream: responseStream - ) - } - - private static func sendServerInfo( - _ serverInfo: ServerInfo, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - var response = Grpc_Testing_ServerStatus() - response.cores = Int32(serverInfo.threadCount) - response.port = Int32(serverInfo.port) - try await responseStream.send(response) - } - - /// Create a server of the requested type. - private static func createServer( - context: GRPCAsyncServerCallContext, - config: Grpc_Testing_ServerConfig, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws -> AsyncQPSServer { - context.request.logger.info( - "Starting server", - metadata: ["type": .stringConvertible(config.serverType)] - ) - - switch config.serverType { - case .asyncServer: - let asyncServer = try await AsyncQPSServerImpl(config: config) - let serverInfo = asyncServer.serverInfo - try await self.sendServerInfo(serverInfo, responseStream: responseStream) - return asyncServer - case .syncServer, - .asyncGenericServer, - .otherServer, - .callbackServer: - throw GRPCStatus(code: .unimplemented, message: "Server Type not implemented") - case .UNRECOGNIZED: - throw GRPCStatus(code: .invalidArgument, message: "Unrecognised server type") - } - } - - // MARK: Run Client - - /// Handle a message from the driver about operating as a client. - private func handleClientMessage( - context: GRPCAsyncServerCallContext, - args: Grpc_Testing_ClientArgs, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - switch args.argtype { - case let .some(.setup(clientConfig)): - try await self.handleClientSetup( - context: context, - config: clientConfig, - responseStream: responseStream - ) - self.runningClient!.startClient() - case let .some(.mark(mark)): - // Capture stats - try await self.handleClientMarkRequested( - context: context, - mark: mark, - responseStream: responseStream - ) - case .none: - () - } - } - - /// Setup a client as described by the message from the driver. - private func handleClientSetup( - context: GRPCAsyncServerCallContext, - config: Grpc_Testing_ClientConfig, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - context.request.logger.info("client setup requested") - guard self.runningClient == nil else { - context.request.logger.error("client already running") - throw GRPCStatus( - code: GRPCStatus.Code.resourceExhausted, - message: "Client worker busy" - ) - } - try self.runClientBody(context: context, clientConfig: config) - // Initial status is the default (in C++) - try await responseStream.send(Grpc_Testing_ClientStatus()) - } - - /// Captures stats and send back to driver process. - private func handleClientMarkRequested( - context: GRPCAsyncServerCallContext, - mark: Grpc_Testing_Mark, - responseStream: GRPCAsyncResponseStreamWriter - ) async throws { - context.request.logger.info("client mark requested") - guard let runningClient = self.runningClient else { - context.request.logger.error("client not running") - throw GRPCStatus( - code: GRPCStatus.Code.failedPrecondition, - message: "Client not running" - ) - } - try await runningClient.sendStatus(reset: mark.reset, responseStream: responseStream) - } - - /// Call when an end message has been received. - /// Causes the running client to shutdown. - private func handleClientEnd(context: GRPCAsyncServerCallContext) async throws { - context.request.logger.info("runClient ended") - if let runningClient = self.runningClient { - self.runningClient = nil - try await runningClient.shutdown() - } - } - - // MARK: Create Client - - /// Setup and run a client of the requested type. - private func runClientBody( - context: GRPCAsyncServerCallContext, - clientConfig: Grpc_Testing_ClientConfig - ) throws { - self.runningClient = try AsyncWorkerServiceImpl.makeClient( - context: context, - clientConfig: clientConfig - ) - } - - /// Create a client of the requested type. - private static func makeClient( - context: GRPCAsyncServerCallContext, - clientConfig: Grpc_Testing_ClientConfig - ) throws -> AsyncQPSClient { - switch clientConfig.clientType { - case .asyncClient: - if case .bytebufParams = clientConfig.payloadConfig.payload { - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - } - return try makeAsyncClient(config: clientConfig) - case .syncClient, - .otherClient, - .callbackClient: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .UNRECOGNIZED: - throw GRPCStatus(code: .invalidArgument, message: "Unrecognised client type") - } - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ClientUtilities.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ClientUtilities.swift deleted file mode 100644 index 18a3d2db2..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ClientUtilities.swift +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import NIOCore - -extension Grpc_Testing_ClientConfig { - /// Work out how many theads to use - defaulting to core count if not specified. - /// - returns: The number of threads to use. - func threadsToUse() -> Int { - return self.asyncClientThreads > 0 ? Int(self.asyncClientThreads) : System.coreCount - } - - /// Get the server targets parsed into a useful format. - /// - returns: Server targets as hosts and ports. - func parsedServerTargets() throws -> [ConnectionTarget] { - let serverTargets = self.serverTargets - return try serverTargets.map { target in - if let splitIndex = target.lastIndex(of: ":") { - let host = target[.. EventLoopFuture { - do { - return context.eventLoop - .makeSucceededFuture(try NIOBenchmarkServiceImpl.processSimpleRPC(request: request)) - } catch { - return context.eventLoop.makeFailedFuture(error) - } - } - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - func streamingCall( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(request): - do { - let response = try NIOBenchmarkServiceImpl.processSimpleRPC(request: request) - context.sendResponse(response, promise: nil) - } catch { - context.statusPromise.fail(error) - } - case .end: - context.statusPromise.succeed(.ok) - } - }) - } - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - context.logger.warning("streamingFromClient not implemented yet") - return context.eventLoop.makeFailedFuture(GRPCStatus( - code: GRPCStatus.Code.unimplemented, - message: "Not implemented" - )) - } - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - func streamingFromServer( - request: Grpc_Testing_SimpleRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - context.logger.warning("streamingFromServer not implemented yet") - return context.eventLoop.makeFailedFuture(GRPCStatus( - code: GRPCStatus.Code.unimplemented, - message: "Not implemented" - )) - } - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - func streamingBothWays( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - context.logger.warning("streamingBothWays not implemented yet") - return context.eventLoop.makeFailedFuture(GRPCStatus( - code: GRPCStatus.Code.unimplemented, - message: "Not implemented" - )) - } - - /// Make a payload for sending back to the client. - private static func makePayload( - type: Grpc_Testing_PayloadType, - size: Int - ) throws -> Grpc_Testing_Payload { - if type != .compressable { - // Making a payload which is not compressable is hard - and not implemented in - // other implementations too. - throw GRPCStatus(code: .internalError, message: "Failed to make payload") - } - var payload = Grpc_Testing_Payload() - payload.body = Data(count: size) - payload.type = type - return payload - } - - /// Process a simple RPC. - /// - parameters: - /// - request: The request from the client. - /// - returns: A response to send back to the client. - private static func processSimpleRPC( - request: Grpc_Testing_SimpleRequest - ) throws -> Grpc_Testing_SimpleResponse { - var response = Grpc_Testing_SimpleResponse() - if request.responseSize > 0 { - response.payload = try self.makePayload( - type: request.responseType, - size: Int(request.responseSize) - ) - } - return response - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOClientProtocol.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOClientProtocol.swift deleted file mode 100644 index dea3f1fdb..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOClientProtocol.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import NIOCore - -/// Protocol which clients must implement. -protocol NIOQPSClient { - /// Send the status of the current test - /// - parameters: - /// - reset: Indicates if the stats collection should be reset after publication or not. - /// - context: Context to describe where to send the status to. - func sendStatus(reset: Bool, context: StreamingResponseCallContext) - - /// Shutdown the service. - /// - parameters: - /// - callbackLoop: Which eventloop should be called back on completion. - /// - returns: A future on the `callbackLoop` which will succeed on completion of shutdown. - func shutdown(callbackLoop: EventLoop) -> EventLoopFuture -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOPingPongRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOPingPongRequestMaker.swift deleted file mode 100644 index 9a40c010c..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOPingPongRequestMaker.swift +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020, 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 Foundation -import GRPC -import Logging -import NIOCore - -/// Makes streaming requests and listens to responses ping-pong style. -/// Iterations can be limited by config. -final class NIOPingPongRequestMaker: NIORequestMaker { - private let client: Grpc_Testing_BenchmarkServiceNIOClient - private let requestMessage: Grpc_Testing_SimpleRequest - private let logger: Logger - private let stats: StatsWithLock - - /// If greater than zero gives a limit to how many messages are exchanged before termination. - private let messagesPerStream: Int - /// Stops more requests being made after stop is requested. - private var stopRequested = false - - /// Initialiser to gather requirements. - /// - Parameters: - /// - config: config from the driver describing what to do. - /// - client: client interface to the server. - /// - requestMessage: Pre-made request message to use possibly repeatedly. - /// - logger: Where to log useful diagnostics. - /// - stats: Where to record statistics on latency. - init( - config: Grpc_Testing_ClientConfig, - client: Grpc_Testing_BenchmarkServiceNIOClient, - requestMessage: Grpc_Testing_SimpleRequest, - logger: Logger, - stats: StatsWithLock - ) { - self.client = client - self.requestMessage = requestMessage - self.logger = logger - self.stats = stats - - self.messagesPerStream = Int(config.messagesPerStream) - } - - /// Initiate a request sequence to the server - in this case the sequence is streaming requests to the server and waiting - /// to see responses before repeating ping-pong style. The number of iterations can be limited by config. - /// - returns: A future which completes when the request-response sequence is complete. - func makeRequest() -> EventLoopFuture { - var startTime = grpcTimeNow() - var messagesSent = 1 - var streamingCall: BidirectionalStreamingCall< - Grpc_Testing_SimpleRequest, - Grpc_Testing_SimpleResponse - >? - - /// Handle a response from the server - potentially triggers making another request. - /// Will execute on the event loop which deals with thread safety concerns. - func handleResponse(response: Grpc_Testing_SimpleResponse) { - streamingCall!.eventLoop.preconditionInEventLoop() - let endTime = grpcTimeNow() - self.stats.add(latency: endTime - startTime) - if !self.stopRequested, - self.messagesPerStream == 0 || messagesSent < self.messagesPerStream { - messagesSent += 1 - startTime = endTime // Use end of previous request as the start of the next. - streamingCall!.sendMessage(self.requestMessage, promise: nil) - } else { - streamingCall!.sendEnd(promise: nil) - } - } - - // Setup the call. - streamingCall = self.client.streamingCall(handler: handleResponse) - // Kick start with initial request - streamingCall!.sendMessage(self.requestMessage, promise: nil) - - return streamingCall!.status - } - - /// Request termination of the request-response sequence. - func requestStop() { - // Flag stop as requested - this will prevent any more requests being made. - self.stopRequested = true - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSClientImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSClientImpl.swift deleted file mode 100644 index 3e7a5f2f8..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSClientImpl.swift +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright 2020, 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 Atomics -import BenchmarkUtils -import Foundation -import GRPC -import Logging -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix - -/// Client to make a series of asynchronous calls. -final class NIOQPSClientImpl: NIOQPSClient { - private let eventLoopGroup: MultiThreadedEventLoopGroup - private let threadCount: Int - - private let logger = Logger(label: "NIOQPSClientImpl") - - private let channelRepeaters: [ChannelRepeater] - - private var statsPeriodStart: DispatchTime - private var cpuStatsPeriodStart: CPUTime - - /// Initialise a client to send requests. - /// - parameters: - /// - config: Config from the driver specifying how the client should behave. - init(config: Grpc_Testing_ClientConfig) throws { - // Parse possible invalid targets before code with side effects. - let serverTargets = try config.parsedServerTargets() - precondition(serverTargets.count > 0) - - // Setup threads - let threadCount = config.threadsToUse() - self.threadCount = threadCount - self.logger.info("Sizing NIOQPSClientImpl", metadata: ["threads": "\(threadCount)"]) - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) - self.eventLoopGroup = eventLoopGroup - - // Start recording stats. - self.statsPeriodStart = grpcTimeNow() - self.cpuStatsPeriodStart = getResourceUsage() - - let requestMessage = try NIOQPSClientImpl - .makeClientRequest(payloadConfig: config.payloadConfig) - - // Start the requested number of channels. - self.channelRepeaters = (0 ..< Int(config.clientChannels)).map { channelNumber in - ChannelRepeater( - target: serverTargets[channelNumber % serverTargets.count], - requestMessage: requestMessage, - config: config, - eventLoop: eventLoopGroup.next() - ) - } - - // Start the train. - for channelRepeater in self.channelRepeaters { - channelRepeater.start() - } - } - - /// Send current status back to the driver process. - /// - parameters: - /// - reset: Should the stats reset after being sent. - /// - context: Calling context to allow results to be sent back to the driver. - func sendStatus(reset: Bool, context: StreamingResponseCallContext) { - let currentTime = grpcTimeNow() - let currentResourceUsage = getResourceUsage() - var result = Grpc_Testing_ClientStatus() - result.stats.timeElapsed = (currentTime - self.statsPeriodStart).asSeconds() - result.stats.timeSystem = currentResourceUsage.systemTime - self.cpuStatsPeriodStart - .systemTime - result.stats.timeUser = currentResourceUsage.userTime - self.cpuStatsPeriodStart.userTime - result.stats.cqPollCount = 0 - - // Collect stats from each of the channels. - var latencyHistogram = Histogram() - var statusCounts = StatusCounts() - for channelRepeater in self.channelRepeaters { - let stats = channelRepeater.getStats(reset: reset) - try! latencyHistogram.merge(source: stats.latencies) - statusCounts.merge(source: stats.statuses) - } - result.stats.latencies = Grpc_Testing_HistogramData(from: latencyHistogram) - result.stats.requestResults = statusCounts.toRequestResultCounts() - self.logger.info("Sending client status") - _ = context.sendResponse(result) - - if reset { - self.statsPeriodStart = currentTime - self.cpuStatsPeriodStart = currentResourceUsage - } - } - - /// Shutdown the service. - /// - parameters: - /// - callbackLoop: Which eventloop should be called back on completion. - /// - returns: A future on the `callbackLoop` which will succeed on completion of shutdown. - func shutdown(callbackLoop: EventLoop) -> EventLoopFuture { - let stoppedFutures = self.channelRepeaters.map { repeater in repeater.stop() } - let allStopped = EventLoopFuture.andAllComplete(stoppedFutures, on: callbackLoop) - return allStopped.flatMap { _ in - let promise: EventLoopPromise = callbackLoop.makePromise() - self.eventLoopGroup.shutdownGracefully { error in - if let error = error { - promise.fail(error) - } else { - promise.succeed(()) - } - } - return promise.futureResult - } - } - - /// Make a request which can be sent to the server. - private static func makeClientRequest(payloadConfig: Grpc_Testing_PayloadConfig) throws - -> Grpc_Testing_SimpleRequest { - if let payload = payloadConfig.payload { - switch payload { - case .bytebufParams: - throw GRPCStatus(code: .invalidArgument, message: "Byte buffer not supported.") - case let .simpleParams(simpleParams): - var result = Grpc_Testing_SimpleRequest() - result.responseType = .compressable - result.responseSize = simpleParams.respSize - result.payload.type = .compressable - let size = Int(simpleParams.reqSize) - let body = Data(count: size) - result.payload.body = body - return result - case .complexParams: - throw GRPCStatus( - code: .invalidArgument, - message: "Complex params not supported." - ) - } - } else { - // Default - simple proto without payloads. - var result = Grpc_Testing_SimpleRequest() - result.responseType = .compressable - result.responseSize = 0 - result.payload.type = .compressable - return result - } - } - - /// Class to manage a channel. Repeatedly makes requests on that channel and records what happens. - private class ChannelRepeater { - private let channel: GRPCChannel - private let eventLoop: EventLoop - private let maxPermittedOutstandingRequests: Int - - private let stats: StatsWithLock - - /// Succeeds after a stop has been requested and all outstanding requests have completed. - private var stopComplete: EventLoopPromise - - private let running = ManagedAtomic(false) - private let outstanding = ManagedAtomic(0) - - private var requestMaker: RequestMakerType - - init( - target: ConnectionTarget, - requestMessage: Grpc_Testing_SimpleRequest, - config: Grpc_Testing_ClientConfig, - eventLoop: EventLoop - ) { - self.eventLoop = eventLoop - // 'try!' is fine; it'll only throw if we can't make an SSL context - // TODO: Support TLS if requested. - self.channel = try! GRPCChannelPool.with( - target: target, - transportSecurity: .plaintext, - eventLoopGroup: eventLoop - ) - - let logger = Logger(label: "ChannelRepeater") - let client = Grpc_Testing_BenchmarkServiceNIOClient(channel: self.channel) - self.maxPermittedOutstandingRequests = Int(config.outstandingRpcsPerChannel) - self.stopComplete = eventLoop.makePromise() - self.stats = StatsWithLock() - - self.requestMaker = RequestMakerType( - config: config, - client: client, - requestMessage: requestMessage, - logger: logger, - stats: self.stats - ) - } - - /// Launch as many requests as allowed on the channel. Must only be called once. - private func launchRequests() { - // The plan here is: - // - store the max number of outstanding requests in an atomic - // - start that many requests asynchronously - // - when a request finishes it will either start a new request or decrement the - // the atomic counter (if we've been told to stop) - // - if the counter drops to zero we're finished. - let exchangedRunning = self.running.compareExchange( - expected: false, - desired: true, - ordering: .relaxed - ) - precondition(exchangedRunning.exchanged, "launchRequests should only be called once") - - // We only decrement the outstanding count when running has been changed back to false. - let exchangedOutstanding = self.outstanding.compareExchange( - expected: 0, - desired: self.maxPermittedOutstandingRequests, - ordering: .relaxed - ) - precondition(exchangedOutstanding.exchanged, "launchRequests should only be called once") - - for _ in 0 ..< self.maxPermittedOutstandingRequests { - self.requestMaker.makeRequest().whenComplete { _ in - self.makeRequest() - } - } - } - - private func makeRequest() { - if self.running.load(ordering: .relaxed) { - self.requestMaker.makeRequest().whenComplete { _ in - self.makeRequest() - } - } else if self.outstanding.loadThenWrappingDecrement(ordering: .relaxed) == 1 { - self.stopIsComplete() - } // else we're no longer running but not all RPCs have finished. - } - - /// Get stats for sending to the driver. - /// - parameters: - /// - reset: Should the stats reset after copying. - /// - returns: The statistics for this channel. - func getStats(reset: Bool) -> Stats { - return self.stats.copyData(reset: reset) - } - - /// Start sending requests to the server. - func start() { - self.launchRequests() - } - - private func stopIsComplete() { - // Close the connection then signal done. - self.channel.close().cascade(to: self.stopComplete) - } - - /// Stop sending requests to the server. - /// - returns: A future which can be waited on to signal when all activity has ceased. - func stop() -> EventLoopFuture { - self.requestMaker.requestStop() - self.running.store(false, ordering: .relaxed) - return self.stopComplete.futureResult - } - } -} - -/// Create an asynchronous client of the requested type. -/// - parameters: -/// - config: Description of the client required. -/// - returns: The client created. -func makeAsyncClient(config: Grpc_Testing_ClientConfig) throws -> NIOQPSClient { - switch config.rpcType { - case .unary: - return try NIOQPSClientImpl(config: config) - case .streaming: - return try NIOQPSClientImpl(config: config) - case .streamingFromClient: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .streamingFromServer: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .streamingBothWays: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .UNRECOGNIZED: - throw GRPCStatus(code: .invalidArgument, message: "Unrecognised client rpc type") - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSServerImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSServerImpl.swift deleted file mode 100644 index 234833ed7..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOQPSServerImpl.swift +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2020, 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 Foundation -import GRPC -import Logging -import NIOCore -import NIOPosix - -/// Server setup for asynchronous requests (using EventLoopFutures). -final class NIOQPSServerImpl: NIOQPSServer { - private let eventLoopGroup: MultiThreadedEventLoopGroup - private let server: EventLoopFuture - private let threadCount: Int - - private var statsPeriodStart: DispatchTime - private var cpuStatsPeriodStart: CPUTime - - private let logger = Logger(label: "AsyncQPSServer") - - /// Initialisation. - /// - parameters: - /// - config: Description of the type of server required. - /// - whenBound: Called when the server has successful bound to a port. - init(config: Grpc_Testing_ServerConfig, whenBound: @escaping (ServerInfo) -> Void) { - // Setup threads as requested. - let threadCount = config.asyncServerThreads > 0 - ? Int(config.asyncServerThreads) - : System.coreCount - self.threadCount = threadCount - self.logger.info("Sizing AsyncQPSServer", metadata: ["threads": "\(threadCount)"]) - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: threadCount) - - // Start stats gathering. - self.statsPeriodStart = grpcTimeNow() - self.cpuStatsPeriodStart = getResourceUsage() - - let workerService = NIOBenchmarkServiceImpl() - - // Start the server. - // TODO: Support TLS if requested. - self.server = Server.insecure(group: self.eventLoopGroup) - .withServiceProviders([workerService]) - .withLogger(self.logger) - .bind(host: "localhost", port: Int(config.port)) - - self.server.whenSuccess { server in - let port = server.channel.localAddress?.port ?? 0 - whenBound(ServerInfo(threadCount: threadCount, port: port)) - } - } - - /// Send the status of the current test - /// - parameters: - /// - reset: Indicates if the stats collection should be reset after publication or not. - /// - context: Context to describe where to send the status to. - func sendStatus(reset: Bool, context: StreamingResponseCallContext) { - let currentTime = grpcTimeNow() - let currentResourceUsage = getResourceUsage() - var result = Grpc_Testing_ServerStatus() - result.stats.timeElapsed = (currentTime - self.statsPeriodStart).asSeconds() - result.stats.timeSystem = currentResourceUsage.systemTime - self.cpuStatsPeriodStart - .systemTime - result.stats.timeUser = currentResourceUsage.userTime - self.cpuStatsPeriodStart.userTime - result.stats.totalCpuTime = 0 - result.stats.idleCpuTime = 0 - result.stats.cqPollCount = 0 - self.logger.info("Sending server status") - _ = context.sendResponse(result) - if reset { - self.statsPeriodStart = currentTime - self.cpuStatsPeriodStart = currentResourceUsage - } - } - - /// Shutdown the service. - /// - parameters: - /// - callbackLoop: Which eventloop should be called back on completion. - /// - returns: A future on the `callbackLoop` which will succeed on completion of shutdown. - func shutdown(callbackLoop: EventLoop) -> EventLoopFuture { - return self.server.flatMap { server in - server.close() - }.recover { error in - self.logger.error("Error closing server", metadata: ["error": "\(error)"]) - // May as well plough on anyway - - // we will hopefully sort outselves out shutting down the eventloops - return () - }.hop(to: callbackLoop).flatMap { _ in - let promise: EventLoopPromise = callbackLoop.makePromise() - self.eventLoopGroup.shutdownGracefully { error in - if let error = error { - promise.fail(error) - } else { - promise.succeed(()) - } - } - return promise.futureResult - } - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIORequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIORequestMaker.swift deleted file mode 100644 index 3bbb74b87..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIORequestMaker.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import Logging -import NIOCore - -/// Implement to provide a method of making requests to a server from a client. -protocol NIORequestMaker { - /// Initialiser to gather requirements. - /// - Parameters: - /// - config: config from the driver describing what to do. - /// - client: client interface to the server. - /// - requestMessage: Pre-made request message to use possibly repeatedly. - /// - logger: Where to log useful diagnostics. - /// - stats: Where to record statistics on latency. - init( - config: Grpc_Testing_ClientConfig, - client: Grpc_Testing_BenchmarkServiceNIOClient, - requestMessage: Grpc_Testing_SimpleRequest, - logger: Logger, - stats: StatsWithLock - ) - - /// Initiate a request sequence to the server. - /// - returns: A future which completes when the request-response sequence is complete. - func makeRequest() -> EventLoopFuture - - /// Request termination of the request-response sequence. - func requestStop() -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOServerProtocol.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOServerProtocol.swift deleted file mode 100644 index 7a04207e6..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOServerProtocol.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import NIOCore - -/// Interface server types must implement when using NIO. -protocol NIOQPSServer { - /// Send the status of the current test - /// - parameters: - /// - reset: Indicates if the stats collection should be reset after publication or not. - /// - context: Context to describe where to send the status to. - func sendStatus(reset: Bool, context: StreamingResponseCallContext) - - /// Shutdown the service. - /// - parameters: - /// - callbackLoop: Which eventloop should be called back on completion. - /// - returns: A future on the `callbackLoop` which will succeed on completion of shutdown. - func shutdown(callbackLoop: EventLoop) -> EventLoopFuture -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOUnaryRequestMaker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOUnaryRequestMaker.swift deleted file mode 100644 index ba2feb807..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOUnaryRequestMaker.swift +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import Logging -import NIOCore - -/// Makes unary requests to the server and records performance statistics. -final class NIOUnaryRequestMaker: NIORequestMaker { - private let client: Grpc_Testing_BenchmarkServiceNIOClient - private let requestMessage: Grpc_Testing_SimpleRequest - private let logger: Logger - private let stats: StatsWithLock - - /// Initialiser to gather requirements. - /// - Parameters: - /// - config: config from the driver describing what to do. - /// - client: client interface to the server. - /// - requestMessage: Pre-made request message to use possibly repeatedly. - /// - logger: Where to log useful diagnostics. - /// - stats: Where to record statistics on latency. - init( - config: Grpc_Testing_ClientConfig, - client: Grpc_Testing_BenchmarkServiceNIOClient, - requestMessage: Grpc_Testing_SimpleRequest, - logger: Logger, - stats: StatsWithLock - ) { - self.client = client - self.requestMessage = requestMessage - self.logger = logger - self.stats = stats - } - - /// Initiate a request sequence to the server - in this case a single unary requests and wait for a response. - /// - returns: A future which completes when the request-response sequence is complete. - func makeRequest() -> EventLoopFuture { - let startTime = grpcTimeNow() - let result = self.client.unaryCall(self.requestMessage) - // Log latency stats on completion. - result.status.whenSuccess { status in - if status.isOk { - let endTime = grpcTimeNow() - self.stats.add(latency: endTime - startTime) - } else { - self.logger.error( - "Bad status from unary request", - metadata: ["status": "\(status)"] - ) - } - } - return result.status - } - - /// Request termination of the request-response sequence. - func requestStop() { - // No action here - we could potentially try and cancel the request easiest to just wait. - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOWorkerServiceImpl.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOWorkerServiceImpl.swift deleted file mode 100644 index 1466d5e17..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/NIO/NIOWorkerServiceImpl.swift +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import NIOCore - -// Implementation of the control service for communication with the driver process. -class NIOWorkerServiceImpl: Grpc_Testing_WorkerServiceProvider { - let interceptors: Grpc_Testing_WorkerServiceServerInterceptorFactoryProtocol? = nil - - private let finishedPromise: EventLoopPromise - private let serverPortOverride: Int? - - private var runningServer: NIOQPSServer? - private var runningClient: NIOQPSClient? - - /// Initialise. - /// - parameters: - /// - finishedPromise: Promise to complete when the server has finished running. - /// - serverPortOverride: An override to port number requested by the driver process. - init(finishedPromise: EventLoopPromise, serverPortOverride: Int?) { - self.finishedPromise = finishedPromise - self.serverPortOverride = serverPortOverride - } - - /// Start server with specified workload. - /// First request sent specifies the ServerConfig followed by ServerStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test server - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runServer(context: StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> { - context.logger.info("runServer stream started.") - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(serverArgs): - self.handleServerMessage(context: context, args: serverArgs) - case .end: - self.handleServerEnd(context: context) - } - }) - } - - /// Start client with specified workload. - /// First request sent specifies the ClientConfig followed by ClientStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test client - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runClient(context: StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> { - context.logger.info("runClient stream started") - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(clientArgs): - self.handleClientMessage(context: context, args: clientArgs) - case .end: - self.handleClientEnd(context: context) - } - }) - } - - /// Just return the core count - unary call - func coreCount( - request: Grpc_Testing_CoreRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - context.logger.notice("coreCount queried") - let cores = Grpc_Testing_CoreResponse.with { $0.cores = Int32(System.coreCount) } - return context.eventLoop.makeSucceededFuture(cores) - } - - /// Quit this worker - func quitWorker( - request: Grpc_Testing_Void, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - context.logger.warning("quitWorker called") - self.finishedPromise.succeed(()) - return context.eventLoop.makeSucceededFuture(Grpc_Testing_Void()) - } - - // MARK: Run Server - - /// Handle a message received from the driver about operating as a server. - private func handleServerMessage( - context: StreamingResponseCallContext, - args: Grpc_Testing_ServerArgs - ) { - switch args.argtype { - case let .some(.setup(serverConfig)): - self.handleServerSetup(context: context, config: serverConfig) - case let .some(.mark(mark)): - self.handleServerMarkRequested(context: context, mark: mark) - case .none: - () - } - } - - /// Handle a request to setup a server. - /// Makes a new server and sets it running. - private func handleServerSetup( - context: StreamingResponseCallContext, - config: Grpc_Testing_ServerConfig - ) { - context.logger.info("server setup requested") - guard self.runningServer == nil else { - context.logger.error("server already running") - context.statusPromise - .fail(GRPCStatus( - code: GRPCStatus.Code.resourceExhausted, - message: "Server worker busy" - )) - return - } - self.runServerBody(context: context, serverConfig: config) - } - - /// Gathers stats and returns them to the driver process. - private func handleServerMarkRequested( - context: StreamingResponseCallContext, - mark: Grpc_Testing_Mark - ) { - context.logger.info("server mark requested") - guard let runningServer = self.runningServer else { - context.logger.error("server not running") - context.statusPromise - .fail(GRPCStatus( - code: GRPCStatus.Code.failedPrecondition, - message: "Server not running" - )) - return - } - runningServer.sendStatus(reset: mark.reset, context: context) - } - - /// Handle a message from the driver asking this server function to stop running. - private func handleServerEnd(context: StreamingResponseCallContext) { - context.logger.info("runServer stream ended.") - if let runningServer = self.runningServer { - self.runningServer = nil - let shutdownFuture = runningServer.shutdown(callbackLoop: context.eventLoop) - shutdownFuture.map { () -> GRPCStatus in - GRPCStatus(code: .ok, message: nil) - }.cascade(to: context.statusPromise) - } else { - context.statusPromise.succeed(.ok) - } - } - - // MARK: Create Server - - /// Start a server running of the requested type. - private func runServerBody( - context: StreamingResponseCallContext, - serverConfig: Grpc_Testing_ServerConfig - ) { - var serverConfig = serverConfig - self.serverPortOverride.map { serverConfig.port = Int32($0) } - - do { - self.runningServer = try NIOWorkerServiceImpl.createServer( - context: context, - config: serverConfig - ) - } catch { - context.statusPromise.fail(error) - } - } - - /// Create a server of the requested type. - private static func createServer( - context: StreamingResponseCallContext, - config: Grpc_Testing_ServerConfig - ) throws -> NIOQPSServer { - context.logger.info( - "Starting server", - metadata: ["type": .stringConvertible(config.serverType)] - ) - - switch config.serverType { - case .syncServer: - throw GRPCStatus(code: .unimplemented, message: "Server Type not implemented") - case .asyncServer: - let asyncServer = NIOQPSServerImpl( - config: config, - whenBound: { serverInfo in - var response = Grpc_Testing_ServerStatus() - response.cores = Int32(serverInfo.threadCount) - response.port = Int32(serverInfo.port) - _ = context.sendResponse(response) - } - ) - return asyncServer - case .asyncGenericServer: - throw GRPCStatus(code: .unimplemented, message: "Server Type not implemented") - case .otherServer: - throw GRPCStatus(code: .unimplemented, message: "Server Type not implemented") - case .callbackServer: - throw GRPCStatus(code: .unimplemented, message: "Server Type not implemented") - case .UNRECOGNIZED: - throw GRPCStatus(code: .invalidArgument, message: "Unrecognised server type") - } - } - - // MARK: Run Client - - /// Handle a message from the driver about operating as a client. - private func handleClientMessage( - context: StreamingResponseCallContext, - args: Grpc_Testing_ClientArgs - ) { - switch args.argtype { - case let .some(.setup(clientConfig)): - self.handleClientSetup(context: context, config: clientConfig) - case let .some(.mark(mark)): - // Capture stats - self.handleClientMarkRequested(context: context, mark: mark) - case .none: - () - } - } - - /// Setup a client as described by the message from the driver. - private func handleClientSetup( - context: StreamingResponseCallContext, - config: Grpc_Testing_ClientConfig - ) { - context.logger.info("client setup requested") - guard self.runningClient == nil else { - context.logger.error("client already running") - context.statusPromise - .fail(GRPCStatus( - code: GRPCStatus.Code.resourceExhausted, - message: "Client worker busy" - )) - return - } - self.runClientBody(context: context, clientConfig: config) - // Initial status is the default (in C++) - _ = context.sendResponse(Grpc_Testing_ClientStatus()) - } - - /// Captures stats and send back to driver process. - private func handleClientMarkRequested( - context: StreamingResponseCallContext, - mark: Grpc_Testing_Mark - ) { - context.logger.info("client mark requested") - guard let runningClient = self.runningClient else { - context.logger.error("client not running") - context.statusPromise - .fail(GRPCStatus( - code: GRPCStatus.Code.failedPrecondition, - message: "Client not running" - )) - return - } - runningClient.sendStatus(reset: mark.reset, context: context) - } - - /// Call when an end message has been received. - /// Causes the running client to shutdown. - private func handleClientEnd(context: StreamingResponseCallContext) { - context.logger.info("runClient ended") - // Shutdown - if let runningClient = self.runningClient { - self.runningClient = nil - let shutdownFuture = runningClient.shutdown(callbackLoop: context.eventLoop) - shutdownFuture.map { () in - GRPCStatus(code: .ok, message: nil) - }.cascade(to: context.statusPromise) - } else { - context.statusPromise.succeed(.ok) - } - } - - // MARK: Create Client - - /// Setup and run a client of the requested type. - private func runClientBody( - context: StreamingResponseCallContext, - clientConfig: Grpc_Testing_ClientConfig - ) { - do { - self.runningClient = try NIOWorkerServiceImpl.makeClient( - context: context, - clientConfig: clientConfig - ) - } catch { - context.statusPromise.fail(error) - } - } - - /// Create a client of the requested type. - private static func makeClient( - context: StreamingResponseCallContext, - clientConfig: Grpc_Testing_ClientConfig - ) throws -> NIOQPSClient { - switch clientConfig.clientType { - case .syncClient: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .asyncClient: - if let payloadConfig = clientConfig.payloadConfig.payload { - switch payloadConfig { - case .bytebufParams: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .simpleParams: - return try makeAsyncClient(config: clientConfig) - case .complexParams: - return try makeAsyncClient(config: clientConfig) - } - } else { - // If there are no parameters assume simple. - return try makeAsyncClient(config: clientConfig) - } - case .otherClient: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .callbackClient: - throw GRPCStatus(code: .unimplemented, message: "Client Type not implemented") - case .UNRECOGNIZED: - throw GRPCStatus(code: .invalidArgument, message: "Unrecognised client type") - } - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift deleted file mode 100644 index 1dd44e86b..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/QPSWorker.swift +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import Logging -import NIOCore -import NIOPosix - -/// Sets up and runs a worker service which listens for instructions on what tests to run. -/// Currently doesn't understand TLS for communication with the driver. -class QPSWorker { - private let driverPort: Int - private let serverPort: Int? - private let useAsync: Bool - - /// Initialise. - /// - parameters: - /// - driverPort: Port to listen for instructions on. - /// - serverPort: Possible override for the port the testing will actually occur on - usually supplied by the driver process. - init(driverPort: Int, serverPort: Int?, useAsync: Bool) { - self.driverPort = driverPort - self.serverPort = serverPort - self.useAsync = useAsync - } - - private let logger = Logger(label: "QPSWorker") - - private var eventLoopGroup: MultiThreadedEventLoopGroup? - private var server: EventLoopFuture? - private var workEndFuture: EventLoopFuture? - - /// Start up the server which listens for instructions from the driver. - /// - parameters: - /// - onQuit: Function to call when the driver has indicated that the server should exit. - func start(onQuit: @escaping () -> Void) { - precondition(self.eventLoopGroup == nil) - self.logger.info("Starting") - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.eventLoopGroup = eventLoopGroup - - let workerService: CallHandlerProvider - let workEndPromise: EventLoopPromise = eventLoopGroup.next().makePromise() - workEndPromise.futureResult.whenSuccess(onQuit) - if self.useAsync { - workerService = AsyncWorkerServiceImpl( - finishedPromise: workEndPromise, - serverPortOverride: self.serverPort - ) - } else { - workerService = NIOWorkerServiceImpl( - finishedPromise: workEndPromise, - serverPortOverride: self.serverPort - ) - } - - // Start the server. - self.logger.info("Binding to localhost", metadata: ["driverPort": "\(self.driverPort)"]) - self.server = Server.insecure(group: eventLoopGroup) - .withServiceProviders([workerService]) - .withLogger(Logger(label: "GRPC")) - .bind(host: "localhost", port: self.driverPort) - } - - /// Shutdown waiting for completion. - func syncShutdown() throws { - precondition(self.eventLoopGroup != nil) - self.logger.info("Stopping") - try self.eventLoopGroup?.syncShutdownGracefully() - self.logger.info("Stopped") - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ResourceUsage.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ResourceUsage.swift deleted file mode 100644 index aeb3e5102..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ResourceUsage.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -import Darwin -#elseif os(Linux) || os(FreeBSD) || os(Android) -import Glibc -#else -let badOS = { fatalError("unsupported OS") }() -#endif - -import Foundation - -extension TimeInterval { - init(_ value: timeval) { - self.init(Double(value.tv_sec) + Double(value.tv_usec) * 1e-9) - } -} - -/// Holder for CPU time consumed. -struct CPUTime { - /// Amount of user process time consumed. - var userTime: TimeInterval - /// Amount of system time consumed. - var systemTime: TimeInterval -} - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -fileprivate let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF -#elseif os(Linux) || os(FreeBSD) || os(Android) -fileprivate let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF.rawValue -#endif - -/// Get resource usage for this process. -/// - returns: The amount of CPU resource consumed. -func getResourceUsage() -> CPUTime { - var usage = rusage() - if getrusage(OUR_RUSAGE_SELF, &usage) == 0 { - return CPUTime( - userTime: TimeInterval(usage.ru_utime), - systemTime: TimeInterval(usage.ru_stime) - ) - } else { - return CPUTime(userTime: 0, systemTime: 0) - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerTypeExtensions.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerTypeExtensions.swift deleted file mode 100644 index 7c5c83276..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerTypeExtensions.swift +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -extension Grpc_Testing_ServerType: CustomStringConvertible { - /// Text descriptions for the server types. - public var description: String { - switch self { - case .syncServer: - return "syncServer" - case .asyncServer: - return "asyncServer" - case .asyncGenericServer: - return "asyncGenericServer" - case .otherServer: - return "otherServer" - case .callbackServer: - return "callbackServer" - case let .UNRECOGNIZED(value): - return "unrecognised\(value)" - } - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerUtilities.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerUtilities.swift deleted file mode 100644 index ec1c4382b..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/ServerUtilities.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -/// Convenient set of information about a server. -struct ServerInfo { - /// Number of threads. - var threadCount: Int - /// The port bound to. - var port: Int -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift deleted file mode 100644 index 5e33f3924..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/Stats.swift +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020, 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 BenchmarkUtils -import NIOConcurrencyHelpers - -/// Convenience holder for collected statistics. -struct Stats { - /// Latency statistics. - var latencies = Histogram() - /// Error status counts. - var statuses = StatusCounts() -} - -/// Stats with access controlled by a lock - -/// Needs locking rather than event loop hopping as the driver refuses to wait shutting -/// the connection immediately after the request. -/// Marked `@unchecked Sendable` since we control access to `data` via a Lock. -final class StatsWithLock: @unchecked Sendable { - private var data = Stats() - private let lock = Lock() - - /// Record a latency value into the stats. - /// - parameters: - /// - latency: The value to record. - func add(latency: Double) { - self.lock.withLockVoid { self.data.latencies.add(value: latency) } - } - - func add(latency: Nanoseconds) { - self.add(latency: Double(latency.value)) - } - - /// Copy the data out. - /// - parameters: - /// - reset: If the statistics should be reset after collection or not. - /// - returns: A copy of the statistics. - func copyData(reset: Bool) -> Stats { - return self.lock.withLock { - let result = self.data - if reset { - self.data = Stats() - } - return result - } - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/StatusCountsSerialisation.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/StatusCountsSerialisation.swift deleted file mode 100644 index f4630e0a3..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/StatusCountsSerialisation.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2020, 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 BenchmarkUtils - -extension StatusCounts { - /// Convert status count to a protobuf for sending to the driver process. - /// - returns: The protobuf message for sending. - public func toRequestResultCounts() -> [Grpc_Testing_RequestResultCount] { - return counts.map { key, value -> Grpc_Testing_RequestResultCount in - var grpc = Grpc_Testing_RequestResultCount() - grpc.count = value - grpc.statusCode = Int32(key) - return grpc - } - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/grpcTime.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/grpcTime.swift deleted file mode 100644 index cbe85b492..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/grpcTime.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020, 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 Foundation - -/// Get the current time. -/// - returns: The current time. -func grpcTimeNow() -> DispatchTime { - return DispatchTime.now() -} - -extension DispatchTime { - /// Subtraction between two DispatchTimes giving the result in Nanoseconds - static func - (_ a: DispatchTime, _ b: DispatchTime) -> Nanoseconds { - return Nanoseconds(value: a.uptimeNanoseconds - b.uptimeNanoseconds) - } -} - -/// A number of nanoseconds -struct Nanoseconds { - /// The actual number of nanoseconds - var value: UInt64 -} - -extension Nanoseconds { - /// Convert to a potentially fractional number of seconds. - func asSeconds() -> Double { - return Double(self.value) * 1e-9 - } -} - -extension Nanoseconds: CustomStringConvertible { - /// Description to aid debugging. - var description: String { - return "\(self.value) ns" - } -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift b/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift deleted file mode 100644 index cb9df44f3..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/Runtime/main.swift +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020, 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 ArgumentParser -import Lifecycle -import Logging - -/// Main entry point to the QPS worker application. -final class QPSWorkerApp: ParsableCommand { - @Option(name: .customLong("driver_port"), help: "Port for communication with driver.") - var driverPort: Int - - @Option(name: .customLong("server_port"), help: "Port for operation as a server.") - var serverPort: Int? - - @Flag - var useAsync: Bool = false - - /// Run the application and wait for completion to be signalled. - func run() throws { - let logger = Logger(label: "QPSWorker") - - assert({ - logger.warning("⚠️ WARNING: YOU ARE RUNNING IN DEBUG MODE ⚠️") - return true - }()) - - logger.info("Starting...") - - logger.info("Initializing the lifecycle container") - // This installs backtrace. - let lifecycle = ServiceLifecycle() - - logger.info("Initializing QPSWorker - useAsync: \(self.useAsync)") - let qpsWorker = QPSWorker( - driverPort: self.driverPort, - serverPort: self.serverPort, - useAsync: self.useAsync - ) - - qpsWorker.start { - lifecycle.shutdown() - } - - lifecycle.registerShutdown(label: "QPSWorker", .sync { - try qpsWorker.syncShutdown() - }) - - lifecycle.start { error in - // Start completion handler. - // if a startup error occurred you can capture it here - if let error = error { - logger.error("failed starting \(self) ☠️: \(error)") - } else { - logger.info("\(self) started successfully 🚀") - } - } - - lifecycle.wait() - - logger.info("Worker has finished.") - } -} - -QPSWorkerApp.main() diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json b/Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json deleted file mode 100644 index 3019862f0..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/grpc-swift-config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "invocations": [ - { - "protoFiles": [ - "Model/benchmark_service.proto", - "Model/worker_service.proto", - ], - "visibility": "public" - } - ] -} diff --git a/Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json b/Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json deleted file mode 100644 index e7f07d3e0..000000000 --- a/Performance/QPSBenchmark/Sources/QPSBenchmark/swift-protobuf-config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "invocations": [ - { - "protoFiles": [ - "Model/messages.proto", - "Model/control.proto", - "Model/core_stats.proto", - "Model/payloads.proto", - "Model/stats.proto" - ], - "visibility": "public" - } - ] -} diff --git a/Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/HistogramTests.swift b/Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/HistogramTests.swift deleted file mode 100644 index 7eabfb1b7..000000000 --- a/Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/HistogramTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -@testable import BenchmarkUtils -import XCTest - -class HistogramTests: XCTestCase { - func testStats() { - var histogram = Histogram() - histogram.add(value: 1) - histogram.add(value: 2) - histogram.add(value: 3) - - XCTAssertEqual(histogram.countOfValuesSeen, 3) - XCTAssertEqual(histogram.maxSeen, 3) - XCTAssertEqual(histogram.minSeen, 1) - XCTAssertEqual(histogram.sum, 6) - XCTAssertEqual(histogram.sumOfSquares, 14) - } - - func testBuckets() { - var histogram = Histogram() - histogram.add(value: 1) - histogram.add(value: 1) - histogram.add(value: 3) - - var twoSeen = false - var oneSeen = false - for bucket in histogram.buckets { - switch bucket { - case 0: - break - case 1: - XCTAssertFalse(oneSeen) - oneSeen = true - case 2: - XCTAssertFalse(twoSeen) - twoSeen = true - default: - XCTFail() - } - } - XCTAssertTrue(oneSeen) - XCTAssertTrue(twoSeen) - } - - func testMerge() { - var histogram = Histogram() - histogram.add(value: 1) - histogram.add(value: 2) - histogram.add(value: 3) - - let histogram2 = Histogram() - histogram.add(value: 1) - histogram.add(value: 1) - histogram.add(value: 3) - - XCTAssertNoThrow(try histogram.merge(source: histogram2)) - - XCTAssertEqual(histogram.countOfValuesSeen, 6) - XCTAssertEqual(histogram.maxSeen, 3) - XCTAssertEqual(histogram.minSeen, 1) - XCTAssertEqual(histogram.sum, 11) - XCTAssertEqual(histogram.sumOfSquares, 25) - - var threeSeen = false - var twoSeen = false - var oneSeen = false - for bucket in histogram.buckets { - switch bucket { - case 0: - break - case 1: - XCTAssertFalse(oneSeen) - oneSeen = true - case 2: - XCTAssertFalse(twoSeen) - twoSeen = true - case 3: - XCTAssertFalse(threeSeen) - threeSeen = true - default: - XCTFail() - } - } - XCTAssertTrue(oneSeen) - XCTAssertTrue(twoSeen) - XCTAssertTrue(threeSeen) - } -} diff --git a/Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/StatusCountsTests.swift b/Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/StatusCountsTests.swift deleted file mode 100644 index a2bea6f4d..000000000 --- a/Performance/QPSBenchmark/Tests/BenchmarkUtilsTests/StatusCountsTests.swift +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -@testable import BenchmarkUtils -import GRPC -import XCTest - -class StatusCountsTests: XCTestCase { - func testIgnoreOK() { - var statusCounts = StatusCounts() - statusCounts.add(status: .ok) - XCTAssertEqual(statusCounts.counts.count, 0) - } - - func testMessageBuilding() { - var statusCounts = StatusCounts() - statusCounts.add(status: .aborted) - statusCounts.add(status: .aborted) - statusCounts.add(status: .alreadyExists) - - let counts = statusCounts.counts - XCTAssertEqual(counts.count, 2) - for stat in counts { - switch stat.key { - case GRPCStatus.Code.aborted.rawValue: - XCTAssertEqual(stat.value, 2) - case GRPCStatus.Code.alreadyExists.rawValue: - XCTAssertEqual(stat.value, 1) - default: - XCTFail() - } - } - } - - func testMergeEmpty() { - var statusCounts = StatusCounts() - statusCounts.add(status: .aborted) - statusCounts.add(status: .aborted) - statusCounts.add(status: .alreadyExists) - - let otherCounts = StatusCounts() - - statusCounts.merge(source: otherCounts) - - let counts = statusCounts.counts - XCTAssertEqual(counts.count, 2) - for stat in counts { - switch stat.key { - case GRPCStatus.Code.aborted.rawValue: - XCTAssertEqual(stat.value, 2) - case GRPCStatus.Code.alreadyExists.rawValue: - XCTAssertEqual(stat.value, 1) - default: - XCTFail() - } - } - } - - func testMergeToEmpty() { - var statusCounts = StatusCounts() - - var otherCounts = StatusCounts() - otherCounts.add(status: .aborted) - otherCounts.add(status: .aborted) - otherCounts.add(status: .alreadyExists) - - statusCounts.merge(source: otherCounts) - - let counts = statusCounts.counts - XCTAssertEqual(counts.count, 2) - for stat in counts { - switch stat.key { - case GRPCStatus.Code.aborted.rawValue: - XCTAssertEqual(stat.value, 2) - case GRPCStatus.Code.alreadyExists.rawValue: - XCTAssertEqual(stat.value, 1) - default: - XCTFail() - } - } - } - - func testMerge() { - var statusCounts = StatusCounts() - statusCounts.add(status: .aborted) - statusCounts.add(status: .aborted) - statusCounts.add(status: .alreadyExists) - - var otherCounts = StatusCounts() - otherCounts.add(status: .alreadyExists) - otherCounts.add(status: .dataLoss) - - statusCounts.merge(source: otherCounts) - - let counts = statusCounts.counts - XCTAssertEqual(counts.count, 3) - for stat in counts { - switch stat.key { - case GRPCStatus.Code.aborted.rawValue: - XCTAssertEqual(stat.value, 2) - case GRPCStatus.Code.alreadyExists.rawValue: - XCTAssertEqual(stat.value, 2) - case GRPCStatus.Code.dataLoss.rawValue: - XCTAssertEqual(stat.value, 1) - default: - XCTFail() - } - } - } -} diff --git a/Performance/QPSBenchmark/scenarios/bidirectional-ping-pong-1-connection.json b/Performance/QPSBenchmark/scenarios/bidirectional-ping-pong-1-connection.json deleted file mode 100644 index f0eb81e31..000000000 --- a/Performance/QPSBenchmark/scenarios/bidirectional-ping-pong-1-connection.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "scenarios": [ - { - "name": "swift_protobuf_async_streaming_ping_pong_insecure", - "warmup_seconds": 5, - "benchmark_seconds": 30, - "num_servers": 1, - "server_config": { - "async_server_threads": 1, - "channel_args": [ - { - "str_value": "latency", - "name": "grpc.optimization_target" - }, - { - "int_value": 1, - "name": "grpc.minimal_stack" - } - ], - "server_type": "ASYNC_SERVER", - "security_params": null, - "threads_per_cq": 0, - "server_processes": 0 - }, - "client_config": { - "security_params": null, - "channel_args": [ - { - "str_value": "latency", - "name": "grpc.optimization_target" - }, - { - "int_value": 1, - "name": "grpc.minimal_stack" - } - ], - "async_client_threads": 1, - "outstanding_rpcs_per_channel": 1, - "rpc_type": "STREAMING", - "payload_config": { - "simple_params": { - "resp_size": 0, - "req_size": 0 - } - }, - "client_channels": 1, - "threads_per_cq": 0, - "load_params": { - "closed_loop": {} - }, - "client_type": "ASYNC_CLIENT", - "histogram_params": { - "max_possible": 60000000000, - "resolution": 0.01 - }, - "client_processes": 0 - }, - "num_clients": 1 - } - ] -} diff --git a/Performance/QPSBenchmark/scenarios/unary-1-connection.json b/Performance/QPSBenchmark/scenarios/unary-1-connection.json deleted file mode 100644 index bb954a0ab..000000000 --- a/Performance/QPSBenchmark/scenarios/unary-1-connection.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "scenarios": [ - { - "name": "swift_protobuf_async_unary_qps_unconstrained_insecure", - "warmup_seconds": 5, - "benchmark_seconds": 30, - "num_servers": 1, - "server_config": { - "async_server_threads": 0, - "channel_args": [ - { - "str_value": "throughput", - "name": "grpc.optimization_target" - } - ], - "server_type": "ASYNC_SERVER", - "security_params": null, - "threads_per_cq": 0, - "server_processes": 0 - }, - "client_config": { - "security_params": null, - "channel_args": [ - { - "str_value": "throughput", - "name": "grpc.optimization_target" - } - ], - "async_client_threads": 0, - "outstanding_rpcs_per_channel": 100, - "rpc_type": "UNARY", - "payload_config": { - "simple_params": { - "resp_size": 0, - "req_size": 0 - } - }, - "client_channels": 1, - "threads_per_cq": 0, - "load_params": { - "closed_loop": {} - }, - "client_type": "ASYNC_CLIENT", - "histogram_params": { - "max_possible": 60000000000, - "resolution": 0.01 - }, - "client_processes": 0 - }, - "num_clients": 0 - } - ] -} diff --git a/Performance/QPSBenchmark/scenarios/unary-unconstrained.json b/Performance/QPSBenchmark/scenarios/unary-unconstrained.json deleted file mode 100644 index 93e47257f..000000000 --- a/Performance/QPSBenchmark/scenarios/unary-unconstrained.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "scenarios": [ - { - "name": "swift_protobuf_async_unary_qps_unconstrained_insecure", - "warmup_seconds": 5, - "benchmark_seconds": 30, - "num_servers": 1, - "server_config": { - "async_server_threads": 0, - "channel_args": [ - { - "str_value": "throughput", - "name": "grpc.optimization_target" - } - ], - "server_type": "ASYNC_SERVER", - "security_params": null, - "threads_per_cq": 0, - "server_processes": 0 - }, - "client_config": { - "security_params": null, - "channel_args": [ - { - "str_value": "throughput", - "name": "grpc.optimization_target" - } - ], - "async_client_threads": 0, - "outstanding_rpcs_per_channel": 100, - "rpc_type": "UNARY", - "payload_config": { - "simple_params": { - "resp_size": 0, - "req_size": 0 - } - }, - "client_channels": 64, - "threads_per_cq": 0, - "load_params": { - "closed_loop": {} - }, - "client_type": "ASYNC_CLIENT", - "histogram_params": { - "max_possible": 60000000000, - "resolution": 0.01 - }, - "client_processes": 0 - }, - "num_clients": 0 - } - ] -} diff --git a/Performance/allocations/test-allocation-counts.sh b/Performance/allocations/test-allocation-counts.sh deleted file mode 100755 index de67782ea..000000000 --- a/Performance/allocations/test-allocation-counts.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -# 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. - -# This script was adapted from SwiftNIO's test_01_allocation_counts.sh. The -# license for the original work is reproduced below. See NOTICES.txt for more. - -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftNIO open source project -## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftNIO project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -tmp="/tmp" - -source "$here/test-utils.sh" - -all_tests=() -for file in "$here/tests/"test_*.swift; do - # Extract the "TESTNAME" from "test_TESTNAME.swift" - test_name=$(basename "$file") - test_name=${test_name#test_*} - test_name=${test_name%*.swift} - all_tests+=( "$test_name" ) -done - -# Run all the tests. -"$here/tests/run-allocation-counter-tests.sh" -t "$tmp" | tee "$tmp/output" - -# Dump some output from each, check for allocations. -for test in "${all_tests[@]}"; do - while read -r test_case; do - test_case=${test_case#test_*} - total_allocations=$(grep "^test_$test_case.total_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g') - not_freed_allocations=$(grep "^test_$test_case.remaining_allocations:" "$tmp/output" | cut -d: -f2 | sed 's/ //g') - max_allowed_env_name="MAX_ALLOCS_ALLOWED_$test_case" - - info "$test_case: allocations not freed: $not_freed_allocations" - info "$test_case: total number of mallocs: $total_allocations" - - assert_less_than "$not_freed_allocations" 5 # allow some slack - assert_greater_than "$not_freed_allocations" -5 # allow some slack - if [[ -z "${!max_allowed_env_name+x}" ]]; then - if [[ -z "${!max_allowed_env_name+x}" ]]; then - warn "no reference number of allocations set (set to \$$max_allowed_env_name)" - warn "to set current number:" - warn " export $max_allowed_env_name=$total_allocations" - fi - else - max_allowed=${!max_allowed_env_name} - assert_less_than_or_equal "$total_allocations" "$max_allowed" - assert_greater_than "$total_allocations" "$(( max_allowed - 1000))" - fi - done < <(grep "^test_$test[^\W]*.total_allocations:" "$tmp/output" | cut -d: -f1 | cut -d. -f1 | sort | uniq) -done diff --git a/Performance/allocations/test-utils.sh b/Performance/allocations/test-utils.sh deleted file mode 100755 index 70688f2cf..000000000 --- a/Performance/allocations/test-utils.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -# 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. - -# This script contains part of SwiftNIO's test_functions.sh script. The license -# for the original work is reproduced below. See NOTICES.txt for more. - -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftNIO open source project -## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftNIO project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -function fail() { - echo >&2 "FAILURE: $*" - false -} - -function assert_less_than() { - if [[ ! "$1" -lt "$2" ]]; then - fail "assertion '$1' < '$2' failed" - fi -} - -function assert_less_than_or_equal() { - if [[ ! "$1" -le "$2" ]]; then - fail "assertion '$1' <= '$2' failed" - fi -} - -function assert_greater_than() { - if [[ ! "$1" -gt "$2" ]]; then - fail "assertion '$1' > '$2' failed" - fi -} - -g_has_previously_infoed=false - -function info() { - if ! $g_has_previously_infoed; then - echo || true # echo an extra newline so it looks better - g_has_previously_infoed=true - fi - echo "info: $*" || true -} - -function warn() { - echo "warning: $*" -} diff --git a/Performance/allocations/tests/run-allocation-counter-tests.sh b/Performance/allocations/tests/run-allocation-counter-tests.sh deleted file mode 100755 index 049aeb573..000000000 --- a/Performance/allocations/tests/run-allocation-counter-tests.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -# 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. - -# This script was adapted from SwiftNIO's 'run-nio-alloc-counter-tests.sh' -# script. The license for the original work is reproduced below. See NOTICES.txt -# for more. - -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftNIO open source project -## -## Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftNIO project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -tmp_dir="/tmp" - -while getopts "t:" opt; do - case "$opt" in - t) - tmp_dir="$OPTARG" - ;; - *) - exit 1 - ;; - esac -done - -nio_checkout=$(mktemp -d "$tmp_dir/.swift-nio_XXXXXX") -( -cd "$nio_checkout" -git clone --depth 1 https://github.com/apple/swift-nio -) - -shift $((OPTIND-1)) - -tests_to_run=("$here"/test_*.swift) - -if [[ $# -gt 0 ]]; then - tests_to_run=("$@") -fi - -# We symlink in a bunch of components from the GRPCPerformanceTests target to -# avoid duplicating a bunch of code. -"$nio_checkout/swift-nio/IntegrationTests/allocation-counter-tests-framework/run-allocation-counter.sh" \ - -p "$here/../../.." \ - -m GRPC \ - -t "$tmp_dir" \ - -s "$here/shared/Common.swift" \ - -s "$here/shared/Benchmark.swift" \ - -s "$here/shared/echo.pb.swift" \ - -s "$here/shared/echo.grpc.swift" \ - -s "$here/shared/MinimalEchoProvider.swift" \ - -s "$here/shared/EmbeddedServer.swift" \ - "${tests_to_run[@]}" diff --git a/Performance/allocations/tests/shared/Benchmark.swift b/Performance/allocations/tests/shared/Benchmark.swift deleted file mode 120000 index 56c05abe6..000000000 --- a/Performance/allocations/tests/shared/Benchmark.swift +++ /dev/null @@ -1 +0,0 @@ -../../../../Sources/GRPCPerformanceTests/Benchmark.swift \ No newline at end of file diff --git a/Performance/allocations/tests/shared/Common.swift b/Performance/allocations/tests/shared/Common.swift deleted file mode 100644 index 02a65b93c..000000000 --- a/Performance/allocations/tests/shared/Common.swift +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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 GRPC -import NIO - -func makeEchoServer( - group: EventLoopGroup, - host: String = "127.0.0.1", - port: Int = 0, - interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil -) -> EventLoopFuture { - return Server.insecure(group: group) - .withServiceProviders([MinimalEchoProvider(interceptors: interceptors)]) - .bind(host: host, port: port) -} - -func makeClientConnection( - group: EventLoopGroup, - host: String = "127.0.0.1", - port: Int -) -> ClientConnection { - return ClientConnection.insecure(group: group) - .connect(host: host, port: port) -} - -func makeEchoClientInterceptors(count: Int) -> Echo_EchoClientInterceptorFactoryProtocol? { - let factory = EchoClientInterceptors() - for _ in 0 ..< count { - factory.register { NoOpEchoClientInterceptor() } - } - return factory -} - -func makeEchoServerInterceptors(count: Int) -> Echo_EchoServerInterceptorFactoryProtocol? { - let factory = EchoServerInterceptors() - for _ in 0 ..< count { - factory.register { NoOpEchoServerInterceptor() } - } - return factory -} - -final class EchoClientInterceptors: Echo_EchoClientInterceptorFactoryProtocol { - internal typealias Factory = () -> ClientInterceptor - private var factories: [Factory] = [] - - internal init(_ factories: Factory...) { - self.factories = factories - } - - internal func register(_ factory: @escaping Factory) { - self.factories.append(factory) - } - - private func makeInterceptors() -> [ClientInterceptor] { - return self.factories.map { $0() } - } - - func makeGetInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } - - func makeExpandInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } - - func makeCollectInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } - - func makeUpdateInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } -} - -internal final class EchoServerInterceptors: Echo_EchoServerInterceptorFactoryProtocol { - internal typealias Factory = () -> ServerInterceptor - private var factories: [Factory] = [] - - internal init(_ factories: Factory...) { - self.factories = factories - } - - internal func register(_ factory: @escaping Factory) { - self.factories.append(factory) - } - - private func makeInterceptors() -> [ServerInterceptor] { - return self.factories.map { $0() } - } - - func makeGetInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } - - func makeExpandInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } - - func makeCollectInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } - - func makeUpdateInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } -} - -final class NoOpEchoClientInterceptor: ClientInterceptor {} -final class NoOpEchoServerInterceptor: ServerInterceptor {} diff --git a/Performance/allocations/tests/shared/EmbeddedServer.swift b/Performance/allocations/tests/shared/EmbeddedServer.swift deleted file mode 120000 index 9c1d98aec..000000000 --- a/Performance/allocations/tests/shared/EmbeddedServer.swift +++ /dev/null @@ -1 +0,0 @@ -../../../../Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift \ No newline at end of file diff --git a/Performance/allocations/tests/shared/MinimalEchoProvider.swift b/Performance/allocations/tests/shared/MinimalEchoProvider.swift deleted file mode 120000 index 0ad07f378..000000000 --- a/Performance/allocations/tests/shared/MinimalEchoProvider.swift +++ /dev/null @@ -1 +0,0 @@ -../../../../Sources/GRPCPerformanceTests/Benchmarks/MinimalEchoProvider.swift \ No newline at end of file diff --git a/Performance/allocations/tests/shared/echo.grpc.swift b/Performance/allocations/tests/shared/echo.grpc.swift deleted file mode 120000 index 2c0efcf5c..000000000 --- a/Performance/allocations/tests/shared/echo.grpc.swift +++ /dev/null @@ -1 +0,0 @@ -../../../../Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift \ No newline at end of file diff --git a/Performance/allocations/tests/shared/echo.pb.swift b/Performance/allocations/tests/shared/echo.pb.swift deleted file mode 120000 index 64ef8232c..000000000 --- a/Performance/allocations/tests/shared/echo.pb.swift +++ /dev/null @@ -1 +0,0 @@ -../../../../Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift \ No newline at end of file diff --git a/Performance/allocations/tests/test_bidi_1k_rpcs.swift b/Performance/allocations/tests/test_bidi_1k_rpcs.swift deleted file mode 100644 index 589ef0d8e..000000000 --- a/Performance/allocations/tests/test_bidi_1k_rpcs.swift +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 Dispatch -import GRPC -import NIO - -class BidiPingPongBenchmark: Benchmark { - let rpcs: Int - let requests: Int - let request: Echo_EchoRequest - - private var group: EventLoopGroup! - private var server: Server! - private var client: ClientConnection! - - init(rpcs: Int, requests: Int, request: String) { - self.rpcs = rpcs - self.requests = requests - self.request = .with { $0.text = request } - } - - func setUp() throws { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.server = try makeEchoServer(group: self.group).wait() - self.client = makeClientConnection( - group: self.group, - port: self.server.channel.localAddress!.port! - ) - } - - func tearDown() throws { - try self.client.close().wait() - try self.server.close().wait() - try self.group.syncShutdownGracefully() - } - - func run() throws -> Int { - let echo = Echo_EchoNIOClient(channel: self.client) - var statusCodeSum = 0 - - // We'll use this semaphore to make sure we're ping-ponging request-response - // pairs on the RPC. Doing so makes the number of allocations much more - // stable. - let waiter = DispatchSemaphore(value: 1) - - for _ in 0 ..< self.rpcs { - let update = echo.update { _ in - waiter.signal() - } - - for _ in 0 ..< self.requests { - waiter.wait() - update.sendMessage(self.request, promise: nil) - } - waiter.wait() - update.sendEnd(promise: nil) - - let status = try update.status.wait() - statusCodeSum += status.code.rawValue - waiter.signal() - } - - return statusCodeSum - } -} - -func run(identifier: String) { - measure(identifier: identifier + "_10_requests") { - let benchmark = BidiPingPongBenchmark(rpcs: 1000, requests: 10, request: "") - return try! benchmark.runOnce() - } - - measure(identifier: identifier + "_1_request") { - let benchmark = BidiPingPongBenchmark(rpcs: 1000, requests: 1, request: "") - return try! benchmark.runOnce() - } -} diff --git a/Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_10_small_requests.swift b/Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_10_small_requests.swift deleted file mode 100644 index cca8d4e24..000000000 --- a/Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_10_small_requests.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ -func run(identifier: String) { - measure(identifier: identifier) { - let benchmark = EmbeddedServerChildChannelBenchmark( - mode: .bidirectional(rpcs: 1000, requestsPerRPC: 10), - text: "" - ) - return try! benchmark.runOnce() - } -} diff --git a/Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_1_small_request.swift b/Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_1_small_request.swift deleted file mode 100644 index 98ba38f2f..000000000 --- a/Performance/allocations/tests/test_embedded_server_bidi_1k_rpcs_1_small_request.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ -func run(identifier: String) { - measure(identifier: identifier) { - let benchmark = EmbeddedServerChildChannelBenchmark( - mode: .bidirectional(rpcs: 1000, requestsPerRPC: 1), - text: "" - ) - return try! benchmark.runOnce() - } -} diff --git a/Performance/allocations/tests/test_embedded_server_unary_1k_rpcs_1_small_request.swift b/Performance/allocations/tests/test_embedded_server_unary_1k_rpcs_1_small_request.swift deleted file mode 100644 index c9a80e157..000000000 --- a/Performance/allocations/tests/test_embedded_server_unary_1k_rpcs_1_small_request.swift +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ -func run(identifier: String) { - measure(identifier: identifier) { - let benchmark = EmbeddedServerChildChannelBenchmark(mode: .unary(rpcs: 1000), text: "") - return try! benchmark.runOnce() - } -} diff --git a/Performance/allocations/tests/test_unary_1k_ping_pong.swift b/Performance/allocations/tests/test_unary_1k_ping_pong.swift deleted file mode 100644 index 89d4e4895..000000000 --- a/Performance/allocations/tests/test_unary_1k_ping_pong.swift +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 GRPC -import NIO - -class UnaryPingPongBenchmark: Benchmark { - let rpcs: Int - let request: Echo_EchoRequest - - private var group: EventLoopGroup! - private var server: Server! - private var client: ClientConnection! - private let clientInterceptors: Echo_EchoClientInterceptorFactoryProtocol? - private let serverInterceptors: Echo_EchoServerInterceptorFactoryProtocol? - - init( - rpcs: Int, - request: String, - clientInterceptors: Int = 0, - serverInterceptors: Int = 0 - ) { - self.rpcs = rpcs - self.request = .with { $0.text = request } - self.clientInterceptors = clientInterceptors > 0 - ? makeEchoClientInterceptors(count: clientInterceptors) - : nil - self.serverInterceptors = serverInterceptors > 0 - ? makeEchoServerInterceptors(count: serverInterceptors) - : nil - } - - func setUp() throws { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.server = try makeEchoServer( - group: self.group, - interceptors: self.serverInterceptors - ).wait() - self.client = makeClientConnection( - group: self.group, - port: self.server.channel.localAddress!.port! - ) - } - - func tearDown() throws { - try self.client.close().wait() - try self.server.close().wait() - try self.group.syncShutdownGracefully() - } - - func run() throws -> Int { - let echo = Echo_EchoClient(channel: self.client, interceptors: self.clientInterceptors) - var responseLength = 0 - - for _ in 0 ..< self.rpcs { - let get = echo.get(self.request) - let response = try get.response.wait() - responseLength += response.text.count - } - - return responseLength - } -} - -func run(identifier: String) { - measure(identifier: identifier) { - let benchmark = UnaryPingPongBenchmark(rpcs: 1000, request: "") - return try! benchmark.runOnce() - } - - measure(identifier: identifier + "_interceptors_server") { - let benchmark = UnaryPingPongBenchmark(rpcs: 1000, request: "", serverInterceptors: 5) - return try! benchmark.runOnce() - } - - measure(identifier: identifier + "_interceptors_client") { - let benchmark = UnaryPingPongBenchmark(rpcs: 1000, request: "", clientInterceptors: 5) - return try! benchmark.runOnce() - } -} diff --git a/Plugins/GRPCSwiftPlugin/plugin.swift b/Plugins/GRPCSwiftPlugin/plugin.swift deleted file mode 100644 index 107b3f787..000000000 --- a/Plugins/GRPCSwiftPlugin/plugin.swift +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright 2022, 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 Foundation -import PackagePlugin - -@main -struct GRPCSwiftPlugin { - /// Errors thrown by the `GRPCSwiftPlugin` - enum PluginError: Error, CustomStringConvertible { - /// Indicates that the target where the plugin was applied to was not `SourceModuleTarget`. - case invalidTarget(String) - /// Indicates that the file extension of an input file was not `.proto`. - case invalidInputFileExtension(String) - /// Indicates that there was no configuration file at the required location. - case noConfigFound(String) - - var description: String { - switch self { - case let .invalidTarget(target): - return "Expected a SwiftSourceModuleTarget but got '\(target)'." - case let .invalidInputFileExtension(path): - return "The input file '\(path)' does not have a '.proto' extension." - case let .noConfigFound(path): - return """ - No configuration file found named '\(path)'. The file must not be listed in the \ - 'exclude:' argument for the target in Package.swift. - """ - } - } - } - - /// The configuration of the plugin. - struct Configuration: Codable { - /// Encapsulates a single invocation of protoc. - struct Invocation: Codable { - /// The visibility of the generated files. - enum Visibility: String, Codable { - /// The generated files should have `internal` access level. - case `internal` - /// The generated files should have `public` access level. - case `public` - /// The generated files should have `package` access level. - case `package` - } - - /// An array of paths to `.proto` files for this invocation. - var protoFiles: [String] - /// The visibility of the generated files. - var visibility: Visibility? - /// Whether server code is generated. - var server: Bool? - /// Whether client code is generated. - var client: Bool? - /// Whether reflection data is generated. - var reflectionData: Bool? - /// Determines whether the casing of generated function names is kept. - var keepMethodCasing: Bool? - /// Whether the invocation is for `grpc-swift` v2. - var _V2: Bool? - } - - /// Specify the directory in which to search for - /// imports. May be specified multiple times; - /// directories will be searched in order. - /// The target source directory is always appended - /// to the import paths. - var importPaths: [String]? - - /// The path to the `protoc` binary. - /// - /// If this is not set, SPM will try to find the tool itself. - var protocPath: String? - - /// A list of invocations of `protoc` with the `GRPCSwiftPlugin`. - var invocations: [Invocation] - } - - static let configurationFileName = "grpc-swift-config.json" - - /// Create build commands for the given arguments - /// - Parameters: - /// - pluginWorkDirectory: The path of a writable directory into which the plugin or the build - /// commands it constructs can write anything it wants. - /// - sourceFiles: The input files that are associated with the target. - /// - tool: The tool method from the context. - /// - Returns: The build commands configured based on the arguments. - func createBuildCommands( - pluginWorkDirectory: PathLike, - sourceFiles: FileList, - tool: (String) throws -> PackagePlugin.PluginContext.Tool - ) throws -> [Command] { - let maybeConfigFile = sourceFiles.map { PathLike($0) }.first { - $0.lastComponent == Self.configurationFileName - } - - guard let configurationFilePath = maybeConfigFile else { - throw PluginError.noConfigFound(Self.configurationFileName) - } - - let data = try Data(contentsOf: URL(configurationFilePath)) - let configuration = try JSONDecoder().decode(Configuration.self, from: data) - - try self.validateConfiguration(configuration) - - let targetDirectory = configurationFilePath.removingLastComponent() - var importPaths: [PathLike] = [targetDirectory] - if let configuredImportPaths = configuration.importPaths { - importPaths.append(contentsOf: configuredImportPaths.map { PathLike($0) }) - } - - // We need to find the path of protoc and protoc-gen-grpc-swift - let protocPath: PathLike - if let configuredProtocPath = configuration.protocPath { - protocPath = PathLike(configuredProtocPath) - } else if let environmentPath = ProcessInfo.processInfo.environment["PROTOC_PATH"] { - // The user set the env variable, so let's take that - protocPath = PathLike(environmentPath) - } else { - // The user didn't set anything so let's try see if SPM can find a binary for us - protocPath = try PathLike(tool("protoc")) - } - - let protocGenGRPCSwiftPath = try PathLike(tool("protoc-gen-grpc-swift")) - - return configuration.invocations.map { invocation in - self.invokeProtoc( - directory: targetDirectory, - invocation: invocation, - protocPath: protocPath, - protocGenGRPCSwiftPath: protocGenGRPCSwiftPath, - outputDirectory: pluginWorkDirectory, - importPaths: importPaths - ) - } - } - - /// Invokes `protoc` with the given inputs - /// - /// - Parameters: - /// - directory: The plugin's target directory. - /// - invocation: The `protoc` invocation. - /// - protocPath: The path to the `protoc` binary. - /// - protocGenSwiftPath: The path to the `protoc-gen-swift` binary. - /// - outputDirectory: The output directory for the generated files. - /// - importPaths: List of paths to pass with "-I " to `protoc`. - /// - Returns: The build command configured based on the arguments - private func invokeProtoc( - directory: PathLike, - invocation: Configuration.Invocation, - protocPath: PathLike, - protocGenGRPCSwiftPath: PathLike, - outputDirectory: PathLike, - importPaths: [PathLike] - ) -> Command { - // Construct the `protoc` arguments. - var protocArgs = [ - "--plugin=protoc-gen-grpc-swift=\(protocGenGRPCSwiftPath)", - "--grpc-swift_out=\(outputDirectory)", - ] - - importPaths.forEach { path in - protocArgs.append("-I") - protocArgs.append("\(path)") - } - - if let visibility = invocation.visibility { - protocArgs.append("--grpc-swift_opt=Visibility=\(visibility.rawValue.capitalized)") - } - - if let generateServerCode = invocation.server { - protocArgs.append("--grpc-swift_opt=Server=\(generateServerCode)") - } - - if let generateClientCode = invocation.client { - protocArgs.append("--grpc-swift_opt=Client=\(generateClientCode)") - } - - if let generateReflectionData = invocation.reflectionData { - protocArgs.append("--grpc-swift_opt=ReflectionData=\(generateReflectionData)") - } - - if let keepMethodCasingOption = invocation.keepMethodCasing { - protocArgs.append("--grpc-swift_opt=KeepMethodCasing=\(keepMethodCasingOption)") - } - - if let v2 = invocation._V2 { - protocArgs.append("--grpc-swift_opt=_V2=\(v2)") - } - - var inputFiles = [PathLike]() - var outputFiles = [PathLike]() - - for var file in invocation.protoFiles { - // Append the file to the protoc args so that it is used for generating - protocArgs.append(file) - inputFiles.append(directory.appending(file)) - - // The name of the output file is based on the name of the input file. - // We validated in the beginning that every file has the suffix of .proto - // This means we can just drop the last 5 elements and append the new suffix - file.removeLast(5) - file.append("grpc.swift") - let protobufOutputPath = outputDirectory.appending(file) - - // Add the outputPath as an output file - outputFiles.append(protobufOutputPath) - - if invocation.reflectionData == true { - // Remove .swift extension and add .reflection extension - file.removeLast(5) - file.append("reflection") - let reflectionOutputPath = outputDirectory.appending(file) - outputFiles.append(reflectionOutputPath) - } - } - - // Construct the command. Specifying the input and output paths lets the build - // system know when to invoke the command. The output paths are passed on to - // the rule engine in the build system. - return Command.buildCommand( - displayName: "Generating gRPC Swift files from proto files", - executable: protocPath, - arguments: protocArgs, - inputFiles: inputFiles + [protocGenGRPCSwiftPath], - outputFiles: outputFiles - ) - } - - /// Validates the configuration file for various user errors. - private func validateConfiguration(_ configuration: Configuration) throws { - for invocation in configuration.invocations { - for protoFile in invocation.protoFiles { - if !protoFile.hasSuffix(".proto") { - throw PluginError.invalidInputFileExtension(protoFile) - } - } - } - } -} - -extension GRPCSwiftPlugin: BuildToolPlugin { - func createBuildCommands( - context: PluginContext, - target: Target - ) async throws -> [Command] { - guard let swiftTarget = target as? SwiftSourceModuleTarget else { - throw PluginError.invalidTarget("\(type(of: target))") - } - - #if compiler(<6.0) - let workDirectory = PathLike(context.pluginWorkDirectory) - #else - let workDirectory = PathLike(context.pluginWorkDirectoryURL) - #endif - - return try self.createBuildCommands( - pluginWorkDirectory: workDirectory, - sourceFiles: swiftTarget.sourceFiles, - tool: context.tool - ) - } -} - -// 'Path' was effectively deprecated in Swift 6 in favour of URL. ('Effectively' because all -// methods, properties, and conformances have been deprecated but the type hasn't.) This type wraps -// either depending on the compiler version. -struct PathLike: CustomStringConvertible { - #if compiler(<6.0) - typealias Value = Path - #else - typealias Value = URL - #endif - - private(set) var value: Value - - init(_ value: Value) { - self.value = value - } - - init(_ path: String) { - #if compiler(<6.0) - self.value = Path(path) - #else - self.value = URL(fileURLWithPath: path) - #endif - } - - init(_ element: FileList.Element) { - #if compiler(<6.0) - self.value = element.path - #else - self.value = element.url - #endif - } - - init(_ element: PluginContext.Tool) { - #if compiler(<6.0) - self.value = element.path - #else - self.value = element.url - #endif - } - - var description: String { - #if compiler(<6.0) - return String(describing: self.value) - #elseif canImport(Darwin) - return self.value.path() - #else - return self.value.path - #endif - } - - var lastComponent: String { - #if compiler(<6.0) - return self.value.lastComponent - #else - return self.value.lastPathComponent - #endif - } - - func removingLastComponent() -> Self { - var copy = self - #if compiler(<6.0) - copy.value = self.value.removingLastComponent() - #else - copy.value = self.value.deletingLastPathComponent() - #endif - return copy - } - - func appending(_ path: String) -> Self { - var copy = self - #if compiler(<6.0) - copy.value = self.value.appending(path) - #else - copy.value = self.value.appendingPathComponent(path) - #endif - return copy - } -} - -extension Command { - static func buildCommand( - displayName: String?, - executable: PathLike, - arguments: [String], - inputFiles: [PathLike], - outputFiles: [PathLike] - ) -> PackagePlugin.Command { - return Self.buildCommand( - displayName: displayName, - executable: executable.value, - arguments: arguments, - inputFiles: inputFiles.map { $0.value }, - outputFiles: outputFiles.map { $0.value } - ) - } -} - -extension URL { - init(_ pathLike: PathLike) { - #if compiler(<6.0) - self = URL(fileURLWithPath: "\(pathLike.value)") - #else - self = pathLike.value - #endif - } -} - -#if canImport(XcodeProjectPlugin) -import XcodeProjectPlugin - -extension GRPCSwiftPlugin: XcodeBuildToolPlugin { - func createBuildCommands( - context: XcodePluginContext, - target: XcodeTarget - ) throws -> [Command] { - #if compiler(<6.0) - let workDirectory = PathLike(context.pluginWorkDirectory) - #else - let workDirectory = PathLike(context.pluginWorkDirectoryURL) - #endif - - return try self.createBuildCommands( - pluginWorkDirectory: workDirectory, - sourceFiles: target.inputFiles, - tool: context.tool - ) - } -} -#endif diff --git a/Protos/generate.sh b/Protos/generate.sh deleted file mode 100755 index 5eb6aa313..000000000 --- a/Protos/generate.sh +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/bash -# -# 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. - -set -eu - -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -root="$here/.." -protoc=$(which protoc) - -# Build the protoc plugins. -swift build -c release --product protoc-gen-swift -swift build -c release --product protoc-gen-grpc-swift - -# Grab the plugin paths. -bin_path=$(swift build -c release --show-bin-path) -protoc_gen_swift="$bin_path/protoc-gen-swift" -protoc_generate_grpc_swift="$bin_path/protoc-gen-grpc-swift" - -# Generates gRPC by invoking protoc with the gRPC Swift plugin. -# Parameters: -# - $1: .proto file -# - $2: proto path -# - $3: output path -# - $4 onwards: options to forward to the plugin -function generate_grpc { - local proto=$1 - local args=("--plugin=$protoc_generate_grpc_swift" "--proto_path=${2}" "--grpc-swift_out=${3}") - - for option in "${@:4}"; do - args+=("--grpc-swift_opt=$option") - done - - invoke_protoc "${args[@]}" "$proto" -} - -# Generates messages by invoking protoc with the Swift plugin. -# Parameters: -# - $1: .proto file -# - $2: proto path -# - $3: output path -# - $4 onwards: options to forward to the plugin -function generate_message { - local proto=$1 - local args=("--plugin=$protoc_gen_swift" "--proto_path=$2" "--swift_out=$3") - - for option in "${@:4}"; do - args+=("--swift_opt=$option") - done - - invoke_protoc "${args[@]}" "$proto" -} - -function invoke_protoc { - # Setting -x when running the script produces a lot of output, instead boil - # just echo out the protoc invocations. - echo "$protoc" "$@" - "$protoc" "$@" -} - -#------------------------------------------------------------------------------ - -function generate_echo_v1_example { - local proto="$here/examples/echo/echo.proto" - local output="$root/Examples/v1/Echo/Model" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" "TestClient=true" -} - -function generate_echo_v2_example { - local proto="$here/examples/echo/echo.proto" - local output="$root/Examples/v2/echo/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" -} - -function generate_routeguide_v1_example { - local proto="$here/examples/route_guide/route_guide.proto" - local output="$root/Examples/v1/RouteGuide/Model" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" -} - -function generate_routeguide_v2_example { - local proto="$here/examples/route_guide/route_guide.proto" - local output="$root/Examples/v2/route-guide/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" -} - -function generate_helloworld_v1_example { - local proto="$here/upstream/grpc/examples/helloworld.proto" - local output="$root/Examples/v1/HelloWorld/Model" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Public" -} - -function generate_helloworld_v2_example { - local proto="$here/upstream/grpc/examples/helloworld.proto" - local output="$root/Examples/v2/hello-world/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "_V2=true" -} - -function generate_reflection_service { - local proto_v1="$here/upstream/grpc/reflection/v1/reflection.proto" - local output_v1="$root/Sources/GRPCReflectionService/v1" - - # Messages were accidentally leaked into public API, they shouldn't be but we - # can't undo that change until the next major version. - generate_message "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Public" - generate_grpc "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Internal" "Client=false" - - # Both protos have the same name so will generate Swift files with the same - # name. SwiftPM can't handle this so rename them. - mv "$output_v1/reflection.pb.swift" "$output_v1/reflection-v1.pb.swift" - mv "$output_v1/reflection.grpc.swift" "$output_v1/reflection-v1.grpc.swift" - - local proto_v1alpha="$here/upstream/grpc/reflection/v1alpha/reflection.proto" - local output_v1alpha="$root/Sources/GRPCReflectionService/v1Alpha" - - # Messages were accidentally leaked into public API, they shouldn't be but we - # can't undo that change until the next major version. - generate_message "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Public" - generate_grpc "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Internal" "Client=false" - - # Both protos have the same name so will generate Swift files with the same - # name. SwiftPM can't handle this so rename them. - mv "$output_v1alpha/reflection.pb.swift" "$output_v1alpha/reflection-v1alpha.pb.swift" - mv "$output_v1alpha/reflection.grpc.swift" "$output_v1alpha/reflection-v1alpha.grpc.swift" -} - -function generate_reflection_client_for_tests { - local proto_v1="$here/upstream/grpc/reflection/v1/reflection.proto" - local output_v1="$root/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1" - - generate_message "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Internal" - generate_grpc "$proto_v1" "$(dirname "$proto_v1")" "$output_v1" "Visibility=Internal" "Server=false" - - # Both protos have the same name so will generate Swift files with the same - # name. SwiftPM can't handle this so rename them. - mv "$output_v1/reflection.pb.swift" "$output_v1/reflection-v1.pb.swift" - mv "$output_v1/reflection.grpc.swift" "$output_v1/reflection-v1.grpc.swift" - - local proto_v1alpha="$here/upstream/grpc/reflection/v1alpha/reflection.proto" - local output_v1alpha="$root/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha" - - generate_message "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Internal" - generate_grpc "$proto_v1alpha" "$(dirname "$proto_v1alpha")" "$output_v1alpha" "Visibility=Internal" "Server=false" - - # Both protos have the same name so will generate Swift files with the same - # name. SwiftPM can't handle this so rename them. - mv "$output_v1alpha/reflection.pb.swift" "$output_v1alpha/reflection-v1alpha.pb.swift" - mv "$output_v1alpha/reflection.grpc.swift" "$output_v1alpha/reflection-v1alpha.grpc.swift" -} - -function generate_normalization_for_tests { - local proto="$here/tests/normalization/normalization.proto" - local output="$root/Tests/GRPCTests/Codegen/Normalization" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "KeepMethodCasing=true" -} - -function generate_echo_reflection_data_for_tests { - local proto="$here/examples/echo/echo.proto" - local output="$root/Tests/GRPCTests/Codegen/Serialization" - - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Client=false" "Server=false" "ReflectionData=true" -} - -function generate_reflection_data_example { - local protos=("$here/examples/echo/echo.proto" "$here/upstream/grpc/examples/helloworld.proto") - local output="$root/Examples/v1/ReflectionService/Generated" - - for proto in "${protos[@]}"; do - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Client=false" "Server=false" "ReflectionData=true" - done -} - -function generate_rpc_code_for_tests { - local protos=( - "$here/upstream/grpc/service_config/service_config.proto" - "$here/upstream/grpc/lookup/v1/rls.proto" - "$here/upstream/grpc/lookup/v1/rls_config.proto" - "$here/upstream/google/rpc/code.proto" - ) - local output="$root/Tests/GRPCCoreTests/Configuration/Generated" - - for proto in "${protos[@]}"; do - generate_message "$proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=DropPath" - done -} - -function generate_http2_transport_tests_service { - local proto="$here/tests/control/control.proto" - local output="$root/Tests/GRPCHTTP2TransportTests/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" "Client=true" "Server=true" "_V2=true" -} - -function generate_service_messages_interop_tests { - local protos=( - "$here/tests/interoperability/src/proto/grpc/testing/empty_service.proto" - "$here/tests/interoperability/src/proto/grpc/testing/empty.proto" - "$here/tests/interoperability/src/proto/grpc/testing/messages.proto" - "$here/tests/interoperability/src/proto/grpc/testing/test.proto" - ) - local output="$root/Sources/InteroperabilityTests/Generated" - - for proto in "${protos[@]}"; do - generate_message "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "FileNaming=DropPath" "UseAccessLevelOnImports=true" - generate_grpc "$proto" "$here/tests/interoperability" "$output" "Visibility=Public" "Server=true" "_V2=true" "FileNaming=DropPath" "UseAccessLevelOnImports=true" - done -} - -function generate_worker_service { - local protos=( - "$here/upstream/grpc/testing/payloads.proto" - "$here/upstream/grpc/testing/control.proto" - "$here/upstream/grpc/testing/messages.proto" - "$here/upstream/grpc/testing/stats.proto" - "$here/upstream/grpc/testing/benchmark_service.proto" - "$here/upstream/grpc/testing/worker_service.proto" - ) - local output="$root/Sources/performance-worker/Generated" - - generate_message "$here/upstream/grpc/core/stats.proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=PathToUnderscores" - - for proto in "${protos[@]}"; do - generate_message "$proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=PathToUnderscores" - if [ "$proto" == "$here/upstream/grpc/testing/worker_service.proto" ]; then - generate_grpc "$proto" "$here/upstream" "$output" "Visibility=Internal" "Client=false" "_V2=true" "FileNaming=PathToUnderscores" - else - generate_grpc "$proto" "$here/upstream" "$output" "Visibility=Internal" "_V2=true" "FileNaming=PathToUnderscores" - fi - done -} - -function generate_health_service { - local proto="$here/upstream/grpc/health/v1/health.proto" - local output="$root/Sources/Services/Health/Generated" - - generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" "UseAccessLevelOnImports=true" - generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Package" "Client=true" "Server=true" "_V2=true" "UseAccessLevelOnImports=true" -} - -#------------------------------------------------------------------------------ - -# Examples -generate_echo_v1_example -generate_echo_v2_example -generate_routeguide_v1_example -generate_routeguide_v2_example -generate_helloworld_v1_example -generate_helloworld_v2_example -generate_reflection_data_example - -# Reflection service and tests -generate_reflection_service -generate_reflection_client_for_tests -generate_echo_reflection_data_for_tests - -# Interoperability tests -generate_service_messages_interop_tests - -# Misc. tests -generate_normalization_for_tests -generate_rpc_code_for_tests -generate_http2_transport_tests_service - -# Performance worker service -generate_worker_service - -# Health -generate_health_service diff --git a/Protos/tests/control/control.proto b/Protos/tests/control/control.proto deleted file mode 100644 index fb32eaa20..000000000 --- a/Protos/tests/control/control.proto +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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. - */ - -syntax = "proto3"; - -// A controllable service for testing. -// -// The control service has one RPC of each kind, the input to each RPC controls -// the output. -service Control { - rpc Unary(ControlInput) returns (ControlOutput) {} - rpc ServerStream(ControlInput) returns (stream ControlOutput) {} - rpc ClientStream(stream ControlInput) returns (ControlOutput) {} - rpc BidiStream(stream ControlInput) returns (stream ControlOutput) {} -} - -message ControlInput { - // Whether metadata should be echo'd back in the initial metadata. - // - // Ignored if the initial metadata has already been sent back to the - // client. - // - // Each header field name in the request headers will be prefixed with - // "echo-". For example the header field name "foo" will be returned - // as "echo-foo. Note that semicolons aren't valid in HTTP header field - // names (apart from pseudo headers). As such all semicolons should be - // removed (":path" should become "echo-path"). - bool echo_metadata_in_headers = 1; - - // Parameters for response messages. - PayloadParameters message_params = 2; - - // The number of response messages. - int32 number_of_messages = 3; - - // The status code and message to use at the end of the RPC. - // - // If this is set then the RPC will be ended after `number_of_messages` - // messages have been sent back to the client. - RPCStatus status = 5; - - // Whether the response should be trailers only. - // - // Ignored unless it's set on the first message on the stream. When set - // the RPC will be completed with a trailers-only response using the - // status code and message from 'status'. The request metadata will be - // included if 'echo_metadata_in_trailers' is set. - // - // If this is set then 'number_of_messages', 'message_params', and - // 'echo_metadata_in_headers' are ignored. - bool is_trailers_only = 6; - - // Whether metadata should be echo'd back in the trailing metadata. - // - // Ignored unless 'status' is set. - // - // Each header field name in the request headers will be prefixed with - // "echo-". For example the header field name "foo" will be returned - // as "echo-foo. Note that semicolons aren't valid in HTTP header field - // names (apart from pseudo headers). As such all semicolons should be - // removed (":path" should become "echo-path"). - bool echo_metadata_in_trailers = 4; -} - -message RPCStatus { - // Status code indicating the outcome of the RPC. - StatusCode code = 1; - - // The message to include with the 'code' at the end of the RPC. - string message = 2; -} - -enum StatusCode { - OK = 0; - CANCELLED = 1; - UNKNOWN = 2; - INVALID_ARGUMENT = 3; - DEADLINE_EXCEEDED = 4; - NOT_FOUND = 5; - ALREADY_EXISTS = 6; - PERMISSION_DENIED = 7; - RESOURCE_EXHAUSTED = 8; - FAILED_PRECONDITION = 9; - ABORTED = 10; - OUT_OF_RANGE = 11; - UNIMPLEMENTED = 12; - INTERNAL = 13; - UNAVAILABLE = 14; - DATA_LOSS = 15; - UNAUTHENTICATED = 16; -} - -message PayloadParameters { - // The number of bytes to put into the output payload. - int32 size = 1; - - // The content to use in the payload. The value is truncated to an octet. - uint32 content = 2; -} - -message ControlOutput { - bytes payload = 1; -} diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/empty.proto b/Protos/tests/interoperability/src/proto/grpc/testing/empty.proto deleted file mode 100644 index dc4cc6067..000000000 --- a/Protos/tests/interoperability/src/proto/grpc/testing/empty.proto +++ /dev/null @@ -1,28 +0,0 @@ - -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -// An empty message that you can re-use to avoid defining duplicated empty -// messages in your project. A typical example is to use it as argument or the -// return value of a service API. For instance: -// -// service Foo { -// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; -// }; -// -message Empty {} \ No newline at end of file diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto b/Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto deleted file mode 100644 index 42e9cee1c..000000000 --- a/Protos/tests/interoperability/src/proto/grpc/testing/empty_service.proto +++ /dev/null @@ -1,23 +0,0 @@ - -// Copyright 2018 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -// A service that has zero methods. -// See https://github.com/grpc/grpc/issues/15574 -service EmptyService { -} \ No newline at end of file diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/messages.proto b/Protos/tests/interoperability/src/proto/grpc/testing/messages.proto deleted file mode 100644 index bbc4d6988..000000000 --- a/Protos/tests/interoperability/src/proto/grpc/testing/messages.proto +++ /dev/null @@ -1,165 +0,0 @@ - -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -syntax = "proto3"; - -package grpc.testing; - -// TODO(dgq): Go back to using well-known types once -// https://github.com/grpc/grpc/issues/6980 has been fixed. -// import "google/protobuf/wrappers.proto"; -message BoolValue { - // The bool value. - bool value = 1; -} - -// The type of payload that should be returned. -enum PayloadType { - // Compressable text format. - COMPRESSABLE = 0; -} - -// A block of data, to simply increase gRPC message size. -message Payload { - // The type of data in body. - PayloadType type = 1; - // Primary contents of payload. - bytes body = 2; -} - -// A protobuf representation for grpc status. This is used by test -// clients to specify a status that the server should attempt to return. -message EchoStatus { - int32 code = 1; - string message = 2; -} - -// Unary request. -message SimpleRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, server randomly chooses one from other formats. - PayloadType response_type = 1; - - // Desired payload size in the response from the server. - int32 response_size = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether SimpleResponse should include username. - bool fill_username = 4; - - // Whether SimpleResponse should include OAuth scope. - bool fill_oauth_scope = 5; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue response_compressed = 6; - - // Whether server should return a given status - EchoStatus response_status = 7; - - // Whether the server should expect this request to be compressed. - BoolValue expect_compressed = 8; -} - -// Unary response, as configured by the request. -message SimpleResponse { - // Payload to increase message size. - Payload payload = 1; - // The user the request came from, for verifying authentication was - // successful when the client expected it. - string username = 2; - // OAuth scope. - string oauth_scope = 3; -} - -// Client-streaming request. -message StreamingInputCallRequest { - // Optional input payload sent along with the request. - Payload payload = 1; - - // Whether the server should expect this request to be compressed. This field - // is "nullable" in order to interoperate seamlessly with servers not able to - // implement the full compression tests by introspecting the call to verify - // the request's compression status. - BoolValue expect_compressed = 2; - - // Not expecting any payload from the response. -} - -// Client-streaming response. -message StreamingInputCallResponse { - // Aggregated size of payloads received from the client. - int32 aggregated_payload_size = 1; -} - -// Configuration for a particular response. -message ResponseParameters { - // Desired payload sizes in responses from the server. - int32 size = 1; - - // Desired interval between consecutive responses in the response stream in - // microseconds. - int32 interval_us = 2; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue compressed = 3; -} - -// Server-streaming request. -message StreamingOutputCallRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, the payload from each response in the stream - // might be of different types. This is to simulate a mixed type of payload - // stream. - PayloadType response_type = 1; - - // Configuration for each expected response message. - repeated ResponseParameters response_parameters = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether server should return a given status - EchoStatus response_status = 7; -} - -// Server-streaming response, as configured by the request and parameters. -message StreamingOutputCallResponse { - // Payload to increase response size. - Payload payload = 1; -} - -// For reconnect interop test only. -// Client tells server what reconnection parameters it used. -message ReconnectParams { - int32 max_reconnect_backoff_ms = 1; -} - -// For reconnect interop test only. -// Server tells client whether its reconnects are following the spec and the -// reconnect backoffs it saw. -message ReconnectInfo { - bool passed = 1; - repeated int32 backoff_ms = 2; -} \ No newline at end of file diff --git a/Protos/tests/interoperability/src/proto/grpc/testing/test.proto b/Protos/tests/interoperability/src/proto/grpc/testing/test.proto deleted file mode 100644 index c049c8fa0..000000000 --- a/Protos/tests/interoperability/src/proto/grpc/testing/test.proto +++ /dev/null @@ -1,79 +0,0 @@ - -// Copyright 2015-2016 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. - -syntax = "proto3"; - -import "src/proto/grpc/testing/empty.proto"; -import "src/proto/grpc/testing/messages.proto"; - -package grpc.testing; - -// A simple service to test the various types of RPCs and experiment with -// performance with various types of payload. -service TestService { - // One empty request followed by one empty response. - rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); - - // One request followed by one response. - rpc UnaryCall(SimpleRequest) returns (SimpleResponse); - - // One request followed by one response. Response has cache control - // headers set such that a caching HTTP proxy (such as GFE) can - // satisfy subsequent requests. - rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); - - // One request followed by a sequence of responses (streamed download). - // The server returns the payload with client desired type and sizes. - rpc StreamingOutputCall(StreamingOutputCallRequest) - returns (stream StreamingOutputCallResponse); - - // A sequence of requests followed by one response (streamed upload). - // The server returns the aggregated size of client payload as the result. - rpc StreamingInputCall(stream StreamingInputCallRequest) - returns (StreamingInputCallResponse); - - // A sequence of requests with each request served by the server immediately. - // As one request could lead to multiple responses, this interface - // demonstrates the idea of full duplexing. - rpc FullDuplexCall(stream StreamingOutputCallRequest) - returns (stream StreamingOutputCallResponse); - - // A sequence of requests followed by a sequence of responses. - // The server buffers all the client requests and then serves them in order. A - // stream of responses are returned to the client when the server starts with - // first request. - rpc HalfDuplexCall(stream StreamingOutputCallRequest) - returns (stream StreamingOutputCallResponse); - - // The test server will not implement this method. It will be used - // to test the behavior when clients call unimplemented methods. - rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); -} - -// A simple service NOT implemented at servers so clients can test for -// that case. -service UnimplementedService { - // A call that no server should implement - rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); -} - -// A service used to control reconnect server. -service ReconnectService { - rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); - rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); -} diff --git a/Protos/tests/normalization/normalization.proto b/Protos/tests/normalization/normalization.proto deleted file mode 100644 index 201c8e45a..000000000 --- a/Protos/tests/normalization/normalization.proto +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2021 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package normalization; - -import "google/protobuf/empty.proto"; - -service Normalization { - rpc Unary(google.protobuf.Empty) returns (FunctionName) {} - rpc unary(google.protobuf.Empty) returns (FunctionName) {} - - rpc ServerStreaming(google.protobuf.Empty) returns (stream FunctionName) {} - rpc serverStreaming(google.protobuf.Empty) returns (stream FunctionName) {} - - rpc ClientStreaming(stream google.protobuf.Empty) returns (FunctionName) {} - rpc clientStreaming(stream google.protobuf.Empty) returns (FunctionName) {} - - rpc BidirectionalStreaming(stream google.protobuf.Empty) returns (stream FunctionName) {} - rpc bidirectionalStreaming(stream google.protobuf.Empty) returns (stream FunctionName) {} -} - -message FunctionName { - // The name of the invoked function. - string functionName = 1; -} diff --git a/Sources/CGRPCZlib/empty.c b/Sources/CGRPCZlib/empty.c deleted file mode 100644 index 13f77e710..000000000 --- a/Sources/CGRPCZlib/empty.c +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -// Xcode's Archive builds with Xcode's Package support struggle with empty .c files -// (https://bugs.swift.org/browse/SR-12939). -void CGRPCZlib_i_do_nothing_just_working_around_a_darwin_toolchain_bug(void) {} diff --git a/Sources/CGRPCZlib/include/CGRPCZlib.h b/Sources/CGRPCZlib/include/CGRPCZlib.h deleted file mode 100644 index 3cea97af9..000000000 --- a/Sources/CGRPCZlib/include/CGRPCZlib.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020, 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. - */ -#ifndef C_GRPC_ZLIB_H_ -#define C_GRPC_ZLIB_H_ - -#include - -static inline int CGRPCZlib_deflateInit2(z_streamp stream, int level, int method, int windowBits, - int memLevel, int strategy) { - return deflateInit2(stream, level, method, windowBits, memLevel, strategy); -} - -static inline unsigned long CGRPCZlib_deflateBound(z_streamp strm, unsigned long sourceLen) { - return deflateBound(strm, sourceLen); -} - -static inline int CGRPCZlib_deflate(z_streamp strm, int flush) { - return deflate(strm, flush); -} - -static inline int CGRPCZlib_deflateReset(z_streamp strm) { - return deflateReset(strm); -} - -static inline int CGRPCZlib_deflateEnd(z_streamp strm) { - return deflateEnd(strm); -} - -static inline int CGRPCZlib_inflateInit2(z_streamp stream, int windowBits) { - return inflateInit2(stream, windowBits); -} - -static inline int CGRPCZlib_inflate(z_streamp strm, int flush) { - return inflate(strm, flush); -} - -static inline int CGRPCZlib_inflateReset(z_streamp strm) { - return inflateReset(strm); -} - -static inline int CGRPCZlib_inflateEnd(z_streamp strm) { - return inflateEnd(strm); -} - -static inline Bytef *CGRPCZlib_castVoidToBytefPointer(void *in) { - return (Bytef *) in; -} - -#endif // C_GRPC_ZLIB_H_ diff --git a/Sources/GRPC/Array+BoundsCheck.swift b/Sources/GRPC/Array+BoundsCheck.swift deleted file mode 100644 index 19c0fd44e..000000000 --- a/Sources/GRPC/Array+BoundsCheck.swift +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -extension Array { - internal subscript(checked index: Index) -> Element? { - if self.indices.contains(index) { - return self[index] - } else { - return nil - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift deleted file mode 100644 index c1aae5c00..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Actions.swift +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2022, 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 NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine { - @usableFromInline - enum HandleMetadataAction: Hashable { - /// Invoke the user handler. - case invokeHandler - /// Cancel the RPC, the metadata was not expected. - case cancel - } - - @usableFromInline - enum HandleMessageAction: Hashable { - /// Forward the message to the interceptors, via the interceptor state machine. - case forward - /// Cancel the RPC, the message was not expected. - case cancel - } - - /// The same as 'HandleMessageAction. - @usableFromInline - typealias HandleEndAction = HandleMessageAction - - @usableFromInline - enum SendMessageAction: Equatable { - /// Intercept the message, but first intercept the headers if they are non-nil. Must go via - /// the interceptor state machine first. - case intercept(headers: HPACKHeaders?) - /// Drop the message. - case drop - } - - @usableFromInline - enum SendStatusAction: Equatable { - /// Intercept the status, providing the given trailers. - case intercept(requestHeaders: HPACKHeaders, trailers: HPACKHeaders) - /// Drop the status. - case drop - } - - @usableFromInline - enum CancelAction: Hashable { - /// Cancel and nil out the handler 'bits'. - case cancelAndNilOutHandlerComponents - /// Don't do anything. - case none - } - - /// Tracks whether response metadata has been written. - @usableFromInline - internal enum ResponseMetadata { - case notWritten(HPACKHeaders) - case written - - /// Update the metadata. It must not have been written yet. - @inlinable - mutating func update(_ metadata: HPACKHeaders) -> Bool { - switch self { - case .notWritten: - self = .notWritten(metadata) - return true - case .written: - return false - } - } - - /// Returns the metadata if it has not been written and moves the state to - /// `written`. Returns `nil` if it has already been written. - @inlinable - mutating func getIfNotWritten() -> HPACKHeaders? { - switch self { - case let .notWritten(metadata): - self = .written - return metadata - case .written: - return nil - } - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift deleted file mode 100644 index ffe572ef2..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Draining.swift +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2022, 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 NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine { - /// In the 'Draining' state the user handler has been invoked and the request stream has been - /// closed (i.e. we have seen 'end' but it has not necessarily been consumed by the user handler). - /// We can transition to a new state either by sending the end of the response stream or by - /// cancelling. - @usableFromInline - internal struct Draining { - @usableFromInline - typealias NextStateAndOutput = - ServerHandlerStateMachine.NextStateAndOutput< - ServerHandlerStateMachine.Draining.NextState, - Output - > - - /// The response headers. - @usableFromInline - internal private(set) var responseHeaders: ResponseMetadata - /// The response trailers. - @usableFromInline - internal private(set) var responseTrailers: ResponseMetadata - /// The request headers. - @usableFromInline - internal let requestHeaders: HPACKHeaders - - @inlinable - init(from state: ServerHandlerStateMachine.Handling) { - self.responseHeaders = state.responseHeaders - self.responseTrailers = state.responseTrailers - self.requestHeaders = state.requestHeaders - } - - @inlinable - mutating func setResponseHeaders( - _ metadata: HPACKHeaders - ) -> Self.NextStateAndOutput { - let output = self.responseHeaders.update(metadata) - return .init(nextState: .draining(self), output: output) - } - - @inlinable - mutating func setResponseTrailers( - _ metadata: HPACKHeaders - ) -> Self.NextStateAndOutput { - _ = self.responseTrailers.update(metadata) - return .init(nextState: .draining(self)) - } - - @inlinable - mutating func handleMetadata() -> Self.NextStateAndOutput { - // We're already draining, i.e. the inbound stream is closed, cancel the RPC. - return .init(nextState: .draining(self), output: .cancel) - } - - @inlinable - mutating func handleMessage() -> Self.NextStateAndOutput { - // We're already draining, i.e. the inbound stream is closed, cancel the RPC. - return .init(nextState: .draining(self), output: .cancel) - } - - @inlinable - mutating func handleEnd() -> Self.NextStateAndOutput { - // We're already draining, i.e. the inbound stream is closed, cancel the RPC. - return .init(nextState: .draining(self), output: .cancel) - } - - @inlinable - mutating func sendMessage() -> Self.NextStateAndOutput { - let headers = self.responseHeaders.getIfNotWritten() - return .init(nextState: .draining(self), output: .intercept(headers: headers)) - } - - @inlinable - mutating func sendStatus() -> Self.NextStateAndOutput { - return .init( - nextState: .finished(from: self), - output: .intercept( - requestHeaders: self.requestHeaders, - // If trailers had been written we'd already be in the finished state so - // the force unwrap is okay here. - trailers: self.responseTrailers.getIfNotWritten()! - ) - ) - } - - @inlinable - mutating func cancel() -> Self.NextStateAndOutput { - return .init(nextState: .finished(from: self), output: .cancelAndNilOutHandlerComponents) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift deleted file mode 100644 index 8c978cbf3..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Finished.swift +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2022, 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 NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine { - @usableFromInline - internal struct Finished { - @usableFromInline - typealias NextStateAndOutput = ServerHandlerStateMachine.NextStateAndOutput< - ServerHandlerStateMachine.Finished.NextState, - Output - > - - @inlinable - internal init(from state: ServerHandlerStateMachine.Idle) {} - @inlinable - internal init(from state: ServerHandlerStateMachine.Handling) {} - @inlinable - internal init(from state: ServerHandlerStateMachine.Draining) {} - - @inlinable - mutating func setResponseHeaders( - _ headers: HPACKHeaders - ) -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: false) - } - - @inlinable - mutating func setResponseTrailers( - _ metadata: HPACKHeaders - ) -> Self.NextStateAndOutput { - return .init(nextState: .finished(self)) - } - - @inlinable - mutating func handleMetadata() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .cancel) - } - - @inlinable - mutating func handleMessage() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .cancel) - } - - @inlinable - mutating func handleEnd() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .cancel) - } - - @inlinable - mutating func sendMessage() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func sendStatus() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func cancel() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .cancelAndNilOutHandlerComponents) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift deleted file mode 100644 index 424f5afb8..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Handling.swift +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2022, 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 NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine { - /// In the 'Handling' state the user handler has been invoked and the request stream is open (but - /// the request metadata has already been seen). We can transition to a new state either by - /// receiving the end of the request stream or by closing the response stream. Cancelling also - /// moves us to the finished state. - @usableFromInline - internal struct Handling { - @usableFromInline - typealias NextStateAndOutput = ServerHandlerStateMachine.NextStateAndOutput< - ServerHandlerStateMachine.Handling.NextState, - Output - > - - /// The response headers. - @usableFromInline - internal private(set) var responseHeaders: ResponseMetadata - /// The response trailers. - @usableFromInline - internal private(set) var responseTrailers: ResponseMetadata - /// The request headers. - @usableFromInline - internal let requestHeaders: HPACKHeaders - - /// Transition from the 'Idle' state. - @inlinable - init(from state: ServerHandlerStateMachine.Idle, requestHeaders: HPACKHeaders) { - self.responseHeaders = .notWritten([:]) - self.responseTrailers = .notWritten([:]) - self.requestHeaders = requestHeaders - } - - @inlinable - mutating func setResponseHeaders( - _ metadata: HPACKHeaders - ) -> Self.NextStateAndOutput { - let output = self.responseHeaders.update(metadata) - return .init(nextState: .handling(self), output: output) - } - - @inlinable - mutating func setResponseTrailers( - _ metadata: HPACKHeaders - ) -> Self.NextStateAndOutput { - _ = self.responseTrailers.update(metadata) - return .init(nextState: .handling(self)) - } - - @inlinable - mutating func handleMetadata() -> Self.NextStateAndOutput { - // We are in the 'Handling' state because we received metadata. If we receive it again we - // should cancel the RPC. - return .init(nextState: .handling(self), output: .cancel) - } - - @inlinable - mutating func handleMessage() -> Self.NextStateAndOutput { - // We can always forward a message since receiving the end of the request stream causes a - // transition to the 'draining' state. - return .init(nextState: .handling(self), output: .forward) - } - - @inlinable - mutating func handleEnd() -> Self.NextStateAndOutput { - // The request stream is finished: move to the draining state so the user handler can finish - // executing. - return .init(nextState: .draining(from: self), output: .forward) - } - - @inlinable - mutating func sendMessage() -> Self.NextStateAndOutput { - let headers = self.responseHeaders.getIfNotWritten() - return .init(nextState: .handling(self), output: .intercept(headers: headers)) - } - - @inlinable - mutating func sendStatus() -> Self.NextStateAndOutput { - return .init( - nextState: .finished(from: self), - output: .intercept( - requestHeaders: self.requestHeaders, - // If trailers had been written we'd already be in the finished state so - // the force unwrap is okay here. - trailers: self.responseTrailers.getIfNotWritten()! - ) - ) - } - - @inlinable - mutating func cancel() -> Self.NextStateAndOutput { - return .init(nextState: .finished(from: self), output: .cancelAndNilOutHandlerComponents) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift deleted file mode 100644 index a017bddac..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine+Idle.swift +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2022, 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 NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine { - /// In the 'Idle' state nothing has happened. To advance we must either receive metadata (i.e. - /// the request headers) and invoke the handler, or we are cancelled. - @usableFromInline - internal struct Idle { - @usableFromInline - typealias NextStateAndOutput = ServerHandlerStateMachine.NextStateAndOutput< - ServerHandlerStateMachine.Idle.NextState, - Output - > - - /// The state of the inbound stream, i.e. the request stream. - @usableFromInline - internal private(set) var inboundState: ServerInterceptorStateMachine.InboundStreamState - - @inlinable - init() { - self.inboundState = .idle - } - - @inlinable - mutating func handleMetadata() -> Self.NextStateAndOutput { - let action: HandleMetadataAction - - switch self.inboundState.receiveMetadata() { - case .accept: - // We tell the caller to invoke the handler immediately: they should then call - // 'handlerInvoked' on the state machine which will cause a transition to the next state. - action = .invokeHandler - case .reject: - action = .cancel - } - - return .init(nextState: .idle(self), output: action) - } - - @inlinable - mutating func handleMessage() -> Self.NextStateAndOutput { - // We can't receive a message before the metadata, doing so is a protocol violation. - return .init(nextState: .idle(self), output: .cancel) - } - - @inlinable - mutating func handleEnd() -> Self.NextStateAndOutput { - // Receiving 'end' before we start is odd but okay, just cancel. - return .init(nextState: .idle(self), output: .cancel) - } - - @inlinable - mutating func handlerInvoked(requestHeaders: HPACKHeaders) -> Self.NextStateAndOutput { - // The handler was invoked as a result of receiving metadata. Move to the next state. - return .init(nextState: .handling(from: self, requestHeaders: requestHeaders)) - } - - @inlinable - mutating func cancel() -> Self.NextStateAndOutput { - // There's no handler to cancel. Move straight to finished. - return .init(nextState: .finished(from: self), output: .none) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift deleted file mode 100644 index 23c3728de..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachine.swift +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2022, 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 NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal struct ServerHandlerStateMachine { - @usableFromInline - internal private(set) var state: Self.State - - @inlinable - init() { - self.state = .idle(.init()) - } - - @inlinable - mutating func setResponseHeaders(_ headers: HPACKHeaders) -> Bool { - switch self.state { - case var .handling(handling): - let nextStateAndOutput = handling.setResponseHeaders(headers) - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.setResponseHeaders(headers) - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.setResponseHeaders(headers) - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case .idle: - preconditionFailure() - } - } - - @inlinable - mutating func setResponseTrailers(_ trailers: HPACKHeaders) { - switch self.state { - case var .handling(handling): - let nextStateAndOutput = handling.setResponseTrailers(trailers) - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.setResponseTrailers(trailers) - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.setResponseTrailers(trailers) - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case .idle: - preconditionFailure() - } - } - - @inlinable - mutating func handleMetadata() -> HandleMetadataAction { - switch self.state { - case var .idle(idle): - let nextStateAndOutput = idle.handleMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .handling(handling): - let nextStateAndOutput = handling.handleMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.handleMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.handleMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func handleMessage() -> HandleMessageAction { - switch self.state { - case var .idle(idle): - let nextStateAndOutput = idle.handleMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .handling(handling): - let nextStateAndOutput = handling.handleMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.handleMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.handleMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func handleEnd() -> HandleEndAction { - switch self.state { - case var .idle(idle): - let nextStateAndOutput = idle.handleEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .handling(handling): - let nextStateAndOutput = handling.handleEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.handleEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.handleEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func sendMessage() -> SendMessageAction { - switch self.state { - case var .handling(handling): - let nextStateAndOutput = handling.sendMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.sendMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.sendMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case .idle: - preconditionFailure() - } - } - - @inlinable - mutating func sendStatus() -> SendStatusAction { - switch self.state { - case var .handling(handling): - let nextStateAndOutput = handling.sendStatus() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.sendStatus() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.sendStatus() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case .idle: - preconditionFailure() - } - } - - @inlinable - mutating func cancel() -> CancelAction { - switch self.state { - case var .idle(idle): - let nextStateAndOutput = idle.cancel() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .handling(handling): - let nextStateAndOutput = handling.cancel() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .draining(draining): - let nextStateAndOutput = draining.cancel() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.cancel() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func handlerInvoked(requestHeaders: HPACKHeaders) { - switch self.state { - case var .idle(idle): - let nextStateAndOutput = idle.handlerInvoked(requestHeaders: requestHeaders) - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case .handling: - preconditionFailure() - case .draining: - preconditionFailure() - case .finished: - preconditionFailure() - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine { - /// The possible states the state machine may be in. - @usableFromInline - internal enum State { - case idle(ServerHandlerStateMachine.Idle) - case handling(ServerHandlerStateMachine.Handling) - case draining(ServerHandlerStateMachine.Draining) - case finished(ServerHandlerStateMachine.Finished) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine { - /// The next state to transition to and any output which may be produced as a - /// result of a substate handling an action. - @usableFromInline - internal struct NextStateAndOutput { - @usableFromInline - internal var nextState: NextState - @usableFromInline - internal var output: Output - - @inlinable - internal init(nextState: NextState, output: Output) { - self.nextState = nextState - self.output = output - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.NextStateAndOutput where Output == Void { - @inlinable - internal init(nextState: NextState) { - self.nextState = nextState - self.output = () - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.Idle { - /// States which can be reached directly from 'Idle'. - @usableFromInline - internal struct NextState { - @usableFromInline - let state: ServerHandlerStateMachine.State - - @inlinable - internal init(_state: ServerHandlerStateMachine.State) { - self.state = _state - } - - @inlinable - internal static func idle(_ state: ServerHandlerStateMachine.Idle) -> Self { - return Self(_state: .idle(state)) - } - - @inlinable - internal static func handling( - from: ServerHandlerStateMachine.Idle, - requestHeaders: HPACKHeaders - ) -> Self { - return Self(_state: .handling(.init(from: from, requestHeaders: requestHeaders))) - } - - @inlinable - internal static func finished(from: ServerHandlerStateMachine.Idle) -> Self { - return Self(_state: .finished(.init(from: from))) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.Handling { - /// States which can be reached directly from 'Handling'. - @usableFromInline - internal struct NextState { - @usableFromInline - let state: ServerHandlerStateMachine.State - - @inlinable - internal init(_state: ServerHandlerStateMachine.State) { - self.state = _state - } - - @inlinable - internal static func handling(_ state: ServerHandlerStateMachine.Handling) -> Self { - return Self(_state: .handling(state)) - } - - @inlinable - internal static func draining(from: ServerHandlerStateMachine.Handling) -> Self { - return Self(_state: .draining(.init(from: from))) - } - - @inlinable - internal static func finished(from: ServerHandlerStateMachine.Handling) -> Self { - return Self(_state: .finished(.init(from: from))) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.Draining { - /// States which can be reached directly from 'Draining'. - @usableFromInline - internal struct NextState { - @usableFromInline - let state: ServerHandlerStateMachine.State - - @inlinable - internal init(_state: ServerHandlerStateMachine.State) { - self.state = _state - } - - @inlinable - internal static func draining(_ state: ServerHandlerStateMachine.Draining) -> Self { - return Self(_state: .draining(state)) - } - - @inlinable - internal static func finished(from: ServerHandlerStateMachine.Draining) -> Self { - return Self(_state: .finished(.init(from: from))) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.Finished { - /// States which can be reached directly from 'Finished'. - @usableFromInline - internal struct NextState { - @usableFromInline - let state: ServerHandlerStateMachine.State - - @inlinable - init(_state: ServerHandlerStateMachine.State) { - self.state = _state - } - - @inlinable - internal static func finished(_ state: ServerHandlerStateMachine.Finished) -> Self { - return Self(_state: .finished(state)) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift deleted file mode 100644 index c570e20b6..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Actions.swift +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2022, 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. - */ -extension ServerInterceptorStateMachine { - @usableFromInline - enum InterceptAction: Hashable { - /// Forward the message to the interceptor pipeline. - case intercept - /// Cancel the call. - case cancel - /// Drop the message. - case drop - - @inlinable - init(from streamFilter: ServerInterceptorStateMachine.StreamFilter) { - switch streamFilter { - case .accept: - self = .intercept - case .reject: - self = .cancel - } - } - } - - @usableFromInline - enum InterceptedAction: Hashable { - /// Forward the message to the network or user handler. - case forward - /// Cancel the call. - case cancel - /// Drop the message. - case drop - - @inlinable - init(from streamFilter: ServerInterceptorStateMachine.StreamFilter) { - switch streamFilter { - case .accept: - self = .forward - case .reject: - self = .cancel - } - } - } - - @usableFromInline - enum CancelAction: Hashable { - /// Write a status then nil out the interceptor pipeline. - case sendStatusThenNilOutInterceptorPipeline - /// Nil out the interceptor pipeline. - case nilOutInterceptorPipeline - /// Do nothing. - case none - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift deleted file mode 100644 index 3c1a2445f..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Finished.swift +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2022, 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. - */ -extension ServerInterceptorStateMachine { - /// The 'Finished' state is, as the name suggests, a terminal state. Nothing can happen in this - /// state. - @usableFromInline - struct Finished { - @usableFromInline - typealias NextStateAndOutput = - ServerInterceptorStateMachine.NextStateAndOutput - - init(from state: ServerInterceptorStateMachine.Intercepting) {} - - @inlinable - mutating func interceptRequestMetadata() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptRequestMessage() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptRequestEnd() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptedRequestMetadata() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptedRequestMessage() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptedRequestEnd() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptResponseMetadata() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptResponseMessage() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptResponseStatus() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptedResponseMetadata() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptedResponseMessage() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func interceptedResponseStatus() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .drop) - } - - @inlinable - mutating func cancel() -> Self.NextStateAndOutput { - return .init(nextState: .finished(self), output: .nilOutInterceptorPipeline) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift deleted file mode 100644 index 2596b319e..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine+Intercepting.swift +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2022, 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. - */ -extension ServerInterceptorStateMachine { - /// The 'Intercepting' state is responsible for validating that appropriate message parts are - /// forwarded to the interceptor pipeline and that messages parts which have been emitted from the - /// interceptors are valid to forward to either the network or the user handler (as interceptors - /// may emit new message parts). - /// - /// We only transition to the next state on `cancel` (which happens at the end of every RPC). - @usableFromInline - struct Intercepting { - @usableFromInline - typealias NextStateAndOutput = - ServerInterceptorStateMachine.NextStateAndOutput - - /// From the network into the interceptors. - @usableFromInline - internal private(set) var requestStreamIn: InboundStreamState - /// From the interceptors out to the handler. - @usableFromInline - internal private(set) var requestStreamOut: InboundStreamState - - /// From the handler into the interceptors. - @usableFromInline - internal private(set) var responseStreamIn: OutboundStreamState - /// From the interceptors out to the network. - @usableFromInline - internal private(set) var responseStreamOut: OutboundStreamState - - @usableFromInline - init() { - self.requestStreamIn = .idle - self.requestStreamOut = .idle - self.responseStreamIn = .idle - self.responseStreamOut = .idle - } - - @inlinable - mutating func interceptRequestMetadata() -> Self.NextStateAndOutput { - let filter = self.requestStreamIn.receiveMetadata() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptRequestMessage() -> Self.NextStateAndOutput { - let filter = self.requestStreamIn.receiveMessage() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptRequestEnd() -> Self.NextStateAndOutput { - let filter = self.requestStreamIn.receiveEnd() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptedRequestMetadata() -> Self.NextStateAndOutput { - let filter = self.requestStreamOut.receiveMetadata() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptedRequestMessage() -> Self.NextStateAndOutput { - let filter = self.requestStreamOut.receiveMessage() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptedRequestEnd() -> Self.NextStateAndOutput { - let filter = self.requestStreamOut.receiveEnd() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptResponseMetadata() -> Self.NextStateAndOutput { - let filter = self.responseStreamIn.sendMetadata() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptResponseMessage() -> Self.NextStateAndOutput { - let filter = self.responseStreamIn.sendMessage() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptResponseStatus() -> Self.NextStateAndOutput { - let filter = self.responseStreamIn.sendEnd() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptedResponseMetadata() -> Self.NextStateAndOutput { - let filter = self.responseStreamOut.sendMetadata() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptedResponseMessage() -> Self.NextStateAndOutput { - let filter = self.responseStreamOut.sendMessage() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func interceptedResponseStatus() -> Self.NextStateAndOutput { - let filter = self.responseStreamOut.sendEnd() - return .init(nextState: .intercepting(self), output: .init(from: filter)) - } - - @inlinable - mutating func cancel() -> Self.NextStateAndOutput { - let output: CancelAction - - // Check the state of the response stream. If we haven't sent a status then we should emit - // one first. It may not reach the other side but we should try. - switch self.responseStreamOut { - case .idle, .writingMessages: - output = .sendStatusThenNilOutInterceptorPipeline - case .done: - output = .nilOutInterceptorPipeline - } - - return .init(nextState: .finished(from: self), output: output) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift deleted file mode 100644 index cbbecf8d0..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachine.swift +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright 2022, 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. - */ -@usableFromInline -internal struct ServerInterceptorStateMachine { - @usableFromInline - internal private(set) var state: Self.State - - @inlinable - init() { - self.state = .intercepting(.init()) - } - - @inlinable - mutating func interceptRequestMetadata() -> InterceptAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptRequestMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptRequestMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptRequestMessage() -> InterceptAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptRequestMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptRequestMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptRequestEnd() -> InterceptAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptRequestEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptRequestEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptedRequestMetadata() -> InterceptedAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptedRequestMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptedRequestMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptedRequestMessage() -> InterceptedAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptedRequestMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptedRequestMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptedRequestEnd() -> InterceptedAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptedRequestEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptedRequestEnd() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptResponseMetadata() -> InterceptAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptResponseMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptResponseMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptResponseMessage() -> InterceptAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptResponseMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptResponseMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptResponseStatus() -> InterceptAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptResponseStatus() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptResponseStatus() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptedResponseMetadata() -> InterceptedAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptedResponseMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptedResponseMetadata() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptedResponseMessage() -> InterceptedAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptedResponseMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptedResponseMessage() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func interceptedResponseStatus() -> InterceptedAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.interceptedResponseStatus() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.interceptedResponseStatus() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } - - @inlinable - mutating func cancel() -> CancelAction { - switch self.state { - case var .intercepting(intercepting): - let nextStateAndOutput = intercepting.cancel() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - case var .finished(finished): - let nextStateAndOutput = finished.cancel() - self.state = nextStateAndOutput.nextState.state - return nextStateAndOutput.output - } - } -} - -extension ServerInterceptorStateMachine { - /// The possible states the state machine may be in. - @usableFromInline - internal enum State { - case intercepting(ServerInterceptorStateMachine.Intercepting) - case finished(ServerInterceptorStateMachine.Finished) - } -} - -extension ServerInterceptorStateMachine { - /// The next state to transition to and any output which may be produced as a - /// result of a substate handling an action. - @usableFromInline - internal struct NextStateAndOutput { - @usableFromInline - internal var nextState: NextState - @usableFromInline - internal var output: Output - - @inlinable - internal init(nextState: NextState, output: Output) { - self.nextState = nextState - self.output = output - } - } -} - -extension ServerInterceptorStateMachine.NextStateAndOutput where Output == Void { - internal init(nextState: NextState) { - self.nextState = nextState - self.output = () - } -} - -extension ServerInterceptorStateMachine.Intercepting { - /// States which can be reached directly from 'Intercepting'. - @usableFromInline - internal struct NextState { - @usableFromInline - let state: ServerInterceptorStateMachine.State - - @inlinable - init(_state: ServerInterceptorStateMachine.State) { - self.state = _state - } - - @usableFromInline - internal static func intercepting(_ state: ServerInterceptorStateMachine.Intercepting) -> Self { - return Self(_state: .intercepting(state)) - } - - @usableFromInline - internal static func finished(from: ServerInterceptorStateMachine.Intercepting) -> Self { - return Self(_state: .finished(.init(from: from))) - } - } -} - -extension ServerInterceptorStateMachine.Finished { - /// States which can be reached directly from 'Finished'. - @usableFromInline - internal struct NextState { - @usableFromInline - let state: ServerInterceptorStateMachine.State - - @inlinable - internal init(_state: ServerInterceptorStateMachine.State) { - self.state = _state - } - - @usableFromInline - internal static func finished(_ state: ServerInterceptorStateMachine.Finished) -> Self { - return Self(_state: .finished(state)) - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift b/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift deleted file mode 100644 index 5fddb3c39..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/StreamState.swift +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2022, 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. - */ -extension ServerInterceptorStateMachine { - @usableFromInline - internal enum StreamFilter: Hashable { - case accept - case reject - } - - @usableFromInline - internal enum InboundStreamState: Hashable { - case idle - case receivingMessages - case done - - @inlinable - mutating func receiveMetadata() -> StreamFilter { - switch self { - case .idle: - self = .receivingMessages - return .accept - case .receivingMessages, .done: - return .reject - } - } - - @inlinable - func receiveMessage() -> StreamFilter { - switch self { - case .receivingMessages: - return .accept - case .idle, .done: - return .reject - } - } - - @inlinable - mutating func receiveEnd() -> StreamFilter { - switch self { - case .idle, .receivingMessages: - self = .done - return .accept - case .done: - return .reject - } - } - } - - @usableFromInline - internal enum OutboundStreamState: Hashable { - case idle - case writingMessages - case done - - @inlinable - mutating func sendMetadata() -> StreamFilter { - switch self { - case .idle: - self = .writingMessages - return .accept - case .writingMessages, .done: - return .reject - } - } - - @inlinable - func sendMessage() -> StreamFilter { - switch self { - case .writingMessages: - return .accept - case .idle, .done: - return .reject - } - } - - @inlinable - mutating func sendEnd() -> StreamFilter { - switch self { - case .idle, .writingMessages: - self = .done - return .accept - case .done: - return .reject - } - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift deleted file mode 100644 index 5381bef32..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/Call+AsyncRequestStreamWriter.swift +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 NIOCore - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Call where Request: Sendable, Response: Sendable { - typealias AsyncWriter = NIOAsyncWriter< - (Request, Compression), - GRPCAsyncWriterSinkDelegate<(Request, Compression)> - > - internal func makeRequestStreamWriter() - -> (GRPCAsyncRequestStreamWriter, AsyncWriter.Sink) - { - let delegate = GRPCAsyncWriterSinkDelegate<(Request, Compression)>( - didYield: { requests in - for (request, compression) in requests { - let compress = - compression - .isEnabled(callDefault: self.options.messageEncoding.enabledForRequests) - - // TODO: be smarter about inserting flushes. - // We currently always flush after every write which may trigger more syscalls than necessary. - let metadata = MessageMetadata(compress: compress, flush: true) - self.send(.message(request, metadata), promise: nil) - } - }, - didTerminate: { _ in self.send(.end, promise: nil) } - ) - - let writer = NIOAsyncWriter.makeWriter(isWritable: false, delegate: delegate) - - // Start as not-writable; writability will be toggled when the stream comes up. - return (GRPCAsyncRequestStreamWriter(asyncWriter: writer.writer), writer.sink) - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift b/Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift deleted file mode 100644 index a64ba5955..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/CancellationError+GRPCStatusTransformable.swift +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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. - */ - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension CancellationError: GRPCStatusTransformable { - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .unavailable, message: nil) - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift deleted file mode 100644 index 339c99731..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncBidirectionalStreamingCall.swift +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -/// Async-await variant of ``BidirectionalStreamingCall``. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncBidirectionalStreamingCall: Sendable { - private let call: Call - private let responseParts: StreamingResponseParts - private let responseSource: - NIOThrowingAsyncSequenceProducer< - Response, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source - private let requestSink: AsyncSink<(Request, Compression)> - - /// A request stream writer for sending messages to the server. - public let requestStream: GRPCAsyncRequestStreamWriter - - /// The stream of responses from the server. - public let responseStream: GRPCAsyncResponseStream - - /// The options used to make the RPC. - public var options: CallOptions { - return self.call.options - } - - /// The path used to make the RPC. - public var path: String { - return self.call.path - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel() { - self.call.cancel(promise: nil) - } - - // MARK: - Response Parts - - private func withRPCCancellation(_ fn: () async throws -> R) async rethrows -> R { - return try await withTaskCancellationHandler(operation: fn) { - self.cancel() - } - } - - /// The initial metadata returned from the server. - /// - /// - Important: The initial metadata will only be available when the first response has been - /// received. However, it is not necessary for the response to have been consumed before reading - /// this property. - public var initialMetadata: HPACKHeaders { - get async throws { - try await self.withRPCCancellation { - try await self.responseParts.initialMetadata.get() - } - } - } - - /// The trailing metadata returned from the server. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var trailingMetadata: HPACKHeaders { - get async throws { - try await self.withRPCCancellation { - try await self.responseParts.trailingMetadata.get() - } - } - } - - /// The final status of the the RPC. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var status: GRPCStatus { - get async { - // force-try acceptable because any error is encapsulated in a successful GRPCStatus future. - await self.withRPCCancellation { - try! await self.responseParts.status.get() - } - } - } - - private init(call: Call) { - self.call = call - self.responseParts = StreamingResponseParts(on: call.eventLoop) { _ in } - - let sequenceProducer = NIOThrowingAsyncSequenceProducer< - Response, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.makeSequence( - backPressureStrategy: .init(lowWatermark: 10, highWatermark: 50), - delegate: GRPCAsyncSequenceProducerDelegate() - ) - - self.responseSource = sequenceProducer.source - self.responseStream = .init(sequenceProducer.sequence) - let (requestStream, requestSink) = call.makeRequestStreamWriter() - self.requestStream = requestStream - self.requestSink = AsyncSink(wrapping: requestSink) - } - - /// We expose this as the only non-private initializer so that the caller - /// knows that invocation is part of initialisation. - internal static func makeAndInvoke(call: Call) -> Self { - let asyncCall = Self(call: call) - - asyncCall.call.invokeStreamingRequests( - onStart: { - asyncCall.requestSink.setWritability(to: true) - }, - onError: { error in - asyncCall.responseParts.handleError(error) - asyncCall.responseSource.finish(error) - asyncCall.requestSink.finish(error: error) - }, - onResponsePart: AsyncCall.makeResponsePartHandler( - responseParts: asyncCall.responseParts, - responseSource: asyncCall.responseSource, - requestStream: asyncCall.requestStream - ) - ) - - return asyncCall - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal enum AsyncCall { - internal static func makeResponsePartHandler( - responseParts: StreamingResponseParts, - responseSource: NIOThrowingAsyncSequenceProducer< - Response, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source, - requestStream: GRPCAsyncRequestStreamWriter?, - requestType: Request.Type = Request.self - ) -> (GRPCClientResponsePart) -> Void { - return { responsePart in - // Handle the metadata, trailers and status. - responseParts.handle(responsePart) - - // Handle the response messages and status. - switch responsePart { - case .metadata: - () - - case let .message(response): - // TODO: when we support backpressure we will need to stop ignoring the return value. - _ = responseSource.yield(response) - - case let .end(status, _): - if status.isOk { - responseSource.finish() - } else { - responseSource.finish(status) - } - requestStream?.finish(status) - } - } - } - - internal static func makeResponsePartHandler( - responseParts: UnaryResponseParts, - requestStream: GRPCAsyncRequestStreamWriter?, - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> (GRPCClientResponsePart) -> Void { - return { responsePart in - // Handle (most of) all parts. - responseParts.handle(responsePart) - - // Handle the status. - switch responsePart { - case .metadata, .message: - () - case let .end(status, _): - requestStream?.finish(status) - } - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift deleted file mode 100644 index 3d96f2a35..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncClientStreamingCall.swift +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -/// Async-await variant of ``ClientStreamingCall``. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncClientStreamingCall: Sendable { - private let call: Call - private let responseParts: UnaryResponseParts - private let requestSink: AsyncSink<(Request, Compression)> - - /// A request stream writer for sending messages to the server. - public let requestStream: GRPCAsyncRequestStreamWriter - - /// The options used to make the RPC. - public var options: CallOptions { - return self.call.options - } - - /// The path used to make the RPC. - public var path: String { - return self.call.path - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel() { - self.call.cancel(promise: nil) - } - - // MARK: - Response Parts - - private func withRPCCancellation(_ fn: () async throws -> R) async rethrows -> R { - return try await withTaskCancellationHandler(operation: fn) { - self.cancel() - } - } - - /// The initial metadata returned from the server. - /// - /// - Important: The initial metadata will only be available when the response has been received. - public var initialMetadata: HPACKHeaders { - get async throws { - return try await self.withRPCCancellation { - try await self.responseParts.initialMetadata.get() - } - } - } - - /// The response returned by the server. - public var response: Response { - get async throws { - return try await self.withRPCCancellation { - try await self.responseParts.response.get() - } - } - } - - /// The trailing metadata returned from the server. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var trailingMetadata: HPACKHeaders { - get async throws { - return try await self.withRPCCancellation { - try await self.responseParts.trailingMetadata.get() - } - } - } - - /// The final status of the the RPC. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var status: GRPCStatus { - get async { - // force-try acceptable because any error is encapsulated in a successful GRPCStatus future. - return await self.withRPCCancellation { - try! await self.responseParts.status.get() - } - } - } - - private init(call: Call) { - self.call = call - self.responseParts = UnaryResponseParts(on: call.eventLoop) - let (requestStream, requestSink) = call.makeRequestStreamWriter() - self.requestStream = requestStream - self.requestSink = AsyncSink(wrapping: requestSink) - } - - /// We expose this as the only non-private initializer so that the caller - /// knows that invocation is part of initialisation. - internal static func makeAndInvoke(call: Call) -> Self { - let asyncCall = Self(call: call) - - asyncCall.call.invokeStreamingRequests( - onStart: { - asyncCall.requestSink.setWritability(to: true) - }, - onError: { error in - asyncCall.responseParts.handleError(error) - asyncCall.requestSink.finish(error: error) - }, - onResponsePart: AsyncCall.makeResponsePartHandler( - responseParts: asyncCall.responseParts, - requestStream: asyncCall.requestStream - ) - ) - - return asyncCall - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift deleted file mode 100644 index 500a859bf..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStream.swift +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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 NIOCore - -/// A type for the stream of request messages send to a gRPC server method. -/// -/// To enable testability this type provides a static ``GRPCAsyncRequestStream/makeTestingRequestStream()`` -/// method which allows you to create a stream that you can drive. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncRequestStream: AsyncSequence { - @usableFromInline - internal typealias _AsyncSequenceProducer = NIOThrowingAsyncSequenceProducer< - Element, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - > - - /// A source used for driving a ``GRPCAsyncRequestStream`` during tests. - public struct Source { - @usableFromInline - internal let continuation: AsyncThrowingStream.Continuation - - @inlinable - init(continuation: AsyncThrowingStream.Continuation) { - self.continuation = continuation - } - - /// Yields the element to the request stream. - /// - /// - Parameter element: The element to yield to the request stream. - @inlinable - public func yield(_ element: Element) { - self.continuation.yield(element) - } - - /// Finished the request stream. - @inlinable - public func finish() { - self.continuation.finish() - } - - /// Finished the request stream. - /// - /// - Parameter error: An optional `Error` to finish the request stream with. - @inlinable - public func finish(throwing error: Error?) { - self.continuation.finish(throwing: error) - } - } - - /// Simple struct for the return type of ``GRPCAsyncRequestStream/makeTestingRequestStream()``. - /// - /// This struct contains two properties: - /// 1. The ``stream`` which is the actual ``GRPCAsyncRequestStream`` and should be passed to the method under testing. - /// 2. The ``source`` which can be used to drive the stream. - public struct TestingStream { - /// The actual stream. - public let stream: GRPCAsyncRequestStream - /// The source used to drive the stream. - public let source: Source - - @inlinable - init(stream: GRPCAsyncRequestStream, source: Source) { - self.stream = stream - self.source = source - } - } - - @usableFromInline - enum Backing: Sendable { - case asyncStream(AsyncThrowingStream) - case throwingAsyncSequenceProducer(_AsyncSequenceProducer) - } - - @usableFromInline - internal let backing: Backing - - @inlinable - internal init(_ sequence: _AsyncSequenceProducer) { - self.backing = .throwingAsyncSequenceProducer(sequence) - } - - @inlinable - internal init(_ stream: AsyncThrowingStream) { - self.backing = .asyncStream(stream) - } - - /// Creates a new testing stream. - /// - /// This is useful for writing unit tests for your gRPC method implementations since it allows you to drive the stream passed - /// to your method. - /// - /// - Returns: A new ``TestingStream`` containing the actual ``GRPCAsyncRequestStream`` and a ``Source``. - @inlinable - public static func makeTestingRequestStream() -> TestingStream { - var continuation: AsyncThrowingStream.Continuation! - let stream = AsyncThrowingStream { continuation = $0 } - let source = Source(continuation: continuation) - let requestStream = Self(stream) - return TestingStream(stream: requestStream, source: source) - } - - @inlinable - public func makeAsyncIterator() -> Iterator { - switch self.backing { - case let .asyncStream(stream): - return Self.AsyncIterator(.asyncStream(stream.makeAsyncIterator())) - case let .throwingAsyncSequenceProducer(sequence): - return Self.AsyncIterator(.throwingAsyncSequenceProducer(sequence.makeAsyncIterator())) - } - } - - public struct Iterator: AsyncIteratorProtocol { - @usableFromInline - enum BackingIterator { - case asyncStream(AsyncThrowingStream.Iterator) - case throwingAsyncSequenceProducer(_AsyncSequenceProducer.AsyncIterator) - } - - @usableFromInline - internal var iterator: BackingIterator - - @usableFromInline - internal init(_ iterator: BackingIterator) { - self.iterator = iterator - } - - @inlinable - public mutating func next() async throws -> Element? { - if Task.isCancelled { throw GRPCStatus(code: .cancelled) } - switch self.iterator { - case var .asyncStream(iterator): - let element = try await iterator.next() - self.iterator = .asyncStream(iterator) - return element - case let .throwingAsyncSequenceProducer(iterator): - return try await iterator.next() - } - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCAsyncRequestStream: Sendable where Element: Sendable {} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift deleted file mode 100644 index 9516d00a6..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncRequestStreamWriter.swift +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 NIOCore - -/// An object allowing the holder -- a client -- to send requests on an RPC. -/// -/// Requests may be sent using ``send(_:compression:)``. After all requests have been sent -/// the user is responsible for closing the request stream by calling ``finish()``. -/// -/// ``` -/// // Send a request on the request stream, use the compression setting configured for the RPC. -/// try await stream.send(request) -/// -/// // Send a request and explicitly disable compression. -/// try await stream.send(request, compression: .disabled) -/// -/// // Finish the stream to indicate that no more messages will be sent. -/// try await stream.finish() -/// ``` -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncRequestStreamWriter: Sendable { - @usableFromInline - typealias AsyncWriter = NIOAsyncWriter< - (Request, Compression), - GRPCAsyncWriterSinkDelegate<(Request, Compression)> - > - - @usableFromInline - /* private */ internal let asyncWriter: AsyncWriter - - @inlinable - internal init(asyncWriter: AsyncWriter) { - self.asyncWriter = asyncWriter - } - - /// Send a single request. - /// - /// It is safe to send multiple requests concurrently by sharing the ``GRPCAsyncRequestStreamWriter`` across tasks. - /// - /// Callers must call ``finish()`` when they have no more requests left to send. - /// - /// - Parameters: - /// - request: The request to send. - /// - compression: Whether the request should be compressed or not. Ignored if compression was - /// not enabled for the RPC. - /// - Throws: If the request stream has already been finished. - @inlinable - public func send( - _ request: Request, - compression: Compression = .deferToCallDefault - ) async throws { - try await self.asyncWriter.yield((request, compression)) - } - - /// Send a sequence of requests. - /// - /// It is safe to send multiple requests concurrently by sharing the ``GRPCAsyncRequestStreamWriter`` across tasks. - /// - /// Callers must call ``finish()`` when they have no more requests left to send. - /// - /// - Parameters: - /// - requests: The requests to send. - /// - compression: Whether the requests should be compressed or not. Ignored if compression was - /// not enabled for the RPC. - /// - Throws: If the request stream has already been finished. - @inlinable - public func send( - _ requests: S, - compression: Compression = .deferToCallDefault - ) async throws where S.Element == Request { - try await self.asyncWriter.yield(contentsOf: requests.lazy.map { ($0, compression) }) - } - - /// Finish the request stream for the RPC. This must be called when there are no more requests to be sent. - public func finish() { - self.asyncWriter.finish() - } - - /// Finish the request stream for the RPC with the given error. - internal func finish(_ error: Error) { - self.asyncWriter.finish(error: error) - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift deleted file mode 100644 index 225fa31d8..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStream.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 NIOCore - -/// This is currently a wrapper around AsyncThrowingStream because we want to be -/// able to swap out the implementation for something else in the future. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncResponseStream: AsyncSequence { - @usableFromInline - internal typealias WrappedStream = NIOThrowingAsyncSequenceProducer< - Element, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - > - - @usableFromInline - internal let stream: WrappedStream - - @inlinable - internal init(_ stream: WrappedStream) { - self.stream = stream - } - - public func makeAsyncIterator() -> Iterator { - Self.AsyncIterator(self.stream) - } - - public struct Iterator: AsyncIteratorProtocol { - @usableFromInline - internal var iterator: WrappedStream.AsyncIterator - - fileprivate init(_ stream: WrappedStream) { - self.iterator = stream.makeAsyncIterator() - } - - @inlinable - public mutating func next() async throws -> Element? { - if Task.isCancelled { throw GRPCStatus(code: .cancelled) } - return try await self.iterator.next() - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCAsyncResponseStream: Sendable {} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift deleted file mode 100644 index c00490c8a..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncResponseStreamWriter.swift +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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 NIOCore - -/// Writer for server-streaming RPC handlers to provide responses. -/// -/// To enable testability this type provides a static ``GRPCAsyncResponseStreamWriter/makeTestingResponseStreamWriter()`` -/// method which allows you to create a stream that you can drive. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncResponseStreamWriter: Sendable { - @usableFromInline - internal typealias AsyncWriter = NIOAsyncWriter< - (Response, Compression), - GRPCAsyncWriterSinkDelegate<(Response, Compression)> - > - - /// An `AsyncSequence` backing a ``GRPCAsyncResponseStreamWriter`` for testing purposes. - /// - /// - Important: This `AsyncSequence` is never finishing. - public struct ResponseStream: AsyncSequence { - public typealias Element = (Response, Compression) - - @usableFromInline - internal let stream: AsyncStream<(Response, Compression)> - - @usableFromInline - internal let continuation: AsyncStream<(Response, Compression)>.Continuation - - @inlinable - init( - stream: AsyncStream<(Response, Compression)>, - continuation: AsyncStream<(Response, Compression)>.Continuation - ) { - self.stream = stream - self.continuation = continuation - } - - public func makeAsyncIterator() -> AsyncIterator { - AsyncIterator(iterator: self.stream.makeAsyncIterator()) - } - - /// Finishes the response stream. - /// - /// This is useful in tests to finish the stream after the async method finished and allows you to collect all written responses. - public func finish() { - self.continuation.finish() - } - - public struct AsyncIterator: AsyncIteratorProtocol { - @usableFromInline - internal var iterator: AsyncStream<(Response, Compression)>.AsyncIterator - - @inlinable - init(iterator: AsyncStream<(Response, Compression)>.AsyncIterator) { - self.iterator = iterator - } - - public mutating func next() async -> Element? { - await self.iterator.next() - } - } - } - - /// Simple struct for the return type of ``GRPCAsyncResponseStreamWriter/makeTestingResponseStreamWriter()``. - /// - /// This struct contains two properties: - /// 1. The ``writer`` which is the actual ``GRPCAsyncResponseStreamWriter`` and should be passed to the method under testing. - /// 2. The ``stream`` which can be used to observe the written responses. - public struct TestingStreamWriter { - /// The actual writer. - public let writer: GRPCAsyncResponseStreamWriter - /// The written responses in a stream. - /// - /// - Important: This `AsyncSequence` is never finishing. - public let stream: ResponseStream - - @inlinable - init(writer: GRPCAsyncResponseStreamWriter, stream: ResponseStream) { - self.writer = writer - self.stream = stream - } - } - - @usableFromInline - enum Backing: Sendable { - case asyncWriter(AsyncWriter) - case closure(@Sendable ((Response, Compression)) async -> Void) - } - - @usableFromInline - internal let backing: Backing - - @inlinable - internal init(wrapping asyncWriter: AsyncWriter) { - self.backing = .asyncWriter(asyncWriter) - } - - @inlinable - internal init(onWrite: @escaping @Sendable ((Response, Compression)) async -> Void) { - self.backing = .closure(onWrite) - } - - @inlinable - public func send( - _ response: Response, - compression: Compression = .deferToCallDefault - ) async throws { - switch self.backing { - case let .asyncWriter(writer): - try await writer.yield((response, compression)) - - case let .closure(closure): - await closure((response, compression)) - } - } - - @inlinable - public func send( - contentsOf responses: S, - compression: Compression = .deferToCallDefault - ) async throws where S.Element == Response { - let responsesWithCompression = responses.lazy.map { ($0, compression) } - switch self.backing { - case let .asyncWriter(writer): - try await writer.yield(contentsOf: responsesWithCompression) - - case let .closure(closure): - for response in responsesWithCompression { - await closure(response) - } - } - } - - /// Creates a new `GRPCAsyncResponseStreamWriter` backed by a ``ResponseStream``. - /// This is mostly useful for testing purposes where one wants to observe the written responses. - /// - /// - Note: For most tests it is useful to call ``ResponseStream/finish()`` after the async method under testing - /// resumed. This allows you to easily collect all written responses. - @inlinable - public static func makeTestingResponseStreamWriter() -> TestingStreamWriter { - var continuation: AsyncStream<(Response, Compression)>.Continuation! - let asyncStream = AsyncStream<(Response, Compression)> { cont in - continuation = cont - } - let writer = Self.init { [continuation] in - continuation!.yield($0) - } - let responseStream = ResponseStream( - stream: asyncStream, - continuation: continuation - ) - - return TestingStreamWriter(writer: writer, stream: responseStream) - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift deleted file mode 100644 index baca7ba91..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncSequenceProducerDelegate.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2022, 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 NIOCore - -@usableFromInline -internal struct GRPCAsyncSequenceProducerDelegate: NIOAsyncSequenceProducerDelegate { - @inlinable - internal init() {} - - // TODO: this method will have to be implemented when we add support for backpressure. - @inlinable - internal func produceMore() {} - - // TODO: this method will have to be implemented when we add support for backpressure. - @inlinable - internal func didTerminate() {} -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift deleted file mode 100644 index d12515962..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerCallContext.swift +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 Logging -import NIOConcurrencyHelpers -import NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncServerCallContext: Sendable { - @usableFromInline - let contextProvider: AsyncServerCallContextProvider - - /// Details of the request, including request headers and a logger. - public var request: Request - - /// A response context which may be used to set response headers and trailers. - public var response: Response { - Response(contextProvider: self.contextProvider) - } - - /// Notifies the client that the RPC has been accepted for processing by the server. - /// - /// On accepting the RPC the server will send the given headers (which may be empty) along with - /// any transport specific headers (such the ":status" pseudo header) to the client. - /// - /// It is not necessary to call this function: the RPC is implicitly accepted when the first - /// response message is sent, however this may be useful when clients require an early indication - /// that the RPC has been accepted. - /// - /// If the RPC has already been accepted (either implicitly or explicitly) then this function is - /// a no-op. - public func acceptRPC(headers: HPACKHeaders) async { - await self.contextProvider.acceptRPC(headers) - } - - /// Access the ``UserInfo`` dictionary which is shared with the interceptor contexts for this RPC. - /// - /// - Important: While ``UserInfo`` has value-semantics, this function accesses a reference - /// wrapped ``UserInfo``. The contexts passed to interceptors provide the same reference. As such - /// this may be used as a mechanism to pass information between interceptors and service - /// providers. - public func withUserInfo( - _ body: @Sendable @escaping (UserInfo) throws -> Result - ) async throws -> Result { - return try await self.contextProvider.withUserInfo(body) - } - - /// Modify the ``UserInfo`` dictionary which is shared with the interceptor contexts for this RPC. - /// - /// - Important: While ``UserInfo`` has value-semantics, this function accesses a reference - /// wrapped ``UserInfo``. The contexts passed to interceptors provide the same reference. As such - /// this may be used as a mechanism to pass information between interceptors and service - /// providers. - public func withMutableUserInfo( - _ modify: @Sendable @escaping (inout UserInfo) -> Result - ) async throws -> Result { - return try await self.contextProvider.withMutableUserInfo(modify) - } - - @inlinable - internal init( - headers: HPACKHeaders, - logger: Logger, - contextProvider: AsyncServerCallContextProvider - ) { - self.request = Request(headers: headers, logger: logger) - self.contextProvider = contextProvider - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCAsyncServerCallContext { - public struct Request: Sendable { - /// The request headers received from the client at the start of the RPC. - public var headers: HPACKHeaders - - /// A logger. - public var logger: Logger - - @usableFromInline - init(headers: HPACKHeaders, logger: Logger) { - self.headers = headers - self.logger = logger - } - } - - public struct Response: Sendable { - private let contextProvider: AsyncServerCallContextProvider - - /// Set the metadata to return at the start of the RPC. - /// - /// - Important: If this is required it should be updated _before_ the first response is sent - /// via the response stream writer. Updates must not be made after the RPC has been accepted - /// or the first response has been sent otherwise this method will throw an error. - public func setHeaders(_ headers: HPACKHeaders) async throws { - try await self.contextProvider.setResponseHeaders(headers) - } - - /// Set the metadata to return at the end of the RPC. - /// - /// If this is required it must be updated before returning from the handler. - public func setTrailers(_ trailers: HPACKHeaders) async throws { - try await self.contextProvider.setResponseTrailers(trailers) - } - - /// Whether compression should be enabled for responses, defaulting to `true`. Note that for - /// this value to take effect compression must have been enabled on the server and a compression - /// algorithm must have been negotiated with the client. - public func compressResponses(_ compress: Bool) async throws { - try await self.contextProvider.setResponseCompression(compress) - } - - @usableFromInline - internal init(contextProvider: AsyncServerCallContextProvider) { - self.contextProvider = contextProvider - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift deleted file mode 100644 index bea30ae6a..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerHandler.swift +++ /dev/null @@ -1,912 +0,0 @@ -/* - * 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 DequeModule -import Logging -import NIOCore -import NIOHPACK - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncServerHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer, - Request: Sendable, - Response: Sendable ->: GRPCServerHandlerProtocol where Serializer.Input == Response, Deserializer.Output == Request { - @usableFromInline - internal let _handler: AsyncServerHandler - - public func receiveMetadata(_ metadata: HPACKHeaders) { - self._handler.receiveMetadata(metadata) - } - - public func receiveMessage(_ bytes: ByteBuffer) { - self._handler.receiveMessage(bytes) - } - - public func receiveEnd() { - self._handler.receiveEnd() - } - - public func receiveError(_ error: Error) { - self._handler.receiveError(error) - } - - public func finish() { - self._handler.finish() - } -} - -// MARK: - RPC Adapters - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCAsyncServerHandler { - public typealias Request = Deserializer.Output - public typealias Response = Serializer.Input - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - wrapping unary: @escaping @Sendable (Request, GRPCAsyncServerCallContext) async throws - -> Response - ) { - self._handler = .init( - context: context, - requestDeserializer: requestDeserializer, - responseSerializer: responseSerializer, - callType: .unary, - interceptors: interceptors, - userHandler: { requestStream, responseStreamWriter, context in - var iterator = requestStream.makeAsyncIterator() - guard let request = try await iterator.next(), try await iterator.next() == nil else { - throw GRPCError.ProtocolViolation("Unary RPC expects exactly one request") - } - let response = try await unary(request, context) - try await responseStreamWriter.send(response) - } - ) - } - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - wrapping clientStreaming: @escaping @Sendable ( - GRPCAsyncRequestStream, - GRPCAsyncServerCallContext - ) async throws -> Response - ) { - self._handler = .init( - context: context, - requestDeserializer: requestDeserializer, - responseSerializer: responseSerializer, - callType: .clientStreaming, - interceptors: interceptors, - userHandler: { requestStream, responseStreamWriter, context in - let response = try await clientStreaming(requestStream, context) - try await responseStreamWriter.send(response) - } - ) - } - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - wrapping serverStreaming: @escaping @Sendable ( - Request, - GRPCAsyncResponseStreamWriter, - GRPCAsyncServerCallContext - ) async throws -> Void - ) { - self._handler = .init( - context: context, - requestDeserializer: requestDeserializer, - responseSerializer: responseSerializer, - callType: .serverStreaming, - interceptors: interceptors, - userHandler: { requestStream, responseStreamWriter, context in - var iterator = requestStream.makeAsyncIterator() - guard let request = try await iterator.next(), try await iterator.next() == nil else { - throw GRPCError.ProtocolViolation("Server-streaming RPC expects exactly one request") - } - try await serverStreaming(request, responseStreamWriter, context) - } - ) - } - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - wrapping bidirectional: @escaping @Sendable ( - GRPCAsyncRequestStream, - GRPCAsyncResponseStreamWriter, - GRPCAsyncServerCallContext - ) async throws -> Void - ) { - self._handler = .init( - context: context, - requestDeserializer: requestDeserializer, - responseSerializer: responseSerializer, - callType: .bidirectionalStreaming, - interceptors: interceptors, - userHandler: bidirectional - ) - } -} - -// MARK: - Server Handler - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal final class AsyncServerHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer, - Request: Sendable, - Response: Sendable ->: GRPCServerHandlerProtocol where Serializer.Input == Response, Deserializer.Output == Request { - /// A response serializer. - @usableFromInline - internal let serializer: Serializer - - /// A request deserializer. - @usableFromInline - internal let deserializer: Deserializer - - /// The event loop that this handler executes on. - @usableFromInline - internal let eventLoop: EventLoop - - /// A `ByteBuffer` allocator provided by the underlying `Channel`. - @usableFromInline - internal let allocator: ByteBufferAllocator - - /// A user-provided error delegate which, if provided, is used to transform errors and potentially - /// pack errors into trailers. - @usableFromInline - internal let errorDelegate: ServerErrorDelegate? - - /// A logger. - @usableFromInline - internal let logger: Logger - - /// A reference to the user info. This is shared with the interceptor pipeline and may be accessed - /// from the async call context. `UserInfo` is _not_ `Sendable` and must always be accessed from - /// an appropriate event loop. - @usableFromInline - internal let userInfoRef: Ref - - /// Whether compression is enabled on the server and an algorithm has been negotiated with - /// the client - @usableFromInline - internal let compressionEnabledOnRPC: Bool - - /// Whether the RPC method would like to compress responses (if possible). Defaults to true. - @usableFromInline - internal var compressResponsesIfPossible: Bool - - /// The interceptor pipeline does not track flushing as a separate event. The flush decision is - /// included with metadata alongside each message. For the status and trailers the flush is - /// implicit. For headers we track whether to flush here. - /// - /// In most cases the flush will be delayed until the first message is flushed and this will - /// remain unset. However, this may be set when the server handler - /// uses ``GRPCAsyncServerCallContext/sendHeaders(_:)``. - @usableFromInline - internal var flushNextHeaders: Bool - - /// A state machine for the interceptor pipeline. - @usableFromInline - internal private(set) var interceptorStateMachine: ServerInterceptorStateMachine - /// The interceptor pipeline. - @usableFromInline - internal private(set) var interceptors: Optional> - /// An object for writing intercepted responses to the channel. - @usableFromInline - internal private(set) var responseWriter: Optional - - /// A state machine for the user implemented function. - @usableFromInline - internal private(set) var handlerStateMachine: ServerHandlerStateMachine - /// A bag of components used by the user handler. - @usableFromInline - internal private(set) var handlerComponents: - Optional< - ServerHandlerComponents< - Request, - Response, - GRPCAsyncWriterSinkDelegate<(Response, Compression)> - > - > - - /// The user provided function to execute. - @usableFromInline - internal let userHandler: - @Sendable ( - GRPCAsyncRequestStream, - GRPCAsyncResponseStreamWriter, - GRPCAsyncServerCallContext - ) async throws -> Void - - @usableFromInline - internal typealias AsyncSequenceProducer = NIOThrowingAsyncSequenceProducer< - Request, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - > - - @inlinable - internal init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - callType: GRPCCallType, - interceptors: [ServerInterceptor], - userHandler: @escaping @Sendable ( - GRPCAsyncRequestStream, - GRPCAsyncResponseStreamWriter, - GRPCAsyncServerCallContext - ) async throws -> Void - ) { - self.serializer = responseSerializer - self.deserializer = requestDeserializer - self.eventLoop = context.eventLoop - self.allocator = context.allocator - self.responseWriter = context.responseWriter - self.errorDelegate = context.errorDelegate - self.compressionEnabledOnRPC = context.encoding.isEnabled - self.compressResponsesIfPossible = true - self.flushNextHeaders = false - self.logger = context.logger - - self.userInfoRef = Ref(UserInfo()) - self.handlerStateMachine = .init() - self.handlerComponents = nil - - self.userHandler = userHandler - - self.interceptorStateMachine = .init() - self.interceptors = nil - self.interceptors = ServerInterceptorPipeline( - logger: context.logger, - eventLoop: context.eventLoop, - path: context.path, - callType: callType, - remoteAddress: context.remoteAddress, - userInfoRef: self.userInfoRef, - closeFuture: context.closeFuture, - interceptors: interceptors, - onRequestPart: self.receiveInterceptedPart(_:), - onResponsePart: self.sendInterceptedPart(_:promise:) - ) - } - - // MARK: - GRPCServerHandlerProtocol conformance - - @inlinable - internal func receiveMetadata(_ headers: HPACKHeaders) { - switch self.interceptorStateMachine.interceptRequestMetadata() { - case .intercept: - self.interceptors?.receive(.metadata(headers)) - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - } - - @inlinable - internal func receiveMessage(_ bytes: ByteBuffer) { - let request: Request - - do { - request = try self.deserializer.deserialize(byteBuffer: bytes) - } catch { - return self.cancel(error: error) - } - - switch self.interceptorStateMachine.interceptRequestMessage() { - case .intercept: - self.interceptors?.receive(.message(request)) - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - } - - @inlinable - internal func receiveEnd() { - switch self.interceptorStateMachine.interceptRequestEnd() { - case .intercept: - self.interceptors?.receive(.end) - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - } - - @inlinable - internal func receiveError(_ error: Error) { - self.cancel(error: error) - } - - @inlinable - internal func finish() { - self.cancel(error: nil) - } - - @usableFromInline - internal func cancel(error: Error?) { - self.eventLoop.assertInEventLoop() - - switch self.handlerStateMachine.cancel() { - case .cancelAndNilOutHandlerComponents: - // Cancel handler related things (task, response writer). - self.handlerComponents?.cancel() - self.handlerComponents = nil - - // We don't distinguish between having sent the status or not; we just tell the interceptor - // state machine that we want to send a response status. It will inform us whether to - // generate and send one or not. - switch self.interceptorStateMachine.interceptedResponseStatus() { - case .forward: - let error = error ?? GRPCStatus.processingError - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.errorDelegate - ) - self.responseWriter?.sendEnd(status: status, trailers: trailers, promise: nil) - case .drop, .cancel: - () - } - - case .none: - () - } - - switch self.interceptorStateMachine.cancel() { - case .sendStatusThenNilOutInterceptorPipeline: - self.responseWriter?.sendEnd(status: .processingError, trailers: [:], promise: nil) - fallthrough - case .nilOutInterceptorPipeline: - self.interceptors = nil - self.responseWriter = nil - case .none: - () - } - } - - // MARK: - Interceptors to User Function - - @inlinable - internal func receiveInterceptedPart(_ part: GRPCServerRequestPart) { - switch part { - case let .metadata(headers): - self.receiveInterceptedMetadata(headers) - case let .message(message): - self.receiveInterceptedMessage(message) - case .end: - self.receiveInterceptedEnd() - } - } - - @inlinable - internal func receiveInterceptedMetadata(_ headers: HPACKHeaders) { - switch self.interceptorStateMachine.interceptedRequestMetadata() { - case .forward: - () // continue - case .cancel: - return self.cancel(error: nil) - case .drop: - return - } - - switch self.handlerStateMachine.handleMetadata() { - case .invokeHandler: - // We're going to invoke the handler. We need to create a handful of things in order to do - // that: - // - // - A context which allows the handler to set response headers/trailers and provides them - // with a logger amongst other things. - // - A request source; we push request messages into this which the handler consumes via - // an async sequence. - // - An async writer and delegate. The delegate calls us back with responses. The writer is - // passed to the handler. - // - // All of these components are held in a bundle ("handler components") outside of the state - // machine. We release these when we eventually call cancel (either when we `self.cancel()` - // as a result of an error or when `self.finish()` is called). - let handlerContext = GRPCAsyncServerCallContext( - headers: headers, - logger: self.logger, - contextProvider: self - ) - - let backpressureStrategy = NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark( - lowWatermark: 10, - highWatermark: 50 - ) - let requestSequenceProducer = NIOThrowingAsyncSequenceProducer.makeSequence( - elementType: Request.self, - failureType: Error.self, - backPressureStrategy: backpressureStrategy, - delegate: GRPCAsyncSequenceProducerDelegate() - ) - - let responseWriter = NIOAsyncWriter.makeWriter( - isWritable: true, - delegate: GRPCAsyncWriterSinkDelegate<(Response, Compression)>( - didYield: self.interceptResponseMessages, - didTerminate: { error in - self.interceptTermination(error) - } - ) - ) - - // Update our state before invoke the handler. - self.handlerStateMachine.handlerInvoked(requestHeaders: headers) - self.handlerComponents = ServerHandlerComponents< - Request, - Response, - GRPCAsyncWriterSinkDelegate<(Response, Compression)> - >( - requestSource: requestSequenceProducer.source, - responseWriterSink: responseWriter.sink, - task: Task { - // We don't have a task cancellation handler here: we do it in `self.cancel()`. - await self.invokeUserHandler( - requestSequence: requestSequenceProducer, - responseWriter: responseWriter.writer, - callContext: handlerContext - ) - } - ) - - case .cancel: - self.cancel(error: nil) - } - } - - @Sendable - @usableFromInline - internal func invokeUserHandler( - requestSequence: AsyncSequenceProducer.NewSequence, - responseWriter: NIOAsyncWriter< - (Response, Compression), - GRPCAsyncWriterSinkDelegate<(Response, Compression)> - >, - callContext: GRPCAsyncServerCallContext - ) async { - defer { - // It's possible the user handler completed before the end of the request stream. We - // explicitly finish it to drop any unconsumed inbound messages. - requestSequence.source.finish() - } - - do { - let grpcRequestStream = GRPCAsyncRequestStream(requestSequence.sequence) - let grpcResponseStreamWriter = GRPCAsyncResponseStreamWriter(wrapping: responseWriter) - try await self.userHandler(grpcRequestStream, grpcResponseStreamWriter, callContext) - - responseWriter.finish() - } catch { - responseWriter.finish(error: error) - } - } - - @inlinable - internal func receiveInterceptedMessage(_ request: Request) { - switch self.interceptorStateMachine.interceptedRequestMessage() { - case .forward: - switch self.handlerStateMachine.handleMessage() { - case .forward: - _ = self.handlerComponents?.requestSource.yield(request) - case .cancel: - self.cancel(error: nil) - } - - case .cancel: - self.cancel(error: nil) - - case .drop: - () - } - } - - @inlinable - internal func receiveInterceptedEnd() { - switch self.interceptorStateMachine.interceptedRequestEnd() { - case .forward: - switch self.handlerStateMachine.handleEnd() { - case .forward: - self.handlerComponents?.requestSource.finish() - case .cancel: - self.cancel(error: nil) - } - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - } - - // MARK: - User Function To Interceptors - - @inlinable - internal func _interceptResponseMessage(_ response: Response, compression: Compression) { - self.eventLoop.assertInEventLoop() - - switch self.handlerStateMachine.sendMessage() { - case let .intercept(.some(headers)): - switch self.interceptorStateMachine.interceptResponseMetadata() { - case .intercept: - self.interceptors?.send(.metadata(headers), promise: nil) - case .cancel: - return self.cancel(error: nil) - case .drop: - () - } - // Fall through to the next case to send the response message. - fallthrough - - case .intercept(.none): - switch self.interceptorStateMachine.interceptResponseMessage() { - case .intercept: - let senderWantsCompression = compression.isEnabled( - callDefault: self.compressResponsesIfPossible - ) - - let compress = self.compressionEnabledOnRPC && senderWantsCompression - - let metadata = MessageMetadata(compress: compress, flush: true) - self.interceptors?.send(.message(response, metadata), promise: nil) - case .cancel: - return self.cancel(error: nil) - case .drop: - () - } - - case .drop: - () - } - } - - @Sendable - @inlinable - internal func interceptResponseMessages(_ messages: Deque<(Response, Compression)>) { - if self.eventLoop.inEventLoop { - for message in messages { - self._interceptResponseMessage(message.0, compression: message.1) - } - } else { - self.eventLoop.execute { - for message in messages { - self._interceptResponseMessage(message.0, compression: message.1) - } - } - } - } - - @inlinable - internal func _interceptTermination(_ error: Error?) { - self.eventLoop.assertInEventLoop() - - let processedError: Error? - if let thrownStatus = error as? GRPCStatus, thrownStatus.isOk { - processedError = GRPCStatus( - code: .unknown, - message: "Handler threw error with status code 'ok'." - ) - } else { - processedError = error - } - - switch self.handlerStateMachine.sendStatus() { - case let .intercept(requestHeaders, trailers): - let status: GRPCStatus - let processedTrailers: HPACKHeaders - - if let processedError = processedError { - (status, processedTrailers) = ServerErrorProcessor.processObserverError( - processedError, - headers: requestHeaders, - trailers: trailers, - delegate: self.errorDelegate - ) - } else { - status = GRPCStatus.ok - processedTrailers = trailers - } - - switch self.interceptorStateMachine.interceptResponseStatus() { - case .intercept: - self.interceptors?.send(.end(status, processedTrailers), promise: nil) - case .cancel: - return self.cancel(error: nil) - case .drop: - () - } - - case .drop: - () - } - } - - @Sendable - @inlinable - internal func interceptTermination(_ status: Error?) { - if self.eventLoop.inEventLoop { - self._interceptTermination(status) - } else { - self.eventLoop.execute { - self._interceptTermination(status) - } - } - } - - @inlinable - internal func sendInterceptedPart( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - switch part { - case let .metadata(headers): - self.sendInterceptedMetadata(headers, promise: promise) - - case let .message(message, metadata): - do { - let bytes = try self.serializer.serialize(message, allocator: ByteBufferAllocator()) - self.sendInterceptedResponse(bytes, metadata: metadata, promise: promise) - } catch { - promise?.fail(error) - self.cancel(error: error) - } - - case let .end(status, trailers): - self.sendInterceptedStatus(status, metadata: trailers, promise: promise) - } - } - - @inlinable - internal func sendInterceptedMetadata( - _ metadata: HPACKHeaders, - promise: EventLoopPromise? - ) { - switch self.interceptorStateMachine.interceptedResponseMetadata() { - case .forward: - if let responseWriter = self.responseWriter { - let flush = self.flushNextHeaders - self.flushNextHeaders = false - responseWriter.sendMetadata(metadata, flush: flush, promise: promise) - } else if let promise = promise { - promise.fail(GRPCStatus.processingError) - } - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - } - - @inlinable - internal func sendInterceptedResponse( - _ bytes: ByteBuffer, - metadata: MessageMetadata, - promise: EventLoopPromise? - ) { - switch self.interceptorStateMachine.interceptedResponseMessage() { - case .forward: - if let responseWriter = self.responseWriter { - responseWriter.sendMessage(bytes, metadata: metadata, promise: promise) - } else if let promise = promise { - promise.fail(GRPCStatus.processingError) - } - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - } - - @inlinable - internal func sendInterceptedStatus( - _ status: GRPCStatus, - metadata: HPACKHeaders, - promise: EventLoopPromise? - ) { - switch self.interceptorStateMachine.interceptedResponseStatus() { - case .forward: - if let responseWriter = self.responseWriter { - responseWriter.sendEnd(status: status, trailers: metadata, promise: promise) - } else if let promise = promise { - promise.fail(GRPCStatus.processingError) - } - case .cancel: - self.cancel(error: nil) - case .drop: - () - } - } -} - -// Sendability is unchecked as all mutable state is accessed/modified from an appropriate event -// loop. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncServerHandler: @unchecked Sendable {} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncServerHandler: AsyncServerCallContextProvider { - @usableFromInline - internal func setResponseHeaders(_ headers: HPACKHeaders) async throws { - let completed = self.eventLoop.submit { - if !self.handlerStateMachine.setResponseHeaders(headers) { - throw GRPCStatus( - code: .failedPrecondition, - message: "Tried to send response headers in an invalid state" - ) - } - } - try await completed.get() - } - - @usableFromInline - internal func acceptRPC(_ headers: HPACKHeaders) async { - let completed = self.eventLoop.submit { - guard self.handlerStateMachine.setResponseHeaders(headers) else { return } - - // Shh,it's a lie! We don't really have a message to send but the state machine doesn't know - // (or care) about that. It will, however, tell us if we can send the headers or not. - switch self.handlerStateMachine.sendMessage() { - case let .intercept(.some(headers)): - switch self.interceptorStateMachine.interceptResponseMetadata() { - case .intercept: - self.flushNextHeaders = true - self.interceptors?.send(.metadata(headers), promise: nil) - case .cancel: - return self.cancel(error: nil) - case .drop: - () - } - - case .intercept(.none), .drop: - // intercept(.none) means headers have already been sent; we should never hit this because - // we guard on setting the response headers above. - () - } - } - try? await completed.get() - } - - @usableFromInline - internal func setResponseTrailers(_ headers: HPACKHeaders) async throws { - let completed = self.eventLoop.submit { - self.handlerStateMachine.setResponseTrailers(headers) - } - try await completed.get() - } - - @usableFromInline - internal func setResponseCompression(_ enabled: Bool) async throws { - let completed = self.eventLoop.submit { - self.compressResponsesIfPossible = enabled - } - try await completed.get() - } - - @usableFromInline - func withUserInfo( - _ modify: @Sendable @escaping (UserInfo) throws -> Result - ) async throws -> Result { - let result = self.eventLoop.submit { - try modify(self.userInfoRef.value) - } - return try await result.get() - } - - @usableFromInline - func withMutableUserInfo( - _ modify: @Sendable @escaping (inout UserInfo) throws -> Result - ) async throws -> Result { - let result = self.eventLoop.submit { - try modify(&self.userInfoRef.value) - } - return try await result.get() - } -} - -/// This protocol exists so that the generic server handler can be erased from the -/// `GRPCAsyncServerCallContext`. -/// -/// It provides methods which update context on the async handler by first executing onto the -/// correct event loop. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -protocol AsyncServerCallContextProvider: Sendable { - func setResponseHeaders(_ headers: HPACKHeaders) async throws - func acceptRPC(_ headers: HPACKHeaders) async - func setResponseTrailers(_ trailers: HPACKHeaders) async throws - func setResponseCompression(_ enabled: Bool) async throws - - func withUserInfo( - _ modify: @Sendable @escaping (UserInfo) throws -> Result - ) async throws -> Result - - func withMutableUserInfo( - _ modify: @Sendable @escaping (inout UserInfo) throws -> Result - ) async throws -> Result -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -@usableFromInline -internal struct ServerHandlerComponents< - Request: Sendable, - Response: Sendable, - Delegate: NIOAsyncWriterSinkDelegate -> where Delegate.Element == (Response, Compression) { - @usableFromInline - internal typealias AsyncWriterSink = NIOAsyncWriter<(Response, Compression), Delegate>.Sink - - @usableFromInline - internal typealias AsyncSequenceSource = NIOThrowingAsyncSequenceProducer< - Request, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source - - @usableFromInline - internal let task: Task - @usableFromInline - internal let responseWriterSink: AsyncWriterSink - @usableFromInline - internal let requestSource: AsyncSequenceSource - - @inlinable - init( - requestSource: AsyncSequenceSource, - responseWriterSink: AsyncWriterSink, - task: Task - ) { - self.task = task - self.responseWriterSink = responseWriterSink - self.requestSource = requestSource - } - - func cancel() { - // Cancel the request and response streams. - // - // The user handler is encouraged to check for cancellation, however, we should assume - // they do not. Finishing the request source stops any more requests from being delivered - // to the request stream, and finishing the writer sink will ensure no more responses are - // written. This should reduce how long the user handler runs for as it can no longer do - // anything useful. - self.requestSource.finish() - self.responseWriterSink.finish(error: CancellationError()) - self.task.cancel() - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift deleted file mode 100644 index e2879783e..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncServerStreamingCall.swift +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -/// Async-await variant of ``ServerStreamingCall``. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncServerStreamingCall { - private let call: Call - private let responseParts: StreamingResponseParts - private let responseSource: - NIOThrowingAsyncSequenceProducer< - Response, - Error, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source - - /// The stream of responses from the server. - public let responseStream: GRPCAsyncResponseStream - - /// The options used to make the RPC. - public var options: CallOptions { - return self.call.options - } - - /// The path used to make the RPC. - public var path: String { - return self.call.path - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel() { - self.call.cancel(promise: nil) - } - - // MARK: - Response Parts - - private func withRPCCancellation(_ fn: () async throws -> R) async rethrows -> R { - return try await withTaskCancellationHandler(operation: fn) { - self.cancel() - } - } - - /// The initial metadata returned from the server. - /// - /// - Important: The initial metadata will only be available when the first response has been - /// received. However, it is not necessary for the response to have been consumed before reading - /// this property. - public var initialMetadata: HPACKHeaders { - get async throws { - try await self.withRPCCancellation { - try await self.responseParts.initialMetadata.get() - } - } - } - - /// The trailing metadata returned from the server. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var trailingMetadata: HPACKHeaders { - get async throws { - try await self.withRPCCancellation { - try await self.responseParts.trailingMetadata.get() - } - } - } - - /// The final status of the the RPC. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var status: GRPCStatus { - get async { - // force-try acceptable because any error is encapsulated in a successful GRPCStatus future. - await self.withRPCCancellation { - try! await self.responseParts.status.get() - } - } - } - - private init(call: Call) { - self.call = call - // We ignore messages in the closure and instead feed them into the response source when we - // invoke the `call`. - self.responseParts = StreamingResponseParts(on: call.eventLoop) { _ in } - - let backpressureStrategy = NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark( - lowWatermark: 10, - highWatermark: 50 - ) - let sequenceProducer = NIOThrowingAsyncSequenceProducer.makeSequence( - elementType: Response.self, - failureType: Error.self, - backPressureStrategy: backpressureStrategy, - delegate: GRPCAsyncSequenceProducerDelegate() - ) - - self.responseSource = sequenceProducer.source - self.responseStream = .init(sequenceProducer.sequence) - } - - /// We expose this as the only non-private initializer so that the caller - /// knows that invocation is part of initialisation. - internal static func makeAndInvoke( - call: Call, - _ request: Request - ) -> Self { - let asyncCall = Self(call: call) - - asyncCall.call.invokeUnaryRequest( - request, - onStart: {}, - onError: { error in - asyncCall.responseParts.handleError(error) - asyncCall.responseSource.finish(error) - }, - onResponsePart: AsyncCall.makeResponsePartHandler( - responseParts: asyncCall.responseParts, - responseSource: asyncCall.responseSource, - requestStream: nil, - requestType: Request.self - ) - ) - - return asyncCall - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift deleted file mode 100644 index 6a9748f84..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncUnaryCall.swift +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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 NIOHPACK - -/// A unary gRPC call. The request is sent on initialization. -/// -/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore -/// has reference semantics. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct GRPCAsyncUnaryCall: Sendable { - private let call: Call - private let responseParts: UnaryResponseParts - - /// The options used to make the RPC. - public var options: CallOptions { - self.call.options - } - - /// The path used to make the RPC. - public var path: String { - self.call.path - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel() { - self.call.cancel(promise: nil) - } - - // MARK: - Response Parts - - private func withRPCCancellation(_ fn: () async throws -> R) async rethrows -> R { - return try await withTaskCancellationHandler(operation: fn) { - self.cancel() - } - } - - /// The initial metadata returned from the server. - /// - /// - Important: The initial metadata will only be available when the response has been received. - public var initialMetadata: HPACKHeaders { - get async throws { - try await self.withRPCCancellation { - try await self.responseParts.initialMetadata.get() - } - } - } - - /// The response message returned from the service if the call is successful. This may be throw - /// if the call encounters an error. - /// - /// Callers should rely on the `status` of the call for the canonical outcome. - public var response: Response { - get async throws { - try await self.withRPCCancellation { - try await self.responseParts.response.get() - } - } - } - - /// The trailing metadata returned from the server. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var trailingMetadata: HPACKHeaders { - get async throws { - try await self.withRPCCancellation { - try await self.responseParts.trailingMetadata.get() - } - } - } - - /// The final status of the the RPC. - /// - /// - Important: Awaiting this property will suspend until the responses have been consumed. - public var status: GRPCStatus { - get async { - // force-try acceptable because any error is encapsulated in a successful GRPCStatus future. - await self.withRPCCancellation { - try! await self.responseParts.status.get() - } - } - } - - private init( - call: Call, - _ request: Request - ) { - self.call = call - self.responseParts = UnaryResponseParts(on: call.eventLoop) - self.call.invokeUnaryRequest( - request, - onStart: {}, - onError: self.responseParts.handleError(_:), - onResponsePart: self.responseParts.handle(_:) - ) - } - - /// We expose this as the only non-private initializer so that the caller - /// knows that invocation is part of initialisation. - internal static func makeAndInvoke( - call: Call, - _ request: Request - ) -> Self { - Self(call: call, request) - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift deleted file mode 100644 index 9e9b088a2..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCAsyncWriterSinkDelegate.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2022, 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 DequeModule -import NIOCore - -@usableFromInline -internal struct GRPCAsyncWriterSinkDelegate: NIOAsyncWriterSinkDelegate { - @usableFromInline - let _didYield: (@Sendable (Deque) -> Void)? - - @usableFromInline - let _didTerminate: (@Sendable (Error?) -> Void)? - - @inlinable - init( - didYield: (@Sendable (Deque) -> Void)? = nil, - didTerminate: (@Sendable (Error?) -> Void)? = nil - ) { - self._didYield = didYield - self._didTerminate = didTerminate - } - - @inlinable - func didYield(contentsOf sequence: Deque) { - self._didYield?(sequence) - } - - @inlinable - func didTerminate(error: Error?) { - self._didTerminate?(error) - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift deleted file mode 100644 index fff10db18..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCChannel+AsyncAwaitSupport.swift +++ /dev/null @@ -1,223 +0,0 @@ -/* - * 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 SwiftProtobuf - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCChannel { - /// Make a unary gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncUnaryCall< - Request: Message & Sendable, - Response: Message & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncUnaryCall { - return GRPCAsyncUnaryCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .unary, - callOptions: callOptions, - interceptors: interceptors - ), - request - ) - } - - /// Make a unary gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncUnaryCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncUnaryCall { - return GRPCAsyncUnaryCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .unary, - callOptions: callOptions, - interceptors: interceptors - ), - request - ) - } - - /// Makes a client-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncClientStreamingCall< - Request: Message & Sendable, - Response: Message & Sendable - >( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncClientStreamingCall { - return GRPCAsyncClientStreamingCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .clientStreaming, - callOptions: callOptions, - interceptors: interceptors - ) - ) - } - - /// Makes a client-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncClientStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncClientStreamingCall { - return GRPCAsyncClientStreamingCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .clientStreaming, - callOptions: callOptions, - interceptors: interceptors - ) - ) - } - - /// Make a server-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncServerStreamingCall< - Request: Message & Sendable, - Response: Message & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncServerStreamingCall { - return GRPCAsyncServerStreamingCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .serverStreaming, - callOptions: callOptions, - interceptors: interceptors - ), - request - ) - } - - /// Make a server-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncServerStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncServerStreamingCall { - return GRPCAsyncServerStreamingCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .serverStreaming, - callOptions: callOptions, - interceptors: [] - ), - request - ) - } - - /// Makes a bidirectional-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncBidirectionalStreamingCall< - Request: Message & Sendable, - Response: Message & Sendable - >( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncBidirectionalStreamingCall { - return GRPCAsyncBidirectionalStreamingCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .bidirectionalStreaming, - callOptions: callOptions, - interceptors: interceptors - ) - ) - } - - /// Makes a bidirectional-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - internal func makeAsyncBidirectionalStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> GRPCAsyncBidirectionalStreamingCall { - return GRPCAsyncBidirectionalStreamingCall.makeAndInvoke( - call: self.makeCall( - path: path, - type: .bidirectionalStreaming, - callOptions: callOptions, - interceptors: interceptors - ) - ) - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift deleted file mode 100644 index b433d6323..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCClient+AsyncAwaitSupport.swift +++ /dev/null @@ -1,481 +0,0 @@ -/* - * 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 SwiftProtobuf - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCClient { - public func makeAsyncUnaryCall( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> GRPCAsyncUnaryCall { - return self.channel.makeAsyncUnaryCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeAsyncUnaryCall( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> GRPCAsyncUnaryCall { - return self.channel.makeAsyncUnaryCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeAsyncServerStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> GRPCAsyncServerStreamingCall { - return self.channel.makeAsyncServerStreamingCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeAsyncServerStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> GRPCAsyncServerStreamingCall { - return self.channel.makeAsyncServerStreamingCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeAsyncClientStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable - >( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncClientStreamingCall { - return self.channel.makeAsyncClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeAsyncClientStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncClientStreamingCall { - return self.channel.makeAsyncClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeAsyncBidirectionalStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable - >( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.channel.makeAsyncBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeAsyncBidirectionalStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.channel.makeAsyncBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } -} - -// MARK: - "Simple, but safe" wrappers. - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCClient { - public func performAsyncUnaryCall( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) async throws -> Response { - let call = self.channel.makeAsyncUnaryCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - - return try await withTaskCancellationHandler { - try await call.response - } onCancel: { - call.cancel() - } - } - - public func performAsyncUnaryCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) async throws -> Response { - let call = self.channel.makeAsyncUnaryCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - - return try await withTaskCancellationHandler { - try await call.response - } onCancel: { - call.cancel() - } - } - - public func performAsyncServerStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> GRPCAsyncResponseStream { - return self.channel.makeAsyncServerStreamingCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ).responseStream - } - - public func performAsyncServerStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable - >( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> GRPCAsyncResponseStream { - return self.channel.makeAsyncServerStreamingCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ).responseStream - } - - public func performAsyncClientStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable, - RequestStream: AsyncSequence & Sendable - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) async throws -> Response where RequestStream.Element == Request { - let call = self.channel.makeAsyncClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return try await self.perform(call, with: requests) - } - - public func performAsyncClientStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable, - RequestStream: AsyncSequence & Sendable - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) async throws -> Response where RequestStream.Element == Request { - let call = self.channel.makeAsyncClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return try await self.perform(call, with: requests) - } - - public func performAsyncClientStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable, - RequestStream: Sequence - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) async throws -> Response where RequestStream.Element == Request { - let call = self.channel.makeAsyncClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return try await self.perform(call, with: AsyncStream(wrapping: requests)) - } - - public func performAsyncClientStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable, - RequestStream: Sequence - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) async throws -> Response where RequestStream.Element == Request { - let call = self.channel.makeAsyncClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return try await self.perform(call, with: AsyncStream(wrapping: requests)) - } - - public func performAsyncBidirectionalStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable, - RequestStream: AsyncSequence & Sendable - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncResponseStream - where RequestStream.Element == Request { - let call = self.channel.makeAsyncBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return self.perform(call, with: requests) - } - - public func performAsyncBidirectionalStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable, - RequestStream: AsyncSequence & Sendable - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncResponseStream - where RequestStream.Element == Request { - let call = self.channel.makeAsyncBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return self.perform(call, with: requests) - } - - public func performAsyncBidirectionalStreamingCall< - Request: SwiftProtobuf.Message & Sendable, - Response: SwiftProtobuf.Message & Sendable, - RequestStream: Sequence - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncResponseStream where RequestStream.Element == Request { - let call = self.channel.makeAsyncBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return self.perform(call, with: AsyncStream(wrapping: requests)) - } - - public func performAsyncBidirectionalStreamingCall< - Request: GRPCPayload & Sendable, - Response: GRPCPayload & Sendable, - RequestStream: Sequence - >( - path: String, - requests: RequestStream, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> GRPCAsyncResponseStream where RequestStream.Element == Request { - let call = self.channel.makeAsyncBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - return self.perform(call, with: AsyncStream(wrapping: requests)) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension GRPCClient { - @inlinable - internal func perform< - Request: Sendable, - Response: Sendable, - RequestStream: AsyncSequence & Sendable - >( - _ call: GRPCAsyncClientStreamingCall, - with requests: RequestStream - ) async throws -> Response where RequestStream.Element == Request { - return try await withTaskCancellationHandler { - Task { - do { - // `AsyncSequence`s are encouraged to co-operatively check for cancellation, and we will - // cancel the call `onCancel` anyway, so there's no need to check here too. - for try await request in requests { - try await call.requestStream.send(request) - } - call.requestStream.finish() - } catch { - // If we throw then cancel the call. We will rely on the response throwing an appropriate - // error below. - call.cancel() - } - } - - return try await call.response - } onCancel: { - call.cancel() - } - } - - @inlinable - internal func perform< - Request: Sendable, - Response: Sendable, - RequestStream: AsyncSequence & Sendable - >( - _ call: GRPCAsyncBidirectionalStreamingCall, - with requests: RequestStream - ) -> GRPCAsyncResponseStream where RequestStream.Element == Request { - Task { - do { - try await withTaskCancellationHandler { - // `AsyncSequence`s are encouraged to co-operatively check for cancellation, and we will - // cancel the call `onCancel` anyway, so there's no need to check here too. - for try await request in requests { - try await call.requestStream.send(request) - } - call.requestStream.finish() - } onCancel: { - call.cancel() - } - } catch { - call.cancel() - } - } - - return call.responseStream - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension AsyncStream { - /// Create an `AsyncStream` from a regular (non-async) `Sequence`. - /// - /// - Note: This is just here to avoid duplicating the above two `perform(_:with:)` functions - /// for `Sequence`. - fileprivate init(wrapping sequence: T) where T: Sequence, T.Element == Element { - self.init { continuation in - var iterator = sequence.makeIterator() - while let value = iterator.next() { - continuation.yield(value) - } - continuation.finish() - } - } -} diff --git a/Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift b/Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift deleted file mode 100644 index a94525a48..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/GRPCSendable.swift +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2022, 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 NIOCore - -@available(*, deprecated, renamed: "Swift.Sendable") -public typealias GRPCSendable = Swift.Sendable - -@preconcurrency -public protocol GRPCPreconcurrencySendable: Sendable {} - -@preconcurrency public typealias GRPCChannelInitializer = @Sendable (Channel) - -> EventLoopFuture diff --git a/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift b/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift deleted file mode 100644 index 651a16151..000000000 --- a/Sources/GRPC/AsyncAwaitSupport/NIOAsyncWrappers.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2022, 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 NIOCore - -/// Unchecked-sendable wrapper for ``NIOAsyncWriter/Sink``, to avoid getting sendability warnings. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct AsyncSink: @unchecked Sendable { - private let sink: - NIOAsyncWriter< - Element, - GRPCAsyncWriterSinkDelegate - >.Sink - - @inlinable - init( - wrapping sink: NIOAsyncWriter< - Element, - GRPCAsyncWriterSinkDelegate - >.Sink - ) { - self.sink = sink - } - - @inlinable - func setWritability(to writability: Bool) { - self.sink.setWritability(to: writability) - } - - @inlinable - func finish(error: Error) { - self.sink.finish(error: error) - } - - @inlinable - func finish() { - self.sink.finish() - } -} diff --git a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift deleted file mode 100644 index 6a9736cdc..000000000 --- a/Sources/GRPC/CallHandlers/BidirectionalStreamingServerHandler.swift +++ /dev/null @@ -1,383 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -public final class BidirectionalStreamingServerHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer ->: GRPCServerHandlerProtocol { - public typealias Request = Deserializer.Output - public typealias Response = Serializer.Input - - /// A response serializer. - @usableFromInline - internal let serializer: Serializer - - /// A request deserializer. - @usableFromInline - internal let deserializer: Deserializer - - /// A pipeline of user provided interceptors. - @usableFromInline - internal var interceptors: ServerInterceptorPipeline! - - /// Stream events which have arrived before the stream observer future has been resolved. - @usableFromInline - internal var requestBuffer: CircularBuffer> = CircularBuffer() - - /// The context required in order create the function. - @usableFromInline - internal let context: CallHandlerContext - - /// A reference to a `UserInfo`. - @usableFromInline - internal let userInfoRef: Ref - - /// The user provided function to execute. - @usableFromInline - internal let observerFactory: - (_StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> - - /// The state of the handler. - @usableFromInline - internal var state: State = .idle - - @usableFromInline - internal enum State { - // No headers have been received. - case idle - // Headers have been received, a context has been created and the user code has been called to - // make a stream observer with. The observer is yet to see any messages. - case creatingObserver(_StreamingResponseCallContext) - // The observer future has resolved and the observer may have seen messages. - case observing((StreamEvent) -> Void, _StreamingResponseCallContext) - // The observer has completed by completing the status promise. - case completed - } - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - observerFactory: @escaping (StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> - ) { - self.serializer = responseSerializer - self.deserializer = requestDeserializer - self.context = context - self.observerFactory = observerFactory - - let userInfoRef = Ref(UserInfo()) - self.userInfoRef = userInfoRef - self.interceptors = ServerInterceptorPipeline( - logger: context.logger, - eventLoop: context.eventLoop, - path: context.path, - callType: .bidirectionalStreaming, - remoteAddress: context.remoteAddress, - userInfoRef: userInfoRef, - closeFuture: context.closeFuture, - interceptors: interceptors, - onRequestPart: self.receiveInterceptedPart(_:), - onResponsePart: self.sendInterceptedPart(_:promise:) - ) - } - - // MARK: - Public API: gRPC to Handler - - @inlinable - public func receiveMetadata(_ headers: HPACKHeaders) { - self.interceptors.receive(.metadata(headers)) - } - - @inlinable - public func receiveMessage(_ bytes: ByteBuffer) { - do { - let message = try self.deserializer.deserialize(byteBuffer: bytes) - self.interceptors.receive(.message(message)) - } catch { - self.handleError(error) - } - } - - @inlinable - public func receiveEnd() { - self.interceptors.receive(.end) - } - - @inlinable - public func receiveError(_ error: Error) { - self.handleError(error) - self.finish() - } - - @inlinable - public func finish() { - switch self.state { - case .idle: - self.interceptors = nil - self.state = .completed - - case let .creatingObserver(context), - let .observing(_, context): - context.statusPromise.fail(GRPCStatus(code: .unavailable, message: nil)) - self.context.eventLoop.execute { - self.interceptors = nil - } - - case .completed: - self.interceptors = nil - } - } - - // MARK: - Interceptors to User Function - - @inlinable - internal func receiveInterceptedPart(_ part: GRPCServerRequestPart) { - switch part { - case let .metadata(headers): - self.receiveInterceptedMetadata(headers) - case let .message(message): - self.receiveInterceptedMessage(message) - case .end: - self.receiveInterceptedEnd() - } - } - - @inlinable - internal func receiveInterceptedMetadata(_ headers: HPACKHeaders) { - switch self.state { - case .idle: - // Make a context to invoke the observer block factory with. - let context = _StreamingResponseCallContext( - eventLoop: self.context.eventLoop, - headers: headers, - logger: self.context.logger, - userInfoRef: self.userInfoRef, - compressionIsEnabled: self.context.encoding.isEnabled, - closeFuture: self.context.closeFuture, - sendResponse: self.interceptResponse(_:metadata:promise:) - ) - - // Move to the next state. - self.state = .creatingObserver(context) - - // Send response headers back via the interceptors. - self.interceptors.send(.metadata([:]), promise: nil) - - // Register callbacks on the status future. - context.statusPromise.futureResult.whenComplete(self.userFunctionStatusResolved(_:)) - - // Make an observer block and register a completion block. - self.observerFactory(context).whenComplete(self.userFunctionResolvedWithResult(_:)) - - case .creatingObserver, .observing: - self.handleError(GRPCError.ProtocolViolation("Multiple header blocks received on RPC")) - - case .completed: - // We may receive headers from the interceptor pipeline if we have already finished (i.e. due - // to an error or otherwise) and an interceptor doing some async work later emitting headers. - // Dropping them is fine. - () - } - } - - @inlinable - internal func receiveInterceptedMessage(_ request: Request) { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("Message received before headers")) - case .creatingObserver: - self.requestBuffer.append(.message(request)) - case let .observing(observer, _): - observer(.message(request)) - case .completed: - // We received a message but we're already done: this may happen if we terminate the RPC - // due to a channel error, for example. - () - } - } - - @inlinable - internal func receiveInterceptedEnd() { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("End of stream received before headers")) - case .creatingObserver: - self.requestBuffer.append(.end) - case let .observing(observer, _): - observer(.end) - case .completed: - // We received a message but we're already done: this may happen if we terminate the RPC - // due to a channel error, for example. - () - } - } - - // MARK: - User Function To Interceptors - - @inlinable - internal func userFunctionResolvedWithResult( - _ result: Result<(StreamEvent) -> Void, Error> - ) { - switch self.state { - case .idle, .observing: - // The observer block can't resolve if it hasn't been created ('idle') and it can't be - // resolved more than once ('observing'). - preconditionFailure() - - case let .creatingObserver(context): - switch result { - case let .success(observer): - // We have an observer block now; unbuffer any requests. - self.state = .observing(observer, context) - while let request = self.requestBuffer.popFirst() { - observer(request) - } - - case let .failure(error): - self.handleError(error, thrownFromHandler: true) - } - - case .completed: - // We've already completed. That's fine. - () - } - } - - @inlinable - internal func interceptResponse( - _ response: Response, - metadata: MessageMetadata, - promise: EventLoopPromise? - ) { - switch self.state { - case .idle: - // The observer block can't end responses if it doesn't exist! - preconditionFailure() - - case .creatingObserver, .observing: - // The user has access to the response context before returning a future observer, - // so 'creatingObserver' is valid here (if a little strange). - self.interceptors.send(.message(response, metadata), promise: promise) - - case .completed: - promise?.fail(GRPCError.AlreadyComplete()) - } - } - - @inlinable - internal func userFunctionStatusResolved(_ result: Result) { - switch self.state { - case .idle: - // The promise can't fail before we create it. - preconditionFailure() - - // Making is possible, the user can complete the status before returning a stream handler. - case let .creatingObserver(context), let .observing(_, context): - switch result { - case let .success(status): - // We're sending end back, we're done. - self.state = .completed - self.interceptors.send(.end(status, context.trailers), promise: nil) - - case let .failure(error): - self.handleError(error, thrownFromHandler: true) - } - - case .completed: - () - } - } - - @inlinable - internal func handleError(_ error: Error, thrownFromHandler isHandlerError: Bool = false) { - switch self.state { - case .idle: - assert(!isHandlerError) - self.state = .completed - // We don't have a promise to fail. Just send back end. - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - self.interceptors.send(.end(status, trailers), promise: nil) - - case let .creatingObserver(context), - let .observing(_, context): - // We don't have a promise to fail. Just send back end. - self.state = .completed - - let status: GRPCStatus - let trailers: HPACKHeaders - - if isHandlerError { - (status, trailers) = ServerErrorProcessor.processObserverError( - error, - headers: context.headers, - trailers: context.trailers, - delegate: self.context.errorDelegate - ) - } else { - (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - } - - self.interceptors.send(.end(status, trailers), promise: nil) - // We're already in the 'completed' state so failing the promise will be a no-op in the - // callback to 'userHandlerCompleted' (but we also need to avoid leaking the promise.) - context.statusPromise.fail(error) - - case .completed: - () - } - } - - @inlinable - internal func sendInterceptedPart( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - switch part { - case let .metadata(headers): - self.context.responseWriter.sendMetadata(headers, flush: true, promise: promise) - - case let .message(message, metadata): - do { - let bytes = try self.serializer.serialize(message, allocator: ByteBufferAllocator()) - self.context.responseWriter.sendMessage(bytes, metadata: metadata, promise: promise) - } catch { - // Serialization failed: fail the promise and send end. - promise?.fail(error) - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - // Loop back via the interceptors. - self.interceptors.send(.end(status, trailers), promise: nil) - } - - case let .end(status, trailers): - self.context.responseWriter.sendEnd(status: status, trailers: trailers, promise: promise) - } - } -} diff --git a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift deleted file mode 100644 index 009b690f3..000000000 --- a/Sources/GRPC/CallHandlers/ClientStreamingServerHandler.swift +++ /dev/null @@ -1,369 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -public final class ClientStreamingServerHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer ->: GRPCServerHandlerProtocol { - public typealias Request = Deserializer.Output - public typealias Response = Serializer.Input - - /// A response serializer. - @usableFromInline - internal let serializer: Serializer - - /// A request deserializer. - @usableFromInline - internal let deserializer: Deserializer - - /// A pipeline of user provided interceptors. - @usableFromInline - internal var interceptors: ServerInterceptorPipeline! - - /// Stream events which have arrived before the stream observer future has been resolved. - @usableFromInline - internal var requestBuffer: CircularBuffer> = CircularBuffer() - - /// The context required in order create the function. - @usableFromInline - internal let context: CallHandlerContext - - /// A reference to a ``UserInfo``. - @usableFromInline - internal let userInfoRef: Ref - - /// The user provided function to execute. - @usableFromInline - internal let handlerFactory: - (UnaryResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> - - /// The state of the handler. - @usableFromInline - internal var state: State = .idle - - @usableFromInline - internal enum State { - // Nothing has happened yet. - case idle - // Headers have been received, a context has been created and the user code has been called to - // make an observer with. The observer future hasn't completed yet and, as such, the observer - // is yet to see any events. - case creatingObserver(UnaryResponseCallContext) - // The observer future has succeeded, messages may have been delivered to it. - case observing((StreamEvent) -> Void, UnaryResponseCallContext) - // The observer has completed by completing the status promise. - case completed - } - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - observerFactory: @escaping (UnaryResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> - ) { - self.serializer = responseSerializer - self.deserializer = requestDeserializer - self.context = context - self.handlerFactory = observerFactory - - let userInfoRef = Ref(UserInfo()) - self.userInfoRef = userInfoRef - self.interceptors = ServerInterceptorPipeline( - logger: context.logger, - eventLoop: context.eventLoop, - path: context.path, - callType: .clientStreaming, - remoteAddress: context.remoteAddress, - userInfoRef: userInfoRef, - closeFuture: context.closeFuture, - interceptors: interceptors, - onRequestPart: self.receiveInterceptedPart(_:), - onResponsePart: self.sendInterceptedPart(_:promise:) - ) - } - - // MARK: Public API; gRPC to Handler - - @inlinable - public func receiveMetadata(_ headers: HPACKHeaders) { - self.interceptors.receive(.metadata(headers)) - } - - @inlinable - public func receiveMessage(_ bytes: ByteBuffer) { - do { - let message = try self.deserializer.deserialize(byteBuffer: bytes) - self.interceptors.receive(.message(message)) - } catch { - self.handleError(error) - } - } - - @inlinable - public func receiveEnd() { - self.interceptors.receive(.end) - } - - @inlinable - public func receiveError(_ error: Error) { - self.handleError(error) - self.finish() - } - - @inlinable - public func finish() { - switch self.state { - case .idle: - self.interceptors = nil - self.state = .completed - - case let .creatingObserver(context), - let .observing(_, context): - context.responsePromise.fail(GRPCStatus(code: .unavailable, message: nil)) - self.context.eventLoop.execute { - self.interceptors = nil - } - - case .completed: - self.interceptors = nil - } - } - - // MARK: Interceptors to User Function - - @inlinable - internal func receiveInterceptedPart(_ part: GRPCServerRequestPart) { - switch part { - case let .metadata(headers): - self.receiveInterceptedMetadata(headers) - case let .message(message): - self.receiveInterceptedMessage(message) - case .end: - self.receiveInterceptedEnd() - } - } - - @inlinable - internal func receiveInterceptedMetadata(_ headers: HPACKHeaders) { - switch self.state { - case .idle: - // Make a context to invoke the observer block factory with. - let context = UnaryResponseCallContext( - eventLoop: self.context.eventLoop, - headers: headers, - logger: self.context.logger, - userInfoRef: self.userInfoRef, - closeFuture: self.context.closeFuture - ) - - // Move to the next state. - self.state = .creatingObserver(context) - - // Register a callback on the response future. - context.responsePromise.futureResult.whenComplete(self.userFunctionCompletedWithResult(_:)) - - // Make an observer block and register a completion block. - self.handlerFactory(context).whenComplete(self.userFunctionResolved(_:)) - - // Send response headers back via the interceptors. - self.interceptors.send(.metadata([:]), promise: nil) - - case .creatingObserver, .observing: - self.handleError(GRPCError.ProtocolViolation("Multiple header blocks received")) - - case .completed: - // We may receive headers from the interceptor pipeline if we have already finished (i.e. due - // to an error or otherwise) and an interceptor doing some async work later emitting headers. - // Dropping them is fine. - () - } - } - - @inlinable - internal func receiveInterceptedMessage(_ request: Request) { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("Message received before headers")) - case .creatingObserver: - self.requestBuffer.append(.message(request)) - case let .observing(observer, _): - observer(.message(request)) - case .completed: - // We received a message but we're already done: this may happen if we terminate the RPC - // due to a channel error, for example. - () - } - } - - @inlinable - internal func receiveInterceptedEnd() { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("end received before headers")) - case .creatingObserver: - self.requestBuffer.append(.end) - case let .observing(observer, _): - observer(.end) - case .completed: - // We received a message but we're already done: this may happen if we terminate the RPC - // due to a channel error, for example. - () - } - } - - // MARK: User Function to Interceptors - - @inlinable - internal func userFunctionResolved(_ result: Result<(StreamEvent) -> Void, Error>) { - switch self.state { - case .idle, .observing: - // The observer block can't resolve if it hasn't been created ('idle') and it can't be - // resolved more than once ('created'). - preconditionFailure() - - case let .creatingObserver(context): - switch result { - case let .success(observer): - // We have an observer block now; unbuffer any requests. - self.state = .observing(observer, context) - while let request = self.requestBuffer.popFirst() { - observer(request) - } - - case let .failure(error): - self.handleError(error, thrownFromHandler: true) - } - - case .completed: - // We've already completed. That's fine. - () - } - } - - @inlinable - internal func userFunctionCompletedWithResult(_ result: Result) { - switch self.state { - case .idle: - // Invalid state: the user function can only complete if it exists.. - preconditionFailure() - - case let .creatingObserver(context), - let .observing(_, context): - switch result { - case let .success(response): - // Complete when we send end. - self.state = .completed - - // Compression depends on whether it's enabled on the server and the setting in the caller - // context. - let compress = self.context.encoding.isEnabled && context.compressionEnabled - let metadata = MessageMetadata(compress: compress, flush: false) - self.interceptors.send(.message(response, metadata), promise: nil) - self.interceptors.send(.end(context.responseStatus, context.trailers), promise: nil) - - case let .failure(error): - self.handleError(error, thrownFromHandler: true) - } - - case .completed: - // We've already completed. Ignore this. - () - } - } - - @inlinable - internal func handleError(_ error: Error, thrownFromHandler isHandlerError: Bool = false) { - switch self.state { - case .idle: - assert(!isHandlerError) - self.state = .completed - // We don't have a promise to fail. Just send back end. - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - self.interceptors.send(.end(status, trailers), promise: nil) - - case let .creatingObserver(context), - let .observing(_, context): - // We don't have a promise to fail. Just send back end. - self.state = .completed - - let status: GRPCStatus - let trailers: HPACKHeaders - - if isHandlerError { - (status, trailers) = ServerErrorProcessor.processObserverError( - error, - headers: context.headers, - trailers: context.trailers, - delegate: self.context.errorDelegate - ) - } else { - (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - } - - self.interceptors.send(.end(status, trailers), promise: nil) - // We're already in the 'completed' state so failing the promise will be a no-op in the - // callback to 'userFunctionCompletedWithResult' (but we also need to avoid leaking the - // promise.) - context.responsePromise.fail(error) - - case .completed: - () - } - } - - // MARK: Interceptor Glue - - @inlinable - internal func sendInterceptedPart( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - switch part { - case let .metadata(headers): - self.context.responseWriter.sendMetadata(headers, flush: true, promise: promise) - - case let .message(message, metadata): - do { - let bytes = try self.serializer.serialize(message, allocator: ByteBufferAllocator()) - self.context.responseWriter.sendMessage(bytes, metadata: metadata, promise: promise) - } catch { - // Serialization failed: fail the promise and send end. - promise?.fail(error) - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - // Loop back via the interceptors. - self.interceptors.send(.end(status, trailers), promise: nil) - } - - case let .end(status, trailers): - self.context.responseWriter.sendEnd(status: status, trailers: trailers, promise: promise) - } - } -} diff --git a/Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift b/Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift deleted file mode 100644 index 8c3f68796..000000000 --- a/Sources/GRPC/CallHandlers/ServerHandlerProtocol.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -/// This protocol lays out the inbound interface between the gRPC module and generated server code. -/// On receiving a new RPC, gRPC will ask all available service providers for an instance of this -/// protocol in order to handle the RPC. -/// -/// See also: ``CallHandlerProvider/handle(method:context:)``. -public protocol GRPCServerHandlerProtocol { - /// Called when request headers have been received at the start of an RPC. - /// - Parameter metadata: The request headers. - func receiveMetadata(_ metadata: HPACKHeaders) - - /// Called when request message has been received. - /// - Parameter bytes: The bytes of the serialized request. - func receiveMessage(_ bytes: ByteBuffer) - - /// Called at the end of the request stream. - func receiveEnd() - - /// Called when an error has been encountered. The handler should be torn down on receiving an - /// error. - /// - Parameter error: The error which has been encountered. - func receiveError(_ error: Error) - - /// Called when the RPC handler should be torn down. - func finish() -} - -/// This protocol defines the outbound interface between the gRPC module and generated server code. -/// It is used by server handlers in order to send responses back to gRPC. -@usableFromInline -internal protocol GRPCServerResponseWriter { - /// Send the initial response metadata. - /// - Parameters: - /// - metadata: The user-provided metadata to send to the client. - /// - flush: Whether a flush should be emitted after writing the metadata. - /// - promise: A promise to complete once the metadata has been handled. - func sendMetadata(_ metadata: HPACKHeaders, flush: Bool, promise: EventLoopPromise?) - - /// Send the serialized bytes of a response message. - /// - Parameters: - /// - bytes: The serialized bytes to send to the client. - /// - metadata: Metadata associated with sending the response, such as whether it should be - /// compressed. - /// - promise: A promise to complete once the message as been handled. - func sendMessage(_ bytes: ByteBuffer, metadata: MessageMetadata, promise: EventLoopPromise?) - - /// Ends the response stream. - /// - Parameters: - /// - status: The final status of the RPC. - /// - trailers: Any user-provided trailers to send back to the client with the status. - /// - promise: A promise to complete once the status and trailers have been handled. - func sendEnd(status: GRPCStatus, trailers: HPACKHeaders, promise: EventLoopPromise?) -} diff --git a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift b/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift deleted file mode 100644 index 3c930b8d4..000000000 --- a/Sources/GRPC/CallHandlers/ServerStreamingServerHandler.swift +++ /dev/null @@ -1,355 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -public final class ServerStreamingServerHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer ->: GRPCServerHandlerProtocol { - public typealias Request = Deserializer.Output - public typealias Response = Serializer.Input - - /// A response serializer. - @usableFromInline - internal let serializer: Serializer - - /// A request deserializer. - @usableFromInline - internal let deserializer: Deserializer - - /// A pipeline of user provided interceptors. - @usableFromInline - internal var interceptors: ServerInterceptorPipeline! - - /// The context required in order create the function. - @usableFromInline - internal let context: CallHandlerContext - - /// A reference to a `UserInfo`. - @usableFromInline - internal let userInfoRef: Ref - - /// The user provided function to execute. - @usableFromInline - internal let userFunction: - (Request, StreamingResponseCallContext) - -> EventLoopFuture - - /// The state of the handler. - @usableFromInline - internal var state: State = .idle - - @usableFromInline - internal enum State { - // Initial state. Nothing has happened yet. - case idle - // Headers have been received and now we're holding a context with which to invoke the user - // function when we receive a message. - case createdContext(_StreamingResponseCallContext) - // The user function has been invoked, we're waiting for the status promise to be completed. - case invokedFunction(_StreamingResponseCallContext) - // The function has completed or we are no longer proceeding with execution (because of an error - // or unexpected closure). - case completed - } - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - userFunction: @escaping (Request, StreamingResponseCallContext) - -> EventLoopFuture - ) { - self.serializer = responseSerializer - self.deserializer = requestDeserializer - self.context = context - self.userFunction = userFunction - - let userInfoRef = Ref(UserInfo()) - self.userInfoRef = userInfoRef - self.interceptors = ServerInterceptorPipeline( - logger: context.logger, - eventLoop: context.eventLoop, - path: context.path, - callType: .serverStreaming, - remoteAddress: context.remoteAddress, - userInfoRef: userInfoRef, - closeFuture: context.closeFuture, - interceptors: interceptors, - onRequestPart: self.receiveInterceptedPart(_:), - onResponsePart: self.sendInterceptedPart(_:promise:) - ) - } - - // MARK: Public API; gRPC to Handler - - @inlinable - public func receiveMetadata(_ headers: HPACKHeaders) { - self.interceptors.receive(.metadata(headers)) - } - - @inlinable - public func receiveMessage(_ bytes: ByteBuffer) { - do { - let message = try self.deserializer.deserialize(byteBuffer: bytes) - self.interceptors.receive(.message(message)) - } catch { - self.handleError(error) - } - } - - @inlinable - public func receiveEnd() { - self.interceptors.receive(.end) - } - - @inlinable - public func receiveError(_ error: Error) { - self.handleError(error) - self.finish() - } - - @inlinable - public func finish() { - switch self.state { - case .idle: - self.interceptors = nil - self.state = .completed - - case let .createdContext(context), - let .invokedFunction(context): - context.statusPromise.fail(GRPCStatus(code: .unavailable, message: nil)) - self.context.eventLoop.execute { - self.interceptors = nil - } - - case .completed: - self.interceptors = nil - } - } - - // MARK: - Interceptors to User Function - - @inlinable - internal func receiveInterceptedPart(_ part: GRPCServerRequestPart) { - switch part { - case let .metadata(headers): - self.receiveInterceptedMetadata(headers) - case let .message(message): - self.receiveInterceptedMessage(message) - case .end: - self.receiveInterceptedEnd() - } - } - - @inlinable - internal func receiveInterceptedMetadata(_ headers: HPACKHeaders) { - switch self.state { - case .idle: - // Make a context to invoke the observer block factory with. - let context = _StreamingResponseCallContext( - eventLoop: self.context.eventLoop, - headers: headers, - logger: self.context.logger, - userInfoRef: self.userInfoRef, - compressionIsEnabled: self.context.encoding.isEnabled, - closeFuture: self.context.closeFuture, - sendResponse: self.interceptResponse(_:metadata:promise:) - ) - - // Move to the next state. - self.state = .createdContext(context) - - // Register a callback on the status future. - context.statusPromise.futureResult.whenComplete(self.userFunctionCompletedWithResult(_:)) - - // Send response headers back via the interceptors. - self.interceptors.send(.metadata([:]), promise: nil) - - case .createdContext, .invokedFunction: - self.handleError(GRPCError.InvalidState("Protocol violation: already received headers")) - - case .completed: - // We may receive headers from the interceptor pipeline if we have already finished (i.e. due - // to an error or otherwise) and an interceptor doing some async work later emitting headers. - // Dropping them is fine. - () - } - } - - @inlinable - internal func receiveInterceptedMessage(_ request: Request) { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("Message received before headers")) - - case let .createdContext(context): - self.state = .invokedFunction(context) - // Complete the status promise with the function outcome. - context.statusPromise.completeWith(self.userFunction(request, context)) - - case .invokedFunction: - let error = GRPCError.ProtocolViolation("Multiple messages received on server streaming RPC") - self.handleError(error) - - case .completed: - // We received a message but we're already done: this may happen if we terminate the RPC - // due to a channel error, for example. - () - } - } - - @inlinable - internal func receiveInterceptedEnd() { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("End received before headers")) - - case .createdContext: - self.handleError(GRPCError.ProtocolViolation("End received before message")) - - case .invokedFunction, .completed: - () - } - } - - // MARK: - User Function To Interceptors - - @inlinable - internal func interceptResponse( - _ response: Response, - metadata: MessageMetadata, - promise: EventLoopPromise? - ) { - switch self.state { - case .idle: - // The observer block can't send responses if it doesn't exist. - preconditionFailure() - - case .createdContext, .invokedFunction: - // The user has access to the response context before returning a future observer, - // so 'createdContext' is valid here (if a little strange). - self.interceptors.send(.message(response, metadata), promise: promise) - - case .completed: - promise?.fail(GRPCError.AlreadyComplete()) - } - } - - @inlinable - internal func userFunctionCompletedWithResult(_ result: Result) { - switch self.state { - case .idle: - // Invalid state: the user function can only completed if it was created. - preconditionFailure() - - case let .createdContext(context), - let .invokedFunction(context): - - switch result { - case let .success(status): - // We're sending end back, we're done. - self.state = .completed - self.interceptors.send(.end(status, context.trailers), promise: nil) - - case let .failure(error): - self.handleError(error, thrownFromHandler: true) - } - - case .completed: - // We've already completed. Ignore this. - () - } - } - - @inlinable - internal func sendInterceptedPart( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - switch part { - case let .metadata(headers): - self.context.responseWriter.sendMetadata(headers, flush: true, promise: promise) - - case let .message(message, metadata): - do { - let bytes = try self.serializer.serialize(message, allocator: self.context.allocator) - self.context.responseWriter.sendMessage(bytes, metadata: metadata, promise: promise) - } catch { - // Serialization failed: fail the promise and send end. - promise?.fail(error) - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - // Loop back via the interceptors. - self.interceptors.send(.end(status, trailers), promise: nil) - } - - case let .end(status, trailers): - self.context.responseWriter.sendEnd(status: status, trailers: trailers, promise: promise) - } - } - - @inlinable - internal func handleError(_ error: Error, thrownFromHandler isHandlerError: Bool = false) { - switch self.state { - case .idle: - assert(!isHandlerError) - self.state = .completed - // We don't have a promise to fail. Just send back end. - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - self.interceptors.send(.end(status, trailers), promise: nil) - - case let .createdContext(context), - let .invokedFunction(context): - // We don't have a promise to fail. Just send back end. - self.state = .completed - - let status: GRPCStatus - let trailers: HPACKHeaders - - if isHandlerError { - (status, trailers) = ServerErrorProcessor.processObserverError( - error, - headers: context.headers, - trailers: context.trailers, - delegate: self.context.errorDelegate - ) - } else { - (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - } - - self.interceptors.send(.end(status, trailers), promise: nil) - // We're already in the 'completed' state so failing the promise will be a no-op in the - // callback to 'userFunctionCompletedWithResult' (but we also need to avoid leaking the - // promise.) - context.statusPromise.fail(error) - - case .completed: - () - } - } -} diff --git a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift b/Sources/GRPC/CallHandlers/UnaryServerHandler.swift deleted file mode 100644 index 5624b601d..000000000 --- a/Sources/GRPC/CallHandlers/UnaryServerHandler.swift +++ /dev/null @@ -1,338 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK - -public final class UnaryServerHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer ->: GRPCServerHandlerProtocol { - public typealias Request = Deserializer.Output - public typealias Response = Serializer.Input - - /// A response serializer. - @usableFromInline - internal let serializer: Serializer - - /// A request deserializer. - @usableFromInline - internal let deserializer: Deserializer - - /// A pipeline of user provided interceptors. - @usableFromInline - internal var interceptors: ServerInterceptorPipeline! - - /// The context required in order create the function. - @usableFromInline - internal let context: CallHandlerContext - - /// A reference to a `UserInfo`. - @usableFromInline - internal let userInfoRef: Ref - - /// The user provided function to execute. - @usableFromInline - internal let userFunction: (Request, StatusOnlyCallContext) -> EventLoopFuture - - /// The state of the function invocation. - @usableFromInline - internal var state: State = .idle - - @usableFromInline - internal enum State { - // Initial state. Nothing has happened yet. - case idle - // Headers have been received and now we're holding a context with which to invoke the user - // function when we receive a message. - case createdContext(UnaryResponseCallContext) - // The user function has been invoked, we're waiting for the response. - case invokedFunction(UnaryResponseCallContext) - // The function has completed or we are no longer proceeding with execution (because of an error - // or unexpected closure). - case completed - } - - @inlinable - public init( - context: CallHandlerContext, - requestDeserializer: Deserializer, - responseSerializer: Serializer, - interceptors: [ServerInterceptor], - userFunction: @escaping (Request, StatusOnlyCallContext) -> EventLoopFuture - ) { - self.userFunction = userFunction - self.serializer = responseSerializer - self.deserializer = requestDeserializer - self.context = context - - let userInfoRef = Ref(UserInfo()) - self.userInfoRef = userInfoRef - self.interceptors = ServerInterceptorPipeline( - logger: context.logger, - eventLoop: context.eventLoop, - path: context.path, - callType: .unary, - remoteAddress: context.remoteAddress, - userInfoRef: userInfoRef, - closeFuture: context.closeFuture, - interceptors: interceptors, - onRequestPart: self.receiveInterceptedPart(_:), - onResponsePart: self.sendInterceptedPart(_:promise:) - ) - } - - // MARK: - Public API: gRPC to Interceptors - - @inlinable - public func receiveMetadata(_ metadata: HPACKHeaders) { - self.interceptors.receive(.metadata(metadata)) - } - - @inlinable - public func receiveMessage(_ bytes: ByteBuffer) { - do { - let message = try self.deserializer.deserialize(byteBuffer: bytes) - self.interceptors.receive(.message(message)) - } catch { - self.handleError(error) - } - } - - @inlinable - public func receiveEnd() { - self.interceptors.receive(.end) - } - - @inlinable - public func receiveError(_ error: Error) { - self.handleError(error) - self.finish() - } - - @inlinable - public func finish() { - switch self.state { - case .idle: - self.interceptors = nil - self.state = .completed - - case let .createdContext(context), - let .invokedFunction(context): - context.responsePromise.fail(GRPCStatus(code: .unavailable, message: nil)) - self.context.eventLoop.execute { - self.interceptors = nil - } - - case .completed: - self.interceptors = nil - } - } - - // MARK: - Interceptors to User Function - - @inlinable - internal func receiveInterceptedPart(_ part: GRPCServerRequestPart) { - switch part { - case let .metadata(headers): - self.receiveInterceptedMetadata(headers) - case let .message(message): - self.receiveInterceptedMessage(message) - case .end: - self.receiveInterceptedEnd() - } - } - - @inlinable - internal func receiveInterceptedMetadata(_ headers: HPACKHeaders) { - switch self.state { - case .idle: - // Make a context to invoke the user function with. - let context = UnaryResponseCallContext( - eventLoop: self.context.eventLoop, - headers: headers, - logger: self.context.logger, - userInfoRef: self.userInfoRef, - closeFuture: self.context.closeFuture - ) - - // Move to the next state. - self.state = .createdContext(context) - - // Register a callback on the response future. The user function will complete this promise. - context.responsePromise.futureResult.whenComplete(self.userFunctionCompletedWithResult(_:)) - - // Send back response headers. - self.interceptors.send(.metadata([:]), promise: nil) - - case .createdContext, .invokedFunction: - self.handleError(GRPCError.ProtocolViolation("Multiple header blocks received on RPC")) - - case .completed: - // We may receive headers from the interceptor pipeline if we have already finished (i.e. due - // to an error or otherwise) and an interceptor doing some async work later emitting headers. - // Dropping them is fine. - () - } - } - - @inlinable - internal func receiveInterceptedMessage(_ request: Request) { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("Message received before headers")) - - case let .createdContext(context): - // Happy path: execute the function; complete the promise with the result. - self.state = .invokedFunction(context) - context.responsePromise.completeWith(self.userFunction(request, context)) - - case .invokedFunction: - // The function's already been invoked with a message. - self.handleError(GRPCError.ProtocolViolation("Multiple messages received on unary RPC")) - - case .completed: - // We received a message but we're already done: this may happen if we terminate the RPC - // due to a channel error, for example. - () - } - } - - @inlinable - internal func receiveInterceptedEnd() { - switch self.state { - case .idle: - self.handleError(GRPCError.ProtocolViolation("End received before headers")) - - case .createdContext: - self.handleError(GRPCError.ProtocolViolation("End received before message")) - - case .invokedFunction, .completed: - () - } - } - - // MARK: - User Function To Interceptors - - @inlinable - internal func userFunctionCompletedWithResult(_ result: Result) { - switch self.state { - case .idle: - // Invalid state: the user function can only complete if it was executed. - preconditionFailure() - - // 'created' is allowed here: we may have to (and tear down) after receiving headers - // but before receiving a message. - case let .createdContext(context), - let .invokedFunction(context): - - switch result { - case let .success(response): - // Complete, as we're sending 'end'. - self.state = .completed - - // Compression depends on whether it's enabled on the server and the setting in the caller - // context. - let compress = self.context.encoding.isEnabled && context.compressionEnabled - let metadata = MessageMetadata(compress: compress, flush: false) - self.interceptors.send(.message(response, metadata), promise: nil) - self.interceptors.send(.end(context.responseStatus, context.trailers), promise: nil) - - case let .failure(error): - self.handleError(error, thrownFromHandler: true) - } - - case .completed: - // We've already failed. Ignore this. - () - } - } - - @inlinable - internal func sendInterceptedPart( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - switch part { - case let .metadata(headers): - // We can delay this flush until the end of the RPC. - self.context.responseWriter.sendMetadata(headers, flush: false, promise: promise) - - case let .message(message, metadata): - do { - let bytes = try self.serializer.serialize(message, allocator: self.context.allocator) - self.context.responseWriter.sendMessage(bytes, metadata: metadata, promise: promise) - } catch { - // Serialization failed: fail the promise and send end. - promise?.fail(error) - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - // Loop back via the interceptors. - self.interceptors.send(.end(status, trailers), promise: nil) - } - - case let .end(status, trailers): - self.context.responseWriter.sendEnd(status: status, trailers: trailers, promise: promise) - } - } - - @inlinable - internal func handleError(_ error: Error, thrownFromHandler isHandlerError: Bool = false) { - switch self.state { - case .idle: - assert(!isHandlerError) - self.state = .completed - // We don't have a promise to fail. Just send back end. - let (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - self.interceptors.send(.end(status, trailers), promise: nil) - - case let .createdContext(context), - let .invokedFunction(context): - // We don't have a promise to fail. Just send back end. - self.state = .completed - - let status: GRPCStatus - let trailers: HPACKHeaders - - if isHandlerError { - (status, trailers) = ServerErrorProcessor.processObserverError( - error, - headers: context.headers, - trailers: context.trailers, - delegate: self.context.errorDelegate - ) - } else { - (status, trailers) = ServerErrorProcessor.processLibraryError( - error, - delegate: self.context.errorDelegate - ) - } - - self.interceptors.send(.end(status, trailers), promise: nil) - // We're already in the 'completed' state so failing the promise will be a no-op in the - // callback to 'userFunctionCompletedWithResult' (but we also need to avoid leaking the - // promise.) - context.responsePromise.fail(error) - - case .completed: - () - } - } -} diff --git a/Sources/GRPC/CallOptions.swift b/Sources/GRPC/CallOptions.swift deleted file mode 100644 index 990b52a53..000000000 --- a/Sources/GRPC/CallOptions.swift +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 - -import struct Foundation.UUID - -/// Options to use for GRPC calls. -public struct CallOptions: Sendable { - /// Additional metadata to send to the service. - public var customMetadata: HPACKHeaders - - /// The time limit for the RPC. - /// - /// - Note: timeouts are treated as deadlines as soon as an RPC has been invoked. - public var timeLimit: TimeLimit - - /// The compression used for requests, and the compression algorithms to advertise as acceptable - /// for the remote peer to use for encoding responses. - /// - /// Compression may also be disabled at the message-level for streaming requests (i.e. client - /// streaming and bidirectional streaming RPCs) by setting `compression` to ``Compression/disabled`` in - /// ``StreamingRequestClientCall/sendMessage(_:compression:)-uvtc``, - /// ``StreamingRequestClientCall/sendMessage(_:compression:promise:)`` , - /// ``StreamingRequestClientCall/sendMessages(_:compression:)-55vb3`` or - /// ``StreamingRequestClientCall/sendMessage(_:compression:promise:)`. - /// - /// Note that enabling `compression` via the `sendMessage` or `sendMessages` methods only applies - /// if encoding has been specified in these options. - public var messageEncoding: ClientMessageEncoding - - /// Whether the call is cacheable. - public var cacheable: Bool - - /// How IDs should be provided for requests. Defaults to ``RequestIDProvider-swift.struct/autogenerated``. - /// - /// The request ID is used for logging and will be added to the headers of a call if - /// `requestIDHeader` is specified. - /// - /// - Important: When setting ``CallOptions`` at the client level, ``RequestIDProvider-swift.struct/userDefined(_:)`` should __not__ be - /// used otherwise each request will have the same ID. - public var requestIDProvider: RequestIDProvider - - /// The name of the header to use when adding a request ID to a call, e.g. "x-request-id". If the - /// value is `nil` (the default) then no additional header will be added. - /// - /// Setting this value will add a request ID to the headers of the call these options are used - /// with. The request ID will be provided by ``requestIDProvider-swift.property`` and will also be used in log - /// messages associated with the call. - public var requestIDHeader: String? - - /// A preference for the `EventLoop` that the call is executed on. - /// - /// The `EventLoop` resulting from the preference will be used to create any `EventLoopFuture`s - /// associated with the call, such as the `response` for calls with a single response (i.e. unary - /// and client streaming). For calls which stream responses (server streaming and bidirectional - /// streaming) the response handler is executed on this event loop. - /// - /// Note that the underlying connection is not guaranteed to run on the same event loop. - public var eventLoopPreference: EventLoopPreference - - /// A logger used for the call. Defaults to a no-op logger. - /// - /// If a ``requestIDProvider-swift.property`` exists then a request ID will automatically attached to the logger's - /// metadata using the 'grpc-request-id' key. - public var logger: Logger - - public init( - customMetadata: HPACKHeaders = HPACKHeaders(), - timeLimit: TimeLimit = .none, - messageEncoding: ClientMessageEncoding = .disabled, - requestIDProvider: RequestIDProvider = .autogenerated, - requestIDHeader: String? = nil, - cacheable: Bool = false, - logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }) - ) { - self.init( - customMetadata: customMetadata, - timeLimit: timeLimit, - messageEncoding: messageEncoding, - requestIDProvider: requestIDProvider, - requestIDHeader: requestIDHeader, - eventLoopPreference: .indifferent, - cacheable: cacheable, - logger: logger - ) - } - - public init( - customMetadata: HPACKHeaders = HPACKHeaders(), - timeLimit: TimeLimit = .none, - messageEncoding: ClientMessageEncoding = .disabled, - requestIDProvider: RequestIDProvider = .autogenerated, - requestIDHeader: String? = nil, - eventLoopPreference: EventLoopPreference, - cacheable: Bool = false, - logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }) - ) { - self.customMetadata = customMetadata - self.messageEncoding = messageEncoding - self.requestIDProvider = requestIDProvider - self.requestIDHeader = requestIDHeader - self.cacheable = cacheable - self.timeLimit = timeLimit - self.logger = logger - self.eventLoopPreference = eventLoopPreference - } -} - -extension CallOptions { - public struct RequestIDProvider: Sendable { - public typealias RequestIDGenerator = @Sendable () -> String - - private enum RequestIDSource: Sendable { - case none - case `static`(String) - case generated(RequestIDGenerator) - } - - private var source: RequestIDSource - private init(_ source: RequestIDSource) { - self.source = source - } - - @usableFromInline - internal func requestID() -> String? { - switch self.source { - case .none: - return nil - case let .static(requestID): - return requestID - case let .generated(generator): - return generator() - } - } - - /// No request IDs are generated. - public static let none = RequestIDProvider(.none) - - /// Generate a new request ID for each RPC. - public static let autogenerated = RequestIDProvider(.generated({ UUID().uuidString })) - - /// Specify an ID to be used. - /// - /// - Important: this should only be used when ``CallOptions`` are passed directly to the call. - /// If it is used for the default options on a client then all calls with have the same ID. - public static func userDefined(_ requestID: String) -> RequestIDProvider { - return RequestIDProvider(.static(requestID)) - } - - /// Provide a factory to generate request IDs. - public static func generated( - _ requestIDFactory: @escaping RequestIDGenerator - ) -> RequestIDProvider { - return RequestIDProvider(.generated(requestIDFactory)) - } - } -} - -extension CallOptions { - public struct EventLoopPreference: Sendable { - /// No preference. The framework will assign an `EventLoop`. - public static let indifferent = EventLoopPreference(.indifferent) - - /// Use the provided `EventLoop` for the call. - public static func exact(_ eventLoop: EventLoop) -> EventLoopPreference { - return EventLoopPreference(.exact(eventLoop)) - } - - @usableFromInline - internal enum Preference: Sendable { - case indifferent - case exact(EventLoop) - } - - @usableFromInline - internal var _preference: Preference - - @inlinable - internal init(_ preference: Preference) { - self._preference = preference - } - } -} - -extension CallOptions.EventLoopPreference { - @inlinable - internal var exact: EventLoop? { - switch self._preference { - case let .exact(eventLoop): - return eventLoop - case .indifferent: - return nil - } - } -} diff --git a/Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift b/Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift deleted file mode 100644 index dff23a5c2..000000000 --- a/Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -/// A bidirectional-streaming gRPC call. Each response is passed to the provided observer block. -/// -/// Messages should be sent via the ``sendMessage(_:compression:)`` and ``sendMessages(_:compression:)`` methods; the stream of messages -/// must be terminated by calling ``sendEnd()`` to indicate the final message has been sent. -/// -/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore -/// has reference semantics. -public struct BidirectionalStreamingCall< - RequestPayload, - ResponsePayload ->: StreamingRequestClientCall { - private let call: Call - private let responseParts: StreamingResponseParts - - /// The options used to make the RPC. - public var options: CallOptions { - return self.call.options - } - - /// The path used to make the RPC. - public var path: String { - return self.call.path - } - - /// The `Channel` used to transport messages for this RPC. - public var subchannel: EventLoopFuture { - return self.call.channel - } - - /// The `EventLoop` this call is running on. - public var eventLoop: EventLoop { - return self.call.eventLoop - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel(promise: EventLoopPromise?) { - self.call.cancel(promise: promise) - } - - // MARK: - Response Parts - - /// The initial metadata returned from the server. - public var initialMetadata: EventLoopFuture { - return self.responseParts.initialMetadata - } - - /// The trailing metadata returned from the server. - public var trailingMetadata: EventLoopFuture { - return self.responseParts.trailingMetadata - } - - /// The final status of the the RPC. - public var status: EventLoopFuture { - return self.responseParts.status - } - - internal init( - call: Call, - callback: @escaping (ResponsePayload) -> Void - ) { - self.call = call - self.responseParts = StreamingResponseParts(on: call.eventLoop, callback) - } - - internal func invoke() { - self.call.invokeStreamingRequests( - onStart: {}, - onError: self.responseParts.handleError(_:), - onResponsePart: self.responseParts.handle(_:) - ) - } - - // MARK: - Requests - - /// Sends a message to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or - /// ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - message: The message to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - /// - promise: A promise to fulfill with the outcome of the send operation. - public func sendMessage( - _ message: RequestPayload, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) { - let compress = self.call.compress(compression) - self.call.send(.message(message, .init(compress: compress, flush: true)), promise: promise) - } - - /// Sends a sequence of messages to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or - /// ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - messages: The sequence of messages to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - /// - promise: A promise to fulfill with the outcome of the send operation. It will only succeed - /// if all messages were written successfully. - public func sendMessages( - _ messages: S, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) where S: Sequence, S.Element == RequestPayload { - self.call.sendMessages(messages, compression: compression, promise: promise) - } - - /// Terminates a stream of messages sent to the service. - /// - /// - Important: This should only ever be called once. - /// - Parameter promise: A promise to be fulfilled when the end has been sent. - public func sendEnd(promise: EventLoopPromise?) { - self.call.send(.end, promise: promise) - } -} diff --git a/Sources/GRPC/ClientCalls/Call.swift b/Sources/GRPC/ClientCalls/Call.swift deleted file mode 100644 index 46777ed20..000000000 --- a/Sources/GRPC/ClientCalls/Call.swift +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -import protocol SwiftProtobuf.Message - -/// An object representing a single RPC from the perspective of a client. It allows the caller to -/// send request parts, request a cancellation, and receive response parts in a provided callback. -/// -/// The call object sits atop an interceptor pipeline (see ``ClientInterceptor``) which allows for -/// request and response streams to be arbitrarily transformed or observed. Requests sent via this -/// call will traverse the pipeline before reaching the network, and responses received will -/// traverse the pipeline having been received from the network. -/// -/// This object is a lower-level API than the equivalent wrapped calls (such as ``UnaryCall`` and -/// ``BidirectionalStreamingCall``). The caller is therefore required to do more in order to use this -/// object correctly. Callers must call ``invoke(onError:onResponsePart:)`` to start the call and ensure that the correct -/// number of request parts are sent in the correct order (exactly one `metadata`, followed -/// by at most one `message` for unary and server streaming calls, and any number of `message` parts -/// for client streaming and bidirectional streaming calls. All call types must terminate their -/// request stream by sending one `end` message. -/// -/// Callers are not able to create ``Call`` objects directly, rather they must be created via an -/// object conforming to ``GRPCChannel`` such as ``ClientConnection``. -public final class Call { - @usableFromInline - internal enum State { - /// Idle, waiting to be invoked. - case idle(ClientTransportFactory) - - /// Invoked, we have a transport on which to send requests. The transport may be closed if the - /// RPC has already completed. - case invoked(ClientTransport) - } - - /// The current state of the call. - @usableFromInline - internal var _state: State - - /// User provided interceptors for the call. - @usableFromInline - internal let _interceptors: [ClientInterceptor] - - /// Whether compression is enabled on the call. - private var isCompressionEnabled: Bool { - return self.options.messageEncoding.enabledForRequests - } - - /// The `EventLoop` the call is being invoked on. - public let eventLoop: EventLoop - - /// The path of the RPC, usually generated from a service definition, e.g. "/echo.Echo/Get". - public let path: String - - /// The type of the RPC, e.g. unary, bidirectional streaming. - public let type: GRPCCallType - - /// Options used to invoke the call. - public let options: CallOptions - - /// A promise for the underlying `Channel`. We only allocate this if the user asks for - /// the `Channel` and we haven't invoked the transport yet. It's a bit unfortunate. - private var channelPromise: EventLoopPromise? - - /// Returns a future for the underlying `Channel`. - internal var channel: EventLoopFuture { - if self.eventLoop.inEventLoop { - return self._channel() - } else { - return self.eventLoop.flatSubmit { - return self._channel() - } - } - } - - // Calls can't be constructed directly: users must make them using a `GRPCChannel`. - @inlinable - internal init( - path: String, - type: GRPCCallType, - eventLoop: EventLoop, - options: CallOptions, - interceptors: [ClientInterceptor], - transportFactory: ClientTransportFactory - ) { - self.path = path - self.type = type - self.options = options - self._state = .idle(transportFactory) - self.eventLoop = eventLoop - self._interceptors = interceptors - } - - /// Starts the call and provides a callback which is invoked on every response part received from - /// the server. - /// - /// This must be called prior to ``send(_:)`` or ``cancel()``. - /// - /// - Parameters: - /// - onError: A callback invoked when an error is received. - /// - onResponsePart: A callback which is invoked on every response part. - /// - Important: This function should only be called once. Subsequent calls will be ignored. - @inlinable - public func invoke( - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - self.options.logger.debug("starting rpc", metadata: ["path": "\(self.path)"], source: "GRPC") - - if self.eventLoop.inEventLoop { - self._invoke(onStart: {}, onError: onError, onResponsePart: onResponsePart) - } else { - self.eventLoop.execute { - self._invoke(onStart: {}, onError: onError, onResponsePart: onResponsePart) - } - } - } - - /// Send a request part on the RPC. - /// - Parameters: - /// - part: The request part to send. - /// - promise: A promise which will be completed when the request part has been handled. - /// - Note: Sending will always fail if ``invoke(onError:onResponsePart:)`` has not been called. - @inlinable - public func send(_ part: GRPCClientRequestPart, promise: EventLoopPromise?) { - if self.eventLoop.inEventLoop { - self._send(part, promise: promise) - } else { - self.eventLoop.execute { - self._send(part, promise: promise) - } - } - } - - /// Attempt to cancel the RPC. - /// - Parameter promise: A promise which will be completed once the cancellation request has been - /// dealt with. - /// - Note: Cancellation will always fail if ``invoke(onError:onResponsePart:)`` has not been called. - public func cancel(promise: EventLoopPromise?) { - if self.eventLoop.inEventLoop { - self._cancel(promise: promise) - } else { - self.eventLoop.execute { - self._cancel(promise: promise) - } - } - } -} - -extension Call { - /// Send a request part on the RPC. - /// - Parameter part: The request part to send. - /// - Returns: A future which will be resolved when the request has been handled. - /// - Note: Sending will always fail if ``invoke(onError:onResponsePart:)`` has not been called. - @inlinable - public func send(_ part: GRPCClientRequestPart) -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.send(part, promise: promise) - return promise.futureResult - } - - /// Attempt to cancel the RPC. - /// - Note: Cancellation will always fail if ``invoke(onError:onResponsePart:)`` has not been called. - /// - Returns: A future which will be resolved when the cancellation request has been cancelled. - public func cancel() -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.cancel(promise: promise) - return promise.futureResult - } -} - -extension Call { - internal func compress(_ compression: Compression) -> Bool { - return compression.isEnabled(callDefault: self.isCompressionEnabled) - } - - internal func sendMessages( - _ messages: Messages, - compression: Compression, - promise: EventLoopPromise? - ) where Messages: Sequence, Messages.Element == Request { - if self.eventLoop.inEventLoop { - if let promise = promise { - self._sendMessages(messages, compression: compression, promise: promise) - } else { - self._sendMessages(messages, compression: compression) - } - } else { - self.eventLoop.execute { - if let promise = promise { - self._sendMessages(messages, compression: compression, promise: promise) - } else { - self._sendMessages(messages, compression: compression) - } - } - } - } - - // Provide a few convenience methods we need from the wrapped call objects. - private func _sendMessages( - _ messages: Messages, - compression: Compression - ) where Messages: Sequence, Messages.Element == Request { - self.eventLoop.assertInEventLoop() - let compress = self.compress(compression) - - var iterator = messages.makeIterator() - var maybeNext = iterator.next() - while let current = maybeNext { - let next = iterator.next() - // If there's no next message, then we'll flush. - let flush = next == nil - self._send(.message(current, .init(compress: compress, flush: flush)), promise: nil) - maybeNext = next - } - } - - private func _sendMessages( - _ messages: Messages, - compression: Compression, - promise: EventLoopPromise - ) where Messages: Sequence, Messages.Element == Request { - self.eventLoop.assertInEventLoop() - let compress = self.compress(compression) - - var iterator = messages.makeIterator() - var maybeNext = iterator.next() - while let current = maybeNext { - let next = iterator.next() - let isLast = next == nil - - // We're already on the event loop, use the `_` send. - if isLast { - // Only flush and attach the promise to the last message. - self._send(.message(current, .init(compress: compress, flush: true)), promise: promise) - } else { - self._send(.message(current, .init(compress: compress, flush: false)), promise: nil) - } - - maybeNext = next - } - } -} - -extension Call { - /// Invoke the RPC with this response part handler. - /// - Important: This *must* to be called from the `eventLoop`. - @usableFromInline - internal func _invoke( - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - self.eventLoop.assertInEventLoop() - - switch self._state { - case let .idle(factory): - let transport = factory.makeConfiguredTransport( - to: self.path, - for: self.type, - withOptions: self.options, - onEventLoop: self.eventLoop, - interceptedBy: self._interceptors, - onStart: onStart, - onError: onError, - onResponsePart: onResponsePart - ) - self._state = .invoked(transport) - - case .invoked: - // We can't be invoked twice. Just ignore this. - () - } - } - - /// Send a request part on the transport. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func _send(_ part: GRPCClientRequestPart, promise: EventLoopPromise?) { - self.eventLoop.assertInEventLoop() - - switch self._state { - case .idle: - promise?.fail(GRPCError.InvalidState("Call must be invoked before sending request parts")) - - case let .invoked(transport): - transport.send(part, promise: promise) - } - } - - /// Attempt to cancel the call. - /// - Important: This *must* to be called from the `eventLoop`. - private func _cancel(promise: EventLoopPromise?) { - self.eventLoop.assertInEventLoop() - - switch self._state { - case .idle: - promise?.succeed(()) - self.channelPromise?.fail(GRPCStatus(code: .cancelled)) - - case let .invoked(transport): - transport.cancel(promise: promise) - } - } - - /// Get the underlying `Channel` for this call. - /// - Important: This *must* to be called from the `eventLoop`. - private func _channel() -> EventLoopFuture { - self.eventLoop.assertInEventLoop() - - switch (self.channelPromise, self._state) { - case let (.some(promise), .idle), - let (.some(promise), .invoked): - // We already have a promise, just use that. - return promise.futureResult - - case (.none, .idle): - // We need to allocate a promise and ask the transport for the channel later. - let promise = self.eventLoop.makePromise(of: Channel.self) - self.channelPromise = promise - return promise.futureResult - - case let (.none, .invoked(transport)): - // Just ask the transport. - return transport.getChannel() - } - } -} - -extension Call { - // These helpers are for our wrapping call objects (`UnaryCall`, etc.). - - /// Invokes the call and sends a single request. Sends the metadata, request and closes the - /// request stream. - /// - Parameters: - /// - request: The request to send. - /// - onError: A callback invoked when an error is received. - /// - onResponsePart: A callback invoked for each response part received. - @inlinable - internal func invokeUnaryRequest( - _ request: Request, - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - if self.eventLoop.inEventLoop { - self._invokeUnaryRequest( - request: request, - onStart: onStart, - onError: onError, - onResponsePart: onResponsePart - ) - } else { - self.eventLoop.execute { - self._invokeUnaryRequest( - request: request, - onStart: onStart, - onError: onError, - onResponsePart: onResponsePart - ) - } - } - } - - /// Invokes the call for streaming requests and sends the initial call metadata. Callers can send - /// additional messages and end the stream by calling `send(_:promise:)`. - /// - Parameters: - /// - onError: A callback invoked when an error is received. - /// - onResponsePart: A callback invoked for each response part received. - @inlinable - internal func invokeStreamingRequests( - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - if self.eventLoop.inEventLoop { - self._invokeStreamingRequests( - onStart: onStart, - onError: onError, - onResponsePart: onResponsePart - ) - } else { - self.eventLoop.execute { - self._invokeStreamingRequests( - onStart: onStart, - onError: onError, - onResponsePart: onResponsePart - ) - } - } - } - - /// On-`EventLoop` implementation of `invokeUnaryRequest(request:_:)`. - @usableFromInline - internal func _invokeUnaryRequest( - request: Request, - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - self.eventLoop.assertInEventLoop() - assert(self.type == .unary || self.type == .serverStreaming) - - self._invoke(onStart: onStart, onError: onError, onResponsePart: onResponsePart) - self._send(.metadata(self.options.customMetadata), promise: nil) - self._send( - .message(request, .init(compress: self.isCompressionEnabled, flush: false)), - promise: nil - ) - self._send(.end, promise: nil) - } - - /// On-`EventLoop` implementation of `invokeStreamingRequests(_:)`. - @usableFromInline - internal func _invokeStreamingRequests( - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - self.eventLoop.assertInEventLoop() - assert(self.type == .clientStreaming || self.type == .bidirectionalStreaming) - - self._invoke(onStart: onStart, onError: onError, onResponsePart: onResponsePart) - self._send(.metadata(self.options.customMetadata), promise: nil) - } -} - -// @unchecked is ok: all mutable state is accessed/modified from the appropriate event loop. -extension Call: @unchecked Sendable where Request: Sendable, Response: Sendable {} diff --git a/Sources/GRPC/ClientCalls/CallDetails.swift b/Sources/GRPC/ClientCalls/CallDetails.swift deleted file mode 100644 index 53df0259a..000000000 --- a/Sources/GRPC/ClientCalls/CallDetails.swift +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -@usableFromInline -internal struct CallDetails { - /// The type of the RPC, e.g. unary. - internal var type: GRPCCallType - - /// The path of the RPC used for the ":path" pseudo header, e.g. "/echo.Echo/Get" - internal var path: String - - /// The host, used for the ":authority" pseudo header. - internal var authority: String - - /// Value used for the ":scheme" pseudo header, e.g. "https". - internal var scheme: String - - /// Call options provided by the user. - @usableFromInline - internal var options: CallOptions -} diff --git a/Sources/GRPC/ClientCalls/ClientCall.swift b/Sources/GRPC/ClientCalls/ClientCall.swift deleted file mode 100644 index ef131159c..000000000 --- a/Sources/GRPC/ClientCalls/ClientCall.swift +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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 Foundation -import NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import SwiftProtobuf - -/// Base protocol for a client call to a gRPC service. -public protocol ClientCall { - /// The type of the request message for the call. - associatedtype RequestPayload - /// The type of the response message for the call. - associatedtype ResponsePayload - - /// The event loop this call is running on. - var eventLoop: EventLoop { get } - - /// The options used to make the RPC. - var options: CallOptions { get } - - /// HTTP/2 stream that requests and responses are sent and received on. - var subchannel: EventLoopFuture { get } - - /// Initial response metadata. - var initialMetadata: EventLoopFuture { get } - - /// Status of this call which may be populated by the server or client. - /// - /// The client may populate the status if, for example, it was not possible to connect to the service. - /// - /// Note: despite ``GRPCStatus`` conforming to `Error`, the value will be __always__ delivered as a __success__ - /// result even if the status represents a __negative__ outcome. This future will __never__ be fulfilled - /// with an error. - var status: EventLoopFuture { get } - - /// Trailing response metadata. - var trailingMetadata: EventLoopFuture { get } - - /// Cancel the current call. - /// - /// Closes the HTTP/2 stream once it becomes available. Additional writes to the channel will be ignored. - /// Any unfulfilled promises will be failed with a cancelled status (excepting ``status`` which will be - /// succeeded, if not already succeeded). - func cancel(promise: EventLoopPromise?) -} - -extension ClientCall { - func cancel() -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.cancel(promise: promise) - return promise.futureResult - } -} - -/// A ``ClientCall`` with request streaming; i.e. client-streaming and bidirectional-streaming. -public protocol StreamingRequestClientCall: ClientCall { - /// Sends a message to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling - /// or ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - message: The message to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - /// - Returns: A future which will be fullfilled when the message has been sent. - func sendMessage(_ message: RequestPayload, compression: Compression) -> EventLoopFuture - - /// Sends a message to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()-7bhdp`` or ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - message: The message to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - /// - promise: A promise to be fulfilled when the message has been sent. - func sendMessage( - _ message: RequestPayload, - compression: Compression, - promise: EventLoopPromise? - ) - - /// Sends a sequence of messages to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling - /// or ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - messages: The sequence of messages to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - func sendMessages(_ messages: S, compression: Compression) -> EventLoopFuture - where S.Element == RequestPayload - - /// Sends a sequence of messages to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()-7bhdp`` or ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - messages: The sequence of messages to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - /// - promise: A promise to be fulfilled when all messages have been sent successfully. - func sendMessages( - _ messages: S, - compression: Compression, - promise: EventLoopPromise? - ) where S.Element == RequestPayload - - /// Terminates a stream of messages sent to the service. - /// - /// - Important: This should only ever be called once. - /// - Returns: A future which will be fulfilled when the end has been sent. - func sendEnd() -> EventLoopFuture - - /// Terminates a stream of messages sent to the service. - /// - /// - Important: This should only ever be called once. - /// - Parameter promise: A promise to be fulfilled when the end has been sent. - func sendEnd(promise: EventLoopPromise?) -} - -extension StreamingRequestClientCall { - public func sendMessage( - _ message: RequestPayload, - compression: Compression = .deferToCallDefault - ) -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.sendMessage(message, compression: compression, promise: promise) - return promise.futureResult - } - - public func sendMessages( - _ messages: S, - compression: Compression = .deferToCallDefault - ) -> EventLoopFuture - where S.Element == RequestPayload { - let promise = self.eventLoop.makePromise(of: Void.self) - self.sendMessages(messages, compression: compression, promise: promise) - return promise.futureResult - } - - public func sendEnd() -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.sendEnd(promise: promise) - return promise.futureResult - } -} - -/// A ``ClientCall`` with a unary response; i.e. unary and client-streaming. -public protocol UnaryResponseClientCall: ClientCall { - /// The response message returned from the service if the call is successful. This may be failed - /// if the call encounters an error. - /// - /// Callers should rely on the `status` of the call for the canonical outcome. - var response: EventLoopFuture { get } -} diff --git a/Sources/GRPC/ClientCalls/ClientStreamingCall.swift b/Sources/GRPC/ClientCalls/ClientStreamingCall.swift deleted file mode 100644 index abf9d0612..000000000 --- a/Sources/GRPC/ClientCalls/ClientStreamingCall.swift +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -/// A client-streaming gRPC call. -/// -/// Messages should be sent via the ``sendMessage(_:compression:)`` and ``sendMessages(_:compression:)`` methods; the stream of messages -/// must be terminated by calling ``sendEnd()`` to indicate the final message has been sent. -/// -/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore -/// has reference semantics. -public struct ClientStreamingCall: StreamingRequestClientCall, - UnaryResponseClientCall -{ - private let call: Call - private let responseParts: UnaryResponseParts - - /// The options used to make the RPC. - public var options: CallOptions { - return self.call.options - } - - /// The path used to make the RPC. - public var path: String { - return self.call.path - } - - /// The `Channel` used to transport messages for this RPC. - public var subchannel: EventLoopFuture { - return self.call.channel - } - - /// The `EventLoop` this call is running on. - public var eventLoop: EventLoop { - return self.call.eventLoop - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel(promise: EventLoopPromise?) { - self.call.cancel(promise: promise) - } - - // MARK: - Response Parts - - /// The initial metadata returned from the server. - public var initialMetadata: EventLoopFuture { - return self.responseParts.initialMetadata - } - - /// The response returned by the server. - public var response: EventLoopFuture { - return self.responseParts.response - } - - /// The trailing metadata returned from the server. - public var trailingMetadata: EventLoopFuture { - return self.responseParts.trailingMetadata - } - - /// The final status of the the RPC. - public var status: EventLoopFuture { - return self.responseParts.status - } - - internal init(call: Call) { - self.call = call - self.responseParts = UnaryResponseParts(on: call.eventLoop) - } - - internal func invoke() { - self.call.invokeStreamingRequests( - onStart: {}, - onError: self.responseParts.handleError(_:), - onResponsePart: self.responseParts.handle(_:) - ) - } - - // MARK: - Request - - /// Sends a message to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or - /// ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - message: The message to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - /// - promise: A promise to fulfill with the outcome of the send operation. - public func sendMessage( - _ message: RequestPayload, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) { - let compress = self.call.compress(compression) - self.call.send(.message(message, .init(compress: compress, flush: true)), promise: promise) - } - - /// Sends a sequence of messages to the service. - /// - /// - Important: Callers must terminate the stream of messages by calling ``sendEnd()`` or - /// ``sendEnd(promise:)``. - /// - /// - Parameters: - /// - messages: The sequence of messages to send. - /// - compression: Whether compression should be used for this message. Ignored if compression - /// was not enabled for the RPC. - /// - promise: A promise to fulfill with the outcome of the send operation. It will only succeed - /// if all messages were written successfully. - public func sendMessages( - _ messages: S, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) where S: Sequence, S.Element == RequestPayload { - self.call.sendMessages(messages, compression: compression, promise: promise) - } - - /// Terminates a stream of messages sent to the service. - /// - /// - Important: This should only ever be called once. - /// - Parameter promise: A promise to be fulfilled when the end has been sent. - public func sendEnd(promise: EventLoopPromise?) { - self.call.send(.end, promise: promise) - } -} diff --git a/Sources/GRPC/ClientCalls/LazyEventLoopPromise.swift b/Sources/GRPC/ClientCalls/LazyEventLoopPromise.swift deleted file mode 100644 index 6abbac63a..000000000 --- a/Sources/GRPC/ClientCalls/LazyEventLoopPromise.swift +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2020, 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 NIOConcurrencyHelpers -import NIOCore - -extension EventLoop { - internal func makeLazyPromise(of: Value.Type = Value.self) -> LazyEventLoopPromise { - return LazyEventLoopPromise(on: self) - } -} - -/// A `LazyEventLoopPromise` is similar to an `EventLoopPromise` except that the underlying -/// `EventLoopPromise` promise is only created if it is required. That is, when the future result -/// has been requested and the promise has not yet been completed. -/// -/// Note that all methods **must** be called from its `eventLoop`. -internal struct LazyEventLoopPromise { - private enum State { - // No future has been requested, no result has been delivered. - case idle - - // No future has been requested, but this result have been delivered. - case resolvedResult(Result) - - // A future has been request; the promise may or may not contain a result. - case unresolvedPromise(EventLoopPromise) - - // A future was requested, it's also been resolved. - case resolvedFuture(EventLoopFuture) - } - - private var state: State - private let eventLoop: EventLoop - - fileprivate init(on eventLoop: EventLoop) { - self.state = .idle - self.eventLoop = eventLoop - } - - /// Get the future result of this promise. - internal mutating func getFutureResult() -> EventLoopFuture { - self.eventLoop.preconditionInEventLoop() - - switch self.state { - case .idle: - let promise = self.eventLoop.makePromise(of: Value.self) - self.state = .unresolvedPromise(promise) - return promise.futureResult - - case let .resolvedResult(result): - let future: EventLoopFuture - switch result { - case let .success(value): - future = self.eventLoop.makeSucceededFuture(value) - case let .failure(error): - future = self.eventLoop.makeFailedFuture(error) - } - self.state = .resolvedFuture(future) - return future - - case let .unresolvedPromise(promise): - return promise.futureResult - - case let .resolvedFuture(future): - return future - } - } - - /// Succeed the promise with the given value. - internal mutating func succeed(_ value: Value) { - self.completeWith(.success(value)) - } - - /// Fail the promise with the given error. - internal mutating func fail(_ error: Error) { - self.completeWith(.failure(error)) - } - - /// Complete the promise with the given result. - internal mutating func completeWith(_ result: Result) { - self.eventLoop.preconditionInEventLoop() - - switch self.state { - case .idle: - self.state = .resolvedResult(result) - - case let .unresolvedPromise(promise): - promise.completeWith(result) - self.state = .resolvedFuture(promise.futureResult) - - case .resolvedResult, .resolvedFuture: - () - } - } -} diff --git a/Sources/GRPC/ClientCalls/ResponseContainers.swift b/Sources/GRPC/ClientCalls/ResponseContainers.swift deleted file mode 100644 index d8bf6411c..000000000 --- a/Sources/GRPC/ClientCalls/ResponseContainers.swift +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOHPACK - -/// A bucket of promises for a unary-response RPC. -internal class UnaryResponseParts { - /// The `EventLoop` we expect to receive these response parts on. - private let eventLoop: EventLoop - - /// A promise for the `Response` message. - private let responsePromise: EventLoopPromise - - /// Lazy promises for the status, initial-, and trailing-metadata. - private var initialMetadataPromise: LazyEventLoopPromise - private var trailingMetadataPromise: LazyEventLoopPromise - private var statusPromise: LazyEventLoopPromise - - internal var response: EventLoopFuture { - return self.responsePromise.futureResult - } - - internal var initialMetadata: EventLoopFuture { - return self.eventLoop.executeOrFlatSubmit { - return self.initialMetadataPromise.getFutureResult() - } - } - - internal var trailingMetadata: EventLoopFuture { - return self.eventLoop.executeOrFlatSubmit { - return self.trailingMetadataPromise.getFutureResult() - } - } - - internal var status: EventLoopFuture { - return self.eventLoop.executeOrFlatSubmit { - return self.statusPromise.getFutureResult() - } - } - - internal init(on eventLoop: EventLoop) { - self.eventLoop = eventLoop - self.responsePromise = eventLoop.makePromise() - self.initialMetadataPromise = eventLoop.makeLazyPromise() - self.trailingMetadataPromise = eventLoop.makeLazyPromise() - self.statusPromise = eventLoop.makeLazyPromise() - } - - /// Handle the response part, completing any promises as necessary. - /// - Important: This *must* be called on `eventLoop`. - internal func handle(_ part: GRPCClientResponsePart) { - self.eventLoop.assertInEventLoop() - - switch part { - case let .metadata(metadata): - self.initialMetadataPromise.succeed(metadata) - - case let .message(response): - self.responsePromise.succeed(response) - - case let .end(status, trailers): - // In case of a "Trailers-Only" RPC (i.e. just the trailers and status), fail the initial - // metadata and status. - self.initialMetadataPromise.fail(status) - self.responsePromise.fail(status) - - self.trailingMetadataPromise.succeed(trailers) - self.statusPromise.succeed(status) - } - } - - internal func handleError(_ error: Error) { - let withoutContext = error.removingContext() - let status = withoutContext.makeGRPCStatus() - self.initialMetadataPromise.fail(withoutContext) - self.responsePromise.fail(withoutContext) - self.trailingMetadataPromise.fail(withoutContext) - self.statusPromise.succeed(status) - } -} - -/// A bucket of promises for a streaming-response RPC. -internal class StreamingResponseParts { - /// The `EventLoop` we expect to receive these response parts on. - private let eventLoop: EventLoop - - /// A callback for response messages. - private var responseCallback: Optional<(Response) -> Void> - - /// Lazy promises for the status, initial-, and trailing-metadata. - private var initialMetadataPromise: LazyEventLoopPromise - private var trailingMetadataPromise: LazyEventLoopPromise - private var statusPromise: LazyEventLoopPromise - - internal var initialMetadata: EventLoopFuture { - return self.eventLoop.executeOrFlatSubmit { - return self.initialMetadataPromise.getFutureResult() - } - } - - internal var trailingMetadata: EventLoopFuture { - return self.eventLoop.executeOrFlatSubmit { - return self.trailingMetadataPromise.getFutureResult() - } - } - - internal var status: EventLoopFuture { - return self.eventLoop.executeOrFlatSubmit { - return self.statusPromise.getFutureResult() - } - } - - internal init(on eventLoop: EventLoop, _ responseCallback: @escaping (Response) -> Void) { - self.eventLoop = eventLoop - self.responseCallback = responseCallback - self.initialMetadataPromise = eventLoop.makeLazyPromise() - self.trailingMetadataPromise = eventLoop.makeLazyPromise() - self.statusPromise = eventLoop.makeLazyPromise() - } - - internal func handle(_ part: GRPCClientResponsePart) { - self.eventLoop.assertInEventLoop() - - switch part { - case let .metadata(metadata): - self.initialMetadataPromise.succeed(metadata) - - case let .message(response): - self.responseCallback?(response) - - case let .end(status, trailers): - // Once the stream has finished, we must release the callback, to make sure don't - // break potential retain cycles (the callback may reference other object's that in - // turn reference `StreamingResponseParts`). - self.responseCallback = nil - self.initialMetadataPromise.fail(status) - self.trailingMetadataPromise.succeed(trailers) - self.statusPromise.succeed(status) - } - } - - internal func handleError(_ error: Error) { - self.eventLoop.assertInEventLoop() - - // Once the stream has finished, we must release the callback, to make sure don't - // break potential retain cycles (the callback may reference other object's that in - // turn reference `StreamingResponseParts`). - self.responseCallback = nil - let withoutContext = error.removingContext() - let status = withoutContext.makeGRPCStatus() - self.initialMetadataPromise.fail(withoutContext) - self.trailingMetadataPromise.fail(withoutContext) - self.statusPromise.succeed(status) - } -} - -extension EventLoop { - fileprivate func executeOrFlatSubmit( - _ body: @escaping () -> EventLoopFuture - ) -> EventLoopFuture { - if self.inEventLoop { - return body() - } else { - return self.flatSubmit { - return body() - } - } - } -} - -extension Error { - fileprivate func removingContext() -> Error { - if let withContext = self as? GRPCError.WithContext { - return withContext.error - } else { - return self - } - } - - fileprivate func makeGRPCStatus() -> GRPCStatus { - if let withContext = self as? GRPCError.WithContext { - return withContext.error.makeGRPCStatus() - } else if let transformable = self as? GRPCStatusTransformable { - return transformable.makeGRPCStatus() - } else { - return GRPCStatus(code: .unknown, message: String(describing: self)) - } - } -} - -// @unchecked is ok: all mutable state is accessed/modified from an appropriate event loop. -extension UnaryResponseParts: @unchecked Sendable where Response: Sendable {} -extension StreamingResponseParts: @unchecked Sendable where Response: Sendable {} diff --git a/Sources/GRPC/ClientCalls/ResponsePartContainer.swift b/Sources/GRPC/ClientCalls/ResponsePartContainer.swift deleted file mode 100644 index 9014a601e..000000000 --- a/Sources/GRPC/ClientCalls/ResponsePartContainer.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOHPACK - -/// A container for RPC response parts. -internal struct ResponsePartContainer { - /// The type of handler for response message part. - enum ResponseHandler { - case unary(EventLoopPromise) - case stream((Response) -> Void) - } - - var lazyInitialMetadataPromise: LazyEventLoopPromise - - /// A handler for response messages. - let responseHandler: ResponseHandler - - /// A promise for the trailing metadata. - var lazyTrailingMetadataPromise: LazyEventLoopPromise - - /// A promise for the call status. - var lazyStatusPromise: LazyEventLoopPromise - - /// Fail all promises - except for the status promise - with the given error status. Succeed the - /// status promise. - mutating func fail(with error: Error, status: GRPCStatus) { - self.lazyInitialMetadataPromise.fail(error) - - switch self.responseHandler { - case let .unary(response): - response.fail(error) - case .stream: - () - } - self.lazyTrailingMetadataPromise.fail(error) - // We always succeed the status. - self.lazyStatusPromise.succeed(status) - } - - /// Make a response container for a unary response. - init(eventLoop: EventLoop, unaryResponsePromise: EventLoopPromise) { - self.lazyInitialMetadataPromise = eventLoop.makeLazyPromise() - self.lazyTrailingMetadataPromise = eventLoop.makeLazyPromise() - self.lazyStatusPromise = eventLoop.makeLazyPromise() - self.responseHandler = .unary(unaryResponsePromise) - } - - /// Make a response container for a response which is streamed. - init(eventLoop: EventLoop, streamingResponseHandler: @escaping (Response) -> Void) { - self.lazyInitialMetadataPromise = eventLoop.makeLazyPromise() - self.lazyTrailingMetadataPromise = eventLoop.makeLazyPromise() - self.lazyStatusPromise = eventLoop.makeLazyPromise() - self.responseHandler = .stream(streamingResponseHandler) - } -} diff --git a/Sources/GRPC/ClientCalls/ServerStreamingCall.swift b/Sources/GRPC/ClientCalls/ServerStreamingCall.swift deleted file mode 100644 index 67b93226e..000000000 --- a/Sources/GRPC/ClientCalls/ServerStreamingCall.swift +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -/// A server-streaming gRPC call. The request is sent on initialization, each response is passed to -/// the provided observer block. -/// -/// Note: while this object is a `struct`, its implementation delegates to `Call`. It therefore -/// has reference semantics. -public struct ServerStreamingCall: ClientCall { - private let call: Call - private let responseParts: StreamingResponseParts - - /// The options used to make the RPC. - public var options: CallOptions { - return self.call.options - } - - /// The path used to make the RPC. - public var path: String { - return self.call.path - } - - /// The `Channel` used to transport messages for this RPC. - public var subchannel: EventLoopFuture { - return self.call.channel - } - - /// The `EventLoop` this call is running on. - public var eventLoop: EventLoop { - return self.call.eventLoop - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel(promise: EventLoopPromise?) { - self.call.cancel(promise: promise) - } - - // MARK: - Response Parts - - /// The initial metadata returned from the server. - public var initialMetadata: EventLoopFuture { - return self.responseParts.initialMetadata - } - - /// The trailing metadata returned from the server. - public var trailingMetadata: EventLoopFuture { - return self.responseParts.trailingMetadata - } - - /// The final status of the the RPC. - public var status: EventLoopFuture { - return self.responseParts.status - } - - internal init( - call: Call, - callback: @escaping (ResponsePayload) -> Void - ) { - self.call = call - self.responseParts = StreamingResponseParts(on: call.eventLoop, callback) - } - - internal func invoke(_ request: RequestPayload) { - self.call.invokeUnaryRequest( - request, - onStart: {}, - onError: self.responseParts.handleError(_:), - onResponsePart: self.responseParts.handle(_:) - ) - } -} diff --git a/Sources/GRPC/ClientCalls/UnaryCall.swift b/Sources/GRPC/ClientCalls/UnaryCall.swift deleted file mode 100644 index 5efd9f819..000000000 --- a/Sources/GRPC/ClientCalls/UnaryCall.swift +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import SwiftProtobuf - -/// A unary gRPC call. The request is sent on initialization. -/// -/// Note: while this object is a `struct`, its implementation delegates to ``Call``. It therefore -/// has reference semantics. -public struct UnaryCall: UnaryResponseClientCall { - private let call: Call - private let responseParts: UnaryResponseParts - - /// The options used to make the RPC. - public var options: CallOptions { - return self.call.options - } - - /// The path used to make the RPC. - public var path: String { - return self.call.path - } - - /// The `Channel` used to transport messages for this RPC. - public var subchannel: EventLoopFuture { - return self.call.channel - } - - /// The `EventLoop` this call is running on. - public var eventLoop: EventLoop { - return self.call.eventLoop - } - - /// Cancel this RPC if it hasn't already completed. - public func cancel(promise: EventLoopPromise?) { - self.call.cancel(promise: promise) - } - - // MARK: - Response Parts - - /// The initial metadata returned from the server. - public var initialMetadata: EventLoopFuture { - return self.responseParts.initialMetadata - } - - /// The response returned by the server. - public var response: EventLoopFuture { - return self.responseParts.response - } - - /// The trailing metadata returned from the server. - public var trailingMetadata: EventLoopFuture { - return self.responseParts.trailingMetadata - } - - /// The final status of the the RPC. - public var status: EventLoopFuture { - return self.responseParts.status - } - - internal init(call: Call) { - self.call = call - self.responseParts = UnaryResponseParts(on: call.eventLoop) - } - - internal func invoke(_ request: RequestPayload) { - self.call.invokeUnaryRequest( - request, - onStart: {}, - onError: self.responseParts.handleError(_:), - onResponsePart: self.responseParts.handle(_:) - ) - } -} diff --git a/Sources/GRPC/ClientConnection.swift b/Sources/GRPC/ClientConnection.swift deleted file mode 100644 index e2f6dbdcb..000000000 --- a/Sources/GRPC/ClientConnection.swift +++ /dev/null @@ -1,686 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 -import NIOPosix -import NIOTLS -import NIOTransportServices -import SwiftProtobuf - -#if os(Linux) -@preconcurrency import Foundation -#else -import Foundation -#endif - -#if canImport(NIOSSL) -import NIOSSL -#endif - -/// Provides a single, managed connection to a server which is guaranteed to always use the same -/// `EventLoop`. -/// -/// The connection to the server is provided by a single channel which will attempt to reconnect to -/// the server if the connection is dropped. When either the client or server detects that the -/// connection has become idle -- that is, there are no outstanding RPCs and the idle timeout has -/// passed (5 minutes, by default) -- the underlying channel will be closed. The client will not -/// idle the connection if any RPC exists, even if there has been no activity on the RPC for the -/// idle timeout. Long-lived, low activity RPCs may benefit from configuring keepalive (see -/// ``ClientConnectionKeepalive``) which periodically pings the server to ensure that the connection -/// is not dropped. If the connection is idle a new channel will be created on-demand when the next -/// RPC is made. -/// -/// The state of the connection can be observed using a ``ConnectivityStateDelegate``. -/// -/// Since the connection is managed, and may potentially spend long periods of time waiting for a -/// connection to come up (cellular connections, for example), different behaviors may be used when -/// starting a call. The different behaviors are detailed in the ``CallStartBehavior`` documentation. -/// -/// ### Channel Pipeline -/// -/// The `NIO.ChannelPipeline` for the connection is configured as such: -/// -/// ┌──────────────────────────┐ -/// │ DelegatingErrorHandler │ -/// └──────────▲───────────────┘ -/// HTTP2Frame│ -/// │ ⠇ ⠇ ⠇ ⠇ -/// │ ┌┴─▼┐ ┌┴─▼┐ -/// │ │ | │ | HTTP/2 streams -/// │ └▲─┬┘ └▲─┬┘ -/// │ │ │ │ │ HTTP2Frame -/// ┌─┴────────────────┴─▼───┴─▼┐ -/// │ HTTP2StreamMultiplexer | -/// └─▲───────────────────────┬─┘ -/// HTTP2Frame│ │HTTP2Frame -/// ┌─┴───────────────────────▼─┐ -/// │ GRPCIdleHandler │ -/// └─▲───────────────────────┬─┘ -/// HTTP2Frame│ │HTTP2Frame -/// ┌─┴───────────────────────▼─┐ -/// │ NIOHTTP2Handler │ -/// └─▲───────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// ┌─┴───────────────────────▼─┐ -/// │ NIOSSLHandler │ -/// └─▲───────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// │ ▼ -/// -/// The 'GRPCIdleHandler' intercepts HTTP/2 frames and various events and is responsible for -/// informing and controlling the state of the connection (idling and keepalive). The HTTP/2 streams -/// are used to handle individual RPCs. -public final class ClientConnection: Sendable { - private let connectionManager: ConnectionManager - - /// HTTP multiplexer from the underlying channel handling gRPC calls. - internal func getMultiplexer() -> EventLoopFuture { - return self.connectionManager.getHTTP2Multiplexer() - } - - /// The configuration for this client. - internal let configuration: Configuration - - /// The scheme of the URI for each RPC, i.e. 'http' or 'https'. - internal let scheme: String - - /// The authority of the URI for each RPC. - internal let authority: String - - /// A monitor for the connectivity state. - public let connectivity: ConnectivityStateMonitor - - /// The `EventLoop` this connection is using. - public var eventLoop: EventLoop { - return self.connectionManager.eventLoop - } - - /// Creates a new connection from the given configuration. Prefer using - /// ``ClientConnection/secure(group:)`` to build a connection secured with TLS or - /// ``ClientConnection/insecure(group:)`` to build a plaintext connection. - /// - /// - Important: Users should prefer using ``ClientConnection/secure(group:)`` to build a connection - /// with TLS, or ``ClientConnection/insecure(group:)`` to build a connection without TLS. - public init(configuration: Configuration) { - self.configuration = configuration - self.scheme = configuration.tlsConfiguration == nil ? "http" : "https" - self.authority = configuration.tlsConfiguration?.hostnameOverride ?? configuration.target.host - - let monitor = ConnectivityStateMonitor( - delegate: configuration.connectivityStateDelegate, - queue: configuration.connectivityStateDelegateQueue - ) - - self.connectivity = monitor - self.connectionManager = ConnectionManager( - configuration: configuration, - connectivityDelegate: monitor, - idleBehavior: .closeWhenIdleTimeout, - logger: configuration.backgroundActivityLogger - ) - } - - /// Close the channel, and any connections associated with it. Any ongoing RPCs may fail. - /// - /// - Returns: Returns a future which will be resolved when shutdown has completed. - public func close() -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.close(promise: promise) - return promise.futureResult - } - - /// Close the channel, and any connections associated with it. Any ongoing RPCs may fail. - /// - /// - Parameter promise: A promise which will be completed when shutdown has completed. - public func close(promise: EventLoopPromise) { - self.connectionManager.shutdown(mode: .forceful, promise: promise) - } - - /// Attempt to gracefully shutdown the channel. New RPCs will be failed immediately and existing - /// RPCs may continue to run until they complete. - /// - /// - Parameters: - /// - deadline: A point in time by which the graceful shutdown must have completed. If the - /// deadline passes and RPCs are still active then the connection will be closed forcefully - /// and any remaining in-flight RPCs may be failed. - /// - promise: A promise which will be completed when shutdown has completed. - public func closeGracefully(deadline: NIODeadline, promise: EventLoopPromise) { - return self.connectionManager.shutdown(mode: .graceful(deadline), promise: promise) - } - - /// Populates the logger in `options` and appends a request ID header to the metadata, if - /// configured. - /// - Parameter options: The options containing the logger to populate. - private func populateLogger(in options: inout CallOptions) { - // Get connection metadata. - self.connectionManager.appendMetadata(to: &options.logger) - - // Attach a request ID. - let requestID = options.requestIDProvider.requestID() - if let requestID = requestID { - options.logger[metadataKey: MetadataKey.requestID] = "\(requestID)" - // Add the request ID header too. - if let requestIDHeader = options.requestIDHeader { - options.customMetadata.add(name: requestIDHeader, value: requestID) - } - } - } -} - -extension ClientConnection: GRPCChannel { - public func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - var options = callOptions - self.populateLogger(in: &options) - let multiplexer = self.getMultiplexer() - let eventLoop = callOptions.eventLoopPreference.exact ?? multiplexer.eventLoop - - // This should be on the same event loop as the multiplexer (i.e. the event loop of the - // underlying `Channel`. - let channel = multiplexer.eventLoop.makePromise(of: Channel.self) - multiplexer.whenComplete { - ClientConnection.makeStreamChannel(using: $0, promise: channel) - } - - return Call( - path: path, - type: type, - eventLoop: eventLoop, - options: options, - interceptors: interceptors, - transportFactory: .http2( - channel: channel.futureResult, - authority: self.authority, - scheme: self.scheme, - maximumReceiveMessageLength: self.configuration.maximumReceiveMessageLength, - errorDelegate: self.configuration.errorDelegate - ) - ) - } - - public func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - var options = callOptions - self.populateLogger(in: &options) - let multiplexer = self.getMultiplexer() - let eventLoop = callOptions.eventLoopPreference.exact ?? multiplexer.eventLoop - - // This should be on the same event loop as the multiplexer (i.e. the event loop of the - // underlying `Channel`. - let channel = multiplexer.eventLoop.makePromise(of: Channel.self) - multiplexer.whenComplete { - ClientConnection.makeStreamChannel(using: $0, promise: channel) - } - - return Call( - path: path, - type: type, - eventLoop: eventLoop, - options: options, - interceptors: interceptors, - transportFactory: .http2( - channel: channel.futureResult, - authority: self.authority, - scheme: self.scheme, - maximumReceiveMessageLength: self.configuration.maximumReceiveMessageLength, - errorDelegate: self.configuration.errorDelegate - ) - ) - } - - private static func makeStreamChannel( - using result: Result, - promise: EventLoopPromise - ) { - switch result { - case let .success(multiplexer): - multiplexer.createStreamChannel(promise: promise) { - $0.eventLoop.makeSucceededVoidFuture() - } - case let .failure(error): - promise.fail(error) - } - } -} - -// MARK: - Configuration structures - -/// A target to connect to. -public struct ConnectionTarget: Sendable, Hashable { - internal enum Wrapped: Hashable { - case hostAndPort(String, Int) - case unixDomainSocket(String) - case socketAddress(SocketAddress) - case connectedSocket(NIOBSDSocket.Handle) - case vsockAddress(VsockAddress) - } - - internal var wrapped: Wrapped - private init(_ wrapped: Wrapped) { - self.wrapped = wrapped - } - - /// The host and port. The port is 443 by default. - public static func host(_ host: String, port: Int = 443) -> ConnectionTarget { - return ConnectionTarget(.hostAndPort(host, port)) - } - - /// The host and port. - public static func hostAndPort(_ host: String, _ port: Int) -> ConnectionTarget { - return ConnectionTarget(.hostAndPort(host, port)) - } - - /// The path of a Unix domain socket. - public static func unixDomainSocket(_ path: String) -> ConnectionTarget { - return ConnectionTarget(.unixDomainSocket(path)) - } - - /// A NIO socket address. - public static func socketAddress(_ address: SocketAddress) -> ConnectionTarget { - return ConnectionTarget(.socketAddress(address)) - } - - /// A connected NIO socket. - public static func connectedSocket(_ socket: NIOBSDSocket.Handle) -> ConnectionTarget { - return ConnectionTarget(.connectedSocket(socket)) - } - - /// A vsock socket. - public static func vsockAddress(_ vsockAddress: VsockAddress) -> ConnectionTarget { - return ConnectionTarget(.vsockAddress(vsockAddress)) - } - - @usableFromInline - var host: String { - switch self.wrapped { - case let .hostAndPort(host, _): - return host - case let .socketAddress(.v4(address)): - return address.host - case let .socketAddress(.v6(address)): - return address.host - case .unixDomainSocket, .socketAddress(.unixDomainSocket), .connectedSocket: - return "localhost" - case let .vsockAddress(address): - return "vsock://\(address.cid)" - } - } -} - -/// The connectivity behavior to use when starting an RPC. -public struct CallStartBehavior: Hashable, Sendable { - internal enum Behavior: Hashable, Sendable { - case waitsForConnectivity - case fastFailure - } - - internal var wrapped: Behavior - private init(_ wrapped: Behavior) { - self.wrapped = wrapped - } - - /// Waits for connectivity (that is, the 'ready' connectivity state) before attempting to start - /// an RPC. Doing so may involve multiple connection attempts. - /// - /// This is the preferred, and default, behaviour. - public static let waitsForConnectivity = CallStartBehavior(.waitsForConnectivity) - - /// The 'fast failure' behaviour is intended for cases where users would rather their RPC failed - /// quickly rather than waiting for an active connection. The behaviour depends on the current - /// connectivity state: - /// - /// - Idle: a connection attempt will be started and the RPC will fail if that attempt fails. - /// - Connecting: a connection attempt is already in progress, the RPC will fail if that attempt - /// fails. - /// - Ready: a connection is already active: the RPC will be started using that connection. - /// - Transient failure: the last connection or connection attempt failed and gRPC is waiting to - /// connect again. The RPC will fail immediately. - /// - Shutdown: the connection is shutdown, the RPC will fail immediately. - public static let fastFailure = CallStartBehavior(.fastFailure) -} - -extension ClientConnection { - /// Configuration for a ``ClientConnection``. Users should prefer using one of the - /// ``ClientConnection`` builders: ``ClientConnection/secure(group:)`` or ``ClientConnection/insecure(group:)``. - public struct Configuration: Sendable { - /// The target to connect to. - public var target: ConnectionTarget - - /// The event loop group to run the connection on. - public var eventLoopGroup: EventLoopGroup - - /// An error delegate which is called when errors are caught. Provided delegates **must not - /// maintain a strong reference to this `ClientConnection`**. Doing so will cause a retain - /// cycle. Defaults to ``LoggingClientErrorDelegate``. - public var errorDelegate: ClientErrorDelegate? = LoggingClientErrorDelegate.shared - - /// A delegate which is called when the connectivity state is changed. Defaults to `nil`. - public var connectivityStateDelegate: ConnectivityStateDelegate? - - /// The `DispatchQueue` on which to call the connectivity state delegate. If a delegate is - /// provided but the queue is `nil` then one will be created by gRPC. Defaults to `nil`. - public var connectivityStateDelegateQueue: DispatchQueue? - - #if canImport(NIOSSL) - /// TLS configuration for this connection. `nil` if TLS is not desired. - /// - /// - Important: `tls` is deprecated; use ``tlsConfiguration`` or one of - /// the ``ClientConnection/usingTLS(with:on:)`` builder functions. - @available(*, deprecated, renamed: "tlsConfiguration") - public var tls: TLS? { - get { - return self.tlsConfiguration?.asDeprecatedClientConfiguration - } - set { - self.tlsConfiguration = newValue.map { .init(transforming: $0) } - } - } - #endif // canImport(NIOSSL) - - /// TLS configuration for this connection. `nil` if TLS is not desired. - public var tlsConfiguration: GRPCTLSConfiguration? - - /// The connection backoff configuration. If no connection retrying is required then this should - /// be `nil`. - public var connectionBackoff: ConnectionBackoff? = ConnectionBackoff() - - /// The connection keepalive configuration. - public var connectionKeepalive = ClientConnectionKeepalive() - - /// The amount of time to wait before closing the connection. The idle timeout will start only - /// if there are no RPCs in progress and will be cancelled as soon as any RPCs start. - /// - /// If a connection becomes idle, starting a new RPC will automatically create a new connection. - /// - /// Defaults to 30 minutes. - public var connectionIdleTimeout: TimeAmount = .minutes(30) - - /// The behavior used to determine when an RPC should start. That is, whether it should wait for - /// an active connection or fail quickly if no connection is currently available. - /// - /// Defaults to ``CallStartBehavior/waitsForConnectivity``. - public var callStartBehavior: CallStartBehavior = .waitsForConnectivity - - /// The HTTP/2 flow control target window size. Defaults to 8MB. Values are clamped between - /// 1 and 2^31-1 inclusive. - public var httpTargetWindowSize = 8 * 1024 * 1024 { - didSet { - self.httpTargetWindowSize = self.httpTargetWindowSize.clamped(to: 1 ... Int(Int32.max)) - } - } - - /// The HTTP/2 max frame size. Defaults to 16384. Value is clamped between 2^14 and 2^24-1 - /// octets inclusive (the minimum and maximum allowable values - HTTP/2 RFC 7540 4.2). - public var httpMaxFrameSize: Int = 16384 { - didSet { - self.httpMaxFrameSize = self.httpMaxFrameSize.clamped(to: 16384 ... 16_777_215) - } - } - - /// The HTTP protocol used for this connection. - public var httpProtocol: HTTP2FramePayloadToHTTP1ClientCodec.HTTPProtocol { - return self.tlsConfiguration == nil ? .http : .https - } - - /// The maximum size in bytes of a message which may be received from a server. Defaults to 4MB. - public var maximumReceiveMessageLength: Int = 4 * 1024 * 1024 { - willSet { - precondition(newValue >= 0, "maximumReceiveMessageLength must be positive") - } - } - - /// A logger for background information (such as connectivity state). A separate logger for - /// requests may be provided in the `CallOptions`. - /// - /// Defaults to a no-op logger. - public var backgroundActivityLogger = Logger( - label: "io.grpc", - factory: { _ in SwiftLogNoOpLogHandler() } - ) - - /// A channel initializer which will be run after gRPC has initialized each channel. This may be - /// used to add additional handlers to the pipeline and is intended for debugging. - /// - /// - Warning: The initializer closure may be invoked *multiple times*. - @preconcurrency - public var debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? - - #if canImport(NIOSSL) - /// Create a `Configuration` with some pre-defined defaults. Prefer using - /// `ClientConnection.secure(group:)` to build a connection secured with TLS or - /// `ClientConnection.insecure(group:)` to build a plaintext connection. - /// - /// - Parameter target: The target to connect to. - /// - Parameter eventLoopGroup: The event loop group to run the connection on. - /// - Parameter errorDelegate: The error delegate, defaulting to a delegate which will log only - /// on debug builds. - /// - Parameter connectivityStateDelegate: A connectivity state delegate, defaulting to `nil`. - /// - Parameter connectivityStateDelegateQueue: A `DispatchQueue` on which to call the - /// `connectivityStateDelegate`. - /// - Parameter tls: TLS configuration, defaulting to `nil`. - /// - Parameter connectionBackoff: The connection backoff configuration to use. - /// - Parameter connectionKeepalive: The keepalive configuration to use. - /// - Parameter connectionIdleTimeout: The amount of time to wait before closing the connection, defaulting to 30 minutes. - /// - Parameter callStartBehavior: The behavior used to determine when a call should start in - /// relation to its underlying connection. Defaults to `waitsForConnectivity`. - /// - Parameter httpTargetWindowSize: The HTTP/2 flow control target window size. - /// - Parameter backgroundActivityLogger: A logger for background information (such as - /// connectivity state). Defaults to a no-op logger. - /// - Parameter debugChannelInitializer: A channel initializer will be called after gRPC has - /// initialized the channel. Defaults to `nil`. - @available(*, deprecated, renamed: "default(target:eventLoopGroup:)") - @preconcurrency - public init( - target: ConnectionTarget, - eventLoopGroup: EventLoopGroup, - errorDelegate: ClientErrorDelegate? = LoggingClientErrorDelegate(), - connectivityStateDelegate: ConnectivityStateDelegate? = nil, - connectivityStateDelegateQueue: DispatchQueue? = nil, - tls: Configuration.TLS? = nil, - connectionBackoff: ConnectionBackoff? = ConnectionBackoff(), - connectionKeepalive: ClientConnectionKeepalive = ClientConnectionKeepalive(), - connectionIdleTimeout: TimeAmount = .minutes(30), - callStartBehavior: CallStartBehavior = .waitsForConnectivity, - httpTargetWindowSize: Int = 8 * 1024 * 1024, - backgroundActivityLogger: Logger = Logger( - label: "io.grpc", - factory: { _ in SwiftLogNoOpLogHandler() } - ), - debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? = nil - ) { - self.target = target - self.eventLoopGroup = eventLoopGroup - self.errorDelegate = errorDelegate - self.connectivityStateDelegate = connectivityStateDelegate - self.connectivityStateDelegateQueue = connectivityStateDelegateQueue - self.tlsConfiguration = tls.map { GRPCTLSConfiguration(transforming: $0) } - self.connectionBackoff = connectionBackoff - self.connectionKeepalive = connectionKeepalive - self.connectionIdleTimeout = connectionIdleTimeout - self.callStartBehavior = callStartBehavior - self.httpTargetWindowSize = httpTargetWindowSize - self.backgroundActivityLogger = backgroundActivityLogger - self.debugChannelInitializer = debugChannelInitializer - } - #endif // canImport(NIOSSL) - - private init(eventLoopGroup: EventLoopGroup, target: ConnectionTarget) { - self.eventLoopGroup = eventLoopGroup - self.target = target - } - - /// Make a new configuration using default values. - /// - /// - Parameters: - /// - target: The target to connect to. - /// - eventLoopGroup: The `EventLoopGroup` providing an `EventLoop` for the connection to - /// run on. - /// - Returns: A configuration with default values set. - public static func `default`( - target: ConnectionTarget, - eventLoopGroup: EventLoopGroup - ) -> Configuration { - return .init(eventLoopGroup: eventLoopGroup, target: target) - } - } -} - -// MARK: - Configuration helpers/extensions - -extension ClientBootstrapProtocol { - /// Connect to the given connection target. - /// - /// - Parameter target: The target to connect to. - func connect(to target: ConnectionTarget) -> EventLoopFuture { - switch target.wrapped { - case let .hostAndPort(host, port): - return self.connect(host: host, port: port) - - case let .unixDomainSocket(path): - return self.connect(unixDomainSocketPath: path) - - case let .socketAddress(address): - return self.connect(to: address) - case let .connectedSocket(socket): - return self.withConnectedSocket(socket) - case let .vsockAddress(address): - return self.connect(to: address) - } - } -} - -#if canImport(NIOSSL) -extension ChannelPipeline.SynchronousOperations { - internal func configureNIOSSLForGRPCClient( - sslContext: Result, - serverHostname: String?, - customVerificationCallback: NIOSSLCustomVerificationCallback?, - logger: Logger - ) throws { - let sslContext = try sslContext.get() - let sslClientHandler: NIOSSLClientHandler - - if let customVerificationCallback = customVerificationCallback { - sslClientHandler = try NIOSSLClientHandler( - context: sslContext, - serverHostname: serverHostname, - customVerificationCallback: customVerificationCallback - ) - } else { - sslClientHandler = try NIOSSLClientHandler( - context: sslContext, - serverHostname: serverHostname - ) - } - - try self.addHandler(sslClientHandler) - try self.addHandler(TLSVerificationHandler(logger: logger)) - } -} -#endif // canImport(NIOSSL) - -extension ChannelPipeline.SynchronousOperations { - internal func configureHTTP2AndGRPCHandlersForGRPCClient( - channel: Channel, - connectionManager: ConnectionManager, - connectionKeepalive: ClientConnectionKeepalive, - connectionIdleTimeout: TimeAmount, - httpTargetWindowSize: Int, - httpMaxFrameSize: Int, - errorDelegate: ClientErrorDelegate?, - logger: Logger - ) throws { - let initialSettings = [ - // As per the default settings for swift-nio-http2: - HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize), - // We never expect (or allow) server initiated streams. - HTTP2Setting(parameter: .maxConcurrentStreams, value: 0), - // As configured by the user. - HTTP2Setting(parameter: .maxFrameSize, value: httpMaxFrameSize), - HTTP2Setting(parameter: .initialWindowSize, value: httpTargetWindowSize), - ] - - // We could use 'configureHTTP2Pipeline' here, but we need to add a few handlers between the - // two HTTP/2 handlers so we'll do it manually instead. - try self.addHandler(NIOHTTP2Handler(mode: .client, initialSettings: initialSettings)) - - let h2Multiplexer = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - targetWindowSize: httpTargetWindowSize, - inboundStreamInitializer: nil - ) - - // The multiplexer is passed through the idle handler so it is only reported on - // successful channel activation - with happy eyeballs multiple pipelines can - // be constructed so it's not safe to report just yet. - try self.addHandler( - GRPCIdleHandler( - connectionManager: connectionManager, - multiplexer: h2Multiplexer, - idleTimeout: connectionIdleTimeout, - keepalive: connectionKeepalive, - logger: logger - ) - ) - - try self.addHandler(h2Multiplexer) - try self.addHandler(DelegatingErrorHandler(logger: logger, delegate: errorDelegate)) - } -} - -extension Channel { - func configureGRPCClient( - errorDelegate: ClientErrorDelegate?, - logger: Logger - ) -> EventLoopFuture { - return self.configureHTTP2Pipeline(mode: .client, inboundStreamInitializer: nil).flatMap { _ in - self.pipeline.addHandler(DelegatingErrorHandler(logger: logger, delegate: errorDelegate)) - } - } -} - -extension TimeAmount { - /// Creates a new `TimeAmount` from the given time interval in seconds. - /// - /// - Parameter timeInterval: The amount of time in seconds - static func seconds(timeInterval: TimeInterval) -> TimeAmount { - return .nanoseconds(Int64(timeInterval * 1_000_000_000)) - } -} - -extension String { - var isIPAddress: Bool { - // We need some scratch space to let inet_pton write into. - var ipv4Addr = in_addr() - var ipv6Addr = in6_addr() - - return self.withCString { ptr in - inet_pton(AF_INET, ptr, &ipv4Addr) == 1 || inet_pton(AF_INET6, ptr, &ipv6Addr) == 1 - } - } -} diff --git a/Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift b/Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift deleted file mode 100644 index d10550db1..000000000 --- a/Sources/GRPC/ClientConnectionConfiguration+NIOSSL.swift +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOSSL - -extension ClientConnection.Configuration { - /// TLS configuration for a `ClientConnection`. - /// - /// Note that this configuration is a subset of `NIOSSL.TLSConfiguration` where certain options - /// are removed from the user's control to ensure the configuration complies with the gRPC - /// specification. - @available(*, deprecated, renamed: "GRPCTLSConfiguration") - public struct TLS { - public private(set) var configuration: TLSConfiguration - - /// Value to use for TLS SNI extension; this must not be an address. - public var hostnameOverride: String? - - /// The certificates to offer during negotiation. If not present, no certificates will be offered. - public var certificateChain: [NIOSSLCertificateSource] { - get { - return self.configuration.certificateChain - } - set { - self.configuration.certificateChain = newValue - } - } - - /// The private key associated with the leaf certificate. - public var privateKey: NIOSSLPrivateKeySource? { - get { - return self.configuration.privateKey - } - set { - self.configuration.privateKey = newValue - } - } - - /// The trust roots to use to validate certificates. This only needs to be provided if you - /// intend to validate certificates. - public var trustRoots: NIOSSLTrustRoots? { - get { - return self.configuration.trustRoots - } - set { - self.configuration.trustRoots = newValue - } - } - - /// Whether to verify remote certificates. - public var certificateVerification: CertificateVerification { - get { - return self.configuration.certificateVerification - } - set { - self.configuration.certificateVerification = newValue - } - } - - /// A custom verification callback that allows completely overriding the certificate verification logic for this connection. - public var customVerificationCallback: NIOSSLCustomVerificationCallback? - - /// TLS Configuration with suitable defaults for clients. - /// - /// This is a wrapper around `NIOSSL.TLSConfiguration` to restrict input to values which comply - /// with the gRPC protocol. - /// - /// - Parameter certificateChain: The certificate to offer during negotiation, defaults to an - /// empty array. - /// - Parameter privateKey: The private key associated with the leaf certificate. This defaults - /// to `nil`. - /// - Parameter trustRoots: The trust roots to validate certificates, this defaults to using a - /// root provided by the platform. - /// - Parameter certificateVerification: Whether to verify the remote certificate. Defaults to - /// `.fullVerification`. - /// - Parameter hostnameOverride: Value to use for TLS SNI extension; this must not be an IP - /// address, defaults to `nil`. - /// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic, - /// defaults to `nil`. - public init( - certificateChain: [NIOSSLCertificateSource] = [], - privateKey: NIOSSLPrivateKeySource? = nil, - trustRoots: NIOSSLTrustRoots = .default, - certificateVerification: CertificateVerification = .fullVerification, - hostnameOverride: String? = nil, - customVerificationCallback: NIOSSLCustomVerificationCallback? = nil - ) { - var configuration = TLSConfiguration.makeClientConfiguration() - configuration.minimumTLSVersion = .tlsv12 - configuration.certificateVerification = certificateVerification - configuration.trustRoots = trustRoots - configuration.certificateChain = certificateChain - configuration.privateKey = privateKey - configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.client - - self.configuration = configuration - self.hostnameOverride = hostnameOverride - self.customVerificationCallback = customVerificationCallback - } - - /// Creates a TLS Configuration using the given `NIOSSL.TLSConfiguration`. - /// - /// - Note: If no ALPN tokens are set in `configuration.applicationProtocols` then "grpc-exp" - /// and "h2" will be used. - /// - Parameters: - /// - configuration: The `NIOSSL.TLSConfiguration` to base this configuration on. - /// - hostnameOverride: The hostname override to use for the TLS SNI extension. - public init(configuration: TLSConfiguration, hostnameOverride: String? = nil) { - self.configuration = configuration - self.hostnameOverride = hostnameOverride - - // Set the ALPN tokens if none were set. - if self.configuration.applicationProtocols.isEmpty { - self.configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.client - } - } - } -} - -#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/ClientErrorDelegate.swift b/Sources/GRPC/ClientErrorDelegate.swift deleted file mode 100644 index ee3734ae2..000000000 --- a/Sources/GRPC/ClientErrorDelegate.swift +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 Foundation -import Logging - -/// Delegate called when errors are caught by the client on individual HTTP/2 streams and errors in -/// the underlying HTTP/2 connection. -/// -/// The intended use of this protocol is with ``ClientConnection``. In order to avoid retain -/// cycles, classes implementing this delegate **must not** maintain a strong reference to the -/// ``ClientConnection``. -public protocol ClientErrorDelegate: AnyObject, GRPCPreconcurrencySendable { - /// Called when the client catches an error. - /// - /// - Parameters: - /// - error: The error which was caught. - /// - logger: A logger with relevant metadata for the RPC or connection the error relates to. - /// - file: The file where the error was raised. - /// - line: The line within the file where the error was raised. - func didCatchError(_ error: Error, logger: Logger, file: StaticString, line: Int) -} - -extension ClientErrorDelegate { - /// Calls ``didCatchError(_:logger:file:line:)`` with appropriate context placeholders when no - /// context is available. - internal func didCatchErrorWithoutContext(_ error: Error, logger: Logger) { - self.didCatchError(error, logger: logger, file: "", line: 0) - } -} - -/// A ``ClientErrorDelegate`` which logs errors. -public final class LoggingClientErrorDelegate: ClientErrorDelegate { - /// A shared instance of this class. - public static let shared = LoggingClientErrorDelegate() - - public init() {} - - public func didCatchError(_ error: Error, logger: Logger, file: StaticString, line: Int) { - logger.error( - "grpc client error", - metadata: [MetadataKey.error: "\(error)"], - source: "GRPC", - file: "\(file)", - function: "", - line: UInt(line) - ) - } -} diff --git a/Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift b/Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift deleted file mode 100644 index f36bb5ad8..000000000 --- a/Sources/GRPC/CoalescingLengthPrefixedMessageWriter.swift +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright 2022, 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 DequeModule -import NIOCore - -internal struct CoalescingLengthPrefixedMessageWriter { - /// Length of the gRPC message header (1 compression byte, 4 bytes for the length). - static let metadataLength = 5 - - /// Message size above which we emit two buffers: one containing the header and one with the - /// actual message bytes. At or below the limit we copy the message into a new buffer containing - /// both the header and the message. - /// - /// Using two buffers avoids expensive copies of large messages. For smaller messages the copy - /// is cheaper than the additional allocations and overhead required to send an extra HTTP/2 DATA - /// frame. - /// - /// The value of 16k was chosen empirically. We subtract the length of the message header - /// as `ByteBuffer` reserve capacity in powers of two and want to avoid overallocating. - static let singleBufferSizeLimit = 16384 - Self.metadataLength - - /// The compression algorithm to use, if one should be used. - private let compression: CompressionAlgorithm? - /// Any compressor associated with the compression algorithm. - private let compressor: Zlib.Deflate? - - /// Whether the compression message flag should be set. - private var supportsCompression: Bool { - return self.compression != nil - } - - /// A scratch buffer that we encode messages into: if the buffer isn't held elsewhere then we - /// can avoid having to allocate a new one. - private var scratch: ByteBuffer - - /// Outbound buffers waiting to be written. - private var pending: OneOrManyQueue - - private struct Pending { - var buffer: ByteBuffer - var promise: EventLoopPromise? - var compress: Bool - - init(buffer: ByteBuffer, compress: Bool, promise: EventLoopPromise?) { - self.buffer = buffer - self.promise = promise - self.compress = compress - } - - var isSmallEnoughToCoalesce: Bool { - let limit = CoalescingLengthPrefixedMessageWriter.singleBufferSizeLimit - return self.buffer.readableBytes <= limit - } - - var shouldCoalesce: Bool { - return self.isSmallEnoughToCoalesce || self.compress - } - } - - private enum State { - // Coalescing pending messages. - case coalescing - // Emitting a non-coalesced message; the header has been written, the body - // needs to be written next. - case emittingLargeFrame(ByteBuffer, EventLoopPromise?) - } - - private var state: State - - init(compression: CompressionAlgorithm? = nil, allocator: ByteBufferAllocator) { - self.compression = compression - self.scratch = allocator.buffer(capacity: 0) - self.state = .coalescing - self.pending = .init() - - switch self.compression?.algorithm { - case .none, .some(.identity): - self.compressor = nil - case .some(.deflate): - self.compressor = Zlib.Deflate(format: .deflate) - case .some(.gzip): - self.compressor = Zlib.Deflate(format: .gzip) - } - } - - /// Append a serialized message buffer to the writer. - mutating func append(buffer: ByteBuffer, compress: Bool, promise: EventLoopPromise?) { - let pending = Pending( - buffer: buffer, - compress: compress && self.supportsCompression, - promise: promise - ) - - self.pending.append(pending) - } - - /// Return a tuple of the next buffer to write and its associated write promise. - mutating func next() -> (Result, EventLoopPromise?)? { - switch self.state { - case .coalescing: - // Nothing pending: exit early. - if self.pending.isEmpty { - return nil - } - - // First up we need to work out how many elements we're going to pop off the front - // and coalesce. - // - // At the same time we'll compute how much capacity we'll need in the buffer and cascade - // their promises. - var messagesToCoalesce = 0 - var requiredCapacity = 0 - var promise: EventLoopPromise? - - for element in self.pending { - if !element.shouldCoalesce { - break - } - - messagesToCoalesce &+= 1 - requiredCapacity += element.buffer.readableBytes + Self.metadataLength - if let existing = promise { - existing.futureResult.cascade(to: element.promise) - } else { - promise = element.promise - } - } - - if messagesToCoalesce == 0 { - // Nothing to coalesce; this means the first element should be emitted with its header in - // a separate buffer. Note: the force unwrap is okay here: we early exit if `self.pending` - // is empty. - let pending = self.pending.pop()! - - // Set the scratch buffer to just be a message header then store the message bytes. - self.scratch.clear(minimumCapacity: Self.metadataLength) - self.scratch.writeMultipleIntegers(UInt8(0), UInt32(pending.buffer.readableBytes)) - self.state = .emittingLargeFrame(pending.buffer, pending.promise) - return (.success(self.scratch), nil) - } else { - self.scratch.clear(minimumCapacity: requiredCapacity) - - // Drop and encode the messages. - while messagesToCoalesce > 0, let next = self.pending.pop() { - messagesToCoalesce &-= 1 - do { - try self.encode(next.buffer, compress: next.compress) - } catch { - return (.failure(error), promise) - } - } - - return (.success(self.scratch), promise) - } - - case let .emittingLargeFrame(buffer, promise): - // We just emitted the header, now emit the body. - self.state = .coalescing - return (.success(buffer), promise) - } - } - - private mutating func encode(_ buffer: ByteBuffer, compress: Bool) throws { - if let compressor = self.compressor, compress { - try self.encode(buffer, compressor: compressor) - } else { - try self.encode(buffer) - } - } - - private mutating func encode(_ buffer: ByteBuffer, compressor: Zlib.Deflate) throws { - // Set the compression byte. - self.scratch.writeInteger(UInt8(1)) - // Set the length to zero; we'll write the actual value in a moment. - let payloadSizeIndex = self.scratch.writerIndex - self.scratch.writeInteger(UInt32(0)) - - let bytesWritten: Int - do { - var buffer = buffer - bytesWritten = try compressor.deflate(&buffer, into: &self.scratch) - } catch { - throw error - } - - self.scratch.setInteger(UInt32(bytesWritten), at: payloadSizeIndex) - - // Finally, the compression context should be reset between messages. - compressor.reset() - } - - private mutating func encode(_ buffer: ByteBuffer) throws { - self.scratch.writeMultipleIntegers(UInt8(0), UInt32(buffer.readableBytes)) - self.scratch.writeImmutableBuffer(buffer) - } -} - -/// A FIFO-queue which allows for a single to be stored on the stack and defers to a -/// heap-implementation if further elements are added. -/// -/// This is useful when optimising for unary streams where avoiding the cost of a heap -/// allocation is desirable. -internal struct OneOrManyQueue: Collection { - private var backing: Backing - - private enum Backing: Collection { - case none - case one(Element) - case many(Deque) - - var startIndex: Int { - switch self { - case .none, .one: - return 0 - case let .many(elements): - return elements.startIndex - } - } - - var endIndex: Int { - switch self { - case .none: - return 0 - case .one: - return 1 - case let .many(elements): - return elements.endIndex - } - } - - subscript(index: Int) -> Element { - switch self { - case .none: - fatalError("Invalid index") - case let .one(element): - assert(index == 0) - return element - case let .many(elements): - return elements[index] - } - } - - func index(after index: Int) -> Int { - switch self { - case .none: - return 0 - case .one: - return 1 - case let .many(elements): - return elements.index(after: index) - } - } - - var count: Int { - switch self { - case .none: - return 0 - case .one: - return 1 - case let .many(elements): - return elements.count - } - } - - var isEmpty: Bool { - switch self { - case .none: - return true - case .one: - return false - case let .many(elements): - return elements.isEmpty - } - } - - mutating func append(_ element: Element) { - switch self { - case .none: - self = .one(element) - case let .one(one): - var elements = Deque() - elements.reserveCapacity(16) - elements.append(one) - elements.append(element) - self = .many(elements) - case var .many(elements): - self = .none - elements.append(element) - self = .many(elements) - } - } - - mutating func pop() -> Element? { - switch self { - case .none: - return nil - case let .one(element): - self = .none - return element - case var .many(many): - self = .none - let element = many.popFirst() - self = .many(many) - return element - } - } - } - - init() { - self.backing = .none - } - - var isEmpty: Bool { - return self.backing.isEmpty - } - - var count: Int { - return self.backing.count - } - - var startIndex: Int { - return self.backing.startIndex - } - - var endIndex: Int { - return self.backing.endIndex - } - - subscript(index: Int) -> Element { - return self.backing[index] - } - - func index(after index: Int) -> Int { - return self.backing.index(after: index) - } - - mutating func append(_ element: Element) { - self.backing.append(element) - } - - mutating func pop() -> Element? { - return self.backing.pop() - } -} diff --git a/Sources/GRPC/Compression/CompressionAlgorithm.swift b/Sources/GRPC/Compression/CompressionAlgorithm.swift deleted file mode 100644 index 960cf7d9b..000000000 --- a/Sources/GRPC/Compression/CompressionAlgorithm.swift +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -/// 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: Equatable, 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) - - // The order here is important: most compression to least. - public static let all: [CompressionAlgorithm] = [.gzip, .deflate, .identity] - - /// The name of the compression algorithm. - public var name: String { - return self.algorithm.rawValue - } - - internal enum Algorithm: String { - case identity - case deflate - case gzip - } - - internal let algorithm: Algorithm - - private init(_ algorithm: Algorithm) { - self.algorithm = algorithm - } - - internal init?(rawValue: String) { - guard let algorithm = Algorithm(rawValue: rawValue) else { - return nil - } - self.algorithm = algorithm - } -} diff --git a/Sources/GRPC/Compression/DecompressionLimit.swift b/Sources/GRPC/Compression/DecompressionLimit.swift deleted file mode 100644 index 6f002ed6f..000000000 --- a/Sources/GRPC/Compression/DecompressionLimit.swift +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -public struct DecompressionLimit: Equatable, Sendable { - private enum Limit: Equatable, Sendable { - case ratio(Int) - case absolute(Int) - } - - private let limit: Limit - - /// Limits decompressed payloads to be no larger than the product of the compressed size - /// and `ratio`. - /// - /// - Parameter ratio: The decompression ratio. - /// - Precondition: `ratio` must be greater than zero. - public static func ratio(_ ratio: Int) -> DecompressionLimit { - precondition(ratio > 0, "ratio must be greater than zero") - return DecompressionLimit(limit: .ratio(ratio)) - } - - /// Limits decompressed payloads to be no larger than the given `size`. - /// - /// - Parameter size: The absolute size limit of decompressed payloads. - /// - Precondition: `size` must not be negative. - public static func absolute(_ size: Int) -> DecompressionLimit { - precondition(size >= 0, "absolute size must be non-negative") - return DecompressionLimit(limit: .absolute(size)) - } -} - -extension DecompressionLimit { - /// The largest allowed decompressed size for this limit. - func maximumDecompressedSize(compressedSize: Int) -> Int { - switch self.limit { - case let .ratio(ratio): - return ratio * compressedSize - case let .absolute(size): - return size - } - } -} diff --git a/Sources/GRPC/Compression/MessageEncoding.swift b/Sources/GRPC/Compression/MessageEncoding.swift deleted file mode 100644 index 6301c12bb..000000000 --- a/Sources/GRPC/Compression/MessageEncoding.swift +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -/// Whether compression should be enabled for the message. -public struct Compression: Hashable, Sendable { - @usableFromInline - internal enum _Wrapped: Hashable, Sendable { - case enabled - case disabled - case deferToCallDefault - } - - @usableFromInline - internal var _wrapped: _Wrapped - - private init(_ wrapped: _Wrapped) { - self._wrapped = wrapped - } - - /// Enable compression. Note that this will be ignored if compression has not been enabled or is - /// not supported on the call. - public static let enabled = Compression(.enabled) - - /// Disable compression. - public static let disabled = Compression(.disabled) - - /// Defer to the call (the ``CallOptions`` for the client, and the context for the server) to - /// determine whether compression should be used for the message. - public static let deferToCallDefault = Compression(.deferToCallDefault) -} - -extension Compression { - @inlinable - internal func isEnabled(callDefault: Bool) -> Bool { - switch self._wrapped { - case .enabled: - return callDefault - case .disabled: - return false - case .deferToCallDefault: - return callDefault - } - } -} - -/// Whether compression is enabled or disabled for a client. -public enum ClientMessageEncoding: Sendable { - /// Compression is enabled with the given configuration. - case enabled(Configuration) - /// Compression is disabled. - case disabled -} - -extension ClientMessageEncoding { - internal var enabledForRequests: Bool { - switch self { - case let .enabled(configuration): - return configuration.outbound != nil - case .disabled: - return false - } - } -} - -extension ClientMessageEncoding { - public struct Configuration: Sendable { - public init( - forRequests outbound: CompressionAlgorithm?, - acceptableForResponses inbound: [CompressionAlgorithm] = CompressionAlgorithm.all, - decompressionLimit: DecompressionLimit - ) { - self.outbound = outbound - self.inbound = inbound - self.decompressionLimit = decompressionLimit - } - - /// The compression algorithm used for outbound messages. - public var outbound: CompressionAlgorithm? - - /// The set of compression algorithms advertised to the remote peer that they may use. - public var inbound: [CompressionAlgorithm] - - /// The decompression limit acceptable for responses. RPCs which receive a message whose - /// decompressed size exceeds the limit will be cancelled. - public var decompressionLimit: DecompressionLimit - - /// Accept all supported compression on responses, do not compress requests. - public static func responsesOnly( - acceptable: [CompressionAlgorithm] = CompressionAlgorithm.all, - decompressionLimit: DecompressionLimit - ) -> Configuration { - return Configuration( - forRequests: .identity, - acceptableForResponses: acceptable, - decompressionLimit: decompressionLimit - ) - } - - internal var acceptEncodingHeader: String { - return self.inbound.map { $0.name }.joined(separator: ",") - } - } -} - -/// Whether compression is enabled or disabled on the server. -public enum ServerMessageEncoding { - /// Compression is supported with this configuration. - case enabled(Configuration) - /// Compression is not enabled. However, 'identity' compression is still supported. - case disabled - - @usableFromInline - internal var isEnabled: Bool { - switch self { - case .enabled: - return true - case .disabled: - return false - } - } -} - -extension ServerMessageEncoding { - public struct Configuration { - /// The set of compression algorithms advertised that we will accept from clients for requests. - /// Note that clients may send us messages compressed with algorithms not included in this list; - /// if we support it then we still accept the message. - /// - /// All cases of `CompressionAlgorithm` are supported. - public var enabledAlgorithms: [CompressionAlgorithm] - - /// The decompression limit acceptable for requests. RPCs which receive a message whose - /// decompressed size exceeds the limit will be cancelled. - public var decompressionLimit: DecompressionLimit - - /// Create a configuration for server message encoding. - /// - /// - Parameters: - /// - enabledAlgorithms: The list of algorithms which are enabled. - /// - decompressionLimit: Decompression limit acceptable for requests. - public init( - enabledAlgorithms: [CompressionAlgorithm] = CompressionAlgorithm.all, - decompressionLimit: DecompressionLimit - ) { - self.enabledAlgorithms = enabledAlgorithms - self.decompressionLimit = decompressionLimit - } - } -} diff --git a/Sources/GRPC/Compression/Zlib.swift b/Sources/GRPC/Compression/Zlib.swift deleted file mode 100644 index 4186f1f9e..000000000 --- a/Sources/GRPC/Compression/Zlib.swift +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Copyright 2020, 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 CGRPCZlib -import NIOCore - -import struct Foundation.Data - -/// Provides minimally configurable wrappers around zlib's compression and decompression -/// functionality. -/// -/// See also: https://www.zlib.net/manual.html -enum Zlib { - // MARK: Deflate (compression) - - /// Creates a new compressor for the given compression format. - /// - /// This compressor is only suitable for compressing whole messages at a time. Callers - /// must `reset()` the compressor between subsequent calls to `deflate`. - /// - /// - Parameter format:The expected compression type. - class Deflate { - private var stream: ZStream - private let format: CompressionFormat - - init(format: CompressionFormat) { - self.stream = ZStream() - self.format = format - self.initialize() - } - - deinit { - self.end() - } - - /// Compresses the data in `input` into the `output` buffer. - /// - /// - Parameter input: The complete data to be compressed. - /// - Parameter output: The `ByteBuffer` into which the compressed message should be written. - /// - Returns: The number of bytes written into the `output` buffer. - func deflate(_ input: inout ByteBuffer, into output: inout ByteBuffer) throws -> Int { - // Note: This is only valid because we always use Z_FINISH to flush. - // - // From the documentation: - // Note that it is possible for the compressed size to be larger than the value returned - // by deflateBound() if flush options other than Z_FINISH or Z_NO_FLUSH are used. - let upperBound = CGRPCZlib_deflateBound(&self.stream.zstream, UInt(input.readableBytes)) - - return try input.readWithUnsafeMutableReadableBytes { inputPointer -> (Int, Int) in - - self.stream.nextInputBuffer = CGRPCZlib_castVoidToBytefPointer(inputPointer.baseAddress!) - self.stream.availableInputBytes = inputPointer.count - - defer { - self.stream.nextInputBuffer = nil - self.stream.availableInputBytes = 0 - } - - let writtenBytes = - try output - .writeWithUnsafeMutableBytes(minimumWritableBytes: Int(upperBound)) { outputPointer in - try self.stream.deflate( - outputBuffer: CGRPCZlib_castVoidToBytefPointer(outputPointer.baseAddress!), - outputBufferSize: outputPointer.count - ) - } - - let bytesRead = inputPointer.count - self.stream.availableInputBytes - return (bytesRead, writtenBytes) - } - } - - /// Resets compression state. This must be called after each call to `deflate` if more - /// messages are to be compressed by this instance. - func reset() { - let rc = CGRPCZlib_deflateReset(&self.stream.zstream) - - // Possible return codes: - // - Z_OK - // - Z_STREAM_ERROR: the source stream state was inconsistent. - // - // If we're in an inconsistent state we can just replace the stream and initialize it. - switch rc { - case Z_OK: - () - - case Z_STREAM_ERROR: - self.end() - self.stream = ZStream() - self.initialize() - - default: - preconditionFailure("deflateReset: unexpected return code rc=\(rc)") - } - } - - /// Initialize the `z_stream` used for deflate. - private func initialize() { - let rc = CGRPCZlib_deflateInit2( - &self.stream.zstream, - Z_DEFAULT_COMPRESSION, // compression level - Z_DEFLATED, // compression method (this must be Z_DEFLATED) - self.format.windowBits, // window size, i.e. deflate/gzip - 8, // memory level (this is the default value in the docs) - Z_DEFAULT_STRATEGY // compression strategy - ) - - // Possible return codes: - // - Z_OK - // - Z_MEM_ERROR: not enough memory - // - Z_STREAM_ERROR: a parameter was invalid - // - // If we can't allocate memory then we can't progress anyway, and we control the parameters - // so not throwing an error here is okay. - assert(rc == Z_OK, "deflateInit2 error: rc=\(rc) \(self.stream.lastErrorMessage ?? "")") - } - - /// Calls `deflateEnd` on the underlying `z_stream` to deallocate resources allocated by zlib. - private func end() { - _ = CGRPCZlib_deflateEnd(&self.stream.zstream) - - // Possible return codes: - // - Z_OK - // - Z_STREAM_ERROR: the source stream state was inconsistent. - // - // Since we're going away there's no reason to fail here. - } - } - - // MARK: Inflate (decompression) - - /// Creates a new decompressor for the given compression format. - /// - /// This decompressor is only suitable for decompressing whole messages at a time. Callers - /// must `reset()` the decompressor between subsequent calls to `inflate`. - /// - /// - Parameter format:The expected compression type. - class Inflate { - enum InflationState { - /// Inflation is in progress. - case inflating(InflatingState) - - /// Inflation completed successfully. - case inflated - - init(compressedSize: Int, limit: DecompressionLimit) { - self = .inflating(InflatingState(compressedSize: compressedSize, limit: limit)) - } - - /// Update the state with the result of `Zlib.ZStream.inflate(outputBuffer:outputBufferSize:)`. - mutating func update(with result: Zlib.ZStream.InflateResult) throws { - switch (result.outcome, self) { - case var (.outputBufferTooSmall, .inflating(state)): - guard state.outputBufferSize < state.maxDecompressedSize else { - // We hit the decompression limit and last time we clamped our output buffer size; we - // can't use a larger buffer without exceeding the limit. - throw GRPCError.DecompressionLimitExceeded(compressedSize: state.compressedSize) - .captureContext() - } - state.increaseOutputBufferSize() - self = .inflating(state) - - case let (.complete, .inflating(state)): - // Since we request a _minimum_ output buffer size from `ByteBuffer` it's possible that - // the decompressed size exceeded the decompression limit. - guard result.totalBytesWritten <= state.maxDecompressedSize else { - throw GRPCError.DecompressionLimitExceeded(compressedSize: state.compressedSize) - .captureContext() - } - self = .inflated - - case (.complete, .inflated), - (.outputBufferTooSmall, .inflated): - preconditionFailure("invalid outcome '\(result.outcome)'; inflation is already complete") - } - } - } - - struct InflatingState { - /// The compressed size of the data to inflate. - let compressedSize: Int - - /// The maximum size the decompressed data may be, according to the user-defined - /// decompression limit. - let maxDecompressedSize: Int - - /// The minimum size requested for the output buffer. - private(set) var outputBufferSize: Int - - init(compressedSize: Int, limit: DecompressionLimit) { - self.compressedSize = compressedSize - self.maxDecompressedSize = limit.maximumDecompressedSize(compressedSize: compressedSize) - self.outputBufferSize = compressedSize - self.increaseOutputBufferSize() - } - - /// Increase the output buffer size without exceeding `maxDecompressedSize`. - mutating func increaseOutputBufferSize() { - let nextOutputBufferSize = 2 * self.outputBufferSize - - if nextOutputBufferSize > self.maxDecompressedSize { - self.outputBufferSize = self.maxDecompressedSize - } else { - self.outputBufferSize = nextOutputBufferSize - } - } - } - - private var stream: ZStream - private let format: CompressionFormat - private let limit: DecompressionLimit - - init(format: CompressionFormat, limit: DecompressionLimit) { - self.stream = ZStream() - self.format = format - self.limit = limit - self.initialize() - } - - deinit { - self.end() - } - - /// Resets decompression state. This must be called after each call to `inflate` if more - /// messages are to be decompressed by this instance. - func reset() { - let rc = CGRPCZlib_inflateReset(&self.stream.zstream) - - // Possible return codes: - // - Z_OK - // - Z_STREAM_ERROR: the source stream state was inconsistent. - // - // If we're in an inconsistent state we can just replace the stream and initialize it. - switch rc { - case Z_OK: - () - - case Z_STREAM_ERROR: - self.end() - self.stream = ZStream() - self.initialize() - - default: - preconditionFailure("inflateReset: unexpected return code rc=\(rc)") - } - } - - /// Inflate the readable bytes from the `input` buffer into the `output` buffer. - /// - /// - Parameters: - /// - input: The buffer read compressed bytes from. - /// - output: The buffer to write the decompressed bytes into. - /// - Returns: The number of bytes written into `output`. - @discardableResult - func inflate(_ input: inout ByteBuffer, into output: inout ByteBuffer) throws -> Int { - return try input.readWithUnsafeMutableReadableBytes { inputPointer -> (Int, Int) in - // Setup the input buffer. - self.stream.availableInputBytes = inputPointer.count - self.stream.nextInputBuffer = CGRPCZlib_castVoidToBytefPointer(inputPointer.baseAddress!) - - defer { - self.stream.availableInputBytes = 0 - self.stream.nextInputBuffer = nil - } - - var bytesWritten = 0 - var state = InflationState(compressedSize: inputPointer.count, limit: self.limit) - while case let .inflating(inflationState) = state { - // Each call to inflate writes into the buffer, so we need to take the writer index into - // account here. - let writerIndex = output.writerIndex - let minimumWritableBytes = inflationState.outputBufferSize - writerIndex - bytesWritten = - try output - .writeWithUnsafeMutableBytes(minimumWritableBytes: minimumWritableBytes) { - outputPointer in - let inflateResult = try self.stream.inflate( - outputBuffer: CGRPCZlib_castVoidToBytefPointer(outputPointer.baseAddress!), - outputBufferSize: outputPointer.count - ) - - try state.update(with: inflateResult) - return inflateResult.bytesWritten - } - } - - let bytesRead = inputPointer.count - self.stream.availableInputBytes - return (bytesRead, bytesWritten) - } - } - - private func initialize() { - let rc = CGRPCZlib_inflateInit2(&self.stream.zstream, self.format.windowBits) - - // Possible return codes: - // - Z_OK - // - Z_MEM_ERROR: not enough memory - // - // If we can't allocate memory then we can't progress anyway so not throwing an error here is - // okay. - precondition(rc == Z_OK, "inflateInit2 error: rc=\(rc) \(self.stream.lastErrorMessage ?? "")") - } - - func end() { - _ = CGRPCZlib_inflateEnd(&self.stream.zstream) - - // Possible return codes: - // - Z_OK - // - Z_STREAM_ERROR: the source stream state was inconsistent. - // - // Since we're going away there's no reason to fail here. - } - } - - // MARK: ZStream - - /// This wraps a zlib `z_stream` to provide more Swift-like access to the underlying C-struct. - struct ZStream { - var zstream: z_stream - - init() { - self.zstream = z_stream() - - self.zstream.next_in = nil - self.zstream.avail_in = 0 - - self.zstream.next_out = nil - self.zstream.avail_out = 0 - - self.zstream.zalloc = nil - self.zstream.zfree = nil - self.zstream.opaque = nil - } - - /// Number of bytes available to read `self.nextInputBuffer`. See also: `z_stream.avail_in`. - var availableInputBytes: Int { - get { - return Int(self.zstream.avail_in) - } - set { - self.zstream.avail_in = UInt32(newValue) - } - } - - /// The next input buffer that zlib should read from. See also: `z_stream.next_in`. - var nextInputBuffer: UnsafeMutablePointer! { - get { - return self.zstream.next_in - } - set { - self.zstream.next_in = newValue - } - } - - /// The remaining writable space in `nextOutputBuffer`. See also: `z_stream.avail_out`. - var availableOutputBytes: Int { - get { - return Int(self.zstream.avail_out) - } - set { - self.zstream.avail_out = UInt32(newValue) - return - } - } - - /// The next output buffer where zlib should write bytes to. See also: `z_stream.next_out`. - var nextOutputBuffer: UnsafeMutablePointer! { - get { - return self.zstream.next_out - } - set { - self.zstream.next_out = newValue - } - } - - /// The total number of bytes written to the output buffer. See also: `z_stream.total_out`. - var totalOutputBytes: Int { - return Int(self.zstream.total_out) - } - - /// The last error message that zlib wrote. No message is guaranteed on error, however, `nil` is - /// guaranteed if there is no error. See also `z_stream.msg`. - var lastErrorMessage: String? { - guard let bytes = self.zstream.msg else { - return nil - } - return String(cString: bytes) - } - - enum InflateOutcome { - /// The data was successfully inflated. - case complete - - /// A larger output buffer is required. - case outputBufferTooSmall - } - - struct InflateResult { - var bytesWritten: Int - var totalBytesWritten: Int - var outcome: InflateOutcome - } - - /// Decompress the stream into the given output buffer. - /// - /// - Parameter outputBuffer: The buffer into which to write the decompressed data. - /// - Parameter outputBufferSize: The space available in `outputBuffer`. - /// - Returns: The result of the `inflate`, whether it was successful or whether a larger - /// output buffer is required. - mutating func inflate( - outputBuffer: UnsafeMutablePointer, - outputBufferSize: Int - ) throws -> InflateResult { - self.nextOutputBuffer = outputBuffer - self.availableOutputBytes = outputBufferSize - - defer { - self.nextOutputBuffer = nil - self.availableOutputBytes = 0 - } - - let rc = CGRPCZlib_inflate(&self.zstream, Z_FINISH) - let outcome: InflateOutcome - - // Possible return codes: - // - Z_OK: some progress has been made - // - Z_STREAM_END: the end of the compressed data has been reached and all uncompressed output - // has been produced - // - Z_NEED_DICT: a preset dictionary is needed at this point - // - Z_DATA_ERROR: the input data was corrupted - // - Z_STREAM_ERROR: the stream structure was inconsistent - // - Z_MEM_ERROR there was not enough memory - // - Z_BUF_ERROR if no progress was possible or if there was not enough room in the output - // buffer when Z_FINISH is used. - // - // Note that Z_OK is not okay here since we always flush with Z_FINISH and therefore - // use Z_STREAM_END as our success criteria. - - switch rc { - case Z_STREAM_END: - outcome = .complete - - case Z_BUF_ERROR: - outcome = .outputBufferTooSmall - - default: - throw GRPCError.ZlibCompressionFailure(code: rc, message: self.lastErrorMessage) - .captureContext() - } - - return InflateResult( - bytesWritten: outputBufferSize - self.availableOutputBytes, - totalBytesWritten: self.totalOutputBytes, - outcome: outcome - ) - } - - /// Compresses the `inputBuffer` into the `outputBuffer`. - /// - /// `outputBuffer` must be large enough to store the compressed data, `deflateBound()` provides - /// an upper bound for this value. - /// - /// - Parameter outputBuffer: The buffer into which to write the compressed data. - /// - Parameter outputBufferSize: The space available in `outputBuffer`. - /// - Returns: The number of bytes written into the `outputBuffer`. - mutating func deflate( - outputBuffer: UnsafeMutablePointer, - outputBufferSize: Int - ) throws -> Int { - self.nextOutputBuffer = outputBuffer - self.availableOutputBytes = outputBufferSize - - defer { - self.nextOutputBuffer = nil - self.availableOutputBytes = 0 - } - - let rc = CGRPCZlib_deflate(&self.zstream, Z_FINISH) - - // Possible return codes: - // - Z_OK: some progress has been made - // - Z_STREAM_END: all input has been consumed and all output has been produced (only when - // flush is set to Z_FINISH) - // - Z_STREAM_ERROR: the stream state was inconsistent - // - Z_BUF_ERROR: no progress is possible - // - // The documentation notes that Z_BUF_ERROR is not fatal, and deflate() can be called again - // with more input and more output space to continue compressing. However, we - // call `deflateBound()` before `deflate()` which guarantees that the output size will not be - // larger than the value returned by `deflateBound()` if `Z_FINISH` flush is used. As such, - // the only acceptable outcome is `Z_STREAM_END`. - guard rc == Z_STREAM_END else { - throw GRPCError.ZlibCompressionFailure(code: rc, message: self.lastErrorMessage) - .captureContext() - } - - return outputBufferSize - self.availableOutputBytes - } - } - - enum CompressionFormat { - case deflate - case gzip - - var windowBits: Int32 { - switch self { - case .deflate: - return 15 - case .gzip: - return 31 - } - } - } -} diff --git a/Sources/GRPC/ConnectionBackoff.swift b/Sources/GRPC/ConnectionBackoff.swift deleted file mode 100644 index 561fc23c6..000000000 --- a/Sources/GRPC/ConnectionBackoff.swift +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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 Foundation - -/// Provides backoff timeouts for making a connection. -/// -/// This algorithm and defaults are determined by the gRPC connection backoff -/// [documentation](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md). -public struct ConnectionBackoff: Sequence, Sendable { - public typealias Iterator = ConnectionBackoffIterator - - /// The initial backoff in seconds. - public var initialBackoff: TimeInterval - - /// The maximum backoff in seconds. Note that the backoff is _before_ jitter has been applied, - /// this means that in practice the maximum backoff can be larger than this value. - public var maximumBackoff: TimeInterval - - /// The backoff multiplier. - public var multiplier: Double - - /// Backoff jitter; should be between 0 and 1. - public var jitter: Double - - /// The minimum amount of time in seconds to try connecting. - public var minimumConnectionTimeout: TimeInterval - - /// A limit on the number of times to attempt reconnection. - public var retries: Retries - - public struct Retries: Hashable, Sendable { - fileprivate enum Limit: Hashable, Sendable { - case limited(Int) - case unlimited - } - - fileprivate var limit: Limit - private init(_ limit: Limit) { - self.limit = limit - } - - /// An unlimited number of retry attempts. - public static let unlimited = Retries(.unlimited) - - /// No retry attempts will be made. - public static let none = Retries(.limited(0)) - - /// A limited number of retry attempts. `limit` must be positive. Note that a limit of zero is - /// identical to `.none`. - public static func upTo(_ limit: Int) -> Retries { - precondition(limit >= 0) - return Retries(.limited(limit)) - } - } - - /// Creates a ``ConnectionBackoff``. - /// - /// - Parameters: - /// - initialBackoff: Initial backoff in seconds, defaults to 1.0. - /// - maximumBackoff: Maximum backoff in seconds (prior to adding jitter), defaults to 120.0. - /// - multiplier: Backoff multiplier, defaults to 1.6. - /// - jitter: Backoff jitter, defaults to 0.2. - /// - minimumConnectionTimeout: Minimum connection timeout in seconds, defaults to 20.0. - /// - retries: A limit on the number of times to retry establishing a connection. - /// Defaults to `.unlimited`. - public init( - initialBackoff: TimeInterval = 1.0, - maximumBackoff: TimeInterval = 120.0, - multiplier: Double = 1.6, - jitter: Double = 0.2, - minimumConnectionTimeout: TimeInterval = 20.0, - retries: Retries = .unlimited - ) { - self.initialBackoff = initialBackoff - self.maximumBackoff = maximumBackoff - self.multiplier = multiplier - self.jitter = jitter - self.minimumConnectionTimeout = minimumConnectionTimeout - self.retries = retries - } - - public func makeIterator() -> ConnectionBackoff.Iterator { - return Iterator(connectionBackoff: self) - } -} - -/// An iterator for ``ConnectionBackoff``. -public class ConnectionBackoffIterator: IteratorProtocol { - public typealias Element = (timeout: TimeInterval, backoff: TimeInterval) - - /// Creates a new connection backoff iterator with the given configuration. - public init(connectionBackoff: ConnectionBackoff) { - self.connectionBackoff = connectionBackoff - self.unjitteredBackoff = connectionBackoff.initialBackoff - - // Since the first backoff is `initialBackoff` it must be generated here instead of - // by `makeNextElement`. - let backoff = min(connectionBackoff.initialBackoff, connectionBackoff.maximumBackoff) - self.initialElement = self.makeElement(backoff: backoff) - } - - /// The configuration being used. - private var connectionBackoff: ConnectionBackoff - - /// The backoff in seconds, without jitter. - private var unjitteredBackoff: TimeInterval - - /// The first element to return. Since the first backoff is defined as `initialBackoff` we can't - /// compute it on-the-fly. - private var initialElement: Element? - - /// Returns the next pair of connection timeout and backoff (in that order) to use should the - /// connection attempt fail. - public func next() -> Element? { - // Should we make another element? - switch self.connectionBackoff.retries.limit { - // Always make a new element. - case .unlimited: - () - - // Use up one from our remaining limit. - case let .limited(limit) where limit > 0: - self.connectionBackoff.retries.limit = .limited(limit - 1) - - // limit must be <= 0, no new element. - case .limited: - return nil - } - - if let initial = self.initialElement { - self.initialElement = nil - return initial - } else { - return self.makeNextElement() - } - } - - /// Produces the next element to return. - private func makeNextElement() -> Element { - let unjittered = self.unjitteredBackoff * self.connectionBackoff.multiplier - self.unjitteredBackoff = min(unjittered, self.connectionBackoff.maximumBackoff) - - let backoff = self.jittered(value: self.unjitteredBackoff) - return self.makeElement(backoff: backoff) - } - - /// Make a timeout-backoff pair from the given backoff. The timeout is the `max` of the backoff - /// and `connectionBackoff.minimumConnectionTimeout`. - private func makeElement(backoff: TimeInterval) -> Element { - let timeout = max(backoff, self.connectionBackoff.minimumConnectionTimeout) - return (timeout, backoff) - } - - /// Adds 'jitter' to the given value. - private func jittered(value: TimeInterval) -> TimeInterval { - let lower = -self.connectionBackoff.jitter * value - let upper = self.connectionBackoff.jitter * value - return value + TimeInterval.random(in: lower ... upper) - } -} diff --git a/Sources/GRPC/ConnectionKeepalive.swift b/Sources/GRPC/ConnectionKeepalive.swift deleted file mode 100644 index f3aadf07a..000000000 --- a/Sources/GRPC/ConnectionKeepalive.swift +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -/// Provides keepalive pings. -/// -/// The defaults are determined by the gRPC keepalive -/// [documentation] (https://github.com/grpc/grpc/blob/master/doc/keepalive.md). -public struct ClientConnectionKeepalive: Hashable, Sendable { - private func checkInvariants(line: UInt = #line) { - precondition(self.timeout < self.interval, "'timeout' must be less than 'interval'", line: line) - } - - /// The amount of time to wait before sending a keepalive ping. - public var interval: TimeAmount { - didSet { self.checkInvariants() } - } - - /// The amount of time to wait for an acknowledgment. - /// If it does not receive an acknowledgment within this time, it will close the connection - /// This value must be less than ``interval``. - public var timeout: TimeAmount { - didSet { self.checkInvariants() } - } - - /// Send keepalive pings even if there are no calls in flight. - public var permitWithoutCalls: Bool - - /// Maximum number of pings that can be sent when there is no data/header frame to be sent. - public var maximumPingsWithoutData: UInt - - /// If there are no data/header frames being received: - /// The minimum amount of time to wait between successive pings. - public var minimumSentPingIntervalWithoutData: TimeAmount - - public init( - interval: TimeAmount = .nanoseconds(Int64.max), - timeout: TimeAmount = .seconds(20), - permitWithoutCalls: Bool = false, - maximumPingsWithoutData: UInt = 2, - minimumSentPingIntervalWithoutData: TimeAmount = .minutes(5) - ) { - self.interval = interval - self.timeout = timeout - self.permitWithoutCalls = permitWithoutCalls - self.maximumPingsWithoutData = maximumPingsWithoutData - self.minimumSentPingIntervalWithoutData = minimumSentPingIntervalWithoutData - self.checkInvariants() - } -} - -extension ClientConnectionKeepalive { - /// Applies jitter to the ``interval``. - /// - /// The current ``interval`` will be adjusted by no more than `maxJitter` in either direction, - /// that is the ``interval`` may increase or decrease by no more than `maxJitter`. As - /// the ``timeout`` must be strictly less than the ``interval``, the lower range of the jittered - /// interval is clamped to `max(interval - maxJitter, timeout + .nanoseconds(1)))`. - /// - /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may - /// be applied in either direction. - public mutating func jitterInterval(byAtMost maxJitter: TimeAmount) { - // The interval must be larger than the timeout so clamp the lower bound to be greater than - // the timeout. - let lowerBound = max(self.interval - maxJitter, self.timeout + .nanoseconds(1)) - let upperBound = self.interval + maxJitter - self.interval = .nanoseconds(.random(in: lowerBound.nanoseconds ... upperBound.nanoseconds)) - } - - /// Returns a new ``ClientConnectionKeepalive`` with a jittered ``interval``. - /// - /// See also ``jitterInterval(byAtMost:)``. - /// - /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may - /// be applied in either direction. - /// - Returns: A new ``ClientConnectionKeepalive``. - public func jitteringInterval(byAtMost maxJitter: TimeAmount) -> Self { - var copy = self - copy.jitterInterval(byAtMost: maxJitter) - return copy - } -} - -public struct ServerConnectionKeepalive: Hashable { - private func checkInvariants(line: UInt = #line) { - precondition(self.timeout < self.interval, "'timeout' must be less than 'interval'", line: line) - } - - /// The amount of time to wait before sending a keepalive ping. - public var interval: TimeAmount { - didSet { self.checkInvariants() } - } - - /// The amount of time to wait for an acknowledgment. - /// If it does not receive an acknowledgment within this time, it will close the connection - /// This value must be less than ``interval``. - public var timeout: TimeAmount { - didSet { self.checkInvariants() } - } - - /// Send keepalive pings even if there are no calls in flight. - public var permitWithoutCalls: Bool - - /// Maximum number of pings that can be sent when there is no data/header frame to be sent. - public var maximumPingsWithoutData: UInt - - /// If there are no data/header frames being received: - /// The minimum amount of time to wait between successive pings. - public var minimumSentPingIntervalWithoutData: TimeAmount - - /// If there are no data/header frames being sent: - /// The minimum amount of time expected between receiving successive pings. - /// If the time between successive pings is less than this value, then the ping will be considered a bad ping from the peer. - /// Such a ping counts as a "ping strike". - public var minimumReceivedPingIntervalWithoutData: TimeAmount - - /// Maximum number of bad pings that the server will tolerate before sending an HTTP2 GOAWAY frame and closing the connection. - /// Setting it to `0` allows the server to accept any number of bad pings. - public var maximumPingStrikes: UInt - - public init( - interval: TimeAmount = .hours(2), - timeout: TimeAmount = .seconds(20), - permitWithoutCalls: Bool = false, - maximumPingsWithoutData: UInt = 2, - minimumSentPingIntervalWithoutData: TimeAmount = .minutes(5), - minimumReceivedPingIntervalWithoutData: TimeAmount = .minutes(5), - maximumPingStrikes: UInt = 2 - ) { - self.interval = interval - self.timeout = timeout - self.permitWithoutCalls = permitWithoutCalls - self.maximumPingsWithoutData = maximumPingsWithoutData - self.minimumSentPingIntervalWithoutData = minimumSentPingIntervalWithoutData - self.minimumReceivedPingIntervalWithoutData = minimumReceivedPingIntervalWithoutData - self.maximumPingStrikes = maximumPingStrikes - self.checkInvariants() - } -} - -extension ServerConnectionKeepalive { - /// Applies jitter to the ``interval``. - /// - /// The current ``interval`` will be adjusted by no more than `maxJitter` in either direction, - /// that is the ``interval`` may increase or decrease by no more than `maxJitter`. As - /// the ``timeout`` must be strictly less than the ``interval``, the lower range of the jittered - /// interval is clamped to `max(interval - maxJitter, timeout + .nanoseconds(1)))`. - /// - /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may - /// be applied in either direction. - public mutating func jitterInterval(byAtMost maxJitter: TimeAmount) { - // The interval must be larger than the timeout so clamp the lower bound to be greater than - // the timeout. - let lowerBound = max(self.interval - maxJitter, self.timeout + .nanoseconds(1)) - let upperBound = self.interval + maxJitter - self.interval = .nanoseconds(.random(in: lowerBound.nanoseconds ... upperBound.nanoseconds)) - } - - /// Returns a new ``ClientConnectionKeepalive`` with a jittered ``interval``. - /// - /// See also ``jitterInterval(byAtMost:)``. - /// - /// - Parameter maxJitter: The maximum amount of jitter to apply to the ``interval``, which may - /// be applied in either direction. - /// - Returns: A new ``ClientConnectionKeepalive``. - public func jitteringInterval(byAtMost maxJitter: TimeAmount) -> Self { - var copy = self - copy.jitterInterval(byAtMost: maxJitter) - return copy - } -} diff --git a/Sources/GRPC/ConnectionManager+Delegates.swift b/Sources/GRPC/ConnectionManager+Delegates.swift deleted file mode 100644 index 5687dc52b..000000000 --- a/Sources/GRPC/ConnectionManager+Delegates.swift +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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. - */ - -internal protocol ConnectionManagerConnectivityDelegate { - /// The state of the connection changed. - /// - /// - Parameters: - /// - connectionManager: The connection manager reporting the change of state. - /// - oldState: The previous `ConnectivityState`. - /// - newState: The current `ConnectivityState`. - func connectionStateDidChange( - _ connectionManager: ConnectionManager, - from oldState: _ConnectivityState, - to newState: _ConnectivityState - ) - - /// The connection is quiescing. - /// - /// - Parameters: - /// - connectionManager: The connection manager whose connection is quiescing. - func connectionIsQuiescing(_ connectionManager: ConnectionManager) -} - -internal protocol ConnectionManagerHTTP2Delegate { - /// An HTTP/2 stream was opened. - /// - /// - Parameters: - /// - connectionManager: The connection manager reporting the opened stream. - func streamOpened(_ connectionManager: ConnectionManager) - - /// An HTTP/2 stream was closed. - /// - /// - Parameters: - /// - connectionManager: The connection manager reporting the closed stream. - func streamClosed(_ connectionManager: ConnectionManager) - - /// The connection received a SETTINGS frame containing SETTINGS_MAX_CONCURRENT_STREAMS. - /// - /// - Parameters: - /// - connectionManager: The connection manager which received the settings update. - /// - maxConcurrentStreams: The value of SETTINGS_MAX_CONCURRENT_STREAMS. - func receivedSettingsMaxConcurrentStreams( - _ connectionManager: ConnectionManager, - maxConcurrentStreams: Int - ) -} - -// This mirrors `ConnectivityState` (which is public API) but adds `Error` as associated data -// to a few cases. -internal enum _ConnectivityState: Sendable { - case idle(Error?) - case connecting - case ready - case transientFailure(Error) - case shutdown - - /// Returns whether this state is the same as the other state (ignoring any associated data). - internal func isSameState(as other: _ConnectivityState) -> Bool { - switch (self, other) { - case (.idle, .idle), - (.connecting, .connecting), - (.ready, .ready), - (.transientFailure, .transientFailure), - (.shutdown, .shutdown): - return true - default: - return false - } - } -} - -extension ConnectivityState { - internal init(_ state: _ConnectivityState) { - switch state { - case .idle: - self = .idle - case .connecting: - self = .connecting - case .ready: - self = .ready - case .transientFailure: - self = .transientFailure - case .shutdown: - self = .shutdown - } - } -} diff --git a/Sources/GRPC/ConnectionManager.swift b/Sources/GRPC/ConnectionManager.swift deleted file mode 100644 index 93cbc7527..000000000 --- a/Sources/GRPC/ConnectionManager.swift +++ /dev/null @@ -1,1232 +0,0 @@ -/* - * Copyright 2020, 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 Foundation -import Logging -import NIOConcurrencyHelpers -import NIOCore -import NIOHTTP2 - -// Unchecked because mutable state is always accessed and modified on a particular event loop. -// APIs which _may_ be called from different threads execute onto the correct event loop first. -// APIs which _must_ be called from an exact event loop have preconditions checking that the correct -// event loop is being used. -@usableFromInline -internal final class ConnectionManager: @unchecked Sendable { - - /// Whether the connection managed by this manager should be allowed to go idle and be closed, or - /// if it should remain open indefinitely even when there are no active streams. - internal enum IdleBehavior { - case closeWhenIdleTimeout - case neverGoIdle - } - - internal enum Reconnect { - case none - case after(TimeInterval) - } - - internal struct ConnectingState { - var backoffIterator: ConnectionBackoffIterator? - var reconnect: Reconnect - var connectError: Error? - - var candidate: EventLoopFuture - var readyChannelMuxPromise: EventLoopPromise - var candidateMuxPromise: EventLoopPromise - } - - internal struct ConnectedState { - var backoffIterator: ConnectionBackoffIterator? - var reconnect: Reconnect - var candidate: Channel - var readyChannelMuxPromise: EventLoopPromise - var multiplexer: HTTP2StreamMultiplexer - var error: Error? - - init(from state: ConnectingState, candidate: Channel, multiplexer: HTTP2StreamMultiplexer) { - self.backoffIterator = state.backoffIterator - self.reconnect = state.reconnect - self.candidate = candidate - self.readyChannelMuxPromise = state.readyChannelMuxPromise - self.multiplexer = multiplexer - } - } - - internal struct ReadyState { - var channel: Channel - var multiplexer: HTTP2StreamMultiplexer - var error: Error? - - init(from state: ConnectedState) { - self.channel = state.candidate - self.multiplexer = state.multiplexer - } - } - - internal struct TransientFailureState { - var backoffIterator: ConnectionBackoffIterator? - var readyChannelMuxPromise: EventLoopPromise - var scheduled: Scheduled - var reason: Error - - init(from state: ConnectingState, scheduled: Scheduled, reason: Error?) { - self.backoffIterator = state.backoffIterator - self.readyChannelMuxPromise = state.readyChannelMuxPromise - self.scheduled = scheduled - self.reason = - reason - ?? GRPCStatus( - code: .unavailable, - message: "Unexpected connection drop" - ) - } - - init(from state: ConnectedState, scheduled: Scheduled) { - self.backoffIterator = state.backoffIterator - self.readyChannelMuxPromise = state.readyChannelMuxPromise - self.scheduled = scheduled - self.reason = - state.error - ?? GRPCStatus( - code: .unavailable, - message: "Unexpected connection drop" - ) - } - - init( - from state: ReadyState, - scheduled: Scheduled, - backoffIterator: ConnectionBackoffIterator? - ) { - self.backoffIterator = backoffIterator - self.readyChannelMuxPromise = state.channel.eventLoop.makePromise() - self.scheduled = scheduled - self.reason = - state.error - ?? GRPCStatus( - code: .unavailable, - message: "Unexpected connection drop" - ) - } - } - - internal struct ShutdownState { - var closeFuture: EventLoopFuture - /// The reason we are shutdown. Any requests for a `Channel` in this state will be failed with - /// this error. - var reason: Error - - init(closeFuture: EventLoopFuture, reason: Error) { - self.closeFuture = closeFuture - self.reason = reason - } - - static func shutdownByUser(closeFuture: EventLoopFuture) -> ShutdownState { - return ShutdownState( - closeFuture: closeFuture, - reason: GRPCStatus(code: .unavailable, message: "Connection was shutdown by the user") - ) - } - } - - internal enum State { - /// No `Channel` is required. - /// - /// Valid next states: - /// - `connecting` - /// - `shutdown` - case idle(lastError: Error?) - - /// We're actively trying to establish a connection. - /// - /// Valid next states: - /// - `active` - /// - `transientFailure` (if our attempt fails and we're going to try again) - /// - `shutdown` - case connecting(ConnectingState) - - /// We've established a `Channel`, it might not be suitable (TLS handshake may fail, etc.). - /// Our signal to be 'ready' is the initial HTTP/2 SETTINGS frame. - /// - /// Valid next states: - /// - `ready` - /// - `transientFailure` (if we our handshake fails or other error happens and we can attempt - /// to re-establish the connection) - /// - `shutdown` - case active(ConnectedState) - - /// We have an active `Channel` which has seen the initial HTTP/2 SETTINGS frame. We can use - /// the channel for making RPCs. - /// - /// Valid next states: - /// - `idle` (we're not serving any RPCs, we can drop the connection for now) - /// - `transientFailure` (we encountered an error and will re-establish the connection) - /// - `shutdown` - case ready(ReadyState) - - /// A `Channel` is desired, we'll attempt to create one in the future. - /// - /// Valid next states: - /// - `connecting` - /// - `shutdown` - case transientFailure(TransientFailureState) - - /// We never want another `Channel`: this state is terminal. - case shutdown(ShutdownState) - - fileprivate var label: String { - switch self { - case .idle: - return "idle" - case .connecting: - return "connecting" - case .active: - return "active" - case .ready: - return "ready" - case .transientFailure: - return "transientFailure" - case .shutdown: - return "shutdown" - } - } - } - - /// The last 'external' state we are in, a subset of the internal state. - private var externalState: _ConnectivityState = .idle(nil) - - /// Update the external state, potentially notifying a delegate about the change. - private func updateExternalState(to nextState: _ConnectivityState) { - if !self.externalState.isSameState(as: nextState) { - let oldState = self.externalState - self.externalState = nextState - self.connectivityDelegate?.connectionStateDidChange(self, from: oldState, to: nextState) - } - } - - /// Our current state. - private var state: State { - didSet { - switch self.state { - case let .idle(error): - self.updateExternalState(to: .idle(error)) - self.updateConnectionID() - - case .connecting: - self.updateExternalState(to: .connecting) - - // This is an internal state. - case .active: - () - - case .ready: - self.updateExternalState(to: .ready) - - case let .transientFailure(state): - self.updateExternalState(to: .transientFailure(state.reason)) - self.updateConnectionID() - - case .shutdown: - self.updateExternalState(to: .shutdown) - } - } - } - - /// Returns whether the state is 'idle'. - private var isIdle: Bool { - self.eventLoop.assertInEventLoop() - switch self.state { - case .idle: - return true - case .connecting, .transientFailure, .active, .ready, .shutdown: - return false - } - } - - /// Returns whether the state is 'connecting'. - private var isConnecting: Bool { - self.eventLoop.assertInEventLoop() - switch self.state { - case .connecting: - return true - case .idle, .transientFailure, .active, .ready, .shutdown: - return false - } - } - - /// Returns whether the state is 'ready'. - private var isReady: Bool { - self.eventLoop.assertInEventLoop() - switch self.state { - case .ready: - return true - case .idle, .active, .connecting, .transientFailure, .shutdown: - return false - } - } - - /// Returns whether the state is 'ready'. - private var isTransientFailure: Bool { - self.eventLoop.assertInEventLoop() - switch self.state { - case .transientFailure: - return true - case .idle, .connecting, .active, .ready, .shutdown: - return false - } - } - - /// Returns whether the state is 'shutdown'. - private var isShutdown: Bool { - self.eventLoop.assertInEventLoop() - switch self.state { - case .shutdown: - return true - case .idle, .connecting, .transientFailure, .active, .ready: - return false - } - } - - /// Returns the `HTTP2StreamMultiplexer` from the 'ready' state or `nil` if it is not available. - private var multiplexer: HTTP2StreamMultiplexer? { - self.eventLoop.assertInEventLoop() - switch self.state { - case let .ready(state): - return state.multiplexer - - case .idle, .connecting, .transientFailure, .active, .shutdown: - return nil - } - } - - /// The `EventLoop` that the managed connection will run on. - internal let eventLoop: EventLoop - - /// A delegate for connectivity changes. Executed on the `EventLoop`. - private var connectivityDelegate: ConnectionManagerConnectivityDelegate? - - /// A delegate for HTTP/2 connection changes. Executed on the `EventLoop`. - private var http2Delegate: ConnectionManagerHTTP2Delegate? - - /// An `EventLoopFuture` provider. - private let channelProvider: ConnectionManagerChannelProvider - - /// The behavior for starting a call, i.e. how patient is the caller when asking for a - /// multiplexer. - private let callStartBehavior: CallStartBehavior.Behavior - - /// The configuration to use when backing off between connection attempts, if reconnection - /// attempts should be made at all. - private let connectionBackoff: ConnectionBackoff? - - /// Whether this connection should be allowed to go idle (and thus be closed when the idle timer fires). - internal let idleBehavior: IdleBehavior - - /// A logger. - internal var logger: Logger - - internal let id: ConnectionManagerID - private var channelNumber: UInt64 - private var channelNumberLock = NIOLock() - - private var _connectionIDAndNumber: String { - return "\(self.id)/\(self.channelNumber)" - } - - private var connectionIDAndNumber: String { - return self.channelNumberLock.withLock { - return self._connectionIDAndNumber - } - } - - private func updateConnectionID() { - self.channelNumberLock.withLock { - self.channelNumber &+= 1 - self.logger[metadataKey: MetadataKey.connectionID] = "\(self._connectionIDAndNumber)" - } - } - - internal func appendMetadata(to logger: inout Logger) { - logger[metadataKey: MetadataKey.connectionID] = "\(self.connectionIDAndNumber)" - } - - internal convenience init( - configuration: ClientConnection.Configuration, - channelProvider: ConnectionManagerChannelProvider? = nil, - connectivityDelegate: ConnectionManagerConnectivityDelegate?, - idleBehavior: IdleBehavior, - logger: Logger - ) { - self.init( - eventLoop: configuration.eventLoopGroup.next(), - channelProvider: channelProvider ?? DefaultChannelProvider(configuration: configuration), - callStartBehavior: configuration.callStartBehavior.wrapped, - idleBehavior: idleBehavior, - connectionBackoff: configuration.connectionBackoff, - connectivityDelegate: connectivityDelegate, - http2Delegate: nil, - logger: logger - ) - } - - internal init( - eventLoop: EventLoop, - channelProvider: ConnectionManagerChannelProvider, - callStartBehavior: CallStartBehavior.Behavior, - idleBehavior: IdleBehavior, - connectionBackoff: ConnectionBackoff?, - connectivityDelegate: ConnectionManagerConnectivityDelegate?, - http2Delegate: ConnectionManagerHTTP2Delegate?, - logger: Logger - ) { - // Setup the logger. - var logger = logger - let connectionID = ConnectionManagerID() - let channelNumber: UInt64 = 0 - logger[metadataKey: MetadataKey.connectionID] = "\(connectionID)/\(channelNumber)" - - self.eventLoop = eventLoop - self.state = .idle(lastError: nil) - - self.channelProvider = channelProvider - self.callStartBehavior = callStartBehavior - self.connectionBackoff = connectionBackoff - self.connectivityDelegate = connectivityDelegate - self.http2Delegate = http2Delegate - self.idleBehavior = idleBehavior - - self.id = connectionID - self.channelNumber = channelNumber - self.logger = logger - } - - /// Get the multiplexer from the underlying channel handling gRPC calls. - /// if the `ConnectionManager` was configured to be `fastFailure` this will have - /// one chance to connect - if not reconnections are managed here. - internal func getHTTP2Multiplexer() -> EventLoopFuture { - func getHTTP2Multiplexer0() -> EventLoopFuture { - switch self.callStartBehavior { - case .waitsForConnectivity: - return self.getHTTP2MultiplexerPatient() - case .fastFailure: - return self.getHTTP2MultiplexerOptimistic() - } - } - - if self.eventLoop.inEventLoop { - return getHTTP2Multiplexer0() - } else { - return self.eventLoop.flatSubmit { - getHTTP2Multiplexer0() - } - } - } - - /// Returns a future for the multiplexer which succeeded when the channel is connected. - /// Reconnects are handled if necessary. - private func getHTTP2MultiplexerPatient() -> EventLoopFuture { - let multiplexer: EventLoopFuture - - switch self.state { - case .idle: - self.startConnecting() - // We started connecting so we must transition to the `connecting` state. - guard case let .connecting(connecting) = self.state else { - self.unreachableState() - } - multiplexer = connecting.readyChannelMuxPromise.futureResult - - case let .connecting(state): - multiplexer = state.readyChannelMuxPromise.futureResult - - case let .active(state): - multiplexer = state.readyChannelMuxPromise.futureResult - - case let .ready(state): - multiplexer = self.eventLoop.makeSucceededFuture(state.multiplexer) - - case let .transientFailure(state): - multiplexer = state.readyChannelMuxPromise.futureResult - - case let .shutdown(state): - multiplexer = self.eventLoop.makeFailedFuture(state.reason) - } - - self.logger.debug( - "vending multiplexer future", - metadata: [ - "connectivity_state": "\(self.state.label)" - ] - ) - - return multiplexer - } - - /// Returns a future for the current HTTP/2 stream multiplexer, or future HTTP/2 stream multiplexer from the current connection - /// attempt, or if the state is 'idle' returns the future for the next connection attempt. - /// - /// Note: if the state is 'transientFailure' or 'shutdown' then a failed future will be returned. - private func getHTTP2MultiplexerOptimistic() -> EventLoopFuture { - // `getHTTP2Multiplexer` makes sure we're on the event loop but let's just be sure. - self.eventLoop.preconditionInEventLoop() - - let muxFuture: EventLoopFuture = { () in - switch self.state { - case .idle: - self.startConnecting() - // We started connecting so we must transition to the `connecting` state. - guard case let .connecting(connecting) = self.state else { - self.unreachableState() - } - return connecting.candidateMuxPromise.futureResult - case let .connecting(state): - return state.candidateMuxPromise.futureResult - case let .active(active): - return self.eventLoop.makeSucceededFuture(active.multiplexer) - case let .ready(ready): - return self.eventLoop.makeSucceededFuture(ready.multiplexer) - case let .transientFailure(state): - return self.eventLoop.makeFailedFuture(state.reason) - case let .shutdown(state): - return self.eventLoop.makeFailedFuture(state.reason) - } - }() - - self.logger.debug( - "vending fast-failing multiplexer future", - metadata: [ - "connectivity_state": "\(self.state.label)" - ] - ) - return muxFuture - } - - @usableFromInline - internal enum ShutdownMode { - /// Closes the underlying channel without waiting for existing RPCs to complete. - case forceful - /// Allows running RPCs to run their course before closing the underlying channel. No new - /// streams may be created. - case graceful(NIODeadline) - } - - /// Shutdown the underlying connection. - /// - /// - Note: Initiating a `forceful` shutdown after a `graceful` shutdown has no effect. - internal func shutdown(mode: ShutdownMode) -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.shutdown(mode: mode, promise: promise) - return promise.futureResult - } - - /// Shutdown the underlying connection. - /// - /// - Note: Initiating a `forceful` shutdown after a `graceful` shutdown has no effect. - internal func shutdown(mode: ShutdownMode, promise: EventLoopPromise) { - if self.eventLoop.inEventLoop { - self._shutdown(mode: mode, promise: promise) - } else { - self.eventLoop.execute { - self._shutdown(mode: mode, promise: promise) - } - } - } - - private func _shutdown(mode: ShutdownMode, promise: EventLoopPromise) { - self.logger.debug( - "shutting down connection", - metadata: [ - "connectivity_state": "\(self.state.label)", - "shutdown.mode": "\(mode)", - ] - ) - - switch self.state { - // We don't have a channel and we don't want one, easy! - case .idle: - let shutdown: ShutdownState = .shutdownByUser(closeFuture: promise.futureResult) - self.state = .shutdown(shutdown) - promise.succeed(()) - - // We're mid-connection: the application doesn't have any 'ready' channels so we'll succeed - // the shutdown future and deal with any fallout from the connecting channel without the - // application knowing. - case let .connecting(state): - let shutdown: ShutdownState = .shutdownByUser(closeFuture: promise.futureResult) - self.state = .shutdown(shutdown) - - // Fail the ready channel mux promise: we're shutting down so even if we manage to successfully - // connect the application shouldn't have access to the channel or multiplexer. - state.readyChannelMuxPromise.fail(GRPCStatus(code: .unavailable, message: nil)) - state.candidateMuxPromise.fail(GRPCStatus(code: .unavailable, message: nil)) - - // Complete the shutdown promise when the connection attempt has completed. - state.candidate.whenComplete { - switch $0 { - case let .success(channel): - // In case we do successfully connect, close on the next loop tick. When connecting a - // channel NIO will complete the promise for the channel before firing channel active. - // That means we may close and fire inactive before active which HTTP/2 will be unhappy - // about. - self.eventLoop.execute { - channel.close(mode: .all, promise: nil) - promise.completeWith(channel.closeFuture.recoveringFromUncleanShutdown()) - } - - case .failure: - // We failed to connect, that's fine we still shutdown successfully. - promise.succeed(()) - } - } - - // We have an active channel but the application doesn't know about it yet. We'll do the same - // as for `.connecting`. - case let .active(state): - let shutdown: ShutdownState = .shutdownByUser(closeFuture: promise.futureResult) - self.state = .shutdown(shutdown) - - // Fail the ready channel mux promise: we're shutting down so even if we manage to successfully - // connect the application shouldn't have access to the channel or multiplexer. - state.readyChannelMuxPromise.fail(GRPCStatus(code: .unavailable, message: nil)) - // We have a channel, close it. We only create streams in the ready state so there's no need - // to quiesce here. - state.candidate.close(mode: .all, promise: nil) - promise.completeWith(state.candidate.closeFuture.recoveringFromUncleanShutdown()) - - // The channel is up and running: the application could be using it. We can close it and - // return the `closeFuture`. - case let .ready(state): - let shutdown: ShutdownState = .shutdownByUser(closeFuture: promise.futureResult) - self.state = .shutdown(shutdown) - - switch mode { - case .forceful: - // We have a channel, close it. - state.channel.close(mode: .all, promise: nil) - - case let .graceful(deadline): - // If we don't close by the deadline forcibly close the channel. - let scheduledForceClose = state.channel.eventLoop.scheduleTask(deadline: deadline) { - self.logger.info("shutdown timer expired, forcibly closing connection") - state.channel.close(mode: .all, promise: nil) - } - - // Cancel the force close if we close normally first. - state.channel.closeFuture.whenComplete { _ in - scheduledForceClose.cancel() - } - - // Tell the channel to quiesce. It will be picked up by the idle handler which will close - // the channel when all streams have been closed. - state.channel.pipeline.fireUserInboundEventTriggered(ChannelShouldQuiesceEvent()) - } - - // Complete the promise when we eventually close. - promise.completeWith(state.channel.closeFuture.recoveringFromUncleanShutdown()) - - // Like `.connecting` and `.active` the application does not have a `.ready` channel. We'll - // do the same but also cancel any scheduled connection attempts and deal with any fallout - // if we cancelled too late. - case let .transientFailure(state): - let shutdown: ShutdownState = .shutdownByUser(closeFuture: promise.futureResult) - self.state = .shutdown(shutdown) - - // Stop the creation of a new channel, if we can. If we can't then the task to - // `startConnecting()` will see our new `shutdown` state and ignore the request to connect. - state.scheduled.cancel() - - // Fail the ready channel mux promise: we're shutting down so even if we manage to successfully - // connect the application shouldn't should have access to the channel. - state.readyChannelMuxPromise.fail(shutdown.reason) - - // No active channel, so complete the shutdown promise now. - promise.succeed(()) - - // We're already shutdown; there's nothing to do. - case let .shutdown(state): - promise.completeWith(state.closeFuture) - } - } - - /// Registers a callback which fires when the current active connection is closed. - /// - /// If there is a connection, the callback will be invoked with `true` when the connection is - /// closed. Otherwise the callback is invoked with `false`. - internal func onCurrentConnectionClose(_ onClose: @escaping (Bool) -> Void) { - if self.eventLoop.inEventLoop { - self._onCurrentConnectionClose(onClose) - } else { - self.eventLoop.execute { - self._onCurrentConnectionClose(onClose) - } - } - } - - private func _onCurrentConnectionClose(_ onClose: @escaping (Bool) -> Void) { - self.eventLoop.assertInEventLoop() - - switch self.state { - case let .ready(state): - state.channel.closeFuture.whenComplete { _ in onClose(true) } - case .idle, .connecting, .active, .transientFailure, .shutdown: - onClose(false) - } - } - - // MARK: - State changes from the channel handler. - - /// The channel caught an error. Hold on to it until the channel becomes inactive, it may provide - /// some context. - internal func channelError(_ error: Error) { - self.eventLoop.preconditionInEventLoop() - - switch self.state { - // Hitting an error in idle is a surprise, but not really something we do anything about. Either the - // error is channel fatal, in which case we'll see channelInactive soon (acceptable), or it's not, - // and future I/O will either fail fast or work. In either case, all we do is log this and move on. - case .idle: - self.logger.warning( - "ignoring unexpected error in idle", - metadata: [ - MetadataKey.error: "\(error)" - ] - ) - - case .connecting(var state): - // Record the error, the channel promise will notify the manager of any error which occurs - // while connecting. It may be overridden by this error if it contains more relevant - // information - if state.connectError == nil { - state.connectError = error - self.state = .connecting(state) - - // The pool is only notified of connection errors when the connection transitions to the - // transient failure state. However, in some cases (i.e. with NIOTS), errors can be thrown - // during the connect but before the connect times out. - // - // This opens up a period of time where you can start a call and have it fail with - // deadline exceeded (because no connection was available within the configured max - // wait time for the pool) but without any diagnostic information. The information is - // available but it hasn't been made available to the pool at that point in time. - // - // The delegate can't easily be modified (it's public API) and a new API doesn't make all - // that much sense so we elect to check whether the delegate is the pool and call it - // directly. - if let pool = self.connectivityDelegate as? ConnectionPool { - pool.sync.updateMostRecentError(error) - } - } - - case var .active(state): - state.error = error - self.state = .active(state) - - case var .ready(state): - state.error = error - self.state = .ready(state) - - // If we've already in one of these states, then additional errors aren't helpful to us. - case .transientFailure, .shutdown: - () - } - } - - /// The connecting channel became `active`. Must be called on the `EventLoop`. - internal func channelActive(channel: Channel, multiplexer: HTTP2StreamMultiplexer) { - self.eventLoop.preconditionInEventLoop() - self.logger.debug( - "activating connection", - metadata: [ - "connectivity_state": "\(self.state.label)" - ] - ) - - switch self.state { - case let .connecting(connecting): - let connected = ConnectedState(from: connecting, candidate: channel, multiplexer: multiplexer) - self.state = .active(connected) - // Optimistic connections are happy this this level of setup. - connecting.candidateMuxPromise.succeed(multiplexer) - - // Application called shutdown before the channel become active; we should close it. - case .shutdown: - channel.close(mode: .all, promise: nil) - - case .idle, .transientFailure: - // Received a channelActive when not connecting. Can happen if channelActive and - // channelInactive are reordered. Ignore. - () - case .active, .ready: - // Received a second 'channelActive', already active so ignore. - () - } - } - - /// An established channel (i.e. `active` or `ready`) has become inactive: should we reconnect? - /// Must be called on the `EventLoop`. - internal func channelInactive() { - self.eventLoop.preconditionInEventLoop() - self.logger.debug( - "deactivating connection", - metadata: [ - "connectivity_state": "\(self.state.label)" - ] - ) - - switch self.state { - // We can hit inactive in connecting if we see channelInactive before channelActive; that's not - // common but we should tolerate it. - case let .connecting(connecting): - // Should we try connecting again? - switch connecting.reconnect { - // No, shutdown instead. - case .none: - self.logger.debug("shutting down connection") - - let error = GRPCStatus( - code: .unavailable, - message: "The connection was dropped and connection re-establishment is disabled" - ) - - let shutdownState = ShutdownState( - closeFuture: self.eventLoop.makeSucceededFuture(()), - reason: error - ) - - self.state = .shutdown(shutdownState) - // Shutting down, so fail the outstanding promises. - connecting.readyChannelMuxPromise.fail(error) - connecting.candidateMuxPromise.fail(error) - - // Yes, after some time. - case let .after(delay): - let error = GRPCStatus(code: .unavailable, message: "Connection closed while connecting") - // Fail the candidate mux promise. Keep the 'readyChannelMuxPromise' as we'll try again. - connecting.candidateMuxPromise.fail(error) - - let scheduled = self.eventLoop.scheduleTask(in: .seconds(timeInterval: delay)) { - self.startConnecting() - } - self.logger.debug("scheduling connection attempt", metadata: ["delay_secs": "\(delay)"]) - self.state = .transientFailure(.init(from: connecting, scheduled: scheduled, reason: nil)) - } - - // The channel is `active` but not `ready`. Should we try again? - case let .active(active): - switch active.reconnect { - // No, shutdown instead. - case .none: - self.logger.debug("shutting down connection") - - let error = GRPCStatus( - code: .unavailable, - message: "The connection was dropped and connection re-establishment is disabled" - ) - - let shutdownState = ShutdownState( - closeFuture: self.eventLoop.makeSucceededFuture(()), - reason: error - ) - - self.state = .shutdown(shutdownState) - active.readyChannelMuxPromise.fail(error) - - // Yes, after some time. - case let .after(delay): - let scheduled = self.eventLoop.scheduleTask(in: .seconds(timeInterval: delay)) { - self.startConnecting() - } - self.logger.debug("scheduling connection attempt", metadata: ["delay_secs": "\(delay)"]) - self.state = .transientFailure(TransientFailureState(from: active, scheduled: scheduled)) - } - - // The channel was ready and working fine but something went wrong. Should we try to replace - // the channel? - case let .ready(ready): - // No, no backoff is configured. - if self.connectionBackoff == nil { - self.logger.debug("shutting down connection, no reconnect configured/remaining") - self.state = .shutdown( - ShutdownState( - closeFuture: ready.channel.closeFuture, - reason: GRPCStatus( - code: .unavailable, - message: "The connection was dropped and a reconnect was not configured" - ) - ) - ) - } else { - // Yes, start connecting now. We should go via `transientFailure`, however. - let scheduled = self.eventLoop.scheduleTask(in: .nanoseconds(0)) { - self.startConnecting() - } - self.logger.debug("scheduling connection attempt", metadata: ["delay": "0"]) - let backoffIterator = self.connectionBackoff?.makeIterator() - self.state = .transientFailure( - TransientFailureState( - from: ready, - scheduled: scheduled, - backoffIterator: backoffIterator - ) - ) - } - - // This is fine: we expect the channel to become inactive after becoming idle. - case .idle: - () - - // We're already shutdown, that's fine. - case .shutdown: - () - - // Received 'channelInactive' twice; fine, ignore. - case .transientFailure: - () - } - } - - /// The channel has become ready, that is, it has seen the initial HTTP/2 SETTINGS frame. Must be - /// called on the `EventLoop`. - internal func ready() { - self.eventLoop.preconditionInEventLoop() - self.logger.debug( - "connection ready", - metadata: [ - "connectivity_state": "\(self.state.label)" - ] - ) - - switch self.state { - case let .active(connected): - self.state = .ready(ReadyState(from: connected)) - connected.readyChannelMuxPromise.succeed(connected.multiplexer) - - case .shutdown: - () - - case .idle, .transientFailure: - // No connection or connection attempt exists but connection was marked as ready. This is - // strange. Ignore it in release mode as there's nothing to close and nowehere to fire an - // error to. - assertionFailure("received initial HTTP/2 SETTINGS frame in \(self.state.label) state") - - case .connecting: - // No channel exists to receive initial HTTP/2 SETTINGS frame on... weird. Ignore in release - // mode. - assertionFailure("received initial HTTP/2 SETTINGS frame in \(self.state.label) state") - - case .ready: - // Already received initial HTTP/2 SETTINGS frame; ignore in release mode. - assertionFailure("received initial HTTP/2 SETTINGS frame in \(self.state.label) state") - } - } - - /// No active RPCs are happening on 'ready' channel: close the channel for now. Must be called on - /// the `EventLoop`. - internal func idle() { - self.eventLoop.preconditionInEventLoop() - self.logger.debug( - "idling connection", - metadata: [ - "connectivity_state": "\(self.state.label)" - ] - ) - - switch self.state { - case let .active(state): - // This state is reachable if the keepalive timer fires before we reach the ready state. - self.state = .idle(lastError: state.error) - state.readyChannelMuxPromise - .fail(GRPCStatus(code: .unavailable, message: "Idled before reaching ready state")) - - case let .ready(state): - self.state = .idle(lastError: state.error) - - case .shutdown: - // This is expected when the connection is closed by the user: when the channel becomes - // inactive and there are no outstanding RPCs, 'idle()' will be called instead of - // 'channelInactive()'. - () - - case .idle, .transientFailure: - // There's no connection to idle; ignore. - () - - case .connecting: - // The idle watchdog is started when the connection is active, this shouldn't happen - // in the connecting state. Ignore it in release mode. - assertionFailure("tried to idle a connection in the \(self.state.label) state") - } - } - - internal func streamOpened() { - self.eventLoop.assertInEventLoop() - self.http2Delegate?.streamOpened(self) - } - - internal func streamClosed() { - self.eventLoop.assertInEventLoop() - self.http2Delegate?.streamClosed(self) - } - - internal func maxConcurrentStreamsChanged(_ maxConcurrentStreams: Int) { - self.eventLoop.assertInEventLoop() - self.http2Delegate?.receivedSettingsMaxConcurrentStreams( - self, - maxConcurrentStreams: maxConcurrentStreams - ) - } - - /// The connection has started quiescing: notify the connectivity monitor of this. - internal func beginQuiescing() { - self.eventLoop.assertInEventLoop() - self.connectivityDelegate?.connectionIsQuiescing(self) - } -} - -extension ConnectionManager { - // A connection attempt failed; we never established a connection. - private func connectionFailed(withError error: Error) { - self.eventLoop.preconditionInEventLoop() - - switch self.state { - case let .connecting(connecting): - let reportedError: Error - switch error as? ChannelError { - case .some(.connectTimeout): - // A more relevant error may have been caught earlier. Use that in preference to the - // timeout as it'll likely be more useful. - reportedError = connecting.connectError ?? error - default: - reportedError = error - } - - // Should we reconnect? - switch connecting.reconnect { - // No, shutdown. - case .none: - self.logger.debug("shutting down connection, no reconnect configured/remaining") - self.state = .shutdown( - ShutdownState(closeFuture: self.eventLoop.makeSucceededFuture(()), reason: reportedError) - ) - connecting.readyChannelMuxPromise.fail(reportedError) - connecting.candidateMuxPromise.fail(reportedError) - - // Yes, after a delay. - case let .after(delay): - self.logger.debug("scheduling connection attempt", metadata: ["delay": "\(delay)"]) - let scheduled = self.eventLoop.scheduleTask(in: .seconds(timeInterval: delay)) { - self.startConnecting() - } - self.state = .transientFailure( - TransientFailureState(from: connecting, scheduled: scheduled, reason: reportedError) - ) - // Candidate mux users are not willing to wait. - connecting.candidateMuxPromise.fail(reportedError) - } - - // The application must have called shutdown while we were trying to establish a connection - // which was doomed to fail anyway. That's fine, we can ignore this. - case .shutdown: - () - - // Connection attempt failed, but no connection attempt is in progress. - case .idle, .active, .ready, .transientFailure: - // Nothing we can do other than ignore in release mode. - assertionFailure("connect promise failed in \(self.state.label) state") - } - } -} - -extension ConnectionManager { - // Start establishing a connection: we can only do this from the `idle` and `transientFailure` - // states. Must be called on the `EventLoop`. - private func startConnecting() { - self.eventLoop.assertInEventLoop() - switch self.state { - case .idle: - let iterator = self.connectionBackoff?.makeIterator() - - // The iterator produces the connect timeout and the backoff to use for the next attempt. This - // is unfortunate if retries is set to none because we need to connect timeout but not the - // backoff yet the iterator will not return a value to us. To workaround this we grab the - // connect timeout and override it. - let connectTimeoutOverride: TimeAmount? - if let backoff = self.connectionBackoff, backoff.retries == .none { - connectTimeoutOverride = .seconds(timeInterval: backoff.minimumConnectionTimeout) - } else { - connectTimeoutOverride = nil - } - - self.startConnecting( - backoffIterator: iterator, - muxPromise: self.eventLoop.makePromise(), - connectTimeoutOverride: connectTimeoutOverride - ) - - case let .transientFailure(pending): - self.startConnecting( - backoffIterator: pending.backoffIterator, - muxPromise: pending.readyChannelMuxPromise - ) - - // We shutdown before a scheduled connection attempt had started. - case .shutdown: - () - - // We only call startConnecting() if the connection does not exist and after checking what the - // current state is, so none of these states should be reachable. - case .connecting: - self.unreachableState() - case .active: - self.unreachableState() - case .ready: - self.unreachableState() - } - } - - private func startConnecting( - backoffIterator: ConnectionBackoffIterator?, - muxPromise: EventLoopPromise, - connectTimeoutOverride: TimeAmount? = nil - ) { - let timeoutAndBackoff = backoffIterator?.next() - - // We're already on the event loop: submit the connect so it starts after we've made the - // state change to `.connecting`. - self.eventLoop.assertInEventLoop() - - let candidate: EventLoopFuture = self.eventLoop.flatSubmit { - let connectTimeout: TimeAmount? - if let connectTimeoutOverride = connectTimeoutOverride { - connectTimeout = connectTimeoutOverride - } else { - connectTimeout = timeoutAndBackoff.map { TimeAmount.seconds(timeInterval: $0.timeout) } - } - - let channel: EventLoopFuture = self.channelProvider.makeChannel( - managedBy: self, - onEventLoop: self.eventLoop, - connectTimeout: connectTimeout, - logger: self.logger - ) - - channel.whenFailure { error in - self.connectionFailed(withError: error) - } - - return channel - } - - // Should we reconnect if the candidate channel fails? - let reconnect: Reconnect = timeoutAndBackoff.map { .after($0.backoff) } ?? .none - let connecting = ConnectingState( - backoffIterator: backoffIterator, - reconnect: reconnect, - candidate: candidate, - readyChannelMuxPromise: muxPromise, - candidateMuxPromise: self.eventLoop.makePromise() - ) - - self.state = .connecting(connecting) - } -} - -extension ConnectionManager { - /// Returns a synchronous view of the connection manager; each operation requires the caller to be - /// executing on the same `EventLoop` as the connection manager. - internal var sync: Sync { - return Sync(self) - } - - internal struct Sync { - private let manager: ConnectionManager - - fileprivate init(_ manager: ConnectionManager) { - self.manager = manager - } - - /// A delegate for connectivity changes. - internal var connectivityDelegate: ConnectionManagerConnectivityDelegate? { - get { - self.manager.eventLoop.assertInEventLoop() - return self.manager.connectivityDelegate - } - nonmutating set { - self.manager.eventLoop.assertInEventLoop() - self.manager.connectivityDelegate = newValue - } - } - - /// A delegate for HTTP/2 connection changes. - internal var http2Delegate: ConnectionManagerHTTP2Delegate? { - get { - self.manager.eventLoop.assertInEventLoop() - return self.manager.http2Delegate - } - nonmutating set { - self.manager.eventLoop.assertInEventLoop() - self.manager.http2Delegate = newValue - } - } - - /// Returns `true` if the connection is in the idle state. - internal var isIdle: Bool { - return self.manager.isIdle - } - - /// Returns `true` if the connection is in the connecting state. - internal var isConnecting: Bool { - return self.manager.isConnecting - } - - /// Returns `true` if the connection is in the ready state. - internal var isReady: Bool { - return self.manager.isReady - } - - /// Returns `true` if the connection is in the transient failure state. - internal var isTransientFailure: Bool { - return self.manager.isTransientFailure - } - - /// Returns `true` if the connection is in the shutdown state. - internal var isShutdown: Bool { - return self.manager.isShutdown - } - - /// Returns the `multiplexer` from a connection in the `ready` state or `nil` if it is any - /// other state. - internal var multiplexer: HTTP2StreamMultiplexer? { - return self.manager.multiplexer - } - - // Start establishing a connection. Must only be called when `isIdle` is `true`. - internal func startConnecting() { - self.manager.startConnecting() - } - } -} - -extension ConnectionManager { - private func unreachableState( - function: StaticString = #function, - file: StaticString = #fileID, - line: UInt = #line - ) -> Never { - fatalError("Invalid state \(self.state) for \(function)", file: file, line: line) - } -} diff --git a/Sources/GRPC/ConnectionManagerChannelProvider.swift b/Sources/GRPC/ConnectionManagerChannelProvider.swift deleted file mode 100644 index 5a2210e73..000000000 --- a/Sources/GRPC/ConnectionManagerChannelProvider.swift +++ /dev/null @@ -1,227 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOPosix -import NIOTransportServices - -#if canImport(NIOSSL) -import NIOSSL -#endif - -@usableFromInline -internal protocol ConnectionManagerChannelProvider { - /// Make an `EventLoopFuture`. - /// - /// - Parameters: - /// - connectionManager: The `ConnectionManager` requesting the `Channel`. - /// - eventLoop: The `EventLoop` to use for the`Channel`. - /// - connectTimeout: Optional connection timeout when starting the connection. - /// - logger: A logger. - func makeChannel( - managedBy connectionManager: ConnectionManager, - onEventLoop eventLoop: EventLoop, - connectTimeout: TimeAmount?, - logger: Logger - ) -> EventLoopFuture -} - -@usableFromInline -internal struct DefaultChannelProvider: ConnectionManagerChannelProvider { - @usableFromInline - enum TLSMode { - #if canImport(NIOSSL) - case configureWithNIOSSL(Result) - #endif // canImport(NIOSSL) - case configureWithNetworkFramework - case disabled - } - - @usableFromInline - internal var connectionTarget: ConnectionTarget - @usableFromInline - internal var connectionKeepalive: ClientConnectionKeepalive - @usableFromInline - internal var connectionIdleTimeout: TimeAmount - - @usableFromInline - internal var tlsMode: TLSMode - @usableFromInline - internal var tlsConfiguration: GRPCTLSConfiguration? - - @usableFromInline - internal var httpTargetWindowSize: Int - @usableFromInline - internal var httpMaxFrameSize: Int - - @usableFromInline - internal var errorDelegate: Optional - @usableFromInline - internal var debugChannelInitializer: Optional<(Channel) -> EventLoopFuture> - - @inlinable - internal init( - connectionTarget: ConnectionTarget, - connectionKeepalive: ClientConnectionKeepalive, - connectionIdleTimeout: TimeAmount, - tlsMode: TLSMode, - tlsConfiguration: GRPCTLSConfiguration?, - httpTargetWindowSize: Int, - httpMaxFrameSize: Int, - errorDelegate: ClientErrorDelegate?, - debugChannelInitializer: ((Channel) -> EventLoopFuture)? - ) { - self.connectionTarget = connectionTarget - self.connectionKeepalive = connectionKeepalive - self.connectionIdleTimeout = connectionIdleTimeout - - self.tlsMode = tlsMode - self.tlsConfiguration = tlsConfiguration - - self.httpTargetWindowSize = httpTargetWindowSize - self.httpMaxFrameSize = httpMaxFrameSize - - self.errorDelegate = errorDelegate - self.debugChannelInitializer = debugChannelInitializer - } - - internal init(configuration: ClientConnection.Configuration) { - // Making a `NIOSSLContext` is expensive and we should only do it (at most) once per TLS - // configuration. We do it now and store it in our `tlsMode` and surface any error during - // channel creation (we're limited by our API in when we can throw any error). - let tlsMode: TLSMode - - if let tlsConfiguration = configuration.tlsConfiguration { - if tlsConfiguration.isNetworkFrameworkTLSBackend { - tlsMode = .configureWithNetworkFramework - } else { - #if canImport(NIOSSL) - // The '!' is okay here, we have a `tlsConfiguration` (so we must be using TLS) and we know - // it's not backed by Network.framework, so it must be backed by NIOSSL. - tlsMode = .configureWithNIOSSL(Result { try tlsConfiguration.makeNIOSSLContext()! }) - #else - // TLS is configured, and we aren't using a Network.framework TLS backend, so we must be - // using NIOSSL, so we must be able to import it. - fatalError() - #endif // canImport(NIOSSL) - } - } else { - tlsMode = .disabled - } - - self.init( - connectionTarget: configuration.target, - connectionKeepalive: configuration.connectionKeepalive, - connectionIdleTimeout: configuration.connectionIdleTimeout, - tlsMode: tlsMode, - tlsConfiguration: configuration.tlsConfiguration, - httpTargetWindowSize: configuration.httpTargetWindowSize, - httpMaxFrameSize: configuration.httpMaxFrameSize, - errorDelegate: configuration.errorDelegate, - debugChannelInitializer: configuration.debugChannelInitializer - ) - } - - private var serverHostname: String? { - let hostname = self.tlsConfiguration?.hostnameOverride ?? self.connectionTarget.host - return hostname.isIPAddress ? nil : hostname - } - - private var hasTLS: Bool { - return self.tlsConfiguration != nil - } - - private func requiresZeroLengthWorkaround(eventLoop: EventLoop) -> Bool { - return PlatformSupport.requiresZeroLengthWriteWorkaround(group: eventLoop, hasTLS: self.hasTLS) - } - - @usableFromInline - internal func makeChannel( - managedBy connectionManager: ConnectionManager, - onEventLoop eventLoop: EventLoop, - connectTimeout: TimeAmount?, - logger: Logger - ) -> EventLoopFuture { - let hostname = self.serverHostname - let needsZeroLengthWriteWorkaround = self.requiresZeroLengthWorkaround(eventLoop: eventLoop) - - var bootstrap = PlatformSupport.makeClientBootstrap( - group: eventLoop, - tlsConfiguration: self.tlsConfiguration, - logger: logger - ) - - bootstrap = - bootstrap - .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) - .channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) - .channelInitializer { channel in - let sync = channel.pipeline.syncOperations - - do { - if needsZeroLengthWriteWorkaround { - try sync.addHandler(NIOFilterEmptyWritesHandler()) - } - - // We have a NIOSSL context to apply. If we're using TLS from NIOTS then the bootstrap - // will already have the TLS options applied. - switch self.tlsMode { - #if canImport(NIOSSL) - case let .configureWithNIOSSL(sslContext): - try sync.configureNIOSSLForGRPCClient( - sslContext: sslContext, - serverHostname: hostname, - customVerificationCallback: self.tlsConfiguration?.nioSSLCustomVerificationCallback, - logger: logger - ) - #endif // canImport(NIOSSL) - - // Network.framework TLS configuration is applied when creating the bootstrap so is a - // no-op here. - case .configureWithNetworkFramework, - .disabled: - () - } - - try sync.configureHTTP2AndGRPCHandlersForGRPCClient( - channel: channel, - connectionManager: connectionManager, - connectionKeepalive: self.connectionKeepalive, - connectionIdleTimeout: self.connectionIdleTimeout, - httpTargetWindowSize: self.httpTargetWindowSize, - httpMaxFrameSize: self.httpMaxFrameSize, - errorDelegate: self.errorDelegate, - logger: logger - ) - } catch { - return channel.eventLoop.makeFailedFuture(error) - } - - // Run the debug initializer, if there is one. - if let debugInitializer = self.debugChannelInitializer { - return debugInitializer(channel) - } else { - return channel.eventLoop.makeSucceededVoidFuture() - } - } - - if let connectTimeout = connectTimeout { - _ = bootstrap.connectTimeout(connectTimeout) - } - - return bootstrap.connect(to: self.connectionTarget) - } -} diff --git a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift b/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift deleted file mode 100644 index a0ce0519f..000000000 --- a/Sources/GRPC/ConnectionPool/ConnectionManagerID.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 struct Foundation.UUID - -@usableFromInline -internal struct ConnectionManagerID: Hashable, CustomStringConvertible, Sendable { - @usableFromInline - internal let id: String - - @usableFromInline - internal init() { - self.id = UUID().uuidString - } - - @usableFromInline - internal var description: String { - return String(describing: self.id) - } -} diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift deleted file mode 100644 index 3ebd6fbd4..000000000 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+PerConnectionState.swift +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 NIOHTTP2 - -extension ConnectionPool { - @usableFromInline - internal struct PerConnectionState { - /// The connection manager for this connection. - @usableFromInline - internal var manager: ConnectionManager - - /// Stream availability for this connection, `nil` if the connection is not available. - @usableFromInline - internal var _availability: StreamAvailability? - - @usableFromInline - internal var isAvailable: Bool { - return self._availability != nil - } - - @usableFromInline - internal var isQuiescing: Bool { - get { - return self._availability?.isQuiescing ?? false - } - set { - self._availability?.isQuiescing = true - } - } - - @usableFromInline - internal struct StreamAvailability { - @usableFromInline - struct Utilization { - @usableFromInline - var used: Int - @usableFromInline - var capacity: Int - - @usableFromInline - init(used: Int, capacity: Int) { - self.used = used - self.capacity = capacity - } - } - - @usableFromInline - var multiplexer: HTTP2StreamMultiplexer - /// Maximum number of available streams. - @usableFromInline - var maxAvailable: Int - /// Number of streams reserved. - @usableFromInline - var reserved: Int = 0 - /// Number of streams opened. - @usableFromInline - var open: Int = 0 - @usableFromInline - var isQuiescing = false - /// Number of available streams. - @usableFromInline - var available: Int { - return self.isQuiescing ? 0 : self.maxAvailable - self.reserved - } - - /// Increment the reserved streams and return the multiplexer. - @usableFromInline - mutating func reserve() -> HTTP2StreamMultiplexer { - assert(!self.isQuiescing) - self.reserved += 1 - return self.multiplexer - } - - @usableFromInline - mutating func opened() -> Utilization { - self.open += 1 - return .init(used: self.open, capacity: self.maxAvailable) - } - - /// Decrement the reserved streams by one. - @usableFromInline - mutating func `return`() -> Utilization { - self.reserved -= 1 - self.open -= 1 - assert(self.reserved >= 0) - assert(self.open >= 0) - return .init(used: self.open, capacity: self.maxAvailable) - } - } - - @usableFromInline - init(manager: ConnectionManager) { - self.manager = manager - self._availability = nil - } - - /// The number of reserved streams. - @usableFromInline - internal var reservedStreams: Int { - return self._availability?.reserved ?? 0 - } - - /// The number of streams available to reserve. If this value is greater than zero then it is - /// safe to call `reserveStream()` and force unwrap the result. - @usableFromInline - internal var availableStreams: Int { - return self._availability?.available ?? 0 - } - - /// The maximum number of concurrent streams which may be available for the connection, if it - /// is ready. - @usableFromInline - internal var maxAvailableStreams: Int? { - return self._availability?.maxAvailable - } - - /// Reserve a stream and return the stream multiplexer. Returns `nil` if it is not possible - /// to reserve a stream. - /// - /// The result may be safely unwrapped if `self.availableStreams > 0` when reserving a stream. - @usableFromInline - internal mutating func reserveStream() -> HTTP2StreamMultiplexer? { - return self._availability?.reserve() - } - - @usableFromInline - internal mutating func openedStream() -> PerConnectionState.StreamAvailability.Utilization? { - return self._availability?.opened() - } - - /// Return a reserved stream to the connection. - @usableFromInline - internal mutating func returnStream() -> PerConnectionState.StreamAvailability.Utilization? { - return self._availability?.return() - } - - /// Update the maximum concurrent streams available on the connection, marking it as available - /// if it was not already. - /// - /// Returns the previous value for max concurrent streams if the connection was ready. - @usableFromInline - internal mutating func updateMaxConcurrentStreams(_ maxConcurrentStreams: Int) -> Int? { - if var availability = self._availability { - var oldValue = maxConcurrentStreams - swap(&availability.maxAvailable, &oldValue) - self._availability = availability - return oldValue - } else { - self._availability = self.manager.sync.multiplexer.map { - StreamAvailability(multiplexer: $0, maxAvailable: maxConcurrentStreams) - } - return nil - } - } - - /// Mark the connection as unavailable returning the number of reserved streams. - @usableFromInline - internal mutating func unavailable() -> Int { - defer { - self._availability = nil - } - return self._availability?.reserved ?? 0 - } - } -} diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift b/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift deleted file mode 100644 index 8a5cd5ad1..000000000 --- a/Sources/GRPC/ConnectionPool/ConnectionPool+Waiter.swift +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 NIOCore -import NIOHTTP2 - -extension ConnectionPool { - @usableFromInline - internal final class Waiter { - /// A promise to complete with the initialized channel. - @usableFromInline - internal let _promise: EventLoopPromise - - @usableFromInline - internal var _channelFuture: EventLoopFuture { - return self._promise.futureResult - } - - /// The channel initializer. - @usableFromInline - internal let _channelInitializer: @Sendable (Channel) -> EventLoopFuture - - /// The deadline at which the timeout is scheduled. - @usableFromInline - internal let _deadline: NIODeadline - - /// A scheduled task which fails the stream promise should the pool not provide - /// a stream in time. - @usableFromInline - internal var _scheduledTimeout: Scheduled? - - /// An identifier for this waiter. - @usableFromInline - internal var id: ID { - return ID(self) - } - - @usableFromInline - internal init( - deadline: NIODeadline, - promise: EventLoopPromise, - channelInitializer: @escaping @Sendable (Channel) -> EventLoopFuture - ) { - self._deadline = deadline - self._promise = promise - self._channelInitializer = channelInitializer - self._scheduledTimeout = nil - } - - /// Schedule a timeout for this waiter. This task will be cancelled when the waiter is - /// succeeded or failed. - /// - /// - Parameters: - /// - eventLoop: The `EventLoop` to run the timeout task on. - /// - body: The closure to execute when the timeout is fired. - @usableFromInline - internal func scheduleTimeout( - on eventLoop: EventLoop, - execute body: @escaping () -> Void - ) { - assert(self._scheduledTimeout == nil) - self._scheduledTimeout = eventLoop.scheduleTask(deadline: self._deadline, body) - } - - /// Returns a boolean value indicating whether the deadline for this waiter occurs after the - /// given deadline. - @usableFromInline - internal func deadlineIsAfter(_ other: NIODeadline) -> Bool { - return self._deadline > other - } - - /// Succeed the waiter with the given multiplexer. - @usableFromInline - internal func succeed(with multiplexer: HTTP2StreamMultiplexer) { - self._scheduledTimeout?.cancel() - self._scheduledTimeout = nil - multiplexer.createStreamChannel(promise: self._promise, self._channelInitializer) - } - - /// Fail the waiter with `error`. - @usableFromInline - internal func fail(_ error: Error) { - self._scheduledTimeout?.cancel() - self._scheduledTimeout = nil - self._promise.fail(error) - } - - /// The ID of a waiter. - @usableFromInline - internal struct ID: Hashable, CustomStringConvertible { - @usableFromInline - internal let _id: ObjectIdentifier - - @usableFromInline - internal init(_ waiter: Waiter) { - self._id = ObjectIdentifier(waiter) - } - - @usableFromInline - internal var description: String { - return String(describing: self._id) - } - } - } -} diff --git a/Sources/GRPC/ConnectionPool/ConnectionPool.swift b/Sources/GRPC/ConnectionPool/ConnectionPool.swift deleted file mode 100644 index 7ae82b6e6..000000000 --- a/Sources/GRPC/ConnectionPool/ConnectionPool.swift +++ /dev/null @@ -1,1106 +0,0 @@ -/* - * 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 Atomics -import Logging -import NIOConcurrencyHelpers -import NIOCore -import NIOHTTP2 - -@usableFromInline -internal final class ConnectionPool { - /// The event loop all connections in this pool are running on. - @usableFromInline - internal let eventLoop: EventLoop - - @usableFromInline - internal enum State { - case active - case shuttingDown(EventLoopFuture) - case shutdown - } - - /// The state of the connection pool. - @usableFromInline - internal var _state: State = .active - - /// The most recent connection error we have observed. - /// - /// This error is used to provide additional context to failed waiters. A waiter may, for example, - /// timeout because the pool is busy, or because no connection can be established because of an - /// underlying connection error. In the latter case it's useful for the caller to know why the - /// connection is failing at the RPC layer. - /// - /// This value is cleared when a connection becomes 'available'. That is, when we receive an - /// http/2 SETTINGS frame. - /// - /// This value is set whenever an underlying connection transitions to the transient failure state - /// or to the idle state and has an associated error. - @usableFromInline - internal var _mostRecentError: Error? = nil - - /// Connection managers and their stream availability state keyed by the ID of the connection - /// manager. - /// - /// Connections are accessed by their ID for connection state changes (infrequent) and when - /// streams are closed (frequent). However when choosing which connection to succeed a waiter - /// with (frequent) requires the connections to be ordered by their availability. A dictionary - /// might not be the most efficient data structure (a queue prioritised by stream availability may - /// be a better choice given the number of connections is likely to be very low in practice). - @usableFromInline - internal var _connections: [ConnectionManagerID: PerConnectionState] - - /// The threshold which if exceeded when creating a stream determines whether the pool will - /// start connecting an idle connection (if one exists). - /// - /// The 'load' is calculated as the ratio of demand for streams (the sum of the number of waiters - /// and the number of reserved streams) and the total number of streams which non-idle connections - /// could support (this includes the streams that a connection in the connecting state could - /// support). - @usableFromInline - internal let reservationLoadThreshold: Double - - /// The assumed value for the maximum number of concurrent streams a connection can support. We - /// assume a connection will support this many streams until we know better. - @usableFromInline - internal let assumedMaxConcurrentStreams: Int - - /// A queue of waiters which may or may not get a stream in the future. - @usableFromInline - internal var waiters: CircularBuffer - - /// The maximum number of waiters allowed, the size of `waiters` must not exceed this value. If - /// there are this many waiters in the queue then the next waiter will be failed immediately. - @usableFromInline - internal let maxWaiters: Int - - /// The number of connections in the pool that should always be kept open (i.e. they won't go idle). - /// In other words, it's the number of connections for which we should ignore idle timers. - @usableFromInline - internal let minConnections: Int - - /// Configuration for backoff between subsequence connection attempts. - @usableFromInline - internal let connectionBackoff: ConnectionBackoff - - /// Provides a channel factory to the `ConnectionManager`. - @usableFromInline - internal let channelProvider: ConnectionManagerChannelProvider - - /// The object to notify about changes to stream reservations; in practice this is usually - /// the `PoolManager`. - @usableFromInline - internal let streamLender: StreamLender - - @usableFromInline - internal var delegate: GRPCConnectionPoolDelegate? - - /// A logger. - @usableFromInline - internal let logger: Logger - - /// Returns `NIODeadline` representing 'now'. This is useful for testing. - @usableFromInline - internal let now: () -> NIODeadline - - /// The ID of this sub-pool. - @usableFromInline - internal let id: GRPCSubPoolID - - /// Logging metadata keys. - @usableFromInline - internal enum Metadata { - /// The ID of this pool. - @usableFromInline - static let id = "pool.id" - /// The number of stream reservations (i.e. number of open streams + number of waiters). - @usableFromInline - static let reservationsCount = "pool.reservations.count" - /// The number of streams this pool can support with non-idle connections at this time. - @usableFromInline - static let reservationsCapacity = "pool.reservations.capacity" - /// The current reservation load (i.e. reservation count / reservation capacity) - @usableFromInline - static let reservationsLoad = "pool.reservations.load" - /// The reservation load threshold, above which a new connection will be created (if possible). - @usableFromInline - static let reservationsLoadThreshold = "pool.reservations.loadThreshold" - /// The current number of waiters in the pool. - @usableFromInline - static let waitersCount = "pool.waiters.count" - /// The maximum number of waiters the pool is configured to allow. - @usableFromInline - static let waitersMax = "pool.waiters.max" - /// The number of waiters which were successfully serviced. - @usableFromInline - static let waitersServiced = "pool.waiters.serviced" - /// The ID of waiter. - @usableFromInline - static let waiterID = "pool.waiter.id" - /// The maximum number of connections allowed in the pool. - @usableFromInline - static let connectionsMax = "pool.connections.max" - /// The number of connections in the ready state. - @usableFromInline - static let connectionsReady = "pool.connections.ready" - /// The number of connections in the connecting state. - @usableFromInline - static let connectionsConnecting = "pool.connections.connecting" - /// The number of connections in the transient failure state. - @usableFromInline - static let connectionsTransientFailure = "pool.connections.transientFailure" - } - - @usableFromInline - init( - eventLoop: EventLoop, - maxWaiters: Int, - minConnections: Int, - reservationLoadThreshold: Double, - assumedMaxConcurrentStreams: Int, - connectionBackoff: ConnectionBackoff, - channelProvider: ConnectionManagerChannelProvider, - streamLender: StreamLender, - delegate: GRPCConnectionPoolDelegate?, - logger: Logger, - now: @escaping () -> NIODeadline = NIODeadline.now - ) { - precondition( - (0.0 ... 1.0).contains(reservationLoadThreshold), - "reservationLoadThreshold must be within the range 0.0 ... 1.0" - ) - - self.reservationLoadThreshold = reservationLoadThreshold - self.assumedMaxConcurrentStreams = assumedMaxConcurrentStreams - - self._connections = [:] - self.maxWaiters = maxWaiters - self.minConnections = minConnections - self.waiters = CircularBuffer(initialCapacity: 16) - - self.eventLoop = eventLoop - self.connectionBackoff = connectionBackoff - self.channelProvider = channelProvider - self.streamLender = streamLender - self.delegate = delegate - self.now = now - - let id = GRPCSubPoolID.next() - var logger = logger - logger[metadataKey: Metadata.id] = "\(id)" - - self.id = id - self.logger = logger - } - - /// Initialize the connection pool. - /// - /// - Parameter connections: The number of connections to add to the pool. - internal func initialize(connections: Int) { - assert(self._connections.isEmpty) - self.logger.debug( - "initializing new sub-pool", - metadata: [ - Metadata.waitersMax: .stringConvertible(self.maxWaiters), - Metadata.connectionsMax: .stringConvertible(connections), - ] - ) - self._connections.reserveCapacity(connections) - var numberOfKeepOpenConnections = self.minConnections - while self._connections.count < connections { - // If we have less than the minimum number of connections, don't let - // the new connection close when idle. - let idleBehavior = - numberOfKeepOpenConnections > 0 - ? ConnectionManager.IdleBehavior.neverGoIdle : .closeWhenIdleTimeout - numberOfKeepOpenConnections -= 1 - self.addConnectionToPool(idleBehavior: idleBehavior) - } - } - - /// Make and add a new connection to the pool. - private func addConnectionToPool(idleBehavior: ConnectionManager.IdleBehavior) { - let manager = ConnectionManager( - eventLoop: self.eventLoop, - channelProvider: self.channelProvider, - callStartBehavior: .waitsForConnectivity, - idleBehavior: idleBehavior, - connectionBackoff: self.connectionBackoff, - connectivityDelegate: self, - http2Delegate: self, - logger: self.logger - ) - let id = manager.id - self._connections[id] = PerConnectionState(manager: manager) - self.delegate?.connectionAdded(id: .init(id)) - - // If it's one of the connections that should be kept open, then connect - // straight away. - switch idleBehavior { - case .neverGoIdle: - self.eventLoop.execute { - if manager.sync.isIdle { - manager.sync.startConnecting() - } - } - case .closeWhenIdleTimeout: - () - } - } - - // MARK: - Called from the pool manager - - /// Make and initialize an HTTP/2 stream `Channel`. - /// - /// - Parameters: - /// - deadline: The point in time by which the `promise` must have been resolved. - /// - promise: A promise for a `Channel`. - /// - logger: A request logger. - /// - initializer: A closure to initialize the `Channel` with. - @inlinable - internal func makeStream( - deadline: NIODeadline, - promise: EventLoopPromise, - logger: Logger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture - ) { - if self.eventLoop.inEventLoop { - self._makeStream( - deadline: deadline, - promise: promise, - logger: logger, - initializer: initializer - ) - } else { - self.eventLoop.execute { - self._makeStream( - deadline: deadline, - promise: promise, - logger: logger, - initializer: initializer - ) - } - } - } - - /// See `makeStream(deadline:promise:logger:initializer:)`. - @inlinable - internal func makeStream( - deadline: NIODeadline, - logger: Logger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture - ) -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Channel.self) - self.makeStream(deadline: deadline, promise: promise, logger: logger, initializer: initializer) - return promise.futureResult - } - - /// Shutdown the connection pool. - /// - /// Existing waiters will be failed and all underlying connections will be shutdown. Subsequent - /// calls to `makeStream` will be failed immediately. - /// - /// - Parameter mode: The mode to use when shutting down. - /// - Returns: A future indicated when shutdown has been completed. - internal func shutdown(mode: ConnectionManager.ShutdownMode) -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - - if self.eventLoop.inEventLoop { - self._shutdown(mode: mode, promise: promise) - } else { - self.eventLoop.execute { - self._shutdown(mode: mode, promise: promise) - } - } - - return promise.futureResult - } - - /// See `makeStream(deadline:promise:logger:initializer:)`. - /// - /// - Important: Must be called on the pool's `EventLoop`. - @inlinable - internal func _makeStream( - deadline: NIODeadline, - promise: EventLoopPromise, - logger: Logger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture - ) { - self.eventLoop.assertInEventLoop() - - guard case .active = self._state else { - // Fail the promise right away if we're shutting down or already shut down. - promise.fail(GRPCConnectionPoolError.shutdown) - return - } - - // Try to make a stream on an existing connection. - let streamCreated = self._tryMakeStream(promise: promise, initializer: initializer) - - if !streamCreated { - // No stream was created, wait for one. - self._enqueueWaiter( - deadline: deadline, - promise: promise, - logger: logger, - initializer: initializer - ) - } - } - - /// Try to find an existing connection on which we can make a stream. - /// - /// - Parameters: - /// - promise: A promise to succeed if we can make a stream. - /// - initializer: A closure to initialize the stream with. - /// - Returns: A boolean value indicating whether the stream was created or not. - @inlinable - internal func _tryMakeStream( - promise: EventLoopPromise, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture - ) -> Bool { - // We shouldn't jump the queue. - guard self.waiters.isEmpty else { - return false - } - - // Reserve a stream, if we can. - guard let multiplexer = self._reserveStreamFromMostAvailableConnection() else { - return false - } - - multiplexer.createStreamChannel(promise: promise, initializer) - - // Has reserving another stream tipped us over the limit for needing another connection? - if self._shouldBringUpAnotherConnection() { - self._startConnectingIdleConnection() - } - - return true - } - - /// Enqueue a waiter to be provided with a stream at some point in the future. - /// - /// - Parameters: - /// - deadline: The point in time by which the promise should have been completed. - /// - promise: The promise to complete with the `Channel`. - /// - logger: A logger. - /// - initializer: A closure to initialize the `Channel` with. - @inlinable - internal func _enqueueWaiter( - deadline: NIODeadline, - promise: EventLoopPromise, - logger: Logger, - initializer: @escaping @Sendable (Channel) -> EventLoopFuture - ) { - // Don't overwhelm the pool with too many waiters. - guard self.waiters.count < self.maxWaiters else { - logger.trace( - "connection pool has too many waiters", - metadata: [ - Metadata.waitersMax: .stringConvertible(self.maxWaiters) - ] - ) - promise.fail(GRPCConnectionPoolError.tooManyWaiters(connectionError: self._mostRecentError)) - return - } - - let waiter = Waiter(deadline: deadline, promise: promise, channelInitializer: initializer) - - // Fail the waiter and punt it from the queue when it times out. It's okay that we schedule the - // timeout before appending it to the waiters, it wont run until the next event loop tick at the - // earliest (even if the deadline has already passed). - waiter.scheduleTimeout(on: self.eventLoop) { - waiter.fail(GRPCConnectionPoolError.deadlineExceeded(connectionError: self._mostRecentError)) - - if let index = self.waiters.firstIndex(where: { $0.id == waiter.id }) { - self.waiters.remove(at: index) - - logger.trace( - "timed out waiting for a connection", - metadata: [ - Metadata.waiterID: "\(waiter.id)", - Metadata.waitersCount: .stringConvertible(self.waiters.count), - ] - ) - } - } - - // request logger - logger.debug( - "waiting for a connection to become available", - metadata: [ - Metadata.waiterID: "\(waiter.id)", - Metadata.waitersCount: .stringConvertible(self.waiters.count), - ] - ) - - self.waiters.append(waiter) - - // pool logger - self.logger.trace( - "enqueued connection waiter", - metadata: [ - Metadata.waitersCount: .stringConvertible(self.waiters.count) - ] - ) - - if self._shouldBringUpAnotherConnection() { - self._startConnectingIdleConnection() - } - } - - /// Compute the current demand and capacity for streams. - /// - /// The 'demand' for streams is the number of reserved streams and the number of waiters. The - /// capacity for streams is the product of max concurrent streams and the number of non-idle - /// connections. - /// - /// - Returns: A tuple of the demand and capacity for streams. - @usableFromInline - internal func _computeStreamDemandAndCapacity() -> (demand: Int, capacity: Int) { - let demand = self.sync.reservedStreams + self.sync.waiters - - // TODO: make this cheaper by storing and incrementally updating the number of idle connections - let capacity = self._connections.values.reduce(0) { sum, state in - if state.manager.sync.isIdle || state.isQuiescing { - // Idle connection or quiescing (so the capacity should be ignored). - return sum - } else if let knownMaxAvailableStreams = state.maxAvailableStreams { - // A known value of max concurrent streams, i.e. the connection is active. - return sum + knownMaxAvailableStreams - } else { - // Not idle and no known value, the connection must be connecting so use our assumed value. - return sum + self.assumedMaxConcurrentStreams - } - } - - return (demand, capacity) - } - - /// Returns whether the pool should start connecting an idle connection (if one exists). - @usableFromInline - internal func _shouldBringUpAnotherConnection() -> Bool { - let (demand, capacity) = self._computeStreamDemandAndCapacity() - - // Infinite -- i.e. all connections are idle or no connections exist -- is okay here as it - // will always be greater than the threshold and a new connection will be spun up. - let load = Double(demand) / Double(capacity) - let loadExceedsThreshold = load >= self.reservationLoadThreshold - - if loadExceedsThreshold { - self.logger.debug( - "stream reservation load factor greater than or equal to threshold, bringing up additional connection if available", - metadata: [ - Metadata.reservationsCount: .stringConvertible(demand), - Metadata.reservationsCapacity: .stringConvertible(capacity), - Metadata.reservationsLoad: .stringConvertible(load), - Metadata.reservationsLoadThreshold: .stringConvertible(self.reservationLoadThreshold), - ] - ) - } - - return loadExceedsThreshold - } - - /// Starts connecting an idle connection, if one exists. - @usableFromInline - internal func _startConnectingIdleConnection() { - if let index = self._connections.values.firstIndex(where: { $0.manager.sync.isIdle }) { - self._connections.values[index].manager.sync.startConnecting() - } else { - let connecting = self._connections.values.count { $0.manager.sync.isConnecting } - let ready = self._connections.values.count { $0.manager.sync.isReady } - let transientFailure = self._connections.values.count { $0.manager.sync.isTransientFailure } - - self.logger.debug( - "no idle connections in pool", - metadata: [ - Metadata.connectionsConnecting: .stringConvertible(connecting), - Metadata.connectionsReady: .stringConvertible(ready), - Metadata.connectionsTransientFailure: .stringConvertible(transientFailure), - Metadata.waitersCount: .stringConvertible(self.waiters.count), - ] - ) - } - } - - /// Returns the index in `self.connections.values` of the connection with the most available - /// streams. Returns `self.connections.endIndex` if no connection has at least one stream - /// available. - /// - /// - Note: this is linear in the number of connections. - @usableFromInline - internal func _mostAvailableConnectionIndex() - -> Dictionary.Index - { - var index = self._connections.values.startIndex - var selectedIndex = self._connections.values.endIndex - var mostAvailableStreams = 0 - - while index != self._connections.values.endIndex { - let availableStreams = self._connections.values[index].availableStreams - if availableStreams > mostAvailableStreams { - mostAvailableStreams = availableStreams - selectedIndex = index - } - - self._connections.values.formIndex(after: &index) - } - - return selectedIndex - } - - /// Reserves a stream from the connection with the most available streams, if one exists. - /// - /// - Returns: The `HTTP2StreamMultiplexer` from the connection the stream was reserved from, - /// or `nil` if no stream could be reserved. - @usableFromInline - internal func _reserveStreamFromMostAvailableConnection() -> HTTP2StreamMultiplexer? { - let index = self._mostAvailableConnectionIndex() - - if index != self._connections.endIndex { - // '!' is okay here; the most available connection must have at least one stream available - // to reserve. - return self._connections.values[index].reserveStream()! - } else { - return nil - } - } - - /// See `shutdown(mode:)`. - /// - /// - Parameter promise: A `promise` to complete when the pool has been shutdown. - @usableFromInline - internal func _shutdown(mode: ConnectionManager.ShutdownMode, promise: EventLoopPromise) { - self.eventLoop.assertInEventLoop() - - switch self._state { - case .active: - self.logger.debug("shutting down connection pool") - - // We're shutting down now and when that's done we'll be fully shutdown. - self._state = .shuttingDown(promise.futureResult) - promise.futureResult.whenComplete { _ in - self._state = .shutdown - self.delegate = nil - self.logger.trace("finished shutting down connection pool") - } - - // Shutdown all the connections and remove them from the pool. - let connections = self._connections - self._connections.removeAll() - - let allShutdown: [EventLoopFuture] = connections.values.map { - let id = $0.manager.id - let manager = $0.manager - - return manager.eventLoop.flatSubmit { - // If the connection was idle/shutdown before calling shutdown then we shouldn't tell - // the delegate the connection closed (because it either never connected or was already - // informed about this). - let connectionIsInactive = manager.sync.isIdle || manager.sync.isShutdown - return manager.shutdown(mode: mode).always { _ in - if !connectionIsInactive { - self.delegate?.connectionClosed(id: .init(id), error: nil) - } - self.delegate?.connectionRemoved(id: .init(id)) - } - } - } - - // Fail the outstanding waiters. - while let waiter = self.waiters.popFirst() { - waiter.fail(GRPCConnectionPoolError.shutdown) - } - - // Cascade the result of the shutdown into the promise. - EventLoopFuture.andAllSucceed(allShutdown, promise: promise) - - case let .shuttingDown(future): - // We're already shutting down, cascade the result. - promise.completeWith(future) - - case .shutdown: - // Already shutdown, fine. - promise.succeed(()) - } - } - - internal func stats() -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: GRPCSubPoolStats.self) - - if self.eventLoop.inEventLoop { - self._stats(promise: promise) - } else { - self.eventLoop.execute { - self._stats(promise: promise) - } - } - - return promise.futureResult - } - - private func _stats(promise: EventLoopPromise) { - self.eventLoop.assertInEventLoop() - - var stats = GRPCSubPoolStats(id: self.id) - - for connection in self._connections.values { - let sync = connection.manager.sync - if sync.isIdle { - stats.connectionStates.idle += 1 - } else if sync.isConnecting { - stats.connectionStates.connecting += 1 - } else if sync.isReady { - stats.connectionStates.ready += 1 - } else if sync.isTransientFailure { - stats.connectionStates.transientFailure += 1 - } - - stats.streamsInUse += connection.reservedStreams - stats.streamsFreeToUse += connection.availableStreams - } - - stats.rpcsWaiting += self.waiters.count - - promise.succeed(stats) - } -} - -extension ConnectionPool: ConnectionManagerConnectivityDelegate { - // We're interested in a few different situations here: - // - // 1. The connection was usable ('ready') and is no longer usable (either it became idle or - // encountered an error. If this happens we need to notify any connections of the change as - // they may no longer be used for new RPCs. - // 2. The connection was not usable but moved to a different unusable state. If this happens and - // we know the cause of the state transition (i.e. the error) then we need to update our most - // recent error with the error. This information is used when failing waiters to provide some - // context as to why they may be failing. - func connectionStateDidChange( - _ manager: ConnectionManager, - from oldState: _ConnectivityState, - to newState: _ConnectivityState - ) { - switch (oldState, newState) { - case let (.ready, .transientFailure(error)), - let (.ready, .idle(.some(error))): - self.updateMostRecentError(error) - self.connectionUnavailable(manager.id) - - case (.ready, .idle(.none)), - (.ready, .shutdown): - self.connectionUnavailable(manager.id) - - case let (_, .transientFailure(error)), - let (_, .idle(.some(error))): - self.updateMostRecentError(error) - - default: - () - } - - guard let delegate = self.delegate else { return } - - switch (oldState, newState) { - case (.idle, .connecting), - (.transientFailure, .connecting): - delegate.startedConnecting(id: .init(manager.id)) - - case (.connecting, .ready): - // The connection becoming ready is handled by 'receivedSettingsMaxConcurrentStreams'. - () - - case (.ready, .idle): - delegate.connectionClosed(id: .init(manager.id), error: nil) - - case let (.ready, .transientFailure(error)): - delegate.connectionClosed(id: .init(manager.id), error: error) - - case let (.connecting, .transientFailure(error)): - delegate.connectFailed(id: .init(manager.id), error: error) - - default: - () - } - } - - func connectionIsQuiescing(_ manager: ConnectionManager) { - self.eventLoop.assertInEventLoop() - - // Find the relevant connection. - guard let index = self._connections.index(forKey: manager.id) else { - return - } - - // Drop the connectivity delegate, we're no longer interested in its events now. - manager.sync.connectivityDelegate = nil - - // Started quiescing; update our state and notify the pool delegate. - self._connections.values[index].isQuiescing = true - self.delegate?.connectionQuiescing(id: .init(manager.id)) - - // As the connection is quescing, we need to know when the current connection its managing has - // closed. When that happens drop the H2 delegate and update the pool delegate. - manager.onCurrentConnectionClose { hadActiveConnection in - assert(hadActiveConnection) - if let removed = self._connections.removeValue(forKey: manager.id) { - removed.manager.sync.http2Delegate = nil - self.delegate?.connectionClosed(id: .init(removed.manager.id), error: nil) - self.delegate?.connectionRemoved(id: .init(removed.manager.id)) - } - } - - // Grab the number of reserved streams (before invalidating the index by adding a connection). - let reservedStreams = self._connections.values[index].reservedStreams - - // Replace the connection with a new idle one. Keep the idle behavior, so that - // if it's a connection that should be kept alive, we maintain it. - self.addConnectionToPool(idleBehavior: manager.idleBehavior) - - // Since we're removing this connection from the pool (and no new streams can be created on - // the connection), the pool manager can ignore any streams reserved against this connection. - // We do still care about the number of reserved streams for the connection though - // - // Note: we don't need to adjust the number of available streams as the effective number of - // connections hasn't changed. - self.streamLender.returnStreams(reservedStreams, to: self) - } - - private func updateMostRecentError(_ error: Error) { - self.eventLoop.assertInEventLoop() - // Update the last known error if there is one. We will use it to provide some context to - // waiters which may fail. - self._mostRecentError = error - } - - /// A connection has become unavailable. - private func connectionUnavailable(_ id: ConnectionManagerID) { - self.eventLoop.assertInEventLoop() - // The connection is no longer available: any streams which haven't been closed will be counted - // as a dropped reservation, we need to tell the pool manager about them. - if let droppedReservations = self._connections[id]?.unavailable(), droppedReservations > 0 { - self.streamLender.returnStreams(droppedReservations, to: self) - } - } -} - -extension ConnectionPool: ConnectionManagerHTTP2Delegate { - internal func streamOpened(_ manager: ConnectionManager) { - self.eventLoop.assertInEventLoop() - if let utilization = self._connections[manager.id]?.openedStream(), - let delegate = self.delegate - { - delegate.connectionUtilizationChanged( - id: .init(manager.id), - streamsUsed: utilization.used, - streamCapacity: utilization.capacity - ) - } - } - - internal func streamClosed(_ manager: ConnectionManager) { - self.eventLoop.assertInEventLoop() - - guard let index = self._connections.index(forKey: manager.id) else { - return - } - - // Return the stream the connection and to the pool manager. - if let utilization = self._connections.values[index].returnStream(), - let delegate = self.delegate - { - delegate.connectionUtilizationChanged( - id: .init(manager.id), - streamsUsed: utilization.used, - streamCapacity: utilization.capacity - ) - } - - // Return the stream to the pool manager if the connection is available and not quiescing. For - // quiescing connections streams were returned when the connection started quiescing. - if self._connections.values[index].isAvailable, !self._connections.values[index].isQuiescing { - self.streamLender.returnStreams(1, to: self) - - // A stream was returned: we may be able to service a waiter now. - self.tryServiceWaiters() - } - } - - internal func receivedSettingsMaxConcurrentStreams( - _ manager: ConnectionManager, - maxConcurrentStreams: Int - ) { - self.eventLoop.assertInEventLoop() - - // Find the relevant connection. - guard let index = self._connections.index(forKey: manager.id) else { - return - } - - // When the connection is quiescing, the pool manager is not interested in updates to the - // connection, bail out early. - if self._connections.values[index].isQuiescing { - return - } - - // If we received a SETTINGS update then a connection is okay: drop the last known error. - self._mostRecentError = nil - - let previous = self._connections.values[index].updateMaxConcurrentStreams(maxConcurrentStreams) - let delta: Int - - if let previousValue = previous { - // There was a previous value of max concurrent streams, i.e. a change in value for an - // existing connection. - delta = maxConcurrentStreams - previousValue - } else { - // There was no previous value so this must be a new connection. We'll compare against our - // assumed default. - delta = maxConcurrentStreams - self.assumedMaxConcurrentStreams - // Notify the delegate. - self.delegate?.connectSucceeded(id: .init(manager.id), streamCapacity: maxConcurrentStreams) - } - - if delta != 0 { - self.streamLender.changeStreamCapacity(by: delta, for: self) - } - - // We always check, even if `delta` isn't greater than zero as this might be a new connection. - self.tryServiceWaiters() - } -} - -extension ConnectionPool { - // MARK: - Waiters - - /// Try to service as many waiters as possible. - /// - /// This an expensive operation, in the worst case it will be `O(W ⨉ N)` where `W` is the number - /// of waiters and `N` is the number of connections. - private func tryServiceWaiters() { - if self.waiters.isEmpty { return } - - self.logger.trace( - "servicing waiters", - metadata: [ - Metadata.waitersCount: .stringConvertible(self.waiters.count) - ] - ) - - let now = self.now() - var serviced = 0 - - while !self.waiters.isEmpty { - if self.waiters.first!.deadlineIsAfter(now) { - if let multiplexer = self._reserveStreamFromMostAvailableConnection() { - // The waiter's deadline is in the future, and we have a suitable connection. Remove and - // succeed the waiter. - let waiter = self.waiters.removeFirst() - serviced &+= 1 - waiter.succeed(with: multiplexer) - } else { - // There are waiters but no available connections, we're done. - break - } - } else { - // The waiter's deadline has already expired, there's no point completing it. Remove it and - // let its scheduled timeout fail the promise. - self.waiters.removeFirst() - } - } - - self.logger.trace( - "done servicing waiters", - metadata: [ - Metadata.waitersCount: .stringConvertible(self.waiters.count), - Metadata.waitersServiced: .stringConvertible(serviced), - ] - ) - } -} - -extension ConnectionPool { - /// Synchronous operations for the pool, mostly used by tests. - internal struct Sync { - private let pool: ConnectionPool - - fileprivate init(_ pool: ConnectionPool) { - self.pool = pool - } - - /// The number of outstanding connection waiters. - internal var waiters: Int { - self.pool.eventLoop.assertInEventLoop() - return self.pool.waiters.count - } - - /// The number of connection currently in the pool (in any state). - internal var connections: Int { - self.pool.eventLoop.assertInEventLoop() - return self.pool._connections.count - } - - /// The number of idle connections in the pool. - internal var idleConnections: Int { - self.pool.eventLoop.assertInEventLoop() - return self.pool._connections.values.reduce(0) { $0 &+ ($1.manager.sync.isIdle ? 1 : 0) } - } - - /// The number of active (i.e. connecting or ready) connections in the pool. - internal var activeConnections: Int { - self.pool.eventLoop.assertInEventLoop() - return self.pool._connections.values.reduce(0) { - $0 &+ (($1.manager.sync.isReady || $1.manager.sync.isConnecting) ? 1 : 0) - } - } - - /// The number of connections in the pool in transient failure state. - internal var transientFailureConnections: Int { - self.pool.eventLoop.assertInEventLoop() - return self.pool._connections.values.reduce(0) { - $0 &+ ($1.manager.sync.isTransientFailure ? 1 : 0) - } - } - - /// The number of streams currently available to reserve across all connections in the pool. - internal var availableStreams: Int { - self.pool.eventLoop.assertInEventLoop() - return self.pool._connections.values.reduce(0) { $0 + $1.availableStreams } - } - - /// The number of streams which have been reserved across all connections in the pool. - internal var reservedStreams: Int { - self.pool.eventLoop.assertInEventLoop() - return self.pool._connections.values.reduce(0) { $0 + $1.reservedStreams } - } - - /// Updates the most recent connection error. - internal func updateMostRecentError(_ error: Error) { - self.pool.eventLoop.assertInEventLoop() - self.pool.updateMostRecentError(error) - } - } - - internal var sync: Sync { - return Sync(self) - } -} - -/// An error thrown from the ``GRPCChannelPool``. -public struct GRPCConnectionPoolError: Error, CustomStringConvertible { - public struct Code: Hashable, Sendable, CustomStringConvertible { - enum Code { - case shutdown - case tooManyWaiters - case deadlineExceeded - } - - fileprivate var code: Code - - private init(_ code: Code) { - self.code = code - } - - public var description: String { - String(describing: self.code) - } - - /// The pool is shutdown or shutting down. - public static var shutdown: Self { Self(.shutdown) } - - /// There are too many waiters in the pool. - public static var tooManyWaiters: Self { Self(.tooManyWaiters) } - - /// The deadline for creating a stream has passed. - public static var deadlineExceeded: Self { Self(.deadlineExceeded) } - } - - /// The error code. - public var code: Code - - /// An underlying error which caused this error to be thrown. - public var underlyingError: Error? - - public var description: String { - if let underlyingError = self.underlyingError { - return "\(self.code) (\(underlyingError))" - } else { - return String(describing: self.code) - } - } - - /// Create a new connection pool error with the given code and underlying error. - /// - /// - Parameters: - /// - code: The error code. - /// - underlyingError: The underlying error which led to this error being thrown. - public init(code: Code, underlyingError: Error? = nil) { - self.code = code - self.underlyingError = underlyingError - } -} - -extension GRPCConnectionPoolError { - @usableFromInline - static let shutdown = Self(code: .shutdown) - - @inlinable - static func tooManyWaiters(connectionError: Error?) -> Self { - Self(code: .tooManyWaiters, underlyingError: connectionError) - } - - @inlinable - static func deadlineExceeded(connectionError: Error?) -> Self { - Self(code: .deadlineExceeded, underlyingError: connectionError) - } -} - -extension GRPCConnectionPoolError: GRPCStatusTransformable { - public func makeGRPCStatus() -> GRPCStatus { - switch self.code.code { - case .shutdown: - return GRPCStatus( - code: .unavailable, - message: "The connection pool is shutdown", - cause: self.underlyingError - ) - - case .tooManyWaiters: - return GRPCStatus( - code: .resourceExhausted, - message: "The connection pool has no capacity for new RPCs or RPC waiters", - cause: self.underlyingError - ) - - case .deadlineExceeded: - return GRPCStatus( - code: .deadlineExceeded, - message: "Timed out waiting for an HTTP/2 stream from the connection pool", - cause: self.underlyingError - ) - } - } -} - -extension Sequence { - fileprivate func count(where predicate: (Element) -> Bool) -> Int { - return self.reduce(0) { count, element in - predicate(element) ? count + 1 : count - } - } -} diff --git a/Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift b/Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift deleted file mode 100644 index 350189172..000000000 --- a/Sources/GRPC/ConnectionPool/ConnectionPoolIDs.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 Atomics - -enum RawID { - private static let source = ManagedAtomic(0) - - static func next() -> Int { - self.source.loadThenWrappingIncrement(ordering: .relaxed) - } -} - -/// The ID of a connection pool. -public struct GRPCConnectionPoolID: Hashable, Sendable, CustomStringConvertible { - private var rawValue: Int - - private init(rawValue: Int) { - self.rawValue = rawValue - } - - public static func next() -> Self { - return Self(rawValue: RawID.next()) - } - - public var description: String { - "ConnectionPool(\(self.rawValue))" - } -} - -/// The ID of a sub-pool in a connection pool. -public struct GRPCSubPoolID: Hashable, Sendable, CustomStringConvertible { - private var rawValue: Int - - private init(rawValue: Int) { - self.rawValue = rawValue - } - - public static func next() -> Self { - return Self(rawValue: RawID.next()) - } - - public var description: String { - "SubPool(\(self.rawValue))" - } -} diff --git a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift b/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift deleted file mode 100644 index 8a6cede36..000000000 --- a/Sources/GRPC/ConnectionPool/GRPCChannelPool.swift +++ /dev/null @@ -1,435 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOPosix - -import struct Foundation.UUID - -public enum GRPCChannelPool { - /// Make a new ``GRPCChannel`` on which calls may be made to gRPC services. - /// - /// The channel is backed by one connection pool per event loop, each of which may make multiple - /// connections to the given target. The size of the connection pool, and therefore the maximum - /// number of connections it may create at a given time is determined by the number of event loops - /// in the provided `EventLoopGroup` and the value of - /// ``GRPCChannelPool/Configuration/ConnectionPool-swift.struct/connectionsPerEventLoop``. - /// - /// The event loop and therefore connection chosen for a call is determined by - /// ``CallOptions/eventLoopPreference-swift.property``. If the `indifferent` preference is used - /// then the least-used event loop is chosen and a connection on that event loop will be selected. - /// If an `exact` preference is used then a connection on that event loop will be chosen provided - /// the given event loop belongs to the `EventLoopGroup` used to create this ``GRPCChannel``. - /// - /// Each connection in the pool is initially idle, and no connections will be established until - /// a call is made. The pool also closes connections after they have been inactive (i.e. are not - /// being used for calls) for some period of time. This is determined by - /// ``GRPCChannelPool/Configuration/idleTimeout``. - /// - /// > Important: The values of `transportSecurity` and `eventLoopGroup` **must** be compatible. - /// > - /// > For ``GRPCChannelPool/Configuration/TransportSecurity-swift.struct/tls(_:)`` the allowed - /// > `EventLoopGroup`s depends on the value of ``GRPCTLSConfiguration``. If a TLS configuration - /// > is known ahead of time, ``PlatformSupport/makeEventLoopGroup(compatibleWith:loopCount:)`` - /// > may be used to construct a compatible `EventLoopGroup`. - /// > - /// > If the `EventLoopGroup` is known ahead of time then a default TLS configuration may be - /// > constructed with ``GRPCTLSConfiguration/makeClientDefault(compatibleWith:)``. - /// > - /// > For ``GRPCChannelPool/Configuration/TransportSecurity-swift.struct/plaintext`` transport - /// > security both `MultiThreadedEventLoopGroup` and `NIOTSEventLoopGroup` (and `EventLoop`s - /// > from either) may be used. - /// - /// - Parameters: - /// - target: The target to connect to. - /// - transportSecurity: Transport layer security for connections. - /// - eventLoopGroup: The `EventLoopGroup` to run connections on. - /// - configure: A closure which may be used to modify defaulted configuration before - /// constructing the ``GRPCChannel``. - /// - Throws: If it is not possible to construct an SSL context. This will never happen when - /// using the ``GRPCChannelPool/Configuration/TransportSecurity-swift.struct/plaintext`` - /// transport security. - /// - Returns: A ``GRPCChannel``. - @inlinable - public static func with( - target: ConnectionTarget, - transportSecurity: GRPCChannelPool.Configuration.TransportSecurity, - eventLoopGroup: EventLoopGroup, - _ configure: (inout GRPCChannelPool.Configuration) -> Void = { _ in } - ) throws -> GRPCChannel { - let configuration = GRPCChannelPool.Configuration.with( - target: target, - transportSecurity: transportSecurity, - eventLoopGroup: eventLoopGroup, - configure - ) - - return try PooledChannel(configuration: configuration) - } - - /// See ``GRPCChannelPool/with(target:transportSecurity:eventLoopGroup:_:)``. - public static func with( - configuration: GRPCChannelPool.Configuration - ) throws -> GRPCChannel { - return try PooledChannel(configuration: configuration) - } -} - -extension GRPCChannelPool { - public struct Configuration: Sendable { - @inlinable - internal init( - target: ConnectionTarget, - transportSecurity: TransportSecurity, - eventLoopGroup: EventLoopGroup - ) { - self.target = target - self.transportSecurity = transportSecurity - self.eventLoopGroup = eventLoopGroup - } - - // Note: we use `configure` blocks to avoid having to add new initializers when properties are - // added to the configuration while allowing the configuration to be constructed as a constant. - - /// Construct and configure a ``GRPCChannelPool/Configuration``. - /// - /// - Parameters: - /// - target: The target to connect to. - /// - transportSecurity: Transport layer security for connections. Note that the value of - /// `eventLoopGroup` must be compatible with the value - /// - eventLoopGroup: The `EventLoopGroup` to run connections on. - /// - configure: A closure which may be used to modify defaulted configuration. - @inlinable - public static func with( - target: ConnectionTarget, - transportSecurity: TransportSecurity, - eventLoopGroup: EventLoopGroup, - _ configure: (inout Configuration) -> Void = { _ in } - ) -> Configuration { - var configuration = Configuration( - target: target, - transportSecurity: transportSecurity, - eventLoopGroup: eventLoopGroup - ) - configure(&configuration) - return configuration - } - - /// The target to connect to. - public var target: ConnectionTarget - - /// Connection security. - public var transportSecurity: TransportSecurity - - /// The `EventLoopGroup` used by the connection pool. - public var eventLoopGroup: EventLoopGroup - - /// Connection pool configuration. - public var connectionPool: ConnectionPool = .defaults - - /// HTTP/2 configuration. - public var http2: HTTP2 = .defaults - - /// The connection backoff configuration. - public var connectionBackoff = ConnectionBackoff() - - /// The amount of time to wait before closing the connection. The idle timeout will start only - /// if there are no RPCs in progress and will be cancelled as soon as any RPCs start. - /// - /// If a connection becomes idle, starting a new RPC will automatically create a new connection. - public var idleTimeout = TimeAmount.minutes(30) - - /// The connection keepalive configuration. - public var keepalive = ClientConnectionKeepalive() - - /// The maximum size in bytes of a message which may be received from a server. Defaults to 4MB. - /// - /// Any received messages whose size exceeds this limit will cause RPCs to fail with - /// a `.resourceExhausted` status code. - public var maximumReceiveMessageLength: Int = 4 * 1024 * 1024 { - willSet { - precondition(newValue >= 0, "maximumReceiveMessageLength must be positive") - } - } - - /// A channel initializer which will be run after gRPC has initialized each `NIOCore.Channel`. - /// This may be used to add additional handlers to the pipeline and is intended for debugging. - /// - /// - Warning: The initializer closure may be invoked *multiple times*. - @preconcurrency - public var debugChannelInitializer: (@Sendable (Channel) -> EventLoopFuture)? - - /// An error delegate which is called when errors are caught. - public var errorDelegate: ClientErrorDelegate? - - /// A delegate which will be notified about changes to the state of connections managed by the - /// pool. - public var delegate: GRPCConnectionPoolDelegate? - - /// The period at which connection pool stats are published to the ``delegate``. - /// - /// Ignored if either this value or ``delegate`` are `nil`. - public var statsPeriod: TimeAmount? - - /// A logger used for background activity, such as connection state changes. - public var backgroundActivityLogger = Logger( - label: "io.grpc", - factory: { _ in - return SwiftLogNoOpLogHandler() - } - ) - } -} - -extension GRPCChannelPool.Configuration { - public struct TransportSecurity: Sendable { - private init(_ configuration: GRPCTLSConfiguration?) { - self.tlsConfiguration = configuration - } - - /// The TLS configuration used. A `nil` value means that no TLS will be used and - /// communication at the transport layer will be plaintext. - public var tlsConfiguration: Optional - - /// Secure the transport layer with TLS. - /// - /// The TLS backend used depends on the value of `configuration`. See ``GRPCTLSConfiguration`` - /// for more details. - /// - /// > Important: the value of `configuration` **must** be compatible with - /// > ``GRPCChannelPool/Configuration/eventLoopGroup``. See the documentation of - /// > ``GRPCChannelPool/with(target:transportSecurity:eventLoopGroup:_:)`` for more details. - public static func tls(_ configuration: GRPCTLSConfiguration) -> TransportSecurity { - return TransportSecurity(configuration) - } - - /// Insecure plaintext communication. - public static let plaintext = TransportSecurity(nil) - } -} - -extension GRPCChannelPool.Configuration { - public struct HTTP2: Hashable, Sendable { - private static let allowedTargetWindowSizes = (1 ... Int(Int32.max)) - private static let allowedMaxFrameSizes = (1 << 14) ... ((1 << 24) - 1) - - /// Default HTTP/2 configuration. - public static let defaults = HTTP2() - - @inlinable - public static func with(_ configure: (inout HTTP2) -> Void) -> HTTP2 { - var configuration = Self.defaults - configure(&configuration) - return configuration - } - - /// The HTTP/2 max frame size. Defaults to 8MB. Values are clamped between 2^14 and 2^24-1 - /// octets inclusive (RFC 7540 § 4.2). - public var targetWindowSize = 8 * 1024 * 1024 { - didSet { - self.targetWindowSize = self.targetWindowSize.clamped(to: Self.allowedTargetWindowSizes) - } - } - - /// The HTTP/2 max frame size. Defaults to 16384. Value is clamped between 2^14 and 2^24-1 - /// octets inclusive (the minimum and maximum allowable values - HTTP/2 RFC 7540 4.2). - public var maxFrameSize: Int = 16384 { - didSet { - self.maxFrameSize = self.maxFrameSize.clamped(to: Self.allowedMaxFrameSizes) - } - } - } -} - -extension GRPCChannelPool.Configuration { - public struct ConnectionPool: Hashable, Sendable { - /// Default connection pool configuration. - public static let defaults = ConnectionPool() - - @inlinable - public static func with(_ configure: (inout ConnectionPool) -> Void) -> ConnectionPool { - var configuration = Self.defaults - configure(&configuration) - return configuration - } - - /// The maximum number of connections per `EventLoop` that may be created at a given time. - /// - /// Defaults to 1. - public var connectionsPerEventLoop: Int = 1 - - /// The maximum number of callers which may be waiting for a stream at any given time on a - /// given `EventLoop`. - /// - /// Any requests for a stream which would cause this limit to be exceeded will be failed - /// immediately. - /// - /// Defaults to 100. - public var maxWaitersPerEventLoop: Int = 100 - - /// The minimum number of connections to keep open in this pool, per EventLoop. - /// This number of connections per EventLoop will never go idle and be closed. - public var minConnectionsPerEventLoop: Int = 0 - - /// The maximum amount of time a caller is willing to wait for a stream for before timing out. - /// - /// Defaults to 30 seconds. - public var maxWaitTime: TimeAmount = .seconds(30) - - /// The threshold which, if exceeded, when creating a stream determines whether the pool will - /// establish another connection (if doing so will not violate ``connectionsPerEventLoop``). - /// - /// The 'load' is calculated as the ratio of demand for streams (the sum of the number of - /// waiters and the number of reserved streams) and the total number of streams which each - /// thread _could support. - public var reservationLoadThreshold: Double = 0.9 - } -} - -/// The ID of a connection in the connection pool. -public struct GRPCConnectionID: Hashable, Sendable, CustomStringConvertible { - private enum Value: Sendable, Hashable { - case managerID(ConnectionManagerID) - case uuid(UUID) - } - - private let id: Value - - public var description: String { - switch self.id { - case .managerID(let id): - return String(describing: id) - case .uuid(let uuid): - return String(describing: uuid) - } - } - - internal init(_ id: ConnectionManagerID) { - self.id = .managerID(id) - } - - /// Create a new unique connection ID. - /// - /// Normally you don't have to create connection IDs, gRPC will create them on your behalf. - /// However creating them manually is useful when testing the ``GRPCConnectionPoolDelegate``. - public init() { - self.id = .uuid(UUID()) - } -} - -/// A delegate for the connection pool which is notified of various lifecycle events. -/// -/// All functions must execute quickly and may be executed on arbitrary threads. The implementor is -/// responsible for ensuring thread safety. -public protocol GRPCConnectionPoolDelegate: Sendable { - /// A new connection was created with the given ID and added to the pool. The connection is not - /// yet active (or connecting). - /// - /// In most cases ``startedConnecting(id:)`` will be the next function called for the given - /// connection but ``connectionRemoved(id:)`` may also be called. - func connectionAdded(id: GRPCConnectionID) - - /// The connection with the given ID was removed from the pool. - func connectionRemoved(id: GRPCConnectionID) - - /// The connection with the given ID has started trying to establish a connection. The outcome - /// of the connection will be reported as either ``connectSucceeded(id:streamCapacity:)`` or - /// ``connectFailed(id:error:)``. - func startedConnecting(id: GRPCConnectionID) - - /// A connection attempt failed with the given error. After some period of - /// time ``startedConnecting(id:)`` may be called again. - func connectFailed(id: GRPCConnectionID, error: Error) - - /// A connection was established on the connection with the given ID. `streamCapacity` streams are - /// available to use on the connection. The maximum number of available streams may change over - /// time and is reported via ``connectionUtilizationChanged(id:streamsUsed:streamCapacity:)``. The - func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) - - /// The utilization of the connection changed; a stream may have been used, returned or the - /// maximum number of concurrent streams available on the connection changed. - func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) - - /// The remote peer is quiescing the connection: no new streams will be created on it. The - /// connection will eventually be closed and removed from the pool. - func connectionQuiescing(id: GRPCConnectionID) - - /// The connection was closed. The connection may be established again in the future (notified - /// via ``startedConnecting(id:)``). - func connectionClosed(id: GRPCConnectionID, error: Error?) - - /// Stats about the current state of the connection pool. - /// - /// Each ``GRPCConnectionPoolStats`` includes the stats for a sub-pool. Each sub-pool is tied - /// to an `EventLoop`. - /// - /// Unlike the other delegate methods, this is called periodically based on the value - /// of ``GRPCChannelPool/Configuration/statsPeriod``. - func connectionPoolStats(_ stats: [GRPCSubPoolStats], id: GRPCConnectionPoolID) -} - -extension GRPCConnectionPoolDelegate { - public func connectionPoolStats(_ stats: [GRPCSubPoolStats], id: GRPCConnectionPoolID) { - // Default conformance to avoid breaking changes. - } -} - -public struct GRPCSubPoolStats: Sendable, Hashable { - public struct ConnectionStates: Sendable, Hashable { - /// The number of idle connections. - public var idle: Int - /// The number of connections trying to establish a connection. - public var connecting: Int - /// The number of connections which are ready to use. - public var ready: Int - /// The number of connections which are backing off waiting to attempt to connect. - public var transientFailure: Int - - public init() { - self.idle = 0 - self.connecting = 0 - self.ready = 0 - self.transientFailure = 0 - } - } - - /// The ID of the subpool. - public var id: GRPCSubPoolID - - /// Counts of connection states. - public var connectionStates: ConnectionStates - - /// The number of streams currently being used. - public var streamsInUse: Int - - /// The number of streams which are currently free to use. - /// - /// The sum of this value and `streamsInUse` gives the capacity of the pool. - public var streamsFreeToUse: Int - - /// The number of RPCs currently waiting for a stream. - /// - /// RPCs waiting for a stream are also known as 'waiters'. - public var rpcsWaiting: Int - - public init(id: GRPCSubPoolID) { - self.id = id - self.connectionStates = ConnectionStates() - self.streamsInUse = 0 - self.streamsFreeToUse = 0 - self.rpcsWaiting = 0 - } -} diff --git a/Sources/GRPC/ConnectionPool/PoolManager.swift b/Sources/GRPC/ConnectionPool/PoolManager.swift deleted file mode 100644 index dd7a6ba92..000000000 --- a/Sources/GRPC/ConnectionPool/PoolManager.swift +++ /dev/null @@ -1,419 +0,0 @@ -/* - * 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 Logging -import NIOConcurrencyHelpers -import NIOCore - -// Unchecked because all mutable state is protected by a lock. -extension PooledChannel: @unchecked Sendable {} - -@usableFromInline -internal final class PoolManager { - /// Configuration used for each connection pool. - @usableFromInline - internal struct PerPoolConfiguration { - /// The maximum number of connections per pool. - @usableFromInline - var maxConnections: Int - - /// The maximum number of waiters per pool. - @usableFromInline - var maxWaiters: Int - - /// The minimum number of connections to keep open per pool. - /// This number of connections will never go idle and be closed. - @usableFromInline - var minConnections: Int - - /// A load threshold in the range `0.0 ... 1.0` beyond which another connection will be started - /// (assuming there is a connection available to start). - @usableFromInline - var loadThreshold: Double - - /// The assumed value of HTTP/2 'SETTINGS_MAX_CONCURRENT_STREAMS'. - @usableFromInline - var assumedMaxConcurrentStreams: Int - - /// The assumed maximum number of streams concurrently available in the pool. - @usableFromInline - var assumedStreamCapacity: Int { - return self.maxConnections * self.assumedMaxConcurrentStreams - } - - @usableFromInline - var connectionBackoff: ConnectionBackoff - - /// A `Channel` provider. - @usableFromInline - var channelProvider: DefaultChannelProvider - - @usableFromInline - var delegate: GRPCConnectionPoolDelegate? - - @usableFromInline - var statsPeriod: TimeAmount? - - @usableFromInline - internal init( - maxConnections: Int, - maxWaiters: Int, - minConnections: Int, - loadThreshold: Double, - assumedMaxConcurrentStreams: Int = 100, - connectionBackoff: ConnectionBackoff, - channelProvider: DefaultChannelProvider, - delegate: GRPCConnectionPoolDelegate?, - statsPeriod: TimeAmount? - ) { - self.maxConnections = maxConnections - self.maxWaiters = maxWaiters - self.minConnections = minConnections - self.loadThreshold = loadThreshold - self.assumedMaxConcurrentStreams = assumedMaxConcurrentStreams - self.connectionBackoff = connectionBackoff - self.channelProvider = channelProvider - self.delegate = delegate - self.statsPeriod = statsPeriod - } - } - - /// Logging metadata keys - private enum Metadata { - /// The ID of the pool manager. - static let id = "poolmanager.id" - /// The number of managed connection pools. - static let poolCount = "poolmanager.pools.count" - /// The maximum number of connections per pool. - static let connectionsPerPool = "poolmanager.pools.conns_per_pool" - /// The maximum number of waiters per pool. - static let waitersPerPool = "poolmanager.pools.waiters_per_pool" - } - - /// The current state of the pool manager, `lock` must be held when accessing or - /// modifying `state`. - @usableFromInline - internal var _state: PoolManagerStateMachine - - @usableFromInline - internal var _pools: [ConnectionPool] - - @usableFromInline - internal let lock = NIOLock() - - /// The `EventLoopGroup` providing `EventLoop`s for connection pools. Once initialized the manager - /// will hold as many pools as there are loops in this `EventLoopGroup`. - @usableFromInline - internal let group: EventLoopGroup - - @usableFromInline - internal let id: GRPCConnectionPoolID - - /// Make a new pool manager and initialize it. - /// - /// The pool manager manages one connection pool per event loop in the provided `EventLoopGroup`. - /// Each connection pool is configured using the `perPoolConfiguration`. - /// - /// - Parameters: - /// - group: The `EventLoopGroup` providing `EventLoop`s to connections managed by the pool - /// manager. - /// - perPoolConfiguration: Configuration used by each connection pool managed by the manager. - /// - logger: A logger. - /// - Returns: An initialized pool manager. - @usableFromInline - internal static func makeInitializedPoolManager( - using group: EventLoopGroup, - perPoolConfiguration: PerPoolConfiguration, - logger: Logger - ) -> PoolManager { - let manager = PoolManager(privateButUsableFromInline_group: group) - manager.initialize(perPoolConfiguration: perPoolConfiguration, logger: logger) - return manager - } - - @usableFromInline - internal init(privateButUsableFromInline_group group: EventLoopGroup) { - self._state = PoolManagerStateMachine(.inactive) - self._pools = [] - self.group = group - self.id = .next() - - // The pool relies on the identity of each `EventLoop` in the `EventLoopGroup` being unique. In - // practice this is unlikely to happen unless a custom `EventLoopGroup` is constructed, because - // of that we'll only check when running in debug mode. - debugOnly { - let eventLoopIDs = group.makeIterator().map { ObjectIdentifier($0) } - let uniqueEventLoopIDs = Set(eventLoopIDs) - assert( - eventLoopIDs.count == uniqueEventLoopIDs.count, - "'group' contains non-unique event loops" - ) - } - } - - deinit { - self.lock.withLock { - assert( - self._state.isShutdownOrShuttingDown, - "The pool manager (\(self.id)) must be shutdown before going out of scope." - ) - } - } - - /// Initialize the pool manager, create and initialize one connection pool per event loop in the - /// pools `EventLoopGroup`. - /// - /// - Important: Must only be called once. - /// - Parameters: - /// - configuration: The configuration used for each connection pool. - /// - logger: A logger. - private func initialize( - perPoolConfiguration configuration: PerPoolConfiguration, - logger: Logger - ) { - var logger = logger - logger[metadataKey: Metadata.id] = "\(self.id)" - - let pools = self.makePools(perPoolConfiguration: configuration, logger: logger) - - logger.debug( - "initializing connection pool manager", - metadata: [ - Metadata.poolCount: "\(pools.count)", - Metadata.connectionsPerPool: "\(configuration.maxConnections)", - Metadata.waitersPerPool: "\(configuration.maxWaiters)", - ] - ) - - // The assumed maximum number of streams concurrently available in each pool. - let assumedCapacity = configuration.assumedStreamCapacity - - // The state machine stores the per-pool state keyed by the pools `EventLoopID` and tells the - // pool manager about which pool to use/operate via the pools index in `self.pools`. - let poolKeys = pools.indices.map { index in - return ConnectionPoolKey( - index: ConnectionPoolIndex(index), - eventLoopID: pools[index].eventLoop.id - ) - } - - let statsTask: RepeatedTask? - if let period = configuration.statsPeriod, let delegate = configuration.delegate { - let loop = self.group.next() - statsTask = loop.scheduleRepeatedTask(initialDelay: period, delay: period) { _ in - self.emitStats(delegate: delegate) - } - } else { - statsTask = nil - } - - self.lock.withLock { - assert(self._pools.isEmpty) - self._pools = pools - - // We'll blow up if we've already been initialized, that's fine, we don't allow callers to - // call `initialize` directly. - self._state.activatePools( - keyedBy: poolKeys, - assumingPerPoolCapacity: assumedCapacity, - statsTask: statsTask - ) - } - - for pool in pools { - pool.initialize(connections: configuration.maxConnections) - } - } - - /// Make one pool per `EventLoop` in the pool's `EventLoopGroup`. - /// - Parameters: - /// - configuration: The configuration to make each pool with. - /// - logger: A logger. - /// - Returns: An array of `ConnectionPool`s. - private func makePools( - perPoolConfiguration configuration: PerPoolConfiguration, - logger: Logger - ) -> [ConnectionPool] { - let eventLoops = self.group.makeIterator() - return eventLoops.map { eventLoop in - // We're creating a retain cycle here as each pool will reference the manager and the per-pool - // state will hold the pool which will in turn be held by the pool manager. That's okay: when - // the pool is shutdown the per-pool state and in turn each connection pool will be dropped. - // and we'll break the cycle. - return ConnectionPool( - eventLoop: eventLoop, - maxWaiters: configuration.maxWaiters, - minConnections: configuration.minConnections, - reservationLoadThreshold: configuration.loadThreshold, - assumedMaxConcurrentStreams: configuration.assumedMaxConcurrentStreams, - connectionBackoff: configuration.connectionBackoff, - channelProvider: configuration.channelProvider, - streamLender: self, - delegate: configuration.delegate, - logger: logger - ) - } - } - - // MARK: Stream Creation - - /// A future for a `Channel` from a managed connection pool. The `eventLoop` indicates the loop - /// that the `Channel` is running on and therefore which event loop the RPC will use for its - /// transport. - @usableFromInline - internal struct PooledStreamChannel { - @inlinable - internal init(futureResult: EventLoopFuture) { - self.futureResult = futureResult - } - - /// The future `Channel`. - @usableFromInline - var futureResult: EventLoopFuture - - /// The `EventLoop` that the `Channel` is using. - @usableFromInline - var eventLoop: EventLoop { - return self.futureResult.eventLoop - } - } - - /// Make a stream and initialize it. - /// - /// - Parameters: - /// - preferredEventLoop: The `EventLoop` that the stream should be created on, if possible. If - /// a pool exists running this `EventLoop` then it will be chosen over all other pools, - /// irregardless of the load on the pool. If no pool exists on the preferred `EventLoop` or - /// no preference is given then the pool with the most streams available will be selected. - /// The `EventLoop` of the selected pool will be the same as the `EventLoop` of - /// the `EventLoopFuture` returned from this call. - /// - deadline: The point in time by which the stream must have been selected. If this deadline - /// is passed then the returned `EventLoopFuture` will be failed. - /// - logger: A logger. - /// - initializer: A closure to initialize the `Channel` with. - /// - Returns: A `PoolStreamChannel` indicating the future channel and `EventLoop` as that the - /// `Channel` is using. The future will be failed if the pool manager has been shutdown, - /// the deadline has passed before a stream was created or if the selected connection pool - /// is unable to create a stream (if there is too much demand on that pool, for example). - @inlinable - internal func makeStream( - preferredEventLoop: EventLoop?, - deadline: NIODeadline, - logger: Logger, - streamInitializer initializer: @escaping @Sendable (Channel) -> EventLoopFuture - ) -> PooledStreamChannel { - let preferredEventLoopID = preferredEventLoop.map { EventLoopID($0) } - let reservedPool = self.lock.withLock { - return self._state.reserveStream(preferringPoolWithEventLoopID: preferredEventLoopID).map { - return self._pools[$0.value] - } - } - - switch reservedPool { - case let .success(pool): - let channel = pool.makeStream(deadline: deadline, logger: logger, initializer: initializer) - return PooledStreamChannel(futureResult: channel) - - case let .failure(error): - let eventLoop = preferredEventLoop ?? self.group.next() - return PooledStreamChannel(futureResult: eventLoop.makeFailedFuture(error)) - } - } - - // MARK: Shutdown - - /// Shutdown the pool manager and all connection pools it manages. - @usableFromInline - internal func shutdown(mode: ConnectionManager.ShutdownMode, promise: EventLoopPromise) { - let (action, pools): (PoolManagerStateMachine.ShutdownAction, [ConnectionPool]?) = self.lock - .withLock { - let action = self._state.shutdown(promise: promise) - - switch action { - case .shutdownPools: - // Clear out the pools; we need to shut them down. - let pools = self._pools - self._pools.removeAll(keepingCapacity: true) - return (action, pools) - - case .alreadyShutdown, .alreadyShuttingDown: - return (action, nil) - } - } - - switch (action, pools) { - case let (.shutdownPools(statsTask), .some(pools)): - statsTask?.cancel(promise: nil) - promise.futureResult.whenComplete { _ in self.shutdownComplete() } - EventLoopFuture.andAllSucceed(pools.map { $0.shutdown(mode: mode) }, promise: promise) - - case let (.alreadyShuttingDown(future), .none): - promise.completeWith(future) - - case (.alreadyShutdown, .none): - promise.succeed(()) - - case (.shutdownPools, .none), - (.alreadyShuttingDown, .some), - (.alreadyShutdown, .some): - preconditionFailure() - } - } - - private func shutdownComplete() { - self.lock.withLock { - self._state.shutdownComplete() - } - } - - // MARK: - Stats - - private func emitStats(delegate: GRPCConnectionPoolDelegate) { - let pools = self.lock.withLock { self._pools } - if pools.isEmpty { return } - - let statsFutures = pools.map { $0.stats() } - EventLoopFuture.whenAllSucceed(statsFutures, on: self.group.any()).whenSuccess { stats in - delegate.connectionPoolStats(stats, id: self.id) - } - } -} - -// MARK: - Connection Pool to Pool Manager - -extension PoolManager: StreamLender { - @usableFromInline - internal func returnStreams(_ count: Int, to pool: ConnectionPool) { - self.lock.withLock { - self._state.returnStreams(count, toPoolOnEventLoopWithID: pool.eventLoop.id) - } - } - - @usableFromInline - internal func changeStreamCapacity(by delta: Int, for pool: ConnectionPool) { - self.lock.withLock { - self._state.changeStreamCapacity(by: delta, forPoolOnEventLoopWithID: pool.eventLoop.id) - } - } -} - -@usableFromInline -internal enum PoolManagerError: Error { - /// The pool manager has not been initialized yet. - case notInitialized - - /// The pool manager has been shutdown or is in the process of shutting down. - case shutdown -} diff --git a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine+PerPoolState.swift b/Sources/GRPC/ConnectionPool/PoolManagerStateMachine+PerPoolState.swift deleted file mode 100644 index 011038ad4..000000000 --- a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine+PerPoolState.swift +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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 NIOCore - -extension PoolManagerStateMachine.ActiveState { - @usableFromInline - internal struct PerPoolState { - /// The index of the connection pool associated with this state. - @usableFromInline - internal var poolIndex: PoolManager.ConnectionPoolIndex - - /// The number of streams reserved in the pool. - @usableFromInline - internal private(set) var reservedStreams: Int - - /// The total number of streams which may be available in the pool. - @usableFromInline - internal var maxAvailableStreams: Int - - /// The number of available streams. - @usableFromInline - internal var availableStreams: Int { - return self.maxAvailableStreams - self.reservedStreams - } - - @usableFromInline - init(poolIndex: PoolManager.ConnectionPoolIndex, assumedMaxAvailableStreams: Int) { - self.poolIndex = poolIndex - self.reservedStreams = 0 - self.maxAvailableStreams = assumedMaxAvailableStreams - } - - /// Reserve a stream and return the pool. - @usableFromInline - internal mutating func reserveStream() -> PoolManager.ConnectionPoolIndex { - self.reservedStreams += 1 - return self.poolIndex - } - - /// Return a reserved stream. - @usableFromInline - internal mutating func returnReservedStreams(_ count: Int) { - self.reservedStreams -= count - assert(self.reservedStreams >= 0) - } - } -} - -extension PoolManager { - @usableFromInline - internal struct ConnectionPoolIndex: Hashable { - @usableFromInline - var value: Int - - @usableFromInline - init(_ value: Int) { - self.value = value - } - } - - @usableFromInline - internal struct ConnectionPoolKey: Hashable { - /// The index of the connection pool. - @usableFromInline - var index: ConnectionPoolIndex - - /// The ID of the`EventLoop` the connection pool uses. - @usableFromInline - var eventLoopID: EventLoopID - } -} - -@usableFromInline -internal struct EventLoopID: Hashable, CustomStringConvertible { - @usableFromInline - internal let _id: ObjectIdentifier - - @usableFromInline - internal init(_ eventLoop: EventLoop) { - self._id = ObjectIdentifier(eventLoop) - } - - @usableFromInline - internal var description: String { - return String(describing: self._id) - } -} - -extension EventLoop { - @usableFromInline - internal var id: EventLoopID { - return EventLoopID(self) - } -} diff --git a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift b/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift deleted file mode 100644 index c3674a8f0..000000000 --- a/Sources/GRPC/ConnectionPool/PoolManagerStateMachine.swift +++ /dev/null @@ -1,279 +0,0 @@ -/* - * 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 NIOCore - -@usableFromInline -internal struct PoolManagerStateMachine { - /// The current state. - @usableFromInline - internal var state: State - - @usableFromInline - internal init(_ state: State) { - self.state = state - } - - @usableFromInline - internal enum State { - case inactive - case active(ActiveState) - case shuttingDown(EventLoopFuture) - case shutdown - case _modifying - } - - @usableFromInline - internal struct ActiveState { - @usableFromInline - internal var pools: [EventLoopID: PerPoolState] - - @usableFromInline - internal var statsTask: RepeatedTask? - - @usableFromInline - internal init( - poolKeys: [PoolManager.ConnectionPoolKey], - assumedMaxAvailableStreamsPerPool: Int, - statsTask: RepeatedTask? - ) { - self.pools = Dictionary( - uniqueKeysWithValues: poolKeys.map { key in - let value = PerPoolState( - poolIndex: key.index, - assumedMaxAvailableStreams: assumedMaxAvailableStreamsPerPool - ) - return (key.eventLoopID, value) - } - ) - self.statsTask = statsTask - } - } - - /// Temporarily sets `self.state` to `._modifying` before calling the provided closure and setting - /// `self.state` to the `State` modified by the closure. - @inlinable - internal mutating func modifyingState(_ modify: (inout State) -> Result) -> Result { - var state = State._modifying - swap(&self.state, &state) - defer { - self.state = state - } - return modify(&state) - } - - /// Returns whether the pool is shutdown or in the process of shutting down. - @usableFromInline - internal var isShutdownOrShuttingDown: Bool { - switch self.state { - case .shuttingDown, .shutdown: - return true - case .inactive, .active: - return false - case ._modifying: - preconditionFailure() - } - } - - /// Activate the pool manager by providing an array of connection pools. - /// - /// - Parameters: - /// - keys: The index and `EventLoopID` of the pools. - /// - capacity: The *assumed* maximum number of streams concurrently available to a pool (that - /// is, the product of the assumed value of max concurrent streams and the number of - /// connections per pool). - @usableFromInline - internal mutating func activatePools( - keyedBy keys: [PoolManager.ConnectionPoolKey], - assumingPerPoolCapacity capacity: Int, - statsTask: RepeatedTask? - ) { - self.modifyingState { state in - switch state { - case .inactive: - let active = ActiveState( - poolKeys: keys, - assumedMaxAvailableStreamsPerPool: capacity, - statsTask: statsTask - ) - state = .active(active) - - case .active, .shuttingDown, .shutdown, ._modifying: - preconditionFailure() - } - } - } - - /// Select and reserve a stream from a connection pool. - @inlinable - mutating func reserveStream( - preferringPoolWithEventLoopID eventLoopID: EventLoopID? - ) -> Result { - return self.modifyingState { state in - switch state { - case var .active(active): - let connectionPoolIndex: PoolManager.ConnectionPoolIndex - - if let index = eventLoopID.flatMap({ eventLoopID in - active.reserveStreamFromPool(onEventLoopWithID: eventLoopID) - }) { - connectionPoolIndex = index - } else { - // Nothing on the preferred event loop; fallback to the pool with the most available - // streams. - connectionPoolIndex = active.reserveStreamFromPoolWithMostAvailableStreams() - } - - state = .active(active) - return .success(connectionPoolIndex) - - case .inactive: - return .failure(.notInitialized) - - case .shuttingDown, .shutdown: - return .failure(.shutdown) - - case ._modifying: - preconditionFailure() - } - } - } - - /// Return streams to the given pool. - mutating func returnStreams(_ count: Int, toPoolOnEventLoopWithID eventLoopID: EventLoopID) { - self.modifyingState { state in - switch state { - case var .active(active): - active.returnStreams(count, toPoolOnEventLoopWithID: eventLoopID) - state = .active(active) - - case .shuttingDown, .shutdown: - () - - case .inactive, ._modifying: - // If the manager is inactive there are no pools which can return streams. - preconditionFailure() - } - } - } - - /// Update the capacity for the given pool. - mutating func changeStreamCapacity( - by delta: Int, - forPoolOnEventLoopWithID eventLoopID: EventLoopID - ) { - self.modifyingState { state in - switch state { - case var .active(active): - active.increaseMaxAvailableStreams(by: delta, forPoolOnEventLoopWithID: eventLoopID) - state = .active(active) - - case .shuttingDown, .shutdown: - () - - case .inactive, ._modifying: - // If the manager is inactive there are no pools which can update their capacity. - preconditionFailure() - } - } - } - - enum ShutdownAction { - case shutdownPools(RepeatedTask?) - case alreadyShutdown - case alreadyShuttingDown(EventLoopFuture) - } - - mutating func shutdown(promise: EventLoopPromise) -> ShutdownAction { - self.modifyingState { state in - switch state { - case .inactive: - state = .shutdown - return .alreadyShutdown - - case .active(let active): - state = .shuttingDown(promise.futureResult) - return .shutdownPools(active.statsTask) - - case let .shuttingDown(future): - return .alreadyShuttingDown(future) - - case .shutdown: - return .alreadyShutdown - - case ._modifying: - preconditionFailure() - } - } - } - - mutating func shutdownComplete() { - self.modifyingState { state in - switch state { - case .shuttingDown: - state = .shutdown - - case .inactive, .active, .shutdown, ._modifying: - preconditionFailure() - } - } - } -} - -extension PoolManagerStateMachine.ActiveState { - @usableFromInline - mutating func reserveStreamFromPool( - onEventLoopWithID eventLoopID: EventLoopID - ) -> PoolManager.ConnectionPoolIndex? { - return self.pools[eventLoopID]?.reserveStream() - } - - @usableFromInline - mutating func reserveStreamFromPoolWithMostAvailableStreams() -> PoolManager.ConnectionPoolIndex { - // We don't allow pools to be empty (while active). - assert(!self.pools.isEmpty) - - var mostAvailableStreams = Int.min - var mostAvailableIndex = self.pools.values.startIndex - var index = mostAvailableIndex - - while index != self.pools.values.endIndex { - let availableStreams = self.pools.values[index].availableStreams - - if availableStreams > mostAvailableStreams { - mostAvailableIndex = index - mostAvailableStreams = availableStreams - } - - self.pools.values.formIndex(after: &index) - } - - return self.pools.values[mostAvailableIndex].reserveStream() - } - - mutating func returnStreams( - _ count: Int, - toPoolOnEventLoopWithID eventLoopID: EventLoopID - ) { - self.pools[eventLoopID]?.returnReservedStreams(count) - } - - mutating func increaseMaxAvailableStreams( - by delta: Int, - forPoolOnEventLoopWithID eventLoopID: EventLoopID - ) { - self.pools[eventLoopID]?.maxAvailableStreams += delta - } -} diff --git a/Sources/GRPC/ConnectionPool/PooledChannel.swift b/Sources/GRPC/ConnectionPool/PooledChannel.swift deleted file mode 100644 index 963cc406f..000000000 --- a/Sources/GRPC/ConnectionPool/PooledChannel.swift +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOHTTP2 -import SwiftProtobuf - -#if canImport(NIOSSL) -import NIOSSL -#endif - -@usableFromInline -internal final class PooledChannel: GRPCChannel { - @usableFromInline - internal let _configuration: GRPCChannelPool.Configuration - @usableFromInline - internal let _pool: PoolManager - @usableFromInline - internal let _authority: String - @usableFromInline - internal let _scheme: String - - @inlinable - internal init(configuration: GRPCChannelPool.Configuration) throws { - self._configuration = configuration - self._authority = configuration.target.host - - let tlsMode: DefaultChannelProvider.TLSMode - let scheme: String - - if let tlsConfiguration = configuration.transportSecurity.tlsConfiguration { - scheme = "https" - #if canImport(NIOSSL) - if let sslContext = try tlsConfiguration.makeNIOSSLContext() { - tlsMode = .configureWithNIOSSL(.success(sslContext)) - } else { - #if canImport(Network) - // - TLS is configured - // - NIOSSL is available but we aren't using it - // - Network.framework is available, we MUST be using that. - tlsMode = .configureWithNetworkFramework - #else - // - TLS is configured - // - NIOSSL is available but we aren't using it - // - Network.framework is not available - // NIOSSL or Network.framework must be available as TLS is configured. - fatalError() - #endif - } - #elseif canImport(Network) - // - TLS is configured - // - NIOSSL is not available - // - Network.framework is available, we MUST be using that. - tlsMode = .configureWithNetworkFramework - #else - // - TLS is configured - // - NIOSSL is not available - // - Network.framework is not available - // NIOSSL or Network.framework must be available as TLS is configured. - fatalError() - #endif // canImport(NIOSSL) - } else { - scheme = "http" - tlsMode = .disabled - } - - self._scheme = scheme - - let provider = DefaultChannelProvider( - connectionTarget: configuration.target, - connectionKeepalive: configuration.keepalive, - connectionIdleTimeout: configuration.idleTimeout, - tlsMode: tlsMode, - tlsConfiguration: configuration.transportSecurity.tlsConfiguration, - httpTargetWindowSize: configuration.http2.targetWindowSize, - httpMaxFrameSize: configuration.http2.maxFrameSize, - errorDelegate: configuration.errorDelegate, - debugChannelInitializer: configuration.debugChannelInitializer - ) - - self._pool = PoolManager.makeInitializedPoolManager( - using: configuration.eventLoopGroup, - perPoolConfiguration: .init( - maxConnections: configuration.connectionPool.connectionsPerEventLoop, - maxWaiters: configuration.connectionPool.maxWaitersPerEventLoop, - minConnections: configuration.connectionPool.minConnectionsPerEventLoop, - loadThreshold: configuration.connectionPool.reservationLoadThreshold, - assumedMaxConcurrentStreams: 100, - connectionBackoff: configuration.connectionBackoff, - channelProvider: provider, - delegate: configuration.delegate, - statsPeriod: configuration.statsPeriod - ), - logger: configuration.backgroundActivityLogger - ) - } - - @inlinable - internal func _makeStreamChannel( - callOptions: CallOptions - ) -> (EventLoopFuture, EventLoop) { - let preferredEventLoop = callOptions.eventLoopPreference.exact - let connectionWaitDeadline = NIODeadline.now() + self._configuration.connectionPool.maxWaitTime - let deadline = min(callOptions.timeLimit.makeDeadline(), connectionWaitDeadline) - - let streamChannel = self._pool.makeStream( - preferredEventLoop: preferredEventLoop, - deadline: deadline, - logger: callOptions.logger - ) { channel in - return channel.eventLoop.makeSucceededVoidFuture() - } - - return (streamChannel.futureResult, preferredEventLoop ?? streamChannel.eventLoop) - } - - // MARK: GRPCChannel conformance - - @inlinable - internal func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call 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( - path: path, - type: type, - eventLoop: eventLoop, - options: callOptions, - interceptors: interceptors, - transportFactory: .http2( - channel: stream, - authority: self._authority, - scheme: self._scheme, - maximumReceiveMessageLength: self._configuration.maximumReceiveMessageLength, - errorDelegate: self._configuration.errorDelegate - ) - ) - } - - @inlinable - internal func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call 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( - path: path, - type: type, - eventLoop: eventLoop, - options: callOptions, - interceptors: interceptors, - transportFactory: .http2( - channel: stream, - authority: self._authority, - scheme: self._scheme, - maximumReceiveMessageLength: self._configuration.maximumReceiveMessageLength, - errorDelegate: self._configuration.errorDelegate - ) - ) - } - - @inlinable - internal func close(promise: EventLoopPromise) { - self._pool.shutdown(mode: .forceful, promise: promise) - } - - @inlinable - internal func close() -> EventLoopFuture { - let promise = self._configuration.eventLoopGroup.next().makePromise(of: Void.self) - self.close(promise: promise) - return promise.futureResult - } - - @usableFromInline - internal func closeGracefully(deadline: NIODeadline, promise: EventLoopPromise) { - 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) - } - } -} diff --git a/Sources/GRPC/ConnectionPool/StreamLender.swift b/Sources/GRPC/ConnectionPool/StreamLender.swift deleted file mode 100644 index cdd507f73..000000000 --- a/Sources/GRPC/ConnectionPool/StreamLender.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -@usableFromInline -internal protocol StreamLender { - /// `count` streams are being returned to the given `pool`. - func returnStreams(_ count: Int, to pool: ConnectionPool) - - /// Update the total number of streams which may be available at given time for `pool` by `delta`. - func changeStreamCapacity(by delta: Int, for pool: ConnectionPool) -} diff --git a/Sources/GRPC/ConnectivityState.swift b/Sources/GRPC/ConnectivityState.swift deleted file mode 100644 index bf9092b9b..000000000 --- a/Sources/GRPC/ConnectivityState.swift +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOConcurrencyHelpers -import NIOCore - -/// The connectivity state of a client connection. Note that this is heavily lifted from the gRPC -/// documentation: https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md. -public enum ConnectivityState: Sendable { - /// This is the state where the channel has not yet been created. - case idle - - /// The channel is trying to establish a connection and is waiting to make progress on one of the - /// steps involved in name resolution, TCP connection establishment or TLS handshake. - case connecting - - /// The channel has successfully established a connection all the way through TLS handshake (or - /// equivalent) and protocol-level (HTTP/2, etc) handshaking. - case ready - - /// There has been some transient failure (such as a TCP 3-way handshake timing out or a socket - /// error). Channels in this state will eventually switch to the ``connecting`` state and try to - /// establish a connection again. Since retries are done with exponential backoff, channels that - /// fail to connect will start out spending very little time in this state but as the attempts - /// fail repeatedly, the channel will spend increasingly large amounts of time in this state. - case transientFailure - - /// This channel has started shutting down. Any new RPCs should fail immediately. Pending RPCs - /// may continue running till the application cancels them. Channels may enter this state either - /// because the application explicitly requested a shutdown or if a non-recoverable error has - /// happened during attempts to connect. Channels that have entered this state will never leave - /// this state. - case shutdown -} - -public protocol ConnectivityStateDelegate: AnyObject, GRPCPreconcurrencySendable { - /// Called when a change in ``ConnectivityState`` has occurred. - /// - /// - Parameter oldState: The old connectivity state. - /// - Parameter newState: The new connectivity state. - func connectivityStateDidChange(from oldState: ConnectivityState, to newState: ConnectivityState) - - /// Called when the connection has started quiescing, that is, the connection is going away but - /// existing RPCs may continue to run. - /// - /// - Important: When this is called no new RPCs may be created until the connectivity state - /// changes to 'idle' (the connection successfully quiesced) or 'transientFailure' (the - /// connection was closed before quiescing completed). Starting RPCs before these state changes - /// will lead to a connection error and the immediate failure of any outstanding RPCs. - func connectionStartedQuiescing() -} - -extension ConnectivityStateDelegate { - public func connectionStartedQuiescing() {} -} - -// Unchecked because all mutable state is protected by locks. -public class ConnectivityStateMonitor: @unchecked Sendable { - private let stateLock = NIOLock() - private var _state: ConnectivityState = .idle - - private let delegateLock = NIOLock() - private var _delegate: ConnectivityStateDelegate? - private let delegateCallbackQueue: DispatchQueue - - /// Creates a new connectivity state monitor. - /// - /// - Parameter delegate: A delegate to call when the connectivity state changes. - /// - Parameter queue: The `DispatchQueue` on which the delegate will be called. - init(delegate: ConnectivityStateDelegate?, queue: DispatchQueue?) { - self._delegate = delegate - self.delegateCallbackQueue = DispatchQueue(label: "io.grpc.connectivity", target: queue) - } - - /// The current state of connectivity. - public var state: ConnectivityState { - return self.stateLock.withLock { - self._state - } - } - - /// A delegate to call when the connectivity state changes. - public var delegate: ConnectivityStateDelegate? { - get { - return self.delegateLock.withLock { - return self._delegate - } - } - set { - self.delegateLock.withLock { - self._delegate = newValue - } - } - } - - internal func updateState(to newValue: ConnectivityState, logger: Logger) { - let change: (ConnectivityState, ConnectivityState)? = self.stateLock.withLock { - let oldValue = self._state - - if oldValue != newValue { - self._state = newValue - return (oldValue, newValue) - } else { - return nil - } - } - - if let (oldState, newState) = change { - logger.debug( - "connectivity state change", - metadata: [ - "old_state": "\(oldState)", - "new_state": "\(newState)", - ] - ) - - self.delegateCallbackQueue.async { - if let delegate = self.delegate { - delegate.connectivityStateDidChange(from: oldState, to: newState) - } - } - } - } - - internal func beginQuiescing() { - self.delegateCallbackQueue.async { - if let delegate = self.delegate { - delegate.connectionStartedQuiescing() - } - } - } -} - -extension ConnectivityStateMonitor: ConnectionManagerConnectivityDelegate { - internal func connectionStateDidChange( - _ connectionManager: ConnectionManager, - from oldState: _ConnectivityState, - to newState: _ConnectivityState - ) { - self.updateState(to: ConnectivityState(newState), logger: connectionManager.logger) - } - - internal func connectionIsQuiescing(_ connectionManager: ConnectionManager) { - self.beginQuiescing() - } -} diff --git a/Sources/GRPC/DebugOnly.swift b/Sources/GRPC/DebugOnly.swift deleted file mode 100644 index c30546970..000000000 --- a/Sources/GRPC/DebugOnly.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -internal func debugOnly(_ body: () -> Void) { - assert( - { - body() - return true - }() - ) -} diff --git a/Sources/GRPC/DelegatingErrorHandler.swift b/Sources/GRPC/DelegatingErrorHandler.swift deleted file mode 100644 index b89562eb4..000000000 --- a/Sources/GRPC/DelegatingErrorHandler.swift +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore - -/// A channel handler which allows caught errors to be passed to a `ClientErrorDelegate`. This -/// handler is intended to be used in the client channel pipeline after the HTTP/2 stream -/// multiplexer to handle errors which occur on the underlying connection. -internal final class DelegatingErrorHandler: ChannelInboundHandler { - typealias InboundIn = Any - - private var logger: Logger - private let delegate: ClientErrorDelegate? - - internal init(logger: Logger, delegate: ClientErrorDelegate?) { - self.logger = logger - self.delegate = delegate - } - - internal func channelActive(context: ChannelHandlerContext) { - self.logger.addIPAddressMetadata(local: context.localAddress, remote: context.remoteAddress) - context.fireChannelActive() - } - - internal func errorCaught(context: ChannelHandlerContext, error: Error) { - // We can ignore unclean shutdown since gRPC is self-terminated and therefore not prone to - // truncation attacks. - // - // Without this we would unnecessarily log when we're communicating with peers which don't - // send `close_notify`. - if error.isNIOSSLUncleanShutdown { - return - } - - if let delegate = self.delegate { - if let context = error as? GRPCError.WithContext { - delegate.didCatchError( - context.error, - logger: self.logger, - file: context.file, - line: context.line - ) - } else { - delegate.didCatchErrorWithoutContext(error, logger: self.logger) - } - } - context.close(promise: nil) - } -} diff --git a/Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md b/Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md deleted file mode 100644 index 4beb5f245..000000000 --- a/Sources/GRPC/Docs.docc/Proposals/0001-stub-api.md +++ /dev/null @@ -1,1135 +0,0 @@ -# gRPC-0001: stub layer and interceptor API - -## Overview - -- Proposal: gRPC-0001 -- Author(s): [George Barnett](https://github.com/glbrntt) -- Revisions: - - v1 (25/09/23): - - Adds type-erased wrappers for `AsyncSequence` and `Writer`. - - Renames `BindableService` to `RPCService` - - Add `AsyncSequence` conveneince API to `Writer` - - Add note about possible workaround for clients returning responses - -## Introduction - -This proposal lays out the API design for the stub layer and interceptors for -gRPC Swift v2. - -See also https://forums.swift.org/t/grpc-swift-plans-for-v2/67361. - -## Motivation - -The stub layer and interceptors are the highest touch point API for users of -gRPC. It's important that the API: - -- Uses natural Swift idioms. -- Feels consistent between the client and server. -- Extends naturally to the interceptor API. -- Enforces the gRPC protocol by design. In other words, it should - be impossible or difficult to construct an invalid request or response - stream. -- Allows the gRPC protocol to fully expressed. For example, server RPC handlers - should be able to send initial and trailing metadata when they - choose to. - -## Detailed design - -This design uses the canonical "echo" service to illustrate the various call -types. The service has four methods each of which map to the four gRPC call -types: - -- Get: a unary RPC, -- Collect: a client streaming RPC, and -- Expand: a server streaming RPC, -- Update: a bidirectional RPC. - -The main focus of this design is the broad shape of the generated code. While -most users generate code from a service definition written in the Protocol -Buffers IDL, this design is agnostic to the source IDL. - -### Request and response objects - -When enumerating options and experimenting with different API, using request and -response objects emerged as the best fit. The request and response objects are -distinct between the client and the server and varied by the number of messages -they accept: - -Type | Used by | Messages ----------------------------|---------|---------- -`ClientRequest.Single` | Client | One -`ClientRequest.Stream` | Client | Many -`ServerRequest.Single` | Server | One -`ServerRequest.Stream` | Server | Many -`ClientResponse.Single` | Client | One -`ClientResponse.Stream` | Client | Many -`ServerResponse.Single` | Server | One -`ServerResponse.Stream` | Server | Many - -Objects to be consumed by users are "pull based" and use `AsyncSequence`s to -represent streams of messages. Objects where messages are produced by users -(`ClientRequest.Stream` and `ServerResponse.Stream`) are "push based" and use a -"producer function" to provide messages. These types are detailed below. - -```swift -public enum ClientRequest { - /// A request created by the client containing a single message. - public struct Single: Sendable { - /// Metadata sent at the begining of the RPC. - public var metadata: Metadata - - /// The message to send to the server. - public var message: Message - - /// Create a new single client request. - public init(message: Message, metadata: Metadata = [:]) { - // ... - } - } - - /// A request created by the client containing a message producer. - public struct Stream: Sendable { - public typealias Producer = @Sendable (RPCWriter) async throws -> Void - - /// Metadata sent at the begining of the RPC. - public var metadata: Metadata - - /// A closure which produces and writes messages into a writer destined for - /// the server. - /// - /// The producer will only be consumed once by gRPC and therefore isn't - /// required to be idempotent. If the producer throws an error then the RPC - /// will be cancelled. - public var producer: Producer - - /// Create a new streaming client request. - public init(metadata: Metadata = [:], producer: @escaping Producer) { - // ... - } - } -} - -public enum ServerRequest { - /// A request received at the server containing a single message. - public struct Single: Sendable { - /// Metadata received from the client at the begining of the RPC. - public var metadata: Metadata - - /// The message received from the client. - public var message: Message - - /// Create a new single server request. - public init(metadata: Metadata, message: Message) { - // ... - } - } - - /// A request received at the server containing a stream of messages. - public struct Stream: Sendable { - /// Metadata received from the client at the begining of the RPC. - public var metadata: Metadata - - /// An `AsyncSequence` of messages received from the client. - public var messages: RPCAsyncSequence - - /// Create a new streaming server request. - public init(metadata: Metadata, messages: RPCAsyncSequence) { - // ... - } - } -} - -public enum ServerResponse { - /// A response returned by a service for a single message. - public struct Single: Sendable { - /// The outcome of the RPC. - /// - /// The `success` indicates the server accepted the RPC for processing and - /// the RPC completed successfully. The `failure` case indicates that the - /// server either rejected the RPC or threw an error while processing the - /// request. In the `failure` case only a status and trailing metadata will - /// be returned to the client. - public var result: Result - - /// An accepted RPC with a successful outcome. - public struct Accepted { - /// Metadata to send to the client at the beginning of the response stream. - public var metadata: Metadata - - /// The single message to send back to the client. - public var message: Message - - /// Metadata to send to the client at the end of the response stream. - public var trailingMetadata: Metadata - } - - public init(result: Result) { - // ... - } - - /// Conveneince API to create an successful response. - public init(message: Message, metadata: Metadata = [:], trailingMetadata: Metadata = [:]) { - // ... - } - - /// Conveneince API to create an unsuccessful response. - public init(error: RPCError) { - // ... - } - } - - /// A response returned by a service producing a stream of messages. - public struct Stream: Sendable { - /// The initial outcome of the RPC; a `success` result indicates that the - /// services has accepted the RPC for processing. The RPC may still result - /// in failure by later throwing an error. - /// - /// The `failure` case indicates that the server rejected the RPC and will - /// not process it. Only status and trailing metadata will be sent to the - /// client. - public var result: Result - - /// A closure which, when called, writes values into the provided writer and - /// returns trailing metadata indicating the end of the response stream. - public typealias Producer = @Sendable (RPCWriter) async throws -> Metadata - - /// An accepted RPC. - public struct Accepted: Sendable { - /// Metadata to send to the client at the beginning of the response stream. - public var metadata: Metadata - - /// A closure which, when called, writes values into the provided writer and - /// returns trailing metadata indicating the end of the response stream. - /// - /// Returning metadata indicates a successful response and gRPC will - /// terminate the RPC with an 'ok' status code. Throwing an error will - /// terminate the RPC with an appropriate status code. You can control the - /// status code, message and metadata returned to the client by throwing an - /// `RPCError`. If the error thrown is not an `RPCError` then the `unknown` - /// status code is used. - /// - /// gRPC will invoke this function at most once therefore it isn't required - /// to be idempotent. - public var producer: Producer - } - - public init(result: Result) { - // ... - } - - /// Conveneince API to create an accepted response. - public init(metadata: Metadata = [:], producer: @escaping Producer) { - // ... - } - - /// Conveneince API to create an unsuccessful response. - public init(error: RPCError) { - // ... - } - } -} - -public enum ClientResponse { - public struct Single { - /// The body of an accepted single response. - public struct Body { - /// Metadata received from the server at the start of the RPC. - public var metadata: Metadata - - /// The message received from the server. - public var message: Message - - /// Metadata received from the server at the end of the RPC. - public var trailingMetadata: Metadata - } - - /// Whether the RPC was accepted or rejected. - /// - /// The `success` case indicates the RPC completed successfully with an 'ok' - /// status code. The `failure` case indicates that the RPC was rejected or - /// couldn't be completed successfully. - public var result: Result - - public init(result: Result) { - // ... - } - - // Note: it's possible to provide a number of conveneince APIs on top: - - /// The metadata received from server at the start of the RPC. - /// - /// The metadata will be empty if `result` is `failure`. - public var metadata: Metadata { - get { - // ... - } - } - - /// The message returned from the server. - /// - /// Throws if the RPC was rejected or failed. - public var message: Message { - get throws { - // ... - } - } - - /// The metadata received from server at the end of the RPC. - public var trailingMetadata: Metadata { - get { - // ... - } - } - } - - public struct Stream: Sendable { - public struct Body { - /// Metadata received from the server at the start of the RPC. - public var metadata: Metadata - - /// A sequence of messages received from the server ending with metadata - /// if the RPC succeeded. - /// - /// If the RPC fails then the sequence will throw an error. - public var bodyParts: RPCAsyncSequence - - public enum BodyPart: Sendable { - case message(Message) - case trailers(Metadata) - } - } - - /// Whether the RPC was accepted or rejected. - /// - /// The `success` case indicates the RPC was accepted by the server for - /// processing, however, the RPC may still fail by throwing an error from its - /// `messages` sequence. The `failure` case indicates that the RPC was - /// rejected by the server. - public var result: Result - - public init(result: Result) { - // ... - } - - // Note: it's possible to provide a number of conveneince APIs on top: - - /// The metadata received from server at the start of the RPC. - /// - /// The metadata will be empty if `result` is `failure`. - public var metadata: Metadata { - get { - // ... - } - } - - /// The stream of messages received from the server. - public var messages: RPCAsyncSequence { - get { - // ... - } - } - - /// The metadata received from server at the end of the RPC. - public var trailingMetadata: Metadata { - get { - // ... - } - } - } -} - -// MARK: - Supporting types - -/// A sink for values which are produced over time. -public protocol Writer: Sendable { - /// Write a sequence of elements. - /// - /// Writes may suspend if the sink is unable to accept writes. - func write(contentsOf elements: some Sequence) async throws -} - -extension Writer { - /// Write a single element. - public func write(_ element: Element) async throws { - try await self.write(contentsOf: CollectionOfOne(element)) - } - - /// Write an `AsyncSequence` of elements. - public func write( - contentsOf elements: Source - ) async throws where Source.Element == Element { - for try await element in elements { - try await self.write(element) - } - } -} - -/// A type-erasing `Writer`. -public struct RPCWriter: Writer { - public init(wrapping other: Other) where Other.Element == Element { - // ... - } -} - -/// A type-erasing `AsyncSequence`. -public struct RPCAsyncSequence: AsyncSequence, Sendable { - public init(wrapping other: Other) where Other.Element == Element { - // ... - } -} - -/// An RPC error. -/// -/// Every RPC is terminated with a status which includes a code, and optionally, -/// a message. The status describes the ultimate outcome of the RPC. -/// -/// This type is like a status but only represents negative outcomes, that is, -/// all status codes except for `ok`. This type can also carry ``Metadata``. -/// This can be used by service authors to transmit additional information to -/// clients if an RPC throws an error. -public struct RPCError: Error, Hashable, Sendable { - public struct Code: Hashable, Sendable { - public var code: UInt8 - - private init(_ code: UInt8) { - // ... - } - - // All non-zero status codes from: - // https://github.com/grpc/grpc/blob/master/doc/statuscodes.md - - /// The operation was cancelled (typically by the caller). - public static let cancelled = Self(code: 1) - - /// Unknown error. An example of where this error may be returned is if a - /// status value received from another address space belongs to an error-space - /// that is not known in this address space. Also errors raised by APIs that - /// do not return enough error information may be converted to this error. - public static let unknown = Self(code: 2) - - // etc. - } - - /// The error code. - public var code: Code - - /// A message describing the error. - public var message: String - - /// Metadata associated with the error. - public var metadata: Metadata - - public init(code: Code, message: String, metadata: Metadata = [:]) { - // ... - } -} -``` - -### Generated server code - -Code generated for each service includes two protocols. The first, higher-level -protocol which most users interact with, includes one function per defined -method. The shape of the function matches the method definition. For example -unary methods accept a `ServerRequest.Single` and return a -`ServerResponse.Single`, bidirectional streaming methods accept a -`ServerRequest.Stream` and return a `ServerResponse.Stream`. - -The second, base protocol, defines each method in terms of streaming requests -and responses. The higher-level protocol refines the base protocol and provides -default implementations of methods in the base protocol in terms of their -higher-level counterpart. - -The base protocol is an escape hatch allowing advanced users to have further -control over their RPCs. As an example, if a service owner needs to respond to -initial metadata in a client streaming RPC before processing the complete stream -of messages from the request they could implement their RPC in terms of the -fully streaming version provided by the base protocol. - -Users can throw any error from each method. Since gRPC has a well defined error -model, gRPC Swift catches errors of type `RPCError` and extracts the code and -message. The code and message are propagated back to the client as the status of -the RPC. The library discards all other errors and returns a status with code -`unknown` to the client. - -The following code demonstrates how these protocols would look for the Echo -service. Some details are elided as they aren't relevant. - -```swift -// (Defined elsewhere.) -public typealias EchoRequest = ... -public typealias EchoResponse = ... - -/// The generated base protocol for the "Echo" service providing each method -/// in a fully streamed form. -/// -/// This protocol should typically not be implemented, instead you should -/// implement ``EchoServiceProtocol`` which refines this protocol. However, if -/// you require more granular control over your RPCs then they may implement -/// this protocol, or methods from this protocol, instead. -public protocol EchoServiceStreamingProtocol: RPCService, Sendable { - func get( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream - - func collect( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream - - func expand( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream - - func update( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream -} - -// Generated conformance to `RPCService`. -extension EchoServiceStreamingProtocol { - public func registerRPCs(with router: inout RPCRouter) { - // Implementation elided. - } -} - -/// The generated protocol for the "Echo" service. -/// -/// You must implement an instance of this protocol with your business logic and -/// register it with a server in order to use it. See also -/// ``EchoServiceStreamingProtocol``. -public protocol EchoServiceProtocol: EchoServiceStreamingProtocol { - func get( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single - - func collect( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single - - func expand( - request: ServerRequest.Single - ) async throws -> ServerResponse.Stream - - func update( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream -} - -// Generated partial conformance to `EchoServiceStreamingProtocol`. -extension EchoServiceProtocol { - public func get( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { - // Implementation elided. Calls corresponding function on `EchoServiceStreamingProtocol`. - } - - public func collect( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { - // Implementation elided. Calls corresponding function on `EchoServiceStreamingProtocol`. - } - - public func expand( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { - // Implementation elided. Calls corresponding function on `EchoServiceStreamingProtocol`. - } - - // Note: 'update' has the same definition in `EchoServiceProtocol` and - // `EchoServiceStreamingProtocol` and is not required here. -} -``` - -#### Example: Echo service implementation - -One could implement the Echo service as: - -```swift -struct EchoService: EchoServiceProtocol { - func get( - request: ServerRequest.Single - ) async throws -> ServerResponse.Single { - // Echo back the original message. - return ServerResponse.Single( - message: EchoResponse(text: "echo: \(request.message.text)") - ) - } - - func collect( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Single { - // Gather all request messages and join them - let joined = try await request.messages.map { - $0.text - }.reduce(into: []) { - $0.append($1) - }.join(separator: " ") - - // Responsd with the joined message. Unlike 'get', we also echo back the - // request metadata as the leading and trailing metadata. - return ServerResponse.Single( - message: EchoResponse(text: "echo: \(joined)") - metadata: request.metadata, - trailingMetadata: request.metadata - ) - } - - func expand( - request: ServerRequest.Single - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in - // Echo back each part of the single request - for part in request.message.text.split(separator: " ") { - try await writer.write(EchoResponse(text: "echo: \(part)")) - } - - return [:] - } - } - - func update( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { - // Echo back the request metadata as the initial metadata. - return ServerResponse.Stream(metadata: request.metadata) { writer in - // Echo back each request message - for try await message in request.messages { - try await writer.write(EchoResponse(text: "echo: \(message.text)")) - } - - // Echo back the request metadata as trailing metadata. - return request.metadata - } - } -} -``` - -### Generated client code - -The generated client code follows a similar pattern to the server code. Each -method has the same shape: it accepts a request and a closure which handles a -response from the server. The closure is generic over its return type and the -method returns that value to the caller once the closure exits. Having a -response handler provides a signal to the caller that once the closure exits the -RPC has finished and gRPC can free any related resources. - -Each method also has additional parameters which the generated code would -provide defaults for, including the request encoder and response decoder. -In most cases users would not need to specify the encoder and decoder. - -Code generated for the client includes a single protocol and a concrete -implementation of that protocol. The following code demonstrates how the -generated client protocol for the Echo service would look. - -```swift -// (Defined elsewhere.) -public typealias EchoRequest = ... -public typealias EchoResponse = ... - -/// The generated protocol for a client of the "Echo" service. -public protocol EchoClientProtocol: Sendable { - func get( - request: ClientRequest.Single, - encoder: some MessageEncoder, - decoder: some MessageDecoder, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R - ) async rethrows -> R - - func collect( - request: ClientRequest.Stream, - encoder: some MessageEncoder, - decoder: some MessageDecoder, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R - ) async rethrows -> R - - func expand( - request: ClientRequest.Single, - encoder: some MessageEncoder, - decoder: some MessageDecoder, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R - ) async rethrows -> R - - func update( - request: ClientRequest.Stream, - encoder: some MessageEncoder, - decoder: some MessageDecoder, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R - ) async rethrows -> R -} - -extension EchoClientProtocol { - public func get( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R - ) async rethrows -> R { - // Implementation elided. Calls corresponding function on - // `EchoClientProtocol` specifying the encoder and decoder. - } - - func collect( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R - ) async rethrows -> R { - // Implementation elided. Calls corresponding function on - // `EchoClientProtocol` specifying the encoder and decoder. - } - - func expand( - request: ClientRequest.Single, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R - ) async rethrows -> R { - // Implementation elided. Calls corresponding function on - // `EchoClientProtocol` specifying the encoder and decoder. - } - - func update( - request: ClientRequest.Stream, - _ body: @Sendable @escaping (ClientResponse.Stream) async throws -> R - ) async rethrows -> R { - // Implementation elided. Calls corresponding function on - // `EchoClientProtocol` specifying the encoder and decoder. - } -} - -// Note: a concerete client implementation would also be generated. The details -// aren't interesting here. -``` - -#### Example: Echo client usage - -An example of using the Echo client follows. Some functions highlight the -"sugared" API built on top of the more verbose lower-level API: - -```swift -func get(echo: some EchoClientProtocol) async throws { - // Make the request: - let request = ClientRequest.Single(message: Echo.Request(text: "foo")) - - // Execute the RPC (most verbose API): - await echo.get(request: request) { response in - switch response.result { - case .success(let body): - print( - """ - 'Get' succeeded. - metadata: \(body.metadata) - message: \(body.message.text) - trailing metadata: \(body.trailingMetadata) - """ - ) - case .failure(let error): - print("'Get' failed with error code '\(error.code)' and metadata '\(error.metadata)'") - } - } - - // Execute the RPC (sugared API): - await echo.get(request: request) { response in - print("'Get' received metadata '\(response.metadata)'") - do { - let message = try response.message - print("'Get' received '\(message.text)'") - } catch { - print("'Get' caught error '\(error)'") - } - } - - // The generated code _could_ default the closure to return the response - // message which would make the common case straighforward: - let message = try await echo.get(request: request) - print("'Get' received '\(message.text)'") -} - -func clientStreaming(echo: some EchoClientProtocol) async throws { - // Make the request: - let request = ClientRequest.Stream { writer in - for text in ["foo", "bar", "baz"] { - try await writer.write(Echo.Request(text: text)) - } - } - - // Execute the RPC: - try await echo.collect(request: request) { response in - // (Same as for the unary "Get") - } -} - -func serverStreaming(echo: some EchoClientProtocol) async throws { - // Make the request, adding metadata: - let request = ClientRequest.Single( - message: Echo.Request(text: "foo bar baz"), - metadata: ["foo": "bar"], - ) - - // Execute the RPC (most verbose API): - try await echo.expand(request: request) { response in - switch response.result { - case .success(let body): - print("'Expand' accepted with metadata '\(body.metadata)'") - do { - for try await part in body.bodyParts { - switch part { - case .message(let message): - print("'Expand' received message '\(message.text)'") - case .trailers(let metadata): - print("'Expand' received trailers '\(metadata)'") - } - } - } catch let error as RPCError { - print("'Expand' failed with error '\(error.code)' and metadata '\(error.metadata)'") - } catch { - print("'Expand' failed with error '\(error)'") - } - case .failure(let error): - print("'Expand' rejected with error '\(error.code)' and metadata '\(error.metadata)'") - } - } - - // Execute the RPC (sugared API): - await echo.expand(request: request) { response in - print("'Expand' received metadata '\(response.metadata)'") - do { - for try await message in response.messages { - print("'Expand' received '\(message.text)'") - } - } catch let error as RPCError { - print("'Expand' failed with error '\(error.code)' and metadata '\(error.metadata)'") - } catch { - print("'Expand' failed with error '\(error)'") - } - } - - // Note: there is no generated 'default' handler, the function body defines - // the lifetime of the RPC and the RPC is cancelled once the closure exits. - // Therefore escaping the message sequence would result in the sequence - // throwing an error. It must be consumed from within the handler. It may - // be possible for the compiler to enforce this in the future with - // `~Escapable`. - // - // See: https://github.com/atrick/swift-evolution/blob/bufferview-roadmap/visions/language-support-for-BufferView.md -} - -func bidirectional(echo: some EchoClient) async throws { - // Make the request, adding metadata: - let request = ClientRequest.Stream(metadata: ["foo": "bar"]) { writer in - for text in ["foo", "bar", "baz"] { - try await writer.write(Echo.Request(text: text)) - } - } - - // Execute the RPC: - try await echo.collect(request: request) { response in - // (Same as for the server streaming "Expand") - } -} -``` - -### Interceptors - -Using the preceding patterns allows for interceptors to follow the shape of -bidirectional streaming calls. This is advantageous: once users are comfortable -with the bidirectional RPC interface the step to writing interceptors is -straighforward. - -The `protocol` for client and server interceptors also have the same shape: they -require a single function `intercept` which accept a `request`, `context`, and -`next` parameters. The `request` parameter is the request object which is -_always_ the streaming variant. The `context` provides additional information -about the intercepted RPC, and `next` is a closure that the interceptor may call -to forward the request and context to the next interceptor. - -```swift -/// A type that intercepts requests and response for clients. -/// -/// Interceptors allow users to inspect and modify requests and responses. -/// Requests are intercepted before they are handed to a transport. Responses -/// are intercepted after they have been received from the transport and before -/// they are returned to the client. -/// -/// They are typically used for cross-cutting concerns like injecting metadata, -/// validating messages, logging additional data, and tracing. -/// -/// Interceptors are registered with a client and apply to all RPCs. Use the -/// ``ClientContext/descriptor`` if you need to configure behaviour on a per-RPC -/// basis. -public protocol ClientInterceptor: Sendable { - /// Intercept a request object. - /// - /// - Parameters: - /// - request: The request object. - /// - context: Additional context about the request, including a descriptor - /// of the method being called. - /// - next: A closure to invoke to hand off the request and context to the next - /// interceptor in the chain. - /// - Returns: A response object. - func intercept( - request: ClientRequest.Stream, - context: ClientContext, - next: @Sendable ( - _ request: ClientRequest.Stream, - _ context: ClientContext - ) async throws -> ClientResponse.Stream - ) async throws -> ClientResponse.Stream -} - -/// A context passed to client interceptors containing additional information -/// about the RPC. -public struct ClientContext: Sendable { - /// A description of the method being called including the method and service - /// name. - public var descriptor: MethodDescriptor -} - -/// A type that intercepts requests and response for servers. -/// -/// Interceptors allow users to inspect and modify requests and responses. -/// Requests are intercepted after they have been received from the transport but -/// before they have been handed off to a service. Responses are intercepted -/// after they have been returned from a service and before they are written to -/// the transport. -/// -/// They are typically used for cross-cutting concerns like validating metadata -/// and messages, logging additional data, and tracing. -/// -/// Interceptors are registered with the server and apply to all RPCs. Use the -/// ``ClientContext/descriptor`` if you need to configure behaviour on a per-RPC -/// basis. -public protocol ServerInterceptor: Sendable { - /// Intercept a request object. - /// - /// - Parameters: - /// - request: The request object. - /// - context: Additional context about the request, including a descriptor - /// of the method being called. - /// - next: A closure to invoke to hand off the request and context to the next - /// interceptor in the chain. - /// - Returns: A response object. - func intercept( - request: ServerRequest.Stream, - context: ServerContext, - next: @Sendable ( - _ request: ServerRequest.Stream - _ context: ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream -} - -/// A context passed to server interceptors containing additional information -/// about the RPC. -public struct ServerContext: Sendable { - /// A description of the method being called including the method and service - /// name. - public var descriptor: MethodDescriptor -} -``` - -Importantly with this pattern, the API is a natural extension of both client and -server API for bidirectional streaming RPCs so users don't need to learn a new -paradigm, the same concepts apply. - -Some examples of interceptors include: - -```swift -struct AuthenticatingServerInterceptor: ServerInterceptor { - func intercept( - request: ServerRequest.Stream, - context: ServerContext, - next: @Sendable ( - _ request: ServerRequest.Stream - _ context: ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream { - guard let token = metadata["auth"], self.validate(token) else { - // Token is missing or not valid, reject the request and respond - // appropriately. - return ServerResponse.Stream( - error: RPCError(code: .unauthenticated, message: "...") - ) - } - - // Valid token is present, forward the request. - return try await next(request, context) - } -} - -struct LoggingClientInterceptor: ClientInterceptor { - struct LoggingWriter: Writer { - let base: RPCWriter - - init(wrapping base: RPCWriter) { - self.base = base - } - - func write(_ value: Value) async throws { - try await self.base.write(value) - print("Sent message: '\(value)'") - } - } - - func intercept( - request: ClientRequest.Stream, - context: ClientContext, - next: @Sendable ( - _ request: ClientRequest.Stream, - _ context: ClientContext - ) async throws -> ClientResponse.Stream - ) async throws -> ClientResponse.Stream { - // Construct a request which wraps the original and uses a logging writer - // to print a message every time a message is written. - let interceptedRequest = ClientRequest.Stream( - metadata: request.metadata - ) { writer in - let loggingWriter = LoggingWriter(wrapping: writer) - try await request.producer(loggingWriter) - print("Send end") - } - - print("Making request to '\(context.descriptor)', metadata: \(request.metadata)") - - let response = try await(interceptedRequest, context) - let interceptedResponse: ClientResponse.Stream - - // Inspect the response. On success re-map the body to print each part. On - // failure print the error. - switch response.result { - case .success(let body): - print("Call accepted, metadata: '\(body.metadata)'") - interceptedResponse = ClientResponse.Stream( - result = .success( - ClientResponse.Stream.Body( - metadata: body.metadata, - bodyParts: body.bodyParts.map { - switch $0 { - case .message(let message): - print("Received message: '\(message)'") - case .metadata(let metadata): - print("Received metadata: '\(metadata)'") - } - - return $0 - } - ) - ) - ) - - case .failure(let error): - print("Call failed with error code: '\(error.code)', metadata: '\(error.metadata)'") - interceptedResponse = response - } - - return interceptedResponse - } -} -``` - -## Alternative approaches - -When enumerating designs there were a number of alternatives considered which -were ultimately dismissed. These are briefly described in the following -sections. - -### Strong error typing - -As gRPC has well defined error codes, having API which enforce `RPCError` as -the thrown error type is appealing as it ensures service authors propagate -appropriate information to clients and clients know they can only observe -`RPCError`s. - -However, Swift doesn't currently have typed throws so would have to resort to -using `Result` types. While the API could be heavily sugared it doesn't result in -idiomatic code. There are a few places where using `Result` types simply doesn't -work in an ergonomic way. Each `AsyncSequence`, for example, would have to be -non-throwing and use `Result` types as their `Element`. - -> Note: there has been recent active work in this area for Embedded Swift so -> this might be worth revisiting in the future. -> -> https://forums.swift.org/t/status-check-typed-throws/66637 - -### Using `~Copyable` writers - -One appealing aspect of `~Copyable` types and ownership modifiers is that it's -easy to represent an writer as a kind of state machine. Consider a writer passed -to a server handler, for example. Initially it may either send metadata or it -may return a status as its first and only value. If it writes metadata it may -then write messages. After any number of messages it may then write a final -status. This composes naturally with `~Copyable` types: - -```swift -struct Writer: ~Copyable { - consuming func writeMetadata(_ metadata: Metadata) async throws -> Body { - // ... - } - - consuming func writeEnd(_ metadata: Metadata) async throws { - // ... - } - - struct Body: ~Copyable { - func writeMessage(_ message: Message) async throws { - // ... - } - - consuming func writeEnd(_ metadata: Metadata) async throws { - // ... - } - } -} -``` - -This neatly encapsulates the stream semantics of gRPC and uses the compiler to -ensure that writing an invalid stream is impossible. However, in practice it isn't -ergonomic to use as it requires dealing with multiple writer types and users can -still reach for writer functions after consuming the type, they just result in -an error. It also doesn't obviously allow for "default" values, the API forces users -to send initial metadata to get the `Body` writer. In practice most users don't -need to set metadata and the framework sends empty metadata on their behalf. - -### Using `AsyncSequence` for outbound message streams - -The `ClientRequest.Stream` and `ServerResponse.Stream` each have a `producer` -closure provided by callers. This is a "push" based system: callers must emit -messages into the writer when they wish to send messages to the other peer. - -An alternative to this would be to use a "pull" based API like `AsyncSequence`. -There are, however, some downsides to this. - -The first is that many `AsyncSequence` implementations don't appropriately exert -backpressure to the producer: `AsyncStream`, for example has an unbounded buffer -by default, this is problematic as the underlying transport may not be able to -consume messages as fast as the sequence is producing them. `AsyncChannel` has a -maximum buffer size of 1, while this wouldn't overwhelm the transport, it has -undesirable performance characteristics requiring expensive suspension points -for each message. The `Writer` approach allows the transport to directly exert -backpressure on the writer. - -Finally, on the server, to allow users to send trailing metadata, the -caller would have to deal with an `enum` of messages and metadata. The onus -would fall on the implementer of the service to ensure that metadata only -appears once as the final element in the stream. The proposed -`ServerResponse.Stream` avoids this by requiring the user to specify a closure -which accepts a writer and returns `Metadata`. This ensures implementers can -send any number of messages followed by exactly one `Metadata`. - -### Clients returning responses - -The proposed client API requires that the caller consumes responses within a -closure. A more obvious spelling is for the client methods to return a response -object to the caller. - -However, this has a number of issues. For example it doesn't make the lifetime -of the RPC obvious to the caller which can result in users accidentally keeping -expensive network resources alive for longer than necessary. Another issue is -that RPCs typically have deadlines associated with them which are naturally -modelled as separate `Task`s. These `Task`s need to run somewhere, which isn't -possible to do without resorting to unstructured concurrency. Using a response -handler allows the client to run the RPC within a `TaskGroup` and have the -response handler run as a child task next to any tasks which require running -concurrently. - -One workaround is for clients to have a long running `TaskGroup` for executing -such tasks required by RPCs. This would allow a model whereby the request is -intercepted in the callers task (and would therefore also have access to task -local values) and then be transferred to the clients long running task for -execution. In this model the lifetime of the RPC would be bounded by the -lifetime of the response object. One major downside to this approach is that -task locals are only reachable from the interceptors: they wouldn't be reachable -from the transport layer which may have its own transport-specific interceptors. diff --git a/Sources/GRPC/Docs.docc/index.md b/Sources/GRPC/Docs.docc/index.md deleted file mode 100644 index 2b6cc1087..000000000 --- a/Sources/GRPC/Docs.docc/index.md +++ /dev/null @@ -1,128 +0,0 @@ -# ``GRPC`` - -gRPC for Swift. - -grpc-swift is a Swift package that contains a gRPC Swift API and code generator. - -It is intended for use with Apple's [SwiftProtobuf][swift-protobuf] support for -Protocol Buffers. Both projects contain code generation plugins for `protoc`, -Google's Protocol Buffer compiler, and both contain libraries of supporting code -that is needed to build and run the generated code. - -APIs and generated code is provided for both gRPC clients and servers, and can -be built either with Xcode or the Swift Package Manager. Support is provided for -all four gRPC API styles (Unary, Server Streaming, Client Streaming, and -Bidirectional Streaming) and connections can be made either over secure (TLS) or -insecure channels. - -## Supported Platforms - -gRPC Swift's platform support is identical to the [platform support of Swift -NIO][swift-nio-platforms]. - -The earliest supported version of Swift for gRPC Swift releases are as follows: - -gRPC Swift Version | Earliest Swift Version --------------------|----------------------- -`1.0.0 ..< 1.8.0` | 5.2 -`1.8.0 ..< 1.11.0` | 5.4 -`1.11.0..< 1.16.0`.| 5.5 -`1.16.0..< 1.20.0` | 5.6 -`1.20.0..< 1.22.0` | 5.7 -`1.22.0..< 1.24.0` | 5.8 -`1.24.0...` | 5.9 - -Versions of clients and services which are use Swift's Concurrency support -are available from gRPC Swift 1.8.0 and require Swift 5.6 and newer. - -## Getting gRPC Swift - -There are two parts to gRPC Swift: the gRPC library and an API code generator. - -### Getting the gRPC library - -The Swift Package Manager is the preferred way to get gRPC Swift. Simply add the -package dependency to your `Package.swift`: - -```swift -dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.15.0") -] -``` - -...and depend on `"GRPC"` in the necessary targets: - -```swift -.target( - name: ..., - dependencies: [.product(name: "GRPC", package: "grpc-swift")] -] -``` - -### Getting the protoc Plugins - -Binary releases of `protoc`, the Protocol Buffer Compiler, are available on -[GitHub][protobuf-releases]. - -To build the plugins, run the following in the main directory: - -```sh -$ swift build --product protoc-gen-swift -$ swift build --product protoc-gen-grpc-swift -``` - -This uses the Swift Package Manager to build both of the necessary plugins: -`protoc-gen-swift`, which generates Protocol Buffer support code and -`protoc-gen-grpc-swift`, which generates gRPC interface code. - -To install these plugins, just copy the two executables (`protoc-gen-swift` and -`protoc-gen-grpc-swift`) that show up in the main directory into a directory -that is part of your `PATH` environment variable. Alternatively the full path to -the plugins can be specified when using `protoc`. - -#### 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. The -[`Examples/v1`][examples] directory contains examples which do not require -additional dependencies and may be built using the Swift Package Manager. - -Some of the examples are accompanied by tutorials, including: -- A [quick start guide][docs-quickstart] for creating and running your first - gRPC service. -- A [basic tutorial][docs-tutorial] covering the creation and implementation of - a gRPC service using all four call types as well as the code required to setup - and run a server and make calls to it using a generated client. -- An [interceptors][docs-interceptors-tutorial] tutorial covering how to create - and use interceptors with gRPC Swift. - -## Additional documentation - -- Options for the `protoc` plugin in [`docs/plugin.md`][docs-plugin] -- How to configure TLS in [`docs/tls.md`][docs-tls] -- How to configure keepalive in [`docs/keepalive.md`][docs-keepalive] -- Support for Apple Platforms and NIO Transport Services in - [`docs/apple-platforms.md`][docs-apple] - -[docs-apple]: https://github.com/grpc/grpc-swift/tree/main/docs/apple-platforms.md -[docs-plugin]: https://github.com/grpc/grpc-swift/tree/main/docs/plugin.md -[docs-quickstart]: https://github.com/grpc/grpc-swift/tree/main/docs/quick-start.md -[docs-tls]: https://github.com/grpc/grpc-swift/tree/main/docs/tls.md -[docs-keepalive]: https://github.com/grpc/grpc-swift/tree/main/docs/keepalive.md -[docs-tutorial]: https://github.com/grpc/grpc-swift/tree/main/docs/basic-tutorial.md -[docs-interceptors-tutorial]: https://github.com/grpc/grpc-swift/tree/main/docs/interceptors-tutorial.md -[grpc]: https://github.com/grpc/grpc -[protobuf-releases]: https://github.com/protocolbuffers/protobuf/releases -[swift-nio-platforms]: https://github.com/apple/swift-nio#supported-platforms -[swift-nio]: https://github.com/apple/swift-nio -[swift-protobuf]: https://github.com/apple/swift-protobuf -[xcode-spm]: https://help.apple.com/xcode/mac/current/#/devb83d64851 -[branch-new]: https://github.com/grpc/grpc-swift/tree/main -[branch-old]: https://github.com/grpc/grpc-swift/tree/cgrpc -[examples]: https://github.com/grpc/grpc-swift/tree/main/Examples/v1 diff --git a/Sources/GRPC/Error+NIOSSL.swift b/Sources/GRPC/Error+NIOSSL.swift deleted file mode 100644 index b796d6fcd..000000000 --- a/Sources/GRPC/Error+NIOSSL.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOSSL -#endif - -extension Error { - internal var isNIOSSLUncleanShutdown: Bool { - #if canImport(NIOSSL) - if let sslError = self as? NIOSSLError { - return sslError == .uncleanShutdown - } else { - return false - } - #else - return false - #endif - } -} diff --git a/Sources/GRPC/EventLoopFuture+RecoverFromUncleanShutdown.swift b/Sources/GRPC/EventLoopFuture+RecoverFromUncleanShutdown.swift deleted file mode 100644 index 335988a97..000000000 --- a/Sources/GRPC/EventLoopFuture+RecoverFromUncleanShutdown.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 NIOCore - -extension EventLoopFuture where Value == Void { - internal func recoveringFromUncleanShutdown() -> EventLoopFuture { - // We can ignore unclean shutdown since gRPC is self-terminated and therefore not prone to - // truncation attacks. - return self.flatMapErrorThrowing { error in - if error.isNIOSSLUncleanShutdown { - return () - } else { - throw error - } - } - } -} diff --git a/Sources/GRPC/FakeChannel.swift b/Sources/GRPC/FakeChannel.swift deleted file mode 100644 index 56ed52c86..000000000 --- a/Sources/GRPC/FakeChannel.swift +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOEmbedded -import SwiftProtobuf - -// This type is deprecated, but we need to '@unchecked Sendable' to avoid warnings in our own code. -@available(swift, deprecated: 5.6) -extension FakeChannel: @unchecked Sendable {} - -/// A fake channel for use with generated test clients. -/// -/// The `FakeChannel` provides factories for calls which avoid most of the gRPC stack and don't do -/// real networking. Each call relies on either a `FakeUnaryResponse` or a `FakeStreamingResponse` -/// to get responses or errors. The fake response of each type should be registered with the channel -/// prior to making a call via `makeFakeUnaryResponse` or `makeFakeStreamingResponse` respectively. -/// -/// Users will typically not be required to interact with the channel directly, instead they should -/// do so via a generated test client. -@available( - swift, - deprecated: 5.6, - message: - "GRPCChannel implementations must be Sendable but this implementation is not. Using a client and server on localhost is the recommended alternative." -) -public class FakeChannel: GRPCChannel { - /// Fake response streams keyed by their path. - private var responseStreams: [String: CircularBuffer] - - /// A logger. - public let logger: Logger - - public init( - logger: Logger = Logger( - label: "io.grpc", - factory: { _ in - SwiftLogNoOpLogHandler() - } - ) - ) { - self.responseStreams = [:] - self.logger = logger - } - - /// Make and store a fake unary response for the given path. Users should prefer making a response - /// stream for their RPC directly via the appropriate method on their generated test client. - public func makeFakeUnaryResponse( - path: String, - requestHandler: @escaping (FakeRequestPart) -> Void - ) -> FakeUnaryResponse { - let proxy = FakeUnaryResponse(requestHandler: requestHandler) - self.responseStreams[path, default: []].append(proxy) - return proxy - } - - /// Make and store a fake streaming response for the given path. Users should prefer making a - /// response stream for their RPC directly via the appropriate method on their generated test - /// client. - public func makeFakeStreamingResponse( - path: String, - requestHandler: @escaping (FakeRequestPart) -> Void - ) -> FakeStreamingResponse { - let proxy = FakeStreamingResponse(requestHandler: requestHandler) - self.responseStreams[path, default: []].append(proxy) - return proxy - } - - /// Returns true if there are fake responses enqueued for the given path. - public func hasFakeResponseEnqueued(forPath path: String) -> Bool { - guard let noStreamsForPath = self.responseStreams[path]?.isEmpty else { - return false - } - return !noStreamsForPath - } - - public func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - return self._makeCall( - path: path, - type: type, - callOptions: callOptions, - interceptors: interceptors - ) - } - - public func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - return self._makeCall( - path: path, - type: type, - callOptions: callOptions, - interceptors: interceptors - ) - } - - private func _makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - let stream: _FakeResponseStream? = self.dequeueResponseStream(forPath: path) - let eventLoop = stream?.channel.eventLoop ?? EmbeddedEventLoop() - return Call( - path: path, - type: type, - eventLoop: eventLoop, - options: callOptions, - interceptors: interceptors, - transportFactory: .fake(stream) - ) - } - - private func _makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - let stream: _FakeResponseStream? = self.dequeueResponseStream(forPath: path) - let eventLoop = stream?.channel.eventLoop ?? EmbeddedEventLoop() - return Call( - path: path, - type: type, - eventLoop: eventLoop, - options: callOptions, - interceptors: interceptors, - transportFactory: .fake(stream) - ) - } - - public func close() -> EventLoopFuture { - // We don't have anything to close. - return EmbeddedEventLoop().makeSucceededFuture(()) - } -} - -@available(swift, deprecated: 5.6) -extension FakeChannel { - /// Dequeue a proxy for the given path and casts it to the given type, if one exists. - private func dequeueResponseStream( - forPath path: String, - as: Stream.Type = Stream.self - ) -> Stream? { - guard var streams = self.responseStreams[path], !streams.isEmpty else { - return nil - } - - // This is fine: we know we're non-empty. - let first = streams.removeFirst() - self.responseStreams.updateValue(streams, forKey: path) - - return first as? Stream - } - - private func makeRequestHead(path: String, callOptions: CallOptions) -> _GRPCRequestHead { - return _GRPCRequestHead( - scheme: "http", - path: path, - host: "localhost", - options: callOptions, - requestID: nil - ) - } -} diff --git a/Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift b/Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift deleted file mode 100644 index b2e81e4b0..000000000 --- a/Sources/GRPC/GRPCChannel/ClientConnection+NIOSSL.swift +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOCore -import NIOSSL - -extension ClientConnection { - /// Returns a `ClientConnection` builder configured with TLS. - @available( - *, - deprecated, - message: - "Use one of 'usingPlatformAppropriateTLS(for:)', 'usingTLSBackedByNIOSSL(on:)' or 'usingTLSBackedByNetworkFramework(on:)' or 'usingTLS(on:with:)'" - ) - public static func secure(group: EventLoopGroup) -> ClientConnection.Builder.Secure { - return ClientConnection.usingTLSBackedByNIOSSL(on: group) - } - - /// Returns a `ClientConnection` builder configured with the 'NIOSSL' TLS backend. - /// - /// This builder may use either a `MultiThreadedEventLoopGroup` or a `NIOTSEventLoopGroup` (or an - /// `EventLoop` from either group). - /// - /// - Parameter group: The `EventLoopGroup` use for the connection. - /// - Returns: A builder for a connection using the NIOSSL TLS backend. - public static func usingTLSBackedByNIOSSL( - on group: EventLoopGroup - ) -> ClientConnection.Builder.Secure { - return Builder.Secure(group: group, tlsConfiguration: .makeClientConfigurationBackedByNIOSSL()) - } -} - -// MARK: - NIOSSL TLS backend options - -extension ClientConnection.Builder.Secure { - /// Sets the sources of certificates to offer during negotiation. No certificates are offered - /// during negotiation by default. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLS(certificateChain: [NIOSSLCertificate]) -> Self { - self.tls.updateNIOCertificateChain(to: certificateChain) - return self - } - - /// Sets the private key associated with the leaf certificate. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLS(privateKey: NIOSSLPrivateKey) -> Self { - self.tls.updateNIOPrivateKey(to: privateKey) - return self - } - - /// Sets the trust roots to use to validate certificates. This only needs to be provided if you - /// intend to validate certificates. Defaults to the system provided trust store (`.default`) if - /// not set. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLS(trustRoots: NIOSSLTrustRoots) -> Self { - self.tls.updateNIOTrustRoots(to: trustRoots) - return self - } - - /// Whether to verify remote certificates. Defaults to `.fullVerification` if not otherwise - /// configured. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLS(certificateVerification: CertificateVerification) -> Self { - self.tls.updateNIOCertificateVerification(to: certificateVerification) - return self - } - - /// A custom verification callback that allows completely overriding the certificate verification logic. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLSCustomVerificationCallback( - _ callback: @escaping NIOSSLCustomVerificationCallback - ) -> Self { - self.tls.updateNIOCustomVerificationCallback(to: callback) - return self - } -} - -#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift b/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift deleted file mode 100644 index be35c43e6..000000000 --- a/Sources/GRPC/GRPCChannel/ClientConnection+NWTLS.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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. - */ -#if canImport(Security) -#if canImport(Network) -import NIOCore -import Security - -extension ClientConnection { - /// Returns a ``ClientConnection`` builder configured with the Network.framework TLS backend. - /// - /// This builder must use a `NIOTSEventLoopGroup` (or an `EventLoop` from a - /// `NIOTSEventLoopGroup`). - /// - /// - Parameter group: The `EventLoopGroup` use for the connection. - /// - Returns: A builder for a connection using the Network.framework TLS backend. - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public static func usingTLSBackedByNetworkFramework( - on group: EventLoopGroup - ) -> ClientConnection.Builder.Secure { - precondition( - PlatformSupport.isTransportServicesEventLoopGroup(group), - "'\(#function)' requires 'group' to be a 'NIOTransportServices.NIOTSEventLoopGroup' or 'NIOTransportServices.QoSEventLoop' (but was '\(type(of: group))'" - ) - return Builder.Secure( - group: group, - tlsConfiguration: .makeClientConfigurationBackedByNetworkFramework() - ) - } -} - -extension ClientConnection.Builder.Secure { - /// Update the local identity. - /// - /// - Note: May only be used with the 'Network.framework' TLS backend. - @discardableResult - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public func withTLS(localIdentity: SecIdentity) -> Self { - self.tls.updateNetworkLocalIdentity(to: localIdentity) - return self - } - - /// Update the callback used to verify a trust object during a TLS handshake. - /// - /// - Note: May only be used with the 'Network.framework' TLS backend. - @discardableResult - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public func withTLSHandshakeVerificationCallback( - on queue: DispatchQueue, - verificationCallback callback: @escaping sec_protocol_verify_t - ) -> Self { - self.tls.updateNetworkVerifyCallbackWithQueue(callback: callback, queue: queue) - return self - } -} - -#endif // canImport(Network) -#endif // canImport(Security) diff --git a/Sources/GRPC/GRPCChannel/GRPCChannel.swift b/Sources/GRPC/GRPCChannel/GRPCChannel.swift deleted file mode 100644 index b34ef5c12..000000000 --- a/Sources/GRPC/GRPCChannel/GRPCChannel.swift +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOHTTP2 -import SwiftProtobuf - -public protocol GRPCChannel: GRPCPreconcurrencySendable { - /// Makes a gRPC call on the channel with requests and responses conforming to - /// `SwiftProtobuf.Message`. - /// - /// Note: this is a lower-level construct that any of ``UnaryCall``, ``ClientStreamingCall``, - /// ``ServerStreamingCall`` or ``BidirectionalStreamingCall`` and does not have an API to protect - /// users against protocol violations (such as sending to requests on a unary call). - /// - /// After making the ``Call``, users must ``Call/invoke(onError:onResponsePart:)`` the call with a callback which is invoked - /// for each response part (or error) received. Any call to ``Call/send(_:)`` prior to calling - /// ``Call/invoke(onError:onResponsePart:)`` will fail and not be sent. Users are also responsible for closing the request stream - /// by sending the `.end` request part. - /// - /// - Parameters: - /// - path: The path of the RPC, e.g. "/echo.Echo/get". - /// - type: The type of the RPC, e.g. ``GRPCCallType/unary``. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call - - /// Makes a gRPC call on the channel with requests and responses conforming to ``GRPCPayload``. - /// - /// Note: this is a lower-level construct that any of ``UnaryCall``, ``ClientStreamingCall``, - /// ``ServerStreamingCall`` or ``BidirectionalStreamingCall`` and does not have an API to protect - /// users against protocol violations (such as sending to requests on a unary call). - /// - /// After making the ``Call``, users must ``Call/invoke(onError:onResponsePart:)`` the call with a callback which is invoked - /// for each response part (or error) received. Any call to ``Call/send(_:)`` prior to calling - /// ``Call/invoke(onError:onResponsePart:)`` will fail and not be sent. Users are also responsible for closing the request stream - /// by sending the `.end` request part. - /// - /// - Parameters: - /// - path: The path of the RPC, e.g. "/echo.Echo/get". - /// - type: The type of the RPC, e.g. ``GRPCCallType/unary``. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call - - /// Close the channel, and any connections associated with it. Any ongoing RPCs may fail. - /// - /// - Returns: Returns a future which will be resolved when shutdown has completed. - func close() -> EventLoopFuture - - /// Close the channel, and any connections associated with it. Any ongoing RPCs may fail. - /// - /// - Parameter promise: A promise which will be completed when shutdown has completed. - func close(promise: EventLoopPromise) - - /// Attempt to gracefully shutdown the channel. New RPCs will be failed immediately and existing - /// RPCs may continue to run until they complete. - /// - /// - Parameters: - /// - deadline: A point in time by which the graceful shutdown must have completed. If the - /// deadline passes and RPCs are still active then the connection will be closed forcefully - /// and any remaining in-flight RPCs may be failed. - /// - promise: A promise which will be completed when shutdown has completed. - func closeGracefully(deadline: NIODeadline, promise: EventLoopPromise) -} - -// Default implementations to avoid breaking API. Implementations provided by GRPC override these. -extension GRPCChannel { - public func close(promise: EventLoopPromise) { - promise.completeWith(self.close()) - } - - public func closeGracefully(deadline: NIODeadline, promise: EventLoopPromise) { - promise.completeWith(self.close()) - } -} - -extension GRPCChannel { - /// Make a unary gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - public func makeUnaryCall( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> UnaryCall { - let unary: UnaryCall = UnaryCall( - call: self.makeCall( - path: path, - type: .unary, - callOptions: callOptions, - interceptors: interceptors - ) - ) - unary.invoke(request) - return unary - } - - /// Make a unary gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - public func makeUnaryCall( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> UnaryCall { - let rpc: UnaryCall = UnaryCall( - call: self.makeCall( - path: path, - type: .unary, - callOptions: callOptions, - interceptors: interceptors - ) - ) - rpc.invoke(request) - return rpc - } - - /// Makes a client-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - public func makeClientStreamingCall( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> ClientStreamingCall { - let rpc: ClientStreamingCall = ClientStreamingCall( - call: self.makeCall( - path: path, - type: .clientStreaming, - callOptions: callOptions, - interceptors: interceptors - ) - ) - rpc.invoke() - return rpc - } - - /// Makes a client-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - public func makeClientStreamingCall( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [] - ) -> ClientStreamingCall { - let rpc: ClientStreamingCall = ClientStreamingCall( - call: self.makeCall( - path: path, - type: .clientStreaming, - callOptions: callOptions, - interceptors: interceptors - ) - ) - rpc.invoke() - return rpc - } - - /// Make a server-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - /// - handler: Response handler; called for every response received from the server. - public func makeServerStreamingCall( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [], - handler: @escaping (Response) -> Void - ) -> ServerStreamingCall { - let rpc: ServerStreamingCall = ServerStreamingCall( - call: self.makeCall( - path: path, - type: .serverStreaming, - callOptions: callOptions, - interceptors: interceptors - ), - callback: handler - ) - rpc.invoke(request) - return rpc - } - - /// Make a server-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - request: The request to send. - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - /// - handler: Response handler; called for every response received from the server. - public func makeServerStreamingCall( - path: String, - request: Request, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [], - handler: @escaping (Response) -> Void - ) -> ServerStreamingCall { - let rpc: ServerStreamingCall = ServerStreamingCall( - call: self.makeCall( - path: path, - type: .serverStreaming, - callOptions: callOptions, - interceptors: [] - ), - callback: handler - ) - rpc.invoke(request) - return rpc - } - - /// Makes a bidirectional-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - /// - handler: Response handler; called for every response received from the server. - public func makeBidirectionalStreamingCall( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [], - handler: @escaping (Response) -> Void - ) -> BidirectionalStreamingCall { - let rpc: BidirectionalStreamingCall = BidirectionalStreamingCall( - call: self.makeCall( - path: path, - type: .bidirectionalStreaming, - callOptions: callOptions, - interceptors: interceptors - ), - callback: handler - ) - rpc.invoke() - return rpc - } - - /// Makes a bidirectional-streaming gRPC call. - /// - /// - Parameters: - /// - path: Path of the RPC, e.g. "/echo.Echo/Get" - /// - callOptions: Options for the RPC. - /// - interceptors: A list of interceptors to intercept the request and response stream with. - /// - handler: Response handler; called for every response received from the server. - public func makeBidirectionalStreamingCall( - path: String, - callOptions: CallOptions, - interceptors: [ClientInterceptor] = [], - handler: @escaping (Response) -> Void - ) -> BidirectionalStreamingCall { - let rpc: BidirectionalStreamingCall = BidirectionalStreamingCall( - call: self.makeCall( - path: path, - type: .bidirectionalStreaming, - callOptions: callOptions, - interceptors: interceptors - ), - callback: handler - ) - rpc.invoke() - return rpc - } -} diff --git a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift b/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift deleted file mode 100644 index 67ea2619b..000000000 --- a/Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2020, 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 Dispatch -import Logging -import NIOCore - -extension ClientConnection { - /// Returns an insecure ``ClientConnection`` builder which is *not configured with TLS*. - public static func insecure(group: EventLoopGroup) -> ClientConnection.Builder { - return Builder(group: group) - } - - /// Returns a ``ClientConnection`` builder configured with a TLS backend appropriate for the - /// given `EventLoopGroup`. - /// - /// gRPC Swift offers two TLS 'backends'. The 'NIOSSL' backend is available on Darwin and Linux - /// platforms and delegates to SwiftNIO SSL. On recent Darwin platforms (macOS 10.14+, iOS 12+, - /// tvOS 12+, and watchOS 6+) the 'Network.framework' backend is available. The two backends have - /// a number of incompatible configuration options and users are responsible for selecting the - /// appropriate APIs. The TLS configuration options on the builder document which backends they - /// support. - /// - /// TLS backends must also be used with an appropriate `EventLoopGroup` implementation. The - /// 'NIOSSL' backend may be used either a `MultiThreadedEventLoopGroup` or a - /// `NIOTSEventLoopGroup`. The 'Network.framework' backend may only be used with a - /// `NIOTSEventLoopGroup`. - /// - /// This function returns a builder using the `NIOSSL` backend if a `MultiThreadedEventLoopGroup` - /// is supplied and a 'Network.framework' backend if a `NIOTSEventLoopGroup` is used. - public static func usingPlatformAppropriateTLS( - for group: EventLoopGroup - ) -> ClientConnection.Builder.Secure { - let networkPreference = NetworkPreference.userDefined(.matchingEventLoopGroup(group)) - return Builder.Secure( - group: group, - tlsConfiguration: .makeClientDefault(for: networkPreference) - ) - } - - /// Returns a ``ClientConnection`` builder configured with the TLS backend appropriate for the - /// provided configuration and `EventLoopGroup`. - /// - /// - Important: The caller is responsible for ensuring the provided `configuration` may be used - /// the the `group`. - public static func usingTLS( - with configuration: GRPCTLSConfiguration, - on group: EventLoopGroup - ) -> ClientConnection.Builder.Secure { - return Builder.Secure(group: group, tlsConfiguration: configuration) - } -} - -extension ClientConnection { - public class Builder { - private var configuration: ClientConnection.Configuration - private var maybeTLS: GRPCTLSConfiguration? { return nil } - - private var connectionBackoff = ConnectionBackoff() - private var connectionBackoffIsEnabled = true - - fileprivate init(group: EventLoopGroup) { - // This is okay: the configuration is only consumed on a call to `connect` which sets the host - // and port. - self.configuration = .default(target: .hostAndPort("", .max), eventLoopGroup: group) - } - - public func connect(host: String, port: Int) -> ClientConnection { - // Finish setting up the configuration. - self.configuration.target = .hostAndPort(host, port) - self.configuration.connectionBackoff = - self.connectionBackoffIsEnabled ? self.connectionBackoff : nil - self.configuration.tlsConfiguration = self.maybeTLS - return ClientConnection(configuration: self.configuration) - } - - public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> ClientConnection { - precondition( - !PlatformSupport.isTransportServicesEventLoopGroup(self.configuration.eventLoopGroup), - "'\(#function)' requires 'group' to not be a 'NIOTransportServices.NIOTSEventLoopGroup' or 'NIOTransportServices.QoSEventLoop' (but was '\(type(of: self.configuration.eventLoopGroup))'" - ) - self.configuration.target = .connectedSocket(socket) - self.configuration.connectionBackoff = - self.connectionBackoffIsEnabled ? self.connectionBackoff : nil - self.configuration.tlsConfiguration = self.maybeTLS - return ClientConnection(configuration: self.configuration) - } - } -} - -extension ClientConnection.Builder { - public class Secure: ClientConnection.Builder { - internal var tls: GRPCTLSConfiguration - override internal var maybeTLS: GRPCTLSConfiguration? { - return self.tls - } - - internal init(group: EventLoopGroup, tlsConfiguration: GRPCTLSConfiguration) { - group.preconditionCompatible(with: tlsConfiguration) - self.tls = tlsConfiguration - super.init(group: group) - } - - /// Connect to `host` on port 443. - public func connect(host: String) -> ClientConnection { - return self.connect(host: host, port: 443) - } - } -} - -extension ClientConnection.Builder { - /// Sets the initial connection backoff. That is, the initial time to wait before re-attempting to - /// establish a connection. Jitter will *not* be applied to the initial backoff. Defaults to - /// 1 second if not set. - @discardableResult - public func withConnectionBackoff(initial amount: TimeAmount) -> Self { - self.connectionBackoff.initialBackoff = .seconds(from: amount) - return self - } - - /// Set the maximum connection backoff. That is, the maximum amount of time to wait before - /// re-attempting to establish a connection. Note that this time amount represents the maximum - /// backoff *before* jitter is applied. Defaults to 120 seconds if not set. - @discardableResult - public func withConnectionBackoff(maximum amount: TimeAmount) -> Self { - self.connectionBackoff.maximumBackoff = .seconds(from: amount) - return self - } - - /// Backoff is 'jittered' to randomise the amount of time to wait before re-attempting to - /// establish a connection. The jittered backoff will be no more than `jitter ⨯ unjitteredBackoff` - /// from `unjitteredBackoff`. Defaults to 0.2 if not set. - /// - /// - Precondition: `0 <= jitter <= 1` - @discardableResult - public func withConnectionBackoff(jitter: Double) -> Self { - self.connectionBackoff.jitter = jitter - return self - } - - /// The multiplier for scaling the unjittered backoff between attempts to establish a connection. - /// Defaults to 1.6 if not set. - @discardableResult - public func withConnectionBackoff(multiplier: Double) -> Self { - self.connectionBackoff.multiplier = multiplier - return self - } - - /// The minimum timeout to use when attempting to establishing a connection. The connection - /// timeout for each attempt is the larger of the jittered backoff and the minimum connection - /// timeout. Defaults to 20 seconds if not set. - @discardableResult - public func withConnectionTimeout(minimum amount: TimeAmount) -> Self { - self.connectionBackoff.minimumConnectionTimeout = .seconds(from: amount) - return self - } - - /// Sets the initial and maximum backoff to given amount. Disables jitter and sets the backoff - /// multiplier to 1.0. - @discardableResult - public func withConnectionBackoff(fixed amount: TimeAmount) -> Self { - let seconds = Double.seconds(from: amount) - self.connectionBackoff.initialBackoff = seconds - self.connectionBackoff.maximumBackoff = seconds - self.connectionBackoff.multiplier = 1.0 - self.connectionBackoff.jitter = 0.0 - return self - } - - /// Sets the limit on the number of times to attempt to re-establish a connection. Defaults - /// to `.unlimited` if not set. - @discardableResult - public func withConnectionBackoff(retries: ConnectionBackoff.Retries) -> Self { - self.connectionBackoff.retries = retries - return self - } - - /// Sets whether the connection should be re-established automatically if it is dropped. Defaults - /// to `true` if not set. - @discardableResult - public func withConnectionReestablishment(enabled: Bool) -> Self { - self.connectionBackoffIsEnabled = enabled - return self - } - - /// Sets a custom configuration for keepalive - /// The defaults for client and server are determined by the gRPC keepalive - /// [documentation] (https://github.com/grpc/grpc/blob/master/doc/keepalive.md). - @discardableResult - public func withKeepalive(_ keepalive: ClientConnectionKeepalive) -> Self { - self.configuration.connectionKeepalive = keepalive - return self - } - - /// The amount of time to wait before closing the connection. The idle timeout will start only - /// if there are no RPCs in progress and will be cancelled as soon as any RPCs start. If a - /// connection becomes idle, starting a new RPC will automatically create a new connection. - /// Defaults to 30 minutes if not set. - @discardableResult - public func withConnectionIdleTimeout(_ timeout: TimeAmount) -> Self { - self.configuration.connectionIdleTimeout = timeout - return self - } - - /// The behavior used to determine when an RPC should start. That is, whether it should wait for - /// an active connection or fail quickly if no connection is currently available. Calls will - /// use `.waitsForConnectivity` by default. - @discardableResult - public func withCallStartBehavior(_ behavior: CallStartBehavior) -> Self { - self.configuration.callStartBehavior = behavior - return self - } -} - -extension ClientConnection.Builder { - /// Sets the client error delegate. - @discardableResult - public func withErrorDelegate(_ delegate: ClientErrorDelegate?) -> Self { - self.configuration.errorDelegate = delegate - return self - } -} - -extension ClientConnection.Builder { - /// Sets the client connectivity state delegate and the `DispatchQueue` on which the delegate - /// should be called. If no `queue` is provided then gRPC will create a `DispatchQueue` on which - /// to run the delegate. - @discardableResult - public func withConnectivityStateDelegate( - _ delegate: ConnectivityStateDelegate?, - executingOn queue: DispatchQueue? = nil - ) -> Self { - self.configuration.connectivityStateDelegate = delegate - self.configuration.connectivityStateDelegateQueue = queue - return self - } -} - -// MARK: - Common TLS options - -extension ClientConnection.Builder.Secure { - /// Sets a server hostname override to be used for the TLS Server Name Indication (SNI) extension. - /// The hostname from `connect(host:port)` is for TLS SNI if this value is not set and hostname - /// verification is enabled. - /// - /// - Note: May be used with the 'NIOSSL' and 'Network.framework' TLS backend. - /// - Note: `serverHostnameOverride` may not be `nil` when using the 'Network.framework' backend. - @discardableResult - public func withTLS(serverHostnameOverride: String?) -> Self { - self.tls.hostnameOverride = serverHostnameOverride - return self - } -} - -extension ClientConnection.Builder { - /// Sets the HTTP/2 flow control target window size. Defaults to 8MB if not explicitly set. - /// Values are clamped between 1 and 2^31-1 inclusive. - @discardableResult - public func withHTTPTargetWindowSize(_ httpTargetWindowSize: Int) -> Self { - self.configuration.httpTargetWindowSize = httpTargetWindowSize - return self - } - - /// Sets the maximum size of an HTTP/2 frame in bytes which the client is willing to receive from - /// the server. Defaults to 16384. Value are clamped between 2^14 and 2^24-1 octets inclusive - /// (the minimum and maximum permitted values per RFC 7540 § 4.2). - /// - /// Raising this value may lower CPU usage for large message at the cost of increasing head of - /// line blocking for small messages. - @discardableResult - public func withHTTPMaxFrameSize(_ httpMaxFrameSize: Int) -> Self { - self.configuration.httpMaxFrameSize = httpMaxFrameSize - return self - } -} - -extension ClientConnection.Builder { - /// Sets the maximum message size the client is permitted to receive in bytes. - /// - /// - Precondition: `limit` must not be negative. - @discardableResult - public func withMaximumReceiveMessageLength(_ limit: Int) -> Self { - self.configuration.maximumReceiveMessageLength = limit - return self - } -} - -extension ClientConnection.Builder { - /// Sets a logger to be used for background activity such as connection state changes. Defaults - /// to a no-op logger if not explicitly set. - /// - /// Note that individual RPCs will use the logger from `CallOptions`, not the logger specified - /// here. - @discardableResult - public func withBackgroundActivityLogger(_ logger: Logger) -> Self { - self.configuration.backgroundActivityLogger = logger - return self - } -} - -extension ClientConnection.Builder { - /// A channel initializer which will be run after gRPC has initialized each channel. This may be - /// used to add additional handlers to the pipeline and is intended for debugging. - /// - /// - Warning: The initializer closure may be invoked *multiple times*. - @discardableResult - @preconcurrency - public func withDebugChannelInitializer( - _ debugChannelInitializer: @Sendable @escaping (Channel) -> EventLoopFuture - ) -> Self { - self.configuration.debugChannelInitializer = debugChannelInitializer - return self - } -} - -extension Double { - fileprivate static func seconds(from amount: TimeAmount) -> Double { - return Double(amount.nanoseconds) / 1_000_000_000 - } -} diff --git a/Sources/GRPC/GRPCClient.swift b/Sources/GRPC/GRPCClient.swift deleted file mode 100644 index 783c3f842..000000000 --- a/Sources/GRPC/GRPCClient.swift +++ /dev/null @@ -1,236 +0,0 @@ -/* - * 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 NIOConcurrencyHelpers -import NIOCore -import NIOHTTP2 -import SwiftProtobuf - -/// A gRPC client. -public protocol GRPCClient: GRPCPreconcurrencySendable { - /// The gRPC channel over which RPCs are sent and received. Note that this is distinct - /// from `NIO.Channel`. - var channel: GRPCChannel { get } - - /// The call options to use should the user not provide per-call options. - var defaultCallOptions: CallOptions { get set } -} - -// MARK: Convenience methods - -extension GRPCClient { - public func makeUnaryCall( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> UnaryCall { - return self.channel.makeUnaryCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeUnaryCall( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self - ) -> UnaryCall { - return self.channel.makeUnaryCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeServerStreamingCall< - Request: SwiftProtobuf.Message, - Response: SwiftProtobuf.Message - >( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self, - handler: @escaping (Response) -> Void - ) -> ServerStreamingCall { - return self.channel.makeServerStreamingCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors, - handler: handler - ) - } - - public func makeServerStreamingCall( - path: String, - request: Request, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - responseType: Response.Type = Response.self, - handler: @escaping (Response) -> Void - ) -> ServerStreamingCall { - return self.channel.makeServerStreamingCall( - path: path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors, - handler: handler - ) - } - - public func makeClientStreamingCall< - Request: SwiftProtobuf.Message, - Response: SwiftProtobuf.Message - >( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> ClientStreamingCall { - return self.channel.makeClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeClientStreamingCall( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> ClientStreamingCall { - return self.channel.makeClientStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors - ) - } - - public func makeBidirectionalStreamingCall< - Request: SwiftProtobuf.Message, - Response: SwiftProtobuf.Message - >( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self, - handler: @escaping (Response) -> Void - ) -> BidirectionalStreamingCall { - return self.channel.makeBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors, - handler: handler - ) - } - - public func makeBidirectionalStreamingCall( - path: String, - callOptions: CallOptions? = nil, - interceptors: [ClientInterceptor] = [], - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self, - handler: @escaping (Response) -> Void - ) -> BidirectionalStreamingCall { - return self.channel.makeBidirectionalStreamingCall( - path: path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: interceptors, - handler: handler - ) - } -} - -/// A client which has no generated stubs and may be used to create gRPC calls manually. -/// See ``GRPCClient`` for details. -/// -/// Example: -/// -/// ``` -/// let client = AnyServiceClient(channel: channel) -/// let rpc: UnaryCall = client.makeUnaryCall( -/// path: "/serviceName/methodName", -/// request: .with { ... }, -/// } -/// ``` -@available(*, deprecated, renamed: "GRPCAnyServiceClient") -public final class AnyServiceClient: GRPCClient { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - - /// The gRPC channel over which RPCs are sent and received. - public let channel: GRPCChannel - - /// The default options passed to each RPC unless passed for each RPC. - public var defaultCallOptions: CallOptions { - get { return self.lock.withLock { self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - - /// Creates a client which may be used to call any service. - /// - /// - Parameters: - /// - connection: ``ClientConnection`` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - public init(channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions()) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - } -} - -// Unchecked because mutable state is protected by a lock. -@available(*, deprecated, renamed: "GRPCAnyServiceClient") -extension AnyServiceClient: @unchecked Sendable {} - -/// A client which has no generated stubs and may be used to create gRPC calls manually. -/// See ``GRPCClient`` for details. -/// -/// Example: -/// -/// ``` -/// let client = GRPCAnyServiceClient(channel: channel) -/// let rpc: UnaryCall = client.makeUnaryCall( -/// path: "/serviceName/methodName", -/// request: .with { ... }, -/// } -/// ``` -public struct GRPCAnyServiceClient: GRPCClient { - public let channel: GRPCChannel - - /// The default options passed to each RPC unless passed for each RPC. - public var defaultCallOptions: CallOptions - - /// Creates a client which may be used to call any service. - /// - /// - Parameters: - /// - connection: ``ClientConnection`` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - public init(channel: GRPCChannel, defaultCallOptions: CallOptions = CallOptions()) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - } -} diff --git a/Sources/GRPC/GRPCClientChannelHandler.swift b/Sources/GRPC/GRPCClientChannelHandler.swift deleted file mode 100644 index 4fbc2ed9b..000000000 --- a/Sources/GRPC/GRPCClientChannelHandler.swift +++ /dev/null @@ -1,658 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import SwiftProtobuf - -/// A gRPC client request message part. -/// -/// - Important: This is **NOT** part of the public API. It is declared as -/// `public` because it is used within performance tests. -public enum _GRPCClientRequestPart { - /// The 'head' of the request, that is, information about the initiation of the RPC. - case head(_GRPCRequestHead) - - /// A deserialized request message to send to the server. - case message(_MessageContext) - - /// Indicates that the client does not intend to send any further messages. - case end -} - -/// As `_GRPCClientRequestPart` but messages are serialized. -/// - Important: This is **NOT** part of the public API. -public typealias _RawGRPCClientRequestPart = _GRPCClientRequestPart - -/// A gRPC client response message part. -/// -/// - Important: This is **NOT** part of the public API. -public enum _GRPCClientResponsePart { - /// Metadata received as the server acknowledges the RPC. - case initialMetadata(HPACKHeaders) - - /// A deserialized response message received from the server. - case message(_MessageContext) - - /// The metadata received at the end of the RPC. - case trailingMetadata(HPACKHeaders) - - /// The final status of the RPC. - case status(GRPCStatus) -} - -/// As `_GRPCClientResponsePart` but messages are serialized. -/// - Important: This is **NOT** part of the public API. -public typealias _RawGRPCClientResponsePart = _GRPCClientResponsePart - -/// - Important: This is **NOT** part of the public API. It is declared as -/// `public` because it is used within performance tests. -public struct _GRPCRequestHead { - private final class _Storage { - var method: String - var scheme: String - var path: String - var host: String - var deadline: NIODeadline - var encoding: ClientMessageEncoding - - init( - method: String, - scheme: String, - path: String, - host: String, - deadline: NIODeadline, - encoding: ClientMessageEncoding - ) { - self.method = method - self.scheme = scheme - self.path = path - self.host = host - self.deadline = deadline - self.encoding = encoding - } - - func copy() -> _Storage { - return .init( - method: self.method, - scheme: self.scheme, - path: self.path, - host: self.host, - deadline: self.deadline, - encoding: self.encoding - ) - } - } - - private var _storage: _Storage - // Don't put this in storage: it would CoW for every mutation. - internal var customMetadata: HPACKHeaders - - internal var method: String { - get { - return self._storage.method - } - set { - if !isKnownUniquelyReferenced(&self._storage) { - self._storage = self._storage.copy() - } - self._storage.method = newValue - } - } - - internal var scheme: String { - get { - return self._storage.scheme - } - set { - if !isKnownUniquelyReferenced(&self._storage) { - self._storage = self._storage.copy() - } - self._storage.scheme = newValue - } - } - - internal var path: String { - get { - return self._storage.path - } - set { - if !isKnownUniquelyReferenced(&self._storage) { - self._storage = self._storage.copy() - } - self._storage.path = newValue - } - } - - internal var host: String { - get { - return self._storage.host - } - set { - if !isKnownUniquelyReferenced(&self._storage) { - self._storage = self._storage.copy() - } - self._storage.host = newValue - } - } - - internal var deadline: NIODeadline { - get { - return self._storage.deadline - } - set { - if !isKnownUniquelyReferenced(&self._storage) { - self._storage = self._storage.copy() - } - self._storage.deadline = newValue - } - } - - internal var encoding: ClientMessageEncoding { - get { - return self._storage.encoding - } - set { - if !isKnownUniquelyReferenced(&self._storage) { - self._storage = self._storage.copy() - } - self._storage.encoding = newValue - } - } - - public init( - method: String, - scheme: String, - path: String, - host: String, - deadline: NIODeadline, - customMetadata: HPACKHeaders, - encoding: ClientMessageEncoding - ) { - self._storage = .init( - method: method, - scheme: scheme, - path: path, - host: host, - deadline: deadline, - encoding: encoding - ) - self.customMetadata = customMetadata - } -} - -extension _GRPCRequestHead { - internal init( - scheme: String, - path: String, - host: String, - options: CallOptions, - requestID: String? - ) { - let metadata: HPACKHeaders - if let requestID = requestID, let requestIDHeader = options.requestIDHeader { - var customMetadata = options.customMetadata - customMetadata.add(name: requestIDHeader, value: requestID) - metadata = customMetadata - } else { - metadata = options.customMetadata - } - - self = _GRPCRequestHead( - method: options.cacheable ? "GET" : "POST", - scheme: scheme, - path: path, - host: host, - deadline: options.timeLimit.makeDeadline(), - customMetadata: metadata, - encoding: options.messageEncoding - ) - } -} - -/// The type of gRPC call. -public enum GRPCCallType: Hashable, Sendable { - /// Unary: a single request and a single response. - case unary - - /// Client streaming: many requests and a single response. - case clientStreaming - - /// Server streaming: a single request and many responses. - case serverStreaming - - /// Bidirectional streaming: many request and many responses. - case bidirectionalStreaming - - public var isStreamingRequests: Bool { - switch self { - case .clientStreaming, .bidirectionalStreaming: - return true - case .unary, .serverStreaming: - return false - } - } - - public var isStreamingResponses: Bool { - switch self { - case .serverStreaming, .bidirectionalStreaming: - return true - case .unary, .clientStreaming: - return false - } - } -} - -// MARK: - GRPCClientChannelHandler - -/// A channel handler for gRPC clients which translates HTTP/2 frames into gRPC messages. -/// -/// This channel handler should typically be used in conjunction with another handler which -/// reads the parsed `GRPCClientResponsePart` messages and surfaces them to the caller -/// in some fashion. Note that for unary and client streaming RPCs this handler will only emit at -/// most one response message. -/// -/// This handler relies heavily on the `GRPCClientStateMachine` to manage the state of the request -/// and response streams, which share a single HTTP/2 stream for transport. -/// -/// Typical usage of this handler is with a `HTTP2StreamMultiplexer` from SwiftNIO HTTP2: -/// -/// ``` -/// let multiplexer: HTTP2StreamMultiplexer = // ... -/// multiplexer.createStreamChannel(promise: nil) { (channel, streamID) in -/// let clientChannelHandler = GRPCClientChannelHandler( -/// streamID: streamID, -/// callType: callType, -/// logger: logger -/// ) -/// return channel.pipeline.addHandler(clientChannelHandler) -/// } -/// ``` -internal final class GRPCClientChannelHandler { - private let logger: Logger - private var stateMachine: GRPCClientStateMachine - private let maximumReceiveMessageLength: Int - - /// Creates a new gRPC channel handler for clients to translate HTTP/2 frames to gRPC messages. - /// - /// - Parameters: - /// - callType: Type of RPC call being made. - /// - maximumReceiveMessageLength: Maximum allowed length in bytes of a received message. - /// - logger: Logger. - internal init( - callType: GRPCCallType, - maximumReceiveMessageLength: Int, - logger: Logger - ) { - self.logger = logger - self.maximumReceiveMessageLength = maximumReceiveMessageLength - switch callType { - case .unary: - self.stateMachine = .init(requestArity: .one, responseArity: .one) - case .clientStreaming: - self.stateMachine = .init(requestArity: .many, responseArity: .one) - case .serverStreaming: - self.stateMachine = .init(requestArity: .one, responseArity: .many) - case .bidirectionalStreaming: - self.stateMachine = .init(requestArity: .many, responseArity: .many) - } - } -} - -// MARK: - GRPCClientChannelHandler: Inbound - -extension GRPCClientChannelHandler: ChannelInboundHandler { - internal typealias InboundIn = HTTP2Frame.FramePayload - internal typealias InboundOut = _RawGRPCClientResponsePart - - internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let payload = self.unwrapInboundIn(data) - switch payload { - case let .headers(content): - self.readHeaders(content: content, context: context) - - case let .data(content): - self.readData(content: content, context: context) - - // We don't need to handle other frame type, just drop them instead. - default: - // TODO: synthesise a more precise `GRPCStatus` from RST_STREAM frames in accordance - // with: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#errors - break - } - } - - /// Read the content from an HTTP/2 HEADERS frame received from the server. - /// - /// We can receive headers in two cases: - /// - when the RPC is being acknowledged, and - /// - when the RPC is being terminated. - /// - /// It is also possible for the RPC to be acknowledged and terminated at the same time, the - /// specification refers to this as a "Trailers-Only" response. - /// - /// - Parameter content: Content of the headers frame. - /// - Parameter context: Channel handler context. - private func readHeaders( - content: HTTP2Frame.FramePayload.Headers, - context: ChannelHandlerContext - ) { - self.logger.trace( - "received HTTP2 frame", - metadata: [ - MetadataKey.h2Payload: "HEADERS", - MetadataKey.h2Headers: "\(content.headers)", - MetadataKey.h2EndStream: "\(content.endStream)", - ] - ) - - // In the case of a "Trailers-Only" response there's no guarantee that end-of-stream will be set - // on the headers frame: end stream may be sent on an empty data frame as well. If the headers - // contain a gRPC status code then they must be for a "Trailers-Only" response. - if content.endStream || content.headers.contains(name: GRPCHeaderName.statusCode) { - // We have the headers, pass them to the next handler: - context.fireChannelRead(self.wrapInboundOut(.trailingMetadata(content.headers))) - - // Are they valid headers? - let result = self.stateMachine.receiveEndOfResponseStream(content.headers) - .mapError { error -> GRPCError.WithContext in - // The headers aren't valid so let's figure out a reasonable error to forward: - switch error { - case let .invalidContentType(contentType): - return GRPCError.InvalidContentType(contentType).captureContext() - case let .invalidHTTPStatus(status): - return GRPCError.InvalidHTTPStatus(status).captureContext() - case .invalidState: - return GRPCError.InvalidState("parsing end-of-stream trailers").captureContext() - } - } - - // Okay, what should we tell the next handler? - switch result { - case let .success(status): - context.fireChannelRead(self.wrapInboundOut(.status(status))) - case let .failure(error): - context.fireErrorCaught(error) - } - } else { - // "Normal" response headers, but are they valid? - let result = self.stateMachine.receiveResponseHeaders(content.headers) - .mapError { error -> GRPCError.WithContext in - // The headers aren't valid so let's figure out a reasonable error to forward: - switch error { - case let .invalidContentType(contentType): - return GRPCError.InvalidContentType(contentType).captureContext() - case let .invalidHTTPStatus(status): - return GRPCError.InvalidHTTPStatus(status).captureContext() - case .unsupportedMessageEncoding: - return GRPCError.CompressionUnsupported().captureContext() - case .invalidState: - return GRPCError.InvalidState("parsing headers").captureContext() - } - } - - // Okay, what should we tell the next handler? - switch result { - case .success: - context.fireChannelRead(self.wrapInboundOut(.initialMetadata(content.headers))) - case let .failure(error): - context.fireErrorCaught(error) - } - } - } - - /// Reads the content from an HTTP/2 DATA frame received from the server and buffers the bytes - /// necessary to deserialize a message (or messages). - /// - /// - Parameter content: Content of the data frame. - /// - Parameter context: Channel handler context. - private func readData(content: HTTP2Frame.FramePayload.Data, context: ChannelHandlerContext) { - // Note: this is replicated from NIO's HTTP2ToHTTP1ClientCodec. - guard case var .byteBuffer(buffer) = content.data else { - preconditionFailure("Received DATA frame with non-ByteBuffer IOData") - } - - self.logger.trace( - "received HTTP2 frame", - metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(content.data.readableBytes)", - MetadataKey.h2EndStream: "\(content.endStream)", - ] - ) - - self.consumeBytes(from: &buffer, context: context) - - // End stream is set; we don't usually expect this but can handle it in some situations. - if content.endStream, let status = self.stateMachine.receiveEndOfResponseStream() { - self.logger.warning("Unexpected end stream set on DATA frame") - context.fireChannelRead(self.wrapInboundOut(.status(status))) - } - } - - private func consumeBytes(from buffer: inout ByteBuffer, context: ChannelHandlerContext) { - // Do we have bytes to read? If there are no bytes to read then we can't do anything. This may - // happen if the end-of-stream flag is not set on the trailing headers frame (i.e. the one - // containing the gRPC status code) and an additional empty data frame is sent with the - // end-of-stream flag set. - guard buffer.readableBytes > 0 else { - return - } - - // Feed the buffer into the state machine. - let result = self.stateMachine.receiveResponseBuffer( - &buffer, - maxMessageLength: self.maximumReceiveMessageLength - ).mapError { error -> GRPCError.WithContext in - switch error { - case .cardinalityViolation: - return GRPCError.StreamCardinalityViolation.response.captureContext() - case .deserializationFailed, .leftOverBytes: - return GRPCError.DeserializationFailure().captureContext() - case let .decompressionLimitExceeded(compressedSize): - return GRPCError.DecompressionLimitExceeded(compressedSize: compressedSize) - .captureContext() - case let .lengthExceedsLimit(underlyingError): - return underlyingError.captureContext() - case .invalidState: - return GRPCError.InvalidState("parsing data as a response message").captureContext() - } - } - - // Did we get any messages? - switch result { - case let .success(messages): - // Awesome: we got some messages. The state machine guarantees we only get at most a single - // message for unary and client-streaming RPCs. - for message in messages { - // Note: `compressed: false` is currently just a placeholder. This is fine since the message - // context is not currently exposed to the user. If we implement interceptors for the client - // and decide to surface this information then we'll need to extract that information from - // the message reader. - context.fireChannelRead(self.wrapInboundOut(.message(.init(message, compressed: false)))) - } - case let .failure(error): - context.fireErrorCaught(error) - } - } -} - -// MARK: - GRPCClientChannelHandler: Outbound - -extension GRPCClientChannelHandler: ChannelOutboundHandler { - internal typealias OutboundIn = _RawGRPCClientRequestPart - internal typealias OutboundOut = HTTP2Frame.FramePayload - - internal func write( - context: ChannelHandlerContext, - data: NIOAny, - promise: EventLoopPromise? - ) { - switch self.unwrapOutboundIn(data) { - case let .head(requestHead): - // Feed the request into the state machine: - switch self.stateMachine.sendRequestHeaders( - requestHead: requestHead, - allocator: context.channel.allocator - ) { - case let .success(headers): - // We're clear to write some headers. Create an appropriate frame and write it. - let framePayload = HTTP2Frame.FramePayload.headers(.init(headers: headers)) - self.logger.trace( - "writing HTTP2 frame", - metadata: [ - MetadataKey.h2Payload: "HEADERS", - MetadataKey.h2Headers: "\(headers)", - MetadataKey.h2EndStream: "false", - ] - ) - context.write(self.wrapOutboundOut(framePayload), promise: promise) - - case let .failure(sendRequestHeadersError): - switch sendRequestHeadersError { - case .invalidState: - // This is bad: we need to trigger an error and close the channel. - promise?.fail(sendRequestHeadersError) - context.fireErrorCaught(GRPCError.InvalidState("unable to initiate RPC").captureContext()) - } - } - - case let .message(request): - // Feed the request message into the state machine: - let result = self.stateMachine.sendRequest( - request.message, - compressed: request.compressed, - promise: promise - ) - - switch result { - case .success: - () - - case let .failure(writeError): - switch writeError { - case .cardinalityViolation: - // This is fine: we can ignore the request. The RPC can continue as if nothing went wrong. - promise?.fail(writeError) - - case .serializationFailed: - // This is bad: we need to trigger an error and close the channel. - promise?.fail(writeError) - context.fireErrorCaught(GRPCError.SerializationFailure().captureContext()) - - case .invalidState: - promise?.fail(writeError) - context - .fireErrorCaught(GRPCError.InvalidState("unable to write message").captureContext()) - } - } - - case .end: - // About to send end: write any outbound messages first. - while let (result, promise) = self.stateMachine.nextRequest() { - switch result { - case let .success(buffer): - let framePayload: HTTP2Frame.FramePayload = .data( - .init(data: .byteBuffer(buffer), endStream: false) - ) - - self.logger.trace( - "writing HTTP2 frame", - metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(buffer.readableBytes)", - MetadataKey.h2EndStream: "false", - ] - ) - context.write(self.wrapOutboundOut(framePayload), promise: promise) - - case let .failure(error): - context.fireErrorCaught(error) - promise?.fail(error) - return - } - } - - // Okay: can we close the request stream? - switch self.stateMachine.sendEndOfRequestStream() { - case .success: - // We can. Send an empty DATA frame with end-stream set. - let empty = context.channel.allocator.buffer(capacity: 0) - let framePayload: HTTP2Frame.FramePayload = .data( - .init(data: .byteBuffer(empty), endStream: true) - ) - - self.logger.trace( - "writing HTTP2 frame", - metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "0", - MetadataKey.h2EndStream: "true", - ] - ) - context.write(self.wrapOutboundOut(framePayload), promise: promise) - - case let .failure(error): - // Why can't we close the request stream? - switch error { - case .alreadyClosed: - // This is fine: we can just ignore it. The RPC can continue as if nothing went wrong. - promise?.fail(error) - - case .invalidState: - // This is bad: we need to trigger an error and close the channel. - promise?.fail(error) - context - .fireErrorCaught( - GRPCError.InvalidState("unable to close request stream") - .captureContext() - ) - } - } - } - } - - func flush(context: ChannelHandlerContext) { - // Drain any requests. - while let (result, promise) = self.stateMachine.nextRequest() { - switch result { - case let .success(buffer): - let framePayload: HTTP2Frame.FramePayload = .data( - .init(data: .byteBuffer(buffer), endStream: false) - ) - - self.logger.trace( - "writing HTTP2 frame", - metadata: [ - MetadataKey.h2Payload: "DATA", - MetadataKey.h2DataBytes: "\(buffer.readableBytes)", - MetadataKey.h2EndStream: "false", - ] - ) - context.write(self.wrapOutboundOut(framePayload), promise: promise) - - case let .failure(error): - context.fireErrorCaught(error) - promise?.fail(error) - return - } - } - - context.flush() - } -} diff --git a/Sources/GRPC/GRPCClientStateMachine.swift b/Sources/GRPC/GRPCClientStateMachine.swift deleted file mode 100644 index a3d87ec52..000000000 --- a/Sources/GRPC/GRPCClientStateMachine.swift +++ /dev/null @@ -1,798 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import SwiftProtobuf - -enum ReceiveResponseHeadError: Error, Equatable { - /// The 'content-type' header was missing or the value is not supported by this implementation. - case invalidContentType(String?) - - /// The HTTP response status from the server was not 200 OK. - case invalidHTTPStatus(String?) - - /// The encoding used by the server is not supported. - case unsupportedMessageEncoding(String) - - /// An invalid state was encountered. This is a serious implementation error. - case invalidState -} - -enum ReceiveEndOfResponseStreamError: Error, Equatable { - /// The 'content-type' header was missing or the value is not supported by this implementation. - case invalidContentType(String?) - - /// The HTTP response status from the server was not 200 OK. - case invalidHTTPStatus(String?) - - /// An invalid state was encountered. This is a serious implementation error. - case invalidState -} - -enum SendRequestHeadersError: Error { - /// An invalid state was encountered. This is a serious implementation error. - case invalidState -} - -enum SendEndOfRequestStreamError: Error { - /// The request stream has already been closed. This may happen if the RPC was cancelled, timed - /// out, the server terminated the RPC, or the user explicitly closed the stream multiple times. - case alreadyClosed - - /// An invalid state was encountered. This is a serious implementation error. - case invalidState -} - -/// A state machine for a single gRPC call from the perspective of a client. -/// -/// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md -struct GRPCClientStateMachine { - /// The combined state of the request (client) and response (server) streams for an RPC call. - /// - /// The following states are not possible: - /// - `.clientIdleServerActive`: The client must initiate the call before the server moves - /// from the idle state. - /// - `.clientIdleServerClosed`: The client must initiate the call before the server moves from - /// the idle state. - /// - `.clientActiveServerClosed`: The client may not stream if the server is closed. - /// - /// Note: when a peer (client or server) state is "active" it means that messages _may_ be sent or - /// received. That is, the headers for the stream have been processed by the state machine and - /// end-of-stream has not yet been processed. A stream may expect any number of messages (i.e. up - /// to one for a unary call and many for a streaming call). - enum State { - /// Initial state. Neither request stream nor response stream have been initiated. Holds the - /// pending write state for the request stream and arity for the response stream, respectively. - /// - /// Valid transitions: - /// - `clientActiveServerIdle`: if the client initiates the RPC, - /// - `clientClosedServerClosed`: if the client terminates the RPC. - case clientIdleServerIdle(pendingWriteState: PendingWriteState, readArity: MessageArity) - - /// The client has initiated an RPC and has not received initial metadata from the server. Holds - /// the writing state for request stream and arity for the response stream. - /// - /// Valid transitions: - /// - `clientActiveServerActive`: if the server acknowledges the RPC initiation, - /// - `clientClosedServerIdle`: if the client closes the request stream, - /// - `clientClosedServerClosed`: if the client terminates the RPC or the server terminates the - /// RPC with a "trailers-only" response. - case clientActiveServerIdle(writeState: WriteState, pendingReadState: PendingReadState) - - /// The client has indicated to the server that it has finished sending requests. The server - /// has not yet sent response headers for the RPC. Holds the response stream arity. - /// - /// Valid transitions: - /// - `clientClosedServerActive`: if the server acknowledges the RPC initiation, - /// - `clientClosedServerClosed`: if the client terminates the RPC or the server terminates the - /// RPC with a "trailers-only" response. - case clientClosedServerIdle(pendingReadState: PendingReadState) - - /// The client has initiated the RPC and the server has acknowledged it. Messages may have been - /// sent and/or received. Holds the request stream write state and response stream read state. - /// - /// Valid transitions: - /// - `clientClosedServerActive`: if the client closes the request stream, - /// - `clientClosedServerClosed`: if the client or server terminates the RPC. - case clientActiveServerActive(writeState: WriteState, readState: ReadState) - - /// The client has indicated to the server that it has finished sending requests. The server - /// has acknowledged the RPC. Holds the response stream read state. - /// - /// Valid transitions: - /// - `clientClosedServerClosed`: if the client or server terminate the RPC. - case clientClosedServerActive(readState: ReadState) - - /// The RPC has terminated. There are no valid transitions from this state. - case clientClosedServerClosed - - /// This isn't a real state. See `withStateAvoidingCoWs`. - case modifying - } - - /// The current state of the state machine. - internal private(set) var state: State - - /// The default user-agent string. - private static let userAgent = "grpc-swift-nio/\(Version.versionString)" - - /// Creates a state machine representing a gRPC client's request and response stream state. - /// - /// - Parameter requestArity: The expected number of messages on the request stream. - /// - Parameter responseArity: The expected number of messages on the response stream. - init(requestArity: MessageArity, responseArity: MessageArity) { - self.state = .clientIdleServerIdle( - pendingWriteState: .init(arity: requestArity, contentType: .protobuf), - readArity: responseArity - ) - } - - /// Creates a state machine representing a gRPC client's request and response stream state. - /// - /// - Parameter state: The initial state of the state machine. - init(state: State) { - self.state = state - } - - /// Initiates an RPC. - /// - /// The only valid state transition is: - /// - `.clientIdleServerIdle` → `.clientActiveServerIdle` - /// - /// All other states will result in an `.invalidState` error. - /// - /// On success the state will transition to `.clientActiveServerIdle`. - /// - /// - Parameter requestHead: The client request head for the RPC. - mutating func sendRequestHeaders( - requestHead: _GRPCRequestHead, - allocator: ByteBufferAllocator - ) -> Result { - return self.withStateAvoidingCoWs { state in - state.sendRequestHeaders(requestHead: requestHead, allocator: allocator) - } - } - - /// Formats a request to send to the server. - /// - /// The client must be streaming in order for this to return successfully. Therefore the valid - /// state transitions are: - /// - `.clientActiveServerIdle` → `.clientActiveServerIdle` - /// - `.clientActiveServerActive` → `.clientActiveServerActive` - /// - /// The client should not attempt to send requests once the request stream is closed, that is - /// from one of the following states: - /// - `.clientClosedServerIdle` - /// - `.clientClosedServerActive` - /// - `.clientClosedServerClosed` - /// Doing so will result in a `.cardinalityViolation`. - /// - /// Sending a message when both peers are idle (in the `.clientIdleServerIdle` state) will result - /// in a `.invalidState` error. - /// - /// - Parameter message: The serialized request to send to the server. - /// - Parameter compressed: Whether the request should be compressed. - /// - Parameter allocator: A `ByteBufferAllocator` to allocate the buffer into which the encoded - /// request will be written. - mutating func sendRequest( - _ message: ByteBuffer, - compressed: Bool, - promise: EventLoopPromise? = nil - ) -> Result { - return self.withStateAvoidingCoWs { state in - state.sendRequest(message, compressed: compressed, promise: promise) - } - } - - mutating func nextRequest() -> (Result, EventLoopPromise?)? { - return self.state.nextRequest() - } - - /// Closes the request stream. - /// - /// The client must be streaming requests in order to terminate the request stream. Valid - /// states transitions are: - /// - `.clientActiveServerIdle` → `.clientClosedServerIdle` - /// - `.clientActiveServerActive` → `.clientClosedServerActive` - /// - /// The client should not attempt to close the request stream if it is already closed, that is - /// from one of the following states: - /// - `.clientClosedServerIdle` - /// - `.clientClosedServerActive` - /// - `.clientClosedServerClosed` - /// Doing so will result in an `.alreadyClosed` error. - /// - /// Closing the request stream when both peers are idle (in the `.clientIdleServerIdle` state) - /// will result in a `.invalidState` error. - mutating func sendEndOfRequestStream() -> Result { - return self.withStateAvoidingCoWs { state in - state.sendEndOfRequestStream() - } - } - - /// Receive an acknowledgement of the RPC from the server. This **must not** be a "Trailers-Only" - /// response. - /// - /// The server must be idle in order to receive response headers. The valid state transitions are: - /// - `.clientActiveServerIdle` → `.clientActiveServerActive` - /// - `.clientClosedServerIdle` → `.clientClosedServerActive` - /// - /// The response head will be parsed and validated against the gRPC specification. The following - /// errors may be returned: - /// - `.invalidHTTPStatus` if the status was not "200", - /// - `.invalidContentType` if the "content-type" header does not start with "application/grpc", - /// - `.unsupportedMessageEncoding` if the "grpc-encoding" header is not supported. - /// - /// It is not possible to receive response headers from the following states: - /// - `.clientIdleServerIdle` - /// - `.clientActiveServerActive` - /// - `.clientClosedServerActive` - /// - `.clientClosedServerClosed` - /// Doing so will result in a `.invalidState` error. - /// - /// - Parameter headers: The headers received from the server. - mutating func receiveResponseHeaders( - _ headers: HPACKHeaders - ) -> Result { - return self.withStateAvoidingCoWs { state in - state.receiveResponseHeaders(headers) - } - } - - /// Read a response buffer from the server and return any decoded messages. - /// - /// If the response stream has an expected count of `.one` then this function is guaranteed to - /// produce *at most* one `Response` in the `Result`. - /// - /// To receive a response buffer the server must be streaming. Valid states are: - /// - `.clientClosedServerActive` → `.clientClosedServerActive` - /// - `.clientActiveServerActive` → `.clientActiveServerActive` - /// - /// This function will read all of the bytes in the `buffer` and attempt to produce as many - /// messages as possible. This may lead to a number of errors: - /// - `.cardinalityViolation` if more than one message is received when the state reader is - /// expects at most one. - /// - `.leftOverBytes` if bytes remain in the buffer after reading one message when at most one - /// message is expected. - /// - `.deserializationFailed` if the message could not be deserialized. - /// - /// It is not possible to receive response headers from the following states: - /// - `.clientIdleServerIdle` - /// - `.clientClosedServerActive` - /// - `.clientActiveServerActive` - /// - `.clientClosedServerClosed` - /// Doing so will result in a `.invalidState` error. - /// - /// - Parameter buffer: A buffer of bytes received from the server. - mutating func receiveResponseBuffer( - _ buffer: inout ByteBuffer, - maxMessageLength: Int - ) -> Result<[ByteBuffer], MessageReadError> { - return self.withStateAvoidingCoWs { state in - state.receiveResponseBuffer(&buffer, maxMessageLength: maxMessageLength) - } - } - - /// Receive the end of the response stream from the server and parse the results into - /// a `GRPCStatus`. - /// - /// To close the response stream the server must be streaming or idle (since the server may choose - /// to 'fast fail' the RPC). Valid states are: - /// - `.clientActiveServerIdle` → `.clientClosedServerClosed` - /// - `.clientActiveServerActive` → `.clientClosedServerClosed` - /// - `.clientClosedServerIdle` → `.clientClosedServerClosed` - /// - `.clientClosedServerActive` → `.clientClosedServerClosed` - /// - /// It is not possible to receive an end-of-stream if the RPC has not been initiated or has - /// already been terminated. That is, in one of the following states: - /// - `.clientIdleServerIdle` - /// - `.clientClosedServerClosed` - /// Doing so will result in a `.invalidState` error. - /// - /// - Parameter trailers: The trailers to parse. - mutating func receiveEndOfResponseStream( - _ trailers: HPACKHeaders - ) -> Result { - return self.withStateAvoidingCoWs { state in - state.receiveEndOfResponseStream(trailers) - } - } - - /// Receive a DATA frame with the end stream flag set. Determines whether it is safe for the - /// caller to ignore the end stream flag or whether a synthesised status should be forwarded. - /// - /// Receiving a DATA frame with the end stream flag set is unexpected: the specification dictates - /// that an RPC should be ended by the server sending the client a HEADERS frame with end stream - /// set. However, we will tolerate end stream on a DATA frame if we believe the RPC has already - /// completed (i.e. we are in the 'clientClosedServerClosed' state). In cases where we don't - /// expect end of stream on a DATA frame we will emit a status with a message explaining - /// the protocol violation. - mutating func receiveEndOfResponseStream() -> GRPCStatus? { - return self.withStateAvoidingCoWs { state in - state.receiveEndOfResponseStream() - } - } - - /// Temporarily sets `self.state` to `.modifying` before calling the provided block and setting - /// `self.state` to the `State` modified by the block. - /// - /// Since we hold state as associated data on our `State` enum, any modification to that state - /// will trigger a copy on write for its heap allocated data. Temporarily setting the `self.state` - /// to `.modifying` allows us to avoid an extra reference to any heap allocated data and therefore - /// avoid a copy on write. - @inline(__always) - private mutating func withStateAvoidingCoWs( - _ body: (inout State) -> ResultType - ) -> ResultType { - var state = State.modifying - swap(&self.state, &state) - defer { - swap(&self.state, &state) - } - return body(&state) - } -} - -extension GRPCClientStateMachine.State { - /// See `GRPCClientStateMachine.sendRequestHeaders(requestHead:)`. - mutating func sendRequestHeaders( - requestHead: _GRPCRequestHead, - allocator: ByteBufferAllocator - ) -> Result { - let result: Result - - switch self { - case let .clientIdleServerIdle(pendingWriteState, responseArity): - let headers = self.makeRequestHeaders( - method: requestHead.method, - scheme: requestHead.scheme, - host: requestHead.host, - path: requestHead.path, - timeout: GRPCTimeout(deadline: requestHead.deadline), - customMetadata: requestHead.customMetadata, - compression: requestHead.encoding - ) - result = .success(headers) - - self = .clientActiveServerIdle( - writeState: pendingWriteState.makeWriteState( - messageEncoding: requestHead.encoding, - allocator: allocator - ), - pendingReadState: .init(arity: responseArity, messageEncoding: requestHead.encoding) - ) - - case .clientActiveServerIdle, - .clientClosedServerIdle, - .clientClosedServerActive, - .clientActiveServerActive, - .clientClosedServerClosed: - result = .failure(.invalidState) - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - - return result - } - - /// See `GRPCClientStateMachine.sendRequest(_:allocator:)`. - mutating func sendRequest( - _ message: ByteBuffer, - compressed: Bool, - promise: EventLoopPromise? - ) -> Result { - let result: Result - - switch self { - case .clientActiveServerIdle(var writeState, let pendingReadState): - let result = writeState.write(message, compressed: compressed, promise: promise) - self = .clientActiveServerIdle(writeState: writeState, pendingReadState: pendingReadState) - return result - - case .clientActiveServerActive(var writeState, let readState): - let result = writeState.write(message, compressed: compressed, promise: promise) - self = .clientActiveServerActive(writeState: writeState, readState: readState) - return result - - case .clientClosedServerIdle, - .clientClosedServerActive, - .clientClosedServerClosed: - result = .failure(.cardinalityViolation) - - case .clientIdleServerIdle: - result = .failure(.invalidState) - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - - return result - } - - mutating func nextRequest() -> (Result, EventLoopPromise?)? { - switch self { - case .clientActiveServerIdle(var writeState, let pendingReadState): - self = .modifying - let result = writeState.next() - self = .clientActiveServerIdle(writeState: writeState, pendingReadState: pendingReadState) - return result - - case .clientActiveServerActive(var writeState, let readState): - self = .modifying - let result = writeState.next() - self = .clientActiveServerActive(writeState: writeState, readState: readState) - return result - - case .clientIdleServerIdle, - .clientClosedServerIdle, - .clientClosedServerActive, - .clientClosedServerClosed: - return nil - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - } - - /// See `GRPCClientStateMachine.sendEndOfRequestStream()`. - mutating func sendEndOfRequestStream() -> Result { - let result: Result - - switch self { - case let .clientActiveServerIdle(_, pendingReadState): - result = .success(()) - self = .clientClosedServerIdle(pendingReadState: pendingReadState) - - case let .clientActiveServerActive(_, readState): - result = .success(()) - self = .clientClosedServerActive(readState: readState) - - case .clientClosedServerIdle, - .clientClosedServerActive, - .clientClosedServerClosed: - result = .failure(.alreadyClosed) - - case .clientIdleServerIdle: - result = .failure(.invalidState) - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - - return result - } - - /// See `GRPCClientStateMachine.receiveResponseHeaders(_:)`. - mutating func receiveResponseHeaders( - _ headers: HPACKHeaders - ) -> Result { - let result: Result - - switch self { - case let .clientActiveServerIdle(writeState, pendingReadState): - result = self.parseResponseHeaders(headers, pendingReadState: pendingReadState) - .map { readState in - self = .clientActiveServerActive(writeState: writeState, readState: readState) - } - - case let .clientClosedServerIdle(pendingReadState): - result = self.parseResponseHeaders(headers, pendingReadState: pendingReadState) - .map { readState in - self = .clientClosedServerActive(readState: readState) - } - - case .clientIdleServerIdle, - .clientClosedServerActive, - .clientActiveServerActive, - .clientClosedServerClosed: - result = .failure(.invalidState) - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - - return result - } - - /// See `GRPCClientStateMachine.receiveResponseBuffer(_:)`. - mutating func receiveResponseBuffer( - _ buffer: inout ByteBuffer, - maxMessageLength: Int - ) -> Result<[ByteBuffer], MessageReadError> { - let result: Result<[ByteBuffer], MessageReadError> - - switch self { - case var .clientClosedServerActive(readState): - result = readState.readMessages(&buffer, maxLength: maxMessageLength) - self = .clientClosedServerActive(readState: readState) - - case .clientActiveServerActive(let writeState, var readState): - result = readState.readMessages(&buffer, maxLength: maxMessageLength) - self = .clientActiveServerActive(writeState: writeState, readState: readState) - - case .clientIdleServerIdle, - .clientActiveServerIdle, - .clientClosedServerIdle, - .clientClosedServerClosed: - result = .failure(.invalidState) - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - - return result - } - - /// See `GRPCClientStateMachine.receiveEndOfResponseStream(_:)`. - mutating func receiveEndOfResponseStream( - _ trailers: HPACKHeaders - ) -> Result { - let result: Result - - switch self { - case .clientActiveServerIdle, - .clientClosedServerIdle: - result = self.parseTrailersOnly(trailers).map { status in - self = .clientClosedServerClosed - return status - } - - case .clientActiveServerActive, - .clientClosedServerActive: - result = .success(self.parseTrailers(trailers)) - self = .clientClosedServerClosed - - case .clientIdleServerIdle, - .clientClosedServerClosed: - result = .failure(.invalidState) - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - - return result - } - - /// See `GRPCClientStateMachine.receiveEndOfResponseStream()`. - mutating func receiveEndOfResponseStream() -> GRPCStatus? { - let status: GRPCStatus? - - switch self { - case .clientIdleServerIdle: - // Can't see end stream before writing on it. - preconditionFailure() - - case .clientActiveServerIdle, - .clientActiveServerActive, - .clientClosedServerIdle, - .clientClosedServerActive: - self = .clientClosedServerClosed - status = .init( - code: .internalError, - message: "Protocol violation: received DATA frame with end stream set" - ) - - case .clientClosedServerClosed: - // We've already closed. Ignore this. - status = nil - - case .modifying: - preconditionFailure("State left as 'modifying'") - } - - return status - } - - /// Makes the request headers (`Request-Headers` in the specification) used to initiate an RPC - /// call. - /// - /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests - /// - /// - Parameter host: The host serving the RPC. - /// - Parameter options: Any options related to the call. - /// - Parameter requestID: A request ID associated with the call. An additional header will be - /// added using this value if `options.requestIDHeader` is specified. - private func makeRequestHeaders( - method: String, - scheme: String, - host: String, - path: String, - timeout: GRPCTimeout, - customMetadata: HPACKHeaders, - compression: ClientMessageEncoding - ) -> HPACKHeaders { - var headers = HPACKHeaders() - // The 10 is: - // - 6 which are required and added just below, and - // - 4 which are possibly added, depending on conditions. - headers.reserveCapacity(10 + customMetadata.count) - - // Add the required headers. - headers.add(name: ":method", value: method) - headers.add(name: ":path", value: path) - headers.add(name: ":authority", value: host) - headers.add(name: ":scheme", value: scheme) - headers.add(name: "content-type", value: "application/grpc") - // Used to detect incompatible proxies, part of the gRPC specification. - headers.add(name: "te", value: "trailers") - - switch compression { - case let .enabled(configuration): - // Request encoding. - if let outbound = configuration.outbound { - headers.add(name: GRPCHeaderName.encoding, value: outbound.name) - } - - // Response encoding. - if !configuration.inbound.isEmpty { - headers.add(name: GRPCHeaderName.acceptEncoding, value: configuration.acceptEncodingHeader) - } - - case .disabled: - () - } - - // Add the timeout header, if a timeout was specified. - if timeout != .infinite { - headers.add(name: GRPCHeaderName.timeout, value: String(describing: timeout)) - } - - // Add user-defined custom metadata: this should come after the call definition headers. - // TODO: make header normalization user-configurable. - headers.add( - contentsOf: customMetadata.lazy.map { name, value, indexing in - (name.lowercased(), value, indexing) - } - ) - - // Add default user-agent value, if `customMetadata` didn't contain user-agent - if !customMetadata.contains(name: "user-agent") { - headers.add(name: "user-agent", value: GRPCClientStateMachine.userAgent) - } - - return headers - } - - /// Parses the response headers ("Response-Headers" in the specification) from the server into - /// a `ReadState`. - /// - /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses - /// - /// - Parameter headers: The headers to parse. - private func parseResponseHeaders( - _ headers: HPACKHeaders, - pendingReadState: PendingReadState - ) -> Result { - // From: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses - // - // "Implementations should expect broken deployments to send non-200 HTTP status codes in - // responses as well as a variety of non-GRPC content-types and to omit Status & Status-Message. - // Implementations must synthesize a Status & Status-Message to propagate to the application - // layer when this occurs." - let statusHeader = headers.first(name: ":status") - let responseStatus = - statusHeader - .flatMap(Int.init) - .map { code in - HTTPResponseStatus(statusCode: code) - } ?? .preconditionFailed - - guard responseStatus == .ok else { - return .failure(.invalidHTTPStatus(statusHeader)) - } - - let contentTypeHeader = headers.first(name: "content-type") - guard contentTypeHeader.flatMap(ContentType.init) != nil else { - return .failure(.invalidContentType(contentTypeHeader)) - } - - let result: Result - - // What compression mechanism is the server using, if any? - if let encodingHeader = headers.first(name: GRPCHeaderName.encoding) { - // Note: the server is allowed to encode messages using an algorithm which wasn't included in - // the 'grpc-accept-encoding' header. If the client still supports that algorithm (despite not - // permitting the server to use it) then it must still decode that message. Ideally we should - // log a message here if that was the case but we don't hold that information. - if let compression = CompressionAlgorithm(rawValue: encodingHeader) { - result = .success(pendingReadState.makeReadState(compression: compression)) - } else { - // The algorithm isn't one we support. - result = .failure(.unsupportedMessageEncoding(encodingHeader)) - } - } else { - // No compression was specified, this is fine. - result = .success(pendingReadState.makeReadState(compression: nil)) - } - - return result - } - - /// Parses the response trailers ("Trailers" in the specification) from the server into - /// a `GRPCStatus`. - /// - /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses - /// - /// - Parameter trailers: Trailers to parse. - private func parseTrailers(_ trailers: HPACKHeaders) -> GRPCStatus { - // Extract the "Status" and "Status-Message" - let code = self.readStatusCode(from: trailers) ?? .unknown - let message = self.readStatusMessage(from: trailers) - return .init(code: code, message: message) - } - - private func readStatusCode(from trailers: HPACKHeaders) -> GRPCStatus.Code? { - return trailers.first(name: GRPCHeaderName.statusCode) - .flatMap(Int.init) - .flatMap({ GRPCStatus.Code(rawValue: $0) }) - } - - private func readStatusMessage(from trailers: HPACKHeaders) -> String? { - return trailers.first(name: GRPCHeaderName.statusMessage) - .map(GRPCStatusMessageMarshaller.unmarshall) - } - - /// Parses a "Trailers-Only" response from the server into a `GRPCStatus`. - /// - /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses - /// - /// - Parameter trailers: Trailers to parse. - private func parseTrailersOnly( - _ trailers: HPACKHeaders - ) -> Result { - // We need to check whether we have a valid HTTP status in the headers, if we don't then we also - // need to check whether we have a gRPC status as it should take preference over a synthesising - // one from the ":status". - // - // See: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - let statusHeader = trailers.first(name: ":status") - let httpResponseStatus = statusHeader.flatMap(Int.init).map { - HTTPResponseStatus(statusCode: $0) - } - - guard let httpResponseStatus = httpResponseStatus else { - return .failure(.invalidHTTPStatus(statusHeader)) - } - - guard httpResponseStatus == .ok else { - // Non-200 response. If there's a 'grpc-status' message we should use that otherwise try - // to create one from the HTTP status code. - let grpcStatusCode = - self.readStatusCode(from: trailers) - ?? GRPCStatus.Code(httpStatus: Int(httpResponseStatus.code)) - ?? .unknown - let message = self.readStatusMessage(from: trailers) - return .success(GRPCStatus(code: grpcStatusCode, message: message)) - } - - // Only validate the content-type header if it's present. This is a small deviation from the - // spec as the content-type is meant to be sent in "Trailers-Only" responses. However, if it's - // missing then we should avoid the error and propagate the status code and message sent by - // the server instead. - if let contentTypeHeader = trailers.first(name: "content-type"), - ContentType(value: contentTypeHeader) == nil - { - return .failure(.invalidContentType(contentTypeHeader)) - } - - // We've verified the status and content type are okay: parse the trailers. - return .success(self.parseTrailers(trailers)) - } -} diff --git a/Sources/GRPC/GRPCContentType.swift b/Sources/GRPC/GRPCContentType.swift deleted file mode 100644 index e8d706963..000000000 --- a/Sources/GRPC/GRPCContentType.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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. - */ - -// See: -// - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md -// - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md -internal enum ContentType { - case protobuf - case webProtobuf - case webTextProtobuf - - init?(value: String) { - switch value { - case "application/grpc", - "application/grpc+proto": - self = .protobuf - - case "application/grpc-web", - "application/grpc-web+proto": - self = .webProtobuf - - case "application/grpc-web-text", - "application/grpc-web-text+proto": - self = .webTextProtobuf - - default: - return nil - } - } - - var canonicalValue: String { - switch self { - case .protobuf: - // This is more widely supported than "application/grpc+proto" - return "application/grpc" - - case .webProtobuf: - return "application/grpc-web+proto" - - case .webTextProtobuf: - return "application/grpc-web-text+proto" - } - } - - static let commonPrefix = "application/grpc" -} diff --git a/Sources/GRPC/GRPCError.swift b/Sources/GRPC/GRPCError.swift deleted file mode 100644 index 5daec8962..000000000 --- a/Sources/GRPC/GRPCError.swift +++ /dev/null @@ -1,370 +0,0 @@ -/* - * 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. - */ - -/// An error thrown by the gRPC library. -/// -/// Implementation details: this is a case-less `enum` with an inner-class per error type. This -/// allows for additional error classes to be added as a SemVer minor change. -/// -/// Unfortunately it is not possible to use a private inner `enum` with static property 'cases' on -/// the outer type to mirror each case of the inner `enum` as many of the errors require associated -/// values (pattern matching is not possible). -public enum GRPCError { - /// The RPC is not implemented on the server. - public struct RPCNotImplemented: GRPCErrorProtocol { - /// The path of the RPC which was called, e.g. '/echo.Echo/Get'. - public var rpc: String - - public init(rpc: String) { - self.rpc = rpc - } - - public var description: String { - return "RPC '\(self.rpc)' is not implemented" - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .unimplemented, message: self.description) - } - } - - /// The RPC was cancelled by the client. - public struct RPCCancelledByClient: GRPCErrorProtocol { - public let description: String = "RPC was cancelled by the client" - - public init() {} - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .cancelled, message: self.description, cause: self) - } - } - - /// The RPC did not complete before the timeout. - public struct RPCTimedOut: GRPCErrorProtocol { - /// The time limit which was exceeded by the RPC. - public var timeLimit: TimeLimit - - public init(_ timeLimit: TimeLimit) { - self.timeLimit = timeLimit - } - - public var description: String { - return "RPC timed out before completing" - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .deadlineExceeded, message: self.description, cause: self) - } - } - - /// A message was not able to be serialized. - public struct SerializationFailure: GRPCErrorProtocol { - public let description = "Message serialization failed" - - public init() {} - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.description, cause: self) - } - } - - /// A message was not able to be deserialized. - public struct DeserializationFailure: GRPCErrorProtocol { - public let description = "Message deserialization failed" - - public init() {} - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.description, cause: self) - } - } - - /// The length of the received payload was longer than is permitted. - public struct PayloadLengthLimitExceeded: GRPCErrorProtocol { - public let description: String - - public init(actualLength length: Int, limit: Int) { - self.description = "Payload length exceeds limit (\(length) > \(limit))" - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .resourceExhausted, message: self.description, cause: self) - } - } - - /// It was not possible to compress or decompress a message with zlib. - public struct ZlibCompressionFailure: GRPCErrorProtocol { - var code: Int32 - var message: String? - - public init(code: Int32, message: String?) { - self.code = code - self.message = message - } - - public var description: String { - if let message = self.message { - return "Zlib error: \(self.code) \(message)" - } else { - return "Zlib error: \(self.code)" - } - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.description, cause: self) - } - } - - /// The decompression limit was exceeded while decompressing a message. - public struct DecompressionLimitExceeded: GRPCErrorProtocol { - /// The size of the compressed payload whose decompressed size exceeded the decompression limit. - public let compressedSize: Int - - public init(compressedSize: Int) { - self.compressedSize = compressedSize - } - - public var description: String { - return "Decompression limit exceeded with \(self.compressedSize) compressed bytes" - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .resourceExhausted, message: nil, cause: self) - } - } - - /// It was not possible to decode a base64 message (gRPC-Web only). - public struct Base64DecodeError: GRPCErrorProtocol { - public let description = "Base64 message decoding failed" - - public init() {} - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.description, cause: self) - } - } - - /// The compression mechanism used was not supported. - public struct CompressionUnsupported: GRPCErrorProtocol { - public let description = "The compression used is not supported" - - public init() {} - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .unimplemented, message: self.description, cause: self) - } - } - - /// Too many, or too few, messages were sent over the given stream. - public struct StreamCardinalityViolation: GRPCErrorProtocol { - /// The stream on which there was a cardinality violation. - public let description: String - - /// A request stream cardinality violation. - public static let request = StreamCardinalityViolation("Request stream cardinality violation") - - /// A response stream cardinality violation. - public static let response = StreamCardinalityViolation("Response stream cardinality violation") - - private init(_ description: String) { - self.description = description - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.description, cause: self) - } - } - - /// The 'content-type' HTTP/2 header was missing or not valid. - public struct InvalidContentType: GRPCErrorProtocol { - /// The value of the 'content-type' header, if it was present. - public var contentType: String? - - public init(_ contentType: String?) { - self.contentType = contentType - } - - public var description: String { - if let contentType = self.contentType { - return "Invalid 'content-type' header: '\(contentType)'" - } else { - return "Missing 'content-type' header" - } - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.description, cause: self) - } - } - - /// The ':status' HTTP/2 header was not "200". - public struct InvalidHTTPStatus: GRPCErrorProtocol { - /// The HTTP/2 ':status' header, if it was present. - public var status: String? - - public init(_ status: String?) { - self.status = status - } - - public var description: String { - if let status = status { - return "Invalid HTTP response status: \(status)" - } else { - return "Missing HTTP ':status' header" - } - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus( - code: .init(httpStatus: self.status) ?? .unknown, - message: self.description, - cause: self - ) - } - } - - /// The ':status' HTTP/2 header was not "200" but the 'grpc-status' header was present and valid. - public struct InvalidHTTPStatusWithGRPCStatus: GRPCErrorProtocol { - public var status: GRPCStatus - - public init(_ status: GRPCStatus) { - self.status = status - } - - public var description: String { - return "Invalid HTTP response status, but gRPC status was present" - } - - public func makeGRPCStatus() -> GRPCStatus { - return self.status - } - } - - /// Action was taken after the RPC had already completed. - public struct AlreadyComplete: GRPCErrorProtocol { - public var description: String { - return "The RPC has already completed" - } - - public init() {} - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .unavailable, message: self.description, cause: self) - } - } - - /// An invalid state has been reached; something has gone very wrong. - public struct InvalidState: GRPCErrorProtocol { - public var message: String - - public init(_ message: String) { - self.message = "Invalid state: \(message)" - } - - public var description: String { - return self.message - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.message, cause: self) - } - } - - public struct ProtocolViolation: GRPCErrorProtocol { - public var message: String - - public init(_ message: String) { - self.message = "Protocol violation: \(message)" - } - - public var description: String { - return self.message - } - - public func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus(code: .internalError, message: self.message, cause: self) - } - } -} - -extension GRPCError { - struct WithContext: Error, GRPCStatusTransformable { - var error: GRPCStatusTransformable - var file: StaticString - var line: Int - var function: StaticString - - init( - _ error: GRPCStatusTransformable, - file: StaticString = #fileID, - line: Int = #line, - function: StaticString = #function - ) { - self.error = error - self.file = file - self.line = line - self.function = function - } - - func makeGRPCStatus() -> GRPCStatus { - return self.error.makeGRPCStatus() - } - } -} - -/// Requirements for ``GRPCError`` types. -public protocol GRPCErrorProtocol: GRPCStatusTransformable, Equatable, CustomStringConvertible {} - -extension GRPCErrorProtocol { - /// Creates a `GRPCError.WithContext` containing a `GRPCError` and the location of the call site. - internal func captureContext( - file: StaticString = #fileID, - line: Int = #line, - function: StaticString = #function - ) -> GRPCError.WithContext { - return GRPCError.WithContext(self, file: file, line: line, function: function) - } -} - -extension GRPCStatus.Code { - /// The gRPC status code associated with the given HTTP status code. This should only be used if - /// the RPC did not return a 'grpc-status' trailer. - internal init?(httpStatus codeString: String?) { - if let code = codeString.flatMap(Int.init) { - self.init(httpStatus: code) - } else { - return nil - } - } - - internal init?(httpStatus: Int) { - /// See: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - switch httpStatus { - case 400: - self = .internalError - case 401: - self = .unauthenticated - case 403: - self = .permissionDenied - case 404: - self = .unimplemented - case 429, 502, 503, 504: - self = .unavailable - default: - return nil - } - } -} diff --git a/Sources/GRPC/GRPCHeaderName.swift b/Sources/GRPC/GRPCHeaderName.swift deleted file mode 100644 index b6d34b311..000000000 --- a/Sources/GRPC/GRPCHeaderName.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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. - */ - -internal enum GRPCHeaderName { - static let timeout = "grpc-timeout" - static let encoding = "grpc-encoding" - static let acceptEncoding = "grpc-accept-encoding" - static let statusCode = "grpc-status" - static let statusMessage = "grpc-message" - static let contentType = "content-type" -} diff --git a/Sources/GRPC/GRPCIdleHandler.swift b/Sources/GRPC/GRPCIdleHandler.swift deleted file mode 100644 index 0f9492163..000000000 --- a/Sources/GRPC/GRPCIdleHandler.swift +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHTTP2 -import NIOTLS -import NIOTransportServices - -internal final class GRPCIdleHandler: ChannelInboundHandler { - typealias InboundIn = HTTP2Frame - typealias OutboundOut = HTTP2Frame - - /// The amount of time to wait before closing the channel when there are no active streams. - /// If nil, then we shouldn't schedule idle tasks. - private let idleTimeout: TimeAmount? - - /// The ping handler. - private var pingHandler: PingHandler - - /// The scheduled task which will close the connection after the keep-alive timeout has expired. - private var scheduledClose: Scheduled? - - /// The scheduled task which will ping. - private var scheduledPing: RepeatedTask? - - /// The mode we're operating in. - private let mode: Mode - - /// The time the handler was created. - private let creationTime: NIODeadline - - /// Returns the age of the connection in seconds. - private var connectionAgeInSeconds: UInt64 { - let now = NIODeadline.now() - let nanoseconds = now.uptimeNanoseconds - self.creationTime.uptimeNanoseconds - let seconds = nanoseconds / 1_000_000_000 - return seconds - } - - private var context: ChannelHandlerContext? - - /// The mode of operation: the client tracks additional connection state in the connection - /// manager. - internal enum Mode { - case client(ConnectionManager, HTTP2StreamMultiplexer) - case server - - var connectionManager: ConnectionManager? { - switch self { - case let .client(manager, _): - return manager - case .server: - return nil - } - } - } - - /// The current state. - private var stateMachine: GRPCIdleHandlerStateMachine - - init( - connectionManager: ConnectionManager, - multiplexer: HTTP2StreamMultiplexer, - idleTimeout: TimeAmount, - keepalive configuration: ClientConnectionKeepalive, - logger: Logger - ) { - self.mode = .client(connectionManager, multiplexer) - switch connectionManager.idleBehavior { - case .neverGoIdle: - self.idleTimeout = nil - case .closeWhenIdleTimeout: - self.idleTimeout = idleTimeout - } - self.stateMachine = .init(role: .client, logger: logger) - self.pingHandler = PingHandler( - pingCode: 5, - interval: configuration.interval, - timeout: configuration.timeout, - permitWithoutCalls: configuration.permitWithoutCalls, - maximumPingsWithoutData: configuration.maximumPingsWithoutData, - minimumSentPingIntervalWithoutData: configuration.minimumSentPingIntervalWithoutData - ) - self.creationTime = .now() - } - - init( - idleTimeout: TimeAmount, - keepalive configuration: ServerConnectionKeepalive, - logger: Logger - ) { - self.mode = .server - self.stateMachine = .init(role: .server, logger: logger) - self.idleTimeout = idleTimeout - self.pingHandler = PingHandler( - pingCode: 10, - interval: configuration.interval, - timeout: configuration.timeout, - permitWithoutCalls: configuration.permitWithoutCalls, - maximumPingsWithoutData: configuration.maximumPingsWithoutData, - minimumSentPingIntervalWithoutData: configuration.minimumSentPingIntervalWithoutData, - minimumReceivedPingIntervalWithoutData: configuration.minimumReceivedPingIntervalWithoutData, - maximumPingStrikes: configuration.maximumPingStrikes - ) - self.creationTime = .now() - } - - private func perform(operations: GRPCIdleHandlerStateMachine.Operations) { - // Prod the connection manager. - if let event = operations.connectionManagerEvent, let manager = self.mode.connectionManager { - switch event { - case .idle: - manager.idle() - case .inactive: - manager.channelInactive() - case .ready: - manager.ready() - case .quiescing: - manager.beginQuiescing() - } - } - - // Max concurrent streams changed. - if let manager = self.mode.connectionManager, - let maxConcurrentStreams = operations.maxConcurrentStreamsChange - { - manager.maxConcurrentStreamsChanged(maxConcurrentStreams) - } - - // Handle idle timeout creation/cancellation. - if let idleTimeout = self.idleTimeout, let idleTask = operations.idleTask { - switch idleTask { - case let .cancel(task): - self.stateMachine.logger.debug("idle timeout task cancelled") - task.cancel() - - case .schedule: - if self.idleTimeout != .nanoseconds(.max), let context = self.context { - self.stateMachine.logger.debug( - "scheduling idle timeout task", - metadata: [MetadataKey.delayMs: "\(idleTimeout.milliseconds)"] - ) - let task = context.eventLoop.scheduleTask(in: idleTimeout) { - self.stateMachine.logger.debug("idle timeout task fired") - self.idleTimeoutFired() - } - self.perform(operations: self.stateMachine.scheduledIdleTimeoutTask(task)) - } - } - } - - // Send a GOAWAY frame. - if let streamID = operations.sendGoAwayWithLastPeerInitiatedStreamID { - self.stateMachine.logger.debug( - "sending GOAWAY frame", - metadata: [ - MetadataKey.h2GoAwayLastStreamID: "\(Int(streamID))" - ] - ) - - let goAwayFrame = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: streamID, errorCode: .noError, opaqueData: nil) - ) - - self.context?.write(self.wrapOutboundOut(goAwayFrame), promise: nil) - - // We emit a ping after some GOAWAY frames. - if operations.shouldPingAfterGoAway { - let pingFrame = HTTP2Frame( - streamID: .rootStream, - payload: .ping(self.pingHandler.pingDataGoAway, ack: false) - ) - self.context?.write(self.wrapOutboundOut(pingFrame), promise: nil) - } - - self.context?.flush() - } - - // Close the channel, if necessary. - if operations.shouldCloseChannel, let context = self.context { - // Close on the next event-loop tick so we don't drop any events which are - // currently being processed. - context.eventLoop.execute { - self.stateMachine.logger.debug( - "closing connection", - metadata: ["connection_age_secs": .stringConvertible(self.connectionAgeInSeconds)] - ) - context.close(mode: .all, promise: nil) - } - } - } - - private func handlePingAction(_ action: PingHandler.Action) { - switch action { - case .none: - () - - case .ack: - // NIO's HTTP2 handler acks for us so this is a no-op. Log so it doesn't appear that we are - // ignoring pings. - self.stateMachine.logger.debug( - "sending PING frame", - metadata: [MetadataKey.h2PingAck: "true"] - ) - - case .cancelScheduledTimeout: - self.scheduledClose?.cancel() - self.scheduledClose = nil - - case let .schedulePing(delay, timeout): - self.schedulePing(in: delay, timeout: timeout) - - case let .reply(framePayload): - switch framePayload { - case .ping(_, let ack): - self.stateMachine.logger.debug( - "sending PING frame", - metadata: [MetadataKey.h2PingAck: "\(ack)"] - ) - default: - () - } - let frame = HTTP2Frame(streamID: .rootStream, payload: framePayload) - self.context?.writeAndFlush(self.wrapOutboundOut(frame), promise: nil) - - case .ratchetDownLastSeenStreamID: - self.perform(operations: self.stateMachine.ratchetDownGoAwayStreamID()) - } - } - - private func schedulePing(in delay: TimeAmount, timeout: TimeAmount) { - guard delay != .nanoseconds(.max) else { - return - } - - self.stateMachine.logger.debug( - "scheduled keepalive pings", - metadata: [MetadataKey.intervalMs: "\(delay.milliseconds)"] - ) - - self.scheduledPing = self.context?.eventLoop.scheduleRepeatedTask( - initialDelay: delay, - delay: delay - ) { _ in - let action = self.pingHandler.pingFired() - if case .none = action { return } - self.handlePingAction(action) - // `timeout` is less than `interval`, guaranteeing that the close task - // will be fired before a new ping is triggered. - assert(timeout < delay, "`timeout` must be less than `interval`") - self.scheduleClose(in: timeout) - } - } - - private func scheduleClose(in timeout: TimeAmount) { - self.scheduledClose = self.context?.eventLoop.scheduleTask(in: timeout) { - self.stateMachine.logger.debug("keepalive timer expired") - self.perform(operations: self.stateMachine.shutdownNow()) - } - } - - private func idleTimeoutFired() { - self.perform(operations: self.stateMachine.idleTimeoutTaskFired()) - } - - func handlerAdded(context: ChannelHandlerContext) { - self.context = context - } - - func handlerRemoved(context: ChannelHandlerContext) { - self.context = nil - } - - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - if let created = event as? NIOHTTP2StreamCreatedEvent { - self.perform(operations: self.stateMachine.streamCreated(withID: created.streamID)) - self.handlePingAction(self.pingHandler.streamCreated()) - self.mode.connectionManager?.streamOpened() - context.fireUserInboundEventTriggered(event) - } else if let closed = event as? StreamClosedEvent { - self.perform(operations: self.stateMachine.streamClosed(withID: closed.streamID)) - self.handlePingAction(self.pingHandler.streamClosed()) - self.mode.connectionManager?.streamClosed() - context.fireUserInboundEventTriggered(event) - } else if event is ChannelShouldQuiesceEvent { - self.perform(operations: self.stateMachine.initiateGracefulShutdown()) - // Swallow this event. - } else if case let .handshakeCompleted(negotiatedProtocol) = event as? TLSUserEvent { - let tlsVersion = try? context.channel.getTLSVersionSync() - self.stateMachine.logger.debug( - "TLS handshake completed", - metadata: [ - "alpn": "\(negotiatedProtocol ?? "nil")", - "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", - ] - ) - context.fireUserInboundEventTriggered(event) - } else { - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - if let waitsForConnectivity = event as? NIOTSNetworkEvents.WaitingForConnectivity { - self.mode.connectionManager?.channelError(waitsForConnectivity.transientError) - } - } - #endif - - context.fireUserInboundEventTriggered(event) - } - } - - func errorCaught(context: ChannelHandlerContext, error: Error) { - // No state machine action here. - self.mode.connectionManager?.channelError(error) - context.fireErrorCaught(error) - } - - func channelActive(context: ChannelHandlerContext) { - self.stateMachine.logger.addIPAddressMetadata( - local: context.localAddress, - remote: context.remoteAddress - ) - - // No state machine action here. - switch self.mode { - case let .client(connectionManager, multiplexer): - connectionManager.channelActive(channel: context.channel, multiplexer: multiplexer) - case .server: - () - } - context.fireChannelActive() - } - - func channelInactive(context: ChannelHandlerContext) { - self.perform(operations: self.stateMachine.channelInactive()) - self.scheduledPing?.cancel() - self.scheduledClose?.cancel() - self.scheduledPing = nil - self.scheduledClose = nil - context.fireChannelInactive() - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let frame = self.unwrapInboundIn(data) - - switch frame.payload { - case let .goAway(lastStreamID, errorCode, _): - self.stateMachine.logger.debug( - "received GOAWAY frame", - metadata: [ - MetadataKey.h2GoAwayLastStreamID: "\(Int(lastStreamID))", - MetadataKey.h2GoAwayError: "\(errorCode.networkCode)", - ] - ) - self.perform(operations: self.stateMachine.receiveGoAway()) - case let .settings(.settings(settings)): - self.perform(operations: self.stateMachine.receiveSettings(settings)) - case let .ping(data, ack): - self.stateMachine.logger.debug( - "received PING frame", - metadata: [MetadataKey.h2PingAck: "\(ack)"] - ) - self.handlePingAction(self.pingHandler.read(pingData: data, ack: ack)) - default: - // We're not interested in other events. - () - } - - context.fireChannelRead(data) - } -} - -extension HTTP2SettingsParameter { - internal var loggingMetadataKey: String { - switch self { - case .headerTableSize: - return "h2_settings_header_table_size" - case .enablePush: - return "h2_settings_enable_push" - case .maxConcurrentStreams: - return "h2_settings_max_concurrent_streams" - case .initialWindowSize: - return "h2_settings_initial_window_size" - case .maxFrameSize: - return "h2_settings_max_frame_size" - case .maxHeaderListSize: - return "h2_settings_max_header_list_size" - case .enableConnectProtocol: - return "h2_settings_enable_connect_protocol" - default: - return String(describing: self) - } - } -} - -extension TimeAmount { - fileprivate var milliseconds: Int64 { - self.nanoseconds / 1_000_000 - } -} diff --git a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift b/Sources/GRPC/GRPCIdleHandlerStateMachine.swift deleted file mode 100644 index 5fbbe5c71..000000000 --- a/Sources/GRPC/GRPCIdleHandlerStateMachine.swift +++ /dev/null @@ -1,725 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHTTP2 - -/// Holds state for the 'GRPCIdleHandler', this isn't really just the idleness of the connection, -/// it also holds state relevant to quiescing the connection as well as logging some HTTP/2 specific -/// information (like stream creation/close events and changes to settings which can be useful when -/// debugging live systems). Much of this information around the connection state is also used to -/// inform the client connection manager since that's strongly tied to various channel and HTTP/2 -/// events. -struct GRPCIdleHandlerStateMachine { - /// Our role in the connection. - enum Role { - case server - case client - } - - /// The 'operating' state of the connection. This is the primary state we expect to be in: the - /// connection is up and running and there are expected to be active RPCs, although this is by no - /// means a requirement. Some of the situations in which there may be no active RPCs are: - /// - /// 1. Before the connection is 'ready' (that is, seen the first SETTINGS frame), - /// 2. After the connection has dropped to zero active streams and before the idle timeout task - /// has been scheduled. - /// 3. When the connection has zero active streams and the connection was configured without an - /// idle timeout. - fileprivate struct Operating: CanOpenStreams, CanCloseStreams { - /// Our role in the connection. - var role: Role - - /// The number of open stream. - var openStreams: Int - - /// The last stream ID initiated by the remote peer. - var lastPeerInitiatedStreamID: HTTP2StreamID - - /// The maximum number of concurrent streams we are allowed to operate. - var maxConcurrentStreams: Int - - /// We keep track of whether we've seen a SETTINGS frame. We expect to see one after the - /// connection preface (RFC 7540 § 3.5). This is primarily for the benefit of the client which - /// determines a connection to be 'ready' once it has seen the first SETTINGS frame. We also - /// won't set an idle timeout until this becomes true. - var hasSeenSettings: Bool - - fileprivate init(role: Role) { - self.role = role - self.openStreams = 0 - self.lastPeerInitiatedStreamID = .rootStream - // Assumed until we know better. - self.maxConcurrentStreams = 100 - self.hasSeenSettings = false - } - - fileprivate init(fromWaitingToIdle state: WaitingToIdle) { - self.role = state.role - self.openStreams = 0 - self.lastPeerInitiatedStreamID = state.lastPeerInitiatedStreamID - self.maxConcurrentStreams = state.maxConcurrentStreams - // We won't transition to 'WaitingToIdle' unless we've seen a SETTINGS frame. - self.hasSeenSettings = true - } - } - - /// The waiting-to-idle state is used when the connection has become 'ready', has no active - /// RPCs and an idle timeout task has been scheduled. In this state, the connection will be closed - /// once the idle is fired. The task will be cancelled on the creation of a stream. - fileprivate struct WaitingToIdle { - /// Our role in the connection. - var role: Role - - /// The last stream ID initiated by the remote peer. - var lastPeerInitiatedStreamID: HTTP2StreamID - - /// The maximum number of concurrent streams we are allowed to operate. - var maxConcurrentStreams: Int - - /// A task which, when fired, will idle the connection. - var idleTask: Scheduled - - fileprivate init(fromOperating state: Operating, idleTask: Scheduled) { - // We won't transition to this state unless we've seen a SETTINGS frame. - assert(state.hasSeenSettings) - - self.role = state.role - self.lastPeerInitiatedStreamID = state.lastPeerInitiatedStreamID - self.maxConcurrentStreams = state.maxConcurrentStreams - self.idleTask = idleTask - } - } - - /// The quiescing state is entered only from the operating state. It may be entered if we receive - /// a GOAWAY frame (the remote peer initiated the quiescing) or we initiate graceful shutdown - /// locally. - fileprivate struct Quiescing: TracksOpenStreams, CanCloseStreams { - /// Our role in the connection. - var role: Role - - /// The number of open stream. - var openStreams: Int - - /// The last stream ID initiated by the remote peer. - var lastPeerInitiatedStreamID: HTTP2StreamID - - /// The maximum number of concurrent streams we are allowed to operate. - var maxConcurrentStreams: Int - - /// Whether this peer initiated shutting down. - var initiatedByUs: Bool - - fileprivate init(fromOperating state: Operating, initiatedByUs: Bool) { - // If we didn't initiate shutdown, the remote peer must have done so by sending a GOAWAY frame - // in which case we must have seen a SETTINGS frame. - assert(initiatedByUs || state.hasSeenSettings) - self.role = state.role - self.initiatedByUs = initiatedByUs - self.openStreams = state.openStreams - self.lastPeerInitiatedStreamID = state.lastPeerInitiatedStreamID - self.maxConcurrentStreams = state.maxConcurrentStreams - } - } - - /// The closing state is entered when one of the previous states initiates a connection closure. - /// From this state the only possible transition is to the closed state. - fileprivate struct Closing { - /// Our role in the connection. - var role: Role - - /// Should the client connection manager receive an idle event when we close? (If not then it - /// will attempt to establish a new connection immediately.) - var shouldIdle: Bool - - fileprivate init(fromOperating state: Operating) { - self.role = state.role - // Idle if there are no open streams and we've seen the first SETTINGS frame. - self.shouldIdle = !state.hasOpenStreams && state.hasSeenSettings - } - - fileprivate init(fromQuiescing state: Quiescing) { - self.role = state.role - // If we initiated the quiescing then we shouldn't go idle (we want to shutdown instead). - self.shouldIdle = !state.initiatedByUs - } - - fileprivate init(fromWaitingToIdle state: WaitingToIdle, shouldIdle: Bool = true) { - self.role = state.role - self.shouldIdle = shouldIdle - } - } - - fileprivate enum State { - case operating(Operating) - case waitingToIdle(WaitingToIdle) - case quiescing(Quiescing) - case closing(Closing) - case closed - } - - /// The set of operations that should be performed as a result of interaction with the state - /// machine. - struct Operations { - /// An event to notify the connection manager about. - private(set) var connectionManagerEvent: ConnectionManagerEvent? - - /// The value of HTTP/2 SETTINGS_MAX_CONCURRENT_STREAMS changed. - private(set) var maxConcurrentStreamsChange: Int? - - /// An idle task, either scheduling or cancelling an idle timeout. - private(set) var idleTask: IdleTask? - - /// Send a GOAWAY frame with the last peer initiated stream ID set to this value. - private(set) var sendGoAwayWithLastPeerInitiatedStreamID: HTTP2StreamID? - - /// Whether the channel should be closed. - private(set) var shouldCloseChannel: Bool - - /// Whether a ping should be sent after a GOAWAY frame. - private(set) var shouldPingAfterGoAway: Bool - - fileprivate static let none = Operations() - - fileprivate mutating func sendGoAwayFrame( - lastPeerInitiatedStreamID streamID: HTTP2StreamID, - followWithPing: Bool = false - ) { - self.sendGoAwayWithLastPeerInitiatedStreamID = streamID - self.shouldPingAfterGoAway = followWithPing - } - - fileprivate mutating func cancelIdleTask(_ task: Scheduled) { - self.idleTask = .cancel(task) - } - - fileprivate mutating func scheduleIdleTask() { - self.idleTask = .schedule - } - - fileprivate mutating func closeChannel() { - self.shouldCloseChannel = true - } - - fileprivate mutating func notifyConnectionManager(about event: ConnectionManagerEvent) { - self.connectionManagerEvent = event - } - - fileprivate mutating func maxConcurrentStreamsChanged(_ newValue: Int) { - self.maxConcurrentStreamsChange = newValue - } - - private init() { - self.connectionManagerEvent = nil - self.idleTask = nil - self.sendGoAwayWithLastPeerInitiatedStreamID = nil - self.shouldCloseChannel = false - self.shouldPingAfterGoAway = false - } - } - - /// An event to notify the 'ConnectionManager' about. - enum ConnectionManagerEvent { - case inactive - case idle - case ready - case quiescing - } - - enum IdleTask { - case schedule - case cancel(Scheduled) - } - - /// The current state. - private var state: State - - /// A logger. - internal var logger: Logger - - /// Create a new state machine. - init(role: Role, logger: Logger) { - self.state = .operating(.init(role: role)) - self.logger = logger - } - - // MARK: Stream Events - - /// An HTTP/2 stream was created. - mutating func streamCreated(withID streamID: HTTP2StreamID) -> Operations { - var operations: Operations = .none - - switch self.state { - case var .operating(state): - // Create the stream. - state.streamCreated(streamID, logger: self.logger) - self.state = .operating(state) - - case let .waitingToIdle(state): - var operating = Operating(fromWaitingToIdle: state) - operating.streamCreated(streamID, logger: self.logger) - self.state = .operating(operating) - operations.cancelIdleTask(state.idleTask) - - case var .quiescing(state): - switch state.role { - case .client where streamID.isServerInitiated: - state.lastPeerInitiatedStreamID = streamID - case .server where streamID.isClientInitiated: - state.lastPeerInitiatedStreamID = streamID - default: - () - } - - state.openStreams += 1 - self.state = .quiescing(state) - - case .closing, .closed: - () - } - - return operations - } - - /// An HTTP/2 stream was closed. - mutating func streamClosed(withID streamID: HTTP2StreamID) -> Operations { - var operations: Operations = .none - - switch self.state { - case var .operating(state): - state.streamClosed(streamID, logger: self.logger) - - if state.hasSeenSettings, !state.hasOpenStreams { - operations.scheduleIdleTask() - } - - self.state = .operating(state) - - case .waitingToIdle: - // If we're waiting to idle then there can't be any streams open which can be closed. - preconditionFailure() - - case var .quiescing(state): - state.streamClosed(streamID, logger: self.logger) - - if state.hasOpenStreams { - self.state = .quiescing(state) - } else { - self.state = .closing(.init(fromQuiescing: state)) - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - operations.closeChannel() - } - - case .closing, .closed: - () - } - - return operations - } - - // MARK: - Idle Events - - /// The given task was scheduled to idle the connection. - mutating func scheduledIdleTimeoutTask(_ task: Scheduled) -> Operations { - var operations: Operations = .none - - switch self.state { - case let .operating(state): - if state.hasOpenStreams { - operations.cancelIdleTask(task) - } else { - self.state = .waitingToIdle(.init(fromOperating: state, idleTask: task)) - } - - case .waitingToIdle: - // There's already an idle task. - preconditionFailure() - - case .quiescing, .closing, .closed: - operations.cancelIdleTask(task) - } - - return operations - } - - /// The idle timeout task fired, the connection should be idled. - mutating func idleTimeoutTaskFired() -> Operations { - var operations: Operations = .none - - switch self.state { - case let .waitingToIdle(state): - self.state = .closing(.init(fromWaitingToIdle: state)) - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - operations.closeChannel() - - // We're either operating on streams, streams are going away, or the connection is going away - // so we don't need to idle the connection. - case .operating, .quiescing, .closing, .closed: - () - } - - return operations - } - - // MARK: - Shutdown Events - - /// Close the connection, this can be caused as a result of a keepalive timeout (i.e. the server - /// has become unresponsive), we'll bin this connection as a result. - mutating func shutdownNow() -> Operations { - var operations = Operations.none - - switch self.state { - case let .operating(state): - var closing = Closing(fromOperating: state) - closing.shouldIdle = false - self.state = .closing(closing) - operations.closeChannel() - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - - case let .waitingToIdle(state): - // Don't idle. - self.state = .closing(Closing(fromWaitingToIdle: state, shouldIdle: false)) - operations.closeChannel() - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - operations.cancelIdleTask(state.idleTask) - - case let .quiescing(state): - self.state = .closing(Closing(fromQuiescing: state)) - // We've already sent a GOAWAY frame if we're in this state, just close. - operations.closeChannel() - - case .closing, .closed: - () - } - - return operations - } - - /// Initiate a graceful shutdown of this connection, that is, begin quiescing. - mutating func initiateGracefulShutdown() -> Operations { - var operations: Operations = .none - - switch self.state { - case let .operating(state): - operations.notifyConnectionManager(about: .quiescing) - if state.hasOpenStreams { - // There are open streams: send a GOAWAY frame and wait for the stream count to reach zero. - // - // It's okay if we haven't seen a SETTINGS frame at this point; we've initiated the shutdown - // so making a connection is ready isn't necessary. - - // TODO: we should ratchet down the last initiated stream after 1-RTT. - // - // As a client we will just stop initiating streams. - if state.role == .server { - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - } - - self.state = .quiescing(.init(fromOperating: state, initiatedByUs: true)) - } else { - // No open streams: send a GOAWAY frame and close the channel. - self.state = .closing(.init(fromOperating: state)) - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - operations.closeChannel() - } - - case let .waitingToIdle(state): - // There can't be any open streams, but we have a few loose ends to clear up: we need to - // cancel the idle timeout, send a GOAWAY frame and then close. We don't want to idle from the - // closing state: we want to shutdown instead. - self.state = .closing(.init(fromWaitingToIdle: state, shouldIdle: false)) - operations.cancelIdleTask(state.idleTask) - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - operations.closeChannel() - - case var .quiescing(state): - // We're already quiescing: either the remote initiated it or we're initiating it more than - // once. Set ourselves as the initiator to ensure we don't idle when we eventually close, this - // is important for the client: if the server initiated this then we establish a new - // connection when we close, unless we also initiated shutdown. - state.initiatedByUs = true - self.state = .quiescing(state) - - case var .closing(state): - // We've already called 'close()', make sure we don't go idle. - state.shouldIdle = false - self.state = .closing(state) - - case .closed: - () - } - - return operations - } - - /// We've received a GOAWAY frame from the remote peer. Either the remote peer wants to close the - /// connection or they're responding to us shutting down the connection. - mutating func receiveGoAway() -> Operations { - var operations: Operations = .none - - switch self.state { - case let .operating(state): - // A SETTINGS frame MUST follow the connection preface. (RFC 7540 § 3.5) - assert(state.hasSeenSettings) - - operations.notifyConnectionManager(about: .quiescing) - if state.hasOpenStreams { - switch state.role { - case .client: - // The server sent us a GOAWAY we'll just stop opening new streams and will send a GOAWAY - // frame before we close later. - () - case .server: - // Client sent us a GOAWAY frame; we'll let the streams drain and then close. We'll tell - // the client that we're going away and send them a ping. When we receive the pong we will - // send another GOAWAY frame with a lower stream ID. In this case, the pong acts as an ack - // for the GOAWAY. - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: .maxID, followWithPing: true) - } - self.state = .quiescing(.init(fromOperating: state, initiatedByUs: false)) - } else { - // No open streams, we can close as well. - self.state = .closing(.init(fromOperating: state)) - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - operations.closeChannel() - } - - case let .waitingToIdle(state): - // There can't be any open streams, but we have a few loose ends to clear up: we need to - // cancel the idle timeout, send a GOAWAY frame and then close. - // We should also notify the connection manager that quiescing is happening. - self.state = .closing(.init(fromWaitingToIdle: state)) - operations.notifyConnectionManager(about: .quiescing) - operations.cancelIdleTask(state.idleTask) - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: state.lastPeerInitiatedStreamID) - operations.closeChannel() - - case .quiescing: - // We're already quiescing, this changes nothing. - () - - case .closing, .closed: - // We're already closing/closed (so must have emitted a GOAWAY frame already). Ignore this. - () - } - - return operations - } - - mutating func ratchetDownGoAwayStreamID() -> Operations { - var operations: Operations = .none - - switch self.state { - case let .quiescing(state): - let streamID = state.lastPeerInitiatedStreamID - operations.sendGoAwayFrame(lastPeerInitiatedStreamID: streamID) - case .operating, .waitingToIdle, .closing, .closed: - // We can only need to ratchet down the stream ID if we're already quiescing. - () - } - - return operations - } - - mutating func receiveSettings(_ settings: HTTP2Settings) -> Operations { - // Log the change in settings. - self.logger.debug( - "HTTP2 settings update", - metadata: Dictionary( - settings.map { - ("\($0.parameter.loggingMetadataKey)", "\($0.value)") - }, - uniquingKeysWith: { a, _ in a } - ) - ) - - var operations: Operations = .none - - switch self.state { - case var .operating(state): - let hasSeenSettingsPreviously = state.hasSeenSettings - - // If we hadn't previously seen settings then we need to notify the client connection manager - // that we're now ready. - if !hasSeenSettingsPreviously { - operations.notifyConnectionManager(about: .ready) - state.hasSeenSettings = true - - // Now that we know the connection is ready, we may want to start an idle timeout as well. - if !state.hasOpenStreams { - operations.scheduleIdleTask() - } - } - - // Update max concurrent streams. - if let maxStreams = settings.last(where: { $0.parameter == .maxConcurrentStreams })?.value { - operations.maxConcurrentStreamsChanged(maxStreams) - state.maxConcurrentStreams = maxStreams - } else if !hasSeenSettingsPreviously { - // We hadn't seen settings before now and max concurrent streams wasn't set we should assume - // the default and emit an update. - operations.maxConcurrentStreamsChanged(100) - state.maxConcurrentStreams = 100 - } - - self.state = .operating(state) - - case var .waitingToIdle(state): - // Update max concurrent streams. - if let maxStreams = settings.last(where: { $0.parameter == .maxConcurrentStreams })?.value { - operations.maxConcurrentStreamsChanged(maxStreams) - state.maxConcurrentStreams = maxStreams - } - self.state = .waitingToIdle(state) - - case .quiescing, .closing, .closed: - () - } - - return operations - } - - // MARK: - Channel Events - - // (Other channel events aren't included here as they don't impact the state machine.) - - /// 'channelActive' was called in the idle handler holding this state machine. - mutating func channelInactive() -> Operations { - var operations: Operations = .none - - switch self.state { - case let .operating(state): - self.state = .closed - - // We unexpectedly became inactive. - if !state.hasSeenSettings || state.hasOpenStreams { - // Haven't seen settings, or we've seen settings and there are open streams. - operations.notifyConnectionManager(about: .inactive) - } else { - // Have seen settings and there are no open streams. - operations.notifyConnectionManager(about: .idle) - } - - case let .waitingToIdle(state): - self.state = .closed - - // We were going to idle anyway. - operations.notifyConnectionManager(about: .idle) - operations.cancelIdleTask(state.idleTask) - - case let .quiescing(state): - self.state = .closed - - if state.initiatedByUs || state.hasOpenStreams { - operations.notifyConnectionManager(about: .inactive) - } else { - operations.notifyConnectionManager(about: .idle) - } - - case let .closing(state): - self.state = .closed - - if state.shouldIdle { - operations.notifyConnectionManager(about: .idle) - } else { - operations.notifyConnectionManager(about: .inactive) - } - - case .closed: - () - } - - return operations - } -} - -// MARK: - Helper Protocols - -private protocol TracksOpenStreams { - /// The number of open streams. - var openStreams: Int { get set } -} - -extension TracksOpenStreams { - /// Whether any streams are open. - fileprivate var hasOpenStreams: Bool { - return self.openStreams != 0 - } -} - -private protocol CanOpenStreams: TracksOpenStreams { - /// The role of this peer in the connection. - var role: GRPCIdleHandlerStateMachine.Role { get } - - /// The ID of the stream most recently initiated by the remote peer. - var lastPeerInitiatedStreamID: HTTP2StreamID { get set } - - /// The maximum number of concurrent streams. - var maxConcurrentStreams: Int { get set } - - mutating func streamCreated(_ streamID: HTTP2StreamID, logger: Logger) -} - -extension CanOpenStreams { - fileprivate mutating func streamCreated(_ streamID: HTTP2StreamID, logger: Logger) { - self.openStreams += 1 - - switch self.role { - case .client where streamID.isServerInitiated: - self.lastPeerInitiatedStreamID = streamID - case .server where streamID.isClientInitiated: - self.lastPeerInitiatedStreamID = streamID - default: - () - } - - logger.debug( - "HTTP2 stream created", - metadata: [ - MetadataKey.h2StreamID: "\(streamID)", - MetadataKey.h2ActiveStreams: "\(self.openStreams)", - ] - ) - - if self.openStreams == self.maxConcurrentStreams { - logger.warning( - "HTTP2 max concurrent stream limit reached", - metadata: [ - MetadataKey.h2ActiveStreams: "\(self.openStreams)" - ] - ) - } - } -} - -private protocol CanCloseStreams: TracksOpenStreams { - /// Notes that a stream has closed. - mutating func streamClosed(_ streamID: HTTP2StreamID, logger: Logger) -} - -extension CanCloseStreams { - fileprivate mutating func streamClosed(_ streamID: HTTP2StreamID, logger: Logger) { - self.openStreams -= 1 - - logger.debug( - "HTTP2 stream closed", - metadata: [ - MetadataKey.h2StreamID: "\(streamID)", - MetadataKey.h2ActiveStreams: "\(self.openStreams)", - ] - ) - } -} diff --git a/Sources/GRPC/GRPCKeepaliveHandlers.swift b/Sources/GRPC/GRPCKeepaliveHandlers.swift deleted file mode 100644 index 38f88a97e..000000000 --- a/Sources/GRPC/GRPCKeepaliveHandlers.swift +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOHTTP2 - -struct PingHandler { - /// Opaque ping data used for keep-alive pings. - private let pingData: HTTP2PingData - - /// Opaque ping data used for a ping sent after a GOAWAY frame. - internal let pingDataGoAway: HTTP2PingData - - /// The amount of time to wait before sending a keepalive ping. - private let interval: TimeAmount - - /// The amount of time to wait for an acknowledgment. - /// If it does not receive an acknowledgment within this time, it will close the connection - private let timeout: TimeAmount - - /// Send keepalive pings even if there are no calls in flight. - private let permitWithoutCalls: Bool - - /// Maximum number of pings that can be sent when there is no data/header frame to be sent. - private let maximumPingsWithoutData: UInt - - /// If there are no data/header frames being received: - /// The minimum amount of time to wait between successive pings. - private let minimumSentPingIntervalWithoutData: TimeAmount - - /// If there are no data/header frames being sent: - /// The minimum amount of time expected between receiving successive pings. - /// If the time between successive pings is less than this value, then the ping will be considered a bad ping from the peer. - /// Such a ping counts as a "ping strike". - /// Ping strikes are only applicable to server handler - private let minimumReceivedPingIntervalWithoutData: TimeAmount? - - /// Maximum number of bad pings that the server will tolerate before sending an HTTP2 GOAWAY frame and closing the connection. - /// Setting it to `0` allows the server to accept any number of bad pings. - /// Ping strikes are only applicable to server handler - private let maximumPingStrikes: UInt? - - /// When the handler started pinging - private var startedAt: NIODeadline? - - /// When the last ping was received - private var lastReceivedPingDate: NIODeadline? - - /// When the last ping was sent - private var lastSentPingDate: NIODeadline? - - /// The number of pings sent on the transport without any data - private var sentPingsWithoutData = 0 - - /// Number of strikes - private var pingStrikes: UInt = 0 - - /// The scheduled task which will close the connection. - private var scheduledClose: Scheduled? - - /// Number of active streams - private var activeStreams = 0 { - didSet { - if self.activeStreams > 0 { - self.sentPingsWithoutData = 0 - } - } - } - - private static let goAwayFrame = HTTP2Frame.FramePayload.goAway( - lastStreamID: .rootStream, - errorCode: .enhanceYourCalm, - opaqueData: nil - ) - - // For testing only - var _testingOnlyNow: NIODeadline? - - enum Action { - case none - case ack - case schedulePing(delay: TimeAmount, timeout: TimeAmount) - case cancelScheduledTimeout - case reply(HTTP2Frame.FramePayload) - case ratchetDownLastSeenStreamID - } - - init( - pingCode: UInt64, - interval: TimeAmount, - timeout: TimeAmount, - permitWithoutCalls: Bool, - maximumPingsWithoutData: UInt, - minimumSentPingIntervalWithoutData: TimeAmount, - minimumReceivedPingIntervalWithoutData: TimeAmount? = nil, - maximumPingStrikes: UInt? = nil - ) { - self.pingData = HTTP2PingData(withInteger: pingCode) - self.pingDataGoAway = HTTP2PingData(withInteger: ~pingCode) - self.interval = interval - self.timeout = timeout - self.permitWithoutCalls = permitWithoutCalls - self.maximumPingsWithoutData = maximumPingsWithoutData - self.minimumSentPingIntervalWithoutData = minimumSentPingIntervalWithoutData - self.minimumReceivedPingIntervalWithoutData = minimumReceivedPingIntervalWithoutData - self.maximumPingStrikes = maximumPingStrikes - } - - mutating func streamCreated() -> Action { - self.activeStreams += 1 - - if self.startedAt == nil { - self.startedAt = self.now() - return .schedulePing(delay: self.interval, timeout: self.timeout) - } else { - return .none - } - } - - mutating func streamClosed() -> Action { - self.activeStreams -= 1 - return .none - } - - mutating func read(pingData: HTTP2PingData, ack: Bool) -> Action { - if ack { - return self.handlePong(pingData) - } else { - return self.handlePing(pingData) - } - } - - private func handlePong(_ pingData: HTTP2PingData) -> Action { - if pingData == self.pingData { - return .cancelScheduledTimeout - } else if pingData == self.pingDataGoAway { - // We received a pong for a ping we sent to trail a GOAWAY frame: this means we can now - // send another GOAWAY frame with a (possibly) lower stream ID. - return .ratchetDownLastSeenStreamID - } else { - return .none - } - } - - private mutating func handlePing(_ pingData: HTTP2PingData) -> Action { - // Do we support ping strikes (only servers support ping strikes)? - if let maximumPingStrikes = self.maximumPingStrikes { - // Is this a ping strike? - if self.isPingStrike { - self.pingStrikes += 1 - - // A maximum ping strike of zero indicates that we tolerate any number of strikes. - if maximumPingStrikes != 0, self.pingStrikes > maximumPingStrikes { - return .reply(PingHandler.goAwayFrame) - } else { - return .none - } - } else { - // This is a valid ping, reset our strike count and reply with a pong. - self.pingStrikes = 0 - self.lastReceivedPingDate = self.now() - return .ack - } - } else { - // We don't support ping strikes. We'll just reply with a pong. - // - // Note: we don't need to update `pingStrikes` or `lastReceivedPingDate` as we don't - // support ping strikes. - return .ack - } - } - - mutating func pingFired() -> Action { - if self.shouldBlockPing { - return .none - } else { - return .reply(self.generatePingFrame(data: self.pingData)) - } - } - - private mutating func generatePingFrame( - data: HTTP2PingData - ) -> HTTP2Frame.FramePayload { - if self.activeStreams == 0 { - self.sentPingsWithoutData += 1 - } - - self.lastSentPingDate = self.now() - return HTTP2Frame.FramePayload.ping(data, ack: false) - } - - /// Returns true if, on receipt of a ping, the ping should be regarded as a ping strike. - /// - /// A ping is considered a 'strike' if: - /// - There are no active streams. - /// - We allow pings to be sent when there are no active streams (i.e. `self.permitWithoutCalls`). - /// - The time since the last ping we received is less than the minimum allowed interval. - /// - /// - Precondition: Ping strikes are supported (i.e. `self.maximumPingStrikes != nil`) - private var isPingStrike: Bool { - assert( - self.maximumPingStrikes != nil, - "Ping strikes are not supported but we're checking for one" - ) - guard self.activeStreams == 0, self.permitWithoutCalls, - let lastReceivedPingDate = self.lastReceivedPingDate, - let minimumReceivedPingIntervalWithoutData = self.minimumReceivedPingIntervalWithoutData - else { - return false - } - - return self.now() - lastReceivedPingDate < minimumReceivedPingIntervalWithoutData - } - - private var shouldBlockPing: Bool { - // There is no active call on the transport and pings should not be sent - guard self.activeStreams > 0 || self.permitWithoutCalls else { - return true - } - - // There is no active call on the transport but pings should be sent - if self.activeStreams == 0, self.permitWithoutCalls { - // The number of pings already sent on the transport without any data has already exceeded the limit - if self.sentPingsWithoutData > self.maximumPingsWithoutData { - return true - } - - // The time elapsed since the previous ping is less than the minimum required - if let lastSentPingDate = self.lastSentPingDate, - self.now() - lastSentPingDate < self.minimumSentPingIntervalWithoutData - { - return true - } - - return false - } - - return false - } - - private func now() -> NIODeadline { - return self._testingOnlyNow ?? .now() - } -} diff --git a/Sources/GRPC/GRPCPayload.swift b/Sources/GRPC/GRPCPayload.swift deleted file mode 100644 index c356e16f5..000000000 --- a/Sources/GRPC/GRPCPayload.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -/// A data type which may be serialized into and out from a `ByteBuffer` in order to be sent between -/// gRPC peers. -public protocol GRPCPayload { - /// Initializes a new payload by deserializing the bytes from the given `ByteBuffer`. - /// - /// - Parameter serializedByteBuffer: A buffer containing the serialized bytes of this payload. - /// - Throws: If the payload could not be deserialized from the buffer. - init(serializedByteBuffer: inout ByteBuffer) throws - - /// Serializes the payload into the given `ByteBuffer`. - /// - /// - Parameter buffer: The buffer to write the serialized payload into. - /// - Throws: If the payload could not be serialized. - /// - Important: Implementers must *NOT* clear or read bytes from `buffer`. - func serialize(into buffer: inout ByteBuffer) throws -} diff --git a/Sources/GRPC/GRPCServerPipelineConfigurator.swift b/Sources/GRPC/GRPCServerPipelineConfigurator.swift deleted file mode 100644 index c1b208e3a..000000000 --- a/Sources/GRPC/GRPCServerPipelineConfigurator.swift +++ /dev/null @@ -1,511 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import NIOTLS - -/// Configures a server pipeline for gRPC with the appropriate handlers depending on the HTTP -/// version used for transport. -/// -/// If TLS is enabled then the handler listens for an 'TLSUserEvent.handshakeCompleted' event and -/// configures the pipeline appropriately for the protocol negotiated via ALPN. If TLS is not -/// configured then the HTTP version is determined by parsing the inbound byte stream. -final class GRPCServerPipelineConfigurator: ChannelInboundHandler, RemovableChannelHandler { - internal typealias InboundIn = ByteBuffer - internal typealias InboundOut = ByteBuffer - - /// The server configuration. - private let configuration: Server.Configuration - - /// A buffer containing the buffered bytes. - private var buffer: ByteBuffer? - - /// The current state. - private var state: State - - private enum ALPN { - /// ALPN is expected. It may or may not be required, however. - case expected(required: Bool) - - /// ALPN was expected but not required and no protocol was negotiated in the handshake. We may - /// now fall back to parsing bytes on the connection. - case expectedButFallingBack - - /// ALPN is not expected; this is a cleartext connection. - case notExpected - } - - private enum State { - /// The pipeline isn't configured yet. - case notConfigured(alpn: ALPN) - /// We're configuring the pipeline. - case configuring - } - - init(configuration: Server.Configuration) { - if let tls = configuration.tlsConfiguration { - self.state = .notConfigured(alpn: .expected(required: tls.requireALPN)) - } else { - self.state = .notConfigured(alpn: .notExpected) - } - - self.configuration = configuration - } - - /// Makes a gRPC idle handler for the server.. - private func makeIdleHandler() -> GRPCIdleHandler { - return .init( - idleTimeout: self.configuration.connectionIdleTimeout, - keepalive: self.configuration.connectionKeepalive, - logger: self.configuration.logger - ) - } - - /// Makes an HTTP/2 handler. - private func makeHTTP2Handler() -> NIOHTTP2Handler { - return .init( - mode: .server, - initialSettings: [ - HTTP2Setting( - parameter: .maxConcurrentStreams, - value: self.configuration.httpMaxConcurrentStreams - ), - HTTP2Setting( - parameter: .maxHeaderListSize, - value: HPACKDecoder.defaultMaxHeaderListSize - ), - HTTP2Setting( - parameter: .maxFrameSize, - value: self.configuration.httpMaxFrameSize - ), - HTTP2Setting( - parameter: .initialWindowSize, - value: self.configuration.httpTargetWindowSize - ), - ] - ) - } - - /// Makes an HTTP/2 multiplexer suitable handling gRPC requests. - private func makeHTTP2Multiplexer(for channel: Channel) -> HTTP2StreamMultiplexer { - return .init( - mode: .server, - channel: channel, - targetWindowSize: self.configuration.httpTargetWindowSize - ) { [logger = self.configuration.logger] stream in - // Sync options were added to the HTTP/2 stream channel in 1.17.0 (we require at least this) - // so this shouldn't be `nil`, but it's not a problem if it is. - let http2StreamID = try? stream.syncOptions?.getOption(HTTP2StreamChannelOptions.streamID) - let streamID = - http2StreamID.map { streamID in - return String(Int(streamID)) - } ?? "" - - var logger = logger - logger[metadataKey: MetadataKey.h2StreamID] = "\(streamID)" - - do { - // TODO: provide user configuration for header normalization. - let handler = self.makeHTTP2ToRawGRPCHandler(normalizeHeaders: true, logger: logger) - try stream.pipeline.syncOperations.addHandler(handler) - return stream.eventLoop.makeSucceededVoidFuture() - } catch { - return stream.eventLoop.makeFailedFuture(error) - } - } - } - - /// Makes an HTTP/2 to raw gRPC server handler. - private func makeHTTP2ToRawGRPCHandler( - normalizeHeaders: Bool, - logger: Logger - ) -> HTTP2ToRawGRPCServerCodec { - return HTTP2ToRawGRPCServerCodec( - servicesByName: self.configuration.serviceProvidersByName, - encoding: self.configuration.messageEncoding, - errorDelegate: self.configuration.errorDelegate, - normalizeHeaders: normalizeHeaders, - maximumReceiveMessageLength: self.configuration.maximumReceiveMessageLength, - logger: logger - ) - } - - /// The pipeline finished configuring. - private func configurationCompleted(result: Result, context: ChannelHandlerContext) { - switch result { - case .success: - context.pipeline.removeHandler(context: context, promise: nil) - case let .failure(error): - self.errorCaught(context: context, error: error) - } - } - - /// Configures the pipeline to handle gRPC requests on an HTTP/2 connection. - private func configureHTTP2(context: ChannelHandlerContext) { - // We're now configuring the pipeline. - self.state = .configuring - - // We could use 'Channel.configureHTTP2Pipeline', but then we'd have to find the right handlers - // to then insert our keepalive and idle handlers between. We can just add everything together. - let result: Result - - do { - // This is only ever called as a result of reading a user inbound event or reading inbound so - // we'll be on the right event loop and sync operations are fine. - let sync = context.pipeline.syncOperations - try sync.addHandler(self.makeHTTP2Handler()) - try sync.addHandler(self.makeIdleHandler()) - try sync.addHandler(self.makeHTTP2Multiplexer(for: context.channel)) - result = .success(()) - } catch { - result = .failure(error) - } - - self.configurationCompleted(result: result, context: context) - } - - /// Configures the pipeline to handle gRPC-Web requests on an HTTP/1 connection. - private func configureHTTP1(context: ChannelHandlerContext) { - // We're now configuring the pipeline. - self.state = .configuring - - let result: Result - do { - // This is only ever called as a result of reading a user inbound event or reading inbound so - // we'll be on the right event loop and sync operations are fine. - let sync = context.pipeline.syncOperations - try sync.configureHTTPServerPipeline(withErrorHandling: true) - try sync.addHandler(WebCORSHandler(configuration: self.configuration.webCORS)) - let scheme = self.configuration.tlsConfiguration == nil ? "http" : "https" - try sync.addHandler(GRPCWebToHTTP2ServerCodec(scheme: scheme)) - // There's no need to normalize headers for HTTP/1. - try sync.addHandler( - self.makeHTTP2ToRawGRPCHandler(normalizeHeaders: false, logger: self.configuration.logger) - ) - result = .success(()) - } catch { - result = .failure(error) - } - - self.configurationCompleted(result: result, context: context) - } - - /// Attempts to determine the HTTP version from the buffer and then configure the pipeline - /// appropriately. Closes the connection if the HTTP version could not be determined. - private func determineHTTPVersionAndConfigurePipeline( - buffer: ByteBuffer, - context: ChannelHandlerContext - ) { - switch HTTPVersionParser.determineHTTPVersion(buffer) { - case .http2: - self.configureHTTP2(context: context) - case .http1: - self.configureHTTP1(context: context) - case .unknown: - // Neither H2 nor H1 or the length limit has been exceeded. - self.configuration.logger.error("Unable to determine http version, closing") - context.close(mode: .all, promise: nil) - case .notEnoughBytes: - () // Try again with more bytes. - } - } - - /// Handles a 'TLSUserEvent.handshakeCompleted' event and configures the pipeline to handle gRPC - /// requests. - private func handleHandshakeCompletedEvent( - _ event: TLSUserEvent, - alpnIsRequired: Bool, - context: ChannelHandlerContext - ) { - switch event { - case let .handshakeCompleted(negotiatedProtocol): - let tlsVersion = try? context.channel.getTLSVersionSync() - self.configuration.logger.debug( - "TLS handshake completed", - metadata: [ - "alpn": "\(negotiatedProtocol ?? "nil")", - "tls_version": "\(tlsVersion.map(String.init(describing:)) ?? "nil")", - ] - ) - - switch negotiatedProtocol { - case let .some(negotiated): - if GRPCApplicationProtocolIdentifier.isHTTP2Like(negotiated) { - self.configureHTTP2(context: context) - } else if GRPCApplicationProtocolIdentifier.isHTTP1(negotiated) { - self.configureHTTP1(context: context) - } else { - self.configuration.logger.warning("Unsupported ALPN identifier '\(negotiated)', closing") - context.close(mode: .all, promise: nil) - } - - case .none: - if alpnIsRequired { - self.configuration.logger.warning("No ALPN protocol negotiated, closing'") - context.close(mode: .all, promise: nil) - } else { - self.configuration.logger.warning("No ALPN protocol negotiated'") - // We're now falling back to parsing bytes. - self.state = .notConfigured(alpn: .expectedButFallingBack) - self.tryParsingBufferedData(context: context) - } - } - - case .shutdownCompleted: - // We don't care about this here. - () - } - } - - /// Try to parse the buffered data to determine whether or not HTTP/2 or HTTP/1 should be used. - private func tryParsingBufferedData(context: ChannelHandlerContext) { - if let buffer = self.buffer { - self.determineHTTPVersionAndConfigurePipeline(buffer: buffer, context: context) - } - } - - // MARK: - Channel Handler - - internal func errorCaught(context: ChannelHandlerContext, error: Error) { - if let delegate = self.configuration.errorDelegate { - let baseError: Error - - if let errorWithContext = error as? GRPCError.WithContext { - baseError = errorWithContext.error - } else { - baseError = error - } - - delegate.observeLibraryError(baseError) - } - - context.close(mode: .all, promise: nil) - } - - internal func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - switch self.state { - case let .notConfigured(alpn: .expected(required)): - if let event = event as? TLSUserEvent { - self.handleHandshakeCompletedEvent(event, alpnIsRequired: required, context: context) - } - - case .notConfigured(alpn: .expectedButFallingBack), - .notConfigured(alpn: .notExpected), - .configuring: - () - } - - context.fireUserInboundEventTriggered(event) - } - - internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - var buffer = self.unwrapInboundIn(data) - self.buffer.setOrWriteBuffer(&buffer) - - switch self.state { - case .notConfigured(alpn: .notExpected), - .notConfigured(alpn: .expectedButFallingBack): - // If ALPN isn't expected, or we didn't negotiate via ALPN and we don't require it then we - // can try parsing the data we just buffered. - self.tryParsingBufferedData(context: context) - - case .notConfigured(alpn: .expected), - .configuring: - // We expect ALPN or we're being configured, just buffer the data, we'll forward it later. - () - } - - // Don't forward the reads: we'll do so when we have configured the pipeline. - } - - internal func removeHandler( - context: ChannelHandlerContext, - removalToken: ChannelHandlerContext.RemovalToken - ) { - // Forward any buffered reads. - if let buffer = self.buffer { - self.buffer = nil - context.fireChannelRead(self.wrapInboundOut(buffer)) - } - context.leavePipeline(removalToken: removalToken) - } -} - -// MARK: - HTTP Version Parser - -struct HTTPVersionParser { - /// HTTP/2 connection preface bytes. See RFC 7540 § 5.3. - private static let http2ClientMagic = [ - UInt8(ascii: "P"), - UInt8(ascii: "R"), - UInt8(ascii: "I"), - UInt8(ascii: " "), - UInt8(ascii: "*"), - UInt8(ascii: " "), - UInt8(ascii: "H"), - UInt8(ascii: "T"), - UInt8(ascii: "T"), - UInt8(ascii: "P"), - UInt8(ascii: "/"), - UInt8(ascii: "2"), - UInt8(ascii: "."), - UInt8(ascii: "0"), - UInt8(ascii: "\r"), - UInt8(ascii: "\n"), - UInt8(ascii: "\r"), - UInt8(ascii: "\n"), - UInt8(ascii: "S"), - UInt8(ascii: "M"), - UInt8(ascii: "\r"), - UInt8(ascii: "\n"), - UInt8(ascii: "\r"), - UInt8(ascii: "\n"), - ] - - /// Determines whether the bytes in the `ByteBuffer` are prefixed with the HTTP/2 client - /// connection preface. - static func prefixedWithHTTP2ConnectionPreface(_ buffer: ByteBuffer) -> SubParseResult { - let view = buffer.readableBytesView - - guard view.count >= HTTPVersionParser.http2ClientMagic.count else { - // Not enough bytes. - return .notEnoughBytes - } - - let slice = view[view.startIndex ..< view.startIndex.advanced(by: self.http2ClientMagic.count)] - return slice.elementsEqual(HTTPVersionParser.http2ClientMagic) ? .accepted : .rejected - } - - enum ParseResult: Hashable { - case http1 - case http2 - case unknown - case notEnoughBytes - } - - enum SubParseResult: Hashable { - case accepted - case rejected - case notEnoughBytes - } - - private static let maxLengthToCheck = 1024 - - static func determineHTTPVersion(_ buffer: ByteBuffer) -> ParseResult { - switch Self.prefixedWithHTTP2ConnectionPreface(buffer) { - case .accepted: - return .http2 - - case .notEnoughBytes: - switch Self.prefixedWithHTTP1RequestLine(buffer) { - case .accepted: - // Not enough bytes to check H2, but enough to confirm H1. - return .http1 - case .notEnoughBytes: - // Not enough bytes to check H2 or H1. - return .notEnoughBytes - case .rejected: - // Not enough bytes to check H2 and definitely not H1. - return .notEnoughBytes - } - - case .rejected: - switch Self.prefixedWithHTTP1RequestLine(buffer) { - case .accepted: - // Not H2, but H1 is confirmed. - return .http1 - case .notEnoughBytes: - // Not H2, but not enough bytes to reject H1 yet. - return .notEnoughBytes - case .rejected: - // Not H2 or H1. - return .unknown - } - } - } - - private static let http1_1 = [ - UInt8(ascii: "H"), - UInt8(ascii: "T"), - UInt8(ascii: "T"), - UInt8(ascii: "P"), - UInt8(ascii: "/"), - UInt8(ascii: "1"), - UInt8(ascii: "."), - UInt8(ascii: "1"), - ] - - /// Determines whether the bytes in the `ByteBuffer` are prefixed with an HTTP/1.1 request line. - static func prefixedWithHTTP1RequestLine(_ buffer: ByteBuffer) -> SubParseResult { - var readableBytesView = buffer.readableBytesView - - // We don't need to validate the request line, only determine whether we think it's an HTTP1 - // request line. Another handler will parse it properly. - - // From RFC 2616 § 5.1: - // Request-Line = Method SP Request-URI SP HTTP-Version CRLF - - // Get through the first space. - guard readableBytesView.dropPrefix(through: UInt8(ascii: " ")) != nil else { - let tooLong = buffer.readableBytes > Self.maxLengthToCheck - return tooLong ? .rejected : .notEnoughBytes - } - - // Get through the second space. - guard readableBytesView.dropPrefix(through: UInt8(ascii: " ")) != nil else { - let tooLong = buffer.readableBytes > Self.maxLengthToCheck - return tooLong ? .rejected : .notEnoughBytes - } - - // +2 for \r\n - guard readableBytesView.count >= (Self.http1_1.count + 2) else { - return .notEnoughBytes - } - - guard let version = readableBytesView.dropPrefix(through: UInt8(ascii: "\r")), - readableBytesView.first == UInt8(ascii: "\n") - else { - // If we didn't drop the prefix OR we did and the next byte wasn't '\n', then we had enough - // bytes but the '\r\n' wasn't present: reject this as being HTTP1. - return .rejected - } - - return version.elementsEqual(Self.http1_1) ? .accepted : .rejected - } -} - -extension Collection where Self == Self.SubSequence, Self.Element: Equatable { - /// Drops the prefix off the collection up to and including the first `separator` - /// only if that separator appears in the collection. - /// - /// Returns the prefix up to but not including the separator if it was found, nil otherwise. - mutating func dropPrefix(through separator: Element) -> SubSequence? { - if self.isEmpty { - return nil - } - - guard let separatorIndex = self.firstIndex(of: separator) else { - return nil - } - - let prefix = self[.. GRPCServerHandlerProtocol? -} - -// This is public because it will be passed into generated code, all members are `internal` because -// the context will get passed from generated code back into gRPC library code and all members should -// be considered an implementation detail to the user. -public struct CallHandlerContext { - @usableFromInline - internal var errorDelegate: ServerErrorDelegate? - @usableFromInline - internal var logger: Logger - @usableFromInline - internal var encoding: ServerMessageEncoding - @usableFromInline - internal var eventLoop: EventLoop - @usableFromInline - internal var path: String - @usableFromInline - internal var remoteAddress: SocketAddress? - @usableFromInline - internal var responseWriter: GRPCServerResponseWriter - @usableFromInline - internal var allocator: ByteBufferAllocator - @usableFromInline - internal var closeFuture: EventLoopFuture -} - -/// A call URI split into components. -struct CallPath { - /// The name of the service to call. - var service: String.UTF8View.SubSequence - /// The name of the method to call. - var method: String.UTF8View.SubSequence - - /// Character used to split the path into components. - private let pathSplitDelimiter = UInt8(ascii: "/") - - /// Split a path into service and method. - /// Split is done in UTF8 as this turns out to be approximately 10x faster than a simple split. - /// URI format: "/package.Servicename/MethodName" - init?(requestURI: String) { - var utf8View = requestURI.utf8[...] - // Check and remove the split character at the beginning. - guard let prefix = utf8View.trimPrefix(to: self.pathSplitDelimiter), prefix.isEmpty else { - return nil - } - guard let service = utf8View.trimPrefix(to: pathSplitDelimiter) else { - return nil - } - guard let method = utf8View.trimPrefix(to: pathSplitDelimiter) else { - return nil - } - - self.service = service - self.method = method - } -} - -extension Collection where Self == Self.SubSequence, Self.Element: Equatable { - /// Trims out the prefix up to `separator`, and returns it. - /// Sets self to the subsequence after the separator, and returns the subsequence before the separator. - /// If self is empty returns `nil` - /// - parameters: - /// - separator : The Element between the head which is returned and the rest which is left in self. - /// - returns: SubSequence containing everything between the beginning and the first occurrence of - /// `separator`. If `separator` is not found this will be the entire Collection. If the collection is empty - /// returns `nil` - mutating func trimPrefix(to separator: Element) -> SubSequence? { - guard !self.isEmpty else { - return nil - } - if let separatorIndex = self.firstIndex(of: separator) { - let indexAfterSeparator = self.index(after: separatorIndex) - defer { self = self[indexAfterSeparator...] } - return self[.. - fileprivate var cause: Optional - - fileprivate static func makeStorage(message: String?, cause: Error?) -> Storage { - if message == nil, cause == nil { - return Storage.none - } else { - return Storage(message: message, cause: cause) - } - } - } - - /// Whether the status is '.ok'. - public var isOk: Bool { - return self.code == .ok - } - - public init(code: Code, message: String?) { - self.init(code: code, message: message, cause: nil) - } - - public init(code: Code, message: String? = nil, cause: Error? = nil) { - self.code = code - self.storage = .makeStorage(message: message, cause: cause) - } - - // Frequently used "default" statuses. - - /// The default status to return for succeeded calls. - /// - /// - Important: This should *not* be used when checking whether a returned status has an 'ok' - /// 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 = 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 { - public static func == (lhs: GRPCStatus, rhs: GRPCStatus) -> Bool { - return lhs.code == rhs.code && lhs.message == rhs.message - } -} - -extension GRPCStatus: CustomStringConvertible { - public var description: String { - 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)" - case let (.none, .some(cause)): - return "\(self.code), cause: \(cause)" - case (.none, .none): - return "\(self.code)" - } - } -} - -extension GRPCStatus { - internal var testingOnly_storageObjectIdentifier: ObjectIdentifier { - return ObjectIdentifier(self.storage) - } -} - -extension GRPCStatus { - /// Status codes for gRPC operations (replicated from `status_code_enum.h` in the - /// [gRPC core library](https://github.com/grpc/grpc)). - public struct Code: Hashable, CustomStringConvertible, Sendable { - // `rawValue` must be an `Int` for API reasons and we don't need (or want) to store anything so - // wide, a `UInt8` is fine. - private let _rawValue: UInt8 - - public var rawValue: Int { - return Int(self._rawValue) - } - - public init?(rawValue: Int) { - switch rawValue { - case 0 ... 16: - self._rawValue = UInt8(truncatingIfNeeded: rawValue) - default: - return nil - } - } - - private init(_ code: UInt8) { - self._rawValue = code - } - - /// Not an error; returned on success. - public static let ok = Code(0) - - /// The operation was cancelled (typically by the caller). - public static let cancelled = Code(1) - - /// Unknown error. An example of where this error may be returned is if a - /// Status value received from another address space belongs to an error-space - /// that is not known in this address space. Also errors raised by APIs that - /// do not return enough error information may be converted to this error. - public static let unknown = Code(2) - - /// Client specified an invalid argument. Note that this differs from - /// FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments that are - /// problematic regardless of the state of the system (e.g., a malformed file - /// name). - public static let invalidArgument = Code(3) - - /// Deadline expired before operation could complete. For operations that - /// change the state of the system, this error may be returned even if the - /// operation has completed successfully. For example, a successful response - /// from a server could have been delayed long enough for the deadline to - /// expire. - public static let deadlineExceeded = Code(4) - - /// Some requested entity (e.g., file or directory) was not found. - public static let notFound = Code(5) - - /// Some entity that we attempted to create (e.g., file or directory) already - /// exists. - public static let alreadyExists = Code(6) - - /// The caller does not have permission to execute the specified operation. - /// PERMISSION_DENIED must not be used for rejections caused by exhausting - /// some resource (use RESOURCE_EXHAUSTED instead for those errors). - /// PERMISSION_DENIED must not be used if the caller can not be identified - /// (use UNAUTHENTICATED instead for those errors). - public static let permissionDenied = Code(7) - - /// Some resource has been exhausted, perhaps a per-user quota, or perhaps the - /// entire file system is out of space. - public static let resourceExhausted = Code(8) - - /// Operation was rejected because the system is not in a state required for - /// the operation's execution. For example, directory to be deleted may be - /// non-empty, an rmdir operation is applied to a non-directory, etc. - /// - /// A litmus test that may help a service implementor in deciding - /// between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: - /// (a) Use UNAVAILABLE if the client can retry just the failing call. - /// (b) Use ABORTED if the client should retry at a higher-level - /// (e.g., restarting a read-modify-write sequence). - /// (c) Use FAILED_PRECONDITION if the client should not retry until - /// the system state has been explicitly fixed. E.g., if an "rmdir" - /// fails because the directory is non-empty, FAILED_PRECONDITION - /// should be returned since the client should not retry unless - /// they have first fixed up the directory by deleting files from it. - /// (d) Use FAILED_PRECONDITION if the client performs conditional - /// REST Get/Update/Delete on a resource and the resource on the - /// server does not match the condition. E.g., conflicting - /// read-modify-write on the same resource. - public static let failedPrecondition = Code(9) - - /// The operation was aborted, typically due to a concurrency issue like - /// sequencer check failures, transaction aborts, etc. - /// - /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, - /// and UNAVAILABLE. - public static let aborted = Code(10) - - /// Operation was attempted past the valid range. E.g., seeking or reading - /// past end of file. - /// - /// Unlike INVALID_ARGUMENT, this error indicates a problem that may be fixed - /// if the system state changes. For example, a 32-bit file system will - /// generate INVALID_ARGUMENT if asked to read at an offset that is not in the - /// range [0,2^32-1], but it will generate OUT_OF_RANGE if asked to read from - /// an offset past the current file size. - /// - /// There is a fair bit of overlap between FAILED_PRECONDITION and - /// OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific error) - /// when it applies so that callers who are iterating through a space can - /// easily look for an OUT_OF_RANGE error to detect when they are done. - public static let outOfRange = Code(11) - - /// Operation is not implemented or not supported/enabled in this service. - public static let unimplemented = Code(12) - - /// Internal errors. Means some invariants expected by underlying System has - /// been broken. If you see one of these errors, Something is very broken. - public static let internalError = Code(13) - - /// The service is currently unavailable. This is a most likely a transient - /// condition and may be corrected by retrying with a backoff. - /// - /// See litmus test above for deciding between FAILED_PRECONDITION, ABORTED, - /// and UNAVAILABLE. - public static let unavailable = Code(14) - - /// Unrecoverable data loss or corruption. - public static let dataLoss = Code(15) - - /// The request does not have valid authentication credentials for the - /// operation. - public static let unauthenticated = Code(16) - - public var description: String { - switch self { - case .ok: - return "ok (\(self._rawValue))" - case .cancelled: - return "cancelled (\(self._rawValue))" - case .unknown: - return "unknown (\(self._rawValue))" - case .invalidArgument: - return "invalid argument (\(self._rawValue))" - case .deadlineExceeded: - return "deadline exceeded (\(self._rawValue))" - case .notFound: - return "not found (\(self._rawValue))" - case .alreadyExists: - return "already exists (\(self._rawValue))" - case .permissionDenied: - return "permission denied (\(self._rawValue))" - case .resourceExhausted: - return "resource exhausted (\(self._rawValue))" - case .failedPrecondition: - return "failed precondition (\(self._rawValue))" - case .aborted: - return "aborted (\(self._rawValue))" - case .outOfRange: - return "out of range (\(self._rawValue))" - case .unimplemented: - return "unimplemented (\(self._rawValue))" - case .internalError: - return "internal error (\(self._rawValue))" - case .unavailable: - return "unavailable (\(self._rawValue))" - case .dataLoss: - return "data loss (\(self._rawValue))" - case .unauthenticated: - return "unauthenticated (\(self._rawValue))" - default: - return String(describing: self._rawValue) - } - } - } -} - -// `GRPCStatus` has CoW semantics so it is inherently `Sendable`. Rather than marking `GRPCStatus` -// as `@unchecked Sendable` we only mark `Storage` as such. -extension GRPCStatus.Storage: @unchecked Sendable {} - -/// This protocol serves as a customisation point for error types so that gRPC calls may be -/// terminated with an appropriate status. -public protocol GRPCStatusTransformable: Error { - /// Make a `GRPCStatus` from the underlying error. - /// - /// - Returns: A `GRPCStatus` representing the underlying error. - func makeGRPCStatus() -> GRPCStatus -} - -extension GRPCStatus: GRPCStatusTransformable { - public func makeGRPCStatus() -> GRPCStatus { - return self - } -} - -extension NIOHTTP2Errors.StreamClosed: GRPCStatusTransformable { - public func makeGRPCStatus() -> GRPCStatus { - return .init(code: .unavailable, message: self.localizedDescription, cause: self) - } -} - -extension NIOHTTP2Errors.IOOnClosedConnection: GRPCStatusTransformable { - public func makeGRPCStatus() -> GRPCStatus { - return .init(code: .unavailable, message: "The connection is closed", cause: self) - } -} - -extension ChannelError: GRPCStatusTransformable { - public func makeGRPCStatus() -> GRPCStatus { - switch self { - case .inputClosed, .outputClosed, .ioOnClosedChannel: - return .init(code: .unavailable, message: "The connection is closed", cause: self) - - default: - var processingError = GRPCStatus.processingError - processingError.cause = self - return processingError - } - } -} diff --git a/Sources/GRPC/GRPCStatusAndMetadata.swift b/Sources/GRPC/GRPCStatusAndMetadata.swift deleted file mode 100644 index 04b58c98a..000000000 --- a/Sources/GRPC/GRPCStatusAndMetadata.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020, 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 NIOHPACK -import NIOHTTP1 - -/// A simple struct holding a ``GRPCStatus`` and optionally trailers in the form of -/// `HPACKHeaders`. -public struct GRPCStatusAndTrailers: Equatable { - /// The status. - public var status: GRPCStatus - - /// The trailers. - public var trailers: HPACKHeaders? - - public init(status: GRPCStatus, trailers: HPACKHeaders? = nil) { - self.status = status - self.trailers = trailers - } -} diff --git a/Sources/GRPC/GRPCStatusMessageMarshaller.swift b/Sources/GRPC/GRPCStatusMessageMarshaller.swift deleted file mode 100644 index 827933054..000000000 --- a/Sources/GRPC/GRPCStatusMessageMarshaller.swift +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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. - */ - -// swiftformat:disable:next enumNamespaces -public struct GRPCStatusMessageMarshaller { - /// Adds percent encoding to the given message. - /// - /// - Parameter message: Message to percent encode. - /// - Returns: Percent encoded string, or `nil` if it could not be encoded. - public static func marshall(_ message: String) -> String? { - return percentEncode(message) - } - - /// Removes percent encoding from the given message. - /// - /// - Parameter message: Message to remove encoding from. - /// - Returns: The string with percent encoding removed, or the input string if the encoding - /// could not be removed. - public static func unmarshall(_ message: String) -> String { - return removePercentEncoding(message) - } -} - -extension GRPCStatusMessageMarshaller { - /// Adds percent encoding to the given message. - /// - /// gRPC uses percent encoding as defined in RFC 3986 § 2.1 but with a different set of restricted - /// characters. The allowed characters are all visible printing characters except for (`%`, - /// `0x25`). That is: `0x20`-`0x24`, `0x26`-`0x7E`. - /// - /// - Parameter message: The message to encode. - /// - Returns: Percent encoded string, or `nil` if it could not be encoded. - private static func percentEncode(_ message: String) -> String? { - let utf8 = message.utf8 - - let encodedLength = self.percentEncodedLength(for: utf8) - // Fast-path: all characters are valid, nothing to encode. - if encodedLength == utf8.count { - return message - } - - var bytes: [UInt8] = [] - bytes.reserveCapacity(encodedLength) - - for char in message.utf8 { - switch char { - // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses - case 0x20 ... 0x24, - 0x26 ... 0x7E: - bytes.append(char) - - default: - bytes.append(UInt8(ascii: "%")) - bytes.append(self.toHex(char >> 4)) - bytes.append(self.toHex(char & 0xF)) - } - } - - return String(bytes: bytes, encoding: .utf8) - } - - /// Returns the percent encoded length of the given `UTF8View`. - private static func percentEncodedLength(for view: String.UTF8View) -> Int { - var count = view.count - for byte in view { - switch byte { - case 0x20 ... 0x24, - 0x26 ... 0x7E: - () - - default: - count += 2 - } - } - return count - } - - /// Encode the given byte as hexadecimal. - /// - /// - Precondition: Only the four least significant bits may be set. - /// - Parameter nibble: The nibble to convert to hexadecimal. - private static func toHex(_ nibble: UInt8) -> UInt8 { - assert(nibble & 0xF == nibble) - - switch nibble { - case 0 ... 9: - return nibble &+ UInt8(ascii: "0") - default: - return nibble &+ (UInt8(ascii: "A") &- 10) - } - } - - /// Remove gRPC percent encoding from `message`. If any portion of the string could not be decoded - /// then the encoded message will be returned. - /// - /// - Parameter message: The message to remove percent encoding from. - /// - Returns: The decoded message. - private static func removePercentEncoding(_ message: String) -> String { - let utf8 = message.utf8 - - let decodedLength = self.percentDecodedLength(for: utf8) - // Fast-path: no decoding to do! Note that we may also have detected that the encoding is - // invalid, in which case we will return the encoded message: this is fine. - if decodedLength == utf8.count { - return message - } - - var chars: [UInt8] = [] - // We can't decode more characters than are already encoded. - chars.reserveCapacity(decodedLength) - - var currentIndex = utf8.startIndex - let endIndex = utf8.endIndex - - while currentIndex < endIndex { - let byte = utf8[currentIndex] - - switch byte { - case UInt8(ascii: "%"): - guard let (nextIndex, nextNextIndex) = utf8.nextTwoIndices(after: currentIndex), - let nextHex = fromHex(utf8[nextIndex]), - let nextNextHex = fromHex(utf8[nextNextIndex]) - else { - // If we can't decode the message, aborting and returning the encoded message is fine - // according to the spec. - return message - } - chars.append((nextHex << 4) | nextNextHex) - currentIndex = nextNextIndex - - default: - chars.append(byte) - } - - currentIndex = utf8.index(after: currentIndex) - } - - return String(decoding: chars, as: Unicode.UTF8.self) - } - - /// Returns the expected length of the decoded `UTF8View`. - private static func percentDecodedLength(for view: String.UTF8View) -> Int { - var encoded = 0 - - for byte in view { - switch byte { - case UInt8(ascii: "%"): - // This can't overflow since it can't be larger than view.count. - encoded &+= 1 - - default: - () - } - } - - let notEncoded = view.count - (encoded * 3) - - guard notEncoded >= 0 else { - // We've received gibberish: more '%' than expected. gRPC allows for the status message to - // be left encoded should it be incorrectly encoded. We'll do exactly that by returning - // the number of bytes in the view which will causes us to take the fast-path exit. - return view.count - } - - return notEncoded + encoded - } - - private static func fromHex(_ byte: UInt8) -> UInt8? { - switch byte { - case UInt8(ascii: "0") ... UInt8(ascii: "9"): - return byte &- UInt8(ascii: "0") - case UInt8(ascii: "A") ... UInt8(ascii: "Z"): - return byte &- (UInt8(ascii: "A") &- 10) - case UInt8(ascii: "a") ... UInt8(ascii: "z"): - return byte &- (UInt8(ascii: "a") &- 10) - default: - return nil - } - } -} - -extension String.UTF8View { - /// Return the next two valid indices after the given index. The indices are considered valid if - /// they less than `endIndex`. - fileprivate func nextTwoIndices(after index: Index) -> (Index, Index)? { - let secondIndex = self.index(index, offsetBy: 2) - guard secondIndex < self.endIndex else { - return nil - } - - return (self.index(after: index), secondIndex) - } -} diff --git a/Sources/GRPC/GRPCTLSConfiguration.swift b/Sources/GRPC/GRPCTLSConfiguration.swift deleted file mode 100644 index 902b4fc97..000000000 --- a/Sources/GRPC/GRPCTLSConfiguration.swift +++ /dev/null @@ -1,716 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOCore -import NIOSSL -#endif - -#if canImport(Network) -import Network -import NIOTransportServices -import Security -#endif - -/// TLS configuration. -/// -/// This structure allow configuring TLS for a wide range of TLS implementations. Some -/// options are removed from the user's control to ensure the configuration complies with -/// the gRPC specification. -public struct GRPCTLSConfiguration: Sendable { - fileprivate enum Backend: Sendable { - #if canImport(NIOSSL) - /// Configuration for NIOSSSL. - case nio(NIOConfiguration) - #endif - #if canImport(Network) - /// Configuration for Network.framework. - case network(NetworkConfiguration) - #endif - } - - /// The TLS backend. - private var backend: Backend - - #if canImport(NIOSSL) - fileprivate init(nio: NIOConfiguration) { - self.backend = .nio(nio) - } - #endif - - #if canImport(Network) - fileprivate init(network: NetworkConfiguration) { - self.backend = .network(network) - } - #endif - - /// Return the configuration for NIOSSL or `nil` if Network.framework is being used as the - /// TLS backend. - #if canImport(NIOSSL) - internal var nioConfiguration: NIOConfiguration? { - switch self.backend { - case let .nio(configuration): - return configuration - #if canImport(Network) - case .network: - return nil - #endif - } - } - #endif // canImport(NIOSSL) - - internal var isNetworkFrameworkTLSBackend: Bool { - switch self.backend { - #if canImport(NIOSSL) - case .nio: - return false - #endif - #if canImport(Network) - case .network: - return true - #endif - } - } - - /// The server hostname override as used by the TLS SNI extension. - /// - /// This value is ignored when the configuration is used for a server. - /// - /// - Note: when using the Network.framework backend, this value may not be set to `nil`. - internal var hostnameOverride: String? { - get { - switch self.backend { - #if canImport(NIOSSL) - case let .nio(config): - return config.hostnameOverride - #endif - - #if canImport(Network) - case let .network(config): - return config.hostnameOverride - #endif - } - } - - set { - switch self.backend { - #if canImport(NIOSSL) - case var .nio(config): - config.hostnameOverride = newValue - self.backend = .nio(config) - #endif - - #if canImport(Network) - case var .network(config): - if #available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) { - if let hostnameOverride = newValue { - config.updateHostnameOverride(to: hostnameOverride) - } else { - // We can't unset the value so error instead. - fatalError("Can't unset hostname override when using Network.framework TLS backend.") - // FIXME: lazily set the value on the backend when applying the options. - } - } else { - // We can only make the `.network` backend if we meet the above availability checks so - // this should be unreachable. - preconditionFailure() - } - self.backend = .network(config) - #endif - } - } - } - - /// Whether the configuration requires ALPN to be used. - /// - /// The Network.framework backend does not support this option and always requires ALPN. - internal var requireALPN: Bool { - get { - switch self.backend { - #if canImport(NIOSSL) - case let .nio(config): - return config.requireALPN - #endif - - #if canImport(Network) - case .network: - return true - #endif - } - } - set { - switch self.backend { - #if canImport(NIOSSL) - case var .nio(config): - config.requireALPN = newValue - self.backend = .nio(config) - #endif - - #if canImport(Network) - case .network: - () - #endif - } - } - } - - #if canImport(NIOSSL) - // Marked to silence the deprecation warning - @available(*, deprecated) - internal init(transforming deprecated: ClientConnection.Configuration.TLS) { - self.backend = .nio( - .init( - configuration: deprecated.configuration, - customVerificationCallback: deprecated.customVerificationCallback, - hostnameOverride: deprecated.hostnameOverride, - requireALPN: false // Not currently supported. - ) - ) - } - - // Marked to silence the deprecation warning - @available(*, deprecated) - internal init(transforming deprecated: Server.Configuration.TLS) { - self.backend = .nio( - .init(configuration: deprecated.configuration, requireALPN: deprecated.requireALPN) - ) - } - - @available(*, deprecated) - internal var asDeprecatedClientConfiguration: ClientConnection.Configuration.TLS? { - if case let .nio(config) = self.backend { - var tls = ClientConnection.Configuration.TLS( - configuration: config.configuration, - hostnameOverride: config.hostnameOverride - ) - tls.customVerificationCallback = config.customVerificationCallback - return tls - } - - return nil - } - - @available(*, deprecated) - internal var asDeprecatedServerConfiguration: Server.Configuration.TLS? { - if case let .nio(config) = self.backend { - return Server.Configuration.TLS(configuration: config.configuration) - } - return nil - } - #endif // canImport(NIOSSL) -} - -// MARK: - NIO Backend - -// canImport(NIOSSL) -#if canImport(NIOSSL) -extension GRPCTLSConfiguration { - internal struct NIOConfiguration { - var configuration: TLSConfiguration - var customVerificationCallback: NIOSSLCustomVerificationCallback? - var hostnameOverride: String? - // The client doesn't support this yet (https://github.com/grpc/grpc-swift/issues/1042). - var requireALPN: Bool - } - - /// TLS Configuration with suitable defaults for clients, using `NIOSSL`. - /// - /// This is a wrapper around `NIOSSL.TLSConfiguration` to restrict input to values which comply - /// with the gRPC protocol. - /// - /// - Parameter certificateChain: The certificate to offer during negotiation, defaults to an - /// empty array. - /// - Parameter privateKey: The private key associated with the leaf certificate. This defaults - /// to `nil`. - /// - Parameter trustRoots: The trust roots to validate certificates, this defaults to using a - /// root provided by the platform. - /// - Parameter certificateVerification: Whether to verify the remote certificate. Defaults to - /// `.fullVerification`. - /// - Parameter hostnameOverride: Value to use for TLS SNI extension; this must not be an IP - /// address, defaults to `nil`. - /// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic, - /// defaults to `nil`. - public static func makeClientConfigurationBackedByNIOSSL( - certificateChain: [NIOSSLCertificateSource] = [], - privateKey: NIOSSLPrivateKeySource? = nil, - trustRoots: NIOSSLTrustRoots = .default, - certificateVerification: CertificateVerification = .fullVerification, - hostnameOverride: String? = nil, - customVerificationCallback: NIOSSLCustomVerificationCallback? = nil - ) -> GRPCTLSConfiguration { - var configuration = TLSConfiguration.makeClientConfiguration() - configuration.minimumTLSVersion = .tlsv12 - configuration.certificateVerification = certificateVerification - configuration.trustRoots = trustRoots - configuration.certificateChain = certificateChain - configuration.privateKey = privateKey - configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.client - - return GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - configuration: configuration, - hostnameOverride: hostnameOverride, - customVerificationCallback: customVerificationCallback - ) - } - - /// Creates a gRPC TLS Configuration using the given `NIOSSL.TLSConfiguration`. - /// - /// - Note: If no ALPN tokens are set in `configuration.applicationProtocols` then "grpc-exp" - /// and "h2" will be used. - /// - Parameters: - /// - configuration: The `NIOSSL.TLSConfiguration` to base this configuration on. - /// - hostnameOverride: The hostname override to use for the TLS SNI extension. - public static func makeClientConfigurationBackedByNIOSSL( - configuration: TLSConfiguration, - hostnameOverride: String? = nil, - customVerificationCallback: NIOSSLCustomVerificationCallback? = nil - ) -> GRPCTLSConfiguration { - var configuration = configuration - - // Set the ALPN tokens if none were set. - if configuration.applicationProtocols.isEmpty { - configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.client - } - - let nioConfiguration = NIOConfiguration( - configuration: configuration, - customVerificationCallback: customVerificationCallback, - hostnameOverride: hostnameOverride, - requireALPN: false // We don't currently support this. - ) - - return GRPCTLSConfiguration(nio: nioConfiguration) - } - - /// TLS Configuration with suitable defaults for servers. - /// - /// This is a wrapper around `NIOSSL.TLSConfiguration` to restrict input to values which comply - /// with the gRPC protocol. - /// - /// - Parameter certificateChain: The certificate to offer during negotiation. - /// - Parameter privateKey: The private key associated with the leaf certificate. - /// - Parameter trustRoots: The trust roots to validate certificates, this defaults to using a - /// root provided by the platform. - /// - Parameter certificateVerification: Whether to verify the remote certificate. Defaults to - /// `.none`. - /// - Parameter requireALPN: Whether ALPN is required or not. - public static func makeServerConfigurationBackedByNIOSSL( - certificateChain: [NIOSSLCertificateSource], - privateKey: NIOSSLPrivateKeySource, - trustRoots: NIOSSLTrustRoots = .default, - certificateVerification: CertificateVerification = .none, - requireALPN: Bool = true - ) -> GRPCTLSConfiguration { - return Self.makeServerConfigurationBackedByNIOSSL( - certificateChain: certificateChain, - privateKey: privateKey, - trustRoots: trustRoots, - certificateVerification: certificateVerification, - requireALPN: requireALPN, - customVerificationCallback: nil - ) - } - - /// TLS Configuration with suitable defaults for servers. - /// - /// This is a wrapper around `NIOSSL.TLSConfiguration` to restrict input to values which comply - /// with the gRPC protocol. - /// - /// - Parameter certificateChain: The certificate to offer during negotiation. - /// - Parameter privateKey: The private key associated with the leaf certificate. - /// - Parameter trustRoots: The trust roots to validate certificates, this defaults to using a - /// root provided by the platform. - /// - Parameter certificateVerification: Whether to verify the remote certificate. Defaults to - /// `.none`. - /// - Parameter requireALPN: Whether ALPN is required or not. - /// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic, - /// defaults to `nil`. - public static func makeServerConfigurationBackedByNIOSSL( - certificateChain: [NIOSSLCertificateSource], - privateKey: NIOSSLPrivateKeySource, - trustRoots: NIOSSLTrustRoots = .default, - certificateVerification: CertificateVerification = .none, - requireALPN: Bool = true, - customVerificationCallback: NIOSSLCustomVerificationCallback? = nil - ) -> GRPCTLSConfiguration { - var configuration = TLSConfiguration.makeServerConfiguration( - certificateChain: certificateChain, - privateKey: privateKey - ) - - configuration.minimumTLSVersion = .tlsv12 - configuration.certificateVerification = certificateVerification - configuration.trustRoots = trustRoots - configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.server - - return GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - configuration: configuration, - requireALPN: requireALPN, - customVerificationCallback: customVerificationCallback - ) - } - - /// Creates a gRPC TLS Configuration suitable for servers using the given - /// `NIOSSL.TLSConfiguration`. - /// - /// - Note: If no ALPN tokens are set in `configuration.applicationProtocols` then "grpc-exp", - /// "h2", and "http/1.1" will be used. - /// - Parameters: - /// - configuration: The `NIOSSL.TLSConfiguration` to base this configuration on. - /// - requiresALPN: Whether the server enforces ALPN. Defaults to `true`. - public static func makeServerConfigurationBackedByNIOSSL( - configuration: TLSConfiguration, - requireALPN: Bool = true - ) -> GRPCTLSConfiguration { - return Self.makeServerConfigurationBackedByNIOSSL( - configuration: configuration, - requireALPN: requireALPN, - customVerificationCallback: nil - ) - } - - /// Creates a gRPC TLS Configuration suitable for servers using the given - /// `NIOSSL.TLSConfiguration`. - /// - /// - Note: If no ALPN tokens are set in `configuration.applicationProtocols` then "grpc-exp", - /// "h2", and "http/1.1" will be used. - /// - Parameters: - /// - configuration: The `NIOSSL.TLSConfiguration` to base this configuration on. - /// - requiresALPN: Whether the server enforces ALPN. Defaults to `true`. - /// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic, - /// defaults to `nil`. - public static func makeServerConfigurationBackedByNIOSSL( - configuration: TLSConfiguration, - requireALPN: Bool = true, - customVerificationCallback: NIOSSLCustomVerificationCallback? = nil - ) -> GRPCTLSConfiguration { - var configuration = configuration - - // Set the ALPN tokens if none were set. - if configuration.applicationProtocols.isEmpty { - configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.server - } - - let nioConfiguration = NIOConfiguration( - configuration: configuration, - customVerificationCallback: customVerificationCallback, - hostnameOverride: nil, - requireALPN: requireALPN - ) - - return GRPCTLSConfiguration(nio: nioConfiguration) - } - - @usableFromInline - internal func makeNIOSSLContext() throws -> NIOSSLContext? { - switch self.backend { - case let .nio(configuration): - return try NIOSSLContext(configuration: configuration.configuration) - #if canImport(Network) - case .network: - return nil - #endif - } - } - - internal var nioSSLCustomVerificationCallback: NIOSSLCustomVerificationCallback? { - switch self.backend { - case let .nio(configuration): - return configuration.customVerificationCallback - #if canImport(Network) - case .network: - return nil - #endif - } - } - - internal mutating func updateNIOCertificateChain(to certificateChain: [NIOSSLCertificate]) { - self.modifyingNIOConfiguration { - $0.configuration.certificateChain = certificateChain.map { .certificate($0) } - } - } - - internal mutating func updateNIOPrivateKey(to privateKey: NIOSSLPrivateKey) { - self.modifyingNIOConfiguration { - $0.configuration.privateKey = .privateKey(privateKey) - } - } - - internal mutating func updateNIOTrustRoots(to trustRoots: NIOSSLTrustRoots) { - self.modifyingNIOConfiguration { - $0.configuration.trustRoots = trustRoots - } - } - - internal mutating func updateNIOCertificateVerification( - to verification: CertificateVerification - ) { - self.modifyingNIOConfiguration { - $0.configuration.certificateVerification = verification - } - } - - internal mutating func updateNIOCustomVerificationCallback( - to callback: @escaping NIOSSLCustomVerificationCallback - ) { - self.modifyingNIOConfiguration { - $0.customVerificationCallback = callback - } - } - - private mutating func modifyingNIOConfiguration(_ modify: (inout NIOConfiguration) -> Void) { - switch self.backend { - case var .nio(configuration): - modify(&configuration) - self.backend = .nio(configuration) - #if canImport(Network) - case .network: - preconditionFailure() - #endif - } - } -} -#endif - -// MARK: - Network Backend - -#if canImport(Network) -extension GRPCTLSConfiguration { - internal struct NetworkConfiguration { - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - internal var options: NWProtocolTLS.Options { - get { - return self._options as! NWProtocolTLS.Options - } - set { - self._options = newValue - } - } - - /// Always a NWProtocolTLS.Options. - /// - /// This somewhat insane type-erasure is necessary because we need to availability-guard the NWProtocolTLS.Options - /// (it isn't available in older SDKs), but we cannot have stored properties guarded by availability in this way, only - /// computed ones. To that end, we have to erase the type and then un-erase it. This is fairly silly. - private var _options: Any - - // This is set privately via `updateHostnameOverride(to:)` because we require availability - // guards to update the value in the underlying `sec_protocol_options`. - internal private(set) var hostnameOverride: String? - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - init(options: NWProtocolTLS.Options, hostnameOverride: String?) { - self._options = options - self.hostnameOverride = hostnameOverride - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - internal mutating func updateHostnameOverride(to hostnameOverride: String) { - self.hostnameOverride = hostnameOverride - sec_protocol_options_set_tls_server_name( - self.options.securityProtocolOptions, - hostnameOverride - ) - } - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public static func makeClientConfigurationBackedByNetworkFramework( - identity: SecIdentity? = nil, - hostnameOverride: String? = nil, - verifyCallbackWithQueue: (sec_protocol_verify_t, DispatchQueue)? = nil - ) -> GRPCTLSConfiguration { - let options = NWProtocolTLS.Options() - - if let identity = identity { - sec_protocol_options_set_local_identity( - options.securityProtocolOptions, - sec_identity_create(identity)! - ) - } - - if let hostnameOverride = hostnameOverride { - sec_protocol_options_set_tls_server_name( - options.securityProtocolOptions, - hostnameOverride - ) - } - - if let verifyCallbackWithQueue = verifyCallbackWithQueue { - sec_protocol_options_set_verify_block( - options.securityProtocolOptions, - verifyCallbackWithQueue.0, - verifyCallbackWithQueue.1 - ) - } - - if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12) - } else { - sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, .tlsProtocol12) - } - - for `protocol` in GRPCApplicationProtocolIdentifier.client { - sec_protocol_options_add_tls_application_protocol( - options.securityProtocolOptions, - `protocol` - ) - } - - return .makeClientConfigurationBackedByNetworkFramework( - options: options, - hostnameOverride: hostnameOverride - ) - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public static func makeClientConfigurationBackedByNetworkFramework( - options: NWProtocolTLS.Options, - hostnameOverride: String? = nil - ) -> GRPCTLSConfiguration { - let network = NetworkConfiguration(options: options, hostnameOverride: hostnameOverride) - return GRPCTLSConfiguration(network: network) - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public static func makeServerConfigurationBackedByNetworkFramework( - identity: SecIdentity - ) -> GRPCTLSConfiguration { - let options = NWProtocolTLS.Options() - - sec_protocol_options_set_local_identity( - options.securityProtocolOptions, - sec_identity_create(identity)! - ) - - if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { - sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12) - } else { - sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, .tlsProtocol12) - } - - for `protocol` in GRPCApplicationProtocolIdentifier.server { - sec_protocol_options_add_tls_application_protocol( - options.securityProtocolOptions, - `protocol` - ) - } - - return GRPCTLSConfiguration.makeServerConfigurationBackedByNetworkFramework(options: options) - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public static func makeServerConfigurationBackedByNetworkFramework( - options: NWProtocolTLS.Options - ) -> GRPCTLSConfiguration { - let network = NetworkConfiguration(options: options, hostnameOverride: nil) - return GRPCTLSConfiguration(network: network) - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - internal mutating func updateNetworkLocalIdentity(to identity: SecIdentity) { - self.modifyingNetworkConfiguration { - sec_protocol_options_set_local_identity( - $0.options.securityProtocolOptions, - sec_identity_create(identity)! - ) - } - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - internal mutating func updateNetworkVerifyCallbackWithQueue( - callback: @escaping sec_protocol_verify_t, - queue: DispatchQueue - ) { - self.modifyingNetworkConfiguration { - sec_protocol_options_set_verify_block( - $0.options.securityProtocolOptions, - callback, - queue - ) - } - } - - private mutating func modifyingNetworkConfiguration( - _ modify: (inout NetworkConfiguration) -> Void - ) { - switch self.backend { - case var .network(_configuration): - modify(&_configuration) - self.backend = .network(_configuration) - #if canImport(NIOSSL) - case .nio: - preconditionFailure() - #endif // canImport(NIOSSL) - } - } -} -#endif - -#if canImport(Network) -extension GRPCTLSConfiguration { - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - internal func applyNetworkTLSOptions( - to bootstrap: NIOTSConnectionBootstrap - ) -> NIOTSConnectionBootstrap { - switch self.backend { - case let .network(_configuration): - return bootstrap.tlsOptions(_configuration.options) - - #if canImport(NIOSSL) - case .nio: - // We're using NIOSSL with Network.framework; that's okay and permitted for backwards - // compatibility. - return bootstrap - #endif // canImport(NIOSSL) - } - } - - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - internal func applyNetworkTLSOptions( - to bootstrap: NIOTSListenerBootstrap - ) -> NIOTSListenerBootstrap { - switch self.backend { - case let .network(_configuration): - return bootstrap.tlsOptions(_configuration.options) - - #if canImport(NIOSSL) - case .nio: - // We're using NIOSSL with Network.framework; that's okay and permitted for backwards - // compatibility. - return bootstrap - #endif // canImport(NIOSSL) - } - } -} - -@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) -extension NIOTSConnectionBootstrap { - internal func tlsOptions( - from _configuration: GRPCTLSConfiguration - ) -> NIOTSConnectionBootstrap { - return _configuration.applyNetworkTLSOptions(to: self) - } -} - -@available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) -extension NIOTSListenerBootstrap { - internal func tlsOptions( - from _configuration: GRPCTLSConfiguration - ) -> NIOTSListenerBootstrap { - return _configuration.applyNetworkTLSOptions(to: self) - } -} -#endif diff --git a/Sources/GRPC/GRPCTimeout.swift b/Sources/GRPC/GRPCTimeout.swift deleted file mode 100644 index fecb22283..000000000 --- a/Sources/GRPC/GRPCTimeout.swift +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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 Dispatch -import NIOCore - -/// A timeout for a gRPC call. -/// -/// Timeouts must be positive and at most 8-digits long. -public struct GRPCTimeout: CustomStringConvertible, Equatable { - /// Creates an infinite timeout. This is a sentinel value which must __not__ be sent to a gRPC service. - public static let infinite = GRPCTimeout( - nanoseconds: Int64.max, - wireEncoding: "infinite" - ) - - /// The largest amount of any unit of time which may be represented by a gRPC timeout. - internal static let maxAmount: Int64 = 99_999_999 - - /// The wire encoding of this timeout as described in the gRPC protocol. - /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md. - public let wireEncoding: String - public let nanoseconds: Int64 - - public var description: String { - return self.wireEncoding - } - - /// Creates a timeout from the given deadline. - /// - /// - Parameter deadline: The deadline to create a timeout from. - internal init(deadline: NIODeadline, testingOnlyNow: NIODeadline? = nil) { - switch deadline { - case .distantFuture: - self = .infinite - default: - let timeAmountUntilDeadline = deadline - (testingOnlyNow ?? .now()) - self.init(rounding: timeAmountUntilDeadline.nanoseconds, unit: .nanoseconds) - } - } - - private init(nanoseconds: Int64, wireEncoding: String) { - self.nanoseconds = nanoseconds - self.wireEncoding = wireEncoding - } - - /// Creates a `GRPCTimeout`. - /// - /// - Precondition: The amount should be greater than or equal to zero and less than or equal - /// to `GRPCTimeout.maxAmount`. - internal init(amount: Int64, unit: GRPCTimeoutUnit) { - precondition(amount >= 0 && amount <= GRPCTimeout.maxAmount) - // See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests - - // If we overflow at this point, which is certainly possible if `amount` is sufficiently large - // and `unit` is `.hours`, clamp the nanosecond timeout to `Int64.max`. It's about 292 years so - // it should be long enough for the user not to notice the difference should the rpc time out. - let (partial, overflow) = amount.multipliedReportingOverflow(by: unit.asNanoseconds) - - self.init( - nanoseconds: overflow ? Int64.max : partial, - wireEncoding: "\(amount)\(unit.rawValue)" - ) - } - - /// Create a timeout by rounding up the timeout so that it may be represented in the gRPC - /// wire format. - internal init(rounding amount: Int64, unit: GRPCTimeoutUnit) { - var roundedAmount = amount - var roundedUnit = unit - - if roundedAmount <= 0 { - roundedAmount = 0 - } else { - while roundedAmount > GRPCTimeout.maxAmount { - switch roundedUnit { - case .nanoseconds: - roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000) - roundedUnit = .microseconds - case .microseconds: - roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000) - roundedUnit = .milliseconds - case .milliseconds: - roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000) - roundedUnit = .seconds - case .seconds: - roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60) - roundedUnit = .minutes - case .minutes: - roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60) - roundedUnit = .hours - case .hours: - roundedAmount = GRPCTimeout.maxAmount - roundedUnit = .hours - } - } - } - - self.init(amount: roundedAmount, unit: roundedUnit) - } -} - -extension Int64 { - /// Returns the quotient of this value when divided by `divisor` rounded up to the nearest - /// multiple of `divisor` if the remainder is non-zero. - /// - /// - Parameter divisor: The value to divide this value by. - fileprivate func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 { - let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor) - return quotient + (remainder != 0 ? 1 : 0) - } -} - -internal enum GRPCTimeoutUnit: String { - case hours = "H" - case minutes = "M" - case seconds = "S" - case milliseconds = "m" - case microseconds = "u" - case nanoseconds = "n" - - internal var asNanoseconds: Int64 { - switch self { - case .hours: - return 60 * 60 * 1000 * 1000 * 1000 - - case .minutes: - return 60 * 1000 * 1000 * 1000 - - case .seconds: - return 1000 * 1000 * 1000 - - case .milliseconds: - return 1000 * 1000 - - case .microseconds: - return 1000 - - case .nanoseconds: - return 1 - } - } -} diff --git a/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift b/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift deleted file mode 100644 index 976be10c1..000000000 --- a/Sources/GRPC/GRPCWebToHTTP2ServerCodec.swift +++ /dev/null @@ -1,788 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 - -import struct Foundation.Data - -/// A codec for translating between gRPC Web (as HTTP/1) and HTTP/2 frame payloads. -internal final class GRPCWebToHTTP2ServerCodec: ChannelDuplexHandler { - internal typealias InboundIn = HTTPServerRequestPart - internal typealias InboundOut = HTTP2Frame.FramePayload - - internal typealias OutboundIn = HTTP2Frame.FramePayload - internal typealias OutboundOut = HTTPServerResponsePart - - private var stateMachine: StateMachine - - /// Create a gRPC Web to server HTTP/2 codec. - /// - /// - Parameter scheme: The value of the ':scheme' pseudo header to insert when converting the - /// request headers. - init(scheme: String) { - self.stateMachine = StateMachine(scheme: scheme) - } - - internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let action = self.stateMachine.processInbound( - serverRequestPart: self.unwrapInboundIn(data), - allocator: context.channel.allocator - ) - self.act(on: action, context: context) - } - - internal func write( - context: ChannelHandlerContext, - data: NIOAny, - promise: EventLoopPromise? - ) { - let action = self.stateMachine.processOutbound( - framePayload: self.unwrapOutboundIn(data), - promise: promise, - allocator: context.channel.allocator - ) - self.act(on: action, context: context) - } - - /// Acts on an action returned by the state machine. - private func act(on action: StateMachine.Action, context: ChannelHandlerContext) { - switch action { - case .none: - () - - case let .fireChannelRead(payload): - context.fireChannelRead(self.wrapInboundOut(payload)) - - case let .write(write): - if let additionalPart = write.additionalPart { - context.write(self.wrapOutboundOut(write.part), promise: nil) - context.write(self.wrapOutboundOut(additionalPart), promise: write.promise) - } else { - context.write(self.wrapOutboundOut(write.part), promise: write.promise) - } - - if write.closeChannel { - context.close(mode: .all, promise: nil) - } - - case let .completePromise(promise, result): - promise?.completeWith(result) - } - } -} - -extension GRPCWebToHTTP2ServerCodec { - internal struct StateMachine { - /// The current state. - private var state: State - private let scheme: String - - internal init(scheme: String) { - self.state = .idle - self.scheme = scheme - } - - /// Process the inbound `HTTPServerRequestPart`. - internal mutating func processInbound( - serverRequestPart: HTTPServerRequestPart, - allocator: ByteBufferAllocator - ) -> Action { - return self.state.processInbound( - serverRequestPart: serverRequestPart, - scheme: self.scheme, - allocator: allocator - ) - } - - /// Process the outbound `HTTP2Frame.FramePayload`. - internal mutating func processOutbound( - framePayload: HTTP2Frame.FramePayload, - promise: EventLoopPromise?, - allocator: ByteBufferAllocator - ) -> Action { - return self.state.processOutbound( - framePayload: framePayload, - promise: promise, - allocator: allocator - ) - } - - /// An action to take as a result of interaction with the state machine. - internal enum Action { - case none - case fireChannelRead(HTTP2Frame.FramePayload) - case write(Write) - case completePromise(EventLoopPromise?, Result) - - internal struct Write { - internal var part: HTTPServerResponsePart - internal var additionalPart: HTTPServerResponsePart? - internal var promise: EventLoopPromise? - internal var closeChannel: Bool - - internal init( - part: HTTPServerResponsePart, - additionalPart: HTTPServerResponsePart? = nil, - promise: EventLoopPromise?, - closeChannel: Bool - ) { - self.part = part - self.additionalPart = additionalPart - self.promise = promise - self.closeChannel = closeChannel - } - } - } - - fileprivate enum State { - /// Idle; nothing has been received or sent. The only valid transition is to 'fullyOpen' when - /// receiving request headers. - case idle - - /// Received request headers. Waiting for the end of request and response streams. - case fullyOpen(InboundState, OutboundState) - - /// The server has closed the response stream, we may receive other request parts from the client. - case clientOpenServerClosed(InboundState) - - /// The client has sent everything, the server still needs to close the response stream. - case clientClosedServerOpen(OutboundState) - - /// Not a real state. - case _modifying - - private var isModifying: Bool { - switch self { - case ._modifying: - return true - case .idle, .fullyOpen, .clientClosedServerOpen, .clientOpenServerClosed: - return false - } - } - - private mutating func withStateAvoidingCoWs(_ body: (inout State) -> Action) -> Action { - self = ._modifying - defer { - assert(!self.isModifying) - } - return body(&self) - } - } - - fileprivate struct InboundState { - /// A `ByteBuffer` containing the base64 encoded bytes of the request stream if gRPC Web Text - /// is being used, `nil` otherwise. - var requestBuffer: ByteBuffer? - - init(isTextEncoded: Bool, allocator: ByteBufferAllocator) { - self.requestBuffer = isTextEncoded ? allocator.buffer(capacity: 0) : nil - } - } - - fileprivate struct OutboundState { - /// A `CircularBuffer` holding any response messages if gRPC Web Text is being used, `nil` - /// otherwise. - var responseBuffer: CircularBuffer? - - /// True if the response headers have been sent. - var responseHeadersSent: Bool - - /// True if the server should close the connection when this request is done. - var closeConnection: Bool - - init(isTextEncoded: Bool, closeConnection: Bool) { - self.responseHeadersSent = false - self.responseBuffer = isTextEncoded ? CircularBuffer() : nil - self.closeConnection = closeConnection - } - } - } -} - -extension GRPCWebToHTTP2ServerCodec.StateMachine.State { - fileprivate mutating func processInbound( - serverRequestPart: HTTPServerRequestPart, - scheme: String, - allocator: ByteBufferAllocator - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch serverRequestPart { - case let .head(head): - return self.processRequestHead(head, scheme: scheme, allocator: allocator) - case var .body(buffer): - return self.processRequestBody(&buffer) - case .end: - return self.processRequestEnd(allocator: allocator) - } - } - - fileprivate mutating func processOutbound( - framePayload: HTTP2Frame.FramePayload, - promise: EventLoopPromise?, - allocator: ByteBufferAllocator - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch framePayload { - case let .headers(payload): - return self.processResponseHeaders(payload, promise: promise, allocator: allocator) - - case let .data(payload): - return self.processResponseData(payload, promise: promise) - - case .priority, - .rstStream, - .settings, - .pushPromise, - .ping, - .goAway, - .windowUpdate, - .alternativeService, - .origin: - preconditionFailure("Unsupported frame payload") - } - } -} - -// MARK: - Inbound - -extension GRPCWebToHTTP2ServerCodec.StateMachine.State { - private mutating func processRequestHead( - _ head: HTTPRequestHead, - scheme: String, - allocator: ByteBufferAllocator - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - return self.withStateAvoidingCoWs { state in - let normalized = HPACKHeaders(httpHeaders: head.headers, normalizeHTTPHeaders: true) - - // Regular headers need to come after the pseudo headers. Unfortunately, this means we need to - // allocate a second headers block to use the normalization provided by NIO HTTP/2. - // - // TODO: Use API provided by https://github.com/apple/swift-nio-http2/issues/254 to avoid the - // extra copy. - var headers = HPACKHeaders() - headers.reserveCapacity(normalized.count + 4) - headers.add(name: ":path", value: head.uri) - headers.add(name: ":method", value: head.method.rawValue) - headers.add(name: ":scheme", value: scheme) - if let host = head.headers.first(name: "host") { - headers.add(name: ":authority", value: host) - } - headers.add(contentsOf: normalized) - - // Check whether we're dealing with gRPC Web Text. No need to fully validate the content-type - // that will be done at the HTTP/2 level. - let contentType = headers.first(name: GRPCHeaderName.contentType).flatMap(ContentType.init) - let isWebText = contentType == .some(.webTextProtobuf) - - let closeConnection = head.headers[canonicalForm: "connection"].contains("close") - - state = .fullyOpen( - .init(isTextEncoded: isWebText, allocator: allocator), - .init(isTextEncoded: isWebText, closeConnection: closeConnection) - ) - return .fireChannelRead(.headers(.init(headers: headers))) - } - - case .fullyOpen, .clientOpenServerClosed, .clientClosedServerOpen: - preconditionFailure("Invalid state: already received request head") - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } - - private mutating func processRequestBody( - _ buffer: inout ByteBuffer - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - preconditionFailure("Invalid state: haven't received request head") - - case .fullyOpen(var inbound, let outbound): - return self.withStateAvoidingCoWs { state in - let action = inbound.processInboundData(buffer: &buffer) - state = .fullyOpen(inbound, outbound) - return action - } - - case var .clientOpenServerClosed(inbound): - // The server is already done, but it's not our place to drop the request. - return self.withStateAvoidingCoWs { state in - let action = inbound.processInboundData(buffer: &buffer) - state = .clientOpenServerClosed(inbound) - return action - } - - case .clientClosedServerOpen: - preconditionFailure("End of request stream already received") - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } - - private mutating func processRequestEnd( - allocator: ByteBufferAllocator - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - preconditionFailure("Invalid state: haven't received request head") - - case let .fullyOpen(_, outbound): - return self.withStateAvoidingCoWs { state in - // We're done with inbound state. - state = .clientClosedServerOpen(outbound) - - // Send an empty DATA frame with the end stream flag set. - let empty = allocator.buffer(capacity: 0) - return .fireChannelRead(.data(.init(data: .byteBuffer(empty), endStream: true))) - } - - case .clientClosedServerOpen: - preconditionFailure("End of request stream already received") - - case .clientOpenServerClosed: - return self.withStateAvoidingCoWs { state in - // Both sides are closed now, back to idle. Don't forget to pass on the .end, as - // it's necessary to communicate to the other peers that the response is done. - state = .idle - - // Send an empty DATA frame with the end stream flag set. - let empty = allocator.buffer(capacity: 0) - return .fireChannelRead(.data(.init(data: .byteBuffer(empty), endStream: true))) - } - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } -} - -// MARK: - Outbound - -extension GRPCWebToHTTP2ServerCodec.StateMachine.State { - private mutating func processResponseTrailers( - _ trailers: HPACKHeaders, - promise: EventLoopPromise?, - allocator: ByteBufferAllocator - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - preconditionFailure("Invalid state: haven't received request head") - - case .fullyOpen(let inbound, var outbound): - return self.withStateAvoidingCoWs { state in - // Double check these are trailers. - assert(outbound.responseHeadersSent) - - // We haven't seen the end of the request stream yet. - state = .clientOpenServerClosed(inbound) - - // Avoid CoW-ing the buffers. - let responseBuffers = outbound.responseBuffer - outbound.responseBuffer = nil - - return Self.processTrailers( - responseBuffers: responseBuffers, - trailers: trailers, - promise: promise, - allocator: allocator, - closeChannel: outbound.closeConnection - ) - } - - case var .clientClosedServerOpen(state): - return self.withStateAvoidingCoWs { nextState in - // Client is closed and now so is the server. - nextState = .idle - - // Avoid CoW-ing the buffers. - let responseBuffers = state.responseBuffer - state.responseBuffer = nil - - return Self.processTrailers( - responseBuffers: responseBuffers, - trailers: trailers, - promise: promise, - allocator: allocator, - closeChannel: state.closeConnection - ) - } - - case .clientOpenServerClosed: - preconditionFailure("Already seen end of response stream") - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } - - private static func processTrailers( - responseBuffers: CircularBuffer?, - trailers: HPACKHeaders, - promise: EventLoopPromise?, - allocator: ByteBufferAllocator, - closeChannel: Bool - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - if var responseBuffers = responseBuffers { - let buffer = GRPCWebToHTTP2ServerCodec.encodeResponsesAndTrailers( - &responseBuffers, - trailers: trailers, - allocator: allocator - ) - return .write( - .init( - part: .body(.byteBuffer(buffer)), - additionalPart: .end(nil), - promise: promise, - closeChannel: closeChannel - ) - ) - } else { - // No response buffer; plain gRPC Web. Trailers are encoded into the body as a regular - // length-prefixed message. - let buffer = GRPCWebToHTTP2ServerCodec.formatTrailers(trailers, allocator: allocator) - return .write( - .init( - part: .body(.byteBuffer(buffer)), - additionalPart: .end(nil), - promise: promise, - closeChannel: closeChannel - ) - ) - } - } - - private mutating func processResponseTrailersOnly( - _ trailers: HPACKHeaders, - promise: EventLoopPromise? - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - preconditionFailure("Invalid state: haven't received request head") - - case let .fullyOpen(inbound, outbound): - return self.withStateAvoidingCoWs { state in - // We still haven't seen the end of the request stream. - state = .clientOpenServerClosed(inbound) - - let head = GRPCWebToHTTP2ServerCodec.makeResponseHead( - hpackHeaders: trailers, - closeConnection: outbound.closeConnection - ) - - return .write( - .init( - part: .head(head), - additionalPart: .end(nil), - promise: promise, - closeChannel: outbound.closeConnection - ) - ) - } - - case let .clientClosedServerOpen(outbound): - return self.withStateAvoidingCoWs { state in - // We're done, back to idle. - state = .idle - - let head = GRPCWebToHTTP2ServerCodec.makeResponseHead( - hpackHeaders: trailers, - closeConnection: outbound.closeConnection - ) - - return .write( - .init( - part: .head(head), - additionalPart: .end(nil), - promise: promise, - closeChannel: outbound.closeConnection - ) - ) - } - - case .clientOpenServerClosed: - preconditionFailure("Already seen end of response stream") - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } - - private mutating func processResponseHeaders( - _ headers: HPACKHeaders, - promise: EventLoopPromise? - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - preconditionFailure("Invalid state: haven't received request head") - - case .fullyOpen(let inbound, var outbound): - return self.withStateAvoidingCoWs { state in - outbound.responseHeadersSent = true - state = .fullyOpen(inbound, outbound) - - let head = GRPCWebToHTTP2ServerCodec.makeResponseHead( - hpackHeaders: headers, - closeConnection: outbound.closeConnection - ) - return .write(.init(part: .head(head), promise: promise, closeChannel: false)) - } - - case var .clientClosedServerOpen(outbound): - return self.withStateAvoidingCoWs { state in - outbound.responseHeadersSent = true - state = .clientClosedServerOpen(outbound) - - let head = GRPCWebToHTTP2ServerCodec.makeResponseHead( - hpackHeaders: headers, - closeConnection: outbound.closeConnection - ) - return .write(.init(part: .head(head), promise: promise, closeChannel: false)) - } - - case .clientOpenServerClosed: - preconditionFailure("Already seen end of response stream") - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } - - private mutating func processResponseHeaders( - _ payload: HTTP2Frame.FramePayload.Headers, - promise: EventLoopPromise?, - allocator: ByteBufferAllocator - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - preconditionFailure("Invalid state: haven't received request head") - - case let .fullyOpen(_, outbound), - let .clientClosedServerOpen(outbound): - if outbound.responseHeadersSent { - // Headers have been sent, these must be trailers, so end stream must be set. - assert(payload.endStream) - return self.processResponseTrailers(payload.headers, promise: promise, allocator: allocator) - } else if payload.endStream { - // Headers haven't been sent yet and end stream is set: this is a trailers only response - // so we need to send 'end' as well. - return self.processResponseTrailersOnly(payload.headers, promise: promise) - } else { - return self.processResponseHeaders(payload.headers, promise: promise) - } - - case .clientOpenServerClosed: - // We've already sent end. - return .completePromise(promise, .failure(GRPCError.AlreadyComplete())) - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } - - private static func processResponseData( - _ payload: HTTP2Frame.FramePayload.Data, - promise: EventLoopPromise?, - state: inout GRPCWebToHTTP2ServerCodec.StateMachine.OutboundState - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - if state.responseBuffer == nil { - // Not gRPC Web Text; just write the body. - return .write(.init(part: .body(payload.data), promise: promise, closeChannel: false)) - } else { - switch payload.data { - case let .byteBuffer(buffer): - // '!' is fine, we checked above. - state.responseBuffer!.append(buffer) - - case .fileRegion: - preconditionFailure("Unexpected IOData.fileRegion") - } - - // The response is buffered, we can consider it dealt with. - return .completePromise(promise, .success(())) - } - } - - private mutating func processResponseData( - _ payload: HTTP2Frame.FramePayload.Data, - promise: EventLoopPromise? - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - switch self { - case .idle: - preconditionFailure("Invalid state: haven't received request head") - - case .fullyOpen(let inbound, var outbound): - return self.withStateAvoidingCoWs { state in - let action = Self.processResponseData(payload, promise: promise, state: &outbound) - state = .fullyOpen(inbound, outbound) - return action - } - - case var .clientClosedServerOpen(outbound): - return self.withStateAvoidingCoWs { state in - let action = Self.processResponseData(payload, promise: promise, state: &outbound) - state = .clientClosedServerOpen(outbound) - return action - } - - case .clientOpenServerClosed: - return .completePromise(promise, .failure(GRPCError.AlreadyComplete())) - - case ._modifying: - preconditionFailure("Left in modifying state") - } - } -} - -// MARK: - Helpers - -extension GRPCWebToHTTP2ServerCodec { - private static func makeResponseHead( - hpackHeaders: HPACKHeaders, - closeConnection: Bool - ) -> HTTPResponseHead { - var headers = HTTPHeaders(hpackHeaders: hpackHeaders) - - if closeConnection { - headers.add(name: "connection", value: "close") - } - - // Grab the status, if this is missing we've messed up in another handler. - guard let statusCode = hpackHeaders.first(name: ":status").flatMap(Int.init) else { - preconditionFailure("Invalid state: missing ':status' pseudo header") - } - - return HTTPResponseHead( - version: .init(major: 1, minor: 1), - status: .init(statusCode: statusCode), - headers: headers - ) - } - - private static func formatTrailers( - _ trailers: HPACKHeaders, - allocator: ByteBufferAllocator - ) -> ByteBuffer { - // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md - let length = trailers.reduce(0) { partial, trailer in - // +4 for: ":", " ", "\r", "\n" - return partial + trailer.name.utf8.count + trailer.value.utf8.count + 4 - } - var buffer = allocator.buffer(capacity: 5 + length) - - // Uncompressed trailer byte. - buffer.writeInteger(UInt8(0x80)) - // Length. - let lengthIndex = buffer.writerIndex - buffer.writeInteger(UInt32(0)) - - var bytesWritten = 0 - for (name, value, _) in trailers { - bytesWritten += buffer.writeString(name) - bytesWritten += buffer.writeString(": ") - bytesWritten += buffer.writeString(value) - bytesWritten += buffer.writeString("\r\n") - } - - buffer.setInteger(UInt32(bytesWritten), at: lengthIndex) - return buffer - } - - private static func encodeResponsesAndTrailers( - _ responses: inout CircularBuffer, - trailers: HPACKHeaders, - allocator: ByteBufferAllocator - ) -> ByteBuffer { - // We need to encode the trailers along with any responses we're holding. - responses.append(self.formatTrailers(trailers, allocator: allocator)) - - let capacity = responses.lazy.map { $0.readableBytes }.reduce(0, +) - // '!' is fine: responses isn't empty, we just appended the trailers. - var buffer = responses.popFirst()! - - // Accumulate all the buffers into a single 'Data'. Ideally we wouldn't copy back and forth - // but this is fine for now. - var accumulatedData = buffer.readData(length: buffer.readableBytes)! - accumulatedData.reserveCapacity(capacity) - while let buffer = responses.popFirst() { - accumulatedData.append(contentsOf: buffer.readableBytesView) - } - - // We can reuse the popped buffer. - let base64Encoded = accumulatedData.base64EncodedString() - buffer.clear(minimumCapacity: base64Encoded.utf8.count) - buffer.writeString(base64Encoded) - - return buffer - } -} - -extension GRPCWebToHTTP2ServerCodec.StateMachine.InboundState { - fileprivate mutating func processInboundData( - buffer: inout ByteBuffer - ) -> GRPCWebToHTTP2ServerCodec.StateMachine.Action { - if self.requestBuffer == nil { - // We're not dealing with gRPC Web Text: just forward the buffer. - return .fireChannelRead(.data(.init(data: .byteBuffer(buffer)))) - } - - if self.requestBuffer!.readableBytes == 0 { - self.requestBuffer = buffer - } else { - self.requestBuffer!.writeBuffer(&buffer) - } - - let readableBytes = self.requestBuffer!.readableBytes - // The length of base64 encoded data must be a multiple of 4. - let bytesToRead = readableBytes - (readableBytes % 4) - - let action: GRPCWebToHTTP2ServerCodec.StateMachine.Action - - if bytesToRead > 0, - let base64Encoded = self.requestBuffer!.readString(length: bytesToRead), - let base64Decoded = Data(base64Encoded: base64Encoded) - { - // Recycle the input buffer and restore the request buffer. - buffer.clear() - buffer.writeContiguousBytes(base64Decoded) - action = .fireChannelRead(.data(.init(data: .byteBuffer(buffer)))) - } else { - action = .none - } - - return action - } -} - -extension HTTPHeaders { - fileprivate init(hpackHeaders headers: HPACKHeaders) { - self.init() - self.reserveCapacity(headers.count) - - // Pseudo-headers are at the start of the block, so drop them and then add the remaining. - let regularHeaders = headers.drop { name, _, _ in - name.utf8.first == .some(UInt8(ascii: ":")) - }.lazy.map { name, value, _ in - (name, value) - } - - self.add(contentsOf: regularHeaders) - } -} diff --git a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift b/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift deleted file mode 100644 index 49c13597f..000000000 --- a/Sources/GRPC/HTTP2ToRawGRPCServerCodec.swift +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -internal final class HTTP2ToRawGRPCServerCodec: ChannelInboundHandler, GRPCServerResponseWriter { - typealias InboundIn = HTTP2Frame.FramePayload - typealias OutboundOut = HTTP2Frame.FramePayload - - private var logger: Logger - private var state: HTTP2ToRawGRPCStateMachine - private let errorDelegate: ServerErrorDelegate? - private var context: ChannelHandlerContext! - - private let servicesByName: [Substring: CallHandlerProvider] - private let encoding: ServerMessageEncoding - private let normalizeHeaders: Bool - private let maxReceiveMessageLength: Int - - /// The configuration state of the handler. - private var configurationState: Configuration = .notConfigured - - /// Whether we are currently reading data from the `Channel`. Should be set to `false` once a - /// burst of reading has completed. - private var isReading = false - - /// Indicates whether a flush event is pending. If a flush is received while `isReading` is `true` - /// then it is held until the read completes in order to elide unnecessary flushes. - private var flushPending = false - - private enum Configuration { - case notConfigured - case configured(GRPCServerHandlerProtocol) - - var isConfigured: Bool { - switch self { - case .configured: - return true - case .notConfigured: - return false - } - } - - mutating func tearDown() -> GRPCServerHandlerProtocol? { - switch self { - case .notConfigured: - return nil - case let .configured(handler): - self = .notConfigured - return handler - } - } - } - - init( - servicesByName: [Substring: CallHandlerProvider], - encoding: ServerMessageEncoding, - errorDelegate: ServerErrorDelegate?, - normalizeHeaders: Bool, - maximumReceiveMessageLength: Int, - logger: Logger - ) { - self.logger = logger - self.errorDelegate = errorDelegate - self.servicesByName = servicesByName - self.encoding = encoding - self.normalizeHeaders = normalizeHeaders - self.maxReceiveMessageLength = maximumReceiveMessageLength - self.state = HTTP2ToRawGRPCStateMachine() - } - - internal func handlerAdded(context: ChannelHandlerContext) { - self.context = context - } - - internal func handlerRemoved(context: ChannelHandlerContext) { - self.context = nil - self.configurationState = .notConfigured - } - - internal func errorCaught(context: ChannelHandlerContext, error: Error) { - switch self.configurationState { - case .notConfigured: - context.close(mode: .all, promise: nil) - case let .configured(hander): - hander.receiveError(error) - } - } - - internal func channelInactive(context: ChannelHandlerContext) { - if let handler = self.configurationState.tearDown() { - handler.finish() - } else { - context.fireChannelInactive() - } - } - - internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - self.isReading = true - let payload = self.unwrapInboundIn(data) - - switch payload { - case let .headers(payload): - let receiveHeaders = self.state.receive( - headers: payload.headers, - eventLoop: context.eventLoop, - errorDelegate: self.errorDelegate, - remoteAddress: context.channel.remoteAddress, - logger: self.logger, - allocator: context.channel.allocator, - responseWriter: self, - closeFuture: context.channel.closeFuture, - services: self.servicesByName, - encoding: self.encoding, - normalizeHeaders: self.normalizeHeaders - ) - - switch receiveHeaders { - case let .configure(handler): - assert(!self.configurationState.isConfigured) - self.configurationState = .configured(handler) - self.configured() - - case let .rejectRPC(trailers): - assert(!self.configurationState.isConfigured) - // We're not handling this request: write headers and end stream. - let payload = HTTP2Frame.FramePayload.headers(.init(headers: trailers, endStream: true)) - context.writeAndFlush(self.wrapOutboundOut(payload), promise: nil) - } - - case let .data(payload): - switch payload.data { - case var .byteBuffer(buffer): - let action = self.state.receive(buffer: &buffer, endStream: payload.endStream) - switch action { - case .tryReading: - self.tryReadingMessage() - - case .finishHandler: - let handler = self.configurationState.tearDown() - handler?.finish() - - case .nothing: - () - } - - case .fileRegion: - preconditionFailure("Unexpected IOData.fileRegion") - } - - // Ignored. - case .alternativeService, - .goAway, - .origin, - .ping, - .priority, - .pushPromise, - .rstStream, - .settings, - .windowUpdate: - () - } - } - - internal func channelReadComplete(context: ChannelHandlerContext) { - self.isReading = false - - if self.flushPending { - self.deliverPendingResponses() - self.flushPending = false - context.flush() - } - - context.fireChannelReadComplete() - } - - private func deliverPendingResponses() { - while let (result, promise) = self.state.nextResponse() { - switch result { - case let .success(buffer): - let payload = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer))) - self.context.write(self.wrapOutboundOut(payload), promise: promise) - case let .failure(error): - promise?.fail(error) - } - } - } - - /// Called when the pipeline has finished configuring. - private func configured() { - switch self.state.pipelineConfigured() { - case let .forwardHeaders(headers): - switch self.configurationState { - case .notConfigured: - preconditionFailure() - case let .configured(handler): - handler.receiveMetadata(headers) - } - - case let .forwardHeadersAndRead(headers): - switch self.configurationState { - case .notConfigured: - preconditionFailure() - case let .configured(handler): - handler.receiveMetadata(headers) - } - self.tryReadingMessage() - } - } - - /// Try to read a request message from the buffer. - private func tryReadingMessage() { - // This while loop exists to break the recursion in `.forwardMessageThenReadNextMessage`. - // Almost all cases return directly out of the loop. - while true { - let action = self.state.readNextRequest( - maxLength: self.maxReceiveMessageLength - ) - switch action { - case .none: - return - - case let .forwardMessage(buffer): - switch self.configurationState { - case .notConfigured: - preconditionFailure() - case let .configured(handler): - handler.receiveMessage(buffer) - } - - return - - case let .forwardMessageThenReadNextMessage(buffer): - switch self.configurationState { - case .notConfigured: - preconditionFailure() - case let .configured(handler): - handler.receiveMessage(buffer) - } - - continue - - case .forwardEnd: - switch self.configurationState { - case .notConfigured: - preconditionFailure() - case let .configured(handler): - handler.receiveEnd() - } - - return - - case let .errorCaught(error): - switch self.configurationState { - case .notConfigured: - preconditionFailure() - case let .configured(handler): - handler.receiveError(error) - } - - return - } - } - } - - internal func sendMetadata( - _ headers: HPACKHeaders, - flush: Bool, - promise: EventLoopPromise? - ) { - switch self.state.send(headers: headers) { - case let .success(headers): - let payload = HTTP2Frame.FramePayload.headers(.init(headers: headers)) - self.context.write(self.wrapOutboundOut(payload), promise: promise) - if flush { - self.markFlushPoint() - } - - case let .failure(error): - promise?.fail(error) - } - } - - internal func sendMessage( - _ buffer: ByteBuffer, - metadata: MessageMetadata, - promise: EventLoopPromise? - ) { - let result = self.state.send( - buffer: buffer, - compress: metadata.compress, - promise: promise - ) - - switch result { - case .success: - if metadata.flush { - self.markFlushPoint() - } - - case let .failure(error): - promise?.fail(error) - } - } - - internal func sendEnd( - status: GRPCStatus, - trailers: HPACKHeaders, - promise: EventLoopPromise? - ) { - // About to end the stream: send any pending responses. - self.deliverPendingResponses() - - switch self.state.send(status: status, trailers: trailers) { - case let .sendTrailers(trailers): - self.sendTrailers(trailers, promise: promise) - - case let .sendTrailersAndFinish(trailers): - self.sendTrailers(trailers, promise: promise) - - // 'finish' the handler. - let handler = self.configurationState.tearDown() - handler?.finish() - - case let .failure(error): - promise?.fail(error) - } - } - - private func sendTrailers(_ trailers: HPACKHeaders, promise: EventLoopPromise?) { - // Always end stream for status and trailers. - let payload = HTTP2Frame.FramePayload.headers(.init(headers: trailers, endStream: true)) - self.context.write(self.wrapOutboundOut(payload), promise: promise) - // We'll always flush on end. - self.markFlushPoint() - } - - /// Mark a flush as pending - to be emitted once the read completes - if we're currently reading, - /// or emit a flush now if we are not. - private func markFlushPoint() { - if self.isReading { - self.flushPending = true - } else { - // About to flush: send any pending responses. - self.deliverPendingResponses() - self.flushPending = false - self.context.flush() - } - } -} diff --git a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift b/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift deleted file mode 100644 index de5456d8e..000000000 --- a/Sources/GRPC/HTTP2ToRawGRPCStateMachine.swift +++ /dev/null @@ -1,1271 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -struct HTTP2ToRawGRPCStateMachine { - /// The current state. - private var state: State = .requestIdleResponseIdle -} - -extension HTTP2ToRawGRPCStateMachine { - enum State { - // Both peers are idle. Nothing has happened to the stream. - case requestIdleResponseIdle - - // Received valid headers. Nothing has been sent in response. - case requestOpenResponseIdle(RequestOpenResponseIdleState) - - // Received valid headers and request(s). Response headers have been sent. - case requestOpenResponseOpen(RequestOpenResponseOpenState) - - // Received valid headers and request(s) but not end of the request stream. Response stream has - // been closed. - case requestOpenResponseClosed - - // The request stream is closed. Nothing has been sent in response. - case requestClosedResponseIdle(RequestClosedResponseIdleState) - - // The request stream is closed. Response headers have been sent. - case requestClosedResponseOpen(RequestClosedResponseOpenState) - - // Both streams are closed. This state is terminal. - case requestClosedResponseClosed - } - - struct RequestOpenResponseIdleState { - /// A length prefixed message reader for request messages. - var reader: LengthPrefixedMessageReader - - /// A length prefixed message writer for response messages. - var writer: CoalescingLengthPrefixedMessageWriter - - /// The content type of the RPC. - var contentType: ContentType - - /// An accept encoding header to send in the response headers indicating the message encoding - /// that the server supports. - var acceptEncoding: String? - - /// A message encoding header to send in the response headers indicating the encoding which will - /// be used for responses. - var responseEncoding: String? - - /// Whether to normalize user-provided metadata. - var normalizeHeaders: Bool - - /// The pipeline configuration state. - var configurationState: ConfigurationState - } - - struct RequestClosedResponseIdleState { - /// A length prefixed message reader for request messages. - var reader: LengthPrefixedMessageReader - - /// A length prefixed message writer for response messages. - var writer: CoalescingLengthPrefixedMessageWriter - - /// The content type of the RPC. - var contentType: ContentType - - /// An accept encoding header to send in the response headers indicating the message encoding - /// that the server supports. - var acceptEncoding: String? - - /// A message encoding header to send in the response headers indicating the encoding which will - /// be used for responses. - var responseEncoding: String? - - /// Whether to normalize user-provided metadata. - var normalizeHeaders: Bool - - /// The pipeline configuration state. - var configurationState: ConfigurationState - - init(from state: RequestOpenResponseIdleState) { - self.reader = state.reader - self.writer = state.writer - self.contentType = state.contentType - self.acceptEncoding = state.acceptEncoding - self.responseEncoding = state.responseEncoding - self.normalizeHeaders = state.normalizeHeaders - self.configurationState = state.configurationState - } - } - - struct RequestOpenResponseOpenState { - /// A length prefixed message reader for request messages. - var reader: LengthPrefixedMessageReader - - /// A length prefixed message writer for response messages. - var writer: CoalescingLengthPrefixedMessageWriter - - /// Whether to normalize user-provided metadata. - var normalizeHeaders: Bool - - init(from state: RequestOpenResponseIdleState) { - self.reader = state.reader - self.writer = state.writer - self.normalizeHeaders = state.normalizeHeaders - } - } - - struct RequestClosedResponseOpenState { - /// A length prefixed message reader for request messages. - var reader: LengthPrefixedMessageReader - - /// A length prefixed message writer for response messages. - var writer: CoalescingLengthPrefixedMessageWriter - - /// Whether to normalize user-provided metadata. - var normalizeHeaders: Bool - - init(from state: RequestOpenResponseOpenState) { - self.reader = state.reader - self.writer = state.writer - self.normalizeHeaders = state.normalizeHeaders - } - - init(from state: RequestClosedResponseIdleState) { - self.reader = state.reader - self.writer = state.writer - self.normalizeHeaders = state.normalizeHeaders - } - } - - /// The pipeline configuration state. - enum ConfigurationState { - /// The pipeline is being configured. Any message data will be buffered into an appropriate - /// message reader. - case configuring(HPACKHeaders) - - /// The pipeline is configured. - case configured - - /// Returns true if the configuration is in the `.configured` state. - var isConfigured: Bool { - switch self { - case .configuring: - return false - case .configured: - return true - } - } - - /// Configuration has completed. - mutating func configured() -> HPACKHeaders { - switch self { - case .configured: - preconditionFailure("Invalid state: already configured") - - case let .configuring(headers): - self = .configured - return headers - } - } - } -} - -extension HTTP2ToRawGRPCStateMachine { - enum PipelineConfiguredAction { - /// Forward the given headers. - case forwardHeaders(HPACKHeaders) - /// Forward the given headers and try reading the next message. - case forwardHeadersAndRead(HPACKHeaders) - } - - enum ReceiveHeadersAction { - /// Configure the RPC to use the given server handler. - case configure(GRPCServerHandlerProtocol) - /// Reject the RPC by writing out the given headers and setting end-stream. - case rejectRPC(HPACKHeaders) - } - - enum ReadNextMessageAction { - /// Do nothing. - case none - /// Forward the buffer. - case forwardMessage(ByteBuffer) - /// Forward the buffer and try reading the next message. - case forwardMessageThenReadNextMessage(ByteBuffer) - /// Forward the 'end' of stream request part. - case forwardEnd - /// Throw an error down the pipeline. - case errorCaught(Error) - } - - struct StateAndReceiveHeadersAction { - /// The next state. - var state: State - /// The action to take. - var action: ReceiveHeadersAction - } - - struct StateAndReceiveDataAction { - /// The next state. - var state: State - /// The action to take - var action: ReceiveDataAction - } - - enum ReceiveDataAction: Hashable { - /// Try to read the next message from the state machine. - case tryReading - /// Invoke 'finish' on the RPC handler. - case finishHandler - /// Do nothing. - case nothing - } - - enum SendEndAction { - /// Send trailers to the client. - case sendTrailers(HPACKHeaders) - /// Send trailers to the client and invoke 'finish' on the RPC handler. - case sendTrailersAndFinish(HPACKHeaders) - /// Fail any promise associated with this send. - case failure(Error) - } -} - -// MARK: Receive Headers - -// This is the only state in which we can receive headers. -extension HTTP2ToRawGRPCStateMachine.State { - private func _receive( - headers: HPACKHeaders, - eventLoop: EventLoop, - errorDelegate: ServerErrorDelegate?, - remoteAddress: SocketAddress?, - logger: Logger, - allocator: ByteBufferAllocator, - responseWriter: GRPCServerResponseWriter, - closeFuture: EventLoopFuture, - services: [Substring: CallHandlerProvider], - encoding: ServerMessageEncoding, - normalizeHeaders: Bool - ) -> HTTP2ToRawGRPCStateMachine.StateAndReceiveHeadersAction { - // Extract and validate the content type. If it's nil we need to close. - guard let contentType = self.extractContentType(from: headers) else { - return self.unsupportedContentType() - } - - // Now extract the request message encoding and setup an appropriate message reader. - // We may send back a list of acceptable request message encodings as well. - let reader: LengthPrefixedMessageReader - let acceptableRequestEncoding: String? - - switch self.extractRequestEncoding(from: headers, encoding: encoding) { - case let .valid(messageReader, acceptEncodingHeader): - reader = messageReader - acceptableRequestEncoding = acceptEncodingHeader - - case let .invalid(status, acceptableRequestEncoding): - return self.invalidRequestEncoding( - status: status, - acceptableRequestEncoding: acceptableRequestEncoding, - contentType: contentType - ) - } - - // Figure out which encoding we should use for responses. - let (writer, responseEncoding) = self.extractResponseEncoding( - from: headers, - encoding: encoding, - allocator: allocator - ) - - // Parse the path, and create a call handler. - guard let path = headers.first(name: ":path") else { - return self.methodNotImplemented("", contentType: contentType) - } - - guard let callPath = CallPath(requestURI: path), - let service = services[Substring(callPath.service)] - else { - return self.methodNotImplemented(path, contentType: contentType) - } - - // Create a call handler context, i.e. a bunch of 'stuff' we need to create the handler with, - // some of which is exposed to service providers. - let context = CallHandlerContext( - errorDelegate: errorDelegate, - logger: logger, - encoding: encoding, - eventLoop: eventLoop, - path: path, - remoteAddress: remoteAddress, - responseWriter: responseWriter, - allocator: allocator, - closeFuture: closeFuture - ) - - // We have a matching service, hopefully we have a provider for the method too. - let method = Substring(callPath.method) - - if let handler = service.handle(method: method, context: context) { - let nextState = HTTP2ToRawGRPCStateMachine.RequestOpenResponseIdleState( - reader: reader, - writer: writer, - contentType: contentType, - acceptEncoding: acceptableRequestEncoding, - responseEncoding: responseEncoding, - normalizeHeaders: normalizeHeaders, - configurationState: .configuring(headers) - ) - - return .init( - state: .requestOpenResponseIdle(nextState), - action: .configure(handler) - ) - } else { - return self.methodNotImplemented(path, contentType: contentType) - } - } - - /// The 'content-type' is not supported; close with status code 415. - private func unsupportedContentType() -> HTTP2ToRawGRPCStateMachine.StateAndReceiveHeadersAction { - // From: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md - // - // If 'content-type' does not begin with "application/grpc", gRPC servers SHOULD respond - // with HTTP status of 415 (Unsupported Media Type). This will prevent other HTTP/2 - // clients from interpreting a gRPC error response, which uses status 200 (OK), as - // successful. - let trailers = HPACKHeaders([(":status", "415")]) - return .init( - state: .requestClosedResponseClosed, - action: .rejectRPC(trailers) - ) - } - - /// The RPC method is not implemented. Close with an appropriate status. - private func methodNotImplemented( - _ path: String, - contentType: ContentType - ) -> HTTP2ToRawGRPCStateMachine.StateAndReceiveHeadersAction { - let trailers = HTTP2ToRawGRPCStateMachine.makeResponseTrailersOnly( - for: GRPCStatus(code: .unimplemented, message: "'\(path)' is not implemented"), - contentType: contentType, - acceptableRequestEncoding: nil, - userProvidedHeaders: nil, - normalizeUserProvidedHeaders: false - ) - - return .init( - state: .requestClosedResponseClosed, - action: .rejectRPC(trailers) - ) - } - - /// The request encoding specified by the client is not supported. Close with an appropriate - /// status. - private func invalidRequestEncoding( - status: GRPCStatus, - acceptableRequestEncoding: String?, - contentType: ContentType - ) -> HTTP2ToRawGRPCStateMachine.StateAndReceiveHeadersAction { - let trailers = HTTP2ToRawGRPCStateMachine.makeResponseTrailersOnly( - for: status, - contentType: contentType, - acceptableRequestEncoding: acceptableRequestEncoding, - userProvidedHeaders: nil, - normalizeUserProvidedHeaders: false - ) - - return .init( - state: .requestClosedResponseClosed, - action: .rejectRPC(trailers) - ) - } - - /// Makes a 'GRPCStatus' and response trailers suitable for returning to the client when the - /// request message encoding is not supported. - /// - /// - Parameters: - /// - encoding: The unsupported request message encoding sent by the client. - /// - acceptable: The list if acceptable request message encoding the client may use. - /// - Returns: The status and trailers to return to the client. - private func makeStatusAndTrailersForUnsupportedEncoding( - _ encoding: String, - advertisedEncoding: [String] - ) -> (GRPCStatus, acceptEncoding: String?) { - let status: GRPCStatus - let acceptEncoding: String? - - if advertisedEncoding.isEmpty { - // No compression is supported; there's nothing to tell the client about. - status = GRPCStatus(code: .unimplemented, message: "compression is not supported") - acceptEncoding = nil - } else { - // Return a list of supported encodings which we advertise. (The list we advertise may be a - // subset of the encodings we support.) - acceptEncoding = advertisedEncoding.joined(separator: ",") - status = GRPCStatus( - code: .unimplemented, - message: "\(encoding) compression is not supported, supported algorithms are " - + "listed in '\(GRPCHeaderName.acceptEncoding)'" - ) - } - - return (status, acceptEncoding) - } - - /// Extract and validate the 'content-type' sent by the client. - /// - Parameter headers: The headers to extract the 'content-type' from - private func extractContentType(from headers: HPACKHeaders) -> ContentType? { - return headers.first(name: GRPCHeaderName.contentType).flatMap(ContentType.init) - } - - /// The result of validating the request encoding header. - private enum RequestEncodingValidation { - /// The encoding was valid. - case valid(messageReader: LengthPrefixedMessageReader, acceptEncoding: String?) - /// The encoding was invalid, the RPC should be terminated with this status. - case invalid(status: GRPCStatus, acceptEncoding: String?) - } - - /// Extract and validate the request message encoding header. - /// - Parameters: - /// - headers: The headers to extract the message encoding header from. - /// - Returns: `RequestEncodingValidation`, either a message reader suitable for decoding requests - /// and an accept encoding response header if the request encoding was valid, or a pair of - /// `GRPCStatus` and trailers to close the RPC with. - private func extractRequestEncoding( - from headers: HPACKHeaders, - encoding: ServerMessageEncoding - ) -> RequestEncodingValidation { - let encodingValues = headers.values(forHeader: GRPCHeaderName.encoding, canonicalForm: true) - var encodingIterator = encodingValues.makeIterator() - let encodingHeader = encodingIterator.next() - - // Fail if there's more than one encoding header. - if let first = encodingHeader, let second = encodingIterator.next() { - var encodings: [Substring] = [] - encodings.reserveCapacity(8) - encodings.append(first) - encodings.append(second) - while let next = encodingIterator.next() { - encodings.append(next) - } - let status = GRPCStatus( - code: .invalidArgument, - message: - "'\(GRPCHeaderName.encoding)' must contain no more than one value but was '\(encodings.joined(separator: ", "))'" - ) - return .invalid(status: status, acceptEncoding: nil) - } - - let result: RequestEncodingValidation - let validator = MessageEncodingHeaderValidator(encoding: encoding) - - switch validator.validate(requestEncoding: encodingHeader.map { String($0) }) { - case let .supported(algorithm, decompressionLimit, acceptEncoding): - // Request message encoding is valid and supported. - result = .valid( - messageReader: LengthPrefixedMessageReader( - compression: algorithm, - decompressionLimit: decompressionLimit - ), - acceptEncoding: acceptEncoding.isEmpty ? nil : acceptEncoding.joined(separator: ",") - ) - - case .noCompression: - // No message encoding header was present. This means no compression is being used. - result = .valid( - messageReader: LengthPrefixedMessageReader(), - acceptEncoding: nil - ) - - case let .unsupported(encoding, acceptable): - // Request encoding is not supported. - let (status, acceptEncoding) = self.makeStatusAndTrailersForUnsupportedEncoding( - encoding, - advertisedEncoding: acceptable - ) - result = .invalid(status: status, acceptEncoding: acceptEncoding) - } - - return result - } - - /// Extract a suitable message encoding for responses. - /// - Parameters: - /// - headers: The headers to extract the acceptable response message encoding from. - /// - configuration: The encoding configuration for the server. - /// - Returns: A message writer and the response encoding header to send back to the client. - private func extractResponseEncoding( - from headers: HPACKHeaders, - encoding: ServerMessageEncoding, - allocator: ByteBufferAllocator - ) -> (CoalescingLengthPrefixedMessageWriter, String?) { - let writer: CoalescingLengthPrefixedMessageWriter - let responseEncoding: String? - - switch encoding { - case let .enabled(configuration): - // Extract the encodings acceptable to the client for response messages. - let acceptableResponseEncoding = headers[canonicalForm: GRPCHeaderName.acceptEncoding] - - // Select the first algorithm that we support and have enabled. If we don't find one then we - // won't compress response messages. - let algorithm = acceptableResponseEncoding.lazy.compactMap { value in - CompressionAlgorithm(rawValue: value) - }.first { - configuration.enabledAlgorithms.contains($0) - } - - writer = .init(compression: algorithm, allocator: allocator) - responseEncoding = algorithm?.name - - case .disabled: - // The server doesn't have compression enabled. - writer = .init(compression: .none, allocator: allocator) - responseEncoding = nil - } - - return (writer, responseEncoding) - } -} - -// MARK: - Receive Data - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseIdleState { - mutating func receive( - buffer: inout ByteBuffer, - endStream: Bool - ) -> HTTP2ToRawGRPCStateMachine.StateAndReceiveDataAction { - // Append the bytes to the reader. - self.reader.append(buffer: &buffer) - - let state: HTTP2ToRawGRPCStateMachine.State - let action: HTTP2ToRawGRPCStateMachine.ReceiveDataAction - - switch (self.configurationState.isConfigured, endStream) { - case (true, true): - /// Configured and end stream: read from the buffer, end will be sent as a result of draining - /// the reader in the next state. - state = .requestClosedResponseIdle(.init(from: self)) - action = .tryReading - - case (true, false): - /// Configured but not end stream, just read from the buffer. - state = .requestOpenResponseIdle(self) - action = .tryReading - - case (false, true): - // Not configured yet, but end of stream. Request stream is now closed but there's no point - // reading yet. - state = .requestClosedResponseIdle(.init(from: self)) - action = .nothing - - case (false, false): - // Not configured yet, not end stream. No point reading a message yet since we don't have - // anywhere to deliver it. - state = .requestOpenResponseIdle(self) - action = .nothing - } - - return .init(state: state, action: action) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseOpenState { - mutating func receive( - buffer: inout ByteBuffer, - endStream: Bool - ) -> HTTP2ToRawGRPCStateMachine.StateAndReceiveDataAction { - self.reader.append(buffer: &buffer) - - let state: HTTP2ToRawGRPCStateMachine.State - - if endStream { - // End stream, so move to the closed state. Any end of request stream events events will - // happen as a result of reading from the closed state. - state = .requestClosedResponseOpen(.init(from: self)) - } else { - state = .requestOpenResponseOpen(self) - } - - return .init(state: state, action: .tryReading) - } -} - -// MARK: - Send Headers - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseIdleState { - func send(headers userProvidedHeaders: HPACKHeaders) -> HPACKHeaders { - return HTTP2ToRawGRPCStateMachine.makeResponseHeaders( - contentType: self.contentType, - responseEncoding: self.responseEncoding, - acceptableRequestEncoding: self.acceptEncoding, - userProvidedHeaders: userProvidedHeaders, - normalizeUserProvidedHeaders: self.normalizeHeaders - ) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseIdleState { - func send(headers userProvidedHeaders: HPACKHeaders) -> HPACKHeaders { - return HTTP2ToRawGRPCStateMachine.makeResponseHeaders( - contentType: self.contentType, - responseEncoding: self.responseEncoding, - acceptableRequestEncoding: self.acceptEncoding, - userProvidedHeaders: userProvidedHeaders, - normalizeUserProvidedHeaders: self.normalizeHeaders - ) - } -} - -// MARK: - Send Data - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseOpenState { - mutating func send( - buffer: ByteBuffer, - compress: Bool, - promise: EventLoopPromise? - ) { - self.writer.append(buffer: buffer, compress: compress, promise: promise) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseOpenState { - mutating func send( - buffer: ByteBuffer, - compress: Bool, - promise: EventLoopPromise? - ) { - self.writer.append(buffer: buffer, compress: compress, promise: promise) - } -} - -// MARK: - Send End - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseIdleState { - func send( - status: GRPCStatus, - trailers userProvidedTrailers: HPACKHeaders - ) -> HPACKHeaders { - return HTTP2ToRawGRPCStateMachine.makeResponseTrailersOnly( - for: status, - contentType: self.contentType, - acceptableRequestEncoding: self.acceptEncoding, - userProvidedHeaders: userProvidedTrailers, - normalizeUserProvidedHeaders: self.normalizeHeaders - ) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseIdleState { - func send( - status: GRPCStatus, - trailers userProvidedTrailers: HPACKHeaders - ) -> HPACKHeaders { - return HTTP2ToRawGRPCStateMachine.makeResponseTrailersOnly( - for: status, - contentType: self.contentType, - acceptableRequestEncoding: self.acceptEncoding, - userProvidedHeaders: userProvidedTrailers, - normalizeUserProvidedHeaders: self.normalizeHeaders - ) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseOpenState { - func send( - status: GRPCStatus, - trailers userProvidedTrailers: HPACKHeaders - ) -> HPACKHeaders { - return HTTP2ToRawGRPCStateMachine.makeResponseTrailers( - for: status, - userProvidedHeaders: userProvidedTrailers, - normalizeUserProvidedHeaders: true - ) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseOpenState { - func send( - status: GRPCStatus, - trailers userProvidedTrailers: HPACKHeaders - ) -> HPACKHeaders { - return HTTP2ToRawGRPCStateMachine.makeResponseTrailers( - for: status, - userProvidedHeaders: userProvidedTrailers, - normalizeUserProvidedHeaders: true - ) - } -} - -// MARK: - Pipeline Configured - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseIdleState { - mutating func pipelineConfigured() -> HTTP2ToRawGRPCStateMachine.PipelineConfiguredAction { - let headers = self.configurationState.configured() - let action: HTTP2ToRawGRPCStateMachine.PipelineConfiguredAction - - // If there are unprocessed bytes then we need to read messages as well. - let hasUnprocessedBytes = self.reader.unprocessedBytes != 0 - - if hasUnprocessedBytes { - // If there are unprocessed bytes, we need to try to read after sending the metadata. - action = .forwardHeadersAndRead(headers) - } else { - // No unprocessed bytes; the reader is empty. Just send the metadata. - action = .forwardHeaders(headers) - } - - return action - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseIdleState { - mutating func pipelineConfigured() -> HTTP2ToRawGRPCStateMachine.PipelineConfiguredAction { - let headers = self.configurationState.configured() - // Since we're already closed, we need to forward the headers and start reading. - return .forwardHeadersAndRead(headers) - } -} - -// MARK: - Read Next Request - -extension HTTP2ToRawGRPCStateMachine { - static func read( - from reader: inout LengthPrefixedMessageReader, - requestStreamClosed: Bool, - maxLength: Int - ) -> HTTP2ToRawGRPCStateMachine.ReadNextMessageAction { - do { - if let buffer = try reader.nextMessage(maxLength: maxLength) { - if reader.unprocessedBytes > 0 || requestStreamClosed { - // Either there are unprocessed bytes or the request stream is now closed: deliver the - // message and then try to read. The subsequent read may be another message or it may - // be end stream. - return .forwardMessageThenReadNextMessage(buffer) - } else { - // Nothing left to process and the stream isn't closed yet, just forward the message. - return .forwardMessage(buffer) - } - } else if requestStreamClosed { - return .forwardEnd - } else { - return .none - } - } catch { - return .errorCaught(error) - } - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseIdleState { - mutating func readNextRequest( - maxLength: Int - ) -> HTTP2ToRawGRPCStateMachine.ReadNextMessageAction { - return HTTP2ToRawGRPCStateMachine.read( - from: &self.reader, - requestStreamClosed: false, - maxLength: maxLength - ) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestOpenResponseOpenState { - mutating func readNextRequest( - maxLength: Int - ) -> HTTP2ToRawGRPCStateMachine.ReadNextMessageAction { - return HTTP2ToRawGRPCStateMachine.read( - from: &self.reader, - requestStreamClosed: false, - maxLength: maxLength - ) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseIdleState { - mutating func readNextRequest( - maxLength: Int - ) -> HTTP2ToRawGRPCStateMachine.ReadNextMessageAction { - return HTTP2ToRawGRPCStateMachine.read( - from: &self.reader, - requestStreamClosed: true, - maxLength: maxLength - ) - } -} - -extension HTTP2ToRawGRPCStateMachine.RequestClosedResponseOpenState { - mutating func readNextRequest( - maxLength: Int - ) -> HTTP2ToRawGRPCStateMachine.ReadNextMessageAction { - return HTTP2ToRawGRPCStateMachine.read( - from: &self.reader, - requestStreamClosed: true, - maxLength: maxLength - ) - } -} - -// MARK: - Top Level State Changes - -extension HTTP2ToRawGRPCStateMachine { - /// Receive request headers. - mutating func receive( - headers: HPACKHeaders, - eventLoop: EventLoop, - errorDelegate: ServerErrorDelegate?, - remoteAddress: SocketAddress?, - logger: Logger, - allocator: ByteBufferAllocator, - responseWriter: GRPCServerResponseWriter, - closeFuture: EventLoopFuture, - services: [Substring: CallHandlerProvider], - encoding: ServerMessageEncoding, - normalizeHeaders: Bool - ) -> ReceiveHeadersAction { - return self.state.receive( - headers: headers, - eventLoop: eventLoop, - errorDelegate: errorDelegate, - remoteAddress: remoteAddress, - logger: logger, - allocator: allocator, - responseWriter: responseWriter, - closeFuture: closeFuture, - services: services, - encoding: encoding, - normalizeHeaders: normalizeHeaders - ) - } - - /// Receive request buffer. - /// - Parameters: - /// - buffer: The received buffer. - /// - endStream: Whether end stream was set. - /// - Returns: Returns whether the caller should try to read a message from the buffer. - mutating func receive(buffer: inout ByteBuffer, endStream: Bool) -> ReceiveDataAction { - self.state.receive(buffer: &buffer, endStream: endStream) - } - - /// Send response headers. - mutating func send(headers: HPACKHeaders) -> Result { - self.state.send(headers: headers) - } - - /// Send a response buffer. - mutating func send( - buffer: ByteBuffer, - compress: Bool, - promise: EventLoopPromise? - ) -> Result { - self.state.send(buffer: buffer, compress: compress, promise: promise) - } - - mutating func nextResponse() -> (Result, EventLoopPromise?)? { - self.state.nextResponse() - } - - /// Send status and trailers. - mutating func send( - status: GRPCStatus, - trailers: HPACKHeaders - ) -> HTTP2ToRawGRPCStateMachine.SendEndAction { - self.state.send(status: status, trailers: trailers) - } - - /// The pipeline has been configured with a service provider. - mutating func pipelineConfigured() -> PipelineConfiguredAction { - self.state.pipelineConfigured() - } - - /// Try to read a request message. - mutating func readNextRequest(maxLength: Int) -> ReadNextMessageAction { - self.state.readNextRequest(maxLength: maxLength) - } -} - -extension HTTP2ToRawGRPCStateMachine.State { - mutating func pipelineConfigured() -> HTTP2ToRawGRPCStateMachine.PipelineConfiguredAction { - switch self { - case .requestIdleResponseIdle: - preconditionFailure("Invalid state: pipeline configured before receiving request headers") - - case var .requestOpenResponseIdle(state): - let action = state.pipelineConfigured() - self = .requestOpenResponseIdle(state) - return action - - case var .requestClosedResponseIdle(state): - let action = state.pipelineConfigured() - self = .requestClosedResponseIdle(state) - return action - - case .requestOpenResponseOpen, - .requestOpenResponseClosed, - .requestClosedResponseOpen, - .requestClosedResponseClosed: - preconditionFailure("Invalid state: response stream opened before pipeline was configured") - } - } - - mutating func receive( - headers: HPACKHeaders, - eventLoop: EventLoop, - errorDelegate: ServerErrorDelegate?, - remoteAddress: SocketAddress?, - logger: Logger, - allocator: ByteBufferAllocator, - responseWriter: GRPCServerResponseWriter, - closeFuture: EventLoopFuture, - services: [Substring: CallHandlerProvider], - encoding: ServerMessageEncoding, - normalizeHeaders: Bool - ) -> HTTP2ToRawGRPCStateMachine.ReceiveHeadersAction { - switch self { - // These are the only states in which we can receive headers. Everything else is invalid. - case .requestIdleResponseIdle, - .requestClosedResponseClosed: - let stateAndAction = self._receive( - headers: headers, - eventLoop: eventLoop, - errorDelegate: errorDelegate, - remoteAddress: remoteAddress, - logger: logger, - allocator: allocator, - responseWriter: responseWriter, - closeFuture: closeFuture, - services: services, - encoding: encoding, - normalizeHeaders: normalizeHeaders - ) - self = stateAndAction.state - return stateAndAction.action - - // We can't receive headers in any of these states. - case .requestOpenResponseIdle, - .requestOpenResponseOpen, - .requestOpenResponseClosed, - .requestClosedResponseIdle, - .requestClosedResponseOpen: - preconditionFailure("Invalid state: \(self)") - } - } - - /// Receive a buffer from the client. - mutating func receive( - buffer: inout ByteBuffer, - endStream: Bool - ) -> HTTP2ToRawGRPCStateMachine.ReceiveDataAction { - switch self { - case .requestIdleResponseIdle: - /// This isn't allowed: we must receive the request headers first. - preconditionFailure("Invalid state") - - case var .requestOpenResponseIdle(state): - let stateAndAction = state.receive(buffer: &buffer, endStream: endStream) - self = stateAndAction.state - return stateAndAction.action - - case var .requestOpenResponseOpen(state): - let stateAndAction = state.receive(buffer: &buffer, endStream: endStream) - self = stateAndAction.state - return stateAndAction.action - - case .requestClosedResponseIdle, - .requestClosedResponseOpen: - preconditionFailure("Invalid state: the request stream is already closed") - - case .requestOpenResponseClosed: - if endStream { - // Server has finish responding and this is the end of the request stream; we're done for - // this RPC now, finish the handler. - self = .requestClosedResponseClosed - return .finishHandler - } else { - // Server has finished responding but this isn't the end of the request stream; ignore the - // input, we need to wait for end stream before tearing down the handler. - return .nothing - } - - case .requestClosedResponseClosed: - return .nothing - } - } - - mutating func readNextRequest( - maxLength: Int - ) -> HTTP2ToRawGRPCStateMachine.ReadNextMessageAction { - switch self { - case .requestIdleResponseIdle: - preconditionFailure("Invalid state") - - case var .requestOpenResponseIdle(state): - let action = state.readNextRequest(maxLength: maxLength) - self = .requestOpenResponseIdle(state) - return action - - case var .requestOpenResponseOpen(state): - let action = state.readNextRequest(maxLength: maxLength) - self = .requestOpenResponseOpen(state) - return action - - case var .requestClosedResponseIdle(state): - let action = state.readNextRequest(maxLength: maxLength) - self = .requestClosedResponseIdle(state) - return action - - case var .requestClosedResponseOpen(state): - let action = state.readNextRequest(maxLength: maxLength) - self = .requestClosedResponseOpen(state) - return action - - case .requestOpenResponseClosed, - .requestClosedResponseClosed: - return .none - } - } - - mutating func send(headers: HPACKHeaders) -> Result { - switch self { - case .requestIdleResponseIdle: - preconditionFailure("Invalid state: the request stream isn't open") - - case let .requestOpenResponseIdle(state): - let headers = state.send(headers: headers) - self = .requestOpenResponseOpen(.init(from: state)) - return .success(headers) - - case let .requestClosedResponseIdle(state): - let headers = state.send(headers: headers) - self = .requestClosedResponseOpen(.init(from: state)) - return .success(headers) - - case .requestOpenResponseOpen, - .requestOpenResponseClosed, - .requestClosedResponseOpen, - .requestClosedResponseClosed: - return .failure(GRPCError.AlreadyComplete()) - } - } - - mutating func send( - buffer: ByteBuffer, - compress: Bool, - promise: EventLoopPromise? - ) -> Result { - switch self { - case .requestIdleResponseIdle: - preconditionFailure("Invalid state: the request stream is still closed") - - case .requestOpenResponseIdle, - .requestClosedResponseIdle: - let error = GRPCError.InvalidState("Response headers must be sent before response message") - return .failure(error) - - case var .requestOpenResponseOpen(state): - self = .requestClosedResponseClosed - state.send(buffer: buffer, compress: compress, promise: promise) - self = .requestOpenResponseOpen(state) - return .success(()) - - case var .requestClosedResponseOpen(state): - self = .requestClosedResponseClosed - state.send(buffer: buffer, compress: compress, promise: promise) - self = .requestClosedResponseOpen(state) - return .success(()) - - case .requestOpenResponseClosed, - .requestClosedResponseClosed: - return .failure(GRPCError.AlreadyComplete()) - } - } - - mutating func nextResponse() -> (Result, EventLoopPromise?)? { - switch self { - case .requestIdleResponseIdle: - preconditionFailure("Invalid state: the request stream is still closed") - - case .requestOpenResponseIdle, - .requestClosedResponseIdle: - return nil - - case var .requestOpenResponseOpen(state): - self = .requestClosedResponseClosed - let result = state.writer.next() - self = .requestOpenResponseOpen(state) - return result - - case var .requestClosedResponseOpen(state): - self = .requestClosedResponseClosed - let result = state.writer.next() - self = .requestClosedResponseOpen(state) - return result - - case .requestOpenResponseClosed, - .requestClosedResponseClosed: - return nil - } - } - - mutating func send( - status: GRPCStatus, - trailers: HPACKHeaders - ) -> HTTP2ToRawGRPCStateMachine.SendEndAction { - switch self { - case .requestIdleResponseIdle: - preconditionFailure("Invalid state: the request stream is still closed") - - case let .requestOpenResponseIdle(state): - self = .requestOpenResponseClosed - return .sendTrailers(state.send(status: status, trailers: trailers)) - - case let .requestClosedResponseIdle(state): - self = .requestClosedResponseClosed - return .sendTrailersAndFinish(state.send(status: status, trailers: trailers)) - - case let .requestOpenResponseOpen(state): - self = .requestOpenResponseClosed - return .sendTrailers(state.send(status: status, trailers: trailers)) - - case let .requestClosedResponseOpen(state): - self = .requestClosedResponseClosed - return .sendTrailersAndFinish(state.send(status: status, trailers: trailers)) - - case .requestOpenResponseClosed, - .requestClosedResponseClosed: - return .failure(GRPCError.AlreadyComplete()) - } - } -} - -// MARK: - Helpers - -extension HTTP2ToRawGRPCStateMachine { - static func makeResponseHeaders( - contentType: ContentType, - responseEncoding: String?, - acceptableRequestEncoding: String?, - userProvidedHeaders: HPACKHeaders, - normalizeUserProvidedHeaders: Bool - ) -> HPACKHeaders { - // 4 because ':status' and 'content-type' are required. We may send back 'grpc-encoding' and - // 'grpc-accept-encoding' as well. - let capacity = 4 + userProvidedHeaders.count - - var headers = HPACKHeaders() - headers.reserveCapacity(capacity) - - headers.add(name: ":status", value: "200") - headers.add(name: GRPCHeaderName.contentType, value: contentType.canonicalValue) - - if let responseEncoding = responseEncoding { - headers.add(name: GRPCHeaderName.encoding, value: responseEncoding) - } - - if let acceptEncoding = acceptableRequestEncoding { - headers.add(name: GRPCHeaderName.acceptEncoding, value: acceptEncoding) - } - - // Add user provided headers, normalizing if required. - headers.add(contentsOf: userProvidedHeaders, normalize: normalizeUserProvidedHeaders) - - return headers - } - - static func makeResponseTrailersOnly( - for status: GRPCStatus, - contentType: ContentType, - acceptableRequestEncoding: String?, - userProvidedHeaders: HPACKHeaders?, - normalizeUserProvidedHeaders: Bool - ) -> HPACKHeaders { - // 5 because ':status', 'content-type', 'grpc-status' are required. We may also send back - // 'grpc-message' and 'grpc-accept-encoding'. - let capacity = 5 + (userProvidedHeaders.map { $0.count } ?? 0) - - var headers = HPACKHeaders() - headers.reserveCapacity(capacity) - - // Add the required trailers. - headers.add(name: ":status", value: "200") - headers.add(name: GRPCHeaderName.contentType, value: contentType.canonicalValue) - headers.add(name: GRPCHeaderName.statusCode, value: String(describing: status.code.rawValue)) - - if let message = status.message.flatMap(GRPCStatusMessageMarshaller.marshall) { - headers.add(name: GRPCHeaderName.statusMessage, value: message) - } - - // We may include this if the requested encoding was not valid. - if let acceptEncoding = acceptableRequestEncoding { - headers.add(name: GRPCHeaderName.acceptEncoding, value: acceptEncoding) - } - - if let userProvided = userProvidedHeaders { - headers.add(contentsOf: userProvided, normalize: normalizeUserProvidedHeaders) - } - - return headers - } - - static func makeResponseTrailers( - for status: GRPCStatus, - userProvidedHeaders: HPACKHeaders, - normalizeUserProvidedHeaders: Bool - ) -> HPACKHeaders { - // Most RPCs should end with status code 'ok' (hopefully!), and if the user didn't provide any - // additional trailers, then we can use a pre-canned set of headers to avoid an extra - // allocation. - if status == .ok, userProvidedHeaders.isEmpty { - return Self.gRPCStatusOkTrailers - } - - // 2 because 'grpc-status' is required, we may also send back 'grpc-message'. - let capacity = 2 + userProvidedHeaders.count - - var trailers = HPACKHeaders() - trailers.reserveCapacity(capacity) - - // status code. - trailers.add(name: GRPCHeaderName.statusCode, value: String(describing: status.code.rawValue)) - - // status message, if present. - if let message = status.message.flatMap(GRPCStatusMessageMarshaller.marshall) { - trailers.add(name: GRPCHeaderName.statusMessage, value: message) - } - - // user provided trailers. - trailers.add(contentsOf: userProvidedHeaders, normalize: normalizeUserProvidedHeaders) - - return trailers - } - - private static let gRPCStatusOkTrailers: HPACKHeaders = [ - GRPCHeaderName.statusCode: String(describing: GRPCStatus.Code.ok.rawValue) - ] -} - -extension HPACKHeaders { - fileprivate mutating func add(contentsOf other: HPACKHeaders, normalize: Bool) { - if normalize { - self.add( - contentsOf: other.lazy.map { name, value, indexable in - (name: name.lowercased(), value: value, indexable: indexable) - } - ) - } else { - self.add(contentsOf: other) - } - } -} diff --git a/Sources/GRPC/Interceptor/ClientInterceptorContext.swift b/Sources/GRPC/Interceptor/ClientInterceptorContext.swift deleted file mode 100644 index a90dead7f..000000000 --- a/Sources/GRPC/Interceptor/ClientInterceptorContext.swift +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore - -public struct ClientInterceptorContext { - /// The interceptor this context is for. - @usableFromInline - internal let interceptor: ClientInterceptor - - /// The pipeline this context is associated with. - @usableFromInline - internal let _pipeline: ClientInterceptorPipeline - - /// The index of this context's interceptor within the pipeline. - @usableFromInline - internal let _index: Int - - /// The `EventLoop` this interceptor pipeline is being executed on. - public var eventLoop: EventLoop { - return self._pipeline.eventLoop - } - - /// A logger. - public var logger: Logger { - return self._pipeline.logger - } - - /// The type of the RPC, e.g. "unary". - public var type: GRPCCallType { - return self._pipeline.details.type - } - - /// The path of the RPC in the format "/Service/Method", e.g. "/echo.Echo/Get". - public var path: String { - return self._pipeline.details.path - } - - /// The options used to invoke the call. - public var options: CallOptions { - return self._pipeline.details.options - } - - /// Construct a ``ClientInterceptorContext`` for the interceptor at the given index within in - /// interceptor pipeline. - @inlinable - internal init( - for interceptor: ClientInterceptor, - atIndex index: Int, - in pipeline: ClientInterceptorPipeline - ) { - self.interceptor = interceptor - self._pipeline = pipeline - self._index = index - } - - /// Forwards the response part to the next inbound interceptor in the pipeline, if there is one. - /// - /// - Parameter part: The response part to forward. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - public func receive(_ part: GRPCClientResponsePart) { - self.eventLoop.assertInEventLoop() - self._pipeline.invokeReceive(part, fromContextAtIndex: self._index) - } - - /// Forwards the error to the next inbound interceptor in the pipeline, if there is one. - /// - /// - Parameter error: The error to forward. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - public func errorCaught(_ error: Error) { - self.eventLoop.assertInEventLoop() - self._pipeline.invokeErrorCaught(error, fromContextAtIndex: self._index) - } - - /// Forwards the request part to the next outbound interceptor in the pipeline, if there is one. - /// - /// - Parameters: - /// - part: The request part to forward. - /// - promise: The promise the complete when the part has been written. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - public func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise? - ) { - self.eventLoop.assertInEventLoop() - self._pipeline.invokeSend(part, promise: promise, fromContextAtIndex: self._index) - } - - /// Forwards a request to cancel the RPC to the next outbound interceptor in the pipeline. - /// - /// - Parameter promise: The promise to complete with the outcome of the cancellation request. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - public func cancel(promise: EventLoopPromise?) { - self.eventLoop.assertInEventLoop() - self._pipeline.invokeCancel(promise: promise, fromContextAtIndex: self._index) - } -} diff --git a/Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift b/Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift deleted file mode 100644 index ec767de5d..000000000 --- a/Sources/GRPC/Interceptor/ClientInterceptorPipeline.swift +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -/// A pipeline for intercepting client request and response streams. -/// -/// The interceptor pipeline lies between the call object (`UnaryCall`, `ClientStreamingCall`, etc.) -/// and the transport used to send and receive messages from the server (a `NIO.Channel`). It holds -/// a collection of interceptors which may be used to observe or alter messages as the travel -/// through the pipeline. -/// -/// ``` -/// ┌───────────────────────────────────────────────────────────────────┐ -/// │ Call │ -/// └────────────────────────────────────────────────────────┬──────────┘ -/// │ send(_:promise) / -/// │ cancel(promise:) -/// ┌────────────────────────────────────────────────────────▼──────────┐ -/// │ InterceptorPipeline ╎ │ -/// │ ╎ │ -/// │ ┌──────────────────────────────────────────────────────▼────────┐ │ -/// │ │ Tail Interceptor (hands response parts to a callback) │ │ -/// │ └────────▲─────────────────────────────────────────────┬────────┘ │ -/// │ ┌────────┴─────────────────────────────────────────────▼────────┐ │ -/// │ │ Interceptor 1 │ │ -/// │ └────────▲─────────────────────────────────────────────┬────────┘ │ -/// │ ┌────────┴─────────────────────────────────────────────▼────────┐ │ -/// │ │ Interceptor 2 │ │ -/// │ └────────▲─────────────────────────────────────────────┬────────┘ │ -/// │ ╎ ╎ │ -/// │ ╎ (More interceptors) ╎ │ -/// │ ╎ ╎ │ -/// │ ┌────────┴─────────────────────────────────────────────▼────────┐ │ -/// │ │ Head Interceptor (interacts with transport) │ │ -/// │ └────────▲─────────────────────────────────────────────┬────────┘ │ -/// │ ╎ receive(_:) │ │ -/// └──────────▲─────────────────────────────────────────────┼──────────┘ -/// │ receive(_:) │ send(_:promise:) / -/// │ │ cancel(promise:) -/// ┌──────────┴─────────────────────────────────────────────▼──────────┐ -/// │ ClientTransport │ -/// │ (a NIO.ChannelHandler) │ -/// ``` -@usableFromInline -internal final class ClientInterceptorPipeline { - /// A logger. - @usableFromInline - internal var logger: Logger - - /// The `EventLoop` this RPC is being executed on. - @usableFromInline - internal let eventLoop: EventLoop - - /// The details of the call. - @usableFromInline - internal let details: CallDetails - - /// A task for closing the RPC in case of a timeout. - @usableFromInline - internal var _scheduledClose: Scheduled? - - @usableFromInline - internal let _errorDelegate: ClientErrorDelegate? - - @usableFromInline - internal private(set) var _onError: ((Error) -> Void)? - - @usableFromInline - internal private(set) var _onCancel: ((EventLoopPromise?) -> Void)? - - @usableFromInline - internal private(set) var _onRequestPart: - ((GRPCClientRequestPart, EventLoopPromise?) -> Void)? - - @usableFromInline - internal private(set) var _onResponsePart: ((GRPCClientResponsePart) -> Void)? - - /// The index after the last user interceptor context index. (i.e. `_userContexts.endIndex`). - @usableFromInline - internal let _headIndex: Int - - /// The index before the first user interceptor context index (always -1). - @usableFromInline - internal let _tailIndex: Int - - @usableFromInline - internal var _userContexts: [ClientInterceptorContext] - - /// Whether the interceptor pipeline is still open. It becomes closed after an 'end' response - /// part has traversed the pipeline. - @usableFromInline - internal var _isOpen = true - - /// The index of the next context on the inbound side of the context at the given index. - @inlinable - internal func _nextInboundIndex(after index: Int) -> Int { - // Unchecked arithmetic is okay here: our smallest inbound index is '_tailIndex' but we will - // never ask for the inbound index after the tail. - assert(self._indexIsValid(index)) - return index &- 1 - } - - /// The index of the next context on the outbound side of the context at the given index. - @inlinable - internal func _nextOutboundIndex(after index: Int) -> Int { - // Unchecked arithmetic is okay here: our greatest outbound index is '_headIndex' but we will - // never ask for the outbound index after the head. - assert(self._indexIsValid(index)) - return index &+ 1 - } - - /// Returns true of the index is in the range `_tailIndex ... _headIndex`. - @inlinable - internal func _indexIsValid(_ index: Int) -> Bool { - return index >= self._tailIndex && index <= self._headIndex - } - - @inlinable - internal init( - eventLoop: EventLoop, - details: CallDetails, - logger: Logger, - interceptors: [ClientInterceptor], - errorDelegate: ClientErrorDelegate?, - onError: @escaping (Error) -> Void, - onCancel: @escaping (EventLoopPromise?) -> Void, - onRequestPart: @escaping (GRPCClientRequestPart, EventLoopPromise?) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - self.eventLoop = eventLoop - self.details = details - self.logger = logger - - self._errorDelegate = errorDelegate - self._onError = onError - self._onCancel = onCancel - self._onRequestPart = onRequestPart - self._onResponsePart = onResponsePart - - // The tail is before the interceptors. - self._tailIndex = -1 - // The head is after the interceptors. - self._headIndex = interceptors.endIndex - - // Make some contexts. - self._userContexts = [] - self._userContexts.reserveCapacity(interceptors.count) - - for index in 0 ..< interceptors.count { - let context = ClientInterceptorContext(for: interceptors[index], atIndex: index, in: self) - self._userContexts.append(context) - } - - self._setupDeadline() - } - - /// Emit a response part message into the interceptor pipeline. - /// - /// This should be called by the transport layer when receiving a response part from the server. - /// - /// - Parameter part: The part to emit into the pipeline. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func receive(_ part: GRPCClientResponsePart) { - self.invokeReceive(part, fromContextAtIndex: self._headIndex) - } - - /// Invoke receive on the appropriate context when called from the context at the given index. - @inlinable - internal func invokeReceive( - _ part: GRPCClientResponsePart, - fromContextAtIndex index: Int - ) { - self._invokeReceive(part, onContextAtIndex: self._nextInboundIndex(after: index)) - } - - /// Invoke receive on the context at the given index, if doing so is safe. - @inlinable - internal func _invokeReceive( - _ part: GRPCClientResponsePart, - onContextAtIndex index: Int - ) { - self.eventLoop.assertInEventLoop() - assert(self._indexIsValid(index)) - guard self._isOpen else { - return - } - - self._invokeReceive(part, onContextAtUncheckedIndex: index) - } - - /// Invoke receive on the context at the given index, assuming that the index is valid and the - /// pipeline is still open. - @inlinable - internal func _invokeReceive( - _ part: GRPCClientResponsePart, - onContextAtUncheckedIndex index: Int - ) { - switch index { - case self._headIndex: - self._invokeReceive(part, onContextAtUncheckedIndex: self._nextInboundIndex(after: index)) - - case self._tailIndex: - if part.isEnd { - // Update our state before handling the response part. - self._isOpen = false - self._onResponsePart?(part) - self.close() - } else { - self._onResponsePart?(part) - } - - default: - self._userContexts[index].invokeReceive(part) - } - } - - /// Emit an error into the interceptor pipeline. - /// - /// This should be called by the transport layer when receiving an error. - /// - /// - Parameter error: The error to emit. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func errorCaught(_ error: Error) { - self.invokeErrorCaught(error, fromContextAtIndex: self._headIndex) - } - - /// Invoke `errorCaught` on the appropriate context when called from the context at the given - /// index. - @inlinable - internal func invokeErrorCaught(_ error: Error, fromContextAtIndex index: Int) { - self._invokeErrorCaught(error, onContextAtIndex: self._nextInboundIndex(after: index)) - } - - /// Invoke `errorCaught` on the context at the given index if that index exists and the pipeline - /// is still open. - @inlinable - internal func _invokeErrorCaught(_ error: Error, onContextAtIndex index: Int) { - self.eventLoop.assertInEventLoop() - assert(self._indexIsValid(index)) - guard self._isOpen else { - return - } - self._invokeErrorCaught(error, onContextAtUncheckedIndex: index) - } - - /// Invoke `errorCaught` on the context at the given index assuming the index exists and the - /// pipeline is still open. - @inlinable - internal func _invokeErrorCaught(_ error: Error, onContextAtUncheckedIndex index: Int) { - switch index { - case self._headIndex: - self._invokeErrorCaught(error, onContextAtIndex: self._nextInboundIndex(after: index)) - - case self._tailIndex: - self._errorCaught(error) - - default: - self._userContexts[index].invokeErrorCaught(error) - } - } - - /// Handles a caught error which has traversed the interceptor pipeline. - @usableFromInline - internal func _errorCaught(_ error: Error) { - // We're about to call out to an error handler: update our state first. - self._isOpen = false - var unwrappedError: Error - - // Unwrap the error, if possible. - if let errorContext = error as? GRPCError.WithContext { - unwrappedError = errorContext.error - self._errorDelegate?.didCatchError( - errorContext.error, - logger: self.logger, - file: errorContext.file, - line: errorContext.line - ) - } else { - unwrappedError = error - self._errorDelegate?.didCatchErrorWithoutContext(error, logger: self.logger) - } - - // Emit the unwrapped error. - self._onError?(unwrappedError) - - // Close the pipeline. - self.close() - } - - /// Writes a request message into the interceptor pipeline. - /// - /// This should be called by the call object to send requests parts to the transport. - /// - /// - Parameters: - /// - part: The request part to write. - /// - promise: A promise to complete when the request part has been successfully written. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func send(_ part: GRPCClientRequestPart, promise: EventLoopPromise?) { - self.invokeSend(part, promise: promise, fromContextAtIndex: self._tailIndex) - } - - /// Invoke send on the appropriate context when called from the context at the given index. - @inlinable - internal func invokeSend( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - fromContextAtIndex index: Int - ) { - self._invokeSend( - part, - promise: promise, - onContextAtIndex: self._nextOutboundIndex(after: index) - ) - } - - /// Invoke send on the context at the given index, if it exists and the pipeline is still open. - @inlinable - internal func _invokeSend( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - onContextAtIndex index: Int - ) { - self.eventLoop.assertInEventLoop() - assert(self._indexIsValid(index)) - guard self._isOpen else { - promise?.fail(GRPCError.AlreadyComplete()) - return - } - self._invokeSend(part, promise: promise, onContextAtUncheckedIndex: index) - } - - /// Invoke send on the context at the given index assuming the index exists and the pipeline is - /// still open. - @inlinable - internal func _invokeSend( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - onContextAtUncheckedIndex index: Int - ) { - switch index { - case self._headIndex: - self._onRequestPart?(part, promise) - - case self._tailIndex: - self._invokeSend( - part, - promise: promise, - onContextAtUncheckedIndex: self._nextOutboundIndex(after: index) - ) - - default: - self._userContexts[index].invokeSend(part, promise: promise) - } - } - - /// Send a request to cancel the RPC through the interceptor pipeline. - /// - /// This should be called by the call object when attempting to cancel the RPC. - /// - /// - Parameter promise: A promise to complete when the cancellation request has been handled. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func cancel(promise: EventLoopPromise?) { - self.invokeCancel(promise: promise, fromContextAtIndex: self._tailIndex) - } - - /// Invoke `cancel` on the appropriate context when called from the context at the given index. - @inlinable - internal func invokeCancel(promise: EventLoopPromise?, fromContextAtIndex index: Int) { - self._invokeCancel(promise: promise, onContextAtIndex: self._nextOutboundIndex(after: index)) - } - - /// Invoke `cancel` on the context at the given index if the index is valid and the pipeline is - /// still open. - @inlinable - internal func _invokeCancel( - promise: EventLoopPromise?, - onContextAtIndex index: Int - ) { - self.eventLoop.assertInEventLoop() - assert(self._indexIsValid(index)) - guard self._isOpen else { - promise?.fail(GRPCError.AlreadyComplete()) - return - } - self._invokeCancel(promise: promise, onContextAtUncheckedIndex: index) - } - - /// Invoke `cancel` on the context at the given index assuming the index is valid and the - /// pipeline is still open. - @inlinable - internal func _invokeCancel( - promise: EventLoopPromise?, - onContextAtUncheckedIndex index: Int - ) { - switch index { - case self._headIndex: - self._onCancel?(promise) - - case self._tailIndex: - self._invokeCancel( - promise: promise, - onContextAtUncheckedIndex: self._nextOutboundIndex(after: index) - ) - - default: - self._userContexts[index].invokeCancel(promise: promise) - } - } -} - -// MARK: - Lifecycle - -extension ClientInterceptorPipeline { - /// Closes the pipeline. This should be called once, by the tail interceptor, to indicate that - /// the RPC has completed. If this is not called, we will leak. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func close() { - self.eventLoop.assertInEventLoop() - self._isOpen = false - - // Cancel the timeout. - self._scheduledClose?.cancel() - self._scheduledClose = nil - - // Drop the contexts since they reference us. - self._userContexts.removeAll() - - // Cancel the transport. - self._onCancel?(nil) - - // `ClientTransport` holds a reference to us and references to itself via these callbacks. Break - // these references now by replacing the callbacks. - self._onError = nil - self._onCancel = nil - self._onRequestPart = nil - self._onResponsePart = nil - } - - /// Sets up a deadline for the pipeline. - @inlinable - internal func _setupDeadline() { - func setup() { - self.eventLoop.assertInEventLoop() - - let timeLimit = self.details.options.timeLimit - let deadline = timeLimit.makeDeadline() - - // There's no point scheduling this. - if deadline == .distantFuture { - return - } - - self._scheduledClose = self.eventLoop.scheduleTask(deadline: deadline) { - // When the error hits the tail we'll call 'close()', this will cancel the transport if - // necessary. - self.errorCaught(GRPCError.RPCTimedOut(timeLimit)) - } - } - - if self.eventLoop.inEventLoop { - setup() - } else { - self.eventLoop.execute { - setup() - } - } - } -} - -extension ClientInterceptorContext { - @inlinable - internal func invokeReceive(_ part: GRPCClientResponsePart) { - self.interceptor.receive(part, context: self) - } - - @inlinable - internal func invokeSend( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise? - ) { - self.interceptor.send(part, promise: promise, context: self) - } - - @inlinable - internal func invokeCancel(promise: EventLoopPromise?) { - self.interceptor.cancel(promise: promise, context: self) - } - - @inlinable - internal func invokeErrorCaught(_ error: Error) { - self.interceptor.errorCaught(error, context: self) - } -} diff --git a/Sources/GRPC/Interceptor/ClientInterceptorProtocol.swift b/Sources/GRPC/Interceptor/ClientInterceptorProtocol.swift deleted file mode 100644 index 968d15f15..000000000 --- a/Sources/GRPC/Interceptor/ClientInterceptorProtocol.swift +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -internal protocol ClientInterceptorProtocol { - associatedtype Request - associatedtype Response - - /// Called when the interceptor has received a response part to handle. - func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) - - /// Called when the interceptor has received an error to handle. - func errorCaught( - _ error: Error, - context: ClientInterceptorContext - ) - - /// Called when the interceptor has received a request part to handle. - func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) - - /// Called when the interceptor has received a request to cancel the RPC. - func cancel( - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) -} diff --git a/Sources/GRPC/Interceptor/ClientInterceptors.swift b/Sources/GRPC/Interceptor/ClientInterceptors.swift deleted file mode 100644 index 74d567567..000000000 --- a/Sources/GRPC/Interceptor/ClientInterceptors.swift +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -/// A base class for client interceptors. -/// -/// Interceptors allow request and response parts to be observed, mutated or dropped as necessary. -/// The default behaviour for this base class is to forward any events to the next interceptor. -/// -/// Interceptors may observe a number of different events: -/// - receiving response parts with ``receive(_:context:)-5v1ih``, -/// - receiving errors with ``errorCaught(_:context:)-6pncp``, -/// - sending request parts with ``send(_:promise:context:)-4igtj``, and -/// - RPC cancellation with ``cancel(promise:context:)-5tkf5``. -/// -/// These events flow through a pipeline of interceptors for each RPC. Request parts sent from the -/// call object (e.g. ``UnaryCall``, ``BidirectionalStreamingCall``) will traverse the pipeline in the -/// outbound direction from its tail via ``send(_:promise:context:)-4igtj`` eventually reaching the head of the -/// pipeline where it will be sent sent to the server. -/// -/// Response parts, or errors, received from the transport fill be fired in the inbound direction -/// back through the interceptor pipeline via ``receive(_:context:)-5v1ih`` and ``errorCaught(_:context:)-6pncp``, -/// respectively. Note that the `end` response part and any error received are terminal: the -/// pipeline will be torn down once these parts reach the the tail and are a signal that the -/// interceptor should free up any resources it may be using. -/// -/// Each of the interceptor functions is provided with a `context` which exposes analogous functions -/// (``receive(_:context:)-5v1ih``, ``errorCaught(_:context:)-6pncp``, ``send(_:promise:context:)-4igtj``, and ``cancel(promise:context:)-5tkf5``) which may be -/// called to forward events to the next interceptor in the appropriate direction. -/// -/// ### Thread Safety -/// -/// Functions on `context` are not thread safe and **must** be called on the `EventLoop` found on -/// the `context`. Since each interceptor is invoked on the same `EventLoop` this does not usually -/// require any extra attention. However, if work is done on a `DispatchQueue` or _other_ -/// `EventLoop` then implementers should ensure that they use `context` from the correct -/// `EventLoop`. -@preconcurrency open class ClientInterceptor: @unchecked Sendable { - public init() {} - - /// Called when the interceptor has received a response part to handle. - /// - Parameters: - /// - part: The response part which has been received from the server. - /// - context: An interceptor context which may be used to forward the response part. - open func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - context.receive(part) - } - - /// Called when the interceptor has received an error. - /// - Parameters: - /// - error: The error. - /// - context: An interceptor context which may be used to forward the error. - open func errorCaught( - _ error: Error, - context: ClientInterceptorContext - ) { - context.errorCaught(error) - } - - /// Called when the interceptor has received a request part to handle. - /// - Parameters: - /// - part: The request part which should be sent to the server. - /// - promise: A promise which should be completed when the response part has been handled. - /// - context: An interceptor context which may be used to forward the request part. - open func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - context.send(part, promise: promise) - } - - /// Called when the interceptor has received a request to cancel the RPC. - /// - Parameters: - /// - promise: A promise which should be cancellation request has been handled. - /// - context: An interceptor context which may be used to forward the cancellation request. - open func cancel( - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - context.cancel(promise: promise) - } -} diff --git a/Sources/GRPC/Interceptor/ClientTransport.swift b/Sources/GRPC/Interceptor/ClientTransport.swift deleted file mode 100644 index 795b29312..000000000 --- a/Sources/GRPC/Interceptor/ClientTransport.swift +++ /dev/null @@ -1,1061 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOHPACK -import NIOHTTP2 - -/// This class is the glue between a `NIO.Channel` and the `ClientInterceptorPipeline`. In fact -/// this object owns the interceptor pipeline and is also a `ChannelHandler`. The caller has very -/// little API to use on this class: they may configure the transport by adding it to a -/// `NIO.ChannelPipeline` with `configure(_:)`, send request parts via `send(_:promise:)` and -/// attempt to cancel the RPC with `cancel(promise:)`. Response parts – after traversing the -/// interceptor pipeline – are emitted to the `onResponsePart` callback supplied to the initializer. -/// -/// In most instances the glue code is simple: transformations are applied to the request and -/// response types used by the interceptor pipeline and the `NIO.Channel`. In addition, the -/// transport keeps track of the state of the call and the `Channel`, taking appropriate action -/// when these change. This includes buffering request parts from the interceptor pipeline until -/// the `NIO.Channel` becomes active. -/// -/// ### Thread Safety -/// -/// This class is not thread safe. All methods **must** be executed on the transport's `callEventLoop`. -@usableFromInline -internal final class ClientTransport { - /// The `EventLoop` the call is running on. State must be accessed from this event loop. - @usableFromInline - internal let callEventLoop: EventLoop - - /// The current state of the transport. - private var state: ClientTransportState = .idle - - /// A promise for the underlying `Channel`. We'll succeed this when we transition to `active` - /// and fail it when we transition to `closed`. - private var channelPromise: EventLoopPromise? - - // Note: initial capacity is 4 because it's a power of 2 and most calls are unary so will - // have 3 parts. - /// A buffer to store request parts and promises in before the channel has become active. - private var writeBuffer = MarkedCircularBuffer(initialCapacity: 4) - - /// The request serializer. - private let serializer: AnySerializer - - /// The response deserializer. - private let deserializer: AnyDeserializer - - /// A request part and a promise. - private struct RequestAndPromise { - var request: GRPCClientRequestPart - var promise: EventLoopPromise? - } - - /// Details about the call. - internal let callDetails: CallDetails - - /// A logger. - internal var logger: Logger - - /// Is the call streaming requests? - private var isStreamingRequests: Bool { - switch self.callDetails.type { - case .unary, .serverStreaming: - return false - case .clientStreaming, .bidirectionalStreaming: - return true - } - } - - // Our `NIO.Channel` will fire trailers and the `GRPCStatus` to us separately. It's more - // convenient to have both at the same time when intercepting response parts. We'll hold on to the - // trailers here and only forward them when we receive the status. - private var trailers: HPACKHeaders? - - /// The interceptor pipeline connected to this transport. The pipeline also holds references - /// to `self` which are dropped when the interceptor pipeline is closed. - @usableFromInline - internal var _pipeline: ClientInterceptorPipeline? - - /// The `NIO.Channel` used by the transport, if it is available. - private var channel: Channel? - - /// A callback which is invoked once when the stream channel becomes active. - private var onStart: (() -> Void)? - - /// Our current state as logging metadata. - private var stateForLogging: Logger.MetadataValue { - if self.state.mayBuffer { - return "\(self.state) (\(self.writeBuffer.count) parts buffered)" - } else { - return "\(self.state)" - } - } - - internal init( - details: CallDetails, - eventLoop: EventLoop, - interceptors: [ClientInterceptor], - serializer: AnySerializer, - deserializer: AnyDeserializer, - errorDelegate: ClientErrorDelegate?, - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) { - self.callEventLoop = eventLoop - self.callDetails = details - self.onStart = onStart - self.logger = details.options.logger - self.serializer = serializer - self.deserializer = deserializer - // The references to self held by the pipeline are dropped when it is closed. - self._pipeline = ClientInterceptorPipeline( - eventLoop: eventLoop, - details: details, - logger: details.options.logger, - interceptors: interceptors, - errorDelegate: errorDelegate, - onError: onError, - onCancel: self.cancelFromPipeline(promise:), - onRequestPart: self.sendFromPipeline(_:promise:), - onResponsePart: onResponsePart - ) - } - - // MARK: - Call Object API - - /// Configure the transport to communicate with the server. - /// - Parameter configurator: A callback to invoke in order to configure this transport. - /// - Important: This *must* to be called from the `callEventLoop`. - internal func configure(_ configurator: @escaping (ChannelHandler) -> EventLoopFuture) { - self.callEventLoop.assertInEventLoop() - if self.state.configureTransport() { - self.configure(using: configurator) - } - } - - /// Send a request part – via the interceptor pipeline – to the server. - /// - Parameters: - /// - part: The part to send. - /// - promise: A promise which will be completed when the request part has been handled. - /// - Important: This *must* to be called from the `callEventLoop`. - @inlinable - internal func send(_ part: GRPCClientRequestPart, promise: EventLoopPromise?) { - self.callEventLoop.assertInEventLoop() - if let pipeline = self._pipeline { - pipeline.send(part, promise: promise) - } else { - promise?.fail(GRPCError.AlreadyComplete()) - } - } - - /// Attempt to cancel the RPC notifying any interceptors. - /// - Parameter promise: A promise which will be completed when the cancellation attempt has - /// been handled. - internal func cancel(promise: EventLoopPromise?) { - self.callEventLoop.assertInEventLoop() - if let pipeline = self._pipeline { - pipeline.cancel(promise: promise) - } else { - promise?.fail(GRPCError.AlreadyComplete()) - } - } - - /// A request for the underlying `Channel`. - internal func getChannel() -> EventLoopFuture { - self.callEventLoop.assertInEventLoop() - - // Do we already have a promise? - if let promise = self.channelPromise { - return promise.futureResult - } else { - // Make and store the promise. - let promise = self.callEventLoop.makePromise(of: Channel.self) - self.channelPromise = promise - - // Ask the state machine if we can have it. - switch self.state.getChannel() { - case .succeed: - if let channel = self.channel { - promise.succeed(channel) - } - - case .fail: - promise.fail(GRPCError.AlreadyComplete()) - - case .doNothing: - () - } - - return promise.futureResult - } - } -} - -// MARK: - Pipeline API - -extension ClientTransport { - /// Sends a request part on the transport. Should only be called from the interceptor pipeline. - /// - Parameters: - /// - part: The request part to send. - /// - promise: A promise which will be completed when the part has been handled. - /// - Important: This *must* to be called from the `callEventLoop`. - private func sendFromPipeline( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise? - ) { - self.callEventLoop.assertInEventLoop() - switch self.state.send() { - case .writeToBuffer: - self.buffer(part, promise: promise) - - case .writeToChannel: - // Banging the channel is okay here: we'll only be told to 'writeToChannel' if we're in the - // correct state, the requirements of that state are having an active `Channel`. - self.writeToChannel( - self.channel!, - part: part, - promise: promise, - flush: self.shouldFlush(after: part) - ) - - case .alreadyComplete: - promise?.fail(GRPCError.AlreadyComplete()) - } - } - - /// Attempt to cancel the RPC. Should only be called from the interceptor pipeline. - /// - Parameter promise: A promise which will be completed when the cancellation has been handled. - /// - Important: This *must* to be called from the `callEventLoop`. - private func cancelFromPipeline(promise: EventLoopPromise?) { - self.callEventLoop.assertInEventLoop() - - if self.state.cancel() { - let error = GRPCError.RPCCancelledByClient() - let status = error.makeGRPCStatus() - self.forwardToInterceptors(.end(status, [:])) - self.failBufferedWrites(with: error) - self.channel?.close(mode: .all, promise: nil) - self.channelPromise?.fail(error) - promise?.succeed(()) - } else { - promise?.succeed(()) - } - } -} - -// MARK: - ChannelHandler API - -extension ClientTransport: ChannelInboundHandler { - @usableFromInline - typealias InboundIn = _RawGRPCClientResponsePart - - @usableFromInline - typealias OutboundOut = _RawGRPCClientRequestPart - - @usableFromInline - internal func handlerRemoved(context: ChannelHandlerContext) { - self.dropReferences() - } - - @usableFromInline - internal func handlerAdded(context: ChannelHandlerContext) { - if context.channel.isActive { - self.transportActivated(channel: context.channel) - } - } - - @usableFromInline - internal func errorCaught(context: ChannelHandlerContext, error: Error) { - self.handleError(error) - } - - @usableFromInline - internal func channelActive(context: ChannelHandlerContext) { - self.transportActivated(channel: context.channel) - } - - @usableFromInline - internal func channelInactive(context: ChannelHandlerContext) { - self.transportDeactivated() - } - - @usableFromInline - internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - switch self.unwrapInboundIn(data) { - case let .initialMetadata(headers): - self.receiveFromChannel(initialMetadata: headers) - - case let .message(box): - self.receiveFromChannel(message: box.message) - - case let .trailingMetadata(trailers): - self.receiveFromChannel(trailingMetadata: trailers) - - case let .status(status): - self.receiveFromChannel(status: status) - } - - // (We're the end of the channel. No need to forward anything.) - } -} - -extension ClientTransport { - /// The `Channel` became active. Send out any buffered requests. - private func transportActivated(channel: Channel) { - if self.callEventLoop.inEventLoop { - self._transportActivated(channel: channel) - } else { - self.callEventLoop.execute { - self._transportActivated(channel: channel) - } - } - } - - /// On-loop implementation of `transportActivated(channel:)`. - private func _transportActivated(channel: Channel) { - self.callEventLoop.assertInEventLoop() - - switch self.state.activate() { - case .unbuffer: - self.logger.addIPAddressMetadata(local: channel.localAddress, remote: channel.remoteAddress) - self._pipeline?.logger = self.logger - self.logger.debug("activated stream channel") - self.channel = channel - self.unbuffer() - - case .close: - channel.close(mode: .all, promise: nil) - - case .doNothing: - () - } - } - - /// The `Channel` became inactive. Fail any buffered writes and forward an error to the - /// interceptor pipeline if necessary. - private func transportDeactivated() { - if self.callEventLoop.inEventLoop { - self._transportDeactivated() - } else { - self.callEventLoop.execute { - self._transportDeactivated() - } - } - } - - /// On-loop implementation of `transportDeactivated()`. - private func _transportDeactivated() { - self.callEventLoop.assertInEventLoop() - switch self.state.deactivate() { - case .doNothing: - () - - case .tearDown: - let status = GRPCStatus(code: .unavailable, message: "Transport became inactive") - self.forwardErrorToInterceptors(status) - self.failBufferedWrites(with: status) - self.channelPromise?.fail(status) - - case .failChannelPromise: - self.channelPromise?.fail(GRPCError.AlreadyComplete()) - } - } - - /// Drops any references to the `Channel` and interceptor pipeline. - private func dropReferences() { - if self.callEventLoop.inEventLoop { - self.channel = nil - } else { - self.callEventLoop.execute { - self.channel = nil - } - } - } - - /// Handles an error caught in the pipeline or from elsewhere. The error may be forwarded to the - /// interceptor pipeline and any buffered writes will be failed. Any underlying `Channel` will - /// also be closed. - internal func handleError(_ error: Error) { - if self.callEventLoop.inEventLoop { - self._handleError(error) - } else { - self.callEventLoop.execute { - self._handleError(error) - } - } - } - - /// On-loop implementation of `handleError(_:)`. - private func _handleError(_ error: Error) { - self.callEventLoop.assertInEventLoop() - - switch self.state.handleError() { - case .doNothing: - () - - case .propagateError: - self.forwardErrorToInterceptors(error) - self.failBufferedWrites(with: error) - - case .propagateErrorAndClose: - self.forwardErrorToInterceptors(error) - self.failBufferedWrites(with: error) - self.channel?.close(mode: .all, promise: nil) - } - } - - /// Receive initial metadata from the `Channel`. - private func receiveFromChannel(initialMetadata headers: HPACKHeaders) { - if self.callEventLoop.inEventLoop { - self._receiveFromChannel(initialMetadata: headers) - } else { - self.callEventLoop.execute { - self._receiveFromChannel(initialMetadata: headers) - } - } - } - - /// On-loop implementation of `receiveFromChannel(initialMetadata:)`. - private func _receiveFromChannel(initialMetadata headers: HPACKHeaders) { - self.callEventLoop.assertInEventLoop() - if self.state.channelRead(isEnd: false) { - self.forwardToInterceptors(.metadata(headers)) - } - } - - /// Receive response message bytes from the `Channel`. - private func receiveFromChannel(message buffer: ByteBuffer) { - if self.callEventLoop.inEventLoop { - self._receiveFromChannel(message: buffer) - } else { - self.callEventLoop.execute { - self._receiveFromChannel(message: buffer) - } - } - } - - /// On-loop implementation of `receiveFromChannel(message:)`. - private func _receiveFromChannel(message buffer: ByteBuffer) { - self.callEventLoop.assertInEventLoop() - do { - let message = try self.deserializer.deserialize(byteBuffer: buffer) - if self.state.channelRead(isEnd: false) { - self.forwardToInterceptors(.message(message)) - } - } catch { - self.handleError(error) - } - } - - /// Receive trailing metadata from the `Channel`. - private func receiveFromChannel(trailingMetadata trailers: HPACKHeaders) { - // The `Channel` delivers trailers and `GRPCStatus` separately, we want to emit them together - // in the interceptor pipeline. - if self.callEventLoop.inEventLoop { - self.trailers = trailers - } else { - self.callEventLoop.execute { - self.trailers = trailers - } - } - } - - /// Receive the final status from the `Channel`. - private func receiveFromChannel(status: GRPCStatus) { - if self.callEventLoop.inEventLoop { - self._receiveFromChannel(status: status) - } else { - self.callEventLoop.execute { - self._receiveFromChannel(status: status) - } - } - } - - /// On-loop implementation of `receiveFromChannel(status:)`. - private func _receiveFromChannel(status: GRPCStatus) { - self.callEventLoop.assertInEventLoop() - if self.state.channelRead(isEnd: true) { - self.forwardToInterceptors(.end(status, self.trailers ?? [:])) - self.trailers = nil - } - } -} - -// MARK: - State Handling - -private enum ClientTransportState { - /// Idle. We're waiting for the RPC to be configured. - /// - /// Valid transitions: - /// - `awaitingTransport` (the transport is being configured) - /// - `closed` (the RPC cancels) - case idle - - /// Awaiting transport. The RPC has requested transport and we're waiting for that transport to - /// activate. We'll buffer any outbound messages from this state. Receiving messages from the - /// transport in this state is an error. - /// - /// Valid transitions: - /// - `activatingTransport` (the channel becomes active) - /// - `closing` (the RPC cancels) - /// - `closed` (the channel fails to become active) - case awaitingTransport - - /// The transport is active but we're unbuffering any requests to write on that transport. - /// We'll continue buffering in this state. Receiving messages from the transport in this state - /// is okay. - /// - /// Valid transitions: - /// - `active` (we finish unbuffering) - /// - `closing` (the RPC cancels, the channel encounters an error) - /// - `closed` (the channel becomes inactive) - case activatingTransport - - /// Fully active. An RPC is in progress and is communicating over an active transport. - /// - /// Valid transitions: - /// - `closing` (the RPC cancels, the channel encounters an error) - /// - `closed` (the channel becomes inactive) - case active - - /// Closing. Either the RPC was cancelled or any `Channel` associated with the transport hasn't - /// become inactive yet. - /// - /// Valid transitions: - /// - `closed` (the channel becomes inactive) - case closing - - /// We're closed. Any writes from the RPC will be failed. Any responses from the transport will - /// be ignored. - /// - /// Valid transitions: - /// - none: this state is terminal. - case closed - - /// Whether writes may be unbuffered in this state. - internal var isUnbuffering: Bool { - switch self { - case .activatingTransport: - return true - case .idle, .awaitingTransport, .active, .closing, .closed: - return false - } - } - - /// Whether this state allows writes to be buffered. (This is useful only to inform logging.) - internal var mayBuffer: Bool { - switch self { - case .idle, .activatingTransport, .awaitingTransport: - return true - case .active, .closing, .closed: - return false - } - } -} - -extension ClientTransportState { - /// The caller would like to configure the transport. Returns a boolean indicating whether we - /// should configure it or not. - mutating func configureTransport() -> Bool { - switch self { - // We're idle until we configure. Anything else is just a repeat request to configure. - case .idle: - self = .awaitingTransport - return true - - case .awaitingTransport, .activatingTransport, .active, .closing, .closed: - return false - } - } - - enum SendAction { - /// Write the request into the buffer. - case writeToBuffer - /// Write the request into the channel. - case writeToChannel - /// The RPC has already completed, fail any promise associated with the write. - case alreadyComplete - } - - /// The pipeline would like to send a request part to the transport. - mutating func send() -> SendAction { - switch self { - // We don't have any transport yet, just buffer the part. - case .idle, .awaitingTransport, .activatingTransport: - return .writeToBuffer - - // We have a `Channel`, we can pipe the write straight through. - case .active: - return .writeToChannel - - // The transport is going or has gone away. Fail the promise. - case .closing, .closed: - return .alreadyComplete - } - } - - enum UnbufferedAction { - /// Nothing needs to be done. - case doNothing - /// Succeed the channel promise associated with the transport. - case succeedChannelPromise - } - - /// We finished dealing with the buffered writes. - mutating func unbuffered() -> UnbufferedAction { - switch self { - // These can't happen since we only begin unbuffering when we transition to - // '.activatingTransport', which must come after these two states.. - case .idle, .awaitingTransport: - preconditionFailure("Requests can't be unbuffered before the transport is activated") - - // We dealt with any buffered writes. We can become active now. This is the only way to become - // active. - case .activatingTransport: - self = .active - return .succeedChannelPromise - - case .active: - preconditionFailure("Unbuffering completed but the transport is already active") - - // Something caused us to close while unbuffering, that's okay, we won't take any further - // action. - case .closing, .closed: - return .doNothing - } - } - - /// Cancel the RPC and associated `Channel`, if possible. Returns a boolean indicated whether - /// cancellation can go ahead (and also whether the channel should be torn down). - mutating func cancel() -> Bool { - switch self { - case .idle: - // No RPC has been started and we don't have a `Channel`. We need to tell the interceptor - // we're done, fail any writes, and then deal with the cancellation promise. - self = .closed - return true - - case .awaitingTransport: - // An RPC has started and we're waiting for the `Channel` to activate. We'll mark ourselves as - // closing. We don't need to explicitly close the `Channel`, this will happen as a result of - // the `Channel` becoming active (see `channelActive(context:)`). - self = .closing - return true - - case .activatingTransport: - // The RPC has started, the `Channel` is active and we're emptying our write buffer. We'll - // mark ourselves as closing: we'll error the interceptor pipeline, close the channel, fail - // any buffered writes and then complete the cancellation promise. - self = .closing - return true - - case .active: - // The RPC and channel are up and running. We'll fail the RPC and close the channel. - self = .closing - return true - - case .closing, .closed: - // We're already closing or closing. The cancellation is too late. - return false - } - } - - enum ActivateAction { - case unbuffer - case close - case doNothing - } - - /// `channelActive` was invoked on the transport by the `Channel`. - mutating func activate() -> ActivateAction { - // The channel has become active: what now? - switch self { - case .idle: - preconditionFailure("Can't activate an idle transport") - - case .awaitingTransport: - self = .activatingTransport - return .unbuffer - - case .activatingTransport, .active: - // Already activated. - return .doNothing - - case .closing: - // We remain in closing: we only transition to closed on 'channelInactive'. - return .close - - case .closed: - preconditionFailure("Invalid state: stream is already inactive") - } - } - - enum ChannelInactiveAction { - /// Tear down the transport; forward an error to the interceptors and fail any buffered writes. - case tearDown - /// Fail the 'Channel' promise, if one exists; the RPC is already complete. - case failChannelPromise - /// Do nothing. - case doNothing - } - - /// `channelInactive` was invoked on the transport by the `Channel`. - mutating func deactivate() -> ChannelInactiveAction { - switch self { - case .idle: - // We can't become inactive before we've requested a `Channel`. - preconditionFailure("Can't deactivate an idle transport") - - case .awaitingTransport, .activatingTransport, .active: - // We're activating the transport - i.e. offloading any buffered requests - and the channel - // became inactive. We haven't received an error (otherwise we'd be `closing`) so we should - // synthesize an error status to fail the RPC with. - self = .closed - return .tearDown - - case .closing: - // We were already closing, now we're fully closed. - self = .closed - return .failChannelPromise - - case .closed: - // We're already closed. - return .doNothing - } - } - - /// `channelRead` was invoked on the transport by the `Channel`. Returns a boolean value - /// indicating whether the part that was read should be forwarded to the interceptor pipeline. - mutating func channelRead(isEnd: Bool) -> Bool { - switch self { - case .idle, .awaitingTransport: - // If there's no `Channel` or the `Channel` isn't active, then we can't read anything. - preconditionFailure("Can't receive response part on idle transport") - - case .activatingTransport, .active: - // We have an active `Channel`, we can forward the request part but we may need to start - // closing if we see the status, since it indicates the call is terminating. - if isEnd { - self = .closing - } - return true - - case .closing, .closed: - // We closed early, ignore any reads. - return false - } - } - - enum HandleErrorAction { - /// Propagate the error to the interceptor pipeline and fail any buffered writes. - case propagateError - /// As above, but close the 'Channel' as well. - case propagateErrorAndClose - /// No action is required. - case doNothing - } - - /// An error was caught. - mutating func handleError() -> HandleErrorAction { - switch self { - case .idle: - // The `Channel` can't error if it doesn't exist. - preconditionFailure("Can't catch error on idle transport") - - case .awaitingTransport: - // We're waiting for the `Channel` to become active. We're toast now, so close, failing any - // buffered writes along the way. - self = .closing - return .propagateError - - case .activatingTransport, - .active: - // We're either fully active or unbuffering. Forward an error, fail any writes and then close. - self = .closing - return .propagateErrorAndClose - - case .closing, .closed: - // We're already closing/closed, we can ignore this. - return .doNothing - } - } - - enum GetChannelAction { - /// No action is required. - case doNothing - /// Succeed the Channel promise. - case succeed - /// Fail the 'Channel' promise, the RPC is already complete. - case fail - } - - /// The caller has asked for the underlying `Channel`. - mutating func getChannel() -> GetChannelAction { - switch self { - case .idle, .awaitingTransport, .activatingTransport: - // Do nothing, we'll complete the promise when we become active or closed. - return .doNothing - - case .active: - // We're already active, so there was no promise to succeed when we made this transition. We - // can complete it now. - return .succeed - - case .closing: - // We'll complete the promise when we transition to closed. - return .doNothing - - case .closed: - // We're already closed; there was no promise to fail when we made this transition. We can go - // ahead and fail it now though. - return .fail - } - } -} - -// MARK: - State Actions - -extension ClientTransport { - /// Configures this transport with the `configurator`. - private func configure(using configurator: (ChannelHandler) -> EventLoopFuture) { - configurator(self).whenFailure { error in - // We might be on a different EL, but `handleError` will sort that out for us, so no need to - // hop. - if error is GRPCStatus || error is GRPCStatusTransformable { - self.handleError(error) - } else { - // Fallback to something which will mark the RPC as 'unavailable'. - self.handleError(ConnectionFailure(reason: error)) - } - } - } - - /// Append a request part to the write buffer. - /// - Parameters: - /// - part: The request part to buffer. - /// - promise: A promise to complete when the request part has been sent. - private func buffer( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise? - ) { - self.callEventLoop.assertInEventLoop() - self.logger.trace( - "buffering request part", - metadata: [ - "request_part": "\(part.name)", - "call_state": self.stateForLogging, - ] - ) - self.writeBuffer.append(.init(request: part, promise: promise)) - } - - /// Writes any buffered request parts to the `Channel`. - private func unbuffer() { - self.callEventLoop.assertInEventLoop() - - guard let channel = self.channel else { - return - } - - // Save any flushing until we're done writing. - var shouldFlush = false - - self.logger.trace( - "unbuffering request parts", - metadata: [ - "request_parts": "\(self.writeBuffer.count)" - ] - ) - - // Why the double loop? A promise completed as a result of the flush may enqueue more writes, - // or causes us to change state (i.e. we may have to close). If we didn't loop around then we - // may miss more buffered writes. - while self.state.isUnbuffering, !self.writeBuffer.isEmpty { - // Pull out as many writes as possible. - while let write = self.writeBuffer.popFirst() { - self.logger.trace( - "unbuffering request part", - metadata: [ - "request_part": "\(write.request.name)" - ] - ) - - if !shouldFlush { - shouldFlush = self.shouldFlush(after: write.request) - } - - self.writeToChannel(channel, part: write.request, promise: write.promise, flush: false) - } - - // Okay, flush now. - if shouldFlush { - shouldFlush = false - channel.flush() - } - } - - if self.writeBuffer.isEmpty { - self.logger.trace("request buffer drained") - } else { - self.logger.notice("unbuffering aborted", metadata: ["call_state": self.stateForLogging]) - } - - // We're unbuffered. What now? - switch self.state.unbuffered() { - case .doNothing: - () - case .succeedChannelPromise: - self.channelPromise?.succeed(channel) - } - } - - /// 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.trace("failing buffered writes", metadata: ["call_state": self.stateForLogging]) - - while let write = self.writeBuffer.popFirst() { - write.promise?.fail(error) - } - } - - /// Write a request part to the `Channel`. - /// - Parameters: - /// - channel: The `Channel` to write `part` to. - /// - part: The request part to write. - /// - promise: A promise to complete once the write has been completed. - /// - flush: Whether to flush the `Channel` after writing. - private func writeToChannel( - _ channel: Channel, - part: GRPCClientRequestPart, - promise: EventLoopPromise?, - flush: Bool - ) { - switch part { - case let .metadata(headers): - let head = self.makeRequestHead(with: headers) - channel.write(self.wrapOutboundOut(.head(head)), promise: promise) - // Messages are buffered by this class and in the async writer for async calls. Initially the - // async writer is not allowed to emit messages; the call to 'onStart()' signals that messages - // may be emitted. We call it here to avoid races between writing headers and messages. - self.onStart?() - self.onStart = nil - - case let .message(request, metadata): - do { - let bytes = try self.serializer.serialize(request, allocator: channel.allocator) - let message = _MessageContext(bytes, compressed: metadata.compress) - channel.write(self.wrapOutboundOut(.message(message)), promise: promise) - } catch { - self.handleError(error) - } - - case .end: - channel.write(self.wrapOutboundOut(.end), promise: promise) - } - - if flush { - channel.flush() - } - } - - /// Forward the response part to the interceptor pipeline. - /// - Parameter part: The response part to forward. - private func forwardToInterceptors(_ part: GRPCClientResponsePart) { - self.callEventLoop.assertInEventLoop() - self._pipeline?.receive(part) - } - - /// Forward the error to the interceptor pipeline. - /// - Parameter error: The error to forward. - private func forwardErrorToInterceptors(_ error: Error) { - self.callEventLoop.assertInEventLoop() - self._pipeline?.errorCaught(error) - } -} - -// MARK: - Helpers - -extension ClientTransport { - /// Returns whether the `Channel` should be flushed after writing the given part to it. - private func shouldFlush(after part: GRPCClientRequestPart) -> Bool { - switch part { - case .metadata: - // If we're not streaming requests then we hold off on the flush until we see end. - return self.isStreamingRequests - - case let .message(_, metadata): - // Message flushing is determined by caller preference. - return metadata.flush - - case .end: - // Always flush at the end of the request stream. - return true - } - } - - /// Make a `_GRPCRequestHead` with the provided metadata. - private func makeRequestHead(with metadata: HPACKHeaders) -> _GRPCRequestHead { - return _GRPCRequestHead( - method: self.callDetails.options.cacheable ? "GET" : "POST", - scheme: self.callDetails.scheme, - path: self.callDetails.path, - host: self.callDetails.authority, - deadline: self.callDetails.options.timeLimit.makeDeadline(), - customMetadata: metadata, - encoding: self.callDetails.options.messageEncoding - ) - } -} - -extension GRPCClientRequestPart { - /// The name of the request part, used for logging. - fileprivate var name: String { - switch self { - case .metadata: - return "metadata" - case .message: - return "message" - case .end: - return "end" - } - } -} - -// A wrapper for connection errors: we need to be able to preserve the underlying error as -// well as extract a 'GRPCStatus' with code '.unavailable'. -internal struct ConnectionFailure: Error, GRPCStatusTransformable, CustomStringConvertible { - /// The reason the connection failed. - var reason: Error - - init(reason: Error) { - self.reason = reason - } - - var description: String { - return String(describing: self.reason) - } - - func makeGRPCStatus() -> GRPCStatus { - return GRPCStatus( - code: .unavailable, - message: String(describing: self.reason), - cause: self.reason - ) - } -} diff --git a/Sources/GRPC/Interceptor/ClientTransportFactory.swift b/Sources/GRPC/Interceptor/ClientTransportFactory.swift deleted file mode 100644 index 5023ebfa7..000000000 --- a/Sources/GRPC/Interceptor/ClientTransportFactory.swift +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOHTTP2 - -import protocol SwiftProtobuf.Message - -/// A `ClientTransport` factory for an RPC. -@usableFromInline -internal struct ClientTransportFactory { - /// The underlying transport factory. - private var factory: Factory - - @usableFromInline - internal enum Factory { - case http2(HTTP2ClientTransportFactory) - case fake(FakeClientTransportFactory) - } - - private init(_ http2: HTTP2ClientTransportFactory) { - self.factory = .http2(http2) - } - - private init(_ fake: FakeClientTransportFactory) { - self.factory = .fake(fake) - } - - /// Create a transport factory for HTTP/2 based transport with `SwiftProtobuf.Message` messages. - /// - Parameters: - /// - multiplexer: The multiplexer used to create an HTTP/2 stream for the RPC. - /// - host: The value of the ":authority" pseudo header. - /// - scheme: The value of the ":scheme" pseudo header. - /// - errorDelegate: A client error delegate. - /// - Returns: A factory for making and configuring HTTP/2 based transport. - @usableFromInline - internal static func http2( - channel: EventLoopFuture, - authority: String, - scheme: String, - maximumReceiveMessageLength: Int, - errorDelegate: ClientErrorDelegate? - ) -> ClientTransportFactory - where - Request: SwiftProtobuf.Message, - Response: SwiftProtobuf.Message - { - let http2 = HTTP2ClientTransportFactory( - streamChannel: channel, - scheme: scheme, - authority: authority, - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer(), - maximumReceiveMessageLength: maximumReceiveMessageLength, - errorDelegate: errorDelegate - ) - return .init(http2) - } - - /// Create a transport factory for HTTP/2 based transport with `GRPCPayload` messages. - /// - Parameters: - /// - multiplexer: The multiplexer used to create an HTTP/2 stream for the RPC. - /// - host: The value of the ":authority" pseudo header. - /// - scheme: The value of the ":scheme" pseudo header. - /// - errorDelegate: A client error delegate. - /// - Returns: A factory for making and configuring HTTP/2 based transport. - @usableFromInline - internal static func http2( - channel: EventLoopFuture, - authority: String, - scheme: String, - maximumReceiveMessageLength: Int, - errorDelegate: ClientErrorDelegate? - ) -> ClientTransportFactory where Request: GRPCPayload, Response: GRPCPayload { - let http2 = HTTP2ClientTransportFactory( - streamChannel: channel, - scheme: scheme, - authority: authority, - serializer: AnySerializer(wrapping: GRPCPayloadSerializer()), - deserializer: AnyDeserializer(wrapping: GRPCPayloadDeserializer()), - maximumReceiveMessageLength: maximumReceiveMessageLength, - errorDelegate: errorDelegate - ) - return .init(http2) - } - - /// Make a factory for 'fake' transport. - /// - Parameter fakeResponse: The fake response stream. - /// - Returns: A factory for making and configuring fake transport. - @usableFromInline - internal static func fake( - _ fakeResponse: _FakeResponseStream? - ) -> ClientTransportFactory - where - Request: SwiftProtobuf.Message, - Response: SwiftProtobuf.Message - { - let factory = FakeClientTransportFactory( - fakeResponse, - requestSerializer: ProtobufSerializer(), - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - responseDeserializer: ProtobufDeserializer() - ) - return .init(factory) - } - - /// Make a factory for 'fake' transport. - /// - Parameter fakeResponse: The fake response stream. - /// - Returns: A factory for making and configuring fake transport. - @usableFromInline - internal static func fake( - _ fakeResponse: _FakeResponseStream? - ) -> ClientTransportFactory where Request: GRPCPayload, Response: GRPCPayload { - let factory = FakeClientTransportFactory( - fakeResponse, - requestSerializer: GRPCPayloadSerializer(), - requestDeserializer: GRPCPayloadDeserializer(), - responseSerializer: GRPCPayloadSerializer(), - responseDeserializer: GRPCPayloadDeserializer() - ) - return .init(factory) - } - - /// Makes a configured `ClientTransport`. - /// - Parameters: - /// - path: The path of the RPC, e.g. "/echo.Echo/Get". - /// - type: The type of RPC, e.g. `.unary`. - /// - options: Options for the RPC. - /// - interceptors: Interceptors to use for the RPC. - /// - onError: A callback invoked when an error is received. - /// - onResponsePart: A closure called for each response part received. - /// - Returns: A configured transport. - internal func makeConfiguredTransport( - to path: String, - for type: GRPCCallType, - withOptions options: CallOptions, - onEventLoop eventLoop: EventLoop, - interceptedBy interceptors: [ClientInterceptor], - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) -> ClientTransport { - switch self.factory { - case let .http2(factory): - let transport = factory.makeTransport( - to: path, - for: type, - withOptions: options, - onEventLoop: eventLoop, - interceptedBy: interceptors, - onStart: onStart, - onError: onError, - onResponsePart: onResponsePart - ) - factory.configure(transport) - return transport - case let .fake(factory): - let transport = factory.makeTransport( - to: path, - for: type, - withOptions: options, - onEventLoop: eventLoop, - interceptedBy: interceptors, - onError: onError, - onResponsePart - ) - factory.configure(transport) - return transport - } - } -} - -@usableFromInline -internal struct HTTP2ClientTransportFactory { - /// The multiplexer providing an HTTP/2 stream for the call. - private var streamChannel: EventLoopFuture - - /// The ":authority" pseudo-header. - private var authority: String - - /// The ":scheme" pseudo-header. - private var scheme: String - - /// An error delegate. - private var errorDelegate: ClientErrorDelegate? - - /// The request serializer. - private let serializer: AnySerializer - - /// The response deserializer. - private let deserializer: AnyDeserializer - - /// Maximum allowed length of a received message. - private let maximumReceiveMessageLength: Int - - @usableFromInline - internal init( - streamChannel: EventLoopFuture, - scheme: String, - authority: String, - serializer: Serializer, - deserializer: Deserializer, - maximumReceiveMessageLength: Int, - errorDelegate: ClientErrorDelegate? - ) where Serializer.Input == Request, Deserializer.Output == Response { - self.streamChannel = streamChannel - self.scheme = scheme - self.authority = authority - self.serializer = AnySerializer(wrapping: serializer) - self.deserializer = AnyDeserializer(wrapping: deserializer) - self.maximumReceiveMessageLength = maximumReceiveMessageLength - self.errorDelegate = errorDelegate - } - - fileprivate func makeTransport( - to path: String, - for type: GRPCCallType, - withOptions options: CallOptions, - onEventLoop eventLoop: EventLoop, - interceptedBy interceptors: [ClientInterceptor], - onStart: @escaping () -> Void, - onError: @escaping (Error) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) -> ClientTransport { - return ClientTransport( - details: self.makeCallDetails(type: type, path: path, options: options), - eventLoop: eventLoop, - interceptors: interceptors, - serializer: self.serializer, - deserializer: self.deserializer, - errorDelegate: self.errorDelegate, - onStart: onStart, - onError: onError, - onResponsePart: onResponsePart - ) - } - - fileprivate func configure(_ transport: ClientTransport) { - transport.configure { _ in - self.streamChannel.flatMapThrowing { channel in - // This initializer will always occur on the appropriate event loop, sync operations are - // fine here. - let syncOperations = channel.pipeline.syncOperations - - do { - let clientHandler = GRPCClientChannelHandler( - callType: transport.callDetails.type, - maximumReceiveMessageLength: self.maximumReceiveMessageLength, - logger: transport.logger - ) - try syncOperations.addHandler(clientHandler) - try syncOperations.addHandler(transport) - } - } - } - } - - private func makeCallDetails( - type: GRPCCallType, - path: String, - options: CallOptions - ) -> CallDetails { - return .init( - type: type, - path: path, - authority: self.authority, - scheme: self.scheme, - options: options - ) - } -} - -@usableFromInline -internal struct FakeClientTransportFactory { - /// The fake response stream for the call. This can be `nil` if the user did not correctly - /// configure their client. The result will be a transport which immediately fails. - private var fakeResponseStream: _FakeResponseStream? - - /// The request serializer. - private let requestSerializer: AnySerializer - - /// The response deserializer. - private let responseDeserializer: AnyDeserializer - - /// A codec for deserializing requests and serializing responses. - private let codec: ChannelHandler - - @usableFromInline - internal init< - RequestSerializer: MessageSerializer, - RequestDeserializer: MessageDeserializer, - ResponseSerializer: MessageSerializer, - ResponseDeserializer: MessageDeserializer - >( - _ fakeResponseStream: _FakeResponseStream?, - requestSerializer: RequestSerializer, - requestDeserializer: RequestDeserializer, - responseSerializer: ResponseSerializer, - responseDeserializer: ResponseDeserializer - ) - where - RequestSerializer.Input == Request, - RequestDeserializer.Output == Request, - ResponseSerializer.Input == Response, - ResponseDeserializer.Output == Response - { - self.fakeResponseStream = fakeResponseStream - self.requestSerializer = AnySerializer(wrapping: requestSerializer) - self.responseDeserializer = AnyDeserializer(wrapping: responseDeserializer) - self.codec = GRPCClientReverseCodecHandler( - serializer: responseSerializer, - deserializer: requestDeserializer - ) - } - - fileprivate func makeTransport( - to path: String, - for type: GRPCCallType, - withOptions options: CallOptions, - onEventLoop eventLoop: EventLoop, - interceptedBy interceptors: [ClientInterceptor], - onError: @escaping (Error) -> Void, - _ onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) -> ClientTransport { - return ClientTransport( - details: CallDetails( - type: type, - path: path, - authority: "localhost", - scheme: "http", - options: options - ), - eventLoop: eventLoop, - interceptors: interceptors, - serializer: self.requestSerializer, - deserializer: self.responseDeserializer, - errorDelegate: nil, - onStart: {}, - onError: onError, - onResponsePart: onResponsePart - ) - } - - fileprivate func configure(_ transport: ClientTransport) { - transport.configure { handler in - if let fakeResponse = self.fakeResponseStream { - return fakeResponse.channel.pipeline.addHandlers(self.codec, handler).always { result in - switch result { - case .success: - fakeResponse.activate() - case .failure: - () - } - } - } else { - return transport.callEventLoop - .makeFailedFuture(GRPCStatus(code: .unavailable, message: nil)) - } - } - } -} diff --git a/Sources/GRPC/Interceptor/MessageParts.swift b/Sources/GRPC/Interceptor/MessageParts.swift deleted file mode 100644 index 1e2495884..000000000 --- a/Sources/GRPC/Interceptor/MessageParts.swift +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2020, 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 NIOHPACK - -public enum GRPCClientRequestPart { - /// User provided metadata sent at the start of the request stream. - case metadata(HPACKHeaders) - - /// A message to send to the server. - case message(Request, MessageMetadata) - - /// The end the request stream. - case end -} - -public enum GRPCClientResponsePart { - /// The metadata returned by the server at the start of the RPC. - case metadata(HPACKHeaders) - - /// A response message from the server. - case message(Response) - - /// The end of response stream sent by the server. - case end(GRPCStatus, HPACKHeaders) -} - -public enum GRPCServerRequestPart { - /// Metadata received from the client at the start of the RPC. - case metadata(HPACKHeaders) - - /// A request message sent by the client. - case message(Request) - - /// The end the request stream. - case end -} - -public enum GRPCServerResponsePart { - /// The metadata to send to the client at the start of the response stream. - case metadata(HPACKHeaders) - - /// A response message sent by the server. - case message(Response, MessageMetadata) - - /// The end of response stream sent by the server. - case end(GRPCStatus, HPACKHeaders) -} - -/// Metadata associated with a request or response message. -public struct MessageMetadata: Equatable, Sendable { - /// Whether the message should be compressed. If compression has not been enabled on the RPC - /// then this setting is ignored. - public var compress: Bool - - /// Whether the underlying transported should be 'flushed' after writing this message. If a batch - /// of messages is to be sent then flushing only after the last message may improve - /// performance. - public var flush: Bool - - public init(compress: Bool, flush: Bool) { - self.compress = compress - self.flush = flush - } -} - -extension GRPCClientResponsePart { - @inlinable - internal var isEnd: Bool { - switch self { - case .end: - return true - case .metadata, .message: - return false - } - } -} - -extension GRPCServerResponsePart { - @inlinable - internal var isEnd: Bool { - switch self { - case .end: - return true - case .metadata, .message: - return false - } - } -} - -extension GRPCClientRequestPart: Sendable where Request: Sendable {} -extension GRPCClientResponsePart: Sendable where Response: Sendable {} -extension GRPCServerRequestPart: Sendable where Request: Sendable {} -extension GRPCServerResponsePart: Sendable where Response: Sendable {} diff --git a/Sources/GRPC/Interceptor/ServerInterceptorContext.swift b/Sources/GRPC/Interceptor/ServerInterceptorContext.swift deleted file mode 100644 index 5543b417f..000000000 --- a/Sources/GRPC/Interceptor/ServerInterceptorContext.swift +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore - -public struct ServerInterceptorContext { - /// The interceptor this context is for. - @usableFromInline - internal let interceptor: ServerInterceptor - - /// The pipeline this context is associated with. - @usableFromInline - internal let _pipeline: ServerInterceptorPipeline - - /// The index of this context's interceptor within the pipeline. - @usableFromInline - internal let _index: Int - - /// The `EventLoop` this interceptor pipeline is being executed on. - public var eventLoop: EventLoop { - return self._pipeline.eventLoop - } - - /// A logger. - public var logger: Logger { - return self._pipeline.logger - } - - /// The type of the RPC, e.g. "unary". - public var type: GRPCCallType { - return self._pipeline.type - } - - /// The path of the RPC in the format "/Service/Method", e.g. "/echo.Echo/Get". - public var path: String { - return self._pipeline.path - } - - /// The address of the remote peer. - public var remoteAddress: SocketAddress? { - return self._pipeline.remoteAddress - } - - /// A future which completes when the call closes. This may be used to register callbacks which - /// free up resources used by the interceptor. - public var closeFuture: EventLoopFuture { - return self._pipeline.closeFuture - } - - /// A 'UserInfo' dictionary. - /// - /// - Important: While `UserInfo` has value-semantics, this property retrieves from, and sets a - /// reference wrapped `UserInfo`. The contexts passed to the service provider share the same - /// reference. As such this may be used as a mechanism to pass information between interceptors - /// and service providers. - /// - Important: `userInfo` *must* be accessed from the context's `eventLoop` in order to ensure - /// thread-safety. - public var userInfo: UserInfo { - get { - return self._pipeline.userInfoRef.value - } - nonmutating set { - self._pipeline.userInfoRef.value = newValue - } - } - - /// Construct a `ServerInterceptorContext` for the interceptor at the given index within the - /// interceptor pipeline. - @inlinable - internal init( - for interceptor: ServerInterceptor, - atIndex index: Int, - in pipeline: ServerInterceptorPipeline - ) { - self.interceptor = interceptor - self._pipeline = pipeline - self._index = index - } - - /// Forwards the request part to the next inbound interceptor in the pipeline, if there is one. - /// - /// - Parameter part: The request part to forward. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - public func receive(_ part: GRPCServerRequestPart) { - self._pipeline.invokeReceive(part, fromContextAtIndex: self._index) - } - - /// Forwards the response part to the next outbound interceptor in the pipeline, if there is one. - /// - /// - Parameters: - /// - part: The response part to forward. - /// - promise: The promise the complete when the part has been written. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - public func send( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - self._pipeline.invokeSend(part, promise: promise, fromContextAtIndex: self._index) - } -} diff --git a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift b/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift deleted file mode 100644 index 15c6442c9..000000000 --- a/Sources/GRPC/Interceptor/ServerInterceptorPipeline.swift +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore - -@usableFromInline -internal final class ServerInterceptorPipeline { - /// The `EventLoop` this RPC is being executed on. - @usableFromInline - internal let eventLoop: EventLoop - - /// The path of the RPC in the format "/Service/Method", e.g. "/echo.Echo/Get". - @usableFromInline - internal let path: String - - /// The type of the RPC, e.g. "unary". - @usableFromInline - internal let type: GRPCCallType - - /// The remote peer's address. - @usableFromInline - internal let remoteAddress: SocketAddress? - - /// A logger. - @usableFromInline - internal let logger: Logger - - /// A reference to a 'UserInfo'. - @usableFromInline - internal let userInfoRef: Ref - - /// A future which completes when the call closes. This may be used to register callbacks which - /// free up resources used by the interceptor. - @usableFromInline - internal let closeFuture: EventLoopFuture - - /// Called when a response part has traversed the interceptor pipeline. - @usableFromInline - internal var _onResponsePart: - Optional< - ( - GRPCServerResponsePart, - EventLoopPromise? - ) -> Void - > - - /// Called when a request part has traversed the interceptor pipeline. - @usableFromInline - internal var _onRequestPart: Optional<(GRPCServerRequestPart) -> Void> - - /// The index before the first user interceptor context index. (always -1). - @usableFromInline - internal let _headIndex: Int - - /// The index after the last user interceptor context index (i.e. 'userContext.endIndex'). - @usableFromInline - internal let _tailIndex: Int - - /// Contexts for user provided interceptors. - @usableFromInline - internal var _userContexts: [ServerInterceptorContext] - - /// Whether the interceptor pipeline is still open. It becomes closed after an 'end' response - /// part has traversed the pipeline. - @usableFromInline - internal var _isOpen = true - - /// The index of the next context on the inbound side of the context at the given index. - @inlinable - internal func _nextInboundIndex(after index: Int) -> Int { - // Unchecked arithmetic is okay here: our greatest inbound index is '_tailIndex' but we will - // never ask for the inbound index after the tail. - assert(self._indexIsValid(index)) - return index &+ 1 - } - - /// The index of the next context on the outbound side of the context at the given index. - @inlinable - internal func _nextOutboundIndex(after index: Int) -> Int { - // Unchecked arithmetic is okay here: our lowest outbound index is '_headIndex' but we will - // never ask for the outbound index after the head. - assert(self._indexIsValid(index)) - return index &- 1 - } - - /// Returns true of the index is in the range `_headIndex ... _tailIndex`. - @inlinable - internal func _indexIsValid(_ index: Int) -> Bool { - return self._headIndex <= index && index <= self._tailIndex - } - - @inlinable - internal init( - logger: Logger, - eventLoop: EventLoop, - path: String, - callType: GRPCCallType, - remoteAddress: SocketAddress?, - userInfoRef: Ref, - closeFuture: EventLoopFuture, - interceptors: [ServerInterceptor], - onRequestPart: @escaping (GRPCServerRequestPart) -> Void, - onResponsePart: @escaping (GRPCServerResponsePart, EventLoopPromise?) -> Void - ) { - self.logger = logger - self.eventLoop = eventLoop - self.path = path - self.type = callType - self.remoteAddress = remoteAddress - self.userInfoRef = userInfoRef - self.closeFuture = closeFuture - - self._onResponsePart = onResponsePart - self._onRequestPart = onRequestPart - - // Head comes before user interceptors. - self._headIndex = -1 - // Tail comes just after. - self._tailIndex = interceptors.endIndex - - // Make some contexts. - self._userContexts = [] - self._userContexts.reserveCapacity(interceptors.count) - - for index in 0 ..< interceptors.count { - let context = ServerInterceptorContext(for: interceptors[index], atIndex: index, in: self) - self._userContexts.append(context) - } - } - - /// Emit a request part message into the interceptor pipeline. - /// - /// - Parameter part: The part to emit into the pipeline. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func receive(_ part: GRPCServerRequestPart) { - self.invokeReceive(part, fromContextAtIndex: self._headIndex) - } - - /// Invoke receive on the appropriate context when called from the context at the given index. - @inlinable - internal func invokeReceive( - _ part: GRPCServerRequestPart, - fromContextAtIndex index: Int - ) { - self._invokeReceive(part, onContextAtIndex: self._nextInboundIndex(after: index)) - } - - /// Invoke receive on the context at the given index, if doing so is safe. - @inlinable - internal func _invokeReceive( - _ part: GRPCServerRequestPart, - onContextAtIndex index: Int - ) { - self.eventLoop.assertInEventLoop() - assert(self._indexIsValid(index)) - guard self._isOpen else { - return - } - - // We've checked the index. - self._invokeReceive(part, onContextAtUncheckedIndex: index) - } - - /// Invoke receive on the context at the given index, assuming that the index is valid and the - /// pipeline is still open. - @inlinable - internal func _invokeReceive( - _ part: GRPCServerRequestPart, - onContextAtUncheckedIndex index: Int - ) { - switch index { - case self._headIndex: - // The next inbound index must exist, either for the tail or a user interceptor. - self._invokeReceive( - part, - onContextAtUncheckedIndex: self._nextInboundIndex(after: self._headIndex) - ) - - case self._tailIndex: - self._onRequestPart?(part) - - default: - self._userContexts[index].invokeReceive(part) - } - } - - /// Write a response message into the interceptor pipeline. - /// - /// - Parameters: - /// - part: The response part to sent. - /// - promise: A promise to complete when the response part has been successfully written. - /// - Important: This *must* to be called from the `eventLoop`. - @inlinable - internal func send(_ part: GRPCServerResponsePart, promise: EventLoopPromise?) { - self.invokeSend(part, promise: promise, fromContextAtIndex: self._tailIndex) - } - - /// Invoke send on the appropriate context when called from the context at the given index. - @inlinable - internal func invokeSend( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise?, - fromContextAtIndex index: Int - ) { - self._invokeSend( - part, - promise: promise, - onContextAtIndex: self._nextOutboundIndex(after: index) - ) - } - - /// Invoke send on the context at the given index, if doing so is safe. Fails the `promise` if it - /// is not safe to do so. - @inlinable - internal func _invokeSend( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise?, - onContextAtIndex index: Int - ) { - self.eventLoop.assertInEventLoop() - assert(self._indexIsValid(index)) - guard self._isOpen else { - promise?.fail(GRPCError.AlreadyComplete()) - return - } - - self._invokeSend(uncheckedIndex: index, part, promise: promise) - } - - /// Invoke send on the context at the given index, assuming that the index is valid and the - /// pipeline is still open. - @inlinable - internal func _invokeSend( - uncheckedIndex index: Int, - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - switch index { - case self._headIndex: - let onResponsePart = self._onResponsePart - if part.isEnd { - self.close() - } - onResponsePart?(part, promise) - - case self._tailIndex: - // The next outbound index must exist: it will be the head or a user interceptor. - self._invokeSend( - uncheckedIndex: self._nextOutboundIndex(after: self._tailIndex), - part, - promise: promise - ) - - default: - self._userContexts[index].invokeSend(part, promise: promise) - } - } - - @inlinable - internal func close() { - // We're no longer open. - self._isOpen = false - // Each context hold a ref to the pipeline; break the retain cycle. - self._userContexts.removeAll() - // Drop the refs to the server handler. - self._onRequestPart = nil - self._onResponsePart = nil - } -} - -extension ServerInterceptorContext { - @inlinable - internal func invokeReceive(_ part: GRPCServerRequestPart) { - self.interceptor.receive(part, context: self) - } - - @inlinable - internal func invokeSend( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise? - ) { - self.interceptor.send(part, promise: promise, context: self) - } -} diff --git a/Sources/GRPC/Interceptor/ServerInterceptors.swift b/Sources/GRPC/Interceptor/ServerInterceptors.swift deleted file mode 100644 index 835f34ded..000000000 --- a/Sources/GRPC/Interceptor/ServerInterceptors.swift +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -/// A base class for server interceptors. -/// -/// Interceptors allow request and response and response parts to be observed, mutated or dropped -/// as necessary. The default behaviour for this base class is to forward any events to the next -/// interceptor. -/// -/// Interceptors may observe two different types of event: -/// - receiving request parts with ``receive(_:context:)``, -/// - sending response parts with ``send(_:promise:context:)``. -/// -/// These events flow through a pipeline of interceptors for each RPC. Request parts will enter -/// the head of the interceptor pipeline once the request router has determined that there is a -/// service provider which is able to handle the request stream. Response parts from the service -/// provider enter the tail of the interceptor pipeline and will be sent to the client after -/// traversing the pipeline through to the head. -/// -/// Each of the interceptor functions is provided with a `context` which exposes analogous functions -/// (``receive(_:context:)`` and ``send(_:promise:context:)``) which may be called to forward events to the next -/// interceptor. -/// -/// ### Thread Safety -/// -/// Functions on `context` are not thread safe and **must** be called on the `EventLoop` found on -/// the `context`. Since each interceptor is invoked on the same `EventLoop` this does not usually -/// require any extra attention. However, if work is done on a `DispatchQueue` or _other_ -/// `EventLoop` then implementers should ensure that they use `context` from the correct -/// `EventLoop`. -open class ServerInterceptor: @unchecked Sendable { - public init() {} - - /// Called when the interceptor has received a request part to handle. - /// - Parameters: - /// - part: The request part which has been received from the client. - /// - context: An interceptor context which may be used to forward the response part. - open func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - context.receive(part) - } - - /// Called when the interceptor has received a response part to handle. - /// - Parameters: - /// - part: The request part which should be sent to the client. - /// - promise: A promise which should be completed when the response part has been written. - /// - context: An interceptor context which may be used to forward the request part. - open func send( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise?, - context: ServerInterceptorContext - ) { - context.send(part, promise: promise) - } -} diff --git a/Sources/GRPC/InterceptorContextList.swift b/Sources/GRPC/InterceptorContextList.swift deleted file mode 100644 index 013ddf615..000000000 --- a/Sources/GRPC/InterceptorContextList.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -/// A non-empty list which is guaranteed to have a first and last element. -/// -/// This is required since we want to directly store the first and last elements: in some cases -/// `Array.first` and `Array.last` will allocate: unfortunately this currently happens to be the -/// case for the interceptor pipelines. Storing the `first` and `last` directly allows us to avoid -/// this. See also: https://bugs.swift.org/browse/SR-11262. -@usableFromInline -internal struct InterceptorContextList { - /// The first element, stored at `middle.startIndex - 1`. - @usableFromInline - internal var first: Element - - /// The last element, stored at the `middle.endIndex`. - @usableFromInline - internal var last: Element - - /// The other elements. - @usableFromInline - internal var _middle: [Element] - - /// The index of `first` - @usableFromInline - internal let firstIndex: Int - - /// The index of `last`. - @usableFromInline - internal let lastIndex: Int - - @usableFromInline - internal subscript(checked index: Int) -> Element? { - switch index { - case self.firstIndex: - return self.first - case self.lastIndex: - return self.last - default: - return self._middle[checked: index] - } - } - - @inlinable - internal init(first: Element, middle: [Element], last: Element) { - self.first = first - self._middle = middle - self.last = last - self.firstIndex = middle.startIndex - 1 - self.lastIndex = middle.endIndex - } -} diff --git a/Sources/GRPC/LengthPrefixedMessageReader.swift b/Sources/GRPC/LengthPrefixedMessageReader.swift deleted file mode 100644 index e1700963f..000000000 --- a/Sources/GRPC/LengthPrefixedMessageReader.swift +++ /dev/null @@ -1,225 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore -import NIOHTTP1 - -/// This class reads and decodes length-prefixed gRPC messages. -/// -/// Messages are expected to be in the following format: -/// - compression flag: 0/1 as a 1-byte unsigned integer, -/// - message length: length of the message as a 4-byte unsigned integer, -/// - message: `message_length` bytes. -/// -/// Messages may span multiple `ByteBuffer`s, and `ByteBuffer`s may contain multiple -/// length-prefixed messages. -/// -/// - SeeAlso: -/// [gRPC Protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) -internal struct LengthPrefixedMessageReader { - let compression: CompressionAlgorithm? - private let decompressor: Zlib.Inflate? - - init() { - self.compression = nil - self.decompressor = nil - } - - init(compression: CompressionAlgorithm, decompressionLimit: DecompressionLimit) { - self.compression = compression - - switch compression.algorithm { - case .identity: - self.decompressor = nil - case .deflate: - self.decompressor = Zlib.Inflate(format: .deflate, limit: decompressionLimit) - case .gzip: - self.decompressor = Zlib.Inflate(format: .gzip, limit: decompressionLimit) - } - } - - /// The result of trying to parse a message with the bytes we currently have. - /// - /// - needMoreData: More data is required to continue reading a message. - /// - continue: Continue reading a message. - /// - message: A message was read. - internal enum ParseResult { - case needMoreData - case `continue` - case message(ByteBuffer) - } - - /// The parsing state; what we expect to be reading next. - internal enum ParseState { - case expectingCompressedFlag - case expectingMessageLength(compressed: Bool) - case expectingMessage(Int, compressed: Bool) - } - - private var buffer: ByteBuffer! - private var state: ParseState = .expectingCompressedFlag - - /// Returns the number of unprocessed bytes. - internal var unprocessedBytes: Int { - return self.buffer.map { $0.readableBytes } ?? 0 - } - - /// Returns the number of bytes that have been consumed and not discarded. - internal var _consumedNonDiscardedBytes: Int { - return self.buffer.map { $0.readerIndex } ?? 0 - } - - /// Whether the reader is mid-way through reading a message. - internal var isReading: Bool { - switch self.state { - case .expectingCompressedFlag: - return false - case .expectingMessageLength, .expectingMessage: - return true - } - } - - /// Appends data to the buffer from which messages will be read. - internal mutating func append(buffer: inout ByteBuffer) { - guard buffer.readableBytes > 0 else { - return - } - - if self.buffer == nil { - self.buffer = buffer.slice() - // mark the bytes as "read" - buffer.moveReaderIndex(forwardBy: buffer.readableBytes) - } else { - switch self.state { - case let .expectingMessage(length, _): - // We need to reserve enough space for the message or the incoming buffer, whichever - // is larger. - let remainingMessageBytes = Int(length) - self.buffer.readableBytes - self.buffer - .reserveCapacity(minimumWritableBytes: max(remainingMessageBytes, buffer.readableBytes)) - - case .expectingCompressedFlag, - .expectingMessageLength: - // Just append the buffer; these parts are too small to make a meaningful difference. - () - } - - self.buffer.writeBuffer(&buffer) - } - } - - /// Reads bytes from the buffer until it is exhausted or a message has been read. - /// - /// - Returns: A buffer containing a message if one has been read, or `nil` if not enough - /// bytes have been consumed to return a message. - /// - Throws: Throws an error if the compression algorithm is not supported. - internal mutating func nextMessage(maxLength: Int) throws -> ByteBuffer? { - switch try self.processNextState(maxLength: maxLength) { - case .needMoreData: - self.nilBufferIfPossible() - return nil - - case .continue: - return try self.nextMessage(maxLength: maxLength) - - case let .message(message): - self.nilBufferIfPossible() - return message - } - } - - /// `nil`s out `buffer` if it exists and has no readable bytes. - /// - /// This allows the next call to `append` to avoid writing the contents of the appended buffer. - private mutating func nilBufferIfPossible() { - let readableBytes = self.buffer?.readableBytes ?? 0 - let readerIndex = self.buffer?.readerIndex ?? 0 - let capacity = self.buffer?.capacity ?? 0 - - if readableBytes == 0 { - self.buffer = nil - } else if readerIndex > 1024, readerIndex > (capacity / 2) { - // A rough-heuristic: if there is a kilobyte of read data, and there is more data that - // has been read than there is space in the rest of the buffer, we'll try to discard some - // read bytes here. We're trying to avoid doing this if there is loads of writable bytes that - // we'll have to shift. - self.buffer?.discardReadBytes() - } - } - - private mutating func processNextState(maxLength: Int) throws -> ParseResult { - guard self.buffer != nil else { - return .needMoreData - } - - switch self.state { - case .expectingCompressedFlag: - guard let compressionFlag: UInt8 = self.buffer.readInteger() else { - return .needMoreData - } - - let isCompressionEnabled = compressionFlag != 0 - // Compression is enabled, but not expected. - if isCompressionEnabled, self.compression == nil { - throw GRPCError.CompressionUnsupported().captureContext() - } - self.state = .expectingMessageLength(compressed: isCompressionEnabled) - - case let .expectingMessageLength(compressed): - guard let messageLength = self.buffer.readInteger(as: UInt32.self).map(Int.init) else { - return .needMoreData - } - - if messageLength > maxLength { - throw GRPCError.PayloadLengthLimitExceeded( - actualLength: messageLength, - limit: maxLength - ).captureContext() - } - - self.state = .expectingMessage(messageLength, compressed: compressed) - - case let .expectingMessage(length, compressed): - guard var message = self.buffer.readSlice(length: length) else { - return .needMoreData - } - - let result: ParseResult - - // TODO: If compression is enabled and we store the buffer slices then we can feed the slices - // into the decompressor. This should eliminate one buffer allocation (i.e. the buffer into - // which we currently accumulate the slices before decompressing it into a new buffer). - - // If compression is set but the algorithm is 'identity' then we will not get a decompressor - // here. - if compressed, let decompressor = self.decompressor { - var decompressed = ByteBufferAllocator().buffer(capacity: 0) - try decompressor.inflate(&message, into: &decompressed) - // Compression contexts should be reset between messages. - decompressor.reset() - result = .message(decompressed) - } else { - result = .message(message) - } - - self.state = .expectingCompressedFlag - return result - } - - return .continue - } -} diff --git a/Sources/GRPC/Logger.swift b/Sources/GRPC/Logger.swift deleted file mode 100644 index ce5991aa5..000000000 --- a/Sources/GRPC/Logger.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 Logging -import NIOCore - -/// Keys for `Logger` metadata. -enum MetadataKey { - static let requestID = "grpc_request_id" - static let connectionID = "grpc_connection_id" - - static let eventLoop = "event_loop" - - static let h2StreamID = "h2_stream_id" - static let h2ActiveStreams = "h2_active_streams" - static let h2EndStream = "h2_end_stream" - static let h2Payload = "h2_payload" - static let h2Headers = "h2_headers" - static let h2DataBytes = "h2_data_bytes" - static let h2GoAwayError = "h2_goaway_error" - static let h2GoAwayLastStreamID = "h2_goaway_last_stream_id" - static let h2PingAck = "h2_ping_ack" - - static let delayMs = "delay_ms" - static let intervalMs = "interval_ms" - - static let error = "error" -} - -extension Logger { - internal mutating func addIPAddressMetadata(local: SocketAddress?, remote: SocketAddress?) { - if let local = local?.ipAddress { - self[metadataKey: "grpc.conn.addr_local"] = "\(local)" - } - if let remote = remote?.ipAddress { - self[metadataKey: "grpc.conn.addr_remote"] = "\(remote)" - } - } -} diff --git a/Sources/GRPC/LoggingServerErrorDelegate.swift b/Sources/GRPC/LoggingServerErrorDelegate.swift deleted file mode 100644 index 8c55178ac..000000000 --- a/Sources/GRPC/LoggingServerErrorDelegate.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 Logging - -public class LoggingServerErrorDelegate: ServerErrorDelegate { - private let logger: Logger - - public init(logger: Logger) { - self.logger = logger - } - - public func observeLibraryError(_ error: Error) { - self.logger.error("library error", metadata: [MetadataKey.error: "\(error)"]) - } - - public func observeRequestHandlerError(_ error: Error) { - self.logger.error("request handler error", metadata: [MetadataKey.error: "\(error)"]) - } -} diff --git a/Sources/GRPC/MessageEncodingHeaderValidator.swift b/Sources/GRPC/MessageEncodingHeaderValidator.swift deleted file mode 100644 index 4c26ba59a..000000000 --- a/Sources/GRPC/MessageEncodingHeaderValidator.swift +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -struct MessageEncodingHeaderValidator { - var encoding: ServerMessageEncoding - - enum ValidationResult { - /// The requested compression is supported. - case supported( - algorithm: CompressionAlgorithm, - decompressionLimit: DecompressionLimit, - acceptEncoding: [String] - ) - - /// The `requestEncoding` is not supported; `acceptEncoding` contains all algorithms we do - /// support. - case unsupported(requestEncoding: String, acceptEncoding: [String]) - - /// No compression was requested. - case noCompression - } - - /// Validates the value of the 'grpc-encoding' header against compression algorithms supported and - /// advertised by this peer. - /// - /// - Parameter requestEncoding: The value of the 'grpc-encoding' header. - func validate(requestEncoding: String?) -> ValidationResult { - switch (self.encoding, requestEncoding) { - // Compression is enabled and the client sent a message encoding header. Do we support it? - case let (.enabled(configuration), .some(header)): - guard let algorithm = CompressionAlgorithm(rawValue: header) else { - return .unsupported( - requestEncoding: header, - acceptEncoding: configuration.enabledAlgorithms.map { $0.name } - ) - } - - if configuration.enabledAlgorithms.contains(algorithm) { - return .supported( - algorithm: algorithm, - decompressionLimit: configuration.decompressionLimit, - acceptEncoding: [] - ) - } else { - // From: https://github.com/grpc/grpc/blob/master/doc/compression.md - // - // Note that a peer MAY choose to not disclose all the encodings it supports. However, if - // it receives a message compressed in an undisclosed but supported encoding, it MUST - // include said encoding in the response's grpc-accept-encoding header. - return .supported( - algorithm: algorithm, - decompressionLimit: configuration.decompressionLimit, - acceptEncoding: configuration.enabledAlgorithms.map { $0.name } + CollectionOfOne(header) - ) - } - - // Compression is disabled and the client sent a message encoding header. We don't support this - // unless the header is "identity", which is no compression. Note this is different to the - // supported but not advertised case since we have explicitly not enabled compression. - case let (.disabled, .some(header)): - guard let algorithm = CompressionAlgorithm(rawValue: header) else { - return .unsupported( - requestEncoding: header, - acceptEncoding: [] - ) - } - - if algorithm == .identity { - return .noCompression - } else { - return .unsupported(requestEncoding: header, acceptEncoding: []) - } - - // The client didn't send a message encoding header. - case (_, .none): - return .noCompression - } - } -} diff --git a/Sources/GRPC/PlatformSupport.swift b/Sources/GRPC/PlatformSupport.swift deleted file mode 100644 index b7f4ff5a4..000000000 --- a/Sources/GRPC/PlatformSupport.swift +++ /dev/null @@ -1,430 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOPosix -import NIOTransportServices - -/// How a network implementation should be chosen. -public struct NetworkPreference: Hashable { - private enum Wrapped: Hashable { - case best - case userDefined(NetworkImplementation) - } - - private var wrapped: Wrapped - private init(_ wrapped: Wrapped) { - self.wrapped = wrapped - } - - /// 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. - public static let best = NetworkPreference(.best) - - /// Use the given implementation. Doing so may require additional availability checks depending - /// on the implementation. - public static func userDefined(_ implementation: NetworkImplementation) -> NetworkPreference { - return NetworkPreference(.userDefined(implementation)) - } -} - -/// The network implementation to use: POSIX sockets or Network.framework. This also determines -/// which variant of NIO to use; NIO or NIOTransportServices, respectively. -public struct NetworkImplementation: Hashable { - fileprivate enum Wrapped: Hashable { - case networkFramework - case posix - } - - fileprivate var wrapped: Wrapped - private init(_ wrapped: Wrapped) { - self.wrapped = wrapped - } - - #if canImport(Network) - /// Network.framework (NIOTransportServices). - @available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) - public static let networkFramework = NetworkImplementation(.networkFramework) - #endif - - /// POSIX (NIO). - public static let posix = NetworkImplementation(.posix) - - internal static func matchingEventLoopGroup(_ group: EventLoopGroup) -> NetworkImplementation { - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - if PlatformSupport.isTransportServicesEventLoopGroup(group) { - return .networkFramework - } - } - #endif - return .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(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)`, or - /// - `#available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)`. - public var implementation: NetworkImplementation { - switch self.wrapped { - case .best: - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - return .networkFramework - } else { - // Older platforms must use the POSIX loop. - return .posix - } - #else - return .posix - #endif - - case let .userDefined(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 - func connect(host: String, port: Int) -> EventLoopFuture - func connect(unixDomainSocketPath: String) -> EventLoopFuture - func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture - func connect(to vsockAddress: VsockAddress) -> EventLoopFuture - - func connectTimeout(_ timeout: TimeAmount) -> Self - func channelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption - - @preconcurrency - func channelInitializer(_ handler: @escaping @Sendable (Channel) -> EventLoopFuture) -> Self -} - -extension ClientBootstrapProtocol { - public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture { - preconditionFailure("withConnectedSocket(_:) is not implemented") - } -} - -extension ClientBootstrap: ClientBootstrapProtocol {} - -#if canImport(Network) -@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) -extension NIOTSConnectionBootstrap: ClientBootstrapProtocol { - public func withConnectedSocket(_ socket: NIOBSDSocket.Handle) -> EventLoopFuture { - preconditionFailure("NIOTSConnectionBootstrap does not support withConnectedSocket(_:)") - } - - public func connect(to vsockAddress: VsockAddress) -> EventLoopFuture { - preconditionFailure("NIOTSConnectionBootstrap does not support connect(to vsockAddress:)") - } -} -#endif - -/// This protocol is intended as a layer of abstraction over `ServerBootstrap` and -/// `NIOTSListenerBootstrap`. -public protocol ServerBootstrapProtocol { - func bind(to: SocketAddress) -> EventLoopFuture - func bind(host: String, port: Int) -> EventLoopFuture - func bind(unixDomainSocketPath: String) -> EventLoopFuture - func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture - func bind(to vsockAddress: VsockAddress) -> EventLoopFuture - - @preconcurrency - func serverChannelInitializer( - _ handler: @escaping @Sendable (Channel) -> EventLoopFuture - ) -> Self - - func serverChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption - - @preconcurrency - func childChannelInitializer( - _ handler: @escaping @Sendable (Channel) -> EventLoopFuture - ) - -> Self - - func childChannelOption(_ option: T, value: T.Value) -> Self where T: ChannelOption -} - -extension ServerBootstrapProtocol { - public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture { - preconditionFailure("withBoundSocket(_:) is not implemented") - } -} - -extension ServerBootstrap: ServerBootstrapProtocol {} - -#if canImport(Network) -@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) -extension NIOTSListenerBootstrap: ServerBootstrapProtocol { - public func withBoundSocket(_ connectedSocket: NIOBSDSocket.Handle) -> EventLoopFuture { - preconditionFailure("NIOTSListenerBootstrap does not support withBoundSocket(_:)") - } - - public func bind(to vsockAddress: VsockAddress) -> EventLoopFuture { - preconditionFailure("NIOTSListenerBootstrap does not support bind(to vsockAddress:)") - } -} -#endif - -// MARK: - Bootstrap / EventLoopGroup helpers - -public enum PlatformSupport { - /// 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 preference; defaulting to `.best`. - public static func makeEventLoopGroup( - loopCount: Int, - networkPreference: NetworkPreference = .best, - logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }) - ) -> EventLoopGroup { - logger.debug("making EventLoopGroup for \(networkPreference) network preference") - switch networkPreference.implementation.wrapped { - case .networkFramework: - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { - logger.critical("Network.framework can be imported but is not supported on this platform") - // This is gated by the availability of `.networkFramework` so should never happen. - fatalError(".networkFramework is being used on an unsupported platform") - } - logger.debug("created NIOTSEventLoopGroup for \(networkPreference) preference") - return NIOTSEventLoopGroup(loopCount: loopCount) - #else - fatalError(".networkFramework is being used on an unsupported platform") - #endif - case .posix: - logger.debug("created MultiThreadedEventLoopGroup for \(networkPreference) preference") - 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, - logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }) - ) -> ClientBootstrapProtocol { - logger.debug("making client bootstrap with event loop group of type \(type(of: group))") - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - if isTransportServicesEventLoopGroup(group) { - logger.debug( - "Network.framework is available and the EventLoopGroup is compatible with NIOTS, creating a NIOTSConnectionBootstrap" - ) - return NIOTSConnectionBootstrap(group: group) - } else { - logger.debug( - "Network.framework is available but the EventLoopGroup is not compatible with NIOTS, falling back to ClientBootstrap" - ) - } - } - #endif - logger.debug("creating a ClientBootstrap") - return ClientBootstrap(group: group) - } - - internal static func isTransportServicesEventLoopGroup(_ group: EventLoopGroup) -> Bool { - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - return group is NIOTSEventLoopGroup || group is QoSEventLoop - } - #endif - return false - } - - internal static func makeClientBootstrap( - group: EventLoopGroup, - tlsConfiguration: GRPCTLSConfiguration?, - logger: Logger - ) -> ClientBootstrapProtocol { - let bootstrap = self.makeClientBootstrap(group: group, logger: logger) - - guard let tlsConfigruation = tlsConfiguration else { - return bootstrap - } - - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), - let transportServicesBootstrap = bootstrap as? NIOTSConnectionBootstrap - { - return transportServicesBootstrap.tlsOptions(from: tlsConfigruation) - } - #endif - - return bootstrap - } - - /// 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, - logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }) - ) -> ServerBootstrapProtocol { - logger.debug("making server bootstrap with event loop group of type \(type(of: group))") - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - if let tsGroup = group as? NIOTSEventLoopGroup { - logger - .debug( - "Network.framework is available and the group is correctly typed, creating a NIOTSListenerBootstrap" - ) - return NIOTSListenerBootstrap(group: tsGroup) - } else if let qosEventLoop = group as? QoSEventLoop { - logger - .debug( - "Network.framework is available and the group is correctly typed, creating a NIOTSListenerBootstrap" - ) - return NIOTSListenerBootstrap(group: qosEventLoop) - } - logger - .debug( - "Network.framework is available but the group is not typed for NIOTS, falling back to ServerBootstrap" - ) - } - #endif - logger.debug("creating a ServerBootstrap") - return ServerBootstrap(group: group) - } - - /// Determines whether we may need to work around an issue in Network.framework with zero-length writes. - /// - /// See https://github.com/apple/swift-nio-transport-services/pull/72 for more. - static func requiresZeroLengthWriteWorkaround(group: EventLoopGroup, hasTLS: Bool) -> Bool { - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - if group is NIOTSEventLoopGroup || group is QoSEventLoop { - // We need the zero-length write workaround on NIOTS when not using TLS. - return !hasTLS - } else { - return false - } - } else { - return false - } - #else - return false - #endif - } -} - -extension PlatformSupport { - /// Make an `EventLoopGroup` which is compatible with the given TLS configuration/ - /// - /// - Parameters: - /// - configuration: The configuration to make a compatible `EventLoopGroup` for. - /// - loopCount: The number of loops the `EventLoopGroup` should have. - /// - Returns: An `EventLoopGroup` compatible with the given `configuration`. - public static func makeEventLoopGroup( - compatibleWith configuration: GRPCTLSConfiguration, - loopCount: Int - ) -> EventLoopGroup { - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - if configuration.isNetworkFrameworkTLSBackend { - return NIOTSEventLoopGroup(loopCount: loopCount) - } - } - #endif - return MultiThreadedEventLoopGroup(numberOfThreads: loopCount) - } -} - -extension GRPCTLSConfiguration { - /// Provides a `GRPCTLSConfiguration` suitable for the given `EventLoopGroup`. - public static func makeClientDefault( - compatibleWith eventLoopGroup: EventLoopGroup - ) -> GRPCTLSConfiguration { - let networkImplementation: NetworkImplementation = .matchingEventLoopGroup(eventLoopGroup) - return GRPCTLSConfiguration.makeClientDefault(for: .userDefined(networkImplementation)) - } - - /// Provides a `GRPCTLSConfiguration` suitable for the given network preference. - public static func makeClientDefault( - for networkPreference: NetworkPreference - ) -> GRPCTLSConfiguration { - switch networkPreference.implementation.wrapped { - case .networkFramework: - #if canImport(Network) - guard #available(macOS 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 .makeClientConfigurationBackedByNetworkFramework() - #else - fatalError(".networkFramework is being used on an unsupported platform") - #endif - - case .posix: - #if canImport(NIOSSL) - return .makeClientConfigurationBackedByNIOSSL() - #else - fatalError("Default client TLS configuration for '.posix' requires NIOSSL") - #endif - } - } -} - -extension EventLoopGroup { - internal func isCompatible(with tlsConfiguration: GRPCTLSConfiguration) -> Bool { - let isTransportServicesGroup = PlatformSupport.isTransportServicesEventLoopGroup(self) - let isNetworkFrameworkTLSBackend = tlsConfiguration.isNetworkFrameworkTLSBackend - // If the group is from NIOTransportServices then we can use either the NIOSSL or the - // Network.framework TLS backend. - // - // If it isn't then we must not use the Network.Framework TLS backend. - return isTransportServicesGroup || !isNetworkFrameworkTLSBackend - } - - internal func preconditionCompatible( - with tlsConfiguration: GRPCTLSConfiguration, - file: StaticString = #fileID, - line: UInt = #line - ) { - precondition( - self.isCompatible(with: tlsConfiguration), - "Unsupported 'EventLoopGroup' and 'GRPCLSConfiguration' pairing (Network.framework backed TLS configurations MUST use an EventLoopGroup from NIOTransportServices)", - file: file, - line: line - ) - } -} diff --git a/Sources/GRPC/ReadWriteStates.swift b/Sources/GRPC/ReadWriteStates.swift deleted file mode 100644 index 9aede44cf..000000000 --- a/Sources/GRPC/ReadWriteStates.swift +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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 NIOCore -import SwiftProtobuf - -/// Number of messages expected on a stream. -enum MessageArity { - case one - case many -} - -/// Encapsulates the state required to create a new write state. -struct PendingWriteState { - /// The number of messages we expect to write to the stream. - var arity: MessageArity - - /// The 'content-type' being written. - var contentType: ContentType - - func makeWriteState( - messageEncoding: ClientMessageEncoding, - allocator: ByteBufferAllocator - ) -> WriteState { - let compression: CompressionAlgorithm? - switch messageEncoding { - case let .enabled(configuration): - compression = configuration.outbound - case .disabled: - compression = nil - } - - let writer = CoalescingLengthPrefixedMessageWriter( - compression: compression, - allocator: allocator - ) - return .init(arity: self.arity, contentType: self.contentType, writer: writer) - } -} - -/// The write state of a stream. -struct WriteState { - private var arity: MessageArity - private var contentType: ContentType - private var writer: CoalescingLengthPrefixedMessageWriter - private var canWrite: Bool - - init( - arity: MessageArity, - contentType: ContentType, - writer: CoalescingLengthPrefixedMessageWriter - ) { - self.arity = arity - self.contentType = contentType - self.writer = writer - self.canWrite = true - } - - /// Writes a message into a buffer using the `writer`. - /// - /// - Parameter message: The `Message` to write. - mutating func write( - _ message: ByteBuffer, - compressed: Bool, - promise: EventLoopPromise? - ) -> Result { - guard self.canWrite else { - return .failure(.cardinalityViolation) - } - - self.writer.append(buffer: message, compress: compressed, promise: promise) - - switch self.arity { - case .one: - self.canWrite = false - case .many: - () - } - - return .success(()) - } - - mutating func next() -> (Result, EventLoopPromise?)? { - if let next = self.writer.next() { - return (next.0.mapError { _ in .serializationFailed }, next.1) - } else { - return nil - } - } -} - -enum MessageWriteError: Error { - /// Too many messages were written. - case cardinalityViolation - - /// Message serialization failed. - case serializationFailed - - /// An invalid state was encountered. This is a serious implementation error. - case invalidState -} - -/// Encapsulates the state required to create a new read state. -struct PendingReadState { - /// The number of messages we expect to read from the stream. - var arity: MessageArity - - /// The message encoding configuration, and whether it's enabled or not. - var messageEncoding: ClientMessageEncoding - - func makeReadState(compression: CompressionAlgorithm? = nil) -> ReadState { - let reader: LengthPrefixedMessageReader - switch (self.messageEncoding, compression) { - case let (.enabled(configuration), .some(compression)): - reader = LengthPrefixedMessageReader( - compression: compression, - decompressionLimit: configuration.decompressionLimit - ) - - case (.enabled, .none), - (.disabled, _): - reader = LengthPrefixedMessageReader() - } - return .reading(self.arity, reader) - } -} - -/// The read state of a stream. -enum ReadState { - /// Reading may be attempted using the given reader. - case reading(MessageArity, LengthPrefixedMessageReader) - - /// Reading may not be attempted: either a read previously failed or it is not valid for any - /// more messages to be read. - case notReading - - /// Consume the given `buffer` then attempt to read length-prefixed serialized messages. - /// - /// For an expected message count of `.one`, this function will produce **at most** 1 message. If - /// a message has been produced then subsequent calls will result in an error. - /// - /// - Parameter buffer: The buffer to read from. - mutating func readMessages( - _ buffer: inout ByteBuffer, - maxLength: Int - ) -> Result<[ByteBuffer], MessageReadError> { - switch self { - case .notReading: - return .failure(.cardinalityViolation) - - case .reading(let readArity, var reader): - self = .notReading // Avoid CoWs - reader.append(buffer: &buffer) - var messages: [ByteBuffer] = [] - - do { - while let serializedBytes = try reader.nextMessage(maxLength: maxLength) { - messages.append(serializedBytes) - } - } catch { - self = .notReading - if let grpcError = error as? GRPCError.WithContext { - if let compressionLimit = grpcError.error as? GRPCError.DecompressionLimitExceeded { - return .failure(.decompressionLimitExceeded(compressionLimit.compressedSize)) - } else if let lengthLimit = grpcError.error as? GRPCError.PayloadLengthLimitExceeded { - return .failure(.lengthExceedsLimit(lengthLimit)) - } - } - - return .failure(.deserializationFailed) - } - - // We need to validate the number of messages we decoded. Zero is fine because the payload may - // be split across frames. - switch (readArity, messages.count) { - // Always allowed: - case (.one, 0), - (.many, 0...): - self = .reading(readArity, reader) - return .success(messages) - - // Also allowed, assuming we have no leftover bytes: - case (.one, 1): - // We can't read more than one message on a unary stream. - self = .notReading - // We shouldn't have any bytes leftover after reading a single message and we also should not - // have partially read a message. - if reader.unprocessedBytes != 0 || reader.isReading { - return .failure(.leftOverBytes) - } else { - return .success(messages) - } - - // Anything else must be invalid. - default: - self = .notReading - return .failure(.cardinalityViolation) - } - } - } -} - -enum MessageReadError: Error, Equatable { - /// Too many messages were read. - case cardinalityViolation - - /// Enough messages were read but bytes there are left-over bytes. - case leftOverBytes - - /// Message deserialization failed. - case deserializationFailed - - /// The limit for decompression was exceeded. - case decompressionLimitExceeded(Int) - - /// The length of the message exceeded the permitted maximum length. - case lengthExceedsLimit(GRPCError.PayloadLengthLimitExceeded) - - /// An invalid state was encountered. This is a serious implementation error. - case invalidState -} diff --git a/Sources/GRPC/Ref.swift b/Sources/GRPC/Ref.swift deleted file mode 100644 index ab7fde89e..000000000 --- a/Sources/GRPC/Ref.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -@usableFromInline -internal final class Ref { - @usableFromInline - internal var value: Value - - @inlinable - internal init(_ value: Value) { - self.value = value - } -} diff --git a/Sources/GRPC/Serialization.swift b/Sources/GRPC/Serialization.swift deleted file mode 100644 index fff0e13ec..000000000 --- a/Sources/GRPC/Serialization.swift +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOFoundationCompat -import SwiftProtobuf - -import struct Foundation.Data - -public protocol MessageSerializer { - associatedtype Input - - /// Serializes `input` into a `ByteBuffer` allocated using the provided `allocator`. - /// - /// - Parameters: - /// - input: The element to serialize. - /// - allocator: A `ByteBufferAllocator`. - @inlinable - func serialize(_ input: Input, allocator: ByteBufferAllocator) throws -> ByteBuffer -} - -public protocol MessageDeserializer { - associatedtype Output - - /// Deserializes `byteBuffer` to produce a single `Output`. - /// - /// - Parameter byteBuffer: The `ByteBuffer` to deserialize. - @inlinable - func deserialize(byteBuffer: ByteBuffer) throws -> Output -} - -// MARK: Protobuf - -public struct ProtobufSerializer: MessageSerializer { - @inlinable - public init() {} - - @inlinable - public func serialize(_ message: Message, allocator: ByteBufferAllocator) throws -> ByteBuffer { - // Serialize the message. - let serialized = try message.serializedData() - - // Allocate enough space and an extra 5 leading bytes. This a minor optimisation win: the length - // prefixed message writer can re-use the leading 5 bytes without needing to allocate a new - // buffer and copy over the serialized message. - var buffer = allocator.buffer(capacity: serialized.count + 5) - buffer.writeRepeatingByte(0, count: 5) - buffer.moveReaderIndex(forwardBy: 5) - - // Now write the serialized message. - buffer.writeContiguousBytes(serialized) - - return buffer - } -} - -public struct ProtobufDeserializer: MessageDeserializer { - @inlinable - public init() {} - - @inlinable - public func deserialize(byteBuffer: ByteBuffer) throws -> Message { - var buffer = byteBuffer - // '!' is okay; we can always read 'readableBytes'. - let data = buffer.readData(length: buffer.readableBytes)! - return try Message(serializedBytes: data) - } -} - -// MARK: GRPCPayload - -public struct GRPCPayloadSerializer: MessageSerializer { - @inlinable - public init() {} - - @inlinable - public func serialize(_ message: Message, allocator: ByteBufferAllocator) throws -> ByteBuffer { - // Reserve 5 leading bytes. This a minor optimisation win: the length prefixed message writer - // can re-use the leading 5 bytes without needing to allocate a new buffer and copy over the - // serialized message. - var buffer = allocator.buffer(repeating: 0, count: 5) - - let readerIndex = buffer.readerIndex - let writerIndex = buffer.writerIndex - - // Serialize the payload into the buffer. - try message.serialize(into: &buffer) - - // Ensure 'serialize(into:)' didn't do anything strange. - assert(buffer.readerIndex == readerIndex, "serialize(into:) must not move the readerIndex") - assert( - buffer.writerIndex >= writerIndex, - "serialize(into:) must not move the writerIndex backwards" - ) - assert( - buffer.getBytes(at: readerIndex, length: 5) == Array(repeating: 0, count: 5), - "serialize(into:) must not write over existing written bytes" - ) - - // 'read' the first 5 bytes so that the buffer's readable bytes are only the bytes of the - // serialized message. - buffer.moveReaderIndex(forwardBy: 5) - - return buffer - } -} - -public struct GRPCPayloadDeserializer: MessageDeserializer { - @inlinable - public init() {} - - @inlinable - public func deserialize(byteBuffer: ByteBuffer) throws -> Message { - var buffer = byteBuffer - return try Message(serializedByteBuffer: &buffer) - } -} - -// MARK: - Any Serializer/Deserializer - -internal struct AnySerializer: MessageSerializer { - private let _serialize: (Input, ByteBufferAllocator) throws -> ByteBuffer - - init(wrapping other: Serializer) where Serializer.Input == Input { - self._serialize = other.serialize(_:allocator:) - } - - internal func serialize(_ input: Input, allocator: ByteBufferAllocator) throws -> ByteBuffer { - return try self._serialize(input, allocator) - } -} - -internal struct AnyDeserializer: MessageDeserializer { - private let _deserialize: (ByteBuffer) throws -> Output - - init( - wrapping other: Deserializer - ) where Deserializer.Output == Output { - self._deserialize = other.deserialize(byteBuffer:) - } - - internal func deserialize(byteBuffer: ByteBuffer) throws -> Output { - return try self._deserialize(byteBuffer) - } -} diff --git a/Sources/GRPC/Server+NIOSSL.swift b/Sources/GRPC/Server+NIOSSL.swift deleted file mode 100644 index 31a1d9e6d..000000000 --- a/Sources/GRPC/Server+NIOSSL.swift +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOSSL - -extension Server.Configuration { - /// TLS configuration for a ``Server``. - /// - /// Note that this configuration is a subset of `NIOSSL.TLSConfiguration` where certain options - /// are removed from the users control to ensure the configuration complies with the gRPC - /// specification. - @available(*, deprecated, renamed: "GRPCTLSConfiguration") - public struct TLS { - public private(set) var configuration: TLSConfiguration - - /// Whether ALPN is required. Disabling this option may be useful in cases where ALPN is not - /// supported. - public var requireALPN: Bool = true - - /// The certificates to offer during negotiation. If not present, no certificates will be - /// offered. - public var certificateChain: [NIOSSLCertificateSource] { - get { - return self.configuration.certificateChain - } - set { - self.configuration.certificateChain = newValue - } - } - - /// The private key associated with the leaf certificate. - public var privateKey: NIOSSLPrivateKeySource? { - get { - return self.configuration.privateKey - } - set { - self.configuration.privateKey = newValue - } - } - - /// The trust roots to use to validate certificates. This only needs to be provided if you - /// intend to validate certificates. - public var trustRoots: NIOSSLTrustRoots? { - get { - return self.configuration.trustRoots - } - set { - self.configuration.trustRoots = newValue - } - } - - /// Whether to verify remote certificates. - public var certificateVerification: CertificateVerification { - get { - return self.configuration.certificateVerification - } - set { - self.configuration.certificateVerification = newValue - } - } - - /// TLS Configuration with suitable defaults for servers. - /// - /// This is a wrapper around `NIOSSL.TLSConfiguration` to restrict input to values which comply - /// with the gRPC protocol. - /// - /// - Parameter certificateChain: The certificate to offer during negotiation. - /// - Parameter privateKey: The private key associated with the leaf certificate. - /// - Parameter trustRoots: The trust roots to validate certificates, this defaults to using a - /// root provided by the platform. - /// - Parameter certificateVerification: Whether to verify the remote certificate. Defaults to - /// `.none`. - /// - Parameter requireALPN: Whether ALPN is required or not. - public init( - certificateChain: [NIOSSLCertificateSource], - privateKey: NIOSSLPrivateKeySource, - trustRoots: NIOSSLTrustRoots = .default, - certificateVerification: CertificateVerification = .none, - requireALPN: Bool = true - ) { - var configuration = TLSConfiguration.makeServerConfiguration( - certificateChain: certificateChain, - privateKey: privateKey - ) - configuration.minimumTLSVersion = .tlsv12 - configuration.certificateVerification = certificateVerification - configuration.trustRoots = trustRoots - configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.server - - self.configuration = configuration - self.requireALPN = requireALPN - } - - /// Creates a TLS Configuration using the given `NIOSSL.TLSConfiguration`. - /// - Note: If no ALPN tokens are set in `configuration.applicationProtocols` then the tokens - /// "grpc-exp", "h2" and "http/1.1" will be used. - /// - Parameters: - /// - configuration: The `NIOSSL.TLSConfiguration` to base this configuration on. - /// - requireALPN: Whether ALPN is required. - public init(configuration: TLSConfiguration, requireALPN: Bool = true) { - self.configuration = configuration - self.requireALPN = requireALPN - - // Set the ALPN tokens if none were set. - if self.configuration.applicationProtocols.isEmpty { - self.configuration.applicationProtocols = GRPCApplicationProtocolIdentifier.server - } - } - } -} - -#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/Server.swift b/Sources/GRPC/Server.swift deleted file mode 100644 index 9117fd7e2..000000000 --- a/Sources/GRPC/Server.swift +++ /dev/null @@ -1,602 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore -import NIOExtras -import NIOHTTP1 -import NIOHTTP2 -import NIOPosix -import NIOTransportServices - -#if canImport(NIOSSL) -import NIOSSL -#endif -#if canImport(Network) -import Network -#endif - -/// Wrapper object to manage the lifecycle of a gRPC server. -/// -/// The pipeline is configured in three stages detailed below. Note: handlers marked with -/// a '*' are responsible for handling errors. -/// -/// 1. Initial stage, prior to pipeline configuration. -/// -/// ┌─────────────────────────────────┐ -/// │ GRPCServerPipelineConfigurator* │ -/// └────▲───────────────────────┬────┘ -/// ByteBuffer│ │ByteBuffer -/// ┌─┴───────────────────────▼─┐ -/// │ NIOSSLHandler │ -/// └─▲───────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// │ ▼ -/// -/// The `NIOSSLHandler` is optional and depends on how the framework user has configured -/// their server. The `GRPCServerPipelineConfigurator` detects which HTTP version is being used -/// (via ALPN if TLS is used or by parsing the first bytes on the connection otherwise) and -/// configures the pipeline accordingly. -/// -/// 2. HTTP version detected. "HTTP Handlers" depends on the HTTP version determined by -/// `GRPCServerPipelineConfigurator`. In the case of HTTP/2: -/// -/// ┌─────────────────────────────────┐ -/// │ HTTP2StreamMultiplexer │ -/// └─▲─────────────────────────────┬─┘ -/// HTTP2Frame│ │HTTP2Frame -/// ┌─┴─────────────────────────────▼─┐ -/// │ HTTP2Handler │ -/// └─▲─────────────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// ┌─┴─────────────────────────────▼─┐ -/// │ NIOSSLHandler │ -/// └─▲─────────────────────────────┬─┘ -/// ByteBuffer│ │ByteBuffer -/// │ ▼ -/// -/// The `HTTP2StreamMultiplexer` provides one `Channel` for each HTTP/2 stream (and thus each -/// RPC). -/// -/// 3. The frames for each stream channel are routed by the `HTTP2ToRawGRPCServerCodec` handler to -/// a handler containing the user-implemented logic provided by a `CallHandlerProvider`: -/// -/// ┌─────────────────────────────────┐ -/// │ BaseCallHandler* │ -/// └─▲─────────────────────────────┬─┘ -/// GRPCServerRequestPart│ │GRPCServerResponsePart -/// ┌─┴─────────────────────────────▼─┐ -/// │ HTTP2ToRawGRPCServerCodec │ -/// └─▲─────────────────────────────┬─┘ -/// HTTP2Frame.FramePayload│ │HTTP2Frame.FramePayload -/// │ ▼ -/// -/// - Note: This class is thread safe. It's marked as `@unchecked Sendable` because the non-Sendable -/// `errorDelegate` property is mutated, but it's done thread-safely, as it only happens inside the `EventLoop`. -public final class Server: @unchecked Sendable { - /// Makes and configures a `ServerBootstrap` using the provided configuration. - public class func makeBootstrap(configuration: Configuration) -> ServerBootstrapProtocol { - let bootstrap = PlatformSupport.makeServerBootstrap(group: configuration.eventLoopGroup) - - // Backlog is only available on `ServerBootstrap`. - if bootstrap is ServerBootstrap { - // Specify a backlog to avoid overloading the server. - _ = bootstrap.serverChannelOption(ChannelOptions.backlog, value: 256) - } - - #if canImport(NIOSSL) - // Making a `NIOSSLContext` is expensive, we should only do it once per TLS configuration so - // we'll do it now, before accepting connections. Unfortunately our API isn't throwing so we'll - // only surface any error when initializing a child channel. - // - // 'nil' means we're not using TLS, or we're using the Network.framework TLS backend. If we're - // using the Network.framework TLS backend we'll apply the settings just below. - let sslContext: Result? - - if let tlsConfiguration = configuration.tlsConfiguration { - do { - sslContext = try tlsConfiguration.makeNIOSSLContext().map { .success($0) } - } catch { - sslContext = .failure(error) - } - - } else { - // No TLS configuration, no SSL context. - sslContext = nil - } - #endif // canImport(NIOSSL) - - #if canImport(Network) - if let tlsConfiguration = configuration.tlsConfiguration { - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), - let transportServicesBootstrap = bootstrap as? NIOTSListenerBootstrap - { - _ = transportServicesBootstrap.tlsOptions(from: tlsConfiguration) - } - } - #endif // canImport(Network) - - return - bootstrap - // Enable `SO_REUSEADDR` to avoid "address already in use" error. - .serverChannelOption( - ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), - value: 1 - ) - // Set the handlers that are applied to the accepted Channels - .childChannelInitializer { channel in - var configuration = configuration - configuration.logger[metadataKey: MetadataKey.connectionID] = "\(UUID().uuidString)" - configuration.logger.addIPAddressMetadata( - local: channel.localAddress, - remote: channel.remoteAddress - ) - - do { - let sync = channel.pipeline.syncOperations - #if canImport(NIOSSL) - if let sslContext = try sslContext?.get() { - let sslHandler: NIOSSLServerHandler - if let verify = configuration.tlsConfiguration?.nioSSLCustomVerificationCallback { - sslHandler = NIOSSLServerHandler( - context: sslContext, - customVerificationCallback: verify - ) - } else { - sslHandler = NIOSSLServerHandler(context: sslContext) - } - - try sync.addHandler(sslHandler) - } - #endif // canImport(NIOSSL) - - // Configures the pipeline based on whether the connection uses TLS or not. - try sync.addHandler(GRPCServerPipelineConfigurator(configuration: configuration)) - - // Work around the zero length write issue, if needed. - let requiresZeroLengthWorkaround = PlatformSupport.requiresZeroLengthWriteWorkaround( - group: configuration.eventLoopGroup, - hasTLS: configuration.tlsConfiguration != nil - ) - if requiresZeroLengthWorkaround, - #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) - { - try sync.addHandler(NIOFilterEmptyWritesHandler()) - } - } catch { - return channel.eventLoop.makeFailedFuture(error) - } - - // Run the debug initializer, if there is one. - if let debugAcceptedChannelInitializer = configuration.debugChannelInitializer { - return debugAcceptedChannelInitializer(channel) - } else { - return channel.eventLoop.makeSucceededVoidFuture() - } - } - - // Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels - .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) - .childChannelOption( - ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), - value: 1 - ) - } - - /// Starts a server with the given configuration. See `Server.Configuration` for the options - /// available to configure the server. - public static func start(configuration: Configuration) -> EventLoopFuture { - let quiescingHelper = ServerQuiescingHelper(group: configuration.eventLoopGroup) - - return self.makeBootstrap(configuration: configuration) - .serverChannelInitializer { channel in - channel.pipeline.addHandler(quiescingHelper.makeServerChannelHandler(channel: channel)) - } - .bind(to: configuration.target) - .map { channel in - Server( - channel: channel, - quiescingHelper: quiescingHelper, - errorDelegate: configuration.errorDelegate - ) - } - } - - public let channel: Channel - private let quiescingHelper: ServerQuiescingHelper - private var errorDelegate: ServerErrorDelegate? - - private init( - channel: Channel, - quiescingHelper: ServerQuiescingHelper, - errorDelegate: ServerErrorDelegate? - ) { - self.channel = channel - self.quiescingHelper = quiescingHelper - - // Maintain a strong reference to ensure it lives as long as the server. - self.errorDelegate = errorDelegate - - // If we have an error delegate, add a server channel error handler as well. We don't need to wait for the handler to - // be added. - if let errorDelegate = errorDelegate { - _ = channel.pipeline.addHandler(ServerChannelErrorHandler(errorDelegate: errorDelegate)) - } - - // nil out errorDelegate to avoid retain cycles. - self.onClose.whenComplete { _ in - self.errorDelegate = nil - } - } - - /// Fired when the server shuts down. - public var onClose: EventLoopFuture { - return self.channel.closeFuture - } - - /// Initiates a graceful shutdown. Existing RPCs may run to completion, any new RPCs or - /// connections will be rejected. - public func initiateGracefulShutdown(promise: EventLoopPromise?) { - self.quiescingHelper.initiateShutdown(promise: promise) - } - - /// Initiates a graceful shutdown. Existing RPCs may run to completion, any new RPCs or - /// connections will be rejected. - public func initiateGracefulShutdown() -> EventLoopFuture { - let promise = self.channel.eventLoop.makePromise(of: Void.self) - self.initiateGracefulShutdown(promise: promise) - return promise.futureResult - } - - /// Shutdown the server immediately. Active RPCs and connections will be terminated. - public func close(promise: EventLoopPromise?) { - self.channel.close(mode: .all, promise: promise) - } - - /// Shutdown the server immediately. Active RPCs and connections will be terminated. - public func close() -> EventLoopFuture { - return self.channel.close(mode: .all) - } -} - -public typealias BindTarget = ConnectionTarget - -extension Server { - /// The configuration for a server. - public struct Configuration { - /// The target to bind to. - public var target: BindTarget - /// The event loop group to run the connection on. - public var eventLoopGroup: EventLoopGroup - - /// Providers the server should use to handle gRPC requests. - public var serviceProviders: [CallHandlerProvider] { - get { - return Array(self.serviceProvidersByName.values) - } - set { - self - .serviceProvidersByName = Dictionary( - uniqueKeysWithValues: - newValue - .map { ($0.serviceName, $0) } - ) - } - } - - /// An error delegate which is called when errors are caught. Provided delegates **must not - /// maintain a strong reference to this `Server`**. Doing so will cause a retain cycle. - public var errorDelegate: ServerErrorDelegate? - - #if canImport(NIOSSL) - /// TLS configuration for this connection. `nil` if TLS is not desired. - @available(*, deprecated, renamed: "tlsConfiguration") - public var tls: TLS? { - get { - return self.tlsConfiguration?.asDeprecatedServerConfiguration - } - set { - self.tlsConfiguration = newValue.map { GRPCTLSConfiguration(transforming: $0) } - } - } - #endif // canImport(NIOSSL) - - public var tlsConfiguration: GRPCTLSConfiguration? - - /// The connection keepalive configuration. - public var connectionKeepalive = ServerConnectionKeepalive() - - /// The amount of time to wait before closing connections. The idle timeout will start only - /// if there are no RPCs in progress and will be cancelled as soon as any RPCs start. - public var connectionIdleTimeout: TimeAmount = .nanoseconds(.max) - - /// The compression configuration for requests and responses. - /// - /// If compression is enabled for the server it may be disabled for responses on any RPC by - /// setting `compressionEnabled` to `false` on the context of the call. - /// - /// Compression may also be disabled at the message-level for streaming responses (i.e. server - /// streaming and bidirectional streaming RPCs) by passing setting `compression` to `.disabled` - /// in `sendResponse(_:compression)`. - /// - /// Defaults to ``ServerMessageEncoding/disabled``. - public var messageEncoding: ServerMessageEncoding = .disabled - - /// The maximum size in bytes of a message which may be received from a client. Defaults to 4MB. - public var maximumReceiveMessageLength: Int = 4 * 1024 * 1024 { - willSet { - precondition(newValue >= 0, "maximumReceiveMessageLength must be positive") - } - } - - /// The HTTP/2 flow control target window size. Defaults to 8MB. Values are clamped between - /// 1 and 2^31-1 inclusive. - public var httpTargetWindowSize = 8 * 1024 * 1024 { - didSet { - self.httpTargetWindowSize = self.httpTargetWindowSize.clamped(to: 1 ... Int(Int32.max)) - } - } - - /// The HTTP/2 max number of concurrent streams. Defaults to 100. Must be non-negative. - public var httpMaxConcurrentStreams: Int = 100 { - willSet { - precondition(newValue >= 0, "httpMaxConcurrentStreams must be non-negative") - } - } - - /// The HTTP/2 max frame size. Defaults to 16384. Value is clamped between 2^14 and 2^24-1 - /// octets inclusive (the minimum and maximum allowable values - HTTP/2 RFC 7540 4.2). - public var httpMaxFrameSize: Int = 16384 { - didSet { - self.httpMaxFrameSize = self.httpMaxFrameSize.clamped(to: 16384 ... 16_777_215) - } - } - - /// The root server logger. Accepted connections will branch from this logger and RPCs on - /// each connection will use a logger branched from the connections logger. This logger is made - /// available to service providers via `context`. Defaults to a no-op logger. - public var logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }) - - /// A channel initializer which will be run after gRPC has initialized each accepted channel. - /// This may be used to add additional handlers to the pipeline and is intended for debugging. - /// This is analogous to `NIO.ServerBootstrap.childChannelInitializer`. - /// - /// - Warning: The initializer closure may be invoked *multiple times*. More precisely: it will - /// be invoked at most once per accepted connection. - public var debugChannelInitializer: ((Channel) -> EventLoopFuture)? - - /// A calculated private cache of the service providers by name. - /// - /// This is how gRPC consumes the service providers internally. Caching this as stored data avoids - /// the need to recalculate this dictionary each time we receive an rpc. - internal var serviceProvidersByName: [Substring: CallHandlerProvider] - - /// CORS configuration for gRPC-Web support. - public var webCORS = Configuration.CORS() - - #if canImport(NIOSSL) - /// Create a `Configuration` with some pre-defined defaults. - /// - /// - Parameters: - /// - target: The target to bind to. - /// - eventLoopGroup: The event loop group to run the server on. - /// - serviceProviders: An array of `CallHandlerProvider`s which the server should use - /// to handle requests. - /// - errorDelegate: The error delegate, defaulting to a logging delegate. - /// - tls: TLS configuration, defaulting to `nil`. - /// - connectionKeepalive: The keepalive configuration to use. - /// - connectionIdleTimeout: The amount of time to wait before closing the connection, this is - /// indefinite by default. - /// - messageEncoding: Message compression configuration, defaulting to no compression. - /// - httpTargetWindowSize: The HTTP/2 flow control target window size. - /// - logger: A logger. Defaults to a no-op logger. - /// - debugChannelInitializer: A channel initializer which will be called for each connection - /// the server accepts after gRPC has initialized the channel. Defaults to `nil`. - @available(*, deprecated, renamed: "default(target:eventLoopGroup:serviceProviders:)") - public init( - target: BindTarget, - eventLoopGroup: EventLoopGroup, - serviceProviders: [CallHandlerProvider], - errorDelegate: ServerErrorDelegate? = nil, - tls: TLS? = nil, - connectionKeepalive: ServerConnectionKeepalive = ServerConnectionKeepalive(), - connectionIdleTimeout: TimeAmount = .nanoseconds(.max), - messageEncoding: ServerMessageEncoding = .disabled, - httpTargetWindowSize: Int = 8 * 1024 * 1024, - logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }), - debugChannelInitializer: ((Channel) -> EventLoopFuture)? = nil - ) { - self.target = target - self.eventLoopGroup = eventLoopGroup - self.serviceProvidersByName = Dictionary( - uniqueKeysWithValues: serviceProviders.map { ($0.serviceName, $0) } - ) - self.errorDelegate = errorDelegate - self.tlsConfiguration = tls.map { GRPCTLSConfiguration(transforming: $0) } - self.connectionKeepalive = connectionKeepalive - self.connectionIdleTimeout = connectionIdleTimeout - self.messageEncoding = messageEncoding - self.httpTargetWindowSize = httpTargetWindowSize - self.logger = logger - self.debugChannelInitializer = debugChannelInitializer - } - #endif // canImport(NIOSSL) - - private init( - eventLoopGroup: EventLoopGroup, - target: BindTarget, - serviceProviders: [CallHandlerProvider] - ) { - self.eventLoopGroup = eventLoopGroup - self.target = target - self.serviceProvidersByName = Dictionary( - uniqueKeysWithValues: serviceProviders.map { - ($0.serviceName, $0) - } - ) - } - - /// Make a new configuration using default values. - /// - /// - Parameters: - /// - target: The target to bind to. - /// - eventLoopGroup: The `EventLoopGroup` the server should run on. - /// - serviceProviders: An array of `CallHandlerProvider`s which the server should use - /// to handle requests. - /// - Returns: A configuration with default values set. - public static func `default`( - target: BindTarget, - eventLoopGroup: EventLoopGroup, - serviceProviders: [CallHandlerProvider] - ) -> Configuration { - return .init( - eventLoopGroup: eventLoopGroup, - target: target, - serviceProviders: serviceProviders - ) - } - } -} - -extension Server.Configuration { - public struct CORS: Hashable, Sendable { - /// Determines which 'origin' header field values are permitted in a CORS request. - public var allowedOrigins: AllowedOrigins - /// Sets the headers which are permitted in a response to a CORS request. - public var allowedHeaders: [String] - /// Enabling this value allows sets the "access-control-allow-credentials" header field - /// to "true" in respones to CORS requests. This must be enabled if the client intends to send - /// credentials. - public var allowCredentialedRequests: Bool - /// The maximum age in seconds which pre-flight CORS requests may be cached for. - public var preflightCacheExpiration: Int - - public init( - allowedOrigins: AllowedOrigins = .all, - allowedHeaders: [String] = ["content-type", "x-grpc-web", "x-user-agent"], - allowCredentialedRequests: Bool = false, - preflightCacheExpiration: Int = 86400 - ) { - self.allowedOrigins = allowedOrigins - self.allowedHeaders = allowedHeaders - self.allowCredentialedRequests = allowCredentialedRequests - self.preflightCacheExpiration = preflightCacheExpiration - } - } -} - -extension Server.Configuration.CORS { - public struct AllowedOrigins: Hashable, Sendable { - enum Wrapped: Hashable, Sendable { - case all - case originBased - case only([String]) - case custom(AnyCustomCORSAllowedOrigin) - } - - private(set) var wrapped: Wrapped - private init(_ wrapped: Wrapped) { - self.wrapped = wrapped - } - - /// Allow all origin values. - public static let all = Self(.all) - - /// Allow all origin values; similar to `all` but returns the value of the origin header field - /// in the 'access-control-allow-origin' response header (rather than "*"). - public static let originBased = Self(.originBased) - - /// Allow only the given origin values. - public static func only(_ allowed: [String]) -> Self { - return Self(.only(allowed)) - } - - /// Provide a custom CORS origin check. - /// - /// - Parameter checkOrigin: A closure which is called with the value of the 'origin' header - /// and returns the value to use in the 'access-control-allow-origin' response header, - /// or `nil` if the origin is not allowed. - public static func custom(_ custom: C) -> Self { - return Self(.custom(AnyCustomCORSAllowedOrigin(custom))) - } - } -} - -extension ServerBootstrapProtocol { - fileprivate func bind(to target: BindTarget) -> EventLoopFuture { - switch target.wrapped { - case let .hostAndPort(host, port): - return self.bind(host: host, port: port) - - case let .unixDomainSocket(path): - return self.bind(unixDomainSocketPath: path) - - case let .socketAddress(address): - return self.bind(to: address) - - case let .connectedSocket(socket): - return self.withBoundSocket(socket) - - case let .vsockAddress(address): - return self.bind(to: address) - } - } -} - -extension Comparable { - internal func clamped(to range: ClosedRange) -> Self { - return min(max(self, range.lowerBound), range.upperBound) - } -} - -public protocol GRPCCustomCORSAllowedOrigin: Sendable, Hashable { - /// Returns the value to use for the 'access-control-allow-origin' response header for the given - /// value of the 'origin' request header. - /// - /// - Parameter origin: The value of the 'origin' request header field. - /// - Returns: The value to use for the 'access-control-allow-origin' header field or `nil` if no - /// CORS related headers should be returned. - func check(origin: String) -> String? -} - -extension Server.Configuration.CORS.AllowedOrigins { - struct AnyCustomCORSAllowedOrigin: GRPCCustomCORSAllowedOrigin { - private var checkOrigin: @Sendable (String) -> String? - private let hashInto: @Sendable (inout Hasher) -> Void - private let isEqualTo: @Sendable (any GRPCCustomCORSAllowedOrigin) -> Bool - - init(_ wrap: W) { - self.checkOrigin = { wrap.check(origin: $0) } - self.hashInto = { wrap.hash(into: &$0) } - self.isEqualTo = { wrap == ($0 as? W) } - } - - func check(origin: String) -> String? { - return self.checkOrigin(origin) - } - - func hash(into hasher: inout Hasher) { - self.hashInto(&hasher) - } - - static func == ( - lhs: Server.Configuration.CORS.AllowedOrigins.AnyCustomCORSAllowedOrigin, - rhs: Server.Configuration.CORS.AllowedOrigins.AnyCustomCORSAllowedOrigin - ) -> Bool { - return lhs.isEqualTo(rhs) - } - } -} diff --git a/Sources/GRPC/ServerBuilder+NIOSSL.swift b/Sources/GRPC/ServerBuilder+NIOSSL.swift deleted file mode 100644 index 93dd36da4..000000000 --- a/Sources/GRPC/ServerBuilder+NIOSSL.swift +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOCore -import NIOSSL - -extension Server { - /// Returns a `Server` builder configured with TLS. - @available( - *, - deprecated, - message: - "Use one of 'usingTLSBackedByNIOSSL(on:certificateChain:privateKey:)', 'usingTLSBackedByNetworkFramework(on:with:)' or 'usingTLS(with:on:)'" - ) - public static func secure( - group: EventLoopGroup, - certificateChain: [NIOSSLCertificate], - privateKey: NIOSSLPrivateKey - ) -> Builder.Secure { - return Server.usingTLSBackedByNIOSSL( - on: group, - certificateChain: certificateChain, - privateKey: privateKey - ) - } - - /// Returns a `Server` builder configured with the 'NIOSSL' TLS backend. - /// - /// This builder may use either a `MultiThreadedEventLoopGroup` or a `NIOTSEventLoopGroup` (or an - /// `EventLoop` from either group). - public static func usingTLSBackedByNIOSSL( - on group: EventLoopGroup, - certificateChain: [NIOSSLCertificate], - privateKey: NIOSSLPrivateKey - ) -> Builder.Secure { - return Builder.Secure( - group: group, - tlsConfiguration: .makeServerConfigurationBackedByNIOSSL( - certificateChain: certificateChain.map { .certificate($0) }, - privateKey: .privateKey(privateKey) - ) - ) - } -} - -extension Server.Builder.Secure { - /// Sets the trust roots to use to validate certificates. This only needs to be provided if you - /// intend to validate certificates. Defaults to the system provided trust store (`.default`) if - /// not set. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLS(trustRoots: NIOSSLTrustRoots) -> Self { - self.tls.updateNIOTrustRoots(to: trustRoots) - return self - } - - /// Sets whether certificates should be verified. Defaults to `.none` if not set. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLS(certificateVerification: CertificateVerification) -> Self { - self.tls.updateNIOCertificateVerification(to: certificateVerification) - return self - } -} - -#endif // canImport(NIOSSL) diff --git a/Sources/GRPC/ServerBuilder.swift b/Sources/GRPC/ServerBuilder.swift deleted file mode 100644 index c783e0d24..000000000 --- a/Sources/GRPC/ServerBuilder.swift +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOPosix - -#if canImport(Network) -import Security -#endif - -extension Server { - public class Builder { - private var configuration: Server.Configuration - private var maybeTLS: GRPCTLSConfiguration? { return nil } - - fileprivate init(group: EventLoopGroup) { - self.configuration = .default( - // This is okay: the configuration is only consumed on a call to `bind` which sets the host - // and port. - target: .hostAndPort("", .max), - eventLoopGroup: group, - serviceProviders: [] - ) - } - - public class Secure: Builder { - internal var tls: GRPCTLSConfiguration - override var maybeTLS: GRPCTLSConfiguration? { - return self.tls - } - - internal init(group: EventLoopGroup, tlsConfiguration: GRPCTLSConfiguration) { - group.preconditionCompatible(with: tlsConfiguration) - self.tls = tlsConfiguration - super.init(group: group) - } - } - - public func bind(host: String, port: Int) -> EventLoopFuture { - // Finish setting up the configuration. - self.configuration.target = .hostAndPort(host, port) - self.configuration.tlsConfiguration = self.maybeTLS - return Server.start(configuration: self.configuration) - } - - public func bind(unixDomainSocketPath path: String) -> EventLoopFuture { - self.configuration.target = .unixDomainSocket(path) - self.configuration.tlsConfiguration = self.maybeTLS - return Server.start(configuration: self.configuration) - } - - public func bind(to socketAddress: SocketAddress) -> EventLoopFuture { - self.configuration.target = .socketAddress(socketAddress) - self.configuration.tlsConfiguration = self.maybeTLS - return Server.start(configuration: self.configuration) - } - - public func bind(vsockAddress: VsockAddress) -> EventLoopFuture { - self.configuration.target = .vsockAddress(vsockAddress) - self.configuration.tlsConfiguration = self.maybeTLS - return Server.start(configuration: self.configuration) - } - - public func bind(to target: BindTarget) -> EventLoopFuture { - self.configuration.target = target - self.configuration.tlsConfiguration = self.maybeTLS - return Server.start(configuration: self.configuration) - } - } -} - -extension Server.Builder { - /// Sets the server error delegate. - @discardableResult - public func withErrorDelegate(_ delegate: ServerErrorDelegate?) -> Self { - self.configuration.errorDelegate = delegate - return self - } -} - -extension Server.Builder { - /// Sets the service providers that this server should offer. Note that calling this multiple - /// times will override any previously set providers. - @discardableResult - public func withServiceProviders(_ providers: [CallHandlerProvider]) -> Self { - self.configuration.serviceProviders = providers - return self - } -} - -extension Server.Builder { - @discardableResult - public func withKeepalive(_ keepalive: ServerConnectionKeepalive) -> Self { - self.configuration.connectionKeepalive = keepalive - return self - } -} - -extension Server.Builder { - /// The amount of time to wait before closing connections. The idle timeout will start only - /// if there are no RPCs in progress and will be cancelled as soon as any RPCs start. Unless a - /// an idle timeout it set connections will not be idled by default. - @discardableResult - public func withConnectionIdleTimeout(_ timeout: TimeAmount) -> Self { - self.configuration.connectionIdleTimeout = timeout - return self - } -} - -extension Server.Builder { - /// Sets the message compression configuration. Compression is disabled if this is not configured - /// and any RPCs using compression will not be accepted. - @discardableResult - public func withMessageCompression(_ encoding: ServerMessageEncoding) -> Self { - self.configuration.messageEncoding = encoding - return self - } - - /// Sets the maximum message size in bytes the server may receive. - /// - /// - Precondition: `limit` must not be negative. - @discardableResult - public func withMaximumReceiveMessageLength(_ limit: Int) -> Self { - self.configuration.maximumReceiveMessageLength = limit - return self - } -} - -extension Server.Builder.Secure { - /// Sets whether the server's TLS handshake requires a protocol to be negotiated via ALPN. This - /// defaults to `true` if not otherwise set. - /// - /// If this option is set to `false` and no protocol is negotiated via ALPN then the server will - /// parse the initial bytes on the connection to determine whether HTTP/2 or HTTP/1.1 (gRPC-Web) - /// is being used and configure the connection appropriately. - /// - /// - Note: May only be used with the 'NIOSSL' TLS backend. - @discardableResult - public func withTLS(requiringALPN: Bool) -> Self { - self.tls.requireALPN = requiringALPN - return self - } -} - -extension Server.Builder { - /// Sets the HTTP/2 flow control target window size. Defaults to 8MB if not explicitly set. - /// Values are clamped between 1 and 2^31-1 inclusive. - @discardableResult - public func withHTTPTargetWindowSize(_ httpTargetWindowSize: Int) -> Self { - self.configuration.httpTargetWindowSize = httpTargetWindowSize - return self - } - - /// Sets the maximum allowed number of concurrent HTTP/2 streams a client may open for a given - /// connection. Defaults to 100. - @discardableResult - public func withHTTPMaxConcurrentStreams(_ httpMaxConcurrentStreams: Int) -> Self { - self.configuration.httpMaxConcurrentStreams = httpMaxConcurrentStreams - return self - } - - /// Sets the HTTP/2 max frame size. Defaults to 16384. Value are clamped between 2^14 and 2^24-1 - /// octets inclusive (the minimum and maximum permitted values per RFC 7540 § 4.2). - /// - /// Raising this value may lower CPU usage for large message at the cost of increasing head of - /// line blocking for small messages. - @discardableResult - public func withHTTPMaxFrameSize(_ httpMaxFrameSize: Int) -> Self { - self.configuration.httpMaxFrameSize = httpMaxFrameSize - return self - } -} - -extension Server.Builder { - /// Set the CORS configuration for gRPC Web. - @discardableResult - public func withCORSConfiguration(_ configuration: Server.Configuration.CORS) -> Self { - self.configuration.webCORS = configuration - return self - } -} - -extension Server.Builder { - /// Sets the root server logger. Accepted connections will branch from this logger and RPCs on - /// each connection will use a logger branched from the connections logger. This logger is made - /// available to service providers via `context`. Defaults to a no-op logger. - @discardableResult - public func withLogger(_ logger: Logger) -> Self { - self.configuration.logger = logger - return self - } -} - -extension Server.Builder { - /// A channel initializer which will be run after gRPC has initialized each accepted channel. - /// This may be used to add additional handlers to the pipeline and is intended for debugging. - /// This is analogous to `NIO.ServerBootstrap.childChannelInitializer`. - /// - /// - Warning: The initializer closure may be invoked *multiple times*. More precisely: it will - /// be invoked at most once per accepted connection. - @discardableResult - public func withDebugChannelInitializer( - _ debugChannelInitializer: @escaping (Channel) -> EventLoopFuture - ) -> Self { - self.configuration.debugChannelInitializer = debugChannelInitializer - return self - } -} - -extension Server { - /// Returns an insecure `Server` builder which is *not configured with TLS*. - public static func insecure(group: EventLoopGroup) -> Builder { - return Builder(group: group) - } - - #if canImport(Network) - /// Returns a `Server` builder configured with the 'Network.framework' TLS backend. - /// - /// This builder must use a `NIOTSEventLoopGroup`. - @available(macOS 10.14, iOS 12.0, watchOS 6.0, tvOS 12.0, *) - public static func usingTLSBackedByNetworkFramework( - on group: EventLoopGroup, - with identity: SecIdentity - ) -> Builder.Secure { - precondition( - PlatformSupport.isTransportServicesEventLoopGroup(group), - "'usingTLSBackedByNetworkFramework(on:with:)' requires 'eventLoopGroup' to be a 'NIOTransportServices.NIOTSEventLoopGroup' or 'NIOTransportServices.QoSEventLoop' (but was '\(type(of: group))'" - ) - return Builder.Secure( - group: group, - tlsConfiguration: .makeServerConfigurationBackedByNetworkFramework(identity: identity) - ) - } - #endif - - /// Returns a `Server` builder configured with the TLS backend appropriate for the - /// provided `configuration` and `EventLoopGroup`. - /// - /// - Important: The caller is responsible for ensuring the provided `configuration` may be used - /// the the `group`. - public static func usingTLS( - with configuration: GRPCTLSConfiguration, - on group: EventLoopGroup - ) -> Builder.Secure { - return Builder.Secure(group: group, tlsConfiguration: configuration) - } -} diff --git a/Sources/GRPC/ServerCallContexts/ServerCallContext.swift b/Sources/GRPC/ServerCallContexts/ServerCallContext.swift deleted file mode 100644 index 603442e1d..000000000 --- a/Sources/GRPC/ServerCallContexts/ServerCallContext.swift +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import SwiftProtobuf - -/// Protocol declaring a minimum set of properties exposed by *all* types of call contexts. -public protocol ServerCallContext: AnyObject { - /// The event loop this call is served on. - var eventLoop: EventLoop { get } - - /// Request headers for this request. - var headers: HPACKHeaders { get } - - /// A 'UserInfo' dictionary which is shared with the interceptor contexts for this RPC. - var userInfo: UserInfo { get set } - - /// The logger used for this call. - var logger: Logger { get } - - /// Whether compression should be enabled for responses, defaulting to `true`. Note that for - /// this value to take effect compression must have been enabled on the server and a compression - /// algorithm must have been negotiated with the client. - var compressionEnabled: Bool { get set } - - /// A future which completes when the call closes. This may be used to register callbacks which - /// free up resources used by the RPC. - var closeFuture: EventLoopFuture { get } -} - -extension ServerCallContext { - // Default implementation to avoid breaking API. - public var closeFuture: EventLoopFuture { - return self.eventLoop.makeFailedFuture(GRPCStatus.closeFutureNotImplemented) - } -} - -extension GRPCStatus { - internal static let closeFutureNotImplemented = GRPCStatus( - code: .unimplemented, - message: "This context type has not implemented support for a 'closeFuture'" - ) -} - -/// Base class providing data provided to the framework user for all server calls. -open class ServerCallContextBase: ServerCallContext { - /// The event loop this call is served on. - public let eventLoop: EventLoop - - /// Request headers for this request. - public let headers: HPACKHeaders - - /// The logger used for this call. - public let logger: Logger - - /// Whether compression should be enabled for responses, defaulting to `true`. Note that for - /// this value to take effect compression must have been enabled on the server and a compression - /// algorithm must have been negotiated with the client. - /// - /// - Important: This *must* be accessed from the context's `eventLoop` in order to ensure - /// thread-safety. - public var compressionEnabled: Bool { - get { - self.eventLoop.assertInEventLoop() - return self._compressionEnabled - } - set { - self.eventLoop.assertInEventLoop() - self._compressionEnabled = newValue - } - } - - private var _compressionEnabled: Bool = true - - /// A `UserInfo` dictionary which is shared with the interceptor contexts for this RPC. - /// - /// - Important: While ``UserInfo`` has value-semantics, this property retrieves from, and sets a - /// reference wrapped ``UserInfo``. The contexts passed to interceptors provide the same - /// reference. As such this may be used as a mechanism to pass information between interceptors - /// and service providers. - /// - Important: This *must* be accessed from the context's `eventLoop` in order to ensure - /// thread-safety. - public var userInfo: UserInfo { - get { - self.eventLoop.assertInEventLoop() - return self.userInfoRef.value - } - set { - self.eventLoop.assertInEventLoop() - self.userInfoRef.value = newValue - } - } - - /// A reference to an underlying ``UserInfo``. We share this with the interceptors. - @usableFromInline - internal let userInfoRef: Ref - - /// Metadata to return at the end of the RPC. If this is required it should be updated before - /// the `responsePromise` or `statusPromise` is fulfilled. - /// - /// - Important: This *must* be accessed from the context's `eventLoop` in order to ensure - /// thread-safety. - public var trailers: HPACKHeaders { - get { - self.eventLoop.assertInEventLoop() - return self._trailers - } - set { - self.eventLoop.assertInEventLoop() - self._trailers = newValue - } - } - - private var _trailers: HPACKHeaders = [:] - - /// A future which completes when the call closes. This may be used to register callbacks which - /// free up resources used by the RPC. - public let closeFuture: EventLoopFuture - - @available(*, deprecated, renamed: "init(eventLoop:headers:logger:userInfo:closeFuture:)") - public convenience init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfo: UserInfo = UserInfo() - ) { - self.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: .init(userInfo), - closeFuture: eventLoop.makeFailedFuture(GRPCStatus.closeFutureNotImplemented) - ) - } - - public convenience init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfo: UserInfo = UserInfo(), - closeFuture: EventLoopFuture - ) { - self.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: .init(userInfo), - closeFuture: closeFuture - ) - } - - @inlinable - internal init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfoRef: Ref, - closeFuture: EventLoopFuture - ) { - self.eventLoop = eventLoop - self.headers = headers - self.userInfoRef = userInfoRef - self.logger = logger - self.closeFuture = closeFuture - } -} diff --git a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift b/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift deleted file mode 100644 index 99a266e2e..000000000 --- a/Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import SwiftProtobuf - -/// An abstract base class for a context provided to handlers for RPCs which may return multiple -/// responses, i.e. server streaming and bidirectional streaming RPCs. -open class StreamingResponseCallContext: ServerCallContextBase { - /// A promise for the ``GRPCStatus``, the end of the response stream. This must be completed by - /// bidirectional streaming RPC handlers to end the RPC. - /// - /// Note that while this is also present for server streaming RPCs, it is not necessary to - /// complete this promise: instead, an `EventLoopFuture` must be returned from the - /// handler. - public let statusPromise: EventLoopPromise - - @available(*, deprecated, renamed: "init(eventLoop:headers:logger:userInfo:closeFuture:)") - public convenience init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfo: UserInfo = UserInfo() - ) { - self.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: .init(userInfo), - closeFuture: eventLoop.makeFailedFuture(GRPCStatus.closeFutureNotImplemented) - ) - } - - public convenience init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfo: UserInfo = UserInfo(), - closeFuture: EventLoopFuture - ) { - self.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: .init(userInfo), - closeFuture: closeFuture - ) - } - - @inlinable - override internal init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfoRef: Ref, - closeFuture: EventLoopFuture - ) { - self.statusPromise = eventLoop.makePromise() - super.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: userInfoRef, - closeFuture: closeFuture - ) - } - - /// Send a response to the client. - /// - /// - Parameters: - /// - message: The message to send to the client. - /// - compression: Whether compression should be used for this response. If compression - /// is enabled in the call context, the value passed here takes precedence. Defaults to - /// deferring to the value set on the call context. - /// - promise: A promise to complete once the message has been sent. - open func sendResponse( - _ message: ResponsePayload, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) { - fatalError("needs to be overridden") - } - - /// Send a response to the client. - /// - /// - Parameters: - /// - message: The message to send to the client. - /// - compression: Whether compression should be used for this response. If compression - /// is enabled in the call context, the value passed here takes precedence. Defaults to - /// deferring to the value set on the call context. - open func sendResponse( - _ message: ResponsePayload, - compression: Compression = .deferToCallDefault - ) -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Void.self) - self.sendResponse(message, compression: compression, promise: promise) - return promise.futureResult - } - - /// Sends a sequence of responses to the client. - /// - Parameters: - /// - messages: The messages to send to the client. - /// - compression: Whether compression should be used for this response. If compression - /// is enabled in the call context, the value passed here takes precedence. Defaults to - /// deferring to the value set on the call context. - /// - promise: A promise to complete once the messages have been sent. - open func sendResponses( - _ messages: Messages, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) where Messages.Element == ResponsePayload { - fatalError("needs to be overridden") - } - - /// Sends a sequence of responses to the client. - /// - Parameters: - /// - messages: The messages to send to the client. - /// - compression: Whether compression should be used for this response. If compression - /// is enabled in the call context, the value passed here takes precedence. Defaults to - /// deferring to the value set on the call context. - open func sendResponses( - _ messages: Messages, - compression: Compression = .deferToCallDefault - ) -> EventLoopFuture where Messages.Element == ResponsePayload { - let promise = self.eventLoop.makePromise(of: Void.self) - self.sendResponses(messages, compression: compression, promise: promise) - return promise.futureResult - } -} - -/// A concrete implementation of `StreamingResponseCallContext` used internally. -@usableFromInline -internal final class _StreamingResponseCallContext: - StreamingResponseCallContext -{ - @usableFromInline - internal let _sendResponse: (Response, MessageMetadata, EventLoopPromise?) -> Void - - @usableFromInline - internal let _compressionEnabledOnServer: Bool - - @inlinable - internal init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfoRef: Ref, - compressionIsEnabled: Bool, - closeFuture: EventLoopFuture, - sendResponse: @escaping (Response, MessageMetadata, EventLoopPromise?) -> Void - ) { - self._sendResponse = sendResponse - self._compressionEnabledOnServer = compressionIsEnabled - super.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: userInfoRef, - closeFuture: closeFuture - ) - } - - @inlinable - internal func shouldCompress(_ compression: Compression) -> Bool { - guard self._compressionEnabledOnServer else { - return false - } - return compression.isEnabled(callDefault: self.compressionEnabled) - } - - @inlinable - override func sendResponse( - _ message: Response, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) { - if self.eventLoop.inEventLoop { - let compress = self.shouldCompress(compression) - self._sendResponse(message, .init(compress: compress, flush: true), promise) - } else { - self.eventLoop.execute { - let compress = self.shouldCompress(compression) - self._sendResponse(message, .init(compress: compress, flush: true), promise) - } - } - } - - @inlinable - override func sendResponses( - _ messages: Messages, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) where Response == Messages.Element { - if self.eventLoop.inEventLoop { - self._sendResponses(messages, compression: compression, promise: promise) - } else { - self.eventLoop.execute { - self._sendResponses(messages, compression: compression, promise: promise) - } - } - } - - @inlinable - internal func _sendResponses( - _ messages: Messages, - compression: Compression, - promise: EventLoopPromise? - ) where Response == Messages.Element { - let compress = self.shouldCompress(compression) - var iterator = messages.makeIterator() - var next = iterator.next() - - while let current = next { - next = iterator.next() - // Attach the promise, if present, to the last message. - let isLast = next == nil - self._sendResponse(current, .init(compress: compress, flush: isLast), isLast ? promise : nil) - } - } -} - -/// Concrete implementation of `StreamingResponseCallContext` used for testing. -/// -/// Simply records all sent messages. -open class StreamingResponseCallContextTestStub: StreamingResponseCallContext< - ResponsePayload -> -{ - open var recordedResponses: [ResponsePayload] = [] - - override open func sendResponse( - _ message: ResponsePayload, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) { - self.recordedResponses.append(message) - promise?.succeed(()) - } - - override open func sendResponses( - _ messages: Messages, - compression: Compression = .deferToCallDefault, - promise: EventLoopPromise? - ) where ResponsePayload == Messages.Element { - self.recordedResponses.append(contentsOf: messages) - promise?.succeed(()) - } -} diff --git a/Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift b/Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift deleted file mode 100644 index 9e3a3a892..000000000 --- a/Sources/GRPC/ServerCallContexts/UnaryResponseCallContext.swift +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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 Foundation -import Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import SwiftProtobuf - -/// A context provided to handlers for RPCs which return a single response, i.e. unary and client -/// streaming RPCs. -/// -/// For client streaming RPCs the handler must complete the `responsePromise` to return the response -/// to the client. Unary RPCs do complete the promise directly: they are provided an -/// ``StatusOnlyCallContext`` view of this context where the `responsePromise` is not exposed. Instead -/// they must return an `EventLoopFuture` from the method they are implementing. -open class UnaryResponseCallContext: ServerCallContextBase, StatusOnlyCallContext { - /// A promise for a single response message. This must be completed to send a response back to the - /// client. If the promise is failed, the failure value will be converted to ``GRPCStatus`` and - /// used as the final status for the RPC. - public let responsePromise: EventLoopPromise - - /// The status sent back to the client at the end of the RPC, providing the `responsePromise` was - /// completed successfully. - /// - /// - Important: This *must* be accessed from the context's `eventLoop` in order to ensure - /// thread-safety. - public var responseStatus: GRPCStatus { - get { - self.eventLoop.assertInEventLoop() - return self._responseStatus - } - set { - self.eventLoop.assertInEventLoop() - self._responseStatus = newValue - } - } - - private var _responseStatus: GRPCStatus = .ok - - @available(*, deprecated, renamed: "init(eventLoop:headers:logger:userInfo:closeFuture:)") - public convenience init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfo: UserInfo = UserInfo() - ) { - self.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: .init(userInfo), - closeFuture: eventLoop.makeFailedFuture(GRPCStatus.closeFutureNotImplemented) - ) - } - - public convenience init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfo: UserInfo = UserInfo(), - closeFuture: EventLoopFuture - ) { - self.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: .init(userInfo), - closeFuture: closeFuture - ) - } - - @inlinable - override internal init( - eventLoop: EventLoop, - headers: HPACKHeaders, - logger: Logger, - userInfoRef: Ref, - closeFuture: EventLoopFuture - ) { - self.responsePromise = eventLoop.makePromise() - super.init( - eventLoop: eventLoop, - headers: headers, - logger: logger, - userInfoRef: userInfoRef, - closeFuture: closeFuture - ) - } -} - -/// Protocol variant of ``UnaryResponseCallContext`` that only exposes the ``responseStatus`` and ``trailers`` -/// fields, but not `responsePromise`. -/// -/// We can use a protocol (instead of an abstract base class) here because removing the generic -/// `responsePromise` field lets us avoid associated-type requirements on the protocol. -public protocol StatusOnlyCallContext: ServerCallContext { - /// The status sent back to the client at the end of the RPC, providing the `responsePromise` was - /// completed successfully. - var responseStatus: GRPCStatus { get set } - - /// Metadata to return at the end of the RPC. - var trailers: HPACKHeaders { get set } -} - -/// Concrete implementation of `UnaryResponseCallContext` used for testing. -/// -/// Only provided to make it clear in tests that no "real" implementation is used. -open class UnaryResponseCallContextTestStub: UnaryResponseCallContext {} diff --git a/Sources/GRPC/ServerChannelErrorHandler.swift b/Sources/GRPC/ServerChannelErrorHandler.swift deleted file mode 100644 index b37b7ecea..000000000 --- a/Sources/GRPC/ServerChannelErrorHandler.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -/// A handler that passes errors thrown into the server channel to the server error delegate. -/// -/// A NIO server bootstrap produces two kinds of channels. The first and most common is the "child" channel: -/// each of these corresponds to one connection, and has the connection state stored on it. The other kind is -/// the "server" channel. Each bootstrap produces only one of these, and it is the channel that owns the listening -/// socket. -/// -/// This channel handler is inserted into the server channel, and is responsible for passing any errors in that pipeline -/// to the server error delegate. If there is no error delegate, this handler is not inserted into the pipeline. -final class ServerChannelErrorHandler { - private let errorDelegate: ServerErrorDelegate - - init(errorDelegate: ServerErrorDelegate) { - self.errorDelegate = errorDelegate - } -} - -extension ServerChannelErrorHandler: ChannelInboundHandler { - typealias InboundIn = Any - typealias InboundOut = Any - - func errorCaught(context: ChannelHandlerContext, error: Error) { - // This handler does not treat errors as fatal to the listening socket, as it's possible they were transiently - // occurring in a single connection setup attempt. - self.errorDelegate.observeLibraryError(error) - context.fireErrorCaught(error) - } -} diff --git a/Sources/GRPC/ServerErrorDelegate.swift b/Sources/GRPC/ServerErrorDelegate.swift deleted file mode 100644 index c9c8e15a3..000000000 --- a/Sources/GRPC/ServerErrorDelegate.swift +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 Foundation -import NIOCore -import NIOHPACK -import NIOHTTP1 - -public protocol ServerErrorDelegate: AnyObject { - //! FIXME: Provide more context about where the error was thrown, i.e. using `GRPCError`. - /// Called when an error is thrown in the channel pipeline. - func observeLibraryError(_ error: Error) - - /// Transforms the given error (thrown somewhere inside the gRPC library) into a new error. - /// - /// This allows library users to transform errors which may be out of their control - /// into more meaningful ``GRPCStatus`` errors before they are sent to the user. - /// - /// - note: - /// Errors returned by this method are not passed to ``observeLibraryError(_:)-5wuhj`` again. - /// - /// - note: - /// This defaults to returning `nil`. In that case, if the original error conforms to ``GRPCStatusTransformable``, - /// that error's ``GRPCStatusTransformable/makeGRPCStatus()`` result will be sent to the user. If that's not the case, either, - /// ``GRPCStatus/processingError`` is returned. - func transformLibraryError(_ error: Error) -> GRPCStatusAndTrailers? - - /// Called when a request's status or response promise is failed somewhere in the user-provided request handler code. - /// - Parameters: - /// - error: The original error the status/response promise was failed with. - /// - headers: The headers of the request whose status/response promise was failed. - func observeRequestHandlerError(_ error: Error, headers: HPACKHeaders) - - /// Transforms the given status or response promise failure into a new error. - /// - /// This allows library users to transform errors which happen during their handling of the request - /// into more meaningful ``GRPCStatus`` errors before they are sent to the user. - /// - /// - note: - /// Errors returned by this method are not passed to `observe` again. - /// - /// - note: - /// This defaults to returning `nil`. In that case, if the original error conforms to ``GRPCStatusTransformable``, - /// that error's ``GRPCStatusTransformable/makeGRPCStatus()`` result will be sent to the user. If that's not the case, either, - /// ``GRPCStatus/processingError`` is returned. - /// - /// - Parameters: - /// - error: The original error the status/response promise was failed with. - /// - headers: The headers of the request whose status/response promise was failed. - func transformRequestHandlerError( - _ error: Error, - headers: HPACKHeaders - ) -> GRPCStatusAndTrailers? -} - -extension ServerErrorDelegate { - public func observeLibraryError(_ error: Error) {} - - public func transformLibraryError(_ error: Error) -> GRPCStatusAndTrailers? { - return nil - } - - public func observeRequestHandlerError(_ error: Error, headers: HPACKHeaders) {} - - public func transformRequestHandlerError( - _ error: Error, - headers: HPACKHeaders - ) -> GRPCStatusAndTrailers? { - return nil - } -} diff --git a/Sources/GRPC/ServerErrorProcessor.swift b/Sources/GRPC/ServerErrorProcessor.swift deleted file mode 100644 index 34f98e846..000000000 --- a/Sources/GRPC/ServerErrorProcessor.swift +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 NIOHPACK - -@usableFromInline -internal enum ServerErrorProcessor { - /// Processes a library error to form a `GRPCStatus` and trailers to send back to the client. - /// - Parameter error: The error to process. - /// - Returns: The status and trailers to send to the client. - @usableFromInline - internal static func processLibraryError( - _ error: Error, - delegate: ServerErrorDelegate? - ) -> (GRPCStatus, HPACKHeaders) { - // Observe the error if we have a delegate. - delegate?.observeLibraryError(error) - - // What status are we terminating this RPC with? - // - If we have a delegate, try transforming the error. If the delegate returns trailers, merge - // them with any on the call context. - // - If we don't have a delegate, then try to transform the error to a status. - // - Fallback to a generic error. - let status: GRPCStatus - let trailers: HPACKHeaders - - if let transformed = delegate?.transformLibraryError(error) { - status = transformed.status - trailers = transformed.trailers ?? [:] - } else if let grpcStatusTransformable = error as? GRPCStatusTransformable { - status = grpcStatusTransformable.makeGRPCStatus() - trailers = [:] - } else { - // Eh... well, we don't know what status to use. Use a generic one. - status = .processingError(cause: error) - trailers = [:] - } - - return (status, trailers) - } - - /// Processes an error, transforming it into a 'GRPCStatus' and any trailers to send to the peer. - @usableFromInline - internal static func processObserverError( - _ error: Error, - headers: HPACKHeaders, - trailers: HPACKHeaders, - delegate: ServerErrorDelegate? - ) -> (GRPCStatus, HPACKHeaders) { - // Observe the error if we have a delegate. - delegate?.observeRequestHandlerError(error, headers: headers) - - // What status are we terminating this RPC with? - // - If we have a delegate, try transforming the error. If the delegate returns trailers, merge - // them with any on the call context. - // - If we don't have a delegate, then try to transform the error to a status. - // - Fallback to a generic error. - let status: GRPCStatus - let mergedTrailers: HPACKHeaders - - if let transformed = delegate?.transformRequestHandlerError(error, headers: headers) { - status = transformed.status - if var transformedTrailers = transformed.trailers { - // The delegate returned trailers: merge in those from the context as well. - transformedTrailers.add(contentsOf: trailers) - mergedTrailers = transformedTrailers - } else { - mergedTrailers = trailers - } - } else if let grpcStatusTransformable = error as? GRPCStatusTransformable { - status = grpcStatusTransformable.makeGRPCStatus() - mergedTrailers = trailers - } else { - // Eh... well, we don't what status to use. Use a generic one. - status = .processingError(cause: error) - mergedTrailers = trailers - } - - return (status, mergedTrailers) - } -} diff --git a/Sources/GRPC/Stopwatch.swift b/Sources/GRPC/Stopwatch.swift deleted file mode 100644 index f3dff7751..000000000 --- a/Sources/GRPC/Stopwatch.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 Foundation - -internal class Stopwatch { - private let dateProvider: () -> Date - private let start: Date - - init(provider: @escaping () -> Date = { Date() }) { - self.dateProvider = provider - self.start = provider() - } - - static func start() -> Stopwatch { - return Stopwatch() - } - - func elapsed() -> TimeInterval { - return self.dateProvider().timeIntervalSince(self.start) - } - - func elapsedMillis() -> Int64 { - return Int64(self.elapsed() * 1000) - } -} diff --git a/Sources/GRPC/StreamEvent.swift b/Sources/GRPC/StreamEvent.swift deleted file mode 100644 index 63922097d..000000000 --- a/Sources/GRPC/StreamEvent.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 SwiftProtobuf - -/// An event that can occur on a client-streaming RPC. Provided to the event observer registered for that call. -public enum StreamEvent { - case message(Message) - case end - //! FIXME: Also support errors in this type, to propagate them to the event handler. -} diff --git a/Sources/GRPC/TLSVerificationHandler.swift b/Sources/GRPC/TLSVerificationHandler.swift deleted file mode 100644 index c8b200449..000000000 --- a/Sources/GRPC/TLSVerificationHandler.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOTLS - -/// Application protocol identifiers for ALPN. -internal enum GRPCApplicationProtocolIdentifier { - static let gRPC = "grpc-exp" - static let h2 = "h2" - static let http1_1 = "http/1.1" - - static let client = [gRPC, h2] - static let server = [gRPC, h2, http1_1] - - static func isHTTP2Like(_ value: String) -> Bool { - switch value { - case self.gRPC, self.h2: - return true - default: - return false - } - } - - static func isHTTP1(_ value: String) -> Bool { - return value == self.http1_1 - } -} - -internal class TLSVerificationHandler: ChannelInboundHandler, RemovableChannelHandler { - typealias InboundIn = Any - private let logger: Logger - - init(logger: Logger) { - self.logger = logger - } - - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - if let tlsEvent = event as? TLSUserEvent { - switch tlsEvent { - case let .handshakeCompleted(negotiatedProtocol: .some(`protocol`)): - self.logger.debug("TLS handshake completed, negotiated protocol: \(`protocol`)") - case .handshakeCompleted(negotiatedProtocol: nil): - self.logger.debug("TLS handshake completed, no protocol negotiated") - case .shutdownCompleted: - () - } - } - - context.fireUserInboundEventTriggered(event) - } -} diff --git a/Sources/GRPC/TLSVersion.swift b/Sources/GRPC/TLSVersion.swift deleted file mode 100644 index 1c1c0c73f..000000000 --- a/Sources/GRPC/TLSVersion.swift +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2022, 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 NIOCore - -#if canImport(NIOSSL) -import NIOSSL -#endif -#if canImport(Network) -import Network -import NIOTransportServices -#endif - -// The same as 'TLSVersion' which is defined in NIOSSL which we don't always have. -enum GRPCTLSVersion: Hashable { - case tlsv1 - case tlsv11 - case tlsv12 - case tlsv13 -} - -#if canImport(NIOSSL) -extension GRPCTLSVersion { - init(_ tlsVersion: TLSVersion) { - switch tlsVersion { - case .tlsv1: - self = .tlsv1 - case .tlsv11: - self = .tlsv11 - case .tlsv12: - self = .tlsv12 - case .tlsv13: - self = .tlsv13 - } - } -} -#endif - -#if canImport(Network) -@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) -extension GRPCTLSVersion { - init?(_ metadata: NWProtocolTLS.Metadata) { - let protocolMetadata = metadata.securityProtocolMetadata - - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - let nwTLSVersion = sec_protocol_metadata_get_negotiated_tls_protocol_version(protocolMetadata) - switch nwTLSVersion { - case .TLSv10: - self = .tlsv1 - case .TLSv11: - self = .tlsv11 - case .TLSv12: - self = .tlsv12 - case .TLSv13: - self = .tlsv13 - case .DTLSv10, .DTLSv12: - return nil - @unknown default: - return nil - } - } else { - let sslVersion = sec_protocol_metadata_get_negotiated_protocol_version(protocolMetadata) - switch sslVersion { - case .sslProtocolUnknown: - return nil - case .tlsProtocol1, .tlsProtocol1Only: - self = .tlsv1 - case .tlsProtocol11: - self = .tlsv11 - case .tlsProtocol12: - self = .tlsv12 - case .tlsProtocol13: - self = .tlsv13 - case .dtlsProtocol1, - .dtlsProtocol12, - .sslProtocol2, - .sslProtocol3, - .sslProtocol3Only, - .sslProtocolAll, - .tlsProtocolMaxSupported: - return nil - @unknown default: - return nil - } - } - } -} -#endif - -extension Channel { - /// This method tries to get the TLS version from either the Network.framework or NIOSSL - /// - Precondition: Must be called on the `EventLoop` the `Channel` is running on. - func getTLSVersionSync( - file: StaticString = #fileID, - line: UInt = #line - ) throws -> GRPCTLSVersion? { - #if canImport(Network) - if #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) { - do { - // cast can never fail because we explicitly ask for the NWProtocolTLS Metadata. - // it may still be nil if Network.framework isn't used for TLS in which case we will - // fall through and try to get the TLS version from NIOSSL - if let metadata = try self.getMetadataSync( - definition: NWProtocolTLS.definition, - file: file, - line: line - ) as! NWProtocolTLS.Metadata? { - return GRPCTLSVersion(metadata) - } - } catch is NIOTSChannelIsNotANIOTSConnectionChannel { - // Not a NIOTS channel, we might be using NIOSSL so try that next. - } - } - #endif - #if canImport(NIOSSL) - return try self.pipeline.syncOperations.nioSSL_tlsVersion().map(GRPCTLSVersion.init) - #else - return nil - #endif - } -} diff --git a/Sources/GRPC/TimeLimit.swift b/Sources/GRPC/TimeLimit.swift deleted file mode 100644 index 7ea6e1de1..000000000 --- a/Sources/GRPC/TimeLimit.swift +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2020, 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 Dispatch -import NIOCore - -/// A time limit for an RPC. -/// -/// RPCs may have a time limit imposed on them by a caller which may be timeout or deadline based. -/// If the RPC has not completed before the limit is reached then the call will be cancelled and -/// completed with a ``GRPCStatus/Code-swift.struct/deadlineExceeded`` status code. -/// -/// - Note: Servers may impose a time limit on an RPC independent of the client's time limit; RPCs -/// may therefore complete with ``GRPCStatus/Code-swift.struct/deadlineExceeded`` even if no time limit was set by the client. -public struct TimeLimit: Equatable, CustomStringConvertible, Sendable { - // private but for shimming. - private enum Wrapped: Equatable, Sendable { - case none - case timeout(TimeAmount) - case deadline(NIODeadline) - } - - // private but for shimming. - private var wrapped: Wrapped - - private init(_ wrapped: Wrapped) { - self.wrapped = wrapped - } - - /// No time limit, the RPC will not be automatically cancelled by the client. Note: some services - /// may impose a time limit on RPCs independent of the client's time limit. - public static let none = TimeLimit(.none) - - /// Create a timeout before which the RPC must have completed. Failure to complete before the - /// deadline will result in the RPC being cancelled. - /// - /// - Note: The timeout is started once the call has been invoked and the call may timeout waiting - /// for an active connection. - public static func timeout(_ timeout: TimeAmount) -> TimeLimit { - return TimeLimit(.timeout(timeout)) - } - - /// Create a point in time by which the RPC must have completed. Failure to complete before the - /// deadline will result in the RPC being cancelled. - public static func deadline(_ deadline: NIODeadline) -> TimeLimit { - return TimeLimit(.deadline(deadline)) - } - - /// Return the timeout, if one was set. - public var timeout: TimeAmount? { - switch self.wrapped { - case let .timeout(timeout): - return timeout - - case .none, .deadline: - return nil - } - } - - /// Return the deadline, if one was set. - public var deadline: NIODeadline? { - switch self.wrapped { - case let .deadline(deadline): - return deadline - - case .none, .timeout: - return nil - } - } -} - -extension TimeLimit { - /// Make a non-distant-future deadline from the give time limit. - @usableFromInline - internal func makeDeadline() -> NIODeadline { - switch self.wrapped { - case .none: - return .distantFuture - - case let .timeout(timeout) where timeout.nanoseconds == .max: - return .distantFuture - - case let .timeout(timeout): - return .now() + timeout - - case let .deadline(deadline): - return deadline - } - } - - public var description: String { - switch self.wrapped { - case .none: - return "none" - - case let .timeout(timeout) where timeout.nanoseconds == .max: - return "timeout=never" - - case let .timeout(timeout): - return "timeout=\(timeout.nanoseconds) nanoseconds" - - case let .deadline(deadline) where deadline == .distantFuture: - return "deadline=.distantFuture" - - case let .deadline(deadline): - return "deadline=\(deadline.uptimeNanoseconds) uptimeNanoseconds" - } - } -} diff --git a/Sources/GRPC/UserInfo.swift b/Sources/GRPC/UserInfo.swift deleted file mode 100644 index 8917a78ba..000000000 --- a/Sources/GRPC/UserInfo.swift +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2020, 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. - */ - -/// ``UserInfo`` is a dictionary for heterogeneously typed values with type safe access to the stored -/// values. -/// -/// ``UserInfo`` is shared between server interceptor contexts and server handlers, this is on a -/// per-RPC basis. ``UserInfo`` is *not* shared across a connection. -/// -/// Values are keyed by a type conforming to the ``UserInfo/Key`` protocol. The protocol requires an -/// `associatedtype`: the type of the value the key is paired with. A key can be created using a -/// caseless `enum`, for example: -/// -/// ``` -/// enum IDKey: UserInfo.Key { -/// typealias Value = Int -/// } -/// ``` -/// -/// Values can be set and retrieved from ``UserInfo`` by subscripting with the key: -/// -/// ``` -/// userInfo[IDKey.self] = 42 -/// let id = userInfo[IDKey.self] // id = 42 -/// -/// userInfo[IDKey.self] = nil -/// ``` -/// -/// More convenient access can be provided with helper extensions on ``UserInfo``: -/// -/// ``` -/// extension UserInfo { -/// var id: IDKey.Value? { -/// get { self[IDKey.self] } -/// set { self[IDKey.self] = newValue } -/// } -/// } -/// ``` -public struct UserInfo: CustomStringConvertible { - private var storage: [AnyUserInfoKey: Any] - - /// A protocol for a key. - public typealias Key = UserInfoKey - - /// Create an empty 'UserInfo'. - public init() { - self.storage = [:] - } - - /// Allows values to be set and retrieved in a type safe way. - public subscript(key: Key.Type) -> Key.Value? { - get { - if let anyValue = self.storage[AnyUserInfoKey(key)] { - // The types must line up here. - return (anyValue as! Key.Value) - } else { - return nil - } - } - set { - self.storage[AnyUserInfoKey(key)] = newValue - } - } - - public var description: String { - return "[" - + self.storage.map { key, value in - "\(key): \(value)" - }.joined(separator: ", ") + "]" - } - - /// A `UserInfoKey` wrapper. - private struct AnyUserInfoKey: Hashable, CustomStringConvertible { - private let keyType: Any.Type - - var description: String { - return String(describing: self.keyType.self) - } - - init(_ keyType: Key.Type) { - self.keyType = keyType - } - - static func == (lhs: AnyUserInfoKey, rhs: AnyUserInfoKey) -> Bool { - return ObjectIdentifier(lhs.keyType) == ObjectIdentifier(rhs.keyType) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self.keyType)) - } - } -} - -public protocol UserInfoKey { - /// The type of identified by this key. - associatedtype Value -} diff --git a/Sources/GRPC/Version.swift b/Sources/GRPC/Version.swift deleted file mode 100644 index 3fa958c6f..000000000 --- a/Sources/GRPC/Version.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -internal enum Version { - /// The major version. - internal static let major = 1 - - /// The minor version. - internal static let minor = 23 - - /// The patch version. - internal static let patch = 1 - - /// The version string. - internal static let versionString = "\(major).\(minor).\(patch)" -} diff --git a/Sources/GRPC/WebCORSHandler.swift b/Sources/GRPC/WebCORSHandler.swift deleted file mode 100644 index 6b2f546aa..000000000 --- a/Sources/GRPC/WebCORSHandler.swift +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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 NIOCore -import NIOHTTP1 - -/// Handler that manages the CORS protocol for requests incoming from the browser. -internal final class WebCORSHandler { - let configuration: Server.Configuration.CORS - - private var state: State = .idle - private enum State: Equatable { - /// Starting state. - case idle - /// CORS preflight request is in progress. - case processingPreflightRequest - /// "Real" request is in progress. - case processingRequest(origin: String?) - } - - init(configuration: Server.Configuration.CORS) { - self.configuration = configuration - } -} - -extension WebCORSHandler: ChannelInboundHandler { - typealias InboundIn = HTTPServerRequestPart - typealias InboundOut = HTTPServerRequestPart - typealias OutboundOut = HTTPServerResponsePart - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - switch self.unwrapInboundIn(data) { - case let .head(head): - self.receivedRequestHead(context: context, head) - - case let .body(body): - self.receivedRequestBody(context: context, body) - - case let .end(trailers): - self.receivedRequestEnd(context: context, trailers) - } - } - - private func receivedRequestHead(context: ChannelHandlerContext, _ head: HTTPRequestHead) { - if head.method == .OPTIONS, - head.headers.contains(.accessControlRequestMethod), - let origin = head.headers.first(name: "origin") - { - // If the request is OPTIONS with a access-control-request-method header it's a CORS - // preflight request and is not propagated further. - self.state = .processingPreflightRequest - self.handlePreflightRequest(context: context, head: head, origin: origin) - } else { - self.state = .processingRequest(origin: head.headers.first(name: "origin")) - context.fireChannelRead(self.wrapInboundOut(.head(head))) - } - } - - private func receivedRequestBody(context: ChannelHandlerContext, _ body: ByteBuffer) { - // OPTIONS requests do not have a body, but still handle this case to be - // cautious. - if self.state == .processingPreflightRequest { - return - } - - context.fireChannelRead(self.wrapInboundOut(.body(body))) - } - - private func receivedRequestEnd(context: ChannelHandlerContext, _ trailers: HTTPHeaders?) { - if self.state == .processingPreflightRequest { - // End of OPTIONS request; reset state and finish the response. - self.state = .idle - context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) - } else { - context.fireChannelRead(self.wrapInboundOut(.end(trailers))) - } - } - - private func handlePreflightRequest( - context: ChannelHandlerContext, - head: HTTPRequestHead, - origin: String - ) { - let responseHead: HTTPResponseHead - - if let allowedOrigin = self.configuration.allowedOrigins.header(origin) { - var headers = HTTPHeaders() - headers.reserveCapacity(4 + self.configuration.allowedHeaders.count) - headers.add(name: .accessControlAllowOrigin, value: allowedOrigin) - headers.add(name: .accessControlAllowMethods, value: "POST") - - for value in self.configuration.allowedHeaders { - headers.add(name: .accessControlAllowHeaders, value: value) - } - - if self.configuration.allowCredentialedRequests { - headers.add(name: .accessControlAllowCredentials, value: "true") - } - - if self.configuration.preflightCacheExpiration > 0 { - headers.add( - name: .accessControlMaxAge, - value: "\(self.configuration.preflightCacheExpiration)" - ) - } - responseHead = HTTPResponseHead(version: head.version, status: .ok, headers: headers) - } else { - // Not allowed; respond with 403. This is okay in a pre-flight request. - responseHead = HTTPResponseHead(version: head.version, status: .forbidden) - } - - context.write(self.wrapOutboundOut(.head(responseHead)), promise: nil) - } -} - -extension WebCORSHandler: ChannelOutboundHandler { - typealias OutboundIn = HTTPServerResponsePart - - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - let responsePart = self.unwrapOutboundIn(data) - switch responsePart { - case var .head(responseHead): - switch self.state { - case let .processingRequest(origin): - self.prepareCORSResponseHead(&responseHead, origin: origin) - context.write(self.wrapOutboundOut(.head(responseHead)), promise: promise) - - case .idle, .processingPreflightRequest: - assertionFailure("Writing response head when no request is in progress") - context.close(promise: nil) - } - - case .body: - context.write(data, promise: promise) - - case .end: - self.state = .idle - context.write(data, promise: promise) - } - } - - private func prepareCORSResponseHead(_ head: inout HTTPResponseHead, origin: String?) { - guard let header = origin.flatMap({ self.configuration.allowedOrigins.header($0) }) else { - // No origin or the origin is not allowed; don't treat it as a CORS request. - return - } - - head.headers.replaceOrAdd(name: .accessControlAllowOrigin, value: header) - - if self.configuration.allowCredentialedRequests { - head.headers.add(name: .accessControlAllowCredentials, value: "true") - } - - //! FIXME: Check whether we can let browsers keep connections alive. It's not possible - // now as the channel has a state that can't be reused since the pipeline is modified to - // inject the gRPC call handler. - head.headers.replaceOrAdd(name: "Connection", value: "close") - } -} - -extension HTTPHeaders { - fileprivate enum CORSHeader: String { - case accessControlRequestMethod = "access-control-request-method" - case accessControlRequestHeaders = "access-control-request-headers" - case accessControlAllowOrigin = "access-control-allow-origin" - case accessControlAllowMethods = "access-control-allow-methods" - case accessControlAllowHeaders = "access-control-allow-headers" - case accessControlAllowCredentials = "access-control-allow-credentials" - case accessControlMaxAge = "access-control-max-age" - } - - fileprivate func contains(_ name: CORSHeader) -> Bool { - return self.contains(name: name.rawValue) - } - - fileprivate mutating func add(name: CORSHeader, value: String) { - self.add(name: name.rawValue, value: value) - } - - fileprivate mutating func replaceOrAdd(name: CORSHeader, value: String) { - self.replaceOrAdd(name: name.rawValue, value: value) - } -} - -extension Server.Configuration.CORS.AllowedOrigins { - internal func header(_ origin: String) -> String? { - switch self.wrapped { - case .all: - return "*" - case .originBased: - return origin - case let .only(allowed): - return allowed.contains(origin) ? origin : nil - case let .custom(custom): - return custom.check(origin: origin) - } - } -} diff --git a/Sources/GRPC/WriteCapturingHandler.swift b/Sources/GRPC/WriteCapturingHandler.swift deleted file mode 100644 index c8ed4e88e..000000000 --- a/Sources/GRPC/WriteCapturingHandler.swift +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -/// A handler which redirects all writes into a callback until the `.end` part is seen, after which -/// all writes will be failed. -/// -/// This handler is intended for use with 'fake' response streams the 'FakeChannel'. -internal final class WriteCapturingHandler: ChannelOutboundHandler { - typealias OutboundIn = _GRPCClientRequestPart - typealias RequestHandler = (FakeRequestPart) -> Void - - private var state: State - private enum State { - case active(RequestHandler) - case inactive - } - - internal init(requestHandler: @escaping RequestHandler) { - self.state = .active(requestHandler) - } - - internal func write( - context: ChannelHandlerContext, - data: NIOAny, - promise: EventLoopPromise? - ) { - guard case let .active(handler) = self.state else { - promise?.fail(ChannelError.ioOnClosedChannel) - return - } - - switch self.unwrapOutboundIn(data) { - case let .head(requestHead): - handler(.metadata(requestHead.customMetadata)) - - case let .message(messageContext): - handler(.message(messageContext.message)) - - case .end: - handler(.end) - // We're done now. - self.state = .inactive - } - - promise?.succeed(()) - } -} diff --git a/Sources/GRPC/_EmbeddedThroughput.swift b/Sources/GRPC/_EmbeddedThroughput.swift deleted file mode 100644 index 397392d66..000000000 --- a/Sources/GRPC/_EmbeddedThroughput.swift +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOEmbedded -import SwiftProtobuf - -extension EmbeddedChannel { - /// Configures an `EmbeddedChannel` for the `EmbeddedClientThroughput` benchmark. - /// - /// - Important: This is **not** part of the public API. - public func _configureForEmbeddedThroughputTest( - callType: GRPCCallType, - logger: Logger, - requestType: Request.Type = Request.self, - responseType: Response.Type = Response.self - ) -> EventLoopFuture { - return self.pipeline.addHandlers([ - GRPCClientChannelHandler( - callType: callType, - maximumReceiveMessageLength: .max, - logger: logger - ), - GRPCClientCodecHandler( - serializer: ProtobufSerializer(), - deserializer: ProtobufDeserializer() - ), - ]) - } - - public func _configureForEmbeddedServerTest( - servicesByName serviceProviders: [Substring: CallHandlerProvider], - encoding: ServerMessageEncoding, - normalizeHeaders: Bool, - logger: Logger - ) -> EventLoopFuture { - let codec = HTTP2ToRawGRPCServerCodec( - servicesByName: serviceProviders, - encoding: encoding, - errorDelegate: nil, - normalizeHeaders: normalizeHeaders, - maximumReceiveMessageLength: .max, - logger: logger - ) - return self.pipeline.addHandler(codec) - } - - public func _configureForServerFuzzing(configuration: Server.Configuration) throws { - let configurator = GRPCServerPipelineConfigurator(configuration: configuration) - // We're always on an `EmbeddedEventLoop`, this is fine. - try self.pipeline.syncOperations.addHandler(configurator) - } -} diff --git a/Sources/GRPC/_FakeResponseStream.swift b/Sources/GRPC/_FakeResponseStream.swift deleted file mode 100644 index 7e23ba8e4..000000000 --- a/Sources/GRPC/_FakeResponseStream.swift +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import NIOHPACK - -public enum FakeRequestPart { - case metadata(HPACKHeaders) - case message(Request) - case end -} - -extension FakeRequestPart: Equatable where Request: Equatable {} - -/// Sending on a fake response stream would have resulted in a protocol violation (such as -/// sending initial metadata multiple times or sending messages after the stream has closed). -public struct FakeResponseProtocolViolation: Error, Hashable { - /// The reason that sending the message would have resulted in a protocol violation. - public var reason: String - - init(_ reason: String) { - self.reason = reason - } -} - -/// A fake response stream into which users may inject response parts for use in unit tests. -/// -/// Users may not interact with this class directly but may do so via one of its subclasses -/// `FakeUnaryResponse` and `FakeStreamingResponse`. -public class _FakeResponseStream { - private enum StreamEvent { - case responsePart(_GRPCClientResponsePart) - case error(Error) - } - - /// The channel to use for communication. - internal let channel: EmbeddedChannel - - /// A buffer to hold responses in before the proxy is activated. - private var responseBuffer: CircularBuffer - - /// The current state of the proxy. - private var activeState: ActiveState - - /// The state of sending response parts. - private var sendState: SendState - - private enum ActiveState { - case inactive - case active - } - - private enum SendState { - // Nothing has been sent; we can send initial metadata to become 'sending' or trailing metadata - // to start 'closing'. - case idle - - // We're sending messages. We can send more messages in this state or trailing metadata to - // transition to 'closing'. - case sending - - // We're closing: we've sent trailing metadata, we may only send a status now to close. - case closing - - // Closed, nothing more can be sent. - case closed - } - - internal init(requestHandler: @escaping (FakeRequestPart) -> Void) { - self.activeState = .inactive - self.sendState = .idle - self.responseBuffer = CircularBuffer() - self.channel = EmbeddedChannel(handler: WriteCapturingHandler(requestHandler: requestHandler)) - } - - /// Activate the test proxy; this should be called - internal func activate() { - switch self.activeState { - case .inactive: - // Activate the channel. This will allow any request parts to be sent. - self.channel.pipeline.fireChannelActive() - - // Unbuffer any response parts. - while !self.responseBuffer.isEmpty { - self.write(self.responseBuffer.removeFirst()) - } - - // Now we're active. - self.activeState = .active - - case .active: - () - } - } - - /// Write or buffer the response part, depending on the our current state. - internal func _sendResponsePart(_ part: _GRPCClientResponsePart) throws { - try self.send(.responsePart(part)) - } - - internal func _sendError(_ error: Error) throws { - try self.send(.error(error)) - } - - private func send(_ event: StreamEvent) throws { - switch self.validate(event) { - case .valid: - self.writeOrBuffer(event) - - case let .validIfSentAfter(extraPart): - self.writeOrBuffer(extraPart) - self.writeOrBuffer(event) - - case let .invalid(reason): - throw FakeResponseProtocolViolation(reason) - } - } - - /// Validate events the user wants to send on the stream. - private func validate(_ event: StreamEvent) -> Validation { - switch (event, self.sendState) { - case (.responsePart(.initialMetadata), .idle): - self.sendState = .sending - return .valid - - case (.responsePart(.initialMetadata), .sending), - (.responsePart(.initialMetadata), .closing), - (.responsePart(.initialMetadata), .closed): - // We can only send initial metadata from '.idle'. - return .invalid(reason: "Initial metadata has already been sent") - - case (.responsePart(.message), .idle): - // This is fine: we don't force the user to specify initial metadata so we send some on their - // behalf. - self.sendState = .sending - return .validIfSentAfter(.responsePart(.initialMetadata([:]))) - - case (.responsePart(.message), .sending): - return .valid - - case (.responsePart(.message), .closing), - (.responsePart(.message), .closed): - // We can't send messages once we're closing or closed. - return .invalid(reason: "Messages can't be sent after the stream has been closed") - - case (.responsePart(.trailingMetadata), .idle), - (.responsePart(.trailingMetadata), .sending): - self.sendState = .closing - return .valid - - case (.responsePart(.trailingMetadata), .closing), - (.responsePart(.trailingMetadata), .closed): - // We're already closing or closed. - return .invalid(reason: "Trailing metadata can't be sent after the stream has been closed") - - case (.responsePart(.status), .idle), - (.error, .idle), - (.responsePart(.status), .sending), - (.error, .sending), - (.responsePart(.status), .closed), - (.error, .closed): - // We can only error/close if we're closing (i.e. have already sent trailers which we enforce - // from the API in the subclasses). - return .invalid(reason: "Status/error can only be sent after trailing metadata has been sent") - - case (.responsePart(.status), .closing), - (.error, .closing): - self.sendState = .closed - return .valid - } - } - - private enum Validation { - /// Sending the part is valid. - case valid - - /// Sending the part, if it is sent after the given part. - case validIfSentAfter(_ part: StreamEvent) - - /// Sending the part would be a protocol violation. - case invalid(reason: String) - } - - private func writeOrBuffer(_ event: StreamEvent) { - switch self.activeState { - case .inactive: - self.responseBuffer.append(event) - - case .active: - self.write(event) - } - } - - private func write(_ part: StreamEvent) { - switch part { - case let .error(error): - self.channel.pipeline.fireErrorCaught(error) - - case let .responsePart(responsePart): - // We tolerate errors here: an error will be thrown if the write results in an error which - // isn't caught in the channel. Errors in the channel get funnelled into the transport held - // by the actual call object and handled there. - _ = try? self.channel.writeInbound(responsePart) - } - } -} - -// MARK: - Unary Response - -/// A fake unary response to be used with a generated test client. -/// -/// Users typically create fake responses via helper methods on their generated test clients -/// corresponding to the RPC which they intend to test. -/// -/// For unary responses users may call one of two functions for each RPC: -/// - `sendMessage(_:initialMetadata:trailingMetadata:status)`, or -/// - `sendError(status:trailingMetadata)` -/// -/// `sendMessage` sends a normal unary response with the provided message and allows the caller to -/// also specify initial metadata, trailing metadata and the status. Both metadata arguments are -/// empty by default and the status defaults to one with an 'ok' status code. -/// -/// `sendError` may be used to terminate an RPC without providing a response. As for `sendMessage`, -/// the `trailingMetadata` defaults to being empty. -public class FakeUnaryResponse: _FakeResponseStream { - override public init(requestHandler: @escaping (FakeRequestPart) -> Void = { _ in }) { - super.init(requestHandler: requestHandler) - } - - /// Send a response message to the client. - /// - /// - Parameters: - /// - response: The message to send. - /// - initialMetadata: The initial metadata to send. By default the metadata will be empty. - /// - trailingMetadata: The trailing metadata to send. By default the metadata will be empty. - /// - status: The status to send. By default this has an '.ok' status code. - /// - Throws: FakeResponseProtocolViolation if sending the message would violate the gRPC - /// protocol, e.g. sending messages after the RPC has ended. - public func sendMessage( - _ response: Response, - initialMetadata: HPACKHeaders = [:], - trailingMetadata: HPACKHeaders = [:], - status: GRPCStatus = .ok - ) throws { - try self._sendResponsePart(.initialMetadata(initialMetadata)) - try self._sendResponsePart(.message(.init(response, compressed: false))) - try self._sendResponsePart(.trailingMetadata(trailingMetadata)) - try self._sendResponsePart(.status(status)) - } - - /// Send an error to the client. - /// - /// - Parameters: - /// - error: The error to send. - /// - trailingMetadata: The trailing metadata to send. By default the metadata will be empty. - public func sendError(_ error: Error, trailingMetadata: HPACKHeaders = [:]) throws { - try self._sendResponsePart(.trailingMetadata(trailingMetadata)) - try self._sendError(error) - } -} - -// MARK: - Streaming Response - -/// A fake streaming response to be used with a generated test client. -/// -/// Users typically create fake responses via helper methods on their generated test clients -/// corresponding to the RPC which they intend to test. -/// -/// For streaming responses users have a number of methods available to them: -/// - `sendInitialMetadata(_:)` -/// - `sendMessage(_:)` -/// - `sendEnd(status:trailingMetadata:)` -/// - `sendError(_:trailingMetadata)` -/// -/// `sendInitialMetadata` may be called to send initial metadata to the client, however, it -/// must be called first in order for the metadata to be sent. If it is not called, empty -/// metadata will be sent automatically if necessary. -/// -/// `sendMessage` may be called to send a response message on the stream. This may be called -/// multiple times. Messages will be ignored if this is called after `sendEnd` or `sendError`. -/// -/// `sendEnd` indicates that the response stream has closed. It – or `sendError` - must be called -/// once. The `status` defaults to a value with the `ok` code and `trailingMetadata` is empty by -/// default. -/// -/// `sendError` may be called at any time to indicate an error on the response stream. -/// Like `sendEnd`, `trailingMetadata` is empty by default. -public class FakeStreamingResponse: _FakeResponseStream { - override public init(requestHandler: @escaping (FakeRequestPart) -> Void = { _ in }) { - super.init(requestHandler: requestHandler) - } - - /// Send initial metadata to the client. - /// - /// Note that calling this function is not required; empty initial metadata will be sent - /// automatically if necessary. - /// - /// - Parameter metadata: The metadata to send - /// - Throws: FakeResponseProtocolViolation if sending initial metadata would violate the gRPC - /// protocol, e.g. sending metadata too many times, or out of order. - public func sendInitialMetadata(_ metadata: HPACKHeaders) throws { - try self._sendResponsePart(.initialMetadata(metadata)) - } - - /// Send a response message to the client. - /// - /// - Parameter response: The response to send. - /// - Throws: FakeResponseProtocolViolation if sending the message would violate the gRPC - /// protocol, e.g. sending messages after the RPC has ended. - public func sendMessage(_ response: Response) throws { - try self._sendResponsePart(.message(.init(response, compressed: false))) - } - - /// Send the RPC status and trailing metadata to the client. - /// - /// - Parameters: - /// - status: The status to send. By default the status code will be '.ok'. - /// - trailingMetadata: The trailing metadata to send. Empty by default. - /// - Throws: FakeResponseProtocolViolation if ending the RPC would violate the gRPC - /// protocol, e.g. sending end after the RPC has already completed. - public func sendEnd(status: GRPCStatus = .ok, trailingMetadata: HPACKHeaders = [:]) throws { - try self._sendResponsePart(.trailingMetadata(trailingMetadata)) - try self._sendResponsePart(.status(status)) - } - - /// Send an error to the client. - /// - /// - Parameters: - /// - error: The error to send. - /// - trailingMetadata: The trailing metadata to send. By default the metadata will be empty. - /// - Throws: FakeResponseProtocolViolation if sending the error would violate the gRPC - /// protocol, e.g. erroring after the RPC has already completed. - public func sendError(_ error: Error, trailingMetadata: HPACKHeaders = [:]) throws { - try self._sendResponsePart(.trailingMetadata(trailingMetadata)) - try self._sendError(error) - } -} diff --git a/Sources/GRPC/_GRPCClientCodecHandler.swift b/Sources/GRPC/_GRPCClientCodecHandler.swift deleted file mode 100644 index f1e1dd43c..000000000 --- a/Sources/GRPC/_GRPCClientCodecHandler.swift +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore - -internal class GRPCClientCodecHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer -> { - /// The request serializer. - private let serializer: Serializer - - /// The response deserializer. - private let deserializer: Deserializer - - internal init(serializer: Serializer, deserializer: Deserializer) { - self.serializer = serializer - self.deserializer = deserializer - } -} - -extension GRPCClientCodecHandler: ChannelInboundHandler { - typealias InboundIn = _RawGRPCClientResponsePart - typealias InboundOut = _GRPCClientResponsePart - - internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - switch self.unwrapInboundIn(data) { - case let .initialMetadata(headers): - context.fireChannelRead(self.wrapInboundOut(.initialMetadata(headers))) - - case let .message(messageContext): - do { - let response = try self.deserializer.deserialize(byteBuffer: messageContext.message) - context - .fireChannelRead( - self - .wrapInboundOut(.message(.init(response, compressed: messageContext.compressed))) - ) - } catch { - context.fireErrorCaught(error) - } - - case let .trailingMetadata(trailers): - context.fireChannelRead(self.wrapInboundOut(.trailingMetadata(trailers))) - - case let .status(status): - context.fireChannelRead(self.wrapInboundOut(.status(status))) - } - } -} - -extension GRPCClientCodecHandler: ChannelOutboundHandler { - typealias OutboundIn = _GRPCClientRequestPart - typealias OutboundOut = _RawGRPCClientRequestPart - - internal func write( - context: ChannelHandlerContext, - data: NIOAny, - promise: EventLoopPromise? - ) { - switch self.unwrapOutboundIn(data) { - case let .head(head): - context.write(self.wrapOutboundOut(.head(head)), promise: promise) - - case let .message(message): - do { - let serialized = try self.serializer.serialize( - message.message, - allocator: context.channel.allocator - ) - context.write( - self.wrapOutboundOut(.message(.init(serialized, compressed: message.compressed))), - promise: promise - ) - } catch { - promise?.fail(error) - context.fireErrorCaught(error) - } - - case .end: - context.write(self.wrapOutboundOut(.end), promise: promise) - } - } -} - -// MARK: Reverse Codec - -internal class GRPCClientReverseCodecHandler< - Serializer: MessageSerializer, - Deserializer: MessageDeserializer -> { - /// The request serializer. - private let serializer: Serializer - - /// The response deserializer. - private let deserializer: Deserializer - - internal init(serializer: Serializer, deserializer: Deserializer) { - self.serializer = serializer - self.deserializer = deserializer - } -} - -extension GRPCClientReverseCodecHandler: ChannelInboundHandler { - typealias InboundIn = _GRPCClientResponsePart - typealias InboundOut = _RawGRPCClientResponsePart - - internal func channelRead(context: ChannelHandlerContext, data: NIOAny) { - switch self.unwrapInboundIn(data) { - case let .initialMetadata(headers): - context.fireChannelRead(self.wrapInboundOut(.initialMetadata(headers))) - - case let .message(messageContext): - do { - let response = try self.serializer.serialize( - messageContext.message, - allocator: context.channel.allocator - ) - context.fireChannelRead( - self.wrapInboundOut(.message(.init(response, compressed: messageContext.compressed))) - ) - } catch { - context.fireErrorCaught(error) - } - - case let .trailingMetadata(trailers): - context.fireChannelRead(self.wrapInboundOut(.trailingMetadata(trailers))) - - case let .status(status): - context.fireChannelRead(self.wrapInboundOut(.status(status))) - } - } -} - -extension GRPCClientReverseCodecHandler: ChannelOutboundHandler { - typealias OutboundIn = _RawGRPCClientRequestPart - typealias OutboundOut = _GRPCClientRequestPart - - internal func write( - context: ChannelHandlerContext, - data: NIOAny, - promise: EventLoopPromise? - ) { - switch self.unwrapOutboundIn(data) { - case let .head(head): - context.write(self.wrapOutboundOut(.head(head)), promise: promise) - - case let .message(message): - do { - let deserialized = try self.deserializer.deserialize(byteBuffer: message.message) - context.write( - self.wrapOutboundOut(.message(.init(deserialized, compressed: message.compressed))), - promise: promise - ) - } catch { - promise?.fail(error) - context.fireErrorCaught(error) - } - - case .end: - context.write(self.wrapOutboundOut(.end), promise: promise) - } - } -} diff --git a/Sources/GRPC/_MessageContext.swift b/Sources/GRPC/_MessageContext.swift deleted file mode 100644 index 74b80e597..000000000 --- a/Sources/GRPC/_MessageContext.swift +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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. - */ - -/// Provides a context for gRPC payloads. -/// -/// - Important: This is **NOT** part of the public API. -public final class _MessageContext { - /// The message being sent or received. - let message: Message - - /// Whether the message was, or should be compressed. - let compressed: Bool - - /// Constructs a box for a value. - /// - /// - Important: This is **NOT** part of the public API. - public init(_ message: Message, compressed: Bool) { - self.message = message - self.compressed = compressed - } -} diff --git a/Sources/GRPCConnectionBackoffInteropTest/README.md b/Sources/GRPCConnectionBackoffInteropTest/README.md deleted file mode 100644 index 3f4d8ee96..000000000 --- a/Sources/GRPCConnectionBackoffInteropTest/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# gRPC Connection Backoff Interoperability Test - -This module implements the gRPC connection backoff interoperability test as -described in the [specification][interop-test]. - -## Running the Test - -The C++ interoperability test server implements the required server and should -be targeted when running this test. It is available in the main [gRPC -repository][grpc-repo] and may be built using `bazel` (`bazel build -test/cpp/interop:reconnect_interop_server`) or one of the other options for -[building the C++ source][grpc-cpp-build]. - -1. Start the server: `./path/to/server --control_port=8080 --retry_port=8081` -1. Start the test: `swift run ConnectionBackoffInteropTestRunner 8080 8081` - -The test takes **approximately 10 minutes to complete** and logs are written to -`stderr`. - -[interop-test]: https://github.com/grpc/grpc/blob/master/doc/connection-backoff-interop-test-description.md -[grpc-cpp-build]: https://github.com/grpc/grpc/blob/master/BUILDING.md -[grpc-repo]: https://github.com/grpc/grpc.git diff --git a/Sources/GRPCConnectionBackoffInteropTest/main.swift b/Sources/GRPCConnectionBackoffInteropTest/main.swift deleted file mode 100644 index c8124a6bc..000000000 --- a/Sources/GRPCConnectionBackoffInteropTest/main.swift +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import ArgumentParser -import struct Foundation.Date -import GRPC -import GRPCInteroperabilityTestModels -import Logging -import NIOCore -import NIOPosix - -// Notes from the test procedure are inline. -// See: https://github.com/grpc/grpc/blob/master/doc/connection-backoff-interop-test-description.md - -// MARK: - Setup - -// Since this is a long running test, print connectivity state changes to stdout with timestamps. -// We'll redirect logs to stderr so that stdout contains information only relevant to the test. -final class PrintingConnectivityStateDelegate: ConnectivityStateDelegate { - func connectivityStateDidChange( - from oldState: ConnectivityState, - to newState: ConnectivityState - ) { - print("[\(Date())] connectivity state change: \(oldState) → \(newState)") - } -} - -func runTest(controlPort: Int, retryPort: Int) throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - - // MARK: - Test Procedure - - print("[\(Date())] Starting connection backoff interoperability test...") - - // 1. Call 'Start' on server control port with a large deadline or no deadline, wait for it to - // finish and check it succeeded. - let controlConnection = ClientConnection.insecure(group: group) - .connect(host: "localhost", port: controlPort) - let controlClient = Grpc_Testing_ReconnectServiceNIOClient(channel: controlConnection) - print("[\(Date())] Control 'Start' call started") - let controlStart = controlClient.start(.init(), callOptions: .init(timeLimit: .none)) - let controlStartStatus = try controlStart.status.wait() - assert(controlStartStatus.code == .ok, "Control Start rpc failed: \(controlStartStatus.code)") - print("[\(Date())] Control 'Start' call succeeded") - - // 2. Initiate a channel connection to server retry port, which should perform reconnections with - // proper backoffs. A convenient way to achieve this is to call 'Start' with a deadline of 540s. - // The rpc should fail with deadline exceeded. - print("[\(Date())] Retry 'Start' call started") - let retryConnection = ClientConnection.usingTLSBackedByNIOSSL(on: group) - .withConnectivityStateDelegate(PrintingConnectivityStateDelegate()) - .connect(host: "localhost", port: retryPort) - let retryClient = Grpc_Testing_ReconnectServiceNIOClient( - channel: retryConnection, - defaultCallOptions: CallOptions(timeLimit: .timeout(.seconds(540))) - ) - let retryStart = retryClient.start(.init()) - // We expect this to take some time! - let retryStartStatus = try retryStart.status.wait() - assert( - retryStartStatus.code == .deadlineExceeded, - "Retry Start rpc status was not 'deadlineExceeded': \(retryStartStatus.code)" - ) - print("[\(Date())] Retry 'Start' call terminated with expected status") - - // 3. Call 'Stop' on server control port and check it succeeded. - print("[\(Date())] Control 'Stop' call started") - let controlStop = controlClient.stop(.init()) - let controlStopStatus = try controlStop.status.wait() - assert(controlStopStatus.code == .ok, "Control Stop rpc failed: \(controlStopStatus.code)") - print("[\(Date())] Control 'Stop' call succeeded") - - // 4. Check the response to see whether the server thinks the backoffs passed the test. - let controlResponse = try controlStop.response.wait() - assert(controlResponse.passed, "TEST FAILED") - print("[\(Date())] TEST PASSED") - - // MARK: - Tear down - - // Close the connections. - - // We expect close to fail on the retry connection because the channel should never be successfully - // started. - print("[\(Date())] Closing Retry connection") - try? retryConnection.close().wait() - print("[\(Date())] Closing Control connection") - try controlConnection.close().wait() -} - -struct ConnectionBackoffInteropTest: ParsableCommand { - @Option - var controlPort: Int - - @Option - var retryPort: Int - - func run() throws { - do { - try runTest(controlPort: self.controlPort, retryPort: self.retryPort) - } catch { - print("[\(Date())] Unexpected error: \(error)") - throw error - } - } -} - -ConnectionBackoffInteropTest.main() -#endif // canImport(NIOSSL) diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift b/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift deleted file mode 100644 index 4cf354857..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/ClientConnectionHandler.swift +++ /dev/null @@ -1,681 +0,0 @@ -/* - * 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. - */ - -package import NIOCore -package import NIOHTTP2 - -/// An event which happens on a client's HTTP/2 connection. -package enum ClientConnectionEvent: Sendable { - package enum CloseReason: Sendable { - /// The server sent a GOAWAY frame to the client. - case goAway(HTTP2ErrorCode, String) - /// The keep alive timer fired and subsequently timed out. - case keepaliveExpired - /// The connection became idle. - case idle - /// The local peer initiated the close. - case initiatedLocally - /// The connection was closed unexpectedly - case unexpected((any Error)?, isIdle: Bool) - } - - /// The connection is now ready. - case ready - - /// The connection has started shutting down, no new streams should be created. - case closing(CloseReason) -} - -/// A `ChannelHandler` which manages part of the lifecycle of a gRPC connection over HTTP/2. -/// -/// This handler is responsible for managing several aspects of the connection. These include: -/// 1. Periodically sending keep alive pings to the server (if configured) and closing the -/// connection if necessary. -/// 2. Closing the connection if it is idle (has no open streams) for a configured amount of time. -/// 3. Forwarding lifecycle events to the next handler. -/// -/// Some of the behaviours are described in [gRFC A8](https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md). -package final class ClientConnectionHandler: ChannelInboundHandler, ChannelOutboundHandler { - package typealias InboundIn = HTTP2Frame - package typealias InboundOut = ClientConnectionEvent - - package typealias OutboundIn = Never - package typealias OutboundOut = HTTP2Frame - - package enum OutboundEvent: Hashable, Sendable { - /// Close the connection gracefully - case closeGracefully - } - - /// The `EventLoop` of the `Channel` this handler exists in. - private let eventLoop: any EventLoop - - /// The maximum amount of time the connection may be idle for. If the connection remains idle - /// (i.e. has no open streams) for this period of time then the connection will be gracefully - /// closed. - private var maxIdleTimer: Timer? - - /// The amount of time to wait before sending a keep alive ping. - private var keepaliveTimer: Timer? - - /// The amount of time the client has to reply after sending a keep alive ping. Only used if - /// `keepaliveTimer` is set. - private var keepaliveTimeoutTimer: Timer - - /// Opaque data sent in keep alive pings. - private let keepalivePingData: HTTP2PingData - - /// The current state of the connection. - private var state: StateMachine - - /// Whether a flush is pending. - private var flushPending: Bool - /// Whether `channelRead` has been called and `channelReadComplete` hasn't yet been called. - /// Resets once `channelReadComplete` returns. - private var inReadLoop: Bool - - /// The context of the channel this handler is in. - private var context: ChannelHandlerContext? - - /// Creates a new handler which manages the lifecycle of a connection. - /// - /// - Parameters: - /// - eventLoop: The `EventLoop` of the `Channel` this handler is placed in. - /// - maxIdleTime: The maximum amount time a connection may be idle for before being closed. - /// - keepaliveTime: The amount of time to wait after reading data before sending a keep-alive - /// ping. - /// - keepaliveTimeout: The amount of time the client has to reply after the server sends a - /// keep-alive ping to keep the connection open. The connection is closed if no reply - /// is received. - /// - keepaliveWithoutCalls: Whether the client sends keep-alive pings when there are no calls - /// in progress. - package init( - eventLoop: any EventLoop, - maxIdleTime: TimeAmount?, - keepaliveTime: TimeAmount?, - keepaliveTimeout: TimeAmount?, - keepaliveWithoutCalls: Bool - ) { - self.eventLoop = eventLoop - self.maxIdleTimer = maxIdleTime.map { Timer(delay: $0) } - self.keepaliveTimer = keepaliveTime.map { Timer(delay: $0, repeat: true) } - self.keepaliveTimeoutTimer = Timer(delay: keepaliveTimeout ?? .seconds(20)) - self.keepalivePingData = HTTP2PingData(withInteger: .random(in: .min ... .max)) - self.state = StateMachine(allowKeepaliveWithoutCalls: keepaliveWithoutCalls) - - self.flushPending = false - self.inReadLoop = false - } - - package func handlerAdded(context: ChannelHandlerContext) { - assert(context.eventLoop === self.eventLoop) - self.context = context - } - - package func handlerRemoved(context: ChannelHandlerContext) { - self.context = nil - } - - package func channelInactive(context: ChannelHandlerContext) { - switch self.state.closed() { - case .none: - () - - case .unexpectedClose(let error, let isIdle): - let event = self.wrapInboundOut(.closing(.unexpected(error, isIdle: isIdle))) - context.fireChannelRead(event) - - case .succeed(let promise): - promise.succeed() - } - - self.keepaliveTimer?.cancel() - self.keepaliveTimeoutTimer.cancel() - context.fireChannelInactive() - } - - package func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - switch event { - case let event as NIOHTTP2StreamCreatedEvent: - self._streamCreated(event.streamID, channel: context.channel) - - case let event as StreamClosedEvent: - self._streamClosed(event.streamID, channel: context.channel) - - default: - () - } - - context.fireUserInboundEventTriggered(event) - } - - package func errorCaught(context: ChannelHandlerContext, error: any Error) { - // Store the error and close, this will result in the final close event being fired down - // the pipeline with an appropriate close reason and appropriate error. (This avoids - // the async channel just throwing the error.) - self.state.receivedError(error) - context.close(mode: .all, promise: nil) - } - - package func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let frame = self.unwrapInboundIn(data) - self.inReadLoop = true - - switch frame.payload { - case .goAway(_, let errorCode, let data): - if errorCode == .noError { - // Receiving a GOAWAY frame means we need to stop creating streams immediately and start - // closing the connection. - switch self.state.beginGracefulShutdown(promise: nil) { - case .sendGoAway(let close): - // gRPC servers may indicate why the GOAWAY was sent in the opaque data. - let message = data.map { String(buffer: $0) } ?? "" - context.fireChannelRead(self.wrapInboundOut(.closing(.goAway(errorCode, message)))) - - // Clients should send GOAWAYs when closing a connection. - self.writeAndFlushGoAway(context: context, errorCode: .noError) - if close { - context.close(promise: nil) - } - - case .none: - () - } - } else { - // Some error, begin closing. - if self.state.beginClosing() { - // gRPC servers may indicate why the GOAWAY was sent in the opaque data. - let message = data.map { String(buffer: $0) } ?? "" - context.fireChannelRead(self.wrapInboundOut(.closing(.goAway(errorCode, message)))) - context.close(promise: nil) - } - } - - case .ping(let data, let ack): - // Pings are ack'd by the HTTP/2 handler so we only pay attention to acks here, and in - // particular only those carrying the keep-alive data. - if ack, data == self.keepalivePingData { - let loopBound = LoopBoundView(handler: self, context: context) - self.keepaliveTimeoutTimer.cancel() - self.keepaliveTimer?.schedule(on: context.eventLoop) { - loopBound.keepaliveTimerFired() - } - } - - case .settings(.settings(_)): - let isInitialSettings = self.state.receivedSettings() - - // The first settings frame indicates that the connection is now ready to use. The channel - // becoming active is insufficient as, for example, a TLS handshake may fail after - // establishing the TCP connection, or the server isn't configured for gRPC (or HTTP/2). - if isInitialSettings { - let loopBound = LoopBoundView(handler: self, context: context) - self.keepaliveTimer?.schedule(on: context.eventLoop) { - loopBound.keepaliveTimerFired() - } - - self.maxIdleTimer?.schedule(on: context.eventLoop) { - loopBound.maxIdleTimerFired() - } - - context.fireChannelRead(self.wrapInboundOut(.ready)) - } - - default: - () - } - } - - package func channelReadComplete(context: ChannelHandlerContext) { - while self.flushPending { - self.flushPending = false - context.flush() - } - - self.inReadLoop = false - context.fireChannelReadComplete() - } - - package func triggerUserOutboundEvent( - context: ChannelHandlerContext, - event: Any, - promise: EventLoopPromise? - ) { - if let event = event as? OutboundEvent { - switch event { - case .closeGracefully: - switch self.state.beginGracefulShutdown(promise: promise) { - case .sendGoAway(let close): - context.fireChannelRead(self.wrapInboundOut(.closing(.initiatedLocally))) - // The client could send a GOAWAY at this point but it's not really necessary, the server - // can't open streams anyway, the client will just close the connection when it's done. - if close { - context.close(promise: nil) - } - - case .none: - () - } - } - } else { - context.triggerUserOutboundEvent(event, promise: promise) - } - } -} - -extension ClientConnectionHandler { - struct LoopBoundView: @unchecked Sendable { - private let handler: ClientConnectionHandler - private let context: ChannelHandlerContext - - init(handler: ClientConnectionHandler, context: ChannelHandlerContext) { - self.handler = handler - self.context = context - } - - func keepaliveTimerFired() { - self.context.eventLoop.assertInEventLoop() - self.handler.keepaliveTimerFired(context: self.context) - } - - func keepaliveTimeoutExpired() { - self.context.eventLoop.assertInEventLoop() - self.handler.keepaliveTimeoutExpired(context: self.context) - } - - func maxIdleTimerFired() { - self.context.eventLoop.assertInEventLoop() - self.handler.maxIdleTimerFired(context: self.context) - } - } -} - -extension ClientConnectionHandler { - package struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { - // @unchecked is okay: the only methods do the appropriate event-loop dance. - - private let handler: ClientConnectionHandler - - init(_ handler: ClientConnectionHandler) { - self.handler = handler - } - - package func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { - if self.handler.eventLoop.inEventLoop { - self.handler._streamCreated(id, channel: channel) - } else { - self.handler.eventLoop.execute { - self.handler._streamCreated(id, channel: channel) - } - } - } - - package func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { - if self.handler.eventLoop.inEventLoop { - self.handler._streamClosed(id, channel: channel) - } else { - self.handler.eventLoop.execute { - self.handler._streamClosed(id, channel: channel) - } - } - } - } - - package var http2StreamDelegate: HTTP2StreamDelegate { - return HTTP2StreamDelegate(self) - } - - private func _streamCreated(_ id: HTTP2StreamID, channel: any Channel) { - self.eventLoop.assertInEventLoop() - - // Stream created, so the connection isn't idle. - self.maxIdleTimer?.cancel() - self.state.streamOpened(id) - } - - private func _streamClosed(_ id: HTTP2StreamID, channel: any Channel) { - guard let context = self.context else { return } - self.eventLoop.assertInEventLoop() - - switch self.state.streamClosed(id) { - case .startIdleTimer(let cancelKeepalive): - // All streams are closed, restart the idle timer, and stop the keep-alive timer (it may - // not stop if keep-alive is allowed when there are no active calls). - let loopBound = LoopBoundView(handler: self, context: context) - self.maxIdleTimer?.schedule(on: context.eventLoop) { - loopBound.maxIdleTimerFired() - } - - if cancelKeepalive { - self.keepaliveTimer?.cancel() - } - - case .close: - // Connection was closing but waiting for all streams to close. They must all be closed - // now so close the connection. - context.close(promise: nil) - - case .none: - () - } - } -} - -extension ClientConnectionHandler { - private func maybeFlush(context: ChannelHandlerContext) { - if self.inReadLoop { - self.flushPending = true - } else { - context.flush() - } - } - - private func keepaliveTimerFired(context: ChannelHandlerContext) { - guard self.state.sendKeepalivePing() else { return } - - // Cancel the keep alive timer when the client sends a ping. The timer is resumed when the ping - // is acknowledged. - self.keepaliveTimer?.cancel() - - let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepalivePingData, ack: false)) - context.write(self.wrapOutboundOut(ping), promise: nil) - self.maybeFlush(context: context) - - // Schedule a timeout on waiting for the response. - let loopBound = LoopBoundView(handler: self, context: context) - self.keepaliveTimeoutTimer.schedule(on: context.eventLoop) { - loopBound.keepaliveTimeoutExpired() - } - } - - private func keepaliveTimeoutExpired(context: ChannelHandlerContext) { - guard self.state.beginClosing() else { return } - - context.fireChannelRead(self.wrapInboundOut(.closing(.keepaliveExpired))) - self.writeAndFlushGoAway(context: context, message: "keepalive_expired") - context.close(promise: nil) - } - - private func maxIdleTimerFired(context: ChannelHandlerContext) { - guard self.state.beginClosing() else { return } - - context.fireChannelRead(self.wrapInboundOut(.closing(.idle))) - self.writeAndFlushGoAway(context: context, message: "idle") - context.close(promise: nil) - } - - private func writeAndFlushGoAway( - context: ChannelHandlerContext, - errorCode: HTTP2ErrorCode = .noError, - message: String? = nil - ) { - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway( - lastStreamID: 0, - errorCode: errorCode, - opaqueData: message.map { context.channel.allocator.buffer(string: $0) } - ) - ) - - context.write(self.wrapOutboundOut(goAway), promise: nil) - self.maybeFlush(context: context) - } -} - -extension ClientConnectionHandler { - struct StateMachine { - private var state: State - - private enum State { - case active(Active) - case closing(Closing) - case closed - case _modifying - - struct Active { - var openStreams: Set - var allowKeepaliveWithoutCalls: Bool - var receivedConnectionPreface: Bool - var error: (any Error)? - - init(allowKeepaliveWithoutCalls: Bool) { - self.openStreams = [] - self.allowKeepaliveWithoutCalls = allowKeepaliveWithoutCalls - self.receivedConnectionPreface = false - self.error = nil - } - - mutating func receivedSettings() -> Bool { - let isFirstSettingsFrame = !self.receivedConnectionPreface - self.receivedConnectionPreface = true - return isFirstSettingsFrame - } - } - - struct Closing { - var allowKeepaliveWithoutCalls: Bool - var openStreams: Set - var closePromise: Optional> - var isGraceful: Bool - - init(from state: Active, isGraceful: Bool, closePromise: EventLoopPromise?) { - self.openStreams = state.openStreams - self.isGraceful = isGraceful - self.allowKeepaliveWithoutCalls = state.allowKeepaliveWithoutCalls - self.closePromise = closePromise - } - } - } - - init(allowKeepaliveWithoutCalls: Bool) { - self.state = .active(State.Active(allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls)) - } - - /// Record that a SETTINGS frame was received from the remote peer. - /// - /// - Returns: `true` if this was the first SETTINGS frame received. - mutating func receivedSettings() -> Bool { - switch self.state { - case .active(var active): - self.state = ._modifying - let isFirstSettingsFrame = active.receivedSettings() - self.state = .active(active) - return isFirstSettingsFrame - - case .closing, .closed: - return false - - case ._modifying: - preconditionFailure() - } - } - - /// Record that an error was received. - mutating func receivedError(_ error: any Error) { - switch self.state { - case .active(var active): - self.state = ._modifying - active.error = error - self.state = .active(active) - case .closing, .closed: - () - case ._modifying: - preconditionFailure() - } - } - - /// Record that the stream with the given ID has been opened. - mutating func streamOpened(_ id: HTTP2StreamID) { - switch self.state { - case .active(var state): - self.state = ._modifying - let (inserted, _) = state.openStreams.insert(id) - assert(inserted, "Can't open stream \(Int(id)), it's already open") - self.state = .active(state) - - case .closing(var state): - self.state = ._modifying - let (inserted, _) = state.openStreams.insert(id) - assert(inserted, "Can't open stream \(Int(id)), it's already open") - self.state = .closing(state) - - case .closed: - () - - case ._modifying: - preconditionFailure() - } - } - - enum OnStreamClosed: Equatable { - /// Start the idle timer, after which the connection should be closed gracefully. - case startIdleTimer(cancelKeepalive: Bool) - /// Close the connection. - case close - /// Do nothing. - case none - } - - /// Record that the stream with the given ID has been closed. - mutating func streamClosed(_ id: HTTP2StreamID) -> OnStreamClosed { - let onStreamClosed: OnStreamClosed - - switch self.state { - case .active(var state): - self.state = ._modifying - let removedID = state.openStreams.remove(id) - assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") - if state.openStreams.isEmpty { - onStreamClosed = .startIdleTimer(cancelKeepalive: !state.allowKeepaliveWithoutCalls) - } else { - onStreamClosed = .none - } - self.state = .active(state) - - case .closing(var state): - self.state = ._modifying - let removedID = state.openStreams.remove(id) - assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") - onStreamClosed = state.openStreams.isEmpty ? .close : .none - self.state = .closing(state) - - case .closed: - onStreamClosed = .none - - case ._modifying: - preconditionFailure() - } - - return onStreamClosed - } - - /// Returns whether a keep alive ping should be sent to the server. - func sendKeepalivePing() -> Bool { - let sendKeepalivePing: Bool - - // Only send a ping if there are open streams or there are no open streams and keep alive - // is permitted when there are no active calls. - switch self.state { - case .active(let state): - sendKeepalivePing = !state.openStreams.isEmpty || state.allowKeepaliveWithoutCalls - case .closing(let state): - sendKeepalivePing = !state.openStreams.isEmpty || state.allowKeepaliveWithoutCalls - case .closed: - sendKeepalivePing = false - case ._modifying: - preconditionFailure() - } - - return sendKeepalivePing - } - - enum OnGracefulShutDown: Equatable { - case sendGoAway(Bool) - case none - } - - mutating func beginGracefulShutdown(promise: EventLoopPromise?) -> OnGracefulShutDown { - let onGracefulShutdown: OnGracefulShutDown - - switch self.state { - case .active(let state): - self.state = ._modifying - // Only close immediately if there are no open streams. The client doesn't need to - // ratchet down the last stream ID as only the client creates streams in gRPC. - let close = state.openStreams.isEmpty - onGracefulShutdown = .sendGoAway(close) - self.state = .closing(State.Closing(from: state, isGraceful: true, closePromise: promise)) - - case .closing(var state): - self.state = ._modifying - state.closePromise.setOrCascade(to: promise) - self.state = .closing(state) - onGracefulShutdown = .none - - case .closed: - onGracefulShutdown = .none - - case ._modifying: - preconditionFailure() - } - - return onGracefulShutdown - } - - /// Returns whether the connection should be closed. - mutating func beginClosing() -> Bool { - switch self.state { - case .active(let active): - self.state = .closing(State.Closing(from: active, isGraceful: false, closePromise: nil)) - return true - case .closing(var state): - self.state = ._modifying - let forceShutdown = state.isGraceful - state.isGraceful = false - self.state = .closing(state) - return forceShutdown - case .closed: - return false - case ._modifying: - preconditionFailure() - } - } - - enum OnClosed { - case succeed(EventLoopPromise) - case unexpectedClose((any Error)?, isIdle: Bool) - case none - } - - /// Marks the state as closed. - mutating func closed() -> OnClosed { - switch self.state { - case .active(let state): - self.state = .closed - return .unexpectedClose(state.error, isIdle: state.openStreams.isEmpty) - case .closing(let closing): - self.state = .closed - return closing.closePromise.map { .succeed($0) } ?? .none - case .closed: - self.state = .closed - return .none - case ._modifying: - preconditionFailure() - } - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift b/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift deleted file mode 100644 index a90d2a9e8..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/Connection.swift +++ /dev/null @@ -1,498 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -package import NIOCore -package import NIOHTTP2 -private import Synchronization - -/// A `Connection` provides communication to a single remote peer. -/// -/// Each `Connection` object is 'one-shot': it may only be used for a single connection over -/// its lifetime. If a connect attempt fails then the `Connection` must be discarded and a new one -/// must be created. However, an active connection may be used multiple times to provide streams -/// to the backend. -/// -/// To use the `Connection` you must run it in a task. You can consume event updates by listening -/// to `events`: -/// -/// ```swift -/// await withTaskGroup(of: Void.self) { group in -/// group.addTask { await connection.run() } -/// -/// for await event in connection.events { -/// switch event { -/// case .connectSucceeded: -/// // ... -/// default: -/// // ... -/// } -/// } -/// } -/// ``` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package final class Connection: Sendable { - /// Events which can happen over the lifetime of the connection. - package enum Event: Sendable { - /// The connect attempt succeeded and the connection is ready to use. - case connectSucceeded - /// The connect attempt failed. - case connectFailed(any Error) - /// The connection received a GOAWAY and will close soon. No new streams - /// should be opened on this connection. - case goingAway(HTTP2ErrorCode, String) - /// The connection is closed. - case closed(Connection.CloseReason) - } - - /// The reason the connection closed. - package enum CloseReason: Sendable { - /// Closed because an idle timeout fired. - case idleTimeout - /// Closed because a keepalive timer fired. - case keepaliveTimeout - /// Closed because the caller initiated shutdown and all RPCs on the connection finished. - case initiatedLocally - /// Closed because the remote peer initiate shutdown (i.e. sent a GOAWAY frame). - case remote - /// Closed because the connection encountered an unexpected error. - case error(any Error, wasIdle: Bool) - } - - /// Inputs to the 'run' method. - private enum Input: Sendable { - case close - } - - /// Events which have happened to the connection. - private let event: (stream: AsyncStream, continuation: AsyncStream.Continuation) - - /// Events which the connection must react to. - private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) - - /// The address to connect to. - private let address: SocketAddress - - /// The default compression algorithm used for requests. - private let defaultCompression: CompressionAlgorithm - - /// The set of enabled compression algorithms. - private let enabledCompression: CompressionAlgorithmSet - - /// A connector used to establish a connection. - private let http2Connector: any HTTP2Connector - - /// The state of the connection. - private let state: Mutex - - /// The default max request message size in bytes, 4 MiB. - private static var defaultMaxRequestMessageSizeBytes: Int { - 4 * 1024 * 1024 - } - - /// A stream of events which can happen to the connection. - package var events: AsyncStream { - self.event.stream - } - - package init( - address: SocketAddress, - http2Connector: any HTTP2Connector, - defaultCompression: CompressionAlgorithm, - enabledCompression: CompressionAlgorithmSet - ) { - self.address = address - self.defaultCompression = defaultCompression - self.enabledCompression = enabledCompression - self.http2Connector = http2Connector - self.event = AsyncStream.makeStream(of: Event.self) - self.input = AsyncStream.makeStream(of: Input.self) - self.state = Mutex(.notConnected) - } - - /// Connect and run the connection. - /// - /// This function returns when the connection has closed. You can observe connection events - /// by consuming the ``events`` sequence. - package func run() async { - let connectResult = await Result { - try await self.http2Connector.establishConnection(to: self.address) - } - - switch connectResult { - case .success(let connected): - // Connected successfully, update state and report the event. - self.state.withLock { state in - state.connected(connected) - } - - await withDiscardingTaskGroup { group in - // Add a task to run the connection and consume events. - group.addTask { - try? await connected.channel.executeThenClose { inbound, outbound in - await self.consumeConnectionEvents(inbound) - } - } - - // Meanwhile, consume input events. This sequence will end when the connection has closed. - for await input in self.input.stream { - switch input { - case .close: - let asyncChannel = self.state.withLock { $0.beginClosing() } - if let channel = asyncChannel?.channel { - let event = ClientConnectionHandler.OutboundEvent.closeGracefully - channel.triggerUserOutboundEvent(event, promise: nil) - } - } - } - } - - case .failure(let error): - // Connect failed, this connection is no longer useful. - self.state.withLock { $0.closed() } - self.finishStreams(withEvent: .connectFailed(error)) - } - } - - /// Gracefully close the connection. - package func close() { - self.input.continuation.yield(.close) - } - - /// Make a stream using the connection if it's connected. - /// - /// - Parameter descriptor: A descriptor of the method to create a stream for. - /// - Returns: The open stream. - package func makeStream( - descriptor: MethodDescriptor, - options: CallOptions - ) async throws -> Stream { - let (multiplexer, scheme) = try self.state.withLock { state in - switch state { - case .connected(let connected): - return (connected.multiplexer, connected.scheme) - case .notConnected, .closing, .closed: - throw RPCError(code: .unavailable, message: "subchannel isn't ready") - } - } - - let compression: CompressionAlgorithm - if let override = options.compression { - compression = self.enabledCompression.contains(override) ? override : .none - } else { - compression = self.defaultCompression - } - - let maxRequestSize = options.maxRequestMessageBytes ?? Self.defaultMaxRequestMessageSizeBytes - - do { - let stream = try await multiplexer.openStream { channel in - channel.eventLoop.makeCompletedFuture { - let streamHandler = GRPCClientStreamHandler( - methodDescriptor: descriptor, - scheme: scheme, - outboundEncoding: compression, - acceptedEncodings: self.enabledCompression, - maxPayloadSize: maxRequestSize - ) - try channel.pipeline.syncOperations.addHandler(streamHandler) - - return try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: NIOAsyncChannel.Configuration( - isOutboundHalfClosureEnabled: true, - inboundType: RPCResponsePart.self, - outboundType: RPCRequestPart.self - ) - ) - } - } - - return Stream(wrapping: stream, descriptor: descriptor) - } catch { - throw RPCError(code: .unavailable, message: "subchannel is unavailable", cause: error) - } - } - - private func consumeConnectionEvents( - _ connectionEvents: NIOAsyncChannelInboundStream - ) async { - // The connection becomes 'ready' when the initial HTTP/2 SETTINGS frame is received. - // Establishing a TCP connection is insufficient as the TLS handshake may not complete or the - // server might not be configured for gRPC or HTTP/2. - // - // This state is tracked here so that if the connection events sequence finishes and the - // connection never became ready then the connection can report that the connect failed. - var isReady = false - - func makeNeverReadyError(cause: (any Error)?) -> RPCError { - return RPCError( - code: .unavailable, - message: """ - The server accepted the TCP connection but closed the connection before completing \ - the HTTP/2 connection preface. - """, - cause: cause - ) - } - - do { - var channelCloseReason: ClientConnectionEvent.CloseReason? - - for try await connectionEvent in connectionEvents { - switch connectionEvent { - case .ready: - isReady = true - self.event.continuation.yield(.connectSucceeded) - - case .closing(let reason): - self.state.withLock { $0.closing() } - - switch reason { - case .goAway(let errorCode, let reason): - // The connection will close at some point soon, yield a notification for this - // because the close might not be imminent and this could result in address resolution. - self.event.continuation.yield(.goingAway(errorCode, reason)) - case .idle, .keepaliveExpired, .initiatedLocally, .unexpected: - // The connection will be closed imminently in these cases there's no need to do - // anything. - () - } - - // Take the reason with the highest precedence. A GOAWAY may be superseded by user - // closing, for example. - if channelCloseReason.map({ reason.precedence > $0.precedence }) ?? true { - channelCloseReason = reason - } - } - } - - let finalEvent: Event - if isReady { - let connectionCloseReason: CloseReason - switch channelCloseReason { - case .keepaliveExpired: - connectionCloseReason = .keepaliveTimeout - - case .idle: - // Connection became idle, that's fine. - connectionCloseReason = .idleTimeout - - case .goAway: - // Remote peer told us to GOAWAY. - connectionCloseReason = .remote - - case .initiatedLocally: - // Shutdown was initiated locally. - connectionCloseReason = .initiatedLocally - - case .unexpected(let error, let isIdle): - let error = RPCError( - code: .unavailable, - message: "The TCP connection was dropped unexpectedly.", - cause: error - ) - connectionCloseReason = .error(error, wasIdle: isIdle) - - case .none: - let error = RPCError( - code: .unavailable, - message: "The TCP connection was dropped unexpectedly.", - cause: nil - ) - connectionCloseReason = .error(error, wasIdle: true) - } - - finalEvent = .closed(connectionCloseReason) - } else { - // The connection never became ready, this therefore counts as a failed connect attempt. - finalEvent = .connectFailed(makeNeverReadyError(cause: nil)) - } - - // The connection events sequence has finished: the connection is now closed. - self.state.withLock { $0.closed() } - self.finishStreams(withEvent: finalEvent) - } catch { - let finalEvent: Event - - if isReady { - // Any error must come from consuming the inbound channel meaning that the connection - // must be borked, wrap it up and close. - let rpcError = RPCError(code: .unavailable, message: "connection closed", cause: error) - finalEvent = .closed(.error(rpcError, wasIdle: true)) - } else { - // The connection never became ready, this therefore counts as a failed connect attempt. - finalEvent = .connectFailed(makeNeverReadyError(cause: error)) - } - - self.state.withLock { $0.closed() } - self.finishStreams(withEvent: finalEvent) - } - } - - private func finishStreams(withEvent event: Event) { - self.event.continuation.yield(event) - self.event.continuation.finish() - self.input.continuation.finish() - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Connection { - package struct Stream { - package typealias Inbound = NIOAsyncChannelInboundStream - - package struct Outbound: ClosableRPCWriterProtocol { - package typealias Element = RPCRequestPart - - private let requestWriter: NIOAsyncChannelOutboundWriter - private let http2Stream: NIOAsyncChannel - - fileprivate init( - requestWriter: NIOAsyncChannelOutboundWriter, - http2Stream: NIOAsyncChannel - ) { - self.requestWriter = requestWriter - self.http2Stream = http2Stream - } - - package func write(_ element: RPCRequestPart) async throws { - try await self.requestWriter.write(element) - } - - package func write(contentsOf elements: some Sequence) async throws { - try await self.requestWriter.write(contentsOf: elements) - } - - package func finish() { - self.requestWriter.finish() - } - - package func finish(throwing error: any Error) { - // Fire the error inbound; this fails the inbound writer. - self.http2Stream.channel.pipeline.fireErrorCaught(error) - } - } - - let descriptor: MethodDescriptor - - private let http2Stream: NIOAsyncChannel - - init( - wrapping stream: NIOAsyncChannel, - descriptor: MethodDescriptor - ) { - self.http2Stream = stream - self.descriptor = descriptor - } - - package func execute( - _ closure: (_ inbound: Inbound, _ outbound: Outbound) async throws -> T - ) async throws -> T where T: Sendable { - try await self.http2Stream.executeThenClose { inbound, outbound in - return try await closure( - inbound, - Outbound(requestWriter: outbound, http2Stream: self.http2Stream) - ) - } - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Connection { - private enum State: Sendable { - /// The connection is idle or connecting. - case notConnected - /// A TCP connection has been established with the remote peer. However, the connection may not - /// be ready to use yet. - case connected(Connected) - /// The connection has started to close. This may be initiated locally or by the remote. - case closing - /// The connection has closed. This is a terminal state. - case closed - - struct Connected: Sendable { - /// The connection channel. - var channel: NIOAsyncChannel - /// Multiplexer for creating HTTP/2 streams. - var multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer - /// Whether the connection is plaintext, `false` implies TLS is being used. - var scheme: Scheme - - init(_ connection: HTTP2Connection) { - self.channel = connection.channel - self.multiplexer = connection.multiplexer - self.scheme = connection.isPlaintext ? .http : .https - } - } - - mutating func connected(_ channel: HTTP2Connection) { - switch self { - case .notConnected: - self = .connected(State.Connected(channel)) - case .connected, .closing, .closed: - fatalError("Invalid state: 'run()' must only be called once") - } - } - - mutating func beginClosing() -> NIOAsyncChannel? { - switch self { - case .notConnected: - fatalError("Invalid state: 'run()' must be called first") - case .connected(let connected): - self = .closing - return connected.channel - case .closing, .closed: - return nil - } - } - - mutating func closing() { - switch self { - case .notConnected: - // Not reachable: happens as a result of a connection event, that can only happen if - // the connection has started (i.e. must be in the 'connected' state or later). - fatalError("Invalid state") - case .connected: - self = .closing - case .closing, .closed: - () - } - } - - mutating func closed() { - self = .closed - } - } -} - -extension ClientConnectionEvent.CloseReason { - fileprivate var precedence: Int { - switch self { - case .unexpected: - return -1 - case .goAway: - return 0 - case .idle: - return 1 - case .keepaliveExpired: - return 2 - case .initiatedLocally: - return 3 - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift deleted file mode 100644 index 8e0ed5e66..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionBackoff.swift +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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. - */ - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -package struct ConnectionBackoff { - package var initial: Duration - package var max: Duration - package var multiplier: Double - package var jitter: Double - - package init(initial: Duration, max: Duration, multiplier: Double, jitter: Double) { - self.initial = initial - self.max = max - self.multiplier = multiplier - self.jitter = jitter - } - - package func makeIterator() -> Iterator { - return Iterator(self) - } - - // Deliberately not conforming to `IteratorProtocol` as `next()` never returns `nil` which - // isn't expressible via `IteratorProtocol`. - package struct Iterator { - private var isInitial: Bool - private var currentBackoffSeconds: Double - - private let jitter: Double - private let multiplier: Double - private let maxBackoffSeconds: Double - - init(_ backoff: ConnectionBackoff) { - self.isInitial = true - self.currentBackoffSeconds = Self.seconds(from: backoff.initial) - self.jitter = backoff.jitter - self.multiplier = backoff.multiplier - self.maxBackoffSeconds = Self.seconds(from: backoff.max) - } - - private static func seconds(from duration: Duration) -> Double { - var seconds = Double(duration.components.seconds) - seconds += Double(duration.components.attoseconds) / 1e18 - return seconds - } - - private static func duration(from seconds: Double) -> Duration { - let nanoseconds = seconds * 1e9 - let wholeNanos = Int64(nanoseconds) - return .nanoseconds(wholeNanos) - } - - package mutating func next() -> Duration { - // The initial backoff doesn't get jittered. - if self.isInitial { - self.isInitial = false - return Self.duration(from: self.currentBackoffSeconds) - } - - // Scale up the last backoff. - self.currentBackoffSeconds *= self.multiplier - - // Limit it to the max backoff. - if self.currentBackoffSeconds > self.maxBackoffSeconds { - self.currentBackoffSeconds = self.maxBackoffSeconds - } - - let backoff = self.currentBackoffSeconds - let jitter = Double.random(in: -(self.jitter * backoff) ... self.jitter * backoff) - let jitteredBackoff = backoff + jitter - - return Self.duration(from: jitteredBackoff) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift deleted file mode 100644 index c56e507db..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectionFactory.swift +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - */ - -package import NIOCore -package import NIOHTTP2 -internal import NIOPosix - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -package protocol HTTP2Connector: Sendable { - func establishConnection(to address: SocketAddress) async throws -> HTTP2Connection -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -package struct HTTP2Connection: Sendable { - /// The underlying TCP connection wrapped up for use with gRPC. - var channel: NIOAsyncChannel - - /// An HTTP/2 stream multiplexer. - var multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer - - /// Whether the connection is insecure (i.e. plaintext). - var isPlaintext: Bool - - package init( - channel: NIOAsyncChannel, - multiplexer: NIOHTTP2Handler.AsyncStreamMultiplexer, - isPlaintext: Bool - ) { - self.channel = channel - self.multiplexer = multiplexer - self.isPlaintext = isPlaintext - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift b/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift deleted file mode 100644 index 6f4b000ca..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/ConnectivityState.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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. - */ - -package enum ConnectivityState: Sendable, Hashable { - /// This channel isn't trying to create a connection because of a lack of new or pending RPCs. - /// - /// New streams may be created in this state. Doing so will cause the channel to enter the - /// connecting state. - case idle - - /// The channel is trying to establish a connection and is waiting to make progress on one of the - /// steps involved in name resolution, TCP connection establishment or TLS handshake. - case connecting - - /// The channel has successfully established a connection all the way through TLS handshake (or - /// equivalent) and protocol-level (HTTP/2, etc) handshaking. - case ready - - /// There has been some transient failure (such as a TCP 3-way handshake timing out or a socket - /// error). Channels in this state will eventually switch to the ``connecting`` state and try to - /// establish a connection again. Since retries are done with exponential backoff, channels that - /// fail to connect will start out spending very little time in this state but as the attempts - /// fail repeatedly, the channel will spend increasingly large amounts of time in this state. - case transientFailure - - /// This channel has started shutting down. Any new RPCs should fail immediately. Pending RPCs - /// may continue running until the application cancels them. Channels may enter this state either - /// because the application explicitly requested a shutdown or if a non-recoverable error has - /// happened during attempts to connect. Channels that have entered this state will never leave - /// this state. - case shutdown -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift deleted file mode 100644 index 7be28da30..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/GRPCChannel.swift +++ /dev/null @@ -1,957 +0,0 @@ -/* - * 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. - */ - -private import DequeModule -package import GRPCCore -private import Synchronization - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package final class GRPCChannel: ClientTransport { - private enum Input: Sendable { - /// Close the channel, if possible. - case close - /// Handle the result of a name resolution. - case handleResolutionResult(NameResolutionResult) - /// Handle the event from the underlying connection object. - case handleLoadBalancerEvent(LoadBalancerEvent, LoadBalancerID) - } - - /// Events which can happen to the channel. - private let _connectivityState: - ( - stream: AsyncStream, - continuation: AsyncStream.Continuation - ) - - /// Inputs which this channel should react to. - private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) - - /// A resolver providing resolved names to the channel. - private let resolver: NameResolver - - /// The state of the channel. - private let state: Mutex - - /// The maximum number of times to attempt to create a stream per RPC. - /// - /// This is the value used by other gRPC implementations. - private static let maxStreamCreationAttempts = 5 - - /// A factory for connections. - private let connector: any HTTP2Connector - - /// The connection backoff configuration used by the subchannel when establishing a connection. - private let backoff: ConnectionBackoff - - /// The default compression algorithm used for requests. - private let defaultCompression: CompressionAlgorithm - - /// The set of enabled compression algorithms. - private let enabledCompression: CompressionAlgorithmSet - - /// The default service config to use. - /// - /// Used when the resolver doesn't provide one. - private let defaultServiceConfig: ServiceConfig - - // These are both read frequently and updated infrequently so may be a bottleneck. - private let _methodConfig: Mutex - private let _retryThrottle: Mutex - - package init( - resolver: NameResolver, - connector: any HTTP2Connector, - config: Config, - defaultServiceConfig: ServiceConfig - ) { - self.resolver = resolver - self.state = Mutex(StateMachine()) - self._connectivityState = AsyncStream.makeStream() - self.input = AsyncStream.makeStream() - self.connector = connector - - self.backoff = ConnectionBackoff( - initial: config.backoff.initial, - max: config.backoff.max, - multiplier: config.backoff.multiplier, - jitter: config.backoff.jitter - ) - self.defaultCompression = config.compression.algorithm - self.enabledCompression = config.compression.enabledAlgorithms - self.defaultServiceConfig = defaultServiceConfig - - let throttle = defaultServiceConfig.retryThrottling.map { RetryThrottle(policy: $0) } - self._retryThrottle = Mutex(throttle) - - let methodConfig = MethodConfigs(serviceConfig: defaultServiceConfig) - self._methodConfig = Mutex(methodConfig) - } - - /// The connectivity state of the channel. - package var connectivityState: AsyncStream { - self._connectivityState.stream - } - - /// Returns a throttle which gRPC uses to determine whether retries can be executed. - package var retryThrottle: RetryThrottle? { - self._retryThrottle.withLock { $0 } - } - - /// Returns the configuration for a given method. - /// - /// - Parameter descriptor: The method to lookup configuration for. - /// - Returns: Configuration for the method, if it exists. - package func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? { - self._methodConfig.withLock { $0[descriptor] } - } - - /// Establishes and maintains a connection to the remote destination. - package func connect() async { - self.state.withLock { $0.start() } - self._connectivityState.continuation.yield(.idle) - - await withDiscardingTaskGroup { group in - var iterator: Optional.AsyncIterator> - - // The resolver can either push or pull values. If it pushes values the channel should - // listen for new results. Otherwise the channel will pull values as and when necessary. - switch self.resolver.updateMode.value { - case .push: - iterator = nil - - let handle = group.addCancellableTask { - do { - for try await result in self.resolver.names { - self.input.continuation.yield(.handleResolutionResult(result)) - } - self.beginGracefulShutdown() - } catch { - self.beginGracefulShutdown() - } - } - - // When the channel is closed gracefully, the task group running the load balancer mustn't - // be cancelled (otherwise in-flight RPCs would fail), but the push based resolver will - // continue indefinitely. Store its handle and cancel it on close when closing the channel. - self.state.withLock { state in - state.setNameResolverTaskHandle(handle) - } - - case .pull: - iterator = self.resolver.names.makeAsyncIterator() - await self.resolve(iterator: &iterator, in: &group) - } - - // Resolver is setup, start handling events. - for await input in self.input.stream { - switch input { - case .close: - self.handleClose(in: &group) - - case .handleResolutionResult(let result): - self.handleNameResolutionResult(result, in: &group) - - case .handleLoadBalancerEvent(let event, let id): - await self.handleLoadBalancerEvent( - event, - loadBalancerID: id, - in: &group, - iterator: &iterator - ) - } - } - } - - if Task.isCancelled { - self._connectivityState.continuation.finish() - } - } - - /// Signal to the transport that no new streams may be created and that connections should be - /// closed when all streams are closed. - package func beginGracefulShutdown() { - self.input.continuation.yield(.close) - } - - /// Opens a stream using the transport, and uses it as input into a user-provided closure. - package func withStream( - descriptor: MethodDescriptor, - options: CallOptions, - _ closure: (_ stream: RPCStream) async throws -> T - ) async throws -> T { - // Merge options from the call with those from the service config. - let methodConfig = self.config(forMethod: descriptor) - var options = options - options.formUnion(with: methodConfig) - - for attempt in 1 ... Self.maxStreamCreationAttempts { - switch await self.makeStream(descriptor: descriptor, options: options) { - case .created(let stream): - return try await stream.execute { inbound, outbound in - let rpcStream = RPCStream( - descriptor: stream.descriptor, - inbound: RPCAsyncSequence(wrapping: inbound), - outbound: RPCWriter.Closable(wrapping: outbound) - ) - return try await closure(rpcStream) - } - - case .tryAgain(let error): - if error is CancellationError || attempt == Self.maxStreamCreationAttempts { - throw error - } else { - continue - } - - case .stopTrying(let error): - throw error - } - } - - fatalError("Internal inconsistency") - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel { - package struct Config: Sendable { - /// Configuration for HTTP/2 connections. - package var http2: HTTP2ClientTransport.Config.HTTP2 - - /// Configuration for backoff used when establishing a connection. - package var backoff: HTTP2ClientTransport.Config.Backoff - - /// Configuration for connection management. - package var connection: HTTP2ClientTransport.Config.Connection - - /// Compression configuration. - package var compression: HTTP2ClientTransport.Config.Compression - - package init( - http2: HTTP2ClientTransport.Config.HTTP2, - backoff: HTTP2ClientTransport.Config.Backoff, - connection: HTTP2ClientTransport.Config.Connection, - compression: HTTP2ClientTransport.Config.Compression - ) { - self.http2 = http2 - self.backoff = backoff - self.connection = connection - self.compression = compression - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel { - enum MakeStreamResult { - /// A stream was created, use it. - case created(Connection.Stream) - /// An error occurred while trying to create a stream, try again if possible. - case tryAgain(any Error) - /// An unrecoverable error occurred (e.g. the channel is closed), fail the RPC and don't retry. - case stopTrying(any Error) - } - - private func makeStream( - descriptor: MethodDescriptor, - options: CallOptions - ) async -> MakeStreamResult { - let waitForReady = options.waitForReady ?? true - switch self.state.withLock({ $0.makeStream(waitForReady: waitForReady) }) { - case .useLoadBalancer(let loadBalancer): - return await self.makeStream( - descriptor: descriptor, - options: options, - loadBalancer: loadBalancer - ) - - case .joinQueue: - do { - let loadBalancer = try await self.enqueue(waitForReady: waitForReady) - return await self.makeStream( - descriptor: descriptor, - options: options, - loadBalancer: loadBalancer - ) - } catch { - // All errors from enqueue are non-recoverable: either the channel is shutting down or - // the request has been cancelled. - return .stopTrying(error) - } - - case .failRPC: - return .stopTrying(RPCError(code: .unavailable, message: "channel isn't ready")) - } - } - - private func makeStream( - descriptor: MethodDescriptor, - options: CallOptions, - loadBalancer: LoadBalancer - ) async -> MakeStreamResult { - guard let subchannel = loadBalancer.pickSubchannel() else { - return .tryAgain(RPCError(code: .unavailable, message: "channel isn't ready")) - } - - let methodConfig = self.config(forMethod: descriptor) - var options = options - options.formUnion(with: methodConfig) - - do { - let stream = try await subchannel.makeStream(descriptor: descriptor, options: options) - return .created(stream) - } catch { - return .tryAgain(error) - } - } - - private func enqueue(waitForReady: Bool) async throws -> LoadBalancer { - let id = QueueEntryID() - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - if Task.isCancelled { - continuation.resume(throwing: CancellationError()) - return - } - - let enqueued = self.state.withLock { state in - state.enqueue(continuation: continuation, waitForReady: waitForReady, id: id) - } - - // Not enqueued because the channel is shutdown or shutting down. - if !enqueued { - let error = RPCError(code: .unavailable, message: "channel is shutdown") - continuation.resume(throwing: error) - } - } - } onCancel: { - let continuation = self.state.withLock { state in - state.dequeueContinuation(id: id) - } - - continuation?.resume(throwing: CancellationError()) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel { - private func handleClose(in group: inout DiscardingTaskGroup) { - switch self.state.withLock({ $0.close() }) { - case .close(let current, let next, let resolver, let continuations): - resolver?.cancel() - current.close() - next?.close() - for continuation in continuations { - continuation.resume(throwing: RPCError(code: .unavailable, message: "channel is closed")) - } - self._connectivityState.continuation.yield(.shutdown) - - case .cancelAll(let continuations): - for continuation in continuations { - continuation.resume(throwing: RPCError(code: .unavailable, message: "channel is closed")) - } - self._connectivityState.continuation.yield(.shutdown) - group.cancelAll() - - case .none: - () - } - } - - private func handleNameResolutionResult( - _ result: NameResolutionResult, - in group: inout DiscardingTaskGroup - ) { - // Ignore empty endpoint lists. - if result.endpoints.isEmpty { return } - - switch result.serviceConfig ?? .success(self.defaultServiceConfig) { - case .success(let config): - // Update per RPC configuration. - let methodConfig = MethodConfigs(serviceConfig: config) - self._methodConfig.withLock { $0 = methodConfig } - - let retryThrottle = config.retryThrottling.map { RetryThrottle(policy: $0) } - self._retryThrottle.withLock { $0 = retryThrottle } - - // Update the load balancer. - self.updateLoadBalancer(serviceConfig: config, endpoints: result.endpoints, in: &group) - - case .failure: - self.beginGracefulShutdown() - } - } - - enum SupportedLoadBalancerConfig { - case roundRobin - case pickFirst(ServiceConfig.LoadBalancingConfig.PickFirst) - - init?(_ config: ServiceConfig.LoadBalancingConfig) { - if let pickFirst = config.pickFirst { - self = .pickFirst(pickFirst) - } else if config.roundRobin != nil { - self = .roundRobin - } else { - return nil - } - } - - func matches(loadBalancer: LoadBalancer) -> Bool { - switch (self, loadBalancer) { - case (.roundRobin, .roundRobin): - return true - case (.pickFirst, .pickFirst): - return true - case (.roundRobin, .pickFirst), - (.pickFirst, .roundRobin): - return false - } - } - } - - private func updateLoadBalancer( - serviceConfig: ServiceConfig, - endpoints: [Endpoint], - in group: inout DiscardingTaskGroup - ) { - assert(!endpoints.isEmpty, "endpoints must be non-empty") - - // Find the first supported config. - var configFromServiceConfig: SupportedLoadBalancerConfig? - for config in serviceConfig.loadBalancingConfig { - if let config = SupportedLoadBalancerConfig(config) { - configFromServiceConfig = config - break - } - } - - let onUpdatePolicy: GRPCChannel.StateMachine.OnChangeLoadBalancer - var endpoints = endpoints - - // Fallback to pick-first if no other config applies. - let loadBalancerConfig = configFromServiceConfig ?? .pickFirst(.init(shuffleAddressList: false)) - switch loadBalancerConfig { - case .roundRobin: - onUpdatePolicy = self.state.withLock { state in - state.changeLoadBalancerKind(to: loadBalancerConfig) { - let loadBalancer = RoundRobinLoadBalancer( - connector: self.connector, - backoff: self.backoff, - defaultCompression: self.defaultCompression, - enabledCompression: self.enabledCompression - ) - return .roundRobin(loadBalancer) - } - } - - case .pickFirst(let pickFirst): - if pickFirst.shuffleAddressList { - endpoints[0].addresses.shuffle() - } - - onUpdatePolicy = self.state.withLock { state in - state.changeLoadBalancerKind(to: loadBalancerConfig) { - let loadBalancer = PickFirstLoadBalancer( - connector: self.connector, - backoff: self.backoff, - defaultCompression: self.defaultCompression, - enabledCompression: self.enabledCompression - ) - return .pickFirst(loadBalancer) - } - } - } - - self.handleLoadBalancerChange(onUpdatePolicy, endpoints: endpoints, in: &group) - } - - private func handleLoadBalancerChange( - _ update: StateMachine.OnChangeLoadBalancer, - endpoints: [Endpoint], - in group: inout DiscardingTaskGroup - ) { - assert(!endpoints.isEmpty, "endpoints must be non-empty") - - switch update { - case .runLoadBalancer(let new, let old): - old?.close() - switch new { - case .roundRobin(let loadBalancer): - loadBalancer.updateAddresses(endpoints) - case .pickFirst(let loadBalancer): - loadBalancer.updateEndpoint(endpoints.first!) - } - - group.addTask { - await new.run() - } - - group.addTask { - for await event in new.events { - self.input.continuation.yield(.handleLoadBalancerEvent(event, new.id)) - } - } - - case .updateLoadBalancer(let existing): - switch existing { - case .roundRobin(let loadBalancer): - loadBalancer.updateAddresses(endpoints) - case .pickFirst(let loadBalancer): - loadBalancer.updateEndpoint(endpoints.first!) - } - - case .none: - () - } - } - - private func handleLoadBalancerEvent( - _ event: LoadBalancerEvent, - loadBalancerID: LoadBalancerID, - in group: inout DiscardingTaskGroup, - iterator: inout RPCAsyncSequence.AsyncIterator? - ) async { - switch event { - case .connectivityStateChanged(let connectivityState): - let actions = self.state.withLock { state in - state.loadBalancerStateChanged(to: connectivityState, id: loadBalancerID) - } - - if let newState = actions.publishState { - self._connectivityState.continuation.yield(newState) - } - - if let subchannel = actions.close { - subchannel.close() - } - - if let resumable = actions.resumeContinuations { - for continuation in resumable.continuations { - continuation.resume(with: resumable.result) - } - } - - if actions.finish { - // Fully closed. - self._connectivityState.continuation.finish() - self.input.continuation.finish() - } - - case .requiresNameResolution: - await self.resolve(iterator: &iterator, in: &group) - } - } - - private func resolve( - iterator: inout RPCAsyncSequence.AsyncIterator?, - in group: inout DiscardingTaskGroup - ) async { - guard var iterator = iterator else { return } - - do { - if let result = try await iterator.next() { - self.handleNameResolutionResult(result, in: &group) - } else { - self.beginGracefulShutdown() - } - } catch { - self.beginGracefulShutdown() - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel { - struct StateMachine { - enum State { - case notRunning(NotRunning) - case running(Running) - case stopping(Stopping) - case stopped - case _modifying - - struct NotRunning { - /// Queue of requests waiting for a load-balancer. - var queue: RequestQueue - /// A handle to the name resolver task. - var nameResolverHandle: CancellableTaskHandle? - - init() { - self.queue = RequestQueue() - } - } - - struct Running { - /// The connectivity state of the channel. - var connectivityState: ConnectivityState - /// The load-balancer currently in use. - var current: LoadBalancer - /// The next load-balancer to use. This will be promoted to `current` when it enters the - /// ready state. - var next: LoadBalancer? - /// Previously created load-balancers which are in the process of shutting down. - var past: [LoadBalancerID: LoadBalancer] - /// Queue of requests wait for a load-balancer. - var queue: RequestQueue - /// A handle to the name resolver task. - var nameResolverHandle: CancellableTaskHandle? - - init( - from state: NotRunning, - loadBalancer: LoadBalancer - ) { - self.connectivityState = .idle - self.current = loadBalancer - self.next = nil - self.past = [:] - self.queue = state.queue - self.nameResolverHandle = state.nameResolverHandle - } - } - - struct Stopping { - /// Previously created load-balancers which are in the process of shutting down. - var past: [LoadBalancerID: LoadBalancer] - - init(from state: Running) { - self.past = state.past - } - - init(loadBalancers: [LoadBalancerID: LoadBalancer]) { - self.past = loadBalancers - } - } - } - - /// The current state. - private var state: State - /// Whether the channel is running. - private var running: Bool - - init() { - self.state = .notRunning(State.NotRunning()) - self.running = false - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel.StateMachine { - mutating func start() { - precondition(!self.running, "channel must only be started once") - self.running = true - } - - mutating func setNameResolverTaskHandle(_ handle: CancellableTaskHandle) { - switch self.state { - case .notRunning(var state): - state.nameResolverHandle = handle - self.state = .notRunning(state) - case .running, .stopping, .stopped, ._modifying: - fatalError("Invalid state") - } - } - - enum OnChangeLoadBalancer { - case runLoadBalancer(LoadBalancer, stop: LoadBalancer?) - case updateLoadBalancer(LoadBalancer) - case none - } - - mutating func changeLoadBalancerKind( - to newLoadBalancerKind: GRPCChannel.SupportedLoadBalancerConfig, - _ makeLoadBalancer: () -> LoadBalancer - ) -> OnChangeLoadBalancer { - let onChangeLoadBalancer: OnChangeLoadBalancer - - switch self.state { - case .notRunning(let state): - let loadBalancer = makeLoadBalancer() - let state = State.Running(from: state, loadBalancer: loadBalancer) - self.state = .running(state) - onChangeLoadBalancer = .runLoadBalancer(state.current, stop: nil) - - case .running(var state): - self.state = ._modifying - - if let next = state.next { - if newLoadBalancerKind.matches(loadBalancer: next) { - onChangeLoadBalancer = .updateLoadBalancer(next) - } else { - // The 'next' didn't become ready in time. Close it and replace it with a load-balancer - // of the next kind. - let nextNext = makeLoadBalancer() - let previous = state.next - state.next = nextNext - state.past[next.id] = next - onChangeLoadBalancer = .runLoadBalancer(nextNext, stop: previous) - } - } else { - if newLoadBalancerKind.matches(loadBalancer: state.current) { - onChangeLoadBalancer = .updateLoadBalancer(state.current) - } else { - // Create the 'next' load-balancer, it'll replace 'current' when it becomes ready. - let next = makeLoadBalancer() - state.next = next - onChangeLoadBalancer = .runLoadBalancer(next, stop: nil) - } - } - - self.state = .running(state) - - case .stopping, .stopped: - onChangeLoadBalancer = .none - - case ._modifying: - fatalError("Invalid state") - } - - return onChangeLoadBalancer - } - - struct ConnectivityStateChangeActions { - var close: LoadBalancer? = nil - var publishState: ConnectivityState? = nil - var resumeContinuations: ResumableContinuations? = nil - var finish: Bool = false - - struct ResumableContinuations { - var continuations: [CheckedContinuation] - var result: Result - } - } - - mutating func loadBalancerStateChanged( - to connectivityState: ConnectivityState, - id: LoadBalancerID - ) -> ConnectivityStateChangeActions { - var actions = ConnectivityStateChangeActions() - - switch self.state { - case .running(var state): - self.state = ._modifying - - if id == state.current.id { - // No change in state, ignore. - if state.connectivityState == connectivityState { - self.state = .running(state) - break - } - - state.connectivityState = connectivityState - actions.publishState = connectivityState - - switch connectivityState { - case .ready: - // Current load-balancer became ready; resume all continuations in the queue. - let continuations = state.queue.removeAll() - actions.resumeContinuations = ConnectivityStateChangeActions.ResumableContinuations( - continuations: continuations, - result: .success(state.current) - ) - - case .transientFailure, .shutdown: // shutdown includes shutting down - // Current load-balancer failed. Remove all the 'fast-failing' continuations in the - // queue, these are RPCs which set the 'wait for ready' option to false. The rest of - // the entries in the queue will wait for a load-balancer to become ready. - let continuations = state.queue.removeFastFailingEntries() - actions.resumeContinuations = ConnectivityStateChangeActions.ResumableContinuations( - continuations: continuations, - result: .failure(RPCError(code: .unavailable, message: "channel isn't ready")) - ) - - case .idle, .connecting: - () // Ignore. - } - } else if let next = state.next, next.id == id { - // State change came from the next LB, if it's now ready promote it to be the current. - switch connectivityState { - case .ready: - // Next load-balancer is ready, promote it to current. - let previous = state.current - state.past[previous.id] = previous - state.current = next - state.next = nil - - actions.close = previous - - if state.connectivityState != connectivityState { - actions.publishState = connectivityState - } - - actions.resumeContinuations = ConnectivityStateChangeActions.ResumableContinuations( - continuations: state.queue.removeAll(), - result: .success(next) - ) - - case .idle, .connecting, .transientFailure, .shutdown: - () - } - } - - self.state = .running(state) - - case .stopping(var state): - self.state = ._modifying - - // Remove the load balancer if it's now shutdown. - switch connectivityState { - case .shutdown: - state.past.removeValue(forKey: id) - case .idle, .connecting, .ready, .transientFailure: - () - } - - // If that was the last load-balancer then finish the input streams so that the channel - // eventually finishes. - if state.past.isEmpty { - actions.finish = true - self.state = .stopped - } else { - self.state = .stopping(state) - } - - case .notRunning, .stopped: - () - - case ._modifying: - fatalError("Invalid state") - } - - return actions - } - - enum OnMakeStream { - /// Use the given load-balancer to make a stream. - case useLoadBalancer(LoadBalancer) - /// Join the queue and wait until a load-balancer becomes ready. - case joinQueue - /// Fail the stream request, the channel isn't in a suitable state. - case failRPC - } - - func makeStream(waitForReady: Bool) -> OnMakeStream { - let onMakeStream: OnMakeStream - - switch self.state { - case .notRunning: - onMakeStream = .joinQueue - - case .running(let state): - switch state.connectivityState { - case .idle, .connecting: - onMakeStream = .joinQueue - case .ready: - onMakeStream = .useLoadBalancer(state.current) - case .transientFailure: - onMakeStream = waitForReady ? .joinQueue : .failRPC - case .shutdown: - onMakeStream = .failRPC - } - - case .stopping, .stopped: - onMakeStream = .failRPC - - case ._modifying: - fatalError("Invalid state") - } - - return onMakeStream - } - - mutating func enqueue( - continuation: CheckedContinuation, - waitForReady: Bool, - id: QueueEntryID - ) -> Bool { - switch self.state { - case .notRunning(var state): - self.state = ._modifying - state.queue.append(continuation: continuation, waitForReady: waitForReady, id: id) - self.state = .notRunning(state) - return true - case .running(var state): - self.state = ._modifying - state.queue.append(continuation: continuation, waitForReady: waitForReady, id: id) - self.state = .running(state) - return true - case .stopping, .stopped: - return false - case ._modifying: - fatalError("Invalid state") - } - } - - mutating func dequeueContinuation( - id: QueueEntryID - ) -> CheckedContinuation? { - switch self.state { - case .notRunning(var state): - self.state = ._modifying - let continuation = state.queue.removeEntry(withID: id) - self.state = .notRunning(state) - return continuation - - case .running(var state): - self.state = ._modifying - let continuation = state.queue.removeEntry(withID: id) - self.state = .running(state) - return continuation - - case .stopping, .stopped: - return nil - - case ._modifying: - fatalError("Invalid state") - } - } - - enum OnClose { - case none - case cancelAll([RequestQueue.Continuation]) - case close(LoadBalancer, LoadBalancer?, CancellableTaskHandle?, [RequestQueue.Continuation]) - } - - mutating func close() -> OnClose { - let onClose: OnClose - - switch self.state { - case .notRunning(var state): - self.state = .stopped - onClose = .cancelAll(state.queue.removeAll()) - - case .running(var state): - let continuations = state.queue.removeAll() - onClose = .close(state.current, state.next, state.nameResolverHandle, continuations) - - state.past[state.current.id] = state.current - if let next = state.next { - state.past[next.id] = next - } - - self.state = .stopping(State.Stopping(loadBalancers: state.past)) - - case .stopping, .stopped: - onClose = .none - - case ._modifying: - fatalError("Invalid state") - } - - return onClose - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift deleted file mode 100644 index 419094aba..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancer.swift +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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. - */ - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package enum LoadBalancer: Sendable { - case roundRobin(RoundRobinLoadBalancer) - case pickFirst(PickFirstLoadBalancer) -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension LoadBalancer { - package init(_ loadBalancer: RoundRobinLoadBalancer) { - self = .roundRobin(loadBalancer) - } - - var id: LoadBalancerID { - switch self { - case .roundRobin(let loadBalancer): - return loadBalancer.id - case .pickFirst(let loadBalancer): - return loadBalancer.id - } - } - - package var events: AsyncStream { - switch self { - case .roundRobin(let loadBalancer): - return loadBalancer.events - case .pickFirst(let loadBalancer): - return loadBalancer.events - } - } - - package func run() async { - switch self { - case .roundRobin(let loadBalancer): - await loadBalancer.run() - case .pickFirst(let loadBalancer): - await loadBalancer.run() - } - } - - package func close() { - switch self { - case .roundRobin(let loadBalancer): - loadBalancer.close() - case .pickFirst(let loadBalancer): - loadBalancer.close() - } - } - - package func pickSubchannel() -> Subchannel? { - switch self { - case .roundRobin(let loadBalancer): - return loadBalancer.pickSubchannel() - case .pickFirst(let loadBalancer): - return loadBalancer.pickSubchannel() - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift deleted file mode 100644 index 439471ac6..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/LoadBalancerEvent.swift +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -/// Events emitted by load-balancers. -package enum LoadBalancerEvent: Sendable, Hashable { - /// The connectivity state of the subchannel changed. - case connectivityStateChanged(ConnectivityState) - /// The subchannel requests that the load balancer re-resolves names. - case requiresNameResolution -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift deleted file mode 100644 index 31c6d4382..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/PickFirstLoadBalancer.swift +++ /dev/null @@ -1,610 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -private import Synchronization - -/// A load-balancer which has a single subchannel. -/// -/// This load-balancer starts in an 'idle' state and begins connecting when a set of addresses is -/// provided to it with ``updateEndpoint(_:)``. Repeated calls to ``updateEndpoint(_:)`` will -/// update the subchannel gracefully: RPCs will continue to use the old subchannel until the new -/// subchannel becomes ready. -/// -/// You must call ``close()`` on the load-balancer when it's no longer required. This will move -/// it to the ``ConnectivityState/shutdown`` state: existing RPCs may continue but all subsequent -/// calls to ``makeStream(descriptor:options:)`` will fail. -/// -/// To use this load-balancer you must run it in a task: -/// -/// ```swift -/// await withDiscardingTaskGroup { group in -/// // Run the load-balancer -/// group.addTask { await pickFirst.run() } -/// -/// // Update its endpoint. -/// let endpoint = Endpoint( -/// addresses: [ -/// .ipv4(host: "127.0.0.1", port: 1001), -/// .ipv4(host: "127.0.0.1", port: 1002), -/// .ipv4(host: "127.0.0.1", port: 1003) -/// ] -/// ) -/// pickFirst.updateEndpoint(endpoint) -/// -/// // Consume state update events -/// for await event in pickFirst.events { -/// switch event { -/// case .connectivityStateChanged(.ready): -/// // ... -/// default: -/// // ... -/// } -/// } -/// } -/// ``` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package final class PickFirstLoadBalancer: Sendable { - enum Input: Sendable, Hashable { - /// Update the addresses used by the load balancer to the following endpoints. - case updateEndpoint(Endpoint) - /// Close the load balancer. - case close - } - - /// Events which can happen to the load balancer. - private let event: - ( - stream: AsyncStream, - continuation: AsyncStream.Continuation - ) - - /// Inputs which this load balancer should react to. - private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) - - /// A connector, capable of creating connections. - private let connector: any HTTP2Connector - - /// Connection backoff configuration. - private let backoff: ConnectionBackoff - - /// The default compression algorithm to use. Can be overridden on a per-call basis. - private let defaultCompression: CompressionAlgorithm - - /// The set of enabled compression algorithms. - private let enabledCompression: CompressionAlgorithmSet - - /// The state of the load-balancer. - private let state: Mutex - - /// The ID of this load balancer. - internal let id: LoadBalancerID - - package init( - connector: any HTTP2Connector, - backoff: ConnectionBackoff, - defaultCompression: CompressionAlgorithm, - enabledCompression: CompressionAlgorithmSet - ) { - self.connector = connector - self.backoff = backoff - self.defaultCompression = defaultCompression - self.enabledCompression = enabledCompression - self.id = LoadBalancerID() - self.state = Mutex(State()) - - self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) - self.input = AsyncStream.makeStream(of: Input.self) - // The load balancer starts in the idle state. - self.event.continuation.yield(.connectivityStateChanged(.idle)) - } - - /// A stream of events which can happen to the load balancer. - package var events: AsyncStream { - self.event.stream - } - - /// Runs the load balancer, returning when it has closed. - /// - /// You can monitor events which happen on the load balancer with ``events``. - package func run() async { - await withDiscardingTaskGroup { group in - for await input in self.input.stream { - switch input { - case .updateEndpoint(let endpoint): - self.handleUpdateEndpoint(endpoint, in: &group) - case .close: - self.handleCloseInput() - } - } - } - - if Task.isCancelled { - // Finish the event stream as it's unlikely to have been finished by a regular code path. - self.event.continuation.finish() - } - } - - /// Update the addresses used by the load balancer. - /// - /// This may result in new subchannels being created and some subchannels being removed. - package func updateEndpoint(_ endpoint: Endpoint) { - self.input.continuation.yield(.updateEndpoint(endpoint)) - } - - /// Close the load balancer, and all subchannels it manages. - package func close() { - self.input.continuation.yield(.close) - } - - /// Pick a ready subchannel from the load balancer. - /// - /// - Returns: A subchannel, or `nil` if there aren't any ready subchannels. - package func pickSubchannel() -> Subchannel? { - let onPickSubchannel = self.state.withLock { $0.pickSubchannel() } - switch onPickSubchannel { - case .picked(let subchannel): - return subchannel - case .notAvailable(let subchannel): - subchannel?.connect() - return nil - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension PickFirstLoadBalancer { - private func handleUpdateEndpoint(_ endpoint: Endpoint, in group: inout DiscardingTaskGroup) { - if endpoint.addresses.isEmpty { return } - - let onUpdate = self.state.withLock { state in - state.updateEndpoint(endpoint) { endpoint, id in - Subchannel( - endpoint: endpoint, - id: id, - connector: self.connector, - backoff: self.backoff, - defaultCompression: self.defaultCompression, - enabledCompression: self.enabledCompression - ) - } - } - - switch onUpdate { - case .connect(let newSubchannel, close: let oldSubchannel): - self.runSubchannel(newSubchannel, in: &group) - oldSubchannel?.shutDown() - - case .none: - () - } - } - - private func runSubchannel( - _ subchannel: Subchannel, - in group: inout DiscardingTaskGroup - ) { - // Start running it and tell it to connect. - subchannel.connect() - group.addTask { - await subchannel.run() - } - - group.addTask { - for await event in subchannel.events { - switch event { - case .connectivityStateChanged(let state): - self.handleSubchannelConnectivityStateChange(state, id: subchannel.id) - case .goingAway: - self.handleGoAway(id: subchannel.id) - case .requiresNameResolution: - self.event.continuation.yield(.requiresNameResolution) - } - } - } - } - - private func handleSubchannelConnectivityStateChange( - _ connectivityState: ConnectivityState, - id: SubchannelID - ) { - let onUpdateState = self.state.withLock { - $0.updateSubchannelConnectivityState(connectivityState, id: id) - } - - switch onUpdateState { - case .close(let subchannel): - subchannel.shutDown() - case .closeAndPublishStateChange(let subchannel, let connectivityState): - subchannel.shutDown() - self.event.continuation.yield(.connectivityStateChanged(connectivityState)) - case .publishStateChange(let connectivityState): - self.event.continuation.yield(.connectivityStateChanged(connectivityState)) - case .closed: - self.event.continuation.finish() - self.input.continuation.finish() - case .none: - () - } - } - - private func handleGoAway(id: SubchannelID) { - self.state.withLock { state in - state.receivedGoAway(id: id) - } - } - - private func handleCloseInput() { - let onClose = self.state.withLock { $0.close() } - switch onClose { - case .closeSubchannels(let subchannel1, let subchannel2): - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - subchannel1.shutDown() - subchannel2?.shutDown() - - case .closed: - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - self.event.continuation.finish() - self.input.continuation.finish() - - case .none: - () - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension PickFirstLoadBalancer { - enum State: Sendable { - case active(Active) - case closing(Closing) - case closed - - init() { - self = .active(Active()) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension PickFirstLoadBalancer.State { - struct Active: Sendable { - var endpoint: Endpoint? - var connectivityState: ConnectivityState - var current: Subchannel? - var next: Subchannel? - var parked: [SubchannelID: Subchannel] - var isCurrentGoingAway: Bool - - init() { - self.endpoint = nil - self.connectivityState = .idle - self.current = nil - self.next = nil - self.parked = [:] - self.isCurrentGoingAway = false - } - } - - struct Closing: Sendable { - var parked: [SubchannelID: Subchannel] - - init(from state: Active) { - self.parked = state.parked - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension PickFirstLoadBalancer.State.Active { - mutating func updateEndpoint( - _ endpoint: Endpoint, - makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel - ) -> PickFirstLoadBalancer.State.OnUpdateEndpoint { - if self.endpoint == endpoint { return .none } - - let onUpdateEndpoint: PickFirstLoadBalancer.State.OnUpdateEndpoint - - let id = SubchannelID() - let newSubchannel = makeSubchannel(endpoint, id) - - switch (self.current, self.next) { - case (.some(let current), .none): - if self.connectivityState == .idle { - // Current subchannel is idle and we have a new endpoint, move straight to the new - // subchannel. - self.current = newSubchannel - self.parked[current.id] = current - onUpdateEndpoint = .connect(newSubchannel, close: current) - } else { - // Current subchannel is in a non-idle state, set it as the next subchannel and promote - // it when it becomes ready. - self.next = newSubchannel - onUpdateEndpoint = .connect(newSubchannel, close: nil) - } - - case (.some, .some(let next)): - // Current and next subchannel exist. Replace the next subchannel. - self.next = newSubchannel - self.parked[next.id] = next - onUpdateEndpoint = .connect(newSubchannel, close: next) - - case (.none, .none): - self.current = newSubchannel - onUpdateEndpoint = .connect(newSubchannel, close: nil) - - case (.none, .some(let next)): - self.current = newSubchannel - self.next = nil - self.parked[next.id] = next - onUpdateEndpoint = .connect(newSubchannel, close: next) - } - - return onUpdateEndpoint - } - - mutating func updateSubchannelConnectivityState( - _ connectivityState: ConnectivityState, - id: SubchannelID - ) -> (PickFirstLoadBalancer.State.OnConnectivityStateUpdate, PickFirstLoadBalancer.State) { - let onUpdate: PickFirstLoadBalancer.State.OnConnectivityStateUpdate - - if let current = self.current, current.id == id { - if connectivityState == self.connectivityState { - onUpdate = .none - } else { - self.connectivityState = connectivityState - onUpdate = .publishStateChange(connectivityState) - } - } else if let next = self.next, next.id == id { - // if it becomes ready then promote it - switch connectivityState { - case .ready: - if self.connectivityState != connectivityState { - self.connectivityState = connectivityState - - if let current = self.current { - onUpdate = .closeAndPublishStateChange(current, connectivityState) - } else { - onUpdate = .publishStateChange(connectivityState) - } - - self.current = next - self.isCurrentGoingAway = false - } else { - // No state change to publish, just roll over. - onUpdate = self.current.map { .close($0) } ?? .none - self.current = next - self.isCurrentGoingAway = false - } - - case .idle, .connecting, .transientFailure, .shutdown: - onUpdate = .none - } - - } else { - switch connectivityState { - case .idle: - if let subchannel = self.parked[id] { - onUpdate = .close(subchannel) - } else { - onUpdate = .none - } - - case .shutdown: - self.parked.removeValue(forKey: id) - onUpdate = .none - - case .connecting, .ready, .transientFailure: - onUpdate = .none - } - } - - return (onUpdate, .active(self)) - } - - mutating func receivedGoAway(id: SubchannelID) { - if let current = self.current, current.id == id { - // When receiving a GOAWAY the subchannel will ask for an address to be re-resolved and the - // connection will eventually become idle. At this point we wait: the connection remains - // in its current state. - self.isCurrentGoingAway = true - } else if let next = self.next, next.id == id { - // The next connection is going away, park it. - // connection. - self.next = nil - self.parked[next.id] = next - } - } - - mutating func close() -> (PickFirstLoadBalancer.State.OnClose, PickFirstLoadBalancer.State) { - let onClose: PickFirstLoadBalancer.State.OnClose - let nextState: PickFirstLoadBalancer.State - - if let current = self.current { - self.parked[current.id] = current - if let next = self.next { - self.parked[next.id] = next - onClose = .closeSubchannels(current, next) - } else { - onClose = .closeSubchannels(current, nil) - } - nextState = .closing(PickFirstLoadBalancer.State.Closing(from: self)) - } else { - onClose = .closed - nextState = .closed - } - - return (onClose, nextState) - } - - func pickSubchannel() -> PickFirstLoadBalancer.State.OnPickSubchannel { - let onPick: PickFirstLoadBalancer.State.OnPickSubchannel - - if let current = self.current, !self.isCurrentGoingAway { - switch self.connectivityState { - case .idle: - onPick = .notAvailable(current) - case .ready: - onPick = .picked(current) - case .connecting, .transientFailure, .shutdown: - onPick = .notAvailable(nil) - } - } else { - onPick = .notAvailable(nil) - } - - return onPick - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension PickFirstLoadBalancer.State.Closing { - mutating func updateSubchannelConnectivityState( - _ connectivityState: ConnectivityState, - id: SubchannelID - ) -> (PickFirstLoadBalancer.State.OnConnectivityStateUpdate, PickFirstLoadBalancer.State) { - let onUpdate: PickFirstLoadBalancer.State.OnConnectivityStateUpdate - let nextState: PickFirstLoadBalancer.State - - switch connectivityState { - case .idle: - if let subchannel = self.parked[id] { - onUpdate = .close(subchannel) - } else { - onUpdate = .none - } - nextState = .closing(self) - - case .shutdown: - if self.parked.removeValue(forKey: id) != nil { - if self.parked.isEmpty { - onUpdate = .closed - nextState = .closed - } else { - onUpdate = .none - nextState = .closing(self) - } - } else { - onUpdate = .none - nextState = .closing(self) - } - - case .connecting, .ready, .transientFailure: - onUpdate = .none - nextState = .closing(self) - } - - return (onUpdate, nextState) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension PickFirstLoadBalancer.State { - enum OnUpdateEndpoint { - case connect(Subchannel, close: Subchannel?) - case none - } - - mutating func updateEndpoint( - _ endpoint: Endpoint, - makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel - ) -> OnUpdateEndpoint { - let onUpdateEndpoint: OnUpdateEndpoint - - switch self { - case .active(var state): - onUpdateEndpoint = state.updateEndpoint(endpoint) { endpoint, id in - makeSubchannel(endpoint, id) - } - self = .active(state) - - case .closing, .closed: - onUpdateEndpoint = .none - } - - return onUpdateEndpoint - } - - enum OnConnectivityStateUpdate { - case closeAndPublishStateChange(Subchannel, ConnectivityState) - case publishStateChange(ConnectivityState) - case close(Subchannel) - case closed - case none - } - - mutating func updateSubchannelConnectivityState( - _ connectivityState: ConnectivityState, - id: SubchannelID - ) -> OnConnectivityStateUpdate { - let onUpdateState: OnConnectivityStateUpdate - - switch self { - case .active(var state): - (onUpdateState, self) = state.updateSubchannelConnectivityState(connectivityState, id: id) - case .closing(var state): - (onUpdateState, self) = state.updateSubchannelConnectivityState(connectivityState, id: id) - case .closed: - onUpdateState = .none - } - - return onUpdateState - } - - mutating func receivedGoAway(id: SubchannelID) { - switch self { - case .active(var state): - state.receivedGoAway(id: id) - self = .active(state) - case .closing, .closed: - () - } - } - - enum OnClose { - case closeSubchannels(Subchannel, Subchannel?) - case closed - case none - } - - mutating func close() -> OnClose { - let onClose: OnClose - - switch self { - case .active(var state): - (onClose, self) = state.close() - case .closing, .closed: - onClose = .none - } - - return onClose - } - - enum OnPickSubchannel { - case picked(Subchannel) - case notAvailable(Subchannel?) - } - - func pickSubchannel() -> OnPickSubchannel { - switch self { - case .active(let state): - return state.pickSubchannel() - case .closing, .closed: - return .notAvailable(nil) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift deleted file mode 100644 index 5c0709175..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/RoundRobinLoadBalancer.swift +++ /dev/null @@ -1,764 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -private import NIOConcurrencyHelpers - -/// A load-balancer which maintains to a set of subchannels and uses round-robin to pick a -/// subchannel when picking a subchannel to use. -/// -/// This load-balancer starts in an 'idle' state and begins connecting when a set of addresses is -/// provided to it with ``updateAddresses(_:)``. Repeated calls to ``updateAddresses(_:)`` will -/// update the subchannels gracefully: new subchannels will be added for new addresses and existing -/// subchannels will be removed if their addresses are no longer present. -/// -/// The state of the load-balancer is aggregated across the state of its subchannels, changes in -/// the aggregate state are reported up via ``events``. -/// -/// You must call ``close()`` on the load-balancer when it's no longer required. This will move -/// it to the ``ConnectivityState/shutdown`` state: existing RPCs may continue but all subsequent -/// calls to ``makeStream(descriptor:options:)`` will fail. -/// -/// To use this load-balancer you must run it in a task: -/// -/// ```swift -/// await withDiscardingTaskGroup { group in -/// // Run the load-balancer -/// group.addTask { await roundRobin.run() } -/// -/// // Update its address list -/// let endpoints: [Endpoint] = [ -/// Endpoint(addresses: [.ipv4(host: "127.0.0.1", port: 1001)]), -/// Endpoint(addresses: [.ipv4(host: "127.0.0.1", port: 1002)]), -/// Endpoint(addresses: [.ipv4(host: "127.0.0.1", port: 1003)]) -/// ] -/// roundRobin.updateAddresses(endpoints) -/// -/// // Consume state update events -/// for await event in roundRobin.events { -/// switch event { -/// case .connectivityStateChanged(.ready): -/// // ... -/// default: -/// // ... -/// } -/// } -/// } -/// ``` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package final class RoundRobinLoadBalancer: Sendable { - enum Input: Sendable, Hashable { - /// Update the addresses used by the load balancer to the following endpoints. - case updateAddresses([Endpoint]) - /// Close the load balancer. - case close - } - - /// A key for an endpoint which identifies it uniquely, regardless of the ordering of addresses. - private struct EndpointKey: Hashable, Sendable, CustomStringConvertible { - /// Opaque data. - private let opaque: [String] - - /// The endpoint this key is for. - let endpoint: Endpoint - - init(_ endpoint: Endpoint) { - self.endpoint = endpoint - self.opaque = endpoint.addresses.map { String(describing: $0) }.sorted() - } - - var description: String { - String(describing: self.endpoint.addresses) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(self.opaque) - } - - static func == (lhs: Self, rhs: Self) -> Bool { - lhs.opaque == rhs.opaque - } - } - - /// Events which can happen to the load balancer. - private let event: - ( - stream: AsyncStream, - continuation: AsyncStream.Continuation - ) - - /// Inputs which this load balancer should react to. - private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) - - // Uses NIOLockedValueBox to workaround: https://github.com/swiftlang/swift/issues/76007 - /// The state of the load balancer. - private let state: NIOLockedValueBox - - /// A connector, capable of creating connections. - private let connector: any HTTP2Connector - - /// Connection backoff configuration. - private let backoff: ConnectionBackoff - - /// The default compression algorithm to use. Can be overridden on a per-call basis. - private let defaultCompression: CompressionAlgorithm - - /// The set of enabled compression algorithms. - private let enabledCompression: CompressionAlgorithmSet - - /// The ID of this load balancer. - internal let id: LoadBalancerID - - package init( - connector: any HTTP2Connector, - backoff: ConnectionBackoff, - defaultCompression: CompressionAlgorithm, - enabledCompression: CompressionAlgorithmSet - ) { - self.connector = connector - self.backoff = backoff - self.defaultCompression = defaultCompression - self.enabledCompression = enabledCompression - self.id = LoadBalancerID() - - self.event = AsyncStream.makeStream(of: LoadBalancerEvent.self) - self.input = AsyncStream.makeStream(of: Input.self) - self.state = NIOLockedValueBox(.active(State.Active())) - - // The load balancer starts in the idle state. - self.event.continuation.yield(.connectivityStateChanged(.idle)) - } - - /// A stream of events which can happen to the load balancer. - package var events: AsyncStream { - self.event.stream - } - - /// Runs the load balancer, returning when it has closed. - /// - /// You can monitor events which happen on the load balancer with ``events``. - package func run() async { - await withDiscardingTaskGroup { group in - for await input in self.input.stream { - switch input { - case .updateAddresses(let addresses): - self.handleUpdateAddresses(addresses, in: &group) - case .close: - self.handleCloseInput() - } - } - } - - if Task.isCancelled { - // Finish the event stream as it's unlikely to have been finished by a regular code path. - self.event.continuation.finish() - } - } - - /// Update the addresses used by the load balancer. - /// - /// This may result in new subchannels being created and some subchannels being removed. - package func updateAddresses(_ endpoints: [Endpoint]) { - self.input.continuation.yield(.updateAddresses(endpoints)) - } - - /// Close the load balancer, and all subchannels it manages. - package func close() { - self.input.continuation.yield(.close) - } - - /// Pick a ready subchannel from the load balancer. - /// - /// - Returns: A subchannel, or `nil` if there aren't any ready subchannels. - package func pickSubchannel() -> Subchannel? { - switch self.state.withLockedValue({ $0.pickSubchannel() }) { - case .picked(let subchannel): - return subchannel - - case .notAvailable(let subchannels): - // Tell the subchannels to start connecting. - for subchannel in subchannels { - subchannel.connect() - } - return nil - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension RoundRobinLoadBalancer { - /// Handles an update in endpoints. - /// - /// The load-balancer will diff the set of endpoints with the existing set of endpoints: - /// - endpoints which are new will have subchannels created for them, - /// - endpoints which existed previously but are not present in `endpoints` are closed, - /// - endpoints which existed previously and are still present in `endpoints` are untouched. - /// - /// This process is gradual: the load-balancer won't remove an old endpoint until a subchannel - /// for a corresponding new subchannel becomes ready. - /// - /// - Parameters: - /// - endpoints: Endpoints which should have subchannels. Must not be empty. - /// - group: The group which should manage and run new subchannels. - private func handleUpdateAddresses(_ endpoints: [Endpoint], in group: inout DiscardingTaskGroup) { - if endpoints.isEmpty { return } - - // Compute the keys for each endpoint. - let newEndpoints = Set(endpoints.map { EndpointKey($0) }) - - let (added, removed, newState) = self.state.withLockedValue { state in - state.updateSubchannels(newEndpoints: newEndpoints) { endpoint, id in - Subchannel( - endpoint: endpoint, - id: id, - connector: self.connector, - backoff: self.backoff, - defaultCompression: self.defaultCompression, - enabledCompression: self.enabledCompression - ) - } - } - - // Publish the new connectivity state. - if let newState = newState { - self.event.continuation.yield(.connectivityStateChanged(newState)) - } - - // Run each of the new subchannels. - for subchannel in added { - let key = EndpointKey(subchannel.endpoint) - self.runSubchannel(subchannel, forKey: key, in: &group) - } - - // Old subchannels are removed when new subchannels become ready. Excess subchannels are only - // present if there are more to remove than to add. These are the excess subchannels which - // are closed now. - for subchannel in removed { - subchannel.shutDown() - } - } - - private func runSubchannel( - _ subchannel: Subchannel, - forKey key: EndpointKey, - in group: inout DiscardingTaskGroup - ) { - // Start running it and tell it to connect. - subchannel.connect() - group.addTask { - await subchannel.run() - } - - group.addTask { - for await event in subchannel.events { - switch event { - case .connectivityStateChanged(let state): - self.handleSubchannelConnectivityStateChange(state, key: key) - case .goingAway: - self.handleSubchannelGoingAway(key: key) - case .requiresNameResolution: - self.event.continuation.yield(.requiresNameResolution) - } - } - } - } - - private func handleSubchannelConnectivityStateChange( - _ connectivityState: ConnectivityState, - key: EndpointKey - ) { - let onChange = self.state.withLockedValue { state in - state.updateSubchannelConnectivityState(connectivityState, key: key) - } - - switch onChange { - case .publishStateChange(let aggregateState): - self.event.continuation.yield(.connectivityStateChanged(aggregateState)) - - case .closeAndPublishStateChange(let subchannel, let aggregateState): - self.event.continuation.yield(.connectivityStateChanged(aggregateState)) - subchannel.shutDown() - - case .close(let subchannel): - subchannel.shutDown() - - case .closed: - // All subchannels are closed; finish the streams so the run loop exits. - self.event.continuation.finish() - self.input.continuation.finish() - - case .none: - () - } - } - - private func handleSubchannelGoingAway(key: EndpointKey) { - switch self.state.withLockedValue({ $0.parkSubchannel(withKey: key) }) { - case .closeAndUpdateState(let subchannel, let connectivityState): - subchannel.shutDown() - if let connectivityState = connectivityState { - self.event.continuation.yield(.connectivityStateChanged(connectivityState)) - } - case .none: - () - } - } - - private func handleCloseInput() { - switch self.state.withLockedValue({ $0.close() }) { - case .closeSubchannels(let subchannels): - // Publish a new shutdown state, this LB is no longer usable for new RPCs. - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - - // Close the subchannels. - for subchannel in subchannels { - subchannel.shutDown() - } - - case .closed: - // No subchannels to close. - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - self.event.continuation.finish() - self.input.continuation.finish() - - case .none: - () - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension RoundRobinLoadBalancer { - private enum State { - case active(Active) - case closing(Closing) - case closed - - struct Active { - private(set) var aggregateConnectivityState: ConnectivityState - private var picker: Picker? - - var endpoints: [Endpoint] - var subchannels: [EndpointKey: SubchannelState] - var parkedSubchannels: [EndpointKey: Subchannel] - - init() { - self.endpoints = [] - self.subchannels = [:] - self.parkedSubchannels = [:] - self.aggregateConnectivityState = .idle - self.picker = nil - } - - mutating func updateConnectivityState( - _ state: ConnectivityState, - key: EndpointKey - ) -> OnSubchannelConnectivityStateUpdate { - if let changed = self.subchannels[key]?.updateState(state) { - guard changed else { return .none } - - let subchannelToClose: Subchannel? - - switch state { - case .ready: - if let index = self.subchannels.firstIndex(where: { $0.value.markedForRemoval }) { - let (key, subchannelState) = self.subchannels.remove(at: index) - self.parkedSubchannels[key] = subchannelState.subchannel - subchannelToClose = subchannelState.subchannel - } else { - subchannelToClose = nil - } - - case .idle, .connecting, .transientFailure, .shutdown: - subchannelToClose = nil - } - - let aggregateState = self.refreshPickerAndAggregateState() - - switch (subchannelToClose, aggregateState) { - case (.some(let subchannel), .some(let state)): - return .closeAndPublishStateChange(subchannel, state) - case (.some(let subchannel), .none): - return .close(subchannel) - case (.none, .some(let state)): - return .publishStateChange(state) - case (.none, .none): - return .none - } - } else { - switch state { - case .idle: - // The subchannel can be parked before it's shutdown. If there are no active RPCs then - // it will enter the idle state instead. If that happens, close it. - if let parked = self.parkedSubchannels[key] { - return .close(parked) - } else { - return .none - } - case .shutdown: - self.parkedSubchannels.removeValue(forKey: key) - case .connecting, .ready, .transientFailure: - () - } - - return .none - } - } - - mutating func refreshPickerAndAggregateState() -> ConnectivityState? { - let ready = self.subchannels.values.compactMap { $0.state == .ready ? $0.subchannel : nil } - self.picker = Picker(subchannels: ready) - - let aggregate = ConnectivityState.aggregate(self.subchannels.values.map { $0.state }) - if aggregate == self.aggregateConnectivityState { - return nil - } else { - self.aggregateConnectivityState = aggregate - return aggregate - } - } - - mutating func pick() -> Subchannel? { - self.picker?.pick() - } - - mutating func markForRemoval( - _ keys: some Sequence, - numberToRemoveNow: Int - ) -> [Subchannel] { - var numberToRemoveNow = numberToRemoveNow - var keyIterator = keys.makeIterator() - var subchannelsToClose = [Subchannel]() - - while numberToRemoveNow > 0, let key = keyIterator.next() { - if let subchannelState = self.subchannels.removeValue(forKey: key) { - numberToRemoveNow -= 1 - self.parkedSubchannels[key] = subchannelState.subchannel - subchannelsToClose.append(subchannelState.subchannel) - } - } - - while let key = keyIterator.next() { - self.subchannels[key]?.markForRemoval() - } - - return subchannelsToClose - } - - mutating func registerSubchannels( - withKeys keys: some Sequence, - _ makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel - ) -> [Subchannel] { - var subchannels = [Subchannel]() - - for key in keys { - let subchannel = makeSubchannel(key.endpoint, SubchannelID()) - subchannels.append(subchannel) - self.subchannels[key] = SubchannelState(subchannel: subchannel) - } - - return subchannels - } - } - - struct Closing { - enum Reason: Sendable, Hashable { - case goAway - case user - } - - var reason: Reason - var parkedSubchannels: [EndpointKey: Subchannel] - - mutating func updateConnectivityState( - _ state: ConnectivityState, - key: EndpointKey - ) -> (OnSubchannelConnectivityStateUpdate, RoundRobinLoadBalancer.State) { - let result: OnSubchannelConnectivityStateUpdate - let nextState: RoundRobinLoadBalancer.State - - switch state { - case .idle: - if let parked = self.parkedSubchannels[key] { - result = .close(parked) - } else { - result = .none - } - nextState = .closing(self) - - case .shutdown: - self.parkedSubchannels.removeValue(forKey: key) - if self.parkedSubchannels.isEmpty { - nextState = .closed - result = .closed - } else { - nextState = .closing(self) - result = .none - } - - case .connecting, .ready, .transientFailure: - result = .none - nextState = .closing(self) - } - - return (result, nextState) - } - } - - struct SubchannelState { - var subchannel: Subchannel - var state: ConnectivityState - var markedForRemoval: Bool - - init(subchannel: Subchannel) { - self.subchannel = subchannel - self.state = .idle - self.markedForRemoval = false - } - - mutating func updateState(_ newState: ConnectivityState) -> Bool { - // The transition from transient failure to connecting is ignored. - // - // See: https://github.com/grpc/grpc/blob/master/doc/load-balancing.md - if self.state == .transientFailure, newState == .connecting { - return false - } - - let oldState = self.state - self.state = newState - return oldState != newState - } - - mutating func markForRemoval() { - self.markedForRemoval = true - } - } - - struct Picker { - private var subchannels: [Subchannel] - private var index: Int - - init?(subchannels: [Subchannel]) { - if subchannels.isEmpty { return nil } - - self.subchannels = subchannels - self.index = (0 ..< subchannels.count).randomElement()! - } - - mutating func pick() -> Subchannel { - defer { - self.index = (self.index + 1) % self.subchannels.count - } - return self.subchannels[self.index] - } - } - - mutating func updateSubchannels( - newEndpoints: Set, - makeSubchannel: (_ endpoint: Endpoint, _ id: SubchannelID) -> Subchannel - ) -> (run: [Subchannel], close: [Subchannel], newState: ConnectivityState?) { - switch self { - case .active(var state): - let existingEndpoints = Set(state.subchannels.keys) - let keysToAdd = newEndpoints.subtracting(existingEndpoints) - let keysToRemove = existingEndpoints.subtracting(newEndpoints) - - if keysToRemove.isEmpty && keysToAdd.isEmpty { - // Nothing to do. - return (run: [], close: [], newState: nil) - } - - // The load balancer should keep subchannels to remove in service until new subchannels - // can replace each of them so that requests can continue to be served. - // - // If there are more keys to remove than to add, remove some now. - let numberToRemoveNow = max(keysToRemove.count - keysToAdd.count, 0) - - let removed = state.markForRemoval(keysToRemove, numberToRemoveNow: numberToRemoveNow) - let added = state.registerSubchannels(withKeys: keysToAdd, makeSubchannel) - - let newState = state.refreshPickerAndAggregateState() - self = .active(state) - return (run: added, close: removed, newState: newState) - - case .closing, .closed: - // Nothing to do. - return (run: [], close: [], newState: nil) - } - - } - - enum OnParkChannel { - case closeAndUpdateState(Subchannel, ConnectivityState?) - case none - } - - mutating func parkSubchannel(withKey key: EndpointKey) -> OnParkChannel { - switch self { - case .active(var state): - guard let subchannelState = state.subchannels.removeValue(forKey: key) else { - return .none - } - - // Parking the subchannel may invalidate the picker and the aggregate state, refresh both. - state.parkedSubchannels[key] = subchannelState.subchannel - let newState = state.refreshPickerAndAggregateState() - self = .active(state) - return .closeAndUpdateState(subchannelState.subchannel, newState) - - case .closing, .closed: - return .none - } - } - - mutating func registerSubchannels( - withKeys keys: some Sequence, - _ makeSubchannel: (Endpoint) -> Subchannel - ) -> [Subchannel] { - switch self { - case .active(var state): - var subchannels = [Subchannel]() - - for key in keys { - let subchannel = makeSubchannel(key.endpoint) - subchannels.append(subchannel) - state.subchannels[key] = SubchannelState(subchannel: subchannel) - } - - self = .active(state) - return subchannels - - case .closing, .closed: - return [] - } - } - - enum OnSubchannelConnectivityStateUpdate { - case closeAndPublishStateChange(Subchannel, ConnectivityState) - case publishStateChange(ConnectivityState) - case close(Subchannel) - case closed - case none - } - - mutating func updateSubchannelConnectivityState( - _ connectivityState: ConnectivityState, - key: EndpointKey - ) -> OnSubchannelConnectivityStateUpdate { - switch self { - case .active(var state): - let result = state.updateConnectivityState(connectivityState, key: key) - self = .active(state) - return result - - case .closing(var state): - let (result, nextState) = state.updateConnectivityState(connectivityState, key: key) - self = nextState - return result - - case .closed: - return .none - } - } - - enum OnClose { - case closeSubchannels([Subchannel]) - case closed - case none - } - - mutating func close() -> OnClose { - switch self { - case .active(var active): - var subchannelsToClose = [Subchannel]() - - for (id, subchannelState) in active.subchannels { - subchannelsToClose.append(subchannelState.subchannel) - active.parkedSubchannels[id] = subchannelState.subchannel - } - - if subchannelsToClose.isEmpty { - self = .closed - return .closed - } else { - self = .closing(Closing(reason: .user, parkedSubchannels: active.parkedSubchannels)) - return .closeSubchannels(subchannelsToClose) - } - - case .closing, .closed: - return .none - } - } - - enum OnPickSubchannel { - case picked(Subchannel) - case notAvailable([Subchannel]) - } - - mutating func pickSubchannel() -> OnPickSubchannel { - let onMakeStream: OnPickSubchannel - - switch self { - case .active(var active): - if let subchannel = active.pick() { - onMakeStream = .picked(subchannel) - } else { - switch active.aggregateConnectivityState { - case .idle: - onMakeStream = .notAvailable(active.subchannels.values.map { $0.subchannel }) - case .connecting, .ready, .transientFailure, .shutdown: - onMakeStream = .notAvailable([]) - } - } - self = .active(active) - - case .closing, .closed: - onMakeStream = .notAvailable([]) - } - - return onMakeStream - } - } -} - -extension ConnectivityState { - static func aggregate(_ states: some Collection) -> ConnectivityState { - // See https://github.com/grpc/grpc/blob/master/doc/load-balancing.md - - // If any one subchannel is in READY state, the channel's state is READY. - if states.contains(where: { $0 == .ready }) { - return .ready - } - - // Otherwise, if there is any subchannel in state CONNECTING, the channel's state is CONNECTING. - if states.contains(where: { $0 == .connecting }) { - return .connecting - } - - // Otherwise, if there is any subchannel in state IDLE, the channel's state is IDLE. - if states.contains(where: { $0 == .idle }) { - return .idle - } - - // Otherwise, if all subchannels are in state TRANSIENT_FAILURE, the channel's state - // is TRANSIENT_FAILURE. - if states.allSatisfy({ $0 == .transientFailure }) { - return .transientFailure - } - - return .shutdown - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift b/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift deleted file mode 100644 index 64f53e305..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/LoadBalancers/Subchannel.swift +++ /dev/null @@ -1,680 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -private import Synchronization - -/// A ``Subchannel`` provides communication to a single ``Endpoint``. -/// -/// Each ``Subchannel`` starts in an 'idle' state where it isn't attempting to connect to an -/// endpoint. You can tell it to start connecting by calling ``connect()`` and you can listen -/// to connectivity state changes by consuming the ``events`` sequence. -/// -/// You must call ``shutDown()`` on the ``Subchannel`` when it's no longer required. This will move -/// it to the ``ConnectivityState/shutdown`` state: existing RPCs may continue but all subsequent -/// calls to ``makeStream(descriptor:options:)`` will fail. -/// -/// To use the ``Subchannel`` you must run it in a task: -/// -/// ```swift -/// await withTaskGroup(of: Void.self) { group in -/// group.addTask { await subchannel.run() } -/// -/// for await event in subchannel.events { -/// switch event { -/// case .connectivityStateChanged(.ready): -/// // ... -/// default: -/// // ... -/// } -/// } -/// } -/// ``` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package final class Subchannel: Sendable { - package enum Event: Sendable, Hashable { - /// The connection received a GOAWAY and will close soon. No new streams - /// should be opened on this connection. - case goingAway - /// The connectivity state of the subchannel changed. - case connectivityStateChanged(ConnectivityState) - /// The subchannel requests that the load balancer re-resolves names. - case requiresNameResolution - } - - private enum Input: Sendable { - /// Request that the connection starts connecting. - case connect - /// A backoff period has ended. - case backedOff - /// Shuts down the connection, if possible. - case shutDown - /// Handle the event from the underlying connection object. - case handleConnectionEvent(Connection.Event) - } - - /// Events which can happen to the subchannel. - private let event: (stream: AsyncStream, continuation: AsyncStream.Continuation) - - /// Inputs which this subchannel should react to. - private let input: (stream: AsyncStream, continuation: AsyncStream.Continuation) - - /// The state of the subchannel. - private let state: Mutex - - /// The endpoint this subchannel is targeting. - let endpoint: Endpoint - - /// The ID of the subchannel. - package let id: SubchannelID - - /// A factory for connections. - private let connector: any HTTP2Connector - - /// The connection backoff configuration used by the subchannel when establishing a connection. - private let backoff: ConnectionBackoff - - /// The default compression algorithm used for requests. - private let defaultCompression: CompressionAlgorithm - - /// The set of enabled compression algorithms. - private let enabledCompression: CompressionAlgorithmSet - - package init( - endpoint: Endpoint, - id: SubchannelID, - connector: any HTTP2Connector, - backoff: ConnectionBackoff, - defaultCompression: CompressionAlgorithm, - enabledCompression: CompressionAlgorithmSet - ) { - assert(!endpoint.addresses.isEmpty, "endpoint.addresses mustn't be empty") - - self.state = Mutex(.notConnected(.initial)) - self.endpoint = endpoint - self.id = id - self.connector = connector - self.backoff = backoff - self.defaultCompression = defaultCompression - self.enabledCompression = enabledCompression - self.event = AsyncStream.makeStream(of: Event.self) - self.input = AsyncStream.makeStream(of: Input.self) - // Subchannel always starts in the idle state. - self.event.continuation.yield(.connectivityStateChanged(.idle)) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Subchannel { - /// A stream of events which can happen to the subchannel. - package var events: AsyncStream { - self.event.stream - } - - /// Run the subchannel. - /// - /// Running the subchannel will attempt to maintain a connection to a remote endpoint. At times - /// the connection may be idle but it will reconnect on-demand when a stream is requested. If - /// connect attempts fail then the subchannel may progressively spend longer in a transient - /// failure state. - /// - /// Events and state changes can be observed via the ``events`` stream. - package func run() async { - await withDiscardingTaskGroup { group in - for await input in self.input.stream { - switch input { - case .connect: - self.handleConnectInput(in: &group) - case .backedOff: - self.handleBackedOffInput(in: &group) - case .shutDown: - self.handleShutDownInput(in: &group) - case .handleConnectionEvent(let event): - self.handleConnectionEvent(event, in: &group) - } - } - } - - // Once the task group is done, the event stream must also be finished. In normal operation - // this is handled via other paths. For cancellation it must be finished explicitly. - if Task.isCancelled { - self.event.continuation.finish() - } - } - - /// Initiate a connection attempt, if possible. - package func connect() { - self.input.continuation.yield(.connect) - } - - /// Initiates graceful shutdown, if possible. - package func shutDown() { - self.input.continuation.yield(.shutDown) - } - - /// Make a stream using the subchannel if it's ready. - /// - /// - Parameter descriptor: A descriptor of the method to create a stream for. - /// - Returns: The open stream. - package func makeStream( - descriptor: MethodDescriptor, - options: CallOptions - ) async throws -> Connection.Stream { - let connection: Connection? = self.state.withLock { state in - switch state { - case .notConnected, .connecting, .goingAway, .shuttingDown, .shutDown: - return nil - case .connected(let connected): - return connected.connection - } - } - - guard let connection = connection else { - throw RPCError(code: .unavailable, message: "subchannel isn't ready") - } - - return try await connection.makeStream(descriptor: descriptor, options: options) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Subchannel { - private func handleConnectInput(in group: inout DiscardingTaskGroup) { - let connection = self.state.withLock { state in - state.makeConnection( - to: self.endpoint.addresses, - using: self.connector, - backoff: self.backoff, - defaultCompression: self.defaultCompression, - enabledCompression: self.enabledCompression - ) - } - - guard let connection = connection else { - // Not in a state to start a connection. - return - } - - // About to start connecting a new connection; emit a state change event. - self.event.continuation.yield(.connectivityStateChanged(.connecting)) - self.runConnection(connection, in: &group) - } - - private func handleBackedOffInput(in group: inout DiscardingTaskGroup) { - switch self.state.withLock({ $0.backedOff() }) { - case .none: - () - - case .finish: - self.event.continuation.finish() - self.input.continuation.finish() - - case .connect(let connection): - // About to start connecting, emit a state change event. - self.event.continuation.yield(.connectivityStateChanged(.connecting)) - self.runConnection(connection, in: &group) - } - } - - private func handleShutDownInput(in group: inout DiscardingTaskGroup) { - switch self.state.withLock({ $0.shutDown() }) { - case .none: - () - - case .emitShutdown: - // Connection closed because the load balancer asked it to, so notify the load balancer. - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - - case .emitShutdownAndClose(let connection): - // Connection closed because the load balancer asked it to, so notify the load balancer. - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - connection.close() - - case .emitShutdownAndFinish: - // Connection closed because the load balancer asked it to, so notify the load balancer. - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - // At this point there are no more events: close the event streams. - self.event.continuation.finish() - self.input.continuation.finish() - } - } - - private func handleConnectionEvent( - _ event: Connection.Event, - in group: inout DiscardingTaskGroup - ) { - switch event { - case .connectSucceeded: - self.handleConnectSucceededEvent() - case .connectFailed: - self.handleConnectFailedEvent(in: &group) - case .goingAway: - self.handleGoingAwayEvent() - case .closed(let reason): - self.handleConnectionClosedEvent(reason, in: &group) - } - } - - private func handleConnectSucceededEvent() { - switch self.state.withLock({ $0.connectSucceeded() }) { - case .updateStateToReady: - // Emit a connectivity state change: the load balancer can now use this subchannel. - self.event.continuation.yield(.connectivityStateChanged(.ready)) - - case .finishAndClose(let connection): - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - self.event.continuation.finish() - self.input.continuation.finish() - connection.close() - - case .none: - () - } - } - - private func handleConnectFailedEvent(in group: inout DiscardingTaskGroup) { - let onConnectFailed = self.state.withLock { $0.connectFailed(connector: self.connector) } - switch onConnectFailed { - case .connect(let connection): - // Try the next address. - self.runConnection(connection, in: &group) - - case .backoff(let duration): - // All addresses have been tried, backoff for some time. - self.event.continuation.yield(.connectivityStateChanged(.transientFailure)) - group.addTask { - do { - try await Task.sleep(for: duration) - self.input.continuation.yield(.backedOff) - } catch { - // Can only be a cancellation error, swallow it. No further connection attempts will be - // made. - () - } - } - - case .finish: - self.event.continuation.finish() - self.input.continuation.finish() - - case .none: - () - } - } - - private func handleGoingAwayEvent() { - let isGoingAway = self.state.withLock { $0.goingAway() } - guard isGoingAway else { return } - - // Notify the load balancer that the subchannel is going away to stop it from being used. - self.event.continuation.yield(.goingAway) - // A GOAWAY also means that the load balancer should re-resolve as the available servers - // may have changed. - self.event.continuation.yield(.requiresNameResolution) - } - - private func handleConnectionClosedEvent( - _ reason: Connection.CloseReason, - in group: inout DiscardingTaskGroup - ) { - switch self.state.withLock({ $0.closed(reason: reason) }) { - case .nothing: - () - - case .emitIdle: - self.event.continuation.yield(.connectivityStateChanged(.idle)) - - case .emitTransientFailureAndReconnect: - // Unclean closes trigger a transient failure state change and a name resolution. - self.event.continuation.yield(.connectivityStateChanged(.transientFailure)) - self.event.continuation.yield(.requiresNameResolution) - // Attempt to reconnect. - self.handleConnectInput(in: &group) - - case .finish(let emitShutdown): - if emitShutdown { - self.event.continuation.yield(.connectivityStateChanged(.shutdown)) - } - - // At this point there are no more events: close the event streams. - self.event.continuation.finish() - self.input.continuation.finish() - } - } - - private func runConnection(_ connection: Connection, in group: inout DiscardingTaskGroup) { - group.addTask { - await connection.run() - } - - group.addTask { - for await event in connection.events { - self.input.continuation.yield(.handleConnectionEvent(event)) - } - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Subchannel { - /// ┌───────────────┐ - /// ┌───────▶│ NOT CONNECTED │───────────shutDown─────────────┐ - /// │ └───────────────┘ │ - /// │ │ │ - /// │ connFailed──┤connect │ - /// │ /backedOff │ │ - /// │ │ ▼ │ - /// │ │ ┌───────────────┐ │ - /// │ └──│ CONNECTING │──────┐ │ - /// │ └───────────────┘ │ │ - /// │ │ │ │ - /// closed connSucceeded │ │ - /// │ │ │ │ - /// │ ▼ │ │ - /// │ ┌───────────────┐ │ ┌───────────────┐ │ - /// │ │ CONNECTED │──shutDown──▶│ SHUTTING DOWN │ │ - /// │ └───────────────┘ │ └───────────────┘ │ - /// │ │ │ │ │ - /// │ goAway │ closed │ - /// │ │ │ │ │ - /// │ ▼ │ ▼ │ - /// │ ┌───────────────┐ │ ┌───────────────┐ │ - /// └────────│ GOING AWAY │──────┘ │ SHUT DOWN │◀─┘ - /// └───────────────┘ └───────────────┘ - private enum State { - /// Not connected and not actively connecting. - case notConnected(NotConnected) - /// A connection attempt is in-progress. - case connecting(Connecting) - /// A connection has been established. - case connected(Connected) - /// The subchannel is going away. It may return to the 'notConnected' state when the underlying - /// connection has closed. - case goingAway(GoingAway) - /// The subchannel is shutting down, it will enter the 'shutDown' state when closed, it may not - /// enter any other state. - case shuttingDown(ShuttingDown) - /// The subchannel is shutdown, this is a terminal state. - case shutDown(ShutDown) - - struct NotConnected { - private init() {} - static let initial = NotConnected() - init(from state: Connected) {} - init(from state: GoingAway) {} - } - - struct Connecting { - var connection: Connection - let addresses: [SocketAddress] - var addressIterator: Array.Iterator - var backoff: ConnectionBackoff.Iterator - } - - struct Connected { - var connection: Connection - - init(from state: Connecting) { - self.connection = state.connection - } - } - - struct GoingAway { - var connection: Connection - - init(from state: Connecting) { - self.connection = state.connection - } - - init(from state: Connected) { - self.connection = state.connection - } - } - - struct ShuttingDown { - var connection: Connection - - init(from state: Connecting) { - self.connection = state.connection - } - - init(from state: Connected) { - self.connection = state.connection - } - - init(from state: GoingAway) { - self.connection = state.connection - } - } - - struct ShutDown { - init(from state: ShuttingDown) {} - init(from state: GoingAway) {} - init(from state: NotConnected) {} - } - - mutating func makeConnection( - to addresses: [SocketAddress], - using connector: any HTTP2Connector, - backoff: ConnectionBackoff, - defaultCompression: CompressionAlgorithm, - enabledCompression: CompressionAlgorithmSet - ) -> Connection? { - switch self { - case .notConnected: - var iterator = addresses.makeIterator() - let address = iterator.next()! // addresses must not be empty. - - let connection = Connection( - address: address, - http2Connector: connector, - defaultCompression: defaultCompression, - enabledCompression: enabledCompression - ) - - let connecting = State.Connecting( - connection: connection, - addresses: addresses, - addressIterator: iterator, - backoff: backoff.makeIterator() - ) - - self = .connecting(connecting) - return connection - - case .connecting, .connected, .goingAway, .shuttingDown, .shutDown: - return nil - } - } - - enum OnClose { - case none - case emitShutdownAndFinish - case emitShutdownAndClose(Connection) - case emitShutdown - } - - mutating func shutDown() -> OnClose { - let onShutDown: OnClose - - switch self { - case .notConnected(let state): - self = .shutDown(ShutDown(from: state)) - onShutDown = .emitShutdownAndFinish - - case .connecting(let state): - // Only emit the shutdown; there's no connection to close yet. - self = .shuttingDown(ShuttingDown(from: state)) - onShutDown = .emitShutdown - - case .connected(let state): - self = .shuttingDown(ShuttingDown(from: state)) - onShutDown = .emitShutdownAndClose(state.connection) - - case .goingAway(let state): - self = .shuttingDown(ShuttingDown(from: state)) - onShutDown = .emitShutdown - - case .shuttingDown, .shutDown: - onShutDown = .none - } - - return onShutDown - } - - enum OnConnectSucceeded { - case updateStateToReady - case finishAndClose(Connection) - case none - } - - mutating func connectSucceeded() -> OnConnectSucceeded { - switch self { - case .connecting(let state): - self = .connected(Connected(from: state)) - return .updateStateToReady - - case .shuttingDown(let state): - self = .shutDown(ShutDown(from: state)) - return .finishAndClose(state.connection) - - case .notConnected, .connected, .goingAway, .shutDown: - return .none - } - } - - enum OnConnectFailed { - case none - case finish - case connect(Connection) - case backoff(Duration) - } - - mutating func connectFailed(connector: any HTTP2Connector) -> OnConnectFailed { - let onConnectFailed: OnConnectFailed - - switch self { - case .connecting(var state): - if let address = state.addressIterator.next() { - state.connection = Connection( - address: address, - http2Connector: connector, - defaultCompression: .none, - enabledCompression: .all - ) - self = .connecting(state) - onConnectFailed = .connect(state.connection) - } else { - state.addressIterator = state.addresses.makeIterator() - let address = state.addressIterator.next()! - state.connection = Connection( - address: address, - http2Connector: connector, - defaultCompression: .none, - enabledCompression: .all - ) - let backoff = state.backoff.next() - self = .connecting(state) - onConnectFailed = .backoff(backoff) - } - - case .shuttingDown(let state): - self = .shutDown(ShutDown(from: state)) - onConnectFailed = .finish - - case .notConnected, .connected, .goingAway, .shutDown: - onConnectFailed = .none - } - - return onConnectFailed - } - - enum OnBackedOff { - case none - case connect(Connection) - case finish - } - - mutating func backedOff() -> OnBackedOff { - switch self { - case .connecting(let state): - self = .connecting(state) - return .connect(state.connection) - - case .shuttingDown(let state): - self = .shutDown(ShutDown(from: state)) - return .finish - - case .notConnected, .connected, .goingAway, .shutDown: - return .none - } - } - - mutating func goingAway() -> Bool { - switch self { - case .connected(let state): - self = .goingAway(GoingAway(from: state)) - return true - case .notConnected, .goingAway, .connecting, .shuttingDown, .shutDown: - return false - } - } - - enum OnClosed { - case nothing - case emitIdle - case emitTransientFailureAndReconnect - case finish(emitShutdown: Bool) - } - - mutating func closed(reason: Connection.CloseReason) -> OnClosed { - let onClosed: OnClosed - - switch self { - case .connected(let state): - switch reason { - case .idleTimeout, .remote, .error(_, wasIdle: true): - self = .notConnected(NotConnected(from: state)) - onClosed = .emitIdle - - case .keepaliveTimeout, .error(_, wasIdle: false): - self = .notConnected(NotConnected(from: state)) - onClosed = .emitTransientFailureAndReconnect - - case .initiatedLocally: - // Should be in the 'shuttingDown' state. - assertionFailure("Invalid state") - let shuttingDown = State.ShuttingDown(from: state) - self = .shutDown(ShutDown(from: shuttingDown)) - onClosed = .finish(emitShutdown: true) - } - - case .goingAway(let state): - self = .notConnected(NotConnected(from: state)) - onClosed = .emitIdle - - case .shuttingDown(let state): - self = .shutDown(ShutDown(from: state)) - return .finish(emitShutdown: false) - - case .notConnected, .connecting, .shutDown: - onClosed = .nothing - } - - return onClosed - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift b/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift deleted file mode 100644 index b935e4a05..000000000 --- a/Sources/GRPCHTTP2Core/Client/Connection/RequestQueue.swift +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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. - */ - -internal import DequeModule - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct RequestQueue { - typealias Continuation = CheckedContinuation - - private struct QueueEntry { - var continuation: Continuation - var waitForReady: Bool - } - - /// IDs of entries in the order they should be processed. - /// - /// If an ID is popped from the queue but isn't present in `entriesByID` then it must've - /// been removed directly by its ID, this is fine. - private var ids: Deque - - /// Entries keyed by their ID. - private var entriesByID: [QueueEntryID: QueueEntry] - - init() { - self.ids = [] - self.entriesByID = [:] - } - - /// Remove the first continuation from the queue. - mutating func popFirst() -> Continuation? { - while let id = self.ids.popFirst() { - if let waiter = self.entriesByID.removeValue(forKey: id) { - return waiter.continuation - } - } - - assert(self.entriesByID.isEmpty) - return nil - } - - /// Append a continuation to the queue. - /// - /// - Parameters: - /// - continuation: The continuation to append. - /// - waitForReady: Whether the request associated with the continuation is willing to wait for - /// the channel to become ready. - /// - id: The unique ID of the queue entry. - mutating func append(continuation: Continuation, waitForReady: Bool, id: QueueEntryID) { - let entry = QueueEntry(continuation: continuation, waitForReady: waitForReady) - let removed = self.entriesByID.updateValue(entry, forKey: id) - assert(removed == nil, "id '\(id)' reused") - self.ids.append(id) - } - - /// Remove the waiter with the given ID, if it exists. - mutating func removeEntry(withID id: QueueEntryID) -> Continuation? { - let waiter = self.entriesByID.removeValue(forKey: id) - return waiter?.continuation - } - - /// Remove all waiters, returning their continuations. - mutating func removeAll() -> [Continuation] { - let continuations = Array(self.entriesByID.values.map { $0.continuation }) - self.ids.removeAll(keepingCapacity: true) - self.entriesByID.removeAll(keepingCapacity: true) - return continuations - } - - /// Remove all entries which were appended to the queue with a value of `false` - /// for `waitForReady`. - mutating func removeFastFailingEntries() -> [Continuation] { - var removed = [Continuation]() - var remainingIDs = Deque() - var remainingEntriesByID = [QueueEntryID: QueueEntry]() - - while let id = self.ids.popFirst() { - guard let waiter = self.entriesByID.removeValue(forKey: id) else { continue } - - if waiter.waitForReady { - remainingEntriesByID[id] = waiter - remainingIDs.append(id) - } else { - removed.append(waiter.continuation) - } - } - - assert(self.entriesByID.isEmpty) - self.entriesByID = remainingEntriesByID - self.ids = remainingIDs - return removed - } -} diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift deleted file mode 100644 index e4562e8de..000000000 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ /dev/null @@ -1,302 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore -internal import NIOCore -internal import NIOHTTP2 - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCClientStreamHandler: ChannelDuplexHandler { - typealias InboundIn = HTTP2Frame.FramePayload - typealias InboundOut = RPCResponsePart - - typealias OutboundIn = RPCRequestPart - typealias OutboundOut = HTTP2Frame.FramePayload - - private var stateMachine: GRPCStreamStateMachine - - private var isReading = false - private var flushPending = false - - init( - methodDescriptor: MethodDescriptor, - scheme: Scheme, - outboundEncoding: CompressionAlgorithm, - acceptedEncodings: CompressionAlgorithmSet, - maxPayloadSize: Int, - skipStateMachineAssertions: Bool = false - ) { - self.stateMachine = .init( - configuration: .client( - .init( - methodDescriptor: methodDescriptor, - scheme: scheme, - outboundEncoding: outboundEncoding, - acceptedEncodings: acceptedEncodings - ) - ), - maxPayloadSize: maxPayloadSize, - skipAssertions: skipStateMachineAssertions - ) - } -} - -// - MARK: ChannelInboundHandler - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCClientStreamHandler { - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - self.isReading = true - let frame = self.unwrapInboundIn(data) - switch frame { - case .data(let frameData): - let endStream = frameData.endStream - switch frameData.data { - case .byteBuffer(let buffer): - do { - switch try self.stateMachine.receive(buffer: buffer, endStream: endStream) { - case .endRPCAndForwardErrorStatus_clientOnly(let status): - context.fireChannelRead(self.wrapInboundOut(.status(status, [:]))) - context.close(promise: nil) - - case .forwardErrorAndClose_serverOnly: - assertionFailure("Unexpected client action") - - case .readInbound: - loop: while true { - switch self.stateMachine.nextInboundMessage() { - case .receiveMessage(let message): - context.fireChannelRead(self.wrapInboundOut(.message(message))) - case .awaitMoreMessages: - break loop - case .noMoreMessages: - // This could only happen if the server sends a data frame with EOS - // set, without sending status and trailers. - // If this happens, we should have forwarded an error status above - // so we should never reach this point. Do nothing. - break loop - } - } - - case .doNothing: - () - } - } catch let invalidState { - let error = RPCError(invalidState) - context.fireErrorCaught(error) - } - - case .fileRegion: - preconditionFailure("Unexpected IOData.fileRegion") - } - - case .headers(let headers): - do { - let action = try self.stateMachine.receive( - headers: headers.headers, - endStream: headers.endStream - ) - switch action { - case .receivedMetadata(let metadata, _): - context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) - - case .receivedStatusAndMetadata_clientOnly(let status, let metadata): - context.fireChannelRead(self.wrapInboundOut(.status(status, metadata))) - context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - - case .rejectRPC_serverOnly, .protocolViolation_serverOnly: - assertionFailure("Unexpected action '\(action)'") - - case .doNothing: - () - } - } catch let invalidState { - let error = RPCError(invalidState) - context.fireErrorCaught(error) - } - - case .rstStream: - self.handleUnexpectedInboundClose(context: context, reason: .streamReset) - - case .ping, .goAway, .priority, .settings, .pushPromise, .windowUpdate, - .alternativeService, .origin: - () - } - } - - func channelReadComplete(context: ChannelHandlerContext) { - self.isReading = false - if self.flushPending { - self.flushPending = false - self.flush(context: context) - } - context.fireChannelReadComplete() - } - - func handlerRemoved(context: ChannelHandlerContext) { - self.stateMachine.tearDown() - } - - func channelInactive(context: ChannelHandlerContext) { - self.handleUnexpectedInboundClose(context: context, reason: .channelInactive) - context.fireChannelInactive() - } - - func errorCaught(context: ChannelHandlerContext, error: any Error) { - self.handleUnexpectedInboundClose(context: context, reason: .errorThrown(error)) - } - - private func handleUnexpectedInboundClose( - context: ChannelHandlerContext, - reason: GRPCStreamStateMachine.UnexpectedInboundCloseReason - ) { - switch self.stateMachine.unexpectedInboundClose(reason: reason) { - case .forwardStatus_clientOnly(let status): - context.fireChannelRead(self.wrapInboundOut(.status(status, [:]))) - case .doNothing: - () - case .fireError_serverOnly: - assertionFailure("`fireError` should only happen on the server side, never on the client.") - } - } -} - -// - MARK: ChannelOutboundHandler - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCClientStreamHandler { - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - switch self.unwrapOutboundIn(data) { - case .metadata(let metadata): - do { - self.flushPending = true - let headers = try self.stateMachine.send(metadata: metadata) - context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: promise) - } catch let invalidState { - let error = RPCError(invalidState) - promise?.fail(error) - context.fireErrorCaught(error) - } - - case .message(let message): - do { - try self.stateMachine.send(message: message, promise: promise) - } catch let invalidState { - let error = RPCError(invalidState) - promise?.fail(error) - context.fireErrorCaught(error) - } - } - } - - func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise?) { - switch mode { - case .input: - context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - promise?.succeed() - - case .output: - // We flush all pending messages and update the internal state machine's - // state, but we don't close the outbound end of the channel, because - // forwarding the close in this case would cause the HTTP2 stream handler - // to close the whole channel (as the mode is ignored in its implementation). - do { - try self.stateMachine.closeOutbound() - // Force a flush by calling _flush instead of flush - // (otherwise, we'd skip flushing if we're in a read loop) - self._flush(context: context) - promise?.succeed() - } catch let invalidState { - let error = RPCError(invalidState) - promise?.fail(error) - context.fireErrorCaught(error) - } - - case .all: - // Since we're closing the whole channel here, we *do* forward the close - // down the pipeline. - do { - try self.stateMachine.closeOutbound() - // Force a flush by calling _flush - // (otherwise, we'd skip flushing if we're in a read loop) - self._flush(context: context) - context.close(mode: mode, promise: promise) - } catch let invalidState { - let error = RPCError(invalidState) - promise?.fail(error) - context.fireErrorCaught(error) - } - } - } - - func flush(context: ChannelHandlerContext) { - if self.isReading { - // We don't want to flush yet if we're still in a read loop. - self.flushPending = true - return - } - - self._flush(context: context) - } - - private func _flush(context: ChannelHandlerContext) { - do { - loop: while true { - switch try self.stateMachine.nextOutboundFrame() { - case .sendFrame(let byteBuffer, let promise): - self.flushPending = true - context.write( - self.wrapOutboundOut(.data(.init(data: .byteBuffer(byteBuffer)))), - promise: promise - ) - - case .noMoreMessages: - // Write an empty data frame with the EOS flag set, to signal the RPC - // request is now finished. - context.write( - self.wrapOutboundOut( - HTTP2Frame.FramePayload.data( - .init( - data: .byteBuffer(.init()), - endStream: true - ) - ) - ), - promise: nil - ) - - context.flush() - break loop - - case .awaitMoreMessages: - if self.flushPending { - self.flushPending = false - context.flush() - } - break loop - - case .closeAndFailPromise(let promise, let error): - context.close(mode: .all, promise: nil) - promise?.fail(error) - break loop - } - - } - } catch let invalidState { - context.fireErrorCaught(RPCError(invalidState)) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift b/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift deleted file mode 100644 index 03ad634e3..000000000 --- a/Sources/GRPCHTTP2Core/Client/HTTP2ClientTransport.swift +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore - -/// A namespace for the HTTP/2 client transport. -public enum HTTP2ClientTransport {} - -extension HTTP2ClientTransport { - /// A namespace for HTTP/2 client transport configuration. - public enum Config {} -} - -extension HTTP2ClientTransport.Config { - public struct Compression: Sendable, Hashable { - /// The default algorithm used for compressing outbound messages. - /// - /// This can be overridden on a per-call basis via `CallOptions`. - public var algorithm: CompressionAlgorithm - - /// Compression algorithms enabled for inbound messages. - /// - /// - Note: `CompressionAlgorithm.none` is always supported, even if it isn't set here. - public var enabledAlgorithms: CompressionAlgorithmSet - - /// Creates a new compression configuration. - /// - /// - SeeAlso: ``defaults``. - public init(algorithm: CompressionAlgorithm, enabledAlgorithms: CompressionAlgorithmSet) { - self.algorithm = algorithm - self.enabledAlgorithms = enabledAlgorithms - } - - /// Default values, compression is disabled. - public static var defaults: Self { - Self(algorithm: .none, enabledAlgorithms: .none) - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Keepalive: Sendable, Hashable { - /// The amount of time to wait after reading data before sending a keepalive ping. - /// - /// - Note: The transport may choose to increase this value if it is less than 10 seconds. - public var time: Duration - - /// The amount of time the server has to respond to a keepalive ping before the connection - /// is closed. - public var timeout: Duration - - /// Whether the client sends keepalive pings when there are no calls in progress. - public var allowWithoutCalls: Bool - - /// Creates a new keepalive configuration. - public init(time: Duration, timeout: Duration, allowWithoutCalls: Bool) { - self.time = time - self.timeout = timeout - self.allowWithoutCalls = allowWithoutCalls - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Connection: Sendable, Hashable { - /// The maximum amount of time a connection may be idle before it's closed. - /// - /// Connections are considered idle when there are no open streams on them. Idle connections - /// can be closed after a configured amount of time to free resources. Note that servers may - /// separately monitor and close idle connections. - public var maxIdleTime: Duration? - - /// Configuration for keepalive. - /// - /// Keepalive is typically applied to connection which have open streams. It can be useful to - /// detect dropped connections, particularly if the streams running on a connection don't have - /// much activity. - /// - /// See also: gRFC A8: Client-side Keepalive. - public var keepalive: Keepalive? - - /// Creates a connection configuration. - public init(maxIdleTime: Duration, keepalive: Keepalive?) { - self.maxIdleTime = maxIdleTime - self.keepalive = keepalive - } - - /// Default values, a 30 minute max idle time and no keepalive. - public static var defaults: Self { - Self(maxIdleTime: .seconds(30 * 60), keepalive: nil) - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Backoff: Sendable, Hashable { - /// The initial duration to wait before reattempting to establish a connection. - public var initial: Duration - - /// The maximum duration to wait (before jitter is applied) to wait between connect attempts. - public var max: Duration - - /// The scaling factor applied to the backoff duration between connect attempts. - public var multiplier: Double - - /// An amount to randomize the backoff by. - /// - /// If backoff is computed to be 10 seconds and jitter is set to `0.2`, then the amount of - /// jitter will be selected randomly from the range `-0.2 ✕ 10` seconds to `0.2 ✕ 10` seconds. - /// The resulting backoff will therefore be between 8 seconds and 12 seconds. - public var jitter: Double - - /// Creates a new backoff configuration. - public init(initial: Duration, max: Duration, multiplier: Double, jitter: Double) { - self.initial = initial - self.max = max - self.multiplier = multiplier - self.jitter = jitter - } - - /// Default values, initial backoff is one second and maximum back off is two minutes. The - /// multiplier is `1.6` and the jitter is set to `0.2`. - public static var defaults: Self { - Self(initial: .seconds(1), max: .seconds(120), multiplier: 1.6, jitter: 0.2) - } - } - - public struct HTTP2: Sendable, Hashable { - /// The max frame size, in bytes. - /// - /// The actual value used is clamped to `(1 << 14) ... (1 << 24) - 1` (the min and max values - /// allowed by RFC 9113 § 6.5.2). - public var maxFrameSize: Int - - /// The target flow control window size, in bytes. - /// - /// The value is clamped to `... (1 << 31) - 1`. - public var targetWindowSize: Int - - /// Creates a new HTTP/2 configuration. - public init(maxFrameSize: Int, targetWindowSize: Int) { - self.maxFrameSize = maxFrameSize - self.targetWindowSize = targetWindowSize - } - - /// Default values, max frame size is 16KiB, and the target window size is 8MiB. - public static var defaults: Self { - Self(maxFrameSize: 1 << 14, targetWindowSize: 8 * 1024 * 1024) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift deleted file mode 100644 index d3b66ea18..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv4.swift +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore - -extension ResolvableTargets { - /// A resolvable target for IPv4 addresses. - /// - /// IPv4 addresses can be resolved by the ``NameResolvers/IPv4`` resolver which creates a - /// separate ``Endpoint`` for each address. - public struct IPv4: ResolvableTarget { - /// The IPv4 addresses. - public var addresses: [SocketAddress.IPv4] - - /// Create a new IPv4 target. - /// - Parameter addresses: The IPv4 addresses. - public init(addresses: [SocketAddress.IPv4]) { - self.addresses = addresses - } - } -} - -extension ResolvableTarget where Self == ResolvableTargets.IPv4 { - /// Creates a new resolvable IPv4 target for a single address. - /// - Parameters: - /// - host: The host address. - /// - port: The port on the host. - /// - Returns: A ``ResolvableTarget``. - public static func ipv4(host: String, port: Int = 443) -> Self { - let address = SocketAddress.IPv4(host: host, port: port) - return Self(addresses: [address]) - } - - /// Creates a new resolvable IPv4 target from the provided host-port pairs. - /// - /// - Parameter pairs: An array of host-port pairs. - /// - Returns: A ``ResolvableTarget``. - public static func ipv4(pairs: [(host: String, port: Int)]) -> Self { - let address = pairs.map { SocketAddress.IPv4(host: $0.host, port: $0.port) } - return Self(addresses: address) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NameResolvers { - /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv4`` targets. - /// - /// The name resolver for a given target always produces the same values, with one endpoint per - /// address in the target. This resolver doesn't support fetching service configuration. - public struct IPv4: NameResolverFactory { - public typealias Target = ResolvableTargets.IPv4 - - /// Create a new IPv4 resolver factory. - public init() {} - - public func resolver(for target: Target) -> NameResolver { - let endpoints = target.addresses.map { Endpoint(addresses: [.ipv4($0)]) } - let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfig: nil) - return NameResolver(names: .constant(resolutionResult), updateMode: .pull) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift deleted file mode 100644 index 6c41a0353..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+IPv6.swift +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore - -extension ResolvableTargets { - /// A resolvable target for IPv4 addresses. - /// - /// IPv4 addresses can be resolved by the ``NameResolvers/IPv6`` resolver which creates a - /// separate ``Endpoint`` for each address. - public struct IPv6: ResolvableTarget { - /// The IPv6 addresses. - public var addresses: [SocketAddress.IPv6] - - /// Create a new IPv6 target. - /// - Parameter addresses: The IPv6 addresses. - public init(addresses: [SocketAddress.IPv6]) { - self.addresses = addresses - } - } -} - -extension ResolvableTarget where Self == ResolvableTargets.IPv6 { - /// Creates a new resolvable IPv6 target for a single address. - /// - Parameters: - /// - host: The host address. - /// - port: The port on the host. - /// - Returns: A ``ResolvableTarget``. - public static func ipv6(host: String, port: Int = 443) -> Self { - let address = SocketAddress.IPv6(host: host, port: port) - return Self(addresses: [address]) - } - - /// Creates a new resolvable IPv6 target from the provided host-port pairs. - /// - /// - Parameter pairs: An array of host-port pairs. - /// - Returns: A ``ResolvableTarget``. - public static func ipv6(pairs: [(host: String, port: Int)]) -> Self { - let address = pairs.map { SocketAddress.IPv6(host: $0.host, port: $0.port) } - return Self(addresses: address) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NameResolvers { - /// A ``NameResolverFactory`` for ``ResolvableTargets/IPv6`` targets. - /// - /// The name resolver for a given target always produces the same values, with one endpoint per - /// address in the target. This resolver doesn't support fetching service configuration. - public struct IPv6: NameResolverFactory { - public typealias Target = ResolvableTargets.IPv6 - - /// Create a new IPv6 resolver factory. - public init() {} - - public func resolver(for target: Target) -> NameResolver { - let endpoints = target.addresses.map { Endpoint(addresses: [.ipv6($0)]) } - let resolutionResult = NameResolutionResult(endpoints: endpoints, serviceConfig: nil) - return NameResolver(names: .constant(resolutionResult), updateMode: .pull) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift deleted file mode 100644 index b957bffd5..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+UDS.swift +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore - -extension ResolvableTargets { - /// A resolvable target for Unix Domain Socket address. - /// - /// ``UnixDomainSocket`` addresses can be resolved by the ``NameResolvers/UnixDomainSocket`` - /// resolver which creates a single ``Endpoint`` for target address. - public struct UnixDomainSocket: ResolvableTarget { - /// The Unix Domain Socket address. - public var address: SocketAddress.UnixDomainSocket - - /// Create a new Unix Domain Socket address. - public init(address: SocketAddress.UnixDomainSocket) { - self.address = address - } - } -} - -extension ResolvableTarget where Self == ResolvableTargets.UnixDomainSocket { - /// Creates a new resolvable Unix Domain Socket target. - /// - Parameter path: The path of the socket. - public static func unixDomainSocket(path: String) -> Self { - return Self(address: SocketAddress.UnixDomainSocket(path: path)) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NameResolvers { - /// A ``NameResolverFactory`` for ``ResolvableTargets/UnixDomainSocket`` targets. - /// - /// The name resolver for a given target always produces the same values, with a single endpoint. - /// This resolver doesn't support fetching service configuration. - public struct UnixDomainSocket: NameResolverFactory { - public typealias Target = ResolvableTargets.UnixDomainSocket - - public init() {} - - public func resolver(for target: Target) -> NameResolver { - let endpoint = Endpoint(addresses: [.unixDomainSocket(target.address)]) - let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfig: nil) - return NameResolver(names: .constant(resolutionResult), updateMode: .pull) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift deleted file mode 100644 index e8c6c815b..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver+VSOCK.swift +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore - -extension ResolvableTargets { - /// A resolvable target for Virtual Socket addresses. - /// - /// ``VirtualSocket`` addresses can be resolved by the ``NameResolvers/VirtualSocket`` - /// resolver which creates a single ``Endpoint`` for target address. - public struct VirtualSocket: ResolvableTarget { - public var address: SocketAddress.VirtualSocket - - public init(address: SocketAddress.VirtualSocket) { - self.address = address - } - } -} - -extension ResolvableTarget where Self == ResolvableTargets.VirtualSocket { - /// Creates a new resolvable Virtual Socket target. - /// - Parameters: - /// - contextID: The context ID ('cid') of the service. - /// - port: The port to connect to. - public static func vsock( - contextID: SocketAddress.VirtualSocket.ContextID, - port: SocketAddress.VirtualSocket.Port - ) -> Self { - let address = SocketAddress.VirtualSocket(contextID: contextID, port: port) - return ResolvableTargets.VirtualSocket(address: address) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NameResolvers { - /// A ``NameResolverFactory`` for ``ResolvableTargets/VirtualSocket`` targets. - /// - /// The name resolver for a given target always produces the same values, with a single endpoint. - /// This resolver doesn't support fetching service configuration. - public struct VirtualSocket: NameResolverFactory { - public typealias Target = ResolvableTargets.VirtualSocket - - public init() {} - - public func resolver(for target: Target) -> NameResolver { - let endpoint = Endpoint(addresses: [.vsock(target.address)]) - let resolutionResult = NameResolutionResult(endpoints: [endpoint], serviceConfig: nil) - return NameResolver(names: .constant(resolutionResult), updateMode: .pull) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift deleted file mode 100644 index 822ddb815..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolver.swift +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore - -/// A name resolver can provide resolved addresses and service configuration values over time. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct NameResolver: Sendable { - /// A sequence of name resolution results. - /// - /// Resolvers may be push or pull based. Resolvers with the ``UpdateMode-swift.struct/push`` - /// update mode have addresses pushed to them by an external source and you should subscribe - /// to changes in addresses by awaiting for new values in a loop. - /// - /// Resolvers with the ``UpdateMode-swift.struct/pull`` update mode shouldn't be subscribed to, - /// instead you should create an iterator and ask for new results as and when necessary. - public var names: RPCAsyncSequence - - /// How ``names`` is updated and should be consumed. - public let updateMode: UpdateMode - - public struct UpdateMode: Hashable, Sendable { - enum Value: Hashable, Sendable { - case push - case pull - } - - let value: Value - - private init(_ value: Value) { - self.value = value - } - - /// Addresses are pushed to the resolve by an external source. - public static var push: Self { Self(.push) } - - /// Addresses are resolved lazily, when the caller asks them to be resolved. - public static var pull: Self { Self(.pull) } - } - - /// Create a new name resolver. - public init(names: RPCAsyncSequence, updateMode: UpdateMode) { - self.names = names - self.updateMode = updateMode - } -} - -/// The result of name resolution, a list of endpoints to connect to and the service -/// configuration reported by the resolver. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct NameResolutionResult: Hashable, Sendable { - /// A list of endpoints to connect to. - public var endpoints: [Endpoint] - - /// The service configuration reported by the resolver, or an error if it couldn't be parsed. - /// This value may be `nil` if the resolver doesn't support fetching service configuration. - public var serviceConfig: Result? - - public init( - endpoints: [Endpoint], - serviceConfig: Result? - ) { - self.endpoints = endpoints - self.serviceConfig = serviceConfig - } -} - -/// A group of addresses which are considered equivalent when establishing a connection. -public struct Endpoint: Hashable, Sendable { - /// A list of equivalent addresses. - /// - /// Earlier addresses are typically but not always connected to first. Some load balancers may - /// choose to ignore the order. - public var addresses: [SocketAddress] - - /// Create a new ``Endpoint``. - /// - Parameter addresses: A list of equivalent addresses. - public init(addresses: [SocketAddress]) { - self.addresses = addresses - } -} - -/// A resolver capable of resolving targets of type ``Target``. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol NameResolverFactory { - /// The type of ``ResolvableTarget`` this factory makes resolvers for. - associatedtype Target: ResolvableTarget - - /// Creates a resolver for the given target. - /// - /// - Parameter target: The target to make a resolver for. - /// - Returns: The name resolver for the target. - func resolver(for target: Target) -> NameResolver -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NameResolverFactory { - /// Returns whether the given target is compatible with this factory. - /// - /// - Parameter target: The target to check the compatibility of. - /// - Returns: Whether the target is compatible with this factory. - func isCompatible(withTarget target: Other) -> Bool { - return target is Target - } - - /// Returns a name resolver if the given target is compatible. - /// - /// - Parameter target: The target to make a name resolver for. - /// - Returns: A name resolver or `nil` if the target isn't compatible. - func makeResolverIfCompatible(_ target: Other) -> NameResolver? { - guard let target = target as? Target else { return nil } - return self.resolver(for: target) - } -} - -/// A target which can be resolved to a ``SocketAddress``. -public protocol ResolvableTarget {} - -/// A namespace for resolvable targets. -public enum ResolvableTargets {} - -/// A namespace for name resolver factories. -public enum NameResolvers {} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift b/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift deleted file mode 100644 index c8e847196..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/NameResolverRegistry.swift +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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. - */ - -/// A registry for name resolver factories. -/// -/// The registry provides name resolvers for resolvable targets. You can control which name -/// resolvers are available by registering and removing resolvers by type. The following code -/// demonstrates how to create a registry, add and remove resolver factories, and create a resolver. -/// -/// ```swift -/// // Create a new resolver registry with the default resolvers. -/// var registry = NameResolverRegistry.defaults -/// -/// // Register a custom resolver, the registry can now resolve targets of -/// // type `CustomResolver.ResolvableTarget`. -/// registry.registerFactory(CustomResolver()) -/// -/// // Remove the Unix Domain Socket and Virtual Socket resolvers, if they exist. -/// registry.removeFactory(ofType: NameResolvers.UnixDomainSocket.self) -/// registry.removeFactory(ofType: NameResolvers.VirtualSocket.self) -/// -/// // Resolve an IPv4 target -/// if let resolver = registry.makeResolver(for: .ipv4(host: "localhost", port: 80)) { -/// // ... -/// } -/// ``` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct NameResolverRegistry { - private enum Factory { - case ipv4(NameResolvers.IPv4) - case ipv6(NameResolvers.IPv6) - case unix(NameResolvers.UnixDomainSocket) - case vsock(NameResolvers.VirtualSocket) - case other(any NameResolverFactory) - - init(_ factory: some NameResolverFactory) { - if let ipv4 = factory as? NameResolvers.IPv4 { - self = .ipv4(ipv4) - } else if let ipv6 = factory as? NameResolvers.IPv6 { - self = .ipv6(ipv6) - } else if let unix = factory as? NameResolvers.UnixDomainSocket { - self = .unix(unix) - } else if let vsock = factory as? NameResolvers.VirtualSocket { - self = .vsock(vsock) - } else { - self = .other(factory) - } - } - - func makeResolverIfCompatible(_ target: Target) -> NameResolver? { - switch self { - case .ipv4(let factory): - return factory.makeResolverIfCompatible(target) - case .ipv6(let factory): - return factory.makeResolverIfCompatible(target) - case .unix(let factory): - return factory.makeResolverIfCompatible(target) - case .vsock(let factory): - return factory.makeResolverIfCompatible(target) - case .other(let factory): - return factory.makeResolverIfCompatible(target) - } - } - - func hasTarget(_ target: Target) -> Bool { - switch self { - case .ipv4(let factory): - return factory.isCompatible(withTarget: target) - case .ipv6(let factory): - return factory.isCompatible(withTarget: target) - case .unix(let factory): - return factory.isCompatible(withTarget: target) - case .vsock(let factory): - return factory.isCompatible(withTarget: target) - case .other(let factory): - return factory.isCompatible(withTarget: target) - } - } - - func `is`(ofType factoryType: Factory.Type) -> Bool { - switch self { - case .ipv4: - return NameResolvers.IPv4.self == factoryType - case .ipv6: - return NameResolvers.IPv6.self == factoryType - case .unix: - return NameResolvers.UnixDomainSocket.self == factoryType - case .vsock: - return NameResolvers.VirtualSocket.self == factoryType - case .other(let factory): - return type(of: factory) == factoryType - } - } - } - - private var factories: [Factory] - - /// Creates a new name resolver registry with no resolve factories. - public init() { - self.factories = [] - } - - /// Returns a new name resolver registry with the default factories registered. - /// - /// The default resolvers include: - /// - ``NameResolvers/IPv4``, - /// - ``NameResolvers/IPv6``, - /// - ``NameResolvers/UnixDomainSocket``, - /// - ``NameResolvers/VirtualSocket``. - public static var defaults: Self { - var resolvers = NameResolverRegistry() - resolvers.registerFactory(NameResolvers.IPv4()) - resolvers.registerFactory(NameResolvers.IPv6()) - resolvers.registerFactory(NameResolvers.UnixDomainSocket()) - resolvers.registerFactory(NameResolvers.VirtualSocket()) - return resolvers - } - - /// The number of resolver factories in the registry. - public var count: Int { - return self.factories.count - } - - /// Whether there are no resolver factories in the registry. - public var isEmpty: Bool { - return self.factories.isEmpty - } - - /// Registers a new name resolver factory. - /// - /// Any factories of the same type are removed prior to inserting the factory. - /// - /// - Parameter factory: The factory to register. - public mutating func registerFactory(_ factory: Factory) { - self.removeFactory(ofType: Factory.self) - self.factories.append(Self.Factory(factory)) - } - - /// Removes any factories which have the given type - /// - /// - Parameter type: The type of factory to remove. - /// - Returns: Whether a factory was removed. - @discardableResult - public mutating func removeFactory( - ofType type: Factory.Type - ) -> Bool { - let factoryCount = self.factories.count - self.factories.removeAll { - $0.is(ofType: Factory.self) - } - return self.factories.count < factoryCount - } - - /// Returns whether the registry contains a factory of the given type. - /// - /// - Parameter type: The type of factory to look for. - /// - Returns: Whether the registry contained the factory of the given type. - public func containsFactory(ofType type: Factory.Type) -> Bool { - self.factories.contains { - $0.is(ofType: Factory.self) - } - } - - /// Returns whether the registry contains a factory capable of resolving the given target. - /// - /// - Parameter target: - /// - Returns: Whether the registry contains a resolve capable of resolving the target. - public func containsFactory(capableOfResolving target: some ResolvableTarget) -> Bool { - self.factories.contains { $0.hasTarget(target) } - } - - /// Makes a ``NameResolver`` for the target, if a suitable factory exists. - /// - /// If multiple factories exist which are capable of resolving the target then the first - /// is used. - /// - /// - Parameter target: The target to make a resolver for. - /// - Returns: The resolver, or `nil` if no factory could make a resolver for the target. - public func makeResolver(for target: some ResolvableTarget) -> NameResolver? { - for factory in self.factories { - if let resolver = factory.makeResolverIfCompatible(target) { - return resolver - } - } - return nil - } -} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift b/Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift deleted file mode 100644 index 8f5d961b1..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/SocketAddress.swift +++ /dev/null @@ -1,313 +0,0 @@ -/* - * 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. - */ - -/// An address to which a socket may connect or bind. -public struct SocketAddress: Hashable, Sendable { - private enum Value: Hashable, Sendable { - case ipv4(IPv4) - case ipv6(IPv6) - case unix(UnixDomainSocket) - case vsock(VirtualSocket) - } - - private var value: Value - private init(_ value: Value) { - self.value = value - } - - /// Returns the address as an IPv4 address, if possible. - public var ipv4: IPv4? { - switch self.value { - case .ipv4(let address): - return address - default: - return nil - } - } - - /// Returns the address as an IPv6 address, if possible. - public var ipv6: IPv6? { - switch self.value { - case .ipv6(let address): - return address - default: - return nil - } - } - - /// Returns the address as an Unix domain socket address, if possible. - public var unixDomainSocket: UnixDomainSocket? { - switch self.value { - case .unix(let address): - return address - default: - return nil - } - } - - /// Returns the address as an VSOCK address, if possible. - public var virtualSocket: VirtualSocket? { - switch self.value { - case .vsock(let address): - return address - default: - return nil - } - } -} - -extension SocketAddress { - /// Creates a socket address by wrapping a ``SocketAddress/IPv4-swift.struct``. - public static func ipv4(_ address: IPv4) -> Self { - return Self(.ipv4(address)) - } - - /// Creates a socket address by wrapping a ``SocketAddress/IPv6-swift.struct``. - public static func ipv6(_ address: IPv6) -> Self { - return Self(.ipv6(address)) - } - - /// Creates a socket address by wrapping a ``SocketAddress/UnixDomainSocket-swift.struct``. - public static func unixDomainSocket(_ address: UnixDomainSocket) -> Self { - return Self(.unix(address)) - } - - /// Creates a socket address by wrapping a ``SocketAddress/VirtualSocket-swift.struct``. - public static func vsock(_ address: VirtualSocket) -> Self { - return Self(.vsock(address)) - } -} - -extension SocketAddress { - /// Creates an IPv4 socket address. - public static func ipv4(host: String, port: Int) -> Self { - return .ipv4(IPv4(host: host, port: port)) - } - - /// Creates an IPv6 socket address. - public static func ipv6(host: String, port: Int) -> Self { - return .ipv6(IPv6(host: host, port: port)) - } - /// Creates a Unix socket address. - public static func unixDomainSocket(path: String) -> Self { - return .unixDomainSocket(UnixDomainSocket(path: path)) - } - - /// Create a Virtual Socket ('vsock') address. - public static func vsock(contextID: VirtualSocket.ContextID, port: VirtualSocket.Port) -> Self { - return .vsock(VirtualSocket(contextID: contextID, port: port)) - } -} - -extension SocketAddress: CustomStringConvertible { - public var description: String { - switch self.value { - case .ipv4(let address): - return String(describing: address) - case .ipv6(let address): - return String(describing: address) - case .unix(let address): - return String(describing: address) - case .vsock(let address): - return String(describing: address) - } - } -} - -extension SocketAddress { - public struct IPv4: Hashable, Sendable { - /// The resolved host address. - public var host: String - /// The port to connect to. - public var port: Int - - /// Creates a new IPv4 address. - /// - /// - Parameters: - /// - host: Resolved host address. - /// - port: Port to connect to. - public init(host: String, port: Int) { - self.host = host - self.port = port - } - } - - public struct IPv6: Hashable, Sendable { - /// The resolved host address. - public var host: String - /// The port to connect to. - public var port: Int - - /// Creates a new IPv6 address. - /// - /// - Parameters: - /// - host: Resolved host address. - /// - port: Port to connect to. - public init(host: String, port: Int) { - self.host = host - self.port = port - } - } - - public struct UnixDomainSocket: Hashable, Sendable { - /// The path name of the Unix domain socket. - public var path: String - - /// Create a new Unix domain socket address. - /// - /// - Parameter path: The path name of the Unix domain socket. - public init(path: String) { - self.path = path - } - } - - public struct VirtualSocket: Hashable, Sendable { - /// A context identifier. - /// - /// Indicates the source or destination which is either a virtual machine or the host. - public var contextID: ContextID - - /// The port number. - public var port: Port - - /// Create a new VSOCK address. - /// - /// - Parameters: - /// - contextID: The context ID (or 'cid') of the address. - /// - port: The port number. - public init(contextID: ContextID, port: Port) { - self.contextID = contextID - self.port = port - } - - public struct Port: Hashable, Sendable, RawRepresentable, ExpressibleByIntegerLiteral { - /// The port number. - public var rawValue: UInt32 - - public init(rawValue: UInt32) { - self.rawValue = rawValue - } - - public init(integerLiteral value: UInt32) { - self.rawValue = value - } - - public init(_ value: Int) { - self.init(rawValue: UInt32(bitPattern: Int32(truncatingIfNeeded: value))) - } - - /// Used to bind to any port number. - /// - /// This is equal to `VMADDR_PORT_ANY (-1U)`. - public static var any: Self { - Self(rawValue: UInt32(bitPattern: -1)) - } - } - - public struct ContextID: Hashable, Sendable, RawRepresentable, ExpressibleByIntegerLiteral { - /// The context identifier. - public var rawValue: UInt32 - - public init(rawValue: UInt32) { - self.rawValue = rawValue - } - - public init(integerLiteral value: UInt32) { - self.rawValue = value - } - - public init(_ value: Int) { - self.rawValue = UInt32(bitPattern: Int32(truncatingIfNeeded: value)) - } - - /// Wildcard, matches any address. - /// - /// On all platforms, using this value with `bind(2)` means "any address". - /// - /// On Darwin platforms, the man page states this can be used with `connect(2)` - /// to mean "this host". - /// - /// This is equal to `VMADDR_CID_ANY (-1U)`. - public static var any: Self { - Self(rawValue: UInt32(bitPattern: -1)) - } - - /// The address of the hypervisor. - /// - /// This is equal to `VMADDR_CID_HYPERVISOR (0)`. - public static var hypervisor: Self { - Self(rawValue: 0) - } - - /// The address of the host. - /// - /// This is equal to `VMADDR_CID_HOST (2)`. - public static var host: Self { - Self(rawValue: 2) - } - - /// The address for local communication (loopback). - /// - /// This directs packets to the same host that generated them. This is useful for testing - /// applications on a single host and for debugging. - /// - /// This is equal to `VMADDR_CID_LOCAL (1)` on platforms that define it. - /// - /// - Warning: `VMADDR_CID_LOCAL (1)` is available from Linux 5.6. Its use is unsupported on - /// other platforms. - /// - SeeAlso: https://man7.org/linux/man-pages/man7/vsock.7.html - public static var local: Self { - Self(rawValue: 1) - } - } - } -} - -extension SocketAddress.IPv4: CustomStringConvertible { - public var description: String { - "[ipv4]\(self.host):\(self.port)" - } -} - -extension SocketAddress.IPv6: CustomStringConvertible { - public var description: String { - "[ipv6]\(self.host):\(self.port)" - } -} - -extension SocketAddress.UnixDomainSocket: CustomStringConvertible { - public var description: String { - "[unix]\(self.path)" - } -} - -extension SocketAddress.VirtualSocket: CustomStringConvertible { - public var description: String { - "[vsock]\(self.contextID):\(self.port)" - } -} - -extension SocketAddress.VirtualSocket.ContextID: CustomStringConvertible { - public var description: String { - self == .any ? "-1" : String(describing: self.rawValue) - } -} - -extension SocketAddress.VirtualSocket.Port: CustomStringConvertible { - public var description: String { - self == .any ? "-1" : String(describing: self.rawValue) - } -} diff --git a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift deleted file mode 100644 index 1608f4c1e..000000000 --- a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore - -extension CompressionAlgorithm { - init?(name: String) { - self.init(name: name[...]) - } - - init?(name: Substring) { - switch name { - case "gzip": - self = .gzip - case "deflate": - self = .deflate - case "identity": - self = .none - default: - return nil - } - } - - var name: String { - switch self.value { - case .gzip: - return "gzip" - case .deflate: - return "deflate" - case .none: - return "identity" - } - } -} - -extension CompressionAlgorithmSet { - var count: Int { - self.rawValue.nonzeroBitCount - } -} diff --git a/Sources/GRPCHTTP2Core/Compression/Zlib.swift b/Sources/GRPCHTTP2Core/Compression/Zlib.swift deleted file mode 100644 index a2a25110f..000000000 --- a/Sources/GRPCHTTP2Core/Compression/Zlib.swift +++ /dev/null @@ -1,419 +0,0 @@ -/* - * 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. - */ - -internal import CGRPCZlib -internal import GRPCCore -internal import NIOCore - -enum Zlib { - enum Method { - case deflate - case gzip - - fileprivate var windowBits: Int32 { - switch self { - case .deflate: - return 15 - case .gzip: - return 31 - } - } - } -} - -extension Zlib { - /// Creates a new compressor for the given compression format. - /// - /// This compressor is only suitable for compressing whole messages at a time. - /// - /// - Important: ``Compressor/end()`` must be called when the compressor is not needed - /// anymore, to deallocate any resources allocated by `Zlib`. - struct Compressor { - // TODO: Make this ~Copyable when 5.9 is the lowest supported Swift version. - - private var stream: UnsafeMutablePointer - private let method: Method - - init(method: Method) { - self.method = method - self.stream = .allocate(capacity: 1) - self.stream.initialize(to: z_stream()) - self.stream.deflateInit(windowBits: self.method.windowBits) - } - - /// Compresses the data in `input` into the `output` buffer. - /// - /// - Parameter input: The complete data to be compressed. - /// - Parameter output: The `ByteBuffer` into which the compressed message should be written. - /// - Returns: The number of bytes written into the `output` buffer. - @discardableResult - func compress(_ input: [UInt8], into output: inout ByteBuffer) throws(ZlibError) -> Int { - defer { self.reset() } - let upperBound = self.stream.deflateBound(inputBytes: input.count) - return try self.stream.deflate(input, into: &output, upperBound: upperBound) - } - - /// Resets compression state. - private func reset() { - do { - try self.stream.deflateReset() - } catch { - self.end() - self.stream.initialize(to: z_stream()) - self.stream.deflateInit(windowBits: self.method.windowBits) - } - } - - /// Deallocates any resources allocated by Zlib. - func end() { - self.stream.deflateEnd() - self.stream.deallocate() - } - } -} - -extension Zlib { - /// Creates a new decompressor for the given compression format. - /// - /// This decompressor is only suitable for compressing whole messages at a time. - /// - /// - Important: ``Decompressor/end()`` must be called when the compressor is not needed - /// anymore, to deallocate any resources allocated by `Zlib`. - struct Decompressor { - // TODO: Make this ~Copyable when 5.9 is the lowest supported Swift version. - - private var stream: UnsafeMutablePointer - private let method: Method - - init(method: Method) { - self.method = method - self.stream = UnsafeMutablePointer.allocate(capacity: 1) - self.stream.initialize(to: z_stream()) - self.stream.inflateInit(windowBits: self.method.windowBits) - } - - /// Returns the decompressed bytes from ``input``. - /// - /// - Parameters: - /// - input: The buffer read compressed bytes from. - /// - limit: The largest size a decompressed payload may be. - func decompress(_ input: inout ByteBuffer, limit: Int) throws -> [UInt8] { - defer { self.reset() } - return try self.stream.inflate(input: &input, limit: limit) - } - - /// Resets decompression state. - private func reset() { - do { - try self.stream.inflateReset() - } catch { - self.end() - self.stream.initialize(to: z_stream()) - self.stream.inflateInit(windowBits: self.method.windowBits) - } - } - - /// Deallocates any resources allocated by Zlib. - func end() { - self.stream.inflateEnd() - self.stream.deallocate() - } - } -} - -struct ZlibError: Error, Hashable { - /// Error code returned from Zlib. - var code: Int - /// Error message produced by Zlib. - var message: String - - init(code: Int, message: String) { - self.code = code - self.message = message - } -} - -extension UnsafeMutablePointer { - func inflateInit(windowBits: Int32) { - self.pointee.zfree = nil - self.pointee.zalloc = nil - self.pointee.opaque = nil - - let rc = CGRPCZlib_inflateInit2(self, windowBits) - // Possible return codes: - // - Z_OK - // - Z_MEM_ERROR: not enough memory - // - // If we can't allocate memory then we can't progress anyway so not throwing an error here is - // okay. - precondition(rc == Z_OK, "inflateInit2 failed with error (\(rc)) \(self.lastError ?? "")") - } - - func inflateReset() throws { - let rc = CGRPCZlib_inflateReset(self) - - // Possible return codes: - // - Z_OK - // - Z_STREAM_ERROR: the source stream state was inconsistent. - switch rc { - case Z_OK: - () - case Z_STREAM_ERROR: - throw ZlibError(code: Int(rc), message: self.lastError ?? "") - default: - preconditionFailure("inflateReset returned unexpected code (\(rc))") - } - } - - func inflateEnd() { - _ = CGRPCZlib_inflateEnd(self) - } - - func deflateInit(windowBits: Int32) { - self.pointee.zfree = nil - self.pointee.zalloc = nil - self.pointee.opaque = nil - - let rc = CGRPCZlib_deflateInit2( - self, - Z_DEFAULT_COMPRESSION, // compression level - Z_DEFLATED, // compression method (this must be Z_DEFLATED) - windowBits, // window size, i.e. deflate/gzip - 8, // memory level (this is the default value in the docs) - Z_DEFAULT_STRATEGY // compression strategy - ) - - // Possible return codes: - // - Z_OK - // - Z_MEM_ERROR: not enough memory - // - Z_STREAM_ERROR: a parameter was invalid - // - // If we can't allocate memory then we can't progress anyway, and we control the parameters - // so not throwing an error here is okay. - precondition(rc == Z_OK, "deflateInit2 failed with error (\(rc)) \(self.lastError ?? "")") - } - - func deflateReset() throws { - let rc = CGRPCZlib_deflateReset(self) - - // Possible return codes: - // - Z_OK - // - Z_STREAM_ERROR: the source stream state was inconsistent. - switch rc { - case Z_OK: - () - case Z_STREAM_ERROR: - throw ZlibError(code: Int(rc), message: self.lastError ?? "") - default: - preconditionFailure("deflateReset returned unexpected code (\(rc))") - } - } - - func deflateEnd() { - _ = CGRPCZlib_deflateEnd(self) - } - - func deflateBound(inputBytes: Int) -> Int { - let bound = CGRPCZlib_deflateBound(self, UInt(inputBytes)) - return Int(bound) - } - - func setNextInputBuffer(_ buffer: UnsafeMutableBufferPointer) { - if let baseAddress = buffer.baseAddress { - self.pointee.next_in = baseAddress - self.pointee.avail_in = UInt32(buffer.count) - } else { - self.pointee.next_in = nil - self.pointee.avail_in = 0 - } - } - - func setNextInputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { - if let buffer = buffer, let baseAddress = buffer.baseAddress { - self.pointee.next_in = CGRPCZlib_castVoidToBytefPointer(baseAddress) - self.pointee.avail_in = UInt32(buffer.count) - } else { - self.pointee.next_in = nil - self.pointee.avail_in = 0 - } - } - - func setNextOutputBuffer(_ buffer: UnsafeMutableBufferPointer) { - if let baseAddress = buffer.baseAddress { - self.pointee.next_out = baseAddress - self.pointee.avail_out = UInt32(buffer.count) - } else { - self.pointee.next_out = nil - self.pointee.avail_out = 0 - } - } - - func setNextOutputBuffer(_ buffer: UnsafeMutableRawBufferPointer?) { - if let buffer = buffer, let baseAddress = buffer.baseAddress { - self.pointee.next_out = CGRPCZlib_castVoidToBytefPointer(baseAddress) - self.pointee.avail_out = UInt32(buffer.count) - } else { - self.pointee.next_out = nil - self.pointee.avail_out = 0 - } - } - - /// Number of bytes available to read `self.nextInputBuffer`. See also: `z_stream.avail_in`. - var availableInputBytes: Int { - get { - Int(self.pointee.avail_in) - } - set { - self.pointee.avail_in = UInt32(newValue) - } - } - - /// The remaining writable space in `nextOutputBuffer`. See also: `z_stream.avail_out`. - var availableOutputBytes: Int { - get { - Int(self.pointee.avail_out) - } - set { - self.pointee.avail_out = UInt32(newValue) - } - } - - /// The total number of bytes written to the output buffer. See also: `z_stream.total_out`. - var totalOutputBytes: Int { - Int(self.pointee.total_out) - } - - /// The last error message that zlib wrote. No message is guaranteed on error, however, `nil` is - /// guaranteed if there is no error. See also `z_stream.msg`. - var lastError: String? { - self.pointee.msg.map { String(cString: $0) } - } - - func inflate(input: inout ByteBuffer, limit: Int) throws -> [UInt8] { - return try input.readWithUnsafeMutableReadableBytes { inputPointer in - self.setNextInputBuffer(inputPointer) - defer { - self.setNextInputBuffer(nil) - self.setNextOutputBuffer(nil) - } - - // Assume the output will be twice as large as the input. - var output = [UInt8](repeating: 0, count: min(inputPointer.count * 2, limit)) - var offset = 0 - - while true { - let (finished, written) = try output[offset...].withUnsafeMutableBytes { outPointer in - self.setNextOutputBuffer(outPointer) - - let finished: Bool - - // Possible return codes: - // - Z_OK: some progress has been made - // - Z_STREAM_END: the end of the compressed data has been reached and all uncompressed - // output has been produced - // - Z_NEED_DICT: a preset dictionary is needed at this point - // - Z_DATA_ERROR: the input data was corrupted - // - Z_STREAM_ERROR: the stream structure was inconsistent - // - Z_MEM_ERROR there was not enough memory - // - Z_BUF_ERROR if no progress was possible or if there was not enough room in the output - // buffer when Z_FINISH is used. - // - // Note that Z_OK is not okay here since we always flush with Z_FINISH and therefore - // use Z_STREAM_END as our success criteria. - let rc = CGRPCZlib_inflate(self, Z_FINISH) - switch rc { - case Z_STREAM_END: - finished = true - case Z_BUF_ERROR: - finished = false - default: - throw RPCError( - code: .internalError, - message: "Decompression error", - cause: ZlibError(code: Int(rc), message: self.lastError ?? "") - ) - } - - let size = outPointer.count - self.availableOutputBytes - return (finished, size) - } - - if finished { - output.removeLast(output.count - self.totalOutputBytes) - let bytesRead = inputPointer.count - self.availableInputBytes - return (bytesRead, output) - } else { - offset += written - let newSize = min(output.count * 2, limit) - if newSize == output.count { - throw RPCError(code: .resourceExhausted, message: "Message is too large to decompress.") - } else { - output.append(contentsOf: repeatElement(0, count: newSize - output.count)) - } - } - } - } - } - - func deflate( - _ input: [UInt8], - into output: inout ByteBuffer, - upperBound: Int - ) throws(ZlibError) -> Int { - defer { - self.setNextInputBuffer(nil) - self.setNextOutputBuffer(nil) - } - - do { - var input = input - return try input.withUnsafeMutableBytes { input in - self.setNextInputBuffer(input) - - return try output.writeWithUnsafeMutableBytes(minimumWritableBytes: upperBound) { output in - self.setNextOutputBuffer(output) - - let rc = CGRPCZlib_deflate(self, Z_FINISH) - - // Possible return codes: - // - Z_OK: some progress has been made - // - Z_STREAM_END: all input has been consumed and all output has been produced (only when - // flush is set to Z_FINISH) - // - Z_STREAM_ERROR: the stream state was inconsistent - // - Z_BUF_ERROR: no progress is possible - // - // The documentation notes that Z_BUF_ERROR is not fatal, and deflate() can be called again - // with more input and more output space to continue compressing. However, we - // call `deflateBound()` before `deflate()` which guarantees that the output size will not be - // larger than the value returned by `deflateBound()` if `Z_FINISH` flush is used. As such, - // the only acceptable outcome is `Z_STREAM_END`. - guard rc == Z_STREAM_END else { - throw ZlibError(code: Int(rc), message: self.lastError ?? "") - } - - return output.count - self.availableOutputBytes - } - } - } catch let error as ZlibError { - throw error - } catch { - // Shouldn't happen as 'withUnsafeMutableBytes' and 'writeWithUnsafeMutableBytes' are - // marked 'rethrows' (but don't support typed throws, yet) and the closure only throws - // an 'RPCError' which is handled above. - fatalError("Unexpected error of type \(type(of: error))") - } - } -} diff --git a/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift b/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift deleted file mode 100644 index 9d557a6bf..000000000 --- a/Sources/GRPCHTTP2Core/GRPCMessageDecoder.swift +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore -package import NIOCore - -/// A ``GRPCMessageDecoder`` helps with the deframing of gRPC data frames: -/// - It reads the frame's metadata to know whether the message payload is compressed or not, and its length -/// - It reads and decompresses the payload, if compressed -/// - It helps put together frames that have been split across multiple `ByteBuffers` by the underlying transport -struct GRPCMessageDecoder: NIOSingleStepByteToMessageDecoder { - /// Length of the gRPC message header (1 compression byte, 4 bytes for the length). - static let metadataLength = 5 - - typealias InboundOut = [UInt8] - - private let decompressor: Zlib.Decompressor? - private let maxPayloadSize: Int - - /// Create a new ``GRPCMessageDeframer``. - /// - Parameters: - /// - maxPayloadSize: The maximum size a message payload can be. - /// - decompressor: A `Zlib.Decompressor` to use when decompressing compressed gRPC messages. - /// - Important: You must call `end()` on the `decompressor` when you're done using it, to clean - /// up any resources allocated by `Zlib`. - init( - maxPayloadSize: Int, - decompressor: Zlib.Decompressor? = nil - ) { - self.maxPayloadSize = maxPayloadSize - self.decompressor = decompressor - } - - mutating func decode(buffer: inout ByteBuffer) throws -> InboundOut? { - guard buffer.readableBytes >= Self.metadataLength else { - // If we cannot read enough bytes to cover the metadata's length, then we - // need to wait for more bytes to become available to us. - return nil - } - - // Store the current reader index in case we don't yet have enough - // bytes in the buffer to decode a full frame, and need to reset it. - // The force-unwraps for the compression flag and message length are safe, - // because we've checked just above that we've got at least enough bytes to - // read all of the metadata. - let originalReaderIndex = buffer.readerIndex - let isMessageCompressed = buffer.readInteger(as: UInt8.self)! == 1 - let messageLength = buffer.readInteger(as: UInt32.self)! - - if messageLength > self.maxPayloadSize { - throw RPCError( - code: .resourceExhausted, - message: """ - Message has exceeded the configured maximum payload size \ - (max: \(self.maxPayloadSize), actual: \(messageLength)) - """ - ) - } - - guard var message = buffer.readSlice(length: Int(messageLength)) else { - // `ByteBuffer/readSlice(length:)` returns nil when there are not enough - // bytes to read the requested length. This can happen if we don't yet have - // enough bytes buffered to read the full message payload. - // By reading the metadata though, we have already moved the reader index, - // so we must reset it to its previous, original position for now, - // and return. We'll try decoding again, once more bytes become available - // in our buffer. - buffer.moveReaderIndex(to: originalReaderIndex) - return nil - } - - if isMessageCompressed { - guard let decompressor = self.decompressor else { - // We cannot decompress the payload - throw an error. - throw RPCError( - code: .internalError, - message: "Received a compressed message payload, but no decompressor has been configured." - ) - } - return try decompressor.decompress(&message, limit: self.maxPayloadSize) - } else { - return Array(buffer: message) - } - } - - mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> InboundOut? { - try self.decode(buffer: &buffer) - } -} - -package struct GRPCMessageDeframer { - private var decoder: GRPCMessageDecoder - private var buffer: Optional - - package var _readerIndex: Int? { - self.buffer?.readerIndex - } - - init(maxPayloadSize: Int, decompressor: Zlib.Decompressor?) { - self.decoder = GRPCMessageDecoder( - maxPayloadSize: maxPayloadSize, - decompressor: decompressor - ) - self.buffer = nil - } - - package init(maxPayloadSize: Int) { - self.decoder = GRPCMessageDecoder(maxPayloadSize: maxPayloadSize, decompressor: nil) - self.buffer = nil - } - - package mutating func append(_ buffer: ByteBuffer) { - if self.buffer == nil || self.buffer!.readableBytes == 0 { - self.buffer = buffer - } else { - // Avoid having too many read bytes in the buffer which can lead to the buffer growing much - // larger than is necessary. - let readerIndex = self.buffer!.readerIndex - if readerIndex > 1024 && readerIndex > (self.buffer!.capacity / 2) { - self.buffer!.discardReadBytes() - } - self.buffer!.writeImmutableBuffer(buffer) - } - } - - package mutating func decodeNext() throws -> [UInt8]? { - guard (self.buffer?.readableBytes ?? 0) > 0 else { return nil } - // Above checks mean this is both non-nil and non-empty. - let message = try self.decoder.decode(buffer: &self.buffer!) - return message - } -} - -extension GRPCMessageDeframer { - mutating func decode(into queue: inout OneOrManyQueue<[UInt8]>) throws { - while let next = try self.decodeNext() { - queue.append(next) - } - } -} diff --git a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift b/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift deleted file mode 100644 index 509b7ea35..000000000 --- a/Sources/GRPCHTTP2Core/GRPCMessageFramer.swift +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore -internal import NIOCore - -/// A ``GRPCMessageFramer`` helps with the framing of gRPC data frames: -/// - It prepends data with the required metadata (compression flag and message length). -/// - It compresses messages using the specified compression algorithm (if configured). -/// - It coalesces multiple messages (appended into the `Framer` by calling ``append(_:compress:)``) -/// into a single `ByteBuffer`. -struct GRPCMessageFramer { - /// Length of the gRPC message header (1 compression byte, 4 bytes for the length). - static let metadataLength = 5 - - /// Maximum size the `writeBuffer` can be when concatenating multiple frames. - /// This limit will not be considered if only a single message/frame is written into the buffer, meaning - /// frames with messages over 64KB can still be written. - /// - Note: This is expressed as the power of 2 closer to 64KB (i.e., 64KiB) because `ByteBuffer` - /// reserves capacity in powers of 2. This way, we can take advantage of the whole buffer. - static let maxWriteBufferLength = 65_536 - - private var pendingMessages: OneOrManyQueue<(bytes: [UInt8], promise: EventLoopPromise?)> - - private var writeBuffer: ByteBuffer - - /// Create a new ``GRPCMessageFramer``. - init() { - self.pendingMessages = OneOrManyQueue() - self.writeBuffer = ByteBuffer() - } - - /// Queue the given bytes to be framed and potentially coalesced alongside other messages in a `ByteBuffer`. - /// The resulting data will be returned when calling ``GRPCMessageFramer/next()``. - mutating func append(_ bytes: [UInt8], promise: EventLoopPromise?) { - self.pendingMessages.append((bytes, promise)) - } - - /// If there are pending messages to be framed, a `ByteBuffer` will be returned with the framed data. - /// Data may also be compressed (if configured) and multiple frames may be coalesced into the same `ByteBuffer`. - /// - Parameter compressor: An optional compressor: if present, payloads will be compressed; otherwise - /// they'll be framed as-is. - /// - Throws: If an error is encountered, such as a compression failure, an error will be thrown. - mutating func nextResult( - compressor: Zlib.Compressor? = nil - ) -> (result: Result, promise: EventLoopPromise?)? { - if self.pendingMessages.isEmpty { - // Nothing pending: exit early. - return nil - } - - defer { - // To avoid holding an excessively large buffer, if its size is larger than - // our threshold (`maxWriteBufferLength`), then reset it to a new `ByteBuffer`. - if self.writeBuffer.capacity > Self.maxWriteBufferLength { - self.writeBuffer = ByteBuffer() - } - } - - var requiredCapacity = 0 - for message in self.pendingMessages { - requiredCapacity += message.bytes.count + Self.metadataLength - } - self.writeBuffer.clear(minimumCapacity: requiredCapacity) - - var pendingWritePromise: EventLoopPromise? - while let message = self.pendingMessages.pop() { - pendingWritePromise.setOrCascade(to: message.promise) - - do { - try self.encode(message.bytes, compressor: compressor) - } catch let rpcError { - return (result: .failure(rpcError), promise: pendingWritePromise) - } - } - - return (result: .success(self.writeBuffer), promise: pendingWritePromise) - } - - private mutating func encode(_ message: [UInt8], compressor: Zlib.Compressor?) throws(RPCError) { - if let compressor { - self.writeBuffer.writeInteger(UInt8(1)) // Set compression flag - - // Write zeroes as length - we'll write the actual compressed size after compression. - let lengthIndex = self.writeBuffer.writerIndex - self.writeBuffer.writeInteger(UInt32(0)) - - // Compress and overwrite the payload length field with the right length. - do { - let writtenBytes = try compressor.compress(message, into: &self.writeBuffer) - self.writeBuffer.setInteger(UInt32(writtenBytes), at: lengthIndex) - } catch let zlibError { - throw RPCError(code: .internalError, message: "Compression failed", cause: zlibError) - } - } else { - self.writeBuffer.writeMultipleIntegers( - UInt8(0), // Clear compression flag - UInt32(message.count) // Set message length - ) - self.writeBuffer.writeBytes(message) - } - } -} diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift deleted file mode 100644 index a040a4524..000000000 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ /dev/null @@ -1,1928 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore -internal import NIOCore -internal import NIOHPACK -internal import NIOHTTP1 - -package enum Scheme: String { - case http - case https -} - -enum GRPCStreamStateMachineConfiguration { - case client(ClientConfiguration) - case server(ServerConfiguration) - - struct ClientConfiguration { - var methodDescriptor: MethodDescriptor - var scheme: Scheme - var outboundEncoding: 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: CompressionAlgorithmSet - - init(scheme: Scheme, acceptedEncodings: CompressionAlgorithmSet) { - self.scheme = scheme - self.acceptedEncodings = acceptedEncodings.union(.none) - } - } -} - -private enum GRPCStreamStateMachineState { - case clientIdleServerIdle(ClientIdleServerIdleState) - case clientOpenServerIdle(ClientOpenServerIdleState) - case clientOpenServerOpen(ClientOpenServerOpenState) - case clientOpenServerClosed(ClientOpenServerClosedState) - case clientClosedServerIdle(ClientClosedServerIdleState) - case clientClosedServerOpen(ClientClosedServerOpenState) - case clientClosedServerClosed(ClientClosedServerClosedState) - case _modifying - - struct ClientIdleServerIdleState { - let maxPayloadSize: Int - } - - struct ClientOpenServerIdleState { - let maxPayloadSize: Int - var framer: GRPCMessageFramer - var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm - - // The deframer must be optional because the client will not have one configured - // until the server opens and sends a grpc-encoding header. - // It will be present for the server though, because even though it's idle, - // it can still receive compressed messages from the client. - var deframer: GRPCMessageDeframer? - var decompressor: Zlib.Decompressor? - - var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - - // Store the headers received from the remote peer, its storage can be reused when sending - // headers back to the remote peer. - var headers: HPACKHeaders - - init( - previousState: ClientIdleServerIdleState, - compressor: Zlib.Compressor?, - outboundCompression: CompressionAlgorithm, - framer: GRPCMessageFramer, - decompressor: Zlib.Decompressor?, - deframer: GRPCMessageDeframer?, - headers: HPACKHeaders - ) { - self.maxPayloadSize = previousState.maxPayloadSize - self.compressor = compressor - self.outboundCompression = outboundCompression - self.framer = framer - self.decompressor = decompressor - self.deframer = deframer - self.inboundMessageBuffer = .init() - self.headers = headers - } - } - - struct ClientOpenServerOpenState { - var framer: GRPCMessageFramer - var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm - - var deframer: GRPCMessageDeframer - var decompressor: Zlib.Decompressor? - - var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - - // Store the headers received from the remote peer, its storage can be reused when sending - // headers back to the remote peer. - var headers: HPACKHeaders - - init( - previousState: ClientOpenServerIdleState, - deframer: GRPCMessageDeframer, - decompressor: Zlib.Decompressor? - ) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - - self.deframer = deframer - self.decompressor = decompressor - - self.inboundMessageBuffer = previousState.inboundMessageBuffer - self.headers = previousState.headers - } - } - - struct ClientOpenServerClosedState { - var framer: GRPCMessageFramer? - var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm - - let deframer: GRPCMessageDeframer? - var decompressor: Zlib.Decompressor? - - var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - - // This transition should only happen on the server-side when, upon receiving - // initial client metadata, some of the headers are invalid and we must reject - // the RPC. - // We will mark the client as open (because it sent initial metadata albeit - // invalid) but we'll close the server, meaning all future messages sent from - // the client will be ignored. Because of this, we won't need to frame or - // deframe any messages, as we won't be reading or writing any messages. - init(previousState: ClientIdleServerIdleState) { - self.framer = nil - self.compressor = nil - self.outboundCompression = .none - self.deframer = nil - self.decompressor = nil - self.inboundMessageBuffer = .init() - } - - init(previousState: ClientOpenServerOpenState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.deframer = previousState.deframer - self.decompressor = previousState.decompressor - self.inboundMessageBuffer = previousState.inboundMessageBuffer - } - - init(previousState: ClientOpenServerIdleState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.inboundMessageBuffer = previousState.inboundMessageBuffer - // The server went directly from idle to closed - this means it sent a - // trailers-only response: - // - if we're the client, the previous state was a nil deframer, but that - // is okay because we don't need a deframer as the server won't be sending - // any messages; - // - if we're the server, we'll keep whatever deframer we had. - self.deframer = previousState.deframer - self.decompressor = previousState.decompressor - } - } - - struct ClientClosedServerIdleState { - let maxPayloadSize: Int - var framer: GRPCMessageFramer - var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm - - let deframer: GRPCMessageDeframer? - var decompressor: Zlib.Decompressor? - - var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - - // Store the headers received from the remote peer, its storage can be reused when sending - // headers back to the remote peer. - var headers: HPACKHeaders - - /// This transition should only happen on the client-side. - /// It can happen if the request times out before the client outbound can be opened, or if the stream is - /// unexpectedly closed for some other reason on the client before it can transition to open. - init(previousState: ClientIdleServerIdleState) { - self.maxPayloadSize = previousState.maxPayloadSize - // We don't need a compressor since we won't be sending any messages. - self.framer = GRPCMessageFramer() - self.compressor = nil - self.outboundCompression = .none - - // We haven't received anything from the server. - self.deframer = nil - self.decompressor = nil - - self.inboundMessageBuffer = .init() - self.headers = [:] - } - - /// This transition should only happen on the server-side. - /// We are closing the client as soon as it opens (i.e., endStream was set when receiving the client's - /// initial metadata). We don't need to know a decompression algorithm, since we won't receive - /// any more messages from the client anyways, as it's closed. - init( - previousState: ClientIdleServerIdleState, - compressionAlgorithm: CompressionAlgorithm, - headers: HPACKHeaders - ) { - self.maxPayloadSize = previousState.maxPayloadSize - - if let zlibMethod = Zlib.Method(encoding: compressionAlgorithm) { - self.compressor = Zlib.Compressor(method: zlibMethod) - self.outboundCompression = compressionAlgorithm - } else { - self.compressor = nil - self.outboundCompression = .none - } - self.framer = GRPCMessageFramer() - // We don't need a deframer since we won't receive any messages from the - // client: it's closed. - self.deframer = nil - self.inboundMessageBuffer = .init() - self.headers = headers - } - - init(previousState: ClientOpenServerIdleState) { - self.maxPayloadSize = previousState.maxPayloadSize - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.deframer = previousState.deframer - self.decompressor = previousState.decompressor - self.inboundMessageBuffer = previousState.inboundMessageBuffer - self.headers = previousState.headers - } - } - - struct ClientClosedServerOpenState { - var framer: GRPCMessageFramer - var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm - - var deframer: GRPCMessageDeframer? - var decompressor: Zlib.Decompressor? - - var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - - // Store the headers received from the remote peer, its storage can be reused when sending - // headers back to the remote peer. - var headers: HPACKHeaders - - init(previousState: ClientOpenServerOpenState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.deframer = previousState.deframer - self.decompressor = previousState.decompressor - self.inboundMessageBuffer = previousState.inboundMessageBuffer - self.headers = previousState.headers - } - - /// This should be called from the server path, as the deframer will already be configured in this scenario. - init(previousState: ClientClosedServerIdleState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - - // In the case of the server, we don't need to deframe/decompress any more - // messages, since the client's closed. - self.deframer = nil - self.decompressor = nil - - self.inboundMessageBuffer = previousState.inboundMessageBuffer - self.headers = previousState.headers - } - - /// This should only be called from the client path, as the deframer has not yet been set up. - init( - previousState: ClientClosedServerIdleState, - decompressionAlgorithm: CompressionAlgorithm - ) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - - // In the case of the client, it will only be able to set up the deframer - // after it receives the chosen encoding from the server. - if let zlibMethod = Zlib.Method(encoding: decompressionAlgorithm) { - self.decompressor = Zlib.Decompressor(method: zlibMethod) - } - - self.deframer = GRPCMessageDeframer( - maxPayloadSize: previousState.maxPayloadSize, - decompressor: self.decompressor - ) - - self.inboundMessageBuffer = previousState.inboundMessageBuffer - self.headers = previousState.headers - } - } - - struct ClientClosedServerClosedState { - // We still need the framer and compressor in case the server has closed - // but its buffer is not yet empty and still needs to send messages out to - // the client. - var framer: GRPCMessageFramer? - var compressor: Zlib.Compressor? - var outboundCompression: CompressionAlgorithm - - // These are already deframed, so we don't need the deframer anymore. - var inboundMessageBuffer: OneOrManyQueue<[UInt8]> - - // This transition should only happen on the server-side when, upon receiving - // initial client metadata, some of the headers are invalid and we must reject - // the RPC. - // We will mark the client as closed (because it set the EOS flag, even if - // the initial metadata was invalid) and we'll close the server too. - // Because of this, we won't need to frame any messages, as we - // won't be writing any messages. - init(previousState: ClientIdleServerIdleState) { - self.framer = nil - self.compressor = nil - self.outboundCompression = .none - self.inboundMessageBuffer = .init() - } - - init(previousState: ClientClosedServerOpenState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.inboundMessageBuffer = previousState.inboundMessageBuffer - } - - init(previousState: ClientClosedServerIdleState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.inboundMessageBuffer = previousState.inboundMessageBuffer - } - - init(previousState: ClientOpenServerIdleState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.inboundMessageBuffer = previousState.inboundMessageBuffer - } - - init(previousState: ClientOpenServerOpenState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.inboundMessageBuffer = previousState.inboundMessageBuffer - } - - init(previousState: ClientOpenServerClosedState) { - self.framer = previousState.framer - self.compressor = previousState.compressor - self.outboundCompression = previousState.outboundCompression - self.inboundMessageBuffer = previousState.inboundMessageBuffer - } - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -struct GRPCStreamStateMachine { - private var state: GRPCStreamStateMachineState - private var configuration: GRPCStreamStateMachineConfiguration - private var skipAssertions: Bool - - struct InvalidState: Error { - var message: String - init(_ message: String) { - self.message = message - } - } - - init( - configuration: GRPCStreamStateMachineConfiguration, - maxPayloadSize: Int, - skipAssertions: Bool = false - ) { - self.state = .clientIdleServerIdle(.init(maxPayloadSize: maxPayloadSize)) - self.configuration = configuration - self.skipAssertions = skipAssertions - } - - mutating func send(metadata: Metadata) throws(InvalidState) -> HPACKHeaders { - switch self.configuration { - case .client(let clientConfiguration): - return try self.clientSend(metadata: metadata, configuration: clientConfiguration) - case .server(let serverConfiguration): - return try self.serverSend(metadata: metadata, configuration: serverConfiguration) - } - } - - mutating func send(message: [UInt8], promise: EventLoopPromise?) throws(InvalidState) { - switch self.configuration { - case .client: - try self.clientSend(message: message, promise: promise) - case .server: - try self.serverSend(message: message, promise: promise) - } - } - - mutating func closeOutbound() throws(InvalidState) { - switch self.configuration { - case .client: - try self.clientCloseOutbound() - case .server: - try self.invalidState("Server cannot call close: it must send status and trailers.") - } - } - - mutating func send( - status: Status, - metadata: Metadata - ) throws(InvalidState) -> HPACKHeaders { - switch self.configuration { - case .client: - try self.invalidState( - "Client cannot send status and trailer." - ) - case .server: - return try self.serverSend( - status: status, - customMetadata: metadata - ) - } - } - - enum OnMetadataReceived: Equatable { - case receivedMetadata(Metadata, MethodDescriptor?) - case doNothing - - // Client-specific actions - case receivedStatusAndMetadata_clientOnly(status: Status, metadata: Metadata) - - // Server-specific actions - case rejectRPC_serverOnly(trailers: HPACKHeaders) - case protocolViolation_serverOnly - } - - mutating func receive( - headers: HPACKHeaders, - endStream: Bool - ) throws(InvalidState) -> OnMetadataReceived { - switch self.configuration { - case .client(let clientConfiguration): - return try self.clientReceive( - headers: headers, - endStream: endStream, - configuration: clientConfiguration - ) - case .server(let serverConfiguration): - return try self.serverReceive( - headers: headers, - endStream: endStream, - configuration: serverConfiguration - ) - } - } - - enum OnBufferReceivedAction: Equatable { - case readInbound - case doNothing - - // This will be returned when the server sends a data frame with EOS set. - // This is invalid as per the protocol specification, because the server - // can only close by sending trailers, not by setting EOS when sending - // a message. - case endRPCAndForwardErrorStatus_clientOnly(Status) - - case forwardErrorAndClose_serverOnly(RPCError) - } - - mutating func receive( - buffer: ByteBuffer, - endStream: Bool - ) throws(InvalidState) -> OnBufferReceivedAction { - switch self.configuration { - case .client: - return try self.clientReceive(buffer: buffer, endStream: endStream) - case .server: - return try self.serverReceive(buffer: buffer, endStream: endStream) - } - } - - /// The result of requesting the next outbound frame, which may contain multiple messages. - enum OnNextOutboundFrame { - /// Either the receiving party is closed, so we shouldn't send any more frames; or the sender is done - /// writing messages (i.e. we are now closed). - case noMoreMessages - /// There isn't a frame ready to be sent, but we could still receive more messages, so keep trying. - case awaitMoreMessages - /// A frame is ready to be sent. - case sendFrame( - frame: ByteBuffer, - promise: EventLoopPromise? - ) - case closeAndFailPromise(EventLoopPromise?, RPCError) - - init(result: Result, promise: EventLoopPromise?) { - switch result { - case .success(let buffer): - self = .sendFrame(frame: buffer, promise: promise) - case .failure(let error): - self = .closeAndFailPromise(promise, error) - } - } - } - - mutating func nextOutboundFrame() throws(InvalidState) -> OnNextOutboundFrame { - switch self.configuration { - case .client: - return try self.clientNextOutboundFrame() - case .server: - return try self.serverNextOutboundFrame() - } - } - - /// The result of requesting the next inbound message. - enum OnNextInboundMessage: Equatable { - /// The sender is done writing messages and there are no more messages to be received. - case noMoreMessages - /// There isn't a message ready to be sent, but we could still receive more, so keep trying. - case awaitMoreMessages - /// A message has been received. - case receiveMessage([UInt8]) - } - - mutating func nextInboundMessage() -> OnNextInboundMessage { - switch self.configuration { - case .client: - return self.clientNextInboundMessage() - case .server: - return self.serverNextInboundMessage() - } - } - - mutating func tearDown() { - switch self.state { - case .clientIdleServerIdle: - () - case .clientOpenServerIdle(let state): - state.compressor?.end() - state.decompressor?.end() - case .clientOpenServerOpen(let state): - state.compressor?.end() - state.decompressor?.end() - case .clientOpenServerClosed(let state): - state.compressor?.end() - state.decompressor?.end() - case .clientClosedServerIdle(let state): - state.compressor?.end() - state.decompressor?.end() - case .clientClosedServerOpen(let state): - state.compressor?.end() - state.decompressor?.end() - case .clientClosedServerClosed(let state): - state.compressor?.end() - case ._modifying: - preconditionFailure() - } - } - - enum OnUnexpectedInboundClose { - case forwardStatus_clientOnly(Status) - case fireError_serverOnly(any Error) - case doNothing - - init(serverCloseReason: UnexpectedInboundCloseReason) { - switch serverCloseReason { - case .streamReset, .channelInactive: - self = .fireError_serverOnly(RPCError(serverCloseReason)) - case .errorThrown(let error): - self = .fireError_serverOnly(error) - } - } - } - - enum UnexpectedInboundCloseReason { - case streamReset - case channelInactive - case errorThrown(any Error) - } - - mutating func unexpectedInboundClose( - reason: UnexpectedInboundCloseReason - ) -> OnUnexpectedInboundClose { - switch self.configuration { - case .client: - return self.clientUnexpectedInboundClose(reason: reason) - case .server: - return self.serverUnexpectedInboundClose(reason: reason) - } - } -} - -// - MARK: Client - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCStreamStateMachine { - private func makeClientHeaders( - methodDescriptor: MethodDescriptor, - scheme: Scheme, - outboundEncoding: CompressionAlgorithm?, - acceptedEncodings: CompressionAlgorithmSet, - customMetadata: Metadata - ) -> HPACKHeaders { - var headers = HPACKHeaders() - headers.reserveCapacity(7 + customMetadata.count) - - // Add required headers. - // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests - - // The order is important here: reserved HTTP2 headers (those starting with `:`) - // must come before all other headers. - headers.add("POST", forKey: .method) - headers.add(scheme.rawValue, forKey: .scheme) - headers.add(methodDescriptor.path, forKey: .path) - - // Add required gRPC headers. - headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) - headers.add("trailers", forKey: .te) // Used to detect incompatible proxies - - if let encoding = outboundEncoding, encoding != .none { - headers.add(encoding.name, forKey: .encoding) - } - - for encoding in acceptedEncodings.elements.filter({ $0 != .none }) { - headers.add(encoding.name, forKey: .acceptEncoding) - } - - for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) - } - - return headers - } - - private mutating func clientSend( - metadata: Metadata, - configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration - ) throws(InvalidState) -> HPACKHeaders { - // Client sends metadata only when opening the stream. - switch self.state { - case .clientIdleServerIdle(let state): - let outboundEncoding = configuration.outboundEncoding - let compressor = Zlib.Method(encoding: outboundEncoding) - .flatMap { Zlib.Compressor(method: $0) } - self.state = .clientOpenServerIdle( - .init( - previousState: state, - compressor: compressor, - outboundCompression: outboundEncoding, - framer: GRPCMessageFramer(), - decompressor: nil, - deframer: nil, - headers: [:] - ) - ) - return self.makeClientHeaders( - methodDescriptor: configuration.methodDescriptor, - scheme: configuration.scheme, - outboundEncoding: configuration.outboundEncoding, - acceptedEncodings: configuration.acceptedEncodings, - customMetadata: metadata - ) - case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - try self.invalidState( - "Client is already open: shouldn't be sending metadata." - ) - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState( - "Client is closed: can't send metadata." - ) - case ._modifying: - preconditionFailure() - } - } - - private mutating func clientSend( - message: [UInt8], - promise: EventLoopPromise? - ) throws(InvalidState) { - switch self.state { - case .clientIdleServerIdle: - try self.invalidState("Client not yet open.") - - case .clientOpenServerIdle(var state): - self.state = ._modifying - state.framer.append(message, promise: promise) - self.state = .clientOpenServerIdle(state) - - case .clientOpenServerOpen(var state): - self.state = ._modifying - state.framer.append(message, promise: promise) - self.state = .clientOpenServerOpen(state) - - case .clientOpenServerClosed: - // The server has closed, so it makes no sense to send the rest of the request. - () - - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState( - "Client is closed, cannot send a message." - ) - - case ._modifying: - preconditionFailure() - } - } - - private mutating func clientCloseOutbound() throws(InvalidState) { - switch self.state { - case .clientIdleServerIdle(let state): - self.state = .clientClosedServerIdle(.init(previousState: state)) - case .clientOpenServerIdle(let state): - self.state = .clientClosedServerIdle(.init(previousState: state)) - case .clientOpenServerOpen(let state): - self.state = .clientClosedServerOpen(.init(previousState: state)) - case .clientOpenServerClosed(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - // Client is already closed - nothing to do. - () - case ._modifying: - preconditionFailure() - } - } - - /// Returns the client's next request to the server. - /// - Returns: The request to be made to the server. - private mutating func clientNextOutboundFrame() throws(InvalidState) -> OnNextOutboundFrame { - - switch self.state { - case .clientIdleServerIdle: - try self.invalidState("Client is not open yet.") - - case .clientOpenServerIdle(var state): - self.state = ._modifying - let next = state.framer.nextResult(compressor: state.compressor) - self.state = .clientOpenServerIdle(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .awaitMoreMessages - } - - case .clientOpenServerOpen(var state): - self.state = ._modifying - let next = state.framer.nextResult(compressor: state.compressor) - self.state = .clientOpenServerOpen(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .awaitMoreMessages - } - - case .clientClosedServerIdle(var state): - self.state = ._modifying - let next = state.framer.nextResult(compressor: state.compressor) - self.state = .clientClosedServerIdle(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .noMoreMessages - } - - case .clientClosedServerOpen(var state): - self.state = ._modifying - let next = state.framer.nextResult(compressor: state.compressor) - self.state = .clientClosedServerOpen(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .noMoreMessages - } - - case .clientOpenServerClosed, .clientClosedServerClosed: - // No point in sending any more requests if the server is closed. - return .noMoreMessages - - case ._modifying: - preconditionFailure() - } - } - - private enum ServerHeadersValidationResult { - case valid - case invalid(OnMetadataReceived) - } - - private mutating func clientValidateHeadersReceivedFromServer( - _ metadata: HPACKHeaders - ) -> ServerHeadersValidationResult { - var httpStatus: String? { - metadata.firstString(forKey: .status) - } - var grpcStatus: Status.Code? { - metadata.firstString(forKey: .grpcStatus) - .flatMap { Int($0) } - .flatMap { Status.Code(rawValue: $0) } - } - guard httpStatus == "200" || grpcStatus != nil else { - let httpStatusCode = - httpStatus - .flatMap { Int($0) } - .map { HTTPResponseStatus(statusCode: $0) } - - guard let httpStatusCode else { - return .invalid( - .receivedStatusAndMetadata_clientOnly( - status: .init(code: .unknown, message: "HTTP Status Code is missing."), - metadata: Metadata(headers: metadata) - ) - ) - } - - if (100 ... 199).contains(httpStatusCode.code) { - // For 1xx status codes, the entire header should be skipped and a - // subsequent header should be read. - // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - return .invalid(.doNothing) - } - - // Forward the mapped status code. - return .invalid( - .receivedStatusAndMetadata_clientOnly( - status: .init( - code: Status.Code(httpStatusCode: httpStatusCode), - message: "Unexpected non-200 HTTP Status Code." - ), - metadata: Metadata(headers: metadata) - ) - ) - } - - let contentTypeHeader = metadata.first(name: GRPCHTTP2Keys.contentType.rawValue) - guard contentTypeHeader.flatMap(ContentType.init) != nil else { - return .invalid( - .receivedStatusAndMetadata_clientOnly( - status: .init( - code: .internalError, - message: "Missing \(GRPCHTTP2Keys.contentType.rawValue) header" - ), - metadata: Metadata(headers: metadata) - ) - ) - } - - return .valid - } - - private enum ProcessInboundEncodingResult { - case error(OnMetadataReceived) - case success(CompressionAlgorithm) - } - - private func processInboundEncoding( - headers: HPACKHeaders, - configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration - ) -> ProcessInboundEncodingResult { - let inboundEncoding: CompressionAlgorithm - if let serverEncoding = headers.first(name: GRPCHTTP2Keys.encoding.rawValue) { - guard let parsedEncoding = CompressionAlgorithm(name: serverEncoding), - configuration.acceptedEncodings.contains(parsedEncoding) - else { - return .error( - .receivedStatusAndMetadata_clientOnly( - status: .init( - code: .internalError, - message: - "The server picked a compression algorithm ('\(serverEncoding)') the client does not know about." - ), - metadata: Metadata(headers: headers) - ) - ) - } - inboundEncoding = parsedEncoding - } else { - inboundEncoding = .none - } - return .success(inboundEncoding) - } - - private func validateTrailers( - _ trailers: HPACKHeaders - ) throws(InvalidState) -> OnMetadataReceived { - let statusValue = trailers.firstString(forKey: .grpcStatus) - let statusCode = statusValue.flatMap { - Int($0) - }.flatMap { - Status.Code(rawValue: $0) - } - - let status: Status - if let code = statusCode { - let messageFieldValue = trailers.firstString(forKey: .grpcStatusMessage, canonicalForm: false) - let message = messageFieldValue.map { GRPCStatusMessageMarshaller.unmarshall($0) } ?? "" - status = Status(code: code, message: message) - } else { - let message: String - if let statusValue = statusValue { - message = "Invalid 'grpc-status' in trailers (\(statusValue))" - } else { - message = "No 'grpc-status' value in trailers" - } - status = Status(code: .unknown, message: message) - } - - var convertedMetadata = Metadata(headers: trailers) - convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) - convertedMetadata.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) - - return .receivedStatusAndMetadata_clientOnly(status: status, metadata: convertedMetadata) - } - - private mutating func clientReceive( - headers: HPACKHeaders, - endStream: Bool, - configuration: GRPCStreamStateMachineConfiguration.ClientConfiguration - ) throws(InvalidState) -> OnMetadataReceived { - switch self.state { - case .clientOpenServerIdle(let state): - switch (self.clientValidateHeadersReceivedFromServer(headers), endStream) { - case (.invalid(let action), true): - // The headers are invalid, but the server signalled that it was - // closing the stream, so close both client and server. - self.state = .clientClosedServerClosed(.init(previousState: state)) - return action - case (.invalid(let action), false): - self.state = .clientClosedServerIdle(.init(previousState: state)) - return action - case (.valid, true): - // This is a trailers-only response: close server. - self.state = .clientOpenServerClosed(.init(previousState: state)) - return try self.validateTrailers(headers) - case (.valid, false): - switch self.processInboundEncoding(headers: headers, configuration: configuration) { - case .error(let failure): - return failure - case .success(let inboundEncoding): - let decompressor = Zlib.Method(encoding: inboundEncoding) - .flatMap { Zlib.Decompressor(method: $0) } - - self.state = .clientOpenServerOpen( - .init( - previousState: state, - deframer: GRPCMessageDeframer( - maxPayloadSize: state.maxPayloadSize, - decompressor: decompressor - ), - decompressor: decompressor - ) - ) - return .receivedMetadata(Metadata(headers: headers), nil) - } - } - - case .clientOpenServerOpen(let state): - // This state is valid even if endStream is not set: server can send - // trailing metadata without END_STREAM set, and follow it with an - // empty message frame where it is set. - // However, we must make sure that grpc-status is set, otherwise this - // is an invalid state. - if endStream { - self.state = .clientOpenServerClosed(.init(previousState: state)) - } - return try self.validateTrailers(headers) - - case .clientClosedServerIdle(let state): - switch (self.clientValidateHeadersReceivedFromServer(headers), endStream) { - case (.invalid(let action), true): - // The headers are invalid, but the server signalled that it was - // closing the stream, so close the server side too. - self.state = .clientClosedServerClosed(.init(previousState: state)) - return action - case (.invalid(let action), false): - // Client is already closed, so we don't need to update our state. - return action - case (.valid, true): - // This is a trailers-only response: close server. - self.state = .clientClosedServerClosed(.init(previousState: state)) - return try self.validateTrailers(headers) - case (.valid, false): - switch self.processInboundEncoding(headers: headers, configuration: configuration) { - case .error(let failure): - return failure - case .success(let inboundEncoding): - self.state = .clientClosedServerOpen( - .init( - previousState: state, - decompressionAlgorithm: inboundEncoding - ) - ) - return .receivedMetadata(Metadata(headers: headers), nil) - } - } - - case .clientClosedServerOpen(let state): - // This state is valid even if endStream is not set: server can send - // trailing metadata without END_STREAM set, and follow it with an - // empty message frame where it is set. - // However, we must make sure that grpc-status is set, otherwise this - // is an invalid state. - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } - return try self.validateTrailers(headers) - - case .clientClosedServerClosed: - // We could end up here if we received a grpc-status header in a previous - // frame (which would have already close the server) and then we receive - // an empty frame with EOS set. - // We wouldn't want to throw in that scenario, so we just ignore it. - // Note that we don't want to ignore it if EOS is not set here though, as - // then it would be an invalid payload. - if !endStream || headers.count > 0 { - try self.invalidState( - "Server is closed, nothing could have been sent." - ) - } - return .doNothing - case .clientIdleServerIdle: - try self.invalidState( - "Server cannot have sent metadata if the client is idle." - ) - case .clientOpenServerClosed: - try self.invalidState( - "Server is closed, nothing could have been sent." - ) - case ._modifying: - preconditionFailure() - } - } - - private mutating func clientReceive( - buffer: ByteBuffer, - endStream: Bool - ) throws(InvalidState) -> OnBufferReceivedAction { - // This is a message received by the client, from the server. - switch self.state { - case .clientIdleServerIdle: - try self.invalidState( - "Cannot have received anything from server if client is not yet open." - ) - - case .clientOpenServerIdle, .clientClosedServerIdle: - try self.invalidState( - "Server cannot have sent a message before sending the initial metadata." - ) - - case .clientOpenServerOpen(var state): - self.state = ._modifying - if endStream { - // This is invalid as per the protocol specification, because the server - // can only close by sending trailers, not by setting EOS when sending - // a message. - self.state = .clientClosedServerClosed(.init(previousState: state)) - return .endRPCAndForwardErrorStatus_clientOnly( - Status( - code: .internalError, - message: """ - Server sent EOS alongside a data frame, but server is only allowed \ - to close by sending status and trailers. - """ - ) - ) - } - - state.deframer.append(buffer) - - do { - try state.deframer.decode(into: &state.inboundMessageBuffer) - self.state = .clientOpenServerOpen(state) - return .readInbound - } catch { - self.state = .clientOpenServerOpen(state) - let status = Status(code: .internalError, message: "Failed to decode message") - return .endRPCAndForwardErrorStatus_clientOnly(status) - } - - case .clientClosedServerOpen(var state): - self.state = ._modifying - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - return .endRPCAndForwardErrorStatus_clientOnly( - Status( - code: .internalError, - message: """ - Server sent EOS alongside a data frame, but server is only allowed \ - to close by sending status and trailers. - """ - ) - ) - } - - // The client may have sent the end stream and thus it's closed, - // but the server may still be responding. - // The client must have a deframer set up, so force-unwrap is okay. - do { - state.deframer!.append(buffer) - try state.deframer!.decode(into: &state.inboundMessageBuffer) - self.state = .clientClosedServerOpen(state) - return .readInbound - } catch { - self.state = .clientClosedServerOpen(state) - let status = Status(code: .internalError, message: "Failed to decode message") - return .endRPCAndForwardErrorStatus_clientOnly(status) - } - - case .clientOpenServerClosed, .clientClosedServerClosed: - try self.invalidState( - "Cannot have received anything from a closed server." - ) - case ._modifying: - preconditionFailure() - } - } - - private mutating func clientNextInboundMessage() -> OnNextInboundMessage { - switch self.state { - case .clientOpenServerOpen(var state): - self.state = ._modifying - let message = state.inboundMessageBuffer.pop() - self.state = .clientOpenServerOpen(state) - return message.map { .receiveMessage($0) } ?? .awaitMoreMessages - - case .clientOpenServerClosed(var state): - self.state = ._modifying - let message = state.inboundMessageBuffer.pop() - self.state = .clientOpenServerClosed(state) - return message.map { .receiveMessage($0) } ?? .noMoreMessages - - case .clientClosedServerOpen(var state): - self.state = ._modifying - let message = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerOpen(state) - return message.map { .receiveMessage($0) } ?? .awaitMoreMessages - - case .clientClosedServerClosed(var state): - self.state = ._modifying - let message = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerClosed(state) - return message.map { .receiveMessage($0) } ?? .noMoreMessages - - case .clientIdleServerIdle, - .clientOpenServerIdle, - .clientClosedServerIdle: - return .awaitMoreMessages - case ._modifying: - preconditionFailure() - } - } - - private func invalidState(_ message: String, line: UInt = #line) throws(InvalidState) -> Never { - if !self.skipAssertions { - assertionFailure(message, line: line) - } - throw InvalidState(message) - } - - private mutating func clientUnexpectedInboundClose( - reason: UnexpectedInboundCloseReason - ) -> OnUnexpectedInboundClose { - switch self.state { - case .clientIdleServerIdle(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return .forwardStatus_clientOnly(Status(RPCError(reason))) - - case .clientOpenServerIdle(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return .forwardStatus_clientOnly(Status(RPCError(reason))) - - case .clientClosedServerIdle(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return .forwardStatus_clientOnly(Status(RPCError(reason))) - - case .clientOpenServerOpen(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return .forwardStatus_clientOnly(Status(RPCError(reason))) - - case .clientClosedServerOpen(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return .forwardStatus_clientOnly(Status(RPCError(reason))) - - case .clientOpenServerClosed, .clientClosedServerClosed: - return .doNothing - - case ._modifying: - preconditionFailure() - } - } -} - -// - MARK: Server - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCStreamStateMachine { - private func formResponseHeaders( - in headers: inout HPACKHeaders, - outboundEncoding: CompressionAlgorithm?, - configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration, - customMetadata: Metadata - ) { - headers.removeAll(keepingCapacity: true) - - // Response headers always contain :status (HTTP Status 200) and content-type. - // They may also contain grpc-encoding, grpc-accept-encoding, and custom metadata. - headers.reserveCapacity(4 + customMetadata.count) - - headers.add("200", forKey: .status) - headers.add(ContentType.grpc.canonicalValue, forKey: .contentType) - - if let outboundEncoding, outboundEncoding != .none { - headers.add(outboundEncoding.name, forKey: .encoding) - } - - for metadataPair in customMetadata { - headers.add(name: metadataPair.key, value: metadataPair.value.encoded()) - } - } - - private mutating func serverSend( - metadata: Metadata, - configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration - ) throws(InvalidState) -> HPACKHeaders { - // Server sends initial metadata - switch self.state { - case .clientOpenServerIdle(var state): - self.state = ._modifying - let outboundEncoding = state.outboundCompression - self.formResponseHeaders( - in: &state.headers, - outboundEncoding: outboundEncoding, - configuration: configuration, - customMetadata: metadata - ) - - self.state = .clientOpenServerOpen( - .init( - previousState: state, - // In the case of the server, it will already have a deframer set up, - // because it already knows what encoding the client is using: - // it's okay to force-unwrap. - deframer: state.deframer!, - decompressor: state.decompressor - ) - ) - - return state.headers - - case .clientClosedServerIdle(var state): - self.state = ._modifying - let outboundEncoding = state.outboundCompression - self.formResponseHeaders( - in: &state.headers, - outboundEncoding: outboundEncoding, - configuration: configuration, - customMetadata: metadata - ) - self.state = .clientClosedServerOpen(.init(previousState: state)) - return state.headers - - case .clientIdleServerIdle: - try self.invalidState( - "Client cannot be idle if server is sending initial metadata: it must have opened." - ) - case .clientOpenServerClosed, .clientClosedServerClosed: - try self.invalidState( - "Server cannot send metadata if closed." - ) - case .clientOpenServerOpen, .clientClosedServerOpen: - try self.invalidState( - "Server has already sent initial metadata." - ) - case ._modifying: - preconditionFailure() - } - } - - private mutating func serverSend( - message: [UInt8], - promise: EventLoopPromise? - ) throws(InvalidState) { - switch self.state { - case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - try self.invalidState( - "Server must have sent initial metadata before sending a message." - ) - - case .clientOpenServerOpen(var state): - self.state = ._modifying - state.framer.append(message, promise: promise) - self.state = .clientOpenServerOpen(state) - - case .clientClosedServerOpen(var state): - self.state = ._modifying - state.framer.append(message, promise: promise) - self.state = .clientClosedServerOpen(state) - - case .clientOpenServerClosed, .clientClosedServerClosed: - try self.invalidState( - "Server can't send a message if it's closed." - ) - case ._modifying: - preconditionFailure() - } - } - - private mutating func serverSend( - status: Status, - customMetadata: Metadata - ) throws(InvalidState) -> HPACKHeaders { - // Close the server. - switch self.state { - case .clientOpenServerOpen(var state): - self.state = ._modifying - state.headers.formTrailers(status: status, metadata: customMetadata) - self.state = .clientOpenServerClosed(.init(previousState: state)) - return state.headers - - case .clientClosedServerOpen(var state): - self.state = ._modifying - state.headers.formTrailers(status: status, metadata: customMetadata) - self.state = .clientClosedServerClosed(.init(previousState: state)) - return state.headers - - case .clientOpenServerIdle(var state): - self.state = ._modifying - state.headers.formTrailersOnly(status: status, metadata: customMetadata) - self.state = .clientOpenServerClosed(.init(previousState: state)) - return state.headers - - case .clientClosedServerIdle(var state): - self.state = ._modifying - state.headers.formTrailersOnly(status: status, metadata: customMetadata) - self.state = .clientClosedServerClosed(.init(previousState: state)) - return state.headers - - case .clientIdleServerIdle: - try self.invalidState( - "Server can't send status if client is idle." - ) - case .clientOpenServerClosed, .clientClosedServerClosed: - try self.invalidState( - "Server can't send anything if closed." - ) - case ._modifying: - preconditionFailure() - } - } - - private mutating func serverReceive( - headers: HPACKHeaders, - endStream: Bool, - configuration: GRPCStreamStateMachineConfiguration.ServerConfiguration - ) throws(InvalidState) -> OnMetadataReceived { - func closeServer( - from state: GRPCStreamStateMachineState.ClientIdleServerIdleState, - endStream: Bool - ) -> GRPCStreamStateMachineState { - if endStream { - return .clientClosedServerClosed(.init(previousState: state)) - } else { - return .clientOpenServerClosed(.init(previousState: state)) - } - } - - switch self.state { - case .clientIdleServerIdle(let state): - let contentType = headers.firstString(forKey: .contentType) - .flatMap { ContentType(value: $0) } - if contentType == nil { - self.state = .clientOpenServerClosed(.init(previousState: state)) - - // Respond with HTTP-level Unsupported Media Type status code. - var trailers = HPACKHeaders() - trailers.add("415", forKey: .status) - return .rejectRPC_serverOnly(trailers: trailers) - } - - guard let pathHeader = headers.firstString(forKey: .path) else { - self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC_serverOnly( - trailers: .trailersOnly( - code: .invalidArgument, - message: "No \(GRPCHTTP2Keys.path.rawValue) header has been set." - ) - ) - } - - guard let path = MethodDescriptor(path: pathHeader) else { - self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC_serverOnly( - trailers: .trailersOnly( - code: .unimplemented, - message: - "The given \(GRPCHTTP2Keys.path.rawValue) (\(pathHeader)) does not correspond to a valid method." - ) - ) - } - - let scheme = headers.firstString(forKey: .scheme).flatMap { Scheme(rawValue: $0) } - if scheme == nil { - self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC_serverOnly( - trailers: .trailersOnly( - code: .invalidArgument, - message: ":scheme header must be present and one of \"http\" or \"https\"." - ) - ) - } - - guard let method = headers.firstString(forKey: .method), method == "POST" else { - self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC_serverOnly( - trailers: .trailersOnly( - code: .invalidArgument, - message: ":method header is expected to be present and have a value of \"POST\"." - ) - ) - } - - // Firstly, find out if we support the client's chosen encoding, and reject - // the RPC if we don't. - let inboundEncoding: CompressionAlgorithm - let encodingValues = headers.values( - forHeader: GRPCHTTP2Keys.encoding.rawValue, - canonicalForm: true - ) - var encodingValuesIterator = encodingValues.makeIterator() - if let rawEncoding = encodingValuesIterator.next() { - guard encodingValuesIterator.next() == nil else { - self.state = closeServer(from: state, endStream: endStream) - return .rejectRPC_serverOnly( - trailers: .trailersOnly( - code: .internalError, - message: "\(GRPCHTTP2Keys.encoding) must contain no more than one value." - ) - ) - } - - guard let clientEncoding = CompressionAlgorithm(name: rawEncoding), - configuration.acceptedEncodings.contains(clientEncoding) - else { - self.state = closeServer(from: state, endStream: endStream) - var trailers = HPACKHeaders.trailersOnly( - code: .unimplemented, - message: """ - \(rawEncoding) compression is not supported; \ - supported algorithms are listed in grpc-accept-encoding - """ - ) - - for acceptedEncoding in configuration.acceptedEncodings.elements { - trailers.add(name: GRPCHTTP2Keys.acceptEncoding.rawValue, value: acceptedEncoding.name) - } - - return .rejectRPC_serverOnly(trailers: trailers) - } - - // Server supports client's encoding. - inboundEncoding = clientEncoding - } else { - 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 = .none - let clientAdvertisedEncodings = headers.values( - forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue, - canonicalForm: true - ) - // Find the preferred encoding and use it to compress responses. - for clientAdvertisedEncoding in clientAdvertisedEncodings { - if let algorithm = CompressionAlgorithm(name: clientAdvertisedEncoding), - configuration.acceptedEncodings.contains(algorithm) - { - outboundEncoding = algorithm - break - } - } - - if endStream { - self.state = .clientClosedServerIdle( - .init( - previousState: state, - compressionAlgorithm: outboundEncoding, - headers: headers - ) - ) - } else { - let compressor = Zlib.Method(encoding: outboundEncoding) - .flatMap { Zlib.Compressor(method: $0) } - let decompressor = Zlib.Method(encoding: inboundEncoding) - .flatMap { Zlib.Decompressor(method: $0) } - - self.state = .clientOpenServerIdle( - .init( - previousState: state, - compressor: compressor, - outboundCompression: outboundEncoding, - framer: GRPCMessageFramer(), - decompressor: decompressor, - deframer: GRPCMessageDeframer( - maxPayloadSize: state.maxPayloadSize, - decompressor: decompressor - ), - headers: headers - ) - ) - } - - return .receivedMetadata(Metadata(headers: headers), path) - - case .clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed: - // Metadata has already been received, should only be sent once by clients. - return .protocolViolation_serverOnly - - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState("Client can't have sent metadata if closed.") - - case ._modifying: - preconditionFailure() - } - } - - private mutating func serverReceive( - buffer: ByteBuffer, - endStream: Bool - ) throws(InvalidState) -> OnBufferReceivedAction { - let action: OnBufferReceivedAction - - switch self.state { - case .clientIdleServerIdle: - try self.invalidState("Can't have received a message if client is idle.") - - case .clientOpenServerIdle(var state): - self.state = ._modifying - // Deframer must be present on the server side, as we know the decompression - // algorithm from the moment the client opens. - do { - state.deframer!.append(buffer) - try state.deframer!.decode(into: &state.inboundMessageBuffer) - action = .readInbound - } catch { - let error = RPCError(code: .internalError, message: "Failed to decode message") - action = .forwardErrorAndClose_serverOnly(error) - } - - if endStream { - self.state = .clientClosedServerIdle(.init(previousState: state)) - } else { - self.state = .clientOpenServerIdle(state) - } - - case .clientOpenServerOpen(var state): - self.state = ._modifying - do { - state.deframer.append(buffer) - try state.deframer.decode(into: &state.inboundMessageBuffer) - action = .readInbound - } catch { - let error = RPCError(code: .internalError, message: "Failed to decode message") - action = .forwardErrorAndClose_serverOnly(error) - } - - if endStream { - self.state = .clientClosedServerOpen(.init(previousState: state)) - } else { - self.state = .clientOpenServerOpen(state) - } - - case .clientOpenServerClosed(let state): - // Client is not done sending request, but server has already closed. - // Ignore the rest of the request: do nothing, unless endStream is set, - // in which case close the client. - if endStream { - self.state = .clientClosedServerClosed(.init(previousState: state)) - } - - action = .doNothing - - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - try self.invalidState("Client can't send a message if closed.") - - case ._modifying: - preconditionFailure() - } - - return action - } - - private mutating func serverNextOutboundFrame() throws(InvalidState) -> OnNextOutboundFrame { - switch self.state { - case .clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle: - try self.invalidState("Server is not open yet.") - - case .clientOpenServerOpen(var state): - self.state = ._modifying - let next = state.framer.nextResult(compressor: state.compressor) - self.state = .clientOpenServerOpen(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .awaitMoreMessages - } - - case .clientClosedServerOpen(var state): - self.state = ._modifying - let next = state.framer.nextResult(compressor: state.compressor) - self.state = .clientClosedServerOpen(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .awaitMoreMessages - } - - case .clientOpenServerClosed(var state): - self.state = ._modifying - let next = state.framer?.nextResult(compressor: state.compressor) - self.state = .clientOpenServerClosed(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .noMoreMessages - } - - case .clientClosedServerClosed(var state): - self.state = ._modifying - let next = state.framer?.nextResult(compressor: state.compressor) - self.state = .clientClosedServerClosed(state) - - if let next = next { - return OnNextOutboundFrame(result: next.result, promise: next.promise) - } else { - return .noMoreMessages - } - - case ._modifying: - preconditionFailure() - } - } - - private mutating func serverNextInboundMessage() -> OnNextInboundMessage { - switch self.state { - case .clientOpenServerIdle(var state): - self.state = ._modifying - let request = state.inboundMessageBuffer.pop() - self.state = .clientOpenServerIdle(state) - return request.map { .receiveMessage($0) } ?? .awaitMoreMessages - - case .clientOpenServerOpen(var state): - self.state = ._modifying - let request = state.inboundMessageBuffer.pop() - self.state = .clientOpenServerOpen(state) - return request.map { .receiveMessage($0) } ?? .awaitMoreMessages - - case .clientClosedServerIdle(var state): - self.state = ._modifying - let request = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerIdle(state) - return request.map { .receiveMessage($0) } ?? .noMoreMessages - - case .clientClosedServerOpen(var state): - self.state = ._modifying - let request = state.inboundMessageBuffer.pop() - self.state = .clientClosedServerOpen(state) - return request.map { .receiveMessage($0) } ?? .noMoreMessages - - case .clientOpenServerClosed, .clientClosedServerClosed: - // Server has closed, no need to read. - return .noMoreMessages - - case .clientIdleServerIdle: - return .awaitMoreMessages - - case ._modifying: - preconditionFailure() - } - } - - private mutating func serverUnexpectedInboundClose( - reason: UnexpectedInboundCloseReason - ) -> OnUnexpectedInboundClose { - switch self.state { - case .clientIdleServerIdle(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return OnUnexpectedInboundClose(serverCloseReason: reason) - - case .clientOpenServerIdle(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return OnUnexpectedInboundClose(serverCloseReason: reason) - - case .clientOpenServerOpen(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return OnUnexpectedInboundClose(serverCloseReason: reason) - - case .clientOpenServerClosed(let state): - self.state = .clientClosedServerClosed(.init(previousState: state)) - return OnUnexpectedInboundClose(serverCloseReason: reason) - - case .clientClosedServerIdle, .clientClosedServerOpen, .clientClosedServerClosed: - return .doNothing - - case ._modifying: - preconditionFailure() - } - } -} - -extension MethodDescriptor { - init?(path: String) { - var view = path[...] - guard view.popFirst() == "/" else { return nil } - - // Find the index of the "/" separating the service and method names. - guard var index = view.firstIndex(of: "/") else { return nil } - - let service = String(view[.. String? { - self.values(forHeader: key.rawValue, canonicalForm: canonicalForm).first(where: { _ in true }) - .map { - String($0) - } - } - - fileprivate mutating func add(_ value: String, forKey key: GRPCHTTP2Keys) { - self.add(name: key.rawValue, value: value) - } - - fileprivate static func trailersOnly(code: Status.Code, message: String) -> Self { - var trailers = HPACKHeaders() - HPACKHeaders.formTrailers( - &trailers, - isTrailersOnly: true, - status: Status(code: code, message: message), - metadata: [:] - ) - return trailers - } - - fileprivate mutating func formTrailersOnly(status: Status, metadata: Metadata = [:]) { - Self.formTrailers(&self, isTrailersOnly: true, status: status, metadata: metadata) - } - - fileprivate mutating func formTrailers(status: Status, metadata: Metadata = [:]) { - Self.formTrailers(&self, isTrailersOnly: false, status: status, metadata: metadata) - } - - private static func formTrailers( - _ trailers: inout HPACKHeaders, - isTrailersOnly: Bool, - status: Status, - metadata: Metadata - ) { - trailers.removeAll(keepingCapacity: true) - - if isTrailersOnly { - trailers.reserveCapacity(4 + metadata.count) - trailers.add("200", forKey: .status) - trailers.add(ContentType.grpc.canonicalValue, forKey: .contentType) - } else { - trailers.reserveCapacity(2 + metadata.count) - } - - trailers.add(String(status.code.rawValue), forKey: .grpcStatus) - if !status.message.isEmpty, let encoded = GRPCStatusMessageMarshaller.marshall(status.message) { - trailers.add(encoded, forKey: .grpcStatusMessage) - } - - for (key, value) in metadata { - trailers.add(name: key, value: value.encoded()) - } - } -} - -extension Zlib.Method { - init?(encoding: CompressionAlgorithm) { - switch encoding { - case .none: - return nil - case .deflate: - self = .deflate - case .gzip: - self = .gzip - default: - return nil - } - } -} - -extension Metadata { - init(headers: HPACKHeaders) { - var metadata = Metadata() - metadata.reserveCapacity(headers.count) - for header in headers { - if header.name.hasSuffix("-bin") { - do { - let decodedBinary = try header.value.base64Decoded() - metadata.addBinary(decodedBinary, forKey: header.name) - } catch { - metadata.addString(header.value, forKey: header.name) - } - } else { - metadata.addString(header.value, forKey: header.name) - } - } - self = metadata - } -} - -extension Status.Code { - // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - init(httpStatusCode: HTTPResponseStatus) { - switch httpStatusCode { - case .badRequest: - self = .internalError - case .unauthorized: - self = .unauthenticated - case .forbidden: - self = .permissionDenied - case .notFound: - self = .unimplemented - case .tooManyRequests, .badGateway, .serviceUnavailable, .gatewayTimeout: - self = .unavailable - default: - self = .unknown - } - } -} - -extension MethodDescriptor { - var path: String { - return "/\(self.service)/\(self.method)" - } -} - -extension RPCError { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - fileprivate init(_ reason: GRPCStreamStateMachine.UnexpectedInboundCloseReason) { - switch reason { - case .streamReset: - self = RPCError( - code: .unavailable, - message: "Stream unexpectedly closed: a RST_STREAM frame was received." - ) - case .channelInactive: - self = RPCError(code: .unavailable, message: "Stream unexpectedly closed.") - case .errorThrown: - self = RPCError(code: .unavailable, message: "Stream unexpectedly closed with error.") - } - } -} - -extension Status { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - fileprivate init(_ error: RPCError) { - self = Status(code: Status.Code(error.code), message: error.message) - } -} - -extension RPCError { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - init(_ invalidState: GRPCStreamStateMachine.InvalidState) { - self = RPCError(code: .internalError, message: "Invalid state", cause: invalidState) - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift b/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift deleted file mode 100644 index d1ecef17e..000000000 --- a/Sources/GRPCHTTP2Core/Internal/ConstantAsyncSequence.swift +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -private struct ConstantAsyncSequence: AsyncSequence, Sendable { - private let element: Element - - init(element: Element) { - self.element = element - } - - func makeAsyncIterator() -> AsyncIterator { - return AsyncIterator(element: self.element) - } - - struct AsyncIterator: AsyncIteratorProtocol { - private let element: Element - - fileprivate init(element: Element) { - self.element = element - } - - func next() async throws -> Element? { - return self.element - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension RPCAsyncSequence where Element: Sendable, Failure == any Error { - static func constant(_ element: Element) -> RPCAsyncSequence { - return RPCAsyncSequence(wrapping: ConstantAsyncSequence(element: element)) - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/ContentType.swift b/Sources/GRPCHTTP2Core/Internal/ContentType.swift deleted file mode 100644 index 2e098d39f..000000000 --- a/Sources/GRPCHTTP2Core/Internal/ContentType.swift +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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. - */ - -// See: -// - https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md -enum ContentType { - case grpc - - init?(value: String) { - switch value { - case "application/grpc", - "application/grpc+proto": - self = .grpc - - default: - return nil - } - } - - var canonicalValue: String { - switch self { - case .grpc: - // This is more widely supported than "application/grpc+proto" - return "application/grpc" - } - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift b/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift deleted file mode 100644 index 11f818c28..000000000 --- a/Sources/GRPCHTTP2Core/Internal/DiscardingTaskGroup+CancellableHandle.swift +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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. - */ - -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension DiscardingTaskGroup { - /// Adds a child task to the group which is individually cancellable. - /// - /// - Parameter operation: The task to add to the group. - /// - Returns: A handle which can be used to cancel the task without cancelling the rest of - /// the group. - @inlinable - mutating func addCancellableTask( - _ operation: @Sendable @escaping () async -> Void - ) -> CancellableTaskHandle { - let signal = AsyncStream.makeStream(of: Void.self) - self.addTask { - return await withTaskGroup(of: FinishedOrCancelled.self) { group in - group.addTask { - await operation() - return .finished - } - - group.addTask { - for await _ in signal.stream {} - return .cancelled - } - - let first = await group.next()! - group.cancelAll() - let second = await group.next()! - - switch (first, second) { - case (.finished, .cancelled), (.cancelled, .finished): - return - default: - fatalError("Internal inconsistency") - } - } - } - - return CancellableTaskHandle(continuation: signal.continuation) - } - - @usableFromInline - enum FinishedOrCancelled: Sendable { - case finished - case cancelled - } -} - -@usableFromInline -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -struct CancellableTaskHandle: Sendable { - @usableFromInline - private(set) var continuation: AsyncStream.Continuation - - @inlinable - init(continuation: AsyncStream.Continuation) { - self.continuation = continuation - } - - @inlinable - func cancel() { - self.continuation.finish() - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift b/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift deleted file mode 100644 index 4f8b1eb40..000000000 --- a/Sources/GRPCHTTP2Core/Internal/GRPCStatusMessageMarshaller.swift +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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. - */ - -enum GRPCStatusMessageMarshaller { - /// Adds percent encoding to the given message. - /// - /// - Parameter message: Message to percent encode. - /// - Returns: Percent encoded string, or `nil` if it could not be encoded. - static func marshall(_ message: String) -> String? { - return percentEncode(message) - } - - /// Removes percent encoding from the given message. - /// - /// - Parameter message: Message to remove encoding from. - /// - Returns: The string with percent encoding removed, or the input string if the encoding - /// could not be removed. - static func unmarshall(_ message: String) -> String { - return removePercentEncoding(message) - } -} - -extension GRPCStatusMessageMarshaller { - /// Adds percent encoding to the given message. - /// - /// gRPC uses percent encoding as defined in RFC 3986 § 2.1 but with a different set of restricted - /// characters. The allowed characters are all visible printing characters except for (`%`, - /// `0x25`). That is: `0x20`-`0x24`, `0x26`-`0x7E`. - /// - /// - Parameter message: The message to encode. - /// - Returns: Percent encoded string, or `nil` if it could not be encoded. - private static func percentEncode(_ message: String) -> String? { - let utf8 = message.utf8 - - let encodedLength = self.percentEncodedLength(for: utf8) - // Fast-path: all characters are valid, nothing to encode. - if encodedLength == utf8.count { - return message - } - - var bytes: [UInt8] = [] - bytes.reserveCapacity(encodedLength) - - for char in message.utf8 { - switch char { - // See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses - case 0x20 ... 0x24, - 0x26 ... 0x7E: - bytes.append(char) - - default: - bytes.append(UInt8(ascii: "%")) - bytes.append(self.toHex(char >> 4)) - bytes.append(self.toHex(char & 0xF)) - } - } - - return String(decoding: bytes, as: UTF8.self) - } - - /// Returns the percent encoded length of the given `UTF8View`. - private static func percentEncodedLength(for view: String.UTF8View) -> Int { - var count = view.count - for byte in view { - switch byte { - case 0x20 ... 0x24, - 0x26 ... 0x7E: - () - - default: - count += 2 - } - } - return count - } - - /// Encode the given byte as hexadecimal. - /// - /// - Precondition: Only the four least significant bits may be set. - /// - Parameter nibble: The nibble to convert to hexadecimal. - private static func toHex(_ nibble: UInt8) -> UInt8 { - assert(nibble & 0xF == nibble) - - switch nibble { - case 0 ... 9: - return nibble &+ UInt8(ascii: "0") - default: - return nibble &+ (UInt8(ascii: "A") &- 10) - } - } - - /// Remove gRPC percent encoding from `message`. If any portion of the string could not be decoded - /// then the encoded message will be returned. - /// - /// - Parameter message: The message to remove percent encoding from. - /// - Returns: The decoded message. - private static func removePercentEncoding(_ message: String) -> String { - let utf8 = message.utf8 - - let decodedLength = self.percentDecodedLength(for: utf8) - // Fast-path: no decoding to do! Note that we may also have detected that the encoding is - // invalid, in which case we will return the encoded message: this is fine. - if decodedLength == utf8.count { - return message - } - - var chars: [UInt8] = [] - // We can't decode more characters than are already encoded. - chars.reserveCapacity(decodedLength) - - var currentIndex = utf8.startIndex - let endIndex = utf8.endIndex - - while currentIndex < endIndex { - let byte = utf8[currentIndex] - - switch byte { - case UInt8(ascii: "%"): - guard let (nextIndex, nextNextIndex) = utf8.nextTwoIndices(after: currentIndex), - let nextHex = fromHex(utf8[nextIndex]), - let nextNextHex = fromHex(utf8[nextNextIndex]) - else { - // If we can't decode the message, aborting and returning the encoded message is fine - // according to the spec. - return message - } - chars.append((nextHex << 4) | nextNextHex) - currentIndex = nextNextIndex - - default: - chars.append(byte) - } - - currentIndex = utf8.index(after: currentIndex) - } - - return String(decoding: chars, as: Unicode.UTF8.self) - } - - /// Returns the expected length of the decoded `UTF8View`. - private static func percentDecodedLength(for view: String.UTF8View) -> Int { - var encoded = 0 - - for byte in view { - switch byte { - case UInt8(ascii: "%"): - // This can't overflow since it can't be larger than view.count. - encoded &+= 1 - - default: - () - } - } - - let notEncoded = view.count - (encoded * 3) - - guard notEncoded >= 0 else { - // We've received gibberish: more '%' than expected. gRPC allows for the status message to - // be left encoded should it be incorrectly encoded. We'll do exactly that by returning - // the number of bytes in the view which will causes us to take the fast-path exit. - return view.count - } - - return notEncoded + encoded - } - - private static func fromHex(_ byte: UInt8) -> UInt8? { - switch byte { - case UInt8(ascii: "0") ... UInt8(ascii: "9"): - return byte &- UInt8(ascii: "0") - case UInt8(ascii: "A") ... UInt8(ascii: "Z"): - return byte &- (UInt8(ascii: "A") &- 10) - case UInt8(ascii: "a") ... UInt8(ascii: "z"): - return byte &- (UInt8(ascii: "a") &- 10) - default: - return nil - } - } -} - -extension String.UTF8View { - /// Return the next two valid indices after the given index. The indices are considered valid if - /// they less than `endIndex`. - fileprivate func nextTwoIndices(after index: Index) -> (Index, Index)? { - let secondIndex = self.index(index, offsetBy: 2) - guard secondIndex < self.endIndex else { - return nil - } - - return (self.index(after: index), secondIndex) - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift b/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift deleted file mode 100644 index cade0f581..000000000 --- a/Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -package import NIOCore -internal import NIOHPACK -package import NIOHTTP2 - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ChannelPipeline.SynchronousOperations { - package typealias HTTP2ConnectionChannel = NIOAsyncChannel - package typealias HTTP2StreamMultiplexer = NIOHTTP2Handler.AsyncStreamMultiplexer< - (NIOAsyncChannel, EventLoopFuture) - > - - package func configureGRPCServerPipeline( - channel: any Channel, - compressionConfig: HTTP2ServerTransport.Config.Compression, - connectionConfig: HTTP2ServerTransport.Config.Connection, - http2Config: HTTP2ServerTransport.Config.HTTP2, - rpcConfig: HTTP2ServerTransport.Config.RPC, - requireALPN: Bool, - scheme: Scheme - ) throws -> (HTTP2ConnectionChannel, HTTP2StreamMultiplexer) { - let serverConnectionHandler = ServerConnectionManagementHandler( - eventLoop: self.eventLoop, - maxIdleTime: connectionConfig.maxIdleTime.map { TimeAmount($0) }, - maxAge: connectionConfig.maxAge.map { TimeAmount($0) }, - maxGraceTime: connectionConfig.maxGraceTime.map { TimeAmount($0) }, - keepaliveTime: TimeAmount(connectionConfig.keepalive.time), - keepaliveTimeout: TimeAmount(connectionConfig.keepalive.timeout), - allowKeepaliveWithoutCalls: connectionConfig.keepalive.clientBehavior.allowWithoutCalls, - minPingIntervalWithoutCalls: TimeAmount( - connectionConfig.keepalive.clientBehavior.minPingIntervalWithoutCalls - ), - requireALPN: requireALPN - ) - let flushNotificationHandler = GRPCServerFlushNotificationHandler( - serverConnectionManagementHandler: serverConnectionHandler - ) - try self.addHandler(flushNotificationHandler) - - let clampedTargetWindowSize = self.clampTargetWindowSize(http2Config.targetWindowSize) - let clampedMaxFrameSize = self.clampMaxFrameSize(http2Config.maxFrameSize) - - var http2HandlerConnectionConfiguration = NIOHTTP2Handler.ConnectionConfiguration() - var http2HandlerHTTP2Settings = HTTP2Settings([ - HTTP2Setting(parameter: .initialWindowSize, value: clampedTargetWindowSize), - HTTP2Setting(parameter: .maxFrameSize, value: clampedMaxFrameSize), - HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize), - ]) - if let maxConcurrentStreams = http2Config.maxConcurrentStreams { - http2HandlerHTTP2Settings.append( - HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams) - ) - } - http2HandlerConnectionConfiguration.initialSettings = http2HandlerHTTP2Settings - - var http2HandlerStreamConfiguration = NIOHTTP2Handler.StreamConfiguration() - http2HandlerStreamConfiguration.targetWindowSize = clampedTargetWindowSize - - let streamMultiplexer = try self.configureAsyncHTTP2Pipeline( - mode: .server, - streamDelegate: serverConnectionHandler.http2StreamDelegate, - configuration: NIOHTTP2Handler.Configuration( - connection: http2HandlerConnectionConfiguration, - stream: http2HandlerStreamConfiguration - ) - ) { streamChannel in - return streamChannel.eventLoop.makeCompletedFuture { - let methodDescriptorPromise = streamChannel.eventLoop.makePromise(of: MethodDescriptor.self) - let streamHandler = GRPCServerStreamHandler( - scheme: scheme, - acceptedEncodings: compressionConfig.enabledAlgorithms, - maxPayloadSize: rpcConfig.maxRequestPayloadSize, - methodDescriptorPromise: methodDescriptorPromise - ) - try streamChannel.pipeline.syncOperations.addHandler(streamHandler) - - let asyncStreamChannel = try NIOAsyncChannel( - wrappingChannelSynchronously: streamChannel - ) - return (asyncStreamChannel, methodDescriptorPromise.futureResult) - } - } - - try self.addHandler(serverConnectionHandler) - - let connectionChannel = try NIOAsyncChannel( - wrappingChannelSynchronously: channel - ) - - return (connectionChannel, streamMultiplexer) - } -} - -extension ChannelPipeline.SynchronousOperations { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package func configureGRPCClientPipeline( - channel: any Channel, - config: GRPCChannel.Config - ) throws -> ( - NIOAsyncChannel, - NIOHTTP2Handler.AsyncStreamMultiplexer - ) { - let clampedTargetWindowSize = self.clampTargetWindowSize(config.http2.targetWindowSize) - let clampedMaxFrameSize = self.clampMaxFrameSize(config.http2.maxFrameSize) - - // Use NIOs defaults as a starting point. - var http2 = NIOHTTP2Handler.Configuration() - http2.stream.targetWindowSize = clampedTargetWindowSize - http2.connection.initialSettings = [ - // Disallow servers from creating push streams. - HTTP2Setting(parameter: .enablePush, value: 0), - // Set the initial window size and max frame size to the clamped configured values. - HTTP2Setting(parameter: .initialWindowSize, value: clampedTargetWindowSize), - HTTP2Setting(parameter: .maxFrameSize, value: clampedMaxFrameSize), - // Use NIOs default max header list size (16kB) - HTTP2Setting(parameter: .maxHeaderListSize, value: HPACKDecoder.defaultMaxHeaderListSize), - ] - - let connectionHandler = ClientConnectionHandler( - eventLoop: self.eventLoop, - maxIdleTime: config.connection.maxIdleTime.map { TimeAmount($0) }, - keepaliveTime: config.connection.keepalive.map { TimeAmount($0.time) }, - keepaliveTimeout: config.connection.keepalive.map { TimeAmount($0.timeout) }, - keepaliveWithoutCalls: config.connection.keepalive?.allowWithoutCalls ?? false - ) - - let multiplexer = try self.configureAsyncHTTP2Pipeline( - mode: .client, - streamDelegate: connectionHandler.http2StreamDelegate, - configuration: http2 - ) { stream in - // Shouldn't happen, push-promises are disabled so the server shouldn't be able to - // open streams. - stream.close() - } - - try self.addHandler(connectionHandler) - - let connection = try NIOAsyncChannel( - wrappingChannelSynchronously: channel, - configuration: NIOAsyncChannel.Configuration( - inboundType: ClientConnectionEvent.self, - outboundType: Void.self - ) - ) - - return (connection, multiplexer) - } -} - -extension ChannelPipeline.SynchronousOperations { - /// Max frame size must be in the range `2^14 ..< 2^24` (RFC 9113 § 4.2). - fileprivate func clampMaxFrameSize(_ maxFrameSize: Int) -> Int { - let clampedMaxFrameSize: Int - if maxFrameSize >= (1 << 24) { - clampedMaxFrameSize = (1 << 24) - 1 - } else if maxFrameSize < (1 << 14) { - clampedMaxFrameSize = (1 << 14) - } else { - clampedMaxFrameSize = maxFrameSize - } - return clampedMaxFrameSize - } - - /// Window size which mustn't exceed `2^31 - 1` (RFC 9113 § 6.5.2). - internal func clampTargetWindowSize(_ targetWindowSize: Int) -> Int { - min(targetWindowSize, (1 << 31) - 1) - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift b/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift deleted file mode 100644 index e27b07659..000000000 --- a/Sources/GRPCHTTP2Core/Internal/NIOSocketAddress+GRPCSocketAddress.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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. - */ - -private import GRPCCore -package import NIOCore - -extension GRPCHTTP2Core.SocketAddress { - package init(_ nioSocketAddress: NIOCore.SocketAddress) { - switch nioSocketAddress { - case .v4(let address): - self = .ipv4( - host: address.host, - port: nioSocketAddress.port ?? 0 - ) - - case .v6(let address): - self = .ipv6( - host: address.host, - port: nioSocketAddress.port ?? 0 - ) - - case .unixDomainSocket: - self = .unixDomainSocket(path: nioSocketAddress.pathname ?? "") - } - } -} - -extension NIOCore.SocketAddress { - package init(_ socketAddress: GRPCHTTP2Core.SocketAddress) throws { - if let ipv4 = socketAddress.ipv4 { - self = try Self(ipv4) - } else if let ipv6 = socketAddress.ipv6 { - self = try Self(ipv6) - } else if let unixDomainSocket = socketAddress.unixDomainSocket { - self = try Self(unixDomainSocket) - } else { - throw RPCError( - code: .internalError, - message: - "Unsupported mapping to NIOCore/SocketAddress for GRPCHTTP2Core/SocketAddress: \(socketAddress)." - ) - } - } - - package init(_ address: GRPCHTTP2Core.SocketAddress.IPv4) throws { - try self.init(ipAddress: address.host, port: address.port) - } - - package init(_ address: GRPCHTTP2Core.SocketAddress.IPv6) throws { - try self.init(ipAddress: address.host, port: address.port) - } - - package init(_ address: GRPCHTTP2Core.SocketAddress.UnixDomainSocket) throws { - try self.init(unixDomainSocketPath: address.path) - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift b/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift deleted file mode 100644 index 5f6f32f7e..000000000 --- a/Sources/GRPCHTTP2Core/Internal/ProcessUniqueID.swift +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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. - */ - -private import Synchronization - -/// An ID which is unique within this process. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct ProcessUniqueID: Hashable, Sendable, CustomStringConvertible { - private static let source = Atomic(UInt64(0)) - private let rawValue: UInt64 - - init() { - let (_, newValue) = Self.source.add(1, ordering: .relaxed) - self.rawValue = newValue - } - - var description: String { - String(describing: self.rawValue) - } -} - -/// A process-unique ID for a subchannel. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package struct SubchannelID: Hashable, Sendable, CustomStringConvertible { - private let id = ProcessUniqueID() - package init() {} - package var description: String { - "subchan_\(self.id)" - } -} - -/// A process-unique ID for a load-balancer. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct LoadBalancerID: Hashable, Sendable, CustomStringConvertible { - private let id = ProcessUniqueID() - var description: String { - "lb_\(self.id)" - } -} - -/// A process-unique ID for an entry in a queue. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct QueueEntryID: Hashable, Sendable, CustomStringConvertible { - private let id = ProcessUniqueID() - var description: String { - "q_entry_\(self.id)" - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/Result+Catching.swift b/Sources/GRPCHTTP2Core/Internal/Result+Catching.swift deleted file mode 100644 index 1cd809e42..000000000 --- a/Sources/GRPCHTTP2Core/Internal/Result+Catching.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Result where Failure == any Error { - /// Like `Result(catching:)`, but `async`. - /// - /// - Parameter body: An `async` closure to catch the result of. - @inlinable - init(catching body: () async throws -> Success) async { - do { - self = .success(try await body()) - } catch { - self = .failure(error) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Internal/Timer.swift b/Sources/GRPCHTTP2Core/Internal/Timer.swift deleted file mode 100644 index bfc4ff29a..000000000 --- a/Sources/GRPCHTTP2Core/Internal/Timer.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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. - */ - -package import NIOCore - -package struct Timer { - /// The delay to wait before running the task. - private let delay: TimeAmount - /// The task to run, if scheduled. - private var task: Kind? - /// Whether the task to schedule is repeated. - private let `repeat`: Bool - - private enum Kind { - case once(Scheduled) - case repeated(RepeatedTask) - - func cancel() { - switch self { - case .once(let task): - task.cancel() - case .repeated(let task): - task.cancel() - } - } - } - - package init(delay: TimeAmount, repeat: Bool = false) { - self.delay = delay - self.task = nil - self.repeat = `repeat` - } - - /// Schedule a task on the given `EventLoop`. - package mutating func schedule( - on eventLoop: any EventLoop, - work: @escaping @Sendable () throws -> Void - ) { - self.task?.cancel() - - if self.repeat { - let task = eventLoop.scheduleRepeatedTask(initialDelay: self.delay, delay: self.delay) { _ in - try work() - } - self.task = .repeated(task) - } else { - let task = eventLoop.scheduleTask(in: self.delay, work) - self.task = .once(task) - } - } - - /// Cancels the task, if one was scheduled. - package mutating func cancel() { - self.task?.cancel() - self.task = nil - } -} diff --git a/Sources/GRPCHTTP2Core/ListeningServerTransport.swift b/Sources/GRPCHTTP2Core/ListeningServerTransport.swift deleted file mode 100644 index 20150d360..000000000 --- a/Sources/GRPCHTTP2Core/ListeningServerTransport.swift +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore - -/// A transport which refines `ServerTransport` to provide the socket address of a listening -/// server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol ListeningServerTransport: ServerTransport { - /// Returns the listening address of the server transport once it has started. - var listeningAddress: SocketAddress { get async throws } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCServer { - /// Returns the listening address of the server transport once it has started. - /// - /// This will be `nil` if the transport doesn't conform to ``ListeningServerTransport``. - public var listeningAddress: SocketAddress? { - get async throws { - if let listener = self.transport as? (any ListeningServerTransport) { - return try await listener.listeningAddress - } else { - return nil - } - } - } -} diff --git a/Sources/GRPCHTTP2Core/OneOrManyQueue.swift b/Sources/GRPCHTTP2Core/OneOrManyQueue.swift deleted file mode 100644 index fdf88186c..000000000 --- a/Sources/GRPCHTTP2Core/OneOrManyQueue.swift +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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. - */ - -internal import DequeModule - -/// A FIFO-queue which allows for a single element to be stored on the stack and defers to a -/// heap-implementation if further elements are added. -/// -/// This is useful when optimising for unary streams where avoiding the cost of a heap -/// allocation is desirable. -internal struct OneOrManyQueue: Collection { - private var backing: Backing - - private enum Backing: Collection { - case none - case one(Element) - case many(Deque) - - var startIndex: Int { - switch self { - case .none, .one: - return 0 - case let .many(elements): - return elements.startIndex - } - } - - var endIndex: Int { - switch self { - case .none: - return 0 - case .one: - return 1 - case let .many(elements): - return elements.endIndex - } - } - - subscript(index: Int) -> Element { - switch self { - case .none: - fatalError("Invalid index") - case let .one(element): - assert(index == 0) - return element - case let .many(elements): - return elements[index] - } - } - - func index(after index: Int) -> Int { - switch self { - case .none: - return 0 - case .one: - return 1 - case let .many(elements): - return elements.index(after: index) - } - } - - var count: Int { - switch self { - case .none: - return 0 - case .one: - return 1 - case let .many(elements): - return elements.count - } - } - - var isEmpty: Bool { - switch self { - case .none: - return true - case .one: - return false - case let .many(elements): - return elements.isEmpty - } - } - - mutating func append(_ element: Element) { - switch self { - case .none: - self = .one(element) - case let .one(one): - var elements = Deque() - elements.reserveCapacity(16) - elements.append(one) - elements.append(element) - self = .many(elements) - case var .many(elements): - self = .none - elements.append(element) - self = .many(elements) - } - } - - mutating func pop() -> Element? { - switch self { - case .none: - return nil - case let .one(element): - self = .none - return element - case var .many(many): - self = .none - let element = many.popFirst() - self = .many(many) - return element - } - } - } - - init() { - self.backing = .none - } - - var isEmpty: Bool { - return self.backing.isEmpty - } - - var count: Int { - return self.backing.count - } - - var startIndex: Int { - return self.backing.startIndex - } - - var endIndex: Int { - return self.backing.endIndex - } - - subscript(index: Int) -> Element { - return self.backing[index] - } - - func index(after index: Int) -> Int { - return self.backing.index(after: index) - } - - mutating func append(_ element: Element) { - self.backing.append(element) - } - - mutating func pop() -> Element? { - return self.backing.pop() - } -} diff --git a/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift deleted file mode 100644 index 769db9bf7..000000000 --- a/Sources/GRPCHTTP2Core/Server/CommonHTTP2ServerTransport.swift +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -package import NIOCore -package import NIOExtras -private import NIOHTTP2 -private import Synchronization - -/// Provides the common functionality for a `NIO`-based server transport. -/// -/// - SeeAlso: ``HTTP2ListenerFactory``. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package final class CommonHTTP2ServerTransport< - ListenerFactory: HTTP2ListenerFactory ->: ServerTransport, ListeningServerTransport { - private let eventLoopGroup: any EventLoopGroup - private let address: SocketAddress - private let listeningAddressState: Mutex - private let serverQuiescingHelper: ServerQuiescingHelper - private let factory: ListenerFactory - - private enum State { - case idle(EventLoopPromise) - case listening(EventLoopFuture) - case closedOrInvalidAddress(RuntimeError) - - var listeningAddressFuture: EventLoopFuture { - get throws { - switch self { - case .idle(let eventLoopPromise): - return eventLoopPromise.futureResult - case .listening(let eventLoopFuture): - return eventLoopFuture - case .closedOrInvalidAddress(let runtimeError): - throw runtimeError - } - } - } - - enum OnBound { - case succeedPromise(_ promise: EventLoopPromise, address: SocketAddress) - case failPromise(_ promise: EventLoopPromise, error: RuntimeError) - } - - mutating func addressBound( - _ address: NIOCore.SocketAddress?, - userProvidedAddress: SocketAddress - ) -> OnBound { - switch self { - case .idle(let listeningAddressPromise): - if let address { - self = .listening(listeningAddressPromise.futureResult) - return .succeedPromise(listeningAddressPromise, address: SocketAddress(address)) - } else if userProvidedAddress.virtualSocket != nil { - self = .listening(listeningAddressPromise.futureResult) - return .succeedPromise(listeningAddressPromise, address: userProvidedAddress) - } else { - assertionFailure("Unknown address type") - let invalidAddressError = RuntimeError( - code: .transportError, - message: "Unknown address type returned by transport." - ) - self = .closedOrInvalidAddress(invalidAddressError) - return .failPromise(listeningAddressPromise, error: invalidAddressError) - } - - case .listening, .closedOrInvalidAddress: - fatalError("Invalid state: addressBound should only be called once and when in idle state") - } - } - - enum OnClose { - case failPromise(EventLoopPromise, error: RuntimeError) - case doNothing - } - - mutating func close() -> OnClose { - let serverStoppedError = RuntimeError( - code: .serverIsStopped, - message: """ - There is no listening address bound for this server: there may have been \ - an error which caused the transport to close, or it may have shut down. - """ - ) - - switch self { - case .idle(let listeningAddressPromise): - self = .closedOrInvalidAddress(serverStoppedError) - return .failPromise(listeningAddressPromise, error: serverStoppedError) - - case .listening: - self = .closedOrInvalidAddress(serverStoppedError) - return .doNothing - - case .closedOrInvalidAddress: - return .doNothing - } - } - } - - /// The listening address for this server transport. - /// - /// It is an `async` property because it will only return once the address has been successfully bound. - /// - /// - Throws: A runtime error will be thrown if the address could not be bound or is not bound any - /// longer, because the transport isn't listening anymore. It can also throw if the transport returned an - /// invalid address. - package var listeningAddress: SocketAddress { - get async throws { - try await self.listeningAddressState - .withLock { try $0.listeningAddressFuture } - .get() - } - } - - package init( - address: SocketAddress, - eventLoopGroup: any EventLoopGroup, - quiescingHelper: ServerQuiescingHelper, - listenerFactory: ListenerFactory - ) { - self.eventLoopGroup = eventLoopGroup - self.address = address - - let eventLoop = eventLoopGroup.any() - self.listeningAddressState = Mutex(.idle(eventLoop.makePromise())) - - self.factory = listenerFactory - self.serverQuiescingHelper = quiescingHelper - } - - package func listen( - streamHandler: @escaping @Sendable ( - _ stream: RPCStream, - _ context: ServerContext - ) async -> Void - ) async throws { - defer { - switch self.listeningAddressState.withLock({ $0.close() }) { - case .failPromise(let promise, let error): - promise.fail(error) - case .doNothing: - () - } - } - - let serverChannel = try await self.factory.makeListeningChannel( - eventLoopGroup: self.eventLoopGroup, - address: self.address, - serverQuiescingHelper: self.serverQuiescingHelper - ) - - let action = self.listeningAddressState.withLock { - $0.addressBound( - serverChannel.channel.localAddress, - userProvidedAddress: self.address - ) - } - switch action { - case .succeedPromise(let promise, let address): - promise.succeed(address) - case .failPromise(let promise, let error): - promise.fail(error) - } - - try await serverChannel.executeThenClose { inbound in - try await withThrowingDiscardingTaskGroup { group in - for try await (connectionChannel, streamMultiplexer) in inbound { - group.addTask { - try await self.handleConnection( - connectionChannel, - multiplexer: streamMultiplexer, - streamHandler: streamHandler - ) - } - } - } - } - } - - private func handleConnection( - _ connection: NIOAsyncChannel, - multiplexer: ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer, - streamHandler: @escaping @Sendable ( - _ stream: RPCStream, - _ context: ServerContext - ) async -> Void - ) async throws { - try await connection.executeThenClose { inbound, _ in - await withDiscardingTaskGroup { group in - group.addTask { - do { - for try await _ in inbound {} - } catch { - // We don't want to close the channel if one connection throws. - return - } - } - - do { - for try await (stream, descriptor) in multiplexer.inbound { - group.addTask { - await self.handleStream(stream, handler: streamHandler, descriptor: descriptor) - } - } - } catch { - return - } - } - } - } - - private func handleStream( - _ stream: NIOAsyncChannel, - handler streamHandler: @escaping @Sendable ( - _ stream: RPCStream, - _ context: ServerContext - ) async -> Void, - descriptor: EventLoopFuture - ) async { - // It's okay to ignore these errors: - // - If we get an error because the http2Stream failed to close, then there's nothing we can do - // - If we get an error because the inner closure threw, then the only possible scenario in which - // that could happen is if methodDescriptor.get() throws - in which case, it means we never got - // the RPC metadata, which means we can't do anything either and it's okay to just kill the stream. - try? await stream.executeThenClose { inbound, outbound in - guard let descriptor = try? await descriptor.get() else { - return - } - - let rpcStream = RPCStream( - descriptor: descriptor, - inbound: RPCAsyncSequence(wrapping: inbound), - outbound: RPCWriter.Closable( - wrapping: ServerConnection.Stream.Outbound( - responseWriter: outbound, - http2Stream: stream - ) - ) - ) - - let context = ServerContext(descriptor: descriptor) - await streamHandler(rpcStream, context) - } - } - - package func beginGracefulShutdown() { - self.serverQuiescingHelper.initiateShutdown(promise: nil) - } -} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift deleted file mode 100644 index 1bbffc205..000000000 --- a/Sources/GRPCHTTP2Core/Server/Connection/GRPCServerFlushNotificationHandler.swift +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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. - */ - -internal import NIOCore - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCServerFlushNotificationHandler: ChannelOutboundHandler { - typealias OutboundIn = Any - typealias OutboundOut = Any - - private let serverConnectionManagementHandler: ServerConnectionManagementHandler - - init( - serverConnectionManagementHandler: ServerConnectionManagementHandler - ) { - self.serverConnectionManagementHandler = serverConnectionManagementHandler - } - - func flush(context: ChannelHandlerContext) { - self.serverConnectionManagementHandler.syncView.connectionWillFlush() - context.flush() - } -} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift deleted file mode 100644 index 84d1d25a2..000000000 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnection.swift +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -package import NIOCore - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public enum ServerConnection { - public enum Stream { - package struct Outbound: ClosableRPCWriterProtocol { - package typealias Element = RPCResponsePart - - private let responseWriter: NIOAsyncChannelOutboundWriter - private let http2Stream: NIOAsyncChannel - - package init( - responseWriter: NIOAsyncChannelOutboundWriter, - http2Stream: NIOAsyncChannel - ) { - self.responseWriter = responseWriter - self.http2Stream = http2Stream - } - - package func write(_ element: RPCResponsePart) async throws { - try await self.responseWriter.write(element) - } - - package func write(contentsOf elements: some Sequence) async throws { - try await self.responseWriter.write(contentsOf: elements) - } - - package func finish() { - self.responseWriter.finish() - } - - package func finish(throwing error: any Error) { - // Fire the error inbound; this fails the inbound writer. - self.http2Stream.channel.pipeline.fireErrorCaught(error) - } - } - } -} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift deleted file mode 100644 index 8ca7660d6..000000000 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler+StateMachine.swift +++ /dev/null @@ -1,395 +0,0 @@ -/* - * 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. - */ - -internal import NIOCore -internal import NIOHTTP2 - -extension ServerConnectionManagementHandler { - /// Tracks the state of TCP connections at the server. - /// - /// The state machine manages the state for the graceful shutdown procedure as well as policing - /// client-side keep alive. - struct StateMachine { - /// Current state. - private var state: State - - /// Opaque data sent to the client in a PING frame after emitting the first GOAWAY frame - /// as part of graceful shutdown. - private let goAwayPingData: HTTP2PingData - - /// Create a new state machine. - /// - /// - Parameters: - /// - allowKeepaliveWithoutCalls: Whether the client is permitted to send keep alive pings - /// when there are no active calls. - /// - minPingReceiveIntervalWithoutCalls: The minimum time interval required between keep - /// alive pings when there are no active calls. - /// - goAwayPingData: Opaque data sent to the client in a PING frame when the server - /// initiates graceful shutdown. - init( - allowKeepaliveWithoutCalls: Bool, - minPingReceiveIntervalWithoutCalls: TimeAmount, - goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: .random(in: .min ... .max)) - ) { - let keepalive = Keepalive( - allowWithoutCalls: allowKeepaliveWithoutCalls, - minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls - ) - - self.state = .active(State.Active(keepalive: keepalive)) - self.goAwayPingData = goAwayPingData - } - - /// Record that the stream with the given ID has been opened. - mutating func streamOpened(_ id: HTTP2StreamID) { - switch self.state { - case .active(var state): - self.state = ._modifying - state.lastStreamID = id - let (inserted, _) = state.openStreams.insert(id) - assert(inserted, "Can't open stream \(Int(id)), it's already open") - self.state = .active(state) - - case .closing(var state): - self.state = ._modifying - state.lastStreamID = id - let (inserted, _) = state.openStreams.insert(id) - assert(inserted, "Can't open stream \(Int(id)), it's already open") - self.state = .closing(state) - - case .closed: - () - - case ._modifying: - preconditionFailure() - } - } - - enum OnStreamClosed: Equatable { - /// Start the idle timer, after which the connection should be closed gracefully. - case startIdleTimer - /// Close the connection. - case close - /// Do nothing. - case none - } - - /// Record that the stream with the given ID has been closed. - mutating func streamClosed(_ id: HTTP2StreamID) -> OnStreamClosed { - let onStreamClosed: OnStreamClosed - - switch self.state { - case .active(var state): - self.state = ._modifying - let removedID = state.openStreams.remove(id) - assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") - onStreamClosed = state.openStreams.isEmpty ? .startIdleTimer : .none - self.state = .active(state) - - case .closing(var state): - self.state = ._modifying - let removedID = state.openStreams.remove(id) - assert(removedID != nil, "Can't close stream \(Int(id)), it wasn't open") - // If the second GOAWAY hasn't been sent it isn't safe to close if there are no open - // streams: the client may have opened a stream which the server doesn't know about yet. - let canClose = state.sentSecondGoAway && state.openStreams.isEmpty - onStreamClosed = canClose ? .close : .none - self.state = .closing(state) - - case .closed: - onStreamClosed = .none - - case ._modifying: - preconditionFailure() - } - - return onStreamClosed - } - - enum OnPing: Equatable { - /// Send a GOAWAY frame with the code "enhance your calm" and immediately close the connection. - case enhanceYourCalmThenClose(HTTP2StreamID) - /// Acknowledge the ping. - case sendAck - /// Ignore the ping. - case none - } - - /// Received a ping with the given data. - /// - /// - Parameters: - /// - time: The time at which the ping was received. - /// - data: The data sent with the ping. - mutating func receivedPing(atTime time: NIODeadline, data: HTTP2PingData) -> OnPing { - let onPing: OnPing - - switch self.state { - case .active(var state): - self.state = ._modifying - let tooManyPings = state.keepalive.receivedPing( - atTime: time, - hasOpenStreams: !state.openStreams.isEmpty - ) - - if tooManyPings { - onPing = .enhanceYourCalmThenClose(state.lastStreamID) - self.state = .closed - } else { - onPing = .sendAck - self.state = .active(state) - } - - case .closing(var state): - self.state = ._modifying - let tooManyPings = state.keepalive.receivedPing( - atTime: time, - hasOpenStreams: !state.openStreams.isEmpty - ) - - if tooManyPings { - onPing = .enhanceYourCalmThenClose(state.lastStreamID) - self.state = .closed - } else { - onPing = .sendAck - self.state = .closing(state) - } - - case .closed: - onPing = .none - - case ._modifying: - preconditionFailure() - } - - return onPing - } - - enum OnPingAck: Equatable { - /// Send a GOAWAY frame with no error and the given last stream ID, optionally closing the - /// connection immediately afterwards. - case sendGoAway(lastStreamID: HTTP2StreamID, close: Bool) - /// Ignore the ack. - case none - } - - /// Received a PING frame with the 'ack' flag set. - mutating func receivedPingAck(data: HTTP2PingData) -> OnPingAck { - let onPingAck: OnPingAck - - switch self.state { - case .closing(var state): - self.state = ._modifying - - // If only one GOAWAY has been sent and the data matches the data from the GOAWAY ping then - // the server should send another GOAWAY ratcheting down the last stream ID. If no streams - // are open then the server can close the connection immediately after, otherwise it must - // wait until all streams are closed. - if !state.sentSecondGoAway, data == self.goAwayPingData { - state.sentSecondGoAway = true - - if state.openStreams.isEmpty { - self.state = .closed - onPingAck = .sendGoAway(lastStreamID: state.lastStreamID, close: true) - } else { - self.state = .closing(state) - onPingAck = .sendGoAway(lastStreamID: state.lastStreamID, close: false) - } - } else { - onPingAck = .none - } - - self.state = .closing(state) - - case .active, .closed: - onPingAck = .none - - case ._modifying: - preconditionFailure() - } - - return onPingAck - } - - enum OnStartGracefulShutdown: Equatable { - /// Initiate graceful shutdown by sending a GOAWAY frame with the last stream ID set as the max - /// stream ID and no error. Follow it immediately with a PING frame with the given data. - case sendGoAwayAndPing(HTTP2PingData) - /// Ignore the request to start graceful shutdown. - case none - } - - /// Request that the connection begins graceful shutdown. - mutating func startGracefulShutdown() -> OnStartGracefulShutdown { - let onStartGracefulShutdown: OnStartGracefulShutdown - - switch self.state { - case .active(let state): - self.state = .closing(State.Closing(from: state)) - onStartGracefulShutdown = .sendGoAwayAndPing(self.goAwayPingData) - - case .closing, .closed: - onStartGracefulShutdown = .none - - case ._modifying: - preconditionFailure() - } - - return onStartGracefulShutdown - } - - /// Reset the state of keep-alive policing. - mutating func resetKeepaliveState() { - switch self.state { - case .active(var state): - self.state = ._modifying - state.keepalive.reset() - self.state = .active(state) - - case .closing(var state): - self.state = ._modifying - state.keepalive.reset() - self.state = .closing(state) - - case .closed: - () - - case ._modifying: - preconditionFailure() - } - } - - /// Marks the state as closed. - mutating func markClosed() { - self.state = .closed - } - } -} - -extension ServerConnectionManagementHandler.StateMachine { - fileprivate struct Keepalive { - /// Allow the client to send keep alive pings when there are no active calls. - private let allowWithoutCalls: Bool - - /// The minimum time interval which pings may be received at when there are no active calls. - private let minPingReceiveIntervalWithoutCalls: TimeAmount - - /// The maximum number of "bad" pings sent by the client the server tolerates before closing - /// the connection. - private let maxPingStrikes: Int - - /// The number of "bad" pings sent by the client. This can be reset when the server sends - /// DATA or HEADERS frames. - /// - /// Ping strikes account for pings being occasionally being used for purposes other than keep - /// alive (a low number of strikes is therefore expected and okay). - private var pingStrikes: Int - - /// The last time a valid ping happened. - /// - /// Note: `distantPast` isn't used to indicate no previous valid ping as `NIODeadline` uses - /// the monotonic clock on Linux which uses an undefined starting point and in some cases isn't - /// always that distant. - private var lastValidPingTime: NIODeadline? - - init(allowWithoutCalls: Bool, minPingReceiveIntervalWithoutCalls: TimeAmount) { - self.allowWithoutCalls = allowWithoutCalls - self.minPingReceiveIntervalWithoutCalls = minPingReceiveIntervalWithoutCalls - self.maxPingStrikes = 2 - self.pingStrikes = 0 - self.lastValidPingTime = nil - } - - /// Reset ping strikes and the time of the last valid ping. - mutating func reset() { - self.lastValidPingTime = nil - self.pingStrikes = 0 - } - - /// Returns whether the client has sent too many pings. - mutating func receivedPing(atTime time: NIODeadline, hasOpenStreams: Bool) -> Bool { - let interval: TimeAmount - - if hasOpenStreams || self.allowWithoutCalls { - interval = self.minPingReceiveIntervalWithoutCalls - } else { - // If there are no open streams and keep alive pings aren't allowed without calls then - // use an interval of two hours. - // - // This comes from gRFC A8: https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md - interval = .hours(2) - } - - // If there's no last ping time then the first is acceptable. - let isAcceptablePing = self.lastValidPingTime.map { $0 + interval <= time } ?? true - let tooManyPings: Bool - - if isAcceptablePing { - self.lastValidPingTime = time - tooManyPings = false - } else { - self.pingStrikes += 1 - tooManyPings = self.pingStrikes > self.maxPingStrikes - } - - return tooManyPings - } - } -} - -extension ServerConnectionManagementHandler.StateMachine { - fileprivate enum State { - /// The connection is active. - struct Active { - /// The number of open streams. - var openStreams: Set - /// The ID of the most recently opened stream (zero indicates no streams have been opened yet). - var lastStreamID: HTTP2StreamID - /// The state of keep alive. - var keepalive: Keepalive - - init(keepalive: Keepalive) { - self.openStreams = [] - self.lastStreamID = .rootStream - self.keepalive = keepalive - } - } - - /// The connection is closing gracefully, an initial GOAWAY frame has been sent (with the - /// last stream ID set to max). - struct Closing { - /// The number of open streams. - var openStreams: Set - /// The ID of the most recently opened stream (zero indicates no streams have been opened yet). - var lastStreamID: HTTP2StreamID - /// The state of keep alive. - var keepalive: Keepalive - /// Whether the second GOAWAY frame has been sent with a lower stream ID. - var sentSecondGoAway: Bool - - init(from state: Active) { - self.openStreams = state.openStreams - self.lastStreamID = state.lastStreamID - self.keepalive = state.keepalive - self.sentSecondGoAway = false - } - } - - case active(Active) - case closing(Closing) - case closed - case _modifying - } -} diff --git a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift b/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift deleted file mode 100644 index 3ceee927b..000000000 --- a/Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift +++ /dev/null @@ -1,556 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore -internal import NIOCore -internal import NIOHTTP2 -internal import NIOTLS - -/// A `ChannelHandler` which manages the lifecycle of a gRPC connection over HTTP/2. -/// -/// This handler is responsible for managing several aspects of the connection. These include: -/// 1. Handling the graceful close of connections. When gracefully closing a connection the server -/// sends a GOAWAY frame with the last stream ID set to the maximum stream ID allowed followed by -/// a PING frame. On receipt of the PING frame the server sends another GOAWAY frame with the -/// highest ID of all streams which have been opened. After this, the handler closes the -/// connection once all streams are closed. -/// 2. Enforcing that graceful shutdown doesn't exceed a configured limit (if configured). -/// 3. Gracefully closing the connection once it reaches the maximum configured age (if configured). -/// 4. Gracefully closing the connection once it has been idle for a given period of time (if -/// configured). -/// 5. Periodically sending keep alive pings to the client (if configured) and closing the -/// connection if necessary. -/// 6. Policing pings sent by the client to ensure that the client isn't misconfigured to send -/// too many pings. -/// -/// Some of the behaviours are described in: -/// - [gRFC A8](https://github.com/grpc/proposal/blob/master/A8-client-side-keepalive.md), and -/// - [gRFC A9](https://github.com/grpc/proposal/blob/master/A9-server-side-conn-mgt.md). -final class ServerConnectionManagementHandler: ChannelDuplexHandler { - typealias InboundIn = HTTP2Frame - typealias InboundOut = HTTP2Frame - typealias OutboundIn = HTTP2Frame - typealias OutboundOut = HTTP2Frame - - /// The `EventLoop` of the `Channel` this handler exists in. - private let eventLoop: any EventLoop - - /// The maximum amount of time a connection may be idle for. If the connection remains idle - /// (i.e. has no open streams) for this period of time then the connection will be gracefully - /// closed. - private var maxIdleTimer: Timer? - - /// The maximum age of a connection. If the connection remains open after this amount of time - /// then it will be gracefully closed. - private var maxAgeTimer: Timer? - - /// The maximum amount of time a connection may spend closing gracefully, after which it is - /// closed abruptly. The timer starts after the second GOAWAY frame has been sent. - private var maxGraceTimer: Timer? - - /// The amount of time to wait before sending a keep alive ping. - private var keepaliveTimer: Timer? - - /// The amount of time the client has to reply after sending a keep alive ping. Only used if - /// `keepaliveTimer` is set. - private var keepaliveTimeoutTimer: Timer - - /// Opaque data sent in keep alive pings. - private let keepalivePingData: HTTP2PingData - - /// Whether a flush is pending. - private var flushPending: Bool - - /// Whether `channelRead` has been called and `channelReadComplete` hasn't yet been called. - /// Resets once `channelReadComplete` returns. - private var inReadLoop: Bool - - /// The context of the channel this handler is in. - private var context: ChannelHandlerContext? - - /// The current state of the connection. - private var state: StateMachine - - /// The clock. - private let clock: Clock - - /// Whether ALPN is required. - /// If it is but the TLS handshake finished without negotiating a protocol, an error will be fired down the - /// pipeline and the channel will be closed. - private let requireALPN: Bool - - /// A clock providing the current time. - /// - /// This is necessary for testing where a manual clock can be used and advanced from the test. - /// While NIO's `EmbeddedEventLoop` provides control over its view of time (and therefore any - /// events scheduled on it) it doesn't offer a way to get the current time. This is usually done - /// via `NIODeadline`. - enum Clock { - case nio - case manual(Manual) - - func now() -> NIODeadline { - switch self { - case .nio: - return .now() - case .manual(let clock): - return clock.time - } - } - - final class Manual { - private(set) var time: NIODeadline - - init() { - self.time = .uptimeNanoseconds(0) - } - - func advance(by amount: TimeAmount) { - self.time = self.time + amount - } - } - } - - /// Stats about recently written frames. Used to determine whether to reset keep-alive state. - private var frameStats: FrameStats - - struct FrameStats { - private(set) var didWriteHeadersOrData = false - - /// Mark that a HEADERS frame has been written. - mutating func wroteHeaders() { - self.didWriteHeadersOrData = true - } - - /// Mark that DATA frame has been written. - mutating func wroteData() { - self.didWriteHeadersOrData = true - } - - /// Resets the state such that no HEADERS or DATA frames have been written. - mutating func reset() { - self.didWriteHeadersOrData = false - } - } - - /// A synchronous view over this handler. - var syncView: SyncView { - return SyncView(self) - } - - /// A synchronous view over this handler. - /// - /// Methods on this view *must* be called from the same `EventLoop` as the `Channel` in which - /// this handler exists. - struct SyncView { - private let handler: ServerConnectionManagementHandler - - fileprivate init(_ handler: ServerConnectionManagementHandler) { - self.handler = handler - } - - /// Notify the handler that the connection has received a flush event. - func connectionWillFlush() { - // The handler can't rely on `flush(context:)` due to its expected position in the pipeline. - // It's expected to be placed after the HTTP/2 handler (i.e. closer to the application) as - // it needs to receive HTTP/2 frames. However, flushes from stream channels aren't sent down - // the entire connection channel, instead they are sent from the point in the channel they - // are multiplexed from (either the HTTP/2 handler or the HTTP/2 multiplexing handler, - // depending on how multiplexing is configured). - self.handler.eventLoop.assertInEventLoop() - if self.handler.frameStats.didWriteHeadersOrData { - self.handler.frameStats.reset() - self.handler.state.resetKeepaliveState() - } - } - - /// Notify the handler that a HEADERS frame was written in the last write loop. - func wroteHeadersFrame() { - self.handler.eventLoop.assertInEventLoop() - self.handler.frameStats.wroteHeaders() - } - - /// Notify the handler that a DATA frame was written in the last write loop. - func wroteDataFrame() { - self.handler.eventLoop.assertInEventLoop() - self.handler.frameStats.wroteData() - } - } - - /// Creates a new handler which manages the lifecycle of a connection. - /// - /// - Parameters: - /// - eventLoop: The `EventLoop` of the `Channel` this handler is placed in. - /// - maxIdleTime: The maximum amount time a connection may be idle for before being closed. - /// - maxAge: The maximum amount of time a connection may exist before being gracefully closed. - /// - maxGraceTime: The maximum amount of time that the connection has to close gracefully. - /// - keepaliveTime: The amount of time to wait after reading data before sending a keep-alive - /// ping. - /// - keepaliveTimeout: The amount of time the client has to reply after the server sends a - /// keep-alive ping to keep the connection open. The connection is closed if no reply - /// is received. - /// - allowKeepaliveWithoutCalls: Whether the server allows the client to send keep-alive pings - /// when there are no calls in progress. - /// - minPingIntervalWithoutCalls: The minimum allowed interval the client is allowed to send - /// keep-alive pings. Pings more frequent than this interval count as 'strikes' and the - /// connection is closed if there are too many strikes. - /// - clock: A clock providing the current time. - init( - eventLoop: any EventLoop, - maxIdleTime: TimeAmount?, - maxAge: TimeAmount?, - maxGraceTime: TimeAmount?, - keepaliveTime: TimeAmount?, - keepaliveTimeout: TimeAmount?, - allowKeepaliveWithoutCalls: Bool, - minPingIntervalWithoutCalls: TimeAmount, - requireALPN: Bool, - clock: Clock = .nio - ) { - self.eventLoop = eventLoop - - self.maxIdleTimer = maxIdleTime.map { Timer(delay: $0) } - self.maxAgeTimer = maxAge.map { Timer(delay: $0) } - self.maxGraceTimer = maxGraceTime.map { Timer(delay: $0) } - - self.keepaliveTimer = keepaliveTime.map { Timer(delay: $0) } - // Always create a keep alive timeout timer, it's only used if there is a keep alive timer. - self.keepaliveTimeoutTimer = Timer(delay: keepaliveTimeout ?? .seconds(20)) - - // Generate a random value to be used as keep alive ping data. - let pingData = UInt64.random(in: .min ... .max) - self.keepalivePingData = HTTP2PingData(withInteger: pingData) - - self.state = StateMachine( - allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls, - minPingReceiveIntervalWithoutCalls: minPingIntervalWithoutCalls, - goAwayPingData: HTTP2PingData(withInteger: ~pingData) - ) - - self.flushPending = false - self.inReadLoop = false - self.clock = clock - self.frameStats = FrameStats() - - self.requireALPN = requireALPN - } - - func handlerAdded(context: ChannelHandlerContext) { - assert(context.eventLoop === self.eventLoop) - self.context = context - } - - func handlerRemoved(context: ChannelHandlerContext) { - self.context = nil - } - - func channelActive(context: ChannelHandlerContext) { - let view = LoopBoundView(handler: self, context: context) - - self.maxAgeTimer?.schedule(on: context.eventLoop) { - view.initiateGracefulShutdown() - } - - self.maxIdleTimer?.schedule(on: context.eventLoop) { - view.initiateGracefulShutdown() - } - - self.keepaliveTimer?.schedule(on: context.eventLoop) { - view.keepaliveTimerFired() - } - - context.fireChannelActive() - } - - func channelInactive(context: ChannelHandlerContext) { - self.maxIdleTimer?.cancel() - self.maxAgeTimer?.cancel() - self.maxGraceTimer?.cancel() - self.keepaliveTimer?.cancel() - self.keepaliveTimeoutTimer.cancel() - context.fireChannelInactive() - } - - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - switch event { - case let event as NIOHTTP2StreamCreatedEvent: - self._streamCreated(event.streamID, channel: context.channel) - - case let event as StreamClosedEvent: - self._streamClosed(event.streamID, channel: context.channel) - - case is ChannelShouldQuiesceEvent: - self.initiateGracefulShutdown(context: context) - - case TLSUserEvent.handshakeCompleted(let negotiatedProtocol): - if negotiatedProtocol == nil, self.requireALPN { - // No ALPN protocol negotiated but it was required: fire an error and close the channel. - context.fireErrorCaught( - RPCError( - code: .internalError, - message: "ALPN resulted in no protocol being negotiated, but it was required." - ) - ) - context.close(mode: .all, promise: nil) - } - - default: - () - } - - context.fireUserInboundEventTriggered(event) - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - self.inReadLoop = true - - // Any read data indicates that the connection is alive so cancel the keep-alive timers. - self.keepaliveTimer?.cancel() - self.keepaliveTimeoutTimer.cancel() - - let frame = self.unwrapInboundIn(data) - switch frame.payload { - case .ping(let data, let ack): - if ack { - self.handlePingAck(context: context, data: data) - } else { - self.handlePing(context: context, data: data) - } - - default: - () // Only interested in PING frames, ignore the rest. - } - - context.fireChannelRead(data) - } - - func channelReadComplete(context: ChannelHandlerContext) { - while self.flushPending { - self.flushPending = false - context.flush() - } - - self.inReadLoop = false - - // Done reading: schedule the keep-alive timer. - let view = LoopBoundView(handler: self, context: context) - self.keepaliveTimer?.schedule(on: context.eventLoop) { - view.keepaliveTimerFired() - } - - context.fireChannelReadComplete() - } - - func flush(context: ChannelHandlerContext) { - self.maybeFlush(context: context) - } -} - -extension ServerConnectionManagementHandler { - struct LoopBoundView: @unchecked Sendable { - private let handler: ServerConnectionManagementHandler - private let context: ChannelHandlerContext - - init(handler: ServerConnectionManagementHandler, context: ChannelHandlerContext) { - self.handler = handler - self.context = context - } - - func initiateGracefulShutdown() { - self.context.eventLoop.assertInEventLoop() - self.handler.initiateGracefulShutdown(context: self.context) - } - - func keepaliveTimerFired() { - self.context.eventLoop.assertInEventLoop() - self.handler.keepaliveTimerFired(context: self.context) - } - - } -} - -extension ServerConnectionManagementHandler { - struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { - // @unchecked is okay: the only methods do the appropriate event-loop dance. - - private let handler: ServerConnectionManagementHandler - - init(_ handler: ServerConnectionManagementHandler) { - self.handler = handler - } - - func streamCreated(_ id: HTTP2StreamID, channel: any Channel) { - if self.handler.eventLoop.inEventLoop { - self.handler._streamCreated(id, channel: channel) - } else { - self.handler.eventLoop.execute { - self.handler._streamCreated(id, channel: channel) - } - } - } - - func streamClosed(_ id: HTTP2StreamID, channel: any Channel) { - if self.handler.eventLoop.inEventLoop { - self.handler._streamClosed(id, channel: channel) - } else { - self.handler.eventLoop.execute { - self.handler._streamClosed(id, channel: channel) - } - } - } - } - - var http2StreamDelegate: HTTP2StreamDelegate { - return HTTP2StreamDelegate(self) - } - - private func _streamCreated(_ id: HTTP2StreamID, channel: any Channel) { - // The connection isn't idle if a stream is open. - self.maxIdleTimer?.cancel() - self.state.streamOpened(id) - } - - private func _streamClosed(_ id: HTTP2StreamID, channel: any Channel) { - guard let context = self.context else { return } - - switch self.state.streamClosed(id) { - case .startIdleTimer: - let loopBound = LoopBoundView(handler: self, context: context) - self.maxIdleTimer?.schedule(on: context.eventLoop) { - loopBound.initiateGracefulShutdown() - } - - case .close: - context.close(mode: .all, promise: nil) - - case .none: - () - } - } -} - -extension ServerConnectionManagementHandler { - private func maybeFlush(context: ChannelHandlerContext) { - if self.inReadLoop { - self.flushPending = true - } else { - context.flush() - } - } - - private func initiateGracefulShutdown(context: ChannelHandlerContext) { - context.eventLoop.assertInEventLoop() - - // Cancel any timers if initiating shutdown. - self.maxIdleTimer?.cancel() - self.maxAgeTimer?.cancel() - self.keepaliveTimer?.cancel() - self.keepaliveTimeoutTimer.cancel() - - switch self.state.startGracefulShutdown() { - case .sendGoAwayAndPing(let pingData): - // There's a time window between the server sending a GOAWAY frame and the client receiving - // it. During this time the client may open new streams as it doesn't yet know about the - // GOAWAY frame. - // - // The server therefore sends a GOAWAY with the last stream ID set to the maximum stream ID - // and follows it with a PING frame. When the server receives the ack for the PING frame it - // knows that the client has received the initial GOAWAY frame and that no more streams may - // be opened. The server can then send an additional GOAWAY frame with a more representative - // last stream ID. - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway( - lastStreamID: .maxID, - errorCode: .noError, - opaqueData: nil - ) - ) - - let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(pingData, ack: false)) - - context.write(self.wrapOutboundOut(goAway), promise: nil) - context.write(self.wrapOutboundOut(ping), promise: nil) - self.maybeFlush(context: context) - - case .none: - () // Already shutting down. - } - } - - private func handlePing(context: ChannelHandlerContext, data: HTTP2PingData) { - switch self.state.receivedPing(atTime: self.clock.now(), data: data) { - case .enhanceYourCalmThenClose(let streamID): - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway( - lastStreamID: streamID, - errorCode: .enhanceYourCalm, - opaqueData: context.channel.allocator.buffer(string: "too_many_pings") - ) - ) - - context.write(self.wrapOutboundOut(goAway), promise: nil) - self.maybeFlush(context: context) - context.close(promise: nil) - - case .sendAck: - () // ACKs are sent by NIO's HTTP/2 handler, don't double ack. - - case .none: - () - } - } - - private func handlePingAck(context: ChannelHandlerContext, data: HTTP2PingData) { - switch self.state.receivedPingAck(data: data) { - case .sendGoAway(let streamID, let close): - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: streamID, errorCode: .noError, opaqueData: nil) - ) - - context.write(self.wrapOutboundOut(goAway), promise: nil) - self.maybeFlush(context: context) - - if close { - context.close(promise: nil) - } else { - // RPCs may have a grace period for finishing once the second GOAWAY frame has finished. - // If this is set close the connection abruptly once the grace period passes. - let loopBound = NIOLoopBound(context, eventLoop: context.eventLoop) - self.maxGraceTimer?.schedule(on: context.eventLoop) { - loopBound.value.close(promise: nil) - } - } - - case .none: - () - } - } - - private func keepaliveTimerFired(context: ChannelHandlerContext) { - let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(self.keepalivePingData, ack: false)) - context.write(self.wrapInboundOut(ping), promise: nil) - self.maybeFlush(context: context) - - // Schedule a timeout on waiting for the response. - let loopBound = LoopBoundView(handler: self, context: context) - self.keepaliveTimeoutTimer.schedule(on: context.eventLoop) { - loopBound.initiateGracefulShutdown() - } - } -} diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift deleted file mode 100644 index 54965cf13..000000000 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ /dev/null @@ -1,291 +0,0 @@ -/* - * 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. - */ - -package import GRPCCore -package import NIOCore -package import NIOHTTP2 - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -package final class GRPCServerStreamHandler: ChannelDuplexHandler, RemovableChannelHandler { - package typealias InboundIn = HTTP2Frame.FramePayload - package typealias InboundOut = RPCRequestPart - - package typealias OutboundIn = RPCResponsePart - package typealias OutboundOut = HTTP2Frame.FramePayload - - private var stateMachine: GRPCStreamStateMachine - - private var isReading = false - private var flushPending = false - - // We buffer the final status + trailers to avoid reordering issues (i.e., - // if there are messages still not written into the channel because flush has - // not been called, but the server sends back trailers). - private var pendingTrailers: - (trailers: HTTP2Frame.FramePayload, promise: EventLoopPromise?)? - - private let methodDescriptorPromise: EventLoopPromise - - // Existential errors unconditionally allocate, avoid this per-use allocation by doing it - // statically. - private static let handlerRemovedBeforeDescriptorResolved: any Error = RPCError( - code: .unavailable, - message: "RPC stream was closed before we got any Metadata." - ) - - package init( - scheme: Scheme, - acceptedEncodings: CompressionAlgorithmSet, - maxPayloadSize: Int, - methodDescriptorPromise: EventLoopPromise, - skipStateMachineAssertions: Bool = false - ) { - self.stateMachine = .init( - configuration: .server(.init(scheme: scheme, acceptedEncodings: acceptedEncodings)), - maxPayloadSize: maxPayloadSize, - skipAssertions: skipStateMachineAssertions - ) - self.methodDescriptorPromise = methodDescriptorPromise - } -} - -// - MARK: ChannelInboundHandler - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCServerStreamHandler { - package func channelRead(context: ChannelHandlerContext, data: NIOAny) { - self.isReading = true - let frame = self.unwrapInboundIn(data) - switch frame { - case .data(let frameData): - let endStream = frameData.endStream - switch frameData.data { - case .byteBuffer(let buffer): - do { - switch try self.stateMachine.receive(buffer: buffer, endStream: endStream) { - case .endRPCAndForwardErrorStatus_clientOnly: - preconditionFailure( - "OnBufferReceivedAction.endRPCAndForwardErrorStatus should never be returned for the server." - ) - - case .forwardErrorAndClose_serverOnly(let error): - context.fireErrorCaught(error) - context.close(mode: .all, promise: nil) - - case .readInbound: - loop: while true { - switch self.stateMachine.nextInboundMessage() { - case .receiveMessage(let message): - context.fireChannelRead(self.wrapInboundOut(.message(message))) - case .awaitMoreMessages: - break loop - case .noMoreMessages: - context.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - break loop - } - } - case .doNothing: - () - } - } catch let invalidState { - let error = RPCError(invalidState) - context.fireErrorCaught(error) - } - - case .fileRegion: - preconditionFailure("Unexpected IOData.fileRegion") - } - - case .headers(let headers): - do { - let action = try self.stateMachine.receive( - headers: headers.headers, - endStream: headers.endStream - ) - switch action { - case .receivedMetadata(let metadata, let methodDescriptor): - if let methodDescriptor = methodDescriptor { - self.methodDescriptorPromise.succeed(methodDescriptor) - context.fireChannelRead(self.wrapInboundOut(.metadata(metadata))) - } else { - assertionFailure("Method descriptor should have been present if we received metadata.") - } - - case .rejectRPC_serverOnly(let trailers): - self.flushPending = true - self.methodDescriptorPromise.fail( - RPCError( - code: .unavailable, - message: "RPC was rejected." - ) - ) - let response = HTTP2Frame.FramePayload.headers(.init(headers: trailers, endStream: true)) - context.write(self.wrapOutboundOut(response), promise: nil) - - case .receivedStatusAndMetadata_clientOnly: - assertionFailure("Unexpected action") - - case .protocolViolation_serverOnly: - context.writeAndFlush(self.wrapOutboundOut(.rstStream(.protocolError)), promise: nil) - context.close(promise: nil) - - case .doNothing: - () - } - } catch let invalidState { - let error = RPCError(invalidState) - context.fireErrorCaught(error) - } - - case .rstStream: - self.handleUnexpectedInboundClose(context: context, reason: .streamReset) - - case .ping, .goAway, .priority, .settings, .pushPromise, .windowUpdate, - .alternativeService, .origin: - () - } - } - - package func channelReadComplete(context: ChannelHandlerContext) { - self.isReading = false - if self.flushPending { - self.flushPending = false - context.flush() - } - context.fireChannelReadComplete() - } - - package func handlerRemoved(context: ChannelHandlerContext) { - self.stateMachine.tearDown() - self.methodDescriptorPromise.fail(Self.handlerRemovedBeforeDescriptorResolved) - } - - package func channelInactive(context: ChannelHandlerContext) { - self.handleUnexpectedInboundClose(context: context, reason: .channelInactive) - context.fireChannelInactive() - } - - package func errorCaught(context: ChannelHandlerContext, error: any Error) { - self.handleUnexpectedInboundClose(context: context, reason: .errorThrown(error)) - } - - private func handleUnexpectedInboundClose( - context: ChannelHandlerContext, - reason: GRPCStreamStateMachine.UnexpectedInboundCloseReason - ) { - switch self.stateMachine.unexpectedInboundClose(reason: reason) { - case .fireError_serverOnly(let wrappedError): - context.fireErrorCaught(wrappedError) - case .doNothing: - () - case .forwardStatus_clientOnly: - assertionFailure( - "`forwardStatus` should only happen on the client side, never on the server." - ) - } - } -} - -// - MARK: ChannelOutboundHandler - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCServerStreamHandler { - package func write( - context: ChannelHandlerContext, - data: NIOAny, - promise: EventLoopPromise? - ) { - let frame = self.unwrapOutboundIn(data) - switch frame { - case .metadata(let metadata): - do { - self.flushPending = true - let headers = try self.stateMachine.send(metadata: metadata) - context.write(self.wrapOutboundOut(.headers(.init(headers: headers))), promise: promise) - } catch let invalidState { - let error = RPCError(invalidState) - promise?.fail(error) - context.fireErrorCaught(error) - } - - case .message(let message): - do { - try self.stateMachine.send(message: message, promise: promise) - } catch let invalidState { - let error = RPCError(invalidState) - promise?.fail(error) - context.fireErrorCaught(error) - } - - case .status(let status, let metadata): - do { - let headers = try self.stateMachine.send(status: status, metadata: metadata) - let response = HTTP2Frame.FramePayload.headers(.init(headers: headers, endStream: true)) - self.pendingTrailers = (response, promise) - } catch let invalidState { - let error = RPCError(invalidState) - promise?.fail(error) - context.fireErrorCaught(error) - } - } - } - - package func flush(context: ChannelHandlerContext) { - if self.isReading { - // We don't want to flush yet if we're still in a read loop. - return - } - - do { - loop: while true { - switch try self.stateMachine.nextOutboundFrame() { - case .sendFrame(let byteBuffer, let promise): - self.flushPending = true - context.write( - self.wrapOutboundOut(.data(.init(data: .byteBuffer(byteBuffer)))), - promise: promise - ) - - case .noMoreMessages: - if let pendingTrailers = self.pendingTrailers { - self.flushPending = true - self.pendingTrailers = nil - context.write( - self.wrapOutboundOut(pendingTrailers.trailers), - promise: pendingTrailers.promise - ) - } - break loop - - case .awaitMoreMessages: - break loop - - case .closeAndFailPromise(let promise, let error): - context.close(mode: .all, promise: nil) - promise?.fail(error) - } - } - - if self.flushPending { - self.flushPending = false - context.flush() - } - } catch let invalidState { - let error = RPCError(invalidState) - context.fireErrorCaught(error) - } - } -} diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift deleted file mode 100644 index 900799a61..000000000 --- a/Sources/GRPCHTTP2Core/Server/HTTP2ListenerFactory.swift +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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. - */ - -package import NIOCore -package import NIOExtras - -/// A factory to produce `NIOAsyncChannel`s to listen for new HTTP/2 connections. -/// -/// - SeeAlso: ``CommonHTTP2ServerTransport`` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package protocol HTTP2ListenerFactory: Sendable { - typealias AcceptedChannel = ( - ChannelPipeline.SynchronousOperations.HTTP2ConnectionChannel, - ChannelPipeline.SynchronousOperations.HTTP2StreamMultiplexer - ) - - func makeListeningChannel( - eventLoopGroup: any EventLoopGroup, - address: SocketAddress, - serverQuiescingHelper: ServerQuiescingHelper - ) async throws -> NIOAsyncChannel -} diff --git a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift b/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift deleted file mode 100644 index e93b09c26..000000000 --- a/Sources/GRPCHTTP2Core/Server/HTTP2ServerTransport.swift +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore -internal import NIOHTTP2 - -/// A namespace for the HTTP/2 server transport. -public enum HTTP2ServerTransport {} - -extension HTTP2ServerTransport { - /// A namespace for HTTP/2 server transport configuration. - public enum Config {} -} - -extension HTTP2ServerTransport.Config { - public struct Compression: Sendable, Hashable { - /// Compression algorithms enabled for inbound messages. - /// - /// - Note: `CompressionAlgorithm.none` is always supported, even if it isn't set here. - public var enabledAlgorithms: CompressionAlgorithmSet - - /// Creates a new compression configuration. - /// - /// - SeeAlso: ``defaults``. - public init(enabledAlgorithms: CompressionAlgorithmSet) { - self.enabledAlgorithms = enabledAlgorithms - } - - /// Default values, compression is disabled. - public static var defaults: Self { - Self(enabledAlgorithms: .none) - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Keepalive: Sendable, Hashable { - /// The amount of time to wait after reading data before sending a keepalive ping. - public var time: Duration - - /// The amount of time the server has to respond to a keepalive ping before the connection is closed. - public var timeout: Duration - - /// Configuration for how the server enforces client keepalive. - public var clientBehavior: ClientKeepaliveBehavior - - /// Creates a new keepalive configuration. - public init( - time: Duration, - timeout: Duration, - clientBehavior: ClientKeepaliveBehavior - ) { - self.time = time - self.timeout = timeout - self.clientBehavior = clientBehavior - } - - /// Default values. The time after reading data a ping should be sent defaults to 2 hours, the timeout for - /// keepalive pings defaults to 20 seconds, pings are not permitted when no calls are in progress, and - /// the minimum allowed interval for clients to send pings defaults to 5 minutes. - public static var defaults: Self { - Self( - time: .seconds(2 * 60 * 60), // 2 hours - timeout: .seconds(20), - clientBehavior: .defaults - ) - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct ClientKeepaliveBehavior: Sendable, Hashable { - /// The minimum allowed interval the client is allowed to send keep-alive pings. - /// Pings more frequent than this interval count as 'strikes' and the connection is closed if there are - /// too many strikes. - public var minPingIntervalWithoutCalls: Duration - - /// Whether the server allows the client to send keepalive pings when there are no calls in progress. - public var allowWithoutCalls: Bool - - /// Creates a new configuration for permitted client keepalive behavior. - public init( - minPingIntervalWithoutCalls: Duration, - allowWithoutCalls: Bool - ) { - self.minPingIntervalWithoutCalls = minPingIntervalWithoutCalls - self.allowWithoutCalls = allowWithoutCalls - } - - /// Default values. The time after reading data a ping should be sent defaults to 2 hours, the timeout for - /// keepalive pings defaults to 20 seconds, pings are not permitted when no calls are in progress, and - /// the minimum allowed interval for clients to send pings defaults to 5 minutes. - public static var defaults: Self { - Self(minPingIntervalWithoutCalls: .seconds(5 * 60), allowWithoutCalls: false) - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - public struct Connection: Sendable, Hashable { - /// The maximum amount of time a connection may exist before being gracefully closed. - public var maxAge: Duration? - - /// The maximum amount of time that the connection has to close gracefully. - public var maxGraceTime: Duration? - - /// The maximum amount of time a connection may be idle before it's closed. - public var maxIdleTime: Duration? - - /// Configuration for keepalive used to detect broken connections. - /// - /// - SeeAlso: gRFC A8 for client side keepalive, and gRFC A9 for server connection management. - public var keepalive: Keepalive - - public init( - maxAge: Duration?, - maxGraceTime: Duration?, - maxIdleTime: Duration?, - keepalive: Keepalive - ) { - self.maxAge = maxAge - self.maxGraceTime = maxGraceTime - self.maxIdleTime = maxIdleTime - self.keepalive = keepalive - } - - /// Default values. The max connection age, max grace time, and max idle time default to - /// `nil` (i.e. infinite). See ``HTTP2ServerTransport/Config/Keepalive/defaults`` for keepalive - /// defaults. - public static var defaults: Self { - Self(maxAge: nil, maxGraceTime: nil, maxIdleTime: nil, keepalive: .defaults) - } - } - - public struct HTTP2: Sendable, Hashable { - /// The maximum frame size to be used in an HTTP/2 connection. - public var maxFrameSize: Int - - /// The target window size for this connection. - /// - /// - Note: This will also be set as the initial window size for the connection. - public var targetWindowSize: Int - - /// The number of concurrent streams on the HTTP/2 connection. - public var maxConcurrentStreams: Int? - - public init( - maxFrameSize: Int, - targetWindowSize: Int, - maxConcurrentStreams: Int? - ) { - self.maxFrameSize = maxFrameSize - self.targetWindowSize = targetWindowSize - self.maxConcurrentStreams = maxConcurrentStreams - } - - /// Default values. The max frame size defaults to 2^14, the target window size defaults to 2^16-1, and - /// the max concurrent streams default to infinite. - public static var defaults: Self { - Self( - maxFrameSize: 1 << 14, - targetWindowSize: (1 << 16) - 1, - maxConcurrentStreams: nil - ) - } - } - - public struct RPC: Sendable, Hashable { - /// The maximum request payload size. - public var maxRequestPayloadSize: Int - - public init(maxRequestPayloadSize: Int) { - self.maxRequestPayloadSize = maxRequestPayloadSize - } - - /// Default values. Maximum request payload size defaults to 4MiB. - public static var defaults: Self { - Self(maxRequestPayloadSize: 4 * 1024 * 1024) - } - } -} diff --git a/Sources/GRPCHTTP2Transport/Exports.swift b/Sources/GRPCHTTP2Transport/Exports.swift deleted file mode 100644 index 64f679933..000000000 --- a/Sources/GRPCHTTP2Transport/Exports.swift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -@_exported import GRPCCore -@_exported import GRPCHTTP2Core -@_exported import GRPCHTTP2TransportNIOPosix -@_exported import GRPCHTTP2TransportNIOTransportServices diff --git a/Sources/GRPCHTTP2TransportNIOPosix/Exports.swift b/Sources/GRPCHTTP2TransportNIOPosix/Exports.swift deleted file mode 100644 index 395308d46..000000000 --- a/Sources/GRPCHTTP2TransportNIOPosix/Exports.swift +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -@_exported import GRPCCore -@_exported import GRPCHTTP2Core diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift deleted file mode 100644 index baf96639a..000000000 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift +++ /dev/null @@ -1,298 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore -public import GRPCHTTP2Core // should be @usableFromInline -public import NIOCore // has to be public because of EventLoopGroup param in init -public import NIOPosix // has to be public because of default argument value in init - -#if canImport(NIOSSL) -private import NIOSSL -#endif - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport { - /// A `ClientTransport` using HTTP/2 built on top of `NIOPosix`. - /// - /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use - /// on Linux and Darwin based platforms (macOS, iOS, etc.). However, it's *strongly* recommended - /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of - /// the `HTTP2ClientTransport`. - /// - /// To use this transport you need to provide a 'target' to connect to which will be resolved - /// by an appropriate resolver from the resolver registry. By default the resolver registry can - /// resolve DNS targets, IPv4 and IPv6 targets, Unix domain socket targets, and Virtual Socket - /// targets. If you use a custom target you must also provide an appropriately configured - /// registry. - /// - /// You can control various aspects of connection creation, management, security and RPC behavior via - /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via - /// the `ServiceConfig` (if it isn't provided by a resolver). - /// - /// Beyond creating the transport you don't need to interact with it directly, instead, pass it - /// to a `GRPCClient`: - /// - /// ```swift - /// try await withThrowingDiscardingTaskGroup { group in - /// let transport = try HTTP2ClientTransport.Posix( - /// target: .ipv4(host: "example.com"), - /// config: .defaults(transportSecurity: .plaintext) - /// ) - /// let client = GRPCClient(transport: transport) - /// group.addTask { - /// try await client.run() - /// } - /// - /// // ... - /// } - /// ``` - public struct Posix: ClientTransport { - private let channel: GRPCChannel - - /// Creates a new NIOPosix-based HTTP/2 client transport. - /// - /// - Parameters: - /// - target: A target to resolve. - /// - config: Configuration for the transport. - /// - resolverRegistry: A registry of resolver factories. - /// - serviceConfig: Service config controlling how the transport should establish and - /// load-balance connections. - /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must - /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from - /// a `MultiThreadedEventLoopGroup`. - /// - Throws: When no suitable resolver could be found for the `target`. - public init( - target: any ResolvableTarget, - config: Config, - resolverRegistry: NameResolverRegistry = .defaults, - serviceConfig: ServiceConfig = ServiceConfig(), - eventLoopGroup: any EventLoopGroup = .singletonMultiThreadedEventLoopGroup - ) throws { - guard let resolver = resolverRegistry.makeResolver(for: target) else { - throw RuntimeError( - code: .transportError, - message: """ - No suitable resolvers to resolve '\(target)'. You must make sure that the resolver \ - registry has a suitable name resolver factory registered for the given target. - """ - ) - } - - self.channel = GRPCChannel( - resolver: resolver, - connector: try Connector(eventLoopGroup: eventLoopGroup, config: config), - config: GRPCChannel.Config(posix: config), - defaultServiceConfig: serviceConfig - ) - } - - public var retryThrottle: RetryThrottle? { - self.channel.retryThrottle - } - - public func connect() async { - await self.channel.connect() - } - - public func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? { - self.channel.config(forMethod: descriptor) - } - - public func beginGracefulShutdown() { - self.channel.beginGracefulShutdown() - } - - public func withStream( - descriptor: MethodDescriptor, - options: CallOptions, - _ closure: (RPCStream) async throws -> T - ) async throws -> T { - try await self.channel.withStream(descriptor: descriptor, options: options, closure) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport.Posix { - struct Connector: HTTP2Connector { - private let config: HTTP2ClientTransport.Posix.Config - private let eventLoopGroup: any EventLoopGroup - - #if canImport(NIOSSL) - private let nioSSLContext: NIOSSLContext? - private let serverHostname: String? - #endif - - init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) throws { - self.eventLoopGroup = eventLoopGroup - self.config = config - - #if canImport(NIOSSL) - switch self.config.transportSecurity.wrapped { - case .plaintext: - self.nioSSLContext = nil - self.serverHostname = nil - case .tls(let tlsConfig): - do { - self.nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig)) - self.serverHostname = tlsConfig.serverHostname - } catch { - throw RuntimeError( - code: .transportError, - message: "Couldn't create SSL context, check your TLS configuration.", - cause: error - ) - } - } - #endif - } - - func establishConnection( - to address: GRPCHTTP2Core.SocketAddress - ) async throws -> HTTP2Connection { - let (channel, multiplexer) = try await ClientBootstrap( - group: self.eventLoopGroup - ).connect(to: address) { channel in - channel.eventLoop.makeCompletedFuture { - #if canImport(NIOSSL) - if let nioSSLContext = self.nioSSLContext { - try channel.pipeline.syncOperations.addHandler( - NIOSSLClientHandler( - context: nioSSLContext, - serverHostname: self.serverHostname - ) - ) - } - #endif - - return try channel.pipeline.syncOperations.configureGRPCClientPipeline( - channel: channel, - config: GRPCChannel.Config(posix: self.config) - ) - } - } - - return HTTP2Connection(channel: channel, multiplexer: multiplexer, isPlaintext: true) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport.Posix { - public struct Config: Sendable { - /// Configuration for HTTP/2 connections. - public var http2: HTTP2ClientTransport.Config.HTTP2 - - /// Configuration for backoff used when establishing a connection. - public var backoff: HTTP2ClientTransport.Config.Backoff - - /// Configuration for connection management. - public var connection: HTTP2ClientTransport.Config.Connection - - /// Compression configuration. - public var compression: HTTP2ClientTransport.Config.Compression - - /// The transport's security. - public var transportSecurity: TransportSecurity - - /// Creates a new connection configuration. - /// - /// - Parameters: - /// - http2: HTTP2 configuration. - /// - backoff: Backoff configuration. - /// - connection: Connection configuration. - /// - compression: Compression configuration. - /// - transportSecurity: The transport's security configuration. - /// - /// - SeeAlso: ``defaults(transportSecurity:configure:)`` - public init( - http2: HTTP2ClientTransport.Config.HTTP2, - backoff: HTTP2ClientTransport.Config.Backoff, - connection: HTTP2ClientTransport.Config.Connection, - compression: HTTP2ClientTransport.Config.Compression, - transportSecurity: TransportSecurity - ) { - self.http2 = http2 - self.connection = connection - self.backoff = backoff - self.compression = compression - self.transportSecurity = transportSecurity - } - - /// Default values. - /// - /// - Parameters: - /// - transportSecurity: The security settings applied to the transport. - /// - configure: A closure which allows you to modify the defaults before returning them. - public static func defaults( - transportSecurity: TransportSecurity, - configure: (_ config: inout Self) -> Void = { _ in } - ) -> Self { - var config = Self( - http2: .defaults, - backoff: .defaults, - connection: .defaults, - compression: .defaults, - transportSecurity: transportSecurity - ) - configure(&config) - return config - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel.Config { - init(posix: HTTP2ClientTransport.Posix.Config) { - self.init( - http2: posix.http2, - backoff: posix.backoff, - connection: posix.connection, - compression: posix.compression - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientTransport where Self == HTTP2ClientTransport.Posix { - /// Creates a new Posix based HTTP/2 client transport. - /// - /// - Parameters: - /// - target: A target to resolve. - /// - config: Configuration for the transport. - /// - resolverRegistry: A registry of resolver factories. - /// - serviceConfig: Service config controlling how the transport should establish and - /// load-balance connections. - /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must - /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from - /// a `MultiThreadedEventLoopGroup`. - /// - Throws: When no suitable resolver could be found for the `target`. - public static func http2NIOPosix( - target: any ResolvableTarget, - config: HTTP2ClientTransport.Posix.Config, - resolverRegistry: NameResolverRegistry = .defaults, - serviceConfig: ServiceConfig = ServiceConfig(), - eventLoopGroup: any EventLoopGroup = .singletonMultiThreadedEventLoopGroup - ) throws -> Self { - return try HTTP2ClientTransport.Posix( - target: target, - config: config, - resolverRegistry: resolverRegistry, - serviceConfig: serviceConfig, - eventLoopGroup: eventLoopGroup - ) - } -} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift b/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift deleted file mode 100644 index 48420178c..000000000 --- a/Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift +++ /dev/null @@ -1,286 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore -public import GRPCHTTP2Core // should be @usableFromInline -internal import NIOCore -internal import NIOExtras -internal import NIOHTTP2 -public import NIOPosix // has to be public because of default argument value in init -private import Synchronization - -#if canImport(NIOSSL) -import NIOSSL -#endif - -extension HTTP2ServerTransport { - /// A `ServerTransport` using HTTP/2 built on top of `NIOPosix`. - /// - /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use - /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended - /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of - /// the `HTTP2ServerTransport`. - /// - /// You can control various aspects of connection creation, management, security and RPC behavior via - /// the ``Config``. - /// - /// Beyond creating the transport you don't need to interact with it directly, instead, pass it - /// to a `GRPCServer`: - /// - /// ```swift - /// try await withThrowingDiscardingTaskGroup { group in - /// let transport = HTTP2ServerTransport.Posix( - /// address: .ipv4(host: "127.0.0.1", port: 0), - /// config: .defaults(transportSecurity: .plaintext) - /// ) - /// let server = GRPCServer(transport: transport, services: someServices) - /// group.addTask { - /// try await server.serve() - /// } - /// - /// // ... - /// } - /// ``` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct Posix: ServerTransport, ListeningServerTransport { - private struct ListenerFactory: HTTP2ListenerFactory { - let config: Config - - func makeListeningChannel( - eventLoopGroup: any EventLoopGroup, - address: GRPCHTTP2Core.SocketAddress, - serverQuiescingHelper: ServerQuiescingHelper - ) async throws -> NIOAsyncChannel { - #if canImport(NIOSSL) - let sslContext: NIOSSLContext? - - switch self.config.transportSecurity.wrapped { - case .plaintext: - sslContext = nil - case .tls(let tlsConfig): - do { - sslContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig)) - } catch { - throw RuntimeError( - code: .transportError, - message: "Couldn't create SSL context, check your TLS configuration.", - cause: error - ) - } - } - #endif - - let serverChannel = try await ServerBootstrap(group: eventLoopGroup) - .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) - .serverChannelInitializer { channel in - let quiescingHandler = serverQuiescingHelper.makeServerChannelHandler(channel: channel) - return channel.pipeline.addHandler(quiescingHandler) - } - .bind(to: address) { channel in - channel.eventLoop.makeCompletedFuture { - #if canImport(NIOSSL) - if let sslContext { - try channel.pipeline.syncOperations.addHandler( - NIOSSLServerHandler(context: sslContext) - ) - } - #endif - - let requireALPN: Bool - let scheme: Scheme - switch self.config.transportSecurity.wrapped { - case .plaintext: - requireALPN = false - scheme = .http - case .tls(let tlsConfig): - requireALPN = tlsConfig.requireALPN - scheme = .https - } - - return try channel.pipeline.syncOperations.configureGRPCServerPipeline( - channel: channel, - compressionConfig: self.config.compression, - connectionConfig: self.config.connection, - http2Config: self.config.http2, - rpcConfig: self.config.rpc, - requireALPN: requireALPN, - scheme: scheme - ) - } - } - - return serverChannel - } - } - - private let underlyingTransport: CommonHTTP2ServerTransport - - /// The listening address for this server transport. - /// - /// It is an `async` property because it will only return once the address has been successfully bound. - /// - /// - Throws: A runtime error will be thrown if the address could not be bound or is not bound any - /// longer, because the transport isn't listening anymore. It can also throw if the transport returned an - /// invalid address. - public var listeningAddress: GRPCHTTP2Core.SocketAddress { - get async throws { - try await self.underlyingTransport.listeningAddress - } - } - - /// Create a new `Posix` transport. - /// - /// - Parameters: - /// - address: The address to which the server should be bound. - /// - config: The transport configuration. - /// - eventLoopGroup: The ELG from which to get ELs to run this transport. - public init( - address: GRPCHTTP2Core.SocketAddress, - config: Config, - eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup - ) { - let factory = ListenerFactory(config: config) - let helper = ServerQuiescingHelper(group: eventLoopGroup) - self.underlyingTransport = CommonHTTP2ServerTransport( - address: address, - eventLoopGroup: eventLoopGroup, - quiescingHelper: helper, - listenerFactory: factory - ) - } - - public func listen( - streamHandler: @escaping @Sendable ( - _ stream: RPCStream, - _ context: ServerContext - ) async -> Void - ) async throws { - try await self.underlyingTransport.listen(streamHandler: streamHandler) - } - - public func beginGracefulShutdown() { - self.underlyingTransport.beginGracefulShutdown() - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ServerTransport.Posix { - /// Config for the `Posix` transport. - public struct Config: Sendable { - /// Compression configuration. - public var compression: HTTP2ServerTransport.Config.Compression - - /// Connection configuration. - public var connection: HTTP2ServerTransport.Config.Connection - - /// HTTP2 configuration. - public var http2: HTTP2ServerTransport.Config.HTTP2 - - /// RPC configuration. - public var rpc: HTTP2ServerTransport.Config.RPC - - /// The transport's security. - public var transportSecurity: TransportSecurity - - /// Construct a new `Config`. - /// - /// - Parameters: - /// - http2: HTTP2 configuration. - /// - rpc: RPC configuration. - /// - connection: Connection configuration. - /// - compression: Compression configuration. - /// - transportSecurity: The transport's security configuration. - /// - /// - SeeAlso: ``defaults(transportSecurity:configure:)`` - public init( - http2: HTTP2ServerTransport.Config.HTTP2, - rpc: HTTP2ServerTransport.Config.RPC, - connection: HTTP2ServerTransport.Config.Connection, - compression: HTTP2ServerTransport.Config.Compression, - transportSecurity: TransportSecurity - ) { - self.compression = compression - self.connection = connection - self.http2 = http2 - self.rpc = rpc - self.transportSecurity = transportSecurity - } - - /// Default values for the different configurations. - /// - /// - Parameters: - /// - transportSecurity: The security settings applied to the transport. - /// - configure: A closure which allows you to modify the defaults before returning them. - public static func defaults( - transportSecurity: TransportSecurity, - configure: (_ config: inout Self) -> Void = { _ in } - ) -> Self { - var config = Self( - http2: .defaults, - rpc: .defaults, - connection: .defaults, - compression: .defaults, - transportSecurity: transportSecurity - ) - configure(&config) - return config - } - } -} - -extension ServerBootstrap { - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - fileprivate func bind( - to address: GRPCHTTP2Core.SocketAddress, - childChannelInitializer: @escaping @Sendable (any Channel) -> EventLoopFuture - ) async throws -> NIOAsyncChannel { - if let virtualSocket = address.virtualSocket { - return try await self.bind( - to: VsockAddress(virtualSocket), - childChannelInitializer: childChannelInitializer - ) - } else { - return try await self.bind( - to: NIOCore.SocketAddress(address), - childChannelInitializer: childChannelInitializer - ) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ServerTransport where Self == HTTP2ServerTransport.Posix { - /// Create a new `Posix` based HTTP/2 server transport. - /// - /// - Parameters: - /// - address: The address to which the server should be bound. - /// - config: The transport configuration. - /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to the server on. This must - /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from - /// a `MultiThreadedEventLoopGroup`. - public static func http2NIOPosix( - address: GRPCHTTP2Core.SocketAddress, - config: HTTP2ServerTransport.Posix.Config, - eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup - ) -> Self { - return HTTP2ServerTransport.Posix( - address: address, - config: config, - eventLoopGroup: eventLoopGroup - ) - } -} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift deleted file mode 100644 index 5eb6e193e..000000000 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOClientBootstrap+SocketAddress.swift +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore -internal import GRPCHTTP2Core -internal import NIOCore -internal import NIOPosix - -extension ClientBootstrap { - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - func connect( - to address: GRPCHTTP2Core.SocketAddress, - _ configure: @Sendable @escaping (any Channel) -> EventLoopFuture - ) async throws -> Result { - if let ipv4 = address.ipv4 { - return try await self.connect(to: NIOCore.SocketAddress(ipv4), channelInitializer: configure) - } else if let ipv6 = address.ipv6 { - return try await self.connect(to: NIOCore.SocketAddress(ipv6), channelInitializer: configure) - } else if let uds = address.unixDomainSocket { - return try await self.connect(to: NIOCore.SocketAddress(uds), channelInitializer: configure) - } else if let vsock = address.virtualSocket { - return try await self.connect(to: VsockAddress(vsock), channelInitializer: configure) - } else { - throw RuntimeError( - code: .transportError, - message: """ - Unhandled socket address '\(address)', this is a gRPC Swift bug. Please file an issue \ - against the project. - """ - ) - } - } -} - -extension NIOPosix.VsockAddress { - init(_ address: GRPCHTTP2Core.SocketAddress.VirtualSocket) { - self.init( - cid: ContextID(rawValue: address.contextID.rawValue), - port: Port(rawValue: address.port.rawValue) - ) - } -} diff --git a/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift b/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift deleted file mode 100644 index 94436f56e..000000000 --- a/Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOSSL - -extension NIOSSLSerializationFormats { - fileprivate init(_ format: TLSConfig.SerializationFormat) { - switch format.wrapped { - case .pem: - self = .pem - case .der: - self = .der - } - } -} - -extension Sequence { - func sslCertificateSources() throws -> [NIOSSLCertificateSource] { - var certificateSources: [NIOSSLCertificateSource] = [] - for source in self { - switch source.wrapped { - case .bytes(let bytes, let serializationFormat): - switch serializationFormat.wrapped { - case .der: - certificateSources.append( - .certificate(try NIOSSLCertificate(bytes: bytes, format: .der)) - ) - - case .pem: - let certificates = try NIOSSLCertificate.fromPEMBytes(bytes).map { - NIOSSLCertificateSource.certificate($0) - } - certificateSources.append(contentsOf: certificates) - } - - case .file(let path, let serializationFormat): - switch serializationFormat.wrapped { - case .der: - certificateSources.append( - .certificate(try NIOSSLCertificate(file: path, format: .der)) - ) - - case .pem: - let certificates = try NIOSSLCertificate.fromPEMFile(path).map { - NIOSSLCertificateSource.certificate($0) - } - certificateSources.append(contentsOf: certificates) - } - } - } - return certificateSources - } -} - -extension NIOSSLPrivateKey { - fileprivate convenience init( - privateKey source: TLSConfig.PrivateKeySource - ) throws { - switch source.wrapped { - case .file(let path, let serializationFormat): - try self.init( - file: path, - format: NIOSSLSerializationFormats(serializationFormat) - ) - case .bytes(let bytes, let serializationFormat): - try self.init( - bytes: bytes, - format: NIOSSLSerializationFormats(serializationFormat) - ) - } - } -} - -extension NIOSSLTrustRoots { - fileprivate init(_ trustRoots: TLSConfig.TrustRootsSource) throws { - switch trustRoots.wrapped { - case .certificates(let certificateSources): - let certificates = try certificateSources.map { source in - switch source.wrapped { - case .bytes(let bytes, let serializationFormat): - return try NIOSSLCertificate( - bytes: bytes, - format: NIOSSLSerializationFormats(serializationFormat) - ) - case .file(let path, let serializationFormat): - return try NIOSSLCertificate( - file: path, - format: NIOSSLSerializationFormats(serializationFormat) - ) - } - } - self = .certificates(certificates) - - case .systemDefault: - self = .default - } - } -} - -extension CertificateVerification { - fileprivate init( - _ verificationMode: TLSConfig.CertificateVerification - ) { - switch verificationMode.wrapped { - case .doNotVerify: - self = .none - case .fullVerification: - self = .fullVerification - case .noHostnameVerification: - self = .noHostnameVerification - } - } -} - -extension TLSConfiguration { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package init(_ tlsConfig: HTTP2ServerTransport.Posix.Config.TLS) throws { - let certificateChain = try tlsConfig.certificateChain.sslCertificateSources() - let privateKey = try NIOSSLPrivateKey(privateKey: tlsConfig.privateKey) - - self = TLSConfiguration.makeServerConfiguration( - certificateChain: certificateChain, - privateKey: .privateKey(privateKey) - ) - self.minimumTLSVersion = .tlsv12 - self.certificateVerification = CertificateVerification( - tlsConfig.clientCertificateVerification - ) - self.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots) - self.applicationProtocols = ["grpc-exp", "h2"] - } - - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package init(_ tlsConfig: HTTP2ClientTransport.Posix.Config.TLS) throws { - self = TLSConfiguration.makeClientConfiguration() - self.certificateChain = try tlsConfig.certificateChain.sslCertificateSources() - - if let privateKey = tlsConfig.privateKey { - let privateKeySource = try NIOSSLPrivateKey(privateKey: privateKey) - self.privateKey = .privateKey(privateKeySource) - } - - self.minimumTLSVersion = .tlsv12 - self.certificateVerification = CertificateVerification( - tlsConfig.serverCertificateVerification - ) - self.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots) - self.applicationProtocols = ["grpc-exp", "h2"] - } -} -#endif diff --git a/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift b/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift deleted file mode 100644 index 2e42d58c1..000000000 --- a/Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift +++ /dev/null @@ -1,299 +0,0 @@ -/* - * 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. - */ - -public enum TLSConfig: Sendable { - /// The serialization format of the provided certificates and private keys. - public struct SerializationFormat: Sendable, Equatable { - package enum Wrapped { - case pem - case der - } - - package let wrapped: Wrapped - - public static let pem = Self(wrapped: .pem) - public static let der = Self(wrapped: .der) - } - - /// A description of where a certificate is coming from: either a byte array or a file. - /// The serialization format is specified by ``TLSConfig/SerializationFormat``. - public struct CertificateSource: Sendable { - package enum Wrapped { - case file(path: String, format: SerializationFormat) - case bytes(bytes: [UInt8], format: SerializationFormat) - } - - package let wrapped: Wrapped - - /// The certificate's source is a file. - /// - Parameters: - /// - path: The file path containing the certificate. - /// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``. - /// - Returns: A source describing the certificate source is the given file. - public static func file(path: String, format: SerializationFormat) -> Self { - Self(wrapped: .file(path: path, format: format)) - } - - /// The certificate's source is an array of bytes. - /// - Parameters: - /// - bytes: The array of bytes making up the certificate. - /// - format: The certificate's format, as a ``TLSConfig/SerializationFormat``. - /// - Returns: A source describing the certificate source is the given bytes. - public static func bytes(_ bytes: [UInt8], format: SerializationFormat) -> Self { - Self(wrapped: .bytes(bytes: bytes, format: format)) - } - } - - /// A description of where the private key is coming from: either a byte array or a file. - /// The serialization format is specified by ``TLSConfig/SerializationFormat``. - public struct PrivateKeySource: Sendable { - package enum Wrapped { - case file(path: String, format: SerializationFormat) - case bytes(bytes: [UInt8], format: SerializationFormat) - } - - package let wrapped: Wrapped - - /// The private key's source is a file. - /// - Parameters: - /// - path: The file path containing the private key. - /// - format: The private key's format, as a ``TLSConfig/SerializationFormat``. - /// - Returns: A source describing the private key source is the given file. - public static func file(path: String, format: SerializationFormat) -> Self { - Self(wrapped: .file(path: path, format: format)) - } - - /// The private key's source is an array of bytes. - /// - Parameters: - /// - bytes: The array of bytes making up the private key. - /// - format: The private key's format, as a ``TLSConfig/SerializationFormat``. - /// - Returns: A source describing the private key source is the given bytes. - public static func bytes( - _ bytes: [UInt8], - format: SerializationFormat - ) -> Self { - Self(wrapped: .bytes(bytes: bytes, format: format)) - } - } - - /// A description of where the trust roots are coming from: either a custom certificate chain, or the system default trust store. - public struct TrustRootsSource: Sendable { - package enum Wrapped { - case certificates([CertificateSource]) - case systemDefault - } - - package let wrapped: Wrapped - - /// A list of ``TLSConfig/CertificateSource``s making up the - /// chain of trust. - /// - Parameter certificateSources: The sources for the certificates that make up the chain of trust. - /// - Returns: A trust root for the given chain of trust. - public static func certificates( - _ certificateSources: [CertificateSource] - ) -> Self { - Self(wrapped: .certificates(certificateSources)) - } - - /// The system default trust store. - public static let systemDefault: Self = Self(wrapped: .systemDefault) - } - - /// How to verify client certificates. - public struct CertificateVerification: Sendable { - package enum Wrapped { - case doNotVerify - case fullVerification - case noHostnameVerification - } - - package let wrapped: Wrapped - - /// All certificate verification disabled. - public static let noVerification: Self = Self(wrapped: .doNotVerify) - - /// Certificates will be validated against the trust store, but will not be checked to see if they are valid for the given hostname. - public static let noHostnameVerification: Self = Self(wrapped: .noHostnameVerification) - - /// Certificates will be validated against the trust store and checked against the hostname of the service we are contacting. - public static let fullVerification: Self = Self(wrapped: .fullVerification) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ServerTransport.Posix.Config { - /// The security configuration for this connection. - public struct TransportSecurity: Sendable { - package enum Wrapped: Sendable { - case plaintext - case tls(TLS) - } - - package let wrapped: Wrapped - - /// This connection is plaintext: no encryption will take place. - public static let plaintext = Self(wrapped: .plaintext) - - #if canImport(NIOSSL) - /// This connection will use TLS. - public static func tls(_ tls: TLS) -> Self { - Self(wrapped: .tls(tls)) - } - #endif - } - - public struct TLS: Sendable { - /// The certificates the server will offer during negotiation. - public var certificateChain: [TLSConfig.CertificateSource] - - /// The private key associated with the leaf certificate. - public var privateKey: TLSConfig.PrivateKeySource - - /// How to verify the client certificate, if one is presented. - public var clientCertificateVerification: TLSConfig.CertificateVerification - - /// The trust roots to be used when verifying client certificates. - public var trustRoots: TLSConfig.TrustRootsSource - - /// Whether ALPN is required. - /// - /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. - public var requireALPN: Bool - - /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: - /// - `clientCertificateVerificationMode` equals `doNotVerify` - /// - `trustRoots` equals `systemDefault` - /// - `requireALPN` equals `false` - /// - /// - Parameters: - /// - certificateChain: The certificates the server will offer during negotiation. - /// - privateKey: The private key associated with the leaf certificate. - /// - Returns: A new HTTP2 NIO Posix transport TLS config. - public static func defaults( - certificateChain: [TLSConfig.CertificateSource], - privateKey: TLSConfig.PrivateKeySource - ) -> Self { - Self( - certificateChain: certificateChain, - privateKey: privateKey, - clientCertificateVerification: .noVerification, - trustRoots: .systemDefault, - requireALPN: false - ) - } - - /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match - /// the requirements of mTLS: - /// - `clientCertificateVerificationMode` equals `noHostnameVerification` - /// - `trustRoots` equals `systemDefault` - /// - `requireALPN` equals `false` - /// - /// - Parameters: - /// - certificateChain: The certificates the server will offer during negotiation. - /// - privateKey: The private key associated with the leaf certificate. - /// - Returns: A new HTTP2 NIO Posix transport TLS config. - public static func mTLS( - certificateChain: [TLSConfig.CertificateSource], - privateKey: TLSConfig.PrivateKeySource - ) -> Self { - Self( - certificateChain: certificateChain, - privateKey: privateKey, - clientCertificateVerification: .noHostnameVerification, - trustRoots: .systemDefault, - requireALPN: false - ) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport.Posix.Config { - /// The security configuration for this connection. - public struct TransportSecurity: Sendable { - package enum Wrapped: Sendable { - case plaintext - case tls(TLS) - } - - package let wrapped: Wrapped - - /// This connection is plaintext: no encryption will take place. - public static let plaintext = Self(wrapped: .plaintext) - - #if canImport(NIOSSL) - /// This connection will use TLS. - public static func tls(_ tls: TLS) -> Self { - Self(wrapped: .tls(tls)) - } - #endif - } - - public struct TLS: Sendable { - /// The certificates the client will offer during negotiation. - public var certificateChain: [TLSConfig.CertificateSource] - - /// The private key associated with the leaf certificate. - public var privateKey: TLSConfig.PrivateKeySource? - - /// How to verify the server certificate, if one is presented. - public var serverCertificateVerification: TLSConfig.CertificateVerification - - /// The trust roots to be used when verifying server certificates. - public var trustRoots: TLSConfig.TrustRootsSource - - /// An optional server hostname to use when verifying certificates. - public var serverHostname: String? - - /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: - /// - `certificateChain` equals `[]` - /// - `privateKey` equals `nil` - /// - `serverCertificateVerification` equals `fullVerification` - /// - `trustRoots` equals `systemDefault` - /// - `serverHostname` equals `nil` - /// - /// - Returns: A new HTTP2 NIO Posix transport TLS config. - public static var defaults: Self { - Self( - certificateChain: [], - privateKey: nil, - serverCertificateVerification: .fullVerification, - trustRoots: .systemDefault, - serverHostname: nil - ) - } - - /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match - /// the requirements of mTLS: - /// - `trustRoots` equals `systemDefault` - /// - /// - Parameters: - /// - certificateChain: The certificates the client will offer during negotiation. - /// - privateKey: The private key associated with the leaf certificate. - /// - Returns: A new HTTP2 NIO Posix transport TLS config. - public static func mTLS( - certificateChain: [TLSConfig.CertificateSource], - privateKey: TLSConfig.PrivateKeySource - ) -> Self { - Self( - certificateChain: certificateChain, - privateKey: privateKey, - serverCertificateVerification: .fullVerification, - trustRoots: .systemDefault - ) - } - } -} diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift deleted file mode 100644 index 395308d46..000000000 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/Exports.swift +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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. - */ - -@_exported import GRPCCore -@_exported import GRPCHTTP2Core diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift deleted file mode 100644 index ee86e5eb5..000000000 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ClientTransport+TransportServices.swift +++ /dev/null @@ -1,339 +0,0 @@ -/* - * 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. - */ - -#if canImport(Network) -public import GRPCCore -public import GRPCHTTP2Core -public import NIOTransportServices // has to be public because of default argument value in init -public import NIOCore // has to be public because of EventLoopGroup param in init - -private import Network - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport { - /// A `ClientTransport` using HTTP/2 built on top of `NIOTransportServices`. - /// - /// This transport builds on top of SwiftNIO's Transport Services networking layer and is the recommended - /// variant for use on Darwin-based platforms (macOS, iOS, etc.). - /// If you are targeting Linux platforms then you should use the `NIOPosix` variant of - /// the `HTTP2ClientTransport`. - /// - /// To use this transport you need to provide a 'target' to connect to which will be resolved - /// by an appropriate resolver from the resolver registry. By default the resolver registry can - /// resolve DNS targets, IPv4 and IPv6 targets, and Unix domain socket targets. Virtual Socket - /// targets are not supported with this transport. If you use a custom target you must also provide an - /// appropriately configured registry. - /// - /// You can control various aspects of connection creation, management, security and RPC behavior via - /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via - /// the `ServiceConfig` (if it isn't provided by a resolver). - /// - /// Beyond creating the transport you don't need to interact with it directly, instead, pass it - /// to a `GRPCClient`: - /// - /// ```swift - /// try await withThrowingDiscardingTaskGroup { group in - /// let transport = try HTTP2ClientTransport.TransportServices( - /// target: .ipv4(host: "example.com"), - /// config: .defaults(transportSecurity: .plaintext) - /// ) - /// let client = GRPCClient(transport: transport) - /// group.addTask { - /// try await client.run() - /// } - /// - /// // ... - /// } - /// ``` - public struct TransportServices: ClientTransport { - private let channel: GRPCChannel - - public var retryThrottle: RetryThrottle? { - self.channel.retryThrottle - } - - /// Creates a new NIOTransportServices-based HTTP/2 client transport. - /// - /// - Parameters: - /// - target: A target to resolve. - /// - config: Configuration for the transport. - /// - resolverRegistry: A registry of resolver factories. - /// - serviceConfig: Service config controlling how the transport should establish and - /// load-balance connections. - /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must - /// be a `MultiThreadedEventLoopGroup` or an `EventLoop` from - /// a `MultiThreadedEventLoopGroup`. - /// - Throws: When no suitable resolver could be found for the `target`. - public init( - target: any ResolvableTarget, - config: Config, - resolverRegistry: NameResolverRegistry = .defaults, - serviceConfig: ServiceConfig = ServiceConfig(), - eventLoopGroup: any EventLoopGroup = .singletonNIOTSEventLoopGroup - ) throws { - guard let resolver = resolverRegistry.makeResolver(for: target) else { - throw RuntimeError( - code: .transportError, - message: """ - No suitable resolvers to resolve '\(target)'. You must make sure that the resolver \ - registry has a suitable name resolver factory registered for the given target. - """ - ) - } - - self.channel = GRPCChannel( - resolver: resolver, - connector: Connector(eventLoopGroup: eventLoopGroup, config: config), - config: GRPCChannel.Config(transportServices: config), - defaultServiceConfig: serviceConfig - ) - } - - public func connect() async throws { - await self.channel.connect() - } - - public func beginGracefulShutdown() { - self.channel.beginGracefulShutdown() - } - - public func withStream( - descriptor: MethodDescriptor, - options: CallOptions, - _ closure: (RPCStream) async throws -> T - ) async throws -> T { - try await self.channel.withStream(descriptor: descriptor, options: options, closure) - } - - public func config(forMethod descriptor: MethodDescriptor) -> MethodConfig? { - self.channel.config(forMethod: descriptor) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport.TransportServices { - struct Connector: HTTP2Connector { - private let config: HTTP2ClientTransport.TransportServices.Config - private let eventLoopGroup: any EventLoopGroup - - init( - eventLoopGroup: any EventLoopGroup, - config: HTTP2ClientTransport.TransportServices.Config - ) { - self.eventLoopGroup = eventLoopGroup - self.config = config - } - - func establishConnection( - to address: GRPCHTTP2Core.SocketAddress - ) async throws -> HTTP2Connection { - let bootstrap: NIOTSConnectionBootstrap - let isPlainText: Bool - switch self.config.transportSecurity.wrapped { - case .plaintext: - isPlainText = true - bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) - - case .tls(let tlsConfig): - isPlainText = false - bootstrap = NIOTSConnectionBootstrap(group: self.eventLoopGroup) - .tlsOptions(try NWProtocolTLS.Options(tlsConfig)) - } - - let (channel, multiplexer) = try await bootstrap.connect(to: address) { channel in - channel.eventLoop.makeCompletedFuture { - try channel.pipeline.syncOperations.configureGRPCClientPipeline( - channel: channel, - config: GRPCChannel.Config(transportServices: self.config) - ) - } - } - - return HTTP2Connection( - channel: channel, - multiplexer: multiplexer, - isPlaintext: isPlainText - ) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport.TransportServices { - /// Configuration for the `TransportServices` transport. - public struct Config: Sendable { - /// Configuration for HTTP/2 connections. - public var http2: HTTP2ClientTransport.Config.HTTP2 - - /// Configuration for backoff used when establishing a connection. - public var backoff: HTTP2ClientTransport.Config.Backoff - - /// Configuration for connection management. - public var connection: HTTP2ClientTransport.Config.Connection - - /// Compression configuration. - public var compression: HTTP2ClientTransport.Config.Compression - - /// The transport's security. - public var transportSecurity: TransportSecurity - - /// Creates a new connection configuration. - /// - /// - Parameters: - /// - http2: HTTP2 configuration. - /// - backoff: Backoff configuration. - /// - connection: Connection configuration. - /// - compression: Compression configuration. - /// - transportSecurity: The transport's security configuration. - /// - /// - SeeAlso: ``defaults(transportSecurity:configure:)`` - public init( - http2: HTTP2ClientTransport.Config.HTTP2, - backoff: HTTP2ClientTransport.Config.Backoff, - connection: HTTP2ClientTransport.Config.Connection, - compression: HTTP2ClientTransport.Config.Compression, - transportSecurity: TransportSecurity - ) { - self.http2 = http2 - self.connection = connection - self.backoff = backoff - self.compression = compression - self.transportSecurity = transportSecurity - } - - /// Default values. - /// - /// - Parameters: - /// - transportSecurity: The security settings applied to the transport. - /// - configure: A closure which allows you to modify the defaults before returning them. - public static func defaults( - transportSecurity: TransportSecurity, - configure: (_ config: inout Self) -> Void = { _ in } - ) -> Self { - var config = Self( - http2: .defaults, - backoff: .defaults, - connection: .defaults, - compression: .defaults, - transportSecurity: transportSecurity - ) - configure(&config) - return config - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel.Config { - init(transportServices config: HTTP2ClientTransport.TransportServices.Config) { - self.init( - http2: config.http2, - backoff: config.backoff, - connection: config.connection, - compression: config.compression - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension NIOTSConnectionBootstrap { - fileprivate func connect( - to address: GRPCHTTP2Core.SocketAddress, - childChannelInitializer: @escaping @Sendable (any Channel) -> EventLoopFuture - ) async throws -> Output { - if address.virtualSocket != nil { - throw RuntimeError( - code: .transportError, - message: """ - Virtual sockets are not supported by 'HTTP2ClientTransport.TransportServices'. \ - Please use the 'HTTP2ClientTransport.Posix' transport. - """ - ) - } else { - return try await self.connect( - to: NIOCore.SocketAddress(address), - channelInitializer: childChannelInitializer - ) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientTransport where Self == HTTP2ClientTransport.TransportServices { - /// Create a new `TransportServices` based HTTP/2 client transport. - /// - /// - Parameters: - /// - target: A target to resolve. - /// - config: Configuration for the transport. - /// - resolverRegistry: A registry of resolver factories. - /// - serviceConfig: Service config controlling how the transport should establish and - /// load-balance connections. - /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to run connections on. This must - /// be a `NIOTSEventLoopGroup` or an `EventLoop` from - /// a `NIOTSEventLoopGroup`. - /// - Throws: When no suitable resolver could be found for the `target`. - public static func http2NIOTS( - target: any ResolvableTarget, - config: HTTP2ClientTransport.TransportServices.Config, - resolverRegistry: NameResolverRegistry = .defaults, - serviceConfig: ServiceConfig = ServiceConfig(), - eventLoopGroup: any EventLoopGroup = .singletonNIOTSEventLoopGroup - ) throws -> Self { - try HTTP2ClientTransport.TransportServices( - target: target, - config: config, - resolverRegistry: resolverRegistry, - serviceConfig: serviceConfig, - eventLoopGroup: eventLoopGroup - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NWProtocolTLS.Options { - convenience init(_ tlsConfig: HTTP2ClientTransport.TransportServices.Config.TLS) throws { - self.init() - - guard let sec_identity = sec_identity_create(try tlsConfig.identityProvider()) else { - throw RuntimeError( - code: .transportError, - message: """ - There was an issue creating the SecIdentity required to set up TLS. \ - Please check your TLS configuration. - """ - ) - } - - sec_protocol_options_set_local_identity( - self.securityProtocolOptions, - sec_identity - ) - - sec_protocol_options_set_min_tls_protocol_version( - self.securityProtocolOptions, - .TLSv12 - ) - - for `protocol` in ["grpc-exp", "h2"] { - sec_protocol_options_add_tls_application_protocol( - self.securityProtocolOptions, - `protocol` - ) - } - } -} -#endif diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift deleted file mode 100644 index 31fd3a312..000000000 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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. - */ - -#if canImport(Network) -public import GRPCCore -public import NIOTransportServices // has to be public because of default argument value in init -public import GRPCHTTP2Core - -private import NIOCore -private import NIOExtras -private import NIOHTTP2 -private import Network - -private import Synchronization - -extension HTTP2ServerTransport { - /// A NIO Transport Services-backed implementation of a server transport. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct TransportServices: ServerTransport, ListeningServerTransport { - private struct ListenerFactory: HTTP2ListenerFactory { - let config: Config - - func makeListeningChannel( - eventLoopGroup: any EventLoopGroup, - address: GRPCHTTP2Core.SocketAddress, - serverQuiescingHelper: ServerQuiescingHelper - ) async throws -> NIOAsyncChannel { - let bootstrap: NIOTSListenerBootstrap - - let requireALPN: Bool - let scheme: Scheme - switch self.config.transportSecurity.wrapped { - case .plaintext: - requireALPN = false - scheme = .http - bootstrap = NIOTSListenerBootstrap(group: eventLoopGroup) - - case .tls(let tlsConfig): - requireALPN = tlsConfig.requireALPN - scheme = .https - bootstrap = NIOTSListenerBootstrap(group: eventLoopGroup) - .tlsOptions(try NWProtocolTLS.Options(tlsConfig)) - } - - let serverChannel = - try await bootstrap - .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) - .serverChannelInitializer { channel in - let quiescingHandler = serverQuiescingHelper.makeServerChannelHandler(channel: channel) - return channel.pipeline.addHandler(quiescingHandler) - } - .bind(to: address) { channel in - channel.eventLoop.makeCompletedFuture { - return try channel.pipeline.syncOperations.configureGRPCServerPipeline( - channel: channel, - compressionConfig: self.config.compression, - connectionConfig: self.config.connection, - http2Config: self.config.http2, - rpcConfig: self.config.rpc, - requireALPN: requireALPN, - scheme: scheme - ) - } - } - - return serverChannel - } - } - - private let underlyingTransport: CommonHTTP2ServerTransport - - /// The listening address for this server transport. - /// - /// It is an `async` property because it will only return once the address has been successfully bound. - /// - /// - Throws: A runtime error will be thrown if the address could not be bound or is not bound any - /// longer, because the transport isn't listening anymore. It can also throw if the transport returned an - /// invalid address. - public var listeningAddress: GRPCHTTP2Core.SocketAddress { - get async throws { - try await self.underlyingTransport.listeningAddress - } - } - - /// Create a new `TransportServices` transport. - /// - /// - Parameters: - /// - address: The address to which the server should be bound. - /// - config: The transport configuration. - /// - eventLoopGroup: The ELG from which to get ELs to run this transport. - public init( - address: GRPCHTTP2Core.SocketAddress, - config: Config, - eventLoopGroup: NIOTSEventLoopGroup = .singletonNIOTSEventLoopGroup - ) { - let factory = ListenerFactory(config: config) - let helper = ServerQuiescingHelper(group: eventLoopGroup) - self.underlyingTransport = CommonHTTP2ServerTransport( - address: address, - eventLoopGroup: eventLoopGroup, - quiescingHelper: helper, - listenerFactory: factory - ) - } - - public func listen( - streamHandler: @escaping @Sendable ( - _ stream: RPCStream, - _ context: ServerContext - ) async -> Void - ) async throws { - try await self.underlyingTransport.listen(streamHandler: streamHandler) - } - - public func beginGracefulShutdown() { - self.underlyingTransport.beginGracefulShutdown() - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ServerTransport.TransportServices { - /// Configuration for the `TransportServices` transport. - public struct Config: Sendable { - /// Compression configuration. - public var compression: HTTP2ServerTransport.Config.Compression - - /// Connection configuration. - public var connection: HTTP2ServerTransport.Config.Connection - - /// HTTP2 configuration. - public var http2: HTTP2ServerTransport.Config.HTTP2 - - /// RPC configuration. - public var rpc: HTTP2ServerTransport.Config.RPC - - /// The transport's security. - public var transportSecurity: TransportSecurity - - /// Construct a new `Config`. - /// - Parameters: - /// - compression: Compression configuration. - /// - connection: Connection configuration. - /// - http2: HTTP2 configuration. - /// - rpc: RPC configuration. - /// - transportSecurity: The transport's security configuration. - public init( - compression: HTTP2ServerTransport.Config.Compression, - connection: HTTP2ServerTransport.Config.Connection, - http2: HTTP2ServerTransport.Config.HTTP2, - rpc: HTTP2ServerTransport.Config.RPC, - transportSecurity: TransportSecurity - ) { - self.compression = compression - self.connection = connection - self.http2 = http2 - self.rpc = rpc - self.transportSecurity = transportSecurity - } - - /// Default values for the different configurations. - /// - /// - Parameters: - /// - transportSecurity: The transport's security configuration. - /// - configure: A closure which allows you to modify the defaults before returning them. - public static func defaults( - transportSecurity: TransportSecurity, - configure: (_ config: inout Self) -> Void = { _ in } - ) -> Self { - var config = Self( - compression: .defaults, - connection: .defaults, - http2: .defaults, - rpc: .defaults, - transportSecurity: transportSecurity - ) - configure(&config) - return config - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension NIOTSListenerBootstrap { - fileprivate func bind( - to address: GRPCHTTP2Core.SocketAddress, - childChannelInitializer: @escaping @Sendable (any Channel) -> EventLoopFuture - ) async throws -> NIOAsyncChannel { - if address.virtualSocket != nil { - throw RuntimeError( - code: .transportError, - message: """ - Virtual sockets are not supported by 'HTTP2ServerTransport.TransportServices'. \ - Please use the 'HTTP2ServerTransport.Posix' transport. - """ - ) - } else { - return try await self.bind( - to: NIOCore.SocketAddress(address), - childChannelInitializer: childChannelInitializer - ) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ServerTransport where Self == HTTP2ServerTransport.TransportServices { - /// Create a new `TransportServices` based HTTP/2 server transport. - /// - /// - Parameters: - /// - address: The address to which the server should be bound. - /// - config: The transport configuration. - /// - eventLoopGroup: The underlying NIO `EventLoopGroup` to the server on. This must - /// be a `NIOTSEventLoopGroup` or an `EventLoop` from a `NIOTSEventLoopGroup`. - public static func http2NIOTS( - address: GRPCHTTP2Core.SocketAddress, - config: HTTP2ServerTransport.TransportServices.Config, - eventLoopGroup: NIOTSEventLoopGroup = .singletonNIOTSEventLoopGroup - ) -> Self { - return HTTP2ServerTransport.TransportServices( - address: address, - config: config, - eventLoopGroup: eventLoopGroup - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NWProtocolTLS.Options { - convenience init(_ tlsConfig: HTTP2ServerTransport.TransportServices.Config.TLS) throws { - self.init() - - guard let sec_identity = sec_identity_create(try tlsConfig.identityProvider()) else { - throw RuntimeError( - code: .transportError, - message: """ - There was an issue creating the SecIdentity required to set up TLS. \ - Please check your TLS configuration. - """ - ) - } - - sec_protocol_options_set_local_identity( - self.securityProtocolOptions, - sec_identity - ) - - sec_protocol_options_set_min_tls_protocol_version( - self.securityProtocolOptions, - .TLSv12 - ) - - for `protocol` in ["grpc-exp", "h2"] { - sec_protocol_options_add_tls_application_protocol( - self.securityProtocolOptions, - `protocol` - ) - } - } -} -#endif diff --git a/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift b/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift deleted file mode 100644 index 94bc7dcb2..000000000 --- a/Sources/GRPCHTTP2TransportNIOTransportServices/TLSConfig.swift +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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. - */ - -#if canImport(Network) -public import Network - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ServerTransport.TransportServices.Config { - /// The security configuration for this connection. - public struct TransportSecurity: Sendable { - package enum Wrapped: Sendable { - case plaintext - case tls(TLS) - } - - package let wrapped: Wrapped - - /// This connection is plaintext: no encryption will take place. - public static let plaintext = Self(wrapped: .plaintext) - - /// This connection will use TLS. - public static func tls(_ tls: TLS) -> Self { - Self(wrapped: .tls(tls)) - } - } - - public struct TLS: Sendable { - /// A provider for the `SecIdentity` to be used when setting up TLS. - public var identityProvider: @Sendable () throws -> SecIdentity - - /// Whether ALPN is required. - /// - /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. - public var requireALPN: Bool - - /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: - /// - `requireALPN` equals `false` - /// - /// - Returns: A new HTTP2 NIO Transport Services transport TLS config. - public static func defaults( - identityProvider: @Sendable @escaping () throws -> SecIdentity - ) -> Self { - Self( - identityProvider: identityProvider, - requireALPN: false - ) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HTTP2ClientTransport.TransportServices.Config { - /// The security configuration for this connection. - public struct TransportSecurity: Sendable { - package enum Wrapped: Sendable { - case plaintext - case tls(TLS) - } - - package let wrapped: Wrapped - - /// This connection is plaintext: no encryption will take place. - public static let plaintext = Self(wrapped: .plaintext) - - /// This connection will use TLS. - public static func tls(_ tls: TLS) -> Self { - Self(wrapped: .tls(tls)) - } - } - - public struct TLS: Sendable { - /// A provider for the `SecIdentity` to be used when setting up TLS. - public var identityProvider: @Sendable () throws -> SecIdentity - - /// Create a new HTTP2 NIO Transport Services transport TLS config. - public init(identityProvider: @Sendable @escaping () throws -> SecIdentity) { - self.identityProvider = identityProvider - } - } -} -#endif diff --git a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift b/Sources/GRPCInterceptors/ClientTracingInterceptor.swift deleted file mode 100644 index 9da8a1f26..000000000 --- a/Sources/GRPCInterceptors/ClientTracingInterceptor.swift +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore -internal import Tracing - -/// A client interceptor that injects tracing information into the request. -/// -/// The tracing information is taken from the current `ServiceContext`, and injected into the request's -/// metadata. It will then be picked up by the server-side ``ServerTracingInterceptor``. -/// -/// For more information, refer to the documentation for `swift-distributed-tracing`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct ClientTracingInterceptor: ClientInterceptor { - private let injector: ClientRequestInjector - private let emitEventOnEachWrite: Bool - - /// Create a new instance of a ``ClientTracingInterceptor``. - /// - /// - Parameter emitEventOnEachWrite: If `true`, each request part sent and response part - /// received will be recorded as a separate event in a tracing span. Otherwise, only the request/response - /// start and end will be recorded as events. - public init(emitEventOnEachWrite: Bool = false) { - self.injector = ClientRequestInjector() - self.emitEventOnEachWrite = emitEventOnEachWrite - } - - /// This interceptor will inject as the request's metadata whatever `ServiceContext` key-value pairs - /// have been made available by the tracing implementation bootstrapped in your application. - /// - /// Which key-value pairs are injected will depend on the specific tracing implementation - /// that has been configured when bootstrapping `swift-distributed-tracing` in your application. - public func intercept( - request: ClientRequest.Stream, - context: ClientContext, - next: ( - ClientRequest.Stream, - ClientContext - ) async throws -> ClientResponse.Stream - ) async throws -> ClientResponse.Stream where Input: Sendable, Output: Sendable { - var request = request - let tracer = InstrumentationSystem.tracer - let serviceContext = ServiceContext.current ?? .topLevel - - tracer.inject( - serviceContext, - into: &request.metadata, - using: self.injector - ) - - return try await tracer.withSpan( - context.descriptor.fullyQualifiedMethod, - context: serviceContext, - ofKind: .client - ) { span in - span.addEvent("Request started") - - if self.emitEventOnEachWrite { - let wrappedProducer = request.producer - request.producer = { writer in - let eventEmittingWriter = HookedWriter( - wrapping: writer, - beforeEachWrite: { - span.addEvent("Sending request part") - }, - afterEachWrite: { - span.addEvent("Sent request part") - } - ) - - do { - try await wrappedProducer(RPCWriter(wrapping: eventEmittingWriter)) - } catch { - span.addEvent("Error encountered") - throw error - } - - span.addEvent("Request end") - } - } - - var response: ClientResponse.Stream - do { - response = try await next(request, context) - } catch { - span.addEvent("Error encountered") - throw error - } - - switch response.accepted { - case .success(var success): - if self.emitEventOnEachWrite { - let onEachPartRecordingSequence = success.bodyParts.map { element in - span.addEvent("Received response part") - return element - } - let onFinishRecordingSequence = OnFinishAsyncSequence( - wrapping: onEachPartRecordingSequence - ) { - span.addEvent("Received response end") - } - success.bodyParts = RPCAsyncSequence(wrapping: onFinishRecordingSequence) - response.accepted = .success(success) - } else { - let onFinishRecordingSequence = OnFinishAsyncSequence(wrapping: success.bodyParts) { - span.addEvent("Received response end") - } - success.bodyParts = RPCAsyncSequence(wrapping: onFinishRecordingSequence) - response.accepted = .success(success) - } - case .failure: - span.addEvent("Received error response") - } - - return response - } - } -} - -/// An injector responsible for injecting the required instrumentation keys from the `ServiceContext` into -/// the request metadata. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct ClientRequestInjector: Instrumentation.Injector { - typealias Carrier = Metadata - - func inject(_ value: String, forKey key: String, into carrier: inout Carrier) { - carrier.addString(value, forKey: key) - } -} diff --git a/Sources/GRPCInterceptors/HookedWriter.swift b/Sources/GRPCInterceptors/HookedWriter.swift deleted file mode 100644 index 9d85df044..000000000 --- a/Sources/GRPCInterceptors/HookedWriter.swift +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - */ -internal import GRPCCore -internal import Tracing - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct HookedWriter: RPCWriterProtocol { - private let writer: any RPCWriterProtocol - private let beforeEachWrite: @Sendable () -> Void - private let afterEachWrite: @Sendable () -> Void - - init( - wrapping other: some RPCWriterProtocol, - beforeEachWrite: @Sendable @escaping () -> Void, - afterEachWrite: @Sendable @escaping () -> Void - ) { - self.writer = other - self.beforeEachWrite = beforeEachWrite - self.afterEachWrite = afterEachWrite - } - - func write(_ element: Element) async throws { - self.beforeEachWrite() - try await self.writer.write(element) - self.afterEachWrite() - } - - func write(contentsOf elements: some Sequence) async throws { - self.beforeEachWrite() - try await self.writer.write(contentsOf: elements) - self.afterEachWrite() - } -} diff --git a/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift b/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift deleted file mode 100644 index d07a8efec..000000000 --- a/Sources/GRPCInterceptors/OnFinishAsyncSequence.swift +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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. - */ - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct OnFinishAsyncSequence: AsyncSequence, Sendable { - private let _makeAsyncIterator: @Sendable () -> AsyncIterator - - init( - wrapping other: S, - onFinish: @escaping @Sendable () -> Void - ) where S.Element == Element, S: Sendable { - self._makeAsyncIterator = { - AsyncIterator(wrapping: other.makeAsyncIterator(), onFinish: onFinish) - } - } - - func makeAsyncIterator() -> AsyncIterator { - self._makeAsyncIterator() - } - - struct AsyncIterator: AsyncIteratorProtocol { - private var iterator: any AsyncIteratorProtocol - private var onFinish: (@Sendable () -> Void)? - - fileprivate init( - wrapping other: Iterator, - onFinish: @escaping @Sendable () -> Void - ) where Iterator: AsyncIteratorProtocol, Iterator.Element == Element { - self.iterator = other - self.onFinish = onFinish - } - - mutating func next() async throws -> Element? { - let elem = try await self.iterator.next() - - if elem == nil { - self.onFinish?() - self.onFinish = nil - } - - return elem as? Element - } - } -} diff --git a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift b/Sources/GRPCInterceptors/ServerTracingInterceptor.swift deleted file mode 100644 index a2ebe456c..000000000 --- a/Sources/GRPCInterceptors/ServerTracingInterceptor.swift +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore -internal import Tracing - -/// A server interceptor that extracts tracing information from the request. -/// -/// The extracted tracing information is made available to user code via the current `ServiceContext`. -/// For more information, refer to the documentation for `swift-distributed-tracing`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct ServerTracingInterceptor: ServerInterceptor { - private let extractor: ServerRequestExtractor - private let emitEventOnEachWrite: Bool - - /// Create a new instance of a ``ServerTracingInterceptor``. - /// - /// - Parameter emitEventOnEachWrite: If `true`, each response part sent and request part - /// received will be recorded as a separate event in a tracing span. Otherwise, only the request/response - /// start and end will be recorded as events. - public init(emitEventOnEachWrite: Bool = false) { - self.extractor = ServerRequestExtractor() - self.emitEventOnEachWrite = emitEventOnEachWrite - } - - /// This interceptor will extract whatever `ServiceContext` key-value pairs have been inserted into the - /// request's metadata, and will make them available to user code via the `ServiceContext/current` - /// context. - /// - /// Which key-value pairs are extracted and made available will depend on the specific tracing implementation - /// that has been configured when bootstrapping `swift-distributed-tracing` in your application. - public func intercept( - request: ServerRequest.Stream, - context: ServerContext, - next: @Sendable (ServerRequest.Stream, ServerContext) async throws -> - ServerResponse.Stream - ) async throws -> ServerResponse.Stream where Input: Sendable, Output: Sendable { - var serviceContext = ServiceContext.topLevel - let tracer = InstrumentationSystem.tracer - - tracer.extract( - request.metadata, - into: &serviceContext, - using: self.extractor - ) - - return try await ServiceContext.withValue(serviceContext) { - try await tracer.withSpan( - context.descriptor.fullyQualifiedMethod, - context: serviceContext, - ofKind: .server - ) { span in - span.addEvent("Received request start") - - var request = request - - if self.emitEventOnEachWrite { - request.messages = RPCAsyncSequence( - wrapping: request.messages.map { element in - span.addEvent("Received request part") - return element - } - ) - } - - var response = try await next(request, context) - - span.addEvent("Received request end") - - switch response.accepted { - case .success(var success): - let wrappedProducer = success.producer - - if self.emitEventOnEachWrite { - success.producer = { writer in - let eventEmittingWriter = HookedWriter( - wrapping: writer, - beforeEachWrite: { - span.addEvent("Sending response part") - }, - afterEachWrite: { - span.addEvent("Sent response part") - } - ) - - let wrappedResult: Metadata - do { - wrappedResult = try await wrappedProducer( - RPCWriter(wrapping: eventEmittingWriter) - ) - } catch { - span.addEvent("Error encountered") - throw error - } - - span.addEvent("Sent response end") - return wrappedResult - } - } else { - success.producer = { writer in - let wrappedResult: Metadata - do { - wrappedResult = try await wrappedProducer(writer) - } catch { - span.addEvent("Error encountered") - throw error - } - - span.addEvent("Sent response end") - return wrappedResult - } - } - - response = .init(accepted: .success(success)) - case .failure: - span.addEvent("Sent error response") - } - - return response - } - } - } -} - -/// An extractor responsible for extracting the required instrumentation keys from request metadata. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct ServerRequestExtractor: Instrumentation.Extractor { - typealias Carrier = Metadata - - func extract(key: String, from carrier: Carrier) -> String? { - var values = carrier[stringValues: key].makeIterator() - // There should only be one value for each key. If more, pick just one. - return values.next() - } -} diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/empty.pb.swift b/Sources/GRPCInteroperabilityTestModels/Generated/empty.pb.swift deleted file mode 100644 index bee17ecca..000000000 --- a/Sources/GRPCInteroperabilityTestModels/Generated/empty.pb.swift +++ /dev/null @@ -1,79 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/empty.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// An empty message that you can re-use to avoid defining duplicated empty -/// messages in your project. A typical example is to use it as argument or the -/// return value of a service API. For instance: -/// -/// service Foo { -/// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; -/// }; -public struct Grpc_Testing_Empty { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_Empty: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_Empty: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Empty" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - public mutating func decodeMessage(decoder: inout D) throws { - while let _ = try decoder.nextFieldNumber() { - } - } - - public func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Empty, rhs: Grpc_Testing_Empty) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/messages.pb.swift b/Sources/GRPCInteroperabilityTestModels/Generated/messages.pb.swift deleted file mode 100644 index aff6104d6..000000000 --- a/Sources/GRPCInteroperabilityTestModels/Generated/messages.pb.swift +++ /dev/null @@ -1,950 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/messages.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The type of payload that should be returned. -public enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum { - public typealias RawValue = Int - - /// Compressable text format. - case compressable // = 0 - case UNRECOGNIZED(Int) - - public init() { - self = .compressable - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .compressable - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .compressable: return 0 - case .UNRECOGNIZED(let i): return i - } - } - -} - -#if swift(>=4.2) - -extension Grpc_Testing_PayloadType: CaseIterable { - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static var allCases: [Grpc_Testing_PayloadType] = [ - .compressable, - ] -} - -#endif // swift(>=4.2) - -/// TODO(dgq): Go back to using well-known types once -/// https://github.com/grpc/grpc/issues/6980 has been fixed. -/// import "google/protobuf/wrappers.proto"; -public struct Grpc_Testing_BoolValue { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The bool value. - public var value: Bool = false - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A block of data, to simply increase gRPC message size. -public struct Grpc_Testing_Payload { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The type of data in body. - public var type: Grpc_Testing_PayloadType = .compressable - - /// Primary contents of payload. - public var body: Data = Data() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A protobuf representation for grpc status. This is used by test -/// clients to specify a status that the server should attempt to return. -public struct Grpc_Testing_EchoStatus { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var code: Int32 = 0 - - public var message: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Unary request. -public struct Grpc_Testing_SimpleRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, server randomly chooses one from other formats. - public var responseType: Grpc_Testing_PayloadType = .compressable - - /// Desired payload size in the response from the server. - public var responseSize: Int32 = 0 - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether SimpleResponse should include username. - public var fillUsername: Bool = false - - /// Whether SimpleResponse should include OAuth scope. - public var fillOauthScope: Bool = false - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - public var responseCompressed: Grpc_Testing_BoolValue { - get {return _responseCompressed ?? Grpc_Testing_BoolValue()} - set {_responseCompressed = newValue} - } - /// Returns true if `responseCompressed` has been explicitly set. - public var hasResponseCompressed: Bool {return self._responseCompressed != nil} - /// Clears the value of `responseCompressed`. Subsequent reads from it will return its default value. - public mutating func clearResponseCompressed() {self._responseCompressed = nil} - - /// Whether server should return a given status - public var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - public var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - public mutating func clearResponseStatus() {self._responseStatus = nil} - - /// Whether the server should expect this request to be compressed. - public var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - public var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - public mutating func clearExpectCompressed() {self._expectCompressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseCompressed: Grpc_Testing_BoolValue? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil -} - -/// Unary response, as configured by the request. -public struct Grpc_Testing_SimpleResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase message size. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// The user the request came from, for verifying authentication was - /// successful when the client expected it. - public var username: String = String() - - /// OAuth scope. - public var oauthScope: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// Client-streaming request. -public struct Grpc_Testing_StreamingInputCallRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether the server should expect this request to be compressed. This field - /// is "nullable" in order to interoperate seamlessly with servers not able to - /// implement the full compression tests by introspecting the call to verify - /// the request's compression status. - public var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - public var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - public mutating func clearExpectCompressed() {self._expectCompressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil -} - -/// Client-streaming response. -public struct Grpc_Testing_StreamingInputCallResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Aggregated size of payloads received from the client. - public var aggregatedPayloadSize: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Configuration for a particular response. -public struct Grpc_Testing_ResponseParameters { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload sizes in responses from the server. - public var size: Int32 = 0 - - /// Desired interval between consecutive responses in the response stream in - /// microseconds. - public var intervalUs: Int32 = 0 - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - public var compressed: Grpc_Testing_BoolValue { - get {return _compressed ?? Grpc_Testing_BoolValue()} - set {_compressed = newValue} - } - /// Returns true if `compressed` has been explicitly set. - public var hasCompressed: Bool {return self._compressed != nil} - /// Clears the value of `compressed`. Subsequent reads from it will return its default value. - public mutating func clearCompressed() {self._compressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _compressed: Grpc_Testing_BoolValue? = nil -} - -/// Server-streaming request. -public struct Grpc_Testing_StreamingOutputCallRequest { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, the payload from each response in the stream - /// might be of different types. This is to simulate a mixed type of payload - /// stream. - public var responseType: Grpc_Testing_PayloadType = .compressable - - /// Configuration for each expected response message. - public var responseParameters: [Grpc_Testing_ResponseParameters] = [] - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether server should return a given status - public var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - public var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - public mutating func clearResponseStatus() {self._responseStatus = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil -} - -/// Server-streaming response, as configured by the request and parameters. -public struct Grpc_Testing_StreamingOutputCallResponse { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase response size. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// For reconnect interop test only. -/// Client tells server what reconnection parameters it used. -public struct Grpc_Testing_ReconnectParams { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var maxReconnectBackoffMs: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// For reconnect interop test only. -/// Server tells client whether its reconnects are following the spec and the -/// reconnect backoffs it saw. -public struct Grpc_Testing_ReconnectInfo { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var passed: Bool = false - - public var backoffMs: [Int32] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -#if swift(>=5.5) && canImport(_Concurrency) -extension Grpc_Testing_PayloadType: @unchecked Sendable {} -extension Grpc_Testing_BoolValue: @unchecked Sendable {} -extension Grpc_Testing_Payload: @unchecked Sendable {} -extension Grpc_Testing_EchoStatus: @unchecked Sendable {} -extension Grpc_Testing_SimpleRequest: @unchecked Sendable {} -extension Grpc_Testing_SimpleResponse: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingInputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ResponseParameters: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallRequest: @unchecked Sendable {} -extension Grpc_Testing_StreamingOutputCallResponse: @unchecked Sendable {} -extension Grpc_Testing_ReconnectParams: @unchecked Sendable {} -extension Grpc_Testing_ReconnectInfo: @unchecked Sendable {} -#endif // swift(>=5.5) && canImport(_Concurrency) - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_PayloadType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "COMPRESSABLE"), - ] -} - -extension Grpc_Testing_BoolValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".BoolValue" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "value"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.value) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.value != false { - try visitor.visitSingularBoolField(value: self.value, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_BoolValue, rhs: Grpc_Testing_BoolValue) -> Bool { - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Payload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Payload" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "body"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 2: try { try decoder.decodeSingularBytesField(value: &self.body) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.type != .compressable { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) - } - if !self.body.isEmpty { - try visitor.visitSingularBytesField(value: self.body, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Payload, rhs: Grpc_Testing_Payload) -> Bool { - if lhs.type != rhs.type {return false} - if lhs.body != rhs.body {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_EchoStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".EchoStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "code"), - 2: .same(proto: "message"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.code != 0 { - try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) - } - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_EchoStatus, rhs: Grpc_Testing_EchoStatus) -> Bool { - if lhs.code != rhs.code {return false} - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SimpleRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_size"), - 3: .same(proto: "payload"), - 4: .standard(proto: "fill_username"), - 5: .standard(proto: "fill_oauth_scope"), - 6: .standard(proto: "response_compressed"), - 7: .standard(proto: "response_status"), - 8: .standard(proto: "expect_compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.responseSize) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.fillUsername) }() - case 5: try { try decoder.decodeSingularBoolField(value: &self.fillOauthScope) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._responseCompressed) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - case 8: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if self.responseSize != 0 { - try visitor.visitSingularInt32Field(value: self.responseSize, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if self.fillUsername != false { - try visitor.visitSingularBoolField(value: self.fillUsername, fieldNumber: 4) - } - if self.fillOauthScope != false { - try visitor.visitSingularBoolField(value: self.fillOauthScope, fieldNumber: 5) - } - try { if let v = self._responseCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SimpleRequest, rhs: Grpc_Testing_SimpleRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseSize != rhs.responseSize {return false} - if lhs._payload != rhs._payload {return false} - if lhs.fillUsername != rhs.fillUsername {return false} - if lhs.fillOauthScope != rhs.fillOauthScope {return false} - if lhs._responseCompressed != rhs._responseCompressed {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SimpleResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .same(proto: "username"), - 3: .standard(proto: "oauth_scope"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.username) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.oauthScope) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.username.isEmpty { - try visitor.visitSingularStringField(value: self.username, fieldNumber: 2) - } - if !self.oauthScope.isEmpty { - try visitor.visitSingularStringField(value: self.oauthScope, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SimpleResponse, rhs: Grpc_Testing_SimpleResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.username != rhs.username {return false} - if lhs.oauthScope != rhs.oauthScope {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .standard(proto: "expect_compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingInputCallRequest, rhs: Grpc_Testing_StreamingInputCallRequest) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "aggregated_payload_size"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.aggregatedPayloadSize) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.aggregatedPayloadSize != 0 { - try visitor.visitSingularInt32Field(value: self.aggregatedPayloadSize, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingInputCallResponse, rhs: Grpc_Testing_StreamingInputCallResponse) -> Bool { - if lhs.aggregatedPayloadSize != rhs.aggregatedPayloadSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ResponseParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ResponseParameters" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "size"), - 2: .standard(proto: "interval_us"), - 3: .same(proto: "compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.intervalUs) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._compressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.size != 0 { - try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) - } - if self.intervalUs != 0 { - try visitor.visitSingularInt32Field(value: self.intervalUs, fieldNumber: 2) - } - try { if let v = self._compressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ResponseParameters, rhs: Grpc_Testing_ResponseParameters) -> Bool { - if lhs.size != rhs.size {return false} - if lhs.intervalUs != rhs.intervalUs {return false} - if lhs._compressed != rhs._compressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_parameters"), - 3: .same(proto: "payload"), - 7: .standard(proto: "response_status"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.responseParameters) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if !self.responseParameters.isEmpty { - try visitor.visitRepeatedMessageField(value: self.responseParameters, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingOutputCallRequest, rhs: Grpc_Testing_StreamingOutputCallRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseParameters != rhs.responseParameters {return false} - if lhs._payload != rhs._payload {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingOutputCallResponse, rhs: Grpc_Testing_StreamingOutputCallResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ReconnectParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "max_reconnect_backoff_ms"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.maxReconnectBackoffMs) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.maxReconnectBackoffMs != 0 { - try visitor.visitSingularInt32Field(value: self.maxReconnectBackoffMs, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ReconnectParams, rhs: Grpc_Testing_ReconnectParams) -> Bool { - if lhs.maxReconnectBackoffMs != rhs.maxReconnectBackoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ReconnectInfo" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "passed"), - 2: .standard(proto: "backoff_ms"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.passed) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.backoffMs) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.passed != false { - try visitor.visitSingularBoolField(value: self.passed, fieldNumber: 1) - } - if !self.backoffMs.isEmpty { - try visitor.visitPackedInt32Field(value: self.backoffMs, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ReconnectInfo, rhs: Grpc_Testing_ReconnectInfo) -> Bool { - if lhs.passed != rhs.passed {return false} - if lhs.backoffMs != rhs.backoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift deleted file mode 100644 index 6f0522adc..000000000 --- a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift +++ /dev/null @@ -1,1742 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: src/proto/grpc/testing/test.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -/// -/// Usage: instantiate `Grpc_Testing_TestServiceClient`, then call methods of this protocol to make API calls. -public protocol Grpc_Testing_TestServiceClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? { get } - - func emptyCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> UnaryCall - - func unaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func cacheableUnaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func streamingOutputCall( - _ request: Grpc_Testing_StreamingOutputCallRequest, - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_StreamingOutputCallResponse) -> Void - ) -> ServerStreamingCall - - func streamingInputCall( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func fullDuplexCall( - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_StreamingOutputCallResponse) -> Void - ) -> BidirectionalStreamingCall - - func halfDuplexCall( - callOptions: CallOptions?, - handler: @escaping (Grpc_Testing_StreamingOutputCallResponse) -> Void - ) -> BidirectionalStreamingCall - - func unimplementedCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Grpc_Testing_TestServiceClientProtocol { - public var serviceName: String { - return "grpc.testing.TestService" - } - - /// One empty request followed by one empty response. - /// - /// - Parameters: - /// - request: Request to send to EmptyCall. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func emptyCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.emptyCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeEmptyCallInterceptors() ?? [] - ) - } - - /// One request followed by one response. - /// - /// - Parameters: - /// - request: Request to send to UnaryCall. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func unaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.unaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [] - ) - } - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - /// - /// - Parameters: - /// - request: Request to send to CacheableUnaryCall. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func cacheableUnaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.cacheableUnaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCacheableUnaryCallInterceptors() ?? [] - ) - } - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - /// - /// - Parameters: - /// - request: Request to send to StreamingOutputCall. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - public func streamingOutputCall( - _ request: Grpc_Testing_StreamingOutputCallRequest, - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_StreamingOutputCallResponse) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.streamingOutputCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingOutputCallInterceptors() ?? [], - handler: handler - ) - } - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. - public func streamingInputCall( - callOptions: CallOptions? = nil - ) -> ClientStreamingCall { - return self.makeClientStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.streamingInputCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingInputCallInterceptors() ?? [] - ) - } - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func fullDuplexCall( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_StreamingOutputCallResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.fullDuplexCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeFullDuplexCallInterceptors() ?? [], - handler: handler - ) - } - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - public func halfDuplexCall( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Testing_StreamingOutputCallResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.halfDuplexCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeHalfDuplexCallInterceptors() ?? [], - handler: handler - ) - } - - /// The test server will not implement this method. It will be used - /// to test the behavior when clients call unimplemented methods. - /// - /// - Parameters: - /// - request: Request to send to UnimplementedCall. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func unimplementedCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.unimplementedCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [] - ) - } -} - -@available(*, deprecated) -extension Grpc_Testing_TestServiceClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Grpc_Testing_TestServiceNIOClient") -public final class Grpc_Testing_TestServiceClient: Grpc_Testing_TestServiceClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the grpc.testing.TestService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Grpc_Testing_TestServiceNIOClient: Grpc_Testing_TestServiceClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? - - /// Creates a client for the grpc.testing.TestService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_TestServiceAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? { get } - - func makeEmptyCallCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeUnaryCallCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeCacheableUnaryCallCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeStreamingOutputCallCall( - _ request: Grpc_Testing_StreamingOutputCallRequest, - callOptions: CallOptions? - ) -> GRPCAsyncServerStreamingCall - - func makeStreamingInputCallCall( - callOptions: CallOptions? - ) -> GRPCAsyncClientStreamingCall - - func makeFullDuplexCallCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall - - func makeHalfDuplexCallCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall - - func makeUnimplementedCallCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_TestServiceAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_TestServiceClientMetadata.serviceDescriptor - } - - public var interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? { - return nil - } - - public func makeEmptyCallCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.emptyCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeEmptyCallInterceptors() ?? [] - ) - } - - public func makeUnaryCallCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.unaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [] - ) - } - - public func makeCacheableUnaryCallCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.cacheableUnaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCacheableUnaryCallInterceptors() ?? [] - ) - } - - public func makeStreamingOutputCallCall( - _ request: Grpc_Testing_StreamingOutputCallRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncServerStreamingCall { - return self.makeAsyncServerStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.streamingOutputCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingOutputCallInterceptors() ?? [] - ) - } - - public func makeStreamingInputCallCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncClientStreamingCall { - return self.makeAsyncClientStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.streamingInputCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingInputCallInterceptors() ?? [] - ) - } - - public func makeFullDuplexCallCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.fullDuplexCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeFullDuplexCallInterceptors() ?? [] - ) - } - - public func makeHalfDuplexCallCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.halfDuplexCall.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeHalfDuplexCallInterceptors() ?? [] - ) - } - - public func makeUnimplementedCallCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.unimplementedCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_TestServiceAsyncClientProtocol { - public func emptyCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_Empty { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.emptyCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeEmptyCallInterceptors() ?? [] - ) - } - - public func unaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_SimpleResponse { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.unaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [] - ) - } - - public func cacheableUnaryCall( - _ request: Grpc_Testing_SimpleRequest, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_SimpleResponse { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.cacheableUnaryCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCacheableUnaryCallInterceptors() ?? [] - ) - } - - public func streamingOutputCall( - _ request: Grpc_Testing_StreamingOutputCallRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream { - return self.performAsyncServerStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.streamingOutputCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingOutputCallInterceptors() ?? [] - ) - } - - public func streamingInputCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_StreamingInputCallResponse where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_StreamingInputCallRequest { - return try await self.performAsyncClientStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.streamingInputCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingInputCallInterceptors() ?? [] - ) - } - - public func streamingInputCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_StreamingInputCallResponse where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_StreamingInputCallRequest { - return try await self.performAsyncClientStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.streamingInputCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStreamingInputCallInterceptors() ?? [] - ) - } - - public func fullDuplexCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_StreamingOutputCallRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.fullDuplexCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeFullDuplexCallInterceptors() ?? [] - ) - } - - public func fullDuplexCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_StreamingOutputCallRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.fullDuplexCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeFullDuplexCallInterceptors() ?? [] - ) - } - - public func halfDuplexCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Testing_StreamingOutputCallRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.halfDuplexCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeHalfDuplexCallInterceptors() ?? [] - ) - } - - public func halfDuplexCall( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Testing_StreamingOutputCallRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.halfDuplexCall.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeHalfDuplexCallInterceptors() ?? [] - ) - } - - public func unimplementedCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_Empty { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_TestServiceClientMetadata.Methods.unimplementedCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Grpc_Testing_TestServiceAsyncClient: Grpc_Testing_TestServiceAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_TestServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -public protocol Grpc_Testing_TestServiceClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'emptyCall'. - func makeEmptyCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'unaryCall'. - func makeUnaryCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'cacheableUnaryCall'. - func makeCacheableUnaryCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'streamingOutputCall'. - func makeStreamingOutputCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'streamingInputCall'. - func makeStreamingInputCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'fullDuplexCall'. - func makeFullDuplexCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'halfDuplexCall'. - func makeHalfDuplexCallInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'unimplementedCall'. - func makeUnimplementedCallInterceptors() -> [ClientInterceptor] -} - -public enum Grpc_Testing_TestServiceClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "TestService", - fullName: "grpc.testing.TestService", - methods: [ - Grpc_Testing_TestServiceClientMetadata.Methods.emptyCall, - Grpc_Testing_TestServiceClientMetadata.Methods.unaryCall, - Grpc_Testing_TestServiceClientMetadata.Methods.cacheableUnaryCall, - Grpc_Testing_TestServiceClientMetadata.Methods.streamingOutputCall, - Grpc_Testing_TestServiceClientMetadata.Methods.streamingInputCall, - Grpc_Testing_TestServiceClientMetadata.Methods.fullDuplexCall, - Grpc_Testing_TestServiceClientMetadata.Methods.halfDuplexCall, - Grpc_Testing_TestServiceClientMetadata.Methods.unimplementedCall, - ] - ) - - public enum Methods { - public static let emptyCall = GRPCMethodDescriptor( - name: "EmptyCall", - path: "/grpc.testing.TestService/EmptyCall", - type: GRPCCallType.unary - ) - - public static let unaryCall = GRPCMethodDescriptor( - name: "UnaryCall", - path: "/grpc.testing.TestService/UnaryCall", - type: GRPCCallType.unary - ) - - public static let cacheableUnaryCall = GRPCMethodDescriptor( - name: "CacheableUnaryCall", - path: "/grpc.testing.TestService/CacheableUnaryCall", - type: GRPCCallType.unary - ) - - public static let streamingOutputCall = GRPCMethodDescriptor( - name: "StreamingOutputCall", - path: "/grpc.testing.TestService/StreamingOutputCall", - type: GRPCCallType.serverStreaming - ) - - public static let streamingInputCall = GRPCMethodDescriptor( - name: "StreamingInputCall", - path: "/grpc.testing.TestService/StreamingInputCall", - type: GRPCCallType.clientStreaming - ) - - public static let fullDuplexCall = GRPCMethodDescriptor( - name: "FullDuplexCall", - path: "/grpc.testing.TestService/FullDuplexCall", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let halfDuplexCall = GRPCMethodDescriptor( - name: "HalfDuplexCall", - path: "/grpc.testing.TestService/HalfDuplexCall", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let unimplementedCall = GRPCMethodDescriptor( - name: "UnimplementedCall", - path: "/grpc.testing.TestService/UnimplementedCall", - type: GRPCCallType.unary - ) - } -} - -/// A simple service NOT implemented at servers so clients can test for -/// that case. -/// -/// Usage: instantiate `Grpc_Testing_UnimplementedServiceClient`, then call methods of this protocol to make API calls. -public protocol Grpc_Testing_UnimplementedServiceClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? { get } - - func unimplementedCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Grpc_Testing_UnimplementedServiceClientProtocol { - public var serviceName: String { - return "grpc.testing.UnimplementedService" - } - - /// A call that no server should implement - /// - /// - Parameters: - /// - request: Request to send to UnimplementedCall. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func unimplementedCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_UnimplementedServiceClientMetadata.Methods.unimplementedCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [] - ) - } -} - -@available(*, deprecated) -extension Grpc_Testing_UnimplementedServiceClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Grpc_Testing_UnimplementedServiceNIOClient") -public final class Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_UnimplementedServiceClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the grpc.testing.UnimplementedService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Grpc_Testing_UnimplementedServiceNIOClient: Grpc_Testing_UnimplementedServiceClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? - - /// Creates a client for the grpc.testing.UnimplementedService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// A simple service NOT implemented at servers so clients can test for -/// that case. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_UnimplementedServiceAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? { get } - - func makeUnimplementedCallCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_UnimplementedServiceAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_UnimplementedServiceClientMetadata.serviceDescriptor - } - - public var interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? { - return nil - } - - public func makeUnimplementedCallCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_UnimplementedServiceClientMetadata.Methods.unimplementedCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_UnimplementedServiceAsyncClientProtocol { - public func unimplementedCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_Empty { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_UnimplementedServiceClientMetadata.Methods.unimplementedCall.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Grpc_Testing_UnimplementedServiceAsyncClient: Grpc_Testing_UnimplementedServiceAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -public protocol Grpc_Testing_UnimplementedServiceClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'unimplementedCall'. - func makeUnimplementedCallInterceptors() -> [ClientInterceptor] -} - -public enum Grpc_Testing_UnimplementedServiceClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "UnimplementedService", - fullName: "grpc.testing.UnimplementedService", - methods: [ - Grpc_Testing_UnimplementedServiceClientMetadata.Methods.unimplementedCall, - ] - ) - - public enum Methods { - public static let unimplementedCall = GRPCMethodDescriptor( - name: "UnimplementedCall", - path: "/grpc.testing.UnimplementedService/UnimplementedCall", - type: GRPCCallType.unary - ) - } -} - -/// A service used to control reconnect server. -/// -/// Usage: instantiate `Grpc_Testing_ReconnectServiceClient`, then call methods of this protocol to make API calls. -public protocol Grpc_Testing_ReconnectServiceClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? { get } - - func start( - _ request: Grpc_Testing_ReconnectParams, - callOptions: CallOptions? - ) -> UnaryCall - - func stop( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Grpc_Testing_ReconnectServiceClientProtocol { - public var serviceName: String { - return "grpc.testing.ReconnectService" - } - - /// Unary call to Start - /// - /// - Parameters: - /// - request: Request to send to Start. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func start( - _ request: Grpc_Testing_ReconnectParams, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_ReconnectServiceClientMetadata.Methods.start.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStartInterceptors() ?? [] - ) - } - - /// Unary call to Stop - /// - /// - Parameters: - /// - request: Request to send to Stop. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - public func stop( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Grpc_Testing_ReconnectServiceClientMetadata.Methods.stop.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStopInterceptors() ?? [] - ) - } -} - -@available(*, deprecated) -extension Grpc_Testing_ReconnectServiceClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Grpc_Testing_ReconnectServiceNIOClient") -public final class Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectServiceClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - public var interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the grpc.testing.ReconnectService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -public struct Grpc_Testing_ReconnectServiceNIOClient: Grpc_Testing_ReconnectServiceClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? - - /// Creates a client for the grpc.testing.ReconnectService service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// A service used to control reconnect server. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_ReconnectServiceAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? { get } - - func makeStartCall( - _ request: Grpc_Testing_ReconnectParams, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeStopCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_ReconnectServiceAsyncClientProtocol { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_ReconnectServiceClientMetadata.serviceDescriptor - } - - public var interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? { - return nil - } - - public func makeStartCall( - _ request: Grpc_Testing_ReconnectParams, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_ReconnectServiceClientMetadata.Methods.start.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStartInterceptors() ?? [] - ) - } - - public func makeStopCall( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Grpc_Testing_ReconnectServiceClientMetadata.Methods.stop.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStopInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_ReconnectServiceAsyncClientProtocol { - public func start( - _ request: Grpc_Testing_ReconnectParams, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_Empty { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_ReconnectServiceClientMetadata.Methods.start.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStartInterceptors() ?? [] - ) - } - - public func stop( - _ request: Grpc_Testing_Empty, - callOptions: CallOptions? = nil - ) async throws -> Grpc_Testing_ReconnectInfo { - return try await self.performAsyncUnaryCall( - path: Grpc_Testing_ReconnectServiceClientMetadata.Methods.stop.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeStopInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public struct Grpc_Testing_ReconnectServiceAsyncClient: Grpc_Testing_ReconnectServiceAsyncClientProtocol { - public var channel: GRPCChannel - public var defaultCallOptions: CallOptions - public var interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? - - public init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -public protocol Grpc_Testing_ReconnectServiceClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'start'. - func makeStartInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'stop'. - func makeStopInterceptors() -> [ClientInterceptor] -} - -public enum Grpc_Testing_ReconnectServiceClientMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "ReconnectService", - fullName: "grpc.testing.ReconnectService", - methods: [ - Grpc_Testing_ReconnectServiceClientMetadata.Methods.start, - Grpc_Testing_ReconnectServiceClientMetadata.Methods.stop, - ] - ) - - public enum Methods { - public static let start = GRPCMethodDescriptor( - name: "Start", - path: "/grpc.testing.ReconnectService/Start", - type: GRPCCallType.unary - ) - - public static let stop = GRPCMethodDescriptor( - name: "Stop", - path: "/grpc.testing.ReconnectService/Stop", - type: GRPCCallType.unary - ) - } -} - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -/// -/// To build a server, implement a class that conforms to this protocol. -public protocol Grpc_Testing_TestServiceProvider: CallHandlerProvider { - var interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? { get } - - /// One empty request followed by one empty response. - func emptyCall(request: Grpc_Testing_Empty, context: StatusOnlyCallContext) -> EventLoopFuture - - /// One request followed by one response. - func unaryCall(request: Grpc_Testing_SimpleRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - func cacheableUnaryCall(request: Grpc_Testing_SimpleRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - func streamingOutputCall(request: Grpc_Testing_StreamingOutputCallRequest, context: StreamingResponseCallContext) -> EventLoopFuture - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - func streamingInputCall(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - func fullDuplexCall(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - func halfDuplexCall(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Grpc_Testing_TestServiceProvider { - public var serviceName: Substring { - return Grpc_Testing_TestServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "EmptyCall": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeEmptyCallInterceptors() ?? [], - userFunction: self.emptyCall(request:context:) - ) - - case "UnaryCall": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [], - userFunction: self.unaryCall(request:context:) - ) - - case "CacheableUnaryCall": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCacheableUnaryCallInterceptors() ?? [], - userFunction: self.cacheableUnaryCall(request:context:) - ) - - case "StreamingOutputCall": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingOutputCallInterceptors() ?? [], - userFunction: self.streamingOutputCall(request:context:) - ) - - case "StreamingInputCall": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingInputCallInterceptors() ?? [], - observerFactory: self.streamingInputCall(context:) - ) - - case "FullDuplexCall": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeFullDuplexCallInterceptors() ?? [], - observerFactory: self.fullDuplexCall(context:) - ) - - case "HalfDuplexCall": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeHalfDuplexCallInterceptors() ?? [], - observerFactory: self.halfDuplexCall(context:) - ) - - default: - return nil - } - } -} - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_TestServiceAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? { get } - - /// One empty request followed by one empty response. - func emptyCall( - request: Grpc_Testing_Empty, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_Empty - - /// One request followed by one response. - func unaryCall( - request: Grpc_Testing_SimpleRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - func cacheableUnaryCall( - request: Grpc_Testing_SimpleRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - func streamingOutputCall( - request: Grpc_Testing_StreamingOutputCallRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - func streamingInputCall( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_StreamingInputCallResponse - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - func fullDuplexCall( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - func halfDuplexCall( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_TestServiceAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_TestServiceServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Grpc_Testing_TestServiceServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "EmptyCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeEmptyCallInterceptors() ?? [], - wrapping: { try await self.emptyCall(request: $0, context: $1) } - ) - - case "UnaryCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnaryCallInterceptors() ?? [], - wrapping: { try await self.unaryCall(request: $0, context: $1) } - ) - - case "CacheableUnaryCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCacheableUnaryCallInterceptors() ?? [], - wrapping: { try await self.cacheableUnaryCall(request: $0, context: $1) } - ) - - case "StreamingOutputCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingOutputCallInterceptors() ?? [], - wrapping: { try await self.streamingOutputCall(request: $0, responseStream: $1, context: $2) } - ) - - case "StreamingInputCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStreamingInputCallInterceptors() ?? [], - wrapping: { try await self.streamingInputCall(requestStream: $0, context: $1) } - ) - - case "FullDuplexCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeFullDuplexCallInterceptors() ?? [], - wrapping: { try await self.fullDuplexCall(requestStream: $0, responseStream: $1, context: $2) } - ) - - case "HalfDuplexCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeHalfDuplexCallInterceptors() ?? [], - wrapping: { try await self.halfDuplexCall(requestStream: $0, responseStream: $1, context: $2) } - ) - - default: - return nil - } - } -} - -public protocol Grpc_Testing_TestServiceServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'emptyCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeEmptyCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'unaryCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeUnaryCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'cacheableUnaryCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCacheableUnaryCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'streamingOutputCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStreamingOutputCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'streamingInputCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStreamingInputCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'fullDuplexCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeFullDuplexCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'halfDuplexCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeHalfDuplexCallInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'unimplementedCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeUnimplementedCallInterceptors() -> [ServerInterceptor] -} - -public enum Grpc_Testing_TestServiceServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "TestService", - fullName: "grpc.testing.TestService", - methods: [ - Grpc_Testing_TestServiceServerMetadata.Methods.emptyCall, - Grpc_Testing_TestServiceServerMetadata.Methods.unaryCall, - Grpc_Testing_TestServiceServerMetadata.Methods.cacheableUnaryCall, - Grpc_Testing_TestServiceServerMetadata.Methods.streamingOutputCall, - Grpc_Testing_TestServiceServerMetadata.Methods.streamingInputCall, - Grpc_Testing_TestServiceServerMetadata.Methods.fullDuplexCall, - Grpc_Testing_TestServiceServerMetadata.Methods.halfDuplexCall, - Grpc_Testing_TestServiceServerMetadata.Methods.unimplementedCall, - ] - ) - - public enum Methods { - public static let emptyCall = GRPCMethodDescriptor( - name: "EmptyCall", - path: "/grpc.testing.TestService/EmptyCall", - type: GRPCCallType.unary - ) - - public static let unaryCall = GRPCMethodDescriptor( - name: "UnaryCall", - path: "/grpc.testing.TestService/UnaryCall", - type: GRPCCallType.unary - ) - - public static let cacheableUnaryCall = GRPCMethodDescriptor( - name: "CacheableUnaryCall", - path: "/grpc.testing.TestService/CacheableUnaryCall", - type: GRPCCallType.unary - ) - - public static let streamingOutputCall = GRPCMethodDescriptor( - name: "StreamingOutputCall", - path: "/grpc.testing.TestService/StreamingOutputCall", - type: GRPCCallType.serverStreaming - ) - - public static let streamingInputCall = GRPCMethodDescriptor( - name: "StreamingInputCall", - path: "/grpc.testing.TestService/StreamingInputCall", - type: GRPCCallType.clientStreaming - ) - - public static let fullDuplexCall = GRPCMethodDescriptor( - name: "FullDuplexCall", - path: "/grpc.testing.TestService/FullDuplexCall", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let halfDuplexCall = GRPCMethodDescriptor( - name: "HalfDuplexCall", - path: "/grpc.testing.TestService/HalfDuplexCall", - type: GRPCCallType.bidirectionalStreaming - ) - - public static let unimplementedCall = GRPCMethodDescriptor( - name: "UnimplementedCall", - path: "/grpc.testing.TestService/UnimplementedCall", - type: GRPCCallType.unary - ) - } -} -/// A simple service NOT implemented at servers so clients can test for -/// that case. -/// -/// To build a server, implement a class that conforms to this protocol. -public protocol Grpc_Testing_UnimplementedServiceProvider: CallHandlerProvider { - var interceptors: Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol? { get } - - /// A call that no server should implement - func unimplementedCall(request: Grpc_Testing_Empty, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension Grpc_Testing_UnimplementedServiceProvider { - public var serviceName: Substring { - return Grpc_Testing_UnimplementedServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "UnimplementedCall": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [], - userFunction: self.unimplementedCall(request:context:) - ) - - default: - return nil - } - } -} - -/// A simple service NOT implemented at servers so clients can test for -/// that case. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_UnimplementedServiceAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol? { get } - - /// A call that no server should implement - func unimplementedCall( - request: Grpc_Testing_Empty, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_Empty -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_UnimplementedServiceAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_UnimplementedServiceServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Grpc_Testing_UnimplementedServiceServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "UnimplementedCall": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [], - wrapping: { try await self.unimplementedCall(request: $0, context: $1) } - ) - - default: - return nil - } - } -} - -public protocol Grpc_Testing_UnimplementedServiceServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'unimplementedCall'. - /// Defaults to calling `self.makeInterceptors()`. - func makeUnimplementedCallInterceptors() -> [ServerInterceptor] -} - -public enum Grpc_Testing_UnimplementedServiceServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "UnimplementedService", - fullName: "grpc.testing.UnimplementedService", - methods: [ - Grpc_Testing_UnimplementedServiceServerMetadata.Methods.unimplementedCall, - ] - ) - - public enum Methods { - public static let unimplementedCall = GRPCMethodDescriptor( - name: "UnimplementedCall", - path: "/grpc.testing.UnimplementedService/UnimplementedCall", - type: GRPCCallType.unary - ) - } -} -/// A service used to control reconnect server. -/// -/// To build a server, implement a class that conforms to this protocol. -public protocol Grpc_Testing_ReconnectServiceProvider: CallHandlerProvider { - var interceptors: Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol? { get } - - func start(request: Grpc_Testing_ReconnectParams, context: StatusOnlyCallContext) -> EventLoopFuture - - func stop(request: Grpc_Testing_Empty, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension Grpc_Testing_ReconnectServiceProvider { - public var serviceName: Substring { - return Grpc_Testing_ReconnectServiceServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Start": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStartInterceptors() ?? [], - userFunction: self.start(request:context:) - ) - - case "Stop": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStopInterceptors() ?? [], - userFunction: self.stop(request:context:) - ) - - default: - return nil - } - } -} - -/// A service used to control reconnect server. -/// -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public protocol Grpc_Testing_ReconnectServiceAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol? { get } - - func start( - request: Grpc_Testing_ReconnectParams, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_Empty - - func stop( - request: Grpc_Testing_Empty, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_ReconnectInfo -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Testing_ReconnectServiceAsyncProvider { - public static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Testing_ReconnectServiceServerMetadata.serviceDescriptor - } - - public var serviceName: Substring { - return Grpc_Testing_ReconnectServiceServerMetadata.serviceDescriptor.fullName[...] - } - - public var interceptors: Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol? { - return nil - } - - public func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Start": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStartInterceptors() ?? [], - wrapping: { try await self.start(request: $0, context: $1) } - ) - - case "Stop": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeStopInterceptors() ?? [], - wrapping: { try await self.stop(request: $0, context: $1) } - ) - - default: - return nil - } - } -} - -public protocol Grpc_Testing_ReconnectServiceServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'start'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStartInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'stop'. - /// Defaults to calling `self.makeInterceptors()`. - func makeStopInterceptors() -> [ServerInterceptor] -} - -public enum Grpc_Testing_ReconnectServiceServerMetadata { - public static let serviceDescriptor = GRPCServiceDescriptor( - name: "ReconnectService", - fullName: "grpc.testing.ReconnectService", - methods: [ - Grpc_Testing_ReconnectServiceServerMetadata.Methods.start, - Grpc_Testing_ReconnectServiceServerMetadata.Methods.stop, - ] - ) - - public enum Methods { - public static let start = GRPCMethodDescriptor( - name: "Start", - path: "/grpc.testing.ReconnectService/Start", - type: GRPCCallType.unary - ) - - public static let stop = GRPCMethodDescriptor( - name: "Stop", - path: "/grpc.testing.ReconnectService/Stop", - type: GRPCCallType.unary - ) - } -} diff --git a/Sources/GRPCInteroperabilityTestModels/Generated/test.pb.swift b/Sources/GRPCInteroperabilityTestModels/Generated/test.pb.swift deleted file mode 100644 index 8fb71aea8..000000000 --- a/Sources/GRPCInteroperabilityTestModels/Generated/test.pb.swift +++ /dev/null @@ -1,38 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/test.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015-2016 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} diff --git a/Sources/GRPCInteroperabilityTestModels/README.md b/Sources/GRPCInteroperabilityTestModels/README.md deleted file mode 100644 index f1cffa62e..000000000 --- a/Sources/GRPCInteroperabilityTestModels/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# gRPC Interoperability Test Protos - -This module contains the generated models for the gRPC interoperability tests -and the script used to generate them. - -The tests require that some methods and services are left unimplemented, this -requires manual edits after code generation. These instructions are emitted to -`stdout` at the end of the `generate.sh` script. - -* `generate.sh`: generates models from interoperability test protobufs. -* `src`: source of protobufs. -* `Generated`: output directory for generated models. diff --git a/Sources/GRPCInteroperabilityTestModels/generate.sh b/Sources/GRPCInteroperabilityTestModels/generate.sh deleted file mode 100755 index 52c945215..000000000 --- a/Sources/GRPCInteroperabilityTestModels/generate.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -CURRENT_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -PLUGIN_SWIFT=../../.build/release/protoc-gen-swift -PLUGIN_SWIFTGRPC=../../.build/release/protoc-gen-grpc-swift -PROTO="src/proto/grpc/testing/test.proto" - -OUTPUT="Generated" -FILE_NAMING="DropPath" -VISIBILITY="Public" - -(cd "${CURRENT_SCRIPT_DIR}" && protoc "src/proto/grpc/testing/test.proto" \ - --plugin=${PLUGIN_SWIFT} \ - --plugin=${PLUGIN_SWIFTGRPC} \ - --swift_out=${OUTPUT} \ - --swift_opt=FileNaming=${FILE_NAMING},Visibility=${VISIBILITY} \ - --grpc-swift_out=${OUTPUT} \ - --grpc-swift_opt=FileNaming=${FILE_NAMING},Visibility=${VISIBILITY}) - -(cd "${CURRENT_SCRIPT_DIR}" && protoc "src/proto/grpc/testing/empty.proto" \ - --plugin=${PLUGIN_SWIFT} \ - --plugin=${PLUGIN_SWIFTGRPC} \ - --swift_out=${OUTPUT} \ - --swift_opt=FileNaming=${FILE_NAMING},Visibility=${VISIBILITY} \ - --grpc-swift_out=${OUTPUT} \ - --grpc-swift_opt=FileNaming=${FILE_NAMING},Visibility=${VISIBILITY}) - -(cd "${CURRENT_SCRIPT_DIR}" && protoc "src/proto/grpc/testing/messages.proto" \ - --plugin=${PLUGIN_SWIFT} \ - --plugin=${PLUGIN_SWIFTGRPC} \ - --swift_out=${OUTPUT} \ - --swift_opt=FileNaming=${FILE_NAMING},Visibility=${VISIBILITY} \ - --grpc-swift_out=${OUTPUT} \ - --grpc-swift_opt=FileNaming=${FILE_NAMING},Visibility=${VISIBILITY}) - -# The generated code needs to be modified to support testing an unimplemented method. -# On the server side, the generated code needs to be removed so the server has no -# knowledge of it. Client code requires no modification, since it is required to call -# the unimplemented method. -(cd "${CURRENT_SCRIPT_DIR}" && patch -p3 < unimplemented_call.patch) diff --git a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty.proto b/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty.proto deleted file mode 100644 index dc4cc6067..000000000 --- a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty.proto +++ /dev/null @@ -1,28 +0,0 @@ - -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -// An empty message that you can re-use to avoid defining duplicated empty -// messages in your project. A typical example is to use it as argument or the -// return value of a service API. For instance: -// -// service Foo { -// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; -// }; -// -message Empty {} \ No newline at end of file diff --git a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty_service.proto b/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty_service.proto deleted file mode 100644 index 42e9cee1c..000000000 --- a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/empty_service.proto +++ /dev/null @@ -1,23 +0,0 @@ - -// Copyright 2018 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -// A service that has zero methods. -// See https://github.com/grpc/grpc/issues/15574 -service EmptyService { -} \ No newline at end of file diff --git a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/messages.proto b/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/messages.proto deleted file mode 100644 index bbc4d6988..000000000 --- a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/messages.proto +++ /dev/null @@ -1,165 +0,0 @@ - -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -syntax = "proto3"; - -package grpc.testing; - -// TODO(dgq): Go back to using well-known types once -// https://github.com/grpc/grpc/issues/6980 has been fixed. -// import "google/protobuf/wrappers.proto"; -message BoolValue { - // The bool value. - bool value = 1; -} - -// The type of payload that should be returned. -enum PayloadType { - // Compressable text format. - COMPRESSABLE = 0; -} - -// A block of data, to simply increase gRPC message size. -message Payload { - // The type of data in body. - PayloadType type = 1; - // Primary contents of payload. - bytes body = 2; -} - -// A protobuf representation for grpc status. This is used by test -// clients to specify a status that the server should attempt to return. -message EchoStatus { - int32 code = 1; - string message = 2; -} - -// Unary request. -message SimpleRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, server randomly chooses one from other formats. - PayloadType response_type = 1; - - // Desired payload size in the response from the server. - int32 response_size = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether SimpleResponse should include username. - bool fill_username = 4; - - // Whether SimpleResponse should include OAuth scope. - bool fill_oauth_scope = 5; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue response_compressed = 6; - - // Whether server should return a given status - EchoStatus response_status = 7; - - // Whether the server should expect this request to be compressed. - BoolValue expect_compressed = 8; -} - -// Unary response, as configured by the request. -message SimpleResponse { - // Payload to increase message size. - Payload payload = 1; - // The user the request came from, for verifying authentication was - // successful when the client expected it. - string username = 2; - // OAuth scope. - string oauth_scope = 3; -} - -// Client-streaming request. -message StreamingInputCallRequest { - // Optional input payload sent along with the request. - Payload payload = 1; - - // Whether the server should expect this request to be compressed. This field - // is "nullable" in order to interoperate seamlessly with servers not able to - // implement the full compression tests by introspecting the call to verify - // the request's compression status. - BoolValue expect_compressed = 2; - - // Not expecting any payload from the response. -} - -// Client-streaming response. -message StreamingInputCallResponse { - // Aggregated size of payloads received from the client. - int32 aggregated_payload_size = 1; -} - -// Configuration for a particular response. -message ResponseParameters { - // Desired payload sizes in responses from the server. - int32 size = 1; - - // Desired interval between consecutive responses in the response stream in - // microseconds. - int32 interval_us = 2; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue compressed = 3; -} - -// Server-streaming request. -message StreamingOutputCallRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, the payload from each response in the stream - // might be of different types. This is to simulate a mixed type of payload - // stream. - PayloadType response_type = 1; - - // Configuration for each expected response message. - repeated ResponseParameters response_parameters = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether server should return a given status - EchoStatus response_status = 7; -} - -// Server-streaming response, as configured by the request and parameters. -message StreamingOutputCallResponse { - // Payload to increase response size. - Payload payload = 1; -} - -// For reconnect interop test only. -// Client tells server what reconnection parameters it used. -message ReconnectParams { - int32 max_reconnect_backoff_ms = 1; -} - -// For reconnect interop test only. -// Server tells client whether its reconnects are following the spec and the -// reconnect backoffs it saw. -message ReconnectInfo { - bool passed = 1; - repeated int32 backoff_ms = 2; -} \ No newline at end of file diff --git a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/test.proto b/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/test.proto deleted file mode 100644 index c049c8fa0..000000000 --- a/Sources/GRPCInteroperabilityTestModels/src/proto/grpc/testing/test.proto +++ /dev/null @@ -1,79 +0,0 @@ - -// Copyright 2015-2016 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. - -syntax = "proto3"; - -import "src/proto/grpc/testing/empty.proto"; -import "src/proto/grpc/testing/messages.proto"; - -package grpc.testing; - -// A simple service to test the various types of RPCs and experiment with -// performance with various types of payload. -service TestService { - // One empty request followed by one empty response. - rpc EmptyCall(grpc.testing.Empty) returns (grpc.testing.Empty); - - // One request followed by one response. - rpc UnaryCall(SimpleRequest) returns (SimpleResponse); - - // One request followed by one response. Response has cache control - // headers set such that a caching HTTP proxy (such as GFE) can - // satisfy subsequent requests. - rpc CacheableUnaryCall(SimpleRequest) returns (SimpleResponse); - - // One request followed by a sequence of responses (streamed download). - // The server returns the payload with client desired type and sizes. - rpc StreamingOutputCall(StreamingOutputCallRequest) - returns (stream StreamingOutputCallResponse); - - // A sequence of requests followed by one response (streamed upload). - // The server returns the aggregated size of client payload as the result. - rpc StreamingInputCall(stream StreamingInputCallRequest) - returns (StreamingInputCallResponse); - - // A sequence of requests with each request served by the server immediately. - // As one request could lead to multiple responses, this interface - // demonstrates the idea of full duplexing. - rpc FullDuplexCall(stream StreamingOutputCallRequest) - returns (stream StreamingOutputCallResponse); - - // A sequence of requests followed by a sequence of responses. - // The server buffers all the client requests and then serves them in order. A - // stream of responses are returned to the client when the server starts with - // first request. - rpc HalfDuplexCall(stream StreamingOutputCallRequest) - returns (stream StreamingOutputCallResponse); - - // The test server will not implement this method. It will be used - // to test the behavior when clients call unimplemented methods. - rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); -} - -// A simple service NOT implemented at servers so clients can test for -// that case. -service UnimplementedService { - // A call that no server should implement - rpc UnimplementedCall(grpc.testing.Empty) returns (grpc.testing.Empty); -} - -// A service used to control reconnect server. -service ReconnectService { - rpc Start(grpc.testing.ReconnectParams) returns (grpc.testing.Empty); - rpc Stop(grpc.testing.Empty) returns (grpc.testing.ReconnectInfo); -} diff --git a/Sources/GRPCInteroperabilityTestModels/unimplemented_call.patch b/Sources/GRPCInteroperabilityTestModels/unimplemented_call.patch deleted file mode 100644 index 526c29be1..000000000 --- a/Sources/GRPCInteroperabilityTestModels/unimplemented_call.patch +++ /dev/null @@ -1,59 +0,0 @@ ---- a/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift -+++ b/Sources/GRPCInteroperabilityTestModels/Generated/test.grpc.swift -@@ -931,10 +931,6 @@ - /// stream of responses are returned to the client when the server starts with - /// first request. - func halfDuplexCall(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -- -- /// The test server will not implement this method. It will be used -- /// to test the behavior when clients call unimplemented methods. -- func unimplementedCall(request: Grpc_Testing_Empty, context: StatusOnlyCallContext) -> EventLoopFuture - } - - extension Grpc_Testing_TestServiceProvider { -@@ -1010,15 +1006,6 @@ - observerFactory: self.halfDuplexCall(context:) - ) - -- case "UnimplementedCall": -- return UnaryServerHandler( -- context: context, -- requestDeserializer: ProtobufDeserializer(), -- responseSerializer: ProtobufSerializer(), -- interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [], -- userFunction: self.unimplementedCall(request:context:) -- ) -- - default: - return nil - } -@@ -1123,13 +1110,6 @@ - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -- -- /// The test server will not implement this method. It will be used -- /// to test the behavior when clients call unimplemented methods. -- @Sendable func unimplementedCall( -- request: Grpc_Testing_Empty, -- context: GRPCAsyncServerCallContext -- ) async throws -> Grpc_Testing_Empty - } - - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -@@ -1210,15 +1190,6 @@ - wrapping: self.halfDuplexCall(requests:responseStream:context:) - ) - -- case "UnimplementedCall": -- return GRPCAsyncServerHandler( -- context: context, -- requestDeserializer: ProtobufDeserializer(), -- responseSerializer: ProtobufSerializer(), -- interceptors: self.interceptors?.makeUnimplementedCallInterceptors() ?? [], -- wrapping: self.unimplementedCall(request:context:) -- ) -- - default: - return nil - } diff --git a/Sources/GRPCInteroperabilityTests/main.swift b/Sources/GRPCInteroperabilityTests/main.swift deleted file mode 100644 index e50de8fb8..000000000 --- a/Sources/GRPCInteroperabilityTests/main.swift +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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 ArgumentParser -import Foundation -import GRPC -import GRPCInteroperabilityTestsImplementation -import Logging -import NIOCore -import NIOPosix - -// Reduce stdout noise. -LoggingSystem.bootstrap(StreamLogHandler.standardError) - -enum InteroperabilityTestError: LocalizedError { - case testNotFound(String) - case testFailed(Error) - - var errorDescription: String? { - switch self { - case let .testNotFound(name): - return "No test named '\(name)' was found" - - case let .testFailed(error): - return "Test failed with error: \(error)" - } - } -} - -/// Runs the test instance using the given connection. -/// -/// Success or failure is indicated by the lack or presence of thrown errors, respectively. -/// -/// - Parameters: -/// - instance: `InteroperabilityTest` instance to run. -/// - name: the name of the test, use for logging only. -/// - host: host of the test server. -/// - port: port of the test server. -/// - useTLS: whether to use TLS when connecting to the test server. -/// - Throws: `InteroperabilityTestError` if the test fails. -func runTest( - _ instance: InteroperabilityTest, - name: String, - host: String, - port: Int, - useTLS: Bool -) throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - - do { - print("Running '\(name)' ... ", terminator: "") - let builder = makeInteroperabilityTestClientBuilder(group: group, useTLS: useTLS) - instance.configure(builder: builder) - let connection = builder.connect(host: host, port: port) - defer { - _ = connection.close() - } - try instance.run(using: connection) - print("PASSED") - } catch { - print("FAILED") - throw InteroperabilityTestError.testFailed(error) - } -} - -/// Creates a new `InteroperabilityTest` instance with the given name, or throws an -/// `InteroperabilityTestError` if no test matches the given name. Implemented test names can be -/// found by running the `list_tests` target. -func makeRunnableTest(name: String) throws -> InteroperabilityTest { - guard let testCase = InteroperabilityTestCase(rawValue: name) else { - throw InteroperabilityTestError.testNotFound(name) - } - - return testCase.makeTest() -} - -// MARK: - Command line options and "main". - -struct InteroperabilityTests: ParsableCommand { - static var configuration = CommandConfiguration( - abstract: "gRPC Swift Interoperability Runner", - subcommands: [StartServer.self, RunTest.self, ListTests.self] - ) - - struct StartServer: ParsableCommand { - static var configuration = CommandConfiguration( - abstract: "Start the gRPC Swift interoperability test server." - ) - - @Option(help: "The port to listen on for new connections") - var port: Int - - @Flag(help: "Whether TLS should be used or not") - var tls = false - - func run() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - try! group.syncShutdownGracefully() - } - - let server = try makeInteroperabilityTestServer( - port: self.port, - eventLoopGroup: group, - useTLS: self.tls - ).wait() - print("server started: \(server.channel.localAddress!)") - - // We never call close; run until we get killed. - try server.onClose.wait() - } - } - - struct RunTest: ParsableCommand { - static var configuration = CommandConfiguration( - abstract: "Runs a gRPC interoperability test using a gRPC Swift client." - ) - - @Flag(help: "Whether TLS should be used or not") - var tls = false - - @Option(help: "The host the server is running on") - var host: String - - @Option(help: "The port to connect to") - var port: Int - - @Argument(help: "The name of the test to run") - var testName: String - - func run() throws { - let test = try makeRunnableTest(name: self.testName) - try runTest( - test, - name: self.testName, - host: self.host, - port: self.port, - useTLS: self.tls - ) - } - } - - struct ListTests: ParsableCommand { - static var configuration = CommandConfiguration( - abstract: "List all interoperability test names." - ) - - func run() throws { - InteroperabilityTestCase.allCases.forEach { - print($0.name) - } - } - } -} - -InteroperabilityTests.main() diff --git a/Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift b/Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift deleted file mode 100644 index 181f9b0f4..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/Assertions.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 NIOCore - -/// Assertion error for interoperability testing. -/// -/// This is required because these tests must be able to run without XCTest. -public struct AssertionError: Error { - let message: String - let file: StaticString - let line: UInt -} - -/// Asserts that the two given values are equal. -public func assertEqual( - _ value1: T, - _ value2: T, - file: StaticString = #fileID, - line: UInt = #line -) throws { - guard value1 == value2 else { - throw AssertionError(message: "'\(value1)' is not equal to '\(value2)'", file: file, line: line) - } -} - -/// Waits for the future to be fulfilled and asserts that its value is equal to the given value. -/// -/// - Important: This should not be run on an event loop since this function calls `wait()` on the -/// given future. -public func waitAndAssertEqual( - _ future: EventLoopFuture, - _ value: T, - file: StaticString = #fileID, - line: UInt = #line -) throws { - try assertEqual(try future.wait(), value, file: file, line: line) -} - -/// Waits for the futures to be fulfilled and ssserts that their values are equal. -/// -/// - Important: This should not be run on an event loop since this function calls `wait()` on the -/// given future. -public func waitAndAssertEqual( - _ future1: EventLoopFuture, - _ future2: EventLoopFuture, - file: StaticString = #fileID, - line: UInt = #line -) throws { - try assertEqual(try future1.wait(), try future2.wait(), file: file, line: line) -} - -public func waitAndAssert( - _ future: EventLoopFuture, - file: StaticString = #fileID, - line: UInt = #line, - message: String = "", - verify: (T) -> Bool -) throws { - let value = try future.wait() - guard verify(value) else { - throw AssertionError(message: message, file: file, line: line) - } -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift b/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift deleted file mode 100644 index 4b291ba13..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/GRPCTestingConvenienceMethods.swift +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 GRPCInteroperabilityTestModels -import NIOHPACK -import SwiftProtobuf - -import struct Foundation.Data - -// MARK: - Payload creation - -extension Grpc_Testing_Payload { - static func bytes(of value: UInt64) -> Grpc_Testing_Payload { - return Grpc_Testing_Payload.with { payload in - withUnsafeBytes(of: value) { bytes in - payload.body = Data(bytes) - } - } - } - - static func zeros(count: Int) -> Grpc_Testing_Payload { - return Grpc_Testing_Payload.with { payload in - payload.body = Data(repeating: 0, count: count) - } - } -} - -// MARK: - Bool value - -extension Grpc_Testing_BoolValue { - public init(_ value: Bool) { - self = .with { $0.value = value } - } -} - -// MARK: - Echo status creation - -extension Grpc_Testing_EchoStatus { - init(code: Int32, message: String) { - self = .with { - $0.code = code - $0.message = message - } - } -} - -// MARK: - Response Parameter creation - -extension Grpc_Testing_ResponseParameters { - static func size(_ size: Int) -> Grpc_Testing_ResponseParameters { - return Grpc_Testing_ResponseParameters.with { parameters in - parameters.size = numericCast(size) - } - } -} - -// MARK: - Echo status - -protocol EchoStatusRequest: Message { - var responseStatus: Grpc_Testing_EchoStatus { get set } -} - -extension EchoStatusRequest { - var shouldEchoStatus: Bool { - return self.responseStatus != Grpc_Testing_EchoStatus() - } -} - -extension EchoStatusRequest { - static func withStatus(of status: Grpc_Testing_EchoStatus) -> Self { - return Self.with { instance in - instance.responseStatus = status - } - } -} - -extension Grpc_Testing_SimpleRequest: EchoStatusRequest {} -extension Grpc_Testing_StreamingOutputCallRequest: EchoStatusRequest {} - -// MARK: - Payload request - -protocol PayloadRequest: Message { - var payload: Grpc_Testing_Payload { get set } -} - -extension PayloadRequest { - static func withPayload(of payload: Grpc_Testing_Payload) -> Self { - return Self.with { instance in - instance.payload = payload - } - } -} - -extension Grpc_Testing_SimpleRequest: PayloadRequest {} -extension Grpc_Testing_StreamingOutputCallRequest: PayloadRequest {} -extension Grpc_Testing_StreamingInputCallRequest: PayloadRequest {} - -// MARK: - Echo metadata - -extension HPACKHeaders { - /// See `ServerFeatures.echoMetadata`. - var shouldEchoMetadata: Bool { - return self.contains(name: "x-grpc-test-echo-initial") - || self - .contains(name: "x-grpc-test-echo-trailing-bin") - } -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCase.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCase.swift deleted file mode 100644 index 8e19856b2..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCase.swift +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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 GRPC -import NIOCore - -public protocol InteroperabilityTest { - /// Run a test case using the given connection. - /// - /// The test case is considered unsuccessful if any exception is thrown, conversely if no - /// exceptions are thrown it is successful. - /// - /// - Parameter connection: The connection to use for the test. - /// - Throws: Any exception may be thrown to indicate an unsuccessful test. - func run(using connection: ClientConnection) throws - - /// Configure the connection from a set of defaults using to run the entire suite. - /// - /// Test cases may use this to, for example, enable compression at the connection level on a - /// per-test basis. - /// - /// - Parameter defaults: The default configuration for the test run. - func configure(builder: ClientConnection.Builder) -} - -extension InteroperabilityTest { - func configure(builder: ClientConnection.Builder) {} -} - -/// Test cases as listed by the [gRPC interoperability test description -/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). -/// -/// This is not a complete list, the following tests have not been implemented: -/// - compute_engine_creds -/// - jwt_token_creds -/// - oauth2_auth_token -/// - per_rpc_creds -/// - google_default_credentials -/// - compute_engine_channel_credentials -/// -/// Note: a description from the specification is included inline for each test as documentation for -/// its associated `InteroperabilityTest` class. -public enum InteroperabilityTestCase: String, CaseIterable { - case emptyUnary = "empty_unary" - case cacheableUnary = "cacheable_unary" - case largeUnary = "large_unary" - case clientCompressedUnary = "client_compressed_unary" - case serverCompressedUnary = "server_compressed_unary" - case clientStreaming = "client_streaming" - case clientCompressedStreaming = "client_compressed_streaming" - case serverStreaming = "server_streaming" - case serverCompressedStreaming = "server_compressed_streaming" - case pingPong = "ping_pong" - case emptyStream = "empty_stream" - case customMetadata = "custom_metadata" - case statusCodeAndMessage = "status_code_and_message" - case specialStatusMessage = "special_status_message" - case unimplementedMethod = "unimplemented_method" - case unimplementedService = "unimplemented_service" - case cancelAfterBegin = "cancel_after_begin" - case cancelAfterFirstResponse = "cancel_after_first_response" - case timeoutOnSleepingServer = "timeout_on_sleeping_server" - - public var name: String { - return self.rawValue - } -} - -extension InteroperabilityTestCase { - /// Return a new instance of the test case. - public func makeTest() -> InteroperabilityTest { - switch self { - case .emptyUnary: - return EmptyUnary() - case .cacheableUnary: - return CacheableUnary() - case .largeUnary: - return LargeUnary() - case .clientCompressedUnary: - return ClientCompressedUnary() - case .serverCompressedUnary: - return ServerCompressedUnary() - case .clientStreaming: - return ClientStreaming() - case .clientCompressedStreaming: - return ClientCompressedStreaming() - case .serverStreaming: - return ServerStreaming() - case .serverCompressedStreaming: - return ServerCompressedStreaming() - case .pingPong: - return PingPong() - case .emptyStream: - return EmptyStream() - case .customMetadata: - return CustomMetadata() - case .statusCodeAndMessage: - return StatusCodeAndMessage() - case .specialStatusMessage: - return SpecialStatusMessage() - case .unimplementedMethod: - return UnimplementedMethod() - case .unimplementedService: - return UnimplementedService() - case .cancelAfterBegin: - return CancelAfterBegin() - case .cancelAfterFirstResponse: - return CancelAfterFirstResponse() - case .timeoutOnSleepingServer: - return TimeoutOnSleepingServer() - } - } - - /// The set of server features required to run this test. - public var requiredServerFeatures: Set { - switch self { - case .emptyUnary: - return [.emptyCall] - case .cacheableUnary: - return [.cacheableUnaryCall] - case .largeUnary: - return [.unaryCall] - case .clientStreaming: - return [.streamingInputCall] - case .clientCompressedStreaming: - return [.streamingInputCall, .compressedRequest] - case .clientCompressedUnary: - return [.unaryCall, .compressedRequest] - case .serverCompressedUnary: - return [.unaryCall, .compressedResponse] - case .serverStreaming: - return [.streamingOutputCall] - case .serverCompressedStreaming: - return [.streamingOutputCall, .compressedResponse] - case .pingPong: - return [.fullDuplexCall] - case .emptyStream: - return [.fullDuplexCall] - case .customMetadata: - return [.unaryCall, .fullDuplexCall, .echoMetadata] - case .statusCodeAndMessage: - return [.unaryCall, .fullDuplexCall, .echoStatus] - case .specialStatusMessage: - return [.unaryCall, .echoStatus] - case .unimplementedMethod: - return [] - case .unimplementedService: - return [] - case .cancelAfterBegin: - return [.streamingInputCall] - case .cancelAfterFirstResponse: - return [.fullDuplexCall] - case .timeoutOnSleepingServer: - return [.fullDuplexCall] - } - } -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift deleted file mode 100644 index a9fe0c16a..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift +++ /dev/null @@ -1,1056 +0,0 @@ -/* - * 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 Dispatch -import GRPC -import GRPCInteroperabilityTestModels -import NIOHPACK - -import struct Foundation.Data - -/// This test verifies that implementations support zero-size messages. Ideally, client -/// implementations would verify that the request and response were zero bytes serialized, but -/// this is generally prohibitive to perform, so is not required. -/// -/// Server features: -/// - EmptyCall -/// -/// Procedure: -/// 1. Client calls EmptyCall with the default Empty message -/// -/// Client asserts: -/// - call was successful -/// - response is non-null -class EmptyUnary: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - let call = client.emptyCall(Grpc_Testing_Empty()) - - try waitAndAssertEqual(call.response, Grpc_Testing_Empty()) - try waitAndAssertEqual(call.status.map { $0.code }, .ok) - } -} - -/// This test verifies that gRPC requests marked as cacheable use GET verb instead of POST, and -/// that server sets appropriate cache control headers for the response to be cached by a proxy. -/// This test requires that the server is behind a caching proxy. Use of current timestamp in the -/// request prevents accidental cache matches left over from previous tests. -/// -/// Server features: -/// - CacheableUnaryCall -/// -/// Procedure: -/// 1. Client calls CacheableUnaryCall with SimpleRequest request with payload set to current -/// timestamp. Timestamp format is irrelevant, and resolution is in nanoseconds. Client adds a -/// x-user-ip header with value 1.2.3.4 to the request. This is done since some proxys such as -/// GFE will not cache requests from localhost. Client marks the request as cacheable by -/// setting the cacheable flag in the request context. Longer term this should be driven by -/// the method option specified in the proto file itself. -/// 2. Client calls CacheableUnaryCall again immediately with the same request and configuration -/// as the previous call. -/// -/// Client asserts: -/// - Both calls were successful -/// - The payload body of both responses is the same. -class CacheableUnary: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let timestamp = DispatchTime.now().uptimeNanoseconds - let request = Grpc_Testing_SimpleRequest.withPayload(of: .bytes(of: timestamp)) - - let headers: HPACKHeaders = ["x-user-ip": "1.2.3.4"] - let callOptions = CallOptions(customMetadata: headers, cacheable: true) - - let call1 = client.cacheableUnaryCall(request, callOptions: callOptions) - let call2 = client.cacheableUnaryCall(request, callOptions: callOptions) - - // The server ignores the request payload so we must not validate against it. - try waitAndAssertEqual(call1.response.map { $0.payload }, call2.response.map { $0.payload }) - try waitAndAssertEqual(call1.status.map { $0.code }, .ok) - try waitAndAssertEqual(call2.status.map { $0.code }, .ok) - } -} - -/// This test verifies unary calls succeed in sending messages, and touches on flow control (even -/// if compression is enabled on the channel). -/// -/// Server features: -/// - UnaryCall -/// -/// Procedure: -/// 1. Client calls UnaryCall with: -/// ``` -/// { -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - response payload body is 314159 bytes in size -/// - clients are free to assert that the response payload body contents are zero and comparing -/// the entire response message against a golden response -class LargeUnary: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let request = Grpc_Testing_SimpleRequest.with { request in - request.responseSize = 314_159 - request.payload = .zeros(count: 271_828) - } - - let call = client.unaryCall(request) - - try waitAndAssertEqual(call.response.map { $0.payload }, .zeros(count: 314_159)) - try waitAndAssertEqual(call.status.map { $0.code }, .ok) - } -} - -/// This test verifies the client can compress unary messages by sending two unary calls, for -/// compressed and uncompressed payloads. It also sends an initial probing request to verify -/// whether the server supports the CompressedRequest feature by checking if the probing call -/// fails with an `INVALID_ARGUMENT` status. -/// -/// Server features: -/// - UnaryCall -/// - CompressedRequest -/// -/// Procedure: -/// 1. Client calls UnaryCall with the feature probe, an *uncompressed* message: -/// ``` -/// { -/// expect_compressed:{ -/// value: true -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// 2. Client calls UnaryCall with the *compressed* message: -/// ``` -/// { -/// expect_compressed:{ -/// value: true -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// 3. Client calls UnaryCall with the *uncompressed* message: -/// ``` -/// { -/// expect_compressed:{ -/// value: false -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - First call failed with `INVALID_ARGUMENT` status. -/// - Subsequent calls were successful. -/// - Response payload body is 314159 bytes in size. -/// - Clients are free to assert that the response payload body contents are zeros and comparing the -/// entire response message against a golden response. -class ClientCompressedUnary: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let compressedRequest = Grpc_Testing_SimpleRequest.with { request in - request.expectCompressed = .init(true) - request.responseSize = 314_159 - request.payload = .zeros(count: 271_828) - } - - var uncompressedRequest = compressedRequest - uncompressedRequest.expectCompressed = .init(false) - - // For unary RPCs we disable compression at the call level. - - // With compression expected but *disabled*. - let probe = client.unaryCall(compressedRequest) - try waitAndAssertEqual(probe.status.map { $0.code }, .invalidArgument) - - // With compression expected and enabled. - let options = - CallOptions( - messageEncoding: .enabled( - .init( - forRequests: .gzip, - decompressionLimit: .absolute(1024 * 1024) - ) - ) - ) - let compressed = client.unaryCall(compressedRequest, callOptions: options) - try waitAndAssertEqual(compressed.response.map { $0.payload }, .zeros(count: 314_159)) - try waitAndAssertEqual(compressed.status.map { $0.code }, .ok) - - // With compression not expected and disabled. - let uncompressed = client.unaryCall(uncompressedRequest) - try waitAndAssertEqual(uncompressed.response.map { $0.payload }, .zeros(count: 314_159)) - try waitAndAssertEqual(uncompressed.status.map { $0.code }, .ok) - } -} - -/// This test verifies the server can compress unary messages. It sends two unary -/// requests, expecting the server's response to be compressed or not according to -/// the `response_compressed` boolean. -/// -/// Whether compression was actually performed is determined by the compression bit -/// in the response's message flags. *Note that some languages may not have access -/// to the message flags, in which case the client will be unable to verify that -/// the `response_compressed` boolean is obeyed by the server*. -/// -/// -/// Server features: -/// - UnaryCall -/// - CompressedResponse -/// -/// Procedure: -/// 1. Client calls UnaryCall with `SimpleRequest`: -/// ``` -/// { -/// response_compressed:{ -/// value: true -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// ``` -/// { -/// response_compressed:{ -/// value: false -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - if supported by the implementation, when `response_compressed` is true, the response MUST have -/// the compressed message flag set. -/// - if supported by the implementation, when `response_compressed` is false, the response MUST NOT -/// have the compressed message flag set. -/// - response payload body is 314159 bytes in size in both cases. -/// - clients are free to assert that the response payload body contents are zero and comparing the -/// entire response message against a golden response -class ServerCompressedUnary: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let compressedRequest = Grpc_Testing_SimpleRequest.with { request in - request.responseCompressed = .init(true) - request.responseSize = 314_159 - request.payload = .zeros(count: 271_828) - } - - let options = - CallOptions( - messageEncoding: .enabled( - .responsesOnly( - decompressionLimit: .absolute( - 1024 * 1024 - ) - ) - ) - ) - let compressed = client.unaryCall(compressedRequest, callOptions: options) - // We can't verify that the compression bit was set, instead we verify that the encoding header - // was sent by the server. This isn't quite the same since as it can still be set but the - // compression may be not set. - try waitAndAssert(compressed.initialMetadata) { headers in - headers.first(name: "grpc-encoding") != nil - } - try waitAndAssertEqual(compressed.response.map { $0.payload }, .zeros(count: 314_159)) - try waitAndAssertEqual(compressed.status.map { $0.code }, .ok) - - var uncompressedRequest = compressedRequest - uncompressedRequest.responseCompressed.value = false - let uncompressed = client.unaryCall(uncompressedRequest) - // We can't check even check for the 'grpc-encoding' header here since it could be set with the - // compression bit on the message not set. - try waitAndAssertEqual(uncompressed.response.map { $0.payload }, .zeros(count: 314_159)) - try waitAndAssertEqual(uncompressed.status.map { $0.code }, .ok) - } -} - -/// This test verifies that client-only streaming succeeds. -/// -/// Server features: -/// - StreamingInputCall -/// -/// Procedure: -/// 1. Client calls StreamingInputCall -/// 2. Client sends: -/// ``` -/// { -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// 3. Client then sends: -/// ``` -/// { -/// payload:{ -/// body: 8 bytes of zeros -/// } -/// } -/// ``` -/// 4. Client then sends: -/// ``` -/// { -/// payload:{ -/// body: 1828 bytes of zeros -/// } -/// } -/// ``` -/// 5. Client then sends: -/// ``` -/// { -/// payload:{ -/// body: 45904 bytes of zeros -/// } -/// } -/// ``` -/// 6. Client half-closes -/// -/// Client asserts: -/// - call was successful -/// - response aggregated_payload_size is 74922 -class ClientStreaming: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - let call = client.streamingInputCall() - - let requests = [27182, 8, 1828, 45904].map { zeros in - Grpc_Testing_StreamingInputCallRequest.withPayload(of: .zeros(count: zeros)) - } - call.sendMessages(requests, promise: nil) - call.sendEnd(promise: nil) - - try waitAndAssertEqual(call.response.map { $0.aggregatedPayloadSize }, 74922) - try waitAndAssertEqual(call.status.map { $0.code }, .ok) - } -} - -/// This test verifies the client can compress requests on per-message basis by performing a -/// two-request streaming call. It also sends an initial probing request to verify whether the -/// server supports the `CompressedRequest` feature by checking if the probing call fails with -/// an `INVALID_ARGUMENT` status. -/// -/// Procedure: -/// 1. Client calls `StreamingInputCall` and sends the following feature-probing -/// *uncompressed* `StreamingInputCallRequest` message -/// -/// ``` -/// { -/// expect_compressed:{ -/// value: true -/// } -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// If the call does not fail with `INVALID_ARGUMENT`, the test fails. -/// Otherwise, we continue. -/// -/// 2. Client calls `StreamingInputCall` again, sending the *compressed* message -/// -/// ``` -/// { -/// expect_compressed:{ -/// value: true -/// } -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// -/// 3. And finally, the *uncompressed* message -/// ``` -/// { -/// expect_compressed:{ -/// value: false -/// } -/// payload:{ -/// body: 45904 bytes of zeros -/// } -/// } -/// ``` -/// -/// 4. Client half-closes -/// -/// Client asserts: -/// - First call fails with `INVALID_ARGUMENT`. -/// - Next calls succeeds. -/// - Response aggregated payload size is 73086. -class ClientCompressedStreaming: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - // Does the server support this test? To find out we need to send an uncompressed probe. However - // we need to disable compression at the RPC level as we don't have access to whether the - // compression byte is set on messages. As such the corresponding code in the service - // implementation checks against the 'grpc-encoding' header as a best guess. Disabling - // compression here will stop that header from being sent. - let probe = client.streamingInputCall() - let probeRequest: Grpc_Testing_StreamingInputCallRequest = .with { request in - request.expectCompressed = .init(true) - request.payload = .zeros(count: 27182) - } - - // Compression is disabled at the RPC level. - probe.sendMessage(probeRequest, promise: nil) - probe.sendEnd(promise: nil) - - // We *expect* invalid argument here. If not then the server doesn't support this test. - try waitAndAssertEqual(probe.status.map { $0.code }, .invalidArgument) - - // Now for the actual test. - - // The first message is identical to the probe message, we'll reuse that. - // The second should not be compressed. - let secondMessage: Grpc_Testing_StreamingInputCallRequest = .with { request in - request.expectCompressed = .init(false) - request.payload = .zeros(count: 45904) - } - - let options = - CallOptions( - messageEncoding: .enabled( - .init( - forRequests: .gzip, - decompressionLimit: .ratio(10) - ) - ) - ) - let streaming = client.streamingInputCall(callOptions: options) - streaming.sendMessage(probeRequest, compression: .enabled, promise: nil) - streaming.sendMessage(secondMessage, compression: .disabled, promise: nil) - streaming.sendEnd(promise: nil) - - try waitAndAssertEqual(streaming.response.map { $0.aggregatedPayloadSize }, 73086) - try waitAndAssertEqual(streaming.status.map { $0.code }, .ok) - } -} - -/// This test verifies that server-only streaming succeeds. -/// -/// Server features: -/// - StreamingOutputCall -/// -/// Procedure: -/// 1. Client calls StreamingOutputCall with StreamingOutputCallRequest: -/// ``` -/// { -/// response_parameters:{ -/// size: 31415 -/// } -/// response_parameters:{ -/// size: 9 -/// } -/// response_parameters:{ -/// size: 2653 -/// } -/// response_parameters:{ -/// size: 58979 -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - exactly four responses -/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 -/// - clients are free to assert that the response payload body contents are zero and -/// comparing the entire response messages against golden responses -class ServerStreaming: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let responseSizes = [31415, 9, 2653, 58979] - let request = Grpc_Testing_StreamingOutputCallRequest.with { request in - request.responseParameters = responseSizes.map { .size($0) } - } - - var payloads: [Grpc_Testing_Payload] = [] - let call = client.streamingOutputCall(request) { response in - payloads.append(response.payload) - } - - // Wait for the status first to ensure we've finished collecting responses. - try waitAndAssertEqual(call.status.map { $0.code }, .ok) - try assertEqual(payloads, responseSizes.map { .zeros(count: $0) }) - } -} - -/// This test verifies that the server can compress streaming messages and disable compression on -/// individual messages, expecting the server's response to be compressed or not according to the -/// `response_compressed` boolean. -/// -/// Whether compression was actually performed is determined by the compression bit in the -/// response's message flags. *Note that some languages may not have access to the message flags, in -/// which case the client will be unable to verify that the `response_compressed` boolean is obeyed -/// by the server*. -/// -/// Server features: -/// - StreamingOutputCall -/// - CompressedResponse -/// -/// Procedure: -/// 1. Client calls StreamingOutputCall with `StreamingOutputCallRequest`: -/// ``` -/// { -/// response_parameters:{ -/// compressed: { -/// value: true -/// } -/// size: 31415 -/// } -/// response_parameters:{ -/// compressed: { -/// value: false -/// } -/// size: 92653 -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - exactly two responses -/// - if supported by the implementation, when `response_compressed` is false, the response's -/// messages MUST NOT have the compressed message flag set. -/// - if supported by the implementation, when `response_compressed` is true, the response's -/// messages MUST have the compressed message flag set. -/// - response payload bodies are sized (in order): 31415, 92653 -/// - clients are free to assert that the response payload body contents are zero and comparing the -/// entire response messages against golden responses -class ServerCompressedStreaming: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let request: Grpc_Testing_StreamingOutputCallRequest = .with { request in - request.responseParameters = [ - .with { - $0.compressed = .init(true) - $0.size = 31415 - }, - .with { - $0.compressed = .init(false) - $0.size = 92653 - }, - ] - } - - let options = - CallOptions( - messageEncoding: .enabled( - .responsesOnly( - decompressionLimit: .absolute( - 1024 * 1024 - ) - ) - ) - ) - var payloads: [Grpc_Testing_Payload] = [] - let rpc = client.streamingOutputCall(request, callOptions: options) { response in - payloads.append(response.payload) - } - - // We can't verify that the compression bit was set, instead we verify that the encoding header - // was sent by the server. This isn't quite the same since as it can still be set but the - // compression may be not set. - try waitAndAssert(rpc.initialMetadata) { headers in - headers.first(name: "grpc-encoding") != nil - } - - let responseSizes = [31415, 92653] - // Wait for the status first to ensure we've finished collecting responses. - try waitAndAssertEqual(rpc.status.map { $0.code }, .ok) - try assertEqual(payloads, responseSizes.map { .zeros(count: $0) }) - } -} - -/// This test verifies that full duplex bidi is supported. -/// -/// Server features: -/// - FullDuplexCall -/// -/// Procedure: -/// 1. Client calls FullDuplexCall with: -/// ``` -/// { -/// response_parameters:{ -/// size: 31415 -/// } -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// 2. After getting a reply, it sends: -/// ``` -/// { -/// response_parameters:{ -/// size: 9 -/// } -/// payload:{ -/// body: 8 bytes of zeros -/// } -/// } -/// ``` -/// 3. After getting a reply, it sends: -/// ``` -/// { -/// response_parameters:{ -/// size: 2653 -/// } -/// payload:{ -/// body: 1828 bytes of zeros -/// } -/// } -/// ``` -/// 4. After getting a reply, it sends: -/// ``` -/// { -/// response_parameters:{ -/// size: 58979 -/// } -/// payload:{ -/// body: 45904 bytes of zeros -/// } -/// } -/// ``` -/// 5. After getting a reply, client half-closes -/// -/// Client asserts: -/// - call was successful -/// - exactly four responses -/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 -/// - clients are free to assert that the response payload body contents are zero and -/// comparing the entire response messages against golden responses -class PingPong: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let requestSizes = [27182, 8, 1828, 45904] - let responseSizes = [31415, 9, 2653, 58979] - - let responseReceived = DispatchSemaphore(value: 0) - - var payloads: [Grpc_Testing_Payload] = [] - let call = client.fullDuplexCall { response in - payloads.append(response.payload) - responseReceived.signal() - } - - try zip(requestSizes, responseSizes).map { requestSize, responseSize in - Grpc_Testing_StreamingOutputCallRequest.with { request in - request.payload = .zeros(count: requestSize) - request.responseParameters = [.size(responseSize)] - } - }.forEach { request in - call.sendMessage(request, promise: nil) - try assertEqual(responseReceived.wait(timeout: .now() + .seconds(1)), .success) - } - call.sendEnd(promise: nil) - - try waitAndAssertEqual(call.status.map { $0.code }, .ok) - try assertEqual(payloads, responseSizes.map { .zeros(count: $0) }) - } -} - -/// This test verifies that streams support having zero-messages in both directions. -/// -/// Server features: -/// - FullDuplexCall -/// -/// Procedure: -/// 1. Client calls FullDuplexCall and then half-closes -/// -/// Client asserts: -/// - call was successful -/// - exactly zero responses -class EmptyStream: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - var responses: [Grpc_Testing_StreamingOutputCallResponse] = [] - let call = client.fullDuplexCall { response in - responses.append(response) - } - - try call.sendEnd().wait() - - try waitAndAssertEqual(call.status.map { $0.code }, .ok) - try assertEqual(responses, []) - } -} - -/// This test verifies that custom metadata in either binary or ascii format can be sent as -/// initial-metadata by the client and as both initial- and trailing-metadata by the server. -/// -/// Server features: -/// - UnaryCall -/// - FullDuplexCall -/// - Echo Metadata -/// -/// Procedure: -/// 1. The client attaches custom metadata with the following keys and values -/// to a UnaryCall with request: -/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" -/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab -/// ``` -/// { -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// 2. The client attaches custom metadata with the following keys and values -/// to a FullDuplexCall with request: -/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" -/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab -/// ``` -/// { -/// response_parameters:{ -/// size: 314159 -/// } -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// and then half-closes -/// -/// Client asserts: -/// - call was successful -/// - metadata with key "x-grpc-test-echo-initial" and value "test_initial_metadata_value" is -/// received in the initial metadata for calls in Procedure steps 1 and 2. -/// - metadata with key "x-grpc-test-echo-trailing-bin" and value 0xababab is received in the -/// trailing metadata for calls in Procedure steps 1 and 2. -class CustomMetadata: InteroperabilityTest { - let initialMetadataName = "x-grpc-test-echo-initial" - let initialMetadataValue = "test_initial_metadata_value" - - let trailingMetadataName = "x-grpc-test-echo-trailing-bin" - let trailingMetadataValue = Data([0xAB, 0xAB, 0xAB]).base64EncodedString() - - func checkMetadata(call: SpecificClientCall) throws - where SpecificClientCall: ClientCall { - let initialName = call.initialMetadata.map { $0[self.initialMetadataName] } - try waitAndAssertEqual(initialName, [self.initialMetadataValue]) - - let trailingName = call.trailingMetadata.map { $0[self.trailingMetadataName] } - try waitAndAssertEqual(trailingName, [self.trailingMetadataValue]) - - try waitAndAssertEqual(call.status.map { $0.code }, .ok) - } - - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let unaryRequest = Grpc_Testing_SimpleRequest.with { request in - request.responseSize = 314_159 - request.payload = .zeros(count: 217_828) - } - - let customMetadata: HPACKHeaders = [ - self.initialMetadataName: self.initialMetadataValue, - self.trailingMetadataName: self.trailingMetadataValue, - ] - - let callOptions = CallOptions(customMetadata: customMetadata) - - let unaryCall = client.unaryCall(unaryRequest, callOptions: callOptions) - try self.checkMetadata(call: unaryCall) - - let duplexCall = client.fullDuplexCall(callOptions: callOptions) { _ in } - let duplexRequest = Grpc_Testing_StreamingOutputCallRequest.with { request in - request.responseParameters = [.size(314_159)] - request.payload = .zeros(count: 271_828) - } - - duplexCall.sendMessage(duplexRequest, promise: nil) - duplexCall.sendEnd(promise: nil) - - try self.checkMetadata(call: duplexCall) - } -} - -/// This test verifies unary calls succeed in sending messages, and propagate back status code and -/// message sent along with the messages. -/// -/// Server features: -/// - UnaryCall -/// - FullDuplexCall -/// - Echo Status -/// -/// Procedure: -/// 1. Client calls UnaryCall with: -/// ``` -/// { -/// response_status:{ -/// code: 2 -/// message: "test status message" -/// } -/// } -/// ``` -/// 2. Client calls FullDuplexCall with: -/// ``` -/// { -/// response_status:{ -/// code: 2 -/// message: "test status message" -/// } -/// } -/// ``` -/// 3. and then half-closes -/// -/// Client asserts: -/// - received status code is the same as the sent code for both Procedure steps 1 and 2 -/// - received status message is the same as the sent message for both Procedure steps 1 and 2 -class StatusCodeAndMessage: InteroperabilityTest { - let expectedCode = 2 - let expectedMessage = "test status message" - - func checkStatus(call: SpecificClientCall) throws - where SpecificClientCall: ClientCall { - try waitAndAssertEqual(call.status.map { $0.code.rawValue }, self.expectedCode) - try waitAndAssertEqual(call.status.map { $0.message }, self.expectedMessage) - } - - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let echoStatus = Grpc_Testing_EchoStatus( - code: Int32(self.expectedCode), - message: self.expectedMessage - ) - - let unaryCall = client.unaryCall(.withStatus(of: echoStatus)) - try self.checkStatus(call: unaryCall) - - var responses: [Grpc_Testing_StreamingOutputCallResponse] = [] - let duplexCall = client.fullDuplexCall { response in - responses.append(response) - } - - duplexCall.sendMessage(.withStatus(of: echoStatus), promise: nil) - duplexCall.sendEnd(promise: nil) - - try self.checkStatus(call: duplexCall) - try assertEqual(responses, []) - } -} - -/// This test verifies Unicode and whitespace is correctly processed in status message. "\t" is -/// horizontal tab. "\r" is carriage return. "\n" is line feed. -/// -/// Server features: -/// - UnaryCall -/// - Echo Status -/// -/// Procedure: -/// 1. Client calls UnaryCall with: -/// ``` -/// { -/// response_status:{ -/// code: 2 -/// message: "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - received status code is the same as the sent code for Procedure step 1 -/// - received status message is the same as the sent message for Procedure step 1, including all -/// whitespace characters -class SpecialStatusMessage: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let code = 2 - let message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" - - let call = client.unaryCall(.withStatus(of: .init(code: Int32(code), message: message))) - try waitAndAssertEqual(call.status.map { $0.code.rawValue }, code) - try waitAndAssertEqual(call.status.map { $0.message }, message) - } -} - -/// This test verifies that calling an unimplemented RPC method returns the UNIMPLEMENTED status -/// code. -/// -/// Server features: N/A -/// -/// Procedure: -/// 1. Client calls grpc.testing.TestService/UnimplementedCall with an empty request (defined as -/// grpc.testing.Empty): -/// ``` -/// { -/// } -/// ``` -/// -/// Client asserts: -/// - received status code is 12 (UNIMPLEMENTED) -class UnimplementedMethod: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - let call = client.unimplementedCall(Grpc_Testing_Empty()) - try waitAndAssertEqual(call.status.map { $0.code }, .unimplemented) - } -} - -/// This test verifies calling an unimplemented server returns the UNIMPLEMENTED status code. -/// -/// Server features: N/A -/// -/// Procedure: -/// 1. Client calls grpc.testing.UnimplementedService/UnimplementedCall with an empty request -/// (defined as grpc.testing.Empty): -/// ``` -/// { -/// } -/// ``` -/// -/// Client asserts: -/// - received status code is 12 (UNIMPLEMENTED) -class UnimplementedService: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_UnimplementedServiceNIOClient(channel: connection) - let call = client.unimplementedCall(Grpc_Testing_Empty()) - try waitAndAssertEqual(call.status.map { $0.code }, .unimplemented) - } -} - -/// This test verifies that a request can be cancelled after metadata has been sent but before -/// payloads are sent. -/// -/// Server features: -/// - StreamingInputCall -/// -/// Procedure: -/// 1. Client starts StreamingInputCall -/// 2. Client immediately cancels request -/// -/// Client asserts: -/// - Call completed with status CANCELLED -class CancelAfterBegin: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - let call = client.streamingInputCall() - call.cancel(promise: nil) - - try waitAndAssertEqual(call.status.map { $0.code }, .cancelled) - } -} - -/// This test verifies that a request can be cancelled after receiving a message from the server. -/// -/// Server features: -/// - FullDuplexCall -/// -/// Procedure: -/// 1. Client starts FullDuplexCall with -/// ``` -/// { -/// response_parameters:{ -/// size: 31415 -/// } -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// 2. After receiving a response, client cancels request -/// -/// Client asserts: -/// - Call completed with status CANCELLED -class CancelAfterFirstResponse: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - let promise = connection.eventLoop.makePromise(of: Void.self) - - let call = client.fullDuplexCall { _ in - promise.succeed(()) - } - - promise.futureResult.whenSuccess { - call.cancel(promise: nil) - } - - let request = Grpc_Testing_StreamingOutputCallRequest.with { request in - request.responseParameters = [.size(31415)] - request.payload = .zeros(count: 27182) - } - - call.sendMessage(request, promise: nil) - - try waitAndAssertEqual(call.status.map { $0.code }, .cancelled) - } -} - -/// This test verifies that an RPC request whose lifetime exceeds its configured timeout value -/// will end with the DeadlineExceeded status. -/// -/// Server features: -/// - FullDuplexCall -/// -/// Procedure: -/// 1. Client calls FullDuplexCall with the following request and sets its timeout to 1ms -/// ``` -/// { -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// 2. Client waits -/// -/// Client asserts: -/// - Call completed with status DEADLINE_EXCEEDED. -class TimeoutOnSleepingServer: InteroperabilityTest { - func run(using connection: ClientConnection) throws { - let client = Grpc_Testing_TestServiceNIOClient(channel: connection) - - let callOptions = CallOptions(timeLimit: .timeout(.milliseconds(1))) - let call = client.fullDuplexCall(callOptions: callOptions) { _ in } - - try waitAndAssertEqual(call.status.map { $0.code }, .deadlineExceeded) - } -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift deleted file mode 100644 index 40be07038..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestClientConnection.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 GRPC -import NIOCore - -#if canImport(NIOSSL) -import NIOSSL -#endif - -public func makeInteroperabilityTestClientBuilder( - group: EventLoopGroup, - useTLS: Bool -) -> ClientConnection.Builder { - let builder: ClientConnection.Builder - - if useTLS { - #if canImport(NIOSSL) - // The CA certificate has a common name of "*.test.google.fr", use the following host override - // so we can do full certificate verification. - builder = ClientConnection.usingTLSBackedByNIOSSL(on: group) - .withTLS(trustRoots: .certificates([InteroperabilityTestCredentials.caCertificate])) - .withTLS(serverHostnameOverride: "foo.test.google.fr") - #else - fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) - } else { - builder = ClientConnection.insecure(group: group) - } - - return builder -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift deleted file mode 100644 index ddcd24453..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCredentials.swift +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import NIOSSL - -/// Contains credentials used for the gRPC interoperability tests. -/// -/// Tests are described in [interop-test-descriptions.md][1], certificates and private keys can be -/// found in the [gRPC repository][2]. -/// -/// [1]: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md -/// [2]: https://github.com/grpc/grpc/tree/master/src/core/tsi/test_creds -public struct InteroperabilityTestCredentials { - private init() {} - - /// Self signed gRPC interoperability test CA certificate. - public static let caCertificate = try! NIOSSLCertificate( - bytes: .init(caCertificatePem.utf8), - format: .pem - ) - - /// gRPC interoperability test server certificate. - /// - /// Note: the specification refers to the certificate and key as "server1", this name is carried - /// across here. - public static let server1Certificate = try! NIOSSLCertificate( - bytes: .init(server1CertificatePem.utf8), - format: .pem - ) - - /// gRPC interoperability test server private key. - /// - /// Note: the specification refers to the certificate and key as "server1", this name is carried - /// across here. - public static let server1Key = try! NIOSSLPrivateKey( - bytes: .init(server1KeyPem.utf8), - format: .pem - ) - - private static let caCertificatePem = """ - -----BEGIN CERTIFICATE----- - MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV - BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX - aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla - Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 - YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT - BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 - +L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu - g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd - Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV - HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau - sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m - oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG - Dfcog5wrJytaQ6UA0wE= - -----END CERTIFICATE----- - """ - - private static let server1CertificatePem = """ - -----BEGIN CERTIFICATE----- - MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET - MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ - dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx - MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV - BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 - ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco - LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg - zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd - 9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw - CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy - em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G - CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 - hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh - y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 - -----END CERTIFICATE----- - """ - - private static let server1KeyPem = """ - -----BEGIN PRIVATE KEY----- - MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD - M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf - 3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY - AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm - V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY - tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p - dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q - K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR - 81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff - DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd - aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 - ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 - XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe - F98XJ7tIFfJq - -----END PRIVATE KEY----- - """ -} -#endif // canImport(NIOSSL) diff --git a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift b/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift deleted file mode 100644 index 61cc5865c..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestServer.swift +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 GRPC -import Logging -import NIOCore - -#if canImport(NIOSSL) -import NIOSSL -#endif - -/// Makes a server for gRPC interoperability testing. -/// -/// - Parameters: -/// - host: The host to bind the server socket to, defaults to "localhost". -/// - port: The port to bind the server socket to. -/// - eventLoopGroup: Event loop group to run the server on. -/// - serviceProviders: Service providers to handle requests with, defaults to provider for the -/// "Test" service. -/// - useTLS: Whether to use TLS or not. If `true` then the server will use the "server1" -/// certificate and CA as set out in the interoperability test specification. The common name -/// is "*.test.google.fr"; clients should set their hostname override accordingly. -/// - Returns: A future `Server` configured to serve the test service. -public func makeInteroperabilityTestServer( - host: String = "localhost", - port: Int, - eventLoopGroup: EventLoopGroup, - serviceProviders: [CallHandlerProvider] = [TestServiceProvider()], - useTLS: Bool, - logger: Logger? = nil -) throws -> EventLoopFuture { - let builder: Server.Builder - - if useTLS { - #if canImport(NIOSSL) - let caCert = InteroperabilityTestCredentials.caCertificate - let serverCert = InteroperabilityTestCredentials.server1Certificate - let serverKey = InteroperabilityTestCredentials.server1Key - - builder = Server.usingTLSBackedByNIOSSL( - on: eventLoopGroup, - certificateChain: [serverCert], - privateKey: serverKey - ) - .withTLS(trustRoots: .certificates([caCert])) - #else - fatalError("'useTLS: true' passed to \(#function) but NIOSSL is not available") - #endif // canImport(NIOSSL) - } else { - builder = Server.insecure(group: eventLoopGroup) - } - - if let logger = logger { - builder.withLogger(logger) - } - - return - builder - .withMessageCompression(.enabled(.init(decompressionLimit: .absolute(1024 * 1024)))) - .withServiceProviders(serviceProviders) - .bind(host: host, port: port) -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/ServerFeatures.swift b/Sources/GRPCInteroperabilityTestsImplementation/ServerFeatures.swift deleted file mode 100644 index 96a9b347f..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/ServerFeatures.swift +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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. - */ - -/// Server features which may be required for tests. -/// -/// We use this enum to match up tests we can run on the NIO client against the NIO server at -/// run time. -/// -/// These features are listed in the [gRPC interoperability test description -/// specification](https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). -/// -/// Missing features: -/// - compressed response -/// - compressed request -/// - observe `ResponseParameter.interval_us` -/// - echo authenticated username -/// - echo authenticated OAuth scope -/// -/// - Note: This is not a complete set of features, only those used in either the client or server. -public enum ServerFeature { - /// See TestServiceProvider_NIO.emptyCall. - case emptyCall - - /// See TestServiceProvider_NIO.unaryCall. - case unaryCall - - /// See TestServiceProvider_NIO.cacheableUnaryCall. - case cacheableUnaryCall - - /// When the client sets expect_compressed to true, the server expects the client request to be - /// compressed. If it's not, it fails the RPC with INVALID_ARGUMENT. Note that - /// `response_compressed` is present on both SimpleRequest (unary) and StreamingOutputCallRequest - /// (streaming). - case compressedRequest - - /// When the client sets response_compressed to true, the server's response is sent back - /// compressed. Note that response_compressed is present on both SimpleRequest (unary) and - /// StreamingOutputCallRequest (streaming). - case compressedResponse - - /// See TestServiceProvider_NIO.streamingInputCall. - case streamingInputCall - - /// See TestServiceProvider_NIO.streamingOutputCall. - case streamingOutputCall - - /// See TestServiceProvider_NIO.fullDuplexCall. - case fullDuplexCall - - /// When the client sends a `responseStatus` in the request payload, the server closes the stream - /// with the status code and messsage contained within said `responseStatus`. The server will not - /// process any further messages on the stream sent by the client. This can be used by clients to - /// verify correct handling of different status codes and associated status messages end-to-end. - case echoStatus - - /// When the client sends metadata with the key "x-grpc-test-echo-initial" with its request, - /// the server sends back exactly this key and the corresponding value back to the client as - /// part of initial metadata. When the client sends metadata with the key - /// "x-grpc-test-echo-trailing-bin" with its request, the server sends back exactly this key - /// and the corresponding value back to the client as trailing metadata. - case echoMetadata -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift deleted file mode 100644 index 1df7f6f7a..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceAsyncProvider.swift +++ /dev/null @@ -1,218 +0,0 @@ -/* - * 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 Foundation -import GRPC -import GRPCInteroperabilityTestModels -import NIOCore - -/// An async service provider for the gRPC interoperability test suite. -/// -/// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md#server -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public final class TestServiceAsyncProvider: Grpc_Testing_TestServiceAsyncProvider { - public let interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? = nil - - public init() {} - - private static let echoMetadataNotImplemented = GRPCStatus( - code: .unimplemented, - message: "Echoing metadata is not yet supported" - ) - - /// Features that this server implements. - /// - /// Some 'features' are methods, whilst others optionally modify the outcome of those methods. The - /// specification is not explicit about where these modifying features should be implemented (i.e. - /// which methods should support them) and they are not listed in the individual method - /// descriptions. As such implementation of these modifying features within each method is - /// determined by the features required by each test. - public static var implementedFeatures: Set { - return [ - .emptyCall, - .unaryCall, - .streamingOutputCall, - .streamingInputCall, - .fullDuplexCall, - .echoStatus, - .compressedResponse, - .compressedRequest, - ] - } - - /// Server implements `emptyCall` which immediately returns the empty message. - public func emptyCall( - request: Grpc_Testing_Empty, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_Empty { - return Grpc_Testing_Empty() - } - - /// Server implements `unaryCall` which immediately returns a `SimpleResponse` with a payload - /// body of size `SimpleRequest.responseSize` bytes and type as appropriate for the - /// `SimpleRequest.responseType`. - /// - /// If the server does not support the `responseType`, then it should fail the RPC with - /// `INVALID_ARGUMENT`. - public func unaryCall( - request: Grpc_Testing_SimpleRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse { - // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is - // set), so we have to check via the encoding header. Note that it is possible for the header - // to be set and for the message to not be compressed. - if request.expectCompressed.value, !context.request.headers.contains(name: "grpc-encoding") { - throw GRPCStatus( - code: .invalidArgument, - message: "Expected compressed request, but 'grpc-encoding' was missing" - ) - } - - // Should we enable compression? The C++ interoperability client only expects compression if - // explicitly requested; we'll do the same. - try await context.response.compressResponses(request.responseCompressed.value) - - if request.shouldEchoStatus { - let code = GRPCStatus.Code(rawValue: numericCast(request.responseStatus.code)) ?? .unknown - throw GRPCStatus(code: code, message: request.responseStatus.message) - } - - if context.request.headers.shouldEchoMetadata { - throw Self.echoMetadataNotImplemented - } - - if case .UNRECOGNIZED = request.responseType { - throw GRPCStatus(code: .invalidArgument, message: nil) - } - - return Grpc_Testing_SimpleResponse.with { response in - response.payload = Grpc_Testing_Payload.with { payload in - payload.body = Data(repeating: 0, count: numericCast(request.responseSize)) - payload.type = request.responseType - } - } - } - - /// Server gets the default `SimpleRequest` proto as the request. The content of the request is - /// ignored. It returns the `SimpleResponse` proto with the payload set to current timestamp. - /// The timestamp is an integer representing current time with nanosecond resolution. This - /// integer is formated as ASCII decimal in the response. The format is not really important as - /// long as the response payload is different for each request. In addition it adds cache control - /// headers such that the response can be cached by proxies in the response path. Server should - /// be behind a caching proxy for this test to pass. Currently we set the max-age to 60 seconds. - public func cacheableUnaryCall( - request: Grpc_Testing_SimpleRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_SimpleResponse { - throw GRPCStatus( - code: .unimplemented, - message: "'cacheableUnaryCall' requires control of the initial metadata which isn't supported" - ) - } - - /// Server implements `streamingOutputCall` by replying, in order, with one - /// `StreamingOutputCallResponse` for each `ResponseParameter`s in `StreamingOutputCallRequest`. - /// Each `StreamingOutputCallResponse` should have a payload body of size `ResponseParameter.size` - /// bytes, as specified by its respective `ResponseParameter`. After sending all responses, it - /// closes with OK. - public func streamingOutputCall( - request: Grpc_Testing_StreamingOutputCallRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for responseParameter in request.responseParameters { - let response = Grpc_Testing_StreamingOutputCallResponse.with { response in - response.payload = Grpc_Testing_Payload.with { payload in - payload.body = Data(repeating: 0, count: numericCast(responseParameter.size)) - } - } - - // Should we enable compression? The C++ interoperability client only expects compression if - // explicitly requested; we'll do the same. - let compression: Compression = responseParameter.compressed.value ? .enabled : .disabled - try await responseStream.send(response, compression: compression) - } - } - - /// Server implements `streamingInputCall` which upon half close immediately returns a - /// `StreamingInputCallResponse` where `aggregatedPayloadSize` is the sum of all request payload - /// bodies received. - public func streamingInputCall( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Grpc_Testing_StreamingInputCallResponse { - var aggregatePayloadSize = 0 - - for try await request in requestStream { - if request.expectCompressed.value { - guard context.request.headers.contains(name: "grpc-encoding") else { - throw GRPCStatus( - code: .invalidArgument, - message: "Expected compressed request, but 'grpc-encoding' was missing" - ) - } - } - aggregatePayloadSize += request.payload.body.count - } - return Grpc_Testing_StreamingInputCallResponse.with { response in - response.aggregatedPayloadSize = numericCast(aggregatePayloadSize) - } - } - - /// Server implements `fullDuplexCall` by replying, in order, with one - /// `StreamingOutputCallResponse` for each `ResponseParameter`s in each - /// `StreamingOutputCallRequest`. Each `StreamingOutputCallResponse` should have a payload body - /// of size `ResponseParameter.size` bytes, as specified by its respective `ResponseParameter`s. - /// After receiving half close and sending all responses, it closes with OK. - public func fullDuplexCall( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - // We don't have support for this yet so just fail the call. - if context.request.headers.shouldEchoMetadata { - throw Self.echoMetadataNotImplemented - } - - for try await request in requestStream { - if request.shouldEchoStatus { - let code = GRPCStatus.Code(rawValue: numericCast(request.responseStatus.code)) - let status = GRPCStatus(code: code ?? .unknown, message: request.responseStatus.message) - throw status - } else { - for responseParameter in request.responseParameters { - let response = Grpc_Testing_StreamingOutputCallResponse.with { response in - response.payload = .zeros(count: numericCast(responseParameter.size)) - } - try await responseStream.send(response) - } - } - } - } - - /// This is not implemented as it is not described in the specification. - /// - /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md - public func halfDuplexCall( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - throw GRPCStatus( - code: .unimplemented, - message: "'halfDuplexCall' was not described in the specification" - ) - } -} diff --git a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift b/Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift deleted file mode 100644 index e4a11ab48..000000000 --- a/Sources/GRPCInteroperabilityTestsImplementation/TestServiceProvider.swift +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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 GRPC -import GRPCInteroperabilityTestModels -import NIOCore - -import struct Foundation.Data - -/// A service provider for the gRPC interoperability test suite. -/// -/// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md#server -public class TestServiceProvider: Grpc_Testing_TestServiceProvider { - public var interceptors: Grpc_Testing_TestServiceServerInterceptorFactoryProtocol? - - public init() {} - - private static let echoMetadataNotImplemented = GRPCStatus( - code: .unimplemented, - message: "Echoing metadata is not yet supported" - ) - - /// Features that this server implements. - /// - /// Some 'features' are methods, whilst others optionally modify the outcome of those methods. The - /// specification is not explicit about where these modifying features should be implemented (i.e. - /// which methods should support them) and they are not listed in the individual method - /// descriptions. As such implementation of these modifying features within each method is - /// determined by the features required by each test. - public static var implementedFeatures: Set { - return [ - .emptyCall, - .unaryCall, - .streamingOutputCall, - .streamingInputCall, - .fullDuplexCall, - .echoStatus, - .compressedResponse, - .compressedRequest, - ] - } - - /// Server implements `emptyCall` which immediately returns the empty message. - public func emptyCall( - request: Grpc_Testing_Empty, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(Grpc_Testing_Empty()) - } - - /// Server implements `unaryCall` which immediately returns a `SimpleResponse` with a payload - /// body of size `SimpleRequest.responseSize` bytes and type as appropriate for the - /// `SimpleRequest.responseType`. - /// - /// If the server does not support the `responseType`, then it should fail the RPC with - /// `INVALID_ARGUMENT`. - public func unaryCall( - request: Grpc_Testing_SimpleRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is - // set), so we have to check via the encoding header. Note that it is possible for the header - // to be set and for the message to not be compressed. - if request.expectCompressed.value, !context.headers.contains(name: "grpc-encoding") { - let status = GRPCStatus( - code: .invalidArgument, - message: "Expected compressed request, but 'grpc-encoding' was missing" - ) - return context.eventLoop.makeFailedFuture(status) - } - - // Should we enable compression? The C++ interoperability client only expects compression if - // explicitly requested; we'll do the same. - context.compressionEnabled = request.responseCompressed.value - - if request.shouldEchoStatus { - let code = GRPCStatus.Code(rawValue: numericCast(request.responseStatus.code)) ?? .unknown - return context.eventLoop - .makeFailedFuture(GRPCStatus(code: code, message: request.responseStatus.message)) - } - - if context.headers.shouldEchoMetadata { - return context.eventLoop.makeFailedFuture(TestServiceProvider.echoMetadataNotImplemented) - } - - if case .UNRECOGNIZED = request.responseType { - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .invalidArgument, message: nil)) - } - - let response = Grpc_Testing_SimpleResponse.with { response in - response.payload = Grpc_Testing_Payload.with { payload in - payload.body = Data(repeating: 0, count: numericCast(request.responseSize)) - payload.type = request.responseType - } - } - - return context.eventLoop.makeSucceededFuture(response) - } - - /// Server gets the default `SimpleRequest` proto as the request. The content of the request is - /// ignored. It returns the `SimpleResponse` proto with the payload set to current timestamp. - /// The timestamp is an integer representing current time with nanosecond resolution. This - /// integer is formated as ASCII decimal in the response. The format is not really important as - /// long as the response payload is different for each request. In addition it adds cache control - /// headers such that the response can be cached by proxies in the response path. Server should - /// be behind a caching proxy for this test to pass. Currently we set the max-age to 60 seconds. - public func cacheableUnaryCall( - request: Grpc_Testing_SimpleRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - let status = GRPCStatus( - code: .unimplemented, - message: "'cacheableUnaryCall' requires control of the initial metadata which isn't supported" - ) - - return context.eventLoop.makeFailedFuture(status) - } - - /// Server implements `streamingOutputCall` by replying, in order, with one - /// `StreamingOutputCallResponse` for each `ResponseParameter`s in `StreamingOutputCallRequest`. - /// Each `StreamingOutputCallResponse` should have a payload body of size `ResponseParameter.size` - /// bytes, as specified by its respective `ResponseParameter`. After sending all responses, it - /// closes with OK. - public func streamingOutputCall( - request: Grpc_Testing_StreamingOutputCallRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - var responseQueue = context.eventLoop.makeSucceededFuture(()) - - for responseParameter in request.responseParameters { - responseQueue = responseQueue.flatMap { - let response = Grpc_Testing_StreamingOutputCallResponse.with { response in - response.payload = Grpc_Testing_Payload.with { payload in - payload.body = Data(repeating: 0, count: numericCast(responseParameter.size)) - } - } - - // Should we enable compression? The C++ interoperability client only expects compression if - // explicitly requested; we'll do the same. - let compression: Compression = responseParameter.compressed.value ? .enabled : .disabled - return context.sendResponse(response, compression: compression) - } - } - - return responseQueue.map { GRPCStatus.ok } - } - - /// Server implements `streamingInputCall` which upon half close immediately returns a - /// `StreamingInputCallResponse` where `aggregatedPayloadSize` is the sum of all request payload - /// bodies received. - public func streamingInputCall( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - var aggregatePayloadSize = 0 - - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(request): - if request.expectCompressed.value, - !context.headers.contains(name: "grpc-encoding") - { - context.responseStatus = GRPCStatus( - code: .invalidArgument, - message: "Expected compressed request, but 'grpc-encoding' was missing" - ) - context.responsePromise.fail(context.responseStatus) - } else { - aggregatePayloadSize += request.payload.body.count - } - - case .end: - context.responsePromise.succeed( - Grpc_Testing_StreamingInputCallResponse.with { response in - response.aggregatedPayloadSize = numericCast(aggregatePayloadSize) - } - ) - } - }) - } - - /// Server implements `fullDuplexCall` by replying, in order, with one - /// `StreamingOutputCallResponse` for each `ResponseParameter`s in each - /// `StreamingOutputCallRequest`. Each `StreamingOutputCallResponse` should have a payload body - /// of size `ResponseParameter.size` bytes, as specified by its respective `ResponseParameter`s. - /// After receiving half close and sending all responses, it closes with OK. - public func fullDuplexCall( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - // We don't have support for this yet so just fail the call. - if context.headers.shouldEchoMetadata { - return context.eventLoop.makeFailedFuture(TestServiceProvider.echoMetadataNotImplemented) - } - - var sendQueue = context.eventLoop.makeSucceededFuture(()) - - func streamHandler(_ event: StreamEvent) { - switch event { - case let .message(message): - if message.shouldEchoStatus { - let code = GRPCStatus.Code(rawValue: numericCast(message.responseStatus.code)) - let status = GRPCStatus(code: code ?? .unknown, message: message.responseStatus.message) - context.statusPromise.succeed(status) - } else { - for responseParameter in message.responseParameters { - let response = Grpc_Testing_StreamingOutputCallResponse.with { response in - response.payload = .zeros(count: numericCast(responseParameter.size)) - } - - sendQueue = sendQueue.flatMap { - context.sendResponse(response) - } - } - } - - case .end: - sendQueue.map { GRPCStatus.ok }.cascade(to: context.statusPromise) - } - } - - return context.eventLoop.makeSucceededFuture(streamHandler(_:)) - } - - /// This is not implemented as it is not described in the specification. - /// - /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md - public func halfDuplexCall( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - let status = GRPCStatus( - code: .unimplemented, - message: "'halfDuplexCall' was not described in the specification" - ) - - return context.eventLoop.makeFailedFuture(status) - } -} diff --git a/Sources/GRPCPerformanceTests/Benchmark.swift b/Sources/GRPCPerformanceTests/Benchmark.swift deleted file mode 100644 index 47cdedc6c..000000000 --- a/Sources/GRPCPerformanceTests/Benchmark.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -protocol Benchmark: AnyObject { - func setUp() throws - func tearDown() throws - func run() throws -> Int -} - -extension Benchmark { - func runOnce() throws -> Int { - try self.setUp() - let result = try self.run() - try self.tearDown() - return result - } -} diff --git a/Sources/GRPCPerformanceTests/BenchmarkUtils.swift b/Sources/GRPCPerformanceTests/BenchmarkUtils.swift deleted file mode 100644 index b88bc16d6..000000000 --- a/Sources/GRPCPerformanceTests/BenchmarkUtils.swift +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 Dispatch - -/// The results of a benchmark. -struct BenchmarkResults { - /// The description of the benchmark. - var desc: String - - /// The duration of each run of the benchmark in milliseconds. - var milliseconds: [UInt64] -} - -extension BenchmarkResults: CustomStringConvertible { - var description: String { - return "\(self.desc): \(self.milliseconds.map(String.init).joined(separator: ","))" - } -} - -/// Runs the benchmark and prints the duration in milliseconds for each run. -/// -/// - Parameters: -/// - description: A description of the benchmark. -/// - benchmark: The benchmark which should be run. -/// - spec: The specification for the test run. -func measureAndPrint(description: String, benchmark: Benchmark, spec: TestSpec) { - switch spec.action { - case .list: - print(description) - case let .run(filter): - guard filter.shouldRun(description) else { - return - } - #if CACHEGRIND - _ = measure(description, benchmark: benchmark, repeats: 1) - #else - print(measure(description, benchmark: benchmark, repeats: spec.repeats)) - #endif - } -} - -/// Runs the given benchmark multiple times, recording the wall time for each iteration. -/// -/// - Parameters: -/// - description: A description of the benchmark. -/// - benchmark: The benchmark to run. -/// - repeats: the number of times to run the benchmark. -func measure(_ description: String, benchmark: Benchmark, repeats: Int) -> BenchmarkResults { - var milliseconds: [UInt64] = [] - for _ in 0 ..< repeats { - do { - try benchmark.setUp() - - #if !CACHEGRIND - let start = DispatchTime.now().uptimeNanoseconds - #endif - _ = try benchmark.run() - - #if !CACHEGRIND - let end = DispatchTime.now().uptimeNanoseconds - - milliseconds.append((end - start) / 1_000_000) - #endif - } catch { - // If tearDown fails now then there's not a lot we can do! - try? benchmark.tearDown() - return BenchmarkResults(desc: description, milliseconds: []) - } - - do { - try benchmark.tearDown() - } catch { - return BenchmarkResults(desc: description, milliseconds: []) - } - } - - return BenchmarkResults(desc: description, milliseconds: milliseconds) -} diff --git a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift deleted file mode 100644 index 5fdf714db..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 GRPC -import Logging -import NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP2 - -import struct Foundation.Data - -/// Tests the throughput on the client side by firing a unary request through an embedded channel -/// and writing back enough gRPC as HTTP/2 frames to get through the state machine. -/// -/// This only measures the handlers in the child channel. -class EmbeddedClientThroughput: Benchmark { - private let requestCount: Int - private let requestText: String - private let maximumResponseFrameSize: Int - - private var logger: Logger! - private var requestHead: _GRPCRequestHead! - private var request: Echo_EchoRequest! - private var responseDataChunks: [ByteBuffer]! - - init(requests: Int, text: String, maxResponseFrameSize: Int = .max) { - self.requestCount = requests - self.requestText = text - self.maximumResponseFrameSize = maxResponseFrameSize - } - - func setUp() throws { - self.logger = Logger(label: "io.grpc.testing", factory: { _ in SwiftLogNoOpLogHandler() }) - - self.requestHead = _GRPCRequestHead( - method: "POST", - scheme: "http", - path: "/echo.Echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ) - - self.request = .with { - $0.text = self.requestText - } - - let response = Echo_EchoResponse.with { - $0.text = self.requestText - } - - let serializedResponse = try response.serializedData() - var buffer = ByteBufferAllocator().buffer(capacity: serializedResponse.count + 5) - buffer.writeInteger(UInt8(0)) // compression byte - buffer.writeInteger(UInt32(serializedResponse.count)) - buffer.writeContiguousBytes(serializedResponse) - - self.responseDataChunks = [] - while buffer.readableBytes > 0, - let slice = buffer.readSlice( - length: min(maximumResponseFrameSize, buffer.readableBytes) - ) - { - self.responseDataChunks.append(slice) - } - } - - func tearDown() throws {} - - func run() throws -> Int { - var messages = 0 - - for _ in 0 ..< self.requestCount { - let channel = EmbeddedChannel() - - try channel._configureForEmbeddedThroughputTest( - callType: .unary, - logger: self.logger, - requestType: Echo_EchoRequest.self, - responseType: Echo_EchoResponse.self - ).wait() - - // Trigger the request handler. - channel.pipeline.fireChannelActive() - - // Write the request parts. - try channel.writeOutbound(_GRPCClientRequestPart.head(self.requestHead)) - try channel.writeOutbound( - _GRPCClientRequestPart.message(.init(self.request, compressed: false)) - ) - try channel.writeOutbound(_GRPCClientRequestPart.end) - messages += 1 - - // Read out the request frames. - var requestFrames = 0 - while let _ = try channel.readOutbound(as: HTTP2Frame.FramePayload.self) { - requestFrames += 1 - } - // headers, data, empty data (end-stream). If the request is large there may be - // two DATA frames. - precondition( - requestFrames == 3 || requestFrames == 4, - "Expected 3/4 HTTP/2 frames but got \(requestFrames)" - ) - - // Okay, let's build a response. - - // Required headers. - let responseHeaders: HPACKHeaders = [ - ":status": "200", - "content-type": "application/grpc+proto", - ] - - let headerFrame = HTTP2Frame.FramePayload.headers(.init(headers: responseHeaders)) - try channel.writeInbound(headerFrame) - - // The response data. - for chunk in self.responseDataChunks { - let frame = HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(chunk))) - try channel.writeInbound(frame) - } - - // Required trailers. - let responseTrailers: HPACKHeaders = [ - "grpc-status": "0", - "grpc-message": "ok", - ] - let trailersFrame = HTTP2Frame.FramePayload.headers(.init(headers: responseTrailers)) - try channel.writeInbound(trailersFrame) - - // And read them back out. - var responseParts = 0 - while let _ = try channel.readInbound(as: _GRPCClientResponsePart.self) { - responseParts += 1 - } - - precondition(responseParts == 4, "received \(responseParts) response parts") - } - - return messages - } -} diff --git a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift b/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift deleted file mode 100644 index 17f6bf1bc..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/EmbeddedServer.swift +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import Logging -import NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP2 - -final class EmbeddedServerChildChannelBenchmark: Benchmark { - private let text: String - private let providers: [Substring: CallHandlerProvider] - private let logger: Logger - private let mode: Mode - - enum Mode { - case unary(rpcs: Int) - case clientStreaming(rpcs: Int, requestsPerRPC: Int) - case serverStreaming(rpcs: Int, responsesPerRPC: Int) - case bidirectional(rpcs: Int, requestsPerRPC: Int) - - var method: String { - switch self { - case .unary: - return "Get" - case .clientStreaming: - return "Collect" - case .serverStreaming: - return "Expand" - case .bidirectional: - return "Update" - } - } - } - - static func makeHeadersPayload(method: String) -> HTTP2Frame.FramePayload { - return .headers( - .init(headers: [ - ":path": "/echo.Echo/\(method)", - ":method": "POST", - "content-type": "application/grpc", - ]) - ) - } - - private var headersPayload: HTTP2Frame.FramePayload! - private var requestPayload: HTTP2Frame.FramePayload! - private var requestPayloadWithEndStream: HTTP2Frame.FramePayload! - - private func makeChannel() throws -> EmbeddedChannel { - let channel = EmbeddedChannel() - try channel._configureForEmbeddedServerTest( - servicesByName: self.providers, - encoding: .disabled, - normalizeHeaders: true, - logger: self.logger - ).wait() - return channel - } - - init(mode: Mode, text: String) { - self.mode = mode - self.text = text - - let echo = MinimalEchoProvider() - self.providers = [echo.serviceName: echo] - self.logger = Logger(label: "noop") { _ in - SwiftLogNoOpLogHandler() - } - } - - func setUp() throws { - var buffer = ByteBuffer() - let requestText: String - - switch self.mode { - case .unary, .clientStreaming, .bidirectional: - requestText = self.text - case let .serverStreaming(_, responsesPerRPC): - // For server streaming the request is split on spaces. We'll build up a request based on text - // and the number of responses we want. - var text = String() - text.reserveCapacity((self.text.count + 1) * responsesPerRPC) - for _ in 0 ..< responsesPerRPC { - text.append(self.text) - text.append(" ") - } - requestText = text - } - - let serialized = try Echo_EchoRequest.with { $0.text = requestText }.serializedData() - buffer.reserveCapacity(5 + serialized.count) - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(serialized.count)) // length - buffer.writeData(serialized) - - self.requestPayload = .data(.init(data: .byteBuffer(buffer), endStream: false)) - self.requestPayloadWithEndStream = .data(.init(data: .byteBuffer(buffer), endStream: true)) - self.headersPayload = Self.makeHeadersPayload(method: self.mode.method) - } - - func tearDown() throws {} - - func run() throws -> Int { - switch self.mode { - case let .unary(rpcs): - return try self.run(rpcs: rpcs, requestsPerRPC: 1) - case let .clientStreaming(rpcs, requestsPerRPC): - return try self.run(rpcs: rpcs, requestsPerRPC: requestsPerRPC) - case let .serverStreaming(rpcs, _): - return try self.run(rpcs: rpcs, requestsPerRPC: 1) - case let .bidirectional(rpcs, requestsPerRPC): - return try self.run(rpcs: rpcs, requestsPerRPC: requestsPerRPC) - } - } - - func run(rpcs: Int, requestsPerRPC: Int) throws -> Int { - var messages = 0 - for _ in 0 ..< rpcs { - let channel = try self.makeChannel() - try channel.writeInbound(self.headersPayload) - for _ in 0 ..< (requestsPerRPC - 1) { - messages += 1 - try channel.writeInbound(self.requestPayload) - } - messages += 1 - try channel.writeInbound(self.requestPayloadWithEndStream) - - while try channel.readOutbound(as: HTTP2Frame.FramePayload.self) != nil { - () - } - - _ = try channel.finish() - } - - return messages - } -} diff --git a/Sources/GRPCPerformanceTests/Benchmarks/MinimalEchoProvider.swift b/Sources/GRPCPerformanceTests/Benchmarks/MinimalEchoProvider.swift deleted file mode 100644 index 7de34a186..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/MinimalEchoProvider.swift +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 GRPC -import NIOCore - -/// The echo provider that comes with the example does some string processing, we'll avoid some of -/// that here so we're looking at the right things. -public class MinimalEchoProvider: Echo_EchoProvider { - public let interceptors: Echo_EchoServerInterceptorFactoryProtocol? - - public init(interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil) { - self.interceptors = interceptors - } - - public func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(.with { $0.text = request.text }) - } - - public func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - for part in request.text.utf8.split(separator: UInt8(ascii: " ")) { - context.sendResponse(.with { $0.text = String(part)! }, promise: nil) - } - return context.eventLoop.makeSucceededFuture(.ok) - } - - public func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - var parts: [String] = [] - - func onEvent(_ event: StreamEvent) { - switch event { - case let .message(request): - parts.append(request.text) - case .end: - context.responsePromise.succeed(.with { $0.text = parts.joined(separator: " ") }) - } - } - - return context.eventLoop.makeSucceededFuture(onEvent(_:)) - } - - public func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - func onEvent(_ event: StreamEvent) { - switch event { - case let .message(request): - context.sendResponse(.with { $0.text = request.text }, promise: nil) - case .end: - context.statusPromise.succeed(.ok) - } - } - - return context.eventLoop.makeSucceededFuture(onEvent(_:)) - } -} diff --git a/Sources/GRPCPerformanceTests/Benchmarks/PercentEncoding.swift b/Sources/GRPCPerformanceTests/Benchmarks/PercentEncoding.swift deleted file mode 100644 index 37c341434..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/PercentEncoding.swift +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 GRPC -import NIOCore - -class PercentEncoding: Benchmark { - let message: String - let allocator = ByteBufferAllocator() - - let iterations: Int - - init(iterations: Int, requiresEncoding: Bool) { - self.iterations = iterations - if requiresEncoding { - // The message is used in the interop-tests. - self.message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" - } else { - // The message above is 62 bytes long. - self.message = String(repeating: "a", count: 62) - } - } - - func setUp() throws {} - - func tearDown() throws {} - - func run() throws -> Int { - var totalLength = 0 - - for _ in 0 ..< self.iterations { - var buffer = self.allocator.buffer(capacity: 0) - - let marshalled = GRPCStatusMessageMarshaller.marshall(self.message)! - let length = buffer.writeString(marshalled) - let unmarshalled = GRPCStatusMessageMarshaller.unmarshall(buffer.readString(length: length)!) - - totalLength += unmarshalled.count - } - - return totalLength - } -} diff --git a/Sources/GRPCPerformanceTests/Benchmarks/ServerProvidingBenchmark.swift b/Sources/GRPCPerformanceTests/Benchmarks/ServerProvidingBenchmark.swift deleted file mode 100644 index e8754386e..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/ServerProvidingBenchmark.swift +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 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], - useNIOTSIfAvailable: Bool, - useTLS: Bool, - threadCount: Int = 1 - ) { - self.providers = providers - self.useNIOTSIfAvailable = useNIOTSIfAvailable - self.useTLS = useTLS - self.threadCount = threadCount - } - - func setUp() throws { - 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 { - try self.server.close().wait() - try self.group.syncShutdownGracefully() - } - - func run() throws -> Int { - return 0 - } -} diff --git a/Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift b/Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift deleted file mode 100644 index 1fdf58fc0..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 GRPC -import GRPCSampleData -import NIOCore -import NIOPosix - -/// Tests unary throughput by sending requests on a single connection. -/// -/// 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_EchoNIOClient! - - let requestCount: Int - let requestText: 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()], - useNIOTSIfAvailable: useNIOTSIfAvailable, - useTLS: useTLS - ) - } - - override func setUp() throws { - try super.setUp() - - 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) - } - - override func run() throws -> Int { - var messages = 0 - let batchSize = 100 - - for lowerBound in stride(from: 0, to: self.requestCount, by: batchSize) { - let upperBound = min(lowerBound + batchSize, self.requestCount) - - let requests = (lowerBound ..< upperBound).map { _ in - self.client.get(Echo_EchoRequest.with { $0.text = self.requestText }).response - } - - messages += requests.count - try EventLoopFuture.andAllSucceed(requests, on: self.group.next()).wait() - } - - return messages - } - - override func tearDown() throws { - try self.client.channel.close().wait() - try self.group.syncShutdownGracefully() - try super.tearDown() - } -} - -/// Tests bidirectional throughput by sending requests over a single stream. -class Bidi: Unary { - let batchSize: Int - - init(requests: Int, text: String, batchSize: Int, useNIOTSIfAvailable: Bool, useTLS: Bool) { - self.batchSize = batchSize - super.init( - requests: requests, - text: text, - useNIOTSIfAvailable: useNIOTSIfAvailable, - useTLS: useTLS - ) - } - - override func run() throws -> Int { - var messages = 0 - let update = self.client.update { _ in } - - for _ in stride(from: 0, to: self.requestCount, by: self.batchSize) { - let batch = (0 ..< self.batchSize).map { _ in - Echo_EchoRequest.with { $0.text = self.requestText } - } - messages += batch.count - update.sendMessages(batch, promise: nil) - } - update.sendEnd(promise: nil) - - _ = try update.status.wait() - return messages - } -} diff --git a/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift b/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift deleted file mode 120000 index 2dd5aef99..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/echo.grpc.swift +++ /dev/null @@ -1 +0,0 @@ -../../../Examples/v1/Echo/Model/echo.grpc.swift \ No newline at end of file diff --git a/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift b/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift deleted file mode 120000 index 611b0845f..000000000 --- a/Sources/GRPCPerformanceTests/Benchmarks/echo.pb.swift +++ /dev/null @@ -1 +0,0 @@ -../../../Examples/v1/Echo/Model/echo.pb.swift \ No newline at end of file diff --git a/Sources/GRPCPerformanceTests/main.swift b/Sources/GRPCPerformanceTests/main.swift deleted file mode 100644 index 6210245e5..000000000 --- a/Sources/GRPCPerformanceTests/main.swift +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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 ArgumentParser -import GRPC -import Logging - -let smallRequest = String(repeating: "x", count: 8) -let largeRequest = String(repeating: "x", count: 1 << 16) // 65k - -// Add benchmarks here! -func runBenchmarks(spec: TestSpec) { - measureAndPrint( - description: "unary_10k_small_requests", - 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, - 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, - 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, - 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, - useNIOTSIfAvailable: spec.useNIOTransportServices, - useTLS: spec.useTLS - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_client_unary_10k_small_requests", - benchmark: EmbeddedClientThroughput(requests: 10000, text: smallRequest), - spec: spec - ) - - measureAndPrint( - description: "embedded_client_unary_1k_large_requests", - benchmark: EmbeddedClientThroughput(requests: 1000, text: largeRequest), - spec: spec - ) - - measureAndPrint( - description: "embedded_client_unary_1k_large_requests_1k_frames", - benchmark: EmbeddedClientThroughput( - requests: 1000, - text: largeRequest, - maxResponseFrameSize: 1024 - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_server_unary_10k_small_requests", - benchmark: EmbeddedServerChildChannelBenchmark( - mode: .unary(rpcs: 10000), - text: smallRequest - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_server_client_streaming_1_rpc_10k_small_requests", - benchmark: EmbeddedServerChildChannelBenchmark( - mode: .clientStreaming(rpcs: 1, requestsPerRPC: 10000), - text: smallRequest - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_server_client_streaming_10k_rpcs_1_small_requests", - benchmark: EmbeddedServerChildChannelBenchmark( - mode: .clientStreaming(rpcs: 10000, requestsPerRPC: 1), - text: smallRequest - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_server_server_streaming_1_rpc_10k_small_responses", - benchmark: EmbeddedServerChildChannelBenchmark( - mode: .serverStreaming(rpcs: 1, responsesPerRPC: 10000), - text: smallRequest - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_server_server_streaming_10k_rpcs_1_small_response", - benchmark: EmbeddedServerChildChannelBenchmark( - mode: .serverStreaming(rpcs: 10000, responsesPerRPC: 1), - text: smallRequest - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_server_bidi_1_rpc_10k_small_requests", - benchmark: EmbeddedServerChildChannelBenchmark( - mode: .bidirectional(rpcs: 1, requestsPerRPC: 10000), - text: smallRequest - ), - spec: spec - ) - - measureAndPrint( - description: "embedded_server_bidi_10k_rpcs_1_small_request", - benchmark: EmbeddedServerChildChannelBenchmark( - mode: .bidirectional(rpcs: 10000, requestsPerRPC: 1), - text: smallRequest - ), - spec: spec - ) - - measureAndPrint( - description: "percent_encode_decode_10k_status_messages", - benchmark: PercentEncoding(iterations: 10000, requiresEncoding: true), - spec: spec - ) - - measureAndPrint( - description: "percent_encode_decode_10k_ascii_status_messages", - benchmark: PercentEncoding(iterations: 10000, requiresEncoding: false), - spec: spec - ) -} - -struct TestSpec { - var action: Action - var repeats: Int - var useNIOTransportServices: Bool - var useTLS: Bool - - init(action: Action, repeats: Int, useNIOTransportServices: Bool, useTLS: Bool) { - self.action = action - self.repeats = repeats - self.useNIOTransportServices = useNIOTransportServices - self.useTLS = useTLS - } - - enum Action { - /// Run the benchmark with the given filter. - case run(Filter) - /// List all benchmarks. - case list - } - - enum Filter { - /// Run all tests. - case all - /// Run the tests which match the given descriptions. - case some([String]) - - func shouldRun(_ description: String) -> Bool { - switch self { - case .all: - return true - case let .some(selectedTests): - return selectedTests.contains(description) - } - } - } -} - -struct PerformanceTests: ParsableCommand { - @Flag(name: .shortAndLong, help: "List all available tests") - var list: Bool = false - - @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] = [] - - func run() throws { - let spec: TestSpec - - if self.list { - spec = TestSpec( - action: .list, - repeats: self.repeats, - useNIOTransportServices: self.useNIOTransportServices, - useTLS: self.useTLS - ) - } else if self.all { - spec = TestSpec( - action: .run(.all), - repeats: self.repeats, - useNIOTransportServices: self.useNIOTransportServices, - useTLS: self.useTLS - ) - } else { - spec = TestSpec( - action: .run(.some(self.tests)), - repeats: self.repeats, - useNIOTransportServices: self.useNIOTransportServices, - useTLS: self.useTLS - ) - } - - runBenchmarks(spec: spec) - } -} - -assert( - { - print("⚠️ WARNING: YOU ARE RUNNING IN DEBUG MODE ⚠️") - return true - }() -) - -PerformanceTests.main() diff --git a/Sources/GRPCProtobuf/Coding.swift b/Sources/GRPCProtobuf/Coding.swift deleted file mode 100644 index df2e10f45..000000000 --- a/Sources/GRPCProtobuf/Coding.swift +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore -public import SwiftProtobuf - -/// Serializes a Protobuf message into a sequence of bytes. -public struct ProtobufSerializer: GRPCCore.MessageSerializer { - public init() {} - - /// Serializes a `Message` into a sequence of bytes. - /// - /// - Parameter message: The message to serialize. - /// - Returns: An array of serialized bytes representing the message. - public func serialize(_ message: Message) throws -> [UInt8] { - do { - return try message.serializedBytes() - } catch let error { - throw RPCError( - code: .invalidArgument, - message: "Can't serialize message of type \(type(of: message)).", - cause: error - ) - } - } -} - -/// Deserializes a sequence of bytes into a Protobuf message. -public struct ProtobufDeserializer: GRPCCore.MessageDeserializer { - public init() {} - - /// Deserializes a sequence of bytes into a `Message`. - /// - /// - Parameter serializedMessageBytes: The array of bytes to deserialize. - /// - Returns: The deserialized message. - public func deserialize(_ serializedMessageBytes: [UInt8]) throws -> Message { - do { - let message = try Message(serializedBytes: serializedMessageBytes) - return message - } catch let error { - throw RPCError( - code: .invalidArgument, - message: "Can't deserialize to message of type \(Message.self)", - cause: error - ) - } - } -} diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift deleted file mode 100644 index 837cb2ce8..000000000 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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. - */ - -internal import Foundation -internal import SwiftProtobuf -internal import SwiftProtobufPluginLibrary - -internal import struct GRPCCodeGen.CodeGenerationRequest -internal import struct GRPCCodeGen.SourceGenerator - -/// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. -internal struct ProtobufCodeGenParser { - let input: FileDescriptor - let namer: SwiftProtobufNamer - let extraModuleImports: [String] - let protoToModuleMappings: ProtoFileToModuleMappings - let accessLevel: SourceGenerator.Config.AccessLevel - - internal init( - input: FileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings, - extraModuleImports: [String], - accessLevel: SourceGenerator.Config.AccessLevel - ) { - self.input = input - self.extraModuleImports = extraModuleImports - self.protoToModuleMappings = protoFileModuleMappings - self.namer = SwiftProtobufNamer( - currentFile: input, - protoFileToModuleMappings: protoFileModuleMappings - ) - self.accessLevel = accessLevel - } - - internal func parse() throws -> CodeGenerationRequest { - var header = self.input.header - // Ensuring there is a blank line after the header. - if !header.isEmpty && !header.hasSuffix("\n\n") { - header.append("\n") - } - let leadingTrivia = """ - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: \(self.input.name) - // - // For information on using the generated types, please see the documentation: - // https://github.com/grpc/grpc-swift - - """ - let lookupSerializer: (String) -> String = { messageType in - "GRPCProtobuf.ProtobufSerializer<\(messageType)>()" - } - let lookupDeserializer: (String) -> String = { messageType in - "GRPCProtobuf.ProtobufDeserializer<\(messageType)>()" - } - let services = self.input.services.map { - CodeGenerationRequest.ServiceDescriptor( - descriptor: $0, - package: input.package, - protobufNamer: self.namer, - file: self.input - ) - } - - return CodeGenerationRequest( - fileName: self.input.name, - leadingTrivia: header + leadingTrivia, - dependencies: self.codeDependencies, - services: services, - lookupSerializer: lookupSerializer, - lookupDeserializer: lookupDeserializer - ) - } -} - -extension ProtobufCodeGenParser { - fileprivate var codeDependencies: [CodeGenerationRequest.Dependency] { - var codeDependencies: [CodeGenerationRequest.Dependency] = [ - .init(module: "GRPCProtobuf", accessLevel: .internal) - ] - // Adding as dependencies the modules containing generated code or types for - // '.proto' files imported in the '.proto' file we are parsing. - codeDependencies.append( - contentsOf: (self.protoToModuleMappings.neededModules(forFile: self.input) ?? []).map { - CodeGenerationRequest.Dependency(module: $0, accessLevel: self.accessLevel) - } - ) - // Adding extra imports passed in as an option to the plugin. - codeDependencies.append( - contentsOf: self.extraModuleImports.sorted().map { - CodeGenerationRequest.Dependency(module: $0, accessLevel: self.accessLevel) - } - ) - return codeDependencies - } -} - -extension CodeGenerationRequest.ServiceDescriptor { - fileprivate init( - descriptor: ServiceDescriptor, - package: String, - protobufNamer: SwiftProtobufNamer, - file: FileDescriptor - ) { - let methods = descriptor.methods.map { - CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( - descriptor: $0, - protobufNamer: protobufNamer - ) - } - let name = CodeGenerationRequest.Name( - base: descriptor.name, - generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), - generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name) - ) - - // Packages that are based on the path of the '.proto' file usually - // contain dots. For example: "grpc.test". - let namespace = CodeGenerationRequest.Name( - base: package, - generatedUpperCase: protobufNamer.formattedUpperCasePackage(file: file), - generatedLowerCase: protobufNamer.formattedLowerCasePackage(file: file) - ) - let documentation = descriptor.protoSourceComments() - self.init(documentation: documentation, name: name, namespace: namespace, methods: methods) - } -} - -extension CodeGenerationRequest.ServiceDescriptor.MethodDescriptor { - fileprivate init(descriptor: MethodDescriptor, protobufNamer: SwiftProtobufNamer) { - let name = CodeGenerationRequest.Name( - base: descriptor.name, - generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name), - generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name) - ) - let documentation = descriptor.protoSourceComments() - self.init( - documentation: documentation, - name: name, - isInputStreaming: descriptor.clientStreaming, - isOutputStreaming: descriptor.serverStreaming, - inputType: protobufNamer.fullName(message: descriptor.inputType), - outputType: protobufNamer.fullName(message: descriptor.outputType) - ) - } -} - -extension FileDescriptor { - fileprivate var header: String { - var header = String() - // Field number used to collect the syntax field which is usually the first - // declaration in a.proto file. - // See more here: - // https://github.com/apple/swift-protobuf/blob/main/Protos/SwiftProtobuf/google/protobuf/descriptor.proto - let syntaxPath = IndexPath(index: 12) - if let syntaxLocation = self.sourceCodeInfoLocation(path: syntaxPath) { - header = syntaxLocation.asSourceComment( - commentPrefix: "///", - leadingDetachedPrefix: "//" - ) - } - return header - } -} - -extension SwiftProtobufNamer { - internal func formattedUpperCasePackage(file: FileDescriptor) -> String { - let unformattedPackage = self.typePrefix(forFile: file) - return unformattedPackage.trimTrailingUnderscores() - } - - internal func formattedLowerCasePackage(file: FileDescriptor) -> String { - let upperCasePackage = self.formattedUpperCasePackage(file: file) - let lowerCaseComponents = upperCasePackage.split(separator: "_").map { component in - NamingUtils.toLowerCamelCase(String(component)) - } - return lowerCaseComponents.joined(separator: "_") - } -} - -extension String { - internal func trimTrailingUnderscores() -> String { - if let index = self.lastIndex(where: { $0 != "_" }) { - return String(self[...index]) - } else { - return "" - } - } -} diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift deleted file mode 100644 index ad888319d..000000000 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - */ - -public import GRPCCodeGen -public import SwiftProtobufPluginLibrary - -public struct ProtobufCodeGenerator { - internal var configuration: SourceGenerator.Config - - public init( - configuration: SourceGenerator.Config - ) { - self.configuration = configuration - } - - public func generateCode( - from fileDescriptor: FileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings, - extraModuleImports: [String] - ) throws -> String { - let parser = ProtobufCodeGenParser( - input: fileDescriptor, - protoFileModuleMappings: protoFileModuleMappings, - extraModuleImports: extraModuleImports, - accessLevel: self.configuration.accessLevel - ) - let sourceGenerator = SourceGenerator(config: self.configuration) - - let codeGenerationRequest = try parser.parse() - let sourceFile = try sourceGenerator.generate(codeGenerationRequest) - return sourceFile.contents - } -} diff --git a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md b/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md deleted file mode 100644 index d43f80a7d..000000000 --- a/Sources/GRPCReflectionService/Documentation.docc/ReflectionServiceTutorial.md +++ /dev/null @@ -1,227 +0,0 @@ -# Reflection service - -This tutorial goes through the steps of adding Reflection service to a -server, running it and testing it using gRPCurl. - - The server used in this example is implemented at - [Examples/v1/ReflectionService/ReflectionServer.swift][reflection-server] - and it supports the "Greeter", "Echo", and "Reflection" services. - - -## Overview - -The Reflection service provides information about the public RPCs served by a server. -It is specific to services defined using the Protocol Buffers IDL. -By calling the Reflection service, clients can construct and send requests to services -without needing to generate code and types for them. - -You can also use CLI clients such as [gRPCurl][grpcurl-setup] and the [gRPC command line tool][grpc-cli] to: -- list services, -- describe services and their methods, -- describe symbols, -- describe extensions, -- construct and invoke RPCs. - -gRPC Swift supports both [v1][v1] and [v1alpha][v1alpha] of the reflection service. - -## Adding the Reflection service to a server - -You can use the Reflection service by adding it as a provider when constructing your server. - -To initialise the Reflection service we will use -``GRPCReflectionService/ReflectionService/init(reflectionDataFileURLs:version:)``. -It receives the URLs of the files containing the reflection data of the proto files -describing the services of the server and the version of the reflection service. - -### Generating the reflection data - -The server from this example uses the `GreeterProvider` and the `EchoProvider`, -besides the `ReflectionService`. - -The associated proto files are located at `Examples/v1/HelloWorld/Model/helloworld.proto`, and -`Examples/v1/Echo/Model/echo.proto` respectively. - -In order to generate the reflection data for the `helloworld.proto`, you can run the following command: - -```sh -$ protoc Examples/v1/HelloWorld/Model/helloworld.proto \ - --proto_path=Examples/v1/HelloWorld/Model \ - --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ - --grpc-swift_out=Examples/v1/ReflectionService/Generated -``` - -Let's break the command down: -- The first argument passed to `protoc` is the path - to the `.proto` file to generate reflection data - for: [`Examples/v1/HelloWorld/Model/helloworld.proto`][helloworld-proto]. -- The `proto_path` flag is the path to search for imports: `Examples/v1/HelloWorld/Model`. -- The 'grpc-swift_opt' flag allows us to list options for the Swift generator. - To generate only the reflection data set: `Client=false,Server=false,ReflectionData=true`. -- The `grpc-swift_out` flag is used to set the path of the directory - where the generated file will be located: `Examples/v1/ReflectionService/Generated`. - -This command assumes that the `protoc-gen-grpc-swift` plugin is in your `$PATH` environment variable. -You can learn how to get the plugin from this section of the `grpc-swift` README: -https://github.com/grpc/grpc-swift#getting-the-protoc-plugins. - -The command for generating the reflection data for the `Echo` service is similar. - -You can use Swift Package Manager [resources][swiftpm-resources] to add the generated reflection data to your target. -In our example the reflection data is written into the "Generated" directory within the target -so we include the `.copy("Generated")` rule in our target's resource list. - -### Instantiating the Reflection service - -To instantiate the `ReflectionService` you need to pass the URLs of the files containing -the generated reflection data and the version to use, in our case `.v1`. - -Depending on the version of [gRPCurl][grpcurl] you are using you might need to use the `.v1alpha` instead. -Beginning with [gRPCurl v1.8.8][grpcurl-v188] it uses the [v1][v1] reflection. Earlier versions use [v1alpha][v1alpha] -reflection. - -```swift -// Getting the URLs of the files containing the reflection data. -guard - let greeterURL = Bundle.module.url( - forResource: "helloworld", - withExtension: "grpc.reflection", - subdirectory: "Generated" - ), - let echoURL = Bundle.module.url( - forResource: "echo", - withExtension: "grpc.reflection", - subdirectory: "Generated" - ) -else { - print("The resource could not be loaded.") - throw ExitCode.failure -} -let reflectionService = try ReflectionService( - reflectionDataFileURLs: [greeterURL, echoURL], - version: .v1 -) -``` - -### Swift Package Manager Plugin - -Reflection data can also be generated via the SPM plugin by including `"reflectionData": true` in `grpc-swift-config.json`. This will generate the same reflection data as running `protoc` above. The generated reflection files are added to your module Bundle and can be accessed at runtime. More about [spm-plugin][spm-plugin] can be found here. - -```json -{ - "invocations": [ - { - "protoFiles": [ - "helloworld.proto" - ], - "visibility": "public", - "server": true, - "reflectionData": true - } - ] -} -``` - -To instantiate the `ReflectionService` you can search for files with the extension `reflection` in your module Bundle. - -```swift -let reflectionDataFilePaths = Bundle.module.paths( - forResourcesOfType: "reflection", - inDirectory: nil -) -let reflectionService = try ReflectionService( - reflectionDataFilePaths: reflectionDataFilePaths, - version: .v1Alpha -) -``` - -### Running the server - -In our example the server isn't configured with TLS and listens on localhost port 1234. -The following code configures and starts the server: - -```swift -let server = try await Server.insecure(group: group) - .withServiceProviders([reflectionService, GreeterProvider(), EchoProvider()]) - .bind(host: "localhost", port: self.port) - .get() - -``` - -To run the server, from the root of the package run: - -```sh -$ swift run ReflectionServer -``` - -## Calling the Reflection service with gRPCurl - -Please follow the instructions from the [gRPCurl README][grpcurl-setup] to set up gRPCurl. - -From a different terminal than the one used for running the server, we will call gRPCurl commands, -following the format: `grpcurl [flags] [address] [list|describe] [symbol]`. - -We use the `-plaintext` flag, because the server isn't configured with TLS, and -the address is set to `localhost:1234`. - - -To see the available services use `list`: - -```sh -$ grpcurl -plaintext localhost:1234 list -echo.Echo -helloworld.Greeter -``` - -To see what methods are available for a service: - -```sh -$ grpcurl -plaintext localhost:1234 list echo.Echo -echo.Echo.Collect -echo.Echo.Expand -echo.Echo.Get -echo.Echo.Update -``` - -You can also get descriptions of objects like services, methods, and messages. The following -command fetches a description of the Echo service: - -```sh -$ grpcurl -plaintext localhost:1234 describe echo.Echo -echo.Echo is a service: -service Echo { - // Collects a stream of messages and returns them concatenated when the caller closes. - rpc Collect ( stream .echo.EchoRequest ) returns ( .echo.EchoResponse ); - // Splits a request into words and returns each word in a stream of messages. - rpc Expand ( .echo.EchoRequest ) returns ( stream .echo.EchoResponse ); - // Immediately returns an echo of a request. - rpc Get ( .echo.EchoRequest ) returns ( .echo.EchoResponse ); - // Streams back messages as they are received in an input stream. - rpc Update ( stream .echo.EchoRequest ) returns ( stream .echo.EchoResponse ); -} -``` - -You can send requests to the services with gRPCurl: - -```sh -$ grpcurl -d '{ "text": "test" }' -plaintext localhost:1234 echo.Echo.Get -{ - "text": "Swift echo get: test" -} -``` - -Note that when specifying a service, a method or a symbol, we have to use the fully qualified names: -- service: \.\ -- method: \.\.\ -- type: \.\ - -[grpcurl-setup]: https://github.com/fullstorydev/grpcurl#grpcurl -[grpcurl]: https://github.com/fullstorydev/grpcurl -[grpc-cli]: https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md -[v1]: ../v1/reflection-v1.proto -[v1alpha]: ../v1Alpha/reflection-v1alpha.proto -[reflection-server]: ../../Examples/v1/ReflectionService/ReflectionServer.swift -[helloworld-proto]: ../../Examples/v1/HelloWorld/Model/helloworld.proto -[echo-proto]: ../../Examples/v1/Echo/Model/echo.proto -[grpcurl-v188]: https://github.com/fullstorydev/grpcurl/releases/tag/v1.8.8 -[swiftpm-resources]: https://github.com/apple/swift-package-manager/blob/main/Documentation/PackageDescription.md#resource -[spm-plugin]: ../../protoc-gen-grpc-swift/Docs.docc/spm-plugin.md diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift deleted file mode 100644 index af9dc2529..000000000 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ /dev/null @@ -1,382 +0,0 @@ -/* - * Copyright 2023, 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 DequeModule -import Foundation -import GRPC -import SwiftProtobuf - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -public final class ReflectionService: CallHandlerProvider, Sendable { - private let provider: Provider - - public var serviceName: Substring { - switch self.provider { - case .v1(let provider): - return provider.serviceName - case .v1Alpha(let provider): - return provider.serviceName - } - } - - /// Creates a `ReflectionService` by loading serialized reflection data created by `protoc-gen-grpc-swift`. - /// - /// You can generate serialized reflection data using the `protoc-gen-grpc-swift` plugin for `protoc` by - /// setting the `ReflectionData` option to `True`. - /// - /// - Parameter fileURLs: The URLs of the files containing serialized reflection data. - /// - Parameter version: The version of the reflection service to create. - /// - /// - Note: Reflection data for well-known-types must be provided if any of your reflection data depends - /// on them. - /// - Throws: When a file can't be read from disk or parsed. - public convenience init(reflectionDataFileURLs fileURLs: [URL], version: Version) throws { - let filePaths: [String] - #if os(Linux) - filePaths = fileURLs.map { $0.path } - #else - if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { - filePaths = fileURLs.map { $0.path() } - } else { - filePaths = fileURLs.map { $0.path } - } - #endif - try self.init(reflectionDataFilePaths: filePaths, version: version) - } - - /// Creates a `ReflectionService` by loading serialized reflection data created by `protoc-gen-grpc-swift`. - /// - /// You can generate serialized reflection data using the `protoc-gen-grpc-swift` plugin for `protoc` by - /// setting the `ReflectionData` option to `True`. The paths provided should be absolute or relative to the - /// current working directory. - /// - /// - Parameter filePaths: The paths to files containing serialized reflection data. - /// - Parameter version: The version of the reflection service to create. - /// - /// - Note: Reflection data for well-known-types must be provided if any of your reflection data depends - /// on them. - /// - Throws: When a file can't be read from disk or parsed. - public init(reflectionDataFilePaths filePaths: [String], version: Version) throws { - let fileDescriptorProtos = try ReflectionService.readSerializedFileDescriptorProtos( - atPaths: filePaths - ) - switch version.wrapped { - case .v1: - self.provider = .v1( - try ReflectionServiceProviderV1(fileDescriptorProtos: fileDescriptorProtos) - ) - case .v1Alpha: - self.provider = .v1Alpha( - try ReflectionServiceProviderV1Alpha(fileDescriptorProtos: fileDescriptorProtos) - ) - } - } - - public init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto], version: Version) throws - { - switch version.wrapped { - case .v1: - self.provider = .v1( - try ReflectionServiceProviderV1(fileDescriptorProtos: fileDescriptorProtos) - ) - case .v1Alpha: - self.provider = .v1Alpha( - try ReflectionServiceProviderV1Alpha(fileDescriptorProtos: fileDescriptorProtos) - ) - } - } - - public func handle( - method name: Substring, - context: GRPC.CallHandlerContext - ) -> GRPC.GRPCServerHandlerProtocol? { - switch self.provider { - case .v1(let reflectionV1Provider): - return reflectionV1Provider.handle(method: name, context: context) - case .v1Alpha(let reflectionV1AlphaProvider): - return reflectionV1AlphaProvider.handle(method: name, context: context) - } - } -} - -internal struct ReflectionServiceData: Sendable { - internal struct FileDescriptorProtoData: Sendable { - internal var serializedFileDescriptorProto: Data - internal var dependencyFileNames: [String] - } - private struct ExtensionDescriptor: Sendable, Hashable { - internal let extendeeTypeName: String - internal let fieldNumber: Int32 - } - - internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData] - internal var serviceNames: [String] - internal var fileNameBySymbol: [String: String] - - // Stores the file names for each extension identified by an ExtensionDescriptor object. - private var fileNameByExtensionDescriptor: [ExtensionDescriptor: String] - // Stores the field numbers for each type that has extensions. - private var fieldNumbersByType: [String: [Int32]] - - internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { - self.serviceNames = [] - self.fileDescriptorDataByFilename = [:] - self.fileNameBySymbol = [:] - self.fileNameByExtensionDescriptor = [:] - self.fieldNumbersByType = [:] - - for fileDescriptorProto in fileDescriptors { - let serializedFileDescriptorProto: Data - do { - serializedFileDescriptorProto = try fileDescriptorProto.serializedData() - } catch { - throw GRPCStatus( - code: .invalidArgument, - message: - "The \(fileDescriptorProto.name) could not be serialized." - ) - } - let protoData = FileDescriptorProtoData( - serializedFileDescriptorProto: serializedFileDescriptorProto, - dependencyFileNames: fileDescriptorProto.dependency - ) - self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData - self.serviceNames.append( - contentsOf: fileDescriptorProto.service.map { fileDescriptorProto.package + "." + $0.name } - ) - // Populating the dictionary. - for qualifiedSybolName in fileDescriptorProto.qualifiedSymbolNames { - let oldValue = self.fileNameBySymbol.updateValue( - fileDescriptorProto.name, - forKey: qualifiedSybolName - ) - if let oldValue = oldValue { - throw GRPCStatus( - code: .alreadyExists, - message: - "The \(qualifiedSybolName) symbol from \(fileDescriptorProto.name) already exists in \(oldValue)." - ) - } - } - - for typeName in fileDescriptorProto.qualifiedMessageTypes { - self.fieldNumbersByType[typeName] = [] - } - - // Populating the dictionary and the one. - for `extension` in fileDescriptorProto.extension { - let typeName = String(`extension`.extendee.drop(while: { $0 == "." })) - let extensionDescriptor = ExtensionDescriptor( - extendeeTypeName: typeName, - fieldNumber: `extension`.number - ) - let oldFileName = self.fileNameByExtensionDescriptor.updateValue( - fileDescriptorProto.name, - forKey: extensionDescriptor - ) - if let oldFileName = oldFileName { - throw GRPCStatus( - code: .alreadyExists, - message: - """ - The extension of the \(extensionDescriptor.extendeeTypeName) type with the field number equal to \ - \(extensionDescriptor.fieldNumber) from \(fileDescriptorProto.name) already exists in \(oldFileName). - """ - ) - } - self.fieldNumbersByType[typeName, default: []].append(`extension`.number) - } - } - } - - internal func serialisedFileDescriptorProtosForDependenciesOfFile( - named fileName: String - ) -> Result<[Data], GRPCStatus> { - var toVisit = Deque() - var visited = Set() - var serializedFileDescriptorProtos: [Data] = [] - toVisit.append(fileName) - - while let currentFileName = toVisit.popFirst() { - if let protoData = self.fileDescriptorDataByFilename[currentFileName] { - toVisit.append( - contentsOf: protoData.dependencyFileNames - .filter { name in - return !visited.contains(name) - } - ) - - let serializedFileDescriptorProto = protoData.serializedFileDescriptorProto - serializedFileDescriptorProtos.append(serializedFileDescriptorProto) - } else { - let base = "No reflection data for '\(currentFileName)'" - let message: String - if fileName == currentFileName { - message = base + "." - } else { - message = base + " which is a dependency of '\(fileName)'." - } - return .failure(GRPCStatus(code: .notFound, message: message)) - } - visited.insert(currentFileName) - } - return .success(serializedFileDescriptorProtos) - } - - internal func nameOfFileContainingSymbol(named symbolName: String) -> Result { - guard let fileName = self.fileNameBySymbol[symbolName] else { - return .failure( - GRPCStatus( - code: .notFound, - message: "The provided symbol could not be found." - ) - ) - } - return .success(fileName) - } - - internal func nameOfFileContainingExtension( - extendeeName: String, - fieldNumber number: Int32 - ) -> Result { - let key = ExtensionDescriptor(extendeeTypeName: extendeeName, fieldNumber: number) - guard let fileName = self.fileNameByExtensionDescriptor[key] else { - return .failure( - GRPCStatus( - code: .notFound, - message: "The provided extension could not be found." - ) - ) - } - return .success(fileName) - } - - // Returns an empty array if the type has no extensions. - internal func extensionsFieldNumbersOfType( - named typeName: String - ) -> Result<[Int32], GRPCStatus> { - guard let fieldNumbers = self.fieldNumbersByType[typeName] else { - return .failure( - GRPCStatus( - code: .invalidArgument, - message: "The provided type is invalid." - ) - ) - } - return .success(fieldNumbers) - } -} - -extension Google_Protobuf_FileDescriptorProto { - var qualifiedServiceAndMethodNames: [String] { - var names: [String] = [] - - for service in self.service { - names.append(self.package + "." + service.name) - names.append( - contentsOf: service.method - .map { self.package + "." + service.name + "." + $0.name } - ) - } - return names - } - - var qualifiedMessageTypes: [String] { - return self.messageType.map { - self.package + "." + $0.name - } - } - - var qualifiedEnumTypes: [String] { - return self.enumType.map { - self.package + "." + $0.name - } - } - - var qualifiedSymbolNames: [String] { - var names = self.qualifiedServiceAndMethodNames - names.append(contentsOf: self.qualifiedMessageTypes) - names.append(contentsOf: self.qualifiedEnumTypes) - return names - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ReflectionService { - /// The version of the reflection service. - /// - /// Depending in the version you are using, when creating the ReflectionService - /// provide the corresponding `Version` variable (`v1` or `v1Alpha`). - public struct Version: Sendable, Hashable { - internal enum Wrapped { - case v1 - case v1Alpha - } - var wrapped: Wrapped - private init(_ wrapped: Wrapped) { self.wrapped = wrapped } - - /// The v1 version of reflection service: https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1/reflection.proto. - public static var v1: Self { Self(.v1) } - /// The v1alpha version of reflection service: https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto. - public static var v1Alpha: Self { Self(.v1Alpha) } - } - - private enum Provider { - case v1(ReflectionServiceProviderV1) - case v1Alpha(ReflectionServiceProviderV1Alpha) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ReflectionService { - static func readSerializedFileDescriptorProto( - atPath path: String - ) throws -> Google_Protobuf_FileDescriptorProto { - let fileURL: URL - #if os(Linux) - fileURL = URL(fileURLWithPath: path) - #else - if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { - fileURL = URL(filePath: path, directoryHint: .notDirectory) - } else { - fileURL = URL(fileURLWithPath: path) - } - #endif - let binaryData = try Data(contentsOf: fileURL) - guard let serializedData = Data(base64Encoded: binaryData) else { - throw GRPCStatus( - code: .invalidArgument, - message: - """ - The \(path) file contents could not be transformed \ - into serialized data representing a file descriptor proto. - """ - ) - } - return try Google_Protobuf_FileDescriptorProto(serializedBytes: serializedData) - } - - static func readSerializedFileDescriptorProtos( - atPaths paths: [String] - ) throws -> [Google_Protobuf_FileDescriptorProto] { - var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]() - fileDescriptorProtos.reserveCapacity(paths.count) - for path in paths { - try fileDescriptorProtos.append(readSerializedFileDescriptorProto(atPath: path)) - } - return fileDescriptorProtos - } -} diff --git a/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift b/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift deleted file mode 100644 index 703baddc4..000000000 --- a/Sources/GRPCReflectionService/Server/ReflectionServiceV1.swift +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2023, 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 Foundation -import GRPC -import SwiftProtobuf - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final class ReflectionServiceProviderV1: Grpc_Reflection_V1_ServerReflectionAsyncProvider { - private let protoRegistry: ReflectionServiceData - - internal init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto]) throws { - self.protoRegistry = try ReflectionServiceData( - fileDescriptors: fileDescriptorProtos - ) - } - - internal func _findFileByFileName( - _ fileName: String - ) -> Result { - return self.protoRegistry - .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) - .map { fileDescriptorProtos in - Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.fileDescriptorResponse( - .with { - $0.fileDescriptorProto = fileDescriptorProtos - } - ) - } - } - - internal func findFileByFileName( - _ fileName: String, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self._findFileByFileName(fileName) - return result.makeResponse(request: request) - } - - internal func getServicesNames( - request: Grpc_Reflection_V1_ServerReflectionRequest - ) throws -> Grpc_Reflection_V1_ServerReflectionResponse { - var listServicesResponse = Grpc_Reflection_V1_ListServiceResponse() - listServicesResponse.service = self.protoRegistry.serviceNames.map { serviceName in - Grpc_Reflection_V1_ServiceResponse.with { - $0.name = serviceName - } - } - return Grpc_Reflection_V1_ServerReflectionResponse( - request: request, - messageResponse: .listServicesResponse(listServicesResponse) - ) - } - - internal func findFileBySymbol( - _ symbolName: String, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.protoRegistry.nameOfFileContainingSymbol( - named: symbolName - ).flatMap { - self._findFileByFileName($0) - } - return result.makeResponse(request: request) - } - - internal func findFileByExtension( - extensionRequest: Grpc_Reflection_V1_ExtensionRequest, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.protoRegistry.nameOfFileContainingExtension( - extendeeName: extensionRequest.containingType, - fieldNumber: extensionRequest.extensionNumber - ).flatMap { - self._findFileByFileName($0) - } - return result.makeResponse(request: request) - } - - internal func findExtensionsFieldNumbersOfType( - named typeName: String, - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.protoRegistry.extensionsFieldNumbersOfType( - named: typeName - ).map { fieldNumbers in - Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse.allExtensionNumbersResponse( - Grpc_Reflection_V1_ExtensionNumberResponse.with { - $0.baseTypeName = typeName - $0.extensionNumber = fieldNumbers - } - ) - } - return result.makeResponse(request: request) - } - - internal func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await request in requestStream { - switch request.messageRequest { - case let .fileByFilename(fileName): - let response = self.findFileByFileName( - fileName, - request: request - ) - try await responseStream.send(response) - - case .listServices: - let response = try self.getServicesNames(request: request) - try await responseStream.send(response) - - case let .fileContainingSymbol(symbolName): - let response = self.findFileBySymbol( - symbolName, - request: request - ) - try await responseStream.send(response) - - case let .fileContainingExtension(extensionRequest): - let response = self.findFileByExtension( - extensionRequest: extensionRequest, - request: request - ) - try await responseStream.send(response) - - case let .allExtensionNumbersOfType(typeName): - let response = self.findExtensionsFieldNumbersOfType( - named: typeName, - request: request - ) - try await responseStream.send(response) - - default: - let response = Grpc_Reflection_V1_ServerReflectionResponse( - request: request, - messageResponse: .errorResponse( - Grpc_Reflection_V1_ErrorResponse.with { - $0.errorCode = Int32(GRPCStatus.Code.unimplemented.rawValue) - $0.errorMessage = "The request is not implemented." - } - ) - ) - try await responseStream.send(response) - } - } - } -} - -extension Grpc_Reflection_V1_ServerReflectionResponse { - init( - request: Grpc_Reflection_V1_ServerReflectionRequest, - messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse - ) { - self = .with { - $0.validHost = request.host - $0.originalRequest = request - $0.messageResponse = messageResponse - } - } -} - -extension Result { - func recover() -> Result - { - self.flatMapError { status in - let error = Grpc_Reflection_V1_ErrorResponse.with { - $0.errorCode = Int32(status.code.rawValue) - $0.errorMessage = status.message ?? "" - } - return .success(.errorResponse(error)) - } - } - - func makeResponse( - request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Grpc_Reflection_V1_ServerReflectionResponse { - let result = self.recover().attachRequest(request) - return result.get() - } -} - -extension Result -where Success == Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse { - func attachRequest( - _ request: Grpc_Reflection_V1_ServerReflectionRequest - ) -> Result { - self.map { message in - Grpc_Reflection_V1_ServerReflectionResponse(request: request, messageResponse: message) - } - } -} diff --git a/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift b/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift deleted file mode 100644 index 02388edd7..000000000 --- a/Sources/GRPCReflectionService/Server/ReflectionServiceV1Alpha.swift +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2023, 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 Foundation -import GRPC -import SwiftProtobuf - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final class ReflectionServiceProviderV1Alpha: - Grpc_Reflection_V1alpha_ServerReflectionAsyncProvider -{ - private let protoRegistry: ReflectionServiceData - - internal init(fileDescriptorProtos: [Google_Protobuf_FileDescriptorProto]) throws { - self.protoRegistry = try ReflectionServiceData( - fileDescriptors: fileDescriptorProtos - ) - } - - internal func _findFileByFileName( - _ fileName: String - ) -> Result { - return self.protoRegistry - .serialisedFileDescriptorProtosForDependenciesOfFile(named: fileName) - .map { fileDescriptorProtos in - Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse - .fileDescriptorResponse( - .with { - $0.fileDescriptorProto = fileDescriptorProtos - } - ) - } - } - - internal func findFileByFileName( - _ fileName: String, - request: Grpc_Reflection_V1alpha_ServerReflectionRequest - ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { - let result = self._findFileByFileName(fileName) - return result.makeResponse(request: request) - } - - internal func getServicesNames( - request: Grpc_Reflection_V1alpha_ServerReflectionRequest - ) throws -> Grpc_Reflection_V1alpha_ServerReflectionResponse { - var listServicesResponse = Grpc_Reflection_V1alpha_ListServiceResponse() - listServicesResponse.service = self.protoRegistry.serviceNames.map { serviceName in - Grpc_Reflection_V1alpha_ServiceResponse.with { - $0.name = serviceName - } - } - return Grpc_Reflection_V1alpha_ServerReflectionResponse( - request: request, - messageResponse: .listServicesResponse(listServicesResponse) - ) - } - - internal func findFileBySymbol( - _ symbolName: String, - request: Grpc_Reflection_V1alpha_ServerReflectionRequest - ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { - let result = self.protoRegistry.nameOfFileContainingSymbol( - named: symbolName - ).flatMap { - self._findFileByFileName($0) - } - return result.makeResponse(request: request) - } - - internal func findFileByExtension( - extensionRequest: Grpc_Reflection_V1alpha_ExtensionRequest, - request: Grpc_Reflection_V1alpha_ServerReflectionRequest - ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { - let result = self.protoRegistry.nameOfFileContainingExtension( - extendeeName: extensionRequest.containingType, - fieldNumber: extensionRequest.extensionNumber - ).flatMap { - self._findFileByFileName($0) - } - return result.makeResponse(request: request) - } - - internal func findExtensionsFieldNumbersOfType( - named typeName: String, - request: Grpc_Reflection_V1alpha_ServerReflectionRequest - ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { - let result = self.protoRegistry.extensionsFieldNumbersOfType( - named: typeName - ).map { fieldNumbers in - Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse - .allExtensionNumbersResponse( - Grpc_Reflection_V1alpha_ExtensionNumberResponse.with { - $0.baseTypeName = typeName - $0.extensionNumber = fieldNumbers - } - ) - } - return result.makeResponse(request: request) - } - - internal func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await request in requestStream { - switch request.messageRequest { - case let .fileByFilename(fileName): - let response = self.findFileByFileName( - fileName, - request: request - ) - try await responseStream.send(response) - - case .listServices: - let response = try self.getServicesNames(request: request) - try await responseStream.send(response) - - case let .fileContainingSymbol(symbolName): - let response = self.findFileBySymbol( - symbolName, - request: request - ) - try await responseStream.send(response) - - case let .fileContainingExtension(extensionRequest): - let response = self.findFileByExtension( - extensionRequest: extensionRequest, - request: request - ) - try await responseStream.send(response) - - case let .allExtensionNumbersOfType(typeName): - let response = self.findExtensionsFieldNumbersOfType( - named: typeName, - request: request - ) - try await responseStream.send(response) - - default: - let response = Grpc_Reflection_V1alpha_ServerReflectionResponse( - request: request, - messageResponse: .errorResponse( - Grpc_Reflection_V1alpha_ErrorResponse.with { - $0.errorCode = Int32(GRPCStatus.Code.unimplemented.rawValue) - $0.errorMessage = "The request is not implemented." - } - ) - ) - try await responseStream.send(response) - } - } - } -} - -extension Grpc_Reflection_V1alpha_ServerReflectionResponse { - init( - request: Grpc_Reflection_V1alpha_ServerReflectionRequest, - messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse - ) { - self = .with { - $0.validHost = request.host - $0.originalRequest = request - $0.messageResponse = messageResponse - } - } -} - -extension Result -{ - func recover() -> Result< - Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse, Never - > { - self.flatMapError { status in - let error = Grpc_Reflection_V1alpha_ErrorResponse.with { - $0.errorCode = Int32(status.code.rawValue) - $0.errorMessage = status.message ?? "" - } - return .success(.errorResponse(error)) - } - } - - func makeResponse( - request: Grpc_Reflection_V1alpha_ServerReflectionRequest - ) -> Grpc_Reflection_V1alpha_ServerReflectionResponse { - let result = self.recover().attachRequest(request) - return result.get() - } -} - -extension Result -where Success == Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse { - func attachRequest( - _ request: Grpc_Reflection_V1alpha_ServerReflectionRequest - ) -> Result { - self.map { message in - Grpc_Reflection_V1alpha_ServerReflectionResponse(request: request, messageResponse: message) - } - } -} - -#if compiler(<6.0) -extension Result where Failure == Never { - func get() -> Success { - switch self { - case .success(let success): - return success - case .failure: - fatalError("Unreachable") - } - } -} -#endif diff --git a/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift b/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift deleted file mode 100644 index 0cd46f660..000000000 --- a/Sources/GRPCReflectionService/v1/reflection-v1.grpc.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: reflection.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// To build a server, implement a class that conforms to this protocol. -internal protocol Grpc_Reflection_V1_ServerReflectionProvider: CallHandlerProvider { - var interceptors: Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol? { get } - - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - func serverReflectionInfo(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Grpc_Reflection_V1_ServerReflectionProvider { - internal var serviceName: Substring { - return Grpc_Reflection_V1_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "ServerReflectionInfo": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], - observerFactory: self.serverReflectionInfo(context:) - ) - - default: - return nil - } - } -} - -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Grpc_Reflection_V1_ServerReflectionAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol? { get } - - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Reflection_V1_ServerReflectionAsyncProvider { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Reflection_V1_ServerReflectionServerMetadata.serviceDescriptor - } - - internal var serviceName: Substring { - return Grpc_Reflection_V1_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] - } - - internal var interceptors: Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol? { - return nil - } - - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "ServerReflectionInfo": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], - wrapping: { try await self.serverReflectionInfo(requestStream: $0, responseStream: $1, context: $2) } - ) - - default: - return nil - } - } -} - -internal protocol Grpc_Reflection_V1_ServerReflectionServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'serverReflectionInfo'. - /// Defaults to calling `self.makeInterceptors()`. - func makeServerReflectionInfoInterceptors() -> [ServerInterceptor] -} - -internal enum Grpc_Reflection_V1_ServerReflectionServerMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "ServerReflection", - fullName: "grpc.reflection.v1.ServerReflection", - methods: [ - Grpc_Reflection_V1_ServerReflectionServerMetadata.Methods.serverReflectionInfo, - ] - ) - - internal enum Methods { - internal static let serverReflectionInfo = GRPCMethodDescriptor( - name: "ServerReflectionInfo", - path: "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", - type: GRPCCallType.bidirectionalStreaming - ) - } -} diff --git a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift b/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift deleted file mode 100644 index 0d15778aa..000000000 --- a/Sources/GRPCReflectionService/v1/reflection-v1.pb.swift +++ /dev/null @@ -1,775 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2016 The gRPC Authors -// -// 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. - -// Service exported by server reflection. A more complete description of how -// server reflection works can be found at -// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md -// -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The message sent by the client when calling ServerReflectionInfo method. -public struct Grpc_Reflection_V1_ServerReflectionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var host: String = String() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - public var messageRequest: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest? = nil - - /// Find a proto file by the file name. - public var fileByFilename: String { - get { - if case .fileByFilename(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileByFilename(newValue)} - } - - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - public var fileContainingSymbol: String { - get { - if case .fileContainingSymbol(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileContainingSymbol(newValue)} - } - - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - public var fileContainingExtension: Grpc_Reflection_V1_ExtensionRequest { - get { - if case .fileContainingExtension(let v)? = messageRequest {return v} - return Grpc_Reflection_V1_ExtensionRequest() - } - set {messageRequest = .fileContainingExtension(newValue)} - } - - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - public var allExtensionNumbersOfType: String { - get { - if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .allExtensionNumbersOfType(newValue)} - } - - /// List the full names of registered services. The content will not be - /// checked. - public var listServices: String { - get { - if case .listServices(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .listServices(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - public enum OneOf_MessageRequest: Equatable, Sendable { - /// Find a proto file by the file name. - case fileByFilename(String) - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - case fileContainingSymbol(String) - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - case fileContainingExtension(Grpc_Reflection_V1_ExtensionRequest) - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - case allExtensionNumbersOfType(String) - /// List the full names of registered services. The content will not be - /// checked. - case listServices(String) - - } - - public init() {} -} - -/// The type name and extension number sent by the client when requesting -/// file_containing_extension. -public struct Grpc_Reflection_V1_ExtensionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Fully-qualified type name. The format should be . - public var containingType: String = String() - - public var extensionNumber: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// The message sent by the server to answer ServerReflectionInfo method. -public struct Grpc_Reflection_V1_ServerReflectionResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var validHost: String = String() - - public var originalRequest: Grpc_Reflection_V1_ServerReflectionRequest { - get {return _originalRequest ?? Grpc_Reflection_V1_ServerReflectionRequest()} - set {_originalRequest = newValue} - } - /// Returns true if `originalRequest` has been explicitly set. - public var hasOriginalRequest: Bool {return self._originalRequest != nil} - /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. - public mutating func clearOriginalRequest() {self._originalRequest = nil} - - /// The server sets one of the following fields according to the message_request - /// in the request. - public var messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse? = nil - - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. - /// As the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - public var fileDescriptorResponse: Grpc_Reflection_V1_FileDescriptorResponse { - get { - if case .fileDescriptorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_FileDescriptorResponse() - } - set {messageResponse = .fileDescriptorResponse(newValue)} - } - - /// This message is used to answer all_extension_numbers_of_type requests. - public var allExtensionNumbersResponse: Grpc_Reflection_V1_ExtensionNumberResponse { - get { - if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_ExtensionNumberResponse() - } - set {messageResponse = .allExtensionNumbersResponse(newValue)} - } - - /// This message is used to answer list_services requests. - public var listServicesResponse: Grpc_Reflection_V1_ListServiceResponse { - get { - if case .listServicesResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_ListServiceResponse() - } - set {messageResponse = .listServicesResponse(newValue)} - } - - /// This message is used when an error occurs. - public var errorResponse: Grpc_Reflection_V1_ErrorResponse { - get { - if case .errorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_ErrorResponse() - } - set {messageResponse = .errorResponse(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The server sets one of the following fields according to the message_request - /// in the request. - public enum OneOf_MessageResponse: Equatable, Sendable { - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. - /// As the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - case fileDescriptorResponse(Grpc_Reflection_V1_FileDescriptorResponse) - /// This message is used to answer all_extension_numbers_of_type requests. - case allExtensionNumbersResponse(Grpc_Reflection_V1_ExtensionNumberResponse) - /// This message is used to answer list_services requests. - case listServicesResponse(Grpc_Reflection_V1_ListServiceResponse) - /// This message is used when an error occurs. - case errorResponse(Grpc_Reflection_V1_ErrorResponse) - - } - - public init() {} - - fileprivate var _originalRequest: Grpc_Reflection_V1_ServerReflectionRequest? = nil -} - -/// Serialized FileDescriptorProto messages sent by the server answering -/// a file_by_filename, file_containing_symbol, or file_containing_extension -/// request. -public struct Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Serialized FileDescriptorProto messages. We avoid taking a dependency on - /// descriptor.proto, which uses proto2 only features, by making them opaque - /// bytes instead. - public var fileDescriptorProto: [Data] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A list of extension numbers sent by the server answering -/// all_extension_numbers_of_type request. -public struct Grpc_Reflection_V1_ExtensionNumberResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of the base type, including the package name. The format - /// is . - public var baseTypeName: String = String() - - public var extensionNumber: [Int32] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A list of ServiceResponse sent by the server answering list_services request. -public struct Grpc_Reflection_V1_ListServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The information of each service may be expanded in the future, so we use - /// ServiceResponse message to encapsulate it. - public var service: [Grpc_Reflection_V1_ServiceResponse] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// The information of a single service used by ListServiceResponse to answer -/// list_services request. -public struct Grpc_Reflection_V1_ServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of a registered service, including its package name. The format - /// is . - public var name: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// The error code and error message sent by the server when an error occurs. -public struct Grpc_Reflection_V1_ErrorResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// This field uses the error codes defined in grpc::StatusCode. - public var errorCode: Int32 = 0 - - public var errorMessage: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.reflection.v1" - -extension Grpc_Reflection_V1_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "host"), - 3: .standard(proto: "file_by_filename"), - 4: .standard(proto: "file_containing_symbol"), - 5: .standard(proto: "file_containing_extension"), - 6: .standard(proto: "all_extension_numbers_of_type"), - 7: .standard(proto: "list_services"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() - case 3: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileByFilename(v) - } - }() - case 4: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingSymbol(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1_ExtensionRequest? - var hadOneofValue = false - if let current = self.messageRequest { - hadOneofValue = true - if case .fileContainingExtension(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingExtension(v) - } - }() - case 6: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .allExtensionNumbersOfType(v) - } - }() - case 7: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .listServices(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.host.isEmpty { - try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) - } - switch self.messageRequest { - case .fileByFilename?: try { - guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - }() - case .fileContainingSymbol?: try { - guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - }() - case .fileContainingExtension?: try { - guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .allExtensionNumbersOfType?: try { - guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 6) - }() - case .listServices?: try { - guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest) -> Bool { - if lhs.host != rhs.host {return false} - if lhs.messageRequest != rhs.messageRequest {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "containing_type"), - 2: .standard(proto: "extension_number"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.containingType.isEmpty { - try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) - } - if self.extensionNumber != 0 { - try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_ExtensionRequest, rhs: Grpc_Reflection_V1_ExtensionRequest) -> Bool { - if lhs.containingType != rhs.containingType {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "valid_host"), - 2: .standard(proto: "original_request"), - 4: .standard(proto: "file_descriptor_response"), - 5: .standard(proto: "all_extension_numbers_response"), - 6: .standard(proto: "list_services_response"), - 7: .standard(proto: "error_response"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() - case 4: try { - var v: Grpc_Reflection_V1_FileDescriptorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .fileDescriptorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .fileDescriptorResponse(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1_ExtensionNumberResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .allExtensionNumbersResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .allExtensionNumbersResponse(v) - } - }() - case 6: try { - var v: Grpc_Reflection_V1_ListServiceResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .listServicesResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .listServicesResponse(v) - } - }() - case 7: try { - var v: Grpc_Reflection_V1_ErrorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .errorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .errorResponse(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.validHost.isEmpty { - try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) - } - try { if let v = self._originalRequest { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - switch self.messageResponse { - case .fileDescriptorResponse?: try { - guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .allExtensionNumbersResponse?: try { - guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .listServicesResponse?: try { - guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .errorResponse?: try { - guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse) -> Bool { - if lhs.validHost != rhs.validHost {return false} - if lhs._originalRequest != rhs._originalRequest {return false} - if lhs.messageResponse != rhs.messageResponse {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "file_descriptor_proto"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.fileDescriptorProto.isEmpty { - try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_FileDescriptorResponse, rhs: Grpc_Reflection_V1_FileDescriptorResponse) -> Bool { - if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "base_type_name"), - 2: .standard(proto: "extension_number"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.baseTypeName.isEmpty { - try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) - } - if !self.extensionNumber.isEmpty { - try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_ExtensionNumberResponse, rhs: Grpc_Reflection_V1_ExtensionNumberResponse) -> Bool { - if lhs.baseTypeName != rhs.baseTypeName {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "service"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.service.isEmpty { - try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_ListServiceResponse, rhs: Grpc_Reflection_V1_ListServiceResponse) -> Bool { - if lhs.service != rhs.service {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServiceResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_ServiceResponse, rhs: Grpc_Reflection_V1_ServiceResponse) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ErrorResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "error_code"), - 2: .standard(proto: "error_message"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.errorCode != 0 { - try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) - } - if !self.errorMessage.isEmpty { - try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1_ErrorResponse, rhs: Grpc_Reflection_V1_ErrorResponse) -> Bool { - if lhs.errorCode != rhs.errorCode {return false} - if lhs.errorMessage != rhs.errorMessage {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift deleted file mode 100644 index ed52d5924..000000000 --- a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.grpc.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: reflection.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// To build a server, implement a class that conforms to this protocol. -internal protocol Grpc_Reflection_V1alpha_ServerReflectionProvider: CallHandlerProvider { - var interceptors: Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol? { get } - - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - func serverReflectionInfo(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Grpc_Reflection_V1alpha_ServerReflectionProvider { - internal var serviceName: Substring { - return Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "ServerReflectionInfo": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], - observerFactory: self.serverReflectionInfo(context:) - ) - - default: - return nil - } - } -} - -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Grpc_Reflection_V1alpha_ServerReflectionAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol? { get } - - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - func serverReflectionInfo( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Reflection_V1alpha_ServerReflectionAsyncProvider { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.serviceDescriptor - } - - internal var serviceName: Substring { - return Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.serviceDescriptor.fullName[...] - } - - internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol? { - return nil - } - - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "ServerReflectionInfo": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], - wrapping: { try await self.serverReflectionInfo(requestStream: $0, responseStream: $1, context: $2) } - ) - - default: - return nil - } - } -} - -internal protocol Grpc_Reflection_V1alpha_ServerReflectionServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'serverReflectionInfo'. - /// Defaults to calling `self.makeInterceptors()`. - func makeServerReflectionInfoInterceptors() -> [ServerInterceptor] -} - -internal enum Grpc_Reflection_V1alpha_ServerReflectionServerMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "ServerReflection", - fullName: "grpc.reflection.v1alpha.ServerReflection", - methods: [ - Grpc_Reflection_V1alpha_ServerReflectionServerMetadata.Methods.serverReflectionInfo, - ] - ) - - internal enum Methods { - internal static let serverReflectionInfo = GRPCMethodDescriptor( - name: "ServerReflectionInfo", - path: "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", - type: GRPCCallType.bidirectionalStreaming - ) - } -} diff --git a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift b/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift deleted file mode 100644 index 5560e7d6e..000000000 --- a/Sources/GRPCReflectionService/v1Alpha/reflection-v1alpha.pb.swift +++ /dev/null @@ -1,788 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2016 The gRPC Authors -// -// 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. -// Service exported by server reflection - -// Warning: this entire file is deprecated. Use this instead: -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The message sent by the client when calling ServerReflectionInfo method. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_ServerReflectionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var host: String = String() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - public var messageRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest? = nil - - /// Find a proto file by the file name. - public var fileByFilename: String { - get { - if case .fileByFilename(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileByFilename(newValue)} - } - - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - public var fileContainingSymbol: String { - get { - if case .fileContainingSymbol(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileContainingSymbol(newValue)} - } - - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - public var fileContainingExtension: Grpc_Reflection_V1alpha_ExtensionRequest { - get { - if case .fileContainingExtension(let v)? = messageRequest {return v} - return Grpc_Reflection_V1alpha_ExtensionRequest() - } - set {messageRequest = .fileContainingExtension(newValue)} - } - - /// Finds the tag numbers used by all known extensions of extendee_type, and - /// appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - public var allExtensionNumbersOfType: String { - get { - if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .allExtensionNumbersOfType(newValue)} - } - - /// List the full names of registered services. The content will not be - /// checked. - public var listServices: String { - get { - if case .listServices(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .listServices(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - public enum OneOf_MessageRequest: Equatable, Sendable { - /// Find a proto file by the file name. - case fileByFilename(String) - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - case fileContainingSymbol(String) - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - case fileContainingExtension(Grpc_Reflection_V1alpha_ExtensionRequest) - /// Finds the tag numbers used by all known extensions of extendee_type, and - /// appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - case allExtensionNumbersOfType(String) - /// List the full names of registered services. The content will not be - /// checked. - case listServices(String) - - } - - public init() {} -} - -/// The type name and extension number sent by the client when requesting -/// file_containing_extension. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_ExtensionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Fully-qualified type name. The format should be . - public var containingType: String = String() - - public var extensionNumber: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// The message sent by the server to answer ServerReflectionInfo method. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_ServerReflectionResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var validHost: String = String() - - public var originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest { - get {return _originalRequest ?? Grpc_Reflection_V1alpha_ServerReflectionRequest()} - set {_originalRequest = newValue} - } - /// Returns true if `originalRequest` has been explicitly set. - public var hasOriginalRequest: Bool {return self._originalRequest != nil} - /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. - public mutating func clearOriginalRequest() {self._originalRequest = nil} - - /// The server set one of the following fields according to the message_request - /// in the request. - public var messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse? = nil - - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. As - /// the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - public var fileDescriptorResponse: Grpc_Reflection_V1alpha_FileDescriptorResponse { - get { - if case .fileDescriptorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_FileDescriptorResponse() - } - set {messageResponse = .fileDescriptorResponse(newValue)} - } - - /// This message is used to answer all_extension_numbers_of_type requst. - public var allExtensionNumbersResponse: Grpc_Reflection_V1alpha_ExtensionNumberResponse { - get { - if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_ExtensionNumberResponse() - } - set {messageResponse = .allExtensionNumbersResponse(newValue)} - } - - /// This message is used to answer list_services request. - public var listServicesResponse: Grpc_Reflection_V1alpha_ListServiceResponse { - get { - if case .listServicesResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_ListServiceResponse() - } - set {messageResponse = .listServicesResponse(newValue)} - } - - /// This message is used when an error occurs. - public var errorResponse: Grpc_Reflection_V1alpha_ErrorResponse { - get { - if case .errorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_ErrorResponse() - } - set {messageResponse = .errorResponse(newValue)} - } - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The server set one of the following fields according to the message_request - /// in the request. - public enum OneOf_MessageResponse: Equatable, Sendable { - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. As - /// the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - case fileDescriptorResponse(Grpc_Reflection_V1alpha_FileDescriptorResponse) - /// This message is used to answer all_extension_numbers_of_type requst. - case allExtensionNumbersResponse(Grpc_Reflection_V1alpha_ExtensionNumberResponse) - /// This message is used to answer list_services request. - case listServicesResponse(Grpc_Reflection_V1alpha_ListServiceResponse) - /// This message is used when an error occurs. - case errorResponse(Grpc_Reflection_V1alpha_ErrorResponse) - - } - - public init() {} - - fileprivate var _originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest? = nil -} - -/// Serialized FileDescriptorProto messages sent by the server answering -/// a file_by_filename, file_containing_symbol, or file_containing_extension -/// request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Serialized FileDescriptorProto messages. We avoid taking a dependency on - /// descriptor.proto, which uses proto2 only features, by making them opaque - /// bytes instead. - public var fileDescriptorProto: [Data] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A list of extension numbers sent by the server answering -/// all_extension_numbers_of_type request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_ExtensionNumberResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of the base type, including the package name. The format - /// is . - public var baseTypeName: String = String() - - public var extensionNumber: [Int32] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A list of ServiceResponse sent by the server answering list_services request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_ListServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The information of each service may be expanded in the future, so we use - /// ServiceResponse message to encapsulate it. - public var service: [Grpc_Reflection_V1alpha_ServiceResponse] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// The information of a single service used by ListServiceResponse to answer -/// list_services request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_ServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of a registered service, including its package name. The format - /// is . - public var name: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// The error code and error message sent by the server when an error occurs. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -public struct Grpc_Reflection_V1alpha_ErrorResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// This field uses the error codes defined in grpc::StatusCode. - public var errorCode: Int32 = 0 - - public var errorMessage: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.reflection.v1alpha" - -extension Grpc_Reflection_V1alpha_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "host"), - 3: .standard(proto: "file_by_filename"), - 4: .standard(proto: "file_containing_symbol"), - 5: .standard(proto: "file_containing_extension"), - 6: .standard(proto: "all_extension_numbers_of_type"), - 7: .standard(proto: "list_services"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() - case 3: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileByFilename(v) - } - }() - case 4: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingSymbol(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1alpha_ExtensionRequest? - var hadOneofValue = false - if let current = self.messageRequest { - hadOneofValue = true - if case .fileContainingExtension(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingExtension(v) - } - }() - case 6: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .allExtensionNumbersOfType(v) - } - }() - case 7: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .listServices(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.host.isEmpty { - try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) - } - switch self.messageRequest { - case .fileByFilename?: try { - guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - }() - case .fileContainingSymbol?: try { - guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - }() - case .fileContainingExtension?: try { - guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .allExtensionNumbersOfType?: try { - guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 6) - }() - case .listServices?: try { - guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest) -> Bool { - if lhs.host != rhs.host {return false} - if lhs.messageRequest != rhs.messageRequest {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "containing_type"), - 2: .standard(proto: "extension_number"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.containingType.isEmpty { - try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) - } - if self.extensionNumber != 0 { - try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionRequest, rhs: Grpc_Reflection_V1alpha_ExtensionRequest) -> Bool { - if lhs.containingType != rhs.containingType {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "valid_host"), - 2: .standard(proto: "original_request"), - 4: .standard(proto: "file_descriptor_response"), - 5: .standard(proto: "all_extension_numbers_response"), - 6: .standard(proto: "list_services_response"), - 7: .standard(proto: "error_response"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() - case 4: try { - var v: Grpc_Reflection_V1alpha_FileDescriptorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .fileDescriptorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .fileDescriptorResponse(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1alpha_ExtensionNumberResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .allExtensionNumbersResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .allExtensionNumbersResponse(v) - } - }() - case 6: try { - var v: Grpc_Reflection_V1alpha_ListServiceResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .listServicesResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .listServicesResponse(v) - } - }() - case 7: try { - var v: Grpc_Reflection_V1alpha_ErrorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .errorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .errorResponse(v) - } - }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.validHost.isEmpty { - try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) - } - try { if let v = self._originalRequest { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - switch self.messageResponse { - case .fileDescriptorResponse?: try { - guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .allExtensionNumbersResponse?: try { - guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .listServicesResponse?: try { - guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .errorResponse?: try { - guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Bool { - if lhs.validHost != rhs.validHost {return false} - if lhs._originalRequest != rhs._originalRequest {return false} - if lhs.messageResponse != rhs.messageResponse {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "file_descriptor_proto"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.fileDescriptorProto.isEmpty { - try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_FileDescriptorResponse, rhs: Grpc_Reflection_V1alpha_FileDescriptorResponse) -> Bool { - if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "base_type_name"), - 2: .standard(proto: "extension_number"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.baseTypeName.isEmpty { - try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) - } - if !self.extensionNumber.isEmpty { - try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse, rhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse) -> Bool { - if lhs.baseTypeName != rhs.baseTypeName {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "service"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.service.isEmpty { - try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_ListServiceResponse, rhs: Grpc_Reflection_V1alpha_ListServiceResponse) -> Bool { - if lhs.service != rhs.service {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ServiceResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_ServiceResponse, rhs: Grpc_Reflection_V1alpha_ServiceResponse) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ErrorResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "error_code"), - 2: .standard(proto: "error_message"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.errorCode != 0 { - try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) - } - if !self.errorMessage.isEmpty { - try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Reflection_V1alpha_ErrorResponse, rhs: Grpc_Reflection_V1alpha_ErrorResponse) -> Bool { - if lhs.errorCode != rhs.errorCode {return false} - if lhs.errorMessage != rhs.errorMessage {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift b/Sources/GRPCSampleData/GRPCSwiftCertificate.swift deleted file mode 100644 index 1b626c06e..000000000 --- a/Sources/GRPCSampleData/GRPCSwiftCertificate.swift +++ /dev/null @@ -1,364 +0,0 @@ -/* - * 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. - */ - -//----------------------------------------------------------------------------- -// THIS FILE WAS GENERATED WITH make-sample-certs.py -// -// DO NOT UPDATE MANUALLY -//----------------------------------------------------------------------------- - -#if canImport(NIOSSL) -import struct Foundation.Date -import NIOSSL - -/// Wraps `NIOSSLCertificate` to provide the certificate common name and expiry date. -public struct SampleCertificate { - public var certificate: NIOSSLCertificate - public var commonName: String - public var notAfter: Date - - public static let ca = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), - commonName: "some-ca", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) - - public static let otherCA = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), - commonName: "some-other-ca", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) - - public static let server = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) - - public static let exampleServer = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), - commonName: "example.com", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) - - public static let serverSignedByOtherCA = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) - - public static let client = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) - - public static let clientSignedByOtherCA = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) - - public static let exampleServerWithExplicitCurve = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: 1_753_797_065) - ) -} - -extension SampleCertificate { - /// Returns whether the certificate has expired. - public var isExpired: Bool { - return self.notAfter < Date() - } -} - -/// Provides convenience methods to make `NIOSSLPrivateKey`s for corresponding `GRPCSwiftCertificate`s. -public struct SamplePrivateKey { - private init() {} - - public static let server = try! NIOSSLPrivateKey(bytes: .init(serverKey.utf8), format: .pem) - public static let exampleServer = try! NIOSSLPrivateKey( - bytes: .init(exampleServerKey.utf8), - format: .pem - ) - public static let client = try! NIOSSLPrivateKey(bytes: .init(clientKey.utf8), format: .pem) - public static let exampleServerWithExplicitCurve = try! NIOSSLPrivateKey( - bytes: .init(serverExplicitCurveKey.utf8), - format: .pem - ) -} - -// MARK: - Certificates and private keys - -private let caCert = """ - -----BEGIN CERTIFICATE----- - MIICoDCCAYgCCQCu3t2RYSXASjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdz - b21lLWNhMB4XDTI0MDcyOTEzNTEwNVoXDTI1MDcyOTEzNTEwNVowEjEQMA4GA1UE - AwwHc29tZS1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOtVwFmJ - Znuf0gC8tZSVasYrSbiDiYGUJd701SskU+RbzNZl7paYIBcM2iAy4L6S2w02ehfa - RZoatGoKKhTZnyMu9NAYM1xAGiODfqC0s467udVBU6J2rU8olhm1ChZqfVBxcd9y - AF7VjvN1N3gnGM2klAWFIgqaHoFAqINwHROjycAnr40uXCLNLukkt90AmMtL5Rah - Sh0wOrx0E5OiiqWyWkjePTcMTwiRaYrUepo+EGFdmERDyiJtp5t4pcqdInJ6uA4s - eiev9NEiGdWeJy83lIdo3N777r8cK9VDsHxHGiz72ZKE35MeIEk9weC1ph81KIZV - cUDuO8nRPwWvBDUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAT6hoeq4sJdqkaN+p - QvpF9cZ4DJLw0dFujcWQtYpPCtMVQx14QSXaPGmUG0GLVJ5mUvzV0cwUC58JDXmS - CDQ/vBnfoWQyblFQDZXOP5aDGOTmNIpFn8hutqsSDvMteh8R3zvJZBr+CQtP2Bos - TH3TcnchhKq580hYazFJJ1P4jOqBXIQb3Osnm8WjJpGuDtOP8DW2Q2AdN/8Zl+FQ - OrwiGMwghkZm2O91tYKvr45VxvyIpah36d5IFyAP7xIT4ua7X7ZyaCMjBmlK1QHd - kKUVuyR2bLgpIRpj/KQY/UOdl1zu3MUs9OkG0suPrY3EOa0K7hDkXnHjX2ZipSw7 - TAuG9Q== - -----END CERTIFICATE----- - """ - -private let otherCACert = """ - -----BEGIN CERTIFICATE----- - MIICrDCCAZQCCQDjS9iNRZ49lzANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1z - b21lLW90aGVyLWNhMB4XDTI0MDcyOTEzNTEwNVoXDTI1MDcyOTEzNTEwNVowGDEW - MBQGA1UEAwwNc29tZS1vdGhlci1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC - AQoCggEBAMC3EEHu7uYtDsH0RZEQHQol1oDEOxL+SDH7dbKIv0UpgW5LXLQDDGR9 - FlKQbeNNtMt5mTd4TalqxZz+eMUhfrv0k3f1heEky/Wz53uRFHVNbDLEf/wa6QMd - 99HOBePy2yDWdQC5/R6zLwjM3LuzZ146QMk0b4tx+/hjSkUKUO6GVQEJrO8DTdij - XAco/3jCeM8wofQZQ6ipZ00gxI3BpubPgj60yRW7+aulHPlZmZuv3kDDmVcL+V3c - V0n0GVckV62xMWMnYGNXqAajkK97f+mlo+zZ2exkGV/2Kja2VT+wZKEkO9RfL6XC - 23hG9pjx5OmD1lihlwYve7VFSo56xvUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA - TubObtDxGjUya7GPqrfC9gP2aQ/mBjprZGzzga0ksWQC4jIhq3qOCYVROBNHeqjH - mH3aleRrq9/QE6/fP7D6YruX6WEJ0hzFxf8eoVGYqETiNndlo9485bNVTB3afL2m - +qLKsvOoSvfO4iYrgvteFKycGICSR63EfN2AJFVNfMPATk7DILJo8gnMx/keKcgG - WWQaKHEeN2ufZRTDXz2/YNWx5K/w/L+/MDqZ9tZvWTiD/q+rQ9q7hbbbpCxrNgZF - 3PnNPtu9cTvaDl9p0liudFUc7FoI1PtEzT5hTMxYWoyNoFn9hUaVNreJKvS78nsx - F4VLaY8K8w3ruk8p0Igclg== - -----END CERTIFICATE----- - """ - -private let serverCert = """ - -----BEGIN CERTIFICATE----- - MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMMCWxvY2Fs - aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMJH2M/mJGXZneOE - 5UWbicTg1BxkdNND50p0fO/35CG4jDQ3CekXUuQ6kK6ZJ2idDQTOWJqd/jSB7Ctc - zmZ9KBAfhP9PHMZQaVQSo+tpvX6vC/hw3PCOEne1l8H8O957hBdOhEDg1crAZ33M - cTOtxTSNw7hh0OXzLyOTfq6h3nHyvjuj82fn8nyJ9lARDZ8grdLS5LVE+Je1G3My - kXJKJoYCGQHGDKmj7o1nrwiii20uE0gnjwGEiTO1ngKQGXzL6guuR1bMmE1UIPD7 - IySu8Yg2nI8YB96dVNFaiB7gJg9Nde7a7GHPh+4t0NSqLlBL+k94c2J8lWgN38bZ - ugoknf0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAXmSnx5fjn0Z9GLQYkaXxKUoc - rYPkmzRCocso3GNMWz3kde351UmPpX3tf11638aIKO0xzJ6PZyYowdbCXZs4Co/o - pYyeW2LOoxLwSBF8wFMAPN3FB54c/KfancXGV1ULTlhfpnoZvUPnqJDYoxFRUkIQ - wVtlyA/p5Zfc9U8czer42eo5aj9D9ircBt4k6hx9IY99YvyNeFfMq4TLOgJZkZT7 - 2AImVq4kBvIUVrK86MGyRuNbAWP4fY5OOymT0rEKA6U5Lx+c9PPaFgozbGk4QAMB - ZTwv8ymHAKdcgiDRAoQ2NhkSlySnKi4oEwcKLYPuyrpt1eG2Lx993gdSa4z2eQ== - -----END CERTIFICATE----- - """ - -private let serverSignedByOtherCACert = """ - -----BEGIN CERTIFICATE----- - MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl - ci1jYTAeFw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMM - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMJH2M/m - JGXZneOE5UWbicTg1BxkdNND50p0fO/35CG4jDQ3CekXUuQ6kK6ZJ2idDQTOWJqd - /jSB7CtczmZ9KBAfhP9PHMZQaVQSo+tpvX6vC/hw3PCOEne1l8H8O957hBdOhEDg - 1crAZ33McTOtxTSNw7hh0OXzLyOTfq6h3nHyvjuj82fn8nyJ9lARDZ8grdLS5LVE - +Je1G3MykXJKJoYCGQHGDKmj7o1nrwiii20uE0gnjwGEiTO1ngKQGXzL6guuR1bM - mE1UIPD7IySu8Yg2nI8YB96dVNFaiB7gJg9Nde7a7GHPh+4t0NSqLlBL+k94c2J8 - lWgN38bZugoknf0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOzQ4ZiHOY9mZyE5e - aQPZn7FE93yZrnvZcuRwrv2WI5vQj70wU4oKdm6RuBbntercKgrP6xIf2mNrUSQk - A0XfB70QZYHKD/Uoy/NXn2CwwExXixQNUv8OaytiR2PGDk2hdeqmcTEo18/v2sT0 - 32PpizVqRTfxARtu7gWt2P+n/RaL9Dj8JqB6vxv4rL2HkrDys3lT5UZwH4W81Lfw - hFI7gHRt9CjzpDIP/GFszdvTHLgozMXGKu+1UKWLepn1XEaKyQlS+CNMVGdI8qHn - 2KvU3L4zzB1MgJsTEmz+rdGtc7paBSHpLqp1DbrU+RjXCG+POBsWpRcHGkM8Q82X - e2/YQg== - -----END CERTIFICATE----- - """ - -private let serverKey = """ - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAwkfYz+YkZdmd44TlRZuJxODUHGR000PnSnR87/fkIbiMNDcJ - 6RdS5DqQrpknaJ0NBM5Ymp3+NIHsK1zOZn0oEB+E/08cxlBpVBKj62m9fq8L+HDc - 8I4Sd7WXwfw73nuEF06EQODVysBnfcxxM63FNI3DuGHQ5fMvI5N+rqHecfK+O6Pz - Z+fyfIn2UBENnyCt0tLktUT4l7UbczKRckomhgIZAcYMqaPujWevCKKLbS4TSCeP - AYSJM7WeApAZfMvqC65HVsyYTVQg8PsjJK7xiDacjxgH3p1U0VqIHuAmD0117trs - Yc+H7i3Q1KouUEv6T3hzYnyVaA3fxtm6CiSd/QIDAQABAoIBAA7RuikJjgcy1UdQ - kMiBd73LxIIx63Nd/5t/TTRkvUMRN6iX9iqQe+Mq0HRw/D+Pkzmln76ThJtuuZwJ - JTlOHKs2LEfpOfGqmo4uKdDALRMnuQsHWOMEg0YcVOoYGlz7IPVCKPZl8AjaKkq/ - OHdPrvY2RhKfa3bO2O6mxof9kuEwF90l+CjxAcKd4GGMFE+tUjfCxveA02eDHAgm - dwgUGDKFLzgiOgKeBjh9kdLP181o3b5jHVqaw5ZkekYSS7KdLZr9dl1qbJ7xFhbj - Jnls98aQ3Kn4zF+LJex44Zf5R/9Gfxul9QtGIyNJtsGhsmF9j+9POqRGyFfyiu9x - guJ7sqECgYEA6+IwRW7wfjXzTSukhKzb385g8P+UiIghNHW8OSiVBR2mOhbRtvZd - +qi35WXK5mr4cK2jrrU0v5Ddvs10xlMyPUkxIOrwsBw/OdPKzRfg+uaei8ldI+ue - tYjnL2hoDVZxMUX0cX7Kju6MUWkf6R3J75av51AVVcvWtSSRu4hVqIUCgYEA0tli - M3txGAOfxrhYxmk/vYYB3eE6gVpEZWo1F/3BnJaH7MeLmjpC/aXp5Srs0GwG31Nx - TNO0nFu1ech17XatlZqk0eEkKau+w/wyd+v0xTy6d49SMvL3yY0H9I2O/TGWwZr3 - wO45pZtEML5S6VEIPf1lj20GEiY7oLm2cBd3VRkCgYEAoPCr9MPTzJkszstnLarv - Pg2GsQgApQMUfMGT0f/xZRMstleZcNc5meuBxT+lp3720ZJ3qp0yRz4lPaja8vIS - xiPpJEeIPvCW5vKtXS/crfOp20Bhjz+VAtFMw1jeHbOL+Y18Ue+rbsgt7uHmBtzv - ScwraoyGcgppDSDNWgGUSC0CgYAkpdISvq7ujJq10I7llZ+Vkng6l44ys3zV37rw - u5NuYx+nARv7p4rDSZY41dgpdc1P/dHgl5952drWGwicSJdtPF7PeAFwGMDkka43 - 99QogCCs7UVNQ7vb1V5/nCcxTPA2IHhVmVJ9vVoB2uLQWNxE4glH/5whhXGxwvW5 - z+pW6QKBgQDn1kRJ+Y98UpDWKdG/7NLsSkHL+Nkf8GXu66fl435Pys5U44oTDNcu - jMDtymBg0IE3lng1WbNILV7O9r9OKt1HH4L7eepzKJLP93PbpLneBlHQG9LvJmsD - 3ErhTxSu80oglR1Hy2UjL70cE1nPUUpUr8yciey0G1tbnxvsWIqGtQ== - -----END RSA PRIVATE KEY----- - """ - -private let exampleServerCert = """ - -----BEGIN CERTIFICATE----- - MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBYxFDASBgNVBAMMC2V4YW1w - bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsqJThZkRGF4y - yfnnYQBuV+UCrwfiXoNvkxtEWufNah1mIWt7biM+s181Dfn52Lj8GUsNiMEZ6qrX - xBzNwo55tmsoxqywxUS4G2FA4nrniAs6UD7hywKt1zosBrneAPclLBblFwJQsQhC - DEgpsl/DDt5oHPRb5x1zB8DuB2zQhpvEu/pCX5OUlCLf0X1YxUCDU2yYGABokWSg - adHgZ+kAB+Cbt/zH+zibdUS1IpVtz90BuoftS6Iwed5XxPCe9FCc/P1vkPd9KiZT - OhREB3Ci8XfqPKSv9BRGbdg2C9tkmkgVTKcjhfkULsBdahrCLna8nOtoUXf1LJCC - IMDjjDfUiQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBj9JfAiC1qFkC7+kearHOB - RDGiAFyxT3cQuSOgQPoU0WoBaQ+/YhFp8zxJHlQEcQTmicODItJA1kGj8iGT18uE - Tno1lg7nkkMhoY/Q59yaMKLdfe6aETN2eqh8GJZdUwhOKO3dQBqUQuj25gxVR+1a - 1bcsv3ds8sNXdUNJM12iXzt5lAgwhLWX0SbxuApB+6rcQBKiqAoo3KY9N5tiEbRy - 1VeMkAl/C926+W2nOAQCxSryZWEUX5EL0VARfxBjrH6KzDk876HtrLuDb2LHFNJU - w+3nE69pEtXzMEYAQgv4yMQJZx6CtCjwS+Oxr5A3AJPk3nUSzOXVYTe5rk3hmC5I - -----END CERTIFICATE----- - """ - -private let exampleServerKey = """ - -----BEGIN RSA PRIVATE KEY----- - MIIEpAIBAAKCAQEAsqJThZkRGF4yyfnnYQBuV+UCrwfiXoNvkxtEWufNah1mIWt7 - biM+s181Dfn52Lj8GUsNiMEZ6qrXxBzNwo55tmsoxqywxUS4G2FA4nrniAs6UD7h - ywKt1zosBrneAPclLBblFwJQsQhCDEgpsl/DDt5oHPRb5x1zB8DuB2zQhpvEu/pC - X5OUlCLf0X1YxUCDU2yYGABokWSgadHgZ+kAB+Cbt/zH+zibdUS1IpVtz90Buoft - S6Iwed5XxPCe9FCc/P1vkPd9KiZTOhREB3Ci8XfqPKSv9BRGbdg2C9tkmkgVTKcj - hfkULsBdahrCLna8nOtoUXf1LJCCIMDjjDfUiQIDAQABAoIBAF65NRDi2e3SBZyU - p90IHXr+NS4bQC5eBAw9qUGLKaHbdQzDse/1QIpdMgT3SUVi0kuXQNYDj3qgnUmg - /HrukhvpNvYjHJl+lyHtsDpocd3yFjn3HkRIZ2Z5sl7esJpSc6OtgE1zLNazSlK4 - 8WNk5Eo+JXc1HIaxVw4FgDLvwKOfkjgzr4W0bvHR/FaJ6ChMfsRaZjrIoDHvIuY/ - mV/1jI0t6hOf3VU3NTg/8gwu35vcVNqe24qV6dVk9dJikyE7P+/e2c5VCwqAcrGL - V/Gnf7iaqHcUxDFihWWMFBP+yVAeQ26rrAzLxWSn+qb1fJ8igIJhTfdHJFjQbuoP - UsoFAAECgYEA6i0RowjUjOakJD/USKHOD0Dy7ql8DehMvAurbuxtN0jXPVaR4ebt - 3jjyQkIrllRtAcZnZH2OlzM5mKQvUdhriMPvgZj4fpepYBbNcuXIDYXgfe5j0Na3 - XtVRjBvm2gwC4OY9G3HFubNVjjR0AxZaIfUeqzl+XsX8t+tms8WvbQECgYEAw0gl - nnHTYtuw1p1mmPZFYJ5P3DFaqtkRnBPgq7XVgRCjmU0SYEQ6ogNbGESXQBDrKYIg - IqpaZvuSv6nEy+b8aEuvkTsRqmu+gK1taATnZRhrzjzUeMOAVOn1gK86GcSq5Rmx - Bj+ie5lBj+yxU+wJg0hRGNik/ltYVGKf/DNDf4kCgYEAiz5bQ19Hy7SFG4zctIeJ - 2GYdTa53tmlP32zs9hsdYgcs/SsRuYqwHDguTRm9gzkWTDzmU8mY1O0/rTTLclZG - st8W9i+4asXRj/JfHZfmWawmbZsnvRE/neMoBzC8FyGXQJWG9l+zW5V4JQOpjABp - fdGb9+JK8x21BMOzoOfGRQECgYEAuyvImtAguu001s9wygWpw4yZoMRRUdXSkhVf - T1VueVFYbRQ5G7nptOWgh2cezVIqA9PsNy2ujmxsYHY44PLZVKHOelXyfbTdl/oi - FgQ1QWmh0r/tKn6/3yOLorbQ6mfdIM96JDIT64GeHHPSF0zyZTmIOVdU9VLaG6+Y - BiOge3kCgYAYe0+Dseqoy4KXICkHcdacbULJnm8ZZ0SpjoBhKWSS3gyC7Anx8UoO - lSz/4owNrD/96NnlnxItq0Pi7ZU30TBdP1ZX7RuwQqS8ORO9xOSVrgzZR/PZCa3V - ziqGo+jUjGowA795F7/hgb3fNML5dUpLe+JEEo/OuQH6Jh8puYlYBQ== - -----END RSA PRIVATE KEY----- - """ - -private let clientCert = """ - -----BEGIN CERTIFICATE----- - MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe - Fw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMMCWxvY2Fs - aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqEdoBgLtT1p+jn - xjEXCQCpS6g5EIyHwjpIxC6gX49wACiFqNz67EmkDTX0HIPgk+/4wI5ljP7mYPzh - NAMFU4P8gDpYhKXLQyaNno1VTXxgpINIp2OXrhtLtkT6oO0hXTFVJCnsO9uyi7UR - 0sBZbXBiAlmnPSMaY15UkzJvS49zBEJ7qnKeZyAer7V9dYe8OhtWt7kVD6sVhf3a - 7QlwQCdbg3jowodpM3mvHnU8W6JBJ6p7dtAG3zDFyHY0erzc4bfPKqJEtV6YRVij - 3zRCEjlU6A7c66y8V66eieNOB2FzEvutOwNrnrWfaR8jjafbhdZZIai9/GJd8w60 - rOBQoxkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAqAYuUyEwGoDK2tOXPVHAFBaN - 7D6SlHQBxYDuI5jYfJWBfdw3+Dc/OoBXHtkg2OQIV315+uIYHguhScvL4GBmEjgn - 17zKGciymTPJ3eTcb6IIXJIkJr89YM5tyr7cveEUXRugSdAtX0aCaURRr2H4ycjk - NLaSJyqCb02g9Ny0/5pql/v3gdY1XGF/hDEMwpLb5TxTt3VMtYj4r59Yz/5e/950 - MeINqAokIoLVtnYA+YW/Vj+T/ut9dFiC9E7arAw2z4zZ3uWvDVHTxhPQplUbpfyu - /rwx/GpotyGL1qU/JKOur2Y5Is8lfGkKZ6OJWAOPG+ZqO233+s1tH/SEQkIfIA== - -----END CERTIFICATE----- - """ - -private let clientSignedByOtherCACert = """ - -----BEGIN CERTIFICATE----- - MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl - ci1jYTAeFw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBQxEjAQBgNVBAMM - CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqEdoBg - LtT1p+jnxjEXCQCpS6g5EIyHwjpIxC6gX49wACiFqNz67EmkDTX0HIPgk+/4wI5l - jP7mYPzhNAMFU4P8gDpYhKXLQyaNno1VTXxgpINIp2OXrhtLtkT6oO0hXTFVJCns - O9uyi7UR0sBZbXBiAlmnPSMaY15UkzJvS49zBEJ7qnKeZyAer7V9dYe8OhtWt7kV - D6sVhf3a7QlwQCdbg3jowodpM3mvHnU8W6JBJ6p7dtAG3zDFyHY0erzc4bfPKqJE - tV6YRVij3zRCEjlU6A7c66y8V66eieNOB2FzEvutOwNrnrWfaR8jjafbhdZZIai9 - /GJd8w60rOBQoxkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEADl9prZ95iXY74KpV - Vm5L/whTnfXQ2t1BVYD+nOKYyipAuVu+gTbBgseF7Ly+mEM0ewIgFgGbYZsO82Tz - nCCYZY+ablJkewNOjn3DAsr3kTjIFnC4fpDbYQMw3IHEOWdollRLGv0d5SJNc9z+ - N4pB8y53Uz2nYBUKGc+HEGKRwn0XZL5Vmd+OnT9Ry0wlYh3NYcTxAY8ArtyJq9h+ - ROG4YH3en8e7RIGg1uB/m515Gm+CA4WphjErEiy5VH4YFAYtBWCxO/h2gPOwX+8o - UnpdgUOkzB/YAc7S7OGGngz2IyBf+Rz/JC41uF4+efg8ijoZlWcO4/gB1yLiofBD - /MgUQQ== - -----END CERTIFICATE----- - """ - -private let clientKey = """ - -----BEGIN RSA PRIVATE KEY----- - MIIEogIBAAKCAQEAuoR2gGAu1PWn6OfGMRcJAKlLqDkQjIfCOkjELqBfj3AAKIWo - 3PrsSaQNNfQcg+CT7/jAjmWM/uZg/OE0AwVTg/yAOliEpctDJo2ejVVNfGCkg0in - Y5euG0u2RPqg7SFdMVUkKew727KLtRHSwFltcGICWac9IxpjXlSTMm9Lj3MEQnuq - cp5nIB6vtX11h7w6G1a3uRUPqxWF/drtCXBAJ1uDeOjCh2kzea8edTxbokEnqnt2 - 0AbfMMXIdjR6vNzht88qokS1XphFWKPfNEISOVToDtzrrLxXrp6J404HYXMS+607 - A2uetZ9pHyONp9uF1lkhqL38Yl3zDrSs4FCjGQIDAQABAoIBAFcoiTuqNpg7h1BV - 5o6QBhvyALHGoM4aro+P62UieiVMIDbPZr6E3x/2clnxDdYuftMXuduQ5tdCjrX9 - AtIajhFSUBVzweC74FBGw32mDASAIMBcliP7AFgvBCitub+15JemArU4eCxM/e4K - OyK5Z2Op2RFODkq2DRNKkFJ0IaoRN3fDSPLXg865RMSjDEd2I0gsADdh12Dk8+x+ - 5tpiQGLIfgBgWcqQrTl908sHB00WwlH166sT6k1G+SFRPK60r2fhOpyQelTUC+Zl - IOAtydE2ypsWG5Z3LnNkPwbwJl8m2hoL3M23syMnsxwTKQIblpYd3YdRR/5EozUf - f33p2IECgYEA6z8VBggDNb29CZgWjJUS3N/xuNyN6K1/jew2LyoAYX5zZL1/pTLE - Cm2MvJglY6B0r0/3eF6bBYGpHT9TWj3yzYlV0Q8iAdbz6se7skFrm7XXv50Bmjo8 - epzvVjM/oAvEz1/2bQXvZRTyunNwdyHBd9QCiuAHU8xuq8Qvq5+BNBECgYEAyvjc - sWwZQJiU7alx5ynDB25GRbXu4APTzaz99vaw/V8DNsYw5c0habq3JfaC8Q0bse8Z - G675M3F+gFRPG9TxqwSuYF9bpz1CQtKAT3pRXjjJQM3vfdixQjgBYspJMPKDi+qC - Dzhr8VBE16HxMArMgDKzYP/gmjHnRlcT12udZokCgYAcd+7YYwHYcBS/Y3tfGe9F - cYh0IaS+wrhL+Yj5HjEbm0zlpRUcbc9Rn75HWHY130YfrSK6m2BRQ0au9mnk4thO - TU9oVFd+N4AfKnqpcMdP+aqZUqvN+Tw2bmV8XglWGfaAThGpUe2NowJY0/2JPTmH - gc2o9sGMP5IpET3fnBbrsQKBgHLSCXbM4hQqvMUdf/P3Kf8AIPy6iOFtCNpnLFwS - /di3cQgBYhP90RMQrx7orvZSJgKocZm5h/vUDm3mQ8JI2lWWllaqWxzmiJ9omXFc - jr8wfJkOZpbYiJ4fNJmAOZtY9ZWnGeAmWNnwQKGDWP+GfF1hURxkY9iWtnCSPgU1 - OZuRAoGAOT0RQvvTVwxBU/BFRNJLSCjyJee8bz7+B/TmNui1Afyv+GBgzPeh5Z+L - vUi1MlvdlTdUVb1LmFgmidHgjRCYDEUxVEl3HmNHljCJqAXcJA61bMfItoteCHr6 - RMrN29F8q/ZPKbTgT5eH6tBX2meUqEDotTbdgVT84IhWyWOVF+g= - -----END RSA PRIVATE KEY----- - """ - -private let serverExplicitCurveCert = """ - -----BEGIN CERTIFICATE----- - MIICEDCCAbYCCQCV4KgFB2WjmjAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt - cGxlLmNvbTAeFw0yNDA3MjkxMzUxMDVaFw0yNTA3MjkxMzUxMDVaMBYxFDASBgNV - BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B - AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB - AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT - sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl - Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo - N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABDJg - pBr9ZhidkGWnjW+hvhPLTUH9V4iNr+WNsb2HjQK4NloOauRQ4mlc534XeBya5tRy - aczylZHH6uC7ULCA8XcwCgYIKoZIzj0EAwIDSAAwRQIgVqWCUtszDMJU5ropnKDh - UhsHq8r0ARIfTsjSKSdung8CIQChqts3cpW/OOp5PS2bEm23Bf7SWksW2kRvXj6E - pjFODQ== - -----END CERTIFICATE----- - """ - -private let serverExplicitCurveKey = """ - -----BEGIN EC PRIVATE KEY----- - MIIBaAIBAQQgYvOsKzMIHYIhfoUF1YqrM64ZR0Aotb++nOzoDB5mPrqggfowgfcC - AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// - MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr - vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE - axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W - K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 - YyVRAgEBoUQDQgAEMmCkGv1mGJ2QZaeNb6G+E8tNQf1XiI2v5Y2xvYeNArg2Wg5q - 5FDiaVznfhd4HJrm1HJpzPKVkcfq4LtQsIDxdw== - -----END EC PRIVATE KEY----- - """ - -#endif // canImport(NIOSSL) diff --git a/Sources/GRPCSampleData/bundle.p12 b/Sources/GRPCSampleData/bundle.p12 deleted file mode 100644 index 2d9708523eb19b1330f974a686ac1cc540e19f90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2405 zcmY+Ec{CJ?9>>kfSd$TkOZH*Pj3r%>u^amqV_zyG*_X&xW5&)PCU-El(njV^Ms^j7 z;Zg=!8ZHXAga~oz^1A1|_wIXt{Lb&3&-a|~f1fXg1PcHHKo}BC6$(?&z-8FU?2_v_Pgq`l!!)U_7^7qav45$<%XUl$btE7@8UhnB|a!=Ia%aV;lXQX-M2Lc^>43V);jrMj>U`=D>%ccZ9#(QDi>6SGV8j-s%GTxq5?ZE~S+(w5c( z?mn+Eo|labMhLNf>LH|Ch84>RSduoXwDBj+I|9rm+53u~Qj-Ub^znod;lb?8L$xyX z*u)h6yaCjr&vVg}m#q}fN%PD)-iUhTJFx1;?Nj~5(HWmbkJk889x|(kgrc&JUeZ(R zgekr#?k}W%oe95P!Rx)xY*!LOK43pggui0i#a;Iq6 ztsceUG_dUW*H3ZZ%bH?#({Iri9gA;rAoZ?nV@u3U`AV02c^JtuHys7`H z%)t9y0r_{0oZwrw%9V@UQ7CR1)tXg+9XAjI&>YY(1r-a&?Yh-EnNH% zbA?yb%u%ZfU8?HrT2bwj8zQoveEJWH1}26py764rKt9qMwI0Ul>_}nO;LX{3ioYOI zv7dz*eosO}=L-a*V-Oe;#Q1-NttLUVz$A#q53K&nI7`7P~KR_QB;8V$g87CObtJGgVgBt~|?Mf>&b|REe()*5*-#2bR8-;ICoG3A=NV zVZW(ig1?;!2>vQ3>0Bzj!M%!4>q{+MJG0rV)CX*b9VkxBr4Jg|4TYaOYxkx+o~~~y zb@fsJn)iQkj7J6Jg#(n| z8^DXF^p%yfzw|evnujN`5sMRTZ=+D(3nZSQ?wK%lXn&enB+q`Flc>eadnUU85`v|L z^n)ZQV;y^?t>uEe4{Y%`UMosx+?@9OXZn#vs`RHFo^0vtFloQ?uF?Rx824xxmt*@Z!7w$UQZ8e@pB?eFy@>4daDbSEAjpzK_pj4+AI zQ+c6+V$F$Xf)ZFL&w)J8Yx7;Xxey0hjBI3xcuuTsi!29gJy(2qF^jLsQ6|T22AF&O zhGv)tgK>G~z=N9RWW7y0zV`F}o95W}_Jl8m2t1TNZ2d@Ye&-~kBJJKNBNS9yFIcVM zv@iPj$(saFpOTaC&Ub5i26b7F<9bcqkBHW!Z1qTz)Oeqcs#--}e$|;{LE!tSRue5E zJGz^3Ft_@&YiuD7_O|;dovjJE_1tsV`72~x)^8?OVz_SQjL9||qB!*C8OWc<3k(L6 z6dpC6oB=jU_2{_@D#oso^RoR9z5nt7CL27SsF+>4b2B0{bNPYcI#}}^$=rdw@B09> zd_(F*ueLh-&XiTS2Bc6YEF7nuEcm6bW#hEiMY&=D>}VfSYuB$KYp7w)Zu0~feB7qx z7-7W%8pn2DJo52VHSl!?c5o)Y=$5Z!sPX`Mf*RB8czfe48>r9L|-I$>Ea$! zZYHJuG%#XgzzeBdbr@S{mcuNFCmT|jBMy}d6_%&w7UH6R1+$t> zT*z19w@%wrY#WIR`h@H~Pz#J8AS4)6$Wk$>ty;B^_Cn%@-GDA$```u&9 znl?rIy_aqFKGj>i^y`&$yQUU1*`~BpXI^9fX2vkEU^+~s#H5xs;Nm*k8Fe*&+9*&A ze&v$Z{3S6o;^pmmUX^eB`utM<$5t)~Qm!8!(9Ean&$|^pljzc)olYZq$n77X-I5;S zu2@$qbVT*ODmbD8^17rhW@Ej}k*n*8jNjwxM!JmZu=$~WinD|Y*TG7o*h!L{DlU}} zw<+|2DsIF(0S#I65d}!=C-&xQ3{I7%4bl2I`o`$0ZMFqJtuUC0X1F$m7x~J4Gri#2 z7`T1o6MQKKKKWA9>Gv0{r#qth&-go1QyHW9-*tqEYn}kZ zjqJ5&^XZ7CtTLCRWrcu^X)3%OqluBnz@QM6FbIf%@POgFPj2a+7^Aug(`+mKuIq_k VdC8=K`J87h)PpYZeEfSQ{{`#XT4?|P diff --git a/Sources/InteroperabilityTests/AssertionFailure.swift b/Sources/InteroperabilityTests/AssertionFailure.swift deleted file mode 100644 index 112a36ee3..000000000 --- a/Sources/InteroperabilityTests/AssertionFailure.swift +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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. - */ - -/// Failure assertion for interoperability testing. -/// -/// This is required because the tests must be able to run without XCTest. -public struct AssertionFailure: Error { - public var message: String - public var file: String - public var line: Int - - public init(message: String, file: String = #fileID, line: Int = #line) { - self.message = message - self.file = file - self.line = line - } -} - -/// Asserts that the value of an expression is `true`. -public func assertTrue( - _ expression: @autoclosure () throws -> Bool, - _ message: String = "The statement is not true.", - file: String = #fileID, - line: Int = #line -) throws { - guard try expression() else { - throw AssertionFailure(message: message, file: file, line: line) - } -} - -/// Asserts that the two given values are equal. -public func assertEqual( - _ value1: T, - _ value2: T, - file: String = #fileID, - line: Int = #line -) throws { - return try assertTrue( - value1 == value2, - "'\(value1)' is not equal to '\(value2)'", - file: file, - line: line - ) -} diff --git a/Sources/InteroperabilityTests/Generated/empty.pb.swift b/Sources/InteroperabilityTests/Generated/empty.pb.swift deleted file mode 100644 index 7e246bf7b..000000000 --- a/Sources/InteroperabilityTests/Generated/empty.pb.swift +++ /dev/null @@ -1,75 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/empty.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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. - -public import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// An empty message that you can re-use to avoid defining duplicated empty -/// messages in your project. A typical example is to use it as argument or the -/// return value of a service API. For instance: -/// -/// service Foo { -/// rpc Bar (grpc.testing.Empty) returns (grpc.testing.Empty) { }; -/// }; -public struct Grpc_Testing_Empty: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_Empty: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Empty" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - public mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - public func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Empty, rhs: Grpc_Testing_Empty) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift b/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift deleted file mode 100644 index ede7a37ea..000000000 --- a/Sources/InteroperabilityTests/Generated/empty_service.grpc.swift +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2018 gRPC authors. -// -// 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. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/empty_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -public import GRPCCore -internal import GRPCProtobuf - -public enum Grpc_Testing_EmptyService { - public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_EmptyService - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Grpc_Testing_EmptyServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Grpc_Testing_EmptyServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = Grpc_Testing_EmptyServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = Grpc_Testing_EmptyServiceClient -} - -extension GRPCCore.ServiceDescriptor { - public static let grpc_testing_EmptyService = Self( - package: "grpc.testing", - service: "EmptyService" - ) -} - -/// A service that has zero methods. -/// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_EmptyServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_EmptyService.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) {} -} - -/// A service that has zero methods. -/// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_EmptyServiceServiceProtocol: Grpc_Testing_EmptyService.StreamingServiceProtocol {} - -/// Partial conformance to `Grpc_Testing_EmptyServiceStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_EmptyService.ServiceProtocol { -} - -/// A service that has zero methods. -/// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_EmptyServiceClientProtocol: Sendable {} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_EmptyService.ClientProtocol { -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_EmptyService.ClientProtocol { -} - -/// A service that has zero methods. -/// See https://github.com/grpc/grpc/issues/15574 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct Grpc_Testing_EmptyServiceClient: Grpc_Testing_EmptyService.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } -} \ No newline at end of file diff --git a/Sources/InteroperabilityTests/Generated/empty_service.pb.swift b/Sources/InteroperabilityTests/Generated/empty_service.pb.swift deleted file mode 100644 index 81eecc29e..000000000 --- a/Sources/InteroperabilityTests/Generated/empty_service.pb.swift +++ /dev/null @@ -1,25 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/empty_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2018 gRPC authors. -// -// 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. - -// This file contained no messages, enums, or extensions. diff --git a/Sources/InteroperabilityTests/Generated/messages.pb.swift b/Sources/InteroperabilityTests/Generated/messages.pb.swift deleted file mode 100644 index cd0a3dd15..000000000 --- a/Sources/InteroperabilityTests/Generated/messages.pb.swift +++ /dev/null @@ -1,929 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/messages.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -public import Foundation -public import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The type of payload that should be returned. -public enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum, Swift.CaseIterable { - public typealias RawValue = Int - - /// Compressable text format. - case compressable // = 0 - case UNRECOGNIZED(Int) - - public init() { - self = .compressable - } - - public init?(rawValue: Int) { - switch rawValue { - case 0: self = .compressable - default: self = .UNRECOGNIZED(rawValue) - } - } - - public var rawValue: Int { - switch self { - case .compressable: return 0 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - public static let allCases: [Grpc_Testing_PayloadType] = [ - .compressable, - ] - -} - -/// TODO(dgq): Go back to using well-known types once -/// https://github.com/grpc/grpc/issues/6980 has been fixed. -/// import "google/protobuf/wrappers.proto"; -public struct Grpc_Testing_BoolValue: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The bool value. - public var value: Bool = false - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A block of data, to simply increase gRPC message size. -public struct Grpc_Testing_Payload: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The type of data in body. - public var type: Grpc_Testing_PayloadType = .compressable - - /// Primary contents of payload. - public var body: Data = Data() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// A protobuf representation for grpc status. This is used by test -/// clients to specify a status that the server should attempt to return. -public struct Grpc_Testing_EchoStatus: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var code: Int32 = 0 - - public var message: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Unary request. -public struct Grpc_Testing_SimpleRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, server randomly chooses one from other formats. - public var responseType: Grpc_Testing_PayloadType = .compressable - - /// Desired payload size in the response from the server. - public var responseSize: Int32 = 0 - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether SimpleResponse should include username. - public var fillUsername: Bool = false - - /// Whether SimpleResponse should include OAuth scope. - public var fillOauthScope: Bool = false - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - public var responseCompressed: Grpc_Testing_BoolValue { - get {return _responseCompressed ?? Grpc_Testing_BoolValue()} - set {_responseCompressed = newValue} - } - /// Returns true if `responseCompressed` has been explicitly set. - public var hasResponseCompressed: Bool {return self._responseCompressed != nil} - /// Clears the value of `responseCompressed`. Subsequent reads from it will return its default value. - public mutating func clearResponseCompressed() {self._responseCompressed = nil} - - /// Whether server should return a given status - public var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - public var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - public mutating func clearResponseStatus() {self._responseStatus = nil} - - /// Whether the server should expect this request to be compressed. - public var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - public var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - public mutating func clearExpectCompressed() {self._expectCompressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseCompressed: Grpc_Testing_BoolValue? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil -} - -/// Unary response, as configured by the request. -public struct Grpc_Testing_SimpleResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase message size. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// The user the request came from, for verifying authentication was - /// successful when the client expected it. - public var username: String = String() - - /// OAuth scope. - public var oauthScope: String = String() - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// Client-streaming request. -public struct Grpc_Testing_StreamingInputCallRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether the server should expect this request to be compressed. This field - /// is "nullable" in order to interoperate seamlessly with servers not able to - /// implement the full compression tests by introspecting the call to verify - /// the request's compression status. - public var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - public var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - public mutating func clearExpectCompressed() {self._expectCompressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil -} - -/// Client-streaming response. -public struct Grpc_Testing_StreamingInputCallResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Aggregated size of payloads received from the client. - public var aggregatedPayloadSize: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// Configuration for a particular response. -public struct Grpc_Testing_ResponseParameters: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload sizes in responses from the server. - public var size: Int32 = 0 - - /// Desired interval between consecutive responses in the response stream in - /// microseconds. - public var intervalUs: Int32 = 0 - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - public var compressed: Grpc_Testing_BoolValue { - get {return _compressed ?? Grpc_Testing_BoolValue()} - set {_compressed = newValue} - } - /// Returns true if `compressed` has been explicitly set. - public var hasCompressed: Bool {return self._compressed != nil} - /// Clears the value of `compressed`. Subsequent reads from it will return its default value. - public mutating func clearCompressed() {self._compressed = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _compressed: Grpc_Testing_BoolValue? = nil -} - -/// Server-streaming request. -public struct Grpc_Testing_StreamingOutputCallRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, the payload from each response in the stream - /// might be of different types. This is to simulate a mixed type of payload - /// stream. - public var responseType: Grpc_Testing_PayloadType = .compressable - - /// Configuration for each expected response message. - public var responseParameters: [Grpc_Testing_ResponseParameters] = [] - - /// Optional input payload sent along with the request. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - /// Whether server should return a given status - public var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - public var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - public mutating func clearResponseStatus() {self._responseStatus = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil -} - -/// Server-streaming response, as configured by the request and parameters. -public struct Grpc_Testing_StreamingOutputCallResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase response size. - public var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - public var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - public mutating func clearPayload() {self._payload = nil} - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// For reconnect interop test only. -/// Client tells server what reconnection parameters it used. -public struct Grpc_Testing_ReconnectParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var maxReconnectBackoffMs: Int32 = 0 - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -/// For reconnect interop test only. -/// Server tells client whether its reconnects are following the spec and the -/// reconnect backoffs it saw. -public struct Grpc_Testing_ReconnectInfo: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - public var passed: Bool = false - - public var backoffMs: [Int32] = [] - - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_PayloadType: SwiftProtobuf._ProtoNameProviding { - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "COMPRESSABLE"), - ] -} - -extension Grpc_Testing_BoolValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".BoolValue" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "value"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.value) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.value != false { - try visitor.visitSingularBoolField(value: self.value, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_BoolValue, rhs: Grpc_Testing_BoolValue) -> Bool { - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Payload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".Payload" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "body"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 2: try { try decoder.decodeSingularBytesField(value: &self.body) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.type != .compressable { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) - } - if !self.body.isEmpty { - try visitor.visitSingularBytesField(value: self.body, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_Payload, rhs: Grpc_Testing_Payload) -> Bool { - if lhs.type != rhs.type {return false} - if lhs.body != rhs.body {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_EchoStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".EchoStatus" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "code"), - 2: .same(proto: "message"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.code != 0 { - try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) - } - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_EchoStatus, rhs: Grpc_Testing_EchoStatus) -> Bool { - if lhs.code != rhs.code {return false} - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SimpleRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_size"), - 3: .same(proto: "payload"), - 4: .standard(proto: "fill_username"), - 5: .standard(proto: "fill_oauth_scope"), - 6: .standard(proto: "response_compressed"), - 7: .standard(proto: "response_status"), - 8: .standard(proto: "expect_compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.responseSize) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.fillUsername) }() - case 5: try { try decoder.decodeSingularBoolField(value: &self.fillOauthScope) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._responseCompressed) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - case 8: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if self.responseSize != 0 { - try visitor.visitSingularInt32Field(value: self.responseSize, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if self.fillUsername != false { - try visitor.visitSingularBoolField(value: self.fillUsername, fieldNumber: 4) - } - if self.fillOauthScope != false { - try visitor.visitSingularBoolField(value: self.fillOauthScope, fieldNumber: 5) - } - try { if let v = self._responseCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SimpleRequest, rhs: Grpc_Testing_SimpleRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseSize != rhs.responseSize {return false} - if lhs._payload != rhs._payload {return false} - if lhs.fillUsername != rhs.fillUsername {return false} - if lhs.fillOauthScope != rhs.fillOauthScope {return false} - if lhs._responseCompressed != rhs._responseCompressed {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".SimpleResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .same(proto: "username"), - 3: .standard(proto: "oauth_scope"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.username) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.oauthScope) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.username.isEmpty { - try visitor.visitSingularStringField(value: self.username, fieldNumber: 2) - } - if !self.oauthScope.isEmpty { - try visitor.visitSingularStringField(value: self.oauthScope, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_SimpleResponse, rhs: Grpc_Testing_SimpleResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.username != rhs.username {return false} - if lhs.oauthScope != rhs.oauthScope {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .standard(proto: "expect_compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingInputCallRequest, rhs: Grpc_Testing_StreamingInputCallRequest) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingInputCallResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "aggregated_payload_size"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.aggregatedPayloadSize) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.aggregatedPayloadSize != 0 { - try visitor.visitSingularInt32Field(value: self.aggregatedPayloadSize, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingInputCallResponse, rhs: Grpc_Testing_StreamingInputCallResponse) -> Bool { - if lhs.aggregatedPayloadSize != rhs.aggregatedPayloadSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ResponseParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ResponseParameters" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "size"), - 2: .standard(proto: "interval_us"), - 3: .same(proto: "compressed"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.intervalUs) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._compressed) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.size != 0 { - try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) - } - if self.intervalUs != 0 { - try visitor.visitSingularInt32Field(value: self.intervalUs, fieldNumber: 2) - } - try { if let v = self._compressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ResponseParameters, rhs: Grpc_Testing_ResponseParameters) -> Bool { - if lhs.size != rhs.size {return false} - if lhs.intervalUs != rhs.intervalUs {return false} - if lhs._compressed != rhs._compressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallRequest" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_parameters"), - 3: .same(proto: "payload"), - 7: .standard(proto: "response_status"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.responseParameters) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if !self.responseParameters.isEmpty { - try visitor.visitRepeatedMessageField(value: self.responseParameters, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingOutputCallRequest, rhs: Grpc_Testing_StreamingOutputCallRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseParameters != rhs.responseParameters {return false} - if lhs._payload != rhs._payload {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallResponse" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_StreamingOutputCallResponse, rhs: Grpc_Testing_StreamingOutputCallResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ReconnectParams" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "max_reconnect_backoff_ms"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.maxReconnectBackoffMs) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.maxReconnectBackoffMs != 0 { - try visitor.visitSingularInt32Field(value: self.maxReconnectBackoffMs, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ReconnectParams, rhs: Grpc_Testing_ReconnectParams) -> Bool { - if lhs.maxReconnectBackoffMs != rhs.maxReconnectBackoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = _protobuf_package + ".ReconnectInfo" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "passed"), - 2: .standard(proto: "backoff_ms"), - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.passed) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.backoffMs) }() - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if self.passed != false { - try visitor.visitSingularBoolField(value: self.passed, fieldNumber: 1) - } - if !self.backoffMs.isEmpty { - try visitor.visitPackedInt32Field(value: self.backoffMs, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - public static func ==(lhs: Grpc_Testing_ReconnectInfo, rhs: Grpc_Testing_ReconnectInfo) -> Bool { - if lhs.passed != rhs.passed {return false} - if lhs.backoffMs != rhs.backoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/InteroperabilityTests/Generated/test.grpc.swift b/Sources/InteroperabilityTests/Generated/test.grpc.swift deleted file mode 100644 index bbdbf3e49..000000000 --- a/Sources/InteroperabilityTests/Generated/test.grpc.swift +++ /dev/null @@ -1,1413 +0,0 @@ -// Copyright 2015-2016 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/test.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -public import GRPCCore -internal import GRPCProtobuf - -public enum Grpc_Testing_ReconnectService { - public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_ReconnectService - public enum Method { - public enum Start { - public typealias Input = Grpc_Testing_ReconnectParams - public typealias Output = Grpc_Testing_Empty - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_ReconnectService.descriptor.fullyQualifiedService, - method: "Start" - ) - } - public enum Stop { - public typealias Input = Grpc_Testing_Empty - public typealias Output = Grpc_Testing_ReconnectInfo - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_ReconnectService.descriptor.fullyQualifiedService, - method: "Stop" - ) - } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - Start.descriptor, - Stop.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Grpc_Testing_ReconnectServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Grpc_Testing_ReconnectServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = Grpc_Testing_ReconnectServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = Grpc_Testing_ReconnectServiceClient -} - -extension GRPCCore.ServiceDescriptor { - public static let grpc_testing_ReconnectService = Self( - package: "grpc.testing", - service: "ReconnectService" - ) -} - -public enum Grpc_Testing_TestService { - public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_TestService - public enum Method { - public enum EmptyCall { - public typealias Input = Grpc_Testing_Empty - public typealias Output = Grpc_Testing_Empty - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "EmptyCall" - ) - } - public enum UnaryCall { - public typealias Input = Grpc_Testing_SimpleRequest - public typealias Output = Grpc_Testing_SimpleResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "UnaryCall" - ) - } - public enum CacheableUnaryCall { - public typealias Input = Grpc_Testing_SimpleRequest - public typealias Output = Grpc_Testing_SimpleResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "CacheableUnaryCall" - ) - } - public enum StreamingOutputCall { - public typealias Input = Grpc_Testing_StreamingOutputCallRequest - public typealias Output = Grpc_Testing_StreamingOutputCallResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "StreamingOutputCall" - ) - } - public enum StreamingInputCall { - public typealias Input = Grpc_Testing_StreamingInputCallRequest - public typealias Output = Grpc_Testing_StreamingInputCallResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "StreamingInputCall" - ) - } - public enum FullDuplexCall { - public typealias Input = Grpc_Testing_StreamingOutputCallRequest - public typealias Output = Grpc_Testing_StreamingOutputCallResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "FullDuplexCall" - ) - } - public enum HalfDuplexCall { - public typealias Input = Grpc_Testing_StreamingOutputCallRequest - public typealias Output = Grpc_Testing_StreamingOutputCallResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "HalfDuplexCall" - ) - } - public enum UnimplementedCall { - public typealias Input = Grpc_Testing_Empty - public typealias Output = Grpc_Testing_Empty - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_TestService.descriptor.fullyQualifiedService, - method: "UnimplementedCall" - ) - } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - EmptyCall.descriptor, - UnaryCall.descriptor, - CacheableUnaryCall.descriptor, - StreamingOutputCall.descriptor, - StreamingInputCall.descriptor, - FullDuplexCall.descriptor, - HalfDuplexCall.descriptor, - UnimplementedCall.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Grpc_Testing_TestServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Grpc_Testing_TestServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = Grpc_Testing_TestServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = Grpc_Testing_TestServiceClient -} - -extension GRPCCore.ServiceDescriptor { - public static let grpc_testing_TestService = Self( - package: "grpc.testing", - service: "TestService" - ) -} - -public enum Grpc_Testing_UnimplementedService { - public static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_UnimplementedService - public enum Method { - public enum UnimplementedCall { - public typealias Input = Grpc_Testing_Empty - public typealias Output = Grpc_Testing_Empty - public static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_UnimplementedService.descriptor.fullyQualifiedService, - method: "UnimplementedCall" - ) - } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - UnimplementedCall.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Grpc_Testing_UnimplementedServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Grpc_Testing_UnimplementedServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = Grpc_Testing_UnimplementedServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = Grpc_Testing_UnimplementedServiceClient -} - -extension GRPCCore.ServiceDescriptor { - public static let grpc_testing_UnimplementedService = Self( - package: "grpc.testing", - service: "UnimplementedService" - ) -} - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_TestServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// One empty request followed by one empty response. - func emptyCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// One request followed by one response. - func unaryCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - func cacheableUnaryCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - func streamingOutputCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - func streamingInputCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - func fullDuplexCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - func halfDuplexCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// The test server will not implement this method. It will be used - /// to test the behavior when clients call unimplemented methods. - func unimplementedCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_TestService.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.EmptyCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.emptyCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.UnaryCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.unaryCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.cacheableUnaryCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.streamingOutputCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.streamingInputCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.fullDuplexCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.halfDuplexCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.unimplementedCall( - request: request, - context: context - ) - } - ) - } -} - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_TestServiceServiceProtocol: Grpc_Testing_TestService.StreamingServiceProtocol { - /// One empty request followed by one empty response. - func emptyCall( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// One request followed by one response. - func unaryCall( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - func cacheableUnaryCall( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - func streamingOutputCall( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - func streamingInputCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - func fullDuplexCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - func halfDuplexCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// The test server will not implement this method. It will be used - /// to test the behavior when clients call unimplemented methods. - func unimplementedCall( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single -} - -/// Partial conformance to `Grpc_Testing_TestServiceStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_TestService.ServiceProtocol { - public func emptyCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.emptyCall( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - public func unaryCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unaryCall( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - public func cacheableUnaryCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.cacheableUnaryCall( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - public func streamingOutputCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingOutputCall( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return response - } - - public func streamingInputCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingInputCall( - request: request, - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - public func unimplementedCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unimplementedCall( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } -} - -/// A simple service NOT implemented at servers so clients can test for -/// that case. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_UnimplementedServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// A call that no server should implement - func unimplementedCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_UnimplementedService.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.unimplementedCall( - request: request, - context: context - ) - } - ) - } -} - -/// A simple service NOT implemented at servers so clients can test for -/// that case. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_UnimplementedServiceServiceProtocol: Grpc_Testing_UnimplementedService.StreamingServiceProtocol { - /// A call that no server should implement - func unimplementedCall( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single -} - -/// Partial conformance to `Grpc_Testing_UnimplementedServiceStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_UnimplementedService.ServiceProtocol { - public func unimplementedCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unimplementedCall( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } -} - -/// A service used to control reconnect server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_ReconnectServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func start( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - func stop( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_ReconnectService.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Grpc_Testing_ReconnectService.Method.Start.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.start( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_ReconnectService.Method.Stop.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.stop( - request: request, - context: context - ) - } - ) - } -} - -/// A service used to control reconnect server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_ReconnectServiceServiceProtocol: Grpc_Testing_ReconnectService.StreamingServiceProtocol { - func start( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - func stop( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single -} - -/// Partial conformance to `Grpc_Testing_ReconnectServiceStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_ReconnectService.ServiceProtocol { - public func start( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.start( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - public func stop( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.stop( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } -} - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_TestServiceClientProtocol: Sendable { - /// One empty request followed by one empty response. - func emptyCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - /// One request followed by one response. - func unaryCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - func cacheableUnaryCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - func streamingOutputCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - func streamingInputCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - func fullDuplexCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - func halfDuplexCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable - - /// The test server will not implement this method. It will be used - /// to test the behavior when clients call unimplemented methods. - func unimplementedCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_TestService.ClientProtocol { - public func emptyCall( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.emptyCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func unaryCall( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.unaryCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func cacheableUnaryCall( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.cacheableUnaryCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func streamingOutputCall( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.streamingOutputCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func streamingInputCall( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.streamingInputCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func fullDuplexCall( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.fullDuplexCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func halfDuplexCall( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.halfDuplexCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func unimplementedCall( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.unimplementedCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_TestService.ClientProtocol { - /// One empty request followed by one empty response. - public func emptyCall( - _ message: Grpc_Testing_Empty, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.emptyCall( - request: request, - options: options, - handleResponse - ) - } - - /// One request followed by one response. - public func unaryCall( - _ message: Grpc_Testing_SimpleRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.unaryCall( - request: request, - options: options, - handleResponse - ) - } - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - public func cacheableUnaryCall( - _ message: Grpc_Testing_SimpleRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.cacheableUnaryCall( - request: request, - options: options, - handleResponse - ) - } - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - public func streamingOutputCall( - _ message: Grpc_Testing_StreamingOutputCallRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.streamingOutputCall( - request: request, - options: options, - handleResponse - ) - } - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - public func streamingInputCall( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.streamingInputCall( - request: request, - options: options, - handleResponse - ) - } - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - public func fullDuplexCall( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.fullDuplexCall( - request: request, - options: options, - handleResponse - ) - } - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - public func halfDuplexCall( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.halfDuplexCall( - request: request, - options: options, - handleResponse - ) - } - - /// The test server will not implement this method. It will be used - /// to test the behavior when clients call unimplemented methods. - public func unimplementedCall( - _ message: Grpc_Testing_Empty, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.unimplementedCall( - request: request, - options: options, - handleResponse - ) - } -} - -/// A simple service to test the various types of RPCs and experiment with -/// performance with various types of payload. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct Grpc_Testing_TestServiceClient: Grpc_Testing_TestService.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// One empty request followed by one empty response. - public func emptyCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_TestService.Method.EmptyCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// One request followed by one response. - public func unaryCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_TestService.Method.UnaryCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// One request followed by one response. Response has cache control - /// headers set such that a caching HTTP proxy (such as GFE) can - /// satisfy subsequent requests. - public func cacheableUnaryCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_TestService.Method.CacheableUnaryCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// One request followed by a sequence of responses (streamed download). - /// The server returns the payload with client desired type and sizes. - public func streamingOutputCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Grpc_Testing_TestService.Method.StreamingOutputCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// A sequence of requests followed by one response (streamed upload). - /// The server returns the aggregated size of client payload as the result. - public func streamingInputCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Grpc_Testing_TestService.Method.StreamingInputCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// A sequence of requests with each request served by the server immediately. - /// As one request could lead to multiple responses, this interface - /// demonstrates the idea of full duplexing. - public func fullDuplexCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Grpc_Testing_TestService.Method.FullDuplexCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// A sequence of requests followed by a sequence of responses. - /// The server buffers all the client requests and then serves them in order. A - /// stream of responses are returned to the client when the server starts with - /// first request. - public func halfDuplexCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Grpc_Testing_TestService.Method.HalfDuplexCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// The test server will not implement this method. It will be used - /// to test the behavior when clients call unimplemented methods. - public func unimplementedCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_TestService.Method.UnimplementedCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} - -/// A simple service NOT implemented at servers so clients can test for -/// that case. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_UnimplementedServiceClientProtocol: Sendable { - /// A call that no server should implement - func unimplementedCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_UnimplementedService.ClientProtocol { - public func unimplementedCall( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.unimplementedCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_UnimplementedService.ClientProtocol { - /// A call that no server should implement - public func unimplementedCall( - _ message: Grpc_Testing_Empty, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.unimplementedCall( - request: request, - options: options, - handleResponse - ) - } -} - -/// A simple service NOT implemented at servers so clients can test for -/// that case. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct Grpc_Testing_UnimplementedServiceClient: Grpc_Testing_UnimplementedService.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// A call that no server should implement - public func unimplementedCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_UnimplementedService.Method.UnimplementedCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} - -/// A service used to control reconnect server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol Grpc_Testing_ReconnectServiceClientProtocol: Sendable { - func start( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - func stop( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_ReconnectService.ClientProtocol { - public func start( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.start( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - public func stop( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.stop( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_ReconnectService.ClientProtocol { - public func start( - _ message: Grpc_Testing_ReconnectParams, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.start( - request: request, - options: options, - handleResponse - ) - } - - public func stop( - _ message: Grpc_Testing_Empty, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.stop( - request: request, - options: options, - handleResponse - ) - } -} - -/// A service used to control reconnect server. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct Grpc_Testing_ReconnectServiceClient: Grpc_Testing_ReconnectService.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - public func start( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_ReconnectService.Method.Start.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - public func stop( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_ReconnectService.Method.Stop.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} \ No newline at end of file diff --git a/Sources/InteroperabilityTests/Generated/test.pb.swift b/Sources/InteroperabilityTests/Generated/test.pb.swift deleted file mode 100644 index 8947a84cb..000000000 --- a/Sources/InteroperabilityTests/Generated/test.pb.swift +++ /dev/null @@ -1,28 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: src/proto/grpc/testing/test.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015-2016 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. - -// This file contained no messages, enums, or extensions. diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift b/Sources/InteroperabilityTests/InteroperabilityTestCase.swift deleted file mode 100644 index 1c60f1401..000000000 --- a/Sources/InteroperabilityTests/InteroperabilityTestCase.swift +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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. - */ -public import GRPCCore - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public protocol InteroperabilityTest { - /// Run a test case using the given connection. - /// - /// The test case is considered unsuccessful if any exception is thrown, conversely if no - /// exceptions are thrown it is successful. - /// - /// - Parameter client: The client to use for the test. - /// - Throws: Any exception may be thrown to indicate an unsuccessful test. - func run(client: GRPCClient) async throws -} - -/// Test cases as listed by the [gRPC interoperability test description specification] -/// (https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md). -/// -/// This is not a complete list, the following tests have not been implemented: -/// - cacheable_unary (caching not supported) -/// - cancel_after_begin (if the client cancels the task running the request, there's no response to be -/// received, so we can't check we got back a Cancelled status code) -/// - cancel_after_first_response (same reason as above) -/// - client_compressed_streaming (we don't support per-message compression, so we can't implement this) -/// - compute_engine_creds -/// - jwt_token_creds -/// - oauth2_auth_token -/// - per_rpc_creds -/// - google_default_credentials -/// - compute_engine_channel_credentials -/// - timeout_on_sleeping_server (timeouts end up being surfaced as `CancellationError`s, so we -/// can't really implement this test) -/// -/// Note: Tests for compression have not been implemented yet as compression is -/// not supported. Once the API which allows for compression will be implemented -/// these tests should be added. -public enum InteroperabilityTestCase: String, CaseIterable, Sendable { - case emptyUnary = "empty_unary" - case largeUnary = "large_unary" - case clientCompressedUnary = "client_compressed_unary" - case serverCompressedUnary = "server_compressed_unary" - case clientStreaming = "client_streaming" - case serverStreaming = "server_streaming" - case serverCompressedStreaming = "server_compressed_streaming" - case pingPong = "ping_pong" - case emptyStream = "empty_stream" - case customMetadata = "custom_metadata" - case statusCodeAndMessage = "status_code_and_message" - case specialStatusMessage = "special_status_message" - case unimplementedMethod = "unimplemented_method" - case unimplementedService = "unimplemented_service" - - public var name: String { - return self.rawValue - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension InteroperabilityTestCase { - /// Return a new instance of the test case. - public func makeTest() -> any InteroperabilityTest { - switch self { - case .emptyUnary: - return EmptyUnary() - case .largeUnary: - return LargeUnary() - case .clientCompressedUnary: - return ClientCompressedUnary() - case .serverCompressedUnary: - return ServerCompressedUnary() - case .clientStreaming: - return ClientStreaming() - case .serverStreaming: - return ServerStreaming() - case .serverCompressedStreaming: - return ServerCompressedStreaming() - case .pingPong: - return PingPong() - case .emptyStream: - return EmptyStream() - case .customMetadata: - return CustomMetadata() - case .statusCodeAndMessage: - return StatusCodeAndMessage() - case .specialStatusMessage: - return SpecialStatusMessage() - case .unimplementedMethod: - return UnimplementedMethod() - case .unimplementedService: - return UnimplementedService() - } - } -} diff --git a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift b/Sources/InteroperabilityTests/InteroperabilityTestCases.swift deleted file mode 100644 index b1be0be50..000000000 --- a/Sources/InteroperabilityTests/InteroperabilityTestCases.swift +++ /dev/null @@ -1,996 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore - -private import struct Foundation.Data - -/// This test verifies that implementations support zero-size messages. Ideally, client -/// implementations would verify that the request and response were zero bytes serialized, but -/// this is generally prohibitive to perform, so is not required. -/// -/// Server features: -/// - EmptyCall -/// -/// Procedure: -/// 1. Client calls EmptyCall with the default Empty message -/// -/// Client asserts: -/// - call was successful -/// - response is non-null -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct EmptyUnary: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - try await testServiceClient.emptyCall( - request: ClientRequest.Single(message: Grpc_Testing_Empty()) - ) { response in - try assertEqual(response.message, Grpc_Testing_Empty()) - } - } -} - -/// This test verifies unary calls succeed in sending messages, and touches on flow control (even -/// if compression is enabled on the channel). -/// -/// Server features: -/// - UnaryCall -/// -/// Procedure: -/// 1. Client calls UnaryCall with: -/// ``` -/// { -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - response payload body is 314159 bytes in size -/// - clients are free to assert that the response payload body contents are zero and comparing -/// the entire response message against a golden response -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct LargeUnary: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - let request = Grpc_Testing_SimpleRequest.with { request in - request.responseSize = 314_159 - request.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: 271_828) - } - } - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: request) - ) { response in - try assertEqual( - response.message.payload, - Grpc_Testing_Payload.with { - $0.body = Data(count: 314_159) - } - ) - } - } -} - -/// This test verifies the client can compress unary messages by sending two unary calls, for -/// compressed and uncompressed payloads. It also sends an initial probing request to verify -/// whether the server supports the CompressedRequest feature by checking if the probing call -/// fails with an `INVALID_ARGUMENT` status. -/// -/// Server features: -/// - UnaryCall -/// - CompressedRequest -/// -/// Procedure: -/// 1. Client calls UnaryCall with the feature probe, an *uncompressed* message: -/// ``` -/// { -/// expect_compressed:{ -/// value: true -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// 2. Client calls UnaryCall with the *compressed* message: -/// ``` -/// { -/// expect_compressed:{ -/// value: true -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// 3. Client calls UnaryCall with the *uncompressed* message: -/// ``` -/// { -/// expect_compressed:{ -/// value: false -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - First call failed with `INVALID_ARGUMENT` status. -/// - Subsequent calls were successful. -/// - Response payload body is 314159 bytes in size. -/// - Clients are free to assert that the response payload body contents are zeros and comparing the -/// entire response message against a golden response. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -class ClientCompressedUnary: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - let compressedRequest = Grpc_Testing_SimpleRequest.with { request in - request.expectCompressed = .with { $0.value = true } - request.responseSize = 314_159 - request.payload = .with { $0.body = Data(repeating: 0, count: 271_828) } - } - - var uncompressedRequest = compressedRequest - uncompressedRequest.expectCompressed = .with { $0.value = false } - - // For unary RPCs we disable compression at the call level. - var options = CallOptions.defaults - - // With compression expected but *disabled*. - options.compression = CompressionAlgorithm.none - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: compressedRequest), - options: options - ) { response in - switch response.accepted { - case .success: - throw AssertionFailure(message: "The result should be an error.") - case .failure(let error): - try assertEqual(error.code, .invalidArgument) - } - } - - // With compression expected and enabled. - options.compression = .gzip - - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: compressedRequest), - options: options - ) { response in - switch response.accepted { - case .success(let success): - try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) - case .failure: - throw AssertionFailure(message: "Response should have been accepted.") - } - } - - // With compression not expected and disabled. - options.compression = CompressionAlgorithm.none - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: uncompressedRequest), - options: options - ) { response in - switch response.accepted { - case .success(let success): - try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) - case .failure: - throw AssertionFailure(message: "Response should have been accepted.") - } - } - } -} - -/// This test verifies the server can compress unary messages. It sends two unary -/// requests, expecting the server's response to be compressed or not according to -/// the `response_compressed` boolean. -/// -/// Whether compression was actually performed is determined by the compression bit -/// in the response's message flags. *Note that some languages may not have access -/// to the message flags, in which case the client will be unable to verify that -/// the `response_compressed` boolean is obeyed by the server*. -/// -/// -/// Server features: -/// - UnaryCall -/// - CompressedResponse -/// -/// Procedure: -/// 1. Client calls UnaryCall with `SimpleRequest`: -/// ``` -/// { -/// response_compressed:{ -/// value: true -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// ``` -/// { -/// response_compressed:{ -/// value: false -/// } -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - if supported by the implementation, when `response_compressed` is true, the response MUST have -/// the compressed message flag set. -/// - if supported by the implementation, when `response_compressed` is false, the response MUST NOT -/// have the compressed message flag set. -/// - response payload body is 314159 bytes in size in both cases. -/// - clients are free to assert that the response payload body contents are zero and comparing the -/// entire response message against a golden response -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -class ServerCompressedUnary: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - - let compressedRequest = Grpc_Testing_SimpleRequest.with { request in - request.responseCompressed = .with { $0.value = true } - request.responseSize = 314_159 - request.payload = .with { $0.body = Data(repeating: 0, count: 271_828) } - } - - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: compressedRequest) - ) { response in - // We can't verify that the compression bit was set, instead we verify that the encoding header - // was sent by the server. This isn't quite the same since as it can still be set but the - // compression may _not_ be set. - try assertTrue(response.metadata["grpc-encoding"].contains { $0 != "identity" }) - - switch response.accepted { - case .success(let success): - try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) - case .failure: - throw AssertionFailure(message: "Response should have been accepted.") - } - } - - var uncompressedRequest = compressedRequest - uncompressedRequest.responseCompressed.value = false - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: compressedRequest) - ) { response in - // We can't even check for the 'grpc-encoding' header here since it could be set with the - // compression bit on the message not set. - switch response.accepted { - case .success(let success): - try assertEqual(success.message.get().payload.body, Data(repeating: 0, count: 314_159)) - case .failure: - throw AssertionFailure( - message: "Response should have been accepted." - ) - } - } - } -} - -/// This test verifies that client-only streaming succeeds. -/// -/// Server features: -/// - StreamingInputCall -/// -/// Procedure: -/// 1. Client calls StreamingInputCall -/// 2. Client sends: -/// ``` -/// { -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// 3. Client then sends: -/// ``` -/// { -/// payload:{ -/// body: 8 bytes of zeros -/// } -/// } -/// ``` -/// 4. Client then sends: -/// ``` -/// { -/// payload:{ -/// body: 1828 bytes of zeros -/// } -/// } -/// ``` -/// 5. Client then sends: -/// ``` -/// { -/// payload:{ -/// body: 45904 bytes of zeros -/// } -/// } -/// ``` -/// 6. Client half-closes -/// -/// Client asserts: -/// - call was successful -/// - response aggregated_payload_size is 74922 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct ClientStreaming: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - let request = ClientRequest.Stream { writer in - for bytes in [27182, 8, 1828, 45904] { - let message = Grpc_Testing_StreamingInputCallRequest.with { - $0.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: bytes) - } - } - try await writer.write(message) - } - } - - try await testServiceClient.streamingInputCall(request: request) { response in - try assertEqual(response.message.aggregatedPayloadSize, 74922) - } - } -} - -/// This test verifies that server-only streaming succeeds. -/// -/// Server features: -/// - StreamingOutputCall -/// -/// Procedure: -/// 1. Client calls StreamingOutputCall with StreamingOutputCallRequest: -/// ``` -/// { -/// response_parameters:{ -/// size: 31415 -/// } -/// response_parameters:{ -/// size: 9 -/// } -/// response_parameters:{ -/// size: 2653 -/// } -/// response_parameters:{ -/// size: 58979 -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - exactly four responses -/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 -/// - clients are free to assert that the response payload body contents are zero and -/// comparing the entire response messages against golden responses -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct ServerStreaming: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - let responseSizes = [31415, 9, 2653, 58979] - let request = Grpc_Testing_StreamingOutputCallRequest.with { request in - request.responseParameters = responseSizes.map { - var parameter = Grpc_Testing_ResponseParameters() - parameter.size = Int32($0) - return parameter - } - } - - try await testServiceClient.streamingOutputCall( - request: ClientRequest.Single(message: request) - ) { response in - var responseParts = response.messages.makeAsyncIterator() - // There are 4 response sizes, so if there isn't a message for each one, - // it means that the client didn't receive 4 messages back. - for responseSize in responseSizes { - if let message = try await responseParts.next() { - try assertEqual(message.payload.body.count, responseSize) - } else { - throw AssertionFailure( - message: "There were less than four responses received." - ) - } - } - // Check that there were not more than 4 responses from the server. - try assertEqual(try await responseParts.next(), nil) - } - } -} - -/// This test verifies that the server can compress streaming messages and disable compression on -/// individual messages, expecting the server's response to be compressed or not according to the -/// `response_compressed` boolean. -/// -/// Whether compression was actually performed is determined by the compression bit in the -/// response's message flags. *Note that some languages may not have access to the message flags, in -/// which case the client will be unable to verify that the `response_compressed` boolean is obeyed -/// by the server*. -/// -/// Server features: -/// - StreamingOutputCall -/// - CompressedResponse -/// -/// Procedure: -/// 1. Client calls StreamingOutputCall with `StreamingOutputCallRequest`: -/// ``` -/// { -/// response_parameters:{ -/// compressed: { -/// value: true -/// } -/// size: 31415 -/// } -/// response_parameters:{ -/// compressed: { -/// value: false -/// } -/// size: 92653 -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - call was successful -/// - exactly two responses -/// - if supported by the implementation, when `response_compressed` is false, the response's -/// messages MUST NOT have the compressed message flag set. -/// - if supported by the implementation, when `response_compressed` is true, the response's -/// messages MUST have the compressed message flag set. -/// - response payload bodies are sized (in order): 31415, 92653 -/// - clients are free to assert that the response payload body contents are zero and comparing the -/// entire response messages against golden responses -class ServerCompressedStreaming: InteroperabilityTest { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - let request: Grpc_Testing_StreamingOutputCallRequest = .with { request in - request.responseParameters = [ - .with { - $0.compressed = .with { $0.value = true } - $0.size = 31415 - }, - .with { - $0.compressed = .with { $0.value = false } - $0.size = 92653 - }, - ] - } - let responseSizes = [31415, 92653] - - try await testServiceClient.streamingOutputCall( - request: ClientRequest.Single(message: request) - ) { response in - var payloads = [Grpc_Testing_Payload]() - - switch response.accepted { - case .success(let success): - // We can't verify that the compression bit was set, instead we verify that the encoding header - // was sent by the server. This isn't quite the same since as it can still be set but the - // compression may be not set. - try assertTrue(success.metadata["grpc-encoding"].contains { $0 != "identity" }) - - for try await part in success.bodyParts { - switch part { - case .message(let message): - payloads.append(message.payload) - case .trailingMetadata: - () - } - } - - case .failure: - throw AssertionFailure(message: "Response should have been accepted.") - } - - try assertEqual( - payloads, - responseSizes.map { size in - Grpc_Testing_Payload.with { - $0.body = Data(repeating: 0, count: size) - } - } - ) - } - } -} - -/// This test verifies that full duplex bidi is supported. -/// -/// Server features: -/// - FullDuplexCall -/// -/// Procedure: -/// 1. Client calls FullDuplexCall with: -/// ``` -/// { -/// response_parameters:{ -/// size: 31415 -/// } -/// payload:{ -/// body: 27182 bytes of zeros -/// } -/// } -/// ``` -/// 2. After getting a reply, it sends: -/// ``` -/// { -/// response_parameters:{ -/// size: 9 -/// } -/// payload:{ -/// body: 8 bytes of zeros -/// } -/// } -/// ``` -/// 3. After getting a reply, it sends: -/// ``` -/// { -/// response_parameters:{ -/// size: 2653 -/// } -/// payload:{ -/// body: 1828 bytes of zeros -/// } -/// } -/// ``` -/// 4. After getting a reply, it sends: -/// ``` -/// { -/// response_parameters:{ -/// size: 58979 -/// } -/// payload:{ -/// body: 45904 bytes of zeros -/// } -/// } -/// ``` -/// 5. After getting a reply, client half-closes -/// -/// Client asserts: -/// - call was successful -/// - exactly four responses -/// - response payload bodies are sized (in order): 31415, 9, 2653, 58979 -/// - clients are free to assert that the response payload body contents are zero and -/// comparing the entire response messages against golden responses -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct PingPong: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - let ids = AsyncStream.makeStream(of: Int.self) - - let request = ClientRequest.Stream { writer in - let sizes = [(31_415, 27_182), (9, 8), (2_653, 1_828), (58_979, 45_904)] - for try await id in ids.stream { - var message = Grpc_Testing_StreamingOutputCallRequest() - switch id { - case 1 ... 4: - let (responseSize, bodySize) = sizes[id - 1] - message.responseParameters = [ - Grpc_Testing_ResponseParameters.with { - $0.size = Int32(responseSize) - } - ] - message.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: bodySize) - } - default: - // When the id is higher than 4 it means the client received all the expected responses - // and it doesn't need to send another message. - return - } - try await writer.write(message) - } - } - ids.continuation.yield(1) - try await testServiceClient.fullDuplexCall(request: request) { response in - var id = 1 - for try await message in response.messages { - switch id { - case 1: - try assertEqual(message.payload.body, Data(count: 31_415)) - case 2: - try assertEqual(message.payload.body, Data(count: 9)) - case 3: - try assertEqual(message.payload.body, Data(count: 2_653)) - case 4: - try assertEqual(message.payload.body, Data(count: 58_979)) - default: - throw AssertionFailure( - message: "We should only receive messages with ids between 1 and 4." - ) - } - - // Add the next id to the continuation. - id += 1 - ids.continuation.yield(id) - } - } - } -} - -/// This test verifies that streams support having zero-messages in both directions. -/// -/// Server features: -/// - FullDuplexCall -/// -/// Procedure: -/// 1. Client calls FullDuplexCall and then half-closes -/// -/// Client asserts: -/// - call was successful -/// - exactly zero responses -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct EmptyStream: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - let request = ClientRequest.Stream { _ in } - - try await testServiceClient.fullDuplexCall(request: request) { response in - var messages = response.messages.makeAsyncIterator() - try await assertEqual(messages.next(), nil) - } - } -} - -/// This test verifies that custom metadata in either binary or ascii format can be sent as -/// initial-metadata by the client and as both initial- and trailing-metadata by the server. -/// -/// Server features: -/// - UnaryCall -/// - FullDuplexCall -/// - Echo Metadata -/// -/// Procedure: -/// 1. The client attaches custom metadata with the following keys and values -/// to a UnaryCall with request: -/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" -/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab -/// ``` -/// { -/// response_size: 314159 -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// 2. The client attaches custom metadata with the following keys and values -/// to a FullDuplexCall with request: -/// - key: "x-grpc-test-echo-initial", value: "test_initial_metadata_value" -/// - key: "x-grpc-test-echo-trailing-bin", value: 0xababab -/// ``` -/// { -/// response_parameters:{ -/// size: 314159 -/// } -/// payload:{ -/// body: 271828 bytes of zeros -/// } -/// } -/// ``` -/// and then half-closes -/// -/// Client asserts: -/// - call was successful -/// - metadata with key "x-grpc-test-echo-initial" and value "test_initial_metadata_value" is -/// received in the initial metadata for calls in Procedure steps 1 and 2. -/// - metadata with key "x-grpc-test-echo-trailing-bin" and value 0xababab is received in the -/// trailing metadata for calls in Procedure steps 1 and 2. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct CustomMetadata: InteroperabilityTest { - let initialMetadataName = "x-grpc-test-echo-initial" - let initialMetadataValue = "test_initial_metadata_value" - - let trailingMetadataName = "x-grpc-test-echo-trailing-bin" - let trailingMetadataValue: [UInt8] = [0xAB, 0xAB, 0xAB] - - func checkInitialMetadata(_ metadata: Metadata) throws { - let values = metadata[self.initialMetadataName] - try assertEqual(Array(values), [.string(self.initialMetadataValue)]) - } - - func checkTrailingMetadata(_ metadata: Metadata) throws { - let values = metadata[self.trailingMetadataName] - try assertEqual(Array(values), [.binary(self.trailingMetadataValue)]) - } - - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - - let unaryRequest = Grpc_Testing_SimpleRequest.with { request in - request.responseSize = 314_159 - request.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: 271_828) - } - } - let metadata: Metadata = [ - self.initialMetadataName: .string(self.initialMetadataValue), - self.trailingMetadataName: .binary(self.trailingMetadataValue), - ] - - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: unaryRequest, metadata: metadata) - ) { response in - // Check the initial metadata. - let receivedInitialMetadata = response.metadata - try checkInitialMetadata(receivedInitialMetadata) - - // Check the message. - try assertEqual(response.message.payload.body, Data(count: 314_159)) - - // Check the trailing metadata. - try checkTrailingMetadata(response.trailingMetadata) - } - - let streamingRequest = ClientRequest.Stream(metadata: metadata) { writer in - let message = Grpc_Testing_StreamingOutputCallRequest.with { - $0.responseParameters = [ - Grpc_Testing_ResponseParameters.with { - $0.size = 314_159 - } - ] - $0.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: 271_828) - } - } - try await writer.write(message) - } - - try await testServiceClient.fullDuplexCall(request: streamingRequest) { response in - switch response.accepted { - case .success(let contents): - // Check the initial metadata. - let receivedInitialMetadata = response.metadata - try self.checkInitialMetadata(receivedInitialMetadata) - - let parts = try await contents.bodyParts.reduce(into: []) { $0.append($1) } - try assertEqual(parts.count, 2) - - for part in parts { - switch part { - // Check the message. - case .message(let message): - try assertEqual(message.payload.body, Data(count: 314_159)) - // Check the trailing metadata. - case .trailingMetadata(let receivedTrailingMetadata): - try self.checkTrailingMetadata(receivedTrailingMetadata) - } - } - case .failure: - throw AssertionFailure( - message: "The client should have received a response from the server." - ) - } - } - } -} - -/// This test verifies unary calls succeed in sending messages, and propagate back status code and -/// message sent along with the messages. -/// -/// Server features: -/// - UnaryCall -/// - FullDuplexCall -/// - Echo Status -/// -/// Procedure: -/// 1. Client calls UnaryCall with: -/// ``` -/// { -/// response_status:{ -/// code: 2 -/// message: "test status message" -/// } -/// } -/// ``` -/// 2. Client calls FullDuplexCall with: -/// ``` -/// { -/// response_status:{ -/// code: 2 -/// message: "test status message" -/// } -/// } -/// ``` -/// 3. and then half-closes -/// -/// Client asserts: -/// - received status code is the same as the sent code for both Procedure steps 1 and 2 -/// - received status message is the same as the sent message for both Procedure steps 1 and 2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct StatusCodeAndMessage: InteroperabilityTest { - let expectedCode = 2 - let expectedMessage = "test status message" - - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - - let message = Grpc_Testing_SimpleRequest.with { - $0.responseStatus = Grpc_Testing_EchoStatus.with { - $0.code = Int32(self.expectedCode) - $0.message = self.expectedMessage - } - } - - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: message) - ) { response in - switch response.accepted { - case .failure(let error): - try assertEqual(error.code.rawValue, self.expectedCode) - try assertEqual(error.message, self.expectedMessage) - case .success: - throw AssertionFailure( - message: - "The client should receive an error with the status code and message sent by the client." - ) - } - } - - let request = ClientRequest.Stream { writer in - let message = Grpc_Testing_StreamingOutputCallRequest.with { - $0.responseStatus = Grpc_Testing_EchoStatus.with { - $0.code = Int32(self.expectedCode) - $0.message = self.expectedMessage - } - } - try await writer.write(message) - } - - try await testServiceClient.fullDuplexCall(request: request) { response in - do { - for try await _ in response.messages { - throw AssertionFailure( - message: - "The client should receive an error with the status code and message sent by the client." - ) - } - } catch let error as RPCError { - try assertEqual(error.code.rawValue, self.expectedCode) - try assertEqual(error.message, self.expectedMessage) - } - } - } -} - -/// This test verifies Unicode and whitespace is correctly processed in status message. "\t" is -/// horizontal tab. "\r" is carriage return. "\n" is line feed. -/// -/// Server features: -/// - UnaryCall -/// - Echo Status -/// -/// Procedure: -/// 1. Client calls UnaryCall with: -/// ``` -/// { -/// response_status:{ -/// code: 2 -/// message: "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" -/// } -/// } -/// ``` -/// -/// Client asserts: -/// - received status code is the same as the sent code for Procedure step 1 -/// - received status message is the same as the sent message for Procedure step 1, including all -/// whitespace characters -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct SpecialStatusMessage: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - - let responseMessage = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" - let message = Grpc_Testing_SimpleRequest.with { - $0.responseStatus = Grpc_Testing_EchoStatus.with { - $0.code = 2 - $0.message = responseMessage - } - } - try await testServiceClient.unaryCall( - request: ClientRequest.Single(message: message) - ) { response in - switch response.accepted { - case .success: - throw AssertionFailure( - message: "The response should be an error with the error code 2." - ) - case .failure(let error): - try assertEqual(error.code.rawValue, 2) - try assertEqual(error.message, responseMessage) - } - } - } -} - -/// This test verifies that calling an unimplemented RPC method returns the UNIMPLEMENTED status -/// code. -/// -/// Server features: N/A -/// -/// Procedure: -/// 1. Client calls grpc.testing.TestService/UnimplementedCall with an empty request (defined as -/// grpc.testing.Empty): -/// ``` -/// { -/// } -/// ``` -/// -/// Client asserts: -/// - received status code is 12 (UNIMPLEMENTED) -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct UnimplementedMethod: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let testServiceClient = Grpc_Testing_TestService.Client(wrapping: client) - try await testServiceClient.unimplementedCall( - request: ClientRequest.Single(message: Grpc_Testing_Empty()) - ) { response in - switch response.accepted { - case .success: - throw AssertionFailure( - message: "The result should be an error." - ) - case .failure(let error): - try assertEqual(error.code, .unimplemented) - } - } - } -} - -/// This test verifies calling an unimplemented server returns the UNIMPLEMENTED status code. -/// -/// Server features: N/A -/// -/// Procedure: -/// 1. Client calls grpc.testing.UnimplementedService/UnimplementedCall with an empty request -/// (defined as grpc.testing.Empty): -/// ``` -/// { -/// } -/// ``` -/// -/// Client asserts: -/// - received status code is 12 (UNIMPLEMENTED) -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct UnimplementedService: InteroperabilityTest { - func run(client: GRPCClient) async throws { - let unimplementedServiceClient = Grpc_Testing_UnimplementedService.Client(wrapping: client) - try await unimplementedServiceClient.unimplementedCall( - request: ClientRequest.Single(message: Grpc_Testing_Empty()) - ) { response in - switch response.accepted { - case .success: - throw AssertionFailure(message: "The result should be an error.") - case .failure(let error): - try assertEqual(error.code, .unimplemented) - } - } - } -} diff --git a/Sources/InteroperabilityTests/TestService.swift b/Sources/InteroperabilityTests/TestService.swift deleted file mode 100644 index f4c79b784..000000000 --- a/Sources/InteroperabilityTests/TestService.swift +++ /dev/null @@ -1,244 +0,0 @@ -/* - * 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. - */ - -private import Foundation -public import GRPCCore - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct TestService: Grpc_Testing_TestService.ServiceProtocol { - public init() {} - - public func unimplementedCall( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") - } - - /// Server implements `emptyCall` which immediately returns the empty message. - public func emptyCall( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - let message = Grpc_Testing_Empty() - let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() - return ServerResponse.Single( - message: message, - metadata: initialMetadata, - trailingMetadata: trailingMetadata - ) - } - - /// Server implements `unaryCall` which immediately returns a `SimpleResponse` with a payload - /// body of size `SimpleRequest.responseSize` bytes and type as appropriate for the - /// `SimpleRequest.responseType`. - /// - /// If the server does not support the `responseType`, then it should fail the RPC with - /// `INVALID_ARGUMENT`. - public func unaryCall( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is - // set), so we have to check via the encoding header. Note that it is possible for the header - // to be set and for the message to not be compressed. - let isRequestCompressed = - request.metadata["grpc-encoding"].filter({ $0 != "identity" }).count > 0 - if request.message.expectCompressed.value, !isRequestCompressed { - throw RPCError( - code: .invalidArgument, - message: "Expected compressed request, but 'grpc-encoding' was missing" - ) - } - - // If the request has a responseStatus set, the server should return that status. - // If the code is an error code, the server will throw an error containing that code - // and the message set in the responseStatus. - // If the code is `ok`, the server will automatically send back an `ok` status. - if request.message.responseStatus.isInitialized { - guard let code = Status.Code(rawValue: Int(request.message.responseStatus.code)) else { - throw RPCError(code: .invalidArgument, message: "The response status code is invalid.") - } - let status = Status( - code: code, - message: request.message.responseStatus.message - ) - if let error = RPCError(status: status) { - throw error - } - } - - if case .UNRECOGNIZED = request.message.responseType { - throw RPCError(code: .invalidArgument, message: "The response type is not recognized.") - } - - let responseMessage = Grpc_Testing_SimpleResponse.with { response in - response.payload = Grpc_Testing_Payload.with { payload in - payload.body = Data(repeating: 0, count: Int(request.message.responseSize)) - payload.type = request.message.responseType - } - } - - let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() - - return ServerResponse.Single( - message: responseMessage, - metadata: initialMetadata, - trailingMetadata: trailingMetadata - ) - } - - /// Server gets the default `SimpleRequest` proto as the request. The content of the request is - /// ignored. It returns the `SimpleResponse` proto with the payload set to current timestamp. - /// The timestamp is an integer representing current time with nanosecond resolution. This - /// integer is formated as ASCII decimal in the response. The format is not really important as - /// long as the response payload is different for each request. In addition it adds cache control - /// headers such that the response can be cached by proxies in the response path. Server should - /// be behind a caching proxy for this test to pass. Currently we set the max-age to 60 seconds. - public func cacheableUnaryCall( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") - } - - /// Server implements `streamingOutputCall` by replying, in order, with one - /// `StreamingOutputCallResponse` for each `ResponseParameter`s in `StreamingOutputCallRequest`. - /// Each `StreamingOutputCallResponse` should have a payload body of size `ResponseParameter.size` - /// bytes, as specified by its respective `ResponseParameter`. After sending all responses, it - /// closes with OK. - public func streamingOutputCall( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Stream { - let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() - return ServerResponse.Stream(metadata: initialMetadata) { writer in - for responseParameter in request.message.responseParameters { - let response = Grpc_Testing_StreamingOutputCallResponse.with { response in - response.payload = Grpc_Testing_Payload.with { payload in - payload.body = Data(repeating: 0, count: Int(responseParameter.size)) - } - } - try await writer.write(response) - // We convert the `intervalUs` value from microseconds to nanoseconds. - try await Task.sleep(nanoseconds: UInt64(responseParameter.intervalUs) * 1000) - } - return trailingMetadata - } - } - - /// Server implements `streamingInputCall` which upon half close immediately returns a - /// `StreamingInputCallResponse` where `aggregatedPayloadSize` is the sum of all request payload - /// bodies received. - public func streamingInputCall( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Single { - let isRequestCompressed = - request.metadata["grpc-encoding"].filter({ $0 != "identity" }).count > 0 - var aggregatedPayloadSize = 0 - - for try await message in request.messages { - // We can't validate messages at the wire-encoding layer (i.e. where the compression byte is - // set), so we have to check via the encoding header. Note that it is possible for the header - // to be set and for the message to not be compressed. - if message.expectCompressed.value, !isRequestCompressed { - throw RPCError( - code: .invalidArgument, - message: "Expected compressed request, but 'grpc-encoding' was missing" - ) - } - - aggregatedPayloadSize += message.payload.body.count - } - - let responseMessage = Grpc_Testing_StreamingInputCallResponse.with { - $0.aggregatedPayloadSize = Int32(aggregatedPayloadSize) - } - - let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() - return ServerResponse.Single( - message: responseMessage, - metadata: initialMetadata, - trailingMetadata: trailingMetadata - ) - } - - /// Server implements `fullDuplexCall` by replying, in order, with one - /// `StreamingOutputCallResponse` for each `ResponseParameter`s in each - /// `StreamingOutputCallRequest`. Each `StreamingOutputCallResponse` should have a payload body - /// of size `ResponseParameter.size` bytes, as specified by its respective `ResponseParameter`s. - /// After receiving half close and sending all responses, it closes with OK. - public func fullDuplexCall( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - let (initialMetadata, trailingMetadata) = request.metadata.makeInitialAndTrailingMetadata() - return ServerResponse.Stream(metadata: initialMetadata) { writer in - for try await message in request.messages { - // If a request message has a responseStatus set, the server should return that status. - // If the code is an error code, the server will throw an error containing that code - // and the message set in the responseStatus. - // If the code is `ok`, the server will automatically send back an `ok` status with the response. - if message.responseStatus.isInitialized { - guard let code = Status.Code(rawValue: Int(message.responseStatus.code)) else { - throw RPCError(code: .invalidArgument, message: "The response status code is invalid.") - } - - let status = Status(code: code, message: message.responseStatus.message) - if let error = RPCError(status: status) { - throw error - } - } - - for responseParameter in message.responseParameters { - let response = Grpc_Testing_StreamingOutputCallResponse.with { response in - response.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: Int(responseParameter.size)) - } - } - try await writer.write(response) - } - } - return trailingMetadata - } - } - - /// This is not implemented as it is not described in the specification. - /// - /// See: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md - public func halfDuplexCall( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - throw RPCError(code: .unimplemented, message: "The RPC is not implemented.") - } -} - -extension Metadata { - fileprivate func makeInitialAndTrailingMetadata() -> (Metadata, Metadata) { - var initialMetadata = Metadata() - var trailingMetadata = Metadata() - for value in self[stringValues: "x-grpc-test-echo-initial"] { - initialMetadata.addString(value, forKey: "x-grpc-test-echo-initial") - } - for value in self[binaryValues: "x-grpc-test-echo-trailing-bin"] { - trailingMetadata.addBinary(value, forKey: "x-grpc-test-echo-trailing-bin") - } - - return (initialMetadata, trailingMetadata) - } -} diff --git a/Sources/Services/Health/Generated/health.grpc.swift b/Sources/Services/Health/Generated/health.grpc.swift deleted file mode 100644 index a2f625c74..000000000 --- a/Sources/Services/Health/Generated/health.grpc.swift +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright 2015 The gRPC Authors -// -// 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. - -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: health.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -package import GRPCCore -internal import GRPCProtobuf - -package enum Grpc_Health_V1_Health { - package static let descriptor = GRPCCore.ServiceDescriptor.grpc_health_v1_Health - package enum Method { - package enum Check { - package typealias Input = Grpc_Health_V1_HealthCheckRequest - package typealias Output = Grpc_Health_V1_HealthCheckResponse - package static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Health_V1_Health.descriptor.fullyQualifiedService, - method: "Check" - ) - } - package enum Watch { - package typealias Input = Grpc_Health_V1_HealthCheckRequest - package typealias Output = Grpc_Health_V1_HealthCheckResponse - package static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Health_V1_Health.descriptor.fullyQualifiedService, - method: "Watch" - ) - } - package static let descriptors: [GRPCCore.MethodDescriptor] = [ - Check.descriptor, - Watch.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = Grpc_Health_V1_HealthStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = Grpc_Health_V1_HealthServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = Grpc_Health_V1_HealthClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = Grpc_Health_V1_HealthClient -} - -extension GRPCCore.ServiceDescriptor { - package static let grpc_health_v1_Health = Self( - package: "grpc.health.v1", - service: "Health" - ) -} - -/// Health is gRPC's mechanism for checking whether a server is able to handle -/// RPCs. Its semantics are documented in -/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package protocol Grpc_Health_V1_HealthStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Check gets the health of the specified service. If the requested service - /// is unknown, the call will fail with status NOT_FOUND. If the caller does - /// not specify a service name, the server should respond with its overall - /// health status. - /// - /// Clients should set a deadline when calling Check, and can declare the - /// server unhealthy if they do not receive a timely response. - /// - /// Check implementations should be idempotent and side effect free. - func check( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Performs a watch for the serving status of the requested service. - /// The server will immediately send back a message indicating the current - /// serving status. It will then subsequently send a new message whenever - /// the service's serving status changes. - /// - /// If the requested service is unknown when the call is received, the - /// server will send a message setting the serving status to - /// SERVICE_UNKNOWN but will *not* terminate the call. If at some - /// future point, the serving status of the service becomes known, the - /// server will send a new message with the service's serving status. - /// - /// If the call terminates with status UNIMPLEMENTED, then clients - /// should assume this method is not supported and should not retry the - /// call. If the call terminates with any other status (including OK), - /// clients should retry the call with appropriate exponential backoff. - func watch( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Health_V1_Health.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Grpc_Health_V1_Health.Method.Check.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.check( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Health_V1_Health.Method.Watch.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.watch( - request: request, - context: context - ) - } - ) - } -} - -/// Health is gRPC's mechanism for checking whether a server is able to handle -/// RPCs. Its semantics are documented in -/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package protocol Grpc_Health_V1_HealthServiceProtocol: Grpc_Health_V1_Health.StreamingServiceProtocol { - /// Check gets the health of the specified service. If the requested service - /// is unknown, the call will fail with status NOT_FOUND. If the caller does - /// not specify a service name, the server should respond with its overall - /// health status. - /// - /// Clients should set a deadline when calling Check, and can declare the - /// server unhealthy if they do not receive a timely response. - /// - /// Check implementations should be idempotent and side effect free. - func check( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// Performs a watch for the serving status of the requested service. - /// The server will immediately send back a message indicating the current - /// serving status. It will then subsequently send a new message whenever - /// the service's serving status changes. - /// - /// If the requested service is unknown when the call is received, the - /// server will send a message setting the serving status to - /// SERVICE_UNKNOWN but will *not* terminate the call. If at some - /// future point, the serving status of the service becomes known, the - /// server will send a new message with the service's serving status. - /// - /// If the call terminates with status UNIMPLEMENTED, then clients - /// should assume this method is not supported and should not retry the - /// call. If the call terminates with any other status (including OK), - /// clients should retry the call with appropriate exponential backoff. - func watch( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Partial conformance to `Grpc_Health_V1_HealthStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Health_V1_Health.ServiceProtocol { - package func check( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.check( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - package func watch( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.watch( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return response - } -} - -/// Health is gRPC's mechanism for checking whether a server is able to handle -/// RPCs. Its semantics are documented in -/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package protocol Grpc_Health_V1_HealthClientProtocol: Sendable { - /// Check gets the health of the specified service. If the requested service - /// is unknown, the call will fail with status NOT_FOUND. If the caller does - /// not specify a service name, the server should respond with its overall - /// health status. - /// - /// Clients should set a deadline when calling Check, and can declare the - /// server unhealthy if they do not receive a timely response. - /// - /// Check implementations should be idempotent and side effect free. - func check( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - /// Performs a watch for the serving status of the requested service. - /// The server will immediately send back a message indicating the current - /// serving status. It will then subsequently send a new message whenever - /// the service's serving status changes. - /// - /// If the requested service is unknown when the call is received, the - /// server will send a message setting the serving status to - /// SERVICE_UNKNOWN but will *not* terminate the call. If at some - /// future point, the serving status of the service becomes known, the - /// server will send a new message with the service's serving status. - /// - /// If the call terminates with status UNIMPLEMENTED, then clients - /// should assume this method is not supported and should not retry the - /// call. If the call terminates with any other status (including OK), - /// clients should retry the call with appropriate exponential backoff. - func watch( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Health_V1_Health.ClientProtocol { - package func check( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.check( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - package func watch( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.watch( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Health_V1_Health.ClientProtocol { - /// Check gets the health of the specified service. If the requested service - /// is unknown, the call will fail with status NOT_FOUND. If the caller does - /// not specify a service name, the server should respond with its overall - /// health status. - /// - /// Clients should set a deadline when calling Check, and can declare the - /// server unhealthy if they do not receive a timely response. - /// - /// Check implementations should be idempotent and side effect free. - package func check( - _ message: Grpc_Health_V1_HealthCheckRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.check( - request: request, - options: options, - handleResponse - ) - } - - /// Performs a watch for the serving status of the requested service. - /// The server will immediately send back a message indicating the current - /// serving status. It will then subsequently send a new message whenever - /// the service's serving status changes. - /// - /// If the requested service is unknown when the call is received, the - /// server will send a message setting the serving status to - /// SERVICE_UNKNOWN but will *not* terminate the call. If at some - /// future point, the serving status of the service becomes known, the - /// server will send a new message with the service's serving status. - /// - /// If the call terminates with status UNIMPLEMENTED, then clients - /// should assume this method is not supported and should not retry the - /// call. If the call terminates with any other status (including OK), - /// clients should retry the call with appropriate exponential backoff. - package func watch( - _ message: Grpc_Health_V1_HealthCheckRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.watch( - request: request, - options: options, - handleResponse - ) - } -} - -/// Health is gRPC's mechanism for checking whether a server is able to handle -/// RPCs. Its semantics are documented in -/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -package struct Grpc_Health_V1_HealthClient: Grpc_Health_V1_Health.ClientProtocol { - private let client: GRPCCore.GRPCClient - - package init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Check gets the health of the specified service. If the requested service - /// is unknown, the call will fail with status NOT_FOUND. If the caller does - /// not specify a service name, the server should respond with its overall - /// health status. - /// - /// Clients should set a deadline when calling Check, and can declare the - /// server unhealthy if they do not receive a timely response. - /// - /// Check implementations should be idempotent and side effect free. - package func check( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Health_V1_Health.Method.Check.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Performs a watch for the serving status of the requested service. - /// The server will immediately send back a message indicating the current - /// serving status. It will then subsequently send a new message whenever - /// the service's serving status changes. - /// - /// If the requested service is unknown when the call is received, the - /// server will send a message setting the serving status to - /// SERVICE_UNKNOWN but will *not* terminate the call. If at some - /// future point, the serving status of the service becomes known, the - /// server will send a new message with the service's serving status. - /// - /// If the call terminates with status UNIMPLEMENTED, then clients - /// should assume this method is not supported and should not retry the - /// call. If the call terminates with any other status (including OK), - /// clients should retry the call with appropriate exponential backoff. - package func watch( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Grpc_Health_V1_Health.Method.Watch.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} \ No newline at end of file diff --git a/Sources/Services/Health/Generated/health.pb.swift b/Sources/Services/Health/Generated/health.pb.swift deleted file mode 100644 index ea2cde5c6..000000000 --- a/Sources/Services/Health/Generated/health.pb.swift +++ /dev/null @@ -1,183 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: health.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 The gRPC Authors -// -// 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. - -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto - -package import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -package struct Grpc_Health_V1_HealthCheckRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - package var service: String = String() - - package var unknownFields = SwiftProtobuf.UnknownStorage() - - package init() {} -} - -package struct Grpc_Health_V1_HealthCheckResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - package var status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown - - package var unknownFields = SwiftProtobuf.UnknownStorage() - - package enum ServingStatus: SwiftProtobuf.Enum, Swift.CaseIterable { - package typealias RawValue = Int - case unknown // = 0 - case serving // = 1 - case notServing // = 2 - - /// Used only by the Watch method. - case serviceUnknown // = 3 - case UNRECOGNIZED(Int) - - package init() { - self = .unknown - } - - package init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .serving - case 2: self = .notServing - case 3: self = .serviceUnknown - default: self = .UNRECOGNIZED(rawValue) - } - } - - package var rawValue: Int { - switch self { - case .unknown: return 0 - case .serving: return 1 - case .notServing: return 2 - case .serviceUnknown: return 3 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - package static let allCases: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ - .unknown, - .serving, - .notServing, - .serviceUnknown, - ] - - } - - package init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.health.v1" - -extension Grpc_Health_V1_HealthCheckRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - package static let protoMessageName: String = _protobuf_package + ".HealthCheckRequest" - package static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "service"), - ] - - package mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.service) }() - default: break - } - } - } - - package func traverse(visitor: inout V) throws { - if !self.service.isEmpty { - try visitor.visitSingularStringField(value: self.service, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - package static func ==(lhs: Grpc_Health_V1_HealthCheckRequest, rhs: Grpc_Health_V1_HealthCheckRequest) -> Bool { - if lhs.service != rhs.service {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Health_V1_HealthCheckResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - package static let protoMessageName: String = _protobuf_package + ".HealthCheckResponse" - package static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "status"), - ] - - package mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.status) }() - default: break - } - } - } - - package func traverse(visitor: inout V) throws { - if self.status != .unknown { - try visitor.visitSingularEnumField(value: self.status, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - package static func ==(lhs: Grpc_Health_V1_HealthCheckResponse, rhs: Grpc_Health_V1_HealthCheckResponse) -> Bool { - if lhs.status != rhs.status {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Health_V1_HealthCheckResponse.ServingStatus: SwiftProtobuf._ProtoNameProviding { - package static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "SERVING"), - 2: .same(proto: "NOT_SERVING"), - 3: .same(proto: "SERVICE_UNKNOWN"), - ] -} diff --git a/Sources/Services/Health/Health.swift b/Sources/Services/Health/Health.swift deleted file mode 100644 index 641de83dd..000000000 --- a/Sources/Services/Health/Health.swift +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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. - */ - -public import GRPCCore - -/// ``Health`` is gRPC’s mechanism for checking whether a server is able to handle RPCs. Its semantics are documented in -/// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -/// -/// `Health` initializes a new ``Health/Service-swift.struct`` and ``Health/Provider-swift.struct``. -/// - `Health.Service` implements the Health service from the `grpc.health.v1` package and can be registered with a server -/// like any other service. -/// - `Health.Provider` provides status updates to `Health.Service`. `Health.Service` doesn't know about the other -/// services running on a server so it must be provided with status updates via `Health.Provider`. To make specifying the service -/// being updated easier, the generated code for services includes an extension to `ServiceDescriptor`. -/// -/// The following shows an example of initializing a Health service and updating the status of the `Foo` service in the `bar` package. -/// -/// ```swift -/// let health = Health() -/// let server = GRPCServer( -/// transport: transport, -/// services: [health.service, FooService()] -/// ) -/// -/// health.provider.updateStatus( -/// .serving, -/// forService: .bar_Foo -/// ) -/// ``` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct Health: Sendable { - /// An implementation of the `grpc.health.v1.Health` service. - public let service: Health.Service - - /// Provides status updates to the Health service. - public let provider: Health.Provider - - /// Constructs a new ``Health``, initializing a ``Health/Service-swift.struct`` and a - /// ``Health/Provider-swift.struct``. - public init() { - let healthService = HealthService() - - self.service = Health.Service(healthService: healthService) - self.provider = Health.Provider(healthService: healthService) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Health { - /// An implementation of the `grpc.health.v1.Health` service. - public struct Service: RegistrableRPCService, Sendable { - private let healthService: HealthService - - public func registerMethods(with router: inout RPCRouter) { - self.healthService.registerMethods(with: &router) - } - - fileprivate init(healthService: HealthService) { - self.healthService = healthService - } - } - - /// Provides status updates to ``Health/Service-swift.struct``. - public struct Provider: Sendable { - private let healthService: HealthService - - /// Updates the status of a service. - /// - /// - Parameters: - /// - status: The status of the service. - /// - service: The description of the service. - public func updateStatus( - _ status: ServingStatus, - forService service: ServiceDescriptor - ) { - self.healthService.updateStatus( - Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), - forService: service.fullyQualifiedService - ) - } - - /// Updates the status of a service. - /// - /// - Parameters: - /// - status: The status of the service. - /// - service: The fully qualified service name in the format: - /// - "package.service": if the service is part of a package. For example, "helloworld.Greeter". - /// - "service": if the service is not part of a package. For example, "Greeter". - public func updateStatus( - _ status: ServingStatus, - forService service: String - ) { - self.healthService.updateStatus( - Grpc_Health_V1_HealthCheckResponse.ServingStatus(status), - forService: service - ) - } - - fileprivate init(healthService: HealthService) { - self.healthService = healthService - } - } -} - -extension Grpc_Health_V1_HealthCheckResponse.ServingStatus { - package init(_ status: ServingStatus) { - switch status.value { - case .serving: - self = .serving - case .notServing: - self = .notServing - } - } -} diff --git a/Sources/Services/Health/HealthService.swift b/Sources/Services/Health/HealthService.swift deleted file mode 100644 index 362e707f2..000000000 --- a/Sources/Services/Health/HealthService.swift +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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. - */ - -internal import GRPCCore -private import Synchronization - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct HealthService: Grpc_Health_V1_HealthServiceProtocol { - private let state = HealthService.State() - - func check( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - let service = request.message.service - - guard let status = self.state.currentStatus(ofService: service) else { - throw RPCError(code: .notFound, message: "Requested service unknown.") - } - - var response = Grpc_Health_V1_HealthCheckResponse() - response.status = status - - return ServerResponse.Single(message: response) - } - - func watch( - request: ServerRequest.Single, - context: ServerContext - ) async -> ServerResponse.Stream { - let service = request.message.service - let statuses = AsyncStream.makeStream(of: Grpc_Health_V1_HealthCheckResponse.ServingStatus.self) - - self.state.addContinuation(statuses.continuation, forService: service) - - return ServerResponse.Stream(of: Grpc_Health_V1_HealthCheckResponse.self) { writer in - var response = Grpc_Health_V1_HealthCheckResponse() - - for await status in statuses.stream { - response.status = status - try await writer.write(response) - } - - return [:] - } - } - - func updateStatus( - _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, - forService service: String - ) { - self.state.updateStatus(status, forService: service) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension HealthService { - private final class State: Sendable { - // The state of each service keyed by the fully qualified service name. - private let lockedStorage = Mutex([String: ServiceState]()) - - fileprivate func currentStatus( - ofService service: String - ) -> Grpc_Health_V1_HealthCheckResponse.ServingStatus? { - return self.lockedStorage.withLock { $0[service]?.currentStatus } - } - - fileprivate func updateStatus( - _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus, - forService service: String - ) { - self.lockedStorage.withLock { storage in - storage[service, default: ServiceState(status: status)].updateStatus(status) - } - } - - fileprivate func addContinuation( - _ continuation: AsyncStream.Continuation, - forService service: String - ) { - self.lockedStorage.withLock { storage in - storage[service, default: ServiceState(status: .serviceUnknown)] - .addContinuation(continuation) - } - } - } - - // Encapsulates the current status of a service and the continuations of its watch streams. - private struct ServiceState: Sendable { - private(set) var currentStatus: Grpc_Health_V1_HealthCheckResponse.ServingStatus - private var continuations: - [AsyncStream.Continuation] - - fileprivate mutating func updateStatus( - _ status: Grpc_Health_V1_HealthCheckResponse.ServingStatus - ) { - guard status != self.currentStatus else { - return - } - - self.currentStatus = status - - for continuation in self.continuations { - continuation.yield(status) - } - } - - fileprivate mutating func addContinuation( - _ continuation: AsyncStream.Continuation - ) { - self.continuations.append(continuation) - continuation.yield(self.currentStatus) - } - - fileprivate init(status: Grpc_Health_V1_HealthCheckResponse.ServingStatus = .unknown) { - self.currentStatus = status - self.continuations = [] - } - } -} diff --git a/Sources/Services/Health/ServingStatus.swift b/Sources/Services/Health/ServingStatus.swift deleted file mode 100644 index cc0fd5b15..000000000 --- a/Sources/Services/Health/ServingStatus.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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. - */ - -/// The status of a service. -/// -/// - ``ServingStatus/serving`` indicates that a service is healthy. -/// - ``ServingStatus/notServing`` indicates that a service is unhealthy. -public struct ServingStatus: Sendable, Hashable { - internal enum Value: Sendable, Hashable { - case serving - case notServing - } - - /// A status indicating that a service is healthy. - public static let serving = ServingStatus(.serving) - - /// A status indicating that a service unhealthy. - public static let notServing = ServingStatus(.notServing) - - internal var value: Value - - private init(_ value: Value) { - self.value = value - } -} diff --git a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift b/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift deleted file mode 100644 index 15e1ba0fa..000000000 --- a/Sources/interoperability-tests/InteroperabilityTestsExecutable.swift +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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 ArgumentParser -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import InteroperabilityTests -import NIOPosix - -@main -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct InteroperabilityTestsExecutable: AsyncParsableCommand { - static let configuration = CommandConfiguration( - abstract: "gRPC Swift Interoperability Runner", - subcommands: [StartServer.self, ListTests.self, RunTests.self] - ) - - struct StartServer: AsyncParsableCommand { - static let configuration = CommandConfiguration( - abstract: "Start the gRPC Swift interoperability test server." - ) - - @Option(help: "The port to listen on for new connections") - var port: Int - - func run() async throws { - let server = GRPCServer( - transport: .http2NIOPosix( - address: .ipv4(host: "0.0.0.0", port: self.port), - config: .defaults(transportSecurity: .plaintext) { - $0.compression.enabledAlgorithms = .all - } - ), - services: [TestService()] - ) - try await server.serve() - } - } - - struct ListTests: ParsableCommand { - static let configuration = CommandConfiguration( - abstract: "List all interoperability test names." - ) - - func run() throws { - for testCase in InteroperabilityTestCase.allCases { - print(testCase.name) - } - } - } - - struct RunTests: AsyncParsableCommand { - static let configuration = CommandConfiguration( - abstract: """ - Run gRPC interoperability tests using a gRPC Swift client. - You can specify a test name as an argument to run a single test. - If no test name is given, all interoperability tests will be run. - """ - ) - - @Option(help: "The host the server is running on") - var host: String - - @Option(help: "The port to connect to") - var port: Int - - @Argument(help: "The name of the tests to run. If none, all tests will be run.") - var testNames: [String] = InteroperabilityTestCase.allCases.map { $0.name } - - func run() async throws { - let client = try self.buildClient(host: self.host, port: self.port) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await client.run() - } - - for testName in testNames { - guard let testCase = InteroperabilityTestCase(rawValue: testName) else { - print(InteroperabilityTestError.testNotFound(name: testName)) - continue - } - await self.runTest(testCase, using: client) - } - - client.beginGracefulShutdown() - } - } - - private func buildClient(host: String, port: Int) throws -> GRPCClient { - let serviceConfig = ServiceConfig(loadBalancingConfig: [.roundRobin]) - return GRPCClient( - transport: try .http2NIOPosix( - target: .ipv4(host: host, port: port), - config: .defaults(transportSecurity: .plaintext) { - $0.compression.enabledAlgorithms = .all - }, - serviceConfig: serviceConfig - ) - ) - } - - private func runTest( - _ testCase: InteroperabilityTestCase, - using client: GRPCClient - ) async { - print("Running '\(testCase.name)' ... ", terminator: "") - do { - try await testCase.makeTest().run(client: client) - print("PASSED") - } catch { - print("FAILED\n" + String(describing: InteroperabilityTestError.testFailed(cause: error))) - } - } - } -} - -enum InteroperabilityTestError: Error, CustomStringConvertible { - case testNotFound(name: String) - case testFailed(cause: any Error) - - var description: String { - switch self { - case .testNotFound(let name): - return "Test \"\(name)\" not found." - case .testFailed(let cause): - return "Test failed with error: \(String(describing: cause))" - } - } -} diff --git a/Sources/performance-worker/BenchmarkClient.swift b/Sources/performance-worker/BenchmarkClient.swift deleted file mode 100644 index 57afa894f..000000000 --- a/Sources/performance-worker/BenchmarkClient.swift +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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 Foundation -import GRPCCore -import NIOConcurrencyHelpers -import Synchronization - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class BenchmarkClient: Sendable { - private let _isShuttingDown = Atomic(false) - - /// Whether the benchmark client is shutting down. Used to control when to stop sending messages - /// or creating new RPCs. - private var isShuttingDown: Bool { - self._isShuttingDown.load(ordering: .relaxed) - } - - /// The underlying client. - private let client: GRPCClient - - /// The number of concurrent RPCs to run. - private let concurrentRPCs: Int - - /// The type of RPC to make against the server. - private let rpcType: RPCType - - /// The max number of messages to send on a stream before replacing the RPC with a new one. A - /// value of zero means there is no limit. - private let messagesPerStream: Int - private var noMessageLimit: Bool { self.messagesPerStream == 0 } - - /// The message to send for all RPC types to the server. - private let message: Grpc_Testing_SimpleRequest - - /// Per RPC stats. - private let rpcStats: NIOLockedValueBox - - init( - client: GRPCClient, - concurrentRPCs: Int, - rpcType: RPCType, - messagesPerStream: Int, - protoParams: Grpc_Testing_SimpleProtoParams, - histogramParams: Grpc_Testing_HistogramParams? - ) { - self.client = client - self.concurrentRPCs = concurrentRPCs - self.messagesPerStream = messagesPerStream - self.rpcType = rpcType - self.message = .with { - $0.responseSize = protoParams.respSize - $0.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: Int(protoParams.reqSize)) - } - } - - let histogram: RPCStats.LatencyHistogram - if let histogramParams = histogramParams { - histogram = RPCStats.LatencyHistogram( - resolution: histogramParams.resolution, - maxBucketStart: histogramParams.maxPossible - ) - } else { - histogram = RPCStats.LatencyHistogram() - } - - self.rpcStats = NIOLockedValueBox(RPCStats(latencyHistogram: histogram)) - } - - enum RPCType { - case unary - case streaming - } - - internal var currentStats: RPCStats { - return self.rpcStats.withLockedValue { stats in - return stats - } - } - - internal func run() async throws { - let benchmarkClient = Grpc_Testing_BenchmarkServiceClient(wrapping: self.client) - return try await withThrowingTaskGroup(of: Void.self) { clientGroup in - // Start the client. - clientGroup.addTask { - try await self.client.run() - } - - try await withThrowingTaskGroup(of: Void.self) { rpcsGroup in - // Start one task for each concurrent RPC and keep looping in that task until indicated - // to stop. - for _ in 0 ..< self.concurrentRPCs { - rpcsGroup.addTask { - while !self.isShuttingDown { - switch self.rpcType { - case .unary: - await self.unary(benchmark: benchmarkClient) - - case .streaming: - await self.streaming(benchmark: benchmarkClient) - } - } - } - } - - try await rpcsGroup.waitForAll() - } - - self.client.beginGracefulShutdown() - try await clientGroup.next() - } - } - - private func record(latencyNanos: Double, errorCode: RPCError.Code?) { - self.rpcStats.withLockedValue { stats in - stats.latencyHistogram.record(latencyNanos) - if let errorCode = errorCode { - stats.requestResultCount[errorCode, default: 0] += 1 - } - } - } - - private func record(errorCode: RPCError.Code) { - self.rpcStats.withLockedValue { stats in - stats.requestResultCount[errorCode, default: 0] += 1 - } - } - - private func timeIt( - _ body: () async throws -> R - ) async rethrows -> (R, nanoseconds: Double) { - let startTime = DispatchTime.now().uptimeNanoseconds - let result = try await body() - let endTime = DispatchTime.now().uptimeNanoseconds - return (result, nanoseconds: Double(endTime - startTime)) - } - - private func unary(benchmark: Grpc_Testing_BenchmarkServiceClient) async { - let (errorCode, nanoseconds): (RPCError.Code?, Double) = await self.timeIt { - do { - try await benchmark.unaryCall(request: ClientRequest.Single(message: self.message)) { - _ = try $0.message - } - return nil - } catch let error as RPCError { - return error.code - } catch { - return .unknown - } - } - - self.record(latencyNanos: nanoseconds, errorCode: errorCode) - } - - private func streaming(benchmark: Grpc_Testing_BenchmarkServiceClient) async { - // Streaming RPCs ping-pong messages back and forth. To achieve this the response message - // stream is sent to the request closure, and the request closure indicates the outcome back - // to the response handler to keep the RPC alive for the appropriate amount of time. - let status = AsyncStream.makeStream(of: RPCError.self) - let response = AsyncStream.makeStream( - of: RPCAsyncSequence.self - ) - - let request = ClientRequest.Stream(of: Grpc_Testing_SimpleRequest.self) { writer in - defer { status.continuation.finish() } - - // The time at which the last message was sent. - var lastMessageSendTime = DispatchTime.now() - try await writer.write(self.message) - - // Wait for the response stream. - var iterator = response.stream.makeAsyncIterator() - guard let responses = await iterator.next() else { - throw RPCError(code: .internalError, message: "") - } - - // Record the first latency. - let now = DispatchTime.now() - let nanos = now.uptimeNanoseconds - lastMessageSendTime.uptimeNanoseconds - lastMessageSendTime = now - self.record(latencyNanos: Double(nanos), errorCode: nil) - - // Now start looping. Only stop when the max messages per stream is hit or told to stop. - var responseIterator = responses.makeAsyncIterator() - var messagesSent = 1 - - while !self.isShuttingDown && (self.noMessageLimit || messagesSent < self.messagesPerStream) { - messagesSent += 1 - do { - if try await responseIterator.next() != nil { - let now = DispatchTime.now() - let nanos = now.uptimeNanoseconds - lastMessageSendTime.uptimeNanoseconds - lastMessageSendTime = now - self.record(latencyNanos: Double(nanos), errorCode: nil) - try await writer.write(self.message) - } else { - break - } - } catch let error as RPCError { - status.continuation.yield(error) - break - } catch { - status.continuation.yield(RPCError(code: .unknown, message: "")) - break - } - } - } - - do { - try await benchmark.streamingCall(request: request) { - response.continuation.yield($0.messages) - response.continuation.finish() - for await errorCode in status.stream { - throw errorCode - } - } - } catch let error as RPCError { - self.record(errorCode: error.code) - } catch { - self.record(errorCode: .unknown) - } - } - - internal func shutdown() { - self._isShuttingDown.store(true, ordering: .relaxed) - self.client.beginGracefulShutdown() - } -} diff --git a/Sources/performance-worker/BenchmarkService.swift b/Sources/performance-worker/BenchmarkService.swift deleted file mode 100644 index b73d46534..000000000 --- a/Sources/performance-worker/BenchmarkService.swift +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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 Synchronization - -import struct Foundation.Data - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class BenchmarkService: Grpc_Testing_BenchmarkService.ServiceProtocol { - /// Used to check if the server can be streaming responses. - private let working = Atomic(true) - - /// One request followed by one response. - /// The server returns a client payload with the size requested by the client. - func unaryCall( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - // Throw an error if the status is not `ok`. Otherwise, an `ok` status is automatically sent - // if the request is successful. - if request.message.responseStatus.isInitialized { - try self.checkOkStatus(request.message.responseStatus) - } - - return ServerResponse.Single( - message: .with { - $0.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: Int(request.message.responseSize)) - } - } - ) - } - - /// Repeated sequence of one request followed by one response. - /// The server returns a payload with the size requested by the client for each received message. - func streamingCall( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in - for try await message in request.messages { - if message.responseStatus.isInitialized { - try self.checkOkStatus(message.responseStatus) - } - - let responseMessage = Grpc_Testing_SimpleResponse.with { - $0.payload = Grpc_Testing_Payload.with { - $0.body = Data(count: Int(message.responseSize)) - } - } - - try await writer.write(responseMessage) - } - - return [:] - } - } - - /// Single-sided unbounded streaming from client to server. - /// The server returns a payload with the size requested by the client once the client does WritesDone. - func streamingFromClient( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Single { - var responseSize = 0 - for try await message in request.messages { - if message.responseStatus.isInitialized { - try self.checkOkStatus(message.responseStatus) - } - responseSize = Int(message.responseSize) - } - - return ServerResponse.Single( - message: .with { - $0.payload = .with { - $0.body = Data(count: responseSize) - } - } - ) - } - - /// Single-sided unbounded streaming from server to client. - /// The server repeatedly returns a payload with the size requested by the client. - func streamingFromServer( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Stream { - if request.message.responseStatus.isInitialized { - try self.checkOkStatus(request.message.responseStatus) - } - - let response = Grpc_Testing_SimpleResponse.with { - $0.payload = .with { - $0.body = Data(count: Int(request.message.responseSize)) - } - } - - return ServerResponse.Stream { writer in - while self.working.load(ordering: .relaxed) { - try await writer.write(response) - } - return [:] - } - } - - /// Two-sided unbounded streaming between server to client. - /// Both sides send the content of their own choice to the other. - func streamingBothWays( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - // The 100 size is used by the other implementations as well. - // We are using the same canned response size for all responses - // as it is allowed by the spec. - let response = Grpc_Testing_SimpleResponse.with { - $0.payload = .with { - $0.body = Data(count: 100) - } - } - - final class InboundStreamingSignal: Sendable { - private let _isStreaming: Atomic - - init() { - self._isStreaming = Atomic(true) - } - - var isStreaming: Bool { - self._isStreaming.load(ordering: .relaxed) - } - - func stop() { - self._isStreaming.store(false, ordering: .relaxed) - } - } - - // Marks if the inbound streaming is ongoing or finished. - let inbound = InboundStreamingSignal() - - return ServerResponse.Stream { writer in - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - for try await message in request.messages { - if message.responseStatus.isInitialized { - try self.checkOkStatus(message.responseStatus) - } - } - inbound.stop() - } - - group.addTask { - while inbound.isStreaming && self.working.load(ordering: .acquiring) { - try await writer.write(response) - } - } - - try await group.next() - group.cancelAll() - return [:] - } - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension BenchmarkService { - private func checkOkStatus(_ responseStatus: Grpc_Testing_EchoStatus) throws { - guard let code = Status.Code(rawValue: Int(responseStatus.code)) else { - throw RPCError(code: .invalidArgument, message: "The response status code is invalid.") - } - if let code = RPCError.Code(code) { - throw RPCError(code: code, message: responseStatus.message) - } - } -} diff --git a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift b/Sources/performance-worker/Generated/grpc_core_stats.pb.swift deleted file mode 100644 index e68cf193f..000000000 --- a/Sources/performance-worker/Generated/grpc_core_stats.pb.swift +++ /dev/null @@ -1,286 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: grpc/core/stats.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2017 gRPC authors. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct Grpc_Core_Bucket: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var start: Double = 0 - - var count: UInt64 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Core_Histogram: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var buckets: [Grpc_Core_Bucket] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Core_Metric: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var name: String = String() - - var value: Grpc_Core_Metric.OneOf_Value? = nil - - var count: UInt64 { - get { - if case .count(let v)? = value {return v} - return 0 - } - set {value = .count(newValue)} - } - - var histogram: Grpc_Core_Histogram { - get { - if case .histogram(let v)? = value {return v} - return Grpc_Core_Histogram() - } - set {value = .histogram(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum OneOf_Value: Equatable, Sendable { - case count(UInt64) - case histogram(Grpc_Core_Histogram) - - } - - init() {} -} - -struct Grpc_Core_Stats: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var metrics: [Grpc_Core_Metric] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.core" - -extension Grpc_Core_Bucket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Bucket" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "start"), - 2: .same(proto: "count"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.start) }() - case 2: try { try decoder.decodeSingularUInt64Field(value: &self.count) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.start.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.start, fieldNumber: 1) - } - if self.count != 0 { - try visitor.visitSingularUInt64Field(value: self.count, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Core_Bucket, rhs: Grpc_Core_Bucket) -> Bool { - if lhs.start != rhs.start {return false} - if lhs.count != rhs.count {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Core_Histogram: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Histogram" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "buckets"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.buckets) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.buckets.isEmpty { - try visitor.visitRepeatedMessageField(value: self.buckets, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Core_Histogram, rhs: Grpc_Core_Histogram) -> Bool { - if lhs.buckets != rhs.buckets {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Core_Metric: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Metric" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 10: .same(proto: "count"), - 11: .same(proto: "histogram"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 10: try { - var v: UInt64? - try decoder.decodeSingularUInt64Field(value: &v) - if let v = v { - if self.value != nil {try decoder.handleConflictingOneOf()} - self.value = .count(v) - } - }() - case 11: try { - var v: Grpc_Core_Histogram? - var hadOneofValue = false - if let current = self.value { - hadOneofValue = true - if case .histogram(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.value = .histogram(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - switch self.value { - case .count?: try { - guard case .count(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularUInt64Field(value: v, fieldNumber: 10) - }() - case .histogram?: try { - guard case .histogram(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Core_Metric, rhs: Grpc_Core_Metric) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Core_Stats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Stats" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "metrics"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.metrics) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.metrics.isEmpty { - try visitor.visitRepeatedMessageField(value: self.metrics, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Core_Stats, rhs: Grpc_Core_Stats) -> Bool { - if lhs.metrics != rhs.metrics {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift deleted file mode 100644 index d8b4cdc6b..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.grpc.swift +++ /dev/null @@ -1,617 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -/// An integration test service that covers all the method signature permutations -/// of unary/streaming requests/responses. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/benchmark_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -internal enum Grpc_Testing_BenchmarkService { - internal static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_BenchmarkService - internal enum Method { - internal enum UnaryCall { - internal typealias Input = Grpc_Testing_SimpleRequest - internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, - method: "UnaryCall" - ) - } - internal enum StreamingCall { - internal typealias Input = Grpc_Testing_SimpleRequest - internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, - method: "StreamingCall" - ) - } - internal enum StreamingFromClient { - internal typealias Input = Grpc_Testing_SimpleRequest - internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, - method: "StreamingFromClient" - ) - } - internal enum StreamingFromServer { - internal typealias Input = Grpc_Testing_SimpleRequest - internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, - method: "StreamingFromServer" - ) - } - internal enum StreamingBothWays { - internal typealias Input = Grpc_Testing_SimpleRequest - internal typealias Output = Grpc_Testing_SimpleResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_BenchmarkService.descriptor.fullyQualifiedService, - method: "StreamingBothWays" - ) - } - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - UnaryCall.descriptor, - StreamingCall.descriptor, - StreamingFromClient.descriptor, - StreamingFromServer.descriptor, - StreamingBothWays.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Grpc_Testing_BenchmarkServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Grpc_Testing_BenchmarkServiceServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Grpc_Testing_BenchmarkServiceClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Grpc_Testing_BenchmarkServiceClient -} - -extension GRPCCore.ServiceDescriptor { - internal static let grpc_testing_BenchmarkService = Self( - package: "grpc.testing", - service: "BenchmarkService" - ) -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Grpc_Testing_BenchmarkServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// One request followed by one response. - /// The server returns the client payload as-is. - func unaryCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - func streamingCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - func streamingFromServer( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - func streamingBothWays( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_BenchmarkService.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.unaryCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.streamingCall( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.streamingFromClient( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.streamingFromServer( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.streamingBothWays( - request: request, - context: context - ) - } - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Grpc_Testing_BenchmarkServiceServiceProtocol: Grpc_Testing_BenchmarkService.StreamingServiceProtocol { - /// One request followed by one response. - /// The server returns the client payload as-is. - func unaryCall( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - func streamingCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - func streamingFromServer( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - func streamingBothWays( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Partial conformance to `Grpc_Testing_BenchmarkServiceStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_BenchmarkService.ServiceProtocol { - internal func unaryCall( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unaryCall( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - internal func streamingFromClient( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingFromClient( - request: request, - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - internal func streamingFromServer( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.streamingFromServer( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return response - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Grpc_Testing_BenchmarkServiceClientProtocol: Sendable { - /// One request followed by one response. - /// The server returns the client payload as-is. - func unaryCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - func streamingCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - func streamingFromClient( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - func streamingFromServer( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - func streamingBothWays( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_BenchmarkService.ClientProtocol { - internal func unaryCall( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.unaryCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func streamingCall( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.streamingCall( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func streamingFromClient( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.streamingFromClient( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func streamingFromServer( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.streamingFromServer( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func streamingBothWays( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.streamingBothWays( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_BenchmarkService.ClientProtocol { - /// One request followed by one response. - /// The server returns the client payload as-is. - internal func unaryCall( - _ message: Grpc_Testing_SimpleRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.unaryCall( - request: request, - options: options, - handleResponse - ) - } - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - internal func streamingCall( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.streamingCall( - request: request, - options: options, - handleResponse - ) - } - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - internal func streamingFromClient( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.streamingFromClient( - request: request, - options: options, - handleResponse - ) - } - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - internal func streamingFromServer( - _ message: Grpc_Testing_SimpleRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.streamingFromServer( - request: request, - options: options, - handleResponse - ) - } - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - internal func streamingBothWays( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.streamingBothWays( - request: request, - options: options, - handleResponse - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Grpc_Testing_BenchmarkServiceClient: Grpc_Testing_BenchmarkService.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// One request followed by one response. - /// The server returns the client payload as-is. - internal func unaryCall( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Grpc_Testing_BenchmarkService.Method.UnaryCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Repeated sequence of one request followed by one response. - /// Should be called streaming ping-pong - /// The server returns the client payload as-is on each response - internal func streamingCall( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Grpc_Testing_BenchmarkService.Method.StreamingCall.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Single-sided unbounded streaming from client to server - /// The server returns the client payload as-is once the client does WritesDone - internal func streamingFromClient( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Grpc_Testing_BenchmarkService.Method.StreamingFromClient.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Single-sided unbounded streaming from server to client - /// The server repeatedly returns the client payload as-is - internal func streamingFromServer( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Grpc_Testing_BenchmarkService.Method.StreamingFromServer.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - /// Two-sided unbounded streaming between server to client - /// Both sides send the content of their own choice to the other - internal func streamingBothWays( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Grpc_Testing_BenchmarkService.Method.StreamingBothWays.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} \ No newline at end of file diff --git a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift deleted file mode 100644 index 268a0f868..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_benchmark_service.pb.swift +++ /dev/null @@ -1,28 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/benchmark_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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. - -/// An integration test service that covers all the method signature permutations -/// of unary/streaming requests/responses. - -// This file contained no messages, enums, or extensions. diff --git a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift b/Sources/performance-worker/Generated/grpc_testing_control.pb.swift deleted file mode 100644 index 777fff519..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_control.pb.swift +++ /dev/null @@ -1,2325 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/control.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -enum Grpc_Testing_ClientType: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - - /// Many languages support a basic distinction between using - /// sync or async client, and this allows the specification - case syncClient // = 0 - case asyncClient // = 1 - - /// used for some language-specific variants - case otherClient // = 2 - case callbackClient // = 3 - case UNRECOGNIZED(Int) - - init() { - self = .syncClient - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .syncClient - case 1: self = .asyncClient - case 2: self = .otherClient - case 3: self = .callbackClient - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .syncClient: return 0 - case .asyncClient: return 1 - case .otherClient: return 2 - case .callbackClient: return 3 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_ClientType] = [ - .syncClient, - .asyncClient, - .otherClient, - .callbackClient, - ] - -} - -enum Grpc_Testing_ServerType: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - case syncServer // = 0 - case asyncServer // = 1 - case asyncGenericServer // = 2 - - /// used for some language-specific variants - case otherServer // = 3 - case callbackServer // = 4 - case UNRECOGNIZED(Int) - - init() { - self = .syncServer - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .syncServer - case 1: self = .asyncServer - case 2: self = .asyncGenericServer - case 3: self = .otherServer - case 4: self = .callbackServer - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .syncServer: return 0 - case .asyncServer: return 1 - case .asyncGenericServer: return 2 - case .otherServer: return 3 - case .callbackServer: return 4 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_ServerType] = [ - .syncServer, - .asyncServer, - .asyncGenericServer, - .otherServer, - .callbackServer, - ] - -} - -enum Grpc_Testing_RpcType: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - case unary // = 0 - case streaming // = 1 - case streamingFromClient // = 2 - case streamingFromServer // = 3 - case streamingBothWays // = 4 - case UNRECOGNIZED(Int) - - init() { - self = .unary - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unary - case 1: self = .streaming - case 2: self = .streamingFromClient - case 3: self = .streamingFromServer - case 4: self = .streamingBothWays - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unary: return 0 - case .streaming: return 1 - case .streamingFromClient: return 2 - case .streamingFromServer: return 3 - case .streamingBothWays: return 4 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_RpcType] = [ - .unary, - .streaming, - .streamingFromClient, - .streamingFromServer, - .streamingBothWays, - ] - -} - -/// Parameters of poisson process distribution, which is a good representation -/// of activity coming in from independent identical stationary sources. -struct Grpc_Testing_PoissonParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The rate of arrivals (a.k.a. lambda parameter of the exp distribution). - var offeredLoad: Double = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Once an RPC finishes, immediately start a new one. -/// No configuration parameters needed. -struct Grpc_Testing_ClosedLoopParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_LoadParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var load: Grpc_Testing_LoadParams.OneOf_Load? = nil - - var closedLoop: Grpc_Testing_ClosedLoopParams { - get { - if case .closedLoop(let v)? = load {return v} - return Grpc_Testing_ClosedLoopParams() - } - set {load = .closedLoop(newValue)} - } - - var poisson: Grpc_Testing_PoissonParams { - get { - if case .poisson(let v)? = load {return v} - return Grpc_Testing_PoissonParams() - } - set {load = .poisson(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum OneOf_Load: Equatable, Sendable { - case closedLoop(Grpc_Testing_ClosedLoopParams) - case poisson(Grpc_Testing_PoissonParams) - - } - - init() {} -} - -/// presence of SecurityParams implies use of TLS -struct Grpc_Testing_SecurityParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var useTestCa: Bool = false - - var serverHostOverride: String = String() - - var credType: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_ChannelArg: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var name: String = String() - - var value: Grpc_Testing_ChannelArg.OneOf_Value? = nil - - var strValue: String { - get { - if case .strValue(let v)? = value {return v} - return String() - } - set {value = .strValue(newValue)} - } - - var intValue: Int32 { - get { - if case .intValue(let v)? = value {return v} - return 0 - } - set {value = .intValue(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum OneOf_Value: Equatable, Sendable { - case strValue(String) - case intValue(Int32) - - } - - init() {} -} - -struct Grpc_Testing_ClientConfig: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// List of targets to connect to. At least one target needs to be specified. - var serverTargets: [String] { - get {return _storage._serverTargets} - set {_uniqueStorage()._serverTargets = newValue} - } - - var clientType: Grpc_Testing_ClientType { - get {return _storage._clientType} - set {_uniqueStorage()._clientType = newValue} - } - - var securityParams: Grpc_Testing_SecurityParams { - get {return _storage._securityParams ?? Grpc_Testing_SecurityParams()} - set {_uniqueStorage()._securityParams = newValue} - } - /// Returns true if `securityParams` has been explicitly set. - var hasSecurityParams: Bool {return _storage._securityParams != nil} - /// Clears the value of `securityParams`. Subsequent reads from it will return its default value. - mutating func clearSecurityParams() {_uniqueStorage()._securityParams = nil} - - /// How many concurrent RPCs to start for each channel. - /// For synchronous client, use a separate thread for each outstanding RPC. - var outstandingRpcsPerChannel: Int32 { - get {return _storage._outstandingRpcsPerChannel} - set {_uniqueStorage()._outstandingRpcsPerChannel = newValue} - } - - /// Number of independent client channels to create. - /// i-th channel will connect to server_target[i % server_targets.size()] - var clientChannels: Int32 { - get {return _storage._clientChannels} - set {_uniqueStorage()._clientChannels = newValue} - } - - /// Only for async client. Number of threads to use to start/manage RPCs. - var asyncClientThreads: Int32 { - get {return _storage._asyncClientThreads} - set {_uniqueStorage()._asyncClientThreads = newValue} - } - - var rpcType: Grpc_Testing_RpcType { - get {return _storage._rpcType} - set {_uniqueStorage()._rpcType = newValue} - } - - /// The requested load for the entire client (aggregated over all the threads). - var loadParams: Grpc_Testing_LoadParams { - get {return _storage._loadParams ?? Grpc_Testing_LoadParams()} - set {_uniqueStorage()._loadParams = newValue} - } - /// Returns true if `loadParams` has been explicitly set. - var hasLoadParams: Bool {return _storage._loadParams != nil} - /// Clears the value of `loadParams`. Subsequent reads from it will return its default value. - mutating func clearLoadParams() {_uniqueStorage()._loadParams = nil} - - var payloadConfig: Grpc_Testing_PayloadConfig { - get {return _storage._payloadConfig ?? Grpc_Testing_PayloadConfig()} - set {_uniqueStorage()._payloadConfig = newValue} - } - /// Returns true if `payloadConfig` has been explicitly set. - var hasPayloadConfig: Bool {return _storage._payloadConfig != nil} - /// Clears the value of `payloadConfig`. Subsequent reads from it will return its default value. - mutating func clearPayloadConfig() {_uniqueStorage()._payloadConfig = nil} - - var histogramParams: Grpc_Testing_HistogramParams { - get {return _storage._histogramParams ?? Grpc_Testing_HistogramParams()} - set {_uniqueStorage()._histogramParams = newValue} - } - /// Returns true if `histogramParams` has been explicitly set. - var hasHistogramParams: Bool {return _storage._histogramParams != nil} - /// Clears the value of `histogramParams`. Subsequent reads from it will return its default value. - mutating func clearHistogramParams() {_uniqueStorage()._histogramParams = nil} - - /// Specify the cores we should run the client on, if desired - var coreList: [Int32] { - get {return _storage._coreList} - set {_uniqueStorage()._coreList = newValue} - } - - var coreLimit: Int32 { - get {return _storage._coreLimit} - set {_uniqueStorage()._coreLimit = newValue} - } - - /// If we use an OTHER_CLIENT client_type, this string gives more detail - var otherClientApi: String { - get {return _storage._otherClientApi} - set {_uniqueStorage()._otherClientApi = newValue} - } - - var channelArgs: [Grpc_Testing_ChannelArg] { - get {return _storage._channelArgs} - set {_uniqueStorage()._channelArgs = newValue} - } - - /// Number of threads that share each completion queue - var threadsPerCq: Int32 { - get {return _storage._threadsPerCq} - set {_uniqueStorage()._threadsPerCq = newValue} - } - - /// Number of messages on a stream before it gets finished/restarted - var messagesPerStream: Int32 { - get {return _storage._messagesPerStream} - set {_uniqueStorage()._messagesPerStream = newValue} - } - - /// Use coalescing API when possible. - var useCoalesceApi: Bool { - get {return _storage._useCoalesceApi} - set {_uniqueStorage()._useCoalesceApi = newValue} - } - - /// If 0, disabled. Else, specifies the period between gathering latency - /// medians in milliseconds. - var medianLatencyCollectionIntervalMillis: Int32 { - get {return _storage._medianLatencyCollectionIntervalMillis} - set {_uniqueStorage()._medianLatencyCollectionIntervalMillis = newValue} - } - - /// Number of client processes. 0 indicates no restriction. - var clientProcesses: Int32 { - get {return _storage._clientProcesses} - set {_uniqueStorage()._clientProcesses = newValue} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -struct Grpc_Testing_ClientStatus: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var stats: Grpc_Testing_ClientStats { - get {return _stats ?? Grpc_Testing_ClientStats()} - set {_stats = newValue} - } - /// Returns true if `stats` has been explicitly set. - var hasStats: Bool {return self._stats != nil} - /// Clears the value of `stats`. Subsequent reads from it will return its default value. - mutating func clearStats() {self._stats = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _stats: Grpc_Testing_ClientStats? = nil -} - -/// Request current stats -struct Grpc_Testing_Mark: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// if true, the stats will be reset after taking their snapshot. - var reset: Bool = false - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_ClientArgs: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var argtype: Grpc_Testing_ClientArgs.OneOf_Argtype? = nil - - var setup: Grpc_Testing_ClientConfig { - get { - if case .setup(let v)? = argtype {return v} - return Grpc_Testing_ClientConfig() - } - set {argtype = .setup(newValue)} - } - - var mark: Grpc_Testing_Mark { - get { - if case .mark(let v)? = argtype {return v} - return Grpc_Testing_Mark() - } - set {argtype = .mark(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum OneOf_Argtype: Equatable, Sendable { - case setup(Grpc_Testing_ClientConfig) - case mark(Grpc_Testing_Mark) - - } - - init() {} -} - -struct Grpc_Testing_ServerConfig: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var serverType: Grpc_Testing_ServerType = .syncServer - - var securityParams: Grpc_Testing_SecurityParams { - get {return _securityParams ?? Grpc_Testing_SecurityParams()} - set {_securityParams = newValue} - } - /// Returns true if `securityParams` has been explicitly set. - var hasSecurityParams: Bool {return self._securityParams != nil} - /// Clears the value of `securityParams`. Subsequent reads from it will return its default value. - mutating func clearSecurityParams() {self._securityParams = nil} - - /// Port on which to listen. Zero means pick unused port. - var port: Int32 = 0 - - /// Only for async server. Number of threads used to serve the requests. - var asyncServerThreads: Int32 = 0 - - /// Specify the number of cores to limit server to, if desired - var coreLimit: Int32 = 0 - - /// payload config, used in generic server. - /// Note this must NOT be used in proto (non-generic) servers. For proto servers, - /// 'response sizes' must be configured from the 'response_size' field of the - /// 'SimpleRequest' objects in RPC requests. - var payloadConfig: Grpc_Testing_PayloadConfig { - get {return _payloadConfig ?? Grpc_Testing_PayloadConfig()} - set {_payloadConfig = newValue} - } - /// Returns true if `payloadConfig` has been explicitly set. - var hasPayloadConfig: Bool {return self._payloadConfig != nil} - /// Clears the value of `payloadConfig`. Subsequent reads from it will return its default value. - mutating func clearPayloadConfig() {self._payloadConfig = nil} - - /// Specify the cores we should run the server on, if desired - var coreList: [Int32] = [] - - /// If we use an OTHER_SERVER client_type, this string gives more detail - var otherServerApi: String = String() - - /// Number of threads that share each completion queue - var threadsPerCq: Int32 = 0 - - /// Buffer pool size (no buffer pool specified if unset) - var resourceQuotaSize: Int32 = 0 - - var channelArgs: [Grpc_Testing_ChannelArg] = [] - - /// Number of server processes. 0 indicates no restriction. - var serverProcesses: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _securityParams: Grpc_Testing_SecurityParams? = nil - fileprivate var _payloadConfig: Grpc_Testing_PayloadConfig? = nil -} - -struct Grpc_Testing_ServerArgs: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var argtype: Grpc_Testing_ServerArgs.OneOf_Argtype? = nil - - var setup: Grpc_Testing_ServerConfig { - get { - if case .setup(let v)? = argtype {return v} - return Grpc_Testing_ServerConfig() - } - set {argtype = .setup(newValue)} - } - - var mark: Grpc_Testing_Mark { - get { - if case .mark(let v)? = argtype {return v} - return Grpc_Testing_Mark() - } - set {argtype = .mark(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum OneOf_Argtype: Equatable, Sendable { - case setup(Grpc_Testing_ServerConfig) - case mark(Grpc_Testing_Mark) - - } - - init() {} -} - -struct Grpc_Testing_ServerStatus: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var stats: Grpc_Testing_ServerStats { - get {return _stats ?? Grpc_Testing_ServerStats()} - set {_stats = newValue} - } - /// Returns true if `stats` has been explicitly set. - var hasStats: Bool {return self._stats != nil} - /// Clears the value of `stats`. Subsequent reads from it will return its default value. - mutating func clearStats() {self._stats = nil} - - /// the port bound by the server - var port: Int32 = 0 - - /// Number of cores available to the server - var cores: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _stats: Grpc_Testing_ServerStats? = nil -} - -struct Grpc_Testing_CoreRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_CoreResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Number of cores available on the server - var cores: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_Void: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A single performance scenario: input to qps_json_driver -struct Grpc_Testing_Scenario: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Human readable name for this scenario - var name: String { - get {return _storage._name} - set {_uniqueStorage()._name = newValue} - } - - /// Client configuration - var clientConfig: Grpc_Testing_ClientConfig { - get {return _storage._clientConfig ?? Grpc_Testing_ClientConfig()} - set {_uniqueStorage()._clientConfig = newValue} - } - /// Returns true if `clientConfig` has been explicitly set. - var hasClientConfig: Bool {return _storage._clientConfig != nil} - /// Clears the value of `clientConfig`. Subsequent reads from it will return its default value. - mutating func clearClientConfig() {_uniqueStorage()._clientConfig = nil} - - /// Number of clients to start for the test - var numClients: Int32 { - get {return _storage._numClients} - set {_uniqueStorage()._numClients = newValue} - } - - /// Server configuration - var serverConfig: Grpc_Testing_ServerConfig { - get {return _storage._serverConfig ?? Grpc_Testing_ServerConfig()} - set {_uniqueStorage()._serverConfig = newValue} - } - /// Returns true if `serverConfig` has been explicitly set. - var hasServerConfig: Bool {return _storage._serverConfig != nil} - /// Clears the value of `serverConfig`. Subsequent reads from it will return its default value. - mutating func clearServerConfig() {_uniqueStorage()._serverConfig = nil} - - /// Number of servers to start for the test - var numServers: Int32 { - get {return _storage._numServers} - set {_uniqueStorage()._numServers = newValue} - } - - /// Warmup period, in seconds - var warmupSeconds: Int32 { - get {return _storage._warmupSeconds} - set {_uniqueStorage()._warmupSeconds = newValue} - } - - /// Benchmark time, in seconds - var benchmarkSeconds: Int32 { - get {return _storage._benchmarkSeconds} - set {_uniqueStorage()._benchmarkSeconds = newValue} - } - - /// Number of workers to spawn locally (usually zero) - var spawnLocalWorkerCount: Int32 { - get {return _storage._spawnLocalWorkerCount} - set {_uniqueStorage()._spawnLocalWorkerCount = newValue} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -/// A set of scenarios to be run with qps_json_driver -struct Grpc_Testing_Scenarios: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var scenarios: [Grpc_Testing_Scenario] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Basic summary that can be computed from ClientStats and ServerStats -/// once the scenario has finished. -struct Grpc_Testing_ScenarioResultSummary: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Total number of operations per second over all clients. What is counted as 1 'operation' depends on the benchmark scenarios: - /// For unary benchmarks, an operation is processing of a single unary RPC. - /// For streaming benchmarks, an operation is processing of a single ping pong of request and response. - var qps: Double { - get {return _storage._qps} - set {_uniqueStorage()._qps = newValue} - } - - /// QPS per server core. - var qpsPerServerCore: Double { - get {return _storage._qpsPerServerCore} - set {_uniqueStorage()._qpsPerServerCore = newValue} - } - - /// The total server cpu load based on system time across all server processes, expressed as percentage of a single cpu core. - /// For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server - /// processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. - /// Same explanation for the total client cpu load below. - var serverSystemTime: Double { - get {return _storage._serverSystemTime} - set {_uniqueStorage()._serverSystemTime = newValue} - } - - /// The total server cpu load based on user time across all server processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - var serverUserTime: Double { - get {return _storage._serverUserTime} - set {_uniqueStorage()._serverUserTime = newValue} - } - - /// The total client cpu load based on system time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - var clientSystemTime: Double { - get {return _storage._clientSystemTime} - set {_uniqueStorage()._clientSystemTime = newValue} - } - - /// The total client cpu load based on user time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - var clientUserTime: Double { - get {return _storage._clientUserTime} - set {_uniqueStorage()._clientUserTime = newValue} - } - - /// X% latency percentiles (in nanoseconds) - var latency50: Double { - get {return _storage._latency50} - set {_uniqueStorage()._latency50 = newValue} - } - - var latency90: Double { - get {return _storage._latency90} - set {_uniqueStorage()._latency90 = newValue} - } - - var latency95: Double { - get {return _storage._latency95} - set {_uniqueStorage()._latency95 = newValue} - } - - var latency99: Double { - get {return _storage._latency99} - set {_uniqueStorage()._latency99 = newValue} - } - - var latency999: Double { - get {return _storage._latency999} - set {_uniqueStorage()._latency999 = newValue} - } - - /// server cpu usage percentage - var serverCpuUsage: Double { - get {return _storage._serverCpuUsage} - set {_uniqueStorage()._serverCpuUsage = newValue} - } - - /// Number of requests that succeeded/failed - var successfulRequestsPerSecond: Double { - get {return _storage._successfulRequestsPerSecond} - set {_uniqueStorage()._successfulRequestsPerSecond = newValue} - } - - var failedRequestsPerSecond: Double { - get {return _storage._failedRequestsPerSecond} - set {_uniqueStorage()._failedRequestsPerSecond = newValue} - } - - /// Number of polls called inside completion queue per request - var clientPollsPerRequest: Double { - get {return _storage._clientPollsPerRequest} - set {_uniqueStorage()._clientPollsPerRequest = newValue} - } - - var serverPollsPerRequest: Double { - get {return _storage._serverPollsPerRequest} - set {_uniqueStorage()._serverPollsPerRequest = newValue} - } - - /// Queries per CPU-sec over all servers or clients - var serverQueriesPerCpuSec: Double { - get {return _storage._serverQueriesPerCpuSec} - set {_uniqueStorage()._serverQueriesPerCpuSec = newValue} - } - - var clientQueriesPerCpuSec: Double { - get {return _storage._clientQueriesPerCpuSec} - set {_uniqueStorage()._clientQueriesPerCpuSec = newValue} - } - - /// Start and end time for the test scenario - var startTime: SwiftProtobuf.Google_Protobuf_Timestamp { - get {return _storage._startTime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} - set {_uniqueStorage()._startTime = newValue} - } - /// Returns true if `startTime` has been explicitly set. - var hasStartTime: Bool {return _storage._startTime != nil} - /// Clears the value of `startTime`. Subsequent reads from it will return its default value. - mutating func clearStartTime() {_uniqueStorage()._startTime = nil} - - var endTime: SwiftProtobuf.Google_Protobuf_Timestamp { - get {return _storage._endTime ?? SwiftProtobuf.Google_Protobuf_Timestamp()} - set {_uniqueStorage()._endTime = newValue} - } - /// Returns true if `endTime` has been explicitly set. - var hasEndTime: Bool {return _storage._endTime != nil} - /// Clears the value of `endTime`. Subsequent reads from it will return its default value. - mutating func clearEndTime() {_uniqueStorage()._endTime = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -/// Results of a single benchmark scenario. -struct Grpc_Testing_ScenarioResult: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Inputs used to run the scenario. - var scenario: Grpc_Testing_Scenario { - get {return _scenario ?? Grpc_Testing_Scenario()} - set {_scenario = newValue} - } - /// Returns true if `scenario` has been explicitly set. - var hasScenario: Bool {return self._scenario != nil} - /// Clears the value of `scenario`. Subsequent reads from it will return its default value. - mutating func clearScenario() {self._scenario = nil} - - /// Histograms from all clients merged into one histogram. - var latencies: Grpc_Testing_HistogramData { - get {return _latencies ?? Grpc_Testing_HistogramData()} - set {_latencies = newValue} - } - /// Returns true if `latencies` has been explicitly set. - var hasLatencies: Bool {return self._latencies != nil} - /// Clears the value of `latencies`. Subsequent reads from it will return its default value. - mutating func clearLatencies() {self._latencies = nil} - - /// Client stats for each client - var clientStats: [Grpc_Testing_ClientStats] = [] - - /// Server stats for each server - var serverStats: [Grpc_Testing_ServerStats] = [] - - /// Number of cores available to each server - var serverCores: [Int32] = [] - - /// An after-the-fact computed summary - var summary: Grpc_Testing_ScenarioResultSummary { - get {return _summary ?? Grpc_Testing_ScenarioResultSummary()} - set {_summary = newValue} - } - /// Returns true if `summary` has been explicitly set. - var hasSummary: Bool {return self._summary != nil} - /// Clears the value of `summary`. Subsequent reads from it will return its default value. - mutating func clearSummary() {self._summary = nil} - - /// Information on success or failure of each worker - var clientSuccess: [Bool] = [] - - var serverSuccess: [Bool] = [] - - /// Number of failed requests (one row per status code seen) - var requestResults: [Grpc_Testing_RequestResultCount] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _scenario: Grpc_Testing_Scenario? = nil - fileprivate var _latencies: Grpc_Testing_HistogramData? = nil - fileprivate var _summary: Grpc_Testing_ScenarioResultSummary? = nil -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_ClientType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "SYNC_CLIENT"), - 1: .same(proto: "ASYNC_CLIENT"), - 2: .same(proto: "OTHER_CLIENT"), - 3: .same(proto: "CALLBACK_CLIENT"), - ] -} - -extension Grpc_Testing_ServerType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "SYNC_SERVER"), - 1: .same(proto: "ASYNC_SERVER"), - 2: .same(proto: "ASYNC_GENERIC_SERVER"), - 3: .same(proto: "OTHER_SERVER"), - 4: .same(proto: "CALLBACK_SERVER"), - ] -} - -extension Grpc_Testing_RpcType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNARY"), - 1: .same(proto: "STREAMING"), - 2: .same(proto: "STREAMING_FROM_CLIENT"), - 3: .same(proto: "STREAMING_FROM_SERVER"), - 4: .same(proto: "STREAMING_BOTH_WAYS"), - ] -} - -extension Grpc_Testing_PoissonParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PoissonParams" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "offered_load"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.offeredLoad) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.offeredLoad.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.offeredLoad, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_PoissonParams, rhs: Grpc_Testing_PoissonParams) -> Bool { - if lhs.offeredLoad != rhs.offeredLoad {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClosedLoopParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClosedLoopParams" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClosedLoopParams, rhs: Grpc_Testing_ClosedLoopParams) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".LoadParams" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "closed_loop"), - 2: .same(proto: "poisson"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ClosedLoopParams? - var hadOneofValue = false - if let current = self.load { - hadOneofValue = true - if case .closedLoop(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.load = .closedLoop(v) - } - }() - case 2: try { - var v: Grpc_Testing_PoissonParams? - var hadOneofValue = false - if let current = self.load { - hadOneofValue = true - if case .poisson(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.load = .poisson(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.load { - case .closedLoop?: try { - guard case .closedLoop(let v)? = self.load else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .poisson?: try { - guard case .poisson(let v)? = self.load else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadParams, rhs: Grpc_Testing_LoadParams) -> Bool { - if lhs.load != rhs.load {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SecurityParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".SecurityParams" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "use_test_ca"), - 2: .standard(proto: "server_host_override"), - 3: .standard(proto: "cred_type"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.useTestCa) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.serverHostOverride) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.credType) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.useTestCa != false { - try visitor.visitSingularBoolField(value: self.useTestCa, fieldNumber: 1) - } - if !self.serverHostOverride.isEmpty { - try visitor.visitSingularStringField(value: self.serverHostOverride, fieldNumber: 2) - } - if !self.credType.isEmpty { - try visitor.visitSingularStringField(value: self.credType, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_SecurityParams, rhs: Grpc_Testing_SecurityParams) -> Bool { - if lhs.useTestCa != rhs.useTestCa {return false} - if lhs.serverHostOverride != rhs.serverHostOverride {return false} - if lhs.credType != rhs.credType {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ChannelArg: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ChannelArg" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .standard(proto: "str_value"), - 3: .standard(proto: "int_value"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - case 2: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.value != nil {try decoder.handleConflictingOneOf()} - self.value = .strValue(v) - } - }() - case 3: try { - var v: Int32? - try decoder.decodeSingularInt32Field(value: &v) - if let v = v { - if self.value != nil {try decoder.handleConflictingOneOf()} - self.value = .intValue(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - switch self.value { - case .strValue?: try { - guard case .strValue(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 2) - }() - case .intValue?: try { - guard case .intValue(let v)? = self.value else { preconditionFailure() } - try visitor.visitSingularInt32Field(value: v, fieldNumber: 3) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ChannelArg, rhs: Grpc_Testing_ChannelArg) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClientConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "server_targets"), - 2: .standard(proto: "client_type"), - 3: .standard(proto: "security_params"), - 4: .standard(proto: "outstanding_rpcs_per_channel"), - 5: .standard(proto: "client_channels"), - 7: .standard(proto: "async_client_threads"), - 8: .standard(proto: "rpc_type"), - 10: .standard(proto: "load_params"), - 11: .standard(proto: "payload_config"), - 12: .standard(proto: "histogram_params"), - 13: .standard(proto: "core_list"), - 14: .standard(proto: "core_limit"), - 15: .standard(proto: "other_client_api"), - 16: .standard(proto: "channel_args"), - 17: .standard(proto: "threads_per_cq"), - 18: .standard(proto: "messages_per_stream"), - 19: .standard(proto: "use_coalesce_api"), - 20: .standard(proto: "median_latency_collection_interval_millis"), - 21: .standard(proto: "client_processes"), - ] - - fileprivate class _StorageClass { - var _serverTargets: [String] = [] - var _clientType: Grpc_Testing_ClientType = .syncClient - var _securityParams: Grpc_Testing_SecurityParams? = nil - var _outstandingRpcsPerChannel: Int32 = 0 - var _clientChannels: Int32 = 0 - var _asyncClientThreads: Int32 = 0 - var _rpcType: Grpc_Testing_RpcType = .unary - var _loadParams: Grpc_Testing_LoadParams? = nil - var _payloadConfig: Grpc_Testing_PayloadConfig? = nil - var _histogramParams: Grpc_Testing_HistogramParams? = nil - var _coreList: [Int32] = [] - var _coreLimit: Int32 = 0 - var _otherClientApi: String = String() - var _channelArgs: [Grpc_Testing_ChannelArg] = [] - var _threadsPerCq: Int32 = 0 - var _messagesPerStream: Int32 = 0 - var _useCoalesceApi: Bool = false - var _medianLatencyCollectionIntervalMillis: Int32 = 0 - var _clientProcesses: Int32 = 0 - - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif - - private init() {} - - init(copying source: _StorageClass) { - _serverTargets = source._serverTargets - _clientType = source._clientType - _securityParams = source._securityParams - _outstandingRpcsPerChannel = source._outstandingRpcsPerChannel - _clientChannels = source._clientChannels - _asyncClientThreads = source._asyncClientThreads - _rpcType = source._rpcType - _loadParams = source._loadParams - _payloadConfig = source._payloadConfig - _histogramParams = source._histogramParams - _coreList = source._coreList - _coreLimit = source._coreLimit - _otherClientApi = source._otherClientApi - _channelArgs = source._channelArgs - _threadsPerCq = source._threadsPerCq - _messagesPerStream = source._messagesPerStream - _useCoalesceApi = source._useCoalesceApi - _medianLatencyCollectionIntervalMillis = source._medianLatencyCollectionIntervalMillis - _clientProcesses = source._clientProcesses - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedStringField(value: &_storage._serverTargets) }() - case 2: try { try decoder.decodeSingularEnumField(value: &_storage._clientType) }() - case 3: try { try decoder.decodeSingularMessageField(value: &_storage._securityParams) }() - case 4: try { try decoder.decodeSingularInt32Field(value: &_storage._outstandingRpcsPerChannel) }() - case 5: try { try decoder.decodeSingularInt32Field(value: &_storage._clientChannels) }() - case 7: try { try decoder.decodeSingularInt32Field(value: &_storage._asyncClientThreads) }() - case 8: try { try decoder.decodeSingularEnumField(value: &_storage._rpcType) }() - case 10: try { try decoder.decodeSingularMessageField(value: &_storage._loadParams) }() - case 11: try { try decoder.decodeSingularMessageField(value: &_storage._payloadConfig) }() - case 12: try { try decoder.decodeSingularMessageField(value: &_storage._histogramParams) }() - case 13: try { try decoder.decodeRepeatedInt32Field(value: &_storage._coreList) }() - case 14: try { try decoder.decodeSingularInt32Field(value: &_storage._coreLimit) }() - case 15: try { try decoder.decodeSingularStringField(value: &_storage._otherClientApi) }() - case 16: try { try decoder.decodeRepeatedMessageField(value: &_storage._channelArgs) }() - case 17: try { try decoder.decodeSingularInt32Field(value: &_storage._threadsPerCq) }() - case 18: try { try decoder.decodeSingularInt32Field(value: &_storage._messagesPerStream) }() - case 19: try { try decoder.decodeSingularBoolField(value: &_storage._useCoalesceApi) }() - case 20: try { try decoder.decodeSingularInt32Field(value: &_storage._medianLatencyCollectionIntervalMillis) }() - case 21: try { try decoder.decodeSingularInt32Field(value: &_storage._clientProcesses) }() - default: break - } - } - } - } - - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !_storage._serverTargets.isEmpty { - try visitor.visitRepeatedStringField(value: _storage._serverTargets, fieldNumber: 1) - } - if _storage._clientType != .syncClient { - try visitor.visitSingularEnumField(value: _storage._clientType, fieldNumber: 2) - } - try { if let v = _storage._securityParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if _storage._outstandingRpcsPerChannel != 0 { - try visitor.visitSingularInt32Field(value: _storage._outstandingRpcsPerChannel, fieldNumber: 4) - } - if _storage._clientChannels != 0 { - try visitor.visitSingularInt32Field(value: _storage._clientChannels, fieldNumber: 5) - } - if _storage._asyncClientThreads != 0 { - try visitor.visitSingularInt32Field(value: _storage._asyncClientThreads, fieldNumber: 7) - } - if _storage._rpcType != .unary { - try visitor.visitSingularEnumField(value: _storage._rpcType, fieldNumber: 8) - } - try { if let v = _storage._loadParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) - } }() - try { if let v = _storage._payloadConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - } }() - try { if let v = _storage._histogramParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 12) - } }() - if !_storage._coreList.isEmpty { - try visitor.visitPackedInt32Field(value: _storage._coreList, fieldNumber: 13) - } - if _storage._coreLimit != 0 { - try visitor.visitSingularInt32Field(value: _storage._coreLimit, fieldNumber: 14) - } - if !_storage._otherClientApi.isEmpty { - try visitor.visitSingularStringField(value: _storage._otherClientApi, fieldNumber: 15) - } - if !_storage._channelArgs.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._channelArgs, fieldNumber: 16) - } - if _storage._threadsPerCq != 0 { - try visitor.visitSingularInt32Field(value: _storage._threadsPerCq, fieldNumber: 17) - } - if _storage._messagesPerStream != 0 { - try visitor.visitSingularInt32Field(value: _storage._messagesPerStream, fieldNumber: 18) - } - if _storage._useCoalesceApi != false { - try visitor.visitSingularBoolField(value: _storage._useCoalesceApi, fieldNumber: 19) - } - if _storage._medianLatencyCollectionIntervalMillis != 0 { - try visitor.visitSingularInt32Field(value: _storage._medianLatencyCollectionIntervalMillis, fieldNumber: 20) - } - if _storage._clientProcesses != 0 { - try visitor.visitSingularInt32Field(value: _storage._clientProcesses, fieldNumber: 21) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClientConfig, rhs: Grpc_Testing_ClientConfig) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._serverTargets != rhs_storage._serverTargets {return false} - if _storage._clientType != rhs_storage._clientType {return false} - if _storage._securityParams != rhs_storage._securityParams {return false} - if _storage._outstandingRpcsPerChannel != rhs_storage._outstandingRpcsPerChannel {return false} - if _storage._clientChannels != rhs_storage._clientChannels {return false} - if _storage._asyncClientThreads != rhs_storage._asyncClientThreads {return false} - if _storage._rpcType != rhs_storage._rpcType {return false} - if _storage._loadParams != rhs_storage._loadParams {return false} - if _storage._payloadConfig != rhs_storage._payloadConfig {return false} - if _storage._histogramParams != rhs_storage._histogramParams {return false} - if _storage._coreList != rhs_storage._coreList {return false} - if _storage._coreLimit != rhs_storage._coreLimit {return false} - if _storage._otherClientApi != rhs_storage._otherClientApi {return false} - if _storage._channelArgs != rhs_storage._channelArgs {return false} - if _storage._threadsPerCq != rhs_storage._threadsPerCq {return false} - if _storage._messagesPerStream != rhs_storage._messagesPerStream {return false} - if _storage._useCoalesceApi != rhs_storage._useCoalesceApi {return false} - if _storage._medianLatencyCollectionIntervalMillis != rhs_storage._medianLatencyCollectionIntervalMillis {return false} - if _storage._clientProcesses != rhs_storage._clientProcesses {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClientStatus" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "stats"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._stats) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._stats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClientStatus, rhs: Grpc_Testing_ClientStatus) -> Bool { - if lhs._stats != rhs._stats {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Mark: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Mark" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "reset"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.reset) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.reset != false { - try visitor.visitSingularBoolField(value: self.reset, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_Mark, rhs: Grpc_Testing_Mark) -> Bool { - if lhs.reset != rhs.reset {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientArgs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClientArgs" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "setup"), - 2: .same(proto: "mark"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ClientConfig? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .setup(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .setup(v) - } - }() - case 2: try { - var v: Grpc_Testing_Mark? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .mark(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .mark(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.argtype { - case .setup?: try { - guard case .setup(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .mark?: try { - guard case .mark(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClientArgs, rhs: Grpc_Testing_ClientArgs) -> Bool { - if lhs.argtype != rhs.argtype {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ServerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "server_type"), - 2: .standard(proto: "security_params"), - 4: .same(proto: "port"), - 7: .standard(proto: "async_server_threads"), - 8: .standard(proto: "core_limit"), - 9: .standard(proto: "payload_config"), - 10: .standard(proto: "core_list"), - 11: .standard(proto: "other_server_api"), - 12: .standard(proto: "threads_per_cq"), - 1001: .standard(proto: "resource_quota_size"), - 1002: .standard(proto: "channel_args"), - 21: .standard(proto: "server_processes"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.serverType) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._securityParams) }() - case 4: try { try decoder.decodeSingularInt32Field(value: &self.port) }() - case 7: try { try decoder.decodeSingularInt32Field(value: &self.asyncServerThreads) }() - case 8: try { try decoder.decodeSingularInt32Field(value: &self.coreLimit) }() - case 9: try { try decoder.decodeSingularMessageField(value: &self._payloadConfig) }() - case 10: try { try decoder.decodeRepeatedInt32Field(value: &self.coreList) }() - case 11: try { try decoder.decodeSingularStringField(value: &self.otherServerApi) }() - case 12: try { try decoder.decodeSingularInt32Field(value: &self.threadsPerCq) }() - case 21: try { try decoder.decodeSingularInt32Field(value: &self.serverProcesses) }() - case 1001: try { try decoder.decodeSingularInt32Field(value: &self.resourceQuotaSize) }() - case 1002: try { try decoder.decodeRepeatedMessageField(value: &self.channelArgs) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.serverType != .syncServer { - try visitor.visitSingularEnumField(value: self.serverType, fieldNumber: 1) - } - try { if let v = self._securityParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if self.port != 0 { - try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 4) - } - if self.asyncServerThreads != 0 { - try visitor.visitSingularInt32Field(value: self.asyncServerThreads, fieldNumber: 7) - } - if self.coreLimit != 0 { - try visitor.visitSingularInt32Field(value: self.coreLimit, fieldNumber: 8) - } - try { if let v = self._payloadConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 9) - } }() - if !self.coreList.isEmpty { - try visitor.visitPackedInt32Field(value: self.coreList, fieldNumber: 10) - } - if !self.otherServerApi.isEmpty { - try visitor.visitSingularStringField(value: self.otherServerApi, fieldNumber: 11) - } - if self.threadsPerCq != 0 { - try visitor.visitSingularInt32Field(value: self.threadsPerCq, fieldNumber: 12) - } - if self.serverProcesses != 0 { - try visitor.visitSingularInt32Field(value: self.serverProcesses, fieldNumber: 21) - } - if self.resourceQuotaSize != 0 { - try visitor.visitSingularInt32Field(value: self.resourceQuotaSize, fieldNumber: 1001) - } - if !self.channelArgs.isEmpty { - try visitor.visitRepeatedMessageField(value: self.channelArgs, fieldNumber: 1002) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ServerConfig, rhs: Grpc_Testing_ServerConfig) -> Bool { - if lhs.serverType != rhs.serverType {return false} - if lhs._securityParams != rhs._securityParams {return false} - if lhs.port != rhs.port {return false} - if lhs.asyncServerThreads != rhs.asyncServerThreads {return false} - if lhs.coreLimit != rhs.coreLimit {return false} - if lhs._payloadConfig != rhs._payloadConfig {return false} - if lhs.coreList != rhs.coreList {return false} - if lhs.otherServerApi != rhs.otherServerApi {return false} - if lhs.threadsPerCq != rhs.threadsPerCq {return false} - if lhs.resourceQuotaSize != rhs.resourceQuotaSize {return false} - if lhs.channelArgs != rhs.channelArgs {return false} - if lhs.serverProcesses != rhs.serverProcesses {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ServerArgs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerArgs" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "setup"), - 2: .same(proto: "mark"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ServerConfig? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .setup(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .setup(v) - } - }() - case 2: try { - var v: Grpc_Testing_Mark? - var hadOneofValue = false - if let current = self.argtype { - hadOneofValue = true - if case .mark(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.argtype = .mark(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.argtype { - case .setup?: try { - guard case .setup(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .mark?: try { - guard case .mark(let v)? = self.argtype else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ServerArgs, rhs: Grpc_Testing_ServerArgs) -> Bool { - if lhs.argtype != rhs.argtype {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ServerStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerStatus" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "stats"), - 2: .same(proto: "port"), - 3: .same(proto: "cores"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._stats) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.port) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &self.cores) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._stats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.port != 0 { - try visitor.visitSingularInt32Field(value: self.port, fieldNumber: 2) - } - if self.cores != 0 { - try visitor.visitSingularInt32Field(value: self.cores, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ServerStatus, rhs: Grpc_Testing_ServerStatus) -> Bool { - if lhs._stats != rhs._stats {return false} - if lhs.port != rhs.port {return false} - if lhs.cores != rhs.cores {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_CoreRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".CoreRequest" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_CoreRequest, rhs: Grpc_Testing_CoreRequest) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_CoreResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".CoreResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "cores"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.cores) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.cores != 0 { - try visitor.visitSingularInt32Field(value: self.cores, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_CoreResponse, rhs: Grpc_Testing_CoreResponse) -> Bool { - if lhs.cores != rhs.cores {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Void: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Void" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_Void, rhs: Grpc_Testing_Void) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Scenario: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Scenario" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - 2: .standard(proto: "client_config"), - 3: .standard(proto: "num_clients"), - 4: .standard(proto: "server_config"), - 5: .standard(proto: "num_servers"), - 6: .standard(proto: "warmup_seconds"), - 7: .standard(proto: "benchmark_seconds"), - 8: .standard(proto: "spawn_local_worker_count"), - ] - - fileprivate class _StorageClass { - var _name: String = String() - var _clientConfig: Grpc_Testing_ClientConfig? = nil - var _numClients: Int32 = 0 - var _serverConfig: Grpc_Testing_ServerConfig? = nil - var _numServers: Int32 = 0 - var _warmupSeconds: Int32 = 0 - var _benchmarkSeconds: Int32 = 0 - var _spawnLocalWorkerCount: Int32 = 0 - - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif - - private init() {} - - init(copying source: _StorageClass) { - _name = source._name - _clientConfig = source._clientConfig - _numClients = source._numClients - _serverConfig = source._serverConfig - _numServers = source._numServers - _warmupSeconds = source._warmupSeconds - _benchmarkSeconds = source._benchmarkSeconds - _spawnLocalWorkerCount = source._spawnLocalWorkerCount - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &_storage._name) }() - case 2: try { try decoder.decodeSingularMessageField(value: &_storage._clientConfig) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &_storage._numClients) }() - case 4: try { try decoder.decodeSingularMessageField(value: &_storage._serverConfig) }() - case 5: try { try decoder.decodeSingularInt32Field(value: &_storage._numServers) }() - case 6: try { try decoder.decodeSingularInt32Field(value: &_storage._warmupSeconds) }() - case 7: try { try decoder.decodeSingularInt32Field(value: &_storage._benchmarkSeconds) }() - case 8: try { try decoder.decodeSingularInt32Field(value: &_storage._spawnLocalWorkerCount) }() - default: break - } - } - } - } - - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !_storage._name.isEmpty { - try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 1) - } - try { if let v = _storage._clientConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if _storage._numClients != 0 { - try visitor.visitSingularInt32Field(value: _storage._numClients, fieldNumber: 3) - } - try { if let v = _storage._serverConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - } }() - if _storage._numServers != 0 { - try visitor.visitSingularInt32Field(value: _storage._numServers, fieldNumber: 5) - } - if _storage._warmupSeconds != 0 { - try visitor.visitSingularInt32Field(value: _storage._warmupSeconds, fieldNumber: 6) - } - if _storage._benchmarkSeconds != 0 { - try visitor.visitSingularInt32Field(value: _storage._benchmarkSeconds, fieldNumber: 7) - } - if _storage._spawnLocalWorkerCount != 0 { - try visitor.visitSingularInt32Field(value: _storage._spawnLocalWorkerCount, fieldNumber: 8) - } - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_Scenario, rhs: Grpc_Testing_Scenario) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._name != rhs_storage._name {return false} - if _storage._clientConfig != rhs_storage._clientConfig {return false} - if _storage._numClients != rhs_storage._numClients {return false} - if _storage._serverConfig != rhs_storage._serverConfig {return false} - if _storage._numServers != rhs_storage._numServers {return false} - if _storage._warmupSeconds != rhs_storage._warmupSeconds {return false} - if _storage._benchmarkSeconds != rhs_storage._benchmarkSeconds {return false} - if _storage._spawnLocalWorkerCount != rhs_storage._spawnLocalWorkerCount {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Scenarios: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Scenarios" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "scenarios"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.scenarios) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.scenarios.isEmpty { - try visitor.visitRepeatedMessageField(value: self.scenarios, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_Scenarios, rhs: Grpc_Testing_Scenarios) -> Bool { - if lhs.scenarios != rhs.scenarios {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ScenarioResultSummary: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ScenarioResultSummary" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "qps"), - 2: .standard(proto: "qps_per_server_core"), - 3: .standard(proto: "server_system_time"), - 4: .standard(proto: "server_user_time"), - 5: .standard(proto: "client_system_time"), - 6: .standard(proto: "client_user_time"), - 7: .standard(proto: "latency_50"), - 8: .standard(proto: "latency_90"), - 9: .standard(proto: "latency_95"), - 10: .standard(proto: "latency_99"), - 11: .standard(proto: "latency_999"), - 12: .standard(proto: "server_cpu_usage"), - 13: .standard(proto: "successful_requests_per_second"), - 14: .standard(proto: "failed_requests_per_second"), - 15: .standard(proto: "client_polls_per_request"), - 16: .standard(proto: "server_polls_per_request"), - 17: .standard(proto: "server_queries_per_cpu_sec"), - 18: .standard(proto: "client_queries_per_cpu_sec"), - 19: .standard(proto: "start_time"), - 20: .standard(proto: "end_time"), - ] - - fileprivate class _StorageClass { - var _qps: Double = 0 - var _qpsPerServerCore: Double = 0 - var _serverSystemTime: Double = 0 - var _serverUserTime: Double = 0 - var _clientSystemTime: Double = 0 - var _clientUserTime: Double = 0 - var _latency50: Double = 0 - var _latency90: Double = 0 - var _latency95: Double = 0 - var _latency99: Double = 0 - var _latency999: Double = 0 - var _serverCpuUsage: Double = 0 - var _successfulRequestsPerSecond: Double = 0 - var _failedRequestsPerSecond: Double = 0 - var _clientPollsPerRequest: Double = 0 - var _serverPollsPerRequest: Double = 0 - var _serverQueriesPerCpuSec: Double = 0 - var _clientQueriesPerCpuSec: Double = 0 - var _startTime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil - var _endTime: SwiftProtobuf.Google_Protobuf_Timestamp? = nil - - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif - - private init() {} - - init(copying source: _StorageClass) { - _qps = source._qps - _qpsPerServerCore = source._qpsPerServerCore - _serverSystemTime = source._serverSystemTime - _serverUserTime = source._serverUserTime - _clientSystemTime = source._clientSystemTime - _clientUserTime = source._clientUserTime - _latency50 = source._latency50 - _latency90 = source._latency90 - _latency95 = source._latency95 - _latency99 = source._latency99 - _latency999 = source._latency999 - _serverCpuUsage = source._serverCpuUsage - _successfulRequestsPerSecond = source._successfulRequestsPerSecond - _failedRequestsPerSecond = source._failedRequestsPerSecond - _clientPollsPerRequest = source._clientPollsPerRequest - _serverPollsPerRequest = source._serverPollsPerRequest - _serverQueriesPerCpuSec = source._serverQueriesPerCpuSec - _clientQueriesPerCpuSec = source._clientQueriesPerCpuSec - _startTime = source._startTime - _endTime = source._endTime - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &_storage._qps) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &_storage._qpsPerServerCore) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &_storage._serverSystemTime) }() - case 4: try { try decoder.decodeSingularDoubleField(value: &_storage._serverUserTime) }() - case 5: try { try decoder.decodeSingularDoubleField(value: &_storage._clientSystemTime) }() - case 6: try { try decoder.decodeSingularDoubleField(value: &_storage._clientUserTime) }() - case 7: try { try decoder.decodeSingularDoubleField(value: &_storage._latency50) }() - case 8: try { try decoder.decodeSingularDoubleField(value: &_storage._latency90) }() - case 9: try { try decoder.decodeSingularDoubleField(value: &_storage._latency95) }() - case 10: try { try decoder.decodeSingularDoubleField(value: &_storage._latency99) }() - case 11: try { try decoder.decodeSingularDoubleField(value: &_storage._latency999) }() - case 12: try { try decoder.decodeSingularDoubleField(value: &_storage._serverCpuUsage) }() - case 13: try { try decoder.decodeSingularDoubleField(value: &_storage._successfulRequestsPerSecond) }() - case 14: try { try decoder.decodeSingularDoubleField(value: &_storage._failedRequestsPerSecond) }() - case 15: try { try decoder.decodeSingularDoubleField(value: &_storage._clientPollsPerRequest) }() - case 16: try { try decoder.decodeSingularDoubleField(value: &_storage._serverPollsPerRequest) }() - case 17: try { try decoder.decodeSingularDoubleField(value: &_storage._serverQueriesPerCpuSec) }() - case 18: try { try decoder.decodeSingularDoubleField(value: &_storage._clientQueriesPerCpuSec) }() - case 19: try { try decoder.decodeSingularMessageField(value: &_storage._startTime) }() - case 20: try { try decoder.decodeSingularMessageField(value: &_storage._endTime) }() - default: break - } - } - } - } - - func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if _storage._qps.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._qps, fieldNumber: 1) - } - if _storage._qpsPerServerCore.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._qpsPerServerCore, fieldNumber: 2) - } - if _storage._serverSystemTime.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverSystemTime, fieldNumber: 3) - } - if _storage._serverUserTime.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverUserTime, fieldNumber: 4) - } - if _storage._clientSystemTime.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientSystemTime, fieldNumber: 5) - } - if _storage._clientUserTime.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientUserTime, fieldNumber: 6) - } - if _storage._latency50.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency50, fieldNumber: 7) - } - if _storage._latency90.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency90, fieldNumber: 8) - } - if _storage._latency95.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency95, fieldNumber: 9) - } - if _storage._latency99.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency99, fieldNumber: 10) - } - if _storage._latency999.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._latency999, fieldNumber: 11) - } - if _storage._serverCpuUsage.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverCpuUsage, fieldNumber: 12) - } - if _storage._successfulRequestsPerSecond.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._successfulRequestsPerSecond, fieldNumber: 13) - } - if _storage._failedRequestsPerSecond.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._failedRequestsPerSecond, fieldNumber: 14) - } - if _storage._clientPollsPerRequest.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientPollsPerRequest, fieldNumber: 15) - } - if _storage._serverPollsPerRequest.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverPollsPerRequest, fieldNumber: 16) - } - if _storage._serverQueriesPerCpuSec.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._serverQueriesPerCpuSec, fieldNumber: 17) - } - if _storage._clientQueriesPerCpuSec.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: _storage._clientQueriesPerCpuSec, fieldNumber: 18) - } - try { if let v = _storage._startTime { - try visitor.visitSingularMessageField(value: v, fieldNumber: 19) - } }() - try { if let v = _storage._endTime { - try visitor.visitSingularMessageField(value: v, fieldNumber: 20) - } }() - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ScenarioResultSummary, rhs: Grpc_Testing_ScenarioResultSummary) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._qps != rhs_storage._qps {return false} - if _storage._qpsPerServerCore != rhs_storage._qpsPerServerCore {return false} - if _storage._serverSystemTime != rhs_storage._serverSystemTime {return false} - if _storage._serverUserTime != rhs_storage._serverUserTime {return false} - if _storage._clientSystemTime != rhs_storage._clientSystemTime {return false} - if _storage._clientUserTime != rhs_storage._clientUserTime {return false} - if _storage._latency50 != rhs_storage._latency50 {return false} - if _storage._latency90 != rhs_storage._latency90 {return false} - if _storage._latency95 != rhs_storage._latency95 {return false} - if _storage._latency99 != rhs_storage._latency99 {return false} - if _storage._latency999 != rhs_storage._latency999 {return false} - if _storage._serverCpuUsage != rhs_storage._serverCpuUsage {return false} - if _storage._successfulRequestsPerSecond != rhs_storage._successfulRequestsPerSecond {return false} - if _storage._failedRequestsPerSecond != rhs_storage._failedRequestsPerSecond {return false} - if _storage._clientPollsPerRequest != rhs_storage._clientPollsPerRequest {return false} - if _storage._serverPollsPerRequest != rhs_storage._serverPollsPerRequest {return false} - if _storage._serverQueriesPerCpuSec != rhs_storage._serverQueriesPerCpuSec {return false} - if _storage._clientQueriesPerCpuSec != rhs_storage._clientQueriesPerCpuSec {return false} - if _storage._startTime != rhs_storage._startTime {return false} - if _storage._endTime != rhs_storage._endTime {return false} - return true - } - if !storagesAreEqual {return false} - } - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ScenarioResult: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ScenarioResult" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "scenario"), - 2: .same(proto: "latencies"), - 3: .standard(proto: "client_stats"), - 4: .standard(proto: "server_stats"), - 5: .standard(proto: "server_cores"), - 6: .same(proto: "summary"), - 7: .standard(proto: "client_success"), - 8: .standard(proto: "server_success"), - 9: .standard(proto: "request_results"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._scenario) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._latencies) }() - case 3: try { try decoder.decodeRepeatedMessageField(value: &self.clientStats) }() - case 4: try { try decoder.decodeRepeatedMessageField(value: &self.serverStats) }() - case 5: try { try decoder.decodeRepeatedInt32Field(value: &self.serverCores) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._summary) }() - case 7: try { try decoder.decodeRepeatedBoolField(value: &self.clientSuccess) }() - case 8: try { try decoder.decodeRepeatedBoolField(value: &self.serverSuccess) }() - case 9: try { try decoder.decodeRepeatedMessageField(value: &self.requestResults) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._scenario { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._latencies { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if !self.clientStats.isEmpty { - try visitor.visitRepeatedMessageField(value: self.clientStats, fieldNumber: 3) - } - if !self.serverStats.isEmpty { - try visitor.visitRepeatedMessageField(value: self.serverStats, fieldNumber: 4) - } - if !self.serverCores.isEmpty { - try visitor.visitPackedInt32Field(value: self.serverCores, fieldNumber: 5) - } - try { if let v = self._summary { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - if !self.clientSuccess.isEmpty { - try visitor.visitPackedBoolField(value: self.clientSuccess, fieldNumber: 7) - } - if !self.serverSuccess.isEmpty { - try visitor.visitPackedBoolField(value: self.serverSuccess, fieldNumber: 8) - } - if !self.requestResults.isEmpty { - try visitor.visitRepeatedMessageField(value: self.requestResults, fieldNumber: 9) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ScenarioResult, rhs: Grpc_Testing_ScenarioResult) -> Bool { - if lhs._scenario != rhs._scenario {return false} - if lhs._latencies != rhs._latencies {return false} - if lhs.clientStats != rhs.clientStats {return false} - if lhs.serverStats != rhs.serverStats {return false} - if lhs.serverCores != rhs.serverCores {return false} - if lhs._summary != rhs._summary {return false} - if lhs.clientSuccess != rhs.clientSuccess {return false} - if lhs.serverSuccess != rhs.serverSuccess {return false} - if lhs.requestResults != rhs.requestResults {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift b/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift deleted file mode 100644 index 0665c8f0c..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_messages.pb.swift +++ /dev/null @@ -1,2140 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/messages.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The type of payload that should be returned. -enum Grpc_Testing_PayloadType: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - - /// Compressable text format. - case compressable // = 0 - case UNRECOGNIZED(Int) - - init() { - self = .compressable - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .compressable - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .compressable: return 0 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_PayloadType] = [ - .compressable, - ] - -} - -/// The type of route that a client took to reach a server w.r.t. gRPCLB. -/// The server must fill in "fallback" if it detects that the RPC reached -/// the server via the "gRPCLB fallback" path, and "backend" if it detects -/// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got -/// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly -/// how this detection is done is context and server dependent. -enum Grpc_Testing_GrpclbRouteType: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - - /// Server didn't detect the route that a client took to reach it. - case unknown // = 0 - - /// Indicates that a client reached a server via gRPCLB fallback. - case fallback // = 1 - - /// Indicates that a client reached a server as a gRPCLB-given backend. - case backend // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .fallback - case 2: self = .backend - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .fallback: return 1 - case .backend: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_GrpclbRouteType] = [ - .unknown, - .fallback, - .backend, - ] - -} - -/// TODO(dgq): Go back to using well-known types once -/// https://github.com/grpc/grpc/issues/6980 has been fixed. -/// import "google/protobuf/wrappers.proto"; -struct Grpc_Testing_BoolValue: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The bool value. - var value: Bool = false - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A block of data, to simply increase gRPC message size. -struct Grpc_Testing_Payload: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The type of data in body. - var type: Grpc_Testing_PayloadType = .compressable - - /// Primary contents of payload. - var body: Data = Data() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A protobuf representation for grpc status. This is used by test -/// clients to specify a status that the server should attempt to return. -struct Grpc_Testing_EchoStatus: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var code: Int32 = 0 - - var message: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Unary request. -struct Grpc_Testing_SimpleRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, server randomly chooses one from other formats. - var responseType: Grpc_Testing_PayloadType = .compressable - - /// Desired payload size in the response from the server. - var responseSize: Int32 = 0 - - /// Optional input payload sent along with the request. - var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - mutating func clearPayload() {self._payload = nil} - - /// Whether SimpleResponse should include username. - var fillUsername: Bool = false - - /// Whether SimpleResponse should include OAuth scope. - var fillOauthScope: Bool = false - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - var responseCompressed: Grpc_Testing_BoolValue { - get {return _responseCompressed ?? Grpc_Testing_BoolValue()} - set {_responseCompressed = newValue} - } - /// Returns true if `responseCompressed` has been explicitly set. - var hasResponseCompressed: Bool {return self._responseCompressed != nil} - /// Clears the value of `responseCompressed`. Subsequent reads from it will return its default value. - mutating func clearResponseCompressed() {self._responseCompressed = nil} - - /// Whether server should return a given status - var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - mutating func clearResponseStatus() {self._responseStatus = nil} - - /// Whether the server should expect this request to be compressed. - var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - mutating func clearExpectCompressed() {self._expectCompressed = nil} - - /// Whether SimpleResponse should include server_id. - var fillServerID: Bool = false - - /// Whether SimpleResponse should include grpclb_route_type. - var fillGrpclbRouteType: Bool = false - - /// If set the server should record this metrics report data for the current RPC. - var orcaPerQueryReport: Grpc_Testing_TestOrcaReport { - get {return _orcaPerQueryReport ?? Grpc_Testing_TestOrcaReport()} - set {_orcaPerQueryReport = newValue} - } - /// Returns true if `orcaPerQueryReport` has been explicitly set. - var hasOrcaPerQueryReport: Bool {return self._orcaPerQueryReport != nil} - /// Clears the value of `orcaPerQueryReport`. Subsequent reads from it will return its default value. - mutating func clearOrcaPerQueryReport() {self._orcaPerQueryReport = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseCompressed: Grpc_Testing_BoolValue? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil - fileprivate var _orcaPerQueryReport: Grpc_Testing_TestOrcaReport? = nil -} - -/// Unary response, as configured by the request. -struct Grpc_Testing_SimpleResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase message size. - var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - mutating func clearPayload() {self._payload = nil} - - /// The user the request came from, for verifying authentication was - /// successful when the client expected it. - var username: String = String() - - /// OAuth scope. - var oauthScope: String = String() - - /// Server ID. This must be unique among different server instances, - /// but the same across all RPC's made to a particular server instance. - var serverID: String = String() - - /// gRPCLB Path. - var grpclbRouteType: Grpc_Testing_GrpclbRouteType = .unknown - - /// Server hostname. - var hostname: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// Client-streaming request. -struct Grpc_Testing_StreamingInputCallRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Optional input payload sent along with the request. - var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - mutating func clearPayload() {self._payload = nil} - - /// Whether the server should expect this request to be compressed. This field - /// is "nullable" in order to interoperate seamlessly with servers not able to - /// implement the full compression tests by introspecting the call to verify - /// the request's compression status. - var expectCompressed: Grpc_Testing_BoolValue { - get {return _expectCompressed ?? Grpc_Testing_BoolValue()} - set {_expectCompressed = newValue} - } - /// Returns true if `expectCompressed` has been explicitly set. - var hasExpectCompressed: Bool {return self._expectCompressed != nil} - /// Clears the value of `expectCompressed`. Subsequent reads from it will return its default value. - mutating func clearExpectCompressed() {self._expectCompressed = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _expectCompressed: Grpc_Testing_BoolValue? = nil -} - -/// Client-streaming response. -struct Grpc_Testing_StreamingInputCallResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Aggregated size of payloads received from the client. - var aggregatedPayloadSize: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Configuration for a particular response. -struct Grpc_Testing_ResponseParameters: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload sizes in responses from the server. - var size: Int32 = 0 - - /// Desired interval between consecutive responses in the response stream in - /// microseconds. - var intervalUs: Int32 = 0 - - /// Whether to request the server to compress the response. This field is - /// "nullable" in order to interoperate seamlessly with clients not able to - /// implement the full compression tests by introspecting the call to verify - /// the response's compression status. - var compressed: Grpc_Testing_BoolValue { - get {return _compressed ?? Grpc_Testing_BoolValue()} - set {_compressed = newValue} - } - /// Returns true if `compressed` has been explicitly set. - var hasCompressed: Bool {return self._compressed != nil} - /// Clears the value of `compressed`. Subsequent reads from it will return its default value. - mutating func clearCompressed() {self._compressed = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _compressed: Grpc_Testing_BoolValue? = nil -} - -/// Server-streaming request. -struct Grpc_Testing_StreamingOutputCallRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Desired payload type in the response from the server. - /// If response_type is RANDOM, the payload from each response in the stream - /// might be of different types. This is to simulate a mixed type of payload - /// stream. - var responseType: Grpc_Testing_PayloadType = .compressable - - /// Configuration for each expected response message. - var responseParameters: [Grpc_Testing_ResponseParameters] = [] - - /// Optional input payload sent along with the request. - var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - mutating func clearPayload() {self._payload = nil} - - /// Whether server should return a given status - var responseStatus: Grpc_Testing_EchoStatus { - get {return _responseStatus ?? Grpc_Testing_EchoStatus()} - set {_responseStatus = newValue} - } - /// Returns true if `responseStatus` has been explicitly set. - var hasResponseStatus: Bool {return self._responseStatus != nil} - /// Clears the value of `responseStatus`. Subsequent reads from it will return its default value. - mutating func clearResponseStatus() {self._responseStatus = nil} - - /// If set the server should update this metrics report data at the OOB server. - var orcaOobReport: Grpc_Testing_TestOrcaReport { - get {return _orcaOobReport ?? Grpc_Testing_TestOrcaReport()} - set {_orcaOobReport = newValue} - } - /// Returns true if `orcaOobReport` has been explicitly set. - var hasOrcaOobReport: Bool {return self._orcaOobReport != nil} - /// Clears the value of `orcaOobReport`. Subsequent reads from it will return its default value. - mutating func clearOrcaOobReport() {self._orcaOobReport = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil - fileprivate var _responseStatus: Grpc_Testing_EchoStatus? = nil - fileprivate var _orcaOobReport: Grpc_Testing_TestOrcaReport? = nil -} - -/// Server-streaming response, as configured by the request and parameters. -struct Grpc_Testing_StreamingOutputCallResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Payload to increase response size. - var payload: Grpc_Testing_Payload { - get {return _payload ?? Grpc_Testing_Payload()} - set {_payload = newValue} - } - /// Returns true if `payload` has been explicitly set. - var hasPayload: Bool {return self._payload != nil} - /// Clears the value of `payload`. Subsequent reads from it will return its default value. - mutating func clearPayload() {self._payload = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _payload: Grpc_Testing_Payload? = nil -} - -/// For reconnect interop test only. -/// Client tells server what reconnection parameters it used. -struct Grpc_Testing_ReconnectParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var maxReconnectBackoffMs: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// For reconnect interop test only. -/// Server tells client whether its reconnects are following the spec and the -/// reconnect backoffs it saw. -struct Grpc_Testing_ReconnectInfo: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var passed: Bool = false - - var backoffMs: [Int32] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_LoadBalancerStatsRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Request stats for the next num_rpcs sent by client. - var numRpcs: Int32 = 0 - - /// If num_rpcs have not completed within timeout_sec, return partial results. - var timeoutSec: Int32 = 0 - - /// Response header + trailer metadata entries we want the values of. - /// Matching of the keys is case-insensitive as per rfc7540#section-8.1.2 - /// * (asterisk) is a special value that will return all metadata entries - var metadataKeys: [String] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_LoadBalancerStatsResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of completed RPCs for each peer. - var rpcsByPeer: Dictionary = [:] - - /// The number of RPCs that failed to record a remote peer. - var numFailures: Int32 = 0 - - var rpcsByMethod: Dictionary = [:] - - /// All the metadata of all RPCs for each peer. - var metadatasByPeer: Dictionary = [:] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum MetadataType: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - case unknown // = 0 - case initial // = 1 - case trailing // = 2 - case UNRECOGNIZED(Int) - - init() { - self = .unknown - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unknown - case 1: self = .initial - case 2: self = .trailing - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unknown: return 0 - case .initial: return 1 - case .trailing: return 2 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_LoadBalancerStatsResponse.MetadataType] = [ - .unknown, - .initial, - .trailing, - ] - - } - - struct MetadataEntry: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Key, exactly as received from the server. Case may be different from what - /// was requested in the LoadBalancerStatsRequest) - var key: String = String() - - /// Value, exactly as received from the server. - var value: String = String() - - /// Metadata type - var type: Grpc_Testing_LoadBalancerStatsResponse.MetadataType = .unknown - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - struct RpcMetadata: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// metadata values for each rpc for the keys specified in - /// LoadBalancerStatsRequest.metadata_keys. - var metadata: [Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - struct MetadataByPeer: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// List of RpcMetadata in for each RPC with a given peer - var rpcMetadata: [Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - struct RpcsByPeer: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of completed RPCs for each peer. - var rpcsByPeer: Dictionary = [:] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - init() {} -} - -/// Request for retrieving a test client's accumulated stats. -struct Grpc_Testing_LoadBalancerAccumulatedStatsRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Accumulated stats for RPCs sent by a test client. -struct Grpc_Testing_LoadBalancerAccumulatedStatsResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The total number of RPCs have ever issued for each type. - /// Deprecated: use stats_per_method.rpcs_started instead. - /// - /// NOTE: This field was marked as deprecated in the .proto file. - var numRpcsStartedByMethod: Dictionary = [:] - - /// The total number of RPCs have ever completed successfully for each type. - /// Deprecated: use stats_per_method.result instead. - /// - /// NOTE: This field was marked as deprecated in the .proto file. - var numRpcsSucceededByMethod: Dictionary = [:] - - /// The total number of RPCs have ever failed for each type. - /// Deprecated: use stats_per_method.result instead. - /// - /// NOTE: This field was marked as deprecated in the .proto file. - var numRpcsFailedByMethod: Dictionary = [:] - - /// Per-method RPC statistics. The key is the RpcType in string form; e.g. - /// 'EMPTY_CALL' or 'UNARY_CALL' - var statsPerMethod: Dictionary = [:] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - struct MethodStats: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of RPCs that were started for this method. - var rpcsStarted: Int32 = 0 - - /// The number of RPCs that completed with each status for this method. The - /// key is the integral value of a google.rpc.Code; the value is the count. - var result: Dictionary = [:] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - init() {} -} - -/// Configurations for a test client. -struct Grpc_Testing_ClientConfigureRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The types of RPCs the client sends. - var types: [Grpc_Testing_ClientConfigureRequest.RpcType] = [] - - /// The collection of custom metadata to be attached to RPCs sent by the client. - var metadata: [Grpc_Testing_ClientConfigureRequest.Metadata] = [] - - /// The deadline to use, in seconds, for all RPCs. If unset or zero, the - /// client will use the default from the command-line. - var timeoutSec: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// Type of RPCs to send. - enum RpcType: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - case emptyCall // = 0 - case unaryCall // = 1 - case UNRECOGNIZED(Int) - - init() { - self = .emptyCall - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .emptyCall - case 1: self = .unaryCall - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .emptyCall: return 0 - case .unaryCall: return 1 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_ClientConfigureRequest.RpcType] = [ - .emptyCall, - .unaryCall, - ] - - } - - /// Metadata to be attached for the given type of RPCs. - struct Metadata: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var type: Grpc_Testing_ClientConfigureRequest.RpcType = .emptyCall - - var key: String = String() - - var value: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - } - - init() {} -} - -/// Response for updating a test client's configuration. -struct Grpc_Testing_ClientConfigureResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_MemorySize: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var rss: Int64 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Metrics data the server will update and send to the client. It mirrors orca load report -/// https://github.com/cncf/xds/blob/eded343319d09f30032952beda9840bbd3dcf7ac/xds/data/orca/v3/orca_load_report.proto#L15, -/// but avoids orca dependency. Used by both per-query and out-of-band reporting tests. -struct Grpc_Testing_TestOrcaReport: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var cpuUtilization: Double = 0 - - var memoryUtilization: Double = 0 - - var requestCost: Dictionary = [:] - - var utilization: Dictionary = [:] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Status that will be return to callers of the Hook method -struct Grpc_Testing_SetReturnStatusRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var grpcCodeToReturn: Int32 = 0 - - var grpcStatusDescription: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_HookRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var command: Grpc_Testing_HookRequest.HookRequestCommand = .unspecified - - var grpcCodeToReturn: Int32 = 0 - - var grpcStatusDescription: String = String() - - /// Server port to listen to - var serverPort: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum HookRequestCommand: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - - /// Default value - case unspecified // = 0 - - /// Start the HTTP endpoint - case start // = 1 - - /// Stop - case stop // = 2 - - /// Return from HTTP GET/POST - case `return` // = 3 - case UNRECOGNIZED(Int) - - init() { - self = .unspecified - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .unspecified - case 1: self = .start - case 2: self = .stop - case 3: self = .return - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .unspecified: return 0 - case .start: return 1 - case .stop: return 2 - case .return: return 3 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Grpc_Testing_HookRequest.HookRequestCommand] = [ - .unspecified, - .start, - .stop, - .return, - ] - - } - - init() {} -} - -struct Grpc_Testing_HookResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_PayloadType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "COMPRESSABLE"), - ] -} - -extension Grpc_Testing_GrpclbRouteType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "GRPCLB_ROUTE_TYPE_UNKNOWN"), - 1: .same(proto: "GRPCLB_ROUTE_TYPE_FALLBACK"), - 2: .same(proto: "GRPCLB_ROUTE_TYPE_BACKEND"), - ] -} - -extension Grpc_Testing_BoolValue: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".BoolValue" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "value"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.value) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.value != false { - try visitor.visitSingularBoolField(value: self.value, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_BoolValue, rhs: Grpc_Testing_BoolValue) -> Bool { - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_Payload: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Payload" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "body"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 2: try { try decoder.decodeSingularBytesField(value: &self.body) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.type != .compressable { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) - } - if !self.body.isEmpty { - try visitor.visitSingularBytesField(value: self.body, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_Payload, rhs: Grpc_Testing_Payload) -> Bool { - if lhs.type != rhs.type {return false} - if lhs.body != rhs.body {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_EchoStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".EchoStatus" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "code"), - 2: .same(proto: "message"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.code) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.code != 0 { - try visitor.visitSingularInt32Field(value: self.code, fieldNumber: 1) - } - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_EchoStatus, rhs: Grpc_Testing_EchoStatus) -> Bool { - if lhs.code != rhs.code {return false} - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".SimpleRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_size"), - 3: .same(proto: "payload"), - 4: .standard(proto: "fill_username"), - 5: .standard(proto: "fill_oauth_scope"), - 6: .standard(proto: "response_compressed"), - 7: .standard(proto: "response_status"), - 8: .standard(proto: "expect_compressed"), - 9: .standard(proto: "fill_server_id"), - 10: .standard(proto: "fill_grpclb_route_type"), - 11: .standard(proto: "orca_per_query_report"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.responseSize) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.fillUsername) }() - case 5: try { try decoder.decodeSingularBoolField(value: &self.fillOauthScope) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._responseCompressed) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - case 8: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - case 9: try { try decoder.decodeSingularBoolField(value: &self.fillServerID) }() - case 10: try { try decoder.decodeSingularBoolField(value: &self.fillGrpclbRouteType) }() - case 11: try { try decoder.decodeSingularMessageField(value: &self._orcaPerQueryReport) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if self.responseSize != 0 { - try visitor.visitSingularInt32Field(value: self.responseSize, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if self.fillUsername != false { - try visitor.visitSingularBoolField(value: self.fillUsername, fieldNumber: 4) - } - if self.fillOauthScope != false { - try visitor.visitSingularBoolField(value: self.fillOauthScope, fieldNumber: 5) - } - try { if let v = self._responseCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() - if self.fillServerID != false { - try visitor.visitSingularBoolField(value: self.fillServerID, fieldNumber: 9) - } - if self.fillGrpclbRouteType != false { - try visitor.visitSingularBoolField(value: self.fillGrpclbRouteType, fieldNumber: 10) - } - try { if let v = self._orcaPerQueryReport { - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_SimpleRequest, rhs: Grpc_Testing_SimpleRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseSize != rhs.responseSize {return false} - if lhs._payload != rhs._payload {return false} - if lhs.fillUsername != rhs.fillUsername {return false} - if lhs.fillOauthScope != rhs.fillOauthScope {return false} - if lhs._responseCompressed != rhs._responseCompressed {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.fillServerID != rhs.fillServerID {return false} - if lhs.fillGrpclbRouteType != rhs.fillGrpclbRouteType {return false} - if lhs._orcaPerQueryReport != rhs._orcaPerQueryReport {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".SimpleResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .same(proto: "username"), - 3: .standard(proto: "oauth_scope"), - 4: .standard(proto: "server_id"), - 5: .standard(proto: "grpclb_route_type"), - 6: .same(proto: "hostname"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.username) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.oauthScope) }() - case 4: try { try decoder.decodeSingularStringField(value: &self.serverID) }() - case 5: try { try decoder.decodeSingularEnumField(value: &self.grpclbRouteType) }() - case 6: try { try decoder.decodeSingularStringField(value: &self.hostname) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if !self.username.isEmpty { - try visitor.visitSingularStringField(value: self.username, fieldNumber: 2) - } - if !self.oauthScope.isEmpty { - try visitor.visitSingularStringField(value: self.oauthScope, fieldNumber: 3) - } - if !self.serverID.isEmpty { - try visitor.visitSingularStringField(value: self.serverID, fieldNumber: 4) - } - if self.grpclbRouteType != .unknown { - try visitor.visitSingularEnumField(value: self.grpclbRouteType, fieldNumber: 5) - } - if !self.hostname.isEmpty { - try visitor.visitSingularStringField(value: self.hostname, fieldNumber: 6) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_SimpleResponse, rhs: Grpc_Testing_SimpleResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.username != rhs.username {return false} - if lhs.oauthScope != rhs.oauthScope {return false} - if lhs.serverID != rhs.serverID {return false} - if lhs.grpclbRouteType != rhs.grpclbRouteType {return false} - if lhs.hostname != rhs.hostname {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".StreamingInputCallRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - 2: .standard(proto: "expect_compressed"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._expectCompressed) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try { if let v = self._expectCompressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_StreamingInputCallRequest, rhs: Grpc_Testing_StreamingInputCallRequest) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs._expectCompressed != rhs._expectCompressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingInputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".StreamingInputCallResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "aggregated_payload_size"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.aggregatedPayloadSize) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.aggregatedPayloadSize != 0 { - try visitor.visitSingularInt32Field(value: self.aggregatedPayloadSize, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_StreamingInputCallResponse, rhs: Grpc_Testing_StreamingInputCallResponse) -> Bool { - if lhs.aggregatedPayloadSize != rhs.aggregatedPayloadSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ResponseParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ResponseParameters" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "size"), - 2: .standard(proto: "interval_us"), - 3: .same(proto: "compressed"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.intervalUs) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._compressed) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.size != 0 { - try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) - } - if self.intervalUs != 0 { - try visitor.visitSingularInt32Field(value: self.intervalUs, fieldNumber: 2) - } - try { if let v = self._compressed { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ResponseParameters, rhs: Grpc_Testing_ResponseParameters) -> Bool { - if lhs.size != rhs.size {return false} - if lhs.intervalUs != rhs.intervalUs {return false} - if lhs._compressed != rhs._compressed {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "response_type"), - 2: .standard(proto: "response_parameters"), - 3: .same(proto: "payload"), - 7: .standard(proto: "response_status"), - 8: .standard(proto: "orca_oob_report"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.responseType) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.responseParameters) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._responseStatus) }() - case 8: try { try decoder.decodeSingularMessageField(value: &self._orcaOobReport) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.responseType != .compressable { - try visitor.visitSingularEnumField(value: self.responseType, fieldNumber: 1) - } - if !self.responseParameters.isEmpty { - try visitor.visitRepeatedMessageField(value: self.responseParameters, fieldNumber: 2) - } - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - try { if let v = self._responseStatus { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try { if let v = self._orcaOobReport { - try visitor.visitSingularMessageField(value: v, fieldNumber: 8) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_StreamingOutputCallRequest, rhs: Grpc_Testing_StreamingOutputCallRequest) -> Bool { - if lhs.responseType != rhs.responseType {return false} - if lhs.responseParameters != rhs.responseParameters {return false} - if lhs._payload != rhs._payload {return false} - if lhs._responseStatus != rhs._responseStatus {return false} - if lhs._orcaOobReport != rhs._orcaOobReport {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_StreamingOutputCallResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".StreamingOutputCallResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._payload) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._payload { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_StreamingOutputCallResponse, rhs: Grpc_Testing_StreamingOutputCallResponse) -> Bool { - if lhs._payload != rhs._payload {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ReconnectParams" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "max_reconnect_backoff_ms"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.maxReconnectBackoffMs) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.maxReconnectBackoffMs != 0 { - try visitor.visitSingularInt32Field(value: self.maxReconnectBackoffMs, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ReconnectParams, rhs: Grpc_Testing_ReconnectParams) -> Bool { - if lhs.maxReconnectBackoffMs != rhs.maxReconnectBackoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ReconnectInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ReconnectInfo" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "passed"), - 2: .standard(proto: "backoff_ms"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.passed) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.backoffMs) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.passed != false { - try visitor.visitSingularBoolField(value: self.passed, fieldNumber: 1) - } - if !self.backoffMs.isEmpty { - try visitor.visitPackedInt32Field(value: self.backoffMs, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ReconnectInfo, rhs: Grpc_Testing_ReconnectInfo) -> Bool { - if lhs.passed != rhs.passed {return false} - if lhs.backoffMs != rhs.backoffMs {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".LoadBalancerStatsRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "num_rpcs"), - 2: .standard(proto: "timeout_sec"), - 3: .standard(proto: "metadata_keys"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.numRpcs) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.timeoutSec) }() - case 3: try { try decoder.decodeRepeatedStringField(value: &self.metadataKeys) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.numRpcs != 0 { - try visitor.visitSingularInt32Field(value: self.numRpcs, fieldNumber: 1) - } - if self.timeoutSec != 0 { - try visitor.visitSingularInt32Field(value: self.timeoutSec, fieldNumber: 2) - } - if !self.metadataKeys.isEmpty { - try visitor.visitRepeatedStringField(value: self.metadataKeys, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerStatsRequest, rhs: Grpc_Testing_LoadBalancerStatsRequest) -> Bool { - if lhs.numRpcs != rhs.numRpcs {return false} - if lhs.timeoutSec != rhs.timeoutSec {return false} - if lhs.metadataKeys != rhs.metadataKeys {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".LoadBalancerStatsResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "rpcs_by_peer"), - 2: .standard(proto: "num_failures"), - 3: .standard(proto: "rpcs_by_method"), - 4: .standard(proto: "metadatas_by_peer"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.rpcsByPeer) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.numFailures) }() - case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.rpcsByMethod) }() - case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.metadatasByPeer) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.rpcsByPeer.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.rpcsByPeer, fieldNumber: 1) - } - if self.numFailures != 0 { - try visitor.visitSingularInt32Field(value: self.numFailures, fieldNumber: 2) - } - if !self.rpcsByMethod.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.rpcsByMethod, fieldNumber: 3) - } - if !self.metadatasByPeer.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.metadatasByPeer, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse, rhs: Grpc_Testing_LoadBalancerStatsResponse) -> Bool { - if lhs.rpcsByPeer != rhs.rpcsByPeer {return false} - if lhs.numFailures != rhs.numFailures {return false} - if lhs.rpcsByMethod != rhs.rpcsByMethod {return false} - if lhs.metadatasByPeer != rhs.metadatasByPeer {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsResponse.MetadataType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNKNOWN"), - 1: .same(proto: "INITIAL"), - 2: .same(proto: "TRAILING"), - ] -} - -extension Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".MetadataEntry" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "key"), - 2: .same(proto: "value"), - 3: .same(proto: "type"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.key) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.value) }() - case 3: try { try decoder.decodeSingularEnumField(value: &self.type) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.key.isEmpty { - try visitor.visitSingularStringField(value: self.key, fieldNumber: 1) - } - if !self.value.isEmpty { - try visitor.visitSingularStringField(value: self.value, fieldNumber: 2) - } - if self.type != .unknown { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry, rhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataEntry) -> Bool { - if lhs.key != rhs.key {return false} - if lhs.value != rhs.value {return false} - if lhs.type != rhs.type {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".RpcMetadata" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "metadata"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.metadata) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.metadata.isEmpty { - try visitor.visitRepeatedMessageField(value: self.metadata, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata, rhs: Grpc_Testing_LoadBalancerStatsResponse.RpcMetadata) -> Bool { - if lhs.metadata != rhs.metadata {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".MetadataByPeer" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "rpc_metadata"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.rpcMetadata) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.rpcMetadata.isEmpty { - try visitor.visitRepeatedMessageField(value: self.rpcMetadata, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer, rhs: Grpc_Testing_LoadBalancerStatsResponse.MetadataByPeer) -> Bool { - if lhs.rpcMetadata != rhs.rpcMetadata {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_Testing_LoadBalancerStatsResponse.protoMessageName + ".RpcsByPeer" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "rpcs_by_peer"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.rpcsByPeer) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.rpcsByPeer.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.rpcsByPeer, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer, rhs: Grpc_Testing_LoadBalancerStatsResponse.RpcsByPeer) -> Bool { - if lhs.rpcsByPeer != rhs.rpcsByPeer {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerAccumulatedStatsRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".LoadBalancerAccumulatedStatsRequest" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerAccumulatedStatsRequest, rhs: Grpc_Testing_LoadBalancerAccumulatedStatsRequest) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".LoadBalancerAccumulatedStatsResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "num_rpcs_started_by_method"), - 2: .standard(proto: "num_rpcs_succeeded_by_method"), - 3: .standard(proto: "num_rpcs_failed_by_method"), - 4: .standard(proto: "stats_per_method"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.numRpcsStartedByMethod) }() - case 2: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.numRpcsSucceededByMethod) }() - case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.numRpcsFailedByMethod) }() - case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: &self.statsPerMethod) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.numRpcsStartedByMethod.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.numRpcsStartedByMethod, fieldNumber: 1) - } - if !self.numRpcsSucceededByMethod.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.numRpcsSucceededByMethod, fieldNumber: 2) - } - if !self.numRpcsFailedByMethod.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.numRpcsFailedByMethod, fieldNumber: 3) - } - if !self.statsPerMethod.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMessageMap.self, value: self.statsPerMethod, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse, rhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse) -> Bool { - if lhs.numRpcsStartedByMethod != rhs.numRpcsStartedByMethod {return false} - if lhs.numRpcsSucceededByMethod != rhs.numRpcsSucceededByMethod {return false} - if lhs.numRpcsFailedByMethod != rhs.numRpcsFailedByMethod {return false} - if lhs.statsPerMethod != rhs.statsPerMethod {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_Testing_LoadBalancerAccumulatedStatsResponse.protoMessageName + ".MethodStats" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "rpcs_started"), - 2: .same(proto: "result"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.rpcsStarted) }() - case 2: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.result) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.rpcsStarted != 0 { - try visitor.visitSingularInt32Field(value: self.rpcsStarted, fieldNumber: 1) - } - if !self.result.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.result, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats, rhs: Grpc_Testing_LoadBalancerAccumulatedStatsResponse.MethodStats) -> Bool { - if lhs.rpcsStarted != rhs.rpcsStarted {return false} - if lhs.result != rhs.result {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientConfigureRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClientConfigureRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "types"), - 2: .same(proto: "metadata"), - 3: .standard(proto: "timeout_sec"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedEnumField(value: &self.types) }() - case 2: try { try decoder.decodeRepeatedMessageField(value: &self.metadata) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &self.timeoutSec) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.types.isEmpty { - try visitor.visitPackedEnumField(value: self.types, fieldNumber: 1) - } - if !self.metadata.isEmpty { - try visitor.visitRepeatedMessageField(value: self.metadata, fieldNumber: 2) - } - if self.timeoutSec != 0 { - try visitor.visitSingularInt32Field(value: self.timeoutSec, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClientConfigureRequest, rhs: Grpc_Testing_ClientConfigureRequest) -> Bool { - if lhs.types != rhs.types {return false} - if lhs.metadata != rhs.metadata {return false} - if lhs.timeoutSec != rhs.timeoutSec {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientConfigureRequest.RpcType: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "EMPTY_CALL"), - 1: .same(proto: "UNARY_CALL"), - ] -} - -extension Grpc_Testing_ClientConfigureRequest.Metadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = Grpc_Testing_ClientConfigureRequest.protoMessageName + ".Metadata" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "type"), - 2: .same(proto: "key"), - 3: .same(proto: "value"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.type) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.key) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.value) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.type != .emptyCall { - try visitor.visitSingularEnumField(value: self.type, fieldNumber: 1) - } - if !self.key.isEmpty { - try visitor.visitSingularStringField(value: self.key, fieldNumber: 2) - } - if !self.value.isEmpty { - try visitor.visitSingularStringField(value: self.value, fieldNumber: 3) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClientConfigureRequest.Metadata, rhs: Grpc_Testing_ClientConfigureRequest.Metadata) -> Bool { - if lhs.type != rhs.type {return false} - if lhs.key != rhs.key {return false} - if lhs.value != rhs.value {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientConfigureResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClientConfigureResponse" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClientConfigureResponse, rhs: Grpc_Testing_ClientConfigureResponse) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_MemorySize: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".MemorySize" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "rss"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt64Field(value: &self.rss) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.rss != 0 { - try visitor.visitSingularInt64Field(value: self.rss, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_MemorySize, rhs: Grpc_Testing_MemorySize) -> Bool { - if lhs.rss != rhs.rss {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_TestOrcaReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".TestOrcaReport" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "cpu_utilization"), - 2: .standard(proto: "memory_utilization"), - 3: .standard(proto: "request_cost"), - 4: .same(proto: "utilization"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.cpuUtilization) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.memoryUtilization) }() - case 3: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.requestCost) }() - case 4: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.utilization) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.cpuUtilization.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.cpuUtilization, fieldNumber: 1) - } - if self.memoryUtilization.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.memoryUtilization, fieldNumber: 2) - } - if !self.requestCost.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.requestCost, fieldNumber: 3) - } - if !self.utilization.isEmpty { - try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.utilization, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_TestOrcaReport, rhs: Grpc_Testing_TestOrcaReport) -> Bool { - if lhs.cpuUtilization != rhs.cpuUtilization {return false} - if lhs.memoryUtilization != rhs.memoryUtilization {return false} - if lhs.requestCost != rhs.requestCost {return false} - if lhs.utilization != rhs.utilization {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SetReturnStatusRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".SetReturnStatusRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "grpc_code_to_return"), - 2: .standard(proto: "grpc_status_description"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.grpcCodeToReturn) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.grpcStatusDescription) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.grpcCodeToReturn != 0 { - try visitor.visitSingularInt32Field(value: self.grpcCodeToReturn, fieldNumber: 1) - } - if !self.grpcStatusDescription.isEmpty { - try visitor.visitSingularStringField(value: self.grpcStatusDescription, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_SetReturnStatusRequest, rhs: Grpc_Testing_SetReturnStatusRequest) -> Bool { - if lhs.grpcCodeToReturn != rhs.grpcCodeToReturn {return false} - if lhs.grpcStatusDescription != rhs.grpcStatusDescription {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_HookRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HookRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "command"), - 2: .standard(proto: "grpc_code_to_return"), - 3: .standard(proto: "grpc_status_description"), - 4: .standard(proto: "server_port"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.command) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.grpcCodeToReturn) }() - case 3: try { try decoder.decodeSingularStringField(value: &self.grpcStatusDescription) }() - case 4: try { try decoder.decodeSingularInt32Field(value: &self.serverPort) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.command != .unspecified { - try visitor.visitSingularEnumField(value: self.command, fieldNumber: 1) - } - if self.grpcCodeToReturn != 0 { - try visitor.visitSingularInt32Field(value: self.grpcCodeToReturn, fieldNumber: 2) - } - if !self.grpcStatusDescription.isEmpty { - try visitor.visitSingularStringField(value: self.grpcStatusDescription, fieldNumber: 3) - } - if self.serverPort != 0 { - try visitor.visitSingularInt32Field(value: self.serverPort, fieldNumber: 4) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_HookRequest, rhs: Grpc_Testing_HookRequest) -> Bool { - if lhs.command != rhs.command {return false} - if lhs.grpcCodeToReturn != rhs.grpcCodeToReturn {return false} - if lhs.grpcStatusDescription != rhs.grpcStatusDescription {return false} - if lhs.serverPort != rhs.serverPort {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_HookRequest.HookRequestCommand: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "UNSPECIFIED"), - 1: .same(proto: "START"), - 2: .same(proto: "STOP"), - 3: .same(proto: "RETURN"), - ] -} - -extension Grpc_Testing_HookResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HookResponse" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_HookResponse, rhs: Grpc_Testing_HookResponse) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift b/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift deleted file mode 100644 index 8624160c0..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_payloads.pb.swift +++ /dev/null @@ -1,305 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/payloads.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct Grpc_Testing_ByteBufferParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var reqSize: Int32 = 0 - - var respSize: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_SimpleProtoParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var reqSize: Int32 = 0 - - var respSize: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// TODO (vpai): Fill this in once the details of complex, representative -/// protos are decided -struct Grpc_Testing_ComplexProtoParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_PayloadConfig: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var payload: Grpc_Testing_PayloadConfig.OneOf_Payload? = nil - - var bytebufParams: Grpc_Testing_ByteBufferParams { - get { - if case .bytebufParams(let v)? = payload {return v} - return Grpc_Testing_ByteBufferParams() - } - set {payload = .bytebufParams(newValue)} - } - - var simpleParams: Grpc_Testing_SimpleProtoParams { - get { - if case .simpleParams(let v)? = payload {return v} - return Grpc_Testing_SimpleProtoParams() - } - set {payload = .simpleParams(newValue)} - } - - var complexParams: Grpc_Testing_ComplexProtoParams { - get { - if case .complexParams(let v)? = payload {return v} - return Grpc_Testing_ComplexProtoParams() - } - set {payload = .complexParams(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - enum OneOf_Payload: Equatable, Sendable { - case bytebufParams(Grpc_Testing_ByteBufferParams) - case simpleParams(Grpc_Testing_SimpleProtoParams) - case complexParams(Grpc_Testing_ComplexProtoParams) - - } - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_ByteBufferParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ByteBufferParams" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "req_size"), - 2: .standard(proto: "resp_size"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.reqSize) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.respSize) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.reqSize != 0 { - try visitor.visitSingularInt32Field(value: self.reqSize, fieldNumber: 1) - } - if self.respSize != 0 { - try visitor.visitSingularInt32Field(value: self.respSize, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ByteBufferParams, rhs: Grpc_Testing_ByteBufferParams) -> Bool { - if lhs.reqSize != rhs.reqSize {return false} - if lhs.respSize != rhs.respSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_SimpleProtoParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".SimpleProtoParams" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "req_size"), - 2: .standard(proto: "resp_size"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.reqSize) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.respSize) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.reqSize != 0 { - try visitor.visitSingularInt32Field(value: self.reqSize, fieldNumber: 1) - } - if self.respSize != 0 { - try visitor.visitSingularInt32Field(value: self.respSize, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_SimpleProtoParams, rhs: Grpc_Testing_SimpleProtoParams) -> Bool { - if lhs.reqSize != rhs.reqSize {return false} - if lhs.respSize != rhs.respSize {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ComplexProtoParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ComplexProtoParams" - static let _protobuf_nameMap = SwiftProtobuf._NameMap() - - mutating func decodeMessage(decoder: inout D) throws { - // Load everything into unknown fields - while try decoder.nextFieldNumber() != nil {} - } - - func traverse(visitor: inout V) throws { - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ComplexProtoParams, rhs: Grpc_Testing_ComplexProtoParams) -> Bool { - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_PayloadConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PayloadConfig" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "bytebuf_params"), - 2: .standard(proto: "simple_params"), - 3: .standard(proto: "complex_params"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { - var v: Grpc_Testing_ByteBufferParams? - var hadOneofValue = false - if let current = self.payload { - hadOneofValue = true - if case .bytebufParams(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payload = .bytebufParams(v) - } - }() - case 2: try { - var v: Grpc_Testing_SimpleProtoParams? - var hadOneofValue = false - if let current = self.payload { - hadOneofValue = true - if case .simpleParams(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payload = .simpleParams(v) - } - }() - case 3: try { - var v: Grpc_Testing_ComplexProtoParams? - var hadOneofValue = false - if let current = self.payload { - hadOneofValue = true - if case .complexParams(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payload = .complexParams(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - switch self.payload { - case .bytebufParams?: try { - guard case .bytebufParams(let v)? = self.payload else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - }() - case .simpleParams?: try { - guard case .simpleParams(let v)? = self.payload else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case .complexParams?: try { - guard case .complexParams(let v)? = self.payload else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_PayloadConfig, rhs: Grpc_Testing_PayloadConfig) -> Bool { - if lhs.payload != rhs.payload {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift b/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift deleted file mode 100644 index 2b45d0bd0..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_stats.pb.swift +++ /dev/null @@ -1,462 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/stats.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct Grpc_Testing_ServerStats: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// wall clock time change in seconds since last reset - var timeElapsed: Double = 0 - - /// change in user time (in seconds) used by the server since last reset - var timeUser: Double = 0 - - /// change in server time (in seconds) used by the server process and all - /// threads since last reset - var timeSystem: Double = 0 - - /// change in total cpu time of the server (data from proc/stat) - var totalCpuTime: UInt64 = 0 - - /// change in idle time of the server (data from proc/stat) - var idleCpuTime: UInt64 = 0 - - /// Number of polls called inside completion queue - var cqPollCount: UInt64 = 0 - - /// Core library stats - var coreStats: Grpc_Core_Stats { - get {return _coreStats ?? Grpc_Core_Stats()} - set {_coreStats = newValue} - } - /// Returns true if `coreStats` has been explicitly set. - var hasCoreStats: Bool {return self._coreStats != nil} - /// Clears the value of `coreStats`. Subsequent reads from it will return its default value. - mutating func clearCoreStats() {self._coreStats = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _coreStats: Grpc_Core_Stats? = nil -} - -/// Histogram params based on grpc/support/histogram.c -struct Grpc_Testing_HistogramParams: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// first bucket is [0, 1 + resolution) - var resolution: Double = 0 - - /// use enough buckets to allow this value - var maxPossible: Double = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// Histogram data based on grpc/support/histogram.c -struct Grpc_Testing_HistogramData: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var bucket: [UInt32] = [] - - var minSeen: Double = 0 - - var maxSeen: Double = 0 - - var sum: Double = 0 - - var sumOfSquares: Double = 0 - - var count: Double = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_RequestResultCount: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var statusCode: Int32 = 0 - - var count: Int64 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct Grpc_Testing_ClientStats: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Latency histogram. Data points are in nanoseconds. - var latencies: Grpc_Testing_HistogramData { - get {return _latencies ?? Grpc_Testing_HistogramData()} - set {_latencies = newValue} - } - /// Returns true if `latencies` has been explicitly set. - var hasLatencies: Bool {return self._latencies != nil} - /// Clears the value of `latencies`. Subsequent reads from it will return its default value. - mutating func clearLatencies() {self._latencies = nil} - - /// See ServerStats for details. - var timeElapsed: Double = 0 - - var timeUser: Double = 0 - - var timeSystem: Double = 0 - - /// Number of failed requests (one row per status code seen) - var requestResults: [Grpc_Testing_RequestResultCount] = [] - - /// Number of polls called inside completion queue - var cqPollCount: UInt64 = 0 - - /// Core library stats - var coreStats: Grpc_Core_Stats { - get {return _coreStats ?? Grpc_Core_Stats()} - set {_coreStats = newValue} - } - /// Returns true if `coreStats` has been explicitly set. - var hasCoreStats: Bool {return self._coreStats != nil} - /// Clears the value of `coreStats`. Subsequent reads from it will return its default value. - mutating func clearCoreStats() {self._coreStats = nil} - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _latencies: Grpc_Testing_HistogramData? = nil - fileprivate var _coreStats: Grpc_Core_Stats? = nil -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.testing" - -extension Grpc_Testing_ServerStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerStats" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "time_elapsed"), - 2: .standard(proto: "time_user"), - 3: .standard(proto: "time_system"), - 4: .standard(proto: "total_cpu_time"), - 5: .standard(proto: "idle_cpu_time"), - 6: .standard(proto: "cq_poll_count"), - 7: .standard(proto: "core_stats"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.timeElapsed) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.timeUser) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &self.timeSystem) }() - case 4: try { try decoder.decodeSingularUInt64Field(value: &self.totalCpuTime) }() - case 5: try { try decoder.decodeSingularUInt64Field(value: &self.idleCpuTime) }() - case 6: try { try decoder.decodeSingularUInt64Field(value: &self.cqPollCount) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._coreStats) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.timeElapsed.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 1) - } - if self.timeUser.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 2) - } - if self.timeSystem.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 3) - } - if self.totalCpuTime != 0 { - try visitor.visitSingularUInt64Field(value: self.totalCpuTime, fieldNumber: 4) - } - if self.idleCpuTime != 0 { - try visitor.visitSingularUInt64Field(value: self.idleCpuTime, fieldNumber: 5) - } - if self.cqPollCount != 0 { - try visitor.visitSingularUInt64Field(value: self.cqPollCount, fieldNumber: 6) - } - try { if let v = self._coreStats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ServerStats, rhs: Grpc_Testing_ServerStats) -> Bool { - if lhs.timeElapsed != rhs.timeElapsed {return false} - if lhs.timeUser != rhs.timeUser {return false} - if lhs.timeSystem != rhs.timeSystem {return false} - if lhs.totalCpuTime != rhs.totalCpuTime {return false} - if lhs.idleCpuTime != rhs.idleCpuTime {return false} - if lhs.cqPollCount != rhs.cqPollCount {return false} - if lhs._coreStats != rhs._coreStats {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_HistogramParams: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HistogramParams" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "resolution"), - 2: .standard(proto: "max_possible"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularDoubleField(value: &self.resolution) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.maxPossible) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.resolution.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.resolution, fieldNumber: 1) - } - if self.maxPossible.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.maxPossible, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_HistogramParams, rhs: Grpc_Testing_HistogramParams) -> Bool { - if lhs.resolution != rhs.resolution {return false} - if lhs.maxPossible != rhs.maxPossible {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_HistogramData: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".HistogramData" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "bucket"), - 2: .standard(proto: "min_seen"), - 3: .standard(proto: "max_seen"), - 4: .same(proto: "sum"), - 5: .standard(proto: "sum_of_squares"), - 6: .same(proto: "count"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedUInt32Field(value: &self.bucket) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.minSeen) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &self.maxSeen) }() - case 4: try { try decoder.decodeSingularDoubleField(value: &self.sum) }() - case 5: try { try decoder.decodeSingularDoubleField(value: &self.sumOfSquares) }() - case 6: try { try decoder.decodeSingularDoubleField(value: &self.count) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.bucket.isEmpty { - try visitor.visitPackedUInt32Field(value: self.bucket, fieldNumber: 1) - } - if self.minSeen.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.minSeen, fieldNumber: 2) - } - if self.maxSeen.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.maxSeen, fieldNumber: 3) - } - if self.sum.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.sum, fieldNumber: 4) - } - if self.sumOfSquares.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.sumOfSquares, fieldNumber: 5) - } - if self.count.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.count, fieldNumber: 6) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_HistogramData, rhs: Grpc_Testing_HistogramData) -> Bool { - if lhs.bucket != rhs.bucket {return false} - if lhs.minSeen != rhs.minSeen {return false} - if lhs.maxSeen != rhs.maxSeen {return false} - if lhs.sum != rhs.sum {return false} - if lhs.sumOfSquares != rhs.sumOfSquares {return false} - if lhs.count != rhs.count {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_RequestResultCount: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".RequestResultCount" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "status_code"), - 2: .same(proto: "count"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.statusCode) }() - case 2: try { try decoder.decodeSingularInt64Field(value: &self.count) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.statusCode != 0 { - try visitor.visitSingularInt32Field(value: self.statusCode, fieldNumber: 1) - } - if self.count != 0 { - try visitor.visitSingularInt64Field(value: self.count, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_RequestResultCount, rhs: Grpc_Testing_RequestResultCount) -> Bool { - if lhs.statusCode != rhs.statusCode {return false} - if lhs.count != rhs.count {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Testing_ClientStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ClientStats" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "latencies"), - 2: .standard(proto: "time_elapsed"), - 3: .standard(proto: "time_user"), - 4: .standard(proto: "time_system"), - 5: .standard(proto: "request_results"), - 6: .standard(proto: "cq_poll_count"), - 7: .standard(proto: "core_stats"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularMessageField(value: &self._latencies) }() - case 2: try { try decoder.decodeSingularDoubleField(value: &self.timeElapsed) }() - case 3: try { try decoder.decodeSingularDoubleField(value: &self.timeUser) }() - case 4: try { try decoder.decodeSingularDoubleField(value: &self.timeSystem) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &self.requestResults) }() - case 6: try { try decoder.decodeSingularUInt64Field(value: &self.cqPollCount) }() - case 7: try { try decoder.decodeSingularMessageField(value: &self._coreStats) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - try { if let v = self._latencies { - try visitor.visitSingularMessageField(value: v, fieldNumber: 1) - } }() - if self.timeElapsed.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.timeElapsed, fieldNumber: 2) - } - if self.timeUser.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.timeUser, fieldNumber: 3) - } - if self.timeSystem.bitPattern != 0 { - try visitor.visitSingularDoubleField(value: self.timeSystem, fieldNumber: 4) - } - if !self.requestResults.isEmpty { - try visitor.visitRepeatedMessageField(value: self.requestResults, fieldNumber: 5) - } - if self.cqPollCount != 0 { - try visitor.visitSingularUInt64Field(value: self.cqPollCount, fieldNumber: 6) - } - try { if let v = self._coreStats { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Testing_ClientStats, rhs: Grpc_Testing_ClientStats) -> Bool { - if lhs._latencies != rhs._latencies {return false} - if lhs.timeElapsed != rhs.timeElapsed {return false} - if lhs.timeUser != rhs.timeUser {return false} - if lhs.timeSystem != rhs.timeSystem {return false} - if lhs.requestResults != rhs.requestResults {return false} - if lhs.cqPollCount != rhs.cqPollCount {return false} - if lhs._coreStats != rhs._coreStats {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift deleted file mode 100644 index 58bad0ad0..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.grpc.swift +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -/// An integration test service that covers all the method signature permutations -/// of unary/streaming requests/responses. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/worker_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -internal enum Grpc_Testing_WorkerService { - internal static let descriptor = GRPCCore.ServiceDescriptor.grpc_testing_WorkerService - internal enum Method { - internal enum RunServer { - internal typealias Input = Grpc_Testing_ServerArgs - internal typealias Output = Grpc_Testing_ServerStatus - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, - method: "RunServer" - ) - } - internal enum RunClient { - internal typealias Input = Grpc_Testing_ClientArgs - internal typealias Output = Grpc_Testing_ClientStatus - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, - method: "RunClient" - ) - } - internal enum CoreCount { - internal typealias Input = Grpc_Testing_CoreRequest - internal typealias Output = Grpc_Testing_CoreResponse - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, - method: "CoreCount" - ) - } - internal enum QuitWorker { - internal typealias Input = Grpc_Testing_Void - internal typealias Output = Grpc_Testing_Void - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Grpc_Testing_WorkerService.descriptor.fullyQualifiedService, - method: "QuitWorker" - ) - } - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - RunServer.descriptor, - RunClient.descriptor, - CoreCount.descriptor, - QuitWorker.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Grpc_Testing_WorkerServiceStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Grpc_Testing_WorkerServiceServiceProtocol -} - -extension GRPCCore.ServiceDescriptor { - internal static let grpc_testing_WorkerService = Self( - package: "grpc.testing", - service: "WorkerService" - ) -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Grpc_Testing_WorkerServiceStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Start server with specified workload. - /// First request sent specifies the ServerConfig followed by ServerStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test server - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runServer( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Start client with specified workload. - /// First request sent specifies the ClientConfig followed by ClientStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test client - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runClient( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Just return the core count - unary call - func coreCount( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Quit this worker - func quitWorker( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_WorkerService.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Grpc_Testing_WorkerService.Method.RunServer.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.runServer( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_WorkerService.Method.RunClient.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.runClient( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_WorkerService.Method.CoreCount.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.coreCount( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Grpc_Testing_WorkerService.Method.QuitWorker.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.quitWorker( - request: request, - context: context - ) - } - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Grpc_Testing_WorkerServiceServiceProtocol: Grpc_Testing_WorkerService.StreamingServiceProtocol { - /// Start server with specified workload. - /// First request sent specifies the ServerConfig followed by ServerStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test server - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runServer( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Start client with specified workload. - /// First request sent specifies the ClientConfig followed by ClientStatus - /// response. After that, a "Mark" can be sent anytime to request the latest - /// stats. Closing the stream will initiate shutdown of the test client - /// and once the shutdown has finished, the OK status is sent to terminate - /// this RPC. - func runClient( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - /// Just return the core count - unary call - func coreCount( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - /// Quit this worker - func quitWorker( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single -} - -/// Partial conformance to `Grpc_Testing_WorkerServiceStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Grpc_Testing_WorkerService.ServiceProtocol { - internal func coreCount( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.coreCount( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - internal func quitWorker( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.quitWorker( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } -} \ No newline at end of file diff --git a/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift b/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift deleted file mode 100644 index 73f9c0029..000000000 --- a/Sources/performance-worker/Generated/grpc_testing_worker_service.pb.swift +++ /dev/null @@ -1,28 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: grpc/testing/worker_service.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2015 gRPC authors. -// -// 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. - -/// An integration test service that covers all the method signature permutations -/// of unary/streaming requests/responses. - -// This file contained no messages, enums, or extensions. diff --git a/Sources/performance-worker/PerformanceWorker.swift b/Sources/performance-worker/PerformanceWorker.swift deleted file mode 100644 index 83c9f7e82..000000000 --- a/Sources/performance-worker/PerformanceWorker.swift +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 ArgumentParser -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import NIOPosix - -@main -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct PerformanceWorker: AsyncParsableCommand { - static var configuration: CommandConfiguration { - CommandConfiguration( - commandName: "performance-worker", - discussion: """ - This program starts a gRPC server running the 'worker' service. The worker service is \ - instructed by a driver program to become a benchmark client or a benchmark server. - - Typically at least two workers are started (at least one server and one client), and the \ - driver instructs benchmark clients to execute various scenarios against benchmark servers. \ - Results are reported back to the driver once scenarios have been completed. - - See https://grpc.io/docs/guides/benchmarking for more details. - """ - ) - } - - @Option( - name: .customLong("driver_port"), - help: "Port to listen on for connections from the driver." - ) - var driverPort: Int - - func run() async throws { - debugOnly { - print("[WARNING] performance-worker built in DEBUG mode, results won't be representative.") - } - - let server = GRPCServer( - transport: .http2NIOPosix( - address: .ipv4(host: "127.0.0.1", port: self.driverPort), - config: .defaults(transportSecurity: .plaintext) - ), - services: [WorkerService()] - ) - try await server.serve() - } -} - -private func debugOnly(_ body: () -> Void) { - assert(alwaysTrue(body)) -} - -private func alwaysTrue(_ body: () -> Void) -> Bool { - body() - return true -} diff --git a/Sources/performance-worker/RPCStats.swift b/Sources/performance-worker/RPCStats.swift deleted file mode 100644 index bc2bba74b..000000000 --- a/Sources/performance-worker/RPCStats.swift +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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 Foundation -import GRPCCore -import NIOConcurrencyHelpers - -/// Stores the real time latency histogram and error code count dictionary, -/// for the RPCs made by a particular GRPCClient. It gets updated after -/// each finished RPC. -/// -/// The time latency is measured in nanoseconds. -struct RPCStats { - var latencyHistogram: LatencyHistogram - var requestResultCount: [RPCError.Code: Int64] - - init(latencyHistogram: LatencyHistogram, requestResultCount: [RPCError.Code: Int64] = [:]) { - self.latencyHistogram = latencyHistogram - self.requestResultCount = requestResultCount - } - - /// Histograms are stored with exponentially increasing bucket sizes. - /// The first bucket is [0, `multiplier`) where `multiplier` = 1 + resolution - /// Bucket n (n>=1) contains [`multiplier`**n, `multiplier`**(n+1)) - /// There are sufficient buckets to reach max_bucket_start - struct LatencyHistogram { - var sum: Double - var sumOfSquares: Double - var countOfValuesSeen: Double - var multiplier: Double - var oneOnLogMultiplier: Double - var minSeen: Double - var maxSeen: Double - var maxPossible: Double - var buckets: [UInt32] - - /// Initialise a histogram. - /// - parameters: - /// - resolution: Defines the width of the buckets - see the description of this structure. - /// - maxBucketStart: Defines the start of the greatest valued bucket. - init(resolution: Double = 0.01, maxBucketStart: Double = 60e9) { - precondition(resolution > 0.0) - precondition(maxBucketStart > resolution) - self.sum = 0.0 - self.sumOfSquares = 0.0 - self.multiplier = 1.0 + resolution - self.oneOnLogMultiplier = 1.0 / log(1.0 + resolution) - self.maxPossible = maxBucketStart - self.countOfValuesSeen = 0.0 - self.minSeen = maxBucketStart - self.maxSeen = 0.0 - let numBuckets = - LatencyHistogram.uncheckedBucket( - forValue: maxBucketStart, - oneOnLogMultiplier: self.oneOnLogMultiplier - ) + 1 - precondition(numBuckets > 1) - precondition(numBuckets < 100_000_000) - self.buckets = .init(repeating: 0, count: numBuckets) - } - - struct HistorgramShapeMismatch: Error {} - - /// Determine a bucket index given a value - does no bounds checking - private static func uncheckedBucket(forValue value: Double, oneOnLogMultiplier: Double) -> Int { - return Int(log(value) * oneOnLogMultiplier) - } - - private func bucket(forValue value: Double) -> Int { - let bucket = LatencyHistogram.uncheckedBucket( - forValue: min(self.maxPossible, max(0, value)), - oneOnLogMultiplier: self.oneOnLogMultiplier - ) - assert(bucket < self.buckets.count) - assert(bucket >= 0) - return bucket - } - - /// Add a value to this histogram, updating buckets and stats - /// - parameters: - /// - value: The value to add. - public mutating func record(_ value: Double) { - self.sum += value - self.sumOfSquares += value * value - self.countOfValuesSeen += 1 - if value < self.minSeen { - self.minSeen = value - } - if value > self.maxSeen { - self.maxSeen = value - } - self.buckets[self.bucket(forValue: value)] += 1 - } - - /// Merge two histograms together updating `self` - /// - parameters: - /// - other: the other histogram to merge into this. - public mutating func merge(_ other: LatencyHistogram) throws { - guard (self.buckets.count == other.buckets.count) || (self.multiplier == other.multiplier) - else { - // Fail because these histograms don't match. - throw HistorgramShapeMismatch() - } - - self.sum += other.sum - self.sumOfSquares += other.sumOfSquares - self.countOfValuesSeen += other.countOfValuesSeen - if other.minSeen < self.minSeen { - self.minSeen = other.minSeen - } - if other.maxSeen > self.maxSeen { - self.maxSeen = other.maxSeen - } - for bucket in 0 ..< self.buckets.count { - self.buckets[bucket] += other.buckets[bucket] - } - } - } - - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - mutating func merge(_ other: RPCStats) throws { - try self.latencyHistogram.merge(other.latencyHistogram) - self.requestResultCount.merge(other.requestResultCount) { (current, new) in - current + new - } - } -} diff --git a/Sources/performance-worker/ResourceUsage.swift b/Sources/performance-worker/ResourceUsage.swift deleted file mode 100644 index 7582a8328..000000000 --- a/Sources/performance-worker/ResourceUsage.swift +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 Dispatch -import NIOCore -import NIOFileSystem - -#if canImport(Darwin) -import Darwin -#elseif canImport(Musl) -import Musl -#elseif canImport(Glibc) -import Glibc -#else -let badOS = { fatalError("unsupported OS") }() -#endif - -#if canImport(Darwin) -private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF -#elseif canImport(Musl) || canImport(Glibc) -private let OUR_RUSAGE_SELF: Int32 = RUSAGE_SELF.rawValue -#endif - -/// Client resource usage stats. -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -internal struct ClientStats: Sendable { - var time: Double - var userTime: Double - var systemTime: Double - - init( - time: Double, - userTime: Double, - systemTime: Double - ) { - self.time = time - self.userTime = userTime - self.systemTime = systemTime - } - - init() { - self.time = Double(DispatchTime.now().uptimeNanoseconds) * 1e-9 - if let usage = System.resourceUsage() { - self.userTime = Double(usage.ru_utime.tv_sec) + Double(usage.ru_utime.tv_usec) * 1e-6 - self.systemTime = Double(usage.ru_stime.tv_sec) + Double(usage.ru_stime.tv_usec) * 1e-6 - } else { - self.userTime = 0 - self.systemTime = 0 - } - } - - internal func difference(to state: ClientStats) -> ClientStats { - return ClientStats( - time: self.time - state.time, - userTime: self.userTime - state.userTime, - systemTime: self.systemTime - state.systemTime - ) - } -} - -/// Server resource usage stats. -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -internal struct ServerStats: Sendable { - var time: Double - var userTime: Double - var systemTime: Double - var totalCPUTime: UInt64 - var idleCPUTime: UInt64 - - init( - time: Double, - userTime: Double, - systemTime: Double, - totalCPUTime: UInt64, - idleCPUTime: UInt64 - ) { - self.time = time - self.userTime = userTime - self.systemTime = systemTime - self.totalCPUTime = totalCPUTime - self.idleCPUTime = idleCPUTime - } - - init() async throws { - self.time = Double(DispatchTime.now().uptimeNanoseconds) * 1e-9 - if let usage = System.resourceUsage() { - self.userTime = Double(usage.ru_utime.tv_sec) + Double(usage.ru_utime.tv_usec) * 1e-6 - self.systemTime = Double(usage.ru_stime.tv_sec) + Double(usage.ru_stime.tv_usec) * 1e-6 - } else { - self.userTime = 0 - self.systemTime = 0 - } - let (totalCPUTime, idleCPUTime) = try await ServerStats.getTotalAndIdleCPUTime() - self.totalCPUTime = totalCPUTime - self.idleCPUTime = idleCPUTime - } - - internal func difference(to stats: ServerStats) -> ServerStats { - return ServerStats( - time: self.time - stats.time, - userTime: self.userTime - stats.userTime, - systemTime: self.systemTime - stats.systemTime, - totalCPUTime: self.totalCPUTime - stats.totalCPUTime, - idleCPUTime: self.idleCPUTime - stats.idleCPUTime - ) - } - - /// Computes the total and idle CPU time after extracting stats from the first line of '/proc/stat'. - /// - /// The first line in '/proc/stat' file looks as follows: - /// CPU [user] [nice] [system] [idle] [iowait] [irq] [softirq] - /// The totalCPUTime is computed as follows: - /// total = user + nice + system + idle - private static func getTotalAndIdleCPUTime() async throws -> ( - totalCPUTime: UInt64, idleCPUTime: UInt64 - ) { - #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(Linux) || os(Android) - let contents: ByteBuffer - do { - contents = try await ByteBuffer( - contentsOf: "/proc/stat", - maximumSizeAllowed: .kilobytes(20) - ) - } catch { - return (0, 0) - } - - let view = contents.readableBytesView - guard let firstNewLineIndex = view.firstIndex(of: UInt8(ascii: "\n")) else { - return (0, 0) - } - let firstLine = String(buffer: ByteBuffer(view[0 ... firstNewLineIndex])) - - let lineComponents = firstLine.components(separatedBy: " ") - if lineComponents.count < 5 || lineComponents[0] != "CPU" { - return (0, 0) - } - - let CPUTime: [UInt64] = lineComponents[1 ... 4].compactMap { UInt64($0) } - if CPUTime.count < 4 { - return (0, 0) - } - - let totalCPUTime = CPUTime.reduce(0, +) - return (totalCPUTime, CPUTime[3]) - - #else - return (0, 0) - #endif - } -} - -extension System { - fileprivate static func resourceUsage() -> rusage? { - var usage = rusage() - - if getrusage(OUR_RUSAGE_SELF, &usage) == 0 { - return usage - } else { - return nil - } - } -} diff --git a/Sources/performance-worker/WorkerService.swift b/Sources/performance-worker/WorkerService.swift deleted file mode 100644 index 945dca3a7..000000000 --- a/Sources/performance-worker/WorkerService.swift +++ /dev/null @@ -1,584 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class WorkerService: Sendable { - private let state: NIOLockedValueBox - - init() { - self.state = NIOLockedValueBox(State()) - } - - private struct State { - private var role: Role - - enum Role { - case none - case client(Client) - case server(Server) - } - - struct Server { - var server: GRPCServer - var stats: ServerStats - var eventLoopGroup: MultiThreadedEventLoopGroup - } - - struct Client { - var clients: [BenchmarkClient] - var stats: ClientStats - var rpcStats: RPCStats - } - - init() { - self.role = .none - } - - mutating func collectServerStats(replaceWith newStats: ServerStats? = nil) -> ServerStats? { - switch self.role { - case var .server(serverState): - let stats = serverState.stats - if let newStats = newStats { - serverState.stats = newStats - self.role = .server(serverState) - } - return stats - case .client, .none: - return nil - } - } - - mutating func collectClientStats( - replaceWith newStats: ClientStats? = nil - ) -> (ClientStats, RPCStats)? { - switch self.role { - case var .client(state): - // Grab the existing stats and update if necessary. - let stats = state.stats - if let newStats = newStats { - state.stats = newStats - } - - // Merge in RPC stats from each client. - for client in state.clients { - try? state.rpcStats.merge(client.currentStats) - } - - self.role = .client(state) - return (stats, state.rpcStats) - - case .server, .none: - return nil - } - } - - enum OnStartedServer { - case runServer - case invalidState(RPCError) - } - - mutating func startedServer( - _ server: GRPCServer, - stats: ServerStats, - eventLoopGroup: MultiThreadedEventLoopGroup - ) -> OnStartedServer { - let action: OnStartedServer - - switch self.role { - case .none: - let state = State.Server(server: server, stats: stats, eventLoopGroup: eventLoopGroup) - self.role = .server(state) - action = .runServer - case .server: - let error = RPCError(code: .alreadyExists, message: "A server has already been set up.") - action = .invalidState(error) - case .client: - let error = RPCError(code: .failedPrecondition, message: "This worker has a client setup.") - action = .invalidState(error) - } - - return action - } - - enum OnStartedClients { - case runClients - case invalidState(RPCError) - } - - mutating func startedClients( - _ clients: [BenchmarkClient], - stats: ClientStats, - rpcStats: RPCStats - ) -> OnStartedClients { - let action: OnStartedClients - - switch self.role { - case .none: - let state = State.Client(clients: clients, stats: stats, rpcStats: rpcStats) - self.role = .client(state) - action = .runClients - case .server: - let error = RPCError(code: .alreadyExists, message: "This worker has a server setup.") - action = .invalidState(error) - case .client: - let error = RPCError( - code: .failedPrecondition, - message: "Clients have already been set up." - ) - action = .invalidState(error) - } - - return action - } - - enum OnServerShutDown { - case shutdown(MultiThreadedEventLoopGroup) - case nothing - } - - mutating func serverShutdown() -> OnServerShutDown { - switch self.role { - case .client: - preconditionFailure("Invalid state") - case .server(let state): - self.role = .none - return .shutdown(state.eventLoopGroup) - case .none: - return .nothing - } - } - - enum OnStopListening { - case stopListening(GRPCServer) - case nothing - } - - func stopListening() -> OnStopListening { - switch self.role { - case .client: - preconditionFailure("Invalid state") - case .server(let state): - return .stopListening(state.server) - case .none: - return .nothing - } - } - - enum OnCloseClient { - case close([BenchmarkClient]) - case nothing - } - - mutating func closeClients() -> OnCloseClient { - switch self.role { - case .client(let state): - self.role = .none - return .close(state.clients) - case .server: - preconditionFailure("Invalid state") - case .none: - return .nothing - } - } - - enum OnQuitWorker { - case shutDownServer(GRPCServer) - case shutDownClients([BenchmarkClient]) - case nothing - } - - mutating func quit() -> OnQuitWorker { - switch self.role { - case .none: - return .nothing - case .client(let state): - self.role = .none - return .shutDownClients(state.clients) - case .server(let state): - self.role = .none - return .shutDownServer(state.server) - } - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension WorkerService: Grpc_Testing_WorkerService.ServiceProtocol { - func quitWorker( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - let onQuit = self.state.withLockedValue { $0.quit() } - - switch onQuit { - case .nothing: - () - - case .shutDownClients(let clients): - for client in clients { - client.shutdown() - } - - case .shutDownServer(let server): - server.beginGracefulShutdown() - } - - return ServerResponse.Single(message: Grpc_Testing_Void()) - } - - func coreCount( - request: ServerRequest.Single, - context: ServerContext - ) async throws -> ServerResponse.Single { - let coreCount = System.coreCount - return ServerResponse.Single( - message: Grpc_Testing_WorkerService.Method.CoreCount.Output.with { - $0.cores = Int32(coreCount) - } - ) - } - - func runServer( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in - try await withThrowingTaskGroup(of: Void.self) { group in - for try await message in request.messages { - switch message.argtype { - case let .some(.setup(serverConfig)): - let (server, transport) = try await self.startServer(serverConfig) - group.addTask { - let result: Result - - do { - try await server.serve() - result = .success(()) - } catch { - result = .failure(error) - } - - switch self.state.withLockedValue({ $0.serverShutdown() }) { - case .shutdown(let eventLoopGroup): - try await eventLoopGroup.shutdownGracefully() - case .nothing: - () - } - - try result.get() - } - - // Wait for the server to bind. - let address = try await transport.listeningAddress - - let port: Int - if let ipv4 = address.ipv4 { - port = ipv4.port - } else if let ipv6 = address.ipv6 { - port = ipv6.port - } else { - throw RPCError( - code: .internalError, - message: "Server listening on unsupported address '\(address)'" - ) - } - - // Tell the client what port the server is listening on. - let message = Grpc_Testing_ServerStatus.with { $0.port = Int32(port) } - try await writer.write(message) - - case let .some(.mark(mark)): - let response = try await self.makeServerStatsResponse(reset: mark.reset) - try await writer.write(response) - - case .none: - () - } - } - - // Request stream ended, tell the server to stop listening. Once it's finished it will - // shutdown its ELG. - switch self.state.withLockedValue({ $0.stopListening() }) { - case .stopListening(let server): - server.beginGracefulShutdown() - case .nothing: - () - } - } - - return [:] - } - } - - func runClient( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in - try await withThrowingTaskGroup(of: Void.self) { group in - for try await message in request.messages { - switch message.argtype { - case let .setup(config): - // Create the clients with the initial stats. - let clients = try await self.setupClients(config) - - for client in clients { - group.addTask { - try await client.run() - } - } - - let message = try await self.makeClientStatsResponse(reset: false) - try await writer.write(message) - - case let .mark(mark): - let response = try await self.makeClientStatsResponse(reset: mark.reset) - try await writer.write(response) - - case .none: - () - } - } - - switch self.state.withLockedValue({ $0.closeClients() }) { - case .close(let clients): - for client in clients { - client.shutdown() - } - case .nothing: - () - } - - try await group.waitForAll() - - return [:] - } - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension WorkerService { - private func startServer( - _ serverConfig: Grpc_Testing_ServerConfig - ) async throws -> (GRPCServer, HTTP2ServerTransport.Posix) { - // Prepare an ELG, the test might require more than the default of one. - let numberOfThreads: Int - if serverConfig.asyncServerThreads > 0 { - numberOfThreads = Int(serverConfig.asyncServerThreads) - } else { - numberOfThreads = System.coreCount - } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads) - - // Don't restrict the max payload size, the client is always trusted. - var config = HTTP2ServerTransport.Posix.Config.defaults(transportSecurity: .plaintext) - config.rpc.maxRequestPayloadSize = .max - - let transport = HTTP2ServerTransport.Posix( - address: .ipv4(host: "127.0.0.1", port: Int(serverConfig.port)), - config: config, - eventLoopGroup: eventLoopGroup - ) - - let server = GRPCServer(transport: transport, services: [BenchmarkService()]) - let stats = try await ServerStats() - - // Hold on to the server and ELG in the state machine. - let action = self.state.withLockedValue { - $0.startedServer(server, stats: stats, eventLoopGroup: eventLoopGroup) - } - - switch action { - case .runServer: - return (server, transport) - case .invalidState(let error): - server.beginGracefulShutdown() - try await eventLoopGroup.shutdownGracefully() - throw error - } - } - - private func makeServerStatsResponse( - reset: Bool - ) async throws -> Grpc_Testing_WorkerService.Method.RunServer.Output { - let currentStats = try await ServerStats() - let initialStats = self.state.withLockedValue { state in - return state.collectServerStats(replaceWith: reset ? currentStats : nil) - } - - guard let initialStats = initialStats else { - throw RPCError( - code: .notFound, - message: "There are no initial server stats. A server must be setup before calling 'mark'." - ) - } - - let differences = currentStats.difference(to: initialStats) - return Grpc_Testing_WorkerService.Method.RunServer.Output.with { - $0.stats = Grpc_Testing_ServerStats.with { - $0.idleCpuTime = differences.idleCPUTime - $0.timeElapsed = differences.time - $0.timeSystem = differences.systemTime - $0.timeUser = differences.userTime - $0.totalCpuTime = differences.totalCPUTime - } - } - } - - private func setupClients(_ config: Grpc_Testing_ClientConfig) async throws -> [BenchmarkClient] { - guard let rpcType = BenchmarkClient.RPCType(config.rpcType) else { - throw RPCError(code: .invalidArgument, message: "Unknown RPC type") - } - - // Parse the server targets into resolvable targets. - let ipv4Addresses = try self.parseServerTargets(config.serverTargets) - let target = ResolvableTargets.IPv4(addresses: ipv4Addresses) - - var clients = [BenchmarkClient]() - for _ in 0 ..< config.clientChannels { - let client = BenchmarkClient( - client: GRPCClient( - transport: try .http2NIOPosix( - target: target, - config: .defaults(transportSecurity: .plaintext) - ) - ), - concurrentRPCs: Int(config.outstandingRpcsPerChannel), - rpcType: rpcType, - messagesPerStream: Int(config.messagesPerStream), - protoParams: config.payloadConfig.simpleParams, - histogramParams: config.histogramParams - ) - - clients.append(client) - } - - let stats = ClientStats() - let histogram = RPCStats.LatencyHistogram( - resolution: config.histogramParams.resolution, - maxBucketStart: config.histogramParams.maxPossible - ) - let rpcStats = RPCStats(latencyHistogram: histogram) - - let action = self.state.withLockedValue { state in - state.startedClients(clients, stats: stats, rpcStats: rpcStats) - } - - switch action { - case .runClients: - return clients - case .invalidState(let error): - for client in clients { - client.shutdown() - } - throw error - } - } - - private func parseServerTarget(_ target: String) -> GRPCHTTP2Core.SocketAddress.IPv4? { - guard let index = target.firstIndex(of: ":") else { return nil } - - let host = target[.. [GRPCHTTP2Core.SocketAddress.IPv4] { - try targets.map { target in - if let ipv4 = self.parseServerTarget(target) { - return ipv4 - } else { - throw RPCError( - code: .invalidArgument, - message: """ - Couldn't parse target '\(target)'. Must be in the format ':' for IPv4 \ - or '[]:' for IPv6. - """ - ) - } - } - } - - private func makeClientStatsResponse( - reset: Bool - ) async throws -> Grpc_Testing_WorkerService.Method.RunClient.Output { - let currentUsageStats = ClientStats() - - let stats = self.state.withLockedValue { state in - state.collectClientStats(replaceWith: reset ? currentUsageStats : nil) - } - - guard let (initialUsageStats, rpcStats) = stats else { - throw RPCError( - code: .notFound, - message: "There are no initial client stats. Clients must be setup before calling 'mark'." - ) - } - - let differences = currentUsageStats.difference(to: initialUsageStats) - - let requestResults = rpcStats.requestResultCount.map { (key, value) in - return Grpc_Testing_RequestResultCount.with { - $0.statusCode = Int32(key.rawValue) - $0.count = value - } - } - - return Grpc_Testing_WorkerService.Method.RunClient.Output.with { - $0.stats = Grpc_Testing_ClientStats.with { - $0.timeElapsed = differences.time - $0.timeSystem = differences.systemTime - $0.timeUser = differences.userTime - $0.requestResults = requestResults - $0.latencies = Grpc_Testing_HistogramData.with { - $0.bucket = rpcStats.latencyHistogram.buckets - $0.minSeen = rpcStats.latencyHistogram.minSeen - $0.maxSeen = rpcStats.latencyHistogram.maxSeen - $0.sum = rpcStats.latencyHistogram.sum - $0.sumOfSquares = rpcStats.latencyHistogram.sumOfSquares - $0.count = rpcStats.latencyHistogram.countOfValuesSeen - } - } - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension BenchmarkClient.RPCType { - init?(_ rpcType: Grpc_Testing_RpcType) { - switch rpcType { - case .unary: - self = .unary - case .streaming: - self = .streaming - default: - return nil - } - } -} diff --git a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md b/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md deleted file mode 100644 index 61a2e10f0..000000000 --- a/Sources/protoc-gen-grpc-swift/Docs.docc/spm-plugin.md +++ /dev/null @@ -1,152 +0,0 @@ -# Using the Swift Package Manager plugin - -The Swift Package Manager introduced new plugin capabilities in Swift 5.6, enabling the extension of -the build process with custom build tools. Learn how to use the `GRPCSwiftPlugin` plugin for the -Swift Package Manager. - -## Overview - -> Warning: Due to limitations of binary executable discovery with Xcode we only recommend using the Swift Package Manager -plugin in leaf packages. For more information, read the `Defining the path to the protoc binary` section of -this article. - -The plugin works by running the system installed `protoc` compiler with the `protoc-gen-grpc-swift` plugin -for specified `.proto` files in your targets source folder. Furthermore, the plugin allows defining a -configuration file which will be used to customize the invocation of `protoc`. - -### Installing the protoc compiler - -First, you must ensure that you have the `protoc` compiler installed. -There are multiple ways to do this. Some of the easiest are: - -1. If you are on macOS, installing it via `brew install protobuf` -2. Download the binary from [Google's github repository](https://github.com/protocolbuffers/protobuf). - -### Adding the plugin to your manifest - -First, you need to add a dependency on `grpc-swift`. Afterwards, you can declare the usage of the plugin -for your target. Here is an example snippet of a `Package.swift` manifest: - -```swift -let package = Package( - name: "YourPackage", - products: [...], - dependencies: [ - ... - .package(url: "https://github.com/grpc/grpc-swift", from: "1.10.0"), - ... - ], - targets: [ - ... - .executableTarget( - name: "YourTarget", - plugins: [ - .plugin(name: "GRPCSwiftPlugin", package: "grpc-swift") - ] - ), - ... -) - -``` - -### Configuring the plugin - -Configuring the plugin is done by adding a `grpc-swift-config.json` file anywhere in your target's sources. Before you start configuring the plugin, you need to add the `.proto` files to your sources. You should also commit these files to your git repository since the generated types are now generated on demand. It's also important to note that the proto files in your configuration should be in the same directory as the config file. Let's see an example to have a better understanding. - -Here's an example file structure that looks like this: - -```text -Sources -├── main.swift -└── ProtoBuf - ├── grpc-swift-config.json - ├── foo.proto - └── Bar - └── Bar.proto -``` - -```json -{ - "invocations": [ - { - "protoFiles": [ - "Foo.proto", - ], - "visibility": "internal", - "server": false - }, - { - "protoFiles": [ - "Bar/Bar.proto" - ], - "visibility": "public", - "client": false, - "keepMethodCasing": false, - "reflectionData": true - } - ] -} -``` - -> Note: paths to your `.proto` files will have to include the relative path from the config file directory to the `.proto` file location. -> Files **must** be contained within the same directory as the config file. - -In the above configuration, notice the relative path of the `.proto` file with respect to the configuration file. If you add a file in the `Sources` folder, the plugin would be unable to access it as the path is computed relative to the `grpc-swift-config.json` file. So the `.proto` files have to be added within the `ProtoBuf` folder, with relative paths taken into consideration. -Here, you declared two invocations to the `protoc` compiler. The first invocation -is generating Swift types for the `Foo.proto` file with `internal` visibility. -We have also specified the `server` option and set it to false: this means that server code won't be generated for this proto. -The second invocation is generating Swift types for the `Bar.proto` file with the `public` visibility. -Notice the `client` option: it's been set to false, so no client code will be generated for this proto. We have also set -the `keepMethodCasing` option to false, which means that the casing of the autogenerated captions won't be kept. - -> Note: You can find more information about supported options in the protoc Swift plugin documentation. Be aware that -`server`, `client` and `keepMethodCasing` are currently the only three options supported in the Swift Package Manager plugin. - -### Defining the path to the protoc binary - -The plugin needs to be able to invoke the `protoc` binary to generate the Swift types. There are several ways to achieve this. - -First, by default, the package manager looks into the `$PATH` to find binaries named `protoc`. -This works immediately if you use `swift build` to build your package and `protoc` is installed -in the `$PATH` (`brew` is adding it to your `$PATH` automatically). -However, this doesn't work if you want to compile from Xcode since Xcode is not passed the `$PATH`. - -If compiling from Xcode, you have **three options** to set the path of `protoc` that the plugin is going to use: - -* Set an environment variable `PROTOC_PATH` that gets picked up by the plugin. Here are two examples of how you can achieve this: - -```shell -# swift build -env PROTOC_PATH=/opt/homebrew/bin/protoc swift build - -# To start Xcode (Xcode MUST NOT be running before invoking this) -env PROTOC_PATH=/opt/homebrew/bin/protoc xed . - -# xcodebuild -env PROTOC_PATH=/opt/homebrew/bin/protoc xcodebuild -``` - -* Point the plugin to the concrete location of the `protoc` compiler is by changing the configuration file like this: - -```json -{ - "protocPath": "/path/to/protoc", - "invocations": [...] -} -``` - -* You can start Xcode by running `$ xed .` from the command line from the directory your project is located - this should make `$PATH` visible to Xcode. - -### Known Issues - -- The configuration file _must not_ be excluded from the list of sources for the - target in the package manifest (that is, it should not be present in the - `exclude` argument for the target). The build system does not have access to - the file if it is excluded, however, `swift build` will result in a warning - that the file should be excluded. -- The plugin should only be used for leaf package. The configuration file option - only solves the problem for leaf packages that are using the Swift package - manager plugin since there you can point the package manager to the right - binary. The environment variable does solve the problem for transitive - packages as well; however, it requires your users to set the variable now. - diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift deleted file mode 100644 index 7d75b9d43..000000000 --- a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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 Foundation -import SwiftProtobuf -import SwiftProtobufPluginLibrary - -#if compiler(>=6.0) -import GRPCCodeGen -import GRPCProtobufCodeGen -#endif - -@main -final class GenerateGRPC: CodeGenerator { - var version: String? { - Version.versionString - } - - var projectURL: String { - "https://github.com/grpc/grpc-swift" - } - - var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { - [.proto3Optional, .supportsEditions] - } - - var supportedEditionRange: ClosedRange { - Google_Protobuf_Edition.proto2 ... Google_Protobuf_Edition.edition2023 - } - - // A count of generated files by desired name (actual name may differ to avoid collisions). - private var generatedFileNames: [String: Int] = [:] - - func generate( - files fileDescriptors: [FileDescriptor], - parameter: any CodeGeneratorParameter, - protoCompilerContext: any ProtoCompilerContext, - generatorOutputs outputs: any GeneratorOutputs - ) throws { - let options = try GeneratorOptions(parameter: parameter) - - for descriptor in fileDescriptors { - if options.generateReflectionData { - try self.generateReflectionData( - descriptor, - options: options, - outputs: outputs - ) - } - - if descriptor.services.isEmpty { - continue - } - - if options.generateClient || options.generateServer || options.generateTestClient { - #if compiler(>=6.0) - if options.v2 { - try self.generateV2Stubs(descriptor, options: options, outputs: outputs) - } else { - try self.generateV1Stubs(descriptor, options: options, outputs: outputs) - } - #else - try self.generateV1Stubs(descriptor, options: options, outputs: outputs) - #endif - } - } - } - - private func generateReflectionData( - _ descriptor: FileDescriptor, - options: GeneratorOptions, - outputs: any GeneratorOutputs - ) throws { - let fileName = self.uniqueOutputFileName( - fileDescriptor: descriptor, - fileNamingOption: options.fileNaming, - extension: "reflection" - ) - - var options = ExtractProtoOptions() - options.includeSourceCodeInfo = true - let proto = descriptor.extractProto(options: options) - let serializedProto = try proto.serializedData() - let reflectionData = serializedProto.base64EncodedString() - try outputs.add(fileName: fileName, contents: reflectionData) - } - - private func generateV1Stubs( - _ descriptor: FileDescriptor, - options: GeneratorOptions, - outputs: any GeneratorOutputs - ) throws { - let fileName = self.uniqueOutputFileName( - fileDescriptor: descriptor, - fileNamingOption: options.fileNaming - ) - - let fileGenerator = Generator(descriptor, options: options) - try outputs.add(fileName: fileName, contents: fileGenerator.code) - } - - #if compiler(>=6.0) - private func generateV2Stubs( - _ descriptor: FileDescriptor, - options: GeneratorOptions, - outputs: any GeneratorOutputs - ) throws { - let fileName = self.uniqueOutputFileName( - fileDescriptor: descriptor, - fileNamingOption: options.fileNaming - ) - - let config = SourceGenerator.Config(options: options) - let fileGenerator = ProtobufCodeGenerator(configuration: config) - let contents = try fileGenerator.generateCode( - from: descriptor, - protoFileModuleMappings: options.protoToModuleMappings, - extraModuleImports: options.extraModuleImports - ) - - try outputs.add(fileName: fileName, contents: contents) - } - #endif -} - -extension GenerateGRPC { - private func uniqueOutputFileName( - fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming, - component: String = "grpc", - extension: String = "swift" - ) -> String { - let defaultName = outputFileName( - component: component, - fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption, - extension: `extension` - ) - if let count = self.generatedFileNames[defaultName] { - self.generatedFileNames[defaultName] = count + 1 - return outputFileName( - component: "\(count)." + component, - fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption, - extension: `extension` - ) - } else { - self.generatedFileNames[defaultName] = 1 - return defaultName - } - } - - private func outputFileName( - component: String, - fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming, - extension: String - ) -> String { - let ext = "." + component + "." + `extension` - let pathParts = splitPath(pathname: fileDescriptor.name) - switch fileNamingOption { - case .fullPath: - return pathParts.dir + pathParts.base + ext - case .pathToUnderscores: - let dirWithUnderscores = - pathParts.dir.replacingOccurrences(of: "/", with: "_") - return dirWithUnderscores + pathParts.base + ext - case .dropPath: - return pathParts.base + ext - } - } -} - -// from apple/swift-protobuf/Sources/protoc-gen-swift/StringUtils.swift -private func splitPath(pathname: String) -> (dir: String, base: String, suffix: String) { - var dir = "" - var base = "" - var suffix = "" - - for character in pathname { - if character == "/" { - dir += base + suffix + String(character) - base = "" - suffix = "" - } else if character == "." { - base += suffix - suffix = String(character) - } else { - suffix += String(character) - } - } - - let validSuffix = suffix.isEmpty || suffix.first == "." - if !validSuffix { - base += suffix - suffix = "" - } - return (dir: dir, base: base, suffix: suffix) -} - -#if compiler(>=6.0) -extension SourceGenerator.Config { - init(options: GeneratorOptions) { - let accessLevel: SourceGenerator.Config.AccessLevel - switch options.visibility { - case .internal: - accessLevel = .internal - case .package: - accessLevel = .package - case .public: - accessLevel = .public - } - - self.init( - accessLevel: accessLevel, - accessLevelOnImports: options.useAccessLevelOnImports, - client: options.generateClient, - server: options.generateServer - ) - } -} -#endif diff --git a/Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift b/Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift deleted file mode 100644 index 9788c5def..000000000 --- a/Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift +++ /dev/null @@ -1,246 +0,0 @@ -/* - * 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 SwiftProtobuf -import SwiftProtobufPluginLibrary - -// MARK: - Client protocol - -extension Generator { - internal func printAsyncServiceClientProtocol() { - let comments = self.service.protoSourceComments() - if !comments.isEmpty { - // Source comments already have the leading '///' - self.println(comments, newline: false) - } - - self.printAvailabilityForAsyncAwait() - self.println("\(self.access) protocol \(self.asyncClientProtocolName): GRPCClient {") - self.withIndentation { - self.println("static var serviceDescriptor: GRPCServiceDescriptor { get }") - self.println("var interceptors: \(self.clientInterceptorProtocolName)? { get }") - - for method in service.methods { - self.println() - self.method = method - - let rpcType = streamingType(self.method) - let callType = Types.call(for: rpcType) - - let arguments: [String] - switch rpcType { - case .unary, .serverStreaming: - arguments = [ - "_ request: \(self.methodInputName)", - "callOptions: \(Types.clientCallOptions)?", - ] - - case .clientStreaming, .bidirectionalStreaming: - arguments = [ - "callOptions: \(Types.clientCallOptions)?" - ] - } - - self.printFunction( - name: self.methodMakeFunctionCallName, - arguments: arguments, - returnType: "\(callType)<\(self.methodInputName), \(self.methodOutputName)>", - bodyBuilder: nil - ) - } - } - self.println("}") // protocol - } -} - -// MARK: - Client protocol default implementation: Calls - -extension Generator { - internal func printAsyncClientProtocolExtension() { - self.printAvailabilityForAsyncAwait() - self.withIndentation("extension \(self.asyncClientProtocolName)", braces: .curly) { - // Service descriptor. - self.withIndentation( - "\(self.access) static var serviceDescriptor: GRPCServiceDescriptor", - braces: .curly - ) { - self.println("return \(self.serviceClientMetadata).serviceDescriptor") - } - - self.println() - - // Interceptor factory. - self.withIndentation( - "\(self.access) var interceptors: \(self.clientInterceptorProtocolName)?", - braces: .curly - ) { - self.println("return nil") - } - - // 'Unsafe' calls. - for method in self.service.methods { - self.println() - self.method = method - - let rpcType = streamingType(self.method) - let callType = Types.call(for: rpcType) - let callTypeWithoutPrefix = Types.call(for: rpcType, withGRPCPrefix: false) - - switch rpcType { - case .unary, .serverStreaming: - self.printFunction( - name: self.methodMakeFunctionCallName, - arguments: [ - "_ request: \(self.methodInputName)", - "callOptions: \(Types.clientCallOptions)? = nil", - ], - returnType: "\(callType)<\(self.methodInputName), \(self.methodOutputName)>", - access: self.access - ) { - self.withIndentation("return self.make\(callTypeWithoutPrefix)", braces: .round) { - self.println("path: \(self.methodPathUsingClientMetadata),") - self.println("request: request,") - self.println("callOptions: callOptions ?? self.defaultCallOptions,") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []" - ) - } - } - - case .clientStreaming, .bidirectionalStreaming: - self.printFunction( - name: self.methodMakeFunctionCallName, - arguments: ["callOptions: \(Types.clientCallOptions)? = nil"], - returnType: "\(callType)<\(self.methodInputName), \(self.methodOutputName)>", - access: self.access - ) { - self.withIndentation("return self.make\(callTypeWithoutPrefix)", braces: .round) { - self.println("path: \(self.methodPathUsingClientMetadata),") - self.println("callOptions: callOptions ?? self.defaultCallOptions,") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []" - ) - } - } - } - } - } - } -} - -// MARK: - Client protocol extension: "Simple, but safe" call wrappers. - -extension Generator { - internal func printAsyncClientProtocolSafeWrappersExtension() { - self.printAvailabilityForAsyncAwait() - self.withIndentation("extension \(self.asyncClientProtocolName)", braces: .curly) { - for (i, method) in self.service.methods.enumerated() { - self.method = method - - let rpcType = streamingType(self.method) - let callTypeWithoutPrefix = Types.call(for: rpcType, withGRPCPrefix: false) - - let streamsResponses = [.serverStreaming, .bidirectionalStreaming].contains(rpcType) - let streamsRequests = [.clientStreaming, .bidirectionalStreaming].contains(rpcType) - - // (protocol, requires sendable) - let sequenceProtocols: [(String, Bool)?] = - streamsRequests - ? [("Sequence", false), ("AsyncSequence", true)] - : [nil] - - for (j, sequenceProtocol) in sequenceProtocols.enumerated() { - // Print a new line if this is not the first function in the extension. - if i > 0 || j > 0 { - self.println() - } - let functionName = - streamsRequests - ? "\(self.methodFunctionName)" - : self.methodFunctionName - let requestParamName = streamsRequests ? "requests" : "request" - let requestParamType = streamsRequests ? "RequestStream" : self.methodInputName - let returnType = - streamsResponses - ? Types.responseStream(of: self.methodOutputName) - : self.methodOutputName - let maybeWhereClause = sequenceProtocol.map { protocolName, mustBeSendable -> String in - let constraints = [ - "RequestStream: \(protocolName)" + (mustBeSendable ? " & Sendable" : ""), - "RequestStream.Element == \(self.methodInputName)", - ] - - return "where " + constraints.joined(separator: ", ") - } - self.printFunction( - name: functionName, - arguments: [ - "_ \(requestParamName): \(requestParamType)", - "callOptions: \(Types.clientCallOptions)? = nil", - ], - returnType: returnType, - access: self.access, - async: !streamsResponses, - throws: !streamsResponses, - genericWhereClause: maybeWhereClause - ) { - self.withIndentation( - "return\(!streamsResponses ? " try await" : "") self.perform\(callTypeWithoutPrefix)", - braces: .round - ) { - self.println("path: \(self.methodPathUsingClientMetadata),") - self.println("\(requestParamName): \(requestParamName),") - self.println("callOptions: callOptions ?? self.defaultCallOptions,") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []" - ) - } - } - } - } - } - } -} - -// MARK: - Client protocol implementation - -extension Generator { - internal func printAsyncServiceClientImplementation() { - self.printAvailabilityForAsyncAwait() - self.withIndentation( - "\(self.access) struct \(self.asyncClientStructName): \(self.asyncClientProtocolName)", - braces: .curly - ) { - self.println("\(self.access) var channel: GRPCChannel") - self.println("\(self.access) var defaultCallOptions: CallOptions") - self.println("\(self.access) var interceptors: \(self.clientInterceptorProtocolName)?") - self.println() - - self.println("\(self.access) init(") - self.withIndentation { - self.println("channel: GRPCChannel,") - self.println("defaultCallOptions: CallOptions = CallOptions(),") - self.println("interceptors: \(self.clientInterceptorProtocolName)? = nil") - } - self.println(") {") - self.withIndentation { - self.println("self.channel = channel") - self.println("self.defaultCallOptions = defaultCallOptions") - self.println("self.interceptors = interceptors") - } - self.println("}") - } - } -} diff --git a/Sources/protoc-gen-grpc-swift/Generator-Client.swift b/Sources/protoc-gen-grpc-swift/Generator-Client.swift deleted file mode 100644 index d18082132..000000000 --- a/Sources/protoc-gen-grpc-swift/Generator-Client.swift +++ /dev/null @@ -1,688 +0,0 @@ -/* - * Copyright 2018, 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 Foundation -import SwiftProtobuf -import SwiftProtobufPluginLibrary - -extension Generator { - internal func printClient() { - if self.options.generateClient { - self.println() - self.printServiceClientProtocol() - self.println() - self.printClientProtocolExtension() - self.println() - self.printClassBackedServiceClientImplementation() - self.println() - self.printStructBackedServiceClientImplementation() - self.println() - self.printAsyncServiceClientProtocol() - self.println() - self.printAsyncClientProtocolExtension() - self.println() - self.printAsyncClientProtocolSafeWrappersExtension() - self.println() - self.printAsyncServiceClientImplementation() - self.println() - // Both implementations share definitions for interceptors and metadata. - self.printServiceClientInterceptorFactoryProtocol() - self.println() - self.printClientMetadata() - } - - if self.options.generateTestClient { - self.println() - self.printTestClient() - } - } - - internal func printFunction( - name: String, - arguments: [String], - returnType: String?, - access: String? = nil, - sendable: Bool = false, - async: Bool = false, - throws: Bool = false, - genericWhereClause: String? = nil, - bodyBuilder: (() -> Void)? - ) { - // Add a space after access, if it exists. - let functionHead = (access.map { $0 + " " } ?? "") + (sendable ? "@Sendable " : "") - let `return` = returnType.map { " -> " + $0 } ?? "" - let genericWhere = genericWhereClause.map { " " + $0 } ?? "" - - let asyncThrows: String - switch (async, `throws`) { - case (true, true): - asyncThrows = " async throws" - case (true, false): - asyncThrows = " async" - case (false, true): - asyncThrows = " throws" - case (false, false): - asyncThrows = "" - } - - let hasBody = bodyBuilder != nil - - if arguments.isEmpty { - // Don't bother splitting across multiple lines if there are no arguments. - self.println( - "\(functionHead)func \(name)()\(asyncThrows)\(`return`)\(genericWhere)", - newline: !hasBody - ) - } else { - self.println("\(functionHead)func \(name)(") - self.withIndentation { - // Add a comma after each argument except the last. - arguments.forEach( - beforeLast: { - self.println($0 + ",") - }, - onLast: { - self.println($0) - } - ) - } - self.println(")\(asyncThrows)\(`return`)\(genericWhere)", newline: !hasBody) - } - - if let bodyBuilder = bodyBuilder { - self.println(" {") - self.withIndentation { - bodyBuilder() - } - self.println("}") - } - } - - private func printServiceClientProtocol() { - let comments = self.service.protoSourceComments() - if !comments.isEmpty { - // Source comments already have the leading '///' - self.println(comments, newline: false) - self.println("///") - } - self.println( - "/// Usage: instantiate `\(self.clientClassName)`, then call methods of this protocol to make API calls." - ) - self.println("\(self.access) protocol \(self.clientProtocolName): GRPCClient {") - self.withIndentation { - self.println("var serviceName: String { get }") - self.println("var interceptors: \(self.clientInterceptorProtocolName)? { get }") - - for method in service.methods { - self.println() - self.method = method - - self.printFunction( - name: self.methodFunctionName, - arguments: self.methodArgumentsWithoutDefaults, - returnType: self.methodReturnType, - bodyBuilder: nil - ) - } - } - println("}") - } - - private func printClientProtocolExtension() { - self.println("extension \(self.clientProtocolName) {") - - self.withIndentation { - // Service name. - self.println("\(self.access) var serviceName: String {") - self.withIndentation { - self.println("return \"\(self.servicePath)\"") - } - self.println("}") - - // Default method implementations. - self.printMethods() - } - - self.println("}") - } - - private func printServiceClientInterceptorFactoryProtocol() { - self.println("\(self.access) protocol \(self.clientInterceptorProtocolName): Sendable {") - self.withIndentation { - // Method specific interceptors. - for method in service.methods { - self.println() - self.method = method - self.println( - "/// - Returns: Interceptors to use when invoking '\(self.methodFunctionName)'." - ) - // Skip the access, we're defining a protocol. - self.printMethodInterceptorFactory(access: nil) - } - } - self.println("}") - } - - private func printMethodInterceptorFactory( - access: String?, - bodyBuilder: (() -> Void)? = nil - ) { - self.printFunction( - name: self.methodInterceptorFactoryName, - arguments: [], - returnType: "[ClientInterceptor<\(self.methodInputName), \(self.methodOutputName)>]", - access: access, - bodyBuilder: bodyBuilder - ) - } - - private func printClassBackedServiceClientImplementation() { - self.println("@available(*, deprecated)") - self.println("extension \(clientClassName): @unchecked Sendable {}") - 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() - 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 printMethods() { - for method in self.service.methods { - self.println() - - self.method = method - switch self.streamType { - case .unary: - self.printUnaryCall() - - case .serverStreaming: - self.printServerStreamingCall() - - case .clientStreaming: - self.printClientStreamingCall() - - case .bidirectionalStreaming: - self.printBidirectionalStreamingCall() - } - } - } - - private func printUnaryCall() { - self.println(self.method.documentation(streamingType: self.streamType), newline: false) - self.println("///") - self.printParameters() - self.printRequestParameter() - self.printCallOptionsParameter() - self.println("/// - Returns: A `UnaryCall` with futures for the metadata, status and response.") - self.printFunction( - name: self.methodFunctionName, - arguments: self.methodArguments, - returnType: self.methodReturnType, - access: self.access - ) { - self.println("return self.makeUnaryCall(") - self.withIndentation { - self.println("path: \(self.methodPathUsingClientMetadata),") - self.println("request: request,") - self.println("callOptions: callOptions ?? self.defaultCallOptions,") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []" - ) - } - self.println(")") - } - } - - private func printServerStreamingCall() { - self.println(self.method.documentation(streamingType: self.streamType), newline: false) - self.println("///") - self.printParameters() - self.printRequestParameter() - self.printCallOptionsParameter() - self.printHandlerParameter() - self.println("/// - Returns: A `ServerStreamingCall` with futures for the metadata and status.") - self.printFunction( - name: self.methodFunctionName, - arguments: self.methodArguments, - returnType: self.methodReturnType, - access: self.access - ) { - self.println("return self.makeServerStreamingCall(") - self.withIndentation { - self.println("path: \(self.methodPathUsingClientMetadata),") - self.println("request: request,") - self.println("callOptions: callOptions ?? self.defaultCallOptions,") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []," - ) - self.println("handler: handler") - } - self.println(")") - } - } - - private func printClientStreamingCall() { - self.println(self.method.documentation(streamingType: self.streamType), newline: false) - self.println("///") - self.printClientStreamingDetails() - self.println("///") - self.printParameters() - self.printCallOptionsParameter() - self - .println( - "/// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response." - ) - self.printFunction( - name: self.methodFunctionName, - arguments: self.methodArguments, - returnType: self.methodReturnType, - access: self.access - ) { - self.println("return self.makeClientStreamingCall(") - self.withIndentation { - self.println("path: \(self.methodPathUsingClientMetadata),") - self.println("callOptions: callOptions ?? self.defaultCallOptions,") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []" - ) - } - self.println(")") - } - } - - private func printBidirectionalStreamingCall() { - self.println(self.method.documentation(streamingType: self.streamType), newline: false) - self.println("///") - self.printClientStreamingDetails() - self.println("///") - self.printParameters() - self.printCallOptionsParameter() - self.printHandlerParameter() - self.println("/// - Returns: A `ClientStreamingCall` with futures for the metadata and status.") - self.printFunction( - name: self.methodFunctionName, - arguments: self.methodArguments, - returnType: self.methodReturnType, - access: self.access - ) { - self.println("return self.makeBidirectionalStreamingCall(") - self.withIndentation { - self.println("path: \(self.methodPathUsingClientMetadata),") - self.println("callOptions: callOptions ?? self.defaultCallOptions,") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []," - ) - self.println("handler: handler") - } - self.println(")") - } - } - - private func printClientStreamingDetails() { - println("/// Callers should use the `send` method on the returned object to send messages") - println( - "/// to the server. The caller should send an `.end` after the final message has been sent." - ) - } - - private func printParameters() { - println("/// - Parameters:") - } - - private func printRequestParameter() { - println("/// - request: Request to send to \(method.name).") - } - - private func printCallOptionsParameter() { - println("/// - callOptions: Call options.") - } - - private func printHandlerParameter() { - println("/// - handler: A closure called when each response is received from the server.") - } -} - -extension Generator { - private func printFakeResponseStreams() { - for method in self.service.methods { - self.println() - - self.method = method - switch self.streamType { - case .unary, .clientStreaming: - self.printUnaryResponse() - - case .serverStreaming, .bidirectionalStreaming: - self.printStreamingResponse() - } - } - } - - private func printUnaryResponse() { - self.printResponseStream(isUnary: true) - self.println() - self.printEnqueueUnaryResponse(isUnary: true) - self.println() - self.printHasResponseStreamEnqueued() - } - - private func printStreamingResponse() { - self.printResponseStream(isUnary: false) - self.println() - self.printEnqueueUnaryResponse(isUnary: false) - self.println() - self.printHasResponseStreamEnqueued() - } - - private func printEnqueueUnaryResponse(isUnary: Bool) { - let name: String - let responseArg: String - let responseArgAndType: String - if isUnary { - name = "enqueue\(self.method.name)Response" - responseArg = "response" - responseArgAndType = "_ \(responseArg): \(self.methodOutputName)" - } else { - name = "enqueue\(self.method.name)Responses" - responseArg = "responses" - responseArgAndType = "_ \(responseArg): [\(self.methodOutputName)]" - } - - self.printFunction( - name: name, - arguments: [ - responseArgAndType, - "_ requestHandler: @escaping (FakeRequestPart<\(self.methodInputName)>) -> () = { _ in }", - ], - returnType: nil, - access: self.access - ) { - self.println("let stream = self.make\(self.method.name)ResponseStream(requestHandler)") - if isUnary { - self.println("// This is the only operation on the stream; try! is fine.") - self.println("try! stream.sendMessage(\(responseArg))") - } else { - self.println("// These are the only operation on the stream; try! is fine.") - self.println("\(responseArg).forEach { try! stream.sendMessage($0) }") - self.println("try! stream.sendEnd()") - } - } - } - - private func printResponseStream(isUnary: Bool) { - let type = isUnary ? "FakeUnaryResponse" : "FakeStreamingResponse" - let factory = isUnary ? "makeFakeUnaryResponse" : "makeFakeStreamingResponse" - - self - .println( - "/// Make a \(isUnary ? "unary" : "streaming") response for the \(self.method.name) RPC. This must be called" - ) - self.println("/// before calling '\(self.methodFunctionName)'. See also '\(type)'.") - self.println("///") - self.println("/// - Parameter requestHandler: a handler for request parts sent by the RPC.") - self.printFunction( - name: "make\(self.method.name)ResponseStream", - arguments: [ - "_ requestHandler: @escaping (FakeRequestPart<\(self.methodInputName)>) -> () = { _ in }" - ], - returnType: "\(type)<\(self.methodInputName), \(self.methodOutputName)>", - access: self.access - ) { - self - .println( - "return self.fakeChannel.\(factory)(path: \(self.methodPathUsingClientMetadata), requestHandler: requestHandler)" - ) - } - } - - private func printHasResponseStreamEnqueued() { - self - .println("/// Returns true if there are response streams enqueued for '\(self.method.name)'") - self.println("\(self.access) var has\(self.method.name)ResponsesRemaining: Bool {") - self.withIndentation { - self.println( - "return self.fakeChannel.hasFakeResponseEnqueued(forPath: \(self.methodPathUsingClientMetadata))" - ) - } - self.println("}") - } - - private func printTestClient() { - self.println("@available(swift, deprecated: 5.6)") - self.println("extension \(self.testClientClassName): @unchecked Sendable {}") - 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("\(access) var defaultCallOptions: CallOptions") - self.println("\(access) var interceptors: \(clientInterceptorProtocolName)?") - self.println() - self.println("\(self.access) var channel: GRPCChannel {") - self.withIndentation { - self.println("return self.fakeChannel") - } - self.println("}") - self.println() - - self.println("\(self.access) init(") - self.withIndentation { - self.println("fakeChannel: FakeChannel = FakeChannel(),") - self.println("defaultCallOptions callOptions: CallOptions = CallOptions(),") - self.println("interceptors: \(clientInterceptorProtocolName)? = nil") - } - self.println(") {") - self.withIndentation { - self.println("self.fakeChannel = fakeChannel") - self.println("self.defaultCallOptions = callOptions") - self.println("self.interceptors = interceptors") - } - self.println("}") - - self.printFakeResponseStreams() - } - - self.println("}") // end class - } -} - -extension Generator { - private var streamType: StreamingType { - return streamingType(self.method) - } -} - -extension Generator { - private var methodArguments: [String] { - switch self.streamType { - case .unary: - return [ - "_ request: \(self.methodInputName)", - "callOptions: CallOptions? = nil", - ] - case .serverStreaming: - return [ - "_ request: \(self.methodInputName)", - "callOptions: CallOptions? = nil", - "handler: @escaping (\(methodOutputName)) -> Void", - ] - - case .clientStreaming: - return ["callOptions: CallOptions? = nil"] - - case .bidirectionalStreaming: - return [ - "callOptions: CallOptions? = nil", - "handler: @escaping (\(methodOutputName)) -> Void", - ] - } - } - - private var methodArgumentsWithoutDefaults: [String] { - return self.methodArguments.map { arg in - // Remove default arg from call options. - if arg == "callOptions: CallOptions? = nil" { - return "callOptions: CallOptions?" - } else { - return arg - } - } - } - - private var methodArgumentsWithoutCallOptions: [String] { - return self.methodArguments.filter { - !$0.hasPrefix("callOptions: ") - } - } - - private var methodReturnType: String { - switch self.streamType { - case .unary: - return "UnaryCall<\(self.methodInputName), \(self.methodOutputName)>" - - case .serverStreaming: - return "ServerStreamingCall<\(self.methodInputName), \(self.methodOutputName)>" - - case .clientStreaming: - return "ClientStreamingCall<\(self.methodInputName), \(self.methodOutputName)>" - - case .bidirectionalStreaming: - return "BidirectionalStreamingCall<\(self.methodInputName), \(self.methodOutputName)>" - } - } -} - -extension StreamingType { - fileprivate var name: String { - switch self { - case .unary: - return "Unary" - case .clientStreaming: - return "Client streaming" - case .serverStreaming: - return "Server streaming" - case .bidirectionalStreaming: - return "Bidirectional streaming" - } - } -} - -extension MethodDescriptor { - var documentation: String? { - let comments = self.protoSourceComments(commentPrefix: "") - return comments.isEmpty ? nil : comments - } - - fileprivate func documentation(streamingType: StreamingType) -> String { - let sourceComments = self.protoSourceComments() - - if sourceComments.isEmpty { - return "/// \(streamingType.name) call to \(self.name)\n" // comments end with "\n" already. - } else { - return sourceComments // already prefixed with "///" - } - } -} - -extension Array { - /// Like `forEach` except that the `body` closure operates on all elements except for the last, - /// and the `last` closure only operates on the last element. - fileprivate func forEach(beforeLast body: (Element) -> Void, onLast last: (Element) -> Void) { - for element in self.dropLast() { - body(element) - } - if let lastElement = self.last { - last(lastElement) - } - } -} diff --git a/Sources/protoc-gen-grpc-swift/Generator-Metadata.swift b/Sources/protoc-gen-grpc-swift/Generator-Metadata.swift deleted file mode 100644 index 2bd48f0b2..000000000 --- a/Sources/protoc-gen-grpc-swift/Generator-Metadata.swift +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 SwiftProtobuf -import SwiftProtobufPluginLibrary - -extension Generator { - internal func printServerMetadata() { - self.printMetadata(server: true) - } - - internal func printClientMetadata() { - self.printMetadata(server: false) - } - - private func printMetadata(server: Bool) { - let enumName = server ? self.serviceServerMetadata : self.serviceClientMetadata - - self.withIndentation("\(self.access) enum \(enumName)", braces: .curly) { - self.println("\(self.access) static let serviceDescriptor = GRPCServiceDescriptor(") - self.withIndentation { - self.println("name: \(quoted(self.service.name)),") - self.println("fullName: \(quoted(self.servicePath)),") - self.println("methods: [") - for method in self.service.methods { - self.method = method - self.withIndentation { - self.println("\(enumName).Methods.\(self.methodFunctionName),") - } - } - self.println("]") - } - self.println(")") - self.println() - - self.withIndentation("\(self.access) enum Methods", braces: .curly) { - for (offset, method) in self.service.methods.enumerated() { - self.method = method - self.println( - "\(self.access) static let \(self.methodFunctionName) = GRPCMethodDescriptor(" - ) - self.withIndentation { - self.println("name: \(quoted(self.method.name)),") - self.println("path: \(quoted(self.methodPath)),") - self.println("type: \(streamingType(self.method).asGRPCCallTypeCase)") - } - self.println(")") - - if (offset + 1) < self.service.methods.count { - self.println() - } - } - } - } - } -} - -extension Generator { - internal var serviceServerMetadata: String { - return nameForPackageService(self.file, self.service) + "ServerMetadata" - } - - internal var serviceClientMetadata: String { - return nameForPackageService(self.file, self.service) + "ClientMetadata" - } - - internal var methodPathUsingClientMetadata: String { - return "\(self.serviceClientMetadata).Methods.\(self.methodFunctionName).path" - } -} diff --git a/Sources/protoc-gen-grpc-swift/Generator-Names.swift b/Sources/protoc-gen-grpc-swift/Generator-Names.swift deleted file mode 100644 index 115693cd4..000000000 --- a/Sources/protoc-gen-grpc-swift/Generator-Names.swift +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2018, 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 Foundation -import SwiftProtobuf -import SwiftProtobufPluginLibrary - -internal func nameForPackageService( - _ file: FileDescriptor, - _ service: ServiceDescriptor -) -> String { - if !file.package.isEmpty { - return SwiftProtobufNamer().typePrefix(forFile: file) + service.name - } else { - return service.name - } -} - -internal func nameForPackageServiceMethod( - _ file: FileDescriptor, - _ service: ServiceDescriptor, - _ method: MethodDescriptor -) -> String { - return nameForPackageService(file, service) + method.name -} - -private let swiftKeywordsUsedInDeclarations: Set = [ - "associatedtype", "class", "deinit", "enum", "extension", - "fileprivate", "func", "import", "init", "inout", "internal", - "let", "open", "operator", "private", "protocol", "public", - "static", "struct", "subscript", "typealias", "var", -] - -private let swiftKeywordsUsedInStatements: Set = [ - "break", "case", - "continue", "default", "defer", "do", "else", "fallthrough", - "for", "guard", "if", "in", "repeat", "return", "switch", "where", - "while", -] - -private let swiftKeywordsUsedInExpressionsAndTypes: Set = [ - "as", - "Any", "catch", "false", "is", "nil", "rethrows", "super", "self", - "Self", "throw", "throws", "true", "try", -] - -private let quotableFieldNames: Set = { () -> Set in - var names: Set = [] - - names = names.union(swiftKeywordsUsedInDeclarations) - names = names.union(swiftKeywordsUsedInStatements) - names = names.union(swiftKeywordsUsedInExpressionsAndTypes) - return names -}() - -extension Generator { - internal var access: String { - return options.visibility.sourceSnippet - } - - internal var serviceClassName: String { - return nameForPackageService(file, service) + "Service" - } - - internal var providerName: String { - return nameForPackageService(file, service) + "Provider" - } - - internal var asyncProviderName: String { - return nameForPackageService(file, service) + "AsyncProvider" - } - - internal var clientClassName: String { - return nameForPackageService(file, service) + "Client" - } - - internal var clientStructName: String { - return nameForPackageService(file, service) + "NIOClient" - } - - internal var asyncClientStructName: String { - return nameForPackageService(file, service) + "AsyncClient" - } - - internal var testClientClassName: String { - return nameForPackageService(self.file, self.service) + "TestClient" - } - - internal var clientProtocolName: String { - return nameForPackageService(file, service) + "ClientProtocol" - } - - internal var asyncClientProtocolName: String { - return nameForPackageService(file, service) + "AsyncClientProtocol" - } - - internal var clientInterceptorProtocolName: String { - return nameForPackageService(file, service) + "ClientInterceptorFactoryProtocol" - } - - internal var serverInterceptorProtocolName: String { - return nameForPackageService(file, service) + "ServerInterceptorFactoryProtocol" - } - - internal var callName: String { - return nameForPackageServiceMethod(file, service, method) + "Call" - } - - internal var methodFunctionName: String { - var name = method.name - if !self.options.keepMethodCasing { - name = name.prefix(1).lowercased() + name.dropFirst() - } - - return self.sanitize(fieldName: name) - } - - internal var methodMakeFunctionCallName: String { - let name: String - - if self.options.keepMethodCasing { - name = self.method.name - } else { - name = NamingUtils.toUpperCamelCase(self.method.name) - } - - let fnName = "make\(name)Call" - return self.sanitize(fieldName: fnName) - } - - internal func sanitize(fieldName string: String) -> String { - if quotableFieldNames.contains(string) { - return "`\(string)`" - } - return string - } - - internal var methodInputName: String { - return protobufNamer.fullName(message: method.inputType) - } - - internal var methodOutputName: String { - return protobufNamer.fullName(message: method.outputType) - } - - internal var methodInterceptorFactoryName: String { - return "make\(self.method.name)Interceptors" - } - - internal var servicePath: String { - if !file.package.isEmpty { - return file.package + "." + service.name - } else { - return service.name - } - } - - internal var methodPath: String { - return "/" + self.fullMethodName - } - - internal var fullMethodName: String { - return self.servicePath + "/" + self.method.name - } -} - -internal func quoted(_ str: String) -> String { - return "\"" + str + "\"" -} diff --git a/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift b/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift deleted file mode 100644 index 71e921eb4..000000000 --- a/Sources/protoc-gen-grpc-swift/Generator-Server+AsyncAwait.swift +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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 SwiftProtobuf -import SwiftProtobufPluginLibrary - -// MARK: - Protocol - -extension Generator { - internal func printServerProtocolAsyncAwait() { - let sourceComments = self.service.protoSourceComments() - if !sourceComments.isEmpty { - // Source comments already have the leading '///' - self.println(sourceComments, newline: false) - self.println("///") - } - self.println("/// To implement a server, implement an object which conforms to this protocol.") - self.printAvailabilityForAsyncAwait() - self.withIndentation( - "\(self.access) protocol \(self.asyncProviderName): CallHandlerProvider, Sendable", - braces: .curly - ) { - self.println("static var serviceDescriptor: GRPCServiceDescriptor { get }") - self.println("var interceptors: \(self.serverInterceptorProtocolName)? { get }") - - for method in service.methods { - self.method = method - self.println() - self.printRPCProtocolRequirement() - } - } - } - - private func printRPCProtocolRequirement() { - // Print any comments; skip the newline as source comments include them already. - self.println(self.method.protoSourceComments(), newline: false) - - let arguments: [String] - let returnType: String? - - switch streamingType(self.method) { - case .unary: - arguments = [ - "request: \(self.methodInputName)", - "context: \(Types.serverContext)", - ] - returnType = self.methodOutputName - - case .clientStreaming: - arguments = [ - "requestStream: \(Types.requestStream(of: self.methodInputName))", - "context: \(Types.serverContext)", - ] - returnType = self.methodOutputName - - case .serverStreaming: - arguments = [ - "request: \(self.methodInputName)", - "responseStream: \(Types.responseStreamWriter(of: self.methodOutputName))", - "context: \(Types.serverContext)", - ] - returnType = nil - - case .bidirectionalStreaming: - arguments = [ - "requestStream: \(Types.requestStream(of: self.methodInputName))", - "responseStream: \(Types.responseStreamWriter(of: self.methodOutputName))", - "context: \(Types.serverContext)", - ] - returnType = nil - } - - self.printFunction( - name: self.methodFunctionName, - arguments: arguments, - returnType: returnType, - sendable: false, - async: true, - throws: true, - bodyBuilder: nil - ) - } -} - -// MARK: - Protocol Extension; RPC handling - -extension Generator { - internal func printServerProtocolExtensionAsyncAwait() { - // Default extension to provide the service name and routing for methods. - self.printAvailabilityForAsyncAwait() - self.withIndentation("extension \(self.asyncProviderName)", braces: .curly) { - self.withIndentation( - "\(self.access) static var serviceDescriptor: GRPCServiceDescriptor", - braces: .curly - ) { - self.println("return \(self.serviceServerMetadata).serviceDescriptor") - } - - self.println() - - // This fulfils a requirement from 'CallHandlerProvider' - self.withIndentation("\(self.access) var serviceName: Substring", braces: .curly) { - /// This API returns a Substring (hence the '[...]') - self.println("return \(self.serviceServerMetadata).serviceDescriptor.fullName[...]") - } - - self.println() - - // Default nil interceptor factory. - self.withIndentation( - "\(self.access) var interceptors: \(self.serverInterceptorProtocolName)?", - braces: .curly - ) { - self.println("return nil") - } - - self.println() - - self.printFunction( - name: "handle", - arguments: [ - "method name: Substring", - "context: CallHandlerContext", - ], - returnType: "GRPCServerHandlerProtocol?", - access: self.access - ) { - self.println("switch name {") - for method in self.service.methods { - self.method = method - - let requestType = self.methodInputName - let responseType = self.methodOutputName - let interceptorFactory = self.methodInterceptorFactoryName - let functionName = self.methodFunctionName - - self.withIndentation("case \"\(self.method.name)\":", braces: .none) { - self.withIndentation("return \(Types.serverHandler)", braces: .round) { - self.println("context: context,") - self.println("requestDeserializer: \(Types.deserializer(for: requestType))(),") - self.println("responseSerializer: \(Types.serializer(for: responseType))(),") - self.println("interceptors: self.interceptors?.\(interceptorFactory)() ?? [],") - let prefix = "wrapping: { try await self.\(functionName)" - switch streamingType(self.method) { - case .unary: - self.println("\(prefix)(request: $0, context: $1) }") - - case .clientStreaming: - self.println("\(prefix)(requestStream: $0, context: $1) }") - - case .serverStreaming: - self.println("\(prefix)(request: $0, responseStream: $1, context: $2) }") - - case .bidirectionalStreaming: - self.println("\(prefix)(requestStream: $0, responseStream: $1, context: $2) }") - } - } - } - } - - // Default case. - self.println("default:") - self.withIndentation { - self.println("return nil") - } - - self.println("}") // switch - } - } - } -} diff --git a/Sources/protoc-gen-grpc-swift/Generator-Server.swift b/Sources/protoc-gen-grpc-swift/Generator-Server.swift deleted file mode 100644 index a99249f9e..000000000 --- a/Sources/protoc-gen-grpc-swift/Generator-Server.swift +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2018, 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 Foundation -import SwiftProtobuf -import SwiftProtobufPluginLibrary - -extension Generator { - internal func printServer() { - if self.options.generateServer { - self.printServerProtocol() - self.println() - self.printServerProtocolExtension() - self.println() - self.printServerProtocolAsyncAwait() - self.println() - self.printServerProtocolExtensionAsyncAwait() - self.println() - // Both implementations share definitions for interceptors and metadata. - self.printServerInterceptorFactoryProtocol() - self.println() - self.printServerMetadata() - } - } - - private func printServerProtocol() { - let comments = self.service.protoSourceComments() - if !comments.isEmpty { - // Source comments already have the leading '///' - self.println(comments, newline: false) - self.println("///") - } - println("/// To build a server, implement a class that conforms to this protocol.") - println("\(access) protocol \(providerName): CallHandlerProvider {") - self.withIndentation { - println("var interceptors: \(self.serverInterceptorProtocolName)? { get }") - for method in service.methods { - self.method = method - self.println() - - switch streamingType(method) { - case .unary: - println(self.method.protoSourceComments(), newline: false) - println( - "func \(methodFunctionName)(request: \(methodInputName), context: StatusOnlyCallContext) -> EventLoopFuture<\(methodOutputName)>" - ) - case .serverStreaming: - println(self.method.protoSourceComments(), newline: false) - println( - "func \(methodFunctionName)(request: \(methodInputName), context: StreamingResponseCallContext<\(methodOutputName)>) -> EventLoopFuture" - ) - case .clientStreaming: - println(self.method.protoSourceComments(), newline: false) - println( - "func \(methodFunctionName)(context: UnaryResponseCallContext<\(methodOutputName)>) -> EventLoopFuture<(StreamEvent<\(methodInputName)>) -> Void>" - ) - case .bidirectionalStreaming: - println(self.method.protoSourceComments(), newline: false) - println( - "func \(methodFunctionName)(context: StreamingResponseCallContext<\(methodOutputName)>) -> EventLoopFuture<(StreamEvent<\(methodInputName)>) -> Void>" - ) - } - } - } - println("}") - } - - private func printServerProtocolExtension() { - self.println("extension \(self.providerName) {") - self.withIndentation { - self.withIndentation("\(self.access) var serviceName: Substring", braces: .curly) { - /// This API returns a Substring (hence the '[...]') - self.println("return \(self.serviceServerMetadata).serviceDescriptor.fullName[...]") - } - self.println() - self.println( - "/// Determines, calls and returns the appropriate request handler, depending on the request's method." - ) - self.println("/// Returns nil for methods not handled by this service.") - self.printFunction( - name: "handle", - arguments: [ - "method name: Substring", - "context: CallHandlerContext", - ], - returnType: "GRPCServerHandlerProtocol?", - access: self.access - ) { - self.println("switch name {") - for method in self.service.methods { - self.method = method - self.println("case \"\(method.name)\":") - self.withIndentation { - // Get the factory name. - let callHandlerType: String - switch streamingType(method) { - case .unary: - callHandlerType = "UnaryServerHandler" - case .serverStreaming: - callHandlerType = "ServerStreamingServerHandler" - case .clientStreaming: - callHandlerType = "ClientStreamingServerHandler" - case .bidirectionalStreaming: - callHandlerType = "BidirectionalStreamingServerHandler" - } - - self.println("return \(callHandlerType)(") - self.withIndentation { - self.println("context: context,") - self.println("requestDeserializer: ProtobufDeserializer<\(self.methodInputName)>(),") - self.println("responseSerializer: ProtobufSerializer<\(self.methodOutputName)>(),") - self.println( - "interceptors: self.interceptors?.\(self.methodInterceptorFactoryName)() ?? []," - ) - switch streamingType(method) { - case .unary, .serverStreaming: - self.println("userFunction: self.\(self.methodFunctionName)(request:context:)") - case .clientStreaming, .bidirectionalStreaming: - self.println("observerFactory: self.\(self.methodFunctionName)(context:)") - } - } - self.println(")") - } - self.println() - } - - // Default case. - self.println("default:") - self.withIndentation { - self.println("return nil") - } - self.println("}") - } - } - self.println("}") - } - - private func printServerInterceptorFactoryProtocol() { - self.println("\(self.access) protocol \(self.serverInterceptorProtocolName): Sendable {") - self.withIndentation { - // Method specific interceptors. - for method in service.methods { - self.println() - self.method = method - self.println( - "/// - Returns: Interceptors to use when handling '\(self.methodFunctionName)'." - ) - self.println("/// Defaults to calling `self.makeInterceptors()`.") - // Skip the access, we're defining a protocol. - self.printMethodInterceptorFactory(access: nil) - } - } - self.println("}") - } - - private func printMethodInterceptorFactory( - access: String?, - bodyBuilder: (() -> Void)? = nil - ) { - self.printFunction( - name: self.methodInterceptorFactoryName, - arguments: [], - returnType: "[ServerInterceptor<\(self.methodInputName), \(self.methodOutputName)>]", - access: access, - bodyBuilder: bodyBuilder - ) - } - - func printServerInterceptorFactoryProtocolExtension() { - self.println("extension \(self.serverInterceptorProtocolName) {") - self.withIndentation { - // Default interceptor factory. - self.printFunction( - name: "makeInterceptors", - arguments: [], - returnType: "[ServerInterceptor]", - access: self.access - ) { - self.println("return []") - } - - for method in self.service.methods { - self.println() - - self.method = method - self.printMethodInterceptorFactory(access: self.access) { - self.println("return self.makeInterceptors()") - } - } - } - self.println("}") - } -} diff --git a/Sources/protoc-gen-grpc-swift/Generator.swift b/Sources/protoc-gen-grpc-swift/Generator.swift deleted file mode 100644 index 7ed0c4aad..000000000 --- a/Sources/protoc-gen-grpc-swift/Generator.swift +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2018, 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 SwiftProtobufPluginLibrary - -class Generator { - internal var options: GeneratorOptions - private var printer: CodePrinter - - internal var file: FileDescriptor - internal var service: ServiceDescriptor! // context during generation - internal var method: MethodDescriptor! // context during generation - - internal let protobufNamer: SwiftProtobufNamer - - init(_ file: FileDescriptor, options: GeneratorOptions) { - self.file = file - self.options = options - self.printer = CodePrinter() - self.protobufNamer = SwiftProtobufNamer( - currentFile: file, - protoFileToModuleMappings: options.protoToModuleMappings - ) - self.printMain() - } - - public var code: String { - return self.printer.content - } - - internal func println(_ text: String = "", newline: Bool = true) { - self.printer.print(text) - if newline { - self.printer.print("\n") - } - } - - internal func indent() { - self.printer.indent() - } - - internal func outdent() { - self.printer.outdent() - } - - internal func withIndentation(body: () -> Void) { - self.indent() - body() - self.outdent() - } - - internal enum Braces { - case none - case curly - case round - - var open: String { - switch self { - case .none: - return "" - case .curly: - return "{" - case .round: - return "(" - } - } - - var close: String { - switch self { - case .none: - return "" - case .curly: - return "}" - case .round: - return ")" - } - } - } - - internal func withIndentation( - _ header: String, - braces: Braces, - _ body: () -> Void - ) { - let spaceBeforeOpeningBrace: Bool - switch braces { - case .curly: - spaceBeforeOpeningBrace = true - case .round, .none: - spaceBeforeOpeningBrace = false - } - - self.println(header + "\(spaceBeforeOpeningBrace ? " " : "")" + "\(braces.open)") - self.withIndentation(body: body) - self.println(braces.close) - } - - private func printMain() { - self.printer.print( - """ - // - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the protocol buffer compiler. - // Source: \(self.file.name) - //\n - """ - ) - - let moduleNames = [ - self.options.gRPCModuleName, - "NIO", - "NIOConcurrencyHelpers", - self.options.swiftProtobufModuleName, - ] - - for moduleName in (moduleNames + self.options.extraModuleImports).sorted() { - self.println("import \(moduleName)") - } - // Add imports for required modules - let moduleMappings = self.options.protoToModuleMappings - for importedProtoModuleName in moduleMappings.neededModules(forFile: self.file) ?? [] { - self.println("import \(importedProtoModuleName)") - } - self.println() - - // We defer the check for printing clients to `printClient()` since this could be the 'real' - // client or the test client. - for service in self.file.services { - self.service = service - self.printClient() - } - self.println() - - if self.options.generateServer { - for service in self.file.services { - self.service = service - printServer() - } - } - } - - func printAvailabilityForAsyncAwait() { - self.println("@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)") - } -} diff --git a/Sources/protoc-gen-grpc-swift/Options.swift b/Sources/protoc-gen-grpc-swift/Options.swift deleted file mode 100644 index 2b0f96d46..000000000 --- a/Sources/protoc-gen-grpc-swift/Options.swift +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2017, 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 Foundation -import SwiftProtobufPluginLibrary - -enum GenerationError: Error { - /// Raised when parsing the parameter string and found an unknown key - case unknownParameter(name: String) - /// Raised when a parameter was giving an invalid value - case invalidParameterValue(name: String, value: String) - /// Raised to wrap another error but provide a context message. - case wrappedError(message: String, error: Error) - - var localizedDescription: String { - switch self { - case let .unknownParameter(name): - return "Unknown generation parameter '\(name)'" - case let .invalidParameterValue(name, value): - return "Unknown value for generation parameter '\(name)': '\(value)'" - case let .wrappedError(message, error): - return "\(message): \(error.localizedDescription)" - } - } -} - -enum FileNaming: String { - case fullPath = "FullPath" - case pathToUnderscores = "PathToUnderscores" - case dropPath = "DropPath" -} - -struct GeneratorOptions { - enum Visibility: String { - case `internal` = "Internal" - case `public` = "Public" - case `package` = "Package" - - var sourceSnippet: String { - switch self { - case .internal: - return "internal" - case .public: - return "public" - case .package: - return "package" - } - } - } - - private(set) var visibility = Visibility.internal - - private(set) var generateServer = true - - private(set) var generateClient = true - private(set) var generateTestClient = false - - private(set) var keepMethodCasing = false - private(set) var protoToModuleMappings = ProtoFileToModuleMappings() - private(set) var fileNaming = FileNaming.fullPath - private(set) var extraModuleImports: [String] = [] - private(set) var gRPCModuleName = "GRPC" - private(set) var swiftProtobufModuleName = "SwiftProtobuf" - private(set) var generateReflectionData = false - #if compiler(>=6.0) - private(set) var v2 = false - #endif - private(set) var useAccessLevelOnImports = false - - init(parameter: any CodeGeneratorParameter) throws { - try self.init(pairs: parameter.parsedPairs) - } - - init(pairs: [(key: String, value: String)]) throws { - for pair in pairs { - switch pair.key { - case "Visibility": - if let value = Visibility(rawValue: pair.value) { - self.visibility = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "Server": - if let value = Bool(pair.value) { - self.generateServer = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "Client": - if let value = Bool(pair.value) { - self.generateClient = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "TestClient": - if let value = Bool(pair.value) { - self.generateTestClient = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "KeepMethodCasing": - if let value = Bool(pair.value) { - self.keepMethodCasing = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "ProtoPathModuleMappings": - if !pair.value.isEmpty { - do { - self.protoToModuleMappings = try ProtoFileToModuleMappings(path: pair.value) - } catch let e { - throw GenerationError.wrappedError( - message: "Parameter 'ProtoPathModuleMappings=\(pair.value)'", - error: e - ) - } - } - - case "FileNaming": - if let value = FileNaming(rawValue: pair.value) { - self.fileNaming = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "ExtraModuleImports": - if !pair.value.isEmpty { - self.extraModuleImports.append(pair.value) - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "GRPCModuleName": - if !pair.value.isEmpty { - self.gRPCModuleName = pair.value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "SwiftProtobufModuleName": - if !pair.value.isEmpty { - self.swiftProtobufModuleName = pair.value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - case "ReflectionData": - if let value = Bool(pair.value) { - self.generateReflectionData = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - #if compiler(>=6.0) - case "_V2": - if let value = Bool(pair.value) { - self.v2 = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - #endif - - case "UseAccessLevelOnImports": - if let value = Bool(pair.value) { - self.useAccessLevelOnImports = value - } else { - throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) - } - - default: - throw GenerationError.unknownParameter(name: pair.key) - } - } - } - - static func parseParameter(string: String?) -> [(key: String, value: String)] { - guard let string = string, !string.isEmpty else { - return [] - } - let parts = string.components(separatedBy: ",") - - // Partitions the string into the section before the = and after the = - let result = parts.map { string -> (key: String, value: String) in - - // Finds the equal sign and exits early if none - guard let index = string.range(of: "=")?.lowerBound else { - return (string, "") - } - - // Creates key/value pair and trims whitespace - let key = string[.. StreamingType { - if method.clientStreaming { - if method.serverStreaming { - return .bidirectionalStreaming - } else { - return .clientStreaming - } - } else { - if method.serverStreaming { - return .serverStreaming - } else { - return .unary - } - } -} diff --git a/Sources/protoc-gen-grpc-swift/Types.swift b/Sources/protoc-gen-grpc-swift/Types.swift deleted file mode 100644 index 650c0fadf..000000000 --- a/Sources/protoc-gen-grpc-swift/Types.swift +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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. - */ - -enum Types { - static let serverContext = "GRPCAsyncServerCallContext" - static let serverHandler = "GRPCAsyncServerHandler" - - static let clientCallOptions = "CallOptions" - - private static let unaryCall = "AsyncUnaryCall" - private static let clientStreamingCall = "AsyncClientStreamingCall" - private static let serverStreamingCall = "AsyncServerStreamingCall" - private static let bidirectionalStreamingCall = "AsyncBidirectionalStreamingCall" - - static func requestStream(of type: String) -> String { - return "GRPCAsyncRequestStream<\(type)>" - } - - static func responseStream(of type: String) -> String { - return "GRPCAsyncResponseStream<\(type)>" - } - - static func responseStreamWriter(of type: String) -> String { - return "GRPCAsyncResponseStreamWriter<\(type)>" - } - - static func serializer(for type: String) -> String { - return "ProtobufSerializer<\(type)>" - } - - static func deserializer(for type: String) -> String { - return "ProtobufDeserializer<\(type)>" - } - - static func call(for streamingType: StreamingType, withGRPCPrefix: Bool = true) -> String { - let typeName: String - - switch streamingType { - case .unary: - typeName = Types.unaryCall - case .clientStreaming: - typeName = Types.clientStreamingCall - case .serverStreaming: - typeName = Types.serverStreamingCall - case .bidirectionalStreaming: - typeName = Types.bidirectionalStreamingCall - } - - if withGRPCPrefix { - return "GRPC" + typeName - } else { - return typeName - } - } -} diff --git a/Sources/protoc-gen-grpc-swift/Version.swift b/Sources/protoc-gen-grpc-swift/Version.swift deleted file mode 120000 index aa26b19d3..000000000 --- a/Sources/protoc-gen-grpc-swift/Version.swift +++ /dev/null @@ -1 +0,0 @@ -../GRPC/Version.swift \ No newline at end of file diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift deleted file mode 100644 index fdf14aa2d..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerStateMachineTests.swift +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import XCTest - -@testable import GRPCHTTP2Core - -final class ClientConnectionHandlerStateMachineTests: XCTestCase { - private func makeStateMachine( - keepaliveWithoutCalls: Bool = false - ) -> ClientConnectionHandler.StateMachine { - return ClientConnectionHandler.StateMachine(allowKeepaliveWithoutCalls: keepaliveWithoutCalls) - } - - func testCloseSomeStreamsWhenActive() { - var state = self.makeStateMachine() - state.streamOpened(1) - state.streamOpened(2) - XCTAssertEqual(state.streamClosed(2), .none) - XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepalive: true)) - } - - func testCloseSomeStreamsWhenClosing() { - var state = self.makeStateMachine() - state.streamOpened(1) - state.streamOpened(2) - XCTAssertTrue(state.beginClosing()) - XCTAssertEqual(state.streamClosed(2), .none) - XCTAssertEqual(state.streamClosed(1), .close) - } - - func testCloseWhenAlreadyClosingGracefully() { - var state = self.makeStateMachine() - state.streamOpened(1) - XCTAssertEqual(state.beginGracefulShutdown(promise: nil), .sendGoAway(false)) - XCTAssertTrue(state.beginClosing()) - } - - func testOpenAndCloseStreamWhenClosed() { - var state = self.makeStateMachine() - _ = state.closed() - state.streamOpened(1) - XCTAssertEqual(state.streamClosed(1), .none) - } - - func testSendKeepalivePing() { - var state = self.makeStateMachine(keepaliveWithoutCalls: false) - // No streams open so ping isn't allowed. - XCTAssertFalse(state.sendKeepalivePing()) - - // Stream open, ping allowed. - state.streamOpened(1) - XCTAssertTrue(state.sendKeepalivePing()) - - // No stream, no ping. - XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepalive: true)) - XCTAssertFalse(state.sendKeepalivePing()) - } - - func testSendKeepalivePingWhenAllowedWithoutCalls() { - var state = self.makeStateMachine(keepaliveWithoutCalls: true) - // Keep alive is allowed when no streams are open, so pings are allowed. - XCTAssertTrue(state.sendKeepalivePing()) - - state.streamOpened(1) - XCTAssertTrue(state.sendKeepalivePing()) - - XCTAssertEqual(state.streamClosed(1), .startIdleTimer(cancelKeepalive: false)) - XCTAssertTrue(state.sendKeepalivePing()) - } - - func testSendKeepalivePingWhenClosing() { - var state = self.makeStateMachine(keepaliveWithoutCalls: false) - state.streamOpened(1) - XCTAssertTrue(state.beginClosing()) - - // Stream is opened and state is closing, ping is allowed. - XCTAssertTrue(state.sendKeepalivePing()) - } - - func testSendKeepalivePingWhenClosed() { - var state = self.makeStateMachine(keepaliveWithoutCalls: true) - _ = state.closed() - XCTAssertFalse(state.sendKeepalivePing()) - } - - func testBeginGracefulShutdownWhenStreamsAreOpen() { - var state = self.makeStateMachine() - state.streamOpened(1) - // Close is false as streams are still open. - XCTAssertEqual(state.beginGracefulShutdown(promise: nil), .sendGoAway(false)) - } - - func testBeginGracefulShutdownWhenNoStreamsAreOpen() { - var state = self.makeStateMachine() - // Close immediately, not streams are open. - XCTAssertEqual(state.beginGracefulShutdown(promise: nil), .sendGoAway(true)) - } - -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift deleted file mode 100644 index 87ac5538c..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ClientConnectionHandlerTests.swift +++ /dev/null @@ -1,405 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOCore -import NIOEmbedded -import NIOHTTP2 -import XCTest - -final class ClientConnectionHandlerTests: XCTestCase { - func testMaxIdleTime() throws { - let connection = try Connection(maxIdleTime: .minutes(5)) - try connection.activate() - - // Write the initial settings to ready the connection. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - // Idle with no streams open we should: - // - read out a closing event, - // - write a GOAWAY frame, - // - close. - connection.loop.advanceTime(by: .minutes(5)) - - XCTAssertEqual(try connection.readEvent(), .closing(.idle)) - - let frame = try XCTUnwrap(try connection.readFrame()) - XCTAssertEqual(frame.streamID, .rootStream) - XCTAssertGoAway(frame.payload) { lastStreamID, error, data in - XCTAssertEqual(lastStreamID, .rootStream) - XCTAssertEqual(error, .noError) - XCTAssertEqual(data, ByteBuffer(string: "idle")) - } - - try connection.waitUntilClosed() - } - - func testMaxIdleTimeWhenOpenStreams() throws { - let connection = try Connection(maxIdleTime: .minutes(5)) - try connection.activate() - - // Open a stream, the idle timer should be cancelled. - connection.streamOpened(1) - - // Advance by the idle time, nothing should happen. - connection.loop.advanceTime(by: .minutes(5)) - XCTAssertNil(try connection.readEvent()) - XCTAssertNil(try connection.readFrame()) - - // Close the stream, the idle timer should begin again. - connection.streamClosed(1) - connection.loop.advanceTime(by: .minutes(5)) - let frame = try XCTUnwrap(try connection.readFrame()) - XCTAssertGoAway(frame.payload) { lastStreamID, error, data in - XCTAssertEqual(lastStreamID, .rootStream) - XCTAssertEqual(error, .noError) - XCTAssertEqual(data, ByteBuffer(string: "idle")) - } - - try connection.waitUntilClosed() - } - - func testKeepaliveWithOpenStreams() throws { - let connection = try Connection(keepaliveTime: .minutes(1), keepaliveTimeout: .seconds(10)) - try connection.activate() - - // Write the initial settings to ready the connection. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - // Open a stream so keep-alive starts. - connection.streamOpened(1) - - for _ in 0 ..< 10 { - // Advance time, a PING should be sent, ACK it. - connection.loop.advanceTime(by: .minutes(1)) - let frame1 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame1.streamID, .rootStream) - try XCTAssertPing(frame1.payload) { data, ack in - XCTAssertFalse(ack) - try connection.ping(data: data, ack: true) - } - - XCTAssertNil(try connection.readFrame()) - } - - // Close the stream, keep-alive pings should stop. - connection.streamClosed(1) - connection.loop.advanceTime(by: .minutes(1)) - XCTAssertNil(try connection.readFrame()) - } - - func testKeepaliveWithNoOpenStreams() throws { - let connection = try Connection(keepaliveTime: .minutes(1), allowKeepaliveWithoutCalls: true) - try connection.activate() - - // Write the initial settings to ready the connection. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - for _ in 0 ..< 10 { - // Advance time, a PING should be sent, ACK it. - connection.loop.advanceTime(by: .minutes(1)) - let frame1 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame1.streamID, .rootStream) - try XCTAssertPing(frame1.payload) { data, ack in - XCTAssertFalse(ack) - try connection.ping(data: data, ack: true) - } - - XCTAssertNil(try connection.readFrame()) - } - } - - func testKeepaliveWithOpenStreamsTimingOut() throws { - let connection = try Connection(keepaliveTime: .minutes(1), keepaliveTimeout: .seconds(10)) - try connection.activate() - - // Write the initial settings to ready the connection. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - // Open a stream so keep-alive starts. - connection.streamOpened(1) - - // Advance time, a PING should be sent, don't ACK it. - connection.loop.advanceTime(by: .minutes(1)) - let frame1 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame1.streamID, .rootStream) - XCTAssertPing(frame1.payload) { _, ack in - XCTAssertFalse(ack) - } - - // Advance time by the keep alive timeout. We should: - // - read a connection event - // - read out a GOAWAY frame - // - be closed - connection.loop.advanceTime(by: .seconds(10)) - - XCTAssertEqual(try connection.readEvent(), .closing(.keepaliveExpired)) - - let frame2 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame2.streamID, .rootStream) - XCTAssertGoAway(frame2.payload) { lastStreamID, error, data in - XCTAssertEqual(lastStreamID, .rootStream) - XCTAssertEqual(error, .noError) - XCTAssertEqual(data, ByteBuffer(string: "keepalive_expired")) - } - - // Doesn't wait for streams to close: the connection is bad. - try connection.waitUntilClosed() - } - - func testPingsAreIgnored() throws { - let connection = try Connection() - try connection.activate() - - // PING frames without ack set should be ignored, we rely on the HTTP/2 handler replying to them. - try connection.ping(data: HTTP2PingData(), ack: false) - XCTAssertNil(try connection.readFrame()) - } - - func testReceiveGoAway() throws { - let connection = try Connection() - try connection.activate() - - try connection.goAway( - lastStreamID: 0, - errorCode: .enhanceYourCalm, - opaqueData: ByteBuffer(string: "too_many_pings") - ) - - // Should read out an event and close (because there are no open streams). - XCTAssertEqual( - try connection.readEvent(), - .closing(.goAway(.enhanceYourCalm, "too_many_pings")) - ) - try connection.waitUntilClosed() - } - - func testReceiveGoAwayWithOpenStreams() throws { - let connection = try Connection() - try connection.activate() - - connection.streamOpened(1) - connection.streamOpened(2) - connection.streamOpened(3) - - try connection.goAway(lastStreamID: .maxID, errorCode: .noError) - - // Should read out an event. - XCTAssertEqual(try connection.readEvent(), .closing(.goAway(.noError, ""))) - - // Close streams so the connection can close. - connection.streamClosed(1) - connection.streamClosed(2) - connection.streamClosed(3) - try connection.waitUntilClosed() - } - - func testGoAwayWithNoErrorThenGoAwayWithProtocolError() throws { - let connection = try Connection() - try connection.activate() - - connection.streamOpened(1) - connection.streamOpened(2) - connection.streamOpened(3) - - try connection.goAway(lastStreamID: .maxID, errorCode: .noError) - // Should read out an event. - XCTAssertEqual(try connection.readEvent(), .closing(.goAway(.noError, ""))) - - // Upgrade the close from graceful to 'error'. - try connection.goAway(lastStreamID: .maxID, errorCode: .protocolError) - // Should read out an event and the connection will be closed without waiting for notification - // from existing streams. - XCTAssertEqual(try connection.readEvent(), .closing(.goAway(.protocolError, ""))) - try connection.waitUntilClosed() - } - - func testOutboundGracefulClose() throws { - let connection = try Connection() - try connection.activate() - - connection.streamOpened(1) - let closed = connection.closeGracefully() - XCTAssertEqual(try connection.readEvent(), .closing(.initiatedLocally)) - connection.streamClosed(1) - try closed.wait() - } - - func testReceiveInitialSettings() throws { - let connection = try Connection() - try connection.activate() - - // Nothing yet. - XCTAssertNil(try connection.readEvent()) - - // Write the initial settings. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - // Receiving another settings frame should be a no-op. - try connection.settings([]) - XCTAssertNil(try connection.readEvent()) - } - - func testReceiveErrorWhenIdle() throws { - let connection = try Connection() - try connection.activate() - - // Write the initial settings. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - // Write an error and close. - let error = RPCError(code: .aborted, message: "") - connection.channel.pipeline.fireErrorCaught(error) - connection.channel.close(mode: .all, promise: nil) - - XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(error, isIdle: true))) - } - - func testReceiveErrorWhenStreamsAreOpen() throws { - let connection = try Connection() - try connection.activate() - - // Write the initial settings. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - // Open a stream. - connection.streamOpened(1) - - // Write an error and close. - let error = RPCError(code: .aborted, message: "") - connection.channel.pipeline.fireErrorCaught(error) - connection.channel.close(mode: .all, promise: nil) - - XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(error, isIdle: false))) - } - - func testUnexpectedCloseWhenIdle() throws { - let connection = try Connection() - try connection.activate() - - // Write the initial settings. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - connection.channel.close(mode: .all, promise: nil) - XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(nil, isIdle: true))) - } - - func testUnexpectedCloseWhenStreamsAreOpen() throws { - let connection = try Connection() - try connection.activate() - - // Write the initial settings. - try connection.settings([]) - XCTAssertEqual(try connection.readEvent(), .ready) - - connection.streamOpened(1) - connection.channel.close(mode: .all, promise: nil) - XCTAssertEqual(try connection.readEvent(), .closing(.unexpected(nil, isIdle: false))) - } -} - -extension ClientConnectionHandlerTests { - struct Connection { - let channel: EmbeddedChannel - let streamDelegate: any NIOHTTP2StreamDelegate - var loop: EmbeddedEventLoop { - self.channel.embeddedEventLoop - } - - init( - maxIdleTime: TimeAmount? = nil, - keepaliveTime: TimeAmount? = nil, - keepaliveTimeout: TimeAmount? = nil, - allowKeepaliveWithoutCalls: Bool = false - ) throws { - let loop = EmbeddedEventLoop() - let handler = ClientConnectionHandler( - eventLoop: loop, - maxIdleTime: maxIdleTime, - keepaliveTime: keepaliveTime, - keepaliveTimeout: keepaliveTimeout, - keepaliveWithoutCalls: allowKeepaliveWithoutCalls - ) - - self.streamDelegate = handler.http2StreamDelegate - self.channel = EmbeddedChannel(handler: handler, loop: loop) - } - - func activate() throws { - try self.channel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() - } - - func streamOpened(_ id: HTTP2StreamID) { - self.streamDelegate.streamCreated(id, channel: self.channel) - } - - func streamClosed(_ id: HTTP2StreamID) { - self.streamDelegate.streamClosed(id, channel: self.channel) - } - - func goAway( - lastStreamID: HTTP2StreamID, - errorCode: HTTP2ErrorCode, - opaqueData: ByteBuffer? = nil - ) throws { - let frame = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: lastStreamID, errorCode: errorCode, opaqueData: opaqueData) - ) - - try self.channel.writeInbound(frame) - } - - func ping(data: HTTP2PingData, ack: Bool) throws { - let frame = HTTP2Frame(streamID: .rootStream, payload: .ping(data, ack: ack)) - try self.channel.writeInbound(frame) - } - - func settings(_ settings: [HTTP2Setting]) throws { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) - try self.channel.writeInbound(frame) - } - - func readFrame() throws -> HTTP2Frame? { - return try self.channel.readOutbound(as: HTTP2Frame.self) - } - - func readEvent() throws -> ClientConnectionEvent? { - return try self.channel.readInbound(as: ClientConnectionEvent.self) - } - - func waitUntilClosed() throws { - self.channel.embeddedEventLoop.run() - try self.channel.closeFuture.wait() - } - - func closeGracefully() -> EventLoopFuture { - let promise = self.channel.embeddedEventLoop.makePromise(of: Void.self) - let event = ClientConnectionHandler.OutboundEvent.closeGracefully - self.channel.pipeline.triggerUserOutboundEvent(event, promise: promise) - return promise.futureResult - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift deleted file mode 100644 index 171e886ac..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Connection+Equatable.swift +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 GRPCHTTP2Core - -// Equatable conformance for these types is 'best effort', this is sufficient for testing but not -// for general use. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Connection.Event: Equatable {} -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Connection.CloseReason: Equatable {} - -extension ClientConnectionEvent: Equatable {} -extension ClientConnectionEvent.CloseReason: Equatable {} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Connection.Event { - package static func == (lhs: Connection.Event, rhs: Connection.Event) -> Bool { - switch (lhs, rhs) { - case (.connectSucceeded, .connectSucceeded), - (.connectFailed, .connectFailed): - return true - - case (.goingAway(let lhsCode, let lhsReason), .goingAway(let rhsCode, let rhsReason)): - return lhsCode == rhsCode && lhsReason == rhsReason - - case (.closed(let lhsReason), .closed(let rhsReason)): - return lhsReason == rhsReason - - default: - return false - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Connection.CloseReason { - package static func == (lhs: Connection.CloseReason, rhs: Connection.CloseReason) -> Bool { - switch (lhs, rhs) { - case (.idleTimeout, .idleTimeout), - (.keepaliveTimeout, .keepaliveTimeout), - (.initiatedLocally, .initiatedLocally), - (.remote, .remote): - return true - - case (.error(let lhsError, let lhsStreams), .error(let rhsError, let rhsStreams)): - if let lhs = lhsError as? RPCError, let rhs = rhsError as? RPCError { - return lhs == rhs && lhsStreams == rhsStreams - } else { - return lhsStreams == rhsStreams - } - - default: - return false - } - } -} - -extension ClientConnectionEvent { - package static func == (lhs: ClientConnectionEvent, rhs: ClientConnectionEvent) -> Bool { - switch (lhs, rhs) { - case (.ready, .ready): - return true - case (.closing(let lhsReason), .closing(let rhsReason)): - return lhsReason == rhsReason - default: - return false - } - } -} - -extension ClientConnectionEvent.CloseReason { - package static func == (lhs: Self, rhs: Self) -> Bool { - switch (lhs, rhs) { - case (.goAway(let lhsCode, let lhsMessage), .goAway(let rhsCode, let rhsMessage)): - return lhsCode == rhsCode && lhsMessage == rhsMessage - case (.unexpected(let lhsError, let lhsIsIdle), .unexpected(let rhsError, let rhsIsIdle)): - if let lhs = lhsError as? RPCError, let rhs = rhsError as? RPCError { - return lhs == rhs && lhsIsIdle == rhsIsIdle - } else { - return lhsIsIdle == rhsIsIdle - } - case (.keepaliveExpired, .keepaliveExpired), - (.idle, .idle), - (.initiatedLocally, .initiatedLocally): - return true - default: - return false - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift deleted file mode 100644 index 3898513ca..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionBackoffTests.swift +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 XCTest - -@testable import GRPCHTTP2Core - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class ConnectionBackoffTests: XCTestCase { - func testUnjitteredBackoff() { - let backoff = ConnectionBackoff( - initial: .seconds(10), - max: .seconds(30), - multiplier: 1.5, - jitter: 0.0 - ) - - var iterator = backoff.makeIterator() - XCTAssertEqual(iterator.next(), .seconds(10)) - // 10 * 1.5 = 15 seconds - XCTAssertEqual(iterator.next(), .seconds(15)) - // 15 * 1.5 = 22.5 seconds - XCTAssertEqual(iterator.next(), .seconds(22.5)) - // 22.5 * 1.5 = 33.75 seconds, clamped to 30 seconds, all future values will be the same. - XCTAssertEqual(iterator.next(), .seconds(30)) - XCTAssertEqual(iterator.next(), .seconds(30)) - XCTAssertEqual(iterator.next(), .seconds(30)) - } - - func testJitteredBackoff() { - let backoff = ConnectionBackoff( - initial: .seconds(10), - max: .seconds(30), - multiplier: 1.5, - jitter: 0.1 - ) - - var iterator = backoff.makeIterator() - - // Initial isn't jittered. - XCTAssertEqual(iterator.next(), .seconds(10)) - - // Next value should be 10 * 1.5 = 15 seconds ± 1.5 seconds - var expected: ClosedRange = .seconds(13.5) ... .seconds(16.5) - XCTAssert(expected.contains(iterator.next())) - - // Next value should be 15 * 1.5 = 22.5 seconds ± 2.25 seconds - expected = .seconds(20.25) ... .seconds(24.75) - XCTAssert(expected.contains(iterator.next())) - - // Next value should be 22.5 * 1.5 = 33.75 seconds, clamped to 30 seconds ± 3 seconds. - // All future values will be in the same range. - expected = .seconds(27) ... .seconds(33) - XCTAssert(expected.contains(iterator.next())) - XCTAssert(expected.contains(iterator.next())) - XCTAssert(expected.contains(iterator.next())) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift deleted file mode 100644 index bcc6d86ed..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/ConnectionTests.swift +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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 DequeModule -import GRPCCore -import GRPCHTTP2Core -import NIOCore -import NIOHPACK -import NIOHTTP2 -import NIOPosix -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class ConnectionTests: XCTestCase { - func testConnectThenClose() async throws { - try await ConnectionTest.run(connector: .posix()) { context, event in - switch event { - case .connectSucceeded: - context.connection.close() - default: - () - } - } validateEvents: { _, events in - XCTAssertEqual(events, [.connectSucceeded, .closed(.initiatedLocally)]) - } - } - - func testConnectThenIdleTimeout() async throws { - try await ConnectionTest.run(connector: .posix(maxIdleTime: .milliseconds(50))) { _, events in - XCTAssertEqual(events, [.connectSucceeded, .closed(.idleTimeout)]) - } - } - - func testConnectThenKeepaliveTimeout() async throws { - try await ConnectionTest.run( - connector: .posix( - keepaliveTime: .milliseconds(50), - keepaliveTimeout: .milliseconds(10), - keepaliveWithoutCalls: true, - dropPingAcks: true - ) - ) { _, events in - XCTAssertEqual(events, [.connectSucceeded, .closed(.keepaliveTimeout)]) - } - } - - func testGoAwayWhenConnected() async throws { - try await ConnectionTest.run(connector: .posix()) { context, event in - switch event { - case .connectSucceeded: - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway( - lastStreamID: 0, - errorCode: .noError, - opaqueData: ByteBuffer(string: "Hello!") - ) - ) - - let accepted = try context.server.acceptedChannel - accepted.writeAndFlush(goAway, promise: nil) - - default: - () - } - } validateEvents: { _, events in - XCTAssertEqual(events, [.connectSucceeded, .goingAway(.noError, "Hello!"), .closed(.remote)]) - } - } - - func testConnectionDropWhenConnected() async throws { - try await ConnectionTest.run(connector: .posix()) { context, event in - switch event { - case .connectSucceeded: - let accepted = try context.server.acceptedChannel - accepted.close(mode: .all, promise: nil) - - default: - () - } - } validateEvents: { _, events in - let error = RPCError( - code: .unavailable, - message: "The TCP connection was dropped unexpectedly." - ) - - let expected: [Connection.Event] = [.connectSucceeded, .closed(.error(error, wasIdle: true))] - XCTAssertEqual(events, expected) - } - } - - func testConnectFails() async throws { - let error = RPCError(code: .unimplemented, message: "") - try await ConnectionTest.run(connector: .throwing(error)) { _, events in - XCTAssertEqual(events, [.connectFailed(error)]) - } - } - - func testConnectFailsOnAcceptedThenClosedTCPConnection() async throws { - try await ConnectionTest.run(connector: .posix(), server: .closeOnAccept) { _, events in - XCTAssertEqual(events.count, 1) - let event = try XCTUnwrap(events.first) - switch event { - case .connectFailed(let error): - XCTAssert(error, as: RPCError.self) { rpcError in - XCTAssertEqual(rpcError.code, .unavailable) - } - default: - XCTFail("Expected '.connectFailed', got '\(event)'") - } - } - } - - func testMakeStreamOnActiveConnection() async throws { - try await ConnectionTest.run(connector: .posix()) { context, event in - switch event { - case .connectSucceeded: - let stream = try await context.connection.makeStream( - descriptor: .echoGet, - options: .defaults - ) - try await stream.execute { inbound, outbound in - try await outbound.write(.metadata(["foo": "bar", "bar": "baz"])) - try await outbound.write(.message([0, 1, 2])) - outbound.finish() - - var parts = [RPCResponsePart]() - for try await part in inbound { - switch part { - case .metadata(let metadata): - // Filter out any transport specific metadata - parts.append(.metadata(Metadata(metadata.suffix(2)))) - case .message, .status: - parts.append(part) - } - } - - let expected: [RPCResponsePart] = [ - .metadata(["foo": "bar", "bar": "baz"]), - .message([0, 1, 2]), - .status(Status(code: .ok, message: ""), [:]), - ] - XCTAssertEqual(parts, expected) - } - - context.connection.close() - - default: - () - } - } validateEvents: { _, events in - XCTAssertEqual(events, [.connectSucceeded, .closed(.initiatedLocally)]) - } - } - - func testMakeStreamOnClosedConnection() async throws { - try await ConnectionTest.run(connector: .posix()) { context, event in - switch event { - case .connectSucceeded: - context.connection.close() - case .closed: - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - _ = try await context.connection.makeStream(descriptor: .echoGet, options: .defaults) - } errorHandler: { error in - XCTAssertEqual(error.code, .unavailable) - } - default: - () - } - } validateEvents: { context, events in - XCTAssertEqual(events, [.connectSucceeded, .closed(.initiatedLocally)]) - } - } - - func testMakeStreamOnNotRunningConnection() async throws { - let connection = Connection( - address: .ipv4(host: "ignored", port: 0), - http2Connector: .never, - defaultCompression: .none, - enabledCompression: .none - ) - - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - _ = try await connection.makeStream(descriptor: .echoGet, options: .defaults) - } errorHandler: { error in - XCTAssertEqual(error.code, .unavailable) - } - } -} - -extension ClientBootstrap { - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - func connect( - to address: GRPCHTTP2Core.SocketAddress, - _ configure: @Sendable @escaping (any Channel) -> EventLoopFuture - ) async throws -> T { - if let ipv4 = address.ipv4 { - return try await self.connect( - host: ipv4.host, - port: ipv4.port, - channelInitializer: configure - ) - } else if let ipv6 = address.ipv6 { - return try await self.connect( - host: ipv6.host, - port: ipv6.port, - channelInitializer: configure - ) - } else if let uds = address.unixDomainSocket { - return try await self.connect( - unixDomainSocketPath: uds.path, - channelInitializer: configure - ) - } else if let vsock = address.virtualSocket { - return try await self.connect( - to: VsockAddress( - cid: .init(Int(vsock.contextID.rawValue)), - port: .init(Int(vsock.port.rawValue)) - ), - channelInitializer: configure - ) - } else { - throw RPCError(code: .unimplemented, message: "Unhandled socket address: \(address)") - } - } -} - -extension Metadata { - init(_ sequence: some Sequence) { - var metadata = Metadata() - for (key, value) in sequence { - switch value { - case .string(let value): - metadata.addString(value, forKey: key) - case .binary(let value): - metadata.addBinary(value, forKey: key) - } - } - - self = metadata - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift deleted file mode 100644 index fc0365703..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/GRPCChannelTests.swift +++ /dev/null @@ -1,842 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOHTTP2 -import NIOPosix -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class GRPCChannelTests: XCTestCase { - func testDefaultServiceConfig() throws { - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoGet)])] - serviceConfig.retryThrottling = try ServiceConfig.RetryThrottling( - maxTokens: 100, - tokenRatio: 0.1 - ) - - let channel = GRPCChannel( - resolver: .static(endpoints: []), - connector: .never, - config: .defaults, - defaultServiceConfig: serviceConfig - ) - - XCTAssertNotNil(channel.config(forMethod: .echoGet)) - XCTAssertNil(channel.config(forMethod: .echoUpdate)) - - let throttle = try XCTUnwrap(channel.retryThrottle) - XCTAssertEqual(throttle.maxTokens, 100) - XCTAssertEqual(throttle.tokenRatio, 0.1) - } - - func testServiceConfigFromResolver() async throws { - // Verify that service config from the resolver takes precedence over the default service - // config. This is done indirectly by checking method config and retry throttle config. - - // Create a service config to provide via the resolver. - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoGet)])] - serviceConfig.retryThrottling = try ServiceConfig.RetryThrottling( - maxTokens: 100, - tokenRatio: 0.1 - ) - - // Need a server to connect to, no RPCs will be created though. - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - - let channel = GRPCChannel( - resolver: .static(endpoints: [Endpoint(addresses: [address])], serviceConfig: serviceConfig), - connector: .posix(), - config: .defaults, - defaultServiceConfig: ServiceConfig() - ) - - // Not resolved yet so the default (empty) service config is used. - XCTAssertNil(channel.config(forMethod: .echoGet)) - XCTAssertNil(channel.config(forMethod: .echoUpdate)) - XCTAssertNil(channel.retryThrottle) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await server.run(.never) - } - - group.addTask { - await channel.connect() - } - - for await event in channel.connectivityState { - switch event { - case .ready: - // When the channel is ready it must have the service config from the resolver. - XCTAssertNotNil(channel.config(forMethod: .echoGet)) - XCTAssertNil(channel.config(forMethod: .echoUpdate)) - - let throttle = try XCTUnwrap(channel.retryThrottle) - XCTAssertEqual(throttle.maxTokens, 100) - XCTAssertEqual(throttle.tokenRatio, 0.1) - - // Now close. - channel.beginGracefulShutdown() - - default: - () - } - } - - group.cancelAll() - } - } - - func testServiceConfigFromResolverAfterUpdate() async throws { - // Verify that the channel uses service config from the resolver and that it uses the latest - // version provided by the resolver. This is done indirectly by checking method config and retry - // throttle config. - - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - - let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) - let channel = GRPCChannel( - resolver: resolver, - connector: .posix(), - config: .defaults, - defaultServiceConfig: ServiceConfig() - ) - - // Not resolved yet so the default (empty) service config is used. - XCTAssertNil(channel.config(forMethod: .echoGet)) - XCTAssertNil(channel.config(forMethod: .echoUpdate)) - XCTAssertNil(channel.retryThrottle) - - // Yield the first address list and service config. - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoGet)])] - serviceConfig.retryThrottling = try ServiceConfig.RetryThrottling( - maxTokens: 100, - tokenRatio: 0.1 - ) - let resolutionResult = NameResolutionResult( - endpoints: [Endpoint(address)], - serviceConfig: .success(serviceConfig) - ) - continuation.yield(resolutionResult) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await server.run(.never) - } - - group.addTask { - await channel.connect() - } - - for await event in channel.connectivityState { - switch event { - case .ready: - // When the channel it must have the service config from the resolver. - XCTAssertNotNil(channel.config(forMethod: .echoGet)) - XCTAssertNil(channel.config(forMethod: .echoUpdate)) - let throttle = try XCTUnwrap(channel.retryThrottle) - XCTAssertEqual(throttle.maxTokens, 100) - XCTAssertEqual(throttle.tokenRatio, 0.1) - - // Now yield a new service config with the same addresses. - var resolutionResult = resolutionResult - serviceConfig.methodConfig = [MethodConfig(names: [MethodConfig.Name(.echoUpdate)])] - serviceConfig.retryThrottling = nil - resolutionResult.serviceConfig = .success(serviceConfig) - continuation.yield(resolutionResult) - - // This should be propagated quickly. - try await XCTPoll(every: .milliseconds(10)) { - let noConfigForGet = channel.config(forMethod: .echoGet) == nil - let configForUpdate = channel.config(forMethod: .echoUpdate) != nil - let noThrottle = channel.retryThrottle == nil - return noConfigForGet && configForUpdate && noThrottle - } - - channel.beginGracefulShutdown() - - default: - () - } - } - - group.cancelAll() - } - } - - func testPushBasedResolutionUpdates() async throws { - // Verify that the channel responds to name resolution changes which are pushed into - // the resolver. Do this by starting two servers and only making the address of one available - // via the resolver at a time. Server identity is provided via metadata in the RPC. - - // Start a few servers. - let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address1 = try await server1.bind() - - let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address2 = try await server2.bind() - - // Setup a resolver and push some changes into it. - let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) - let resolution1 = NameResolutionResult(endpoints: [Endpoint(address1)], serviceConfig: nil) - continuation.yield(resolution1) - - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - let channel = GRPCChannel( - resolver: resolver, - connector: .posix(), - config: .defaults, - defaultServiceConfig: serviceConfig - ) - - try await withThrowingDiscardingTaskGroup { group in - // Servers respond with their own address in the trailing metadata. - for (server, address) in [(server1, address1), (server2, address2)] { - group.addTask { - try await server.run { inbound, outbound in - let status = Status(code: .ok, message: "") - let metadata: Metadata = ["server-addr": "\(address)"] - try await outbound.write(.status(status, metadata)) - outbound.finish() - } - } - } - - group.addTask { - await channel.connect() - } - - // The stream will be queued until the channel is ready. - let serverAddress1 = try await channel.serverAddress() - XCTAssertEqual(serverAddress1, "\(address1)") - XCTAssertEqual(server1.clients.count, 1) - XCTAssertEqual(server2.clients.count, 0) - - // Yield the second address. Because this happens asynchronously there's no guarantee that - // the next stream will be made against the same server, so poll until the servers have the - // appropriate connections. - let resolution2 = NameResolutionResult(endpoints: [Endpoint(address2)], serviceConfig: nil) - continuation.yield(resolution2) - - try await XCTPoll(every: .milliseconds(10)) { - server1.clients.count == 0 && server2.clients.count == 1 - } - - let serverAddress2 = try await channel.serverAddress() - XCTAssertEqual(serverAddress2, "\(address2)") - - group.cancelAll() - } - } - - func testPullBasedResolutionUpdates() async throws { - // Verify that the channel responds to name resolution changes which are pulled because a - // subchannel asked the channel to re-resolve. Do this by starting two servers and changing - // which is available via resolution updates. Server identity is provided via metadata in - // the RPC. - - // Start a few servers. - let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address1 = try await server1.bind() - - let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address2 = try await server2.bind() - - // Setup a resolve which we push changes into. - let (resolver, continuation) = NameResolver.dynamic(updateMode: .pull) - - // Yield the addresses. - for address in [address1, address2] { - let resolution = NameResolutionResult(endpoints: [Endpoint(address)], serviceConfig: nil) - continuation.yield(resolution) - } - - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - let channel = GRPCChannel( - resolver: resolver, - connector: .posix(), - config: .defaults, - defaultServiceConfig: serviceConfig - ) - - try await withThrowingDiscardingTaskGroup { group in - // Servers respond with their own address in the trailing metadata. - for (server, address) in [(server1, address1), (server2, address2)] { - group.addTask { - try await server.run { inbound, outbound in - let status = Status(code: .ok, message: "") - let metadata: Metadata = ["server-addr": "\(address)"] - try await outbound.write(.status(status, metadata)) - outbound.finish() - } - } - } - - group.addTask { - await channel.connect() - } - - // The stream will be queued until the channel is ready. - let serverAddress1 = try await channel.serverAddress() - XCTAssertEqual(serverAddress1, "\(address1)") - XCTAssertEqual(server1.clients.count, 1) - XCTAssertEqual(server2.clients.count, 0) - - // Tell the first server to GOAWAY. This will cause the subchannel to re-resolve. - let server1Client = try XCTUnwrap(server1.clients.first) - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: 1, errorCode: .noError, opaqueData: nil) - ) - try await server1Client.writeAndFlush(goAway) - - // Poll until the first client drops, addresses are re-resolved, and a connection is - // established to server2. - try await XCTPoll(every: .milliseconds(10)) { - server1.clients.count == 0 && server2.clients.count == 1 - } - - let serverAddress2 = try await channel.serverAddress() - XCTAssertEqual(serverAddress2, "\(address2)") - - group.cancelAll() - } - } - - func testCloseWhenRPCsAreInProgress() async throws { - // Verify that closing the channel while there are RPCs in progress allows the RPCs to finish - // gracefully. - - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await server.run(.echo) - } - - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - - let channel = GRPCChannel( - resolver: .static(endpoints: [Endpoint(address)]), - connector: .posix(), - config: .defaults, - defaultServiceConfig: serviceConfig - ) - - group.addTask { - await channel.connect() - } - - try await channel.withStream(descriptor: .echoGet, options: .defaults) { stream in - try await stream.outbound.write(.metadata([:])) - - var iterator = stream.inbound.makeAsyncIterator() - let part1 = try await iterator.next() - switch part1 { - case .metadata: - // Got metadata, close the channel. - channel.beginGracefulShutdown() - case .message, .status, .none: - XCTFail("Expected metadata, got \(String(describing: part1))") - } - - for await state in channel.connectivityState { - switch state { - case .shutdown: - // Happens when shutting-down has been initiated, so finish the RPC. - await stream.outbound.finish() - - let part2 = try await iterator.next() - switch part2 { - case .status(let status, _): - XCTAssertEqual(status.code, .ok) - case .metadata, .message, .none: - XCTFail("Expected status, got \(String(describing: part2))") - } - - default: - () - } - } - } - - group.cancelAll() - } - } - - func testQueueRequestsWhileNotReady() async throws { - // Verify that requests are queued until the channel becomes ready. As creating streams - // will race with the channel becoming ready, we add numerous tasks to the task group which - // each create a stream before making the server address known to the channel via the resolver. - // This isn't perfect as the resolution _could_ happen before attempting to create all streams - // although this is unlikely. - - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - - let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - let channel = GRPCChannel( - resolver: resolver, - connector: .posix(), - config: .defaults, - defaultServiceConfig: serviceConfig - ) - - enum Subtask { case rpc, other } - try await withThrowingTaskGroup(of: Subtask.self) { group in - // Run the server. - group.addTask { - try await server.run { inbound, outbound in - for try await part in inbound { - switch part { - case .metadata: - try await outbound.write(.metadata([:])) - case .message(let bytes): - try await outbound.write(.message(bytes)) - } - } - - let status = Status(code: .ok, message: "") - try await outbound.write(.status(status, [:])) - outbound.finish() - } - - return .other - } - - group.addTask { - await channel.connect() - return .other - } - - // Start a bunch of requests. These won't start until an address is yielded, they should - // be queued though. - for _ in 1 ... 100 { - group.addTask { - try await channel.withStream(descriptor: .echoGet, options: .defaults) { stream in - try await stream.outbound.write(.metadata([:])) - await stream.outbound.finish() - - for try await part in stream.inbound { - switch part { - case .metadata, .message: - () - case .status(let status, _): - XCTAssertEqual(status.code, .ok) - } - } - } - - return .rpc - } - } - - // At least some of the RPCs should have been queued by now. - let resolution = NameResolutionResult(endpoints: [Endpoint(address)], serviceConfig: nil) - continuation.yield(resolution) - - var outstandingRPCs = 100 - for try await subtask in group { - switch subtask { - case .rpc: - outstandingRPCs -= 1 - - // All RPCs done, close the channel and cancel the group to stop the server. - if outstandingRPCs == 0 { - channel.beginGracefulShutdown() - group.cancelAll() - } - - case .other: - () - } - } - } - } - - func testQueueRequestsFailFast() async throws { - // Verifies that if 'waitsForReady' is 'false', that queued requests are failed when there is - // a transient failure. The transient failure is triggered by attempting to connect to a - // non-existent server. - - let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - let channel = GRPCChannel( - resolver: resolver, - connector: .posix(), - config: .defaults, - defaultServiceConfig: serviceConfig - ) - - enum Subtask { case rpc, other } - try await withThrowingTaskGroup(of: Subtask.self) { group in - group.addTask { - await channel.connect() - return .other - } - - for _ in 1 ... 100 { - group.addTask { - var options = CallOptions.defaults - options.waitForReady = false - - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await channel.withStream(descriptor: .echoGet, options: options) { _ in - XCTFail("Unexpected stream") - } - } errorHandler: { error in - XCTAssertEqual(error.code, .unavailable) - } - - return .rpc - } - } - - // At least some of the RPCs should have been queued by now. - let resolution = NameResolutionResult( - endpoints: [Endpoint(.unixDomainSocket(path: "/test-queue-requests-fail-fast"))], - serviceConfig: nil - ) - continuation.yield(resolution) - - var outstandingRPCs = 100 - for try await subtask in group { - switch subtask { - case .rpc: - outstandingRPCs -= 1 - - // All RPCs done, close the channel and cancel the group to stop the server. - if outstandingRPCs == 0 { - channel.beginGracefulShutdown() - group.cancelAll() - } - - case .other: - () - } - } - } - } - - func testLoadBalancerChangingFromRoundRobinToPickFirst() async throws { - // The test will push different configs to the resolver, first a round-robin LB, then a - // pick-first LB. - let (resolver, continuation) = NameResolver.dynamic(updateMode: .push) - let channel = GRPCChannel( - resolver: resolver, - connector: .posix(), - config: .defaults, - defaultServiceConfig: ServiceConfig() - ) - - // Start a few servers. - let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address1 = try await server1.bind() - - let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address2 = try await server2.bind() - - let server3 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address3 = try await server3.bind() - - try await withThrowingTaskGroup(of: Void.self) { group in - // Run the servers, no RPCs will be run against them. - for server in [server1, server2, server3] { - group.addTask { - try await server.run(.never) - } - } - - group.addTask { - await channel.connect() - } - - for await event in channel.connectivityState { - switch event { - case .idle: - let endpoints = [address1, address2].map { Endpoint(addresses: [$0]) } - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - let resolutionResult = NameResolutionResult( - endpoints: endpoints, - serviceConfig: .success(serviceConfig) - ) - - // Push the first resolution result which uses round robin. This will result in the - // channel becoming ready. - continuation.yield(resolutionResult) - - case .ready: - // Channel is ready, server 1 and 2 should have clients shortly. - try await XCTPoll(every: .milliseconds(10)) { - server1.clients.count == 1 && server2.clients.count == 1 && server3.clients.count == 0 - } - - // Both subchannels are ready, prepare and yield an update to the resolver. - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.pickFirst(shuffleAddressList: false)] - let resolutionResult = NameResolutionResult( - endpoints: [Endpoint(addresses: [address3])], - serviceConfig: .success(serviceConfig) - ) - continuation.yield(resolutionResult) - - // Only server 3 should have a connection. - try await XCTPoll(every: .milliseconds(10)) { - server1.clients.count == 0 && server2.clients.count == 0 && server3.clients.count == 1 - } - - channel.beginGracefulShutdown() - - case .shutdown: - group.cancelAll() - - default: - () - } - } - } - } - - func testPickFirstShufflingAddressList() async throws { - // This test checks that the pick first load-balancer has its address list shuffled. We can't - // assert this deterministically, so instead we'll run an experiment a number of times. Each - // round will create N servers and provide them as endpoints to the pick-first load balancer. - // The channel will establish a connection to one of the servers and its identity will be noted. - let numberOfRounds = 100 - let numberOfServers = 2 - - let servers = (0 ..< numberOfServers).map { _ in - TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - } - - var addresses = [SocketAddress]() - for server in servers { - let address = try await server.bind() - addresses.append(address) - } - - let endpoint = Endpoint(addresses: addresses) - var counts = Array(repeating: 0, count: addresses.count) - - // Supply service config on init, not via the load-balancer. - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.pickFirst(shuffleAddressList: true)] - - try await withThrowingDiscardingTaskGroup { group in - // Run the servers. - for server in servers { - group.addTask { - try await server.run(.never) - } - } - - // Run the experiment. - for _ in 0 ..< numberOfRounds { - let channel = GRPCChannel( - resolver: .static(endpoints: [endpoint]), - connector: .posix(), - config: .defaults, - defaultServiceConfig: serviceConfig - ) - - group.addTask { - await channel.connect() - } - - for await state in channel.connectivityState { - switch state { - case .ready: - for index in servers.indices { - if servers[index].clients.count == 1 { - counts[index] += 1 - break - } - } - channel.beginGracefulShutdown() - default: - () - } - } - } - - // Stop the servers. - group.cancelAll() - } - - // The address list is shuffled, so there's no guarantee how many times we'll hit each server. - // Assert that the minimum a server should be hit is 10% of the time. - let expected = Double(numberOfRounds) / Double(numberOfServers) - let minimum = expected * 0.1 - XCTAssert(counts.allSatisfy({ Double($0) >= minimum }), "\(counts)") - } - - func testPickFirstIsFallbackPolicy() async throws { - // Start a few servers. - let server1 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address1 = try await server1.bind() - - let server2 = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address2 = try await server2.bind() - - // Prepare a channel with an empty service config. - let channel = GRPCChannel( - resolver: .static(endpoints: [Endpoint(address1, address2)]), - connector: .posix(), - config: .defaults, - defaultServiceConfig: ServiceConfig() - ) - - try await withThrowingDiscardingTaskGroup { group in - // Run the servers. - for server in [server1, server2] { - group.addTask { - try await server.run(.never) - } - } - - group.addTask { - await channel.connect() - } - - for try await state in channel.connectivityState { - switch state { - case .ready: - // Only server 1 should have a connection. - try await XCTPoll(every: .milliseconds(10)) { - server1.clients.count == 1 && server2.clients.count == 0 - } - - channel.beginGracefulShutdown() - - default: - () - } - } - - group.cancelAll() - } - } - - func testQueueRequestsThenClose() async throws { - // Set a high backoff so the channel stays in transient failure for long enough. - var config = GRPCChannel.Config.defaults - config.backoff.initial = .seconds(120) - - let channel = GRPCChannel( - resolver: .static( - endpoints: [ - Endpoint(.unixDomainSocket(path: "/testQueueRequestsThenClose")) - ] - ), - connector: .posix(), - config: .defaults, - defaultServiceConfig: ServiceConfig() - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - await channel.connect() - } - - for try await state in channel.connectivityState { - switch state { - case .transientFailure: - group.addTask { - // Sleep a little to increase the chances of the stream being queued before the channel - // reacts to the close. - try await Task.sleep(for: .milliseconds(10)) - channel.beginGracefulShutdown() - } - - // Try to open a new stream. - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await channel.withStream(descriptor: .echoGet, options: .defaults) { stream in - XCTFail("Unexpected new stream") - } - } errorHandler: { error in - XCTAssertEqual(error.code, .unavailable) - } - - default: - () - } - } - - group.cancelAll() - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel.Config { - static var defaults: Self { - Self( - http2: .defaults, - backoff: .defaults, - connection: .defaults, - compression: .defaults - ) - } -} - -extension Endpoint { - init(_ addresses: SocketAddress...) { - self.init(addresses: addresses) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension GRPCChannel { - fileprivate func serverAddress() async throws -> String? { - let values: Metadata.StringValues? = try await self.withStream( - descriptor: .echoGet, - options: .defaults - ) { stream in - try await stream.outbound.write(.metadata([:])) - await stream.outbound.finish() - - for try await part in stream.inbound { - switch part { - case .metadata, .message: - XCTFail("Unexpected part: \(part)") - case .status(_, let metadata): - return metadata[stringValues: "server-addr"] - } - } - return nil - } - - return values?.first(where: { _ in true }) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift deleted file mode 100644 index 571f38436..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/LoadBalancerTest.swift +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -enum LoadBalancerTest { - struct Context { - let servers: [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)] - let loadBalancer: LoadBalancer - } - - static func pickFirst( - servers serverCount: Int, - connector: any HTTP2Connector, - backoff: ConnectionBackoff = .defaults, - timeout: Duration = .seconds(10), - function: String = #function, - handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, - verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in } - ) async throws { - try await Self.run( - servers: serverCount, - timeout: timeout, - function: function, - handleEvent: handleEvent, - verifyEvents: verifyEvents - ) { - let pickFirst = PickFirstLoadBalancer( - connector: connector, - backoff: backoff, - defaultCompression: .none, - enabledCompression: .none - ) - return .pickFirst(pickFirst) - } - } - - static func roundRobin( - servers serverCount: Int, - connector: any HTTP2Connector, - backoff: ConnectionBackoff = .defaults, - timeout: Duration = .seconds(10), - function: String = #function, - handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, - verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in } - ) async throws { - try await Self.run( - servers: serverCount, - timeout: timeout, - function: function, - handleEvent: handleEvent, - verifyEvents: verifyEvents - ) { - let roundRobin = RoundRobinLoadBalancer( - connector: connector, - backoff: backoff, - defaultCompression: .none, - enabledCompression: .none - ) - return .roundRobin(roundRobin) - } - } - - private static func run( - servers serverCount: Int, - timeout: Duration, - function: String, - handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, - verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void = { _ in }, - makeLoadBalancer: @escaping @Sendable () -> LoadBalancer - ) async throws { - enum TestEvent { - case timedOut - case completed(Result) - } - - try await withThrowingTaskGroup(of: TestEvent.self) { group in - group.addTask { - try? await Task.sleep(for: timeout) - return .timedOut - } - - group.addTask { - do { - try await Self._run( - servers: serverCount, - handleEvent: handleEvent, - verifyEvents: verifyEvents, - makeLoadBalancer: makeLoadBalancer - ) - return .completed(.success(())) - } catch { - return .completed(.failure(error)) - } - } - - let result = try await group.next()! - group.cancelAll() - - switch result { - case .timedOut: - XCTFail("'\(function)' timed out after \(timeout)") - case .completed(let result): - try result.get() - } - } - } - - private static func _run( - servers serverCount: Int, - handleEvent: @escaping @Sendable (Context, LoadBalancerEvent) async throws -> Void, - verifyEvents: @escaping @Sendable ([LoadBalancerEvent]) -> Void, - makeLoadBalancer: @escaping @Sendable () -> LoadBalancer - ) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - // Create the test servers. - var servers = [(server: TestServer, address: GRPCHTTP2Core.SocketAddress)]() - for _ in 0 ..< serverCount { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - servers.append((server, address)) - - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - } - - // Create the load balancer. - let loadBalancer = makeLoadBalancer() - - group.addTask { - await loadBalancer.run() - } - - let context = Context(servers: servers, loadBalancer: loadBalancer) - - var events = [LoadBalancerEvent]() - for await event in loadBalancer.events { - events.append(event) - try await handleEvent(context, event) - } - - verifyEvents(events) - group.cancelAll() - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension LoadBalancerTest.Context { - var roundRobin: RoundRobinLoadBalancer? { - switch self.loadBalancer { - case .roundRobin(let loadBalancer): - return loadBalancer - case .pickFirst: - return nil - } - } - - var pickFirst: PickFirstLoadBalancer? { - switch self.loadBalancer { - case .roundRobin: - return nil - case .pickFirst(let loadBalancer): - return loadBalancer - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift deleted file mode 100644 index 29764adb5..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/PickFirstLoadBalancerTests.swift +++ /dev/null @@ -1,334 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOHTTP2 -import NIOPosix -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class PickFirstLoadBalancerTests: XCTestCase { - func testPickFirstConnectsToServer() async throws { - try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let endpoint = Endpoint(addresses: context.servers.map { $0.address }) - context.pickFirst!.updateEndpoint(endpoint) - case .connectivityStateChanged(.ready): - context.loadBalancer.close() - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testPickSubchannelWhenNotReady() async throws { - try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - XCTAssertNil(context.loadBalancer.pickSubchannel()) - context.loadBalancer.close() - case .connectivityStateChanged(.shutdown): - XCTAssertNil(context.loadBalancer.pickSubchannel()) - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testPickSubchannelReturnsSameSubchannel() async throws { - try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let endpoint = Endpoint(addresses: context.servers.map { $0.address }) - context.pickFirst!.updateEndpoint(endpoint) - - case .connectivityStateChanged(.ready): - var ids = Set() - for _ in 0 ..< 100 { - let subchannel = try XCTUnwrap(context.loadBalancer.pickSubchannel()) - ids.insert(subchannel.id) - } - XCTAssertEqual(ids.count, 1) - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testEndpointUpdateHandledGracefully() async throws { - try await LoadBalancerTest.pickFirst(servers: 2, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let endpoint = Endpoint(addresses: [context.servers[0].address]) - context.pickFirst!.updateEndpoint(endpoint) - - case .connectivityStateChanged(.ready): - // Must be connected to server-0. - try await XCTPoll(every: .milliseconds(10)) { - context.servers[0].server.clients.count == 1 - } - - // Update the endpoint so that it contains server-1. - let endpoint = Endpoint(addresses: [context.servers[1].address]) - context.pickFirst!.updateEndpoint(endpoint) - - // Should remain in the ready state - try await XCTPoll(every: .milliseconds(10)) { - context.servers[0].server.clients.isEmpty && context.servers[1].server.clients.count == 1 - } - - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testSameEndpointUpdateIsIgnored() async throws { - try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let endpoint = Endpoint(addresses: context.servers.map { $0.address }) - context.pickFirst!.updateEndpoint(endpoint) - - case .connectivityStateChanged(.ready): - // Must be connected to server-0. - try await XCTPoll(every: .milliseconds(10)) { - context.servers[0].server.clients.count == 1 - } - - // Update the endpoint. This should be a no-op, server should remain connected. - let endpoint = Endpoint(addresses: context.servers.map { $0.address }) - context.pickFirst!.updateEndpoint(endpoint) - try await XCTPoll(every: .milliseconds(10)) { - context.servers[0].server.clients.count == 1 - } - - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testEmptyEndpointUpdateIsIgnored() async throws { - // Checks that an update using the empty endpoint is ignored. - try await LoadBalancerTest.pickFirst(servers: 0, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let endpoint = Endpoint(addresses: []) - // Should no-op. - context.pickFirst!.updateEndpoint(endpoint) - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testPickOnIdleTriggersConnect() async throws { - // Tests that picking a subchannel when the load balancer is idle triggers a reconnect and - // becomes ready again. Uses a very short idle time to re-enter the idle state. - let idle = AtomicCounter() - - try await LoadBalancerTest.pickFirst( - servers: 1, - connector: .posix(maxIdleTime: .milliseconds(1)) // Aggressively idle the connection - ) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let (_, idleCount) = idle.increment() - - switch idleCount { - case 1: - // The first idle happens when the load balancer in started, give it an endpoint - // which it will connect to. Wait for it to be ready and then idle again. - let endpoint = Endpoint(addresses: context.servers.map { $0.address }) - context.pickFirst!.updateEndpoint(endpoint) - case 2: - // Load-balancer has the endpoints but all are idle. Picking will trigger a connect. - XCTAssertNil(context.loadBalancer.pickSubchannel()) - case 3: - // Connection idled again. Shut it down. - context.loadBalancer.close() - - default: - XCTFail("Became idle too many times") - } - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.idle), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testPickFirstConnectionDropReturnsToIdle() async throws { - // Checks that when the load balancers connection is unexpectedly dropped when there are no - // open streams that it returns to the idle state. - let idleCount = AtomicCounter() - - try await LoadBalancerTest.pickFirst(servers: 1, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let (_, newIdleCount) = idleCount.increment() - switch newIdleCount { - case 1: - let endpoint = Endpoint(addresses: context.servers.map { $0.address }) - context.pickFirst!.updateEndpoint(endpoint) - case 2: - context.loadBalancer.close() - default: - () - } - - case .connectivityStateChanged(.ready): - // Drop the connection. - context.servers[0].server.clients[0].close(mode: .all, promise: nil) - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.idle), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testPickFirstReceivesGoAway() async throws { - let idleCount = AtomicCounter() - try await LoadBalancerTest.pickFirst(servers: 2, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let (_, newIdleCount) = idleCount.increment() - switch newIdleCount { - case 1: - // Provide the address of the first server. - context.pickFirst!.updateEndpoint(Endpoint(context.servers[0].address)) - case 2: - // Provide the address of the second server. - context.pickFirst!.updateEndpoint(Endpoint(context.servers[1].address)) - default: - () - } - - case .connectivityStateChanged(.ready): - switch idleCount.value { - case 1: - // Must be connected to server 1, send a GOAWAY frame. - let channel = context.servers[0].server.clients.first! - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: 0, errorCode: .noError, opaqueData: nil) - ) - channel.writeAndFlush(goAway, promise: nil) - - case 2: - // Must only be connected to server 2 now. - XCTAssertEqual(context.servers[0].server.clients.count, 0) - XCTAssertEqual(context.servers[1].server.clients.count, 1) - context.loadBalancer.close() - - default: - () - } - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .requiresNameResolution, - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift deleted file mode 100644 index b53e038b7..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/RoundRobinLoadBalancerTests.swift +++ /dev/null @@ -1,379 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOHTTP2 -import NIOPosix -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class RoundRobinLoadBalancerTests: XCTestCase { - func testMultipleConnectionsAreEstablished() async throws { - try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - // Update the addresses for the load balancer, this will trigger subchannels to be created - // for each. - let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } - context.roundRobin!.updateAddresses(endpoints) - - case .connectivityStateChanged(.ready): - // Poll until each server has one connected client. - try await XCTPoll(every: .milliseconds(10)) { - context.servers.allSatisfy { server, _ in server.clients.count == 1 } - } - - // Close to end the test. - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testSubchannelsArePickedEvenly() async throws { - try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - // Update the addresses for the load balancer, this will trigger subchannels to be created - // for each. - let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } - context.roundRobin!.updateAddresses(endpoints) - - case .connectivityStateChanged(.ready): - // Subchannel is ready. This happens when any subchannel becomes ready. Loop until - // we can pick three distinct subchannels. - try await XCTPoll(every: .milliseconds(10)) { - var subchannelIDs = Set() - for _ in 0 ..< 3 { - let subchannel = try XCTUnwrap(context.loadBalancer.pickSubchannel()) - subchannelIDs.insert(subchannel.id) - } - return subchannelIDs.count == 3 - } - - // Now that all are ready, load should be distributed evenly among them. - var counts = [SubchannelID: Int]() - - for round in 1 ... 10 { - for _ in 1 ... 3 { - if let subchannel = context.loadBalancer.pickSubchannel() { - counts[subchannel.id, default: 0] += 1 - } else { - XCTFail("Didn't pick subchannel from ready load balancer") - } - } - - XCTAssertEqual(counts.count, 3, "\(counts)") - XCTAssert(counts.values.allSatisfy({ $0 == round }), "\(counts)") - } - - // Close to finish the test. - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testAddressUpdatesAreHandledGracefully() async throws { - try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - // Do the first connect. - let endpoints = [Endpoint(addresses: [context.servers[0].address])] - context.roundRobin!.updateAddresses(endpoints) - - case .connectivityStateChanged(.ready): - // Now the first connection should be established. - do { - try await XCTPoll(every: .milliseconds(10)) { - context.servers[0].server.clients.count == 1 - } - } - - // First connection is okay, add a second. - do { - let endpoints = [ - Endpoint(addresses: [context.servers[0].address]), - Endpoint(addresses: [context.servers[1].address]), - ] - context.roundRobin!.updateAddresses(endpoints) - - try await XCTPoll(every: .milliseconds(10)) { - context.servers.prefix(2).allSatisfy { $0.server.clients.count == 1 } - } - } - - // Remove those two endpoints and add a third. - do { - let endpoints = [Endpoint(addresses: [context.servers[2].address])] - context.roundRobin!.updateAddresses(endpoints) - - try await XCTPoll(every: .milliseconds(10)) { - let disconnected = context.servers.prefix(2).allSatisfy { $0.server.clients.isEmpty } - let connected = context.servers.last!.server.clients.count == 1 - return disconnected && connected - } - } - - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - // Transitioning to new addresses should be graceful, i.e. a complete change shouldn't - // result in dropping away from the ready state. - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testSameAddressUpdatesAreIgnored() async throws { - try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } - context.roundRobin!.updateAddresses(endpoints) - - case .connectivityStateChanged(.ready): - // Update with the same addresses, these should be ignored. - let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } - context.roundRobin!.updateAddresses(endpoints) - - // We should still have three connections. - try await XCTPoll(every: .milliseconds(10)) { - context.servers.allSatisfy { $0.server.clients.count == 1 } - } - - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testEmptyAddressUpdatesAreIgnored() async throws { - try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let endpoints = context.servers.map { _, address in Endpoint(addresses: [address]) } - context.roundRobin!.updateAddresses(endpoints) - - case .connectivityStateChanged(.ready): - // Update with no-addresses, should be ignored so a subchannel can still be picked. - context.roundRobin!.updateAddresses([]) - - // We should still have three connections. - try await XCTPoll(every: .milliseconds(10)) { - context.servers.allSatisfy { $0.server.clients.count == 1 } - } - - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testSubchannelReceivesGoAway() async throws { - try await LoadBalancerTest.roundRobin(servers: 3, connector: .posix()) { context, event in - switch event { - case .connectivityStateChanged(.idle): - // Trigger the connect. - let endpoints = context.servers.map { Endpoint(addresses: [$0.address]) } - context.roundRobin!.updateAddresses(endpoints) - - case .connectivityStateChanged(.ready): - // Wait for all servers to become ready. - try await XCTPoll(every: .milliseconds(10)) { - context.servers.allSatisfy { $0.server.clients.count == 1 } - } - - // The above only checks whether each server has a client, the test relies on all three - // subchannels being ready, poll until we get three distinct IDs. - var ids = Set() - try await XCTPoll(every: .milliseconds(10)) { - for _ in 1 ... 3 { - if let subchannel = context.loadBalancer.pickSubchannel() { - ids.insert(subchannel.id) - } - } - return ids.count == 3 - } - - // Pick the first server and send a GOAWAY to the client. - let client = context.servers[0].server.clients[0] - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: 0, errorCode: .cancel, opaqueData: nil) - ) - - // Send a GOAWAY, this should eventually close the subchannel and trigger a name - // resolution. - client.writeAndFlush(goAway, promise: nil) - - case .requiresNameResolution: - // One subchannel should've been taken out, meaning we can only pick from the remaining two: - let id1 = try XCTUnwrap(context.loadBalancer.pickSubchannel()?.id) - let id2 = try XCTUnwrap(context.loadBalancer.pickSubchannel()?.id) - let id3 = try XCTUnwrap(context.loadBalancer.pickSubchannel()?.id) - XCTAssertNotEqual(id1, id2) - XCTAssertEqual(id1, id3) - - // End the test. - context.loadBalancer.close() - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .requiresNameResolution, - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } - - func testPickSubchannelWhenNotReady() { - let loadBalancer = RoundRobinLoadBalancer( - connector: .never, - backoff: .defaults, - defaultCompression: .none, - enabledCompression: .none - ) - - XCTAssertNil(loadBalancer.pickSubchannel()) - } - - func testPickSubchannelWhenClosed() async { - let loadBalancer = RoundRobinLoadBalancer( - connector: .never, - backoff: .defaults, - defaultCompression: .none, - enabledCompression: .none - ) - - loadBalancer.close() - await loadBalancer.run() - - XCTAssertNil(loadBalancer.pickSubchannel()) - } - - func testPickOnIdleLoadBalancerTriggersConnect() async throws { - let idle = AtomicCounter() - let ready = AtomicCounter() - - try await LoadBalancerTest.roundRobin( - servers: 1, - connector: .posix(maxIdleTime: .milliseconds(25)) // Aggressively idle the connection - ) { context, event in - switch event { - case .connectivityStateChanged(.idle): - let (_, newIdleCount) = idle.increment() - - switch newIdleCount { - case 1: - // The first idle happens when the load balancer in started, give it a set of addresses - // which it will connect to. Wait for it to be ready and then idle again. - let address = context.servers[0].address - let endpoints = [Endpoint(addresses: [address])] - context.roundRobin!.updateAddresses(endpoints) - - case 2: - // Load-balancer has the endpoints but all are idle. Picking will trigger a connect. - XCTAssertNil(context.loadBalancer.pickSubchannel()) - - case 3: - // Connection idled again. Shut it down. - context.loadBalancer.close() - - default: - XCTFail("Became idle too many times") - } - - case .connectivityStateChanged(.ready): - let (_, newReadyCount) = ready.increment() - - if newReadyCount == 2 { - XCTAssertNotNil(context.loadBalancer.pickSubchannel()) - } - - default: - () - } - } verifyEvents: { events in - let expected: [LoadBalancerEvent] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.idle), - .connectivityStateChanged(.shutdown), - ] - XCTAssertEqual(events, expected) - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift deleted file mode 100644 index b0456aee0..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/LoadBalancers/SubchannelTests.swift +++ /dev/null @@ -1,581 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOCore -import NIOHTTP2 -import NIOPosix -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class SubchannelTests: XCTestCase { - func testMakeStreamOnIdleSubchannel() async throws { - let subchannel = self.makeSubchannel( - address: .unixDomainSocket(path: "ignored"), - connector: .never - ) - - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) - } errorHandler: { error in - XCTAssertEqual(error.code, .unavailable) - } - - subchannel.shutDown() - } - - func testMakeStreamOnShutdownSubchannel() async throws { - let subchannel = self.makeSubchannel( - address: .unixDomainSocket(path: "ignored"), - connector: .never - ) - - subchannel.shutDown() - await subchannel.run() - - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) - } errorHandler: { error in - XCTAssertEqual(error.code, .unavailable) - } - } - - func testMakeStreamOnReadySubchannel() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - let subchannel = self.makeSubchannel(address: address, connector: .posix()) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await server.run { inbound, outbound in - for try await part in inbound { - switch part { - case .metadata: - try await outbound.write(.metadata([:])) - case .message(let message): - try await outbound.write(.message(message)) - } - } - try await outbound.write(.status(Status(code: .ok, message: ""), [:])) - } - } - - group.addTask { - await subchannel.run() - } - - subchannel.connect() - - for await event in subchannel.events { - switch event { - case .connectivityStateChanged(.ready): - let stream = try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) - try await stream.execute { inbound, outbound in - try await outbound.write(.metadata([:])) - try await outbound.write(.message([0, 1, 2])) - outbound.finish() - - for try await part in inbound { - switch part { - case .metadata: - () // Don't validate, contains http/2 specific metadata too. - case .message(let message): - XCTAssertEqual(message, [0, 1, 2]) - case .status(let status, _): - XCTAssertEqual(status.code, .ok) - XCTAssertEqual(status.message, "") - } - } - } - subchannel.shutDown() - - default: - () - } - } - - group.cancelAll() - } - } - - func testConnectEventuallySucceeds() async throws { - let path = "test-connect-eventually-succeeds" - let subchannel = self.makeSubchannel( - address: .unixDomainSocket(path: path), - connector: .posix(), - backoff: .fixed(at: .milliseconds(10)) - ) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { await subchannel.run() } - - var hasServer = false - var events = [Subchannel.Event]() - - for await event in subchannel.events { - events.append(event) - switch event { - case .connectivityStateChanged(.idle): - subchannel.connect() - - case .connectivityStateChanged(.transientFailure): - // Don't start more than one server. - if hasServer { continue } - hasServer = true - - group.addTask { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - _ = try await server.bind(to: .uds(path)) - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - - case .connectivityStateChanged(.ready): - subchannel.shutDown() - - case .connectivityStateChanged(.shutdown): - group.cancelAll() - - default: - () - } - } - - // First four events are known: - XCTAssertEqual( - Array(events.prefix(4)), - [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.transientFailure), - .connectivityStateChanged(.connecting), - ] - ) - - // Because there is backoff timing involved, the subchannel may flip from transient failure - // to connecting multiple times. Just check that it eventually becomes ready and is then - // shutdown. - XCTAssertEqual( - Array(events.suffix(2)), - [ - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - ) - } - } - - func testConnectIteratesThroughAddresses() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - let subchannel = self.makeSubchannel( - addresses: [ - .unixDomainSocket(path: "not-listening-1"), - .unixDomainSocket(path: "not-listening-2"), - address, - ], - connector: .posix() - ) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - - group.addTask { - await subchannel.run() - } - - for await event in subchannel.events { - switch event { - case .connectivityStateChanged(.idle): - subchannel.connect() - case .connectivityStateChanged(.ready): - subchannel.shutDown() - case .connectivityStateChanged(.shutdown): - group.cancelAll() - default: - () - } - } - } - } - - func testConnectIteratesThroughAddressesWithBackoff() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let udsPath = "test-wrap-around-addrs" - - let subchannel = self.makeSubchannel( - addresses: [ - .unixDomainSocket(path: "not-listening-1"), - .unixDomainSocket(path: "not-listening-2"), - .unixDomainSocket(path: udsPath), - ], - connector: .posix(), - backoff: .fixed(at: .zero) // Skip the backoff period - ) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - await subchannel.run() - } - - var isServerRunning = false - - for await event in subchannel.events { - switch event { - case .connectivityStateChanged(.idle): - subchannel.connect() - - case .connectivityStateChanged(.transientFailure): - // The subchannel enters the transient failure state when all addresses have been tried. - // Bind the server now so that the next attempts succeeds. - if isServerRunning { break } - isServerRunning = true - - let address = try await server.bind(to: .uds(udsPath)) - XCTAssertEqual(address, .unixDomainSocket(path: udsPath)) - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - - case .connectivityStateChanged(.ready): - subchannel.shutDown() - - case .connectivityStateChanged(.shutdown): - group.cancelAll() - - default: - () - } - } - } - } - - func testIdleTimeout() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - let subchannel = self.makeSubchannel( - address: address, - connector: .posix(maxIdleTime: .milliseconds(1)) // Aggressively idle - ) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - await subchannel.run() - } - - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - - var idleCount = 0 - var events = [Subchannel.Event]() - for await event in subchannel.events { - events.append(event) - switch event { - case .connectivityStateChanged(.idle): - idleCount += 1 - if idleCount == 1 { - subchannel.connect() - } else { - subchannel.shutDown() - } - - case .connectivityStateChanged(.shutdown): - group.cancelAll() - - default: - () - } - } - - let expected: [Subchannel.Event] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.idle), - .connectivityStateChanged(.shutdown), - ] - - XCTAssertEqual(events, expected) - } - } - - func testConnectionDropWhenIdle() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - let subchannel = self.makeSubchannel(address: address, connector: .posix()) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - await subchannel.run() - } - - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected RPC") - } - } - - var events = [Subchannel.Event]() - var idleCount = 0 - - for await event in subchannel.events { - events.append(event) - - switch event { - case .connectivityStateChanged(.idle): - idleCount += 1 - switch idleCount { - case 1: - subchannel.connect() - case 2: - subchannel.shutDown() - default: - XCTFail("Unexpected idle") - } - - case .connectivityStateChanged(.ready): - // Close the connection without a GOAWAY. - server.clients.first?.close(mode: .all, promise: nil) - - case .connectivityStateChanged(.shutdown): - group.cancelAll() - - default: - () - } - } - - let expected: [Subchannel.Event] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.idle), - .connectivityStateChanged(.shutdown), - ] - - XCTAssertEqual(events, expected) - } - } - - func testConnectionDropWithOpenStreams() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - let subchannel = self.makeSubchannel(address: address, connector: .posix()) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - await subchannel.run() - } - - group.addTask { - try await server.run(.echo) - } - - var events = [Subchannel.Event]() - var readyCount = 0 - - for await event in subchannel.events { - events.append(event) - switch event { - case .connectivityStateChanged(.idle): - subchannel.connect() - - case .connectivityStateChanged(.ready): - readyCount += 1 - // When the connection becomes ready the first time, open a stream and forcibly close the - // channel. This will result in an automatic reconnect. Close the subchannel when that - // happens. - if readyCount == 1 { - let stream = try await subchannel.makeStream(descriptor: .echoGet, options: .defaults) - try await stream.execute { inbound, outbound in - try await outbound.write(.metadata([:])) - - // Wait for the metadata to be echo'd back. - var iterator = inbound.makeAsyncIterator() - let _ = try await iterator.next() - - // Stream is definitely open. Bork the connection. - server.clients.first?.close(mode: .all, promise: nil) - - // Wait for the next message which won't arrive, client won't send a message. The - // stream should fail - let _ = try await iterator.next() - } - } else if readyCount == 2 { - subchannel.shutDown() - } - - case .connectivityStateChanged(.shutdown): - group.cancelAll() - - default: - () - } - } - - let expected: [Subchannel.Event] = [ - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.transientFailure), - .requiresNameResolution, - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - .connectivityStateChanged(.shutdown), - ] - - XCTAssertEqual(events, expected) - } - } - - func testConnectedReceivesGoAway() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - let subchannel = self.makeSubchannel(address: address, connector: .posix()) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - - group.addTask { - await subchannel.run() - } - - var events = [Subchannel.Event]() - - var idleCount = 0 - for await event in subchannel.events { - events.append(event) - - switch event { - case .connectivityStateChanged(.idle): - idleCount += 1 - if idleCount == 1 { - subchannel.connect() - } else if idleCount == 2 { - subchannel.shutDown() - } - - case .connectivityStateChanged(.ready): - // Now the subchannel is ready, send a GOAWAY from the server. - let channel = try XCTUnwrap(server.clients.first) - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: 0, errorCode: .cancel, opaqueData: nil) - ) - try await channel.writeAndFlush(goAway) - - case .connectivityStateChanged(.shutdown): - group.cancelAll() - - default: - () - } - } - - let expectedEvents: [Subchannel.Event] = [ - // Normal connect flow. - .connectivityStateChanged(.idle), - .connectivityStateChanged(.connecting), - .connectivityStateChanged(.ready), - // GOAWAY triggers name resolution and idling. - .goingAway, - .requiresNameResolution, - .connectivityStateChanged(.idle), - // The second idle triggers a close. - .connectivityStateChanged(.shutdown), - ] - - XCTAssertEqual(expectedEvents, events) - } - } - - func testCancelReadySubchannel() async throws { - let server = TestServer(eventLoopGroup: .singletonMultiThreadedEventLoopGroup) - let address = try await server.bind() - let subchannel = self.makeSubchannel(address: address, connector: .posix()) - - await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await server.run { _, _ in - XCTFail("Unexpected stream") - } - } - - group.addTask { - subchannel.connect() - await subchannel.run() - } - - for await event in subchannel.events { - switch event { - case .connectivityStateChanged(.ready): - group.cancelAll() - default: - () - } - } - } - } - - private func makeSubchannel( - addresses: [GRPCHTTP2Core.SocketAddress], - connector: any HTTP2Connector, - backoff: ConnectionBackoff? = nil - ) -> Subchannel { - return Subchannel( - endpoint: Endpoint(addresses: addresses), - id: SubchannelID(), - connector: connector, - backoff: backoff ?? .defaults, - defaultCompression: .none, - enabledCompression: .none - ) - } - - private func makeSubchannel( - address: GRPCHTTP2Core.SocketAddress, - connector: any HTTP2Connector, - backoff: ConnectionBackoff? = nil - ) -> Subchannel { - self.makeSubchannel(addresses: [address], connector: connector, backoff: backoff) - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension ConnectionBackoff { - static func fixed(at interval: Duration, jitter: Double = 0.0) -> Self { - return Self(initial: interval, max: interval, multiplier: 1.0, jitter: jitter) - } - - static var defaults: Self { - ConnectionBackoff(initial: .seconds(10), max: .seconds(120), multiplier: 1.6, jitter: 1.2) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift deleted file mode 100644 index 31b046571..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/RequestQueueTests.swift +++ /dev/null @@ -1,274 +0,0 @@ -/* - * 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 Synchronization -import XCTest - -@testable import GRPCHTTP2Core - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class RequestQueueTests: XCTestCase { - struct AnErrorToAvoidALeak: Error {} - - func testPopFirstEmpty() { - var queue = RequestQueue() - XCTAssertNil(queue.popFirst()) - } - - func testPopFirstNonEmpty() async { - _ = try? await withCheckedThrowingContinuation { continuation in - var queue = RequestQueue() - let id = QueueEntryID() - - queue.append(continuation: continuation, waitForReady: false, id: id) - guard let popped = queue.popFirst() else { - return XCTFail("Missing continuation") - } - XCTAssertNil(queue.popFirst()) - - popped.resume(throwing: AnErrorToAvoidALeak()) - } - } - - func testPopFirstMultiple() async { - await withTaskGroup(of: QueueEntryID.self) { group in - let queue = SharedRequestQueue() - let signal1 = AsyncStream.makeStream(of: Void.self) - let signal2 = AsyncStream.makeStream(of: Void.self) - - let id1 = QueueEntryID() - let id2 = QueueEntryID() - - group.addTask { - _ = try? await withCheckedThrowingContinuation { continuation in - queue.withQueue { - $0.append(continuation: continuation, waitForReady: false, id: id1) - } - - signal1.continuation.yield() - signal1.continuation.finish() - } - - return id1 - } - - group.addTask { - // Wait until instructed to append. - for await _ in signal1.stream {} - - _ = try? await withCheckedThrowingContinuation { continuation in - queue.withQueue { - $0.append(continuation: continuation, waitForReady: false, id: id2) - } - - signal2.continuation.yield() - signal2.continuation.finish() - } - - return id2 - } - - // Wait for both continuations to be enqueued. - for await _ in signal2.stream {} - - for id in [id1, id2] { - let continuation = queue.withQueue { $0.popFirst() } - continuation?.resume(throwing: AnErrorToAvoidALeak()) - let actual = await group.next() - XCTAssertEqual(id, actual) - } - } - } - - func testRemoveEntryByID() async { - _ = try? await withCheckedThrowingContinuation { continuation in - var queue = RequestQueue() - let id = QueueEntryID() - - queue.append(continuation: continuation, waitForReady: false, id: id) - guard let popped = queue.removeEntry(withID: id) else { - return XCTFail("Missing continuation") - } - XCTAssertNil(queue.removeEntry(withID: id)) - - popped.resume(throwing: AnErrorToAvoidALeak()) - } - } - - func testRemoveEntryByIDMultiple() async { - await withTaskGroup(of: QueueEntryID.self) { group in - let queue = SharedRequestQueue() - let signal1 = AsyncStream.makeStream(of: Void.self) - let signal2 = AsyncStream.makeStream(of: Void.self) - - let id1 = QueueEntryID() - let id2 = QueueEntryID() - - group.addTask { - _ = try? await withCheckedThrowingContinuation { continuation in - queue.withQueue { - $0.append(continuation: continuation, waitForReady: false, id: id1) - } - - signal1.continuation.yield() - signal1.continuation.finish() - } - - return id1 - } - - group.addTask { - // Wait until instructed to append. - for await _ in signal1.stream {} - - _ = try? await withCheckedThrowingContinuation { continuation in - queue.withQueue { - $0.append(continuation: continuation, waitForReady: false, id: id2) - } - - signal2.continuation.yield() - signal2.continuation.finish() - } - - return id2 - } - - // Wait for both continuations to be enqueued. - for await _ in signal2.stream {} - - for id in [id1, id2] { - let continuation = queue.withQueue { $0.removeEntry(withID: id) } - continuation?.resume(throwing: AnErrorToAvoidALeak()) - let actual = await group.next() - XCTAssertEqual(id, actual) - } - } - } - - func testRemoveFastFailingEntries() async throws { - let queue = SharedRequestQueue() - let enqueued = AsyncStream.makeStream(of: Void.self) - - try await withThrowingTaskGroup(of: Void.self) { group in - var waitForReadyIDs = [QueueEntryID]() - var failFastIDs = [QueueEntryID]() - - for _ in 0 ..< 50 { - waitForReadyIDs.append(QueueEntryID()) - failFastIDs.append(QueueEntryID()) - } - - for ids in [waitForReadyIDs, failFastIDs] { - let waitForReady = ids == waitForReadyIDs - for id in ids { - group.addTask { - do { - _ = try await withCheckedThrowingContinuation { continuation in - queue.withQueue { - $0.append(continuation: continuation, waitForReady: waitForReady, id: id) - } - enqueued.continuation.yield() - } - } catch is AnErrorToAvoidALeak { - () - } - } - } - } - - // Wait for all continuations to be enqueued. - var numberEnqueued = 0 - for await _ in enqueued.stream { - numberEnqueued += 1 - if numberEnqueued == (waitForReadyIDs.count + failFastIDs.count) { - enqueued.continuation.finish() - } - } - - // Remove all fast-failing continuations. - let continuations = queue.withQueue { - $0.removeFastFailingEntries() - } - - for continuation in continuations { - continuation.resume(throwing: AnErrorToAvoidALeak()) - } - - for id in failFastIDs { - queue.withQueue { - XCTAssertNil($0.removeEntry(withID: id)) - } - } - - for id in waitForReadyIDs { - let maybeContinuation = queue.withQueue { $0.removeEntry(withID: id) } - let continuation = try XCTUnwrap(maybeContinuation) - continuation.resume(throwing: AnErrorToAvoidALeak()) - } - } - } - - func testRemoveAll() async throws { - let queue = SharedRequestQueue() - let enqueued = AsyncStream.makeStream(of: Void.self) - - await withThrowingTaskGroup(of: Void.self) { group in - for _ in 0 ..< 10 { - group.addTask { - _ = try await withCheckedThrowingContinuation { continuation in - queue.withQueue { - $0.append(continuation: continuation, waitForReady: false, id: QueueEntryID()) - } - - enqueued.continuation.yield() - } - } - } - - // Wait for all continuations to be enqueued. - var numberEnqueued = 0 - for await _ in enqueued.stream { - numberEnqueued += 1 - if numberEnqueued == 10 { - enqueued.continuation.finish() - } - } - - let continuations = queue.withQueue { $0.removeAll() } - XCTAssertEqual(continuations.count, 10) - XCTAssertNil(queue.withQueue { $0.popFirst() }) - - for continuation in continuations { - continuation.resume(throwing: AnErrorToAvoidALeak()) - } - } - } - - final class SharedRequestQueue: Sendable { - private let protectedQueue: Mutex - - init() { - self.protectedQueue = Mutex(RequestQueue()) - } - - func withQueue(_ body: @Sendable (inout RequestQueue) throws -> T) rethrows -> T { - try self.protectedQueue.withLock { - try body(&$0) - } - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift deleted file mode 100644 index aecfd2a8e..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/ConnectionTest.swift +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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 DequeModule -import GRPCCore -import GRPCHTTP2Core -import NIOCore -import NIOHTTP2 -import NIOPosix - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -enum ConnectionTest { - struct Context { - var server: Server - var connection: Connection - } - - static func run( - connector: any HTTP2Connector, - server mode: Server.Mode = .regular, - handlEvents: ( - _ context: Context, - _ event: Connection.Event - ) async throws -> Void = { _, _ in }, - validateEvents: (_ context: Context, _ events: [Connection.Event]) throws -> Void - ) async throws { - let server = Server(mode: mode) - let address = try await server.bind() - - try await withThrowingTaskGroup(of: Void.self) { group in - let connection = Connection( - address: address, - http2Connector: connector, - defaultCompression: .none, - enabledCompression: .none - ) - let context = Context(server: server, connection: connection) - group.addTask { await connection.run() } - - var events: [Connection.Event] = [] - for await event in connection.events { - events.append(event) - try await handlEvents(context, event) - } - - try validateEvents(context, events) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ConnectionTest { - /// A server which only expected to accept a single connection. - final class Server { - private let eventLoop: any EventLoop - private var listener: (any Channel)? - private let client: EventLoopPromise - private let mode: Mode - - enum Mode: Sendable { - case regular - case closeOnAccept - } - - init(mode: Mode) { - self.mode = mode - self.eventLoop = .singletonMultiThreadedEventLoopGroup.next() - self.client = self.eventLoop.next().makePromise() - } - - deinit { - self.listener?.close(promise: nil) - self.client.futureResult.whenSuccess { $0.close(mode: .all, promise: nil) } - } - - var acceptedChannel: any Channel { - get throws { - try self.client.futureResult.wait() - } - } - - func bind() async throws -> GRPCHTTP2Core.SocketAddress { - precondition(self.listener == nil, "\(#function) must only be called once") - - let hasAcceptedChannel = try await self.eventLoop.submit { [loop = self.eventLoop] in - NIOLoopBoundBox(false, eventLoop: loop) - }.get() - - let bootstrap = ServerBootstrap( - group: self.eventLoop - ).childChannelInitializer { [mode = self.mode, client = self.client] channel in - precondition(!hasAcceptedChannel.value, "already accepted a channel") - hasAcceptedChannel.value = true - - switch mode { - case .closeOnAccept: - return channel.close() - - case .regular: - return channel.eventLoop.makeCompletedFuture { - let sync = channel.pipeline.syncOperations - let h2 = NIOHTTP2Handler(mode: .server) - let mux = HTTP2StreamMultiplexer(mode: .server, channel: channel) { stream in - let sync = stream.pipeline.syncOperations - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: .none, - maxPayloadSize: .max, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - - return stream.eventLoop.makeCompletedFuture { - try sync.addHandler(handler) - try sync.addHandler(EchoHandler()) - } - } - - try sync.addHandler(h2) - try sync.addHandler(mux) - try sync.addHandlers(SucceedOnSettingsAck(promise: client)) - } - } - } - - let channel = try await bootstrap.bind(host: "127.0.0.1", port: 0).get() - self.listener = channel - return .ipv4(host: "127.0.0.1", port: channel.localAddress!.port!) - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ConnectionTest { - /// Succeeds a promise when a SETTINGS frame ack has been read. - private final class SucceedOnSettingsAck: ChannelInboundHandler { - typealias InboundIn = HTTP2Frame - typealias InboundOut = HTTP2Frame - - private let promise: EventLoopPromise - - init(promise: EventLoopPromise) { - self.promise = promise - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let frame = self.unwrapInboundIn(data) - switch frame.payload { - case .settings(.ack): - self.promise.succeed(context.channel) - default: - () - } - - context.fireChannelRead(data) - } - } - - final class EchoHandler: ChannelInboundHandler { - typealias InboundIn = RPCRequestPart - typealias OutboundOut = RPCResponsePart - - private var received: Deque = [] - private var receivedEnd = false - - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - if let event = event as? ChannelEvent, event == .inputClosed { - self.receivedEnd = true - } - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - self.received.append(self.unwrapInboundIn(data)) - } - - func channelReadComplete(context: ChannelHandlerContext) { - while let part = self.received.popFirst() { - switch part { - case .metadata(let metadata): - var filtered = Metadata() - - // Remove any pseudo-headers. - for (key, value) in metadata where !key.hasPrefix(":") { - switch value { - case .string(let value): - filtered.addString(value, forKey: key) - case .binary(let value): - filtered.addBinary(value, forKey: key) - } - } - - context.write(self.wrapOutboundOut(.metadata(filtered)), promise: nil) - - case .message(let message): - context.write(self.wrapOutboundOut(.message(message)), promise: nil) - } - } - - if self.receivedEnd { - let status = Status(code: .ok, message: "") - context.write(self.wrapOutboundOut(.status(status, [:])), promise: nil) - } - - context.flush() - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift deleted file mode 100644 index 4c08cb018..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/HTTP2Connectors.swift +++ /dev/null @@ -1,161 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOCore -import NIOHTTP2 -import NIOPosix - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension HTTP2Connector where Self == ThrowingConnector { - /// A connector which throws the given error on a connect attempt. - static func throwing(_ error: RPCError) -> Self { - return ThrowingConnector(error: error) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension HTTP2Connector where Self == NeverConnector { - /// A connector which fatal errors if a connect attempt is made. - static var never: Self { - NeverConnector() - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension HTTP2Connector where Self == NIOPosixConnector { - /// A connector which uses NIOPosix to establish a connection. - static func posix( - maxIdleTime: TimeAmount? = nil, - keepaliveTime: TimeAmount? = nil, - keepaliveTimeout: TimeAmount? = nil, - keepaliveWithoutCalls: Bool = false, - dropPingAcks: Bool = false - ) -> Self { - return NIOPosixConnector( - maxIdleTime: maxIdleTime, - keepaliveTime: keepaliveTime, - keepaliveTimeout: keepaliveTimeout, - keepaliveWithoutCalls: keepaliveWithoutCalls, - dropPingAcks: dropPingAcks - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct ThrowingConnector: HTTP2Connector { - private let error: RPCError - - init(error: RPCError) { - self.error = error - } - - func establishConnection( - to address: GRPCHTTP2Core.SocketAddress - ) async throws -> HTTP2Connection { - throw self.error - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct NeverConnector: HTTP2Connector { - func establishConnection( - to address: GRPCHTTP2Core.SocketAddress - ) async throws -> HTTP2Connection { - fatalError("\(#function) called unexpectedly") - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct NIOPosixConnector: HTTP2Connector { - private let eventLoopGroup: any EventLoopGroup - private let maxIdleTime: TimeAmount? - private let keepaliveTime: TimeAmount? - private let keepaliveTimeout: TimeAmount? - private let keepaliveWithoutCalls: Bool - private let dropPingAcks: Bool - - init( - eventLoopGroup: (any EventLoopGroup)? = nil, - maxIdleTime: TimeAmount? = nil, - keepaliveTime: TimeAmount? = nil, - keepaliveTimeout: TimeAmount? = nil, - keepaliveWithoutCalls: Bool = false, - dropPingAcks: Bool = false - ) { - self.eventLoopGroup = eventLoopGroup ?? .singletonMultiThreadedEventLoopGroup - self.maxIdleTime = maxIdleTime - self.keepaliveTime = keepaliveTime - self.keepaliveTimeout = keepaliveTimeout - self.keepaliveWithoutCalls = keepaliveWithoutCalls - self.dropPingAcks = dropPingAcks - } - - func establishConnection( - to address: GRPCHTTP2Core.SocketAddress - ) async throws -> HTTP2Connection { - return try await ClientBootstrap(group: self.eventLoopGroup).connect(to: address) { channel in - channel.eventLoop.makeCompletedFuture { - let sync = channel.pipeline.syncOperations - - let connectionHandler = ClientConnectionHandler( - eventLoop: channel.eventLoop, - maxIdleTime: self.maxIdleTime, - keepaliveTime: self.keepaliveTime, - keepaliveTimeout: self.keepaliveTimeout, - keepaliveWithoutCalls: self.keepaliveWithoutCalls - ) - - let multiplexer = try sync.configureAsyncHTTP2Pipeline( - mode: .client, - streamDelegate: connectionHandler.http2StreamDelegate - ) { stream in - // Server shouldn't be opening streams. - stream.close() - } - - if self.dropPingAcks { - try sync.addHandler(PingAckDropper()) - } - - try sync.addHandler(connectionHandler) - - let asyncChannel = try NIOAsyncChannel( - wrappingChannelSynchronously: channel - ) - - return HTTP2Connection(channel: asyncChannel, multiplexer: multiplexer, isPlaintext: true) - } - } - } - - /// Drops all acks for PING frames. This is useful to help trigger the keepalive timeout. - final class PingAckDropper: ChannelInboundHandler { - typealias InboundIn = HTTP2Frame - typealias InboundOut = HTTP2Frame - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let frame = self.unwrapInboundIn(data) - switch frame.payload { - case .ping(_, ack: true): - () // drop-it - default: - context.fireChannelRead(data) - } - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift deleted file mode 100644 index 9d3973025..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/NameResolvers.swift +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 GRPCHTTP2Core - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension NameResolver { - static func `static`( - endpoints: [Endpoint], - serviceConfig: ServiceConfig? = nil - ) -> Self { - let result = NameResolutionResult( - endpoints: endpoints, - serviceConfig: serviceConfig.map { .success($0) } - ) - - return NameResolver( - names: RPCAsyncSequence(wrapping: ConstantAsyncSequence(element: result)), - updateMode: .pull - ) - } - - static func `dynamic`( - updateMode: UpdateMode - ) -> (Self, AsyncThrowingStream.Continuation) { - let (stream, continuation) = AsyncThrowingStream.makeStream(of: NameResolutionResult.self) - let resolver = NameResolver(names: RPCAsyncSequence(wrapping: stream), updateMode: updateMode) - return (resolver, continuation) - } -} - -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -struct ConstantAsyncSequence: AsyncSequence, Sendable { - private let result: Result - - init(element: Element) { - self.result = .success(element) - } - - init(error: any Error) { - self.result = .failure(error) - } - - func makeAsyncIterator() -> AsyncIterator { - AsyncIterator(result: self.result) - } - - struct AsyncIterator: AsyncIteratorProtocol { - private let result: Result - - fileprivate init(result: Result) { - self.result = result - } - - func next() async throws -> Element? { - try self.result.get() - } - } - -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift b/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift deleted file mode 100644 index 37292e4ea..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Connection/Utilities/TestServer.swift +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 NIOCore -import NIOHTTP2 -import NIOPosix -import Synchronization -import XCTest - -@testable import GRPCHTTP2Core - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class TestServer: Sendable { - private let eventLoopGroup: any EventLoopGroup - private typealias Stream = NIOAsyncChannel - private typealias Multiplexer = NIOHTTP2AsyncSequence - - private let connected: Mutex<[any Channel]> - - typealias Inbound = NIOAsyncChannelInboundStream - typealias Outbound = NIOAsyncChannelOutboundWriter - - private let server: Mutex?> - - init(eventLoopGroup: any EventLoopGroup) { - self.eventLoopGroup = eventLoopGroup - self.server = Mutex(nil) - self.connected = Mutex([]) - } - - enum Target { - case localhost - case uds(String) - } - - var clients: [any Channel] { - return self.connected.withLock { $0 } - } - - func bind(to target: Target = .localhost) async throws -> GRPCHTTP2Core.SocketAddress { - precondition(self.server.withLock { $0 } == nil) - - @Sendable - func configure(_ channel: any Channel) -> EventLoopFuture { - self.connected.withLock { - $0.append(channel) - } - - channel.closeFuture.whenSuccess { - self.connected.withLock { connected in - guard let index = connected.firstIndex(where: { $0 === channel }) else { return } - connected.remove(at: index) - } - } - - return channel.eventLoop.makeCompletedFuture { - let sync = channel.pipeline.syncOperations - let multiplexer = try sync.configureAsyncHTTP2Pipeline(mode: .server) { stream in - stream.eventLoop.makeCompletedFuture { - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: .all, - maxPayloadSize: .max, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - - try stream.pipeline.syncOperations.addHandlers(handler) - return try NIOAsyncChannel( - wrappingChannelSynchronously: stream, - configuration: .init( - inboundType: RPCRequestPart.self, - outboundType: RPCResponsePart.self - ) - ) - } - } - - return multiplexer.inbound - } - } - - let bootstrap = ServerBootstrap(group: self.eventLoopGroup) - let server: NIOAsyncChannel - let address: GRPCHTTP2Core.SocketAddress - - switch target { - case .localhost: - server = try await bootstrap.bind(host: "127.0.0.1", port: 0) { channel in - configure(channel) - } - address = .ipv4(host: "127.0.0.1", port: server.channel.localAddress!.port!) - - case .uds(let path): - server = try await bootstrap.bind(unixDomainSocketPath: path, cleanupExistingSocketFile: true) - { channel in - configure(channel) - } - address = .unixDomainSocket(path: server.channel.localAddress!.pathname!) - } - - self.server.withLock { $0 = server } - return address - } - - func run(_ handle: @Sendable @escaping (Inbound, Outbound) async throws -> Void) async throws { - guard let server = self.server.withLock({ $0 }) else { - fatalError("bind() must be called first") - } - - do { - try await server.executeThenClose { inbound, _ in - try await withThrowingTaskGroup(of: Void.self) { multiplexerGroup in - for try await multiplexer in inbound { - multiplexerGroup.addTask { - try await withThrowingTaskGroup(of: Void.self) { streamGroup in - for try await stream in multiplexer { - streamGroup.addTask { - try await stream.executeThenClose { inbound, outbound in - try await handle(inbound, outbound) - } - } - } - } - } - } - } - } - } catch is CancellationError { - () - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension TestServer { - enum RunHandler { - case echo - case never - } - - func run(_ handler: RunHandler) async throws { - switch handler { - case .echo: - try await self.run { inbound, outbound in - for try await part in inbound { - switch part { - case .metadata: - try await outbound.write(.metadata([:])) - case .message(let bytes): - try await outbound.write(.message(bytes)) - } - } - try await outbound.write(.status(Status(code: .ok, message: ""), [:])) - } - - case .never: - try await self.run { inbound, outbound in - XCTFail("Unexpected stream") - try await outbound.write(.status(Status(code: .unavailable, message: ""), [:])) - outbound.finish() - } - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift deleted file mode 100644 index 590153380..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift +++ /dev/null @@ -1,942 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import XCTest - -@testable import GRPCHTTP2Core - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCClientStreamHandlerTests: XCTestCase { - func testH2FramesAreIgnored() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1 - ) - - let channel = EmbeddedChannel(handler: handler) - - let framesToBeIgnored: [HTTP2Frame.FramePayload] = [ - .ping(.init(), ack: false), - .goAway(lastStreamID: .rootStream, errorCode: .cancel, opaqueData: nil), - // TODO: uncomment when it's possible to build a `StreamPriorityData`. - // .priority( - // HTTP2Frame.StreamPriorityData(exclusive: false, dependency: .rootStream, weight: 4) - // ), - .settings(.ack), - .pushPromise(.init(pushedStreamID: .maxID, headers: [:])), - .windowUpdate(windowSizeIncrement: 4), - .alternativeService(origin: nil, field: nil), - .origin([]), - ] - - for toBeIgnored in framesToBeIgnored { - XCTAssertNoThrow(try channel.writeInbound(toBeIgnored)) - XCTAssertNil(try channel.readInbound(as: HTTP2Frame.FramePayload.self)) - } - } - - func testServerInitialMetadataMissingHTTPStatusCodeResultsInFinishedRPC() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - let request = RPCRequestPart.metadata([:]) - XCTAssertNoThrow(try channel.writeOutbound(request)) - - // Receive server's initial metadata without :status - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue - ] - - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init(code: .unknown, message: "HTTP Status Code is missing."), - Metadata(headers: serverInitialMetadata) - ) - ) - } - - func testServerInitialMetadata1xxHTTPStatusCodeResultsInNothingRead() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - let request = RPCRequestPart.metadata([:]) - XCTAssertNoThrow(try channel.writeOutbound(request)) - - // Receive server's initial metadata with 1xx status - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "104", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - ] - - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - } - - func testServerInitialMetadataOtherNon200HTTPStatusCodeResultsInFinishedRPC() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - let request = RPCRequestPart.metadata([:]) - XCTAssertNoThrow(try channel.writeOutbound(request)) - - // Receive server's initial metadata with non-200 and non-1xx :status - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: String(HTTPResponseStatus.tooManyRequests.code), - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - ] - - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init(code: .unavailable, message: "Unexpected non-200 HTTP Status Code."), - Metadata(headers: serverInitialMetadata) - ) - ) - } - - func testServerInitialMetadataMissingContentTypeResultsInFinishedRPC() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - let request = RPCRequestPart.metadata([:]) - XCTAssertNoThrow(try channel.writeOutbound(request)) - - // Receive server's initial metadata without content-type - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200" - ] - - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init(code: .internalError, message: "Missing content-type header"), - Metadata(headers: serverInitialMetadata) - ) - ) - } - - func testNotAcceptedEncodingResultsInFinishedRPC() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .deflate, - acceptedEncodings: [.deflate], - maxPayloadSize: 1 - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - XCTAssertNoThrow( - try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) - ) - - // Make sure we have sent right metadata. - let writtenMetadata = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenMetadata.headers, - [ - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - GRPCHTTP2Keys.encoding.rawValue: "deflate", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", - ] - ) - - // Server sends initial metadata with unsupported encoding - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "gzip", - ] - - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init( - code: .internalError, - message: - "The server picked a compression algorithm ('gzip') the client does not know about." - ), - Metadata(headers: serverInitialMetadata) - ) - ) - } - - func testOverMaximumPayloadSize() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - XCTAssertNoThrow( - try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) - ) - - // Make sure we have sent right metadata. - let writtenMetadata = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenMetadata.headers, - [ - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - ) - - // Server sends initial metadata - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .metadata(Metadata(headers: serverInitialMetadata)) - ) - - // Server sends message over payload limit - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - let clientDataPayload = HTTP2Frame.FramePayload.Data( - data: .byteBuffer(buffer), - endStream: false - ) - - // Invalid payload should result in error status and stream being closed - try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) - let part = try channel.readInbound(as: RPCResponsePart.self) - XCTAssertEqual( - part, - .status(Status(code: .internalError, message: "Failed to decode message"), [:]) - ) - channel.embeddedEventLoop.run() - try channel.closeFuture.wait() - } - - func testServerSendsEOSWhenSendingMessage_ResultsInErrorStatus() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 100, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - XCTAssertNoThrow( - try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) - ) - - // Make sure we have sent right metadata. - let writtenMetadata = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenMetadata.headers, - [ - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - ) - - // Server sends initial metadata - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .metadata(Metadata(headers: serverInitialMetadata)) - ) - - // Server sends message with EOS set. - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) - XCTAssertNoThrow(try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload))) - - // Make sure we got status + trailers with the right error. - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - Status( - code: .internalError, - message: - "Server sent EOS alongside a data frame, but server is only allowed to close by sending status and trailers." - ), - [:] - ) - ) - } - - func testServerEndsStream() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Write client's initial metadata - XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - let writtenInitialMetadata = try channel.assertReadHeadersOutbound() - XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) - - // Receive server's initial metadata with end stream set - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.grpcStatus.rawValue: "0", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers( - .init( - headers: serverInitialMetadata, - endStream: true - ) - ) - ) - ) - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init(code: .ok, message: ""), - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - ] - ) - ) - - // We should throw if the server sends another message, since it's closed the stream already. - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - let serverDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeInbound(HTTP2Frame.FramePayload.data(serverDataPayload)) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } - - func testNormalFlow() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 100, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - let request = RPCRequestPart.metadata([:]) - XCTAssertNoThrow(try channel.writeOutbound(request)) - - // Make sure we have sent the corresponding frame, and that nothing has been written back. - let writtenHeaders = try channel.assertReadHeadersOutbound() - XCTAssertEqual( - writtenHeaders.headers, - [ - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - - ] - ) - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - - // Receive server's initial metadata - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - "some-custom-header": "some-custom-value", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - RPCResponsePart.metadata(Metadata(headers: serverInitialMetadata)) - ) - - // Send a message - XCTAssertNoThrow( - try channel.writeOutbound(RPCRequestPart.message(.init(repeating: 1, count: 42))) - ) - - // Assert we wrote it successfully into the channel - let writtenMessage = try channel.assertReadDataOutbound() - var expectedBuffer = ByteBuffer() - expectedBuffer.writeInteger(UInt8(0)) // not compressed - expectedBuffer.writeInteger(UInt32(42)) // message length - expectedBuffer.writeRepeatingByte(1, count: 42) // message - XCTAssertEqual(writtenMessage.data, .byteBuffer(expectedBuffer)) - - // Half-close the outbound end: this would be triggered by finishing the client's writer. - XCTAssertNoThrow(channel.close(mode: .output, promise: nil)) - - // Flush to make sure the EOS is written. - channel.flush() - - // Make sure the EOS frame was sent - let emptyEOSFrame = try channel.assertReadDataOutbound() - XCTAssertEqual(emptyEOSFrame.data, .byteBuffer(.init())) - XCTAssertTrue(emptyEOSFrame.endStream) - - // Make sure we cannot write anymore because client's closed. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(RPCRequestPart.message(.init(repeating: 1, count: 42))) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - - // This is needed to clear the EmbeddedChannel's stored error, otherwise - // it will be thrown when writing inbound. - try? channel.throwIfErrorCaught() - - // Server sends back response message - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - let serverDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer)) - XCTAssertNoThrow(try channel.writeInbound(HTTP2Frame.FramePayload.data(serverDataPayload))) - - // Make sure we read the message properly - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - RPCResponsePart.message([UInt8](repeating: 0, count: 42)) - ) - - // Server sends status to end RPC - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers( - .init(headers: [ - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.dataLoss.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: "Test data loss", - "custom-header": "custom-value", - ]) - ) - ) - ) - - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status(.init(code: .dataLoss, message: "Test data loss"), ["custom-header": "custom-value"]) - ) - } - - func testReceiveMessageSplitAcrossMultipleBuffers() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 100, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - let request = RPCRequestPart.metadata([:]) - XCTAssertNoThrow(try channel.writeOutbound(request)) - - // Make sure we have sent the corresponding frame, and that nothing has been written back. - let writtenHeaders = try channel.assertReadHeadersOutbound() - XCTAssertEqual( - writtenHeaders.headers, - [ - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - - ] - ) - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - - // Receive server's initial metadata - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - "some-custom-header": "some-custom-value", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - RPCResponsePart.metadata(Metadata(headers: serverInitialMetadata)) - ) - - // Send a message - XCTAssertNoThrow( - try channel.writeOutbound(RPCRequestPart.message(.init(repeating: 1, count: 42))) - ) - - // Assert we wrote it successfully into the channel - let writtenMessage = try channel.assertReadDataOutbound() - var expectedBuffer = ByteBuffer() - expectedBuffer.writeInteger(UInt8(0)) // not compressed - expectedBuffer.writeInteger(UInt32(42)) // message length - expectedBuffer.writeRepeatingByte(1, count: 42) // message - XCTAssertEqual(writtenMessage.data, .byteBuffer(expectedBuffer)) - - // Receive server's first message - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - - buffer.clear() - buffer.writeInteger(UInt32(30)) // message length - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - - buffer.clear() - buffer.writeRepeatingByte(0, count: 10) // first part of the message - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - - buffer.clear() - buffer.writeRepeatingByte(1, count: 10) // second part of the message - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - - buffer.clear() - buffer.writeRepeatingByte(2, count: 10) // third part of the message - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - - // Make sure we read the message properly - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - RPCResponsePart.message( - [UInt8](repeating: 0, count: 10) + [UInt8](repeating: 1, count: 10) - + [UInt8](repeating: 2, count: 10) - ) - ) - } - - func testSendMultipleMessagesInSingleBuffer() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 100, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Send client's initial metadata - let request = RPCRequestPart.metadata([:]) - XCTAssertNoThrow(try channel.writeOutbound(request)) - - // Make sure we have sent the corresponding frame, and that nothing has been written back. - let writtenHeaders = try channel.assertReadHeadersOutbound() - XCTAssertEqual( - writtenHeaders.headers, - [ - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - - ] - ) - XCTAssertNil(try channel.readInbound(as: RPCResponsePart.self)) - - // Receive server's initial metadata - let serverInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - "some-custom-header": "some-custom-value", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: serverInitialMetadata)) - ) - ) - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - RPCResponsePart.metadata(Metadata(headers: serverInitialMetadata)) - ) - - // This is where this test actually begins. We want to write two messages - // without flushing, and make sure that no messages are sent down the pipeline - // until we flush. Once we flush, both messages should be sent in the same ByteBuffer. - - // Write back first message and make sure nothing's written in the channel. - XCTAssertNoThrow(channel.write(RPCRequestPart.message([UInt8](repeating: 1, count: 4)))) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Write back second message and make sure nothing's written in the channel. - XCTAssertNoThrow(channel.write(RPCRequestPart.message([UInt8](repeating: 2, count: 4)))) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Now flush and check we *do* write the data. - channel.flush() - - let writtenMessage = try channel.assertReadDataOutbound() - - // Make sure both messages have been framed together in the ByteBuffer. - XCTAssertEqual( - writtenMessage.data, - .byteBuffer( - .init(bytes: [ - // First message - 0, // Compression disabled - 0, 0, 0, 4, // Message length - 1, 1, 1, 1, // First message data - - // Second message - 0, // Compression disabled - 0, 0, 0, 4, // Message length - 2, 2, 2, 2, // Second message data - ]) - ) - ) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - } - - func testUnexpectedStreamClose_ErrorFired() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Write client's initial metadata - XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - let writtenInitialMetadata = try channel.assertReadHeadersOutbound() - XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) - - // An error is fired down the pipeline - let thrownError = ChannelError.connectTimeout(.milliseconds(100)) - channel.pipeline.fireErrorCaught(thrownError) - - // The client receives a status explaining the stream was closed because of the thrown error. - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init( - code: .unavailable, - message: "Stream unexpectedly closed with error." - ), - [:] - ) - ) - - // We should now be closed: check we can't write anymore. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } - - func testUnexpectedStreamClose_ChannelInactive() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Write client's initial metadata - XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - let writtenInitialMetadata = try channel.assertReadHeadersOutbound() - XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) - - // Channel becomes inactive - channel.pipeline.fireChannelInactive() - - // The client receives a status explaining the stream was closed. - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init(code: .unavailable, message: "Stream unexpectedly closed."), - [:] - ) - ) - - // We should now be closed: check we can't write anymore. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } - - func testUnexpectedStreamClose_ResetStreamFrame() throws { - let handler = GRPCClientStreamHandler( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: .none, - acceptedEncodings: [], - maxPayloadSize: 1, - skipStateMachineAssertions: true - ) - - let channel = EmbeddedChannel(handler: handler) - - // Write client's initial metadata - XCTAssertNoThrow(try channel.writeOutbound(RPCRequestPart.metadata(Metadata()))) - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - let writtenInitialMetadata = try channel.assertReadHeadersOutbound() - XCTAssertEqual(writtenInitialMetadata.headers, clientInitialMetadata) - - // Receive RST_STREAM - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.rstStream(.internalError) - ) - ) - - // The client receives a status explaining RST_STREAM was sent. - XCTAssertEqual( - try channel.readInbound(as: RPCResponsePart.self), - .status( - .init( - code: .unavailable, - message: "Stream unexpectedly closed: a RST_STREAM frame was received." - ), - [:] - ) - ) - - // We should now be closed: check we can't write anymore. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(RPCRequestPart.metadata(Metadata())) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } -} - -extension EmbeddedChannel { - fileprivate func assertReadHeadersOutbound() throws -> HTTP2Frame.FramePayload.Headers { - guard - case .headers(let writtenHeaders) = try XCTUnwrap( - try self.readOutbound(as: HTTP2Frame.FramePayload.self) - ) - else { - throw TestError.assertionFailure("Expected to write headers") - } - return writtenHeaders - } - - fileprivate func assertReadDataOutbound() throws -> HTTP2Frame.FramePayload.Data { - guard - case .data(let writtenMessage) = try XCTUnwrap( - try self.readOutbound(as: HTTP2Frame.FramePayload.self) - ) - else { - throw TestError.assertionFailure("Expected to write data") - } - return writtenMessage - } -} - -private enum TestError: Error { - case assertionFailure(String) -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift b/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift deleted file mode 100644 index 8b5857008..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/HTTP2ClientTransportConfigTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import XCTest - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class HTTP2ClientTransportConfigTests: XCTestCase { - func testCompressionDefaults() { - let config = HTTP2ClientTransport.Config.Compression.defaults - XCTAssertEqual(config.algorithm, .none) - XCTAssertEqual(config.enabledAlgorithms, .none) - } - - func testConnectionDefaults() { - let config = HTTP2ClientTransport.Config.Connection.defaults - XCTAssertEqual(config.maxIdleTime, .seconds(30 * 60)) - XCTAssertNil(config.keepalive) - } - - func testBackoffDefaults() { - let config = HTTP2ClientTransport.Config.Backoff.defaults - XCTAssertEqual(config.initial, .seconds(1)) - XCTAssertEqual(config.max, .seconds(120)) - XCTAssertEqual(config.multiplier, 1.6) - XCTAssertEqual(config.jitter, 0.2) - } - - func testHTTP2Defaults() { - let config = HTTP2ClientTransport.Config.HTTP2.defaults - XCTAssertEqual(config.maxFrameSize, 16384) - XCTAssertEqual(config.targetWindowSize, 8 * 1024 * 1024) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift deleted file mode 100644 index 82d27a421..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/NameResolverRegistryTests.swift +++ /dev/null @@ -1,271 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class NameResolverRegistryTests: XCTestCase { - struct FailingResolver: NameResolverFactory { - typealias Target = StringTarget - - private let code: RPCError.Code - - init(code: RPCError.Code = .unavailable) { - self.code = code - } - - func resolver(for target: NameResolverRegistryTests.StringTarget) -> NameResolver { - let stream = AsyncThrowingStream(NameResolutionResult.self) { - $0.yield(with: .failure(RPCError(code: self.code, message: target.value))) - } - - return NameResolver(names: RPCAsyncSequence(wrapping: stream), updateMode: .pull) - } - } - - struct StringTarget: ResolvableTarget { - var value: String - - init(value: String) { - self.value = value - } - } - - func testEmptyNameResolvers() { - let resolvers = NameResolverRegistry() - XCTAssert(resolvers.isEmpty) - XCTAssertEqual(resolvers.count, 0) - } - - func testRegisterFactory() async throws { - var resolvers = NameResolverRegistry() - resolvers.registerFactory(FailingResolver(code: .unknown)) - XCTAssertEqual(resolvers.count, 1) - - do { - let resolver = resolvers.makeResolver(for: StringTarget(value: "foo")) - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - var iterator = resolver?.names.makeAsyncIterator() - _ = try await iterator?.next() - } errorHandler: { error in - XCTAssertEqual(error.code, .unknown) - } - } - - // Adding a resolver of the same type replaces it. Use the code of the thrown error to - // distinguish between the instances. - resolvers.registerFactory(FailingResolver(code: .cancelled)) - XCTAssertEqual(resolvers.count, 1) - - do { - let resolver = resolvers.makeResolver(for: StringTarget(value: "foo")) - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - var iterator = resolver?.names.makeAsyncIterator() - _ = try await iterator?.next() - } errorHandler: { error in - XCTAssertEqual(error.code, .cancelled) - } - } - } - - func testRemoveFactory() { - var resolvers = NameResolverRegistry() - resolvers.registerFactory(FailingResolver()) - XCTAssertEqual(resolvers.count, 1) - - resolvers.removeFactory(ofType: FailingResolver.self) - XCTAssertEqual(resolvers.count, 0) - - // Removing an unknown factory is a no-op. - resolvers.removeFactory(ofType: FailingResolver.self) - XCTAssertEqual(resolvers.count, 0) - } - - func testContainsFactoryOfType() { - var resolvers = NameResolverRegistry() - XCTAssertFalse(resolvers.containsFactory(ofType: FailingResolver.self)) - - resolvers.registerFactory(FailingResolver()) - XCTAssertTrue(resolvers.containsFactory(ofType: FailingResolver.self)) - } - - func testContainsFactoryCapableOfResolving() { - var resolvers = NameResolverRegistry() - XCTAssertFalse(resolvers.containsFactory(capableOfResolving: StringTarget(value: ""))) - - resolvers.registerFactory(FailingResolver()) - XCTAssertTrue(resolvers.containsFactory(capableOfResolving: StringTarget(value: ""))) - } - - func testMakeFailingResolver() async throws { - var resolvers = NameResolverRegistry() - XCTAssertNil(resolvers.makeResolver(for: StringTarget(value: ""))) - - resolvers.registerFactory(FailingResolver()) - - let resolver = try XCTUnwrap(resolvers.makeResolver(for: StringTarget(value: "foo"))) - XCTAssertEqual(resolver.updateMode, .pull) - - var iterator = resolver.names.makeAsyncIterator() - await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await iterator.next() - } errorHandler: { error in - XCTAssertEqual(error.code, .unavailable) - XCTAssertEqual(error.message, "foo") - } - } - - func testDefaultResolvers() { - let resolvers = NameResolverRegistry.defaults - XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv4.self)) - XCTAssert(resolvers.containsFactory(ofType: NameResolvers.IPv6.self)) - XCTAssert(resolvers.containsFactory(ofType: NameResolvers.UnixDomainSocket.self)) - XCTAssert(resolvers.containsFactory(ofType: NameResolvers.VirtualSocket.self)) - XCTAssertEqual(resolvers.count, 4) - } - - func testMakeResolver() { - let resolvers = NameResolverRegistry() - XCTAssertNil(resolvers.makeResolver(for: .ipv4(host: "foo"))) - } - - func testCustomResolver() async throws { - struct EmptyTarget: ResolvableTarget { - static var scheme: String { "empty" } - } - - struct CustomResolver: NameResolverFactory { - func resolver(for target: EmptyTarget) -> NameResolver { - return NameResolver( - names: RPCAsyncSequence(wrapping: AsyncThrowingStream { $0.finish() }), - updateMode: .push - ) - } - } - - var resolvers = NameResolverRegistry.defaults - resolvers.registerFactory(CustomResolver()) - let resolver = try XCTUnwrap(resolvers.makeResolver(for: EmptyTarget())) - XCTAssertEqual(resolver.updateMode, .push) - for try await _ in resolver.names { - XCTFail("Expected an empty sequence") - } - } - - func testIPv4ResolverForSingleHost() async throws { - let factory = NameResolvers.IPv4() - let resolver = factory.resolver(for: .ipv4(host: "foo", port: 1234)) - - XCTAssertEqual(resolver.updateMode, .pull) - - // The IPv4 resolver always returns the same values. - var iterator = resolver.names.makeAsyncIterator() - for _ in 0 ..< 1000 { - let result = try await XCTUnwrapAsync { try await iterator.next() } - XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.ipv4(host: "foo", port: 1234)])]) - XCTAssertNil(result.serviceConfig) - } - } - - func testIPv4ResolverForMultipleHosts() async throws { - let factory = NameResolvers.IPv4() - let resolver = factory.resolver(for: .ipv4(pairs: [("foo", 443), ("bar", 444)])) - - XCTAssertEqual(resolver.updateMode, .pull) - - // The IPv4 resolver always returns the same values. - var iterator = resolver.names.makeAsyncIterator() - for _ in 0 ..< 1000 { - let result = try await XCTUnwrapAsync { try await iterator.next() } - XCTAssertEqual( - result.endpoints, - [ - Endpoint(addresses: [.ipv4(host: "foo", port: 443)]), - Endpoint(addresses: [.ipv4(host: "bar", port: 444)]), - ] - ) - XCTAssertNil(result.serviceConfig) - } - } - - func testIPv6ResolverForSingleHost() async throws { - let factory = NameResolvers.IPv6() - let resolver = factory.resolver(for: .ipv6(host: "foo", port: 1234)) - - XCTAssertEqual(resolver.updateMode, .pull) - - // The IPv6 resolver always returns the same values. - var iterator = resolver.names.makeAsyncIterator() - for _ in 0 ..< 1000 { - let result = try await XCTUnwrapAsync { try await iterator.next() } - XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.ipv6(host: "foo", port: 1234)])]) - XCTAssertNil(result.serviceConfig) - } - } - - func testIPv6ResolverForMultipleHosts() async throws { - let factory = NameResolvers.IPv6() - let resolver = factory.resolver(for: .ipv6(pairs: [("foo", 443), ("bar", 444)])) - - XCTAssertEqual(resolver.updateMode, .pull) - - // The IPv6 resolver always returns the same values. - var iterator = resolver.names.makeAsyncIterator() - for _ in 0 ..< 1000 { - let result = try await XCTUnwrapAsync { try await iterator.next() } - XCTAssertEqual( - result.endpoints, - [ - Endpoint(addresses: [.ipv6(host: "foo", port: 443)]), - Endpoint(addresses: [.ipv6(host: "bar", port: 444)]), - ] - ) - XCTAssertNil(result.serviceConfig) - } - } - - func testUDSResolver() async throws { - let factory = NameResolvers.UnixDomainSocket() - let resolver = factory.resolver(for: .unixDomainSocket(path: "/foo")) - - XCTAssertEqual(resolver.updateMode, .pull) - - // The UDS resolver always returns the same values. - var iterator = resolver.names.makeAsyncIterator() - for _ in 0 ..< 1000 { - let result = try await XCTUnwrapAsync { try await iterator.next() } - XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.unixDomainSocket(path: "/foo")])]) - XCTAssertNil(result.serviceConfig) - } - } - - func testVSOCKResolver() async throws { - let factory = NameResolvers.VirtualSocket() - let resolver = factory.resolver(for: .vsock(contextID: .any, port: .any)) - - XCTAssertEqual(resolver.updateMode, .pull) - - // The VSOCK resolver always returns the same values. - var iterator = resolver.names.makeAsyncIterator() - for _ in 0 ..< 1000 { - let result = try await XCTUnwrapAsync { try await iterator.next() } - XCTAssertEqual(result.endpoints, [Endpoint(addresses: [.vsock(contextID: .any, port: .any)])]) - XCTAssertNil(result.serviceConfig) - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift deleted file mode 100644 index ab081a631..000000000 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/SocketAddressTests.swift +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import XCTest - -final class SocketAddressTests: XCTestCase { - func testSocketAddressUnwrapping() { - var address: SocketAddress = .ipv4(host: "foo", port: 42) - XCTAssertEqual(address.ipv4, SocketAddress.IPv4(host: "foo", port: 42)) - XCTAssertNil(address.ipv6) - XCTAssertNil(address.unixDomainSocket) - XCTAssertNil(address.virtualSocket) - - address = .ipv6(host: "bar", port: 42) - XCTAssertEqual(address.ipv6, SocketAddress.IPv6(host: "bar", port: 42)) - XCTAssertNil(address.ipv4) - XCTAssertNil(address.unixDomainSocket) - XCTAssertNil(address.virtualSocket) - - address = .unixDomainSocket(path: "baz") - XCTAssertEqual(address.unixDomainSocket, SocketAddress.UnixDomainSocket(path: "baz")) - XCTAssertNil(address.ipv4) - XCTAssertNil(address.ipv6) - XCTAssertNil(address.virtualSocket) - - address = .vsock(contextID: .any, port: .any) - XCTAssertEqual(address.virtualSocket, SocketAddress.VirtualSocket(contextID: .any, port: .any)) - XCTAssertNil(address.ipv4) - XCTAssertNil(address.ipv6) - XCTAssertNil(address.unixDomainSocket) - } - - func testSocketAddressDescription() { - var address: SocketAddress = .ipv4(host: "127.0.0.1", port: 42) - XCTAssertDescription(address, "[ipv4]127.0.0.1:42") - - address = .ipv6(host: "::1", port: 42) - XCTAssertDescription(address, "[ipv6]::1:42") - - address = .unixDomainSocket(path: "baz") - XCTAssertDescription(address, "[unix]baz") - - address = .vsock(contextID: 314, port: 159) - XCTAssertDescription(address, "[vsock]314:159") - address = .vsock(contextID: .any, port: .any) - XCTAssertDescription(address, "[vsock]-1:-1") - - } - - func testSocketAddressSubTypesDescription() { - let ipv4 = SocketAddress.IPv4(host: "127.0.0.1", port: 42) - XCTAssertDescription(ipv4, "[ipv4]127.0.0.1:42") - - let ipv6 = SocketAddress.IPv6(host: "foo", port: 42) - XCTAssertDescription(ipv6, "[ipv6]foo:42") - - let uds = SocketAddress.UnixDomainSocket(path: "baz") - XCTAssertDescription(uds, "[unix]baz") - - var vsock = SocketAddress.VirtualSocket(contextID: 314, port: 159) - XCTAssertDescription(vsock, "[vsock]314:159") - vsock.contextID = .any - vsock.port = .any - XCTAssertDescription(vsock, "[vsock]-1:-1") - } -} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift deleted file mode 100644 index 544825a84..000000000 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageDecoderTests.swift +++ /dev/null @@ -1,237 +0,0 @@ -/* - * 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 NIOCore -import NIOTestUtils -import XCTest - -@testable import GRPCHTTP2Core - -final class GRPCMessageDecoderTests: XCTestCase { - func testReadMultipleMessagesWithoutCompression() throws { - let firstMessage = { - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - buffer.writeInteger(UInt32(16)) - buffer.writeRepeatingByte(42, count: 16) - return buffer - }() - - let secondMessage = { - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - buffer.writeInteger(UInt32(8)) - buffer.writeRepeatingByte(43, count: 8) - return buffer - }() - - try ByteToMessageDecoderVerifier.verifyDecoder( - inputOutputPairs: [ - (firstMessage, [Array(repeating: UInt8(42), count: 16)]), - (secondMessage, [Array(repeating: UInt8(43), count: 8)]), - ]) { - GRPCMessageDecoder(maxPayloadSize: .max) - } - } - - func testReadMessageOverSizeLimitWithoutCompression() throws { - let deframer = GRPCMessageDecoder(maxPayloadSize: 100) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - buffer.writeInteger(UInt32(101)) - buffer.writeRepeatingByte(42, count: 101) - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: buffer) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" - ) - } - } - - func testReadMessageOverSizeLimitButWithoutActualMessageBytes() throws { - let deframer = GRPCMessageDecoder(maxPayloadSize: 100) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) - // Set the message length field to be over the maximum payload size, but - // don't write the actual message bytes. This is to ensure that the payload - // size limit is enforced _before_ the payload is actually read. - buffer.writeInteger(UInt32(101)) - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: buffer) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - "Message has exceeded the configured maximum payload size (max: 100, actual: 101)" - ) - } - } - - func testCompressedMessageWithoutConfiguringDecompressor() throws { - let deframer = GRPCMessageDecoder(maxPayloadSize: 100) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(1)) - buffer.writeInteger(UInt32(10)) - buffer.writeRepeatingByte(42, count: 10) - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: buffer) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual( - error.message, - "Received a compressed message payload, but no decompressor has been configured." - ) - } - } - - private func testReadMultipleMessagesWithCompression(method: Zlib.Method) throws { - let decompressor = Zlib.Decompressor(method: method) - let compressor = Zlib.Compressor(method: method) - var framer = GRPCMessageFramer() - defer { - decompressor.end() - compressor.end() - } - - let firstMessage = try { - framer.append(Array(repeating: 42, count: 100), promise: nil) - return try framer.next(compressor: compressor)! - }() - - let secondMessage = try { - framer.append(Array(repeating: 43, count: 110), promise: nil) - return try framer.next(compressor: compressor)! - }() - - try ByteToMessageDecoderVerifier.verifyDecoder( - inputOutputPairs: [ - (firstMessage.bytes, [Array(repeating: 42, count: 100)]), - (secondMessage.bytes, [Array(repeating: 43, count: 110)]), - ]) { - GRPCMessageDecoder(maxPayloadSize: 1000, decompressor: decompressor) - } - } - - func testReadMultipleMessagesWithDeflateCompression() throws { - try self.testReadMultipleMessagesWithCompression(method: .deflate) - } - - func testReadMultipleMessagesWithGZIPCompression() throws { - try self.testReadMultipleMessagesWithCompression(method: .gzip) - } - - func testReadCompressedMessageOverSizeLimitBeforeDecompressing() throws { - let deframer = GRPCMessageDecoder(maxPayloadSize: 1) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - let compressor = Zlib.Compressor(method: .gzip) - var framer = GRPCMessageFramer() - defer { - compressor.end() - } - - framer.append(Array(repeating: 42, count: 100), promise: nil) - let framedMessage = try framer.next(compressor: compressor)! - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: framedMessage.bytes) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual( - error.message, - """ - Message has exceeded the configured maximum payload size \ - (max: 1, actual: \(framedMessage.bytes.readableBytes - GRPCMessageDecoder.metadataLength)) - """ - ) - } - } - - private func testReadDecompressedMessageOverSizeLimit(method: Zlib.Method) throws { - let decompressor = Zlib.Decompressor(method: method) - let deframer = GRPCMessageDecoder(maxPayloadSize: 100, decompressor: decompressor) - let processor = NIOSingleStepByteToMessageProcessor(deframer) - let compressor = Zlib.Compressor(method: method) - var framer = GRPCMessageFramer() - defer { - decompressor.end() - compressor.end() - } - - framer.append(Array(repeating: 42, count: 101), promise: nil) - let framedMessage = try framer.next(compressor: compressor)! - - XCTAssertThrowsError( - ofType: RPCError.self, - try processor.process(buffer: framedMessage.bytes) { _ in - XCTFail("No message should be produced.") - } - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - XCTAssertEqual(error.message, "Message is too large to decompress.") - } - } - - func testReadDecompressedMessageOverSizeLimitWithDeflateCompression() throws { - try self.testReadDecompressedMessageOverSizeLimit(method: .deflate) - } - - func testReadDecompressedMessageOverSizeLimitWithGZIPCompression() throws { - try self.testReadDecompressedMessageOverSizeLimit(method: .gzip) - } -} - -extension GRPCMessageFramer { - mutating func next( - compressor: Zlib.Compressor? = nil - ) throws(RPCError) -> (bytes: ByteBuffer, promise: EventLoopPromise?)? { - if let (result, promise) = self.nextResult(compressor: compressor) { - switch result { - case .success(let buffer): - return (bytes: buffer, promise: promise) - case .failure(let error): - promise?.fail(error) - throw error - } - } else { - return nil - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift deleted file mode 100644 index a5ef3d8b4..000000000 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageDeframerTests.swift +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOCore -import XCTest - -final class GRPCMessageDeframerTests: XCTestCase { - // Most of the functionality is tested by the 'GRPCMessageDecoder' tests. - - func testDecodeNoBytes() { - var deframer = GRPCMessageDeframer(maxPayloadSize: .max) - XCTAssertNil(try deframer.decodeNext()) - } - - func testDecodeNotEnoughBytes() { - var deframer = GRPCMessageDeframer(maxPayloadSize: .max) - let bytes: [UInt8] = [ - 0x0, // Compression byte (not compressed) - 0x0, 0x0, 0x0, 0x1, // Length (1) - ] - deframer.append(ByteBuffer(bytes: bytes)) - XCTAssertNil(try deframer.decodeNext()) - } - - func testDecodeZeroLengthMessage() { - var deframer = GRPCMessageDeframer(maxPayloadSize: .max) - let bytes: [UInt8] = [ - 0x0, // Compression byte (not compressed) - 0x0, 0x0, 0x0, 0x0, // Length (0) - ] - deframer.append(ByteBuffer(bytes: bytes)) - XCTAssertEqual(try deframer.decodeNext(), []) - } - - func testDecodeMessage() { - var deframer = GRPCMessageDeframer(maxPayloadSize: .max) - let bytes: [UInt8] = [ - 0x0, // Compression byte (not compressed) - 0x0, 0x0, 0x0, 0x1, // Length (1) - 0xf, // Payload - ] - deframer.append(ByteBuffer(bytes: bytes)) - XCTAssertEqual(try deframer.decodeNext(), [0xf]) - } - - func testDripFeedAndDecode() { - var deframer = GRPCMessageDeframer(maxPayloadSize: .max) - let bytes: [UInt8] = [ - 0x0, // Compression byte (not compressed) - 0x0, 0x0, 0x0, 0x1, // Length (1) - ] - - for byte in bytes { - deframer.append(ByteBuffer(bytes: [byte])) - XCTAssertNil(try deframer.decodeNext()) - } - - // Drip feed the last byte. - deframer.append(ByteBuffer(bytes: [0xf])) - XCTAssertEqual(try deframer.decodeNext(), [0xf]) - } - - func testReadBytesAreDiscarded() throws { - var deframer = GRPCMessageDeframer(maxPayloadSize: .max) - - var input = ByteBuffer() - input.writeInteger(UInt8(0)) // Compression byte (not compressed) - input.writeInteger(UInt32(1024)) // Length - input.writeRepeatingByte(42, count: 1024) // Payload - - input.writeInteger(UInt8(0)) // Compression byte (not compressed) - input.writeInteger(UInt32(1024)) // Length - input.writeRepeatingByte(43, count: 512) // Payload (most of it) - - deframer.append(input) - XCTAssertEqual(deframer._readerIndex, 0) - - let message1 = try deframer.decodeNext() - XCTAssertEqual(message1, Array(repeating: 42, count: 1024)) - XCTAssertNotEqual(deframer._readerIndex, 0) - - // Append the final byte. This should discard any read bytes and set the reader index back - // to zero. - deframer.append(ByteBuffer(repeating: 43, count: 512)) - XCTAssertEqual(deframer._readerIndex, 0) - - // Read the message - let message2 = try deframer.decodeNext() - XCTAssertEqual(message2, Array(repeating: 43, count: 1024)) - XCTAssertNotEqual(deframer._readerIndex, 0) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift deleted file mode 100644 index bf9696e73..000000000 --- a/Tests/GRPCHTTP2CoreTests/GRPCMessageFramerTests.swift +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import XCTest - -@testable import GRPCHTTP2Core - -final class GRPCMessageFramerTests: XCTestCase { - func testSingleWrite() throws { - var framer = GRPCMessageFramer() - framer.append(Array(repeating: 42, count: 128), promise: nil) - - var buffer = try XCTUnwrap(framer.next()).bytes - let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compressed) - XCTAssertEqual(length, 128) - XCTAssertEqual(buffer.readSlice(length: Int(length)), ByteBuffer(repeating: 42, count: 128)) - XCTAssertEqual(buffer.readableBytes, 0) - - // No more bufers. - XCTAssertNil(try framer.next()) - } - - private func testSingleWrite(compressionMethod: Zlib.Method) throws { - let compressor = Zlib.Compressor(method: compressionMethod) - defer { - compressor.end() - } - var framer = GRPCMessageFramer() - - let message = [UInt8](repeating: 42, count: 128) - framer.append(message, promise: nil) - - var buffer = ByteBuffer() - let testCompressor = Zlib.Compressor(method: compressionMethod) - let compressedSize = try testCompressor.compress(message, into: &buffer) - let compressedMessage = buffer.readSlice(length: compressedSize) - defer { - testCompressor.end() - } - - buffer = try XCTUnwrap(framer.next(compressor: compressor)).bytes - let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertTrue(compressed) - XCTAssertEqual(length, UInt32(compressedSize)) - XCTAssertEqual(buffer.readSlice(length: Int(length)), compressedMessage) - XCTAssertEqual(buffer.readableBytes, 0) - - // No more bufers. - XCTAssertNil(try framer.next()) - } - - func testSingleWriteDeflateCompressed() throws { - try self.testSingleWrite(compressionMethod: .deflate) - } - - func testSingleWriteGZIPCompressed() throws { - try self.testSingleWrite(compressionMethod: .gzip) - } - - func testMultipleWrites() throws { - var framer = GRPCMessageFramer() - let eventLoop = EmbeddedEventLoop() - - // Create 100 messages and link a different promise with each of them. - let messagesCount = 100 - var promises = [EventLoopPromise]() - promises.reserveCapacity(messagesCount) - for _ in 0 ..< messagesCount { - let promise = eventLoop.makePromise(of: Void.self) - promises.append(promise) - framer.append(Array(repeating: 42, count: 128), promise: promise) - } - - let nextFrame = try XCTUnwrap(framer.next()) - - // Assert the messages have been framed all together in the same frame. - var buffer = nextFrame.bytes - for _ in 0 ..< messagesCount { - let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compressed) - XCTAssertEqual(length, 128) - XCTAssertEqual(buffer.readSlice(length: Int(length)), ByteBuffer(repeating: 42, count: 128)) - } - XCTAssertEqual(buffer.readableBytes, 0) - - // Assert the promise returned from the framer is the promise linked to the - // first message appended to the framer. - let returnedPromise = nextFrame.promise - XCTAssertEqual(returnedPromise?.futureResult, promises.first?.futureResult) - - // Succeed the returned promise to simulate a write into the channel - // succeeding, and assert that all other promises have been chained and are - // also succeeded as a result. - returnedPromise?.succeed() - XCTAssertEqual(promises.count, messagesCount) - for promise in promises { - try promise.futureResult.assertSuccess().wait() - } - - // No more frames. - XCTAssertNil(try framer.next()) - } -} - -extension ByteBuffer { - mutating func readMessageHeader() -> (Bool, UInt32)? { - if let (compressed, length) = self.readMultipleIntegers(as: (UInt8, UInt32).self) { - return (compressed != 0, length) - } else { - return nil - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift deleted file mode 100644 index 4ca3e608b..000000000 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ /dev/null @@ -1,2864 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import NIOHPACK -import XCTest - -@testable import GRPCHTTP2Core - -private enum TargetStateMachineState: CaseIterable { - case clientIdleServerIdle - case clientOpenServerIdle - case clientOpenServerOpen - case clientOpenServerClosed - case clientClosedServerIdle - case clientClosedServerOpen - case clientClosedServerClosed -} - -extension HPACKHeaders { - // Client - fileprivate static let clientInitialMetadata: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - fileprivate static let clientInitialMetadataWithDeflateCompression: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "https", - GRPCHTTP2Keys.te.rawValue: "trailers", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", - GRPCHTTP2Keys.encoding.rawValue: "deflate", - ] - fileprivate static let clientInitialMetadataWithGzipCompression: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.scheme.rawValue: "https", - GRPCHTTP2Keys.te.rawValue: "trailers", - GRPCHTTP2Keys.acceptEncoding.rawValue: "gzip", - GRPCHTTP2Keys.encoding.rawValue: "gzip", - ] - fileprivate static let receivedWithoutContentType: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test" - ] - fileprivate static let receivedWithInvalidContentType: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.contentType.rawValue: "invalid/invalid", - ] - fileprivate static let receivedWithInvalidPath: Self = [ - GRPCHTTP2Keys.path.rawValue: "someinvalidpath", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - ] - fileprivate static let receivedWithoutEndpoint: Self = [ - GRPCHTTP2Keys.contentType.rawValue: "application/grpc" - ] - fileprivate static let receivedWithoutTE: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - ] - fileprivate static let receivedWithInvalidTE: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "invalidte", - ] - fileprivate static let receivedWithoutMethod: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - fileprivate static let receivedWithInvalidMethod: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "GET", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - fileprivate static let receivedWithoutScheme: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - fileprivate static let receivedWithInvalidScheme: Self = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "invalidscheme", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - - // Server - fileprivate static let serverInitialMetadata: Self = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - ] - fileprivate static let serverInitialMetadataWithDeflateCompression: Self = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - ] - fileprivate static let serverInitialMetadataWithGZIPCompression: Self = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "gzip", - ] - fileprivate static let serverTrailers: Self = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.grpcStatus.rawValue: "0", - ] -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCStreamClientStateMachineTests: XCTestCase { - private func makeClientStateMachine( - targetState: TargetStateMachineState, - compressionEnabled: Bool = false - ) -> GRPCStreamStateMachine { - var stateMachine = GRPCStreamStateMachine( - configuration: .client( - .init( - methodDescriptor: .init(service: "test", method: "test"), - scheme: .http, - outboundEncoding: compressionEnabled ? .deflate : .none, - acceptedEncodings: [.deflate] - ) - ), - maxPayloadSize: 100, - skipAssertions: true - ) - - let serverMetadata: HPACKHeaders = - compressionEnabled ? .serverInitialMetadataWithDeflateCompression : .serverInitialMetadata - switch targetState { - case .clientIdleServerIdle: - break - case .clientOpenServerIdle: - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - case .clientOpenServerOpen: - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - // Open server - XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) - case .clientOpenServerClosed: - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - // Open server - XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) - // Close server - XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) - case .clientClosedServerIdle: - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - // Close client - XCTAssertNoThrow(try stateMachine.closeOutbound()) - case .clientClosedServerOpen: - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - // Open server - XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) - // Close client - XCTAssertNoThrow(try stateMachine.closeOutbound()) - case .clientClosedServerClosed: - // Open client - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - // Open server - XCTAssertNoThrow(try stateMachine.receive(headers: serverMetadata, endStream: false)) - // Close client - XCTAssertNoThrow(try stateMachine.closeOutbound()) - // Close server - XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) - } - - return stateMachine - } - - // - MARK: Send Metadata - - func testSendMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - XCTAssertNoThrow(try stateMachine.send(metadata: [])) - } - - func testSendMetadataWhenClientAlreadyOpen() throws { - for targetState in [ - TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // Try sending metadata again: should throw - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(metadata: .init()) - ) { - error in - XCTAssertEqual(error.message, "Client is already open: shouldn't be sending metadata.") - } - } - } - - func testSendMetadataWhenClientAlreadyClosed() throws { - for targetState in [ - TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, - .clientClosedServerClosed, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // Try sending metadata again: should throw - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(metadata: .init()) - ) { - error in - XCTAssertEqual(error.message, "Client is closed: can't send metadata.") - } - } - } - - // - MARK: Send Message - - func testSendMessageWhenClientIdleAndServerIdle() { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - // Try to send a message without opening (i.e. without sending initial metadata) - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Client not yet open.") - } - } - - func testSendMessageWhenClientOpen() { - for targetState in [ - TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen, .clientOpenServerClosed, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], promise: nil)) - } - } - - func testSendMessageWhenClientClosed() { - for targetState in [ - TargetStateMachineState.clientClosedServerIdle, .clientClosedServerOpen, - .clientClosedServerClosed, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // Try sending another message: it should fail - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Client is closed, cannot send a message.") - } - } - } - - // - MARK: Send Status and Trailers - - func testSendStatusAndTrailers() { - for targetState in TargetStateMachineState.allCases { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // This operation is never allowed on the client. - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send( - status: Status(code: .ok, message: ""), - metadata: .init() - ) - ) { error in - XCTAssertEqual(error.message, "Client cannot send status and trailer.") - } - } - } - - // - MARK: Receive initial metadata - - func testReceiveInitialMetadataWhenClientIdleAndServerIdle() { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(headers: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") - } - } - - func testReceiveInvalidInitialMetadataWhenServerIdle() throws { - for targetState in [ - TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // Receive metadata with unexpected non-200 status code - let action = try stateMachine.receive( - headers: [GRPCHTTP2Keys.status.rawValue: "300"], - endStream: false - ) - - XCTAssertEqual( - action, - .receivedStatusAndMetadata_clientOnly( - status: .init(code: .unknown, message: "Unexpected non-200 HTTP Status Code."), - metadata: [":status": "300"] - ) - ) - } - } - - func testReceiveInitialMetadataWhenServerIdle_ClientUnsupportedEncoding() throws { - // Create client with deflate compression enabled - var stateMachine = self.makeClientStateMachine( - targetState: .clientOpenServerIdle, - compressionEnabled: true - ) - - // Try opening server with gzip compression, which client does not support. - let action = try stateMachine.receive( - headers: .serverInitialMetadataWithGZIPCompression, - endStream: false - ) - - XCTAssertEqual( - action, - .receivedStatusAndMetadata_clientOnly( - status: Status( - code: .internalError, - message: - "The server picked a compression algorithm ('gzip') the client does not know about." - ), - metadata: [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "gzip", - ] - ) - ) - } - - func testReceiveMessage_ClientCompressionEnabled() throws { - // Enable deflate compression on client - var stateMachine = self.makeClientStateMachine( - targetState: .clientOpenServerOpen, - compressionEnabled: true - ) - - let originalMessage = [UInt8]([42, 42, 43, 43]) - - // Receiving uncompressed message should still work. - let receivedUncompressedBytes = try self.frameMessage(originalMessage, compression: .none) - XCTAssertNoThrow(try stateMachine.receive(buffer: receivedUncompressedBytes, endStream: false)) - var receivedAction = stateMachine.nextInboundMessage() - switch receivedAction { - case .noMoreMessages, .awaitMoreMessages: - XCTFail("Should have received message") - case .receiveMessage(let receivedMessaged): - XCTAssertEqual(originalMessage, receivedMessaged) - } - - // Receiving compressed message with deflate should work - let receivedDeflateCompressedBytes = try self.frameMessage( - originalMessage, - compression: .deflate - ) - XCTAssertNoThrow( - try stateMachine.receive(buffer: receivedDeflateCompressedBytes, endStream: false) - ) - receivedAction = stateMachine.nextInboundMessage() - switch receivedAction { - case .noMoreMessages, .awaitMoreMessages: - XCTFail("Should have received message") - case .receiveMessage(let receivedMessaged): - XCTAssertEqual(originalMessage, receivedMessaged) - } - - // Receiving compressed message with gzip (unsupported) should throw error - let receivedGZIPCompressedBytes = try self.frameMessage(originalMessage, compression: .gzip) - let action = try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) - XCTAssertEqual( - action, - .endRPCAndForwardErrorStatus_clientOnly( - Status(code: .internalError, message: "Failed to decode message") - ) - ) - - receivedAction = stateMachine.nextInboundMessage() - switch receivedAction { - case .awaitMoreMessages: - () - case .noMoreMessages: - XCTFail("Should be awaiting for more messages") - case .receiveMessage: - XCTFail("Should not have received message") - } - } - - func testReceiveInitialMetadataWhenServerIdle() throws { - for targetState in [ - TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // Receive metadata = open server - let action = try stateMachine.receive( - headers: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - "custom-bin": String(base64Encoding: [42, 43, 44]), - ], - endStream: false - ) - - var expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - expectedMetadata.addBinary([42, 43, 44], forKey: "custom-bin") - XCTAssertEqual(action, .receivedMetadata(expectedMetadata, nil)) - } - } - - func testReceiveInitialMetadataWhenServerOpen() throws { - for targetState in [ - TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - let action1 = try stateMachine.receive( - headers: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - "custom-bin": String(base64Encoding: [42, 43, 44]), - ], - endStream: false - ) - - let expectedStatus = Status(code: .unknown, message: "No 'grpc-status' value in trailers") - let expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - "custom-bin": .binary([42, 43, 44]), - ] - - XCTAssertEqual( - action1, - .receivedStatusAndMetadata_clientOnly(status: expectedStatus, metadata: expectedMetadata) - ) - - // Now make sure everything works well if we include grpc-status - let action2 = try stateMachine.receive( - headers: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - "custom-bin": String(base64Encoding: [42, 43, 44]), - ], - endStream: false - ) - - XCTAssertEqual( - action2, - .receivedStatusAndMetadata_clientOnly( - status: Status(code: .ok, message: ""), - metadata: expectedMetadata - ) - ) - } - } - - func testReceiveInitialMetadataWhenServerClosed() { - for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(headers: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") - } - } - } - - // - MARK: Receive end trailers - - func testReceiveEndTrailerWhenClientIdleAndServerIdle() { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - // Receive an end trailer - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(headers: .init(), endStream: true) - ) { error in - XCTAssertEqual(error.message, "Server cannot have sent metadata if the client is idle.") - } - } - - func testReceiveEndTrailerWhenClientOpenAndServerIdle() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) - - // Receive a trailers-only response - let trailersOnlyResponse: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall( - "Some, status, message" - )!, - "custom-key": "custom-value", - ] - let trailers = try stateMachine.receive(headers: trailersOnlyResponse, endStream: true) - switch trailers { - case .receivedStatusAndMetadata_clientOnly(let status, let metadata): - XCTAssertEqual(status, Status(code: .internalError, message: "Some, status, message")) - XCTAssertEqual( - metadata, - [ - ":status": "200", - "content-type": "application/grpc", - "custom-key": "custom-value", - ] - ) - case .receivedMetadata, .doNothing, .rejectRPC_serverOnly, .protocolViolation_serverOnly: - XCTFail("Expected .receivedStatusAndMetadata") - } - } - - func testReceiveEndTrailerWhenServerOpen() throws { - for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - // Receive an end trailer - let action = try stateMachine.receive( - headers: [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.ok.rawValue), - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.encoding.rawValue: "deflate", - "custom": "123", - ], - endStream: true - ) - - let expectedMetadata: Metadata = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - "custom": "123", - ] - XCTAssertEqual( - action, - .receivedStatusAndMetadata_clientOnly( - status: .init(code: .ok, message: ""), - metadata: expectedMetadata - ) - ) - } - } - - func testReceiveEndTrailerWhenClientOpenAndServerClosed() { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) - - // Receive another end trailer - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(headers: .init(), endStream: true) - ) { error in - XCTAssertEqual(error.message, "Server is closed, nothing could have been sent.") - } - } - - func testReceiveEndTrailerWhenClientClosedAndServerIdle() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientClosedServerIdle) - - // Server sends a trailers-only response - let trailersOnlyResponse: HPACKHeaders = [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: ContentType.grpc.canonicalValue, - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.internalError.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: GRPCStatusMessageMarshaller.marshall( - "Some status message" - )!, - "custom-key": "custom-value", - ] - let trailers = try stateMachine.receive(headers: trailersOnlyResponse, endStream: true) - switch trailers { - case .receivedStatusAndMetadata_clientOnly(let status, let metadata): - XCTAssertEqual(status, Status(code: .internalError, message: "Some status message")) - XCTAssertEqual( - metadata, - [ - ":status": "200", - "content-type": "application/grpc", - "custom-key": "custom-value", - ] - ) - case .receivedMetadata, .doNothing, .rejectRPC_serverOnly, .protocolViolation_serverOnly: - XCTFail("Expected .receivedStatusAndMetadata") - } - } - - func testReceiveEndTrailerWhenClientClosedAndServerClosed() { - var stateMachine = self.makeClientStateMachine(targetState: .clientClosedServerClosed) - - // Close server again (endStream = true) and assert we don't throw. - // This can happen if the previous close was caused by a grpc-status header - // and then the server sends an empty frame with EOS set. - XCTAssertEqual(try stateMachine.receive(headers: .init(), endStream: true), .doNothing) - } - - // - MARK: Receive message - - func testReceiveMessageWhenClientIdleAndServerIdle() { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual( - error.message, - "Cannot have received anything from server if client is not yet open." - ) - } - } - - func testReceiveMessageWhenServerIdle() { - for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientClosedServerIdle] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual( - error.message, - "Server cannot have sent a message before sending the initial metadata." - ) - } - } - } - - func testReceiveMessageWhenServerOpen() throws { - for targetState in [TargetStateMachineState.clientOpenServerOpen, .clientClosedServerOpen] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - XCTAssertEqual( - try stateMachine.receive(buffer: .init(), endStream: false), - .readInbound - ) - XCTAssertEqual( - try stateMachine.receive(buffer: .init(), endStream: true), - .endRPCAndForwardErrorStatus_clientOnly( - Status( - code: .internalError, - message: """ - Server sent EOS alongside a data frame, but server is only allowed \ - to close by sending status and trailers. - """ - ) - ) - ) - } - } - - func testReceiveMessageWhenServerClosed() { - for targetState in [TargetStateMachineState.clientOpenServerClosed, .clientClosedServerClosed] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Cannot have received anything from a closed server.") - } - } - } - - // - MARK: Next outbound message - - func testNextOutboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.nextOutboundFrame() - ) { error in - XCTAssertEqual(error.message, "Client is not open yet.") - } - } - - func testNextOutboundMessageWhenClientOpenAndServerOpenOrIdle() throws { - for targetState in [TargetStateMachineState.clientOpenServerIdle, .clientOpenServerOpen] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual( - try stateMachine.nextOutboundFrame(), - .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil) - ) - - // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - } - } - - func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { - var stateMachine = self.makeClientStateMachine( - targetState: .clientOpenServerIdle, - compressionEnabled: true - ) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) - - let request = try stateMachine.nextOutboundFrame() - let framedMessage = try self.frameMessage(originalMessage, compression: .deflate) - XCTAssertEqual(request, .sendFrame(frame: framedMessage, promise: nil)) - } - - func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = self.makeClientStateMachine( - targetState: .clientOpenServerOpen, - compressionEnabled: true - ) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) - - let request = try stateMachine.nextOutboundFrame() - let framedMessage = try self.frameMessage(originalMessage, compression: .deflate) - XCTAssertEqual(request, .sendFrame(frame: framedMessage, promise: nil)) - } - - func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerClosed) - - // No more messages to send - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - - // Queue a message, but assert the action is .noMoreMessages nevertheless, - // because the server is closed. - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - } - - func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerIdle) - - // Send a message and close client - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - XCTAssertNoThrow(try stateMachine.closeOutbound()) - - // Make sure that getting the next outbound message _does_ return the message - // we have enqueued. - let request = try stateMachine.nextOutboundFrame() - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual(request, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) - - // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - } - - func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) - - // Send a message and close client - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - XCTAssertNoThrow(try stateMachine.closeOutbound()) - - // Make sure that getting the next outbound message _does_ return the message - // we have enqueued. - let request = try stateMachine.nextOutboundFrame() - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual(request, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) - - // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - } - - func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) - // Send a message - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) - - // Close client - XCTAssertNoThrow(try stateMachine.closeOutbound()) - - // Even though we have enqueued a message, don't send it, because the server - // is closed. - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - } - - // - MARK: Next inbound message - - func testNextInboundMessageWhenServerIdle() { - for targetState in [ - TargetStateMachineState.clientIdleServerIdle, .clientOpenServerIdle, .clientClosedServerIdle, - ] { - var stateMachine = self.makeClientStateMachine(targetState: targetState) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - } - - func testNextInboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - - func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = self.makeClientStateMachine( - targetState: .clientOpenServerOpen, - compressionEnabled: true - ) - - let originalMessage = [UInt8]([42, 42, 43, 43]) - let receivedBytes = try self.frameMessage(originalMessage, compression: .deflate) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - - func testNextInboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) - - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testNextInboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - // Close client - XCTAssertNoThrow(try stateMachine.closeOutbound()) - - // Even though the client is closed, because it received a message while open, - // we must get the message now. - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - - func testNextInboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - // Close server - XCTAssertNoThrow(try stateMachine.receive(headers: .serverTrailers, endStream: true)) - - // Close client - XCTAssertNoThrow(try stateMachine.closeOutbound()) - - // Even though the client is closed, because it received a message while open, - // we must get the message now. - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - // - MARK: Unexpected close - - func testUnexpectedCloseWhenServerIdleOrOpen() throws { - let thrownError = RPCError(code: .deadlineExceeded, message: "Test error") - let reasonAndExpectedStatusPairs = [ - ( - GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, - Status(code: .unavailable, message: "Stream unexpectedly closed.") - ), - ( - GRPCStreamStateMachine.UnexpectedInboundCloseReason.streamReset, - Status( - code: .unavailable, - message: "Stream unexpectedly closed: a RST_STREAM frame was received." - ) - ), - ( - GRPCStreamStateMachine.UnexpectedInboundCloseReason.errorThrown(thrownError), - Status( - code: .unavailable, - message: "Stream unexpectedly closed with error." - ) - ), - ] - let states = [ - TargetStateMachineState.clientIdleServerIdle, - .clientOpenServerIdle, - .clientOpenServerOpen, - .clientClosedServerIdle, - .clientClosedServerOpen, - ] - - for state in states { - for (closeReason, expectedStatus) in reasonAndExpectedStatusPairs { - var stateMachine = self.makeClientStateMachine(targetState: state) - var action = stateMachine.unexpectedInboundClose(reason: closeReason) - - guard case .forwardStatus_clientOnly(let status) = action else { - XCTFail("Should have been `fireError` but was `\(action)` (state: \(state)).") - return - } - XCTAssertEqual(status, expectedStatus) - - // Calling unexpectedInboundClose again should return `doNothing` because - // we're already closed. - action = stateMachine.unexpectedInboundClose(reason: closeReason) - guard case .doNothing = action else { - XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") - return - } - } - } - } - - func testUnexpectedCloseWhenServerClosed() throws { - let closeReasons = [ - GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, - .streamReset, - .errorThrown(RPCError(code: .deadlineExceeded, message: "Test error")), - ] - let states = [ - TargetStateMachineState.clientOpenServerClosed, - .clientClosedServerClosed, - ] - - for state in states { - for closeReason in closeReasons { - var stateMachine = self.makeClientStateMachine(targetState: state) - var action = stateMachine.unexpectedInboundClose(reason: closeReason) - guard case .doNothing = action else { - XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") - return - } - - // Calling unexpectedInboundClose again should return `doNothing` again. - action = stateMachine.unexpectedInboundClose(reason: closeReason) - guard case .doNothing = action else { - XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") - return - } - } - } - } - - // - MARK: Common paths - - func testNormalFlow() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - // Client sends metadata - let clientInitialMetadata = try stateMachine.send(metadata: .init()) - XCTAssertEqual( - clientInitialMetadata, - [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", - ] - ) - - // Server sends initial metadata - let serverInitialHeadersAction = try stateMachine.receive( - headers: .serverInitialMetadata, - endStream: false - ) - XCTAssertEqual( - serverInitialHeadersAction, - .receivedMetadata( - [ - ":status": "200", - "content-type": "application/grpc", - ], - nil - ) - ) - - // Client sends messages - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - let message = [UInt8]([1, 2, 3, 4]) - let framedMessage = try self.frameMessage(message, compression: .none) - try stateMachine.send(message: message, promise: nil) - XCTAssertEqual( - try stateMachine.nextOutboundFrame(), - .sendFrame(frame: framedMessage, promise: nil) - ) - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - // Server sends response - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - - let firstResponseBytes = [UInt8]([5, 6, 7]) - let firstResponse = try self.frameMessage(firstResponseBytes, compression: .none) - let secondResponseBytes = [UInt8]([8, 9, 10]) - let secondResponse = try self.frameMessage(secondResponseBytes, compression: .none) - XCTAssertEqual( - try stateMachine.receive(buffer: firstResponse, endStream: false), - .readInbound - ) - XCTAssertEqual( - try stateMachine.receive(buffer: secondResponse, endStream: false), - .readInbound - ) - - // Make sure messages have arrived - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - - // Client sends end - XCTAssertNoThrow(try stateMachine.closeOutbound()) - - // Server ends - let metadataReceivedAction = try stateMachine.receive( - headers: .serverTrailers, - endStream: true - ) - let receivedMetadata = { - var m = Metadata(headers: .serverTrailers) - m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) - m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) - return m - }() - XCTAssertEqual( - metadataReceivedAction, - .receivedStatusAndMetadata_clientOnly( - status: .init(code: .ok, message: ""), - metadata: receivedMetadata - ) - ) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testClientClosesBeforeItCanOpen() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - XCTAssertNoThrow(try stateMachine.closeOutbound()) - } - - func testClientClosesBeforeServerOpens() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - // Client sends metadata - let clientInitialMetadata = try stateMachine.send(metadata: .init()) - XCTAssertEqual( - clientInitialMetadata, - [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", - ] - ) - - // Client sends messages and ends - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - let message = [UInt8]([1, 2, 3, 4]) - let framedMessage = try self.frameMessage(message, compression: .none) - XCTAssertNoThrow(try stateMachine.send(message: message, promise: nil)) - XCTAssertNoThrow(try stateMachine.closeOutbound()) - XCTAssertEqual( - try stateMachine.nextOutboundFrame(), - .sendFrame(frame: framedMessage, promise: nil) - ) - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - - // Server sends initial metadata - let serverInitialHeadersAction = try stateMachine.receive( - headers: .serverInitialMetadata, - endStream: false - ) - XCTAssertEqual( - serverInitialHeadersAction, - .receivedMetadata( - [ - ":status": "200", - "content-type": "application/grpc", - ], - nil - ) - ) - - // Server sends response - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - - let firstResponseBytes = [UInt8]([5, 6, 7]) - let firstResponse = try self.frameMessage(firstResponseBytes, compression: .none) - let secondResponseBytes = [UInt8]([8, 9, 10]) - let secondResponse = try self.frameMessage(secondResponseBytes, compression: .none) - XCTAssertEqual( - try stateMachine.receive(buffer: firstResponse, endStream: false), - .readInbound - ) - XCTAssertEqual( - try stateMachine.receive(buffer: secondResponse, endStream: false), - .readInbound - ) - - // Make sure messages have arrived - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - - // Server ends - let metadataReceivedAction = try stateMachine.receive( - headers: .serverTrailers, - endStream: true - ) - let receivedMetadata = { - var m = Metadata(headers: .serverTrailers) - m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) - m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) - return m - }() - XCTAssertEqual( - metadataReceivedAction, - .receivedStatusAndMetadata_clientOnly( - status: .init(code: .ok, message: ""), - metadata: receivedMetadata - ) - ) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testClientClosesBeforeServerResponds() throws { - var stateMachine = self.makeClientStateMachine(targetState: .clientIdleServerIdle) - - // Client sends metadata - let clientInitialMetadata = try stateMachine.send(metadata: .init()) - XCTAssertEqual( - clientInitialMetadata, - [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - GRPCHTTP2Keys.acceptEncoding.rawValue: "deflate", - ] - ) - - // Client sends messages - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - let message = [UInt8]([1, 2, 3, 4]) - let framedMessage = try self.frameMessage(message, compression: .none) - try stateMachine.send(message: message, promise: nil) - XCTAssertEqual( - try stateMachine.nextOutboundFrame(), - .sendFrame(frame: framedMessage, promise: nil) - ) - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - // Server sends initial metadata - let serverInitialHeadersAction = try stateMachine.receive( - headers: .serverInitialMetadata, - endStream: false - ) - XCTAssertEqual( - serverInitialHeadersAction, - .receivedMetadata( - [ - ":status": "200", - "content-type": "application/grpc", - ], - nil - ) - ) - - // Client closes - XCTAssertNoThrow(try stateMachine.closeOutbound()) - - // Server sends response - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - - let firstResponseBytes = [UInt8]([5, 6, 7]) - let firstResponse = try self.frameMessage(firstResponseBytes, compression: .none) - let secondResponseBytes = [UInt8]([8, 9, 10]) - let secondResponse = try self.frameMessage(secondResponseBytes, compression: .none) - XCTAssertEqual( - try stateMachine.receive(buffer: firstResponse, endStream: false), - .readInbound - ) - XCTAssertEqual( - try stateMachine.receive(buffer: secondResponse, endStream: false), - .readInbound - ) - - // Make sure messages have arrived - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(firstResponseBytes)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(secondResponseBytes)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - - // Server ends - let metadataReceivedAction = try stateMachine.receive( - headers: .serverTrailers, - endStream: true - ) - let receivedMetadata = { - var m = Metadata(headers: .serverTrailers) - m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatus.rawValue) - m.removeAllValues(forKey: GRPCHTTP2Keys.grpcStatusMessage.rawValue) - return m - }() - XCTAssertEqual( - metadataReceivedAction, - .receivedStatusAndMetadata_clientOnly( - status: .init(code: .ok, message: ""), - metadata: receivedMetadata - ) - ) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCStreamServerStateMachineTests: XCTestCase { - private func makeServerStateMachine( - targetState: TargetStateMachineState, - deflateCompressionEnabled: Bool = false - ) -> GRPCStreamStateMachine { - - var stateMachine = GRPCStreamStateMachine( - configuration: .server( - .init( - scheme: .http, - acceptedEncodings: deflateCompressionEnabled ? [.deflate] : [] - ) - ), - maxPayloadSize: 100, - skipAssertions: true - ) - - let clientMetadata: HPACKHeaders = - deflateCompressionEnabled - ? .clientInitialMetadataWithDeflateCompression : .clientInitialMetadata - switch targetState { - case .clientIdleServerIdle: - break - case .clientOpenServerIdle: - // Open client - XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) - case .clientOpenServerOpen: - // Open client - XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) - case .clientOpenServerClosed: - // Open client - XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) - // Close server - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - ) - case .clientClosedServerIdle: - // Open client - XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) - // Close client - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - case .clientClosedServerOpen: - // Open client - XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) - // Close client - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - case .clientClosedServerClosed: - // Open client - XCTAssertNoThrow(try stateMachine.receive(headers: clientMetadata, endStream: false)) - // Open server - XCTAssertNoThrow(try stateMachine.send(metadata: Metadata(headers: .serverInitialMetadata))) - // Close client - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - // Close server - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - ) - } - - return stateMachine - } - - // - MARK: Send Metadata - - func testSendMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(metadata: .init()) - ) { error in - XCTAssertEqual( - error.message, - "Client cannot be idle if server is sending initial metadata: it must have opened." - ) - } - } - - func testSendMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine( - targetState: .clientOpenServerIdle, - deflateCompressionEnabled: false - ) - XCTAssertEqual( - try stateMachine.send(metadata: .init()), - [ - ":status": "200", - "content-type": "application/grpc", - ] - ) - } - - func testSendMetadataWhenClientOpenAndServerIdle_AndCompressionEnabled() { - // Enable deflate compression on server - var stateMachine = self.makeServerStateMachine( - targetState: .clientOpenServerIdle, - deflateCompressionEnabled: true - ) - - XCTAssertEqual( - try stateMachine.send(metadata: .init()), - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-encoding": "deflate", - ] - ) - } - - func testSendMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - // Try sending metadata again: should throw - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(metadata: .init()) - ) { error in - XCTAssertEqual(error.message, "Server has already sent initial metadata.") - } - } - - func testSendMetadataWhenClientOpenAndServerClosed() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) - - // Try sending metadata again: should throw - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(metadata: .init()) - ) { error in - XCTAssertEqual(error.message, "Server cannot send metadata if closed.") - } - } - - func testSendMetadataWhenClientClosedAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) - - // We should be allowed to send initial metadata if client is closed: - // client may be finished sending request but may still be awaiting response. - XCTAssertNoThrow(try stateMachine.send(metadata: .init())) - } - - func testSendMetadataWhenClientClosedAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) - - // Try sending metadata again: should throw - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(metadata: .init()) - ) { error in - XCTAssertEqual(error.message, "Server has already sent initial metadata.") - } - } - - func testSendMetadataWhenClientClosedAndServerClosed() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) - - // Try sending metadata again: should throw - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(metadata: .init()) - ) { error in - XCTAssertEqual(error.message, "Server cannot send metadata if closed.") - } - } - - // - MARK: Send Message - - func testSendMessageWhenClientIdleAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual( - error.message, - "Server must have sent initial metadata before sending a message." - ) - } - } - - func testSendMessageWhenClientOpenAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - - // Now send a message - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual( - error.message, - "Server must have sent initial metadata before sending a message." - ) - } - } - - func testSendMessageWhenClientOpenAndServerOpen() { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - // Now send a message - XCTAssertNoThrow(try stateMachine.send(message: [], promise: nil)) - } - - func testSendMessageWhenClientOpenAndServerClosed() { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) - - // Try sending another message: it should fail - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Server can't send a message if it's closed.") - } - } - - func testSendMessageWhenClientClosedAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual( - error.message, - "Server must have sent initial metadata before sending a message." - ) - } - } - - func testSendMessageWhenClientClosedAndServerOpen() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) - - // Try sending a message: even though client is closed, we should send it - // because it may be expecting a response. - XCTAssertNoThrow(try stateMachine.send(message: [], promise: nil)) - } - - func testSendMessageWhenClientClosedAndServerClosed() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) - - // Try sending another message: it should fail - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Server can't send a message if it's closed.") - } - } - - // - MARK: Send Status and Trailers - - func testSendStatusAndTrailersWhenClientIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init() - ) - ) { error in - XCTAssertEqual(error.message, "Server can't send status if client is idle.") - } - } - - func testSendStatusAndTrailersWhenClientOpenAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - - let trailers = try stateMachine.send( - status: .init(code: .unknown, message: "RPC unknown"), - metadata: .init() - ) - - // Make sure it's a trailers-only response: it must have :status header and content-type - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "2", - "grpc-message": "RPC unknown", - ] - ) - - // Try sending another message: it should fail because server is now closed. - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Server can't send a message if it's closed.") - } - } - - func testSendStatusAndTrailersWhenClientOpenAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - let trailers = try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init() - ) - - // Make sure it's NOT a trailers-only response, because the server was - // already open (so it sent initial metadata): it shouldn't have :status or content-type headers - XCTAssertEqual(trailers, ["grpc-status": "0"]) - - // Try sending another message: it should fail because server is now closed. - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Server can't send a message if it's closed.") - } - } - - func testSendStatusAndTrailersWhenClientOpenAndServerClosed() { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init() - ) - ) { error in - XCTAssertEqual(error.message, "Server can't send anything if closed.") - } - } - - func testSendStatusAndTrailersWhenClientClosedAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) - - let trailers = try stateMachine.send( - status: .init(code: .unknown, message: "RPC unknown"), - metadata: .init() - ) - - // Make sure it's a trailers-only response: it must have :status header and content-type - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "2", - "grpc-message": "RPC unknown", - ] - ) - - // Try sending another message: it should fail because server is now closed. - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Server can't send a message if it's closed.") - } - } - - func testSendStatusAndTrailersWhenClientClosedAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) - - let trailers = try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init() - ) - - // Make sure it's NOT a trailers-only response, because the server was - // already open (so it sent initial metadata): it shouldn't have :status or content-type headers - XCTAssertEqual(trailers, ["grpc-status": "0"]) - - // Try sending another message: it should fail because server is now closed. - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send(message: [], promise: nil) - ) { error in - XCTAssertEqual(error.message, "Server can't send a message if it's closed.") - } - } - - func testSendStatusAndTrailersWhenClientClosedAndServerClosed() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: .init() - ) - ) { error in - XCTAssertEqual(error.message, "Server can't send anything if closed.") - } - } - - // - MARK: Receive metadata - - func testReceiveMetadataWhenClientIdleAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - XCTAssertEqual( - action, - .receivedMetadata( - Metadata(headers: .clientInitialMetadata), - MethodDescriptor(path: "/test/test") - ) - ) - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_WithEndStream() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: true) - XCTAssertEqual( - action, - .receivedMetadata( - Metadata(headers: .clientInitialMetadata), - MethodDescriptor(path: "/test/test") - ) - ) - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_MissingContentType() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithoutContentType, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers.count, 1) - XCTAssertEqual(trailers.firstString(forKey: .status), "415") - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidContentType() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithInvalidContentType, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual(trailers.count, 1) - XCTAssertEqual(trailers.firstString(forKey: .status), "415") - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_MissingPath() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithoutEndpoint, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": String(Status.Code.invalidArgument.rawValue), - "grpc-message": "No :path header has been set.", - ] - ) - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidPath() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithInvalidPath, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": String(Status.Code.unimplemented.rawValue), - "grpc-message": - "The given :path (someinvalidpath) does not correspond to a valid method.", - ] - ) - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_MissingTE() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithoutTE, - endStream: false - ) - - let metadata: Metadata = [ - ":path": "/test/test", - ":scheme": "http", - ":method": "POST", - "content-type": "application/grpc", - ] - let descriptor = MethodDescriptor(service: "test", method: "test") - XCTAssertEqual(action, .receivedMetadata(metadata, descriptor)) - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_MissingMethod() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithoutMethod, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "3", - "grpc-message": - ":method header is expected to be present and have a value of \"POST\".", - ] - ) - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidMethod() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithInvalidMethod, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "3", - "grpc-message": - ":method header is expected to be present and have a value of \"POST\".", - ] - ) - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_MissingScheme() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithoutScheme, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "3", - "grpc-message": ":scheme header must be present and one of \"http\" or \"https\".", - ] - ) - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_InvalidScheme() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - let action = try stateMachine.receive( - headers: .receivedWithInvalidScheme, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - XCTAssertEqual( - trailers, - [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "3", - "grpc-message": ":scheme header must be present and one of \"http\" or \"https\".", - ] - ) - } - } - - func testReceiveMetadataWhenClientIdleAndServerIdle_ServerUnsupportedEncoding() throws { - var stateMachine = self.makeServerStateMachine( - targetState: .clientIdleServerIdle, - deflateCompressionEnabled: true - ) - - // Try opening client with a compression algorithm that is not accepted - // by the server. - let action = try stateMachine.receive( - headers: .clientInitialMetadataWithGzipCompression, - endStream: false - ) - - self.assertRejectedRPC(action) { trailers in - let expected: HPACKHeaders = [ - ":status": "200", - "content-type": "application/grpc", - "grpc-status": "12", - "grpc-message": - "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding", - "grpc-accept-encoding": "deflate", - "grpc-accept-encoding": "identity", - ] - XCTAssertEqual(expected.count, trailers.count, "Expected \(expected) but got \(trailers)") - for header in trailers { - XCTAssertTrue( - expected.contains { name, value, _ in - header.name == name && header.value == header.value - } - ) - } - } - } - - func testReceiveMetadataWhenClientOpenAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - - // Try receiving initial metadata again - should be a protocol violation - let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - XCTAssertEqual(action, .protocolViolation_serverOnly) - } - - func testReceiveMetadataWhenClientOpenAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - XCTAssertEqual(action, .protocolViolation_serverOnly) - } - - func testReceiveMetadataWhenClientOpenAndServerClosed() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) - - let action = try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - XCTAssertEqual(action, .protocolViolation_serverOnly) - } - - func testReceiveMetadataWhenClientClosedAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") - } - } - - func testReceiveMetadataWhenClientClosedAndServerOpen() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") - } - } - - func testReceiveMetadataWhenClientClosedAndServerClosed() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(headers: .clientInitialMetadata, endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't have sent metadata if closed.") - } - } - - // - MARK: Receive message - - func testReceiveMessageWhenClientIdleAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Can't have received a message if client is idle.") - } - } - - func testReceiveMessageWhenClientOpenAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - - // Receive messages successfully: the second one should close client. - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - - // Verify client is now closed - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't send a message if closed.") - } - } - - func testReceiveMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - // Receive messages successfully: the second one should close client. - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: false)) - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - - // Verify client is now closed - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't send a message if closed.") - } - } - - func testReceiveMessage_ServerCompressionEnabled() throws { - // Enable deflate compression on server - var stateMachine = self.makeServerStateMachine( - targetState: .clientOpenServerOpen, - deflateCompressionEnabled: true - ) - - let originalMessage = [UInt8]([42, 42, 43, 43]) - - // Receiving uncompressed message should still work. - let receivedUncompressedBytes = try self.frameMessage(originalMessage, compression: .none) - XCTAssertNoThrow(try stateMachine.receive(buffer: receivedUncompressedBytes, endStream: false)) - var receivedAction = stateMachine.nextInboundMessage() - switch receivedAction { - case .noMoreMessages, .awaitMoreMessages: - XCTFail("Should have received message") - case .receiveMessage(let receivedMessaged): - XCTAssertEqual(originalMessage, receivedMessaged) - } - - // Receiving compressed message with deflate should work - let receivedDeflateCompressedBytes = try self.frameMessage( - originalMessage, - compression: .deflate - ) - XCTAssertNoThrow( - try stateMachine.receive(buffer: receivedDeflateCompressedBytes, endStream: false) - ) - receivedAction = stateMachine.nextInboundMessage() - switch receivedAction { - case .noMoreMessages, .awaitMoreMessages: - XCTFail("Should have received message") - case .receiveMessage(let receivedMessaged): - XCTAssertEqual(originalMessage, receivedMessaged) - } - - // Receiving compressed message with gzip (unsupported) should throw error - let receivedGZIPCompressedBytes = try self.frameMessage(originalMessage, compression: .gzip) - let action = try stateMachine.receive(buffer: receivedGZIPCompressedBytes, endStream: false) - XCTAssertEqual( - action, - .forwardErrorAndClose_serverOnly( - RPCError(code: .internalError, message: "Failed to decode message") - ) - ) - - receivedAction = stateMachine.nextInboundMessage() - switch receivedAction { - case .awaitMoreMessages: - () - case .noMoreMessages: - XCTFail("Should be awaiting for more messages") - case .receiveMessage: - XCTFail("Should not have received message") - } - } - - func testReceiveMessageWhenClientOpenAndServerClosed() { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerClosed) - - // Client is not done sending request, don't fail. - XCTAssertEqual(try stateMachine.receive(buffer: ByteBuffer(), endStream: false), .doNothing) - } - - func testReceiveMessageWhenClientClosedAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't send a message if closed.") - } - } - - func testReceiveMessageWhenClientClosedAndServerOpen() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't send a message if closed.") - } - } - - func testReceiveMessageWhenClientClosedAndServerClosed() { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerClosed) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.receive(buffer: .init(), endStream: false) - ) { error in - XCTAssertEqual(error.message, "Client can't send a message if closed.") - } - } - - // - MARK: Next outbound message - - func testNextOutboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.nextOutboundFrame() - ) { error in - XCTAssertEqual(error.message, "Server is not open yet.") - } - } - - func testNextOutboundMessageWhenClientOpenAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.nextOutboundFrame() - ) { error in - XCTAssertEqual(error.message, "Server is not open yet.") - } - } - - func testNextOutboundMessageWhenClientOpenAndServerIdle_WithCompression() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.nextOutboundFrame() - ) { error in - XCTAssertEqual(error.message, "Server is not open yet.") - } - } - - func testNextOutboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - - let response = try stateMachine.nextOutboundFrame() - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) - - // And then make sure that nothing else is returned - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - } - - func testNextOutboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = self.makeServerStateMachine( - targetState: .clientOpenServerOpen, - deflateCompressionEnabled: true - ) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - let originalMessage = [UInt8]([42, 42, 43, 43]) - XCTAssertNoThrow(try stateMachine.send(message: originalMessage, promise: nil)) - - let response = try stateMachine.nextOutboundFrame() - let framedMessage = try self.frameMessage(originalMessage, compression: .deflate) - XCTAssertEqual(response, .sendFrame(frame: framedMessage, promise: nil)) - } - - func testNextOutboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - // Send message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - ) - - let response = try stateMachine.nextOutboundFrame() - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) - - // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - } - - func testNextOutboundMessageWhenClientClosedAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerIdle) - - XCTAssertThrowsError( - ofType: GRPCStreamStateMachine.InvalidState.self, - try stateMachine.nextOutboundFrame() - ) { error in - XCTAssertEqual(error.message, "Server is not open yet.") - } - } - - func testNextOutboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - // Send a message - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - - // Send another message - XCTAssertNoThrow(try stateMachine.send(message: [43, 43], promise: nil)) - - // Make sure that getting the next outbound message _does_ return the message - // we have enqueued. - let response = try stateMachine.nextOutboundFrame() - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - // End of first message - beginning of second - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 43, 43, // original message - ] - XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) - - // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - } - - func testNextOutboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientClosedServerOpen) - - // Send a message and close server - XCTAssertNoThrow(try stateMachine.send(message: [42, 42], promise: nil)) - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - ) - - // We have enqueued a message, make sure we return it even though server is closed, - // because we haven't yet drained all of the pending messages. - let response = try stateMachine.nextOutboundFrame() - let expectedBytes: [UInt8] = [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ] - XCTAssertEqual(response, .sendFrame(frame: ByteBuffer(bytes: expectedBytes), promise: nil)) - - // And then make sure that nothing else is returned anymore - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - } - - // - MARK: Next inbound message - - func testNextInboundMessageWhenClientIdleAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - - func testNextInboundMessageWhenClientOpenAndServerIdle() { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - - func testNextInboundMessageWhenClientOpenAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - - func testNextInboundMessageWhenClientOpenAndServerOpen_WithCompression() throws { - var stateMachine = self.makeServerStateMachine( - targetState: .clientOpenServerOpen, - deflateCompressionEnabled: true - ) - - let originalMessage = [UInt8]([42, 42, 43, 43]) - let receivedBytes = try self.frameMessage(originalMessage, compression: .deflate) - - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(originalMessage)) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - } - - func testNextInboundMessageWhenClientOpenAndServerClosed() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - // Close server - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - ) - - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testNextInboundMessageWhenClientClosedAndServerIdle() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerIdle) - let action = try stateMachine.receive( - buffer: ByteBuffer(repeating: 0, count: 5), - endStream: true - ) - XCTAssertEqual(action, .readInbound) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testNextInboundMessageWhenClientClosedAndServerOpen() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - - // Even though the client is closed, because the server received a message - // while it was still open, we must get the message now. - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage([42, 42])) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testNextInboundMessageWhenClientClosedAndServerClosed() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientOpenServerOpen) - - let receivedBytes = ByteBuffer(bytes: [ - 0, // compression flag: unset - 0, 0, 0, 2, // message length: 2 bytes - 42, 42, // original message - ]) - XCTAssertEqual( - try stateMachine.receive(buffer: receivedBytes, endStream: false), - .readInbound - ) - - // Close server - XCTAssertNoThrow( - try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - ) - - // Close client - XCTAssertNoThrow(try stateMachine.receive(buffer: .init(), endStream: true)) - - // The server is closed, the message should be dropped. - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - // - MARK: Unexpected close - - func testUnexpectedCloseWhenClientIdleOrOpen() throws { - let reasonAndExpectedErrorPairs = [ - ( - GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, - RPCError(code: .unavailable, message: "Stream unexpectedly closed.") - ), - ( - GRPCStreamStateMachine.UnexpectedInboundCloseReason.streamReset, - RPCError( - code: .unavailable, - message: "Stream unexpectedly closed: a RST_STREAM frame was received." - ) - ), - ( - GRPCStreamStateMachine.UnexpectedInboundCloseReason.errorThrown( - RPCError(code: .deadlineExceeded, message: "Test error") - ), - RPCError(code: .deadlineExceeded, message: "Test error") - ), - ] - let states = [ - TargetStateMachineState.clientIdleServerIdle, - .clientOpenServerIdle, - .clientOpenServerOpen, - .clientOpenServerClosed, - ] - - for state in states { - for (closeReason, expectedError) in reasonAndExpectedErrorPairs { - var stateMachine = self.makeServerStateMachine(targetState: state) - var action = stateMachine.unexpectedInboundClose(reason: closeReason) - guard case .fireError_serverOnly(let error) = action else { - XCTFail("Should have been `fireError` but was `\(action)` (state: \(state)).") - return - } - XCTAssertEqual(error as? RPCError, expectedError) - - // Calling unexpectedInboundClose again should return `doNothing` because - // we're already closed. - action = stateMachine.unexpectedInboundClose(reason: closeReason) - guard case .doNothing = action else { - XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") - return - } - } - } - } - - func testUnexpectedCloseWhenClientClosed() throws { - let closeReasons = [ - GRPCStreamStateMachine.UnexpectedInboundCloseReason.channelInactive, - .streamReset, - .errorThrown(RPCError(code: .deadlineExceeded, message: "Test error")), - ] - let states = [ - TargetStateMachineState.clientClosedServerIdle, - .clientClosedServerOpen, - .clientClosedServerClosed, - ] - - for state in states { - for closeReason in closeReasons { - var stateMachine = self.makeServerStateMachine(targetState: state) - var action = stateMachine.unexpectedInboundClose(reason: closeReason) - guard case .doNothing = action else { - XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") - return - } - - // Calling unexpectedInboundClose again should return `doNothing` again. - action = stateMachine.unexpectedInboundClose(reason: closeReason) - guard case .doNothing = action else { - XCTFail("Should have been `doNothing` but was `\(action)` (state: \(state)).") - return - } - } - } - } - - // - MARK: Common paths - - func testNormalFlow() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - // Client sends metadata - let receiveMetadataAction = try stateMachine.receive( - headers: .clientInitialMetadata, - endStream: false - ) - XCTAssertEqual( - receiveMetadataAction, - .receivedMetadata( - Metadata(headers: .clientInitialMetadata), - MethodDescriptor(path: "/test/test") - ) - ) - - // Server sends initial metadata - let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) - XCTAssertEqual( - sentInitialHeaders, - [ - ":status": "200", - "content-type": "application/grpc", - "custom": "value", - ] - ) - - // Client sends messages - let deframedMessage = [UInt8]([1, 2, 3, 4]) - let completeMessage = try self.frameMessage(deframedMessage, compression: .none) - // Split message into two parts to make sure the stitching together of the frames works well - let firstMessage = completeMessage.getSlice(at: 0, length: 4)! - let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - - XCTAssertEqual( - try stateMachine.receive(buffer: firstMessage, endStream: false), - .readInbound - ) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - XCTAssertEqual( - try stateMachine.receive(buffer: secondMessage, endStream: false), - .readInbound - ) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) - - // Server sends response - let eventLoop = EmbeddedEventLoop() - let firstPromise = eventLoop.makePromise(of: Void.self) - let secondPromise = eventLoop.makePromise(of: Void.self) - - let firstResponse = [UInt8]([5, 6, 7]) - let secondResponse = [UInt8]([8, 9, 10]) - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - - try stateMachine.send(message: firstResponse, promise: firstPromise) - try stateMachine.send(message: secondResponse, promise: secondPromise) - - // Make sure messages are outbound - let framedMessages = try self.frameMessages( - [firstResponse, secondResponse], - compression: .none - ) - - guard - case .sendFrame(let nextOutboundByteBuffer, let nextOutboundPromise) = - try stateMachine.nextOutboundFrame() - else { - XCTFail("Should have received .sendMessage") - return - } - XCTAssertEqual(nextOutboundByteBuffer, framedMessages) - XCTAssertTrue(firstPromise.futureResult === nextOutboundPromise?.futureResult) - - // Make sure that the promises associated with each sent message are chained - // together: when succeeding the one returned by the state machine on - // `nextOutboundMessage()`, the others should also be succeeded. - firstPromise.succeed() - try secondPromise.futureResult.assertSuccess().wait() - - // Client sends end - XCTAssertEqual( - try stateMachine.receive(buffer: ByteBuffer(), endStream: true), - .readInbound - ) - - // Server ends - let response = try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - XCTAssertEqual(response, ["grpc-status": "0"]) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testClientClosesBeforeServerOpens() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - // Client sends metadata - let receiveMetadataAction = try stateMachine.receive( - headers: .clientInitialMetadata, - endStream: false - ) - XCTAssertEqual( - receiveMetadataAction, - .receivedMetadata( - Metadata(headers: .clientInitialMetadata), - MethodDescriptor(path: "/test/test") - ) - ) - - // Client sends messages - let deframedMessage = [UInt8]([1, 2, 3, 4]) - let completeMessage = try self.frameMessage(deframedMessage, compression: .none) - // Split message into two parts to make sure the stitching together of the frames works well - let firstMessage = completeMessage.getSlice(at: 0, length: 4)! - let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - - XCTAssertEqual( - try stateMachine.receive(buffer: firstMessage, endStream: false), - .readInbound - ) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - XCTAssertEqual( - try stateMachine.receive(buffer: secondMessage, endStream: false), - .readInbound - ) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) - - // Client sends end - XCTAssertEqual( - try stateMachine.receive(buffer: ByteBuffer(), endStream: true), - .readInbound - ) - - // Server sends initial metadata - let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) - XCTAssertEqual( - sentInitialHeaders, - [ - "custom": "value", - ":status": "200", - "content-type": "application/grpc", - ] - ) - - // Server sends response - let firstResponse = [UInt8]([5, 6, 7]) - let secondResponse = [UInt8]([8, 9, 10]) - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse, promise: nil) - try stateMachine.send(message: secondResponse, promise: nil) - - // Make sure messages are outbound - let framedMessages = try self.frameMessages( - [firstResponse, secondResponse], - compression: .none - ) - XCTAssertEqual( - try stateMachine.nextOutboundFrame(), - .sendFrame(frame: framedMessages, promise: nil) - ) - - // Server ends - let response = try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - XCTAssertEqual(response, ["grpc-status": "0"]) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } - - func testClientClosesBeforeServerResponds() throws { - var stateMachine = self.makeServerStateMachine(targetState: .clientIdleServerIdle) - - // Client sends metadata - let receiveMetadataAction = try stateMachine.receive( - headers: .clientInitialMetadata, - endStream: false - ) - XCTAssertEqual( - receiveMetadataAction, - .receivedMetadata( - Metadata(headers: .clientInitialMetadata), - MethodDescriptor(path: "/test/test") - ) - ) - - // Client sends messages - let deframedMessage = [UInt8]([1, 2, 3, 4]) - let completeMessage = try self.frameMessage(deframedMessage, compression: .none) - // Split message into two parts to make sure the stitching together of the frames works well - let firstMessage = completeMessage.getSlice(at: 0, length: 4)! - let secondMessage = completeMessage.getSlice(at: 4, length: completeMessage.readableBytes - 4)! - - XCTAssertEqual( - try stateMachine.receive(buffer: firstMessage, endStream: false), - .readInbound - ) - XCTAssertEqual(stateMachine.nextInboundMessage(), .awaitMoreMessages) - XCTAssertEqual( - try stateMachine.receive(buffer: secondMessage, endStream: false), - .readInbound - ) - XCTAssertEqual(stateMachine.nextInboundMessage(), .receiveMessage(deframedMessage)) - - // Server sends initial metadata - let sentInitialHeaders = try stateMachine.send(metadata: Metadata(headers: ["custom": "value"])) - XCTAssertEqual( - sentInitialHeaders, - [ - "custom": "value", - ":status": "200", - "content-type": "application/grpc", - ] - ) - - // Client sends end - XCTAssertEqual( - try stateMachine.receive(buffer: ByteBuffer(), endStream: true), - .readInbound - ) - - // Server sends response - let firstResponse = [UInt8]([5, 6, 7]) - let secondResponse = [UInt8]([8, 9, 10]) - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .awaitMoreMessages) - try stateMachine.send(message: firstResponse, promise: nil) - try stateMachine.send(message: secondResponse, promise: nil) - - // Make sure messages are outbound - let framedMessages = try self.frameMessages( - [firstResponse, secondResponse], - compression: .none - ) - XCTAssertEqual( - try stateMachine.nextOutboundFrame(), - .sendFrame(frame: framedMessages, promise: nil) - ) - - // Server ends - let response = try stateMachine.send( - status: .init(code: .ok, message: ""), - metadata: [] - ) - XCTAssertEqual(response, ["grpc-status": "0"]) - - XCTAssertEqual(try stateMachine.nextOutboundFrame(), .noMoreMessages) - XCTAssertEqual(stateMachine.nextInboundMessage(), .noMoreMessages) - } -} - -extension XCTestCase { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) - func assertRejectedRPC( - _ action: GRPCStreamStateMachine.OnMetadataReceived, - expression: (HPACKHeaders) throws -> Void - ) rethrows { - guard case .rejectRPC_serverOnly(let trailers) = action else { - XCTFail("RPC should have been rejected.") - return - } - try expression(trailers) - } - - func frameMessage(_ message: [UInt8], compression: CompressionAlgorithm) throws -> ByteBuffer { - try frameMessages([message], compression: compression) - } - - func frameMessages(_ messages: [[UInt8]], compression: CompressionAlgorithm) throws -> ByteBuffer - { - var framer = GRPCMessageFramer() - let compressor: Zlib.Compressor? = { - switch compression { - case .deflate: - return Zlib.Compressor(method: .deflate) - case .gzip: - return Zlib.Compressor(method: .gzip) - default: - return nil - } - }() - defer { compressor?.end() } - for message in messages { - framer.append(message, promise: nil) - } - return try XCTUnwrap(framer.next(compressor: compressor)).bytes - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCStreamStateMachine.OnNextOutboundFrame { - public static func == ( - lhs: GRPCStreamStateMachine.OnNextOutboundFrame, - rhs: GRPCStreamStateMachine.OnNextOutboundFrame - ) -> Bool { - switch (lhs, rhs) { - case (.noMoreMessages, .noMoreMessages): - return true - case (.awaitMoreMessages, .awaitMoreMessages): - return true - case (.sendFrame(let lhsMessage, _), .sendFrame(let rhsMessage, _)): - // Note that we're not comparing the EventLoopPromises here, as they're - // not Equatable. This is fine though, since we only use this in tests. - return lhsMessage == rhsMessage - default: - return false - } - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension GRPCStreamStateMachine.OnNextOutboundFrame: Equatable {} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift deleted file mode 100644 index ac659fad9..000000000 --- a/Tests/GRPCHTTP2CoreTests/Internal/GRPCStatusMessageMarshallerTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 XCTest - -@testable import GRPCHTTP2Core - -class GRPCStatusMessageMarshallerTests: XCTestCase { - func testASCIIMarshallingAndUnmarshalling() { - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("Hello, World!"), "Hello, World!") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("Hello, World!"), "Hello, World!") - } - - func testPercentMarshallingAndUnmarshalling() { - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("%"), "%25") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%25"), "%") - - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("25%"), "25%25") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("25%25"), "25%") - } - - func testUnicodeMarshalling() { - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("🚀"), "%F0%9F%9A%80") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%F0%9F%9A%80"), "🚀") - - let message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" - let marshalled = - "%09%0Atest with whitespace%0D%0Aand Unicode BMP %E2%98%BA and non-BMP %F0%9F%98%88%09%0A" - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall(message), marshalled) - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall(marshalled), message) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift deleted file mode 100644 index 180bb030f..000000000 --- a/Tests/GRPCHTTP2CoreTests/Internal/ProcessUniqueIDTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 XCTest - -@testable import GRPCHTTP2Core - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class ProcessUniqueIDTests: XCTestCase { - func testProcessUniqueIDIsUnique() { - var ids: Set = [] - for _ in 1 ... 100 { - let (inserted, _) = ids.insert(ProcessUniqueID()) - XCTAssertTrue(inserted) - } - - XCTAssertEqual(ids.count, 100) - } - - func testProcessUniqueIDDescription() { - let id = ProcessUniqueID() - let description = String(describing: id) - // We can't verify the exact description as we don't know what value to expect, we only - // know that it'll be an integer. - XCTAssertNotNil(UInt64(description)) - } - - func testSubchannelIDDescription() { - let id = SubchannelID() - let description = String(describing: id) - XCTAssert(description.hasPrefix("subchan_")) - } - - func testLoadBalancerIDDescription() { - let id = LoadBalancerID() - let description = String(describing: id) - XCTAssert(description.hasPrefix("lb_")) - } - - func testQueueEntryDescription() { - let id = QueueEntryID() - let description = String(describing: id) - XCTAssert(description.hasPrefix("q_entry_")) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift b/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift deleted file mode 100644 index eb920976c..000000000 --- a/Tests/GRPCHTTP2CoreTests/Internal/TimerTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOEmbedded -import Synchronization -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal final class TimerTests: XCTestCase { - func testScheduleOneOffTimer() { - let loop = EmbeddedEventLoop() - defer { try! loop.close() } - - let value = Atomic(0) - var timer = Timer(delay: .seconds(1), repeat: false) - timer.schedule(on: loop) { - let (old, _) = value.add(1, ordering: .releasing) - XCTAssertEqual(old, 0) - } - - loop.advanceTime(by: .milliseconds(999)) - XCTAssertEqual(value.load(ordering: .acquiring), 0) - loop.advanceTime(by: .milliseconds(1)) - XCTAssertEqual(value.load(ordering: .acquiring), 1) - - // Run again to make sure the task wasn't repeated. - loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(value.load(ordering: .acquiring), 1) - } - - func testCancelOneOffTimer() { - let loop = EmbeddedEventLoop() - defer { try! loop.close() } - - var timer = Timer(delay: .seconds(1), repeat: false) - timer.schedule(on: loop) { - XCTFail("Timer wasn't cancelled") - } - - loop.advanceTime(by: .milliseconds(999)) - timer.cancel() - loop.advanceTime(by: .milliseconds(1)) - } - - func testScheduleRepeatedTimer() throws { - let loop = EmbeddedEventLoop() - defer { try! loop.close() } - - let counter = AtomicCounter() - var timer = Timer(delay: .seconds(1), repeat: true) - timer.schedule(on: loop) { - counter.increment() - } - - loop.advanceTime(by: .milliseconds(999)) - XCTAssertEqual(counter.value, 0) - loop.advanceTime(by: .milliseconds(1)) - XCTAssertEqual(counter.value, 1) - - loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(counter.value, 2) - loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(counter.value, 3) - - timer.cancel() - loop.advanceTime(by: .seconds(1)) - XCTAssertEqual(counter.value, 3) - } - - func testCancelRepeatedTimer() { - let loop = EmbeddedEventLoop() - defer { try! loop.close() } - - var timer = Timer(delay: .seconds(1), repeat: true) - timer.schedule(on: loop) { - XCTFail("Timer wasn't cancelled") - } - - loop.advanceTime(by: .milliseconds(999)) - timer.cancel() - loop.advanceTime(by: .milliseconds(1)) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift b/Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift deleted file mode 100644 index 7f1fbf9f5..000000000 --- a/Tests/GRPCHTTP2CoreTests/OneOrManyQueueTests.swift +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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 XCTest - -@testable import GRPCHTTP2Core - -internal final class OneOrManyQueueTests: XCTestCase { - func testIsEmpty() { - XCTAssertTrue(OneOrManyQueue().isEmpty) - } - - func testIsEmptyManyBacked() { - XCTAssertTrue(OneOrManyQueue.manyBacked.isEmpty) - } - - func testCount() { - var queue = OneOrManyQueue() - XCTAssertEqual(queue.count, 0) - queue.append(1) - XCTAssertEqual(queue.count, 1) - } - - func testCountManyBacked() { - var manyBacked = OneOrManyQueue.manyBacked - XCTAssertEqual(manyBacked.count, 0) - for i in 1 ... 100 { - manyBacked.append(1) - XCTAssertEqual(manyBacked.count, i) - } - } - - func testAppendAndPop() { - var queue = OneOrManyQueue() - XCTAssertNil(queue.pop()) - - queue.append(1) - XCTAssertEqual(queue.count, 1) - XCTAssertEqual(queue.pop(), 1) - - XCTAssertNil(queue.pop()) - XCTAssertEqual(queue.count, 0) - XCTAssertTrue(queue.isEmpty) - } - - func testAppendAndPopManyBacked() { - var manyBacked = OneOrManyQueue.manyBacked - XCTAssertNil(manyBacked.pop()) - - manyBacked.append(1) - XCTAssertEqual(manyBacked.count, 1) - manyBacked.append(2) - XCTAssertEqual(manyBacked.count, 2) - - XCTAssertEqual(manyBacked.pop(), 1) - XCTAssertEqual(manyBacked.count, 1) - - XCTAssertEqual(manyBacked.pop(), 2) - XCTAssertEqual(manyBacked.count, 0) - - XCTAssertNil(manyBacked.pop()) - XCTAssertTrue(manyBacked.isEmpty) - } - - func testIndexes() { - var queue = OneOrManyQueue() - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, 0) - - // Non-empty. - queue.append(1) - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, 1) - } - - func testIndexesManyBacked() { - var queue = OneOrManyQueue.manyBacked - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, 0) - - for i in 1 ... 100 { - queue.append(i) - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, i) - } - } - - func testIndexAfter() { - var queue = OneOrManyQueue() - XCTAssertEqual(queue.startIndex, queue.endIndex) - XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) - - queue.append(1) - XCTAssertNotEqual(queue.startIndex, queue.endIndex) - XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) - } - - func testSubscript() throws { - var queue = OneOrManyQueue() - queue.append(42) - let index = try XCTUnwrap(queue.firstIndex(of: 42)) - XCTAssertEqual(queue[index], 42) - } - - func testSubscriptManyBacked() throws { - var queue = OneOrManyQueue.manyBacked - for i in 0 ... 100 { - queue.append(i) - } - - for i in 0 ... 100 { - XCTAssertEqual(queue[i], i) - } - } -} - -extension OneOrManyQueue where Element == Int { - static var manyBacked: Self { - var queue = OneOrManyQueue() - // Append and pop to move to the 'many' backing. - queue.append(1) - queue.append(2) - XCTAssertEqual(queue.pop(), 1) - XCTAssertEqual(queue.pop(), 2) - return queue - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift deleted file mode 100644 index bcee1f3e2..000000000 --- a/Tests/GRPCHTTP2CoreTests/Server/Compression/ZlibTests.swift +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 NIOCore -import XCTest - -@testable import GRPCHTTP2Core - -final class ZlibTests: XCTestCase { - private let text = """ - Here's to the crazy ones. The misfits. The rebels. The troublemakers. The round pegs in the - square holes. The ones who see things differently. They're not fond of rules. And they have - no respect for the status quo. You can quote them, disagree with them, glorify or vilify them. - About the only thing you can't do is ignore them. Because they change things. They push the - human race forward. And while some may see them as the crazy ones, we see genius. Because - the people who are crazy enough to think they can change the world, are the ones who do. - """ - - private func compress(_ input: [UInt8], method: Zlib.Method) throws -> ByteBuffer { - let compressor = Zlib.Compressor(method: method) - defer { compressor.end() } - - var buffer = ByteBuffer() - try compressor.compress(input, into: &buffer) - return buffer - } - - private func decompress( - _ input: ByteBuffer, - method: Zlib.Method, - limit: Int = .max - ) throws -> [UInt8] { - let decompressor = Zlib.Decompressor(method: method) - defer { decompressor.end() } - - var input = input - return try decompressor.decompress(&input, limit: limit) - } - - func testRoundTripUsingDeflate() throws { - let original = Array(self.text.utf8) - let compressed = try self.compress(original, method: .deflate) - let decompressed = try self.decompress(compressed, method: .deflate) - XCTAssertEqual(original, decompressed) - } - - func testRoundTripUsingGzip() throws { - let original = Array(self.text.utf8) - let compressed = try self.compress(original, method: .gzip) - let decompressed = try self.decompress(compressed, method: .gzip) - XCTAssertEqual(original, decompressed) - } - - func testRepeatedCompresses() throws { - let original = Array(self.text.utf8) - let compressor = Zlib.Compressor(method: .deflate) - defer { compressor.end() } - - var compressed = ByteBuffer() - let bytesWritten = try compressor.compress(original, into: &compressed) - XCTAssertEqual(compressed.readableBytes, bytesWritten) - - for _ in 0 ..< 10 { - var buffer = ByteBuffer() - try compressor.compress(original, into: &buffer) - XCTAssertEqual(compressed, buffer) - } - } - - func testRepeatedDecompresses() throws { - let original = Array(self.text.utf8) - let decompressor = Zlib.Decompressor(method: .deflate) - defer { decompressor.end() } - - let compressed = try self.compress(original, method: .deflate) - var input = compressed - let decompressed = try decompressor.decompress(&input, limit: .max) - - for _ in 0 ..< 10 { - var input = compressed - let buffer = try decompressor.decompress(&input, limit: .max) - XCTAssertEqual(buffer, decompressed) - } - } - - func testDecompressGrowsOutputBuffer() throws { - // This compresses down to 17 bytes with deflate. The decompressor sets the output buffer to - // be double the size of the input buffer and will grow it if necessary. This test exercises - // that path. - let original = [UInt8](repeating: 0, count: 1024) - let compressed = try self.compress(original, method: .deflate) - let decompressed = try self.decompress(compressed, method: .deflate) - XCTAssertEqual(decompressed, original) - } - - func testDecompressRespectsLimit() throws { - let compressed = try self.compress(Array(self.text.utf8), method: .deflate) - let limit = compressed.readableBytes - 1 - XCTAssertThrowsError( - ofType: RPCError.self, - try self.decompress(compressed, method: .deflate, limit: limit) - ) { error in - XCTAssertEqual(error.code, .resourceExhausted) - } - } - - func testCompressAppendsToBuffer() throws { - let compressor = Zlib.Compressor(method: .deflate) - defer { compressor.end() } - - var buffer = ByteBuffer() - try compressor.compress(Array(repeating: 0, count: 1024), into: &buffer) - - // Should be some readable bytes. - let byteCount1 = buffer.readableBytes - XCTAssertGreaterThan(byteCount1, 0) - - try compressor.compress(Array(repeating: 1, count: 1024), into: &buffer) - - // Should be some readable bytes. - let byteCount2 = buffer.readableBytes - XCTAssertGreaterThan(byteCount2, byteCount1) - - let slice1 = buffer.readSlice(length: byteCount1)! - let decompressed1 = try self.decompress(slice1, method: .deflate) - XCTAssertEqual(decompressed1, Array(repeating: 0, count: 1024)) - - let decompressed2 = try self.decompress(buffer, method: .deflate) - XCTAssertEqual(decompressed2, Array(repeating: 1, count: 1024)) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift deleted file mode 100644 index 77a067a5b..000000000 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandler+StateMachineTests.swift +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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 NIOCore -import NIOHTTP2 -import XCTest - -@testable import GRPCHTTP2Core - -final class ServerConnectionManagementHandlerStateMachineTests: XCTestCase { - private func makeStateMachine( - allowKeepaliveWithoutCalls: Bool = false, - minPingReceiveIntervalWithoutCalls: TimeAmount = .minutes(5), - goAwayPingData: HTTP2PingData = HTTP2PingData(withInteger: 42) - ) -> ServerConnectionManagementHandler.StateMachine { - return .init( - allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls, - minPingReceiveIntervalWithoutCalls: minPingReceiveIntervalWithoutCalls, - goAwayPingData: goAwayPingData - ) - } - - func testCloseAllStreamsWhenActive() { - var state = self.makeStateMachine() - state.streamOpened(1) - XCTAssertEqual(state.streamClosed(1), .startIdleTimer) - } - - func testCloseSomeStreamsWhenActive() { - var state = self.makeStateMachine() - state.streamOpened(1) - state.streamOpened(2) - XCTAssertEqual(state.streamClosed(2), .none) - } - - func testOpenAndCloseStreamWhenClosed() { - var state = self.makeStateMachine() - state.markClosed() - state.streamOpened(1) - XCTAssertEqual(state.streamClosed(1), .none) - } - - func testGracefulShutdownWhenNoOpenStreams() { - let pingData = HTTP2PingData(withInteger: 42) - var state = self.makeStateMachine(goAwayPingData: pingData) - XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) - } - - func testGracefulShutdownWhenClosing() { - let pingData = HTTP2PingData(withInteger: 42) - var state = self.makeStateMachine(goAwayPingData: pingData) - XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) - XCTAssertEqual(state.startGracefulShutdown(), .none) - } - - func testGracefulShutdownWhenClosed() { - let pingData = HTTP2PingData(withInteger: 42) - var state = self.makeStateMachine(goAwayPingData: pingData) - state.markClosed() - XCTAssertEqual(state.startGracefulShutdown(), .none) - } - - func testReceiveAckForGoAwayPingWhenStreamsOpenedBeforeShutdownOnly() { - let pingData = HTTP2PingData(withInteger: 42) - var state = self.makeStateMachine(goAwayPingData: pingData) - state.streamOpened(1) - XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) - XCTAssertEqual( - state.receivedPingAck(data: pingData), - .sendGoAway(lastStreamID: 1, close: false) - ) - } - - func testReceiveAckForGoAwayPingWhenStreamsOpenedBeforeAck() { - let pingData = HTTP2PingData(withInteger: 42) - var state = self.makeStateMachine(goAwayPingData: pingData) - XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) - state.streamOpened(1) - XCTAssertEqual( - state.receivedPingAck(data: pingData), - .sendGoAway(lastStreamID: 1, close: false) - ) - } - - func testReceiveAckForGoAwayPingWhenNoOpenStreams() { - let pingData = HTTP2PingData(withInteger: 42) - var state = self.makeStateMachine(goAwayPingData: pingData) - XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) - XCTAssertEqual( - state.receivedPingAck(data: pingData), - .sendGoAway(lastStreamID: .rootStream, close: true) - ) - } - - func testReceiveAckNotForGoAwayPing() { - let pingData = HTTP2PingData(withInteger: 42) - var state = self.makeStateMachine(goAwayPingData: pingData) - XCTAssertEqual(state.startGracefulShutdown(), .sendGoAwayAndPing(pingData)) - - let otherPingData = HTTP2PingData(withInteger: 0) - XCTAssertEqual(state.receivedPingAck(data: otherPingData), .none) - } - - func testReceivePingAckWhenActive() { - var state = self.makeStateMachine() - XCTAssertEqual(state.receivedPingAck(data: HTTP2PingData()), .none) - } - - func testReceivePingAckWhenClosed() { - var state = self.makeStateMachine() - state.markClosed() - XCTAssertEqual(state.receivedPingAck(data: HTTP2PingData()), .none) - } - - func testGracefulShutdownFlow() { - var state = self.makeStateMachine() - // Open a few streams. - state.streamOpened(1) - state.streamOpened(2) - - switch state.startGracefulShutdown() { - case .sendGoAwayAndPing(let pingData): - // Open another stream and then receive the ping ack. - state.streamOpened(3) - XCTAssertEqual( - state.receivedPingAck(data: pingData), - .sendGoAway(lastStreamID: 3, close: false) - ) - case .none: - XCTFail("Expected '.sendGoAwayAndPing'") - } - - // Both GOAWAY frames have been sent. Start closing streams. - XCTAssertEqual(state.streamClosed(1), .none) - XCTAssertEqual(state.streamClosed(2), .none) - XCTAssertEqual(state.streamClosed(3), .close) - } - - func testGracefulShutdownWhenNoOpenStreamsBeforeSecondGoAway() { - var state = self.makeStateMachine() - // Open a stream. - state.streamOpened(1) - - switch state.startGracefulShutdown() { - case .sendGoAwayAndPing(let pingData): - // Close the stream. This shouldn't lead to a close. - XCTAssertEqual(state.streamClosed(1), .none) - // Only on receiving the ack do we send a GOAWAY and close. - XCTAssertEqual( - state.receivedPingAck(data: pingData), - .sendGoAway(lastStreamID: 1, close: true) - ) - case .none: - XCTFail("Expected '.sendGoAwayAndPing'") - } - } - - func testPingStrikeUsingMinReceiveInterval( - state: inout ServerConnectionManagementHandler.StateMachine, - interval: TimeAmount, - expectedID id: HTTP2StreamID - ) { - var time = NIODeadline.now() - let data = HTTP2PingData() - - // The first ping is never a strike. - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) - - // Advance time by just less than the interval and get two strikes. - time = time + interval - .nanoseconds(1) - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) - - // Advance time so that we're at one interval since the last valid ping. This isn't a - // strike (but doesn't reset strikes) and updates the last valid ping time. - time = time + .nanoseconds(1) - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) - - // Now get a third and final strike. - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .enhanceYourCalmThenClose(id)) - } - - func testPingStrikesWhenKeepaliveIsNotPermittedWithoutCalls() { - let initialState = self.makeStateMachine( - allowKeepaliveWithoutCalls: false, - minPingReceiveIntervalWithoutCalls: .minutes(5) - ) - - var state = initialState - state.streamOpened(1) - self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 1) - - state = initialState - self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .hours(2), expectedID: 0) - } - - func testPingStrikesWhenKeepaliveIsPermittedWithoutCalls() { - var state = self.makeStateMachine( - allowKeepaliveWithoutCalls: true, - minPingReceiveIntervalWithoutCalls: .minutes(5) - ) - - self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 0) - } - - func testResetPingStrikeState() { - var state = self.makeStateMachine( - allowKeepaliveWithoutCalls: true, - minPingReceiveIntervalWithoutCalls: .minutes(5) - ) - - var time = NIODeadline.now() - let data = HTTP2PingData() - - // The first ping is never a strike. - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) - - // Advance time by less than the interval and get two strikes. - time = time + .minutes(1) - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) - XCTAssertEqual(state.receivedPing(atTime: time, data: data), .sendAck) - - // Reset the ping strike state and test ping strikes as normal. - state.resetKeepaliveState() - self.testPingStrikeUsingMinReceiveInterval(state: &state, interval: .minutes(5), expectedID: 0) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift deleted file mode 100644 index b4bfb0066..000000000 --- a/Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift +++ /dev/null @@ -1,410 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import NIOHTTP2 -import XCTest - -@testable import GRPCHTTP2Core - -final class ServerConnectionManagementHandlerTests: XCTestCase { - func testIdleTimeoutOnNewConnection() throws { - let connection = try Connection(maxIdleTime: .minutes(1)) - try connection.activate() - // Hit the max idle time. - connection.advanceTime(by: .minutes(1)) - - // Follow the graceful shutdown flow. - try self.testGracefulShutdown(connection: connection, lastStreamID: 0) - - // Closed because no streams were open. - try connection.waitUntilClosed() - } - - func testIdleTimerIsCancelledWhenStreamIsOpened() throws { - let connection = try Connection(maxIdleTime: .minutes(1)) - try connection.activate() - - // Open a stream to cancel the idle timer and run through the max idle time. - connection.streamOpened(1) - connection.advanceTime(by: .minutes(1)) - - // No GOAWAY frame means the timer was cancelled. - XCTAssertNil(try connection.readFrame()) - } - - func testIdleTimerStartsWhenAllStreamsAreClosed() throws { - let connection = try Connection(maxIdleTime: .minutes(1)) - try connection.activate() - - // Open a stream to cancel the idle timer and run through the max idle time. - connection.streamOpened(1) - connection.advanceTime(by: .minutes(1)) - XCTAssertNil(try connection.readFrame()) - - // Close the stream to start the timer again. - connection.streamClosed(1) - connection.advanceTime(by: .minutes(1)) - - // Follow the graceful shutdown flow. - try self.testGracefulShutdown(connection: connection, lastStreamID: 1) - - // Closed because no streams were open. - try connection.waitUntilClosed() - } - - func testMaxAge() throws { - let connection = try Connection(maxAge: .minutes(1)) - try connection.activate() - - // Open some streams. - connection.streamOpened(1) - connection.streamOpened(3) - - // Run to the max age and follow the graceful shutdown flow. - connection.advanceTime(by: .minutes(1)) - try self.testGracefulShutdown(connection: connection, lastStreamID: 3) - - // Close the streams. - connection.streamClosed(1) - connection.streamClosed(3) - - // Connection will be closed now. - try connection.waitUntilClosed() - } - - func testGracefulShutdownRatchetsDownStreamID() throws { - // This test uses the idle timeout to trigger graceful shutdown. The mechanism is the same - // regardless of how it's triggered. - let connection = try Connection(maxIdleTime: .minutes(1)) - try connection.activate() - - // Trigger the shutdown, but open a stream during shutdown. - connection.advanceTime(by: .minutes(1)) - try self.testGracefulShutdown( - connection: connection, - lastStreamID: 1, - streamToOpenBeforePingAck: 1 - ) - - // Close the stream to trigger closing the connection. - connection.streamClosed(1) - try connection.waitUntilClosed() - } - - func testGracefulShutdownGracePeriod() throws { - // This test uses the idle timeout to trigger graceful shutdown. The mechanism is the same - // regardless of how it's triggered. - let connection = try Connection( - maxIdleTime: .minutes(1), - maxGraceTime: .seconds(5) - ) - try connection.activate() - - // Trigger the shutdown, but open a stream during shutdown. - connection.advanceTime(by: .minutes(1)) - try self.testGracefulShutdown( - connection: connection, - lastStreamID: 1, - streamToOpenBeforePingAck: 1 - ) - - // Wait out the grace period without closing the stream. - connection.advanceTime(by: .seconds(5)) - try connection.waitUntilClosed() - } - - func testKeepaliveOnNewConnection() throws { - let connection = try Connection( - keepaliveTime: .minutes(5), - keepaliveTimeout: .seconds(5) - ) - try connection.activate() - - // Wait for the keep alive timer to fire which should cause the server to send a keep - // alive PING. - connection.advanceTime(by: .minutes(5)) - let frame1 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame1.streamID, .rootStream) - try XCTAssertPing(frame1.payload) { data, ack in - XCTAssertFalse(ack) - // Data is opaque, send it back. - try connection.ping(data: data, ack: true) - } - - // Run past the timeout, nothing should happen. - connection.advanceTime(by: .seconds(5)) - XCTAssertNil(try connection.readFrame()) - } - - func testKeepaliveStartsAfterReadLoop() throws { - let connection = try Connection( - keepaliveTime: .minutes(5), - keepaliveTimeout: .seconds(5) - ) - try connection.activate() - - // Write a frame into the channel _without_ calling channel read complete. This will cancel - // the keep alive timer. - let settings = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - connection.channel.pipeline.fireChannelRead(NIOAny(settings)) - - // Run out the keep alive timer, it shouldn't fire. - connection.advanceTime(by: .minutes(5)) - XCTAssertNil(try connection.readFrame()) - - // Fire channel read complete to start the keep alive timer again. - connection.channel.pipeline.fireChannelReadComplete() - - // Now expire the keep alive timer again, we should read out a PING frame. - connection.advanceTime(by: .minutes(5)) - let frame1 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame1.streamID, .rootStream) - XCTAssertPing(frame1.payload) { data, ack in - XCTAssertFalse(ack) - } - } - - func testKeepaliveOnNewConnectionWithoutResponse() throws { - let connection = try Connection( - keepaliveTime: .minutes(5), - keepaliveTimeout: .seconds(5) - ) - try connection.activate() - - // Wait for the keep alive timer to fire which should cause the server to send a keep - // alive PING. - connection.advanceTime(by: .minutes(5)) - let frame1 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame1.streamID, .rootStream) - XCTAssertPing(frame1.payload) { data, ack in - XCTAssertFalse(ack) - } - - // We didn't ack the PING, the connection should shutdown after the timeout. - connection.advanceTime(by: .seconds(5)) - try self.testGracefulShutdown(connection: connection, lastStreamID: 0) - - // Connection is closed now. - try connection.waitUntilClosed() - } - - func testClientKeepalivePolicing() throws { - let connection = try Connection( - allowKeepaliveWithoutCalls: true, - minPingIntervalWithoutCalls: .minutes(1) - ) - try connection.activate() - - // The first ping is valid, the second and third are strikes. - for _ in 1 ... 3 { - try connection.ping(data: HTTP2PingData(), ack: false) - XCTAssertNil(try connection.readFrame()) - } - - // The fourth ping is the third strike and triggers a GOAWAY. - try connection.ping(data: HTTP2PingData(), ack: false) - let frame = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame.streamID, .rootStream) - XCTAssertGoAway(frame.payload) { streamID, error, data in - XCTAssertEqual(streamID, .rootStream) - XCTAssertEqual(error, .enhanceYourCalm) - XCTAssertEqual(data, ByteBuffer(string: "too_many_pings")) - } - - // The server should close the connection. - try connection.waitUntilClosed() - } - - func testClientKeepaliveWithPermissibleIntervals() throws { - let connection = try Connection( - allowKeepaliveWithoutCalls: true, - minPingIntervalWithoutCalls: .minutes(1), - manualClock: true - ) - try connection.activate() - - for _ in 1 ... 100 { - try connection.ping(data: HTTP2PingData(), ack: false) - XCTAssertNil(try connection.readFrame()) - - // Advance by the ping interval. - connection.advanceTime(by: .minutes(1)) - } - } - - func testClientKeepaliveResetState() throws { - let connection = try Connection( - allowKeepaliveWithoutCalls: true, - minPingIntervalWithoutCalls: .minutes(1) - ) - try connection.activate() - - func sendThreeKeepalivePings() throws { - // The first ping is valid, the second and third are strikes. - for _ in 1 ... 3 { - try connection.ping(data: HTTP2PingData(), ack: false) - XCTAssertNil(try connection.readFrame()) - } - } - - try sendThreeKeepalivePings() - - // "send" a HEADERS frame and flush to reset keep alive state. - connection.syncView.wroteHeadersFrame() - connection.syncView.connectionWillFlush() - - // As above, the first ping is valid, the next two are strikes. - try sendThreeKeepalivePings() - - // The next ping is the third strike and triggers a GOAWAY. - try connection.ping(data: HTTP2PingData(), ack: false) - let frame = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame.streamID, .rootStream) - XCTAssertGoAway(frame.payload) { streamID, error, data in - XCTAssertEqual(streamID, .rootStream) - XCTAssertEqual(error, .enhanceYourCalm) - XCTAssertEqual(data, ByteBuffer(string: "too_many_pings")) - } - - // The server should close the connection. - try connection.waitUntilClosed() - } -} - -extension ServerConnectionManagementHandlerTests { - private func testGracefulShutdown( - connection: Connection, - lastStreamID: HTTP2StreamID, - streamToOpenBeforePingAck: HTTP2StreamID? = nil - ) throws { - let frame1 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame1.streamID, .rootStream) - XCTAssertGoAway(frame1.payload) { streamID, errorCode, _ in - XCTAssertEqual(streamID, .maxID) - XCTAssertEqual(errorCode, .noError) - } - - // Followed by a PING - let frame2 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame2.streamID, .rootStream) - try XCTAssertPing(frame2.payload) { data, ack in - XCTAssertFalse(ack) - - if let id = streamToOpenBeforePingAck { - connection.streamOpened(id) - } - - // Send the PING ACK. - try connection.ping(data: data, ack: true) - } - - // PING ACK triggers another GOAWAY. - let frame3 = try XCTUnwrap(connection.readFrame()) - XCTAssertEqual(frame3.streamID, .rootStream) - XCTAssertGoAway(frame3.payload) { streamID, errorCode, _ in - XCTAssertEqual(streamID, lastStreamID) - XCTAssertEqual(errorCode, .noError) - } - } -} - -extension ServerConnectionManagementHandlerTests { - struct Connection { - let channel: EmbeddedChannel - let streamDelegate: any NIOHTTP2StreamDelegate - let syncView: ServerConnectionManagementHandler.SyncView - - var loop: EmbeddedEventLoop { - self.channel.embeddedEventLoop - } - - private let clock: ServerConnectionManagementHandler.Clock - - init( - maxIdleTime: TimeAmount? = nil, - maxAge: TimeAmount? = nil, - maxGraceTime: TimeAmount? = nil, - keepaliveTime: TimeAmount? = nil, - keepaliveTimeout: TimeAmount? = nil, - allowKeepaliveWithoutCalls: Bool = false, - minPingIntervalWithoutCalls: TimeAmount = .minutes(5), - manualClock: Bool = false - ) throws { - if manualClock { - self.clock = .manual(ServerConnectionManagementHandler.Clock.Manual()) - } else { - self.clock = .nio - } - - let loop = EmbeddedEventLoop() - let handler = ServerConnectionManagementHandler( - eventLoop: loop, - maxIdleTime: maxIdleTime, - maxAge: maxAge, - maxGraceTime: maxGraceTime, - keepaliveTime: keepaliveTime, - keepaliveTimeout: keepaliveTimeout, - allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls, - minPingIntervalWithoutCalls: minPingIntervalWithoutCalls, - requireALPN: false, - clock: self.clock - ) - - self.streamDelegate = handler.http2StreamDelegate - self.syncView = handler.syncView - self.channel = EmbeddedChannel(handler: handler, loop: loop) - } - - func activate() throws { - try self.channel.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() - } - - func advanceTime(by delta: TimeAmount) { - switch self.clock { - case .nio: - () - case .manual(let clock): - clock.advance(by: delta) - } - - self.loop.advanceTime(by: delta) - } - - func streamOpened(_ id: HTTP2StreamID) { - self.streamDelegate.streamCreated(id, channel: self.channel) - } - - func streamClosed(_ id: HTTP2StreamID) { - self.streamDelegate.streamClosed(id, channel: self.channel) - } - - func ping(data: HTTP2PingData, ack: Bool) throws { - let frame = HTTP2Frame(streamID: .rootStream, payload: .ping(data, ack: ack)) - try self.channel.writeInbound(frame) - } - - func readFrame() throws -> HTTP2Frame? { - return try self.channel.readOutbound(as: HTTP2Frame.self) - } - - func waitUntilClosed() throws { - self.channel.embeddedEventLoop.run() - try self.channel.closeFuture.wait() - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift deleted file mode 100644 index 72486d92d..000000000 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ /dev/null @@ -1,1075 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP2 -import XCTest - -@testable import GRPCHTTP2Core - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class GRPCServerStreamHandlerTests: XCTestCase { - func testH2FramesAreIgnored() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - let framesToBeIgnored: [HTTP2Frame.FramePayload] = [ - .ping(.init(), ack: false), - .goAway(lastStreamID: .rootStream, errorCode: .cancel, opaqueData: nil), - // TODO: uncomment when it's possible to build a `StreamPriorityData`. - // .priority( - // HTTP2Frame.StreamPriorityData(exclusive: false, dependency: .rootStream, weight: 4) - // ), - .settings(.ack), - .pushPromise(.init(pushedStreamID: .maxID, headers: [:])), - .windowUpdate(windowSizeIncrement: 4), - .alternativeService(origin: nil, field: nil), - .origin([]), - ] - - for toBeIgnored in framesToBeIgnored { - XCTAssertNoThrow(try channel.writeInbound(toBeIgnored)) - XCTAssertNil(try channel.readInbound(as: HTTP2Frame.FramePayload.self)) - } - } - - func testClientInitialMetadataWithoutContentTypeResultsInRejectedRPC() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata without content-type - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we have sent a trailers-only response - let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() - - XCTAssertEqual(writtenTrailersOnlyResponse.headers, [":status": "415"]) - XCTAssertTrue(writtenTrailersOnlyResponse.endStream) - } - - func testClientInitialMetadataWithoutMethodResultsInRejectedRPC() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata without :method - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we have sent a trailers-only response - let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenTrailersOnlyResponse.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: - ":method header is expected to be present and have a value of \"POST\".", - ] - ) - XCTAssertTrue(writtenTrailersOnlyResponse.endStream) - } - - func testClientInitialMetadataWithoutSchemeResultsInRejectedRPC() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata without :scheme - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we have sent a trailers-only response - let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenTrailersOnlyResponse.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: - ":scheme header must be present and one of \"http\" or \"https\".", - ] - ) - XCTAssertTrue(writtenTrailersOnlyResponse.endStream) - } - - func testClientInitialMetadataWithoutPathResultsInRejectedRPC() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata without :path - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we have sent a trailers-only response - let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenTrailersOnlyResponse.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.invalidArgument.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: "No :path header has been set.", - ] - ) - XCTAssertTrue(writtenTrailersOnlyResponse.endStream) - } - - func testNotAcceptedEncodingResultsInRejectedRPC() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - GRPCHTTP2Keys.encoding.rawValue: "deflate", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we have sent a trailers-only response - let writtenTrailersOnlyResponse = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenTrailersOnlyResponse.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.unimplemented.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: - "deflate compression is not supported; supported algorithms are listed in grpc-accept-encoding", - GRPCHTTP2Keys.acceptEncoding.rawValue: "identity", - ] - ) - XCTAssertTrue(writtenTrailersOnlyResponse.endStream) - } - - func testOverMaximumPayloadSize() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // Write back server's initial metadata - let headers: HPACKHeaders = [ - "some-custom-header": "some-custom-value" - ] - let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) - XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) - - // Make sure we wrote back the initial metadata - let writtenHeaders = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenHeaders.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - "some-custom-header": "some-custom-value", - ] - ) - - // Receive client's message - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Failed to decode message") - } - - // Make sure we haven't sent a response back and that we didn't read the received message - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) - } - - func testClientEndsStream() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 1, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self), - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata with end stream set - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata, endStream: true)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // Write back server's initial metadata - let headers: HPACKHeaders = [ - "some-custom-header": "some-custom-value" - ] - let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) - XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) - - // Make sure we wrote back the initial metadata - let writtenHeaders = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenHeaders.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - "some-custom-header": "some-custom-value", - ] - ) - - // We should throw if the client sends another message, since it's closed the stream already. - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload)) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } - - func testNormalFlow() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 42, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self), - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // Write back server's initial metadata - let headers: HPACKHeaders = [ - "some-custom-header": "some-custom-value" - ] - let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) - XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) - - // Make sure we wrote back the initial metadata - let writtenHeaders = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenHeaders.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - "some-custom-header": "some-custom-value", - ] - ) - - // Receive client's message - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - let clientDataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) - XCTAssertNoThrow(try channel.writeInbound(HTTP2Frame.FramePayload.data(clientDataPayload))) - - // Make sure we haven't sent back an error response, and that we read the message properly - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.message([UInt8](repeating: 0, count: 42)) - ) - - // Write back response - let serverDataPayload = RPCResponsePart.message([UInt8](repeating: 1, count: 42)) - XCTAssertNoThrow(try channel.writeOutbound(serverDataPayload)) - - // Make sure we wrote back the right message - let writtenMessage = try channel.assertReadDataOutbound() - - var expectedBuffer = ByteBuffer() - expectedBuffer.writeInteger(UInt8(0)) // not compressed - expectedBuffer.writeInteger(UInt32(42)) // message length - expectedBuffer.writeRepeatingByte(1, count: 42) // message - XCTAssertEqual(writtenMessage.data, .byteBuffer(expectedBuffer)) - - // Send back status to end RPC - let trailers = RPCResponsePart.status( - .init(code: .dataLoss, message: "Test data loss"), - ["custom-header": "custom-value"] - ) - XCTAssertNoThrow(try channel.writeOutbound(trailers)) - - // Make sure we wrote back the status and trailers - let writtenStatus = try channel.assertReadHeadersOutbound() - - XCTAssertTrue(writtenStatus.endStream) - XCTAssertEqual( - writtenStatus.headers, - [ - GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.dataLoss.rawValue), - GRPCHTTP2Keys.grpcStatusMessage.rawValue: "Test data loss", - "custom-header": "custom-value", - ] - ) - - // Try writing and assert it throws to make sure we don't allow writes - // after closing. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(trailers) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } - - func testReceiveMessageSplitAcrossMultipleBuffers() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // Write back server's initial metadata - let headers: HPACKHeaders = [ - "some-custom-header": "some-custom-value" - ] - let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) - XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) - - // Make sure we wrote back the initial metadata - let writtenHeaders = try channel.assertReadHeadersOutbound() - - XCTAssertEqual( - writtenHeaders.headers, - [ - GRPCHTTP2Keys.status.rawValue: "200", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - "some-custom-header": "some-custom-value", - ] - ) - - // Receive client's first message - var buffer = ByteBuffer() - buffer.writeInteger(UInt8(0)) // not compressed - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) - - buffer.clear() - buffer.writeInteger(UInt32(30)) // message length - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) - - buffer.clear() - buffer.writeRepeatingByte(0, count: 10) // first part of the message - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) - - buffer.clear() - buffer.writeRepeatingByte(1, count: 10) // second part of the message - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - XCTAssertNil(try channel.readInbound(as: RPCRequestPart.self)) - - buffer.clear() - buffer.writeRepeatingByte(2, count: 10) // third part of the message - XCTAssertNoThrow( - try channel.writeInbound(HTTP2Frame.FramePayload.data(.init(data: .byteBuffer(buffer)))) - ) - - // Make sure we haven't sent back an error response, and that we read the message properly - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.message( - [UInt8](repeating: 0, count: 10) + [UInt8](repeating: 1, count: 10) - + [UInt8](repeating: 2, count: 10) - ) - ) - } - - func testReceiveMultipleHeaders() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - try channel.writeInbound(HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata))) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Receive them again. Should be a protocol violation. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) { error in - XCTAssertEqual(error.code, .unavailable) - XCTAssertEqual(error.message, "Stream unexpectedly closed.") - } - let payload = try XCTUnwrap(channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - switch payload { - case .rstStream(let errorCode): - XCTAssertEqual(errorCode, .protocolError) - default: - XCTFail("Expected RST_STREAM, got \(payload)") - } - } - - func testSendMultipleMessagesInSingleBuffer() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // Write back server's initial metadata - let headers: HPACKHeaders = [ - "some-custom-header": "some-custom-value" - ] - let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: headers)) - XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) - - // Read out the metadata - _ = try channel.readOutbound(as: HTTP2Frame.FramePayload.self) - - // This is where this test actually begins. We want to write two messages - // without flushing, and make sure that no messages are sent down the pipeline - // until we flush. Once we flush, both messages should be sent in the same ByteBuffer. - - // Write back first message and make sure nothing's written in the channel. - XCTAssertNoThrow(channel.write(RPCResponsePart.message([UInt8](repeating: 1, count: 4)))) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Write back second message and make sure nothing's written in the channel. - XCTAssertNoThrow(channel.write(RPCResponsePart.message([UInt8](repeating: 2, count: 4)))) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Now flush and check we *do* write the data. - channel.flush() - - let writtenMessage = try channel.assertReadDataOutbound() - - // Make sure both messages have been framed together in the ByteBuffer. - XCTAssertEqual( - writtenMessage.data, - .byteBuffer( - .init(bytes: [ - // First message - 0, // Compression disabled - 0, 0, 0, 4, // Message length - 1, 1, 1, 1, // First message data - - // Second message - 0, // Compression disabled - 0, 0, 0, 4, // Message length - 2, 2, 2, 2, // Second message data - ]) - ) - ) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - } - - func testMessageAndStatusAreNotReordered() throws { - let channel = EmbeddedChannel() - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: channel.eventLoop.makePromise(of: MethodDescriptor.self) - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/test/test", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // Write back server's initial metadata - let serverInitialMetadata = RPCResponsePart.metadata(Metadata(headers: [:])) - XCTAssertNoThrow(try channel.writeOutbound(serverInitialMetadata)) - - // Read out the metadata - _ = try channel.readOutbound(as: HTTP2Frame.FramePayload.self) - - // This is where this test actually begins. We want to write a message followed - // by status and trailers, and only flush after both writes. - // Because messages are buffered and potentially bundled together in a single - // ByteBuffer by the GPRCMessageFramer, we want to make sure that the status - // and trailers won't be written before the messages. - - // Write back message and make sure nothing's written in the channel. - XCTAssertNoThrow(channel.write(RPCResponsePart.message([UInt8](repeating: 1, count: 4)))) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Write status + metadata and make sure nothing's written. - XCTAssertNoThrow(channel.write(RPCResponsePart.status(.init(code: .ok, message: ""), [:]))) - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Now flush and check we *do* write the data in the right order: message first, - // trailers second. - channel.flush() - - let writtenMessage = try channel.assertReadDataOutbound() - - // Make sure we first get message. - XCTAssertEqual( - writtenMessage.data, - .byteBuffer( - .init(bytes: [ - // First message - 0, // Compression disabled - 0, 0, 0, 4, // Message length - 1, 1, 1, 1, // First message data - ]) - ) - ) - XCTAssertFalse(writtenMessage.endStream) - - // Make sure we get trailers. - let writtenTrailers = try channel.assertReadHeadersOutbound() - XCTAssertEqual(writtenTrailers.headers, ["grpc-status": "0"]) - XCTAssertTrue(writtenTrailers.endStream) - - // Make sure we get nothing else. - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - } - - func testMethodDescriptorPromiseSucceeds() throws { - let channel = EmbeddedChannel() - let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: promise, - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - XCTAssertEqual( - try promise.futureResult.wait(), - MethodDescriptor(service: "SomeService", method: "SomeMethod") - ) - } - - func testMethodDescriptorPromiseIsFailedWhenHandlerRemoved() throws { - let channel = EmbeddedChannel() - let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: promise, - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - try channel.pipeline.syncOperations.removeHandler(handler).wait() - - XCTAssertThrowsError( - ofType: RPCError.self, - try promise.futureResult.wait() - ) { error in - XCTAssertEqual(error.code, .unavailable) - XCTAssertEqual(error.message, "RPC stream was closed before we got any Metadata.") - } - } - - func testMethodDescriptorPromiseIsFailedIfRPCRejected() throws { - let channel = EmbeddedChannel() - let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: promise, - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "SomeService/SomeMethod", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/not-valid-contenttype", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - XCTAssertThrowsError( - ofType: RPCError.self, - try promise.futureResult.wait() - ) { error in - XCTAssertEqual(error.code, .unavailable) - XCTAssertEqual(error.message, "RPC was rejected.") - } - } - - func testUnexpectedStreamClose_ErrorFired() throws { - let channel = EmbeddedChannel() - let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: promise, - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // An error is fired down the pipeline - let thrownError = ChannelError.connectTimeout(.milliseconds(100)) - channel.pipeline.fireErrorCaught(thrownError) - - // The server handler simply forwards the error. - XCTAssertThrowsError( - ofType: type(of: thrownError), - try channel.throwIfErrorCaught() - ) { error in - XCTAssertEqual(error, thrownError) - } - - // We should now be closed: check we can't write anymore. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } - - func testUnexpectedStreamClose_ChannelInactive() throws { - let channel = EmbeddedChannel() - let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: promise, - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // Channel becomes inactive - channel.pipeline.fireChannelInactive() - - // The server handler fires an error - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.throwIfErrorCaught() - ) { error in - XCTAssertEqual(error.code, .unavailable) - XCTAssertEqual(error.message, "Stream unexpectedly closed.") - } - - // We should now be closed: check we can't write anymore. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } - - func testUnexpectedStreamClose_ResetStreamFrame() throws { - let channel = EmbeddedChannel() - let promise = channel.eventLoop.makePromise(of: MethodDescriptor.self) - let handler = GRPCServerStreamHandler( - scheme: .http, - acceptedEncodings: [], - maxPayloadSize: 100, - methodDescriptorPromise: promise, - skipStateMachineAssertions: true - ) - try channel.pipeline.syncOperations.addHandler(handler) - - // Receive client's initial metadata - let clientInitialMetadata: HPACKHeaders = [ - GRPCHTTP2Keys.path.rawValue: "/SomeService/SomeMethod", - GRPCHTTP2Keys.scheme.rawValue: "http", - GRPCHTTP2Keys.method.rawValue: "POST", - GRPCHTTP2Keys.contentType.rawValue: "application/grpc", - GRPCHTTP2Keys.te.rawValue: "trailers", - ] - XCTAssertNoThrow( - try channel.writeInbound( - HTTP2Frame.FramePayload.headers(.init(headers: clientInitialMetadata)) - ) - ) - - // Make sure we haven't sent back an error response, and that we read the initial metadata - XCTAssertNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - XCTAssertEqual( - try channel.readInbound(as: RPCRequestPart.self), - RPCRequestPart.metadata(Metadata(headers: clientInitialMetadata)) - ) - - // We receive RST_STREAM frame - // Assert the server handler fires an error - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeInbound( - HTTP2Frame.FramePayload.rstStream(.internalError) - ) - ) { error in - XCTAssertEqual(error.code, .unavailable) - XCTAssertEqual(error.message, "Stream unexpectedly closed: a RST_STREAM frame was received.") - } - - // We should now be closed: check we can't write anymore. - XCTAssertThrowsError( - ofType: RPCError.self, - try channel.writeOutbound(RPCResponsePart.metadata(Metadata())) - ) { error in - XCTAssertEqual(error.code, .internalError) - XCTAssertEqual(error.message, "Invalid state") - } - } -} - -extension EmbeddedChannel { - fileprivate func assertReadHeadersOutbound() throws -> HTTP2Frame.FramePayload.Headers { - guard - case .headers(let writtenHeaders) = try XCTUnwrap( - try self.readOutbound(as: HTTP2Frame.FramePayload.self) - ) - else { - throw TestError.assertionFailure("Expected to write headers") - } - return writtenHeaders - } - - fileprivate func assertReadDataOutbound() throws -> HTTP2Frame.FramePayload.Data { - guard - case .data(let writtenMessage) = try XCTUnwrap( - try self.readOutbound(as: HTTP2Frame.FramePayload.self) - ) - else { - throw TestError.assertionFailure("Expected to write data") - } - return writtenMessage - } -} - -private enum TestError: Error { - case assertionFailure(String) -} diff --git a/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift b/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift deleted file mode 100644 index d7e1c06b8..000000000 --- a/Tests/GRPCHTTP2CoreTests/Server/HTTP2ServerTransportConfigTests.swift +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import XCTest - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -final class HTTP2ServerTransportConfigTests: XCTestCase { - func testCompressionDefaults() { - let config = HTTP2ServerTransport.Config.Compression.defaults - XCTAssertEqual(config.enabledAlgorithms, .none) - } - - func testKeepaliveDefaults() { - let config = HTTP2ServerTransport.Config.Keepalive.defaults - XCTAssertEqual(config.time, .seconds(7200)) - XCTAssertEqual(config.timeout, .seconds(20)) - XCTAssertEqual(config.clientBehavior.allowWithoutCalls, false) - XCTAssertEqual(config.clientBehavior.minPingIntervalWithoutCalls, .seconds(300)) - } - - func testConnectionDefaults() { - let config = HTTP2ServerTransport.Config.Connection.defaults - XCTAssertNil(config.maxAge) - XCTAssertNil(config.maxGraceTime) - XCTAssertNil(config.maxIdleTime) - } - - func testHTTP2Defaults() { - let config = HTTP2ServerTransport.Config.HTTP2.defaults - XCTAssertEqual(config.maxFrameSize, 16_384) - XCTAssertEqual(config.targetWindowSize, 65_535) - XCTAssertNil(config.maxConcurrentStreams) - } - - func testRPCDefaults() { - let config = HTTP2ServerTransport.Config.RPC.defaults - XCTAssertEqual(config.maxRequestPayloadSize, 4_194_304) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift deleted file mode 100644 index b9e9fb5b8..000000000 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/AtomicCounter.swift +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 Synchronization - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class AtomicCounter: Sendable { - private let counter: Atomic - - init(_ initialValue: Int = 0) { - self.counter = Atomic(initialValue) - } - - var value: Int { - self.counter.load(ordering: .sequentiallyConsistent) - } - - @discardableResult - func increment() -> (oldValue: Int, newValue: Int) { - self.counter.add(1, ordering: .sequentiallyConsistent) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift deleted file mode 100644 index e7c1ef50a..000000000 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/MethodDescriptor+Common.swift +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 - -extension MethodDescriptor { - static var echoGet: Self { - MethodDescriptor(service: "echo.Echo", method: "Get") - } - - static var echoUpdate: Self { - MethodDescriptor(service: "echo.Echo", method: "Update") - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension MethodConfig.Name { - init(_ descriptor: MethodDescriptor) { - self = MethodConfig.Name(service: descriptor.service, method: descriptor.method) - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift deleted file mode 100644 index 1183b9df1..000000000 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/Task+Poll.swift +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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. - */ - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -extension Task where Success == Never, Failure == Never { - static func poll( - every interval: Duration, - timeLimit: Duration = .seconds(5), - until predicate: () async throws -> Bool - ) async throws -> Bool { - var start = ContinuousClock.now - let end = start.advanced(by: timeLimit) - - while end > .now { - let canReturn = try await predicate() - if canReturn { return true } - - start = start.advanced(by: interval) - try await Task.sleep(until: start) - } - - return false - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift deleted file mode 100644 index 2e036f985..000000000 --- a/Tests/GRPCHTTP2CoreTests/Test Utilities/XCTest+Utilities.swift +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 - -func XCTAssertThrowsError( - ofType: E.Type, - file: StaticString = #filePath, - line: UInt = #line, - _ expression: @autoclosure () throws -> T, - _ errorHandler: (E) -> Void -) { - XCTAssertThrowsError(try expression(), file: file, line: line) { error in - guard let error = error as? E else { - return XCTFail("Error had unexpected type '\(type(of: error))'", file: file, line: line) - } - errorHandler(error) - } -} - -func XCTAssertDescription( - _ subject: some CustomStringConvertible, - _ expected: String, - file: StaticString = #filePath, - line: UInt = #line -) { - XCTAssertEqual(String(describing: subject), expected, file: file, line: line) -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func XCTUnwrapAsync(_ expression: () async throws -> T?) async throws -> T { - let value = try await expression() - return try XCTUnwrap(value) -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -func XCTAssertThrowsErrorAsync( - ofType: E.Type = E.self, - _ expression: () async throws -> T, - errorHandler: (E) -> Void -) async { - do { - _ = try await expression() - XCTFail("Expression didn't throw") - } catch let error as E { - errorHandler(error) - } catch { - XCTFail("Error had unexpected type '\(type(of: error))'") - } -} - -func XCTAssert(_ value: Any, as type: T.Type, _ verify: (T) throws -> Void) rethrows { - if let value = value as? T { - try verify(value) - } else { - XCTFail("\(value) couldn't be cast to \(T.self)") - } -} - -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -func XCTPoll( - every interval: Duration, - timeLimit: Duration = .seconds(5), - until predicate: () async throws -> Bool -) async throws { - let becameTrue = try await Task.poll(every: interval, timeLimit: timeLimit, until: predicate) - XCTAssertTrue(becameTrue, "Predicate didn't return true within \(timeLimit)") -} diff --git a/Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift b/Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift deleted file mode 100644 index b6892d0db..000000000 --- a/Tests/GRPCHTTP2CoreTests/XCTest+FramePayload.swift +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 NIOCore -import NIOHTTP2 -import XCTest - -func XCTAssertGoAway( - _ payload: HTTP2Frame.FramePayload, - verify: (HTTP2StreamID, HTTP2ErrorCode, ByteBuffer?) throws -> Void = { _, _, _ in } -) rethrows { - switch payload { - case .goAway(let lastStreamID, let errorCode, let opaqueData): - try verify(lastStreamID, errorCode, opaqueData) - default: - XCTFail("Expected '.goAway' got '\(payload)'") - } -} - -func XCTAssertPing( - _ payload: HTTP2Frame.FramePayload, - verify: (HTTP2PingData, Bool) throws -> Void = { _, _ in } -) rethrows { - switch payload { - case .ping(let data, ack: let ack): - try verify(data, ack) - default: - XCTFail("Expected '.ping' got '\(payload)'") - } -} diff --git a/Tests/GRPCHTTP2TransportTests/ControlService.swift b/Tests/GRPCHTTP2TransportTests/ControlService.swift deleted file mode 100644 index 9139d9a42..000000000 --- a/Tests/GRPCHTTP2TransportTests/ControlService.swift +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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 struct Foundation.Data - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct ControlService: ControlStreamingServiceProtocol { - func unary( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - try await self.handle(request: request) - } - - func serverStream( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - try await self.handle(request: request) - } - - func clientStream( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - try await self.handle(request: request) - } - - func bidiStream( - request: ServerRequest.Stream, - context: ServerContext - ) async throws -> ServerResponse.Stream { - try await self.handle(request: request) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ControlService { - private func handle( - request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { - var iterator = request.messages.makeAsyncIterator() - - guard let message = try await iterator.next() else { - // Empty input stream, empty output stream. - return ServerResponse.Stream { _ in [:] } - } - - // Check if the request is for a trailers-only response. - if message.hasStatus, message.isTrailersOnly { - let trailers = message.echoMetadataInTrailers ? request.metadata.echo() : [:] - let code = Status.Code(rawValue: message.status.code.rawValue).flatMap { RPCError.Code($0) } - - if let code = code { - throw RPCError(code: code, message: message.status.message, metadata: trailers) - } else { - // Invalid code, the request is invalid, so throw an appropriate error. - throw RPCError( - code: .invalidArgument, - message: "Trailers only response must use a non-OK status code" - ) - } - } - - // Not a trailers-only response. Should the metadata be echo'd back? - let metadata = message.echoMetadataInHeaders ? request.metadata.echo() : [:] - - // The iterator needs to be transferred into the response. This is okay: we won't touch the - // iterator again from the current concurrency domain. - let transfer = UnsafeTransfer(iterator) - - return ServerResponse.Stream(metadata: metadata) { writer in - // Finish dealing with the first message. - switch try await self.processMessage(message, metadata: request.metadata, writer: writer) { - case .return(let metadata): - return metadata - case .continue: - () - } - - var iterator = transfer.wrappedValue - // Process the rest of the messages. - while let message = try await iterator.next() { - switch try await self.processMessage(message, metadata: request.metadata, writer: writer) { - case .return(let metadata): - return metadata - case .continue: - () - } - } - - // Input stream finished without explicitly setting a status; finish the RPC cleanly. - return [:] - } - } - - private enum NextProcessingStep { - case `return`(Metadata) - case `continue` - } - - private func processMessage( - _ input: ControlInput, - metadata: Metadata, - writer: RPCWriter - ) async throws -> NextProcessingStep { - // If messages were requested, build a response and send them back. - if input.numberOfMessages > 0 { - let output = ControlOutput.with { - $0.payload = Data( - repeating: UInt8(truncatingIfNeeded: input.messageParams.content), - count: Int(input.messageParams.size) - ) - } - - for _ in 0 ..< input.numberOfMessages { - try await writer.write(output) - } - } - - // Check whether the RPC should be finished (i.e. the input `hasStatus`). - guard input.hasStatus else { - if input.echoMetadataInTrailers { - // There was no status in the input, but echo metadata in trailers was set. This is an - // implicit 'ok' status. - let trailers = input.echoMetadataInTrailers ? metadata.echo() : [:] - return .return(trailers) - } else { - // No status, and not echoing back metadata. Continue consuming the input stream. - return .continue - } - } - - // Build the trailers. - let trailers = input.echoMetadataInTrailers ? metadata.echo() : [:] - - if input.status.code == .ok { - return .return(trailers) - } - - // Non-OK status code, throw an error. - let code = Status.Code(rawValue: input.status.code.rawValue).flatMap { RPCError.Code($0) } - - if let code = code { - // Valid error code, throw it. - throw RPCError(code: code, message: input.status.message, metadata: trailers) - } else { - // Invalid error code, throw an appropriate error. - throw RPCError( - code: .invalidArgument, - message: "Invalid error code '\(input.status.code)'" - ) - } - } -} - -extension Metadata { - fileprivate func echo() -> Self { - var copy = Metadata() - copy.reserveCapacity(self.count) - - for (key, value) in self { - // Header field names mustn't contain ":". - let key = "echo-" + key.replacingOccurrences(of: ":", with: "") - switch value { - case .string(let stringValue): - copy.addString(stringValue, forKey: key) - case .binary(let binaryValue): - copy.addBinary(binaryValue, forKey: key) - } - } - - return copy - } -} - -private struct UnsafeTransfer { - var wrappedValue: Wrapped - - init(_ wrappedValue: Wrapped) { - self.wrappedValue = wrappedValue - } -} - -extension UnsafeTransfer: @unchecked Sendable {} diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift deleted file mode 100644 index 8833dc856..000000000 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.grpc.swift +++ /dev/null @@ -1,490 +0,0 @@ -// -// 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. - -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the gRPC Swift generator plugin for the protocol buffer compiler. -// Source: control.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/grpc/grpc-swift - -import GRPCCore -import GRPCProtobuf - -internal enum Control { - internal static let descriptor = GRPCCore.ServiceDescriptor.Control - internal enum Method { - internal enum Unary { - internal typealias Input = ControlInput - internal typealias Output = ControlOutput - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Control.descriptor.fullyQualifiedService, - method: "Unary" - ) - } - internal enum ServerStream { - internal typealias Input = ControlInput - internal typealias Output = ControlOutput - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Control.descriptor.fullyQualifiedService, - method: "ServerStream" - ) - } - internal enum ClientStream { - internal typealias Input = ControlInput - internal typealias Output = ControlOutput - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Control.descriptor.fullyQualifiedService, - method: "ClientStream" - ) - } - internal enum BidiStream { - internal typealias Input = ControlInput - internal typealias Output = ControlOutput - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Control.descriptor.fullyQualifiedService, - method: "BidiStream" - ) - } - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - Unary.descriptor, - ServerStream.descriptor, - ClientStream.descriptor, - BidiStream.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = ControlStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = ControlServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = ControlClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = ControlClient -} - -extension GRPCCore.ServiceDescriptor { - internal static let Control = Self( - package: "", - service: "Control" - ) -} - -/// A controllable service for testing. -/// -/// The control service has one RPC of each kind, the input to each RPC controls -/// the output. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol ControlStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - func unary( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - func serverStream( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - func clientStream( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - func bidiStream( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Conformance to `GRPCCore.RegistrableRPCService`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Control.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Control.Method.Unary.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.unary( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Control.Method.ServerStream.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.serverStream( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Control.Method.ClientStream.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.clientStream( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: Control.Method.BidiStream.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.bidiStream( - request: request, - context: context - ) - } - ) - } -} - -/// A controllable service for testing. -/// -/// The control service has one RPC of each kind, the input to each RPC controls -/// the output. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol ControlServiceProtocol: Control.StreamingServiceProtocol { - func unary( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - func serverStream( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - - func clientStream( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - - func bidiStream( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream -} - -/// Partial conformance to `ControlStreamingServiceProtocol`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Control.ServiceProtocol { - internal func unary( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.unary( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - - internal func serverStream( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.serverStream( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return response - } - - internal func clientStream( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.clientStream( - request: request, - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } -} - -/// A controllable service for testing. -/// -/// The control service has one RPC of each kind, the input to each RPC controls -/// the output. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol ControlClientProtocol: Sendable { - func unary( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - func serverStream( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable - - func clientStream( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - - func bidiStream( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Control.ClientProtocol { - internal func unary( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.unary( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func serverStream( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.serverStream( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func clientStream( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.clientStream( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - - internal func bidiStream( - request: GRPCCore.ClientRequest.Stream, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.bidiStream( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension Control.ClientProtocol { - internal func unary( - _ message: ControlInput, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.unary( - request: request, - options: options, - handleResponse - ) - } - - internal func serverStream( - _ message: ControlInput, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.serverStream( - request: request, - options: options, - handleResponse - ) - } - - internal func clientStream( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.clientStream( - request: request, - options: options, - handleResponse - ) - } - - internal func bidiStream( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( - metadata: metadata, - producer: requestProducer - ) - return try await self.bidiStream( - request: request, - options: options, - handleResponse - ) - } -} - -/// A controllable service for testing. -/// -/// The control service has one RPC of each kind, the input to each RPC controls -/// the output. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct ControlClient: Control.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - internal func unary( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Control.Method.Unary.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - internal func serverStream( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: Control.Method.ServerStream.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - internal func clientStream( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: Control.Method.ClientStream.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - - internal func bidiStream( - request: GRPCCore.ClientRequest.Stream, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R - ) async throws -> R where R: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: Control.Method.BidiStream.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } -} \ No newline at end of file diff --git a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift b/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift deleted file mode 100644 index 435bd944c..000000000 --- a/Tests/GRPCHTTP2TransportTests/Generated/control.pb.swift +++ /dev/null @@ -1,446 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: control.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// -// 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 Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -enum StatusCode: SwiftProtobuf.Enum, Swift.CaseIterable { - typealias RawValue = Int - case ok // = 0 - case cancelled // = 1 - case unknown // = 2 - case invalidArgument // = 3 - case deadlineExceeded // = 4 - case notFound // = 5 - case alreadyExists // = 6 - case permissionDenied // = 7 - case resourceExhausted // = 8 - case failedPrecondition // = 9 - case aborted // = 10 - case outOfRange // = 11 - case unimplemented // = 12 - case `internal` // = 13 - case unavailable // = 14 - case dataLoss // = 15 - case unauthenticated // = 16 - case UNRECOGNIZED(Int) - - init() { - self = .ok - } - - init?(rawValue: Int) { - switch rawValue { - case 0: self = .ok - case 1: self = .cancelled - case 2: self = .unknown - case 3: self = .invalidArgument - case 4: self = .deadlineExceeded - case 5: self = .notFound - case 6: self = .alreadyExists - case 7: self = .permissionDenied - case 8: self = .resourceExhausted - case 9: self = .failedPrecondition - case 10: self = .aborted - case 11: self = .outOfRange - case 12: self = .unimplemented - case 13: self = .internal - case 14: self = .unavailable - case 15: self = .dataLoss - case 16: self = .unauthenticated - default: self = .UNRECOGNIZED(rawValue) - } - } - - var rawValue: Int { - switch self { - case .ok: return 0 - case .cancelled: return 1 - case .unknown: return 2 - case .invalidArgument: return 3 - case .deadlineExceeded: return 4 - case .notFound: return 5 - case .alreadyExists: return 6 - case .permissionDenied: return 7 - case .resourceExhausted: return 8 - case .failedPrecondition: return 9 - case .aborted: return 10 - case .outOfRange: return 11 - case .unimplemented: return 12 - case .internal: return 13 - case .unavailable: return 14 - case .dataLoss: return 15 - case .unauthenticated: return 16 - case .UNRECOGNIZED(let i): return i - } - } - - // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [StatusCode] = [ - .ok, - .cancelled, - .unknown, - .invalidArgument, - .deadlineExceeded, - .notFound, - .alreadyExists, - .permissionDenied, - .resourceExhausted, - .failedPrecondition, - .aborted, - .outOfRange, - .unimplemented, - .internal, - .unavailable, - .dataLoss, - .unauthenticated, - ] - -} - -struct ControlInput: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Whether metadata should be echo'd back in the initial metadata. - /// - /// Ignored if the initial metadata has already been sent back to the - /// client. - /// - /// Each header field name in the request headers will be prefixed with - /// "echo-". For example the header field name "foo" will be returned - /// as "echo-foo. Note that semicolons aren't valid in HTTP header field - /// names (apart from pseudo headers). As such all semicolons should be - /// removed (":path" should become "echo-path"). - var echoMetadataInHeaders: Bool = false - - /// Parameters for response messages. - var messageParams: PayloadParameters { - get {return _messageParams ?? PayloadParameters()} - set {_messageParams = newValue} - } - /// Returns true if `messageParams` has been explicitly set. - var hasMessageParams: Bool {return self._messageParams != nil} - /// Clears the value of `messageParams`. Subsequent reads from it will return its default value. - mutating func clearMessageParams() {self._messageParams = nil} - - /// The number of response messages. - var numberOfMessages: Int32 = 0 - - /// The status code and message to use at the end of the RPC. - /// - /// If this is set then the RPC will be ended after `number_of_messages` - /// messages have been sent back to the client. - var status: RPCStatus { - get {return _status ?? RPCStatus()} - set {_status = newValue} - } - /// Returns true if `status` has been explicitly set. - var hasStatus: Bool {return self._status != nil} - /// Clears the value of `status`. Subsequent reads from it will return its default value. - mutating func clearStatus() {self._status = nil} - - /// Whether the response should be trailers only. - /// - /// Ignored unless it's set on the first message on the stream. When set - /// the RPC will be completed with a trailers-only response using the - /// status code and message from 'status'. The request metadata will be - /// included if 'echo_metadata_in_trailers' is set. - /// - /// If this is set then 'number_of_messages', 'message_params', and - /// 'echo_metadata_in_headers' are ignored. - var isTrailersOnly: Bool = false - - /// Whether metadata should be echo'd back in the trailing metadata. - /// - /// Ignored unless 'status' is set. - /// - /// Each header field name in the request headers will be prefixed with - /// "echo-". For example the header field name "foo" will be returned - /// as "echo-foo. Note that semicolons aren't valid in HTTP header field - /// names (apart from pseudo headers). As such all semicolons should be - /// removed (":path" should become "echo-path"). - var echoMetadataInTrailers: Bool = false - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _messageParams: PayloadParameters? = nil - fileprivate var _status: RPCStatus? = nil -} - -struct RPCStatus: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Status code indicating the outcome of the RPC. - var code: StatusCode = .ok - - /// The message to include with the 'code' at the end of the RPC. - var message: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct PayloadParameters: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The number of bytes to put into the output payload. - var size: Int32 = 0 - - /// The content to use in the payload. The value is truncated to an octet. - var content: UInt32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -struct ControlOutput: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var payload: Data = Data() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -extension StatusCode: SwiftProtobuf._ProtoNameProviding { - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 0: .same(proto: "OK"), - 1: .same(proto: "CANCELLED"), - 2: .same(proto: "UNKNOWN"), - 3: .same(proto: "INVALID_ARGUMENT"), - 4: .same(proto: "DEADLINE_EXCEEDED"), - 5: .same(proto: "NOT_FOUND"), - 6: .same(proto: "ALREADY_EXISTS"), - 7: .same(proto: "PERMISSION_DENIED"), - 8: .same(proto: "RESOURCE_EXHAUSTED"), - 9: .same(proto: "FAILED_PRECONDITION"), - 10: .same(proto: "ABORTED"), - 11: .same(proto: "OUT_OF_RANGE"), - 12: .same(proto: "UNIMPLEMENTED"), - 13: .same(proto: "INTERNAL"), - 14: .same(proto: "UNAVAILABLE"), - 15: .same(proto: "DATA_LOSS"), - 16: .same(proto: "UNAUTHENTICATED"), - ] -} - -extension ControlInput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "ControlInput" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "echo_metadata_in_headers"), - 2: .standard(proto: "message_params"), - 3: .standard(proto: "number_of_messages"), - 5: .same(proto: "status"), - 6: .standard(proto: "is_trailers_only"), - 4: .standard(proto: "echo_metadata_in_trailers"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBoolField(value: &self.echoMetadataInHeaders) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._messageParams) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &self.numberOfMessages) }() - case 4: try { try decoder.decodeSingularBoolField(value: &self.echoMetadataInTrailers) }() - case 5: try { try decoder.decodeSingularMessageField(value: &self._status) }() - case 6: try { try decoder.decodeSingularBoolField(value: &self.isTrailersOnly) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if self.echoMetadataInHeaders != false { - try visitor.visitSingularBoolField(value: self.echoMetadataInHeaders, fieldNumber: 1) - } - try { if let v = self._messageParams { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - if self.numberOfMessages != 0 { - try visitor.visitSingularInt32Field(value: self.numberOfMessages, fieldNumber: 3) - } - if self.echoMetadataInTrailers != false { - try visitor.visitSingularBoolField(value: self.echoMetadataInTrailers, fieldNumber: 4) - } - try { if let v = self._status { - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - } }() - if self.isTrailersOnly != false { - try visitor.visitSingularBoolField(value: self.isTrailersOnly, fieldNumber: 6) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: ControlInput, rhs: ControlInput) -> Bool { - if lhs.echoMetadataInHeaders != rhs.echoMetadataInHeaders {return false} - if lhs._messageParams != rhs._messageParams {return false} - if lhs.numberOfMessages != rhs.numberOfMessages {return false} - if lhs._status != rhs._status {return false} - if lhs.isTrailersOnly != rhs.isTrailersOnly {return false} - if lhs.echoMetadataInTrailers != rhs.echoMetadataInTrailers {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension RPCStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "RPCStatus" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "code"), - 2: .same(proto: "message"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularEnumField(value: &self.code) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.message) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.code != .ok { - try visitor.visitSingularEnumField(value: self.code, fieldNumber: 1) - } - if !self.message.isEmpty { - try visitor.visitSingularStringField(value: self.message, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: RPCStatus, rhs: RPCStatus) -> Bool { - if lhs.code != rhs.code {return false} - if lhs.message != rhs.message {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension PayloadParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "PayloadParameters" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "size"), - 2: .same(proto: "content"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.size) }() - case 2: try { try decoder.decodeSingularUInt32Field(value: &self.content) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.size != 0 { - try visitor.visitSingularInt32Field(value: self.size, fieldNumber: 1) - } - if self.content != 0 { - try visitor.visitSingularUInt32Field(value: self.content, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: PayloadParameters, rhs: PayloadParameters) -> Bool { - if lhs.size != rhs.size {return false} - if lhs.content != rhs.content {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension ControlOutput: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = "ControlOutput" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularBytesField(value: &self.payload) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.payload.isEmpty { - try visitor.visitSingularBytesField(value: self.payload, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: ControlOutput, rhs: ControlOutput) -> Bool { - if lhs.payload != rhs.payload {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift deleted file mode 100644 index 70ae83707..000000000 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift +++ /dev/null @@ -1,483 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import XCTest - -#if canImport(NIOSSL) -import NIOSSL -#endif - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class HTTP2TransportNIOPosixTests: XCTestCase { - func testGetListeningAddress_IPv4() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .ipv4(host: "0.0.0.0", port: 0), - config: .defaults(transportSecurity: .plaintext) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - let address = try await transport.listeningAddress - let ipv4Address = try XCTUnwrap(address.ipv4) - XCTAssertNotEqual(ipv4Address.port, 0) - transport.beginGracefulShutdown() - } - } - } - - func testGetListeningAddress_IPv6() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .ipv6(host: "::1", port: 0), - config: .defaults(transportSecurity: .plaintext) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - let address = try await transport.listeningAddress - let ipv6Address = try XCTUnwrap(address.ipv6) - XCTAssertNotEqual(ipv6Address.port, 0) - transport.beginGracefulShutdown() - } - } - } - - func testGetListeningAddress_UnixDomainSocket() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .unixDomainSocket(path: "/tmp/posix-uds-test"), - config: .defaults(transportSecurity: .plaintext) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - let address = try await transport.listeningAddress - XCTAssertEqual( - address.unixDomainSocket, - GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/posix-uds-test") - ) - transport.beginGracefulShutdown() - } - } - } - - func testGetListeningAddress_Vsock() async throws { - try XCTSkipUnless(self.vsockAvailable(), "Vsock unavailable") - - let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .vsock(contextID: .any, port: .any), - config: .defaults(transportSecurity: .plaintext) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - let address = try await transport.listeningAddress - XCTAssertNotNil(address.virtualSocket) - transport.beginGracefulShutdown() - } - } - } - - func testGetListeningAddress_InvalidAddress() async { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"), - config: .defaults(transportSecurity: .plaintext) - ) - - try? await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - do { - _ = try await transport.listeningAddress - XCTFail("Should have thrown a RuntimeError") - } catch let error as RuntimeError { - XCTAssertEqual(error.code, .serverIsStopped) - XCTAssertEqual( - error.message, - """ - There is no listening address bound for this server: there may have \ - been an error which caused the transport to close, or it may have shut down. - """ - ) - } - } - } - } - - func testGetListeningAddress_StoppedListening() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix( - address: .ipv4(host: "0.0.0.0", port: 0), - config: .defaults(transportSecurity: .plaintext) - ) - - try? await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - - do { - _ = try await transport.listeningAddress - XCTFail("Should have thrown a RuntimeError") - } catch let error as RuntimeError { - XCTAssertEqual(error.code, .serverIsStopped) - XCTAssertEqual( - error.message, - """ - There is no listening address bound for this server: there may have \ - been an error which caused the transport to close, or it may have shut down. - """ - ) - } - } - - group.addTask { - let address = try await transport.listeningAddress - XCTAssertNotNil(address.ipv4) - transport.beginGracefulShutdown() - } - } - } - - func testServerConfig_Defaults() throws { - let grpcConfig = HTTP2ServerTransport.Posix.Config.defaults( - transportSecurity: .plaintext - ) - - XCTAssertEqual(grpcConfig.compression, HTTP2ServerTransport.Config.Compression.defaults) - XCTAssertEqual(grpcConfig.connection, HTTP2ServerTransport.Config.Connection.defaults) - XCTAssertEqual(grpcConfig.http2, HTTP2ServerTransport.Config.HTTP2.defaults) - XCTAssertEqual(grpcConfig.rpc, HTTP2ServerTransport.Config.RPC.defaults) - } - - func testClientConfig_Defaults() throws { - let grpcConfig = HTTP2ClientTransport.Posix.Config.defaults( - transportSecurity: .plaintext - ) - - XCTAssertEqual(grpcConfig.compression, HTTP2ClientTransport.Config.Compression.defaults) - XCTAssertEqual(grpcConfig.connection, HTTP2ClientTransport.Config.Connection.defaults) - XCTAssertEqual(grpcConfig.http2, HTTP2ClientTransport.Config.HTTP2.defaults) - XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults) - } - - #if canImport(NIOSSL) - static let samplePemCert = """ - -----BEGIN CERTIFICATE----- - MIIGGzCCBAOgAwIBAgIJAJ/X0Fo0ynmEMA0GCSqGSIb3DQEBCwUAMIGjMQswCQYD - VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5z - b2t5bzEuMCwGA1UECgwlU2FuIEZyYW5zb2t5byBJbnN0aXR1dGUgb2YgVGVjaG5v - bG9neTEVMBMGA1UECwwMUm9ib3RpY3MgTGFiMSAwHgYDVQQDDBdyb2JvdHMuc2Fu - ZnJhbnNva3lvLmVkdTAeFw0xNzEwMTYyMTAxMDJaFw00NzEwMDkyMTAxMDJaMIGj - MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu - IEZyYW5zb2t5bzEuMCwGA1UECgwlU2FuIEZyYW5zb2t5byBJbnN0aXR1dGUgb2Yg - VGVjaG5vbG9neTEVMBMGA1UECwwMUm9ib3RpY3MgTGFiMSAwHgYDVQQDDBdyb2Jv - dHMuc2FuZnJhbnNva3lvLmVkdTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC - ggIBAO9rzJOOE8cmsIqAJMCrHDxkBAMgZhMsJ863MnWtVz5JIJK6CKI/Nu26tEzo - kHy3EI9565RwikvauheMsWaTFA4PD/P+s1DtxRCGIcK5x+SoTN7Drn5ZueoJNZRf - TYuN+gwyhprzrZrYjXpvEVPYuSIeUqK5XGrTyFA2uGj9wY3f9IF4rd7JT0ewRb1U - 8OcR7xQbXKGjkY4iJE1TyfmIsBZboKaG/aYa9KbnWyTkDssaELWUIKrjwwuPgVgS - vlAYmo12MlsGEzkO9z78jvFmhUOsaEldM8Ua2AhOKW0oSYgauVuro/Ap/o5zn8PD - IDapl9g+5vjN2LucqX2a9utoFvxSKXT4NvfpL9fJvzdBNMM4xpqtHIkV0fkiMbWk - EW2FFlOXKnIJV8wT4a9iduuIDMg8O7oc+gt9pG9MHTWthXm4S29DARTqfZ48bW77 - z8RrEURV03o05b/twuAJSRyyOCUi61yMo3YNytebjY2W3Pxqpq+YmT5qhqBZDLlT - LMptuFdISv6SQgg7JoFHGMWRXUavMj/sn5qZD4pQyZToHJ2Vtg5W/MI1pKwc3oKD - 6M3/7Gf35r92V/ox6XT7+fnEsAH8AtQiZJkEbvzJ5lpUihSIaV3a/S+jnk7Lw8Tp - vjtpfjOg+wBblc38Oa9tk2WdXwYDbnvbeL26WmyHwQTUBi1jAgMBAAGjUDBOMB0G - A1UdDgQWBBToPRmTBQEF5F5LcPiUI5qBNPBU+DAfBgNVHSMEGDAWgBToPRmTBQEF - 5F5LcPiUI5qBNPBU+DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCY - gxM5lufF2lTB9sH0s1E1VTERv37qoapNP+aw06oZkAD67QOTXFzbsM3JU1diY6rV - Y0g9CLzRO7gZY+kmi1WWnsYiMMSIGjIfsB8S+ot43LME+AJXPVeDZQnoZ6KQ/9r+ - 71Umi4AKLoZ9dInyUIM3EHg9pg5B0eEINrh4J+OPGtlC3NMiWxdmIkZwzfXa+64Z - 8k5aX5piMTI+9BQSMWw5l7tFT/PISuI8b/Ln4IUBXKA0xkONXVnjPOmS0h7MBoc2 - EipChDKnK+Mtm9GQewOCKdS2nsrCndGkIBnUix4ConUYIoywVzWGMD+9OzKNg76d - O6A7MxdjEdKhf1JDvklxInntDUDTlSFL4iEFELwyRseoTzj8vJE+cL6h6ClasYQ6 - p0EeL3UpICYerfIvPhohftCivCH3k7Q1BSf0fq73cQ55nrFAHrqqYjD7HBeBS9hn - 3L6bz9Eo6U9cuxX42k3l1N44BmgcDPin0+CRTirEmahUMb3gmvoSZqQ3Cz86GkIg - 7cNJosc9NyevQlU9SX3ptEbv33tZtlB5GwgZ2hiGBTY0C3HaVFjLpQiSS5ygZLgI - /+AKtah7sTHIAtpUH1ZZEgKPl1Hg6J4x/dBkuk3wxPommNHaYaHREXF+fHMhBrSi - yH8agBmmECpa21SVnr7vrL+KSqfuF+GxwjSNsSR4SA== - -----END CERTIFICATE----- - """ - - static let samplePemKey = """ - -----BEGIN RSA PRIVATE KEY----- - MIIJKAIBAAKCAgEA72vMk44TxyawioAkwKscPGQEAyBmEywnzrcyda1XPkkgkroI - oj827bq0TOiQfLcQj3nrlHCKS9q6F4yxZpMUDg8P8/6zUO3FEIYhwrnH5KhM3sOu - flm56gk1lF9Ni436DDKGmvOtmtiNem8RU9i5Ih5SorlcatPIUDa4aP3Bjd/0gXit - 3slPR7BFvVTw5xHvFBtcoaORjiIkTVPJ+YiwFlugpob9phr0pudbJOQOyxoQtZQg - quPDC4+BWBK+UBiajXYyWwYTOQ73PvyO8WaFQ6xoSV0zxRrYCE4pbShJiBq5W6uj - 8Cn+jnOfw8MgNqmX2D7m+M3Yu5ypfZr262gW/FIpdPg29+kv18m/N0E0wzjGmq0c - iRXR+SIxtaQRbYUWU5cqcglXzBPhr2J264gMyDw7uhz6C32kb0wdNa2FebhLb0MB - FOp9njxtbvvPxGsRRFXTejTlv+3C4AlJHLI4JSLrXIyjdg3K15uNjZbc/Gqmr5iZ - PmqGoFkMuVMsym24V0hK/pJCCDsmgUcYxZFdRq8yP+yfmpkPilDJlOgcnZW2Dlb8 - wjWkrBzegoPozf/sZ/fmv3ZX+jHpdPv5+cSwAfwC1CJkmQRu/MnmWlSKFIhpXdr9 - L6OeTsvDxOm+O2l+M6D7AFuVzfw5r22TZZ1fBgNue9t4vbpabIfBBNQGLWMCAwEA - AQKCAgArWV9PEBhwpIaubQk6gUC5hnpbfpA8xG/os67FM79qHZ9yMZDCn6N4Y6el - jS4sBpFPCQoodD/2AAJVpTmxksu8x+lhiio5avOVTFPsh+qzce2JH/EGG4TX5Rb4 - aFEIBYrSjotknt49/RuQoW+HuOO8U7UulVUwWmwYae/1wow6/eOtVYZVoilil33p - C+oaTFr3TwT0l0MRcwkTnyogrikDw09RF3vxiUvmtFkCUvCCwZNo7QsFJfv4qeEH - a01d/zZsiowPgwgT+qu1kdDn0GIsoJi5P9DRzUx0JILHqtW1ePE6sdca8t+ON00k - Cr5YZ1iA5NK5Fbw6K+FcRqSSduRCLYXAnI5GH1zWMki5TUdl+psvCnpdZK5wysGe - tYfIbrVHXIlg7J3R4BrbMF4q3HwOppTHMrqsGyRVCCSjDwXjreugInV0CRzlapDs - JNEVyrbt6Ild6ie7c1AJqTpibJ9lVYRVpG35Dni9RJy5Uk5m89uWnF9PCjCRCHOf - 4UATY+qie6wlu0E8y43LcTvDi8ROXQQoCnys2ES8DmS+GKJ1uzG1l8jx3jF9BMAJ - kyzZfSmPwuS2NUk8sftYQ8neJSgk4DOV4h7x5ghaBWYzseomy3uo3gD4IyuiO56K - y7IYZnXSt2s8LfzhVcB5I4IZbSIvP/MAEkGMC09SV+dEcEJSQQKCAQEA/uJex1ef - g+q4gb/C4/biPr+ZRFheVuHu49ES0DXxoxmTbosGRDPRFBLwtPxCLuzHXa1Du2Vc - c0E12zLy8wNczv5bGAxynPo57twJCyeptFNFJkb+0uxRrCi+CZ56Qertg2jr460Q - cg+TMYxauDleLzR7uwL6VnOhTSq3CVTA2TrQ+kjIHgVqmmpwgk5bPBRDj2EuqdyD - dEQmt4z/0fFFBmW6iBcXS9y8Q1rCnAHKjDUEoXKyJYL85szupjUuerOt6iTIe7CJ - pH0REwQO4djwM4Ju/PEGfBs+RqgNXoHmBMcFdf9RdogCuFit7lX0+LlRT/KJitan - LaaFgY1TXTVkcwKCAQEA8HgZuPGVHQTMHCOfNesXxnCY9Dwqa9ZVukqDLMaZ0TVy - PIqXhdNeVCWpP+VXWhj9JRLNuW8VWYMxk+poRmsZgbdwSbq30ljsGlfoupCpXfhd - AIhUeRwLVl4XnaHW+MjAmY/rqO156/LvNbV5e0YsqObzynlTczmhhYwi48x1tdf0 - iuCn8o3+Ikv8xM7MuMnv5QmGp2l8Q3BhwxLN1x4MXfbG+4BGsqavudIkt71RVbSb - Sp7U4Khq3UEnCekrceRLQpJykRFu11/ntPsJ0Q+fLuvuRUMg/wsq8WTuVlwLrw46 - hlRcq6S99jc9j2TbidxHyps6j8SDnEsEFHMHH8THUQKCAQAd03WN1CYZdL0UidEP - hhNhjmAsDD814Yhn5k5SSQ22rUaAWApqrrmXpMPAGgjQnuqRfrX/VtQjtIzN0r91 - Sn5wxnj4bnR3BB0FY4A3avPD4z6jRQmKuxavk7DxRTc/QXN7vipkYRscjdAGq0ru - ZeAsm/Kipq2Oskc81XPHxsAua2CK+TtZr/6ShUQXK34noKNrQs8IF4LWdycksX46 - Hgaawgq65CDYwsLRCuzc/qSqFYYuMlLAavyXMYH3tx9yQlZmoNlJCBaDRhNaa04m - hZFOJcRBGx9MJI/8CqxN09uL0ZJFBZSNz0qqMc5gpnRdKqpmNZZ8xbOYdvUGfPg1 - XwsbAoIBAGdH7iRU/mp8SP48/oC1/HwqmEcuIDo40JE2t6hflGkav3npPLMp2XXi - xxK+egokeXWW4e0nHNBZXM3e+/JixY3FL+E65QDfWGjoIPkgcN3/clJsO3vY47Ww - rAv0GtS3xKEwA1OGy7rfmIZE72xW84+HwmXQPltbAVjOm52jj1sO6eVMIFY5TlGE - uYf+Gkez0+lXchItaEW+2v5h8S7XpRAmkcgrjDHnDcqNy19vXKOm8pvWJDBppZxq - A05qa1J7byekprhP+H9gnbBJsimsv/3zL19oOZ/ROBx98S/+ULZbMh/H1BWUqFI7 - 36Da/L/1cJBAo6JkEPLr9VCjJwgqCEECggEBAI6+35Lf4jDwRPvZV7kE+FQuFp1G - /tKxIJtPOZU3sbOVlsFsOoyEfV6+HbpeWxlWnrOnKRFOLoC3s5MVTjPglu1rC0ZX - 4b0wMetvun5S1MGadB808rvu5EsEB1vznz1vOXV8oDdkdgBiiUcKewSeCrG1IrXy - B9ux859S3JjELzeuNdz+xHqu2AqR22gtqN72tJUEQ95qLGZ8vo+ytY9MDVDqoSWJ - 9pqHXFUVLmwHTM0/pciXN4Kx1IL9FZ3fjXgME0vdYpWYQkcvSKLsswXN+LnYcpoQ - h33H/Kz4yji7jPN6Uk9wMyG7XGqpjYAuKCd6V3HEHUiGJZzho/VBgb3TVnw= - -----END RSA PRIVATE KEY----- - """ - - func testServerTLSConfig_Defaults() throws { - let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( - certificateChain: [ - .bytes(Array(Self.samplePemCert.utf8), format: .pem) - ], - privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) - ) - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual( - nioSSLTLSConfig.certificateChain, - [ - .certificate( - try NIOSSLCertificate( - bytes: Array(Self.samplePemCert.utf8), - format: .pem - ) - ) - ] - ) - XCTAssertEqual( - nioSSLTLSConfig.privateKey, - .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) - ) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .none) - XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - - func testServerTLSConfig_mTLS() throws { - let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.mTLS( - certificateChain: [ - .bytes(Array(Self.samplePemCert.utf8), format: .pem) - ], - privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) - ) - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual( - nioSSLTLSConfig.certificateChain, - [ - .certificate( - try NIOSSLCertificate( - bytes: Array(Self.samplePemCert.utf8), - format: .pem - ) - ) - ] - ) - XCTAssertEqual( - nioSSLTLSConfig.privateKey, - .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) - ) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .noHostnameVerification) - XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - - func testServerTLSConfig_FullVerifyClient() throws { - var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( - certificateChain: [ - .bytes(Array(Self.samplePemCert.utf8), format: .pem) - ], - privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) - ) - grpcTLSConfig.clientCertificateVerification = .fullVerification - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual( - nioSSLTLSConfig.certificateChain, - [ - .certificate( - try NIOSSLCertificate( - bytes: Array(Self.samplePemCert.utf8), - format: .pem - ) - ) - ] - ) - XCTAssertEqual( - nioSSLTLSConfig.privateKey, - .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) - ) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) - XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - - func testServerTLSConfig_CustomTrustRoots() throws { - var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults( - certificateChain: [ - .bytes(Array(Self.samplePemCert.utf8), format: .pem) - ], - privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem) - ) - grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.utf8), format: .pem)]) - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual( - nioSSLTLSConfig.certificateChain, - [ - .certificate( - try NIOSSLCertificate( - bytes: Array(Self.samplePemCert.utf8), - format: .pem - ) - ) - ] - ) - XCTAssertEqual( - nioSSLTLSConfig.privateKey, - .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) - ) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .none) - XCTAssertEqual( - nioSSLTLSConfig.trustRoots, - .certificates(try NIOSSLCertificate.fromPEMBytes(Array(Self.samplePemCert.utf8))) - ) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - - func testClientTLSConfig_Defaults() throws { - let grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) - XCTAssertNil(nioSSLTLSConfig.privateKey) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) - XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - - func testClientTLSConfig_CustomCertificateChainAndPrivateKey() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults - grpcTLSConfig.certificateChain = [ - .bytes(Array(Self.samplePemCert.utf8), format: .pem) - ] - grpcTLSConfig.privateKey = .bytes(Array(Self.samplePemKey.utf8), format: .pem) - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual( - nioSSLTLSConfig.certificateChain, - [ - .certificate( - try NIOSSLCertificate( - bytes: Array(Self.samplePemCert.utf8), - format: .pem - ) - ) - ] - ) - XCTAssertEqual( - nioSSLTLSConfig.privateKey, - .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem)) - ) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) - XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - - func testClientTLSConfig_CustomTrustRoots() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults - grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.utf8), format: .pem)]) - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) - XCTAssertNil(nioSSLTLSConfig.privateKey) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification) - XCTAssertEqual( - nioSSLTLSConfig.trustRoots, - .certificates(try NIOSSLCertificate.fromPEMBytes(Array(Self.samplePemCert.utf8))) - ) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - - func testClientTLSConfig_CustomCertificateVerification() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults - grpcTLSConfig.serverCertificateVerification = .noHostnameVerification - let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) - - XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) - XCTAssertNil(nioSSLTLSConfig.privateKey) - XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12) - XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .noHostnameVerification) - XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default) - XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"]) - } - #endif -} diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift deleted file mode 100644 index 2afe9e9fa..000000000 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOTransportServicesTests.swift +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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. - */ - -#if canImport(Network) -import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOTransportServices -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class HTTP2TransportNIOTransportServicesTests: XCTestCase { - private static let p12bundleURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // (this file) - .deletingLastPathComponent() // GRPCHTTP2TransportTests - .deletingLastPathComponent() // Tests - .appendingPathComponent("Sources") - .appendingPathComponent("GRPCSampleData") - .appendingPathComponent("bundle") - .appendingPathExtension("p12") - - @Sendable private static func loadIdentity() throws -> SecIdentity { - let data = try Data(contentsOf: Self.p12bundleURL) - - var externalFormat = SecExternalFormat.formatUnknown - var externalItemType = SecExternalItemType.itemTypeUnknown - let passphrase = "password" as CFTypeRef - var exportKeyParams = SecItemImportExportKeyParameters() - exportKeyParams.passphrase = Unmanaged.passUnretained(passphrase) - var items: CFArray? - - let status = SecItemImport( - data as CFData, - "bundle.p12" as CFString, - &externalFormat, - &externalItemType, - SecItemImportExportFlags(rawValue: 0), - &exportKeyParams, - nil, - &items - ) - - if status != errSecSuccess { - XCTFail( - """ - Unable to load identity from '\(Self.p12bundleURL)'. \ - SecItemImport failed with status \(status) - """ - ) - } else if items == nil { - XCTFail( - """ - Unable to load identity from '\(Self.p12bundleURL)'. \ - SecItemImport failed. - """ - ) - } - - return ((items! as NSArray)[0] as! SecIdentity) - } - - func testGetListeningAddress_IPv4() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .ipv4(host: "0.0.0.0", port: 0), - config: .defaults(transportSecurity: .plaintext) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - let address = try await transport.listeningAddress - let ipv4Address = try XCTUnwrap(address.ipv4) - XCTAssertNotEqual(ipv4Address.port, 0) - transport.beginGracefulShutdown() - } - } - } - - func testGetListeningAddress_IPv6() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .ipv6(host: "::1", port: 0), - config: .defaults(transportSecurity: .plaintext) - ) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - let address = try await transport.listeningAddress - let ipv6Address = try XCTUnwrap(address.ipv6) - XCTAssertNotEqual(ipv6Address.port, 0) - transport.beginGracefulShutdown() - } - } - } - - func testGetListeningAddress_UnixDomainSocket() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .unixDomainSocket(path: "/tmp/niots-uds-test"), - config: .defaults(transportSecurity: .plaintext) - ) - defer { - // NIOTS does not unlink the UDS on close. - try? FileManager.default.removeItem(atPath: "/tmp/niots-uds-test") - } - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - let address = try await transport.listeningAddress - XCTAssertEqual( - address.unixDomainSocket, - GRPCHTTP2Core.SocketAddress.UnixDomainSocket(path: "/tmp/niots-uds-test") - ) - transport.beginGracefulShutdown() - } - } - } - - func testGetListeningAddress_InvalidAddress() async { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"), - config: .defaults(transportSecurity: .plaintext) - ) - - try? await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - } - - group.addTask { - do { - _ = try await transport.listeningAddress - XCTFail("Should have thrown a RuntimeError") - } catch let error as RuntimeError { - XCTAssertEqual(error.code, .serverIsStopped) - XCTAssertEqual( - error.message, - """ - There is no listening address bound for this server: there may have \ - been an error which caused the transport to close, or it may have shut down. - """ - ) - } - } - } - } - - func testGetListeningAddress_StoppedListening() async throws { - let transport = GRPCHTTP2Core.HTTP2ServerTransport.TransportServices( - address: .ipv4(host: "0.0.0.0", port: 0), - config: .defaults(transportSecurity: .plaintext) - ) - - try? await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await transport.listen { _, _ in } - - do { - _ = try await transport.listeningAddress - XCTFail("Should have thrown a RuntimeError") - } catch let error as RuntimeError { - XCTAssertEqual(error.code, .serverIsStopped) - XCTAssertEqual( - error.message, - """ - There is no listening address bound for this server: there may have \ - been an error which caused the transport to close, or it may have shut down. - """ - ) - } - } - - group.addTask { - let address = try await transport.listeningAddress - XCTAssertNotNil(address.ipv4) - transport.beginGracefulShutdown() - } - } - } - - func testServerConfig_Defaults() throws { - let identityProvider = Self.loadIdentity - let grpcTLSConfig = HTTP2ServerTransport.TransportServices.Config.TLS.defaults( - identityProvider: identityProvider - ) - let grpcConfig = HTTP2ServerTransport.TransportServices.Config.defaults( - transportSecurity: .tls(grpcTLSConfig) - ) - - XCTAssertEqual(grpcConfig.compression, HTTP2ServerTransport.Config.Compression.defaults) - XCTAssertEqual(grpcConfig.connection, HTTP2ServerTransport.Config.Connection.defaults) - XCTAssertEqual(grpcConfig.http2, HTTP2ServerTransport.Config.HTTP2.defaults) - XCTAssertEqual(grpcConfig.rpc, HTTP2ServerTransport.Config.RPC.defaults) - XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider()) - XCTAssertEqual(grpcTLSConfig.requireALPN, false) - } - - func testClientConfig_Defaults() throws { - let identityProvider = Self.loadIdentity - let grpcTLSConfig = HTTP2ClientTransport.TransportServices.Config.TLS( - identityProvider: identityProvider - ) - let grpcConfig = HTTP2ClientTransport.TransportServices.Config.defaults( - transportSecurity: .tls(grpcTLSConfig) - ) - - XCTAssertEqual(grpcConfig.compression, HTTP2ClientTransport.Config.Compression.defaults) - XCTAssertEqual(grpcConfig.connection, HTTP2ClientTransport.Config.Connection.defaults) - XCTAssertEqual(grpcConfig.http2, HTTP2ClientTransport.Config.HTTP2.defaults) - XCTAssertEqual(grpcConfig.backoff, HTTP2ClientTransport.Config.Backoff.defaults) - XCTAssertEqual(try grpcTLSConfig.identityProvider(), try identityProvider()) - } -} -#endif diff --git a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift b/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift deleted file mode 100644 index 108cc709a..000000000 --- a/Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift +++ /dev/null @@ -1,1507 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix -import GRPCHTTP2TransportNIOTransportServices -import GRPCProtobuf -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class HTTP2TransportTests: XCTestCase { - // A combination of client and server transport kinds. - struct Transport: Sendable, CustomStringConvertible { - var server: Kind - var client: Kind - - enum Kind: Sendable, CustomStringConvertible { - case posix - case niots - - var description: String { - switch self { - case .posix: - return "NIOPosix" - case .niots: - return "NIOTS" - } - } - } - - var description: String { - "server=\(self.server) client=\(self.client)" - } - } - - func forEachTransportPair( - _ transport: [Transport] = .supported, - enableControlService: Bool = true, - clientCompression: CompressionAlgorithm = .none, - clientEnabledCompression: CompressionAlgorithmSet = .none, - serverCompression: CompressionAlgorithmSet = .none, - _ execute: (ControlClient, Transport) async throws -> Void - ) async throws { - for pair in transport { - try await withThrowingTaskGroup(of: Void.self) { group in - let (server, address) = try await self.runServer( - in: &group, - kind: pair.server, - enableControlService: enableControlService, - compression: serverCompression - ) - - let target: any ResolvableTarget - if let ipv4 = address.ipv4 { - target = .ipv4(host: ipv4.host, port: ipv4.port) - } else if let ipv6 = address.ipv6 { - target = .ipv6(host: ipv6.host, port: ipv6.port) - } else if let uds = address.unixDomainSocket { - target = .unixDomainSocket(path: uds.path) - } else { - XCTFail("Unexpected address to connect to") - return - } - - let client = try self.makeClient( - kind: pair.client, - target: target, - compression: clientCompression, - enabledCompression: clientEnabledCompression - ) - - group.addTask { - try await client.run() - } - - do { - let control = ControlClient(wrapping: client) - try await execute(control, pair) - } catch { - XCTFail("Unexpected error: '\(error)' (\(pair))") - } - - server.beginGracefulShutdown() - client.beginGracefulShutdown() - } - } - } - - func forEachClientAndHTTPStatusCodeServer( - _ kind: [Transport.Kind] = [.posix, .niots], - _ execute: (ControlClient, Transport.Kind) async throws -> Void - ) async throws { - for clientKind in kind { - try await withThrowingTaskGroup(of: Void.self) { group in - let server = HTTP2StatusCodeServer() - group.addTask { - try await server.run() - } - - let address = try await server.listeningAddress - let client = try self.makeClient( - kind: clientKind, - target: .ipv4(host: address.host, port: address.port), - compression: .none, - enabledCompression: .none - ) - group.addTask { - try await client.run() - } - - do { - let control = ControlClient(wrapping: client) - try await execute(control, clientKind) - } catch { - XCTFail("Unexpected error: '\(error)' (\(clientKind))") - } - - group.cancelAll() - } - } - } - - private func runServer( - in group: inout ThrowingTaskGroup, - kind: Transport.Kind, - enableControlService: Bool, - compression: CompressionAlgorithmSet - ) async throws -> (GRPCServer, GRPCHTTP2Core.SocketAddress) { - let services = enableControlService ? [ControlService()] : [] - - switch kind { - case .posix: - let server = GRPCServer( - transport: .http2NIOPosix( - address: .ipv4(host: "127.0.0.1", port: 0), - config: .defaults(transportSecurity: .plaintext) { - $0.compression.enabledAlgorithms = compression - } - ), - services: services - ) - - group.addTask { - try await server.serve() - } - - let address = try await server.listeningAddress! - return (server, address) - - case .niots: - #if canImport(Network) - let server = GRPCServer( - transport: .http2NIOTS( - address: .ipv4(host: "127.0.0.1", port: 0), - config: .defaults(transportSecurity: .plaintext) { - $0.compression.enabledAlgorithms = compression - } - ), - services: services - ) - - group.addTask { - try await server.serve() - } - - let address = try await server.listeningAddress! - return (server, address) - #else - throw XCTSkip("Transport not supported on this platform") - #endif - } - } - - private func makeClient( - kind: Transport.Kind, - target: any ResolvableTarget, - compression: CompressionAlgorithm, - enabledCompression: CompressionAlgorithmSet - ) throws -> GRPCClient { - let transport: any ClientTransport - - switch kind { - case .posix: - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - transport = try HTTP2ClientTransport.Posix( - target: target, - config: .defaults(transportSecurity: .plaintext) { - $0.compression.algorithm = compression - $0.compression.enabledAlgorithms = enabledCompression - }, - serviceConfig: serviceConfig - ) - - case .niots: - #if canImport(Network) - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] - transport = try HTTP2ClientTransport.TransportServices( - target: target, - config: .defaults(transportSecurity: .plaintext) { - $0.compression.algorithm = compression - $0.compression.enabledAlgorithms = enabledCompression - }, - serviceConfig: serviceConfig - ) - #else - throw XCTSkip("Transport not supported on this platform") - #endif - } - - return GRPCClient(transport: transport) - } - - func testUnaryOK() async throws { - // Client sends one message, server sends back metadata, a single message, and an ok status with - // trailing metadata. - try await self.forEachTransportPair { control, pair in - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.echoMetadataInTrailers = true - $0.numberOfMessages = 1 - $0.messageParams = .with { - $0.content = 0 - $0.size = 1024 - } - } - - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Single(message: input, metadata: metadata) - - try await control.unary(request: request) { response in - let message = try response.message - XCTAssertEqual(message.payload, Data(repeating: 0, count: 1024), "\(pair)") - - let initial = response.metadata - XCTAssertEqual(Array(initial["echo-test-key"]), ["test-value"], "\(pair)") - - let trailing = response.trailingMetadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - } - } - - func testUnaryNotOK() async throws { - // Client sends one message, server sends back metadata, a single message, and a non-ok status - // with trailing metadata. - try await self.forEachTransportPair { control, pair in - let input = ControlInput.with { - $0.echoMetadataInTrailers = true - $0.numberOfMessages = 1 - $0.messageParams = .with { - $0.content = 0 - $0.size = 1024 - } - $0.status = .with { - $0.code = .aborted - $0.message = "\(#function)" - } - } - - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Single(message: input, metadata: metadata) - - try await control.unary(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in - XCTAssertEqual(error.code, .aborted) - XCTAssertEqual(error.message, "\(#function)") - - let trailing = error.metadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - - let trailing = response.trailingMetadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - } - } - - func testUnaryRejected() async throws { - // Client sends one message, server sends non-ok status with trailing metadata. - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Single( - message: .trailersOnly(code: .aborted, message: "\(#function)", echoMetadata: true), - metadata: metadata - ) - - try await control.unary(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in - XCTAssertEqual(error.code, .aborted, "\(pair)") - XCTAssertEqual(error.message, "\(#function)", "\(pair)") - - let trailing = error.metadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - - // No initial metadata for trailers-only. - XCTAssertEqual(response.metadata, [:]) - - let trailing = response.trailingMetadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - } - } - - func testClientStreamingOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: metadata - ) { writer in - try await writer.write(.echoMetadata) - // Send a few messages which are ignored. - try await writer.write(.noOp) - try await writer.write(.noOp) - try await writer.write(.noOp) - // Send a message. - try await writer.write(.messages(1, repeating: 42, count: 1024)) - // ... and the final status. - try await writer.write(.status(code: .ok, message: "", echoMetadata: true)) - } - - try await control.clientStream(request: request) { response in - let message = try response.message - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") - - let initial = response.metadata - XCTAssertEqual(Array(initial["echo-test-key"]), ["test-value"], "\(pair)") - - let trailing = response.trailingMetadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - } - } - - func testClientStreamingNotOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: metadata - ) { writer in - try await writer.write(.echoMetadata) - // Send a few messages which are ignored. - try await writer.write(.noOp) - try await writer.write(.noOp) - try await writer.write(.noOp) - // Send a message. - try await writer.write(.messages(1, repeating: 42, count: 1024)) - // Send the final status. - try await writer.write(.status(code: .aborted, message: "\(#function)", echoMetadata: true)) - } - - try await control.clientStream(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in - XCTAssertEqual(error.code, .aborted, "\(pair)") - XCTAssertEqual(error.message, "\(#function)", "\(pair)") - - let trailing = error.metadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - - let initial = response.metadata - XCTAssertEqual(Array(initial["echo-test-key"]), ["test-value"], "\(pair)") - - let trailing = response.trailingMetadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - } - } - - func testClientStreamingRejected() async throws { - // Client sends one message, server sends non-ok status with trailing metadata. - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: metadata - ) { writer in - let message: ControlInput = .trailersOnly( - code: .aborted, - message: "\(#function)", - echoMetadata: true - ) - - try await writer.write(message) - } - - try await control.clientStream(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in - XCTAssertEqual(error.code, .aborted, "\(pair)") - XCTAssertEqual(error.message, "\(#function)", "\(pair)") - - let trailing = error.metadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - - // No initial metadata for trailers-only. - XCTAssertEqual(response.metadata, [:]) - - let trailing = response.trailingMetadata - XCTAssertEqual(Array(trailing["echo-test-key"]), ["test-value"], "\(pair)") - } - } - } - - func testServerStreamingOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.echoMetadataInTrailers = true - $0.numberOfMessages = 5 - $0.messageParams = .with { - $0.content = 42 - $0.size = 1024 - } - } - - let request = ClientRequest.Single(message: input, metadata: metadata) - try await control.serverStream(request: request) { response in - switch response.accepted { - case .success(let contents): - XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") - - var messagesReceived = 0 - for try await part in contents.bodyParts { - switch part { - case .message(let message): - messagesReceived += 1 - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) - case .trailingMetadata(let metadata): - XCTAssertEqual(Array(metadata["echo-test-key"]), ["test-value"], "\(pair)") - } - } - - XCTAssertEqual(messagesReceived, 5) - - case .failure(let error): - throw error - } - } - } - } - - func testServerStreamingEmptyOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - // Echo back metadata, but don't send any messages. - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.echoMetadataInTrailers = true - } - - let request = ClientRequest.Single(message: input, metadata: metadata) - try await control.serverStream(request: request) { response in - switch response.accepted { - case .success(let contents): - XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") - - for try await part in contents.bodyParts { - switch part { - case .message: - XCTFail("Unexpected message") - case .trailingMetadata(let metadata): - XCTAssertEqual(Array(metadata["echo-test-key"]), ["test-value"], "\(pair)") - } - } - - case .failure(let error): - throw error - } - } - } - } - - func testServerStreamingNotOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.echoMetadataInTrailers = true - $0.numberOfMessages = 5 - $0.messageParams = .with { - $0.content = 42 - $0.size = 1024 - } - $0.status = .with { - $0.code = .aborted - $0.message = "\(#function)" - } - } - - let request = ClientRequest.Single(message: input, metadata: metadata) - try await control.serverStream(request: request) { response in - switch response.accepted { - case .success(let contents): - XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") - - var messagesReceived = 0 - do { - for try await part in contents.bodyParts { - switch part { - case .message(let message): - messagesReceived += 1 - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) - case .trailingMetadata: - XCTFail("Unexpected trailing metadata, should be provided in RPCError") - } - } - XCTFail("Expected error to be thrown") - } catch let error as RPCError { - XCTAssertEqual(error.code, .aborted) - XCTAssertEqual(error.message, "\(#function)") - XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") - } - - XCTAssertEqual(messagesReceived, 5) - - case .failure(let error): - throw error - } - } - } - } - - func testServerStreamingEmptyNotOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.echoMetadataInTrailers = true - $0.status = .with { - $0.code = .aborted - $0.message = "\(#function)" - } - } - - let request = ClientRequest.Single(message: input, metadata: metadata) - try await control.serverStream(request: request) { response in - switch response.accepted { - case .success(let contents): - XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") - - do { - for try await _ in contents.bodyParts { - XCTFail("Unexpected message, \(pair)") - } - XCTFail("Expected error to be thrown") - } catch let error as RPCError { - XCTAssertEqual(error.code, .aborted) - XCTAssertEqual(error.message, "\(#function)") - XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") - } - - case .failure(let error): - throw error - } - } - } - } - - func testServerStreamingRejected() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Single( - message: .trailersOnly(code: .aborted, message: "\(#function)", echoMetadata: true), - metadata: metadata - ) - - try await control.serverStream(request: request) { response in - switch response.accepted { - case .success: - XCTFail("Expected RPC to be rejected \(pair)") - case .failure(let error): - XCTAssertEqual(error.code, .aborted, "\(pair)") - XCTAssertEqual(error.message, "\(#function)", "\(pair)") - XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") - } - } - } - } - - func testBidiStreamingOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: metadata - ) { writer in - try await writer.write(.echoMetadata) - // Send a few messages, each is echo'd back. - try await writer.write(.messages(1, repeating: 42, count: 1024)) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - // Send the final status. - try await writer.write(.status(code: .ok, message: "", echoMetadata: true)) - } - - try await control.bidiStream(request: request) { response in - switch response.accepted { - case .success(let contents): - XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") - - var messagesReceived = 0 - for try await part in contents.bodyParts { - switch part { - case .message(let message): - messagesReceived += 1 - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) - case .trailingMetadata(let metadata): - XCTAssertEqual(Array(metadata["echo-test-key"]), ["test-value"], "\(pair)") - } - } - XCTAssertEqual(messagesReceived, 3) - - case .failure(let error): - throw error - } - } - } - } - - func testBidiStreamingEmptyOK() async throws { - try await self.forEachTransportPair { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { _ in } - try await control.bidiStream(request: request) { response in - switch response.accepted { - case .success(let contents): - var receivedTrailingMetadata = false - for try await part in contents.bodyParts { - switch part { - case .message: - XCTFail("Unexpected message \(pair)") - case .trailingMetadata: - XCTAssertFalse(receivedTrailingMetadata, "\(pair)") - receivedTrailingMetadata = true - } - } - case .failure(let error): - throw error - } - } - } - } - - func testBidiStreamingNotOK() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: metadata - ) { writer in - try await writer.write(.echoMetadata) - // Send a few messages, each is echo'd back. - try await writer.write(.messages(1, repeating: 42, count: 1024)) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - // Send the final status. - try await writer.write(.status(code: .aborted, message: "\(#function)", echoMetadata: true)) - } - - try await control.bidiStream(request: request) { response in - switch response.accepted { - case .success(let contents): - XCTAssertEqual(Array(contents.metadata["echo-test-key"]), ["test-value"], "\(pair)") - - var messagesReceived = 0 - do { - for try await part in contents.bodyParts { - switch part { - case .message(let message): - messagesReceived += 1 - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024)) - case .trailingMetadata: - XCTFail("Trailing metadata should be provided by error") - } - } - XCTFail("Should've thrown error \(pair)") - } catch let error as RPCError { - XCTAssertEqual(error.code, .aborted) - XCTAssertEqual(error.message, "\(#function)") - XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"], "\(pair)") - } - - XCTAssertEqual(messagesReceived, 3) - - case .failure(let error): - throw error - } - } - } - } - - func testBidiStreamingRejected() async throws { - try await self.forEachTransportPair { control, pair in - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: metadata - ) { writer in - try await writer.write( - .trailersOnly( - code: .aborted, - message: "\(#function)", - echoMetadata: true - ) - ) - } - - try await control.bidiStream(request: request) { response in - switch response.accepted { - case .success: - XCTFail("Expected RPC to fail \(pair)") - case .failure(let error): - XCTAssertEqual(error.code, .aborted) - XCTAssertEqual(error.message, "\(#function)") - XCTAssertEqual(Array(error.metadata["echo-test-key"]), ["test-value"]) - } - } - } - } - - // MARK: - Not Implemented - - func testUnaryNotImplemented() async throws { - try await self.forEachTransportPair(enableControlService: false) { control, pair in - let request = ClientRequest.Single(message: ControlInput()) - try await control.unary(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in - XCTAssertEqual(error.code, .unimplemented) - } - } - } - } - - func testClientStreamingNotImplemented() async throws { - try await self.forEachTransportPair(enableControlService: false) { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { _ in } - try await control.clientStream(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.message) { error in - XCTAssertEqual(error.code, .unimplemented) - } - } - } - } - - func testServerStreamingNotImplemented() async throws { - try await self.forEachTransportPair(enableControlService: false) { control, pair in - let request = ClientRequest.Single(message: ControlInput()) - try await control.serverStream(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.accepted.get()) { error in - XCTAssertEqual(error.code, .unimplemented) - } - } - } - } - - func testBidiStreamingNotImplemented() async throws { - try await self.forEachTransportPair(enableControlService: false) { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { _ in } - try await control.bidiStream(request: request) { response in - XCTAssertThrowsError(ofType: RPCError.self, try response.accepted.get()) { error in - XCTAssertEqual(error.code, .unimplemented) - } - } - } - } - - // MARK: - Compression tests - - private func testUnaryCompression( - client: CompressionAlgorithm, - server: CompressionAlgorithm, - control: ControlClient, - pair: Transport - ) async throws { - let message = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - $0.messageParams = .with { - $0.content = 42 - $0.size = 1024 - } - } - - var options = CallOptions.defaults - options.compression = client - - try await control.unary( - request: ClientRequest.Single(message: message), - options: options - ) { response in - // Check the client algorithm. - switch client { - case .deflate, .gzip: - // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. - let encoding = Array(response.metadata["echo-grpc-encoding"]) - XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - // Check the server algorithm. - switch server { - case .deflate, .gzip: - let encoding = Array(response.metadata["grpc-encoding"]) - XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - let message = try response.message - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") - } - } - - private func testClientStreamingCompression( - client: CompressionAlgorithm, - server: CompressionAlgorithm, - control: ControlClient, - pair: Transport - ) async throws { - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - try await writer.write(.echoMetadata) - try await writer.write(.noOp) - try await writer.write(.noOp) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - } - - var options = CallOptions.defaults - options.compression = client - - try await control.clientStream(request: request, options: options) { response in - // Check the client algorithm. - switch client { - case .deflate, .gzip: - // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. - let encoding = Array(response.metadata["echo-grpc-encoding"]) - XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - // Check the server algorithm. - switch server { - case .deflate, .gzip: - let encoding = Array(response.metadata["grpc-encoding"]) - XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - let message = try response.message - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") - } - } - - private func testServerStreamingCompression( - client: CompressionAlgorithm, - server: CompressionAlgorithm, - control: ControlClient, - pair: Transport - ) async throws { - let message = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 5 - $0.messageParams = .with { - $0.content = 42 - $0.size = 1024 - } - } - - var options = CallOptions.defaults - options.compression = client - - try await control.serverStream( - request: ClientRequest.Single(message: message), - options: options - ) { response in - // Check the client algorithm. - switch client { - case .deflate, .gzip: - // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. - let encoding = Array(response.metadata["echo-grpc-encoding"]) - XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - // Check the server algorithm. - switch server { - case .deflate, .gzip: - let encoding = Array(response.metadata["grpc-encoding"]) - XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - for try await message in response.messages { - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") - } - } - } - - private func testBidiStreamingCompression( - client: CompressionAlgorithm, - server: CompressionAlgorithm, - control: ControlClient, - pair: Transport - ) async throws { - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - try await writer.write(.echoMetadata) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - try await writer.write(.messages(1, repeating: 42, count: 1024)) - } - - var options = CallOptions.defaults - options.compression = client - - try await control.bidiStream(request: request, options: options) { response in - // Check the client algorithm. - switch client { - case .deflate, .gzip: - // "echo-grpc-encoding" is the value of "grpc-encoding" sent from the client to the server. - let encoding = Array(response.metadata["echo-grpc-encoding"]) - XCTAssertEqual(encoding, ["\(client.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - // Check the server algorithm. - switch server { - case .deflate, .gzip: - let encoding = Array(response.metadata["grpc-encoding"]) - XCTAssertEqual(encoding, ["\(server.name)"], "\(pair)") - case .none: - () - default: - XCTFail("Unhandled compression '\(client)'") - } - - for try await message in response.messages { - XCTAssertEqual(message.payload, Data(repeating: 42, count: 1024), "\(pair)") - } - } - } - - func testUnaryDeflateCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .deflate, - clientEnabledCompression: .deflate, - serverCompression: .deflate - ) { control, pair in - try await self.testUnaryCompression( - client: .deflate, - server: .deflate, - control: control, - pair: pair - ) - } - } - - func testUnaryGzipCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .gzip, - clientEnabledCompression: .gzip, - serverCompression: .gzip - ) { control, pair in - try await self.testUnaryCompression( - client: .gzip, - server: .gzip, - control: control, - pair: pair - ) - } - } - - func testClientStreamingDeflateCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .deflate, - clientEnabledCompression: .deflate, - serverCompression: .deflate - ) { control, pair in - try await self.testClientStreamingCompression( - client: .deflate, - server: .deflate, - control: control, - pair: pair - ) - } - } - - func testClientStreamingGzipCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .gzip, - clientEnabledCompression: .gzip, - serverCompression: .gzip - ) { control, pair in - try await self.testClientStreamingCompression( - client: .gzip, - server: .gzip, - control: control, - pair: pair - ) - } - } - - func testServerStreamingDeflateCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .deflate, - clientEnabledCompression: .deflate, - serverCompression: .deflate - ) { control, pair in - try await self.testServerStreamingCompression( - client: .deflate, - server: .deflate, - control: control, - pair: pair - ) - } - } - - func testServerStreamingGzipCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .gzip, - clientEnabledCompression: .gzip, - serverCompression: .gzip - ) { control, pair in - try await self.testServerStreamingCompression( - client: .gzip, - server: .gzip, - control: control, - pair: pair - ) - } - } - - func testBidiStreamingDeflateCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .deflate, - clientEnabledCompression: .deflate, - serverCompression: .deflate - ) { control, pair in - try await self.testBidiStreamingCompression( - client: .deflate, - server: .deflate, - control: control, - pair: pair - ) - } - } - - func testBidiStreamingGzipCompression() async throws { - try await self.forEachTransportPair( - clientCompression: .gzip, - clientEnabledCompression: .gzip, - serverCompression: .gzip - ) { control, pair in - try await self.testBidiStreamingCompression( - client: .gzip, - server: .gzip, - control: control, - pair: pair - ) - } - } - - func testUnaryUnsupportedCompression() async throws { - try await self.forEachTransportPair( - clientEnabledCompression: .all, - serverCompression: .gzip - ) { control, pair in - let message = ControlInput.with { - $0.numberOfMessages = 1 - $0.messageParams = .with { - $0.content = 42 - $0.size = 1024 - } - } - let request = ClientRequest.Single(message: message) - - var options = CallOptions.defaults - options.compression = .deflate - try await control.unary(request: request, options: options) { response in - switch response.accepted { - case .success: - XCTFail("RPC should've been rejected") - case .failure(let error): - let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) - // "identity" may or may not be included, so only test for values which must be present. - XCTAssertTrue(acceptEncoding.contains("gzip")) - XCTAssertFalse(acceptEncoding.contains("deflate")) - } - } - } - } - - func testClientStreamingUnsupportedCompression() async throws { - try await self.forEachTransportPair( - clientEnabledCompression: .all, - serverCompression: .gzip - ) { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - try await writer.write(.noOp) - } - - var options = CallOptions.defaults - options.compression = .deflate - try await control.clientStream(request: request, options: options) { response in - switch response.accepted { - case .success: - XCTFail("RPC should've been rejected") - case .failure(let error): - let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) - // "identity" may or may not be included, so only test for values which must be present. - XCTAssertTrue(acceptEncoding.contains("gzip")) - XCTAssertFalse(acceptEncoding.contains("deflate")) - } - } - } - } - - func testServerStreamingUnsupportedCompression() async throws { - try await self.forEachTransportPair( - clientEnabledCompression: .all, - serverCompression: .gzip - ) { control, pair in - let message = ControlInput.with { - $0.numberOfMessages = 1 - $0.messageParams = .with { - $0.content = 42 - $0.size = 1024 - } - } - let request = ClientRequest.Single(message: message) - - var options = CallOptions.defaults - options.compression = .deflate - try await control.serverStream(request: request, options: options) { response in - switch response.accepted { - case .success: - XCTFail("RPC should've been rejected") - case .failure(let error): - let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) - // "identity" may or may not be included, so only test for values which must be present. - XCTAssertTrue(acceptEncoding.contains("gzip")) - XCTAssertFalse(acceptEncoding.contains("deflate")) - } - } - } - } - - func testBidiStreamingUnsupportedCompression() async throws { - try await self.forEachTransportPair( - clientEnabledCompression: .all, - serverCompression: .gzip - ) { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - try await writer.write(.noOp) - } - - var options = CallOptions.defaults - options.compression = .deflate - try await control.bidiStream(request: request, options: options) { response in - switch response.accepted { - case .success: - XCTFail("RPC should've been rejected") - case .failure(let error): - let acceptEncoding = Array(error.metadata["grpc-accept-encoding"]) - // "identity" may or may not be included, so only test for values which must be present. - XCTAssertTrue(acceptEncoding.contains("gzip")) - XCTAssertFalse(acceptEncoding.contains("deflate")) - } - } - } - } - - func testUnaryTimeoutPropagatedToServer() async throws { - try await self.forEachTransportPair { control, pair in - let message = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - } - - let request = ClientRequest.Single(message: message) - var options = CallOptions.defaults - options.timeout = .seconds(10) - try await control.unary(request: request, options: options) { response in - let timeout = Array(response.metadata["echo-grpc-timeout"]) - XCTAssertEqual(timeout.count, 1) - } - } - } - - func testClientStreamingTimeoutPropagatedToServer() async throws { - try await self.forEachTransportPair { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - let message = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - } - try await writer.write(message) - } - - var options = CallOptions.defaults - options.timeout = .seconds(10) - try await control.clientStream(request: request, options: options) { response in - let timeout = Array(response.metadata["echo-grpc-timeout"]) - XCTAssertEqual(timeout.count, 1) - } - } - } - - func testServerStreamingTimeoutPropagatedToServer() async throws { - try await self.forEachTransportPair { control, pair in - let message = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - } - - let request = ClientRequest.Single(message: message) - var options = CallOptions.defaults - options.timeout = .seconds(10) - try await control.serverStream(request: request, options: options) { response in - let timeout = Array(response.metadata["echo-grpc-timeout"]) - XCTAssertEqual(timeout.count, 1) - } - } - } - - func testBidiStreamingTimeoutPropagatedToServer() async throws { - try await self.forEachTransportPair { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - try await writer.write(.echoMetadata) - } - - var options = CallOptions.defaults - options.timeout = .seconds(10) - try await control.bidiStream(request: request, options: options) { response in - let timeout = Array(response.metadata["echo-grpc-timeout"]) - XCTAssertEqual(timeout.count, 1) - } - } - } - - private static let httpToStatusCodePairs: [(Int, RPCError.Code)] = [ - // See https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - (400, .internalError), - (401, .unauthenticated), - (403, .permissionDenied), - (404, .unimplemented), - (418, .unknown), - (429, .unavailable), - (502, .unavailable), - (503, .unavailable), - (504, .unavailable), - (504, .unavailable), - ] - - func testUnaryAgainstNonGRPCServer() async throws { - try await self.forEachClientAndHTTPStatusCodeServer { control, kind in - for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { - // Tell the server what to respond with. - let metadata: Metadata = ["response-status": "\(httpCode)"] - - try await control.unary( - request: ClientRequest.Single(message: .noOp, metadata: metadata) - ) { response in - switch response.accepted { - case .success: - XCTFail("RPC should have failed with '\(expectedStatus)'") - case .failure(let error): - XCTAssertEqual(error.code, expectedStatus) - } - } - } - } - } - - func testClientStreamingAgainstNonGRPCServer() async throws { - try await self.forEachClientAndHTTPStatusCodeServer { control, kind in - for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { - // Tell the server what to respond with. - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: ["response-status": "\(httpCode)"] - ) { _ in - } - - try await control.clientStream(request: request) { response in - switch response.accepted { - case .success: - XCTFail("RPC should have failed with '\(expectedStatus)'") - case .failure(let error): - XCTAssertEqual(error.code, expectedStatus) - } - } - } - } - } - - func testServerStreamingAgainstNonGRPCServer() async throws { - try await self.forEachClientAndHTTPStatusCodeServer { control, kind in - for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { - // Tell the server what to respond with. - let metadata: Metadata = ["response-status": "\(httpCode)"] - - try await control.serverStream( - request: ClientRequest.Single(message: .noOp, metadata: metadata) - ) { response in - switch response.accepted { - case .success: - XCTFail("RPC should have failed with '\(expectedStatus)'") - case .failure(let error): - XCTAssertEqual(error.code, expectedStatus) - } - } - } - } - } - - func testBidiStreamingAgainstNonGRPCServer() async throws { - try await self.forEachClientAndHTTPStatusCodeServer { control, kind in - for (httpCode, expectedStatus) in Self.httpToStatusCodePairs { - // Tell the server what to respond with. - let request = ClientRequest.Stream( - of: ControlInput.self, - metadata: ["response-status": "\(httpCode)"] - ) { _ in - } - - try await control.bidiStream(request: request) { response in - switch response.accepted { - case .success: - XCTFail("RPC should have failed with '\(expectedStatus)'") - case .failure(let error): - XCTAssertEqual(error.code, expectedStatus) - } - } - } - } - } - - func testUnaryScheme() async throws { - try await self.forEachTransportPair { control, pair in - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - } - let request = ClientRequest.Single(message: input) - try await control.unary(request: request) { response in - XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) - } - } - } - - func testServerStreamingScheme() async throws { - try await self.forEachTransportPair { control, pair in - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - } - let request = ClientRequest.Single(message: input) - try await control.serverStream(request: request) { response in - XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) - } - } - } - - func testClientStreamingScheme() async throws { - try await self.forEachTransportPair { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - } - try await writer.write(input) - } - try await control.clientStream(request: request) { response in - XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) - } - } - } - - func testBidiStreamingScheme() async throws { - try await self.forEachTransportPair { control, pair in - let request = ClientRequest.Stream(of: ControlInput.self) { writer in - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.numberOfMessages = 1 - } - try await writer.write(input) - } - try await control.bidiStream(request: request) { response in - XCTAssertEqual(Array(response.metadata["echo-scheme"]), ["http"]) - } - } - } -} - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension [HTTP2TransportTests.Transport] { - static let supported = [ - HTTP2TransportTests.Transport(server: .posix, client: .posix), - HTTP2TransportTests.Transport(server: .niots, client: .niots), - HTTP2TransportTests.Transport(server: .niots, client: .posix), - HTTP2TransportTests.Transport(server: .posix, client: .niots), - ] -} - -extension ControlInput { - fileprivate static let echoMetadata = Self.with { - $0.echoMetadataInHeaders = true - } - - fileprivate static let noOp = Self() - - fileprivate static func messages( - _ numberOfMessages: Int, - repeating: UInt8, - count: Int - ) -> Self { - return Self.with { - $0.numberOfMessages = Int32(numberOfMessages) - $0.messageParams = .with { - $0.content = UInt32(repeating) - $0.size = Int32(count) - } - } - } - - fileprivate static func status( - code: Status.Code, - message: String, - echoMetadata: Bool - ) -> Self { - return Self.with { - $0.echoMetadataInTrailers = echoMetadata - $0.status = .with { - $0.code = StatusCode(rawValue: code.rawValue)! - $0.message = message - } - } - } - - fileprivate static func trailersOnly( - code: Status.Code, - message: String, - echoMetadata: Bool - ) -> Self { - return Self.with { - $0.echoMetadataInTrailers = echoMetadata - $0.isTrailersOnly = true - $0.status = .with { - $0.code = StatusCode(rawValue: code.rawValue)! - $0.message = message - } - } - } -} - -extension CompressionAlgorithm { - var name: String { - switch self { - case .deflate: - return "deflate" - case .gzip: - return "gzip" - case .none: - return "identity" - default: - return "" - } - } -} diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift deleted file mode 100644 index d4f6e00ee..000000000 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/HTTP2StatusCodeServer.swift +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 GRPCHTTP2Core -import NIOCore -import NIOHPACK -import NIOHTTP2 -import NIOPosix - -/// An HTTP/2 test server which only responds to request headers by sending response headers and -/// then closing. Each stream will be closed with the ":status" set to the value of the -/// "response-status" header field in the request headers. -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -final class HTTP2StatusCodeServer: Sendable { - private let address: EventLoopPromise - private let eventLoopGroup: MultiThreadedEventLoopGroup - - var listeningAddress: GRPCHTTP2Core.SocketAddress.IPv4 { - get async throws { - try await self.address.futureResult.get() - } - } - - init() { - self.eventLoopGroup = .singleton - self.address = self.eventLoopGroup.next().makePromise() - } - - func run() async throws { - do { - let channel = try await ServerBootstrap(group: .singletonMultiThreadedEventLoopGroup) - .bind(host: "127.0.0.1", port: 0) { channel in - channel.eventLoop.makeCompletedFuture { - let sync = channel.pipeline.syncOperations - let multiplexer = try sync.configureAsyncHTTP2Pipeline(mode: .server) { stream in - stream.eventLoop.makeCompletedFuture { - try NIOAsyncChannel( - wrappingChannelSynchronously: stream - ) - } - } - - let wrapped = try NIOAsyncChannel( - wrappingChannelSynchronously: channel - ) - - return (wrapped, multiplexer) - } - } - - let port = channel.channel.localAddress!.port! - self.address.succeed(.init(host: "127.0.0.1", port: port)) - - try await channel.executeThenClose { inbound in - try await withThrowingTaskGroup(of: Void.self) { acceptedGroup in - for try await (accepted, mux) in inbound { - acceptedGroup.addTask { - try await withThrowingTaskGroup(of: Void.self) { connectionGroup in - // Run the connection. - connectionGroup.addTask { - try await accepted.executeThenClose { inbound, outbound in - for try await _ in inbound {} - } - } - - // Consume the streams. - for try await stream in mux.inbound { - connectionGroup.addTask { - try await stream.executeThenClose { inbound, outbound in - do { - for try await frame in inbound { - switch frame { - case .headers(let requestHeaders): - if let status = requestHeaders.headers.first(name: "response-status") { - let headers: HPACKHeaders = [":status": "\(status)"] - try await outbound.write( - .headers(.init(headers: headers, endStream: true)) - ) - } - - default: - () // Ignore the others - } - } - } catch { - // Ignore errors - } - } - } - } - } - } - } - } - } - } catch { - self.address.fail(error) - } - } -} diff --git a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift deleted file mode 100644 index 3848388bc..000000000 --- a/Tests/GRPCHTTP2TransportTests/Test Utilities/XCTest+Utilities.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 XCTest - -func XCTAssertThrowsError( - ofType: E.Type, - _ expression: @autoclosure () throws -> T, - _ errorHandler: (E) -> Void -) { - XCTAssertThrowsError(try expression()) { error in - guard let error = error as? E else { - return XCTFail("Error had unexpected type '\(type(of: error))'") - } - errorHandler(error) - } -} diff --git a/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift b/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift deleted file mode 100644 index cb613fb63..000000000 --- a/Tests/GRPCHTTP2TransportTests/XCTestCase+Vsock.swift +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 NIOPosix -import XCTest - -extension XCTestCase { - func vsockAvailable() -> Bool { - let fd: CInt - #if os(Linux) - fd = socket(AF_VSOCK, CInt(SOCK_STREAM.rawValue), 0) - #elseif canImport(Darwin) - fd = socket(AF_VSOCK, SOCK_STREAM, 0) - #else - fd = -1 - #endif - if fd == -1 { return false } - precondition(close(fd) == 0) - return true - } -} diff --git a/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift b/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift deleted file mode 100644 index 5535c2d6c..000000000 --- a/Tests/GRPCInterceptorsTests/TracingInterceptorTests.swift +++ /dev/null @@ -1,334 +0,0 @@ -/* - * 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 Tracing -import XCTest - -@testable import GRPCInterceptors - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class TracingInterceptorTests: XCTestCase { - override class func setUp() { - InstrumentationSystem.bootstrap(TestTracer()) - } - - func testClientInterceptor() async throws { - var serviceContext = ServiceContext.topLevel - let traceIDString = UUID().uuidString - let interceptor = ClientTracingInterceptor(emitEventOnEachWrite: false) - let (stream, continuation) = AsyncStream.makeStream() - serviceContext.traceID = traceIDString - - try await ServiceContext.withValue(serviceContext) { - let methodDescriptor = MethodDescriptor( - service: "TracingInterceptorTests", - method: "testClientInterceptor" - ) - let response = try await interceptor.intercept( - request: .init(producer: { writer in - try await writer.write(contentsOf: ["request1"]) - try await writer.write(contentsOf: ["request2"]) - }), - context: .init(descriptor: methodDescriptor) - ) { stream, _ in - // Assert the metadata contains the injected context key-value. - XCTAssertEqual(stream.metadata, ["trace-id": "\(traceIDString)"]) - - // Write into the response stream to make sure the `producer` closure's called. - let writer = RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) - try await stream.producer(writer) - continuation.finish() - - return .init( - metadata: [], - bodyParts: RPCAsyncSequence( - wrapping: AsyncThrowingStream { - $0.yield(.message(["response"])) - $0.finish() - } - ) - ) - } - - var streamIterator = stream.makeAsyncIterator() - var element = await streamIterator.next() - XCTAssertEqual(element, "request1") - element = await streamIterator.next() - XCTAssertEqual(element, "request2") - element = await streamIterator.next() - XCTAssertNil(element) - - var messages = response.messages.makeAsyncIterator() - var message = try await messages.next() - XCTAssertEqual(message, ["response"]) - message = try await messages.next() - XCTAssertNil(message) - - let tracer = InstrumentationSystem.tracer as! TestTracer - XCTAssertEqual( - tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { - $0.name - }, - [ - "Request started", - "Received response end", - ] - ) - } - } - - func testClientInterceptorAllEventsRecorded() async throws { - let methodDescriptor = MethodDescriptor( - service: "TracingInterceptorTests", - method: "testClientInterceptorAllEventsRecorded" - ) - var serviceContext = ServiceContext.topLevel - let traceIDString = UUID().uuidString - let interceptor = ClientTracingInterceptor(emitEventOnEachWrite: true) - let (stream, continuation) = AsyncStream.makeStream() - serviceContext.traceID = traceIDString - - try await ServiceContext.withValue(serviceContext) { - let response = try await interceptor.intercept( - request: .init(producer: { writer in - try await writer.write(contentsOf: ["request1"]) - try await writer.write(contentsOf: ["request2"]) - }), - context: .init(descriptor: methodDescriptor) - ) { stream, _ in - // Assert the metadata contains the injected context key-value. - XCTAssertEqual(stream.metadata, ["trace-id": "\(traceIDString)"]) - - // Write into the response stream to make sure the `producer` closure's called. - let writer = RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) - try await stream.producer(writer) - continuation.finish() - - return .init( - metadata: [], - bodyParts: RPCAsyncSequence( - wrapping: AsyncThrowingStream { - $0.yield(.message(["response"])) - $0.finish() - } - ) - ) - } - - var streamIterator = stream.makeAsyncIterator() - var element = await streamIterator.next() - XCTAssertEqual(element, "request1") - element = await streamIterator.next() - XCTAssertEqual(element, "request2") - element = await streamIterator.next() - XCTAssertNil(element) - - var messages = response.messages.makeAsyncIterator() - var message = try await messages.next() - XCTAssertEqual(message, ["response"]) - message = try await messages.next() - XCTAssertNil(message) - - let tracer = InstrumentationSystem.tracer as! TestTracer - XCTAssertEqual( - tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { - $0.name - }, - [ - "Request started", - // Recorded when `request1` is sent - "Sending request part", - "Sent request part", - // Recorded when `request2` is sent - "Sending request part", - "Sent request part", - // Recorded after all request parts have been sent - "Request end", - // Recorded when receiving response part - "Received response part", - // Recorded at end of response - "Received response end", - ] - ) - } - } - - func testServerInterceptorErrorResponse() async throws { - let methodDescriptor = MethodDescriptor( - service: "TracingInterceptorTests", - method: "testServerInterceptorErrorResponse" - ) - let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: false) - let single = ServerRequest.Single(metadata: ["trace-id": "some-trace-id"], message: [UInt8]()) - let response = try await interceptor.intercept( - request: .init(single: single), - context: .init(descriptor: methodDescriptor) - ) { _, _ in - ServerResponse.Stream(error: .init(code: .unknown, message: "Test error")) - } - XCTAssertThrowsError(try response.accepted.get()) - - let tracer = InstrumentationSystem.tracer as! TestTracer - XCTAssertEqual( - tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { - $0.name - }, - [ - "Received request start", - "Received request end", - "Sent error response", - ] - ) - } - - func testServerInterceptor() async throws { - let methodDescriptor = MethodDescriptor( - service: "TracingInterceptorTests", - method: "testServerInterceptor" - ) - let (stream, continuation) = AsyncStream.makeStream() - let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: false) - let single = ServerRequest.Single(metadata: ["trace-id": "some-trace-id"], message: [UInt8]()) - let response = try await interceptor.intercept( - request: .init(single: single), - context: .init(descriptor: methodDescriptor) - ) { _, _ in - { [serviceContext = ServiceContext.current] in - return ServerResponse.Stream( - accepted: .success( - .init( - metadata: [], - producer: { writer in - guard let serviceContext else { - XCTFail("There should be a service context present.") - return ["Result": "Test failed"] - } - - let traceID = serviceContext.traceID - XCTAssertEqual("some-trace-id", traceID) - - try await writer.write("response1") - try await writer.write("response2") - - return ["Result": "Trailing metadata"] - } - ) - ) - ) - }() - } - - let responseContents = try response.accepted.get() - let trailingMetadata = try await responseContents.producer( - RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) - ) - continuation.finish() - XCTAssertEqual(trailingMetadata, ["Result": "Trailing metadata"]) - - var streamIterator = stream.makeAsyncIterator() - var element = await streamIterator.next() - XCTAssertEqual(element, "response1") - element = await streamIterator.next() - XCTAssertEqual(element, "response2") - element = await streamIterator.next() - XCTAssertNil(element) - - let tracer = InstrumentationSystem.tracer as! TestTracer - XCTAssertEqual( - tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { - $0.name - }, - [ - "Received request start", - "Received request end", - "Sent response end", - ] - ) - } - - func testServerInterceptorAllEventsRecorded() async throws { - let methodDescriptor = MethodDescriptor( - service: "TracingInterceptorTests", - method: "testServerInterceptorAllEventsRecorded" - ) - let (stream, continuation) = AsyncStream.makeStream() - let interceptor = ServerTracingInterceptor(emitEventOnEachWrite: true) - let single = ServerRequest.Single(metadata: ["trace-id": "some-trace-id"], message: [UInt8]()) - let response = try await interceptor.intercept( - request: .init(single: single), - context: .init(descriptor: methodDescriptor) - ) { _, _ in - { [serviceContext = ServiceContext.current] in - return ServerResponse.Stream( - accepted: .success( - .init( - metadata: [], - producer: { writer in - guard let serviceContext else { - XCTFail("There should be a service context present.") - return ["Result": "Test failed"] - } - - let traceID = serviceContext.traceID - XCTAssertEqual("some-trace-id", traceID) - - try await writer.write("response1") - try await writer.write("response2") - - return ["Result": "Trailing metadata"] - } - ) - ) - ) - }() - } - - let responseContents = try response.accepted.get() - let trailingMetadata = try await responseContents.producer( - RPCWriter(wrapping: TestWriter(streamContinuation: continuation)) - ) - continuation.finish() - XCTAssertEqual(trailingMetadata, ["Result": "Trailing metadata"]) - - var streamIterator = stream.makeAsyncIterator() - var element = await streamIterator.next() - XCTAssertEqual(element, "response1") - element = await streamIterator.next() - XCTAssertEqual(element, "response2") - element = await streamIterator.next() - XCTAssertNil(element) - - let tracer = InstrumentationSystem.tracer as! TestTracer - XCTAssertEqual( - tracer.getEventsForTestSpan(ofOperationName: methodDescriptor.fullyQualifiedMethod).map { - $0.name - }, - [ - "Received request start", - "Received request end", - // Recorded when `response1` is sent - "Sending response part", - "Sent response part", - // Recorded when `response2` is sent - "Sending response part", - "Sent response part", - // Recorded when we're done sending response - "Sent response end", - ] - ) - } -} diff --git a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift b/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift deleted file mode 100644 index 218541fa2..000000000 --- a/Tests/GRPCInterceptorsTests/TracingTestsUtilities.swift +++ /dev/null @@ -1,195 +0,0 @@ -/* - * 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 NIOConcurrencyHelpers -import Tracing - -final class TestTracer: Tracer { - typealias Span = TestSpan - - private let testSpans: NIOLockedValueBox<[String: TestSpan]> = .init([:]) - - func getEventsForTestSpan(ofOperationName operationName: String) -> [SpanEvent] { - let span = self.testSpans.withLockedValue({ $0[operationName] }) - return span?.events ?? [] - } - - func extract( - _ carrier: Carrier, - into context: inout ServiceContextModule.ServiceContext, - using extractor: Extract - ) where Carrier == Extract.Carrier, Extract: Instrumentation.Extractor { - let traceID = extractor.extract(key: TraceID.keyName, from: carrier) - context[TraceID.self] = traceID - } - - func inject( - _ context: ServiceContextModule.ServiceContext, - into carrier: inout Carrier, - using injector: Inject - ) where Carrier == Inject.Carrier, Inject: Instrumentation.Injector { - if let traceID = context.traceID { - injector.inject(traceID, forKey: TraceID.keyName, into: &carrier) - } - } - - func forceFlush() { - // no-op - } - - func startSpan( - _ operationName: String, - context: @autoclosure () -> ServiceContext, - ofKind kind: SpanKind, - at instant: @autoclosure () -> Instant, - function: String, - file fileID: String, - line: UInt - ) -> TestSpan where Instant: TracerInstant { - return self.testSpans.withLockedValue { testSpans in - let span = TestSpan(context: context(), operationName: operationName) - testSpans[operationName] = span - return span - } - } -} - -struct TestSpan: Span, Sendable { - private struct State { - var context: ServiceContextModule.ServiceContext - var operationName: String - var attributes: Tracing.SpanAttributes - var status: Tracing.SpanStatus? - var events: [Tracing.SpanEvent] = [] - } - - private let state: NIOLockedValueBox - let isRecording: Bool - - var context: ServiceContextModule.ServiceContext { - self.state.withLockedValue { $0.context } - } - - var operationName: String { - get { self.state.withLockedValue { $0.operationName } } - nonmutating set { self.state.withLockedValue { $0.operationName = newValue } } - } - - var attributes: Tracing.SpanAttributes { - get { self.state.withLockedValue { $0.attributes } } - nonmutating set { self.state.withLockedValue { $0.attributes = newValue } } - } - - var events: [Tracing.SpanEvent] { - self.state.withLockedValue { $0.events } - } - - init( - context: ServiceContextModule.ServiceContext, - operationName: String, - attributes: Tracing.SpanAttributes = [:], - isRecording: Bool = true - ) { - let state = State(context: context, operationName: operationName, attributes: attributes) - self.state = NIOLockedValueBox(state) - self.isRecording = isRecording - } - - func setStatus(_ status: Tracing.SpanStatus) { - self.state.withLockedValue { $0.status = status } - } - - func addEvent(_ event: Tracing.SpanEvent) { - self.state.withLockedValue { $0.events.append(event) } - } - - func recordError( - _ error: any Error, - attributes: Tracing.SpanAttributes, - at instant: @autoclosure () -> Instant - ) where Instant: Tracing.TracerInstant { - self.setStatus( - .init( - code: .error, - message: "Error: \(error), attributes: \(attributes), at instant: \(instant())" - ) - ) - } - - func addLink(_ link: Tracing.SpanLink) { - self.state.withLockedValue { - $0.context.spanLinks?.append(link) - } - } - - func end(at instant: @autoclosure () -> Instant) where Instant: Tracing.TracerInstant { - self.setStatus(.init(code: .ok, message: "Ended at instant: \(instant())")) - } -} - -enum TraceID: ServiceContextModule.ServiceContextKey { - typealias Value = String - - static let keyName = "trace-id" -} - -enum ServiceContextSpanLinksKey: ServiceContextModule.ServiceContextKey { - typealias Value = [SpanLink] - - static let keyName = "span-links" -} - -extension ServiceContext { - var traceID: String? { - get { - self[TraceID.self] - } - set { - self[TraceID.self] = newValue - } - } - - var spanLinks: [SpanLink]? { - get { - self[ServiceContextSpanLinksKey.self] - } - set { - self[ServiceContextSpanLinksKey.self] = newValue - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -struct TestWriter: RPCWriterProtocol { - typealias Element = WriterElement - - private let streamContinuation: AsyncStream.Continuation - - init(streamContinuation: AsyncStream.Continuation) { - self.streamContinuation = streamContinuation - } - - func write(_ element: WriterElement) { - self.streamContinuation.yield(element) - } - - func write(contentsOf elements: some Sequence) { - elements.forEach { element in - self.write(element) - } - } -} diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift deleted file mode 100644 index 6e814bb8a..000000000 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ /dev/null @@ -1,443 +0,0 @@ -/* - * 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 GRPCCodeGen -import SwiftProtobuf -import SwiftProtobufPluginLibrary -import XCTest - -@testable import GRPCProtobufCodeGen - -final class ProtobufCodeGenParserTests: XCTestCase { - func testParser() throws { - let descriptorSet = DescriptorSet( - protos: [ - Google_Protobuf_FileDescriptorProto( - name: "same-module.proto", - package: "same-package" - ), - Google_Protobuf_FileDescriptorProto( - name: "different-module.proto", - package: "different-package" - ), - Google_Protobuf_FileDescriptorProto.helloWorld, - ] - ) - - guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { - return XCTFail( - """ - Could not find the file descriptor of "helloworld.proto". - """ - ) - } - let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { - $0.mapping = [ - SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { - $0.protoFilePath = ["different-module.proto"] - $0.moduleName = "DifferentModule" - } - ] - } - let parsedCodeGenRequest = try ProtobufCodeGenParser( - input: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"], - accessLevel: .internal - ).parse() - - self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) - - let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( - documentation: "/// Sends a greeting.\n", - name: CodeGenerationRequest.Name( - base: "SayHello", - generatedUpperCase: "SayHello", - generatedLowerCase: "sayHello" - ), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "Helloworld_HelloRequest", - outputType: "Helloworld_HelloReply" - ) - guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } - XCTAssertEqual(method, expectedMethod) - - let expectedService = CodeGenerationRequest.ServiceDescriptor( - documentation: "/// The greeting service definition.\n", - name: CodeGenerationRequest.Name( - base: "Greeter", - generatedUpperCase: "Greeter", - generatedLowerCase: "greeter" - ), - namespace: CodeGenerationRequest.Name( - base: "helloworld", - generatedUpperCase: "Helloworld", - generatedLowerCase: "helloworld" - ), - methods: [expectedMethod] - ) - guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } - XCTAssertEqual(service, expectedService) - XCTAssertEqual(service.methods.count, 1) - - XCTAssertEqual( - parsedCodeGenRequest.lookupSerializer("Helloworld_HelloRequest"), - "GRPCProtobuf.ProtobufSerializer()" - ) - XCTAssertEqual( - parsedCodeGenRequest.lookupDeserializer("Helloworld_HelloRequest"), - "GRPCProtobuf.ProtobufDeserializer()" - ) - } - - func testParserNestedPackage() throws { - let descriptorSet = DescriptorSet( - protos: [ - Google_Protobuf_FileDescriptorProto( - name: "same-module.proto", - package: "same-package" - ), - Google_Protobuf_FileDescriptorProto( - name: "different-module.proto", - package: "different-package" - ), - Google_Protobuf_FileDescriptorProto.helloWorldNestedPackage, - ] - ) - - guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { - return XCTFail( - """ - Could not find the file descriptor of "helloworld.proto". - """ - ) - } - let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { - $0.mapping = [ - SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { - $0.protoFilePath = ["different-module.proto"] - $0.moduleName = "DifferentModule" - } - ] - } - let parsedCodeGenRequest = try ProtobufCodeGenParser( - input: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"], - accessLevel: .internal - ).parse() - - self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) - - let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( - documentation: "/// Sends a greeting.\n", - name: CodeGenerationRequest.Name( - base: "SayHello", - generatedUpperCase: "SayHello", - generatedLowerCase: "sayHello" - ), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "Hello_World_HelloRequest", - outputType: "Hello_World_HelloReply" - ) - guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } - XCTAssertEqual(method, expectedMethod) - - let expectedService = CodeGenerationRequest.ServiceDescriptor( - documentation: "/// The greeting service definition.\n", - name: CodeGenerationRequest.Name( - base: "Greeter", - generatedUpperCase: "Greeter", - generatedLowerCase: "greeter" - ), - namespace: CodeGenerationRequest.Name( - base: "hello.world", - generatedUpperCase: "Hello_World", - generatedLowerCase: "hello_world" - ), - methods: [expectedMethod] - ) - guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } - XCTAssertEqual(service, expectedService) - XCTAssertEqual(service.methods.count, 1) - - XCTAssertEqual( - parsedCodeGenRequest.lookupSerializer("Hello_World_HelloRequest"), - "GRPCProtobuf.ProtobufSerializer()" - ) - XCTAssertEqual( - parsedCodeGenRequest.lookupDeserializer("Hello_World_HelloRequest"), - "GRPCProtobuf.ProtobufDeserializer()" - ) - } - - func testParserEmptyPackage() throws { - let descriptorSet = DescriptorSet( - protos: [ - Google_Protobuf_FileDescriptorProto( - name: "same-module.proto", - package: "same-package" - ), - Google_Protobuf_FileDescriptorProto( - name: "different-module.proto", - package: "different-package" - ), - Google_Protobuf_FileDescriptorProto.helloWorldEmptyPackage, - ] - ) - - guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { - return XCTFail( - """ - Could not find the file descriptor of "helloworld.proto". - """ - ) - } - let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { - $0.mapping = [ - SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { - $0.protoFilePath = ["different-module.proto"] - $0.moduleName = "DifferentModule" - } - ] - } - let parsedCodeGenRequest = try ProtobufCodeGenParser( - input: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"], - accessLevel: .internal - ).parse() - - self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) - - let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor( - documentation: "/// Sends a greeting.\n", - name: CodeGenerationRequest.Name( - base: "SayHello", - generatedUpperCase: "SayHello", - generatedLowerCase: "sayHello" - ), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "HelloRequest", - outputType: "HelloReply" - ) - guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } - XCTAssertEqual(method, expectedMethod) - - let expectedService = CodeGenerationRequest.ServiceDescriptor( - documentation: "/// The greeting service definition.\n", - name: CodeGenerationRequest.Name( - base: "Greeter", - generatedUpperCase: "Greeter", - generatedLowerCase: "greeter" - ), - namespace: CodeGenerationRequest.Name( - base: "", - generatedUpperCase: "", - generatedLowerCase: "" - ), - methods: [expectedMethod] - ) - guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } - XCTAssertEqual(service, expectedService) - XCTAssertEqual(service.methods.count, 1) - - XCTAssertEqual( - parsedCodeGenRequest.lookupSerializer("HelloRequest"), - "GRPCProtobuf.ProtobufSerializer()" - ) - XCTAssertEqual( - parsedCodeGenRequest.lookupDeserializer("HelloRequest"), - "GRPCProtobuf.ProtobufDeserializer()" - ) - } -} - -extension ProtobufCodeGenParserTests { - func testCommonHelloworldParsedRequestFields(for request: CodeGenerationRequest) { - XCTAssertEqual(request.fileName, "helloworld.proto") - XCTAssertEqual( - request.leadingTrivia, - """ - // Copyright 2015 gRPC authors. - // - // 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. - - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: helloworld.proto - // - // For information on using the generated types, please see the documentation: - // https://github.com/grpc/grpc-swift - - """ - ) - XCTAssertEqual(request.dependencies.count, 3) - let expectedDependencyNames = ["GRPCProtobuf", "DifferentModule", "ExtraModule"] - let parsedDependencyNames = request.dependencies.map { $0.module } - XCTAssertEqual(parsedDependencyNames, expectedDependencyNames) - XCTAssertEqual(request.services.count, 1) - } -} - -extension Google_Protobuf_FileDescriptorProto { - static var helloWorld: Google_Protobuf_FileDescriptorProto { - let requestType = Google_Protobuf_DescriptorProto.with { - $0.name = "HelloRequest" - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "name" - $0.number = 1 - $0.label = .optional - $0.type = .string - $0.jsonName = "name" - } - ] - } - let responseType = Google_Protobuf_DescriptorProto.with { - $0.name = "HelloReply" - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "message" - $0.number = 1 - $0.label = .optional - $0.type = .string - $0.jsonName = "message" - } - ] - } - - let service = Google_Protobuf_ServiceDescriptorProto.with { - $0.name = "Greeter" - $0.method = [ - Google_Protobuf_MethodDescriptorProto.with { - $0.name = "SayHello" - $0.inputType = ".helloworld.HelloRequest" - $0.outputType = ".helloworld.HelloReply" - $0.clientStreaming = false - $0.serverStreaming = false - } - ] - } - return Google_Protobuf_FileDescriptorProto.with { - $0.name = "helloworld.proto" - $0.package = "helloworld" - $0.dependency = ["same-module.proto", "different-module.proto"] - $0.publicDependency = [0, 1] - $0.messageType = [requestType, responseType] - $0.service = [service] - $0.sourceCodeInfo = Google_Protobuf_SourceCodeInfo.with { - $0.location = [ - Google_Protobuf_SourceCodeInfo.Location.with { - $0.path = [12] - $0.span = [14, 0, 18] - $0.leadingDetachedComments = [ - """ - Copyright 2015 gRPC authors. - - 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. - - """ - ] - }, - Google_Protobuf_SourceCodeInfo.Location.with { - $0.path = [6, 0] - $0.span = [19, 0, 22, 1] - $0.leadingComments = " The greeting service definition.\n" - }, - Google_Protobuf_SourceCodeInfo.Location.with { - $0.path = [6, 0, 2, 0] - $0.span = [21, 2, 53] - $0.leadingComments = " Sends a greeting.\n" - }, - ] - } - $0.syntax = "proto3" - } - } - - static var helloWorldNestedPackage: Google_Protobuf_FileDescriptorProto { - let service = Google_Protobuf_ServiceDescriptorProto.with { - $0.name = "Greeter" - $0.method = [ - Google_Protobuf_MethodDescriptorProto.with { - $0.name = "SayHello" - $0.inputType = ".hello.world.HelloRequest" - $0.outputType = ".hello.world.HelloReply" - $0.clientStreaming = false - $0.serverStreaming = false - } - ] - } - - var helloWorldCopy = self.helloWorld - helloWorldCopy.package = "hello.world" - helloWorldCopy.service = [service] - - return helloWorldCopy - } - - static var helloWorldEmptyPackage: Google_Protobuf_FileDescriptorProto { - let service = Google_Protobuf_ServiceDescriptorProto.with { - $0.name = "Greeter" - $0.method = [ - Google_Protobuf_MethodDescriptorProto.with { - $0.name = "SayHello" - $0.inputType = ".HelloRequest" - $0.outputType = ".HelloReply" - $0.clientStreaming = false - $0.serverStreaming = false - } - ] - } - var helloWorldCopy = self.helloWorld - helloWorldCopy.package = "" - helloWorldCopy.service = [service] - - return helloWorldCopy - } - - internal init(name: String, package: String) { - self.init() - self.name = name - self.package = package - } -} diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift deleted file mode 100644 index e8c44043f..000000000 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ /dev/null @@ -1,628 +0,0 @@ -/* - * 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. - */ - -#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) - -import GRPCCodeGen -import GRPCProtobufCodeGen -import SwiftProtobuf -import SwiftProtobufPluginLibrary -import XCTest - -final class ProtobufCodeGeneratorTests: XCTestCase { - func testProtobufCodeGenerator() throws { - try testCodeGeneration( - proto: Google_Protobuf_FileDescriptorProto.helloWorldNestedPackage, - indentation: 4, - visibility: .internal, - client: true, - server: false, - expectedCode: """ - // Copyright 2015 gRPC authors. - // - // 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. - - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: helloworld.proto - // - // For information on using the generated types, please see the documentation: - // https://github.com/grpc/grpc-swift - - internal import GRPCCore - internal import GRPCProtobuf - internal import DifferentModule - internal import ExtraModule - - internal enum Hello_World_Greeter { - internal static let descriptor = GRPCCore.ServiceDescriptor.hello_world_Greeter - internal enum Method { - internal enum SayHello { - internal typealias Input = Hello_World_HelloRequest - internal typealias Output = Hello_World_HelloReply - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Hello_World_Greeter.descriptor.fullyQualifiedService, - method: "SayHello" - ) - } - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Hello_World_GreeterClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Hello_World_GreeterClient - } - - extension GRPCCore.ServiceDescriptor { - internal static let hello_world_Greeter = Self( - package: "hello.world", - service: "Greeter" - ) - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol Hello_World_GreeterClientProtocol: Sendable { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - } - - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Hello_World_Greeter.ClientProtocol { - internal func sayHello( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.sayHello( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - } - - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Hello_World_Greeter.ClientProtocol { - /// Sends a greeting. - internal func sayHello( - _ message: Hello_World_HelloRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.sayHello( - request: request, - options: options, - handleResponse - ) - } - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal struct Hello_World_GreeterClient: Hello_World_Greeter.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Sends a greeting. - internal func sayHello( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Hello_World_Greeter.Method.SayHello.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - } - """ - ) - - try testCodeGeneration( - proto: Google_Protobuf_FileDescriptorProto.helloWorld, - indentation: 2, - visibility: .public, - client: false, - server: true, - expectedCode: """ - // Copyright 2015 gRPC authors. - // - // 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. - - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: helloworld.proto - // - // For information on using the generated types, please see the documentation: - // https://github.com/grpc/grpc-swift - - public import GRPCCore - internal import GRPCProtobuf - public import DifferentModule - public import ExtraModule - - public enum Helloworld_Greeter { - public static let descriptor = GRPCCore.ServiceDescriptor.helloworld_Greeter - public enum Method { - public enum SayHello { - public typealias Input = Helloworld_HelloRequest - public typealias Output = Helloworld_HelloReply - public static let descriptor = GRPCCore.MethodDescriptor( - service: Helloworld_Greeter.descriptor.fullyQualifiedService, - method: "SayHello" - ) - } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol - } - - extension GRPCCore.ServiceDescriptor { - public static let helloworld_Greeter = Self( - package: "helloworld", - service: "Greeter" - ) - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - } - - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Helloworld_Greeter.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Helloworld_Greeter.Method.SayHello.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.sayHello( - request: request, - context: context - ) - } - ) - } - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - } - - /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Helloworld_Greeter.ServiceProtocol { - public func sayHello( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.sayHello( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - } - """ - ) - - try testCodeGeneration( - proto: Google_Protobuf_FileDescriptorProto.helloWorldEmptyPackage, - indentation: 2, - visibility: .package, - client: true, - server: true, - expectedCode: """ - // Copyright 2015 gRPC authors. - // - // 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. - - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: helloworld.proto - // - // For information on using the generated types, please see the documentation: - // https://github.com/grpc/grpc-swift - - package import GRPCCore - internal import GRPCProtobuf - package import DifferentModule - package import ExtraModule - - package enum Greeter { - package static let descriptor = GRPCCore.ServiceDescriptor.Greeter - package enum Method { - package enum SayHello { - package typealias Input = HelloRequest - package typealias Output = HelloReply - package static let descriptor = GRPCCore.MethodDescriptor( - service: Greeter.descriptor.fullyQualifiedService, - method: "SayHello" - ) - } - package static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = GreeterStreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = GreeterServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = GreeterClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = GreeterClient - } - - extension GRPCCore.ServiceDescriptor { - package static let Greeter = Self( - package: "", - service: "Greeter" - ) - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream - } - - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Greeter.Method.SayHello.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.sayHello( - request: request, - context: context - ) - } - ) - } - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol GreeterServiceProtocol: Greeter.StreamingServiceProtocol { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ServerRequest.Single, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single - } - - /// Partial conformance to `GreeterStreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.ServiceProtocol { - package func sayHello( - request: GRPCCore.ServerRequest.Stream, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { - let response = try await self.sayHello( - request: GRPCCore.ServerRequest.Single(stream: request), - context: context - ) - return GRPCCore.ServerResponse.Stream(single: response) - } - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol GreeterClientProtocol: Sendable { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R - ) async throws -> R where R: Sendable - } - - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.ClientProtocol { - package func sayHello( - request: GRPCCore.ClientRequest.Single, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.sayHello( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } - } - - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.ClientProtocol { - /// Sends a greeting. - package func sayHello( - _ message: HelloRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( - message: message, - metadata: metadata - ) - return try await self.sayHello( - request: request, - options: options, - handleResponse - ) - } - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package struct GreeterClient: Greeter.ClientProtocol { - private let client: GRPCCore.GRPCClient - - package init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Sends a greeting. - package func sayHello( - request: GRPCCore.ClientRequest.Single, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Greeter.Method.SayHello.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } - } - """ - ) - } - - func testNoAccessLevelOnImports() throws { - let proto = Google_Protobuf_FileDescriptorProto(name: "helloworld.proto", package: "") - try testCodeGeneration( - proto: proto, - indentation: 2, - visibility: .package, - client: true, - server: true, - accessLevelOnImports: false, - expectedCode: """ - // DO NOT EDIT. - // swift-format-ignore-file - // - // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. - // Source: helloworld.proto - // - // For information on using the generated types, please see the documentation: - // https://github.com/grpc/grpc-swift - - import GRPCCore - import GRPCProtobuf - import ExtraModule - - """ - ) - } - - func testCodeGeneration( - proto: Google_Protobuf_FileDescriptorProto, - indentation: Int, - visibility: SourceGenerator.Config.AccessLevel, - client: Bool, - server: Bool, - accessLevelOnImports: Bool = true, - expectedCode: String, - file: StaticString = #filePath, - line: UInt = #line - ) throws { - let config = SourceGenerator.Config( - accessLevel: visibility, - accessLevelOnImports: accessLevelOnImports, - client: client, - server: server, - indentation: indentation - ) - - let descriptorSet = DescriptorSet( - protos: [ - Google_Protobuf_FileDescriptorProto(name: "same-module.proto", package: "same-package"), - Google_Protobuf_FileDescriptorProto( - name: "different-module.proto", - package: "different-package" - ), - proto, - ]) - guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { - return XCTFail( - """ - Could not find the file descriptor of "helloworld.proto". - """ - ) - } - - let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { - $0.mapping = [ - SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { - $0.protoFilePath = ["different-module.proto"] - $0.moduleName = "DifferentModule" - } - ] - } - let generator = ProtobufCodeGenerator(configuration: config) - try XCTAssertEqualWithDiff( - try generator.generateCode( - from: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"] - ), - expectedCode, - file: file, - line: line - ) - } -} - -private func diff(expected: String, actual: String) throws -> String { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") - process.arguments = [ - "bash", "-c", - "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')", - ] - let pipe = Pipe() - process.standardOutput = pipe - try process.run() - process.waitUntilExit() - let pipeData = try XCTUnwrap( - pipe.fileHandleForReading.readToEnd(), - """ - No output from command: - \(process.executableURL!.path) \(process.arguments!.joined(separator: " ")) - """ - ) - return String(decoding: pipeData, as: UTF8.self) -} - -internal func XCTAssertEqualWithDiff( - _ actual: String, - _ expected: String, - file: StaticString = #filePath, - line: UInt = #line -) throws { - if actual == expected { return } - XCTFail( - """ - XCTAssertEqualWithDiff failed (click for diff) - \(try diff(expected: expected, actual: actual)) - """, - file: file, - line: line - ) -} - -#endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift b/Tests/GRPCProtobufTests/ProtobufCodingTests.swift deleted file mode 100644 index dd2202af1..000000000 --- a/Tests/GRPCProtobufTests/ProtobufCodingTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 GRPCProtobuf -import SwiftProtobuf -import XCTest - -final class ProtobufCodingTests: XCTestCase { - func testSerializeDeserializeRoundtrip() throws { - let message = Google_Protobuf_Timestamp.with { - $0.seconds = 4 - } - - let serializer = ProtobufSerializer() - let deserializer = ProtobufDeserializer() - - let bytes = try serializer.serialize(message) - let roundTrip = try deserializer.deserialize(bytes) - XCTAssertEqual(roundTrip, message) - } - - func testSerializerError() throws { - let message = TestMessage() - let serializer = ProtobufSerializer() - - XCTAssertThrowsError( - try serializer.serialize(message) - ) { error in - XCTAssertEqual( - error as? RPCError, - RPCError( - code: .invalidArgument, - message: - """ - Can't serialize message of type TestMessage. - """ - ) - ) - } - } - - func testDeserializerError() throws { - let bytes = Array("%%%%%££££".utf8) - let deserializer = ProtobufDeserializer() - XCTAssertThrowsError( - try deserializer.deserialize(bytes) - ) { error in - XCTAssertEqual( - error as? RPCError, - RPCError( - code: .invalidArgument, - message: - """ - Can't deserialize to message of type TestMessage - """ - ) - ) - } - } -} - -struct TestMessage: SwiftProtobuf.Message { - var text: String = "" - var unknownFields = SwiftProtobuf.UnknownStorage() - static let protoMessageName: String = "Test.ServiceRequest" - init() {} - - mutating func decodeMessage(decoder: inout D) throws where D: SwiftProtobuf.Decoder { - throw RPCError(code: .internalError, message: "Decoding error") - } - - func traverse(visitor: inout V) throws where V: SwiftProtobuf.Visitor { - throw RPCError(code: .internalError, message: "Traversing error") - } - - public var isInitialized: Bool { - if self.text.isEmpty { return false } - return true - } - - func isEqualTo(message: any SwiftProtobuf.Message) -> Bool { - return false - } -} diff --git a/Tests/GRPCTests/ALPNConfigurationTests.swift b/Tests/GRPCTests/ALPNConfigurationTests.swift deleted file mode 100644 index b106f9c0e..000000000 --- a/Tests/GRPCTests/ALPNConfigurationTests.swift +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -@testable import GRPC -import NIOSSL -import XCTest - -class ALPNConfigurationTests: GRPCTestCase { - private func assertExpectedClientALPNTokens(in tokens: [String]) { - XCTAssertEqual(tokens, ["grpc-exp", "h2"]) - } - - private func assertExpectedServerALPNTokens(in tokens: [String]) { - XCTAssertEqual(tokens, ["grpc-exp", "h2", "http/1.1"]) - } - - func testClientDefaultALPN() { - let config = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL() - self.assertExpectedClientALPNTokens( - in: config.nioConfiguration!.configuration.applicationProtocols - ) - } - - func testClientAddsTokensFromEmptyNIOSSLConfig() { - let tlsConfig = TLSConfiguration.makeClientConfiguration() - XCTAssertTrue(tlsConfig.applicationProtocols.isEmpty) - - let config = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - configuration: tlsConfig - ) - - // Should now contain expected config. - self.assertExpectedClientALPNTokens( - in: config.nioConfiguration!.configuration.applicationProtocols - ) - } - - func testClientDoesNotOverrideNonEmptyNIOSSLConfig() { - var tlsConfig = TLSConfiguration.makeClientConfiguration() - tlsConfig.applicationProtocols = ["foo"] - - let config = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - configuration: tlsConfig - ) - // Should not be overridden. - XCTAssertEqual(config.nioConfiguration!.configuration.applicationProtocols, ["foo"]) - } - - func testServerDefaultALPN() { - let config = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - certificateChain: [], - privateKey: .file("") - ) - - self.assertExpectedServerALPNTokens( - in: config.nioConfiguration!.configuration.applicationProtocols - ) - } - - func testServerAddsTokensFromEmptyNIOSSLConfig() { - let tlsConfig = TLSConfiguration.makeServerConfiguration( - certificateChain: [], - privateKey: .file("") - ) - XCTAssertTrue(tlsConfig.applicationProtocols.isEmpty) - - let config = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - configuration: tlsConfig - ) - - // Should now contain expected config. - self.assertExpectedServerALPNTokens( - in: config.nioConfiguration!.configuration.applicationProtocols - ) - } - - func testServerDoesNotOverrideNonEmptyNIOSSLConfig() { - var tlsConfig = TLSConfiguration.makeServerConfiguration( - certificateChain: [], - privateKey: .file("") - ) - tlsConfig.applicationProtocols = ["foo"] - - let config = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - configuration: tlsConfig - ) - // Should not be overridden. - XCTAssertEqual(config.nioConfiguration!.configuration.applicationProtocols, ["foo"]) - } -} -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/AnyServiceClientTests.swift b/Tests/GRPCTests/AnyServiceClientTests.swift deleted file mode 100644 index a20a1d025..000000000 --- a/Tests/GRPCTests/AnyServiceClientTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import GRPC -import XCTest - -class AnyServiceClientTests: EchoTestCaseBase { - var anyServiceClient: GRPCAnyServiceClient { - return GRPCAnyServiceClient( - channel: self.client.channel, - defaultCallOptions: self.callOptionsWithLogger - ) - } - - func testUnary() throws { - let get = self.anyServiceClient.makeUnaryCall( - path: "/echo.Echo/Get", - request: Echo_EchoRequest.with { $0.text = "foo" }, - responseType: Echo_EchoResponse.self - ) - - XCTAssertEqual(try get.status.map { $0.code }.wait(), .ok) - } - - func testClientStreaming() throws { - let collect = self.anyServiceClient.makeClientStreamingCall( - path: "/echo.Echo/Collect", - requestType: Echo_EchoRequest.self, - responseType: Echo_EchoResponse.self - ) - - collect.sendEnd(promise: nil) - - XCTAssertEqual(try collect.status.map { $0.code }.wait(), .ok) - } - - func testServerStreaming() throws { - let expand = self.anyServiceClient.makeServerStreamingCall( - path: "/echo.Echo/Expand", - request: Echo_EchoRequest.with { $0.text = "foo" }, - responseType: Echo_EchoResponse.self, - handler: { _ in } - ) - - XCTAssertEqual(try expand.status.map { $0.code }.wait(), .ok) - } - - func testBidirectionalStreaming() throws { - let update = self.anyServiceClient.makeBidirectionalStreamingCall( - path: "/echo.Echo/Update", - requestType: Echo_EchoRequest.self, - responseType: Echo_EchoResponse.self, - handler: { _ in } - ) - - update.sendEnd(promise: nil) - - XCTAssertEqual(try update.status.map { $0.code }.wait(), .ok) - } -} diff --git a/Tests/GRPCTests/Array+BoundsCheckingTests.swift b/Tests/GRPCTests/Array+BoundsCheckingTests.swift deleted file mode 100644 index 80c83c531..000000000 --- a/Tests/GRPCTests/Array+BoundsCheckingTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020, 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 XCTest - -@testable import GRPC - -class ArrayBoundsCheckingTests: GRPCTestCase { - func testBoundsCheckEmpty() { - let array: [Int] = [] - - XCTAssertNil(array[checked: array.startIndex]) - XCTAssertNil(array[checked: array.endIndex]) - XCTAssertNil(array[checked: -1]) - } - - func testBoundsCheckNonEmpty() { - let array: [Int] = Array(0 ..< 10) - - var index = array.startIndex - while index != array.endIndex { - XCTAssertEqual(array[checked: index], array[index]) - array.formIndex(after: &index) - } - - XCTAssertEqual(index, array.endIndex) - XCTAssertNil(array[checked: index]) - XCTAssertNil(array[checked: -1]) - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift deleted file mode 100644 index 0a6378030..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift +++ /dev/null @@ -1,539 +0,0 @@ -/* - * Copyright 2022, 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 EchoImplementation -import EchoModel -import GRPC -import NIOCore -import NIOPosix -import XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -final class AsyncClientCancellationTests: GRPCTestCase { - private var server: Server! - private var group: EventLoopGroup! - private var pool: GRPCChannel! - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() async throws { - if self.pool != nil { - try await self.pool.close().get() - self.pool = nil - } - - if self.server != nil { - try await self.server.close().get() - self.server = nil - } - - try await self.group.shutdownGracefully() - self.group = nil - - try await super.tearDown() - } - - private func startServer(service: CallHandlerProvider) throws { - precondition(self.server == nil) - - self.server = try Server.insecure(group: self.group) - .withServiceProviders([service]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - } - - private func startServerAndClient(service: CallHandlerProvider) throws -> Echo_EchoAsyncClient { - try self.startServer(service: service) - return try self.makeClient(port: self.server.channel.localAddress!.port!) - } - - private func makeClient( - port: Int, - configure: (inout GRPCChannelPool.Configuration) -> Void = { _ in } - ) throws -> Echo_EchoAsyncClient { - precondition(self.pool == nil) - - self.pool = try GRPCChannelPool.with( - target: .host("127.0.0.1", port: port), - transportSecurity: .plaintext, - eventLoopGroup: self.group - ) { - $0.backgroundActivityLogger = self.clientLogger - configure(&$0) - } - - return Echo_EchoAsyncClient(channel: self.pool) - } - - func testCancelUnaryFailsResponse() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) - get.cancel() - - do { - _ = try await get.response - XCTFail("Expected to throw a status with code .cancelled") - } catch let status as GRPCStatus { - XCTAssertEqual(status.code, .cancelled) - } catch { - XCTFail("Expected to throw a status with code .cancelled") - } - - // Status should be 'cancelled'. - let status = await get.status - XCTAssertEqual(status.code, .cancelled) - } - - func testCancelFailsUnaryResponseForWrappedCall() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - let task = Task { - try await echo.get(.with { $0.text = "I'll be cancelled" }) - } - - task.cancel() - - do { - _ = try await task.value - XCTFail("Expected to throw a status with code .cancelled") - } catch let status as GRPCStatus { - XCTAssertEqual(status.code, .cancelled) - } catch { - XCTFail("Expected to throw a status with code .cancelled") - } - } - - func testCancelServerStreamingClosesResponseStream() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - let expand = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) - expand.cancel() - - var responseStream = expand.responseStream.makeAsyncIterator() - - do { - _ = try await responseStream.next() - XCTFail("Expected to throw a status with code .cancelled") - } catch let status as GRPCStatus { - XCTAssertEqual(status.code, .cancelled) - } catch { - XCTFail("Expected to throw a status with code .cancelled") - } - - // Status should be 'cancelled'. - let status = await expand.status - XCTAssertEqual(status.code, .cancelled) - } - - func testCancelServerStreamingClosesResponseStreamForWrappedCall() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - let task = Task { - let responseStream = echo.expand(.with { $0.text = "foo bar baz" }) - var responseIterator = responseStream.makeAsyncIterator() - do { - _ = try await responseIterator.next() - XCTFail("Expected to throw a status with code .cancelled") - } catch let status as GRPCStatus { - XCTAssertEqual(status.code, .cancelled) - } catch { - XCTFail("Expected to throw a status with code .cancelled") - } - } - - task.cancel() - await task.value - } - - func testCancelClientStreamingClosesRequestStreamAndFailsResponse() async throws { - let echo = try self.startServerAndClient(service: EchoProvider()) - - let collect = echo.makeCollectCall() - // Make sure the stream is up before we cancel it. - try await collect.requestStream.send(.with { $0.text = "foo" }) - collect.cancel() - - // Cancellation is async so loop until we error. - while true { - do { - try await collect.requestStream.send(.with { $0.text = "foo" }) - try await Task.sleep(nanoseconds: 1000) - } catch { - break - } - } - - // There should be no response. - await XCTAssertThrowsError(try await collect.response) - - // Status should be 'cancelled'. - let status = await collect.status - XCTAssertEqual(status.code, .cancelled) - } - - func testCancelClientStreamingClosesRequestStreamAndFailsResponseForWrappedCall() async throws { - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - let requests = (0 ..< 10).map { i in - Echo_EchoRequest.with { - $0.text = String(i) - } - } - - let task = Task { - do { - let _ = try await echo.collect(requests) - XCTFail("Expected to throw a status with code .cancelled") - } catch let status as GRPCStatus { - XCTAssertEqual(status.code, .cancelled) - } catch { - XCTFail("Expected to throw a status with code .cancelled") - } - } - - task.cancel() - await task.value - } - - func testClientStreamingClosesRequestStreamOnEnd() async throws { - let echo = try self.startServerAndClient(service: EchoProvider()) - - let collect = echo.makeCollectCall() - // Send and close. - try await collect.requestStream.send(.with { $0.text = "foo" }) - collect.requestStream.finish() - - // Await the response and status. - _ = try await collect.response - let status = await collect.status - XCTAssert(status.isOk) - - // Sending should fail. - await XCTAssertThrowsError( - try await collect.requestStream.send(.with { $0.text = "should throw" }) - ) - } - - func testCancelBidiStreamingClosesRequestStreamAndResponseStream() async throws { - let echo = try self.startServerAndClient(service: EchoProvider()) - - let update = echo.makeUpdateCall() - // Make sure the stream is up before we cancel it. - try await update.requestStream.send(.with { $0.text = "foo" }) - // Wait for the response. - var responseStream = update.responseStream.makeAsyncIterator() - _ = try await responseStream.next() - - update.cancel() - - // Cancellation is async so loop until we error. - while true { - do { - try await update.requestStream.send(.with { $0.text = "foo" }) - try await Task.sleep(nanoseconds: 1000) - } catch { - break - } - } - - // Status should be 'cancelled'. - let status = await update.status - XCTAssertEqual(status.code, .cancelled) - } - - func testCancelBidiStreamingClosesRequestStreamAndResponseStreamForWrappedCall() async throws { - let echo = try self.startServerAndClient(service: EchoProvider()) - let requests = (0 ..< 10).map { i in - Echo_EchoRequest.with { - $0.text = String(i) - } - } - - let task = Task { - let responseStream = echo.update(requests) - var responseIterator = responseStream.makeAsyncIterator() - - do { - _ = try await responseIterator.next() - XCTFail("Expected to throw a status with code .cancelled") - } catch let status as GRPCStatus { - XCTAssertEqual(status.code, .cancelled) - } catch { - XCTFail("Expected to throw a status with code .cancelled") - } - } - - task.cancel() - await task.value - } - - func testBidiStreamingClosesRequestStreamOnEnd() async throws { - let echo = try self.startServerAndClient(service: EchoProvider()) - - let update = echo.makeUpdateCall() - // Send and close. - try await update.requestStream.send(.with { $0.text = "foo" }) - update.requestStream.finish() - - // Await the response and status. - let responseCount = try await update.responseStream.count() - XCTAssertEqual(responseCount, 1) - - let status = await update.status - XCTAssert(status.isOk) - - // Sending should fail. - await XCTAssertThrowsError( - try await update.requestStream.send(.with { $0.text = "should throw" }) - ) - } - - private enum RequestStreamingRPC { - typealias Request = Echo_EchoRequest - typealias Response = Echo_EchoResponse - - case clientStreaming(GRPCAsyncClientStreamingCall) - case bidirectionalStreaming(GRPCAsyncBidirectionalStreamingCall) - - func sendRequest(_ text: String) async throws { - switch self { - case let .clientStreaming(call): - try await call.requestStream.send(.with { $0.text = text }) - case let .bidirectionalStreaming(call): - try await call.requestStream.send(.with { $0.text = text }) - } - } - - func cancel() { - switch self { - case let .clientStreaming(call): - call.cancel() - case let .bidirectionalStreaming(call): - call.cancel() - } - } - } - - private func testSendingRequestsSuspendsWhileStreamIsNotReady( - makeRPC: @escaping () -> RequestStreamingRPC - ) async throws { - // The strategy for this test is to race two different tasks. The first will attempt to send a - // message on a request stream on a connection which will never establish. The second will sleep - // for a little while. Each task returns a `SendOrTimedOut` event. If the message is sent then - // the test definitely failed; it should not be possible to send a message on a stream which is - // not open. If the time out happens first then it probably did not fail. - enum SentOrTimedOut: Equatable, Sendable { - case messageSent - case timedOut - } - - await withThrowingTaskGroup(of: SentOrTimedOut.self) { group in - group.addTask { - let rpc = makeRPC() - - return try await withTaskCancellationHandler { - // This should suspend until we cancel it: we're never going to start a server so it - // should never succeed. - try await rpc.sendRequest("I should suspend") - return .messageSent - } onCancel: { - rpc.cancel() - } - } - - group.addTask { - // Wait for 100ms. - try await Task.sleep(nanoseconds: 100_000_000) - return .timedOut - } - - do { - let event = try await group.next() - // If this isn't timed out then the message was sent before the stream was ready. - XCTAssertEqual(event, .timedOut) - } catch { - XCTFail("Unexpected error \(error)") - } - - // Cancel the other task. - group.cancelAll() - } - } - - func testClientStreamingSuspendsWritesUntilStreamIsUp() async throws { - // Make a client for a server which isn't up yet. It will continually fail to establish a - // connection. - let echo = try self.makeClient(port: 0) - try await self.testSendingRequestsSuspendsWhileStreamIsNotReady { - return .clientStreaming(echo.makeCollectCall()) - } - } - - func testBidirectionalStreamingSuspendsWritesUntilStreamIsUp() async throws { - // Make a client for a server which isn't up yet. It will continually fail to establish a - // connection. - let echo = try self.makeClient(port: 0) - try await self.testSendingRequestsSuspendsWhileStreamIsNotReady { - return .bidirectionalStreaming(echo.makeUpdateCall()) - } - } - - func testConnectionFailureCancelsRequestStreamWithError() async throws { - let echo = try self.makeClient(port: 0) { - // Configure a short wait time; we will not start a server so fail quickly. - $0.connectionPool.maxWaitTime = .milliseconds(10) - } - - let update = echo.makeUpdateCall() - await XCTAssertThrowsError(try await update.requestStream.send(.init())) { error in - XCTAssertFalse(error is CancellationError) - } - - let collect = echo.makeCollectCall() - await XCTAssertThrowsError(try await collect.requestStream.send(.init())) { error in - XCTAssertFalse(error is CancellationError) - } - } - - func testCancelUnary() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - do { - let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) - let task = Task { try await get.initialMetadata } - task.cancel() - await XCTAssertThrowsError(try await task.value) - } - - do { - let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) - let task = Task { try await get.response } - task.cancel() - await XCTAssertThrowsError(try await task.value) - } - - do { - let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) - let task = Task { try await get.trailingMetadata } - task.cancel() - await XCTAssertNoThrowAsync(try await task.value) - } - - do { - let get = echo.makeGetCall(.with { $0.text = "foo bar baz" }) - let task = Task { await get.status } - task.cancel() - let status = await task.value - XCTAssertEqual(status.code, .cancelled) - } - } - - func testCancelClientStreaming() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - do { - let collect = echo.makeCollectCall() - let task = Task { try await collect.initialMetadata } - task.cancel() - await XCTAssertThrowsError(try await task.value) - } - - do { - let collect = echo.makeCollectCall() - let task = Task { try await collect.response } - task.cancel() - await XCTAssertThrowsError(try await task.value) - } - - do { - let collect = echo.makeCollectCall() - let task = Task { try await collect.trailingMetadata } - task.cancel() - await XCTAssertNoThrowAsync(try await task.value) - } - - do { - let collect = echo.makeCollectCall() - let task = Task { await collect.status } - task.cancel() - let status = await task.value - XCTAssertEqual(status.code, .cancelled) - } - } - - func testCancelServerStreaming() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - do { - let expand = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) - let task = Task { try await expand.initialMetadata } - task.cancel() - await XCTAssertThrowsError(try await task.value) - } - - do { - let expand = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) - let task = Task { try await expand.trailingMetadata } - task.cancel() - await XCTAssertNoThrowAsync(try await task.value) - } - - do { - let expand = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) - let task = Task { await expand.status } - task.cancel() - let status = await task.value - XCTAssertEqual(status.code, .cancelled) - } - } - - func testCancelBidirectionalStreaming() async throws { - // We don't want the RPC to complete before we cancel it so use the never resolving service. - let echo = try self.startServerAndClient(service: NeverResolvingEchoProvider()) - - do { - let update = echo.makeUpdateCall() - let task = Task { try await update.initialMetadata } - task.cancel() - await XCTAssertThrowsError(try await task.value) - } - - do { - let update = echo.makeUpdateCall() - let task = Task { try await update.trailingMetadata } - task.cancel() - await XCTAssertNoThrowAsync(try await task.value) - } - - do { - let update = echo.makeUpdateCall() - let task = Task { await update.status } - task.cancel() - let status = await task.value - XCTAssertEqual(status.code, .cancelled) - } - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift deleted file mode 100644 index fe3d0f3db..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncIntegrationTests.swift +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import GRPC -import NIOCore -import NIOHPACK -import NIOPosix -import XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -final class AsyncIntegrationTests: GRPCTestCase { - private var group: EventLoopGroup! - private var server: Server! - private var client: GRPCChannel! - - private var echo: Echo_EchoAsyncClient { - return .init(channel: self.client, defaultCallOptions: self.callOptionsWithLogger) - } - - override func setUp() { - super.setUp() - - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - self.server = try! Server.insecure(group: self.group) - .withLogger(self.serverLogger) - .withServiceProviders([EchoAsyncProvider()]) - .bind(host: "127.0.0.1", port: 0) - .wait() - - let port = self.server.channel.localAddress!.port! - self.client = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "127.0.0.1", port: port) - } - - override func tearDown() { - XCTAssertNoThrow(try self.client?.close().wait()) - XCTAssertNoThrow(try self.server?.close().wait()) - XCTAssertNoThrow(try self.group?.syncShutdownGracefully()) - super.tearDown() - } - - func testUnary() async throws { - let get = self.echo.makeGetCall(.with { $0.text = "hello" }) - - let initialMetadata = try await get.initialMetadata - initialMetadata.assertFirst("200", forName: ":status") - - let response = try await get.response - XCTAssertEqual(response.text, "Swift echo get: hello") - - let trailingMetadata = try await get.trailingMetadata - trailingMetadata.assertFirst("0", forName: "grpc-status") - - let status = await get.status - XCTAssertTrue(status.isOk) - } - - func testUnaryWrapper() async throws { - let response = try await self.echo.get(.with { $0.text = "hello" }) - XCTAssertEqual(response.text, "Swift echo get: hello") - } - - func testClientStreaming() async throws { - let collect = self.echo.makeCollectCall() - - try await collect.requestStream.send(.with { $0.text = "boyle" }) - try await collect.requestStream.send(.with { $0.text = "jeffers" }) - try await collect.requestStream.send(.with { $0.text = "holt" }) - collect.requestStream.finish() - - let initialMetadata = try await collect.initialMetadata - initialMetadata.assertFirst("200", forName: ":status") - - let response = try await collect.response - XCTAssertEqual(response.text, "Swift echo collect: boyle jeffers holt") - - let trailingMetadata = try await collect.trailingMetadata - trailingMetadata.assertFirst("0", forName: "grpc-status") - - let status = await collect.status - XCTAssertTrue(status.isOk) - } - - func testClientStreamingWrapper() async throws { - let requests: [Echo_EchoRequest] = [ - .with { $0.text = "boyle" }, - .with { $0.text = "jeffers" }, - .with { $0.text = "holt" }, - ] - - let response = try await self.echo.collect(requests) - XCTAssertEqual(response.text, "Swift echo collect: boyle jeffers holt") - } - - func testServerStreaming() async throws { - let expand = self.echo.makeExpandCall(.with { $0.text = "boyle jeffers holt" }) - - let initialMetadata = try await expand.initialMetadata - initialMetadata.assertFirst("200", forName: ":status") - - let responses = try await expand.responseStream.map { $0.text }.collect() - XCTAssertEqual( - responses, - [ - "Swift echo expand (0): boyle", - "Swift echo expand (1): jeffers", - "Swift echo expand (2): holt", - ] - ) - - let trailingMetadata = try await expand.trailingMetadata - trailingMetadata.assertFirst("0", forName: "grpc-status") - - let status = await expand.status - XCTAssertTrue(status.isOk) - } - - func testServerStreamingWrapper() async throws { - let responseStream = self.echo.expand(.with { $0.text = "boyle jeffers holt" }) - let responses = try await responseStream.map { $0.text }.collect() - XCTAssertEqual( - responses, - [ - "Swift echo expand (0): boyle", - "Swift echo expand (1): jeffers", - "Swift echo expand (2): holt", - ] - ) - } - - func testBidirectionalStreaming() async throws { - let update = self.echo.makeUpdateCall() - - var responseIterator = update.responseStream.map { $0.text }.makeAsyncIterator() - - for (i, name) in ["boyle", "jeffers", "holt"].enumerated() { - try await update.requestStream.send(.with { $0.text = name }) - let response = try await responseIterator.next() - XCTAssertEqual(response, "Swift echo update (\(i)): \(name)") - } - - update.requestStream.finish() - - // This isn't right after we make the call as servers are not guaranteed to send metadata back - // immediately. Concretely, we don't send initial metadata back until the first response - // message is sent by the server. - let initialMetadata = try await update.initialMetadata - initialMetadata.assertFirst("200", forName: ":status") - - let trailingMetadata = try await update.trailingMetadata - trailingMetadata.assertFirst("0", forName: "grpc-status") - - let status = await update.status - XCTAssertTrue(status.isOk) - } - - func testBidirectionalStreamingWrapper() async throws { - let requests: [Echo_EchoRequest] = [ - .with { $0.text = "boyle" }, - .with { $0.text = "jeffers" }, - .with { $0.text = "holt" }, - ] - - let responseStream = self.echo.update(requests) - let responses = try await responseStream.map { $0.text }.collect() - XCTAssertEqual( - responses, - [ - "Swift echo update (0): boyle", - "Swift echo update (1): jeffers", - "Swift echo update (2): holt", - ] - ) - } - - func testServerCloseAfterMessage() async throws { - let update = self.echo.makeUpdateCall() - try await update.requestStream.send(.with { $0.text = "hello" }) - _ = try await update.responseStream.first(where: { _ in true }) - XCTAssertNoThrow(try self.server.close().wait()) - self.server = nil // So that tearDown() does not call close() again. - update.requestStream.finish() - } -} - -extension HPACKHeaders { - func assertFirst(_ value: String, forName name: String) { - XCTAssertEqual(self.first(name: name), value) - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift deleted file mode 100644 index 6e596d758..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncSequence+Helpers.swift +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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. - */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) -extension AsyncSequence { - internal func collect() async throws -> [Element] { - return try await self.reduce(into: []) { accumulated, next in - accumulated.append(next) - } - } - - internal func count() async throws -> Int { - return try await self.reduce(0) { count, _ in count + 1 } - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift deleted file mode 100644 index 32f9497cd..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerHandlerStateMachine/ServerHandlerStateMachineTests.swift +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright 2022, 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 NIOCore -import NIOEmbedded -import NIOHPACK -import XCTest - -@testable import GRPC - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final class ServerHandlerStateMachineTests: GRPCTestCase { - private enum InitialState { - case idle - case handling - case draining - case finished - } - - private func makeStateMachine(inState state: InitialState = .idle) -> ServerHandlerStateMachine { - var stateMachine = ServerHandlerStateMachine() - - switch state { - case .idle: - return stateMachine - case .handling: - stateMachine.handleMetadata().assertInvokeHandler() - stateMachine.handlerInvoked(requestHeaders: [:]) - return stateMachine - case .draining: - stateMachine.handleMetadata().assertInvokeHandler() - stateMachine.handlerInvoked(requestHeaders: [:]) - stateMachine.handleEnd().assertForward() - return stateMachine - case .finished: - stateMachine.cancel().assertNone() - return stateMachine - } - } - - private func makeCallHandlerContext() -> CallHandlerContext { - let loop = EmbeddedEventLoop() - defer { - try! loop.syncShutdownGracefully() - } - return CallHandlerContext( - logger: self.logger, - encoding: .disabled, - eventLoop: loop, - path: "", - responseWriter: NoOpResponseWriter(), - allocator: ByteBufferAllocator(), - closeFuture: loop.makeSucceededVoidFuture() - ) - } - - // MARK: - Test Cases - - func testHandleMetadataWhenIdle() { - var stateMachine = self.makeStateMachine() - // Receiving metadata is the signal to invoke the user handler. - stateMachine.handleMetadata().assertInvokeHandler() - // On invoking the handler we move to the next state. No output. - stateMachine.handlerInvoked(requestHeaders: [:]) - } - - func testHandleMetadataWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - // Must not receive metadata more than once. - stateMachine.handleMetadata().assertInvokeCancel() - } - - func testHandleMetadataWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - // We can't receive metadata more than once. - stateMachine.handleMetadata().assertInvokeCancel() - } - - func testHandleMetadataWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - // We can't receive anything when finished. - stateMachine.handleMetadata().assertInvokeCancel() - } - - func testHandleMessageWhenIdle() { - var stateMachine = self.makeStateMachine() - // Metadata must be received first. - stateMachine.handleMessage().assertCancel() - } - - func testHandleMessageWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - // Messages are good, we can forward those while handling. - for _ in 0 ..< 10 { - stateMachine.handleMessage().assertForward() - } - } - - func testHandleMessageWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - // We entered the 'draining' state as we received 'end', another message is a protocol - // violation so cancel. - stateMachine.handleMessage().assertCancel() - } - - func testHandleMessageWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - // We can't receive anything when finished. - stateMachine.handleMessage().assertCancel() - } - - func testHandleEndWhenIdle() { - var stateMachine = self.makeStateMachine() - // Metadata must be received first. - stateMachine.handleEnd().assertCancel() - } - - func testHandleEndWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - // End is good; it transitions us to the draining state. - stateMachine.handleEnd().assertForward() - } - - func testHandleEndWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - // We entered the 'draining' state as we received 'end', another 'end' is a protocol - // violation so cancel. - stateMachine.handleEnd().assertCancel() - } - - func testHandleEndWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - // We can't receive anything when finished. - stateMachine.handleEnd().assertCancel() - } - - func testSendMessageWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - // The first message should prompt headers to be sent as well. - stateMachine.sendMessage().assertInterceptHeadersThenMessage() - // Additional messages should be just the message. - stateMachine.sendMessage().assertInterceptMessage() - } - - func testSendMessageWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - // The first message should prompt headers to be sent as well. - stateMachine.sendMessage().assertInterceptHeadersThenMessage() - // Additional messages should be just the message. - stateMachine.sendMessage().assertInterceptMessage() - } - - func testSendMessageWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - // We can't send anything if we're finished. - stateMachine.sendMessage().assertDrop() - } - - func testSendStatusWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - // This moves the state machine to the 'finished' state. - stateMachine.sendStatus().assertIntercept() - } - - func testSendStatusWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - // This moves the state machine to the 'finished' state. - stateMachine.sendStatus().assertIntercept() - } - - func testSendStatusWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - // We can't send anything if we're finished. - stateMachine.sendStatus().assertDrop() - } - - func testCancelWhenIdle() { - var stateMachine = self.makeStateMachine() - // Cancelling when idle is effectively a no-op; there's nothing to cancel. - stateMachine.cancel().assertNone() - } - - func testCancelWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - // We have things to cancel in this state. - stateMachine.cancel().assertDoCancel() - } - - func testCancelWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - // We have things to cancel in this state. - stateMachine.cancel().assertDoCancel() - } - - func testCancelWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - stateMachine.cancel().assertDoCancel() - } - - func testSetResponseHeadersWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - XCTAssertTrue(stateMachine.setResponseHeaders(["foo": "bar"])) - stateMachine.sendMessage().assertInterceptHeadersThenMessage { headers in - XCTAssertEqual(headers, ["foo": "bar"]) - } - } - - func testSetResponseHeadersWhenHandlingAreMovedToDraining() { - var stateMachine = self.makeStateMachine(inState: .handling) - XCTAssertTrue(stateMachine.setResponseHeaders(["foo": "bar"])) - stateMachine.handleEnd().assertForward() - stateMachine.sendMessage().assertInterceptHeadersThenMessage { headers in - XCTAssertEqual(headers, ["foo": "bar"]) - } - } - - func testSetResponseHeadersWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - XCTAssertTrue(stateMachine.setResponseHeaders(["foo": "bar"])) - stateMachine.sendMessage().assertInterceptHeadersThenMessage { headers in - XCTAssertEqual(headers, ["foo": "bar"]) - } - } - - func testSetResponseHeadersWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - XCTAssertFalse(stateMachine.setResponseHeaders(["foo": "bar"])) - } - - func testSetResponseTrailersWhenHandling() { - var stateMachine = self.makeStateMachine(inState: .handling) - stateMachine.setResponseTrailers(["foo": "bar"]) - stateMachine.sendStatus().assertIntercept { trailers in - XCTAssertEqual(trailers, ["foo": "bar"]) - } - } - - func testSetResponseTrailersWhenDraining() { - var stateMachine = self.makeStateMachine(inState: .draining) - stateMachine.setResponseTrailers(["foo": "bar"]) - stateMachine.sendStatus().assertIntercept { trailers in - XCTAssertEqual(trailers, ["foo": "bar"]) - } - } - - func testSetResponseTrailersWhenFinished() { - var stateMachine = self.makeStateMachine(inState: .finished) - stateMachine.setResponseTrailers(["foo": "bar"]) - // Nothing we can assert on, only that we don't crash. - } -} - -// MARK: - Action Assertions - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.HandleMetadataAction { - func assertInvokeHandler() { - XCTAssertEqual(self, .invokeHandler) - } - - func assertInvokeCancel() { - XCTAssertEqual(self, .cancel) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.HandleMessageAction { - func assertForward() { - XCTAssertEqual(self, .forward) - } - - func assertCancel() { - XCTAssertEqual(self, .cancel) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.SendMessageAction { - func assertInterceptHeadersThenMessage(_ verify: (HPACKHeaders) -> Void = { _ in }) { - switch self { - case let .intercept(headers: .some(headers)): - verify(headers) - default: - XCTFail("Expected .intercept(.some) but got \(self)") - } - } - - func assertInterceptMessage() { - XCTAssertEqual(self, .intercept(headers: nil)) - } - - func assertDrop() { - XCTAssertEqual(self, .drop) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.SendStatusAction { - func assertIntercept(_ verify: (HPACKHeaders) -> Void = { _ in }) { - switch self { - case let .intercept(_, trailers: trailers): - verify(trailers) - case .drop: - XCTFail("Expected .intercept but got .drop") - } - } - - func assertDrop() { - XCTAssertEqual(self, .drop) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerHandlerStateMachine.CancelAction { - func assertNone() { - XCTAssertEqual(self, .none) - } - - func assertDoCancel() { - XCTAssertEqual(self, .cancelAndNilOutHandlerComponents) - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift deleted file mode 100644 index 789a126a4..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineStreamStateTests.swift +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2022, 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 XCTest - -@testable import GRPC - -internal final class ServerInterceptorStateMachineStreamStateTests: GRPCTestCase { - func testInboundStreamState_receiveMetadataWhileIdle() { - var state = ServerInterceptorStateMachine.InboundStreamState.idle - XCTAssertEqual(state.receiveMetadata(), .accept) - XCTAssertEqual(state, .receivingMessages) - } - - func testInboundStreamState_receiveMessageWhileIdle() { - let state = ServerInterceptorStateMachine.InboundStreamState.idle - XCTAssertEqual(state.receiveMessage(), .reject) - XCTAssertEqual(state, .idle) - } - - func testInboundStreamState_receiveEndWhileIdle() { - var state = ServerInterceptorStateMachine.InboundStreamState.idle - XCTAssertEqual(state.receiveEnd(), .accept) - XCTAssertEqual(state, .done) - } - - func testInboundStreamState_receiveMetadataWhileReceivingMessages() { - var state = ServerInterceptorStateMachine.InboundStreamState.receivingMessages - XCTAssertEqual(state.receiveMetadata(), .reject) - XCTAssertEqual(state, .receivingMessages) - } - - func testInboundStreamState_receiveMessageWhileReceivingMessages() { - let state = ServerInterceptorStateMachine.InboundStreamState.receivingMessages - XCTAssertEqual(state.receiveMessage(), .accept) - XCTAssertEqual(state, .receivingMessages) - } - - func testInboundStreamState_receiveEndWhileReceivingMessages() { - var state = ServerInterceptorStateMachine.InboundStreamState.receivingMessages - XCTAssertEqual(state.receiveEnd(), .accept) - XCTAssertEqual(state, .done) - } - - func testInboundStreamState_receiveMetadataWhileDone() { - var state = ServerInterceptorStateMachine.InboundStreamState.done - XCTAssertEqual(state.receiveMetadata(), .reject) - XCTAssertEqual(state, .done) - } - - func testInboundStreamState_receiveMessageWhileDone() { - let state = ServerInterceptorStateMachine.InboundStreamState.done - XCTAssertEqual(state.receiveMessage(), .reject) - XCTAssertEqual(state, .done) - } - - func testInboundStreamState_receiveEndWhileDone() { - var state = ServerInterceptorStateMachine.InboundStreamState.done - XCTAssertEqual(state.receiveEnd(), .reject) - XCTAssertEqual(state, .done) - } - - func testOutboundStreamState_sendMetadataWhileIdle() { - var state = ServerInterceptorStateMachine.OutboundStreamState.idle - XCTAssertEqual(state.sendMetadata(), .accept) - XCTAssertEqual(state, .writingMessages) - } - - func testOutboundStreamState_sendMessageWhileIdle() { - let state = ServerInterceptorStateMachine.OutboundStreamState.idle - XCTAssertEqual(state.sendMessage(), .reject) - XCTAssertEqual(state, .idle) - } - - func testOutboundStreamState_sendEndWhileIdle() { - var state = ServerInterceptorStateMachine.OutboundStreamState.idle - XCTAssertEqual(state.sendEnd(), .accept) - XCTAssertEqual(state, .done) - } - - func testOutboundStreamState_sendMetadataWhileReceivingMessages() { - var state = ServerInterceptorStateMachine.OutboundStreamState.writingMessages - XCTAssertEqual(state.sendMetadata(), .reject) - XCTAssertEqual(state, .writingMessages) - } - - func testOutboundStreamState_sendMessageWhileReceivingMessages() { - let state = ServerInterceptorStateMachine.OutboundStreamState.writingMessages - XCTAssertEqual(state.sendMessage(), .accept) - XCTAssertEqual(state, .writingMessages) - } - - func testOutboundStreamState_sendEndWhileReceivingMessages() { - var state = ServerInterceptorStateMachine.OutboundStreamState.writingMessages - XCTAssertEqual(state.sendEnd(), .accept) - XCTAssertEqual(state, .done) - } - - func testOutboundStreamState_sendMetadataWhileDone() { - var state = ServerInterceptorStateMachine.OutboundStreamState.done - XCTAssertEqual(state.sendMetadata(), .reject) - XCTAssertEqual(state, .done) - } - - func testOutboundStreamState_sendMessageWhileDone() { - let state = ServerInterceptorStateMachine.OutboundStreamState.done - XCTAssertEqual(state.sendMessage(), .reject) - XCTAssertEqual(state, .done) - } - - func testOutboundStreamState_sendEndWhileDone() { - var state = ServerInterceptorStateMachine.OutboundStreamState.done - XCTAssertEqual(state.sendEnd(), .reject) - XCTAssertEqual(state, .done) - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift deleted file mode 100644 index 98bd6d7c5..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/AsyncServerHandler/ServerInterceptorStateMachine/ServerInterceptorStateMachineTests.swift +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2022, 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 NIOEmbedded -import XCTest - -@testable import GRPC - -final class ServerInterceptorStateMachineTests: GRPCTestCase { - func testInterceptRequestMetadataWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptRequestMetadata().assertCancel() // Can't receive metadata twice. - } - - func testInterceptRequestMessageWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMessage().assertCancel() - } - - func testInterceptRequestEndWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestEnd().assertIntercept() - stateMachine.interceptRequestEnd().assertCancel() // Can't receive end twice. - } - - func testInterceptedRequestMetadataWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - stateMachine.interceptedRequestMetadata().assertCancel() // Can't intercept metadata twice. - } - - func testInterceptedRequestMessageWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - for _ in 0 ..< 100 { - stateMachine.interceptRequestMessage().assertIntercept() - stateMachine.interceptedRequestMessage().assertForward() - } - } - - func testInterceptedRequestEndWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - stateMachine.interceptRequestEnd().assertIntercept() - stateMachine.interceptedRequestEnd().assertForward() - stateMachine.interceptedRequestEnd().assertCancel() // Can't intercept end twice. - } - - func testInterceptResponseMetadataWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - - stateMachine.interceptResponseMetadata().assertIntercept() - stateMachine.interceptResponseMetadata().assertCancel() - } - - func testInterceptedResponseMetadataWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - - stateMachine.interceptResponseMetadata().assertIntercept() - stateMachine.interceptedResponseMetadata().assertForward() - stateMachine.interceptedResponseMetadata().assertCancel() - } - - func testInterceptResponseMessageWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - - stateMachine.interceptResponseMetadata().assertIntercept() - stateMachine.interceptResponseMessage().assertIntercept() - } - - func testInterceptedResponseMessageWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - - stateMachine.interceptResponseMetadata().assertIntercept() - stateMachine.interceptedResponseMetadata().assertForward() - stateMachine.interceptResponseMessage().assertIntercept() - stateMachine.interceptedResponseMessage().assertForward() - // Still fine: interceptor could insert extra message. - stateMachine.interceptedResponseMessage().assertForward() - } - - func testInterceptResponseStatusWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - - stateMachine.interceptResponseMetadata().assertIntercept() - stateMachine.interceptResponseMessage().assertIntercept() - stateMachine.interceptResponseStatus().assertIntercept() - - stateMachine.interceptResponseMessage().assertCancel() - stateMachine.interceptResponseStatus().assertCancel() - } - - func testInterceptedResponseStatusWhenIntercepting() { - var stateMachine = ServerInterceptorStateMachine() - stateMachine.interceptRequestMetadata().assertIntercept() - stateMachine.interceptedRequestMetadata().assertForward() - - stateMachine.interceptResponseMetadata().assertIntercept() - stateMachine.interceptedResponseMetadata().assertForward() - stateMachine.interceptResponseStatus().assertIntercept() - stateMachine.interceptedResponseStatus().assertForward() - } - - func testAllOperationsDropWhenFinished() { - var stateMachine = ServerInterceptorStateMachine() - // Get to the finished state. - stateMachine.cancel().assertSendStatusThenNilOutInterceptorPipeline() - - stateMachine.interceptRequestMetadata().assertDrop() - stateMachine.interceptedRequestMetadata().assertDrop() - stateMachine.interceptRequestMessage().assertDrop() - stateMachine.interceptedRequestMessage().assertDrop() - stateMachine.interceptRequestEnd().assertDrop() - stateMachine.interceptedRequestEnd().assertDrop() - - stateMachine.interceptResponseMetadata().assertDrop() - stateMachine.interceptedResponseMetadata().assertDrop() - stateMachine.interceptResponseMessage().assertDrop() - stateMachine.interceptedResponseMessage().assertDrop() - stateMachine.interceptResponseStatus().assertDrop() - stateMachine.interceptedResponseStatus().assertDrop() - } -} - -extension ServerInterceptorStateMachine.InterceptAction { - func assertIntercept() { - XCTAssertEqual(self, .intercept) - } - - func assertCancel() { - XCTAssertEqual(self, .cancel) - } - - func assertDrop() { - XCTAssertEqual(self, .drop) - } -} - -extension ServerInterceptorStateMachine.InterceptedAction { - func assertForward() { - XCTAssertEqual(self, .forward) - } - - func assertCancel() { - XCTAssertEqual(self, .cancel) - } - - func assertDrop() { - XCTAssertEqual(self, .drop) - } -} - -extension ServerInterceptorStateMachine.CancelAction { - func assertSendStatusThenNilOutInterceptorPipeline() { - XCTAssertEqual(self, .sendStatusThenNilOutInterceptorPipeline) - } - - func assertNilOutInterceptorPipeline() { - XCTAssertEqual(self, .nilOutInterceptorPipeline) - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift deleted file mode 100644 index 708b5da55..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncRequestStreamTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022, 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 GRPC -import XCTest - -@available(macOS 12, iOS 13, tvOS 13, watchOS 6, *) -final class GRPCAsyncRequestStreamTests: XCTestCase { - func testRecorder() async throws { - let testingStream = GRPCAsyncRequestStream.makeTestingRequestStream() - - testingStream.source.yield(1) - testingStream.source.finish(throwing: nil) - - let results = try await testingStream.stream.collect() - - XCTAssertEqual(results, [1]) - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift deleted file mode 100644 index 2c903d15b..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/GRPCAsyncResponseStreamWriterTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2022, 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 GRPC -import XCTest - -@available(macOS 12, iOS 13, tvOS 13, watchOS 6, *) -final class GRPCAsyncResponseStreamWriterTests: XCTestCase { - func testRecorder() async throws { - let responseStreamWriter = GRPCAsyncResponseStreamWriter.makeTestingResponseStreamWriter() - - try await responseStreamWriter.writer.send(1, compression: .disabled) - responseStreamWriter.stream.finish() - - let results = try await responseStreamWriter.stream.collect() - XCTAssertEqual(results[0].0, 1) - XCTAssertEqual(results[0].1, .disabled) - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift b/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift deleted file mode 100644 index dca269d66..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/InterceptorsAsyncTests.swift +++ /dev/null @@ -1,170 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import GRPC -import HelloWorldModel -import NIOCore -import NIOHPACK -import NIOPosix -import SwiftProtobuf -import XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -class InterceptorsAsyncTests: GRPCTestCase { - private var group: EventLoopGroup! - private var server: Server! - private var connection: ClientConnection! - private var echo: Echo_EchoAsyncClient! - - override func setUp() { - super.setUp() - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.group = group - - let server = try! Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - - self.server = server - - let connection = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "127.0.0.1", port: server.channel.localAddress!.port!) - - self.connection = connection - - self.echo = Echo_EchoAsyncClient( - channel: connection, - defaultCallOptions: CallOptions(logger: self.clientLogger), - interceptors: ReversingInterceptors() - ) - } - - override func tearDown() { - if let connection = self.connection { - XCTAssertNoThrow(try connection.close().wait()) - } - if let server = self.server { - XCTAssertNoThrow(try server.close().wait()) - } - if let group = self.group { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - super.tearDown() - } - - func testUnaryCall() async throws { - let get = try await self.echo.get(.with { $0.text = "hello" }) - await assertThat(get, .is(.with { $0.text = "hello :teg ohce tfiwS" })) - } - - func testMakingUnaryCall() async throws { - let call = self.echo.makeGetCall(.with { $0.text = "hello" }) - await assertThat(try await call.response, .is(.with { $0.text = "hello :teg ohce tfiwS" })) - } - - func testClientStreamingSequence() async throws { - let requests = ["1 2", "3 4"].map { item in - Echo_EchoRequest.with { $0.text = item } - } - let response = try await self.echo.collect(requests, callOptions: .init()) - - await assertThat(response, .is(.with { $0.text = "3 4 1 2 :tcelloc ohce tfiwS" })) - } - - func testClientStreamingAsyncSequence() async throws { - let stream = AsyncStream { continuation in - continuation.yield(.with { $0.text = "1 2" }) - continuation.yield(.with { $0.text = "3 4" }) - continuation.finish() - } - let response = try await self.echo.collect(stream, callOptions: .init()) - - await assertThat(response, .is(.with { $0.text = "3 4 1 2 :tcelloc ohce tfiwS" })) - } - - func testMakingCallClientStreaming() async throws { - let call = self.echo.makeCollectCall(callOptions: .init()) - try await call.requestStream.send(.with { $0.text = "1 2" }) - try await call.requestStream.send(.with { $0.text = "3 4" }) - call.requestStream.finish() - - await assertThat( - try await call.response, - .is(.with { $0.text = "3 4 1 2 :tcelloc ohce tfiwS" }) - ) - } - - func testServerStreaming() async throws { - let responses = self.echo.expand(.with { $0.text = "hello" }, callOptions: .init()) - for try await response in responses { - // Expand splits on spaces, so we only expect one response. - await assertThat(response, .is(.with { $0.text = "hello :)0( dnapxe ohce tfiwS" })) - } - } - - func testMakingCallServerStreaming() async throws { - let call = self.echo.makeExpandCall(.with { $0.text = "hello" }, callOptions: .init()) - for try await response in call.responseStream { - // Expand splits on spaces, so we only expect one response. - await assertThat(response, .is(.with { $0.text = "hello :)0( dnapxe ohce tfiwS" })) - } - } - - func testBidirectionalStreaming() async throws { - let requests = ["1 2", "3 4"].map { item in - Echo_EchoRequest.with { $0.text = item } - } - let responses = self.echo.update(requests, callOptions: .init()) - - var count = 0 - for try await response in responses { - switch count { - case 0: - await assertThat(response, .is(.with { $0.text = "1 2 :)0( etadpu ohce tfiwS" })) - case 1: - await assertThat(response, .is(.with { $0.text = "3 4 :)1( etadpu ohce tfiwS" })) - default: - XCTFail("Got more than 2 responses") - } - count += 1 - } - } - - func testMakingCallBidirectionalStreaming() async throws { - let call = self.echo.makeUpdateCall(callOptions: .init()) - try await call.requestStream.send(.with { $0.text = "1 2" }) - try await call.requestStream.send(.with { $0.text = "3 4" }) - call.requestStream.finish() - - var count = 0 - for try await response in call.responseStream { - switch count { - case 0: - await assertThat(response, .is(.with { $0.text = "1 2 :)0( etadpu ohce tfiwS" })) - case 1: - await assertThat(response, .is(.with { $0.text = "3 4 :)1( etadpu ohce tfiwS" })) - default: - XCTFail("Got more than 2 responses") - } - count += 1 - } - } -} diff --git a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift b/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift deleted file mode 100644 index 95c415f30..000000000 --- a/Tests/GRPCTests/AsyncAwaitSupport/XCTest+AsyncAwait.swift +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 XCTest - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -internal func XCTAssertThrowsError( - _ expression: @autoclosure () async throws -> T, - verify: (Error) -> Void = { _ in }, - file: StaticString = #filePath, - line: UInt = #line -) async { - do { - _ = try await expression() - XCTFail("Expression did not throw error", file: file, line: line) - } catch { - verify(error) - } -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -internal func XCTAssertNoThrowAsync( - _ expression: @autoclosure () async throws -> T, - file: StaticString = #filePath, - line: UInt = #line -) async { - do { - _ = try await expression() - } catch { - XCTFail("Expression throw error '\(error)'", file: file, line: line) - } -} - -private enum TaskResult { - case operation(Result) - case cancellation -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -func withTaskCancelledAfter( - nanoseconds: UInt64, - operation: @escaping @Sendable () async -> Result -) async throws { - try await withThrowingTaskGroup(of: TaskResult.self) { group in - group.addTask { - return .operation(await operation()) - } - - group.addTask { - try await Task.sleep(nanoseconds: nanoseconds) - return .cancellation - } - - // Only the sleeping task can throw if it's cancelled, in which case we want to throw. - let firstResult = try await group.next() - // A task completed, cancel the rest. - group.cancelAll() - - // Check which task completed. - switch firstResult { - case .cancellation: - () // Fine, what we expect. - case .operation: - XCTFail("Operation completed before cancellation") - case .none: - XCTFail("No tasks completed") - } - - // Wait for the other task. The operation cannot, only the sleeping task can. - try await group.waitForAll() - } -} diff --git a/Tests/GRPCTests/BasicEchoTestCase.swift b/Tests/GRPCTests/BasicEchoTestCase.swift deleted file mode 100644 index f8ccf2f58..000000000 --- a/Tests/GRPCTests/BasicEchoTestCase.swift +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2018, 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 Dispatch -import EchoImplementation -import EchoModel -import Foundation -import GRPC -import GRPCSampleData -import NIOCore -import XCTest - -#if canImport(NIOSSL) -import NIOSSL -#endif - -extension Echo_EchoRequest { - init(text: String) { - self = .with { - $0.text = text - } - } -} - -extension Echo_EchoResponse { - init(text: String) { - self = .with { - $0.text = text - } - } -} - -enum TransportSecurity { - case none - case anonymousClient - case mutualAuthentication -} - -class EchoTestCaseBase: GRPCTestCase { - // Things can be slow when running under TSAN; bias towards a really long timeout so that we know - // for sure a test is wedged rather than simply slow. - var defaultTestTimeout: TimeInterval = 120.0 - - var serverEventLoopGroup: EventLoopGroup! - var clientEventLoopGroup: EventLoopGroup! - - var transportSecurity: TransportSecurity { return .none } - - var server: Server! - var client: Echo_EchoNIOClient! - var port: Int! - - // Prefer POSIX: subclasses can override this and add availability checks to ensure NIOTS - // variants run where possible. - var networkPreference: NetworkPreference { - return .userDefined(.posix) - } - - func connectionBuilder() -> ClientConnection.Builder { - switch self.transportSecurity { - case .none: - return ClientConnection.insecure(group: self.clientEventLoopGroup) - - case .anonymousClient: - #if canImport(NIOSSL) - return ClientConnection.usingTLSBackedByNIOSSL(on: self.clientEventLoopGroup) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - #else - fatalError("NIOSSL must be imported to use TLS") - #endif - - case .mutualAuthentication: - #if canImport(NIOSSL) - return ClientConnection.usingTLSBackedByNIOSSL(on: self.clientEventLoopGroup) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withTLS(certificateChain: [SampleCertificate.client.certificate]) - .withTLS(privateKey: SamplePrivateKey.client) - #else - fatalError("NIOSSL must be imported to use TLS") - #endif - } - } - - func serverBuilder() -> Server.Builder { - switch self.transportSecurity { - case .none: - return Server.insecure(group: self.serverEventLoopGroup) - - case .anonymousClient: - #if canImport(NIOSSL) - return Server.usingTLSBackedByNIOSSL( - on: self.serverEventLoopGroup, - certificateChain: [SampleCertificate.server.certificate], - privateKey: SamplePrivateKey.server - ).withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - #else - fatalError("NIOSSL must be imported to use TLS") - #endif - - case .mutualAuthentication: - #if canImport(NIOSSL) - return Server.usingTLSBackedByNIOSSL( - on: self.serverEventLoopGroup, - certificateChain: [SampleCertificate.server.certificate], - privateKey: SamplePrivateKey.server - ) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withTLS(certificateVerification: .noHostnameVerification) - #else - fatalError("NIOSSL must be imported to use TLS") - #endif - } - } - - func makeServer() throws -> Server { - return try self.serverBuilder() - .withErrorDelegate(self.makeErrorDelegate()) - .withServiceProviders([self.makeEchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - } - - func makeClientConnection(port: Int) throws -> ClientConnection { - return self.connectionBuilder() - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: port) - } - - func makeEchoProvider() -> Echo_EchoProvider { return EchoProvider() } - - func makeErrorDelegate() -> ServerErrorDelegate? { return nil } - - func makeEchoClient(port: Int) throws -> Echo_EchoNIOClient { - return Echo_EchoNIOClient( - channel: try self.makeClientConnection(port: port), - defaultCallOptions: self.callOptionsWithLogger - ) - } - - override func setUp() { - super.setUp() - self.serverEventLoopGroup = PlatformSupport.makeEventLoopGroup( - loopCount: 1, - networkPreference: self.networkPreference - ) - self.server = try! self.makeServer() - - self.port = self.server.channel.localAddress!.port! - - self.clientEventLoopGroup = PlatformSupport.makeEventLoopGroup( - loopCount: 1, - networkPreference: self.networkPreference - ) - self.client = try! self.makeEchoClient(port: self.port) - } - - override func tearDown() { - // Some tests close the channel, so would throw here if called twice. - try? self.client.channel.close().wait() - XCTAssertNoThrow(try self.clientEventLoopGroup.syncShutdownGracefully()) - self.client = nil - self.clientEventLoopGroup = nil - - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully()) - self.server = nil - self.serverEventLoopGroup = nil - self.port = nil - - super.tearDown() - } -} - -extension EchoTestCaseBase { - func makeExpectation( - description: String, - expectedFulfillmentCount: Int = 1, - assertForOverFulfill: Bool = true - ) -> XCTestExpectation { - let expectation = self.expectation(description: description) - expectation.expectedFulfillmentCount = expectedFulfillmentCount - expectation.assertForOverFulfill = assertForOverFulfill - return expectation - } - - func makeStatusExpectation(expectedFulfillmentCount: Int = 1) -> XCTestExpectation { - return self.makeExpectation( - description: "Expecting status received", - expectedFulfillmentCount: expectedFulfillmentCount - ) - } - - func makeResponseExpectation(expectedFulfillmentCount: Int = 1) -> XCTestExpectation { - return self.makeExpectation( - description: "Expecting \(expectedFulfillmentCount) response(s)", - expectedFulfillmentCount: expectedFulfillmentCount - ) - } - - func makeRequestExpectation(expectedFulfillmentCount: Int = 1) -> XCTestExpectation { - return self.makeExpectation( - description: "Expecting \(expectedFulfillmentCount) request(s) to have been sent", - expectedFulfillmentCount: expectedFulfillmentCount - ) - } - - func makeInitialMetadataExpectation() -> XCTestExpectation { - return self.makeExpectation(description: "Expecting initial metadata") - } -} diff --git a/Tests/GRPCTests/CallPathTests.swift b/Tests/GRPCTests/CallPathTests.swift deleted file mode 100644 index 304a9598f..000000000 --- a/Tests/GRPCTests/CallPathTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020, 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 XCTest - -@testable import GRPC - -class CallPathTests: GRPCTestCase { - func testSplitPathNormal() { - let path = "/server/method" - let parsedPath = CallPath(requestURI: path) - let splitPath = path.split(separator: "/") - - XCTAssertEqual(splitPath[0], String.SubSequence(parsedPath!.service)) - XCTAssertEqual(splitPath[1], String.SubSequence(parsedPath!.method)) - } - - func testSplitPathTooShort() { - let path = "/badPath" - let parsedPath = CallPath(requestURI: path) - - XCTAssertNil(parsedPath) - } - - func testSplitPathTooLong() { - let path = "/server/method/discard" - let parsedPath = CallPath(requestURI: path) - let splitPath = path.split(separator: "/") - - XCTAssertEqual(splitPath[0], String.SubSequence(parsedPath!.service)) - XCTAssertEqual(splitPath[1], String.SubSequence(parsedPath!.method)) - } - - func testTrimPrefixEmpty() { - var toSplit = "".utf8[...] - let head = toSplit.trimPrefix(to: UInt8(ascii: "/")) - XCTAssertNil(head) - XCTAssertEqual(toSplit.count, 0) - } - - func testTrimPrefixAll() { - let source = "words" - var toSplit = source.utf8[...] - let head = toSplit.trimPrefix(to: UInt8(ascii: "/")) - XCTAssertEqual(head?.count, source.utf8.count) - XCTAssertEqual(toSplit.count, 0) - } - - func testTrimPrefixAndRest() { - let source = "words/moreWords" - var toSplit = source.utf8[...] - let head = toSplit.trimPrefix(to: UInt8(ascii: "/")) - XCTAssertEqual(head?.count, "words".utf8.count) - XCTAssertEqual(toSplit.count, "moreWords".utf8.count) - } -} diff --git a/Tests/GRPCTests/CallStartBehaviorTests.swift b/Tests/GRPCTests/CallStartBehaviorTests.swift deleted file mode 100644 index b2d04c1f3..000000000 --- a/Tests/GRPCTests/CallStartBehaviorTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020, 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 - -class CallStartBehaviorTests: GRPCTestCase { - func testFastFailure() { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - // If the policy was 'waitsForConnectivity' we'd continue attempting to connect with backoff - // and the RPC wouldn't complete until we call shutdown (because we're not setting a timeout). - let channel = ClientConnection.insecure(group: group) - .withCallStartBehavior(.fastFailure) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "http://unreachable.invalid", port: 0) - defer { - XCTAssertNoThrow(try channel.close().wait()) - } - - let echo = Echo_EchoNIOClient(channel: channel, defaultCallOptions: self.callOptionsWithLogger) - let get = echo.get(.with { $0.text = "Is anyone out there?" }) - - XCTAssertThrowsError(try get.response.wait()) - XCTAssertNoThrow(try get.status.wait()) - } -} diff --git a/Tests/GRPCTests/CapturingLogHandler.swift b/Tests/GRPCTests/CapturingLogHandler.swift deleted file mode 100644 index 6791ff958..000000000 --- a/Tests/GRPCTests/CapturingLogHandler.swift +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOConcurrencyHelpers - -import struct Foundation.Date -import class Foundation.DateFormatter - -/// A `LogHandler` factory which captures all logs emitted by the handlers it makes. -internal class CapturingLogHandlerFactory { - private var lock = NIOLock() - private var _logs: [CapturedLog] = [] - - private var logFormatter: CapturedLogFormatter? - - init(printWhenCaptured: Bool) { - if printWhenCaptured { - self.logFormatter = CapturedLogFormatter() - } else { - self.logFormatter = nil - } - } - - /// Returns all captured logs and empties the store of captured logs. - func clearCapturedLogs() -> [CapturedLog] { - return self.lock.withLock { - let logs = self._logs - self._logs.removeAll() - return logs - } - } - - /// Make a `LogHandler` whose logs will be recorded by this factory. - func make(_ label: String) -> LogHandler { - return CapturingLogHandler(label: label) { log in - self.lock.withLock { - self._logs.append(log) - } - - // If we have a formatter, print the log as well. - if let formatter = self.logFormatter { - print(formatter.string(for: log)) - } - } - } -} - -/// A captured log. -internal struct CapturedLog { - var label: String - var level: Logger.Level - var message: Logger.Message - var metadata: Logger.Metadata - var source: String - var file: String - var function: String - var line: UInt - var date: Date -} - -/// A log handler which captures all logs it records. -internal struct CapturingLogHandler: LogHandler { - private let capture: (CapturedLog) -> Void - - internal let label: String - internal var metadata: Logger.Metadata = [:] - internal var logLevel: Logger.Level = .trace - - fileprivate init(label: String, capture: @escaping (CapturedLog) -> Void) { - self.label = label - self.capture = capture - } - - internal func log( - level: Logger.Level, - message: Logger.Message, - metadata: Logger.Metadata?, - source: String, - file: String, - function: String, - line: UInt - ) { - let merged: Logger.Metadata - - if let metadata = metadata { - merged = self.metadata.merging(metadata, uniquingKeysWith: { _, new in new }) - } else { - merged = self.metadata - } - - let log = CapturedLog( - label: self.label, - level: level, - message: message, - metadata: merged, - source: source, - file: file, - function: function, - line: line, - date: Date() - ) - - self.capture(log) - } - - internal subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { - get { - return self.metadata[metadataKey] - } - set { - self.metadata[metadataKey] = newValue - } - } -} - -struct CapturedLogFormatter { - private var dateFormatter: DateFormatter - - init() { - self.dateFormatter = DateFormatter() - // We don't care about the date. - self.dateFormatter.dateFormat = "HH:mm:ss.SSS" - } - - func string(for log: CapturedLog) -> String { - let date = self.dateFormatter.string(from: log.date) - let level = log.level.short - - // Format the metadata. - let formattedMetadata = log.metadata - .sorted(by: { $0.key < $1.key }) - .map { key, value in "\(key)=\(value)" } - .joined(separator: " ") - - return "\(date) \(level) \(log.label): \(log.message) { \(formattedMetadata) }" - } -} - -extension Logger.Level { - fileprivate var short: String { - switch self { - case .info: - return "I" - case .debug: - return "D" - case .warning: - return "W" - case .error: - return "E" - case .critical: - return "C" - case .trace: - return "T" - case .notice: - return "N" - } - } -} diff --git a/Tests/GRPCTests/ClientCallTests.swift b/Tests/GRPCTests/ClientCallTests.swift deleted file mode 100644 index fc65fead1..000000000 --- a/Tests/GRPCTests/ClientCallTests.swift +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import NIOCore -import NIOPosix -import XCTest - -@testable import GRPC - -class ClientCallTests: GRPCTestCase { - private var group: MultiThreadedEventLoopGroup! - private var server: Server! - private var connection: ClientConnection! - - override func setUp() { - super.setUp() - - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.server = try! Server.insecure(group: self.group) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - let port = self.server.channel.localAddress!.port! - self.connection = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: port) - } - - override func tearDown() { - XCTAssertNoThrow(try self.connection.close().wait()) - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - - super.tearDown() - } - - private func makeCall( - path: String, - type: GRPCCallType - ) -> Call { - return self.connection.makeCall(path: path, type: type, callOptions: .init(), interceptors: []) - } - - private func get() -> Call { - return self.makeCall(path: "/echo.Echo/Get", type: .unary) - } - - private func collect() -> Call { - return self.makeCall(path: "/echo.Echo/Collect", type: .clientStreaming) - } - - private func expand() -> Call { - return self.makeCall(path: "/echo.Echo/Expand", type: .serverStreaming) - } - - private func update() -> Call { - return self.makeCall(path: "/echo.Echo/Update", type: .bidirectionalStreaming) - } - - private func makeStatusPromise() -> EventLoopPromise { - return self.connection.eventLoop.makePromise() - } - - /// Makes a response part handler which succeeds the promise when receiving the status and fails - /// it if an error is received. - private func makeResponsePartHandler( - for: Response.Type = Response.self, - completing promise: EventLoopPromise - ) -> (GRPCClientResponsePart) -> Void { - return { part in - switch part { - case .metadata, .message: - () - case let .end(status, _): - promise.succeed(status) - } - } - } - - // MARK: - Tests - - func testFullyManualUnary() throws { - let get = self.get() - - let statusPromise = self.makeStatusPromise() - get.invoke( - onError: statusPromise.fail(_:), - onResponsePart: self.makeResponsePartHandler(completing: statusPromise) - ) - - let f1 = get.send(.metadata(get.options.customMetadata)) - let f2 = get.send(.message(.with { $0.text = "get" }, .init(compress: false, flush: false))) - let f3 = get.send(.end) - - // '.end' will flush, so we can wait on the futures now. - assertThat(try f1.wait(), .doesNotThrow()) - assertThat(try f2.wait(), .doesNotThrow()) - assertThat(try f3.wait(), .doesNotThrow()) - - // Status should be ok. - assertThat(try statusPromise.futureResult.wait(), .hasCode(.ok)) - } - - func testUnaryCall() { - let get = self.get() - - let promise = self.makeStatusPromise() - get.invokeUnaryRequest( - .with { $0.text = "get" }, - onStart: {}, - onError: promise.fail(_:), - onResponsePart: self.makeResponsePartHandler(completing: promise) - ) - - assertThat(try promise.futureResult.wait(), .hasCode(.ok)) - } - - func testClientStreaming() { - let collect = self.collect() - - let promise = self.makeStatusPromise() - collect.invokeStreamingRequests( - onStart: {}, - onError: promise.fail(_:), - onResponsePart: self.makeResponsePartHandler(completing: promise) - ) - collect.send( - .message(.with { $0.text = "collect" }, .init(compress: false, flush: false)), - promise: nil - ) - collect.send(.end, promise: nil) - - assertThat(try promise.futureResult.wait(), .hasCode(.ok)) - } - - func testServerStreaming() { - let expand = self.expand() - - let promise = self.makeStatusPromise() - expand.invokeUnaryRequest( - .with { $0.text = "expand" }, - onStart: {}, - onError: promise.fail(_:), - onResponsePart: self.makeResponsePartHandler(completing: promise) - ) - - assertThat(try promise.futureResult.wait(), .hasCode(.ok)) - } - - func testBidirectionalStreaming() { - let update = self.update() - - let promise = self.makeStatusPromise() - update.invokeStreamingRequests( - onStart: {}, - onError: promise.fail(_:), - onResponsePart: self.makeResponsePartHandler(completing: promise) - ) - update.send( - .message(.with { $0.text = "update" }, .init(compress: false, flush: false)), - promise: nil - ) - update.send(.end, promise: nil) - - assertThat(try promise.futureResult.wait(), .hasCode(.ok)) - } - - func testSendBeforeInvoke() throws { - let get = self.get() - assertThat(try get.send(.end).wait(), .throws()) - } - - func testCancelBeforeInvoke() throws { - let get = self.get() - XCTAssertNoThrow(try get.cancel().wait()) - } - - func testCancelMidRPC() throws { - let get = self.get() - let promise = self.makeStatusPromise() - get.invoke( - onError: promise.fail(_:), - onResponsePart: self.makeResponsePartHandler(completing: promise) - ) - - // Cancellation should succeed. - assertThat(try get.cancel().wait(), .doesNotThrow()) - - assertThat(try promise.futureResult.wait(), .hasCode(.cancelled)) - - // Cancellation should now fail, we've already cancelled. - assertThat(try get.cancel().wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self))) - } - - func testWriteMessageOnStart() throws { - // This test isn't deterministic so run a bunch of iterations. - for _ in 0 ..< 100 { - let call = self.update() - let promise = call.eventLoop.makePromise(of: Void.self) - let finished = call.eventLoop.makePromise(of: Void.self) - - call.invokeStreamingRequests { - // Send in onStart. - call.send( - .message(.with { $0.text = "foo" }, .init(compress: false, flush: false)), - promise: promise - ) - } onError: { _ in // ignore errors - } onResponsePart: { - switch $0 { - case .metadata, .message: - () - case .end: - finished.succeed(()) - } - } - - // End the stream. - promise.futureResult.whenComplete { _ in - call.send(.end, promise: nil) - } - - do { - try promise.futureResult.wait() - try finished.futureResult.wait() - } catch { - // Stop on the first error. - XCTFail("Unexpected error: \(error)") - return - } - } - } -} diff --git a/Tests/GRPCTests/ClientCancellingTests.swift b/Tests/GRPCTests/ClientCancellingTests.swift deleted file mode 100644 index a12d9b828..000000000 --- a/Tests/GRPCTests/ClientCancellingTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import GRPC -import XCTest - -class ClientCancellingTests: EchoTestCaseBase { - func testUnary() { - let statusReceived = self.expectation(description: "status received") - let responseReceived = self.expectation(description: "response received") - - let call = client.get(Echo_EchoRequest(text: "foo bar baz")) - call.cancel(promise: nil) - - call.response.whenFailure { error in - XCTAssertEqual((error as? GRPCStatus)?.code, .cancelled) - responseReceived.fulfill() - } - - call.status.whenSuccess { status in - XCTAssertEqual(status.code, .cancelled) - statusReceived.fulfill() - } - - waitForExpectations(timeout: self.defaultTestTimeout) - } - - func testClientStreaming() throws { - let statusReceived = self.expectation(description: "status received") - let responseReceived = self.expectation(description: "response received") - - let call = client.collect() - call.cancel(promise: nil) - - call.response.whenFailure { error in - XCTAssertEqual((error as? GRPCStatus)?.code, .cancelled) - responseReceived.fulfill() - } - - call.status.whenSuccess { status in - XCTAssertEqual(status.code, .cancelled) - statusReceived.fulfill() - } - - waitForExpectations(timeout: self.defaultTestTimeout) - } - - func testServerStreaming() { - let statusReceived = self.expectation(description: "status received") - - let call = client.expand(Echo_EchoRequest(text: "foo bar baz")) { _ in - XCTFail("response should not be received after cancelling call") - } - call.cancel(promise: nil) - - call.status.whenSuccess { status in - XCTAssertEqual(status.code, .cancelled) - statusReceived.fulfill() - } - - waitForExpectations(timeout: self.defaultTestTimeout) - } - - func testBidirectionalStreaming() { - let statusReceived = self.expectation(description: "status received") - - let call = client.update { _ in - XCTFail("response should not be received after cancelling call") - } - call.cancel(promise: nil) - - call.status.whenSuccess { status in - XCTAssertEqual(status.code, .cancelled) - statusReceived.fulfill() - } - - waitForExpectations(timeout: self.defaultTestTimeout) - } -} diff --git a/Tests/GRPCTests/ClientClosedChannelTests.swift b/Tests/GRPCTests/ClientClosedChannelTests.swift deleted file mode 100644 index ddde82ae4..000000000 --- a/Tests/GRPCTests/ClientClosedChannelTests.swift +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import GRPC -import NIOCore -import XCTest - -class ClientClosedChannelTests: EchoTestCaseBase { - func testUnaryOnClosedConnection() throws { - let initialMetadataExpectation = self.makeInitialMetadataExpectation() - let responseExpectation = self.makeResponseExpectation() - let statusExpectation = self.makeStatusExpectation() - - self.client.channel.close().map { - self.client.get(Echo_EchoRequest(text: "foo")) - }.whenSuccess { get in - get.initialMetadata.assertError(fulfill: initialMetadataExpectation) - get.response.assertError(fulfill: responseExpectation) - get.status.map { $0.code }.assertEqual(.unavailable, fulfill: statusExpectation) - } - - self.wait( - for: [initialMetadataExpectation, responseExpectation, statusExpectation], - timeout: self.defaultTestTimeout - ) - } - - func testClientStreamingOnClosedConnection() throws { - let initialMetadataExpectation = self.makeInitialMetadataExpectation() - let responseExpectation = self.makeResponseExpectation() - let statusExpectation = self.makeStatusExpectation() - - self.client.channel.close().map { - self.client.collect() - }.whenSuccess { collect in - collect.initialMetadata.assertError(fulfill: initialMetadataExpectation) - collect.response.assertError(fulfill: responseExpectation) - collect.status.map { $0.code }.assertEqual(.unavailable, fulfill: statusExpectation) - } - - self.wait( - for: [initialMetadataExpectation, responseExpectation, statusExpectation], - timeout: self.defaultTestTimeout - ) - } - - func testClientStreamingWhenConnectionIsClosedBetweenMessages() throws { - let statusExpectation = self.makeStatusExpectation() - let responseExpectation = self.makeResponseExpectation() - let requestExpectation = self.makeRequestExpectation(expectedFulfillmentCount: 3) - - let collect = self.client.collect() - - collect.sendMessage(Echo_EchoRequest(text: "foo")).peek { - requestExpectation.fulfill() - }.flatMap { - collect.sendMessage(Echo_EchoRequest(text: "bar")) - }.peek { - requestExpectation.fulfill() - }.flatMap { - self.client.channel.close() - }.peekError { error in - XCTFail("Encountered error before or during closing the connection: \(error)") - }.flatMap { - collect.sendMessage(Echo_EchoRequest(text: "baz")) - }.assertError(fulfill: requestExpectation) - - collect.response.assertError(fulfill: responseExpectation) - collect.status.map { $0.code }.assertEqual(.unavailable, fulfill: statusExpectation) - - self.wait( - for: [statusExpectation, responseExpectation, requestExpectation], - timeout: self.defaultTestTimeout - ) - } - - func testServerStreamingOnClosedConnection() throws { - let initialMetadataExpectation = self.makeInitialMetadataExpectation() - let statusExpectation = self.makeStatusExpectation() - - self.client.channel.close().map { - self.client.expand(Echo_EchoRequest(text: "foo")) { response in - XCTFail("No response expected but got: \(response)") - } - }.whenSuccess { expand in - expand.initialMetadata.assertError(fulfill: initialMetadataExpectation) - expand.status.map { $0.code }.assertEqual(.unavailable, fulfill: statusExpectation) - } - - self.wait( - for: [initialMetadataExpectation, statusExpectation], - timeout: self.defaultTestTimeout - ) - } - - func testBidirectionalStreamingOnClosedConnection() throws { - let initialMetadataExpectation = self.makeInitialMetadataExpectation() - let statusExpectation = self.makeStatusExpectation() - - self.client.channel.close().map { - self.client.update { response in - XCTFail("No response expected but got: \(response)") - } - }.whenSuccess { update in - update.initialMetadata.assertError(fulfill: initialMetadataExpectation) - update.status.map { $0.code }.assertEqual(.unavailable, fulfill: statusExpectation) - } - - self.wait( - for: [initialMetadataExpectation, statusExpectation], - timeout: self.defaultTestTimeout - ) - } - - func testBidirectionalStreamingWhenConnectionIsClosedBetweenMessages() throws { - let statusExpectation = self.makeStatusExpectation() - let requestExpectation = self.makeRequestExpectation(expectedFulfillmentCount: 3) - - // We can't make any assertions about the number of responses we will receive before closing - // the connection; just ignore all responses. - let update = self.client.update { _ in } - - update.sendMessage(Echo_EchoRequest(text: "foo")).peek { - requestExpectation.fulfill() - }.flatMap { - update.sendMessage(Echo_EchoRequest(text: "bar")) - }.peek { - requestExpectation.fulfill() - }.flatMap { - self.client.channel.close() - }.peekError { error in - XCTFail("Encountered error before or during closing the connection: \(error)") - }.flatMap { - update.sendMessage(Echo_EchoRequest(text: "baz")) - }.assertError(fulfill: requestExpectation) - - update.status.map { $0.code }.assertEqual(.unavailable, fulfill: statusExpectation) - - self.wait(for: [statusExpectation, requestExpectation], timeout: self.defaultTestTimeout) - } - - func testBidirectionalStreamingWithNoPromiseWhenConnectionIsClosedBetweenMessages() throws { - let statusExpectation = self.makeStatusExpectation() - - let update = self.client.update { response in - XCTFail("No response expected but got: \(response)") - } - - update.sendMessage(.with { $0.text = "0" }).flatMap { - self.client.channel.close() - }.whenSuccess { - update.sendMessage(.with { $0.text = "1" }, promise: nil) - } - - update.status.map { $0.code }.assertEqual(.unavailable, fulfill: statusExpectation) - self.wait(for: [statusExpectation], timeout: self.defaultTestTimeout) - } -} diff --git a/Tests/GRPCTests/ClientConnectionBackoffTests.swift b/Tests/GRPCTests/ClientConnectionBackoffTests.swift deleted file mode 100644 index 3d6fd9a34..000000000 --- a/Tests/GRPCTests/ClientConnectionBackoffTests.swift +++ /dev/null @@ -1,254 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import Foundation -import GRPC -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix -import XCTest - -class ClientConnectionBackoffTests: GRPCTestCase { - let port = 8080 - - var client: ClientConnection! - var server: EventLoopFuture! - - var serverGroup: EventLoopGroup! - var clientGroup: EventLoopGroup! - - var connectionStateRecorder = RecordingConnectivityDelegate() - - override func setUp() { - super.setUp() - self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.clientGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - // We have additional state changes during tear down, in some cases we can over-fulfill a test - // expectation which causes false negatives. - self.client.connectivity.delegate = nil - - if let server = self.server { - XCTAssertNoThrow(try server.flatMap { $0.channel.close() }.wait()) - } - XCTAssertNoThrow(try? self.serverGroup.syncShutdownGracefully()) - self.server = nil - self.serverGroup = nil - - // We don't always expect a client to be closed cleanly, since in some cases we deliberately - // timeout the connection. - try? self.client.close().wait() - XCTAssertNoThrow(try self.clientGroup.syncShutdownGracefully()) - self.client = nil - self.clientGroup = nil - - super.tearDown() - } - - func makeServer() -> EventLoopFuture { - return Server.insecure(group: self.serverGroup) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: self.port) - } - - func connectionBuilder() -> ClientConnection.Builder { - return ClientConnection.insecure(group: self.clientGroup) - .withConnectivityStateDelegate(self.connectionStateRecorder) - .withConnectionBackoff(maximum: .milliseconds(100)) - .withConnectionTimeout(minimum: .milliseconds(100)) - .withBackgroundActivityLogger(self.clientLogger) - } - - func testClientConnectionFailsWithNoBackoff() throws { - self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ] - ) - } - - self.client = self.connectionBuilder() - .withConnectionReestablishment(enabled: false) - .connect(host: "localhost", port: self.port) - - // Start an RPC to trigger creating a channel. - let echo = Echo_EchoNIOClient( - channel: self.client, - defaultCallOptions: self.callOptionsWithLogger - ) - _ = echo.get(.with { $0.text = "foo" }) - - self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) - } - - func testClientConnectionFailureIsLimited() throws { - self.connectionStateRecorder.expectChanges(4) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .transientFailure), - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .shutdown), - ] - ) - } - - self.client = self.connectionBuilder() - .withConnectionBackoff(retries: .upTo(1)) - .connect(host: "localhost", port: self.port) - - // Start an RPC to trigger creating a channel. - let echo = Echo_EchoNIOClient( - channel: self.client, - defaultCallOptions: self.callOptionsWithLogger - ) - _ = echo.get(.with { $0.text = "foo" }) - - self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) - } - - func testClientEventuallyConnects() throws { - self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ] - ) - } - - // Start the client first. - self.client = self.connectionBuilder() - .connect(host: "localhost", port: self.port) - - // Start an RPC to trigger creating a channel. - let echo = Echo_EchoNIOClient( - channel: self.client, - defaultCallOptions: self.callOptionsWithLogger - ) - _ = echo.get(.with { $0.text = "foo" }) - - self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) - - self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .ready), - ] - ) - } - - self.server = self.makeServer() - let serverStarted = self.expectation(description: "server started") - self.server.assertSuccess(fulfill: serverStarted) - - self.wait(for: [serverStarted], timeout: 5.0) - self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) - } - - func testClientReconnectsAutomatically() throws { - // Wait for the server to start. - self.server = self.makeServer() - let server = try self.server.wait() - - // Prepare the delegate so it expects the connection to hit `.ready`. - self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .ready), - ] - ) - } - - // Configure the client backoff to have a short backoff. - self.client = self.connectionBuilder() - .withConnectionBackoff(maximum: .seconds(2)) - .connect(host: "localhost", port: self.port) - - // Start an RPC to trigger creating a channel, it's a streaming RPC so that when the server is - // killed, the client still has one active RPC and transitions to transient failure (rather than - // idle if there were no active RPCs). - let echo = Echo_EchoNIOClient( - channel: self.client, - defaultCallOptions: self.callOptionsWithLogger - ) - _ = echo.update { _ in } - - // Wait for the connection to be ready. - self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) - - // Now that we have a healthy connection, prepare for two transient failures: - // 1. when the server has been killed, and - // 2. when the client attempts to reconnect. - self.connectionStateRecorder.expectChanges(3) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .ready, to: .transientFailure), - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ] - ) - } - - // Okay, kill the server! - try server.close().wait() - try self.serverGroup.syncShutdownGracefully() - self.server = nil - self.serverGroup = nil - - // Our connection should fail now. - self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) - - // Get ready for the new healthy connection. - self.connectionStateRecorder.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .ready), - ] - ) - } - - // This should succeed once we get a connection again. - let get = echo.get(.with { $0.text = "hello" }) - - // Start a new server. - self.serverGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.server = self.makeServer() - - self.connectionStateRecorder.waitForExpectedChanges(timeout: .seconds(5)) - - // The call should be able to succeed now. - XCTAssertEqual(try get.status.map { $0.code }.wait(), .ok) - - try self.client.close().wait() - } -} diff --git a/Tests/GRPCTests/ClientEventLoopPreferenceTests.swift b/Tests/GRPCTests/ClientEventLoopPreferenceTests.swift deleted file mode 100644 index 77885ce76..000000000 --- a/Tests/GRPCTests/ClientEventLoopPreferenceTests.swift +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import GRPC -import NIOCore -import NIOPosix -import XCTest - -final class ClientEventLoopPreferenceTests: GRPCTestCase { - private var group: MultiThreadedEventLoopGroup! - - private var serverLoop: EventLoop! - private var clientLoop: EventLoop! - private var clientCallbackLoop: EventLoop! - - private var server: Server! - private var connection: ClientConnection! - - private var echo: Echo_EchoNIOClient { - let options = CallOptions( - eventLoopPreference: .exact(self.clientCallbackLoop), - logger: self.clientLogger - ) - - return Echo_EchoNIOClient(channel: self.connection, defaultCallOptions: options) - } - - override func setUp() { - super.setUp() - - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 3) - self.serverLoop = self.group.next() - self.clientLoop = self.group.next() - self.clientCallbackLoop = self.group.next() - - XCTAssert(self.serverLoop !== self.clientLoop) - XCTAssert(self.serverLoop !== self.clientCallbackLoop) - XCTAssert(self.clientLoop !== self.clientCallbackLoop) - - self.server = try! Server.insecure(group: self.serverLoop) - .withLogger(self.serverLogger) - .withServiceProviders([EchoProvider()]) - .bind(host: "localhost", port: 0) - .wait() - - self.connection = ClientConnection.insecure(group: self.clientLoop) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: self.server.channel.localAddress!.port!) - } - - override func tearDown() { - XCTAssertNoThrow(try self.connection.close().wait()) - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - - super.tearDown() - } - - private func assertClientCallbackEventLoop(_ eventLoop: EventLoop, line: UInt = #line) { - XCTAssert(eventLoop === self.clientCallbackLoop, line: line) - } - - func testUnaryWithDifferentEventLoop() throws { - let get = self.echo.get(.with { $0.text = "Hello!" }) - - self.assertClientCallbackEventLoop(get.eventLoop) - self.assertClientCallbackEventLoop(get.initialMetadata.eventLoop) - self.assertClientCallbackEventLoop(get.response.eventLoop) - self.assertClientCallbackEventLoop(get.trailingMetadata.eventLoop) - self.assertClientCallbackEventLoop(get.status.eventLoop) - - assertThat(try get.response.wait(), .is(.with { $0.text = "Swift echo get: Hello!" })) - assertThat(try get.status.wait(), .hasCode(.ok)) - } - - func testClientStreamingWithDifferentEventLoop() throws { - let collect = self.echo.collect() - - self.assertClientCallbackEventLoop(collect.eventLoop) - self.assertClientCallbackEventLoop(collect.initialMetadata.eventLoop) - self.assertClientCallbackEventLoop(collect.response.eventLoop) - self.assertClientCallbackEventLoop(collect.trailingMetadata.eventLoop) - self.assertClientCallbackEventLoop(collect.status.eventLoop) - - XCTAssertNoThrow(try collect.sendMessage(.with { $0.text = "a" }).wait()) - XCTAssertNoThrow(try collect.sendEnd().wait()) - - assertThat(try collect.response.wait(), .is(.with { $0.text = "Swift echo collect: a" })) - assertThat(try collect.status.wait(), .hasCode(.ok)) - } - - func testServerStreamingWithDifferentEventLoop() throws { - let response = self.clientCallbackLoop.makePromise(of: Void.self) - - let expand = self.echo.expand(.with { $0.text = "a" }) { _ in - self.clientCallbackLoop.preconditionInEventLoop() - response.succeed(()) - } - - self.assertClientCallbackEventLoop(expand.eventLoop) - self.assertClientCallbackEventLoop(expand.initialMetadata.eventLoop) - self.assertClientCallbackEventLoop(expand.trailingMetadata.eventLoop) - self.assertClientCallbackEventLoop(expand.status.eventLoop) - - XCTAssertNoThrow(try response.futureResult.wait()) - assertThat(try expand.status.wait(), .hasCode(.ok)) - } - - func testBidirectionalStreamingWithDifferentEventLoop() throws { - let response = self.clientCallbackLoop.makePromise(of: Void.self) - - let update = self.echo.update { _ in - self.clientCallbackLoop.preconditionInEventLoop() - response.succeed(()) - } - - self.assertClientCallbackEventLoop(update.eventLoop) - self.assertClientCallbackEventLoop(update.initialMetadata.eventLoop) - self.assertClientCallbackEventLoop(update.trailingMetadata.eventLoop) - self.assertClientCallbackEventLoop(update.status.eventLoop) - - XCTAssertNoThrow(try update.sendMessage(.with { $0.text = "a" }).wait()) - XCTAssertNoThrow(try update.sendEnd().wait()) - - XCTAssertNoThrow(try response.futureResult.wait()) - assertThat(try update.status.wait(), .hasCode(.ok)) - } -} diff --git a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift b/Tests/GRPCTests/ClientInterceptorPipelineTests.swift deleted file mode 100644 index d20860586..000000000 --- a/Tests/GRPCTests/ClientInterceptorPipelineTests.swift +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOEmbedded -import NIOHPACK -import XCTest - -@testable import GRPC - -class ClientInterceptorPipelineTests: GRPCTestCase { - override func setUp() { - super.setUp() - self.embeddedEventLoop = EmbeddedEventLoop() - } - - private var embeddedEventLoop: EmbeddedEventLoop! - - private func makePipeline( - requests: Request.Type = Request.self, - responses: Response.Type = Response.self, - details: CallDetails? = nil, - interceptors: [ClientInterceptor] = [], - errorDelegate: ClientErrorDelegate? = nil, - onError: @escaping (Error) -> Void = { _ in }, - onCancel: @escaping (EventLoopPromise?) -> Void = { _ in }, - onRequestPart: @escaping (GRPCClientRequestPart, EventLoopPromise?) -> Void, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void - ) -> ClientInterceptorPipeline { - let callDetails = details ?? self.makeCallDetails() - return ClientInterceptorPipeline( - eventLoop: self.embeddedEventLoop, - details: callDetails, - logger: callDetails.options.logger, - interceptors: interceptors, - errorDelegate: errorDelegate, - onError: onError, - onCancel: onCancel, - onRequestPart: onRequestPart, - onResponsePart: onResponsePart - ) - } - - private func makeCallDetails(timeLimit: TimeLimit = .none) -> CallDetails { - return CallDetails( - type: .unary, - path: "ignored", - authority: "ignored", - scheme: "ignored", - options: CallOptions(timeLimit: timeLimit, logger: self.clientLogger) - ) - } - - func testEmptyPipeline() throws { - var requestParts: [GRPCClientRequestPart] = [] - var responseParts: [GRPCClientResponsePart] = [] - - let pipeline = self.makePipeline( - requests: String.self, - responses: String.self, - onRequestPart: { request, promise in - requestParts.append(request) - XCTAssertNil(promise) - }, - onResponsePart: { responseParts.append($0) } - ) - - // Write some request parts. - pipeline.send(.metadata([:]), promise: nil) - pipeline.send(.message("foo", .init(compress: false, flush: false)), promise: nil) - pipeline.send(.end, promise: nil) - - XCTAssertEqual(requestParts.count, 3) - XCTAssertEqual(requestParts[0].metadata, [:]) - let (message, metadata) = try assertNotNil(requestParts[1].message) - XCTAssertEqual(message, "foo") - XCTAssertEqual(metadata, .init(compress: false, flush: false)) - XCTAssertTrue(requestParts[2].isEnd) - - // Write some responses parts. - pipeline.receive(.metadata([:])) - pipeline.receive(.message("bar")) - pipeline.receive(.end(.ok, [:])) - - XCTAssertEqual(responseParts.count, 3) - XCTAssertEqual(responseParts[0].metadata, [:]) - XCTAssertEqual(responseParts[1].message, "bar") - let (status, trailers) = try assertNotNil(responseParts[2].end) - XCTAssertEqual(status, .ok) - XCTAssertEqual(trailers, [:]) - } - - func testPipelineWhenClosed() throws { - let pipeline = self.makePipeline( - requests: String.self, - responses: String.self, - onRequestPart: { _, promise in - XCTAssertNil(promise) - }, - onResponsePart: { _ in } - ) - - // Fire an error; this should close the pipeline. - struct DummyError: Error {} - pipeline.errorCaught(DummyError()) - - // We're closed, writes should fail. - let writePromise = pipeline.eventLoop.makePromise(of: Void.self) - pipeline.send(.end, promise: writePromise) - XCTAssertThrowsError(try writePromise.futureResult.wait()) - - // As should cancellation. - let cancelPromise = pipeline.eventLoop.makePromise(of: Void.self) - pipeline.cancel(promise: cancelPromise) - XCTAssertThrowsError(try cancelPromise.futureResult.wait()) - - // And reads should be ignored. (We only expect errors in the response handler.) - pipeline.receive(.metadata([:])) - } - - func testPipelineWithTimeout() throws { - var cancelled = false - var timedOut = false - - class FailOnCancel: ClientInterceptor, - @unchecked Sendable - { - override func cancel( - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - XCTFail("Unexpected cancellation") - context.cancel(promise: promise) - } - } - - let deadline = NIODeadline.uptimeNanoseconds(100) - let pipeline = self.makePipeline( - requests: String.self, - responses: String.self, - details: self.makeCallDetails(timeLimit: .deadline(deadline)), - interceptors: [FailOnCancel()], - onError: { error in - assertThat(error, .is(.instanceOf(GRPCError.RPCTimedOut.self))) - assertThat(timedOut, .is(false)) - timedOut = true - }, - onCancel: { promise in - assertThat(cancelled, .is(false)) - cancelled = true - // We don't expect a promise: this cancellation is fired by the pipeline. - assertThat(promise, .is(.none())) - }, - onRequestPart: { _, _ in - XCTFail("Unexpected request part") - }, - onResponsePart: { _ in - XCTFail("Unexpected response part") - } - ) - - // Trigger the timeout. - self.embeddedEventLoop.advanceTime(to: deadline) - assertThat(timedOut, .is(true)) - - // We'll receive a cancellation; we only get this 'onCancel' callback. We'll fail in the - // interceptor if a cancellation is received. - assertThat(cancelled, .is(true)) - - // Pipeline should be torn down. Writes and cancellation should fail. - let p1 = pipeline.eventLoop.makePromise(of: Void.self) - pipeline.send(.end, promise: p1) - assertThat(try p1.futureResult.wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self))) - - let p2 = pipeline.eventLoop.makePromise(of: Void.self) - pipeline.cancel(promise: p2) - assertThat(try p2.futureResult.wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self))) - - // Reads should be ignored too. (We'll fail in `onRequestPart` if this goes through.) - pipeline.receive(.metadata([:])) - } - - func testTimeoutIsCancelledOnCompletion() throws { - let deadline = NIODeadline.uptimeNanoseconds(100) - var cancellations = 0 - - let pipeline = self.makePipeline( - requests: String.self, - responses: String.self, - details: self.makeCallDetails(timeLimit: .deadline(deadline)), - onCancel: { promise in - assertThat(cancellations, .is(0)) - cancellations += 1 - // We don't expect a promise: this cancellation is fired by the pipeline. - assertThat(promise, .is(.none())) - }, - onRequestPart: { _, _ in - XCTFail("Unexpected request part") - }, - onResponsePart: { part in - // We only expect the end. - assertThat(part.end, .is(.some())) - } - ) - - // Read the end part. - pipeline.receive(.end(.ok, [:])) - // Just a single cancellation. - assertThat(cancellations, .is(1)) - - // Pass the deadline. - self.embeddedEventLoop.advanceTime(to: deadline) - // We should still have just the one cancellation. - assertThat(cancellations, .is(1)) - } - - func testPipelineWithInterceptor() throws { - // We're not testing much here, just that the interceptors are in the right order, from outbound - // to inbound. - let recorder = RecordingInterceptor() - let pipeline = self.makePipeline( - interceptors: [StringRequestReverser(), recorder], - onRequestPart: { _, _ in }, - onResponsePart: { _ in } - ) - - pipeline.send(.message("foo", .init(compress: false, flush: false)), promise: nil) - XCTAssertEqual(recorder.requestParts.count, 1) - let (message, _) = try assertNotNil(recorder.requestParts[0].message) - XCTAssertEqual(message, "oof") - } - - func testErrorDelegateIsCalled() throws { - final class Delegate: ClientErrorDelegate { - let expectedError: GRPCError.InvalidState - let file: StaticString? - let line: Int? - - init( - expected: GRPCError.InvalidState, - file: StaticString?, - line: Int? - ) { - precondition(file == nil && line == nil || file != nil && line != nil) - self.expectedError = expected - self.file = file - self.line = line - } - - func didCatchError(_ error: Error, logger: Logger, file: StaticString, line: Int) { - XCTAssertEqual(error as? GRPCError.InvalidState, self.expectedError) - - // Check the file and line, if expected. - if let expectedFile = self.file, let expectedLine = self.line { - XCTAssertEqual("\(file)", "\(expectedFile)") // StaticString isn't Equatable - XCTAssertEqual(line, expectedLine) - } - } - } - - func doTest(withDelegate delegate: Delegate, error: Error) { - let pipeline = self.makePipeline( - requests: String.self, - responses: String.self, - errorDelegate: delegate, - onRequestPart: { _, _ in }, - onResponsePart: { _ in } - ) - pipeline.errorCaught(error) - } - - let invalidState = GRPCError.InvalidState("invalid state") - let withContext = GRPCError.WithContext(invalidState) - - doTest( - withDelegate: .init(expected: invalidState, file: withContext.file, line: withContext.line), - error: withContext - ) - - doTest( - withDelegate: .init(expected: invalidState, file: nil, line: nil), - error: invalidState - ) - } -} - -// MARK: - Test Interceptors - -/// A simple interceptor which records and then forwards and request and response parts it sees. -class RecordingInterceptor: ClientInterceptor, @unchecked - Sendable -{ - var requestParts: [GRPCClientRequestPart] = [] - var responseParts: [GRPCClientResponsePart] = [] - - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - self.requestParts.append(part) - context.send(part, promise: promise) - } - - override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - self.responseParts.append(part) - context.receive(part) - } -} - -/// An interceptor which reverses string request messages. -class StringRequestReverser: ClientInterceptor, @unchecked Sendable { - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - switch part { - case let .message(value, metadata): - context.send(.message(String(value.reversed()), metadata), promise: promise) - default: - context.send(part, promise: promise) - } - } -} - -// MARK: - Request/Response part helpers - -extension GRPCClientRequestPart { - var metadata: HPACKHeaders? { - switch self { - case let .metadata(headers): - return headers - case .message, .end: - return nil - } - } - - var message: (Request, MessageMetadata)? { - switch self { - case let .message(request, metadata): - return (request, metadata) - case .metadata, .end: - return nil - } - } - - var isEnd: Bool { - switch self { - case .end: - return true - case .metadata, .message: - return false - } - } -} - -extension GRPCClientResponsePart { - var metadata: HPACKHeaders? { - switch self { - case let .metadata(headers): - return headers - case .message, .end: - return nil - } - } - - var message: Response? { - switch self { - case let .message(response): - return response - case .metadata, .end: - return nil - } - } - - var end: (GRPCStatus, HPACKHeaders)? { - switch self { - case let .end(status, trailers): - return (status, trailers) - case .metadata, .message: - return nil - } - } -} diff --git a/Tests/GRPCTests/ClientQuiescingTests.swift b/Tests/GRPCTests/ClientQuiescingTests.swift deleted file mode 100644 index 77c69c203..000000000 --- a/Tests/GRPCTests/ClientQuiescingTests.swift +++ /dev/null @@ -1,507 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import GRPC -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix -import XCTest - -internal final class ClientQuiescingTests: GRPCTestCase { - private var group: EventLoopGroup! - private var channel: GRPCChannel! - private var server: Server! - private let tracker = RPCTracker() - - private var echo: Echo_EchoNIOClient { - return Echo_EchoNIOClient(channel: self.channel) - } - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 2) - self.server = try! Server.insecure(group: self.group) - .withLogger(self.serverLogger) - .withServiceProviders([EchoProvider()]) - .bind(host: "127.0.0.1", port: 1234) - .wait() - } - - override func tearDown() { - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - // We don't shutdown the client: it will have been shutdown by the test case. - super.tearDown() - } - - private func setUpClientConnection() { - self.channel = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "127.0.0.1", port: self.server!.channel.localAddress!.port!) - } - - private func setUpChannelPool(useSingleEventLoop: Bool = false) { - // Only throws for TLS which we aren't using here. - self.channel = try! GRPCChannelPool.with( - target: .host("127.0.0.1", port: self.server!.channel.localAddress!.port!), - transportSecurity: .plaintext, - eventLoopGroup: useSingleEventLoop ? self.group.next() : self.group - ) { - $0.connectionPool.connectionsPerEventLoop = 1 - $0.connectionPool.maxWaitersPerEventLoop = 100 - $0.backgroundActivityLogger = self.clientLogger - } - } - - private enum ChannelKind { - case single - case pooled - } - - private func setUpChannel(kind: ChannelKind) { - switch kind { - case .single: - self.setUpClientConnection() - case .pooled: - self.setUpChannelPool() - } - } - - private func startRPC( - withTracking: Bool = true - ) -> ClientStreamingCall { - if withTracking { - self.tracker.assert(.active) - self.tracker.willStartRPC() - } - - let collect = self.echo.collect(callOptions: self.callOptionsWithLogger) - - if withTracking { - collect.status.whenSuccess { status in - self.tracker.didFinishRPC() - XCTAssert(status.isOk) - } - } - - return collect - } - - private func assertConnectionEstablished() { - self.tracker.assert(.active) - let rpc = self.startRPC() - XCTAssertNoThrow(try rpc.sendEnd().wait()) - XCTAssert(try rpc.status.wait().isOk) - self.tracker.assert(.active) - } - - private func gracefulShutdown( - deadline: NIODeadline = .distantFuture, - withTracking: Bool = true - ) -> EventLoopFuture { - if withTracking { - self.tracker.willRequestGracefulShutdown() - } - - let promise = self.group.next().makePromise(of: Void.self) - self.channel.closeGracefully(deadline: deadline, promise: promise) - - if withTracking { - promise.futureResult.whenComplete { _ in - self.tracker.didShutdown() - } - } - return promise.futureResult - } -} - -// MARK: - Test Helpers - -extension ClientQuiescingTests { - private func _testQuiescingWhenIdle(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - XCTAssertNoThrow(try self.gracefulShutdown().wait()) - } - - private func _testQuiescingWithNoOutstandingRPCs(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - XCTAssertNoThrow(try self.gracefulShutdown().wait()) - } - - private func _testQuiescingWithOneOutstandingRPC(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - - let collect = self.startRPC() - XCTAssertNoThrow(try collect.sendMessage(.empty).wait()) - - let shutdownFuture = self.gracefulShutdown() - XCTAssertNoThrow(try collect.sendEnd().wait()) - XCTAssertNoThrow(try shutdownFuture.wait()) - } - - private func _testQuiescingWithManyOutstandingRPCs(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - - // Start a bunch of RPCs. Send a message on each to ensure it's open. - let rpcs: [ClientStreamingCall] = (0 ..< 50).map { _ in - self.startRPC() - } - - for rpc in rpcs { - XCTAssertNoThrow(try rpc.sendMessage(.empty).wait()) - } - - // Start shutting down. - let shutdownFuture = self.gracefulShutdown() - - // All existing RPCs should continue to work. Send a message and end each. - for rpc in rpcs { - XCTAssertNoThrow(try rpc.sendMessage(.empty).wait()) - XCTAssertNoThrow(try rpc.sendEnd().wait()) - } - - // All RPCs should have finished so the shutdown future should complete. - XCTAssertNoThrow(try shutdownFuture.wait()) - } - - private func _testQuiescingTimesOutAndFailsExistingRPC(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - - // Tracking asserts that the RPC completes successfully: we don't expect that. - let rpc = self.startRPC(withTracking: false) - XCTAssertNoThrow(try rpc.sendMessage(.empty).wait()) - - let shutdownFuture = self.gracefulShutdown(deadline: .now() + .milliseconds(50)) - XCTAssertNoThrow(try shutdownFuture.wait()) - - // RPC should fail because the shutdown deadline passed. - XCTAssertThrowsError(try rpc.response.wait()) - } - - private func _testStartRPCAfterQuiescing(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - - // Start an RPC, ensure it's up and running. - let rpc = self.startRPC() - XCTAssertNoThrow(try rpc.sendMessage(.empty).wait()) - XCTAssertNoThrow(try rpc.initialMetadata.wait()) - - // Start the shutdown. - let shutdownFuture = self.gracefulShutdown() - - // Start another RPC. This should fail immediately. - self.tracker.assert(.shutdownRequested) - let untrackedRPC = self.startRPC(withTracking: false) - XCTAssertThrowsError(try untrackedRPC.response.wait()) - XCTAssertFalse(try untrackedRPC.status.wait().isOk) - - // The existing RPC should be fine. - XCTAssertNoThrow(try rpc.sendMessage(.empty).wait()) - // .. we shutdown should complete after sending end - XCTAssertNoThrow(try rpc.sendEnd().wait()) - XCTAssertNoThrow(try shutdownFuture.wait()) - } - - private func _testStartRPCAfterShutdownCompletes(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - XCTAssertNoThrow(try self.gracefulShutdown().wait()) - self.tracker.assert(.shutdown) - - // New RPCs should fail. - let untrackedRPC = self.startRPC(withTracking: false) - XCTAssertThrowsError(try untrackedRPC.response.wait()) - XCTAssertFalse(try untrackedRPC.status.wait().isOk) - } - - private func _testInitiateShutdownTwice(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - - let shutdown1 = self.gracefulShutdown() - // Tracking checks 'normal' paths, this path is allowed but not normal so don't track it. - let shutdown2 = self.gracefulShutdown(withTracking: false) - - XCTAssertNoThrow(try shutdown1.wait()) - XCTAssertNoThrow(try shutdown2.wait()) - } - - private func _testInitiateShutdownWithPastDeadline(channelKind kind: ChannelKind) { - self.setUpChannel(kind: kind) - self.assertConnectionEstablished() - - // Start a bunch of RPCs. Send a message on each to ensure it's open. - let rpcs: [ClientStreamingCall] = (0 ..< 5).map { _ in - self.startRPC(withTracking: false) - } - - for rpc in rpcs { - XCTAssertNoThrow(try rpc.sendMessage(.empty).wait()) - } - - XCTAssertNoThrow(try self.gracefulShutdown(deadline: .distantPast).wait()) - - for rpc in rpcs { - XCTAssertThrowsError(try rpc.response.wait()) - } - } -} - -// MARK: - Common Tests - -extension ClientQuiescingTests { - internal func testQuiescingWhenIdle_clientConnection() { - self._testQuiescingWhenIdle(channelKind: .single) - } - - internal func testQuiescingWithNoOutstandingRPCs_clientConnection() { - self._testQuiescingWithNoOutstandingRPCs(channelKind: .single) - } - - internal func testQuiescingWithOneOutstandingRPC_clientConnection() { - self._testQuiescingWithOneOutstandingRPC(channelKind: .single) - } - - internal func testQuiescingWithManyOutstandingRPCs_clientConnection() { - self._testQuiescingWithManyOutstandingRPCs(channelKind: .single) - } - - internal func testQuiescingTimesOutAndFailsExistingRPC_clientConnection() { - self._testQuiescingTimesOutAndFailsExistingRPC(channelKind: .single) - } - - internal func testStartRPCAfterQuiescing_clientConnection() { - self._testStartRPCAfterQuiescing(channelKind: .single) - } - - internal func testStartRPCAfterShutdownCompletes_clientConnection() { - self._testStartRPCAfterShutdownCompletes(channelKind: .single) - } - - internal func testInitiateShutdownTwice_clientConnection() { - self._testInitiateShutdownTwice(channelKind: .single) - } - - internal func testInitiateShutdownWithPastDeadline_clientConnection() { - self._testInitiateShutdownWithPastDeadline(channelKind: .single) - } - - internal func testQuiescingWhenIdle_channelPool() { - self._testQuiescingWhenIdle(channelKind: .pooled) - } - - internal func testQuiescingWithNoOutstandingRPCs_channelPool() { - self._testQuiescingWithNoOutstandingRPCs(channelKind: .pooled) - } - - internal func testQuiescingWithOneOutstandingRPC_channelPool() { - self._testQuiescingWithOneOutstandingRPC(channelKind: .pooled) - } - - internal func testQuiescingWithManyOutstandingRPCs_channelPool() { - self._testQuiescingWithManyOutstandingRPCs(channelKind: .pooled) - } - - internal func testQuiescingTimesOutAndFailsExistingRPC_channelPool() { - self._testQuiescingTimesOutAndFailsExistingRPC(channelKind: .pooled) - } - - internal func testStartRPCAfterQuiescing_channelPool() { - self._testStartRPCAfterQuiescing(channelKind: .pooled) - } - - internal func testStartRPCAfterShutdownCompletes_channelPool() { - self._testStartRPCAfterShutdownCompletes(channelKind: .pooled) - } - - internal func testInitiateShutdownTwice_channelPool() { - self._testInitiateShutdownTwice(channelKind: .pooled) - } - - internal func testInitiateShutdownWithPastDeadline_channelPool() { - self._testInitiateShutdownWithPastDeadline(channelKind: .pooled) - } -} - -// MARK: - Pool Specific Tests - -extension ClientQuiescingTests { - internal func testQuiescingTimesOutAndFailsWaiters_channelPool() throws { - self.setUpChannelPool(useSingleEventLoop: true) - self.assertConnectionEstablished() - - // We should have an established connection so we can load it up with 100 (i.e. http/2 max - // concurrent streams) RPCs. These are all going to fail so we disable tracking. - let rpcs: [ClientStreamingCall] = try (0 ..< 100) - .map { _ in - let rpc = self.startRPC(withTracking: false) - XCTAssertNoThrow(try rpc.sendMessage(.empty).wait()) - return rpc - } - - // Now we'll create a handful of RPCs which will be waiters. We expect these to fail too. - let waitingRPCs = (0 ..< 50).map { _ in - self.startRPC(withTracking: false) - } - - // The RPCs won't complete before the deadline as we don't half close them. - let closeFuture = self.gracefulShutdown(deadline: .now() + .milliseconds(50)) - XCTAssertNoThrow(try closeFuture.wait()) - - // All open and waiting RPCs will fail. - for rpc in rpcs { - XCTAssertThrowsError(try rpc.response.wait()) - } - - for rpc in waitingRPCs { - XCTAssertThrowsError(try rpc.response.wait()) - } - } - - internal func testQuiescingAllowsForStreamsCreatedBeforeInitiatingShutdown() { - self.setUpChannelPool(useSingleEventLoop: true) - self.assertConnectionEstablished() - - // Each of these RPCs will create a stream 'Channel' before we initiate the shutdown but the - // 'HTTP2Handler' may not know about each stream before we initiate shutdown. This test is to - // validate that we allow all of these calls to run normally. - let rpcsWhichShouldSucceed = (0 ..< 100).map { _ in - self.startRPC() - } - - // Initiate shutdown. The RPCs should be allowed to complete. - let closeFuture = self.gracefulShutdown() - - // These should all fail because they were started after initiating shutdown. - let rpcsWhichShouldFail = (0 ..< 100).map { _ in - self.startRPC(withTracking: false) - } - - for rpc in rpcsWhichShouldSucceed { - XCTAssertNoThrow(try rpc.sendEnd().wait()) - XCTAssertNoThrow(try rpc.response.wait()) - } - - for rpc in rpcsWhichShouldFail { - XCTAssertThrowsError(try rpc.sendEnd().wait()) - XCTAssertThrowsError(try rpc.response.wait()) - } - - XCTAssertNoThrow(try closeFuture.wait()) - } -} - -extension ClientQuiescingTests { - private final class RPCTracker { - private enum _State { - case active(Int) - case shutdownRequested(Int) - case shutdown - } - - internal enum State { - case active - case shutdownRequested - case shutdown - } - - private var state = _State.active(0) - private let lock = NIOLock() - - internal func assert(_ state: State, line: UInt = #line) { - self.lock.withLock { - switch (self.state, state) { - case (.active, .active), - (.shutdownRequested, .shutdownRequested), - (.shutdown, .shutdown): - () - default: - XCTFail("Expected \(state) but state is \(self.state)", line: line) - } - } - } - - internal func willStartRPC() { - self.lock.withLock { - switch self.state { - case let .active(outstandingRPCs): - self.state = .active(outstandingRPCs + 1) - - case let .shutdownRequested(outstandingRPCs): - // We still increment despite the shutdown having been requested since the RPC will - // fail immediately and we'll hit 'didFinishRPC'. - self.state = .shutdownRequested(outstandingRPCs + 1) - - case .shutdown: - XCTFail("Will start RPC when channel has been shutdown") - } - } - } - - internal func didFinishRPC() { - self.lock.withLock { - switch self.state { - case let .active(outstandingRPCs): - XCTAssertGreaterThan(outstandingRPCs, 0) - self.state = .active(outstandingRPCs - 1) - - case let .shutdownRequested(outstandingRPCs): - XCTAssertGreaterThan(outstandingRPCs, 0) - self.state = .shutdownRequested(outstandingRPCs - 1) - - case .shutdown: - XCTFail("Finished RPC after completing shutdown") - } - } - } - - internal func willRequestGracefulShutdown() { - self.lock.withLock { - switch self.state { - case let .active(outstandingRPCs): - self.state = .shutdownRequested(outstandingRPCs) - - case .shutdownRequested, .shutdown: - XCTFail("Shutdown has already been requested or completed") - } - } - } - - internal func didShutdown() { - switch self.state { - case let .active(outstandingRPCs): - XCTFail("Shutdown completed but not requested with \(outstandingRPCs) outstanding RPCs") - - case let .shutdownRequested(outstandingRPCs): - if outstandingRPCs != 0 { - XCTFail("Shutdown completed with \(outstandingRPCs) outstanding RPCs") - } else { - // Expected case. - self.state = .shutdown - } - - case .shutdown: - XCTFail("Already shutdown") - } - } - } -} diff --git a/Tests/GRPCTests/ClientTLSFailureTests.swift b/Tests/GRPCTests/ClientTLSFailureTests.swift deleted file mode 100644 index b14e8fe9c..000000000 --- a/Tests/GRPCTests/ClientTLSFailureTests.swift +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import EchoImplementation -import EchoModel -@testable import GRPC -import GRPCSampleData -import NIOCore -import NIOPosix -import NIOSSL -import XCTest - -class ClientTLSFailureTests: GRPCTestCase { - let defaultServerTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.server.certificate)], - privateKey: .privateKey(SamplePrivateKey.server) - ) - - let defaultClientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.client.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([SampleCertificate.ca.certificate]), - hostnameOverride: SampleCertificate.server.commonName - ) - - var defaultTestTimeout: TimeInterval = 1.0 - - var clientEventLoopGroup: EventLoopGroup! - var serverEventLoopGroup: EventLoopGroup! - var server: Server! - var port: Int! - - func makeClientConfiguration( - tls: GRPCTLSConfiguration - ) -> ClientConnection.Configuration { - var configuration = ClientConnection.Configuration.default( - target: .hostAndPort("localhost", self.port), - eventLoopGroup: self.clientEventLoopGroup - ) - - configuration.tlsConfiguration = tls - // No need to retry connecting. - configuration.connectionBackoff = nil - configuration.backgroundActivityLogger = self.clientLogger - - return configuration - } - - func makeClientConnectionExpectation() -> XCTestExpectation { - return self.expectation(description: "EventLoopFuture resolved") - } - - override func setUp() { - super.setUp() - - self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - self.server = try! Server.usingTLSBackedByNIOSSL( - on: self.serverEventLoopGroup, - certificateChain: [SampleCertificate.server.certificate], - privateKey: SamplePrivateKey.server - ).withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - self.port = self.server.channel.localAddress?.port - - self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - // Delay the client connection creation until the test. - } - - override func tearDown() { - self.port = nil - - XCTAssertNoThrow(try self.clientEventLoopGroup.syncShutdownGracefully()) - self.clientEventLoopGroup = nil - - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully()) - self.server = nil - self.serverEventLoopGroup = nil - - super.tearDown() - } - - func testClientConnectionFailsWhenServerIsUnknown() throws { - let errorExpectation = self.expectation(description: "error") - // 2 errors: one for the failed handshake, and another for failing the ready-channel promise - // (because the handshake failed). - errorExpectation.expectedFulfillmentCount = 2 - - var tls = self.defaultClientTLSConfiguration - tls.updateNIOTrustRoots(to: .certificates([])) - var configuration = self.makeClientConfiguration(tls: tls) - - let errorRecorder = ErrorRecordingDelegate(expectation: errorExpectation) - configuration.errorDelegate = errorRecorder - - let stateChangeDelegate = RecordingConnectivityDelegate() - stateChangeDelegate.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ] - ) - } - configuration.connectivityStateDelegate = stateChangeDelegate - - // Start an RPC to trigger creating a channel. - let echo = Echo_EchoNIOClient(channel: ClientConnection(configuration: configuration)) - _ = echo.get(.with { $0.text = "foo" }) - - self.wait(for: [errorExpectation], timeout: self.defaultTestTimeout) - stateChangeDelegate.waitForExpectedChanges(timeout: .seconds(5)) - - if let nioSSLError = errorRecorder.errors.first as? NIOSSLError, - case .handshakeFailed(.sslError) = nioSSLError - { - // Expected case. - } else { - XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)") - } - } - - func testClientConnectionFailsWhenHostnameIsNotValid() throws { - let errorExpectation = self.expectation(description: "error") - // 2 errors: one for the failed handshake, and another for failing the ready-channel promise - // (because the handshake failed). - errorExpectation.expectedFulfillmentCount = 2 - - var tls = self.defaultClientTLSConfiguration - tls.hostnameOverride = "not-the-server-hostname" - - var configuration = self.makeClientConfiguration(tls: tls) - let errorRecorder = ErrorRecordingDelegate(expectation: errorExpectation) - configuration.errorDelegate = errorRecorder - - let stateChangeDelegate = RecordingConnectivityDelegate() - stateChangeDelegate.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ] - ) - } - configuration.connectivityStateDelegate = stateChangeDelegate - - // Start an RPC to trigger creating a channel. - let echo = Echo_EchoNIOClient(channel: ClientConnection(configuration: configuration)) - _ = echo.get(.with { $0.text = "foo" }) - - self.wait(for: [errorExpectation], timeout: self.defaultTestTimeout) - stateChangeDelegate.waitForExpectedChanges(timeout: .seconds(5)) - - if let nioSSLError = errorRecorder.errors.first as? NIOSSLExtraError { - XCTAssertEqual(nioSSLError, .failedToValidateHostname) - // Expected case. - } else { - XCTFail("Expected NIOSSLExtraError.failedToValidateHostname") - } - } - - func testClientConnectionFailsWhenCertificateValidationDenied() throws { - let errorExpectation = self.expectation(description: "error") - // 2 errors: one for the failed handshake, and another for failing the ready-channel promise - // (because the handshake failed). - errorExpectation.expectedFulfillmentCount = 2 - - let tlsConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.client.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([SampleCertificate.ca.certificate]), - hostnameOverride: SampleCertificate.server.commonName, - customVerificationCallback: { _, promise in - // The certificate validation is forced to fail - promise.fail(NIOSSLError.unableToValidateCertificate) - } - ) - - var configuration = self.makeClientConfiguration(tls: tlsConfiguration) - let errorRecorder = ErrorRecordingDelegate(expectation: errorExpectation) - configuration.errorDelegate = errorRecorder - - let stateChangeDelegate = RecordingConnectivityDelegate() - stateChangeDelegate.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ] - ) - } - configuration.connectivityStateDelegate = stateChangeDelegate - - // Start an RPC to trigger creating a channel. - let echo = Echo_EchoNIOClient(channel: ClientConnection(configuration: configuration)) - _ = echo.get(.with { $0.text = "foo" }) - - self.wait(for: [errorExpectation], timeout: self.defaultTestTimeout) - stateChangeDelegate.waitForExpectedChanges(timeout: .seconds(5)) - - if let nioSSLError = errorRecorder.errors.first as? NIOSSLError, - case .handshakeFailed(.sslError) = nioSSLError - { - // Expected case. - } else { - XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)") - } - } -} - -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ClientTLSTests.swift b/Tests/GRPCTests/ClientTLSTests.swift deleted file mode 100644 index 4bcea05a7..000000000 --- a/Tests/GRPCTests/ClientTLSTests.swift +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import EchoImplementation -import EchoModel -import Foundation -import GRPC -import GRPCSampleData -import NIOCore -import NIOPosix -import NIOSSL -import XCTest - -class ClientTLSHostnameOverrideTests: GRPCTestCase { - var eventLoopGroup: EventLoopGroup! - var server: Server! - var connection: ClientConnection! - - override func setUp() { - super.setUp() - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.connection.close().wait()) - XCTAssertNoThrow(try self.eventLoopGroup.syncShutdownGracefully()) - super.tearDown() - } - - func doTestUnary() throws { - let client = Echo_EchoNIOClient( - channel: self.connection, - defaultCallOptions: self.callOptionsWithLogger - ) - let get = client.get(.with { $0.text = "foo" }) - - let response = try get.response.wait() - XCTAssertEqual(response.text, "Swift echo get: foo") - - let status = try get.status.wait() - XCTAssertEqual(status.code, .ok) - } - - func testTLSWithHostnameOverride() throws { - // Run a server presenting a certificate for example.com on localhost. - let cert = SampleCertificate.exampleServer.certificate - let key = SamplePrivateKey.exampleServer - - self.server = try Server.usingTLSBackedByNIOSSL( - on: self.eventLoopGroup, - certificateChain: [cert], - privateKey: key - ) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - guard let port = self.server.channel.localAddress?.port else { - XCTFail("could not get server port") - return - } - - self.connection = ClientConnection.usingTLSBackedByNIOSSL(on: self.eventLoopGroup) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withTLS(serverHostnameOverride: "example.com") - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: port) - - try self.doTestUnary() - } - - func testTLSWithoutHostnameOverride() throws { - // Run a server presenting a certificate for localhost on localhost. - let cert = SampleCertificate.server.certificate - let key = SamplePrivateKey.server - - self.server = try Server.usingTLSBackedByNIOSSL( - on: self.eventLoopGroup, - certificateChain: [cert], - privateKey: key - ) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - guard let port = self.server.channel.localAddress?.port else { - XCTFail("could not get server port") - return - } - - self.connection = ClientConnection.usingTLSBackedByNIOSSL(on: self.eventLoopGroup) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: port) - - try self.doTestUnary() - } - - func testTLSWithNoCertificateVerification() throws { - self.server = try Server.usingTLSBackedByNIOSSL( - on: self.eventLoopGroup, - certificateChain: [SampleCertificate.server.certificate], - privateKey: SamplePrivateKey.server - ) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - guard let port = self.server.channel.localAddress?.port else { - XCTFail("could not get server port") - return - } - - self.connection = ClientConnection.usingTLSBackedByNIOSSL(on: self.eventLoopGroup) - .withTLS(trustRoots: .certificates([])) - .withTLS(certificateVerification: .none) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: port) - - try self.doTestUnary() - } - - func testAuthorityUsesTLSHostnameOverride() throws { - // This test validates that when suppled with a server hostname override, the client uses it - // as the ":authority" pseudo-header. - - self.server = try Server.usingTLSBackedByNIOSSL( - on: self.eventLoopGroup, - certificateChain: [SampleCertificate.exampleServer.certificate], - privateKey: SamplePrivateKey.exampleServer - ) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withServiceProviders([AuthorityCheckingEcho()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - guard let port = self.server.channel.localAddress?.port else { - XCTFail("could not get server port") - return - } - - self.connection = ClientConnection.usingTLSBackedByNIOSSL(on: self.eventLoopGroup) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withTLS(serverHostnameOverride: "example.com") - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: port) - - try self.doTestUnary() - } -} - -private class AuthorityCheckingEcho: Echo_EchoProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - guard let authority = context.headers.first(name: ":authority") else { - let status = GRPCStatus( - code: .failedPrecondition, - message: "Missing ':authority' pseudo header" - ) - return context.eventLoop.makeFailedFuture(status) - } - - XCTAssertEqual(authority, SampleCertificate.exampleServer.commonName) - XCTAssertNotEqual(authority, "localhost") - - return context.eventLoop.makeSucceededFuture( - .with { - $0.text = "Swift echo get: \(request.text)" - } - ) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - preconditionFailure("Not implemented") - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - preconditionFailure("Not implemented") - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - preconditionFailure("Not implemented") - } -} - -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ClientTimeoutTests.swift b/Tests/GRPCTests/ClientTimeoutTests.swift deleted file mode 100644 index 4dc9abac7..000000000 --- a/Tests/GRPCTests/ClientTimeoutTests.swift +++ /dev/null @@ -1,261 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import Logging -import NIOCore -import NIOEmbedded -import NIOHTTP2 -import SwiftProtobuf -import XCTest - -@testable import GRPC - -class ClientTimeoutTests: GRPCTestCase { - var channel: EmbeddedChannel! - var client: Echo_EchoNIOClient! - - let timeout = TimeAmount.milliseconds(100) - var callOptions: CallOptions { - // We use a deadline here because internally we convert timeouts into deadlines by diffing - // with `DispatchTime.now()`. We therefore need the deadline to be known in advance. Note we - // use zero because `EmbeddedEventLoop`s time starts at zero. - var options = self.callOptionsWithLogger - options.timeLimit = .deadline(.uptimeNanoseconds(0) + self.timeout) - return options - } - - // Note: this is not related to the call timeout since we're using an EmbeddedChannel. We require - // this in case the timeout doesn't work. - let testTimeout: TimeInterval = 0.1 - - override func setUp() { - super.setUp() - - let connection = EmbeddedGRPCChannel(logger: self.clientLogger) - XCTAssertNoThrow( - try connection.embeddedChannel - .connect(to: SocketAddress(unixDomainSocketPath: "/foo")) - ) - let client = Echo_EchoNIOClient(channel: connection, defaultCallOptions: self.callOptions) - - self.channel = connection.embeddedChannel - self.client = client - } - - override func tearDown() { - XCTAssertNoThrow(try self.channel.finish()) - super.tearDown() - } - - func assertRPCTimedOut( - _ response: EventLoopFuture, - expectation: XCTestExpectation - ) { - response.whenComplete { result in - switch result { - case let .success(response): - XCTFail("unexpected response: \(response)") - case let .failure(error): - XCTAssertTrue(error is GRPCError.RPCTimedOut) - } - expectation.fulfill() - } - } - - func assertDeadlineExceeded( - _ status: EventLoopFuture, - expectation: XCTestExpectation - ) { - status.whenComplete { result in - switch result { - case let .success(status): - XCTAssertEqual(status.code, .deadlineExceeded) - case let .failure(error): - XCTFail("unexpected error: \(error)") - } - expectation.fulfill() - } - } - - func testUnaryTimeoutAfterSending() throws { - let statusExpectation = self.expectation(description: "status fulfilled") - - let call = self.client.get(Echo_EchoRequest(text: "foo")) - self.channel.embeddedEventLoop.advanceTime(by: self.timeout) - - self.assertDeadlineExceeded(call.status, expectation: statusExpectation) - self.wait(for: [statusExpectation], timeout: self.testTimeout) - } - - func testServerStreamingTimeoutAfterSending() throws { - let statusExpectation = self.expectation(description: "status fulfilled") - - let call = self.client.expand(Echo_EchoRequest(text: "foo bar baz")) { _ in } - self.channel.embeddedEventLoop.advanceTime(by: self.timeout) - - self.assertDeadlineExceeded(call.status, expectation: statusExpectation) - self.wait(for: [statusExpectation], timeout: self.testTimeout) - } - - func testClientStreamingTimeoutBeforeSending() throws { - let responseExpectation = self.expectation(description: "response fulfilled") - let statusExpectation = self.expectation(description: "status fulfilled") - - let call = self.client.collect() - self.channel.embeddedEventLoop.advanceTime(by: self.timeout) - - self.assertRPCTimedOut(call.response, expectation: responseExpectation) - self.assertDeadlineExceeded(call.status, expectation: statusExpectation) - self.wait(for: [responseExpectation, statusExpectation], timeout: self.testTimeout) - } - - func testClientStreamingTimeoutAfterSending() throws { - let responseExpectation = self.expectation(description: "response fulfilled") - let statusExpectation = self.expectation(description: "status fulfilled") - - let call = self.client.collect() - - self.assertRPCTimedOut(call.response, expectation: responseExpectation) - self.assertDeadlineExceeded(call.status, expectation: statusExpectation) - - call.sendMessage(Echo_EchoRequest(text: "foo"), promise: nil) - call.sendEnd(promise: nil) - self.channel.embeddedEventLoop.advanceTime(by: self.timeout) - - self.wait(for: [responseExpectation, statusExpectation], timeout: 1.0) - } - - func testBidirectionalStreamingTimeoutBeforeSending() { - let statusExpectation = self.expectation(description: "status fulfilled") - - let call = self.client.update { _ in } - - self.channel.embeddedEventLoop.advanceTime(by: self.timeout) - - self.assertDeadlineExceeded(call.status, expectation: statusExpectation) - self.wait(for: [statusExpectation], timeout: self.testTimeout) - } - - func testBidirectionalStreamingTimeoutAfterSending() { - let statusExpectation = self.expectation(description: "status fulfilled") - - let call = self.client.update { _ in } - - self.assertDeadlineExceeded(call.status, expectation: statusExpectation) - - call.sendMessage(Echo_EchoRequest(text: "foo"), promise: nil) - call.sendEnd(promise: nil) - self.channel.embeddedEventLoop.advanceTime(by: self.timeout) - - self.wait(for: [statusExpectation], timeout: self.testTimeout) - } -} - -// Unchecked as it uses an 'EmbeddedChannel'. -extension EmbeddedGRPCChannel: @unchecked Sendable {} - -private final class EmbeddedGRPCChannel: GRPCChannel { - let embeddedChannel: EmbeddedChannel - let multiplexer: EventLoopFuture - - let logger: Logger - let scheme: String - let authority: String - let errorDelegate: ClientErrorDelegate? - - func close() -> EventLoopFuture { - return self.embeddedChannel.close() - } - - var eventLoop: EventLoop { - return self.embeddedChannel.eventLoop - } - - init( - logger: Logger = Logger(label: "io.grpc", factory: { _ in SwiftLogNoOpLogHandler() }), - errorDelegate: ClientErrorDelegate? = nil - ) { - let embeddedChannel = EmbeddedChannel() - self.embeddedChannel = embeddedChannel - self.logger = logger - self.multiplexer = embeddedChannel.configureGRPCClient( - errorDelegate: errorDelegate, - logger: logger - ).flatMap { - embeddedChannel.pipeline.handler(type: HTTP2StreamMultiplexer.self) - } - self.scheme = "http" - self.authority = "localhost" - self.errorDelegate = errorDelegate - } - - internal func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - return Call( - path: path, - type: type, - eventLoop: self.eventLoop, - options: callOptions, - interceptors: interceptors, - transportFactory: .http2( - channel: self.makeStreamChannel(), - authority: self.authority, - scheme: self.scheme, - // This is internal and only for testing, so max is fine here. - maximumReceiveMessageLength: .max, - errorDelegate: self.errorDelegate - ) - ) - } - - internal func makeCall( - path: String, - type: GRPCCallType, - callOptions: CallOptions, - interceptors: [ClientInterceptor] - ) -> Call { - return Call( - path: path, - type: type, - eventLoop: self.eventLoop, - options: callOptions, - interceptors: interceptors, - transportFactory: .http2( - channel: self.makeStreamChannel(), - authority: self.authority, - scheme: self.scheme, - // This is internal and only for testing, so max is fine here. - maximumReceiveMessageLength: .max, - errorDelegate: self.errorDelegate - ) - ) - } - - private func makeStreamChannel() -> EventLoopFuture { - let promise = self.eventLoop.makePromise(of: Channel.self) - self.multiplexer.whenSuccess { - $0.createStreamChannel(promise: promise) { - $0.eventLoop.makeSucceededVoidFuture() - } - } - return promise.futureResult - } -} diff --git a/Tests/GRPCTests/ClientTransportTests.swift b/Tests/GRPCTests/ClientTransportTests.swift deleted file mode 100644 index 500d799dd..000000000 --- a/Tests/GRPCTests/ClientTransportTests.swift +++ /dev/null @@ -1,384 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import XCTest - -@testable import GRPC - -class ClientTransportTests: GRPCTestCase { - override func setUp() { - super.setUp() - self.channel = EmbeddedChannel() - } - - // MARK: - Setup Helpers - - private func makeDetails(type: GRPCCallType = .unary) -> CallDetails { - return CallDetails( - type: type, - path: "/echo.Echo/Get", - authority: "localhost", - scheme: "https", - options: .init(logger: self.logger) - ) - } - - private var channel: EmbeddedChannel! - private var transport: ClientTransport! - - private var eventLoop: EventLoop { - return self.channel.eventLoop - } - - private func setUpTransport( - details: CallDetails? = nil, - interceptors: [ClientInterceptor] = [], - onError: @escaping (Error) -> Void = { _ in }, - onResponsePart: @escaping (GRPCClientResponsePart) -> Void = { _ in } - ) { - self.transport = .init( - details: details ?? self.makeDetails(), - eventLoop: self.eventLoop, - interceptors: interceptors, - serializer: AnySerializer(wrapping: StringSerializer()), - deserializer: AnyDeserializer(wrapping: StringDeserializer()), - errorDelegate: nil, - onStart: {}, - onError: onError, - onResponsePart: onResponsePart - ) - } - - private func configureTransport(additionalHandlers handlers: [ChannelHandler] = []) { - self.transport.configure { - var handlers = handlers - handlers.append( - GRPCClientReverseCodecHandler( - serializer: StringSerializer(), - deserializer: StringDeserializer() - ) - ) - handlers.append($0) - return self.channel.pipeline.addHandlers(handlers) - } - } - - private func configureTransport(_ body: @escaping (ChannelHandler) -> EventLoopFuture) { - self.transport.configure(body) - } - - private func connect(file: StaticString = #filePath, line: UInt = #line) throws { - let address = try assertNoThrow(SocketAddress(unixDomainSocketPath: "/whatever")) - assertThat( - try self.channel.connect(to: address).wait(), - .doesNotThrow(), - file: file, - line: line - ) - } - - private func sendRequest( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise? = nil - ) { - self.transport.send(part, promise: promise) - } - - private func cancel(promise: EventLoopPromise? = nil) { - self.transport.cancel(promise: promise) - } - - private func sendResponse( - _ part: _GRPCClientResponsePart, - file: StaticString = #filePath, - line: UInt = #line - ) throws { - assertThat(try self.channel.writeInbound(part), .doesNotThrow(), file: file, line: line) - } -} - -// MARK: - Tests - -extension ClientTransportTests { - func testUnaryFlow() throws { - let recorder = WriteRecorder<_GRPCClientRequestPart>() - let recorderInterceptor = RecordingInterceptor() - - self.setUpTransport(interceptors: [recorderInterceptor]) - - // Buffer up some parts. - self.sendRequest(.metadata([:])) - self.sendRequest(.message("0", .init(compress: false, flush: false))) - - // Configure the transport and connect. This will unbuffer the parts. - self.configureTransport(additionalHandlers: [recorder]) - try self.connect() - - // Send the end, this shouldn't require buffering. - self.sendRequest(.end) - - // We should have recorded 3 parts in the 'Channel' now. - assertThat(recorder.writes, .hasCount(3)) - - // Write some responses. - try self.sendResponse(.initialMetadata([:])) - try self.sendResponse(.message(.init("1", compressed: false))) - try self.sendResponse(.trailingMetadata([:])) - try self.sendResponse(.status(.ok)) - - // The recording interceptor should now have three parts. - assertThat(recorderInterceptor.responseParts, .hasCount(3)) - } - - func testCancelWhenIdle() throws { - // Set up the transport, configure it and connect. - self.setUpTransport(onError: { error in - assertThat(error, .is(.instanceOf(GRPCError.RPCCancelledByClient.self))) - }) - - // Cancellation should succeed. - let promise = self.eventLoop.makePromise(of: Void.self) - self.cancel(promise: promise) - assertThat(try promise.futureResult.wait(), .doesNotThrow()) - } - - func testCancelWhenAwaitingTransport() throws { - // Set up the transport, configure it and connect. - self.setUpTransport(onError: { error in - assertThat(error, .is(.instanceOf(GRPCError.RPCCancelledByClient.self))) - }) - - // Start configuring the transport. - let transportActivatedPromise = self.eventLoop.makePromise(of: Void.self) - // Let's not leak this. - defer { - transportActivatedPromise.succeed(()) - } - self.configureTransport { handler in - self.channel.pipeline.addHandler(handler).flatMap { - transportActivatedPromise.futureResult - } - } - - // Write a request. - let p1 = self.eventLoop.makePromise(of: Void.self) - self.sendRequest(.metadata([:]), promise: p1) - - let p2 = self.eventLoop.makePromise(of: Void.self) - self.cancel(promise: p2) - - // Cancellation should succeed, and fail the write as a result. - assertThat(try p2.futureResult.wait(), .doesNotThrow()) - assertThat( - try p1.futureResult.wait(), - .throws(.instanceOf(GRPCError.RPCCancelledByClient.self)) - ) - } - - func testCancelWhenActivating() throws { - // Set up the transport, configure it and connect. - // We use bidirectional streaming here so that we also flush after writing the metadata. - self.setUpTransport( - details: self.makeDetails(type: .bidirectionalStreaming), - onError: { error in - assertThat(error, .is(.instanceOf(GRPCError.RPCCancelledByClient.self))) - } - ) - - // Write a request. This will buffer. - let writePromise1 = self.eventLoop.makePromise(of: Void.self) - self.sendRequest(.metadata([:]), promise: writePromise1) - - // Chain a cancel from the first write promise. - let cancelPromise = self.eventLoop.makePromise(of: Void.self) - writePromise1.futureResult.whenSuccess { - self.cancel(promise: cancelPromise) - } - - // Enqueue a second write. - let writePromise2 = self.eventLoop.makePromise(of: Void.self) - self.sendRequest(.message("foo", .init(compress: false, flush: false)), promise: writePromise2) - - // Now we can configure and connect to trigger the unbuffering. - // We don't actually want to record writes, by the recorder will fulfill promises as we catch - // them; and we need that. - self.configureTransport(additionalHandlers: [WriteRecorder<_GRPCClientRequestPart>()]) - try self.connect() - - // The first write should succeed. - assertThat(try writePromise1.futureResult.wait(), .doesNotThrow()) - // As should the cancellation. - assertThat(try cancelPromise.futureResult.wait(), .doesNotThrow()) - // The second write should fail: the cancellation happened first. - assertThat( - try writePromise2.futureResult.wait(), - .throws(.instanceOf(GRPCError.RPCCancelledByClient.self)) - ) - } - - func testCancelWhenActive() throws { - // Set up the transport, configure it and connect. We'll record request parts in the `Channel`. - let recorder = WriteRecorder<_GRPCClientRequestPart>() - self.setUpTransport() - self.configureTransport(additionalHandlers: [recorder]) - try self.connect() - - // We should have an active transport now. - self.sendRequest(.metadata([:])) - self.sendRequest(.message("0", .init(compress: false, flush: false))) - - // We should have picked these parts up in the recorder. - assertThat(recorder.writes, .hasCount(2)) - - // Let's cancel now. - let promise = self.eventLoop.makePromise(of: Void.self) - self.cancel(promise: promise) - - // Cancellation should succeed. - assertThat(try promise.futureResult.wait(), .doesNotThrow()) - } - - func testCancelWhenClosing() throws { - self.setUpTransport() - - // Hold the configuration until we succeed the promise. - let configuredPromise = self.eventLoop.makePromise(of: Void.self) - self.configureTransport { handler in - self.channel.pipeline.addHandler(handler).flatMap { - configuredPromise.futureResult - } - } - } - - func testCancelWhenClosed() throws { - // Setup and close immediately. - self.setUpTransport() - self.configureTransport() - try self.connect() - assertThat(try self.channel.close().wait(), .doesNotThrow()) - - // Let's cancel now. - let promise = self.eventLoop.makePromise(of: Void.self) - self.cancel(promise: promise) - - // Cancellation should fail, we're already closed. - assertThat( - try promise.futureResult.wait(), - .throws(.instanceOf(GRPCError.AlreadyComplete.self)) - ) - } - - func testErrorWhenActive() throws { - // Setup the transport, we only expect an error back. - self.setUpTransport(onError: { error in - assertThat(error, .is(.instanceOf(DummyError.self))) - }) - - // Configure and activate. - self.configureTransport() - try self.connect() - - // Send a request. - let p1 = self.eventLoop.makePromise(of: Void.self) - self.sendRequest(.metadata([:]), promise: p1) - // The transport is for a unary call, so we need to send '.end' to emit a flush and for the - // promise to be completed. - self.sendRequest(.end, promise: nil) - - assertThat(try p1.futureResult.wait(), .doesNotThrow()) - - // Fire an error back. (We'll see an error on the response handler.) - self.channel.pipeline.fireErrorCaught(DummyError()) - - // Writes should now fail, we're closed. - let p2 = self.eventLoop.makePromise(of: Void.self) - self.sendRequest(.end, promise: p2) - assertThat(try p2.futureResult.wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self))) - } - - func testConfigurationFails() throws { - self.setUpTransport() - - let p1 = self.eventLoop.makePromise(of: Void.self) - self.sendRequest(.metadata([:]), promise: p1) - - let p2 = self.eventLoop.makePromise(of: Void.self) - self.sendRequest(.message("0", .init(compress: false, flush: false)), promise: p2) - - // Fail to configure the transport. Our promises should fail. - self.configureTransport { _ in - self.eventLoop.makeFailedFuture(DummyError()) - } - - // The promises should fail. - assertThat(try p1.futureResult.wait(), .throws()) - assertThat(try p2.futureResult.wait(), .throws()) - - // Cancellation should also fail because we're already closed. - let p3 = self.eventLoop.makePromise(of: Void.self) - self.transport.cancel(promise: p3) - assertThat(try p3.futureResult.wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self))) - } -} - -// MARK: - Helper Objects - -class WriteRecorder: ChannelOutboundHandler { - typealias OutboundIn = Write - var writes: [Write] = [] - - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - self.writes.append(self.unwrapOutboundIn(data)) - promise?.succeed(()) - } -} - -private struct DummyError: Error {} - -internal struct StringSerializer: MessageSerializer { - typealias Input = String - - func serialize(_ input: String, allocator: ByteBufferAllocator) throws -> ByteBuffer { - return allocator.buffer(string: input) - } -} - -internal struct StringDeserializer: MessageDeserializer { - typealias Output = String - - func deserialize(byteBuffer: ByteBuffer) throws -> String { - var buffer = byteBuffer - return buffer.readString(length: buffer.readableBytes)! - } -} - -internal struct ThrowingStringSerializer: MessageSerializer { - typealias Input = String - - func serialize(_ input: String, allocator: ByteBufferAllocator) throws -> ByteBuffer { - throw DummyError() - } -} - -internal struct ThrowingStringDeserializer: MessageDeserializer { - typealias Output = String - - func deserialize(byteBuffer: ByteBuffer) throws -> String { - throw DummyError() - } -} diff --git a/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift b/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift deleted file mode 100644 index f61963e9f..000000000 --- a/Tests/GRPCTests/CoalescingLengthPrefixedMessageWriterTests.swift +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2022, 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 NIOCore -import NIOEmbedded -import XCTest - -@testable import GRPC - -internal final class CoalescingLengthPrefixedMessageWriterTests: GRPCTestCase { - private let loop = EmbeddedEventLoop() - - private func makeWriter( - compression: CompressionAlgorithm? = .none - ) -> CoalescingLengthPrefixedMessageWriter { - return .init(compression: compression, allocator: .init()) - } - - private func testSingleSmallWrite(withPromise: Bool) throws { - var writer = self.makeWriter() - - let promise = withPromise ? self.loop.makePromise(of: Void.self) : nil - writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: promise) - - let (result, maybePromise) = try XCTUnwrap(writer.next()) - try result.assertValue { buffer in - var buffer = buffer - let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compressed) - XCTAssertEqual(length, UInt32(ByteBuffer.smallEnoughToCoalesce.readableBytes)) - XCTAssertEqual(buffer.readSlice(length: Int(length)), .smallEnoughToCoalesce) - XCTAssertEqual(buffer.readableBytes, 0) - } - - // No more bufers. - XCTAssertNil(writer.next()) - - if withPromise { - XCTAssertNotNil(maybePromise) - } else { - XCTAssertNil(maybePromise) - } - - // Don't leak the promise. - maybePromise?.succeed(()) - } - - private func testMultipleSmallWrites(withPromise: Bool) throws { - var writer = self.makeWriter() - let messages = 100 - - for _ in 0 ..< messages { - let promise = withPromise ? self.loop.makePromise(of: Void.self) : nil - writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: promise) - } - - let (result, maybePromise) = try XCTUnwrap(writer.next()) - try result.assertValue { buffer in - var buffer = buffer - - // Read all the messages. - for _ in 0 ..< messages { - let (compressed, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compressed) - XCTAssertEqual(length, UInt32(ByteBuffer.smallEnoughToCoalesce.readableBytes)) - XCTAssertEqual(buffer.readSlice(length: Int(length)), .smallEnoughToCoalesce) - } - - XCTAssertEqual(buffer.readableBytes, 0) - } - - // No more bufers. - XCTAssertNil(writer.next()) - - if withPromise { - XCTAssertNotNil(maybePromise) - } else { - XCTAssertNil(maybePromise) - } - - // Don't leak the promise. - maybePromise?.succeed(()) - } - - func testSingleSmallWriteWithPromise() throws { - try self.testSingleSmallWrite(withPromise: true) - } - - func testSingleSmallWriteWithoutPromise() throws { - try self.testSingleSmallWrite(withPromise: false) - } - - func testMultipleSmallWriteWithPromise() throws { - try self.testMultipleSmallWrites(withPromise: true) - } - - func testMultipleSmallWriteWithoutPromise() throws { - try self.testMultipleSmallWrites(withPromise: false) - } - - func testSingleLargeMessage() throws { - var writer = self.makeWriter() - writer.append(buffer: .tooBigToCoalesce, compress: false, promise: nil) - - let (result1, promise1) = try XCTUnwrap(writer.next()) - XCTAssertNil(promise1) - try result1.assertValue { buffer in - var buffer = buffer - let (compress, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compress) - XCTAssertEqual(Int(length), ByteBuffer.tooBigToCoalesce.readableBytes) - XCTAssertEqual(buffer.readableBytes, 0) - } - - let (result2, promise2) = try XCTUnwrap(writer.next()) - XCTAssertNil(promise2) - result2.assertValue { buffer in - XCTAssertEqual(buffer, .tooBigToCoalesce) - } - - XCTAssertNil(writer.next()) - } - - func testMessagesBeforeLargeAreCoalesced() throws { - var writer = self.makeWriter() - // First two should be coalesced. The third should be split as two buffers. - writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: nil) - writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: nil) - writer.append(buffer: .tooBigToCoalesce, compress: false, promise: nil) - - let (result1, _) = try XCTUnwrap(writer.next()) - try result1.assertValue { buffer in - var buffer = buffer - for _ in 0 ..< 2 { - let (compress, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compress) - XCTAssertEqual(Int(length), ByteBuffer.smallEnoughToCoalesce.readableBytes) - XCTAssertEqual(buffer.readSlice(length: Int(length)), .smallEnoughToCoalesce) - } - XCTAssertEqual(buffer.readableBytes, 0) - } - - let (result2, _) = try XCTUnwrap(writer.next()) - try result2.assertValue { buffer in - var buffer = buffer - let (compress, length) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compress) - XCTAssertEqual(Int(length), ByteBuffer.tooBigToCoalesce.readableBytes) - XCTAssertEqual(buffer.readableBytes, 0) - } - - let (result3, _) = try XCTUnwrap(writer.next()) - result3.assertValue { buffer in - XCTAssertEqual(buffer, .tooBigToCoalesce) - } - - XCTAssertNil(writer.next()) - } - - func testCompressedMessagesAreAlwaysCoalesced() throws { - var writer = self.makeWriter(compression: .gzip) - writer.append(buffer: .smallEnoughToCoalesce, compress: false, promise: nil) - writer.append(buffer: .tooBigToCoalesce, compress: true, promise: nil) - - let (result, _) = try XCTUnwrap(writer.next()) - try result.assertValue { buffer in - var buffer = buffer - - let (compress1, length1) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertFalse(compress1) - XCTAssertEqual(Int(length1), ByteBuffer.smallEnoughToCoalesce.readableBytes) - XCTAssertEqual(buffer.readSlice(length: Int(length1)), .smallEnoughToCoalesce) - - let (compress2, length2) = try XCTUnwrap(buffer.readMessageHeader()) - XCTAssertTrue(compress2) - // Can't assert the length or the content, only that the length must be equal - // to the number of remaining bytes. - XCTAssertEqual(Int(length2), buffer.readableBytes) - } - - XCTAssertNil(writer.next()) - } -} - -extension Result { - func assertValue(_ body: (Success) throws -> Void) rethrows { - switch self { - case let .success(success): - try body(success) - case let .failure(error): - XCTFail("Unexpected failure: \(error)") - } - } -} - -extension ByteBuffer { - fileprivate static let smallEnoughToCoalesce = Self(repeating: 42, count: 128) - fileprivate static let tooBigToCoalesce = Self( - repeating: 42, - count: CoalescingLengthPrefixedMessageWriter.singleBufferSizeLimit + 1 - ) - - mutating func readMessageHeader() -> (Bool, UInt32)? { - if let (compressed, length) = self.readMultipleIntegers(as: (UInt8, UInt32).self) { - return (compressed != 0, length) - } else { - return nil - } - } -} diff --git a/Tests/GRPCTests/Codegen/Normalization/NormalizationProvider.swift b/Tests/GRPCTests/Codegen/Normalization/NormalizationProvider.swift deleted file mode 100644 index 7f81cb4a6..000000000 --- a/Tests/GRPCTests/Codegen/Normalization/NormalizationProvider.swift +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 GRPC -import NIOCore -import SwiftProtobuf - -final class NormalizationProvider: Normalization_NormalizationProvider { - let interceptors: Normalization_NormalizationServerInterceptorFactoryProtocol? = nil - - // MARK: Unary - - private func unary( - context: StatusOnlyCallContext, - function: String = #function - ) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(.with { $0.functionName = function }) - } - - func Unary( - request: Google_Protobuf_Empty, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return self.unary(context: context) - } - - func unary( - request: Google_Protobuf_Empty, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return self.unary(context: context) - } - - // MARK: Server Streaming - - private func serverStreaming( - context: StreamingResponseCallContext, - function: String = #function - ) -> EventLoopFuture { - context.sendResponse(.with { $0.functionName = function }, promise: nil) - return context.eventLoop.makeSucceededFuture(.ok) - } - - func ServerStreaming( - request: Google_Protobuf_Empty, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return self.serverStreaming(context: context) - } - - func serverStreaming( - request: Google_Protobuf_Empty, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return self.serverStreaming(context: context) - } - - // MARK: Client Streaming - - private func _clientStreaming( - context: UnaryResponseCallContext, - function: String = #function - ) -> EventLoopFuture<(StreamEvent) -> Void> { - func handle(_ event: StreamEvent) { - switch event { - case .message: - () - case .end: - context.responsePromise.succeed(.with { $0.functionName = function }) - } - } - - return context.eventLoop.makeSucceededFuture(handle(_:)) - } - - func ClientStreaming( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return self._clientStreaming(context: context) - } - - func clientStreaming( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return self._clientStreaming(context: context) - } - - // MARK: Bidirectional Streaming - - private func _bidirectionalStreaming( - context: StreamingResponseCallContext, - function: String = #function - ) -> EventLoopFuture<(StreamEvent) -> Void> { - func handle(_ event: StreamEvent) { - switch event { - case .message: - () - case .end: - context.sendResponse(.with { $0.functionName = function }, promise: nil) - context.statusPromise.succeed(.ok) - } - } - - return context.eventLoop.makeSucceededFuture(handle(_:)) - } - - func BidirectionalStreaming( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return self._bidirectionalStreaming(context: context) - } - - func bidirectionalStreaming( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return self._bidirectionalStreaming(context: context) - } -} diff --git a/Tests/GRPCTests/Codegen/Normalization/NormalizationTests.swift b/Tests/GRPCTests/Codegen/Normalization/NormalizationTests.swift deleted file mode 100644 index 4692f6830..000000000 --- a/Tests/GRPCTests/Codegen/Normalization/NormalizationTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 GRPC -import NIOCore -import NIOPosix -import XCTest - -/// These tests validate that: -/// - we can compile generated code for functions with same (case-insensitive) name (providing they -/// are generated with 'KeepMethodCasing=true') -/// - the right client function calls the server function with the expected casing. -final class NormalizationTests: GRPCTestCase { - var group: EventLoopGroup! - var server: Server! - var channel: ClientConnection! - - override func setUp() { - super.setUp() - - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - self.server = try! Server.insecure(group: self.group) - .withLogger(self.serverLogger) - .withServiceProviders([NormalizationProvider()]) - .bind(host: "localhost", port: 0) - .wait() - - self.channel = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: self.server.channel.localAddress!.port!) - } - - override func tearDown() { - XCTAssertNoThrow(try self.channel.close().wait()) - XCTAssertNoThrow(try self.server.initiateGracefulShutdown().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - func testUnary() throws { - let client = Normalization_NormalizationNIOClient(channel: channel) - - let unary1 = client.unary(.init()) - let response1 = try unary1.response.wait() - XCTAssert(response1.functionName.starts(with: "unary")) - - let unary2 = client.Unary(.init()) - let response2 = try unary2.response.wait() - XCTAssert(response2.functionName.starts(with: "Unary")) - } - - func testClientStreaming() throws { - let client = Normalization_NormalizationNIOClient(channel: channel) - - let clientStreaming1 = client.clientStreaming() - clientStreaming1.sendEnd(promise: nil) - let response1 = try clientStreaming1.response.wait() - XCTAssert(response1.functionName.starts(with: "clientStreaming")) - - let clientStreaming2 = client.ClientStreaming() - clientStreaming2.sendEnd(promise: nil) - let response2 = try clientStreaming2.response.wait() - XCTAssert(response2.functionName.starts(with: "ClientStreaming")) - } - - func testServerStreaming() throws { - let client = Normalization_NormalizationNIOClient(channel: channel) - - let serverStreaming1 = client.serverStreaming(.init()) { - XCTAssert($0.functionName.starts(with: "serverStreaming")) - } - XCTAssertEqual(try serverStreaming1.status.wait(), .ok) - - let serverStreaming2 = client.ServerStreaming(.init()) { - XCTAssert($0.functionName.starts(with: "ServerStreaming")) - } - XCTAssertEqual(try serverStreaming2.status.wait(), .ok) - } - - func testBidirectionalStreaming() throws { - let client = Normalization_NormalizationNIOClient(channel: channel) - - let bidirectionalStreaming1 = client.bidirectionalStreaming { - XCTAssert($0.functionName.starts(with: "bidirectionalStreaming")) - } - bidirectionalStreaming1.sendEnd(promise: nil) - XCTAssertEqual(try bidirectionalStreaming1.status.wait(), .ok) - - let bidirectionalStreaming2 = client.BidirectionalStreaming { - XCTAssert($0.functionName.starts(with: "BidirectionalStreaming")) - } - bidirectionalStreaming2.sendEnd(promise: nil) - XCTAssertEqual(try bidirectionalStreaming2.status.wait(), .ok) - } -} diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift deleted file mode 100644 index 48cd11100..000000000 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.grpc.swift +++ /dev/null @@ -1,1037 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: normalization.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Usage: instantiate `Normalization_NormalizationClient`, then call methods of this protocol to make API calls. -internal protocol Normalization_NormalizationClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? { get } - - func Unary( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? - ) -> UnaryCall - - func unary( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? - ) -> UnaryCall - - func ServerStreaming( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions?, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> ServerStreamingCall - - func serverStreaming( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions?, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> ServerStreamingCall - - func ClientStreaming( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func clientStreaming( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func BidirectionalStreaming( - callOptions: CallOptions?, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> BidirectionalStreamingCall - - func bidirectionalStreaming( - callOptions: CallOptions?, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> BidirectionalStreamingCall -} - -extension Normalization_NormalizationClientProtocol { - internal var serviceName: String { - return "normalization.Normalization" - } - - /// Unary call to Unary - /// - /// - Parameters: - /// - request: Request to send to Unary. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func Unary( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Normalization_NormalizationClientMetadata.Methods.Unary.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryInterceptors() ?? [] - ) - } - - /// Unary call to unary - /// - /// - Parameters: - /// - request: Request to send to unary. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func unary( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: Normalization_NormalizationClientMetadata.Methods.unary.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeunaryInterceptors() ?? [] - ) - } - - /// Server streaming call to ServerStreaming - /// - /// - Parameters: - /// - request: Request to send to ServerStreaming. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - internal func ServerStreaming( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.ServerStreaming.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerStreamingInterceptors() ?? [], - handler: handler - ) - } - - /// Server streaming call to serverStreaming - /// - /// - Parameters: - /// - request: Request to send to serverStreaming. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - internal func serverStreaming( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.serverStreaming.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeserverStreamingInterceptors() ?? [], - handler: handler - ) - } - - /// Client streaming call to ClientStreaming - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. - internal func ClientStreaming( - callOptions: CallOptions? = nil - ) -> ClientStreamingCall { - return self.makeClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.ClientStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeClientStreamingInterceptors() ?? [] - ) - } - - /// Client streaming call to clientStreaming - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. - internal func clientStreaming( - callOptions: CallOptions? = nil - ) -> ClientStreamingCall { - return self.makeClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.clientStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeclientStreamingInterceptors() ?? [] - ) - } - - /// Bidirectional streaming call to BidirectionalStreaming - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - internal func BidirectionalStreaming( - callOptions: CallOptions? = nil, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.BidirectionalStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeBidirectionalStreamingInterceptors() ?? [], - handler: handler - ) - } - - /// Bidirectional streaming call to bidirectionalStreaming - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - internal func bidirectionalStreaming( - callOptions: CallOptions? = nil, - handler: @escaping (Normalization_FunctionName) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.bidirectionalStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makebidirectionalStreamingInterceptors() ?? [], - handler: handler - ) - } -} - -@available(*, deprecated) -extension Normalization_NormalizationClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Normalization_NormalizationNIOClient") -internal final class Normalization_NormalizationClient: Normalization_NormalizationClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - internal var interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the normalization.Normalization service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -internal struct Normalization_NormalizationNIOClient: Normalization_NormalizationClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? - - /// Creates a client for the normalization.Normalization service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Normalization_NormalizationAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? { get } - - func makeUnaryCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeunaryCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncUnaryCall - - func makeServerStreamingCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncServerStreamingCall - - func makeserverStreamingCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? - ) -> GRPCAsyncServerStreamingCall - - func makeClientStreamingCall( - callOptions: CallOptions? - ) -> GRPCAsyncClientStreamingCall - - func makeclientStreamingCall( - callOptions: CallOptions? - ) -> GRPCAsyncClientStreamingCall - - func makeBidirectionalStreamingCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall - - func makebidirectionalStreamingCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Normalization_NormalizationAsyncClientProtocol { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Normalization_NormalizationClientMetadata.serviceDescriptor - } - - internal var interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? { - return nil - } - - internal func makeUnaryCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Normalization_NormalizationClientMetadata.Methods.Unary.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryInterceptors() ?? [] - ) - } - - internal func makeunaryCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncUnaryCall { - return self.makeAsyncUnaryCall( - path: Normalization_NormalizationClientMetadata.Methods.unary.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeunaryInterceptors() ?? [] - ) - } - - internal func makeServerStreamingCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncServerStreamingCall { - return self.makeAsyncServerStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.ServerStreaming.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerStreamingInterceptors() ?? [] - ) - } - - internal func makeserverStreamingCall( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncServerStreamingCall { - return self.makeAsyncServerStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.serverStreaming.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeserverStreamingInterceptors() ?? [] - ) - } - - internal func makeClientStreamingCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncClientStreamingCall { - return self.makeAsyncClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.ClientStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeClientStreamingInterceptors() ?? [] - ) - } - - internal func makeclientStreamingCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncClientStreamingCall { - return self.makeAsyncClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.clientStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeclientStreamingInterceptors() ?? [] - ) - } - - internal func makeBidirectionalStreamingCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.BidirectionalStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeBidirectionalStreamingInterceptors() ?? [] - ) - } - - internal func makebidirectionalStreamingCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.bidirectionalStreaming.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makebidirectionalStreamingInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Normalization_NormalizationAsyncClientProtocol { - internal func Unary( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) async throws -> Normalization_FunctionName { - return try await self.performAsyncUnaryCall( - path: Normalization_NormalizationClientMetadata.Methods.Unary.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUnaryInterceptors() ?? [] - ) - } - - internal func unary( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) async throws -> Normalization_FunctionName { - return try await self.performAsyncUnaryCall( - path: Normalization_NormalizationClientMetadata.Methods.unary.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeunaryInterceptors() ?? [] - ) - } - - internal func ServerStreaming( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream { - return self.performAsyncServerStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.ServerStreaming.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerStreamingInterceptors() ?? [] - ) - } - - internal func serverStreaming( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream { - return self.performAsyncServerStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.serverStreaming.path, - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeserverStreamingInterceptors() ?? [] - ) - } - - internal func ClientStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Normalization_FunctionName where RequestStream: Sequence, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return try await self.performAsyncClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.ClientStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeClientStreamingInterceptors() ?? [] - ) - } - - internal func ClientStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Normalization_FunctionName where RequestStream: AsyncSequence & Sendable, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return try await self.performAsyncClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.ClientStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeClientStreamingInterceptors() ?? [] - ) - } - - internal func clientStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Normalization_FunctionName where RequestStream: Sequence, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return try await self.performAsyncClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.clientStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeclientStreamingInterceptors() ?? [] - ) - } - - internal func clientStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Normalization_FunctionName where RequestStream: AsyncSequence & Sendable, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return try await self.performAsyncClientStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.clientStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeclientStreamingInterceptors() ?? [] - ) - } - - internal func BidirectionalStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return self.performAsyncBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.BidirectionalStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeBidirectionalStreamingInterceptors() ?? [] - ) - } - - internal func BidirectionalStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return self.performAsyncBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.BidirectionalStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeBidirectionalStreamingInterceptors() ?? [] - ) - } - - internal func bidirectionalStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return self.performAsyncBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.bidirectionalStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makebidirectionalStreamingInterceptors() ?? [] - ) - } - - internal func bidirectionalStreaming( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == SwiftProtobuf.Google_Protobuf_Empty { - return self.performAsyncBidirectionalStreamingCall( - path: Normalization_NormalizationClientMetadata.Methods.bidirectionalStreaming.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makebidirectionalStreamingInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct Normalization_NormalizationAsyncClient: Normalization_NormalizationAsyncClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? - - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Normalization_NormalizationClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -internal protocol Normalization_NormalizationClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'Unary'. - func makeUnaryInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'unary'. - func makeunaryInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'ServerStreaming'. - func makeServerStreamingInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'serverStreaming'. - func makeserverStreamingInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'ClientStreaming'. - func makeClientStreamingInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'clientStreaming'. - func makeclientStreamingInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'BidirectionalStreaming'. - func makeBidirectionalStreamingInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'bidirectionalStreaming'. - func makebidirectionalStreamingInterceptors() -> [ClientInterceptor] -} - -internal enum Normalization_NormalizationClientMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "Normalization", - fullName: "normalization.Normalization", - methods: [ - Normalization_NormalizationClientMetadata.Methods.Unary, - Normalization_NormalizationClientMetadata.Methods.unary, - Normalization_NormalizationClientMetadata.Methods.ServerStreaming, - Normalization_NormalizationClientMetadata.Methods.serverStreaming, - Normalization_NormalizationClientMetadata.Methods.ClientStreaming, - Normalization_NormalizationClientMetadata.Methods.clientStreaming, - Normalization_NormalizationClientMetadata.Methods.BidirectionalStreaming, - Normalization_NormalizationClientMetadata.Methods.bidirectionalStreaming, - ] - ) - - internal enum Methods { - internal static let Unary = GRPCMethodDescriptor( - name: "Unary", - path: "/normalization.Normalization/Unary", - type: GRPCCallType.unary - ) - - internal static let unary = GRPCMethodDescriptor( - name: "unary", - path: "/normalization.Normalization/unary", - type: GRPCCallType.unary - ) - - internal static let ServerStreaming = GRPCMethodDescriptor( - name: "ServerStreaming", - path: "/normalization.Normalization/ServerStreaming", - type: GRPCCallType.serverStreaming - ) - - internal static let serverStreaming = GRPCMethodDescriptor( - name: "serverStreaming", - path: "/normalization.Normalization/serverStreaming", - type: GRPCCallType.serverStreaming - ) - - internal static let ClientStreaming = GRPCMethodDescriptor( - name: "ClientStreaming", - path: "/normalization.Normalization/ClientStreaming", - type: GRPCCallType.clientStreaming - ) - - internal static let clientStreaming = GRPCMethodDescriptor( - name: "clientStreaming", - path: "/normalization.Normalization/clientStreaming", - type: GRPCCallType.clientStreaming - ) - - internal static let BidirectionalStreaming = GRPCMethodDescriptor( - name: "BidirectionalStreaming", - path: "/normalization.Normalization/BidirectionalStreaming", - type: GRPCCallType.bidirectionalStreaming - ) - - internal static let bidirectionalStreaming = GRPCMethodDescriptor( - name: "bidirectionalStreaming", - path: "/normalization.Normalization/bidirectionalStreaming", - type: GRPCCallType.bidirectionalStreaming - ) - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol Normalization_NormalizationProvider: CallHandlerProvider { - var interceptors: Normalization_NormalizationServerInterceptorFactoryProtocol? { get } - - func Unary(request: SwiftProtobuf.Google_Protobuf_Empty, context: StatusOnlyCallContext) -> EventLoopFuture - - func unary(request: SwiftProtobuf.Google_Protobuf_Empty, context: StatusOnlyCallContext) -> EventLoopFuture - - func ServerStreaming(request: SwiftProtobuf.Google_Protobuf_Empty, context: StreamingResponseCallContext) -> EventLoopFuture - - func serverStreaming(request: SwiftProtobuf.Google_Protobuf_Empty, context: StreamingResponseCallContext) -> EventLoopFuture - - func ClientStreaming(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - func clientStreaming(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - func BidirectionalStreaming(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - func bidirectionalStreaming(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Normalization_NormalizationProvider { - internal var serviceName: Substring { - return Normalization_NormalizationServerMetadata.serviceDescriptor.fullName[...] - } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Unary": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnaryInterceptors() ?? [], - userFunction: self.Unary(request:context:) - ) - - case "unary": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeunaryInterceptors() ?? [], - userFunction: self.unary(request:context:) - ) - - case "ServerStreaming": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeServerStreamingInterceptors() ?? [], - userFunction: self.ServerStreaming(request:context:) - ) - - case "serverStreaming": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeserverStreamingInterceptors() ?? [], - userFunction: self.serverStreaming(request:context:) - ) - - case "ClientStreaming": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeClientStreamingInterceptors() ?? [], - observerFactory: self.ClientStreaming(context:) - ) - - case "clientStreaming": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeclientStreamingInterceptors() ?? [], - observerFactory: self.clientStreaming(context:) - ) - - case "BidirectionalStreaming": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeBidirectionalStreamingInterceptors() ?? [], - observerFactory: self.BidirectionalStreaming(context:) - ) - - case "bidirectionalStreaming": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makebidirectionalStreamingInterceptors() ?? [], - observerFactory: self.bidirectionalStreaming(context:) - ) - - default: - return nil - } - } -} - -/// To implement a server, implement an object which conforms to this protocol. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Normalization_NormalizationAsyncProvider: CallHandlerProvider, Sendable { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Normalization_NormalizationServerInterceptorFactoryProtocol? { get } - - func Unary( - request: SwiftProtobuf.Google_Protobuf_Empty, - context: GRPCAsyncServerCallContext - ) async throws -> Normalization_FunctionName - - func unary( - request: SwiftProtobuf.Google_Protobuf_Empty, - context: GRPCAsyncServerCallContext - ) async throws -> Normalization_FunctionName - - func ServerStreaming( - request: SwiftProtobuf.Google_Protobuf_Empty, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - func serverStreaming( - request: SwiftProtobuf.Google_Protobuf_Empty, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - func ClientStreaming( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Normalization_FunctionName - - func clientStreaming( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Normalization_FunctionName - - func BidirectionalStreaming( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws - - func bidirectionalStreaming( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Normalization_NormalizationAsyncProvider { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Normalization_NormalizationServerMetadata.serviceDescriptor - } - - internal var serviceName: Substring { - return Normalization_NormalizationServerMetadata.serviceDescriptor.fullName[...] - } - - internal var interceptors: Normalization_NormalizationServerInterceptorFactoryProtocol? { - return nil - } - - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Unary": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUnaryInterceptors() ?? [], - wrapping: { try await self.Unary(request: $0, context: $1) } - ) - - case "unary": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeunaryInterceptors() ?? [], - wrapping: { try await self.unary(request: $0, context: $1) } - ) - - case "ServerStreaming": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeServerStreamingInterceptors() ?? [], - wrapping: { try await self.ServerStreaming(request: $0, responseStream: $1, context: $2) } - ) - - case "serverStreaming": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeserverStreamingInterceptors() ?? [], - wrapping: { try await self.serverStreaming(request: $0, responseStream: $1, context: $2) } - ) - - case "ClientStreaming": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeClientStreamingInterceptors() ?? [], - wrapping: { try await self.ClientStreaming(requestStream: $0, context: $1) } - ) - - case "clientStreaming": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeclientStreamingInterceptors() ?? [], - wrapping: { try await self.clientStreaming(requestStream: $0, context: $1) } - ) - - case "BidirectionalStreaming": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeBidirectionalStreamingInterceptors() ?? [], - wrapping: { try await self.BidirectionalStreaming(requestStream: $0, responseStream: $1, context: $2) } - ) - - case "bidirectionalStreaming": - return GRPCAsyncServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makebidirectionalStreamingInterceptors() ?? [], - wrapping: { try await self.bidirectionalStreaming(requestStream: $0, responseStream: $1, context: $2) } - ) - - default: - return nil - } - } -} - -internal protocol Normalization_NormalizationServerInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when handling 'Unary'. - /// Defaults to calling `self.makeInterceptors()`. - func makeUnaryInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'unary'. - /// Defaults to calling `self.makeInterceptors()`. - func makeunaryInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'ServerStreaming'. - /// Defaults to calling `self.makeInterceptors()`. - func makeServerStreamingInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'serverStreaming'. - /// Defaults to calling `self.makeInterceptors()`. - func makeserverStreamingInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'ClientStreaming'. - /// Defaults to calling `self.makeInterceptors()`. - func makeClientStreamingInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'clientStreaming'. - /// Defaults to calling `self.makeInterceptors()`. - func makeclientStreamingInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'BidirectionalStreaming'. - /// Defaults to calling `self.makeInterceptors()`. - func makeBidirectionalStreamingInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'bidirectionalStreaming'. - /// Defaults to calling `self.makeInterceptors()`. - func makebidirectionalStreamingInterceptors() -> [ServerInterceptor] -} - -internal enum Normalization_NormalizationServerMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "Normalization", - fullName: "normalization.Normalization", - methods: [ - Normalization_NormalizationServerMetadata.Methods.Unary, - Normalization_NormalizationServerMetadata.Methods.unary, - Normalization_NormalizationServerMetadata.Methods.ServerStreaming, - Normalization_NormalizationServerMetadata.Methods.serverStreaming, - Normalization_NormalizationServerMetadata.Methods.ClientStreaming, - Normalization_NormalizationServerMetadata.Methods.clientStreaming, - Normalization_NormalizationServerMetadata.Methods.BidirectionalStreaming, - Normalization_NormalizationServerMetadata.Methods.bidirectionalStreaming, - ] - ) - - internal enum Methods { - internal static let Unary = GRPCMethodDescriptor( - name: "Unary", - path: "/normalization.Normalization/Unary", - type: GRPCCallType.unary - ) - - internal static let unary = GRPCMethodDescriptor( - name: "unary", - path: "/normalization.Normalization/unary", - type: GRPCCallType.unary - ) - - internal static let ServerStreaming = GRPCMethodDescriptor( - name: "ServerStreaming", - path: "/normalization.Normalization/ServerStreaming", - type: GRPCCallType.serverStreaming - ) - - internal static let serverStreaming = GRPCMethodDescriptor( - name: "serverStreaming", - path: "/normalization.Normalization/serverStreaming", - type: GRPCCallType.serverStreaming - ) - - internal static let ClientStreaming = GRPCMethodDescriptor( - name: "ClientStreaming", - path: "/normalization.Normalization/ClientStreaming", - type: GRPCCallType.clientStreaming - ) - - internal static let clientStreaming = GRPCMethodDescriptor( - name: "clientStreaming", - path: "/normalization.Normalization/clientStreaming", - type: GRPCCallType.clientStreaming - ) - - internal static let BidirectionalStreaming = GRPCMethodDescriptor( - name: "BidirectionalStreaming", - path: "/normalization.Normalization/BidirectionalStreaming", - type: GRPCCallType.bidirectionalStreaming - ) - - internal static let bidirectionalStreaming = GRPCMethodDescriptor( - name: "bidirectionalStreaming", - path: "/normalization.Normalization/bidirectionalStreaming", - type: GRPCCallType.bidirectionalStreaming - ) - } -} diff --git a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift b/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift deleted file mode 100644 index 0a3f36e95..000000000 --- a/Tests/GRPCTests/Codegen/Normalization/normalization.pb.swift +++ /dev/null @@ -1,84 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: normalization.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2021 gRPC authors. -// -// 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 SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -struct Normalization_FunctionName: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The name of the invoked function. - var functionName: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "normalization" - -extension Normalization_FunctionName: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".FunctionName" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "functionName"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.functionName) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.functionName.isEmpty { - try visitor.visitSingularStringField(value: self.functionName, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Normalization_FunctionName, rhs: Normalization_FunctionName) -> Bool { - if lhs.functionName != rhs.functionName {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift deleted file mode 100644 index a4bcd28e8..000000000 --- a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023, 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 Foundation -import GRPC -import SwiftProtobuf -import XCTest - -final class SerializationTests: GRPCTestCase { - var fileDescriptorProto: Google_Protobuf_FileDescriptorProto! - - override func setUp() { - super.setUp() - let binaryFileURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent().appendingPathComponent("echo.grpc.reflection") - let base64EncodedData = try! Data(contentsOf: binaryFileURL) - let binaryData = Data(base64Encoded: base64EncodedData)! - self.fileDescriptorProto = try! Google_Protobuf_FileDescriptorProto(serializedBytes: binaryData) - } - - func testFileDescriptorMetadata() throws { - let name = self.fileDescriptorProto.name - XCTAssertEqual(name, "echo.proto") - - let syntax = self.fileDescriptorProto.syntax - XCTAssertEqual(syntax, "proto3") - - let package = self.fileDescriptorProto.package - XCTAssertEqual(package, "echo") - } - - func testFileDescriptorMessages() { - let messages = self.fileDescriptorProto.messageType - XCTAssertEqual(messages.count, 2) - for message in messages { - XCTAssert((message.name == "EchoRequest") || (message.name == "EchoResponse")) - XCTAssertEqual(message.field.count, 1) - XCTAssertEqual(message.field.first!.name, "text") - XCTAssert(message.field.first!.hasNumber) - } - } - - func testFileDescriptorServices() { - let services = self.fileDescriptorProto.service - XCTAssertEqual(services.count, 1) - XCTAssertEqual(self.fileDescriptorProto.service.first!.method.count, 4) - for method in self.fileDescriptorProto.service.first!.method { - switch method.name { - case "Get": - XCTAssertEqual(method.inputType, ".echo.EchoRequest") - XCTAssertEqual(method.outputType, ".echo.EchoResponse") - case "Expand": - XCTAssertEqual(method.inputType, ".echo.EchoRequest") - XCTAssertEqual(method.outputType, ".echo.EchoResponse") - XCTAssert(method.serverStreaming) - case "Collect": - XCTAssertEqual(method.inputType, ".echo.EchoRequest") - XCTAssertEqual(method.outputType, ".echo.EchoResponse") - XCTAssert(method.clientStreaming) - case "Update": - XCTAssertEqual(method.inputType, ".echo.EchoRequest") - XCTAssertEqual(method.outputType, ".echo.EchoResponse") - XCTAssert(method.clientStreaming) - XCTAssert(method.serverStreaming) - default: - XCTFail("The method name is incorrect.") - } - } - } -} diff --git a/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection b/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection deleted file mode 100644 index af26ef4a7..000000000 --- a/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection +++ /dev/null @@ -1 +0,0 @@ -CgplY2hvLnByb3RvEgRlY2hvIiEKC0VjaG9SZXF1ZXN0EhIKBHRleHQYASABKAlSBHRleHQiIgoMRWNob1Jlc3BvbnNlEhIKBHRleHQYASABKAlSBHRleHQy2AEKBEVjaG8SLgoDR2V0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgASMwoGRXhwYW5kEhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAwARI0CgdDb2xsZWN0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAoARI1CgZVcGRhdGUSES5lY2hvLkVjaG9SZXF1ZXN0GhIuZWNoby5FY2hvUmVzcG9uc2UiACgBMAFK/QoKBhIEDgAoAQrCBAoBDBIDDgASMrcEIENvcHlyaWdodCAoYykgMjAxNSwgR29vZ2xlIEluYy4KCiBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCggKAQISAxAADQoKCgIGABIEEgAeAQoKCgMGAAESAxIIDAo4CgQGAAIAEgMUAjAaKyBJbW1lZGlhdGVseSByZXR1cm5zIGFuIGVjaG8gb2YgYSByZXF1ZXN0LgoKDAoFBgACAAESAxQGCQoMCgUGAAIAAhIDFAoVCgwKBQYAAgADEgMUICwKWQoEBgACARIDFwI6GkwgU3BsaXRzIGEgcmVxdWVzdCBpbnRvIHdvcmRzIGFuZCByZXR1cm5zIGVhY2ggd29yZCBpbiBhIHN0cmVhbSBvZiBtZXNzYWdlcy4KCgwKBQYAAgEBEgMXBgwKDAoFBgACAQISAxcNGAoMCgUGAAIBBhIDFyMpCgwKBQYAAgEDEgMXKjYKYgoEBgACAhIDGgI7GlUgQ29sbGVjdHMgYSBzdHJlYW0gb2YgbWVzc2FnZXMgYW5kIHJldHVybnMgdGhlbSBjb25jYXRlbmF0ZWQgd2hlbiB0aGUgY2FsbGVyIGNsb3Nlcy4KCgwKBQYAAgIBEgMaBg0KDAoFBgACAgUSAxoOFAoMCgUGAAICAhIDGhUgCgwKBQYAAgIDEgMaKzcKTQoEBgACAxIDHQJBGkAgU3RyZWFtcyBiYWNrIG1lc3NhZ2VzIGFzIHRoZXkgYXJlIHJlY2VpdmVkIGluIGFuIGlucHV0IHN0cmVhbS4KCgwKBQYAAgMBEgMdBgwKDAoFBgACAwUSAx0NEwoMCgUGAAIDAhIDHRQfCgwKBQYAAgMGEgMdKjAKDAoFBgACAwMSAx0xPQoKCgIEABIEIAAjAQoKCgMEAAESAyAIEwoyCgQEAAIAEgMiAhIaJSBUaGUgdGV4dCBvZiBhIG1lc3NhZ2UgdG8gYmUgZWNob2VkLgoKDAoFBAACAAUSAyICCAoMCgUEAAIAARIDIgkNCgwKBQQAAgADEgMiEBEKCgoCBAESBCUAKAEKCgoDBAEBEgMlCBQKLAoEBAECABIDJwISGh8gVGhlIHRleHQgb2YgYW4gZWNobyByZXNwb25zZS4KCgwKBQQBAgAFEgMnAggKDAoFBAECAAESAycJDQoMCgUEAQIAAxIDJxARYgZwcm90bzM= \ No newline at end of file diff --git a/Tests/GRPCTests/CompressionTests.swift b/Tests/GRPCTests/CompressionTests.swift deleted file mode 100644 index 99f3693d0..000000000 --- a/Tests/GRPCTests/CompressionTests.swift +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import GRPC -import NIOConcurrencyHelpers -import NIOCore -import NIOHPACK -import NIOPosix -import XCTest - -class MessageCompressionTests: GRPCTestCase { - var group: EventLoopGroup! - var server: Server! - var client: ClientConnection! - var defaultTimeout: TimeInterval = 1.0 - - var echo: Echo_EchoNIOClient! - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - XCTAssertNoThrow(try self.client.close().wait()) - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - func setupServer(encoding: ServerMessageEncoding) throws { - self.server = try Server.insecure(group: self.group) - .withServiceProviders([EchoProvider()]) - .withMessageCompression(encoding) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - } - - func setupClient(encoding: ClientMessageEncoding) { - self.client = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: self.server.channel.localAddress!.port!) - - self.echo = Echo_EchoNIOClient( - channel: self.client, - defaultCallOptions: CallOptions(messageEncoding: encoding, logger: self.clientLogger) - ) - } - - func testCompressedRequestsUncompressedResponses() throws { - // Enable compression, but don't advertise that it's enabled. - // The spec says that servers should handle compression they support but don't advertise. - try self - .setupServer(encoding: .enabled(.init(enabledAlgorithms: [], decompressionLimit: .ratio(10)))) - self.setupClient( - encoding: .enabled( - .init( - forRequests: .gzip, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ) - ) - ) - - let get = self.echo.get(.with { $0.text = "foo" }) - - let initialMetadata = self.expectation(description: "received initial metadata") - get.initialMetadata.map { - $0.contains(name: "grpc-encoding") - }.assertEqual(false, fulfill: initialMetadata) - - let status = self.expectation(description: "received status") - get.status.map { - $0.code - }.assertEqual(.ok, fulfill: status) - - self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout) - } - - func testUncompressedRequestsCompressedResponses() throws { - try self.setupServer(encoding: .enabled(.init(decompressionLimit: .ratio(10)))) - self.setupClient( - encoding: .enabled( - .init( - forRequests: .none, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ) - ) - ) - - let get = self.echo.get(.with { $0.text = "foo" }) - - let initialMetadata = self.expectation(description: "received initial metadata") - get.initialMetadata.map { - $0.first(name: "grpc-encoding") - }.assertEqual("deflate", fulfill: initialMetadata) - - let status = self.expectation(description: "received status") - get.status.map { - $0.code - }.assertEqual(.ok, fulfill: status) - - self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout) - } - - func testServerCanDecompressNonAdvertisedButSupportedCompression() throws { - // Server should be able to decompress a format it supports but does not advertise. In doing - // so it must also return a "grpc-accept-encoding" header which includes the value it did not - // advertise. - try self - .setupServer( - encoding: .enabled( - .init( - enabledAlgorithms: [.gzip], - decompressionLimit: .ratio(10) - ) - ) - ) - self - .setupClient( - encoding: .enabled( - .init( - forRequests: .deflate, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - ) - ) - ) - - let get = self.echo.get(.with { $0.text = "foo" }) - - let initialMetadata = self.expectation(description: "received initial metadata") - get.initialMetadata.map { - $0[canonicalForm: "grpc-accept-encoding"] - }.assertEqual(["gzip", "deflate"], fulfill: initialMetadata) - - let status = self.expectation(description: "received status") - get.status.map { - $0.code - }.assertEqual(.ok, fulfill: status) - - self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout) - } - - func testServerCompressesResponseWithDifferentAlgorithmToRequest() throws { - // Server should be able to compress responses with a different method to the client, providing - // the client supports it. - try self - .setupServer( - encoding: .enabled( - .init( - enabledAlgorithms: [.gzip], - decompressionLimit: .ratio(10) - ) - ) - ) - self.setupClient( - encoding: .enabled( - .init( - forRequests: .deflate, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ) - ) - ) - - let get = self.echo.get(.with { $0.text = "foo" }) - - let initialMetadata = self.expectation(description: "received initial metadata") - get.initialMetadata.map { - $0.first(name: "grpc-encoding") - }.assertEqual("gzip", fulfill: initialMetadata) - - let status = self.expectation(description: "received status") - get.status.map { - $0.code - }.assertEqual(.ok, fulfill: status) - - self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout) - } - - func testCompressedRequestWithCompressionNotSupportedOnServer() throws { - try self - .setupServer( - encoding: .enabled( - .init( - enabledAlgorithms: [.gzip, .deflate], - decompressionLimit: .ratio(10) - ) - ) - ) - // We can't specify a compression we don't support, so we'll specify no compression and then - // send a 'grpc-encoding' with our initial metadata. - self.setupClient( - encoding: .enabled( - .init( - forRequests: .none, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ) - ) - ) - - let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"] - let get = self.echo.get( - .with { $0.text = "foo" }, - callOptions: CallOptions(customMetadata: headers) - ) - - let response = self.expectation(description: "received response") - get.response.assertError(fulfill: response) - - let trailers = self.expectation(description: "received trailing metadata") - get.trailingMetadata.map { - $0[canonicalForm: "grpc-accept-encoding"] - }.assertEqual(["gzip", "deflate"], fulfill: trailers) - - let status = self.expectation(description: "received status") - get.status.map { - $0.code - }.assertEqual(.unimplemented, fulfill: status) - - self.wait(for: [response, trailers, status], timeout: self.defaultTimeout) - } - - func testDecompressionLimitIsRespectedByServerForUnaryCall() throws { - try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1)))) - self - .setupClient( - encoding: .enabled( - .init( - forRequests: .gzip, - decompressionLimit: .absolute(1024) - ) - ) - ) - - let get = self.echo.get(.with { $0.text = "foo" }) - let status = self.expectation(description: "received status") - - get.status.map { - $0.code - }.assertEqual(.resourceExhausted, fulfill: status) - - self.wait(for: [status], timeout: self.defaultTimeout) - } - - func testDecompressionLimitIsRespectedByServerForStreamingCall() throws { - try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(1024)))) - self - .setupClient( - encoding: .enabled( - .init( - forRequests: .gzip, - decompressionLimit: .absolute(2048) - ) - ) - ) - - let collect = self.echo.collect() - let status = self.expectation(description: "received status") - - // Smaller than limit. - collect.sendMessage(.with { $0.text = "foo" }, promise: nil) - // Should be just over the limit. - collect.sendMessage(.with { $0.text = String(repeating: "x", count: 1024) }, promise: nil) - collect.sendEnd(promise: nil) - - collect.status.map { - $0.code - }.assertEqual(.resourceExhausted, fulfill: status) - - self.wait(for: [status], timeout: self.defaultTimeout) - } - - func testDecompressionLimitIsRespectedByClientForUnaryCall() throws { - try self - .setupServer( - encoding: .enabled( - .init( - enabledAlgorithms: [.gzip], - decompressionLimit: .absolute(1024) - ) - ) - ) - self.setupClient(encoding: .enabled(.responsesOnly(decompressionLimit: .absolute(1)))) - - let get = self.echo.get(.with { $0.text = "foo" }) - let status = self.expectation(description: "received status") - - get.status.map { - $0.code - }.assertEqual(.resourceExhausted, fulfill: status) - - self.wait(for: [status], timeout: self.defaultTimeout) - } - - func testDecompressionLimitIsRespectedByClientForStreamingCall() throws { - try self.setupServer(encoding: .enabled(.init(decompressionLimit: .absolute(2048)))) - self.setupClient( - encoding: .enabled(.init(forRequests: .gzip, decompressionLimit: .absolute(1024))) - ) - - let responsePromise = self.group.next().makePromise(of: Echo_EchoResponse.self) - let lock = NIOLock() - var responseCount = 0 - - let update = self.echo.update { - lock.withLock { - responseCount += 1 - } - responsePromise.succeed($0) - } - - let status = self.expectation(description: "received status") - - // Smaller than limit. - update.sendMessage(.with { $0.text = "foo" }, promise: nil) - XCTAssertNoThrow(try responsePromise.futureResult.wait()) - - // Should be just over the limit. - update.sendMessage(.with { $0.text = String(repeating: "x", count: 1024) }, promise: nil) - update.sendEnd(promise: nil) - - update.status.map { - $0.code - }.assertEqual(.resourceExhausted, fulfill: status) - - self.wait(for: [status], timeout: self.defaultTimeout) - let receivedResponses = lock.withLock { responseCount } - XCTAssertEqual(receivedResponses, 1) - } - - func testIdentityCompressionIsntCompression() throws { - // The client offers "identity" compression, the server doesn't support compression. We should - // tolerate this, as "identity" is no compression at all. - try self - .setupServer(encoding: .disabled) - // We can't specify a compression we don't support, like identity, so we'll specify no compression and then - // send a 'grpc-encoding' with our initial metadata. - self.setupClient(encoding: .disabled) - - let headers: HPACKHeaders = ["grpc-encoding": "identity"] - let get = self.echo.get( - .with { $0.text = "foo" }, - callOptions: CallOptions(customMetadata: headers) - ) - - let initialMetadata = self.expectation(description: "received initial metadata") - get.initialMetadata.map { - $0.contains(name: "grpc-encoding") - }.assertEqual(false, fulfill: initialMetadata) - - let status = self.expectation(description: "received status") - get.status.map { - $0.code - }.assertEqual(.ok, fulfill: status) - - self.wait(for: [initialMetadata, status], timeout: self.defaultTimeout) - } - - func testCompressedRequestWithDisabledServerCompressionAndUnknownCompressionAlgorithm() throws { - try self.setupServer(encoding: .disabled) - // We can't specify a compression we don't support, so we'll specify no compression and then - // send a 'grpc-encoding' with our initial metadata. - self.setupClient( - encoding: .enabled( - .init( - forRequests: .none, - acceptableForResponses: [.deflate, .gzip], - decompressionLimit: .ratio(10) - ) - ) - ) - - let headers: HPACKHeaders = ["grpc-encoding": "you-don't-support-this"] - let get = self.echo.get( - .with { $0.text = "foo" }, - callOptions: CallOptions(customMetadata: headers) - ) - - let response = self.expectation(description: "received response") - get.response.assertError(fulfill: response) - - let trailers = self.expectation(description: "received trailing metadata") - get.trailingMetadata.map { - $0.contains(name: "grpc-accept-encoding") - }.assertEqual(false, fulfill: trailers) - - let status = self.expectation(description: "received status") - get.status.map { - $0.code - }.assertEqual(.unimplemented, fulfill: status) - - self.wait(for: [response, trailers, status], timeout: self.defaultTimeout) - } -} diff --git a/Tests/GRPCTests/ConfigurationTests.swift b/Tests/GRPCTests/ConfigurationTests.swift deleted file mode 100644 index 780ae2a1f..000000000 --- a/Tests/GRPCTests/ConfigurationTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 GRPC -import NIOEmbedded -import XCTest - -final class ConfigurationTests: GRPCTestCase { - private var eventLoop: EmbeddedEventLoop! - - private var clientDefaults: ClientConnection.Configuration { - return .default(target: .unixDomainSocket("/ignored"), eventLoopGroup: self.eventLoop) - } - - private var serverDefaults: Server.Configuration { - return .default( - target: .unixDomainSocket("/ignored"), - eventLoopGroup: self.eventLoop, - serviceProviders: [] - ) - } - - override func setUp() { - super.setUp() - self.eventLoop = EmbeddedEventLoop() - } - - override func tearDown() { - XCTAssertNoThrow(try self.eventLoop.syncShutdownGracefully()) - super.tearDown() - } - - private let maxFrameSizeMinimum = (1 << 14) - private let maxFrameSizeMaximum = (1 << 24) - 1 - - private func doTestHTTPMaxFrameSizeIsClamped(for configuration: HasHTTP2Configuration) { - var configuration = configuration - configuration.httpMaxFrameSize = 0 - XCTAssertEqual(configuration.httpMaxFrameSize, self.maxFrameSizeMinimum) - - configuration.httpMaxFrameSize = .max - XCTAssertEqual(configuration.httpMaxFrameSize, self.maxFrameSizeMaximum) - - configuration.httpMaxFrameSize = self.maxFrameSizeMinimum + 1 - XCTAssertEqual(configuration.httpMaxFrameSize, self.maxFrameSizeMinimum + 1) - } - - func testHTTPMaxFrameSizeIsClampedForClient() { - self.doTestHTTPMaxFrameSizeIsClamped(for: self.clientDefaults) - } - - func testHTTPMaxFrameSizeIsClampedForServer() { - self.doTestHTTPMaxFrameSizeIsClamped(for: self.serverDefaults) - } - - private let targetWindowSizeMinimum = 1 - private let targetWindowSizeMaximum = Int(Int32.max) - - private func doTestHTTPTargetWindowSizeIsClamped(for configuration: HasHTTP2Configuration) { - var configuration = configuration - configuration.httpTargetWindowSize = .min - XCTAssertEqual(configuration.httpTargetWindowSize, self.targetWindowSizeMinimum) - - configuration.httpTargetWindowSize = .max - XCTAssertEqual(configuration.httpTargetWindowSize, self.targetWindowSizeMaximum) - - configuration.httpTargetWindowSize = self.targetWindowSizeMinimum + 1 - XCTAssertEqual(configuration.httpTargetWindowSize, self.targetWindowSizeMinimum + 1) - } - - func testHTTPTargetWindowSizeIsClampedForClient() { - self.doTestHTTPTargetWindowSizeIsClamped(for: self.clientDefaults) - } - - func testHTTPTargetWindowSizeIsClampedForServer() { - self.doTestHTTPTargetWindowSizeIsClamped(for: self.serverDefaults) - } -} - -private protocol HasHTTP2Configuration { - var httpMaxFrameSize: Int { get set } - var httpTargetWindowSize: Int { get set } -} - -extension ClientConnection.Configuration: HasHTTP2Configuration {} -extension Server.Configuration: HasHTTP2Configuration {} diff --git a/Tests/GRPCTests/ConnectionBackoffTests.swift b/Tests/GRPCTests/ConnectionBackoffTests.swift deleted file mode 100644 index 6308e8afd..000000000 --- a/Tests/GRPCTests/ConnectionBackoffTests.swift +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 Foundation -import GRPC -import XCTest - -class ConnectionBackoffTests: GRPCTestCase { - var backoff = ConnectionBackoff() - - func testExpectedValuesWithNoJitter() { - self.backoff.jitter = 0.0 - self.backoff.multiplier = 2.0 - self.backoff.initialBackoff = 1.0 - self.backoff.maximumBackoff = 16.0 - self.backoff.minimumConnectionTimeout = 4.2 - - let timeoutAndBackoff = self.backoff.prefix(5) - - let expectedBackoff: [TimeInterval] = [1.0, 2.0, 4.0, 8.0, 16.0] - XCTAssertEqual(expectedBackoff, timeoutAndBackoff.map { $0.backoff }) - - let expectedTimeout: [TimeInterval] = [4.2, 4.2, 4.2, 8.0, 16.0] - XCTAssertEqual(expectedTimeout, timeoutAndBackoff.map { $0.timeout }) - } - - func testBackoffWithNoJitter() { - self.backoff.jitter = 0.0 - for (i, backoff) in self.backoff.prefix(100).map({ $0.backoff }).enumerated() { - let expected = min( - pow(self.backoff.initialBackoff * self.backoff.multiplier, Double(i)), - self.backoff.maximumBackoff - ) - XCTAssertEqual(expected, backoff, accuracy: 1e-6) - } - } - - func testBackoffWithJitter() { - for (i, timeoutAndBackoff) in self.backoff.prefix(100).enumerated() { - let unjittered = min( - pow(self.backoff.initialBackoff * self.backoff.multiplier, Double(i)), - self.backoff.maximumBackoff - ) - let halfJitterRange = self.backoff.jitter * unjittered - let jitteredRange = (unjittered - halfJitterRange) ... (unjittered + halfJitterRange) - XCTAssert(jitteredRange.contains(timeoutAndBackoff.backoff)) - } - } - - func testBackoffDoesNotExceedMaximum() { - // Since jitter is applied after checking against the maximum allowed backoff, the maximum - // backoff can still be exceeded if jitter is non-zero. - self.backoff.jitter = 0.0 - - for backoff in self.backoff.prefix(100).map({ $0.backoff }) { - XCTAssertLessThanOrEqual(backoff, self.backoff.maximumBackoff) - } - } - - func testConnectionTimeoutAlwaysGreaterThanOrEqualToMinimum() { - for connectionTimeout in self.backoff.prefix(100).map({ $0.timeout }) { - XCTAssertGreaterThanOrEqual(connectionTimeout, self.backoff.minimumConnectionTimeout) - } - } - - func testConnectionBackoffHasLimitedRetries() { - for limit in [1, 3, 5] { - let backoff = ConnectionBackoff(retries: .upTo(limit)) - let values = Array(backoff) - XCTAssertEqual(values.count, limit) - } - } - - func testConnectionBackoffWhenLimitedToZeroRetries() { - let backoff = ConnectionBackoff(retries: .upTo(0)) - let values = Array(backoff) - XCTAssertTrue(values.isEmpty) - } - - func testConnectionBackoffWithNoRetries() { - let backoff = ConnectionBackoff(retries: .none) - let values = Array(backoff) - XCTAssertTrue(values.isEmpty) - } -} diff --git a/Tests/GRPCTests/ConnectionFailingTests.swift b/Tests/GRPCTests/ConnectionFailingTests.swift deleted file mode 100644 index b7170e614..000000000 --- a/Tests/GRPCTests/ConnectionFailingTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2020, 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 - -class ConnectionFailingTests: GRPCTestCase { - func testStartRPCWhenChannelIsInTransientFailure() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let waiter = RecordingConnectivityDelegate() - let connection = ClientConnection.insecure(group: group) - // We want to make sure we sit in transient failure for a long time. - .withConnectionBackoff(initial: .hours(24)) - .withCallStartBehavior(.fastFailure) - .withConnectivityStateDelegate(waiter) - .connect(host: "http://unreachable.invalid", port: 0) - defer { - XCTAssertNoThrow(try connection.close().wait()) - } - - let echo = Echo_EchoNIOClient(channel: connection) - - // Set our expectation. - waiter.expectChanges(2) { changes in - XCTAssertEqual(changes[0], Change(from: .idle, to: .connecting)) - XCTAssertEqual(changes[1], Change(from: .connecting, to: .transientFailure)) - } - - // This will trigger a connection attempt and subsequently fail. - _ = echo.get(.with { $0.text = "cheddar" }) - - // Wait for the changes. - waiter.waitForExpectedChanges(timeout: .seconds(10)) - - // Okay, now let's try another RPC. It should fail immediately with the connection error. - let get = echo.get(.with { $0.text = "comté" }) - XCTAssertThrowsError(try get.response.wait()) - let status = try get.status.wait() - XCTAssertEqual(status.code, .unavailable) - // We can't say too much about the message here. It should contain details about the transient - // failure error. - XCTAssertNotNil(status.message) - XCTAssertTrue(status.message?.contains("unreachable.invalid") ?? false) - } -} diff --git a/Tests/GRPCTests/ConnectionManagerTests.swift b/Tests/GRPCTests/ConnectionManagerTests.swift deleted file mode 100644 index e416684cc..000000000 --- a/Tests/GRPCTests/ConnectionManagerTests.swift +++ /dev/null @@ -1,1573 +0,0 @@ -/* - * Copyright 2020, 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 Logging -import NIOCore -import NIOEmbedded -import NIOHTTP2 -import XCTest - -@testable import GRPC - -class ConnectionManagerTests: GRPCTestCase { - private let loop = EmbeddedEventLoop() - private let recorder = RecordingConnectivityDelegate() - private var monitor: ConnectivityStateMonitor! - - private var defaultConfiguration: ClientConnection.Configuration { - var configuration = ClientConnection.Configuration.default( - target: .unixDomainSocket("/ignored"), - eventLoopGroup: self.loop - ) - - configuration.connectionBackoff = nil - configuration.backgroundActivityLogger = self.clientLogger - - return configuration - } - - override func setUp() { - super.setUp() - self.monitor = ConnectivityStateMonitor(delegate: self.recorder, queue: nil) - } - - override func tearDown() { - XCTAssertNoThrow(try self.loop.syncShutdownGracefully()) - super.tearDown() - } - - private func makeConnectionManager( - configuration config: ClientConnection.Configuration? = nil, - channelProvider: ((ConnectionManager, EventLoop) -> EventLoopFuture)? = nil - ) -> ConnectionManager { - let configuration = config ?? self.defaultConfiguration - - return ConnectionManager( - configuration: configuration, - channelProvider: channelProvider.map { HookedChannelProvider($0) }, - connectivityDelegate: self.monitor, - idleBehavior: .closeWhenIdleTimeout, - logger: self.logger - ) - } - - private func waitForStateChange( - from: ConnectivityState, - to: ConnectivityState, - timeout: DispatchTimeInterval = .seconds(1), - file: StaticString = #filePath, - line: UInt = #line, - body: () throws -> Result - ) rethrows -> Result { - self.recorder.expectChange { - XCTAssertEqual($0, Change(from: from, to: to), file: file, line: line) - } - let result = try body() - self.recorder.waitForExpectedChanges(timeout: timeout, file: file, line: line) - return result - } - - private func waitForStateChanges( - _ changes: [Change], - timeout: DispatchTimeInterval = .seconds(1), - file: StaticString = #filePath, - line: UInt = #line, - body: () throws -> Result - ) rethrows -> Result { - self.recorder.expectChanges(changes.count) { - XCTAssertEqual($0, changes) - } - let result = try body() - self.recorder.waitForExpectedChanges(timeout: timeout, file: file, line: line) - return result - } -} - -extension ConnectionManagerTests { - func testIdleShutdown() throws { - let manager = self.makeConnectionManager() - - try self.waitForStateChange(from: .idle, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - - // Getting a multiplexer should fail. - let multiplexer = manager.getHTTP2Multiplexer() - self.loop.run() - XCTAssertThrowsError(try multiplexer.wait()) - } - - func testConnectFromIdleFailsWithNoReconnect() { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - let multiplexer: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let channel = manager.getHTTP2Multiplexer() - self.loop.run() - return channel - } - - self.waitForStateChange(from: .connecting, to: .shutdown) { - channelPromise.fail(DoomedChannelError()) - } - - XCTAssertThrowsError(try multiplexer.wait()) { - XCTAssertTrue($0 is DoomedChannelError) - } - } - - func testConnectAndDisconnect() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - // Start the connection. - self.waitForStateChange(from: .idle, to: .connecting) { - _ = manager.getHTTP2Multiplexer() - self.loop.run() - } - - // Setup the real channel and activate it. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - channelPromise.succeed(channel) - XCTAssertNoThrow( - try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // Write a settings frame on the root stream; this'll make the channel 'ready'. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) - } - - // Close the channel. - try self.waitForStateChange(from: .ready, to: .shutdown) { - // Now the channel should be available: shut it down. - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testConnectAndIdle() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - // Start the connection. - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Setup the channel. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - channelPromise.succeed(channel) - XCTAssertNoThrow( - try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // Write a settings frame on the root stream; this'll make the channel 'ready'. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) - // Wait for the multiplexer, it _must_ be ready now. - XCTAssertNoThrow(try readyChannelMux.wait()) - } - - // Go idle. This will shutdown the channel. - try self.waitForStateChange(from: .ready, to: .idle) { - self.loop.advanceTime(by: .minutes(5)) - XCTAssertNoThrow(try channel.closeFuture.wait()) - } - - // Now shutdown. - try self.waitForStateChange(from: .idle, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testChannelInactiveBeforeActiveWithNoReconnect() throws { - let channel = EmbeddedChannel(loop: self.loop) - let channelPromise = self.loop.makePromise(of: Channel.self) - - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - // Start the connection. - self.waitForStateChange(from: .idle, to: .connecting) { - // Triggers the connect. - _ = manager.getHTTP2Multiplexer() - self.loop.run() - } - - try channel.pipeline.syncOperations.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ), - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ) - channelPromise.succeed(channel) - // Oops: wrong way around. We should tolerate this. - self.waitForStateChange(from: .connecting, to: .shutdown) { - channel.pipeline.fireChannelInactive() - } - - // Should be ignored. - channel.pipeline.fireChannelActive() - } - - func testChannelInactiveBeforeActiveWillReconnect() throws { - var channels = [EmbeddedChannel(loop: self.loop), EmbeddedChannel(loop: self.loop)] - var channelPromises: [EventLoopPromise] = [ - self.loop.makePromise(), - self.loop.makePromise(), - ] - var channelFutures = Array(channelPromises.map { $0.futureResult }) - - var configuration = self.defaultConfiguration - configuration.connectionBackoff = .oneSecondFixed - - let manager = self.makeConnectionManager(configuration: configuration) { _, _ in - return channelFutures.removeLast() - } - - // Start the connection. - self.waitForStateChange(from: .idle, to: .connecting) { - // Triggers the connect. - _ = manager.getHTTP2Multiplexer() - self.loop.run() - } - - // Setup the channel. - let channel1 = channels.removeLast() - let channel1Promise = channelPromises.removeLast() - - try channel1.pipeline.syncOperations.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: HTTP2StreamMultiplexer( - mode: .client, - channel: channel1, - inboundStreamInitializer: nil - ), - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ) - channel1Promise.succeed(channel1) - // Oops: wrong way around. We should tolerate this. - self.waitForStateChange(from: .connecting, to: .transientFailure) { - channel1.pipeline.fireChannelInactive() - } - - channel1.pipeline.fireChannelActive() - - // Start the next attempt. - self.waitForStateChange(from: .transientFailure, to: .connecting) { - self.loop.advanceTime(by: .seconds(1)) - } - - let channel2 = channels.removeLast() - let channel2Promise = channelPromises.removeLast() - try channel2.pipeline.syncOperations.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: HTTP2StreamMultiplexer( - mode: .client, - channel: channel1, - inboundStreamInitializer: nil - ), - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ) - - channel2Promise.succeed(channel2) - - try self.waitForStateChange(from: .connecting, to: .ready) { - channel2.pipeline.fireChannelActive() - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel2.writeInbound(frame)) - } - } - - func testIdleTimeoutWhenThereAreActiveStreams() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - // Start the connection. - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Setup the channel. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - - channelPromise.succeed(channel) - XCTAssertNoThrow( - try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // Write a settings frame on the root stream; this'll make the channel 'ready'. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) - // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. - XCTAssertNoThrow(try readyChannelMux.wait()) - } - - // "create" a stream; the details don't matter here. - let streamCreated = NIOHTTP2StreamCreatedEvent( - streamID: 1, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - channel.pipeline.fireUserInboundEventTriggered(streamCreated) - - // Wait for the idle timeout: this should _not_ cause the channel to idle. - self.loop.advanceTime(by: .minutes(5)) - - // Now we're going to close the stream and wait for an idle timeout and then shutdown. - self.waitForStateChange(from: .ready, to: .idle) { - // Close the stream. - let streamClosed = StreamClosedEvent(streamID: 1, reason: nil) - channel.pipeline.fireUserInboundEventTriggered(streamClosed) - // ... wait for the idle timeout, - self.loop.advanceTime(by: .minutes(5)) - } - - // Now shutdown. - try self.waitForStateChange(from: .idle, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testConnectAndThenBecomeInactive() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Setup the channel. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - channelPromise.succeed(channel) - XCTAssertNoThrow( - try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - try self.waitForStateChange(from: .connecting, to: .shutdown) { - // Okay: now close the channel; the `readyChannel` future has not been completed yet. - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - - // We failed to get a channel and we don't have reconnect configured: we should be shutdown and - // the `readyChannelMux` should error. - XCTAssertThrowsError(try readyChannelMux.wait()) - } - - func testConnectOnSecondAttempt() throws { - let channelPromise: EventLoopPromise = self.loop.makePromise() - let channelFutures: [EventLoopFuture] = [ - self.loop.makeFailedFuture(DoomedChannelError()), - channelPromise.futureResult, - ] - var channelFutureIterator = channelFutures.makeIterator() - - var configuration = self.defaultConfiguration - configuration.connectionBackoff = .oneSecondFixed - - let manager = self.makeConnectionManager(configuration: configuration) { _, _ in - guard let next = channelFutureIterator.next() else { - XCTFail("Too many channels requested") - return self.loop.makeFailedFuture(DoomedChannelError()) - } - return next - } - - let readyChannelMux: EventLoopFuture = self.waitForStateChanges([ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ]) { - // Get a HTTP/2 stream multiplexer. - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Get a HTTP/2 stream mux from the manager - it is a future for the one we made earlier. - let anotherReadyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - - // Move time forwards by a second to start the next connection attempt. - self.waitForStateChange(from: .transientFailure, to: .connecting) { - self.loop.advanceTime(by: .seconds(1)) - } - - // Setup the actual channel and complete the promise. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - channelPromise.succeed(channel) - XCTAssertNoThrow( - try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // Write a SETTINGS frame on the root stream. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) - } - - // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. - XCTAssertNoThrow(try readyChannelMux.wait()) - XCTAssertNoThrow(try anotherReadyChannelMux.wait()) - - // Now shutdown. - try self.waitForStateChange(from: .ready, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testShutdownWhileConnecting() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Now shutdown. - let shutdownFuture: EventLoopFuture = self.waitForStateChange( - from: .connecting, - to: .shutdown - ) { - let shutdown = manager.shutdown() - self.loop.run() - return shutdown - } - - // The multiplexer we were requesting should fail. - XCTAssertThrowsError(try readyChannelMux.wait()) - - // We still have our channel promise to fulfil: if it succeeds then it too should be closed. - channelPromise.succeed(EmbeddedChannel(loop: self.loop)) - let channel = try channelPromise.futureResult.wait() - self.loop.run() - XCTAssertNoThrow(try channel.closeFuture.wait()) - XCTAssertNoThrow(try shutdownFuture.wait()) - } - - func testShutdownWhileTransientFailure() throws { - var configuration = self.defaultConfiguration - configuration.connectionBackoff = .oneSecondFixed - - let manager = self.makeConnectionManager(configuration: configuration) { _, _ in - self.loop.makeFailedFuture(DoomedChannelError()) - } - - let readyChannelMux: EventLoopFuture = self.waitForStateChanges([ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ]) { - // Get a HTTP/2 stream multiplexer. - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Now shutdown. - try self.waitForStateChange(from: .transientFailure, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - - // The HTTP/2 stream mux we were requesting should fail. - XCTAssertThrowsError(try readyChannelMux.wait()) - } - - func testShutdownWhileActive() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Prepare the channel - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - channelPromise.succeed(channel) - XCTAssertNoThrow( - try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // (No state change expected here: active is an internal state.) - - // Now shutdown. - try self.waitForStateChange(from: .connecting, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - - // The HTTP/2 stream multiplexer we were requesting should fail. - XCTAssertThrowsError(try readyChannelMux.wait()) - } - - func testShutdownWhileShutdown() throws { - let manager = self.makeConnectionManager() - - try self.waitForStateChange(from: .idle, to: .shutdown) { - let firstShutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try firstShutdown.wait()) - } - - let secondShutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try secondShutdown.wait()) - } - - func testTransientFailureWhileActive() throws { - var configuration = self.defaultConfiguration - configuration.connectionBackoff = .oneSecondFixed - - let channelPromise: EventLoopPromise = self.loop.makePromise() - let channelFutures: [EventLoopFuture] = [ - channelPromise.futureResult, - self.loop.makeFailedFuture(DoomedChannelError()), - ] - var channelFutureIterator = channelFutures.makeIterator() - - let manager = self.makeConnectionManager(configuration: configuration) { _, _ in - guard let next = channelFutureIterator.next() else { - XCTFail("Too many channels requested") - return self.loop.makeFailedFuture(DoomedChannelError()) - } - return next - } - - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Prepare the channel - let firstChannel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: firstChannel, - inboundStreamInitializer: nil - ) - try firstChannel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - - channelPromise.succeed(firstChannel) - XCTAssertNoThrow( - try firstChannel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // (No state change expected here: active is an internal state.) - - // Close the channel (simulate e.g. TLS handshake failed) - try self.waitForStateChange(from: .connecting, to: .transientFailure) { - XCTAssertNoThrow(try firstChannel.close().wait()) - } - - // Start connecting again. - self.waitForStateChanges([ - Change(from: .transientFailure, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ]) { - self.loop.advanceTime(by: .seconds(1)) - } - - // Now shutdown - try self.waitForStateChange(from: .transientFailure, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - - // The channel never came up: it should be throw. - XCTAssertThrowsError(try readyChannelMux.wait()) - } - - func testTransientFailureWhileReady() throws { - var configuration = self.defaultConfiguration - configuration.connectionBackoff = .oneSecondFixed - - let firstChannelPromise: EventLoopPromise = self.loop.makePromise() - let secondChannelPromise: EventLoopPromise = self.loop.makePromise() - let channelFutures: [EventLoopFuture] = [ - firstChannelPromise.futureResult, - secondChannelPromise.futureResult, - ] - var channelFutureIterator = channelFutures.makeIterator() - - let manager = self.makeConnectionManager(configuration: configuration) { _, _ in - guard let next = channelFutureIterator.next() else { - XCTFail("Too many channels requested") - return self.loop.makeFailedFuture(DoomedChannelError()) - } - return next - } - - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Prepare the first channel - let firstChannel = EmbeddedChannel(loop: self.loop) - let firstH2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: firstChannel, - inboundStreamInitializer: nil - ) - try firstChannel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: firstH2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - firstChannelPromise.succeed(firstChannel) - XCTAssertNoThrow( - try firstChannel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // Write a SETTINGS frame on the root stream. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try firstChannel.writeInbound(frame)) - } - - // Channel should now be ready. - XCTAssertNoThrow(try readyChannelMux.wait()) - - // Kill the first channel. But first ensure there's an active RPC, otherwise we'll idle. - let streamCreated = NIOHTTP2StreamCreatedEvent( - streamID: 1, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - firstChannel.pipeline.fireUserInboundEventTriggered(streamCreated) - - try self.waitForStateChange(from: .ready, to: .transientFailure) { - XCTAssertNoThrow(try firstChannel.close().wait()) - } - - // Run to start connecting again. - self.waitForStateChange(from: .transientFailure, to: .connecting) { - self.loop.advanceTime(by: .seconds(1)) - } - - // Prepare the second channel - let secondChannel = EmbeddedChannel(loop: self.loop) - let secondH2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: secondChannel, - inboundStreamInitializer: nil - ) - try secondChannel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: secondH2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - secondChannelPromise.succeed(secondChannel) - XCTAssertNoThrow( - try secondChannel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - // Write a SETTINGS frame on the root stream. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try secondChannel.writeInbound(frame)) - } - - // Now shutdown - try self.waitForStateChange(from: .ready, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testGoAwayWhenReady() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - let readyChannelMux: EventLoopFuture = - self - .waitForStateChange(from: .idle, to: .connecting) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Setup the channel. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - try channel.pipeline.addHandler( - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ).wait() - channelPromise.succeed(channel) - XCTAssertNoThrow( - try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored")) - .wait() - ) - - try self.waitForStateChange(from: .connecting, to: .ready) { - // Write a SETTINGS frame on the root stream. - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) - } - - // Wait for the HTTP/2 stream multiplexer, it _must_ be ready now. - XCTAssertNoThrow(try readyChannelMux.wait()) - - // Send a GO_AWAY; the details don't matter. This will cause the connection to go idle and the - // channel to close. - try self.waitForStateChange(from: .ready, to: .idle) { - let goAway = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: 1, errorCode: .noError, opaqueData: nil) - ) - XCTAssertNoThrow(try channel.writeInbound(goAway)) - self.loop.run() - } - - self.loop.run() - XCTAssertNoThrow(try channel.closeFuture.wait()) - - // Now shutdown - try self.waitForStateChange(from: .idle, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testDoomedOptimisticChannelFromIdle() { - var configuration = self.defaultConfiguration - configuration.callStartBehavior = .fastFailure - let manager = ConnectionManager( - configuration: configuration, - channelProvider: HookedChannelProvider { _, loop in - return loop.makeFailedFuture(DoomedChannelError()) - }, - connectivityDelegate: nil, - idleBehavior: .closeWhenIdleTimeout, - logger: self.logger - ) - let candidate = manager.getHTTP2Multiplexer() - self.loop.run() - XCTAssertThrowsError(try candidate.wait()) - } - - func testDoomedOptimisticChannelFromConnecting() throws { - var configuration = self.defaultConfiguration - configuration.callStartBehavior = .fastFailure - let promise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return promise.futureResult - } - - self.waitForStateChange(from: .idle, to: .connecting) { - // Trigger channel creation, and a connection attempt, we don't care about the HTTP/2 stream multiplexer. - _ = manager.getHTTP2Multiplexer() - self.loop.run() - } - - // We're connecting: get an optimistic HTTP/2 stream multiplexer - this was selected in config. - let optimisticChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - - // Fail the promise. - promise.fail(DoomedChannelError()) - - XCTAssertThrowsError(try optimisticChannelMux.wait()) - } - - func testOptimisticChannelFromTransientFailure() throws { - var configuration = self.defaultConfiguration - configuration.callStartBehavior = .fastFailure - configuration.connectionBackoff = ConnectionBackoff() - - let manager = self.makeConnectionManager(configuration: configuration) { _, _ in - self.loop.makeFailedFuture(DoomedChannelError()) - } - defer { - try! manager.shutdown().wait() - } - - self.waitForStateChanges([ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .transientFailure), - ]) { - // Trigger channel creation, and a connection attempt, we don't care about the HTTP/2 stream multiplexer. - _ = manager.getHTTP2Multiplexer() - self.loop.run() - } - - // Now we're sitting in transient failure. Get a HTTP/2 stream mux optimistically - selected in config. - let optimisticChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - - XCTAssertThrowsError(try optimisticChannelMux.wait()) { error in - XCTAssertTrue(error is DoomedChannelError) - } - } - - func testOptimisticChannelFromShutdown() throws { - var configuration = self.defaultConfiguration - configuration.callStartBehavior = .fastFailure - let manager = self.makeConnectionManager { _, _ in - return self.loop.makeFailedFuture(DoomedChannelError()) - } - - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - - // Get a channel optimistically. It'll fail, obviously. - let channelMux = manager.getHTTP2Multiplexer() - self.loop.run() - XCTAssertThrowsError(try channelMux.wait()) - } - - func testForceIdleAfterInactive() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - // Start the connection. - let readyChannelMux: EventLoopFuture = self.waitForStateChange( - from: .idle, - to: .connecting - ) { - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Setup the real channel and activate it. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - XCTAssertNoThrow( - try channel.pipeline.addHandlers([ - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ]).wait() - ) - channelPromise.succeed(channel) - self.loop.run() - - let connect = channel.connect(to: try SocketAddress(unixDomainSocketPath: "/ignored")) - XCTAssertNoThrow(try connect.wait()) - - // Write a SETTINGS frame on the root stream. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) - } - - // The channel should now be ready. - XCTAssertNoThrow(try readyChannelMux.wait()) - - // Now drop the connection. - try self.waitForStateChange(from: .ready, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testCloseWithoutActiveRPCs() throws { - let channelPromise = self.loop.makePromise(of: Channel.self) - let manager = self.makeConnectionManager { _, _ in - return channelPromise.futureResult - } - - // Start the connection. - let readyChannelMux = self.waitForStateChange( - from: .idle, - to: .connecting - ) { () -> EventLoopFuture in - let readyChannelMux = manager.getHTTP2Multiplexer() - self.loop.run() - return readyChannelMux - } - - // Setup the actual channel and activate it. - let channel = EmbeddedChannel(loop: self.loop) - let h2mux = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - XCTAssertNoThrow( - try channel.pipeline.addHandlers([ - GRPCIdleHandler( - connectionManager: manager, - multiplexer: h2mux, - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.logger - ) - ]).wait() - ) - channelPromise.succeed(channel) - self.loop.run() - - let connect = channel.connect(to: try SocketAddress(unixDomainSocketPath: "/ignored")) - XCTAssertNoThrow(try connect.wait()) - - // "ready" the connection. - try self.waitForStateChange(from: .connecting, to: .ready) { - let frame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings([]))) - XCTAssertNoThrow(try channel.writeInbound(frame)) - } - - // The HTTP/2 stream multiplexer should now be ready. - XCTAssertNoThrow(try readyChannelMux.wait()) - - // Close the channel. There are no active RPCs so we should idle rather than be in the transient - // failure state. - self.waitForStateChange(from: .ready, to: .idle) { - channel.pipeline.fireChannelInactive() - } - } - - func testIdleErrorDoesNothing() throws { - let manager = self.makeConnectionManager() - - // Dropping an error on this manager should be fine. - manager.channelError(DoomedChannelError()) - - // Shutting down is then safe. - try self.waitForStateChange(from: .idle, to: .shutdown) { - let shutdown = manager.shutdown() - self.loop.run() - XCTAssertNoThrow(try shutdown.wait()) - } - } - - func testHTTP2Delegates() throws { - let channel = EmbeddedChannel(loop: self.loop) - // The channel gets shut down by the connection manager. - - let multiplexer = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - - class HTTP2Delegate: ConnectionManagerHTTP2Delegate { - var streamsOpened = 0 - var streamsClosed = 0 - var maxConcurrentStreams = 0 - - func streamOpened(_ connectionManager: ConnectionManager) { - self.streamsOpened += 1 - } - - func streamClosed(_ connectionManager: ConnectionManager) { - self.streamsClosed += 1 - } - - func receivedSettingsMaxConcurrentStreams( - _ connectionManager: ConnectionManager, - maxConcurrentStreams: Int - ) { - self.maxConcurrentStreams = maxConcurrentStreams - } - } - - let http2 = HTTP2Delegate() - - let manager = ConnectionManager( - eventLoop: self.loop, - channelProvider: HookedChannelProvider { manager, eventLoop -> EventLoopFuture in - let idleHandler = GRPCIdleHandler( - connectionManager: manager, - multiplexer: multiplexer, - idleTimeout: .minutes(5), - keepalive: ClientConnectionKeepalive(), - logger: self.logger - ) - - // We're going to cheat a bit by not putting the multiplexer in the channel. This allows - // us to just fire stream created/closed events into the channel. - do { - try channel.pipeline.syncOperations.addHandler(idleHandler) - } catch { - return eventLoop.makeFailedFuture(error) - } - - return eventLoop.makeSucceededFuture(channel) - }, - callStartBehavior: .waitsForConnectivity, - idleBehavior: .closeWhenIdleTimeout, - connectionBackoff: ConnectionBackoff(), - connectivityDelegate: nil, - http2Delegate: http2, - logger: self.logger - ) - defer { - let future = manager.shutdown() - self.loop.run() - try! future.wait() - } - - // Start connecting. - let futureMultiplexer = manager.getHTTP2Multiplexer() - self.loop.run() - - // Do the actual connecting. - XCTAssertNoThrow(try channel.connect(to: SocketAddress(unixDomainSocketPath: "/ignored"))) - - // The channel isn't ready until it's seen a SETTINGS frame. - - func makeSettingsFrame(maxConcurrentStreams: Int) -> HTTP2Frame { - let settings = [HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams)] - return HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) - } - XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 42))) - - // We're ready now so the future multiplexer will resolve and we'll have seen an update to - // max concurrent streams. - XCTAssertNoThrow(try futureMultiplexer.wait()) - XCTAssertEqual(http2.maxConcurrentStreams, 42) - - XCTAssertNoThrow(try channel.writeInbound(makeSettingsFrame(maxConcurrentStreams: 13))) - XCTAssertEqual(http2.maxConcurrentStreams, 13) - - // Open some streams. - for streamID in stride(from: HTTP2StreamID(1), to: HTTP2StreamID(9), by: 2) { - let streamCreated = NIOHTTP2StreamCreatedEvent( - streamID: streamID, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - channel.pipeline.fireUserInboundEventTriggered(streamCreated) - } - - // ... and then close them. - for streamID in stride(from: HTTP2StreamID(1), to: HTTP2StreamID(9), by: 2) { - let streamClosed = StreamClosedEvent(streamID: streamID, reason: nil) - channel.pipeline.fireUserInboundEventTriggered(streamClosed) - } - - XCTAssertEqual(http2.streamsOpened, 4) - 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 = self.waitForStateChange( - from: .idle, - to: .connecting - ) { - let channel = manager.getHTTP2Multiplexer() - self.loop.run() - return channel - } - - self.waitForStateChange(from: .connecting, to: .shutdown) { - channelPromise.fail(EventLoopError.shutdown) - } - - XCTAssertThrowsError(try multiplexer.wait()) - } - - func testChannelErrorAndConnectFailWhenConnecting() throws { - // This test checks a path through the connection manager which previously led to an invalid - // state (a connect failure in a state other than connecting). To trigger these we need to - // fire an error down the pipeline containing the idle handler and fail the connect promise. - let escapedChannelPromise = self.loop.makePromise(of: Channel.self) - let channelPromise = self.loop.makePromise(of: Channel.self) - - var configuration = self.defaultConfiguration - configuration.connectionBackoff = ConnectionBackoff() - let manager = self.makeConnectionManager( - configuration: configuration - ) { connectionManager, loop in - let channel = EmbeddedChannel(loop: loop as! EmbeddedEventLoop) - let multiplexer = HTTP2StreamMultiplexer(mode: .client, channel: channel) { - $0.eventLoop.makeSucceededVoidFuture() - } - - let idleHandler = GRPCIdleHandler( - connectionManager: connectionManager, - multiplexer: multiplexer, - idleTimeout: .minutes(60), - keepalive: .init(), - logger: self.clientLogger - ) - - channel.pipeline.addHandler(idleHandler).whenSuccess { - escapedChannelPromise.succeed(channel) - } - - return channelPromise.futureResult - } - - // Ask for the multiplexer to trigger channel creation. - self.waitForStateChange(from: .idle, to: .connecting) { - _ = manager.getHTTP2Multiplexer() - self.loop.run() - } - - // Fire an error down the pipeline. - let channel = try escapedChannelPromise.futureResult.wait() - channel.pipeline.fireErrorCaught(GRPCStatus(code: .unavailable)) - - // Fail the channel promise. - channelPromise.fail(GRPCStatus(code: .unavailable)) - } - - func testClientKeepaliveJitterWithoutClamping() { - let original = ClientConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) - let keepalive = original.jitteringInterval(byAtMost: .milliseconds(500)) - - XCTAssertGreaterThanOrEqual(keepalive.interval, .milliseconds(1500)) - XCTAssertLessThanOrEqual(keepalive.interval, .milliseconds(2500)) - } - - func testClientKeepaliveJitterClampedToTimeout() { - let original = ClientConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) - let keepalive = original.jitteringInterval(byAtMost: .seconds(2)) - - // Strictly greater than the timeout of 1 seconds. - XCTAssertGreaterThan(keepalive.interval, .seconds(1)) - XCTAssertLessThanOrEqual(keepalive.interval, .seconds(4)) - } - - func testServerKeepaliveJitterWithoutClamping() { - let original = ServerConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) - let keepalive = original.jitteringInterval(byAtMost: .milliseconds(500)) - - XCTAssertGreaterThanOrEqual(keepalive.interval, .milliseconds(1500)) - XCTAssertLessThanOrEqual(keepalive.interval, .milliseconds(2500)) - } - - func testServerKeepaliveJitterClampedToTimeout() { - let original = ServerConnectionKeepalive(interval: .seconds(2), timeout: .seconds(1)) - let keepalive = original.jitteringInterval(byAtMost: .seconds(2)) - - // Strictly greater than the timeout of 1 seconds. - XCTAssertGreaterThan(keepalive.interval, .seconds(1)) - XCTAssertLessThanOrEqual(keepalive.interval, .seconds(4)) - } - - func testConnectTimeoutIsRespectedWithNoRetries() { - // Setup a factory which makes channels. We'll use this as the point to check that the - // connect timeout is as expected. - struct Provider: ConnectionManagerChannelProvider { - func makeChannel( - managedBy connectionManager: ConnectionManager, - onEventLoop eventLoop: any EventLoop, - connectTimeout: TimeAmount?, - logger: Logger - ) -> EventLoopFuture { - XCTAssertEqual(connectTimeout, .seconds(314_159_265)) - return eventLoop.makeFailedFuture(DoomedChannelError()) - } - } - - var configuration = self.defaultConfiguration - configuration.connectionBackoff = ConnectionBackoff( - minimumConnectionTimeout: 314_159_265, - retries: .none - ) - - let manager = ConnectionManager( - configuration: configuration, - channelProvider: Provider(), - connectivityDelegate: self.monitor, - idleBehavior: .closeWhenIdleTimeout, - logger: self.logger - ) - - // Setup the state change expectations and trigger them by asking for the multiplexer. - // We expect connecting to shutdown as no connect retries are configured and the factory - // always returns errors. - let multiplexer = self.waitForStateChanges([ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ]) { - let multiplexer = manager.getHTTP2Multiplexer() - self.loop.run() - return multiplexer - } - - XCTAssertThrowsError(try multiplexer.wait()) { error in - XCTAssert(error is DoomedChannelError) - } - } -} - -internal struct Change: Hashable, CustomStringConvertible { - var from: ConnectivityState - var to: ConnectivityState - - var description: String { - return "\(self.from) → \(self.to)" - } -} - -// Unchecked as all mutable state is modified from a serial queue. -extension RecordingConnectivityDelegate: @unchecked Sendable {} - -internal class RecordingConnectivityDelegate: ConnectivityStateDelegate { - private let serialQueue = DispatchQueue(label: "io.grpc.testing") - private let semaphore = DispatchSemaphore(value: 0) - private var expectation: Expectation = .noExpectation - - private let quiescingSemaphore = DispatchSemaphore(value: 0) - - private enum Expectation { - /// We have no expectation of any changes. We'll just ignore any changes. - case noExpectation - - /// We expect one change. - case one((Change) -> Void) - - /// We expect 'count' changes. - case some(count: Int, recorded: [Change], ([Change]) -> Void) - - var count: Int { - switch self { - case .noExpectation: - return 0 - case .one: - return 1 - case let .some(count, _, _): - return count - } - } - } - - func connectivityStateDidChange( - from oldState: ConnectivityState, - to newState: ConnectivityState - ) { - self.serialQueue.async { - switch self.expectation { - case let .one(verify): - // We don't care about future changes. - self.expectation = .noExpectation - - // Verify and notify. - verify(Change(from: oldState, to: newState)) - self.semaphore.signal() - - case .some(let count, var recorded, let verify): - recorded.append(Change(from: oldState, to: newState)) - if recorded.count == count { - // We don't care about future changes. - self.expectation = .noExpectation - - // Verify and notify. - verify(recorded) - self.semaphore.signal() - } else { - // Still need more responses. - self.expectation = .some(count: count, recorded: recorded, verify) - } - - case .noExpectation: - // Ignore any changes. - () - } - } - } - - func connectionStartedQuiescing() { - self.serialQueue.async { - self.quiescingSemaphore.signal() - } - } - - func expectChanges(_ count: Int, verify: @escaping ([Change]) -> Void) { - self.serialQueue.async { - self.expectation = .some(count: count, recorded: [], verify) - } - } - - func expectChange(verify: @escaping (Change) -> Void) { - self.serialQueue.async { - self.expectation = .one(verify) - } - } - - func waitForExpectedChanges( - timeout: DispatchTimeInterval, - file: StaticString = #filePath, - line: UInt = #line - ) { - let result = self.semaphore.wait(timeout: .now() + timeout) - switch result { - case .success: - () - case .timedOut: - XCTFail( - "Timed out before verifying \(self.expectation.count) change(s)", - file: file, - line: line - ) - } - } - - func waitForQuiescing(timeout: DispatchTimeInterval) { - let result = self.quiescingSemaphore.wait(timeout: .now() + timeout) - switch result { - case .success: - () - case .timedOut: - XCTFail("Timed out waiting for connection to start quiescing") - } - } -} - -extension ConnectionBackoff { - fileprivate static let oneSecondFixed = ConnectionBackoff( - initialBackoff: 1.0, - maximumBackoff: 1.0, - multiplier: 1.0, - jitter: 0.0 - ) -} - -private struct DoomedChannelError: Error {} - -internal struct HookedChannelProvider: ConnectionManagerChannelProvider { - internal var provider: (ConnectionManager, EventLoop) -> EventLoopFuture - - init(_ provider: @escaping (ConnectionManager, EventLoop) -> EventLoopFuture) { - self.provider = provider - } - - func makeChannel( - managedBy connectionManager: ConnectionManager, - onEventLoop eventLoop: EventLoop, - connectTimeout: TimeAmount?, - logger: Logger - ) -> EventLoopFuture { - return self.provider(connectionManager, eventLoop) - } -} - -extension ConnectionManager { - // For backwards compatibility, to avoid large diffs in these tests. - fileprivate func shutdown() -> EventLoopFuture { - return self.shutdown(mode: .forceful) - } -} diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift deleted file mode 100644 index cb0a677cd..000000000 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolDelegates.swift +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2022, 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 GRPC -import NIOConcurrencyHelpers -import NIOCore - -final class IsConnectingDelegate: GRPCConnectionPoolDelegate { - private let lock = NIOLock() - private var connecting = Set() - private var active = Set() - - enum StateNotifacation: Hashable, Sendable { - case connecting - case connected - } - - private let onStateChange: @Sendable (StateNotifacation) -> Void - - init(onStateChange: @escaping @Sendable (StateNotifacation) -> Void) { - self.onStateChange = onStateChange - } - - func startedConnecting(id: GRPCConnectionID) { - let didStartConnecting: Bool = self.lock.withLock { - let (inserted, _) = self.connecting.insert(id) - // Only intereseted new connection attempts when there are no active connections. - return inserted && self.connecting.count == 1 && self.active.isEmpty - } - - if didStartConnecting { - self.onStateChange(.connecting) - } - } - - func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) { - let didStopConnecting: Bool = self.lock.withLock { - let removed = self.connecting.remove(id) != nil - let (inserted, _) = self.active.insert(id) - return removed && inserted && self.active.count == 1 - } - - if didStopConnecting { - self.onStateChange(.connected) - } - } - - func connectionClosed(id: GRPCConnectionID, error: Error?) { - self.lock.withLock { - self.active.remove(id) - self.connecting.remove(id) - } - } - - func connectionQuiescing(id: GRPCConnectionID) { - self.lock.withLock { - _ = self.active.remove(id) - } - } - - // No-op. - func connectionAdded(id: GRPCConnectionID) {} - - // No-op. - func connectionRemoved(id: GRPCConnectionID) {} - - // Conection failures put the connection into a backing off state, we consider that to still - // be 'connecting' at this point. - func connectFailed(id: GRPCConnectionID, error: Error) {} - - // No-op. - func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) {} -} - -extension IsConnectingDelegate: @unchecked Sendable {} - -final class EventRecordingConnectionPoolDelegate: GRPCConnectionPoolDelegate { - struct UnexpectedEvent: Error { - var event: Event - - init(_ event: Event) { - self.event = event - } - } - - enum Event: Equatable { - case connectionAdded(GRPCConnectionID) - case startedConnecting(GRPCConnectionID) - case connectFailed(GRPCConnectionID) - case connectSucceeded(GRPCConnectionID, Int) - case connectionClosed(GRPCConnectionID) - case connectionUtilizationChanged(GRPCConnectionID, Int, Int) - case connectionQuiescing(GRPCConnectionID) - case connectionRemoved(GRPCConnectionID) - case stats([GRPCSubPoolStats], GRPCConnectionPoolID) - - var id: GRPCConnectionID? { - switch self { - case let .connectionAdded(id), - let .startedConnecting(id), - let .connectFailed(id), - let .connectSucceeded(id, _), - let .connectionClosed(id), - let .connectionUtilizationChanged(id, _, _), - let .connectionQuiescing(id), - let .connectionRemoved(id): - return id - case .stats: - return nil - } - } - } - - private var events: CircularBuffer = [] - private let lock = NIOLock() - - var first: Event? { - return self.lock.withLock { - self.events.first - } - } - - var isEmpty: Bool { - return self.lock.withLock { self.events.isEmpty } - } - - func popFirst() -> Event? { - return self.lock.withLock { - self.events.popFirst() - } - } - - func removeAll() -> CircularBuffer { - return self.lock.withLock { - defer { self.events.removeAll() } - return self.events - } - } - - func connectionAdded(id: GRPCConnectionID) { - self.lock.withLock { - self.events.append(.connectionAdded(id)) - } - } - - func startedConnecting(id: GRPCConnectionID) { - self.lock.withLock { - self.events.append(.startedConnecting(id)) - } - } - - func connectFailed(id: GRPCConnectionID, error: Error) { - self.lock.withLock { - self.events.append(.connectFailed(id)) - } - } - - func connectSucceeded(id: GRPCConnectionID, streamCapacity: Int) { - self.lock.withLock { - self.events.append(.connectSucceeded(id, streamCapacity)) - } - } - - func connectionClosed(id: GRPCConnectionID, error: Error?) { - self.lock.withLock { - self.events.append(.connectionClosed(id)) - } - } - - func connectionUtilizationChanged(id: GRPCConnectionID, streamsUsed: Int, streamCapacity: Int) { - self.lock.withLock { - self.events.append(.connectionUtilizationChanged(id, streamsUsed, streamCapacity)) - } - } - - func connectionQuiescing(id: GRPCConnectionID) { - self.lock.withLock { - self.events.append(.connectionQuiescing(id)) - } - } - - func connectionRemoved(id: GRPCConnectionID) { - self.lock.withLock { - self.events.append(.connectionRemoved(id)) - } - } - - func connectionPoolStats(_ stats: [GRPCSubPoolStats], id: GRPCConnectionPoolID) { - self.lock.withLock { - self.events.append(.stats(stats, id)) - } - } -} - -extension EventRecordingConnectionPoolDelegate: @unchecked Sendable {} diff --git a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift b/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift deleted file mode 100644 index d25489630..000000000 --- a/Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift +++ /dev/null @@ -1,1338 +0,0 @@ -/* - * 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 Logging -import NIOCore -import NIOEmbedded -import NIOHTTP2 -import XCTest - -@testable import GRPC - -final class ConnectionPoolTests: GRPCTestCase { - private enum TestError: Error { - case noChannelExpected - } - - private var eventLoop: EmbeddedEventLoop! - private var tearDownBlocks: [() throws -> Void] = [] - - override func setUp() { - super.setUp() - self.eventLoop = EmbeddedEventLoop() - } - - override func tearDown() { - XCTAssertNoThrow(try self.eventLoop.close()) - self.tearDownBlocks.forEach { try? $0() } - super.tearDown() - } - - private func noChannelExpected( - _: ConnectionManager, - _ eventLoop: EventLoop, - line: UInt = #line - ) -> EventLoopFuture { - XCTFail("Channel unexpectedly created", line: line) - return eventLoop.makeFailedFuture(TestError.noChannelExpected) - } - - private func makePool( - waiters: Int = 1000, - reservationLoadThreshold: Double = 0.9, - minConnections: Int = 0, - assumedMaxConcurrentStreams: Int = 100, - now: @escaping () -> NIODeadline = { .now() }, - connectionBackoff: ConnectionBackoff = ConnectionBackoff(), - delegate: GRPCConnectionPoolDelegate? = nil, - onReservationReturned: @escaping (Int) -> Void = { _ in }, - onMaximumReservationsChange: @escaping (Int) -> Void = { _ in }, - channelProvider: ConnectionManagerChannelProvider - ) -> ConnectionPool { - return ConnectionPool( - eventLoop: self.eventLoop, - maxWaiters: waiters, - minConnections: minConnections, - reservationLoadThreshold: reservationLoadThreshold, - assumedMaxConcurrentStreams: assumedMaxConcurrentStreams, - connectionBackoff: connectionBackoff, - channelProvider: channelProvider, - streamLender: HookedStreamLender( - onReturnStreams: onReservationReturned, - onUpdateMaxAvailableStreams: onMaximumReservationsChange - ), - delegate: delegate, - logger: self.logger, - now: now - ) - } - - private func makePool( - waiters: Int = 1000, - delegate: GRPCConnectionPoolDelegate? = nil, - makeChannel: @escaping (ConnectionManager, EventLoop) -> EventLoopFuture - ) -> ConnectionPool { - return self.makePool( - waiters: waiters, - delegate: delegate, - channelProvider: HookedChannelProvider(makeChannel) - ) - } - - private func setUpPoolAndController( - waiters: Int = 1000, - reservationLoadThreshold: Double = 0.9, - now: @escaping () -> NIODeadline = { .now() }, - connectionBackoff: ConnectionBackoff = ConnectionBackoff(), - delegate: GRPCConnectionPoolDelegate? = nil, - onReservationReturned: @escaping (Int) -> Void = { _ in }, - onMaximumReservationsChange: @escaping (Int) -> Void = { _ in } - ) -> (ConnectionPool, ChannelController) { - let controller = ChannelController() - let pool = self.makePool( - waiters: waiters, - reservationLoadThreshold: reservationLoadThreshold, - now: now, - connectionBackoff: connectionBackoff, - delegate: delegate, - onReservationReturned: onReservationReturned, - onMaximumReservationsChange: onMaximumReservationsChange, - channelProvider: controller - ) - - self.tearDownBlocks.append { - let shutdown = pool.shutdown() - self.eventLoop.run() - XCTAssertNoThrow(try shutdown.wait()) - controller.finish() - } - - return (pool, controller) - } - - func testEmptyConnectionPool() { - let pool = self.makePool { - self.noChannelExpected($0, $1) - } - XCTAssertEqual(pool.sync.connections, 0) - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - - pool.initialize(connections: 20) - XCTAssertEqual(pool.sync.connections, 20) - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - - let shutdownFuture = pool.shutdown() - self.eventLoop.run() - XCTAssertNoThrow(try shutdownFuture.wait()) - } - - func testShutdownEmptyPool() { - let pool = self.makePool { - self.noChannelExpected($0, $1) - } - XCTAssertNoThrow(try pool.shutdown().wait()) - // Shutting down twice should also be fine. - XCTAssertNoThrow(try pool.shutdown().wait()) - } - - func testMakeStreamWhenShutdown() { - let pool = self.makePool { - self.noChannelExpected($0, $1) - } - XCTAssertNoThrow(try pool.shutdown().wait()) - - let stream = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - XCTAssertThrowsError(try stream.wait()) { error in - XCTAssert((error as? GRPCConnectionPoolError).isShutdown) - } - } - - func testMakeStreamWhenWaiterQueueIsFull() { - let maxWaiters = 5 - let pool = self.makePool(waiters: maxWaiters) { - self.noChannelExpected($0, $1) - } - - let waiting = (0 ..< maxWaiters).map { _ in - return pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - } - - let tooManyWaiters = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - XCTAssertThrowsError(try tooManyWaiters.wait()) { error in - XCTAssert((error as? GRPCConnectionPoolError).isTooManyWaiters) - } - - XCTAssertNoThrow(try pool.shutdown().wait()) - // All 'waiting' futures will be failed by the shutdown promise. - for waiter in waiting { - XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? GRPCConnectionPoolError).isShutdown) - } - } - } - - func testWaiterTimingOut() { - let pool = self.makePool { - self.noChannelExpected($0, $1) - } - - let waiter = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - XCTAssertEqual(pool.sync.waiters, 1) - - self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) - XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded) - } - - XCTAssertEqual(pool.sync.waiters, 0) - } - - func testWaiterTimingOutInPast() { - let pool = self.makePool { - self.noChannelExpected($0, $1) - } - - self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) - - let waiter = pool.makeStream(deadline: .uptimeNanoseconds(5), logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - XCTAssertEqual(pool.sync.waiters, 1) - - self.eventLoop.run() - XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded) - } - - XCTAssertEqual(pool.sync.waiters, 0) - } - - func testMakeStreamTriggersChannelCreation() { - let (pool, controller) = self.setUpPoolAndController() - - pool.initialize(connections: 1) - XCTAssertEqual(pool.sync.connections, 1) - // No channels yet. - XCTAssertEqual(controller.count, 0) - - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Start creating the channel. - self.eventLoop.run() - - // We should have been asked for a channel now. - XCTAssertEqual(controller.count, 1) - // The connection isn't ready yet though, so no streams available. - XCTAssertEqual(pool.sync.availableStreams, 0) - - // Make the connection 'ready'. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) - - // We have a multiplexer and a 'ready' connection. - XCTAssertEqual(pool.sync.reservedStreams, 1) - XCTAssertEqual(pool.sync.availableStreams, 9) - XCTAssertEqual(pool.sync.waiters, 0) - - // Run the loop to create the stream, we need to fire the event too. - self.eventLoop.run() - XCTAssertNoThrow(try waiter.wait()) - controller.openStreamInChannel(atIndex: 0) - - // Now close the stream. - controller.closeStreamInChannel(atIndex: 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - XCTAssertEqual(pool.sync.availableStreams, 10) - } - - func testMakeStreamWhenConnectionIsAlreadyAvailable() { - let (pool, controller) = self.setUpPoolAndController() - pool.initialize(connections: 1) - - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Start creating the channel. - self.eventLoop.run() - XCTAssertEqual(controller.count, 1) - - // Fire up the connection. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) - - // Run the loop to create the stream, we need to fire the stream creation event too. - self.eventLoop.run() - XCTAssertNoThrow(try waiter.wait()) - controller.openStreamInChannel(atIndex: 0) - - // Now we can create another stream, but as there's already an available stream on an active - // connection we won't have to wait. - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.reservedStreams, 1) - let notWaiting = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Still no waiters. - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.reservedStreams, 2) - - // Run the loop to create the stream, we need to fire the stream creation event too. - self.eventLoop.run() - XCTAssertNoThrow(try notWaiting.wait()) - controller.openStreamInChannel(atIndex: 0) - } - - func testMakeMoreWaitersThanConnectionCanHandle() { - var returnedStreams: [Int] = [] - let (pool, controller) = self.setUpPoolAndController(onReservationReturned: { - returnedStreams.append($0) - }) - pool.initialize(connections: 1) - - // Enqueue twice as many waiters as the connection will be able to handle. - let maxConcurrentStreams = 10 - let waiters = (0 ..< maxConcurrentStreams * 2).map { _ in - return pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - } - - XCTAssertEqual(pool.sync.waiters, 2 * maxConcurrentStreams) - - // Fire up the connection. - self.eventLoop.run() - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: maxConcurrentStreams) - - // We should have assigned a bunch of streams to waiters now. - XCTAssertEqual(pool.sync.waiters, maxConcurrentStreams) - XCTAssertEqual(pool.sync.reservedStreams, maxConcurrentStreams) - XCTAssertEqual(pool.sync.availableStreams, 0) - - // Do the stream creation and make sure the first batch are succeeded. - self.eventLoop.run() - let firstBatch = waiters.prefix(maxConcurrentStreams) - var others = waiters.dropFirst(maxConcurrentStreams) - - for waiter in firstBatch { - XCTAssertNoThrow(try waiter.wait()) - controller.openStreamInChannel(atIndex: 0) - } - - // Close a stream. - controller.closeStreamInChannel(atIndex: 0) - XCTAssertEqual(returnedStreams, [1]) - // We have another stream so a waiter should be succeeded. - XCTAssertEqual(pool.sync.waiters, maxConcurrentStreams - 1) - self.eventLoop.run() - XCTAssertNoThrow(try others.popFirst()?.wait()) - - // Shutdown the pool: the remaining waiters should be failed. - let shutdown = pool.shutdown() - self.eventLoop.run() - XCTAssertNoThrow(try shutdown.wait()) - for waiter in others { - XCTAssertThrowsError(try waiter.wait()) { error in - XCTAssert((error as? GRPCConnectionPoolError).isShutdown) - } - } - } - - func testDropConnectionWithOutstandingReservations() { - var streamsReturned: [Int] = [] - let (pool, controller) = self.setUpPoolAndController( - onReservationReturned: { streamsReturned.append($0) } - ) - pool.initialize(connections: 1) - - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Start creating the channel. - self.eventLoop.run() - XCTAssertEqual(controller.count, 1) - - // Fire up the connection. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) - - // Run the loop to create the stream, we need to fire the stream creation event too. - self.eventLoop.run() - XCTAssertNoThrow(try waiter.wait()) - controller.openStreamInChannel(atIndex: 0) - - // Create a handful of streams. - XCTAssertEqual(pool.sync.availableStreams, 9) - for _ in 0 ..< 5 { - let notWaiting = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - self.eventLoop.run() - XCTAssertNoThrow(try notWaiting.wait()) - controller.openStreamInChannel(atIndex: 0) - } - - XCTAssertEqual(pool.sync.availableStreams, 4) - XCTAssertEqual(pool.sync.reservedStreams, 6) - - // Blast the connection away. We'll be notified about dropped reservations. - XCTAssertEqual(streamsReturned, []) - controller.throwError(ChannelError.ioOnClosedChannel, inChannelAtIndex: 0) - controller.fireChannelInactiveForChannel(atIndex: 0) - XCTAssertEqual(streamsReturned, [6]) - - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - } - - func testDropConnectionWithOutstandingReservationsAndWaiters() { - var streamsReturned: [Int] = [] - let (pool, controller) = self.setUpPoolAndController( - onReservationReturned: { streamsReturned.append($0) } - ) - pool.initialize(connections: 1) - - // Reserve a bunch of streams. - let waiters = (0 ..< 10).map { _ in - return pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - } - - // Connect and setup all the streams. - self.eventLoop.run() - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) - self.eventLoop.run() - for waiter in waiters { - XCTAssertNoThrow(try waiter.wait()) - controller.openStreamInChannel(atIndex: 0) - } - - // All streams should be reserved. - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 10) - - // Add a waiter. - XCTAssertEqual(pool.sync.waiters, 0) - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - XCTAssertEqual(pool.sync.waiters, 1) - - // Now bork the connection. We'll be notified about the 10 dropped reservation but not the one - // waiter . - XCTAssertEqual(streamsReturned, []) - controller.throwError(ChannelError.ioOnClosedChannel, inChannelAtIndex: 0) - controller.fireChannelInactiveForChannel(atIndex: 0) - XCTAssertEqual(streamsReturned, [10]) - - // The connection dropped, let the reconnect kick in. - self.eventLoop.run() - XCTAssertEqual(controller.count, 2) - - controller.connectChannel(atIndex: 1) - controller.sendSettingsToChannel(atIndex: 1, maxConcurrentStreams: 10) - self.eventLoop.run() - XCTAssertNoThrow(try waiter.wait()) - controller.openStreamInChannel(atIndex: 1) - controller.closeStreamInChannel(atIndex: 1) - XCTAssertEqual(streamsReturned, [10, 1]) - - XCTAssertEqual(pool.sync.availableStreams, 10) - XCTAssertEqual(pool.sync.reservedStreams, 0) - } - - func testDeadlineExceededInSameTickAsSucceedingWaiters() { - // deadline must be exceeded just as servicing waiter is done - - // - setup waiter with deadline x - // - start connecting - // - set time to x - // - finish connecting - - let (pool, controller) = self.setUpPoolAndController(now: { - return NIODeadline.uptimeNanoseconds(12) - }) - pool.initialize(connections: 1) - - let waiter1 = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - let waiter2 = pool.makeStream(deadline: .uptimeNanoseconds(15), logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - // Start creating the channel. - self.eventLoop.run() - XCTAssertEqual(controller.count, 1) - - // Fire up the connection. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) - - // The deadline for the first waiter is already after 'now', so it'll fail with deadline - // exceeded. - self.eventLoop.run() - // We need to advance the time to fire the timeout to fail the waiter. - self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) - XCTAssertThrowsError(try waiter1.wait()) { error in - XCTAssert((error as? GRPCConnectionPoolError).isDeadlineExceeded) - } - - self.eventLoop.run() - XCTAssertNoThrow(try waiter2.wait()) - controller.openStreamInChannel(atIndex: 0) - - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.reservedStreams, 1) - XCTAssertEqual(pool.sync.availableStreams, 9) - - controller.closeStreamInChannel(atIndex: 0) - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - XCTAssertEqual(pool.sync.availableStreams, 10) - } - - func testConnectionsAreBroughtUpAtAppropriateTimes() { - let (pool, controller) = self.setUpPoolAndController(reservationLoadThreshold: 0.2) - // We'll allow 3 connections and configure max concurrent streams to 10. With our reservation - // threshold we'll bring up a new connection after enqueueing the 1st, 2nd and 4th waiters. - pool.initialize(connections: 3) - let maxConcurrentStreams = 10 - - // No demand so all three connections are idle. - XCTAssertEqual(pool.sync.idleConnections, 3) - - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - // demand=1, available=0, load=infinite, one connection should be non-idle - XCTAssertEqual(pool.sync.idleConnections, 2) - - // Connect the first channel and write the first settings frame; this allows us to lower the - // default max concurrent streams value (from 100). - self.eventLoop.run() - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: maxConcurrentStreams) - - self.eventLoop.run() - XCTAssertNoThrow(try w1.wait()) - controller.openStreamInChannel(atIndex: 0) - - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - self.eventLoop.run() - XCTAssertNoThrow(try w2.wait()) - controller.openStreamInChannel(atIndex: 0) - - // demand=2, available=10, load=0.2; only one idle connection now. - XCTAssertEqual(pool.sync.idleConnections, 1) - - // Add more demand before the second connection comes up. - let w3 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - // demand=3, available=20, load=0.15; still one idle connection. - XCTAssertEqual(pool.sync.idleConnections, 1) - - // Connection the next channel - self.eventLoop.run() - controller.connectChannel(atIndex: 1) - controller.sendSettingsToChannel(atIndex: 1, maxConcurrentStreams: maxConcurrentStreams) - - XCTAssertNoThrow(try w3.wait()) - controller.openStreamInChannel(atIndex: 1) - } - - func testQuiescingConnectionIsReplaced() { - var reservationsReturned: [Int] = [] - let (pool, controller) = self.setUpPoolAndController(onReservationReturned: { - reservationsReturned.append($0) - }) - pool.initialize(connections: 1) - XCTAssertEqual(pool.sync.connections, 1) - - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Start creating the channel. - self.eventLoop.run() - - // Make the connection 'ready'. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0) - - // Run the loop to create the stream. - self.eventLoop.run() - XCTAssertNoThrow(try w1.wait()) - controller.openStreamInChannel(atIndex: 0) - - // One stream reserved by 'w1' on the only connection in the pool (which isn't idle). - XCTAssertEqual(pool.sync.reservedStreams, 1) - XCTAssertEqual(pool.sync.connections, 1) - XCTAssertEqual(pool.sync.idleConnections, 0) - - // Quiesce the connection. It should be punted from the pool and any active RPCs allowed to run - // their course. A new (idle) connection should replace it in the pool. - controller.sendGoAwayToChannel(atIndex: 0) - - // The quiescing connection had 1 stream reserved, it's now returned to the outer pool and we - // have a new idle connection in place of the old one. - XCTAssertEqual(reservationsReturned, [1]) - // The inner pool still knows about the reserved stream. - XCTAssertEqual(pool.sync.reservedStreams, 1) - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.idleConnections, 1) - - // Ask for another stream: this will be on the new idle connection. - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - self.eventLoop.run() - XCTAssertEqual(controller.count, 2) - - // Make the connection 'ready'. - controller.connectChannel(atIndex: 1) - controller.sendSettingsToChannel(atIndex: 1) - - self.eventLoop.run() - XCTAssertNoThrow(try w2.wait()) - controller.openStreamInChannel(atIndex: 1) - - // The stream on the quiescing connection is still reserved. - XCTAssertEqual(pool.sync.reservedStreams, 2) - XCTAssertEqual(pool.sync.availableStreams, 99) - - // Return a stream for the _quiescing_ connection: nothing should change in the pool. - controller.closeStreamInChannel(atIndex: 0) - - XCTAssertEqual(pool.sync.reservedStreams, 1) - XCTAssertEqual(pool.sync.availableStreams, 99) - - // Return a stream for the new connection. - controller.closeStreamInChannel(atIndex: 1) - - XCTAssertEqual(reservationsReturned, [1, 1]) - XCTAssertEqual(pool.sync.reservedStreams, 0) - XCTAssertEqual(pool.sync.availableStreams, 100) - } - - func testBackoffIsUsedForReconnections() { - // Fix backoff to always be 1 second. - let backoff = ConnectionBackoff( - initialBackoff: 1.0, - maximumBackoff: 1.0, - multiplier: 1.0, - jitter: 0.0 - ) - - let (pool, controller) = self.setUpPoolAndController(connectionBackoff: backoff) - pool.initialize(connections: 1) - XCTAssertEqual(pool.sync.connections, 1) - - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Start creating the channel. - self.eventLoop.run() - - // Make the connection 'ready'. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0) - self.eventLoop.run() - XCTAssertNoThrow(try w1.wait()) - controller.openStreamInChannel(atIndex: 0) - - // Close the connection. It should hit the transient failure state. - controller.fireChannelInactiveForChannel(atIndex: 0) - // Now nothing is available in the pool. - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - XCTAssertEqual(pool.sync.idleConnections, 0) - - // Enqueue two waiters. One to time out before the reconnect happens. - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - let w3 = pool.makeStream( - deadline: .uptimeNanoseconds(UInt64(TimeAmount.milliseconds(500).nanoseconds)), - logger: self.logger - ) { - $0.eventLoop.makeSucceededVoidFuture() - } - - XCTAssertEqual(pool.sync.waiters, 2) - - // Time out w3. - self.eventLoop.advanceTime(by: .milliseconds(500)) - XCTAssertThrowsError(try w3.wait()) - XCTAssertEqual(pool.sync.waiters, 1) - - // Wait a little more for the backoff to pass. The controller should now have a second channel. - self.eventLoop.advanceTime(by: .milliseconds(500)) - XCTAssertEqual(controller.count, 2) - - // Start up the next channel. - controller.connectChannel(atIndex: 1) - controller.sendSettingsToChannel(atIndex: 1) - self.eventLoop.run() - XCTAssertNoThrow(try w2.wait()) - controller.openStreamInChannel(atIndex: 1) - } - - func testFailedWaiterWithError() throws { - // We want to check a few things in this test: - // - // 1. When an active channel throws an error that any waiter in the connection pool which has - // its deadline exceeded or any waiter which exceeds the waiter limit fails with an error - // which includes the underlying channel error. - // 2. When a reconnect happens and the pool is just busy, no underlying error is passed through - // to failing waiters. - - // Fix backoff to always be 1 second. This is necessary to figure out timings later on when - // we try to establish a new connection. - let backoff = ConnectionBackoff( - initialBackoff: 1.0, - maximumBackoff: 1.0, - multiplier: 1.0, - jitter: 0.0 - ) - - let (pool, controller) = self.setUpPoolAndController(waiters: 10, connectionBackoff: backoff) - pool.initialize(connections: 1) - - // First we'll create two streams which will fail for different reasons. - // - w1 will fail because of a timeout (no channel came up before the waiters own deadline - // passed but no connection has previously failed) - // - w2 will fail because of a timeout but after the underlying channel has failed to connect so - // should have that additional failure information. - let w1 = pool.makeStream(deadline: .uptimeNanoseconds(10), logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - let w2 = pool.makeStream(deadline: .uptimeNanoseconds(20), logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - // Start creating the channel. - self.eventLoop.run() - XCTAssertEqual(controller.count, 1) - - // Fire up the connection. - controller.connectChannel(atIndex: 0) - - // Advance time to fail the w1. - self.eventLoop.advanceTime(to: .uptimeNanoseconds(10)) - - XCTAssertThrowsError(try w1.wait()) { error in - switch error as? GRPCConnectionPoolError { - case .some(let error): - XCTAssertEqual(error.code, .deadlineExceeded) - XCTAssertNil(error.underlyingError) - // Deadline exceeded but no underlying error, as expected. - () - default: - XCTFail("Expected ConnectionPoolError.deadlineExceeded(.none) but got \(error)") - } - } - - // Now fail the connection and timeout w2. - struct DummyError: Error {} - controller.throwError(DummyError(), inChannelAtIndex: 0) - controller.fireChannelInactiveForChannel(atIndex: 0) - self.eventLoop.advanceTime(to: .uptimeNanoseconds(20)) - - XCTAssertThrowsError(try w2.wait()) { error in - switch error as? GRPCConnectionPoolError { - case let .some(error): - XCTAssertEqual(error.code, .deadlineExceeded) - // Deadline exceeded and we have the underlying error. - XCTAssert(error.underlyingError is DummyError) - default: - XCTFail("Expected ConnectionPoolError.deadlineExceeded(.some) but got \(error)") - } - } - - // For the next part of the test we want to validate that when a new channel is created after - // the backoff period passes that no additional errors are attached when the pool is just busy - // but otherwise operational. - // - // To do this we'll create a bunch of waiters. These will be succeeded when the new connection - // comes up and, importantly, use up all available streams on that connection. - // - // We'll then enqueue enough waiters to fill the waiter queue. We'll then validate that one more - // waiter trips over the queue limit but does not include the connection error we saw earlier. - // We'll then timeout the waiters in the queue and validate the same thing. - - // These streams should succeed when the new connection is up. We'll limit the connection to 10 - // streams when we bring it up. - let streams = (0 ..< 10).map { _ in - pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - } - - // The connection is backing off; advance time to create another channel. - XCTAssertEqual(controller.count, 1) - self.eventLoop.advanceTime(by: .seconds(1)) - XCTAssertEqual(controller.count, 2) - controller.connectChannel(atIndex: 1) - controller.sendSettingsToChannel(atIndex: 1, maxConcurrentStreams: 10) - self.eventLoop.run() - - // Make sure the streams are succeeded. - for stream in streams { - XCTAssertNoThrow(try stream.wait()) - controller.openStreamInChannel(atIndex: 1) - } - - // All streams should be reserved. - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 10) - XCTAssertEqual(pool.sync.waiters, 0) - - // We configured the pool to allow for 10 waiters, so let's enqueue that many which will time - // out at a known point in time. - let now = NIODeadline.now() - self.eventLoop.advanceTime(to: now) - let waiters = (0 ..< 10).map { _ in - pool.makeStream(deadline: now + .seconds(1), logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - } - - // This is one waiter more than is allowed so it should hit too-many-waiters. We don't expect - // an inner error though, the connection is just busy. - let tooManyWaiters = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - XCTAssertThrowsError(try tooManyWaiters.wait()) { error in - switch error as? GRPCConnectionPoolError { - case .some(let error): - XCTAssertEqual(error.code, .tooManyWaiters) - XCTAssertNil(error.underlyingError) - default: - XCTFail("Expected ConnectionPoolError.tooManyWaiters(.none) but got \(error)") - } - } - - // Finally, timeout the remaining waiters. Again, no inner error, the connection is just busy. - self.eventLoop.advanceTime(by: .seconds(1)) - for waiter in waiters { - XCTAssertThrowsError(try waiter.wait()) { error in - switch error as? GRPCConnectionPoolError { - case .some(let error): - XCTAssertEqual(error.code, .deadlineExceeded) - XCTAssertNil(error.underlyingError) - default: - XCTFail("Expected ConnectionPoolError.deadlineExceeded(.none) but got \(error)") - } - } - } - } - - func testWaiterStoresItsScheduledTask() throws { - let deadline = NIODeadline.uptimeNanoseconds(42) - let promise = self.eventLoop.makePromise(of: Channel.self) - let waiter = ConnectionPool.Waiter(deadline: deadline, promise: promise) { - return $0.eventLoop.makeSucceededVoidFuture() - } - - XCTAssertNil(waiter._scheduledTimeout) - - waiter.scheduleTimeout(on: self.eventLoop) { - waiter.fail(GRPCConnectionPoolError.deadlineExceeded(connectionError: nil)) - } - - XCTAssertNotNil(waiter._scheduledTimeout) - self.eventLoop.advanceTime(to: deadline) - XCTAssertThrowsError(try promise.futureResult.wait()) - XCTAssertNil(waiter._scheduledTimeout) - } - - func testReturnStreamAfterConnectionCloses() throws { - var returnedStreams = 0 - let (pool, controller) = self.setUpPoolAndController(onReservationReturned: { returned in - returnedStreams += returned - }) - pool.initialize(connections: 1) - - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Start creating the channel. - self.eventLoop.run() - XCTAssertEqual(controller.count, 1) - - // Fire up the connection. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) - - // Run the loop to create the stream, we need to fire the stream creation event too. - self.eventLoop.run() - XCTAssertNoThrow(try waiter.wait()) - controller.openStreamInChannel(atIndex: 0) - - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.availableStreams, 9) - XCTAssertEqual(pool.sync.reservedStreams, 1) - XCTAssertEqual(pool.sync.connections, 1) - - // Close all streams on connection 0. - let error = GRPCStatus(code: .internalError, message: nil) - controller.throwError(error, inChannelAtIndex: 0) - controller.fireChannelInactiveForChannel(atIndex: 0) - XCTAssertEqual(returnedStreams, 1) - - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - XCTAssertEqual(pool.sync.connections, 1) - - // The connection is closed so the stream shouldn't be returned again. - controller.closeStreamInChannel(atIndex: 0) - XCTAssertEqual(returnedStreams, 1) - } - - func testConnectionPoolDelegate() throws { - let recorder = EventRecordingConnectionPoolDelegate() - let (pool, controller) = self.setUpPoolAndController(delegate: recorder) - pool.initialize(connections: 2) - - func assertConnectionAdded( - _ event: EventRecordingConnectionPoolDelegate.Event? - ) throws -> GRPCConnectionID { - let unwrappedEvent = try XCTUnwrap(event) - switch unwrappedEvent { - case let .connectionAdded(id): - return id - default: - throw EventRecordingConnectionPoolDelegate.UnexpectedEvent(unwrappedEvent) - } - } - - let connID1 = try assertConnectionAdded(recorder.popFirst()) - let connID2 = try assertConnectionAdded(recorder.popFirst()) - - let waiter = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - // Start creating the channel. - self.eventLoop.run() - - let startedConnecting = recorder.popFirst() - let firstConn: GRPCConnectionID - let secondConn: GRPCConnectionID - - if startedConnecting == .startedConnecting(connID1) { - firstConn = connID1 - secondConn = connID2 - } else if startedConnecting == .startedConnecting(connID2) { - firstConn = connID2 - secondConn = connID1 - } else { - return XCTFail("Unexpected event") - } - - // Connect the connection. - self.eventLoop.run() - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0, maxConcurrentStreams: 10) - XCTAssertEqual(recorder.popFirst(), .connectSucceeded(firstConn, 10)) - - // Open a stream for the waiter. - controller.openStreamInChannel(atIndex: 0) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, 1, 10)) - self.eventLoop.run() - XCTAssertNoThrow(try waiter.wait()) - - // Okay, more utilization! - for n in 2 ... 8 { - let w = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - controller.openStreamInChannel(atIndex: 0) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, n, 10)) - self.eventLoop.run() - XCTAssertNoThrow(try w.wait()) - } - - // The utilisation threshold before bringing up a new connection is 0.9; we have 8 open streams - // (out of 10) now so opening the next should trigger a connect on the other connection. - let w9 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - XCTAssertEqual(recorder.popFirst(), .startedConnecting(secondConn)) - - // Deal with the 9th stream. - controller.openStreamInChannel(atIndex: 0) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, 9, 10)) - self.eventLoop.run() - XCTAssertNoThrow(try w9.wait()) - - // Bring up the next connection. - controller.connectChannel(atIndex: 1) - controller.sendSettingsToChannel(atIndex: 1, maxConcurrentStreams: 10) - XCTAssertEqual(recorder.popFirst(), .connectSucceeded(secondConn, 10)) - - // The next stream should be on the new connection. - let w10 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - // Deal with the 10th stream. - controller.openStreamInChannel(atIndex: 1) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(secondConn, 1, 10)) - self.eventLoop.run() - XCTAssertNoThrow(try w10.wait()) - - // Close the streams. - for i in 1 ... 9 { - controller.closeStreamInChannel(atIndex: 0) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(firstConn, 9 - i, 10)) - } - - controller.closeStreamInChannel(atIndex: 1) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(secondConn, 0, 10)) - - // Close the connections. - controller.fireChannelInactiveForChannel(atIndex: 0) - XCTAssertEqual(recorder.popFirst(), .connectionClosed(firstConn)) - controller.fireChannelInactiveForChannel(atIndex: 1) - XCTAssertEqual(recorder.popFirst(), .connectionClosed(secondConn)) - - // All conns are already closed. - let shutdownFuture = pool.shutdown() - self.eventLoop.run() - XCTAssertNoThrow(try shutdownFuture.wait()) - - // Two connections must be removed. - for _ in 0 ..< 2 { - if let event = recorder.popFirst() { - XCTAssertEqual(event, event.id.map { .connectionRemoved($0) }) - } else { - XCTFail("Expected .connectionRemoved") - } - } - } - - func testConnectionPoolErrorDescription() { - var error = GRPCConnectionPoolError(code: .deadlineExceeded) - XCTAssertEqual(String(describing: error), "deadlineExceeded") - error.code = .shutdown - XCTAssertEqual(String(describing: error), "shutdown") - error.code = .tooManyWaiters - XCTAssertEqual(String(describing: error), "tooManyWaiters") - - struct DummyError: Error {} - error.underlyingError = DummyError() - XCTAssertEqual(String(describing: error), "tooManyWaiters (DummyError())") - } - - func testConnectionPoolErrorCodeEquality() { - let error = GRPCConnectionPoolError(code: .deadlineExceeded) - XCTAssertEqual(error.code, .deadlineExceeded) - XCTAssertNotEqual(error.code, .shutdown) - } - - func testMinimumConnectionsAreOpenRightAfterInitializing() { - let controller = ChannelController() - let pool = self.makePool(minConnections: 5, channelProvider: controller) - - pool.initialize(connections: 20) - self.eventLoop.run() - - XCTAssertEqual(pool.sync.connections, 20) - XCTAssertEqual(pool.sync.idleConnections, 15) - XCTAssertEqual(pool.sync.activeConnections, 5) - XCTAssertEqual(pool.sync.waiters, 0) - XCTAssertEqual(pool.sync.availableStreams, 0) - XCTAssertEqual(pool.sync.reservedStreams, 0) - XCTAssertEqual(pool.sync.transientFailureConnections, 0) - } - - func testMinimumConnectionsAreOpenAfterOneIsQuiesced() { - let controller = ChannelController() - let pool = self.makePool( - minConnections: 1, - assumedMaxConcurrentStreams: 1, - channelProvider: controller - ) - - // Initialize two connections, and make sure that only one of them is active, - // since we have set minConnections to 1. - pool.initialize(connections: 2) - self.eventLoop.run() - XCTAssertEqual(pool.sync.connections, 2) - XCTAssertEqual(pool.sync.idleConnections, 1) - XCTAssertEqual(pool.sync.activeConnections, 1) - XCTAssertEqual(pool.sync.transientFailureConnections, 0) - - // Open two streams, which, because the maxConcurrentStreams is 1, will - // create two channels. - let w1 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - let w2 = pool.makeStream(deadline: .distantFuture, logger: self.logger) { - $0.eventLoop.makeSucceededVoidFuture() - } - - // Start creating the channels. - self.eventLoop.run() - - // Make both connections ready. - controller.connectChannel(atIndex: 0) - controller.sendSettingsToChannel(atIndex: 0) - controller.connectChannel(atIndex: 1) - controller.sendSettingsToChannel(atIndex: 1) - - // Run the loop to create the streams/connections. - self.eventLoop.run() - XCTAssertNoThrow(try w1.wait()) - controller.openStreamInChannel(atIndex: 0) - XCTAssertNoThrow(try w2.wait()) - controller.openStreamInChannel(atIndex: 1) - - XCTAssertEqual(pool.sync.connections, 2) - XCTAssertEqual(pool.sync.idleConnections, 0) - XCTAssertEqual(pool.sync.activeConnections, 2) - XCTAssertEqual(pool.sync.transientFailureConnections, 0) - - // Quiesce the connection that should be kept alive. - // Another connection should be brought back up immediately after, to maintain - // the minimum number of active connections that won't go idle. - controller.sendGoAwayToChannel(atIndex: 0) - XCTAssertEqual(pool.sync.connections, 3) - XCTAssertEqual(pool.sync.idleConnections, 1) - XCTAssertEqual(pool.sync.activeConnections, 2) - XCTAssertEqual(pool.sync.transientFailureConnections, 0) - - // Now quiesce the other one. This will add a new idle connection, but it - // won't connect it right away. - controller.sendGoAwayToChannel(atIndex: 1) - XCTAssertEqual(pool.sync.connections, 4) - XCTAssertEqual(pool.sync.idleConnections, 2) - XCTAssertEqual(pool.sync.activeConnections, 2) - XCTAssertEqual(pool.sync.transientFailureConnections, 0) - } -} - -extension ConnectionPool { - // For backwards compatibility, to avoid large diffs in these tests. - fileprivate func shutdown() -> EventLoopFuture { - return self.shutdown(mode: .forceful) - } -} - -// MARK: - Helpers - -internal final class ChannelController { - private var channels: [EmbeddedChannel] = [] - - internal var count: Int { - return self.channels.count - } - - internal func finish() { - while let channel = self.channels.popLast() { - // We're okay with this throwing: some channels are left in a bad state (i.e. with errors). - _ = try? channel.finish() - } - } - - private func isValidIndex( - _ index: Int, - file: StaticString = #filePath, - line: UInt = #line - ) -> Bool { - let isValid = self.channels.indices.contains(index) - XCTAssertTrue(isValid, "Invalid connection index '\(index)'", file: file, line: line) - return isValid - } - - internal func connectChannel( - atIndex index: Int, - file: StaticString = #filePath, - line: UInt = #line - ) { - guard self.isValidIndex(index, file: file, line: line) else { return } - - XCTAssertNoThrow( - try self.channels[index].connect(to: .init(unixDomainSocketPath: "/")), - file: file, - line: line - ) - } - - internal func fireChannelInactiveForChannel( - atIndex index: Int, - file: StaticString = #filePath, - line: UInt = #line - ) { - guard self.isValidIndex(index, file: file, line: line) else { return } - self.channels[index].pipeline.fireChannelInactive() - } - - internal func throwError( - _ error: Error, - inChannelAtIndex index: Int, - file: StaticString = #filePath, - line: UInt = #line - ) { - guard self.isValidIndex(index, file: file, line: line) else { return } - self.channels[index].pipeline.fireErrorCaught(error) - } - - internal func sendSettingsToChannel( - atIndex index: Int, - maxConcurrentStreams: Int = 100, - file: StaticString = #filePath, - line: UInt = #line - ) { - guard self.isValidIndex(index, file: file, line: line) else { return } - - let settings = [HTTP2Setting(parameter: .maxConcurrentStreams, value: maxConcurrentStreams)] - let settingsFrame = HTTP2Frame(streamID: .rootStream, payload: .settings(.settings(settings))) - - XCTAssertNoThrow(try self.channels[index].writeInbound(settingsFrame), file: file, line: line) - } - - internal func sendGoAwayToChannel( - atIndex index: Int, - file: StaticString = #filePath, - line: UInt = #line - ) { - guard self.isValidIndex(index, file: file, line: line) else { return } - - let goAwayFrame = HTTP2Frame( - streamID: .rootStream, - payload: .goAway(lastStreamID: .maxID, errorCode: .noError, opaqueData: nil) - ) - - XCTAssertNoThrow(try self.channels[index].writeInbound(goAwayFrame), file: file, line: line) - } - - internal func openStreamInChannel( - atIndex index: Int, - file: StaticString = #filePath, - line: UInt = #line - ) { - guard self.isValidIndex(index, file: file, line: line) else { return } - - // The details don't matter here. - let event = NIOHTTP2StreamCreatedEvent( - streamID: .rootStream, - localInitialWindowSize: nil, - remoteInitialWindowSize: nil - ) - - self.channels[index].pipeline.fireUserInboundEventTriggered(event) - } - - internal func closeStreamInChannel( - atIndex index: Int, - file: StaticString = #filePath, - line: UInt = #line - ) { - guard self.isValidIndex(index, file: file, line: line) else { return } - - // The details don't matter here. - let event = StreamClosedEvent(streamID: .rootStream, reason: nil) - self.channels[index].pipeline.fireUserInboundEventTriggered(event) - } -} - -extension ChannelController: ConnectionManagerChannelProvider { - internal func makeChannel( - managedBy connectionManager: ConnectionManager, - onEventLoop eventLoop: EventLoop, - connectTimeout: TimeAmount?, - logger: Logger - ) -> EventLoopFuture { - let channel = EmbeddedChannel(loop: eventLoop as! EmbeddedEventLoop) - self.channels.append(channel) - - let multiplexer = HTTP2StreamMultiplexer( - mode: .client, - channel: channel, - inboundStreamInitializer: nil - ) - - let idleHandler = GRPCIdleHandler( - connectionManager: connectionManager, - multiplexer: multiplexer, - idleTimeout: .minutes(5), - keepalive: ClientConnectionKeepalive(), - logger: logger - ) - - XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(idleHandler)) - XCTAssertNoThrow(try channel.pipeline.syncOperations.addHandler(multiplexer)) - - return eventLoop.makeSucceededFuture(channel) - } -} - -internal struct HookedStreamLender: StreamLender { - internal var onReturnStreams: (Int) -> Void - internal var onUpdateMaxAvailableStreams: (Int) -> Void - - internal func returnStreams(_ count: Int, to pool: ConnectionPool) { - self.onReturnStreams(count) - } - - internal func changeStreamCapacity(by delta: Int, for: ConnectionPool) { - self.onUpdateMaxAvailableStreams(delta) - } -} - -extension Optional where Wrapped == GRPCConnectionPoolError { - internal var isTooManyWaiters: Bool { - self?.code == .tooManyWaiters - } - - internal var isDeadlineExceeded: Bool { - self?.code == .deadlineExceeded - } - - internal var isShutdown: Bool { - self?.code == .shutdown - } -} diff --git a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift b/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift deleted file mode 100644 index 90b53057d..000000000 --- a/Tests/GRPCTests/ConnectionPool/GRPCChannelPoolTests.swift +++ /dev/null @@ -1,622 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import EchoImplementation -import EchoModel -import GRPC -import GRPCSampleData -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix -import NIOSSL -import XCTest - -final class GRPCChannelPoolTests: GRPCTestCase { - private var group: MultiThreadedEventLoopGroup! - private var server: Server? - private var channel: GRPCChannel? - - private var serverPort: Int? { - return self.server?.channel.localAddress?.port - } - - private var echo: Echo_EchoNIOClient { - return Echo_EchoNIOClient(channel: self.channel!) - } - - override func tearDown() { - if let channel = self.channel { - XCTAssertNoThrow(try channel.close().wait()) - } - - if let server = self.server { - XCTAssertNoThrow(try server.close().wait()) - } - - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - private func configureEventLoopGroup(threads: Int = System.coreCount) { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: threads) - } - - private func makeServerBuilder(withTLS: Bool) -> Server.Builder { - let builder: Server.Builder - - if withTLS { - builder = Server.usingTLSBackedByNIOSSL( - on: self.group, - certificateChain: [SampleCertificate.server.certificate], - privateKey: SamplePrivateKey.server - ).withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - } else { - builder = Server.insecure(group: self.group) - } - - return - builder - .withLogger(self.serverLogger) - .withServiceProviders([EchoProvider()]) - } - - private func startServer(withTLS: Bool = false, port: Int = 0) { - self.server = try! self.makeServerBuilder(withTLS: withTLS) - .bind(host: "localhost", port: port) - .wait() - } - - private func startChannel( - withTLS: Bool = false, - overrideTarget targetOverride: ConnectionTarget? = nil, - _ configure: (inout GRPCChannelPool.Configuration) -> Void = { _ in } - ) { - let transportSecurity: GRPCChannelPool.Configuration.TransportSecurity - - if withTLS { - let configuration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - trustRoots: .certificates([SampleCertificate.ca.certificate]) - ) - transportSecurity = .tls(configuration) - } else { - transportSecurity = .plaintext - } - - self.channel = try! GRPCChannelPool.with( - target: targetOverride ?? .hostAndPort("localhost", self.serverPort!), - transportSecurity: transportSecurity, - eventLoopGroup: self.group - ) { configuration in - configuration.backgroundActivityLogger = self.clientLogger - configure(&configuration) - } - } - - private func setUpClientAndServer( - withTLS tls: Bool, - threads: Int = System.coreCount, - _ configure: (inout GRPCChannelPool.Configuration) -> Void = { _ in } - ) { - self.configureEventLoopGroup(threads: threads) - self.startServer(withTLS: tls) - self.startChannel(withTLS: tls) { - // We'll allow any number of waiters since we immediately fire off a bunch of RPCs and don't - // want to bounce off the limit as we wait for a connection to come up. - $0.connectionPool.maxWaitersPerEventLoop = .max - configure(&$0) - } - } - - private func doTestUnaryRPCs(count: Int) throws { - var futures: [EventLoopFuture] = [] - futures.reserveCapacity(count) - - for i in 1 ... count { - let request = Echo_EchoRequest.with { $0.text = String(describing: i) } - let get = self.echo.get(request) - futures.append(get.status) - } - - let statuses = try EventLoopFuture.whenAllSucceed(futures, on: self.group.next()).wait() - XCTAssert(statuses.allSatisfy { $0.isOk }) - } - - func testUnaryRPCs_plaintext() throws { - self.setUpClientAndServer(withTLS: false) - try self.doTestUnaryRPCs(count: 100) - } - - func testUnaryRPCs_tls() throws { - self.setUpClientAndServer(withTLS: true) - try self.doTestUnaryRPCs(count: 100) - } - - private func doTestClientStreamingRPCs(count: Int) throws { - var futures: [EventLoopFuture] = [] - futures.reserveCapacity(count) - - for i in 1 ... count { - let request = Echo_EchoRequest.with { $0.text = String(describing: i) } - let collect = self.echo.collect() - collect.sendMessage(request, promise: nil) - collect.sendMessage(request, promise: nil) - collect.sendMessage(request, promise: nil) - collect.sendEnd(promise: nil) - futures.append(collect.status) - } - - let statuses = try EventLoopFuture.whenAllSucceed(futures, on: self.group.next()).wait() - XCTAssert(statuses.allSatisfy { $0.isOk }) - } - - func testClientStreamingRPCs_plaintext() throws { - self.setUpClientAndServer(withTLS: false) - try self.doTestClientStreamingRPCs(count: 100) - } - - func testClientStreamingRPCs() throws { - self.setUpClientAndServer(withTLS: true) - try self.doTestClientStreamingRPCs(count: 100) - } - - private func doTestServerStreamingRPCs(count: Int) throws { - var futures: [EventLoopFuture] = [] - futures.reserveCapacity(count) - - for i in 1 ... count { - let request = Echo_EchoRequest.with { $0.text = String(describing: i) } - let expand = self.echo.expand(request) { _ in } - futures.append(expand.status) - } - - let statuses = try EventLoopFuture.whenAllSucceed(futures, on: self.group.next()).wait() - XCTAssert(statuses.allSatisfy { $0.isOk }) - } - - func testServerStreamingRPCs_plaintext() throws { - self.setUpClientAndServer(withTLS: false) - try self.doTestServerStreamingRPCs(count: 100) - } - - func testServerStreamingRPCs() throws { - self.setUpClientAndServer(withTLS: true) - try self.doTestServerStreamingRPCs(count: 100) - } - - private func doTestBidiStreamingRPCs(count: Int) throws { - var futures: [EventLoopFuture] = [] - futures.reserveCapacity(count) - - for i in 1 ... count { - let request = Echo_EchoRequest.with { $0.text = String(describing: i) } - let update = self.echo.update { _ in } - update.sendMessage(request, promise: nil) - update.sendMessage(request, promise: nil) - update.sendMessage(request, promise: nil) - update.sendEnd(promise: nil) - futures.append(update.status) - } - - let statuses = try EventLoopFuture.whenAllSucceed(futures, on: self.group.next()).wait() - XCTAssert(statuses.allSatisfy { $0.isOk }) - } - - func testBidiStreamingRPCs_plaintext() throws { - self.setUpClientAndServer(withTLS: false) - try self.doTestBidiStreamingRPCs(count: 100) - } - - func testBidiStreamingRPCs() throws { - self.setUpClientAndServer(withTLS: true) - try self.doTestBidiStreamingRPCs(count: 100) - } - - func testWaitersTimeoutWhenNoConnectionCannotBeEstablished() throws { - // 4 threads == 4 pools - self.configureEventLoopGroup(threads: 4) - // Don't start a server; override the target (otherwise we'll fail to unwrap `serverPort`). - self.startChannel(overrideTarget: .unixDomainSocket("/nope")) { - // Tiny wait time for waiters. - $0.connectionPool.maxWaitTime = .milliseconds(50) - } - - var statuses: [EventLoopFuture] = [] - statuses.reserveCapacity(40) - - // Queue RPCs on each loop. - for eventLoop in self.group.makeIterator() { - let options = CallOptions(eventLoopPreference: .exact(eventLoop)) - for i in 0 ..< 10 { - let get = self.echo.get(.with { $0.text = String(describing: i) }, callOptions: options) - statuses.append(get.status) - } - } - - let results = try EventLoopFuture.whenAllComplete(statuses, on: self.group.next()).wait() - for result in results { - result.assertSuccess { - XCTAssertEqual($0.code, .deadlineExceeded) - } - } - } - - func testRPCsAreDistributedAcrossEventLoops() throws { - self.configureEventLoopGroup(threads: 4) - - // We don't need a server here, but we do need a different target - self.startChannel(overrideTarget: .unixDomainSocket("/nope")) { - // Increase the max wait time: we're relying on the server will never coming up, so the RPCs - // never complete and streams are not returned back to pools. - $0.connectionPool.maxWaitTime = .hours(1) - } - - var echo = self.echo - echo.defaultCallOptions.eventLoopPreference = .indifferent - - let rpcs = (0 ..< 40).map { _ in echo.update { _ in } } - - let rpcsByEventLoop = Dictionary(grouping: rpcs, by: { ObjectIdentifier($0.eventLoop) }) - for rpcs in rpcsByEventLoop.values { - // 40 RPCs over 4 ELs should be 10 RPCs per EL. - XCTAssertEqual(rpcs.count, 10) - } - - // All RPCs are waiting for connections since we never brought up a server. Each will fail when - // we shutdown the pool. - XCTAssertNoThrow(try self.channel?.close().wait()) - // Unset the channel to avoid shutting down again in tearDown(). - self.channel = nil - - for rpc in rpcs { - XCTAssertEqual(try rpc.status.wait().code, .unavailable) - } - } - - func testWaiterLimitPerEventLoop() throws { - self.configureEventLoopGroup(threads: 4) - self.startChannel(overrideTarget: .unixDomainSocket("/nope")) { - $0.connectionPool.maxWaitersPerEventLoop = 10 - $0.connectionPool.maxWaitTime = .hours(1) - } - - let loop = self.group.next() - let options = CallOptions(eventLoopPreference: .exact(loop)) - - // The first 10 will be waiting for the connection. The 11th should be failed immediately. - let rpcs = (1 ... 11).map { _ in - self.echo.get(.with { $0.text = "" }, callOptions: options) - } - - XCTAssertEqual(try rpcs.last?.status.wait().code, .resourceExhausted) - - // If we express no event loop preference then we should not get the loaded loop. - let indifferentLoopRPCs = (1 ... 10).map { - _ in self.echo.get(.with { $0.text = "" }) - } - - XCTAssert(indifferentLoopRPCs.map { $0.eventLoop }.allSatisfy { $0 !== loop }) - } - - func testWaitingRPCStartsWhenStreamCapacityIsAvailable() throws { - self.configureEventLoopGroup(threads: 1) - self.startServer() - self.startChannel { - $0.connectionPool.connectionsPerEventLoop = 1 - $0.connectionPool.maxWaitTime = .hours(1) - } - - let lock = NIOLock() - var order = 0 - - // We need a connection to be up and running to avoid hitting the waiter limit when creating a - // batch of RPCs in one go. - let warmup = self.echo.get(.with { $0.text = "" }) - XCTAssert(try warmup.status.wait().isOk) - - // MAX_CONCURRENT_STREAMS should be 100, we'll create 101 RPCs, 100 of which should not have to - // wait because there's already an active connection. - let rpcs = (0 ..< 101).map { _ in self.echo.update { _ in } } - // The first RPC should (obviously) complete first. - rpcs.first!.status.whenComplete { _ in - lock.withLock { - XCTAssertEqual(order, 0) - order += 1 - } - } - - // The 101st RPC will complete once the first is completed (we explicitly terminate the 1st - // RPC below). - rpcs.last!.status.whenComplete { _ in - lock.withLock { - XCTAssertEqual(order, 1) - order += 1 - } - } - - // Still zero: the first RPC is still active. - lock.withLock { XCTAssertEqual(order, 0) } - // End the first RPC. - XCTAssertNoThrow(try rpcs.first!.sendEnd().wait()) - XCTAssertNoThrow(try rpcs.first!.status.wait()) - lock.withLock { XCTAssertEqual(order, 1) } - // End the last RPC. - XCTAssertNoThrow(try rpcs.last!.sendEnd().wait()) - XCTAssertNoThrow(try rpcs.last!.status.wait()) - lock.withLock { XCTAssertEqual(order, 2) } - - // End the rest. - for rpc in rpcs.dropFirst().dropLast() { - XCTAssertNoThrow(try rpc.sendEnd().wait()) - } - } - - func testRPCOnShutdownPool() { - self.configureEventLoopGroup(threads: 1) - self.startChannel(overrideTarget: .unixDomainSocket("/ignored")) - - let echo = self.echo - - XCTAssertNoThrow(try self.channel?.close().wait()) - // Avoid shutting down again in tearDown() - self.channel = nil - - let get = echo.get(.with { $0.text = "" }) - XCTAssertEqual(try get.status.wait().code, .unavailable) - } - - func testCallDeadlineIsUsedIfSoonerThanWaitingDeadline() { - self.configureEventLoopGroup(threads: 1) - self.startChannel(overrideTarget: .unixDomainSocket("/nope")) { - $0.connectionPool.maxWaitTime = .hours(24) - } - - // Deadline is sooner than the 24 hour waiter time, we expect to time out sooner rather than - // (much) later! - let options = CallOptions(timeLimit: .deadline(.now())) - let timedOutOnOwnDeadline = self.echo.get(.with { $0.text = "" }, callOptions: options) - - XCTAssertEqual(try timedOutOnOwnDeadline.status.wait().code, .deadlineExceeded) - } - - func testTLSFailuresAreClearerAtTheRPCLevel() throws { - // Mix and match TLS. - self.configureEventLoopGroup(threads: 1) - self.startServer(withTLS: false) - self.startChannel(withTLS: true) { - $0.connectionPool.maxWaitersPerEventLoop = 10 - } - - // We can't guarantee an error happens within a certain time limit, so if we don't see what we - // expect we'll loop until a given deadline passes. - let testDeadline = NIODeadline.now() + .seconds(5) - var seenError = false - while testDeadline > .now() { - let options = CallOptions(timeLimit: .deadline(.now() + .milliseconds(50))) - let get = self.echo.get(.with { $0.text = "foo" }, callOptions: options) - - let status = try get.status.wait() - XCTAssertEqual(status.code, .deadlineExceeded) - - if let cause = status.cause, cause is NIOSSLError { - // What we expect. - seenError = true - break - } else { - // Try again. - continue - } - } - XCTAssert(seenError) - - // Now queue up a bunch of RPCs to fill up the waiter queue. We don't care about the outcome - // of these. (They'll fail when we tear down the pool at the end of the test.) - _ = (0 ..< 10).map { i -> UnaryCall in - let options = CallOptions(timeLimit: .deadline(.distantFuture)) - return self.echo.get(.with { $0.text = String(describing: i) }, callOptions: options) - } - - // Queue up one more. - let options = CallOptions(timeLimit: .deadline(.distantFuture)) - let tooManyWaiters = self.echo.get(.with { $0.text = "foo" }, callOptions: options) - - let status = try tooManyWaiters.status.wait() - XCTAssertEqual(status.code, .resourceExhausted) - - if let cause = status.cause { - XCTAssert(cause is NIOSSLError) - } else { - XCTFail("Status message did not contain a possible cause: '\(status.message ?? "nil")'") - } - } - - func testConnectionPoolDelegateSingleConnection() throws { - let recorder = EventRecordingConnectionPoolDelegate() - self.setUpClientAndServer(withTLS: false, threads: 1) { - $0.delegate = recorder - } - - let warmup = self.echo.get(.with { $0.text = "" }) - XCTAssertNoThrow(try warmup.status.wait()) - - let id = try XCTUnwrap(recorder.first?.id) - XCTAssertEqual(recorder.popFirst(), .connectionAdded(id)) - XCTAssertEqual(recorder.popFirst(), .startedConnecting(id)) - XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 0, 100)) - - let rpcs: [ClientStreamingCall] = try (1 ... 10).map { i in - let rpc = self.echo.collect() - XCTAssertNoThrow(try rpc.sendMessage(.with { $0.text = "foo" }).wait()) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, i, 100)) - return rpc - } - - for (i, rpc) in rpcs.enumerated() { - XCTAssertNoThrow(try rpc.sendEnd().wait()) - XCTAssertNoThrow(try rpc.status.wait()) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id, 10 - (i + 1), 100)) - } - - XCTAssertNoThrow(try self.channel?.close().wait()) - XCTAssertEqual(recorder.popFirst(), .connectionClosed(id)) - XCTAssertEqual(recorder.popFirst(), .connectionRemoved(id)) - XCTAssert(recorder.isEmpty) - } - - func testConnectionPoolDelegateQuiescing() throws { - let recorder = EventRecordingConnectionPoolDelegate() - self.setUpClientAndServer(withTLS: false, threads: 1) { - $0.delegate = recorder - } - - XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) - let id1 = try XCTUnwrap(recorder.first?.id) - XCTAssertEqual(recorder.popFirst(), .connectionAdded(id1)) - XCTAssertEqual(recorder.popFirst(), .startedConnecting(id1)) - XCTAssertEqual(recorder.popFirst(), .connectSucceeded(id1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 0, 100)) - - // Start an RPC. - let rpc = self.echo.collect() - XCTAssertNoThrow(try rpc.sendMessage(.with { $0.text = "foo" }).wait()) - // Complete another one to make sure the previous one is known by the server. - XCTAssertNoThrow(try self.echo.get(.with { $0.text = "foo" }).status.wait()) - - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 2, 100)) - XCTAssertEqual(recorder.popFirst(), .connectionUtilizationChanged(id1, 1, 100)) - - // Start shutting the server down. - let didShutdown = self.server!.initiateGracefulShutdown() - self.server = nil // Avoid shutting down again in tearDown - - // Pause a moment so we know the client received the GOAWAY. - let sleep = self.group.any().scheduleTask(in: .milliseconds(50)) {} - XCTAssertNoThrow(try sleep.futureResult.wait()) - XCTAssertEqual(recorder.popFirst(), .connectionQuiescing(id1)) - - // Finish the RPC. - XCTAssertNoThrow(try rpc.sendEnd().wait()) - XCTAssertNoThrow(try rpc.status.wait()) - - // Server should shutdown now. - XCTAssertNoThrow(try didShutdown.wait()) - } - - func testDelegateCanTellWhenFirstConnectionIsBeingEstablished() { - final class State { - private enum Storage { - case idle - case connecting - case connected - } - - private var state: Storage = .idle - private let lock = NIOLock() - - var isConnected: Bool { - return self.lock.withLock { - switch self.state { - case .connected: - return true - case .idle, .connecting: - return false - } - } - } - - func startedConnecting() { - self.lock.withLock { - switch self.state { - case .idle: - self.state = .connecting - case .connecting, .connected: - XCTFail("Invalid state \(self.state) for \(#function)") - } - } - } - - func connected() { - self.lock.withLock { - switch self.state { - case .connecting: - self.state = .connected - case .idle, .connected: - XCTFail("Invalid state \(self.state) for \(#function)") - } - } - } - } - - let state = State() - - self.setUpClientAndServer(withTLS: false, threads: 1) { - $0.delegate = IsConnectingDelegate { stateChange in - switch stateChange { - case .connecting: - state.startedConnecting() - case .connected: - state.connected() - } - } - } - - XCTAssertFalse(state.isConnected) - let rpc = self.echo.get(.with { $0.text = "" }) - XCTAssertNoThrow(try rpc.status.wait()) - XCTAssertTrue(state.isConnected) - - // We should be able to do a bunch of other RPCs without the state changing (we'll XCTFail if - // a state change happens). - let rpcs: [EventLoopFuture] = (0 ..< 20).map { i in - let rpc = self.echo.get(.with { $0.text = "\(i)" }) - return rpc.status - } - XCTAssertNoThrow(try EventLoopFuture.andAllSucceed(rpcs, on: self.group.any()).wait()) - } - - func testDelegateGetsCalledWithStats() throws { - let recorder = EventRecordingConnectionPoolDelegate() - - self.configureEventLoopGroup(threads: 4) - self.startServer(withTLS: false) - self.startChannel(withTLS: false) { - $0.statsPeriod = .milliseconds(1) - $0.delegate = recorder - } - - let scheduled = self.group.next().scheduleTask(in: .milliseconds(100)) { - _ = self.channel?.close() - } - - try scheduled.futureResult.wait() - - let events = recorder.removeAll() - let statsEvents = events.compactMap { event in - switch event { - case .stats(let stats, _): - return stats - default: - return nil - } - } - - XCTAssertGreaterThan(statsEvents.count, 0) - } -} -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift b/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift deleted file mode 100644 index ed956d740..000000000 --- a/Tests/GRPCTests/ConnectionPool/PoolManagerStateMachineTests.swift +++ /dev/null @@ -1,372 +0,0 @@ -/* - * 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 NIOConcurrencyHelpers -import NIOCore -import NIOEmbedded -import XCTest - -@testable import GRPC - -class PoolManagerStateMachineTests: GRPCTestCase { - private func makeConnectionPool( - on eventLoop: EventLoop, - maxWaiters: Int = 100, - maxConcurrentStreams: Int = 100, - loadThreshold: Double = 0.9, - connectionBackoff: ConnectionBackoff = ConnectionBackoff(), - makeChannel: @escaping (ConnectionManager, EventLoop) -> EventLoopFuture - ) -> ConnectionPool { - return ConnectionPool( - eventLoop: eventLoop, - maxWaiters: maxWaiters, - minConnections: 0, - reservationLoadThreshold: loadThreshold, - assumedMaxConcurrentStreams: maxConcurrentStreams, - connectionBackoff: connectionBackoff, - channelProvider: HookedChannelProvider(makeChannel), - streamLender: HookedStreamLender( - onReturnStreams: { _ in }, - onUpdateMaxAvailableStreams: { _ in } - ), - delegate: nil, - logger: self.logger - ) - } - - private func makeInitializedPools( - group: EmbeddedEventLoopGroup, - connectionsPerPool: Int = 1 - ) -> [ConnectionPool] { - let pools = group.loops.map { - self.makeConnectionPool(on: $0) { _, _ in fatalError() } - } - - for pool in pools { - pool.initialize(connections: 1) - } - - return pools - } - - private func makeConnectionPoolKeys( - for pools: [ConnectionPool] - ) -> [PoolManager.ConnectionPoolKey] { - return pools.enumerated().map { index, pool in - return .init(index: .init(index), eventLoopID: pool.eventLoop.id) - } - } - - func testReserveStreamOnPreferredEventLoop() { - let group = EmbeddedEventLoopGroup(loops: 5) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) - let keys = self.makeConnectionPoolKeys(for: pools) - var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) - ) - - for (index, loop) in group.loops.enumerated() { - let reservePreferredLoop = state.reserveStream(preferringPoolWithEventLoopID: loop.id) - reservePreferredLoop.assertSuccess { - XCTAssertEqual($0, PoolManager.ConnectionPoolIndex(index)) - } - } - } - - func testReserveStreamOnPreferredEventLoopWhichNoPoolUses() { - let group = EmbeddedEventLoopGroup(loops: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) - let keys = self.makeConnectionPoolKeys(for: pools) - var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) - ) - - let anotherLoop = EmbeddedEventLoop() - let reservePreferredLoop = state.reserveStream(preferringPoolWithEventLoopID: anotherLoop.id) - reservePreferredLoop.assertSuccess { - XCTAssert((0 ..< pools.count).contains($0.value)) - } - } - - func testReserveStreamWithNoPreferenceReturnsPoolWithHighestAvailability() { - let group = EmbeddedEventLoopGroup(loops: 5) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) - let keys = self.makeConnectionPoolKeys(for: pools) - var state = PoolManagerStateMachine(.inactive) - state.activatePools(keyedBy: keys, assumingPerPoolCapacity: 100, statsTask: nil) - - // Reserve some streams. - for (index, loop) in group.loops.enumerated() { - for _ in 0 ..< 2 * index { - state.reserveStream(preferringPoolWithEventLoopID: loop.id).assertSuccess() - } - } - - // We expect pools[0] to be reserved. - // index: 0 1 2 3 4 - // available: 100 98 96 94 92 - state.reserveStream(preferringPoolWithEventLoopID: nil).assertSuccess { poolIndex in - XCTAssertEqual(poolIndex.value, 0) - } - - // We expect pools[0] to be reserved again. - // index: 0 1 2 3 4 - // available: 99 98 96 94 92 - state.reserveStream(preferringPoolWithEventLoopID: nil).assertSuccess { poolIndex in - XCTAssertEqual(poolIndex.value, 0) - } - - // Return some streams to pools[3]. - state.returnStreams(5, toPoolOnEventLoopWithID: pools[3].eventLoop.id) - - // As we returned streams to pools[3] we expect this to be the current state: - // index: 0 1 2 3 4 - // available: 98 98 96 99 92 - state.reserveStream(preferringPoolWithEventLoopID: nil).assertSuccess { poolIndex in - XCTAssertEqual(poolIndex.value, 3) - } - - // Give an event loop preference for a pool which has more streams reserved. - state.reserveStream( - preferringPoolWithEventLoopID: pools[2].eventLoop.id - ).assertSuccess { poolIndex in - XCTAssertEqual(poolIndex.value, 2) - } - - // Update the capacity for one pool, this makes it relatively more available. - state.changeStreamCapacity(by: 900, forPoolOnEventLoopWithID: pools[4].eventLoop.id) - // pools[4] has a bunch more streams now: - // index: 0 1 2 3 4 - // available: 98 98 96 99 992 - state.reserveStream(preferringPoolWithEventLoopID: nil).assertSuccess { poolIndex in - XCTAssertEqual(poolIndex.value, 4) - } - } - - func testReserveStreamWithNoEventLoopPreference() { - let group = EmbeddedEventLoopGroup(loops: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) - let keys = self.makeConnectionPoolKeys(for: pools) - var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) - ) - - let reservePreferredLoop = state.reserveStream(preferringPoolWithEventLoopID: nil) - reservePreferredLoop.assertSuccess() - } - - func testReserveStreamWhenInactive() { - var state = PoolManagerStateMachine(.inactive) - let action = state.reserveStream(preferringPoolWithEventLoopID: nil) - action.assertFailure { error in - XCTAssertEqual(error, .notInitialized) - } - } - - func testReserveStreamWhenShuttingDown() { - let future = EmbeddedEventLoop().makeSucceededFuture(()) - var state = PoolManagerStateMachine(.shuttingDown(future)) - let action = state.reserveStream(preferringPoolWithEventLoopID: nil) - action.assertFailure { error in - XCTAssertEqual(error, .shutdown) - } - } - - func testReserveStreamWhenShutdown() { - var state = PoolManagerStateMachine(.shutdown) - let action = state.reserveStream(preferringPoolWithEventLoopID: nil) - action.assertFailure { error in - XCTAssertEqual(error, .shutdown) - } - } - - func testShutdownWhenInactive() { - let loop = EmbeddedEventLoop() - let promise = loop.makePromise(of: Void.self) - - var state = PoolManagerStateMachine(.inactive) - let action = state.shutdown(promise: promise) - action.assertAlreadyShutdown() - - // Don't leak the promise. - promise.succeed(()) - } - - func testShutdownWhenActive() { - let group = EmbeddedEventLoopGroup(loops: 5) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let pools = self.makeInitializedPools(group: group, connectionsPerPool: 1) - let keys = self.makeConnectionPoolKeys(for: pools) - var state = PoolManagerStateMachine( - .active(.init(poolKeys: keys, assumedMaxAvailableStreamsPerPool: 100, statsTask: nil)) - ) - - let promise = group.loops[0].makePromise(of: Void.self) - promise.succeed(()) - - state.shutdown(promise: promise).assertShutdownPools() - } - - func testShutdownWhenShuttingDown() { - let loop = EmbeddedEventLoop() - let future = loop.makeSucceededVoidFuture() - var state = PoolManagerStateMachine(.shuttingDown(future)) - - let promise = loop.makePromise(of: Void.self) - promise.succeed(()) - - let action = state.shutdown(promise: promise) - action.assertAlreadyShuttingDown { - XCTAssert($0 === future) - } - - // Fully shutdown. - state.shutdownComplete() - state.shutdown(promise: promise).assertAlreadyShutdown() - } - - func testShutdownWhenShutdown() { - let loop = EmbeddedEventLoop() - var state = PoolManagerStateMachine(.shutdown) - - let promise = loop.makePromise(of: Void.self) - promise.succeed(()) - - let action = state.shutdown(promise: promise) - action.assertAlreadyShutdown() - } -} - -// MARK: - Test Helpers - -extension Result { - internal func assertSuccess( - file: StaticString = #filePath, - line: UInt = #line, - verify: (Success) -> Void = { _ in } - ) { - if case let .success(value) = self { - verify(value) - } else { - XCTFail("Expected '.success' but got '\(self)'", file: file, line: line) - } - } - - internal func assertFailure( - file: StaticString = #filePath, - line: UInt = #line, - verify: (Failure) -> Void = { _ in } - ) { - if case let .failure(value) = self { - verify(value) - } else { - XCTFail("Expected '.failure' but got '\(self)'", file: file, line: line) - } - } -} - -extension PoolManagerStateMachine.ShutdownAction { - internal func assertShutdownPools( - file: StaticString = #filePath, - line: UInt = #line - ) { - if case .shutdownPools = self { - () - } else { - XCTFail("Expected '.shutdownPools' but got '\(self)'", file: file, line: line) - } - } - - internal func assertAlreadyShuttingDown( - file: StaticString = #filePath, - line: UInt = #line, - verify: (EventLoopFuture) -> Void = { _ in } - ) { - if case let .alreadyShuttingDown(future) = self { - verify(future) - } else { - XCTFail("Expected '.alreadyShuttingDown' but got '\(self)'", file: file, line: line) - } - } - - internal func assertAlreadyShutdown(file: StaticString = #filePath, line: UInt = #line) { - if case .alreadyShutdown = self { - () - } else { - XCTFail("Expected '.alreadyShutdown' but got '\(self)'", file: file, line: line) - } - } -} - -/// An `EventLoopGroup` of `EmbeddedEventLoop`s. -private final class EmbeddedEventLoopGroup: EventLoopGroup { - internal let loops: [EmbeddedEventLoop] - - internal let lock = NIOLock() - internal var index = 0 - - internal init(loops: Int) { - self.loops = (0 ..< loops).map { _ in EmbeddedEventLoop() } - } - - internal func next() -> EventLoop { - let index: Int = self.lock.withLock { - let index = self.index - self.index += 1 - return index - } - return self.loops[index % self.loops.count] - } - - internal func makeIterator() -> EventLoopIterator { - return EventLoopIterator(self.loops) - } - - internal func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) { - var shutdownError: Error? - - for loop in self.loops { - loop.shutdownGracefully(queue: queue) { error in - if let error = error { - shutdownError = error - } - } - } - - queue.sync { - callback(shutdownError) - } - } -} diff --git a/Tests/GRPCTests/ConnectivityStateMonitorTests.swift b/Tests/GRPCTests/ConnectivityStateMonitorTests.swift deleted file mode 100644 index 3ecdb794c..000000000 --- a/Tests/GRPCTests/ConnectivityStateMonitorTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 Logging -import XCTest - -@testable import GRPC - -class ConnectivityStateMonitorTests: GRPCTestCase { - // Ensure `.idle` isn't first since it is the initial state and we only trigger callbacks - // when the state changes, not when the state is set. - let states: [ConnectivityState] = [.connecting, .ready, .transientFailure, .shutdown, .idle] - - func testDelegateOnlyCalledForChanges() { - let recorder = RecordingConnectivityDelegate() - - recorder.expectChanges(3) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .ready), - Change(from: .ready, to: .shutdown), - ] - ) - } - - let monitor = ConnectivityStateMonitor(delegate: recorder, queue: nil) - monitor.delegate = recorder - - monitor.updateState(to: .connecting, logger: self.logger) - monitor.updateState(to: .ready, logger: self.logger) - monitor.updateState(to: .ready, logger: self.logger) - monitor.updateState(to: .shutdown, logger: self.logger) - - recorder.waitForExpectedChanges(timeout: .seconds(1)) - } -} diff --git a/Tests/GRPCTests/DebugChannelInitializerTests.swift b/Tests/GRPCTests/DebugChannelInitializerTests.swift deleted file mode 100644 index ca5641ff3..000000000 --- a/Tests/GRPCTests/DebugChannelInitializerTests.swift +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import GRPC -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix -import XCTest - -class DebugChannelInitializerTests: GRPCTestCase { - func testDebugChannelInitializerIsCalled() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let serverDebugInitializerCalled = group.next().makePromise(of: Void.self) - let server = try Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) - .withDebugChannelInitializer { channel in - serverDebugInitializerCalled.succeed(()) - return channel.eventLoop.makeSucceededFuture(()) - } - .bind(host: "localhost", port: 0) - .wait() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - let clientDebugInitializerCalled = group.next().makePromise(of: Void.self) - let connection = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - .withDebugChannelInitializer { channel in - clientDebugInitializerCalled.succeed(()) - return channel.eventLoop.makeSucceededFuture(()) - } - .connect(host: "localhost", port: server.channel.localAddress!.port!) - defer { - XCTAssertNoThrow(try connection.close().wait()) - } - - let echo = Echo_EchoNIOClient(channel: connection) - // Make an RPC to trigger channel creation. - let get = echo.get(.with { $0.text = "Hello!" }) - XCTAssertTrue(try get.status.map { $0.isOk }.wait()) - - // Check the initializers were called. - XCTAssertNoThrow(try clientDebugInitializerCalled.futureResult.wait()) - XCTAssertNoThrow(try serverDebugInitializerCalled.futureResult.wait()) - } -} diff --git a/Tests/GRPCTests/DelegatingErrorHandlerTests.swift b/Tests/GRPCTests/DelegatingErrorHandlerTests.swift deleted file mode 100644 index 71fa4b7b3..000000000 --- a/Tests/GRPCTests/DelegatingErrorHandlerTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import Foundation -@testable import GRPC -import Logging -import NIOCore -import NIOEmbedded -import NIOSSL -import XCTest - -class DelegatingErrorHandlerTests: GRPCTestCase { - final class ErrorRecorder: ClientErrorDelegate { - var errors: [Error] = [] - - init() {} - - func didCatchError(_ error: Error, logger: Logger, file: StaticString, line: Int) { - self.errors.append(error) - } - } - - func testUncleanShutdownIsIgnored() throws { - let delegate = ErrorRecorder() - let channel = - EmbeddedChannel(handler: DelegatingErrorHandler(logger: self.logger, delegate: delegate)) - channel.pipeline.fireErrorCaught(NIOSSLError.uncleanShutdown) - channel.pipeline.fireErrorCaught(NIOSSLError.writeDuringTLSShutdown) - - XCTAssertEqual(delegate.errors.count, 1) - XCTAssertEqual(delegate.errors[0] as? NIOSSLError, .writeDuringTLSShutdown) - } -} - -// Unchecked because the error recorder is only ever used in the context of an EmbeddedChannel. -extension DelegatingErrorHandlerTests.ErrorRecorder: @unchecked Sendable {} -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/EchoHelpers/EchoMessageHelpers.swift b/Tests/GRPCTests/EchoHelpers/EchoMessageHelpers.swift deleted file mode 100644 index e4a087cf2..000000000 --- a/Tests/GRPCTests/EchoHelpers/EchoMessageHelpers.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 - -extension Echo_EchoRequest { - internal static let empty: Self = .init() -} - -extension Echo_EchoResponse { - internal static let empty: Self = .init() -} diff --git a/Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift b/Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift deleted file mode 100644 index 26ddb2deb..000000000 --- a/Tests/GRPCTests/EchoHelpers/Interceptors/DelegatingClientInterceptor.swift +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 SwiftProtobuf - -/// A client interceptor which delegates the implementation of `send` and `receive` to callbacks. -final class DelegatingClientInterceptor< - Request: Message, - Response: Message ->: ClientInterceptor, @unchecked Sendable { - typealias RequestPart = GRPCClientRequestPart - typealias ResponsePart = GRPCClientResponsePart - typealias Context = ClientInterceptorContext - typealias OnSend = (RequestPart, EventLoopPromise?, Context) -> Void - typealias OnReceive = (ResponsePart, Context) -> Void - - private let onSend: OnSend - private let onReceive: OnReceive - - init( - onSend: @escaping OnSend = { part, promise, context in context.send(part, promise: promise) }, - onReceive: @escaping OnReceive = { part, context in context.receive(part) } - ) { - self.onSend = onSend - self.onReceive = onReceive - } - - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - self.onSend(part, promise, context) - } - - override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - self.onReceive(part, context) - } -} - -final class DelegatingEchoClientInterceptorFactory: Echo_EchoClientInterceptorFactoryProtocol { - typealias OnSend = DelegatingClientInterceptor.OnSend - let interceptor: DelegatingClientInterceptor - - init(onSend: @escaping OnSend) { - self.interceptor = DelegatingClientInterceptor(onSend: onSend) - } - - func makeGetInterceptors() -> [ClientInterceptor] { - return [self.interceptor] - } - - func makeExpandInterceptors() -> [ClientInterceptor] { - return [self.interceptor] - } - - func makeCollectInterceptors() -> [ClientInterceptor] { - return [self.interceptor] - } - - func makeUpdateInterceptors() -> [ClientInterceptor] { - return [self.interceptor] - } -} diff --git a/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift b/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift deleted file mode 100644 index 907ea783a..000000000 --- a/Tests/GRPCTests/EchoHelpers/Interceptors/EchoInterceptorFactories.swift +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 - -// MARK: - Client - -internal final class EchoClientInterceptors: Echo_EchoClientInterceptorFactoryProtocol { - typealias Factory = @Sendable () -> ClientInterceptor - private let factories: [Factory] - - internal init(_ factories: Factory...) { - self.factories = factories - } - - private func makeInterceptors() -> [ClientInterceptor] { - return self.factories.map { $0() } - } - - func makeGetInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } - - func makeExpandInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } - - func makeCollectInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } - - func makeUpdateInterceptors() -> [ClientInterceptor] { - return self.makeInterceptors() - } -} - -// MARK: - Server - -internal final class EchoServerInterceptors: Echo_EchoServerInterceptorFactoryProtocol { - typealias Factory = @Sendable () -> ServerInterceptor - private let factories: [Factory] - - internal init(_ factories: Factory...) { - self.factories = factories - } - - private func makeInterceptors() -> [ServerInterceptor] { - return self.factories.map { $0() } - } - - func makeGetInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } - - func makeExpandInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } - - func makeCollectInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } - - func makeUpdateInterceptors() -> [ServerInterceptor] { - return self.makeInterceptors() - } -} diff --git a/Tests/GRPCTests/EchoHelpers/Providers/DelegatingOnCloseEchoProvider.swift b/Tests/GRPCTests/EchoHelpers/Providers/DelegatingOnCloseEchoProvider.swift deleted file mode 100644 index 1b21dbcf1..000000000 --- a/Tests/GRPCTests/EchoHelpers/Providers/DelegatingOnCloseEchoProvider.swift +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 - -/// An `Echo_EchoProvider` which sets `onClose` for each RPC and then calls a delegate to provide -/// the RPC implementation. -class OnCloseEchoProvider: Echo_EchoProvider { - let interceptors: Echo_EchoServerInterceptorFactoryProtocol? - - let onClose: (Result) -> Void - let delegate: Echo_EchoProvider - - init( - delegate: Echo_EchoProvider, - interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil, - onClose: @escaping (Result) -> Void - ) { - self.delegate = delegate - self.onClose = onClose - self.interceptors = interceptors - } - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - context.closeFuture.whenComplete(self.onClose) - return self.delegate.get(request: request, context: context) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - context.closeFuture.whenComplete(self.onClose) - return self.delegate.expand(request: request, context: context) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - context.closeFuture.whenComplete(self.onClose) - return self.delegate.collect(context: context) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - context.closeFuture.whenComplete(self.onClose) - return self.delegate.update(context: context) - } -} diff --git a/Tests/GRPCTests/EchoHelpers/Providers/FailingEchoProvider.swift b/Tests/GRPCTests/EchoHelpers/Providers/FailingEchoProvider.swift deleted file mode 100644 index f6664ab78..000000000 --- a/Tests/GRPCTests/EchoHelpers/Providers/FailingEchoProvider.swift +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 - -/// An `Echo_EchoProvider` which always returns failed future for each RPC. -class FailingEchoProvider: Echo_EchoProvider { - let interceptors: Echo_EchoServerInterceptorFactoryProtocol? - - init(interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil) { - self.interceptors = interceptors - } - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } -} diff --git a/Tests/GRPCTests/EchoHelpers/Providers/MetadataEchoProvider.swift b/Tests/GRPCTests/EchoHelpers/Providers/MetadataEchoProvider.swift deleted file mode 100644 index 2b39df4e1..000000000 --- a/Tests/GRPCTests/EchoHelpers/Providers/MetadataEchoProvider.swift +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 { - 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 - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unimplemented)) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unimplemented)) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unimplemented)) - } -} diff --git a/Tests/GRPCTests/EchoHelpers/Providers/NeverResolvingEchoProvider.swift b/Tests/GRPCTests/EchoHelpers/Providers/NeverResolvingEchoProvider.swift deleted file mode 100644 index ef91efc70..000000000 --- a/Tests/GRPCTests/EchoHelpers/Providers/NeverResolvingEchoProvider.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 - -/// An `Echo_EchoProvider` which returns a failed future for each RPC which resolves in the distant -/// future. -class NeverResolvingEchoProvider: Echo_EchoProvider { - let interceptors: Echo_EchoServerInterceptorFactoryProtocol? - - init(interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil) { - self.interceptors = interceptors - } - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return context.eventLoop.scheduleTask(deadline: .distantFuture) { - throw GRPCStatus.processingError - }.futureResult - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return context.eventLoop.scheduleTask(deadline: .distantFuture) { - throw GRPCStatus.processingError - }.futureResult - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.scheduleTask(deadline: .distantFuture) { - throw GRPCStatus.processingError - }.futureResult - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.scheduleTask(deadline: .distantFuture) { - throw GRPCStatus.processingError - }.futureResult - } -} diff --git a/Tests/GRPCTests/EchoMetadataTests.swift b/Tests/GRPCTests/EchoMetadataTests.swift deleted file mode 100644 index 510222ab9..000000000 --- a/Tests/GRPCTests/EchoMetadataTests.swift +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 XCTest - -internal final class EchoMetadataTests: GRPCTestCase { - private func testServiceDescriptor(_ description: GRPCServiceDescriptor) { - XCTAssertEqual(description.name, "Echo") - XCTAssertEqual(description.fullName, "echo.Echo") - - XCTAssertEqual(description.methods.count, 4) - - if let get = description.methods.first(where: { $0.name == "Get" }) { - self._testGet(get) - } else { - XCTFail("No 'Get' method found") - } - - if let collect = description.methods.first(where: { $0.name == "Collect" }) { - self._testCollect(collect) - } else { - XCTFail("No 'Collect' method found") - } - - if let expand = description.methods.first(where: { $0.name == "Expand" }) { - self._testExpand(expand) - } else { - XCTFail("No 'Expand' method found") - } - - if let update = description.methods.first(where: { $0.name == "Update" }) { - self._testUpdate(update) - } else { - XCTFail("No 'Update' method found") - } - } - - private func _testGet(_ description: GRPCMethodDescriptor) { - XCTAssertEqual(description.name, "Get") - XCTAssertEqual(description.fullName, "echo.Echo/Get") - XCTAssertEqual(description.path, "/echo.Echo/Get") - XCTAssertEqual(description.type, .unary) - } - - private func _testCollect(_ description: GRPCMethodDescriptor) { - XCTAssertEqual(description.name, "Collect") - XCTAssertEqual(description.fullName, "echo.Echo/Collect") - XCTAssertEqual(description.path, "/echo.Echo/Collect") - XCTAssertEqual(description.type, .clientStreaming) - } - - private func _testExpand(_ description: GRPCMethodDescriptor) { - XCTAssertEqual(description.name, "Expand") - XCTAssertEqual(description.fullName, "echo.Echo/Expand") - XCTAssertEqual(description.path, "/echo.Echo/Expand") - XCTAssertEqual(description.type, .serverStreaming) - } - - private func _testUpdate(_ description: GRPCMethodDescriptor) { - XCTAssertEqual(description.name, "Update") - XCTAssertEqual(description.fullName, "echo.Echo/Update") - XCTAssertEqual(description.path, "/echo.Echo/Update") - XCTAssertEqual(description.type, .bidirectionalStreaming) - } - - func testServiceDescriptor() { - self.testServiceDescriptor(Echo_EchoClientMetadata.serviceDescriptor) - self.testServiceDescriptor(Echo_EchoServerMetadata.serviceDescriptor) - - if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { - self.testServiceDescriptor(Echo_EchoAsyncClient.serviceDescriptor) - } - } - - func testGet() { - self._testGet(Echo_EchoClientMetadata.Methods.get) - self._testGet(Echo_EchoServerMetadata.Methods.get) - } - - func testCollect() { - self._testCollect(Echo_EchoClientMetadata.Methods.collect) - self._testCollect(Echo_EchoServerMetadata.Methods.collect) - } - - func testExpand() { - self._testExpand(Echo_EchoClientMetadata.Methods.expand) - self._testExpand(Echo_EchoServerMetadata.Methods.expand) - } - - func testUpdate() { - self._testUpdate(Echo_EchoClientMetadata.Methods.update) - self._testUpdate(Echo_EchoServerMetadata.Methods.update) - } -} diff --git a/Tests/GRPCTests/EchoTestClientTests.swift b/Tests/GRPCTests/EchoTestClientTests.swift deleted file mode 100644 index d8373ebc7..000000000 --- a/Tests/GRPCTests/EchoTestClientTests.swift +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import GRPC -import NIOCore -import NIOPosix -import XCTest - -/// An example model using a generated client for the 'Echo' service. -/// -/// This demonstrates how one might extract a generated client into a component which could be -/// backed by a real or fake client. -class EchoModel { - private let client: Echo_EchoClientProtocol - - init(client: Echo_EchoClientProtocol) { - self.client = client - } - - /// Call 'get' with the given word and call the `callback` with the result. - func getWord(_ text: String, _ callback: @escaping (Result) -> Void) { - let get = self.client.get(.with { $0.text = text }) - get.response.whenComplete { result in - switch result { - case let .success(response): - callback(.success(response.text)) - case let .failure(error): - callback(.failure(error)) - } - } - } - - /// Call 'update' with the given words. Call `onResponse` for each response and then `onEnd` when - /// the RPC has completed. - func updateWords( - _ words: [String], - onResponse: @escaping (String) -> Void, - onEnd: @escaping (GRPCStatus) -> Void - ) { - let update = self.client.update { response in - onResponse(response.text) - } - - update.status.whenSuccess { status in - onEnd(status) - } - - update.sendMessages(words.map { word in .with { $0.text = word } }, promise: nil) - update.sendEnd(promise: nil) - } -} - -class EchoTestClientTests: GRPCTestCase { - private var group: MultiThreadedEventLoopGroup? - private var server: Server? - private var channel: ClientConnection? - - private func setUpServerAndChannel() throws -> ClientConnection { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.group = group - - let server = try Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - - self.server = server - - let channel = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "127.0.0.1", port: server.channel.localAddress!.port!) - - self.channel = channel - - return channel - } - - override func tearDown() { - if let channel = self.channel { - XCTAssertNoThrow(try channel.close().wait()) - } - if let server = self.server { - XCTAssertNoThrow(try server.close().wait()) - } - if let group = self.group { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - super.tearDown() - } - - @available(swift, deprecated: 5.6) - func testGetWithTestClient() { - let client = Echo_EchoTestClient(defaultCallOptions: self.callOptionsWithLogger) - let model = EchoModel(client: client) - - let completed = self.expectation(description: "'Get' completed") - - // Enqueue a response for the next call to Get. - client.enqueueGetResponse(.with { $0.text = "Expected response" }) - - model.getWord("Hello") { result in - switch result { - case let .success(text): - XCTAssertEqual(text, "Expected response") - case let .failure(error): - XCTFail("Unexpected error \(error)") - } - - completed.fulfill() - } - - self.wait(for: [completed], timeout: 10.0) - } - - func testGetWithRealClientAndServer() throws { - let channel = try self.setUpServerAndChannel() - let client = Echo_EchoNIOClient( - channel: channel, - defaultCallOptions: self.callOptionsWithLogger - ) - let model = EchoModel(client: client) - - let completed = self.expectation(description: "'Get' completed") - - model.getWord("Hello") { result in - switch result { - case let .success(text): - XCTAssertEqual(text, "Swift echo get: Hello") - case let .failure(error): - XCTFail("Unexpected error \(error)") - } - - completed.fulfill() - } - - self.wait(for: [completed], timeout: 10.0) - } - - @available(swift, deprecated: 5.6) - func testUpdateWithTestClient() { - let client = Echo_EchoTestClient(defaultCallOptions: self.callOptionsWithLogger) - let model = EchoModel(client: client) - - let completed = self.expectation(description: "'Update' completed") - let responses = self.expectation(description: "Received responses") - responses.expectedFulfillmentCount = 3 - - // Create a response stream for 'Update'. - let stream = client.makeUpdateResponseStream() - - model.updateWords( - ["foo", "bar", "baz"], - onResponse: { response in - XCTAssertEqual(response, "Expected response") - responses.fulfill() - }, - onEnd: { status in - XCTAssertEqual(status.code, .ok) - completed.fulfill() - } - ) - - // Send some responses: - XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = "Expected response" })) - XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = "Expected response" })) - XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = "Expected response" })) - XCTAssertNoThrow(try stream.sendEnd()) - - self.wait(for: [responses, completed], timeout: 10.0) - } - - func testUpdateWithRealClientAndServer() throws { - let channel = try self.setUpServerAndChannel() - let client = Echo_EchoNIOClient( - channel: channel, - defaultCallOptions: self.callOptionsWithLogger - ) - let model = EchoModel(client: client) - - let completed = self.expectation(description: "'Update' completed") - let responses = self.expectation(description: "Received responses") - responses.expectedFulfillmentCount = 3 - - model.updateWords( - ["foo", "bar", "baz"], - onResponse: { response in - XCTAssertTrue(response.hasPrefix("Swift echo update")) - responses.fulfill() - }, - onEnd: { status in - XCTAssertEqual(status.code, .ok) - completed.fulfill() - } - ) - - self.wait(for: [responses, completed], timeout: 10.0) - } -} diff --git a/Tests/GRPCTests/ErrorRecordingDelegate.swift b/Tests/GRPCTests/ErrorRecordingDelegate.swift deleted file mode 100644 index 2991ad26a..000000000 --- a/Tests/GRPCTests/ErrorRecordingDelegate.swift +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 GRPC -import Logging -import NIOConcurrencyHelpers -import XCTest - -// Unchecked as all mutable state is accessed and modified behind a lock. -extension ErrorRecordingDelegate: @unchecked Sendable {} - -final class ErrorRecordingDelegate: ClientErrorDelegate { - private let lock: NIOLock - private var _errors: [Error] = [] - - internal var errors: [Error] { - return self.lock.withLock { - return self._errors - } - } - - var expectation: XCTestExpectation - - init(expectation: XCTestExpectation) { - self.expectation = expectation - self.lock = NIOLock() - } - - func didCatchError(_ error: Error, logger: Logger, file: StaticString, line: Int) { - self.lock.withLock { - self._errors.append(error) - } - self.expectation.fulfill() - } -} - -class ServerErrorRecordingDelegate: ServerErrorDelegate { - var errors: [Error] = [] - var expectation: XCTestExpectation - - init(expectation: XCTestExpectation) { - self.expectation = expectation - } - - func observeLibraryError(_ error: Error) { - self.errors.append(error) - self.expectation.fulfill() - } -} diff --git a/Tests/GRPCTests/EventLoopFuture+Assertions.swift b/Tests/GRPCTests/EventLoopFuture+Assertions.swift deleted file mode 100644 index 4678b4ac8..000000000 --- a/Tests/GRPCTests/EventLoopFuture+Assertions.swift +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 Foundation -import NIOCore -import XCTest - -extension EventLoopFuture where Value: Equatable { - /// Registers a callback which asserts the value promised by this future is equal to - /// the expected value. Causes a test failure if the future returns an error. - /// - /// - Parameters: - /// - expected: The expected value. - /// - expectation: A test expectation to fulfill once the future has completed. - func assertEqual( - _ expected: Value, - fulfill expectation: XCTestExpectation, - file: StaticString = #filePath, - line: UInt = #line - ) { - self.whenComplete { result in - defer { - expectation.fulfill() - } - - switch result { - case let .success(actual): - // swiftformat:disable:next redundantParens - XCTAssertEqual(expected, actual, file: (file), line: line) - - case let .failure(error): - // swiftformat:disable:next redundantParens - XCTFail("Expecteded '\(expected)' but received error: \(error)", file: (file), line: line) - } - } - } -} - -extension EventLoopFuture { - /// Registers a callback which asserts that this future is fulfilled with an error. Causes a test - /// failure if the future is not fulfilled with an error. - /// - /// Callers can additionally verify the error by providing an error handler. - /// - /// - Parameters: - /// - expectation: A test expectation to fulfill once the future has completed. - /// - handler: A block to run additional verification on the error. Defaults to no-op. - func assertError( - fulfill expectation: XCTestExpectation, - file: StaticString = #filePath, - line: UInt = #line, - handler: @escaping (Error) -> Void = { _ in } - ) { - self.whenComplete { result in - defer { - expectation.fulfill() - } - - switch result { - case .success: - // swiftformat:disable:next redundantParens - XCTFail("Unexpectedly received \(Value.self), expected an error", file: (file), line: line) - - case let .failure(error): - handler(error) - } - } - } - - /// Registers a callback which fulfills an expectation when the future succeeds. - /// - /// - Parameter expectation: The expectation to fulfill. - func assertSuccess( - fulfill expectation: XCTestExpectation, - file: StaticString = #filePath, - line: UInt = #line - ) { - self.whenSuccess { _ in - expectation.fulfill() - } - } -} - -extension EventLoopFuture { - // TODO: Replace with `always` once https://github.com/apple/swift-nio/pull/981 is released. - func peekError(callback: @escaping (Error) -> Void) -> EventLoopFuture { - self.whenFailure(callback) - return self - } - - // TODO: Replace with `always` once https://github.com/apple/swift-nio/pull/981 is released. - func peek(callback: @escaping (Value) -> Void) -> EventLoopFuture { - self.whenSuccess(callback) - return self - } -} diff --git a/Tests/GRPCTests/FakeChannelTests.swift b/Tests/GRPCTests/FakeChannelTests.swift deleted file mode 100644 index 57bb65de4..000000000 --- a/Tests/GRPCTests/FakeChannelTests.swift +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2020, 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 XCTest - -@available(swift, deprecated: 5.6) -class FakeChannelTests: GRPCTestCase { - typealias Request = Echo_EchoRequest - typealias Response = Echo_EchoResponse - - var channel: FakeChannel! - - override func setUp() { - super.setUp() - self.channel = FakeChannel(logger: self.clientLogger) - } - - private func makeUnaryResponse( - path: String = "/foo/bar", - requestHandler: @escaping (FakeRequestPart) -> Void = { _ in } - ) -> FakeUnaryResponse { - return self.channel.makeFakeUnaryResponse(path: path, requestHandler: requestHandler) - } - - private func makeStreamingResponse( - path: String = "/foo/bar", - requestHandler: @escaping (FakeRequestPart) -> Void = { _ in } - ) -> FakeStreamingResponse { - return self.channel.makeFakeStreamingResponse(path: path, requestHandler: requestHandler) - } - - private func makeUnaryCall( - request: Request, - path: String = "/foo/bar", - callOptions: CallOptions = CallOptions() - ) -> UnaryCall { - return self.channel.makeUnaryCall(path: path, request: request, callOptions: callOptions) - } - - private func makeBidirectionalStreamingCall( - path: String = "/foo/bar", - callOptions: CallOptions = CallOptions(), - handler: @escaping (Response) -> Void - ) -> BidirectionalStreamingCall { - return self.channel.makeBidirectionalStreamingCall( - path: path, - callOptions: callOptions, - handler: handler - ) - } - - func testUnary() { - let response = self.makeUnaryResponse { part in - switch part { - case let .message(request): - XCTAssertEqual(request, Request.with { $0.text = "Foo" }) - default: - () - } - } - - let call = self.makeUnaryCall(request: .with { $0.text = "Foo" }) - - XCTAssertNoThrow(try response.sendMessage(.with { $0.text = "Bar" })) - XCTAssertEqual(try call.response.wait(), .with { $0.text = "Bar" }) - XCTAssertTrue(try call.status.map { $0.isOk }.wait()) - } - - func testBidirectional() { - final class ResponseCollector { - private(set) var responses = [Response]() - func collect(_ response: Response) { self.responses.append(response) } - } - var requests: [Request] = [] - let response = self.makeStreamingResponse { part in - switch part { - case let .message(request): - requests.append(request) - default: - () - } - } - - var collector = ResponseCollector() - XCTAssertTrue(isKnownUniquelyReferenced(&collector)) - let call = self.makeBidirectionalStreamingCall { [collector] in - collector.collect($0) - } - XCTAssertFalse(isKnownUniquelyReferenced(&collector)) - - XCTAssertNoThrow(try call.sendMessage(.with { $0.text = "1" }).wait()) - XCTAssertNoThrow(try call.sendMessage(.with { $0.text = "2" }).wait()) - XCTAssertNoThrow(try call.sendMessage(.with { $0.text = "3" }).wait()) - XCTAssertNoThrow(try call.sendEnd().wait()) - - XCTAssertEqual(requests, (1 ... 3).map { number in .with { $0.text = "\(number)" } }) - - XCTAssertNoThrow(try response.sendMessage(.with { $0.text = "4" })) - XCTAssertNoThrow(try response.sendMessage(.with { $0.text = "5" })) - XCTAssertNoThrow(try response.sendMessage(.with { $0.text = "6" })) - XCTAssertEqual(collector.responses.count, 3) - XCTAssertFalse(isKnownUniquelyReferenced(&collector)) - XCTAssertNoThrow(try response.sendEnd()) - XCTAssertTrue(isKnownUniquelyReferenced(&collector)) - - XCTAssertEqual(collector.responses, (4 ... 6).map { number in .with { $0.text = "\(number)" } }) - XCTAssertTrue(try call.status.map { $0.isOk }.wait()) - } - - func testMissingResponse() { - let call = self.makeUnaryCall(request: .with { $0.text = "Not going to work" }) - - XCTAssertThrowsError(try call.initialMetadata.wait()) - XCTAssertThrowsError(try call.response.wait()) - XCTAssertThrowsError(try call.trailingMetadata.wait()) - XCTAssertFalse(try call.status.map { $0.isOk }.wait()) - } - - func testResponseIsReallyDequeued() { - let response = self.makeUnaryResponse() - let call = self.makeUnaryCall(request: .with { $0.text = "Ping" }) - - XCTAssertNoThrow(try response.sendMessage(.with { $0.text = "Pong" })) - XCTAssertEqual(try call.response.wait(), .with { $0.text = "Pong" }) - - let failedCall = self.makeUnaryCall(request: .with { $0.text = "Not going to work" }) - XCTAssertThrowsError(try failedCall.initialMetadata.wait()) - XCTAssertThrowsError(try failedCall.response.wait()) - XCTAssertThrowsError(try failedCall.trailingMetadata.wait()) - XCTAssertFalse(try failedCall.status.map { $0.isOk }.wait()) - } - - func testHasResponseStreamsEnqueued() { - XCTAssertFalse(self.channel.hasFakeResponseEnqueued(forPath: "whatever")) - _ = self.makeUnaryResponse(path: "whatever") - XCTAssertTrue(self.channel.hasFakeResponseEnqueued(forPath: "whatever")) - _ = self.makeUnaryCall(request: .init(), path: "whatever") - XCTAssertFalse(self.channel.hasFakeResponseEnqueued(forPath: "whatever")) - } -} diff --git a/Tests/GRPCTests/FakeResponseStreamTests.swift b/Tests/GRPCTests/FakeResponseStreamTests.swift deleted file mode 100644 index 64e615136..000000000 --- a/Tests/GRPCTests/FakeResponseStreamTests.swift +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import NIOHPACK -import XCTest - -@testable import GRPC - -class FakeResponseStreamTests: GRPCTestCase { - private typealias Request = Echo_EchoRequest - private typealias Response = Echo_EchoResponse - - private typealias ResponsePart = _GRPCClientResponsePart - - func testUnarySendMessage() { - let unary = FakeUnaryResponse() - unary.activate() - XCTAssertNoThrow(try unary.sendMessage(.with { $0.text = "foo" })) - - unary.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertInitialMetadata() - } - - unary.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertMessage { - XCTAssertEqual($0, .with { $0.text = "foo" }) - } - } - - unary.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertTrailingMetadata() - } - - unary.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertStatus() - } - } - - func testUnarySendError() { - let unary = FakeUnaryResponse() - unary.activate() - XCTAssertNoThrow(try unary.sendError(GRPCError.RPCNotImplemented(rpc: "uh oh!"))) - - // Expect trailers and then an error. - unary.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertTrailingMetadata() - } - - XCTAssertThrowsError(try unary.channel.throwIfErrorCaught()) - } - - func testUnaryIgnoresExtraMessages() { - let unary = FakeUnaryResponse() - unary.activate() - XCTAssertNoThrow(try unary.sendError(GRPCError.RPCNotImplemented(rpc: "uh oh!"))) - - // Expected. - unary.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertTrailingMetadata() - } - XCTAssertThrowsError(try unary.channel.throwIfErrorCaught()) - - // Send another error; this should on-op. - XCTAssertThrowsError(try unary.sendError(GRPCError.RPCCancelledByClient())) { error in - XCTAssertTrue(error is FakeResponseProtocolViolation) - } - XCTAssertNil(try unary.channel.readInbound(as: ResponsePart.self)) - XCTAssertNoThrow(try unary.channel.throwIfErrorCaught()) - - // Send a message; this should on-op. - XCTAssertThrowsError(try unary.sendMessage(.with { $0.text = "ignored" })) { error in - XCTAssertTrue(error is FakeResponseProtocolViolation) - } - XCTAssertNil(try unary.channel.readInbound(as: ResponsePart.self)) - XCTAssertNoThrow(try unary.channel.throwIfErrorCaught()) - } - - func testStreamingSendMessage() { - let streaming = FakeStreamingResponse() - streaming.activate() - - XCTAssertNoThrow(try streaming.sendMessage(.with { $0.text = "1" })) - XCTAssertNoThrow(try streaming.sendMessage(.with { $0.text = "2" })) - XCTAssertNoThrow(try streaming.sendMessage(.with { $0.text = "3" })) - XCTAssertNoThrow(try streaming.sendEnd()) - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertInitialMetadata() - } - - for expected in ["1", "2", "3"] { - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertMessage { message in - XCTAssertEqual(message, .with { $0.text = expected }) - } - } - } - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertTrailingMetadata() - } - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertStatus() - } - } - - func testStreamingSendInitialMetadata() { - let streaming = FakeStreamingResponse() - streaming.activate() - - XCTAssertNoThrow(try streaming.sendInitialMetadata(["foo": "bar"])) - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertInitialMetadata { metadata in - XCTAssertEqual(metadata, ["foo": "bar"]) - } - } - - // This should be dropped. - XCTAssertThrowsError(try streaming.sendInitialMetadata(["bar": "baz"])) { error in - XCTAssertTrue(error is FakeResponseProtocolViolation) - } - - // Trailers and status. - XCTAssertNoThrow(try streaming.sendEnd(trailingMetadata: ["bar": "foo"])) - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertTrailingMetadata { metadata in - XCTAssertEqual(metadata, ["bar": "foo"]) - } - } - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertStatus() - } - } - - func streamingSendError() { - let streaming = FakeStreamingResponse() - streaming.activate() - - XCTAssertNoThrow(try streaming.sendMessage(.with { $0.text = "1" })) - XCTAssertNoThrow(try streaming.sendError(GRPCError.RPCCancelledByClient())) - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertInitialMetadata() - } - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertMessage { message in - XCTAssertEqual(message, .with { $0.text = "1" }) - } - } - - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertTrailingMetadata() - } - - XCTAssertThrowsError(try streaming.channel.throwIfErrorCaught()) - } - - func testStreamingIgnoresExtraMessages() { - let streaming = FakeStreamingResponse() - streaming.activate() - XCTAssertNoThrow(try streaming.sendError(GRPCError.RPCNotImplemented(rpc: "uh oh!"))) - - // Expected. - streaming.channel.verifyInbound(as: ResponsePart.self) { part in - part.assertTrailingMetadata() - } - XCTAssertThrowsError(try streaming.channel.throwIfErrorCaught()) - - // Send another error; this should on-op. - XCTAssertThrowsError(try streaming.sendError(GRPCError.RPCCancelledByClient())) { error in - XCTAssertTrue(error is FakeResponseProtocolViolation) - } - XCTAssertNil(try streaming.channel.readInbound(as: ResponsePart.self)) - XCTAssertNoThrow(try streaming.channel.throwIfErrorCaught()) - - // Send a message; this should on-op. - XCTAssertThrowsError(try streaming.sendMessage(.with { $0.text = "ignored" })) { error in - XCTAssertTrue(error is FakeResponseProtocolViolation) - } - XCTAssertNil(try streaming.channel.readInbound(as: ResponsePart.self)) - XCTAssertNoThrow(try streaming.channel.throwIfErrorCaught()) - } -} - -extension EmbeddedChannel { - fileprivate func verifyInbound( - as: Inbound.Type = Inbound.self, - _ verify: (Inbound) -> Void = { _ in - } - ) { - do { - if let inbound = try self.readInbound(as: Inbound.self) { - verify(inbound) - } else { - XCTFail("Nothing to read") - } - } catch { - XCTFail("Unable to readInbound: \(error)") - } - } -} - -extension _GRPCClientResponsePart { - fileprivate func assertInitialMetadata(_ verify: (HPACKHeaders) -> Void = { _ in }) { - switch self { - case let .initialMetadata(headers): - verify(headers) - default: - XCTFail("Expected initial metadata but got: \(self)") - } - } - - fileprivate func assertMessage(_ verify: (Response) -> Void = { _ in }) { - switch self { - case let .message(context): - verify(context.message) - default: - XCTFail("Expected message but got: \(self)") - } - } - - fileprivate func assertTrailingMetadata(_ verify: (HPACKHeaders) -> Void = { _ in }) { - switch self { - case let .trailingMetadata(headers): - verify(headers) - default: - XCTFail("Expected trailing metadata but got: \(self)") - } - } - - fileprivate func assertStatus(_ verify: (GRPCStatus) -> Void = { _ in }) { - switch self { - case let .status(status): - verify(status) - default: - XCTFail("Expected status but got: \(self)") - } - } -} diff --git a/Tests/GRPCTests/FunctionalTests.swift b/Tests/GRPCTests/FunctionalTests.swift deleted file mode 100644 index 96b759b69..000000000 --- a/Tests/GRPCTests/FunctionalTests.swift +++ /dev/null @@ -1,569 +0,0 @@ -/* - * 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 Dispatch -import EchoModel -import Foundation -import NIOCore -import NIOHTTP1 -import NIOHTTP2 -import XCTest - -@testable import GRPC - -class FunctionalTestsInsecureTransport: EchoTestCaseBase { - override var transportSecurity: TransportSecurity { - return .none - } - - var aFewStrings: [String] { - return ["foo", "bar", "baz"] - } - - var lotsOfStrings: [String] { - return (0 ..< 500).map { - String(describing: $0) - } - } - - func doTestUnary( - request: Echo_EchoRequest, - expect response: Echo_EchoResponse, - file: StaticString = #filePath, - line: UInt = #line - ) { - let responseExpectation = self.makeResponseExpectation() - let statusExpectation = self.makeStatusExpectation() - - let call = client.get(request) - call.response.assertEqual(response, fulfill: responseExpectation, file: file, line: line) - call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, file: file, line: line) - - self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout) - } - - func doTestUnary(message: String, file: StaticString = #filePath, line: UInt = #line) { - self.doTestUnary( - request: Echo_EchoRequest(text: message), - expect: Echo_EchoResponse(text: "Swift echo get: \(message)"), - file: file, - line: line - ) - } - - func testUnary() throws { - self.doTestUnary(message: "foo") - } - - func testUnaryLotsOfRequests() throws { - guard self.runTimeSensitiveTests() else { return } - - // Sending that many requests at once can sometimes trip things up, it seems. - let clockStart = clock() - let numberOfRequests = 200 - - // Due to https://github.com/apple/swift-nio-http2/issues/87#issuecomment-483542401 we need to - // limit the number of active streams. The default in NIOHTTP2 is 100, so we'll use it too. - // - // In the future we might want to build in some kind of mechanism which handles this for the - // user. - let batchSize = 100 - - // Instead of setting a timeout out on the test we'll set one for each batch, if any of them - // timeout then we'll bail out of the test. - let batchTimeout: TimeInterval = 30.0 - self.continueAfterFailure = false - - for lowerBound in stride(from: 0, to: numberOfRequests, by: batchSize) { - let upperBound = min(lowerBound + batchSize, numberOfRequests) - let numberOfCalls = upperBound - lowerBound - let responseExpectation = - self - .makeResponseExpectation(expectedFulfillmentCount: numberOfCalls) - let statusExpectation = self.makeStatusExpectation(expectedFulfillmentCount: numberOfCalls) - - for i in lowerBound ..< upperBound { - let request = Echo_EchoRequest(text: "foo \(i)") - let response = Echo_EchoResponse(text: "Swift echo get: foo \(i)") - - let get = client.get(request) - get.response.assertEqual(response, fulfill: responseExpectation) - get.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation) - } - - if upperBound % 100 == 0 { - print( - "\(upperBound) requests sent so far, elapsed time: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))" - ) - } - - self.wait(for: [responseExpectation, statusExpectation], timeout: batchTimeout) - } - - print( - "total time to receive \(numberOfRequests) responses: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))" - ) - } - - func testUnaryWithLargeData() throws { - // Default max frame size is: 16,384. We'll exceed this as we also have to send the size and compression flag. - let longMessage = String(repeating: "e", count: 16384) - self.doTestUnary(message: longMessage) - } - - func testUnaryEmptyRequest() throws { - self.doTestUnary( - request: Echo_EchoRequest(), - expect: Echo_EchoResponse(text: "Swift echo get: ") - ) - } - - func doTestClientStreaming( - messages: [String], - file: StaticString = #filePath, - line: UInt = #line - ) throws { - let responseExpectation = self.makeResponseExpectation() - let statusExpectation = self.makeStatusExpectation() - - let call = client.collect(callOptions: CallOptions(timeLimit: .none)) - call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, file: file, line: line) - call.response.assertEqual( - Echo_EchoResponse(text: "Swift echo collect: \(messages.joined(separator: " "))"), - fulfill: responseExpectation - ) - - call.sendMessages(messages.map { .init(text: $0) }, promise: nil) - call.sendEnd(promise: nil) - - self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout) - } - - func testClientStreaming() { - XCTAssertNoThrow(try self.doTestClientStreaming(messages: self.aFewStrings)) - } - - func testClientStreamingLotsOfMessages() throws { - guard self.runTimeSensitiveTests() else { return } - XCTAssertNoThrow(try self.doTestClientStreaming(messages: self.lotsOfStrings)) - } - - private func doTestServerStreaming(messages: [String], line: UInt = #line) throws { - let responseExpectation = self.makeResponseExpectation(expectedFulfillmentCount: messages.count) - let statusExpectation = self.makeStatusExpectation() - - var iterator = messages.enumerated().makeIterator() - let call = client.expand(Echo_EchoRequest(text: messages.joined(separator: " "))) { response in - if let (index, message) = iterator.next() { - XCTAssertEqual( - Echo_EchoResponse(text: "Swift echo expand (\(index)): \(message)"), - response, - line: line - ) - responseExpectation.fulfill() - } else { - XCTFail("Too many responses received", line: line) - } - } - - call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, line: line) - self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout) - } - - func testServerStreaming() { - XCTAssertNoThrow(try self.doTestServerStreaming(messages: self.aFewStrings)) - } - - func testServerStreamingLotsOfMessages() { - guard self.runTimeSensitiveTests() else { return } - XCTAssertNoThrow(try self.doTestServerStreaming(messages: self.lotsOfStrings)) - } - - private func doTestBidirectionalStreaming( - messages: [String], - waitForEachResponse: Bool = false, - line: UInt = #line - ) throws { - let responseExpectation = self.makeResponseExpectation(expectedFulfillmentCount: messages.count) - let statusExpectation = self.makeStatusExpectation() - - let responseReceived = waitForEachResponse ? DispatchSemaphore(value: 0) : nil - - var iterator = messages.enumerated().makeIterator() - let call = client.update { response in - if let (index, message) = iterator.next() { - XCTAssertEqual( - Echo_EchoResponse(text: "Swift echo update (\(index)): \(message)"), - response, - line: line - ) - responseExpectation.fulfill() - responseReceived?.signal() - } else { - XCTFail("Too many responses received", line: line) - } - } - - call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, line: line) - - messages.forEach { part in - call.sendMessage(Echo_EchoRequest(text: part), promise: nil) - XCTAssertNotEqual( - responseReceived?.wait(timeout: .now() + .seconds(30)), - .some(.timedOut), - line: line - ) - } - call.sendEnd(promise: nil) - - self.wait(for: [responseExpectation, statusExpectation], timeout: self.defaultTestTimeout) - } - - func testBidirectionalStreamingBatched() throws { - XCTAssertNoThrow(try self.doTestBidirectionalStreaming(messages: self.aFewStrings)) - } - - func testBidirectionalStreamingPingPong() throws { - XCTAssertNoThrow( - try self - .doTestBidirectionalStreaming(messages: self.aFewStrings, waitForEachResponse: true) - ) - } - - func testBidirectionalStreamingLotsOfMessagesBatched() throws { - guard self.runTimeSensitiveTests() else { return } - XCTAssertNoThrow(try self.doTestBidirectionalStreaming(messages: self.lotsOfStrings)) - } - - func testBidirectionalStreamingLotsOfMessagesPingPong() throws { - guard self.runTimeSensitiveTests() else { return } - XCTAssertNoThrow( - try self - .doTestBidirectionalStreaming(messages: self.lotsOfStrings, waitForEachResponse: true) - ) - } -} - -#if canImport(NIOSSL) -class FunctionalTestsAnonymousClient: FunctionalTestsInsecureTransport { - override var transportSecurity: TransportSecurity { - return .anonymousClient - } - - override func testUnary() throws { - try super.testUnary() - } - - override func testUnaryLotsOfRequests() throws { - try super.testUnaryLotsOfRequests() - } - - override func testUnaryWithLargeData() throws { - try super.testUnaryWithLargeData() - } - - override func testUnaryEmptyRequest() throws { - try super.testUnaryEmptyRequest() - } - - override func testClientStreaming() { - super.testClientStreaming() - } - - override func testClientStreamingLotsOfMessages() throws { - try super.testClientStreamingLotsOfMessages() - } - - override func testServerStreaming() { - super.testServerStreaming() - } - - override func testServerStreamingLotsOfMessages() { - super.testServerStreamingLotsOfMessages() - } - - override func testBidirectionalStreamingBatched() throws { - try super.testBidirectionalStreamingBatched() - } - - override func testBidirectionalStreamingPingPong() throws { - try super.testBidirectionalStreamingPingPong() - } - - override func testBidirectionalStreamingLotsOfMessagesBatched() throws { - try super.testBidirectionalStreamingLotsOfMessagesBatched() - } - - override func testBidirectionalStreamingLotsOfMessagesPingPong() throws { - try super.testBidirectionalStreamingLotsOfMessagesPingPong() - } -} - -class FunctionalTestsMutualAuthentication: FunctionalTestsInsecureTransport { - override var transportSecurity: TransportSecurity { - return .mutualAuthentication - } - - override func testUnary() throws { - try super.testUnary() - } - - override func testUnaryLotsOfRequests() throws { - try super.testUnaryLotsOfRequests() - } - - override func testUnaryWithLargeData() throws { - try super.testUnaryWithLargeData() - } - - override func testUnaryEmptyRequest() throws { - try super.testUnaryEmptyRequest() - } - - override func testClientStreaming() { - super.testClientStreaming() - } - - override func testClientStreamingLotsOfMessages() throws { - try super.testClientStreamingLotsOfMessages() - } - - override func testServerStreaming() { - super.testServerStreaming() - } - - override func testServerStreamingLotsOfMessages() { - super.testServerStreamingLotsOfMessages() - } - - override func testBidirectionalStreamingBatched() throws { - try super.testBidirectionalStreamingBatched() - } - - override func testBidirectionalStreamingPingPong() throws { - try super.testBidirectionalStreamingPingPong() - } - - override func testBidirectionalStreamingLotsOfMessagesBatched() throws { - try super.testBidirectionalStreamingLotsOfMessagesBatched() - } - - override func testBidirectionalStreamingLotsOfMessagesPingPong() throws { - try super.testBidirectionalStreamingLotsOfMessagesPingPong() - } -} -#endif // canImport(NIOSSL) - -// 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(macOS 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 - } -} - -#if canImport(NIOSSL) -@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) -class FunctionalTestsAnonymousClientNIOTS: FunctionalTestsInsecureTransportNIOTS { - override var transportSecurity: TransportSecurity { - return .anonymousClient - } - - override func testUnary() throws { - try super.testUnary() - } - - override func testUnaryLotsOfRequests() throws { - try super.testUnaryLotsOfRequests() - } - - override func testUnaryWithLargeData() throws { - try super.testUnaryWithLargeData() - } - - override func testUnaryEmptyRequest() throws { - try super.testUnaryEmptyRequest() - } - - override func testClientStreaming() { - super.testClientStreaming() - } - - override func testClientStreamingLotsOfMessages() throws { - try super.testClientStreamingLotsOfMessages() - } - - override func testServerStreaming() { - super.testServerStreaming() - } - - override func testServerStreamingLotsOfMessages() { - super.testServerStreamingLotsOfMessages() - } - - override func testBidirectionalStreamingBatched() throws { - try super.testBidirectionalStreamingBatched() - } - - override func testBidirectionalStreamingPingPong() throws { - try super.testBidirectionalStreamingPingPong() - } - - override func testBidirectionalStreamingLotsOfMessagesBatched() throws { - try super.testBidirectionalStreamingLotsOfMessagesBatched() - } - - override func testBidirectionalStreamingLotsOfMessagesPingPong() throws { - try super.testBidirectionalStreamingLotsOfMessagesPingPong() - } -} - -@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) -class FunctionalTestsMutualAuthenticationNIOTS: FunctionalTestsInsecureTransportNIOTS { - override var transportSecurity: TransportSecurity { - return .mutualAuthentication - } - - override func testUnary() throws { - try super.testUnary() - } - - override func testUnaryLotsOfRequests() throws { - try super.testUnaryLotsOfRequests() - } - - override func testUnaryWithLargeData() throws { - try super.testUnaryWithLargeData() - } - - override func testUnaryEmptyRequest() throws { - try super.testUnaryEmptyRequest() - } - - override func testClientStreaming() { - super.testClientStreaming() - } - - override func testClientStreamingLotsOfMessages() throws { - try super.testClientStreamingLotsOfMessages() - } - - override func testServerStreaming() { - super.testServerStreaming() - } - - override func testServerStreamingLotsOfMessages() { - super.testServerStreamingLotsOfMessages() - } - - override func testBidirectionalStreamingBatched() throws { - try super.testBidirectionalStreamingBatched() - } - - override func testBidirectionalStreamingPingPong() throws { - try super.testBidirectionalStreamingPingPong() - } - - override func testBidirectionalStreamingLotsOfMessagesBatched() throws { - try super.testBidirectionalStreamingLotsOfMessagesBatched() - } - - override func testBidirectionalStreamingLotsOfMessagesPingPong() throws { - try super.testBidirectionalStreamingLotsOfMessagesPingPong() - } -} -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift b/Tests/GRPCTests/GRPCAsyncClientCallTests.swift deleted file mode 100644 index e74fa2c8d..000000000 --- a/Tests/GRPCTests/GRPCAsyncClientCallTests.swift +++ /dev/null @@ -1,396 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import NIOHPACK -import NIOPosix -import XCTest - -@testable import GRPC - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -class GRPCAsyncClientCallTests: GRPCTestCase { - private var group: MultiThreadedEventLoopGroup? - private var server: Server? - private var channel: ClientConnection? - - private static let OKInitialMetadata = HPACKHeaders([ - (":status", "200"), - ("content-type", "application/grpc"), - ]) - - private static let OKTrailingMetadata = HPACKHeaders([ - ("grpc-status", "0") - ]) - - private func setUpServerAndChannel( - service: CallHandlerProvider = EchoProvider() - ) throws -> ClientConnection { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.group = group - - let server = try Server.insecure(group: group) - .withServiceProviders([service]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - - self.server = server - - let channel = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "127.0.0.1", port: server.channel.localAddress!.port!) - - self.channel = channel - - return channel - } - - override func tearDown() { - if let channel = self.channel { - XCTAssertNoThrow(try channel.close().wait()) - } - if let server = self.server { - XCTAssertNoThrow(try server.close().wait()) - } - if let group = self.group { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - super.tearDown() - } - - func testAsyncUnaryCall() async throws { - let channel = try self.setUpServerAndChannel() - let get: GRPCAsyncUnaryCall = channel.makeAsyncUnaryCall( - path: "/echo.Echo/Get", - request: .with { $0.text = "holt" }, - callOptions: .init() - ) - - await assertThat(try await get.initialMetadata, .is(.equalTo(Self.OKInitialMetadata))) - await assertThat(try await get.response, .doesNotThrow()) - await assertThat(try await get.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) - await assertThat(await get.status, .hasCode(.ok)) - print(try await get.trailingMetadata) - } - - func testAsyncClientStreamingCall() async throws { - let channel = try self.setUpServerAndChannel() - let collect: GRPCAsyncClientStreamingCall = - channel - .makeAsyncClientStreamingCall( - path: "/echo.Echo/Collect", - callOptions: .init() - ) - - for word in ["boyle", "jeffers", "holt"] { - try await collect.requestStream.send(.with { $0.text = word }) - } - collect.requestStream.finish() - - await assertThat(try await collect.initialMetadata, .is(.equalTo(Self.OKInitialMetadata))) - await assertThat(try await collect.response, .doesNotThrow()) - await assertThat(try await collect.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) - await assertThat(await collect.status, .hasCode(.ok)) - } - - func testAsyncServerStreamingCall() async throws { - let channel = try self.setUpServerAndChannel() - let expand: GRPCAsyncServerStreamingCall = - channel - .makeAsyncServerStreamingCall( - path: "/echo.Echo/Expand", - request: .with { $0.text = "boyle jeffers holt" }, - callOptions: .init() - ) - - await assertThat(try await expand.initialMetadata, .is(.equalTo(Self.OKInitialMetadata))) - - let numResponses = try await expand.responseStream.map { _ in 1 }.reduce(0, +) - - await assertThat(numResponses, .is(.equalTo(3))) - await assertThat(try await expand.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) - await assertThat(await expand.status, .hasCode(.ok)) - } - - func testAsyncBidirectionalStreamingCall() async throws { - let channel = try self.setUpServerAndChannel() - let update: GRPCAsyncBidirectionalStreamingCall = - channel - .makeAsyncBidirectionalStreamingCall( - path: "/echo.Echo/Update", - callOptions: .init() - ) - - let requests = ["boyle", "jeffers", "holt"] - .map { word in Echo_EchoRequest.with { $0.text = word } } - for request in requests { - try await update.requestStream.send(request) - } - try await update.requestStream.send(requests) - update.requestStream.finish() - - let numResponses = try await update.responseStream.map { _ in 1 }.reduce(0, +) - - await assertThat(numResponses, .is(.equalTo(6))) - await assertThat(try await update.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) - await assertThat(await update.status, .hasCode(.ok)) - } - - func testAsyncBidirectionalStreamingCall_InterleavedRequestsAndResponses() async throws { - let channel = try self.setUpServerAndChannel() - let update: GRPCAsyncBidirectionalStreamingCall = - channel - .makeAsyncBidirectionalStreamingCall( - path: "/echo.Echo/Update", - callOptions: .init() - ) - - await assertThat(try await update.initialMetadata, .is(.equalTo(Self.OKInitialMetadata))) - - var responseStreamIterator = update.responseStream.makeAsyncIterator() - for word in ["boyle", "jeffers", "holt"] { - try await update.requestStream.send(.with { $0.text = word }) - await assertThat(try await responseStreamIterator.next(), .is(.some())) - } - - update.requestStream.finish() - - await assertThat(try await responseStreamIterator.next(), .is(.none())) - - await assertThat(try await update.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) - await assertThat(await update.status, .hasCode(.ok)) - } - - func testAsyncBidirectionalStreamingCall_ConcurrentTasks() async throws { - let channel = try self.setUpServerAndChannel() - let update: GRPCAsyncBidirectionalStreamingCall = - channel - .makeAsyncBidirectionalStreamingCall( - path: "/echo.Echo/Update", - callOptions: .init() - ) - - await assertThat(try await update.initialMetadata, .is(.equalTo(Self.OKInitialMetadata))) - - let counter = RequestResponseCounter() - - // Send the requests and get responses in separate concurrent tasks and await the group. - _ = await withThrowingTaskGroup(of: Void.self) { taskGroup in - // Send requests, then end, in a task. - taskGroup.addTask { - for word in ["boyle", "jeffers", "holt"] { - try await update.requestStream.send(.with { $0.text = word }) - await counter.incrementRequests() - } - update.requestStream.finish() - } - // Get responses in a separate task. - taskGroup.addTask { - for try await _ in update.responseStream { - await counter.incrementResponses() - } - } - } - - await assertThat(await counter.numRequests, .is(.equalTo(3))) - await assertThat(await counter.numResponses, .is(.equalTo(3))) - await assertThat(try await update.trailingMetadata, .is(.equalTo(Self.OKTrailingMetadata))) - await assertThat(await update.status, .hasCode(.ok)) - } - - func testExplicitAcceptUnary(twice: Bool, function: String = #function) async throws { - let headers: HPACKHeaders = ["fn": function] - let channel = try self.setUpServerAndChannel( - service: AsyncEchoProvider(headers: headers, sendTwice: twice) - ) - let echo = Echo_EchoAsyncClient(channel: channel) - let call = echo.makeGetCall(.with { $0.text = "" }) - let responseHeaders = try await call.initialMetadata - XCTAssertEqual(responseHeaders.first(name: "fn"), function) - let status = await call.status - XCTAssertEqual(status.code, .ok) - } - - func testExplicitAcceptUnary() async throws { - try await self.testExplicitAcceptUnary(twice: false) - } - - func testExplicitAcceptTwiceUnary() async throws { - try await self.testExplicitAcceptUnary(twice: true) - } - - func testExplicitAcceptClientStreaming(twice: Bool, function: String = #function) async throws { - let headers: HPACKHeaders = ["fn": function] - let channel = try self.setUpServerAndChannel( - service: AsyncEchoProvider(headers: headers, sendTwice: twice) - ) - let echo = Echo_EchoAsyncClient(channel: channel) - let call = echo.makeCollectCall() - let responseHeaders = try await call.initialMetadata - XCTAssertEqual(responseHeaders.first(name: "fn"), function) - - // Close request stream; the response should be empty. - call.requestStream.finish() - let response = try await call.response - XCTAssertEqual(response.text, "") - - let status = await call.status - XCTAssertEqual(status.code, .ok) - } - - func testExplicitAcceptClientStreaming() async throws { - try await self.testExplicitAcceptClientStreaming(twice: false) - } - - func testExplicitAcceptTwiceClientStreaming() async throws { - try await self.testExplicitAcceptClientStreaming(twice: true) - } - - func testExplicitAcceptServerStreaming(twice: Bool, function: String = #function) async throws { - let headers: HPACKHeaders = ["fn": #function] - let channel = try self.setUpServerAndChannel( - service: AsyncEchoProvider(headers: headers, sendTwice: twice) - ) - let echo = Echo_EchoAsyncClient(channel: channel) - let call = echo.makeExpandCall(.with { $0.text = "foo bar baz" }) - let responseHeaders = try await call.initialMetadata - XCTAssertEqual(responseHeaders.first(name: "fn"), #function) - - // Close request stream; the response should be empty. - let responses = try await call.responseStream.collect() - XCTAssertEqual(responses.count, 3) - - let status = await call.status - XCTAssertEqual(status.code, .ok) - } - - func testExplicitAcceptServerStreaming() async throws { - try await self.testExplicitAcceptServerStreaming(twice: false) - } - - func testExplicitAcceptTwiceServerStreaming() async throws { - try await self.testExplicitAcceptServerStreaming(twice: true) - } - - func testExplicitAcceptBidirectionalStreaming( - twice: Bool, - function: String = #function - ) async throws { - let headers: HPACKHeaders = ["fn": function] - let channel = try self.setUpServerAndChannel( - service: AsyncEchoProvider(headers: headers, sendTwice: twice) - ) - let echo = Echo_EchoAsyncClient(channel: channel) - let call = echo.makeUpdateCall() - let responseHeaders = try await call.initialMetadata - XCTAssertEqual(responseHeaders.first(name: "fn"), function) - - // Close request stream; there should be no responses. - call.requestStream.finish() - let responses = try await call.responseStream.collect() - XCTAssertEqual(responses.count, 0) - - let status = await call.status - XCTAssertEqual(status.code, .ok) - } - - func testExplicitAcceptBidirectionalStreaming() async throws { - try await self.testExplicitAcceptBidirectionalStreaming(twice: false) - } - - func testExplicitAcceptTwiceBidirectionalStreaming() async throws { - try await self.testExplicitAcceptBidirectionalStreaming(twice: true) - } -} - -// Workaround https://bugs.swift.org/browse/SR-15070 (compiler crashes when defining a class/actor -// in an async context). -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -private actor RequestResponseCounter { - var numResponses = 0 - var numRequests = 0 - - func incrementResponses() async { - self.numResponses += 1 - } - - func incrementRequests() async { - self.numRequests += 1 - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -private final class AsyncEchoProvider: Echo_EchoAsyncProvider { - let headers: HPACKHeaders - let sendTwice: Bool - - init(headers: HPACKHeaders, sendTwice: Bool = false) { - self.headers = headers - self.sendTwice = sendTwice - } - - private func accept(context: GRPCAsyncServerCallContext) async { - await context.acceptRPC(headers: self.headers) - if self.sendTwice { - await context.acceptRPC(headers: self.headers) // Should be a no-op. - } - } - - func get( - request: Echo_EchoRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Echo_EchoResponse { - await self.accept(context: context) - return Echo_EchoResponse.with { $0.text = request.text } - } - - func expand( - request: Echo_EchoRequest, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - await self.accept(context: context) - for part in request.text.components(separatedBy: " ") { - let response = Echo_EchoResponse.with { - $0.text = part - } - try await responseStream.send(response) - } - } - - func collect( - requestStream: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext - ) async throws -> Echo_EchoResponse { - await self.accept(context: context) - let collected = try await requestStream.map { $0.text }.collect().joined(separator: " ") - return Echo_EchoResponse.with { $0.text = collected } - } - - func update( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - await self.accept(context: context) - for try await request in requestStream { - let response = Echo_EchoResponse.with { $0.text = request.text } - try await responseStream.send(response) - } - } -} diff --git a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift b/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift deleted file mode 100644 index fda2aac07..000000000 --- a/Tests/GRPCTests/GRPCAsyncServerHandlerTests.swift +++ /dev/null @@ -1,619 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import NIOHPACK -import NIOPosix -import XCTest - -@testable import GRPC - -// MARK: - Tests - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -class AsyncServerHandlerTests: GRPCTestCase { - private let recorder = AsyncResponseStream() - private var group: EventLoopGroup! - private var loop: EventLoop! - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.loop = self.group.next() - } - - override func tearDown() { - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - func makeCallHandlerContext( - encoding: ServerMessageEncoding = .disabled - ) -> CallHandlerContext { - let closeFuture = self.loop.makeSucceededVoidFuture() - - return CallHandlerContext( - errorDelegate: nil, - logger: self.logger, - encoding: encoding, - eventLoop: self.loop, - path: "/ignored", - remoteAddress: nil, - responseWriter: self.recorder, - allocator: ByteBufferAllocator(), - closeFuture: closeFuture - ) - } - - private func makeHandler( - encoding: ServerMessageEncoding = .disabled, - callType: GRPCCallType = .bidirectionalStreaming, - observer: @escaping @Sendable ( - GRPCAsyncRequestStream, - GRPCAsyncResponseStreamWriter, - GRPCAsyncServerCallContext - ) async throws -> Void - ) -> AsyncServerHandler { - return AsyncServerHandler( - context: self.makeCallHandlerContext(encoding: encoding), - requestDeserializer: StringDeserializer(), - responseSerializer: StringSerializer(), - callType: callType, - interceptors: [], - userHandler: observer - ) - } - - @Sendable - private static func echo( - requests: GRPCAsyncRequestStream, - responseStreamWriter: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await message in requests { - try await responseStreamWriter.send(message) - } - } - - @Sendable - private static func neverReceivesMessage( - requests: GRPCAsyncRequestStream, - responseStreamWriter: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - for try await message in requests { - XCTFail("Unexpected message: '\(message)'") - } - } - - @Sendable - private static func neverCalled( - requests: GRPCAsyncRequestStream, - responseStreamWriter: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext - ) async throws { - XCTFail("This observer should never be called") - } - - func testHappyPath() async throws { - let handler = self.makeHandler( - observer: Self.echo(requests:responseStreamWriter:context:) - ) - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata() - for expected in ["1", "2", "3"] { - await responseStream.next().assertMessage { buffer, metadata in - XCTAssertEqual(buffer, .init(string: expected)) - XCTAssertFalse(metadata.compress) - } - } - - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .ok) - } - await responseStream.next().assertNil() - } - - func testHappyPathWithCompressionEnabled() async throws { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))), - observer: Self.echo(requests:responseStreamWriter:context:) - ) - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata() - for expected in ["1", "2", "3"] { - await responseStream.next().assertMessage { buffer, metadata in - XCTAssertEqual(buffer, .init(string: expected)) - XCTAssertTrue(metadata.compress) - } - } - await responseStream.next().assertStatus() - await responseStream.next().assertNil() - } - - func testHappyPathWithCompressionEnabledButDisabledByCaller() async throws { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))) - ) { requests, responseStreamWriter, context in - try await context.response.compressResponses(false) - return try await Self.echo( - requests: requests, - responseStreamWriter: responseStreamWriter, - context: context - ) - } - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata() - for expected in ["1", "2", "3"] { - await responseStream.next().assertMessage { buffer, metadata in - XCTAssertEqual(buffer, .init(string: expected)) - XCTAssertFalse(metadata.compress) - } - } - await responseStream.next().assertStatus() - await responseStream.next().assertNil() - } - - func testResponseHeadersAndTrailersSentFromContext() async throws { - let handler = self.makeHandler { _, responseStreamWriter, context in - try await context.response.setHeaders(["pontiac": "bandit"]) - try await responseStreamWriter.send("1") - try await context.response.setTrailers(["disco": "strangler"]) - } - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveEnd() - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata { headers in - XCTAssertEqual(headers, ["pontiac": "bandit"]) - } - await responseStream.next().assertMessage() - await responseStream.next().assertStatus { _, trailers in - XCTAssertEqual(trailers, ["disco": "strangler"]) - } - await responseStream.next().assertNil() - } - - func testResponseSequence() async throws { - let handler = self.makeHandler { _, responseStreamWriter, _ in - try await responseStreamWriter.send(contentsOf: ["1", "2", "3"]) - } - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveEnd() - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata { _ in } - await responseStream.next().assertMessage() - await responseStream.next().assertMessage() - await responseStream.next().assertMessage() - await responseStream.next().assertStatus { _, _ in } - await responseStream.next().assertNil() - } - - func testThrowingDeserializer() async throws { - let handler = AsyncServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: ThrowingStringDeserializer(), - responseSerializer: StringSerializer(), - callType: .bidirectionalStreaming, - interceptors: [], - userHandler: Self.neverReceivesMessage(requests:responseStreamWriter:context:) - ) - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .internalError) - } - await responseStream.next().assertNil() - } - - func testThrowingSerializer() async throws { - let handler = AsyncServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: StringDeserializer(), - responseSerializer: ThrowingStringSerializer(), - callType: .bidirectionalStreaming, - interceptors: [], - userHandler: Self.echo(requests:responseStreamWriter:context:) - ) - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .internalError) - } - await responseStream.next().assertNil() - } - - func testReceiveMessageBeforeHeaders() async throws { - let handler = self.makeHandler( - observer: Self.neverCalled(requests:responseStreamWriter:context:) - ) - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMessage(ByteBuffer(string: "foo")) - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .internalError) - } - await responseStream.next().assertNil() - } - - func testReceiveMultipleHeaders() async throws { - let handler = self.makeHandler( - observer: Self.neverReceivesMessage(requests:responseStreamWriter:context:) - ) - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMetadata([:]) - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .internalError) - } - await responseStream.next().assertNil() - } - - func testFinishBeforeStarting() async throws { - let handler = self.makeHandler( - observer: Self.neverCalled(requests:responseStreamWriter:context:) - ) - - self.loop.execute { - handler.finish() - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus() - await responseStream.next().assertNil() - } - - func testFinishAfterHeaders() async throws { - let handler = self.makeHandler( - observer: Self.neverReceivesMessage(requests:responseStreamWriter:context:) - ) - - self.loop.execute { - handler.receiveMetadata([:]) - handler.finish() - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus() - await responseStream.next().assertNil() - } - - func testFinishAfterMessage() async throws { - let handler = self.makeHandler(observer: Self.echo(requests:responseStreamWriter:context:)) - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - } - - // Await the metadata and message so we know the user function is running. - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata() - await responseStream.next().assertMessage() - - // Finish, i.e. terminate early. - self.loop.execute { - handler.finish() - } - - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .internalError) - } - await responseStream.next().assertNil() - } - - func testErrorAfterHeaders() async throws { - let handler = self.makeHandler(observer: Self.echo(requests:responseStreamWriter:context:)) - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveError(CancellationError()) - } - - // We don't send a message so we don't expect any responses. As metadata is sent lazily on the - // first message we don't expect to get metadata back either. - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .unavailable) - } - - await responseStream.next().assertNil() - } - - func testErrorAfterMessage() async throws { - let handler = self.makeHandler(observer: Self.echo(requests:responseStreamWriter:context:)) - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - } - - // Wait the metadata and message; i.e. for function to have been invoked. - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertMetadata() - await responseStream.next().assertMessage() - - // Throw in an error. - self.loop.execute { - handler.receiveError(CancellationError()) - } - - // The RPC should end. - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .unavailable) - } - await responseStream.next().assertNil() - } - - func testHandlerThrowsGRPCStatusOKResultsInUnknownStatus() async throws { - // Create a user function that immediately throws GRPCStatus.ok. - let handler = self.makeHandler { _, _, _ in - throw GRPCStatus.ok - } - - // Send some metadata to trigger the creation of the async task with the user function. - self.loop.execute { - handler.receiveMetadata([:]) - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .unknown) - } - await responseStream.next().assertNil() - } - - func testUnaryHandlerReceivingMultipleMessages() async throws { - @Sendable - func neverCalled(_: String, _: GRPCAsyncServerCallContext) async throws -> String { - XCTFail("Should not be called") - return "" - } - - let handler = GRPCAsyncServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: StringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - wrapping: neverCalled(_:_:) - ) - - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .internalError) - } - } - - func testServerStreamingHandlerReceivingMultipleMessages() async throws { - @Sendable - func neverCalled( - _: String, - _: GRPCAsyncResponseStreamWriter, - _: GRPCAsyncServerCallContext - ) async throws { - XCTFail("Should not be called") - } - - let handler = GRPCAsyncServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: StringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - wrapping: neverCalled(_:_:_:) - ) - - defer { - XCTAssertNoThrow(try self.loop.submit { handler.finish() }.wait()) - } - - self.loop.execute { - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - } - - let responseStream = self.recorder.responseSequence.makeAsyncIterator() - await responseStream.next().assertStatus { status, _ in - XCTAssertEqual(status.code, .internalError) - } - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal final class AsyncResponseStream: GRPCServerResponseWriter { - private let source: - NIOAsyncSequenceProducer< - GRPCServerResponsePart, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - >.Source - - internal var responseSequence: - NIOAsyncSequenceProducer< - GRPCServerResponsePart, - NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark, - GRPCAsyncSequenceProducerDelegate - > - - init() { - let backpressureStrategy = NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark( - lowWatermark: 10, - highWatermark: 50 - ) - let sequence = NIOAsyncSequenceProducer.makeSequence( - elementType: GRPCServerResponsePart.self, - backPressureStrategy: backpressureStrategy, - delegate: GRPCAsyncSequenceProducerDelegate() - ) - self.source = sequence.source - self.responseSequence = sequence.sequence - } - - func sendMetadata( - _ metadata: HPACKHeaders, - flush: Bool, - promise: EventLoopPromise? - ) { - _ = self.source.yield(.metadata(metadata)) - promise?.succeed(()) - } - - func sendMessage( - _ bytes: ByteBuffer, - metadata: MessageMetadata, - promise: EventLoopPromise? - ) { - _ = self.source.yield(.message(bytes, metadata)) - promise?.succeed(()) - } - - func sendEnd( - status: GRPCStatus, - trailers: HPACKHeaders, - promise: EventLoopPromise? - ) { - _ = self.source.yield(.end(status, trailers)) - self.source.finish() - promise?.succeed(()) - } - - func stopRecording() { - self.source.finish() - } -} - -extension Optional where Wrapped == GRPCServerResponsePart { - func assertNil() { - XCTAssertNil(self) - } - - func assertMetadata(_ verify: (HPACKHeaders) -> Void = { _ in }) { - switch self { - case let .some(.metadata(headers)): - verify(headers) - default: - XCTFail("Expected metadata but value was \(String(describing: self))") - } - } - - func assertMessage(_ verify: (ByteBuffer, MessageMetadata) -> Void = { _, _ in }) { - switch self { - case let .some(.message(buffer, metadata)): - verify(buffer, metadata) - default: - XCTFail("Expected message but value was \(String(describing: self))") - } - } - - func assertStatus(_ verify: (GRPCStatus, HPACKHeaders) -> Void = { _, _ in }) { - switch self { - case let .some(.end(status, trailers)): - verify(status, trailers) - default: - XCTFail("Expected status but value was \(String(describing: self))") - } - } -} diff --git a/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift b/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift deleted file mode 100644 index 85feb8ea5..000000000 --- a/Tests/GRPCTests/GRPCClientChannelHandlerTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP2 -import XCTest - -@testable import GRPC - -class GRPCClientChannelHandlerTests: GRPCTestCase { - private func makeRequestHead() -> _GRPCRequestHead { - return _GRPCRequestHead( - method: "POST", - scheme: "https", - path: "/foo/bar", - host: "localhost", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ) - } - - func doTestDataFrameWithEndStream(dataContainsMessage: Bool) throws { - let handler = GRPCClientChannelHandler( - callType: .unary, - maximumReceiveMessageLength: .max, - logger: self.clientLogger - ) - - let channel = EmbeddedChannel(handler: handler) - - // Write request head. - let head = self.makeRequestHead() - XCTAssertNoThrow(try channel.writeOutbound(_RawGRPCClientRequestPart.head(head))) - // Read out a frame payload. - XCTAssertNotNil(try channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - - // Respond with headers. - let headers: HPACKHeaders = [":status": "200", "content-type": "application/grpc"] - let headersPayload = HTTP2Frame.FramePayload.headers(.init(headers: headers)) - XCTAssertNoThrow(try channel.writeInbound(headersPayload)) - // Read them out the other side. - XCTAssertNotNil(try channel.readInbound(as: _RawGRPCClientResponsePart.self)) - - // Respond with DATA and end stream. - var buffer = ByteBuffer() - - // Write a message, if we need to. - if dataContainsMessage { - buffer.writeInteger(UInt8(0)) // not compressed - buffer.writeInteger(UInt32(42)) // message length - buffer.writeRepeatingByte(0, count: 42) // message - } - - let dataPayload = HTTP2Frame.FramePayload.Data(data: .byteBuffer(buffer), endStream: true) - XCTAssertNoThrow(try channel.writeInbound(HTTP2Frame.FramePayload.data(dataPayload))) - - if dataContainsMessage { - // Read the message out the other side. - XCTAssertNotNil(try channel.readInbound(as: _RawGRPCClientResponsePart.self)) - } - - // We should also generate a status since end stream was set. - if let part = try channel.readInbound(as: _RawGRPCClientResponsePart.self) { - switch part { - case .initialMetadata, .message, .trailingMetadata: - XCTFail("Unexpected response part") - case .status: - () // Expected - } - } else { - XCTFail("Expected to read another response part") - } - } - - func testDataFrameWithEndStream() throws { - try self.doTestDataFrameWithEndStream(dataContainsMessage: true) - } - - func testEmptyDataFrameWithEndStream() throws { - try self.doTestDataFrameWithEndStream(dataContainsMessage: false) - } -} diff --git a/Tests/GRPCTests/GRPCClientStateMachineTests.swift b/Tests/GRPCTests/GRPCClientStateMachineTests.swift deleted file mode 100644 index 5cef96594..000000000 --- a/Tests/GRPCTests/GRPCClientStateMachineTests.swift +++ /dev/null @@ -1,1387 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import Logging -import NIOCore -import NIOHPACK -import NIOHTTP1 -import SwiftProtobuf -import XCTest - -@testable import GRPC - -class GRPCClientStateMachineTests: GRPCTestCase { - typealias Request = Echo_EchoRequest - typealias Response = Echo_EchoResponse - typealias StateMachine = GRPCClientStateMachine - - var allocator = ByteBufferAllocator() - - func makeStateMachine(_ state: StateMachine.State) -> StateMachine { - return StateMachine(state: state) - } - - /// Writes a message into a new `ByteBuffer` (with length-prefixing). - func writeMessage(_ message: String) throws -> ByteBuffer { - let buffer = self.allocator.buffer(string: message) - - var writer = CoalescingLengthPrefixedMessageWriter(compression: .none, allocator: .init()) - writer.append(buffer: buffer, compress: false, promise: nil) - - var result: ByteBuffer? - while let next = writer.next() { - switch next.0 { - case let .success(buffer): - result.setOrWriteImmutableBuffer(buffer) - case let .failure(error): - throw error - } - } - - // We wrote a message, we must get at least one buffer out (or throw). - return result! - } - - /// Writes a message into the given `buffer`. - func writeMessage(_ message: String, into buffer: inout ByteBuffer) throws { - var other = try self.writeMessage(message) - buffer.writeBuffer(&other) - } - - /// Returns a minimally valid `HPACKHeaders` for a response. - func makeResponseHeaders( - status: String? = "200", - contentType: String? = "application/grpc+proto" - ) -> HPACKHeaders { - var headers: HPACKHeaders = [:] - status.map { headers.add(name: ":status", value: $0) } - contentType.map { headers.add(name: "content-type", value: $0) } - return headers - } -} - -// MARK: - Send Request Headers - -extension GRPCClientStateMachineTests { - func doTestSendRequestHeadersFromInvalidState(_ state: StateMachine.State) { - var stateMachine = self.makeStateMachine(state) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "host", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), - allocator: .init() - ).assertFailure { - XCTAssertEqual($0, .invalidState) - } - } - - func testSendRequestHeadersFromIdle() { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "host", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), - allocator: .init() - ).assertSuccess() - } - - func testSendRequestHeadersFromClientActiveServerIdle() { - self.doTestSendRequestHeadersFromInvalidState( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - } - - func testSendRequestHeadersFromClientClosedServerIdle() { - self - .doTestSendRequestHeadersFromInvalidState( - .clientClosedServerIdle( - pendingReadState: .init( - arity: .one, - messageEncoding: .disabled - ) - ) - ) - } - - func testSendRequestHeadersFromActive() { - self - .doTestSendRequestHeadersFromInvalidState( - .clientActiveServerActive( - writeState: .one(), - readState: .one() - ) - ) - } - - func testSendRequestHeadersFromClientClosedServerActive() { - self.doTestSendRequestHeadersFromInvalidState(.clientClosedServerActive(readState: .one())) - } - - func testSendRequestHeadersFromClosed() { - self.doTestSendRequestHeadersFromInvalidState(.clientClosedServerClosed) - } -} - -// MARK: - Send Request - -extension GRPCClientStateMachineTests { - func doTestSendRequestFromInvalidState(_ state: StateMachine.State, expected: MessageWriteError) { - var stateMachine = self.makeStateMachine(state) - stateMachine.sendRequest( - ByteBuffer(string: "Hello!"), - compressed: false - ).assertFailure { - XCTAssertEqual($0, expected) - } - } - - func doTestSendRequestFromValidState(_ state: StateMachine.State) { - var stateMachine = self.makeStateMachine(state) - - let request = "Hello!" - stateMachine.sendRequest( - ByteBuffer(string: request), - compressed: false - ).assertSuccess() - } - - func testSendRequestFromIdle() { - self.doTestSendRequestFromInvalidState( - .clientIdleServerIdle(pendingWriteState: .one(), readArity: .one), - expected: .invalidState - ) - } - - func testSendRequestFromClientActiveServerIdle() { - self.doTestSendRequestFromValidState( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - } - - func testSendRequestFromClientClosedServerIdle() { - self.doTestSendRequestFromInvalidState( - .clientClosedServerIdle(pendingReadState: .init(arity: .one, messageEncoding: .disabled)), - expected: .cardinalityViolation - ) - } - - func testSendRequestFromActive() { - self - .doTestSendRequestFromValidState( - .clientActiveServerActive( - writeState: .one(), - readState: .one() - ) - ) - } - - func testSendRequestFromClientClosedServerActive() { - self.doTestSendRequestFromInvalidState( - .clientClosedServerIdle(pendingReadState: .init(arity: .one, messageEncoding: .disabled)), - expected: .cardinalityViolation - ) - } - - func testSendRequestFromClosed() { - self.doTestSendRequestFromInvalidState( - .clientClosedServerClosed, - expected: .cardinalityViolation - ) - } -} - -// MARK: - Send End of Request Stream - -extension GRPCClientStateMachineTests { - func doTestSendEndOfRequestStreamFromInvalidState( - _ state: StateMachine.State, - expected: SendEndOfRequestStreamError - ) { - var stateMachine = self.makeStateMachine(state) - stateMachine.sendEndOfRequestStream().assertFailure { - XCTAssertEqual($0, expected) - } - } - - func doTestSendEndOfRequestStreamFromValidState(_ state: StateMachine.State) { - var stateMachine = self.makeStateMachine(state) - stateMachine.sendEndOfRequestStream().assertSuccess() - } - - func testSendEndOfRequestStreamFromIdle() { - self.doTestSendEndOfRequestStreamFromInvalidState( - .clientIdleServerIdle(pendingWriteState: .one(), readArity: .one), - expected: .invalidState - ) - } - - func testSendEndOfRequestStreamFromClientActiveServerIdle() { - self.doTestSendEndOfRequestStreamFromValidState( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - } - - func testSendEndOfRequestStreamFromClientClosedServerIdle() { - self.doTestSendEndOfRequestStreamFromInvalidState( - .clientClosedServerIdle(pendingReadState: .init(arity: .one, messageEncoding: .disabled)), - expected: .alreadyClosed - ) - } - - func testSendEndOfRequestStreamFromActive() { - self.doTestSendEndOfRequestStreamFromValidState( - .clientActiveServerActive(writeState: .one(), readState: .one()) - ) - } - - func testSendEndOfRequestStreamFromClientClosedServerActive() { - self.doTestSendEndOfRequestStreamFromInvalidState( - .clientClosedServerActive(readState: .one()), - expected: .alreadyClosed - ) - } - - func testSendEndOfRequestStreamFromClosed() { - self.doTestSendEndOfRequestStreamFromInvalidState( - .clientClosedServerClosed, - expected: .alreadyClosed - ) - } -} - -// MARK: - Receive Response Headers - -extension GRPCClientStateMachineTests { - func doTestReceiveResponseHeadersFromInvalidState( - _ state: StateMachine.State, - expected: ReceiveResponseHeadError - ) { - var stateMachine = self.makeStateMachine(state) - stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertFailure { - XCTAssertEqual($0, expected) - } - } - - func doTestReceiveResponseHeadersFromValidState(_ state: StateMachine.State) { - var stateMachine = self.makeStateMachine(state) - stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() - } - - func testReceiveResponseHeadersFromIdle() { - self.doTestReceiveResponseHeadersFromInvalidState( - .clientIdleServerIdle(pendingWriteState: .one(), readArity: .one), - expected: .invalidState - ) - } - - func testReceiveResponseHeadersFromClientActiveServerIdle() { - self.doTestReceiveResponseHeadersFromValidState( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - } - - func testReceiveResponseHeadersFromClientClosedServerIdle() { - self.doTestReceiveResponseHeadersFromValidState( - .clientClosedServerIdle(pendingReadState: .init(arity: .one, messageEncoding: .disabled)) - ) - } - - func testReceiveResponseHeadersFromActive() { - self.doTestReceiveResponseHeadersFromInvalidState( - .clientActiveServerActive(writeState: .one(), readState: .one()), - expected: .invalidState - ) - } - - func testReceiveResponseHeadersFromClientClosedServerActive() { - self.doTestReceiveResponseHeadersFromInvalidState( - .clientClosedServerActive(readState: .one()), - expected: .invalidState - ) - } - - func testReceiveResponseHeadersFromClosed() { - self.doTestReceiveResponseHeadersFromInvalidState( - .clientClosedServerClosed, - expected: .invalidState - ) - } -} - -// MARK: - Receive Response - -extension GRPCClientStateMachineTests { - func doTestReceiveResponseFromInvalidState( - _ state: StateMachine.State, - expected: MessageReadError - ) throws { - var stateMachine = self.makeStateMachine(state) - - let message = "Hello!" - var buffer = try self.writeMessage(message) - - stateMachine.receiveResponseBuffer(&buffer, maxMessageLength: .max).assertFailure { - XCTAssertEqual($0, expected) - } - } - - func doTestReceiveResponseFromValidState(_ state: StateMachine.State) throws { - var stateMachine = self.makeStateMachine(state) - - let message = "Hello!" - var buffer = try self.writeMessage(message) - - stateMachine.receiveResponseBuffer(&buffer, maxMessageLength: .max).assertSuccess { messages in - XCTAssertEqual(messages, [ByteBuffer(string: message)]) - } - } - - func testReceiveResponseFromIdle() throws { - try self.doTestReceiveResponseFromInvalidState( - .clientIdleServerIdle(pendingWriteState: .one(), readArity: .one), - expected: .invalidState - ) - } - - func testReceiveResponseFromClientActiveServerIdle() throws { - try self.doTestReceiveResponseFromInvalidState( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ), - expected: .invalidState - ) - } - - func testReceiveResponseFromClientClosedServerIdle() throws { - try self.doTestReceiveResponseFromInvalidState( - .clientClosedServerIdle(pendingReadState: .init(arity: .one, messageEncoding: .disabled)), - expected: .invalidState - ) - } - - func testReceiveResponseFromActive() throws { - try self.doTestReceiveResponseFromValidState( - .clientActiveServerActive(writeState: .one(), readState: .one()) - ) - } - - func testReceiveResponseFromClientClosedServerActive() throws { - try self.doTestReceiveResponseFromValidState(.clientClosedServerActive(readState: .one())) - } - - func testReceiveResponseFromClosed() throws { - try self.doTestReceiveResponseFromInvalidState( - .clientClosedServerClosed, - expected: .invalidState - ) - } -} - -// MARK: - Receive End of Response Stream - -extension GRPCClientStateMachineTests { - func doTestReceiveEndOfResponseStreamFromInvalidState( - _ state: StateMachine.State, - expected: ReceiveEndOfResponseStreamError - ) { - var stateMachine = self.makeStateMachine(state) - stateMachine.receiveEndOfResponseStream(.init()).assertFailure() - } - - func doTestReceiveEndOfResponseStreamFromValidState(_ state: StateMachine.State) { - var stateMachine = self.makeStateMachine(state) - - var trailers: HPACKHeaders = [ - GRPCHeaderName.statusCode: "0", - GRPCHeaderName.statusMessage: "ok", - ] - - // When the server is idle it's a "Trailers-Only" response, we need the :status and - // content-type to make a valid set of trailers. - switch state { - case .clientActiveServerIdle, - .clientClosedServerIdle: - trailers.add(name: ":status", value: "200") - trailers.add(name: "content-type", value: "application/grpc+proto") - default: - break - } - - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code, .ok) - XCTAssertEqual(status.message, "ok") - } - } - - func testReceiveEndOfResponseStreamFromIdle() { - self.doTestReceiveEndOfResponseStreamFromInvalidState( - .clientIdleServerIdle(pendingWriteState: .one(), readArity: .one), - expected: .invalidState - ) - } - - func testReceiveEndOfResponseStreamFromClientActiveServerIdle() { - self.doTestReceiveEndOfResponseStreamFromValidState( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - } - - func testReceiveEndOfResponseStreamFromClientClosedServerIdle() { - self.doTestReceiveEndOfResponseStreamFromValidState( - .clientClosedServerIdle(pendingReadState: .init(arity: .one, messageEncoding: .disabled)) - ) - } - - func testReceiveEndOfResponseStreamFromActive() { - self.doTestReceiveEndOfResponseStreamFromValidState( - .clientActiveServerActive(writeState: .one(), readState: .one()) - ) - } - - func testReceiveEndOfResponseStreamFromClientClosedServerActive() { - self.doTestReceiveEndOfResponseStreamFromValidState( - .clientClosedServerActive(readState: .one()) - ) - } - - func testReceiveEndOfResponseStreamFromClosed() { - self.doTestReceiveEndOfResponseStreamFromInvalidState( - .clientClosedServerClosed, - expected: .invalidState - ) - } - - private func doTestReceiveEndStreamOnDataWhenActive(_ state: StateMachine.State) throws { - var stateMachine = self.makeStateMachine(state) - let status = try assertNotNil(stateMachine.receiveEndOfResponseStream()) - XCTAssertEqual(status.code, .internalError) - } - - func testReceiveEndStreamOnDataClientActiveServerIdle() throws { - try self.doTestReceiveEndStreamOnDataWhenActive( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - } - - func testReceiveEndStreamOnDataClientClosedServerIdle() throws { - try self.doTestReceiveEndStreamOnDataWhenActive( - .clientClosedServerIdle(pendingReadState: .init(arity: .one, messageEncoding: .disabled)) - ) - } - - func testReceiveEndStreamOnDataClientActiveServerActive() throws { - try self.doTestReceiveEndStreamOnDataWhenActive( - .clientActiveServerActive(writeState: .one(), readState: .one()) - ) - } - - func testReceiveEndStreamOnDataClientClosedServerActive() throws { - try self.doTestReceiveEndStreamOnDataWhenActive( - .clientClosedServerActive(readState: .one()) - ) - } - - func testReceiveEndStreamOnDataWhenClosed() { - var stateMachine = self.makeStateMachine(.clientClosedServerClosed) - // Already closed, end stream is ignored. - XCTAssertNil(stateMachine.receiveEndOfResponseStream()) - } -} - -// MARK: - Basic RPC flow. - -extension GRPCClientStateMachineTests { - func makeTrailers(status: GRPCStatus.Code, message: String? = nil) -> HPACKHeaders { - var headers = HPACKHeaders() - headers.add(name: GRPCHeaderName.statusCode, value: "\(status.rawValue)") - if let message = message { - headers.add(name: GRPCHeaderName.statusMessage, value: message) - } - return headers - } - - func testSimpleUnaryFlow() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - - // Initiate the RPC - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), - allocator: .init() - ).assertSuccess() - - // Receive acknowledgement. - stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() - - // Send a request. - stateMachine.sendRequest( - ByteBuffer(string: "Hello!"), - compressed: false - ).assertSuccess() - - // Close the request stream. - stateMachine.sendEndOfRequestStream().assertSuccess() - - // Receive a response. - var buffer = try self.writeMessage("Hello!") - stateMachine.receiveResponseBuffer(&buffer, maxMessageLength: .max).assertSuccess() - - // Receive the status. - stateMachine.receiveEndOfResponseStream(self.makeTrailers(status: .ok)).assertSuccess() - } - - func testSimpleClientActiveFlow() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .many(), readArity: .one)) - - // Initiate the RPC - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), - allocator: .init() - ).assertSuccess() - - // Receive acknowledgement. - stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() - - // Send some requests. - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() - stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertSuccess() - stateMachine.sendRequest(ByteBuffer(string: "3"), compressed: false).assertSuccess() - - // Close the request stream. - stateMachine.sendEndOfRequestStream().assertSuccess() - - // Receive a response. - var buffer = try self.writeMessage("Hello!") - stateMachine.receiveResponseBuffer(&buffer, maxMessageLength: .max).assertSuccess() - - // Receive the status. - stateMachine.receiveEndOfResponseStream(self.makeTrailers(status: .ok)).assertSuccess() - } - - func testSimpleServerActiveFlow() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .many)) - - // Initiate the RPC - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), - allocator: .init() - ).assertSuccess() - - // Receive acknowledgement. - stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() - - // Send a request. - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() - - // Close the request stream. - stateMachine.sendEndOfRequestStream().assertSuccess() - - // Receive a response. - var firstBuffer = try self.writeMessage("1") - stateMachine.receiveResponseBuffer(&firstBuffer, maxMessageLength: .max).assertSuccess() - - // Receive two responses in one buffer. - var secondBuffer = try self.writeMessage("2") - try self.writeMessage("3", into: &secondBuffer) - stateMachine.receiveResponseBuffer(&secondBuffer, maxMessageLength: .max).assertSuccess() - - // Receive the status. - stateMachine.receiveEndOfResponseStream(self.makeTrailers(status: .ok)).assertSuccess() - } - - func testSimpleBidirectionalActiveFlow() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .many(), readArity: .many)) - - // Initiate the RPC - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "https", - path: "/echo/Get", - host: "foo", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ), - allocator: .init() - ).assertSuccess() - - // Receive acknowledgement. - stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() - - // Interleave requests and responses: - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() - - // Receive a response. - var firstBuffer = try self.writeMessage("1") - stateMachine.receiveResponseBuffer(&firstBuffer, maxMessageLength: .max).assertSuccess() - - // Send two more requests. - stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertSuccess() - stateMachine.sendRequest(ByteBuffer(string: "3"), compressed: false).assertSuccess() - - // Receive two responses in one buffer. - var secondBuffer = try self.writeMessage("2") - try self.writeMessage("3", into: &secondBuffer) - stateMachine.receiveResponseBuffer(&secondBuffer, maxMessageLength: .max).assertSuccess() - - // Close the request stream. - stateMachine.sendEndOfRequestStream().assertSuccess() - - // Receive the status. - stateMachine.receiveEndOfResponseStream(self.makeTrailers(status: .ok)).assertSuccess() - } -} - -// MARK: - Too many requests / responses. - -extension GRPCClientStateMachineTests { - func testSendTooManyRequestsFromClientActiveServerIdle() { - for messageCount in [MessageArity.one, MessageArity.many] { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: messageCount, messageEncoding: .disabled) - ) - ) - - // One is fine. - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() - // Two is not. - stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } - } - } - - func testSendTooManyRequestsFromActive() { - for readState in [ReadState.one(), ReadState.many()] { - var stateMachine = - self - .makeStateMachine(.clientActiveServerActive(writeState: .one(), readState: readState)) - - // One is fine. - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertSuccess() - // Two is not. - stateMachine.sendRequest(ByteBuffer(string: "2"), compressed: false).assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } - } - } - - func testSendTooManyRequestsFromClosed() { - var stateMachine = self.makeStateMachine(.clientClosedServerClosed) - - // No requests allowed! - stateMachine.sendRequest(ByteBuffer(string: "1"), compressed: false).assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } - } - - func testReceiveTooManyRequests() throws { - for writeState in [WriteState.one(), WriteState.many()] { - var stateMachine = - self - .makeStateMachine(.clientActiveServerActive(writeState: writeState, readState: .one())) - - // One response is fine. - var firstBuffer = try self.writeMessage("foo") - stateMachine.receiveResponseBuffer(&firstBuffer, maxMessageLength: .max).assertSuccess() - - var secondBuffer = try self.writeMessage("bar") - stateMachine.receiveResponseBuffer(&secondBuffer, maxMessageLength: .max).assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } - } - } - - func testReceiveTooManyRequestsInOneBuffer() throws { - for writeState in [WriteState.one(), WriteState.many()] { - var stateMachine = - self - .makeStateMachine(.clientActiveServerActive(writeState: writeState, readState: .one())) - - // Write two responses into a single buffer. - var buffer = try self.writeMessage("foo") - var other = try self.writeMessage("bar") - buffer.writeBuffer(&other) - - stateMachine.receiveResponseBuffer(&buffer, maxMessageLength: .max).assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } - } - } -} - -// MARK: - Send Request Headers - -extension GRPCClientStateMachineTests { - func testSendRequestHeaders() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .now() + .hours(1), - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled( - .init( - forRequests: .identity, - acceptableForResponses: [.identity], - decompressionLimit: .ratio(10) - ) - ) - ), - allocator: .init() - ).assertSuccess { headers in - XCTAssertEqual(headers[":method"], ["POST"]) - XCTAssertEqual(headers[":path"], ["/echo/Get"]) - XCTAssertEqual(headers[":authority"], ["localhost"]) - XCTAssertEqual(headers[":scheme"], ["http"]) - XCTAssertEqual(headers["content-type"], ["application/grpc"]) - XCTAssertEqual(headers["te"], ["trailers"]) - // We convert the deadline into a timeout, we can't be exactly sure what that timeout is. - XCTAssertTrue(headers.contains(name: "grpc-timeout")) - XCTAssertEqual(headers["x-grpc-id"], ["request-id"]) - XCTAssertEqual(headers["grpc-encoding"], ["identity"]) - XCTAssertTrue(headers["grpc-accept-encoding"].contains("identity")) - XCTAssertTrue(headers["user-agent"].first?.starts(with: "grpc-swift") ?? false) - } - } - - func testSendRequestHeadersNormalizesCustomMetadata() throws { - // `HPACKHeaders` uses case-insensitive lookup for header names so we can't check the equality - // for individual headers. We'll pull out the entries we care about by matching a sentinel value - // and then compare `HPACKHeaders` instances (since the equality check *is* case sensitive). - let filterKey = "a-key-for-filtering" - let customMetadata: HPACKHeaders = [ - "partiallyLower": filterKey, - "ALLUPPER": filterKey, - ] - - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: customMetadata, - encoding: .disabled - ), - allocator: .init() - ).assertSuccess { headers in - // Pull out the entries we care about by matching values - let filtered = headers.filter { _, value, _ in - value == filterKey - }.map { name, value, _ in - (name, value) - } - - let justCustomMetadata = HPACKHeaders(filtered) - let expected: HPACKHeaders = [ - "partiallylower": filterKey, - "allupper": filterKey, - ] - - XCTAssertEqual(justCustomMetadata, expected) - } - } - - func testSendRequestHeadersWithCustomUserAgent() throws { - let customMetadata: HPACKHeaders = [ - "user-agent": "test-user-agent" - ] - - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: customMetadata, - encoding: .enabled( - .init( - forRequests: nil, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - ) - ) - ), - allocator: .init() - ).assertSuccess { headers in - XCTAssertEqual(headers["user-agent"], ["test-user-agent"]) - } - } - - func testSendRequestHeadersWithNoCompressionInEitherDirection() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled( - .init( - forRequests: nil, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - ) - ) - ), - allocator: .init() - ).assertSuccess { headers in - XCTAssertFalse(headers.contains(name: "grpc-encoding")) - XCTAssertFalse(headers.contains(name: "grpc-accept-encoding")) - } - } - - func testSendRequestHeadersWithNoCompressionForRequests() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled( - .init( - forRequests: nil, - acceptableForResponses: [.identity, .gzip], - decompressionLimit: .ratio(10) - ) - ) - ), - allocator: .init() - ).assertSuccess { headers in - XCTAssertFalse(headers.contains(name: "grpc-encoding")) - XCTAssertTrue(headers.contains(name: "grpc-accept-encoding")) - } - } - - func testSendRequestHeadersWithNoCompressionForResponses() throws { - var stateMachine = - self - .makeStateMachine(.clientIdleServerIdle(pendingWriteState: .one(), readArity: .one)) - stateMachine.sendRequestHeaders( - requestHead: .init( - method: "POST", - scheme: "http", - path: "/echo/Get", - host: "localhost", - deadline: .distantFuture, - customMetadata: ["x-grpc-id": "request-id"], - encoding: .enabled( - .init( - forRequests: .gzip, - acceptableForResponses: [], - decompressionLimit: .ratio(10) - ) - ) - ), - allocator: .init() - ).assertSuccess { headers in - XCTAssertEqual(headers["grpc-encoding"], ["gzip"]) - // This asymmetry is strange but allowed: if a client does not advertise support of the - // compression it is using, the server may still process the message so long as it too - // supports the compression. - XCTAssertFalse(headers.contains(name: "grpc-accept-encoding")) - } - } - - func testReceiveResponseHeadersWithOkStatus() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - stateMachine.receiveResponseHeaders(self.makeResponseHeaders()).assertSuccess() - } - - func testReceiveResponseHeadersWithNotOkStatus() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - let code = "\(HTTPResponseStatus.paymentRequired.code)" - let headers = self.makeResponseHeaders(status: code) - stateMachine.receiveResponseHeaders(headers).assertFailure { - XCTAssertEqual($0, .invalidHTTPStatus(code)) - } - } - - func testReceiveResponseHeadersWithoutContentType() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - let headers = self.makeResponseHeaders(contentType: nil) - stateMachine.receiveResponseHeaders(headers).assertFailure { - XCTAssertEqual($0, .invalidContentType(nil)) - } - } - - func testReceiveResponseHeadersWithInvalidContentType() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - let headers = self.makeResponseHeaders(contentType: "video/mpeg") - stateMachine.receiveResponseHeaders(headers).assertFailure { - XCTAssertEqual($0, .invalidContentType("video/mpeg")) - } - } - - func testReceiveResponseHeadersWithSupportedCompressionMechanism() throws { - let configuration = ClientMessageEncoding.Configuration( - forRequests: .none, - acceptableForResponses: [.identity], - decompressionLimit: .ratio(1) - ) - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .enabled(configuration)) - ) - ) - - var headers = self.makeResponseHeaders() - // Identity should always be supported. - headers.add(name: "grpc-encoding", value: "identity") - - stateMachine.receiveResponseHeaders(headers).assertSuccess() - - switch stateMachine.state { - case let .clientActiveServerActive(_, .reading(_, reader)): - XCTAssertEqual(reader.compression?.algorithm, .identity) - default: - XCTFail("unexpected state \(stateMachine.state)") - } - } - - func testReceiveResponseHeadersWithUnsupportedCompressionMechanism() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - var headers = self.makeResponseHeaders() - headers.add(name: "grpc-encoding", value: "snappy") - - stateMachine.receiveResponseHeaders(headers).assertFailure { - XCTAssertEqual($0, .unsupportedMessageEncoding("snappy")) - } - } - - func testReceiveResponseHeadersWithUnknownCompressionMechanism() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - var headers = self.makeResponseHeaders() - headers.add(name: "grpc-encoding", value: "not-a-known-compression-(probably)") - - stateMachine.receiveResponseHeaders(headers).assertFailure { - XCTAssertEqual($0, .unsupportedMessageEncoding("not-a-known-compression-(probably)")) - } - } - - func testReceiveEndOfResponseStreamWithStatus() throws { - var stateMachine = self.makeStateMachine(.clientClosedServerActive(readState: .one())) - - let trailers: HPACKHeaders = ["grpc-status": "0"] - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code, GRPCStatus.Code(rawValue: 0)) - XCTAssertEqual(status.message, nil) - } - } - - func testReceiveEndOfResponseStreamWithUnknownStatus() throws { - var stateMachine = self.makeStateMachine(.clientClosedServerActive(readState: .one())) - - let trailers: HPACKHeaders = ["grpc-status": "999"] - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code, .unknown) - } - } - - func testReceiveEndOfResponseStreamWithNonIntStatus() throws { - var stateMachine = self.makeStateMachine(.clientClosedServerActive(readState: .one())) - - let trailers: HPACKHeaders = ["grpc-status": "not-a-real-status-code"] - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code, .unknown) - } - } - - func testReceiveEndOfResponseStreamWithStatusAndMessage() throws { - var stateMachine = self.makeStateMachine(.clientClosedServerActive(readState: .one())) - - let trailers: HPACKHeaders = [ - "grpc-status": "5", - "grpc-message": "foo bar 🚀", - ] - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code, GRPCStatus.Code(rawValue: 5)) - XCTAssertEqual(status.message, "foo bar 🚀") - } - } - - func testReceiveTrailersOnlyEndOfResponseStreamWithoutContentType() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - let trailers: HPACKHeaders = [ - ":status": "200", - "grpc-status": "5", - "grpc-message": "foo bar 🚀", - ] - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code, GRPCStatus.Code(rawValue: 5)) - XCTAssertEqual(status.message, "foo bar 🚀") - } - } - - func testReceiveTrailersOnlyEndOfResponseStreamWithInvalidContentType() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - let trailers: HPACKHeaders = [ - ":status": "200", - "grpc-status": "5", - "grpc-message": "foo bar 🚀", - "content-type": "invalid", - ] - stateMachine.receiveEndOfResponseStream(trailers).assertFailure { error in - XCTAssertEqual(error, .invalidContentType("invalid")) - } - } - - func testReceiveTrailersOnlyEndOfResponseStreamWithInvalidHTTPStatusAndValidGRPCStatus() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - let trailers: HPACKHeaders = [ - ":status": "418", - "grpc-status": "5", - ] - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code.rawValue, 5) - } - } - - func testReceiveTrailersOnlyEndOfResponseStreamWithInvalidHTTPStatusAndNoGRPCStatus() throws { - var stateMachine = self.makeStateMachine( - .clientActiveServerIdle( - writeState: .one(), - pendingReadState: .init(arity: .one, messageEncoding: .disabled) - ) - ) - - let trailers: HPACKHeaders = [":status": "418"] - stateMachine.receiveEndOfResponseStream(trailers).assertSuccess { status in - XCTAssertEqual(status.code, .unknown) - } - } -} - -class ReadStateTests: GRPCTestCase { - var allocator = ByteBufferAllocator() - - func writeMessage(_ message: String) -> ByteBuffer { - var buffer = self.allocator.buffer(capacity: 5 + message.utf8.count) - buffer.writeInteger(UInt8(0)) - buffer.writeInteger(UInt32(message.utf8.count)) - buffer.writeBytes(message.utf8) - return buffer - } - - func testReadWhenNoExpectedMessages() { - var state: ReadState = .notReading - var buffer = self.allocator.buffer(capacity: 0) - state.readMessages(&buffer, maxLength: .max).assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } - state.assertNotReading() - } - - func testReadWithLeftOverBytesForOneExpectedMessage() throws { - var buffer = self.writeMessage("Hello!") - // And some extra junk bytes: - let bytes: [UInt8] = [0x00] - buffer.writeBytes(bytes) - - var state: ReadState = .one() - state.readMessages(&buffer, maxLength: .max).assertFailure { - XCTAssertEqual($0, .leftOverBytes) - } - state.assertNotReading() - } - - func testReadTooManyMessagesForOneExpectedMessages() throws { - // Write a message into the buffer twice: - var buffer1 = self.writeMessage("Hello!") - let buffer2 = buffer1 - buffer1.writeImmutableBuffer(buffer2) - - var state: ReadState = .one() - state.readMessages(&buffer1, maxLength: .max).assertFailure { - XCTAssertEqual($0, .cardinalityViolation) - } - state.assertNotReading() - } - - func testReadOneMessageForOneExpectedMessages() throws { - var buffer = self.writeMessage("Hello!") - var state: ReadState = .one() - state.readMessages(&buffer, maxLength: .max).assertSuccess { - XCTAssertEqual($0, [ByteBuffer(string: "Hello!")]) - } - - // We shouldn't be able to read anymore. - state.assertNotReading() - } - - func testReadOneMessageForManyExpectedMessages() throws { - var buffer = self.writeMessage("Hello!") - var state: ReadState = .many() - state.readMessages(&buffer, maxLength: .max).assertSuccess { - XCTAssertEqual($0, [ByteBuffer(string: "Hello!")]) - } - - // We should still be able to read. - state.assertReading() - } - - func testReadManyMessagesForManyExpectedMessages() throws { - let lengthPrefixed = self.writeMessage("Hello!") - var buffer = lengthPrefixed - buffer.writeImmutableBuffer(lengthPrefixed) - buffer.writeImmutableBuffer(lengthPrefixed) - - var state: ReadState = .many() - state.readMessages(&buffer, maxLength: .max).assertSuccess { - XCTAssertEqual($0, Array(repeating: ByteBuffer(string: "Hello!"), count: 3)) - } - - // We should still be able to read. - state.assertReading() - } -} - -// MARK: Result helpers - -extension Result { - /// Asserts the `Result` was a success. - func assertSuccess(verify: (Success) throws -> Void = { _ in }) { - switch self { - case let .success(success): - do { - try verify(success) - } catch { - XCTFail("verify threw: \(error)") - } - case let .failure(error): - XCTFail("unexpected .failure: \(error)") - } - } - - /// Asserts the `Result` was a failure. - func assertFailure(verify: (Failure) throws -> Void = { _ in }) { - switch self { - case let .success(success): - XCTFail("unexpected .success: \(success)") - case let .failure(error): - do { - try verify(error) - } catch { - XCTFail("verify threw: \(error)") - } - } - } -} - -// MARK: ReadState, PendingWriteState, and WriteState helpers - -extension ReadState { - static func one() -> ReadState { - let reader = LengthPrefixedMessageReader() - return .reading(.one, reader) - } - - static func many() -> ReadState { - let reader = LengthPrefixedMessageReader() - return .reading(.many, reader) - } - - func assertReading() { - switch self { - case .reading: - () - case .notReading: - XCTFail("unexpected state .notReading") - } - } - - func assertNotReading() { - switch self { - case .reading: - XCTFail("unexpected state .reading") - case .notReading: - () - } - } -} - -extension PendingWriteState { - static func one() -> PendingWriteState { - return .init(arity: .one, contentType: .protobuf) - } - - static func many() -> PendingWriteState { - return .init(arity: .many, contentType: .protobuf) - } -} - -extension WriteState { - static func one() -> WriteState { - return .init( - arity: .one, - contentType: .protobuf, - writer: .init(compression: .none, allocator: .init()) - ) - } - - static func many() -> WriteState { - return .init( - arity: .many, - contentType: .protobuf, - writer: .init(compression: .none, allocator: .init()) - ) - } -} diff --git a/Tests/GRPCTests/GRPCCustomPayloadTests.swift b/Tests/GRPCTests/GRPCCustomPayloadTests.swift deleted file mode 100644 index 92cbfa656..000000000 --- a/Tests/GRPCTests/GRPCCustomPayloadTests.swift +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright 2020, 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 GRPC -import NIOCore -import NIOPosix -import XCTest - -// These tests demonstrate how to use gRPC to create a service provider using your own payload type, -// or alternatively, how to avoid deserialization and just extract the raw bytes from a payload. -class GRPCCustomPayloadTests: GRPCTestCase { - var group: EventLoopGroup! - var server: Server! - var client: GRPCAnyServiceClient! - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - self.server = try! Server.insecure(group: self.group) - .withServiceProviders([CustomPayloadProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - let channel = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: self.server.channel.localAddress!.port!) - - self.client = GRPCAnyServiceClient( - channel: channel, - defaultCallOptions: self.callOptionsWithLogger - ) - } - - override func tearDown() { - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.client.channel.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - func testCustomPayload() throws { - // This test demonstrates how to call a manually created bidirectional RPC with custom payloads. - let statusExpectation = self.expectation(description: "status received") - - var responses: [CustomPayload] = [] - - // Make a bidirectional stream using `CustomPayload` as the request and response type. - // The service defined below is called "CustomPayload", and the method we call on it - // is "AddOneAndReverseMessage" - let rpc: BidirectionalStreamingCall = self.client - .makeBidirectionalStreamingCall( - path: "/CustomPayload/AddOneAndReverseMessage", - handler: { responses.append($0) } - ) - - // Make and send some requests: - let requests: [CustomPayload] = [ - CustomPayload(message: "one", number: .random(in: Int64.min ..< Int64.max)), - CustomPayload(message: "two", number: .random(in: Int64.min ..< Int64.max)), - CustomPayload(message: "three", number: .random(in: Int64.min ..< Int64.max)), - ] - rpc.sendMessages(requests, promise: nil) - rpc.sendEnd(promise: nil) - - // Wait for the RPC to finish before comparing responses. - rpc.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation) - self.wait(for: [statusExpectation], timeout: 1.0) - - // Are the responses as expected? - let expected = requests.map { request in - CustomPayload(message: String(request.message.reversed()), number: request.number + 1) - } - XCTAssertEqual(responses, expected) - } - - func testNoDeserializationOnTheClient() throws { - // This test demonstrates how to skip the deserialization step on the client. It isn't necessary - // to use a custom service provider to do this, although we do here. - let statusExpectation = self.expectation(description: "status received") - - var responses: [IdentityPayload] = [] - // Here we use `IdentityPayload` for our response type: we define it below such that it does - // not deserialize the bytes provided to it by gRPC. - let rpc: BidirectionalStreamingCall = self.client - .makeBidirectionalStreamingCall( - path: "/CustomPayload/AddOneAndReverseMessage", - handler: { responses.append($0) } - ) - - let request = CustomPayload(message: "message", number: 42) - rpc.sendMessage(request, promise: nil) - rpc.sendEnd(promise: nil) - - // Wait for the RPC to finish before comparing responses. - rpc.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation) - self.wait(for: [statusExpectation], timeout: 1.0) - - guard var response = responses.first?.buffer else { - XCTFail("RPC completed without a response") - return - } - - // We just took the raw bytes from the payload: we can still decode it because we know the - // server returned a serialized `CustomPayload`. - let actual = try CustomPayload(serializedByteBuffer: &response) - XCTAssertEqual(actual.message, "egassem") - XCTAssertEqual(actual.number, 43) - } - - func testCustomPayloadUnary() throws { - let rpc: UnaryCall = self.client.makeUnaryCall( - path: "/CustomPayload/Reverse", - request: StringPayload(message: "foobarbaz") - ) - - XCTAssertEqual(try rpc.response.map { $0.message }.wait(), "zabraboof") - XCTAssertEqual(try rpc.status.map { $0.code }.wait(), .ok) - } - - func testCustomPayloadClientStreaming() throws { - let rpc: ClientStreamingCall = self.client - .makeClientStreamingCall(path: "/CustomPayload/ReverseThenJoin") - rpc.sendMessages(["foo", "bar", "baz"].map(StringPayload.init(message:)), promise: nil) - rpc.sendEnd(promise: nil) - - XCTAssertEqual(try rpc.response.map { $0.message }.wait(), "baz bar foo") - XCTAssertEqual(try rpc.status.map { $0.code }.wait(), .ok) - } - - func testCustomPayloadServerStreaming() throws { - let message = "abc" - var expectedIterator = message.reversed().makeIterator() - - let rpc: ServerStreamingCall = self.client - .makeServerStreamingCall( - path: "/CustomPayload/ReverseThenSplit", - request: StringPayload(message: message) - ) { response in - if let next = expectedIterator.next() { - XCTAssertEqual(String(next), response.message) - } else { - XCTFail("Unexpected message: \(response.message)") - } - } - - XCTAssertEqual(try rpc.status.map { $0.code }.wait(), .ok) - } -} - -// MARK: Custom Payload Service - -private class CustomPayloadProvider: CallHandlerProvider { - var serviceName: Substring = "CustomPayload" - - fileprivate func reverseString( - request: StringPayload, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - let reversed = StringPayload(message: String(request.message.reversed())) - return context.eventLoop.makeSucceededFuture(reversed) - } - - fileprivate func reverseThenJoin( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - var messages: [String] = [] - - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(request): - messages.append(request.message) - - case .end: - let response = messages.reversed().joined(separator: " ") - context.responsePromise.succeed(StringPayload(message: response)) - } - }) - } - - fileprivate func reverseThenSplit( - request: StringPayload, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - let responses = request.message.reversed().map { - context.sendResponse(StringPayload(message: String($0))) - } - - return EventLoopFuture.andAllSucceed(responses, on: context.eventLoop).map { .ok } - } - - // Bidirectional RPC which returns a new `CustomPayload` for each `CustomPayload` received. - // The returned payloads have their `message` reversed and their `number` incremented by one. - fileprivate func addOneAndReverseMessage( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(payload): - let response = CustomPayload( - message: String(payload.message.reversed()), - number: payload.number + 1 - ) - _ = context.sendResponse(response) - - case .end: - context.statusPromise.succeed(.ok) - } - }) - } - - func handle(method name: Substring, context: CallHandlerContext) -> GRPCServerHandlerProtocol? { - switch name { - case "Reverse": - return UnaryServerHandler( - context: context, - requestDeserializer: GRPCPayloadDeserializer(), - responseSerializer: GRPCPayloadSerializer(), - interceptors: [], - userFunction: self.reverseString(request:context:) - ) - - case "ReverseThenJoin": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: GRPCPayloadDeserializer(), - responseSerializer: GRPCPayloadSerializer(), - interceptors: [], - observerFactory: self.reverseThenJoin(context:) - ) - - case "ReverseThenSplit": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: GRPCPayloadDeserializer(), - responseSerializer: GRPCPayloadSerializer(), - interceptors: [], - userFunction: self.reverseThenSplit(request:context:) - ) - - case "AddOneAndReverseMessage": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: GRPCPayloadDeserializer(), - responseSerializer: GRPCPayloadSerializer(), - interceptors: [], - observerFactory: self.addOneAndReverseMessage(context:) - ) - - default: - return nil - } - } -} - -private struct IdentityPayload: GRPCPayload { - var buffer: ByteBuffer - - init(serializedByteBuffer: inout ByteBuffer) throws { - self.buffer = serializedByteBuffer - } - - func serialize(into buffer: inout ByteBuffer) throws { - // This will never be called, however, it could be implemented as a direct copy of the bytes - // we hold, e.g.: - // - // var copy = self.buffer - // buffer.writeBuffer(©) - fatalError("Unimplemented") - } -} - -/// A toy custom payload which holds a `String` and an `Int64`. -/// -/// The payload is serialized as: -/// - the `UInt32` encoded length of the message, -/// - the UTF-8 encoded bytes of the message, and -/// - the `Int64` bytes of the number. -private struct CustomPayload: GRPCPayload, Equatable { - var message: String - var number: Int64 - - init(message: String, number: Int64) { - self.message = message - self.number = number - } - - init(serializedByteBuffer: inout ByteBuffer) throws { - guard let messageLength = serializedByteBuffer.readInteger(as: UInt32.self), - let message = serializedByteBuffer.readString(length: Int(messageLength)), - let number = serializedByteBuffer.readInteger(as: Int64.self) - else { - throw GRPCError.DeserializationFailure() - } - - self.message = message - self.number = number - } - - func serialize(into buffer: inout ByteBuffer) throws { - buffer.writeInteger(UInt32(self.message.count)) - buffer.writeString(self.message) - buffer.writeInteger(self.number) - } -} - -private struct StringPayload: GRPCPayload { - var message: String - - init(message: String) { - self.message = message - } - - init(serializedByteBuffer: inout ByteBuffer) throws { - self.message = serializedByteBuffer.readString(length: serializedByteBuffer.readableBytes)! - } - - func serialize(into buffer: inout ByteBuffer) throws { - buffer.writeString(self.message) - } -} diff --git a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift b/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift deleted file mode 100644 index 50eb009a6..000000000 --- a/Tests/GRPCTests/GRPCIdleHandlerStateMachineTests.swift +++ /dev/null @@ -1,635 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import NIOHTTP2 -import XCTest - -@testable import GRPC - -class GRPCIdleHandlerStateMachineTests: GRPCTestCase { - private func makeClientStateMachine() -> GRPCIdleHandlerStateMachine { - return GRPCIdleHandlerStateMachine(role: .client, logger: self.clientLogger) - } - - private func makeServerStateMachine() -> GRPCIdleHandlerStateMachine { - return GRPCIdleHandlerStateMachine(role: .server, logger: self.serverLogger) - } - - private func makeNoOpScheduled() -> Scheduled { - let loop = EmbeddedEventLoop() - return loop.scheduleTask(deadline: .distantFuture) { return () } - } - - func testInactiveBeforeSettings() { - var stateMachine = self.makeClientStateMachine() - let op1 = stateMachine.channelInactive() - op1.assertConnectionManager(.inactive) - } - - func testInactiveAfterSettings() { - var stateMachine = self.makeClientStateMachine() - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - - let readyStateMachine = stateMachine - - // Inactive with a stream open. - let op2 = stateMachine.streamCreated(withID: 1) - op2.assertDoNothing() - let op3 = stateMachine.channelInactive() - op3.assertConnectionManager(.inactive) - - // Inactive with no open streams. - stateMachine = readyStateMachine - let op4 = stateMachine.channelInactive() - op4.assertConnectionManager(.idle) - } - - func testInactiveWhenWaitingToIdle() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the timeout. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - // Become inactive unexpectedly. - let op3 = stateMachine.channelInactive() - op3.assertConnectionManager(.idle) - } - - func testInactiveWhenQuiescing() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - - // Try a few combinations: initiator of shutdown, and whether streams are open or not when - // shutdown is initiated. - let readyStateMachine = stateMachine - - // (1) Peer initiates shutdown, no streams are open. - do { - let op2 = stateMachine.receiveGoAway() - op2.assertGoAway(streamID: .rootStream) - op2.assertShouldClose() - - // We become idle. - let op3 = stateMachine.channelInactive() - op3.assertConnectionManager(.idle) - } - - // (2) We initiate shutdown, no streams are open. - stateMachine = readyStateMachine - do { - let op2 = stateMachine.initiateGracefulShutdown() - op2.assertGoAway(streamID: .rootStream) - op2.assertShouldClose() - - // We become idle. - let op3 = stateMachine.channelInactive() - op3.assertConnectionManager(.idle) - } - - stateMachine = readyStateMachine - _ = stateMachine.streamCreated(withID: 1) - let streamOpenStateMachine = stateMachine - - // (3) Peer initiates shutdown, streams are open. - do { - let op2 = stateMachine.receiveGoAway() - op2.assertNoGoAway() - op2.assertShouldNotClose() - - // We become inactive. - let op3 = stateMachine.channelInactive() - op3.assertConnectionManager(.inactive) - } - - // (4) We initiate shutdown, streams are open. - stateMachine = streamOpenStateMachine - do { - let op2 = stateMachine.initiateGracefulShutdown() - op2.assertShouldNotClose() - - // We become inactive. - let op3 = stateMachine.channelInactive() - op3.assertConnectionManager(.inactive) - } - } - - func testReceiveSettings() { - var stateMachine = self.makeClientStateMachine() - - // No open streams. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Open streams. - stateMachine = self.makeClientStateMachine() - let op2 = stateMachine.streamCreated(withID: 1) - op2.assertDoNothing() - let op3 = stateMachine.receiveSettings([]) - // No idle timeout to cancel. - op3.assertConnectionManager(.ready) - op3.assertNoIdleTimeoutTask() - } - - func testReceiveSettingsWhenWaitingToIdle() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Receive more settings. - let op2 = stateMachine.receiveSettings([]) - op2.assertDoNothing() - - // Schedule the timeout. - let op3 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op3.assertDoNothing() - - // More settings. - let op4 = stateMachine.receiveSettings([]) - op4.assertDoNothing() - } - - func testReceiveGoAwayWhenWaitingToIdle() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the timeout. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - // Receive a GOAWAY frame. - let op3 = stateMachine.receiveGoAway() - op3.assertGoAway(streamID: .rootStream) - op3.assertShouldClose() - op3.assertCancelIdleTimeout() - op3.assertConnectionManager(.quiescing) - - // Close; we were going to go idle anyway. - let op4 = stateMachine.channelInactive() - op4.assertConnectionManager(.idle) - } - - func testInitiateGracefulShutdownWithNoOpenStreams() { - var stateMachine = self.makeClientStateMachine() - - // No open streams: so GOAWAY and close. - let op1 = stateMachine.initiateGracefulShutdown() - op1.assertGoAway(streamID: .rootStream) - op1.assertShouldClose() - op1.assertConnectionManager(.quiescing) - - // Closed. - let op2 = stateMachine.channelInactive() - op2.assertConnectionManager(.inactive) - } - - func testInitiateGracefulShutdownWithOpenStreams() { - var stateMachine = self.makeClientStateMachine() - - // Open a stream. - let op1 = stateMachine.streamCreated(withID: 1) - op1.assertDoNothing() - - // Initiate shutdown. - let op2 = stateMachine.initiateGracefulShutdown() - op2.assertShouldNotClose() - op2.assertConnectionManager(.quiescing) - - // Receive a GOAWAY; no change. - let op3 = stateMachine.receiveGoAway() - op3.assertDoNothing() - - // Close the remaining open stream, connection should close as a result. - let op4 = stateMachine.streamClosed(withID: 1) - op4.assertShouldClose() - - // Connection closed. - let op5 = stateMachine.channelInactive() - op5.assertConnectionManager(.inactive) - } - - func testInitiateGracefulShutdownWhenWaitingToIdle() { - var stateMachine = self.makeClientStateMachine() - - // Become 'ready' - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the task. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - // Initiate shutdown: cancel the timeout, send a GOAWAY and close. - let op3 = stateMachine.initiateGracefulShutdown() - op3.assertCancelIdleTimeout() - op3.assertGoAway(streamID: .rootStream) - op3.assertShouldClose() - - // Closed: become inactive. - let op4 = stateMachine.channelInactive() - op4.assertConnectionManager(.inactive) - } - - func testInitiateGracefulShutdownWhenQuiescing() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Open a few streams. - for streamID in stride(from: HTTP2StreamID(1), to: HTTP2StreamID(6), by: 2) { - let op = stateMachine.streamCreated(withID: streamID) - op.assertDoNothing() - } - - // Receive a GOAWAY. - let op2 = stateMachine.receiveGoAway() - op2.assertNoGoAway() - - // Initiate shutdown from our side: we've already sent GOAWAY and have a stream open, we don't - // need to do anything. - let op3 = stateMachine.initiateGracefulShutdown() - op3.assertDoNothing() - - // Close the first couple of streams; should be a no-op. - for streamID in [HTTP2StreamID(1), HTTP2StreamID(3)] { - let op = stateMachine.streamClosed(withID: streamID) - op.assertDoNothing() - } - // Close the final stream. - let op4 = stateMachine.streamClosed(withID: 5) - op4.assertShouldClose() - - // Initiate shutdown again: we're closing so this should be a no-op. - let op5 = stateMachine.initiateGracefulShutdown() - op5.assertDoNothing() - - // Closed. - let op6 = stateMachine.channelInactive() - op6.assertConnectionManager(.inactive) - } - - func testScheduleIdleTaskWhenStreamsAreOpen() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Open a stream before scheduling the task. - let op2 = stateMachine.streamCreated(withID: 1) - op2.assertDoNothing() - - // Schedule an idle timeout task: there are open streams so this should be cancelled. - let op3 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op3.assertCancelIdleTimeout() - } - - func testScheduleIdleTaskWhenQuiescing() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Save the state machine so we can test a few branches. - let readyStateMachine = stateMachine - - // (1) Scheduled when quiescing. - let op2 = stateMachine.streamCreated(withID: 1) - op2.assertDoNothing() - // Start shutting down. - _ = stateMachine.initiateGracefulShutdown() - // Schedule an idle timeout task: we're quiescing, so cancel the task. - let op4 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op4.assertCancelIdleTimeout() - - // (2) Scheduled when closing. - stateMachine = readyStateMachine - let op5 = stateMachine.initiateGracefulShutdown() - op5.assertGoAway(streamID: .rootStream) - op5.assertShouldClose() - // Schedule an idle timeout task: we're already closing, so cancel the task. - let op6 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op6.assertCancelIdleTimeout() - } - - func testIdleTimeoutTaskFiresWhenIdle() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the task. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - // Fire the task. - let op3 = stateMachine.idleTimeoutTaskFired() - op3.assertGoAway(streamID: .rootStream) - op3.assertShouldClose() - - // Close. - let op4 = stateMachine.channelInactive() - op4.assertConnectionManager(.idle) - } - - func testIdleTimeoutTaskFiresWhenClosed() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the task. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - // Close. - let op3 = stateMachine.channelInactive() - op3.assertCancelIdleTimeout() - - // Fire the idle timeout task. - let op4 = stateMachine.idleTimeoutTaskFired() - op4.assertDoNothing() - } - - func testShutdownNow() { - var stateMachine = self.makeClientStateMachine() - - let op1 = stateMachine.shutdownNow() - op1.assertGoAway(streamID: .rootStream) - op1.assertShouldClose() - - let op2 = stateMachine.channelInactive() - op2.assertConnectionManager(.inactive) - } - - func testShutdownNowWhenWaitingToIdle() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the task. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - let op3 = stateMachine.shutdownNow() - op3.assertGoAway(streamID: .rootStream) - op3.assertShouldClose() - - let op4 = stateMachine.channelInactive() - op4.assertConnectionManager(.inactive) - } - - func testShutdownNowWhenQuiescing() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Open a stream. - let op2 = stateMachine.streamCreated(withID: 1) - op2.assertDoNothing() - - // Initiate shutdown. - let op3 = stateMachine.initiateGracefulShutdown() - op3.assertNoGoAway() - - // Shutdown now. - let op4 = stateMachine.shutdownNow() - op4.assertShouldClose() - } - - func testNormalFlow() { - var stateMachine = self.makeClientStateMachine() - - // Become ready. - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the task. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - // Create a stream to cancel the task. - let op3 = stateMachine.streamCreated(withID: 1) - op3.assertCancelIdleTimeout() - - // Close the stream. - let op4 = stateMachine.streamClosed(withID: 1) - op4.assertScheduleIdleTimeout() - - // Receive a GOAWAY frame. - let op5 = stateMachine.receiveGoAway() - // We're the client, there are no server initiated streams, so GOAWAY with root stream. - op5.assertGoAway(streamID: 0) - // No open streams, so we can close now. Also assert the connection manager got a quiescing event. - op5.assertShouldClose() - op5.assertConnectionManager(.quiescing) - - // Closed. - let op6 = stateMachine.channelInactive() - // The peer initiated shutdown by sending GOAWAY, we'll idle. - op6.assertConnectionManager(.idle) - } - - func testClientSendsGoAwayAndOpensStream() { - var stateMachine = self.makeServerStateMachine() - - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - op1.assertScheduleIdleTimeout() - - // Schedule the idle timeout. - let op2 = stateMachine.scheduledIdleTimeoutTask(self.makeNoOpScheduled()) - op2.assertDoNothing() - - // Create a stream to cancel the task. - let op3 = stateMachine.streamCreated(withID: 1) - op3.assertCancelIdleTimeout() - - // Receive a GOAWAY frame from the client. - let op4 = stateMachine.receiveGoAway() - op4.assertGoAway(streamID: .maxID) - op4.assertShouldPingAfterGoAway() - op4.assertConnectionManager(.quiescing) - - // Create another stream. This is fine, the client hasn't ack'd the ping yet. - let op5 = stateMachine.streamCreated(withID: 7) - op5.assertDoNothing() - - // Receiving the ping is handled by a different state machine which will tell us to ratchet - // down the go away stream ID. - let op6 = stateMachine.ratchetDownGoAwayStreamID() - op6.assertGoAway(streamID: 7) - op6.assertShouldNotPingAfterGoAway() - - let op7 = stateMachine.streamClosed(withID: 7) - op7.assertDoNothing() - - let op8 = stateMachine.streamClosed(withID: 1) - op8.assertShouldClose() - } - - func testRatchetDownStreamIDWhenNotQuiescing() { - var stateMachine = self.makeServerStateMachine() - _ = stateMachine.receiveSettings([]) - - // from the 'operating' state. - stateMachine.ratchetDownGoAwayStreamID().assertDoNothing() - - // move to the 'waiting to idle' state. - let promise = EmbeddedEventLoop().makePromise(of: Void.self) - let task = Scheduled(promise: promise, cancellationTask: {}) - stateMachine.scheduledIdleTimeoutTask(task).assertDoNothing() - promise.succeed(()) - stateMachine.ratchetDownGoAwayStreamID().assertDoNothing() - - // move to 'closing' - _ = stateMachine.idleTimeoutTaskFired() - stateMachine.ratchetDownGoAwayStreamID().assertDoNothing() - - // move to 'closed' - _ = stateMachine.channelInactive() - stateMachine.ratchetDownGoAwayStreamID().assertDoNothing() - } - - func testStreamIDWhenQuiescing() { - var stateMachine = self.makeClientStateMachine() - let op1 = stateMachine.receiveSettings([]) - op1.assertConnectionManager(.ready) - - // Open a stream so we enter quiescing when receiving the GOAWAY. - let op2 = stateMachine.streamCreated(withID: 1) - op2.assertDoNothing() - - let op3 = stateMachine.receiveGoAway() - op3.assertConnectionManager(.quiescing) - - // Create a new stream. This can happen if the GOAWAY races with opening the stream; HTTP2 will - // open and then close the stream with an error. - let op4 = stateMachine.streamCreated(withID: 3) - op4.assertDoNothing() - - // Close the newly opened stream. - let op5 = stateMachine.streamClosed(withID: 3) - op5.assertDoNothing() - - // Close the original stream. - let op6 = stateMachine.streamClosed(withID: 1) - // Now we can send a GOAWAY with stream ID zero (we're the client and the server didn't open - // any streams). - XCTAssertEqual(op6.sendGoAwayWithLastPeerInitiatedStreamID, 0) - } -} - -extension GRPCIdleHandlerStateMachine.Operations { - func assertDoNothing() { - XCTAssertNil(self.connectionManagerEvent) - XCTAssertNil(self.idleTask) - XCTAssertNil(self.sendGoAwayWithLastPeerInitiatedStreamID) - XCTAssertFalse(self.shouldCloseChannel) - XCTAssertFalse(self.shouldPingAfterGoAway) - } - - func assertGoAway(streamID: HTTP2StreamID) { - XCTAssertEqual(self.sendGoAwayWithLastPeerInitiatedStreamID, streamID) - } - - func assertNoGoAway() { - XCTAssertNil(self.sendGoAwayWithLastPeerInitiatedStreamID) - } - - func assertScheduleIdleTimeout() { - switch self.idleTask { - case .some(.schedule): - () - case .some(.cancel), .none: - XCTFail("Expected 'schedule' but was '\(String(describing: self.idleTask))'") - } - } - - func assertCancelIdleTimeout() { - switch self.idleTask { - case .some(.cancel): - () - case .some(.schedule), .none: - XCTFail("Expected 'cancel' but was '\(String(describing: self.idleTask))'") - } - } - - func assertNoIdleTimeoutTask() { - XCTAssertNil(self.idleTask) - } - - func assertConnectionManager(_ event: GRPCIdleHandlerStateMachine.ConnectionManagerEvent) { - XCTAssertEqual(self.connectionManagerEvent, event) - } - - func assertNoConnectionManagerEvent() { - XCTAssertNil(self.connectionManagerEvent) - } - - func assertShouldClose() { - XCTAssertTrue(self.shouldCloseChannel) - } - - func assertShouldNotClose() { - XCTAssertFalse(self.shouldCloseChannel) - } - - func assertShouldPingAfterGoAway() { - XCTAssert(self.shouldPingAfterGoAway) - } - - func assertShouldNotPingAfterGoAway() { - XCTAssertFalse(self.shouldPingAfterGoAway) - } -} diff --git a/Tests/GRPCTests/GRPCIdleTests.swift b/Tests/GRPCTests/GRPCIdleTests.swift deleted file mode 100644 index aa0635be6..000000000 --- a/Tests/GRPCTests/GRPCIdleTests.swift +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import NIOCore -import NIOPosix -import XCTest - -@testable import GRPC - -class GRPCIdleTests: GRPCTestCase { - func testClientIdleTimeout() { - XCTAssertNoThrow( - try self - .doTestIdleTimeout(serverIdle: .minutes(5), clientIdle: .milliseconds(100)) - ) - } - - func testServerIdleTimeout() throws { - XCTAssertNoThrow( - try self - .doTestIdleTimeout(serverIdle: .milliseconds(100), clientIdle: .minutes(5)) - ) - } - - func doTestIdleTimeout(serverIdle: TimeAmount, clientIdle: TimeAmount) throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - // Setup a server. - let server = try Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) - .withConnectionIdleTimeout(serverIdle) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - // Setup a state change recorder for the client. - let stateRecorder = RecordingConnectivityDelegate() - stateRecorder.expectChanges(3) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .ready), - Change(from: .ready, to: .idle), - ] - ) - } - - // Setup a connection. - let connection = ClientConnection.insecure(group: group) - .withConnectivityStateDelegate(stateRecorder) - .withConnectionIdleTimeout(clientIdle) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: server.channel.localAddress!.port!) - defer { - XCTAssertNoThrow(try connection.close().wait()) - } - - let client = Echo_EchoNIOClient(channel: connection) - - // Make a call; this will trigger channel creation. - let get = client.get(.with { $0.text = "ignored" }) - let status = try get.status.wait() - XCTAssertEqual(status.code, .ok) - - // Now wait for the state changes. - stateRecorder.waitForExpectedChanges(timeout: .seconds(10)) - } -} diff --git a/Tests/GRPCTests/GRPCInteroperabilityTests.swift b/Tests/GRPCTests/GRPCInteroperabilityTests.swift deleted file mode 100644 index e86c9b452..000000000 --- a/Tests/GRPCTests/GRPCInteroperabilityTests.swift +++ /dev/null @@ -1,419 +0,0 @@ -/* - * 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 Foundation -import GRPC -import GRPCInteroperabilityTestsImplementation -import NIOCore -import NIOPosix -import XCTest - -/// These are the gRPC interoperability tests running on the NIO client and server. -class GRPCInsecureInteroperabilityTests: GRPCTestCase { - var useTLS: Bool { return false } - - var serverEventLoopGroup: EventLoopGroup! - var server: Server! - var serverPort: Int! - - var clientEventLoopGroup: EventLoopGroup! - var clientConnection: ClientConnection! - - override func setUp() { - super.setUp() - - self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.server = try! makeInteroperabilityTestServer( - host: "localhost", - port: 0, - eventLoopGroup: self.serverEventLoopGroup!, - serviceProviders: [self.makeProvider()], - useTLS: self.useTLS, - logger: self.serverLogger - ).wait() - - guard let serverPort = self.server.channel.localAddress?.port else { - XCTFail("Unable to get server port") - return - } - - self.serverPort = serverPort - - self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - // This may throw if we shutdown before the channel was ready. - try? self.clientConnection?.close().wait() - XCTAssertNoThrow(try self.clientEventLoopGroup.syncShutdownGracefully()) - self.clientConnection = nil - self.clientEventLoopGroup = nil - - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully()) - self.server = nil - self.serverPort = nil - self.serverEventLoopGroup = nil - - super.tearDown() - } - - internal func makeProvider() -> CallHandlerProvider { - return TestServiceProvider() - } - - private func doRunTest(_ testCase: InteroperabilityTestCase, line: UInt = #line) { - // Does the server support the test? - let implementedFeatures = TestServiceProvider.implementedFeatures - let missingFeatures = testCase.requiredServerFeatures.subtracting(implementedFeatures) - guard missingFeatures.isEmpty else { - print("\(testCase.name) requires features the server does not implement: \(missingFeatures)") - return - } - - let test = testCase.makeTest() - let builder = makeInteroperabilityTestClientBuilder( - group: self.clientEventLoopGroup, - useTLS: self.useTLS - ).withBackgroundActivityLogger(self.clientLogger) - test.configure(builder: builder) - self.clientConnection = builder.connect(host: "localhost", port: self.serverPort) - XCTAssertNoThrow(try test.run(using: self.clientConnection), line: line) - } - - func testEmptyUnary() { - self.doRunTest(.emptyUnary) - } - - func testCacheableUnary() { - self.doRunTest(.cacheableUnary) - } - - func testLargeUnary() { - self.doRunTest(.largeUnary) - } - - func testClientCompressedUnary() { - self.doRunTest(.clientCompressedUnary) - } - - func testServerCompressedUnary() { - self.doRunTest(.serverCompressedUnary) - } - - func testClientStreaming() { - self.doRunTest(.clientStreaming) - } - - func testClientCompressedStreaming() { - self.doRunTest(.clientCompressedStreaming) - } - - func testServerStreaming() { - self.doRunTest(.serverStreaming) - } - - func testServerCompressedStreaming() { - self.doRunTest(.serverCompressedStreaming) - } - - func testPingPong() { - self.doRunTest(.pingPong) - } - - func testEmptyStream() { - self.doRunTest(.emptyStream) - } - - func testCustomMetadata() { - self.doRunTest(.customMetadata) - } - - func testStatusCodeAndMessage() { - self.doRunTest(.statusCodeAndMessage) - } - - func testSpecialStatusAndMessage() { - self.doRunTest(.specialStatusMessage) - } - - func testUnimplementedMethod() { - self.doRunTest(.unimplementedMethod) - } - - func testUnimplementedService() { - self.doRunTest(.unimplementedService) - } - - func testCancelAfterBegin() { - self.doRunTest(.cancelAfterBegin) - } - - func testCancelAfterFirstResponse() { - self.doRunTest(.cancelAfterFirstResponse) - } - - func testTimeoutOnSleepingServer() { - self.doRunTest(.timeoutOnSleepingServer) - } -} - -#if canImport(NIOSSL) -class GRPCSecureInteroperabilityTests: GRPCInsecureInteroperabilityTests { - override var useTLS: Bool { return true } - - override func testEmptyUnary() { - super.testEmptyUnary() - } - - override func testCacheableUnary() { - super.testCacheableUnary() - } - - override func testLargeUnary() { - super.testLargeUnary() - } - - override func testClientCompressedUnary() { - super.testClientCompressedUnary() - } - - override func testServerCompressedUnary() { - super.testServerCompressedUnary() - } - - override func testClientStreaming() { - super.testClientStreaming() - } - - override func testClientCompressedStreaming() { - super.testClientCompressedStreaming() - } - - override func testServerStreaming() { - super.testServerStreaming() - } - - override func testServerCompressedStreaming() { - super.testServerCompressedStreaming() - } - - override func testPingPong() { - super.testPingPong() - } - - override func testEmptyStream() { - super.testEmptyStream() - } - - override func testCustomMetadata() { - super.testCustomMetadata() - } - - override func testStatusCodeAndMessage() { - super.testStatusCodeAndMessage() - } - - override func testSpecialStatusAndMessage() { - super.testSpecialStatusAndMessage() - } - - override func testUnimplementedMethod() { - super.testUnimplementedMethod() - } - - override func testUnimplementedService() { - super.testUnimplementedService() - } - - override func testCancelAfterBegin() { - super.testCancelAfterBegin() - } - - override func testCancelAfterFirstResponse() { - super.testCancelAfterFirstResponse() - } - - override func testTimeoutOnSleepingServer() { - super.testTimeoutOnSleepingServer() - } -} -#endif // canImport(NIOSSL) - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -class GRPCInsecureInteroperabilityAsyncTests: GRPCInsecureInteroperabilityTests { - override func makeProvider() -> CallHandlerProvider { - return TestServiceAsyncProvider() - } - - override func testEmptyStream() { - super.testEmptyStream() - } - - override func testPingPong() { - super.testPingPong() - } - - override func testEmptyUnary() { - super.testEmptyUnary() - } - - override func testTimeoutOnSleepingServer() { - super.testTimeoutOnSleepingServer() - } - - override func testCacheableUnary() { - super.testCacheableUnary() - } - - override func testLargeUnary() { - super.testLargeUnary() - } - - override func testServerCompressedUnary() { - super.testServerCompressedUnary() - } - - override func testStatusCodeAndMessage() { - super.testStatusCodeAndMessage() - } - - override func testUnimplementedService() { - super.testUnimplementedService() - } - - override func testCancelAfterBegin() { - super.testCancelAfterBegin() - } - - override func testCustomMetadata() { - super.testCustomMetadata() - } - - override func testServerStreaming() { - super.testServerStreaming() - } - - override func testClientStreaming() { - super.testClientStreaming() - } - - override func testUnimplementedMethod() { - super.testUnimplementedMethod() - } - - override func testServerCompressedStreaming() { - super.testServerCompressedStreaming() - } - - override func testCancelAfterFirstResponse() { - super.testCancelAfterFirstResponse() - } - - override func testSpecialStatusAndMessage() { - super.testSpecialStatusAndMessage() - } - - override func testClientCompressedStreaming() { - super.testClientCompressedStreaming() - } - - override func testClientCompressedUnary() { - super.testClientCompressedUnary() - } -} - -#if canImport(NIOSSL) -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -class GRPCSecureInteroperabilityAsyncTests: GRPCInsecureInteroperabilityAsyncTests { - override var useTLS: Bool { return true } - - override func testServerStreaming() { - super.testServerStreaming() - } - - override func testLargeUnary() { - super.testLargeUnary() - } - - override func testServerCompressedUnary() { - super.testServerCompressedUnary() - } - - override func testUnimplementedMethod() { - super.testUnimplementedMethod() - } - - override func testServerCompressedStreaming() { - super.testServerCompressedStreaming() - } - - override func testCustomMetadata() { - super.testCustomMetadata() - } - - override func testCancelAfterBegin() { - super.testCancelAfterBegin() - } - - override func testClientStreaming() { - super.testClientStreaming() - } - - override func testCacheableUnary() { - super.testCacheableUnary() - } - - override func testSpecialStatusAndMessage() { - super.testSpecialStatusAndMessage() - } - - override func testTimeoutOnSleepingServer() { - super.testTimeoutOnSleepingServer() - } - - override func testClientCompressedUnary() { - super.testClientCompressedUnary() - } - - override func testStatusCodeAndMessage() { - super.testStatusCodeAndMessage() - } - - override func testCancelAfterFirstResponse() { - super.testCancelAfterFirstResponse() - } - - override func testPingPong() { - super.testPingPong() - } - - override func testEmptyStream() { - super.testEmptyStream() - } - - override func testEmptyUnary() { - super.testEmptyUnary() - } - - override func testUnimplementedService() { - super.testUnimplementedService() - } - - override func testClientCompressedStreaming() { - super.testClientCompressedStreaming() - } -} -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/GRPCKeepaliveTests.swift b/Tests/GRPCTests/GRPCKeepaliveTests.swift deleted file mode 100644 index 359e70c5b..000000000 --- a/Tests/GRPCTests/GRPCKeepaliveTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import NIOCore -import NIOPosix -import XCTest - -@testable import GRPC - -class GRPCClientKeepaliveTests: GRPCTestCase { - func testKeepaliveTimeoutFiresBeforeConnectionIsReady() throws { - // This test relates to https://github.com/grpc/grpc-swift/issues/949 - // - // When a stream is created, a ping may be sent on the connection. If a ping is sent we then - // schedule a task for some time in the future to close the connection (if we don't receive the - // ping ack in the meantime). - // - // The task to close actually fires an event which is picked up by the idle handler; this will - // tell the connection manager to idle the connection. However, the connection manager only - // tolerates being idled from the ready state. Since we protect from idling multiple times in - // the handler we must be in a state where we have connection but are not yet ready (i.e. - // channel active has fired but we have not seen the initial settings frame). To be in this - // state the user must be using the 'fastFailure' call start behaviour (if this is not the case - // then no channel will be vended until we reach the ready state, so it would not be possible - // to create the stream). - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - // Setup a server. - let server = try Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - // Setup a connection. We'll add a handler to drop all reads, this is somewhat equivalent to - // simulating bad network conditions and allows us to setup a connection and have our keepalive - // timeout expire. - let connection = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - // See above comments for why we need this. - .withCallStartBehavior(.fastFailure) - .withKeepalive(.init(interval: .seconds(1), timeout: .milliseconds(100))) - .withDebugChannelInitializer { channel in - channel.pipeline.addHandler(ReadDroppingHandler(), position: .first) - } - .connect(host: "localhost", port: server.channel.localAddress!.port!) - defer { - XCTAssertNoThrow(try connection.close().wait()) - } - - let client = Echo_EchoNIOClient(channel: connection) - let get = client.get(.with { $0.text = "Hello" }) - XCTAssertThrowsError(try get.response.wait()) - XCTAssertEqual(try get.status.map { $0.code }.wait(), .unavailable) - } - - class ReadDroppingHandler: ChannelDuplexHandler { - typealias InboundIn = Any - typealias OutboundIn = Any - - func channelRead(context: ChannelHandlerContext, data: NIOAny) {} - } -} diff --git a/Tests/GRPCTests/GRPCMessageLengthLimitTests.swift b/Tests/GRPCTests/GRPCMessageLengthLimitTests.swift deleted file mode 100644 index ceaa78314..000000000 --- a/Tests/GRPCTests/GRPCMessageLengthLimitTests.swift +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import GRPC -import NIOCore -import NIOPosix -import XCTest - -final class GRPCMessageLengthLimitTests: GRPCTestCase { - private var group: EventLoopGroup! - private var server: Server! - private var connection: ClientConnection! - - private var echo: Echo_EchoNIOClient { - return Echo_EchoNIOClient(channel: self.connection) - } - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - XCTAssertNoThrow(try self.connection?.close().wait()) - XCTAssertNoThrow(try self.server?.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - private func startEchoServer(receiveLimit: Int) throws { - self.server = try Server.insecure(group: self.group) - .withServiceProviders([EchoProvider()]) - .withMaximumReceiveMessageLength(receiveLimit) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - } - - private func startConnection(receiveLimit: Int) { - self.connection = ClientConnection.insecure(group: self.group) - .withMaximumReceiveMessageLength(receiveLimit) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "127.0.0.1", port: self.server.channel.localAddress!.port!) - } - - private func makeRequest(minimumLength: Int) -> Echo_EchoRequest { - return .with { - $0.text = String(repeating: "x", count: minimumLength) - } - } - - func testServerRejectsLongUnaryRequest() throws { - // Server limits request size to 1024, no client limits. - try self.startEchoServer(receiveLimit: 1024) - self.startConnection(receiveLimit: .max) - - let get = self.echo.get(self.makeRequest(minimumLength: 1024)) - XCTAssertThrowsError(try get.response.wait()) - XCTAssertEqual(try get.status.map { $0.code }.wait(), .resourceExhausted) - } - - func testServerRejectsLongClientStreamingRequest() throws { - try self.startEchoServer(receiveLimit: 1024) - self.startConnection(receiveLimit: .max) - - let collect = self.echo.collect() - XCTAssertNoThrow(try collect.sendMessage(self.makeRequest(minimumLength: 1)).wait()) - XCTAssertNoThrow(try collect.sendMessage(self.makeRequest(minimumLength: 1024)).wait()) - // (No need to send end, the server is going to close the RPC because the message was too long.) - - XCTAssertThrowsError(try collect.response.wait()) - XCTAssertEqual(try collect.status.map { $0.code }.wait(), .resourceExhausted) - } - - func testServerRejectsLongServerStreamingRequest() throws { - try self.startEchoServer(receiveLimit: 1024) - self.startConnection(receiveLimit: .max) - - let expand = self.echo.expand(self.makeRequest(minimumLength: 1024)) { _ in - XCTFail("Unexpected response") - } - - XCTAssertEqual(try expand.status.map { $0.code }.wait(), .resourceExhausted) - } - - func testServerRejectsLongBidirectionalStreamingRequest() throws { - try self.startEchoServer(receiveLimit: 1024) - self.startConnection(receiveLimit: .max) - - let update = self.echo.update { _ in } - - XCTAssertNoThrow(try update.sendMessage(self.makeRequest(minimumLength: 1)).wait()) - XCTAssertNoThrow(try update.sendMessage(self.makeRequest(minimumLength: 1024)).wait()) - // (No need to send end, the server is going to close the RPC because the message was too long.) - - XCTAssertEqual(try update.status.map { $0.code }.wait(), .resourceExhausted) - } - - func testClientRejectsLongUnaryResponse() throws { - // No server limits, client limits response size to 1024. - try self.startEchoServer(receiveLimit: .max) - self.startConnection(receiveLimit: 1024) - - let get = self.echo.get(.with { $0.text = String(repeating: "x", count: 1024) }) - XCTAssertThrowsError(try get.response.wait()) - XCTAssertEqual(try get.status.map { $0.code }.wait(), .resourceExhausted) - } - - func testClientRejectsLongClientStreamingResponse() throws { - try self.startEchoServer(receiveLimit: .max) - self.startConnection(receiveLimit: 1024) - - let collect = self.echo.collect() - XCTAssertNoThrow(try collect.sendMessage(self.makeRequest(minimumLength: 1)).wait()) - XCTAssertNoThrow(try collect.sendMessage(self.makeRequest(minimumLength: 1024)).wait()) - XCTAssertNoThrow(try collect.sendEnd().wait()) - - XCTAssertThrowsError(try collect.response.wait()) - XCTAssertEqual(try collect.status.map { $0.code }.wait(), .resourceExhausted) - } - - func testClientRejectsLongServerStreamingRequest() throws { - try self.startEchoServer(receiveLimit: .max) - self.startConnection(receiveLimit: 1024) - - let expand = self.echo.expand(self.makeRequest(minimumLength: 1024)) { _ in - // Expand splits on spaces, there are no spaces in the request and it should be too long for - // the client to expect it. - XCTFail("Unexpected response") - } - - XCTAssertEqual(try expand.status.map { $0.code }.wait(), .resourceExhausted) - } - - func testClientRejectsLongServerBidirectionalStreamingResponse() throws { - try self.startEchoServer(receiveLimit: .max) - self.startConnection(receiveLimit: 1024) - - let update = self.echo.update { _ in } - - XCTAssertNoThrow(try update.sendMessage(self.makeRequest(minimumLength: 1)).wait()) - XCTAssertNoThrow(try update.sendMessage(self.makeRequest(minimumLength: 1024)).wait()) - // (No need to send end, the client will close the RPC when it receives a response which is too - // long. - - XCTAssertEqual(try update.status.map { $0.code }.wait(), .resourceExhausted) - } -} diff --git a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift b/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift deleted file mode 100644 index d0c0e1809..000000000 --- a/Tests/GRPCTests/GRPCNetworkFrameworkTests.swift +++ /dev/null @@ -1,271 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -#if canImport(Network) -import Dispatch -import EchoImplementation -import EchoModel -import GRPC -import Network -import NIOCore -import NIOPosix -import NIOSSL -import NIOTransportServices -import GRPCSampleData -import Security -import XCTest - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -final class GRPCNetworkFrameworkTests: GRPCTestCase { - private var server: Server! - private var client: ClientConnection! - private var identity: SecIdentity! - private var pkcs12Bundle: NIOSSLPKCS12Bundle! - private var tsGroup: NIOTSEventLoopGroup! - private var group: MultiThreadedEventLoopGroup! - private let queue = DispatchQueue(label: "io.grpc.verify-handshake") - - private static let p12bundleURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // (this file) - .deletingLastPathComponent() // GRPCTests - .deletingLastPathComponent() // Tests - .appendingPathComponent("Sources") - .appendingPathComponent("GRPCSampleData") - .appendingPathComponent("bundle") - .appendingPathExtension("p12") - - // Not really 'async' but there is no 'func setUp() throws' to override. - override func setUp() async throws { - try await super.setUp() - - self.tsGroup = NIOTSEventLoopGroup(loopCount: 1) - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - self.identity = try self.loadIdentity() - XCTAssertNotNil( - self.identity, - "Unable to load identity from '\(GRPCNetworkFrameworkTests.p12bundleURL)'" - ) - - self.pkcs12Bundle = try NIOSSLPKCS12Bundle( - file: GRPCNetworkFrameworkTests.p12bundleURL.path, - passphrase: "password".utf8 - ) - - XCTAssertNotNil( - self.pkcs12Bundle, - "Unable to load PCKS12 bundle from '\(GRPCNetworkFrameworkTests.p12bundleURL)'" - ) - } - - override func tearDown() { - XCTAssertNoThrow(try self.client?.close().wait()) - XCTAssertNoThrow(try self.server?.close().wait()) - XCTAssertNoThrow(try self.group?.syncShutdownGracefully()) - XCTAssertNoThrow(try self.tsGroup?.syncShutdownGracefully()) - super.tearDown() - } - - private func loadIdentity() throws -> SecIdentity? { - let data = try Data(contentsOf: GRPCNetworkFrameworkTests.p12bundleURL) - let options = [kSecImportExportPassphrase as String: "password"] - - var rawItems: CFArray? - let status = SecPKCS12Import(data as CFData, options as CFDictionary, &rawItems) - - switch status { - case errSecSuccess: - () - case errSecInteractionNotAllowed: - throw XCTSkip("Unable to import PKCS12 bundle: no interaction allowed") - default: - XCTFail("SecPKCS12Import: failed with status \(status)") - return nil - } - - let items = rawItems! as! [[String: Any]] - return items.first?[kSecImportItemIdentity as String] as! SecIdentity? - } - - private func doEchoGet() throws { - let echo = Echo_EchoNIOClient(channel: self.client) - let get = echo.get(.with { $0.text = "hello" }) - XCTAssertNoThrow(try get.response.wait()) - } - - private func startServer(_ builder: Server.Builder) throws { - self.server = - try builder - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - } - - private func startClient(_ builder: ClientConnection.Builder) { - self.client = - builder - .withBackgroundActivityLogger(self.clientLogger) - .withConnectionReestablishment(enabled: false) - .connect(host: "127.0.0.1", port: self.server.channel.localAddress!.port!) - } - - func testNetworkFrameworkServerWithNIOSSLClient() throws { - let serverBuilder = Server.usingTLSBackedByNetworkFramework( - on: self.tsGroup, - with: self.identity - ) - XCTAssertNoThrow(try self.startServer(serverBuilder)) - - let clientBuilder = ClientConnection.usingTLSBackedByNIOSSL(on: self.group) - .withTLS(serverHostnameOverride: "localhost") - .withTLS(trustRoots: .certificates(self.pkcs12Bundle.certificateChain)) - - self.startClient(clientBuilder) - - XCTAssertNoThrow(try self.doEchoGet()) - } - - func testNIOSSLServerOnMTELGWithNetworkFrameworkClient() throws { - try self.doTestNIOSSLServerWithNetworkFrameworkClient(serverGroup: self.group) - } - - func testNIOSSLServerOnNIOTSGroupWithNetworkFrameworkClient() throws { - try self.doTestNIOSSLServerWithNetworkFrameworkClient(serverGroup: self.tsGroup) - } - - func doTestNIOSSLServerWithNetworkFrameworkClient(serverGroup: EventLoopGroup) throws { - let serverBuilder = Server.usingTLSBackedByNIOSSL( - on: serverGroup, - certificateChain: self.pkcs12Bundle.certificateChain, - privateKey: self.pkcs12Bundle.privateKey - ) - XCTAssertNoThrow(try self.startServer(serverBuilder)) - - var certificate: SecCertificate? - guard SecIdentityCopyCertificate(self.identity, &certificate) == errSecSuccess else { - XCTFail("Unable to extract certificate from identity") - return - } - - let clientBuilder = ClientConnection.usingTLSBackedByNetworkFramework(on: self.tsGroup) - .withTLS(serverHostnameOverride: "localhost") - .withTLSHandshakeVerificationCallback(on: self.queue) { _, trust, verify in - let actualTrust = sec_trust_copy_ref(trust).takeRetainedValue() - SecTrustSetAnchorCertificates(actualTrust, [certificate!] as CFArray) - SecTrustEvaluateAsyncWithError(actualTrust, self.queue) { _, valid, error in - if let error = error { - XCTFail("Trust evaluation error: \(error)") - } - verify(valid) - } - } - - self.startClient(clientBuilder) - - XCTAssertNoThrow(try self.doEchoGet()) - } - - func testNetworkFrameworkTLServerAndClient() throws { - let serverBuilder = Server.usingTLSBackedByNetworkFramework( - on: self.tsGroup, - with: self.identity - ) - XCTAssertNoThrow(try self.startServer(serverBuilder)) - - var certificate: SecCertificate? - guard SecIdentityCopyCertificate(self.identity, &certificate) == errSecSuccess else { - XCTFail("Unable to extract certificate from identity") - return - } - - let clientBuilder = ClientConnection.usingTLSBackedByNetworkFramework(on: self.tsGroup) - .withTLS(serverHostnameOverride: "localhost") - .withTLSHandshakeVerificationCallback(on: self.queue) { _, trust, verify in - let actualTrust = sec_trust_copy_ref(trust).takeRetainedValue() - SecTrustSetAnchorCertificates(actualTrust, [certificate!] as CFArray) - SecTrustEvaluateAsyncWithError(actualTrust, self.queue) { _, valid, error in - if let error = error { - XCTFail("Trust evaluation error: \(error)") - } - verify(valid) - } - } - - self.startClient(clientBuilder) - - XCTAssertNoThrow(try self.doEchoGet()) - } - - func testWaiterPicksUpNWError( - _ configure: (inout GRPCChannelPool.Configuration) -> Void - ) async throws { - let builder = Server.usingTLSBackedByNIOSSL( - on: self.group, - certificateChain: [SampleCertificate.server.certificate], - privateKey: SamplePrivateKey.server - ) - - let server = try await builder.bind(host: "127.0.0.1", port: 0).get() - defer { try? server.close().wait() } - - let client = try GRPCChannelPool.with( - target: .hostAndPort("127.0.0.1", server.channel.localAddress!.port!), - transportSecurity: .tls(.makeClientConfigurationBackedByNetworkFramework()), - eventLoopGroup: self.tsGroup - ) { - configure(&$0) - } - - let echo = Echo_EchoAsyncClient(channel: client) - do { - let _ = try await echo.get(.with { $0.text = "ignored" }) - } catch let error as GRPCConnectionPoolError { - XCTAssertEqual(error.code, .deadlineExceeded) - XCTAssert(error.underlyingError is NWError) - } catch { - XCTFail("Expected GRPCConnectionPoolError") - } - - let promise = self.group.next().makePromise(of: Void.self) - client.closeGracefully(deadline: .now() + .seconds(1), promise: promise) - try await promise.futureResult.get() - } - - func testErrorPickedUpBeforeConnectTimeout() async throws { - try await self.testWaiterPicksUpNWError { - // Configure the wait time to be less than the connect timeout, the waiter - // should fail with the appropriate NWError before the connect times out. - $0.connectionPool.maxWaitTime = .milliseconds(500) - $0.connectionBackoff.minimumConnectionTimeout = 1.0 - } - } - - func testNotWaitingForConnectivity() async throws { - try await self.testWaiterPicksUpNWError { - // The minimum connect time is still high, but setting wait for activity to false - // means it fails on entering the waiting state rather than seeing out the connect - // timeout. - $0.connectionPool.maxWaitTime = .milliseconds(500) - $0.debugChannelInitializer = { channel in - channel.setOption(NIOTSChannelOptions.waitForActivity, value: false) - } - } - } -} - -#endif // canImport(Network) -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/GRPCPingHandlerTests.swift b/Tests/GRPCTests/GRPCPingHandlerTests.swift deleted file mode 100644 index 554a33062..000000000 --- a/Tests/GRPCTests/GRPCPingHandlerTests.swift +++ /dev/null @@ -1,481 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import NIOHTTP2 -import XCTest - -@testable import GRPC - -class GRPCPingHandlerTests: GRPCTestCase { - var pingHandler: PingHandler! - - func testClosingStreamWithoutPermitCalls() { - // Do not allow pings without calls - self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1)) - - // New stream created - var response: PingHandler.Action = self.pingHandler.streamCreated() - XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1))) - - // Stream closed - response = self.pingHandler.streamClosed() - XCTAssertEqual(response, .none) - } - - func testClosingStreamWithPermitCalls() { - // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect) - self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true) - - // New stream created - var response: PingHandler.Action = self.pingHandler.streamCreated() - XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1))) - - // Stream closed - response = self.pingHandler.streamClosed() - XCTAssertEqual(response, .none) - } - - func testIntervalWithCallInFlight() { - // Do not allow pings without calls - self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1)) - - // New stream created - var response: PingHandler.Action = self.pingHandler.streamCreated() - XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1))) - - // Move time to 1 second in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(1) - - // Send ping, which is valid - response = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Received valid pong, scheduled timeout should be cancelled - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true) - XCTAssertEqual(response, .cancelScheduledTimeout) - - // Stream closed - response = self.pingHandler.streamClosed() - XCTAssertEqual(response, .none) - } - - func testIntervalWithoutCallsInFlight() { - // Do not allow pings without calls - self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1)) - - // Send ping, which is invalid - let response: PingHandler.Action = self.pingHandler.pingFired() - XCTAssertEqual(response, .none) - } - - func testIntervalWithCallNoLongerInFlight() { - // Do not allow pings without calls - self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1)) - - // New stream created - var response: PingHandler.Action = self.pingHandler.streamCreated() - XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1))) - - // Stream closed - response = self.pingHandler.streamClosed() - XCTAssertEqual(response, .none) - - // Move time to 1 second in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(1) - - // Send ping, which is invalid - response = self.pingHandler.pingFired() - XCTAssertEqual(response, .none) - } - - func testIntervalWithoutCallsInFlightButPermitted() { - // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect) - self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true) - - // Send ping, which is valid - var response: PingHandler.Action = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Received valid pong, scheduled timeout should be cancelled - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true) - XCTAssertEqual(response, .cancelScheduledTimeout) - } - - func testIntervalWithCallNoLongerInFlightButPermitted() { - // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect) - self.setupPingHandler(interval: .seconds(1), timeout: .seconds(1), permitWithoutCalls: true) - - // New stream created - var response: PingHandler.Action = self.pingHandler.streamCreated() - XCTAssertEqual(response, .schedulePing(delay: .seconds(1), timeout: .seconds(1))) - - // Stream closed - response = self.pingHandler.streamClosed() - XCTAssertEqual(response, .none) - - // Move time to 1 second in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(1) - - // Send ping, which is valid - response = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Received valid pong, scheduled timeout should be cancelled - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: true) - XCTAssertEqual(response, .cancelScheduledTimeout) - } - - func testIntervalTooEarlyWithCallInFlight() { - // Do not allow pings without calls - self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1)) - - // New stream created - var response: PingHandler.Action = self.pingHandler.streamCreated() - XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1))) - - // Send first ping - response = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Move time to 1 second in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(1) - - // Send another ping, which is valid since client do not check ping strikes - response = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Stream closed - response = self.pingHandler.streamClosed() - XCTAssertEqual(response, .none) - } - - func testIntervalTooEarlyWithoutCallsInFlight() { - // Allow pings without calls with a maximum pings of 2 - self.setupPingHandler( - interval: .seconds(2), - timeout: .seconds(1), - permitWithoutCalls: true, - maximumPingsWithoutData: 2, - minimumSentPingIntervalWithoutData: .seconds(5) - ) - - // Send first ping - var response: PingHandler.Action = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Move time to 1 second in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(1) - - // Send another ping, but since `now` is less than the ping interval, response should be no action - response = self.pingHandler.pingFired() - XCTAssertEqual(response, .none) - - // Move time to 5 seconds in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(5) - - // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData` - response = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Move time to 10 seconds in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(10) - - // Send another ping, which is valid since we waited `minimumSentPingIntervalWithoutData` - response = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Send another ping, but we've exceeded `maximumPingsWithoutData` so response should be no action - response = self.pingHandler.pingFired() - XCTAssertEqual(response, .none) - - // New stream created - response = self.pingHandler.streamCreated() - XCTAssertEqual(response, .schedulePing(delay: .seconds(2), timeout: .seconds(1))) - - // Send another ping, now that there is call, ping is valid - response = self.pingHandler.pingFired() - XCTAssertEqual( - response, - .reply(HTTP2Frame.FramePayload.ping(HTTP2PingData(withInteger: 1), ack: false)) - ) - - // Stream closed - response = self.pingHandler.streamClosed() - XCTAssertEqual(response, .none) - } - - func testPingStrikesOnClientShouldHaveNoEffect() { - // Allow pings without calls (since `minimumReceivedPingIntervalWithoutData` and `maximumPingStrikes` are not set, ping strikes should not have any effect) - self.setupPingHandler(interval: .seconds(2), timeout: .seconds(1), permitWithoutCalls: true) - - // Received first ping, response should be a pong - var response: PingHandler.Action = self.pingHandler.read( - pingData: HTTP2PingData(withInteger: 1), - ack: false - ) - XCTAssertEqual(response, .ack) - - // Received another ping, response should be a pong (ping strikes not in effect) - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual(response, .ack) - - // Received another ping, response should be a pong (ping strikes not in effect) - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual(response, .ack) - } - - func testPingWithoutDataResultsInPongForClient() { - // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated). - self.setupPingHandler(permitWithoutCalls: false) - - let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual(action, .ack) - } - - func testPingWithoutDataResultsInPongForServer() { - // Don't allow _sending_ pings when no calls are active (receiving pings should be tolerated). - // Set 'minimumReceivedPingIntervalWithoutData' and 'maximumPingStrikes' so that we enable - // support for ping strikes. - self.setupPingHandler( - permitWithoutCalls: false, - minimumReceivedPingIntervalWithoutData: .seconds(5), - maximumPingStrikes: 1 - ) - - let action = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual(action, .ack) - } - - func testPingStrikesOnServer() { - // Set a maximum ping strikes of 1 without a minimum of 1 second between pings - self.setupPingHandler( - interval: .seconds(2), - timeout: .seconds(1), - permitWithoutCalls: true, - minimumReceivedPingIntervalWithoutData: .seconds(1), - maximumPingStrikes: 1 - ) - - // Received first ping, response should be a pong - var response: PingHandler.Action = self.pingHandler.read( - pingData: HTTP2PingData(withInteger: 1), - ack: false - ) - XCTAssertEqual(response, .ack) - - // Received another ping, which is invalid (ping strike), response should be no action - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual(response, .none) - - // Move time to 2 seconds in the future - self.pingHandler._testingOnlyNow = .now() + .seconds(2) - - // Received another ping, which is valid now, response should be a pong - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual(response, .ack) - - // Received another ping, which is invalid (ping strike), response should be no action - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual(response, .none) - - // Received another ping, which is invalid (ping strike), since number of ping strikes is over the limit, response should be go away - response = self.pingHandler.read(pingData: HTTP2PingData(withInteger: 1), ack: false) - XCTAssertEqual( - response, - .reply( - HTTP2Frame.FramePayload.goAway( - lastStreamID: .rootStream, - errorCode: .enhanceYourCalm, - opaqueData: nil - ) - ) - ) - } - - func testPongWithGoAwayPingData() { - self.setupPingHandler() - let response = self.pingHandler.read(pingData: self.pingHandler.pingDataGoAway, ack: true) - XCTAssertEqual(response, .ratchetDownLastSeenStreamID) - } - - private func setupPingHandler( - pingCode: UInt64 = 1, - interval: TimeAmount = .seconds(15), - timeout: TimeAmount = .seconds(5), - permitWithoutCalls: Bool = false, - maximumPingsWithoutData: UInt = 2, - minimumSentPingIntervalWithoutData: TimeAmount = .seconds(5), - minimumReceivedPingIntervalWithoutData: TimeAmount? = nil, - maximumPingStrikes: UInt? = nil - ) { - self.pingHandler = PingHandler( - pingCode: pingCode, - interval: interval, - timeout: timeout, - permitWithoutCalls: permitWithoutCalls, - maximumPingsWithoutData: maximumPingsWithoutData, - minimumSentPingIntervalWithoutData: minimumSentPingIntervalWithoutData, - minimumReceivedPingIntervalWithoutData: minimumReceivedPingIntervalWithoutData, - maximumPingStrikes: maximumPingStrikes - ) - } -} - -#if compiler(>=6.0) -extension PingHandler.Action: @retroactive Equatable {} -#else -extension PingHandler.Action: Equatable {} -#endif - -extension PingHandler.Action { - public static func == (lhs: PingHandler.Action, rhs: PingHandler.Action) -> Bool { - switch (lhs, rhs) { - case (.none, .none): - return true - case (.ack, .ack): - return true - case (let .schedulePing(lhsDelay, lhsTimeout), let .schedulePing(rhsDelay, rhsTimeout)): - return lhsDelay == rhsDelay && lhsTimeout == rhsTimeout - case (.cancelScheduledTimeout, .cancelScheduledTimeout): - return true - case (.ratchetDownLastSeenStreamID, .ratchetDownLastSeenStreamID): - return true - case let (.reply(lhsPayload), .reply(rhsPayload)): - switch (lhsPayload, rhsPayload) { - case (let .ping(lhsData, ack: lhsAck), let .ping(rhsData, ack: rhsAck)): - return lhsData == rhsData && lhsAck == rhsAck - case (let .goAway(_, lhsErrorCode, _), let .goAway(_, rhsErrorCode, _)): - return lhsErrorCode == rhsErrorCode - default: - return false - } - default: - return false - } - } -} - -extension GRPCPingHandlerTests { - func testSingleAckIsEmittedOnPing() throws { - let client = EmbeddedChannel() - _ = try client.configureHTTP2Pipeline(mode: .client) { _ in - fatalError("Unexpected inbound stream") - }.wait() - - let server = EmbeddedChannel() - let serverMux = try server.configureHTTP2Pipeline(mode: .server) { _ in - fatalError("Unexpected inbound stream") - }.wait() - - let idleHandler = GRPCIdleHandler( - idleTimeout: .minutes(5), - keepalive: .init(), - logger: self.serverLogger - ) - try server.pipeline.syncOperations.addHandler(idleHandler, position: .before(serverMux)) - try server.connect(to: .init(unixDomainSocketPath: "/ignored")).wait() - try client.connect(to: .init(unixDomainSocketPath: "/ignored")).wait() - - func interact(client: EmbeddedChannel, server: EmbeddedChannel) throws { - var didRead = true - while didRead { - didRead = false - - if let data = try client.readOutbound(as: ByteBuffer.self) { - didRead = true - try server.writeInbound(data) - } - - if let data = try server.readOutbound(as: ByteBuffer.self) { - didRead = true - try client.writeInbound(data) - } - } - } - - try interact(client: client, server: server) - - // Settings. - let f1 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self)) - f1.payload.assertSettings(ack: false) - - // Settings ack. - let f2 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self)) - f2.payload.assertSettings(ack: true) - - // Send a ping. - let ping = HTTP2Frame(streamID: .rootStream, payload: .ping(.init(withInteger: 42), ack: false)) - try client.writeOutbound(ping) - try interact(client: client, server: server) - - // Ping ack. - let f3 = try XCTUnwrap(client.readInbound(as: HTTP2Frame.self)) - f3.payload.assertPing(ack: true) - - XCTAssertNil(try client.readInbound(as: HTTP2Frame.self)) - } -} - -extension HTTP2Frame.FramePayload { - func assertSettings(ack: Bool, file: StaticString = #file, line: UInt = #line) { - switch self { - case let .settings(settings): - switch settings { - case .ack: - XCTAssertTrue(ack, file: file, line: line) - case .settings: - XCTAssertFalse(ack, file: file, line: line) - } - default: - XCTFail("Expected .settings got \(self)", file: file, line: line) - } - } - - func assertPing(ack: Bool, file: StaticString = #file, line: UInt = #line) { - switch self { - case let .ping(_, ack: pingAck): - XCTAssertEqual(pingAck, ack, file: file, line: line) - default: - XCTFail("Expected .ping got \(self)", file: file, line: line) - } - } -} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift deleted file mode 100644 index 57a605e77..000000000 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.grpc.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: reflection.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Usage: instantiate `Grpc_Reflection_V1_ServerReflectionClient`, then call methods of this protocol to make API calls. -internal protocol Grpc_Reflection_V1_ServerReflectionClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { get } - - func serverReflectionInfo( - callOptions: CallOptions?, - handler: @escaping (Grpc_Reflection_V1_ServerReflectionResponse) -> Void - ) -> BidirectionalStreamingCall -} - -extension Grpc_Reflection_V1_ServerReflectionClientProtocol { - internal var serviceName: String { - return "grpc.reflection.v1.ServerReflection" - } - - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - internal func serverReflectionInfo( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Reflection_V1_ServerReflectionResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], - handler: handler - ) - } -} - -@available(*, deprecated) -extension Grpc_Reflection_V1_ServerReflectionClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Grpc_Reflection_V1_ServerReflectionNIOClient") -internal final class Grpc_Reflection_V1_ServerReflectionClient: Grpc_Reflection_V1_ServerReflectionClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the grpc.reflection.v1.ServerReflection service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -internal struct Grpc_Reflection_V1_ServerReflectionNIOClient: Grpc_Reflection_V1_ServerReflectionClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? - - /// Creates a client for the grpc.reflection.v1.ServerReflection service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { get } - - func makeServerReflectionInfoCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Reflection_V1_ServerReflectionClientMetadata.serviceDescriptor - } - - internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? { - return nil - } - - internal func makeServerReflectionInfoCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol { - internal func serverReflectionInfo( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Reflection_V1_ServerReflectionRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] - ) - } - - internal func serverReflectionInfo( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Reflection_V1_ServerReflectionRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct Grpc_Reflection_V1_ServerReflectionAsyncClient: Grpc_Reflection_V1_ServerReflectionAsyncClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? - - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -internal protocol Grpc_Reflection_V1_ServerReflectionClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'serverReflectionInfo'. - func makeServerReflectionInfoInterceptors() -> [ClientInterceptor] -} - -internal enum Grpc_Reflection_V1_ServerReflectionClientMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "ServerReflection", - fullName: "grpc.reflection.v1.ServerReflection", - methods: [ - Grpc_Reflection_V1_ServerReflectionClientMetadata.Methods.serverReflectionInfo, - ] - ) - - internal enum Methods { - internal static let serverReflectionInfo = GRPCMethodDescriptor( - name: "ServerReflectionInfo", - path: "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", - type: GRPCCallType.bidirectionalStreaming - ) - } -} - diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift deleted file mode 100644 index 1c7a59122..000000000 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1/reflection-v1.pb.swift +++ /dev/null @@ -1,775 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2016 The gRPC Authors -// -// 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. - -// Service exported by server reflection. A more complete description of how -// server reflection works can be found at -// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md -// -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The message sent by the client when calling ServerReflectionInfo method. -struct Grpc_Reflection_V1_ServerReflectionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var host: String = String() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - var messageRequest: Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest? = nil - - /// Find a proto file by the file name. - var fileByFilename: String { - get { - if case .fileByFilename(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileByFilename(newValue)} - } - - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - var fileContainingSymbol: String { - get { - if case .fileContainingSymbol(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileContainingSymbol(newValue)} - } - - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - var fileContainingExtension: Grpc_Reflection_V1_ExtensionRequest { - get { - if case .fileContainingExtension(let v)? = messageRequest {return v} - return Grpc_Reflection_V1_ExtensionRequest() - } - set {messageRequest = .fileContainingExtension(newValue)} - } - - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - var allExtensionNumbersOfType: String { - get { - if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .allExtensionNumbersOfType(newValue)} - } - - /// List the full names of registered services. The content will not be - /// checked. - var listServices: String { - get { - if case .listServices(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .listServices(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - enum OneOf_MessageRequest: Equatable, Sendable { - /// Find a proto file by the file name. - case fileByFilename(String) - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - case fileContainingSymbol(String) - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - case fileContainingExtension(Grpc_Reflection_V1_ExtensionRequest) - /// Finds the tag numbers used by all known extensions of the given message - /// type, and appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - case allExtensionNumbersOfType(String) - /// List the full names of registered services. The content will not be - /// checked. - case listServices(String) - - } - - init() {} -} - -/// The type name and extension number sent by the client when requesting -/// file_containing_extension. -struct Grpc_Reflection_V1_ExtensionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Fully-qualified type name. The format should be . - var containingType: String = String() - - var extensionNumber: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The message sent by the server to answer ServerReflectionInfo method. -struct Grpc_Reflection_V1_ServerReflectionResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var validHost: String = String() - - var originalRequest: Grpc_Reflection_V1_ServerReflectionRequest { - get {return _originalRequest ?? Grpc_Reflection_V1_ServerReflectionRequest()} - set {_originalRequest = newValue} - } - /// Returns true if `originalRequest` has been explicitly set. - var hasOriginalRequest: Bool {return self._originalRequest != nil} - /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. - mutating func clearOriginalRequest() {self._originalRequest = nil} - - /// The server sets one of the following fields according to the message_request - /// in the request. - var messageResponse: Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse? = nil - - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. - /// As the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - var fileDescriptorResponse: Grpc_Reflection_V1_FileDescriptorResponse { - get { - if case .fileDescriptorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_FileDescriptorResponse() - } - set {messageResponse = .fileDescriptorResponse(newValue)} - } - - /// This message is used to answer all_extension_numbers_of_type requests. - var allExtensionNumbersResponse: Grpc_Reflection_V1_ExtensionNumberResponse { - get { - if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_ExtensionNumberResponse() - } - set {messageResponse = .allExtensionNumbersResponse(newValue)} - } - - /// This message is used to answer list_services requests. - var listServicesResponse: Grpc_Reflection_V1_ListServiceResponse { - get { - if case .listServicesResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_ListServiceResponse() - } - set {messageResponse = .listServicesResponse(newValue)} - } - - /// This message is used when an error occurs. - var errorResponse: Grpc_Reflection_V1_ErrorResponse { - get { - if case .errorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1_ErrorResponse() - } - set {messageResponse = .errorResponse(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The server sets one of the following fields according to the message_request - /// in the request. - enum OneOf_MessageResponse: Equatable, Sendable { - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. - /// As the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - case fileDescriptorResponse(Grpc_Reflection_V1_FileDescriptorResponse) - /// This message is used to answer all_extension_numbers_of_type requests. - case allExtensionNumbersResponse(Grpc_Reflection_V1_ExtensionNumberResponse) - /// This message is used to answer list_services requests. - case listServicesResponse(Grpc_Reflection_V1_ListServiceResponse) - /// This message is used when an error occurs. - case errorResponse(Grpc_Reflection_V1_ErrorResponse) - - } - - init() {} - - fileprivate var _originalRequest: Grpc_Reflection_V1_ServerReflectionRequest? = nil -} - -/// Serialized FileDescriptorProto messages sent by the server answering -/// a file_by_filename, file_containing_symbol, or file_containing_extension -/// request. -struct Grpc_Reflection_V1_FileDescriptorResponse: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Serialized FileDescriptorProto messages. We avoid taking a dependency on - /// descriptor.proto, which uses proto2 only features, by making them opaque - /// bytes instead. - var fileDescriptorProto: [Data] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A list of extension numbers sent by the server answering -/// all_extension_numbers_of_type request. -struct Grpc_Reflection_V1_ExtensionNumberResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of the base type, including the package name. The format - /// is . - var baseTypeName: String = String() - - var extensionNumber: [Int32] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A list of ServiceResponse sent by the server answering list_services request. -struct Grpc_Reflection_V1_ListServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The information of each service may be expanded in the future, so we use - /// ServiceResponse message to encapsulate it. - var service: [Grpc_Reflection_V1_ServiceResponse] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The information of a single service used by ListServiceResponse to answer -/// list_services request. -struct Grpc_Reflection_V1_ServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of a registered service, including its package name. The format - /// is . - var name: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The error code and error message sent by the server when an error occurs. -struct Grpc_Reflection_V1_ErrorResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// This field uses the error codes defined in grpc::StatusCode. - var errorCode: Int32 = 0 - - var errorMessage: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.reflection.v1" - -extension Grpc_Reflection_V1_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "host"), - 3: .standard(proto: "file_by_filename"), - 4: .standard(proto: "file_containing_symbol"), - 5: .standard(proto: "file_containing_extension"), - 6: .standard(proto: "all_extension_numbers_of_type"), - 7: .standard(proto: "list_services"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() - case 3: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileByFilename(v) - } - }() - case 4: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingSymbol(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1_ExtensionRequest? - var hadOneofValue = false - if let current = self.messageRequest { - hadOneofValue = true - if case .fileContainingExtension(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingExtension(v) - } - }() - case 6: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .allExtensionNumbersOfType(v) - } - }() - case 7: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .listServices(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.host.isEmpty { - try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) - } - switch self.messageRequest { - case .fileByFilename?: try { - guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - }() - case .fileContainingSymbol?: try { - guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - }() - case .fileContainingExtension?: try { - guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .allExtensionNumbersOfType?: try { - guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 6) - }() - case .listServices?: try { - guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_ServerReflectionRequest, rhs: Grpc_Reflection_V1_ServerReflectionRequest) -> Bool { - if lhs.host != rhs.host {return false} - if lhs.messageRequest != rhs.messageRequest {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "containing_type"), - 2: .standard(proto: "extension_number"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.containingType.isEmpty { - try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) - } - if self.extensionNumber != 0 { - try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_ExtensionRequest, rhs: Grpc_Reflection_V1_ExtensionRequest) -> Bool { - if lhs.containingType != rhs.containingType {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "valid_host"), - 2: .standard(proto: "original_request"), - 4: .standard(proto: "file_descriptor_response"), - 5: .standard(proto: "all_extension_numbers_response"), - 6: .standard(proto: "list_services_response"), - 7: .standard(proto: "error_response"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() - case 4: try { - var v: Grpc_Reflection_V1_FileDescriptorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .fileDescriptorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .fileDescriptorResponse(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1_ExtensionNumberResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .allExtensionNumbersResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .allExtensionNumbersResponse(v) - } - }() - case 6: try { - var v: Grpc_Reflection_V1_ListServiceResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .listServicesResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .listServicesResponse(v) - } - }() - case 7: try { - var v: Grpc_Reflection_V1_ErrorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .errorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .errorResponse(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.validHost.isEmpty { - try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) - } - try { if let v = self._originalRequest { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - switch self.messageResponse { - case .fileDescriptorResponse?: try { - guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .allExtensionNumbersResponse?: try { - guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .listServicesResponse?: try { - guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .errorResponse?: try { - guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_ServerReflectionResponse, rhs: Grpc_Reflection_V1_ServerReflectionResponse) -> Bool { - if lhs.validHost != rhs.validHost {return false} - if lhs._originalRequest != rhs._originalRequest {return false} - if lhs.messageResponse != rhs.messageResponse {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "file_descriptor_proto"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.fileDescriptorProto.isEmpty { - try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_FileDescriptorResponse, rhs: Grpc_Reflection_V1_FileDescriptorResponse) -> Bool { - if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "base_type_name"), - 2: .standard(proto: "extension_number"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.baseTypeName.isEmpty { - try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) - } - if !self.extensionNumber.isEmpty { - try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_ExtensionNumberResponse, rhs: Grpc_Reflection_V1_ExtensionNumberResponse) -> Bool { - if lhs.baseTypeName != rhs.baseTypeName {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "service"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.service.isEmpty { - try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_ListServiceResponse, rhs: Grpc_Reflection_V1_ListServiceResponse) -> Bool { - if lhs.service != rhs.service {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServiceResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_ServiceResponse, rhs: Grpc_Reflection_V1_ServiceResponse) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ErrorResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "error_code"), - 2: .standard(proto: "error_message"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.errorCode != 0 { - try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) - } - if !self.errorMessage.isEmpty { - try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1_ErrorResponse, rhs: Grpc_Reflection_V1_ErrorResponse) -> Bool { - if lhs.errorCode != rhs.errorCode {return false} - if lhs.errorMessage != rhs.errorMessage {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift deleted file mode 100644 index b68c8530f..000000000 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.grpc.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: reflection.proto -// -import GRPC -import NIO -import NIOConcurrencyHelpers -import SwiftProtobuf - - -/// Usage: instantiate `Grpc_Reflection_V1alpha_ServerReflectionClient`, then call methods of this protocol to make API calls. -internal protocol Grpc_Reflection_V1alpha_ServerReflectionClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { get } - - func serverReflectionInfo( - callOptions: CallOptions?, - handler: @escaping (Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Void - ) -> BidirectionalStreamingCall -} - -extension Grpc_Reflection_V1alpha_ServerReflectionClientProtocol { - internal var serviceName: String { - return "grpc.reflection.v1alpha.ServerReflection" - } - - /// The reflection service is structured as a bidirectional stream, ensuring - /// all related requests go to a single server. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - internal func serverReflectionInfo( - callOptions: CallOptions? = nil, - handler: @escaping (Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [], - handler: handler - ) - } -} - -@available(*, deprecated) -extension Grpc_Reflection_V1alpha_ServerReflectionClient: @unchecked Sendable {} - -@available(*, deprecated, renamed: "Grpc_Reflection_V1alpha_ServerReflectionNIOClient") -internal final class Grpc_Reflection_V1alpha_ServerReflectionClient: Grpc_Reflection_V1alpha_ServerReflectionClientProtocol { - private let lock = Lock() - private var _defaultCallOptions: CallOptions - private var _interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions { - get { self.lock.withLock { return self._defaultCallOptions } } - set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } - } - internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { - get { self.lock.withLock { return self._interceptors } } - set { self.lock.withLockVoid { self._interceptors = newValue } } - } - - /// Creates a client for the grpc.reflection.v1alpha.ServerReflection service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self._defaultCallOptions = defaultCallOptions - self._interceptors = interceptors - } -} - -internal struct Grpc_Reflection_V1alpha_ServerReflectionNIOClient: Grpc_Reflection_V1alpha_ServerReflectionClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? - - /// Creates a client for the grpc.reflection.v1alpha.ServerReflection service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal protocol Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol: GRPCClient { - static var serviceDescriptor: GRPCServiceDescriptor { get } - var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { get } - - func makeServerReflectionInfoCall( - callOptions: CallOptions? - ) -> GRPCAsyncBidirectionalStreamingCall -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol { - internal static var serviceDescriptor: GRPCServiceDescriptor { - return Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.serviceDescriptor - } - - internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? { - return nil - } - - internal func makeServerReflectionInfoCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - return self.makeAsyncBidirectionalStreamingCall( - path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol { - internal func serverReflectionInfo( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: Sequence, RequestStream.Element == Grpc_Reflection_V1alpha_ServerReflectionRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] - ) - } - - internal func serverReflectionInfo( - _ requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncResponseStream where RequestStream: AsyncSequence & Sendable, RequestStream.Element == Grpc_Reflection_V1alpha_ServerReflectionRequest { - return self.performAsyncBidirectionalStreamingCall( - path: Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo.path, - requests: requests, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeServerReflectionInfoInterceptors() ?? [] - ) - } -} - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -internal struct Grpc_Reflection_V1alpha_ServerReflectionAsyncClient: Grpc_Reflection_V1alpha_ServerReflectionAsyncClientProtocol { - internal var channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? - - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -internal protocol Grpc_Reflection_V1alpha_ServerReflectionClientInterceptorFactoryProtocol: Sendable { - - /// - Returns: Interceptors to use when invoking 'serverReflectionInfo'. - func makeServerReflectionInfoInterceptors() -> [ClientInterceptor] -} - -internal enum Grpc_Reflection_V1alpha_ServerReflectionClientMetadata { - internal static let serviceDescriptor = GRPCServiceDescriptor( - name: "ServerReflection", - fullName: "grpc.reflection.v1alpha.ServerReflection", - methods: [ - Grpc_Reflection_V1alpha_ServerReflectionClientMetadata.Methods.serverReflectionInfo, - ] - ) - - internal enum Methods { - internal static let serverReflectionInfo = GRPCMethodDescriptor( - name: "ServerReflectionInfo", - path: "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", - type: GRPCCallType.bidirectionalStreaming - ) - } -} - diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift deleted file mode 100644 index bcb7d31a2..000000000 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Generated/v1Alpha/reflection-v1alpha.pb.swift +++ /dev/null @@ -1,788 +0,0 @@ -// DO NOT EDIT. -// swift-format-ignore-file -// swiftlint:disable all -// -// Generated by the Swift generator plugin for the protocol buffer compiler. -// Source: reflection.proto -// -// For information on using the generated types, please see the documentation: -// https://github.com/apple/swift-protobuf/ - -// Copyright 2016 The gRPC Authors -// -// 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. -// Service exported by server reflection - -// Warning: this entire file is deprecated. Use this instead: -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto - -import Foundation -import SwiftProtobuf - -// If the compiler emits an error on this type, it is because this file -// was generated by a version of the `protoc` Swift plug-in that is -// incompatible with the version of SwiftProtobuf to which you are linking. -// Please ensure that you are building against the same version of the API -// that was used to generate this file. -fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { - struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} - typealias Version = _2 -} - -/// The message sent by the client when calling ServerReflectionInfo method. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_ServerReflectionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var host: String = String() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - var messageRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest? = nil - - /// Find a proto file by the file name. - var fileByFilename: String { - get { - if case .fileByFilename(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileByFilename(newValue)} - } - - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - var fileContainingSymbol: String { - get { - if case .fileContainingSymbol(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .fileContainingSymbol(newValue)} - } - - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - var fileContainingExtension: Grpc_Reflection_V1alpha_ExtensionRequest { - get { - if case .fileContainingExtension(let v)? = messageRequest {return v} - return Grpc_Reflection_V1alpha_ExtensionRequest() - } - set {messageRequest = .fileContainingExtension(newValue)} - } - - /// Finds the tag numbers used by all known extensions of extendee_type, and - /// appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - var allExtensionNumbersOfType: String { - get { - if case .allExtensionNumbersOfType(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .allExtensionNumbersOfType(newValue)} - } - - /// List the full names of registered services. The content will not be - /// checked. - var listServices: String { - get { - if case .listServices(let v)? = messageRequest {return v} - return String() - } - set {messageRequest = .listServices(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// To use reflection service, the client should set one of the following - /// fields in message_request. The server distinguishes requests by their - /// defined field and then handles them using corresponding methods. - enum OneOf_MessageRequest: Equatable, Sendable { - /// Find a proto file by the file name. - case fileByFilename(String) - /// Find the proto file that declares the given fully-qualified symbol name. - /// This field should be a fully-qualified symbol name - /// (e.g. .[.] or .). - case fileContainingSymbol(String) - /// Find the proto file which defines an extension extending the given - /// message type with the given field number. - case fileContainingExtension(Grpc_Reflection_V1alpha_ExtensionRequest) - /// Finds the tag numbers used by all known extensions of extendee_type, and - /// appends them to ExtensionNumberResponse in an undefined order. - /// Its corresponding method is best-effort: it's not guaranteed that the - /// reflection service will implement this method, and it's not guaranteed - /// that this method will provide all extensions. Returns - /// StatusCode::UNIMPLEMENTED if it's not implemented. - /// This field should be a fully-qualified type name. The format is - /// . - case allExtensionNumbersOfType(String) - /// List the full names of registered services. The content will not be - /// checked. - case listServices(String) - - } - - init() {} -} - -/// The type name and extension number sent by the client when requesting -/// file_containing_extension. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_ExtensionRequest: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Fully-qualified type name. The format should be . - var containingType: String = String() - - var extensionNumber: Int32 = 0 - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The message sent by the server to answer ServerReflectionInfo method. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_ServerReflectionResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - var validHost: String = String() - - var originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest { - get {return _originalRequest ?? Grpc_Reflection_V1alpha_ServerReflectionRequest()} - set {_originalRequest = newValue} - } - /// Returns true if `originalRequest` has been explicitly set. - var hasOriginalRequest: Bool {return self._originalRequest != nil} - /// Clears the value of `originalRequest`. Subsequent reads from it will return its default value. - mutating func clearOriginalRequest() {self._originalRequest = nil} - - /// The server set one of the following fields according to the message_request - /// in the request. - var messageResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse.OneOf_MessageResponse? = nil - - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. As - /// the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - var fileDescriptorResponse: Grpc_Reflection_V1alpha_FileDescriptorResponse { - get { - if case .fileDescriptorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_FileDescriptorResponse() - } - set {messageResponse = .fileDescriptorResponse(newValue)} - } - - /// This message is used to answer all_extension_numbers_of_type requst. - var allExtensionNumbersResponse: Grpc_Reflection_V1alpha_ExtensionNumberResponse { - get { - if case .allExtensionNumbersResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_ExtensionNumberResponse() - } - set {messageResponse = .allExtensionNumbersResponse(newValue)} - } - - /// This message is used to answer list_services request. - var listServicesResponse: Grpc_Reflection_V1alpha_ListServiceResponse { - get { - if case .listServicesResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_ListServiceResponse() - } - set {messageResponse = .listServicesResponse(newValue)} - } - - /// This message is used when an error occurs. - var errorResponse: Grpc_Reflection_V1alpha_ErrorResponse { - get { - if case .errorResponse(let v)? = messageResponse {return v} - return Grpc_Reflection_V1alpha_ErrorResponse() - } - set {messageResponse = .errorResponse(newValue)} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - /// The server set one of the following fields according to the message_request - /// in the request. - enum OneOf_MessageResponse: Equatable, Sendable { - /// This message is used to answer file_by_filename, file_containing_symbol, - /// file_containing_extension requests with transitive dependencies. As - /// the repeated label is not allowed in oneof fields, we use a - /// FileDescriptorResponse message to encapsulate the repeated fields. - /// The reflection service is allowed to avoid sending FileDescriptorProtos - /// that were previously sent in response to earlier requests in the stream. - case fileDescriptorResponse(Grpc_Reflection_V1alpha_FileDescriptorResponse) - /// This message is used to answer all_extension_numbers_of_type requst. - case allExtensionNumbersResponse(Grpc_Reflection_V1alpha_ExtensionNumberResponse) - /// This message is used to answer list_services request. - case listServicesResponse(Grpc_Reflection_V1alpha_ListServiceResponse) - /// This message is used when an error occurs. - case errorResponse(Grpc_Reflection_V1alpha_ErrorResponse) - - } - - init() {} - - fileprivate var _originalRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest? = nil -} - -/// Serialized FileDescriptorProto messages sent by the server answering -/// a file_by_filename, file_containing_symbol, or file_containing_extension -/// request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_FileDescriptorResponse: @unchecked Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Serialized FileDescriptorProto messages. We avoid taking a dependency on - /// descriptor.proto, which uses proto2 only features, by making them opaque - /// bytes instead. - var fileDescriptorProto: [Data] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A list of extension numbers sent by the server answering -/// all_extension_numbers_of_type request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_ExtensionNumberResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of the base type, including the package name. The format - /// is . - var baseTypeName: String = String() - - var extensionNumber: [Int32] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// A list of ServiceResponse sent by the server answering list_services request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_ListServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// The information of each service may be expanded in the future, so we use - /// ServiceResponse message to encapsulate it. - var service: [Grpc_Reflection_V1alpha_ServiceResponse] = [] - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The information of a single service used by ListServiceResponse to answer -/// list_services request. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_ServiceResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// Full name of a registered service, including its package name. The format - /// is . - var name: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -/// The error code and error message sent by the server when an error occurs. -/// -/// NOTE: The whole .proto file that defined this message was marked as deprecated. -struct Grpc_Reflection_V1alpha_ErrorResponse: Sendable { - // SwiftProtobuf.Message conformance is added in an extension below. See the - // `Message` and `Message+*Additions` files in the SwiftProtobuf library for - // methods supported on all messages. - - /// This field uses the error codes defined in grpc::StatusCode. - var errorCode: Int32 = 0 - - var errorMessage: String = String() - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - -// MARK: - Code below here is support for the SwiftProtobuf runtime. - -fileprivate let _protobuf_package = "grpc.reflection.v1alpha" - -extension Grpc_Reflection_V1alpha_ServerReflectionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerReflectionRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "host"), - 3: .standard(proto: "file_by_filename"), - 4: .standard(proto: "file_containing_symbol"), - 5: .standard(proto: "file_containing_extension"), - 6: .standard(proto: "all_extension_numbers_of_type"), - 7: .standard(proto: "list_services"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.host) }() - case 3: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileByFilename(v) - } - }() - case 4: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingSymbol(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1alpha_ExtensionRequest? - var hadOneofValue = false - if let current = self.messageRequest { - hadOneofValue = true - if case .fileContainingExtension(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageRequest = .fileContainingExtension(v) - } - }() - case 6: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .allExtensionNumbersOfType(v) - } - }() - case 7: try { - var v: String? - try decoder.decodeSingularStringField(value: &v) - if let v = v { - if self.messageRequest != nil {try decoder.handleConflictingOneOf()} - self.messageRequest = .listServices(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.host.isEmpty { - try visitor.visitSingularStringField(value: self.host, fieldNumber: 1) - } - switch self.messageRequest { - case .fileByFilename?: try { - guard case .fileByFilename(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 3) - }() - case .fileContainingSymbol?: try { - guard case .fileContainingSymbol(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 4) - }() - case .fileContainingExtension?: try { - guard case .fileContainingExtension(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .allExtensionNumbersOfType?: try { - guard case .allExtensionNumbersOfType(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 6) - }() - case .listServices?: try { - guard case .listServices(let v)? = self.messageRequest else { preconditionFailure() } - try visitor.visitSingularStringField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionRequest, rhs: Grpc_Reflection_V1alpha_ServerReflectionRequest) -> Bool { - if lhs.host != rhs.host {return false} - if lhs.messageRequest != rhs.messageRequest {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ExtensionRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ExtensionRequest" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "containing_type"), - 2: .standard(proto: "extension_number"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.containingType) }() - case 2: try { try decoder.decodeSingularInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.containingType.isEmpty { - try visitor.visitSingularStringField(value: self.containingType, fieldNumber: 1) - } - if self.extensionNumber != 0 { - try visitor.visitSingularInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionRequest, rhs: Grpc_Reflection_V1alpha_ExtensionRequest) -> Bool { - if lhs.containingType != rhs.containingType {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ServerReflectionResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServerReflectionResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "valid_host"), - 2: .standard(proto: "original_request"), - 4: .standard(proto: "file_descriptor_response"), - 5: .standard(proto: "all_extension_numbers_response"), - 6: .standard(proto: "list_services_response"), - 7: .standard(proto: "error_response"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.validHost) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._originalRequest) }() - case 4: try { - var v: Grpc_Reflection_V1alpha_FileDescriptorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .fileDescriptorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .fileDescriptorResponse(v) - } - }() - case 5: try { - var v: Grpc_Reflection_V1alpha_ExtensionNumberResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .allExtensionNumbersResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .allExtensionNumbersResponse(v) - } - }() - case 6: try { - var v: Grpc_Reflection_V1alpha_ListServiceResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .listServicesResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .listServicesResponse(v) - } - }() - case 7: try { - var v: Grpc_Reflection_V1alpha_ErrorResponse? - var hadOneofValue = false - if let current = self.messageResponse { - hadOneofValue = true - if case .errorResponse(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.messageResponse = .errorResponse(v) - } - }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.validHost.isEmpty { - try visitor.visitSingularStringField(value: self.validHost, fieldNumber: 1) - } - try { if let v = self._originalRequest { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - switch self.messageResponse { - case .fileDescriptorResponse?: try { - guard case .fileDescriptorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .allExtensionNumbersResponse?: try { - guard case .allExtensionNumbersResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .listServicesResponse?: try { - guard case .listServicesResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .errorResponse?: try { - guard case .errorResponse(let v)? = self.messageResponse else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - }() - case nil: break - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_ServerReflectionResponse, rhs: Grpc_Reflection_V1alpha_ServerReflectionResponse) -> Bool { - if lhs.validHost != rhs.validHost {return false} - if lhs._originalRequest != rhs._originalRequest {return false} - if lhs.messageResponse != rhs.messageResponse {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_FileDescriptorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".FileDescriptorResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "file_descriptor_proto"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedBytesField(value: &self.fileDescriptorProto) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.fileDescriptorProto.isEmpty { - try visitor.visitRepeatedBytesField(value: self.fileDescriptorProto, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_FileDescriptorResponse, rhs: Grpc_Reflection_V1alpha_FileDescriptorResponse) -> Bool { - if lhs.fileDescriptorProto != rhs.fileDescriptorProto {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ExtensionNumberResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ExtensionNumberResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "base_type_name"), - 2: .standard(proto: "extension_number"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.baseTypeName) }() - case 2: try { try decoder.decodeRepeatedInt32Field(value: &self.extensionNumber) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.baseTypeName.isEmpty { - try visitor.visitSingularStringField(value: self.baseTypeName, fieldNumber: 1) - } - if !self.extensionNumber.isEmpty { - try visitor.visitPackedInt32Field(value: self.extensionNumber, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse, rhs: Grpc_Reflection_V1alpha_ExtensionNumberResponse) -> Bool { - if lhs.baseTypeName != rhs.baseTypeName {return false} - if lhs.extensionNumber != rhs.extensionNumber {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ListServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ListServiceResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "service"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.service) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.service.isEmpty { - try visitor.visitRepeatedMessageField(value: self.service, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_ListServiceResponse, rhs: Grpc_Reflection_V1alpha_ListServiceResponse) -> Bool { - if lhs.service != rhs.service {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ServiceResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ServiceResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "name"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularStringField(value: &self.name) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if !self.name.isEmpty { - try visitor.visitSingularStringField(value: self.name, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_ServiceResponse, rhs: Grpc_Reflection_V1alpha_ServiceResponse) -> Bool { - if lhs.name != rhs.name {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} - -extension Grpc_Reflection_V1alpha_ErrorResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ErrorResponse" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "error_code"), - 2: .standard(proto: "error_message"), - ] - - mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularInt32Field(value: &self.errorCode) }() - case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() - default: break - } - } - } - - func traverse(visitor: inout V) throws { - if self.errorCode != 0 { - try visitor.visitSingularInt32Field(value: self.errorCode, fieldNumber: 1) - } - if !self.errorMessage.isEmpty { - try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) - } - try unknownFields.traverse(visitor: &visitor) - } - - static func ==(lhs: Grpc_Reflection_V1alpha_ErrorResponse, rhs: Grpc_Reflection_V1alpha_ErrorResponse) -> Bool { - if lhs.errorCode != rhs.errorCode {return false} - if lhs.errorMessage != rhs.errorMessage {return false} - if lhs.unknownFields != rhs.unknownFields {return false} - return true - } -} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift deleted file mode 100644 index 1836a1f68..000000000 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright 2023, 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 Foundation -import GRPC -import GRPCReflectionService -import NIOPosix -import SwiftProtobuf -import XCTest - -@testable import GRPCReflectionService - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -final class ReflectionServiceIntegrationTests: GRPCTestCase { - private var server: Server? - private var channel: GRPCChannel? - private let protos: [Google_Protobuf_FileDescriptorProto] = makeProtosWithDependencies() - private let independentProto: Google_Protobuf_FileDescriptorProto = generateFileDescriptorProto( - fileName: "independentBar", - suffix: "5" - ) - private let versions: [ReflectionService.Version] = [.v1, .v1Alpha] - - private func setUpServerAndChannel(version: ReflectionService.Version) throws { - let reflectionServiceProvider = try ReflectionService( - fileDescriptorProtos: self.protos + [self.independentProto], - version: version - ) - - let server = try Server.insecure(group: MultiThreadedEventLoopGroup.singleton) - .withServiceProviders([reflectionServiceProvider]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - self.server = server - - let channel = try GRPCChannelPool.with( - target: .hostAndPort("127.0.0.1", server.channel.localAddress!.port!), - transportSecurity: .plaintext, - eventLoopGroup: MultiThreadedEventLoopGroup.singleton - ) { - $0.backgroundActivityLogger = self.clientLogger - } - - self.channel = channel - } - - override func tearDown() { - if let channel = self.channel { - XCTAssertNoThrow(try channel.close().wait()) - } - if let server = self.server { - XCTAssertNoThrow(try server.close().wait()) - } - - super.tearDown() - } - - private func getServerReflectionResponse( - for request: Grpc_Reflection_V1_ServerReflectionRequest, - version: ReflectionService.Version - ) async throws -> Grpc_Reflection_V1_ServerReflectionResponse? { - let response: Grpc_Reflection_V1_ServerReflectionResponse? - switch version { - case .v1: - let client = Grpc_Reflection_V1_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send(request) - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - response = try await iterator.next() - case .v1Alpha: - let client = Grpc_Reflection_V1alpha_ServerReflectionAsyncClient(channel: self.channel!) - let serviceReflectionInfo = client.makeServerReflectionInfoCall() - try await serviceReflectionInfo.requestStream.send( - Grpc_Reflection_V1alpha_ServerReflectionRequest(request) - ) - serviceReflectionInfo.requestStream.finish() - var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() - response = try await iterator.next().map { - Grpc_Reflection_V1_ServerReflectionResponse($0) - } - default: - return nil - } - return response - } - - private func forEachVersion( - _ body: (GRPCChannel?, ReflectionService.Version) async throws -> Void - ) async throws { - for version in self.versions { - try setUpServerAndChannel(version: version) - let result: Result - do { - try await body(self.channel, version) - result = .success(()) - } catch { - result = .failure(error) - } - try result.get() - try await self.tearDown() - } - } - - func testFileByFileName() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.fileByFilename = "bar1.proto" - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - - // response can't be nil as we just checked it. - let receivedFileDescriptorProto = - try Google_Protobuf_FileDescriptorProto( - serializedBytes: message.fileDescriptorResponse.fileDescriptorProto[0] - ) - - XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto") - XCTAssertEqual(receivedFileDescriptorProto.service.count, 1) - - let service = try XCTUnwrap( - receivedFileDescriptorProto.service.first, - "The received file descriptor proto doesn't have any services." - ) - let method = try XCTUnwrap( - service.method.first, - "The service of the received file descriptor proto doesn't have any methods." - ) - XCTAssertEqual(method.name, "testMethod1") - XCTAssertEqual(message.fileDescriptorResponse.fileDescriptorProto.count, 4) - } - } - - func testListServices() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.listServices = "services" - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted() - let servicesNames = (self.protos + [self.independentProto]).flatMap { - $0.qualifiedServiceNames - } - .sorted() - - XCTAssertEqual(receivedServices, servicesNames) - } - } - - func testFileBySymbol() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.fileContainingSymbol = "packagebar1.enumType1" - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - let receivedData: [Google_Protobuf_FileDescriptorProto] - do { - receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { - try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) - } - } catch { - return XCTFail("Could not serialize data received as a message.") - } - - let fileToFind = self.protos[0] - let dependentProtos = self.protos[1...] - for fileDescriptorProto in receivedData { - if fileDescriptorProto == fileToFind { - XCTAssert( - fileDescriptorProto.enumType.names.contains("enumType1"), - """ - The response doesn't contain the serialized file descriptor proto \ - containing the \"packagebar1.enumType1\" symbol. - """ - ) - } else { - XCTAssert( - dependentProtos.contains(fileDescriptorProto), - """ - The \(fileDescriptorProto.name) is not a dependency of the \ - proto file containing the \"packagebar1.enumType1\" symbol. - """ - ) - } - } - } - } - - func testFileByExtension() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.fileContainingExtension = .with { - $0.containingType = "packagebar1.inputMessage1" - $0.extensionNumber = 2 - } - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - let receivedData: [Google_Protobuf_FileDescriptorProto] - do { - receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { - try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) - } - } catch { - return XCTFail("Could not serialize data received as a message.") - } - - let fileToFind = self.protos[0] - let dependentProtos = self.protos[1...] - var receivedProtoContainingExtension = 0 - var dependenciesCount = 0 - for fileDescriptorProto in receivedData { - if fileDescriptorProto == fileToFind { - receivedProtoContainingExtension += 1 - XCTAssert( - fileDescriptorProto.extension.map { $0.name }.contains( - "extension.packagebar1.inputMessage1-2" - ), - """ - The response doesn't contain the serialized file descriptor proto \ - containing the \"extensioninputMessage1-2\" extension. - """ - ) - } else { - dependenciesCount += 1 - XCTAssert( - dependentProtos.contains(fileDescriptorProto), - """ - The \(fileDescriptorProto.name) is not a dependency of the \ - proto file containing the \"extensioninputMessage1-2\" extension. - """ - ) - } - } - XCTAssertEqual( - receivedProtoContainingExtension, - 1, - "The file descriptor proto of the proto containing the extension was not received." - ) - XCTAssertEqual(dependenciesCount, 3) - } - } - - func testAllExtensionNumbersOfType() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.allExtensionNumbersOfType = "packagebar2.inputMessage2" - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - XCTAssertEqual(message.allExtensionNumbersResponse.baseTypeName, "packagebar2.inputMessage2") - XCTAssertEqual(message.allExtensionNumbersResponse.extensionNumber, [1, 2, 3, 4, 5]) - } - } - - func testErrorResponseFileByFileNameRequest() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.fileByFilename = "invalidFileName.proto" - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) - XCTAssertEqual( - message.errorResponse.errorMessage, - "No reflection data for 'invalidFileName.proto'." - ) - } - } - - func testErrorResponseFileBySymbolRequest() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.fileContainingSymbol = "packagebar1.invalidEnumType1" - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) - XCTAssertEqual(message.errorResponse.errorMessage, "The provided symbol could not be found.") - } - } - - func testErrorResponseFileByExtensionRequest() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.fileContainingExtension = .with { - $0.containingType = "packagebar1.invalidInputMessage1" - $0.extensionNumber = 2 - } - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - XCTAssertEqual(message.errorResponse.errorCode, Int32(GRPCStatus.Code.notFound.rawValue)) - XCTAssertEqual( - message.errorResponse.errorMessage, - "The provided extension could not be found." - ) - } - } - - func testErrorResponseAllExtensionNumbersOfTypeRequest() async throws { - try await self.forEachVersion { channel, version in - let request = Grpc_Reflection_V1_ServerReflectionRequest.with { - $0.host = "127.0.0.1" - $0.allExtensionNumbersOfType = "packagebar2.invalidInputMessage2" - } - let response = try await self.getServerReflectionResponse(for: request, version: version) - let message = try XCTUnwrap(response, "Could not get a response message.") - XCTAssertEqual( - message.errorResponse.errorCode, - Int32(GRPCStatus.Code.invalidArgument.rawValue) - ) - XCTAssertEqual(message.errorResponse.errorMessage, "The provided type is invalid.") - } - } -} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift deleted file mode 100644 index 69f680311..000000000 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ /dev/null @@ -1,615 +0,0 @@ -/* - * Copyright 2023, 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 Foundation -import GRPC -import GRPCReflectionService -import SwiftProtobuf -import XCTest - -@testable import GRPCReflectionService - -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -final class ReflectionServiceUnitTests: GRPCTestCase { - /// Testing the fileDescriptorDataByFilename dictionary of the ReflectionServiceData object. - func testFileDescriptorDataByFilename() throws { - var protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - - let registryFileDescriptorData = registry.fileDescriptorDataByFilename - - for (fileName, protoData) in registryFileDescriptorData { - let serializedFiledescriptorData = protoData.serializedFileDescriptorProto - let dependencyFileNames = protoData.dependencyFileNames - - guard let index = protos.firstIndex(where: { $0.name == fileName }) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileName) \ - in the original file descriptor protos list. - """ - ) - } - - let originalProto = protos[index] - XCTAssertEqual(originalProto.name, fileName) - XCTAssertEqual(try originalProto.serializedData(), serializedFiledescriptorData) - XCTAssertEqual(originalProto.dependency, dependencyFileNames) - - protos.remove(at: index) - } - XCTAssert(protos.isEmpty) - } - - /// Testing the serviceNames array of the ReflectionServiceData object. - func testServiceNames() throws { - let protos = makeProtosWithDependencies() - let servicesNames = protos.flatMap { $0.qualifiedServiceNames }.sorted() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let registryServices = registry.serviceNames.sorted() - XCTAssertEqual(registryServices, servicesNames) - } - - /// Testing the fileNameBySymbol dictionary of the ReflectionServiceData object. - func testFileNameBySymbol() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let registryFileNameBySymbol = registry.fileNameBySymbol - - var symbolsCount = 0 - - for proto in protos { - let qualifiedSymbolNames = proto.qualifiedSymbolNames - symbolsCount += qualifiedSymbolNames.count - for qualifiedSymbolName in qualifiedSymbolNames { - XCTAssertEqual(registryFileNameBySymbol[qualifiedSymbolName], proto.name) - } - } - - XCTAssertEqual(symbolsCount, registryFileNameBySymbol.count) - } - - func testFileNameBySymbolDuplicatedSymbol() throws { - var protos = makeProtosWithDependencies() - protos[1].messageType.append( - Google_Protobuf_DescriptorProto.with { - $0.name = "inputMessage2" - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "inputField" - $0.type = .bool - } - ] - } - ) - - XCTAssertThrowsError( - try ReflectionServiceData(fileDescriptors: protos) - ) { error in - XCTAssertEqual( - error as? GRPCStatus, - GRPCStatus( - code: .alreadyExists, - message: - """ - The packagebar2.inputMessage2 symbol from bar2.proto \ - already exists in bar2.proto. - """ - ) - ) - } - } - - // Testing the nameOfFileContainingSymbol method for different types of symbols. - - func testNameOfFileContainingSymbolEnum() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( - named: "packagebar2.enumType2" - ) - XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar2.proto") - } - - func testNameOfFileContainingSymbolMessage() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( - named: "packagebar1.inputMessage1" - ) - XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar1.proto") - } - - func testNameOfFileContainingSymbolService() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( - named: "packagebar3.service3" - ) - XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar3.proto") - } - - func testNameOfFileContainingSymbolMethod() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( - named: "packagebar4.service4.testMethod4" - ) - XCTAssertEqual(try nameOfFileContainingSymbolResult.get(), "bar4.proto") - } - - func testNameOfFileContainingSymbolNonExistentSymbol() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let nameOfFileContainingSymbolResult = registry.nameOfFileContainingSymbol( - named: "packagebar2.enumType3" - ) - XCTAssertThrowsGRPCStatus(try nameOfFileContainingSymbolResult.get()) { - status in - XCTAssertEqual( - status, - GRPCStatus(code: .notFound, message: "The provided symbol could not be found.") - ) - } - } - - // Testing the serializedFileDescriptorProto method in different cases. - - func testSerialisedFileDescriptorProtosForDependenciesOfFile() throws { - var protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtosResult = - registry - .serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") - - switch serializedFileDescriptorProtosResult { - case .success(let serializedFileDescriptorProtos): - let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) - } - // Tests that the functions returns all the transitive dependencies, with their services and - // methods, together with the initial proto, as serialized data. - XCTAssertEqual(fileDescriptorProtos.count, 4) - for fileDescriptorProto in fileDescriptorProtos { - guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileDescriptorProto.name) \ - in the original file descriptor protos list. - """ - ) - } - - for service in fileDescriptorProto.service { - guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { - return XCTFail( - """ - Could not find the \(service.name) in the service \ - list of the \(fileDescriptorProto.name) file descriptor proto. - """ - ) - } - - let originalMethods = protos[protoIndex].service[serviceIndex].method - for method in service.method { - XCTAssert(originalMethods.contains(method)) - } - - for messageType in fileDescriptorProto.messageType { - XCTAssert(protos[protoIndex].messageType.contains(messageType)) - } - } - - protos.removeAll { $0 == fileDescriptorProto } - } - XCTAssert(protos.isEmpty) - case .failure(let status): - XCTFail( - "Faild with GRPCStatus code: " + String(status.code.rawValue) + " and message: " - + (status.message ?? "empty") + "." - ) - } - } - - func testSerialisedFileDescriptorProtosForDependenciesOfFileComplexDependencyGraph() throws { - var protos = makeProtosWithComplexDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtosResult = - registry - .serialisedFileDescriptorProtosForDependenciesOfFile(named: "foo0.proto") - switch serializedFileDescriptorProtosResult { - case .success(let serializedFileDescriptorProtos): - let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) - } - // Tests that the functions returns all the tranzitive dependencies, with their services and - // methods, together with the initial proto, as serialized data. - XCTAssertEqual(fileDescriptorProtos.count, 21) - for fileDescriptorProto in fileDescriptorProtos { - guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileDescriptorProto.name) \ - in the original file descriptor protos list. - """ - ) - } - - for service in fileDescriptorProto.service { - guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { - return XCTFail( - """ - Could not find the \(service.name) in the service \ - list of the \(fileDescriptorProto.name) file descriptor proto. - """ - ) - } - - let originalMethods = protos[protoIndex].service[serviceIndex].method - for method in service.method { - XCTAssert(originalMethods.contains(method)) - } - - for messageType in fileDescriptorProto.messageType { - XCTAssert(protos[protoIndex].messageType.contains(messageType)) - } - } - - protos.removeAll { $0 == fileDescriptorProto } - } - XCTAssert(protos.isEmpty) - case .failure(let status): - XCTFail( - "Faild with GRPCStatus code: " + String(status.code.rawValue) + " and message: " - + (status.message ?? "empty") + "." - ) - } - } - - func testSerialisedFileDescriptorProtosForDependenciesOfFileDependencyLoops() throws { - var protos = makeProtosWithDependencies() - // Making dependencies of the "bar1.proto" to depend on "bar1.proto". - protos[1].dependency.append("bar1.proto") - protos[2].dependency.append("bar1.proto") - protos[3].dependency.append("bar1.proto") - let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtosResult = - registry - .serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") - switch serializedFileDescriptorProtosResult { - case .success(let serializedFileDescriptorProtos): - let fileDescriptorProtos = try serializedFileDescriptorProtos.map { - try Google_Protobuf_FileDescriptorProto(serializedBytes: $0) - } - // Test that we get only 4 serialized File Descriptor Protos as response. - XCTAssertEqual(fileDescriptorProtos.count, 4) - for fileDescriptorProto in fileDescriptorProtos { - guard let protoIndex = protos.firstIndex(of: fileDescriptorProto) else { - return XCTFail( - """ - Could not find the file descriptor proto of \(fileDescriptorProto.name) \ - in the original file descriptor protos list. - """ - ) - } - - for service in fileDescriptorProto.service { - guard let serviceIndex = protos[protoIndex].service.firstIndex(of: service) else { - return XCTFail( - """ - Could not find the \(service.name) in the service \ - list of the \(fileDescriptorProto.name) file descriptor proto. - """ - ) - } - - let originalMethods = protos[protoIndex].service[serviceIndex].method - for method in service.method { - XCTAssert(originalMethods.contains(method)) - } - - for messageType in fileDescriptorProto.messageType { - XCTAssert(protos[protoIndex].messageType.contains(messageType)) - } - } - - protos.removeAll { $0 == fileDescriptorProto } - } - XCTAssert(protos.isEmpty) - case .failure(let status): - XCTFail( - "Faild with GRPCStatus code: " + String(status.code.rawValue) + " and message: " - + (status.message ?? "empty") + "." - ) - } - } - - func testSerialisedFileDescriptorProtosForDependenciesOfFileInvalidFile() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtosForDependenciesOfFileResult = - registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "invalid.proto") - - XCTAssertThrowsGRPCStatus(try serializedFileDescriptorProtosForDependenciesOfFileResult.get()) { - status in - XCTAssertEqual( - status, - GRPCStatus( - code: .notFound, - message: "No reflection data for 'invalid.proto'." - ) - ) - } - } - - func testSerialisedFileDescriptorProtosForDependenciesOfFileDependencyNotProto() throws { - var protos = makeProtosWithDependencies() - protos[0].dependency.append("invalidDependency") - let registry = try ReflectionServiceData(fileDescriptors: protos) - let serializedFileDescriptorProtosForDependenciesOfFileResult = - registry.serialisedFileDescriptorProtosForDependenciesOfFile(named: "bar1.proto") - - XCTAssertThrowsGRPCStatus(try serializedFileDescriptorProtosForDependenciesOfFileResult.get()) { - status in - XCTAssertEqual( - status, - GRPCStatus( - code: .notFound, - message: - "No reflection data for 'invalidDependency' which is a dependency of 'bar1.proto'." - ) - ) - } - } - - // Testing the nameOfFileContainingExtension() method. - - func testNameOfFileContainingExtensions() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - for proto in protos { - for `extension` in proto.extension { - let typeName = String(`extension`.extendee.drop(while: { $0 == "." })) - let registryFileNameResult = registry.nameOfFileContainingExtension( - extendeeName: typeName, - fieldNumber: `extension`.number - ) - XCTAssertEqual(try registryFileNameResult.get(), proto.name) - } - } - } - - func testNameOfFileContainingExtensionsInvalidTypeName() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let registryFileNameResult = registry.nameOfFileContainingExtension( - extendeeName: "InvalidType", - fieldNumber: 2 - ) - - XCTAssertThrowsGRPCStatus(try registryFileNameResult.get()) { - status in - XCTAssertEqual( - status, - GRPCStatus(code: .notFound, message: "The provided extension could not be found.") - ) - } - } - - func testNameOfFileContainingExtensionsInvalidFieldNumber() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let registryFileNameResult = registry.nameOfFileContainingExtension( - extendeeName: protos[0].extension[0].extendee, - fieldNumber: 9 - ) - - XCTAssertThrowsGRPCStatus(try registryFileNameResult.get()) { - status in - XCTAssertEqual( - status, - GRPCStatus(code: .notFound, message: "The provided extension could not be found.") - ) - } - } - - func testNameOfFileContainingExtensionsDuplicatedExtensions() throws { - var protos = makeProtosWithDependencies() - protos[0].extension.append( - .with { - $0.extendee = ".packagebar1.inputMessage1" - $0.number = 2 - } - ) - XCTAssertThrowsError( - try ReflectionServiceData(fileDescriptors: protos) - ) { error in - XCTAssertEqual( - error as? GRPCStatus, - GRPCStatus( - code: .alreadyExists, - message: - """ - The extension of the packagebar1.inputMessage1 type with the field number equal to \ - 2 from \(protos[0].name) already exists in \(protos[0].name). - """ - ) - ) - } - } - - // Testing the extensionsFieldNumbersOfType() method. - - func testExtensionsFieldNumbersOfType() throws { - var protos = makeProtosWithDependencies() - protos[0].extension.append( - .with { - $0.extendee = ".packagebar1.inputMessage1" - $0.number = 120 - } - ) - let registry = try ReflectionServiceData(fileDescriptors: protos) - let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( - named: "packagebar1.inputMessage1" - ) - - XCTAssertEqual(try extensionsFieldNumbersOfTypeResult.get(), [1, 2, 3, 4, 5, 120]) - } - - func testExtensionsFieldNumbersOfTypeNoExtensionsType() throws { - var protos = makeProtosWithDependencies() - protos[0].messageType.append( - Google_Protobuf_DescriptorProto.with { - $0.name = "noExtensionMessage" - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "noExtensionField" - $0.type = .bool - } - ] - } - ) - let registry = try ReflectionServiceData(fileDescriptors: protos) - let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( - named: "packagebar1.noExtensionMessage" - ) - - XCTAssertEqual(try extensionsFieldNumbersOfTypeResult.get(), []) - } - - func testExtensionsFieldNumbersOfTypeInvalidTypeName() throws { - let protos = makeProtosWithDependencies() - let registry = try ReflectionServiceData(fileDescriptors: protos) - let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( - named: "packagebar1.invalidTypeMessage" - ) - - XCTAssertThrowsGRPCStatus(try extensionsFieldNumbersOfTypeResult.get()) { - status in - XCTAssertEqual( - status, - GRPCStatus(code: .invalidArgument, message: "The provided type is invalid.") - ) - } - } - - func testExtensionsFieldNumbersOfTypeExtensionsInDifferentProtoFiles() throws { - var protos = makeProtosWithDependencies() - protos[2].extension.append( - .with { - $0.extendee = ".packagebar1.inputMessage1" - $0.number = 130 - } - ) - let registry = try ReflectionServiceData(fileDescriptors: protos) - let extensionsFieldNumbersOfTypeResult = registry.extensionsFieldNumbersOfType( - named: "packagebar1.inputMessage1" - ) - - XCTAssertEqual(try extensionsFieldNumbersOfTypeResult.get(), [1, 2, 3, 4, 5, 130]) - } - - func testReadSerializedFileDescriptorProto() throws { - let initialFileDescriptorProto = generateFileDescriptorProto(fileName: "test", suffix: "1") - let data = try initialFileDescriptorProto.serializedData().base64EncodedData() - let temporaryDirectory: String - #if os(Linux) - temporaryDirectory = "/tmp/" - #else - if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { - temporaryDirectory = FileManager.default.temporaryDirectory.path() - } else { - temporaryDirectory = "/tmp/" - } - #endif - let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" - FileManager.default.createFile(atPath: filePath, contents: data) - defer { - XCTAssertNoThrow(try FileManager.default.removeItem(atPath: filePath)) - } - let reflectionServiceFileDescriptorProto = - try ReflectionService.readSerializedFileDescriptorProto(atPath: filePath) - XCTAssertEqual(reflectionServiceFileDescriptorProto, initialFileDescriptorProto) - } - - func testReadSerializedFileDescriptorProtoInvalidFileContents() throws { - let invalidData = "%%%%%££££".data(using: .utf8) - let temporaryDirectory: String - #if os(Linux) - temporaryDirectory = "/tmp/" - #else - if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { - temporaryDirectory = FileManager.default.temporaryDirectory.path() - } else { - temporaryDirectory = "/tmp/" - } - #endif - let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" - FileManager.default.createFile(atPath: filePath, contents: invalidData) - defer { - XCTAssertNoThrow(try FileManager.default.removeItem(atPath: filePath)) - } - - XCTAssertThrowsGRPCStatus( - try ReflectionService.readSerializedFileDescriptorProto(atPath: filePath) - ) { - status in - XCTAssertEqual( - status, - GRPCStatus( - code: .invalidArgument, - message: - """ - The \(filePath) file contents could not be transformed \ - into serialized data representing a file descriptor proto. - """ - ) - ) - } - } - - func testReadSerializedFileDescriptorProtos() throws { - let initialFileDescriptorProtos = makeProtosWithDependencies() - var filePaths: [String] = [] - - for initialFileDescriptorProto in initialFileDescriptorProtos { - let data = try initialFileDescriptorProto.serializedData() - .base64EncodedData() - let temporaryDirectory: String - #if os(Linux) - temporaryDirectory = "/tmp/" - #else - if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { - temporaryDirectory = FileManager.default.temporaryDirectory.path() - } else { - temporaryDirectory = "/tmp/" - } - #endif - let filePath = "\(temporaryDirectory)test\(UUID()).grpc.reflection" - FileManager.default.createFile(atPath: filePath, contents: data) - filePaths.append(filePath) - } - defer { - for filePath in filePaths { - XCTAssertNoThrow(try FileManager.default.removeItem(atPath: filePath)) - } - } - - let reflectionServiceFileDescriptorProtos = - try ReflectionService.readSerializedFileDescriptorProtos(atPaths: filePaths) - XCTAssertEqual(reflectionServiceFileDescriptorProtos, initialFileDescriptorProtos) - } -} diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift deleted file mode 100644 index 75a1e2fed..000000000 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 2023, 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 Foundation -import GRPC -import SwiftProtobuf -import XCTest - -internal func makeExtensions( - forType typeName: String, - number: Int -) -> [Google_Protobuf_FieldDescriptorProto] { - var extensions: [Google_Protobuf_FieldDescriptorProto] = [] - for id in 1 ... number { - extensions.append( - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "extension" + typeName + "-" + String(id) - $0.extendee = typeName - $0.number = Int32(id) - } - ) - } - return extensions -} - -internal func generateFileDescriptorProto( - fileName name: String, - suffix: String -) -> Google_Protobuf_FileDescriptorProto { - let inputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "inputMessage" + suffix - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "inputField" - $0.type = .bool - } - ] - } - - let packageName = "package" + name + suffix - let inputMessageExtensions = makeExtensions( - forType: "." + packageName + "." + "inputMessage" + suffix, - number: 5 - ) - - let outputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "outputMessage" + suffix - $0.field = [ - Google_Protobuf_FieldDescriptorProto.with { - $0.name = "outputField" - $0.type = .int32 - } - ] - } - - let enumType = Google_Protobuf_EnumDescriptorProto.with { - $0.name = "enumType" + suffix - $0.value = [ - Google_Protobuf_EnumValueDescriptorProto.with { - $0.name = "value1" - }, - Google_Protobuf_EnumValueDescriptorProto.with { - $0.name = "value2" - }, - ] - } - - let method = Google_Protobuf_MethodDescriptorProto.with { - $0.name = "testMethod" + suffix - $0.inputType = inputMessage.name - $0.outputType = outputMessage.name - } - - let serviceDescriptor = Google_Protobuf_ServiceDescriptorProto.with { - $0.method = [method] - $0.name = "service" + suffix - } - - let fileDescriptorProto = Google_Protobuf_FileDescriptorProto.with { - $0.service = [serviceDescriptor] - $0.name = name + suffix + ".proto" - $0.package = "package" + name + suffix - $0.messageType = [inputMessage, outputMessage] - $0.enumType = [enumType] - $0.extension = inputMessageExtensions - } - - return fileDescriptorProto -} - -/// Creates the dependencies of the proto used in the testing context. -internal func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorProto] { - var fileDependencies: [Google_Protobuf_FileDescriptorProto] = [] - for id in 1 ... 4 { - let fileDescriptorProto = generateFileDescriptorProto(fileName: "bar", suffix: String(id)) - if id != 1 { - // Dependency of the first dependency. - fileDependencies[0].dependency.append(fileDescriptorProto.name) - } - fileDependencies.append(fileDescriptorProto) - } - return fileDependencies -} - -internal func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescriptorProto] { - var protos: [Google_Protobuf_FileDescriptorProto] = [] - protos.append(generateFileDescriptorProto(fileName: "foo", suffix: "0")) - for id in 1 ... 10 { - let fileDescriptorProtoA = generateFileDescriptorProto( - fileName: "fooA", - suffix: String(id) + "A" - ) - let fileDescriptorProtoB = generateFileDescriptorProto( - fileName: "fooB", - suffix: String(id) + "B" - ) - - let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1 - protos[parent].dependency.append(fileDescriptorProtoA.name) - protos[parent].dependency.append(fileDescriptorProtoB.name) - protos.append(fileDescriptorProtoA) - protos.append(fileDescriptorProtoB) - } - return protos -} - -func XCTAssertThrowsGRPCStatus( - _ expression: @autoclosure () throws -> T, - _ errorHandler: (GRPCStatus) -> Void -) { - XCTAssertThrowsError(try expression()) { error in - guard let error = error as? GRPCStatus else { - return XCTFail("Error had unexpected type '\(type(of: error))'") - } - - errorHandler(error) - } -} - -extension Google_Protobuf_FileDescriptorProto { - var qualifiedServiceNames: [String] { - self.service.map { self.package + "." + $0.name } - } -} - -extension Sequence where Element == Google_Protobuf_EnumDescriptorProto { - var names: [String] { - self.map { $0.name } - } -} - -extension Grpc_Reflection_V1_ExtensionRequest { - init(_ v1AlphaExtensionRequest: Grpc_Reflection_V1alpha_ExtensionRequest) { - self = .with { - $0.containingType = v1AlphaExtensionRequest.containingType - $0.extensionNumber = v1AlphaExtensionRequest.extensionNumber - $0.unknownFields = v1AlphaExtensionRequest.unknownFields - } - } -} - -extension Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest? { - init(_ v1AlphaRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest) { - guard let messageRequest = v1AlphaRequest.messageRequest else { - self = nil - return - } - switch messageRequest { - case .allExtensionNumbersOfType(let typeName): - self = .allExtensionNumbersOfType(typeName) - case .fileByFilename(let fileName): - self = .fileByFilename(fileName) - case .fileContainingSymbol(let symbol): - self = .fileContainingSymbol(symbol) - case .fileContainingExtension(let v1AlphaExtensionRequest): - self = .fileContainingExtension( - Grpc_Reflection_V1_ExtensionRequest(v1AlphaExtensionRequest) - ) - case .listServices(let parameter): - self = .listServices(parameter) - } - } -} - -extension Grpc_Reflection_V1_ServerReflectionRequest { - init(_ v1AlphaRequest: Grpc_Reflection_V1alpha_ServerReflectionRequest) { - self = .with { - $0.host = v1AlphaRequest.host - $0.messageRequest = Grpc_Reflection_V1_ServerReflectionRequest.OneOf_MessageRequest?( - v1AlphaRequest - ) - } - } -} - -extension Grpc_Reflection_V1_FileDescriptorResponse { - init(_ v1AlphaFileDescriptorResponse: Grpc_Reflection_V1alpha_FileDescriptorResponse) { - self = .with { - $0.fileDescriptorProto = v1AlphaFileDescriptorResponse.fileDescriptorProto - $0.unknownFields = v1AlphaFileDescriptorResponse.unknownFields - } - } -} - -extension Grpc_Reflection_V1_ExtensionNumberResponse { - init(_ v1AlphaExtensionNumberResponse: Grpc_Reflection_V1alpha_ExtensionNumberResponse) { - self = .with { - $0.baseTypeName = v1AlphaExtensionNumberResponse.baseTypeName - $0.extensionNumber = v1AlphaExtensionNumberResponse.extensionNumber - $0.unknownFields = v1AlphaExtensionNumberResponse.unknownFields - } - } -} - -extension Grpc_Reflection_V1_ServiceResponse { - init(_ v1AlphaServiceResponse: Grpc_Reflection_V1alpha_ServiceResponse) { - self = .with { - $0.name = v1AlphaServiceResponse.name - $0.unknownFields = v1AlphaServiceResponse.unknownFields - } - } -} - -extension Grpc_Reflection_V1_ListServiceResponse { - init(_ v1AlphaListServicesResponse: Grpc_Reflection_V1alpha_ListServiceResponse) { - self = .with { - $0.service = v1AlphaListServicesResponse.service.map { - Grpc_Reflection_V1_ServiceResponse($0) - } - $0.unknownFields = v1AlphaListServicesResponse.unknownFields - } - } -} - -extension Grpc_Reflection_V1_ErrorResponse { - init(_ v1AlphaErrorResponse: Grpc_Reflection_V1alpha_ErrorResponse) { - self = .with { - $0.errorCode = v1AlphaErrorResponse.errorCode - $0.errorMessage = v1AlphaErrorResponse.errorMessage - $0.unknownFields = v1AlphaErrorResponse.unknownFields - } - } -} - -extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse? { - init(_ v1AlphaResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse) { - guard let messageRequest = v1AlphaResponse.messageResponse else { - self = nil - return - } - switch messageRequest { - case .fileDescriptorResponse(let v1AlphaFileDescriptorResponse): - self = .fileDescriptorResponse( - Grpc_Reflection_V1_FileDescriptorResponse( - v1AlphaFileDescriptorResponse - ) - ) - case .allExtensionNumbersResponse(let v1AlphaAllExtensionNumbersResponse): - self = .allExtensionNumbersResponse( - Grpc_Reflection_V1_ExtensionNumberResponse( - v1AlphaAllExtensionNumbersResponse - ) - ) - case .listServicesResponse(let v1AlphaListServicesResponse): - self = .listServicesResponse( - Grpc_Reflection_V1_ListServiceResponse( - v1AlphaListServicesResponse - ) - ) - case .errorResponse(let v1AlphaErrorResponse): - self = .errorResponse( - Grpc_Reflection_V1_ErrorResponse(v1AlphaErrorResponse) - ) - } - } -} - -extension Grpc_Reflection_V1_ServerReflectionResponse { - init(_ v1AlphaResponse: Grpc_Reflection_V1alpha_ServerReflectionResponse) { - self = .with { - $0.validHost = v1AlphaResponse.validHost - $0.originalRequest = Grpc_Reflection_V1_ServerReflectionRequest( - v1AlphaResponse.originalRequest - ) - $0.messageResponse = Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse?( - v1AlphaResponse - ) - } - } -} - -extension Grpc_Reflection_V1alpha_ExtensionRequest { - init(_ v1ExtensionRequest: Grpc_Reflection_V1_ExtensionRequest) { - self = .with { - $0.containingType = v1ExtensionRequest.containingType - $0.extensionNumber = v1ExtensionRequest.extensionNumber - $0.unknownFields = v1ExtensionRequest.unknownFields - } - } -} - -extension Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest? { - init(_ v1Request: Grpc_Reflection_V1_ServerReflectionRequest) { - guard let messageRequest = v1Request.messageRequest else { - self = nil - return - } - switch messageRequest { - case .allExtensionNumbersOfType(let typeName): - self = .allExtensionNumbersOfType(typeName) - case .fileByFilename(let fileName): - self = .fileByFilename(fileName) - case .fileContainingSymbol(let symbol): - self = .fileContainingSymbol(symbol) - case .fileContainingExtension(let v1ExtensionRequest): - self = .fileContainingExtension( - Grpc_Reflection_V1alpha_ExtensionRequest(v1ExtensionRequest) - ) - case .listServices(let parameter): - self = .listServices(parameter) - } - } -} - -extension Grpc_Reflection_V1alpha_ServerReflectionRequest { - init(_ v1Request: Grpc_Reflection_V1_ServerReflectionRequest) { - self = .with { - $0.host = v1Request.host - $0.messageRequest = Grpc_Reflection_V1alpha_ServerReflectionRequest.OneOf_MessageRequest?( - v1Request - ) - } - } -} diff --git a/Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift b/Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift deleted file mode 100644 index e5dbd836a..000000000 --- a/Tests/GRPCTests/GRPCServerPipelineConfiguratorTests.swift +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import NIOHTTP2 -import NIOTLS -import XCTest - -@testable import GRPC - -class GRPCServerPipelineConfiguratorTests: GRPCTestCase { - private var channel: EmbeddedChannel! - - private func assertConfigurator(isPresent: Bool) { - assertThat( - try self.channel.pipeline.handler(type: GRPCServerPipelineConfigurator.self).wait(), - isPresent ? .doesNotThrow() : .throws() - ) - } - - private func assertHTTP2Handler(isPresent: Bool) { - assertThat( - try self.channel.pipeline.handler(type: NIOHTTP2Handler.self).wait(), - isPresent ? .doesNotThrow() : .throws() - ) - } - - private func assertGRPCWebToHTTP2Handler(isPresent: Bool) { - assertThat( - try self.channel.pipeline.handler(type: GRPCWebToHTTP2ServerCodec.self).wait(), - isPresent ? .doesNotThrow() : .throws() - ) - } - - private func setUp(tls: Bool, requireALPN: Bool = true) { - self.channel = EmbeddedChannel() - - var configuration = Server.Configuration.default( - target: .unixDomainSocket("/ignored"), - eventLoopGroup: self.channel.eventLoop, - serviceProviders: [] - ) - - configuration.logger = self.serverLogger - - if tls { - #if canImport(NIOSSL) - configuration.tlsConfiguration = .makeServerConfigurationBackedByNIOSSL( - certificateChain: [], - privateKey: .file("not used"), - requireALPN: requireALPN - ) - #else - fatalError("TLS enabled for a test when NIOSSL is not available") - #endif - } - - let handler = GRPCServerPipelineConfigurator(configuration: configuration) - assertThat(try self.channel.pipeline.addHandler(handler).wait(), .doesNotThrow()) - } - - #if canImport(NIOSSL) - func testHTTP2SetupViaALPN() { - self.setUp(tls: true, requireALPN: true) - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: "h2") - self.channel.pipeline.fireUserInboundEventTriggered(event) - self.assertConfigurator(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - } - - func testGRPCExpSetupViaALPN() { - self.setUp(tls: true, requireALPN: true) - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: "grpc-exp") - self.channel.pipeline.fireUserInboundEventTriggered(event) - self.assertConfigurator(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - } - - func testHTTP1Dot1SetupViaALPN() { - self.setUp(tls: true, requireALPN: true) - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: "http/1.1") - self.channel.pipeline.fireUserInboundEventTriggered(event) - self.assertConfigurator(isPresent: false) - self.assertGRPCWebToHTTP2Handler(isPresent: true) - } - - func testUnrecognisedALPNCloses() { - self.setUp(tls: true, requireALPN: true) - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: "unsupported") - self.channel.pipeline.fireUserInboundEventTriggered(event) - self.channel.embeddedEventLoop.run() - assertThat(try self.channel.closeFuture.wait(), .doesNotThrow()) - } - - func testNoNegotiatedProtocolCloses() { - self.setUp(tls: true, requireALPN: true) - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: nil) - self.channel.pipeline.fireUserInboundEventTriggered(event) - self.channel.embeddedEventLoop.run() - assertThat(try self.channel.closeFuture.wait(), .doesNotThrow()) - } - - func testNoNegotiatedProtocolFallbackToBytesWhenALPNNotRequired() throws { - self.setUp(tls: true, requireALPN: false) - - // Require ALPN is disabled, so this is a no-op. - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: nil) - self.channel.pipeline.fireUserInboundEventTriggered(event) - - // Configure via bytes. - let bytes = ByteBuffer(staticString: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") - assertThat(try self.channel.writeInbound(bytes), .doesNotThrow()) - self.assertConfigurator(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - } - #endif // canImport(NIOSSL) - - func testHTTP2SetupViaBytes() { - self.setUp(tls: false) - let bytes = ByteBuffer(staticString: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") - assertThat(try self.channel.writeInbound(bytes), .doesNotThrow()) - self.assertConfigurator(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - } - - func testHTTP2SetupViaBytesDripFed() { - self.setUp(tls: false) - var bytes = ByteBuffer(staticString: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") - var head = bytes.readSlice(length: bytes.readableBytes - 1)! - let tail = bytes.readSlice(length: 1)! - - while let slice = head.readSlice(length: 1) { - assertThat(try self.channel.writeInbound(slice), .doesNotThrow()) - self.assertConfigurator(isPresent: true) - self.assertHTTP2Handler(isPresent: false) - } - - // Final byte. - assertThat(try self.channel.writeInbound(tail), .doesNotThrow()) - self.assertConfigurator(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - } - - func testHTTP1Dot1SetupViaBytes() { - self.setUp(tls: false) - let bytes = ByteBuffer(staticString: "GET http://www.foo.bar HTTP/1.1\r\n") - assertThat(try self.channel.writeInbound(bytes), .doesNotThrow()) - self.assertConfigurator(isPresent: false) - self.assertGRPCWebToHTTP2Handler(isPresent: true) - } - - func testHTTP1Dot1SetupViaBytesDripFed() { - self.setUp(tls: false) - var bytes = ByteBuffer(staticString: "GET http://www.foo.bar HTTP/1.1\r\n") - var head = bytes.readSlice(length: bytes.readableBytes - 1)! - let tail = bytes.readSlice(length: 1)! - - while let slice = head.readSlice(length: 1) { - assertThat(try self.channel.writeInbound(slice), .doesNotThrow()) - self.assertConfigurator(isPresent: true) - self.assertGRPCWebToHTTP2Handler(isPresent: false) - } - - // Final byte. - assertThat(try self.channel.writeInbound(tail), .doesNotThrow()) - self.assertConfigurator(isPresent: false) - self.assertGRPCWebToHTTP2Handler(isPresent: true) - } - - func testUnexpectedInputClosesEventuallyWhenDripFed() { - self.setUp(tls: false) - var bytes = ByteBuffer(repeating: UInt8(ascii: "a"), count: 2048) - - while let slice = bytes.readSlice(length: 1) { - assertThat(try self.channel.writeInbound(slice), .doesNotThrow()) - self.assertConfigurator(isPresent: true) - self.assertHTTP2Handler(isPresent: false) - self.assertGRPCWebToHTTP2Handler(isPresent: false) - } - - self.channel.embeddedEventLoop.run() - assertThat(try self.channel.closeFuture.wait(), .doesNotThrow()) - } - - func testReadsAreUnbufferedAfterConfiguration() throws { - self.setUp(tls: false) - - var bytes = ByteBuffer(staticString: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") - // A SETTINGS frame MUST follow the connection preface. Append one so that the HTTP/2 handler - // responds with its initial settings (and we validate that we forward frames once configuring). - let emptySettingsFrameBytes: [UInt8] = [ - 0x00, 0x00, 0x00, // 3-byte payload length (0 bytes) - 0x04, // 1-byte frame type (SETTINGS) - 0x00, // 1-byte flags (none) - 0x00, 0x00, 0x00, 0x00, // 4-byte stream identifier - ] - bytes.writeBytes(emptySettingsFrameBytes) - - // Do the setup. - assertThat(try self.channel.writeInbound(bytes), .doesNotThrow()) - self.assertConfigurator(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - - // We expect the server to respond with a SETTINGS frame now. - let ioData = try channel.readOutbound(as: IOData.self) - switch ioData { - case var .some(.byteBuffer(buffer)): - if let frame = buffer.readBytes(length: 9) { - // Just check it's a SETTINGS frame. - assertThat(frame[3], .is(0x04)) - } else { - XCTFail("Expected more bytes") - } - - default: - XCTFail("Expected ByteBuffer but got \(String(describing: ioData))") - } - } - - #if canImport(NIOSSL) - func testALPNIsPreferredOverBytes() throws { - self.setUp(tls: true, requireALPN: true) - - // Write in an HTTP/1 request line. This should just be buffered. - let bytes = ByteBuffer(staticString: "GET http://www.foo.bar HTTP/1.1\r\n") - assertThat(try self.channel.writeInbound(bytes), .doesNotThrow()) - - self.assertConfigurator(isPresent: true) - self.assertHTTP2Handler(isPresent: false) - self.assertGRPCWebToHTTP2Handler(isPresent: false) - - // Now configure HTTP/2 with ALPN. This should be used to configure the pipeline. - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: "h2") - self.channel.pipeline.fireUserInboundEventTriggered(event) - - self.assertConfigurator(isPresent: false) - self.assertGRPCWebToHTTP2Handler(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - } - - func testALPNFallbackToAlreadyBufferedBytes() throws { - self.setUp(tls: true, requireALPN: false) - - // Write in an HTTP/2 connection preface. This should just be buffered. - let bytes = ByteBuffer(staticString: "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") - assertThat(try self.channel.writeInbound(bytes), .doesNotThrow()) - - self.assertConfigurator(isPresent: true) - self.assertHTTP2Handler(isPresent: false) - - // Complete the handshake with no protocol negotiated, we should fallback to the buffered bytes. - let event = TLSUserEvent.handshakeCompleted(negotiatedProtocol: nil) - self.channel.pipeline.fireUserInboundEventTriggered(event) - - self.assertConfigurator(isPresent: false) - self.assertHTTP2Handler(isPresent: true) - } - #endif // canImport(NIOSSL) -} diff --git a/Tests/GRPCTests/GRPCStatusCodeTests.swift b/Tests/GRPCTests/GRPCStatusCodeTests.swift deleted file mode 100644 index 227645d6a..000000000 --- a/Tests/GRPCTests/GRPCStatusCodeTests.swift +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import Logging -import NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import XCTest - -@testable import GRPC - -class GRPCStatusCodeTests: GRPCTestCase { - var channel: EmbeddedChannel! - - override func setUp() { - super.setUp() - - let handler = GRPCClientChannelHandler( - callType: .unary, - maximumReceiveMessageLength: .max, - logger: self.logger - ) - self.channel = EmbeddedChannel(handler: handler) - } - - func headersFramePayload(status: HTTPResponseStatus) -> HTTP2Frame.FramePayload { - let headers: HPACKHeaders = [":status": "\(status.code)"] - return .headers(.init(headers: headers)) - } - - func sendRequestHead() { - let requestHead = _GRPCRequestHead( - method: "POST", - scheme: "http", - path: "/foo/bar", - host: "localhost", - deadline: .distantFuture, - customMetadata: [:], - encoding: .disabled - ) - let clientRequestHead: _RawGRPCClientRequestPart = .head(requestHead) - XCTAssertNoThrow(try self.channel.writeOutbound(clientRequestHead)) - } - - func doTestResponseStatus(_ status: HTTPResponseStatus, expected: GRPCStatus.Code) throws { - // Send the request head so we're in a valid state to receive headers. - self.sendRequestHead() - XCTAssertThrowsError( - try self.channel - .writeInbound(self.headersFramePayload(status: status)) - ) { error in - guard let withContext = error as? GRPCError.WithContext, - let invalidHTTPStatus = withContext.error as? GRPCError.InvalidHTTPStatus - else { - XCTFail("Unexpected error: \(error)") - return - } - - XCTAssertEqual(invalidHTTPStatus.makeGRPCStatus().code, expected) - } - } - - func testTooManyRequests() throws { - try self.doTestResponseStatus(.tooManyRequests, expected: .unavailable) - } - - func testBadGateway() throws { - try self.doTestResponseStatus(.badGateway, expected: .unavailable) - } - - func testServiceUnavailable() throws { - try self.doTestResponseStatus(.serviceUnavailable, expected: .unavailable) - } - - func testGatewayTimeout() throws { - try self.doTestResponseStatus(.gatewayTimeout, expected: .unavailable) - } - - func testBadRequest() throws { - try self.doTestResponseStatus(.badRequest, expected: .internalError) - } - - func testUnauthorized() throws { - try self.doTestResponseStatus(.unauthorized, expected: .unauthenticated) - } - - func testForbidden() throws { - try self.doTestResponseStatus(.forbidden, expected: .permissionDenied) - } - - func testNotFound() throws { - try self.doTestResponseStatus(.notFound, expected: .unimplemented) - } - - func testStatusCodeAndMessageAreRespectedForNon200Responses() throws { - let status = GRPCStatus(code: .unknown, message: "Not the HTTP error phrase") - - let headers: HPACKHeaders = [ - ":status": "\(HTTPResponseStatus.imATeapot.code)", - GRPCHeaderName.statusCode: "\(status.code.rawValue)", - GRPCHeaderName.statusMessage: status.message!, - ] - - self.sendRequestHead() - let headerFramePayload = HTTP2Frame.FramePayload.headers(.init(headers: headers)) - try self.channel.writeInbound(headerFramePayload) - - let responsePart1 = try XCTUnwrap( - self.channel.readInbound(as: _GRPCClientResponsePart.self) - ) - - switch responsePart1 { - case .trailingMetadata: - () - case .initialMetadata, .message, .status: - XCTFail("Unexpected response part \(responsePart1)") - } - - let responsePart2 = try XCTUnwrap( - self.channel.readInbound(as: _GRPCClientResponsePart.self) - ) - - switch responsePart2 { - case .initialMetadata, .message, .trailingMetadata: - XCTFail("Unexpected response part \(responsePart2)") - case let .status(actual): - XCTAssertEqual(actual.code, status.code) - XCTAssertEqual(actual.message, status.message) - } - } - - func testNon200StatusCodesAreConverted() throws { - let tests: [(Int, GRPCStatus.Code)] = [ - (400, .internalError), - (401, .unauthenticated), - (403, .permissionDenied), - (404, .unimplemented), - (429, .unavailable), - (502, .unavailable), - (503, .unavailable), - (504, .unavailable), - ] - - for (httpStatusCode, grpcStatusCode) in tests { - let headers: HPACKHeaders = [":status": "\(httpStatusCode)"] - - self.setUp() - self.sendRequestHead() - let headerFramePayload = HTTP2Frame.FramePayload - .headers(.init(headers: headers, endStream: true)) - try self.channel.writeInbound(headerFramePayload) - - let responsePart1 = try XCTUnwrap( - self.channel.readInbound(as: _GRPCClientResponsePart.self) - ) - - switch responsePart1 { - case .trailingMetadata: - () - case .initialMetadata, .message, .status: - XCTFail("Unexpected response part \(responsePart1)") - } - - let responsePart2 = try XCTUnwrap( - self.channel.readInbound(as: _GRPCClientResponsePart.self) - ) - - switch responsePart2 { - case .initialMetadata, .message, .trailingMetadata: - XCTFail("Unexpected response part \(responsePart2)") - case let .status(actual): - XCTAssertEqual(actual.code, grpcStatusCode) - } - } - } - - func testCodeFromRawValue() { - XCTAssertEqual(GRPCStatus.Code(rawValue: 0), .ok) - XCTAssertEqual(GRPCStatus.Code(rawValue: 1), .cancelled) - XCTAssertEqual(GRPCStatus.Code(rawValue: 2), .unknown) - XCTAssertEqual(GRPCStatus.Code(rawValue: 3), .invalidArgument) - XCTAssertEqual(GRPCStatus.Code(rawValue: 4), .deadlineExceeded) - XCTAssertEqual(GRPCStatus.Code(rawValue: 5), .notFound) - XCTAssertEqual(GRPCStatus.Code(rawValue: 6), .alreadyExists) - XCTAssertEqual(GRPCStatus.Code(rawValue: 7), .permissionDenied) - XCTAssertEqual(GRPCStatus.Code(rawValue: 8), .resourceExhausted) - XCTAssertEqual(GRPCStatus.Code(rawValue: 9), .failedPrecondition) - XCTAssertEqual(GRPCStatus.Code(rawValue: 10), .aborted) - XCTAssertEqual(GRPCStatus.Code(rawValue: 11), .outOfRange) - XCTAssertEqual(GRPCStatus.Code(rawValue: 12), .unimplemented) - XCTAssertEqual(GRPCStatus.Code(rawValue: 13), .internalError) - XCTAssertEqual(GRPCStatus.Code(rawValue: 14), .unavailable) - XCTAssertEqual(GRPCStatus.Code(rawValue: 15), .dataLoss) - XCTAssertEqual(GRPCStatus.Code(rawValue: 16), .unauthenticated) - - XCTAssertNil(GRPCStatus.Code(rawValue: -1)) - XCTAssertNil(GRPCStatus.Code(rawValue: 17)) - } -} diff --git a/Tests/GRPCTests/GRPCStatusMessageMarshallerTests.swift b/Tests/GRPCTests/GRPCStatusMessageMarshallerTests.swift deleted file mode 100644 index 0f98325a1..000000000 --- a/Tests/GRPCTests/GRPCStatusMessageMarshallerTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 Foundation -import GRPC -import XCTest - -class GRPCStatusMessageMarshallerTests: GRPCTestCase { - func testASCIIMarshallingAndUnmarshalling() { - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("Hello, World!"), "Hello, World!") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("Hello, World!"), "Hello, World!") - } - - func testPercentMarshallingAndUnmarshalling() { - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("%"), "%25") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%25"), "%") - - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("25%"), "25%25") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("25%25"), "25%") - } - - func testUnicodeMarshalling() { - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall("🚀"), "%F0%9F%9A%80") - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall("%F0%9F%9A%80"), "🚀") - - let message = "\t\ntest with whitespace\r\nand Unicode BMP ☺ and non-BMP 😈\t\n" - let marshalled = - "%09%0Atest with whitespace%0D%0Aand Unicode BMP %E2%98%BA and non-BMP %F0%9F%98%88%09%0A" - XCTAssertEqual(GRPCStatusMessageMarshaller.marshall(message), marshalled) - XCTAssertEqual(GRPCStatusMessageMarshaller.unmarshall(marshalled), message) - } -} diff --git a/Tests/GRPCTests/GRPCStatusTests.swift b/Tests/GRPCTests/GRPCStatusTests.swift deleted file mode 100644 index 8dde8ed39..000000000 --- a/Tests/GRPCTests/GRPCStatusTests.swift +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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 XCTest - -@testable import GRPC - -class GRPCStatusTests: GRPCTestCase { - func testStatusDescriptionWithoutMessage() { - XCTAssertEqual( - "ok (0)", - String(describing: GRPCStatus(code: .ok, message: nil)) - ) - - XCTAssertEqual( - "aborted (10)", - String(describing: GRPCStatus(code: .aborted, message: nil)) - ) - - XCTAssertEqual( - "internal error (13)", - String(describing: GRPCStatus(code: .internalError, message: nil)) - ) - } - - func testStatusDescriptionWithWithMessageWithoutCause() { - XCTAssertEqual( - "ok (0): OK", - String(describing: GRPCStatus(code: .ok, message: "OK")) - ) - - XCTAssertEqual( - "resource exhausted (8): a resource was exhausted", - String(describing: GRPCStatus(code: .resourceExhausted, message: "a resource was exhausted")) - ) - - XCTAssertEqual( - "failed precondition (9): invalid state", - String(describing: GRPCStatus(code: .failedPrecondition, message: "invalid state")) - ) - } - - 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) - - // No message/cause, so uses the nil backing storage. - XCTAssertEqual(status.testingOnly_storageObjectIdentifier, nilStorageID) - - status.message = "no longer using the nil backing storage" - let storageID = status.testingOnly_storageObjectIdentifier - XCTAssertNotEqual(storageID, nilStorageID) - XCTAssertEqual(status.message, "no longer using the nil backing storage") - - // The storage of status should be uniquely ref'd, so setting message to nil should not change - // the backing storage (even if the nil storage could now be used). - status.message = nil - XCTAssertEqual(status.testingOnly_storageObjectIdentifier, storageID) - XCTAssertNil(status.message) - } - - func testCoWSemanticsModifyingCause() { - let nilStorageID = GRPCStatus.ok.testingOnly_storageObjectIdentifier - var status = GRPCStatus(code: .cancelled) - - // No message/cause, so uses the nil backing storage. - XCTAssertEqual(status.testingOnly_storageObjectIdentifier, nilStorageID) - - status.cause = GRPCConnectionPoolError.tooManyWaiters(connectionError: nil) - let storageID = status.testingOnly_storageObjectIdentifier - XCTAssertNotEqual(storageID, nilStorageID) - XCTAssert(status.cause is GRPCConnectionPoolError) - - // The storage of status should be uniquely ref'd, so setting cause to nil should not change - // the backing storage (even if the nil storage could now be used). - status.cause = nil - XCTAssertEqual(status.testingOnly_storageObjectIdentifier, storageID) - XCTAssertNil(status.cause) - } - - func testStatusesWithNoMessageOrCauseShareBackingStorage() { - let validStatusCodes = (0 ... 16) - let statuses: [GRPCStatus] = validStatusCodes.map { code in - // 0...16 are all valid, '!' is fine. - let code = GRPCStatus.Code(rawValue: code)! - return GRPCStatus(code: code) - } - - let storageIDs = Set(statuses.map { $0.testingOnly_storageObjectIdentifier }) - XCTAssertEqual(storageIDs.count, 1) - } -} diff --git a/Tests/GRPCTests/GRPCTestCase.swift b/Tests/GRPCTests/GRPCTestCase.swift deleted file mode 100644 index 3c9747265..000000000 --- a/Tests/GRPCTests/GRPCTestCase.swift +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 GRPC -import Logging -import XCTest - -/// This should be used instead of `XCTestCase`. -class GRPCTestCase: XCTestCase { - /// Unless `GRPC_ALWAYS_LOG` is set, logs will only be printed if a test case fails. - private static let alwaysLog = Bool( - fromTruthLike: ProcessInfo.processInfo.environment["GRPC_ALWAYS_LOG"], - defaultingTo: false - ) - - private static let runTimeSensitiveTests = Bool( - fromTruthLike: ProcessInfo.processInfo.environment["ENABLE_TIMING_TESTS"], - defaultingTo: true - ) - - override func setUp() { - super.setUp() - self.logFactory = CapturingLogHandlerFactory(printWhenCaptured: GRPCTestCase.alwaysLog) - } - - override func tearDown() { - // Only print logs when there's a failure and we're *not* always logging (when we are always - // logging, logs will be printed as they're caught). - if !GRPCTestCase.alwaysLog, self.testRun.map({ $0.totalFailureCount > 0 }) ?? false { - let logs = self.capturedLogs() - self.printCapturedLogs(logs) - } - - super.tearDown() - } - - func runTimeSensitiveTests() -> Bool { - let shouldRun = GRPCTestCase.runTimeSensitiveTests - if !shouldRun { - print("Skipping '\(self.name)' as ENABLE_TIMING_TESTS=false") - } - return shouldRun - } - - private(set) var logFactory: CapturingLogHandlerFactory! - - /// A general-use logger. - var logger: Logger { - return Logger(label: "grpc", factory: self.logFactory.make) - } - - /// A logger for clients to use. - var clientLogger: Logger { - // Label is ignored; we already have a handler. - return Logger(label: "client", factory: self.logFactory.make) - } - - /// A logger for servers to use. - var serverLogger: Logger { - // Label is ignored; we already have a handler. - return Logger(label: "server", factory: self.logFactory.make) - } - - /// The default client call options using `self.clientLogger`. - var callOptionsWithLogger: CallOptions { - return CallOptions(logger: self.clientLogger) - } - - /// Returns all captured logs sorted by date. - private func capturedLogs() -> [CapturedLog] { - assert(self.logFactory != nil, "Missing call to super.setUp()") - - var logs = self.logFactory.clearCapturedLogs() - logs.sort(by: { $0.date < $1.date }) - - return logs - } - - /// Prints all captured logs. - private func printCapturedLogs(_ logs: [CapturedLog]) { - print("Test Case '\(self.name)' logs started") - - // The logs are already sorted by date. - let formatter = CapturedLogFormatter() - for log in logs { - print(formatter.string(for: log)) - } - - print("Test Case '\(self.name)' logs finished") - } -} - -extension Bool { - fileprivate init(fromTruthLike value: String?, defaultingTo defaultValue: Bool) { - switch value?.lowercased() { - case "0", "false", "no": - self = false - case "1", "true", "yes": - self = true - default: - self = defaultValue - } - } -} diff --git a/Tests/GRPCTests/GRPCTimeoutTests.swift b/Tests/GRPCTests/GRPCTimeoutTests.swift deleted file mode 100644 index a6f260f48..000000000 --- a/Tests/GRPCTests/GRPCTimeoutTests.swift +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 Dispatch -import Foundation -import NIOCore -import XCTest - -@testable import GRPC - -class GRPCTimeoutTests: GRPCTestCase { - func testRoundingNegativeTimeout() { - let timeout = GRPCTimeout(rounding: -10, unit: .seconds) - XCTAssertEqual(String(describing: timeout), "0S") - XCTAssertEqual(timeout.nanoseconds, 0) - } - - func testRoundingNanosecondsTimeout() throws { - let timeout = GRPCTimeout(rounding: 123_456_789, unit: .nanoseconds) - XCTAssertEqual(timeout, GRPCTimeout(amount: 123_457, unit: .microseconds)) - - // 123_456_789 (nanoseconds) / 1_000 - // = 123_456.789 - // = 123_457 (microseconds, rounded up) - XCTAssertEqual(String(describing: timeout), "123457u") - - // 123_457 (microseconds) * 1_000 - // = 123_457_000 (nanoseconds) - XCTAssertEqual(timeout.nanoseconds, 123_457_000) - } - - func testRoundingMicrosecondsTimeout() throws { - let timeout = GRPCTimeout(rounding: 123_456_789, unit: .microseconds) - XCTAssertEqual(timeout, GRPCTimeout(amount: 123_457, unit: .milliseconds)) - - // 123_456_789 (microseconds) / 1_000 - // = 123_456.789 - // = 123_457 (milliseconds, rounded up) - XCTAssertEqual(String(describing: timeout), "123457m") - - // 123_457 (milliseconds) * 1_000 * 1_000 - // = 123_457_000_000 (nanoseconds) - XCTAssertEqual(timeout.nanoseconds, 123_457_000_000) - } - - func testRoundingMillisecondsTimeout() throws { - let timeout = GRPCTimeout(rounding: 123_456_789, unit: .milliseconds) - XCTAssertEqual(timeout, GRPCTimeout(amount: 123_457, unit: .seconds)) - - // 123_456_789 (milliseconds) / 1_000 - // = 123_456.789 - // = 123_457 (seconds, rounded up) - XCTAssertEqual(String(describing: timeout), "123457S") - - // 123_457 (milliseconds) * 1_000 * 1_000 * 1_000 - // = 123_457_000_000_000 (nanoseconds) - XCTAssertEqual(timeout.nanoseconds, 123_457_000_000_000) - } - - func testRoundingSecondsTimeout() throws { - let timeout = GRPCTimeout(rounding: 123_456_789, unit: .seconds) - XCTAssertEqual(timeout, GRPCTimeout(amount: 2_057_614, unit: .minutes)) - - // 123_456_789 (seconds) / 60 - // = 2_057_613.15 - // = 2_057_614 (minutes, rounded up) - XCTAssertEqual(String(describing: timeout), "2057614M") - - // 2_057_614 (minutes) * 60 * 1_000 * 1_000 * 1_000 - // = 123_456_840_000_000_000 (nanoseconds) - XCTAssertEqual(timeout.nanoseconds, 123_456_840_000_000_000) - } - - func testRoundingMinutesTimeout() throws { - let timeout = GRPCTimeout(rounding: 123_456_789, unit: .minutes) - XCTAssertEqual(timeout, GRPCTimeout(amount: 2_057_614, unit: .hours)) - - // 123_456_789 (minutes) / 60 - // = 2_057_613.15 - // = 2_057_614 (hours, rounded up) - XCTAssertEqual(String(describing: timeout), "2057614H") - - // 123_457 (minutes) * 60 * 60 * 1_000 * 1_000 * 1_000 - // = 7_407_410_400_000_000_000 (nanoseconds) - XCTAssertEqual(timeout.nanoseconds, 7_407_410_400_000_000_000) - } - - func testRoundingHoursTimeout() throws { - let timeout = GRPCTimeout(rounding: 123_456_789, unit: .hours) - XCTAssertEqual(timeout, GRPCTimeout(amount: 99_999_999, unit: .hours)) - - // Hours are the largest unit of time we have (as per the gRPC spec) so we can't round to a - // different unit. In this case we clamp to the largest value. - XCTAssertEqual(String(describing: timeout), "99999999H") - // Unfortunately the largest value representable by the specification is too long to represent - // in nanoseconds within 64 bits, again the value is clamped. - XCTAssertEqual(timeout.nanoseconds, Int64.max) - } - - func testTimeoutFromDeadline() throws { - let deadline = NIODeadline.uptimeNanoseconds(0) + .seconds(42) - let timeout = GRPCTimeout(deadline: deadline, testingOnlyNow: .uptimeNanoseconds(0)) - XCTAssertEqual(timeout.nanoseconds, 42_000_000_000) - - // Wire encoding may have at most 8 digits, we should automatically coarsen the resolution until - // we're within that limit. - XCTAssertEqual(timeout.wireEncoding, "42000000u") - } - - func testTimeoutFromPastDeadline() throws { - let deadline = NIODeadline.uptimeNanoseconds(100) + .nanoseconds(50) - // testingOnlyNow >= deadline: timeout should be zero. - let timeout = GRPCTimeout(deadline: deadline, testingOnlyNow: .uptimeNanoseconds(200)) - XCTAssertEqual(timeout.nanoseconds, 0) - } - - func testTimeoutFromDistantFuture() throws { - XCTAssertEqual(GRPCTimeout(deadline: .distantFuture), .infinite) - } -} diff --git a/Tests/GRPCTests/GRPCTypeSizeTests.swift b/Tests/GRPCTests/GRPCTypeSizeTests.swift deleted file mode 100644 index 359ea36a9..000000000 --- a/Tests/GRPCTests/GRPCTypeSizeTests.swift +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import GRPC -import XCTest - -/// These test check the size of types which get wrapped in `NIOAny`. If the size of the type is -/// greater than 24 bytes (the size of the value buffer in an existential container) then it will -/// incur an additional heap allocation. -/// -/// This commit message explains the problem and one way to mitigate the issue: -/// https://github.com/apple/swift-nio-http2/commit/4097c3a807a83661f0add383edef29b426e666cb -/// -/// Session 416 of WWDC 2016 also provides a good explanation of existential containers. -class GRPCTypeSizeTests: GRPCTestCase { - let existentialContainerBufferSize = 24 - - private func checkSize(of: T.Type, line: UInt = #line) { - XCTAssertLessThanOrEqual(MemoryLayout.size, self.existentialContainerBufferSize, line: line) - } - - // `GRPCStatus` isn't wrapped in `NIOAny` but is passed around through functions taking a type - // conforming to `Error`, so size is important here too. - func testGRPCStatus() { - self.checkSize(of: GRPCStatus.self) - } - - func testGRPCClientRequestPart() { - self.checkSize(of: _GRPCClientRequestPart.self) - } - - func testGRPCClientResponsePart() { - self.checkSize(of: _GRPCClientResponsePart.self) - } -} diff --git a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift deleted file mode 100644 index ab73141d2..000000000 --- a/Tests/GRPCTests/GRPCWebToHTTP2ServerCodecTests.swift +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import XCTest - -import struct Foundation.Data - -@testable import GRPC - -class GRPCWebToHTTP2ServerCodecTests: GRPCTestCase { - private func writeTrailers(_ trailers: HPACKHeaders, into buffer: inout ByteBuffer) { - buffer.writeInteger(UInt8(0x80)) - try! buffer.writeLengthPrefixed(as: UInt32.self) { - var length = 0 - for (name, value, _) in trailers { - length += $0.writeString("\(name): \(value)\r\n") - } - return length - } - } - - private func receiveHead( - contentType: ContentType, - path: String, - on channel: EmbeddedChannel - ) throws { - let head = HTTPRequestHead( - version: .init(major: 1, minor: 1), - method: .POST, - uri: path, - headers: [GRPCHeaderName.contentType: contentType.canonicalValue] - ) - assertThat(try channel.writeInbound(HTTPServerRequestPart.head(head)), .doesNotThrow()) - let headersPayload = try channel.readInbound(as: HTTP2Frame.FramePayload.self) - assertThat(headersPayload, .some(.headers(.contains(":path", [path])))) - } - - private func receiveBytes( - _ buffer: ByteBuffer, - on channel: EmbeddedChannel, - expectedBytes: [UInt8]? = nil - ) throws { - assertThat(try channel.writeInbound(HTTPServerRequestPart.body(buffer)), .doesNotThrow()) - - if let expectedBytes = expectedBytes { - let dataPayload = try channel.readInbound(as: HTTP2Frame.FramePayload.self) - assertThat(dataPayload, .some(.data(buffer: ByteBuffer(bytes: expectedBytes)))) - } - } - - private func receiveEnd(on channel: EmbeddedChannel) throws { - assertThat(try channel.writeInbound(HTTPServerRequestPart.end(nil)), .doesNotThrow()) - let dataEndPayload = try channel.readInbound(as: HTTP2Frame.FramePayload.self) - assertThat(dataEndPayload, .some(.data(buffer: ByteBuffer(), endStream: true))) - } - - private func sendResponseHeaders(on channel: EmbeddedChannel) throws { - let responseHeaders: HPACKHeaders = [":status": "200"] - let headerPayload: HTTP2Frame.FramePayload = .headers(.init(headers: responseHeaders)) - assertThat(try channel.writeOutbound(headerPayload), .doesNotThrow()) - let responseHead = try channel.readOutbound(as: HTTPServerResponsePart.self) - assertThat(responseHead, .some(.head(status: .ok))) - } - - private func sendTrailersOnlyResponse(on channel: EmbeddedChannel) throws { - let headers: HPACKHeaders = [":status": "200"] - let headerPayload: HTTP2Frame.FramePayload = .headers(.init(headers: headers, endStream: true)) - - assertThat(try channel.writeOutbound(headerPayload), .doesNotThrow()) - let responseHead = try channel.readOutbound(as: HTTPServerResponsePart.self) - assertThat(responseHead, .some(.head(status: .ok))) - let end = try channel.readOutbound(as: HTTPServerResponsePart.self) - assertThat(end, .some(.end())) - } - - private func sendBytes( - _ bytes: [UInt8], - on channel: EmbeddedChannel, - expectedBytes: [UInt8]? = nil - ) throws { - let responseBuffer = ByteBuffer(bytes: bytes) - let dataPayload: HTTP2Frame.FramePayload = .data(.init(data: .byteBuffer(responseBuffer))) - assertThat(try channel.writeOutbound(dataPayload), .doesNotThrow()) - - if let expectedBytes = expectedBytes { - let expectedBuffer = ByteBuffer(bytes: expectedBytes) - assertThat(try channel.readOutbound(), .some(.body(.is(expectedBuffer)))) - } else { - assertThat(try channel.readOutbound(as: HTTPServerResponsePart.self), .doesNotThrow(.none())) - } - } - - private func sendEnd( - status: GRPCStatus.Code, - on channel: EmbeddedChannel, - expectedBytes: ByteBuffer? = nil - ) throws { - let headers: HPACKHeaders = ["grpc-status": "\(status.rawValue)"] - let headersPayload: HTTP2Frame.FramePayload = .headers(.init(headers: headers, endStream: true)) - assertThat(try channel.writeOutbound(headersPayload), .doesNotThrow()) - - if let expectedBytes = expectedBytes { - assertThat(try channel.readOutbound(), .some(.body(.is(expectedBytes)))) - } - - assertThat(try channel.readOutbound(), .some(.end())) - } - - func testWebBinaryHappyPath() throws { - let channel = EmbeddedChannel(handler: GRPCWebToHTTP2ServerCodec(scheme: "http")) - - // Inbound - try self.receiveHead(contentType: .webProtobuf, path: "foo", on: channel) - try self.receiveBytes(ByteBuffer(bytes: [1, 2, 3]), on: channel, expectedBytes: [1, 2, 3]) - try self.receiveEnd(on: channel) - - // Outbound - try self.sendResponseHeaders(on: channel) - try self.sendBytes([1, 2, 3], on: channel, expectedBytes: [1, 2, 3]) - - var buffer = ByteBuffer() - self.writeTrailers(["grpc-status": "0"], into: &buffer) - try self.sendEnd(status: .ok, on: channel, expectedBytes: buffer) - } - - func testWebTextHappyPath() throws { - let channel = EmbeddedChannel(handler: GRPCWebToHTTP2ServerCodec(scheme: "http")) - - // Inbound - try self.receiveHead(contentType: .webTextProtobuf, path: "foo", on: channel) - try self.receiveBytes( - ByteBuffer(bytes: [1, 2, 3]).base64Encoded(), - on: channel, - expectedBytes: [1, 2, 3] - ) - try self.receiveEnd(on: channel) - - // Outbound - try self.sendResponseHeaders(on: channel) - try self.sendBytes([1, 2, 3], on: channel) - - // Build up the expected response, i.e. the response bytes and the trailers, base64 encoded. - var expectedBodyBuffer = ByteBuffer(bytes: [1, 2, 3]) - let status = GRPCStatus.Code.ok - self.writeTrailers(["grpc-status": "\(status.rawValue)"], into: &expectedBodyBuffer) - try self.sendEnd(status: status, on: channel, expectedBytes: expectedBodyBuffer.base64Encoded()) - } - - func testWebTextStatusOnlyResponse() throws { - let channel = EmbeddedChannel(handler: GRPCWebToHTTP2ServerCodec(scheme: "http")) - - try self.receiveHead(contentType: .webTextProtobuf, path: "foo", on: channel) - try self.sendTrailersOnlyResponse(on: channel) - } - - func testWebTextByteByByte() throws { - let channel = EmbeddedChannel(handler: GRPCWebToHTTP2ServerCodec(scheme: "http")) - - try self.receiveHead(contentType: .webTextProtobuf, path: "foo", on: channel) - - let bytes = ByteBuffer(bytes: [1, 2, 3]).base64Encoded() - try self.receiveBytes(bytes.getSlice(at: 0, length: 1)!, on: channel, expectedBytes: nil) - try self.receiveBytes(bytes.getSlice(at: 1, length: 1)!, on: channel, expectedBytes: nil) - try self.receiveBytes(bytes.getSlice(at: 2, length: 1)!, on: channel, expectedBytes: nil) - try self.receiveBytes(bytes.getSlice(at: 3, length: 1)!, on: channel, expectedBytes: [1, 2, 3]) - } - - func testSendAfterEnd() throws { - let channel = EmbeddedChannel(handler: GRPCWebToHTTP2ServerCodec(scheme: "http")) - // Get to a closed state. - try self.receiveHead(contentType: .webTextProtobuf, path: "foo", on: channel) - try self.sendTrailersOnlyResponse(on: channel) - - let headersPayload: HTTP2Frame.FramePayload = .headers(.init(headers: [:])) - assertThat(try channel.write(headersPayload).wait(), .throws()) - - let dataPayload: HTTP2Frame.FramePayload = .data(.init(data: .byteBuffer(.init()))) - assertThat(try channel.write(dataPayload).wait(), .throws()) - } -} - -extension ByteBuffer { - fileprivate func base64Encoded() -> ByteBuffer { - let data = self.getData(at: self.readerIndex, length: self.readableBytes)! - return ByteBuffer(string: data.base64EncodedString()) - } -} diff --git a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift b/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift deleted file mode 100644 index f35163888..000000000 --- a/Tests/GRPCTests/GRPCWebToHTTP2StateMachineTests.swift +++ /dev/null @@ -1,693 +0,0 @@ -/* - * 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 NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import XCTest - -@testable import GRPC - -final class GRPCWebToHTTP2StateMachineTests: GRPCTestCase { - fileprivate typealias StateMachine = GRPCWebToHTTP2ServerCodec.StateMachine - - private let allocator = ByteBufferAllocator() - - private func makeStateMachine(scheme: String = "http") -> StateMachine { - return StateMachine(scheme: scheme) - } - - private func makeRequestHead( - version: HTTPVersion = .http1_1, - method: HTTPMethod = .POST, - uri: String, - headers: HTTPHeaders = [:] - ) -> HTTPServerRequestPart { - return .head(.init(version: version, method: method, uri: uri, headers: headers)) - } - - // MARK: - grpc-web - - func test_gRPCWeb_requestHeaders() { - var state = self.makeStateMachine(scheme: "http") - let head = self.makeRequestHead(method: .POST, uri: "foo", headers: ["host": "localhost"]) - - let action = state.processInbound(serverRequestPart: head, allocator: self.allocator) - action.assertRead { payload in - payload.assertHeaders { payload in - XCTAssertFalse(payload.endStream) - XCTAssertEqual(payload.headers[canonicalForm: ":path"], ["foo"]) - XCTAssertEqual(payload.headers[canonicalForm: ":method"], ["POST"]) - XCTAssertEqual(payload.headers[canonicalForm: ":scheme"], ["http"]) - XCTAssertEqual(payload.headers[canonicalForm: ":authority"], ["localhost"]) - } - } - } - - func test_gRPCWeb_requestBody() { - var state = self.makeStateMachine() - let head = self.makeRequestHead( - uri: "foo", - headers: ["content-type": "application/grpc-web"] - ) - - state.processInbound(serverRequestPart: head, allocator: self.allocator).assertRead { - $0.assertHeaders() - } - - let b1 = ByteBuffer(string: "hello") - for _ in 0 ..< 5 { - state.processInbound(serverRequestPart: .body(b1), allocator: self.allocator).assertRead { - $0.assertData { - XCTAssertFalse($0.endStream) - $0.data.assertByteBuffer { buffer in - var buffer = buffer - XCTAssertEqual(buffer.readString(length: buffer.readableBytes), "hello") - } - } - } - } - - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead { - $0.assertEmptyDataWithEndStream() - } - } - - private func checkResponseHeaders( - from state: StateMachine, - expectConnectionCloseHeader: Bool, - line: UInt = #line - ) { - var state = state - state.processOutbound( - framePayload: .headers(.init(headers: [":status": "200"])), - promise: nil, - allocator: self.allocator - ).assertWrite { write in - write.part.assertHead { - XCTAssertEqual($0.status, .ok, line: line) - XCTAssertFalse($0.headers.contains(name: ":status"), line: line) - - if expectConnectionCloseHeader { - XCTAssertEqual($0.headers[canonicalForm: "connection"], ["close"], line: line) - } else { - XCTAssertFalse($0.headers.contains(name: "connection"), line: line) - } - } - XCTAssertNil(write.additionalPart, line: line) - XCTAssertFalse(write.closeChannel, line: line) - } - } - - func test_gRPCWeb_responseHeaders() { - for connectionClose in [true, false] { - let headers: HTTPHeaders = connectionClose ? ["connection": "close"] : [:] - let requestHead = self.makeRequestHead(uri: "/echo", headers: headers) - - var state = self.makeStateMachine() - state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead() - self.checkResponseHeaders(from: state, expectConnectionCloseHeader: connectionClose) - - // Do it again with the request stream closed. - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead() - self.checkResponseHeaders(from: state, expectConnectionCloseHeader: connectionClose) - } - } - - private func checkTrailersOnlyResponse( - from state: StateMachine, - expectConnectionCloseHeader: Bool, - line: UInt = #line - ) { - var state = state - - state.processOutbound( - framePayload: .headers(.init(headers: [":status": "415"], endStream: true)), - promise: nil, - allocator: self.allocator - ).assertWrite { write in - write.part.assertHead { - XCTAssertEqual($0.status, .unsupportedMediaType, line: line) - XCTAssertFalse($0.headers.contains(name: ":status"), line: line) - - if expectConnectionCloseHeader { - XCTAssertEqual($0.headers[canonicalForm: "connection"], ["close"], line: line) - } else { - XCTAssertFalse($0.headers.contains(name: "connection"), line: line) - } - } - - // Should also send end. - write.additionalPart.assertSome { $0.assertEnd() } - XCTAssertEqual(write.closeChannel, expectConnectionCloseHeader, line: line) - } - } - - func test_gRPCWeb_responseTrailersOnly() { - for connectionClose in [true, false] { - let headers: HTTPHeaders = connectionClose ? ["connection": "close"] : [:] - let requestHead = self.makeRequestHead(uri: "/echo", headers: headers) - - var state = self.makeStateMachine() - state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead() - self.checkTrailersOnlyResponse(from: state, expectConnectionCloseHeader: connectionClose) - - // Do it again with the request stream closed. - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead() - self.checkTrailersOnlyResponse(from: state, expectConnectionCloseHeader: connectionClose) - } - } - - private func checkGRPCWebResponseData(from state: StateMachine, line: UInt = #line) { - var state = state - - for i in 0 ..< 10 { - let buffer = ByteBuffer(string: "foo-\(i)") - state.processOutbound( - framePayload: .data(.init(data: .byteBuffer(buffer))), - promise: nil, - allocator: self.allocator - ).assertWrite { write in - write.part.assertBody { - XCTAssertEqual($0, buffer, line: line) - } - XCTAssertNil(write.additionalPart, line: line) - XCTAssertFalse(write.closeChannel, line: line) - } - } - } - - func test_gRPCWeb_responseData() { - var state = self.makeStateMachine() - let requestHead = self.makeRequestHead( - uri: "/echo", - headers: ["content-type": "application/grpc-web"] - ) - state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead() - state.processOutbound( - framePayload: .headers(.init(headers: [":status": "200"])), - promise: nil, - allocator: self.allocator - ).assertWrite() - - // Request stream is open. - self.checkGRPCWebResponseData(from: state) - - // Close request stream and test again. - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead() - self.checkGRPCWebResponseData(from: state) - } - - private func checkGRPCWebResponseTrailers( - from state: StateMachine, - expectChannelClose: Bool, - line: UInt = #line - ) { - var state = state - - state.processOutbound( - framePayload: .headers(.init(headers: ["grpc-status": "0"], endStream: true)), - promise: nil, - allocator: self.allocator - ).assertWrite { write in - write.part.assertBody { buffer in - var buffer = buffer - let trailers = buffer.readLengthPrefixedMessage().map { String(buffer: $0) } - XCTAssertEqual(trailers, "grpc-status: 0\r\n") - } - XCTAssertEqual(write.closeChannel, expectChannelClose) - } - } - - func test_gRPCWeb_responseTrailers() { - for connectionClose in [true, false] { - let headers: HTTPHeaders = connectionClose ? ["connection": "close"] : [:] - let requestHead = self.makeRequestHead(uri: "/echo", headers: headers) - - var state = self.makeStateMachine() - state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead() - state.processOutbound( - framePayload: .headers(.init(headers: [":status": "200"])), - promise: nil, - allocator: self.allocator - ).assertWrite() - - // Request stream is open. - self.checkGRPCWebResponseTrailers(from: state, expectChannelClose: connectionClose) - - // Check again with request stream closed. - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead() - self.checkGRPCWebResponseTrailers(from: state, expectChannelClose: connectionClose) - } - } - - // MARK: - grpc-web-text - - func test_gRPCWebText_requestBody() { - var state = self.makeStateMachine() - let head = self.makeRequestHead( - uri: "foo", - headers: ["content-type": "application/grpc-web-text"] - ) - - state.processInbound(serverRequestPart: head, allocator: self.allocator).assertRead { - $0.assertHeaders() - } - - let expected = ["hel", "lo"] - let buffers = [ByteBuffer(string: "aGVsb"), ByteBuffer(string: "G8=")] - - for (buffer, expected) in zip(buffers, expected) { - state.processInbound(serverRequestPart: .body(buffer), allocator: self.allocator).assertRead { - $0.assertData { - XCTAssertFalse($0.endStream) - $0.data.assertByteBuffer { buffer in - var buffer = buffer - XCTAssertEqual(buffer.readString(length: buffer.readableBytes), expected) - } - } - } - } - - // If there's not enough to decode, there's nothing to do. - let buffer = ByteBuffer(string: "a") - state.processInbound(serverRequestPart: .body(buffer), allocator: self.allocator).assertNone() - - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead { - $0.assertEmptyDataWithEndStream() - } - } - - private func checkResponseDataAndTrailersForGRPCWebText( - from state: StateMachine, - line: UInt = #line - ) { - var state = state - - state.processOutbound( - framePayload: .headers(.init(headers: [":status": "200"])), - promise: nil, - allocator: self.allocator - ).assertWrite() - - // Write some bytes. - for text in ["hello", ", world!"] { - let buffer = ByteBuffer(string: text) - state.processOutbound( - framePayload: .data(.init(data: .byteBuffer(buffer))), - promise: nil, - allocator: self.allocator - ).assertCompletePromise { error in - XCTAssertNil(error) - } - } - - state.processOutbound( - framePayload: .headers(.init(headers: ["grpc-status": "0"], endStream: true)), - promise: nil, - allocator: self.allocator - ).assertWrite { write in - // The response is encoded by: - // - accumulating the bytes of request messages (these would normally be gRPC length prefixed - // messages) - // - appending a 'trailers' byte (0x80) - // - appending the UInt32 length of the trailers when encoded as HTTP/1 header lines - // - the encoded headers - write.part.assertBody { buffer in - var buffer = buffer - let base64Encoded = buffer.readString(length: buffer.readableBytes)! - XCTAssertEqual(base64Encoded, "aGVsbG8sIHdvcmxkIYAAAAAQZ3JwYy1zdGF0dXM6IDANCg==") - - let data = Data(base64Encoded: base64Encoded)! - buffer.writeData(data) - - XCTAssertEqual(buffer.readString(length: 13), "hello, world!") - XCTAssertEqual(buffer.readInteger(), UInt8(0x80)) - XCTAssertEqual(buffer.readInteger(), UInt32(16)) - XCTAssertEqual(buffer.readString(length: 16), "grpc-status: 0\r\n") - XCTAssertEqual(buffer.readableBytes, 0) - } - - // There should be an end now. - write.additionalPart.assertSome { $0.assertEnd() } - - XCTAssertFalse(write.closeChannel) - } - } - - func test_gRPCWebText_responseDataAndTrailers() { - var state = self.makeStateMachine() - let requestHead = self.makeRequestHead( - uri: "/echo", - headers: ["content-type": "application/grpc-web-text"] - ) - state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead() - - // Request stream is still open. - self.checkResponseDataAndTrailersForGRPCWebText(from: state) - - // Check again with request stream closed. - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead() - self.checkResponseDataAndTrailersForGRPCWebText(from: state) - } - - // MARK: - General - - func test_requestPartsAfterServerClosed() { - var state = self.makeStateMachine() - let requestHead = self.makeRequestHead(uri: "/echo") - state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead() - - // Close the response stream. - state.processOutbound( - framePayload: .headers(.init(headers: [":status": "415"], endStream: true)), - promise: nil, - allocator: self.allocator - ).assertWrite() - - state.processInbound( - serverRequestPart: .body(ByteBuffer(string: "hello world")), - allocator: self.allocator - ).assertRead { - $0.assertData { - XCTAssertFalse($0.endStream) - $0.data.assertByteBuffer { buffer in - XCTAssertTrue(buffer.readableBytesView.elementsEqual("hello world".utf8)) - } - } - } - state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator).assertRead { - $0.assertEmptyDataWithEndStream() - } - } - - func test_responsePartsAfterServerClosed() { - var state = self.makeStateMachine() - let requestHead = self.makeRequestHead(uri: "/echo") - state.processInbound(serverRequestPart: requestHead, allocator: self.allocator).assertRead() - - // Close the response stream. - state.processOutbound( - framePayload: .headers(.init(headers: [":status": "415"], endStream: true)), - promise: nil, - allocator: self.allocator - ).assertWrite() - - // More writes should be told to fail their promise. - state.processOutbound( - framePayload: .headers(.init(headers: .init())), - promise: nil, - allocator: self.allocator - ).assertCompletePromise { error in - XCTAssertNotNil(error) - } - - state.processOutbound( - framePayload: .data(.init(data: .byteBuffer(.init()))), - promise: nil, - allocator: self.allocator - ).assertCompletePromise { error in - XCTAssertNotNil(error) - } - } - - func test_handleMultipleRequests() { - func sendRequestHead( - _ state: inout StateMachine, - contentType: ContentType - ) - -> StateMachine - .Action - { - let requestHead = self.makeRequestHead( - uri: "/echo", - headers: ["content-type": contentType.canonicalValue] - ) - return state.processInbound(serverRequestPart: requestHead, allocator: self.allocator) - } - - func sendRequestBody(_ state: inout StateMachine, buffer: ByteBuffer) -> StateMachine.Action { - return state.processInbound(serverRequestPart: .body(buffer), allocator: self.allocator) - } - - func sendRequestEnd(_ state: inout StateMachine) -> StateMachine.Action { - return state.processInbound(serverRequestPart: .end(nil), allocator: self.allocator) - } - - func sendResponseHeaders( - _ state: inout StateMachine, - headers: HPACKHeaders, - endStream: Bool = false - ) -> StateMachine.Action { - return state.processOutbound( - framePayload: .headers(.init(headers: headers, endStream: endStream)), - promise: nil, - allocator: self.allocator - ) - } - - func sendResponseData( - _ state: inout StateMachine, - buffer: ByteBuffer - ) -> StateMachine.Action { - return state.processOutbound( - framePayload: .data(.init(data: .byteBuffer(buffer))), - promise: nil, - allocator: self.allocator - ) - } - - var state = self.makeStateMachine() - - // gRPC-Web, all request parts then all response parts. - sendRequestHead(&state, contentType: .webProtobuf).assertRead() - sendRequestBody(&state, buffer: .init(string: "hello")).assertRead() - sendRequestEnd(&state).assertRead() - sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite() - sendResponseData(&state, buffer: .init(string: "bye")).assertWrite() - sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite() - - // gRPC-Web text, all requests then all response parts. - sendRequestHead(&state, contentType: .webTextProtobuf).assertRead() - sendRequestBody(&state, buffer: .init(string: "hello")).assertRead() - sendRequestEnd(&state).assertRead() - sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite() - // nothing; buffered and sent with end. - sendResponseData(&state, buffer: .init(string: "bye")).assertCompletePromise() - sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite() - - // gRPC-Web, interleaving - sendRequestHead(&state, contentType: .webProtobuf).assertRead() - sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite() - sendRequestBody(&state, buffer: .init(string: "hello")).assertRead() - sendResponseData(&state, buffer: .init(string: "bye")).assertWrite() - sendRequestEnd(&state).assertRead() - sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite() - - // gRPC-Web text, interleaving - sendRequestHead(&state, contentType: .webTextProtobuf).assertRead() - sendResponseHeaders(&state, headers: [":status": "200"]).assertWrite() - sendRequestBody(&state, buffer: .init(string: "hello")).assertRead() - sendResponseData(&state, buffer: .init(string: "bye")).assertCompletePromise() - sendRequestEnd(&state).assertRead() - sendResponseHeaders(&state, headers: ["grpc-status": "0"], endStream: true).assertWrite() - - // gRPC-Web, server closes immediately. - sendRequestHead(&state, contentType: .webProtobuf).assertRead() - sendResponseHeaders(&state, headers: [":status": "415"], endStream: true).assertWrite() - sendRequestBody(&state, buffer: .init(string: "hello")).assertRead() - sendRequestEnd(&state).assertRead() - - // gRPC-Web text, server closes immediately. - sendRequestHead(&state, contentType: .webTextProtobuf).assertRead() - sendResponseHeaders(&state, headers: [":status": "415"], endStream: true).assertWrite() - sendRequestBody(&state, buffer: .init(string: "hello")).assertRead() - sendRequestEnd(&state).assertRead() - } -} - -// MARK: - Assertions - -extension GRPCWebToHTTP2ServerCodec.StateMachine.Action { - func assertRead( - file: StaticString = #filePath, - line: UInt = #line, - verify: (HTTP2Frame.FramePayload) -> Void = { _ in } - ) { - if case let .fireChannelRead(payload) = self { - verify(payload) - } else { - XCTFail("Expected '.fireChannelRead' but got '\(self)'", file: file, line: line) - } - } - - func assertWrite( - file: StaticString = #filePath, - line: UInt = #line, - verify: (Write) -> Void = { _ in } - ) { - if case let .write(write) = self { - verify(write) - } else { - XCTFail("Expected '.write' but got '\(self)'", file: file, line: line) - } - } - - func assertCompletePromise( - file: StaticString = #filePath, - line: UInt = #line, - verify: (Error?) -> Void = { _ in } - ) { - if case let .completePromise(_, result) = self { - do { - try result.get() - verify(nil) - } catch { - verify(error) - } - } else { - XCTFail("Expected '.completePromise' but got '\(self)'", file: file, line: line) - } - } - - func assertNone( - file: StaticString = #filePath, - line: UInt = #line - ) { - if case .none = self { - () - } else { - XCTFail("Expected '.none' but got '\(self)'", file: file, line: line) - } - } -} - -extension HTTP2Frame.FramePayload { - func assertHeaders( - file: StaticString = #filePath, - line: UInt = #line, - verify: (Headers) -> Void = { _ in } - ) { - if case let .headers(headers) = self { - verify(headers) - } else { - XCTFail("Expected '.headers' but got '\(self)'", file: file, line: line) - } - } - - func assertData( - file: StaticString = #filePath, - line: UInt = #line, - verify: (Data) -> Void = { _ in } - ) { - if case let .data(data) = self { - verify(data) - } else { - XCTFail("Expected '.data' but got '\(self)'", file: file, line: line) - } - } - - func assertEmptyDataWithEndStream( - file: StaticString = #filePath, - line: UInt = #line - ) { - self.assertData(file: file, line: line) { - XCTAssertTrue($0.endStream) - $0.data.assertByteBuffer { buffer in - XCTAssertEqual(buffer.readableBytes, 0) - } - } - } -} - -extension HTTPServerResponsePart { - func assertHead( - file: StaticString = #filePath, - line: UInt = #line, - verify: (HTTPResponseHead) -> Void = { _ in } - ) { - if case let .head(head) = self { - verify(head) - } else { - XCTFail("Expected '.head' but got '\(self)'", file: file, line: line) - } - } - - func assertBody( - file: StaticString = #filePath, - line: UInt = #line, - verify: (ByteBuffer) -> Void = { _ in } - ) { - if case let .body(.byteBuffer(buffer)) = self { - verify(buffer) - } else { - XCTFail("Expected '.body(.byteBuffer)' but got '\(self)'", file: file, line: line) - } - } - - func assertEnd( - file: StaticString = #filePath, - line: UInt = #line, - verify: (HTTPHeaders?) -> Void = { _ in } - ) { - if case let .end(trailers) = self { - verify(trailers) - } else { - XCTFail("Expected '.end' but got '\(self)'", file: file, line: line) - } - } -} - -extension IOData { - func assertByteBuffer( - file: StaticString = #filePath, - line: UInt = #line, - verify: (ByteBuffer) -> Void = { _ in } - ) { - if case let .byteBuffer(buffer) = self { - verify(buffer) - } else { - XCTFail("Expected '.byteBuffer' but got '\(self)'", file: file, line: line) - } - } -} - -extension Optional { - func assertSome( - file: StaticString = #filePath, - line: UInt = #line, - verify: (Wrapped) -> Void = { _ in } - ) { - switch self { - case let .some(wrapped): - verify(wrapped) - case .none: - XCTFail("Expected '.some' but got 'nil'", file: file, line: line) - } - } -} - -extension ByteBuffer { - mutating func readLengthPrefixedMessage() -> ByteBuffer? { - // Read off and ignore the compression byte. - if self.readInteger(as: UInt8.self) == nil { - return nil - } - - return self.readLengthPrefixedSlice(as: UInt32.self) - } -} diff --git a/Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift b/Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift deleted file mode 100644 index 50a5bd637..000000000 --- a/Tests/GRPCTests/HTTP2MaxConcurrentStreamsTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import NIOCore -import NIOHTTP2 -import NIOPosix -import XCTest - -@testable import GRPC - -class HTTP2MaxConcurrentStreamsTests: GRPCTestCase { - enum Constants { - static let testTimeout: TimeInterval = 10 - - static let defaultMaxNumberOfConcurrentStreams = - nioDefaultSettings.first(where: { $0.parameter == .maxConcurrentStreams })!.value - - static let testNumberOfConcurrentStreams: Int = defaultMaxNumberOfConcurrentStreams + 20 - } - - func testHTTP2MaxConcurrentStreamsSetting() { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - - let server = try! Server.insecure(group: eventLoopGroup) - .withLogger(self.serverLogger) - .withHTTPMaxConcurrentStreams(Constants.testNumberOfConcurrentStreams) - .withServiceProviders([EchoProvider()]) - .bind(host: "localhost", port: 0) - .wait() - - defer { XCTAssertNoThrow(try server.initiateGracefulShutdown().wait()) } - - let clientConnection = ClientConnection.insecure(group: eventLoopGroup) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: server.channel.localAddress!.port!) - - defer { XCTAssertNoThrow(try clientConnection.close().wait()) } - - let echoClient = Echo_EchoNIOClient( - channel: clientConnection, - defaultCallOptions: CallOptions(logger: self.clientLogger) - ) - - var clientStreamingCalls = - (0 ..< Constants.testNumberOfConcurrentStreams) - .map { _ in echoClient.collect() } - - let allMessagesSentExpectation = self.expectation(description: "all messages sent") - - let sendMessageFutures = - clientStreamingCalls - .map { $0.sendMessage(.with { $0.text = "Hi!" }) } - - EventLoopFuture - .whenAllSucceed(sendMessageFutures, on: eventLoopGroup.next()) - .assertSuccess(fulfill: allMessagesSentExpectation) - - self.wait(for: [allMessagesSentExpectation], timeout: Constants.testTimeout) - - let lastCall = clientStreamingCalls.popLast()! - - let lastCallCompletedExpectation = self.expectation(description: "last call completed") - _ = lastCall.sendEnd() - - lastCall.status.assertSuccess(fulfill: lastCallCompletedExpectation) - - self.wait(for: [lastCallCompletedExpectation], timeout: Constants.testTimeout) - - let allCallsCompletedExpectation = self.expectation(description: "all calls completed") - let endFutures = clientStreamingCalls.map { $0.sendEnd() } - - EventLoopFuture - .whenAllSucceed(endFutures, on: eventLoopGroup.next()) - .assertSuccess(fulfill: allCallsCompletedExpectation) - - self.wait(for: [allCallsCompletedExpectation], timeout: Constants.testTimeout) - } -} diff --git a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift b/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift deleted file mode 100644 index a0d7a8b32..000000000 --- a/Tests/GRPCTests/HTTP2ToRawGRPCStateMachineTests.swift +++ /dev/null @@ -1,795 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP2 -import NIOPosix -import XCTest - -@testable import GRPC - -class HTTP2ToRawGRPCStateMachineTests: GRPCTestCase { - typealias StateMachine = HTTP2ToRawGRPCStateMachine - typealias State = StateMachine.State - - // An event loop gets passed to any service handler that's created, we don't actually use it here. - private var eventLoop: EventLoop { - return EmbeddedEventLoop() - } - - /// An allocator, just here for convenience. - private let allocator = ByteBufferAllocator() - - private func makeHeaders( - path: String = "/echo.Echo/Get", - contentType: String?, - encoding: String? = nil, - acceptEncoding: [String]? = nil - ) -> HPACKHeaders { - var headers = HPACKHeaders() - headers.add(name: ":path", value: path) - if let contentType = contentType { - headers.add(name: GRPCHeaderName.contentType, value: contentType) - } - if let encoding = encoding { - headers.add(name: GRPCHeaderName.encoding, value: encoding) - } - if let acceptEncoding = acceptEncoding { - headers.add(name: GRPCHeaderName.acceptEncoding, value: acceptEncoding.joined(separator: ",")) - } - return headers - } - - private func makeHeaders( - path: String = "/echo.Echo/Get", - contentType: ContentType? = .protobuf, - encoding: CompressionAlgorithm? = nil, - acceptEncoding: [CompressionAlgorithm]? = nil - ) -> HPACKHeaders { - return self.makeHeaders( - path: path, - contentType: contentType?.canonicalValue, - encoding: encoding?.name, - acceptEncoding: acceptEncoding?.map { $0.name } - ) - } - - /// A minimum set of viable request headers for the service providers we register by default. - private var viableHeaders: HPACKHeaders { - return self.makeHeaders( - path: "/echo.Echo/Get", - contentType: "application/grpc" - ) - } - - /// Just the echo service. - private var services: [Substring: CallHandlerProvider] { - let provider = EchoProvider() - return [provider.serviceName: provider] - } - - private enum DesiredState { - case requestOpenResponseIdle(pipelineConfigured: Bool) - case requestOpenResponseOpen - case requestClosedResponseIdle(pipelineConfigured: Bool) - case requestClosedResponseOpen - } - - /// Makes a state machine in the desired state. - private func makeStateMachine( - services: [Substring: CallHandlerProvider]? = nil, - encoding: ServerMessageEncoding = .disabled, - state: DesiredState = .requestOpenResponseIdle(pipelineConfigured: true) - ) -> StateMachine { - var machine = StateMachine() - - let receiveHeadersAction = machine.receive( - headers: self.viableHeaders, - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: services ?? self.services, - encoding: encoding, - normalizeHeaders: true - ) - - assertThat(receiveHeadersAction, .is(.configure())) - - switch state { - case .requestOpenResponseIdle(pipelineConfigured: false): - () - - case .requestOpenResponseIdle(pipelineConfigured: true): - let configuredAction = machine.pipelineConfigured() - assertThat(configuredAction, .is(.forwardHeaders())) - - case .requestOpenResponseOpen: - let configuredAction = machine.pipelineConfigured() - assertThat(configuredAction, .is(.forwardHeaders())) - - let sendHeadersAction = machine.send(headers: [:]) - assertThat(sendHeadersAction, .is(.success())) - - case .requestClosedResponseIdle(pipelineConfigured: false): - var emptyBuffer = ByteBuffer() - let receiveEnd = machine.receive(buffer: &emptyBuffer, endStream: true) - assertThat(receiveEnd, .is(.nothing)) - - case .requestClosedResponseIdle(pipelineConfigured: true): - let configuredAction = machine.pipelineConfigured() - assertThat(configuredAction, .is(.forwardHeaders())) - - var emptyBuffer = ByteBuffer() - let receiveEnd = machine.receive(buffer: &emptyBuffer, endStream: true) - assertThat(receiveEnd, .is(.tryReading)) - - case .requestClosedResponseOpen: - let configuredAction = machine.pipelineConfigured() - assertThat(configuredAction, .is(.forwardHeaders())) - - var emptyBuffer = ByteBuffer() - let receiveEndAction = machine.receive(buffer: &emptyBuffer, endStream: true) - assertThat(receiveEndAction, .is(.tryReading)) - let readAction = machine.readNextRequest() - assertThat(readAction, .is(.forwardEnd())) - - let sendHeadersAction = machine.send(headers: [:]) - assertThat(sendHeadersAction, .is(.success())) - } - - return machine - } - - /// Makes a gRPC framed message; i.e. a compression flag (UInt8), the message length (UIn32), the - /// message bytes (UInt8 ⨉ message length). - private func makeLengthPrefixedBytes(_ count: Int, setCompressFlag: Bool = false) -> ByteBuffer { - var buffer = ByteBuffer() - buffer.reserveCapacity(count + 5) - buffer.writeInteger(UInt8(setCompressFlag ? 1 : 0)) - buffer.writeInteger(UInt32(count)) - buffer.writeRepeatingByte(0, count: count) - return buffer - } - - // MARK: Receive Headers Tests - - func testReceiveValidHeaders() { - var machine = StateMachine() - let action = machine.receive( - headers: self.viableHeaders, - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - assertThat(action, .is(.configure())) - } - - func testReceiveInvalidContentType() { - var machine = StateMachine() - let action = machine.receive( - headers: self.makeHeaders(contentType: "application/json"), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - assertThat(action, .is(.rejectRPC(.contains(":status", ["415"])))) - } - - func testReceiveValidHeadersForUnknownService() { - var machine = StateMachine() - let action = machine.receive( - headers: self.makeHeaders(path: "/foo.Foo/Get"), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - assertThat(action, .is(.rejectRPC(.trailersOnly(code: .unimplemented)))) - } - - func testReceiveValidHeadersForUnknownMethod() { - var machine = StateMachine() - let action = machine.receive( - headers: self.makeHeaders(path: "/echo.Echo/Foo"), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - assertThat(action, .is(.rejectRPC(.trailersOnly(code: .unimplemented)))) - } - - func testReceiveValidHeadersForInvalidPath() { - var machine = StateMachine() - let action = machine.receive( - headers: self.makeHeaders(path: "nope"), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - assertThat(action, .is(.rejectRPC(.trailersOnly(code: .unimplemented)))) - } - - func testReceiveHeadersWithUnsupportedEncodingWhenCompressionIsDisabled() { - var machine = StateMachine() - let action = machine.receive( - headers: self.makeHeaders(encoding: .gzip), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - assertThat(action, .is(.rejectRPC(.trailersOnly(code: .unimplemented)))) - } - - func testReceiveHeadersWithMultipleEncodings() { - var machine = StateMachine() - // We can't have multiple encodings. - let action = machine.receive( - headers: self.makeHeaders(contentType: "application/grpc", encoding: "gzip,identity"), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - assertThat(action, .is(.rejectRPC(.trailersOnly(code: .invalidArgument)))) - } - - func testReceiveHeadersWithUnsupportedEncodingWhenCompressionIsEnabled() { - var machine = StateMachine() - - let action = machine.receive( - headers: self.makeHeaders(contentType: "application/grpc", encoding: "foozip"), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .enabled(.deflate, .identity), - normalizeHeaders: false - ) - - assertThat(action, .is(.rejectRPC(.trailersOnly(code: .unimplemented)))) - assertThat( - action, - .is(.rejectRPC(.contains("grpc-accept-encoding", ["deflate", "identity"]))) - ) - } - - func testReceiveHeadersWithSupportedButNotAdvertisedEncoding() { - var machine = StateMachine() - - // We didn't advertise gzip, but we do support it. - let action = machine.receive( - headers: self.makeHeaders(encoding: .gzip), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .enabled(.deflate, .identity), - normalizeHeaders: false - ) - - // This is expected: however, we also expect 'grpc-accept-encoding' to be in the response - // metadata. Send back headers to test this. - assertThat(action, .is(.configure())) - let sendAction = machine.send(headers: [:]) - assertThat( - sendAction, - .success( - .contains( - "grpc-accept-encoding", - ["deflate", "identity", "gzip"] - ) - ) - ) - } - - func testReceiveHeadersWithIdentityCompressionWhenCompressionIsDisabled() { - var machine = StateMachine() - - // Identity is always supported, even if compression is disabled. - let action = machine.receive( - headers: self.makeHeaders(encoding: .identity), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .disabled, - normalizeHeaders: false - ) - - assertThat(action, .is(.configure())) - } - - func testReceiveHeadersNegotiatesResponseEncoding() { - var machine = StateMachine() - - let action = machine.receive( - headers: self.makeHeaders(acceptEncoding: [.deflate]), - eventLoop: self.eventLoop, - errorDelegate: nil, - remoteAddress: nil, - logger: self.logger, - allocator: ByteBufferAllocator(), - responseWriter: NoOpResponseWriter(), - closeFuture: self.eventLoop.makeSucceededVoidFuture(), - services: self.services, - encoding: .enabled(.gzip, .deflate), - normalizeHeaders: false - ) - - // This is expected, but we need to check the value of 'grpc-encoding' in the response headers. - assertThat(action, .is(.configure())) - let sendAction = machine.send(headers: [:]) - assertThat(sendAction, .success(.contains("grpc-encoding", ["deflate"]))) - } - - // MARK: Receive Data Tests - - func testReceiveDataBeforePipelineIsConfigured() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: false)) - let buffer = self.makeLengthPrefixedBytes(1024) - - // Receive a request. The pipeline isn't configured so no action. - var buffer1 = buffer - let action1 = machine.receive(buffer: &buffer1, endStream: false) - assertThat(action1, .is(.nothing)) - - // Receive another request, still not configured so no action. - var buffer2 = buffer - let action2 = machine.receive(buffer: &buffer2, endStream: false) - assertThat(action2, .is(.nothing)) - - // Configure the pipeline. We'll have headers to forward and messages to read. - let action3 = machine.pipelineConfigured() - assertThat(action3, .is(.forwardHeadersThenRead())) - - // Do the first read. - let action4 = machine.readNextRequest() - assertThat(action4, .is(.forwardMessageThenRead())) - - // Do the second and final read. - let action5 = machine.readNextRequest() - assertThat(action5, .is(.forwardMessage())) - - // Receive an empty buffer with end stream. Since we're configured we'll always try to read - // after receiving. - var emptyBuffer = ByteBuffer() - let action6 = machine.receive(buffer: &emptyBuffer, endStream: true) - assertThat(action6, .is(.tryReading)) - - // There's nothing in the reader to consume, but since we saw end stream we'll have to close. - let action7 = machine.readNextRequest() - assertThat(action7, .is(.forwardEnd())) - } - - func testReceiveDataWhenPipelineIsConfigured() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: true)) - let buffer = self.makeLengthPrefixedBytes(1024) - - // Receive a request. The pipeline is configured, so we should try reading. - var buffer1 = buffer - let action1 = machine.receive(buffer: &buffer1, endStream: false) - assertThat(action1, .is(.tryReading)) - - // Read the message, consuming all bytes. - let action2 = machine.readNextRequest() - assertThat(action2, .is(.forwardMessage())) - - // Receive another request, we'll split buffer into two parts. - var buffer3 = buffer - var buffer2 = buffer3.readSlice(length: 20)! - - // Not enough bytes to form a message, so read won't result in anything. - let action4 = machine.receive(buffer: &buffer2, endStream: false) - assertThat(action4, .is(.tryReading)) - let action5 = machine.readNextRequest() - assertThat(action5, .is(.none())) - - // Now the rest of the message. - let action6 = machine.receive(buffer: &buffer3, endStream: false) - assertThat(action6, .is(.tryReading)) - let action7 = machine.readNextRequest() - assertThat(action7, .is(.forwardMessage())) - - // Receive an empty buffer with end stream. Since we're configured we'll always try to read - // after receiving. - var emptyBuffer = ByteBuffer() - let action8 = machine.receive(buffer: &emptyBuffer, endStream: true) - assertThat(action8, .is(.tryReading)) - - // There's nothing in the reader to consume, but since we saw end stream we'll have to close. - let action9 = machine.readNextRequest() - assertThat(action9, .is(.forwardEnd())) - } - - func testReceiveDataAndEndStreamBeforePipelineIsConfigured() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: false)) - let buffer = self.makeLengthPrefixedBytes(1024) - - // No action: the pipeline isn't configured. - var buffer1 = buffer - let action1 = machine.receive(buffer: &buffer1, endStream: false) - assertThat(action1, .is(.nothing)) - - // Still no action. - var buffer2 = buffer - let action2 = machine.receive(buffer: &buffer2, endStream: true) - assertThat(action2, .is(.nothing)) - - // Configure the pipeline. We have headers to forward and messages to read. - let action3 = machine.pipelineConfigured() - assertThat(action3, .is(.forwardHeadersThenRead())) - - // Read the first message. - let action4 = machine.readNextRequest() - assertThat(action4, .is(.forwardMessageThenRead())) - - // Read the second and final message. - let action5 = machine.readNextRequest() - assertThat(action5, .is(.forwardMessageThenRead())) - let action6 = machine.readNextRequest() - assertThat(action6, .is(.forwardEnd())) - } - - func testReceiveDataAfterPipelineIsConfigured() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: true)) - let buffer = self.makeLengthPrefixedBytes(1024) - - // Pipeline is configured, we should be able to read then forward the message. - var buffer1 = buffer - let action1 = machine.receive(buffer: &buffer1, endStream: false) - assertThat(action1, .is(.tryReading)) - let action2 = machine.readNextRequest() - assertThat(action2, .is(.forwardMessage())) - - // Receive another message with end stream set. - // Still no action. - var buffer2 = buffer - let action3 = machine.receive(buffer: &buffer2, endStream: true) - assertThat(action3, .is(.tryReading)) - let action4 = machine.readNextRequest() - assertThat(action4, .is(.forwardMessageThenRead())) - let action5 = machine.readNextRequest() - assertThat(action5, .is(.forwardEnd())) - } - - func testReceiveDataWhenResponseStreamIsOpen() { - var machine = self.makeStateMachine(state: .requestOpenResponseOpen) - let buffer = self.makeLengthPrefixedBytes(1024) - - // Receive a message. We should read and forward it. - var buffer1 = buffer - let action1 = machine.receive(buffer: &buffer1, endStream: false) - assertThat(action1, .is(.tryReading)) - let action2 = machine.readNextRequest() - assertThat(action2, .is(.forwardMessage())) - - // Receive a message and end stream. We should read it then forward message and end. - var buffer2 = buffer - let action3 = machine.receive(buffer: &buffer2, endStream: true) - assertThat(action3, .is(.tryReading)) - let action4 = machine.readNextRequest() - assertThat(action4, .is(.forwardMessageThenRead())) - let action5 = machine.readNextRequest() - assertThat(action5, .is(.forwardEnd())) - } - - func testReceiveCompressedMessageWhenCompressionIsDisabled() { - var machine = self.makeStateMachine(state: .requestOpenResponseOpen) - var buffer = self.makeLengthPrefixedBytes(1024, setCompressFlag: true) - - let action1 = machine.receive(buffer: &buffer, endStream: false) - assertThat(action1, .is(.tryReading)) - let action2 = machine.readNextRequest() - assertThat(action2, .is(.errorCaught())) - } - - func testReceiveDataWhenClosed() { - var machine = self.makeStateMachine(state: .requestOpenResponseOpen) - // Close while the request stream is still open. - let action1 = machine.send( - status: GRPCStatus(code: .ok, message: "ok"), - trailers: [:] - ) - assertThat(action1, .is(.sendTrailers(.trailers(code: .ok, message: "ok")))) - - // Now receive end of request stream: tear down the handler, we're closed - var emptyBuffer = ByteBuffer() - let action2 = machine.receive(buffer: &emptyBuffer, endStream: true) - assertThat(action2, .is(.finishHandler)) - } - - // MARK: Send Metadata Tests - - func testSendMetadataRequestStreamOpen() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: true)) - - // We tested most of the weird (request encoding, negotiating response encoding etc.) above. - // We'll just validate more 'normal' things here. - let action1 = machine.send(headers: [:]) - assertThat(action1, .is(.success(.contains(":status", ["200"])))) - - let action2 = machine.send(headers: [:]) - assertThat(action2, .is(.failure())) - } - - func testSendMetadataRequestStreamClosed() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: true)) - - var buffer = ByteBuffer() - let action1 = machine.receive(buffer: &buffer, endStream: true) - assertThat(action1, .is(.tryReading)) - let action2 = machine.readNextRequest() - assertThat(action2, .is(.forwardEnd())) - - // Write some headers back. - let action3 = machine.send(headers: [:]) - assertThat(action3, .is(.success(.contains(":status", ["200"])))) - } - - func testSendMetadataWhenOpen() { - var machine = self.makeStateMachine(state: .requestOpenResponseOpen) - - // Response stream is already open. - let action = machine.send(headers: [:]) - assertThat(action, .is(.failure())) - } - - func testSendMetadataNormalizesUserProvidedMetadata() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: true)) - let action = machine.send(headers: ["FOO": "bar"]) - assertThat(action, .success(.contains(caseSensitive: "foo"))) - } - - // MARK: Send Data Tests - - func testSendData() { - for startingState in [DesiredState.requestOpenResponseOpen, .requestClosedResponseOpen] { - var machine = self.makeStateMachine(state: startingState) - let buffer = ByteBuffer(repeating: 0, count: 1024) - - // We should be able to do this multiple times. - for _ in 0 ..< 5 { - let action = machine.send( - buffer: buffer, - compress: false, - promise: nil - ) - assertThat(action, .is(.success())) - } - - // Set the compress flag, we're not setup to compress so the flag will just be ignored, we'll - // write as normal. - let action = machine.send( - buffer: buffer, - compress: true, - promise: nil - ) - assertThat(action, .is(.success())) - } - } - - func testSendDataAfterClose() { - var machine = self.makeStateMachine(state: .requestClosedResponseOpen) - let action1 = machine.send(status: .ok, trailers: [:]) - assertThat(action1, .is(.sendTrailersAndFinish(.contains("grpc-status", ["0"])))) - - // We're already closed, this should fail. - let buffer = ByteBuffer(repeating: 0, count: 1024) - let action2 = machine.send( - buffer: buffer, - compress: false, - promise: nil - ) - assertThat(action2, .is(.failure())) - } - - func testSendDataBeforeMetadata() { - var machine = self.makeStateMachine(state: .requestClosedResponseIdle(pipelineConfigured: true)) - - // Response stream is still idle, so this should fail. - let buffer = ByteBuffer(repeating: 0, count: 1024) - let action2 = machine.send( - buffer: buffer, - compress: false, - promise: nil - ) - assertThat(action2, .is(.failure())) - } - - // MARK: Next Response - - func testNextResponseBeforeMetadata() { - var machine = self.makeStateMachine(state: .requestOpenResponseIdle(pipelineConfigured: true)) - XCTAssertNil(machine.nextResponse()) - } - - func testNextResponseWhenOpen() throws { - for startingState in [DesiredState.requestOpenResponseOpen, .requestClosedResponseOpen] { - var machine = self.makeStateMachine(state: startingState) - - // No response buffered yet. - XCTAssertNil(machine.nextResponse()) - - let buffer = ByteBuffer(repeating: 0, count: 1024) - machine.send(buffer: buffer, compress: false, promise: nil).assertSuccess() - - let (framedBuffer, promise) = try XCTUnwrap(machine.nextResponse()) - XCTAssertNil(promise) // Didn't provide a promise. - framedBuffer.assertSuccess() - - // No more responses. - XCTAssertNil(machine.nextResponse()) - } - } - - func testNextResponseWhenClosed() throws { - var machine = self.makeStateMachine(state: .requestClosedResponseOpen) - let action = machine.send(status: .ok, trailers: [:]) - switch action { - case .sendTrailersAndFinish: - () - default: - XCTFail("Expected 'sendTrailersAndFinish' but got \(action)") - } - - XCTAssertNil(machine.nextResponse()) - } - - // MARK: Send End - - func testSendEndWhenResponseStreamIsIdle() { - for (state, closed) in zip( - [ - DesiredState.requestOpenResponseIdle(pipelineConfigured: true), - DesiredState.requestClosedResponseIdle(pipelineConfigured: true), - ], - [false, true] - ) { - var machine = self.makeStateMachine(state: state) - let action1 = machine.send(status: .ok, trailers: [:]) - // This'll be a trailers-only response. - if closed { - assertThat(action1, .is(.sendTrailersAndFinish(.trailersOnly(code: .ok)))) - } else { - assertThat(action1, .is(.sendTrailers(.trailersOnly(code: .ok)))) - } - - // Already closed. - let action2 = machine.send(status: .ok, trailers: [:]) - assertThat(action2, .is(.failure())) - } - } - - func testSendEndWhenResponseStreamIsOpen() { - for (state, closed) in zip( - [ - DesiredState.requestOpenResponseOpen, - DesiredState.requestClosedResponseOpen, - ], - [false, true] - ) { - var machine = self.makeStateMachine(state: state) - let action = machine.send( - status: GRPCStatus(code: .ok, message: "ok"), - trailers: [:] - ) - if closed { - assertThat(action, .is(.sendTrailersAndFinish(.trailers(code: .ok, message: "ok")))) - } else { - assertThat(action, .is(.sendTrailers(.trailers(code: .ok, message: "ok")))) - } - - // Already closed. - let action2 = machine.send(status: .ok, trailers: [:]) - assertThat(action2, .is(.failure())) - } - } -} - -extension ServerMessageEncoding { - fileprivate static func enabled(_ algorithms: CompressionAlgorithm...) -> ServerMessageEncoding { - return .enabled(.init(enabledAlgorithms: algorithms, decompressionLimit: .absolute(.max))) - } -} - -class NoOpResponseWriter: GRPCServerResponseWriter { - func sendMetadata(_ metadata: HPACKHeaders, flush: Bool, promise: EventLoopPromise?) { - promise?.succeed(()) - } - - func sendMessage( - _ bytes: ByteBuffer, - metadata: MessageMetadata, - promise: EventLoopPromise? - ) { - promise?.succeed(()) - } - - func sendEnd(status: GRPCStatus, trailers: HPACKHeaders, promise: EventLoopPromise?) { - promise?.succeed(()) - } -} - -extension HTTP2ToRawGRPCStateMachine { - fileprivate mutating func readNextRequest() -> HTTP2ToRawGRPCStateMachine.ReadNextMessageAction { - return self.readNextRequest(maxLength: .max) - } -} diff --git a/Tests/GRPCTests/HTTPVersionParserTests.swift b/Tests/GRPCTests/HTTPVersionParserTests.swift deleted file mode 100644 index a282ef08c..000000000 --- a/Tests/GRPCTests/HTTPVersionParserTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import XCTest - -@testable import GRPC - -class HTTPVersionParserTests: GRPCTestCase { - private let preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" - - func testHTTP2ExactlyTheRightBytes() { - let buffer = ByteBuffer(string: self.preface) - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .accepted) - } - - func testHTTP2TheRightBytesAndMore() { - var buffer = ByteBuffer(string: self.preface) - buffer.writeRepeatingByte(42, count: 1024) - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .accepted) - } - - func testHTTP2NoBytes() { - let empty = ByteBuffer() - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(empty), .notEnoughBytes) - } - - func testHTTP2NotEnoughBytes() { - var buffer = ByteBuffer(string: self.preface) - buffer.moveWriterIndex(to: buffer.writerIndex - 1) - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .notEnoughBytes) - } - - func testHTTP2EnoughOfTheWrongBytes() { - let buffer = ByteBuffer(string: String(self.preface.reversed())) - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .rejected) - } - - func testHTTP1RequestLine() { - let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html HTTP/1.1\r\n") - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .accepted) - } - - func testHTTP1RequestLineAndMore() { - let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html HTTP/1.1\r\nMore") - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .accepted) - } - - func testHTTP1RequestLineWithoutCRLF() { - let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html HTTP/1.1") - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .notEnoughBytes) - } - - func testHTTP1NoBytes() { - let empty = ByteBuffer() - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(empty), .notEnoughBytes) - } - - func testHTTP1IncompleteRequestLine() { - let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html") - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .notEnoughBytes) - } - - func testHTTP1MalformedVersion() { - let buffer = ByteBuffer(staticString: "GET https://grpc.io/index.html ptth/1.1\r\n") - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .rejected) - } - - func testTooManyIncorrectBytes() { - let buffer = ByteBuffer(repeating: UInt8(ascii: "\r"), count: 2048) - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP2ConnectionPreface(buffer), .rejected) - XCTAssertEqual(HTTPVersionParser.prefixedWithHTTP1RequestLine(buffer), .rejected) - } -} diff --git a/Tests/GRPCTests/HeaderNormalizationTests.swift b/Tests/GRPCTests/HeaderNormalizationTests.swift deleted file mode 100644 index cf7e079e5..000000000 --- a/Tests/GRPCTests/HeaderNormalizationTests.swift +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOPosix -import XCTest - -@testable import GRPC - -class EchoMetadataValidator: Echo_EchoProvider { - let interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil - - private func assertCustomMetadataIsLowercased( - _ headers: HPACKHeaders, - line: UInt = #line - ) { - // Header lookup is case-insensitive so we need to pull out the values we know the client sent - // as custom-metadata and then compare a new set of headers. - let customMetadata = HPACKHeaders( - headers.filter { _, value, _ in - value == "client" - }.map { - ($0.name, $0.value) - } - ) - XCTAssertEqual(customMetadata, ["client": "client"], line: line) - } - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - self.assertCustomMetadataIsLowercased(context.headers) - context.trailers.add(name: "SERVER", value: "server") - return context.eventLoop.makeSucceededFuture(.with { $0.text = request.text }) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - self.assertCustomMetadataIsLowercased(context.headers) - context.trailers.add(name: "SERVER", value: "server") - return context.eventLoop.makeSucceededFuture(.ok) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - self.assertCustomMetadataIsLowercased(context.headers) - context.trailers.add(name: "SERVER", value: "server") - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case .message: - () - case .end: - context.responsePromise.succeed(.with { $0.text = "foo" }) - } - }) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - self.assertCustomMetadataIsLowercased(context.headers) - context.trailers.add(name: "SERVER", value: "server") - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case .message: - () - case .end: - context.statusPromise.succeed(.ok) - } - }) - } -} - -class HeaderNormalizationTests: GRPCTestCase { - var group: EventLoopGroup! - var server: Server! - var channel: GRPCChannel! - var client: Echo_EchoNIOClient! - - override func setUp() { - super.setUp() - - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - self.server = try! Server.insecure(group: self.group) - .withServiceProviders([EchoMetadataValidator()]) - .bind(host: "localhost", port: 0) - .wait() - - self.channel = ClientConnection.insecure(group: self.group) - .connect(host: "localhost", port: self.server.channel.localAddress!.port!) - self.client = Echo_EchoNIOClient(channel: self.channel) - } - - override func tearDown() { - XCTAssertNoThrow(try self.channel.close().wait()) - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - private func assertCustomMetadataIsLowercased( - _ headers: EventLoopFuture, - expectation: XCTestExpectation, - file: StaticString = #filePath, - line: UInt = #line - ) { - // Header lookup is case-insensitive so we need to pull out the values we know the server sent - // us as trailing-metadata and then compare a new set of headers. - headers.map { trailers -> HPACKHeaders in - let filtered = trailers.filter { - $0.value == "server" - }.map { name, value, _ in - (name, value) - } - return HPACKHeaders(filtered) - }.assertEqual(["server": "server"], fulfill: expectation, file: file, line: line) - } - - func testHeadersAreNormalizedForUnary() throws { - let trailingMetadata = self.expectation(description: "received trailing metadata") - let options = CallOptions(customMetadata: ["CLIENT": "client"]) - let rpc = self.client.get(.with { $0.text = "foo" }, callOptions: options) - self.assertCustomMetadataIsLowercased(rpc.trailingMetadata, expectation: trailingMetadata) - self.wait(for: [trailingMetadata], timeout: 1.0) - } - - func testHeadersAreNormalizedForClientStreaming() throws { - let trailingMetadata = self.expectation(description: "received trailing metadata") - let options = CallOptions(customMetadata: ["CLIENT": "client"]) - let rpc = self.client.collect(callOptions: options) - rpc.sendEnd(promise: nil) - self.assertCustomMetadataIsLowercased(rpc.trailingMetadata, expectation: trailingMetadata) - self.wait(for: [trailingMetadata], timeout: 1.0) - } - - func testHeadersAreNormalizedForServerStreaming() throws { - let trailingMetadata = self.expectation(description: "received trailing metadata") - let options = CallOptions(customMetadata: ["CLIENT": "client"]) - let rpc = self.client.expand(.with { $0.text = "foo" }, callOptions: options) { - XCTFail("unexpected response: \($0)") - } - self.assertCustomMetadataIsLowercased(rpc.trailingMetadata, expectation: trailingMetadata) - self.wait(for: [trailingMetadata], timeout: 1.0) - } - - func testHeadersAreNormalizedForBidirectionalStreaming() throws { - let trailingMetadata = self.expectation(description: "received trailing metadata") - let options = CallOptions(customMetadata: ["CLIENT": "client"]) - let rpc = self.client.update(callOptions: options) { - XCTFail("unexpected response: \($0)") - } - rpc.sendEnd(promise: nil) - self.assertCustomMetadataIsLowercased(rpc.trailingMetadata, expectation: trailingMetadata) - self.wait(for: [trailingMetadata], timeout: 1.0) - } -} diff --git a/Tests/GRPCTests/ImmediateServerFailureTests.swift b/Tests/GRPCTests/ImmediateServerFailureTests.swift deleted file mode 100644 index 66db72941..000000000 --- a/Tests/GRPCTests/ImmediateServerFailureTests.swift +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import GRPC -import NIOCore -import XCTest - -class ImmediatelyFailingEchoProvider: Echo_EchoProvider { - let interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil - - static let status: GRPCStatus = .init(code: .unavailable, message: nil) - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(ImmediatelyFailingEchoProvider.status) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(ImmediatelyFailingEchoProvider.status) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - context.responsePromise.fail(ImmediatelyFailingEchoProvider.status) - return context.eventLoop.makeSucceededFuture({ _ in - // no-op - }) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - context.statusPromise.fail(ImmediatelyFailingEchoProvider.status) - return context.eventLoop.makeSucceededFuture({ _ in - // no-op - }) - } -} - -class ImmediatelyFailingProviderTests: EchoTestCaseBase { - override func makeEchoProvider() -> Echo_EchoProvider { - return ImmediatelyFailingEchoProvider() - } - - func testUnary() throws { - let expcectation = self.makeStatusExpectation() - let call = self.client.get(Echo_EchoRequest(text: "foo")) - call.status.map { $0.code }.assertEqual(.unavailable, fulfill: expcectation) - - self.wait(for: [expcectation], timeout: self.defaultTestTimeout) - } - - func testServerStreaming() throws { - let expcectation = self.makeStatusExpectation() - let call = self.client.expand(Echo_EchoRequest(text: "foo")) { response in - XCTFail("unexpected response: \(response)") - } - - call.status.map { $0.code }.assertEqual(.unavailable, fulfill: expcectation) - self.wait(for: [expcectation], timeout: self.defaultTestTimeout) - } - - func testClientStreaming() throws { - let expcectation = self.makeStatusExpectation() - let call = self.client.collect() - - call.status.map { $0.code }.assertEqual(.unavailable, fulfill: expcectation) - self.wait(for: [expcectation], timeout: self.defaultTestTimeout) - } - - func testBidirectionalStreaming() throws { - let expcectation = self.makeStatusExpectation() - let call = self.client.update { response in - XCTFail("unexpected response: \(response)") - } - - call.status.map { $0.code }.assertEqual(.unavailable, fulfill: expcectation) - self.wait(for: [expcectation], timeout: self.defaultTestTimeout) - } -} diff --git a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift b/Tests/GRPCTests/InterceptedRPCCancellationTests.swift deleted file mode 100644 index 61e41bb90..000000000 --- a/Tests/GRPCTests/InterceptedRPCCancellationTests.swift +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import Logging -import NIOCore -import NIOPosix -import XCTest - -import protocol SwiftProtobuf.Message - -@testable import GRPC - -final class InterceptedRPCCancellationTests: GRPCTestCase { - func testCancellationWithinInterceptedRPC() throws { - // This test validates that when using interceptors to replay an RPC that the lifecycle of - // the interceptor pipeline is correctly managed. That is, the transport maintains a reference - // to the pipeline for as long as the call is alive (rather than dropping the reference when - // the RPC ends). - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - // Interceptor checks that a "magic" header is present. - let serverInterceptors = EchoServerInterceptors({ MagicRequiredServerInterceptor() }) - let server = try Server.insecure(group: group) - .withLogger(self.serverLogger) - .withServiceProviders([EchoProvider(interceptors: serverInterceptors)]) - .bind(host: "127.0.0.1", port: 0) - .wait() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - let connection = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "127.0.0.1", port: server.channel.localAddress!.port!) - defer { - XCTAssertNoThrow(try connection.close().wait()) - } - - // Retries an RPC with a "magic" header if it fails with the permission denied status code. - let clientInterceptors = EchoClientInterceptors { - return MagicAddingClientInterceptor(channel: connection) - } - - let echo = Echo_EchoNIOClient(channel: connection, interceptors: clientInterceptors) - - let receivedFirstResponse = connection.eventLoop.makePromise(of: Void.self) - let update = echo.update { _ in - receivedFirstResponse.succeed(()) - } - - XCTAssertNoThrow(try update.sendMessage(.with { $0.text = "ping" }).wait()) - // Wait for the pong: it means the second RPC is up and running and the first should have - // completed. - XCTAssertNoThrow(try receivedFirstResponse.futureResult.wait()) - XCTAssertNoThrow(try update.cancel().wait()) - - let status = try update.status.wait() - XCTAssertEqual(status.code, .cancelled) - } -} - -final class MagicRequiredServerInterceptor< - Request: Message, - Response: Message ->: ServerInterceptor, @unchecked Sendable { - override func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - switch part { - case let .metadata(metadata): - if metadata.contains(name: "magic") { - context.logger.debug("metadata contains magic; accepting rpc") - context.receive(part) - } else { - context.logger.debug("metadata does not contains magic; rejecting rpc") - let status = GRPCStatus(code: .permissionDenied, message: nil) - context.send(.end(status, [:]), promise: nil) - } - case .message, .end: - context.receive(part) - } - } -} - -final class MagicAddingClientInterceptor< - Request: Message, - Response: Message ->: ClientInterceptor, @unchecked Sendable { - private let channel: GRPCChannel - private var requestParts = CircularBuffer>() - private var retry: Call? - - init(channel: GRPCChannel) { - self.channel = channel - } - - override func cancel( - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - if let retry = self.retry { - context.logger.debug("cancelling retry RPC") - retry.cancel(promise: promise) - } else { - context.cancel(promise: promise) - } - } - - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - if let retry = self.retry { - context.logger.debug("retrying part \(part)") - retry.send(part, promise: promise) - } else { - switch part { - case .metadata: - // Replace the metadata with the magic words. - self.requestParts.append(.metadata(["magic": "it's real!"])) - case .message, .end: - self.requestParts.append(part) - } - context.send(part, promise: promise) - } - } - - override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - switch part { - case .metadata, .message: - XCTFail("Unexpected response part \(part)") - context.receive(part) - - case let .end(status, _): - guard status.code == .permissionDenied else { - XCTFail("Unexpected status code \(status)") - context.receive(part) - return - } - - XCTAssertNil(self.retry) - - context.logger.debug("initial rpc failed, retrying") - - self.retry = self.channel.makeCall( - path: context.path, - type: context.type, - callOptions: CallOptions(logger: context.logger), - interceptors: [] - ) - - self.retry!.invoke { - context.logger.debug("intercepting error from retried rpc") - context.errorCaught($0) - } onResponsePart: { responsePart in - context.logger.debug("intercepting response part from retried rpc") - context.receive(responsePart) - } - - while let requestPart = self.requestParts.popFirst() { - context.logger.debug("replaying \(requestPart) on new rpc") - self.retry!.send(requestPart, promise: nil) - } - } - } -} diff --git a/Tests/GRPCTests/InterceptorsTests.swift b/Tests/GRPCTests/InterceptorsTests.swift deleted file mode 100644 index c27a99008..000000000 --- a/Tests/GRPCTests/InterceptorsTests.swift +++ /dev/null @@ -1,444 +0,0 @@ -/* - * Copyright 2020, 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 Atomics -import EchoImplementation -import EchoModel -import GRPC -import HelloWorldModel -import NIOCore -import NIOHPACK -import NIOPosix -import SwiftProtobuf -import XCTest - -class InterceptorsTests: GRPCTestCase { - private var group: EventLoopGroup! - private var server: Server! - private var connection: ClientConnection! - private var echo: Echo_EchoNIOClient! - private let onCloseCounter = ManagedAtomic(0) - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - self.server = try! Server.insecure(group: self.group) - .withServiceProviders([ - EchoProvider(interceptors: CountOnCloseInterceptors(counter: self.onCloseCounter)), - HelloWorldProvider(interceptors: HelloWorldServerInterceptorFactory()), - ]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - self.connection = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: self.server.channel.localAddress!.port!) - - self.echo = Echo_EchoNIOClient( - channel: self.connection, - defaultCallOptions: CallOptions(logger: self.clientLogger), - interceptors: ReversingInterceptors() - ) - } - - override func tearDown() { - super.tearDown() - XCTAssertNoThrow(try self.connection.close().wait()) - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - } - - func testEcho() { - let get = self.echo.get(.with { $0.text = "hello" }) - assertThat(try get.response.wait(), .is(.with { $0.text = "hello :teg ohce tfiwS" })) - assertThat(try get.status.wait(), .hasCode(.ok)) - - XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) - } - - func testCollect() { - let collect = self.echo.collect() - collect.sendMessage(.with { $0.text = "1 2" }, promise: nil) - collect.sendMessage(.with { $0.text = "3 4" }, promise: nil) - collect.sendEnd(promise: nil) - assertThat(try collect.response.wait(), .is(.with { $0.text = "3 4 1 2 :tcelloc ohce tfiwS" })) - assertThat(try collect.status.wait(), .hasCode(.ok)) - - XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) - } - - func testExpand() { - let expand = self.echo.expand(.with { $0.text = "hello" }) { response in - // Expand splits on spaces, so we only expect one response. - assertThat(response, .is(.with { $0.text = "hello :)0( dnapxe ohce tfiwS" })) - } - assertThat(try expand.status.wait(), .hasCode(.ok)) - - XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) - } - - func testUpdate() { - let update = self.echo.update { response in - // We'll just send the one message, so only expect one response. - assertThat(response, .is(.with { $0.text = "hello :)0( etadpu ohce tfiwS" })) - } - update.sendMessage(.with { $0.text = "hello" }, promise: nil) - update.sendEnd(promise: nil) - assertThat(try update.status.wait(), .hasCode(.ok)) - - XCTAssertEqual(self.onCloseCounter.load(ordering: .sequentiallyConsistent), 1) - } - - func testSayHello() { - var greeter = Helloworld_GreeterNIOClient( - channel: self.connection, - defaultCallOptions: CallOptions(logger: self.clientLogger) - ) - - // Make a call without interceptors. - let notAuthed = greeter.sayHello(.with { $0.name = "World" }) - assertThat(try notAuthed.response.wait(), .throws()) - assertThat( - try notAuthed.trailingMetadata.wait(), - .contains("www-authenticate", ["Magic"]) - ) - assertThat(try notAuthed.status.wait(), .hasCode(.unauthenticated)) - - // Add an interceptor factory. - greeter.interceptors = HelloWorldClientInterceptorFactory(client: greeter) - // Make sure we break the reference cycle. - defer { - greeter.interceptors = nil - } - - // Try again with the not-really-auth interceptor: - let hello = greeter.sayHello(.with { $0.name = "PanCakes" }) - assertThat( - try hello.response.map { $0.message }.wait(), - .is(.equalTo("Hello, PanCakes, you're authorized!")) - ) - assertThat(try hello.status.wait(), .hasCode(.ok)) - } -} - -// MARK: - Helpers - -class HelloWorldProvider: Helloworld_GreeterProvider { - var interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? - - init(interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? = nil) { - self.interceptors = interceptors - } - - func sayHello( - request: Helloworld_HelloRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - // Since we're auth'd, the 'userInfo' should have some magic set. - assertThat(context.userInfo.magic, .is("Magic")) - - let response = Helloworld_HelloReply.with { - $0.message = "Hello, \(request.name), you're authorized!" - } - return context.eventLoop.makeSucceededFuture(response) - } -} - -extension HelloWorldClientInterceptorFactory: @unchecked Sendable {} - -private class HelloWorldClientInterceptorFactory: - Helloworld_GreeterClientInterceptorFactoryProtocol -{ - var client: Helloworld_GreeterNIOClient - - init(client: Helloworld_GreeterNIOClient) { - self.client = client - } - - func makeSayHelloInterceptors() -> [ClientInterceptor< - Helloworld_HelloRequest, Helloworld_HelloReply - >] { - return [NotReallyAuthClientInterceptor(client: self.client)] - } -} - -class RemoteAddressExistsInterceptor: - ServerInterceptor, @unchecked Sendable -{ - override func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - XCTAssertNotNil(context.remoteAddress) - super.receive(part, context: context) - } -} - -class NotReallyAuthServerInterceptor: - ServerInterceptor, - @unchecked Sendable -{ - override func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - switch part { - case let .metadata(headers): - if let auth = headers.first(name: "authorization"), auth == "Magic" { - context.userInfo.magic = auth - context.receive(part) - } else { - // Not auth'd. Fail the RPC. - let status = GRPCStatus(code: .unauthenticated, message: "You need some magic auth!") - let trailers = HPACKHeaders([("www-authenticate", "Magic")]) - context.send(.end(status, trailers), promise: nil) - } - - case .message, .end: - context.receive(part) - } - } -} - -final class HelloWorldServerInterceptorFactory: Helloworld_GreeterServerInterceptorFactoryProtocol { - func makeSayHelloInterceptors() -> [ServerInterceptor< - Helloworld_HelloRequest, Helloworld_HelloReply - >] { - return [RemoteAddressExistsInterceptor(), NotReallyAuthServerInterceptor()] - } -} - -class NotReallyAuthClientInterceptor: - ClientInterceptor, @unchecked Sendable -{ - private let client: Helloworld_GreeterNIOClient - - private enum State { - // We're trying the call, these are the parts we've sent so far. - case trying([GRPCClientRequestPart]) - // We're retrying using this call. - case retrying(Call) - } - - private var state: State = .trying([]) - - init(client: Helloworld_GreeterNIOClient) { - self.client = client - } - - override func cancel( - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - switch self.state { - case .trying: - context.cancel(promise: promise) - - case let .retrying(call): - call.cancel(promise: promise) - context.cancel(promise: nil) - } - } - - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - switch self.state { - case var .trying(parts): - // Record the part, incase we need to retry. - parts.append(part) - self.state = .trying(parts) - // Forward the request part. - context.send(part, promise: promise) - - case let .retrying(call): - // We're retrying, send the part to the retry call. - call.send(part, promise: promise) - } - } - - override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - switch self.state { - case var .trying(parts): - switch part { - // If 'authentication' fails this is the only part we expect, we can forward everything else. - case let .end(status, trailers) where status.code == .unauthenticated: - // We only know how to deal with magic. - guard trailers.first(name: "www-authenticate") == "Magic" else { - // We can't handle this, fail. - context.receive(part) - return - } - - // We know how to handle this: make a new call. - let call: Call = self.client.channel.makeCall( - path: context.path, - type: context.type, - callOptions: context.options, - // We could grab interceptors from the client, but we don't need to. - interceptors: [] - ) - - // We're retying the call now. - self.state = .retrying(call) - - // Invoke the call and redirect responses here. - call.invoke(onError: context.errorCaught(_:), onResponsePart: context.receive(_:)) - - // Parts must contain the metadata as the first item if we got that first response. - if case var .some(.metadata(metadata)) = parts.first { - metadata.replaceOrAdd(name: "authorization", value: "Magic") - parts[0] = .metadata(metadata) - } - - // Now replay any requests on the retry call. - for part in parts { - call.send(part, promise: nil) - } - - default: - context.receive(part) - } - - case .retrying: - // Ignore anything we receive on the original call. - () - } - } -} - -final class EchoReverseInterceptor: ClientInterceptor, - @unchecked Sendable -{ - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - switch part { - case .message(var request, let metadata): - request.text = String(request.text.reversed()) - context.send(.message(request, metadata), promise: promise) - default: - context.send(part, promise: promise) - } - } - - override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - switch part { - case var .message(response): - response.text = String(response.text.reversed()) - context.receive(.message(response)) - default: - context.receive(part) - } - } -} - -final class ReversingInterceptors: Echo_EchoClientInterceptorFactoryProtocol { - // This interceptor is stateless, let's just share it. - private let interceptors = [EchoReverseInterceptor()] - - func makeGetInterceptors() -> [ClientInterceptor] { - return self.interceptors - } - - func makeExpandInterceptors() -> [ClientInterceptor] { - return self.interceptors - } - - func makeCollectInterceptors() -> [ClientInterceptor] { - return self.interceptors - } - - func makeUpdateInterceptors() -> [ClientInterceptor] { - return self.interceptors - } -} - -final class CountOnCloseInterceptors: Echo_EchoServerInterceptorFactoryProtocol { - // This interceptor is stateless, let's just share it. - private let interceptors: [ServerInterceptor] - - init(counter: ManagedAtomic) { - self.interceptors = [CountOnCloseServerInterceptor(counter: counter)] - } - - func makeGetInterceptors() -> [ServerInterceptor] { - return self.interceptors - } - - func makeExpandInterceptors() -> [ServerInterceptor] { - return self.interceptors - } - - func makeCollectInterceptors() -> [ServerInterceptor] { - return self.interceptors - } - - func makeUpdateInterceptors() -> [ServerInterceptor] { - return self.interceptors - } -} - -final class CountOnCloseServerInterceptor: ServerInterceptor, - @unchecked Sendable -{ - private let counter: ManagedAtomic - - init(counter: ManagedAtomic) { - self.counter = counter - } - - override func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - switch part { - case .metadata: - context.closeFuture.whenComplete { _ in - self.counter.wrappingIncrement(ordering: .sequentiallyConsistent) - } - default: - () - } - context.receive(part) - } -} - -private enum MagicKey: UserInfo.Key { - typealias Value = String -} - -extension UserInfo { - fileprivate var magic: MagicKey.Value? { - get { - return self[MagicKey.self] - } - set { - self[MagicKey.self] = newValue - } - } -} diff --git a/Tests/GRPCTests/LazyEventLoopPromiseTests.swift b/Tests/GRPCTests/LazyEventLoopPromiseTests.swift deleted file mode 100644 index 6e771631b..000000000 --- a/Tests/GRPCTests/LazyEventLoopPromiseTests.swift +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOEmbedded -import XCTest - -@testable import GRPC - -class LazyEventLoopPromiseTests: GRPCTestCase { - func testGetFutureAfterSuccess() { - let loop = EmbeddedEventLoop() - var promise = loop.makeLazyPromise(of: String.self) - promise.succeed("foo") - XCTAssertEqual(try promise.getFutureResult().wait(), "foo") - } - - func testGetFutureBeforeSuccess() { - let loop = EmbeddedEventLoop() - var promise = loop.makeLazyPromise(of: String.self) - let future = promise.getFutureResult() - promise.succeed("foo") - XCTAssertEqual(try future.wait(), "foo") - } - - func testGetFutureAfterError() { - let loop = EmbeddedEventLoop() - var promise = loop.makeLazyPromise(of: String.self) - promise.fail(GRPCStatus.processingError) - XCTAssertThrowsError(try promise.getFutureResult().wait()) { error in - XCTAssertTrue(error is GRPCStatus) - } - } - - func testGetFutureBeforeError() { - let loop = EmbeddedEventLoop() - var promise = loop.makeLazyPromise(of: String.self) - let future = promise.getFutureResult() - promise.fail(GRPCStatus.processingError) - XCTAssertThrowsError(try future.wait()) { error in - XCTAssertTrue(error is GRPCStatus) - } - } - - func testGetFutureMultipleTimes() { - let loop = EmbeddedEventLoop() - var promise = loop.makeLazyPromise(of: String.self) - let f1 = promise.getFutureResult() - let f2 = promise.getFutureResult() - promise.succeed("foo") - XCTAssertEqual(try f1.wait(), try f2.wait()) - } - - func testMultipleResolutionsIgnored() { - let loop = EmbeddedEventLoop() - var promise = loop.makeLazyPromise(of: String.self) - - promise.succeed("foo") - XCTAssertEqual(try promise.getFutureResult().wait(), "foo") - - promise.succeed("bar") - XCTAssertEqual(try promise.getFutureResult().wait(), "foo") - - promise.fail(GRPCStatus.processingError) - XCTAssertEqual(try promise.getFutureResult().wait(), "foo") - } - - func testNoFuture() { - let loop = EmbeddedEventLoop() - var promise = loop.makeLazyPromise(of: String.self) - promise.succeed("foo") - } -} diff --git a/Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift b/Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift deleted file mode 100644 index 99b3f8096..000000000 --- a/Tests/GRPCTests/LengthPrefixedMessageReaderTests.swift +++ /dev/null @@ -1,283 +0,0 @@ -/* - * 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 Logging -import NIOCore -import XCTest - -@testable import GRPC - -class LengthPrefixedMessageReaderTests: GRPCTestCase { - var reader: LengthPrefixedMessageReader! - - override func setUp() { - super.setUp() - self.reader = LengthPrefixedMessageReader() - } - - var allocator = ByteBufferAllocator() - - func byteBuffer(withBytes bytes: [UInt8]) -> ByteBuffer { - var buffer = self.allocator.buffer(capacity: bytes.count) - buffer.writeBytes(bytes) - return buffer - } - - final let twoByteMessage: [UInt8] = [0x01, 0x02] - func lengthPrefixedTwoByteMessage(withCompression compression: Bool = false) -> [UInt8] { - return [ - compression ? 0x01 : 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) - ] + self.twoByteMessage - } - - private func assertMessagesEqual( - expected expectedBytes: [UInt8], - actual buffer: ByteBuffer?, - line: UInt = #line - ) { - guard let buffer = buffer else { - XCTFail("buffer is nil", line: line) - return - } - - guard let bytes = buffer.getBytes(at: buffer.readerIndex, length: expectedBytes.count) else { - XCTFail( - "Expected \(expectedBytes.count) bytes, but only \(buffer.readableBytes) bytes are readable", - line: line - ) - return - } - - XCTAssertEqual(expectedBytes, bytes, line: line) - } - - func testNextMessageReturnsNilWhenNoBytesAppended() throws { - XCTAssertNil(try self.reader.nextMessage()) - } - - func testNextMessageReturnsMessageIsAppendedInOneBuffer() throws { - var buffer = self.byteBuffer(withBytes: self.lengthPrefixedTwoByteMessage()) - self.reader.append(buffer: &buffer) - - self.assertMessagesEqual(expected: self.twoByteMessage, actual: try self.reader.nextMessage()) - } - - func testNextMessageReturnsMessageForZeroLengthMessage() throws { - let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x00, // 4-byte message length (0) - // 0-byte message - ] - - var buffer = self.byteBuffer(withBytes: bytes) - self.reader.append(buffer: &buffer) - - self.assertMessagesEqual(expected: [], actual: try self.reader.nextMessage()) - } - - func testNextMessageDeliveredAcrossMultipleByteBuffers() throws { - let firstBytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, // first 3 bytes of 4-byte message length - ] - - let secondBytes: [UInt8] = [ - 0x02, // fourth byte of 4-byte message length (2) - 0xF0, 0xBA, // 2-byte message - ] - - var firstBuffer = self.byteBuffer(withBytes: firstBytes) - self.reader.append(buffer: &firstBuffer) - var secondBuffer = self.byteBuffer(withBytes: secondBytes) - self.reader.append(buffer: &secondBuffer) - - self.assertMessagesEqual(expected: [0xF0, 0xBA], actual: try self.reader.nextMessage()) - } - - func testNextMessageWhenMultipleMessagesAreBuffered() throws { - let bytes: [UInt8] = [ - // 1st message - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) - 0x0F, 0x00, // 2-byte message - // 2nd message - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x04, // 4-byte message length (4) - 0xDE, 0xAD, 0xBE, 0xEF, // 4-byte message - // 3rd message - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x01, // 4-byte message length (1) - 0x01, // 1-byte message - ] - - var buffer = self.byteBuffer(withBytes: bytes) - self.reader.append(buffer: &buffer) - - self.assertMessagesEqual(expected: [0x0F, 0x00], actual: try self.reader.nextMessage()) - self.assertMessagesEqual( - expected: [0xDE, 0xAD, 0xBE, 0xEF], - actual: try self.reader.nextMessage() - ) - self.assertMessagesEqual(expected: [0x01], actual: try self.reader.nextMessage()) - } - - func testNextMessageReturnsNilWhenNoMessageLengthIsAvailable() throws { - let bytes: [UInt8] = [ - 0x00 // 1-byte compression flag - ] - - var buffer = self.byteBuffer(withBytes: bytes) - self.reader.append(buffer: &buffer) - - XCTAssertNil(try self.reader.nextMessage()) - - // Ensure we can read a message when the rest of the bytes are delivered - let restOfBytes: [UInt8] = [ - 0x00, 0x00, 0x00, 0x01, // 4-byte message length (1) - 0x00, // 1-byte message - ] - - var secondBuffer = self.byteBuffer(withBytes: restOfBytes) - self.reader.append(buffer: &secondBuffer) - self.assertMessagesEqual(expected: [0x00], actual: try self.reader.nextMessage()) - } - - func testNextMessageReturnsNilWhenNotAllMessageLengthIsAvailable() throws { - let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, // 2-bytes of message length (should be 4) - ] - - var buffer = self.byteBuffer(withBytes: bytes) - self.reader.append(buffer: &buffer) - - XCTAssertNil(try self.reader.nextMessage()) - - // Ensure we can read a message when the rest of the bytes are delivered - let restOfBytes: [UInt8] = [ - 0x00, 0x01, // 4-byte message length (1) - 0x00, // 1-byte message - ] - - var secondBuffer = self.byteBuffer(withBytes: restOfBytes) - self.reader.append(buffer: &secondBuffer) - self.assertMessagesEqual(expected: [0x00], actual: try self.reader.nextMessage()) - } - - func testNextMessageReturnsNilWhenNoMessageBytesAreAvailable() throws { - let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) - ] - - var buffer = self.byteBuffer(withBytes: bytes) - self.reader.append(buffer: &buffer) - - XCTAssertNil(try self.reader.nextMessage()) - - // Ensure we can read a message when the rest of the bytes are delivered - var secondBuffer = self.byteBuffer(withBytes: self.twoByteMessage) - self.reader.append(buffer: &secondBuffer) - self.assertMessagesEqual(expected: self.twoByteMessage, actual: try self.reader.nextMessage()) - } - - func testNextMessageReturnsNilWhenNotAllMessageBytesAreAvailable() throws { - let bytes: [UInt8] = [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x00, 0x02, // 4-byte message length (2) - 0x00, // 1-byte of message - ] - - var buffer = self.byteBuffer(withBytes: bytes) - self.reader.append(buffer: &buffer) - - XCTAssertNil(try self.reader.nextMessage()) - - // Ensure we can read a message when the rest of the bytes are delivered - let restOfBytes: [UInt8] = [ - 0x01 // final byte of message - ] - - var secondBuffer = self.byteBuffer(withBytes: restOfBytes) - self.reader.append(buffer: &secondBuffer) - self.assertMessagesEqual(expected: [0x00, 0x01], actual: try self.reader.nextMessage()) - } - - func testNextMessageThrowsWhenCompressionFlagIsSetButNotExpected() throws { - // Default compression mechanism is `nil` which requires that no - // compression flag is set as it indicates a lack of message encoding header. - XCTAssertNil(self.reader.compression) - - var buffer = - self - .byteBuffer(withBytes: self.lengthPrefixedTwoByteMessage(withCompression: true)) - self.reader.append(buffer: &buffer) - - XCTAssertThrowsError(try self.reader.nextMessage()) { error in - let errorWithContext = error as? GRPCError.WithContext - XCTAssertTrue(errorWithContext?.error is GRPCError.CompressionUnsupported) - } - } - - func testNextMessageDoesNotThrowWhenCompressionFlagIsExpectedButNotSet() throws { - // `.identity` should always be supported and requires a flag. - self.reader = LengthPrefixedMessageReader(compression: .identity, decompressionLimit: .ratio(1)) - - var buffer = self.byteBuffer(withBytes: self.lengthPrefixedTwoByteMessage()) - self.reader.append(buffer: &buffer) - - self.assertMessagesEqual(expected: self.twoByteMessage, actual: try self.reader.nextMessage()) - } - - func testAppendReadsAllBytes() throws { - var buffer = self.byteBuffer(withBytes: self.lengthPrefixedTwoByteMessage()) - self.reader.append(buffer: &buffer) - - XCTAssertEqual(0, buffer.readableBytes) - } - - func testExcessiveBytesAreDiscarded() throws { - // We're going to use a 1kB message here for ease of testing. - let message = Array(repeating: UInt8(0), count: 1024) - let largeMessage: [UInt8] = - [ - 0x00, // 1-byte compression flag - 0x00, 0x00, 0x04, 0x00, // 4-byte message length (1024) - ] + message - var buffer = self.byteBuffer(withBytes: largeMessage) - buffer.writeBytes(largeMessage) - buffer.writeBytes(largeMessage) - self.reader.append(buffer: &buffer) - - XCTAssertEqual(self.reader.unprocessedBytes, (1024 + 5) * 3) - XCTAssertEqual(self.reader._consumedNonDiscardedBytes, 0) - - self.assertMessagesEqual(expected: message, actual: try self.reader.nextMessage()) - XCTAssertEqual(self.reader.unprocessedBytes, (1024 + 5) * 2) - XCTAssertEqual(self.reader._consumedNonDiscardedBytes, 1024 + 5) - - self.assertMessagesEqual(expected: message, actual: try self.reader.nextMessage()) - XCTAssertEqual(self.reader.unprocessedBytes, 1024 + 5) - XCTAssertEqual(self.reader._consumedNonDiscardedBytes, 0) - } -} - -extension LengthPrefixedMessageReader { - fileprivate mutating func nextMessage() throws -> ByteBuffer? { - return try self.nextMessage(maxLength: .max) - } -} diff --git a/Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift b/Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift deleted file mode 100644 index d21c39f67..000000000 --- a/Tests/GRPCTests/MessageEncodingHeaderValidatorTests.swift +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2020, 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 XCTest - -@testable import GRPC - -class MessageEncodingHeaderValidatorTests: GRPCTestCase { - func testSupportedAlgorithm() throws { - let validator = MessageEncodingHeaderValidator( - encoding: .enabled( - .init( - enabledAlgorithms: [.deflate, .gzip], - decompressionLimit: .absolute(10) - ) - ) - ) - - let validation = validator.validate(requestEncoding: "gzip") - switch validation { - case .supported(.gzip, .absolute(10), acceptEncoding: []): - () // Expected - default: - XCTFail("Expected .supported but was \(validation)") - } - } - - func testSupportedButNotAdvertisedAlgorithm() throws { - let validator = MessageEncodingHeaderValidator( - encoding: .enabled(.init(enabledAlgorithms: [.deflate], decompressionLimit: .absolute(10))) - ) - - let validation = validator.validate(requestEncoding: "gzip") - switch validation { - case .supported(.gzip, .absolute(10), acceptEncoding: ["deflate", "gzip"]): - () // Expected - default: - XCTFail("Expected .supported but was \(validation)") - } - } - - func testSupportedButExplicitlyDisabled() throws { - let validator = MessageEncodingHeaderValidator(encoding: .disabled) - - let validation = validator.validate(requestEncoding: "gzip") - switch validation { - case .unsupported(requestEncoding: "gzip", acceptEncoding: []): - () // Expected - default: - XCTFail("Expected .unsupported but was \(validation)") - } - } - - func testUnsupportedButEnabled() throws { - let validator = MessageEncodingHeaderValidator( - encoding: - .enabled(.init(enabledAlgorithms: [.gzip], decompressionLimit: .absolute(10))) - ) - - let validation = validator.validate(requestEncoding: "not-supported") - switch validation { - case .unsupported(requestEncoding: "not-supported", acceptEncoding: ["gzip"]): - () // Expected - default: - XCTFail("Expected .unsupported but was \(validation)") - } - } - - func testNoCompressionWhenExplicitlyDisabled() throws { - let validator = MessageEncodingHeaderValidator(encoding: .disabled) - - let validation = validator.validate(requestEncoding: nil) - switch validation { - case .noCompression: - () // Expected - default: - XCTFail("Expected .noCompression but was \(validation)") - } - } - - func testNoCompressionWhenEnabled() throws { - let validator = MessageEncodingHeaderValidator( - encoding: - .enabled( - .init( - enabledAlgorithms: CompressionAlgorithm.all, - decompressionLimit: .absolute(10) - ) - ) - ) - - let validation = validator.validate(requestEncoding: nil) - switch validation { - case .noCompression: - () // Expected - default: - XCTFail("Expected .noCompression but was \(validation)") - } - } -} diff --git a/Tests/GRPCTests/MutualTLSTests.swift b/Tests/GRPCTests/MutualTLSTests.swift deleted file mode 100644 index 4282bba65..000000000 --- a/Tests/GRPCTests/MutualTLSTests.swift +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import EchoImplementation -import EchoModel -@testable import GRPC -import GRPCSampleData -import NIOCore -import NIOPosix -import NIOSSL -import XCTest - -class MutualTLSTests: GRPCTestCase { - enum ExpectedClientError { - case handshakeError - case alertCertRequired - case dropped - } - - var clientEventLoopGroup: EventLoopGroup! - var serverEventLoopGroup: EventLoopGroup! - var channel: GRPCChannel? - var server: Server? - - override func setUp() { - super.setUp() - self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - XCTAssertNoThrow(try self.channel?.close().wait()) - XCTAssertNoThrow(try self.server?.close().wait()) - XCTAssertNoThrow(try self.clientEventLoopGroup.syncShutdownGracefully()) - XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully()) - super.tearDown() - } - - func performTestWith( - _ serverTLSConfiguration: GRPCTLSConfiguration?, - _ clientTLSConfiguration: GRPCTLSConfiguration?, - expectServerHandshakeError: Bool, - expectedClientError: ExpectedClientError? - ) throws { - // Setup the server. - var serverConfiguration = Server.Configuration.default( - target: .hostAndPort("localhost", 0), - eventLoopGroup: self.serverEventLoopGroup, - serviceProviders: [EchoProvider()] - ) - serverConfiguration.tlsConfiguration = serverTLSConfiguration - serverConfiguration.logger = self.serverLogger - let serverErrorExpectation = self.expectation(description: "server error") - serverErrorExpectation.isInverted = !expectServerHandshakeError - serverErrorExpectation.assertForOverFulfill = false - let serverErrorDelegate = ServerErrorRecordingDelegate(expectation: serverErrorExpectation) - serverConfiguration.errorDelegate = serverErrorDelegate - - self.server = try! Server.start(configuration: serverConfiguration).wait() - - let port = self.server!.channel.localAddress!.port! - - // Setup the client. - var clientConfiguration = ClientConnection.Configuration.default( - target: .hostAndPort("localhost", port), - eventLoopGroup: self.clientEventLoopGroup - ) - clientConfiguration.tlsConfiguration = clientTLSConfiguration - clientConfiguration.connectionBackoff = nil - clientConfiguration.backgroundActivityLogger = self.clientLogger - let clientErrorExpectation = self.expectation(description: "client error") - switch expectedClientError { - case .none: - clientErrorExpectation.isInverted = true - case .handshakeError, .alertCertRequired: - // After the SSL error, the connection being closed also presents as an error. - clientErrorExpectation.expectedFulfillmentCount = 2 - case .dropped: - clientErrorExpectation.expectedFulfillmentCount = 1 - } - let clientErrorDelegate = ErrorRecordingDelegate(expectation: clientErrorExpectation) - clientConfiguration.errorDelegate = clientErrorDelegate - - self.channel = ClientConnection(configuration: clientConfiguration) - let client = Echo_EchoNIOClient(channel: channel!) - - // Make the call. - let call = client.get(.with { $0.text = "mumble" }) - - // Wait for side effects. - self.wait(for: [clientErrorExpectation, serverErrorExpectation], timeout: 10) - - if !expectServerHandshakeError { - XCTAssert( - serverErrorDelegate.errors.isEmpty, - "Unexpected server errors: \(serverErrorDelegate.errors)" - ) - } else if case .handshakeFailed = serverErrorDelegate.errors.first as? NIOSSLError { - // This is the expected error. - } else { - XCTFail( - "Expected NIOSSLError.handshakeFailed, actual error(s): \(serverErrorDelegate.errors)" - ) - } - - switch expectedClientError { - case .none: - XCTAssert( - clientErrorDelegate.errors.isEmpty, - "Unexpected client errors: \(clientErrorDelegate.errors)" - ) - case .some(.handshakeError): - if case .handshakeFailed = clientErrorDelegate.errors.first as? NIOSSLError { - // This is the expected error. - } else { - XCTFail( - "Expected NIOSSLError.handshakeFailed, actual error(s): \(clientErrorDelegate.errors)" - ) - } - case .some(.alertCertRequired): - if let error = clientErrorDelegate.errors.first, error is BoringSSLError { - // This is the expected error when client receives TLSV1_ALERT_CERTIFICATE_REQUIRED. - } else { - XCTFail("Expected BoringSSLError, actual error(s): \(clientErrorDelegate.errors)") - } - case .some(.dropped): - if let error = clientErrorDelegate.errors.first as? GRPCStatus, error.code == .unavailable { - // This is the expected error when client closes the connection. - } else { - XCTFail("Expected BoringSSLError, actual error(s): \(clientErrorDelegate.errors)") - } - } - - if !expectServerHandshakeError, expectedClientError == nil { - // Verify response. - let response = try call.response.wait() - XCTAssertEqual(response.text, "Swift echo get: mumble") - let status = try call.status.wait() - XCTAssertEqual(status.code, .ok) - } - } - - func test_trustedClientAndServerCerts_success() throws { - let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.server.certificate)], - privateKey: .privateKey(SamplePrivateKey.server), - trustRoots: .certificates([ - SampleCertificate.ca.certificate, - SampleCertificate.otherCA.certificate, - ]), - certificateVerification: .noHostnameVerification - ) - let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([ - SampleCertificate.ca.certificate, - SampleCertificate.otherCA.certificate, - ]), - certificateVerification: .fullVerification - ) - try self.performTestWith( - serverTLSConfiguration, - clientTLSConfiguration, - expectServerHandshakeError: false, - expectedClientError: nil - ) - } - - func test_untrustedServerCert_clientError() throws { - let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.server.certificate)], - privateKey: .privateKey(SamplePrivateKey.server), - trustRoots: .certificates([ - SampleCertificate.ca.certificate, - SampleCertificate.otherCA.certificate, - ]), - certificateVerification: .noHostnameVerification - ) - let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([ - SampleCertificate.otherCA.certificate - ]), - certificateVerification: .fullVerification - ) - try self.performTestWith( - serverTLSConfiguration, - clientTLSConfiguration, - expectServerHandshakeError: true, - expectedClientError: .handshakeError - ) - } - - func test_untrustedClientCert_serverError() throws { - let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.server.certificate)], - privateKey: .privateKey(SamplePrivateKey.server), - trustRoots: .certificates([ - SampleCertificate.ca.certificate - ]), - certificateVerification: .noHostnameVerification - ) - let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([ - SampleCertificate.ca.certificate, - SampleCertificate.otherCA.certificate, - ]), - certificateVerification: .fullVerification - ) - try self.performTestWith( - serverTLSConfiguration, - clientTLSConfiguration, - expectServerHandshakeError: true, - expectedClientError: .alertCertRequired - ) - } - - func test_plaintextServer_clientError() throws { - let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([ - SampleCertificate.ca.certificate, - SampleCertificate.otherCA.certificate, - ]), - certificateVerification: .fullVerification - ) - try self.performTestWith( - nil, - clientTLSConfiguration, - expectServerHandshakeError: false, - expectedClientError: .handshakeError - ) - } - - func test_plaintextClient_serverError() throws { - let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.server.certificate)], - privateKey: .privateKey(SamplePrivateKey.server), - trustRoots: .certificates([ - SampleCertificate.ca.certificate, - SampleCertificate.otherCA.certificate, - ]), - certificateVerification: .noHostnameVerification - ) - try self.performTestWith( - serverTLSConfiguration, - nil, - expectServerHandshakeError: true, - expectedClientError: .dropped - ) - } -} - -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/OneOrManyQueueTests.swift b/Tests/GRPCTests/OneOrManyQueueTests.swift deleted file mode 100644 index 63c8acd0a..000000000 --- a/Tests/GRPCTests/OneOrManyQueueTests.swift +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2022, 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 XCTest - -@testable import GRPC - -internal final class OneOrManyQueueTests: GRPCTestCase { - func testIsEmpty() { - XCTAssertTrue(OneOrManyQueue().isEmpty) - } - - func testIsEmptyManyBacked() { - XCTAssertTrue(OneOrManyQueue.manyBacked.isEmpty) - } - - func testCount() { - var queue = OneOrManyQueue() - XCTAssertEqual(queue.count, 0) - queue.append(1) - XCTAssertEqual(queue.count, 1) - } - - func testCountManyBacked() { - var manyBacked = OneOrManyQueue.manyBacked - XCTAssertEqual(manyBacked.count, 0) - for i in 1 ... 100 { - manyBacked.append(1) - XCTAssertEqual(manyBacked.count, i) - } - } - - func testAppendAndPop() { - var queue = OneOrManyQueue() - XCTAssertNil(queue.pop()) - - queue.append(1) - XCTAssertEqual(queue.count, 1) - XCTAssertEqual(queue.pop(), 1) - - XCTAssertNil(queue.pop()) - XCTAssertEqual(queue.count, 0) - XCTAssertTrue(queue.isEmpty) - } - - func testAppendAndPopManyBacked() { - var manyBacked = OneOrManyQueue.manyBacked - XCTAssertNil(manyBacked.pop()) - - manyBacked.append(1) - XCTAssertEqual(manyBacked.count, 1) - manyBacked.append(2) - XCTAssertEqual(manyBacked.count, 2) - - XCTAssertEqual(manyBacked.pop(), 1) - XCTAssertEqual(manyBacked.count, 1) - - XCTAssertEqual(manyBacked.pop(), 2) - XCTAssertEqual(manyBacked.count, 0) - - XCTAssertNil(manyBacked.pop()) - XCTAssertTrue(manyBacked.isEmpty) - } - - func testIndexes() { - var queue = OneOrManyQueue() - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, 0) - - // Non-empty. - queue.append(1) - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, 1) - } - - func testIndexesManyBacked() { - var queue = OneOrManyQueue.manyBacked - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, 0) - - for i in 1 ... 100 { - queue.append(i) - XCTAssertEqual(queue.startIndex, 0) - XCTAssertEqual(queue.endIndex, i) - } - } - - func testIndexAfter() { - var queue = OneOrManyQueue() - XCTAssertEqual(queue.startIndex, queue.endIndex) - XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) - - queue.append(1) - XCTAssertNotEqual(queue.startIndex, queue.endIndex) - XCTAssertEqual(queue.index(after: queue.startIndex), queue.endIndex) - } - - func testSubscript() throws { - var queue = OneOrManyQueue() - queue.append(42) - let index = try XCTUnwrap(queue.firstIndex(of: 42)) - XCTAssertEqual(queue[index], 42) - } - - func testSubscriptManyBacked() throws { - var queue = OneOrManyQueue.manyBacked - for i in 0 ... 100 { - queue.append(i) - } - - for i in 0 ... 100 { - XCTAssertEqual(queue[i], i) - } - } -} - -extension OneOrManyQueue where Element == Int { - static var manyBacked: Self { - var queue = OneOrManyQueue() - // Append and pop to move to the 'many' backing. - queue.append(1) - queue.append(2) - XCTAssertEqual(queue.pop(), 1) - XCTAssertEqual(queue.pop(), 2) - return queue - } -} diff --git a/Tests/GRPCTests/PlatformSupportTests.swift b/Tests/GRPCTests/PlatformSupportTests.swift deleted file mode 100644 index 02f24dc54..000000000 --- a/Tests/GRPCTests/PlatformSupportTests.swift +++ /dev/null @@ -1,264 +0,0 @@ -/* - * 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 Foundation -import NIOCore -import NIOPosix -import NIOTransportServices -import XCTest - -@testable import GRPC - -#if canImport(Network) -import Network -#endif - -class PlatformSupportTests: GRPCTestCase { - var group: EventLoopGroup! - - override func tearDown() { - XCTAssertNoThrow(try self.group?.syncShutdownGracefully()) - super.tearDown() - } - - func testMakeEventLoopGroupReturnsMultiThreadedGroupForPosix() { - self.group = PlatformSupport.makeEventLoopGroup( - loopCount: 1, - networkPreference: .userDefined(.posix) - ) - - XCTAssertTrue(self.group is MultiThreadedEventLoopGroup) - } - - func testMakeEventLoopGroupReturnsNIOTSGroupForNetworkFramework() { - // If we don't have Network.framework then we can't test this. - #if canImport(Network) - guard #available(macOS 10.14, *) else { return } - - self.group = PlatformSupport.makeEventLoopGroup( - loopCount: 1, - networkPreference: .userDefined(.networkFramework) - ) - - XCTAssertTrue(self.group is NIOTSEventLoopGroup) - #endif - } - - func testMakeClientBootstrapReturnsClientBootstrapForMultiThreadedGroup() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let bootstrap = PlatformSupport.makeClientBootstrap(group: self.group) - XCTAssertTrue(bootstrap is ClientBootstrap) - } - - func testMakeClientBootstrapReturnsClientBootstrapForEventLoop() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let eventLoop = self.group.next() - let bootstrap = PlatformSupport.makeClientBootstrap(group: eventLoop) - XCTAssertTrue(bootstrap is ClientBootstrap) - } - - func testMakeClientBootstrapReturnsNIOTSConnectionBootstrapForNIOTSGroup() { - // If we don't have Network.framework then we can't test this. - #if canImport(Network) - guard #available(macOS 10.14, *) else { return } - - self.group = NIOTSEventLoopGroup(loopCount: 1) - let bootstrap = PlatformSupport.makeClientBootstrap(group: self.group) - XCTAssertTrue(bootstrap is NIOTSConnectionBootstrap) - #endif - } - - func testMakeClientBootstrapReturnsNIOTSConnectionBootstrapForQoSEventLoop() { - // If we don't have Network.framework then we can't test this. - #if canImport(Network) - guard #available(macOS 10.14, *) else { return } - - self.group = NIOTSEventLoopGroup(loopCount: 1) - - let eventLoop = self.group.next() - XCTAssertTrue(eventLoop is QoSEventLoop) - - let bootstrap = PlatformSupport.makeClientBootstrap(group: eventLoop) - XCTAssertTrue(bootstrap is NIOTSConnectionBootstrap) - #endif - } - - func testMakeServerBootstrapReturnsServerBootstrapForMultiThreadedGroup() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let bootstrap = PlatformSupport.makeServerBootstrap(group: self.group) - XCTAssertTrue(bootstrap is ServerBootstrap) - } - - func testMakeServerBootstrapReturnsServerBootstrapForEventLoop() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - let eventLoop = self.group.next() - let bootstrap = PlatformSupport.makeServerBootstrap(group: eventLoop) - XCTAssertTrue(bootstrap is ServerBootstrap) - } - - func testMakeServerBootstrapReturnsNIOTSListenerBootstrapForNIOTSGroup() { - // If we don't have Network.framework then we can't test this. - #if canImport(Network) - guard #available(macOS 10.14, *) else { return } - - self.group = NIOTSEventLoopGroup(loopCount: 1) - let bootstrap = PlatformSupport.makeServerBootstrap(group: self.group) - XCTAssertTrue(bootstrap is NIOTSListenerBootstrap) - #endif - } - - func testMakeServerBootstrapReturnsNIOTSListenerBootstrapForQoSEventLoop() { - // If we don't have Network.framework then we can't test this. - #if canImport(Network) - guard #available(macOS 10.14, *) else { return } - - self.group = NIOTSEventLoopGroup(loopCount: 1) - - let eventLoop = self.group.next() - XCTAssertTrue(eventLoop is QoSEventLoop) - - let bootstrap = PlatformSupport.makeServerBootstrap(group: eventLoop) - XCTAssertTrue(bootstrap is NIOTSListenerBootstrap) - #endif - } - - func testRequiresZeroLengthWorkaroundWithMTELG() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - // No MTELG or individual loop requires the workaround. - XCTAssertFalse( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: true) - ) - XCTAssertFalse( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: false) - ) - XCTAssertFalse( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: true) - ) - XCTAssertFalse( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: false) - ) - } - - func testRequiresZeroLengthWorkaroundWithNetworkFramework() { - // If we don't have Network.framework we can't test this. - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - self.group = NIOTSEventLoopGroup(loopCount: 1) - - // We require the workaround for any of these loops when TLS is not enabled. - XCTAssertFalse( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: true) - ) - XCTAssertTrue( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group, hasTLS: false) - ) - XCTAssertFalse( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: true) - ) - XCTAssertTrue( - PlatformSupport - .requiresZeroLengthWriteWorkaround(group: self.group.next(), hasTLS: false) - ) - #endif - } - - func testIsTransportServicesGroup() { - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - - let tsGroup = NIOTSEventLoopGroup(loopCount: 1) - defer { - XCTAssertNoThrow(try tsGroup.syncShutdownGracefully()) - } - - XCTAssertTrue(PlatformSupport.isTransportServicesEventLoopGroup(tsGroup)) - XCTAssertTrue(PlatformSupport.isTransportServicesEventLoopGroup(tsGroup.next())) - - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - XCTAssertFalse(PlatformSupport.isTransportServicesEventLoopGroup(group)) - XCTAssertFalse(PlatformSupport.isTransportServicesEventLoopGroup(group.next())) - - #endif - } - - func testIsTLSConfigruationCompatible() { - #if canImport(Network) - #if canImport(NIOSSL) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - - let nwConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNetworkFramework() - let nioSSLConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL() - - let tsGroup = NIOTSEventLoopGroup(loopCount: 1) - defer { - XCTAssertNoThrow(try tsGroup.syncShutdownGracefully()) - } - - XCTAssertTrue(tsGroup.isCompatible(with: nwConfiguration)) - XCTAssertTrue(tsGroup.isCompatible(with: nioSSLConfiguration)) - XCTAssertTrue(tsGroup.next().isCompatible(with: nwConfiguration)) - XCTAssertTrue(tsGroup.next().isCompatible(with: nioSSLConfiguration)) - - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - XCTAssertFalse(group.isCompatible(with: nwConfiguration)) - XCTAssertTrue(group.isCompatible(with: nioSSLConfiguration)) - XCTAssertFalse(group.next().isCompatible(with: nwConfiguration)) - XCTAssertTrue(group.next().isCompatible(with: nioSSLConfiguration)) - #endif - #endif - } - - func testMakeCompatibleEventLoopGroupForNIOSSL() { - #if canImport(NIOSSL) - let configuration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL() - let group = PlatformSupport.makeEventLoopGroup(compatibleWith: configuration, loopCount: 1) - XCTAssertNoThrow(try group.syncShutdownGracefully()) - XCTAssert(group is MultiThreadedEventLoopGroup) - #endif - } - - func testMakeCompatibleEventLoopGroupForNetworkFramework() { - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - - let options = NWProtocolTLS.Options() - let configuration = GRPCTLSConfiguration.makeClientConfigurationBackedByNetworkFramework( - options: options - ) - - let group = PlatformSupport.makeEventLoopGroup(compatibleWith: configuration, loopCount: 1) - XCTAssertNoThrow(try group.syncShutdownGracefully()) - XCTAssert(group is NIOTSEventLoopGroup) - - #endif - } -} diff --git a/Tests/GRPCTests/RequestIDProviderTests.swift b/Tests/GRPCTests/RequestIDProviderTests.swift deleted file mode 100644 index 2a4c8dc10..000000000 --- a/Tests/GRPCTests/RequestIDProviderTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020, 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 XCTest - -@testable import GRPC - -class RequestIDProviderTests: GRPCTestCase { - func testUserDefined() { - let provider = CallOptions.RequestIDProvider.userDefined("foo") - XCTAssertEqual(provider.requestID(), "foo") - XCTAssertEqual(provider.requestID(), "foo") - } - - func testAutogenerated() { - let provider = CallOptions.RequestIDProvider.autogenerated - XCTAssertNotEqual(provider.requestID(), provider.requestID()) - } - - func testGenerator() { - let provider = CallOptions.RequestIDProvider.generated { - "foo" - } - XCTAssertEqual(provider.requestID(), "foo") - } -} diff --git a/Tests/GRPCTests/RequestIDTests.swift b/Tests/GRPCTests/RequestIDTests.swift deleted file mode 100644 index 9dc4d1e12..000000000 --- a/Tests/GRPCTests/RequestIDTests.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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_EchoNIOClient(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")) - } -} diff --git a/Tests/GRPCTests/SampleCertificate+Assertions.swift b/Tests/GRPCTests/SampleCertificate+Assertions.swift deleted file mode 100644 index b1a03e0d1..000000000 --- a/Tests/GRPCTests/SampleCertificate+Assertions.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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. - */ -#if canImport(NIOSSL) -import Foundation -import GRPCSampleData -import XCTest - -extension SampleCertificate { - func assertNotExpired(file: StaticString = #filePath, line: UInt = #line) { - XCTAssertFalse( - self.isExpired, - "Certificate expired at \(self.notAfter)", - // swiftformat:disable:next redundantParens - file: (file), - line: line - ) - } -} -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ServerErrorDelegateTests.swift b/Tests/GRPCTests/ServerErrorDelegateTests.swift deleted file mode 100644 index 05d72ae68..000000000 --- a/Tests/GRPCTests/ServerErrorDelegateTests.swift +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import Foundation -import Logging -import NIOCore -import NIOEmbedded -import NIOHPACK -import NIOHTTP2 -import XCTest - -@testable import GRPC - -private class ServerErrorDelegateMock: ServerErrorDelegate { - private let transformLibraryErrorHandler: (Error) -> (GRPCStatusAndTrailers?) - - init(transformLibraryErrorHandler: @escaping ((Error) -> (GRPCStatusAndTrailers?))) { - self.transformLibraryErrorHandler = transformLibraryErrorHandler - } - - func transformLibraryError(_ error: Error) -> GRPCStatusAndTrailers? { - return self.transformLibraryErrorHandler(error) - } -} - -class ServerErrorDelegateTests: GRPCTestCase { - private var channel: EmbeddedChannel! - private var errorDelegate: ServerErrorDelegate! - - override func tearDown() { - XCTAssertNoThrow(try self.channel.finish(acceptAlreadyClosed: true)) - super.tearDown() - } - - func testTransformLibraryError_whenTransformingErrorToStatus_unary() throws { - try self.testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Get") - } - - func testTransformLibraryError_whenTransformingErrorToStatus_clientStreaming() throws { - try self.testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Collect") - } - - func testTransformLibraryError_whenTransformingErrorToStatus_serverStreaming() throws { - try self.testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Expand") - } - - func testTransformLibraryError_whenTransformingErrorToStatus_bidirectionalStreaming() throws { - try self.testTransformLibraryError_whenTransformingErrorToStatus(uri: "/echo.Echo/Update") - } - - private func testTransformLibraryError_whenTransformingErrorToStatus(uri: String) throws { - self.setupChannelAndDelegate { _ in - GRPCStatusAndTrailers(status: .init(code: .notFound, message: "some error")) - } - let requestHeaders: HPACKHeaders = [ - ":method": "POST", - ":path": uri, - "content-type": "application/grpc", - ] - - let headersPayload: HTTP2Frame.FramePayload = .headers(.init(headers: requestHeaders)) - XCTAssertNoThrow(try self.channel.writeInbound(headersPayload)) - self.channel.pipeline.fireErrorCaught(GRPCStatus(code: .aborted, message: nil)) - - // Read out the response headers. - XCTAssertNoThrow(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - let end = try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self) - - guard case let .some(.headers(trailers)) = end else { - XCTFail("Expected headers but got \(end.debugDescription)") - return - } - - XCTAssertEqual(trailers.headers.first(name: "grpc-status"), "5") - XCTAssertEqual(trailers.headers.first(name: "grpc-message"), "some error") - XCTAssertTrue(trailers.endStream) - } - - func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_unary() throws { - try self - .testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Get") - } - - func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_clientStreaming() throws { - try self - .testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Collect") - } - - func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_serverStreaming() throws { - try self - .testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Expand") - } - - func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata_bidirectionalStreaming() - throws - { - try self - .testTransformLibraryError_whenTransformingErrorToStatusAndMetadata(uri: "/echo.Echo/Update") - } - - private func testTransformLibraryError_whenTransformingErrorToStatusAndMetadata( - uri: String, - line: UInt = #line - ) throws { - self.setupChannelAndDelegate { _ in - GRPCStatusAndTrailers( - status: .init(code: .notFound, message: "some error"), - trailers: ["some-metadata": "test"] - ) - } - - let requestHeaders: HPACKHeaders = [ - ":method": "POST", - ":path": uri, - "content-type": "application/grpc", - ] - - let headersPayload: HTTP2Frame.FramePayload = .headers(.init(headers: requestHeaders)) - XCTAssertNoThrow(try self.channel.writeInbound(headersPayload)) - self.channel.pipeline.fireErrorCaught(GRPCStatus(code: .aborted, message: nil)) - - // Read out the response headers. - XCTAssertNoThrow(try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self)) - let end = try self.channel.readOutbound(as: HTTP2Frame.FramePayload.self) - - guard case let .some(.headers(trailers)) = end else { - XCTFail("Expected headers but got \(end.debugDescription)") - return - } - - XCTAssertEqual(trailers.headers.first(name: "grpc-status"), "5", line: line) - XCTAssertEqual(trailers.headers.first(name: "grpc-message"), "some error", line: line) - XCTAssertEqual(trailers.headers.first(name: "some-metadata"), "test", line: line) - XCTAssertTrue(trailers.endStream) - } - - private func setupChannelAndDelegate( - transformLibraryErrorHandler: @escaping (Error) -> GRPCStatusAndTrailers? - ) { - let provider = EchoProvider() - self.errorDelegate = ServerErrorDelegateMock( - transformLibraryErrorHandler: transformLibraryErrorHandler - ) - - let handler = HTTP2ToRawGRPCServerCodec( - servicesByName: [provider.serviceName: provider], - encoding: .disabled, - errorDelegate: self.errorDelegate, - normalizeHeaders: true, - maximumReceiveMessageLength: .max, - logger: self.logger - ) - - self.channel = EmbeddedChannel(handler: handler) - } -} diff --git a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift b/Tests/GRPCTests/ServerFuzzingRegressionTests.swift deleted file mode 100644 index 38b0e2bee..000000000 --- a/Tests/GRPCTests/ServerFuzzingRegressionTests.swift +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 EchoImplementation -import GRPC -import NIOCore -import NIOEmbedded -import XCTest - -import struct Foundation.Data -import struct Foundation.URL - -final class ServerFuzzingRegressionTests: GRPCTestCase { - private static let failCasesURL = URL(fileURLWithPath: #filePath) - .deletingLastPathComponent() // ServerFuzzingRegressionTests.swift - .deletingLastPathComponent() // GRPCTests - .deletingLastPathComponent() // Tests - .appendingPathComponent("FuzzTesting") - .appendingPathComponent("FailCases") - - private func runTest(withInput buffer: ByteBuffer) { - let channel = EmbeddedChannel() - try! channel.connect(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 0)).wait() - defer { - _ = try? channel.finish() - } - - let configuration = Server.Configuration.default( - target: .unixDomainSocket("/ignored"), - eventLoopGroup: channel.eventLoop, - serviceProviders: [EchoProvider()] - ) - - XCTAssertNoThrow(try channel._configureForServerFuzzing(configuration: configuration)) - // We're okay with errors. Crashes are bad though. - _ = try? channel.writeInbound(buffer) - channel.embeddedEventLoop.run() - } - - private func runTest(withInputNamed name: String) throws { - let url = ServerFuzzingRegressionTests.failCasesURL.appendingPathComponent(name) - let data = try Data(contentsOf: url) - let buffer = ByteBuffer(data: data) - self.runTest(withInput: buffer) - } - - func testFuzzCase_debug_4645975625957376() { - let name = "clusterfuzz-testcase-minimized-grpc-swift-fuzz-debug-4645975625957376" - XCTAssertNoThrow(try self.runTest(withInputNamed: name)) - } - - func testFuzzCase_release_5413100925878272() { - let name = "clusterfuzz-testcase-minimized-grpc-swift-fuzz-release-5413100925878272" - XCTAssertNoThrow(try self.runTest(withInputNamed: name)) - } - - func testFuzzCase_release_5077460227063808() { - let name = "clusterfuzz-testcase-minimized-ServerFuzzer-release-5077460227063808" - XCTAssertNoThrow(try self.runTest(withInputNamed: name)) - } - - func testFuzzCase_release_5134158417494016() { - let name = "clusterfuzz-testcase-minimized-ServerFuzzer-release-5134158417494016" - XCTAssertNoThrow(try self.runTest(withInputNamed: name)) - } - - func testFuzzCase_release_5448955772141568() { - let name = "clusterfuzz-testcase-minimized-ServerFuzzer-release-5448955772141568" - XCTAssertNoThrow(try self.runTest(withInputNamed: name)) - } - - func testFuzzCase_release_5285159577452544() { - let name = "clusterfuzz-testcase-minimized-ServerFuzzer-release-5285159577452544" - XCTAssertNoThrow(try self.runTest(withInputNamed: name)) - } - - func testFuzzCase_release_4739158818553856() { - let name = "clusterfuzz-testcase-minimized-ServerFuzzer-release-4739158818553856" - XCTAssertNoThrow(try self.runTest(withInputNamed: name)) - } -} diff --git a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift b/Tests/GRPCTests/ServerInterceptorPipelineTests.swift deleted file mode 100644 index 5f61afc4f..000000000 --- a/Tests/GRPCTests/ServerInterceptorPipelineTests.swift +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright 2020, 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 NIOConcurrencyHelpers -import NIOCore -import NIOEmbedded -import NIOHPACK -import XCTest - -@testable import GRPC - -class ServerInterceptorPipelineTests: GRPCTestCase { - override func setUp() { - super.setUp() - self.embeddedEventLoop = EmbeddedEventLoop() - } - - private var embeddedEventLoop: EmbeddedEventLoop! - - private func makePipeline( - requests: Request.Type = Request.self, - responses: Response.Type = Response.self, - path: String = "/foo/bar", - callType: GRPCCallType = .unary, - interceptors: [ServerInterceptor] = [], - onRequestPart: @escaping (GRPCServerRequestPart) -> Void, - onResponsePart: @escaping (GRPCServerResponsePart, EventLoopPromise?) -> Void - ) -> ServerInterceptorPipeline { - return ServerInterceptorPipeline( - logger: self.logger, - eventLoop: self.embeddedEventLoop, - path: path, - callType: callType, - remoteAddress: nil, - userInfoRef: Ref(UserInfo()), - closeFuture: self.embeddedEventLoop.makeSucceededVoidFuture(), - interceptors: interceptors, - onRequestPart: onRequestPart, - onResponsePart: onResponsePart - ) - } - - func testEmptyPipeline() { - var requestParts: [GRPCServerRequestPart] = [] - var responseParts: [GRPCServerResponsePart] = [] - - let pipeline = self.makePipeline( - requests: String.self, - responses: String.self, - onRequestPart: { requestParts.append($0) }, - onResponsePart: { part, promise in - responseParts.append(part) - assertThat(promise, .is(.none())) - } - ) - - pipeline.receive(.metadata([:])) - pipeline.receive(.message("foo")) - pipeline.receive(.end) - - assertThat(requestParts, .hasCount(3)) - assertThat(requestParts[0].metadata, .is([:])) - assertThat(requestParts[1].message, .is("foo")) - assertThat(requestParts[2].isEnd, .is(true)) - - pipeline.send(.metadata([:]), promise: nil) - pipeline.send(.message("bar", .init(compress: false, flush: false)), promise: nil) - pipeline.send(.end(.ok, [:]), promise: nil) - - assertThat(responseParts, .hasCount(3)) - assertThat(responseParts[0].metadata, .is([:])) - assertThat(responseParts[1].message, .is("bar")) - assertThat(responseParts[2].end, .is(.some())) - - // Pipelines should now be closed. We can't send or receive. - let p = self.embeddedEventLoop.makePromise(of: Void.self) - pipeline.send(.metadata([:]), promise: p) - assertThat(try p.futureResult.wait(), .throws(.instanceOf(GRPCError.AlreadyComplete.self))) - - responseParts.removeAll() - pipeline.receive(.end) - assertThat(responseParts, .isEmpty()) - } - - func testRecordingPipeline() { - let recorder = RecordingServerInterceptor() - let pipeline = self.makePipeline( - interceptors: [recorder], - onRequestPart: { _ in }, - onResponsePart: { _, _ in } - ) - - pipeline.receive(.metadata([:])) - pipeline.receive(.message("foo")) - pipeline.receive(.end) - - pipeline.send(.metadata([:]), promise: nil) - pipeline.send(.message("bar", .init(compress: false, flush: false)), promise: nil) - pipeline.send(.end(.ok, [:]), promise: nil) - - // Check the request parts are there. - assertThat(recorder.requestParts, .hasCount(3)) - assertThat(recorder.requestParts[0].metadata, .is(.some())) - assertThat(recorder.requestParts[1].message, .is(.some())) - assertThat(recorder.requestParts[2].isEnd, .is(true)) - - // Check the response parts are there. - assertThat(recorder.responseParts, .hasCount(3)) - assertThat(recorder.responseParts[0].metadata, .is(.some())) - assertThat(recorder.responseParts[1].message, .is(.some())) - assertThat(recorder.responseParts[2].end, .is(.some())) - } -} - -internal class RecordingServerInterceptor: - ServerInterceptor, @unchecked Sendable -{ - private let lock = NIOLock() - private var _requestParts: [GRPCServerRequestPart] = [] - private var _responseParts: [GRPCServerResponsePart] = [] - - var requestParts: [GRPCServerRequestPart] { - self.lock.withLock { self._requestParts } - } - - var responseParts: [GRPCServerResponsePart] { - self.lock.withLock { self._responseParts } - } - - override func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - self.lock.withLock { self._requestParts.append(part) } - context.receive(part) - } - - override func send( - _ part: GRPCServerResponsePart, - promise: EventLoopPromise?, - context: ServerInterceptorContext - ) { - self.lock.withLock { self._responseParts.append(part) } - context.send(part, promise: promise) - } -} - -extension GRPCServerRequestPart { - var metadata: HPACKHeaders? { - switch self { - case let .metadata(metadata): - return metadata - default: - return nil - } - } - - var message: Request? { - switch self { - case let .message(message): - return message - default: - return nil - } - } - - var isEnd: Bool { - switch self { - case .end: - return true - default: - return false - } - } -} - -extension GRPCServerResponsePart { - var metadata: HPACKHeaders? { - switch self { - case let .metadata(metadata): - return metadata - default: - return nil - } - } - - var message: Response? { - switch self { - case let .message(message, _): - return message - default: - return nil - } - } - - var end: (GRPCStatus, HPACKHeaders)? { - switch self { - case let .end(status, trailers): - return (status, trailers) - default: - return nil - } - } -} diff --git a/Tests/GRPCTests/ServerInterceptorTests.swift b/Tests/GRPCTests/ServerInterceptorTests.swift deleted file mode 100644 index f4ff18da5..000000000 --- a/Tests/GRPCTests/ServerInterceptorTests.swift +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import HelloWorldModel -import NIOCore -import NIOEmbedded -import NIOHTTP1 -import SwiftProtobuf -import XCTest - -@testable import GRPC - -extension GRPCServerHandlerProtocol { - fileprivate func receiveRequest(_ request: Echo_EchoRequest) { - let serializer = ProtobufSerializer() - do { - let buffer = try serializer.serialize(request, allocator: ByteBufferAllocator()) - self.receiveMessage(buffer) - } catch { - XCTFail("Unexpected error: \(error)") - } - } -} - -class ServerInterceptorTests: GRPCTestCase { - private let eventLoop = EmbeddedEventLoop() - private var recorder: ResponseRecorder! - - override func setUp() { - super.setUp() - self.recorder = ResponseRecorder(eventLoop: self.eventLoop) - } - - private func makeRecordingInterceptor() - -> RecordingServerInterceptor - { - return .init() - } - - private func echoProvider( - interceptedBy interceptor: ServerInterceptor - ) -> EchoProvider { - return EchoProvider(interceptors: EchoInterceptorFactory(interceptor: interceptor)) - } - - private func makeHandlerContext(for path: String) -> CallHandlerContext { - return CallHandlerContext( - errorDelegate: nil, - logger: self.serverLogger, - encoding: .disabled, - eventLoop: self.eventLoop, - path: path, - responseWriter: self.recorder, - allocator: ByteBufferAllocator(), - closeFuture: self.eventLoop.makeSucceededVoidFuture() - ) - } - - // This is only useful for the type inference. - private func request( - _ request: GRPCServerRequestPart - ) -> GRPCServerRequestPart { - return request - } - - private func handleMethod( - _ method: Substring, - using provider: CallHandlerProvider - ) -> GRPCServerHandlerProtocol? { - let path = "/\(provider.serviceName)/\(method)" - let context = self.makeHandlerContext(for: path) - return provider.handle(method: method, context: context) - } - - fileprivate typealias ResponsePart = GRPCServerResponsePart - - func testPassThroughInterceptor() throws { - let recordingInterceptor = self.makeRecordingInterceptor() - let provider = self.echoProvider(interceptedBy: recordingInterceptor) - - let handler = try assertNotNil(self.handleMethod("Get", using: provider)) - - // Send requests. - handler.receiveMetadata([:]) - handler.receiveRequest(.with { $0.text = "" }) - handler.receiveEnd() - - // Expect responses. - assertThat(self.recorder.metadata, .is(.some())) - assertThat(self.recorder.messages.count, .is(1)) - assertThat(self.recorder.status, .is(.some())) - - // We expect 2 request parts: the provider responds before it sees end, that's fine. - assertThat(recordingInterceptor.requestParts, .hasCount(2)) - assertThat(recordingInterceptor.requestParts[0], .is(.metadata())) - assertThat(recordingInterceptor.requestParts[1], .is(.message())) - - assertThat(recordingInterceptor.responseParts, .hasCount(3)) - assertThat(recordingInterceptor.responseParts[0], .is(.metadata())) - assertThat(recordingInterceptor.responseParts[1], .is(.message())) - assertThat(recordingInterceptor.responseParts[2], .is(.end(status: .is(.ok)))) - } - - func testUnaryFromInterceptor() throws { - let provider = EchoFromInterceptor() - let handler = try assertNotNil(self.handleMethod("Get", using: provider)) - - // Send the requests. - handler.receiveMetadata([:]) - handler.receiveRequest(.with { $0.text = "foo" }) - handler.receiveEnd() - - // Get the responses. - assertThat(self.recorder.metadata, .is(.some())) - assertThat(self.recorder.messages.count, .is(1)) - assertThat(self.recorder.status, .is(.some())) - } - - func testClientStreamingFromInterceptor() throws { - let provider = EchoFromInterceptor() - let handler = try assertNotNil(self.handleMethod("Collect", using: provider)) - - // Send the requests. - handler.receiveMetadata([:]) - for text in ["a", "b", "c"] { - handler.receiveRequest(.with { $0.text = text }) - } - handler.receiveEnd() - - // Get the responses. - assertThat(self.recorder.metadata, .is(.some())) - assertThat(self.recorder.messages.count, .is(1)) - assertThat(self.recorder.status, .is(.some())) - } - - func testServerStreamingFromInterceptor() throws { - let provider = EchoFromInterceptor() - let handler = try assertNotNil(self.handleMethod("Expand", using: provider)) - - // Send the requests. - handler.receiveMetadata([:]) - handler.receiveRequest(.with { $0.text = "a b c" }) - handler.receiveEnd() - - // Get the responses. - assertThat(self.recorder.metadata, .is(.some())) - assertThat(self.recorder.messages.count, .is(3)) - assertThat(self.recorder.status, .is(.some())) - } - - func testBidirectionalStreamingFromInterceptor() throws { - let provider = EchoFromInterceptor() - let handler = try assertNotNil(self.handleMethod("Update", using: provider)) - - // Send the requests. - handler.receiveMetadata([:]) - for text in ["a", "b", "c"] { - handler.receiveRequest(.with { $0.text = text }) - } - handler.receiveEnd() - - // Get the responses. - assertThat(self.recorder.metadata, .is(.some())) - assertThat(self.recorder.messages.count, .is(3)) - assertThat(self.recorder.status, .is(.some())) - } -} - -final class EchoInterceptorFactory: Echo_EchoServerInterceptorFactoryProtocol { - private let interceptor: ServerInterceptor - - init(interceptor: ServerInterceptor) { - self.interceptor = interceptor - } - - func makeGetInterceptors() -> [ServerInterceptor] { - return [self.interceptor] - } - - func makeExpandInterceptors() -> [ServerInterceptor] { - return [self.interceptor] - } - - func makeCollectInterceptors() -> [ServerInterceptor] { - return [self.interceptor] - } - - func makeUpdateInterceptors() -> [ServerInterceptor] { - return [self.interceptor] - } -} - -class ExtraRequestPartEmitter: - ServerInterceptor, - @unchecked Sendable -{ - enum Part { - case metadata - case message - case end - } - - private let part: Part - private let count: Int - - init(repeat part: Part, times count: Int) { - self.part = part - self.count = count - } - - override func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - let count: Int - - switch (self.part, part) { - case (.metadata, .metadata), - (.message, .message), - (.end, .end): - count = self.count - default: - count = 1 - } - - for _ in 0 ..< count { - context.receive(part) - } - } -} - -class EchoFromInterceptor: Echo_EchoProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? = Interceptors() - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - XCTFail("Unexpected call to \(#function)") - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - XCTFail("Unexpected call to \(#function)") - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - XCTFail("Unexpected call to \(#function)") - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - XCTFail("Unexpected call to \(#function)") - return context.eventLoop.makeFailedFuture(GRPCStatus.processingError) - } - - final class Interceptors: Echo_EchoServerInterceptorFactoryProtocol { - func makeGetInterceptors() -> [ServerInterceptor] { - return [Interceptor()] - } - - func makeExpandInterceptors() -> [ServerInterceptor] { - return [Interceptor()] - } - - func makeCollectInterceptors() -> [ServerInterceptor] { - return [Interceptor()] - } - - func makeUpdateInterceptors() -> [ServerInterceptor] { - return [Interceptor()] - } - } - - // Since all methods use the same request/response types, we can use a single interceptor to - // respond to all of them. - class Interceptor: ServerInterceptor, @unchecked Sendable { - private var collectedRequests: [Echo_EchoRequest] = [] - - override func receive( - _ part: GRPCServerRequestPart, - context: ServerInterceptorContext - ) { - switch part { - case .metadata: - context.send(.metadata([:]), promise: nil) - - case let .message(request): - if context.path.hasSuffix("Get") { - // Unary, just reply. - let response = Echo_EchoResponse.with { - $0.text = "echo: \(request.text)" - } - context.send(.message(response, .init(compress: false, flush: false)), promise: nil) - } else if context.path.hasSuffix("Expand") { - // Server streaming. - let parts = request.text.split(separator: " ") - let metadata = MessageMetadata(compress: false, flush: false) - for part in parts { - context.send(.message(.with { $0.text = "echo: \(part)" }, metadata), promise: nil) - } - } else if context.path.hasSuffix("Collect") { - // Client streaming, store the requests, reply on '.end' - self.collectedRequests.append(request) - } else if context.path.hasSuffix("Update") { - // Bidirectional streaming. - let response = Echo_EchoResponse.with { - $0.text = "echo: \(request.text)" - } - let metadata = MessageMetadata(compress: false, flush: true) - context.send(.message(response, metadata), promise: nil) - } else { - XCTFail("Unexpected path '\(context.path)'") - } - - case .end: - if !self.collectedRequests.isEmpty { - let response = Echo_EchoResponse.with { - $0.text = "echo: " + self.collectedRequests.map { $0.text }.joined(separator: " ") - } - context.send(.message(response, .init(compress: false, flush: false)), promise: nil) - } - - context.send(.end(.ok, [:]), promise: nil) - } - } - } -} - -// Avoid having to serialize/deserialize messages in test cases. -private class Codec: ChannelDuplexHandler { - typealias InboundIn = GRPCServerRequestPart - typealias InboundOut = GRPCServerRequestPart - - typealias OutboundIn = GRPCServerResponsePart - typealias OutboundOut = GRPCServerResponsePart - - private let serializer = ProtobufSerializer() - private let deserializer = ProtobufDeserializer() - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - switch self.unwrapInboundIn(data) { - case let .metadata(headers): - context.fireChannelRead(self.wrapInboundOut(.metadata(headers))) - - case let .message(message): - let serialized = try! self.serializer.serialize(message, allocator: context.channel.allocator) - context.fireChannelRead(self.wrapInboundOut(.message(serialized))) - - case .end: - context.fireChannelRead(self.wrapInboundOut(.end)) - } - } - - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { - switch self.unwrapOutboundIn(data) { - case let .metadata(headers): - context.write(self.wrapOutboundOut(.metadata(headers)), promise: promise) - - case let .message(message, metadata): - let deserialzed = try! self.deserializer.deserialize(byteBuffer: message) - context.write(self.wrapOutboundOut(.message(deserialzed, metadata)), promise: promise) - - case let .end(status, trailers): - context.write(self.wrapOutboundOut(.end(status, trailers)), promise: promise) - } - } -} diff --git a/Tests/GRPCTests/ServerOnCloseTests.swift b/Tests/GRPCTests/ServerOnCloseTests.swift deleted file mode 100644 index 9a94fbf3b..000000000 --- a/Tests/GRPCTests/ServerOnCloseTests.swift +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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 EchoImplementation -import EchoModel -import GRPC -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix -import XCTest - -final class ServerOnCloseTests: GRPCTestCase { - private var group: EventLoopGroup? - private var server: Server? - private var client: ClientConnection? - private var echo: Echo_EchoNIOClient! - - private var eventLoop: EventLoop { - return self.group!.next() - } - - override func tearDown() { - // Some tests shut down the client/server so we tolerate errors here. - try? self.client?.close().wait() - try? self.server?.close().wait() - XCTAssertNoThrow(try self.group?.syncShutdownGracefully()) - super.tearDown() - } - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - - private func setUp(provider: Echo_EchoProvider) throws { - self.server = try Server.insecure(group: self.group!) - .withLogger(self.serverLogger) - .withServiceProviders([provider]) - .bind(host: "localhost", port: 0) - .wait() - - self.client = ClientConnection.insecure(group: self.group!) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: self.server!.channel.localAddress!.port!) - - self.echo = Echo_EchoNIOClient( - channel: self.client!, - defaultCallOptions: CallOptions(logger: self.clientLogger) - ) - } - - private func startServer( - echoDelegate: Echo_EchoProvider, - onClose: @escaping (Result) -> Void - ) { - let provider = OnCloseEchoProvider(delegate: echoDelegate, onClose: onClose) - XCTAssertNoThrow(try self.setUp(provider: provider)) - } - - private func doTestUnary( - echoProvider: Echo_EchoProvider, - completesWithStatus code: GRPCStatus.Code - ) { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: echoProvider) { result in - promise.completeWith(result) - } - - let get = self.echo.get(.with { $0.text = "" }) - assertThat(try get.status.wait(), .hasCode(code)) - XCTAssertNoThrow(try promise.futureResult.wait()) - } - - func testUnaryOnCloseHappyPath() throws { - self.doTestUnary(echoProvider: EchoProvider(), completesWithStatus: .ok) - } - - func testUnaryOnCloseAfterUserFunctionFails() throws { - self.doTestUnary(echoProvider: FailingEchoProvider(), completesWithStatus: .internalError) - } - - func testUnaryOnCloseAfterClientKilled() throws { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: NeverResolvingEchoProvider()) { result in - promise.completeWith(result) - } - - // We want to wait until the client has sent the request parts before closing. We'll grab the - // promise for sending end. - let endSent = self.client!.eventLoop.makePromise(of: Void.self) - self.echo.interceptors = DelegatingEchoClientInterceptorFactory { part, promise, context in - switch part { - case .metadata, .message: - context.send(part, promise: promise) - case .end: - endSent.futureResult.cascade(to: promise) - context.send(part, promise: endSent) - } - } - - _ = self.echo.get(.with { $0.text = "" }) - // Make sure end has been sent before closing the connection. - XCTAssertNoThrow(try endSent.futureResult.wait()) - XCTAssertNoThrow(try self.client!.close().wait()) - XCTAssertNoThrow(try promise.futureResult.wait()) - } - - private func doTestClientStreaming( - echoProvider: Echo_EchoProvider, - completesWithStatus code: GRPCStatus.Code - ) { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: echoProvider) { result in - promise.completeWith(result) - } - - let collect = self.echo.collect() - // We don't know if we'll send successfully or not. - try? collect.sendEnd().wait() - assertThat(try collect.status.wait(), .hasCode(code)) - XCTAssertNoThrow(try promise.futureResult.wait()) - } - - func testClientStreamingOnCloseHappyPath() throws { - self.doTestClientStreaming(echoProvider: EchoProvider(), completesWithStatus: .ok) - } - - func testClientStreamingOnCloseAfterUserFunctionFails() throws { - self.doTestClientStreaming( - echoProvider: FailingEchoProvider(), - completesWithStatus: .internalError - ) - } - - func testClientStreamingOnCloseAfterClientKilled() throws { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: NeverResolvingEchoProvider()) { error in - promise.completeWith(error) - } - - let collect = self.echo.collect() - XCTAssertNoThrow(try collect.sendMessage(.with { $0.text = "" }).wait()) - XCTAssertNoThrow(try self.client!.close().wait()) - XCTAssertNoThrow(try promise.futureResult.wait()) - } - - private func doTestServerStreaming( - echoProvider: Echo_EchoProvider, - completesWithStatus code: GRPCStatus.Code - ) { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: echoProvider) { result in - promise.completeWith(result) - } - - let expand = self.echo.expand(.with { $0.text = "1 2 3" }) { _ in /* ignore responses */ } - assertThat(try expand.status.wait(), .hasCode(code)) - XCTAssertNoThrow(try promise.futureResult.wait()) - } - - func testServerStreamingOnCloseHappyPath() throws { - self.doTestServerStreaming(echoProvider: EchoProvider(), completesWithStatus: .ok) - } - - func testServerStreamingOnCloseAfterUserFunctionFails() throws { - self.doTestServerStreaming( - echoProvider: FailingEchoProvider(), - completesWithStatus: .internalError - ) - } - - func testServerStreamingOnCloseAfterClientKilled() throws { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: NeverResolvingEchoProvider()) { result in - promise.completeWith(result) - } - - // We want to wait until the client has sent the request parts before closing. We'll grab the - // promise for sending end. - let endSent = self.client!.eventLoop.makePromise(of: Void.self) - self.echo.interceptors = DelegatingEchoClientInterceptorFactory { part, promise, context in - switch part { - case .metadata, .message: - context.send(part, promise: promise) - case .end: - endSent.futureResult.cascade(to: promise) - context.send(part, promise: endSent) - } - } - - _ = self.echo.expand(.with { $0.text = "1 2 3" }) { _ in /* ignore responses */ } - // Make sure end has been sent before closing the connection. - XCTAssertNoThrow(try endSent.futureResult.wait()) - XCTAssertNoThrow(try self.client!.close().wait()) - XCTAssertNoThrow(try promise.futureResult.wait()) - } - - private func doTestBidirectionalStreaming( - echoProvider: Echo_EchoProvider, - completesWithStatus code: GRPCStatus.Code - ) { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: echoProvider) { result in - promise.completeWith(result) - } - - let update = self.echo.update { _ in /* ignored */ } - // We don't know if we'll send successfully or not. - try? update.sendEnd().wait() - assertThat(try update.status.wait(), .hasCode(code)) - XCTAssertNoThrow(try promise.futureResult.wait()) - } - - func testBidirectionalStreamingOnCloseHappyPath() throws { - self.doTestBidirectionalStreaming(echoProvider: EchoProvider(), completesWithStatus: .ok) - } - - func testBidirectionalStreamingOnCloseAfterUserFunctionFails() throws { - // TODO: https://github.com/grpc/grpc-swift/issues/1215 - // self.doTestBidirectionalStreaming( - // echoProvider: FailingEchoProvider(), - // completesWithStatus: .internalError - // ) - } - - func testBidirectionalStreamingOnCloseAfterClientKilled() throws { - let promise = self.eventLoop.makePromise(of: Void.self) - self.startServer(echoDelegate: NeverResolvingEchoProvider()) { result in - promise.completeWith(result) - } - - let update = self.echo.update { _ in /* ignored */ } - XCTAssertNoThrow(try update.sendMessage(.with { $0.text = "" }).wait()) - XCTAssertNoThrow(try self.client!.close().wait()) - XCTAssertNoThrow(try promise.futureResult.wait()) - } -} diff --git a/Tests/GRPCTests/ServerQuiescingTests.swift b/Tests/GRPCTests/ServerQuiescingTests.swift deleted file mode 100644 index de7785ce9..000000000 --- a/Tests/GRPCTests/ServerQuiescingTests.swift +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import GRPC -import NIOCore -import NIOPosix -import XCTest - -class ServerQuiescingTests: GRPCTestCase { - func testServerQuiescing() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - assertThat(try group.syncShutdownGracefully(), .doesNotThrow()) - } - - let server = try Server.insecure(group: group) - .withLogger(self.serverLogger) - .withServiceProviders([EchoProvider()]) - .bind(host: "127.0.0.1", port: 0) - .wait() - - let connectivityStateDelegate = RecordingConnectivityDelegate() - let connection = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - .withErrorDelegate(LoggingClientErrorDelegate()) - .withConnectivityStateDelegate(connectivityStateDelegate) - .connect(host: "127.0.0.1", port: server.channel.localAddress!.port!) - defer { - assertThat(try connection.close().wait(), .doesNotThrow()) - } - - let echo = Echo_EchoNIOClient(channel: connection) - - // Expect the connection to setup as normal. - connectivityStateDelegate.expectChanges(2) { changes in - XCTAssertEqual(changes[0], Change(from: .idle, to: .connecting)) - XCTAssertEqual(changes[1], Change(from: .connecting, to: .ready)) - } - - // Fire up a handful of client streaming RPCs, this will start the connection. - let rpcs = (0 ..< 5).map { _ in - echo.collect() - } - - // Wait for the connectivity changes. - connectivityStateDelegate.waitForExpectedChanges(timeout: .seconds(5)) - - // Wait for the response metadata so both peers know about all RPCs. - for rpc in rpcs { - assertThat(try rpc.initialMetadata.wait(), .doesNotThrow()) - } - - // Start shutting down the server. - let serverShutdown = server.initiateGracefulShutdown() - - // We should observe that we're quiescing now: this is a signal to not start any new RPCs. - connectivityStateDelegate.waitForQuiescing(timeout: .seconds(5)) - - // Queue up the expected change back to idle (i.e. when the connection is quiesced). - connectivityStateDelegate.expectChange { - XCTAssertEqual($0, Change(from: .ready, to: .idle)) - } - - // Finish each RPC. - for (index, rpc) in rpcs.enumerated() { - assertThat(try rpc.sendMessage(.with { $0.text = "\(index)" }).wait(), .doesNotThrow()) - assertThat(try rpc.sendEnd().wait(), .doesNotThrow()) - assertThat(try rpc.response.wait(), .is(.with { $0.text = "Swift echo collect: \(index)" })) - } - - // All RPCs are done, the connection should drop back to idle. - connectivityStateDelegate.waitForExpectedChanges(timeout: .seconds(5)) - - // The server should be shutdown now. - assertThat(try serverShutdown.wait(), .doesNotThrow()) - } -} diff --git a/Tests/GRPCTests/ServerTLSErrorTests.swift b/Tests/GRPCTests/ServerTLSErrorTests.swift deleted file mode 100644 index cdaa7fad8..000000000 --- a/Tests/GRPCTests/ServerTLSErrorTests.swift +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2020, 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. - */ -#if canImport(NIOSSL) -import EchoImplementation -import EchoModel -@testable import GRPC -import GRPCSampleData -import Logging -import NIOCore -import NIOPosix -import NIOSSL -import XCTest - -class ServerTLSErrorTests: GRPCTestCase { - let defaultClientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.client.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([SampleCertificate.ca.certificate]), - hostnameOverride: SampleCertificate.server.commonName - ) - - var defaultTestTimeout: TimeInterval = 1.0 - - var clientEventLoopGroup: EventLoopGroup! - var serverEventLoopGroup: EventLoopGroup! - - func makeClientConfiguration( - tls: GRPCTLSConfiguration, - port: Int - ) -> ClientConnection.Configuration { - var configuration = ClientConnection.Configuration.default( - target: .hostAndPort("localhost", port), - eventLoopGroup: self.clientEventLoopGroup - ) - - configuration.tlsConfiguration = tls - // No need to retry connecting. - configuration.connectionBackoff = nil - - return configuration - } - - func makeClientConnectionExpectation() -> XCTestExpectation { - return self.expectation(description: "EventLoopFuture resolved") - } - - override func setUp() { - super.setUp() - self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - } - - override func tearDown() { - XCTAssertNoThrow(try self.clientEventLoopGroup.syncShutdownGracefully()) - self.clientEventLoopGroup = nil - - XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully()) - self.serverEventLoopGroup = nil - super.tearDown() - } - - func testErrorIsLoggedWhenSSLContextErrors() throws { - let errorExpectation = self.expectation(description: "error") - let errorDelegate = ServerErrorRecordingDelegate(expectation: errorExpectation) - - let server = try! Server.usingTLSBackedByNIOSSL( - on: self.serverEventLoopGroup, - certificateChain: [SampleCertificate.exampleServerWithExplicitCurve.certificate], - privateKey: SamplePrivateKey.exampleServerWithExplicitCurve - ).withServiceProviders([EchoProvider()]) - .withErrorDelegate(errorDelegate) - .bind(host: "localhost", port: 0) - .wait() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - let port = server.channel.localAddress!.port! - - var tls = self.defaultClientTLSConfiguration - tls.updateNIOTrustRoots( - to: .certificates([SampleCertificate.exampleServerWithExplicitCurve.certificate]) - ) - - var configuration = self.makeClientConfiguration(tls: tls, port: port) - - let stateChangeDelegate = RecordingConnectivityDelegate() - stateChangeDelegate.expectChanges(2) { changes in - XCTAssertEqual( - changes, - [ - Change(from: .idle, to: .connecting), - Change(from: .connecting, to: .shutdown), - ] - ) - } - - configuration.connectivityStateDelegate = stateChangeDelegate - - // Start an RPC to trigger creating a channel. - let echo = Echo_EchoNIOClient(channel: ClientConnection(configuration: configuration)) - defer { - XCTAssertNoThrow(try echo.channel.close().wait()) - } - _ = echo.get(.with { $0.text = "foo" }) - - self.wait(for: [errorExpectation], timeout: self.defaultTestTimeout) - stateChangeDelegate.waitForExpectedChanges(timeout: .seconds(1)) - - if let nioSSLError = errorDelegate.errors.first as? NIOSSLError, - case .failedToLoadCertificate = nioSSLError - { - // Expected case. - } else { - XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)") - } - } - - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - func testServerCustomVerificationCallback() async throws { - let verificationCallbackInvoked = self.serverEventLoopGroup.next().makePromise(of: Void.self) - let configuration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.server.certificate)], - privateKey: .privateKey(SamplePrivateKey.server), - certificateVerification: .fullVerification, - customVerificationCallback: { _, promise in - verificationCallbackInvoked.succeed() - promise.succeed(.failed) - } - ) - - let server = try await Server.usingTLS(with: configuration, on: self.serverEventLoopGroup) - .withServiceProviders([EchoProvider()]) - .bind(host: "localhost", port: 0) - .get() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL( - certificateChain: [.certificate(SampleCertificate.client.certificate)], - privateKey: .privateKey(SamplePrivateKey.client), - trustRoots: .certificates([SampleCertificate.ca.certificate]), - certificateVerification: .noHostnameVerification, - hostnameOverride: SampleCertificate.server.commonName - ) - - let client = try GRPCChannelPool.with( - target: .hostAndPort("localhost", server.channel.localAddress!.port!), - transportSecurity: .tls(clientTLSConfiguration), - eventLoopGroup: self.clientEventLoopGroup - ) - defer { - XCTAssertNoThrow(try client.close().wait()) - } - - let echo = Echo_EchoAsyncClient(channel: client) - - enum TaskResult { - case rpcFailed - case rpcSucceeded - case verificationCallbackInvoked - } - - await withTaskGroup(of: TaskResult.self, returning: Void.self) { group in - group.addTask { - // Call the service to start an RPC. - do { - _ = try await echo.get(.with { $0.text = "foo" }) - return .rpcSucceeded - } catch { - return .rpcFailed - } - } - - group.addTask { - // '!' is okay, the promise is only ever succeeded. - try! await verificationCallbackInvoked.futureResult.get() - return .verificationCallbackInvoked - } - - while let next = await group.next() { - switch next { - case .verificationCallbackInvoked: - // Expected. - group.cancelAll() - case .rpcFailed: - // Expected, carry on. - continue - case .rpcSucceeded: - XCTFail("RPC succeeded but shouldn't have") - } - } - } - } -} - -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ServerThrowingTests.swift b/Tests/GRPCTests/ServerThrowingTests.swift deleted file mode 100644 index b8d3e95f7..000000000 --- a/Tests/GRPCTests/ServerThrowingTests.swift +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright 2018, 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 Dispatch -import EchoModel -import Foundation -import NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import XCTest - -@testable import GRPC - -let thrownError = GRPCStatus(code: .internalError, message: "expected error") -let transformedError = GRPCStatus(code: .aborted, message: "transformed error") -let transformedMetadata = HPACKHeaders([("transformed", "header")]) - -// Motivation for two different providers: Throwing immediately causes the event observer future (in the -// client-streaming and bidi-streaming cases) to throw immediately, _before_ the corresponding handler has even added -// to the channel. We want to test that case as well as the one where we throw only _after_ the handler has been added -// to the channel. -class ImmediateThrowingEchoProvider: Echo_EchoProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { return nil } - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(thrownError) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(thrownError) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(thrownError) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(thrownError) - } -} - -extension EventLoop { - func makeFailedFuture(_ error: Error, delay: TimeInterval) -> EventLoopFuture { - return self.scheduleTask(in: .nanoseconds(Int64(delay * 1000 * 1000 * 1000))) { () } - .futureResult - .flatMapThrowing { _ -> T in throw error } - } -} - -/// See `ImmediateThrowingEchoProvider`. -class DelayedThrowingEchoProvider: Echo_EchoProvider { - let interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil - - func get( - request: Echo_EchoRequest, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(thrownError, delay: 0.01) - } - - func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeFailedFuture(thrownError, delay: 0.01) - } - - func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(thrownError, delay: 0.01) - } - - func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeFailedFuture(thrownError, delay: 0.01) - } -} - -/// Ensures that fulfilling the status promise (where possible) with an error yields the same result as failing the future. -class ErrorReturningEchoProvider: ImmediateThrowingEchoProvider { - // There's no status promise to fulfill for unary calls (only the response promise), so that case is omitted. - - override func expand( - request: Echo_EchoRequest, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(thrownError) - } - - override func collect( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeSucceededFuture({ _ in - context.responseStatus = thrownError - context.responsePromise.succeed(Echo_EchoResponse()) - }) - } - - override func update( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - return context.eventLoop.makeSucceededFuture({ _ in - context.statusPromise.succeed(thrownError) - }) - } -} - -private class ErrorTransformingDelegate: ServerErrorDelegate { - func transformRequestHandlerError( - _ error: Error, - headers: HPACKHeaders - ) -> GRPCStatusAndTrailers? { - return GRPCStatusAndTrailers(status: transformedError, trailers: transformedMetadata) - } -} - -class ServerThrowingTests: EchoTestCaseBase { - var expectedError: GRPCStatus { return thrownError } - var expectedMetadata: HPACKHeaders? { - return HPACKHeaders([("grpc-status", "13"), ("grpc-message", "expected error")]) - } - - override func makeEchoProvider() -> Echo_EchoProvider { return ImmediateThrowingEchoProvider() } - - func testUnary() throws { - let call = client.get(Echo_EchoRequest(text: "foo")) - XCTAssertEqual(self.expectedError, try call.status.wait()) - let trailers = try call.trailingMetadata.wait() - if let expected = self.expectedMetadata { - for (name, value, _) in expected { - XCTAssertTrue(trailers[name].contains(value)) - } - } - XCTAssertThrowsError(try call.response.wait()) { - XCTAssertEqual(self.expectedError, $0 as? GRPCStatus) - } - } - - func testClientStreaming() throws { - let call = client.collect() - // This is racing with the server error; it might fail, it might not. - try? call.sendEnd().wait() - XCTAssertEqual(self.expectedError, try call.status.wait()) - let trailers = try call.trailingMetadata.wait() - if let expected = self.expectedMetadata { - for (name, value, _) in expected { - XCTAssertTrue(trailers[name].contains(value)) - } - } - - if type(of: self.makeEchoProvider()) != ErrorReturningEchoProvider.self { - // With `ErrorReturningEchoProvider` we actually _return_ a response, which means that the `response` future - // will _not_ fail, so in that case this test doesn't apply. - XCTAssertThrowsError(try call.response.wait()) { - XCTAssertEqual(self.expectedError, $0 as? GRPCStatus) - } - } - } - - func testServerStreaming() throws { - let call = client.expand( - Echo_EchoRequest(text: "foo") - ) { - XCTFail("no message expected, got \($0)") - } - // Nothing to throw here, but the `status` should be the expected error. - XCTAssertEqual(self.expectedError, try call.status.wait()) - let trailers = try call.trailingMetadata.wait() - if let expected = self.expectedMetadata { - for (name, value, _) in expected { - XCTAssertTrue(trailers[name].contains(value)) - } - } - } - - func testBidirectionalStreaming() throws { - let call = client.update { XCTFail("no message expected, got \($0)") } - // This is racing with the server error; it might fail, it might not. - try? call.sendEnd().wait() - // Nothing to throw here, but the `status` should be the expected error. - XCTAssertEqual(self.expectedError, try call.status.wait()) - let trailers = try call.trailingMetadata.wait() - if let expected = self.expectedMetadata { - for (name, value, _) in expected { - XCTAssertTrue(trailers[name].contains(value)) - } - } - } -} - -class ServerDelayedThrowingTests: ServerThrowingTests { - override func makeEchoProvider() -> Echo_EchoProvider { return DelayedThrowingEchoProvider() } - - override func testUnary() throws { - try super.testUnary() - } - - override func testClientStreaming() throws { - try super.testClientStreaming() - } - - override func testServerStreaming() throws { - try super.testServerStreaming() - } - - override func testBidirectionalStreaming() throws { - try super.testBidirectionalStreaming() - } -} - -class ClientThrowingWhenServerReturningErrorTests: ServerThrowingTests { - override func makeEchoProvider() -> Echo_EchoProvider { return ErrorReturningEchoProvider() } - - override func testUnary() throws { - try super.testUnary() - } - - override func testClientStreaming() throws { - try super.testClientStreaming() - } - - override func testServerStreaming() throws { - try super.testServerStreaming() - } - - override func testBidirectionalStreaming() throws { - try super.testBidirectionalStreaming() - } -} - -class ServerErrorTransformingTests: ServerThrowingTests { - override var expectedError: GRPCStatus { return transformedError } - override var expectedMetadata: HPACKHeaders? { - return HPACKHeaders([ - ("grpc-status", "10"), ("grpc-message", "transformed error"), - ("transformed", "header"), - ]) - } - - override func makeErrorDelegate() -> ServerErrorDelegate? { return ErrorTransformingDelegate() } - - override func testUnary() throws { - try super.testUnary() - } - - override func testClientStreaming() throws { - try super.testClientStreaming() - } - - override func testServerStreaming() throws { - try super.testServerStreaming() - } - - override func testBidirectionalStreaming() throws { - try super.testBidirectionalStreaming() - } -} diff --git a/Tests/GRPCTests/ServerWebTests.swift b/Tests/GRPCTests/ServerWebTests.swift deleted file mode 100644 index 254783357..000000000 --- a/Tests/GRPCTests/ServerWebTests.swift +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2018, 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 Foundation -import NIOCore -import XCTest - -@testable import GRPC - -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -// Only test Unary and ServerStreaming, as ClientStreaming is not -// supported in HTTP1. -// TODO: Add tests for application/grpc-web as well. -class ServerWebTests: EchoTestCaseBase { - private func gRPCEncodedEchoRequest(_ text: String) -> Data { - var request = Echo_EchoRequest() - request.text = text - var data = try! request.serializedData() - // Add the gRPC prefix with the compression byte and the 4 length bytes. - for i in 0 ..< 4 { - data.insert(UInt8((data.count >> (i * 8)) & 0xFF), at: 0) - } - data.insert(UInt8(0), at: 0) - return data - } - - private func gRPCWebTrailers(status: Int = 0, message: String? = nil) -> Data { - var data: Data - if let message = message { - data = "grpc-status: \(status)\r\ngrpc-message: \(message)\r\n".data(using: .utf8)! - } else { - data = "grpc-status: \(status)\r\n".data(using: .utf8)! - } - - // Add the gRPC prefix with the compression byte and the 4 length bytes. - for i in 0 ..< 4 { - data.insert(UInt8((data.count >> (i * 8)) & 0xFF), at: 0) - } - data.insert(UInt8(0x80), at: 0) - return data - } - - private func sendOverHTTP1( - rpcMethod: String, - message: String?, - handler: @escaping (Data?, Error?) -> Void - ) { - let serverURL = URL(string: "http://localhost:\(self.port!)/echo.Echo/\(rpcMethod)")! - var request = URLRequest(url: serverURL) - request.httpMethod = "POST" - request.setValue("application/grpc-web-text", forHTTPHeaderField: "content-type") - - if let message = message { - request.httpBody = self.gRPCEncodedEchoRequest(message).base64EncodedData() - } - - let sem = DispatchSemaphore(value: 0) - URLSession.shared.dataTask(with: request) { data, _, error in - handler(data, error) - sem.signal() - }.resume() - sem.wait() - } -} - -extension ServerWebTests { - func testUnary() { - let message = "hello, world!" - let expectedData = - self.gRPCEncodedEchoRequest("Swift echo get: \(message)") + self.gRPCWebTrailers() - let expectedResponse = expectedData.base64EncodedString() - - let completionHandlerExpectation = expectation(description: "completion handler called") - - self.sendOverHTTP1(rpcMethod: "Get", message: message) { data, error in - XCTAssertNil(error) - if let data = data { - XCTAssertEqual(String(data: data, encoding: .utf8), expectedResponse) - completionHandlerExpectation.fulfill() - } else { - XCTFail("no data returned") - } - } - - waitForExpectations(timeout: defaultTestTimeout) - } - - func testUnaryWithoutRequestMessage() { - let expectedData = self.gRPCWebTrailers( - status: 13, - message: "Protocol violation: End received before message" - ) - - let expectedResponse = expectedData.base64EncodedString() - - let completionHandlerExpectation = expectation(description: "completion handler called") - - self.sendOverHTTP1(rpcMethod: "Get", message: nil) { data, error in - XCTAssertNil(error) - if let data = data { - XCTAssertEqual(String(data: data, encoding: .utf8), expectedResponse) - completionHandlerExpectation.fulfill() - } else { - XCTFail("no data returned") - } - } - - waitForExpectations(timeout: defaultTestTimeout) - } - - func testUnaryLotsOfRequests() { - guard self.runTimeSensitiveTests() else { return } - // Sending that many requests at once can sometimes trip things up, it seems. - let clockStart = clock() - let numberOfRequests = 2000 - - let completionHandlerExpectation = expectation(description: "completion handler called") - completionHandlerExpectation.expectedFulfillmentCount = numberOfRequests - completionHandlerExpectation.assertForOverFulfill = true - - for i in 0 ..< numberOfRequests { - let message = "foo \(i)" - let expectedData = - self.gRPCEncodedEchoRequest("Swift echo get: \(message)") + self.gRPCWebTrailers() - let expectedResponse = expectedData.base64EncodedString() - self.sendOverHTTP1(rpcMethod: "Get", message: message) { data, error in - XCTAssertNil(error) - if let data = data { - XCTAssertEqual(String(data: data, encoding: .utf8), expectedResponse) - completionHandlerExpectation.fulfill() - } - } - } - waitForExpectations(timeout: 10) - print( - "total time for \(numberOfRequests) requests: \(Double(clock() - clockStart) / Double(CLOCKS_PER_SEC))" - ) - } - - func testServerStreaming() { - let message = "foo bar baz" - - var expectedData = Data() - var index = 0 - message.split(separator: " ").forEach { component in - expectedData.append(self.gRPCEncodedEchoRequest("Swift echo expand (\(index)): \(component)")) - index += 1 - } - expectedData.append(self.gRPCWebTrailers()) - let expectedResponse = expectedData.base64EncodedString() - let completionHandlerExpectation = expectation(description: "completion handler called") - - self.sendOverHTTP1(rpcMethod: "Expand", message: message) { data, error in - XCTAssertNil(error) - if let data = data { - XCTAssertEqual(String(data: data, encoding: .utf8), expectedResponse) - completionHandlerExpectation.fulfill() - } - } - - waitForExpectations(timeout: defaultTestTimeout) - } -} diff --git a/Tests/GRPCTests/StopwatchTests.swift b/Tests/GRPCTests/StopwatchTests.swift deleted file mode 100644 index 4a5e46709..000000000 --- a/Tests/GRPCTests/StopwatchTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 Foundation -import XCTest - -@testable import GRPC - -class StopwatchTests: GRPCTestCase { - func testElapsed() { - var time: TimeInterval = 0.0 - - let stopwatch = Stopwatch { - Date(timeIntervalSinceNow: time) - } - - time = 1.0 - XCTAssertEqual(1.0, stopwatch.elapsed(), accuracy: 0.001) - - time = 42.0 - XCTAssertEqual(42.0, stopwatch.elapsed(), accuracy: 0.001) - - time = 3650.123 - XCTAssertEqual(3650.123, stopwatch.elapsed(), accuracy: 0.001) - } -} diff --git a/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift b/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift deleted file mode 100644 index 39fb2225c..000000000 --- a/Tests/GRPCTests/StreamResponseHandlerRetainCycleTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2022, 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 EchoImplementation -import EchoModel -import GRPC -import NIOConcurrencyHelpers -import NIOCore -import NIOPosix -import XCTest - -final class StreamResponseHandlerRetainCycleTests: GRPCTestCase { - var group: EventLoopGroup! - var server: Server! - var client: ClientConnection! - - var echo: Echo_EchoNIOClient! - - override func setUp() { - super.setUp() - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - self.server = try! Server.insecure(group: self.group) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "localhost", port: 0) - .wait() - - self.client = ClientConnection.insecure(group: self.group) - .withBackgroundActivityLogger(self.clientLogger) - .connect(host: "localhost", port: self.server.channel.localAddress!.port!) - - self.echo = Echo_EchoNIOClient( - channel: self.client, - defaultCallOptions: CallOptions(logger: self.clientLogger) - ) - } - - override func tearDown() { - XCTAssertNoThrow(try self.client.close().wait()) - XCTAssertNoThrow(try self.server.close().wait()) - XCTAssertNoThrow(try self.group.syncShutdownGracefully()) - super.tearDown() - } - - func testHandlerClosureIsReleasedOnceStreamEnds() { - final class Counter { - private let lock = NIOLock() - private var _value = 0 - - func increment() { - self.lock.withLock { - self._value += 1 - } - } - - var value: Int { - return self.lock.withLock { - self._value - } - } - } - - var counter = Counter() - XCTAssertTrue(isKnownUniquelyReferenced(&counter)) - let get = self.echo.update { [capturedCounter = counter] _ in - capturedCounter.increment() - } - XCTAssertFalse(isKnownUniquelyReferenced(&counter)) - - get.sendMessage(.init(text: "hello world"), promise: nil) - XCTAssertFalse(isKnownUniquelyReferenced(&counter)) - XCTAssertNoThrow(try get.sendEnd().wait()) - XCTAssertNoThrow(try get.status.wait()) - XCTAssertEqual(counter.value, 1) - XCTAssertTrue(isKnownUniquelyReferenced(&counter)) - } -} diff --git a/Tests/GRPCTests/StreamingRequestClientCallTests.swift b/Tests/GRPCTests/StreamingRequestClientCallTests.swift deleted file mode 100644 index d4dd34f84..000000000 --- a/Tests/GRPCTests/StreamingRequestClientCallTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 EchoModel -import Foundation -import GRPC -import XCTest - -class StreamingRequestClientCallTests: EchoTestCaseBase { - class ResponseCounter { - var expectation: XCTestExpectation - - init(expectation: XCTestExpectation) { - self.expectation = expectation - } - - func increment() { - self.expectation.fulfill() - } - } - - func testSendMessages() throws { - let messagesReceived = self.expectation(description: "messages received") - let counter = ResponseCounter(expectation: messagesReceived) - - let update = self.client.update { _ in - counter.increment() - } - - // Send the first batch. - let requests = ["foo", "bar", "baz"].map { Echo_EchoRequest(text: $0) } - messagesReceived.expectedFulfillmentCount = requests.count - XCTAssertNoThrow(try update.sendMessages(requests).wait()) - - // Wait for the responses. - self.wait(for: [messagesReceived], timeout: 0.5) - - let statusReceived = self.expectation(description: "status received") - update.status.map { $0.code }.assertEqual(.ok, fulfill: statusReceived) - - // End the call. - update.sendEnd(promise: nil) - - self.wait(for: [statusReceived], timeout: 0.5) - } -} diff --git a/Tests/GRPCTests/TestClientExample.swift b/Tests/GRPCTests/TestClientExample.swift deleted file mode 100644 index 861152505..000000000 --- a/Tests/GRPCTests/TestClientExample.swift +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright 2020, 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 EchoImplementation -import EchoModel -import GRPC -import NIOCore -import XCTest - -@available(swift, deprecated: 5.6) -class FakeResponseStreamExampleTests: GRPCTestCase { - var client: Echo_EchoTestClient! - - override func setUp() { - super.setUp() - self.client = Echo_EchoTestClient(defaultCallOptions: self.callOptionsWithLogger) - } - - func testUnary() { - // Enqueue a Get response. This will be dequeued when the client makes the next Get RPC. - // - // We can also use a response stream, see 'testUnaryResponseStream'. - self.client.enqueueGetResponse(.with { $0.text = "Bar!" }) - - // Start the Get RPC. - let get = self.client.get(.with { $0.text = "Foo!" }) - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasGetResponsesRemaining) - - // Check the response values: - XCTAssertEqual(try get.response.wait(), .with { $0.text = "Bar!" }) - XCTAssertTrue(try get.status.map { $0.isOk }.wait()) - } - - func testUnaryResponseStream() { - // Enqueue a Get response stream. This will be used for the next Get RPC and we can choose when - // to send responses on it. - let stream = self.client.makeGetResponseStream() - - // Start the Get RPC. - let get = self.client.get(.with { $0.text = "Foo!" }) - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasGetResponsesRemaining) - - // Now send the response on the stream we made. - XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = "Bar!" })) - - // Check the response values: - XCTAssertEqual(try get.response.wait(), .with { $0.text = "Bar!" }) - XCTAssertTrue(try get.status.map { $0.isOk }.wait()) - } - - func testClientStreaming() { - // Enqueue a Collect response. This will be dequeued when the client makes the next Collect RPC. - // - // We can also use a response stream, see 'testClientStreamingResponseStream'. - self.client.enqueueCollectResponse(.with { $0.text = "Foo" }) - - // Start the Collect RPC. - let collect = self.client.collect() - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasCollectResponsesRemaining) - - XCTAssertEqual(try collect.response.wait(), .with { $0.text = "Foo" }) - XCTAssertTrue(try collect.status.map { $0.isOk }.wait()) - } - - func testClientStreamingResponseStream() { - // Enqueue a Collect response stream. This will be used for the next Collect RPC and we can choose when - // to send responses on it. - let stream = self.client.makeCollectResponseStream() - - // Start the Collect RPC. - let collect = self.client.collect() - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasCollectResponsesRemaining) - - // Send the response on the stream we made. - XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = "Foo" })) - - XCTAssertEqual(try collect.response.wait(), .with { $0.text = "Foo" }) - XCTAssertTrue(try collect.status.map { $0.isOk }.wait()) - } - - func testServerStreaming() { - // Enqueue some Expand responses. These will be dequeued when the client makes the next Expand RPC. - // - // We can also use a response stream, see 'testServerStreamingResponseStream'. - let fooBarBaz = ["Foo", "Bar", "Baz"] - self.client.enqueueExpandResponses(fooBarBaz.map { text in .with { $0.text = text } }) - - // Start the 'Expand' RPC. We'll create a handler which records responses. - // - // Note that in normal applications this wouldn't be thread-safe since the response handler is - // executed on a different thread; for the test client the calling thread is thread is the same - // as the tread on which the RPC is called, i.e. this thread. - var responses: [String] = [] - let expand = self.client.expand(.with { $0.text = "Hello!" }) { response in - responses.append(response.text) - } - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasExpandResponsesRemaining) - - XCTAssertTrue(try expand.status.map { $0.isOk }.wait()) - XCTAssertEqual(responses, fooBarBaz) - } - - func testServerStreamingResponseStream() { - // Enqueue an Expand response stream. This will be used for the next Expand RPC and we can choose - // when to send responses on it. - let stream = self.client.makeExpandResponseStream() - - // Start the 'Expand' RPC. We'll create a handler which records responses. - // - // Note that in normal applications this wouldn't be thread-safe since the response handler is - // executed on a different thread; for the test client the calling thread is thread is the same - // as the tread on which the RPC is called, i.e. this thread. - var responses: [String] = [] - let expand = self.client.expand(.with { $0.text = "Hello!" }) { response in - responses.append(response.text) - } - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasExpandResponsesRemaining) - - // Send some responses. - let fooBarBaz = ["Foo", "Bar", "Baz"] - for message in fooBarBaz { - XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = message })) - } - // Close the stream. - XCTAssertNoThrow(try stream.sendEnd()) - - XCTAssertTrue(try expand.status.map { $0.isOk }.wait()) - XCTAssertEqual(responses, fooBarBaz) - } - - func testBidirectionalStreaming() { - // Enqueue some Update responses. These will be dequeued when the client makes the next Update RPC. - // - // We can also use a response stream, see 'testBidirectionalStreamingResponseStream'. - let fooBarBaz = ["Foo", "Bar", "Baz"] - self.client.enqueueUpdateResponses(fooBarBaz.map { text in .with { $0.text = text } }) - - // Start the 'Update' RPC. We'll create a handler which records responses. - // - // Note that in normal applications this wouldn't be thread-safe since the response handler is - // executed on a different thread; for the test client the calling thread is thread is the same - // as the tread on which the RPC is called, i.e. this thread. - var responses: [String] = [] - let update = self.client.update { response in - responses.append(response.text) - } - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasUpdateResponsesRemaining) - - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - XCTAssertEqual(responses, fooBarBaz) - } - - func testBidirectionalStreamingResponseStream() { - // Enqueue an Update response stream. This will be used for the next Update RPC and we can choose - // when to send responses on it. - let stream = self.client.makeUpdateResponseStream() - - // Start the 'Update' RPC. We'll create a handler which records responses. - // - // Note that in normal applications this wouldn't be thread-safe since the response handler is - // executed on a different thread; for the test client the calling thread is thread is the same - // as the tread on which the RPC is called, i.e. this thread. - var responses: [String] = [] - let update = self.client.update { response in - responses.append(response.text) - } - - // The response stream has been consumed by the call. - XCTAssertFalse(self.client.hasUpdateResponsesRemaining) - - // Send some responses. - let fooBarBaz = ["Foo", "Bar", "Baz"] - for message in fooBarBaz { - XCTAssertNoThrow(try stream.sendMessage(.with { $0.text = message })) - } - // Close the stream. - XCTAssertNoThrow(try stream.sendEnd()) - - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - XCTAssertEqual(responses, fooBarBaz) - } -} - -// These tests demonstrate the finer grained control enabled by the response streams. -@available(swift, deprecated: 5.6) -extension FakeResponseStreamExampleTests { - func testUnaryWithTrailingMetadata() { - // Create a response stream for the RPC. - let getResponseStream = self.client.makeGetResponseStream() - - // Send the request. - let get = self.client.get(.with { $0.text = "Hello!" }) - - // Send the response as well as some trailing metadata. - XCTAssertNoThrow( - try getResponseStream.sendMessage( - .with { $0.text = "Goodbye!" }, - trailingMetadata: ["bar": "baz"] - ) - ) - - // Check the response values: - XCTAssertEqual(try get.response.wait(), .with { $0.text = "Goodbye!" }) - XCTAssertEqual(try get.trailingMetadata.wait(), ["bar": "baz"]) - XCTAssertTrue(try get.status.map { $0.isOk }.wait()) - } - - func testUnaryError() { - // Create a response stream for the RPC. - let getResponseStream = self.client.makeGetResponseStream() - - // Send the request. - let get = self.client.get(.with { $0.text = "Hello!" }) - - // Respond with an error. We could send trailing metadata here as well. - struct DummyError: Error {} - XCTAssertNoThrow(try getResponseStream.sendError(DummyError())) - - // Check the response values: - XCTAssertThrowsError(try get.response.wait()) { error in - XCTAssertTrue(error is DummyError) - } - - // We sent a dummy error; we could have sent a `GRPCStatus` error in which case we could assert - // for equality here. - XCTAssertFalse(try get.status.map { $0.isOk }.wait()) - } - - func testUnaryWithRequestHandler() { - // Create a response stream for the RPC we want to make, we'll specify a *request* handler as well. - let getResponseStream = self.client.makeGetResponseStream { requestPart in - switch requestPart { - case let .metadata(headers): - XCTAssertTrue(headers.contains(name: "a-test-key")) - - case let .message(request): - XCTAssertEqual(request, .with { $0.text = "Hello!" }) - - case .end: - () - } - } - - // We'll send some custom metadata for the call as well. It will be validated above. - let callOptions = CallOptions(customMetadata: ["a-test-key": "a test value"]) - let get = self.client.get(.with { $0.text = "Hello!" }, callOptions: callOptions) - - // Send the response. - XCTAssertNoThrow(try getResponseStream.sendMessage(.with { $0.text = "Goodbye!" })) - XCTAssertEqual(try get.response.wait(), .with { $0.text = "Goodbye!" }) - XCTAssertTrue(try get.status.map { $0.isOk }.wait()) - } - - func testUnaryResponseOrdering() { - // Create a response stream for the RPC we want to make. - let getResponseStream = self.client.makeGetResponseStream() - - // We can queue up the response *before* we make the RPC. - XCTAssertNoThrow(try getResponseStream.sendMessage(.with { $0.text = "Goodbye!" })) - - // Start the RPC: the response will be sent automatically. - let get = self.client.get(.with { $0.text = "Hello!" }) - - // Check the response values. - XCTAssertEqual(try get.response.wait(), .with { $0.text = "Goodbye!" }) - XCTAssertTrue(try get.status.map { $0.isOk }.wait()) - } - - func testBidirectionalResponseOrdering() { - // Create a response stream for the RPC we want to make. - let updateResponseStream = self.client.makeUpdateResponseStream() - - // We can queue up responses *before* we make the RPC. - XCTAssertNoThrow(try updateResponseStream.sendMessage(.with { $0.text = "1" })) - XCTAssertNoThrow(try updateResponseStream.sendMessage(.with { $0.text = "2" })) - - // Start the RPC: the response will be sent automatically. - var responses: [Echo_EchoResponse] = [] - let update = self.client.update { response in - responses.append(response) - } - - // We should have two responses already. - XCTAssertEqual(responses.count, 2) - - // We can also send responses after starting the RPC. - XCTAssertNoThrow(try updateResponseStream.sendMessage(.with { $0.text = "3" })) - - // And interleave requests with responses. - XCTAssertNoThrow(try update.sendMessage(.with { $0.text = "a" }).wait()) - XCTAssertNoThrow(try update.sendMessage(.with { $0.text = "b" }).wait()) - XCTAssertNoThrow(try update.sendMessage(.with { $0.text = "c" }).wait()) - XCTAssertNoThrow(try update.sendEnd().wait()) - - // Send the final response and close. - XCTAssertNoThrow(try updateResponseStream.sendMessage(.with { $0.text = "4" })) - XCTAssertNoThrow(try updateResponseStream.sendEnd()) - - // Check the response values. - let expected = (1 ... 4).map { number in - Echo_EchoResponse.with { $0.text = "\(number)" } - } - XCTAssertEqual(responses, expected) - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - } - - func testBidirectionalStreamingSendAfterEnd() { - // Enqueue the responses for Update. - self.client.enqueueUpdateResponses([.with { $0.text = "Foo" }]) - - // Start a call. - let update = self.client.update { response in - XCTAssertEqual(response, .with { $0.text = "Foo" }) - } - - // Since the RPC has already completed (the status promise has been fulfilled), send will fail. - XCTAssertThrowsError(try update.sendMessage(.with { $0.text = "Kaboom!" }).wait()) - XCTAssertThrowsError(try update.sendEnd().wait()) - - // The call completed *before* we tried to send "Kaboom!". - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - } - - func testBidirectionalWithCustomInitialMetadata() { - // Create a response stream for the RPC we want to make. - let updateResponseStream = self.client.makeUpdateResponseStream() - - // Send back some initial metadata, response, and trailers. - XCTAssertNoThrow(try updateResponseStream.sendInitialMetadata(["foo": "bar"])) - XCTAssertNoThrow(try updateResponseStream.sendMessage(.with { $0.text = "foo" })) - XCTAssertNoThrow(try updateResponseStream.sendEnd(trailingMetadata: ["bar": "baz"])) - - // Start the RPC. We only expect one response so we'll validate it in the handler. - let update = self.client.update { response in - XCTAssertEqual(response, .with { $0.text = "foo" }) - } - - // Check the rest of the response part values. - XCTAssertEqual(try update.initialMetadata.wait(), ["foo": "bar"]) - XCTAssertEqual(try update.trailingMetadata.wait(), ["bar": "baz"]) - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - } - - func testWriteAfterEndFails() { - // Create a response stream for the RPC we want to make. - let updateResponseStream = self.client.makeUpdateResponseStream() - - // Start the RPC. - let update = self.client.update { response in - XCTFail("Unexpected response: \(response)") - } - - // Send a message and end. - XCTAssertNoThrow(try update.sendMessage(.with { $0.text = "1" }).wait()) - XCTAssertNoThrow(try update.sendEnd().wait()) - - // Send another message, the write should fail. - XCTAssertThrowsError(try update.sendMessage(.with { $0.text = "Too late!" }).wait()) { error in - XCTAssertEqual(error as? ChannelError, .ioOnClosedChannel) - } - - // Send close from the server. - XCTAssertNoThrow(try updateResponseStream.sendEnd()) - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - } - - func testWeGetAllRequestParts() { - var requestParts: [FakeRequestPart] = [] - let updateResponseStream = self.client.makeUpdateResponseStream { request in - requestParts.append(request) - } - - let update = self.client.update(callOptions: CallOptions(customMetadata: ["foo": "bar"])) { - XCTFail("Unexpected response: \($0)") - } - - update.sendMessage(.with { $0.text = "foo" }, promise: nil) - update.sendEnd(promise: nil) - - // These should be ignored since we've already sent end. - update.sendMessage(.with { $0.text = "bar" }, promise: nil) - update.sendEnd(promise: nil) - - // Check the expected request parts. - XCTAssertEqual( - requestParts, - [ - .metadata(["foo": "bar"]), - .message(.with { $0.text = "foo" }), - .end, - ] - ) - - // Send close from the server. - XCTAssertNoThrow(try updateResponseStream.sendEnd()) - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - } - - func testInitialMetadataIsSentAutomatically() { - let updateResponseStream = self.client.makeUpdateResponseStream() - let update = self.client.update { response in - XCTAssertEqual(response, .with { $0.text = "foo" }) - } - - // Send a message and end. Initial metadata is explicitly not set but will be sent on our - // behalf. It will be empty. - XCTAssertNoThrow(try updateResponseStream.sendMessage(.with { $0.text = "foo" })) - XCTAssertNoThrow(try updateResponseStream.sendEnd()) - - // Metadata should be empty. - XCTAssertEqual(try update.initialMetadata.wait(), [:]) - XCTAssertTrue(try update.status.map { $0.isOk }.wait()) - } - - func testMissingResponseStream() { - // If no response stream is created for a call then it will fail with status code 'unavailable'. - let get = self.client.get(.with { $0.text = "Uh oh!" }) - - XCTAssertEqual(try get.status.map { $0.code }.wait(), .unavailable) - XCTAssertThrowsError(try get.response.wait()) { error in - guard let status = error as? GRPCStatus else { - XCTFail("Expected a GRPCStatus, had the error was: \(error)") - return - } - XCTAssertEqual(status.code, .unavailable) - } - } -} diff --git a/Tests/GRPCTests/TimeLimitTests.swift b/Tests/GRPCTests/TimeLimitTests.swift deleted file mode 100644 index 003de9272..000000000 --- a/Tests/GRPCTests/TimeLimitTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import XCTest - -@testable import GRPC - -class TimeLimitTests: GRPCTestCase { - func testTimeout() { - XCTAssertEqual(TimeLimit.timeout(.seconds(42)).timeout, .seconds(42)) - XCTAssertNil(TimeLimit.none.timeout) - XCTAssertNil(TimeLimit.deadline(.now()).timeout) - } - - func testDeadline() { - XCTAssertEqual(TimeLimit.deadline(.uptimeNanoseconds(42)).deadline, .uptimeNanoseconds(42)) - XCTAssertNil(TimeLimit.none.deadline) - XCTAssertNil(TimeLimit.timeout(.milliseconds(31415)).deadline) - } - - func testMakeDeadline() { - XCTAssertEqual(TimeLimit.none.makeDeadline(), .distantFuture) - XCTAssertEqual(TimeLimit.timeout(.nanoseconds(.max)).makeDeadline(), .distantFuture) - - let now = NIODeadline.now() - XCTAssertEqual(TimeLimit.deadline(now).makeDeadline(), now) - XCTAssertEqual(TimeLimit.deadline(.distantFuture).makeDeadline(), .distantFuture) - } -} diff --git a/Tests/GRPCTests/UnaryServerHandlerTests.swift b/Tests/GRPCTests/UnaryServerHandlerTests.swift deleted file mode 100644 index 40648e9b5..000000000 --- a/Tests/GRPCTests/UnaryServerHandlerTests.swift +++ /dev/null @@ -1,1048 +0,0 @@ -/* - * 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 NIOCore -import NIOEmbedded -import NIOHPACK -import XCTest - -@testable import GRPC - -// MARK: - Utils - -final class ResponseRecorder: GRPCServerResponseWriter { - var metadata: HPACKHeaders? - var messages: [ByteBuffer] = [] - var messageMetadata: [MessageMetadata] = [] - var status: GRPCStatus? - var trailers: HPACKHeaders? - - func sendMetadata(_ metadata: HPACKHeaders, flush: Bool, promise: EventLoopPromise?) { - XCTAssertNil(self.metadata) - self.metadata = metadata - promise?.succeed(()) - self.recordedMetadataPromise.succeed(()) - } - - func sendMessage( - _ bytes: ByteBuffer, - metadata: MessageMetadata, - promise: EventLoopPromise? - ) { - self.messages.append(bytes) - self.messageMetadata.append(metadata) - promise?.succeed(()) - self.recordedMessagePromise.succeed(()) - } - - func sendEnd(status: GRPCStatus, trailers: HPACKHeaders, promise: EventLoopPromise?) { - XCTAssertNil(self.status) - XCTAssertNil(self.trailers) - self.status = status - self.trailers = trailers - promise?.succeed(()) - self.recordedEndPromise.succeed(()) - } - - var recordedMetadataPromise: EventLoopPromise - var recordedMessagePromise: EventLoopPromise - var recordedEndPromise: EventLoopPromise - - init(eventLoop: EventLoop) { - self.recordedMetadataPromise = eventLoop.makePromise() - self.recordedMessagePromise = eventLoop.makePromise() - self.recordedEndPromise = eventLoop.makePromise() - } - - deinit { - struct RecordedDidNotIntercept: Error {} - self.recordedMetadataPromise.fail(RecordedDidNotIntercept()) - self.recordedMessagePromise.fail(RecordedDidNotIntercept()) - self.recordedEndPromise.fail(RecordedDidNotIntercept()) - } -} - -class ServerHandlerTestCaseBase: GRPCTestCase { - let eventLoop = EmbeddedEventLoop() - let allocator = ByteBufferAllocator() - var recorder: ResponseRecorder! - - override func setUp() { - super.setUp() - self.recorder = ResponseRecorder(eventLoop: self.eventLoop) - } - - func makeCallHandlerContext(encoding: ServerMessageEncoding = .disabled) -> CallHandlerContext { - return CallHandlerContext( - errorDelegate: nil, - logger: self.logger, - encoding: encoding, - eventLoop: self.eventLoop, - path: "/ignored", - remoteAddress: nil, - responseWriter: self.recorder, - allocator: self.allocator, - closeFuture: self.eventLoop.makeSucceededVoidFuture() - ) - } -} - -// MARK: - Unary - -class UnaryServerHandlerTests: ServerHandlerTestCaseBase { - private func makeHandler( - encoding: ServerMessageEncoding = .disabled, - function: @escaping (String, StatusOnlyCallContext) -> EventLoopFuture - ) -> UnaryServerHandler { - return UnaryServerHandler( - context: self.makeCallHandlerContext(encoding: encoding), - requestDeserializer: StringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - userFunction: function - ) - } - - private func echo(_ request: String, context: StatusOnlyCallContext) -> EventLoopFuture { - return context.eventLoop.makeSucceededFuture(request) - } - - private func neverComplete( - _ request: String, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - let scheduled = context.eventLoop.scheduleTask(deadline: .distantFuture) { - return request - } - return scheduled.futureResult - } - - private func neverCalled( - _ request: String, - context: StatusOnlyCallContext - ) -> EventLoopFuture { - XCTFail("Unexpected function invocation") - return context.eventLoop.makeFailedFuture(GRPCError.InvalidState("")) - } - - func testHappyPath() { - let handler = self.makeHandler(function: self.echo(_:context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - handler.receiveEnd() - handler.finish() - - assertThat(self.recorder.messages.first, .is(buffer)) - assertThat(self.recorder.messageMetadata.first?.compress, .is(false)) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testHappyPathWithCompressionEnabled() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))), - function: self.echo(_:context:) - ) - - handler.receiveMetadata([:]) - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages.first, .is(buffer)) - assertThat(self.recorder.messageMetadata.first?.compress, .is(true)) - } - - func testHappyPathWithCompressionEnabledButDisabledByCaller() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))) - ) { request, context in - context.compressionEnabled = false - return self.echo(request, context: context) - } - - handler.receiveMetadata([:]) - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages.first, .is(buffer)) - assertThat(self.recorder.messageMetadata.first?.compress, .is(false)) - } - - func testThrowingDeserializer() { - let handler = UnaryServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: ThrowingStringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - userFunction: self.neverCalled(_:context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testThrowingSerializer() { - let handler = UnaryServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: StringDeserializer(), - responseSerializer: ThrowingStringSerializer(), - interceptors: [], - userFunction: self.echo(_:context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - handler.receiveEnd() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testUserFunctionReturnsFailedFuture() { - let handler = self.makeHandler { _, context in - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unavailable, message: ":(")) - } - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.status?.message, .is(":(")) - } - - func testReceiveMessageBeforeHeaders() { - let handler = self.makeHandler(function: self.neverCalled(_:context:)) - - handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testReceiveMultipleHeaders() { - let handler = self.makeHandler(function: self.neverCalled(_:context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.receiveMetadata([:]) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testReceiveMultipleMessages() { - let handler = self.makeHandler(function: self.neverComplete(_:context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - handler.receiveEnd() - // Send another message before the function completes. - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testFinishBeforeStarting() { - let handler = self.makeHandler(function: self.neverCalled(_:context:)) - - handler.finish() - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.none())) - assertThat(self.recorder.trailers, .is(.none())) - } - - func testFinishAfterHeaders() { - let handler = self.makeHandler(function: self.neverCalled(_:context:)) - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.finish() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testFinishAfterMessage() { - let handler = self.makeHandler(function: self.neverComplete(_:context:)) - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - handler.finish() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } -} - -// MARK: - Client Streaming - -class ClientStreamingServerHandlerTests: ServerHandlerTestCaseBase { - private func makeHandler( - encoding: ServerMessageEncoding = .disabled, - observerFactory: @escaping (UnaryResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> - ) -> ClientStreamingServerHandler { - return ClientStreamingServerHandler( - context: self.makeCallHandlerContext(encoding: encoding), - requestDeserializer: StringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - observerFactory: observerFactory - ) - } - - private func joinWithSpaces( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - var messages: [String] = [] - func onEvent(_ event: StreamEvent) { - switch event { - case let .message(message): - messages.append(message) - case .end: - context.responsePromise.succeed(messages.joined(separator: " ")) - } - } - return context.eventLoop.makeSucceededFuture(onEvent(_:)) - } - - private func neverReceivesMessage( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - func onEvent(_ event: StreamEvent) { - switch event { - case let .message(message): - XCTFail("Unexpected message: '\(message)'") - case .end: - context.responsePromise.succeed("") - } - } - return context.eventLoop.makeSucceededFuture(onEvent(_:)) - } - - private func neverCalled( - context: UnaryResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - XCTFail("This observer factory should never be called") - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .aborted, message: nil)) - } - - func testHappyPath() { - let handler = self.makeHandler(observerFactory: self.joinWithSpaces(context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - handler.finish() - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3"))) - assertThat(self.recorder.messageMetadata.first?.compress, .is(false)) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testHappyPathWithCompressionEnabled() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))), - observerFactory: self.joinWithSpaces(context:) - ) - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3"))) - assertThat(self.recorder.messageMetadata.first?.compress, .is(true)) - } - - func testHappyPathWithCompressionEnabledButDisabledByCaller() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))) - ) { context in - context.compressionEnabled = false - return self.joinWithSpaces(context: context) - } - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3"))) - assertThat(self.recorder.messageMetadata.first?.compress, .is(false)) - } - - func testThrowingDeserializer() { - let handler = ClientStreamingServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: ThrowingStringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - observerFactory: self.neverReceivesMessage(context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testThrowingSerializer() { - let handler = ClientStreamingServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: StringDeserializer(), - responseSerializer: ThrowingStringSerializer(), - interceptors: [], - observerFactory: self.joinWithSpaces(context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - handler.receiveEnd() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testObserverFactoryReturnsFailedFuture() { - let handler = self.makeHandler { context in - context.eventLoop.makeFailedFuture(GRPCStatus(code: .unavailable, message: ":(")) - } - - handler.receiveMetadata([:]) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.status?.message, .is(":(")) - } - - func testDelayedObserverFactory() { - let promise = self.eventLoop.makePromise(of: Void.self) - let handler = self.makeHandler { context in - return promise.futureResult.flatMap { - self.joinWithSpaces(context: context) - } - } - - handler.receiveMetadata([:]) - // Queue up some messages. - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - // Succeed the observer block. - promise.succeed(()) - // A few more messages. - handler.receiveMessage(ByteBuffer(string: "4")) - handler.receiveMessage(ByteBuffer(string: "5")) - handler.receiveEnd() - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3 4 5"))) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - } - - func testDelayedObserverFactoryAllMessagesBeforeSucceeding() { - let promise = self.eventLoop.makePromise(of: Void.self) - let handler = self.makeHandler { context in - return promise.futureResult.flatMap { - self.joinWithSpaces(context: context) - } - } - - handler.receiveMetadata([:]) - // Queue up some messages. - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - // Succeed the observer block. - promise.succeed(()) - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "1 2 3"))) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - } - - func testReceiveMessageBeforeHeaders() { - let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) - - handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testReceiveMultipleHeaders() { - let handler = self.makeHandler(observerFactory: self.neverReceivesMessage(context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.receiveMetadata([:]) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testFinishBeforeStarting() { - let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) - - handler.finish() - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.none())) - assertThat(self.recorder.trailers, .is(.none())) - } - - func testFinishAfterHeaders() { - let handler = self.makeHandler(observerFactory: self.joinWithSpaces(context:)) - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.finish() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testFinishAfterMessage() { - let handler = self.makeHandler(observerFactory: self.joinWithSpaces(context:)) - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - handler.finish() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } -} - -class ServerStreamingServerHandlerTests: ServerHandlerTestCaseBase { - private func makeHandler( - encoding: ServerMessageEncoding = .disabled, - userFunction: @escaping (String, StreamingResponseCallContext) - -> EventLoopFuture - ) -> ServerStreamingServerHandler { - return ServerStreamingServerHandler( - context: self.makeCallHandlerContext(encoding: encoding), - requestDeserializer: StringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - userFunction: userFunction - ) - } - - private func breakOnSpaces( - _ request: String, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - let parts = request.components(separatedBy: " ") - context.sendResponses(parts, promise: nil) - return context.eventLoop.makeSucceededFuture(.ok) - } - - private func neverCalled( - _ request: String, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - XCTFail("Unexpected invocation") - return context.eventLoop.makeSucceededFuture(.processingError) - } - - private func neverComplete( - _ request: String, - context: StreamingResponseCallContext - ) -> EventLoopFuture { - return context.eventLoop.scheduleTask(deadline: .distantFuture) { - return .processingError - }.futureResult - } - - func testHappyPath() { - let handler = self.makeHandler(userFunction: self.breakOnSpaces(_:context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.receiveMessage(ByteBuffer(string: "a b")) - handler.receiveEnd() - handler.finish() - - assertThat( - self.recorder.messages, - .is([ByteBuffer(string: "a"), ByteBuffer(string: "b")]) - ) - assertThat(self.recorder.messageMetadata.map { $0.compress }, .is([false, false])) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testHappyPathWithCompressionEnabled() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))), - userFunction: self.breakOnSpaces(_:context:) - ) - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "a")) - handler.receiveEnd() - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "a"))) - assertThat(self.recorder.messageMetadata.first?.compress, .is(true)) - } - - func testHappyPathWithCompressionEnabledButDisabledByCaller() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))) - ) { request, context in - context.compressionEnabled = false - return self.breakOnSpaces(request, context: context) - } - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "a")) - handler.receiveEnd() - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "a"))) - assertThat(self.recorder.messageMetadata.first?.compress, .is(false)) - } - - func testThrowingDeserializer() { - let handler = ServerStreamingServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: ThrowingStringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - userFunction: self.neverCalled(_:context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testThrowingSerializer() { - let handler = ServerStreamingServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: StringDeserializer(), - responseSerializer: ThrowingStringSerializer(), - interceptors: [], - userFunction: self.breakOnSpaces(_:context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "1 2 3") - handler.receiveMessage(buffer) - handler.receiveEnd() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testUserFunctionReturnsFailedFuture() { - let handler = self.makeHandler { _, context in - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unavailable, message: ":(")) - } - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.status?.message, .is(":(")) - } - - func testReceiveMessageBeforeHeaders() { - let handler = self.makeHandler(userFunction: self.neverCalled(_:context:)) - - handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testReceiveMultipleHeaders() { - let handler = self.makeHandler(userFunction: self.neverCalled(_:context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.receiveMetadata([:]) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testReceiveMultipleMessages() { - let handler = self.makeHandler(userFunction: self.neverComplete(_:context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - handler.receiveEnd() - // Send another message before the function completes. - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testFinishBeforeStarting() { - let handler = self.makeHandler(userFunction: self.neverCalled(_:context:)) - - handler.finish() - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.none())) - assertThat(self.recorder.trailers, .is(.none())) - } - - func testFinishAfterHeaders() { - let handler = self.makeHandler(userFunction: self.neverCalled(_:context:)) - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.finish() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testFinishAfterMessage() { - let handler = self.makeHandler(userFunction: self.neverComplete(_:context:)) - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - handler.finish() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } -} - -// MARK: - Bidirectional Streaming - -class BidirectionalStreamingServerHandlerTests: ServerHandlerTestCaseBase { - private func makeHandler( - encoding: ServerMessageEncoding = .disabled, - observerFactory: @escaping (StreamingResponseCallContext) - -> EventLoopFuture<(StreamEvent) -> Void> - ) -> BidirectionalStreamingServerHandler { - return BidirectionalStreamingServerHandler( - context: self.makeCallHandlerContext(encoding: encoding), - requestDeserializer: StringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - observerFactory: observerFactory - ) - } - - private func echo( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - func onEvent(_ event: StreamEvent) { - switch event { - case let .message(message): - context.sendResponse(message, promise: nil) - case .end: - context.statusPromise.succeed(.ok) - } - } - return context.eventLoop.makeSucceededFuture(onEvent(_:)) - } - - private func neverReceivesMessage( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - func onEvent(_ event: StreamEvent) { - switch event { - case let .message(message): - XCTFail("Unexpected message: '\(message)'") - case .end: - context.statusPromise.succeed(.ok) - } - } - return context.eventLoop.makeSucceededFuture(onEvent(_:)) - } - - private func neverCalled( - context: StreamingResponseCallContext - ) -> EventLoopFuture<(StreamEvent) -> Void> { - XCTFail("This observer factory should never be called") - return context.eventLoop.makeFailedFuture(GRPCStatus(code: .aborted, message: nil)) - } - - func testHappyPath() { - let handler = self.makeHandler(observerFactory: self.echo(context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - handler.finish() - - assertThat( - self.recorder.messages, - .is([ByteBuffer(string: "1"), ByteBuffer(string: "2"), ByteBuffer(string: "3")]) - ) - assertThat(self.recorder.messageMetadata.map { $0.compress }, .is([false, false, false])) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testHappyPathWithCompressionEnabled() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))), - observerFactory: self.echo(context:) - ) - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - - assertThat( - self.recorder.messages, - .is([ByteBuffer(string: "1"), ByteBuffer(string: "2"), ByteBuffer(string: "3")]) - ) - assertThat(self.recorder.messageMetadata.map { $0.compress }, .is([true, true, true])) - } - - func testHappyPathWithCompressionEnabledButDisabledByCaller() { - let handler = self.makeHandler( - encoding: .enabled(.init(decompressionLimit: .absolute(.max))) - ) { context in - context.compressionEnabled = false - return self.echo(context: context) - } - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveMessage(ByteBuffer(string: "3")) - handler.receiveEnd() - - assertThat( - self.recorder.messages, - .is([ByteBuffer(string: "1"), ByteBuffer(string: "2"), ByteBuffer(string: "3")]) - ) - assertThat(self.recorder.messageMetadata.map { $0.compress }, .is([false, false, false])) - } - - func testThrowingDeserializer() { - let handler = BidirectionalStreamingServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: ThrowingStringDeserializer(), - responseSerializer: StringSerializer(), - interceptors: [], - observerFactory: self.neverReceivesMessage(context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testThrowingSerializer() { - let handler = BidirectionalStreamingServerHandler( - context: self.makeCallHandlerContext(), - requestDeserializer: StringDeserializer(), - responseSerializer: ThrowingStringSerializer(), - interceptors: [], - observerFactory: self.echo(context:) - ) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - let buffer = ByteBuffer(string: "hello") - handler.receiveMessage(buffer) - handler.receiveEnd() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testObserverFactoryReturnsFailedFuture() { - let handler = self.makeHandler { context in - context.eventLoop.makeFailedFuture(GRPCStatus(code: .unavailable, message: ":(")) - } - - handler.receiveMetadata([:]) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.status?.message, .is(":(")) - } - - func testDelayedObserverFactory() { - let promise = self.eventLoop.makePromise(of: Void.self) - let handler = self.makeHandler { context in - return promise.futureResult.flatMap { - self.echo(context: context) - } - } - - handler.receiveMetadata([:]) - // Queue up some messages. - handler.receiveMessage(ByteBuffer(string: "1")) - // Succeed the observer block. - promise.succeed(()) - // A few more messages. - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveEnd() - - assertThat( - self.recorder.messages, - .is([ByteBuffer(string: "1"), ByteBuffer(string: "2")]) - ) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - } - - func testDelayedObserverFactoryAllMessagesBeforeSucceeding() { - let promise = self.eventLoop.makePromise(of: Void.self) - let handler = self.makeHandler { context in - return promise.futureResult.flatMap { - self.echo(context: context) - } - } - - handler.receiveMetadata([:]) - // Queue up some messages. - handler.receiveMessage(ByteBuffer(string: "1")) - handler.receiveMessage(ByteBuffer(string: "2")) - handler.receiveEnd() - // Succeed the observer block. - promise.succeed(()) - - assertThat( - self.recorder.messages, - .is([ByteBuffer(string: "1"), ByteBuffer(string: "2")]) - ) - assertThat(self.recorder.status, .some(.hasCode(.ok))) - } - - func testReceiveMessageBeforeHeaders() { - let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) - - handler.receiveMessage(ByteBuffer(string: "foo")) - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testReceiveMultipleHeaders() { - let handler = self.makeHandler(observerFactory: self.neverReceivesMessage(context:)) - - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.receiveMetadata([:]) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.internalError))) - } - - func testFinishBeforeStarting() { - let handler = self.makeHandler(observerFactory: self.neverCalled(context:)) - - handler.finish() - assertThat(self.recorder.metadata, .is(.none())) - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .is(.none())) - assertThat(self.recorder.trailers, .is(.none())) - } - - func testFinishAfterHeaders() { - let handler = self.makeHandler(observerFactory: self.echo(context:)) - handler.receiveMetadata([:]) - assertThat(self.recorder.metadata, .is([:])) - - handler.finish() - - assertThat(self.recorder.messages, .isEmpty()) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } - - func testFinishAfterMessage() { - let handler = self.makeHandler(observerFactory: self.echo(context:)) - - handler.receiveMetadata([:]) - handler.receiveMessage(ByteBuffer(string: "hello")) - handler.finish() - - assertThat(self.recorder.messages.first, .is(ByteBuffer(string: "hello"))) - assertThat(self.recorder.status, .some(.hasCode(.unavailable))) - assertThat(self.recorder.trailers, .is([:])) - } -} diff --git a/Tests/GRPCTests/UserInfoTests.swift b/Tests/GRPCTests/UserInfoTests.swift deleted file mode 100644 index e9546ecd9..000000000 --- a/Tests/GRPCTests/UserInfoTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2020, 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 GRPC - -class UserInfoTests: GRPCTestCase { - func testWithSubscript() { - var userInfo = UserInfo() - - userInfo[FooKey.self] = "foo" - assertThat(userInfo[FooKey.self], .is("foo")) - - userInfo[BarKey.self] = 42 - assertThat(userInfo[BarKey.self], .is(42)) - - userInfo[FooKey.self] = nil - assertThat(userInfo[FooKey.self], .is(.none())) - - userInfo[BarKey.self] = nil - assertThat(userInfo[BarKey.self], .is(.none())) - } - - func testWithExtensions() { - var userInfo = UserInfo() - - userInfo.foo = "foo" - assertThat(userInfo.foo, .is("foo")) - - userInfo.bar = 42 - assertThat(userInfo.bar, .is(42)) - - userInfo.foo = nil - assertThat(userInfo.foo, .is(.none())) - - userInfo.bar = nil - assertThat(userInfo.bar, .is(.none())) - } - - func testDescription() { - var userInfo = UserInfo() - assertThat(String(describing: userInfo), .is("[]")) - - // (We can't test with multiple values since ordering isn't stable.) - userInfo.foo = "foo" - assertThat(String(describing: userInfo), .is("[FooKey: foo]")) - } -} - -private enum FooKey: UserInfoKey { - typealias Value = String -} - -private enum BarKey: UserInfoKey { - typealias Value = Int -} - -extension UserInfo { - fileprivate var foo: FooKey.Value? { - get { - return self[FooKey.self] - } - set { - self[FooKey.self] = newValue - } - } - - fileprivate var bar: BarKey.Value? { - get { - return self[BarKey.self] - } - set { - self[BarKey.self] = newValue - } - } -} diff --git a/Tests/GRPCTests/VsockSocketTests.swift b/Tests/GRPCTests/VsockSocketTests.swift deleted file mode 100644 index f9bc09c30..000000000 --- a/Tests/GRPCTests/VsockSocketTests.swift +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2022, 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 EchoImplementation -import EchoModel -import GRPC -import NIOPosix -import XCTest - -class VsockSocketTests: GRPCTestCase { - func testVsockSocket() throws { - try XCTSkipUnless(self.vsockAvailable(), "Vsock unavailable") - let group = NIOPosix.MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - // Setup a server. - let server = try Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(vsockAddress: .init(cid: .any, port: 31234)) - .wait() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - let channel = try GRPCChannelPool.with( - target: .vsockAddress(.init(cid: .local, port: 31234)), - transportSecurity: .plaintext, - eventLoopGroup: group - ) - defer { - XCTAssertNoThrow(try channel.close().wait()) - } - - let client = Echo_EchoNIOClient(channel: channel) - let resp = try client.get(Echo_EchoRequest(text: "Hello")).response.wait() - XCTAssertEqual(resp.text, "Swift echo get: Hello") - } - - private func vsockAvailable() -> Bool { - let fd: CInt - #if os(Linux) - fd = socket(AF_VSOCK, CInt(SOCK_STREAM.rawValue), 0) - #elseif canImport(Darwin) - fd = socket(AF_VSOCK, SOCK_STREAM, 0) - #else - fd = -1 - #endif - if fd == -1 { return false } - precondition(close(fd) == 0) - return true - } -} diff --git a/Tests/GRPCTests/WebCORSHandlerTests.swift b/Tests/GRPCTests/WebCORSHandlerTests.swift deleted file mode 100644 index 7a347b51c..000000000 --- a/Tests/GRPCTests/WebCORSHandlerTests.swift +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright 2023, 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 NIOCore -import NIOEmbedded -import NIOHTTP1 -import XCTest - -@testable import GRPC - -internal final class WebCORSHandlerTests: XCTestCase { - struct PreflightRequestSpec { - var configuration: Server.Configuration.CORS - var requestOrigin: Optional - var expectOrigin: Optional - var expectAllowedHeaders: [String] - var expectAllowCredentials: Bool - var expectMaxAge: Optional - var expectStatus: HTTPResponseStatus = .ok - } - - func runPreflightRequestTest(spec: PreflightRequestSpec) throws { - let channel = EmbeddedChannel(handler: WebCORSHandler(configuration: spec.configuration)) - - var request = HTTPRequestHead(version: .http1_1, method: .OPTIONS, uri: "http://foo.example") - if let origin = spec.requestOrigin { - request.headers.add(name: "origin", value: origin) - } - request.headers.add(name: "access-control-request-method", value: "POST") - try channel.writeRequestPart(.head(request)) - try channel.writeRequestPart(.end(nil)) - - switch try channel.readResponsePart() { - case let .head(response): - XCTAssertEqual(response.version, request.version) - - if let expected = spec.expectOrigin { - XCTAssertEqual(response.headers["access-control-allow-origin"], [expected]) - } else { - XCTAssertFalse(response.headers.contains(name: "access-control-allow-origin")) - } - - if spec.expectAllowedHeaders.isEmpty { - XCTAssertFalse(response.headers.contains(name: "access-control-allow-headers")) - } else { - XCTAssertEqual(response.headers["access-control-allow-headers"], spec.expectAllowedHeaders) - } - - if spec.expectAllowCredentials { - XCTAssertEqual(response.headers["access-control-allow-credentials"], ["true"]) - } else { - XCTAssertFalse(response.headers.contains(name: "access-control-allow-credentials")) - } - - if let maxAge = spec.expectMaxAge { - XCTAssertEqual(response.headers["access-control-max-age"], [maxAge]) - } else { - XCTAssertFalse(response.headers.contains(name: "access-control-max-age")) - } - - XCTAssertEqual(response.status, spec.expectStatus) - - case .body, .end, .none: - XCTFail("Unexpected response part") - } - } - - func testOptionsPreflightAllowAllOrigins() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowedHeaders: ["x-grpc-web"], - allowCredentialedRequests: false, - preflightCacheExpiration: 60 - ), - requestOrigin: "foo", - expectOrigin: "*", - expectAllowedHeaders: ["x-grpc-web"], - expectAllowCredentials: false, - expectMaxAge: "60" - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightOriginBased() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .originBased, - allowedHeaders: ["x-grpc-web"], - allowCredentialedRequests: false, - preflightCacheExpiration: 60 - ), - requestOrigin: "foo", - expectOrigin: "foo", - expectAllowedHeaders: ["x-grpc-web"], - expectAllowCredentials: false, - expectMaxAge: "60" - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightCustom() throws { - struct Wrapper: GRPCCustomCORSAllowedOrigin { - func check(origin: String) -> String? { - if origin == "foo" { - return "bar" - } else { - return nil - } - } - } - - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .custom(Wrapper()), - allowedHeaders: ["x-grpc-web"], - allowCredentialedRequests: false, - preflightCacheExpiration: 60 - ), - requestOrigin: "foo", - expectOrigin: "bar", - expectAllowedHeaders: ["x-grpc-web"], - expectAllowCredentials: false, - expectMaxAge: "60" - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightAllowSomeOrigins() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .only(["bar", "foo"]), - allowedHeaders: ["x-grpc-web"], - allowCredentialedRequests: false, - preflightCacheExpiration: 60 - ), - requestOrigin: "foo", - expectOrigin: "foo", - expectAllowedHeaders: ["x-grpc-web"], - expectAllowCredentials: false, - expectMaxAge: "60" - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightAllowNoHeaders() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowedHeaders: [], - allowCredentialedRequests: false, - preflightCacheExpiration: 60 - ), - requestOrigin: "foo", - expectOrigin: "*", - expectAllowedHeaders: [], - expectAllowCredentials: false, - expectMaxAge: "60" - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightNoMaxAge() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowedHeaders: [], - allowCredentialedRequests: false, - preflightCacheExpiration: 0 - ), - requestOrigin: "foo", - expectOrigin: "*", - expectAllowedHeaders: [], - expectAllowCredentials: false, - expectMaxAge: nil - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightNegativeMaxAge() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowedHeaders: [], - allowCredentialedRequests: false, - preflightCacheExpiration: -1 - ), - requestOrigin: "foo", - expectOrigin: "*", - expectAllowedHeaders: [], - expectAllowCredentials: false, - expectMaxAge: nil - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightWithCredentials() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowedHeaders: [], - allowCredentialedRequests: true, - preflightCacheExpiration: 60 - ), - requestOrigin: "foo", - expectOrigin: "*", - expectAllowedHeaders: [], - expectAllowCredentials: true, - expectMaxAge: "60" - ) - try self.runPreflightRequestTest(spec: spec) - } - - func testOptionsPreflightWithDisallowedOrigin() throws { - let spec = PreflightRequestSpec( - configuration: .init( - allowedOrigins: .only(["foo"]), - allowedHeaders: [], - allowCredentialedRequests: false, - preflightCacheExpiration: 60 - ), - requestOrigin: "bar", - expectOrigin: nil, - expectAllowedHeaders: [], - expectAllowCredentials: false, - expectMaxAge: nil, - expectStatus: .forbidden - ) - try self.runPreflightRequestTest(spec: spec) - } -} - -extension WebCORSHandlerTests { - struct RegularRequestSpec { - var configuration: Server.Configuration.CORS - var requestOrigin: Optional - var expectOrigin: Optional - var expectAllowCredentials: Bool - } - - func runRegularRequestTest( - spec: RegularRequestSpec - ) throws { - let channel = EmbeddedChannel(handler: WebCORSHandler(configuration: spec.configuration)) - - var request = HTTPRequestHead(version: .http1_1, method: .OPTIONS, uri: "http://foo.example") - if let origin = spec.requestOrigin { - request.headers.add(name: "origin", value: origin) - } - - try channel.writeRequestPart(.head(request)) - try channel.writeRequestPart(.end(nil)) - XCTAssertEqual(try channel.readRequestPart(), .head(request)) - XCTAssertEqual(try channel.readRequestPart(), .end(nil)) - - let response = HTTPResponseHead(version: request.version, status: .imATeapot) - try channel.writeResponsePart(.head(response)) - try channel.writeResponsePart(.end(nil)) - - switch try channel.readResponsePart() { - case let .head(head): - // Should not be modified. - XCTAssertEqual(head.version, response.version) - XCTAssertEqual(head.status, response.status) - - if let expected = spec.expectOrigin { - XCTAssertEqual(head.headers["access-control-allow-origin"], [expected]) - } else { - XCTAssertFalse(head.headers.contains(name: "access-control-allow-origin")) - } - - if spec.expectAllowCredentials { - XCTAssertEqual(head.headers["access-control-allow-credentials"], ["true"]) - } else { - XCTAssertFalse(head.headers.contains(name: "access-control-allow-credentials")) - } - - case .body, .end, .none: - XCTFail("Unexpected response part") - } - - XCTAssertEqual(try channel.readResponsePart(), .end(nil)) - } - - func testRegularRequestWithWildcardOrigin() throws { - let spec = RegularRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowCredentialedRequests: false - ), - requestOrigin: "foo", - expectOrigin: "*", - expectAllowCredentials: false - ) - try self.runRegularRequestTest(spec: spec) - } - - func testRegularRequestWithLimitedOrigin() throws { - let spec = RegularRequestSpec( - configuration: .init( - allowedOrigins: .only(["foo", "bar"]), - allowCredentialedRequests: false - ), - requestOrigin: "foo", - expectOrigin: "foo", - expectAllowCredentials: false - ) - try self.runRegularRequestTest(spec: spec) - } - - func testRegularRequestWithNoOrigin() throws { - let spec = RegularRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowCredentialedRequests: false - ), - requestOrigin: nil, - expectOrigin: nil, - expectAllowCredentials: false - ) - try self.runRegularRequestTest(spec: spec) - } - - func testRegularRequestWithCredentials() throws { - let spec = RegularRequestSpec( - configuration: .init( - allowedOrigins: .all, - allowCredentialedRequests: true - ), - requestOrigin: "foo", - expectOrigin: "*", - expectAllowCredentials: true - ) - try self.runRegularRequestTest(spec: spec) - } - - func testRegularRequestWithDisallowedOrigin() throws { - let spec = RegularRequestSpec( - configuration: .init( - allowedOrigins: .only(["foo"]), - allowCredentialedRequests: true - ), - requestOrigin: "bar", - expectOrigin: nil, - expectAllowCredentials: false - ) - try self.runRegularRequestTest(spec: spec) - } -} - -extension EmbeddedChannel { - fileprivate func writeRequestPart(_ part: HTTPServerRequestPart) throws { - try self.writeInbound(part) - } - - fileprivate func readRequestPart() throws -> HTTPServerRequestPart? { - try self.readInbound() - } - - fileprivate func writeResponsePart(_ part: HTTPServerResponsePart) throws { - try self.writeOutbound(part) - } - - fileprivate func readResponsePart() throws -> HTTPServerResponsePart? { - try self.readOutbound() - } -} diff --git a/Tests/GRPCTests/WithConnectedSocketTests.swift b/Tests/GRPCTests/WithConnectedSocketTests.swift deleted file mode 100644 index a4af27c77..000000000 --- a/Tests/GRPCTests/WithConnectedSocketTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2022, 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 EchoImplementation -import EchoModel -import NIOCore -import NIOPosix -import XCTest - -@testable import GRPC - -class WithConnectedSockettests: GRPCTestCase { - func testWithConnectedSocket() throws { - let group = NIOPosix.MultiThreadedEventLoopGroup(numberOfThreads: 1) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let path = "/tmp/grpc-\(getpid()).sock" - // Setup a server. - let server = try Server.insecure(group: group) - .withServiceProviders([EchoProvider()]) - .withLogger(self.serverLogger) - .bind(unixDomainSocketPath: path) - .wait() - defer { - XCTAssertNoThrow(try server.close().wait()) - } - - #if os(Linux) - let sockStream = CInt(SOCK_STREAM.rawValue) - #else - let sockStream = SOCK_STREAM - #endif - let clientSocket = socket(AF_UNIX, sockStream, 0) - - XCTAssert(clientSocket != -1) - let addr = try SocketAddress(unixDomainSocketPath: path) - addr.withSockAddr { addr, size in - let ret = connect(clientSocket, addr, UInt32(size)) - XCTAssert(ret != -1) - } - let flags = fcntl(clientSocket, F_GETFL, 0) - XCTAssert(flags != -1) - XCTAssert(fcntl(clientSocket, F_SETFL, flags | O_NONBLOCK) == 0) - - let connection = ClientConnection.insecure(group: group) - .withBackgroundActivityLogger(self.clientLogger) - .withConnectedSocket(clientSocket) - defer { - XCTAssertNoThrow(try connection.close().wait()) - } - - let client = Echo_EchoNIOClient(channel: connection) - let resp = try client.get(Echo_EchoRequest(text: "Hello")).response.wait() - XCTAssertEqual(resp.text, "Swift echo get: Hello") - } -} diff --git a/Tests/GRPCTests/XCTestHelpers.swift b/Tests/GRPCTests/XCTestHelpers.swift deleted file mode 100644 index 0fcdd9747..000000000 --- a/Tests/GRPCTests/XCTestHelpers.swift +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import NIOHPACK -import NIOHTTP1 -import NIOHTTP2 -import XCTest - -@testable import GRPC - -struct UnwrapError: Error {} - -// We support Swift versions before 'XCTUnwrap' was introduced. -func assertNotNil( - _ expression: @autoclosure () throws -> Value?, - message: @autoclosure () -> String = "Optional value was nil", - file: StaticString = #filePath, - line: UInt = #line -) throws -> Value { - guard let value = try expression() else { - XCTFail(message(), file: file, line: line) - throw UnwrapError() - } - return value -} - -func assertNoThrow( - _ expression: @autoclosure () throws -> Value, - message: @autoclosure () -> String = "Unexpected error thrown", - file: StaticString = #filePath, - line: UInt = #line -) throws -> Value { - do { - return try expression() - } catch { - XCTFail(message(), file: file, line: line) - throw error - } -} - -// MARK: - Matchers. - -// The Swift 5.2 compiler will crash when trying to -// inline this function if the tests are running in -// release mode. -@inline(never) -func assertThat( - _ expression: @autoclosure @escaping () throws -> Value, - _ matcher: Matcher, - file: StaticString = #filePath, - line: UInt = #line -) { - // For value matchers we'll assert that we don't throw by default. - assertThat(try expression(), .doesNotThrow(matcher), file: file, line: line) -} - -func assertThat( - _ expression: @autoclosure @escaping () throws -> Value, - _ matcher: ExpressionMatcher, - file: StaticString = #filePath, - line: UInt = #line -) { - switch matcher.evaluate(expression) { - case .match: - () - case let .noMatch(actual: actual, expected: expected): - XCTFail("ACTUAL: \(actual), EXPECTED: \(expected)", file: file, line: line) - } -} - -enum MatchResult { - case match - case noMatch(actual: String, expected: String) -} - -struct Matcher { - fileprivate typealias Evaluator = (Value) -> MatchResult - private var matcher: Evaluator - - fileprivate init(_ matcher: @escaping Evaluator) { - self.matcher = matcher - } - - fileprivate func evaluate(_ value: Value) -> MatchResult { - return self.matcher(value) - } - - // MARK: Sugar - - /// Just returns the provided matcher. - static func `is`(_ matcher: Matcher) -> Matcher { - return matcher - } - - /// Just returns the provided matcher. - static func and(_ matcher: Matcher) -> Matcher { - return matcher - } - - // MARK: Equality - - /// Checks the equality of the actual value against the provided value. See `equalTo(_:)`. - static func `is`(_ value: V) -> Matcher { - return .equalTo(value) - } - - /// Checks the equality of the actual value against the provided value. - static func equalTo(_ expected: V) -> Matcher { - return .init { actual in - actual == expected - ? .match - : .noMatch(actual: "\(actual)", expected: "equal to \(expected)") - } - } - - /// Always returns a 'match', useful when the expected value is `Void`. - static func isVoid() -> Matcher { - return .init { - return .match - } - } - - /// Matches if the value is `nil`. - static func none() -> Matcher { - return .init { actual in - actual == nil - ? .match - : .noMatch(actual: String(describing: actual), expected: "nil") - } - } - - /// Matches if the value is not `nil`. - static func some(_ matcher: Matcher? = nil) -> Matcher { - return .init { actual in - if let actual = actual { - return matcher?.evaluate(actual) ?? .match - } else { - return .noMatch(actual: "nil", expected: "not nil") - } - } - } - - // MARK: Result - - static func success(_ matcher: Matcher? = nil) -> Matcher> { - return .init { actual in - switch actual { - case let .success(value): - return matcher?.evaluate(value) ?? .match - case let .failure(error): - return .noMatch(actual: "\(error)", expected: "success") - } - } - } - - static func success() -> Matcher> { - return .init { actual in - switch actual { - case .success: - return .match - case let .failure(error): - return .noMatch(actual: "\(error)", expected: "success") - } - } - } - - static func failure( - _ matcher: Matcher? = nil - ) -> Matcher> { - return .init { actual in - switch actual { - case let .success(value): - return .noMatch(actual: "\(value)", expected: "failure") - case let .failure(error): - return matcher?.evaluate(error) ?? .match - } - } - } - - // MARK: Utility - - static func all(_ matchers: Matcher...) -> Matcher { - return .init { actual in - for matcher in matchers { - let result = matcher.evaluate(actual) - switch result { - case .noMatch: - return result - case .match: - () - } - } - return .match - } - } - - // MARK: Type - - /// Checks that the actual value is an instance of the given type. - static func instanceOf(_: Expected.Type) -> Matcher { - return .init { actual in - if actual is Expected { - return .match - } else { - return .noMatch( - actual: String(describing: type(of: actual)) + " (\(actual))", - expected: "value of type \(Expected.self)" - ) - } - } - } - - // MARK: Collection - - /// Checks whether the collection has the expected count. - static func hasCount(_ count: Int) -> Matcher { - return .init { actual in - actual.count == count - ? .match - : .noMatch(actual: "has count \(actual.count)", expected: "count of \(count)") - } - } - - static func isEmpty() -> Matcher { - return .init { actual in - actual.isEmpty - ? .match - : .noMatch(actual: "has \(actual.count) items", expected: "is empty") - } - } - - // MARK: gRPC matchers - - static func hasCode(_ code: GRPCStatus.Code) -> Matcher { - return .init { actual in - actual.code == code - ? .match - : .noMatch(actual: "has status code \(actual)", expected: "\(code)") - } - } - - static func metadata( - _ matcher: Matcher? = nil - ) -> Matcher> { - return .init { actual in - switch actual { - case let .metadata(headers): - return matcher?.evaluate(headers) ?? .match - default: - return .noMatch(actual: String(describing: actual), expected: "metadata") - } - } - } - - static func message( - _ matcher: Matcher? = nil - ) -> Matcher> { - return .init { actual in - switch actual { - case let .message(message): - return matcher?.evaluate(message) ?? .match - default: - return .noMatch(actual: String(describing: actual), expected: "message") - } - } - } - - static func metadata( - _ matcher: Matcher? = nil - ) -> Matcher> { - return .init { actual in - switch actual { - case let .metadata(headers): - return matcher?.evaluate(headers) ?? .match - default: - return .noMatch(actual: String(describing: actual), expected: "metadata") - } - } - } - - static func message( - _ matcher: Matcher? = nil - ) -> Matcher> { - return .init { actual in - switch actual { - case let .message(message, _): - return matcher?.evaluate(message) ?? .match - default: - return .noMatch(actual: String(describing: actual), expected: "message") - } - } - } - - static func end( - status statusMatcher: Matcher? = nil, - trailers trailersMatcher: Matcher? = nil - ) -> Matcher> { - return .init { actual in - switch actual { - case let .end(status, trailers): - let statusMatch = (statusMatcher?.evaluate(status) ?? .match) - switch statusMatcher?.evaluate(status) ?? .match { - case .match: - return trailersMatcher?.evaluate(trailers) ?? .match - case .noMatch: - return statusMatch - } - default: - return .noMatch(actual: String(describing: actual), expected: "end") - } - } - } - - static func sendTrailers( - _ matcher: Matcher? = nil - ) -> Matcher { - return .init { actual in - switch actual { - case let .sendTrailers(trailers): - return matcher?.evaluate(trailers) ?? .match - case .sendTrailersAndFinish: - return .noMatch(actual: "sendTrailersAndFinish", expected: "sendTrailers") - case let .failure(error): - return .noMatch(actual: "\(error)", expected: "sendTrailers") - } - } - } - - static func sendTrailersAndFinish( - _ matcher: Matcher? = nil - ) -> Matcher { - return .init { actual in - switch actual { - case let .sendTrailersAndFinish(trailers): - return matcher?.evaluate(trailers) ?? .match - case .sendTrailers: - return .noMatch(actual: "sendTrailers", expected: "sendTrailersAndFinish") - case let .failure(error): - return .noMatch(actual: "\(error)", expected: "sendTrailersAndFinish") - } - } - } - - static func failure( - _ matcher: Matcher? = nil - ) -> Matcher { - return .init { actual in - switch actual { - case .sendTrailers: - return .noMatch(actual: "sendTrailers", expected: "failure") - case .sendTrailersAndFinish: - return .noMatch(actual: "sendTrailersAndFinish", expected: "failure") - case let .failure(error): - return matcher?.evaluate(error) ?? .match - } - } - } - - // MARK: HTTP/1 - - static func head( - status: HTTPResponseStatus, - headers: HTTPHeaders? = nil - ) -> Matcher { - return .init { actual in - switch actual { - case let .head(head): - let statusMatches = Matcher.is(status).evaluate(head.status) - switch statusMatches { - case .match: - return headers.map { Matcher.is($0).evaluate(head.headers) } ?? .match - case .noMatch: - return statusMatches - } - - case .body, .end: - return .noMatch(actual: "\(actual)", expected: "head") - } - } - } - - static func body(_ matcher: Matcher? = nil) -> Matcher { - return .init { actual in - switch actual { - case let .body(.byteBuffer(buffer)): - return matcher.map { $0.evaluate(buffer) } ?? .match - default: - return .noMatch(actual: "\(actual)", expected: "body") - } - } - } - - static func end() -> Matcher { - return .init { actual in - switch actual { - case .end: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "end") - } - } - } - - // MARK: HTTP/2 - - static func contains( - _ name: String, - _ values: [String]? = nil - ) -> Matcher { - return .init { actual in - let headers = actual[canonicalForm: name] - - if headers.isEmpty { - return .noMatch(actual: "does not contain '\(name)'", expected: "contains '\(name)'") - } else { - return values.map { Matcher.equalTo($0).evaluate(headers) } ?? .match - } - } - } - - static func contains( - caseSensitive caseSensitiveName: String - ) -> Matcher { - return .init { actual in - for (name, _, _) in actual { - if name == caseSensitiveName { - return .match - } - } - - return .noMatch( - actual: "does not contain '\(caseSensitiveName)'", - expected: "contains '\(caseSensitiveName)'" - ) - } - } - - static func headers( - _ headers: Matcher? = nil, - endStream: Bool? = nil - ) -> Matcher { - return .init { actual in - switch actual { - case let .headers(payload): - let headersMatch = headers?.evaluate(payload.headers) - - switch headersMatch { - case .none, - .some(.match): - return endStream.map { Matcher.is($0).evaluate(payload.endStream) } ?? .match - case .some(.noMatch): - return headersMatch! - } - default: - return .noMatch(actual: "\(actual)", expected: "headers") - } - } - } - - static func data( - buffer: ByteBuffer? = nil, - endStream: Bool? = nil - ) -> Matcher { - return .init { actual in - switch actual { - case let .data(payload): - let endStreamMatches = endStream.map { Matcher.is($0).evaluate(payload.endStream) } - - switch (endStreamMatches, payload.data) { - case let (.none, .byteBuffer(b)), - let (.some(.match), .byteBuffer(b)): - return buffer.map { Matcher.is($0).evaluate(b) } ?? .match - - case (.some(.noMatch), .byteBuffer): - return endStreamMatches! - - case (_, .fileRegion): - preconditionFailure("Unexpected IOData.fileRegion") - } - - default: - return .noMatch(actual: "\(actual)", expected: "data") - } - } - } - - static func trailersOnly( - code: GRPCStatus.Code, - contentType: String = "application/grpc" - ) -> Matcher { - return .all( - .contains(":status", ["200"]), - .contains("content-type", [contentType]), - .contains("grpc-status", ["\(code.rawValue)"]) - ) - } - - static func trailers(code: GRPCStatus.Code, message: String) -> Matcher { - return .all( - .contains("grpc-status", ["\(code.rawValue)"]), - .contains("grpc-message", [message]) - ) - } - - // MARK: HTTP2ToRawGRPCStateMachine.Action - - static func errorCaught() -> Matcher { - return .init { actual in - switch actual { - case .errorCaught: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "errorCaught") - } - } - } - - static func configure() -> Matcher { - return .init { actual in - switch actual { - case .configure: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "configurePipeline") - } - } - } - - static func rejectRPC( - _ matcher: Matcher? = nil - ) -> Matcher { - return .init { actual in - switch actual { - case let .rejectRPC(headers): - return matcher?.evaluate(headers) ?? .match - default: - return .noMatch(actual: "\(actual)", expected: "rejectRPC") - } - } - } - - static func forwardHeaders() -> Matcher { - return .init { actual in - switch actual { - case .forwardHeaders: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "forwardHeaders") - } - } - } - - static func none() -> Matcher { - return .init { actual in - switch actual { - case .none: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "none") - } - } - } - - static func forwardMessage() -> Matcher { - return .init { actual in - switch actual { - case .forwardMessage: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "forwardMessage") - } - } - } - - static func forwardEnd() -> Matcher { - return .init { actual in - switch actual { - case .forwardEnd: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "forwardEnd") - } - } - } - - static func forwardHeadersThenRead() - -> Matcher - { - return .init { actual in - switch actual { - case .forwardHeadersAndRead: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "forwardHeadersAndRead") - } - } - } - - static func forwardMessageThenRead() - -> Matcher - { - return .init { actual in - switch actual { - case .forwardMessageThenReadNextMessage: - return .match - default: - return .noMatch(actual: "\(actual)", expected: "forwardMessageThenReadNextMessage") - } - } - } -} - -struct ExpressionMatcher { - typealias Expression = () throws -> Value - private typealias Evaluator = (Expression) -> MatchResult - private var evaluator: Evaluator - - private init(_ evaluator: @escaping Evaluator) { - self.evaluator = evaluator - } - - fileprivate func evaluate(_ expression: Expression) -> MatchResult { - return self.evaluator(expression) - } - - /// Asserts that the expression does not throw and error. Returns the result of any provided - /// matcher on the result of the expression. - static func doesNotThrow(_ matcher: Matcher? = nil) -> ExpressionMatcher { - return .init { expression in - do { - let value = try expression() - return matcher?.evaluate(value) ?? .match - } catch { - return .noMatch(actual: "threw '\(error)'", expected: "should not throw error") - } - } - } - - /// Asserts that the expression throws and error. Returns the result of any provided matcher - /// on the error thrown by the expression. - static func `throws`(_ matcher: Matcher? = nil) -> ExpressionMatcher { - return .init { expression in - do { - let value = try expression() - return .noMatch(actual: "returned '\(value)'", expected: "should throw error") - } catch { - return matcher?.evaluate(error) ?? .match - } - } - } -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -func assertThat( - _ expression: @autoclosure @escaping () async throws -> Value, - _ matcher: Matcher, - file: StaticString = #filePath, - line: UInt = #line -) async { - // For value matchers we'll assert that we don't throw by default. - await assertThat(try await expression(), .doesNotThrow(matcher), file: file, line: line) -} - -@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -func assertThat( - _ expression: @autoclosure @escaping () async throws -> Value, - _ matcher: ExpressionMatcher, - file: StaticString = #filePath, - line: UInt = #line -) async { - // Create a shim here from async-await world... - let result: Result - do { - let value = try await expression() - result = .success(value) - } catch { - result = .failure(error) - } - switch matcher.evaluate(result.get) { - case .match: - () - case let .noMatch(actual: actual, expected: expected): - XCTFail("ACTUAL: \(actual), EXPECTED: \(expected)", file: file, line: line) - } -} diff --git a/Tests/GRPCTests/ZeroLengthWriteTests.swift b/Tests/GRPCTests/ZeroLengthWriteTests.swift deleted file mode 100644 index 1cb070de1..000000000 --- a/Tests/GRPCTests/ZeroLengthWriteTests.swift +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2020, 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. - */ -#if canImport(NIOSSL) -import Dispatch -import EchoImplementation -import EchoModel -import Foundation -import GRPC -import GRPCSampleData -import NIOCore -import NIOSSL -import NIOTransportServices -import XCTest - -final class ZeroLengthWriteTests: GRPCTestCase { - func clientBuilder( - group: EventLoopGroup, - secure: Bool, - debugInitializer: @escaping GRPCChannelInitializer - ) -> ClientConnection.Builder { - if secure { - return ClientConnection.usingTLSBackedByNIOSSL(on: group) - .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withDebugChannelInitializer(debugInitializer) - } else { - return ClientConnection.insecure(group: group) - .withDebugChannelInitializer(debugInitializer) - } - } - - func serverBuilder( - group: EventLoopGroup, - secure: Bool, - debugInitializer: @escaping (Channel) -> EventLoopFuture - ) -> Server.Builder { - if secure { - return Server.usingTLSBackedByNIOSSL( - on: group, - certificateChain: [SampleCertificate.server.certificate], - privateKey: SamplePrivateKey.server - ).withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate])) - .withDebugChannelInitializer(debugInitializer) - } else { - return Server.insecure(group: group) - .withDebugChannelInitializer(debugInitializer) - } - } - - func makeServer( - group: EventLoopGroup, - secure: Bool, - debugInitializer: @escaping (Channel) -> EventLoopFuture - ) throws -> Server { - return try self.serverBuilder(group: group, secure: secure, debugInitializer: debugInitializer) - .withServiceProviders([self.makeEchoProvider()]) - .withLogger(self.serverLogger) - .bind(host: "127.0.0.1", port: 0) - .wait() - } - - func makeClientConnection( - group: EventLoopGroup, - secure: Bool, - port: Int, - debugInitializer: @escaping GRPCChannelInitializer - ) throws -> ClientConnection { - return self.clientBuilder(group: group, secure: secure, debugInitializer: debugInitializer) - .withBackgroundActivityLogger(self.clientLogger) - .withConnectionReestablishment(enabled: false) - .connect(host: "127.0.0.1", port: port) - } - - func makeEchoProvider() -> Echo_EchoProvider { return EchoProvider() } - - func makeEchoClient( - group: EventLoopGroup, - secure: Bool, - port: Int, - debugInitializer: @escaping GRPCChannelInitializer - ) throws -> Echo_EchoNIOClient { - return Echo_EchoNIOClient( - channel: try self.makeClientConnection( - group: group, - secure: secure, - port: port, - debugInitializer: debugInitializer - ), - defaultCallOptions: self.callOptionsWithLogger - ) - } - - func zeroLengthWriteExpectation() -> XCTestExpectation { - let expectation = self.expectation(description: "Expecting zero length write workaround") - expectation.expectedFulfillmentCount = 1 - expectation.assertForOverFulfill = true - return expectation - } - - func noZeroLengthWriteExpectation() -> XCTestExpectation { - let expectation = self.expectation(description: "Not expecting zero length write workaround") - expectation.expectedFulfillmentCount = 1 - expectation.assertForOverFulfill = true - return expectation - } - - func debugPipelineExpectation( - _ callback: @escaping (Result) -> Void - ) -> GRPCChannelInitializer { - return { channel in - channel.pipeline.handler(type: NIOFilterEmptyWritesHandler.self).always { result in - callback(result) - }.map { _ in () }.recover { _ in () } - } - } - - private func _runTest( - networkPreference: NetworkPreference, - secure: Bool, - clientHandlerCallback: @escaping (Result) -> Void, - serverHandlerCallback: @escaping (Result) -> Void - ) { - // We can only run this test on platforms where the zero-length write workaround _could_ be added. - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - let group = PlatformSupport.makeEventLoopGroup( - loopCount: 1, - networkPreference: networkPreference - ) - let server = try! self.makeServer( - group: group, - secure: secure, - debugInitializer: self.debugPipelineExpectation(serverHandlerCallback) - ) - - defer { - XCTAssertNoThrow(try server.close().wait()) - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - - let port = server.channel.localAddress!.port! - let client = try! self.makeEchoClient( - group: group, - secure: secure, - port: port, - debugInitializer: self.debugPipelineExpectation(clientHandlerCallback) - ) - defer { - XCTAssertNoThrow(try client.channel.close().wait()) - } - - // We need to wait here to confirm that the RPC completes. All expectations should have completed by then. - let call = client.get(Echo_EchoRequest(text: "foo bar baz")) - XCTAssertNoThrow(try call.status.wait()) - self.waitForExpectations(timeout: 1.0) - #endif - } - - func testZeroLengthWriteTestPosixSecure() throws { - // We can only run this test on platforms where the zero-length write workaround _could_ be added. - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - - let serverExpectation = self.noZeroLengthWriteExpectation() - let clientExpectation = self.noZeroLengthWriteExpectation() - self._runTest( - networkPreference: .userDefined(.posix), - secure: true, - clientHandlerCallback: { result in - if case .failure = result { - clientExpectation.fulfill() - } - }, - serverHandlerCallback: { result in - if case .failure = result { - serverExpectation.fulfill() - } - } - ) - #endif - } - - func testZeroLengthWriteTestPosixInsecure() throws { - // We can only run this test on platforms where the zero-length write workaround _could_ be added. - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - - let serverExpectation = self.noZeroLengthWriteExpectation() - let clientExpectation = self.noZeroLengthWriteExpectation() - self._runTest( - networkPreference: .userDefined(.posix), - secure: false, - clientHandlerCallback: { result in - if case .failure = result { - clientExpectation.fulfill() - } - }, - serverHandlerCallback: { result in - if case .failure = result { - serverExpectation.fulfill() - } - } - ) - #endif - } - - func testZeroLengthWriteTestNetworkFrameworkSecure() throws { - // We can only run this test on platforms where the zero-length write workaround _could_ be added. - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - - let serverExpectation = self.noZeroLengthWriteExpectation() - let clientExpectation = self.noZeroLengthWriteExpectation() - self._runTest( - networkPreference: .userDefined(.networkFramework), - secure: true, - clientHandlerCallback: { result in - if case .failure = result { - clientExpectation.fulfill() - } - }, - serverHandlerCallback: { result in - if case .failure = result { - serverExpectation.fulfill() - } - } - ) - #endif - } - - func testZeroLengthWriteTestNetworkFrameworkInsecure() throws { - // We can only run this test on platforms where the zero-length write workaround _could_ be added. - #if canImport(Network) - guard #available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else { return } - - let serverExpectation = self.zeroLengthWriteExpectation() - let clientExpectation = self.zeroLengthWriteExpectation() - self._runTest( - networkPreference: .userDefined(.networkFramework), - secure: false, - clientHandlerCallback: { result in - if case .success = result { - clientExpectation.fulfill() - } - }, - serverHandlerCallback: { result in - if case .success = result { - serverExpectation.fulfill() - } - } - ) - #endif - } -} - -#endif // canImport(NIOSSL) diff --git a/Tests/GRPCTests/ZlibTests.swift b/Tests/GRPCTests/ZlibTests.swift deleted file mode 100644 index 5ee37be52..000000000 --- a/Tests/GRPCTests/ZlibTests.swift +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2020, 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 NIOCore -import XCTest - -@testable import GRPC - -class ZlibTests: GRPCTestCase { - var allocator = ByteBufferAllocator() - var inputSize = 4096 - - func makeBytes(count: Int) -> [UInt8] { - return (0 ..< count).map { _ in - UInt8.random(in: UInt8(ascii: "a") ... UInt8(ascii: "z")) - } - } - - @discardableResult - func doCompressAndDecompress( - of bytes: [UInt8], - format: Zlib.CompressionFormat, - initialInflateBufferSize: Int? = nil - ) throws -> Int { - var data = self.allocator.buffer(capacity: 0) - data.writeBytes(bytes) - - // Compress it. - let deflate = Zlib.Deflate(format: format) - var compressed = self.allocator.buffer(capacity: 0) - let compressedBytesWritten = try deflate.deflate(&data, into: &compressed) - // Did we write the right number of bytes? - XCTAssertEqual(compressedBytesWritten, compressed.readableBytes) - - // Decompress it. - let inflate = Zlib.Inflate(format: format, limit: .absolute(bytes.count * 2)) - var decompressed = self.allocator.buffer(capacity: initialInflateBufferSize ?? self.inputSize) - let decompressedBytesWritten = try inflate.inflate(&compressed, into: &decompressed) - // Did we write the right number of bytes? - XCTAssertEqual(decompressedBytesWritten, decompressed.readableBytes) - - // Did we get back to where we started? - XCTAssertEqual(decompressed.readBytes(length: decompressed.readableBytes), bytes) - - return compressedBytesWritten - } - - func testCompressionAndDecompressionOfASCIIBytes() throws { - let bytes = self.makeBytes(count: self.inputSize) - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - try self.doCompressAndDecompress(of: bytes, format: format) - } - } - - func testCompressionAndDecompressionOfZeros() throws { - // This test makes sure the decompressor is capable of increasing the output buffer size a - // number of times. - let bytes: [UInt8] = Array(repeating: 0, count: self.inputSize) - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - let compressedSize = try self.doCompressAndDecompress(of: bytes, format: format) - // Is the compressed size significantly smaller than the input size? - XCTAssertLessThan(compressedSize, bytes.count / 4) - } - } - - func testCompressionAndDecompressionOfHardToCompressData() throws { - let bytes: [UInt8] = (0 ..< self.inputSize).map { _ in - UInt8.random(in: UInt8.min ... UInt8.max) - } - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - // Is the compressed size larger than the input size? - let compressedSize = try self.doCompressAndDecompress(of: bytes, format: format) - XCTAssertGreaterThan(compressedSize, bytes.count) - } - } - - func testDecompressionAutomaticallyResizesOutputBuffer() throws { - let bytes = self.makeBytes(count: self.inputSize) - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - try self.doCompressAndDecompress(of: bytes, format: format, initialInflateBufferSize: 0) - } - } - - func testCompressionAndDecompressionWithResets() throws { - // Generate some input. - let byteArrays = (0 ..< 5).map { _ in - self.makeBytes(count: self.inputSize) - } - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - let deflate = Zlib.Deflate(format: format) - let inflate = Zlib.Inflate(format: format, limit: .absolute(self.inputSize * 2)) - - for bytes in byteArrays { - var data = self.allocator.buffer(capacity: 0) - data.writeBytes(bytes) - - // Compress it. - var compressed = self.allocator.buffer(capacity: 0) - let compressedBytesWritten = try deflate.deflate(&data, into: &compressed) - deflate.reset() - - // Did we write the right number of bytes? - XCTAssertEqual(compressedBytesWritten, compressed.readableBytes) - - // Decompress it. - var decompressed = self.allocator.buffer(capacity: self.inputSize) - let decompressedBytesWritten = try inflate.inflate(&compressed, into: &decompressed) - inflate.reset() - - // Did we write the right number of bytes? - XCTAssertEqual(decompressedBytesWritten, decompressed.readableBytes) - - // Did we get back to where we started? - XCTAssertEqual(decompressed.readBytes(length: decompressed.readableBytes), bytes) - } - } - } - - func testDecompressThrowsOnGibberish() throws { - let bytes = self.makeBytes(count: self.inputSize) - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - var buffer = self.allocator.buffer(capacity: bytes.count) - buffer.writeBytes(bytes) - - let inflate = Zlib.Inflate(format: format, limit: .ratio(1)) - - var output = self.allocator.buffer(capacity: 0) - XCTAssertThrowsError(try inflate.inflate(&buffer, into: &output)) { error in - let withContext = error as? GRPCError.WithContext - XCTAssert(withContext?.error is GRPCError.ZlibCompressionFailure) - } - } - } - - func testAbsoluteDecompressionLimit() throws { - let bytes = self.makeBytes(count: self.inputSize) - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - var data = self.allocator.buffer(capacity: 0) - data.writeBytes(bytes) - - // Compress it. - let deflate = Zlib.Deflate(format: format) - var compressed = self.allocator.buffer(capacity: 0) - let compressedBytesWritten = try deflate.deflate(&data, into: &compressed) - // Did we write the right number of bytes? - XCTAssertEqual(compressedBytesWritten, compressed.readableBytes) - - let inflate = Zlib.Inflate(format: format, limit: .absolute(compressedBytesWritten - 1)) - var output = self.allocator.buffer(capacity: 0) - XCTAssertThrowsError(try inflate.inflate(&compressed, into: &output)) { error in - let withContext = error as? GRPCError.WithContext - XCTAssert(withContext?.error is GRPCError.DecompressionLimitExceeded) - } - } - } - - func testRatioDecompressionLimit() throws { - let bytes = self.makeBytes(count: self.inputSize) - - for format in [Zlib.CompressionFormat.deflate, .gzip] { - var data = self.allocator.buffer(capacity: 0) - data.writeBytes(bytes) - - // Compress it. - let deflate = Zlib.Deflate(format: format) - var compressed = self.allocator.buffer(capacity: 0) - let compressedBytesWritten = try deflate.deflate(&data, into: &compressed) - // Did we write the right number of bytes? - XCTAssertEqual(compressedBytesWritten, compressed.readableBytes) - - let inflate = Zlib.Inflate(format: format, limit: .ratio(1)) - var output = self.allocator.buffer(capacity: 0) - XCTAssertThrowsError(try inflate.inflate(&compressed, into: &output)) { error in - let withContext = error as? GRPCError.WithContext - XCTAssert(withContext?.error is GRPCError.DecompressionLimitExceeded) - } - } - } - - func testAbsoluteDecompressionLimitMaximumSize() throws { - let absolute: DecompressionLimit = .absolute(1234) - // The compressed size is ignored here. - XCTAssertEqual(absolute.maximumDecompressedSize(compressedSize: -42), 1234) - } - - func testRatioDecompressionLimitMaximumSize() throws { - let ratio: DecompressionLimit = .ratio(2) - XCTAssertEqual(ratio.maximumDecompressedSize(compressedSize: 10), 20) - } -} diff --git a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift b/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift deleted file mode 100644 index 437d31916..000000000 --- a/Tests/InProcessInteroperabilityTests/InProcessInteroperabilityTests.swift +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 GRPCInProcessTransport -import InteroperabilityTests -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class InProcessInteroperabilityTests: XCTestCase { - func runInProcessTransport( - interopTestCase: InteroperabilityTestCase - ) async throws { - do { - let inProcess = InProcessTransport.makePair() - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - let server = GRPCServer(transport: inProcess.server, services: [TestService()]) - try await server.serve() - } - - group.addTask { - try await withThrowingTaskGroup(of: Void.self) { clientGroup in - let client = GRPCClient(transport: inProcess.client) - clientGroup.addTask { - try await client.run() - } - try await interopTestCase.makeTest().run(client: client) - - clientGroup.cancelAll() - } - } - - try await group.next() - group.cancelAll() - } - } catch let error as AssertionFailure { - XCTFail(error.message) - } - } - - func testEmptyUnary() async throws { - try await self.runInProcessTransport(interopTestCase: .emptyUnary) - } - - func testLargeUnary() async throws { - try await self.runInProcessTransport(interopTestCase: .largeUnary) - } - - func testClientStreaming() async throws { - try await self.runInProcessTransport(interopTestCase: .clientStreaming) - } - - func testServerStreaming() async throws { - try await self.runInProcessTransport(interopTestCase: .serverStreaming) - } - - func testPingPong() async throws { - try await self.runInProcessTransport(interopTestCase: .pingPong) - } - - func testEmptyStream() async throws { - try await self.runInProcessTransport(interopTestCase: .emptyStream) - } - - func testCustomMetdata() async throws { - try await self.runInProcessTransport(interopTestCase: .customMetadata) - } - - func testStatusCodeAndMessage() async throws { - try await self.runInProcessTransport(interopTestCase: .statusCodeAndMessage) - } - - func testSpecialStatusMessage() async throws { - try await self.runInProcessTransport(interopTestCase: .specialStatusMessage) - } - - func testUnimplementedMethod() async throws { - try await self.runInProcessTransport(interopTestCase: .unimplementedMethod) - } - - func testUnimplementedService() async throws { - try await self.runInProcessTransport(interopTestCase: .unimplementedService) - } -} diff --git a/Tests/Services/HealthTests/HealthTests.swift b/Tests/Services/HealthTests/HealthTests.swift deleted file mode 100644 index cd762f4ab..000000000 --- a/Tests/Services/HealthTests/HealthTests.swift +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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 GRPCHealth -import GRPCInProcessTransport -import XCTest - -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -final class HealthTests: XCTestCase { - private func withHealthClient( - _ body: @Sendable (Grpc_Health_V1_HealthClient, Health.Provider) async throws -> Void - ) async throws { - let health = Health() - let inProcess = InProcessTransport.makePair() - let server = GRPCServer(transport: inProcess.server, services: [health.service]) - let client = GRPCClient(transport: inProcess.client) - let healthClient = Grpc_Health_V1_HealthClient(wrapping: client) - - try await withThrowingDiscardingTaskGroup { group in - group.addTask { - try await server.serve() - } - - group.addTask { - try await client.run() - } - - do { - try await body(healthClient, health.provider) - } catch { - XCTFail("Unexpected error: \(error)") - } - - group.cancelAll() - } - } - - func testCheckOnKnownService() async throws { - try await withHealthClient { (healthClient, healthProvider) in - let testServiceDescriptor = ServiceDescriptor.testService - - healthProvider.updateStatus(.serving, forService: testServiceDescriptor) - - let message = Grpc_Health_V1_HealthCheckRequest.with { - $0.service = testServiceDescriptor.fullyQualifiedService - } - - try await healthClient.check(request: ClientRequest.Single(message: message)) { response in - try XCTAssertEqual(response.message.status, .serving) - } - } - } - - func testCheckOnUnknownService() async throws { - try await withHealthClient { (healthClient, healthProvider) in - let message = Grpc_Health_V1_HealthCheckRequest.with { - $0.service = "does.not.Exist" - } - - try await healthClient.check(request: ClientRequest.Single(message: message)) { response in - try XCTAssertThrowsError(ofType: RPCError.self, response.message) { error in - XCTAssertEqual(error.code, .notFound) - } - } - } - } - - func testCheckOnServer() async throws { - try await withHealthClient { (healthClient, healthProvider) in - // An unspecified service refers to the server. - healthProvider.updateStatus(.notServing, forService: "") - - let message = Grpc_Health_V1_HealthCheckRequest() - - try await healthClient.check(request: ClientRequest.Single(message: message)) { response in - try XCTAssertEqual(response.message.status, .notServing) - } - } - } - - func testWatchOnKnownService() async throws { - try await withHealthClient { (healthClient, healthProvider) in - let testServiceDescriptor = ServiceDescriptor.testService - - let statusesToBeSent: [ServingStatus] = [.serving, .notServing, .serving] - - // Before watching the service, make the status of the service known to the Health service. - healthProvider.updateStatus(statusesToBeSent[0], forService: testServiceDescriptor) - - let message = Grpc_Health_V1_HealthCheckRequest.with { - $0.service = testServiceDescriptor.fullyQualifiedService - } - - try await healthClient.watch(request: ClientRequest.Single(message: message)) { response in - var responseStreamIterator = response.messages.makeAsyncIterator() - - for i in 0 ..< statusesToBeSent.count { - let next = try await responseStreamIterator.next() - let message = try XCTUnwrap(next) - let expectedStatus = Grpc_Health_V1_HealthCheckResponse.ServingStatus(statusesToBeSent[i]) - - XCTAssertEqual(message.status, expectedStatus) - - if i < statusesToBeSent.count - 1 { - healthProvider.updateStatus(statusesToBeSent[i + 1], forService: testServiceDescriptor) - } - } - } - } - } - - func testWatchOnUnknownServiceDoesNotTerminateTheRPC() async throws { - try await withHealthClient { (healthClient, healthProvider) in - let testServiceDescriptor = ServiceDescriptor.testService - - let message = Grpc_Health_V1_HealthCheckRequest.with { - $0.service = testServiceDescriptor.fullyQualifiedService - } - - try await healthClient.watch(request: ClientRequest.Single(message: message)) { response in - var responseStreamIterator = response.messages.makeAsyncIterator() - var next = try await responseStreamIterator.next() - var message = try XCTUnwrap(next) - - // As the service was watched before being updated, the first status received should be - // .serviceUnknown. - XCTAssertEqual(message.status, .serviceUnknown) - - healthProvider.updateStatus(.notServing, forService: testServiceDescriptor) - - next = try await responseStreamIterator.next() - message = try XCTUnwrap(next) - - // The RPC was not terminated and a status update was received successfully. - XCTAssertEqual(message.status, .notServing) - } - } - } - - func testMultipleWatchOnTheSameService() async throws { - try await withHealthClient { (healthClient, healthProvider) in - let testServiceDescriptor = ServiceDescriptor.testService - - let statusesToBeSent: [ServingStatus] = [.serving, .notServing, .serving] - - try await withThrowingTaskGroup( - of: [Grpc_Health_V1_HealthCheckResponse.ServingStatus].self - ) { group in - let message = Grpc_Health_V1_HealthCheckRequest.with { - $0.service = testServiceDescriptor.fullyQualifiedService - } - - // The continuation of this stream will be used to signal when the watch response streams - // are up and ready. - let signal = AsyncStream.makeStream(of: Void.self) - let numberOfWatches = 2 - - for _ in 0 ..< numberOfWatches { - group.addTask { - return try await healthClient.watch( - request: ClientRequest.Single(message: message) - ) { response in - signal.continuation.yield() // Make signal - - var statuses = [Grpc_Health_V1_HealthCheckResponse.ServingStatus]() - var responseStreamIterator = response.messages.makeAsyncIterator() - - // Since responseStreamIterator.next() will never be nil (ideally, as the response - // stream is always open), the iteration cannot be based on when - // responseStreamIterator.next() is nil. Else, the iteration infinitely awaits and the - // test never finishes. Hence, it is based on the expected number of statuses to be - // received. - for _ in 0 ..< statusesToBeSent.count + 1 { - // As the service will be watched before being updated, the first status received - // should be .serviceUnknown. Hence, the range of this iteration is increased by 1. - - let next = try await responseStreamIterator.next() - let message = try XCTUnwrap(next) - statuses.append(message.status) - } - - return statuses - } - } - } - - // Wait until all the watch streams are up and ready. - for await _ in signal.stream.prefix(numberOfWatches) {} - - for status in statusesToBeSent { - healthProvider.updateStatus(status, forService: testServiceDescriptor) - } - - for try await receivedStatuses in group { - XCTAssertEqual(receivedStatuses[0], .serviceUnknown) - - for i in 0 ..< statusesToBeSent.count { - let sentStatus = Grpc_Health_V1_HealthCheckResponse.ServingStatus(statusesToBeSent[i]) - XCTAssertEqual(sentStatus, receivedStatuses[i + 1]) - } - } - } - } - } - - func testWatchWithUnchangingStatusUpdates() async throws { - try await withHealthClient { (healthClient, healthProvider) in - let testServiceDescriptor = ServiceDescriptor.testService - - let statusesToBeSent: [ServingStatus] = [.notServing, .notServing, .notServing, .serving] - - // The repeated .notServing updates should be received only once. Also, as the service will - // be watched before being updated, the first status received should be .serviceUnknown. - let expectedStatuses: [Grpc_Health_V1_HealthCheckResponse.ServingStatus] = [ - .serviceUnknown, - .notServing, - .serving, - ] - - let message = Grpc_Health_V1_HealthCheckRequest.with { - $0.service = testServiceDescriptor.fullyQualifiedService - } - - try await healthClient.watch( - request: ClientRequest.Single(message: message) - ) { response in - // Send all status updates. - for status in statusesToBeSent { - healthProvider.updateStatus(status, forService: testServiceDescriptor) - } - - var responseStreamIterator = response.messages.makeAsyncIterator() - - for i in 0 ..< expectedStatuses.count { - let next = try await responseStreamIterator.next() - let message = try XCTUnwrap(next) - - XCTAssertEqual(message.status, expectedStatuses[i]) - } - } - } - } - - func testWatchOnServer() async throws { - try await withHealthClient { (healthClient, healthProvider) in - let statusesToBeSent: [ServingStatus] = [.serving, .notServing, .serving] - - // An unspecified service refers to the server. - healthProvider.updateStatus(statusesToBeSent[0], forService: "") - - let message = Grpc_Health_V1_HealthCheckRequest() - - try await healthClient.watch(request: ClientRequest.Single(message: message)) { response in - var responseStreamIterator = response.messages.makeAsyncIterator() - - for i in 0 ..< statusesToBeSent.count { - let next = try await responseStreamIterator.next() - let message = try XCTUnwrap(next) - let expectedStatus = Grpc_Health_V1_HealthCheckResponse.ServingStatus(statusesToBeSent[i]) - - XCTAssertEqual(message.status, expectedStatus) - - if i < statusesToBeSent.count - 1 { - healthProvider.updateStatus(statusesToBeSent[i + 1], forService: "") - } - } - } - } - } -} - -extension ServiceDescriptor { - fileprivate static let testService = ServiceDescriptor(package: "test", service: "Service") -} diff --git a/Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift b/Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift deleted file mode 100644 index 3848388bc..000000000 --- a/Tests/Services/HealthTests/Test Utilities/XCTest+Utilities.swift +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 XCTest - -func XCTAssertThrowsError( - ofType: E.Type, - _ expression: @autoclosure () throws -> T, - _ errorHandler: (E) -> Void -) { - XCTAssertThrowsError(try expression()) { error in - guard let error = error as? E else { - return XCTFail("Error had unexpected type '\(type(of: error))'") - } - errorHandler(error) - } -} diff --git a/Protos/README.md b/dev/Protos/README.md similarity index 100% rename from Protos/README.md rename to dev/Protos/README.md diff --git a/Protos/examples/echo/echo.proto b/dev/Protos/examples/echo/echo.proto similarity index 100% rename from Protos/examples/echo/echo.proto rename to dev/Protos/examples/echo/echo.proto diff --git a/Protos/examples/route_guide/route_guide.proto b/dev/Protos/examples/route_guide/route_guide.proto similarity index 100% rename from Protos/examples/route_guide/route_guide.proto rename to dev/Protos/examples/route_guide/route_guide.proto diff --git a/Protos/fetch.sh b/dev/Protos/fetch.sh similarity index 63% rename from Protos/fetch.sh rename to dev/Protos/fetch.sh index 5d0cee63b..e68ce6531 100755 --- a/Protos/fetch.sh +++ b/dev/Protos/fetch.sh @@ -32,23 +32,12 @@ rm -rf "$upstream" # Create new directories to poulate. These are based on proto package name # rather than source repository name. mkdir -p "$upstream/google" -mkdir -p "$upstream/grpc/testing" mkdir -p "$upstream/grpc/core" -mkdir -p "$upstream/grpc/health/v1" # Copy over the grpc-proto protos. cp -rp "$checkouts/grpc-proto/grpc/service_config" "$upstream/grpc/service_config" cp -rp "$checkouts/grpc-proto/grpc/lookup" "$upstream/grpc/lookup" -cp -rp "$checkouts/grpc-proto/grpc/reflection" "$upstream/grpc/reflection" cp -rp "$checkouts/grpc-proto/grpc/examples" "$upstream/grpc/examples" -cp -rp "$checkouts/grpc-proto/grpc/testing/benchmark_service.proto" "$upstream/grpc/testing/benchmark_service.proto" -cp -rp "$checkouts/grpc-proto/grpc/testing/messages.proto" "$upstream/grpc/testing/messages.proto" -cp -rp "$checkouts/grpc-proto/grpc/testing/worker_service.proto" "$upstream/grpc/testing/worker_service.proto" -cp -rp "$checkouts/grpc-proto/grpc/testing/control.proto" "$upstream/grpc/testing/control.proto" -cp -rp "$checkouts/grpc-proto/grpc/testing/payloads.proto" "$upstream/grpc/testing/payloads.proto" -cp -rp "$checkouts/grpc-proto/grpc/testing/stats.proto" "$upstream/grpc/testing/stats.proto" -cp -rp "$checkouts/grpc-proto/grpc/core/stats.proto" "$upstream/grpc/core/stats.proto" -cp -rp "$checkouts/grpc-proto/grpc/health/v1/health.proto" "$upstream/grpc/health/v1/health.proto" # Copy over the googleapis protos. mkdir -p "$upstream/google/rpc" diff --git a/dev/Protos/generate.sh b/dev/Protos/generate.sh new file mode 100755 index 000000000..17cad9ec1 --- /dev/null +++ b/dev/Protos/generate.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# 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. + +set -eu + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +root="$here/../.." +protoc=$(which protoc) + +# Build the protoc plugins. +swift build -c release --product protoc-gen-swift + +# Grab the plugin paths. +bin_path=$(swift build -c release --show-bin-path) +protoc_gen_swift="$bin_path/protoc-gen-swift" + +# Generates messages by invoking protoc with the Swift plugin. +# Parameters: +# - $1: .proto file +# - $2: proto path +# - $3: output path +# - $4 onwards: options to forward to the plugin +function generate_message { + local proto=$1 + local args=("--plugin=$protoc_gen_swift" "--proto_path=$2" "--swift_out=$3") + + for option in "${@:4}"; do + args+=("--swift_opt=$option") + done + + invoke_protoc "${args[@]}" "$proto" +} + +function invoke_protoc { + # Setting -x when running the script produces a lot of output, instead boil + # just echo out the protoc invocations. + echo "$protoc" "$@" + "$protoc" "$@" +} + +#------------------------------------------------------------------------------ + +function generate_rpc_code_for_tests { + local protos=( + "$here/upstream/grpc/service_config/service_config.proto" + "$here/upstream/grpc/lookup/v1/rls.proto" + "$here/upstream/grpc/lookup/v1/rls_config.proto" + "$here/upstream/google/rpc/code.proto" + ) + local output="$root/Tests/GRPCCoreTests/Configuration/Generated" + + for proto in "${protos[@]}"; do + generate_message "$proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=DropPath" + done +} + +#------------------------------------------------------------------------------ + +# Tests +generate_rpc_code_for_tests diff --git a/Protos/upstream/google/rpc/code.proto b/dev/Protos/upstream/google/rpc/code.proto similarity index 100% rename from Protos/upstream/google/rpc/code.proto rename to dev/Protos/upstream/google/rpc/code.proto diff --git a/Protos/upstream/grpc/core/stats.proto b/dev/Protos/upstream/grpc/core/stats.proto similarity index 100% rename from Protos/upstream/grpc/core/stats.proto rename to dev/Protos/upstream/grpc/core/stats.proto diff --git a/Protos/upstream/grpc/examples/helloworld.proto b/dev/Protos/upstream/grpc/examples/helloworld.proto similarity index 100% rename from Protos/upstream/grpc/examples/helloworld.proto rename to dev/Protos/upstream/grpc/examples/helloworld.proto diff --git a/Protos/upstream/grpc/health/v1/health.proto b/dev/Protos/upstream/grpc/health/v1/health.proto similarity index 100% rename from Protos/upstream/grpc/health/v1/health.proto rename to dev/Protos/upstream/grpc/health/v1/health.proto diff --git a/Protos/upstream/grpc/lookup/v1/rls.proto b/dev/Protos/upstream/grpc/lookup/v1/rls.proto similarity index 100% rename from Protos/upstream/grpc/lookup/v1/rls.proto rename to dev/Protos/upstream/grpc/lookup/v1/rls.proto diff --git a/Protos/upstream/grpc/lookup/v1/rls_config.proto b/dev/Protos/upstream/grpc/lookup/v1/rls_config.proto similarity index 100% rename from Protos/upstream/grpc/lookup/v1/rls_config.proto rename to dev/Protos/upstream/grpc/lookup/v1/rls_config.proto diff --git a/Protos/upstream/grpc/reflection/v1/reflection.proto b/dev/Protos/upstream/grpc/reflection/v1/reflection.proto similarity index 100% rename from Protos/upstream/grpc/reflection/v1/reflection.proto rename to dev/Protos/upstream/grpc/reflection/v1/reflection.proto diff --git a/Protos/upstream/grpc/reflection/v1alpha/reflection.proto b/dev/Protos/upstream/grpc/reflection/v1alpha/reflection.proto similarity index 100% rename from Protos/upstream/grpc/reflection/v1alpha/reflection.proto rename to dev/Protos/upstream/grpc/reflection/v1alpha/reflection.proto diff --git a/Protos/upstream/grpc/service_config/service_config.proto b/dev/Protos/upstream/grpc/service_config/service_config.proto similarity index 100% rename from Protos/upstream/grpc/service_config/service_config.proto rename to dev/Protos/upstream/grpc/service_config/service_config.proto diff --git a/Protos/upstream/grpc/testing/benchmark_service.proto b/dev/Protos/upstream/grpc/testing/benchmark_service.proto similarity index 100% rename from Protos/upstream/grpc/testing/benchmark_service.proto rename to dev/Protos/upstream/grpc/testing/benchmark_service.proto diff --git a/Protos/upstream/grpc/testing/control.proto b/dev/Protos/upstream/grpc/testing/control.proto similarity index 100% rename from Protos/upstream/grpc/testing/control.proto rename to dev/Protos/upstream/grpc/testing/control.proto diff --git a/Protos/upstream/grpc/testing/messages.proto b/dev/Protos/upstream/grpc/testing/messages.proto similarity index 100% rename from Protos/upstream/grpc/testing/messages.proto rename to dev/Protos/upstream/grpc/testing/messages.proto diff --git a/Protos/upstream/grpc/testing/payloads.proto b/dev/Protos/upstream/grpc/testing/payloads.proto similarity index 100% rename from Protos/upstream/grpc/testing/payloads.proto rename to dev/Protos/upstream/grpc/testing/payloads.proto diff --git a/Protos/upstream/grpc/testing/stats.proto b/dev/Protos/upstream/grpc/testing/stats.proto similarity index 100% rename from Protos/upstream/grpc/testing/stats.proto rename to dev/Protos/upstream/grpc/testing/stats.proto diff --git a/Protos/upstream/grpc/testing/worker_service.proto b/dev/Protos/upstream/grpc/testing/worker_service.proto similarity index 100% rename from Protos/upstream/grpc/testing/worker_service.proto rename to dev/Protos/upstream/grpc/testing/worker_service.proto diff --git a/scripts/check-generated-code.sh b/dev/check-generated-code.sh similarity index 96% rename from scripts/check-generated-code.sh rename to dev/check-generated-code.sh index 3444d9d88..8e1ef7ce7 100755 --- a/scripts/check-generated-code.sh +++ b/dev/check-generated-code.sh @@ -24,7 +24,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Re-generate everything. log "Regenerating protos..." -"$here"/../Protos/generate.sh +"$here"/Protos/generate.sh # Check for changes. GIT_PAGER='' git diff --exit-code '*.swift' diff --git a/dev/codegen-tests/01-echo/generate-and-diff.sh b/dev/codegen-tests/01-echo/generate-and-diff.sh deleted file mode 100755 index b817e7bb3..000000000 --- a/dev/codegen-tests/01-echo/generate-and-diff.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${HERE}/../test-boilerplate.sh" - -function all_at_once { - echo "[${TEST}]" - - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/* - - validate -} - -all_at_once diff --git a/dev/codegen-tests/01-echo/golden/echo.grpc.swift b/dev/codegen-tests/01-echo/golden/echo.grpc.swift deleted file mode 100644 index 8b509dcb9..000000000 --- a/dev/codegen-tests/01-echo/golden/echo.grpc.swift +++ /dev/null @@ -1,246 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: echo.proto -// -import GRPC -import NIO -import SwiftProtobuf - - -/// Usage: instantiate `Echo_EchoClient`, then call methods of this protocol to make API calls. -internal protocol Echo_EchoClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Echo_EchoClientInterceptorFactoryProtocol? { get } - - func get( - _ request: Echo_EchoRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func expand( - _ request: Echo_EchoRequest, - callOptions: CallOptions?, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> ServerStreamingCall - - func collect( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func update( - callOptions: CallOptions?, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> BidirectionalStreamingCall -} - -extension Echo_EchoClientProtocol { - internal var serviceName: String { - return "echo.Echo" - } - - /// Immediately returns an echo of a request. - /// - /// - Parameters: - /// - request: Request to send to Get. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func get( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: "/echo.Echo/Get", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetInterceptors() ?? [] - ) - } - - /// Splits a request into words and returns each word in a stream of messages. - /// - /// - Parameters: - /// - request: Request to send to Expand. - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. - internal func expand( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> ServerStreamingCall { - return self.makeServerStreamingCall( - path: "/echo.Echo/Expand", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeExpandInterceptors() ?? [], - handler: handler - ) - } - - /// Collects a stream of messages and returns them concatenated when the caller closes. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - Returns: A `ClientStreamingCall` with futures for the metadata, status and response. - internal func collect( - callOptions: CallOptions? = nil - ) -> ClientStreamingCall { - return self.makeClientStreamingCall( - path: "/echo.Echo/Collect", - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCollectInterceptors() ?? [] - ) - } - - /// Streams back messages as they are received in an input stream. - /// - /// Callers should use the `send` method on the returned object to send messages - /// to the server. The caller should send an `.end` after the final message has been sent. - /// - /// - Parameters: - /// - callOptions: Call options. - /// - handler: A closure called when each response is received from the server. - /// - Returns: A `ClientStreamingCall` with futures for the metadata and status. - internal func update( - callOptions: CallOptions? = nil, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> BidirectionalStreamingCall { - return self.makeBidirectionalStreamingCall( - path: "/echo.Echo/Update", - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [], - handler: handler - ) - } -} - -internal protocol Echo_EchoClientInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when invoking 'get'. - func makeGetInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'expand'. - func makeExpandInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'collect'. - func makeCollectInterceptors() -> [ClientInterceptor] - - /// - Returns: Interceptors to use when invoking 'update'. - func makeUpdateInterceptors() -> [ClientInterceptor] -} - -internal final class Echo_EchoClient: Echo_EchoClientProtocol { - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Echo_EchoClientInterceptorFactoryProtocol? - - /// Creates a client for the echo.Echo service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Echo_EchoClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol Echo_EchoProvider: CallHandlerProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get } - - /// Immediately returns an echo of a request. - func get(request: Echo_EchoRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Splits a request into words and returns each word in a stream of messages. - func expand(request: Echo_EchoRequest, context: StreamingResponseCallContext) -> EventLoopFuture - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// Streams back messages as they are received in an input stream. - func update(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} - -extension Echo_EchoProvider { - internal var serviceName: Substring { return "echo.Echo" } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Get": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeGetInterceptors() ?? [], - userFunction: self.get(request:context:) - ) - - case "Expand": - return ServerStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeExpandInterceptors() ?? [], - userFunction: self.expand(request:context:) - ) - - case "Collect": - return ClientStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCollectInterceptors() ?? [], - observerFactory: self.collect(context:) - ) - - case "Update": - return BidirectionalStreamingServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeUpdateInterceptors() ?? [], - observerFactory: self.update(context:) - ) - - default: - return nil - } - } -} - -internal protocol Echo_EchoServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'get'. - /// Defaults to calling `self.makeInterceptors()`. - func makeGetInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'expand'. - /// Defaults to calling `self.makeInterceptors()`. - func makeExpandInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'collect'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCollectInterceptors() -> [ServerInterceptor] - - /// - Returns: Interceptors to use when handling 'update'. - /// Defaults to calling `self.makeInterceptors()`. - func makeUpdateInterceptors() -> [ServerInterceptor] -} diff --git a/dev/codegen-tests/01-echo/proto/echo.proto b/dev/codegen-tests/01-echo/proto/echo.proto deleted file mode 120000 index 1af81ed41..000000000 --- a/dev/codegen-tests/01-echo/proto/echo.proto +++ /dev/null @@ -1 +0,0 @@ -../../../../Protos/examples/echo/echo.proto \ No newline at end of file diff --git a/dev/codegen-tests/02-multifile/generate-and-diff.sh b/dev/codegen-tests/02-multifile/generate-and-diff.sh deleted file mode 100755 index 2a6162361..000000000 --- a/dev/codegen-tests/02-multifile/generate-and-diff.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${HERE}/../test-boilerplate.sh" - -function all_at_once { - echo "[${TEST}] all_at_once" - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/*.proto - - validate -} - -function one_at_a_time { - echo "[${TEST}] one_at_a_time" - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/a.proto - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/b.proto - - validate -} - -one_at_a_time -all_at_once diff --git a/dev/codegen-tests/02-multifile/golden/a.grpc.swift b/dev/codegen-tests/02-multifile/golden/a.grpc.swift deleted file mode 100644 index 9856ae1b7..000000000 --- a/dev/codegen-tests/02-multifile/golden/a.grpc.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: a.proto -// -import GRPC -import NIO -import SwiftProtobuf - - -/// Usage: instantiate `A_ServiceAClient`, then call methods of this protocol to make API calls. -internal protocol A_ServiceAClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: A_ServiceAClientInterceptorFactoryProtocol? { get } - - func callServiceA( - _ request: A_MessageA, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension A_ServiceAClientProtocol { - internal var serviceName: String { - return "a.ServiceA" - } - - /// Unary call to CallServiceA - /// - /// - Parameters: - /// - request: Request to send to CallServiceA. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func callServiceA( - _ request: A_MessageA, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: "/a.ServiceA/CallServiceA", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCallServiceAInterceptors() ?? [] - ) - } -} - -internal protocol A_ServiceAClientInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when invoking 'callServiceA'. - func makeCallServiceAInterceptors() -> [ClientInterceptor] -} - -internal final class A_ServiceAClient: A_ServiceAClientProtocol { - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: A_ServiceAClientInterceptorFactoryProtocol? - - /// Creates a client for the a.ServiceA service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: A_ServiceAClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol A_ServiceAProvider: CallHandlerProvider { - var interceptors: A_ServiceAServerInterceptorFactoryProtocol? { get } - - func callServiceA(request: A_MessageA, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension A_ServiceAProvider { - internal var serviceName: Substring { return "a.ServiceA" } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "CallServiceA": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCallServiceAInterceptors() ?? [], - userFunction: self.callServiceA(request:context:) - ) - - default: - return nil - } - } -} - -internal protocol A_ServiceAServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'callServiceA'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCallServiceAInterceptors() -> [ServerInterceptor] -} diff --git a/dev/codegen-tests/02-multifile/golden/b.grpc.swift b/dev/codegen-tests/02-multifile/golden/b.grpc.swift deleted file mode 100644 index b9f8590f5..000000000 --- a/dev/codegen-tests/02-multifile/golden/b.grpc.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: b.proto -// -import GRPC -import NIO -import SwiftProtobuf - - -/// Usage: instantiate `B_ServiceBClient`, then call methods of this protocol to make API calls. -internal protocol B_ServiceBClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: B_ServiceBClientInterceptorFactoryProtocol? { get } - - func callServiceB( - _ request: B_MessageB, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension B_ServiceBClientProtocol { - internal var serviceName: String { - return "b.ServiceB" - } - - /// Unary call to CallServiceB - /// - /// - Parameters: - /// - request: Request to send to CallServiceB. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func callServiceB( - _ request: B_MessageB, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: "/b.ServiceB/CallServiceB", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCallServiceBInterceptors() ?? [] - ) - } -} - -internal protocol B_ServiceBClientInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when invoking 'callServiceB'. - func makeCallServiceBInterceptors() -> [ClientInterceptor] -} - -internal final class B_ServiceBClient: B_ServiceBClientProtocol { - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: B_ServiceBClientInterceptorFactoryProtocol? - - /// Creates a client for the b.ServiceB service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: B_ServiceBClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol B_ServiceBProvider: CallHandlerProvider { - var interceptors: B_ServiceBServerInterceptorFactoryProtocol? { get } - - func callServiceB(request: B_MessageB, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension B_ServiceBProvider { - internal var serviceName: Substring { return "b.ServiceB" } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "CallServiceB": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCallServiceBInterceptors() ?? [], - userFunction: self.callServiceB(request:context:) - ) - - default: - return nil - } - } -} - -internal protocol B_ServiceBServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'callServiceB'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCallServiceBInterceptors() -> [ServerInterceptor] -} diff --git a/dev/codegen-tests/02-multifile/proto/a.proto b/dev/codegen-tests/02-multifile/proto/a.proto deleted file mode 100644 index fcdd1efa7..000000000 --- a/dev/codegen-tests/02-multifile/proto/a.proto +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020, 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. -syntax = "proto3"; - -package a; - -import "google/protobuf/empty.proto"; -import "b.proto"; - -message MessageA { - b.MessageB b = 1; -} - -service ServiceA { - rpc CallServiceA(MessageA) returns (google.protobuf.Empty); -} diff --git a/dev/codegen-tests/02-multifile/proto/b.proto b/dev/codegen-tests/02-multifile/proto/b.proto deleted file mode 100644 index b8f2db144..000000000 --- a/dev/codegen-tests/02-multifile/proto/b.proto +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020, 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. -syntax = "proto3"; - -package b; - -import "google/protobuf/empty.proto"; - -message MessageB { - string name = 1; -} - -service ServiceB { - rpc CallServiceB(MessageB) returns (google.protobuf.Empty); -} diff --git a/dev/codegen-tests/03-multifile-with-module-map/generate-and-diff.sh b/dev/codegen-tests/03-multifile-with-module-map/generate-and-diff.sh deleted file mode 100755 index db35990fa..000000000 --- a/dev/codegen-tests/03-multifile-with-module-map/generate-and-diff.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${HERE}/../test-boilerplate.sh" - -MODULE_MAP="${HERE}/swift.modulemap" - -function all_at_once { - echo "[${TEST}] all_at_once" - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_opt=ProtoPathModuleMappings="${MODULE_MAP}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/*.proto - - validate -} - -function one_at_a_time { - echo "[${TEST}] one_at_a_time" - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_opt=ProtoPathModuleMappings="${MODULE_MAP}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/a.proto - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_opt=ProtoPathModuleMappings="${MODULE_MAP}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/b.proto - - validate -} - -one_at_a_time -all_at_once diff --git a/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift b/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift deleted file mode 100644 index be25e087f..000000000 --- a/dev/codegen-tests/03-multifile-with-module-map/golden/a.grpc.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: a.proto -// -import GRPC -import NIO -import SwiftProtobuf -import ModuleB - - -/// Usage: instantiate `A_ServiceAClient`, then call methods of this protocol to make API calls. -internal protocol A_ServiceAClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: A_ServiceAClientInterceptorFactoryProtocol? { get } - - func callServiceA( - _ request: A_MessageA, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension A_ServiceAClientProtocol { - internal var serviceName: String { - return "a.ServiceA" - } - - /// Unary call to CallServiceA - /// - /// - Parameters: - /// - request: Request to send to CallServiceA. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func callServiceA( - _ request: A_MessageA, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: "/a.ServiceA/CallServiceA", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCallServiceAInterceptors() ?? [] - ) - } -} - -internal protocol A_ServiceAClientInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when invoking 'callServiceA'. - func makeCallServiceAInterceptors() -> [ClientInterceptor] -} - -internal final class A_ServiceAClient: A_ServiceAClientProtocol { - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: A_ServiceAClientInterceptorFactoryProtocol? - - /// Creates a client for the a.ServiceA service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: A_ServiceAClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol A_ServiceAProvider: CallHandlerProvider { - var interceptors: A_ServiceAServerInterceptorFactoryProtocol? { get } - - func callServiceA(request: A_MessageA, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension A_ServiceAProvider { - internal var serviceName: Substring { return "a.ServiceA" } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "CallServiceA": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCallServiceAInterceptors() ?? [], - userFunction: self.callServiceA(request:context:) - ) - - default: - return nil - } - } -} - -internal protocol A_ServiceAServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'callServiceA'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCallServiceAInterceptors() -> [ServerInterceptor] -} diff --git a/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift b/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift deleted file mode 100644 index b9f8590f5..000000000 --- a/dev/codegen-tests/03-multifile-with-module-map/golden/b.grpc.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: b.proto -// -import GRPC -import NIO -import SwiftProtobuf - - -/// Usage: instantiate `B_ServiceBClient`, then call methods of this protocol to make API calls. -internal protocol B_ServiceBClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: B_ServiceBClientInterceptorFactoryProtocol? { get } - - func callServiceB( - _ request: B_MessageB, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension B_ServiceBClientProtocol { - internal var serviceName: String { - return "b.ServiceB" - } - - /// Unary call to CallServiceB - /// - /// - Parameters: - /// - request: Request to send to CallServiceB. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func callServiceB( - _ request: B_MessageB, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: "/b.ServiceB/CallServiceB", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeCallServiceBInterceptors() ?? [] - ) - } -} - -internal protocol B_ServiceBClientInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when invoking 'callServiceB'. - func makeCallServiceBInterceptors() -> [ClientInterceptor] -} - -internal final class B_ServiceBClient: B_ServiceBClientProtocol { - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: B_ServiceBClientInterceptorFactoryProtocol? - - /// Creates a client for the b.ServiceB service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: B_ServiceBClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol B_ServiceBProvider: CallHandlerProvider { - var interceptors: B_ServiceBServerInterceptorFactoryProtocol? { get } - - func callServiceB(request: B_MessageB, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension B_ServiceBProvider { - internal var serviceName: Substring { return "b.ServiceB" } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "CallServiceB": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeCallServiceBInterceptors() ?? [], - userFunction: self.callServiceB(request:context:) - ) - - default: - return nil - } - } -} - -internal protocol B_ServiceBServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'callServiceB'. - /// Defaults to calling `self.makeInterceptors()`. - func makeCallServiceBInterceptors() -> [ServerInterceptor] -} diff --git a/dev/codegen-tests/03-multifile-with-module-map/proto/a.proto b/dev/codegen-tests/03-multifile-with-module-map/proto/a.proto deleted file mode 120000 index c72b754c0..000000000 --- a/dev/codegen-tests/03-multifile-with-module-map/proto/a.proto +++ /dev/null @@ -1 +0,0 @@ -../../02-multifile/proto/a.proto \ No newline at end of file diff --git a/dev/codegen-tests/03-multifile-with-module-map/proto/b.proto b/dev/codegen-tests/03-multifile-with-module-map/proto/b.proto deleted file mode 120000 index 4085594f8..000000000 --- a/dev/codegen-tests/03-multifile-with-module-map/proto/b.proto +++ /dev/null @@ -1 +0,0 @@ -../../02-multifile/proto/b.proto \ No newline at end of file diff --git a/dev/codegen-tests/03-multifile-with-module-map/swift.modulemap b/dev/codegen-tests/03-multifile-with-module-map/swift.modulemap deleted file mode 100644 index 1b7aaa269..000000000 --- a/dev/codegen-tests/03-multifile-with-module-map/swift.modulemap +++ /dev/null @@ -1,8 +0,0 @@ -mapping { - module_name: "ModuleA" - proto_file_path: "a.proto" -} -mapping { - module_name: "ModuleB" - proto_file_path: "b.proto" -} diff --git a/dev/codegen-tests/04-service-with-message-import/generate-and-diff.sh b/dev/codegen-tests/04-service-with-message-import/generate-and-diff.sh deleted file mode 100755 index b817e7bb3..000000000 --- a/dev/codegen-tests/04-service-with-message-import/generate-and-diff.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${HERE}/../test-boilerplate.sh" - -function all_at_once { - echo "[${TEST}]" - - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/* - - validate -} - -all_at_once diff --git a/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift b/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift deleted file mode 100644 index fda36f0ac..000000000 --- a/dev/codegen-tests/04-service-with-message-import/golden/service.grpc.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: service.proto -// -import GRPC -import NIO -import SwiftProtobuf - - -/// Usage: instantiate `Codegentest_FooClient`, then call methods of this protocol to make API calls. -internal protocol Codegentest_FooClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Codegentest_FooClientInterceptorFactoryProtocol? { get } - - func get( - _ request: Codegentest_FooMessage, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Codegentest_FooClientProtocol { - internal var serviceName: String { - return "codegentest.Foo" - } - - /// Unary call to Get - /// - /// - Parameters: - /// - request: Request to send to Get. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func get( - _ request: Codegentest_FooMessage, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: "/codegentest.Foo/Get", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeGetInterceptors() ?? [] - ) - } -} - -internal protocol Codegentest_FooClientInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when invoking 'get'. - func makeGetInterceptors() -> [ClientInterceptor] -} - -internal final class Codegentest_FooClient: Codegentest_FooClientProtocol { - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Codegentest_FooClientInterceptorFactoryProtocol? - - /// Creates a client for the codegentest.Foo service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Codegentest_FooClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol Codegentest_FooProvider: CallHandlerProvider { - var interceptors: Codegentest_FooServerInterceptorFactoryProtocol? { get } - - func get(request: Codegentest_FooMessage, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension Codegentest_FooProvider { - internal var serviceName: Substring { return "codegentest.Foo" } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Get": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeGetInterceptors() ?? [], - userFunction: self.get(request:context:) - ) - - default: - return nil - } - } -} - -internal protocol Codegentest_FooServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'get'. - /// Defaults to calling `self.makeInterceptors()`. - func makeGetInterceptors() -> [ServerInterceptor] -} diff --git a/dev/codegen-tests/04-service-with-message-import/proto/message.proto b/dev/codegen-tests/04-service-with-message-import/proto/message.proto deleted file mode 100644 index 065ba3d01..000000000 --- a/dev/codegen-tests/04-service-with-message-import/proto/message.proto +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2020, 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. -syntax = "proto3"; - -package codegentest; - -message FooMessage {} diff --git a/dev/codegen-tests/04-service-with-message-import/proto/service.proto b/dev/codegen-tests/04-service-with-message-import/proto/service.proto deleted file mode 100644 index 70708746b..000000000 --- a/dev/codegen-tests/04-service-with-message-import/proto/service.proto +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2020, 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. -syntax = "proto3"; - -package codegentest; - -import "message.proto"; - -service Foo { - rpc Get(FooMessage) returns (FooMessage) {} -} diff --git a/dev/codegen-tests/05-service-only/generate-and-diff.sh b/dev/codegen-tests/05-service-only/generate-and-diff.sh deleted file mode 100755 index b817e7bb3..000000000 --- a/dev/codegen-tests/05-service-only/generate-and-diff.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${HERE}/../test-boilerplate.sh" - -function all_at_once { - echo "[${TEST}]" - - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/* - - validate -} - -all_at_once diff --git a/dev/codegen-tests/05-service-only/golden/test.grpc.swift b/dev/codegen-tests/05-service-only/golden/test.grpc.swift deleted file mode 100644 index 2d4a36484..000000000 --- a/dev/codegen-tests/05-service-only/golden/test.grpc.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: test.proto -// -import GRPC -import NIO -import SwiftProtobuf - - -/// Usage: instantiate `Codegentest_FooClient`, then call methods of this protocol to make API calls. -internal protocol Codegentest_FooClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Codegentest_FooClientInterceptorFactoryProtocol? { get } - - func bar( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? - ) -> UnaryCall -} - -extension Codegentest_FooClientProtocol { - internal var serviceName: String { - return "codegentest.Foo" - } - - /// Unary call to Bar - /// - /// - Parameters: - /// - request: Request to send to Bar. - /// - callOptions: Call options. - /// - Returns: A `UnaryCall` with futures for the metadata, status and response. - internal func bar( - _ request: SwiftProtobuf.Google_Protobuf_Empty, - callOptions: CallOptions? = nil - ) -> UnaryCall { - return self.makeUnaryCall( - path: "/codegentest.Foo/Bar", - request: request, - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: self.interceptors?.makeBarInterceptors() ?? [] - ) - } -} - -internal protocol Codegentest_FooClientInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when invoking 'bar'. - func makeBarInterceptors() -> [ClientInterceptor] -} - -internal final class Codegentest_FooClient: Codegentest_FooClientProtocol { - internal let channel: GRPCChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Codegentest_FooClientInterceptorFactoryProtocol? - - /// Creates a client for the codegentest.Foo service. - /// - /// - Parameters: - /// - channel: `GRPCChannel` to the service host. - /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. - /// - interceptors: A factory providing interceptors for each RPC. - internal init( - channel: GRPCChannel, - defaultCallOptions: CallOptions = CallOptions(), - interceptors: Codegentest_FooClientInterceptorFactoryProtocol? = nil - ) { - self.channel = channel - self.defaultCallOptions = defaultCallOptions - self.interceptors = interceptors - } -} - -/// To build a server, implement a class that conforms to this protocol. -internal protocol Codegentest_FooProvider: CallHandlerProvider { - var interceptors: Codegentest_FooServerInterceptorFactoryProtocol? { get } - - func bar(request: SwiftProtobuf.Google_Protobuf_Empty, context: StatusOnlyCallContext) -> EventLoopFuture -} - -extension Codegentest_FooProvider { - internal var serviceName: Substring { return "codegentest.Foo" } - - /// Determines, calls and returns the appropriate request handler, depending on the request's method. - /// Returns nil for methods not handled by this service. - internal func handle( - method name: Substring, - context: CallHandlerContext - ) -> GRPCServerHandlerProtocol? { - switch name { - case "Bar": - return UnaryServerHandler( - context: context, - requestDeserializer: ProtobufDeserializer(), - responseSerializer: ProtobufSerializer(), - interceptors: self.interceptors?.makeBarInterceptors() ?? [], - userFunction: self.bar(request:context:) - ) - - default: - return nil - } - } -} - -internal protocol Codegentest_FooServerInterceptorFactoryProtocol { - - /// - Returns: Interceptors to use when handling 'bar'. - /// Defaults to calling `self.makeInterceptors()`. - func makeBarInterceptors() -> [ServerInterceptor] -} diff --git a/dev/codegen-tests/05-service-only/proto/test.proto b/dev/codegen-tests/05-service-only/proto/test.proto deleted file mode 100644 index 89374c559..000000000 --- a/dev/codegen-tests/05-service-only/proto/test.proto +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2020, 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. -syntax = "proto3"; - -package codegentest; - -import "google/protobuf/empty.proto"; - -service Foo { - rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty) {} -} diff --git a/dev/codegen-tests/06-test-client-only/generate-and-diff.sh b/dev/codegen-tests/06-test-client-only/generate-and-diff.sh deleted file mode 100755 index 094781e02..000000000 --- a/dev/codegen-tests/06-test-client-only/generate-and-diff.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -source "${HERE}/../test-boilerplate.sh" - -function all_at_once { - echo "[${TEST}]" - - prepare - - protoc \ - --proto_path="${PROTO_DIR}" \ - --plugin="${PROTOC_GEN_GRPC_SWIFT}" \ - --grpc-swift_opt=Server=false,Client=false,TestClient=true \ - --grpc-swift_out="${OUTPUT_DIR}" \ - "${PROTO_DIR}"/* - - validate -} - -all_at_once diff --git a/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift b/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift deleted file mode 100644 index 196bb33a4..000000000 --- a/dev/codegen-tests/06-test-client-only/golden/test.grpc.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// DO NOT EDIT. -// swift-format-ignore-file -// -// Generated by the protocol buffer compiler. -// Source: test.proto -// -import GRPC -import NIO -import SwiftProtobuf - - -internal final class Codegentest_FooTestClient: Codegentest_FooClientProtocol { - private let fakeChannel: FakeChannel - internal var defaultCallOptions: CallOptions - internal var interceptors: Codegentest_FooClientInterceptorFactoryProtocol? - - internal var channel: GRPCChannel { - return self.fakeChannel - } - - internal init( - fakeChannel: FakeChannel = FakeChannel(), - defaultCallOptions callOptions: CallOptions = CallOptions(), - interceptors: Codegentest_FooClientInterceptorFactoryProtocol? = nil - ) { - self.fakeChannel = fakeChannel - self.defaultCallOptions = callOptions - self.interceptors = interceptors - } - - /// Make a unary response for the Bar RPC. This must be called - /// before calling 'bar'. See also 'FakeUnaryResponse'. - /// - /// - Parameter requestHandler: a handler for request parts sent by the RPC. - internal func makeBarResponseStream( - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) -> FakeUnaryResponse { - return self.fakeChannel.makeFakeUnaryResponse(path: "/codegentest.Foo/Bar", requestHandler: requestHandler) - } - - internal func enqueueBarResponse( - _ response: Codegentest_BarResponse, - _ requestHandler: @escaping (FakeRequestPart) -> () = { _ in } - ) { - let stream = self.makeBarResponseStream(requestHandler) - // This is the only operation on the stream; try! is fine. - try! stream.sendMessage(response) - } - - /// Returns true if there are response streams enqueued for 'Bar' - internal var hasBarResponsesRemaining: Bool { - return self.fakeChannel.hasFakeResponseEnqueued(forPath: "/codegentest.Foo/Bar") - } -} - diff --git a/dev/codegen-tests/06-test-client-only/proto/test.proto b/dev/codegen-tests/06-test-client-only/proto/test.proto deleted file mode 100644 index 1e7d233b9..000000000 --- a/dev/codegen-tests/06-test-client-only/proto/test.proto +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2020, 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. -syntax = "proto3"; - -package codegentest; - -service Foo { - rpc Bar(BarRequest) returns (BarResponse) {} -} - -message BarRequest { - string text = 1; -} - -message BarResponse { - string text = 1; -} diff --git a/dev/codegen-tests/README.md b/dev/codegen-tests/README.md deleted file mode 100644 index ef412062b..000000000 --- a/dev/codegen-tests/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# `protoc-gen-grpc-swift` Tests - -This directory contains tests for the `protoc-gen-grpc-swift` plugin. - -Each test runs `protoc` with the `protoc-gen-grpc-swift` plugin with input -`.proto` files and compares the generated output to "good" output files. Each -test directory must contain the following files/directories: - -- `proto/` a directory containing the input `.proto` files -- `golden/` a directory containing the good generated code -- `generate-and-diff.sh` for generating and diffing the generated files against - the golden output - -The tests also require that the absolute path of the plugin is set in the -`PROTOC_GEN_GRPC_SWIFT` environment variable. - -## Running the Tests - -All Tests can be run by invoking: - -```bash -./run-tests.sh -``` - -Individual tests can be run by invoking the `generate-and-diff.sh` script in -the relevant test directory: - -```bash -./01-echo/generate-and-diff.sh -``` diff --git a/dev/codegen-tests/run-tests.sh b/dev/codegen-tests/run-tests.sh deleted file mode 100755 index 1403cbc30..000000000 --- a/dev/codegen-tests/run-tests.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -find . -type f -name "generate-and-diff.sh" -print \ - | sort \ - | while IFS= read -r filename; do "$filename"; done diff --git a/dev/codegen-tests/test-boilerplate.sh b/dev/codegen-tests/test-boilerplate.sh deleted file mode 100644 index 21064e9a0..000000000 --- a/dev/codegen-tests/test-boilerplate.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -# The name of the test, i.e. the directory it is contained in. -TEST=$(basename "${HERE}") - -# Directory to read .proto files from. -PROTO_DIR="${HERE}/proto" - -# Root directory into which generated code should be written. -OUTPUT_DIR="${HERE}/generated" - -# Root directory containing the known good generated code. -GOLDEN_DIR="${HERE}/golden" - -# Export these so they're available in the test scripts. -export TEST PROTO_DIR OUTPUT_DIR GOLDEN_DIR - -function prepare { - rm -rf "${OUTPUT_DIR}" - mkdir "${OUTPUT_DIR}" -} - -function validate { - diff "${OUTPUT_DIR}" "${GOLDEN_DIR}" -} diff --git a/scripts/format.sh b/dev/format.sh similarity index 92% rename from scripts/format.sh rename to dev/format.sh index f9ded9164..4554e2f19 100755 --- a/scripts/format.sh +++ b/dev/format.sh @@ -61,7 +61,7 @@ if "$lint"; then "${repo}/Sources" \ "${repo}/Tests" \ "${repo}/Plugins" \ - "${repo}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then @@ -80,8 +80,7 @@ elif "$format"; then --parallel --recursive --in-place \ "${repo}/Sources" \ "${repo}/Tests" \ - "${repo}/Plugins" \ - "${repo}/Performance/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ + "${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then diff --git a/scripts/license-check.sh b/dev/license-check.sh similarity index 93% rename from scripts/license-check.sh rename to dev/license-check.sh index e006af8bc..49dfb4f44 100755 --- a/scripts/license-check.sh +++ b/dev/license-check.sh @@ -105,13 +105,7 @@ check_copyright_headers() { done < <(find . -name '*.swift' \ ! -name '*.pb.swift' \ ! -name '*.grpc.swift' \ - ! -name 'LinuxMain.swift' \ - ! -name 'XCTestManifests.swift' \ ! -path './Sources/GRPCCore/Documentation.docc/*' \ - ! -path './FuzzTesting/.build/*' \ - ! -path './Performance/QPSBenchmark/.build/*' \ - ! -path './Performance/Benchmarks/.build/*' \ - ! -path './scripts/.swift-format-source/*' \ ! -path './.build/*') } diff --git a/scripts/sanity.sh b/dev/sanity.sh similarity index 100% rename from scripts/sanity.sh rename to dev/sanity.sh diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 52535da81..000000000 --- a/docs/api.md +++ /dev/null @@ -1,144 +0,0 @@ -# gRPC Swift Public API - -gRPC Swift follows [Semantic Versioning 2.0.0][semver] (SemVer) which requires -that projects declare a public API. - -For the purpose of this document we consider the gRPC Swift public API to be -limited to: - -1. the `GRPC` module (i.e., code in in [`Sources/GRPC`](../Sources/GRPC)), and -1. modules generated by the `protoc-gen-grpc-swift` plugin. - -Below we cover what consitutes the public API of each, how to use them in an -acceptable manner, and compatability between `GRPC` and modules generated by -`protoc-gen-grpc-swift`. - -## `GRPC` and Generated Modules - -All exported types and methods from the [`GRPC`](../Sources/GRPC) module and those -generated by `protoc-gen-grpc-swift` are considered public API except for the -following: - -- types which are prefixed with an an underscore (`_`) -- all methods on types whose name is prefixed with an underscore (`_`) -- methods which are prefixed with an underscore (`_`) - -**Examples** - -- `Server.insecure(group:)` **is** part of the public API. -- `_GRPCClientChannelHandler.channelRead(context:data:)` **is not** part of the - public API, the type `_GRPCClientChannelHandler` starts with an underscore. -- `EmbeddedChannel._configureForEmbeddedServerTest()` **is not** part of the - public API, the method starts with an underscore. - -### Acceptable Use of the API - -#### Conforming Types to Protocols - -In gRPC Swift, and more generally Swift, it is only acceptable to conform a type -to a protocol if you control either the type, the protocol, or both. - -- You must not conform any types from `GRPC` or generated code to protocols - which you do not own. -- You must not add conformance to protocols defined by `GRPC` or generated - code for types you do not own. - -**Examples** - -- `extension MyMessageSerializer: MessageSerializer { ... }` is acceptable - assuming `MyMessageSerializer` exists in your own codebase. -- `extension GRPCPayloadSerializer: DebugStringConvertible { ... }` is **not** - acceptable, `GRPCPayloadSerializer` is defined in `GRPC` and - `DebugStringConvertible` is a standard library protocol. - -#### Extending `GRPC` or Generated Types - -Extending `GRPC` or generated types with `private` or `internal` methods and -properties is always allowed. - -In order to avoid the potential for clashing names, extending `GRPC` or -generated types with `public` methods and properties is acceptable if: - -- The extension uses a type you own, either as a return type or as a non-default - argument. -- The extension is prefixed with a name which will avoid avoid ambiguity (such - as the name of your module). - -**Examples** - -The following **is not** acceptable since it is public and does not use a type -owned by the implementer of the extension. - -```swift -extension Server.Builder { - public func withSignalHandler(_ handler: @escaping (Int) -> Void) -> Self { - ... - } -} -``` - -The following **is** acceptable, `MySignal` is defined in the same module as the -extension and is used as a non-default argument. - -```swift -public struct MySignal { - public var signal: Int -} - -extension Server.Builder { - public func withSignalHandler(_ handler: @escaping (MySignal) -> Void) -> Self { - ... - } -} -``` - -The following **is** acceptable because the method is `internal`. - -```swift -extension Server.Builder { - internal func withSignalHandler(_ handler: @escaping (Int) -> Void) -> Self { - ... - } -} -``` - -### Promises the `GRPC` team make - -Before releasing a new major version, i.e. gRPC Swift 2.0.0, we promise that: - -- None of the public API will be knowingly removed. We will restore any - accidental removals as we become aware of them. -- All additions to the global namespace will be prefixed with `GRPC` or `gRPC`. - -We may deprecate APIs before the next major release, however, they will remain -supported. - -**Examples** - -- We **might** add a new type called `GRPCChannelPool` -- We **might** add a new module called `GRPCTestKit` -- We **might** add a new gloabl function called `gRPCRun` -- We **will not** add a new type called `ChannelPool` -- We **will not** add a new module called `TestKit` -- We **will not** add a new gloabl function called `run` - -## Generated Code - -gRPC Swift generates code via its `protoc` plugin `protoc-gen-grpc-swift`. -In order to develop the library over time without breaking existing generated -code it is important to lay out any API and compatability guarantees. - -Before releasing a new major version, i.e. gRPC Swift 2.0.0, we promise that: - -- The `GRPC` module will be backward compatible with all code generated by - the `protoc-gen-grpc-swift` plugin. -- Generated code will not have its API broken by being updated. - -**Examples** - -- Code generated with `protoc-gen-grpc-swift` 1.3.0 will always be compatible - with `GRPC` 1.3.0 and higher. -- Code generated with `protoc-gen-grpc-swift` 1.5.0 offers no compatability - guarantees compatible with `GRPC` versions lower than 1.5.0. - -[semver]: https://semver.org/spec/v2.0.0.html diff --git a/docs/apple-platforms.md b/docs/apple-platforms.md deleted file mode 100644 index a5e57485b..000000000 --- a/docs/apple-platforms.md +++ /dev/null @@ -1,27 +0,0 @@ -# Apple Platforms - -NIO offers extensions to provide first-class support for Apple platforms (iOS -12+, macOS 10.14+, tvOS 12+, watchOS 6+) via [NIO Transport Services][nio-ts]. -NIO Transport Services uses [Network.framework][network-framework] and -`DispatchQueue`s to schedule tasks. - -To use NIO Transport Services in gRPC Swift you need to provide a -`NIOTSEventLoopGroup` to the builder for your client or server. -gRPC Swift provides a helper method to provide the correct `EventLoopGroup` -based on the network preference: - -```swift -PlatformSupport.makeEventLoopGroup(loopCount:networkPreference:) -> EventLoopGroup -``` - -Here `networkPreference` defaults to `.best`, which chooses the -`.networkFramework` implementation if it is available (iOS 12+, macOS 10.14+, -tvOS 12+, watchOS 6+) and uses `.posix` otherwise. - -Note that the TLS implementation used by gRPC depends on the type of `EventLoopGroup` -provided to the client or server and that some combinations are not supported. -See the [TLS docs][docs-tls] for more. - -[network-framework]: https://developer.apple.com/documentation/network -[nio-ts]: https://github.com/apple/swift-nio-transport-services -[docs-tls]: ./tls.md diff --git a/docs/async-await-proposal.md b/docs/async-await-proposal.md deleted file mode 100644 index 421765b9c..000000000 --- a/docs/async-await-proposal.md +++ /dev/null @@ -1,473 +0,0 @@ -# Proposal: Async/await support - -## Introduction - -With the introduction of [async/await][SE-0296] in Swift 5.5, it is -now possible to write asynchronous code without the need for callbacks. -Language support for [`AsyncSequence`][SE-0298] also allows for writing -functions that return values over time. - -We would like to explore how we could offer APIs that make use of these new -language features to allow users to implement and call gRPC services using -these new idioms. - -This proposal describes what these APIs could look like and explores some of -the potential usability concerns. - -## Motivation - -Consider the familiar example Echo service which exposes all four types of -call: unary, client-streaming, server-streaming, and bidirectional-streaming. -It is defined as follows: - -### Example Echo service - -```proto -service Echo { - // Immediately returns an echo of a request. - rpc Get(EchoRequest) returns (EchoResponse) {} - - // Splits a request into words and returns each word in a stream of messages. - rpc Expand(EchoRequest) returns (stream EchoResponse) {} - - // Collects a stream of messages and returns them concatenated when the caller closes. - rpc Collect(stream EchoRequest) returns (EchoResponse) {} - - // Streams back messages as they are received in an input stream. - rpc Update(stream EchoRequest) returns (stream EchoResponse) {} -} - -message EchoRequest { - // The text of a message to be echoed. - string text = 1; -} - -message EchoResponse { - // The text of an echo response. - string text = 1; -} -``` - -### Existing server API - -To implement the Echo server, the user must implement a type that conforms to -the following generated protocol: - -```swift -/// To build a server, implement a class that conforms to this protocol. -public protocol Echo_EchoProvider: CallHandlerProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get } - - /// Immediately returns an echo of a request. - func get(request: Echo_EchoRequest, context: StatusOnlyCallContext) -> EventLoopFuture - - /// Splits a request into words and returns each word in a stream of messages. - func expand(request: Echo_EchoRequest, context: StreamingResponseCallContext) -> EventLoopFuture - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect(context: UnaryResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> - - /// Streams back messages as they are received in an input stream. - func update(context: StreamingResponseCallContext) -> EventLoopFuture<(StreamEvent) -> Void> -} -``` - -### Existing example server implementation - -Here is an example implementation of the bidirectional streaming handler for `update`: - -```swift -public func update( - context: StreamingResponseCallContext -) -> EventLoopFuture<(StreamEvent) -> Void> { - var count = 0 - return context.eventLoop.makeSucceededFuture({ event in - switch event { - case let .message(message): - let response = Echo_EchoResponse.with { - $0.text = "Swift echo update (\(count)): \(message.text)" - } - count += 1 - context.sendResponse(response, promise: nil) - - case .end: - context.statusPromise.succeed(.ok) - } - }) -} -``` - -This API exposes a number incidental types and patterns that the user need -concern themselves with that are not specific to their application: - -1. The fact that gRPC is implemented on top of NIO is not hidden from the user - and they need to implement an API in terms of an `EventLoopFuture` and access - an `EventLoop` from the call context. -2. There is a different context type passed to the user function for each - different type of call and this context is generic over the response type. -3. In the server- and bidirectional-streaming call handlers, an added layer of - asynchrony is exposed. That is, the user must return a _future_ for - a closure that will handle incoming events. -4. The user _must_ fulfil the `statusPromise` when it receives `.end`, but there -is nothing that enforces this. - -### Existing client API - -Turning our attention to the client API, in order to make calls to the Echo server, the user must instantiate the generated `Echo_EchoClient` which provides the following API: - -```swift -public protocol Echo_EchoClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Echo_EchoClientInterceptorFactoryProtocol? { get } - - func get( - _ request: Echo_EchoRequest, - callOptions: CallOptions? - ) -> UnaryCall - - func expand( - _ request: Echo_EchoRequest, - callOptions: CallOptions?, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> ServerStreamingCall - - func collect( - callOptions: CallOptions? - ) -> ClientStreamingCall - - func update( - callOptions: CallOptions?, - handler: @escaping (Echo_EchoResponse) -> Void - ) -> BidirectionalStreamingCall -} -``` - -### Existing example client usage - -Here is an example use of the client, making a bidirectional streaming call to -`update`: - -```swift -// Update is a bidirectional streaming call; provide a response handler. -let update = client.update { response in - print("update received: \(response.text)") -} - -// Send a bunch of messages to the service. -for word in ["boyle", "jeffers", "holt"] { - let request = Echo_EchoRequest.with { $0.text = word } - update.sendMessage(request, promise: nil) -} - -// Close the request stream. -update.sendEnd(promise: nil) - -// wait() for the call to terminate -let status = try update.status.wait() -print("update completed with status: \(status.code)") -``` - -This API also exposes a number incidental types and patterns that the user need -concern themselves with that are not specific to their application: - -1. It exposes the NIO types to the user, allowing the provision of an - `EventLoopPromise` when sending messages and requiring the use of - `EventLoopFuture` to obtain the `status` of the call. -2. Code does not read in a straight line due to the need to provide a completion - handler when making the call. - -## Proposed solution - -### Proposed server API - -We propose generating the following new protocol which the user must conform to in -order to implement the server: - -```swift -/// To build a server, implement a class that conforms to this protocol. -public protocol Echo_AsyncEchoProvider: CallHandlerProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get } - - /// Immediately returns an echo of a request. - func get( - request: Echo_EchoRequest, - context: AsyncServerCallContext - ) async throws -> Echo_EchoResponse - - /// Splits a request into words and returns each word in a stream of messages. - func expand( - request: Echo_EchoRequest, - responseStreamWriter: AsyncResponseStreamWriter, - context: AsyncServerCallContext - ) async throws - - /// Collects a stream of messages and returns them concatenated when the caller closes. - func collect( - requests: GRPCAsyncStream, - context: AsyncServerCallContext - ) async throws -> Echo_EchoResponse - - /// Streams back messages as they are received in an input stream. - func update( - requests: GRPCAsyncStream, - responseStreamWriter: AsyncResponseStreamWriter, - context: AsyncServerCallContext - ) async throws -} -``` - -Here is an example implementation of the bidirectional streaming `update` -handler using this new API: - -```swift -public func update( - requests: GRPCAsyncStream, - responseStreamWriter: AsyncResponseStreamWriter, - context: AsyncServerCallContext -) async throws { - var count = 0 - for try await request in requests { - let response = Echo_EchoResponse.with { - $0.text = "Swift echo update (\(count)): \(request.text)" - } - count += 1 - try await responseStreamWriter.sendResponse(response) - } -} -``` - -This API addresses the previously noted drawbacks the existing API: - -> 1. The fact that gRPC is implemented on top of NIO is not hidden from the user -> and they need to implement an API in terms of an `EventLoopFuture` and needs -> to access an `EventLoop` from the call context. - -There is no longer a need for the adopter to `import NIO` nor implement anything -in terms of NIO types. Instead they now implement an `async` function. - -> 2. There is a different context type passed to the user function for each -> different type of call and this context is generic over the response type. - -The same non-generic `AsyncServerCallContext` is passed to the user function -regardless of the type of RPC. - -> 3. In the server- and bidirectional-streaming call handlers, an added layer of -> asynchrony is exposed. That is, the user must return a _future_ for -> a closure that will handle incoming events. - -The user function consumes requests from an `AsyncSequence`, using the new -language idioms. - -> 4. The user _must_ fulfil the `statusPromise` when it receives `.end` but there -> is nothing that enforces this. - -The user need simply return from the function or throw an error. The closing of -the call is handled by the library. - -If the user function throws a `GRPCStatus` (which already conforms to `Error`) -or a value of a type that conforms to `GRPCStatusTransformable` then the library -will take care of setting the RPC status appropriately. If the user throws -anything else then the library will still take care of setting the status -appropriately, but in this case it will use `internalError` for the RPC status. - -### Proposed client API - -We propose generating a client which conforms to this new protocol: - -```swift -public protocol Echo_AsyncEchoClientProtocol: GRPCClient { - var serviceName: String { get } - var interceptors: Echo_EchoClientInterceptorFactoryProtocol? { get } - - func makeGetCall( - _ request: Echo_EchoRequest, - callOptions: CallOptions? - ) -> AsyncUnaryCall - - func makeExpandCall( - _ request: Echo_EchoRequest, - callOptions: CallOptions? - ) -> AsyncServerStreamingCall - - func makeCollectCall( - callOptions: CallOptions? - ) -> AsyncClientStreamingCall - - func makeUpdateCall( - callOptions: CallOptions? - ) -> AsyncBidirectionalStreamingCall -} -``` - -Here is an example use of the new client API, making a bidirectional streaming -call to `update`: - -```swift -// No longer provide a response handler when making the call. -let update = client.makeUpdateCall() - -Task { - // Send requests as before but using `await` instead of a `promise`. - for word in ["foo", "bar", "baz"] { - try await update.sendMessage(.with { $0.text = word }) - } - // Close the request stream, again using `await` instead of a `promise`. - try await update.sendEnd() -} - -// Consume responses as an AsyncSequence. -for try await response in update.responseStream { - print("update received: \(response.text)") -} - -// Wait for the call to terminate, but using `await` rather than a future. -let status = await update.status -print("update completed with status: \(status.code)") -``` - -As highlighted in the code comments above, it allows the user to write -staight-line code, using the new async/await language support, and for -consuming responses from an `AsyncSequence` using the new `for try await ... in -{ ... }` idiom. - -Specifically, this API addresses the previously noted drawbacks the existing -client API [anchor link]: - -> 1. It exposes the NIO types to the user, allowing for the provision of an -> `EventLoopPromise` when sending messages and requiring the use of -> `EventLoopFuture` to obtain the `status` of the call. - -NIO types are not exposed. Asynchronous functions and properties are marked as -`async` and the user makes use of the `await` keyword when using them. - -> 2. Code does not read in a straight line due to the need to provide a completion -> handler when making the call. - -While the above example is reasonably artificial, the response handling code can -be placed after the code that is sending requests. - -#### Simple/safe wrappers - -The client API above provides maximum expressibility but has a notable drawback -that was not present in the existing callback-based API. Specifically, in the -server- and bidirectional-streaming cases, if the user does not consume the -responses then waiting on the status will block indefinitely. This can be -considered the converse of the drawback with the _existing_ server API that this -proposal addresses. - -It is for this reason that the above proposed client APIs have slightly obscured -names (e.g. `makeUpdateCall` instead of `update`) and we propose also generating -additional, less expressive, but safer APIs. These APIs will not expose the RPC -metadata (e.g. the status, initial metadata, trailing metadata), but will -instead either simply return the response(s) or throw an error. - -In addition to avoiding the pitfall of the expressive counterparts, the client- -and bidirectional-streaming calls provide the ability to pass an `AsyncSequence` -of requests. In this way, the underlying library takes care of ensuring that no -part of the RPC goes unterminated (both the request and response streams). It -also offers an opportunity for users who have an `AsyncSequence` from which they -are generating requests to make use of the combinators of `AsyncSequence` to not -have to introduce unnecessary synchrony. - -We expect these will be sufficient for a lot of client use cases and, because -they do not have the same pitfalls as their more expressive counterparts, we -propose they be generated with the "plain names" of the RPC calls (e.g. -`update`). - -For example, these are the additional APIs we propose to generate: - -```swift -extension Echo_AsyncEchoClientProtocol { - public func get( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) async throws -> Echo_EchoResponse { ... } - - public func collect( - requests: RequestStream, - callOptions: CallOptions? = nil - ) async throws -> Echo_EchoResponse - where RequestStream: AsyncSequence, RequestStream.Element == Echo_EchoRequest { ... } - - public func expand( - _ request: Echo_EchoRequest, - callOptions: CallOptions? = nil - ) -> GRPCAsyncStream { ... } - - public func update( - requests: RequestStream, - callOptions: CallOptions? = nil - ) -> GRPCAsyncStream - where RequestStream: AsyncSequence, RequestStream.Element == Echo_EchoRequest { ... } -``` - -Here is an example use of the safer client API, making a bidirectional streaming -call to `update` using an `AsyncSequence` of requests: - -```swift -let requestStream: AsyncStream = ... // constructed elsewhere - -for try await response in client.update(requests: requestStream) { - print("update received: \(response.text)") -} -``` - -Note how there is no call handler that the user needs to hold onto and use in a -safe way, they just pass in a stream of requests and consume a stream of -responses. - -## Alternatives considered - -### Using throwing effectful read-only properties - -[Effectful read-only properties][SE-0310] were also recently added to the Swift -language. These allow for a read-only property to be marked with effects (e.g. -`async` and/or `throws`). - -We considered making the status and trailing metadata properties that could -throw an error if they are awaited before the call was in the final state. The -drawback here is that you may _actually_ want to wait on the completion of the -call if for example your responses were being consumed in a concurrent task. - -### Adding a throwing function to access the status - -When looking at the C# implementation (which is of interest because C# also has -async/await language constructs), they provide throwing APIs to access the final -metadata for the RPC. We could consider doing the same and have not ruled it -out. - -### Opaque return type for response streams - -It would be nice if we didn't have to return the `GRPCAsyncStream` wrapper type -for server-streaming RPCs. Ideally we would be able to declare an opaque return -type with a constraint on its associated type. This would make the return type of -server-streaming calls more symmetric with the inputs to client-streaming calls. -For example, the bidirectional API could be defined as follows: - -```swift -func update( - requests: RequestStream, - callOptions: CallOptions? = nil -) -> some AsyncSequence where Element == Echo_EchoResponse -where RequestStream: AsyncSequence, RequestStream.Element == Echo_EchoRequest -``` - -Unfortunately this isn't currently supported by `AsyncSequence`, but it _has_ -been called out as a [possible future enhancement][opaque-asyncsequence]. - -### Backpressure - -This proposal makes no attempt to implement backpressure, which is also not -handled by the existing implementation. However the API should not prevent -implementing backpressure in the future. - -Since the `GRPCAsyncStream` of responses is wrapping [`AsyncStream`][SE-0314], -it may be able to offer backpressure by making use of its `init(unfolding:)`, or -`AsyncResponseStreamWriter.sendResponse(_:)` could block when the buffer is -full. - -[SE-0296]: https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md -[SE-0298]: https://github.com/apple/swift-evolution/blob/main/proposals/0298-asyncsequence.md -[SE-0310]: https://github.com/apple/swift-evolution/blob/main/proposals/0310-effectful-readonly-properties.md -[SE-0314]: https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md -[opaque-asyncsequence]: https://github.com/apple/swift-evolution/blob/0c2f85b3/proposals/0298-asyncsequence.md#opaque-types diff --git a/docs/basic-tutorial.md b/docs/basic-tutorial.md deleted file mode 100644 index 2d63d4887..000000000 --- a/docs/basic-tutorial.md +++ /dev/null @@ -1,614 +0,0 @@ -# Basic Tutorial - -This tutorial provides a basic Swift programmer's introduction to working with -gRPC. - -By walking through this example you'll learn how to: - -- Define a service in a .proto file. -- Generate server and client code using the protocol buffer compiler. -- Use the Swift gRPC API to write a simple client and server for your service. - -It assumes that you have read the [Overview][grpc-docs] and are familiar -with [protocol buffers][protocol-buffers]. Note that the example in this -tutorial uses the [proto3][protobuf-releases] version of the protocol -buffers language: you can find out more in the [proto3 language -guide][protobuf-docs]. - - -### Why use gRPC? - -Our example is a simple route mapping application that lets clients get -information about features on their route, create a summary of their route, and -exchange route information such as traffic updates with the server and other -clients. - -With gRPC we can define our service once in a .proto file and implement clients -and servers in any of gRPC's supported languages, which in turn can be run in -environments ranging from servers inside Google to your own tablet - all the -complexity of communication between different languages and environments is -handled for you by gRPC. We also get all the advantages of working with protocol -buffers, including efficient serialization, a simple IDL, and easy interface -updating. - -### Example code and setup - -The example code for our tutorial is in -[grpc/grpc-swift/Examples/v1/RouteGuide][routeguide-source]. -To download the example, clone the latest release in `grpc-swift` repository by -running the following command (replacing `x.y.z` with the latest release, for -example `1.7.0`): - -```sh -$ git clone -b x.y.z https://github.com/grpc/grpc-swift -``` - -Then change your current directory to `grpc-swift/Examples/v1/RouteGuide`: - -```sh -$ cd grpc-swift/Examples/v1/RouteGuide -``` - - -### Defining the service - -Our first step (as you'll know from the [Overview][grpc-docs]) is to -define the gRPC *service* and the method *request* and *response* types using -[protocol buffers][protocol-buffers]. You can see the complete .proto file in -[`grpc-swift/Protos/examples/route_guide/route_guide.proto`][routeguide-proto]. - -To define a service, we specify a named `service` in the .proto file: - -```proto -service RouteGuide { - ... -} -``` - -Then we define `rpc` methods inside our service definition, specifying their -request and response types. gRPC lets you define four kinds of service methods, -all of which are used in the `RouteGuide` service: - -- A *simple RPC* where the client sends a request to the server using the stub - and waits for a response to come back, just like a normal function call. - -```proto -// Obtains the feature at a given position. -rpc GetFeature(Point) returns (Feature) {} -``` - -- A *server-side streaming RPC* where the client sends a request to the server - and gets a stream to read a sequence of messages back. The client reads from - the returned stream until there are no more messages. As you can see in our - example, you specify a server-side streaming method by placing the `stream` - keyword before the *response* type. - -```proto -// Obtains the Features available within the given Rectangle. Results are -// streamed rather than returned at once (e.g. in a response message with a -// repeated field), as the rectangle may cover a large area and contain a -// huge number of features. -rpc ListFeatures(Rectangle) returns (stream Feature) {} -``` - -- A *client-side streaming RPC* where the client writes a sequence of messages - and sends them to the server, again using a provided stream. Once the client - has finished writing the messages, it waits for the server to read them all - and return its response. You specify a client-side streaming method by placing - the `stream` keyword before the *request* type. - -```proto -// Accepts a stream of Points on a route being traversed, returning a -// RouteSummary when traversal is completed. -rpc RecordRoute(stream Point) returns (RouteSummary) {} -``` - -- A *bidirectional streaming RPC* where both sides send a sequence of messages - using a read-write stream. The two streams operate independently, so clients - and servers can read and write in whatever order they like: for example, the - server could wait to receive all the client messages before writing its - responses, or it could alternately read a message then write a message, or - some other combination of reads and writes. The order of messages in each - stream is preserved. You specify this type of method by placing the `stream` - keyword before both the request and the response. - -```proto -// Accepts a stream of RouteNotes sent while a route is being traversed, -// while receiving other RouteNotes (e.g. from other users). -rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} -``` - -Our .proto file also contains protocol buffer message type definitions for all -the request and response types used in our service methods - for example, here's -the `Point` message type: - -```proto -// Points are represented as latitude-longitude pairs in the E7 representation -// (degrees multiplied by 10**7 and rounded to the nearest integer). -// Latitudes should be in the range +/- 90 degrees and longitude should be in -// the range +/- 180 degrees (inclusive). -message Point { - int32 latitude = 1; - int32 longitude = 2; -} -``` - -### Generating client and server code - -Next we need to generate the gRPC client and server interfaces from our .proto -service definition. We do this using the protocol buffer compiler `protoc` with -two plugins: one providing protocol buffer support for Swift (via [Swift -Protobuf][swift-protobuf]) and the other for gRPC. You need to use the -[proto3][protobuf-releases] compiler (which supports both proto2 and proto3 -syntax) in order to generate gRPC services. - -For simplicity, we've provided a shell script ([grpc-swift/Protos/generate.sh][run-protoc]) that -runs protoc for you with the appropriate plugin, input, and output (if you want -to run this yourself, make sure you've installed protoc first): - -```sh -$ Protos/generate.sh -``` - -Running this command generates the following files in the -`Examples/v1/RouteGuide/Model` directory: - -- `route_guide.pb.swift`, which contains the implementation of your generated - message classes -- `route_guide.grpc.swift`, which contains the implementation of your generated - service classes - -Let's look at how to run the same command manually: - -```sh -$ protoc Protos/examples/route_guide/route_guide.proto \ - --proto_path=Protos/examples/route_guide \ - --plugin=./.build/debug/protoc-gen-swift \ - --swift_opt=Visibility=Public \ - --swift_out=Examples/v1/RouteGuide/Model \ - --plugin=./.build/debug/protoc-gen-grpc-swift \ - --grpc-swift_opt=Visibility=Public \ - --grpc-swift_out=Examples/v1/RouteGuide/Model -``` - -We invoke the protocol buffer compiler `protoc` with the path to our service -definition `route_guide.proto` as well as specifying the path to search for -imports. We then specify the path to the [Swift Protobuf][swift-protobuf] plugin -and any options. In our case the generated code is in a separate module to the -client and server, so the generated code must have `Public` visibility. We also -specified that the 'async' client and server should be generated. The 'async' -versions use Swift concurrency features introduced in Swift 5.5. We then -specify the directory into which the generated messages should be written. The -remainder of the arguments are very similar but pertain to the generation of the -service code and use the `protoc-gen-grpc-swift` plugin. - -### Creating the server - -First let's look at how we create a `RouteGuide` server. If you're only -interested in creating gRPC clients, you can skip this section and go straight -to [Creating the client](#creating-the-client) (though you might find it interesting -anyway!). - -There are two parts to making our `RouteGuide` service do its job: - -- Implementing the service protocol generated from our service definition: doing - the actual "work" of our service. -- Running a gRPC server to listen for requests from clients and return the - service responses. - -You can find our example `RouteGuide` provider in -[grpc-swift/Examples/v1/RouteGuide/Server/RouteGuideProvider.swift][routeguide-provider]. -Let's take a closer look at how it works. - -#### Implementing RouteGuide - -As you can see, our server has a `RouteGuideProvider` class that extends the -generated `Routeguide_RouteGuideAsyncProvider` protocol: - -```swift -final class RouteGuideProvider: Routeguide_RouteGuideAsyncProvider { -... -} -``` - -#### Simple RPC - -`RouteGuideProvider` implements all our service methods. Let's -look at the simplest type first, `GetFeature`, which just gets a `Point` from -the client and returns the corresponding feature information from its database -in a `Feature`. - -```swift -/// A simple RPC. -/// -/// Obtains the feature at a given position. -/// -/// A feature with an empty name is returned if there's no feature at the given position. -func getFeature( - request point: Routeguide_Point, - context: GRPCAsyncServerCallContext -) async throws -> Routeguide_Feature { - return self.lookupFeature(at: point) ?? Routeguide_Feature.with { - // No feature was found: return an unnamed feature. - $0.name = "" - $0.location = location - } -} - -/// Returns a feature at the given location or an unnamed feature if none exist at that location. -private func lookupFeature( - at location: Routeguide_Point -) -> Routeguide_Feature? { - return self.features.first(where: { - return $0.location.latitude == location.latitude - && $0.location.longitude == location.longitude - }) -} -``` - -`getFeature(request:context:)` takes two parameters: - -- `Routeguide_Point`: the request -- `GRPCAsyncServerCallContext`: a context which exposes various pieces of - information about the call. - -To return our response to the client and complete the call: - -1. We construct and populate a `Routeguide_Feature` response object to return to - the client, as specified in our service definition. In this example, we do - this in a separate private `lookupFeature(at:)` method. -2. We return the feature returned from `lookupFeature(at:)` or an unnamed one if - there was no feature at the given location. - -##### Server-side streaming RPC - -Next let's look at one of our streaming RPCs. `ListFeatures` is a server-side -streaming RPC, so we need to send back multiple `Routeguide_Feature `s to our -client. - -```swift -/// A server-to-client streaming RPC. -/// -/// Obtains the Features available within the given Rectangle. Results are streamed rather than -/// returned at once (e.g. in a response message with a repeated field), as the rectangle may -/// cover a large area and contain a huge number of features. -func listFeatures( - request: Routeguide_Rectangle, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext -) async throws { - let longitudeRange = request.lo.longitude ... request.hi.longitude - let latitudeRange = request.lo.latitude ... request.hi.latitude - - for feature in self.features where !feature.name.isEmpty { - if feature.location.isWithin(latitude: latitudeRange, longitude: longitudeRange) { - try await responseStream.send(feature) - } - } -} -``` - -Like the simple RPC, this method gets a request object (the -`Routeguide_Rectangle` in which our client wants to find `Routeguide_Feature`s), -a stream to write responses on and a context. - -This time, we get as many `Routeguide_Feature` objects as we need to return to -the client (in this case, we select them from the service's feature collection -based on whether they're inside our request `Routeguide_Rectangle`), and write -them each in turn to the response stream using `send(_:)` method on -`responseStream`. - -##### Client-side streaming RPC - -Now let's look at something a little more complicated: the client-side streaming -method `RecordRoute`, where we get a stream of `Routeguide_Point`s from the client and -return a single `Routeguide_RouteSummary` with information about their trip. - -```swift -/// A client-to-server streaming RPC. -/// -/// Accepts a stream of Points on a route being traversed, returning a RouteSummary when traversal -/// is completed. -internal func recordRoute( - requestStream points: GRPCAsyncRequestStream, - context: GRPCAsyncServerCallContext -) async throws -> Routeguide_RouteSummary { - var pointCount: Int32 = 0 - var featureCount: Int32 = 0 - var distance = 0.0 - var previousPoint: Routeguide_Point? - let startTimeNanos = DispatchTime.now().uptimeNanoseconds - - for try await point in points { - pointCount += 1 - - if let feature = self.lookupFeature(at: point), !feature.name.isEmpty { - featureCount += 1 - } - - if let previous = previousPoint { - distance += previous.distance(to: point) - } - - previousPoint = point - } - - let durationInNanos = DispatchTime.now().uptimeNanoseconds - startTimeNanos - let durationInSeconds = Double(durationInNanos) / 1e9 - - return .with { - $0.pointCount = pointCount - $0.featureCount = featureCount - $0.elapsedTime = Int32(durationInSeconds) - $0.distance = Int32(distance) - } -} -``` - -As you can see our method gets a `GRPCAsyncServerCallContext` parameter and a -request stream of points and returns a summary. - -In the method body we iterate over the asynchronous stream of points send by the -client. For each point we: - -- Check if there is a feature at that point. -- Calculate the distance between the point and the last point we saw. - -After the *client* has finished sending points we populate and return a -`Routeguide_RouteSummary`. - -##### Bidirectional streaming RPC - -Finally, let's look at our bidirectional streaming RPC `routeChat()`. - -```swift -func routeChat( - requestStream: GRPCAsyncRequestStream, - responseStream: GRPCAsyncResponseStreamWriter, - context: GRPCAsyncServerCallContext -) async throws { - for try await note in requestStream { - let existingNotes = await self.notes.addNote(note, to: note.location) - - // Respond with all existing notes. - for existingNote in existingNotes { - try await responseStream.send(existingNote) - } - } -} - -final actor Notes { - private var recordedNotes: [Routeguide_Point: [Routeguide_RouteNote]] = [:] - - /// Record a note at the given location and return the all notes which were previously recorded - /// at the location. - func addNote( - _ note: Routeguide_RouteNote, - to location: Routeguide_Point - ) -> ArraySlice { - self.recordedNotes[location, default: []].append(note) - return self.recordedNotes[location]!.dropLast(1) - } -} -``` - -Here we receive a request stream of `Routeguide_RouteNote`s and a response -stream of `Routeguide_RouteNote`s as well as the `GRPCAsyncServerCallContext` -we got in other RPCs. - -For the route chat for iterate over the stream of notes sent by the *client* and -for each note we add it to a `Notes` helper `actor`. When a note is added to -the `Notes` `actor` all notes previously recorded at the same location are -returned and are sent back to the client. - -#### Starting the server - -Once we've implemented all our methods, we also need to start up a gRPC server -so that clients can actually use our service. The following snippet shows how we -do this for our `RouteGuide` service: - -```swift -// Create an event loop group for the server to run on. -let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) -defer { - try! group.syncShutdownGracefully() -} - -// Read the feature database. -let features = try loadFeatures() - -// Create a provider using the features we read. -let provider = RouteGuideProvider(features: features) - -// Start the server and print its address once it has started. -let server = Server.insecure(group: group) - .withServiceProviders([provider]) - .bind(host: "localhost", port: 0) - -server.map { - $0.channel.localAddress -}.whenSuccess { address in - print("server started on port \(address!.port!)") -} - -// Wait on the server's `onClose` future to stop the program from exiting. -_ = try server.flatMap { - $0.onClose -}.wait() -``` -As you can see, we configure and start our server using a builder. - -To do this, we: - -1. Create an insecure server builder; it's insecure because it does not use - TLS. -1. Create an instance of our service implementation class `RouteGuideProvider` - and configure the builder to use it with `withServiceProviders(_:)`, -1. Call `bind(host:port:)` on the builder with the address and port we - want to use to listen for client requests, this starts the server. - -Once the server has started succesfully we print out the port the server is -listening on. We then `wait()` on the server's `onClose` future to stop the -program from exiting (since `close()` is never called on the server). - -## Creating the client - -In this section, we'll look at creating a Swift client for our `RouteGuide` -service. You can see our complete example client code in -[grpc-swift/Examples/v1/RouteGuide/Client/RouteGuideClient.swift][routeguide-client]. - -#### Creating a stub - -To call service methods, we first need to create a *stub*. All generated Swift -stubs are *non-blocking/asynchronous*. - -First we need to create a gRPC channel for our stub, we're not using TLS so we -use the `.plaintext` security transport and specify the server address and port -we want to connect to: - -```swift -let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) -defer { - try? group.syncShutdownGracefully() -} - -let channel = try GRPCChannelPool.with( - target: .host("localhost", port: port), - transportSecurity: .plaintext, - eventLoopGroup: group -) - -let routeGuide = Routeguide_RouteGuideAsyncClient(channel: channel) -``` - -#### Calling service methods - -Now let's look at how we call our service methods. - -##### Simple RPC - -Calling the simple RPC `GetFeature` is straightforward. - - -```swift -let point: Routeguide_Point = .with { - $0.latitude = latitude - $0.longitude = longitude -} - -let feature = try await routeGuide.getFeature(point) -``` - -We create and populate a request protocol buffer object (in our case -`Routeguide_Point`), pass it to the `getFeature()` method on our stub, and -`await` the response `Routeguide_Feature`. - -If an error occurs, it is encoded as a `GRPCStatus` and thrown whilst -`await`-ing the response. - -##### Server-side streaming RPC - -Next, let's look at a server-side streaming call to `ListFeatures`, which -returns a stream of geographical `Feature`s: - -```swift -let rectangle: Routeguide_Rectangle = .with { - $0.lo = .with { - $0.latitude = numericCast(lowLatitude) - $0.longitude = numericCast(lowLongitude) - } - $0.hi = .with { - $0.latitude = numericCast(highLatitude) - $0.longitude = numericCast(highLongitude) - } -} - -for try await feature in routeGuide.listFeatures(rectangle) { - print("Received feature: \(feature)") -} -``` - -As you can see, it's very similar to the simple RPC we just looked at, except -the `listFeatures(_:)` returns a stream of responses. Here we `await` each -response on the stream, once we finish iterating the response stream the call is -complete. - -##### Client-side streaming RPC - -Now for something a little more complicated: the client-side streaming method -`RecordRoute`, where we send a stream of `Routeguide_Point`s to the server and -get back a single `Routeguide_RouteSummary`. - -```swift -let recordRoute = routeGuide.makeRecordRouteCall() - -for _ in 1 ... featuresToVisit { - if let feature = features.randomElement() { - let point = feature.location - try await recordRoute.requestStream.send(point) - } -} - -try await recordRoute.requestStream.finish() -let summary = try await recordRoute.response -``` - -Here we we create a record route call. It has a request stream and a single -`await`-able response for the `Routeguide_RouteSummary`. - -We call `recordRoute.requestStream.send(_:)` for each point we want to send to the -server and `await` for the call to accept the request. - -Once we've finished writing points, we call `recordRoute.requestStream.finish()` -to tell gRPC that we've finished writing on the client side. Once we're done, we -`await` on the `recordRoute.summary` to check that the server responded with. - -##### Bidirectional streaming RPC - -Finally, let's look at our bidirectional streaming RPC `RouteChat`. - -```swift -let notes: [Routeguide_RouteNote] = ... - -try await withThrowingTaskGroup(of: Void.self) { group in - let routeChat = self.routeGuide.makeRouteChatCall() - - group.addTask { - for note in notes { - try await routeChat.requestStream.send(note) - } - try await routeChat.requestStream.finish() - } - - group.addTask { - for try await note in routeChat.responseStream { - print("Received message '\(note.message)' at \(note.location)") - } - } - - try await group.waitForAll() -} -``` - -As with our client-side streaming example, we have a `routeChat` call object -with a `requestStream` but a `responseStream` instead of a single `await`-able -response. In this example we create a task group and create separate tasks for -sending requests and receiving responses and await for both to complete. - -### Try it out! - -Follow the instructions in the Route Guide example directory -[README][routeguide-readme] to build and run the client and server. - -[grpc-docs]: https://grpc.io/docs/ -[protobuf-docs]: https://developers.google.com/protocol-buffers/docs/proto3 -[protobuf-releases]: https://github.com/google/protobuf/releases -[protocol-buffers]: https://developers.google.com/protocol-buffers/docs/overview -[routeguide-client]: ../Examples/v1/RouteGuide/Client/RouteGuideClient.swift -[routeguide-proto]: ../Protos/examples/route_guide/route_guide.proto -[routeguide-provider]: ../Examples/v1/RouteGuide/Server/RouteGuideProvider.swift -[routeguide-readme]: ../Examples/v1/RouteGuide/README.md -[routeguide-source]: ../Examples/v1/RouteGuide -[run-protoc]: ../Protos/generate.sh -[swift-protobuf-guide]: https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md -[swift-protobuf]: https://github.com/apple/swift-protobuf diff --git a/docs/client-without-codegen.md b/docs/client-without-codegen.md deleted file mode 100644 index 924c22cde..000000000 --- a/docs/client-without-codegen.md +++ /dev/null @@ -1,30 +0,0 @@ -# Calling a Service Without a Generated Client - -It is also possible to call gRPC services without a generated client. The models -for the requests and responses are required, however. - -If you are calling a service which you don't have a generated client for, you -can use `AnyServiceClient`. For example, to call the "SayHello" RPC on the -[Greeter][helloworld-source] service you can do the following: - -```swift -let channel = ... // get a GRPCChannel -let anyService = AnyServiceClient(channel: channel) - -let sayHello = anyService.makeUnaryCall( - path: "/helloworld.Greeter/SayHello", - request: Helloworld_HelloRequest.with { - $0.name = "gRPC Swift user" - }, - responseType: Helloworld_HelloResponse.self -) -``` - -Calls for client-, server- and bidirectional-streaming are done in a similar way -using `makeClientStreamingCall`, `makeServerStreamingCall`, and -`makeBidirectionalStreamingCall` respectively. - -These methods are also available on generated clients, allowing you to call -methods which have been added to the service since the client was generated. - -[helloworld-source]: ../Examples/v1/HelloWorld diff --git a/docs/faqs.md b/docs/faqs.md deleted file mode 100644 index d5f706b8a..000000000 --- a/docs/faqs.md +++ /dev/null @@ -1,159 +0,0 @@ -# FAQs - -## Logging / Tracing - -### Is Logging supported? - -Logging is supported by providing a `Logger` from [SwiftLog][swift-log] to the -relevant configuration object. - -For the client: - -- `ClientConnection.Builder.withBackgroundActivityLogger` allows a logger to be - supplied which logs information at the connection level, relating to things - such as connectivity state changes, or connection errors. -- `CallOptions` allows `logger` to be specified. It is used to log information - at the RPC level, such as changes to the state of the RPC. Any connection - level metadata will be attached to the logger so that RPC logs may be - correlated with connection level changes. - -For the server: - -- `Server.Builder.withLogger` allows a `logger` to be specified. It is used as a - root logger and is passed down to each accepted connection and in turn each - RPC on a connection. The logger is made available in the `context` of each - RPC handler. - -Note that gRPC will not emit logs unless a logger is explicitly provided. - -### How can RPCs be traced? - -For the client: - -If `logger` is set in the `CallOptions` for an RPC then gRPC may attach a -request ID to the metadata of that logger. This is determined by the value of -`requestIDProvider` in `CallOptions`. The options for `requestIDProvider` -include: - -- `autogenerated`: a new UUID will be generated (default). -- `generated(_:)`: the provided callback will be called to generate a new ID. -- `userDefined(_:)`: the provided value will be used. Note: this option should - not be set as a default option on a client as all RPCs would used the same ID. -- `none`: no request ID will be attached to the logger, this can be useful if a - logger already has a request ID associated with it. - -If a request ID is attached to the logger's metadata it will use the key -`grpc_request_id`. - -If a request ID is provided and the `requestIDHeader` option is set then gRPC -will add the ID to the request headers. By default `requestIDHeader` is not set. - -## Client Connection Lifecycle - -### Is the client's connection long-lived? - -The `ClientConnection` is intended to be used as a long-living connection to a -remote peer. It will manage the underlying network resources automatically, -creating a connection when necessary and dropping it when it is no longer -required. However, the user must `close()` the connection when finished with it. - -The underlying connection may be in any of the following states: - -- Idle: there is no underlying connection. -- Connecting: an attempt to establish a connection is being made. -- Ready: the connection has been established, a TLS handshake has completed - (if applicable) and the first HTTP/2 settings frame has been received. -- Transient failure: A transient error occurred either from a ready connection - or from a connection attempt. An new connection will be established after some - time. (See later sections on connection backoff for more details.) -- Shutdown: The application requested that the connection be closed, or a - connection error occurred and connection re-establishment was disabled. This - state is terminal. - -The gRPC library [documents][grpc-conn-states] these states in more details. -Note that not all information linked is applicable to gRPC Swift. - -### How can connection states be observed? - -A connectivity state delegate may be set using -`withConnectivityStateDelegate(_:executingOn:)` on the -`ClientConnection.Builder`. - -The delegate is called on the `DispatchQueue` specified by the `executingOn` -parameter. gRPC will create a `DispatchQueue` if one isn't otherwise specified. - -These state changes will also be logged by the `backgroundActivityLogger` (see -above). - -### When will the connection idle? - -The connection will be idled (i.e. the underlying connection closed and moved to -the 'idle' state) if there are no outstanding RPCs for 5 minutes (by default). -The connection will _not_ be idled if there outstanding RPCs which are not -sending or receiving messages. This option may be configured using -`withConnectionIdleTimeout` on the `ClientConnection.Builder`. - -Any RPC called after the connection has idled will trigger a connection -attempt. - -### How can I keep a connection alive? - -For long-lived, low-activity RPCs it may be beneficial to configure keepalive. -Doing so will periodically send pings to the remote peer. It may also be used to -detect unresponsive peers. - -See the [gRPC Keepalive][grpc-keepalive] documentation for details. - -## RPC Lifecycle - -### How do I start an RPC? - -RPCs are usually started by invoking a method on a generated client. Each -generated client relies on an underlying `GRPCChannel` to provide transport. - -RPCs can also be made without a generated client by using `AnyServiceClient`. -This requires the user know the path (i.e. '/echo/Get') and request and response -types for the RPC. - -### Are failing RPCs retried automatically? - -RPCs are never automatically retried by gRPC Swift. - -The framework cannot determine whether your RPC is idempotent, it is therefore -not safe for gRPC Swift to automatically retry RPCs for you. - -### Deadlines and Timeouts - -It's recommended that deadlines are used to enforce a limit on the duration of -an RPC. Users may set a time limit (either a deadline or a timeout) on the -`CallOptions` for each call, or as a default on a client. RPCs which have not -completed before the time limit will be failed with status code 4 -('deadline exceeded'). - -## Compression - -gRPC Swift supports compression. - -### How to enable compression for the Client - -You can configure compression via the messageEncoding property on CallOptions: - -```swift -// Configure encoding -let encodingConfiguration = ClientMessageEncoding.Configuration( - forRequests: .gzip, // use gzip for requests - acceptableForResponses: .all, // accept all supported algorithms for responses - decompressionLimit: .ratio(20) // reject messages and fail the RPC if a response decompresses to over 20x its compressed size -) - -// Enable compression with configuration -let options = CallOptions(messageEncoding: .enabled(encodingConfiguration)) - -// Use the options to make a request -let rpc = echo.get(request, callOptions: options) -// ... -``` - -[grpc-conn-states]: connectivity-semantics-and-api.md -[grpc-keepalive]: keepalive.md -[swift-log]: https://github.com/apple/swift-log diff --git a/docs/interceptors-tutorial.md b/docs/interceptors-tutorial.md deleted file mode 100644 index 4882aa4f5..000000000 --- a/docs/interceptors-tutorial.md +++ /dev/null @@ -1,296 +0,0 @@ -# Interceptors Tutorial - -This tutorial provides an introduction to interceptors in gRPC Swift. It assumes -you are familiar with gRPC Swift (if you aren't, try the -[quick-start guide][quick-start] or [basic tutorial][basic-tutorial] first). - -### What are Interceptors? - -Interceptors are a mechanism which allows users to, as the name suggests, -intercept the request and response streams of RPCs. They may be used on the -client and the server, and any number of interceptors may be used for a single -RPC. They are often used to provide cross-cutting functionality such as logging, -metrics, and authentication. - -### Interceptor API - -Interceptors are created by implementing a subclass of `ClientInterceptor` or -`ServerInterceptor` depending on which peer the interceptor is intended for. -Each type is interceptor base class is generic over the request and response -type for the RPC: `ClientInterceptor` and -`ServerInterceptor`. - -The API for the client and server interceptors are broadly similar (with -differences in the message types on the stream). Each offer -`send(_:promise:context:)` and `receive(_:context:)` functions where the -provided `context` (`ClientInterceptorContext` and -`ServerInterceptorContext` respectively) exposes methods for -calling the next interceptor once the message part has been handled. - -Each `context` type also provides the `EventLoop` that the RPC is being invoked -on and some additional information, such as the type of the RPC (unary, -client-streaming, etc.) the path (e.g. "/echo.Echo/Get"), and a logger. - -### Defining an interceptor - -This tutorial builds on top of the [Echo example][echo-example]. - -As described above, interceptors are created by subclassing `ClientInterceptor` -or `ServerInterceptor`. For the sake of brevity we will only cover creating our -own `ClientInterceptor` which prints events as they happen. - -First we create our interceptor class, for the Echo service all RPCs have the -same request and response type so we'll use these types concretely here. An -interceptor may of course remain generic over the request and response types. - -```swift -class LoggingEchoClientInterceptor: ClientInterceptor { - // ... -} -``` - -Note that the default behavior of every interceptor method is a no-op; it will -just pass the unmodified part to the next interceptor by invoking the -appropriate method on the context. - -Let's look at intercepting the request stream by overriding `send`: - -```swift -override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext -) { - // ... -} -``` - -`send` is called with a request part generic over the request type for the RPC -(for a sever interceptor this would be a response part generic over the response -type), an optional `EventLoopPromise` promise which will be completed when -the request has been written to the network, and a `ClientInterceptorContext`. - -The `GRPCClientRequestPart` `enum` has three cases: -- `metadata(HPACKHeaders)`: the user-provided request headers which are sent at - the start of each RPC. The headers will be augmented with transport and - protocol specific headers once the request part reaches the transport. -- `message(Request, MessageMetadata)`: a request message and associated metadata - (such as whether the message should be compressed and whether to flush the - transport after writing the message). For unary and server-streaming RPCs we - expect exactly one message, for client-streaming and bidirectional-streaming - RPCs any number of messages (including zero) is permitted. -- `end`: the end of the request stream which must be sent exactly once as the - final part on the stream, after which no more request parts may be sent. - -Below demonstrates how one could log information about a request stream using an -interceptor, after which we use the `context` to forward the request part and -promise to the next interceptor: - -```swift -class LoggingEchoClientInterceptor: ClientInterceptor { - override func send( - _ part: GRPCClientRequestPart, - promise: EventLoopPromise?, - context: ClientInterceptorContext - ) { - switch part { - case let .metadata(headers): - print("> Starting '\(context.path)' RPC, headers: \(headers)") - - case let .message(request, _): - print("> Sending request with text '\(request.text)'") - - case .end: - print("> Closing request stream") - } - - // Forward the request part to the next interceptor. - context.send(part, promise: promise) - } - - // ... -} -``` - -Now let's look at the response stream by intercepting `receive`: - -```swift -override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext -) { - // ... -} -``` - -`receive` is called with a response part generic over the response type for the -RPC and the same `ClientInterceptorContext` as used in `send`. The response -parts are also similar: - -The `GRPCClientResponsePart` `enum` has three cases: -- `metadata(HPACKHeaders)`: the response headers returned from the server. We - expect these at the start of a response stream, however it is also valid to - see no `metadata` parts on the response stream if the server fails the RPC - immediately (in which case we will just see the `end` part). -- `message(Response)`: a response message received from the server. For unary - and client-streaming RPCs at most one message is expected (but not required). - For server-streaming and bidirectional-streaming any number of messages - (including zero) is permitted. -- `end(GRPCStatus, HPACKHeaders)`: the end of the response stream (and by - extension, request stream) containing the RPC status (why the RPC ended) and - any trailers returned by the server. We expect one `end` part per RPC, after - which no more response parts may be received and no more request parts will be - sent. - -The code for receiving is similar to that for sending: - -```swift -class LoggingEchoClientInterceptor: ClientInterceptor { - // ... - - override func receive( - _ part: GRPCClientResponsePart, - context: ClientInterceptorContext - ) { - switch part { - case let .metadata(headers): - print("< Received headers: \(headers)") - - case let .message(response): - print("< Received response with text '\(response.text)'") - - case let .end(status, trailers): - print("< Response stream closed with status: '\(status)' and trailers: \(trailers)") - } - - // Forward the response part to the next interceptor. - context.receive(part) - } -} -``` - -In this example the implementations of `send` and `receive` directly forward the -request and response parts to the next interceptor. This is not a requirement: -implementations are free to drop, delay or redirect parts as necessary, -`context.send(_:promise:)` may be called in `receive(_:context:)` and -`context.receive(_:)` may be called in `send(_:promise:context:)`. A server -interceptor which validates an authorization header, for example, may -immediately send back an `end` when receiving request headers lacking a valid -authorization header. - -### Using interceptors - -Interceptors are provided to a generated client or service provider via an -implementation of generated factory protocol. For our echo example this will be -`Echo_EchoClientInterceptorFactoryProtocol` for the client and -`Echo_EchoServerInterceptorFactoryProtocol` for the server. - -Each protocol has one method per RPC which returns an array of -appropriately typed interceptors to use when intercepting that RPC. Factory -methods are called at the start of each RPC. - -It's important to note the order in which the interceptors are called. For the -client the array of interceptors should be in 'outbound' order, that is, when -sending a request part the _first_ interceptor to be called is the first in the -array. When the client receives a response part from the server the _last_ -interceptor in the array will receive that part first. - -For server factories the order is reversed: when receiving a request part the -_first_ interceptor in the array will be called first, when sending a response -part the _last_ interceptor in the array will be called first. - -Implementing a factory is straightforward, in our case the Echo service has four -RPCs, all of which return the `LoggingEchoClientInterceptor` we defined above. - -``` -class ExampleClientInterceptorFactory: Echo_EchoClientInterceptorFactoryProtocol { - // Returns an array of interceptors to use for the 'Get' RPC. - func makeGetInterceptors() -> [ClientInterceptor] { - return [LoggingEchoClientInterceptor()] - } - - // Returns an array of interceptors to use for the 'Expand' RPC. - func makeExpandInterceptors() -> [ClientInterceptor] { - return [LoggingEchoClientInterceptor()] - } - - // Returns an array of interceptors to use for the 'Collect' RPC. - func makeCollectInterceptors() -> [ClientInterceptor] { - return [LoggingEchoClientInterceptor()] - } - - // Returns an array of interceptors to use for the 'Update' RPC. - func makeUpdateInterceptors() -> [ClientInterceptor] { - return [LoggingEchoClientInterceptor()] - } -} -``` - -An interceptor factory may be passed to the generated client on initialization: - -```swift -let echo = Echo_EchoClient(channel: channel, interceptors: ExampleClientInterceptorFactory()) -``` - -For the server, providing an (optional) interceptor factory is a requirement -of the generated service provider protocol and is left to the implementation of -the provider: - -```swift -protocol Echo_EchoProvider: CallHandlerProvider { - var interceptors: Echo_EchoServerInterceptorFactoryProtocol? { get } - - // ... -} -``` - -### Running the example - -The code listed above is available in the [Echo example][echo-example]. To run -it, from the root of your gRPC-Swift checkout start the Echo server on a free -port by running: - -``` -$ swift run Echo server -starting insecure server -``` - -In another terminal run the client without the interceptors with: - -``` -$ swift run Echo client "Hello" -get receieved: Swift echo get: Hello -get completed with status: ok (0) -``` - -This calls the unary "Get" RPC and prints the response and status from the RPC. -Let's run it with our interceptor enabled by adding the `--intercept` flag: - -``` -$ swift run Echo client --intercept "Hello" -> Starting '/echo.Echo/Get' RPC, headers: [] -> Sending request with text 'Hello' -> Closing request stream -< Received headers: [':status': '200', 'content-type': 'application/grpc'] -< Received response with text 'Swift echo get: Hello' -get receieved: Swift echo get: Hello -< Response stream closed with status: 'ok (0): OK' and trailers: ['grpc-status': '0', 'grpc-message': 'OK'] -get completed with status: ok (0) -``` - -Now we see the output from the logging interceptor: we invoke an RPC to -'Get' on the 'echo.Echo' service followed by the request with the text we -provided and the end of the request stream. Then we see response parts from the -server, the headers at the start of the response stream: a 200-OK status and the -gRPC content-type header, followed by the response and the end of response -stream and trailers. - -### A note on thread safety - -It is important to note that interceptor functions are invoked on the -`EventLoop` provided by the context and that implementations *must* respect this -by invoking methods on the `context` from that `EventLoop`. - -[quick-start]: ./quick-start.md -[basic-tutorial]: ./basic-tutorial.md -[echo-example]: ../Examples/v1/Echo diff --git a/docs/keepalive.md b/docs/keepalive.md deleted file mode 100644 index 6724e485c..000000000 --- a/docs/keepalive.md +++ /dev/null @@ -1,62 +0,0 @@ -# Keepalive - -gRPC sends HTTP2 pings on the transport to detect if the connection is down. -If the ping is not acknowledged by the other side within a certain period, the connection -will be closed. Note that pings are only necessary when there is no activity on the connection. - -## What should I set? - -It should be sufficient for most users to only change `interval` and `timeout` properties, but the -following properties can also be useful in certain use cases. - -Property | Client | Server | Description ----------|--------|--------|------------ -interval|Int64.max (disabled)|.hours(2)|The amount of time to wait before sending a keepalive ping. -timeout|.seconds(20)|.seconds(20)|The amount of time to wait for an acknowledgment. This value must be less than `interval`. -permitWithoutCalls|false|false|Send keepalive pings even if there are no calls in flight. -maximumPingsWithoutData|2|2|Maximum number of pings that can be sent when there is no data/header frame to be sent/ -minimumSentPingIntervalWithoutData|.minutes(5)|.minutes(5)|If there are no data/header frames being received: the minimum amount of time to wait between successive pings. -minimumReceivedPingIntervalWithoutData|N/A|.minutes(5)|If there are no data/header frames being sent: the minimum amount of time expected between receiving successive pings. If the time between successive pings is less than this value, then the ping will be considered a bad ping from the peer. Such a ping counts as a "ping strike". -maximumPingStrikes|N/A|2|Maximum number of bad pings that the server will tolerate before sending an HTTP2 GOAWAY frame and closing the connection. Setting it to `0` allows the server to accept any number of bad pings. - -### Client - -```swift -let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - -let keepalive = ClientConnectionKeepalive( - interval: .seconds(15), - timeout: .seconds(10) -) - -let channel = try GRPCChannelPool.with( - target: .host("localhost"), - transportSecurity: .tls(...), - eventLoopGroup: group -) { - // Configure keepalive. - $0.keepalive = keepalive -} -``` - -### Server - -```swift -let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - -let keepalive = ServerConnectionKeepalive( - interval: .seconds(15), - timeout: .seconds(10) -) - -let configuration = Server.Configuration( - target: .hostAndPort("localhost", 443), - eventLoopGroup: group, - connectionKeepalive: keepalive, - serviceProviders: [YourCallHandlerProvider()] -) - -let server = Server.makeBootstrap(configuration: configuration) -``` - -Fore more information, please visit the [gRPC Core documentation for keepalive](https://github.com/grpc/grpc/blob/master/doc/keepalive.md) diff --git a/docs/plugin.md b/docs/plugin.md deleted file mode 100644 index 06da5dac6..000000000 --- a/docs/plugin.md +++ /dev/null @@ -1,124 +0,0 @@ -# `protoc` gRPC Swift plugin - -gRPC Swift provides a plugin for the [protocol buffer][protocol-buffers] -compiler `protoc` to generate classes for clients and services. - -## Building the Plugin - -The `protoc-gen-grpc-swift` plugin can be built using the Swift Package Manager: - -```sh -$ swift build --product protoc-gen-grpc-swift -``` - -The plugin must be in your `PATH` environment variable or specified directly -when invoking `protoc`. - -## Plugin Options - -### Visibility - -The **Visibility** option determines the access control for generated code. - -- **Possible values:** Public, Internal, Package -- **Default value:** Internal - -### Server - -The **Server** option determines whether server code is generated. The -generated server code includes a `protocol` which users must implement -to provide the logic for their service. - -- **Possible values:** true, false -- **Default value:** true - -### Client - -The **Client** option determines whether client code is generated. The -generated client code includes a `protocol` and a `class` conforming to that -protocol. - -- **Possible values:** true, false -- **Default value:** true - -### TestClient - -The **TestClient** option determines whether test client code is generated. -This does *not* include the `protocol` generated by the **Client** option. - -- **Possible values:** true, false -- **Default value:** false - -### FileNaming - -The **FileNaming** option determines how generated source files should be named. - -- **Possible values:** - - **FullPath**: The full path of the input file will be used; - `foo/bar/baz.proto` will generate `foo/bar/baz.grpc.proto` - - **PathToUnderscores**: Directories structures are flattened; - `foo/bar/baz.proto` will generate `foo_bar_baz.grpc.proto` - - **DropPath**: The path is dropped and only the name of the file is kept; - `foo/bar/baz.proto` will generate `baz.grpc.proto` -- **Default value:** FullPath - -### ProtoPathModuleMappings - -The **ProtoPathModuleMappings** option allows module mappings to be specified. -See the [SwiftProtobuf documentation][swift-protobuf-module-mappings]. - -### KeepMethodCasing - -The **KeepMethodCasing** determines whether the casing of generated function -names is kept. - -For example, for the following RPC definition: - -```proto -rpc Foo(FooRequest) returns (FooRequest) {} -``` - -Will generate stubs named `foo` by default. However, in some cases this is not -desired, and setting `KeepMethodCasing=true` will yield stubs named `Foo`. - -- **Possible values:** true, false -- **Default value:** false - -### GRPCModuleName - -The **GRPCModuleName** option allows the name of the gRPC Swift runtime module -to be specified. The value, if not specified, defaults to "GRPC". - -*Note: most users will not need to use this option.* - -### SwiftProtobufModuleName - - The **SwiftProtobufModuleName** option allows the name of the SwiftProtobuf - runtime module to be specified. The value, if not specified, defaults to - "SwiftProtobuf". - - *Note: most users will not need to use this option. Introduced to match - the option that exists in [SwiftProtobuf][swift-protobuf-module-name]. - -## Specifying Options - -To pass extra parameters to the plugin, use a comma-separated parameter list -separated from the output directory by a colon. Alternatively use the -`--grpc-swift_opt` flag. - -For example, to generate only client stubs: - -```sh -protoc --grpc-swift_out=Client=true,Server=false:. -``` - -Or, in the alternate syntax: - -```sh -protoc --grpc-swift_opt=Client=true,Server=false --grpc-swift_out=. -``` - -[protocol-buffers]: https://developers.google.com/protocol-buffers/docs/overview -[swift-protobuf-filenaming]: https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md#generation-option-filenaming---naming-of-generated-sources -[swift-protobuf-module-mappings]: https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md#generation-option-protopathmodulemappings---swift-module-names-for-proto-paths -[swift-protobuf-module-name]: https://github.com/apple/swift-protobuf/commit/9df381f72ff22062080d434e9c2f68e71ee44298#diff-1b08f0a80bd568509049d851b8d8af90d1f2db3cd8711eaba974b5380cd59bf3 diff --git a/docs/quick-start.md b/docs/quick-start.md deleted file mode 100644 index e66b91b89..000000000 --- a/docs/quick-start.md +++ /dev/null @@ -1,239 +0,0 @@ -# gRPC Swift Quick Start - -## Before you begin - -### Prerequisites - -#### Swift Version - -gRPC requires Swift 5.8 or higher. - -#### Install Protocol Buffers - -Install the protoc compiler that is used to generate gRPC service code. The -simplest way to do this is to download pre-compiled binaries for your -platform (`protoc--.zip`) from here: -[https://github.com/google/protobuf/releases][protobuf-releases]. - -* Unzip this file. -* Update the environment variable `PATH` to include the path to the `protoc` - binary file. - -### Download the example - -You'll need a local copy of the example code to work through this quickstart. -Download the example code from our GitHub repository (the following command -clones the entire repository, but you just need the examples for this quickstart -and other tutorials): - -```sh -$ # Clone the repository at the latest release to get the example code (replacing x.y.z with the latest release, for example 1.13.0): -$ git clone -b x.y.z https://github.com/grpc/grpc-swift -$ # Navigate to the repository -$ cd grpc-swift/ -``` - -## Run a gRPC application - -From the `grpc-swift` directory: - -1. Compile and run the server - - ```sh - $ swift run HelloWorldServer - ``` - -2. In another terminal, compile and run the client - - ```sh - $ swift run HelloWorldClient - Greeter received: Hello stranger! - ``` - -Congratulations! You've just run a client-server application with gRPC. - -## Update a gRPC service - -Now let's look at how to update the application with an extra method on the -server for the client to call. Our gRPC service is defined using protocol -buffers; you can find out lots more about how to define a service in a `.proto` -file in [What is gRPC?][grpc-guides]. For now all you need to know is that both -the server and the client "stub" have a `SayHello` RPC method that takes a -`HelloRequest` parameter from the client and returns a `HelloReply` from the -server, and that this method is defined like this: - -```proto -// The greeting service definition. -service Greeter { - // Sends a greeting. - rpc SayHello (HelloRequest) returns (HelloReply) {} -} - -// The request message containing the user's name. -message HelloRequest { - string name = 1; -} - -// The response message containing the greetings. -message HelloReply { - string message = 1; -} -``` - -Let's update this so that the `Greeter` service has two methods. Edit -`Protos/upstream/grpc/examples/helloworld.proto` and update it with a new -`SayHelloAgain` method, with the same request and response types: - -```proto -// The greeting service definition. -service Greeter { - // Sends a greeting. - rpc SayHello (HelloRequest) returns (HelloReply) {} - // Sends another greeting. - rpc SayHelloAgain (HelloRequest) returns (HelloReply) {} -} - -// The request message containing the user's name. -message HelloRequest { - string name = 1; -} - -// The response message containing the greetings. -message HelloReply { - string message = 1; -} -``` - -(Don't forget to save the file!) - -### Update and run the application - -We need to regenerate -`Examples/v1/HelloWorld/Model/helloworld.grpc.swift`, which -contains our generated gRPC client and server classes. - -From the `grpc-swift` directory run - -```sh -$ Protos/generate.sh -``` - -This also regenerates classes for populating, serializing, and retrieving our -request and response types. - -However, we still need to implement and call the new method in the human-written -parts of our example application. - -#### Update the server - -In the same directory, open -`Examples/v1/HelloWorld/Server/GreeterProvider.swift`. Implement the new -method like this: - -```swift -final class GreeterProvider: Helloworld_GreeterAsyncProvider { - let interceptors: Helloworld_GreeterServerInterceptorFactoryProtocol? = nil - - func sayHello( - request: Helloworld_HelloRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Helloworld_HelloReply { - let recipient = request.name.isEmpty ? "stranger" : request.name - return Helloworld_HelloReply.with { - $0.message = "Hello \(recipient)!" - } - } - - func sayHelloAgain( - request: Helloworld_HelloRequest, - context: GRPCAsyncServerCallContext - ) async throws -> Helloworld_HelloReply { - let recipient = request.name.isEmpty ? "stranger" : request.name - return Helloworld_HelloReply.with { - $0.message = "Hello again \(recipient)!" - } - } -} -``` - -#### Update the client - -In the same directory, open -`Examples/v1/HelloWorld/Client/HelloWorldClient.swift`. Call the new method like this: - -```swift -func run() async throws { - // Setup an `EventLoopGroup` for the connection to run on. - // - // See: https://github.com/apple/swift-nio#eventloops-and-eventloopgroups - let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - // Make sure the group is shutdown when we're done with it. - defer { - try! group.syncShutdownGracefully() - } - - // Configure the channel, we're not using TLS so the connection is `insecure`. - let channel = try GRPCChannelPool.with( - target: .host("localhost", port: self.port), - transportSecurity: .plaintext, - eventLoopGroup: group - ) - - // Close the connection when we're done with it. - defer { - try! channel.close().wait() - } - - // Provide the connection to the generated client. - let greeter = Helloworld_GreeterAsyncClient(channel: channel) - - // Form the request with the name, if one was provided. - let request = Helloworld_HelloRequest.with { - $0.name = self.name ?? "" - } - - do { - let greeting = try await greeter.sayHello(request) - print("Greeter received: \(greeting.message)") - } catch { - print("Greeter failed: \(error)") - } - - do { - let greetingAgain = try await greeter.sayHelloAgain(request) - print("Greeter received: \(greetingAgain.message)") - } catch { - print("Greeter failed: \(error)") - } - } -``` - -#### Run! - -Just like we did before, from the top level `grpc-swift` directory: - -1. Compile and run the server - - ```sh - $ swift run HelloWorldServer - ``` - -2. In another terminal, compile and run the client - - ```sh - $ swift run HelloWorldClient - Greeter received: Hello stranger! - Greeter received: Hello again stranger! - ``` - -### What's next - -- Read a full explanation of how gRPC works in [What is gRPC?][grpc-guides] and - [gRPC Concepts][grpc-concepts] -- Work through a more detailed tutorial in [gRPC Basics: Swift][basic-tutorial] - -[grpc-guides]: https://grpc.io/docs/guides/ -[grpc-concepts]: https://grpc.io/docs/guides/concepts/ -[protobuf-releases]: https://github.com/google/protobuf/releases -[basic-tutorial]: ./basic-tutorial.md diff --git a/docs/tls.md b/docs/tls.md deleted file mode 100644 index fd9a6f45a..000000000 --- a/docs/tls.md +++ /dev/null @@ -1,95 +0,0 @@ -# Using TLS - -gRPC Swift offers two TLS 'backends'. A 'NIOSSL' backend and a 'Network.framework' backend. - -The NIOSSL backend is available on Darwin and Linux and delegates to SwiftNIO SSL. The -Network.framework backend is available on recent Darwin platforms (macOS 10.14+, iOS 12+, tvOS 12+, -and watchOS 6+) and uses the TLS implementation provided by Network.framework. Moreover, the -Network.framework backend is only compatible with clients and servers using the `EventLoopGroup` -provided by SwiftNIO Transport Services, `NIOTSEventLoopGroup`. - -| | NIOSSL backend | Network.framework backend | -|-----------------------------|------------------------------------------------------|---------------------------------------------| -| Platform Availability | Darwin and Linux | macOS 10.14+, iOS 12+, tvOS 12+, watchOS 6+ | -| Compatible `EventLoopGroup` | `MultiThreadedEventLoopGroup`, `NIOTSEventLoopGroup` | `NIOTSEventLoopGroup` | - -Note that on supported Darwin platforms users should the prefer using `NIOTSEventLoopGroup` and the -Network.framework backend. - -## Configuring TLS - -TLS may be configured in two different ways: using a client/server builder, or by constructing a -configuration object to instantiate the builder with. - -### Configuring a Client - -The simplest way to configure a client to use TLS is to let gRPC decide which TLS backend to use -based on the type of the provided `EventLoopGroup`: - -```swift -let group = PlatformSupport.makeEventLoopGroup(loopCount: 1) -let builder = ClientConnection.usingPlatformAppropriateTLS(for: group) -``` - -The `builder` exposes additional methods for configuring TLS, however most methods are specific to a -backend and must not be called when that backend is not being used (the documentation for -each `withTLS(...)` method states which backend it may be applied to). - -If more control is required over the configuration users may signal which backend to use and provide -an appropriate `EventLoopGroup` to one of `ClientConnection.usingTLSBackedByNIOSSL(on:)` and -`ClientConnection.usingTLSBackedByNetworkFramework(on:)`. - -gRPC Swift also includes a `GRPCTLSConfiguration` object which wraps the configuration used by each -backend. An instance of this may also be provided to `ClientConnection.usingTLS(with:on:)` with an -appropriate `EventLoopGroup`. - -### Configuring a Server - -Servers always require some backend specific configuration, as such there is no -automatically detectable 'platform appropriate' server configuration. - -To configure a server callers must pair one of -`Server.usingTLSBackedByNIOSSL(on:certificateChain:privateKey:)` and -`Server.usingTLSBackedByNetworkFramework(on:with:)` with an appropriate `EventLoopGroup` or provide -a `GRPCTLSConfiguration` and appropriate `EventLoopGroup` to `Server.usingTLS(with:on:)`. - -## NIOSSL Backend: Loading Certificates and Private Keys - -Using the NIOSSL backend, certificates and private keys are represented by -[`NIOSSLCertificate`][nio-ref-tlscert] and [`NIOSSLPrivateKey`][nio-ref-privatekey], -respectively. - -A certificate or private key may be loaded from: -- a file using `NIOSSLCertificate(file:format:)` or `NIOSSLPrivateKey(file:format:)`, or -- an array of bytes using `NIOSSLCertificate(buffer:format:)` or `NIOSSLPrivateKey(bytes:format)`. - -It is also possible to load a certificate or private key from a `String` by -constructing an array from its UTF8 view and passing it to the appropriate -initializer (`NIOSSLCertificate(buffer:format)` or -`NIOSSLPrivateKey(bytes:format:)`): - -```swift -let certificateString = ... -let bytes: = Array(certificateString.utf8) - -let certificateFormat = ... -let certificate = try NIOSSLCertificate(buffer: bytes, format: certificateFormat) -``` - -Certificate chains may also be loaded from: - -- a file: `NIOSSLCertificate.fromPEMFile(_:)`, or -- an array of bytes: `NIOSSLCertificate.fromPEMBytes(_:)`. - -These functions return an _array_ of certificates (`[NIOSSLCertificate]`). - -Similar to loading a certificate, a certificate chain may also be loaded from -a `String` using by using the UTF8 view on the string with the -`fromPEMBytes(_:)` method. - -Refer to the [certificate][nio-ref-tlscert] or [private -key][nio-ref-privatekey] documentation for more information. - -[nio-ref-privatekey]: https://apple.github.io/swift-nio-ssl/docs/current/NIOSSL/Classes/NIOSSLPrivateKey.html -[nio-ref-tlscert]: https://apple.github.io/swift-nio-ssl/docs/current/NIOSSL/Classes/NIOSSLCertificate.html -[nio-ref-tlsconfig]: https://apple.github.io/swift-nio-ssl/docs/current/NIOSSL/Structs/TLSConfiguration.html diff --git a/scripts/alloc-limits.sh b/scripts/alloc-limits.sh deleted file mode 100755 index 16aa0063b..000000000 --- a/scripts/alloc-limits.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -# 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. - -# This script parses output from the SwiftNIO allocation counter framework to -# generate a list of per-test limits for total_allocations. -# -# Input is like: -# ... -# test_embedded_server_unary_1k_rpcs_1_small_request.total_allocated_bytes: 5992858 -# test_embedded_server_unary_1k_rpcs_1_small_request.total_allocations: 63000 -# test_embedded_server_unary_1k_rpcs_1_small_request.remaining_allocations: 0 -# DEBUG: [["total_allocated_bytes": 5992858, "total_allocations": ... -# -# Output: -# MAX_ALLOCS_ALLOWED_embedded_server_unary_1k_rpcs_1_small_request=64000 - -grep 'test_.*\.total_allocations: ' \ - | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}T[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}.[0-9]*Z //' \ - | sed 's/^test_/MAX_ALLOCS_ALLOWED_/' \ - | sed 's/.total_allocations://' \ - | awk '{ print " " $1 ": " ((int($2 / 1000) + 1) * 1000) }' \ - | sort diff --git a/scripts/bundle-plugins-for-release.sh b/scripts/bundle-plugins-for-release.sh deleted file mode 100755 index baec8643b..000000000 --- a/scripts/bundle-plugins-for-release.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - -# Copyright 2020, 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. - -set -eu - -# This script bundles up the gRPC and Protobuf protoc plugins into a zip file -# suitable for the 'gRPC-Swift-Plugins' CocoaPod. -# -# The contents of thie zip should look like this: -# -# ├── LICENSE -# └── bin -# ├── protoc-gen-grpc-swift -# └── protoc-gen-swift - -if [[ $# -lt 1 ]]; then - echo "Usage: $0 RELEASE_VERSION" - exit 1 -fi - -version=$1 -zipfile="protoc-grpc-swift-plugins-${version}.zip" - -# Where are we? -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -# The root of the repo is just above us. -root="${here}/.." - -# Make a staging area. -stage=$(mktemp -d) -stage_bin="${stage}/bin" -mkdir -p "${stage_bin}" - -# Make sure dependencies are up-to-date -swift package update -# Make the plugins. -swift build -c release --arch arm64 --arch x86_64 --product protoc-gen-grpc-swift -swift build -c release --arch arm64 --arch x86_64 --product protoc-gen-swift -binpath=$(swift build -c release --arch arm64 --arch x86_64 --show-bin-path) - -# Copy them to the stage. -cp "${binpath}/protoc-gen-grpc-swift" "${stage_bin}" -cp "${binpath}/protoc-gen-swift" "${stage_bin}" - -# Copy the LICENSE to the stage. -cp "${root}/LICENSE" "${stage}" - -# Zip it up. -pushd "${stage}" || exit -zip -r "${zipfile}" . -popd || exit - -# Tell us where it is. -echo "Created ${stage}/${zipfile}" diff --git a/scripts/cg_diff.py b/scripts/cg_diff.py deleted file mode 100755 index 51739e5bd..000000000 --- a/scripts/cg_diff.py +++ /dev/null @@ -1,338 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2020, 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 argparse -import enum -import os -import subprocess -import sys - - -class State(enum.Enum): - READING_HEADERS = enum.auto() - READING_INSTRUCTION = enum.auto() - READING_COUNTS = enum.auto() - READING_SUMMARY = enum.auto() - - -class InstructionCounts(object): - def __init__(self, events): - self._events = events - self._counts = {} - - @property - def events(self): - return self._events - - @property - def instructions(self): - return self._counts.keys() - - def add(self, instruction, counts): - """Add a list of counts or the given instruction.""" - if instruction in self._counts: - existing = self._counts[instruction] - self._counts[instruction] = [a + b for (a, b) in zip(existing, counts)] - else: - self._counts[instruction] = counts - - def count(self, instruction, event): - """The number of occurrences of the event for the given instruction.""" - counts = self._counts.get(instruction) - index = self._events.index(event) - if counts: - return counts[index] - else: - return 0 - - def aggregate(self): - """Aggregates event counts over all instructions.""" - return [sum(x) for x in zip(*self._counts.values())] - - def aggregate_by_event(self, event): - """Aggregates event counts over all instructions for a given event.""" - return self.aggregate_by_index(self._events.index(event)) - - def aggregate_by_index(self, index): - """Aggregates event counts over all instructions for the event at the given index.""" - return sum(x[index] for x in self._counts.values()) - - -class Parser(object): - HEADERS = ["desc:", "cmd:"] - - def __init__(self): - # Parsing state. - self._state = State.READING_HEADERS - # File for current instruction - self._file = None - # Function for current instruction - self._function = None - # Instruction counts - self._counts = None - - @property - def counts(self): - return self._counts - - @property - def _key(self): - fl = "???" if self._file is None else self._file - fn = "???" if self._function is None else self._function - return fl + ":" + fn - - ### Helpers - - def _is_header(self, line): - return any(line.startswith(p) for p in Parser.HEADERS) - - def _read_events_header(self, line): - if line.startswith("events:"): - self._counts = InstructionCounts(line[7:].strip().split(" ")) - return True - else: - return False - - def _read_function(self, line): - if not line.startswith("fn="): - return None - return line[3:].strip() - - def _read_file(self, line): - if not line.startswith("fl="): - return None - return line[3:].strip() - - def _read_file_or_function(self, line, reset_instruction=False): - function = self._read_function(line) - if function is not None: - self._function = function - self._file = None if reset_instruction else self._file - return State.READING_INSTRUCTION - - file = self._read_file(line) - if file is not None: - self._file = file - self._function = None if reset_instruction else self._function - return State.READING_INSTRUCTION - - return None - - ### Section parsing - - def _read_headers(self, line): - if self._read_events_header(line) or self._is_header(line): - # Still reading headers. - return State.READING_HEADERS - - # Not a header, maybe a file or function. - next_state = self._read_file_or_function(line) - if next_state is None: - raise RuntimeWarning("Unhandled line:", line) - - return next_state - - def _read_instruction(self, line, reset_instruction=False): - next_state = self._read_file_or_function(line, reset_instruction) - if next_state is not None: - return next_state - - if self._read_summary(line): - return State.READING_SUMMARY - - return self._read_counts(line) - - def _read_counts(self, line): - # Drop the line number - counts = [int(x) for x in line.split(" ")][1:] - self._counts.add(self._key, counts) - return State.READING_COUNTS - - def _read_summary(self, line): - if line.startswith("summary:"): - summary = [int(x) for x in line[8:].strip().split(" ")] - computed_summary = self._counts.aggregate() - assert summary == computed_summary - return True - else: - return False - - ### Parse - - def parse(self, file, demangle): - """Parse the given file.""" - with open(file) as fh: - if demangle: - demangled = subprocess.check_output(["swift", "demangle"], stdin=fh) - self._parse_lines(x.decode("utf-8") for x in demangled.splitlines()) - else: - self._parse_lines(fh) - - return self._counts - - def _parse_lines(self, lines): - for line in lines: - self._next_line(line) - - def _next_line(self, line): - """Parses a line of input.""" - if self._state is State.READING_HEADERS: - self._state = self._read_headers(line) - elif self._state is State.READING_INSTRUCTION: - self._state = self._read_instruction(line) - elif self._state is State.READING_COUNTS: - self._state = self._read_instruction(line, reset_instruction=True) - elif self._state is State.READING_SUMMARY: - # We're done. - return - else: - raise RuntimeError("Unexpected state", self._state) - - -def parse(filename, demangle): - parser = Parser() - return parser.parse(filename, demangle) - - -def print_summary(args): - # No need to demangle for summary. - counts1 = parse(args.file1, False) - aggregate1 = counts1.aggregate_by_event(args.event) - counts2 = parse(args.file2, False) - aggregate2 = counts2.aggregate_by_event(args.event) - - delta = aggregate2 - aggregate1 - pc = 100.0 * delta / aggregate1 - print("{:16,} {}".format(aggregate1, os.path.basename(args.file1))) - print("{:16,} {}".format(aggregate2, os.path.basename(args.file2))) - print("{:+16,} ({:+.3f}%)".format(delta, pc)) - - -def print_diff_table(args): - counts1 = parse(args.file1, args.demangle) - aggregate1 = counts1.aggregate_by_event(args.event) - counts2 = parse(args.file2, args.demangle) - aggregate2 = counts2.aggregate_by_event(args.event) - - file1_total = aggregate1 - diffs = [] - - def _count(key, counts): - block = counts.get(key) - return 0 if block is None else block.counts[0] - - def _row(c1, c2, key): - delta = c2 - c1 - delta_pc = 100.0 * (delta / file1_total) - return (c1, c2, delta, delta_pc, key) - - def _row_for_key(key): - c1 = counts1.count(key, args.event) - c2 = counts2.count(key, args.event) - return _row(c1, c2, key) - - if args.only_common: - keys = counts1.instructions & counts2.instructions - else: - keys = counts1.instructions | counts2.instructions - - rows = [_row_for_key(k) for k in keys] - rows.append(_row(aggregate1, aggregate2, "PROGRAM TOTALS")) - - print( - " | ".join( - [ - "file1".rjust(14), - "file2".rjust(14), - "delta".rjust(14), - "%".rjust(7), - "name", - ] - ) - ) - - index = _sort_index(args.sort) - reverse = not args.ascending - sorted_rows = sorted(rows, key=lambda x: x[index], reverse=reverse) - for (c1, c2, delta, delta_pc, key) in sorted_rows: - if abs(delta_pc) >= args.low_watermark: - print( - " | ".join( - [ - "{:14,}".format(c1), - "{:14,}".format(c2), - "{:+14,}".format(delta), - "{:+7.3f}".format(delta_pc), - key, - ] - ) - ) - - -def _sort_index(key): - return ("file1", "file2", "delta").index(key) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser("cg_diff.py") - - parser.add_argument( - "--sort", - choices=("file1", "file2", "delta"), - default="file1", - help="The column to sort on.", - ) - - parser.add_argument( - "--ascending", action="store_true", help="Sorts in ascending order." - ) - - parser.add_argument( - "--only-common", - action="store_true", - help="Only print instructions present in both files.", - ) - - parser.add_argument( - "--no-demangle", - action="store_false", - dest="demangle", - help="Disables demangling of input files.", - ) - - parser.add_argument("--event", default="Ir", help="The event to compare.") - - parser.add_argument( - "--low-watermark", - type=float, - default=0.01, - help="A low watermark, percentage changes in counts " - "relative to the total instruction count of " - "file1 below this value will not be printed.", - ) - - parser.add_argument( - "--summary", action="store_true", help="Prints a summary of the diff." - ) - - parser.add_argument("file1") - parser.add_argument("file2") - - args = parser.parse_args() - - if args.summary: - print_summary(args) - else: - print_diff_table(args) diff --git a/scripts/fix-project-settings.rb b/scripts/fix-project-settings.rb deleted file mode 100644 index c6038fca0..000000000 --- a/scripts/fix-project-settings.rb +++ /dev/null @@ -1,26 +0,0 @@ -require 'xcodeproj' -project_path = ARGV[0] -project = Xcodeproj::Project.open(project_path) - -# Fix indentation settings. -project.main_group.uses_tabs = '0' -project.main_group.tab_width = '2' -project.main_group.indent_width = '2' - -# Set the `CURRENT_PROJECT_VERSION` variable for each config to ensure -# that the generated frameworks pass App Store validation (#291). -project.build_configurations.each do |config| - config.build_settings["CURRENT_PROJECT_VERSION"] = "1.0" -end - -# Set each target's iOS deployment target to 10.0 -project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings["IPHONEOS_DEPLOYMENT_TARGET"] = "10.0" - if config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] then - config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] = "io.grpc." + config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] - end - end -end - -project.save \ No newline at end of file diff --git a/scripts/make-sample-certs.py b/scripts/make-sample-certs.py deleted file mode 100755 index 8f4ec0596..000000000 --- a/scripts/make-sample-certs.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2022, 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 argparse -import datetime -import os -import shutil -import subprocess -import tempfile - -TEMPLATE = """\ -/* - * Copyright {year}, 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. - */ - -//----------------------------------------------------------------------------- -// THIS FILE WAS GENERATED WITH make-sample-certs.py -// -// DO NOT UPDATE MANUALLY -//----------------------------------------------------------------------------- - -#if canImport(NIOSSL) -import struct Foundation.Date -import NIOSSL - -/// Wraps `NIOSSLCertificate` to provide the certificate common name and expiry date. -public struct SampleCertificate {{ - public var certificate: NIOSSLCertificate - public var commonName: String - public var notAfter: Date - - public static let ca = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem), - commonName: "some-ca", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) - - public static let otherCA = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem), - commonName: "some-other-ca", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) - - public static let server = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) - - public static let exampleServer = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem), - commonName: "example.com", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) - - public static let serverSignedByOtherCA = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) - - public static let client = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) - - public static let clientSignedByOtherCA = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) - - public static let exampleServerWithExplicitCurve = SampleCertificate( - certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem), - commonName: "localhost", - notAfter: Date(timeIntervalSince1970: {timestamp}) - ) -}} - -extension SampleCertificate {{ - /// Returns whether the certificate has expired. - public var isExpired: Bool {{ - return self.notAfter < Date() - }} -}} - -/// Provides convenience methods to make `NIOSSLPrivateKey`s for corresponding `GRPCSwiftCertificate`s. -public struct SamplePrivateKey {{ - private init() {{}} - - public static let server = try! NIOSSLPrivateKey(bytes: .init(serverKey.utf8), format: .pem) - public static let exampleServer = try! NIOSSLPrivateKey( - bytes: .init(exampleServerKey.utf8), - format: .pem - ) - public static let client = try! NIOSSLPrivateKey(bytes: .init(clientKey.utf8), format: .pem) - public static let exampleServerWithExplicitCurve = try! NIOSSLPrivateKey( - bytes: .init(serverExplicitCurveKey.utf8), - format: .pem - ) -}} - -// MARK: - Certificates and private keys - -private let caCert = \""" -{ca_cert} -\""" - -private let otherCACert = \""" -{other_ca_cert} -\""" - -private let serverCert = \""" -{server_cert} -\""" - -private let serverSignedByOtherCACert = \""" -{server_signed_by_other_ca_cert} -\""" - -private let serverKey = \""" -{server_key} -\""" - -private let exampleServerCert = \""" -{example_server_cert} -\""" - -private let exampleServerKey = \""" -{example_server_key} -\""" - -private let clientCert = \""" -{client_cert} -\""" - -private let clientSignedByOtherCACert = \""" -{client_signed_by_other_ca_cert} -\""" - -private let clientKey = \""" -{client_key} -\""" - -private let serverExplicitCurveCert = \""" -{server_explicit_curve_cert} -\""" - -private let serverExplicitCurveKey = \""" -{server_explicit_curve_key} -\""" - -#endif // canImport(NIOSSL) -""" - -def load_file(root, name): - with open(os.path.join(root, name)) as fh: - return fh.read().strip() - - -def extract_key(ec_key_and_params): - lines = [] - include_line = True - for line in ec_key_and_params.split("\n"): - if line == "-----BEGIN EC PARAMETERS-----": - include_line = False - elif line == "-----BEGIN EC PRIVATE KEY-----": - include_line = True - - if include_line: - lines.append(line) - return "\n".join(lines).strip() - - -def update_sample_certs(): - now = datetime.datetime.now() - # makecert uses an expiry of 365 days. - delta = datetime.timedelta(days=365) - # Seconds since epoch - not_after = (now + delta).strftime("%s") - - # Expect to be called from the root of the checkout. - root = os.path.abspath(os.curdir) - executable = os.path.join(root, "scripts", "makecert") - try: - subprocess.check_call(executable) - except FileNotFoundError: - print("Please run the script from the root of the repository") - exit(1) - - kwargs = { - "year": now.year, - "timestamp": not_after, - "ca_cert": load_file(root, "ca.crt"), - "other_ca_cert": load_file(root, "other-ca.crt"), - "server_cert": load_file(root, "server-localhost.crt"), - "server_signed_by_other_ca_cert": load_file(root, "server-localhost-other-ca.crt"), - "server_key": load_file(root, "server-localhost.key"), - "example_server_cert": load_file(root, "server-example.com.crt"), - "example_server_key": load_file(root, "server-example.com.key"), - "client_cert": load_file(root, "client.crt"), - "client_signed_by_other_ca_cert": load_file(root, "client-other-ca.crt"), - "client_key": load_file(root, "client.key"), - "server_explicit_curve_cert": load_file(root, "server-explicit-ec.crt"), - "server_explicit_curve_key": extract_key(load_file(root, - "server-explicit-ec.key")) - } - - formatted = TEMPLATE.format(**kwargs) - with open("Sources/GRPCSampleData/GRPCSwiftCertificate.swift", "w") as fh: - fh.write(formatted) - - -def update_p12_bundle(): - tmp_dir = tempfile.TemporaryDirectory() - subprocess.check_call(["git", "clone", "--single-branch", "--branch", - "make-p12-bundle-for-grpc-swift-tests", - "https://github.com/glbrntt/swift-nio-ssl", - tmp_dir.name]) - - subprocess.check_call(["./make-pkcs12.sh"], cwd=tmp_dir.name) - shutil.copyfile(tmp_dir.name + "/bundle.p12", "Sources/GRPCSampleData/bundle.p12") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--no-p12-bundle", action="store_false", dest="p12") - parser.add_argument("--no-sample-certs", action="store_false", dest="sample_certs") - args = parser.parse_args() - - if args.sample_certs: - update_sample_certs() - - if args.p12: - update_p12_bundle() diff --git a/scripts/makecert b/scripts/makecert deleted file mode 100755 index 6f97ec4d8..000000000 --- a/scripts/makecert +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -# -# Creates a trust collection certificate (ca.crt) -# and self-signed server certificate (server.crt) and private key (server.pem) -# and client certificate (client.crt) and key file (client.pem) for mutual TLS. -# Replace "example.com" with the host name you'd like for your certificate. -# -# https://github.com/grpc/grpc-java/tree/master/examples -# - -set -euo pipefail - -SIZE=2048 - -# CA -openssl genrsa -out ca.key $SIZE -openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=some-ca" - -# Other CA -openssl genrsa -out other-ca.key $SIZE -openssl req -new -x509 -days 365 -key other-ca.key -out other-ca.crt -subj "/CN=some-other-ca" - -# Server certs (localhost) -openssl genrsa -out server-localhost.key $SIZE -openssl req -new -key server-localhost.key -out server-localhost.csr -subj "/CN=localhost" -openssl x509 -req -days 365 -in server-localhost.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server-localhost.crt -openssl x509 -req -days 365 -in server-localhost.csr -CA other-ca.crt -CAkey other-ca.key -set_serial 01 -out server-localhost-other-ca.crt - -# Server certs (example.com) -openssl genrsa -out server-example.com.key $SIZE -openssl req -new -key server-example.com.key -out server-example.com.csr -subj "/CN=example.com" -openssl x509 -req -days 365 -in server-example.com.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server-example.com.crt - -# Client certs (localhost) -openssl genrsa -out client.key $SIZE -openssl req -new -key client.key -out client.csr -subj "/CN=localhost" -openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt -openssl x509 -req -days 365 -in client.csr -CA other-ca.crt -CAkey other-ca.key -set_serial 01 -out client-other-ca.crt - -# netty only supports PKCS8 keys. openssl is used to convert from PKCS1 to PKCS8 -# http://netty.io/wiki/sslcontextbuilder-and-private-key.html -openssl pkcs8 -topk8 -nocrypt -in client.key -out client.pem -openssl pkcs8 -topk8 -nocrypt -in server-example.com.key -out server.pem - -# Server cert with explicit EC parameters (not supported) -openssl ecparam -name prime256v1 -genkey -param_enc explicit -out server-explicit-ec.key -openssl req -new -x509 -days 365 -key server-explicit-ec.key -out server-explicit-ec.crt -subj "/CN=example.com" diff --git a/scripts/run-plugin-tests.sh b/scripts/run-plugin-tests.sh deleted file mode 100755 index c772508c2..000000000 --- a/scripts/run-plugin-tests.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/bin/bash - -# 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. - -set -eux - -HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -GRPC_PATH="${HERE}/.." - -function generate_package_manifest { - local tools_version=$1 - local grpc_path=$2 - local grpc_version=$3 - - echo "// swift-tools-version: $tools_version" - echo "import PackageDescription" - echo "" - echo "let package = Package(" - echo " name: \"Foo\"," - echo " dependencies: [" - echo " .package(path: \"$grpc_path\")," - echo " .package(url: \"https://github.com/apple/swift-protobuf\", from: \"1.26.0\")" - echo " ]," - echo " targets: [" - echo " .executableTarget(" - echo " name: \"Foo\"," - echo " dependencies: [" - - if [ "$grpc_version" == "v1" ]; then - echo " .product(name: \"GRPC\", package: \"grpc-swift\")," - elif [ "$grpc_version" == "v2" ]; then - echo " .product(name: \"_GRPCCore\", package: \"grpc-swift\")," - echo " .product(name: \"_GRPCProtobuf\", package: \"grpc-swift\")," - fi - - echo " ]," - echo " path: \"Sources/Foo\"," - echo " plugins: [" - echo " .plugin(name: \"GRPCSwiftPlugin\", package: \"grpc-swift\")," - echo " .plugin(name: \"SwiftProtobufPlugin\", package: \"swift-protobuf\")," - echo " ]" - echo " )," - echo " ]" - echo ")" -} - -function generate_grpc_plugin_config { - local grpc_version=$1 - - echo "{" - echo " \"invocations\": [" - echo " {" - if [ "$grpc_version" == "v2" ]; then - echo " \"_V2\": true," - fi - echo " \"protoFiles\": [\"Foo.proto\"]," - echo " \"visibility\": \"internal\"" - echo " }" - echo " ]" - echo "}" -} - -function generate_protobuf_plugin_config { - echo "{" - echo " \"invocations\": [" - echo " {" - echo " \"protoFiles\": [\"Foo.proto\"]," - echo " \"visibility\": \"internal\"" - echo " }" - echo " ]" - echo "}" -} - -function generate_proto { - cat < "$dir/Package.swift" - - generate_protobuf_plugin_config > "$dir/Sources/Foo/swift-protobuf-config.json" - generate_proto > "$dir/Sources/Foo/Foo.proto" - generate_main > "$dir/Sources/Foo/main.swift" - generate_grpc_plugin_config "$grpc_version" > "$dir/Sources/Foo/grpc-swift-config.json" - - PROTOC_PATH=$protoc_path swift build --package-path "$dir" -} - -if [[ $# -lt 2 ]]; then - echo "Usage: $0 SWIFT_TOOLS_VERSION GRPC_SWIFT_VERSION" -fi - -if [ "$2" != "v1" ] && [ "$2" != "v2" ]; then - echo "Invalid gRPC Swift version '$2' (must be 'v1' or 'v2')" - exit 1 -fi - -generate_and_build "$1" "${GRPC_PATH}" "$2" From cf29048827619a9d0dd304ba60e34143403f4e58 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 24 Sep 2024 11:10:05 +0100 Subject: [PATCH 472/580] Make examples standalone packages (#2067) Motivation: Standalone packages for examples are an easier on-ramp for newcomers as they only see the details they care about and it can act as a starting point which they can develop from. Modifications: - Turn each example into a standalone package and add a README to each - Update the generate and fetch scripts and re-run them Result: Better examples --------- Co-authored-by: Gus Cairo --- .github/workflows/ci.yaml | 15 + Examples/echo/.gitignore | 8 + Examples/echo/Package.swift | 38 ++ Examples/echo/README.md | 46 +++ Examples/{v2/echo => echo/Sources}/Echo.swift | 1 - .../Sources}/Generated/echo.grpc.swift | 0 .../Sources}/Generated/echo.pb.swift | 0 .../Subcommands/ClientArguments.swift | 2 +- .../Sources}/Subcommands/Collect.swift | 4 +- .../Sources}/Subcommands/Expand.swift | 4 +- .../Sources}/Subcommands/Get.swift | 4 +- .../Sources}/Subcommands/Serve.swift | 4 +- .../Sources}/Subcommands/Update.swift | 4 +- Examples/hello-world/.gitignore | 8 + Examples/hello-world/Package.swift | 38 ++ .../hello-world/Protos | 0 Examples/hello-world/README.md | 34 ++ .../Sources}/Generated/helloworld.grpc.swift | 0 .../Sources}/Generated/helloworld.pb.swift | 0 .../Sources}/HelloWorld.swift | 1 - .../Sources}/Subcommands/Greet.swift | 3 +- .../Sources}/Subcommands/Serve.swift | 4 +- Examples/route-guide/.gitignore | 8 + Examples/route-guide/Package.swift | 41 +++ Examples/route-guide/README.md | 44 +++ .../Sources}/Generated/route_guide.grpc.swift | 0 .../Sources}/Generated/route_guide.pb.swift | 0 .../Sources}/RouteGuide.swift | 0 .../Sources}/Subcommands/GetFeature.swift | 3 +- .../Sources}/Subcommands/ListFeatures.swift | 3 +- .../Sources}/Subcommands/RecordRoute.swift | 3 +- .../Sources}/Subcommands/RouteChat.swift | 3 +- .../Sources}/Subcommands/Serve.swift | 11 +- .../Sources}/route_guide_db.json | 0 Examples/v2/hello-world/HelloWorld.proto | 1 - .../Generated/rls_config.pb.swift | 9 + dev/Protos/generate.sh | 73 ---- .../upstream/grpc/health/v1/health.proto | 73 ---- .../grpc/reflection/v1/reflection.proto | 147 -------- .../grpc/reflection/v1alpha/reflection.proto | 145 -------- .../grpc/testing/benchmark_service.proto | 48 --- .../upstream/grpc/testing/control.proto | 299 --------------- .../upstream/grpc/testing/messages.proto | 347 ------------------ .../upstream/grpc/testing/payloads.proto | 44 --- dev/Protos/upstream/grpc/testing/stats.proto | 87 ----- .../grpc/testing/worker_service.proto | 49 --- dev/build-examples.sh | 39 ++ dev/check-generated-code.sh | 2 +- dev/format.sh | 3 +- dev/license-check.sh | 2 +- dev/{Protos => protos}/README.md | 0 .../examples/echo/echo.proto | 2 +- .../examples/route_guide/route_guide.proto | 0 dev/{Protos => protos}/fetch.sh | 4 +- dev/protos/generate.sh | 125 +++++++ .../upstream/google/rpc/code.proto | 0 .../upstream/grpc/examples/helloworld.proto} | 31 +- .../upstream/grpc/lookup/v1/rls.proto | 0 .../upstream/grpc/lookup/v1/rls_config.proto | 3 + .../grpc/service_config/service_config.proto | 0 60 files changed, 495 insertions(+), 1372 deletions(-) create mode 100644 Examples/echo/.gitignore create mode 100644 Examples/echo/Package.swift create mode 100644 Examples/echo/README.md rename Examples/{v2/echo => echo/Sources}/Echo.swift (92%) rename Examples/{v2/echo => echo/Sources}/Generated/echo.grpc.swift (100%) rename Examples/{v2/echo => echo/Sources}/Generated/echo.pb.swift (100%) rename Examples/{v2/echo => echo/Sources}/Subcommands/ClientArguments.swift (97%) rename Examples/{v2/echo => echo/Sources}/Subcommands/Collect.swift (92%) rename Examples/{v2/echo => echo/Sources}/Subcommands/Expand.swift (92%) rename Examples/{v2/echo => echo/Sources}/Subcommands/Get.swift (92%) rename Examples/{v2/echo => echo/Sources}/Subcommands/Serve.swift (95%) rename Examples/{v2/echo => echo/Sources}/Subcommands/Update.swift (93%) create mode 100644 Examples/hello-world/.gitignore create mode 100644 Examples/hello-world/Package.swift rename dev/Protos/upstream/grpc/examples/helloworld.proto => Examples/hello-world/Protos (100%) create mode 100644 Examples/hello-world/README.md rename Examples/{v2/hello-world => hello-world/Sources}/Generated/helloworld.grpc.swift (100%) rename Examples/{v2/hello-world => hello-world/Sources}/Generated/helloworld.pb.swift (100%) rename Examples/{v2/hello-world => hello-world/Sources}/HelloWorld.swift (92%) rename Examples/{v2/hello-world => hello-world/Sources}/Subcommands/Greet.swift (93%) rename Examples/{v2/hello-world => hello-world/Sources}/Subcommands/Serve.swift (91%) create mode 100644 Examples/route-guide/.gitignore create mode 100644 Examples/route-guide/Package.swift create mode 100644 Examples/route-guide/README.md rename Examples/{v2/route-guide => route-guide/Sources}/Generated/route_guide.grpc.swift (100%) rename Examples/{v2/route-guide => route-guide/Sources}/Generated/route_guide.pb.swift (100%) rename Examples/{v2/route-guide => route-guide/Sources}/RouteGuide.swift (100%) rename Examples/{v2/route-guide => route-guide/Sources}/Subcommands/GetFeature.swift (95%) rename Examples/{v2/route-guide => route-guide/Sources}/Subcommands/ListFeatures.swift (96%) rename Examples/{v2/route-guide => route-guide/Sources}/Subcommands/RecordRoute.swift (95%) rename Examples/{v2/route-guide => route-guide/Sources}/Subcommands/RouteChat.swift (95%) rename Examples/{v2/route-guide => route-guide/Sources}/Subcommands/Serve.swift (96%) rename Examples/{v2/route-guide => route-guide/Sources}/route_guide_db.json (100%) delete mode 120000 Examples/v2/hello-world/HelloWorld.proto delete mode 100755 dev/Protos/generate.sh delete mode 100644 dev/Protos/upstream/grpc/health/v1/health.proto delete mode 100644 dev/Protos/upstream/grpc/reflection/v1/reflection.proto delete mode 100644 dev/Protos/upstream/grpc/reflection/v1alpha/reflection.proto delete mode 100644 dev/Protos/upstream/grpc/testing/benchmark_service.proto delete mode 100644 dev/Protos/upstream/grpc/testing/control.proto delete mode 100644 dev/Protos/upstream/grpc/testing/messages.proto delete mode 100644 dev/Protos/upstream/grpc/testing/payloads.proto delete mode 100644 dev/Protos/upstream/grpc/testing/stats.proto delete mode 100644 dev/Protos/upstream/grpc/testing/worker_service.proto create mode 100755 dev/build-examples.sh rename dev/{Protos => protos}/README.md (100%) rename dev/{Protos => protos}/examples/echo/echo.proto (99%) rename dev/{Protos => protos}/examples/route_guide/route_guide.proto (100%) rename dev/{Protos => protos}/fetch.sh (91%) create mode 100755 dev/protos/generate.sh rename dev/{Protos => protos}/upstream/google/rpc/code.proto (100%) rename dev/{Protos/upstream/grpc/core/stats.proto => protos/upstream/grpc/examples/helloworld.proto} (53%) rename dev/{Protos => protos}/upstream/grpc/lookup/v1/rls.proto (100%) rename dev/{Protos => protos}/upstream/grpc/lookup/v1/rls_config.proto (99%) rename dev/{Protos => protos}/upstream/grpc/service_config/service_config.proto (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d795e9681..05a329f30 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,3 +65,18 @@ jobs: working-directory: "./IntegrationTests/Benchmarks" run: swift package benchmark baseline check --no-progress --check-absolute-path Thresholds/${{ matrix.swift-version }}/ timeout-minutes: 20 + examples: + strategy: + fail-fast: false + matrix: + include: + - image: swiftlang/swift:nightly-jammy + - image: swift:6.0-jammy + name: Build examples using ${{ matrix.image }} + runs-on: ubuntu-latest + container: + image: ${{ matrix.image }} + steps: + - uses: actions/checkout@v4 + - name: Build examples + run: ./dev/build-examples.sh diff --git a/Examples/echo/.gitignore b/Examples/echo/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/echo/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift new file mode 100644 index 000000000..d9769fb16 --- /dev/null +++ b/Examples/echo/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version:6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "echo", + platforms: [.macOS("15.0")], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift-protobuf", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "main"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "echo", + dependencies: [ + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ) + ] +) diff --git a/Examples/echo/README.md b/Examples/echo/README.md new file mode 100644 index 000000000..bea921190 --- /dev/null +++ b/Examples/echo/README.md @@ -0,0 +1,46 @@ +# Echo + +This example demonstrates all four RPC types using a simple 'echo' service and +client and the Swift NIO based HTTP/2 transport. + +## Overview + +An "echo" command line tool that uses generated stubs for an 'echo' service +which allows you to start a server and to make requests against it for each of +the four RPC types. + +The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) +HTTP/2 transport. + +## Usage + +Build and run the server using the CLI: + +```console +$ swift run echo serve +Echo listening on [ipv4]127.0.0.1:1234 +``` + +Use the CLI to make a unary 'Get' request against it: + +```console +$ swift run echo get --message "Hello" +get → Hello +get ← Hello +``` + +Use the CLI to make a bidirectional streaming 'Update' request: + +```console +$ swift run echo update --message "Hello World" +update → Hello +update → World +update ← Hello +update ← World +``` + +Get help with the CLI by running: + +```console +$ swift run echo --help +``` diff --git a/Examples/v2/echo/Echo.swift b/Examples/echo/Sources/Echo.swift similarity index 92% rename from Examples/v2/echo/Echo.swift rename to Examples/echo/Sources/Echo.swift index 8ff07f420..edbe8d12f 100644 --- a/Examples/v2/echo/Echo.swift +++ b/Examples/echo/Sources/Echo.swift @@ -17,7 +17,6 @@ import ArgumentParser @main -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Echo: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "echo", diff --git a/Examples/v2/echo/Generated/echo.grpc.swift b/Examples/echo/Sources/Generated/echo.grpc.swift similarity index 100% rename from Examples/v2/echo/Generated/echo.grpc.swift rename to Examples/echo/Sources/Generated/echo.grpc.swift diff --git a/Examples/v2/echo/Generated/echo.pb.swift b/Examples/echo/Sources/Generated/echo.pb.swift similarity index 100% rename from Examples/v2/echo/Generated/echo.pb.swift rename to Examples/echo/Sources/Generated/echo.pb.swift diff --git a/Examples/v2/echo/Subcommands/ClientArguments.swift b/Examples/echo/Sources/Subcommands/ClientArguments.swift similarity index 97% rename from Examples/v2/echo/Subcommands/ClientArguments.swift rename to Examples/echo/Sources/Subcommands/ClientArguments.swift index 7dea8e59f..afa8cbd46 100644 --- a/Examples/v2/echo/Subcommands/ClientArguments.swift +++ b/Examples/echo/Sources/Subcommands/ClientArguments.swift @@ -15,7 +15,7 @@ */ import ArgumentParser -import GRPCHTTP2Core +import GRPCNIOTransportHTTP2 struct ClientArguments: ParsableArguments { @Option(help: "The server's listening port") diff --git a/Examples/v2/echo/Subcommands/Collect.swift b/Examples/echo/Sources/Subcommands/Collect.swift similarity index 92% rename from Examples/v2/echo/Subcommands/Collect.swift rename to Examples/echo/Sources/Subcommands/Collect.swift index 3a61915df..6022d8a2d 100644 --- a/Examples/v2/echo/Subcommands/Collect.swift +++ b/Examples/echo/Sources/Subcommands/Collect.swift @@ -16,10 +16,8 @@ import ArgumentParser import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Collect: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "Makes a client streaming RPC to the echo server." diff --git a/Examples/v2/echo/Subcommands/Expand.swift b/Examples/echo/Sources/Subcommands/Expand.swift similarity index 92% rename from Examples/v2/echo/Subcommands/Expand.swift rename to Examples/echo/Sources/Subcommands/Expand.swift index 1d06bdd99..d2e48bbf3 100644 --- a/Examples/v2/echo/Subcommands/Expand.swift +++ b/Examples/echo/Sources/Subcommands/Expand.swift @@ -16,10 +16,8 @@ import ArgumentParser import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Expand: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "Makes a server streaming RPC to the echo server." diff --git a/Examples/v2/echo/Subcommands/Get.swift b/Examples/echo/Sources/Subcommands/Get.swift similarity index 92% rename from Examples/v2/echo/Subcommands/Get.swift rename to Examples/echo/Sources/Subcommands/Get.swift index 0dd551002..ea69bc5f3 100644 --- a/Examples/v2/echo/Subcommands/Get.swift +++ b/Examples/echo/Sources/Subcommands/Get.swift @@ -16,10 +16,8 @@ import ArgumentParser import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Get: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Makes a unary RPC to the echo server.") diff --git a/Examples/v2/echo/Subcommands/Serve.swift b/Examples/echo/Sources/Subcommands/Serve.swift similarity index 95% rename from Examples/v2/echo/Subcommands/Serve.swift rename to Examples/echo/Sources/Subcommands/Serve.swift index 5bfa1772f..6ea8648cb 100644 --- a/Examples/v2/echo/Subcommands/Serve.swift +++ b/Examples/echo/Sources/Subcommands/Serve.swift @@ -16,10 +16,8 @@ import ArgumentParser import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Serve: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Starts an echo server.") diff --git a/Examples/v2/echo/Subcommands/Update.swift b/Examples/echo/Sources/Subcommands/Update.swift similarity index 93% rename from Examples/v2/echo/Subcommands/Update.swift rename to Examples/echo/Sources/Subcommands/Update.swift index 1c189caa8..1b4c81d00 100644 --- a/Examples/v2/echo/Subcommands/Update.swift +++ b/Examples/echo/Sources/Subcommands/Update.swift @@ -16,10 +16,8 @@ import ArgumentParser import GRPCCore -import GRPCHTTP2Core -import GRPCHTTP2TransportNIOPosix +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Update: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "Makes a bidirectional server streaming RPC to the echo server." diff --git a/Examples/hello-world/.gitignore b/Examples/hello-world/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/hello-world/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift new file mode 100644 index 000000000..94c96c027 --- /dev/null +++ b/Examples/hello-world/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version:6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "hello-world", + platforms: [.macOS("15.0")], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift-protobuf", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "main"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "hello-world", + dependencies: [ + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ) + ] +) diff --git a/dev/Protos/upstream/grpc/examples/helloworld.proto b/Examples/hello-world/Protos similarity index 100% rename from dev/Protos/upstream/grpc/examples/helloworld.proto rename to Examples/hello-world/Protos diff --git a/Examples/hello-world/README.md b/Examples/hello-world/README.md new file mode 100644 index 000000000..9a1ff32ce --- /dev/null +++ b/Examples/hello-world/README.md @@ -0,0 +1,34 @@ +# Hello World + +This example demonstrates the canonical "Hello World" in gRPC. + +## Overview + +A "hello-world" command line tool that uses generated stubs for the 'Greeter' +service which allows you to start a server and to make requests against it. + +The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) +HTTP/2 transport. + +## Usage + +Build and run the server using the CLI: + +```console +$ swift run hello-world serve +Greeter listening on [ipv4]127.0.0.1:31415 +``` + +Use the CLI to send a request to the service: + +```console +$ swift run hello-world greet +Hello, stranger +``` + +Send the name of the greetee in the request by specifying a `--name`: + +```console +$ swift run hello-world greet --name "PanCakes 🐶" +Hello, PanCakes 🐶 +``` diff --git a/Examples/v2/hello-world/Generated/helloworld.grpc.swift b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift similarity index 100% rename from Examples/v2/hello-world/Generated/helloworld.grpc.swift rename to Examples/hello-world/Sources/Generated/helloworld.grpc.swift diff --git a/Examples/v2/hello-world/Generated/helloworld.pb.swift b/Examples/hello-world/Sources/Generated/helloworld.pb.swift similarity index 100% rename from Examples/v2/hello-world/Generated/helloworld.pb.swift rename to Examples/hello-world/Sources/Generated/helloworld.pb.swift diff --git a/Examples/v2/hello-world/HelloWorld.swift b/Examples/hello-world/Sources/HelloWorld.swift similarity index 92% rename from Examples/v2/hello-world/HelloWorld.swift rename to Examples/hello-world/Sources/HelloWorld.swift index 8d467670a..6877b055f 100644 --- a/Examples/v2/hello-world/HelloWorld.swift +++ b/Examples/hello-world/Sources/HelloWorld.swift @@ -17,7 +17,6 @@ import ArgumentParser @main -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct HelloWorld: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "hello-world", diff --git a/Examples/v2/hello-world/Subcommands/Greet.swift b/Examples/hello-world/Sources/Subcommands/Greet.swift similarity index 93% rename from Examples/v2/hello-world/Subcommands/Greet.swift rename to Examples/hello-world/Sources/Subcommands/Greet.swift index 069b8faee..940a1c9cb 100644 --- a/Examples/v2/hello-world/Subcommands/Greet.swift +++ b/Examples/hello-world/Sources/Subcommands/Greet.swift @@ -15,10 +15,9 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 import GRPCProtobuf -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Greet: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Sends a request to the greeter server") diff --git a/Examples/v2/hello-world/Subcommands/Serve.swift b/Examples/hello-world/Sources/Subcommands/Serve.swift similarity index 91% rename from Examples/v2/hello-world/Subcommands/Serve.swift rename to Examples/hello-world/Sources/Subcommands/Serve.swift index a9dd178ec..75f083ece 100644 --- a/Examples/v2/hello-world/Subcommands/Serve.swift +++ b/Examples/hello-world/Sources/Subcommands/Serve.swift @@ -15,10 +15,9 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 import GRPCProtobuf -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Serve: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Starts a greeter server.") @@ -43,7 +42,6 @@ struct Serve: AsyncParsableCommand { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Greeter: Helloworld_GreeterServiceProtocol { func sayHello( request: ServerRequest.Single, diff --git a/Examples/route-guide/.gitignore b/Examples/route-guide/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/Examples/route-guide/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift new file mode 100644 index 000000000..baf6d2162 --- /dev/null +++ b/Examples/route-guide/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version:6.0 +/* + * 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 PackageDescription + +let package = Package( + name: "route-guide", + platforms: [.macOS("15.0")], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift-protobuf", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "main"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), + ], + targets: [ + .executableTarget( + name: "route-guide", + dependencies: [ + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + resources: [ + .copy("route_guide_db.json") + ] + ) + ] +) diff --git a/Examples/route-guide/README.md b/Examples/route-guide/README.md new file mode 100644 index 000000000..d19a53c87 --- /dev/null +++ b/Examples/route-guide/README.md @@ -0,0 +1,44 @@ +# Route Guide + +This example demonstrates all four RPC types using a 'Route Guide' service and +client. + +## Overview + +A "route-guide" command line tool that uses generated stubs for a 'Route Guide' +service allows you to start a server and to make requests against it for +each of the four RPC types. + +The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) +HTTP/2 transport. + +This example has an accompanying tutorial hosted on the [Swift Package +Index](https://swiftpackageindex.com/grpc/grpc-swift/main/tutorials/grpccore/route-guide). + +## Usage + +Build and run the server using the CLI: + +```console +$ swift run route-guide serve +server listening on [ipv4]127.0.0.1:31415 +``` + +Use the CLI to interrogate the different RPCs you can call: + +```console +$ swift run route-guide --help +USAGE: route-guide + +OPTIONS: + -h, --help Show help information. + +SUBCOMMANDS: + serve Starts a route-guide server. + get-feature Gets a feature at a given location. + list-features List all features within a bounding rectangle. + record-route Records a route by visiting N randomly selected points and prints a summary of it. + route-chat Visits a few points and records a note at each, and prints all notes previously recorded at each point. + + See 'route-guide help ' for detailed help. +``` diff --git a/Examples/v2/route-guide/Generated/route_guide.grpc.swift b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift similarity index 100% rename from Examples/v2/route-guide/Generated/route_guide.grpc.swift rename to Examples/route-guide/Sources/Generated/route_guide.grpc.swift diff --git a/Examples/v2/route-guide/Generated/route_guide.pb.swift b/Examples/route-guide/Sources/Generated/route_guide.pb.swift similarity index 100% rename from Examples/v2/route-guide/Generated/route_guide.pb.swift rename to Examples/route-guide/Sources/Generated/route_guide.pb.swift diff --git a/Examples/v2/route-guide/RouteGuide.swift b/Examples/route-guide/Sources/RouteGuide.swift similarity index 100% rename from Examples/v2/route-guide/RouteGuide.swift rename to Examples/route-guide/Sources/RouteGuide.swift diff --git a/Examples/v2/route-guide/Subcommands/GetFeature.swift b/Examples/route-guide/Sources/Subcommands/GetFeature.swift similarity index 95% rename from Examples/v2/route-guide/Subcommands/GetFeature.swift rename to Examples/route-guide/Sources/Subcommands/GetFeature.swift index 1c42ec6da..b3110e0c9 100644 --- a/Examples/v2/route-guide/Subcommands/GetFeature.swift +++ b/Examples/route-guide/Sources/Subcommands/GetFeature.swift @@ -15,9 +15,8 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct GetFeature: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Gets a feature at a given location.") diff --git a/Examples/v2/route-guide/Subcommands/ListFeatures.swift b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift similarity index 96% rename from Examples/v2/route-guide/Subcommands/ListFeatures.swift rename to Examples/route-guide/Sources/Subcommands/ListFeatures.swift index 887a944e2..383eda154 100644 --- a/Examples/v2/route-guide/Subcommands/ListFeatures.swift +++ b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift @@ -15,9 +15,8 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ListFeatures: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "List all features within a bounding rectangle." diff --git a/Examples/v2/route-guide/Subcommands/RecordRoute.swift b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift similarity index 95% rename from Examples/v2/route-guide/Subcommands/RecordRoute.swift rename to Examples/route-guide/Sources/Subcommands/RecordRoute.swift index cd443230b..c3fcce361 100644 --- a/Examples/v2/route-guide/Subcommands/RecordRoute.swift +++ b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift @@ -15,9 +15,8 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RecordRoute: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "Records a route by visiting N randomly selected points and prints a summary of it." diff --git a/Examples/v2/route-guide/Subcommands/RouteChat.swift b/Examples/route-guide/Sources/Subcommands/RouteChat.swift similarity index 95% rename from Examples/v2/route-guide/Subcommands/RouteChat.swift rename to Examples/route-guide/Sources/Subcommands/RouteChat.swift index 81cb5c3e2..23266e3bf 100644 --- a/Examples/v2/route-guide/Subcommands/RouteChat.swift +++ b/Examples/route-guide/Sources/Subcommands/RouteChat.swift @@ -15,9 +15,8 @@ */ import ArgumentParser -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RouteChat: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: """ diff --git a/Examples/v2/route-guide/Subcommands/Serve.swift b/Examples/route-guide/Sources/Subcommands/Serve.swift similarity index 96% rename from Examples/v2/route-guide/Subcommands/Serve.swift rename to Examples/route-guide/Sources/Subcommands/Serve.swift index f9b942876..5bfdc200e 100644 --- a/Examples/v2/route-guide/Subcommands/Serve.swift +++ b/Examples/route-guide/Sources/Subcommands/Serve.swift @@ -16,11 +16,10 @@ import ArgumentParser import Foundation -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 import GRPCProtobuf import Synchronization -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct Serve: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Starts a route-guide server.") @@ -52,7 +51,6 @@ struct Serve: AsyncParsableCommand { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RouteGuideService { /// Known features. private let features: [Routeguide_Feature] @@ -100,7 +98,6 @@ struct RouteGuideService { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { func getFeature( request: ServerRequest.Single, @@ -131,7 +128,7 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in - let featuresWithinBounds = self.features.filter { feature in + let featuresWithinBounds = self.features.filter { feature in !feature.name.isEmpty && feature.isContained(by: request.message) } @@ -223,8 +220,8 @@ private func greatCircleDistance( let phi1 = radians(degreesInE7: point1.latitude) let lambda2 = radians(degreesInE7: point2.longitude) let phi2 = radians(degreesInE7: point2.latitude) - - // Δλ = λ2 - λ1 + + // Δλ = λ2 - λ1 let deltaLambda = lambda2 - lambda1 // Δφ = φ2 - φ1 let deltaPhi = phi2 - phi1 diff --git a/Examples/v2/route-guide/route_guide_db.json b/Examples/route-guide/Sources/route_guide_db.json similarity index 100% rename from Examples/v2/route-guide/route_guide_db.json rename to Examples/route-guide/Sources/route_guide_db.json diff --git a/Examples/v2/hello-world/HelloWorld.proto b/Examples/v2/hello-world/HelloWorld.proto deleted file mode 120000 index f746c2066..000000000 --- a/Examples/v2/hello-world/HelloWorld.proto +++ /dev/null @@ -1 +0,0 @@ -../../../Protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file diff --git a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift index 879269999..bc6047eda 100644 --- a/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift +++ b/Tests/GRPCCoreTests/Configuration/Generated/rls_config.pb.swift @@ -221,6 +221,9 @@ struct Grpc_Lookup_V1_HttpKeyBuilder: Sendable { /// need to separately cache and request URLs with that content. var constantKeys: Dictionary = [:] + /// If specified, the HTTP method/verb will be extracted under this key name. + var method: String = String() + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -528,6 +531,7 @@ extension Grpc_Lookup_V1_HttpKeyBuilder: SwiftProtobuf.Message, SwiftProtobuf._M 3: .standard(proto: "query_parameters"), 4: .same(proto: "headers"), 5: .standard(proto: "constant_keys"), + 6: .same(proto: "method"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -541,6 +545,7 @@ extension Grpc_Lookup_V1_HttpKeyBuilder: SwiftProtobuf.Message, SwiftProtobuf._M case 3: try { try decoder.decodeRepeatedMessageField(value: &self.queryParameters) }() case 4: try { try decoder.decodeRepeatedMessageField(value: &self.headers) }() case 5: try { try decoder.decodeMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: &self.constantKeys) }() + case 6: try { try decoder.decodeSingularStringField(value: &self.method) }() default: break } } @@ -562,6 +567,9 @@ extension Grpc_Lookup_V1_HttpKeyBuilder: SwiftProtobuf.Message, SwiftProtobuf._M if !self.constantKeys.isEmpty { try visitor.visitMapField(fieldType: SwiftProtobuf._ProtobufMap.self, value: self.constantKeys, fieldNumber: 5) } + if !self.method.isEmpty { + try visitor.visitSingularStringField(value: self.method, fieldNumber: 6) + } try unknownFields.traverse(visitor: &visitor) } @@ -571,6 +579,7 @@ extension Grpc_Lookup_V1_HttpKeyBuilder: SwiftProtobuf.Message, SwiftProtobuf._M if lhs.queryParameters != rhs.queryParameters {return false} if lhs.headers != rhs.headers {return false} if lhs.constantKeys != rhs.constantKeys {return false} + if lhs.method != rhs.method {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/dev/Protos/generate.sh b/dev/Protos/generate.sh deleted file mode 100755 index 17cad9ec1..000000000 --- a/dev/Protos/generate.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/bash -# -# 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. - -set -eu - -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -root="$here/../.." -protoc=$(which protoc) - -# Build the protoc plugins. -swift build -c release --product protoc-gen-swift - -# Grab the plugin paths. -bin_path=$(swift build -c release --show-bin-path) -protoc_gen_swift="$bin_path/protoc-gen-swift" - -# Generates messages by invoking protoc with the Swift plugin. -# Parameters: -# - $1: .proto file -# - $2: proto path -# - $3: output path -# - $4 onwards: options to forward to the plugin -function generate_message { - local proto=$1 - local args=("--plugin=$protoc_gen_swift" "--proto_path=$2" "--swift_out=$3") - - for option in "${@:4}"; do - args+=("--swift_opt=$option") - done - - invoke_protoc "${args[@]}" "$proto" -} - -function invoke_protoc { - # Setting -x when running the script produces a lot of output, instead boil - # just echo out the protoc invocations. - echo "$protoc" "$@" - "$protoc" "$@" -} - -#------------------------------------------------------------------------------ - -function generate_rpc_code_for_tests { - local protos=( - "$here/upstream/grpc/service_config/service_config.proto" - "$here/upstream/grpc/lookup/v1/rls.proto" - "$here/upstream/grpc/lookup/v1/rls_config.proto" - "$here/upstream/google/rpc/code.proto" - ) - local output="$root/Tests/GRPCCoreTests/Configuration/Generated" - - for proto in "${protos[@]}"; do - generate_message "$proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=DropPath" - done -} - -#------------------------------------------------------------------------------ - -# Tests -generate_rpc_code_for_tests diff --git a/dev/Protos/upstream/grpc/health/v1/health.proto b/dev/Protos/upstream/grpc/health/v1/health.proto deleted file mode 100644 index 13b03f567..000000000 --- a/dev/Protos/upstream/grpc/health/v1/health.proto +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2015 The gRPC Authors -// -// 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. - -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto - -syntax = "proto3"; - -package grpc.health.v1; - -option csharp_namespace = "Grpc.Health.V1"; -option go_package = "google.golang.org/grpc/health/grpc_health_v1"; -option java_multiple_files = true; -option java_outer_classname = "HealthProto"; -option java_package = "io.grpc.health.v1"; - -message HealthCheckRequest { - string service = 1; -} - -message HealthCheckResponse { - enum ServingStatus { - UNKNOWN = 0; - SERVING = 1; - NOT_SERVING = 2; - SERVICE_UNKNOWN = 3; // Used only by the Watch method. - } - ServingStatus status = 1; -} - -// Health is gRPC's mechanism for checking whether a server is able to handle -// RPCs. Its semantics are documented in -// https://github.com/grpc/grpc/blob/master/doc/health-checking.md. -service Health { - // Check gets the health of the specified service. If the requested service - // is unknown, the call will fail with status NOT_FOUND. If the caller does - // not specify a service name, the server should respond with its overall - // health status. - // - // Clients should set a deadline when calling Check, and can declare the - // server unhealthy if they do not receive a timely response. - // - // Check implementations should be idempotent and side effect free. - rpc Check(HealthCheckRequest) returns (HealthCheckResponse); - - // Performs a watch for the serving status of the requested service. - // The server will immediately send back a message indicating the current - // serving status. It will then subsequently send a new message whenever - // the service's serving status changes. - // - // If the requested service is unknown when the call is received, the - // server will send a message setting the serving status to - // SERVICE_UNKNOWN but will *not* terminate the call. If at some - // future point, the serving status of the service becomes known, the - // server will send a new message with the service's serving status. - // - // If the call terminates with status UNIMPLEMENTED, then clients - // should assume this method is not supported and should not retry the - // call. If the call terminates with any other status (including OK), - // clients should retry the call with appropriate exponential backoff. - rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse); -} diff --git a/dev/Protos/upstream/grpc/reflection/v1/reflection.proto b/dev/Protos/upstream/grpc/reflection/v1/reflection.proto deleted file mode 100644 index 1a2ceedc3..000000000 --- a/dev/Protos/upstream/grpc/reflection/v1/reflection.proto +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2016 The gRPC Authors -// -// 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. - -// Service exported by server reflection. A more complete description of how -// server reflection works can be found at -// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md -// -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto - -syntax = "proto3"; - -package grpc.reflection.v1; - -option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1"; -option java_multiple_files = true; -option java_package = "io.grpc.reflection.v1"; -option java_outer_classname = "ServerReflectionProto"; - -service ServerReflection { - // The reflection service is structured as a bidirectional stream, ensuring - // all related requests go to a single server. - rpc ServerReflectionInfo(stream ServerReflectionRequest) - returns (stream ServerReflectionResponse); -} - -// The message sent by the client when calling ServerReflectionInfo method. -message ServerReflectionRequest { - string host = 1; - // To use reflection service, the client should set one of the following - // fields in message_request. The server distinguishes requests by their - // defined field and then handles them using corresponding methods. - oneof message_request { - // Find a proto file by the file name. - string file_by_filename = 3; - - // Find the proto file that declares the given fully-qualified symbol name. - // This field should be a fully-qualified symbol name - // (e.g. .[.] or .). - string file_containing_symbol = 4; - - // Find the proto file which defines an extension extending the given - // message type with the given field number. - ExtensionRequest file_containing_extension = 5; - - // Finds the tag numbers used by all known extensions of the given message - // type, and appends them to ExtensionNumberResponse in an undefined order. - // Its corresponding method is best-effort: it's not guaranteed that the - // reflection service will implement this method, and it's not guaranteed - // that this method will provide all extensions. Returns - // StatusCode::UNIMPLEMENTED if it's not implemented. - // This field should be a fully-qualified type name. The format is - // . - string all_extension_numbers_of_type = 6; - - // List the full names of registered services. The content will not be - // checked. - string list_services = 7; - } -} - -// The type name and extension number sent by the client when requesting -// file_containing_extension. -message ExtensionRequest { - // Fully-qualified type name. The format should be . - string containing_type = 1; - int32 extension_number = 2; -} - -// The message sent by the server to answer ServerReflectionInfo method. -message ServerReflectionResponse { - string valid_host = 1; - ServerReflectionRequest original_request = 2; - // The server sets one of the following fields according to the message_request - // in the request. - oneof message_response { - // This message is used to answer file_by_filename, file_containing_symbol, - // file_containing_extension requests with transitive dependencies. - // As the repeated label is not allowed in oneof fields, we use a - // FileDescriptorResponse message to encapsulate the repeated fields. - // The reflection service is allowed to avoid sending FileDescriptorProtos - // that were previously sent in response to earlier requests in the stream. - FileDescriptorResponse file_descriptor_response = 4; - - // This message is used to answer all_extension_numbers_of_type requests. - ExtensionNumberResponse all_extension_numbers_response = 5; - - // This message is used to answer list_services requests. - ListServiceResponse list_services_response = 6; - - // This message is used when an error occurs. - ErrorResponse error_response = 7; - } -} - -// Serialized FileDescriptorProto messages sent by the server answering -// a file_by_filename, file_containing_symbol, or file_containing_extension -// request. -message FileDescriptorResponse { - // Serialized FileDescriptorProto messages. We avoid taking a dependency on - // descriptor.proto, which uses proto2 only features, by making them opaque - // bytes instead. - repeated bytes file_descriptor_proto = 1; -} - -// A list of extension numbers sent by the server answering -// all_extension_numbers_of_type request. -message ExtensionNumberResponse { - // Full name of the base type, including the package name. The format - // is . - string base_type_name = 1; - repeated int32 extension_number = 2; -} - -// A list of ServiceResponse sent by the server answering list_services request. -message ListServiceResponse { - // The information of each service may be expanded in the future, so we use - // ServiceResponse message to encapsulate it. - repeated ServiceResponse service = 1; -} - -// The information of a single service used by ListServiceResponse to answer -// list_services request. -message ServiceResponse { - // Full name of a registered service, including its package name. The format - // is . - string name = 1; -} - -// The error code and error message sent by the server when an error occurs. -message ErrorResponse { - // This field uses the error codes defined in grpc::StatusCode. - int32 error_code = 1; - string error_message = 2; -} - diff --git a/dev/Protos/upstream/grpc/reflection/v1alpha/reflection.proto b/dev/Protos/upstream/grpc/reflection/v1alpha/reflection.proto deleted file mode 100644 index 9cab8a4e0..000000000 --- a/dev/Protos/upstream/grpc/reflection/v1alpha/reflection.proto +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2016 The gRPC Authors -// -// 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. -// Service exported by server reflection - - -// Warning: this entire file is deprecated. Use this instead: -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto - -syntax = "proto3"; - -package grpc.reflection.v1alpha; - -option deprecated = true; -option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"; -option java_multiple_files = true; -option java_package = "io.grpc.reflection.v1alpha"; -option java_outer_classname = "ServerReflectionProto"; - -service ServerReflection { - // The reflection service is structured as a bidirectional stream, ensuring - // all related requests go to a single server. - rpc ServerReflectionInfo(stream ServerReflectionRequest) - returns (stream ServerReflectionResponse); -} - -// The message sent by the client when calling ServerReflectionInfo method. -message ServerReflectionRequest { - string host = 1; - // To use reflection service, the client should set one of the following - // fields in message_request. The server distinguishes requests by their - // defined field and then handles them using corresponding methods. - oneof message_request { - // Find a proto file by the file name. - string file_by_filename = 3; - - // Find the proto file that declares the given fully-qualified symbol name. - // This field should be a fully-qualified symbol name - // (e.g. .[.] or .). - string file_containing_symbol = 4; - - // Find the proto file which defines an extension extending the given - // message type with the given field number. - ExtensionRequest file_containing_extension = 5; - - // Finds the tag numbers used by all known extensions of extendee_type, and - // appends them to ExtensionNumberResponse in an undefined order. - // Its corresponding method is best-effort: it's not guaranteed that the - // reflection service will implement this method, and it's not guaranteed - // that this method will provide all extensions. Returns - // StatusCode::UNIMPLEMENTED if it's not implemented. - // This field should be a fully-qualified type name. The format is - // . - string all_extension_numbers_of_type = 6; - - // List the full names of registered services. The content will not be - // checked. - string list_services = 7; - } -} - -// The type name and extension number sent by the client when requesting -// file_containing_extension. -message ExtensionRequest { - // Fully-qualified type name. The format should be . - string containing_type = 1; - int32 extension_number = 2; -} - -// The message sent by the server to answer ServerReflectionInfo method. -message ServerReflectionResponse { - string valid_host = 1; - ServerReflectionRequest original_request = 2; - // The server set one of the following fields according to the message_request - // in the request. - oneof message_response { - // This message is used to answer file_by_filename, file_containing_symbol, - // file_containing_extension requests with transitive dependencies. As - // the repeated label is not allowed in oneof fields, we use a - // FileDescriptorResponse message to encapsulate the repeated fields. - // The reflection service is allowed to avoid sending FileDescriptorProtos - // that were previously sent in response to earlier requests in the stream. - FileDescriptorResponse file_descriptor_response = 4; - - // This message is used to answer all_extension_numbers_of_type requst. - ExtensionNumberResponse all_extension_numbers_response = 5; - - // This message is used to answer list_services request. - ListServiceResponse list_services_response = 6; - - // This message is used when an error occurs. - ErrorResponse error_response = 7; - } -} - -// Serialized FileDescriptorProto messages sent by the server answering -// a file_by_filename, file_containing_symbol, or file_containing_extension -// request. -message FileDescriptorResponse { - // Serialized FileDescriptorProto messages. We avoid taking a dependency on - // descriptor.proto, which uses proto2 only features, by making them opaque - // bytes instead. - repeated bytes file_descriptor_proto = 1; -} - -// A list of extension numbers sent by the server answering -// all_extension_numbers_of_type request. -message ExtensionNumberResponse { - // Full name of the base type, including the package name. The format - // is . - string base_type_name = 1; - repeated int32 extension_number = 2; -} - -// A list of ServiceResponse sent by the server answering list_services request. -message ListServiceResponse { - // The information of each service may be expanded in the future, so we use - // ServiceResponse message to encapsulate it. - repeated ServiceResponse service = 1; -} - -// The information of a single service used by ListServiceResponse to answer -// list_services request. -message ServiceResponse { - // Full name of a registered service, including its package name. The format - // is . - string name = 1; -} - -// The error code and error message sent by the server when an error occurs. -message ErrorResponse { - // This field uses the error codes defined in grpc::StatusCode. - int32 error_code = 1; - string error_message = 2; -} diff --git a/dev/Protos/upstream/grpc/testing/benchmark_service.proto b/dev/Protos/upstream/grpc/testing/benchmark_service.proto deleted file mode 100644 index 5209bd6ef..000000000 --- a/dev/Protos/upstream/grpc/testing/benchmark_service.proto +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. -syntax = "proto3"; - -import "grpc/testing/messages.proto"; - -package grpc.testing; - -option java_multiple_files = true; -option java_package = "io.grpc.testing"; -option java_outer_classname = "BenchmarkServiceProto"; - -service BenchmarkService { - // One request followed by one response. - // The server returns the client payload as-is. - rpc UnaryCall(SimpleRequest) returns (SimpleResponse); - - // Repeated sequence of one request followed by one response. - // Should be called streaming ping-pong - // The server returns the client payload as-is on each response - rpc StreamingCall(stream SimpleRequest) returns (stream SimpleResponse); - - // Single-sided unbounded streaming from client to server - // The server returns the client payload as-is once the client does WritesDone - rpc StreamingFromClient(stream SimpleRequest) returns (SimpleResponse); - - // Single-sided unbounded streaming from server to client - // The server repeatedly returns the client payload as-is - rpc StreamingFromServer(SimpleRequest) returns (stream SimpleResponse); - - // Two-sided unbounded streaming between server to client - // Both sides send the content of their own choice to the other - rpc StreamingBothWays(stream SimpleRequest) returns (stream SimpleResponse); -} diff --git a/dev/Protos/upstream/grpc/testing/control.proto b/dev/Protos/upstream/grpc/testing/control.proto deleted file mode 100644 index e309e5f9c..000000000 --- a/dev/Protos/upstream/grpc/testing/control.proto +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -import "grpc/testing/payloads.proto"; -import "grpc/testing/stats.proto"; -import "google/protobuf/timestamp.proto"; - -package grpc.testing; - -option java_multiple_files = true; -option java_package = "io.grpc.testing"; -option java_outer_classname = "ControlProto"; - -enum ClientType { - // Many languages support a basic distinction between using - // sync or async client, and this allows the specification - SYNC_CLIENT = 0; - ASYNC_CLIENT = 1; - OTHER_CLIENT = 2; // used for some language-specific variants - CALLBACK_CLIENT = 3; -} - -enum ServerType { - SYNC_SERVER = 0; - ASYNC_SERVER = 1; - ASYNC_GENERIC_SERVER = 2; - OTHER_SERVER = 3; // used for some language-specific variants - CALLBACK_SERVER = 4; -} - -enum RpcType { - UNARY = 0; - STREAMING = 1; - STREAMING_FROM_CLIENT = 2; - STREAMING_FROM_SERVER = 3; - STREAMING_BOTH_WAYS = 4; -} - -// Parameters of poisson process distribution, which is a good representation -// of activity coming in from independent identical stationary sources. -message PoissonParams { - // The rate of arrivals (a.k.a. lambda parameter of the exp distribution). - double offered_load = 1; -} - -// Once an RPC finishes, immediately start a new one. -// No configuration parameters needed. -message ClosedLoopParams {} - -message LoadParams { - oneof load { - ClosedLoopParams closed_loop = 1; - PoissonParams poisson = 2; - }; -} - -// presence of SecurityParams implies use of TLS -message SecurityParams { - bool use_test_ca = 1; - string server_host_override = 2; - string cred_type = 3; -} - -message ChannelArg { - string name = 1; - oneof value { - string str_value = 2; - int32 int_value = 3; - } -} - -message ClientConfig { - // List of targets to connect to. At least one target needs to be specified. - repeated string server_targets = 1; - ClientType client_type = 2; - SecurityParams security_params = 3; - // How many concurrent RPCs to start for each channel. - // For synchronous client, use a separate thread for each outstanding RPC. - int32 outstanding_rpcs_per_channel = 4; - // Number of independent client channels to create. - // i-th channel will connect to server_target[i % server_targets.size()] - int32 client_channels = 5; - // Only for async client. Number of threads to use to start/manage RPCs. - int32 async_client_threads = 7; - RpcType rpc_type = 8; - // The requested load for the entire client (aggregated over all the threads). - LoadParams load_params = 10; - PayloadConfig payload_config = 11; - HistogramParams histogram_params = 12; - - // Specify the cores we should run the client on, if desired - repeated int32 core_list = 13; - int32 core_limit = 14; - - // If we use an OTHER_CLIENT client_type, this string gives more detail - string other_client_api = 15; - - repeated ChannelArg channel_args = 16; - - // Number of threads that share each completion queue - int32 threads_per_cq = 17; - - // Number of messages on a stream before it gets finished/restarted - int32 messages_per_stream = 18; - - // Use coalescing API when possible. - bool use_coalesce_api = 19; - - // If 0, disabled. Else, specifies the period between gathering latency - // medians in milliseconds. - int32 median_latency_collection_interval_millis = 20; - - // Number of client processes. 0 indicates no restriction. - int32 client_processes = 21; -} - -message ClientStatus { ClientStats stats = 1; } - -// Request current stats -message Mark { - // if true, the stats will be reset after taking their snapshot. - bool reset = 1; -} - -message ClientArgs { - oneof argtype { - ClientConfig setup = 1; - Mark mark = 2; - } -} - -message ServerConfig { - ServerType server_type = 1; - SecurityParams security_params = 2; - // Port on which to listen. Zero means pick unused port. - int32 port = 4; - // Only for async server. Number of threads used to serve the requests. - int32 async_server_threads = 7; - // Specify the number of cores to limit server to, if desired - int32 core_limit = 8; - // payload config, used in generic server. - // Note this must NOT be used in proto (non-generic) servers. For proto servers, - // 'response sizes' must be configured from the 'response_size' field of the - // 'SimpleRequest' objects in RPC requests. - PayloadConfig payload_config = 9; - - // Specify the cores we should run the server on, if desired - repeated int32 core_list = 10; - - // If we use an OTHER_SERVER client_type, this string gives more detail - string other_server_api = 11; - - // Number of threads that share each completion queue - int32 threads_per_cq = 12; - - // c++-only options (for now) -------------------------------- - - // Buffer pool size (no buffer pool specified if unset) - int32 resource_quota_size = 1001; - repeated ChannelArg channel_args = 1002; - - // Number of server processes. 0 indicates no restriction. - int32 server_processes = 21; -} - -message ServerArgs { - oneof argtype { - ServerConfig setup = 1; - Mark mark = 2; - } -} - -message ServerStatus { - ServerStats stats = 1; - // the port bound by the server - int32 port = 2; - // Number of cores available to the server - int32 cores = 3; -} - -message CoreRequest { -} - -message CoreResponse { - // Number of cores available on the server - int32 cores = 1; -} - -message Void { -} - -// A single performance scenario: input to qps_json_driver -message Scenario { - // Human readable name for this scenario - string name = 1; - // Client configuration - ClientConfig client_config = 2; - // Number of clients to start for the test - int32 num_clients = 3; - // Server configuration - ServerConfig server_config = 4; - // Number of servers to start for the test - int32 num_servers = 5; - // Warmup period, in seconds - int32 warmup_seconds = 6; - // Benchmark time, in seconds - int32 benchmark_seconds = 7; - // Number of workers to spawn locally (usually zero) - int32 spawn_local_worker_count = 8; -} - -// A set of scenarios to be run with qps_json_driver -message Scenarios { - repeated Scenario scenarios = 1; -} - -// Basic summary that can be computed from ClientStats and ServerStats -// once the scenario has finished. -message ScenarioResultSummary -{ - // Total number of operations per second over all clients. What is counted as 1 'operation' depends on the benchmark scenarios: - // For unary benchmarks, an operation is processing of a single unary RPC. - // For streaming benchmarks, an operation is processing of a single ping pong of request and response. - double qps = 1; - // QPS per server core. - double qps_per_server_core = 2; - // The total server cpu load based on system time across all server processes, expressed as percentage of a single cpu core. - // For example, 85 implies 85% of a cpu core, 125 implies 125% of a cpu core. Since we are accumulating the cpu load across all the server - // processes, the value could > 100 when there are multiple servers or a single server using multiple threads and cores. - // Same explanation for the total client cpu load below. - double server_system_time = 3; - // The total server cpu load based on user time across all server processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - double server_user_time = 4; - // The total client cpu load based on system time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - double client_system_time = 5; - // The total client cpu load based on user time across all client processes, expressed as percentage of a single cpu core. (85 => 85%, 125 => 125%) - double client_user_time = 6; - - // X% latency percentiles (in nanoseconds) - double latency_50 = 7; - double latency_90 = 8; - double latency_95 = 9; - double latency_99 = 10; - double latency_999 = 11; - - // server cpu usage percentage - double server_cpu_usage = 12; - - // Number of requests that succeeded/failed - double successful_requests_per_second = 13; - double failed_requests_per_second = 14; - - // Number of polls called inside completion queue per request - double client_polls_per_request = 15; - double server_polls_per_request = 16; - - // Queries per CPU-sec over all servers or clients - double server_queries_per_cpu_sec = 17; - double client_queries_per_cpu_sec = 18; - - - // Start and end time for the test scenario - google.protobuf.Timestamp start_time = 19; - google.protobuf.Timestamp end_time =20; -} - -// Results of a single benchmark scenario. -message ScenarioResult { - // Inputs used to run the scenario. - Scenario scenario = 1; - // Histograms from all clients merged into one histogram. - HistogramData latencies = 2; - // Client stats for each client - repeated ClientStats client_stats = 3; - // Server stats for each server - repeated ServerStats server_stats = 4; - // Number of cores available to each server - repeated int32 server_cores = 5; - // An after-the-fact computed summary - ScenarioResultSummary summary = 6; - // Information on success or failure of each worker - repeated bool client_success = 7; - repeated bool server_success = 8; - // Number of failed requests (one row per status code seen) - repeated RequestResultCount request_results = 9; -} diff --git a/dev/Protos/upstream/grpc/testing/messages.proto b/dev/Protos/upstream/grpc/testing/messages.proto deleted file mode 100644 index 99e34dcc8..000000000 --- a/dev/Protos/upstream/grpc/testing/messages.proto +++ /dev/null @@ -1,347 +0,0 @@ -// Copyright 2015-2016 gRPC authors. -// -// 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 definitions to be used by integration test service definitions. - -syntax = "proto3"; - -package grpc.testing; - -option java_package = "io.grpc.testing.integration"; - -// TODO(dgq): Go back to using well-known types once -// https://github.com/grpc/grpc/issues/6980 has been fixed. -// import "google/protobuf/wrappers.proto"; -message BoolValue { - // The bool value. - bool value = 1; -} - -// The type of payload that should be returned. -enum PayloadType { - // Compressable text format. - COMPRESSABLE = 0; -} - -// A block of data, to simply increase gRPC message size. -message Payload { - // The type of data in body. - PayloadType type = 1; - // Primary contents of payload. - bytes body = 2; -} - -// A protobuf representation for grpc status. This is used by test -// clients to specify a status that the server should attempt to return. -message EchoStatus { - int32 code = 1; - string message = 2; -} - -// The type of route that a client took to reach a server w.r.t. gRPCLB. -// The server must fill in "fallback" if it detects that the RPC reached -// the server via the "gRPCLB fallback" path, and "backend" if it detects -// that the RPC reached the server via "gRPCLB backend" path (i.e. if it got -// the address of this server from the gRPCLB server BalanceLoad RPC). Exactly -// how this detection is done is context and server dependent. -enum GrpclbRouteType { - // Server didn't detect the route that a client took to reach it. - GRPCLB_ROUTE_TYPE_UNKNOWN = 0; - // Indicates that a client reached a server via gRPCLB fallback. - GRPCLB_ROUTE_TYPE_FALLBACK = 1; - // Indicates that a client reached a server as a gRPCLB-given backend. - GRPCLB_ROUTE_TYPE_BACKEND = 2; -} - -// Unary request. -message SimpleRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, server randomly chooses one from other formats. - PayloadType response_type = 1; - - // Desired payload size in the response from the server. - int32 response_size = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether SimpleResponse should include username. - bool fill_username = 4; - - // Whether SimpleResponse should include OAuth scope. - bool fill_oauth_scope = 5; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue response_compressed = 6; - - // Whether server should return a given status - EchoStatus response_status = 7; - - // Whether the server should expect this request to be compressed. - BoolValue expect_compressed = 8; - - // Whether SimpleResponse should include server_id. - bool fill_server_id = 9; - - // Whether SimpleResponse should include grpclb_route_type. - bool fill_grpclb_route_type = 10; - - // If set the server should record this metrics report data for the current RPC. - TestOrcaReport orca_per_query_report = 11; -} - -// Unary response, as configured by the request. -message SimpleResponse { - // Payload to increase message size. - Payload payload = 1; - // The user the request came from, for verifying authentication was - // successful when the client expected it. - string username = 2; - // OAuth scope. - string oauth_scope = 3; - - // Server ID. This must be unique among different server instances, - // but the same across all RPC's made to a particular server instance. - string server_id = 4; - // gRPCLB Path. - GrpclbRouteType grpclb_route_type = 5; - - // Server hostname. - string hostname = 6; -} - -// Client-streaming request. -message StreamingInputCallRequest { - // Optional input payload sent along with the request. - Payload payload = 1; - - // Whether the server should expect this request to be compressed. This field - // is "nullable" in order to interoperate seamlessly with servers not able to - // implement the full compression tests by introspecting the call to verify - // the request's compression status. - BoolValue expect_compressed = 2; - - // Not expecting any payload from the response. -} - -// Client-streaming response. -message StreamingInputCallResponse { - // Aggregated size of payloads received from the client. - int32 aggregated_payload_size = 1; -} - -// Configuration for a particular response. -message ResponseParameters { - // Desired payload sizes in responses from the server. - int32 size = 1; - - // Desired interval between consecutive responses in the response stream in - // microseconds. - int32 interval_us = 2; - - // Whether to request the server to compress the response. This field is - // "nullable" in order to interoperate seamlessly with clients not able to - // implement the full compression tests by introspecting the call to verify - // the response's compression status. - BoolValue compressed = 3; -} - -// Server-streaming request. -message StreamingOutputCallRequest { - // Desired payload type in the response from the server. - // If response_type is RANDOM, the payload from each response in the stream - // might be of different types. This is to simulate a mixed type of payload - // stream. - PayloadType response_type = 1; - - // Configuration for each expected response message. - repeated ResponseParameters response_parameters = 2; - - // Optional input payload sent along with the request. - Payload payload = 3; - - // Whether server should return a given status - EchoStatus response_status = 7; - - // If set the server should update this metrics report data at the OOB server. - TestOrcaReport orca_oob_report = 8; -} - -// Server-streaming response, as configured by the request and parameters. -message StreamingOutputCallResponse { - // Payload to increase response size. - Payload payload = 1; -} - -// For reconnect interop test only. -// Client tells server what reconnection parameters it used. -message ReconnectParams { - int32 max_reconnect_backoff_ms = 1; -} - -// For reconnect interop test only. -// Server tells client whether its reconnects are following the spec and the -// reconnect backoffs it saw. -message ReconnectInfo { - bool passed = 1; - repeated int32 backoff_ms = 2; -} - -message LoadBalancerStatsRequest { - // Request stats for the next num_rpcs sent by client. - int32 num_rpcs = 1; - // If num_rpcs have not completed within timeout_sec, return partial results. - int32 timeout_sec = 2; - // Response header + trailer metadata entries we want the values of. - // Matching of the keys is case-insensitive as per rfc7540#section-8.1.2 - // * (asterisk) is a special value that will return all metadata entries - repeated string metadata_keys = 3; -} - -message LoadBalancerStatsResponse { - enum MetadataType { - UNKNOWN = 0; - INITIAL = 1; - TRAILING = 2; - } - message MetadataEntry { - // Key, exactly as received from the server. Case may be different from what - // was requested in the LoadBalancerStatsRequest) - string key = 1; - // Value, exactly as received from the server. - string value = 2; - // Metadata type - MetadataType type = 3; - } - message RpcMetadata { - // metadata values for each rpc for the keys specified in - // LoadBalancerStatsRequest.metadata_keys. - repeated MetadataEntry metadata = 1; - } - message MetadataByPeer { - // List of RpcMetadata in for each RPC with a given peer - repeated RpcMetadata rpc_metadata = 1; - } - message RpcsByPeer { - // The number of completed RPCs for each peer. - map rpcs_by_peer = 1; - } - // The number of completed RPCs for each peer. - map rpcs_by_peer = 1; - // The number of RPCs that failed to record a remote peer. - int32 num_failures = 2; - map rpcs_by_method = 3; - // All the metadata of all RPCs for each peer. - map metadatas_by_peer = 4; -} - -// Request for retrieving a test client's accumulated stats. -message LoadBalancerAccumulatedStatsRequest {} - -// Accumulated stats for RPCs sent by a test client. -message LoadBalancerAccumulatedStatsResponse { - // The total number of RPCs have ever issued for each type. - // Deprecated: use stats_per_method.rpcs_started instead. - map num_rpcs_started_by_method = 1 [deprecated = true]; - // The total number of RPCs have ever completed successfully for each type. - // Deprecated: use stats_per_method.result instead. - map num_rpcs_succeeded_by_method = 2 [deprecated = true]; - // The total number of RPCs have ever failed for each type. - // Deprecated: use stats_per_method.result instead. - map num_rpcs_failed_by_method = 3 [deprecated = true]; - - message MethodStats { - // The number of RPCs that were started for this method. - int32 rpcs_started = 1; - - // The number of RPCs that completed with each status for this method. The - // key is the integral value of a google.rpc.Code; the value is the count. - map result = 2; - } - - // Per-method RPC statistics. The key is the RpcType in string form; e.g. - // 'EMPTY_CALL' or 'UNARY_CALL' - map stats_per_method = 4; -} - -// Configurations for a test client. -message ClientConfigureRequest { - // Type of RPCs to send. - enum RpcType { - EMPTY_CALL = 0; - UNARY_CALL = 1; - } - - // Metadata to be attached for the given type of RPCs. - message Metadata { - RpcType type = 1; - string key = 2; - string value = 3; - } - - // The types of RPCs the client sends. - repeated RpcType types = 1; - // The collection of custom metadata to be attached to RPCs sent by the client. - repeated Metadata metadata = 2; - // The deadline to use, in seconds, for all RPCs. If unset or zero, the - // client will use the default from the command-line. - int32 timeout_sec = 3; -} - -// Response for updating a test client's configuration. -message ClientConfigureResponse {} - -message MemorySize { - int64 rss = 1; -} - -// Metrics data the server will update and send to the client. It mirrors orca load report -// https://github.com/cncf/xds/blob/eded343319d09f30032952beda9840bbd3dcf7ac/xds/data/orca/v3/orca_load_report.proto#L15, -// but avoids orca dependency. Used by both per-query and out-of-band reporting tests. -message TestOrcaReport { - double cpu_utilization = 1; - double memory_utilization = 2; - map request_cost = 3; - map utilization = 4; -} - -// Status that will be return to callers of the Hook method -message SetReturnStatusRequest { - int32 grpc_code_to_return = 1; - string grpc_status_description = 2; -} - -message HookRequest { - enum HookRequestCommand { - // Default value - UNSPECIFIED = 0; - // Start the HTTP endpoint - START = 1; - // Stop - STOP = 2; - // Return from HTTP GET/POST - RETURN = 3; - } - HookRequestCommand command = 1; - int32 grpc_code_to_return = 2; - string grpc_status_description = 3; - // Server port to listen to - int32 server_port = 4; -} - -message HookResponse { -} diff --git a/dev/Protos/upstream/grpc/testing/payloads.proto b/dev/Protos/upstream/grpc/testing/payloads.proto deleted file mode 100644 index 8cbc9db6c..000000000 --- a/dev/Protos/upstream/grpc/testing/payloads.proto +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -option java_multiple_files = true; -option java_package = "io.grpc.testing"; -option java_outer_classname = "PayloadsProto"; - -message ByteBufferParams { - int32 req_size = 1; - int32 resp_size = 2; -} - -message SimpleProtoParams { - int32 req_size = 1; - int32 resp_size = 2; -} - -message ComplexProtoParams { - // TODO (vpai): Fill this in once the details of complex, representative - // protos are decided -} - -message PayloadConfig { - oneof payload { - ByteBufferParams bytebuf_params = 1; - SimpleProtoParams simple_params = 2; - ComplexProtoParams complex_params = 3; - } -} diff --git a/dev/Protos/upstream/grpc/testing/stats.proto b/dev/Protos/upstream/grpc/testing/stats.proto deleted file mode 100644 index 1f0fae4e5..000000000 --- a/dev/Protos/upstream/grpc/testing/stats.proto +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -syntax = "proto3"; - -package grpc.testing; - -import "grpc/core/stats.proto"; - -option java_multiple_files = true; -option java_package = "io.grpc.testing"; -option java_outer_classname = "StatsProto"; - -message ServerStats { - // wall clock time change in seconds since last reset - double time_elapsed = 1; - - // change in user time (in seconds) used by the server since last reset - double time_user = 2; - - // change in server time (in seconds) used by the server process and all - // threads since last reset - double time_system = 3; - - // change in total cpu time of the server (data from proc/stat) - uint64 total_cpu_time = 4; - - // change in idle time of the server (data from proc/stat) - uint64 idle_cpu_time = 5; - - // Number of polls called inside completion queue - uint64 cq_poll_count = 6; - - // Core library stats - grpc.core.Stats core_stats = 7; -} - -// Histogram params based on grpc/support/histogram.c -message HistogramParams { - double resolution = 1; // first bucket is [0, 1 + resolution) - double max_possible = 2; // use enough buckets to allow this value -} - -// Histogram data based on grpc/support/histogram.c -message HistogramData { - repeated uint32 bucket = 1; - double min_seen = 2; - double max_seen = 3; - double sum = 4; - double sum_of_squares = 5; - double count = 6; -} - -message RequestResultCount { - int32 status_code = 1; - int64 count = 2; -} - -message ClientStats { - // Latency histogram. Data points are in nanoseconds. - HistogramData latencies = 1; - - // See ServerStats for details. - double time_elapsed = 2; - double time_user = 3; - double time_system = 4; - - // Number of failed requests (one row per status code seen) - repeated RequestResultCount request_results = 5; - - // Number of polls called inside completion queue - uint64 cq_poll_count = 6; - - // Core library stats - grpc.core.Stats core_stats = 7; -} diff --git a/dev/Protos/upstream/grpc/testing/worker_service.proto b/dev/Protos/upstream/grpc/testing/worker_service.proto deleted file mode 100644 index ff3aa2931..000000000 --- a/dev/Protos/upstream/grpc/testing/worker_service.proto +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. - -// An integration test service that covers all the method signature permutations -// of unary/streaming requests/responses. -syntax = "proto3"; - -import "grpc/testing/control.proto"; - -package grpc.testing; - -option java_multiple_files = true; -option java_package = "io.grpc.testing"; -option java_outer_classname = "WorkerServiceProto"; - -service WorkerService { - // Start server with specified workload. - // First request sent specifies the ServerConfig followed by ServerStatus - // response. After that, a "Mark" can be sent anytime to request the latest - // stats. Closing the stream will initiate shutdown of the test server - // and once the shutdown has finished, the OK status is sent to terminate - // this RPC. - rpc RunServer(stream ServerArgs) returns (stream ServerStatus); - - // Start client with specified workload. - // First request sent specifies the ClientConfig followed by ClientStatus - // response. After that, a "Mark" can be sent anytime to request the latest - // stats. Closing the stream will initiate shutdown of the test client - // and once the shutdown has finished, the OK status is sent to terminate - // this RPC. - rpc RunClient(stream ClientArgs) returns (stream ClientStatus); - - // Just return the core count - unary call - rpc CoreCount(CoreRequest) returns (CoreResponse); - - // Quit this worker - rpc QuitWorker(Void) returns (Void); -} diff --git a/dev/build-examples.sh b/dev/build-examples.sh new file mode 100755 index 000000000..e44a8f5c6 --- /dev/null +++ b/dev/build-examples.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# 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. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +examples="$here/../Examples" + +for dir in "$examples"/*/ ; do + if [[ -f "$dir/Package.swift" ]]; then + example=$(basename "$dir") + log "Building '$example' example" + + if ! build_output=$(swift build --package-path "$dir" 2>&1); then + # Only print the build output on failure. + echo "$build_output" + fatal "Build failed for '$example'" + else + log "Build succeeded for '$example'" + fi + fi +done diff --git a/dev/check-generated-code.sh b/dev/check-generated-code.sh index 8e1ef7ce7..67bc71b8e 100755 --- a/dev/check-generated-code.sh +++ b/dev/check-generated-code.sh @@ -24,7 +24,7 @@ here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Re-generate everything. log "Regenerating protos..." -"$here"/Protos/generate.sh +"$here"/protos/generate.sh # Check for changes. GIT_PAGER='' git diff --exit-code '*.swift' diff --git a/dev/format.sh b/dev/format.sh index 4554e2f19..ac2d7f61a 100755 --- a/dev/format.sh +++ b/dev/format.sh @@ -60,7 +60,7 @@ if "$lint"; then --parallel --recursive --strict \ "${repo}/Sources" \ "${repo}/Tests" \ - "${repo}/Plugins" \ + "${repo}/Examples" \ "${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? @@ -80,6 +80,7 @@ elif "$format"; then --parallel --recursive --in-place \ "${repo}/Sources" \ "${repo}/Tests" \ + "${repo}/Examples" \ "${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \ && SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? diff --git a/dev/license-check.sh b/dev/license-check.sh index 49dfb4f44..d540293aa 100755 --- a/dev/license-check.sh +++ b/dev/license-check.sh @@ -106,7 +106,7 @@ check_copyright_headers() { ! -name '*.pb.swift' \ ! -name '*.grpc.swift' \ ! -path './Sources/GRPCCore/Documentation.docc/*' \ - ! -path './.build/*') + ! -path '.*/.build/*') } errors=0 diff --git a/dev/Protos/README.md b/dev/protos/README.md similarity index 100% rename from dev/Protos/README.md rename to dev/protos/README.md diff --git a/dev/Protos/examples/echo/echo.proto b/dev/protos/examples/echo/echo.proto similarity index 99% rename from dev/Protos/examples/echo/echo.proto rename to dev/protos/examples/echo/echo.proto index 7de1534ac..cfba875db 100644 --- a/dev/Protos/examples/echo/echo.proto +++ b/dev/protos/examples/echo/echo.proto @@ -38,4 +38,4 @@ message EchoRequest { message EchoResponse { // The text of an echo response. string text = 1; -} +} \ No newline at end of file diff --git a/dev/Protos/examples/route_guide/route_guide.proto b/dev/protos/examples/route_guide/route_guide.proto similarity index 100% rename from dev/Protos/examples/route_guide/route_guide.proto rename to dev/protos/examples/route_guide/route_guide.proto diff --git a/dev/Protos/fetch.sh b/dev/protos/fetch.sh similarity index 91% rename from dev/Protos/fetch.sh rename to dev/protos/fetch.sh index e68ce6531..01bf4d780 100755 --- a/dev/Protos/fetch.sh +++ b/dev/protos/fetch.sh @@ -18,6 +18,7 @@ set -eu here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" upstream="$here/upstream" +root="$here/../.." # Create a temporary directory for the repo checkouts. checkouts="$(mktemp -d)" @@ -33,11 +34,12 @@ rm -rf "$upstream" # rather than source repository name. mkdir -p "$upstream/google" mkdir -p "$upstream/grpc/core" +mkdir -p "$upstream/grpc/examples" # Copy over the grpc-proto protos. cp -rp "$checkouts/grpc-proto/grpc/service_config" "$upstream/grpc/service_config" cp -rp "$checkouts/grpc-proto/grpc/lookup" "$upstream/grpc/lookup" -cp -rp "$checkouts/grpc-proto/grpc/examples" "$upstream/grpc/examples" +cp "$checkouts/grpc-proto/grpc/examples/helloworld.proto" "$upstream/grpc/examples/helloworld.proto" # Copy over the googleapis protos. mkdir -p "$upstream/google/rpc" diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh new file mode 100755 index 000000000..666e87c49 --- /dev/null +++ b/dev/protos/generate.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# +# 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. + +set -eu + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +root="$here/../.." +protoc=$(which protoc) + +# Checkout and build the plugins. +build_dir=$(mktemp -d) +git clone https://github.com/grpc/grpc-swift-protobuf --depth 1 "$build_dir" +swift build --package-path "$build_dir" --product protoc-gen-swift +swift build --package-path "$build_dir" --product protoc-gen-grpc-swift + +# Grab the plugin paths. +bin_path=$(swift build --package-path "$build_dir" --show-bin-path) +protoc_gen_swift="$bin_path/protoc-gen-swift" +protoc_gen_grpc_swift="$bin_path/protoc-gen-grpc-swift" + +# Generates gRPC by invoking protoc with the gRPC Swift plugin. +# Parameters: +# - $1: .proto file +# - $2: proto path +# - $3: output path +# - $4 onwards: options to forward to the plugin +function generate_grpc { + local proto=$1 + local args=("--plugin=$protoc_gen_grpc_swift" "--proto_path=${2}" "--grpc-swift_out=${3}") + + for option in "${@:4}"; do + args+=("--grpc-swift_opt=$option") + done + + invoke_protoc "${args[@]}" "$proto" +} + +# Generates messages by invoking protoc with the Swift plugin. +# Parameters: +# - $1: .proto file +# - $2: proto path +# - $3: output path +# - $4 onwards: options to forward to the plugin +function generate_message { + local proto=$1 + local args=("--plugin=$protoc_gen_swift" "--proto_path=$2" "--swift_out=$3") + + for option in "${@:4}"; do + args+=("--swift_opt=$option") + done + + invoke_protoc "${args[@]}" "$proto" +} + +function invoke_protoc { + # Setting -x when running the script produces a lot of output, instead boil + # just echo out the protoc invocations. + echo "$protoc" "$@" + "$protoc" "$@" +} + +#- EXAMPLES ------------------------------------------------------------------- + +function generate_echo_example { + local proto="$here/examples/echo/echo.proto" + local output="$root/Examples/echo/Sources/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" +} + +function generate_helloworld_example { + local proto="$here/upstream/grpc/examples/helloworld.proto" + local output="$root/Examples/hello-world/Sources/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" +} + +function generate_routeguide_example { + local proto="$here/examples/route_guide/route_guide.proto" + local output="$root/Examples/route-guide/Sources/Generated" + + generate_message "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" + generate_grpc "$proto" "$(dirname "$proto")" "$output" "Visibility=Internal" +} + +#- TESTS ---------------------------------------------------------------------- + +function generate_service_config_for_tests { + local protos=( + "$here/upstream/grpc/service_config/service_config.proto" + "$here/upstream/grpc/lookup/v1/rls.proto" + "$here/upstream/grpc/lookup/v1/rls_config.proto" + "$here/upstream/google/rpc/code.proto" + ) + local output="$root/Tests/GRPCCoreTests/Configuration/Generated" + + for proto in "${protos[@]}"; do + generate_message "$proto" "$here/upstream" "$output" "Visibility=Internal" "FileNaming=DropPath" + done +} + +#------------------------------------------------------------------------------ + +# Generate examples +generate_echo_example +generate_helloworld_example +generate_routeguide_example + +# Tests +generate_service_config_for_tests diff --git a/dev/Protos/upstream/google/rpc/code.proto b/dev/protos/upstream/google/rpc/code.proto similarity index 100% rename from dev/Protos/upstream/google/rpc/code.proto rename to dev/protos/upstream/google/rpc/code.proto diff --git a/dev/Protos/upstream/grpc/core/stats.proto b/dev/protos/upstream/grpc/examples/helloworld.proto similarity index 53% rename from dev/Protos/upstream/grpc/core/stats.proto rename to dev/protos/upstream/grpc/examples/helloworld.proto index ac181b043..2be480c2f 100644 --- a/dev/Protos/upstream/grpc/core/stats.proto +++ b/dev/protos/upstream/grpc/examples/helloworld.proto @@ -1,4 +1,4 @@ -// Copyright 2017 gRPC authors. +// Copyright 2015 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,28 +11,27 @@ // 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. - syntax = "proto3"; -package grpc.core; +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; -message Bucket { - double start = 1; - uint64 count = 2; -} +package helloworld; -message Histogram { - repeated Bucket buckets = 1; +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} } -message Metric { +// The request message containing the user's name. +message HelloRequest { string name = 1; - oneof value { - uint64 count = 10; - Histogram histogram = 11; - } } -message Stats { - repeated Metric metrics = 1; +// The response message containing the greetings +message HelloReply { + string message = 1; } diff --git a/dev/Protos/upstream/grpc/lookup/v1/rls.proto b/dev/protos/upstream/grpc/lookup/v1/rls.proto similarity index 100% rename from dev/Protos/upstream/grpc/lookup/v1/rls.proto rename to dev/protos/upstream/grpc/lookup/v1/rls.proto diff --git a/dev/Protos/upstream/grpc/lookup/v1/rls_config.proto b/dev/protos/upstream/grpc/lookup/v1/rls_config.proto similarity index 99% rename from dev/Protos/upstream/grpc/lookup/v1/rls_config.proto rename to dev/protos/upstream/grpc/lookup/v1/rls_config.proto index 9d2b6c54c..9762be752 100644 --- a/dev/Protos/upstream/grpc/lookup/v1/rls_config.proto +++ b/dev/protos/upstream/grpc/lookup/v1/rls_config.proto @@ -159,6 +159,9 @@ message HttpKeyBuilder { // for example if you are suppressing a lot of information from the URL, but // need to separately cache and request URLs with that content. map constant_keys = 5; + + // If specified, the HTTP method/verb will be extracted under this key name. + string method = 6; } message RouteLookupConfig { diff --git a/dev/Protos/upstream/grpc/service_config/service_config.proto b/dev/protos/upstream/grpc/service_config/service_config.proto similarity index 100% rename from dev/Protos/upstream/grpc/service_config/service_config.proto rename to dev/protos/upstream/grpc/service_config/service_config.proto From c8d550040c911d4331bf6509aaa42b7fcba8b27d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 25 Sep 2024 10:14:49 +0100 Subject: [PATCH 473/580] Add an underscore between generated service name and component (#2072) Motivation: A 'bar' package defining a 'FooService' service will have a number of types generateed for it, for example, `Bar_FooServiceServiceProtocol`, or `Bar_FooServiceClient`. The protocol buffers style guide reccomends that services are suffixed with "Service" (e.g. "FooService", not "Foo") which results in some strange protocol names (like `Bar_FooServiceServiceProtocol`). There should be a clearer differentiation between the qualified service name ("bar.FooServce") and the type being generated. The code generator already replaces "." with "_" in fully qualified names, so "_" is a natural choice as a separator. Modifications: - Add an "_" between the generated fully qualified service name and the gRPC component being generated - Update tests Result: Better names in generated code. The `Bar_FooServiceServiceProtocol` example above becomes `Bar_FooService_ServiceProtocol`, while `Bar_FooServiceClient` becomes `Bar_FooService_Client`. --- Package.swift | 1 + .../Translator/ClientCodeTranslator.swift | 4 +- .../Translator/ServerCodeTranslator.swift | 4 +- .../Translator/TypealiasTranslator.swift | 8 +- ...lientCodeTranslatorSnippetBasedTests.swift | 32 ++--- ...uredSwiftTranslatorSnippetBasedTests.swift | 10 +- ...erverCodeTranslatorSnippetBasedTests.swift | 48 ++++---- ...TypealiasTranslatorSnippetBasedTests.swift | 112 +++++++++--------- 8 files changed, 110 insertions(+), 109 deletions(-) diff --git a/Package.swift b/Package.swift index 8e1b4c19d..e14774b1a 100644 --- a/Package.swift +++ b/Package.swift @@ -65,6 +65,7 @@ let targets: [Target] = [ name: "GRPCCoreTests", dependencies: [ .target(name: "GRPCCore"), + .target(name: "GRPCInProcessTransport"), .product(name: "SwiftProtobuf", package: "swift-protobuf") ], resources: [ diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 5ead61a67..29285c243 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -121,7 +121,7 @@ extension ClientCodeTranslator { let clientProtocol = Declaration.protocol( ProtocolDescription( accessModifier: self.accessModifier, - name: "\(service.namespacedGeneratedName)ClientProtocol", + name: "\(service.namespacedGeneratedName)_ClientProtocol", conformances: ["Sendable"], members: methods ) @@ -592,7 +592,7 @@ extension ClientCodeTranslator { .struct( StructDescription( accessModifier: self.accessModifier, - name: "\(service.namespacedGeneratedName)Client", + name: "\(service.namespacedGeneratedName)_Client", conformances: ["\(service.namespacedGeneratedName).ClientProtocol"], members: [clientProperty, initializer] + methods ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index c264eea30..d392bf5b0 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -481,8 +481,8 @@ extension ServerCodeTranslator { streaming: Bool ) -> String { if streaming { - return "\(service.namespacedGeneratedName)StreamingServiceProtocol" + return "\(service.namespacedGeneratedName)_StreamingServiceProtocol" } - return "\(service.namespacedGeneratedName)ServiceProtocol" + return "\(service.namespacedGeneratedName)_ServiceProtocol" } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index a6bcc55ae..5936dc6f5 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -256,12 +256,12 @@ extension TypealiasTranslator { let streamingServiceProtocolTypealias = Declaration.typealias( accessModifier: self.accessModifier, name: "StreamingServiceProtocol", - existingType: .member("\(service.namespacedGeneratedName)StreamingServiceProtocol") + existingType: .member("\(service.namespacedGeneratedName)_StreamingServiceProtocol") ) let serviceProtocolTypealias = Declaration.typealias( accessModifier: self.accessModifier, name: "ServiceProtocol", - existingType: .member("\(service.namespacedGeneratedName)ServiceProtocol") + existingType: .member("\(service.namespacedGeneratedName)_ServiceProtocol") ) return [ @@ -284,7 +284,7 @@ extension TypealiasTranslator { .typealias( accessModifier: self.accessModifier, name: "ClientProtocol", - existingType: .member("\(service.namespacedGeneratedName)ClientProtocol") + existingType: .member("\(service.namespacedGeneratedName)_ClientProtocol") ) ) } @@ -297,7 +297,7 @@ extension TypealiasTranslator { .typealias( accessModifier: self.accessModifier, name: "Client", - existingType: .member("\(service.namespacedGeneratedName)Client") + existingType: .member("\(service.namespacedGeneratedName)_Client") ) ) } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 88a713679..17b50ad0b 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -44,7 +44,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: GRPCCore.ClientRequest.Single, @@ -96,7 +96,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { + public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(wrapping client: GRPCCore.GRPCClient) { @@ -151,7 +151,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: GRPCCore.ClientRequest.Stream, @@ -203,7 +203,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { + public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(wrapping client: GRPCCore.GRPCClient) { @@ -258,7 +258,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: GRPCCore.ClientRequest.Single, @@ -306,7 +306,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { + public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(wrapping client: GRPCCore.GRPCClient) { @@ -359,7 +359,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAClientProtocol: Sendable { + public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: GRPCCore.ClientRequest.Stream, @@ -407,7 +407,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { + public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(wrapping client: GRPCCore.GRPCClient) { @@ -468,7 +468,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceAClientProtocol: Sendable { + package protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: GRPCCore.ClientRequest.Stream, @@ -561,7 +561,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { + package struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient package init(wrapping client: GRPCCore.GRPCClient) { @@ -634,7 +634,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol ServiceAClientProtocol: Sendable { + internal protocol ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( request: GRPCCore.ClientRequest.Single, @@ -686,7 +686,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal struct ServiceAClient: ServiceA.ClientProtocol { + internal struct ServiceA_Client: ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient internal init(wrapping client: GRPCCore.GRPCClient) { @@ -747,7 +747,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAClientProtocol: Sendable {} + public protocol NamespaceA_ServiceA_ClientProtocol: Sendable {} @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { } @@ -756,7 +756,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceAClient: NamespaceA_ServiceA.ClientProtocol { + public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient public init(wrapping client: GRPCCore.GRPCClient) { @@ -767,7 +767,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// /// Line 2 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol ServiceBClientProtocol: Sendable {} + public protocol ServiceB_ClientProtocol: Sendable {} @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceB.ClientProtocol { } @@ -778,7 +778,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// /// Line 2 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct ServiceBClient: ServiceB.ClientProtocol { + public struct ServiceB_Client: ServiceB.ClientProtocol { private let client: GRPCCore.GRPCClient public init(wrapping client: GRPCCore.GRPCClient) { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index c11dd3bda..4ea88262d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -222,9 +222,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol } extension GRPCCore.ServiceDescriptor { @@ -236,7 +236,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { /// Documentation for AService @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -247,9 +247,9 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { /// Documentation for AService @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} + public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index d4d2bc571..9125eea40 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -52,7 +52,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod func unary( request: GRPCCore.ServerRequest.Stream, @@ -79,14 +79,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { + public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod func unary( request: GRPCCore.ServerRequest.Single, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse.Single } - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func unary( @@ -136,7 +136,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + package protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming( request: GRPCCore.ServerRequest.Stream, @@ -163,14 +163,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { + package protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming( request: GRPCCore.ServerRequest.Stream, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse.Single } - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { package func inputStreaming( @@ -224,7 +224,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod func outputStreaming( request: GRPCCore.ServerRequest.Stream, @@ -251,14 +251,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { + public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod func outputStreaming( request: GRPCCore.ServerRequest.Single, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse.Stream } - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func outputStreaming( @@ -312,7 +312,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + package protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming( request: GRPCCore.ServerRequest.Stream, @@ -339,14 +339,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { + package protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming( request: GRPCCore.ServerRequest.Stream, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse.Stream } - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } @@ -402,7 +402,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + internal protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming( request: GRPCCore.ServerRequest.Stream, @@ -446,7 +446,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { + internal protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming( request: GRPCCore.ServerRequest.Stream, @@ -459,7 +459,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse.Stream } - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { internal func inputStreaming( @@ -516,7 +516,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService { + internal protocol ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA func methodA( request: GRPCCore.ServerRequest.Stream, @@ -543,14 +543,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol ServiceAServiceProtocol: ServiceA.StreamingServiceProtocol { + internal protocol ServiceA_ServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA func methodA( request: GRPCCore.ServerRequest.Single, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse.Single } - /// Partial conformance to `ServiceAStreamingServiceProtocol`. + /// Partial conformance to `ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ServiceProtocol { internal func methodA( @@ -598,7 +598,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { @@ -607,14 +607,14 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} - /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} + /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } /// Documentation for ServiceB @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceBStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + public protocol NamespaceA_ServiceB_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceB.StreamingServiceProtocol { @@ -623,8 +623,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for ServiceB @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceBServiceProtocol: NamespaceA_ServiceB.StreamingServiceProtocol {} - /// Partial conformance to `NamespaceA_ServiceBStreamingServiceProtocol`. + public protocol NamespaceA_ServiceB_ServiceProtocol: NamespaceA_ServiceB.StreamingServiceProtocol {} + /// Partial conformance to `NamespaceA_ServiceB_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceB.ServiceProtocol { } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index af0a9017a..608ca4b27 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -62,13 +62,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_ServiceAClient + public typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( @@ -106,13 +106,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_ServiceAClient + public typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( @@ -150,9 +150,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol } extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( @@ -190,9 +190,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_ServiceAClient + public typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( @@ -280,13 +280,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = ServiceAStreamingServiceProtocol + public typealias StreamingServiceProtocol = ServiceA_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = ServiceAServiceProtocol + public typealias ServiceProtocol = ServiceA_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = ServiceAClientProtocol + public typealias ClientProtocol = ServiceA_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = ServiceAClient + public typealias Client = ServiceA_Client } extension GRPCCore.ServiceDescriptor { public static let ServiceA = Self( @@ -359,13 +359,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_ServiceAClient + public typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { public static let namespaceA_ServiceA = Self( @@ -403,13 +403,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = NamespaceA_ServiceAStreamingServiceProtocol + package typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + package typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = NamespaceA_ServiceAClientProtocol + package typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = NamespaceA_ServiceAClient + package typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { package static let namespaceA_ServiceA = Self( @@ -459,13 +459,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_AserviceStreamingServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_Aservice_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_AserviceServiceProtocol + public typealias ServiceProtocol = NamespaceA_Aservice_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_AserviceClientProtocol + public typealias ClientProtocol = NamespaceA_Aservice_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_AserviceClient + public typealias Client = NamespaceA_Aservice_Client } extension GRPCCore.ServiceDescriptor { public static let namespaceA_AService = Self( @@ -479,13 +479,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_BserviceStreamingServiceProtocol + public typealias StreamingServiceProtocol = NamespaceA_Bservice_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_BserviceServiceProtocol + public typealias ServiceProtocol = NamespaceA_Bservice_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_BserviceClientProtocol + public typealias ClientProtocol = NamespaceA_Bservice_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_BserviceClient + public typealias Client = NamespaceA_Bservice_Client } extension GRPCCore.ServiceDescriptor { public static let namespaceA_BService = Self( @@ -527,13 +527,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = AServiceStreamingServiceProtocol + package typealias StreamingServiceProtocol = AService_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = AServiceServiceProtocol + package typealias ServiceProtocol = AService_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = AServiceClientProtocol + package typealias ClientProtocol = AService_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = AServiceClient + package typealias Client = AService_Client } extension GRPCCore.ServiceDescriptor { package static let AService = Self( @@ -547,13 +547,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol + package typealias StreamingServiceProtocol = BService_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = BServiceServiceProtocol + package typealias ServiceProtocol = BService_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = BServiceClientProtocol + package typealias ClientProtocol = BService_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = BServiceClient + package typealias Client = BService_Client } extension GRPCCore.ServiceDescriptor { package static let BService = Self( @@ -603,13 +603,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { internal static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol + internal typealias StreamingServiceProtocol = Anamespace_AService_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + internal typealias ServiceProtocol = Anamespace_AService_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Anamespace_AServiceClientProtocol + internal typealias ClientProtocol = Anamespace_AService_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Anamespace_AServiceClient + internal typealias Client = Anamespace_AService_Client } extension GRPCCore.ServiceDescriptor { internal static let anamespace_AService = Self( @@ -623,13 +623,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { internal static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Bnamespace_BServiceStreamingServiceProtocol + internal typealias StreamingServiceProtocol = Bnamespace_BService_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Bnamespace_BServiceServiceProtocol + internal typealias ServiceProtocol = Bnamespace_BService_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Bnamespace_BServiceClientProtocol + internal typealias ClientProtocol = Bnamespace_BService_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Bnamespace_BServiceClient + internal typealias Client = Bnamespace_BService_Client } extension GRPCCore.ServiceDescriptor { internal static let bnamespace_BService = Self( @@ -673,13 +673,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Anamespace_AServiceStreamingServiceProtocol + public typealias StreamingServiceProtocol = Anamespace_AService_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Anamespace_AServiceServiceProtocol + public typealias ServiceProtocol = Anamespace_AService_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = Anamespace_AServiceClientProtocol + public typealias ClientProtocol = Anamespace_AService_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = Anamespace_AServiceClient + public typealias Client = Anamespace_AService_Client } extension GRPCCore.ServiceDescriptor { public static let anamespace_AService = Self( @@ -693,13 +693,13 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = BServiceStreamingServiceProtocol + public typealias StreamingServiceProtocol = BService_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = BServiceServiceProtocol + public typealias ServiceProtocol = BService_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = BServiceClientProtocol + public typealias ClientProtocol = BService_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = BServiceClient + public typealias Client = BService_Client } extension GRPCCore.ServiceDescriptor { public static let BService = Self( From 4e2112010660bbf855f5c02fde39f57c9dd27ee2 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 25 Sep 2024 12:49:51 +0100 Subject: [PATCH 474/580] Update examples (#2073) Motivation: In #2072 the code gen was changed to add an underscore to the generated names. This wasn't picked up in CI because the examples depend on "main" at the moment. Modifications: - Update examples Result: CI passes --- .../echo/Sources/Generated/echo.grpc.swift | 18 +++++++++--------- .../echo/Sources/Subcommands/Collect.swift | 2 +- Examples/echo/Sources/Subcommands/Expand.swift | 2 +- Examples/echo/Sources/Subcommands/Get.swift | 2 +- Examples/echo/Sources/Subcommands/Serve.swift | 2 +- Examples/echo/Sources/Subcommands/Update.swift | 2 +- .../Sources/Generated/helloworld.grpc.swift | 18 +++++++++--------- .../Sources/Subcommands/Greet.swift | 2 +- .../Sources/Subcommands/Serve.swift | 2 +- .../Sources/Generated/route_guide.grpc.swift | 18 +++++++++--------- .../Sources/Subcommands/GetFeature.swift | 2 +- .../Sources/Subcommands/ListFeatures.swift | 2 +- .../Sources/Subcommands/RecordRoute.swift | 2 +- .../Sources/Subcommands/RouteChat.swift | 2 +- 14 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Examples/echo/Sources/Generated/echo.grpc.swift b/Examples/echo/Sources/Generated/echo.grpc.swift index 63a264205..a782d7d4a 100644 --- a/Examples/echo/Sources/Generated/echo.grpc.swift +++ b/Examples/echo/Sources/Generated/echo.grpc.swift @@ -67,13 +67,13 @@ internal enum Echo_Echo { ] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Echo_EchoStreamingServiceProtocol + internal typealias StreamingServiceProtocol = Echo_Echo_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Echo_EchoServiceProtocol + internal typealias ServiceProtocol = Echo_Echo_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Echo_EchoClientProtocol + internal typealias ClientProtocol = Echo_Echo_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Echo_EchoClient + internal typealias Client = Echo_Echo_Client } extension GRPCCore.ServiceDescriptor { @@ -84,7 +84,7 @@ extension GRPCCore.ServiceDescriptor { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService { +internal protocol Echo_Echo_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Immediately returns an echo of a request. func get( request: GRPCCore.ServerRequest.Stream, @@ -163,7 +163,7 @@ extension Echo_Echo.StreamingServiceProtocol { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { +internal protocol Echo_Echo_ServiceProtocol: Echo_Echo.StreamingServiceProtocol { /// Immediately returns an echo of a request. func get( request: GRPCCore.ServerRequest.Single, @@ -189,7 +189,7 @@ internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol { ) async throws -> GRPCCore.ServerResponse.Stream } -/// Partial conformance to `Echo_EchoStreamingServiceProtocol`. +/// Partial conformance to `Echo_Echo_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ServiceProtocol { internal func get( @@ -227,7 +227,7 @@ extension Echo_Echo.ServiceProtocol { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Echo_EchoClientProtocol: Sendable { +internal protocol Echo_Echo_ClientProtocol: Sendable { /// Immediately returns an echo of a request. func get( request: GRPCCore.ClientRequest.Single, @@ -408,7 +408,7 @@ extension Echo_Echo.ClientProtocol { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Echo_EchoClient: Echo_Echo.ClientProtocol { +internal struct Echo_Echo_Client: Echo_Echo.ClientProtocol { private let client: GRPCCore.GRPCClient internal init(wrapping client: GRPCCore.GRPCClient) { diff --git a/Examples/echo/Sources/Subcommands/Collect.swift b/Examples/echo/Sources/Subcommands/Collect.swift index 6022d8a2d..d105237c3 100644 --- a/Examples/echo/Sources/Subcommands/Collect.swift +++ b/Examples/echo/Sources/Subcommands/Collect.swift @@ -39,7 +39,7 @@ struct Collect: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(wrapping: client) + let echo = Echo_Echo_Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = try await echo.collect { writer in diff --git a/Examples/echo/Sources/Subcommands/Expand.swift b/Examples/echo/Sources/Subcommands/Expand.swift index d2e48bbf3..72285dc08 100644 --- a/Examples/echo/Sources/Subcommands/Expand.swift +++ b/Examples/echo/Sources/Subcommands/Expand.swift @@ -39,7 +39,7 @@ struct Expand: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(wrapping: client) + let echo = Echo_Echo_Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } diff --git a/Examples/echo/Sources/Subcommands/Get.swift b/Examples/echo/Sources/Subcommands/Get.swift index ea69bc5f3..242e4b0a1 100644 --- a/Examples/echo/Sources/Subcommands/Get.swift +++ b/Examples/echo/Sources/Subcommands/Get.swift @@ -37,7 +37,7 @@ struct Get: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(wrapping: client) + let echo = Echo_Echo_Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { let message = Echo_EchoRequest.with { $0.text = self.arguments.message } diff --git a/Examples/echo/Sources/Subcommands/Serve.swift b/Examples/echo/Sources/Subcommands/Serve.swift index 6ea8648cb..b5e126788 100644 --- a/Examples/echo/Sources/Subcommands/Serve.swift +++ b/Examples/echo/Sources/Subcommands/Serve.swift @@ -43,7 +43,7 @@ struct Serve: AsyncParsableCommand { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -struct EchoService: Echo_EchoServiceProtocol { +struct EchoService: Echo_Echo_ServiceProtocol { func get( request: ServerRequest.Single, context: ServerContext diff --git a/Examples/echo/Sources/Subcommands/Update.swift b/Examples/echo/Sources/Subcommands/Update.swift index 1b4c81d00..2166a6aa1 100644 --- a/Examples/echo/Sources/Subcommands/Update.swift +++ b/Examples/echo/Sources/Subcommands/Update.swift @@ -39,7 +39,7 @@ struct Update: AsyncParsableCommand { try await client.run() } - let echo = Echo_EchoClient(wrapping: client) + let echo = Echo_Echo_Client(wrapping: client) for _ in 0 ..< self.arguments.repetitions { try await echo.update { writer in diff --git a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift index 8044411e5..7ae35011b 100644 --- a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift +++ b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift @@ -40,13 +40,13 @@ internal enum Helloworld_Greeter { ] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Helloworld_GreeterStreamingServiceProtocol + internal typealias StreamingServiceProtocol = Helloworld_Greeter_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Helloworld_GreeterServiceProtocol + internal typealias ServiceProtocol = Helloworld_Greeter_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Helloworld_GreeterClientProtocol + internal typealias ClientProtocol = Helloworld_Greeter_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Helloworld_GreeterClient + internal typealias Client = Helloworld_Greeter_Client } extension GRPCCore.ServiceDescriptor { @@ -58,7 +58,7 @@ extension GRPCCore.ServiceDescriptor { /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { +internal protocol Helloworld_Greeter_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting func sayHello( request: GRPCCore.ServerRequest.Stream, @@ -87,7 +87,7 @@ extension Helloworld_Greeter.StreamingServiceProtocol { /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { +internal protocol Helloworld_Greeter_ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting func sayHello( request: GRPCCore.ServerRequest.Single, @@ -95,7 +95,7 @@ internal protocol Helloworld_GreeterServiceProtocol: Helloworld_Greeter.Streamin ) async throws -> GRPCCore.ServerResponse.Single } -/// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. +/// Partial conformance to `Helloworld_Greeter_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.ServiceProtocol { internal func sayHello( @@ -112,7 +112,7 @@ extension Helloworld_Greeter.ServiceProtocol { /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Helloworld_GreeterClientProtocol: Sendable { +internal protocol Helloworld_Greeter_ClientProtocol: Sendable { /// Sends a greeting func sayHello( request: GRPCCore.ClientRequest.Single, @@ -167,7 +167,7 @@ extension Helloworld_Greeter.ClientProtocol { /// The greeting service definition. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Helloworld_GreeterClient: Helloworld_Greeter.ClientProtocol { +internal struct Helloworld_Greeter_Client: Helloworld_Greeter.ClientProtocol { private let client: GRPCCore.GRPCClient internal init(wrapping client: GRPCCore.GRPCClient) { diff --git a/Examples/hello-world/Sources/Subcommands/Greet.swift b/Examples/hello-world/Sources/Subcommands/Greet.swift index 940a1c9cb..64d405863 100644 --- a/Examples/hello-world/Sources/Subcommands/Greet.swift +++ b/Examples/hello-world/Sources/Subcommands/Greet.swift @@ -44,7 +44,7 @@ struct Greet: AsyncParsableCommand { client.beginGracefulShutdown() } - let greeter = Helloworld_GreeterClient(wrapping: client) + let greeter = Helloworld_Greeter_Client(wrapping: client) let reply = try await greeter.sayHello(.with { $0.name = self.name }) print(reply.message) } diff --git a/Examples/hello-world/Sources/Subcommands/Serve.swift b/Examples/hello-world/Sources/Subcommands/Serve.swift index 75f083ece..6ae06d013 100644 --- a/Examples/hello-world/Sources/Subcommands/Serve.swift +++ b/Examples/hello-world/Sources/Subcommands/Serve.swift @@ -42,7 +42,7 @@ struct Serve: AsyncParsableCommand { } } -struct Greeter: Helloworld_GreeterServiceProtocol { +struct Greeter: Helloworld_Greeter_ServiceProtocol { func sayHello( request: ServerRequest.Single, context: ServerContext diff --git a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift index 6a89ed2a3..3e9f77ffc 100644 --- a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift +++ b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift @@ -67,13 +67,13 @@ internal enum Routeguide_RouteGuide { ] } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Routeguide_RouteGuideStreamingServiceProtocol + internal typealias StreamingServiceProtocol = Routeguide_RouteGuide_StreamingServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Routeguide_RouteGuideServiceProtocol + internal typealias ServiceProtocol = Routeguide_RouteGuide_ServiceProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Routeguide_RouteGuideClientProtocol + internal typealias ClientProtocol = Routeguide_RouteGuide_ClientProtocol @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Routeguide_RouteGuideClient + internal typealias Client = Routeguide_RouteGuide_Client } extension GRPCCore.ServiceDescriptor { @@ -85,7 +85,7 @@ extension GRPCCore.ServiceDescriptor { /// Interface exported by the server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Routeguide_RouteGuideStreamingServiceProtocol: GRPCCore.RegistrableRPCService { +internal protocol Routeguide_RouteGuide_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// A simple RPC. /// /// Obtains the feature at a given position. @@ -181,7 +181,7 @@ extension Routeguide_RouteGuide.StreamingServiceProtocol { /// Interface exported by the server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Routeguide_RouteGuideServiceProtocol: Routeguide_RouteGuide.StreamingServiceProtocol { +internal protocol Routeguide_RouteGuide_ServiceProtocol: Routeguide_RouteGuide.StreamingServiceProtocol { /// A simple RPC. /// /// Obtains the feature at a given position. @@ -223,7 +223,7 @@ internal protocol Routeguide_RouteGuideServiceProtocol: Routeguide_RouteGuide.St ) async throws -> GRPCCore.ServerResponse.Stream } -/// Partial conformance to `Routeguide_RouteGuideStreamingServiceProtocol`. +/// Partial conformance to `Routeguide_RouteGuide_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Routeguide_RouteGuide.ServiceProtocol { internal func getFeature( @@ -262,7 +262,7 @@ extension Routeguide_RouteGuide.ServiceProtocol { /// Interface exported by the server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal protocol Routeguide_RouteGuideClientProtocol: Sendable { +internal protocol Routeguide_RouteGuide_ClientProtocol: Sendable { /// A simple RPC. /// /// Obtains the feature at a given position. @@ -476,7 +476,7 @@ extension Routeguide_RouteGuide.ClientProtocol { /// Interface exported by the server. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -internal struct Routeguide_RouteGuideClient: Routeguide_RouteGuide.ClientProtocol { +internal struct Routeguide_RouteGuide_Client: Routeguide_RouteGuide.ClientProtocol { private let client: GRPCCore.GRPCClient internal init(wrapping client: GRPCCore.GRPCClient) { diff --git a/Examples/route-guide/Sources/Subcommands/GetFeature.swift b/Examples/route-guide/Sources/Subcommands/GetFeature.swift index b3110e0c9..72ba1d1a4 100644 --- a/Examples/route-guide/Sources/Subcommands/GetFeature.swift +++ b/Examples/route-guide/Sources/Subcommands/GetFeature.swift @@ -47,7 +47,7 @@ struct GetFeature: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) let point = Routeguide_Point.with { $0.latitude = self.latitude diff --git a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift index 383eda154..7e83add71 100644 --- a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift +++ b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift @@ -61,7 +61,7 @@ struct ListFeatures: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) let boundingRectangle = Routeguide_Rectangle.with { $0.lo.latitude = self.minLatitude $0.hi.latitude = self.maxLatitude diff --git a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift index c3fcce361..7cdf30cd2 100644 --- a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift +++ b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift @@ -40,7 +40,7 @@ struct RecordRoute: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) // Get all features. let rectangle = Routeguide_Rectangle.with { diff --git a/Examples/route-guide/Sources/Subcommands/RouteChat.swift b/Examples/route-guide/Sources/Subcommands/RouteChat.swift index 23266e3bf..3461e1f75 100644 --- a/Examples/route-guide/Sources/Subcommands/RouteChat.swift +++ b/Examples/route-guide/Sources/Subcommands/RouteChat.swift @@ -40,7 +40,7 @@ struct RouteChat: AsyncParsableCommand { try await client.run() } - let routeGuide = Routeguide_RouteGuideClient(wrapping: client) + let routeGuide = Routeguide_RouteGuide_Client(wrapping: client) try await routeGuide.routeChat { writer in let notes: [(String, (Int32, Int32))] = [ From 1fd34e73089419a5335c6392a88cd176884bca01 Mon Sep 17 00:00:00 2001 From: Clinton Nkwocha <32041805+clintonpi@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:08:20 +0100 Subject: [PATCH 475/580] Restructure in-process transport (#2071) Motivation: To create a connected pair of `ServerTransport` and `ClientTransport`, users need to call `InProcessTransport.makePair(serviceConfig:)` which returns a tuple. The spelling of this API can be more intuitive and simple. Modifications: - Restructure `InProcessTransport` to include two properties: `server` (the `ServerTransport`) and `client` (the `ClientTransport`), and an `init(serviceConfig:)` that pairs these properties. - Add missing GRPCInProcessTransport dependency to GRPCCore test target. Result: The spelling of the `InProcessTransport` API will be better. --- Sources/GRPCCore/GRPCClient.swift | 5 +- Sources/GRPCCore/GRPCServer.swift | 4 +- .../InProcessClientTransport.swift | 353 ----------------- .../InProcessServerTransport.swift | 80 ---- .../InProcessTransport+Client.swift | 359 ++++++++++++++++++ .../InProcessTransport+Server.swift | 83 ++++ .../InProcessTransport.swift | 25 +- ...ientRPCExecutorTestHarness+Transport.swift | 6 +- .../ClientRPCExecutorTestHarness.swift | 4 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 12 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 8 +- .../InProcessClientTransportTests.swift | 18 +- .../InProcessServerTransportTests.swift | 4 +- 13 files changed, 481 insertions(+), 480 deletions(-) delete mode 100644 Sources/GRPCInProcessTransport/InProcessClientTransport.swift delete mode 100644 Sources/GRPCInProcessTransport/InProcessServerTransport.swift create mode 100644 Sources/GRPCInProcessTransport/InProcessTransport+Client.swift create mode 100644 Sources/GRPCInProcessTransport/InProcessTransport+Server.swift diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 6c2729548..a3f9fe2d9 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -65,11 +65,10 @@ private import Synchronization /// ) /// /// // Finally create a transport and instantiate the client, adding an interceptor. -/// let inProcessServerTransport = InProcessServerTransport() -/// let inProcessClientTransport = InProcessClientTransport(serverTransport: inProcessServerTransport) +/// let inProcessTransport = InProcessTransport() /// /// let client = GRPCClient( -/// transport: inProcessClientTransport, +/// transport: inProcessTransport.client, /// interceptors: [StatsRecordingClientInterceptor()], /// configuration: configuration /// ) diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index b3d99b7de..feb218b19 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -35,7 +35,7 @@ private import Synchronization /// /// ```swift /// // Create and an in-process transport. -/// let inProcessTransport = InProcessServerTransport() +/// let inProcessTransport = InProcessTransport() /// /// // Create the 'Greeter' and 'Echo' services. /// let greeter = GreeterService() @@ -46,7 +46,7 @@ private import Synchronization /// /// // Finally create the server. /// let server = GRPCServer( -/// transport: inProcessTransport, +/// transport: inProcessTransport.server, /// services: [greeter, echo], /// interceptors: [statsRecorder] /// ) diff --git a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift b/Sources/GRPCInProcessTransport/InProcessClientTransport.swift deleted file mode 100644 index 822138f33..000000000 --- a/Sources/GRPCInProcessTransport/InProcessClientTransport.swift +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -public import GRPCCore -private import Synchronization - -/// An in-process implementation of a ``ClientTransport``. -/// -/// This is useful when you're interested in testing your application without any actual networking layers -/// involved, as the client and server will communicate directly with each other via in-process streams. -/// -/// To use this client, you'll have to provide an ``InProcessServerTransport`` upon creation, as well -/// as a ``ServiceConfig``. -/// -/// Once you have a client, you must keep a long-running task executing ``connect()``, which -/// will return only once all streams have been finished and ``beginGracefulShutdown()`` has been called on this client; or -/// when the containing task is cancelled. -/// -/// To execute requests using this client, use ``withStream(descriptor:options:_:)``. If this function is -/// called before ``connect()`` is called, then any streams will remain pending and the call will -/// block until ``connect()`` is called or the task is cancelled. -/// -/// - SeeAlso: ``ClientTransport`` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public final class InProcessClientTransport: ClientTransport { - private enum State: Sendable { - struct UnconnectedState { - var serverTransport: InProcessServerTransport - var pendingStreams: [AsyncStream.Continuation] - - init(serverTransport: InProcessServerTransport) { - self.serverTransport = serverTransport - self.pendingStreams = [] - } - } - - struct ConnectedState { - var serverTransport: InProcessServerTransport - var nextStreamID: Int - var openStreams: - [Int: ( - RPCStream, - RPCStream< - RPCAsyncSequence, RPCWriter.Closable - > - )] - var signalEndContinuation: AsyncStream.Continuation - - init( - fromUnconnected state: UnconnectedState, - signalEndContinuation: AsyncStream.Continuation - ) { - self.serverTransport = state.serverTransport - self.nextStreamID = 0 - self.openStreams = [:] - self.signalEndContinuation = signalEndContinuation - } - } - - struct ClosedState { - var openStreams: - [Int: ( - RPCStream, - RPCStream< - RPCAsyncSequence, RPCWriter.Closable - > - )] - var signalEndContinuation: AsyncStream.Continuation? - - init() { - self.openStreams = [:] - self.signalEndContinuation = nil - } - - init(fromConnected state: ConnectedState) { - self.openStreams = state.openStreams - self.signalEndContinuation = state.signalEndContinuation - } - } - - case unconnected(UnconnectedState) - case connected(ConnectedState) - case closed(ClosedState) - } - - public typealias Inbound = RPCAsyncSequence - public typealias Outbound = RPCWriter.Closable - - public let retryThrottle: RetryThrottle? - - private let methodConfig: MethodConfigs - private let state: Mutex - - /// Creates a new in-process client transport. - /// - /// - Parameters: - /// - server: The in-process server transport to connect to. - /// - serviceConfig: Service configuration. - public init( - server: InProcessServerTransport, - serviceConfig: ServiceConfig = ServiceConfig() - ) { - self.retryThrottle = serviceConfig.retryThrottling.map { RetryThrottle(policy: $0) } - self.methodConfig = MethodConfigs(serviceConfig: serviceConfig) - self.state = Mutex(.unconnected(.init(serverTransport: server))) - } - - /// Establish and maintain a connection to the remote destination. - /// - /// Maintains a long-lived connection, or set of connections, to a remote destination. - /// Connections may be added or removed over time as required by the implementation and the - /// demand for streams by the client. - /// - /// Implementations of this function will typically create a long-lived task group which - /// maintains connections. The function exits when all open streams have been closed and new connections - /// are no longer required by the caller who signals this by calling ``beginGracefulShutdown()``, or by cancelling the - /// task this function runs in. - public func connect() async throws { - let (stream, continuation) = AsyncStream.makeStream() - try self.state.withLock { state in - switch state { - case .unconnected(let unconnectedState): - state = .connected( - .init( - fromUnconnected: unconnectedState, - signalEndContinuation: continuation - ) - ) - for pendingStream in unconnectedState.pendingStreams { - pendingStream.finish() - } - case .connected: - throw RPCError( - code: .failedPrecondition, - message: "Already connected to server." - ) - case .closed: - throw RPCError( - code: .failedPrecondition, - message: "Can't connect to server, transport is closed." - ) - } - } - - for await _ in stream { - // This for-await loop will exit (and thus `connect()` will return) - // only when the task is cancelled, or when the stream's continuation is - // finished - whichever happens first. - // The continuation will be finished when `close()` is called and there - // are no more open streams. - } - - // If at this point there are any open streams, it's because Cancellation - // occurred and all open streams must now be closed. - let openStreams = self.state.withLock { state in - switch state { - case .unconnected: - // We have transitioned to connected, and we can't transition back. - fatalError("Invalid state") - case .connected(let connectedState): - state = .closed(.init()) - return connectedState.openStreams.values - case .closed(let closedState): - return closedState.openStreams.values - } - } - - for (clientStream, serverStream) in openStreams { - await clientStream.outbound.finish(throwing: CancellationError()) - await serverStream.outbound.finish(throwing: CancellationError()) - } - } - - /// Signal to the transport that no new streams may be created. - /// - /// Existing streams may run to completion naturally but calling ``withStream(descriptor:options:_:)`` - /// will result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. - /// - /// If you want to forcefully cancel all active streams then cancel the task running ``connect()``. - public func beginGracefulShutdown() { - let maybeContinuation: AsyncStream.Continuation? = self.state.withLock { state in - switch state { - case .unconnected: - state = .closed(.init()) - return nil - case .connected(let connectedState): - if connectedState.openStreams.count == 0 { - state = .closed(.init()) - return connectedState.signalEndContinuation - } else { - state = .closed(.init(fromConnected: connectedState)) - return nil - } - case .closed: - return nil - } - } - maybeContinuation?.finish() - } - - /// Opens a stream using the transport, and uses it as input into a user-provided closure. - /// - /// - Important: The opened stream is closed after the closure is finished. - /// - /// This transport implementation throws ``RPCError/Code/failedPrecondition`` if the transport - /// is closing or has been closed. - /// - /// This implementation will queue any streams (and thus block this call) if this function is called before - /// ``connect()``, until a connection is established - at which point all streams will be - /// created. - /// - /// - Parameters: - /// - descriptor: A description of the method to open a stream for. - /// - options: Options specific to the stream. - /// - closure: A closure that takes the opened stream as parameter. - /// - Returns: Whatever value was returned from `closure`. - public func withStream( - descriptor: MethodDescriptor, - options: CallOptions, - _ closure: (RPCStream) async throws -> T - ) async throws -> T { - let request = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) - let response = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) - - let clientStream = RPCStream( - descriptor: descriptor, - inbound: RPCAsyncSequence(wrapping: response.stream), - outbound: RPCWriter.Closable(wrapping: request.continuation) - ) - - let serverStream = RPCStream( - descriptor: descriptor, - inbound: RPCAsyncSequence(wrapping: request.stream), - outbound: RPCWriter.Closable(wrapping: response.continuation) - ) - - let waitForConnectionStream: AsyncStream? = self.state.withLock { state in - if case .unconnected(var unconnectedState) = state { - let (stream, continuation) = AsyncStream.makeStream() - unconnectedState.pendingStreams.append(continuation) - state = .unconnected(unconnectedState) - return stream - } - return nil - } - - if let waitForConnectionStream { - for await _ in waitForConnectionStream { - // This loop will exit either when the task is cancelled or when the - // client connects and this stream can be opened. - } - try Task.checkCancellation() - } - - let acceptStream: Result = self.state.withLock { state in - switch state { - case .unconnected: - // The state cannot be unconnected because if it was, then the above - // for-await loop on `pendingStream` would have not returned. - // The only other option is for the task to have been cancelled, - // and that's why we check for cancellation right after the loop. - fatalError("Invalid state.") - - case .connected(var connectedState): - let streamID = connectedState.nextStreamID - do { - try connectedState.serverTransport.acceptStream(serverStream) - connectedState.openStreams[streamID] = (clientStream, serverStream) - connectedState.nextStreamID += 1 - state = .connected(connectedState) - return .success(streamID) - } catch let acceptStreamError as RPCError { - return .failure(acceptStreamError) - } catch { - return .failure(RPCError(code: .unknown, message: "Unknown error: \(error).")) - } - - case .closed: - let error = RPCError(code: .failedPrecondition, message: "The client transport is closed.") - return .failure(error) - } - } - - switch acceptStream { - case .success(let streamID): - let streamHandlingResult: Result - do { - let result = try await closure(clientStream) - streamHandlingResult = .success(result) - } catch { - streamHandlingResult = .failure(error) - } - - await clientStream.outbound.finish() - self.removeStream(id: streamID) - - return try streamHandlingResult.get() - - case .failure(let error): - await serverStream.outbound.finish(throwing: error) - await clientStream.outbound.finish(throwing: error) - throw error - } - } - - private func removeStream(id streamID: Int) { - let maybeEndContinuation = self.state.withLock { state in - switch state { - case .unconnected: - // The state cannot be unconnected at this point, because if we made - // it this far, it's because the transport was connected. - // Once connected, it's impossible to transition back to unconnected, - // so this is an invalid state. - fatalError("Invalid state") - case .connected(var connectedState): - connectedState.openStreams.removeValue(forKey: streamID) - state = .connected(connectedState) - case .closed(var closedState): - closedState.openStreams.removeValue(forKey: streamID) - state = .closed(closedState) - if closedState.openStreams.isEmpty { - // This was the last open stream: signal the closure of the client. - return closedState.signalEndContinuation - } - } - return nil - } - maybeEndContinuation?.finish() - } - - /// Returns the execution configuration for a given method. - /// - /// - Parameter descriptor: The method to lookup configuration for. - /// - Returns: Execution configuration for the method, if it exists. - public func config( - forMethod descriptor: MethodDescriptor - ) -> MethodConfig? { - self.methodConfig[descriptor] - } -} diff --git a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift b/Sources/GRPCInProcessTransport/InProcessServerTransport.swift deleted file mode 100644 index 2bb2ed57d..000000000 --- a/Sources/GRPCInProcessTransport/InProcessServerTransport.swift +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -public import GRPCCore - -/// An in-process implementation of a ``ServerTransport``. -/// -/// This is useful when you're interested in testing your application without any actual networking layers -/// involved, as the client and server will communicate directly with each other via in-process streams. -/// -/// To use this server, you call ``listen(_:)`` and iterate over the returned `AsyncSequence` to get all -/// RPC requests made from clients (as ``RPCStream``s). -/// To stop listening to new requests, call ``beginGracefulShutdown()``. -/// -/// - SeeAlso: ``ClientTransport`` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public struct InProcessServerTransport: ServerTransport, Sendable { - public typealias Inbound = RPCAsyncSequence - public typealias Outbound = RPCWriter.Closable - - private let newStreams: AsyncStream> - private let newStreamsContinuation: AsyncStream>.Continuation - - /// Creates a new instance of ``InProcessServerTransport``. - public init() { - (self.newStreams, self.newStreamsContinuation) = AsyncStream.makeStream() - } - - /// Publish a new ``RPCStream``, which will be returned by the transport's ``events`` - /// successful case. - /// - /// - Parameter stream: The new ``RPCStream`` to publish. - /// - Throws: ``RPCError`` with code ``RPCError/Code-swift.struct/failedPrecondition`` - /// if the server transport stopped listening to new streams (i.e., if ``beginGracefulShutdown()`` has been called). - internal func acceptStream(_ stream: RPCStream) throws { - let yieldResult = self.newStreamsContinuation.yield(stream) - if case .terminated = yieldResult { - throw RPCError( - code: .failedPrecondition, - message: "The server transport is closed." - ) - } - } - - public func listen( - streamHandler: @escaping @Sendable ( - _ stream: RPCStream, - _ context: ServerContext - ) async -> Void - ) async throws { - await withDiscardingTaskGroup { group in - for await stream in self.newStreams { - group.addTask { - let context = ServerContext(descriptor: stream.descriptor) - await streamHandler(stream, context) - } - } - } - } - - /// Stop listening to any new ``RPCStream`` publications. - /// - /// - SeeAlso: ``ServerTransport`` - public func beginGracefulShutdown() { - self.newStreamsContinuation.finish() - } -} diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift new file mode 100644 index 000000000..08c6b3476 --- /dev/null +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -0,0 +1,359 @@ +/* + * 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. + */ + +public import GRPCCore +private import Synchronization + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension InProcessTransport { + /// An in-process implementation of a ``ClientTransport``. + /// + /// This is useful when you're interested in testing your application without any actual networking layers + /// involved, as the client and server will communicate directly with each other via in-process streams. + /// + /// To use this client, you'll have to provide a ``ServerTransport`` upon creation, as well + /// as a ``ServiceConfig``. + /// + /// Once you have a client, you must keep a long-running task executing ``connect()``, which + /// will return only once all streams have been finished and ``beginGracefulShutdown()`` has been called on this client; or + /// when the containing task is cancelled. + /// + /// To execute requests using this client, use ``withStream(descriptor:options:_:)``. If this function is + /// called before ``connect()`` is called, then any streams will remain pending and the call will + /// block until ``connect()`` is called or the task is cancelled. + /// + /// - SeeAlso: ``ClientTransport`` + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + public final class Client: ClientTransport { + private enum State: Sendable { + struct UnconnectedState { + var serverTransport: InProcessTransport.Server + var pendingStreams: [AsyncStream.Continuation] + + init(serverTransport: InProcessTransport.Server) { + self.serverTransport = serverTransport + self.pendingStreams = [] + } + } + + struct ConnectedState { + var serverTransport: InProcessTransport.Server + var nextStreamID: Int + var openStreams: + [Int: ( + RPCStream, + RPCStream< + RPCAsyncSequence, RPCWriter.Closable + > + )] + var signalEndContinuation: AsyncStream.Continuation + + init( + fromUnconnected state: UnconnectedState, + signalEndContinuation: AsyncStream.Continuation + ) { + self.serverTransport = state.serverTransport + self.nextStreamID = 0 + self.openStreams = [:] + self.signalEndContinuation = signalEndContinuation + } + } + + struct ClosedState { + var openStreams: + [Int: ( + RPCStream, + RPCStream< + RPCAsyncSequence, RPCWriter.Closable + > + )] + var signalEndContinuation: AsyncStream.Continuation? + + init() { + self.openStreams = [:] + self.signalEndContinuation = nil + } + + init(fromConnected state: ConnectedState) { + self.openStreams = state.openStreams + self.signalEndContinuation = state.signalEndContinuation + } + } + + case unconnected(UnconnectedState) + case connected(ConnectedState) + case closed(ClosedState) + } + + public typealias Inbound = RPCAsyncSequence + public typealias Outbound = RPCWriter.Closable + + public let retryThrottle: RetryThrottle? + + private let methodConfig: MethodConfigs + private let state: Mutex + + /// Creates a new in-process client transport. + /// + /// - Parameters: + /// - server: The in-process server transport to connect to. + /// - serviceConfig: Service configuration. + public init( + server: InProcessTransport.Server, + serviceConfig: ServiceConfig = ServiceConfig() + ) { + self.retryThrottle = serviceConfig.retryThrottling.map { RetryThrottle(policy: $0) } + self.methodConfig = MethodConfigs(serviceConfig: serviceConfig) + self.state = Mutex(.unconnected(.init(serverTransport: server))) + } + + /// Establish and maintain a connection to the remote destination. + /// + /// Maintains a long-lived connection, or set of connections, to a remote destination. + /// Connections may be added or removed over time as required by the implementation and the + /// demand for streams by the client. + /// + /// Implementations of this function will typically create a long-lived task group which + /// maintains connections. The function exits when all open streams have been closed and new connections + /// are no longer required by the caller who signals this by calling ``beginGracefulShutdown()``, or by cancelling the + /// task this function runs in. + public func connect() async throws { + let (stream, continuation) = AsyncStream.makeStream() + try self.state.withLock { state in + switch state { + case .unconnected(let unconnectedState): + state = .connected( + .init( + fromUnconnected: unconnectedState, + signalEndContinuation: continuation + ) + ) + for pendingStream in unconnectedState.pendingStreams { + pendingStream.finish() + } + case .connected: + throw RPCError( + code: .failedPrecondition, + message: "Already connected to server." + ) + case .closed: + throw RPCError( + code: .failedPrecondition, + message: "Can't connect to server, transport is closed." + ) + } + } + + for await _ in stream { + // This for-await loop will exit (and thus `connect()` will return) + // only when the task is cancelled, or when the stream's continuation is + // finished - whichever happens first. + // The continuation will be finished when `close()` is called and there + // are no more open streams. + } + + // If at this point there are any open streams, it's because Cancellation + // occurred and all open streams must now be closed. + let openStreams = self.state.withLock { state in + switch state { + case .unconnected: + // We have transitioned to connected, and we can't transition back. + fatalError("Invalid state") + case .connected(let connectedState): + state = .closed(.init()) + return connectedState.openStreams.values + case .closed(let closedState): + return closedState.openStreams.values + } + } + + for (clientStream, serverStream) in openStreams { + await clientStream.outbound.finish(throwing: CancellationError()) + await serverStream.outbound.finish(throwing: CancellationError()) + } + } + + /// Signal to the transport that no new streams may be created. + /// + /// Existing streams may run to completion naturally but calling ``withStream(descriptor:options:_:)`` + /// will result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. + /// + /// If you want to forcefully cancel all active streams then cancel the task running ``connect()``. + public func beginGracefulShutdown() { + let maybeContinuation: AsyncStream.Continuation? = self.state.withLock { state in + switch state { + case .unconnected: + state = .closed(.init()) + return nil + case .connected(let connectedState): + if connectedState.openStreams.count == 0 { + state = .closed(.init()) + return connectedState.signalEndContinuation + } else { + state = .closed(.init(fromConnected: connectedState)) + return nil + } + case .closed: + return nil + } + } + maybeContinuation?.finish() + } + + /// Opens a stream using the transport, and uses it as input into a user-provided closure. + /// + /// - Important: The opened stream is closed after the closure is finished. + /// + /// This transport implementation throws ``RPCError/Code/failedPrecondition`` if the transport + /// is closing or has been closed. + /// + /// This implementation will queue any streams (and thus block this call) if this function is called before + /// ``connect()``, until a connection is established - at which point all streams will be + /// created. + /// + /// - Parameters: + /// - descriptor: A description of the method to open a stream for. + /// - options: Options specific to the stream. + /// - closure: A closure that takes the opened stream as parameter. + /// - Returns: Whatever value was returned from `closure`. + public func withStream( + descriptor: MethodDescriptor, + options: CallOptions, + _ closure: (RPCStream) async throws -> T + ) async throws -> T { + let request = GRPCAsyncThrowingStream.makeStream(of: RPCRequestPart.self) + let response = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) + + let clientStream = RPCStream( + descriptor: descriptor, + inbound: RPCAsyncSequence(wrapping: response.stream), + outbound: RPCWriter.Closable(wrapping: request.continuation) + ) + + let serverStream = RPCStream( + descriptor: descriptor, + inbound: RPCAsyncSequence(wrapping: request.stream), + outbound: RPCWriter.Closable(wrapping: response.continuation) + ) + + let waitForConnectionStream: AsyncStream? = self.state.withLock { state in + if case .unconnected(var unconnectedState) = state { + let (stream, continuation) = AsyncStream.makeStream() + unconnectedState.pendingStreams.append(continuation) + state = .unconnected(unconnectedState) + return stream + } + return nil + } + + if let waitForConnectionStream { + for await _ in waitForConnectionStream { + // This loop will exit either when the task is cancelled or when the + // client connects and this stream can be opened. + } + try Task.checkCancellation() + } + + let acceptStream: Result = self.state.withLock { state in + switch state { + case .unconnected: + // The state cannot be unconnected because if it was, then the above + // for-await loop on `pendingStream` would have not returned. + // The only other option is for the task to have been cancelled, + // and that's why we check for cancellation right after the loop. + fatalError("Invalid state.") + + case .connected(var connectedState): + let streamID = connectedState.nextStreamID + do { + try connectedState.serverTransport.acceptStream(serverStream) + connectedState.openStreams[streamID] = (clientStream, serverStream) + connectedState.nextStreamID += 1 + state = .connected(connectedState) + return .success(streamID) + } catch let acceptStreamError as RPCError { + return .failure(acceptStreamError) + } catch { + return .failure(RPCError(code: .unknown, message: "Unknown error: \(error).")) + } + + case .closed: + let error = RPCError( + code: .failedPrecondition, + message: "The client transport is closed." + ) + return .failure(error) + } + } + + switch acceptStream { + case .success(let streamID): + let streamHandlingResult: Result + do { + let result = try await closure(clientStream) + streamHandlingResult = .success(result) + } catch { + streamHandlingResult = .failure(error) + } + + await clientStream.outbound.finish() + self.removeStream(id: streamID) + + return try streamHandlingResult.get() + + case .failure(let error): + await serverStream.outbound.finish(throwing: error) + await clientStream.outbound.finish(throwing: error) + throw error + } + } + + private func removeStream(id streamID: Int) { + let maybeEndContinuation = self.state.withLock { state in + switch state { + case .unconnected: + // The state cannot be unconnected at this point, because if we made + // it this far, it's because the transport was connected. + // Once connected, it's impossible to transition back to unconnected, + // so this is an invalid state. + fatalError("Invalid state") + case .connected(var connectedState): + connectedState.openStreams.removeValue(forKey: streamID) + state = .connected(connectedState) + case .closed(var closedState): + closedState.openStreams.removeValue(forKey: streamID) + state = .closed(closedState) + if closedState.openStreams.isEmpty { + // This was the last open stream: signal the closure of the client. + return closedState.signalEndContinuation + } + } + return nil + } + maybeEndContinuation?.finish() + } + + /// Returns the execution configuration for a given method. + /// + /// - Parameter descriptor: The method to lookup configuration for. + /// - Returns: Execution configuration for the method, if it exists. + public func config( + forMethod descriptor: MethodDescriptor + ) -> MethodConfig? { + self.methodConfig[descriptor] + } + } +} diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift new file mode 100644 index 000000000..0637255af --- /dev/null +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -0,0 +1,83 @@ +/* + * 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. + */ + +public import GRPCCore + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension InProcessTransport { + /// An in-process implementation of a ``ServerTransport``. + /// + /// This is useful when you're interested in testing your application without any actual networking layers + /// involved, as the client and server will communicate directly with each other via in-process streams. + /// + /// To use this server, you call ``listen(_:)`` and iterate over the returned `AsyncSequence` to get all + /// RPC requests made from clients (as ``RPCStream``s). + /// To stop listening to new requests, call ``beginGracefulShutdown()``. + /// + /// - SeeAlso: ``ClientTransport`` + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + public struct Server: ServerTransport, Sendable { + public typealias Inbound = RPCAsyncSequence + public typealias Outbound = RPCWriter.Closable + + private let newStreams: AsyncStream> + private let newStreamsContinuation: AsyncStream>.Continuation + + /// Creates a new instance of ``Server``. + public init() { + (self.newStreams, self.newStreamsContinuation) = AsyncStream.makeStream() + } + + /// Publish a new ``RPCStream``, which will be returned by the transport's ``events`` + /// successful case. + /// + /// - Parameter stream: The new ``RPCStream`` to publish. + /// - Throws: ``RPCError`` with code ``RPCError/Code-swift.struct/failedPrecondition`` + /// if the server transport stopped listening to new streams (i.e., if ``beginGracefulShutdown()`` has been called). + internal func acceptStream(_ stream: RPCStream) throws { + let yieldResult = self.newStreamsContinuation.yield(stream) + if case .terminated = yieldResult { + throw RPCError( + code: .failedPrecondition, + message: "The server transport is closed." + ) + } + } + + public func listen( + streamHandler: @escaping @Sendable ( + _ stream: RPCStream, + _ context: ServerContext + ) async -> Void + ) async throws { + await withDiscardingTaskGroup { group in + for await stream in self.newStreams { + group.addTask { + let context = ServerContext(descriptor: stream.descriptor) + await streamHandler(stream, context) + } + } + } + } + + /// Stop listening to any new ``RPCStream`` publications. + /// + /// - SeeAlso: ``ServerTransport`` + public func beginGracefulShutdown() { + self.newStreamsContinuation.finish() + } + } +} diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index 32a2002e9..e1bee0ae5 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -16,24 +16,17 @@ public import GRPCCore -public enum InProcessTransport { - /// Returns a pair containing an ``InProcessServerTransport`` and an ``InProcessClientTransport``. - /// - /// This function is purely for convenience and does no more than constructing a server transport - /// and a client using that server transport. +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct InProcessTransport: Sendable { + public let server: Self.Server + public let client: Self.Client + + /// Initializes a new ``InProcessTransport`` pairing a ``Client`` and a ``Server``. /// /// - Parameters: /// - serviceConfig: Configuration describing how methods should be executed. - /// - Returns: A tuple containing the connected server and client in-process transports. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public static func makePair( - serviceConfig: ServiceConfig = ServiceConfig() - ) -> (server: InProcessServerTransport, client: InProcessClientTransport) { - let server = InProcessServerTransport() - let client = InProcessClientTransport( - server: server, - serviceConfig: serviceConfig - ) - return (server, client) + public init(serviceConfig: ServiceConfig = ServiceConfig()) { + self.server = Self.Server() + self.client = Self.Client(server: self.server, serviceConfig: serviceConfig) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 0500b23ec..4198c9b31 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -18,10 +18,10 @@ import GRPCCore import GRPCInProcessTransport @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension InProcessServerTransport { +extension InProcessTransport.Server { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) - ) -> InProcessClientTransport { - return InProcessClientTransport(server: self) + ) -> InProcessTransport.Client { + return InProcessTransport.Client(server: self) } } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 06f200ae6..7bfef6912 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -48,13 +48,13 @@ struct ClientRPCExecutorTestHarness { switch transport { case .inProcess: - let server = InProcessServerTransport() + let server = InProcessTransport.Server() let client = server.spawnClientTransport() self.serverTransport = StreamCountingServerTransport(wrapping: server) self.clientTransport = StreamCountingClientTransport(wrapping: client) case .throwsOnStreamCreation(let code): - let server = InProcessServerTransport() // Will never be called. + let server = InProcessTransport.Server() // Will never be called. let client = ThrowOnStreamCreationTransport(code: code) self.serverTransport = StreamCountingServerTransport(wrapping: server) self.clientTransport = StreamCountingClientTransport(wrapping: client) diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index af566279d..b5558ae48 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -25,7 +25,7 @@ final class GRPCClientTests: XCTestCase { interceptors: [any ClientInterceptor] = [], _ body: (GRPCClient, GRPCServer) async throws -> Void ) async throws { - let inProcess = InProcessTransport.makePair() + let inProcess = InProcessTransport() let client = GRPCClient(transport: inProcess.client, interceptors: interceptors) let server = GRPCServer(transport: inProcess.server, services: services) @@ -329,7 +329,7 @@ final class GRPCClientTests: XCTestCase { } func testCancelRunningClient() async throws { - let inProcess = InProcessTransport.makePair() + let inProcess = InProcessTransport() let client = GRPCClient(transport: inProcess.client) try await withThrowingTaskGroup(of: Void.self) { group in @@ -377,8 +377,8 @@ final class GRPCClientTests: XCTestCase { } func testRunStoppedClient() async throws { - let (_, clientTransport) = InProcessTransport.makePair() - let client = GRPCClient(transport: clientTransport) + let inProcess = InProcessTransport() + let client = GRPCClient(transport: inProcess.client) // Run the client. let task = Task { try await client.run() } task.cancel() @@ -393,8 +393,8 @@ final class GRPCClientTests: XCTestCase { } func testRunAlreadyRunningClient() async throws { - let (_, clientTransport) = InProcessTransport.makePair() - let client = GRPCClient(transport: clientTransport) + let inProcess = InProcessTransport() + let client = GRPCClient(transport: inProcess.client) // Run the client. let task = Task { try await client.run() } // Make sure the client is run for the first time here. diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index d8771d81e..a513f5d15 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -23,9 +23,9 @@ final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [], - _ body: (InProcessClientTransport, GRPCServer) async throws -> Void + _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { - let inProcess = InProcessTransport.makePair() + let inProcess = InProcessTransport() let server = GRPCServer( transport: inProcess.server, services: services, @@ -317,7 +317,7 @@ final class GRPCServerTests: XCTestCase { } func testCancelRunningServer() async throws { - let inProcess = InProcessTransport.makePair() + let inProcess = InProcessTransport() let task = Task { let server = GRPCServer(transport: inProcess.server, services: [BinaryEcho()]) try await server.serve() @@ -338,7 +338,7 @@ final class GRPCServerTests: XCTestCase { } func testTestRunStoppedServer() async throws { - let server = GRPCServer(transport: InProcessServerTransport(), services: []) + let server = GRPCServer(transport: InProcessTransport.Server(), services: []) // Run the server. let task = Task { try await server.serve() } task.cancel() diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index d33b2774d..5730ccf46 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -149,7 +149,7 @@ final class InProcessClientTransportTests: XCTestCase { } func testOpenStreamSuccessfullyAndThenClose() async throws { - let server = InProcessServerTransport() + let server = InProcessTransport.Server() let client = makeClient(server: server) try await withThrowingTaskGroup(of: Void.self) { group in @@ -208,8 +208,8 @@ final class InProcessClientTransportTests: XCTestCase { ] ) - var client = InProcessClientTransport( - server: InProcessServerTransport(), + var client = InProcessTransport.Client( + server: InProcessTransport.Server(), serviceConfig: serviceConfig ) @@ -232,8 +232,8 @@ final class InProcessClientTransportTests: XCTestCase { executionPolicy: .retry(retryPolicy) ) serviceConfig.methodConfig.append(overrideConfiguration) - client = InProcessClientTransport( - server: InProcessServerTransport(), + client = InProcessTransport.Client( + server: InProcessTransport.Server(), serviceConfig: serviceConfig ) @@ -249,7 +249,7 @@ final class InProcessClientTransportTests: XCTestCase { } func testOpenMultipleStreamsThenClose() async throws { - let server = InProcessServerTransport() + let server = InProcessTransport.Server() let client = makeClient(server: server) try await withThrowingTaskGroup(of: Void.self) { group in @@ -285,8 +285,8 @@ final class InProcessClientTransportTests: XCTestCase { } func makeClient( - server: InProcessServerTransport = InProcessServerTransport() - ) -> InProcessClientTransport { + server: InProcessTransport.Server = InProcessTransport.Server() + ) -> InProcessTransport.Client { let defaultPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -304,7 +304,7 @@ final class InProcessClientTransportTests: XCTestCase { ] ) - return InProcessClientTransport( + return InProcessTransport.Client( server: server, serviceConfig: serviceConfig ) diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 7cd8fed20..5d451fb9d 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -22,7 +22,7 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { - let transport = InProcessServerTransport() + let transport = InProcessTransport.Server() let outbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let stream = RPCStream< @@ -54,7 +54,7 @@ final class InProcessServerTransportTests: XCTestCase { } func testStopListening() async throws { - let transport = InProcessServerTransport() + let transport = InProcessTransport.Server() let firstStreamOutbound = GRPCAsyncThrowingStream.makeStream(of: RPCResponsePart.self) let firstStream = RPCStream< From cfa674302019c9ad6ef82806c4f79df1c4bf758e Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 30 Sep 2024 08:38:24 +0100 Subject: [PATCH 476/580] Update tutorials and docs (#2070) Co-authored-by: Gus Cairo --- .spi.yml | 6 +- README.md | 67 +++++---- .../Call/Server/ServerInterceptor.swift | 4 +- .../Articles/Generating-stubs.md | 137 +----------------- .../Documentation.docc/Documentation.md | 43 ++++-- .../Hello-World/Hello-World.tutorial | 16 +- .../Resources/hello-world-sec04-step01.swift | 3 +- .../Resources/hello-world-sec04-step02.swift | 6 +- ...route-guide-sec01-step07-description.swift | 4 +- ...route-guide-sec01-step08-description.swift | 9 +- .../route-guide-sec03-step04-gen-grpc.txt | 1 - ...ute-guide-sec04-step02-unimplemented.swift | 12 +- .../route-guide-sec04-step03-features.swift | 12 +- .../route-guide-sec04-step04-unary.swift | 12 +- .../route-guide-sec04-step05-unary.swift | 12 +- ...-guide-sec04-step06-server-streaming.swift | 12 +- ...-guide-sec04-step07-server-streaming.swift | 12 +- ...-guide-sec04-step08-client-streaming.swift | 12 +- ...te-guide-sec04-step09-bidi-streaming.swift | 12 +- ...te-guide-sec04-step10-bidi-streaming.swift | 12 +- .../route-guide-sec05-step00-package.swift | 9 +- .../route-guide-sec05-step01-package.swift | 9 +- .../route-guide-sec05-step02-package.swift | 9 +- .../route-guide-sec05-step07-server.swift | 2 +- .../route-guide-sec05-step08-run.swift | 2 +- ...te-guide-sec06-step02-add-run-client.swift | 2 +- ...ute-guide-sec06-step03-create-client.swift | 2 +- .../route-guide-sec06-step04-run-client.swift | 2 +- .../route-guide-sec06-step05-stub.swift | 2 +- ...route-guide-sec06-step06-get-feature.swift | 2 +- ...ute-guide-sec06-step07-list-features.swift | 2 +- ...oute-guide-sec06-step08-record-route.swift | 2 +- .../route-guide-sec06-step09-route-chat.swift | 2 +- .../Route-Guide/Route-Guide.tutorial | 15 +- .../Documentation.docc/Documentation.md | 17 +++ .../InProcessTransport+Server.swift | 2 +- 36 files changed, 225 insertions(+), 260 deletions(-) create mode 100644 Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md diff --git a/.spi.yml b/.spi.yml index 6db16665f..b6434a60a 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,5 +1,9 @@ version: 1 builder: configs: - - documentation_targets: [GRPC, GRPCReflectionService, protoc-gen-grpc-swift, GRPCCore] + - documentation_targets: [GRPCCore, GRPCCodeGen] swift_version: 6.0 + - documentation_targets: [GRPCInProcessTransport] + swift_version: 6.0 + # Don't include @_exported types from GRPCCore + custom_documentation_parameters: [--exclude-extended-types] diff --git a/README.md b/README.md index df7eced27..0d1d9b5fa 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,47 @@ # gRPC Swift -This repository contains a gRPC code generator and runtime libraries for Swift. -You can read more about gRPC on the [gRPC project's website][grpcio]. - -## Versions - -gRPC Swift is currently undergoing active development to take full advantage of -Swift's native concurrency features. The culmination of this work will be a new -major version, v2.x. Pre-release versions will be available in the near future. - -In the meantime, v1.x is available and still supported. You can read more about -it on the [Swift Package Index][spi-grpc-swift-main]. - -## Security - -Please see [SECURITY.md](SECURITY.md). - -## License - -gRPC Swift is released under the same license as [gRPC][gh-grpc], repeated in -[LICENSE](LICENSE). - -## Contributing - -Please get involved! See our [guidelines for contributing](CONTRIBUTING.md). +This repository contains a gRPC implementation for Swift. You can read more +about gRPC on the [gRPC project's website][grpcio]. + +> gRPC Swift v2.x is under active development on the `main` branch and takes +> full advantage of Swift's native concurrency features. +> +> v1.x is still supported and maintained on the `release/1.x` branch. + +- 📚 **Documentation** and **tutorials** are available on the [Swift Package Index][spi-grpc-swift] +- 💻 **Examples** are available in the [Examples](Examples) directory +- 🚀 **Contributions** are welcome, please see [CONTRIBUTING.md](CONTRIBUTING.md) +- 🪪 **License** is Apache 2.0, repeated in [LICENSE](License) +- 🔒 **Security** issues should be reported via the process in [SECURITY.md](SECURITY.md) + +## Quick Start + +The following snippet contains a Swift Package manifest to use gRPC Swift v2.x with +the SwiftNIO based transport and SwiftProtobuf serialization: + +```swift +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "foo-package", + platforms: [.macOS("15.0")], + dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), + ], + targets: [ + .executableTarget( + name: "bar-target", + dependencies: [ + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), + ] + ) + ] +) +``` [gh-grpc]: https://github.com/grpc/grpc [grpcio]: https://grpc.io -[spi-grpc-swift-main]: https://swiftpackageindex.com/grpc/grpc-swift/main/documentation/grpccore +[spi-grpc-swift]: https://swiftpackageindex.com/grpc/grpc-swift/documentation diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index 2243fe8f2..c10c166ca 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -23,11 +23,9 @@ /// /// Interceptors are registered with the server apply to all RPCs. If you need to modify the /// behavior of an interceptor on a per-RPC basis then you can use the -/// ``ServerInterceptorContext/descriptor`` to determine which RPC is being called and +/// ``ServerContext/descriptor`` to determine which RPC is being called and /// conditionalise behavior accordingly. /// -/// - TODO: Update example and documentation to show how to register an interceptor. -/// /// ## RPC filtering /// /// A common use of server-side interceptors is to filter requests from clients. Interceptors can diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md index 34b57f8d2..c55691a36 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md @@ -2,129 +2,14 @@ Learn how to generate stubs for gRPC Swift from a service defined using the Protocol Buffers IDL. -## Overview - -There are two approaches to generating stubs from Protocol Buffers: - -1. With the Swift Package Manager build plugin, or -2. With the Protocol Buffers compiler (`protoc`). - -The following sections describe how and when to use each. - -### Using the Swift Package Manager build plugin - -You can generate stubs at build time by using `GRPCSwiftPlugin` which is a build plugin for the -Swift Package Manager. Using it means that you don't have to manage the generation of -stubs with separate tooling, or check the generated stubs into your source repository. - -The build plugin will generate gRPC stubs for you by building `protoc-gen-grpc-swift` (more details -in the following section) for you and invoking `protoc`. Because of the implicit -dependency on `protoc` being made available by the system `GRPCSwiftPlugin` isn't suitable for use -in: - -- Library packages, or -- Environments where `protoc` isn't available. - -> `GRPCSwiftPlugin` _only_ generates gRPC stubs, it doesn't generate messages. You must generate -> messages in addition to the gRPC Stubs. The [Swift Protobuf](https://github.com/apple/swift-protobuf) -> project provides an equivalent build plugin, `SwiftProtobufPlugin`, for this. - -#### Configuring the build plugin - -You can configure which stubs `GRPCSwiftPlugin` generates and how via a configuration file. This -must be called `grpc-swift-config.json` and can be placed anywhere in the source directory for your -target. - -A config file for the plugin is made up of a number of `protoc` invocations. Each invocation -describes the inputs to `protoc` as well as any options. - -The following is a list of options which can be applied to each invocation object: -- `protoFiles`, an array of strings where each string is the path to an input `.proto` file - _relative to `grpc-swift-config.json`_. -- `visibility`, a string describing the access level of the generated stub (must be one - of `"public"`, `"internal"`, or `"package"`). If not specified then stubs are generated as - `internal`. -- `server`, a boolean indicating whether server stubs should be generated. Defaults to `true` if - not specified. -- `client`, a boolean indicating whether client stubs should be generated. Defaults to `true` if - not specified. -- `_V2`, a boolean indicated whether the generated stubs should be for v2.x. Defaults to `false` if - not specified. - -> The `GRPCSwiftPlugin` build plugin is currently shared between gRPC Swift v1.x and v2.x. To -> generate stubs for v2.x you _must_ set `_V2` to `true` in your config. -> -> This option will be deprecated and removed once v2.x has been released. - -#### Finding protoc - -The build plugin requires a copy of the `protoc` binary to be available. To resolve which copy of -the binary to use, `GRPCSwiftPlugin` will look at the following in order: - -1. The exact path specified in the `protocPath` property in `grpc-swift-config.json`, if present. -2. The exact path specified in the `PROTOC_PATH` environment variable, if set. -3. The first `protoc` binary found in your `PATH` environment variable. - -#### Using the build plugin from Xcode - -Xcode doesn't have access to your `PATH` so in order to use `GRPCSwiftPlugin` with Xcode you must -either set `protocPath` in your `grpc-swift-config.json` or explicitly set `PROTOC_PATH` when -opening Xcode. - -You can do this by running: - -```sh -env PROTOC_PATH=/path/to/protoc xed /path/to/your-project -``` - -Note that Xcode must _not_ be open before running this command. - -#### Example configuration - -We recommend putting your config and `.proto` files in a directory called `Protos` within your -target. Here's an example package structure: - -``` -MyPackage -├── Package.swift -└── Sources - └── MyTarget - └── Protos - ├── foo - │   └── bar - │   ├── baz.proto - │   └── buzz.proto - └── grpc-swift-config.json -``` - -If you wanted the generated stubs from `baz.proto` to be `public`, and to only generate a client -for `buzz.proto` then the `grpc-swift-config` could look like this: - -```json -{ - "invocations": [ - { - "_V2": true, - "protoFiles": ["foo/bar/baz.proto"], - "visibility": "public" - }, - { - "_V2": true, - "protoFiles": ["foo/bar/buzz.proto"], - "server": false - } - ] -} -``` - -### Using protoc +## Using protoc If you've used Protocol Buffers before then generating gRPC Swift stubs should be simple. If you're unfamiliar with Protocol Buffers then you should get comfortable with the concepts before continuing; the [Protocol Buffers website](https://protobuf.dev/) is a great place to start. -gRPC Swift provides `protoc-gen-grpc-swift`, a program which is a plugin for the Protocol Buffers -compiler, `protoc`. +The [`grpc-swift-protobuf`](https://github.com/grpc/grpc-swift-protobuf) package provides +`protoc-gen-grpc-swift`, a program which is a plugin for the Protocol Buffers compiler, `protoc`. > `protoc-gen-grpc-swift` only generates gRPC stubs, it doesn't generate messages. You must use > `protoc-gen-swift` to generate messages in addition to gRPC Stubs. @@ -160,22 +45,16 @@ protoc \ --grpc-swift_out=. ``` -#### Generator options +### Generator options | Name | Possible Values | Default | Description | |---------------------------|--------------------------------------------|------------|----------------------------------------------------------| -| `_V2` | `True`, `False` | `False` | Whether stubs are generated for gRPC Swift v2.x | | `Visibility` | `Public`, `Package`, `Internal` | `Internal` | Access level for generated stubs | | `Server` | `True`, `False` | `True` | Generate server stubs | | `Client` | `True`, `False` | `True` | Generate client stubs | | `FileNaming` | `FullPath`, `PathToUnderscore`, `DropPath` | `FullPath` | How generated source files should be named. (See below.) | | `ProtoPathModuleMappings` | | | Path to module map `.asciipb` file. (See below.) | -| `AccessLevelOnImports` | `True`, `False` | `True` | Whether imports should have explicit access levels. | - -> The `protoc-gen-grpc-swift` binary is currently shared between gRPC Swift v1.x and v2.x. To -> generate stubs for v2.x you _must_ specify `_V2=True`. -> -> This option will be deprecated and removed once v2.x has been released. +| `UseAccessLevelOnImports` | `True`, `False` | `False` | Whether imports should have explicit access levels. | The `FileNaming` option has three possible values, for an input of `foo/bar/baz.proto` the following output file will be generated: @@ -188,12 +67,12 @@ allows you to specify a mapping from `.proto` files to the Swift module they are allows the code generator to add appropriate imports to your generated stubs. This is described in more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md). -#### Building the plugin +### Building the plugin > The version of `protoc-gen-grpc-swift` you use mustn't be newer than the version of -> the `grpc-swift` you're using. +> the `grpc-swift-protobuf` you're using. -If your package depends on `grpc-swift` then you can get a copy of `protoc-gen-grpc-swift` +If your package depends on `grpc-swift-protobuf` then you can get a copy of `protoc-gen-grpc-swift` by building it directly: ```console diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index de33f0bb1..4ccc4b5f2 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -7,17 +7,38 @@ A gRPC library for Swift written natively in Swift. ## Package structure -gRPC Swift is made up of a number of modules, each of which is documented separately. However this -module – ``GRPCCore`` – includes higher level documentation such as tutorials. The following list -contains products of this package: - -- ``GRPCCore`` contains core types and abstractions and is the 'base' module for the project. -- `GRPCInProcessTransport` contains an implementation of an in-process transport. -- `GRPCHTTP2TransportNIOPosix` provides client and server implementations of HTTP/2 transports built - on top of SwiftNIO's POSIX Sockets abstractions. -- `GRPCHTTP2TransportNIOTransportServices` provides client and server implementations of HTTP/2 - transports built on top of SwiftNIO's Network.framework abstraction, `NIOTransportServices`. -- `GRPCProtobuf` provides serialization and deserialization components for `SwiftProtobuf`. +gRPC Swift is distributed across multiple Swift packages. These are: + +- `grpc-swift` (this package) containing core gRPC abstractions and an in-process transport. + - GitHub repository: [`grpc/grpc-swift`](https://github.com/grpc/grpc-swift) + - Documentation: hosted on the [Swift Package + Index](https://swiftpackageindex.com/grpc/grpc-swift/documentation) +- `grpc-swift-nio-transport` contains high-performance HTTP/2 transports built on top + of [SwiftNIO](https://github.com/apple/swift-nio). + - GitHub repository: [`grpc/grpc-swift-nio-transport`](https://github.com/grpc/grpc-swift-nio-transport) + - Documentation: hosted on the [Swift Package + Index](https://swiftpackageindex.com/grpc/grpc-swift-nio-transport/documentation) +- `grpc-swift-protobuf` contains runtime serialization components to interoperate with + [SwiftProtobuf](https://github.com/apple/swift-protobuf) as well as a plugin for the Protocol + Buffers compiler, `protoc`. + - GitHub repository: [`grpc/grpc-swift-protobuf`](https://github.com/grpc/grpc-swift-protobuf) + - Documentation: hosted on the [Swift Package + Index](https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation) +- `grpc-swift-extras` contains optional runtime components and integrations with other packages. + - GitHub repository: [`grpc/grpc-swift-extras`](https://github.com/grpc/grpc-swift-extras) + - Documentation: hosted on the [Swift Package + Index](https://swiftpackageindex.com/grpc/grpc-swift-extras/documentation) + +This package, and this module (``GRPCCore``) in particular, include higher level documentation such +as tutorials. + +## Modules in this package + +- ``GRPCCore`` (this module) contains core abstractions, currency types and runtime components + for gRPC Swift. +- `GRPCInProcessTransport` contains an in-process implementation of the ``ClientTransport`` and + ``ServerTransport`` protocols. +- `GRPCodeGen` contains components for building a code generator. ## Topics diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial index 32ecd4847..e3e4241a4 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Hello-World.tutorial @@ -21,8 +21,8 @@ git clone https://github.com/grpc/grpc-swift ``` - The rest of the tutorial assumes that your current working directory is the cloned `grpc-swift` - directory. + You then need to change directory to the `Examples/hello-world` directory of the cloned + repository. The rest of the tutorial assumes this is the current working directory. } @Section(title: "Run a gRPC application") { @@ -60,8 +60,7 @@ @Steps { @Step { - Open `HelloWorld.proto` in the `Examples/v2/hello-world` directory to see how the - service is defined. + Open `HelloWorld.proto` in to see how the service is defined. @Code(name: "HelloWorld.proto", file: "hello-world-sec03-step01.proto") } @@ -77,10 +76,10 @@ @Section(title: "Update and run the application") { You need to regenerate the stubs as the service definition has changed. To do this run the - following command from the root of the checked out repository: + following command from the _root of the checked out repository_: ```console - Protos/generate.sh + dev/protos/generate.sh ``` To learn how to generate stubs check out the article. @@ -90,7 +89,7 @@ @Steps { @Step { - Open `Serve.swift` in the `Examples/v2/hello-world/Subcommands` directory. + Open `Serve.swift` in the `Subcommands` directory. @Code(name: "Serve.swift", file: "hello-world-sec04-step01.swift") } @@ -102,8 +101,7 @@ } @Step { - Let's update the client now. Open `Greet.swift` in the - `Examples/v2/hello-world/Subcommands` directory. + Let's update the client now. Open `Greet.swift` in the `Subcommands` directory. @Code(name: "Greet.swift", file: "hello-world-sec04-step03.swift") } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift index 2c89bab8d..ef1120c64 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift @@ -1,6 +1,7 @@ struct Greeter: Helloworld_GreeterServiceProtocol { func sayHello( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift index e9dde27f9..44e571bec 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift @@ -1,6 +1,7 @@ struct Greeter: Helloworld_GreeterServiceProtocol { func sayHello( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name @@ -9,7 +10,8 @@ struct Greeter: Helloworld_GreeterServiceProtocol { } func sayHelloAgain( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift index baaffab21..f475a700c 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step07-description.swift @@ -5,8 +5,8 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), - .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), ], targets: [] ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift index c9f71095f..09de19627 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -5,16 +5,15 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), - .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), ], targets: [ .executableTarget( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ] ) ] diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt index ab2c22fd8..89d320b69 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec03-step04-gen-grpc.txt @@ -1,5 +1,4 @@ $ protoc --plugin=.build/debug/protoc-gen-grpc-swift \ -I Protos \ --grpc-swift_out=Sources/Generated \ - --grpc-swift_opt=_V2=true \ Protos/route_guide.proto diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift index de6f415cd..b2651be95 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift @@ -2,22 +2,26 @@ import GRPCCore struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift index 99bb69ad7..46925a6bb 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift @@ -11,22 +11,26 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift index cf791d6f8..11fbc8215 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift @@ -18,7 +18,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -27,17 +28,20 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift index 452c74a85..d2d82d553 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift @@ -18,7 +18,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -41,17 +42,20 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift index b52288fa3..b833d72f8 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift @@ -18,7 +18,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -41,7 +42,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for feature in self.features { @@ -53,12 +55,14 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift index 780c63a14..6c64829e5 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift @@ -18,7 +18,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -41,7 +42,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for feature in self.features { @@ -55,12 +57,14 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift index 76f399c50..d9221cd01 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift @@ -19,7 +19,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -42,7 +43,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for feature in self.features { @@ -56,7 +58,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { let startTime = ContinuousClock.now var pointsVisited = 0 @@ -90,7 +93,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift index 5c8ad560d..c8f9010b5 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift @@ -50,7 +50,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -73,7 +74,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for feature in self.features { @@ -87,7 +89,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { let startTime = ContinuousClock.now var pointsVisited = 0 @@ -121,7 +124,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift index 3913b0c36..8f4027b5e 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift @@ -50,7 +50,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Single { let feature = self.findFeature( latitude: request.message.latitude, @@ -73,7 +74,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single + request: ServerRequest.Single, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for feature in self.features { @@ -87,7 +89,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Single { let startTime = ContinuousClock.now var pointsVisited = 0 @@ -121,7 +124,8 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func routeChat( - request: ServerRequest.Stream + request: ServerRequest.Stream, + context: ServerContext ) async throws -> ServerResponse.Stream { return ServerResponse.Stream { writer in for try await note in request.messages { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index c9f71095f..09de19627 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -5,16 +5,15 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), - .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), ], targets: [ .executableTarget( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ] ) ] diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index 05447cfb9..aac2037d5 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -5,16 +5,15 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), - .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), ], targets: [ .executableTarget( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ], resources: [ .copy("route_guide_db.json") diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index 1a49f098d..13c5b9816 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -5,17 +5,16 @@ let package = Package( name: "RouteGuide", platforms: [.macOS(.v15)], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift", branch: "main"), - .package(url: "https://github.com/apple/swift-protobuf", from: "1.27.0"), + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ .executableTarget( name: "RouteGuide", dependencies: [ - .product(name: "_GRPCHTTP2Transport", package: "grpc-swift"), - .product(name: "_GRPCProtobuf", package: "grpc-swift"), - .product(name: "SwiftProtobuf", package: "swift-protobuf"), + .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), + .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "ArgumentParser", package: "swift-argument-parser"), ], resources: [ diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift index b3ffcaf33..a9797e762 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runServer() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift index b99885335..ac8280a58 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runServer() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift index eee2b6838..55d8b69ee 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift index ac97861b9..a5f51df0a 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift index 76332bada..990676047 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift index e231432bc..693b6993c 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift index 5d4e5e725..deb719eda 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift index d22daa343..b36d22e88 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift index 6b5ecaba7..8068f6c31 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift index a0fa49c55..401bf06ec 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift @@ -1,4 +1,4 @@ -import GRPCHTTP2Transport +import GRPCNIOTransportHTTP2 extension RouteGuide { func runClient() async throws { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial index 52b404049..0380b6540 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -65,7 +65,7 @@ @Step { We need to add a dependency on the gRPC Swift and Swift Protobuf packages. As gRPC Swift v2 - hasn't yet been released the dependency must be on the `main` branch. + hasn't yet been released the dependencies must use the `-alpha` tags. Note that we also add `.macOS(.v15)` to platforms, this is the earliest macOS version supported by gRPC Swift v2. @@ -77,13 +77,10 @@ Next we can add a target. In this tutorial we'll create a single executable target which can act as both a client and a server. - We require two gRPC dependencies: `_GRPCHTTP2Transport"` provides an implementation of an HTTP/2 - client and server, while `_GRPCProtobuf` provides the necessary components to serialize + We require two gRPC dependencies: `GRPCNIOTransportHTTP2"` provides an implementation of an HTTP/2 + client and server, while `GRPCProtobuf` provides the necessary components to serialize and deserialize `SwiftProtobuf` messages. - > Because gRPC Swift v2 hasn't been released yet the product names are prefixed - > with an `"_"`; this indicates that they aren't yet considered as stable public API. - @Code(name: "Package.swift", file: "route-guide-sec01-step08-description.swift") } } @@ -320,7 +317,7 @@ @Steps { @Step { First we need to download a database of known features for the service. Copy - `route_guide_db.json` from the [gRPC Swift repository](https://github.com/grpc/grpc-swift/tree/main/Examples/v2/route-guide) + `route_guide_db.json` from the [gRPC Swift repository](https://github.com/grpc/grpc-swift/tree/main/Examples/route-guide) and place it in the `Sources` directory. } @@ -331,7 +328,7 @@ } @Step { - Now add a `resouces` argument to copy the `route_guide_db.json` database. + Now add a `resources` argument to copy the `route_guide_db.json` database. @Code(name: "Package.swift", file: "route-guide-sec05-step01-package.swift") } @@ -379,7 +376,7 @@ The server is instantiated with a transport which provides the communication with a remote peer. - We'll use an HTTP/2 transport provided by the `GRPCHTTP2Transport` module which is + We'll use an HTTP/2 transport provided by the `GRPCNIOTransportHTTP2` module which is implemented on top of SwiftNIO's Posix sockets abstraction. The server is configured to bind to "127.0.0.1:31415" and use the default configuration. The `.plaintext` transport security means that the connections won't use TLS and will be unencrypted. diff --git a/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md b/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md new file mode 100644 index 000000000..c57b262b9 --- /dev/null +++ b/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md @@ -0,0 +1,17 @@ +# ``GRPCInProcessTransport`` + +This module contains an in-process transport. + +## Overview + +The in-process transport allows you to run a gRPC client and server within the same process +without using a networking stack. This is great for testing but is also suitable for production +use cases. + +## Topics + +### Transports + +- ``InProcessTransport`` +- ``InProcessClientTransport`` +- ``InProcessServerTransport`` diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 0637255af..5079fad5c 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -23,7 +23,7 @@ extension InProcessTransport { /// This is useful when you're interested in testing your application without any actual networking layers /// involved, as the client and server will communicate directly with each other via in-process streams. /// - /// To use this server, you call ``listen(_:)`` and iterate over the returned `AsyncSequence` to get all + /// To use this server, you call ``listen(streamHandler:)`` and iterate over the returned `AsyncSequence` to get all /// RPC requests made from clients (as ``RPCStream``s). /// To stop listening to new requests, call ``beginGracefulShutdown()``. /// From 1e92fc8ca88503d46b35871fc4add7119a08f9b2 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 1 Oct 2024 11:02:14 +0100 Subject: [PATCH 477/580] Improve naming of request/response types (#2076) Motivation: The names of the request/response types are quite stilted. We can improve them by removing the namespacing and by removing the explicit "Single". Modifications: - `ServerRequest.Single` -> `ServerRequest` - `ServerRequest.Stream` -> `StreamingServerRequest` - `ServerResponse.Single` -> `ServerResponse` - `ServerResponse.Stream` -> `StreamingServerResponse` - `ClientRequest.Single` -> `ClientRequest` - `ClientRequest.Stream` -> `StreamingClientRequest` - `ClientResponse.Single` -> `ClientResponse` - `ClientResponse.Stream` -> `StreamingClientResponse` Result: Better naming --- .../Translator/ClientCodeTranslator.swift | 33 +- .../Translator/ServerCodeTranslator.swift | 42 +- .../Call/Client/ClientInterceptor.swift | 24 +- .../GRPCCore/Call/Client/ClientRequest.swift | 147 +++--- .../GRPCCore/Call/Client/ClientResponse.swift | 447 +++++++++--------- .../ClientRPCExecutor+HedgingExecutor.swift | 18 +- .../ClientRPCExecutor+OneShotExecutor.swift | 8 +- .../ClientRPCExecutor+RetryExecutor.swift | 8 +- .../Client/Internal/ClientRPCExecutor.swift | 20 +- .../Internal/ClientRequest+Convenience.swift | 4 +- .../Internal/ClientResponse+Convenience.swift | 14 +- .../Internal/ClientStreamExecutor.swift | 18 +- .../Server/Internal/ServerRPCExecutor.swift | 38 +- Sources/GRPCCore/Call/Server/RPCRouter.swift | 8 +- .../Call/Server/ServerInterceptor.swift | 16 +- .../GRPCCore/Call/Server/ServerRequest.swift | 89 ++-- .../GRPCCore/Call/Server/ServerResponse.swift | 387 ++++++++------- .../Resources/hello-world-sec04-step01.swift | 6 +- .../Resources/hello-world-sec04-step02.swift | 12 +- ...ute-guide-sec04-step02-unimplemented.swift | 16 +- .../route-guide-sec04-step03-features.swift | 16 +- .../route-guide-sec04-step04-unary.swift | 16 +- .../route-guide-sec04-step05-unary.swift | 20 +- ...-guide-sec04-step06-server-streaming.swift | 22 +- ...-guide-sec04-step07-server-streaming.swift | 22 +- ...-guide-sec04-step08-client-streaming.swift | 24 +- ...te-guide-sec04-step09-bidi-streaming.swift | 24 +- ...te-guide-sec04-step10-bidi-streaming.swift | 26 +- .../Route-Guide/Route-Guide.tutorial | 14 +- Sources/GRPCCore/GRPCClient.swift | 26 +- ...lientCodeTranslatorSnippetBasedTests.swift | 112 ++--- ...erverCodeTranslatorSnippetBasedTests.swift | 96 ++-- .../Call/Client/ClientRequestTests.swift | 4 +- .../Call/Client/ClientResponseTests.swift | 38 +- .../ClientRPCExecutorTestHarness.swift | 36 +- .../ClientRPCExecutorTests+Hedging.swift | 10 +- .../ClientRPCExecutorTests+Retries.swift | 18 +- .../Internal/ClientRPCExecutorTests.swift | 26 +- .../ServerRPCExecutorTestHarness.swift | 17 +- .../Internal/ServerRPCExecutorTests.swift | 10 +- .../Call/Server/ServerRequestTests.swift | 4 +- .../Call/Server/ServerResponseTests.swift | 19 +- Tests/GRPCCoreTests/GRPCClientTests.swift | 2 +- .../Call/Client/ClientInterceptors.swift | 18 +- .../Call/Server/ServerInterceptors.swift | 18 +- .../Test Utilities/Services/BinaryEcho.swift | 32 +- .../Test Utilities/XCTest+Utilities.swift | 4 +- 47 files changed, 1000 insertions(+), 1029 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 29285c243..1d935151f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -25,19 +25,19 @@ /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarClientProtocol: Sendable { /// func baz( -/// request: GRPCCore.ClientRequest.Single, +/// request: GRPCCore.ClientRequest, /// serializer: some GRPCCore.MessageSerializer, /// deserializer: some GRPCCore.MessageDeserializer, /// options: GRPCCore.CallOptions = .defaults, -/// _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R +/// _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R /// ) async throws -> R where R: Sendable /// } /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// extension Foo_Bar.ClientProtocol { /// public func baz( -/// request: GRPCCore.ClientRequest.Single, +/// request: GRPCCore.ClientRequest, /// options: GRPCCore.CallOptions = .defaults, -/// _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { +/// _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { /// try $0.message /// } /// ) async throws -> R where R: Sendable { @@ -56,11 +56,11 @@ /// self.client = client /// } /// public func methodA( -/// request: GRPCCore.ClientRequest.Stream, +/// request: GRPCCore.StreamingClientRequest, /// serializer: some GRPCCore.MessageSerializer, /// deserializer: some GRPCCore.MessageDeserializer, /// options: GRPCCore.CallOptions = .defaults, -/// _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { +/// _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { /// try $0.message /// } /// ) async throws -> R where R: Sendable { @@ -263,13 +263,13 @@ extension ClientCodeTranslator { // All methods have a response handler. var responseHandler = ParameterDescription(label: "onResponse", name: "handleResponse") - let responseKind = method.isOutputStreaming ? "Stream" : "Single" + let responseKind = method.isOutputStreaming ? "Streaming" : "" responseHandler.type = .closure( ClosureSignatureDescription( parameters: [ ParameterDescription( type: .generic( - wrapper: .member(["GRPCCore", "ClientResponse", responseKind]), + wrapper: .member(["GRPCCore", "\(responseKind)ClientResponse"]), wrapped: .member(method.outputType) ) ) @@ -299,21 +299,21 @@ extension ClientCodeTranslator { ) -> [CodeBlock] { // Produces the following: // - // let request = GRPCCore.ClientRequest.Single(message: message, metadata: metadata) + // let request = GRPCCore.ClientRequest(message: message, metadata: metadata) // return try await method(request: request, options: options, responseHandler: responseHandler) // // or: // - // let request = GRPCCore.ClientRequest.Stream(metadata: metadata, producer: writer) + // let request = GRPCCore.StreamingClientRequest(metadata: metadata, producer: writer) // return try await method(request: request, options: options, responseHandler: responseHandler) // First, make the init for the ClientRequest - let requestType = method.isInputStreaming ? "Stream" : "Single" + let requestType = method.isInputStreaming ? "Streaming" : "" var requestInit = FunctionCallDescription( calledExpression: .identifier( .type( .generic( - wrapper: .member(["GRPCCore", "ClientRequest", requestType]), + wrapper: .member(["GRPCCore", "\(requestType)ClientRequest"]), wrapped: .member(method.inputType) ) ) @@ -490,9 +490,10 @@ extension ClientCodeTranslator { for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, in service: CodeGenerationRequest.ServiceDescriptor ) -> ParameterDescription { - let requestType = method.isInputStreaming ? "Stream" : "Single" + let requestType = method.isInputStreaming ? "Streaming" : "" let clientRequestType = ExistingTypeDescription.member([ - "GRPCCore", "ClientRequest", requestType, + "GRPCCore", + "\(requestType)ClientRequest", ]) return ParameterDescription( label: "request", @@ -538,9 +539,9 @@ extension ClientCodeTranslator { in service: CodeGenerationRequest.ServiceDescriptor, includeDefaultResponseHandler: Bool ) -> ParameterDescription { - let clientStreaming = method.isOutputStreaming ? "Stream" : "Single" + let clientStreaming = method.isOutputStreaming ? "Streaming" : "" let closureParameterType = ExistingTypeDescription.generic( - wrapper: .member(["GRPCCore", "ClientResponse", clientStreaming]), + wrapper: .member(["GRPCCore", "\(clientStreaming)ClientResponse"]), wrapped: .member(method.outputType) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index d392bf5b0..100233d11 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -25,8 +25,8 @@ /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// func baz( -/// request: GRPCCore.ServerRequest.Stream -/// ) async throws -> GRPCCore.ServerResponse.Stream +/// request: GRPCCore.StreamingServerRequest +/// ) async throws -> GRPCCore.StreamingServerResponse /// } /// // Conformance to `GRPCCore.RegistrableRPCService`. /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @@ -43,17 +43,17 @@ /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// public protocol Foo_BarServiceProtocol: Foo_Bar.StreamingServiceProtocol { /// func baz( -/// request: GRPCCore.ServerRequest.Single -/// ) async throws -> GRPCCore.ServerResponse.Single +/// request: GRPCCore.ServerRequest +/// ) async throws -> GRPCCore.ServerResponse /// } /// // Partial conformance to `Foo_BarStreamingServiceProtocol`. /// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) /// extension Foo_Bar.ServiceProtocol { /// public func baz( -/// request: GRPCCore.ServerRequest.Stream -/// ) async throws -> GRPCCore.ServerResponse.Stream { -/// let response = try await self.baz(request: GRPCCore.ServerRequest.Single(stream: request)) -/// return GRPCCore.ServerResponse.Stream(single: response) +/// request: GRPCCore.StreamingServerRequest +/// ) async throws -> GRPCCore.StreamingServerResponse { +/// let response = try await self.baz(request: GRPCCore.ServerRequest(stream: request)) +/// return GRPCCore.StreamingServerResponse(single: response) /// } /// } ///``` @@ -147,7 +147,7 @@ extension ServerCodeTranslator { .init( label: "request", type: .generic( - wrapper: .member(["GRPCCore", "ServerRequest", "Stream"]), + wrapper: .member(["GRPCCore", "StreamingServerRequest"]), wrapped: .member(method.inputType) ) ), @@ -156,7 +156,7 @@ extension ServerCodeTranslator { keywords: [.async, .throws], returnType: .identifierType( .generic( - wrapper: .member(["GRPCCore", "ServerResponse", "Stream"]), + wrapper: .member(["GRPCCore", "StreamingServerResponse"]), wrapped: .member(method.outputType) ) ) @@ -313,8 +313,8 @@ extension ServerCodeTranslator { in service: CodeGenerationRequest.ServiceDescriptor, accessModifier: AccessModifier? = nil ) -> Declaration { - let inputStreaming = method.isInputStreaming ? "Stream" : "Single" - let outputStreaming = method.isOutputStreaming ? "Stream" : "Single" + let inputStreaming = method.isInputStreaming ? "Streaming" : "" + let outputStreaming = method.isOutputStreaming ? "Streaming" : "" let functionSignature = FunctionSignatureDescription( accessModifier: accessModifier, @@ -324,7 +324,7 @@ extension ServerCodeTranslator { label: "request", type: .generic( - wrapper: .member(["GRPCCore", "ServerRequest", inputStreaming]), + wrapper: .member(["GRPCCore", "\(inputStreaming)ServerRequest"]), wrapped: .member(method.inputType) ) ), @@ -333,7 +333,7 @@ extension ServerCodeTranslator { keywords: [.async, .throws], returnType: .identifierType( .generic( - wrapper: .member(["GRPCCore", "ServerResponse", outputStreaming]), + wrapper: .member(["GRPCCore", "\(outputStreaming)ServerResponse"]), wrapped: .member(method.outputType) ) ) @@ -391,12 +391,7 @@ extension ServerCodeTranslator { if !method.isInputStreaming { // Transform the streaming request into a unary request. serverRequest = Expression.functionCall( - calledExpression: .memberAccess( - MemberAccessDescription( - left: .identifierPattern("GRPCCore.ServerRequest"), - right: "Single" - ) - ), + calledExpression: .identifierType(.member(["GRPCCore", "ServerRequest"])), arguments: [ FunctionArgumentDescription(label: "stream", expression: .identifierPattern("request")) ] @@ -433,12 +428,7 @@ extension ServerCodeTranslator { // Transforming the unary response into a streaming one. if !method.isOutputStreaming { returnValue = .functionCall( - calledExpression: .memberAccess( - MemberAccessDescription( - left: .identifierType(.member(["GRPCCore", "ServerResponse"])), - right: "Stream" - ) - ), + calledExpression: .identifier(.type(.member(["GRPCCore", "StreamingServerResponse"]))), arguments: [ (FunctionArgumentDescription(label: "single", expression: .identifierPattern("response"))) ] diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 93ddf9cf5..2a81a0ddc 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -40,13 +40,13 @@ /// let fetchMetadata: @Sendable () async -> String /// /// func intercept( -/// request: ClientRequest.Stream, +/// request: StreamingClientRequest, /// context: ClientContext, /// next: @Sendable ( -/// _ request: ClientRequest.Stream, +/// _ request: StreamingClientRequest, /// _ context: ClientContext -/// ) async throws -> ClientResponse.Stream -/// ) async throws -> ClientResponse.Stream { +/// ) async throws -> StreamingClientResponse +/// ) async throws -> StreamingClientResponse { /// // Fetch the metadata value and attach it. /// let value = await self.fetchMetadata() /// var request = request @@ -65,13 +65,13 @@ /// ```swift /// struct LoggingClientInterceptor: ClientInterceptor { /// func intercept( -/// request: ClientRequest.Stream, +/// request: StreamingClientRequest, /// context: ClientContext, /// next: @Sendable ( -/// _ request: ClientRequest.Stream, +/// _ request: StreamingClientRequest, /// _ context: ClientContext -/// ) async throws -> ClientResponse.Stream -/// ) async throws -> ClientResponse.Stream { +/// ) async throws -> StreamingClientResponse +/// ) async throws -> StreamingClientResponse { /// print("Invoking method '\(context.descriptor)'") /// let response = try await next(request, context) /// @@ -100,11 +100,11 @@ public protocol ClientInterceptor: Sendable { /// interceptor in the chain. /// - Returns: A response object. func intercept( - request: ClientRequest.Stream, + request: StreamingClientRequest, context: ClientContext, next: ( - _ request: ClientRequest.Stream, + _ request: StreamingClientRequest, _ context: ClientContext - ) async throws -> ClientResponse.Stream - ) async throws -> ClientResponse.Stream + ) async throws -> StreamingClientResponse + ) async throws -> StreamingClientResponse } diff --git a/Sources/GRPCCore/Call/Client/ClientRequest.swift b/Sources/GRPCCore/Call/Client/ClientRequest.swift index 17e5e1077..fbbf686e6 100644 --- a/Sources/GRPCCore/Call/Client/ClientRequest.swift +++ b/Sources/GRPCCore/Call/Client/ClientRequest.swift @@ -14,91 +14,84 @@ * limitations under the License. */ -/// A namespace for request message types used by clients. -public enum ClientRequest {} - -extension ClientRequest { - /// A request created by the client for a single message. - /// - /// This is used for unary and server-streaming RPCs. - /// - /// See ``ClientRequest/Stream`` for streaming requests and ``ServerRequest/Single`` for the - /// servers representation of a single-message request. - /// - /// ## Creating ``Single`` requests +/// A request created by the client for a single message. +/// +/// This is used for unary and server-streaming RPCs. +/// +/// See ``StreamingClientRequest`` for streaming requests and ``ServerRequest`` for the +/// servers representation of a single-message request. +/// +/// ## Creating ``Single`` requests +/// +/// ```swift +/// let request = ClientRequest(message: "Hello, gRPC!") +/// print(request.metadata) // prints '[:]' +/// print(request.message) // prints 'Hello, gRPC!' +/// ``` +public struct ClientRequest: Sendable { + /// Caller-specified metadata to send to the server at the start of the RPC. /// - /// ```swift - /// let request = ClientRequest.Single(message: "Hello, gRPC!") - /// print(request.metadata) // prints '[:]' - /// print(request.message) // prints 'Hello, gRPC!' - /// ``` - public struct Single: Sendable { - /// Caller-specified metadata to send to the server at the start of the RPC. - /// - /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with - /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert - /// their own metadata, you should avoid using key names which may clash with transport specific - /// metadata. Note that transports may also impose limits in the amount of metadata which may - /// be sent. - public var metadata: Metadata + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport specific + /// metadata. Note that transports may also impose limits in the amount of metadata which may + /// be sent. + public var metadata: Metadata - /// The message to send to the server. - public var message: Message + /// The message to send to the server. + public var message: Message - /// Create a new single client request. - /// - /// - Parameters: - /// - message: The message to send to the server. - /// - metadata: Metadata to send to the server at the start of the request. Defaults to empty. - public init( - message: Message, - metadata: Metadata = [:] - ) { - self.metadata = metadata - self.message = message - } + /// Create a new single client request. + /// + /// - Parameters: + /// - message: The message to send to the server. + /// - metadata: Metadata to send to the server at the start of the request. Defaults to empty. + public init( + message: Message, + metadata: Metadata = [:] + ) { + self.metadata = metadata + self.message = message } } -extension ClientRequest { - /// A request created by the client for a stream of messages. - /// - /// This is used for client-streaming and bidirectional-streaming RPCs. +/// A request created by the client for a stream of messages. +/// +/// This is used for client-streaming and bidirectional-streaming RPCs. +/// +/// See ``ClientRequest`` for single-message requests and ``StreamingServerRequest`` for the +/// servers representation of a streaming-message request. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct StreamingClientRequest: Sendable { + /// Caller-specified metadata sent to the server at the start of the RPC. /// - /// See ``ClientRequest/Single`` for single-message requests and ``ServerRequest/Stream`` for the - /// servers representation of a streaming-message request. - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - public struct Stream: Sendable { - /// Caller-specified metadata sent to the server at the start of the RPC. - /// - /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with - /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert - /// their own metadata, you should avoid using key names which may clash with transport specific - /// metadata. Note that transports may also impose limits in the amount of metadata which may - /// be sent. - public var metadata: Metadata + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport specific + /// metadata. Note that transports may also impose limits in the amount of metadata which may + /// be sent. + public var metadata: Metadata - /// A closure which, when called, writes messages in the writer. - /// - /// The producer will only be consumed once by gRPC and therefore isn't required to be - /// idempotent. If the producer throws an error then the RPC will be cancelled. Once the - /// producer returns the request stream is closed. - public var producer: @Sendable (RPCWriter) async throws -> Void + /// A closure which, when called, writes messages in the writer. + /// + /// The producer will only be consumed once by gRPC and therefore isn't required to be + /// idempotent. If the producer throws an error then the RPC will be cancelled. Once the + /// producer returns the request stream is closed. + public var producer: @Sendable (RPCWriter) async throws -> Void - /// Create a new streaming client request. - /// - /// - Parameters: - /// - messageType: The type of message contained in this request, defaults to `Message.self`. - /// - metadata: Metadata to send to the server at the start of the request. Defaults to empty. - /// - producer: A closure which writes messages to send to the server. The closure is called - /// at most once and may not be called. - public init( - of messageType: Message.Type = Message.self, - metadata: Metadata = [:], - producer: @escaping @Sendable (RPCWriter) async throws -> Void - ) { - self.metadata = metadata - self.producer = producer - } + /// Create a new streaming client request. + /// + /// - Parameters: + /// - messageType: The type of message contained in this request, defaults to `Message.self`. + /// - metadata: Metadata to send to the server at the start of the request. Defaults to empty. + /// - producer: A closure which writes messages to send to the server. The closure is called + /// at most once and may not be called. + public init( + of messageType: Message.Type = Message.self, + metadata: Metadata = [:], + producer: @escaping @Sendable (RPCWriter) async throws -> Void + ) { + self.metadata = metadata + self.producer = producer } } diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index a031933fd..dd2e71a38 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -14,255 +14,248 @@ * limitations under the License. */ -/// A namespace for response message types used by clients. -public enum ClientResponse {} - -extension ClientResponse { - /// A response for a single message received by a client. - /// - /// Single responses are used for unary and client-streaming RPCs. For streaming responses - /// see ``ClientResponse/Stream``. - /// - /// A single response captures every part of the response stream and distinguishes successful - /// and unsuccessful responses via the ``accepted`` property. The value for the `success` case - /// contains the initial metadata, response message, and the trailing metadata and implicitly - /// has an ``Status/Code-swift.struct/ok`` status code. - /// - /// The `failure` case indicates that the server chose not to process the RPC, or the processing - /// of the RPC failed, or the client failed to execute the request. The failure case contains - /// an ``RPCError`` describing why the RPC failed, including an error code, error message and any - /// metadata sent by the server. - /// - /// ### Using ``Single`` responses - /// - /// Each response has a ``accepted`` property which contains all RPC information. You can create - /// one by calling ``init(accepted:)`` or one of the two convenience initializers: - /// - ``init(message:metadata:trailingMetadata:)`` to create a successful response, or - /// - ``init(of:error:)`` to create a failed response. - /// - /// You can interrogate a response by inspecting the ``accepted`` property directly or by using - /// its convenience properties: - /// - ``metadata`` extracts the initial metadata, - /// - ``message`` extracts the message, or throws if the response failed, and - /// - ``trailingMetadata`` extracts the trailing metadata. - /// - /// The following example demonstrates how you can use the API: - /// - /// ```swift - /// // Create a successful response - /// let response = ClientResponse.Single( - /// message: "Hello, World!", - /// metadata: ["hello": "initial metadata"], - /// trailingMetadata: ["goodbye": "trailing metadata"] - /// ) - /// - /// // The explicit API: - /// switch response { - /// case .success(let contents): - /// print("Received response with message '\(try contents.message.get())'") - /// case .failure(let error): - /// print("RPC failed with code '\(error.code)'") - /// } - /// - /// // The convenience API: - /// do { - /// print("Received response with message '\(try response.message)'") - /// } catch let error as RPCError { - /// print("RPC failed with code '\(error.code)'") - /// } - /// ``` - public struct Single: Sendable { - /// The contents of an accepted response with a single message. - public struct Contents: Sendable { - /// Metadata received from the server at the beginning of the response. - /// - /// The metadata may contain transport-specific information in addition to any application - /// level metadata provided by the service. - public var metadata: Metadata - - /// The response message received from the server, or an error of the RPC failed with a - /// non-ok status. - public var message: Result - - /// Metadata received from the server at the end of the response. - /// - /// The metadata may contain transport-specific information in addition to any application - /// level metadata provided by the service. - public var trailingMetadata: Metadata +/// A response for a single message received by a client. +/// +/// Single responses are used for unary and client-streaming RPCs. For streaming responses +/// see ``StreamingClientResponse``. +/// +/// A single response captures every part of the response stream and distinguishes successful +/// and unsuccessful responses via the ``accepted`` property. The value for the `success` case +/// contains the initial metadata, response message, and the trailing metadata and implicitly +/// has an ``Status/Code-swift.struct/ok`` status code. +/// +/// The `failure` case indicates that the server chose not to process the RPC, or the processing +/// of the RPC failed, or the client failed to execute the request. The failure case contains +/// an ``RPCError`` describing why the RPC failed, including an error code, error message and any +/// metadata sent by the server. +/// +/// ### Using ``Single`` responses +/// +/// Each response has a ``accepted`` property which contains all RPC information. You can create +/// one by calling ``init(accepted:)`` or one of the two convenience initializers: +/// - ``init(message:metadata:trailingMetadata:)`` to create a successful response, or +/// - ``init(of:error:)`` to create a failed response. +/// +/// You can interrogate a response by inspecting the ``accepted`` property directly or by using +/// its convenience properties: +/// - ``metadata`` extracts the initial metadata, +/// - ``message`` extracts the message, or throws if the response failed, and +/// - ``trailingMetadata`` extracts the trailing metadata. +/// +/// The following example demonstrates how you can use the API: +/// +/// ```swift +/// // Create a successful response +/// let response = ClientResponse( +/// message: "Hello, World!", +/// metadata: ["hello": "initial metadata"], +/// trailingMetadata: ["goodbye": "trailing metadata"] +/// ) +/// +/// // The explicit API: +/// switch response { +/// case .success(let contents): +/// print("Received response with message '\(try contents.message.get())'") +/// case .failure(let error): +/// print("RPC failed with code '\(error.code)'") +/// } +/// +/// // The convenience API: +/// do { +/// print("Received response with message '\(try response.message)'") +/// } catch let error as RPCError { +/// print("RPC failed with code '\(error.code)'") +/// } +/// ``` +public struct ClientResponse: Sendable { + /// The contents of an accepted response with a single message. + public struct Contents: Sendable { + /// Metadata received from the server at the beginning of the response. + /// + /// The metadata may contain transport-specific information in addition to any application + /// level metadata provided by the service. + public var metadata: Metadata - /// Creates a `Contents`. - /// - /// - Parameters: - /// - metadata: Metadata received from the server at the beginning of the response. - /// - message: The response message received from the server. - /// - trailingMetadata: Metadata received from the server at the end of the response. - public init( - metadata: Metadata, - message: Message, - trailingMetadata: Metadata - ) { - self.metadata = metadata - self.message = .success(message) - self.trailingMetadata = trailingMetadata - } + /// The response message received from the server, or an error of the RPC failed with a + /// non-ok status. + public var message: Result - /// Creates a `Contents`. - /// - /// - Parameters: - /// - metadata: Metadata received from the server at the beginning of the response. - /// - error: Error received from the server. - public init( - metadata: Metadata, - error: RPCError - ) { - self.metadata = metadata - self.message = .failure(error) - self.trailingMetadata = error.metadata - } - } + /// Metadata received from the server at the end of the response. + /// + /// The metadata may contain transport-specific information in addition to any application + /// level metadata provided by the service. + public var trailingMetadata: Metadata - /// Whether the RPC was accepted or rejected. + /// Creates a `Contents`. /// - /// The `success` case indicates the RPC completed successfully with an - /// ``Status/Code-swift.struct/ok`` status code. The `failure` case indicates that the RPC was - /// rejected by the server and wasn't processed or couldn't be processed successfully. - public var accepted: Result + /// - Parameters: + /// - metadata: Metadata received from the server at the beginning of the response. + /// - message: The response message received from the server. + /// - trailingMetadata: Metadata received from the server at the end of the response. + public init( + metadata: Metadata, + message: Message, + trailingMetadata: Metadata + ) { + self.metadata = metadata + self.message = .success(message) + self.trailingMetadata = trailingMetadata + } - /// Creates a new response. + /// Creates a `Contents`. /// - /// - Parameter accepted: The result of the RPC. - public init(accepted: Result) { - self.accepted = accepted + /// - Parameters: + /// - metadata: Metadata received from the server at the beginning of the response. + /// - error: Error received from the server. + public init( + metadata: Metadata, + error: RPCError + ) { + self.metadata = metadata + self.message = .failure(error) + self.trailingMetadata = error.metadata } } -} -extension ClientResponse { - /// A response for a stream of messages received by a client. - /// - /// Stream responses are used for server-streaming and bidirectional-streaming RPCs. For single - /// responses see ``ClientResponse/Single``. - /// - /// A stream response captures every part of the response stream over time and distinguishes - /// accepted and rejected requests via the ``accepted`` property. An "accepted" request is one - /// where the the server responds with initial metadata and attempts to process the request. A - /// "rejected" request is one where the server responds with a status as the first and only - /// response part and doesn't process the request body. + /// Whether the RPC was accepted or rejected. /// - /// The value for the `success` case contains the initial metadata and a ``RPCAsyncSequence`` of - /// message parts (messages followed by a single status). If the sequence completes without - /// throwing then the response implicitly has an ``Status/Code-swift.struct/ok`` status code. - /// However, the response sequence may also throw an ``RPCError`` if the server fails to complete - /// processing the request. - /// - /// The `failure` case indicates that the server chose not to process the RPC or the client failed - /// to execute the request. The failure case contains an ``RPCError`` describing why the RPC - /// failed, including an error code, error message and any metadata sent by the server. - /// - /// ### Using ``Stream`` responses - /// - /// Each response has a ``accepted`` property which contains RPC information. You can create - /// one by calling ``init(accepted:)`` or one of the two convenience initializers: - /// - ``init(of:metadata:bodyParts:)`` to create an accepted response, or - /// - ``init(of:error:)`` to create a failed response. - /// - /// You can interrogate a response by inspecting the ``accepted`` property directly or by using - /// its convenience properties: - /// - ``metadata`` extracts the initial metadata, - /// - ``messages`` extracts the sequence of response message, or throws if the response failed. - /// - /// The following example demonstrates how you can use the API: - /// - /// ```swift - /// // Create a failed response - /// let response = ClientResponse.Stream( - /// of: String.self, - /// error: RPCError(code: .notFound, message: "The requested resource couldn't be located") - /// ) - /// - /// // The explicit API: - /// switch response { - /// case .success(let contents): - /// for try await part in contents.bodyParts { - /// switch part { - /// case .message(let message): - /// print("Received message '\(message)'") - /// case .trailingMetadata(let metadata): - /// print("Received trailing metadata '\(metadata)'") - /// } - /// } - /// case .failure(let error): - /// print("RPC failed with code '\(error.code)'") - /// } + /// The `success` case indicates the RPC completed successfully with an + /// ``Status/Code-swift.struct/ok`` status code. The `failure` case indicates that the RPC was + /// rejected by the server and wasn't processed or couldn't be processed successfully. + public var accepted: Result + + /// Creates a new response. /// - /// // The convenience API: - /// do { - /// for try await message in response.messages { - /// print("Received message '\(message)'") - /// } - /// } catch let error as RPCError { - /// print("RPC failed with code '\(error.code)'") - /// } - /// ``` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct Stream: Sendable { - public struct Contents: Sendable { - /// Metadata received from the server at the beginning of the response. - /// - /// The metadata may contain transport-specific information in addition to any application - /// level metadata provided by the service. - public var metadata: Metadata + /// - Parameter accepted: The result of the RPC. + public init(accepted: Result) { + self.accepted = accepted + } +} - /// A sequence of stream parts received from the server ending with metadata if the RPC - /// succeeded. - /// - /// If the RPC fails then the sequence will throw an ``RPCError``. - /// - /// The sequence may only be iterated once. - public var bodyParts: RPCAsyncSequence +/// A response for a stream of messages received by a client. +/// +/// Stream responses are used for server-streaming and bidirectional-streaming RPCs. For single +/// responses see ``ClientResponse``. +/// +/// A stream response captures every part of the response stream over time and distinguishes +/// accepted and rejected requests via the ``accepted`` property. An "accepted" request is one +/// where the the server responds with initial metadata and attempts to process the request. A +/// "rejected" request is one where the server responds with a status as the first and only +/// response part and doesn't process the request body. +/// +/// The value for the `success` case contains the initial metadata and a ``RPCAsyncSequence`` of +/// message parts (messages followed by a single status). If the sequence completes without +/// throwing then the response implicitly has an ``Status/Code-swift.struct/ok`` status code. +/// However, the response sequence may also throw an ``RPCError`` if the server fails to complete +/// processing the request. +/// +/// The `failure` case indicates that the server chose not to process the RPC or the client failed +/// to execute the request. The failure case contains an ``RPCError`` describing why the RPC +/// failed, including an error code, error message and any metadata sent by the server. +/// +/// ### Using ``Stream`` responses +/// +/// Each response has a ``accepted`` property which contains RPC information. You can create +/// one by calling ``init(accepted:)`` or one of the two convenience initializers: +/// - ``init(of:metadata:bodyParts:)`` to create an accepted response, or +/// - ``init(of:error:)`` to create a failed response. +/// +/// You can interrogate a response by inspecting the ``accepted`` property directly or by using +/// its convenience properties: +/// - ``metadata`` extracts the initial metadata, +/// - ``messages`` extracts the sequence of response message, or throws if the response failed. +/// +/// The following example demonstrates how you can use the API: +/// +/// ```swift +/// // Create a failed response +/// let response = StreamingClientResponse( +/// of: String.self, +/// error: RPCError(code: .notFound, message: "The requested resource couldn't be located") +/// ) +/// +/// // The explicit API: +/// switch response { +/// case .success(let contents): +/// for try await part in contents.bodyParts { +/// switch part { +/// case .message(let message): +/// print("Received message '\(message)'") +/// case .trailingMetadata(let metadata): +/// print("Received trailing metadata '\(metadata)'") +/// } +/// } +/// case .failure(let error): +/// print("RPC failed with code '\(error.code)'") +/// } +/// +/// // The convenience API: +/// do { +/// for try await message in response.messages { +/// print("Received message '\(message)'") +/// } +/// } catch let error as RPCError { +/// print("RPC failed with code '\(error.code)'") +/// } +/// ``` +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +public struct StreamingClientResponse: Sendable { + public struct Contents: Sendable { + /// Metadata received from the server at the beginning of the response. + /// + /// The metadata may contain transport-specific information in addition to any application + /// level metadata provided by the service. + public var metadata: Metadata - /// Parts received from the server. - public enum BodyPart: Sendable { - /// A response message. - case message(Message) - /// Metadata. Must be the final value of the sequence unless the stream throws an error. - case trailingMetadata(Metadata) - } + /// A sequence of stream parts received from the server ending with metadata if the RPC + /// succeeded. + /// + /// If the RPC fails then the sequence will throw an ``RPCError``. + /// + /// The sequence may only be iterated once. + public var bodyParts: RPCAsyncSequence - /// Creates a ``Contents``. - /// - /// - Parameters: - /// - metadata: Metadata received from the server at the beginning of the response. - /// - bodyParts: An `AsyncSequence` of parts received from the server. - public init( - metadata: Metadata, - bodyParts: RPCAsyncSequence - ) { - self.metadata = metadata - self.bodyParts = bodyParts - } + /// Parts received from the server. + public enum BodyPart: Sendable { + /// A response message. + case message(Message) + /// Metadata. Must be the final value of the sequence unless the stream throws an error. + case trailingMetadata(Metadata) } - /// Whether the RPC was accepted or rejected. - /// - /// The `success` case indicates the RPC was accepted by the server for - /// processing, however, the RPC may still fail by throwing an error from its - /// `messages` sequence. The `failure` case indicates that the RPC was - /// rejected by the server. - public var accepted: Result - - /// Creates a new response. + /// Creates a ``Contents``. /// - /// - Parameter accepted: The result of the RPC. - public init(accepted: Result) { - self.accepted = accepted + /// - Parameters: + /// - metadata: Metadata received from the server at the beginning of the response. + /// - bodyParts: An `AsyncSequence` of parts received from the server. + public init( + metadata: Metadata, + bodyParts: RPCAsyncSequence + ) { + self.metadata = metadata + self.bodyParts = bodyParts } } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` case indicates the RPC was accepted by the server for + /// processing, however, the RPC may still fail by throwing an error from its + /// `messages` sequence. The `failure` case indicates that the RPC was + /// rejected by the server. + public var accepted: Result + + /// Creates a new response. + /// + /// - Parameter accepted: The result of the RPC. + public init(accepted: Result) { + self.accepted = accepted + } } // MARK: - Convenience API -extension ClientResponse.Single { +extension ClientResponse { /// Creates a new accepted response. /// /// - Parameters: @@ -333,7 +326,7 @@ extension ClientResponse.Single { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Stream { +extension StreamingClientResponse { /// Creates a new accepted response. /// /// - Parameters: diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index bf57dae23..ca85fabdb 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -66,10 +66,10 @@ extension ClientRPCExecutor { extension ClientRPCExecutor.HedgingExecutor { @inlinable func execute( - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, options: CallOptions, - responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + responseHandler: @Sendable @escaping (StreamingClientResponse) async throws -> R ) async throws -> R { // The high level approach is to have two levels of task group. In the outer level tasks are // run to: @@ -102,7 +102,7 @@ extension ClientRPCExecutor.HedgingExecutor { } group.addTask { - let replayableRequest = ClientRequest.Stream(metadata: request.metadata) { writer in + let replayableRequest = StreamingClientRequest(metadata: request.metadata) { writer in try await writer.write(contentsOf: broadcast.stream) } @@ -148,10 +148,10 @@ extension ClientRPCExecutor.HedgingExecutor { @inlinable func executeAttempt( - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, options: CallOptions, - responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + responseHandler: @Sendable @escaping (StreamingClientResponse) async throws -> R ) async -> Result { await withTaskGroup( of: _HedgingAttemptTaskResult.self, @@ -201,7 +201,7 @@ extension ClientRPCExecutor.HedgingExecutor { } // Stop the most recent unusable response in case no response succeeds. - var unusableResponse: ClientResponse.Stream? + var unusableResponse: StreamingClientResponse? while let next = await group.next() { switch next { @@ -312,13 +312,13 @@ extension ClientRPCExecutor.HedgingExecutor { @inlinable func _startAttempt( - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, options: CallOptions, attempt: Int, state: SharedState, picker: (stream: BroadcastAsyncSequence, continuation: BroadcastAsyncSequence.Source), - responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + responseHandler: @Sendable @escaping (StreamingClientResponse) async throws -> R ) async -> _HedgingAttemptTaskResult.AttemptResult { do { return try await self.transport.withStream( @@ -562,7 +562,7 @@ enum _HedgingAttemptTaskResult: Sendable { @usableFromInline enum AttemptResult: Sendable { - case unusableResponse(ClientResponse.Stream, Metadata.RetryPushback?) + case unusableResponse(StreamingClientResponse, Metadata.RetryPushback?) case usableResponse(Result) case noStreamAvailable(any Error) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index a1288999c..493949fbc 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -58,10 +58,10 @@ extension ClientRPCExecutor { extension ClientRPCExecutor.OneShotExecutor { @inlinable func execute( - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, options: CallOptions, - responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + responseHandler: @Sendable @escaping (StreamingClientResponse) async throws -> R ) async throws -> R { let result: Result @@ -94,10 +94,10 @@ extension ClientRPCExecutor.OneShotExecutor { extension ClientRPCExecutor.OneShotExecutor { @inlinable func _execute( - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, options: CallOptions, - responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + responseHandler: @Sendable @escaping (StreamingClientResponse) async throws -> R ) async -> Result { return await withTaskGroup(of: Void.self, returning: Result.self) { group in do { diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index d808b1f4a..7188b4a6f 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -64,10 +64,10 @@ extension ClientRPCExecutor { extension ClientRPCExecutor.RetryExecutor { @inlinable func execute( - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, options: CallOptions, - responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + responseHandler: @Sendable @escaping (StreamingClientResponse) async throws -> R ) async throws -> R { // There's quite a lot going on here... // @@ -201,13 +201,13 @@ extension ClientRPCExecutor.RetryExecutor { retryStream: BroadcastAsyncSequence, method: MethodDescriptor, attempt: Int, - responseHandler: @Sendable @escaping (ClientResponse.Stream) async throws -> R + responseHandler: @Sendable @escaping (StreamingClientResponse) async throws -> R ) async -> _RetryExecutorTask { return await withTaskGroup( of: Void.self, returning: _RetryExecutorTask.self ) { group in - let request = ClientRequest.Stream(metadata: metadata) { + let request = StreamingClientRequest(metadata: metadata) { try await $0.write(contentsOf: retryStream) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index 33e46fd2d..f6e91d94f 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -33,14 +33,14 @@ enum ClientRPCExecutor { /// - Returns: The result returns from the `handler`. @inlinable static func execute( - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, options: CallOptions, serializer: some MessageSerializer, deserializer: some MessageDeserializer, transport: some ClientTransport, interceptors: [any ClientInterceptor], - handler: @Sendable @escaping (ClientResponse.Stream) async throws -> Result + handler: @Sendable @escaping (StreamingClientResponse) async throws -> Result ) async throws -> Result { let deadline = options.timeout.map { ContinuousClock.now + $0 } @@ -116,14 +116,14 @@ extension ClientRPCExecutor { @inlinable // would be private static func _execute( in group: inout TaskGroup, - request: ClientRequest.Stream, + request: StreamingClientRequest, method: MethodDescriptor, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, interceptors: [any ClientInterceptor], stream: RPCStream - ) async -> ClientResponse.Stream { + ) async -> StreamingClientResponse { let context = ClientContext(descriptor: method) if interceptors.isEmpty { @@ -159,15 +159,15 @@ extension ClientRPCExecutor { @inlinable static func _intercept( in group: inout TaskGroup, - request: ClientRequest.Stream, + request: StreamingClientRequest, context: ClientContext, iterator: Array.Iterator, finally: ( _ group: inout TaskGroup, - _ request: ClientRequest.Stream, + _ request: StreamingClientRequest, _ context: ClientContext - ) async -> ClientResponse.Stream - ) async -> ClientResponse.Stream { + ) async -> StreamingClientResponse + ) async -> StreamingClientResponse { var iterator = iterator switch iterator.next() { @@ -184,10 +184,10 @@ extension ClientRPCExecutor { ) } } catch let error as RPCError { - return ClientResponse.Stream(error: error) + return StreamingClientResponse(error: error) } catch let other { let error = RPCError(code: .unknown, message: "", cause: other) - return ClientResponse.Stream(error: error) + return StreamingClientResponse(error: error) } case .none: diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift index 74beb368b..9f9b3223b 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift @@ -15,8 +15,8 @@ */ @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ClientRequest.Stream { - internal init(single request: ClientRequest.Single) { +extension StreamingClientRequest { + internal init(single request: ClientRequest) { self.init(metadata: request.metadata) { try await $0.write(request.message) } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift index ecba7cf57..c6e315ba8 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -15,11 +15,11 @@ */ @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Single { +extension ClientResponse { /// Converts a streaming response into a single response. /// /// - Parameter response: The streaming response to convert. - init(stream response: ClientResponse.Stream) async { + init(stream response: StreamingClientResponse) async { switch response.accepted { case .success(let contents): do { @@ -83,7 +83,7 @@ extension ClientResponse.Single { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Stream { +extension StreamingClientResponse { /// Creates a streaming response from the given status and metadata. /// /// If the ``Status`` has code ``Status/Code-swift.struct/ok`` then an accepted stream is created @@ -104,7 +104,7 @@ extension ClientResponse.Stream { } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ClientResponse.Stream { +extension StreamingClientResponse { /// Returns a new response which maps the messages of this response. /// /// - Parameter transform: The function to transform each message with. @@ -112,10 +112,10 @@ extension ClientResponse.Stream { @inlinable func map( _ transform: @escaping @Sendable (Message) throws -> Mapped - ) -> ClientResponse.Stream { + ) -> StreamingClientResponse { switch self.accepted { case .success(let contents): - return ClientResponse.Stream( + return StreamingClientResponse( metadata: self.metadata, bodyParts: RPCAsyncSequence( wrapping: contents.bodyParts.map { @@ -130,7 +130,7 @@ extension ClientResponse.Stream { ) case .failure(let error): - return ClientResponse.Stream(accepted: .failure(error)) + return StreamingClientResponse(accepted: .failure(error)) } } } diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 8b635bca8..728f4e337 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -31,13 +31,13 @@ internal enum ClientStreamExecutor { @inlinable static func execute( in group: inout TaskGroup, - request: ClientRequest.Stream, + request: StreamingClientRequest, context: ClientContext, attempt: Int, serializer: some MessageSerializer, deserializer: some MessageDeserializer, stream: RPCStream - ) async -> ClientResponse.Stream { + ) async -> StreamingClientResponse { // Let the server know this is a retry. var metadata = request.metadata if attempt > 1 { @@ -63,7 +63,7 @@ internal enum ClientStreamExecutor { ) // Expected happy case: the server is processing the request. - return ClientResponse.Stream( + return StreamingClientResponse( metadata: metadata, bodyParts: RPCAsyncSequence(wrapping: bodyParts) ) @@ -75,18 +75,18 @@ internal enum ClientStreamExecutor { } // Expected unhappy (but okay) case; the server rejected the request. - return ClientResponse.Stream(status: status, metadata: metadata) + return StreamingClientResponse(status: status, metadata: metadata) case .failed(let error): // Very unhappy case: the server did something unexpected. - return ClientResponse.Stream(error: error) + return StreamingClientResponse(error: error) } } @inlinable // would be private static func _processRequest( on stream: some ClosableRPCWriterProtocol, - request: ClientRequest.Stream, + request: StreamingClientRequest, serializer: some MessageSerializer ) async { let result = await Result { @@ -194,7 +194,7 @@ internal enum ClientStreamExecutor { @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline - typealias Element = ClientResponse.Stream.Contents.BodyPart + typealias Element = StreamingClientResponse.Contents.BodyPart @usableFromInline var base: Base.AsyncIterator @@ -210,7 +210,7 @@ internal enum ClientStreamExecutor { @inlinable mutating func next( isolation actor: isolated (any Actor)? - ) async throws(any Error) -> ClientResponse.Stream.Contents.BodyPart? { + ) async throws(any Error) -> StreamingClientResponse.Contents.BodyPart? { guard let part = try await self.base.next(isolation: `actor`) else { return nil } switch part { @@ -238,7 +238,7 @@ internal enum ClientStreamExecutor { } @inlinable - mutating func next() async throws -> ClientResponse.Stream.Contents.BodyPart? { + mutating func next() async throws -> StreamingClientResponse.Contents.BodyPart? { try await self.next(isolation: nil) } } diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index a67bfbe37..79fba9b5c 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -37,9 +37,9 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @Sendable @escaping ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream + ) async throws -> StreamingServerResponse ) async { // Wait for the first request part from the transport. let firstPart = await Self._waitForFirstRequestPart(inbound: stream.inbound) @@ -75,9 +75,9 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @escaping @Sendable ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream + ) async throws -> StreamingServerResponse ) async { if let timeout = metadata.timeout { await Self._processRPCWithTimeout( @@ -116,9 +116,9 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @escaping @Sendable ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream + ) async throws -> StreamingServerResponse ) async { await withTaskGroup(of: ServerExecutorTask.self) { group in group.addTask { @@ -170,9 +170,9 @@ struct ServerRPCExecutor { serializer: some MessageSerializer, interceptors: [any ServerInterceptor], handler: @escaping @Sendable ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream + ) async throws -> StreamingServerResponse ) async { let messages = UncheckedAsyncIteratorSequence(inbound.wrappedValue).map { part in switch part { @@ -192,7 +192,7 @@ struct ServerRPCExecutor { let response = await Result { // Run the request through the interceptors, finally passing it to the handler. return try await Self._intercept( - request: ServerRequest.Stream( + request: StreamingServerRequest( metadata: metadata, messages: RPCAsyncSequence(wrapping: messages) ), @@ -300,14 +300,14 @@ struct ServerRPCExecutor { extension ServerRPCExecutor { @inlinable static func _intercept( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext, interceptors: [any ServerInterceptor], finally: @escaping @Sendable ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse + ) async throws -> StreamingServerResponse { return try await self._intercept( request: request, context: context, @@ -318,14 +318,14 @@ extension ServerRPCExecutor { @inlinable static func _intercept( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext, iterator: Array.Iterator, finally: @escaping @Sendable ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse + ) async throws -> StreamingServerResponse { var iterator = iterator switch iterator.next() { @@ -336,10 +336,10 @@ extension ServerRPCExecutor { try await self._intercept(request: $0, context: $1, iterator: iter, finally: finally) } } catch let error as RPCError { - return ServerResponse.Stream(error: error) + return StreamingServerResponse(error: error) } catch let other { let error = RPCError(code: .unknown, message: "", cause: other) - return ServerResponse.Stream(error: error) + return StreamingServerResponse(error: error) } case .none: diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index bc2f58fef..35d14cbd5 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -53,9 +53,9 @@ public struct RPCRouter: Sendable { deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @Sendable @escaping ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream + ) async throws -> StreamingServerResponse ) { self._fn = { stream, context, interceptors in await ServerRPCExecutor.execute( @@ -123,9 +123,9 @@ public struct RPCRouter: Sendable { deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @Sendable @escaping ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream + ) async throws -> StreamingServerResponse ) { self.handlers[descriptor] = RPCHandler( method: descriptor, diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index c10c166ca..aa1fff090 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -37,13 +37,13 @@ /// let isAuthorized: @Sendable (String, MethodDescriptor) async throws -> Void /// /// func intercept( -/// request: ServerRequest.Stream, +/// request: StreamingServerRequest, /// context: ServerInterceptorContext, /// next: @Sendable ( -/// _ request: ServerRequest.Stream, +/// _ request: StreamingServerRequest, /// _ context: ServerInterceptorContext -/// ) async throws -> ServerResponse.Stream -/// ) async throws -> ServerResponse.Stream { +/// ) async throws -> StreamingServerResponse +/// ) async throws -> StreamingServerResponse { /// // Extract the auth token. /// guard let token = request.metadata["authorization"] else { /// throw RPCError(code: .unauthenticated, message: "Not authenticated") @@ -71,11 +71,11 @@ public protocol ServerInterceptor: Sendable { /// interceptor in the chain. /// - Returns: A response object. func intercept( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext, next: @Sendable ( - _ request: ServerRequest.Stream, + _ request: StreamingServerRequest, _ context: ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream + ) async throws -> StreamingServerResponse + ) async throws -> StreamingServerResponse } diff --git a/Sources/GRPCCore/Call/Server/ServerRequest.swift b/Sources/GRPCCore/Call/Server/ServerRequest.swift index 90618bdfe..b0318ee15 100644 --- a/Sources/GRPCCore/Call/Server/ServerRequest.swift +++ b/Sources/GRPCCore/Call/Server/ServerRequest.swift @@ -14,72 +14,65 @@ * limitations under the License. */ -/// A namespace for request message types used by servers. -public enum ServerRequest {} +/// A request received at the server containing a single message. +public struct ServerRequest: Sendable { + /// Metadata received from the client at the start of the RPC. + /// + /// The metadata contains gRPC and transport specific entries in addition to user-specified + /// metadata. + public var metadata: Metadata -extension ServerRequest { - /// A request received at the server containing a single message. - public struct Single: Sendable { - /// Metadata received from the client at the start of the RPC. - /// - /// The metadata contains gRPC and transport specific entries in addition to user-specified - /// metadata. - public var metadata: Metadata - - /// The message received from the client. - public var message: Message + /// The message received from the client. + public var message: Message - /// Create a new single server request. - /// - /// - Parameters: - /// - metadata: Metadata received from the client. - /// - message: The message received from the client. - public init(metadata: Metadata, message: Message) { - self.metadata = metadata - self.message = message - } + /// Create a new single server request. + /// + /// - Parameters: + /// - metadata: Metadata received from the client. + /// - message: The message received from the client. + public init(metadata: Metadata, message: Message) { + self.metadata = metadata + self.message = message } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ServerRequest { - /// A request received at the server containing a stream of messages. - public struct Stream: Sendable { - /// Metadata received from the client at the start of the RPC. - /// - /// The metadata contains gRPC and transport specific entries in addition to user-specified - /// metadata. - public var metadata: Metadata +/// A request received at the server containing a stream of messages. +public struct StreamingServerRequest: Sendable { + /// Metadata received from the client at the start of the RPC. + /// + /// The metadata contains gRPC and transport specific entries in addition to user-specified + /// metadata. + public var metadata: Metadata - /// A sequence of messages received from the client. - /// - /// The sequence may be iterated at most once. - public var messages: RPCAsyncSequence + /// A sequence of messages received from the client. + /// + /// The sequence may be iterated at most once. + public var messages: RPCAsyncSequence - /// Create a new streaming request. - /// - /// - Parameters: - /// - metadata: Metadata received from the client. - /// - messages: A sequence of messages received from the client. - public init(metadata: Metadata, messages: RPCAsyncSequence) { - self.metadata = metadata - self.messages = messages - } + /// Create a new streaming request. + /// + /// - Parameters: + /// - metadata: Metadata received from the client. + /// - messages: A sequence of messages received from the client. + public init(metadata: Metadata, messages: RPCAsyncSequence) { + self.metadata = metadata + self.messages = messages } } // MARK: - Conversion @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ServerRequest.Stream { - public init(single request: ServerRequest.Single) { +extension StreamingServerRequest { + public init(single request: ServerRequest) { self.init(metadata: request.metadata, messages: .one(request.message)) } } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -extension ServerRequest.Single { - public init(stream request: ServerRequest.Stream) async throws { +extension ServerRequest { + public init(stream request: StreamingServerRequest) async throws { var iterator = request.messages.makeAsyncIterator() guard let message = try await iterator.next() else { @@ -90,6 +83,6 @@ extension ServerRequest.Single { throw RPCError(code: .internalError, message: "Too many messages.") } - self = ServerRequest.Single(metadata: request.metadata, message: message) + self = ServerRequest(metadata: request.metadata, message: message) } } diff --git a/Sources/GRPCCore/Call/Server/ServerResponse.swift b/Sources/GRPCCore/Call/Server/ServerResponse.swift index a0b516815..778d1b395 100644 --- a/Sources/GRPCCore/Call/Server/ServerResponse.swift +++ b/Sources/GRPCCore/Call/Server/ServerResponse.swift @@ -14,220 +14,213 @@ * limitations under the License. */ -/// A namespace for response message types used by servers. -public enum ServerResponse {} - -extension ServerResponse { - /// A response for a single message sent by a server. - /// - /// Single responses are used for unary and client-streaming RPCs. For streaming responses - /// see ``ServerResponse/Stream``. - /// - /// A single response captures every part of the response stream and distinguishes successful - /// and unsuccessful responses via the ``accepted`` property. The value for the `success` case - /// contains the initial metadata, response message, and the trailing metadata and implicitly - /// has an ``Status/Code-swift.struct/ok`` status code. - /// - /// The `failure` case indicates that the server chose not to process the RPC, or the processing - /// of the RPC failed. The failure case contains an ``RPCError`` describing why the RPC failed, - /// including an error code, error message and any metadata sent by the server. - /// - /// ### Using ``Single`` responses - /// - /// Each response has an ``accepted`` property which contains all RPC information. You can create - /// one by calling ``init(accepted:)`` or one of the two convenience initializers: - /// - ``init(message:metadata:trailingMetadata:)`` to create a successful response, or - /// - ``init(of:error:)`` to create a failed response. - /// - /// You can interrogate a response by inspecting the ``accepted`` property directly or by using - /// its convenience properties: - /// - ``metadata`` extracts the initial metadata, - /// - ``message`` extracts the message, or throws if the response failed, and - /// - ``trailingMetadata`` extracts the trailing metadata. - /// - /// The following example demonstrates how you can use the API: - /// - /// ```swift - /// // Create a successful response - /// let response = ServerResponse.Single( - /// message: "Hello, World!", - /// metadata: ["hello": "initial metadata"], - /// trailingMetadata: ["goodbye": "trailing metadata"] - /// ) - /// - /// // The explicit API: - /// switch response { - /// case .success(let contents): - /// print("Received response with message '\(contents.message)'") - /// case .failure(let error): - /// print("RPC failed with code '\(error.code)'") - /// } - /// - /// // The convenience API: - /// do { - /// print("Received response with message '\(try response.message)'") - /// } catch let error as RPCError { - /// print("RPC failed with code '\(error.code)'") - /// } - /// ``` - public struct Single: Sendable { - /// An accepted RPC with a successful outcome. - public struct Contents: Sendable { - /// Caller-specified metadata to send to the client at the start of the response. - /// - /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with - /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert - /// their own metadata, you should avoid using key names which may clash with transport - /// specific metadata. Note that transports may also impose limits in the amount of metadata - /// which may be sent. - public var metadata: Metadata - - /// The message to send to the client. - public var message: Message - - /// Caller-specified metadata to send to the client at the end of the response. - /// - /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with - /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert - /// their own metadata, you should avoid using key names which may clash with transport - /// specific metadata. Note that transports may also impose limits in the amount of metadata - /// which may be sent. - public var trailingMetadata: Metadata +/// A response for a single message sent by a server. +/// +/// Single responses are used for unary and client-streaming RPCs. For streaming responses +/// see ``StreamingServerResponse``. +/// +/// A single response captures every part of the response stream and distinguishes successful +/// and unsuccessful responses via the ``accepted`` property. The value for the `success` case +/// contains the initial metadata, response message, and the trailing metadata and implicitly +/// has an ``Status/Code-swift.struct/ok`` status code. +/// +/// The `failure` case indicates that the server chose not to process the RPC, or the processing +/// of the RPC failed. The failure case contains an ``RPCError`` describing why the RPC failed, +/// including an error code, error message and any metadata sent by the server. +/// +/// ### Using ``Single`` responses +/// +/// Each response has an ``accepted`` property which contains all RPC information. You can create +/// one by calling ``init(accepted:)`` or one of the two convenience initializers: +/// - ``init(message:metadata:trailingMetadata:)`` to create a successful response, or +/// - ``init(of:error:)`` to create a failed response. +/// +/// You can interrogate a response by inspecting the ``accepted`` property directly or by using +/// its convenience properties: +/// - ``metadata`` extracts the initial metadata, +/// - ``message`` extracts the message, or throws if the response failed, and +/// - ``trailingMetadata`` extracts the trailing metadata. +/// +/// The following example demonstrates how you can use the API: +/// +/// ```swift +/// // Create a successful response +/// let response = ServerResponse( +/// message: "Hello, World!", +/// metadata: ["hello": "initial metadata"], +/// trailingMetadata: ["goodbye": "trailing metadata"] +/// ) +/// +/// // The explicit API: +/// switch response { +/// case .success(let contents): +/// print("Received response with message '\(contents.message)'") +/// case .failure(let error): +/// print("RPC failed with code '\(error.code)'") +/// } +/// +/// // The convenience API: +/// do { +/// print("Received response with message '\(try response.message)'") +/// } catch let error as RPCError { +/// print("RPC failed with code '\(error.code)'") +/// } +/// ``` +public struct ServerResponse: Sendable { + /// An accepted RPC with a successful outcome. + public struct Contents: Sendable { + /// Caller-specified metadata to send to the client at the start of the response. + /// + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport + /// specific metadata. Note that transports may also impose limits in the amount of metadata + /// which may be sent. + public var metadata: Metadata - /// Create a new single client request. - /// - /// - Parameters: - /// - message: The message to send to the server. - /// - metadata: Metadata to send to the client at the start of the response. Defaults to - /// empty. - /// - trailingMetadata: Metadata to send to the client at the end of the response. Defaults - /// to empty. - public init( - message: Message, - metadata: Metadata = [:], - trailingMetadata: Metadata = [:] - ) { - self.metadata = metadata - self.message = message - self.trailingMetadata = trailingMetadata - } - } + /// The message to send to the client. + public var message: Message - /// Whether the RPC was accepted or rejected. + /// Caller-specified metadata to send to the client at the end of the response. /// - /// The `success` indicates the server accepted the RPC for processing and the RPC completed - /// successfully and implies the RPC succeeded with the ``Status/Code-swift.struct/ok`` status - /// code. The `failure` case indicates that the service rejected the RPC without processing it - /// or could not process it successfully. - public var accepted: Result + /// Both gRPC Swift and its transport layer may insert additional metadata. Keys prefixed with + /// "grpc-" are prohibited and may result in undefined behaviour. Transports may also insert + /// their own metadata, you should avoid using key names which may clash with transport + /// specific metadata. Note that transports may also impose limits in the amount of metadata + /// which may be sent. + public var trailingMetadata: Metadata - /// Creates a response. + /// Create a new single client request. /// - /// - Parameter accepted: Whether the RPC was accepted or rejected. - public init(accepted: Result) { - self.accepted = accepted + /// - Parameters: + /// - message: The message to send to the server. + /// - metadata: Metadata to send to the client at the start of the response. Defaults to + /// empty. + /// - trailingMetadata: Metadata to send to the client at the end of the response. Defaults + /// to empty. + public init( + message: Message, + metadata: Metadata = [:], + trailingMetadata: Metadata = [:] + ) { + self.metadata = metadata + self.message = message + self.trailingMetadata = trailingMetadata } } -} -extension ServerResponse { - /// A response for a stream of messages sent by a server. - /// - /// Stream responses are used for server-streaming and bidirectional-streaming RPCs. For single - /// responses see ``ServerResponse/Single``. + /// Whether the RPC was accepted or rejected. /// - /// A stream response captures every part of the response stream and distinguishes whether the - /// request was processed by the server via the ``accepted`` property. The value for the `success` - /// case contains the initial metadata and a closure which is provided with a message write and - /// returns trailing metadata. If the closure returns without error then the response implicitly - /// has an ``Status/Code-swift.struct/ok`` status code. You can throw an error from the producer - /// to indicate that the request couldn't be handled successfully. If an ``RPCError`` is thrown - /// then the client will receive an equivalent error populated with the same code and message. If - /// an error of any other type is thrown then the client will receive an error with the - /// ``Status/Code-swift.struct/unknown`` status code. - /// - /// The `failure` case indicates that the server chose not to process the RPC. The failure case - /// contains an ``RPCError`` describing why the RPC failed, including an error code, error - /// message and any metadata to send to the client. - /// - /// ### Using ``Stream`` responses - /// - /// Each response has an ``accepted`` property which contains all RPC information. You can create - /// one by calling ``init(accepted:)`` or one of the two convenience initializers: - /// - ``init(of:metadata:producer:)`` to create a successful response, or - /// - ``init(of:error:)`` to create a failed response. - /// - /// You can interrogate a response by inspecting the ``accepted`` property directly. The following - /// example demonstrates how you can use the API: - /// - /// ```swift - /// // Create a successful response - /// let response = ServerResponse.Stream( - /// of: String.self, - /// metadata: ["hello": "initial metadata"] - /// ) { writer in - /// // Write a few messages. - /// try await writer.write("Hello") - /// try await writer.write("World") - /// - /// // Send trailing metadata to the client. - /// return ["goodbye": "trailing metadata"] - /// } - /// ``` - @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) - public struct Stream: Sendable { - /// The contents of a response to a request which has been accepted for processing. - public struct Contents: Sendable { - /// Metadata to send to the client at the beginning of the response stream. - public var metadata: Metadata + /// The `success` indicates the server accepted the RPC for processing and the RPC completed + /// successfully and implies the RPC succeeded with the ``Status/Code-swift.struct/ok`` status + /// code. The `failure` case indicates that the service rejected the RPC without processing it + /// or could not process it successfully. + public var accepted: Result - /// A closure which, when called, writes values into the provided writer and returns trailing - /// metadata indicating the end of the response stream. - /// - /// Returning metadata indicates a successful response and gRPC will terminate the RPC with - /// an ``Status/Code-swift.struct/ok`` status code. Throwing an error will terminate the RPC - /// with an appropriate status code. You can control the status code, message and metadata - /// returned to the client by throwing an ``RPCError``. If the error thrown is a type other - /// than ``RPCError`` then a status with code ``Status/Code-swift.struct/unknown`` will - /// be returned to the client. - /// - /// gRPC will invoke this function at most once therefore it isn't required to be idempotent. - public var producer: @Sendable (RPCWriter) async throws -> Metadata + /// Creates a response. + /// + /// - Parameter accepted: Whether the RPC was accepted or rejected. + public init(accepted: Result) { + self.accepted = accepted + } +} - /// Create a ``Contents``. - /// - /// - Parameters: - /// - metadata: Metadata to send to the client at the start of the response. - /// - producer: A function which produces values - public init( - metadata: Metadata, - producer: @escaping @Sendable (RPCWriter) async throws -> Metadata - ) { - self.metadata = metadata - self.producer = producer - } - } +/// A response for a stream of messages sent by a server. +/// +/// Stream responses are used for server-streaming and bidirectional-streaming RPCs. For single +/// responses see ``ServerResponse``. +/// +/// A stream response captures every part of the response stream and distinguishes whether the +/// request was processed by the server via the ``accepted`` property. The value for the `success` +/// case contains the initial metadata and a closure which is provided with a message write and +/// returns trailing metadata. If the closure returns without error then the response implicitly +/// has an ``Status/Code-swift.struct/ok`` status code. You can throw an error from the producer +/// to indicate that the request couldn't be handled successfully. If an ``RPCError`` is thrown +/// then the client will receive an equivalent error populated with the same code and message. If +/// an error of any other type is thrown then the client will receive an error with the +/// ``Status/Code-swift.struct/unknown`` status code. +/// +/// The `failure` case indicates that the server chose not to process the RPC. The failure case +/// contains an ``RPCError`` describing why the RPC failed, including an error code, error +/// message and any metadata to send to the client. +/// +/// ### Using ``Stream`` responses +/// +/// Each response has an ``accepted`` property which contains all RPC information. You can create +/// one by calling ``init(accepted:)`` or one of the two convenience initializers: +/// - ``init(of:metadata:producer:)`` to create a successful response, or +/// - ``init(of:error:)`` to create a failed response. +/// +/// You can interrogate a response by inspecting the ``accepted`` property directly. The following +/// example demonstrates how you can use the API: +/// +/// ```swift +/// // Create a successful response +/// let response = StreamingServerResponse( +/// of: String.self, +/// metadata: ["hello": "initial metadata"] +/// ) { writer in +/// // Write a few messages. +/// try await writer.write("Hello") +/// try await writer.write("World") +/// +/// // Send trailing metadata to the client. +/// return ["goodbye": "trailing metadata"] +/// } +/// ``` +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct StreamingServerResponse: Sendable { + /// The contents of a response to a request which has been accepted for processing. + public struct Contents: Sendable { + /// Metadata to send to the client at the beginning of the response stream. + public var metadata: Metadata - /// Whether the RPC was accepted or rejected. + /// A closure which, when called, writes values into the provided writer and returns trailing + /// metadata indicating the end of the response stream. /// - /// The `success` case indicates that the service accepted the RPC for processing and will - /// send initial metadata back to the client before producing response messages. The RPC may - /// still result in failure by later throwing an error. + /// Returning metadata indicates a successful response and gRPC will terminate the RPC with + /// an ``Status/Code-swift.struct/ok`` status code. Throwing an error will terminate the RPC + /// with an appropriate status code. You can control the status code, message and metadata + /// returned to the client by throwing an ``RPCError``. If the error thrown is a type other + /// than ``RPCError`` then a status with code ``Status/Code-swift.struct/unknown`` will + /// be returned to the client. /// - /// The `failure` case indicates that the server rejected the RPC and will not process it. Only - /// the status and trailing metadata will be sent to the client. - public var accepted: Result + /// gRPC will invoke this function at most once therefore it isn't required to be idempotent. + public var producer: @Sendable (RPCWriter) async throws -> Metadata - /// Creates a response. + /// Create a ``Contents``. /// - /// - Parameter accepted: Whether the RPC was accepted or rejected. - public init(accepted: Result) { - self.accepted = accepted + /// - Parameters: + /// - metadata: Metadata to send to the client at the start of the response. + /// - producer: A function which produces values + public init( + metadata: Metadata, + producer: @escaping @Sendable (RPCWriter) async throws -> Metadata + ) { + self.metadata = metadata + self.producer = producer } } + + /// Whether the RPC was accepted or rejected. + /// + /// The `success` case indicates that the service accepted the RPC for processing and will + /// send initial metadata back to the client before producing response messages. The RPC may + /// still result in failure by later throwing an error. + /// + /// The `failure` case indicates that the server rejected the RPC and will not process it. Only + /// the status and trailing metadata will be sent to the client. + public var accepted: Result + + /// Creates a response. + /// + /// - Parameter accepted: Whether the RPC was accepted or rejected. + public init(accepted: Result) { + self.accepted = accepted + } } -extension ServerResponse.Single { +extension ServerResponse { /// Creates a new accepted response. /// /// - Parameters: @@ -287,7 +280,7 @@ extension ServerResponse.Single { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerResponse.Stream { +extension StreamingServerResponse { /// Creates a new accepted response. /// /// - Parameters: @@ -326,8 +319,8 @@ extension ServerResponse.Stream { } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) -extension ServerResponse.Stream { - public init(single response: ServerResponse.Single) { +extension StreamingServerResponse { + public init(single response: ServerResponse) { switch response.accepted { case .success(let contents): let contents = Contents(metadata: contents.metadata) { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift index ef1120c64..b4b20841a 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step01.swift @@ -1,11 +1,11 @@ struct Greeter: Helloworld_GreeterServiceProtocol { func sayHello( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello, \(recipient)" - return ServerResponse.Single(message: reply) + return ServerResponse(message: reply) } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift index 44e571bec..9ecccd975 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Hello-World/Resources/hello-world-sec04-step02.swift @@ -1,21 +1,21 @@ struct Greeter: Helloworld_GreeterServiceProtocol { func sayHello( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello, \(recipient)" - return ServerResponse.Single(message: reply) + return ServerResponse(message: reply) } func sayHelloAgain( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello again, \(recipient)" - return ServerResponse.Single(message: reply) + return ServerResponse(message: reply) } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift index b2651be95..283c9a8fb 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step02-unimplemented.swift @@ -2,26 +2,26 @@ import GRPCCore struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift index 46925a6bb..84ce31e89 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step03-features.swift @@ -11,26 +11,26 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift index 11fbc8215..cf1b88e21 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step04-unary.swift @@ -18,9 +18,9 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude @@ -28,20 +28,20 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift index d2d82d553..dcbde29fa 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step05-unary.swift @@ -18,16 +18,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return ServerResponse(message: feature) } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -37,25 +37,25 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return ServerResponse(message: unknownFeature) } } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift index b833d72f8..779857268 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step06-server-streaming.swift @@ -18,16 +18,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return ServerResponse(message: feature) } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -37,15 +37,15 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return ServerResponse(message: unknownFeature) } } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for feature in self.features { if !feature.name.isEmpty, feature.isContained(by: request.message) { try await writer.write(feature) @@ -55,15 +55,15 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift index 6c64829e5..8fe80e2dd 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step07-server-streaming.swift @@ -18,16 +18,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return ServerResponse(message: feature) } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -37,15 +37,15 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return ServerResponse(message: unknownFeature) } } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for feature in self.features { if !feature.name.isEmpty, feature.isContained(by: request.message) { try await writer.write(feature) @@ -57,15 +57,15 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift index d9221cd01..2004a8ddc 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step08-client-streaming.swift @@ -19,16 +19,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return ServerResponse(message: feature) } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -38,15 +38,15 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return ServerResponse(message: unknownFeature) } } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for feature in self.features { if !feature.name.isEmpty, feature.isContained(by: request.message) { try await writer.write(feature) @@ -58,9 +58,9 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 @@ -89,13 +89,13 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return ServerResponse(message: summary) } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift index c8f9010b5..d48385753 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step09-bidi-streaming.swift @@ -50,16 +50,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return ServerResponse(message: feature) } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -69,15 +69,15 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return ServerResponse(message: unknownFeature) } } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for feature in self.features { if !feature.name.isEmpty, feature.isContained(by: request.message) { try await writer.write(feature) @@ -89,9 +89,9 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 @@ -120,13 +120,13 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return ServerResponse(message: summary) } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse { } } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift index 8f4027b5e..eb2b88a78 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec04-step10-bidi-streaming.swift @@ -50,16 +50,16 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return ServerResponse(message: feature) } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -69,15 +69,15 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return ServerResponse(message: unknownFeature) } } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for feature in self.features { if !feature.name.isEmpty, feature.isContained(by: request.message) { try await writer.write(feature) @@ -89,9 +89,9 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 @@ -120,14 +120,14 @@ struct RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return ServerResponse(message: summary) } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for try await note in request.messages { let notes = self.receivedNotes.recordNote(note) try await writer.write(contentsOf: notes) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial index 0380b6540..ef4f3809f 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -242,14 +242,14 @@ @Step { `GetFeature` is a unary RPC which takes a single point as input and returns a single feature back to the client. Its generated method, `getFeature`, has one parameter: - `ServerRequest.Single` describing the request. To return our response to + `ServerRequest` describing the request. To return our response to the client and complete the call we must first lookup a feature at the given point. @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step04-unary.swift") } @Step { - Then create and return an appropriate `ServerResponse.Single` to the + Then create and return an appropriate `ServerResponse` to the client. @Code(name: "Sources/RouteGuideService.swift", file: "route-guide-sec04-step05-unary.swift") @@ -257,11 +257,11 @@ @Step { Next, let's look at one of our streaming RPCs. Like the unary RPC, this method gets a - request object, `ServerRequest.Single`, which has a message describing + request object, `ServerRequest`, which has a message describing the area in which the client wants to list features. As this is a server-side streaming RPC we can send back multiple `Routeguide_Feature` messages to our client. - To implement the method we must return a `ServerResponse.Stream` which is initialized with + To implement the method we must return a `StreamingServerResponse` which is initialized with a closure to produce messages. The closure is passed a writer allowing you to write back messages. We can write back a message for each feature we find in the rectangle. @@ -280,8 +280,8 @@ method `RecordRoute`, where we get a stream of `Routeguide_Point`s from the client and return a single `Routeguide_RouteSummary` with information about their trip. - As you can see our method gets a `ServerRequest.Stream` parameter and - returns a `ServerResponse.Single`. In the method we iterate over + As you can see our method gets a `StreamingServerRequest` parameter and + returns a `ServerResponse`. In the method we iterate over the asynchronous stream of points sent by the client. For each point we check if there's a feature at that point and calculate the distance between that and the last point we saw. After the *client* has finished sending points we populate a `Routeguide_RouteSummary` which @@ -300,7 +300,7 @@ } @Step { - To implement the RPC we return a `ServerResponse.Stream`. Like in the + To implement the RPC we return a `StreamingServerResponse`. Like in the server-side streaming RPC it's initialized with a closure for writing back messages. In the body of the closure we iterate the request messages and for each one call our helper class to record the note and get all other notes recorded in the same location. We then diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index a3f9fe2d9..31a1aed67 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -89,7 +89,7 @@ private import Synchronization /// /// // Execute a request against the "echo.Echo" service. /// try await client.unary( -/// request: ClientRequest.Single<[UInt8]>(message: [72, 101, 108, 108, 111, 33]), +/// request: ClientRequest<[UInt8]>(message: [72, 101, 108, 108, 111, 33]), /// descriptor: MethodDescriptor(service: "echo.Echo", method: "Get"), /// serializer: IdentitySerializer(), /// deserializer: IdentityDeserializer(), @@ -256,21 +256,21 @@ public final class GRPCClient: Sendable { /// /// - Returns: The return value from the `handler`. public func unary( - request: ClientRequest.Single, + request: ClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue + handler: @Sendable @escaping (ClientResponse) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( - request: ClientRequest.Stream(single: request), + request: StreamingClientRequest(single: request), descriptor: descriptor, serializer: serializer, deserializer: deserializer, options: options ) { stream in - let singleResponse = await ClientResponse.Single(stream: stream) + let singleResponse = await ClientResponse(stream: stream) return try await handler(singleResponse) } } @@ -287,12 +287,12 @@ public final class GRPCClient: Sendable { /// /// - Returns: The return value from the `handler`. public func clientStreaming( - request: ClientRequest.Stream, + request: StreamingClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Single) async throws -> ReturnValue + handler: @Sendable @escaping (ClientResponse) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( request: request, @@ -301,7 +301,7 @@ public final class GRPCClient: Sendable { deserializer: deserializer, options: options ) { stream in - let singleResponse = await ClientResponse.Single(stream: stream) + let singleResponse = await ClientResponse(stream: stream) return try await handler(singleResponse) } } @@ -318,15 +318,15 @@ public final class GRPCClient: Sendable { /// /// - Returns: The return value from the `handler`. public func serverStreaming( - request: ClientRequest.Single, + request: ClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue + handler: @Sendable @escaping (StreamingClientResponse) async throws -> ReturnValue ) async throws -> ReturnValue { try await self.bidirectionalStreaming( - request: ClientRequest.Stream(single: request), + request: StreamingClientRequest(single: request), descriptor: descriptor, serializer: serializer, deserializer: deserializer, @@ -350,12 +350,12 @@ public final class GRPCClient: Sendable { /// /// - Returns: The return value from the `handler`. public func bidirectionalStreaming( - request: ClientRequest.Stream, + request: StreamingClientRequest, descriptor: MethodDescriptor, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @Sendable @escaping (ClientResponse.Stream) async throws -> ReturnValue + handler: @Sendable @escaping (StreamingClientResponse) async throws -> ReturnValue ) async throws -> ReturnValue { try self.state.withLock { try $0.checkExecutable() } let methodConfig = self.transport.config(forMethod: descriptor) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 17b50ad0b..129db4c26 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -47,19 +47,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -79,11 +79,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ message: NamespaceA_ServiceARequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -105,11 +105,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -154,19 +154,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -186,11 +186,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( + let request = GRPCCore.StreamingClientRequest( metadata: metadata, producer: requestProducer ) @@ -212,11 +212,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -261,19 +261,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, @@ -291,9 +291,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ message: NamespaceA_ServiceARequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -315,11 +315,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -362,19 +362,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.methodA( request: request, @@ -392,9 +392,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( + let request = GRPCCore.StreamingClientRequest( metadata: metadata, producer: requestProducer ) @@ -416,11 +416,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA public func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, @@ -471,28 +471,28 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable /// Documentation for MethodB func methodB( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { package func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -506,9 +506,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } package func methodB( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.methodB( request: request, @@ -526,11 +526,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( + let request = GRPCCore.StreamingClientRequest( metadata: metadata, producer: requestProducer ) @@ -546,9 +546,9 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ message: NamespaceA_ServiceARequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -570,11 +570,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA package func methodA( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -590,11 +590,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodB package func methodB( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -637,19 +637,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ClientProtocol { internal func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -669,11 +669,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ message: ServiceARequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -695,11 +695,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { /// Documentation for MethodA internal func methodA( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 9125eea40..d2b0f624c 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -55,9 +55,9 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod func unary( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -82,22 +82,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod func unary( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse } /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func unary( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.unary( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } } """ @@ -139,9 +139,9 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -166,22 +166,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse } /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { package func inputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.inputStreaming( request: request, context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } } """ @@ -227,9 +227,9 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for outputStreamingMethod func outputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -254,19 +254,19 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for outputStreamingMethod func outputStreaming( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func outputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.outputStreaming( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) return response @@ -315,9 +315,9 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -342,9 +342,9 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { package protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for bidirectionalStreamingMethod func bidirectionalStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -405,15 +405,15 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for inputStreamingMethod func inputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// Documentation for outputStreamingMethod func outputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -449,36 +449,36 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for inputStreamingMethod func inputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse /// Documentation for outputStreamingMethod func outputStreaming( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { internal func inputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.inputStreaming( request: request, context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } internal func outputStreaming( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.outputStreaming( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) return response @@ -519,9 +519,9 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for MethodA func methodA( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @@ -546,22 +546,22 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { internal protocol ServiceA_ServiceProtocol: ServiceA.StreamingServiceProtocol { /// Documentation for MethodA func methodA( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse } /// Partial conformance to `ServiceA_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ServiceProtocol { internal func methodA( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.methodA( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } } """ diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift index 7d0304260..5e802a622 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift @@ -22,8 +22,8 @@ import XCTest final class ClientRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let (messages, continuation) = AsyncStream.makeStream(of: String.self) - let single = ClientRequest.Single(message: "foo", metadata: ["bar": "baz"]) - let stream = ClientRequest.Stream(single: single) + let single = ClientRequest(message: "foo", metadata: ["bar": "baz"]) + let stream = StreamingClientRequest(single: single) XCTAssertEqual(stream.metadata, ["bar": "baz"]) try await stream.producer(.gathering(into: continuation)) diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index a284112be..d0fcf3611 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -21,7 +21,7 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ClientResponseTests: XCTestCase { func testAcceptedSingleResponseConvenienceMethods() { - let response = ClientResponse.Single( + let response = ClientResponse( message: "message", metadata: ["foo": "bar"], trailingMetadata: ["bar": "baz"] @@ -34,7 +34,7 @@ final class ClientResponseTests: XCTestCase { func testRejectedSingleResponseConvenienceMethods() { let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"]) - let response = ClientResponse.Single(of: String.self, error: error) + let response = ClientResponse(of: String.self, error: error) XCTAssertEqual(response.metadata, [:]) XCTAssertThrowsRPCError(try response.message) { @@ -45,7 +45,7 @@ final class ClientResponseTests: XCTestCase { func testAcceptedButFailedSingleResponseConvenienceMethods() { let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"]) - let response = ClientResponse.Single(of: String.self, metadata: ["foo": "bar"], error: error) + let response = ClientResponse(of: String.self, metadata: ["foo": "bar"], error: error) XCTAssertEqual(response.metadata, ["foo": "bar"]) XCTAssertThrowsRPCError(try response.message) { @@ -55,7 +55,7 @@ final class ClientResponseTests: XCTestCase { } func testAcceptedStreamResponseConvenienceMethods() async throws { - let response = ClientResponse.Stream( + let response = StreamingClientResponse( of: String.self, metadata: ["foo": "bar"], bodyParts: RPCAsyncSequence( @@ -76,7 +76,7 @@ final class ClientResponseTests: XCTestCase { func testRejectedStreamResponseConvenienceMethods() async throws { let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"]) - let response = ClientResponse.Stream(of: String.self, error: error) + let response = StreamingClientResponse(of: String.self, error: error) XCTAssertEqual(response.metadata, [:]) await XCTAssertThrowsRPCErrorAsync { @@ -87,13 +87,13 @@ final class ClientResponseTests: XCTestCase { } func testStreamToSingleConversionForValidStream() async throws { - let stream = ClientResponse.Stream( + let stream = StreamingClientResponse( of: String.self, metadata: ["foo": "bar"], bodyParts: .elements(.message("foo"), .trailingMetadata(["bar": "baz"])) ) - let single = await ClientResponse.Single(stream: stream) + let single = await ClientResponse(stream: stream) XCTAssertEqual(single.metadata, ["foo": "bar"]) XCTAssertEqual(try single.message, "foo") XCTAssertEqual(single.trailingMetadata, ["bar": "baz"]) @@ -101,9 +101,9 @@ final class ClientResponseTests: XCTestCase { func testStreamToSingleConversionForFailedStream() async throws { let error = RPCError(code: .aborted, message: "aborted", metadata: ["bar": "baz"]) - let stream = ClientResponse.Stream(of: String.self, error: error) + let stream = StreamingClientResponse(of: String.self, error: error) - let single = await ClientResponse.Single(stream: stream) + let single = await ClientResponse(stream: stream) XCTAssertEqual(single.metadata, [:]) XCTAssertThrowsRPCError(try single.message) { XCTAssertEqual($0, error) @@ -112,19 +112,19 @@ final class ClientResponseTests: XCTestCase { } func testStreamToSingleConversionForInvalidSingleStream() async throws { - let bodies: [[ClientResponse.Stream.Contents.BodyPart]] = [ + let bodies: [[StreamingClientResponse.Contents.BodyPart]] = [ [.message("1"), .message("2")], // Too many messages. [.trailingMetadata([:])], // Too few messages ] for body in bodies { - let stream = ClientResponse.Stream( + let stream = StreamingClientResponse( of: String.self, metadata: ["foo": "bar"], bodyParts: .elements(body) ) - let single = await ClientResponse.Single(stream: stream) + let single = await ClientResponse(stream: stream) XCTAssertEqual(single.metadata, [:]) XCTAssertThrowsRPCError(try single.message) { error in XCTAssertEqual(error.code, .unimplemented) @@ -134,20 +134,20 @@ final class ClientResponseTests: XCTestCase { } func testStreamToSingleConversionForInvalidStream() async throws { - let bodies: [[ClientResponse.Stream.Contents.BodyPart]] = [ + let bodies: [[StreamingClientResponse.Contents.BodyPart]] = [ [], // Empty stream [.trailingMetadata([:]), .trailingMetadata([:])], // Multiple metadatas [.trailingMetadata([:]), .message("")], // Metadata then message ] for body in bodies { - let stream = ClientResponse.Stream( + let stream = StreamingClientResponse( of: String.self, metadata: ["foo": "bar"], bodyParts: .elements(body) ) - let single = await ClientResponse.Single(stream: stream) + let single = await ClientResponse(stream: stream) XCTAssertEqual(single.metadata, [:]) XCTAssertThrowsRPCError(try single.message) { error in XCTAssertEqual(error.code, .internalError) @@ -158,26 +158,26 @@ final class ClientResponseTests: XCTestCase { func testStreamToSingleConversionForStreamThrowingRPCError() async throws { let error = RPCError(code: .dataLoss, message: "oops") - let stream = ClientResponse.Stream( + let stream = StreamingClientResponse( of: String.self, metadata: [:], bodyParts: .throwing(error) ) - let single = await ClientResponse.Single(stream: stream) + let single = await ClientResponse(stream: stream) XCTAssertThrowsRPCError(try single.message) { XCTAssertEqual($0, error) } } func testStreamToSingleConversionForStreamThrowingUnknownError() async throws { - let stream = ClientResponse.Stream( + let stream = StreamingClientResponse( of: String.self, metadata: [:], bodyParts: .throwing(CancellationError()) ) - let single = await ClientResponse.Single(stream: stream) + let single = await ClientResponse(stream: stream) XCTAssertThrowsRPCError(try single.message) { error in XCTAssertEqual(error.code, .unknown) } diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 7bfef6912..4ead582fd 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -67,41 +67,45 @@ struct ClientRPCExecutorTestHarness { } func unary( - request: ClientRequest.Single<[UInt8]>, + request: ClientRequest<[UInt8]>, options: CallOptions = .defaults, - handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void + handler: @escaping @Sendable (ClientResponse<[UInt8]>) async throws -> Void ) async throws { - try await self.bidirectional(request: ClientRequest.Stream(single: request), options: options) { - response in - try await handler(ClientResponse.Single(stream: response)) + try await self.bidirectional( + request: StreamingClientRequest(single: request), + options: options + ) { response in + try await handler(ClientResponse(stream: response)) } } func clientStreaming( - request: ClientRequest.Stream<[UInt8]>, + request: StreamingClientRequest<[UInt8]>, options: CallOptions = .defaults, - handler: @escaping @Sendable (ClientResponse.Single<[UInt8]>) async throws -> Void + handler: @escaping @Sendable (ClientResponse<[UInt8]>) async throws -> Void ) async throws { try await self.bidirectional(request: request, options: options) { response in - try await handler(ClientResponse.Single(stream: response)) + try await handler(ClientResponse(stream: response)) } } func serverStreaming( - request: ClientRequest.Single<[UInt8]>, + request: ClientRequest<[UInt8]>, options: CallOptions = .defaults, - handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void + handler: @escaping @Sendable (StreamingClientResponse<[UInt8]>) async throws -> Void ) async throws { - try await self.bidirectional(request: ClientRequest.Stream(single: request), options: options) { - response in + try await self.bidirectional( + request: StreamingClientRequest(single: request), + options: options + ) { response in try await handler(response) } } func bidirectional( - request: ClientRequest.Stream<[UInt8]>, + request: StreamingClientRequest<[UInt8]>, options: CallOptions = .defaults, - handler: @escaping @Sendable (ClientResponse.Stream<[UInt8]>) async throws -> Void + handler: @escaping @Sendable (StreamingClientResponse<[UInt8]>) async throws -> Void ) async throws { try await self.execute( request: request, @@ -113,11 +117,11 @@ struct ClientRPCExecutorTestHarness { } private func execute( - request: ClientRequest.Stream, + request: StreamingClientRequest, serializer: some MessageSerializer, deserializer: some MessageDeserializer, options: CallOptions, - handler: @escaping @Sendable (ClientResponse.Stream) async throws -> Void + handler: @escaping @Sendable (StreamingClientResponse) async throws -> Void ) async throws { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index 6dceb9976..ae924eea8 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -24,7 +24,7 @@ extension ClientRPCExecutorTests { ) try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -48,7 +48,7 @@ extension ClientRPCExecutorTests { ) try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -84,7 +84,7 @@ extension ClientRPCExecutorTests { let start = ContinuousClock.now try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -128,7 +128,7 @@ extension ClientRPCExecutorTests { let start = ContinuousClock.now try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -169,7 +169,7 @@ extension ClientRPCExecutorTests { ) try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) }, options: .hedge(delay: .seconds(60), nonFatalCodes: [.unavailable]) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index 6c2e6cd1e..2c5ab960d 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -44,7 +44,7 @@ extension ClientRPCExecutorTests { consumeInboundStream: true ) try await harness.bidirectional( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -70,7 +70,7 @@ extension ClientRPCExecutorTests { func testRetriesRespectRetryableCodes() async throws { let harness = self.makeHarnessForRetries(rejectUntilAttempt: 3, withCode: .unavailable) try await harness.bidirectional( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([0, 1, 2]) }, options: .retry(codes: [.aborted]) @@ -91,7 +91,7 @@ extension ClientRPCExecutorTests { func testRetriesRespectRetryLimit() async throws { let harness = self.makeHarnessForRetries(rejectUntilAttempt: 5, withCode: .unavailable) try await harness.bidirectional( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([0, 1, 2]) }, options: .retry(maximumAttempts: 2, codes: [.unavailable]) @@ -118,7 +118,7 @@ extension ClientRPCExecutorTests { ) try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { for _ in 0 ..< 1000 { try await $0.write([]) } @@ -148,7 +148,7 @@ extension ClientRPCExecutorTests { await XCTAssertThrowsErrorAsync { try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -169,7 +169,7 @@ extension ClientRPCExecutorTests { await XCTAssertThrowsErrorAsync { try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -193,7 +193,7 @@ extension ClientRPCExecutorTests { await XCTAssertThrowsErrorAsync { try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) try await $0.write([1]) try await $0.write([2]) @@ -233,7 +233,7 @@ extension ClientRPCExecutorTests { let start = ContinuousClock.now try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) }, options: .retry(retryPolicy) @@ -269,7 +269,7 @@ extension ClientRPCExecutorTests { ) try await harness.bidirectional( - request: ClientRequest.Stream { + request: StreamingClientRequest { try await $0.write([0]) }, options: .retry(retryPolicy) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index c2396fdef..310486aa3 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -23,7 +23,7 @@ final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) try await tester.unary( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + request: ClientRequest(message: [1, 2, 3], metadata: ["foo": "bar"]) ) { response in XCTAssertEqual(response.metadata, ["foo": "bar"]) XCTAssertEqual(try response.message, [1, 2, 3]) @@ -36,7 +36,7 @@ final class ClientRPCExecutorTests: XCTestCase { func testClientStreamingEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) try await tester.clientStreaming( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([1, 2, 3]) } ) { response in @@ -51,7 +51,7 @@ final class ClientRPCExecutorTests: XCTestCase { func testServerStreamingEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) try await tester.serverStreaming( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + request: ClientRequest(message: [1, 2, 3], metadata: ["foo": "bar"]) ) { response in XCTAssertEqual(response.metadata, ["foo": "bar"]) let messages = try await response.messages.collect() @@ -65,7 +65,7 @@ final class ClientRPCExecutorTests: XCTestCase { func testBidirectionalStreamingEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) try await tester.bidirectional( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([1, 2, 3]) } ) { response in @@ -82,7 +82,7 @@ final class ClientRPCExecutorTests: XCTestCase { let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) try await tester.unary( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + request: ClientRequest(message: [1, 2, 3], metadata: ["foo": "bar"]) ) { response in XCTAssertThrowsRPCError(try response.message) { XCTAssertEqual($0, error) @@ -97,7 +97,7 @@ final class ClientRPCExecutorTests: XCTestCase { let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) try await tester.clientStreaming( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([1, 2, 3]) } ) { response in @@ -114,7 +114,7 @@ final class ClientRPCExecutorTests: XCTestCase { let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) try await tester.serverStreaming( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + request: ClientRequest(message: [1, 2, 3], metadata: ["foo": "bar"]) ) { response in await XCTAssertThrowsRPCErrorAsync { try await response.messages.collect() @@ -131,7 +131,7 @@ final class ClientRPCExecutorTests: XCTestCase { let error = RPCError(code: .unauthenticated, message: "", metadata: ["metadata": "error"]) let tester = ClientRPCExecutorTestHarness(server: .reject(withError: error)) try await tester.bidirectional( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([1, 2, 3]) } ) { response in @@ -154,7 +154,7 @@ final class ClientRPCExecutorTests: XCTestCase { await XCTAssertThrowsRPCErrorAsync { try await tester.unary( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + request: ClientRequest(message: [1, 2, 3], metadata: ["foo": "bar"]) ) { _ in } } errorHandler: { error in XCTAssertEqual(error.code, .aborted) @@ -173,7 +173,7 @@ final class ClientRPCExecutorTests: XCTestCase { await XCTAssertThrowsRPCErrorAsync { try await tester.clientStreaming( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([1, 2, 3]) } ) { _ in } @@ -194,7 +194,7 @@ final class ClientRPCExecutorTests: XCTestCase { await XCTAssertThrowsRPCErrorAsync { try await tester.serverStreaming( - request: ClientRequest.Single(message: [1, 2, 3], metadata: ["foo": "bar"]) + request: ClientRequest(message: [1, 2, 3], metadata: ["foo": "bar"]) ) { _ in } } errorHandler: { XCTAssertEqual($0.code, .aborted) @@ -213,7 +213,7 @@ final class ClientRPCExecutorTests: XCTestCase { await XCTAssertThrowsRPCErrorAsync { try await tester.bidirectional( - request: ClientRequest.Stream(metadata: ["foo": "bar"]) { + request: StreamingClientRequest(metadata: ["foo": "bar"]) { try await $0.write([1, 2, 3]) } ) { _ in } @@ -254,7 +254,7 @@ final class ClientRPCExecutorTests: XCTestCase { let tester = ClientRPCExecutorTestHarness(transport: .inProcess, server: .echo) try await tester.unary( - request: ClientRequest.Single(message: []), + request: ClientRequest(message: []), options: options ) { response in let timeoutMetadata = Array(response.metadata[stringValues: "grpc-timeout"]) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index 8d7e0a543..f53c46232 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -20,19 +20,20 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ServerRPCExecutorTestHarness { struct ServerHandler: Sendable { - let fn: @Sendable (ServerRequest.Stream) async throws -> ServerResponse.Stream + let fn: + @Sendable (StreamingServerRequest) async throws -> StreamingServerResponse init( _ fn: @escaping @Sendable ( - ServerRequest.Stream - ) async throws -> ServerResponse.Stream + StreamingServerRequest + ) async throws -> StreamingServerResponse ) { self.fn = fn } func handle( - _ request: ServerRequest.Stream - ) async throws -> ServerResponse.Stream { + _ request: StreamingServerRequest + ) async throws -> StreamingServerResponse { try await self.fn(request) } @@ -51,8 +52,8 @@ struct ServerRPCExecutorTestHarness { deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @escaping @Sendable ( - ServerRequest.Stream - ) async throws -> ServerResponse.Stream, + StreamingServerRequest + ) async throws -> StreamingServerResponse, producer: @escaping @Sendable ( RPCWriter.Closable ) async throws -> Void, @@ -137,7 +138,7 @@ struct ServerRPCExecutorTestHarness { extension ServerRPCExecutorTestHarness.ServerHandler where Input == Output { static var echo: Self { return Self { request in - return ServerResponse.Stream(metadata: request.metadata) { writer in + return StreamingServerResponse(metadata: request.metadata) { writer in try await writer.write(contentsOf: request.messages) return [:] } diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 5d2aa0029..df894dd8e 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -87,7 +87,7 @@ final class ServerRPCExecutorTests: XCTestCase { ) { request in let messages = try await request.messages.collect() XCTAssertEqual(messages, ["hello"]) - return ServerResponse.Stream(metadata: request.metadata) { writer in + return StreamingServerResponse(metadata: request.metadata) { writer in try await writer.write("hello") return [:] } @@ -116,7 +116,7 @@ final class ServerRPCExecutorTests: XCTestCase { ) { request in let messages = try await request.messages.collect() XCTAssertEqual(messages, ["hello", "world"]) - return ServerResponse.Stream(metadata: request.metadata) { writer in + return StreamingServerResponse(metadata: request.metadata) { writer in try await writer.write("hello") try await writer.write("world") return [:] @@ -146,7 +146,7 @@ final class ServerRPCExecutorTests: XCTestCase { deserializer: IdentityDeserializer(), serializer: IdentitySerializer() ) { request in - return ServerResponse.Stream(metadata: request.metadata) { _ in + return StreamingServerResponse(metadata: request.metadata) { _ in return ["bar": "baz"] } } producer: { inbound in @@ -244,7 +244,7 @@ final class ServerRPCExecutorTests: XCTestCase { } XCTFail("Server handler should've been cancelled by timeout.") - return ServerResponse.Stream(error: RPCError(code: .failedPrecondition, message: "")) + return StreamingServerResponse(error: RPCError(code: .failedPrecondition, message: "")) } producer: { inbound in try await inbound.write(.metadata(["grpc-timeout": "1000n"])) await inbound.finish() @@ -271,7 +271,7 @@ final class ServerRPCExecutorTests: XCTestCase { serializer: IdentitySerializer() ) { request in XCTFail("Unexpected request") - return ServerResponse.Stream( + return StreamingServerResponse( of: [UInt8].self, error: RPCError(code: .failedPrecondition, message: "") ) diff --git a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift index 532e5e51c..658a2855e 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift @@ -19,8 +19,8 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ServerRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { - let single = ServerRequest.Single(metadata: ["bar": "baz"], message: "foo") - let stream = ServerRequest.Stream(single: single) + let single = ServerRequest(metadata: ["bar": "baz"], message: "foo") + let stream = StreamingServerRequest(single: single) XCTAssertEqual(stream.metadata, ["bar": "baz"]) let collected = try await stream.messages.collect() diff --git a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift index d5614e906..02196a9dd 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift @@ -19,7 +19,7 @@ import XCTest @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ServerResponseTests: XCTestCase { func testSingleConvenienceInit() { - var response = ServerResponse.Single( + var response = ServerResponse( message: "message", metadata: ["metadata": "initial"], trailingMetadata: ["metadata": "trailing"] @@ -35,7 +35,7 @@ final class ServerResponseTests: XCTestCase { } let error = RPCError(code: .aborted, message: "Aborted") - response = ServerResponse.Single(of: String.self, error: error) + response = ServerResponse(of: String.self, error: error) switch response.accepted { case .success: XCTFail("Unexpected success") @@ -45,7 +45,10 @@ final class ServerResponseTests: XCTestCase { } func testStreamConvenienceInit() async throws { - var response = ServerResponse.Stream(of: String.self, metadata: ["metadata": "initial"]) { _ in + var response = StreamingServerResponse( + of: String.self, + metadata: ["metadata": "initial"] + ) { _ in // Empty body. return ["metadata": "trailing"] } @@ -60,7 +63,7 @@ final class ServerResponseTests: XCTestCase { } let error = RPCError(code: .aborted, message: "Aborted") - response = ServerResponse.Stream(of: String.self, error: error) + response = StreamingServerResponse(of: String.self, error: error) switch response.accepted { case .success: XCTFail("Unexpected success") @@ -70,13 +73,13 @@ final class ServerResponseTests: XCTestCase { } func testSingleToStreamConversionForSuccessfulResponse() async throws { - let single = ServerResponse.Single( + let single = ServerResponse( message: "foo", metadata: ["metadata": "initial"], trailingMetadata: ["metadata": "trailing"] ) - let stream = ServerResponse.Stream(single: single) + let stream = StreamingServerResponse(single: single) let (messages, continuation) = AsyncStream.makeStream(of: String.self) let trailingMetadata: Metadata @@ -96,8 +99,8 @@ final class ServerResponseTests: XCTestCase { func testSingleToStreamConversionForFailedResponse() async throws { let error = RPCError(code: .aborted, message: "aborted") - let single = ServerResponse.Single(of: String.self, error: error) - let stream = ServerResponse.Stream(single: single) + let single = ServerResponse(of: String.self, error: error) + let stream = StreamingServerResponse(single: single) XCTAssertThrowsRPCError(try stream.accepted.get()) { XCTAssertEqual($0, error) diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index b5558ae48..f55f921dd 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -356,7 +356,7 @@ final class GRPCClientTests: XCTestCase { let task = Task { try await client.clientStreaming( - request: ClientRequest.Stream { writer in + request: StreamingClientRequest { writer in try await Task.sleep(for: .seconds(5)) }, descriptor: BinaryEcho.Methods.collect, diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index a45d64fd5..dd73cb96d 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -50,17 +50,17 @@ struct RejectAllClientInterceptor: ClientInterceptor { } func intercept( - request: ClientRequest.Stream, + request: StreamingClientRequest, context: ClientContext, next: ( - ClientRequest.Stream, + StreamingClientRequest, ClientContext - ) async throws -> ClientResponse.Stream - ) async throws -> ClientResponse.Stream { + ) async throws -> StreamingClientResponse + ) async throws -> StreamingClientResponse { if self.throw { throw self.error } else { - return ClientResponse.Stream(error: self.error) + return StreamingClientResponse(error: self.error) } } } @@ -75,13 +75,13 @@ struct RequestCountingClientInterceptor: ClientInterceptor { } func intercept( - request: ClientRequest.Stream, + request: StreamingClientRequest, context: ClientContext, next: ( - ClientRequest.Stream, + StreamingClientRequest, ClientContext - ) async throws -> ClientResponse.Stream - ) async throws -> ClientResponse.Stream { + ) async throws -> StreamingClientResponse + ) async throws -> StreamingClientResponse { self.counter.increment() return try await next(request, context) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 9a3dc96c7..1467cddad 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -49,17 +49,17 @@ struct RejectAllServerInterceptor: ServerInterceptor { } func intercept( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext, next: @Sendable ( - ServerRequest.Stream, + StreamingServerRequest, ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse + ) async throws -> StreamingServerResponse { if self.throw { throw self.error } else { - return ServerResponse.Stream(error: self.error) + return StreamingServerResponse(error: self.error) } } } @@ -74,13 +74,13 @@ struct RequestCountingServerInterceptor: ServerInterceptor { } func intercept( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext, next: @Sendable ( - ServerRequest.Stream, + StreamingServerRequest, ServerContext - ) async throws -> ServerResponse.Stream - ) async throws -> ServerResponse.Stream { + ) async throws -> StreamingServerResponse + ) async throws -> StreamingServerResponse { self.counter.increment() return try await next(request, context) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index e5438c550..817539a9d 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -19,22 +19,22 @@ import XCTest @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct BinaryEcho: RegistrableRPCService { func get( - _ request: ServerRequest.Single<[UInt8]> - ) async throws -> ServerResponse.Single<[UInt8]> { - ServerResponse.Single(message: request.message, metadata: request.metadata) + _ request: ServerRequest<[UInt8]> + ) async throws -> ServerResponse<[UInt8]> { + ServerResponse(message: request.message, metadata: request.metadata) } func collect( - _ request: ServerRequest.Stream<[UInt8]> - ) async throws -> ServerResponse.Single<[UInt8]> { + _ request: StreamingServerRequest<[UInt8]> + ) async throws -> ServerResponse<[UInt8]> { let collected = try await request.messages.reduce(into: []) { $0.append(contentsOf: $1) } - return ServerResponse.Single(message: collected, metadata: request.metadata) + return ServerResponse(message: collected, metadata: request.metadata) } func expand( - _ request: ServerRequest.Single<[UInt8]> - ) async throws -> ServerResponse.Stream<[UInt8]> { - return ServerResponse.Stream(metadata: request.metadata) { + _ request: ServerRequest<[UInt8]> + ) async throws -> StreamingServerResponse<[UInt8]> { + return StreamingServerResponse(metadata: request.metadata) { for byte in request.message { try await $0.write([byte]) } @@ -43,9 +43,9 @@ struct BinaryEcho: RegistrableRPCService { } func update( - _ request: ServerRequest.Stream<[UInt8]> - ) async throws -> ServerResponse.Stream<[UInt8]> { - return ServerResponse.Stream(metadata: request.metadata) { + _ request: StreamingServerRequest<[UInt8]> + ) async throws -> StreamingServerResponse<[UInt8]> { + return StreamingServerResponse(metadata: request.metadata) { for try await message in request.messages { try await $0.write(message) } @@ -62,9 +62,9 @@ struct BinaryEcho: RegistrableRPCService { deserializer: deserializer, serializer: serializer ) { streamRequest, context in - let singleRequest = try await ServerRequest.Single(stream: streamRequest) + let singleRequest = try await ServerRequest(stream: streamRequest) let singleResponse = try await self.get(singleRequest) - return ServerResponse.Stream(single: singleResponse) + return StreamingServerResponse(single: singleResponse) } router.registerHandler( @@ -73,7 +73,7 @@ struct BinaryEcho: RegistrableRPCService { serializer: serializer ) { streamRequest, context in let singleResponse = try await self.collect(streamRequest) - return ServerResponse.Stream(single: singleResponse) + return StreamingServerResponse(single: singleResponse) } router.registerHandler( @@ -81,7 +81,7 @@ struct BinaryEcho: RegistrableRPCService { deserializer: deserializer, serializer: serializer ) { streamRequest, context in - let singleRequest = try await ServerRequest.Single(stream: streamRequest) + let singleRequest = try await ServerRequest(stream: streamRequest) let streamResponse = try await self.expand(singleRequest) return streamResponse } diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index a6bb1ee50..400ab3037 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -97,7 +97,7 @@ func XCTAssertThrowsRPCErrorAsync( @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func XCTAssertRejected( - _ response: ClientResponse.Stream, + _ response: StreamingClientResponse, errorHandler: (RPCError) -> Void ) { switch response.accepted { @@ -109,7 +109,7 @@ func XCTAssertRejected( } func XCTAssertRejected( - _ response: ClientResponse.Single, + _ response: ClientResponse, errorHandler: (RPCError) -> Void ) { switch response.accepted { From 432c955d9781bfb4c20c813de042bc098d2277cf Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 1 Oct 2024 12:44:10 +0100 Subject: [PATCH 478/580] Update example (#2077) Motivation: Some names changed in https://github.com/grpc/grpc-swift/pull/2076 and now our examples don't compile. Modifications: - Update examples Result: Examples compile --- .../echo/Sources/Generated/echo.grpc.swift | 116 +++++++++--------- Examples/echo/Sources/Subcommands/Serve.swift | 24 ++-- .../Sources/Generated/helloworld.grpc.swift | 32 ++--- .../Sources/Subcommands/Serve.swift | 6 +- .../Sources/Generated/route_guide.grpc.swift | 116 +++++++++--------- .../Sources/Subcommands/Serve.swift | 26 ++-- 6 files changed, 160 insertions(+), 160 deletions(-) diff --git a/Examples/echo/Sources/Generated/echo.grpc.swift b/Examples/echo/Sources/Generated/echo.grpc.swift index a782d7d4a..88b929b9a 100644 --- a/Examples/echo/Sources/Generated/echo.grpc.swift +++ b/Examples/echo/Sources/Generated/echo.grpc.swift @@ -87,27 +87,27 @@ extension GRPCCore.ServiceDescriptor { internal protocol Echo_Echo_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Immediately returns an echo of a request. func get( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// Splits a request into words and returns each word in a stream of messages. func expand( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// Collects a stream of messages and returns them concatenated when the caller closes. func collect( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// Streams back messages as they are received in an input stream. func update( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -166,63 +166,63 @@ extension Echo_Echo.StreamingServiceProtocol { internal protocol Echo_Echo_ServiceProtocol: Echo_Echo.StreamingServiceProtocol { /// Immediately returns an echo of a request. func get( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse /// Splits a request into words and returns each word in a stream of messages. func expand( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// Collects a stream of messages and returns them concatenated when the caller closes. func collect( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse /// Streams back messages as they are received in an input stream. func update( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Partial conformance to `Echo_Echo_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ServiceProtocol { internal func get( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.get( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } internal func expand( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.expand( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) return response } internal func collect( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.collect( request: request, context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } } @@ -230,47 +230,47 @@ extension Echo_Echo.ServiceProtocol { internal protocol Echo_Echo_ClientProtocol: Sendable { /// Immediately returns an echo of a request. func get( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable /// Splits a request into words and returns each word in a stream of messages. func expand( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable /// Collects a stream of messages and returns them concatenated when the caller closes. func collect( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable /// Streams back messages as they are received in an input stream. func update( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Echo_Echo.ClientProtocol { internal func get( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -284,9 +284,9 @@ extension Echo_Echo.ClientProtocol { } internal func expand( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.expand( request: request, @@ -298,9 +298,9 @@ extension Echo_Echo.ClientProtocol { } internal func collect( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -314,9 +314,9 @@ extension Echo_Echo.ClientProtocol { } internal func update( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.update( request: request, @@ -335,11 +335,11 @@ extension Echo_Echo.ClientProtocol { _ message: Echo_EchoRequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -355,9 +355,9 @@ extension Echo_Echo.ClientProtocol { _ message: Echo_EchoRequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -373,11 +373,11 @@ extension Echo_Echo.ClientProtocol { metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( + let request = GRPCCore.StreamingClientRequest( metadata: metadata, producer: requestProducer ) @@ -393,9 +393,9 @@ extension Echo_Echo.ClientProtocol { metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( + let request = GRPCCore.StreamingClientRequest( metadata: metadata, producer: requestProducer ) @@ -417,11 +417,11 @@ internal struct Echo_Echo_Client: Echo_Echo.ClientProtocol { /// Immediately returns an echo of a request. internal func get( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -437,11 +437,11 @@ internal struct Echo_Echo_Client: Echo_Echo.ClientProtocol { /// Splits a request into words and returns each word in a stream of messages. internal func expand( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -455,11 +455,11 @@ internal struct Echo_Echo_Client: Echo_Echo.ClientProtocol { /// Collects a stream of messages and returns them concatenated when the caller closes. internal func collect( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -475,11 +475,11 @@ internal struct Echo_Echo_Client: Echo_Echo.ClientProtocol { /// Streams back messages as they are received in an input stream. internal func update( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, diff --git a/Examples/echo/Sources/Subcommands/Serve.swift b/Examples/echo/Sources/Subcommands/Serve.swift index b5e126788..396ecef55 100644 --- a/Examples/echo/Sources/Subcommands/Serve.swift +++ b/Examples/echo/Sources/Subcommands/Serve.swift @@ -45,26 +45,26 @@ struct Serve: AsyncParsableCommand { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EchoService: Echo_Echo_ServiceProtocol { func get( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { - return ServerResponse.Single(message: .with { $0.text = request.message.text }) + ) async throws -> ServerResponse { + return ServerResponse(message: .with { $0.text = request.message.text }) } func collect( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let messages = try await request.messages.reduce(into: []) { $0.append($1.text) } let joined = messages.joined(separator: " ") - return ServerResponse.Single(message: .with { $0.text = joined }) + return ServerResponse(message: .with { $0.text = joined }) } func expand( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in let parts = request.message.text.split(separator: " ") let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } } try await writer.write(contentsOf: messages) @@ -73,10 +73,10 @@ struct EchoService: Echo_Echo_ServiceProtocol { } func update( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for try await message in request.messages { try await writer.write(.with { $0.text = message.text }) } diff --git a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift index 7ae35011b..bc729483a 100644 --- a/Examples/hello-world/Sources/Generated/helloworld.grpc.swift +++ b/Examples/hello-world/Sources/Generated/helloworld.grpc.swift @@ -61,9 +61,9 @@ extension GRPCCore.ServiceDescriptor { internal protocol Helloworld_Greeter_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting func sayHello( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -90,23 +90,23 @@ extension Helloworld_Greeter.StreamingServiceProtocol { internal protocol Helloworld_Greeter_ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { /// Sends a greeting func sayHello( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse } /// Partial conformance to `Helloworld_Greeter_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.ServiceProtocol { internal func sayHello( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.sayHello( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } } @@ -115,20 +115,20 @@ extension Helloworld_Greeter.ServiceProtocol { internal protocol Helloworld_Greeter_ClientProtocol: Sendable { /// Sends a greeting func sayHello( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Helloworld_Greeter.ClientProtocol { internal func sayHello( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -149,11 +149,11 @@ extension Helloworld_Greeter.ClientProtocol { _ message: Helloworld_HelloRequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -176,11 +176,11 @@ internal struct Helloworld_Greeter_Client: Helloworld_Greeter.ClientProtocol { /// Sends a greeting internal func sayHello( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { diff --git a/Examples/hello-world/Sources/Subcommands/Serve.swift b/Examples/hello-world/Sources/Subcommands/Serve.swift index 6ae06d013..ef5dc2935 100644 --- a/Examples/hello-world/Sources/Subcommands/Serve.swift +++ b/Examples/hello-world/Sources/Subcommands/Serve.swift @@ -44,12 +44,12 @@ struct Serve: AsyncParsableCommand { struct Greeter: Helloworld_Greeter_ServiceProtocol { func sayHello( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { var reply = Helloworld_HelloReply() let recipient = request.message.name.isEmpty ? "stranger" : request.message.name reply.message = "Hello, \(recipient)" - return ServerResponse.Single(message: reply) + return ServerResponse(message: reply) } } diff --git a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift index 3e9f77ffc..2468fd7d7 100644 --- a/Examples/route-guide/Sources/Generated/route_guide.grpc.swift +++ b/Examples/route-guide/Sources/Generated/route_guide.grpc.swift @@ -93,9 +93,9 @@ internal protocol Routeguide_RouteGuide_StreamingServiceProtocol: GRPCCore.Regis /// A feature with an empty name is returned if there's no feature at the given /// position. func getFeature( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// A server-to-client streaming RPC. /// @@ -104,27 +104,27 @@ internal protocol Routeguide_RouteGuide_StreamingServiceProtocol: GRPCCore.Regis /// repeated field), as the rectangle may cover a large area and contain a /// huge number of features. func listFeatures( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// A client-to-server streaming RPC. /// /// Accepts a stream of Points on a route being traversed, returning a /// RouteSummary when traversal is completed. func recordRoute( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// A Bidirectional streaming RPC. /// /// Accepts a stream of RouteNotes sent while a route is being traversed, /// while receiving other RouteNotes (e.g. from other users). func routeChat( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. @@ -189,9 +189,9 @@ internal protocol Routeguide_RouteGuide_ServiceProtocol: Routeguide_RouteGuide.S /// A feature with an empty name is returned if there's no feature at the given /// position. func getFeature( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse /// A server-to-client streaming RPC. /// @@ -200,63 +200,63 @@ internal protocol Routeguide_RouteGuide_ServiceProtocol: Routeguide_RouteGuide.S /// repeated field), as the rectangle may cover a large area and contain a /// huge number of features. func listFeatures( - request: GRPCCore.ServerRequest.Single, + request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse /// A client-to-server streaming RPC. /// /// Accepts a stream of Points on a route being traversed, returning a /// RouteSummary when traversal is completed. func recordRoute( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Single + ) async throws -> GRPCCore.ServerResponse /// A Bidirectional streaming RPC. /// /// Accepts a stream of RouteNotes sent while a route is being traversed, /// while receiving other RouteNotes (e.g. from other users). func routeChat( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream + ) async throws -> GRPCCore.StreamingServerResponse } /// Partial conformance to `Routeguide_RouteGuide_StreamingServiceProtocol`. @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Routeguide_RouteGuide.ServiceProtocol { internal func getFeature( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.getFeature( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } internal func listFeatures( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.listFeatures( - request: GRPCCore.ServerRequest.Single(stream: request), + request: GRPCCore.ServerRequest(stream: request), context: context ) return response } internal func recordRoute( - request: GRPCCore.ServerRequest.Stream, + request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse.Stream { + ) async throws -> GRPCCore.StreamingServerResponse { let response = try await self.recordRoute( request: request, context: context ) - return GRPCCore.ServerResponse.Stream(single: response) + return GRPCCore.StreamingServerResponse(single: response) } } @@ -270,11 +270,11 @@ internal protocol Routeguide_RouteGuide_ClientProtocol: Sendable { /// A feature with an empty name is returned if there's no feature at the given /// position. func getFeature( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable /// A server-to-client streaming RPC. @@ -284,11 +284,11 @@ internal protocol Routeguide_RouteGuide_ClientProtocol: Sendable { /// repeated field), as the rectangle may cover a large area and contain a /// huge number of features. func listFeatures( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable /// A client-to-server streaming RPC. @@ -296,11 +296,11 @@ internal protocol Routeguide_RouteGuide_ClientProtocol: Sendable { /// Accepts a stream of Points on a route being traversed, returning a /// RouteSummary when traversal is completed. func recordRoute( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable /// A Bidirectional streaming RPC. @@ -308,20 +308,20 @@ internal protocol Routeguide_RouteGuide_ClientProtocol: Sendable { /// Accepts a stream of RouteNotes sent while a route is being traversed, /// while receiving other RouteNotes (e.g. from other users). func routeChat( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension Routeguide_RouteGuide.ClientProtocol { internal func getFeature( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -335,9 +335,9 @@ extension Routeguide_RouteGuide.ClientProtocol { } internal func listFeatures( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.listFeatures( request: request, @@ -349,9 +349,9 @@ extension Routeguide_RouteGuide.ClientProtocol { } internal func recordRoute( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -365,9 +365,9 @@ extension Routeguide_RouteGuide.ClientProtocol { } internal func routeChat( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.routeChat( request: request, @@ -391,11 +391,11 @@ extension Routeguide_RouteGuide.ClientProtocol { _ message: Routeguide_Point, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -416,9 +416,9 @@ extension Routeguide_RouteGuide.ClientProtocol { _ message: Routeguide_Rectangle, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Single( + let request = GRPCCore.ClientRequest( message: message, metadata: metadata ) @@ -437,11 +437,11 @@ extension Routeguide_RouteGuide.ClientProtocol { metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> Result = { + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { try $0.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( + let request = GRPCCore.StreamingClientRequest( metadata: metadata, producer: requestProducer ) @@ -460,9 +460,9 @@ extension Routeguide_RouteGuide.ClientProtocol { metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> Result + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest.Stream( + let request = GRPCCore.StreamingClientRequest( metadata: metadata, producer: requestProducer ) @@ -490,11 +490,11 @@ internal struct Routeguide_RouteGuide_Client: Routeguide_RouteGuide.ClientProtoc /// A feature with an empty name is returned if there's no feature at the given /// position. internal func getFeature( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -515,11 +515,11 @@ internal struct Routeguide_RouteGuide_Client: Routeguide_RouteGuide.ClientProtoc /// repeated field), as the rectangle may cover a large area and contain a /// huge number of features. internal func listFeatures( - request: GRPCCore.ClientRequest.Single, + request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.client.serverStreaming( request: request, @@ -536,11 +536,11 @@ internal struct Routeguide_RouteGuide_Client: Routeguide_RouteGuide.ClientProtoc /// Accepts a stream of Points on a route being traversed, returning a /// RouteSummary when traversal is completed. internal func recordRoute( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Single) async throws -> R = { + _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { try $0.message } ) async throws -> R where R: Sendable { @@ -559,11 +559,11 @@ internal struct Routeguide_RouteGuide_Client: Routeguide_RouteGuide.ClientProtoc /// Accepts a stream of RouteNotes sent while a route is being traversed, /// while receiving other RouteNotes (e.g. from other users). internal func routeChat( - request: GRPCCore.ClientRequest.Stream, + request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse.Stream) async throws -> R + _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R ) async throws -> R where R: Sendable { try await self.client.bidirectionalStreaming( request: request, diff --git a/Examples/route-guide/Sources/Subcommands/Serve.swift b/Examples/route-guide/Sources/Subcommands/Serve.swift index 5bfdc200e..c799b4fb7 100644 --- a/Examples/route-guide/Sources/Subcommands/Serve.swift +++ b/Examples/route-guide/Sources/Subcommands/Serve.swift @@ -100,16 +100,16 @@ struct RouteGuideService { extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { func getFeature( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let feature = self.findFeature( latitude: request.message.latitude, longitude: request.message.longitude ) if let feature { - return ServerResponse.Single(message: feature) + return ServerResponse(message: feature) } else { // No feature: return a feature with an empty name. let unknownFeature = Routeguide_Feature.with { @@ -119,15 +119,15 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.longitude = request.message.longitude } } - return ServerResponse.Single(message: unknownFeature) + return ServerResponse(message: unknownFeature) } } func listFeatures( - request: ServerRequest.Single, + request: ServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in let featuresWithinBounds = self.features.filter { feature in !feature.name.isEmpty && feature.isContained(by: request.message) } @@ -138,9 +138,9 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { } func recordRoute( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Single { + ) async throws -> ServerResponse { let startTime = ContinuousClock.now var pointsVisited = 0 var featuresVisited = 0 @@ -169,14 +169,14 @@ extension RouteGuideService: Routeguide_RouteGuide.ServiceProtocol { $0.distance = Int32(distanceTravelled) } - return ServerResponse.Single(message: summary) + return ServerResponse(message: summary) } func routeChat( - request: ServerRequest.Stream, + request: StreamingServerRequest, context: ServerContext - ) async throws -> ServerResponse.Stream { - return ServerResponse.Stream { writer in + ) async throws -> StreamingServerResponse { + return StreamingServerResponse { writer in for try await note in request.messages { let notes = self.receivedNotes.recordNote(note) try await writer.write(contentsOf: notes) From 82b3a7556b1d04f14049622d3a5b9cf35a8fe162 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 1 Oct 2024 15:25:01 +0100 Subject: [PATCH 479/580] Update examples (#2078) Motivation: The examples depend on the main branch of our various packages. These can now use the tagged releases. Modifications: - Update dependencies in examples Result: Examples are up-to-date --- Examples/echo/Package.swift | 4 ++-- Examples/hello-world/Package.swift | 4 ++-- Examples/route-guide/Package.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index d9769fb16..4510456be 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift-protobuf", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "1.0.0-alpha.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 94c96c027..52a9b349a 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift-protobuf", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index baf6d2162..79e553920 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,8 +21,8 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ - .package(url: "https://github.com/grpc/grpc-swift-protobuf", branch: "main"), - .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "main"), + .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), + .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), ], targets: [ From 5287f05a9006b37e66d1fdc1e8f398d089809e3c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 4 Oct 2024 13:30:57 +0100 Subject: [PATCH 480/580] Add deployment targets to the package manifest (#2082) Motivation: Core components of grpc-swift v2 require API from the latest SDKs. This causes a proliferation of availability annotations through our API. Rather than doing this we can set the minimum platforms in the package manifest. Modifications: - Remove availability annotations - Set platforms in the package manifest Result: - Less boilerplate - Users must set platforms in their package manifest --- Examples/echo/Sources/Subcommands/Serve.swift | 1 - Examples/route-guide/Sources/RouteGuide.swift | 1 - Package.swift | 7 +++++++ Sources/GRPCCore/Call/Client/CallOptions.swift | 3 --- Sources/GRPCCore/Call/Client/ClientInterceptor.swift | 1 - Sources/GRPCCore/Call/Client/ClientRequest.swift | 1 - Sources/GRPCCore/Call/Client/ClientResponse.swift | 2 -- .../Internal/ClientRPCExecutor+HedgingExecutor.swift | 4 ---- .../Internal/ClientRPCExecutor+OneShotExecutor.swift | 4 ---- .../Internal/ClientRPCExecutor+RetryExecutor.swift | 3 --- .../Call/Client/Internal/ClientRPCExecutor.swift | 2 -- .../Client/Internal/ClientRequest+Convenience.swift | 1 - .../Client/Internal/ClientResponse+Convenience.swift | 3 --- .../Call/Client/Internal/ClientStreamExecutor.swift | 2 -- .../Call/Client/Internal/RetryDelaySequence.swift | 1 - .../Call/Server/Internal/ServerRPCExecutor.swift | 2 -- Sources/GRPCCore/Call/Server/RPCRouter.swift | 2 -- .../GRPCCore/Call/Server/RegistrableRPCService.swift | 1 - Sources/GRPCCore/Call/Server/ServerInterceptor.swift | 1 - Sources/GRPCCore/Call/Server/ServerRequest.swift | 3 --- Sources/GRPCCore/Call/Server/ServerResponse.swift | 3 --- Sources/GRPCCore/Configuration/MethodConfig.swift | 10 ---------- Sources/GRPCCore/Configuration/ServiceConfig.swift | 6 ------ Sources/GRPCCore/GRPCClient.swift | 1 - Sources/GRPCCore/GRPCServer.swift | 1 - Sources/GRPCCore/Internal/Metadata+GRPC.swift | 2 -- Sources/GRPCCore/Internal/MethodConfigs.swift | 1 - Sources/GRPCCore/Internal/Result+Catching.swift | 1 - .../GRPCCore/Internal/TaskGroup+CancellableTask.swift | 2 -- .../Streaming/Internal/AsyncSequenceOfOne.swift | 2 -- .../Internal/BroadcastAsyncSequence+RPCWriter.swift | 1 - .../Streaming/Internal/BroadcastAsyncSequence.swift | 8 -------- .../Streaming/Internal/GRPCAsyncThrowingStream.swift | 3 --- .../GRPCCore/Streaming/Internal/RPCWriter+Map.swift | 2 -- .../Internal/RPCWriter+MessageToRPCResponsePart.swift | 2 -- .../Streaming/Internal/RPCWriter+Serialize.swift | 2 -- .../Internal/UncheckedAsyncIteratorSequence.swift | 1 - Sources/GRPCCore/Streaming/RPCAsyncSequence.swift | 1 - Sources/GRPCCore/Streaming/RPCWriter+Closable.swift | 1 - Sources/GRPCCore/Streaming/RPCWriter.swift | 1 - Sources/GRPCCore/Streaming/RPCWriterProtocol.swift | 3 --- Sources/GRPCCore/Timeout.swift | 2 -- Sources/GRPCCore/Transport/ClientTransport.swift | 1 - Sources/GRPCCore/Transport/RPCStream.swift | 1 - Sources/GRPCCore/Transport/RetryThrottle.swift | 1 - Sources/GRPCCore/Transport/ServerTransport.swift | 1 - .../InProcessTransport+Client.swift | 2 -- .../InProcessTransport+Server.swift | 2 -- .../GRPCInProcessTransport/InProcessTransport.swift | 1 - .../GRPCCoreTests/Call/Client/ClientRequestTests.swift | 1 - .../Call/Client/ClientResponseTests.swift | 1 - .../ClientRPCExecutorTestHarness+ServerBehavior.swift | 2 -- .../ClientRPCExecutorTestHarness+Transport.swift | 1 - .../ClientRPCExecutorTestHarness.swift | 1 - .../Internal/ClientRPCExecutorTests+Hedging.swift | 2 -- .../Internal/ClientRPCExecutorTests+Retries.swift | 2 -- .../Call/Client/Internal/ClientRPCExecutorTests.swift | 1 - .../Call/Client/RetryDelaySequenceTests.swift | 1 - .../ServerRPCExecutorTestHarness.swift | 2 -- .../Call/Server/Internal/ServerRPCExecutorTests.swift | 1 - Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift | 1 - .../GRPCCoreTests/Call/Server/ServerRequestTests.swift | 1 - .../Call/Server/ServerResponseTests.swift | 1 - .../Configuration/ServiceConfigCodingTests.swift | 1 - Tests/GRPCCoreTests/GRPCClientTests.swift | 1 - Tests/GRPCCoreTests/GRPCServerTests.swift | 1 - Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift | 1 - Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift | 1 - .../GRPCCoreTests/Internal/Result+CatchingTests.swift | 1 - Tests/GRPCCoreTests/RuntimeErrorTests.swift | 1 - .../Streaming/Internal/AsyncSequenceOfOne.swift | 1 - .../Internal/BroadcastAsyncSequenceTests.swift | 1 - .../Test Utilities/AsyncSequence+Utilities.swift | 1 - Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift | 1 - .../Call/Client/ClientInterceptors.swift | 4 ---- .../Call/Server/ServerInterceptors.swift | 4 ---- .../Test Utilities/RPCAsyncSequence+Utilities.swift | 1 - .../Test Utilities/RPCWriter+Utilities.swift | 3 --- .../Test Utilities/Services/BinaryEcho.swift | 1 - .../Test Utilities/Transport/AnyTransport.swift | 2 -- .../Transport/StreamCountingTransport.swift | 2 -- .../Test Utilities/Transport/ThrowingTransport.swift | 3 --- .../Test Utilities/XCTest+Utilities.swift | 6 ------ Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift | 1 - .../InProcessClientTransportTests.swift | 1 - .../InProcessServerTransportTests.swift | 1 - .../Test Utilities/XCTest+Utilities.swift | 1 - 87 files changed, 7 insertions(+), 162 deletions(-) diff --git a/Examples/echo/Sources/Subcommands/Serve.swift b/Examples/echo/Sources/Subcommands/Serve.swift index 396ecef55..4e12baa5d 100644 --- a/Examples/echo/Sources/Subcommands/Serve.swift +++ b/Examples/echo/Sources/Subcommands/Serve.swift @@ -42,7 +42,6 @@ struct Serve: AsyncParsableCommand { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct EchoService: Echo_Echo_ServiceProtocol { func get( request: ServerRequest, diff --git a/Examples/route-guide/Sources/RouteGuide.swift b/Examples/route-guide/Sources/RouteGuide.swift index d53882726..425fe2103 100644 --- a/Examples/route-guide/Sources/RouteGuide.swift +++ b/Examples/route-guide/Sources/RouteGuide.swift @@ -17,7 +17,6 @@ import ArgumentParser @main -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RouteGuide: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "route-guide", diff --git a/Package.swift b/Package.swift index e14774b1a..c1aa65bfc 100644 --- a/Package.swift +++ b/Package.swift @@ -105,6 +105,13 @@ let targets: [Target] = [ let package = Package( name: "grpc-swift", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], products: products, dependencies: dependencies, targets: targets diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index ce6388050..2e6f8d939 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -21,7 +21,6 @@ /// /// You can create the default set of options, which defers all possible /// configuration to the transport, by using ``CallOptions/defaults``. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct CallOptions: Sendable { /// The default timeout for the RPC. /// @@ -108,7 +107,6 @@ public struct CallOptions: Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { /// Default call options. /// @@ -125,7 +123,6 @@ extension CallOptions { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { package mutating func formUnion(with methodConfig: MethodConfig?) { guard let methodConfig = methodConfig else { return } diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 2a81a0ddc..939461e54 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -88,7 +88,6 @@ /// ``` /// /// For server-side interceptors see ``ServerInterceptor``. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ClientInterceptor: Sendable { /// Intercept a request object. /// diff --git a/Sources/GRPCCore/Call/Client/ClientRequest.swift b/Sources/GRPCCore/Call/Client/ClientRequest.swift index fbbf686e6..ebb25316e 100644 --- a/Sources/GRPCCore/Call/Client/ClientRequest.swift +++ b/Sources/GRPCCore/Call/Client/ClientRequest.swift @@ -61,7 +61,6 @@ public struct ClientRequest: Sendable { /// /// See ``ClientRequest`` for single-message requests and ``StreamingServerRequest`` for the /// servers representation of a streaming-message request. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct StreamingClientRequest: Sendable { /// Caller-specified metadata sent to the server at the start of the RPC. /// diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index dd2e71a38..e88ab1d77 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -198,7 +198,6 @@ public struct ClientResponse: Sendable { /// print("RPC failed with code '\(error.code)'") /// } /// ``` -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct StreamingClientResponse: Sendable { public struct Contents: Sendable { /// Metadata received from the server at the beginning of the response. @@ -325,7 +324,6 @@ extension ClientResponse { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension StreamingClientResponse { /// Creates a new accepted response. /// diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift index ca85fabdb..cb44fefff 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+HedgingExecutor.swift @@ -16,7 +16,6 @@ public import Synchronization // would be internal but for usableFromInline -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { @usableFromInline struct HedgingExecutor< @@ -62,7 +61,6 @@ extension ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.HedgingExecutor { @inlinable func execute( @@ -545,7 +543,6 @@ extension ClientRPCExecutor.HedgingExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline enum _HedgingTaskResult: Sendable { case rpcHandled(Result) @@ -553,7 +550,6 @@ enum _HedgingTaskResult: Sendable { case timedOut(Result) } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline enum _HedgingAttemptTaskResult: Sendable { case attemptPicked(Bool) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift index 493949fbc..32dde4a66 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+OneShotExecutor.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { /// An executor for requests which doesn't apply retries or hedging. The request has just one /// attempt at execution. @@ -54,7 +53,6 @@ extension ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.OneShotExecutor { @inlinable func execute( @@ -90,7 +88,6 @@ extension ClientRPCExecutor.OneShotExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.OneShotExecutor { @inlinable func _execute( @@ -129,7 +126,6 @@ extension ClientRPCExecutor.OneShotExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @inlinable func withDeadline( _ deadline: ContinuousClock.Instant, diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift index 7188b4a6f..6e7da3433 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor+RetryExecutor.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { @usableFromInline struct RetryExecutor< @@ -60,7 +59,6 @@ extension ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor.RetryExecutor { @inlinable func execute( @@ -302,7 +300,6 @@ extension ClientRPCExecutor.RetryExecutor { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline enum _RetryExecutorTask: Sendable { case timedOut(Result) diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift index f6e91d94f..ade536d65 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRPCExecutor.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline enum ClientRPCExecutor { /// Execute the request and handle its response. @@ -100,7 +99,6 @@ enum ClientRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutor { /// Executes a request on a given stream processor. /// diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift index 9f9b3223b..477378599 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientRequest+Convenience.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension StreamingClientRequest { internal init(single request: ClientRequest) { self.init(metadata: request.metadata) { diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift index c6e315ba8..30b381e72 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientResponse { /// Converts a streaming response into a single response. /// @@ -82,7 +81,6 @@ extension ClientResponse { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension StreamingClientResponse { /// Creates a streaming response from the given status and metadata. /// @@ -103,7 +101,6 @@ extension StreamingClientResponse { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension StreamingClientResponse { /// Returns a new response which maps the messages of this response. /// diff --git a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift index 728f4e337..f4cf1c482 100644 --- a/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift +++ b/Sources/GRPCCore/Call/Client/Internal/ClientStreamExecutor.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline internal enum ClientStreamExecutor { /// Execute a request on the stream executor. @@ -165,7 +164,6 @@ internal enum ClientStreamExecutor { } @usableFromInline - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RawBodyPartToMessageSequence< Base: AsyncSequence, Message: Sendable, diff --git a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift index d1ef417f3..f07606f5c 100644 --- a/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift +++ b/Sources/GRPCCore/Call/Client/Internal/RetryDelaySequence.swift @@ -19,7 +19,6 @@ public import Darwin // should be @usableFromInline public import Glibc // should be @usableFromInline #endif -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline struct RetryDelaySequence: Sequence { @usableFromInline diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 79fba9b5c..f85cbe318 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline struct ServerRPCExecutor { /// Executes an RPC using the provided handler. @@ -296,7 +295,6 @@ struct ServerRPCExecutor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRPCExecutor { @inlinable static func _intercept( diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index 35d14cbd5..a574a1186 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -32,7 +32,6 @@ /// 1. Remove individual methods by calling ``removeHandler(forMethod:)``, or /// 2. Implement ``RegistrableRPCService/registerMethods(with:)`` to register only the methods you /// want to be served. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct RPCRouter: Sendable { @usableFromInline struct RPCHandler: Sendable { @@ -145,7 +144,6 @@ public struct RPCRouter: Sendable { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RPCRouter { internal func handle( stream: RPCStream< diff --git a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift index d9236c75b..f2a9bee03 100644 --- a/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift +++ b/Sources/GRPCCore/Call/Server/RegistrableRPCService.swift @@ -22,7 +22,6 @@ /// generated conformance by implementing ``registerMethods(with:)`` manually by calling /// ``RPCRouter/registerHandler(forMethod:deserializer:serializer:handler:)`` for each method /// you want to register with the router. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol RegistrableRPCService: Sendable { /// Registers methods to server with the provided ``RPCRouter``. /// diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index aa1fff090..3c71cd3e5 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -59,7 +59,6 @@ /// ``` /// /// For client-side interceptors see ``ClientInterceptor``. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ServerInterceptor: Sendable { /// Intercept a request object. /// diff --git a/Sources/GRPCCore/Call/Server/ServerRequest.swift b/Sources/GRPCCore/Call/Server/ServerRequest.swift index b0318ee15..40ec6b568 100644 --- a/Sources/GRPCCore/Call/Server/ServerRequest.swift +++ b/Sources/GRPCCore/Call/Server/ServerRequest.swift @@ -36,7 +36,6 @@ public struct ServerRequest: Sendable { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) /// A request received at the server containing a stream of messages. public struct StreamingServerRequest: Sendable { /// Metadata received from the client at the start of the RPC. @@ -63,14 +62,12 @@ public struct StreamingServerRequest: Sendable { // MARK: - Conversion -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension StreamingServerRequest { public init(single request: ServerRequest) { self.init(metadata: request.metadata, messages: .one(request.message)) } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRequest { public init(stream request: StreamingServerRequest) async throws { var iterator = request.messages.makeAsyncIterator() diff --git a/Sources/GRPCCore/Call/Server/ServerResponse.swift b/Sources/GRPCCore/Call/Server/ServerResponse.swift index 778d1b395..15509f8ff 100644 --- a/Sources/GRPCCore/Call/Server/ServerResponse.swift +++ b/Sources/GRPCCore/Call/Server/ServerResponse.swift @@ -168,7 +168,6 @@ public struct ServerResponse: Sendable { /// return ["goodbye": "trailing metadata"] /// } /// ``` -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct StreamingServerResponse: Sendable { /// The contents of a response to a request which has been accepted for processing. public struct Contents: Sendable { @@ -279,7 +278,6 @@ extension ServerResponse { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension StreamingServerResponse { /// Creates a new accepted response. /// @@ -318,7 +316,6 @@ extension StreamingServerResponse { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension StreamingServerResponse { public init(single response: ServerResponse) { switch response.accepted { diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index 295d049a3..9e1543636 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -17,7 +17,6 @@ /// Configuration values for executing an RPC. /// /// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct MethodConfig: Hashable, Sendable { public struct Name: Sendable, Hashable { /// The name of the service, including the namespace. @@ -144,7 +143,6 @@ public struct MethodConfig: Hashable, Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct RPCExecutionPolicy: Hashable, Sendable { @usableFromInline enum Wrapped: Hashable, Sendable { @@ -214,7 +212,6 @@ public struct RPCExecutionPolicy: Hashable, Sendable { /// /// For more information see [gRFC A6 Client /// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct RetryPolicy: Hashable, Sendable { /// The maximum number of RPC attempts, including the original attempt. /// @@ -332,7 +329,6 @@ public struct RetryPolicy: Hashable, Sendable { /// /// For more information see [gRFC A6 Client /// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct HedgingPolicy: Hashable, Sendable { /// The maximum number of RPC attempts, including the original attempt. /// @@ -397,7 +393,6 @@ private func validateMaxAttempts(_ value: Int) throws -> Int { return min(value, 5) } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Duration { fileprivate init(googleProtobufDuration duration: String) throws { guard duration.utf8.last == UInt8(ascii: "s"), @@ -413,7 +408,6 @@ extension Duration { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension MethodConfig: Codable { private enum CodingKeys: String, CodingKey { case name @@ -472,7 +466,6 @@ extension MethodConfig: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension MethodConfig.Name: Codable { private enum CodingKeys: String, CodingKey { case service @@ -498,7 +491,6 @@ extension MethodConfig.Name: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension RetryPolicy: Codable { private enum CodingKeys: String, CodingKey { case maxAttempts @@ -546,7 +538,6 @@ extension RetryPolicy: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension HedgingPolicy: Codable { private enum CodingKeys: String, CodingKey { case maxAttempts @@ -578,7 +569,6 @@ extension HedgingPolicy: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) struct GoogleProtobufDuration: Codable { var duration: Duration diff --git a/Sources/GRPCCore/Configuration/ServiceConfig.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift index f0e41c4bc..a13bf08dc 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -17,7 +17,6 @@ /// Service configuration values. /// /// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) public struct ServiceConfig: Hashable, Sendable { /// Per-method configuration. public var methodConfig: [MethodConfig] @@ -63,7 +62,6 @@ public struct ServiceConfig: Hashable, Sendable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceConfig: Codable { private enum CodingKeys: String, CodingKey { case methodConfig @@ -100,7 +98,6 @@ extension ServiceConfig: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceConfig { /// Configuration used by clients for load-balancing. public struct LoadBalancingConfig: Hashable, Sendable { @@ -166,7 +163,6 @@ extension ServiceConfig { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceConfig.LoadBalancingConfig { /// Configuration for the pick-first load balancing policy. public struct PickFirst: Hashable, Sendable, Codable { @@ -194,7 +190,6 @@ extension ServiceConfig.LoadBalancingConfig { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceConfig.LoadBalancingConfig: Codable { private enum CodingKeys: String, CodingKey { case roundRobin = "round_robin" @@ -225,7 +220,6 @@ extension ServiceConfig.LoadBalancingConfig: Codable { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension ServiceConfig { public struct RetryThrottling: Hashable, Sendable, Codable { /// The initial, and maximum number of tokens. diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 31a1aed67..98c1c4f3d 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -108,7 +108,6 @@ private import Synchronization /// more abruptly you can cancel the task running your client. If your application requires /// additional resources that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public final class GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. private let transport: any ClientTransport diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index feb218b19..8ba69985d 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -69,7 +69,6 @@ private import Synchronization /// you can cancel the task running your server. If your application requires additional resources /// that need their lifecycles managed you should consider using [Swift Service /// Lifecycle](https://github.com/swift-server/swift-service-lifecycle). -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public final class GRPCServer: Sendable { typealias Stream = RPCStream diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index 9bff423e3..8920140f6 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Metadata { @inlinable var previousRPCAttempts: Int? { @@ -77,7 +76,6 @@ extension Metadata { } extension Metadata { - @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline enum RetryPushback: Hashable, Sendable { case retryAfter(Duration) diff --git a/Sources/GRPCCore/Internal/MethodConfigs.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift index 1992b59a8..163122dfe 100644 --- a/Sources/GRPCCore/Internal/MethodConfigs.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -22,7 +22,6 @@ /// service, or set a default configuration for all methods by calling ``setDefaultConfig(_:)``. /// /// Use the subscript to get and set configurations for specific methods. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) package struct MethodConfigs: Sendable, Hashable { private var elements: [MethodConfig.Name: MethodConfig] diff --git a/Sources/GRPCCore/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift index 68bbbebd7..bf2393752 100644 --- a/Sources/GRPCCore/Internal/Result+Catching.swift +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension Result where Failure == any Error { /// Like `Result(catching:)`, but `async`. /// diff --git a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift index 454a85b85..d0f38d6d3 100644 --- a/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift +++ b/Sources/GRPCCore/Internal/TaskGroup+CancellableTask.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension TaskGroup { /// Adds a child task to the group which is individually cancellable. /// @@ -65,7 +64,6 @@ extension TaskGroup { } @usableFromInline -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) struct CancellableTaskHandle: Sendable { @usableFromInline private(set) var continuation: AsyncStream.Continuation diff --git a/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift index c0a72e176..40ea25e71 100644 --- a/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Sources/GRPCCore/Streaming/Internal/AsyncSequenceOfOne.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RPCAsyncSequence { /// Returns an ``RPCAsyncSequence`` containing just the given element. @inlinable @@ -33,7 +32,6 @@ extension RPCAsyncSequence { /// An `AsyncSequence` of a single value. @usableFromInline -@available(macOS 10.15, iOS 13.0, tvOS 13, watchOS 6, *) struct AsyncSequenceOfOne: AsyncSequence, Sendable { @usableFromInline let result: Result diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift index ff54d9814..f7e4f72ff 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence+RPCWriter.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BroadcastAsyncSequence.Source: ClosableRPCWriterProtocol { @inlinable func write(contentsOf elements: some Sequence) async throws { diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift index 5f812eb11..ac7e6c084 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -30,7 +30,6 @@ public import DequeModule // should be @usableFromInline /// /// The expectation is that the number of subscribers will be low; for retries there will be at most /// one subscriber at a time, for hedging there may be at most five subscribers at a time. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline struct BroadcastAsyncSequence: Sendable, AsyncSequence { @usableFromInline @@ -85,7 +84,6 @@ struct BroadcastAsyncSequence: Sendable, AsyncSequence { // MARK: - AsyncIterator -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BroadcastAsyncSequence { @usableFromInline struct AsyncIterator: AsyncIteratorProtocol { @@ -112,7 +110,6 @@ extension BroadcastAsyncSequence { // MARK: - Continuation -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension BroadcastAsyncSequence { @usableFromInline struct Source: Sendable { @@ -156,7 +153,6 @@ enum BroadcastAsyncSequenceError: Error { // MARK: - Storage -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline final class _BroadcastSequenceStorage: Sendable { @usableFromInline @@ -331,7 +327,6 @@ final class _BroadcastSequenceStorage: Sendable { // MARK: - State machine -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline @@ -1369,7 +1364,6 @@ struct _BroadcastSequenceStateMachine: Sendable { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension _BroadcastSequenceStateMachine { /// A collection of elements tagged with an identifier. /// @@ -1524,7 +1518,6 @@ extension _BroadcastSequenceStateMachine { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension _BroadcastSequenceStateMachine { /// A collection of subscriptions. @usableFromInline @@ -1788,7 +1781,6 @@ extension _BroadcastSequenceStateMachine { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension _BroadcastSequenceStateMachine { // TODO: tiny array @usableFromInline diff --git a/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift b/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift index 5b4bcb58b..3eadd1d85 100644 --- a/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift +++ b/Sources/GRPCCore/Streaming/Internal/GRPCAsyncThrowingStream.swift @@ -19,7 +19,6 @@ // 'RPCWriterProtocol'. (Adding a constrained conformance to 'RPCWriterProtocol' on // 'AsyncThrowingStream.Continuation' isn't possible because 'Sendable' is a marker protocol.) -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct GRPCAsyncThrowingStream: AsyncSequence, Sendable { package typealias Element = Element package typealias Failure = any Error @@ -78,7 +77,6 @@ package struct GRPCAsyncThrowingStream: AsyncSequence, Sendab } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCAsyncThrowingStream.Continuation: RPCWriterProtocol { package func write(_ element: Element) async throws { self.yield(element) @@ -91,7 +89,6 @@ extension GRPCAsyncThrowingStream.Continuation: RPCWriterProtocol { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension GRPCAsyncThrowingStream.Continuation: ClosableRPCWriterProtocol { package func finish() async { self.finish(throwing: nil) diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift index 7755b46e1..41332f1f2 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Map.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline struct MapRPCWriter< Value: Sendable, @@ -47,7 +46,6 @@ struct MapRPCWriter< } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriter { @inlinable static func map( diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift index c3dc290e9..15dd4b6cd 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+MessageToRPCResponsePart.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline struct MessageToRPCResponsePartWriter< Serializer: MessageSerializer @@ -48,7 +47,6 @@ struct MessageToRPCResponsePartWriter< } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriter { @inlinable static func serializingToRPCResponsePart( diff --git a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift index f2a9acd22..d3d93d74d 100644 --- a/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift +++ b/Sources/GRPCCore/Streaming/Internal/RPCWriter+Serialize.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @usableFromInline struct SerializingRPCWriter< Base: RPCWriterProtocol<[UInt8]>, @@ -49,7 +48,6 @@ struct SerializingRPCWriter< } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriter { @inlinable static func serializing( diff --git a/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift b/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift index e3bdcafee..5b963e3bd 100644 --- a/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/UncheckedAsyncIteratorSequence.swift @@ -16,7 +16,6 @@ public import Synchronization // should be @usableFromInline -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) @usableFromInline /// An `AsyncSequence` which wraps an existing async iterator. final class UncheckedAsyncIteratorSequence< diff --git a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift index d3603fe00..edf451179 100644 --- a/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/RPCAsyncSequence.swift @@ -15,7 +15,6 @@ */ /// A type-erasing `AsyncSequence`. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct RPCAsyncSequence< Element: Sendable, Failure: Error diff --git a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift index dda689464..075c05ad8 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter+Closable.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriter { public struct Closable: ClosableRPCWriterProtocol { @usableFromInline diff --git a/Sources/GRPCCore/Streaming/RPCWriter.swift b/Sources/GRPCCore/Streaming/RPCWriter.swift index d0b7b940b..cc680b01d 100644 --- a/Sources/GRPCCore/Streaming/RPCWriter.swift +++ b/Sources/GRPCCore/Streaming/RPCWriter.swift @@ -15,7 +15,6 @@ */ /// A type-erasing ``RPCWriterProtocol``. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct RPCWriter: Sendable, RPCWriterProtocol { private let writer: any RPCWriterProtocol diff --git a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift index 63e3ee26d..076043e30 100644 --- a/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift +++ b/Sources/GRPCCore/Streaming/RPCWriterProtocol.swift @@ -15,7 +15,6 @@ */ /// A sink for values which are produced over time. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol RPCWriterProtocol: Sendable { /// The type of value written. associatedtype Element: Sendable @@ -37,7 +36,6 @@ public protocol RPCWriterProtocol: Sendable { func write(contentsOf elements: some Sequence) async throws } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriterProtocol { /// Writes an `AsyncSequence` of values into the sink. /// @@ -51,7 +49,6 @@ extension RPCWriterProtocol { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public protocol ClosableRPCWriterProtocol: RPCWriterProtocol { /// Indicate to the writer that no more writes are to be accepted. /// diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index 4640a5cca..68dfcd145 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -22,7 +22,6 @@ internal import Dispatch /// one of ``Timeout/Unit`` (hours, minutes, seconds, milliseconds, microseconds or nanoseconds). /// /// Timeouts must be positive and at most 8-digits long. -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) @usableFromInline struct Timeout: CustomStringConvertible, Hashable, Sendable { /// Possible units for a ``Timeout``. @@ -182,7 +181,6 @@ extension Int64 { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension Duration { /// Construct a `Duration` given a number of minutes represented as an `Int64`. /// diff --git a/Sources/GRPCCore/Transport/ClientTransport.swift b/Sources/GRPCCore/Transport/ClientTransport.swift index 800aa5b64..a86a79fea 100644 --- a/Sources/GRPCCore/Transport/ClientTransport.swift +++ b/Sources/GRPCCore/Transport/ClientTransport.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ClientTransport: Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Sources/GRPCCore/Transport/RPCStream.swift b/Sources/GRPCCore/Transport/RPCStream.swift index eca345c2a..445d27f45 100644 --- a/Sources/GRPCCore/Transport/RPCStream.swift +++ b/Sources/GRPCCore/Transport/RPCStream.swift @@ -15,7 +15,6 @@ */ /// A bidirectional communication channel between a client and server for a given method. -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct RPCStream< Inbound: AsyncSequence & Sendable, Outbound: ClosableRPCWriterProtocol & Sendable diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index 70c934dfc..e3f3ead4f 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -30,7 +30,6 @@ private import Synchronization /// the server. /// /// See also [gRFC A6: client retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public final class RetryThrottle: Sendable { // Note: only three figures after the decimal point from the original token ratio are used so // all computation is done a scaled number of tokens (tokens * 1000). This allows us to do all diff --git a/Sources/GRPCCore/Transport/ServerTransport.swift b/Sources/GRPCCore/Transport/ServerTransport.swift index 79c5c0fc6..15148c78e 100644 --- a/Sources/GRPCCore/Transport/ServerTransport.swift +++ b/Sources/GRPCCore/Transport/ServerTransport.swift @@ -15,7 +15,6 @@ */ /// A protocol server transport implementations must conform to. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol ServerTransport: Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift index 08c6b3476..84c7f85a7 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -17,7 +17,6 @@ public import GRPCCore private import Synchronization -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension InProcessTransport { /// An in-process implementation of a ``ClientTransport``. /// @@ -36,7 +35,6 @@ extension InProcessTransport { /// block until ``connect()`` is called or the task is cancelled. /// /// - SeeAlso: ``ClientTransport`` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public final class Client: ClientTransport { private enum State: Sendable { struct UnconnectedState { diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 5079fad5c..66e32d06f 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -16,7 +16,6 @@ public import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension InProcessTransport { /// An in-process implementation of a ``ServerTransport``. /// @@ -28,7 +27,6 @@ extension InProcessTransport { /// To stop listening to new requests, call ``beginGracefulShutdown()``. /// /// - SeeAlso: ``ClientTransport`` - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct Server: ServerTransport, Sendable { public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable diff --git a/Sources/GRPCInProcessTransport/InProcessTransport.swift b/Sources/GRPCInProcessTransport/InProcessTransport.swift index e1bee0ae5..1a563f079 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport.swift @@ -16,7 +16,6 @@ public import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct InProcessTransport: Sendable { public let server: Self.Server public let client: Self.Client diff --git a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift index 5e802a622..c5f0dbb9f 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientRequestTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ClientRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let (messages, continuation) = AsyncStream.makeStream(of: String.self) diff --git a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift index d0fcf3611..01557d02c 100644 --- a/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ClientResponseTests: XCTestCase { func testAcceptedSingleResponseConvenienceMethods() { let response = ClientResponse( diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift index 0c2ab936f..4a6e17ed2 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+ServerBehavior.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTestHarness { struct ServerStreamHandler: Sendable { private let handler: @@ -59,7 +58,6 @@ extension ClientRPCExecutorTestHarness { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTestHarness.ServerStreamHandler { static var echo: Self { return Self { stream in diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift index 4198c9b31..aa5a21332 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness+Transport.swift @@ -17,7 +17,6 @@ import GRPCCore import GRPCInProcessTransport -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension InProcessTransport.Server { func spawnClientTransport( throttle: RetryThrottle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift index 4ead582fd..9a456c7b0 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTestSupport/ClientRPCExecutorTestHarness.swift @@ -25,7 +25,6 @@ import XCTest /// of the server to allow for flexible testing scenarios with minimal boilerplate. The harness /// also tracks how many streams the client has opened, how many streams the server accepted, and /// how many streams the client failed to open. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ClientRPCExecutorTestHarness { private let server: ServerStreamHandler private let clientTransport: StreamCountingClientTransport diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift index ae924eea8..14b31b0c5 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Hedging.swift @@ -16,7 +16,6 @@ import GRPCCore import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTests { func testHedgingWhenAllAttemptsResultInNonFatalCodes() async throws { let harness = ClientRPCExecutorTestHarness( @@ -186,7 +185,6 @@ extension ClientRPCExecutorTests { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { fileprivate static func hedge( maxAttempts: Int = 5, diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift index 2c5ab960d..d4e1e210c 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests+Retries.swift @@ -16,7 +16,6 @@ import GRPCCore import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientRPCExecutorTests { fileprivate func makeHarnessForRetries( rejectUntilAttempt firstSuccessfulAttempt: Int, @@ -289,7 +288,6 @@ extension ClientRPCExecutorTests { } } -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) extension CallOptions { fileprivate static func retry( _ policy: RetryPolicy diff --git a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift index 310486aa3..1d3e78d69 100644 --- a/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/Internal/ClientRPCExecutorTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ClientRPCExecutorTests: XCTestCase { func testUnaryEcho() async throws { let tester = ClientRPCExecutorTestHarness(server: .echo) diff --git a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift index cbe0d7a09..71fa0a495 100644 --- a/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift +++ b/Tests/GRPCCoreTests/Call/Client/RetryDelaySequenceTests.swift @@ -17,7 +17,6 @@ import XCTest @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class RetryDelaySequenceTests: XCTestCase { func testSequence() { let policy = RetryPolicy( diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index f53c46232..6cca2d4d2 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -17,7 +17,6 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ServerRPCExecutorTestHarness { struct ServerHandler: Sendable { let fn: @@ -134,7 +133,6 @@ struct ServerRPCExecutorTestHarness { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerRPCExecutorTestHarness.ServerHandler where Input == Output { static var echo: Self { return Self { request in diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index df894dd8e..f047955da 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ServerRPCExecutorTests: XCTestCase { func testEchoNoMessages() async throws { let harness = ServerRPCExecutorTestHarness() diff --git a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift index 86fe1bd1e..af02f2894 100644 --- a/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/RPCRouterTests.swift @@ -17,7 +17,6 @@ import GRPCCore import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class RPCRouterTests: XCTestCase { func testEmptyRouter() async throws { var router = RPCRouter() diff --git a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift index 658a2855e..c341bc992 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerRequestTests.swift @@ -16,7 +16,6 @@ @_spi(Testing) import GRPCCore import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class ServerRequestTests: XCTestCase { func testSingleToStreamConversion() async throws { let single = ServerRequest(metadata: ["bar": "baz"], message: "foo") diff --git a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift index 02196a9dd..99e76fc56 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift @@ -16,7 +16,6 @@ @_spi(Testing) import GRPCCore import XCTest -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ServerResponseTests: XCTestCase { func testSingleConvenienceInit() { var response = ServerResponse( diff --git a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift index 8dbcd7340..cb5e38b7a 100644 --- a/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/ServiceConfigCodingTests.swift @@ -18,7 +18,6 @@ import Foundation import GRPCCore import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class ServiceConfigCodingTests: XCTestCase { private let encoder = JSONEncoder() private let decoder = JSONDecoder() diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index f55f921dd..42a6e3b3b 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -18,7 +18,6 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index a513f5d15..b7866c80c 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -18,7 +18,6 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], diff --git a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift index 25ded0048..a6bb11a84 100644 --- a/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift +++ b/Tests/GRPCCoreTests/Internal/Metadata+GRPCTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class MetadataGRPCTests: XCTestCase { func testPreviousRPCAttemptsValidValues() { let testData = [("0", 0), ("1", 1), ("-1", -1)] diff --git a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift index 58ddcde4f..095a5eded 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -16,7 +16,6 @@ import GRPCCore import XCTest -@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) final class MethodConfigsTests: XCTestCase { func testGetConfigurationForKnownMethod() async throws { let policy = HedgingPolicy( diff --git a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift index aee39daf4..cbe5ac742 100644 --- a/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift +++ b/Tests/GRPCCoreTests/Internal/Result+CatchingTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class ResultCatchingTests: XCTestCase { func testResultCatching() async { let result = await Result { diff --git a/Tests/GRPCCoreTests/RuntimeErrorTests.swift b/Tests/GRPCCoreTests/RuntimeErrorTests.swift index 2d30f96f0..fb8411687 100644 --- a/Tests/GRPCCoreTests/RuntimeErrorTests.swift +++ b/Tests/GRPCCoreTests/RuntimeErrorTests.swift @@ -16,7 +16,6 @@ import GRPCCore import XCTest -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class RuntimeErrorTests: XCTestCase { func testCopyOnWrite() { // RuntimeError has a heap based storage, so check CoW semantics are correctly implemented. diff --git a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift index 648e935e9..79cfef19d 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/AsyncSequenceOfOne.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal final class AsyncSequenceOfOneTests: XCTestCase { func testSuccessPath() async throws { let sequence = RPCAsyncSequence.one("foo") diff --git a/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift b/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift index 6195977c6..55479d38c 100644 --- a/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift +++ b/Tests/GRPCCoreTests/Streaming/Internal/BroadcastAsyncSequenceTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) final class BroadcastAsyncSequenceTests: XCTestCase { func testSingleSubscriberToEmptyStream() async throws { let (stream, source) = BroadcastAsyncSequence.makeStream(of: Int.self, bufferSize: 16) diff --git a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift index b82b7185e..0e05a407d 100644 --- a/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/AsyncSequence+Utilities.swift @@ -14,7 +14,6 @@ * limitations under the License. */ -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension AsyncSequence { func collect() async throws -> [Element] { return try await self.reduce(into: []) { $0.append($1) } diff --git a/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift b/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift index b9e9fb5b8..cf9c4f679 100644 --- a/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift +++ b/Tests/GRPCCoreTests/Test Utilities/AtomicCounter.swift @@ -16,7 +16,6 @@ import Synchronization -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class AtomicCounter: Sendable { private let counter: Atomic diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift index dd73cb96d..e13228b3e 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Client/ClientInterceptors.swift @@ -16,7 +16,6 @@ import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientInterceptor where Self == RejectAllClientInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllClientInterceptor(error: error, throw: false) @@ -28,7 +27,6 @@ extension ClientInterceptor where Self == RejectAllClientInterceptor { } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ClientInterceptor where Self == RequestCountingClientInterceptor { static func requestCounter(_ counter: AtomicCounter) -> Self { return RequestCountingClientInterceptor(counter: counter) @@ -36,7 +34,6 @@ extension ClientInterceptor where Self == RequestCountingClientInterceptor { } /// Rejects all RPCs with the provided error. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RejectAllClientInterceptor: ClientInterceptor { /// The error to reject all RPCs with. let error: RPCError @@ -65,7 +62,6 @@ struct RejectAllClientInterceptor: ClientInterceptor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RequestCountingClientInterceptor: ClientInterceptor { /// The number of requests made. let counter: AtomicCounter diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 1467cddad..4356fdc1f 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -16,7 +16,6 @@ import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerInterceptor where Self == RejectAllServerInterceptor { static func rejectAll(with error: RPCError) -> Self { return RejectAllServerInterceptor(error: error, throw: false) @@ -27,7 +26,6 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServerInterceptor where Self == RequestCountingServerInterceptor { static func requestCounter(_ counter: AtomicCounter) -> Self { return RequestCountingServerInterceptor(counter: counter) @@ -35,7 +33,6 @@ extension ServerInterceptor where Self == RequestCountingServerInterceptor { } /// Rejects all RPCs with the provided error. -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RejectAllServerInterceptor: ServerInterceptor { /// The error to reject all RPCs with. let error: RPCError @@ -64,7 +61,6 @@ struct RejectAllServerInterceptor: ServerInterceptor { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct RequestCountingServerInterceptor: ServerInterceptor { /// The number of requests made. let counter: AtomicCounter diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift index b996e3d27..950e2c61a 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCAsyncSequence+Utilities.swift @@ -15,7 +15,6 @@ */ import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension RPCAsyncSequence where Failure == any Error { static func elements(_ elements: Element...) -> Self { return .elements(elements) diff --git a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift index 923ab267d..eafd569ab 100644 --- a/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/RPCWriter+Utilities.swift @@ -16,7 +16,6 @@ import GRPCCore import XCTest -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RPCWriter { /// Returns a writer which calls `XCTFail(_:)` on every write. static func failTestOnWrite(elementType: Element.Type = Element.self) -> Self { @@ -29,7 +28,6 @@ extension RPCWriter { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private struct FailOnWrite: RPCWriterProtocol { func write(_ element: Element) async throws { XCTFail("Unexpected write") @@ -40,7 +38,6 @@ private struct FailOnWrite: RPCWriterProtocol { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) private struct AsyncStreamGatheringWriter: RPCWriterProtocol { let continuation: AsyncStream.Continuation diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index 817539a9d..3859eec24 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -16,7 +16,6 @@ import GRPCCore import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct BinaryEcho: RegistrableRPCService { func get( _ request: ServerRequest<[UInt8]> diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift index eb8208b72..7ca178fef 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/AnyTransport.swift @@ -15,7 +15,6 @@ */ @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct AnyClientTransport: ClientTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable @@ -81,7 +80,6 @@ struct AnyClientTransport: ClientTransport, Sendable { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct AnyServerTransport: ServerTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift index 835c81b79..970109286 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/StreamCountingTransport.swift @@ -16,7 +16,6 @@ @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct StreamCountingClientTransport: ClientTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable @@ -78,7 +77,6 @@ struct StreamCountingClientTransport: ClientTransport, Sendable { } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct StreamCountingServerTransport: ServerTransport, Sendable { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable diff --git a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift index 804be7d52..e73bdbdf1 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Transport/ThrowingTransport.swift @@ -15,7 +15,6 @@ */ @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) struct ThrowOnStreamCreationTransport: ClientTransport { typealias Inbound = RPCAsyncSequence typealias Outbound = RPCWriter.Closable @@ -51,7 +50,6 @@ struct ThrowOnStreamCreationTransport: ClientTransport { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ThrowOnRunServerTransport: ServerTransport { func listen( streamHandler: ( @@ -70,7 +68,6 @@ struct ThrowOnRunServerTransport: ServerTransport { } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) struct ThrowOnSignalServerTransport: ServerTransport { let signal: AsyncStream diff --git a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift index 400ab3037..cc50b14b9 100644 --- a/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCCoreTests/Test Utilities/XCTest+Utilities.swift @@ -25,7 +25,6 @@ func XCTAssertDescription( XCTAssertEqual(String(describing: subject), expected, file: file, line: line) } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsErrorAsync( _ expression: () async throws -> T, errorHandler: (any Error) -> Void @@ -51,7 +50,6 @@ func XCTAssertThrowsError( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsErrorAsync( ofType: E.Type = E.self, _ expression: () async throws -> T, @@ -80,7 +78,6 @@ func XCTAssertThrowsRPCError( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsRPCErrorAsync( _ expression: () async throws -> T, errorHandler: (RPCError) -> Void @@ -95,7 +92,6 @@ func XCTAssertThrowsRPCErrorAsync( } } -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) func XCTAssertRejected( _ response: StreamingClientResponse, errorHandler: (RPCError) -> Void @@ -132,7 +128,6 @@ func XCTAssertMetadata( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertMetadata( _ part: RPCRequestPart?, metadataHandler: (Metadata) async throws -> Void = { _ in } @@ -157,7 +152,6 @@ func XCTAssertMessage( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertMessage( _ part: RPCRequestPart?, messageHandler: ([UInt8]) async throws -> Void = { _ in } diff --git a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift index ee2ad3742..9e89e032d 100644 --- a/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift +++ b/Tests/GRPCCoreTests/Transport/RetryThrottleTests.swift @@ -18,7 +18,6 @@ import XCTest @testable import GRPCCore -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class RetryThrottleTests: XCTestCase { func testThrottleOnInit() { let throttle = RetryThrottle(maxTokens: 10, tokenRatio: 0.1) diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 5730ccf46..16b915f41 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -18,7 +18,6 @@ import GRPCCore import GRPCInProcessTransport import XCTest -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class InProcessClientTransportTests: XCTestCase { struct FailTest: Error {} diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 5d451fb9d..45baa66e4 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -19,7 +19,6 @@ import XCTest @testable import GRPCCore @testable import GRPCInProcessTransport -@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) final class InProcessServerTransportTests: XCTestCase { func testStartListening() async throws { let transport = InProcessTransport.Server() diff --git a/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift b/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift index 67d381073..b49af0ec9 100644 --- a/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift +++ b/Tests/GRPCInProcessTransportTests/Test Utilities/XCTest+Utilities.swift @@ -28,7 +28,6 @@ func XCTAssertThrowsError( } } -@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) func XCTAssertThrowsErrorAsync( ofType: E.Type = E.self, _ expression: () async throws -> T, From 74d08489ba6496870c964334afb8db6aeb35a2d7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Sat, 5 Oct 2024 16:07:50 +0100 Subject: [PATCH 481/580] Add a design doc (#2079) Motivation: For library maintainers and some users, having an understanding of how the library is structured is incredibly helpful. Modifications: - Add a high-level design doc linking to the relevant symbols - Fix up a couple of other DocC warnings Result: More docs --------- Co-authored-by: Gus Cairo --- .../GRPCCore/Call/Client/ClientRequest.swift | 2 +- .../GRPCCore/Call/Client/ClientResponse.swift | 4 +- .../GRPCCore/Call/Server/ServerResponse.swift | 4 +- .../Documentation.docc/Development/Design.md | 446 ++++++++++++++++++ .../Documentation.docc/Documentation.md | 1 + 5 files changed, 452 insertions(+), 5 deletions(-) create mode 100644 Sources/GRPCCore/Documentation.docc/Development/Design.md diff --git a/Sources/GRPCCore/Call/Client/ClientRequest.swift b/Sources/GRPCCore/Call/Client/ClientRequest.swift index ebb25316e..c36df63db 100644 --- a/Sources/GRPCCore/Call/Client/ClientRequest.swift +++ b/Sources/GRPCCore/Call/Client/ClientRequest.swift @@ -21,7 +21,7 @@ /// See ``StreamingClientRequest`` for streaming requests and ``ServerRequest`` for the /// servers representation of a single-message request. /// -/// ## Creating ``Single`` requests +/// ## Creating requests /// /// ```swift /// let request = ClientRequest(message: "Hello, gRPC!") diff --git a/Sources/GRPCCore/Call/Client/ClientResponse.swift b/Sources/GRPCCore/Call/Client/ClientResponse.swift index e88ab1d77..b99f6f3ae 100644 --- a/Sources/GRPCCore/Call/Client/ClientResponse.swift +++ b/Sources/GRPCCore/Call/Client/ClientResponse.swift @@ -29,7 +29,7 @@ /// an ``RPCError`` describing why the RPC failed, including an error code, error message and any /// metadata sent by the server. /// -/// ### Using ``Single`` responses +/// ### Using responses /// /// Each response has a ``accepted`` property which contains all RPC information. You can create /// one by calling ``init(accepted:)`` or one of the two convenience initializers: @@ -153,7 +153,7 @@ public struct ClientResponse: Sendable { /// to execute the request. The failure case contains an ``RPCError`` describing why the RPC /// failed, including an error code, error message and any metadata sent by the server. /// -/// ### Using ``Stream`` responses +/// ### Using streaming responses /// /// Each response has a ``accepted`` property which contains RPC information. You can create /// one by calling ``init(accepted:)`` or one of the two convenience initializers: diff --git a/Sources/GRPCCore/Call/Server/ServerResponse.swift b/Sources/GRPCCore/Call/Server/ServerResponse.swift index 15509f8ff..524db2868 100644 --- a/Sources/GRPCCore/Call/Server/ServerResponse.swift +++ b/Sources/GRPCCore/Call/Server/ServerResponse.swift @@ -28,7 +28,7 @@ /// of the RPC failed. The failure case contains an ``RPCError`` describing why the RPC failed, /// including an error code, error message and any metadata sent by the server. /// -/// ### Using ``Single`` responses +/// ### Using responses /// /// Each response has an ``accepted`` property which contains all RPC information. You can create /// one by calling ``init(accepted:)`` or one of the two convenience initializers: @@ -144,7 +144,7 @@ public struct ServerResponse: Sendable { /// contains an ``RPCError`` describing why the RPC failed, including an error code, error /// message and any metadata to send to the client. /// -/// ### Using ``Stream`` responses +/// ### Using streaming responses /// /// Each response has an ``accepted`` property which contains all RPC information. You can create /// one by calling ``init(accepted:)`` or one of the two convenience initializers: diff --git a/Sources/GRPCCore/Documentation.docc/Development/Design.md b/Sources/GRPCCore/Documentation.docc/Development/Design.md new file mode 100644 index 000000000..341580539 --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Development/Design.md @@ -0,0 +1,446 @@ +# Design + +This article provides a high-level overview of the design of gRPC Swift. + +The library is split into three broad layers: +1. Transport, +2. Call, and +3. Stub. + +The _transport_ layer provides (typically) long-lived bidirectional +communication between two peers and provides streams of request and response +parts. On top of the transport is the _call_ layer which is responsible for +mapping a call onto a stream and dealing with serialization. The highest level +of abstraction is the _stub_ layer which provides client and server interfaces +generated from an interface definition language (IDL). + +## Transport + +The transport layer provides a bidirectional communication channel with a remote +peer which is typically long-lived. + +Transports have two main interfaces: +1. Streams, used by the call layer. +2. The transport specific communication with its corresponding remote peer. + +The most common transport in gRPC is HTTP/2. However others such as gRPC-Web, +HTTP/3 and in-process also exist. (gRPC Swift has transports for HTTP/2 built on +top of Swift NIO and also provides an in-process transport.) + +You shouldn't think of a transport as a single connection, they're more +abstract. For example, a transport may maintain a set of connections to a +collection of remote endpoints which change over time. By extension, client +transports are also responsible for balancing load across multiple connections +where applicable. + +Each peer (client and server) has their own transport protocol, in gRPC Swift +these are: +1. ``ServerTransport``, and +2. ``ClientTransport``. + +The vast majority of users won't need to implement either of these protocols. +However, many users will need to create instances of types conforming to these +protocols to create a server or client, respectively. + +### Server transport + +The ``ServerTransport`` is responsible for the server half of a transport. It +listens for new gRPC streams and then processes them. This is achieved via the +``ServerTransport/listen(streamHandler:)`` requirement. + +A handler is passed into the `listen` method which is provided by the gRPC +server. It's responsible for routing and handling the stream. The stream is +executed in the context of the server transport – that is, the `listen` method +is an ancestor task of all RPCs handled by the server. + +Note that the server transport doesn't include the idea of a "connection". While +an HTTP/2 server transport will in all likelihood have multiple connections open +at any given time, that detail isn't surfaced at this level of abstraction. + +### Client transport + +While the server is responsible for handling streams, the ``ClientTransport`` is +responsible for creating them. Client transports will typically maintain a +number of connections which may change over a period of time. Maintaining these +connections and other background work is done in the ``ClientTransport/connect()`` +method. Cancelling the task running this method will result in the transport +abruptly closing. The transport can be shutdown gracefully by calling +``ClientTransport/beginGracefulShutdown()``. + +Streams are created using ``ClientTransport/withStream(descriptor:options:_:)`` +and the lifetime of the stream is limited to the closure. The handler passed to +the method will be provided by a gRPC client and will ultimately include the +caller's code to send request messages and process response messages. Cancelling +the task abruptly closes the stream, although the transport should ensure that +doing this doesn't leave the other side waiting indefinitely. + +gRPC has mechanisms to deliver method-specific configuration at the transport +layer which can also change dynamically (see [gRFC A2: ServiceConfig in +DNS](https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md).) +This configuration is used to determine how clients should interact with servers +and how methods should be executed, such as the conditions under which they +may be retried. Some of this is exposed via the ``ClientTransport`` as +the ``ClientTransport/retryThrottle`` and +``ClientTransport/config(forMethod:)``. + +### Streams + +Both client and server transport protocols use ``RPCStream`` to represent +streams of information. Each RPC can be thought of as having two logical +streams: a request stream where information flows from client to server, +and a response stream where information flows from server to client. +Each ``RPCStream`` has inbound and outbound types corresponding to one end of +each stream. + +Inbound types are `AsyncSequence`s (specifically ``RPCAsyncSequence``) of stream +parts, and the outbound types are writer objects (``RPCWriter``) of stream parts. + +The stream parts are defined as: +- ``RPCRequestPart``, and +- ``RPCResponsePart``. + +A client stream has its outbound type as ``RPCRequestPart`` and its inbound type +as ``RPCResponsePart``. The server stream has its inbound type as ``RPCRequestPart`` +and its outbound type as ``RPCResponsePart``. + +The ``RPCRequestPart`` is made up of ``Metadata`` and messages (as `[UInt8]`). The +``RPCResponsePart`` extends this to include a final ``Status`` and ``Metadata``. + +``Metadata`` contains information about an RPC in the form of a list of +key-value pairs. Keys are strings and values may be strings or binary data (but are +typically strings). Keys for binary values have a "-bin" suffix. The transport +layer may use metadata to propagate transport-specific information about the call to +its peer. The call layer may attach gRPC specific metadata such as call time out +information. Users may also make use of metadata to propagate app specific information +to the remote peer. + +Each message part contains the binary data, typically this would be the serialized +representation of a Protocol Buffers message. + +The combined ``Status`` and ``Metadata`` part only appears in the ``RPCResponsePart`` +and indicates the final outcome of an RPC. It includes a ``Status/Code-swift.struct`` +and string describing the final outcome while the ``Metadata`` may contain additional +information about the RPC. + +## Call + +The "call" layer builds on top the transport layer to map higher level RPCs calls on +to streams. It also implements transport-agnostic functionality, like serialization +and deserialization, retries, hedging, and deadlines. + +Serialization is pluggable: you have control over the type of messages used although +most users will use Protocol Buffers. The serialization interface is small, there are +two protocols: +1. ``MessageSerializer`` for serializing messages to bytes, and +2. ``MessageDeserializer`` for deserializing messages from bytes. + +The [grpc/grpc-swift-protobuf](https://github.com/grpc/grpc-swift-protobuf) package +provides support for [SwiftProtobuf](https://github.com/apple/swift-protobuf) by +implementing serializers and a code generator for the Protocol Buffers +compiler, `protoc`. + +### Interceptors + +This layer also provides client and server interceptors allowing you to modify requests +and responses between the caller and the network. These are implemented as +``ClientInterceptor`` and ``ServerInterceptor``, respectively. + +As all RPC types are special-cases of bidirectional streaming RPCs, the interceptor +APIs follow the shape of the respective client and server bidirectional streaming APIs. +Naturally, the interceptors APIs are `async`. + +Interceptors are registered directly with the ``GRPCClient`` and ``GRPCServer`` and +can either be applied to all RPCs or to specific services. + +### Client + +The call layer includes a concrete ``GRPCClient`` which provides API to execute all +four types of RPC against a ``ClientTransport``. These methods are: + +- ``GRPCClient/unary(request:descriptor:serializer:deserializer:options:handler:)``, +- ``GRPCClient/clientStreaming(request:descriptor:serializer:deserializer:options:handler:)``, +- ``GRPCClient/serverStreaming(request:descriptor:serializer:deserializer:options:handler:)``, and +- ``GRPCClient/bidirectionalStreaming(request:descriptor:serializer:deserializer:options:handler:)``. + +As lower level methods they require you to pass in a serializer and +deserializer, as well as the descriptor of the method being called. Each method +has a response handling closure to process the response from the server and the +method won't return until the handler has returned. This enforces structured +concurrency. + +Most users won't use ``GRPCClient`` to execute RPCs directly, instead they will +use the generated client stubs which wrap the ``GRPCClient``. Users are +responsible for creating the client and running it (which starts and runs the +underlying transport). This is done by calling ``GRPCClient/run()``. The client +can be shutdown gracefully by calling ``GRPCClient/beginGracefulShutdown()`` +which will stop new RPCs from starting (by failing them with +``RPCError/Code-swift.struct/unavailable``) but allow existing ones to continue. +Existing work can be stopped more abruptly by cancelling the task where +``GRPCClient/run()`` is executing. + +### Server + +``GRPCServer`` is provided by the call layer to host services for a given +transport. Beyond creating the server it has a very limited API surface: it has +a ``GRPCServer/serve()`` method which runs the underlying transport and is the +task from which all accepted streams are run under. Much like the client, you +can initiate graceful shutdown by calling ``GRPCServer/beginGracefulShutdown()`` +which will stop new RPCs from being handled but will let existing RPCs run to +completion. Cancelling the task will close the server more abruptly. + +## Stub + +The stub layer is the layer which most users interact with. It provides service +specific interfaces generated from an interface definition language (IDL) such +as Protobuf. For clients this includes a concrete type per service for invoking +the methods provided by that service. For services this includes a protocol +which the service owner implements with the business logic for their service. + +The purpose of the stub layer is to reduce boilerplate: users generate stubs +from a single source of truth to native Swift types to remove errors which would +otherwise arise from writing them manually. + +However, the stub layer is optional, users may choose to not use it and +construct clients and services manually. A gRPC proxy, for example, would not +use the stub layer. + +### Server stubs + +Users implement services by conforming a type to a generated service `protocol`. +Each service has three protocols generated for it: +1. A "simple" service protocol (_note: this hasn't been implemented yet_), +2. A "regular" service protocol, and +3. A "streaming" service protocol. + +The streaming service protocol is the root `protocol`, most users won't need to +implement this protocol directly. It treats each of the four RPC types as a +bidirectional streaming RPC: this allows users to have the most flexibility over +how their RPCs are implemented at the cost of a harder to use API. The following +code shows how the streaming service protocol would look for a service: + +```swift +protocol ServiceName.StreamingServiceProtocol { + func unaryRPC( + request: StreamingServerRequest, + context: ServerContext + ) async throws -> StreamingServerResponse + + // client-, server-, and bidirectional-streaming are exactly the same as + // unary. +} +``` + +An example of where this is useful is when a user wants to implement a unary +method that first sends the initial metadata and then does some other processing +before sending a message. + +Many users won't need this much fidelity and will use the "regular" service +protocol which provides APIs which are more appropriate for the type of RPC. The +following code shows how the regular service protocol would look: + +```swift +protocol ServiceName.ServiceProtocol: ServiceName.StreamingServiceProtocol { + func unaryRPC( + request: ServerRequest, + context: ServerContext + ) async throws -> ServerResponse + + func clientStreamingRPC( + request: StreamingServerRequest, + context: ServerContext + ) async throws -> ServerResponse + + func serverStreamingRPC( + request: ServerRequest, + context: ServerContext + ) async throws -> StreamingServerResponse + + func bidirectionalStreamingRPC( + request: StreamingServerRequest, + context: ServerContext + ) async throws -> StreamingServerResponse +} +``` + +The conformance to the `StreamingServiceProtocol` is generated an implemented in +terms of the requirements of `ServiceProtocol`. This allows users to use the +higher-level API where possible but can implement the fully-streamed version +per-RPC if necessary. + +Some users also won't need access to metadata and will only be interested in the +messages sent and received on an RPC. A higher level "simple" service protocol +is provided for this use case: + +```swift +protocol ServiceName.SimpleServiceProtocol: ServiceName.ServiceProtocol { + func unaryRPC( + request: InputName, + context: ServerContext + ) async throws -> OutputName + + func clientStreamingRPC( + request: RPCAsyncSequence, + context: ServerContext + ) async throws -> OutputName + + func serverStreamingRPC( + request: InputName, + response: RPCWriter, + context: ServerContext + ) async throws + + func bidirectionalStreamingRPC( + request: RPCAsyncSequence, + response: RPCWriter, + context: ServerContext + ) async throws +} +``` + +> Note: the "simple" version hasn't been implemented yet. + +Much like the "regular" protocol, the "simple" version refines another service +protocol. In this case it refines the "regular" `ServiceProtocol` for which it +also has a default implementation. + +The root of the protocol hierarchy, the `StreamingServiceProtocol`, also +refines the ``RegistrableRPCService`` protocol. This `protocol` has a single +requirement for registering methods with an ``RPCRouter``. A default +implementation of this method is also provided. + +### Client stubs + +Generated client code is split into a `protocol` and a concrete `struct` +implementing the `protocol`. An example of the client protocol is: + +```swift +protocol ServiceName.ClientProtocol { + func unaryRPC( + request: ClientRequest, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse) async throws -> R + ) async throws -> R where R: Sendable + + func clientStreamingRPC( + request: StreamingClientRequest, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (ClientResponse) async throws -> R + ) async throws -> R where R: Sendable + + func serverStreamingRPC( + request: ClientRequest, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (StreamingClientResponse) async throws -> R + ) async throws -> R where R: Sendable + + func bidirectionalStreamingRPC( + request: StreamingClientRequest, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + options: CallOptions, + _ body: @Sendable @escaping (StreamingClientResponse) async throws -> R + ) async throws -> R where R: Sendable +} +``` + +Each method takes a request appropriate for its RPC type, a serializer, a +deserializer, a set of options and a handler for processing the response. The +function doesn't return until the response handler has returned and all +resources associated with the RPC have been cleaned up. + +An extension to the protocol is also generated which provides an appropriate +serializer and deserializer, defaults the options to `.defaults`, and for RPCs +with a single response message, defaults the closure to returning the response +message: + +```swift +extension ServiceName.ClientProtocol { + func unaryRPC( + request: ClientRequest, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse) async throws -> R = { try $0.message } + ) async throws -> R where R: Sendable { + // ... + } + + func clientStreamingRPC( + request: StreamingClientRequest, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (ClientResponse) async throws -> R = { try $0.message } + ) async throws -> R where R: Sendable { + // ... + } + + func serverStreamingRPC( + request: ClientRequest, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (StreamingClientResponse) async throws -> R + ) async throws -> R where R: Sendable { + // ... + } + + func bidirectionalStreamingRPC( + request: StreamingClientRequest, + options: CallOptions = .defaults, + _ body: @Sendable @escaping (StreamingClientResponse) async throws -> R + ) async throws -> R where R: Sendable { + // ... + } +} +``` + +An additional extension is also generated providing even higher level APIs. +These allow the user to avoid creating the request types by creating them on +behalf of the user. For unary RPCs this API distils down to message-in, +message-out, for bidirectional streaming it distils down to two closures, one +for sending messages, one for handling response messages. + +```swift +extension ServiceName.ClientProtocol { + func unaryRPC( + _ message: InputName, + metadata: Metadata = [:], + options: CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (ClientResponse) async throws -> Result = { try $0.message } + ) async throws -> Result where Result: Sendable { + // ... + } + + func clientStreamingRPC( + metadata: Metadata = [:], + options: CallOptions = .defaults, + requestProducer: @Sendable @escaping (RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (ClientResponse) async throws -> Result = { try $0.message } + ) async throws -> Result where Result: Sendable { + // ... + } + + func serverStreamingRPC( + _ message: InputName, + metadata: Metadata = [:], + options: CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + // ... + } + + func bidirectionalStreamingRPC( + metadata: Metadata = [:], + options: CallOptions = .defaults, + requestProducer: @Sendable @escaping (RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + // ... + } +} +``` + +To see this in use refer to the or tutorials +or the examples in the [grpc/grpc-swift](https://github.com/grpc/grpc-swift) +repository on GitHub. diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 4ccc4b5f2..dc6ae14dd 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -55,4 +55,5 @@ as tutorials. Resources for developers working on gRPC Swift: +- - From d5e0d7048089bc68b7f70f7ad3edc98c3f771ac1 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 7 Oct 2024 17:19:13 +0100 Subject: [PATCH 482/580] Add a merge method to Metadata (#2084) ## Motivation We want to be able to flatten `RPCError`s, and to do so we need to be able to merge the `Metadata` contained in each. ## Modifications This PR adds a helper function to merge one `Metadata` instance into another. ## Result Unblocks https://github.com/grpc/grpc-swift/pull/2083 and also provides a potentially useful API for users. **- Note:** Because of the way `Metadata` has been implemented, we can have multiple _identical_ key-value pairs. This isn't ideal, as it's particularly feasible that we'll end up with multiple repeated identical pairs when merging two `Metadata`s. I think we should reconsider the backing data structure (using a set for example) or add a check before inserting to avoid this. --- Sources/GRPCCore/Metadata.swift | 14 ++++++ Tests/GRPCCoreTests/MetadataTests.swift | 62 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/Sources/GRPCCore/Metadata.swift b/Sources/GRPCCore/Metadata.swift index 8326eb336..dfc095e1e 100644 --- a/Sources/GRPCCore/Metadata.swift +++ b/Sources/GRPCCore/Metadata.swift @@ -171,6 +171,20 @@ public struct Metadata: Sendable, Hashable { self.elements.append(.init(key: key, value: value)) } + /// Add the contents of a `Sequence` of key-value pairs to this `Metadata` instance. + /// + /// - Parameter other: the `Sequence` whose key-value pairs should be added into this `Metadata` instance. + public mutating func add(contentsOf other: some Sequence) { + self.elements.append(contentsOf: other.map(KeyValuePair.init)) + } + + /// Add the contents of another `Metadata` to this instance. + /// + /// - Parameter other: the `Metadata` whose key-value pairs should be added into this one. + public mutating func add(contentsOf other: Metadata) { + self.elements.append(contentsOf: other.elements) + } + /// Removes all values associated with the given key. /// /// - Parameter key: The key for which all values should be removed. diff --git a/Tests/GRPCCoreTests/MetadataTests.swift b/Tests/GRPCCoreTests/MetadataTests.swift index f0b29df04..68c3df85c 100644 --- a/Tests/GRPCCoreTests/MetadataTests.swift +++ b/Tests/GRPCCoreTests/MetadataTests.swift @@ -252,4 +252,66 @@ struct MetadataTests { #expect(self.metadata == ["key1": "value1", "key3": "value1"]) } } + + @Suite("Merge") + struct Merge { + var metadata: Metadata = [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + ] + var otherMetadata: Metadata = [ + "key4": "value4", + "key5": "value5", + ] + + @Test("Where key is already present with a different value") + mutating func mergeWhereKeyIsAlreadyPresentWithDifferentValue() async throws { + self.otherMetadata.addString("value1-2", forKey: "key1") + self.metadata.add(contentsOf: self.otherMetadata) + + #expect( + self.metadata == [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key1": "value1-2", + ] + ) + } + + @Test("Where key is already present with same value") + mutating func mergeWhereKeyIsAlreadyPresentWithSameValue() async throws { + self.otherMetadata.addString("value1-1", forKey: "key1") + self.metadata.add(contentsOf: self.otherMetadata) + + #expect( + self.metadata == [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + "key1": "value1-1", + ] + ) + } + + @Test("Where key is not already present") + mutating func mergeWhereKeyIsNotAlreadyPresent() async throws { + self.metadata.add(contentsOf: self.otherMetadata) + + #expect( + self.metadata == [ + "key1": "value1-1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5", + ] + ) + } + } } From 22d1b57e6dbf26374cb63e9d6523d8ed5f34ee6f Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 9 Oct 2024 07:46:17 +0100 Subject: [PATCH 483/580] Remove unbounded variable from formatting script (#2091) The formatting script should print a helpful message when formatting check fails showing which command to run to fix it. However, it fails to print this because it cannot find the `THIS_SCRIPT` env variable. --- dev/format.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/format.sh b/dev/format.sh index ac2d7f61a..f1e75bc9d 100755 --- a/dev/format.sh +++ b/dev/format.sh @@ -69,7 +69,7 @@ if "$lint"; then To fix, run the following command: - % $THIS_SCRIPT -f + % $here/format.sh -f " exit "${SWIFT_FORMAT_RC}" fi From ce25e2ce302f0837acad2508b3bdc3491a50ec31 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 9 Oct 2024 10:41:39 +0100 Subject: [PATCH 484/580] Flatten `RPCError` causes if they're also `RPCError`s with the same code (#2083) ## Motivation For errors happening deep in the task tree, we'd wrap them in many layers of `RPCError`s. This isn't particularly nice. ## Modifications This PR changes the behaviour of the `RPCError` initialiser to flatten the cause as long as it's an `RPCError` with the same status code as the wrapping error. ## Result Friendlier errors. --- Sources/GRPCCore/RPCError.swift | 56 ++++++- Tests/GRPCCoreTests/RPCErrorTests.swift | 209 ++++++++++++++++-------- 2 files changed, 187 insertions(+), 78 deletions(-) diff --git a/Sources/GRPCCore/RPCError.swift b/Sources/GRPCCore/RPCError.swift index 7354a7b83..810298e3a 100644 --- a/Sources/GRPCCore/RPCError.swift +++ b/Sources/GRPCCore/RPCError.swift @@ -35,18 +35,62 @@ public struct RPCError: Sendable, Hashable, Error { /// The original error which led to this error being thrown. public var cause: (any Error)? - /// Create a new RPC error. + /// Create a new RPC error. If the given `cause` is also an ``RPCError`` sharing the same `code`, + /// then they will be flattened into a single error, by merging the messages and metadata. /// /// - Parameters: /// - code: The status code. /// - message: A message providing additional context about the code. /// - metadata: Any metadata to attach to the error. /// - cause: An underlying error which led to this error being thrown. - public init(code: Code, message: String, metadata: Metadata = [:], cause: (any Error)? = nil) { - self.code = code - self.message = message - self.metadata = metadata - self.cause = cause + public init( + code: Code, + message: String, + metadata: Metadata = [:], + cause: (any Error)? = nil + ) { + if let rpcErrorCause = cause as? RPCError { + self = .init( + code: code, + message: message, + metadata: metadata, + cause: rpcErrorCause + ) + } else { + self.code = code + self.message = message + self.metadata = metadata + self.cause = cause + } + } + + /// Create a new RPC error. If the given `cause` shares the same `code`, then it will be flattened + /// into a single error, by merging the messages and metadata. + /// + /// - Parameters: + /// - code: The status code. + /// - message: A message providing additional context about the code. + /// - metadata: Any metadata to attach to the error. + /// - cause: An underlying ``RPCError`` which led to this error being thrown. + public init( + code: Code, + message: String, + metadata: Metadata = [:], + cause: RPCError + ) { + if cause.code == code { + self.code = code + self.message = message + " \(cause.message)" + var mergedMetadata = metadata + mergedMetadata.add(contentsOf: cause.metadata) + self.metadata = mergedMetadata + self.cause = cause.cause + } else { + self.code = code + self.message = message + self.metadata = metadata + self.cause = cause + } } /// Create a new RPC error from the provided ``Status``. diff --git a/Tests/GRPCCoreTests/RPCErrorTests.swift b/Tests/GRPCCoreTests/RPCErrorTests.swift index dc65122b0..7f87e697a 100644 --- a/Tests/GRPCCoreTests/RPCErrorTests.swift +++ b/Tests/GRPCCoreTests/RPCErrorTests.swift @@ -14,114 +14,179 @@ * limitations under the License. */ import GRPCCore -import XCTest - -final class RPCErrorTests: XCTestCase { - private static let statusCodeRawValue: [(RPCError.Code, Int)] = [ - (.cancelled, 1), - (.unknown, 2), - (.invalidArgument, 3), - (.deadlineExceeded, 4), - (.notFound, 5), - (.alreadyExists, 6), - (.permissionDenied, 7), - (.resourceExhausted, 8), - (.failedPrecondition, 9), - (.aborted, 10), - (.outOfRange, 11), - (.unimplemented, 12), - (.internalError, 13), - (.unavailable, 14), - (.dataLoss, 15), - (.unauthenticated, 16), - ] +import Testing +@Suite("RPCError Tests") +struct RPCErrorTests { + @Test("Custom String Convertible") func testCustomStringConvertible() { - XCTAssertDescription(RPCError(code: .dataLoss, message: ""), #"dataLoss: """#) - XCTAssertDescription(RPCError(code: .unknown, message: "message"), #"unknown: "message""#) - XCTAssertDescription(RPCError(code: .aborted, message: "message"), #"aborted: "message""#) + #expect(String(describing: RPCError(code: .dataLoss, message: "")) == #"dataLoss: """#) + #expect( + String(describing: RPCError(code: .unknown, message: "message")) == #"unknown: "message""# + ) + #expect( + String(describing: RPCError(code: .aborted, message: "message")) == #"aborted: "message""# + ) struct TestError: Error {} - XCTAssertDescription( - RPCError(code: .aborted, message: "message", cause: TestError()), - #"aborted: "message" (cause: "TestError()")"# + #expect( + String(describing: RPCError(code: .aborted, message: "message", cause: TestError())) + == #"aborted: "message" (cause: "TestError()")"# ) } + @Test("Error from Status") func testErrorFromStatus() throws { var status = Status(code: .ok, message: "") // ok isn't an error - XCTAssertNil(RPCError(status: status)) + #expect(RPCError(status: status) == nil) status.code = .invalidArgument - var error = try XCTUnwrap(RPCError(status: status)) - XCTAssertEqual(error.code, .invalidArgument) - XCTAssertEqual(error.message, "") - XCTAssertEqual(error.metadata, [:]) + var error = try #require(RPCError(status: status)) + #expect(error.code == .invalidArgument) + #expect(error.message == "") + #expect(error.metadata == [:]) status.code = .cancelled status.message = "an error message" - error = try XCTUnwrap(RPCError(status: status)) - XCTAssertEqual(error.code, .cancelled) - XCTAssertEqual(error.message, "an error message") - XCTAssertEqual(error.metadata, [:]) + error = try #require(RPCError(status: status)) + #expect(error.code == .cancelled) + #expect(error.message == "an error message") + #expect(error.metadata == [:]) } - func testErrorCodeFromStatusCode() throws { - XCTAssertNil(RPCError.Code(Status.Code.ok)) - XCTAssertEqual(RPCError.Code(Status.Code.cancelled), .cancelled) - XCTAssertEqual(RPCError.Code(Status.Code.unknown), .unknown) - XCTAssertEqual(RPCError.Code(Status.Code.invalidArgument), .invalidArgument) - XCTAssertEqual(RPCError.Code(Status.Code.deadlineExceeded), .deadlineExceeded) - XCTAssertEqual(RPCError.Code(Status.Code.notFound), .notFound) - XCTAssertEqual(RPCError.Code(Status.Code.alreadyExists), .alreadyExists) - XCTAssertEqual(RPCError.Code(Status.Code.permissionDenied), .permissionDenied) - XCTAssertEqual(RPCError.Code(Status.Code.resourceExhausted), .resourceExhausted) - XCTAssertEqual(RPCError.Code(Status.Code.failedPrecondition), .failedPrecondition) - XCTAssertEqual(RPCError.Code(Status.Code.aborted), .aborted) - XCTAssertEqual(RPCError.Code(Status.Code.outOfRange), .outOfRange) - XCTAssertEqual(RPCError.Code(Status.Code.unimplemented), .unimplemented) - XCTAssertEqual(RPCError.Code(Status.Code.internalError), .internalError) - XCTAssertEqual(RPCError.Code(Status.Code.unavailable), .unavailable) - XCTAssertEqual(RPCError.Code(Status.Code.dataLoss), .dataLoss) - XCTAssertEqual(RPCError.Code(Status.Code.unauthenticated), .unauthenticated) + @Test( + "Error Code from Status Code", + arguments: [ + (Status.Code.ok, nil), + (Status.Code.cancelled, RPCError.Code.cancelled), + (Status.Code.unknown, RPCError.Code.unknown), + (Status.Code.invalidArgument, RPCError.Code.invalidArgument), + (Status.Code.deadlineExceeded, RPCError.Code.deadlineExceeded), + (Status.Code.notFound, RPCError.Code.notFound), + (Status.Code.alreadyExists, RPCError.Code.alreadyExists), + (Status.Code.permissionDenied, RPCError.Code.permissionDenied), + (Status.Code.resourceExhausted, RPCError.Code.resourceExhausted), + (Status.Code.failedPrecondition, RPCError.Code.failedPrecondition), + (Status.Code.aborted, RPCError.Code.aborted), + (Status.Code.outOfRange, RPCError.Code.outOfRange), + (Status.Code.unimplemented, RPCError.Code.unimplemented), + (Status.Code.internalError, RPCError.Code.internalError), + (Status.Code.unavailable, RPCError.Code.unavailable), + (Status.Code.dataLoss, RPCError.Code.dataLoss), + (Status.Code.unauthenticated, RPCError.Code.unauthenticated), + ] + ) + func testErrorCodeFromStatusCode(statusCode: Status.Code, rpcErrorCode: RPCError.Code?) throws { + #expect(RPCError.Code(statusCode) == rpcErrorCode) } + @Test("Equatable Conformance") func testEquatableConformance() { - XCTAssertEqual( - RPCError(code: .cancelled, message: ""), + #expect( RPCError(code: .cancelled, message: "") + == RPCError(code: .cancelled, message: "") ) - XCTAssertEqual( - RPCError(code: .cancelled, message: "message"), + #expect( RPCError(code: .cancelled, message: "message") + == RPCError(code: .cancelled, message: "message") ) - XCTAssertEqual( - RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]), + #expect( RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]) + == RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]) ) - XCTAssertNotEqual( - RPCError(code: .cancelled, message: ""), + #expect( + RPCError(code: .cancelled, message: "") + != RPCError(code: .cancelled, message: "message") + ) + + #expect( RPCError(code: .cancelled, message: "message") + != RPCError(code: .unknown, message: "message") ) - XCTAssertNotEqual( - RPCError(code: .cancelled, message: "message"), - RPCError(code: .unknown, message: "message") + #expect( + RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]) + != RPCError(code: .cancelled, message: "message", metadata: ["foo": "baz"]) ) + } - XCTAssertNotEqual( - RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]), - RPCError(code: .cancelled, message: "message", metadata: ["foo": "baz"]) + @Test( + "Status Code Raw Values", + arguments: [ + (RPCError.Code.cancelled, 1), + (.unknown, 2), + (.invalidArgument, 3), + (.deadlineExceeded, 4), + (.notFound, 5), + (.alreadyExists, 6), + (.permissionDenied, 7), + (.resourceExhausted, 8), + (.failedPrecondition, 9), + (.aborted, 10), + (.outOfRange, 11), + (.unimplemented, 12), + (.internalError, 13), + (.unavailable, 14), + (.dataLoss, 15), + (.unauthenticated, 16), + ] + ) + func testStatusCodeRawValues(statusCode: RPCError.Code, rawValue: Int) { + #expect(statusCode.rawValue == rawValue, "\(statusCode) had unexpected raw value") + } + + @Test("Flatten causes with same status code") + func testFlattenCausesWithSameStatusCode() { + let error1 = RPCError(code: .unknown, message: "Error 1.") + let error2 = RPCError(code: .unknown, message: "Error 2.", cause: error1) + let error3 = RPCError(code: .dataLoss, message: "Error 3.", cause: error2) + let error4 = RPCError(code: .aborted, message: "Error 4.", cause: error3) + let error5 = RPCError( + code: .aborted, + message: "Error 5.", + cause: error4 + ) + + let unknownMerged = RPCError(code: .unknown, message: "Error 2. Error 1.") + let dataLossMerged = RPCError(code: .dataLoss, message: "Error 3.", cause: unknownMerged) + let abortedMerged = RPCError( + code: .aborted, + message: "Error 5. Error 4.", + cause: dataLossMerged ) + #expect(error5 == abortedMerged) } - func testStatusCodeRawValues() { - for (code, expected) in Self.statusCodeRawValue { - XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value") - } + @Test("Causes of errors with different status codes aren't flattened") + func testDifferentStatusCodeAreNotFlattened() throws { + let error1 = RPCError(code: .unknown, message: "Error 1.") + let error2 = RPCError(code: .dataLoss, message: "Error 2.", cause: error1) + let error3 = RPCError(code: .alreadyExists, message: "Error 3.", cause: error2) + let error4 = RPCError(code: .aborted, message: "Error 4.", cause: error3) + let error5 = RPCError( + code: .deadlineExceeded, + message: "Error 5.", + cause: error4 + ) + + #expect(error5.code == .deadlineExceeded) + #expect(error5.message == "Error 5.") + let wrappedError4 = try #require(error5.cause as? RPCError) + #expect(wrappedError4.code == .aborted) + #expect(wrappedError4.message == "Error 4.") + let wrappedError3 = try #require(wrappedError4.cause as? RPCError) + #expect(wrappedError3.code == .alreadyExists) + #expect(wrappedError3.message == "Error 3.") + let wrappedError2 = try #require(wrappedError3.cause as? RPCError) + #expect(wrappedError2.code == .dataLoss) + #expect(wrappedError2.message == "Error 2.") + let wrappedError1 = try #require(wrappedError2.cause as? RPCError) + #expect(wrappedError1.code == .unknown) + #expect(wrappedError1.message == "Error 1.") + #expect(wrappedError1.cause == nil) } } From 945cbf69936dded6c982f609efd0ec317cb9aea5 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 9 Oct 2024 16:26:25 +0100 Subject: [PATCH 485/580] Add an RPC cancellation handler (#2090) Motivation: As a service author it's useful to know if the RPC has been cancelled (because it's timed out, the remote peer closed it, the connection dropped etc). For cases where the stream has already closed this can be surfaced by a read or write failing. However, for cases like server-streaming RPCs where there are no reads and writes can be infrequent it's useful to have a more explicit signal. Modifications: - Add a `ServerCancellationManager`, this is internal per-stream storage for registering cancellation handlers and storing whether the RPC has been cancelled. - Add the `RPCCancellationHandle` nested within the `ServerContext`. This holds an instance of the manager and provides higher level APIs allowing users to check if the RPC has been cancellation and to wait until the RPC has been cancelled. - Add a top-level `withRPCCancellationHandler` which registers a callback with the manager. - Add a top-level `withServerContextRPCCancellationHandle` for creating and binding the task local manager. This is intended for use by transport implementations rather than users. - Update the in-process transport to cancel RPCs when shutting down gracefully. - Update the server executor to cancel RPCs when the timeout fires. Result: Users can watch for cancellation using `withRPCCancellationHandler`. --- .../Internal/ServerCancellationManager.swift | 254 ++++++++++++++++++ .../Server/Internal/ServerRPCExecutor.swift | 48 ++-- .../ServerContext+RPCCancellationHandle.swift | 117 ++++++++ .../GRPCCore/Call/Server/ServerContext.swift | 11 +- .../GRPCCore/Internal/Result+Catching.swift | 4 +- .../InProcessTransport+Server.swift | 61 ++++- .../ServerCancellationManagerTests.swift | 91 +++++++ .../ServerRPCExecutorTestHarness.swift | 56 ++-- .../Internal/ServerRPCExecutorTests.swift | 22 +- .../Call/Server/ServerContextTests.swift | 62 +++++ .../InProcessTransportTests.swift | 125 +++++++++ 11 files changed, 778 insertions(+), 73 deletions(-) create mode 100644 Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift create mode 100644 Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift create mode 100644 Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift create mode 100644 Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift create mode 100644 Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift b/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift new file mode 100644 index 000000000..471f9d007 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/Internal/ServerCancellationManager.swift @@ -0,0 +1,254 @@ +/* + * 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. + */ + +private import Synchronization + +/// Stores cancellation state for an RPC on the server . +package final class ServerCancellationManager: Sendable { + private let state: Mutex + + package init() { + self.state = Mutex(State()) + } + + /// Returns whether the RPC has been marked as cancelled. + package var isRPCCancelled: Bool { + self.state.withLock { + return $0.isRPCCancelled + } + } + + /// Marks the RPC as cancelled, potentially running any cancellation handlers. + package func cancelRPC() { + switch self.state.withLock({ $0.cancelRPC() }) { + case .executeAndResume(let onCancelHandlers, let onCancelWaiters): + for handler in onCancelHandlers { + handler.handler() + } + + for onCancelWaiter in onCancelWaiters { + switch onCancelWaiter { + case .taskCancelled: + () + case .waiting(_, let continuation): + continuation.resume(returning: .rpc) + } + } + + case .doNothing: + () + } + } + + /// Adds a handler which is invoked when the RPC is cancelled. + /// + /// - Returns: The ID of the handler, if it was added, or `nil` if the RPC is already cancelled. + package func addRPCCancelledHandler(_ handler: @Sendable @escaping () -> Void) -> UInt64? { + return self.state.withLock { state -> UInt64? in + state.addRPCCancelledHandler(handler) + } + } + + /// Removes a handler by its ID. + package func removeRPCCancelledHandler(withID id: UInt64) { + self.state.withLock { state in + state.removeRPCCancelledHandler(withID: id) + } + } + + /// Suspends until the RPC is cancelled or the `Task` is cancelled. + package func suspendUntilRPCIsCancelled() async throws(CancellationError) { + let id = self.state.withLock { $0.nextID() } + + let source = await withTaskCancellationHandler { + await withCheckedContinuation { continuation in + let onAddWaiter = self.state.withLock { + $0.addRPCIsCancelledWaiter(continuation: continuation, withID: id) + } + + switch onAddWaiter { + case .doNothing: + () + case .complete(let continuation, let result): + continuation.resume(returning: result) + } + } + } onCancel: { + switch self.state.withLock({ $0.cancelRPCCancellationWaiter(withID: id) }) { + case .resume(let continuation, let result): + continuation.resume(returning: result) + case .doNothing: + () + } + } + + switch source { + case .rpc: + () + case .task: + throw CancellationError() + } + } +} + +extension ServerCancellationManager { + enum CancellationSource { + case rpc + case task + } + + struct Handler: Sendable { + var id: UInt64 + var handler: @Sendable () -> Void + } + + enum Waiter: Sendable { + case waiting(UInt64, CheckedContinuation) + case taskCancelled(UInt64) + + var id: UInt64 { + switch self { + case .waiting(let id, _): + return id + case .taskCancelled(let id): + return id + } + } + } + + struct State { + private var handlers: [Handler] + private var waiters: [Waiter] + private var _nextID: UInt64 + var isRPCCancelled: Bool + + mutating func nextID() -> UInt64 { + let id = self._nextID + self._nextID &+= 1 + return id + } + + init() { + self.handlers = [] + self.waiters = [] + self._nextID = 0 + self.isRPCCancelled = false + } + + mutating func cancelRPC() -> OnCancelRPC { + let onCancel: OnCancelRPC + + if self.isRPCCancelled { + onCancel = .doNothing + } else { + self.isRPCCancelled = true + onCancel = .executeAndResume(self.handlers, self.waiters) + self.handlers = [] + self.waiters = [] + } + + return onCancel + } + + mutating func addRPCCancelledHandler(_ handler: @Sendable @escaping () -> Void) -> UInt64? { + if self.isRPCCancelled { + handler() + return nil + } else { + let id = self.nextID() + self.handlers.append(.init(id: id, handler: handler)) + return id + } + } + + mutating func removeRPCCancelledHandler(withID id: UInt64) { + if let index = self.handlers.firstIndex(where: { $0.id == id }) { + self.handlers.remove(at: index) + } + } + + enum OnCancelRPC { + case executeAndResume([Handler], [Waiter]) + case doNothing + } + + enum OnAddWaiter { + case complete(CheckedContinuation, CancellationSource) + case doNothing + } + + mutating func addRPCIsCancelledWaiter( + continuation: CheckedContinuation, + withID id: UInt64 + ) -> OnAddWaiter { + let onAddWaiter: OnAddWaiter + + if self.isRPCCancelled { + onAddWaiter = .complete(continuation, .rpc) + } else if let index = self.waiters.firstIndex(where: { $0.id == id }) { + switch self.waiters[index] { + case .taskCancelled: + onAddWaiter = .complete(continuation, .task) + case .waiting: + // There's already a continuation enqueued. + fatalError("Inconsistent state") + } + } else { + self.waiters.append(.waiting(id, continuation)) + onAddWaiter = .doNothing + } + + return onAddWaiter + } + + enum OnCancelRPCCancellationWaiter { + case resume(CheckedContinuation, CancellationSource) + case doNothing + } + + mutating func cancelRPCCancellationWaiter(withID id: UInt64) -> OnCancelRPCCancellationWaiter { + let onCancelWaiter: OnCancelRPCCancellationWaiter + + if let index = self.waiters.firstIndex(where: { $0.id == id }) { + let waiter = self.waiters.removeWithoutMaintainingOrder(at: index) + switch waiter { + case .taskCancelled: + onCancelWaiter = .doNothing + case .waiting(_, let continuation): + onCancelWaiter = .resume(continuation, .task) + } + } else { + self.waiters.append(.taskCancelled(id)) + onCancelWaiter = .doNothing + } + + return onCancelWaiter + } + } +} + +extension Array { + fileprivate mutating func removeWithoutMaintainingOrder(at index: Int) -> Element { + let lastElementIndex = self.index(before: self.endIndex) + + if index == lastElementIndex { + return self.remove(at: index) + } else { + self.swapAt(index, lastElementIndex) + return self.removeLast() + } + } +} diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index f85cbe318..d9a35da51 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -119,43 +119,29 @@ struct ServerRPCExecutor { _ context: ServerContext ) async throws -> StreamingServerResponse ) async { - await withTaskGroup(of: ServerExecutorTask.self) { group in + await withTaskGroup(of: Void.self) { group in group.addTask { - let result = await Result { + do { try await Task.sleep(for: timeout, clock: .continuous) + context.cancellation.cancel() + } catch { + () // Only cancel the RPC if the timeout completes. } - return .timedOut(result) } - group.addTask { - await Self._processRPC( - context: context, - metadata: metadata, - inbound: inbound, - outbound: outbound, - deserializer: deserializer, - serializer: serializer, - interceptors: interceptors, - handler: handler - ) - return .executed - } - - while let next = await group.next() { - switch next { - case .timedOut(.success): - // Timeout expired; cancel the work. - group.cancelAll() - - case .timedOut(.failure): - // Timeout failed (because it was cancelled). Wait for more tasks to finish. - () + await Self._processRPC( + context: context, + metadata: metadata, + inbound: inbound, + outbound: outbound, + deserializer: deserializer, + serializer: serializer, + interceptors: interceptors, + handler: handler + ) - case .executed: - // The work finished. Cancel any remaining tasks. - group.cancelAll() - } - } + // Cancel the timeout + group.cancelAll() } } diff --git a/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift new file mode 100644 index 000000000..5e0f63367 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/ServerContext+RPCCancellationHandle.swift @@ -0,0 +1,117 @@ +/* + * 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 Synchronization + +extension ServerContext { + @TaskLocal + internal static var rpcCancellation: RPCCancellationHandle? + + /// A handle for the cancellation status of the RPC. + public struct RPCCancellationHandle: Sendable { + internal let manager: ServerCancellationManager + + /// Create a cancellation handle. + /// + /// To create an instance of this handle appropriately bound to a `Task` + /// use ``withServerContextRPCCancellationHandle(_:)``. + public init() { + self.manager = ServerCancellationManager() + } + + /// Returns whether the RPC has been cancelled. + public var isCancelled: Bool { + self.manager.isRPCCancelled + } + + /// Waits until the RPC has been cancelled. + /// + /// Throws a `CancellationError` if the `Task` is cancelled. + /// + /// You can also be notified when an RPC is cancelled by using + /// ``withRPCCancellationHandler(operation:onCancelRPC:)``. + public var cancelled: Void { + get async throws { + try await self.manager.suspendUntilRPCIsCancelled() + } + } + + /// Signal that the RPC should be cancelled. + /// + /// This is idempotent: calling it more than once has no effect. + public func cancel() { + self.manager.cancelRPC() + } + } +} + +/// Execute an operation with an RPC cancellation handler that's immediately invoked +/// if the RPC is canceled. +/// +/// RPCs can be cancelled for a number of reasons including: +/// 1. The RPC was taking too long to process and a timeout passed. +/// 2. The remote peer closed the underlying stream, either because they were no longer +/// interested in the result or due to a broken connection. +/// 3. The server began shutting down. +/// +/// - Important: This only applies to RPCs on the server. +/// - Parameters: +/// - operation: The operation to execute. +/// - handler: The handler which is invoked when the RPC is cancelled. +/// - Throws: Any error thrown by the `operation` closure. +/// - Returns: The result of the `operation` closure. +public func withRPCCancellationHandler( + operation: () async throws(Failure) -> Result, + onCancelRPC handler: @Sendable @escaping () -> Void +) async throws(Failure) -> Result { + guard let manager = ServerContext.rpcCancellation?.manager, + let id = manager.addRPCCancelledHandler(handler) + else { + return try await operation() + } + + defer { + manager.removeRPCCancelledHandler(withID: id) + } + + return try await operation() +} + +/// Provides scoped access to a server RPC cancellation handle. +/// +/// The cancellation handle should be passed to a ``ServerContext`` and last +/// the duration of the RPC. +/// +/// - Important: This function is intended for use when implementing +/// a ``ServerTransport``. +/// +/// If you want to be notified about RPCs being cancelled +/// use ``withRPCCancellationHandler(operation:onCancelRPC:)``. +/// +/// - Parameter operation: The operation to execute with the handle. +public func withServerContextRPCCancellationHandle( + _ operation: (ServerContext.RPCCancellationHandle) async throws(Failure) -> Success +) async throws(Failure) -> Success { + let handle = ServerContext.RPCCancellationHandle() + let result = await ServerContext.$rpcCancellation.withValue(handle) { + // Wrap up the outcome in a result as 'withValue' doesn't support typed throws. + return await Swift.Result { () async throws(Failure) -> Success in + return try await operation(handle) + } + } + + return try result.get() +} diff --git a/Sources/GRPCCore/Call/Server/ServerContext.swift b/Sources/GRPCCore/Call/Server/ServerContext.swift index a11f09acb..4d8613f93 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -19,8 +19,17 @@ public struct ServerContext: Sendable { /// A description of the method being called. public var descriptor: MethodDescriptor + /// A handle for checking the cancellation status of an RPC. + public var cancellation: RPCCancellationHandle + /// Create a new server context. - public init(descriptor: MethodDescriptor) { + /// + /// - Parameters: + /// - descriptor: A description of the method being called. + /// - cancellation: A cancellation handle. You can create a cancellation handle + /// using ``withServerContextRPCCancellationHandle(_:)``. + public init(descriptor: MethodDescriptor, cancellation: RPCCancellationHandle) { self.descriptor = descriptor + self.cancellation = cancellation } } diff --git a/Sources/GRPCCore/Internal/Result+Catching.swift b/Sources/GRPCCore/Internal/Result+Catching.swift index bf2393752..8f9cbe59c 100644 --- a/Sources/GRPCCore/Internal/Result+Catching.swift +++ b/Sources/GRPCCore/Internal/Result+Catching.swift @@ -14,12 +14,12 @@ * limitations under the License. */ -extension Result where Failure == any Error { +extension Result { /// Like `Result(catching:)`, but `async`. /// /// - Parameter body: An `async` closure to catch the result of. @inlinable - init(catching body: () async throws -> Success) async { + init(catching body: () async throws(Failure) -> Success) async { do { self = .success(try await body()) } catch { diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 66e32d06f..02b132ac8 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -15,6 +15,7 @@ */ public import GRPCCore +private import Synchronization extension InProcessTransport { /// An in-process implementation of a ``ServerTransport``. @@ -27,16 +28,54 @@ extension InProcessTransport { /// To stop listening to new requests, call ``beginGracefulShutdown()``. /// /// - SeeAlso: ``ClientTransport`` - public struct Server: ServerTransport, Sendable { + public final class Server: ServerTransport, Sendable { public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable private let newStreams: AsyncStream> private let newStreamsContinuation: AsyncStream>.Continuation + private struct State: Sendable { + private var _nextID: UInt64 + private var handles: [UInt64: ServerContext.RPCCancellationHandle] + private var isShutdown: Bool + + private mutating func nextID() -> UInt64 { + let id = self._nextID + self._nextID &+= 1 + return id + } + + init() { + self._nextID = 0 + self.handles = [:] + self.isShutdown = false + } + + mutating func addHandle(_ handle: ServerContext.RPCCancellationHandle) -> (UInt64, Bool) { + let handleID = self.nextID() + self.handles[handleID] = handle + return (handleID, self.isShutdown) + } + + mutating func removeHandle(withID id: UInt64) { + self.handles.removeValue(forKey: id) + } + + mutating func beginShutdown() -> [ServerContext.RPCCancellationHandle] { + self.isShutdown = true + let values = Array(self.handles.values) + self.handles.removeAll() + return values + } + } + + private let handles: Mutex + /// Creates a new instance of ``Server``. public init() { (self.newStreams, self.newStreamsContinuation) = AsyncStream.makeStream() + self.handles = Mutex(State()) } /// Publish a new ``RPCStream``, which will be returned by the transport's ``events`` @@ -64,8 +103,21 @@ extension InProcessTransport { await withDiscardingTaskGroup { group in for await stream in self.newStreams { group.addTask { - let context = ServerContext(descriptor: stream.descriptor) - await streamHandler(stream, context) + await withServerContextRPCCancellationHandle { handle in + let (id, isShutdown) = self.handles.withLock({ $0.addHandle(handle) }) + defer { + self.handles.withLock { $0.removeHandle(withID: id) } + } + + // This happens if `beginGracefulShutdown` is called after the stream is added to + // new streams but before it's dequeued. + if isShutdown { + handle.cancel() + } + + let context = ServerContext(descriptor: stream.descriptor, cancellation: handle) + await streamHandler(stream, context) + } } } } @@ -76,6 +128,9 @@ extension InProcessTransport { /// - SeeAlso: ``ServerTransport`` public func beginGracefulShutdown() { self.newStreamsContinuation.finish() + for handle in self.handles.withLock({ $0.beginShutdown() }) { + handle.cancel() + } } } } diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift new file mode 100644 index 000000000..45c851de8 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift @@ -0,0 +1,91 @@ +/* + * 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 Testing + +@Suite +struct ServerCancellationManagerTests { + @Test("Isn't cancelled after init") + func isNotCancelled() { + let manager = ServerCancellationManager() + #expect(!manager.isRPCCancelled) + } + + @Test("Is cancelled") + func isCancelled() { + let manager = ServerCancellationManager() + manager.cancelRPC() + #expect(manager.isRPCCancelled) + } + + @Test("Cancellation handler runs") + func addCancellationHandler() async throws { + let manager = ServerCancellationManager() + let signal = AsyncStream.makeStream(of: Void.self) + + let id = manager.addRPCCancelledHandler { + signal.continuation.finish() + } + + #expect(id != nil) + manager.cancelRPC() + let events: [Void] = await signal.stream.reduce(into: []) { $0.append($1) } + #expect(events.isEmpty) + } + + @Test("Cancellation handler runs immediately when already cancelled") + func addCancellationHandlerAfterCancelled() async throws { + let manager = ServerCancellationManager() + let signal = AsyncStream.makeStream(of: Void.self) + manager.cancelRPC() + + let id = manager.addRPCCancelledHandler { + signal.continuation.finish() + } + + #expect(id == nil) + let events: [Void] = await signal.stream.reduce(into: []) { $0.append($1) } + #expect(events.isEmpty) + } + + @Test("Remove cancellation handler") + func removeCancellationHandler() async throws { + let manager = ServerCancellationManager() + let signal = AsyncStream.makeStream(of: Void.self) + + let id = manager.addRPCCancelledHandler { + Issue.record("Unexpected cancellation") + } + + #expect(id != nil) + manager.removeRPCCancelledHandler(withID: id!) + manager.cancelRPC() + } + + @Test("Wait for cancellation") + func waitForCancellation() async throws { + let manager = ServerCancellationManager() + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await manager.suspendUntilRPCIsCancelled() + } + + manager.cancelRPC() + try await group.waitForAll() + } + } +} diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift index 6cca2d4d2..e645c5c20 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTestSupport/ServerRPCExecutorTestHarness.swift @@ -20,24 +20,29 @@ import XCTest struct ServerRPCExecutorTestHarness { struct ServerHandler: Sendable { let fn: - @Sendable (StreamingServerRequest) async throws -> StreamingServerResponse + @Sendable ( + _ request: StreamingServerRequest, + _ context: ServerContext + ) async throws -> StreamingServerResponse init( _ fn: @escaping @Sendable ( - StreamingServerRequest + _ request: StreamingServerRequest, + _ context: ServerContext ) async throws -> StreamingServerResponse ) { self.fn = fn } func handle( - _ request: StreamingServerRequest + _ request: StreamingServerRequest, + _ context: ServerContext ) async throws -> StreamingServerResponse { - try await self.fn(request) + try await self.fn(request, context) } static func throwing(_ error: any Error) -> Self { - return Self { _ in throw error } + return Self { _, _ in throw error } } } @@ -51,7 +56,8 @@ struct ServerRPCExecutorTestHarness { deserializer: some MessageDeserializer, serializer: some MessageSerializer, handler: @escaping @Sendable ( - StreamingServerRequest + StreamingServerRequest, + ServerContext ) async throws -> StreamingServerResponse, producer: @escaping @Sendable ( RPCWriter.Closable @@ -93,21 +99,27 @@ struct ServerRPCExecutorTestHarness { } group.addTask { - let context = ServerContext(descriptor: MethodDescriptor(service: "foo", method: "bar")) - await ServerRPCExecutor.execute( - context: context, - stream: RPCStream( - descriptor: context.descriptor, - inbound: RPCAsyncSequence(wrapping: input.stream), - outbound: RPCWriter.Closable(wrapping: output.continuation) - ), - deserializer: deserializer, - serializer: serializer, - interceptors: self.interceptors, - handler: { stream, context in - try await handler.handle(stream) - } - ) + await withServerContextRPCCancellationHandle { cancellation in + let context = ServerContext( + descriptor: MethodDescriptor(service: "foo", method: "bar"), + cancellation: cancellation + ) + + await ServerRPCExecutor.execute( + context: context, + stream: RPCStream( + descriptor: context.descriptor, + inbound: RPCAsyncSequence(wrapping: input.stream), + outbound: RPCWriter.Closable(wrapping: output.continuation) + ), + deserializer: deserializer, + serializer: serializer, + interceptors: self.interceptors, + handler: { stream, context in + try await handler.handle(stream, context) + } + ) + } } try await group.waitForAll() @@ -135,7 +147,7 @@ struct ServerRPCExecutorTestHarness { extension ServerRPCExecutorTestHarness.ServerHandler where Input == Output { static var echo: Self { - return Self { request in + return Self { request, context in return StreamingServerResponse(metadata: request.metadata) { writer in try await writer.write(contentsOf: request.messages) return [:] diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index f047955da..0533fe26b 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -83,7 +83,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute( deserializer: JSONDeserializer(), serializer: JSONSerializer() - ) { request in + ) { request, _ in let messages = try await request.messages.collect() XCTAssertEqual(messages, ["hello"]) return StreamingServerResponse(metadata: request.metadata) { writer in @@ -112,7 +112,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute( deserializer: JSONDeserializer(), serializer: JSONSerializer() - ) { request in + ) { request, _ in let messages = try await request.messages.collect() XCTAssertEqual(messages, ["hello", "world"]) return StreamingServerResponse(metadata: request.metadata) { writer in @@ -144,7 +144,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute( deserializer: IdentityDeserializer(), serializer: IdentitySerializer() - ) { request in + ) { request, _ in return StreamingServerResponse(metadata: request.metadata) { _ in return ["bar": "baz"] } @@ -235,15 +235,9 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute( deserializer: IdentityDeserializer(), serializer: IdentitySerializer() - ) { request in - do { - try await Task.sleep(until: .now.advanced(by: .seconds(180)), clock: .continuous) - } catch is CancellationError { - throw RPCError(code: .cancelled, message: "Sleep was cancelled") - } - - XCTFail("Server handler should've been cancelled by timeout.") - return StreamingServerResponse(error: RPCError(code: .failedPrecondition, message: "")) + ) { request, context in + try await context.cancellation.cancelled + throw RPCError(code: .cancelled, message: "Cancelled from server handler") } producer: { inbound in try await inbound.write(.metadata(["grpc-timeout": "1000n"])) await inbound.finish() @@ -251,7 +245,7 @@ final class ServerRPCExecutorTests: XCTestCase { let part = try await outbound.collect().first XCTAssertStatus(part) { status, _ in XCTAssertEqual(status.code, .cancelled) - XCTAssertEqual(status.message, "Sleep was cancelled") + XCTAssertEqual(status.message, "Cancelled from server handler") } } } @@ -268,7 +262,7 @@ final class ServerRPCExecutorTests: XCTestCase { try await harness.execute( deserializer: IdentityDeserializer(), serializer: IdentitySerializer() - ) { request in + ) { request, _ in XCTFail("Unexpected request") return StreamingServerResponse( of: [UInt8].self, diff --git a/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift new file mode 100644 index 000000000..c524519ff --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/ServerContextTests.swift @@ -0,0 +1,62 @@ +/* + * 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 Testing + +@Suite("ServerContext") +struct ServerContextTests { + @Suite("CancellationHandle") + struct CancellationHandle { + @Test("Is cancelled") + func isCancelled() async throws { + await withServerContextRPCCancellationHandle { handle in + #expect(!handle.isCancelled) + handle.cancel() + #expect(handle.isCancelled) + } + } + + @Test("Wait for cancellation") + func waitForCancellation() async throws { + await withServerContextRPCCancellationHandle { handle in + await withTaskGroup(of: Void.self) { group in + group.addTask { + try? await handle.cancelled + } + handle.cancel() + await group.waitForAll() + } + } + } + + @Test("Binds task local") + func bindsTaskLocal() async throws { + await withServerContextRPCCancellationHandle { handle in + let signal = AsyncStream.makeStream(of: Void.self) + + await withRPCCancellationHandler { + handle.cancel() + for await _ in signal.stream {} + } onCancelRPC: { + // If the task local wasn't bound, this wouldn't run. + signal.continuation.finish() + } + } + + } + } +} diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift new file mode 100644 index 000000000..3396b259f --- /dev/null +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -0,0 +1,125 @@ +/* + * 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 GRPCInProcessTransport +import Testing + +@Suite("InProcess transport") +struct InProcessTransportTests { + private static let cancellationModes = ["await-cancelled", "with-cancellation-handler"] + + private func withTestServerAndClient( + execute: (GRPCServer, GRPCClient) async throws -> Void + ) async throws { + try await withThrowingDiscardingTaskGroup { group in + let inProcess = InProcessTransport() + + let server = GRPCServer(transport: inProcess.server, services: [TestService()]) + group.addTask { + try await server.serve() + } + + let client = GRPCClient(transport: inProcess.client) + group.addTask { + try await client.run() + } + + try await execute(server, client) + } + } + + @Test("RPC cancelled by graceful shutdown", arguments: Self.cancellationModes) + func cancelledByGracefulShutdown(mode: String) async throws { + try await self.withTestServerAndClient { server, client in + try await client.serverStreaming( + request: ClientRequest(message: mode), + descriptor: .testCancellation, + serializer: UTF8Serializer(), + deserializer: UTF8Deserializer(), + options: .defaults + ) { response in + // Got initial metadata, begin shutdown to cancel the RPC. + server.beginGracefulShutdown() + + // Now wait for the response. + let messages = try await response.messages.reduce(into: []) { $0.append($1) } + #expect(messages == ["isCancelled=true"]) + } + + // Finally, shutdown the client so its run() method returns. + client.beginGracefulShutdown() + } + } +} + +private struct TestService: RegistrableRPCService { + func cancellation( + request: ServerRequest, + context: ServerContext + ) async throws -> StreamingServerResponse { + switch request.message { + case "await-cancelled": + return StreamingServerResponse { body in + try await context.cancellation.cancelled + try await body.write("isCancelled=\(context.cancellation.isCancelled)") + return [:] + } + + case "with-cancellation-handler": + let signal = AsyncStream.makeStream(of: Void.self) + return StreamingServerResponse { body in + try await withRPCCancellationHandler { + for await _ in signal.stream {} + try await body.write("isCancelled=\(context.cancellation.isCancelled)") + return [:] + } onCancelRPC: { + signal.continuation.finish() + } + } + + default: + throw RPCError(code: .invalidArgument, message: "Invalid argument '\(request.message)'") + } + } + + func registerMethods(with router: inout RPCRouter) { + router.registerHandler( + forMethod: .testCancellation, + deserializer: UTF8Deserializer(), + serializer: UTF8Serializer(), + handler: { + try await self.cancellation(request: ServerRequest(stream: $0), context: $1) + } + ) + } +} + +extension MethodDescriptor { + fileprivate static let testCancellation = Self(service: "test", method: "cancellation") +} + +private struct UTF8Serializer: MessageSerializer { + func serialize(_ message: String) throws -> [UInt8] { + Array(message.utf8) + } +} + +private struct UTF8Deserializer: MessageDeserializer { + func deserialize(_ serializedMessageBytes: [UInt8]) throws -> String { + String(decoding: serializedMessageBytes, as: UTF8.self) + } +} From 7393a28d55e95580e2fd2059e5f82d74c2957ee8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 11 Oct 2024 13:55:22 +0100 Subject: [PATCH 486/580] Fix decoding of durations in MethodConfig (#2093) Motivation: MethodConfig uses a particular string based format for durations based on the "google.protobuf.duration" message. On some decoding paths a string was read and then decoded into a `Swift.Duration` rather than decoding the `GoogleProtobufDuration` message directly. The string-to-Swift.Duration path had a bug meaning fractional seconds were incorrectly decoded. Modifications: - Add a test - Remove the string to `Swift.Duration` path and always decode via `GoogleProtobufDuration` which has a correct implementation. Result: Fewer bugs --- .../GRPCCore/Configuration/MethodConfig.swift | 27 +++++-------------- .../ServerCancellationManagerTests.swift | 1 - .../MethodConfigCodingTests.swift | 1 + 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index 9e1543636..13bafcf0e 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -393,21 +393,6 @@ private func validateMaxAttempts(_ value: Int) throws -> Int { return min(value, 5) } -extension Duration { - fileprivate init(googleProtobufDuration duration: String) throws { - guard duration.utf8.last == UInt8(ascii: "s"), - let fractionalSeconds = Double(duration.dropLast()) - else { - throw RuntimeError(code: .invalidArgument, message: "Invalid google.protobuf.duration") - } - - let seconds = fractionalSeconds.rounded(.down) - let attoseconds = (fractionalSeconds - seconds) / 1e18 - - self.init(secondsComponent: Int64(seconds), attosecondsComponent: Int64(attoseconds)) - } -} - extension MethodConfig: Codable { private enum CodingKeys: String, CodingKey { case name @@ -506,12 +491,12 @@ extension RetryPolicy: Codable { let maxAttempts = try container.decode(Int.self, forKey: .maxAttempts) self.maxAttempts = try validateMaxAttempts(maxAttempts) - let initialBackoff = try container.decode(String.self, forKey: .initialBackoff) - self.initialBackoff = try Duration(googleProtobufDuration: initialBackoff) + let initialBackoff = try container.decode(GoogleProtobufDuration.self, forKey: .initialBackoff) + self.initialBackoff = initialBackoff.duration try Self.validateInitialBackoff(self.initialBackoff) - let maxBackoff = try container.decode(String.self, forKey: .maxBackoff) - self.maxBackoff = try Duration(googleProtobufDuration: maxBackoff) + let maxBackoff = try container.decode(GoogleProtobufDuration.self, forKey: .maxBackoff) + self.maxBackoff = maxBackoff.duration try Self.validateMaxBackoff(self.maxBackoff) self.backoffMultiplier = try container.decode(Double.self, forKey: .backoffMultiplier) @@ -551,8 +536,8 @@ extension HedgingPolicy: Codable { let maxAttempts = try container.decode(Int.self, forKey: .maxAttempts) self.maxAttempts = try validateMaxAttempts(maxAttempts) - let delay = try container.decode(String.self, forKey: .hedgingDelay) - self.hedgingDelay = try Duration(googleProtobufDuration: delay) + let delay = try container.decode(GoogleProtobufDuration.self, forKey: .hedgingDelay) + self.hedgingDelay = delay.duration let statusCodes = try container.decode([GoogleRPCCode].self, forKey: .nonFatalStatusCodes) self.nonFatalStatusCodes = Set(statusCodes.map { $0.code }) diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift index 45c851de8..528ab88c3 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerCancellationManagerTests.swift @@ -65,7 +65,6 @@ struct ServerCancellationManagerTests { @Test("Remove cancellation handler") func removeCancellationHandler() async throws { let manager = ServerCancellationManager() - let signal = AsyncStream.makeStream(of: Void.self) let id = manager.addRPCCancelledHandler { Issue.record("Unexpected cancellation") diff --git a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift index d9797343a..634ef3399 100644 --- a/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift +++ b/Tests/GRPCCoreTests/Configuration/MethodConfigCodingTests.swift @@ -186,6 +186,7 @@ struct MethodConfigCodingTests { ("1s", .seconds(1)), ("1.000000s", .seconds(1)), ("0s", .zero), + ("0.1s", .milliseconds(100)), ("100.123s", .milliseconds(100_123)), ] as [(String, Duration)] ) From 574eea6fc652d2c32f27d577f9e074f2292c0d06 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 15 Oct 2024 11:54:02 +0000 Subject: [PATCH 487/580] Fix route guide tutorial (#2094) We changed some APIs and the tutorial code snippets have to be updated. --- .../Resources/route-guide-sec06-step03-create-client.swift | 2 +- .../Resources/route-guide-sec06-step04-run-client.swift | 2 +- .../Route-Guide/Resources/route-guide-sec06-step05-stub.swift | 2 +- .../Resources/route-guide-sec06-step06-get-feature.swift | 2 +- .../Resources/route-guide-sec06-step07-list-features.swift | 2 +- .../Resources/route-guide-sec06-step08-record-route.swift | 2 +- .../Resources/route-guide-sec06-step09-route-chat.swift | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift index a5f51df0a..445f09ede 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift @@ -5,7 +5,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift index 990676047..c667bf626 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift @@ -5,7 +5,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift index 693b6993c..6d4e9dc7e 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift @@ -5,7 +5,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift index deb719eda..4f7efb31c 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift @@ -5,7 +5,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift index b36d22e88..22caf8eeb 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift @@ -5,7 +5,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift index 8068f6c31..ab68b76c9 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift @@ -5,7 +5,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift index 401bf06ec..fddc42966 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift @@ -5,7 +5,7 @@ extension RouteGuide { let client = try GRPCClient( transport: .http2NIOPosix( target: .ipv4(host: "127.0.0.1", port: 31415), - config: .defaults() + config: .defaults(transportSecurity: .plaintext) ) ) From 54227cbe301abfb273e60057bc45df64e335ec08 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 31 Oct 2024 10:29:01 +0000 Subject: [PATCH 488/580] Update README to include related repos (#2100) Co-authored-by: George Barnett --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0d1d9b5fa..bc2c4d3cf 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ about gRPC on the [gRPC project's website][grpcio]. - 🚀 **Contributions** are welcome, please see [CONTRIBUTING.md](CONTRIBUTING.md) - 🪪 **License** is Apache 2.0, repeated in [LICENSE](License) - 🔒 **Security** issues should be reported via the process in [SECURITY.md](SECURITY.md) +- 🔀 **Related Repositories**: + - [`grpc-swift-nio-transport`][grpc-swift-nio-transport] contains high-performance HTTP/2 client and server transport implementations for gRPC Swift built on top of SwiftNIO. + - [`grpc-swift-protobuf`][grpc-swift-protobuf] contains integrations with SwiftProtobuf for gRPC Swift. + - [`grpc-swift-extras`][grpc-swift-extras] contains optional extras for gRPC Swift. + ## Quick Start @@ -45,3 +50,6 @@ let package = Package( [gh-grpc]: https://github.com/grpc/grpc [grpcio]: https://grpc.io [spi-grpc-swift]: https://swiftpackageindex.com/grpc/grpc-swift/documentation +[grpc-swift-nio-transport]: https://github.com/grpc/grpc-swift-nio-transport +[grpc-swift-protobuf]: https://github.com/grpc/grpc-swift-protobuf +[grpc-swift-extras]: https://github.com/grpc/grpc-swift-extras From 07e38e97567011028cd8d57dd640fc0a6b829483 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 7 Nov 2024 12:07:35 +0000 Subject: [PATCH 489/580] Migrate CI to use swiftlang / SwiftNIO common GitHub Actions. (#2105) ### Motivation: * Reduce duplication * Centralise boilerplate changes when new Swift versions are picked up. * Benefit from centralised work to add new linting / test infrastructure. ### Modifications: Changes of note: * Use soundness checks from swiftlang/github-workflows. * Define a gRPC-specific soundness check which retains bespoke license-checking code for .swift files as the gRPC header style is very different to most templates and checks that generated code is up-to-date. ### Result: More test, linting, formatting coverage. More common CI with other Swift on Server projects. --- .github/release.yml | 14 ++++ .github/workflows/ci.yaml | 82 ------------------- .github/workflows/main.yml | 26 ++++++ .github/workflows/pull_request.yml | 51 ++++++++++++ .github/workflows/pull_request_label.yml | 18 ++++ .github/workflows/soundness.yml | 37 +++++++++ .license_header_template | 13 +++ .licenseignore | 42 ++++++++++ .swiftformatignore | 2 + .unacceptablelanguageignore | 3 + CONTRIBUTING.md | 4 + Examples/hello-world/Protos | 37 --------- Examples/hello-world/Protos/HelloWorld.proto | 1 + IntegrationTests/Benchmarks/Package.swift | 42 +++++----- ...wiftBenchmark.Metadata_Add_binary.p90.json | 0 ...wiftBenchmark.Metadata_Add_string.p90.json | 0 ...hmark.Metadata_Iterate_all_values.p90.json | 0 ...es_when_only_binary_values_stored.p90.json | 0 ...y_values_when_only_strings_stored.p90.json | 0 ...rk.Metadata_Iterate_string_values.p90.json | 0 ...rk.Metadata_Remove_values_for_key.p90.json | 0 ...wiftBenchmark.Metadata_Add_binary.p90.json | 7 ++ ...wiftBenchmark.Metadata_Add_string.p90.json | 7 ++ ...hmark.Metadata_Iterate_all_values.p90.json | 7 ++ ...es_when_only_binary_values_stored.p90.json | 7 ++ ...y_values_when_only_strings_stored.p90.json | 7 ++ ...rk.Metadata_Iterate_string_values.p90.json | 7 ++ ...rk.Metadata_Remove_values_for_key.p90.json | 7 ++ Package.swift | 12 ++- .../GRPCCore/Configuration/MethodConfig.swift | 6 +- .../Configuration/ServiceConfig.swift | 2 +- .../Documentation.docc/Development/Design.md | 2 +- Sources/GRPCCore/Internal/Base64.swift | 4 +- Sources/GRPCCore/Internal/Metadata+GRPC.swift | 2 +- Sources/GRPCCore/Status.swift | 2 +- Sources/GRPCCore/Timeout.swift | 2 +- .../GRPCCore/Transport/RetryThrottle.swift | 2 +- .../Documentation.docc/Documentation.md | 4 +- .../InProcessTransport+Client.swift | 12 +-- .../InProcessTransport+Server.swift | 10 +-- dev/build-examples.sh | 27 +++--- dev/check-generated-code.sh | 27 +++--- dev/format.sh | 30 ++++--- dev/license-check.sh | 35 ++++---- dev/protos/fetch.sh | 28 +++---- dev/protos/generate.sh | 27 +++--- dev/{sanity.sh => soundness.sh} | 32 +++----- 47 files changed, 404 insertions(+), 283 deletions(-) create mode 100644 .github/release.yml delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/pull_request_label.yml create mode 100644 .github/workflows/soundness.yml create mode 100644 .license_header_template create mode 100644 .licenseignore create mode 100644 .swiftformatignore create mode 100644 .unacceptablelanguageignore delete mode 100644 Examples/hello-world/Protos create mode 120000 Examples/hello-world/Protos/HelloWorld.proto rename IntegrationTests/Benchmarks/Thresholds/{main => nightly-6.0}/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{main => nightly-6.0}/GRPCSwiftBenchmark.Metadata_Add_string.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{main => nightly-6.0}/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{main => nightly-6.0}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{main => nightly-6.0}/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{main => nightly-6.0}/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json (100%) rename IntegrationTests/Benchmarks/Thresholds/{main => nightly-6.0}/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json (100%) create mode 100644 IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json create mode 100644 IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename dev/{sanity.sh => soundness.sh} (50%) diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..f96b51492 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,14 @@ +changelog: + categories: + - title: SemVer Major + labels: + - ⚠️ semver/major + - title: SemVer Minor + labels: + - semver/minor + - title: SemVer Patch + labels: + - semver/patch + - title: Other Changes + labels: + - semver/none diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 05a329f30..000000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: CI -on: - push: - branches: [main] - pull_request: - branches: [main] -jobs: - preflight: - name: License Header and Formatting Checks - runs-on: ubuntu-latest - container: - image: swift:6.0-jammy - steps: - - name: "Checkout repository" - uses: actions/checkout@v4 - - name: Mark the workspace as safe - run: git config --global --add safe.directory ${GITHUB_WORKSPACE} - - name: "Install protoc" - run: apt update && apt install -y protobuf-compiler - - name: "Formatting, License Headers, and Generated Code check" - run: | - ./dev/sanity.sh - unit-tests: - strategy: - fail-fast: false - matrix: - include: - - image: swiftlang/swift:nightly-jammy - # No TSAN because of: https://github.com/apple/swift/issues/59068 - # swift-test-flags: "--sanitize=thread" - - image: swift:6.0-jammy - # No TSAN because of: https://github.com/apple/swift/issues/59068 - # swift-test-flags: "--sanitize=thread" - name: Build and Test on ${{ matrix.image }} - runs-on: ubuntu-latest - container: - image: ${{ matrix.image }} - steps: - - uses: actions/checkout@v4 - - name: 🔧 Build - run: swift build ${{ matrix.swift-build-flags }} - timeout-minutes: 20 - - name: 🧪 Test - run: swift test ${{ matrix.swift-test-flags }} - timeout-minutes: 20 - performance-tests: - strategy: - fail-fast: false - matrix: - include: - - image: swiftlang/swift:nightly-jammy - swift-version: 'main' - - image: swift:6.0-jammy - swift-version: '6.0' - name: Performance Tests on ${{ matrix.image }} - runs-on: ubuntu-latest - container: - image: ${{ matrix.image }} - steps: - - uses: actions/checkout@v4 - - name: Install jemalloc for benchmarking - run: apt update && apt-get install -y libjemalloc-dev - timeout-minutes: 20 - - name: Run Benchmarks - working-directory: "./IntegrationTests/Benchmarks" - run: swift package benchmark baseline check --no-progress --check-absolute-path Thresholds/${{ matrix.swift-version }}/ - timeout-minutes: 20 - examples: - strategy: - fail-fast: false - matrix: - include: - - image: swiftlang/swift:nightly-jammy - - image: swift:6.0-jammy - name: Build examples using ${{ matrix.image }} - runs-on: ubuntu-latest - container: - image: ${{ matrix.image }} - steps: - - uses: actions/checkout@v4 - - name: Build examples - run: ./dev/build-examples.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..aea5da49f --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,26 @@ +name: Main + +on: + push: + branches: [main] + schedule: + - cron: "0 8,20 * * *" + +jobs: + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_9_enabled: false + linux_5_10_enabled: false + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + + benchmarks: + name: Benchmarks + uses: apple/swift-nio/.github/workflows/benchmarks.yml@main + with: + benchmark_package_path: "IntegrationTests/Benchmarks" + linux_5_9_enabled: false + linux_5_10_enabled: false diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 000000000..e7ae36d8b --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,51 @@ +name: PR + +on: + pull_request: + branches: [main] + types: [opened, reopened, synchronize] + +jobs: + soundness: + name: Soundness + uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "gRPC" + + grpc-soundness: + name: Soundness + uses: ./.github/workflows/soundness.yml + + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_9_enabled: false + linux_5_10_enabled: false + linux_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable" + + examples: + name: Examples + uses: apple/swift-nio/.github/workflows/swift_matrix.yml@main + with: + name: "Examples" + matrix_linux_5_9_enabled: false + matrix_linux_5_10_enabled: false + matrix_linux_command: "./dev/build-examples.sh" + + benchmarks: + name: Benchmarks + uses: apple/swift-nio/.github/workflows/benchmarks.yml@main + with: + benchmark_package_path: "IntegrationTests/Benchmarks" + linux_5_9_enabled: false + linux_5_10_enabled: false + + cxx-interop: + name: Cxx interop + uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main + with: + linux_5_9_enabled: false + linux_5_10_enabled: false diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml new file mode 100644 index 000000000..d83c59999 --- /dev/null +++ b/.github/workflows/pull_request_label.yml @@ -0,0 +1,18 @@ +name: PR + +on: + pull_request: + types: [labeled, unlabeled, opened, reopened, synchronize] + +jobs: + semver-label-check: + name: Semantic version label check + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Check for Semantic Version label + uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main diff --git a/.github/workflows/soundness.yml b/.github/workflows/soundness.yml new file mode 100644 index 000000000..35a50d70a --- /dev/null +++ b/.github/workflows/soundness.yml @@ -0,0 +1,37 @@ +name: Soundness + +on: + workflow_call: + +jobs: + swift-license-check: + name: Swift license headers check + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Mark the workspace as safe + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: Run license check + run: | + ./dev/license-check.sh + + check-generated-code: + name: Check generated code + runs-on: ubuntu-latest + container: + image: swift:latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Mark the workspace as safe + run: git config --global --add safe.directory ${GITHUB_WORKSPACE} + - name: Install protoc + run: apt update && apt install -y protobuf-compiler + - name: Run soundness checks + run: | + ./dev/check-generated-code.sh diff --git a/.license_header_template b/.license_header_template new file mode 100644 index 000000000..a07a9ad01 --- /dev/null +++ b/.license_header_template @@ -0,0 +1,13 @@ +@@ Copyright YEARS, 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. diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 000000000..314cb7efe --- /dev/null +++ b/.licenseignore @@ -0,0 +1,42 @@ +.gitignore +**/.gitignore +.licenseignore +.gitattributes +.git-blame-ignore-revs +.gitmodules +.mailfilter +.mailmap +.spi.yml +.swift-format +.editorconfig +.github/* +*.md +*.txt +*.yml +*.yaml +*.json +Package.swift +**/Package.swift +Package@-*.swift +**/Package@-*.swift +Package.resolved +**/Package.resolved +Makefile +*.modulemap +**/*.modulemap +**/*.docc/* +*.xcprivacy +**/*.xcprivacy +*.symlink +**/*.symlink +Dockerfile +**/Dockerfile +Snippets/* +dev/git.commit.template +dev/version-bump.commit.template +.unacceptablelanguageignore +.swiftformatignore +LICENSE +**/*.swift +dev/protos/**/*.proto +Examples/hello-world/Protos/HelloWorld.proto diff --git a/.swiftformatignore b/.swiftformatignore new file mode 100644 index 000000000..c73cb4c26 --- /dev/null +++ b/.swiftformatignore @@ -0,0 +1,2 @@ +*.grpc.swift +*.pb.swift diff --git a/.unacceptablelanguageignore b/.unacceptablelanguageignore new file mode 100644 index 000000000..fe70e9db9 --- /dev/null +++ b/.unacceptablelanguageignore @@ -0,0 +1,3 @@ +**/*.pb.swift +**/*.grpc.swift +dev/protos/upstream/**/*.proto diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe4675837..3d9d354a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,3 +17,7 @@ In order to protect both you and ourselves, you will need to sign the Please see the [main gRPC repository](https://github.com/grpc/grpc) for more information about gRPC. + +### Run CI checks locally + +You can run the GitHub Actions workflows locally using [act](https://github.com/nektos/act) or in some cases calling scripts directly. For detailed steps on how to do this please see [https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally](https://github.com/swiftlang/github-workflows?tab=readme-ov-file#running-workflows-locally). diff --git a/Examples/hello-world/Protos b/Examples/hello-world/Protos deleted file mode 100644 index 2be480c2f..000000000 --- a/Examples/hello-world/Protos +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2015 gRPC authors. -// -// 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. -syntax = "proto3"; - -option java_multiple_files = true; -option java_package = "io.grpc.examples.helloworld"; -option java_outer_classname = "HelloWorldProto"; -option objc_class_prefix = "HLW"; - -package helloworld; - -// The greeting service definition. -service Greeter { - // Sends a greeting - rpc SayHello (HelloRequest) returns (HelloReply) {} -} - -// The request message containing the user's name. -message HelloRequest { - string name = 1; -} - -// The response message containing the greetings -message HelloReply { - string message = 1; -} diff --git a/Examples/hello-world/Protos/HelloWorld.proto b/Examples/hello-world/Protos/HelloWorld.proto new file mode 120000 index 000000000..b5e1142c1 --- /dev/null +++ b/Examples/hello-world/Protos/HelloWorld.proto @@ -0,0 +1 @@ +../../..//dev/protos/upstream/grpc/examples/helloworld.proto \ No newline at end of file diff --git a/IntegrationTests/Benchmarks/Package.swift b/IntegrationTests/Benchmarks/Package.swift index 6a818aab9..3fe8b8aed 100644 --- a/IntegrationTests/Benchmarks/Package.swift +++ b/IntegrationTests/Benchmarks/Package.swift @@ -17,25 +17,25 @@ import PackageDescription let package = Package( - name: "benchmarks", - platforms: [ - .macOS(.v13), - ], - dependencies: [ - .package(path: "../../"), - .package(url: "https://github.com/ordo-one/package-benchmark", from: "1.11.2") - ], - targets: [ - .executableTarget( - name: "GRPCSwiftBenchmark", - dependencies: [ - .product(name: "Benchmark", package: "package-benchmark"), - .product(name: "GRPCCore", package: "grpc-swift") - ], - path: "Benchmarks/GRPCSwiftBenchmark", - plugins: [ - .plugin(name: "BenchmarkPlugin", package: "package-benchmark") - ] - ), - ] + name: "benchmarks", + platforms: [ + .macOS(.v13) + ], + dependencies: [ + .package(path: "../../"), + .package(url: "https://github.com/ordo-one/package-benchmark", from: "1.11.2"), + ], + targets: [ + .executableTarget( + name: "GRPCSwiftBenchmark", + dependencies: [ + .product(name: "Benchmark", package: "package-benchmark"), + .product(name: "GRPCCore", package: "grpc-swift"), + ], + path: "Benchmarks/GRPCSwiftBenchmark", + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "package-benchmark") + ] + ) + ] ) diff --git a/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Add_string.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json similarity index 100% rename from IntegrationTests/Benchmarks/Thresholds/main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json rename to IntegrationTests/Benchmarks/Thresholds/nightly-6.0/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json new file mode 100644 index 000000000..b642696c1 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_binary.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 3012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json new file mode 100644 index 000000000..7fde30a69 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Add_string.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 11, + "memoryLeaked" : 0, + "releaseCount" : 4012, + "retainCount" : 2000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_all_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_binary_values_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json new file mode 100644 index 000000000..b4aba1c3f --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_binary_values_when_only_strings_stored.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 2000, + "memoryLeaked" : 0, + "releaseCount" : 7001, + "retainCount" : 3000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json new file mode 100644 index 000000000..1b1303873 --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Iterate_string_values.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 3001, + "retainCount" : 1000, + "syscalls" : 0 +} diff --git a/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json new file mode 100644 index 000000000..5750750bc --- /dev/null +++ b/IntegrationTests/Benchmarks/Thresholds/nightly-main/GRPCSwiftBenchmark.Metadata_Remove_values_for_key.p90.json @@ -0,0 +1,7 @@ +{ + "mallocCountTotal" : 0, + "memoryLeaked" : 0, + "releaseCount" : 2002001, + "retainCount" : 1999000, + "syscalls" : 0 +} diff --git a/Package.swift b/Package.swift index c1aa65bfc..86cbe9c07 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,6 @@ * limitations under the License. */ - import PackageDescription let products: [Product] = [ @@ -49,7 +48,7 @@ let dependencies: [Package.Dependency] = [ let defaultSwiftSettings: [SwiftSetting] = [ .swiftLanguageMode(.v6), .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") + .enableUpcomingFeature("InternalImportsByDefault"), ] let targets: [Target] = [ @@ -57,7 +56,7 @@ let targets: [Target] = [ .target( name: "GRPCCore", dependencies: [ - .product(name: "DequeModule", package: "swift-collections"), + .product(name: "DequeModule", package: "swift-collections") ], swiftSettings: defaultSwiftSettings ), @@ -66,7 +65,7 @@ let targets: [Target] = [ dependencies: [ .target(name: "GRPCCore"), .target(name: "GRPCInProcessTransport"), - .product(name: "SwiftProtobuf", package: "swift-protobuf") + .product(name: "SwiftProtobuf", package: "swift-protobuf"), ], resources: [ .copy("Configuration/Inputs") @@ -91,8 +90,7 @@ let targets: [Target] = [ // Code generator library for protoc-gen-grpc-swift .target( name: "GRPCCodeGen", - dependencies: [ - ], + dependencies: [], swiftSettings: defaultSwiftSettings ), .testTarget( @@ -100,7 +98,7 @@ let targets: [Target] = [ dependencies: [ .target(name: "GRPCCodeGen") ] - ) + ), ] let package = Package( diff --git a/Sources/GRPCCore/Configuration/MethodConfig.swift b/Sources/GRPCCore/Configuration/MethodConfig.swift index 13bafcf0e..23a867926 100644 --- a/Sources/GRPCCore/Configuration/MethodConfig.swift +++ b/Sources/GRPCCore/Configuration/MethodConfig.swift @@ -16,7 +16,7 @@ /// Configuration values for executing an RPC. /// -/// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto +/// See also: https://github.com/grpc/grpc-proto/blob/0b30c8c05277ab78ec72e77c9cbf66a26684673d/grpc/service_config/service_config.proto public struct MethodConfig: Hashable, Sendable { public struct Name: Sendable, Hashable { /// The name of the service, including the namespace. @@ -211,7 +211,7 @@ public struct RPCExecutionPolicy: Hashable, Sendable { /// and `min(initialBackoff * backoffMultiplier^(n-1), maxBackoff)`. /// /// For more information see [gRFC A6 Client -/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +/// Retries](https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A6-client-retries.md). public struct RetryPolicy: Hashable, Sendable { /// The maximum number of RPC attempts, including the original attempt. /// @@ -328,7 +328,7 @@ public struct RetryPolicy: Hashable, Sendable { /// by ``hedgingDelay``. /// /// For more information see [gRFC A6 Client -/// Retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +/// Retries](https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A6-client-retries.md). public struct HedgingPolicy: Hashable, Sendable { /// The maximum number of RPC attempts, including the original attempt. /// diff --git a/Sources/GRPCCore/Configuration/ServiceConfig.swift b/Sources/GRPCCore/Configuration/ServiceConfig.swift index a13bf08dc..316ad8b61 100644 --- a/Sources/GRPCCore/Configuration/ServiceConfig.swift +++ b/Sources/GRPCCore/Configuration/ServiceConfig.swift @@ -16,7 +16,7 @@ /// Service configuration values. /// -/// See also: https://github.com/grpc/grpc-proto/blob/master/grpc/service_config/service_config.proto +/// See also: https://github.com/grpc/grpc-proto/blob/0b30c8c05277ab78ec72e77c9cbf66a26684673d/grpc/service_config/service_config.proto public struct ServiceConfig: Hashable, Sendable { /// Per-method configuration. public var methodConfig: [MethodConfig] diff --git a/Sources/GRPCCore/Documentation.docc/Development/Design.md b/Sources/GRPCCore/Documentation.docc/Development/Design.md index 341580539..fe664a38d 100644 --- a/Sources/GRPCCore/Documentation.docc/Development/Design.md +++ b/Sources/GRPCCore/Documentation.docc/Development/Design.md @@ -76,7 +76,7 @@ doing this doesn't leave the other side waiting indefinitely. gRPC has mechanisms to deliver method-specific configuration at the transport layer which can also change dynamically (see [gRFC A2: ServiceConfig in -DNS](https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md).) +DNS](https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A2-service-configs-in-dns.md).) This configuration is used to determine how clients should interact with servers and how methods should be executed, such as the conditions under which they may be retried. Some of this is exposed via the ``ClientTransport`` as diff --git a/Sources/GRPCCore/Internal/Base64.swift b/Sources/GRPCCore/Internal/Base64.swift index f2078331f..24501f3ab 100644 --- a/Sources/GRPCCore/Internal/Base64.swift +++ b/Sources/GRPCCore/Internal/Base64.swift @@ -15,7 +15,7 @@ */ // This base64 implementation is heavily inspired by: -// https://github.com/lemire/fastbase64/blob/master/src/chromiumbase64.c +// https://github.com/lemire/fastbase64/blob/a2e0967dfcb8f0129ea45b9b24cc410e4cac117f/src/chromiumbase64.c /* Copyright (c) 2015-2016, Wojciech Muła, Alfred Klomp, Daniel Lemire (Unless otherwise stated in the source code) @@ -45,7 +45,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -// https://github.com/client9/stringencoders/blob/master/src/modp_b64.c +// https://github.com/client9/stringencoders/blob/e1448a9415f4ebf6f559c86718193ba067cbb99d/src/modp_b64.c /* The MIT License (MIT) diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index 8920140f6..bb1c74a0d 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -91,7 +91,7 @@ extension Metadata { self = .retryAfter(Duration(secondsComponent: seconds, attosecondsComponent: attoseconds)) } else { // Negative or not parseable means stop trying. - // Source: https://github.com/grpc/proposal/blob/master/A6-client-retries.md + // Source: https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A6-client-retries.md self = .stopRetrying } } diff --git a/Sources/GRPCCore/Status.swift b/Sources/GRPCCore/Status.swift index ed6636b7f..7a78c12e1 100644 --- a/Sources/GRPCCore/Status.swift +++ b/Sources/GRPCCore/Status.swift @@ -111,7 +111,7 @@ extension Status { /// /// The outcome of every RPC is indicated by a status code. public struct Code: Hashable, CustomStringConvertible, Sendable { - // Source: https://github.com/grpc/grpc/blob/master/doc/statuscodes.md + // Source: https://github.com/grpc/grpc/blob/6c0578099835c854b0ff36a4b8db98ed49278ed5/doc/statuscodes.md enum Wrapped: UInt8, Hashable, Sendable { case ok = 0 case cancelled = 1 diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index 68dfcd145..68fa49779 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -46,7 +46,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { } /// The wire encoding of this timeout as described in the gRPC protocol. - /// See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests + /// See "Timeout" in https://github.com/grpc/grpc/blob/6c0578099835c854b0ff36a4b8db98ed49278ed5/doc/PROTOCOL-HTTP2.md#requests var wireEncoding: String { "\(amount)\(unit.rawValue)" } diff --git a/Sources/GRPCCore/Transport/RetryThrottle.swift b/Sources/GRPCCore/Transport/RetryThrottle.swift index e3f3ead4f..6b52739d9 100644 --- a/Sources/GRPCCore/Transport/RetryThrottle.swift +++ b/Sources/GRPCCore/Transport/RetryThrottle.swift @@ -29,7 +29,7 @@ private import Synchronization /// ``HedgingPolicy/nonFatalStatusCodes``) or when the client receives a pushback response from /// the server. /// -/// See also [gRFC A6: client retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md). +/// See also [gRFC A6: client retries](https://github.com/grpc/proposal/blob/0e1807a6e30a1a915c0dcadc873bca92b9fa9720/A6-client-retries.md). public final class RetryThrottle: Sendable { // Note: only three figures after the decimal point from the original token ratio are used so // all computation is done a scaled number of tokens (tokens * 1000). This allows us to do all diff --git a/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md b/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md index c57b262b9..19d0ab978 100644 --- a/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md +++ b/Sources/GRPCInProcessTransport/Documentation.docc/Documentation.md @@ -13,5 +13,5 @@ use cases. ### Transports - ``InProcessTransport`` -- ``InProcessClientTransport`` -- ``InProcessServerTransport`` +- ``InProcessTransport/Client`` +- ``InProcessTransport/Server`` diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift index 84c7f85a7..11828108c 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Client.swift @@ -18,13 +18,13 @@ public import GRPCCore private import Synchronization extension InProcessTransport { - /// An in-process implementation of a ``ClientTransport``. + /// An in-process implementation of a `ClientTransport`. /// /// This is useful when you're interested in testing your application without any actual networking layers /// involved, as the client and server will communicate directly with each other via in-process streams. /// - /// To use this client, you'll have to provide a ``ServerTransport`` upon creation, as well - /// as a ``ServiceConfig``. + /// To use this client, you'll have to provide a `ServerTransport` upon creation, as well + /// as a `ServiceConfig`. /// /// Once you have a client, you must keep a long-running task executing ``connect()``, which /// will return only once all streams have been finished and ``beginGracefulShutdown()`` has been called on this client; or @@ -34,7 +34,7 @@ extension InProcessTransport { /// called before ``connect()`` is called, then any streams will remain pending and the call will /// block until ``connect()`` is called or the task is cancelled. /// - /// - SeeAlso: ``ClientTransport`` + /// - SeeAlso: `ClientTransport` public final class Client: ClientTransport { private enum State: Sendable { struct UnconnectedState { @@ -187,7 +187,7 @@ extension InProcessTransport { /// Signal to the transport that no new streams may be created. /// /// Existing streams may run to completion naturally but calling ``withStream(descriptor:options:_:)`` - /// will result in an ``RPCError`` with code ``RPCError/Code/failedPrecondition`` being thrown. + /// will result in an `RPCError` with code `RPCError/Code/failedPrecondition` being thrown. /// /// If you want to forcefully cancel all active streams then cancel the task running ``connect()``. public func beginGracefulShutdown() { @@ -215,7 +215,7 @@ extension InProcessTransport { /// /// - Important: The opened stream is closed after the closure is finished. /// - /// This transport implementation throws ``RPCError/Code/failedPrecondition`` if the transport + /// This transport implementation throws `RPCError/Code/failedPrecondition` if the transport /// is closing or has been closed. /// /// This implementation will queue any streams (and thus block this call) if this function is called before diff --git a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift index 02b132ac8..953d2ffdb 100644 --- a/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift +++ b/Sources/GRPCInProcessTransport/InProcessTransport+Server.swift @@ -18,16 +18,16 @@ public import GRPCCore private import Synchronization extension InProcessTransport { - /// An in-process implementation of a ``ServerTransport``. + /// An in-process implementation of a `ServerTransport`. /// /// This is useful when you're interested in testing your application without any actual networking layers /// involved, as the client and server will communicate directly with each other via in-process streams. /// /// To use this server, you call ``listen(streamHandler:)`` and iterate over the returned `AsyncSequence` to get all - /// RPC requests made from clients (as ``RPCStream``s). + /// RPC requests made from clients (as `RPCStream`s). /// To stop listening to new requests, call ``beginGracefulShutdown()``. /// - /// - SeeAlso: ``ClientTransport`` + /// - SeeAlso: `ClientTransport` public final class Server: ServerTransport, Sendable { public typealias Inbound = RPCAsyncSequence public typealias Outbound = RPCWriter.Closable @@ -123,9 +123,9 @@ extension InProcessTransport { } } - /// Stop listening to any new ``RPCStream`` publications. + /// Stop listening to any new `RPCStream` publications. /// - /// - SeeAlso: ``ServerTransport`` + /// - SeeAlso: `ServerTransport` public func beginGracefulShutdown() { self.newStreamsContinuation.finish() for handle in self.handles.withLock({ $0.beginShutdown() }) { diff --git a/dev/build-examples.sh b/dev/build-examples.sh index e44a8f5c6..4144dab2c 100755 --- a/dev/build-examples.sh +++ b/dev/build-examples.sh @@ -1,18 +1,17 @@ #!/bin/bash - -# 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. +## 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. set -euo pipefail diff --git a/dev/check-generated-code.sh b/dev/check-generated-code.sh index 67bc71b8e..3094c38b4 100755 --- a/dev/check-generated-code.sh +++ b/dev/check-generated-code.sh @@ -1,18 +1,17 @@ #!/bin/bash - -# 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. +## 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. set -euo pipefail diff --git a/dev/format.sh b/dev/format.sh index f1e75bc9d..7e4e46519 100755 --- a/dev/format.sh +++ b/dev/format.sh @@ -1,18 +1,17 @@ #!/bin/bash - -# Copyright 2020, 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. +## Copyright 2020, 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. set -eu @@ -70,8 +69,7 @@ if "$lint"; then To fix, run the following command: % $here/format.sh -f - " - exit "${SWIFT_FORMAT_RC}" + " "${SWIFT_FORMAT_RC}" fi log "Ran swift format lint with no errors." diff --git a/dev/license-check.sh b/dev/license-check.sh index d540293aa..889478b31 100755 --- a/dev/license-check.sh +++ b/dev/license-check.sh @@ -1,18 +1,17 @@ #!/bin/bash - -# 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. +## 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. # This script checks the copyright headers in source *.swift source files and # exits if they do not match the expected header. The year, or year range in @@ -38,10 +37,6 @@ read -r -d '' COPYRIGHT_HEADER_SWIFT << 'EOF' EOF SWIFT_SHA=$(echo "$COPYRIGHT_HEADER_SWIFT" | shasum | awk '{print $1}') -replace_years() { - sed -e 's/201[56789]-20[12][0-9]/YEARS/' -e 's/201[56789]/YEARS/' -} - # Checks the Copyright headers for *.swift files in this repository against the # expected headers. # @@ -74,12 +69,12 @@ check_copyright_headers() { drop_first=1 expected_lines=15 ;; - */Package@swift-*.swift) + */Package@swift-*.*.swift) expected_sha="$SWIFT_SHA" drop_first=1 expected_lines=15 ;; - */Package@swift-*.*.swift) + */Package@swift-*.swift) expected_sha="$SWIFT_SHA" drop_first=1 expected_lines=15 diff --git a/dev/protos/fetch.sh b/dev/protos/fetch.sh index 01bf4d780..2032a2408 100755 --- a/dev/protos/fetch.sh +++ b/dev/protos/fetch.sh @@ -1,24 +1,22 @@ #!/bin/bash -# -# 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. +## 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. set -eu here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" upstream="$here/upstream" -root="$here/../.." # Create a temporary directory for the repo checkouts. checkouts="$(mktemp -d)" diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh index 666e87c49..5efb7018c 100755 --- a/dev/protos/generate.sh +++ b/dev/protos/generate.sh @@ -1,18 +1,17 @@ #!/bin/bash -# -# 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. +## 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. set -eu diff --git a/dev/sanity.sh b/dev/soundness.sh similarity index 50% rename from dev/sanity.sh rename to dev/soundness.sh index de039b514..8c9f16e07 100755 --- a/dev/sanity.sh +++ b/dev/soundness.sh @@ -1,18 +1,17 @@ #!/bin/bash - -# Copyright 2020, 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. +## Copyright 2020, 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. set -eu @@ -41,16 +40,11 @@ function check_license_headers() { run_logged "Checking license headers" "$here/license-check.sh" } -function check_formatting() { - run_logged "Checking formatting" "$here/format.sh -l" -} - function check_generated_code_is_up_to_date() { run_logged "Checking generated code is up-to-date" "$here/check-generated-code.sh" } errors=0 check_license_headers -check_formatting check_generated_code_is_up_to_date exit $errors From 61cc2d1f421f628d585d0c9f2981800eefc46404 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Nov 2024 11:27:19 +0000 Subject: [PATCH 490/580] Avoid using vendored lock in BroadcastAsyncSequence (#2108) Motivation: ManagedBuffer is explicitly not 'Sendable' on Swift nightly builds. As a result, our Lock type stops being 'Sendable'. This is only used by the 'BroadcastAsyncSequence'. Modifications: - Modify 'BroadcastAsyncSequence' to use 'Mutex'. One downside to this approach is that when a subscribe wants to consume an element and there's no element available, the lock must be released and then re-acquired once a continuation has been created. - Remove our lock, and update NOTICES.txt to reflect the fact we no longer use a copy of NIOs lock. Result: - Less code - CI passes --- NOTICES.txt | 14 - .../Concurrency Primitives/Lock.swift | 291 ------------------ .../Internal/BroadcastAsyncSequence.swift | 68 ++-- 3 files changed, 40 insertions(+), 333 deletions(-) delete mode 100644 Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift diff --git a/NOTICES.txt b/NOTICES.txt index 11221c3e0..61ce63d4e 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -17,20 +17,6 @@ under the License. ------------------------------------------------------------------------------- -This product uses scripts derived from SwiftNIO's integration testing -framework: 'test_01_allocation_counts.sh', 'run-nio-alloc-counter-tests.sh' and -'test_functions.sh'. - -It also uses derivations of SwiftNIO's lock 'NIOLock.swift' and locked value box -'NIOLockedValueBox.swift'. - - * LICENSE (Apache License 2.0): - * https://github.com/apple/swift-nio/blob/main/LICENSE.txt - * HOMEPAGE: - * https://github.com/apple/swift-nio - ---- - This product uses derivations of SwiftNIOHTTP2's implementation of case insensitive comparison of strings, found in 'HPACKHeader.swift'. diff --git a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift b/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift deleted file mode 100644 index 2867e9982..000000000 --- a/Sources/GRPCCore/Internal/Concurrency Primitives/Lock.swift +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2023, 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. - */ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftNIO open source project -// -// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftNIO project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if canImport(Darwin) -public import Darwin // should be @usableFromInline -#elseif canImport(Glibc) -public import Glibc // should be @usableFromInline -#endif - -@usableFromInline -typealias LockPrimitive = pthread_mutex_t - -@usableFromInline -enum LockOperations {} - -extension LockOperations { - @inlinable - static func create(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - - let err = pthread_mutex_init(mutex, &attr) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - } - - @inlinable - static func destroy(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - let err = pthread_mutex_destroy(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - } - - @inlinable - static func lock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - let err = pthread_mutex_lock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - } - - @inlinable - static func unlock(_ mutex: UnsafeMutablePointer) { - mutex.assertValidAlignment() - - let err = pthread_mutex_unlock(mutex) - precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)") - } -} - -// Tail allocate both the mutex and a generic value using ManagedBuffer. -// Both the header pointer and the elements pointer are stable for -// the class's entire lifetime. -// -// However, for safety reasons, we elect to place the lock in the "elements" -// section of the buffer instead of the head. The reasoning here is subtle, -// so buckle in. -// -// _As a practical matter_, the implementation of ManagedBuffer ensures that -// the pointer to the header is stable across the lifetime of the class, and so -// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader` -// the value of the header pointer will be the same. This is because ManagedBuffer uses -// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure -// that it does not invoke any weird Swift accessors that might copy the value. -// -// _However_, the header is also available via the `.header` field on the ManagedBuffer. -// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends -// do not interact with Swift's exclusivity model. That is, the various `with` functions do not -// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because -// there's literally no other way to perform the access, but for `.header` it's entirely possible -// to accidentally recursively read it. -// -// Our implementation is free from these issues, so we don't _really_ need to worry about it. -// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive -// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry, -// and future maintainers will be happier that we were cautious. -// -// See also: https://github.com/apple/swift/pull/40000 -@usableFromInline -final class LockStorage: ManagedBuffer { - - @inlinable - static func create(value: Value) -> Self { - let buffer = Self.create(minimumCapacity: 1) { _ in - return value - } - let storage = unsafeDowncast(buffer, to: Self.self) - - storage.withUnsafeMutablePointers { _, lockPtr in - LockOperations.create(lockPtr) - } - - return storage - } - - @inlinable - func lock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.lock(lockPtr) - } - } - - @inlinable - func unlock() { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.unlock(lockPtr) - } - } - - @inlinable - deinit { - self.withUnsafeMutablePointerToElements { lockPtr in - LockOperations.destroy(lockPtr) - } - } - - @inlinable - func withLockPrimitive( - _ body: (UnsafeMutablePointer) throws -> T - ) rethrows -> T { - try self.withUnsafeMutablePointerToElements { lockPtr in - return try body(lockPtr) - } - } - - @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - try self.withUnsafeMutablePointers { valuePtr, lockPtr in - LockOperations.lock(lockPtr) - defer { LockOperations.unlock(lockPtr) } - return try mutate(&valuePtr.pointee) - } - } -} - -extension LockStorage: @unchecked Sendable {} - -/// A threading lock based on `libpthread` instead of `libdispatch`. -/// -/// - note: ``Lock`` has reference semantics. -/// -/// This object provides a lock on top of a single `pthread_mutex_t`. This kind -/// of lock is safe to use with `libpthread`-based threading models, such as the -/// one used by NIO. On Windows, the lock is based on the substantially similar -/// `SRWLOCK` type. -@usableFromInline -struct Lock { - @usableFromInline - internal let _storage: LockStorage - - /// Create a new lock. - @inlinable - init() { - self._storage = .create(value: ()) - } - - /// Acquire the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `unlock`, to simplify lock handling. - @inlinable - func lock() { - self._storage.lock() - } - - /// Release the lock. - /// - /// Whenever possible, consider using `withLock` instead of this method and - /// `lock`, to simplify lock handling. - @inlinable - func unlock() { - self._storage.unlock() - } - - @inlinable - internal func withLockPrimitive( - _ body: (UnsafeMutablePointer) throws -> T - ) rethrows -> T { - return try self._storage.withLockPrimitive(body) - } -} - -extension Lock { - /// Acquire the lock for the duration of the given block. - /// - /// This convenience method should be preferred to `lock` and `unlock` in - /// most situations, as it ensures that the lock will be released regardless - /// of how `body` exits. - /// - /// - Parameter body: The block to execute while holding the lock. - /// - Returns: The value returned by the block. - @inlinable - func withLock(_ body: () throws -> T) rethrows -> T { - self.lock() - defer { - self.unlock() - } - return try body() - } -} - -extension Lock: Sendable {} - -extension UnsafeMutablePointer { - @inlinable - func assertValidAlignment() { - assert(UInt(bitPattern: self) % UInt(MemoryLayout.alignment) == 0) - } -} - -@usableFromInline -struct LockedValueBox { - @usableFromInline - let storage: LockStorage - - @inlinable - init(_ value: Value) { - self.storage = .create(value: value) - } - - @inlinable - func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { - return try self.storage.withLockedValue(mutate) - } - - /// An unsafe view over the locked value box. - /// - /// Prefer ``withLockedValue(_:)`` where possible. - @usableFromInline - var unsafe: Unsafe { - Unsafe(storage: self.storage) - } - - @usableFromInline - struct Unsafe { - @usableFromInline - let storage: LockStorage - - /// Manually acquire the lock. - @inlinable - func lock() { - self.storage.lock() - } - - /// Manually release the lock. - @inlinable - func unlock() { - self.storage.unlock() - } - - /// Mutate the value, assuming the lock has been acquired manually. - @inlinable - func withValueAssumingLockIsAcquired( - _ mutate: (inout Value) throws -> T - ) rethrows -> T { - return try self.storage.withUnsafeMutablePointerToHeader { value in - try mutate(&value.pointee) - } - } - } -} - -extension LockedValueBox: Sendable where Value: Sendable {} diff --git a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift index ac7e6c084..4e4860508 100644 --- a/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift +++ b/Sources/GRPCCore/Streaming/Internal/BroadcastAsyncSequence.swift @@ -15,6 +15,7 @@ */ public import DequeModule // should be @usableFromInline +public import Synchronization // should be @usableFromInline /// An `AsyncSequence` which can broadcast its values to multiple consumers concurrently. /// @@ -156,15 +157,15 @@ enum BroadcastAsyncSequenceError: Error { @usableFromInline final class _BroadcastSequenceStorage: Sendable { @usableFromInline - let _state: LockedValueBox<_BroadcastSequenceStateMachine> + let _state: Mutex<_BroadcastSequenceStateMachine> @inlinable init(bufferSize: Int) { - self._state = LockedValueBox(_BroadcastSequenceStateMachine(bufferSize: bufferSize)) + self._state = Mutex(_BroadcastSequenceStateMachine(bufferSize: bufferSize)) } deinit { - let onDrop = self._state.withLockedValue { state in + let onDrop = self._state.withLock { state in state.dropResources() } @@ -184,7 +185,7 @@ final class _BroadcastSequenceStorage: Sendable { /// - Parameter element: The element to write. @inlinable func yield(_ element: Element) async throws { - let onYield = self._state.withLockedValue { state in state.yield(element) } + let onYield = self._state.withLock { state in state.yield(element) } switch onYield { case .none: @@ -196,7 +197,7 @@ final class _BroadcastSequenceStorage: Sendable { case .suspend(let token): try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in - let onProduceMore = self._state.withLockedValue { state in + let onProduceMore = self._state.withLock { state in state.waitToProduceMore(continuation: continuation, token: token) } @@ -208,7 +209,7 @@ final class _BroadcastSequenceStorage: Sendable { } } } onCancel: { - let onCancel = self._state.withLockedValue { state in + let onCancel = self._state.withLock { state in state.cancelProducer(withToken: token) } @@ -230,7 +231,7 @@ final class _BroadcastSequenceStorage: Sendable { /// - Parameter result: Whether the stream is finishing cleanly or because of an error. @inlinable func finish(_ result: Result) { - let action = self._state.withLockedValue { state in state.finish(result: result) } + let action = self._state.withLock { state in state.finish(result: result) } switch action { case .none: () @@ -247,7 +248,7 @@ final class _BroadcastSequenceStorage: Sendable { /// - Returns: Returns a unique subscription ID. @inlinable func subscribe() -> _BroadcastSequenceStateMachine.Subscriptions.ID { - self._state.withLockedValue { $0.subscribe() } + self._state.withLock { $0.subscribe() } } /// Returns the next element for the given subscriber, if it is available. @@ -259,35 +260,32 @@ final class _BroadcastSequenceStorage: Sendable { forSubscriber id: _BroadcastSequenceStateMachine.Subscriptions.ID ) async throws -> Element? { return try await withTaskCancellationHandler { - self._state.unsafe.lock() - let onNext = self._state.unsafe.withValueAssumingLockIsAcquired { + let onNext = self._state.withLock { $0.nextElement(forSubscriber: id) } switch onNext { case .return(let returnAndProduceMore): - self._state.unsafe.unlock() returnAndProduceMore.producers.resume() return try returnAndProduceMore.nextResult.get() case .suspend: return try await withCheckedThrowingContinuation { continuation in - let onSetContinuation = self._state.unsafe.withValueAssumingLockIsAcquired { state in + let onSetContinuation = self._state.withLock { state in state.setContinuation(continuation, forSubscription: id) } - self._state.unsafe.unlock() - switch onSetContinuation { - case .resume(let continuation, let result): + case .resume(let continuation, let result, let producerContinuations): continuation.resume(with: result) + producerContinuations?.resume() case .none: () } } } } onCancel: { - let onCancel = self._state.withLockedValue { state in + let onCancel = self._state.withLock { state in state.cancelSubscription(withID: id) } @@ -304,7 +302,7 @@ final class _BroadcastSequenceStorage: Sendable { /// elements. @inlinable var isKnownSafeForNextSubscriber: Bool { - self._state.withLockedValue { state in + self._state.withLock { state in state.nextSubscriptionIsValid } } @@ -312,7 +310,7 @@ final class _BroadcastSequenceStorage: Sendable { /// Invalidates all active subscriptions. @inlinable func invalidateAllSubscriptions() { - let action = self._state.withLockedValue { state in + let action = self._state.withLock { state in state.invalidateAllSubscriptions() } @@ -467,10 +465,17 @@ struct _BroadcastSequenceStateMachine: Sendable { _ continuation: ConsumerContinuation, forSubscription id: _BroadcastSequenceStateMachine.Subscriptions.ID ) -> OnSetContinuation { - if self.subscriptions.setContinuation(continuation, forSubscriber: id) { - return .none - } else { - return .resume(continuation, .failure(CancellationError())) + // 'next(id)' must be checked first: an element might've been provided between the lock + // being dropped and a continuation being created and the lock being acquired again. + switch self.next(id) { + case .return(let resultAndProducers): + return .resume(continuation, resultAndProducers.nextResult, resultAndProducers.producers) + case .suspend: + if self.subscriptions.setContinuation(continuation, forSubscriber: id) { + return .none + } else { + return .resume(continuation, .failure(CancellationError()), nil) + } } } @@ -697,10 +702,17 @@ struct _BroadcastSequenceStateMachine: Sendable { _ continuation: ConsumerContinuation, forSubscription id: _BroadcastSequenceStateMachine.Subscriptions.ID ) -> OnSetContinuation { - if self.subscriptions.setContinuation(continuation, forSubscriber: id) { - return .none - } else { - return .resume(continuation, .failure(CancellationError())) + // 'next(id)' must be checked first: an element might've been provided between the lock + // being dropped and a continuation being created and the lock being acquired again. + switch self.next(id) { + case .return(let resultAndProducers): + return .resume(continuation, resultAndProducers.nextResult, resultAndProducers.producers) + case .suspend: + if self.subscriptions.setContinuation(continuation, forSubscriber: id) { + return .none + } else { + return .resume(continuation, .failure(CancellationError()), nil) + } } } @@ -1149,7 +1161,7 @@ struct _BroadcastSequenceStateMachine: Sendable { @usableFromInline enum OnSetContinuation { case none - case resume(ConsumerContinuation, Result) + case resume(ConsumerContinuation, Result, ProducerContinuations?) } @inlinable @@ -1175,7 +1187,7 @@ struct _BroadcastSequenceStateMachine: Sendable { self._state = .streaming(state) case .finished(let state): - onSetContinuation = .resume(continuation, state.result.map { _ in nil }) + onSetContinuation = .resume(continuation, state.result.map { _ in nil }, nil) case ._modifying: // All values must have been produced, nothing to wait for. From 55e39fa96be302e0d82c86dcbe18f4df8994e5b8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Nov 2024 11:31:57 +0000 Subject: [PATCH 491/580] Remove '@_exported import GRPCCore' (#2109) Motivation: We added '@_exported import GRPCCore' to transport modules to make it easier for users as it saves them from adding an import. On reflection we've our position on this has changed and the implicit dependency will likely make things less clear. Modifications: - Remove exported import from in-proc transport - Update tutorials/examples to have explicit 'GRPCCore' imports Result: More explicit dependencies --- Examples/echo/Package.swift | 2 ++ Examples/hello-world/Package.swift | 2 ++ .../hello-world/Sources/Subcommands/Greet.swift | 1 + .../hello-world/Sources/Subcommands/Serve.swift | 1 + Examples/route-guide/Package.swift | 2 ++ .../Sources/Subcommands/GetFeature.swift | 1 + .../Sources/Subcommands/ListFeatures.swift | 1 + .../Sources/Subcommands/RecordRoute.swift | 1 + .../Sources/Subcommands/RouteChat.swift | 1 + .../route-guide/Sources/Subcommands/Serve.swift | 1 + README.md | 6 ++++-- .../route-guide-sec01-step08-description.swift | 1 + .../route-guide-sec05-step00-package.swift | 1 + .../route-guide-sec05-step01-package.swift | 1 + .../route-guide-sec05-step02-package.swift | 1 + .../route-guide-sec05-step07-server.swift | 1 + .../route-guide-sec05-step08-run.swift | 1 + ...oute-guide-sec06-step02-add-run-client.swift | 2 -- ...route-guide-sec06-step03-create-client.swift | 1 + .../route-guide-sec06-step04-run-client.swift | 1 + .../route-guide-sec06-step05-stub.swift | 1 + .../route-guide-sec06-step06-get-feature.swift | 1 + ...route-guide-sec06-step07-list-features.swift | 1 + .../route-guide-sec06-step08-record-route.swift | 1 + .../route-guide-sec06-step09-route-chat.swift | 1 + .../Tutorials/Route-Guide/Route-Guide.tutorial | 5 +++-- Sources/GRPCInProcessTransport/Exports.swift | 17 ----------------- 27 files changed, 33 insertions(+), 23 deletions(-) delete mode 100644 Sources/GRPCInProcessTransport/Exports.swift diff --git a/Examples/echo/Package.swift b/Examples/echo/Package.swift index 4510456be..3792fcb04 100644 --- a/Examples/echo/Package.swift +++ b/Examples/echo/Package.swift @@ -21,6 +21,7 @@ let package = Package( name: "echo", platforms: [.macOS("15.0")], dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", exact: "2.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-nio-transport", branch: "1.0.0-alpha.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), @@ -29,6 +30,7 @@ let package = Package( .executableTarget( name: "echo", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "ArgumentParser", package: "swift-argument-parser"), diff --git a/Examples/hello-world/Package.swift b/Examples/hello-world/Package.swift index 52a9b349a..5437aa7dc 100644 --- a/Examples/hello-world/Package.swift +++ b/Examples/hello-world/Package.swift @@ -21,6 +21,7 @@ let package = Package( name: "hello-world", platforms: [.macOS("15.0")], dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", exact: "2.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), @@ -29,6 +30,7 @@ let package = Package( .executableTarget( name: "hello-world", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "ArgumentParser", package: "swift-argument-parser"), diff --git a/Examples/hello-world/Sources/Subcommands/Greet.swift b/Examples/hello-world/Sources/Subcommands/Greet.swift index 64d405863..cd005534a 100644 --- a/Examples/hello-world/Sources/Subcommands/Greet.swift +++ b/Examples/hello-world/Sources/Subcommands/Greet.swift @@ -15,6 +15,7 @@ */ import ArgumentParser +import GRPCCore import GRPCNIOTransportHTTP2 import GRPCProtobuf diff --git a/Examples/hello-world/Sources/Subcommands/Serve.swift b/Examples/hello-world/Sources/Subcommands/Serve.swift index ef5dc2935..d65483865 100644 --- a/Examples/hello-world/Sources/Subcommands/Serve.swift +++ b/Examples/hello-world/Sources/Subcommands/Serve.swift @@ -15,6 +15,7 @@ */ import ArgumentParser +import GRPCCore import GRPCNIOTransportHTTP2 import GRPCProtobuf diff --git a/Examples/route-guide/Package.swift b/Examples/route-guide/Package.swift index 79e553920..7aba9a836 100644 --- a/Examples/route-guide/Package.swift +++ b/Examples/route-guide/Package.swift @@ -21,6 +21,7 @@ let package = Package( name: "route-guide", platforms: [.macOS("15.0")], dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift", exact: "2.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-protobuf", exact: "1.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-nio-transport", exact: "1.0.0-alpha.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), @@ -29,6 +30,7 @@ let package = Package( .executableTarget( name: "route-guide", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "ArgumentParser", package: "swift-argument-parser"), diff --git a/Examples/route-guide/Sources/Subcommands/GetFeature.swift b/Examples/route-guide/Sources/Subcommands/GetFeature.swift index 72ba1d1a4..890d432cd 100644 --- a/Examples/route-guide/Sources/Subcommands/GetFeature.swift +++ b/Examples/route-guide/Sources/Subcommands/GetFeature.swift @@ -15,6 +15,7 @@ */ import ArgumentParser +import GRPCCore import GRPCNIOTransportHTTP2 struct GetFeature: AsyncParsableCommand { diff --git a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift index 7e83add71..e2d1a5f5c 100644 --- a/Examples/route-guide/Sources/Subcommands/ListFeatures.swift +++ b/Examples/route-guide/Sources/Subcommands/ListFeatures.swift @@ -15,6 +15,7 @@ */ import ArgumentParser +import GRPCCore import GRPCNIOTransportHTTP2 struct ListFeatures: AsyncParsableCommand { diff --git a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift index 7cdf30cd2..e6d1611f1 100644 --- a/Examples/route-guide/Sources/Subcommands/RecordRoute.swift +++ b/Examples/route-guide/Sources/Subcommands/RecordRoute.swift @@ -15,6 +15,7 @@ */ import ArgumentParser +import GRPCCore import GRPCNIOTransportHTTP2 struct RecordRoute: AsyncParsableCommand { diff --git a/Examples/route-guide/Sources/Subcommands/RouteChat.swift b/Examples/route-guide/Sources/Subcommands/RouteChat.swift index 3461e1f75..d25352662 100644 --- a/Examples/route-guide/Sources/Subcommands/RouteChat.swift +++ b/Examples/route-guide/Sources/Subcommands/RouteChat.swift @@ -15,6 +15,7 @@ */ import ArgumentParser +import GRPCCore import GRPCNIOTransportHTTP2 struct RouteChat: AsyncParsableCommand { diff --git a/Examples/route-guide/Sources/Subcommands/Serve.swift b/Examples/route-guide/Sources/Subcommands/Serve.swift index c799b4fb7..8651346d4 100644 --- a/Examples/route-guide/Sources/Subcommands/Serve.swift +++ b/Examples/route-guide/Sources/Subcommands/Serve.swift @@ -16,6 +16,7 @@ import ArgumentParser import Foundation +import GRPCCore import GRPCNIOTransportHTTP2 import GRPCProtobuf import Synchronization diff --git a/README.md b/README.md index bc2c4d3cf..ea764d8e0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ about gRPC on the [gRPC project's website][grpcio]. - [`grpc-swift-nio-transport`][grpc-swift-nio-transport] contains high-performance HTTP/2 client and server transport implementations for gRPC Swift built on top of SwiftNIO. - [`grpc-swift-protobuf`][grpc-swift-protobuf] contains integrations with SwiftProtobuf for gRPC Swift. - [`grpc-swift-extras`][grpc-swift-extras] contains optional extras for gRPC Swift. - + ## Quick Start @@ -32,6 +32,7 @@ let package = Package( name: "foo-package", platforms: [.macOS("15.0")], dependencies: [ + .package(url: "https://github.com/grpc/grpc-swift.git", from: "2.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "1.0.0-alpha.1"), .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "1.0.0-alpha.1"), ], @@ -39,6 +40,7 @@ let package = Package( .executableTarget( name: "bar-target", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ] @@ -52,4 +54,4 @@ let package = Package( [spi-grpc-swift]: https://swiftpackageindex.com/grpc/grpc-swift/documentation [grpc-swift-nio-transport]: https://github.com/grpc/grpc-swift-nio-transport [grpc-swift-protobuf]: https://github.com/grpc/grpc-swift-protobuf -[grpc-swift-extras]: https://github.com/grpc/grpc-swift-extras +[grpc-swift-extras]: https://github.com/grpc/grpc-swift-extras diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift index 09de19627..65a51d4b9 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec01-step08-description.swift @@ -12,6 +12,7 @@ let package = Package( .executableTarget( name: "RouteGuide", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ] diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift index 09de19627..65a51d4b9 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step00-package.swift @@ -12,6 +12,7 @@ let package = Package( .executableTarget( name: "RouteGuide", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ] diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift index aac2037d5..e5f9b1088 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step01-package.swift @@ -12,6 +12,7 @@ let package = Package( .executableTarget( name: "RouteGuide", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), ], diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift index 13c5b9816..1e8ef47e1 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step02-package.swift @@ -13,6 +13,7 @@ let package = Package( .executableTarget( name: "RouteGuide", dependencies: [ + .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"), .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"), .product(name: "ArgumentParser", package: "swift-argument-parser"), diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift index a9797e762..a0ba287f4 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step07-server.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift index ac8280a58..6502dee78 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec05-step08-run.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift index 55d8b69ee..15d650a6f 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step02-add-run-client.swift @@ -1,5 +1,3 @@ -import GRPCNIOTransportHTTP2 - extension RouteGuide { func runClient() async throws { } diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift index 445f09ede..ef8bc14ae 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step03-create-client.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift index c667bf626..f87f8cc80 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step04-run-client.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift index 6d4e9dc7e..15322b30f 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step05-stub.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift index 4f7efb31c..04f75c699 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step06-get-feature.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift index 22caf8eeb..adc6a25f4 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step07-list-features.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift index ab68b76c9..448eaa0ac 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step08-record-route.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift index fddc42966..12f75d83d 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Resources/route-guide-sec06-step09-route-chat.swift @@ -1,3 +1,4 @@ +import GRPCCore import GRPCNIOTransportHTTP2 extension RouteGuide { diff --git a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial index ef4f3809f..2d9192efc 100644 --- a/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial +++ b/Sources/GRPCCore/Documentation.docc/Tutorials/Route-Guide/Route-Guide.tutorial @@ -77,8 +77,9 @@ Next we can add a target. In this tutorial we'll create a single executable target which can act as both a client and a server. - We require two gRPC dependencies: `GRPCNIOTransportHTTP2"` provides an implementation of an HTTP/2 - client and server, while `GRPCProtobuf` provides the necessary components to serialize + We require three gRPC dependencies: `GRPCCore` provides core abstractions and runtime + components, `GRPCNIOTransportHTTP2` provides an implementation of an HTTP/2 + client and server, and `GRPCProtobuf` provides the necessary components to serialize and deserialize `SwiftProtobuf` messages. @Code(name: "Package.swift", file: "route-guide-sec01-step08-description.swift") diff --git a/Sources/GRPCInProcessTransport/Exports.swift b/Sources/GRPCInProcessTransport/Exports.swift deleted file mode 100644 index 1f32ac4d1..000000000 --- a/Sources/GRPCInProcessTransport/Exports.swift +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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. - */ - -@_exported import GRPCCore From f97f76c048486247531cb551aa351357e27dc686 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 8 Nov 2024 13:59:42 +0000 Subject: [PATCH 492/580] Add more build settings to test targets (#2106) Motivation: The test targets don't set the same Swift as the library targets. There's no reason for them not to. Modifications: Apply default settings to test targets. Result: Test targets use Swift 6 language mode, explicit existential any, and internal imports by default --- Package.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 86cbe9c07..e73076761 100644 --- a/Package.swift +++ b/Package.swift @@ -69,7 +69,8 @@ let targets: [Target] = [ ], resources: [ .copy("Configuration/Inputs") - ] + ], + swiftSettings: defaultSwiftSettings ), // In-process client and server transport implementations @@ -84,7 +85,8 @@ let targets: [Target] = [ name: "GRPCInProcessTransportTests", dependencies: [ .target(name: "GRPCInProcessTransport") - ] + ], + swiftSettings: defaultSwiftSettings ), // Code generator library for protoc-gen-grpc-swift @@ -97,7 +99,8 @@ let targets: [Target] = [ name: "GRPCCodeGenTests", dependencies: [ .target(name: "GRPCCodeGen") - ] + ], + swiftSettings: defaultSwiftSettings ), ] From f9635236753283ce40c3ab189c77815f5b9a922d Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 13 Nov 2024 14:03:06 +0000 Subject: [PATCH 493/580] Fix some docs formatting (#2112) There were some warnings when building the documentation so this PR fixes them. --- .../Articles/Generating-stubs.md | 8 ++++--- .../Development/Benchmarks.md | 2 ++ .../Documentation.docc/Development/Design.md | 24 ++++++++++--------- .../Documentation.docc/Documentation.md | 6 +++-- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md index c55691a36..80c983184 100644 --- a/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md +++ b/Sources/GRPCCore/Documentation.docc/Articles/Generating-stubs.md @@ -2,7 +2,9 @@ Learn how to generate stubs for gRPC Swift from a service defined using the Protocol Buffers IDL. -## Using protoc +## Overview + +### Using protoc If you've used Protocol Buffers before then generating gRPC Swift stubs should be simple. If you're unfamiliar with Protocol Buffers then you should get comfortable with the concepts before @@ -45,7 +47,7 @@ protoc \ --grpc-swift_out=. ``` -### Generator options +#### Generator options | Name | Possible Values | Default | Description | |---------------------------|--------------------------------------------|------------|----------------------------------------------------------| @@ -67,7 +69,7 @@ allows you to specify a mapping from `.proto` files to the Swift module they are allows the code generator to add appropriate imports to your generated stubs. This is described in more detail in the [SwiftProtobuf documentation](https://github.com/apple/swift-protobuf/blob/main/Documentation/PLUGIN.md). -### Building the plugin +#### Building the plugin > The version of `protoc-gen-grpc-swift` you use mustn't be newer than the version of > the `grpc-swift-protobuf` you're using. diff --git a/Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md b/Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md index 4033857f7..d15cf8082 100644 --- a/Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md +++ b/Sources/GRPCCore/Documentation.docc/Development/Benchmarks.md @@ -1,5 +1,7 @@ # Benchmarks +This article discusses benchmarking in `grpc-swift`. + ## Overview Benchmarks for this package are in a separate Swift Package in the `Performance/Benchmarks` diff --git a/Sources/GRPCCore/Documentation.docc/Development/Design.md b/Sources/GRPCCore/Documentation.docc/Development/Design.md index fe664a38d..1861d3ae0 100644 --- a/Sources/GRPCCore/Documentation.docc/Development/Design.md +++ b/Sources/GRPCCore/Documentation.docc/Development/Design.md @@ -14,7 +14,9 @@ mapping a call onto a stream and dealing with serialization. The highest level of abstraction is the _stub_ layer which provides client and server interfaces generated from an interface definition language (IDL). -## Transport +## Overview + +### Transport The transport layer provides a bidirectional communication channel with a remote peer which is typically long-lived. @@ -42,7 +44,7 @@ The vast majority of users won't need to implement either of these protocols. However, many users will need to create instances of types conforming to these protocols to create a server or client, respectively. -### Server transport +#### Server transport The ``ServerTransport`` is responsible for the server half of a transport. It listens for new gRPC streams and then processes them. This is achieved via the @@ -57,7 +59,7 @@ Note that the server transport doesn't include the idea of a "connection". While an HTTP/2 server transport will in all likelihood have multiple connections open at any given time, that detail isn't surfaced at this level of abstraction. -### Client transport +#### Client transport While the server is responsible for handling streams, the ``ClientTransport`` is responsible for creating them. Client transports will typically maintain a @@ -83,7 +85,7 @@ may be retried. Some of this is exposed via the ``ClientTransport`` as the ``ClientTransport/retryThrottle`` and ``ClientTransport/config(forMethod:)``. -### Streams +#### Streams Both client and server transport protocols use ``RPCStream`` to represent streams of information. Each RPC can be thought of as having two logical @@ -122,7 +124,7 @@ and indicates the final outcome of an RPC. It includes a ``Status/Code-swift.str and string describing the final outcome while the ``Metadata`` may contain additional information about the RPC. -## Call +### Call The "call" layer builds on top the transport layer to map higher level RPCs calls on to streams. It also implements transport-agnostic functionality, like serialization @@ -139,7 +141,7 @@ provides support for [SwiftProtobuf](https://github.com/apple/swift-protobuf) by implementing serializers and a code generator for the Protocol Buffers compiler, `protoc`. -### Interceptors +#### Interceptors This layer also provides client and server interceptors allowing you to modify requests and responses between the caller and the network. These are implemented as @@ -152,7 +154,7 @@ Naturally, the interceptors APIs are `async`. Interceptors are registered directly with the ``GRPCClient`` and ``GRPCServer`` and can either be applied to all RPCs or to specific services. -### Client +#### Client The call layer includes a concrete ``GRPCClient`` which provides API to execute all four types of RPC against a ``ClientTransport``. These methods are: @@ -178,7 +180,7 @@ which will stop new RPCs from starting (by failing them with Existing work can be stopped more abruptly by cancelling the task where ``GRPCClient/run()`` is executing. -### Server +#### Server ``GRPCServer`` is provided by the call layer to host services for a given transport. Beyond creating the server it has a very limited API surface: it has @@ -188,7 +190,7 @@ can initiate graceful shutdown by calling ``GRPCServer/beginGracefulShutdown()`` which will stop new RPCs from being handled but will let existing RPCs run to completion. Cancelling the task will close the server more abruptly. -## Stub +### Stub The stub layer is the layer which most users interact with. It provides service specific interfaces generated from an interface definition language (IDL) such @@ -204,7 +206,7 @@ However, the stub layer is optional, users may choose to not use it and construct clients and services manually. A gRPC proxy, for example, would not use the stub layer. -### Server stubs +#### Server stubs Users implement services by conforming a type to a generated service `protocol`. Each service has three protocols generated for it: @@ -308,7 +310,7 @@ refines the ``RegistrableRPCService`` protocol. This `protocol` has a single requirement for registering methods with an ``RPCRouter``. A default implementation of this method is also provided. -### Client stubs +#### Client stubs Generated client code is split into a `protocol` and a concrete `struct` implementing the `protocol`. An example of the client protocol is: diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index dc6ae14dd..2e0103514 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -5,7 +5,9 @@ A gRPC library for Swift written natively in Swift. > 🚧 This module is part of gRPC Swift v2 which is under active development and in the pre-release > stage. -## Package structure +## Overview + +### Package structure gRPC Swift is distributed across multiple Swift packages. These are: @@ -32,7 +34,7 @@ gRPC Swift is distributed across multiple Swift packages. These are: This package, and this module (``GRPCCore``) in particular, include higher level documentation such as tutorials. -## Modules in this package +### Modules in this package - ``GRPCCore`` (this module) contains core abstractions, currency types and runtime components for gRPC Swift. From c3f09df0c0ac8733b1e992daa809a640e1df7125 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 13 Nov 2024 14:07:44 +0000 Subject: [PATCH 494/580] Allow adding `ServerInterceptor`s to specific services and methods (#2096) ## Motivation We want to allow users to customise the RPCs a registered interceptor should apply to on the server: - Intercept all requests - Intercept requests only meant for specific services - Intercept requests only meant for specific methods ## Modifications This PR adds a new `ServerInterceptorTarget` type that allows users to specify what the target of the interceptor should be. Existing APIs accepting `[any ServerInterceptor]` have been changed to instead take `[ServerInterceptorTarget]`. ## Result Users can have more control over to which requests interceptors are applied. --------- Co-authored-by: George Barnett --- Sources/GRPCCore/Call/Server/RPCRouter.swift | 32 ++- .../Call/Server/ServerInterceptor.swift | 17 +- .../ServerInterceptorPipelineOperation.swift | 98 +++++++ Sources/GRPCCore/GRPCServer.swift | 47 ++-- .../Internal/ServerRPCExecutorTests.swift | 4 +- Tests/GRPCCoreTests/GRPCServerTests.swift | 255 +++++++++++++++++- .../Call/Server/ServerInterceptors.swift | 4 +- .../Test Utilities/Services/BinaryEcho.swift | 23 +- .../Test Utilities/Services/HelloWorld.swift | 49 ++++ 9 files changed, 480 insertions(+), 49 deletions(-) create mode 100644 Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift create mode 100644 Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift diff --git a/Sources/GRPCCore/Call/Server/RPCRouter.swift b/Sources/GRPCCore/Call/Server/RPCRouter.swift index a574a1186..d40bd71c4 100644 --- a/Sources/GRPCCore/Call/Server/RPCRouter.swift +++ b/Sources/GRPCCore/Call/Server/RPCRouter.swift @@ -22,6 +22,8 @@ /// the router has a handler for a method with ``hasHandler(forMethod:)`` or get a list of all /// methods with handlers registered by calling ``methods``. You can also remove the handler for a /// given method by calling ``removeHandler(forMethod:)``. +/// You can also register any interceptors that you want applied to registered handlers via the +/// ``registerInterceptors(pipeline:)`` method. /// /// In most cases you won't need to interact with the router directly. Instead you should register /// your services with ``GRPCServer/init(transport:services:interceptors:)`` which will in turn @@ -82,7 +84,8 @@ public struct RPCRouter: Sendable { } @usableFromInline - private(set) var handlers: [MethodDescriptor: RPCHandler] + private(set) var handlers: + [MethodDescriptor: (handler: RPCHandler, interceptors: [any ServerInterceptor])] /// Creates a new router with no methods registered. public init() { @@ -126,12 +129,13 @@ public struct RPCRouter: Sendable { _ context: ServerContext ) async throws -> StreamingServerResponse ) { - self.handlers[descriptor] = RPCHandler( + let handler = RPCHandler( method: descriptor, deserializer: deserializer, serializer: serializer, handler: handler ) + self.handlers[descriptor] = (handler, []) } /// Removes any handler registered for the specified method. @@ -142,6 +146,25 @@ public struct RPCRouter: Sendable { public mutating func removeHandler(forMethod descriptor: MethodDescriptor) -> Bool { return self.handlers.removeValue(forKey: descriptor) != nil } + + /// Registers applicable interceptors to all currently-registered handlers. + /// + /// - Important: Calling this method will apply the interceptors only to existing handlers. Any handlers registered via + /// ``registerHandler(forMethod:deserializer:serializer:handler:)`` _after_ calling this method will not have + /// any interceptors applied to them. If you want to make sure all registered methods have any applicable interceptors applied, + /// only call this method _after_ you have registered all handlers. + /// - Parameter pipeline: The interceptor pipeline operations to register to all currently-registered handlers. The order of the + /// interceptors matters. + /// - SeeAlso: ``ServerInterceptorPipelineOperation``. + @inlinable + public mutating func registerInterceptors(pipeline: [ServerInterceptorPipelineOperation]) { + for descriptor in self.handlers.keys { + let applicableOperations = pipeline.filter { $0.applies(to: descriptor) } + if !applicableOperations.isEmpty { + self.handlers[descriptor]?.interceptors = applicableOperations.map { $0.interceptor } + } + } + } } extension RPCRouter { @@ -150,10 +173,9 @@ extension RPCRouter { RPCAsyncSequence, RPCWriter.Closable >, - context: ServerContext, - interceptors: [any ServerInterceptor] + context: ServerContext ) async { - if let handler = self.handlers[stream.descriptor] { + if let (handler, interceptors) = self.handlers[stream.descriptor] { await handler.handle(stream: stream, context: context, interceptors: interceptors) } else { // If this throws then the stream must be closed which we can't do anything about, so ignore diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift index 3c71cd3e5..e90266862 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptor.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptor.swift @@ -21,10 +21,11 @@ /// been returned from a service. They are typically used for cross-cutting concerns like filtering /// requests, validating messages, logging additional data, and tracing. /// -/// Interceptors are registered with the server apply to all RPCs. If you need to modify the -/// behavior of an interceptor on a per-RPC basis then you can use the -/// ``ServerContext/descriptor`` to determine which RPC is being called and -/// conditionalise behavior accordingly. +/// Interceptors can be registered with the server either directly or via ``ServerInterceptorPipelineOperation``s. +/// You may register them for all services registered with a server, for RPCs directed to specific services, or +/// for RPCs directed to specific methods. If you need to modify the behavior of an interceptor on a +/// per-RPC basis in more detail, then you can use the ``ServerContext/descriptor`` to determine +/// which RPC is being called and conditionalise behavior accordingly. /// /// ## RPC filtering /// @@ -33,19 +34,19 @@ /// demonstrates this. /// /// ```swift -/// struct AuthServerInterceptor: Sendable { +/// struct AuthServerInterceptor: ServerInterceptor { /// let isAuthorized: @Sendable (String, MethodDescriptor) async throws -> Void /// /// func intercept( /// request: StreamingServerRequest, -/// context: ServerInterceptorContext, +/// context: ServerContext, /// next: @Sendable ( /// _ request: StreamingServerRequest, -/// _ context: ServerInterceptorContext +/// _ context: ServerContext /// ) async throws -> StreamingServerResponse /// ) async throws -> StreamingServerResponse { /// // Extract the auth token. -/// guard let token = request.metadata["authorization"] else { +/// guard let token = request.metadata[stringValues: "authorization"].first(where: { _ in true }) else { /// throw RPCError(code: .unauthenticated, message: "Not authenticated") /// } /// diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift new file mode 100644 index 000000000..3d2731fd4 --- /dev/null +++ b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift @@ -0,0 +1,98 @@ +/* + * 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. + */ + +/// A `ServerInterceptorPipelineOperation` describes to which RPCs a server interceptor should be applied. +/// +/// You can configure a server interceptor to be applied to: +/// - all RPCs and services; +/// - requests directed only to specific services registered with your server; or +/// - requests directed only to specific methods (of a specific service). +/// +/// - SeeAlso: ``ServerInterceptor`` for more information on server interceptors. +public struct ServerInterceptorPipelineOperation: Sendable { + /// The subject of a ``ServerInterceptorPipelineOperation``. + /// The subject of an interceptor can either be all services and methods, only specific services, or only specific methods. + public struct Subject: Sendable { + internal enum Wrapped: Sendable { + case all + case services(Set) + case methods(Set) + } + + private let wrapped: Wrapped + + /// An operation subject specifying an interceptor that applies to all RPCs across all services will be registered with this server. + public static var all: Self { .init(wrapped: .all) } + + /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services. + /// - Parameters: + /// - services: The list of service names for which this interceptor should intercept RPCs. + /// - Returns: A ``ServerInterceptorPipelineOperation``. + public static func services(_ services: Set) -> Self { + Self(wrapped: .services(services)) + } + + /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified service methods. + /// - Parameters: + /// - methods: The list of method descriptors for which this interceptor should intercept RPCs. + /// - Returns: A ``ServerInterceptorPipelineOperation``. + public static func methods(_ methods: Set) -> Self { + Self(wrapped: .methods(methods)) + } + + @usableFromInline + internal func applies(to descriptor: MethodDescriptor) -> Bool { + switch self.wrapped { + case .all: + return true + + case .services(let services): + return services.map({ $0.fullyQualifiedService }).contains(descriptor.service) + + case .methods(let methods): + return methods.contains(descriptor) + } + } + } + + /// The interceptor specified for this operation. + public let interceptor: any ServerInterceptor + + @usableFromInline + internal let subject: Subject + + private init(interceptor: any ServerInterceptor, appliesTo: Subject) { + self.interceptor = interceptor + self.subject = appliesTo + } + + /// Create an operation, specifying which ``ServerInterceptor`` to apply and to which ``Subject``. + /// - Parameters: + /// - interceptor: The ``ServerInterceptor`` to register with the server. + /// - subject: The ``Subject`` to which the `interceptor` applies. + /// - Returns: A ``ServerInterceptorPipelineOperation``. + public static func apply(_ interceptor: any ServerInterceptor, to subject: Subject) -> Self { + Self(interceptor: interceptor, appliesTo: subject) + } + + /// Returns whether this ``ServerInterceptorPipelineOperation`` applies to the given `descriptor`. + /// - Parameter descriptor: A ``MethodDescriptor`` for which to test whether this interceptor applies. + /// - Returns: `true` if this interceptor applies to the given `descriptor`, or `false` otherwise. + @inlinable + internal func applies(to descriptor: MethodDescriptor) -> Bool { + self.subject.applies(to: descriptor) + } +} diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 8ba69985d..6ff82b9dd 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -78,14 +78,6 @@ public final class GRPCServer: Sendable { /// The services registered which the server is serving. private let router: RPCRouter - /// A collection of ``ServerInterceptor`` implementations which are applied to all accepted - /// RPCs. - /// - /// RPCs are intercepted in the order that interceptors are added. That is, a request received - /// from the client will first be intercepted by the first added interceptor followed by the - /// second, and so on. - private let interceptors: [any ServerInterceptor] - /// The state of the server. private let state: Mutex @@ -154,33 +146,46 @@ public final class GRPCServer: Sendable { services: [any RegistrableRPCService], interceptors: [any ServerInterceptor] = [] ) { - var router = RPCRouter() - for service in services { - service.registerMethods(with: &router) - } - - self.init(transport: transport, router: router, interceptors: interceptors) + self.init( + transport: transport, + services: services, + interceptorPipeline: interceptors.map { .apply($0, to: .all) } + ) } /// Creates a new server with no resources. /// /// - Parameters: /// - transport: The transport the server should listen on. - /// - router: A ``RPCRouter`` used by the server to route accepted streams to method handlers. - /// - interceptors: A collection of interceptors providing cross-cutting functionality to each + /// - services: Services offered by the server. + /// - interceptorPipeline: A collection of interceptors providing cross-cutting functionality to each /// accepted RPC. The order in which interceptors are added reflects the order in which they /// are called. The first interceptor added will be the first interceptor to intercept each /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. - public init( + public convenience init( transport: any ServerTransport, - router: RPCRouter, - interceptors: [any ServerInterceptor] = [] + services: [any RegistrableRPCService], + interceptorPipeline: [ServerInterceptorPipelineOperation] ) { + var router = RPCRouter() + for service in services { + service.registerMethods(with: &router) + } + router.registerInterceptors(pipeline: interceptorPipeline) + + self.init(transport: transport, router: router) + } + + /// Creates a new server with no resources. + /// + /// - Parameters: + /// - transport: The transport the server should listen on. + /// - router: A ``RPCRouter`` used by the server to route accepted streams to method handlers. + public init(transport: any ServerTransport, router: RPCRouter) { self.state = Mutex(.notStarted) self.transport = transport self.router = router - self.interceptors = interceptors } /// Starts the server and runs until the registered transport has closed. @@ -206,7 +211,7 @@ public final class GRPCServer: Sendable { do { try await transport.listen { stream, context in - await self.router.handle(stream: stream, context: context, interceptors: self.interceptors) + await self.router.handle(stream: stream, context: context) } } catch { throw RuntimeError( diff --git a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift index 0533fe26b..fe8d301aa 100644 --- a/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/Internal/ServerRPCExecutorTests.swift @@ -333,7 +333,9 @@ final class ServerRPCExecutorTests: XCTestCase { func testThrowingInterceptor() async throws { let harness = ServerRPCExecutorTestHarness( - interceptors: [.throwError(RPCError(code: .unavailable, message: "Unavailable"))] + interceptors: [ + .throwError(RPCError(code: .unavailable, message: "Unavailable")) + ] ) try await harness.execute(handler: .echo) { inbound in diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index b7866c80c..9b20785d5 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -16,19 +16,20 @@ import GRPCCore import GRPCInProcessTransport +import Testing import XCTest final class GRPCServerTests: XCTestCase { func withInProcessClientConnectedToServer( services: [any RegistrableRPCService], - interceptors: [any ServerInterceptor] = [], + interceptorPipeline: [ServerInterceptorPipelineOperation] = [], _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() let server = GRPCServer( transport: inProcess.server, services: services, - interceptors: interceptors + interceptorPipeline: interceptorPipeline ) try await withThrowingTaskGroup(of: Void.self) { group in @@ -219,10 +220,10 @@ final class GRPCServerTests: XCTestCase { try await self.withInProcessClientConnectedToServer( services: [BinaryEcho()], - interceptors: [ - .requestCounter(counter1), - .rejectAll(with: RPCError(code: .unavailable, message: "")), - .requestCounter(counter2), + interceptorPipeline: [ + .apply(.requestCounter(counter1), to: .all), + .apply(.rejectAll(with: RPCError(code: .unavailable, message: "")), to: .all), + .apply(.requestCounter(counter2), to: .all), ] ) { client, _ in try await client.withStream( @@ -248,7 +249,7 @@ final class GRPCServerTests: XCTestCase { try await self.withInProcessClientConnectedToServer( services: [BinaryEcho()], - interceptors: [.requestCounter(counter)] + interceptorPipeline: [.apply(.requestCounter(counter), to: .all)] ) { client, _ in try await client.withStream( descriptor: MethodDescriptor(service: "not", method: "implemented"), @@ -374,3 +375,243 @@ final class GRPCServerTests: XCTestCase { } } } + +@Suite("GRPC Server Tests") +struct ServerTests { + @Test("Interceptors are applied only to specified services") + func testInterceptorsAreAppliedToSpecifiedServices() async throws { + let onlyBinaryEchoCounter = AtomicCounter() + let allServicesCounter = AtomicCounter() + let onlyHelloWorldCounter = AtomicCounter() + let bothServicesCounter = AtomicCounter() + + try await self.withInProcessClientConnectedToServer( + services: [BinaryEcho(), HelloWorld()], + interceptorPipeline: [ + .apply( + .requestCounter(onlyBinaryEchoCounter), + to: .services([BinaryEcho.serviceDescriptor]) + ), + .apply(.requestCounter(allServicesCounter), to: .all), + .apply( + .requestCounter(onlyHelloWorldCounter), + to: .services([HelloWorld.serviceDescriptor]) + ), + .apply( + .requestCounter(bothServicesCounter), + to: .services([BinaryEcho.serviceDescriptor, HelloWorld.serviceDescriptor]) + ), + ] + ) { client, _ in + // Make a request to the `BinaryEcho` service and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message(Array("hello".utf8))) + await stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + self.assertMetadata(metadata) + + let message = try await responseParts.next() + self.assertMessage(message) { + #expect($0 == Array("hello".utf8)) + } + + let status = try await responseParts.next() + self.assertStatus(status) { status, _ in + #expect(status.code == .ok, Comment(rawValue: status.description)) + } + } + + #expect(onlyBinaryEchoCounter.value == 1) + #expect(allServicesCounter.value == 1) + #expect(onlyHelloWorldCounter.value == 0) + #expect(bothServicesCounter.value == 1) + + // Now, make a request to the `HelloWorld` service and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.withStream( + descriptor: HelloWorld.Methods.sayHello, + options: .defaults + ) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message(Array("Swift".utf8))) + await stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + self.assertMetadata(metadata) + + let message = try await responseParts.next() + self.assertMessage(message) { + #expect($0 == Array("Hello, Swift!".utf8)) + } + + let status = try await responseParts.next() + self.assertStatus(status) { status, _ in + #expect(status.code == .ok, Comment(rawValue: status.description)) + } + } + + #expect(onlyBinaryEchoCounter.value == 1) + #expect(allServicesCounter.value == 2) + #expect(onlyHelloWorldCounter.value == 1) + #expect(bothServicesCounter.value == 2) + } + } + + @Test("Interceptors are applied only to specified methods") + func testInterceptorsAreAppliedToSpecifiedMethods() async throws { + let onlyBinaryEchoGetCounter = AtomicCounter() + let onlyBinaryEchoCollectCounter = AtomicCounter() + let bothBinaryEchoMethodsCounter = AtomicCounter() + let allMethodsCounter = AtomicCounter() + + try await self.withInProcessClientConnectedToServer( + services: [BinaryEcho()], + interceptorPipeline: [ + .apply( + .requestCounter(onlyBinaryEchoGetCounter), + to: .methods([BinaryEcho.Methods.get]) + ), + .apply(.requestCounter(allMethodsCounter), to: .all), + .apply( + .requestCounter(onlyBinaryEchoCollectCounter), + to: .methods([BinaryEcho.Methods.collect]) + ), + .apply( + .requestCounter(bothBinaryEchoMethodsCounter), + to: .methods([BinaryEcho.Methods.get, BinaryEcho.Methods.collect]) + ), + ] + ) { client, _ in + // Make a request to the `BinaryEcho/get` method and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.withStream( + descriptor: BinaryEcho.Methods.get, + options: .defaults + ) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message(Array("hello".utf8))) + await stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + self.assertMetadata(metadata) + + let message = try await responseParts.next() + self.assertMessage(message) { + #expect($0 == Array("hello".utf8)) + } + + let status = try await responseParts.next() + self.assertStatus(status) { status, _ in + #expect(status.code == .ok, Comment(rawValue: status.description)) + } + } + + #expect(onlyBinaryEchoGetCounter.value == 1) + #expect(allMethodsCounter.value == 1) + #expect(onlyBinaryEchoCollectCounter.value == 0) + #expect(bothBinaryEchoMethodsCounter.value == 1) + + // Now, make a request to the `BinaryEcho/collect` method and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.withStream( + descriptor: BinaryEcho.Methods.collect, + options: .defaults + ) { stream in + try await stream.outbound.write(.metadata([:])) + try await stream.outbound.write(.message(Array("hello".utf8))) + await stream.outbound.finish() + + var responseParts = stream.inbound.makeAsyncIterator() + let metadata = try await responseParts.next() + self.assertMetadata(metadata) + + let message = try await responseParts.next() + self.assertMessage(message) { + #expect($0 == Array("hello".utf8)) + } + + let status = try await responseParts.next() + self.assertStatus(status) { status, _ in + #expect(status.code == .ok, Comment(rawValue: status.description)) + } + } + + #expect(onlyBinaryEchoGetCounter.value == 1) + #expect(allMethodsCounter.value == 2) + #expect(onlyBinaryEchoCollectCounter.value == 1) + #expect(bothBinaryEchoMethodsCounter.value == 2) + } + } + + func withInProcessClientConnectedToServer( + services: [any RegistrableRPCService], + interceptorPipeline: [ServerInterceptorPipelineOperation] = [], + _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void + ) async throws { + let inProcess = InProcessTransport() + let server = GRPCServer( + transport: inProcess.server, + services: services, + interceptorPipeline: interceptorPipeline + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.serve() + } + + group.addTask { + try await inProcess.client.connect() + } + + try await body(inProcess.client, server) + inProcess.client.beginGracefulShutdown() + server.beginGracefulShutdown() + } + } + + func assertMetadata( + _ part: RPCResponsePart?, + metadataHandler: (Metadata) -> Void = { _ in } + ) { + switch part { + case .some(.metadata(let metadata)): + metadataHandler(metadata) + default: + Issue.record("Expected '.metadata' but found '\(String(describing: part))'") + } + } + + func assertMessage( + _ part: RPCResponsePart?, + messageHandler: ([UInt8]) -> Void = { _ in } + ) { + switch part { + case .some(.message(let message)): + messageHandler(message) + default: + Issue.record("Expected '.message' but found '\(String(describing: part))'") + } + } + + func assertStatus( + _ part: RPCResponsePart?, + statusHandler: (Status, Metadata) -> Void = { _, _ in } + ) { + switch part { + case .some(.status(let status, let metadata)): + statusHandler(status, metadata) + default: + Issue.record("Expected '.status' but found '\(String(describing: part))'") + } + } +} diff --git a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift index 4356fdc1f..fdb869d1c 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Call/Server/ServerInterceptors.swift @@ -22,13 +22,13 @@ extension ServerInterceptor where Self == RejectAllServerInterceptor { } static func throwError(_ error: RPCError) -> Self { - return RejectAllServerInterceptor(error: error, throw: true) + RejectAllServerInterceptor(error: error, throw: true) } } extension ServerInterceptor where Self == RequestCountingServerInterceptor { static func requestCounter(_ counter: AtomicCounter) -> Self { - return RequestCountingServerInterceptor(counter: counter) + RequestCountingServerInterceptor(counter: counter) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index 3859eec24..8d0ece3c7 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -14,9 +14,10 @@ * limitations under the License. */ import GRPCCore -import XCTest struct BinaryEcho: RegistrableRPCService { + static let serviceDescriptor = ServiceDescriptor(package: "echo", service: "Echo") + func get( _ request: ServerRequest<[UInt8]> ) async throws -> ServerResponse<[UInt8]> { @@ -96,9 +97,21 @@ struct BinaryEcho: RegistrableRPCService { } enum Methods { - static let get = MethodDescriptor(service: "echo.Echo", method: "Get") - static let collect = MethodDescriptor(service: "echo.Echo", method: "Collect") - static let expand = MethodDescriptor(service: "echo.Echo", method: "Expand") - static let update = MethodDescriptor(service: "echo.Echo", method: "Update") + static let get = MethodDescriptor( + service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + method: "Get" + ) + static let collect = MethodDescriptor( + service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + method: "Collect" + ) + static let expand = MethodDescriptor( + service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + method: "Expand" + ) + static let update = MethodDescriptor( + service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + method: "Update" + ) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift new file mode 100644 index 000000000..01501e0bb --- /dev/null +++ b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift @@ -0,0 +1,49 @@ +/* + * 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 + +struct HelloWorld: RegistrableRPCService { + static let serviceDescriptor = ServiceDescriptor(package: "helloworld", service: "HelloWorld") + + func sayHello( + _ request: ServerRequest<[UInt8]> + ) async throws -> ServerResponse<[UInt8]> { + let name = String(bytes: request.message, encoding: .utf8) ?? "world" + return ServerResponse(message: Array("Hello, \(name)!".utf8), metadata: []) + } + + func registerMethods(with router: inout RPCRouter) { + let serializer = IdentitySerializer() + let deserializer = IdentityDeserializer() + + router.registerHandler( + forMethod: Methods.sayHello, + deserializer: deserializer, + serializer: serializer + ) { streamRequest, context in + let singleRequest = try await ServerRequest(stream: streamRequest) + let singleResponse = try await self.sayHello(singleRequest) + return StreamingServerResponse(single: singleResponse) + } + } + + enum Methods { + static let sayHello = MethodDescriptor( + service: HelloWorld.serviceDescriptor.fullyQualifiedService, + method: "SayHello" + ) + } +} From 02fa77fe1d6419091f6adf9b753f8414f9ee0a87 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 13 Nov 2024 20:44:58 +0000 Subject: [PATCH 495/580] Make structured Swift representation types Sendable (#2115) Motivation: The structured Swift types aren't marked as Sendable. This means that defining a 'static let' isn't possible, and a static computed property must be used instead. This is fine, just verbose. Modifications: - Make all structured swift representation types 'Sendable' Result: Can use 'static let' with structured Swift types --- .../StructuredSwiftRepresentation.swift | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index c6171f72f..bc61819d3 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -30,7 +30,7 @@ /// A description of an import declaration. /// /// For example: `import Foo`. -struct ImportDescription: Equatable, Codable { +struct ImportDescription: Equatable, Codable, Sendable { /// The access level of the imported module. /// /// For example, the `public` in `public import Foo`. @@ -62,7 +62,7 @@ struct ImportDescription: Equatable, Codable { var item: Item? = nil /// Describes any requirement for the `@preconcurrency` attribute. - enum PreconcurrencyRequirement: Equatable, Codable { + enum PreconcurrencyRequirement: Equatable, Codable, Sendable { /// The attribute is always required. case always /// The attribute is not required. @@ -72,7 +72,7 @@ struct ImportDescription: Equatable, Codable { } /// Represents an item imported from a module. - struct Item: Equatable, Codable { + struct Item: Equatable, Codable, Sendable { /// The keyword that specifies the item's kind (e.g. `func`, `struct`). var kind: Kind @@ -85,7 +85,7 @@ struct ImportDescription: Equatable, Codable { } } - enum Kind: String, Equatable, Codable { + enum Kind: String, Equatable, Codable, Sendable { case `typealias` case `struct` case `class` @@ -120,7 +120,7 @@ internal enum AccessModifier: String, Sendable, Equatable, Codable { /// A description of a comment. /// /// For example `/// Hello`. -enum Comment: Equatable, Codable { +enum Comment: Equatable, Codable, Sendable { /// An inline comment. /// @@ -150,7 +150,7 @@ enum Comment: Equatable, Codable { /// A description of a literal. /// /// For example `"hello"` or `42`. -enum LiteralDescription: Equatable, Codable { +enum LiteralDescription: Equatable, Codable, Sendable { /// A string literal. /// @@ -189,7 +189,7 @@ enum LiteralDescription: Equatable, Codable { /// A description of an identifier, such as a variable name. /// /// For example, in `let foo = 42`, `foo` is an identifier. -enum IdentifierDescription: Equatable, Codable { +enum IdentifierDescription: Equatable, Codable, Sendable { /// A pattern identifier. /// @@ -205,7 +205,7 @@ enum IdentifierDescription: Equatable, Codable { /// A description of a member access expression. /// /// For example `foo.bar`. -struct MemberAccessDescription: Equatable, Codable { +struct MemberAccessDescription: Equatable, Codable, Sendable { /// The expression of which a member `right` is accessed. /// @@ -221,7 +221,7 @@ struct MemberAccessDescription: Equatable, Codable { /// A description of a function argument. /// /// For example in `foo(bar: 42)`, the function argument is `bar: 42`. -struct FunctionArgumentDescription: Equatable, Codable { +struct FunctionArgumentDescription: Equatable, Codable, Sendable { /// An optional label of the function argument. /// @@ -237,7 +237,7 @@ struct FunctionArgumentDescription: Equatable, Codable { /// A description of a function call. /// /// For example `foo(bar: 42)`. -struct FunctionCallDescription: Equatable, Codable { +struct FunctionCallDescription: Equatable, Codable, Sendable { /// The expression that returns the function to be called. /// @@ -284,7 +284,7 @@ struct FunctionCallDescription: Equatable, Codable { } /// A type of a variable binding: `let` or `var`. -enum BindingKind: Equatable, Codable { +enum BindingKind: Equatable, Codable, Sendable { /// A mutable variable. case `var` @@ -296,7 +296,7 @@ enum BindingKind: Equatable, Codable { /// A description of a variable declaration. /// /// For example `let foo = 42`. -struct VariableDescription: Equatable, Codable { +struct VariableDescription: Equatable, Codable, Sendable { /// An access modifier. var accessModifier: AccessModifier? @@ -346,7 +346,7 @@ struct VariableDescription: Equatable, Codable { } /// A requirement of a where clause. -enum WhereClauseRequirement: Equatable, Codable { +enum WhereClauseRequirement: Equatable, Codable, Sendable { /// A conformance requirement. /// @@ -357,7 +357,7 @@ enum WhereClauseRequirement: Equatable, Codable { /// A description of a where clause. /// /// For example: `extension Array where Element: Foo {`. -struct WhereClause: Equatable, Codable { +struct WhereClause: Equatable, Codable, Sendable { /// One or more requirements to be added after the `where` keyword. var requirements: [WhereClauseRequirement] @@ -366,7 +366,7 @@ struct WhereClause: Equatable, Codable { /// A description of an extension declaration. /// /// For example `extension Foo {`. -struct ExtensionDescription: Equatable, Codable { +struct ExtensionDescription: Equatable, Codable, Sendable { /// An access modifier. var accessModifier: AccessModifier? = nil @@ -391,7 +391,7 @@ struct ExtensionDescription: Equatable, Codable { /// A description of a struct declaration. /// /// For example `struct Foo {`. -struct StructDescription: Equatable, Codable { +struct StructDescription: Equatable, Codable, Sendable { /// An access modifier. var accessModifier: AccessModifier? = nil @@ -413,7 +413,7 @@ struct StructDescription: Equatable, Codable { /// A description of an enum declaration. /// /// For example `enum Bar {`. -struct EnumDescription: Equatable, Codable { +struct EnumDescription: Equatable, Codable, Sendable { /// A Boolean value that indicates whether the enum has a `@frozen` /// attribute. @@ -441,7 +441,7 @@ struct EnumDescription: Equatable, Codable { } /// A description of a type reference. -indirect enum ExistingTypeDescription: Equatable, Codable { +indirect enum ExistingTypeDescription: Equatable, Codable, Sendable { /// A type with the `any` keyword in front of it. /// @@ -488,7 +488,7 @@ indirect enum ExistingTypeDescription: Equatable, Codable { /// A description of a typealias declaration. /// /// For example `typealias Foo = Int`. -struct TypealiasDescription: Equatable, Codable { +struct TypealiasDescription: Equatable, Codable, Sendable { /// An access modifier. var accessModifier: AccessModifier? @@ -507,7 +507,7 @@ struct TypealiasDescription: Equatable, Codable { /// A description of a protocol declaration. /// /// For example `protocol Foo {`. -struct ProtocolDescription: Equatable, Codable { +struct ProtocolDescription: Equatable, Codable, Sendable { /// An access modifier. var accessModifier: AccessModifier? = nil @@ -531,7 +531,7 @@ struct ProtocolDescription: Equatable, Codable { /// /// For example, in `func foo(bar baz: String = "hi")`, the parameter /// description represents `bar baz: String = "hi"` -struct ParameterDescription: Equatable, Codable { +struct ParameterDescription: Equatable, Codable, Sendable { /// An external parameter label. /// @@ -561,7 +561,7 @@ struct ParameterDescription: Equatable, Codable { } /// A function kind: `func` or `init`. -enum FunctionKind: Equatable, Codable { +enum FunctionKind: Equatable, Codable, Sendable { /// An initializer. /// @@ -578,7 +578,7 @@ enum FunctionKind: Equatable, Codable { } /// A function keyword, such as `async` and `throws`. -enum FunctionKeyword: Equatable, Codable { +enum FunctionKeyword: Equatable, Codable, Sendable { /// An asynchronous function. case `async` @@ -593,7 +593,7 @@ enum FunctionKeyword: Equatable, Codable { /// A description of a function signature. /// /// For example: `func foo(bar: String) async throws -> Int`. -struct FunctionSignatureDescription: Equatable, Codable { +struct FunctionSignatureDescription: Equatable, Codable, Sendable { /// An access modifier. var accessModifier: AccessModifier? = nil @@ -620,7 +620,7 @@ struct FunctionSignatureDescription: Equatable, Codable { /// A description of a function definition. /// /// For example: `func foo() { }`. -struct FunctionDescription: Equatable, Codable { +struct FunctionDescription: Equatable, Codable, Sendable { /// The signature of the function. var signature: FunctionSignatureDescription @@ -703,7 +703,7 @@ struct FunctionDescription: Equatable, Codable { /// A description of a closure signature. /// /// For example: `(String) async throws -> Int`. -struct ClosureSignatureDescription: Equatable, Codable { +struct ClosureSignatureDescription: Equatable, Codable, Sendable { /// The parameters of the function. var parameters: [ParameterDescription] = [] @@ -724,7 +724,7 @@ struct ClosureSignatureDescription: Equatable, Codable { /// /// For example, in `case foo(bar: String)`, the associated value /// represents `bar: String`. -struct EnumCaseAssociatedValueDescription: Equatable, Codable { +struct EnumCaseAssociatedValueDescription: Equatable, Codable, Sendable { /// A variable label. /// @@ -740,7 +740,7 @@ struct EnumCaseAssociatedValueDescription: Equatable, Codable { /// An enum case kind. /// /// For example: `case foo` versus `case foo(String)`, and so on. -enum EnumCaseKind: Equatable, Codable { +enum EnumCaseKind: Equatable, Codable, Sendable { /// A case with only a name. /// @@ -761,7 +761,7 @@ enum EnumCaseKind: Equatable, Codable { /// A description of an enum case. /// /// For example: `case foo(String)`. -struct EnumCaseDescription: Equatable, Codable { +struct EnumCaseDescription: Equatable, Codable, Sendable { /// The name of the enum case. /// @@ -773,7 +773,7 @@ struct EnumCaseDescription: Equatable, Codable { } /// A declaration of a Swift entity. -indirect enum Declaration: Equatable, Codable { +indirect enum Declaration: Equatable, Codable, Sendable { /// A declaration that adds a comment on top of the provided declaration. case commentable(Comment?, Declaration) @@ -812,7 +812,7 @@ indirect enum Declaration: Equatable, Codable { /// A description of a deprecation notice. /// /// For example: `@available(*, deprecated, message: "This is going away", renamed: "other(param:)")` -struct DeprecationDescription: Equatable, Codable { +struct DeprecationDescription: Equatable, Codable, Sendable { /// A message used by the deprecation attribute. var message: String? @@ -824,7 +824,7 @@ struct DeprecationDescription: Equatable, Codable { /// A description of an availability guard. /// /// For example: `@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)` -struct AvailabilityDescription: Equatable, Codable { +struct AvailabilityDescription: Equatable, Codable, Sendable { /// The array of OSes and versions which are specified in the availability guard. var osVersions: [OSVersion] init(osVersions: [OSVersion]) { @@ -832,7 +832,7 @@ struct AvailabilityDescription: Equatable, Codable { } /// An OS and its version. - struct OSVersion: Equatable, Codable { + struct OSVersion: Equatable, Codable, Sendable { var os: OS var version: String init(os: OS, version: String) { @@ -843,7 +843,7 @@ struct AvailabilityDescription: Equatable, Codable { /// One of the possible OSes. // swift-format-ignore: DontRepeatTypeInStaticProperties - struct OS: Equatable, Codable { + struct OS: Equatable, Codable, Sendable { var name: String init(name: String) { @@ -861,7 +861,7 @@ struct AvailabilityDescription: Equatable, Codable { /// A description of an assignment expression. /// /// For example: `foo = 42`. -struct AssignmentDescription: Equatable, Codable { +struct AssignmentDescription: Equatable, Codable, Sendable { /// The left-hand side expression, the variable to assign to. /// @@ -875,7 +875,7 @@ struct AssignmentDescription: Equatable, Codable { } /// A switch case kind, either a `case` or a `default`. -enum SwitchCaseKind: Equatable, Codable { +enum SwitchCaseKind: Equatable, Codable, Sendable { /// A case. /// @@ -894,7 +894,7 @@ enum SwitchCaseKind: Equatable, Codable { /// A description of a switch case definition. /// /// For example: `case foo: print("foo")`. -struct SwitchCaseDescription: Equatable, Codable { +struct SwitchCaseDescription: Equatable, Codable, Sendable { /// The kind of the switch case. var kind: SwitchCaseKind @@ -909,7 +909,7 @@ struct SwitchCaseDescription: Equatable, Codable { /// A description of a switch statement expression. /// /// For example: `switch foo {`. -struct SwitchDescription: Equatable, Codable { +struct SwitchDescription: Equatable, Codable, Sendable { /// The expression evaluated by the switch statement. /// @@ -924,7 +924,7 @@ struct SwitchDescription: Equatable, Codable { /// /// For example: in `if foo { bar }`, the condition pair represents /// `foo` + `bar`. -struct IfBranch: Equatable, Codable { +struct IfBranch: Equatable, Codable, Sendable { /// The expressions evaluated by the if statement and their corresponding /// body blocks. If more than one is provided, an `else if` branch is added. @@ -941,7 +941,7 @@ struct IfBranch: Equatable, Codable { /// A description of an if[[/elseif]/else] statement expression. /// /// For example: `if foo { } else if bar { } else { }`. -struct IfStatementDescription: Equatable, Codable { +struct IfStatementDescription: Equatable, Codable, Sendable { /// The primary `if` branch. var ifBranch: IfBranch @@ -958,7 +958,7 @@ struct IfStatementDescription: Equatable, Codable { /// A description of a do statement. /// /// For example: `do { try foo() } catch { return bar }`. -struct DoStatementDescription: Equatable, Codable { +struct DoStatementDescription: Equatable, Codable, Sendable { /// The code blocks in the `do` statement body. /// @@ -978,7 +978,7 @@ struct DoStatementDescription: Equatable, Codable { /// A description of a value binding used in enums with associated values. /// /// For example: `let foo(bar)`. -struct ValueBindingDescription: Equatable, Codable { +struct ValueBindingDescription: Equatable, Codable, Sendable { /// The binding kind: `let` or `var`. var kind: BindingKind @@ -990,7 +990,7 @@ struct ValueBindingDescription: Equatable, Codable { } /// A kind of a keyword. -enum KeywordKind: Equatable, Codable { +enum KeywordKind: Equatable, Codable, Sendable { /// The return keyword. case `return` @@ -1009,7 +1009,7 @@ enum KeywordKind: Equatable, Codable { } /// A description of an expression that places a keyword before an expression. -struct UnaryKeywordDescription: Equatable, Codable { +struct UnaryKeywordDescription: Equatable, Codable, Sendable { /// The keyword to place before the expression. /// @@ -1025,7 +1025,7 @@ struct UnaryKeywordDescription: Equatable, Codable { /// A description of a closure invocation. /// /// For example: `{ foo in return foo + "bar" }`. -struct ClosureInvocationDescription: Equatable, Codable { +struct ClosureInvocationDescription: Equatable, Codable, Sendable { /// The names of the arguments taken by the closure. /// @@ -1043,7 +1043,7 @@ struct ClosureInvocationDescription: Equatable, Codable { /// A binary operator. /// /// For example: `+=` in `a += b`. -enum BinaryOperator: String, Equatable, Codable { +enum BinaryOperator: String, Equatable, Codable, Sendable { /// The += operator, adds and then assigns another value. case plusEquals = "+=" @@ -1061,7 +1061,7 @@ enum BinaryOperator: String, Equatable, Codable { /// A description of a binary operation expression. /// /// For example: `foo += 1`. -struct BinaryOperationDescription: Equatable, Codable { +struct BinaryOperationDescription: Equatable, Codable, Sendable { /// The left-hand side expression of the operation. /// @@ -1083,7 +1083,7 @@ struct BinaryOperationDescription: Equatable, Codable { /// reference to a variable. /// /// For example, `&foo` passes a reference to the `foo` variable. -struct InOutDescription: Equatable, Codable { +struct InOutDescription: Equatable, Codable, Sendable { /// The referenced expression. /// @@ -1094,7 +1094,7 @@ struct InOutDescription: Equatable, Codable { /// A description of an optional chaining expression. /// /// For example, in `foo?`, `referencedExpr` is `foo`. -struct OptionalChainingDescription: Equatable, Codable { +struct OptionalChainingDescription: Equatable, Codable, Sendable { /// The referenced expression. /// @@ -1105,7 +1105,7 @@ struct OptionalChainingDescription: Equatable, Codable { /// A description of a tuple. /// /// For example: `(foo, bar)`. -struct TupleDescription: Equatable, Codable { +struct TupleDescription: Equatable, Codable, Sendable { /// The member expressions. /// @@ -1114,7 +1114,7 @@ struct TupleDescription: Equatable, Codable { } /// A Swift expression. -indirect enum Expression: Equatable, Codable { +indirect enum Expression: Equatable, Codable, Sendable { /// A literal. /// @@ -1191,7 +1191,7 @@ indirect enum Expression: Equatable, Codable { } /// A code block item, either a declaration or an expression. -enum CodeBlockItem: Equatable, Codable { +enum CodeBlockItem: Equatable, Codable, Sendable { /// A declaration, such as of a new type or function. case declaration(Declaration) @@ -1201,7 +1201,7 @@ enum CodeBlockItem: Equatable, Codable { } /// A code block, with an optional comment. -struct CodeBlock: Equatable, Codable { +struct CodeBlock: Equatable, Codable, Sendable { /// The comment to prepend to the code block item. var comment: Comment? @@ -1211,7 +1211,7 @@ struct CodeBlock: Equatable, Codable { } /// A description of a Swift file. -struct FileDescription: Equatable, Codable { +struct FileDescription: Equatable, Codable, Sendable { /// A comment placed at the top of the file. var topComment: Comment? @@ -1225,7 +1225,7 @@ struct FileDescription: Equatable, Codable { } /// A description of a named Swift file. -struct NamedFileDescription: Equatable, Codable { +struct NamedFileDescription: Equatable, Codable, Sendable { /// A file name, including the file extension. /// @@ -1237,7 +1237,7 @@ struct NamedFileDescription: Equatable, Codable { } /// A file with contents made up of structured Swift code. -struct StructuredSwiftRepresentation: Equatable, Codable { +struct StructuredSwiftRepresentation: Equatable, Codable, Sendable { /// The contents of the file. var file: NamedFileDescription From 6bcdbb225071c74dcc42b108830cd42309bdac26 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 14 Nov 2024 08:08:00 +0000 Subject: [PATCH 496/580] Don't render just spaces in a line (#2114) Motivation: If a blank line is render, the text based renderer will include the current indentation level resulting in lines with just spaces. Modifications: - Don't include indentation if the line to render is otherwise empty Result: Less trailing whitespace. --- .../Internal/Renderer/TextBasedRenderer.swift | 3 ++ ...lientCodeTranslatorSnippetBasedTests.swift | 36 +++++++++---------- ...erverCodeTranslatorSnippetBasedTests.swift | 6 ++-- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index fe7f038a6..9986164a9 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -70,6 +70,9 @@ final class StringCodeWriter { if nextWriteAppendsToLastLine && !lines.isEmpty { let existingLine = lines.removeLast() newLine = existingLine + line + } else if line.isEmpty { + // Skip indentation to avoid trailing whitespace on blank lines. + newLine = line } else { let indentation = Array(repeating: " ", count: self.indentation * level).joined() newLine = indentation + line diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 129db4c26..67d97edc0 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -98,11 +98,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Documentation for MethodA public func methodA( request: GRPCCore.ClientRequest, @@ -205,11 +205,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Documentation for MethodA public func methodA( request: GRPCCore.StreamingClientRequest, @@ -308,11 +308,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Documentation for MethodA public func methodA( request: GRPCCore.ClientRequest, @@ -409,11 +409,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Documentation for MethodA public func methodA( request: GRPCCore.StreamingClientRequest, @@ -477,7 +477,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { options: GRPCCore.CallOptions, _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R ) async throws -> R where R: Sendable - + /// Documentation for MethodB func methodB( request: GRPCCore.ClientRequest, @@ -504,7 +504,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { body ) } - + package func methodB( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, @@ -540,7 +540,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { handleResponse ) } - + /// Documentation for MethodB package func methodB( _ message: NamespaceA_ServiceARequest, @@ -563,11 +563,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - + package init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Documentation for MethodA package func methodA( request: GRPCCore.StreamingClientRequest, @@ -587,7 +587,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { handler: body ) } - + /// Documentation for MethodB package func methodB( request: GRPCCore.ClientRequest, @@ -688,11 +688,11 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal struct ServiceA_Client: ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - + internal init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - + /// Documentation for MethodA internal func methodA( request: GRPCCore.ClientRequest, @@ -758,7 +758,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } @@ -780,7 +780,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct ServiceB_Client: ServiceB.ClientProtocol { private let client: GRPCCore.GRPCClient - + public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index d2b0f624c..6afe4c3f4 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -408,7 +408,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext ) async throws -> GRPCCore.StreamingServerResponse - + /// Documentation for outputStreamingMethod func outputStreaming( request: GRPCCore.StreamingServerRequest, @@ -452,7 +452,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse - + /// Documentation for outputStreamingMethod func outputStreaming( request: GRPCCore.ServerRequest, @@ -472,7 +472,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ) return GRPCCore.StreamingServerResponse(single: response) } - + internal func outputStreaming( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext From c41e0a7c95969e5fb325b5e7c28b90a650aa3d39 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 14 Nov 2024 08:13:58 +0000 Subject: [PATCH 497/580] Simplify naming of code gen types (#2116) Motivation: A number of types are nested within 'CodeGenerationRequest' which make them verbose to type out and a bit of a pain to work with. Modification: Remove the nesting of types. Result: Easier types to work with. --- .../GRPCCodeGen/CodeGenerationRequest.swift | 436 +++++++++--------- .../Translator/ClientCodeTranslator.swift | 46 +- .../IDLToStructuredSwiftTranslator.swift | 12 +- .../Translator/ServerCodeTranslator.swift | 40 +- .../Translator/TypealiasTranslator.swift | 24 +- ...lientCodeTranslatorSnippetBasedTests.swift | 4 - ...uredSwiftTranslatorSnippetBasedTests.swift | 44 +- ...erverCodeTranslatorSnippetBasedTests.swift | 4 - .../Internal/Translator/TestFunctions.swift | 4 +- ...TypealiasTranslatorSnippetBasedTests.swift | 4 - 10 files changed, 301 insertions(+), 317 deletions(-) diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 700f33866..432469d74 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -79,123 +79,68 @@ public struct CodeGenerationRequest { self.lookupSerializer = lookupSerializer self.lookupDeserializer = lookupDeserializer } +} - /// Represents an import: a module or a specific item from a module. - public struct Dependency: Equatable { - /// If the dependency is an item, the property's value is the item representation. - /// If the dependency is a module, this property is nil. - public var item: Item? - - /// The access level to be included in imports of this dependency. - public var accessLevel: SourceGenerator.Config.AccessLevel - - /// The name of the imported module or of the module an item is imported from. - public var module: String - - /// The name of the private interface for an `@_spi` import. - /// - /// For example, if `spi` was "Secret" and the module name was "Foo" then the import - /// would be `@_spi(Secret) import Foo`. - public var spi: String? - - /// Requirements for the `@preconcurrency` attribute. - public var preconcurrency: PreconcurrencyRequirement - - public init( - item: Item? = nil, - module: String, - spi: String? = nil, - preconcurrency: PreconcurrencyRequirement = .notRequired, - accessLevel: SourceGenerator.Config.AccessLevel - ) { - self.item = item - self.module = module - self.spi = spi - self.preconcurrency = preconcurrency - self.accessLevel = accessLevel - } +/// Represents an import: a module or a specific item from a module. +public struct Dependency: Equatable { + /// If the dependency is an item, the property's value is the item representation. + /// If the dependency is a module, this property is nil. + public var item: Item? - /// Represents an item imported from a module. - public struct Item: Equatable { - /// The keyword that specifies the item's kind (e.g. `func`, `struct`). - public var kind: Kind + /// The access level to be included in imports of this dependency. + public var accessLevel: SourceGenerator.Config.AccessLevel - /// The name of the imported item. - public var name: String + /// The name of the imported module or of the module an item is imported from. + public var module: String - public init(kind: Kind, name: String) { - self.kind = kind - self.name = name - } + /// The name of the private interface for an `@_spi` import. + /// + /// For example, if `spi` was "Secret" and the module name was "Foo" then the import + /// would be `@_spi(Secret) import Foo`. + public var spi: String? - /// Represents the imported item's kind. - public struct Kind: Equatable { - /// Describes the keyword associated with the imported item. - internal enum Value: String { - case `typealias` - case `struct` - case `class` - case `enum` - case `protocol` - case `let` - case `var` - case `func` - } - - internal var value: Value - - internal init(_ value: Value) { - self.value = value - } - - /// The imported item is a typealias. - public static var `typealias`: Self { - Self(.`typealias`) - } - - /// The imported item is a struct. - public static var `struct`: Self { - Self(.`struct`) - } - - /// The imported item is a class. - public static var `class`: Self { - Self(.`class`) - } - - /// The imported item is an enum. - public static var `enum`: Self { - Self(.`enum`) - } - - /// The imported item is a protocol. - public static var `protocol`: Self { - Self(.`protocol`) - } - - /// The imported item is a let. - public static var `let`: Self { - Self(.`let`) - } - - /// The imported item is a var. - public static var `var`: Self { - Self(.`var`) - } - - /// The imported item is a function. - public static var `func`: Self { - Self(.`func`) - } - } + /// Requirements for the `@preconcurrency` attribute. + public var preconcurrency: PreconcurrencyRequirement + + public init( + item: Item? = nil, + module: String, + spi: String? = nil, + preconcurrency: PreconcurrencyRequirement = .notRequired, + accessLevel: SourceGenerator.Config.AccessLevel + ) { + self.item = item + self.module = module + self.spi = spi + self.preconcurrency = preconcurrency + self.accessLevel = accessLevel + } + + /// Represents an item imported from a module. + public struct Item: Equatable { + /// The keyword that specifies the item's kind (e.g. `func`, `struct`). + public var kind: Kind + + /// The name of the imported item. + public var name: String + + public init(kind: Kind, name: String) { + self.kind = kind + self.name = name } - /// Describes any requirement for the `@preconcurrency` attribute. - public struct PreconcurrencyRequirement: Equatable { - internal enum Value: Equatable { - case required - case notRequired - case requiredOnOS([String]) + /// Represents the imported item's kind. + public struct Kind: Equatable { + /// Describes the keyword associated with the imported item. + internal enum Value: String { + case `typealias` + case `struct` + case `class` + case `enum` + case `protocol` + case `let` + case `var` + case `func` } internal var value: Value @@ -204,130 +149,185 @@ public struct CodeGenerationRequest { self.value = value } - /// The attribute is always required. - public static var required: Self { - Self(.required) + /// The imported item is a typealias. + public static var `typealias`: Self { + Self(.`typealias`) + } + + /// The imported item is a struct. + public static var `struct`: Self { + Self(.`struct`) + } + + /// The imported item is a class. + public static var `class`: Self { + Self(.`class`) + } + + /// The imported item is an enum. + public static var `enum`: Self { + Self(.`enum`) + } + + /// The imported item is a protocol. + public static var `protocol`: Self { + Self(.`protocol`) + } + + /// The imported item is a let. + public static var `let`: Self { + Self(.`let`) } - /// The attribute is not required. - public static var notRequired: Self { - Self(.notRequired) + /// The imported item is a var. + public static var `var`: Self { + Self(.`var`) } - /// The attribute is required only on the named operating systems. - public static func requiredOnOS(_ OSs: [String]) -> PreconcurrencyRequirement { - return Self(.requiredOnOS(OSs)) + /// The imported item is a function. + public static var `func`: Self { + Self(.`func`) } } } - /// Represents a service described in an IDL file. - public struct ServiceDescriptor: Hashable { - /// Documentation from comments above the IDL service description. - /// It is already formatted, meaning it contains "///" and new lines. - public var documentation: String - - /// The service name in different formats. - /// - /// All properties of this object must be unique for each service from within a namespace. - public var name: Name - - /// The service namespace in different formats. - /// - /// All different services from within the same namespace must have - /// the same ``Name`` object as this property. - /// For `.proto` files the base name of this object is the package name. - public var namespace: Name - - /// A description of each method of a service. - /// - /// - SeeAlso: ``MethodDescriptor``. - public var methods: [MethodDescriptor] - - public init( - documentation: String, - name: Name, - namespace: Name, - methods: [MethodDescriptor] - ) { - self.documentation = documentation - self.name = name - self.namespace = namespace - self.methods = methods + /// Describes any requirement for the `@preconcurrency` attribute. + public struct PreconcurrencyRequirement: Equatable { + internal enum Value: Equatable { + case required + case notRequired + case requiredOnOS([String]) } - /// Represents a method described in an IDL file. - public struct MethodDescriptor: Hashable { - /// Documentation from comments above the IDL method description. - /// It is already formatted, meaning it contains "///" and new lines. - public var documentation: String - - /// Method name in different formats. - /// - /// All properties of this object must be unique for each method - /// from within a service. - public var name: Name - - /// Identifies if the method is input streaming. - public var isInputStreaming: Bool - - /// Identifies if the method is output streaming. - public var isOutputStreaming: Bool - - /// The generated input type for the described method. - public var inputType: String - - /// The generated output type for the described method. - public var outputType: String - - public init( - documentation: String, - name: Name, - isInputStreaming: Bool, - isOutputStreaming: Bool, - inputType: String, - outputType: String - ) { - self.documentation = documentation - self.name = name - self.isInputStreaming = isInputStreaming - self.isOutputStreaming = isOutputStreaming - self.inputType = inputType - self.outputType = outputType - } + internal var value: Value + + internal init(_ value: Value) { + self.value = value + } + + /// The attribute is always required. + public static var required: Self { + Self(.required) + } + + /// The attribute is not required. + public static var notRequired: Self { + Self(.notRequired) } - } - /// Represents the name associated with a namespace, service or a method, in three different formats. - public struct Name: Hashable { - /// The base name is the name used for the namespace/service/method in the IDL file, so it should follow - /// the specific casing of the IDL. - /// - /// The base name is also used in the descriptors that identify a specific method or service : - /// `..`. - public var base: String - - /// The `generatedUpperCase` name is used in the generated code. It is expected - /// to be the UpperCamelCase version of the base name - /// - /// For example, if `base` is "fooBar", then `generatedUpperCase` is "FooBar". - public var generatedUpperCase: String - - /// The `generatedLowerCase` name is used in the generated code. It is expected - /// to be the lowerCamelCase version of the base name - /// - /// For example, if `base` is "FooBar", then `generatedLowerCase` is "fooBar". - public var generatedLowerCase: String - - public init(base: String, generatedUpperCase: String, generatedLowerCase: String) { - self.base = base - self.generatedUpperCase = generatedUpperCase - self.generatedLowerCase = generatedLowerCase + /// The attribute is required only on the named operating systems. + public static func requiredOnOS(_ OSs: [String]) -> PreconcurrencyRequirement { + return Self(.requiredOnOS(OSs)) } } } -extension CodeGenerationRequest.Name { +/// Represents a service described in an IDL file. +public struct ServiceDescriptor: Hashable { + /// Documentation from comments above the IDL service description. + /// It is already formatted, meaning it contains "///" and new lines. + public var documentation: String + + /// The service name in different formats. + /// + /// All properties of this object must be unique for each service from within a namespace. + public var name: Name + + /// The service namespace in different formats. + /// + /// All different services from within the same namespace must have + /// the same ``Name`` object as this property. + /// For `.proto` files the base name of this object is the package name. + public var namespace: Name + + /// A description of each method of a service. + /// + /// - SeeAlso: ``MethodDescriptor``. + public var methods: [MethodDescriptor] + + public init( + documentation: String, + name: Name, + namespace: Name, + methods: [MethodDescriptor] + ) { + self.documentation = documentation + self.name = name + self.namespace = namespace + self.methods = methods + } +} + +/// Represents a method described in an IDL file. +public struct MethodDescriptor: Hashable { + /// Documentation from comments above the IDL method description. + /// It is already formatted, meaning it contains "///" and new lines. + public var documentation: String + + /// Method name in different formats. + /// + /// All properties of this object must be unique for each method + /// from within a service. + public var name: Name + + /// Identifies if the method is input streaming. + public var isInputStreaming: Bool + + /// Identifies if the method is output streaming. + public var isOutputStreaming: Bool + + /// The generated input type for the described method. + public var inputType: String + + /// The generated output type for the described method. + public var outputType: String + + public init( + documentation: String, + name: Name, + isInputStreaming: Bool, + isOutputStreaming: Bool, + inputType: String, + outputType: String + ) { + self.documentation = documentation + self.name = name + self.isInputStreaming = isInputStreaming + self.isOutputStreaming = isOutputStreaming + self.inputType = inputType + self.outputType = outputType + } +} + +/// Represents the name associated with a namespace, service or a method, in three different formats. +public struct Name: Hashable { + /// The base name is the name used for the namespace/service/method in the IDL file, so it should follow + /// the specific casing of the IDL. + /// + /// The base name is also used in the descriptors that identify a specific method or service : + /// `..`. + public var base: String + + /// The `generatedUpperCase` name is used in the generated code. It is expected + /// to be the UpperCamelCase version of the base name + /// + /// For example, if `base` is "fooBar", then `generatedUpperCase` is "FooBar". + public var generatedUpperCase: String + + /// The `generatedLowerCase` name is used in the generated code. It is expected + /// to be the lowerCamelCase version of the base name + /// + /// For example, if `base` is "FooBar", then `generatedLowerCase` is "fooBar". + public var generatedLowerCase: String + + public init(base: String, generatedUpperCase: String, generatedLowerCase: String) { + self.base = base + self.generatedUpperCase = generatedUpperCase + self.generatedLowerCase = generatedLowerCase + } +} + +extension Name { /// The base name replacing occurrences of "." with "_". /// /// For example, if `base` is "Foo.Bar", then `normalizedBase` is "Foo_Bar". diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 1d935151f..3a628cf47 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -105,7 +105,7 @@ struct ClientCodeTranslator: SpecializedTranslator { extension ClientCodeTranslator { private func makeClientProtocol( - for service: CodeGenerationRequest.ServiceDescriptor, + for service: ServiceDescriptor, in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let methods = service.methods.map { @@ -130,7 +130,7 @@ extension ClientCodeTranslator { } private func makeDefaultImplementation( - for service: CodeGenerationRequest.ServiceDescriptor, + for service: ServiceDescriptor, in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let methods = service.methods.map { @@ -156,7 +156,7 @@ extension ClientCodeTranslator { } private func makeSugaredAPI( - forService service: CodeGenerationRequest.ServiceDescriptor, + forService service: ServiceDescriptor, request: CodeGenerationRequest ) -> Declaration { let sugaredAPIExtension = Declaration.extension( @@ -175,7 +175,7 @@ extension ClientCodeTranslator { } private func makeSugaredMethodDeclaration( - method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, + method: MethodDescriptor, accessModifier: AccessModifier? ) -> Declaration { let signature = FunctionSignatureDescription( @@ -205,7 +205,7 @@ extension ClientCodeTranslator { } private func makeParametersForSugaredMethodDeclaration( - method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + method: MethodDescriptor ) -> [ParameterDescription] { var parameters = [ParameterDescription]() @@ -295,7 +295,7 @@ extension ClientCodeTranslator { } private func makeFunctionBodyForSugaredMethodDeclaration( - method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + method: MethodDescriptor ) -> [CodeBlock] { // Produces the following: // @@ -375,8 +375,8 @@ extension ClientCodeTranslator { } private func makeClientProtocolMethod( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest, includeBody: Bool, accessModifier: AccessModifier? = nil, @@ -421,8 +421,8 @@ extension ClientCodeTranslator { } private func makeClientProtocolMethodCall( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest ) -> [CodeBlock] { let functionCall = Expression.functionCall( @@ -455,8 +455,8 @@ extension ClientCodeTranslator { } private func makeParameters( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest, includeSerializationParameters: Bool, includeDefaultCallOptions: Bool, @@ -487,8 +487,8 @@ extension ClientCodeTranslator { return parameters } private func clientRequestParameter( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + for method: MethodDescriptor, + in service: ServiceDescriptor ) -> ParameterDescription { let requestType = method.isInputStreaming ? "Streaming" : "" let clientRequestType = ExistingTypeDescription.member([ @@ -505,8 +505,8 @@ extension ClientCodeTranslator { } private func serializerParameter( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + for method: MethodDescriptor, + in service: ServiceDescriptor ) -> ParameterDescription { return ParameterDescription( label: "serializer", @@ -520,8 +520,8 @@ extension ClientCodeTranslator { } private func deserializerParameter( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + for method: MethodDescriptor, + in service: ServiceDescriptor ) -> ParameterDescription { return ParameterDescription( label: "deserializer", @@ -535,8 +535,8 @@ extension ClientCodeTranslator { } private func bodyParameter( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, includeDefaultResponseHandler: Bool ) -> ParameterDescription { let clientStreaming = method.isOutputStreaming ? "Streaming" : "" @@ -571,7 +571,7 @@ extension ClientCodeTranslator { } private func makeClientStruct( - for service: CodeGenerationRequest.ServiceDescriptor, + for service: ServiceDescriptor, in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let clientProperty = Declaration.variable( @@ -637,8 +637,8 @@ extension ClientCodeTranslator { } private func makeClientMethod( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let parameters = self.makeParameters( diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index e0899291f..2319e900f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -62,7 +62,7 @@ struct IDLToStructuredSwiftTranslator: Translator { } private func makeImports( - dependencies: [CodeGenerationRequest.Dependency], + dependencies: [Dependency], accessLevel: SourceGenerator.Config.AccessLevel, accessLevelOnImports: Bool ) throws -> [ImportDescription] { @@ -98,7 +98,7 @@ extension AccessModifier { extension IDLToStructuredSwiftTranslator { private func translateImport( - dependency: CodeGenerationRequest.Dependency, + dependency: Dependency, accessLevelOnImports: Bool ) throws -> ImportDescription { var importDescription = ImportDescription( @@ -146,7 +146,7 @@ extension IDLToStructuredSwiftTranslator { // Verify service enum names are unique. private func checkServiceEnumNamesAreUnique( - for servicesByGeneratedEnumName: [String: [CodeGenerationRequest.ServiceDescriptor]] + for servicesByGeneratedEnumName: [String: [ServiceDescriptor]] ) throws { for (generatedEnumName, services) in servicesByGeneratedEnumName { if services.count > 1 { @@ -163,7 +163,7 @@ extension IDLToStructuredSwiftTranslator { // Verify method names are unique within a service. private func checkMethodNamesAreUnique( - in service: CodeGenerationRequest.ServiceDescriptor + in service: ServiceDescriptor ) throws { // Check that the method descriptors are unique, by checking that the base names // of the methods of a specific service are unique. @@ -206,7 +206,7 @@ extension IDLToStructuredSwiftTranslator { } private func checkServiceDescriptorsAreUnique( - _ services: [CodeGenerationRequest.ServiceDescriptor] + _ services: [ServiceDescriptor] ) throws { var descriptors: Set = [] for service in services { @@ -227,7 +227,7 @@ extension IDLToStructuredSwiftTranslator { } } -extension CodeGenerationRequest.ServiceDescriptor { +extension ServiceDescriptor { var namespacedGeneratedName: String { if self.namespace.generatedUpperCase.isEmpty { return self.name.generatedUpperCase diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 100233d11..a08431ad9 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -107,7 +107,7 @@ struct ServerCodeTranslator: SpecializedTranslator { extension ServerCodeTranslator { private func makeStreamingProtocol( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> Declaration { let methods = service.methods.compactMap { Declaration.commentable( @@ -136,8 +136,8 @@ extension ServerCodeTranslator { } private func makeStreamingMethodSignature( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, accessModifier: AccessModifier? = nil ) -> FunctionSignatureDescription { return FunctionSignatureDescription( @@ -164,7 +164,7 @@ extension ServerCodeTranslator { } private func makeConformanceToRPCServiceExtension( - for service: CodeGenerationRequest.ServiceDescriptor, + for service: ServiceDescriptor, in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) @@ -179,7 +179,7 @@ extension ServerCodeTranslator { } private func makeRegisterRPCsMethod( - for service: CodeGenerationRequest.ServiceDescriptor, + for service: ServiceDescriptor, in codeGenerationRequest: CodeGenerationRequest ) -> Declaration { let registerRPCsSignature = FunctionSignatureDescription( @@ -202,7 +202,7 @@ extension ServerCodeTranslator { } private func makeRegisterRPCsMethodBody( - for service: CodeGenerationRequest.ServiceDescriptor, + for service: ServiceDescriptor, in codeGenerationRequest: CodeGenerationRequest ) -> [CodeBlock] { let registerHandlerCalls = service.methods.compactMap { @@ -224,8 +224,8 @@ extension ServerCodeTranslator { } private func makeArgumentsForRegisterHandler( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, from codeGenerationRequest: CodeGenerationRequest ) -> [FunctionArgumentDescription] { var arguments = [FunctionArgumentDescription]() @@ -284,7 +284,7 @@ extension ServerCodeTranslator { } private func makeServiceProtocol( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> Declaration { let methods = service.methods.compactMap { self.makeServiceProtocolMethod(for: $0, in: service) @@ -309,8 +309,8 @@ extension ServerCodeTranslator { } private func makeServiceProtocolMethod( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor, + for method: MethodDescriptor, + in service: ServiceDescriptor, accessModifier: AccessModifier? = nil ) -> Declaration { let inputStreaming = method.isInputStreaming ? "Streaming" : "" @@ -346,7 +346,7 @@ extension ServerCodeTranslator { } private func makeExtensionServiceProtocol( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> Declaration { let methods = service.methods.compactMap { self.makeServiceProtocolExtensionMethod(for: $0, in: service) @@ -363,8 +363,8 @@ extension ServerCodeTranslator { } private func makeServiceProtocolExtensionMethod( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + for method: MethodDescriptor, + in service: ServiceDescriptor ) -> Declaration? { // The method has the same definition in StreamingServiceProtocol and ServiceProtocol. if method.isInputStreaming && method.isOutputStreaming { @@ -385,7 +385,7 @@ extension ServerCodeTranslator { } private func makeResponse( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + for method: MethodDescriptor ) -> Declaration { let serverRequest: Expression if !method.isInputStreaming { @@ -422,7 +422,7 @@ extension ServerCodeTranslator { } private func makeReturnStatement( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor + for method: MethodDescriptor ) -> Expression { let returnValue: Expression // Transforming the unary response into a streaming one. @@ -447,8 +447,8 @@ extension ServerCodeTranslator { /// Generates the fully qualified name of a method descriptor. private func methodDescriptorPath( - for method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - service: CodeGenerationRequest.ServiceDescriptor + for method: MethodDescriptor, + service: ServiceDescriptor ) -> String { return "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" @@ -456,7 +456,7 @@ extension ServerCodeTranslator { /// Generates the fully qualified name of the type alias for a service protocol. internal func protocolNameTypealias( - service: CodeGenerationRequest.ServiceDescriptor, + service: ServiceDescriptor, streaming: Bool ) -> String { if streaming { @@ -467,7 +467,7 @@ extension ServerCodeTranslator { /// Generates the name of a service protocol. internal func protocolName( - service: CodeGenerationRequest.ServiceDescriptor, + service: ServiceDescriptor, streaming: Bool ) -> String { if streaming { diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 5936dc6f5..7f36fe70e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -108,7 +108,7 @@ struct TypealiasTranslator: SpecializedTranslator { extension TypealiasTranslator { private func makeServiceEnum( - from service: CodeGenerationRequest.ServiceDescriptor, + from service: ServiceDescriptor, named name: String ) throws -> Declaration { var serviceEnum = EnumDescription( @@ -154,8 +154,8 @@ extension TypealiasTranslator { } private func makeMethodEnum( - from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + from method: MethodDescriptor, + in service: ServiceDescriptor ) -> Declaration { var methodEnum = EnumDescription(name: method.name.generatedUpperCase) @@ -183,8 +183,8 @@ extension TypealiasTranslator { } private func makeMethodDescriptor( - from method: CodeGenerationRequest.ServiceDescriptor.MethodDescriptor, - in service: CodeGenerationRequest.ServiceDescriptor + from method: MethodDescriptor, + in service: ServiceDescriptor ) -> Declaration { let fullyQualifiedService = MemberAccessDescription( left: .memberAccess( @@ -223,7 +223,7 @@ extension TypealiasTranslator { } private func makeMethodDescriptors( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> Declaration { var methodDescriptors = [Expression]() let methodNames = service.methods.map { $0.name.generatedUpperCase } @@ -251,7 +251,7 @@ extension TypealiasTranslator { } private func makeServiceProtocolsTypealiases( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> [Declaration] { let streamingServiceProtocolTypealias = Declaration.typealias( accessModifier: self.accessModifier, @@ -277,7 +277,7 @@ extension TypealiasTranslator { } private func makeClientProtocolTypealias( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> Declaration { return .guarded( self.availabilityGuard, @@ -290,7 +290,7 @@ extension TypealiasTranslator { } private func makeClientStructTypealias( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> Declaration { return .guarded( self.availabilityGuard, @@ -302,7 +302,7 @@ extension TypealiasTranslator { ) } - private func makeServiceIdentifier(_ service: CodeGenerationRequest.ServiceDescriptor) -> String { + private func makeServiceIdentifier(_ service: ServiceDescriptor) -> String { let prefix: String if service.namespace.normalizedBase.isEmpty { @@ -315,7 +315,7 @@ extension TypealiasTranslator { } private func makeStaticServiceDescriptorProperty( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> VariableDescription { let serviceIdentifier = makeServiceIdentifier(service) @@ -334,7 +334,7 @@ extension TypealiasTranslator { } private func makeServiceDescriptorExtension( - for service: CodeGenerationRequest.ServiceDescriptor + for service: ServiceDescriptor ) -> Declaration { let serviceIdentifier = makeServiceIdentifier(service) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 67d97edc0..6dd72600c 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -21,10 +21,6 @@ import XCTest @testable import GRPCCodeGen final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { - typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor - typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor - typealias Name = GRPCCodeGen.CodeGenerationRequest.Name - func testClientCodeTranslatorUnaryMethod() throws { let method = MethodDescriptor( documentation: "/// Documentation for MethodA", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 4ea88262d..c362e1c91 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -21,64 +21,60 @@ import XCTest @testable import GRPCCodeGen final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { - typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor - typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor - typealias Name = GRPCCodeGen.CodeGenerationRequest.Name - func testImports() throws { - var dependencies = [CodeGenerationRequest.Dependency]() - dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", accessLevel: .public)) + var dependencies = [Dependency]() + dependencies.append(Dependency(module: "Foo", accessLevel: .public)) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .typealias, name: "Bar"), module: "Foo", accessLevel: .internal ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .struct, name: "Baz"), module: "Foo", accessLevel: .package ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .class, name: "Bac"), module: "Foo", accessLevel: .package ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .enum, name: "Bap"), module: "Foo", accessLevel: .package ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .protocol, name: "Bat"), module: "Foo", accessLevel: .package ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .let, name: "Baq"), module: "Foo", accessLevel: .package ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .var, name: "Bag"), module: "Foo", accessLevel: .package ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .func, name: "Bak"), module: "Foo", accessLevel: .package @@ -109,16 +105,16 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { } func testPreconcurrencyImports() throws { - var dependencies = [CodeGenerationRequest.Dependency]() + var dependencies = [Dependency]() dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( module: "Foo", preconcurrency: .required, accessLevel: .internal ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .enum, name: "Bar"), module: "Foo", preconcurrency: .required, @@ -126,7 +122,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( module: "Baz", preconcurrency: .requiredOnOS(["Deq", "Der"]), accessLevel: .internal @@ -154,12 +150,12 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { } func testSPIImports() throws { - var dependencies = [CodeGenerationRequest.Dependency]() + var dependencies = [Dependency]() dependencies.append( - CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) + Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .enum, name: "Bar"), module: "Foo", spi: "Secret", @@ -184,12 +180,12 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { } func testGeneration() throws { - var dependencies = [CodeGenerationRequest.Dependency]() + var dependencies = [Dependency]() dependencies.append( - CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) + Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) ) dependencies.append( - CodeGenerationRequest.Dependency( + Dependency( item: .init(kind: .enum, name: "Bar"), module: "Foo", spi: "Secret", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 6afe4c3f4..fde5448d9 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -21,10 +21,6 @@ import XCTest @testable import GRPCCodeGen final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { - typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor - typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor - typealias Name = GRPCCodeGen.CodeGenerationRequest.Name - func testServerCodeTranslatorUnaryMethod() throws { let method = MethodDescriptor( documentation: "/// Documentation for unaryMethod", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 52ab821a1..24c7dd330 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -72,8 +72,8 @@ internal func XCTAssertEqualWithDiff( } internal func makeCodeGenerationRequest( - services: [CodeGenerationRequest.ServiceDescriptor] = [], - dependencies: [CodeGenerationRequest.Dependency] = [] + services: [ServiceDescriptor] = [], + dependencies: [Dependency] = [] ) -> CodeGenerationRequest { return CodeGenerationRequest( fileName: "test.grpc", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 608ca4b27..658a41b66 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -21,10 +21,6 @@ import XCTest @testable import GRPCCodeGen final class TypealiasTranslatorSnippetBasedTests: XCTestCase { - typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor - typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor - typealias Name = GRPCCodeGen.CodeGenerationRequest.Name - func testTypealiasTranslator() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", From e160fd093883d107f3e4cd87d00c3e2e421593b1 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Fri, 15 Nov 2024 11:03:12 +0000 Subject: [PATCH 498/580] Allow adding `ClientInterceptor`s to specific services and methods (#2113) ## Motivation We want to allow users to customise the RPCs a registered interceptor should apply to on the client: - Intercept all requests - Intercept requests only meant for specific services - Intercept requests only meant for specific methods ## Modifications This PR adds a new `ClientInterceptorPipelineOperation` type that allows users to specify what the target of the interceptor should be. Existing APIs accepting `[any ClientInterceptor]` have been kept, but new initialisers taking `[ClientInterceptorPipelineOperation]` instead have been added. ## Result Users can have more control over to which requests interceptors are applied. --- .../Call/Client/ClientInterceptor.swift | 9 +- .../ClientInterceptorPipelineOperation.swift | 99 +++++++++++ .../Server/Internal/ServerRPCExecutor.swift | 3 +- .../ServerInterceptorPipelineOperation.swift | 3 +- Sources/GRPCCore/GRPCClient.swift | 88 +++++++-- ...entInterceptorPipelineOperationTests.swift | 68 +++++++ .../ServerInterceptorPipelineOperation.swift | 68 +++++++ Tests/GRPCCoreTests/GRPCClientTests.swift | 168 +++++++++++++++++- 8 files changed, 476 insertions(+), 30 deletions(-) create mode 100644 Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift create mode 100644 Tests/GRPCCoreTests/Call/Client/ClientInterceptorPipelineOperationTests.swift create mode 100644 Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift index 939461e54..68a1fcf45 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptor.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptor.swift @@ -21,10 +21,11 @@ /// received from the transport. They are typically used for cross-cutting concerns like injecting /// metadata, validating messages, logging additional data, and tracing. /// -/// Interceptors are registered with a client and apply to all RPCs. If you need to modify the -/// behavior of an interceptor on a per-RPC basis then you can use the -/// ``ClientContext/descriptor`` to determine which RPC is being called and -/// conditionalise behavior accordingly. +/// Interceptors are registered with the server via ``ClientInterceptorPipelineOperation``s. +/// You may register them for all services registered with a server, for RPCs directed to specific services, or +/// for RPCs directed to specific methods. If you need to modify the behavior of an interceptor on a +/// per-RPC basis in more detail, then you can use the ``ClientContext/descriptor`` to determine +/// which RPC is being called and conditionalise behavior accordingly. /// /// - TODO: Update example and documentation to show how to register an interceptor. /// diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift new file mode 100644 index 000000000..4ae2df8d5 --- /dev/null +++ b/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift @@ -0,0 +1,99 @@ +/* + * 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. + */ + +/// A `ClientInterceptorPipelineOperation` describes to which RPCs a client interceptor should be applied. +/// +/// You can configure a client interceptor to be applied to: +/// - all RPCs and services; +/// - requests directed only to specific services; or +/// - requests directed only to specific methods (of a specific service). +/// +/// - SeeAlso: ``ClientInterceptor`` for more information on client interceptors, and +/// ``ServerInterceptorPipelineOperation`` for the server-side version of this type. +public struct ClientInterceptorPipelineOperation: Sendable { + /// The subject of a ``ClientInterceptorPipelineOperation``. + /// The subject of an interceptor can either be all services and methods, only specific services, or only specific methods. + public struct Subject: Sendable { + internal enum Wrapped: Sendable { + case all + case services(Set) + case methods(Set) + } + + private let wrapped: Wrapped + + /// An operation subject specifying an interceptor that applies to all RPCs across all services will be registered with this client. + public static var all: Self { .init(wrapped: .all) } + + /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services. + /// - Parameters: + /// - services: The list of service names for which this interceptor should intercept RPCs. + /// - Returns: A ``ClientInterceptorPipelineOperation``. + public static func services(_ services: Set) -> Self { + Self(wrapped: .services(services)) + } + + /// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified service methods. + /// - Parameters: + /// - methods: The list of method descriptors for which this interceptor should intercept RPCs. + /// - Returns: A ``ClientInterceptorPipelineOperation``. + public static func methods(_ methods: Set) -> Self { + Self(wrapped: .methods(methods)) + } + + @usableFromInline + internal func applies(to descriptor: MethodDescriptor) -> Bool { + switch self.wrapped { + case .all: + return true + + case .services(let services): + return services.map({ $0.fullyQualifiedService }).contains(descriptor.service) + + case .methods(let methods): + return methods.contains(descriptor) + } + } + } + + /// The interceptor specified for this operation. + public let interceptor: any ClientInterceptor + + @usableFromInline + internal let subject: Subject + + private init(interceptor: any ClientInterceptor, appliesTo: Subject) { + self.interceptor = interceptor + self.subject = appliesTo + } + + /// Create an operation, specifying which ``ClientInterceptor`` to apply and to which ``Subject``. + /// - Parameters: + /// - interceptor: The ``ClientInterceptor`` to register with the client. + /// - subject: The ``Subject`` to which the `interceptor` applies. + /// - Returns: A ``ClientInterceptorPipelineOperation``. + public static func apply(_ interceptor: any ClientInterceptor, to subject: Subject) -> Self { + Self(interceptor: interceptor, appliesTo: subject) + } + + /// Returns whether this ``ClientInterceptorPipelineOperation`` applies to the given `descriptor`. + /// - Parameter descriptor: A ``MethodDescriptor`` for which to test whether this interceptor applies. + /// - Returns: `true` if this interceptor applies to the given `descriptor`, or `false` otherwise. + @inlinable + internal func applies(to descriptor: MethodDescriptor) -> Bool { + self.subject.applies(to: descriptor) + } +} diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index d9a35da51..aa2163424 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -23,7 +23,8 @@ struct ServerRPCExecutor { /// - stream: The accepted stream to execute the RPC on. /// - deserializer: A deserializer for messages received from the client. /// - serializer: A serializer for messages to send to the client. - /// - interceptors: Server interceptors to apply to this RPC. + /// - interceptors: Server interceptors to apply to this RPC. The + /// interceptors will be called in the order of the array. /// - handler: A handler which turns the request into a response. @inlinable static func execute( diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift index 3d2731fd4..e511ea3ec 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift @@ -21,7 +21,8 @@ /// - requests directed only to specific services registered with your server; or /// - requests directed only to specific methods (of a specific service). /// -/// - SeeAlso: ``ServerInterceptor`` for more information on server interceptors. +/// - SeeAlso: ``ServerInterceptor`` for more information on server interceptors, and +/// ``ClientInterceptorPipelineOperation`` for the client-side version of this type. public struct ServerInterceptorPipelineOperation: Sendable { /// The subject of a ``ServerInterceptorPipelineOperation``. /// The subject of an interceptor can either be all services and methods, only specific services, or only specific methods. diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 98c1c4f3d..79e3deb4a 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -112,19 +112,12 @@ public final class GRPCClient: Sendable { /// The transport which provides a bidirectional communication channel with the server. private let transport: any ClientTransport - /// A collection of interceptors providing cross-cutting functionality to each accepted RPC. - /// - /// The order in which interceptors are added reflects the order in which they are called. The - /// first interceptor added will be the first interceptor to intercept each request. The last - /// interceptor added will be the final interceptor to intercept each request before calling - /// the appropriate handler. - private let interceptors: [any ClientInterceptor] - /// The current state of the client. - private let state: Mutex + private let stateMachine: Mutex /// The state of the client. private enum State: Sendable { + /// The client hasn't been started yet. Can transition to `running` or `stopped`. case notStarted /// The client is running and can send RPCs. Can transition to `stopping`. @@ -187,22 +180,79 @@ public final class GRPCClient: Sendable { } } + private struct StateMachine { + var state: State + + private let interceptorPipeline: [ClientInterceptorPipelineOperation] + + /// A collection of interceptors providing cross-cutting functionality to each accepted RPC, keyed by the method to which they apply. + /// + /// The list of interceptors for each method is computed from `interceptorsPipeline` when calling a method for the first time. + /// This caching is done to avoid having to compute the applicable interceptors for each request made. + /// + /// The order in which interceptors are added reflects the order in which they are called. The + /// first interceptor added will be the first interceptor to intercept each request. The last + /// interceptor added will be the final interceptor to intercept each request before calling + /// the appropriate handler. + var interceptorsPerMethod: [MethodDescriptor: [any ClientInterceptor]] + + init(interceptorPipeline: [ClientInterceptorPipelineOperation]) { + self.state = .notStarted + self.interceptorPipeline = interceptorPipeline + self.interceptorsPerMethod = [:] + } + + mutating func checkExecutableAndGetApplicableInterceptors( + for method: MethodDescriptor + ) throws -> [any ClientInterceptor] { + try self.state.checkExecutable() + + guard let applicableInterceptors = self.interceptorsPerMethod[method] else { + let applicableInterceptors = self.interceptorPipeline + .filter { $0.applies(to: method) } + .map { $0.interceptor } + self.interceptorsPerMethod[method] = applicableInterceptors + return applicableInterceptors + } + + return applicableInterceptors + } + } + /// Creates a new client with the given transport, interceptors and configuration. /// /// - Parameters: /// - transport: The transport used to establish a communication channel with a server. - /// - interceptors: A collection of interceptors providing cross-cutting functionality to each + /// - interceptors: A collection of ``ClientInterceptor``s providing cross-cutting functionality to each /// accepted RPC. The order in which interceptors are added reflects the order in which they /// are called. The first interceptor added will be the first interceptor to intercept each /// request. The last interceptor added will be the final interceptor to intercept each /// request before calling the appropriate handler. - public init( + convenience public init( transport: some ClientTransport, interceptors: [any ClientInterceptor] = [] + ) { + self.init( + transport: transport, + interceptorPipeline: interceptors.map { .apply($0, to: .all) } + ) + } + + /// Creates a new client with the given transport, interceptors and configuration. + /// + /// - Parameters: + /// - transport: The transport used to establish a communication channel with a server. + /// - interceptorPipeline: A collection of ``ClientInterceptorPipelineOperation`` providing cross-cutting + /// functionality to each accepted RPC. Only applicable interceptors from the pipeline will be applied to each RPC. + /// The order in which interceptors are added reflects the order in which they are called. + /// The first interceptor added will be the first interceptor to intercept each request. + /// The last interceptor added will be the final interceptor to intercept each request before calling the appropriate handler. + public init( + transport: some ClientTransport, + interceptorPipeline: [ClientInterceptorPipelineOperation] ) { self.transport = transport - self.interceptors = interceptors - self.state = Mutex(.notStarted) + self.stateMachine = Mutex(StateMachine(interceptorPipeline: interceptorPipeline)) } /// Start the client. @@ -213,11 +263,11 @@ public final class GRPCClient: Sendable { /// The client, and by extension this function, can only be run once. If the client is already /// running or has already been closed then a ``RuntimeError`` is thrown. public func run() async throws { - try self.state.withLock { try $0.run() } + try self.stateMachine.withLock { try $0.state.run() } // When this function exits the client must have stopped. defer { - self.state.withLock { $0.stopped() } + self.stateMachine.withLock { $0.state.stopped() } } do { @@ -237,7 +287,7 @@ public final class GRPCClient: Sendable { /// in-flight RPCs to finish executing, but no new RPCs will be accepted. You can cancel the task /// executing ``run()`` if you want to abruptly stop in-flight RPCs. public func beginGracefulShutdown() { - let wasRunning = self.state.withLock { $0.beginGracefulShutdown() } + let wasRunning = self.stateMachine.withLock { $0.state.beginGracefulShutdown() } if wasRunning { self.transport.beginGracefulShutdown() } @@ -356,7 +406,9 @@ public final class GRPCClient: Sendable { options: CallOptions, handler: @Sendable @escaping (StreamingClientResponse) async throws -> ReturnValue ) async throws -> ReturnValue { - try self.state.withLock { try $0.checkExecutable() } + let applicableInterceptors = try self.stateMachine.withLock { + try $0.checkExecutableAndGetApplicableInterceptors(for: descriptor) + } let methodConfig = self.transport.config(forMethod: descriptor) var options = options options.formUnion(with: methodConfig) @@ -368,7 +420,7 @@ public final class GRPCClient: Sendable { serializer: serializer, deserializer: deserializer, transport: self.transport, - interceptors: self.interceptors, + interceptors: applicableInterceptors, handler: handler ) } diff --git a/Tests/GRPCCoreTests/Call/Client/ClientInterceptorPipelineOperationTests.swift b/Tests/GRPCCoreTests/Call/Client/ClientInterceptorPipelineOperationTests.swift new file mode 100644 index 000000000..2c8a0ae56 --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Client/ClientInterceptorPipelineOperationTests.swift @@ -0,0 +1,68 @@ +/* + * 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 Testing + +@testable import GRPCCore + +@Suite("ClientInterceptorPipelineOperation") +struct ClientInterceptorPipelineOperationTests { + @Test( + "Applies to", + arguments: [ + ( + .all, + [.fooBar, .fooBaz, .barFoo, .barBaz], + [] + ), + ( + .services([ServiceDescriptor(package: "pkg", service: "foo")]), + [.fooBar, .fooBaz], + [.barFoo, .barBaz] + ), + ( + .methods([.barFoo]), + [.barFoo], + [.fooBar, .fooBaz, .barBaz] + ), + ] as [(ClientInterceptorPipelineOperation.Subject, [MethodDescriptor], [MethodDescriptor])] + ) + func appliesTo( + operationSubject: ClientInterceptorPipelineOperation.Subject, + applicableMethods: [MethodDescriptor], + notApplicableMethods: [MethodDescriptor] + ) { + let operation = ClientInterceptorPipelineOperation.apply( + .requestCounter(.init()), + to: operationSubject + ) + + for applicableMethod in applicableMethods { + #expect(operation.applies(to: applicableMethod)) + } + + for notApplicableMethod in notApplicableMethods { + #expect(!operation.applies(to: notApplicableMethod)) + } + } +} + +extension MethodDescriptor { + fileprivate static let fooBar = Self(service: "pkg.foo", method: "bar") + fileprivate static let fooBaz = Self(service: "pkg.foo", method: "baz") + fileprivate static let barFoo = Self(service: "pkg.bar", method: "foo") + fileprivate static let barBaz = Self(service: "pkg.bar", method: "Baz") +} diff --git a/Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift b/Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift new file mode 100644 index 000000000..2055bb6cc --- /dev/null +++ b/Tests/GRPCCoreTests/Call/Server/ServerInterceptorPipelineOperation.swift @@ -0,0 +1,68 @@ +/* + * 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 Testing + +@testable import GRPCCore + +@Suite("ServerInterceptorPipelineOperation") +struct ServerInterceptorPipelineOperationTests { + @Test( + "Applies to", + arguments: [ + ( + .all, + [.fooBar, .fooBaz, .barFoo, .barBaz], + [] + ), + ( + .services([ServiceDescriptor(package: "pkg", service: "foo")]), + [.fooBar, .fooBaz], + [.barFoo, .barBaz] + ), + ( + .methods([.barFoo]), + [.barFoo], + [.fooBar, .fooBaz, .barBaz] + ), + ] as [(ServerInterceptorPipelineOperation.Subject, [MethodDescriptor], [MethodDescriptor])] + ) + func appliesTo( + operationSubject: ServerInterceptorPipelineOperation.Subject, + applicableMethods: [MethodDescriptor], + notApplicableMethods: [MethodDescriptor] + ) { + let operation = ServerInterceptorPipelineOperation.apply( + .requestCounter(.init()), + to: operationSubject + ) + + for applicableMethod in applicableMethods { + #expect(operation.applies(to: applicableMethod)) + } + + for notApplicableMethod in notApplicableMethods { + #expect(!operation.applies(to: notApplicableMethod)) + } + } +} + +extension MethodDescriptor { + fileprivate static let fooBar = Self(service: "pkg.foo", method: "bar") + fileprivate static let fooBaz = Self(service: "pkg.foo", method: "baz") + fileprivate static let barFoo = Self(service: "pkg.bar", method: "foo") + fileprivate static let barBaz = Self(service: "pkg.bar", method: "Baz") +} diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index 42a6e3b3b..ed5396da1 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -16,16 +16,17 @@ import GRPCCore import GRPCInProcessTransport +import Testing import XCTest final class GRPCClientTests: XCTestCase { func withInProcessConnectedClient( services: [any RegistrableRPCService], - interceptors: [any ClientInterceptor] = [], + interceptorPipeline: [ClientInterceptorPipelineOperation] = [], _ body: (GRPCClient, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() - let client = GRPCClient(transport: inProcess.client, interceptors: interceptors) + let client = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) let server = GRPCServer(transport: inProcess.server, services: services) try await withThrowingTaskGroup(of: Void.self) { group in @@ -234,10 +235,10 @@ final class GRPCClientTests: XCTestCase { try await self.withInProcessConnectedClient( services: [BinaryEcho()], - interceptors: [ - .requestCounter(counter1), - .rejectAll(with: RPCError(code: .unavailable, message: "")), - .requestCounter(counter2), + interceptorPipeline: [ + .apply(.requestCounter(counter1), to: .all), + .apply(.rejectAll(with: RPCError(code: .unavailable, message: "")), to: .all), + .apply(.requestCounter(counter2), to: .all), ] ) { client, _ in try await client.unary( @@ -409,3 +410,158 @@ final class GRPCClientTests: XCTestCase { task.cancel() } } + +@Suite("GRPC Client Tests") +struct ClientTests { + @Test("Interceptors are applied only to specified services") + func testInterceptorsAreAppliedToSpecifiedServices() async throws { + let onlyBinaryEchoCounter = AtomicCounter() + let allServicesCounter = AtomicCounter() + let onlyHelloWorldCounter = AtomicCounter() + let bothServicesCounter = AtomicCounter() + + try await self.withInProcessConnectedClient( + services: [BinaryEcho(), HelloWorld()], + interceptorPipeline: [ + .apply( + .requestCounter(onlyBinaryEchoCounter), + to: .services([BinaryEcho.serviceDescriptor]) + ), + .apply(.requestCounter(allServicesCounter), to: .all), + .apply( + .requestCounter(onlyHelloWorldCounter), + to: .services([HelloWorld.serviceDescriptor]) + ), + .apply( + .requestCounter(bothServicesCounter), + to: .services([BinaryEcho.serviceDescriptor, HelloWorld.serviceDescriptor]) + ), + ] + ) { client, _ in + // Make a request to the `BinaryEcho` service and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.unary( + request: .init(message: Array("hello".utf8)), + descriptor: BinaryEcho.Methods.get, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer(), + options: .defaults + ) { response in + let message = try #require(try response.message) + #expect(message == Array("hello".utf8)) + } + + #expect(onlyBinaryEchoCounter.value == 1) + #expect(allServicesCounter.value == 1) + #expect(onlyHelloWorldCounter.value == 0) + #expect(bothServicesCounter.value == 1) + + // Now, make a request to the `HelloWorld` service and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.unary( + request: .init(message: Array("Swift".utf8)), + descriptor: HelloWorld.Methods.sayHello, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer(), + options: .defaults + ) { response in + let message = try #require(try response.message) + #expect(message == Array("Hello, Swift!".utf8)) + } + + #expect(onlyBinaryEchoCounter.value == 1) + #expect(allServicesCounter.value == 2) + #expect(onlyHelloWorldCounter.value == 1) + #expect(bothServicesCounter.value == 2) + } + } + + @Test("Interceptors are applied only to specified methods") + func testInterceptorsAreAppliedToSpecifiedMethods() async throws { + let onlyBinaryEchoGetCounter = AtomicCounter() + let onlyBinaryEchoCollectCounter = AtomicCounter() + let bothBinaryEchoMethodsCounter = AtomicCounter() + let allMethodsCounter = AtomicCounter() + + try await self.withInProcessConnectedClient( + services: [BinaryEcho()], + interceptorPipeline: [ + .apply( + .requestCounter(onlyBinaryEchoGetCounter), + to: .methods([BinaryEcho.Methods.get]) + ), + .apply(.requestCounter(allMethodsCounter), to: .all), + .apply( + .requestCounter(onlyBinaryEchoCollectCounter), + to: .methods([BinaryEcho.Methods.collect]) + ), + .apply( + .requestCounter(bothBinaryEchoMethodsCounter), + to: .methods([BinaryEcho.Methods.get, BinaryEcho.Methods.collect]) + ), + ] + ) { client, _ in + // Make a request to the `BinaryEcho/get` method and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.unary( + request: .init(message: Array("hello".utf8)), + descriptor: BinaryEcho.Methods.get, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer(), + options: .defaults + ) { response in + let message = try #require(try response.message) + #expect(message == Array("hello".utf8)) + } + + #expect(onlyBinaryEchoGetCounter.value == 1) + #expect(allMethodsCounter.value == 1) + #expect(onlyBinaryEchoCollectCounter.value == 0) + #expect(bothBinaryEchoMethodsCounter.value == 1) + + // Now, make a request to the `BinaryEcho/collect` method and assert that only + // the counters associated to interceptors that apply to it are incremented. + try await client.unary( + request: .init(message: Array("hello".utf8)), + descriptor: BinaryEcho.Methods.collect, + serializer: IdentitySerializer(), + deserializer: IdentityDeserializer(), + options: .defaults + ) { response in + let message = try #require(try response.message) + #expect(message == Array("hello".utf8)) + } + + #expect(onlyBinaryEchoGetCounter.value == 1) + #expect(allMethodsCounter.value == 2) + #expect(onlyBinaryEchoCollectCounter.value == 1) + #expect(bothBinaryEchoMethodsCounter.value == 2) + } + } + + func withInProcessConnectedClient( + services: [any RegistrableRPCService], + interceptorPipeline: [ClientInterceptorPipelineOperation] = [], + _ body: (GRPCClient, GRPCServer) async throws -> Void + ) async throws { + let inProcess = InProcessTransport() + let client = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) + let server = GRPCServer(transport: inProcess.server, services: services) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await server.serve() + } + + group.addTask { + try await client.run() + } + + // Make sure both server and client are running + try await Task.sleep(for: .milliseconds(100)) + try await body(client, server) + client.beginGracefulShutdown() + server.beginGracefulShutdown() + } + } +} From dd22b3938eecdf1008dddd9d95e7a95272459dc8 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 15 Nov 2024 13:56:25 +0000 Subject: [PATCH 499/580] Allow metadata to be mutated on server response types (#2120) Motivation: The server response types have a metadata computed property with only a getter. It's entirely possible to mutate the metadata manually, it's just a bit of a faff. This should be easier. Modifications: - Add a setter to the computed property - Migrate server response tests to swift-testing Result: - Easier to use API --- .../GRPCCore/Call/Server/ServerResponse.swift | 52 ++++++--- .../Call/Server/ServerResponseTests.swift | 109 ++++++++++++------ 2 files changed, 107 insertions(+), 54 deletions(-) diff --git a/Sources/GRPCCore/Call/Server/ServerResponse.swift b/Sources/GRPCCore/Call/Server/ServerResponse.swift index 524db2868..f71fe4204 100644 --- a/Sources/GRPCCore/Call/Server/ServerResponse.swift +++ b/Sources/GRPCCore/Call/Server/ServerResponse.swift @@ -244,15 +244,25 @@ extension ServerResponse { self.accepted = .failure(error) } - /// Returns the metadata to be sent to the client at the start of the response. - /// - /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty. + /// The metadata to be sent to the client at the start of the response. public var metadata: Metadata { - switch self.accepted { - case let .success(contents): - return contents.metadata - case .failure: - return [:] + get { + switch self.accepted { + case let .success(contents): + return contents.metadata + case .failure(let error): + return error.metadata + } + } + set { + switch self.accepted { + case var .success(contents): + contents.metadata = newValue + self.accepted = .success(contents) + case var .failure(error): + error.metadata = newValue + self.accepted = .failure(error) + } } } @@ -303,15 +313,25 @@ extension StreamingServerResponse { self.accepted = .failure(error) } - /// Returns metadata received from the server at the start of the response. - /// - /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty. + /// The metadata to be sent to the client at the start of the response. public var metadata: Metadata { - switch self.accepted { - case let .success(contents): - return contents.metadata - case .failure: - return [:] + get { + switch self.accepted { + case let .success(contents): + return contents.metadata + case .failure(let error): + return error.metadata + } + } + set { + switch self.accepted { + case var .success(contents): + contents.metadata = newValue + self.accepted = .success(contents) + case var .failure(error): + error.metadata = newValue + self.accepted = .failure(error) + } } } } diff --git a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift index 99e76fc56..4461620d0 100644 --- a/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift +++ b/Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift @@ -13,38 +13,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@_spi(Testing) import GRPCCore -import XCTest -final class ServerResponseTests: XCTestCase { - func testSingleConvenienceInit() { - var response = ServerResponse( +import GRPCCore +import Testing + +@Suite("ServerResponse") +struct ServerResponseTests { + @Test("ServerResponse(message:metadata:trailingMetadata:)") + func responseInitSuccess() throws { + let response = ServerResponse( message: "message", metadata: ["metadata": "initial"], trailingMetadata: ["metadata": "trailing"] ) - switch response.accepted { - case .success(let contents): - XCTAssertEqual(contents.message, "message") - XCTAssertEqual(contents.metadata, ["metadata": "initial"]) - XCTAssertEqual(contents.trailingMetadata, ["metadata": "trailing"]) - case .failure: - XCTFail("Unexpected error") - } + let contents = try #require(try response.accepted.get()) + #expect(contents.message == "message") + #expect(contents.metadata == ["metadata": "initial"]) + #expect(contents.trailingMetadata == ["metadata": "trailing"]) + } + @Test("ServerResponse(of:error:)") + func responseInitError() throws { let error = RPCError(code: .aborted, message: "Aborted") - response = ServerResponse(of: String.self, error: error) + let response = ServerResponse(of: String.self, error: error) switch response.accepted { case .success: - XCTFail("Unexpected success") - case .failure(let error): - XCTAssertEqual(error, error) + Issue.record("Expected error") + case .failure(let rpcError): + #expect(rpcError == error) } } - func testStreamConvenienceInit() async throws { - var response = StreamingServerResponse( + @Test("StreamingServerResponse(of:metadata:producer:)") + func streamingResponseInitSuccess() async throws { + let response = StreamingServerResponse( of: String.self, metadata: ["metadata": "initial"] ) { _ in @@ -52,26 +55,26 @@ final class ServerResponseTests: XCTestCase { return ["metadata": "trailing"] } - switch response.accepted { - case .success(let contents): - XCTAssertEqual(contents.metadata, ["metadata": "initial"]) - let trailingMetadata = try await contents.producer(.failTestOnWrite()) - XCTAssertEqual(trailingMetadata, ["metadata": "trailing"]) - case .failure: - XCTFail("Unexpected error") - } + let contents = try #require(try response.accepted.get()) + #expect(contents.metadata == ["metadata": "initial"]) + let trailingMetadata = try await contents.producer(.failTestOnWrite()) + #expect(trailingMetadata == ["metadata": "trailing"]) + } + @Test("StreamingServerResponse(of:error:)") + func streamingResponseInitError() async throws { let error = RPCError(code: .aborted, message: "Aborted") - response = StreamingServerResponse(of: String.self, error: error) + let response = StreamingServerResponse(of: String.self, error: error) switch response.accepted { case .success: - XCTFail("Unexpected success") - case .failure(let error): - XCTAssertEqual(error, error) + Issue.record("Expected error") + case .failure(let rpcError): + #expect(rpcError == error) } } - func testSingleToStreamConversionForSuccessfulResponse() async throws { + @Test("StreamingServerResponse(single:) (accepted)") + func singleToStreamConversionForSuccessfulResponse() async throws { let single = ServerResponse( message: "foo", metadata: ["metadata": "initial"], @@ -90,19 +93,49 @@ final class ServerResponseTests: XCTestCase { throw error } - XCTAssertEqual(stream.metadata, ["metadata": "initial"]) + #expect(stream.metadata == ["metadata": "initial"]) let collected = try await messages.collect() - XCTAssertEqual(collected, ["foo"]) - XCTAssertEqual(trailingMetadata, ["metadata": "trailing"]) + #expect(collected == ["foo"]) + #expect(trailingMetadata == ["metadata": "trailing"]) } - func testSingleToStreamConversionForFailedResponse() async throws { + @Test("StreamingServerResponse(single:) (rejected)") + func singleToStreamConversionForFailedResponse() async throws { let error = RPCError(code: .aborted, message: "aborted") let single = ServerResponse(of: String.self, error: error) let stream = StreamingServerResponse(single: single) - XCTAssertThrowsRPCError(try stream.accepted.get()) { - XCTAssertEqual($0, error) + switch stream.accepted { + case .success: + Issue.record("Expected error") + case .failure(let rpcError): + #expect(rpcError == error) + } + } + + @Test("Mutate metadata on response", arguments: [true, false]) + func mutateMetadataOnResponse(accepted: Bool) { + var response: ServerResponse + if accepted { + response = ServerResponse(message: "") + } else { + response = ServerResponse(error: RPCError(code: .aborted, message: "")) } + + response.metadata.addString("value", forKey: "key") + #expect(response.metadata == ["key": "value"]) + } + + @Test("Mutate metadata on streaming response", arguments: [true, false]) + func mutateMetadataOnStreamingResponse(accepted: Bool) { + var response: StreamingServerResponse + if accepted { + response = StreamingServerResponse { _ in [:] } + } else { + response = StreamingServerResponse(error: RPCError(code: .aborted, message: "")) + } + + response.metadata.addString("value", forKey: "key") + #expect(response.metadata == ["key": "value"]) } } From c960d066dc4fb507704755c1f239d8831803ffa1 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 18 Nov 2024 13:35:00 +0000 Subject: [PATCH 500/580] Add structured swift represntation for the generated metadata (#2117) Motivation: The code gen as it stands tests too much in one go: a small change leads to many tests being updated. This stems from the translator not being very testable. To improve this, and make future code gen changes less painful, we can refactor it such that helpers build structured swift. These are significantly less coupled and can be tested more easily. The strategy here is to add all the structured representations for the metadata, client, and server and then cut over the translators to using the new code. This first change will introduce the structured represntation for metadata. Modifications: - Add metadata structured swift - Add helpers for commonly used types Result: Metadata is in place --- .../StructuredSwift+ServiceMetadata.swift | 304 ++++++++++++++++++ .../Internal/StructuredSwift+Types.swift | 88 +++++ .../StructuredSwiftRepresentation.swift | 2 +- .../IDLToStructuredSwiftTranslator.swift | 2 +- .../StructuredSwift+MetadataTests.swift | 295 +++++++++++++++++ .../Internal/StructuredSwiftTestHelpers.swift | 66 ++++ 6 files changed, 755 insertions(+), 2 deletions(-) create mode 100644 Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift create mode 100644 Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift new file mode 100644 index 000000000..a71cafd83 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -0,0 +1,304 @@ +/* + * 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. + */ + +extension TypealiasDescription { + /// `typealias Input = ` + package static func methodInput( + accessModifier: AccessModifier? = nil, + name: String + ) -> Self { + return TypealiasDescription( + accessModifier: accessModifier, + name: "Input", + existingType: .member(name) + ) + } + + /// `typealias Output = ` + package static func methodOutput( + accessModifier: AccessModifier? = nil, + name: String + ) -> Self { + return TypealiasDescription( + accessModifier: accessModifier, + name: "Output", + existingType: .member(name) + ) + } +} + +extension VariableDescription { + /// ``` + /// static let descriptor = GRPCCore.MethodDescriptor( + /// service: .descriptor.fullyQualifiedService, + /// method: "" + /// ``` + package static func methodDescriptor( + accessModifier: AccessModifier? = nil, + serviceNamespace: String, + literalMethodName: String + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern("descriptor")), + right: .functionCall( + FunctionCallDescription( + calledExpression: .identifierType(.methodDescriptor), + arguments: [ + FunctionArgumentDescription( + label: "service", + expression: .identifierType( + .member([serviceNamespace, "descriptor"]) + ).dot("fullyQualifiedService") + ), + FunctionArgumentDescription( + label: "method", + expression: .literal(literalMethodName) + ), + ] + ) + ) + ) + } + + /// ``` + /// static let descriptor = GRPCCore.ServiceDescriptor. + /// ``` + package static func serviceDescriptor( + accessModifier: AccessModifier? = nil, + namespacedProperty: String + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifierPattern("descriptor"), + right: .identifier(.type(.serviceDescriptor)).dot(namespacedProperty) + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension GRPCCore.ServiceDescriptor { + /// static let = Self( + /// package: "", + /// service: "" + /// ) + /// } + /// ``` + package static func serviceDescriptor( + accessModifier: AccessModifier? = nil, + propertyName: String, + literalNamespace: String, + literalService: String + ) -> ExtensionDescription { + return ExtensionDescription( + onType: "GRPCCore.ServiceDescriptor", + declarations: [ + .variable( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern(propertyName)), + right: .functionCall( + calledExpression: .identifierType(.member("Self")), + arguments: [ + FunctionArgumentDescription( + label: "package", + expression: .literal(literalNamespace) + ), + FunctionArgumentDescription( + label: "service", + expression: .literal(literalService) + ), + ] + ) + ) + ] + ) + } +} + +extension VariableDescription { + /// ``` + /// static let descriptors: [GRPCCore.MethodDescriptor] = [.descriptor, ...] + /// ``` + package static func methodDescriptorsArray( + accessModifier: AccessModifier? = nil, + methodNamespaceNames names: [String] + ) -> Self { + return VariableDescription( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern("descriptors")), + type: .array(.methodDescriptor), + right: .literal(.array(names.map { name in .identifierPattern(name).dot("descriptor") })) + ) + } +} + +extension EnumDescription { + /// ``` + /// enum { + /// typealias Input = + /// typealias Output = + /// static let descriptor = GRPCCore.MethodDescriptor( + /// service: .descriptor.fullyQualifiedService, + /// method: "" + /// ) + /// } + /// ``` + package static func methodNamespace( + accessModifier: AccessModifier? = nil, + name: String, + literalMethod: String, + serviceNamespace: String, + inputType: String, + outputType: String + ) -> Self { + return EnumDescription( + accessModifier: accessModifier, + name: name, + members: [ + .typealias(.methodInput(accessModifier: accessModifier, name: inputType)), + .typealias(.methodOutput(accessModifier: accessModifier, name: outputType)), + .variable( + .methodDescriptor( + accessModifier: accessModifier, + serviceNamespace: serviceNamespace, + literalMethodName: literalMethod + ) + ), + ] + ) + } + + /// ``` + /// enum Method { + /// enum { + /// typealias Input = + /// typealias Output = + /// static let descriptor = GRPCCore.MethodDescriptor( + /// service: .descriptor.fullyQualifiedService, + /// method: "" + /// ) + /// } + /// ... + /// static let descriptors: [GRPCCore.MethodDescriptor] = [ + /// .descriptor, + /// ... + /// ] + /// } + /// ``` + package static func methodsNamespace( + accessModifier: AccessModifier? = nil, + serviceNamespace: String, + methods: [MethodDescriptor] + ) -> EnumDescription { + var description = EnumDescription(accessModifier: accessModifier, name: "Method") + + // Add a namespace for each method. + let methodNamespaces: [Declaration] = methods.map { method in + return .enum( + .methodNamespace( + accessModifier: accessModifier, + name: method.name.base, + literalMethod: method.name.base, + serviceNamespace: serviceNamespace, + inputType: method.inputType, + outputType: method.outputType + ) + ) + } + description.members.append(contentsOf: methodNamespaces) + + // Add an array of method descriptors + let methodDescriptorsArray: VariableDescription = .methodDescriptorsArray( + accessModifier: accessModifier, + methodNamespaceNames: methods.map { $0.name.base } + ) + description.members.append(.variable(methodDescriptorsArray)) + + return description + } + + /// ``` + /// enum { + /// static let descriptor = GRPCCore.ServiceDescriptor. + /// enum Method { + /// ... + /// } + /// @available(...) + /// typealias StreamingServiceProtocol = ... + /// @available(...) + /// typealias ServiceProtocol = ... + /// ... + /// } + /// ``` + package static func serviceNamespace( + accessModifier: AccessModifier? = nil, + name: String, + serviceDescriptorProperty: String, + client: Bool, + server: Bool, + methods: [MethodDescriptor] + ) -> EnumDescription { + var description = EnumDescription(accessModifier: accessModifier, name: name) + + // static let descriptor = GRPCCore.ServiceDescriptor. + let descriptor = VariableDescription.serviceDescriptor( + accessModifier: accessModifier, + namespacedProperty: serviceDescriptorProperty + ) + description.members.append(.variable(descriptor)) + + // enum Method { ... } + let methodsNamespace: EnumDescription = .methodsNamespace( + accessModifier: accessModifier, + serviceNamespace: name, + methods: methods + ) + description.members.append(.enum(methodsNamespace)) + + // Typealiases for the various protocols. + var typealiasNames: [String] = [] + if server { + typealiasNames.append("StreamingServiceProtocol") + typealiasNames.append("ServiceProtocol") + } + if client { + typealiasNames.append("ClientProtocol") + typealiasNames.append("Client") + } + let typealiases: [Declaration] = typealiasNames.map { alias in + .guarded( + .grpc, + .typealias( + accessModifier: accessModifier, + name: alias, + existingType: .member(name + "_" + alias) + ) + ) + } + description.members.append(contentsOf: typealiases) + + return description + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift new file mode 100644 index 000000000..981566543 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift @@ -0,0 +1,88 @@ +/* + * 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. + */ + +extension AvailabilityDescription { + package static let grpc = AvailabilityDescription( + osVersions: [ + OSVersion(os: .macOS, version: "15.0"), + OSVersion(os: .iOS, version: "18.0"), + OSVersion(os: .watchOS, version: "11.0"), + OSVersion(os: .tvOS, version: "18.0"), + OSVersion(os: .visionOS, version: "2.0"), + ] + ) +} + +extension ExistingTypeDescription { + fileprivate static func grpcCore(_ typeName: String) -> Self { + return .member(["GRPCCore", typeName]) + } + + fileprivate static func requestResponse( + for type: String?, + isRequest: Bool, + isStreaming: Bool, + isClient: Bool + ) -> Self { + let prefix = isStreaming ? "Streaming" : "" + let peer = isClient ? "Client" : "Server" + let kind = isRequest ? "Request" : "Response" + let baseType: Self = .grpcCore(prefix + peer + kind) + + if let type = type { + return .generic(wrapper: baseType, wrapped: .member(type)) + } else { + return baseType + } + } + + package static func serverRequest(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: true, isStreaming: streaming, isClient: false) + } + + package static func serverResponse(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: false, isStreaming: streaming, isClient: false) + } + + package static func clientRequest(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: true, isStreaming: streaming, isClient: true) + } + + package static func clientResponse(forType type: String?, streaming: Bool) -> Self { + return .requestResponse(for: type, isRequest: false, isStreaming: streaming, isClient: true) + } + + package static let serverContext: Self = .grpcCore("ServerContext") + package static let rpcRouter: Self = .grpcCore("RPCRouter") + package static let serviceDescriptor: Self = .grpcCore("ServiceDescriptor") + package static let methodDescriptor: Self = .grpcCore("MethodDescriptor") + + package static func serializer(forType type: String) -> Self { + .generic(wrapper: .grpcCore("MessageSerializer"), wrapped: .member(type)) + } + + package static func deserializer(forType type: String) -> Self { + .generic(wrapper: .grpcCore("MessageDeserializer"), wrapped: .member(type)) + } + + package static func rpcWriter(forType type: String) -> Self { + .generic(wrapper: .grpcCore("RPCWriter"), wrapped: .member(type)) + } + + package static let callOptions: Self = .grpcCore("CallOptions") + package static let metadata: Self = .grpcCore("Metadata") + package static let grpcClient: Self = .grpcCore("GRPCClient") +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index bc61819d3..a9f9c33d7 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -100,7 +100,7 @@ struct ImportDescription: Equatable, Codable, Sendable { /// A description of an access modifier. /// /// For example: `public`. -internal enum AccessModifier: String, Sendable, Equatable, Codable { +internal enum AccessModifier: String, Sendable, Equatable, Codable, CaseIterable { /// A declaration accessible outside of the module. case `public` diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 2319e900f..fb5474859 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -87,7 +87,7 @@ struct IDLToStructuredSwiftTranslator: Translator { } extension AccessModifier { - fileprivate init(_ accessLevel: SourceGenerator.Config.AccessLevel) { + init(_ accessLevel: SourceGenerator.Config.AccessLevel) { switch accessLevel.level { case .internal: self = .internal case .package: self = .package diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift new file mode 100644 index 000000000..76997a643 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -0,0 +1,295 @@ +/* + * 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 Testing + +@testable import GRPCCodeGen + +extension StructuedSwiftTests { + @Suite("Metadata") + struct Metadata { + @Test("@available(...)") + func grpcAvailability() async throws { + let availability: AvailabilityDescription = .grpc + let structDecl = StructDescription(name: "Ignored") + let expected = """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + struct Ignored {} + """ + + #expect(render(.guarded(availability, .struct(structDecl))) == expected) + } + + @Test("typealias Input = ", arguments: AccessModifier.allCases) + func methodInputTypealias(access: AccessModifier) { + let decl: TypealiasDescription = .methodInput(accessModifier: access, name: "Foo") + let expected = "\(access) typealias Input = Foo" + #expect(render(.typealias(decl)) == expected) + } + + @Test("typealias Output = ", arguments: AccessModifier.allCases) + func methodOutputTypealias(access: AccessModifier) { + let decl: TypealiasDescription = .methodOutput(accessModifier: access, name: "Foo") + let expected = "\(access) typealias Output = Foo" + #expect(render(.typealias(decl)) == expected) + } + + @Test( + "static let descriptor = GRPCCore.MethodDescriptor(...)", + arguments: AccessModifier.allCases + ) + func staticMethodDescriptorProperty(access: AccessModifier) { + let decl: VariableDescription = .methodDescriptor( + accessModifier: access, + serviceNamespace: "FooService", + literalMethodName: "Bar" + ) + + let expected = """ + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: FooService.descriptor.fullyQualifiedService, + method: "Bar" + ) + """ + #expect(render(.variable(decl)) == expected) + } + + @Test( + "static let descriptor = GRPCCore.ServiceDescriptor.", + arguments: AccessModifier.allCases + ) + func staticServiceDescriptorProperty(access: AccessModifier) { + let decl: VariableDescription = .serviceDescriptor( + accessModifier: access, + namespacedProperty: "foo" + ) + + let expected = "\(access) static let descriptor = GRPCCore.ServiceDescriptor.foo" + #expect(render(.variable(decl)) == expected) + } + + @Test("extension GRPCCore.ServiceDescriptor { ... }", arguments: AccessModifier.allCases) + func staticServiceDescriptorPropertyExtension(access: AccessModifier) { + let decl: ExtensionDescription = .serviceDescriptor( + accessModifier: access, + propertyName: "foo", + literalNamespace: "echo", + literalService: "EchoService" + ) + + let expected = """ + extension GRPCCore.ServiceDescriptor { + \(access) static let foo = Self( + package: "echo", + service: "EchoService" + ) + } + """ + #expect(render(.extension(decl)) == expected) + } + + @Test( + "static let descriptors: [GRPCCore.MethodDescriptor] = [...]", + arguments: AccessModifier.allCases + ) + func staticMethodDescriptorsArray(access: AccessModifier) { + let decl: VariableDescription = .methodDescriptorsArray( + accessModifier: access, + methodNamespaceNames: ["Foo", "Bar", "Baz"] + ) + + let expected = """ + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ + Foo.descriptor, + Bar.descriptor, + Baz.descriptor + ] + """ + #expect(render(.variable(decl)) == expected) + } + + @Test("enum { ... }", arguments: AccessModifier.allCases) + func methodNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .methodNamespace( + accessModifier: access, + name: "Foo", + literalMethod: "Foo", + serviceNamespace: "Bar_Baz", + inputType: "FooInput", + outputType: "FooOutput" + ) + + let expected = """ + \(access) enum Foo { + \(access) typealias Input = FooInput + \(access) typealias Output = FooOutput + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: Bar_Baz.descriptor.fullyQualifiedService, + method: "Foo" + ) + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum Method { ... }", arguments: AccessModifier.allCases) + func methodsNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .methodsNamespace( + accessModifier: access, + serviceNamespace: "Bar_Baz", + methods: [ + .init( + documentation: "", + name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ) + ] + ) + + let expected = """ + \(access) enum Method { + \(access) enum Foo { + \(access) typealias Input = FooInput + \(access) typealias Output = FooOutput + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: Bar_Baz.descriptor.fullyQualifiedService, + method: "Foo" + ) + } + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ + Foo.descriptor + ] + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum Method { ... } (no methods)", arguments: AccessModifier.allCases) + func methodsNamespaceEnumNoMethods(access: AccessModifier) { + let decl: EnumDescription = .methodsNamespace( + accessModifier: access, + serviceNamespace: "Bar_Baz", + methods: [] + ) + + let expected = """ + \(access) enum Method { + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test("enum { ... }", arguments: AccessModifier.allCases) + func serviceNamespaceEnum(access: AccessModifier) { + let decl: EnumDescription = .serviceNamespace( + accessModifier: access, + name: "Foo", + serviceDescriptorProperty: "foo", + client: false, + server: false, + methods: [ + .init( + documentation: "", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ] + ) + + let expected = """ + \(access) enum Foo { + \(access) static let descriptor = GRPCCore.ServiceDescriptor.foo + \(access) enum Method { + \(access) enum Bar { + \(access) typealias Input = BarInput + \(access) typealias Output = BarOutput + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: Foo.descriptor.fullyQualifiedService, + method: "Bar" + ) + } + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ + Bar.descriptor + ] + } + } + """ + #expect(render(.enum(decl)) == expected) + } + + @Test( + "enum { ... } (no methods)", + arguments: AccessModifier.allCases, + [(true, true), (false, false), (true, false), (false, true)] + ) + func serviceNamespaceEnumNoMethods(access: AccessModifier, config: (client: Bool, server: Bool)) + { + let decl: EnumDescription = .serviceNamespace( + accessModifier: access, + name: "Foo", + serviceDescriptorProperty: "foo", + client: config.client, + server: config.server, + methods: [] + ) + + var expected = """ + \(access) enum Foo { + \(access) static let descriptor = GRPCCore.ServiceDescriptor.foo + \(access) enum Method { + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] + }\n + """ + + if config.server { + expected += """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias StreamingServiceProtocol = Foo_StreamingServiceProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias ServiceProtocol = Foo_ServiceProtocol + """ + } + + if config.client { + if config.server { + expected += "\n" + } + + expected += """ + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias ClientProtocol = Foo_ClientProtocol + @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) + \(access) typealias Client = Foo_Client + """ + } + + if config.client || config.server { + expected += "\n}" + } else { + expected += "}" + } + + #expect(render(.enum(decl)) == expected) + } + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift new file mode 100644 index 000000000..d364e843f --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift @@ -0,0 +1,66 @@ +/* + * 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 Testing + +@testable import GRPCCodeGen + +// Used as a namespace for organising other structured swift tests. +@Suite("Structued Swift") +struct StructuedSwiftTests {} + +func render(_ declaration: Declaration) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderDeclaration(declaration) + return renderer.renderedContents() +} + +func render(_ expression: Expression) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderExpression(expression) + return renderer.renderedContents() +} + +func render(_ blocks: [CodeBlock]) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderCodeBlocks(blocks) + return renderer.renderedContents() +} + +enum RPCKind: Hashable, Sendable, CaseIterable { + case unary + case clientStreaming + case serverStreaming + case bidirectionalStreaming + + var streamsInput: Bool { + switch self { + case .clientStreaming, .bidirectionalStreaming: + return true + case .unary, .serverStreaming: + return false + } + } + + var streamsOutput: Bool { + switch self { + case .serverStreaming, .bidirectionalStreaming: + return true + case .unary, .clientStreaming: + return false + } + } +} From 3cbc033f16bd29ed5837fe60837192be3fdbd69c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 20 Nov 2024 13:57:52 +0000 Subject: [PATCH 501/580] Add structured swift represntation for the generated server (#2122) Motivation: Follow-up from #2117, we also need the server code. Modifications: - Add structured swift for server code Result: Can build server code from structured swift --- .../Internal/StructuredSwift+Server.swift | 425 ++++++++++++++++++ .../StructuredSwift+ServerTests.swift | 336 ++++++++++++++ 2 files changed, 761 insertions(+) create mode 100644 Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift new file mode 100644 index 000000000..c46986fa3 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -0,0 +1,425 @@ +/* + * 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. + */ + +extension FunctionSignatureDescription { + /// ``` + /// func ( + /// request: GRPCCore.ServerRequest, + /// context: GRPCCore.ServerContext + /// ) async throws -> GRPCCore.ServerResponse + /// ``` + static func serverMethod( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + return FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name), + parameters: [ + ParameterDescription( + label: "request", + type: .serverRequest(forType: input, streaming: streamingInput) + ), + ParameterDescription(label: "context", type: .serverContext), + ], + keywords: [.async, .throws], + returnType: .identifierType(.serverResponse(forType: output, streaming: streamingOutput)) + ) + } +} + +extension ProtocolDescription { + /// ``` + /// protocol : GRPCCore.RegistrableRPCService { + /// ... + /// } + /// ``` + static func streamingService( + accessLevel: AccessModifier? = nil, + name: String, + methods: [MethodDescriptor] + ) -> Self { + return ProtocolDescription( + accessModifier: accessLevel, + name: name, + conformances: ["GRPCCore.RegistrableRPCService"], + members: methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + signature: .serverMethod( + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: true, + streamingOutput: true + ) + ) + ) + } + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension { + /// func registerMethods(with router: inout GRPCCore.RPCRouter) { + /// // ... + /// } + /// } + /// ``` + static func registrableRPCServiceDefaultImplementation( + accessLevel: AccessModifier? = nil, + on extensionName: String, + serviceNamespace: String, + methods: [MethodDescriptor], + serializer: (String) -> String, + deserializer: (String) -> String + ) -> Self { + return ExtensionDescription( + onType: extensionName, + declarations: [ + .function( + .registerMethods( + accessLevel: accessLevel, + serviceNamespace: serviceNamespace, + methods: methods, + serializer: serializer, + deserializer: deserializer + ) + ) + ] + ) + } +} + +extension ProtocolDescription { + /// ``` + /// protocol : { + /// ... + /// } + /// ``` + static func service( + accessLevel: AccessModifier? = nil, + name: String, + streamingProtocol: String, + methods: [MethodDescriptor] + ) -> Self { + return ProtocolDescription( + accessModifier: accessLevel, + name: name, + conformances: [streamingProtocol], + members: methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + signature: .serverMethod( + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + ) + } + ) + } +} + +extension FunctionCallDescription { + /// ``` + /// self.(request: request, context: context) + /// ``` + static func serverMethodCallOnSelf( + name: String, + requestArgument: Expression = .identifierPattern("request") + ) -> Self { + return FunctionCallDescription( + calledExpression: .memberAccess( + MemberAccessDescription( + left: .identifierPattern("self"), + right: name + ) + ), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: requestArgument + ), + FunctionArgumentDescription( + label: "context", + expression: .identifierPattern("context") + ), + ] + ) + } +} + +extension ClosureInvocationDescription { + /// ``` + /// { router, context in + /// try await self.( + /// request: request, + /// context: context + /// ) + /// } + /// ``` + static func routerHandlerInvokingRPC(method: String) -> Self { + return ClosureInvocationDescription( + argumentNames: ["request", "context"], + body: [ + .expression( + .unaryKeyword( + kind: .try, + expression: .unaryKeyword( + kind: .await, + expression: .functionCall(.serverMethodCallOnSelf(name: method)) + ) + ) + ) + ] + ) + } +} + +/// ``` +/// router.registerHandler( +/// forMethod: ..., +/// deserializer: ... +/// serializer: ... +/// handler: { request, context in +/// // ... +/// } +/// ) +/// ``` +extension FunctionCallDescription { + static func registerWithRouter( + serviceNamespace: String, + methodNamespace: String, + methodName: String, + inputDeserializer: String, + outputSerializer: String + ) -> Self { + return FunctionCallDescription( + calledExpression: .memberAccess( + .init(left: .identifierPattern("router"), right: "registerHandler") + ), + arguments: [ + FunctionArgumentDescription( + label: "forMethod", + expression: .identifierPattern("\(serviceNamespace).Method.\(methodNamespace).descriptor") + ), + FunctionArgumentDescription( + label: "deserializer", + expression: .identifierPattern(inputDeserializer) + ), + FunctionArgumentDescription( + label: "serializer", + expression: .identifierPattern(outputSerializer) + ), + FunctionArgumentDescription( + label: "handler", + expression: .closureInvocation(.routerHandlerInvokingRPC(method: methodName)) + ), + ] + ) + } +} + +extension FunctionDescription { + /// ``` + /// func registerMethods(with router: inout GRPCCore.RPCRouter) { + /// // ... + /// } + /// ``` + static func registerMethods( + accessLevel: AccessModifier? = nil, + serviceNamespace: String, + methods: [MethodDescriptor], + serializer: (String) -> String, + deserializer: (String) -> String + ) -> Self { + return FunctionDescription( + accessModifier: accessLevel, + kind: .function(name: "registerMethods"), + parameters: [ + ParameterDescription( + label: "with", + name: "router", + type: .rpcRouter, + `inout`: true + ) + ], + body: methods.map { method in + .functionCall( + .registerWithRouter( + serviceNamespace: serviceNamespace, + methodNamespace: method.name.generatedUpperCase, + methodName: method.name.generatedLowerCase, + inputDeserializer: deserializer(method.inputType), + outputSerializer: serializer(method.outputType) + ) + ) + } + ) + } +} + +extension FunctionDescription { + /// ``` + /// func ( + /// request: GRPCCore.StreamingServerRequest + /// context: GRPCCore.ServerContext + /// ) async throws -> GRPCCore.StreamingServerResponse { + /// let response = try await self.( + /// request: GRPCCore.ServerRequest(stream: request), + /// context: context + /// ) + /// return GRPCCore.StreamingServerResponse(single: response) + /// } + /// ``` + static func serverStreamingMethodsCallingMethod( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> FunctionDescription { + let signature: FunctionSignatureDescription = .serverMethod( + accessLevel: accessLevel, + name: name, + input: input, + output: output, + // This method converts from the fully streamed version to the specified version. + streamingInput: true, + streamingOutput: true + ) + + // Call the underlying function. + let functionCall: Expression = .functionCall( + calledExpression: .memberAccess( + MemberAccessDescription( + left: .identifierPattern("self"), + right: name + ) + ), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: streamingInput + ? .identifierPattern("request") + : .functionCall( + calledExpression: .identifierType(.serverRequest(forType: nil, streaming: false)), + arguments: [ + FunctionArgumentDescription( + label: "stream", + expression: .identifierPattern("request") + ) + ] + ) + ), + FunctionArgumentDescription( + label: "context", + expression: .identifierPattern("context") + ), + ] + ) + + // Call the function and assign to 'response'. + let response: Declaration = .variable( + kind: .let, + left: "response", + right: .unaryKeyword( + kind: .try, + expression: .unaryKeyword( + kind: .await, + expression: functionCall + ) + ) + ) + + // Build the return statement. + let returnExpression: Expression = .unaryKeyword( + kind: .return, + expression: streamingOutput + ? .identifierPattern("response") + : .functionCall( + calledExpression: .identifierType(.serverResponse(forType: nil, streaming: true)), + arguments: [ + FunctionArgumentDescription( + label: "single", + expression: .identifierPattern("response") + ) + ] + ) + ) + + return Self( + signature: signature, + body: [.declaration(response), .expression(returnExpression)] + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension { + /// func ( + /// request: GRPCCore.StreamingServerRequest + /// context: GRPCCore.ServerContext + /// ) async throws -> GRPCCore.StreamingServerResponse { + /// let response = try await self.( + /// request: GRPCCore.ServerRequest(stream: request), + /// context: context + /// ) + /// return GRPCCore.StreamingServerResponse(single: response) + /// } + /// ... + /// } + /// ``` + static func streamingServiceProtocolDefaultImplementation( + accessModifier: AccessModifier? = nil, + on extensionName: String, + methods: [MethodDescriptor] + ) -> Self { + return ExtensionDescription( + onType: extensionName, + declarations: methods.compactMap { method -> Declaration? in + // Bidirectional streaming methods don't need a default implementation as their signatures + // match across the two protocols. + if method.isInputStreaming, method.isOutputStreaming { return nil } + + return .function( + .serverStreamingMethodsCallingMethod( + accessLevel: accessModifier, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + } + ) + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift new file mode 100644 index 000000000..78dbac42d --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -0,0 +1,336 @@ +/* + * 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 Testing + +@testable import GRPCCodeGen + +extension StructuedSwiftTests { + @Suite("Server") + struct Server { + @Test( + "func (request:context:) async throws -> ...", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func serverMethodSignature(access: AccessModifier, kind: RPCKind) { + let decl: FunctionSignatureDescription = .serverMethod( + accessLevel: access, + name: "foo", + input: "Input", + output: "Output", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput + ) + + let expected: String + + switch kind { + case .unary: + expected = """ + \(access) func foo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + """ + case .clientStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + """ + case .serverStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + """ + case .bidirectionalStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + """ + } + + #expect(render(.function(signature: decl)) == expected) + } + + @Test("protocol StreamingServiceProtocol { ... }", arguments: AccessModifier.allCases) + func serverStreamingServiceProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .streamingService( + accessLevel: access, + name: "FooService", + methods: [ + .init( + documentation: "/// Some docs", + name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ) + ] + ) + + let expected = """ + \(access) protocol FooService: GRPCCore.RegistrableRPCService { + /// Some docs + func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + """ + + #expect(render(.protocol(decl)) == expected) + } + + @Test("protocol ServiceProtocol { ... }", arguments: AccessModifier.allCases) + func serverServiceProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .service( + accessLevel: access, + name: "FooService", + streamingProtocol: "FooService_StreamingServiceProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ) + ] + ) + + let expected = """ + \(access) protocol FooService: FooService_StreamingServiceProtocol { + /// Some docs + func foo( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + } + """ + + #expect(render(.protocol(decl)) == expected) + } + + @Test("{ router, context in try await self.(...) }") + func routerHandlerInvokingRPC() { + let expression: ClosureInvocationDescription = .routerHandlerInvokingRPC(method: "foo") + let expected = """ + { request, context in + try await self.foo( + request: request, + context: context + ) + } + """ + #expect(render(.closureInvocation(expression)) == expected) + } + + @Test("router.registerHandler(...) { ... }") + func registerMethodsWithRouter() { + let expression: FunctionCallDescription = .registerWithRouter( + serviceNamespace: "FooService", + methodNamespace: "Bar", + methodName: "bar", + inputDeserializer: "Deserialize()", + outputSerializer: "Serialize()" + ) + + let expected = """ + router.registerHandler( + forMethod: FooService.Method.Bar.descriptor, + deserializer: Deserialize(), + serializer: Serialize(), + handler: { request, context in + try await self.bar( + request: request, + context: context + ) + } + ) + """ + + #expect(render(.functionCall(expression)) == expected) + } + + @Test("func registerMethods(router:)", arguments: AccessModifier.allCases) + func registerMethods(access: AccessModifier) { + let expression: FunctionDescription = .registerMethods( + accessLevel: access, + serviceNamespace: "FooService", + methods: [ + .init( + documentation: "", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ] + ) { type in + "Serialize<\(type)>()" + } deserializer: { type in + "Deserialize<\(type)>()" + } + + let expected = """ + \(access) func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: FooService.Method.Bar.descriptor, + deserializer: Deserialize(), + serializer: Serialize(), + handler: { request, context in + try await self.bar( + request: request, + context: context + ) + } + ) + } + """ + + #expect(render(.function(expression)) == expected) + } + + @Test( + "func (request:context:) async throw { ... (convert to/from single) ... }", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func serverStreamingMethodsCallingMethod(access: AccessModifier, kind: RPCKind) { + let expression: FunctionDescription = .serverStreamingMethodsCallingMethod( + accessLevel: access, + name: "foo", + input: "Input", + output: "Output", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput + ) + + let expected: String + + switch kind { + case .unary: + expected = """ + \(access) func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.foo( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + """ + case .serverStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.foo( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response + } + """ + case .clientStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.foo( + request: request, + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + """ + case .bidirectionalStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.foo( + request: request, + context: context + ) + return response + } + """ + } + + #expect(render(.function(expression)) == expected) + } + + @Test("extension FooService_ServiceProtocol { ... }", arguments: AccessModifier.allCases) + func streamingServiceProtocolDefaultImplementation(access: AccessModifier) { + let decl: ExtensionDescription = .streamingServiceProtocolDefaultImplementation( + accessModifier: access, + on: "Foo_ServiceProtocol", + methods: [ + .init( + documentation: "", + name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "FooInput", + outputType: "FooOutput" + ), + // Will be ignored as a bidirectional streaming method. + .init( + documentation: "", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: true, + isOutputStreaming: true, + inputType: "BarInput", + outputType: "BarOutput" + ), + ] + ) + + let expected = """ + extension Foo_ServiceProtocol { + \(access) func foo( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.foo( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) + } + } + """ + + #expect(render(.extension(decl)) == expected) + } + } +} From 52605795e3b007e1e8bc50ac40bca342c2432795 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 20 Nov 2024 16:23:38 +0000 Subject: [PATCH 502/580] Simplify typealias translator (#2126) Motivation: The typealias used to generte an enum per package name and nested enums for each service within that namespace. In order for that to work, the services had to be grouped by namespace. This is no longer the case, each service is generated within a combined package-service enum. It's simpler to just generate the services in the order they are created. Modifications: - Remove the sorting - Remove tests which test the unwanted behaviour Result: Less code --- .../Translator/TypealiasTranslator.swift | 27 +- ...TypealiasTranslatorSnippetBasedTests.swift | 369 ------------------ 2 files changed, 12 insertions(+), 384 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 7f36fe70e..8ae83f70e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -81,25 +81,22 @@ struct TypealiasTranslator: SpecializedTranslator { func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { var codeBlocks = [CodeBlock]() - let services = codeGenerationRequest.services - let servicesByEnumName = Dictionary( - grouping: services, - by: { $0.namespacedGeneratedName } - ) - // Sorting the keys of the dictionary is necessary so that the generated enums are deterministically ordered. - for (generatedEnumName, services) in servicesByEnumName.sorted(by: { $0.key < $1.key }) { - for service in services { - codeBlocks.append( - CodeBlock( - item: .declaration(try self.makeServiceEnum(from: service, named: generatedEnumName)) + for service in codeGenerationRequest.services { + codeBlocks.append( + CodeBlock( + item: .declaration( + try self.makeServiceEnum( + from: service, + named: service.namespacedGeneratedName + ) ) ) + ) - codeBlocks.append( - CodeBlock(item: .declaration(self.makeServiceDescriptorExtension(for: service))) - ) - } + codeBlocks.append( + CodeBlock(item: .declaration(self.makeServiceDescriptorExtension(for: service))) + ) } return codeBlocks diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 658a41b66..a8294ef31 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -301,85 +301,6 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ) } - func testTypealiasTranslatorCheckMethodsOrder() throws { - let methodA = MethodDescriptor( - documentation: "Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let methodB = MethodDescriptor( - documentation: "Documentation for MethodB", - name: Name(base: "MethodB", generatedUpperCase: "MethodB", generatedLowerCase: "methodB"), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [methodA, methodB] - ) - let expectedSwift = - """ - public enum NamespaceA_ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA - public enum Method { - public enum MethodA { - public typealias Input = NamespaceA_ServiceARequest - public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, - method: "MethodA" - ) - } - public enum MethodB { - public typealias Input = NamespaceA_ServiceARequest - public typealias Output = NamespaceA_ServiceAResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, - method: "MethodB" - ) - } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - MethodA.descriptor, - MethodB.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_ServiceA_Client - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .public - ) - } - func testTypealiasTranslatorNoMethodsService() throws { let service = ServiceDescriptor( documentation: "Documentation for ServiceA", @@ -423,296 +344,6 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { accessLevel: .package ) } - - func testTypealiasTranslatorServiceAlphabeticalOrder() throws { - let serviceB = ServiceDescriptor( - documentation: "Documentation for BService", - name: Name(base: "BService", generatedUpperCase: "Bservice", generatedLowerCase: "bservice"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - - let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "Aservice", generatedLowerCase: "aservice"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - - let expectedSwift = - """ - public enum NamespaceA_Aservice { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_AService - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_Aservice_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_Aservice_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_Aservice_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_Aservice_Client - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_AService = Self( - package: "namespaceA", - service: "AService" - ) - } - public enum NamespaceA_Bservice { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_BService - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = NamespaceA_Bservice_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = NamespaceA_Bservice_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = NamespaceA_Bservice_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = NamespaceA_Bservice_Client - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_BService = Self( - package: "namespaceA", - service: "BService" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .public - ) - } - - func testTypealiasTranslatorServiceAlphabeticalOrderNoNamespace() throws { - let serviceB = ServiceDescriptor( - documentation: "Documentation for BService", - name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bservice"), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), - methods: [] - ) - - let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aservice"), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), - methods: [] - ) - - let expectedSwift = - """ - package enum AService { - package static let descriptor = GRPCCore.ServiceDescriptor.AService - package enum Method { - package static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = AService_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = AService_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = AService_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = AService_Client - } - extension GRPCCore.ServiceDescriptor { - package static let AService = Self( - package: "", - service: "AService" - ) - } - package enum BService { - package static let descriptor = GRPCCore.ServiceDescriptor.BService - package enum Method { - package static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = BService_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = BService_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = BService_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = BService_Client - } - extension GRPCCore.ServiceDescriptor { - package static let BService = Self( - package: "", - service: "BService" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .package - ) - } - - func testTypealiasTranslatorNamespaceAlphabeticalOrder() throws { - let serviceB = ServiceDescriptor( - documentation: "Documentation for BService", - name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bservice"), - namespace: Name( - base: "bnamespace", - generatedUpperCase: "Bnamespace", - generatedLowerCase: "bnamespace" - ), - methods: [] - ) - - let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aservice"), - namespace: Name( - base: "anamespace", - generatedUpperCase: "Anamespace", - generatedLowerCase: "anamespace" - ), - methods: [] - ) - - let expectedSwift = - """ - internal enum Anamespace_AService { - internal static let descriptor = GRPCCore.ServiceDescriptor.anamespace_AService - internal enum Method { - internal static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Anamespace_AService_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Anamespace_AService_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Anamespace_AService_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Anamespace_AService_Client - } - extension GRPCCore.ServiceDescriptor { - internal static let anamespace_AService = Self( - package: "anamespace", - service: "AService" - ) - } - internal enum Bnamespace_BService { - internal static let descriptor = GRPCCore.ServiceDescriptor.bnamespace_BService - internal enum Method { - internal static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias StreamingServiceProtocol = Bnamespace_BService_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ServiceProtocol = Bnamespace_BService_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Bnamespace_BService_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Bnamespace_BService_Client - } - extension GRPCCore.ServiceDescriptor { - internal static let bnamespace_BService = Self( - package: "bnamespace", - service: "BService" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [serviceB, serviceA]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .internal - ) - } - - func testTypealiasTranslatorNamespaceNoNamespaceOrder() throws { - let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", - name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), - namespace: Name( - base: "anamespace", - generatedUpperCase: "Anamespace", - generatedLowerCase: "anamespace" - ), - methods: [] - ) - let serviceB = ServiceDescriptor( - documentation: "Documentation for BService", - name: Name(base: "BService", generatedUpperCase: "BService", generatedLowerCase: "bService"), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), - methods: [] - ) - let expectedSwift = - """ - public enum Anamespace_AService { - public static let descriptor = GRPCCore.ServiceDescriptor.anamespace_AService - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Anamespace_AService_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Anamespace_AService_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = Anamespace_AService_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = Anamespace_AService_Client - } - extension GRPCCore.ServiceDescriptor { - public static let anamespace_AService = Self( - package: "anamespace", - service: "AService" - ) - } - public enum BService { - public static let descriptor = GRPCCore.ServiceDescriptor.BService - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = BService_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = BService_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ClientProtocol = BService_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias Client = BService_Client - } - extension GRPCCore.ServiceDescriptor { - public static let BService = Self( - package: "", - service: "BService" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .public - ) - } } extension TypealiasTranslatorSnippetBasedTests { From 8d0bf6f088286be1446c596ee2ab324959992051 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 20 Nov 2024 17:35:14 +0000 Subject: [PATCH 503/580] Add compatibility doc (#2123) --- .../Articles/Compatibility.md | 22 +++++++++++++++++++ .../Documentation.docc/Documentation.md | 4 ++++ 2 files changed, 26 insertions(+) create mode 100644 Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md diff --git a/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md b/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md new file mode 100644 index 000000000..9748c0d0e --- /dev/null +++ b/Sources/GRPCCore/Documentation.docc/Articles/Compatibility.md @@ -0,0 +1,22 @@ +# Compatibility + +Learn which versions of Swift are supported by gRPC Swift and how this changes +over time. + +## Overview + +### Supported Swift Versions + +gRPC Swift supports the **three most recent Swift versions** that are **Swift 6 +or newer**. Within a minor Swift release only the latest patch version is supported. + +| grpc-swift | Minimum Supported Swift version | +|------------|---------------------------------| +| 2.0 | 6.0 | + +### Platforms + +gRPC Swift aims to support the same platforms as Swift project. These are listed +on [swift.org](https://www.swift.org/platform-support/). + +The only known unsupported platform from that list is currently Windows. diff --git a/Sources/GRPCCore/Documentation.docc/Documentation.md b/Sources/GRPCCore/Documentation.docc/Documentation.md index 2e0103514..258a45a9e 100644 --- a/Sources/GRPCCore/Documentation.docc/Documentation.md +++ b/Sources/GRPCCore/Documentation.docc/Documentation.md @@ -53,6 +53,10 @@ as tutorials. - +### Project Information + +- + ### Getting involved Resources for developers working on gRPC Swift: From 3b0fe70fb1bd3f9dd2877220c9caaedbaadb4d73 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 21 Nov 2024 10:23:44 +0000 Subject: [PATCH 504/580] Add 'with-' methods for client and server (#2121) Motivation: In some situations, like examples, testing, and prototyping, it can be useful to have a client and server with scoped lifetimes. This is all achievable using task groups but in a number of situations having helpers is also useful. Modifications: - Add 'with-' methods for client and server - Update docs Result: Easier to use API for some scenarios. --- Sources/GRPCCore/GRPCClient.swift | 139 +++++++++--------- Sources/GRPCCore/GRPCServer.swift | 99 +++++++++++-- Tests/GRPCCoreTests/GRPCClientTests.swift | 23 ++- Tests/GRPCCoreTests/GRPCServerTests.swift | 22 ++- .../ClientServerWithMethods.swift | 56 +++++++ 5 files changed, 234 insertions(+), 105 deletions(-) create mode 100644 Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift diff --git a/Sources/GRPCCore/GRPCClient.swift b/Sources/GRPCCore/GRPCClient.swift index 79e3deb4a..0ac39c8e0 100644 --- a/Sources/GRPCCore/GRPCClient.swift +++ b/Sources/GRPCCore/GRPCClient.swift @@ -28,79 +28,25 @@ private import Synchronization /// /// However, in most cases you should prefer wrapping the ``GRPCClient`` with a generated stub. /// -/// You can set ``ServiceConfig``s on this client to override whatever configurations have been -/// set on the given transport. You can also use ``ClientInterceptor``s to implement cross-cutting -/// logic which apply to all RPCs. Example uses of interceptors include authentication and logging. +/// ## Creating a client /// -/// ## Creating and configuring a client -/// -/// The following example demonstrates how to create and configure a client. +/// You can create and run a client using ``withGRPCClient(transport:interceptors:isolation:handleClient:)`` +/// or ``withGRPCClient(transport:interceptorPipeline:isolation:handleClient:)`` which create, configure and +/// run the client providing scoped access to it via the `handleClient` closure. The client will +/// begin gracefully shutting down when the closure returns. /// /// ```swift -/// // Create a configuration object for the client and override the timeout for the 'Get' method on -/// // the 'echo.Echo' service. This configuration takes precedence over any set by the transport. -/// var configuration = GRPCClient.Configuration() -/// configuration.service.override = ServiceConfig( -/// methodConfig: [ -/// MethodConfig( -/// names: [ -/// MethodConfig.Name(service: "echo.Echo", method: "Get") -/// ], -/// timeout: .seconds(5) -/// ) -/// ] -/// ) -/// -/// // Configure a fallback timeout for all RPCs (indicated by an empty service and method name) if -/// // no configuration is provided in the overrides or by the transport. -/// configuration.service.defaults = ServiceConfig( -/// methodConfig: [ -/// MethodConfig( -/// names: [ -/// MethodConfig.Name(service: "", method: "") -/// ], -/// timeout: .seconds(10) -/// ) -/// ] -/// ) -/// -/// // Finally create a transport and instantiate the client, adding an interceptor. -/// let inProcessTransport = InProcessTransport() -/// -/// let client = GRPCClient( -/// transport: inProcessTransport.client, -/// interceptors: [StatsRecordingClientInterceptor()], -/// configuration: configuration -/// ) +/// let transport: any ClientTransport = ... +/// try await withGRPCClient(transport: transport) { client in +/// // ... +/// } /// ``` /// -/// ## Starting and stopping the client +/// ## Creating a client manually /// -/// Once you have configured the client, call ``run()`` to start it. Calling ``run()`` instructs the -/// transport to start connecting to the server. -/// -/// ```swift -/// // Start running the client. 'run()' must be running while RPCs are execute so it's executed in -/// // a task group. -/// try await withThrowingTaskGroup(of: Void.self) { group in -/// group.addTask { -/// try await client.run() -/// } -/// -/// // Execute a request against the "echo.Echo" service. -/// try await client.unary( -/// request: ClientRequest<[UInt8]>(message: [72, 101, 108, 108, 111, 33]), -/// descriptor: MethodDescriptor(service: "echo.Echo", method: "Get"), -/// serializer: IdentitySerializer(), -/// deserializer: IdentityDeserializer(), -/// ) { response in -/// print(response.message) -/// } -/// -/// // The RPC has completed, close the client. -/// client.beginGracefulShutdown() -/// } -/// ``` +/// If the `with`-style methods for creating clients isn't suitable for your application then you +/// can create and run a client manually. This requires you to call the ``run()`` method in a task +/// which instructs the client to start connecting to the server. /// /// The ``run()`` method won't return until the client has finished handling all requests. You can /// signal to the client that it should stop creating new request streams by calling ``beginGracefulShutdown()``. @@ -425,3 +371,62 @@ public final class GRPCClient: Sendable { ) } } + +/// Creates and runs a new client with the given transport and interceptors. +/// +/// - Parameters: +/// - transport: The transport used to establish a communication channel with a server. +/// - interceptors: A collection of ``ClientInterceptor``s providing cross-cutting functionality to each +/// accepted RPC. The order in which interceptors are added reflects the order in which they +/// are called. The first interceptor added will be the first interceptor to intercept each +/// request. The last interceptor added will be the final interceptor to intercept each +/// request before calling the appropriate handler. +/// - isolation: A reference to the actor to which the enclosing code is isolated, or nil if the +/// code is nonisolated. +/// - handleClient: A closure which is called with the client. When the closure returns, the +/// client is shutdown gracefully. +public func withGRPCClient( + transport: some ClientTransport, + interceptors: [any ClientInterceptor] = [], + isolation: isolated (any Actor)? = #isolation, + handleClient: (GRPCClient) async throws -> Result +) async throws -> Result { + try await withGRPCClient( + transport: transport, + interceptorPipeline: interceptors.map { .apply($0, to: .all) }, + isolation: isolation, + handleClient: handleClient + ) +} + +/// Creates and runs a new client with the given transport and interceptors. +/// +/// - Parameters: +/// - transport: The transport used to establish a communication channel with a server. +/// - interceptorPipeline: A collection of ``ClientInterceptorPipelineOperation`` providing cross-cutting +/// functionality to each accepted RPC. Only applicable interceptors from the pipeline will be applied to each RPC. +/// The order in which interceptors are added reflects the order in which they are called. +/// The first interceptor added will be the first interceptor to intercept each request. +/// The last interceptor added will be the final interceptor to intercept each request before calling the appropriate handler. +/// - isolation: A reference to the actor to which the enclosing code is isolated, or nil if the +/// code is nonisolated. +/// - handleClient: A closure which is called with the client. When the closure returns, the +/// client is shutdown gracefully. +/// - Returns: The result of the `handleClient` closure. +public func withGRPCClient( + transport: some ClientTransport, + interceptorPipeline: [ClientInterceptorPipelineOperation], + isolation: isolated (any Actor)? = #isolation, + handleClient: (GRPCClient) async throws -> Result +) async throws -> Result { + try await withThrowingDiscardingTaskGroup { group in + let client = GRPCClient(transport: transport, interceptorPipeline: interceptorPipeline) + group.addTask { + try await client.run() + } + + let result = try await handleClient(client) + client.beginGracefulShutdown() + return result + } +} diff --git a/Sources/GRPCCore/GRPCServer.swift b/Sources/GRPCCore/GRPCServer.swift index 6ff82b9dd..f8f576e65 100644 --- a/Sources/GRPCCore/GRPCServer.swift +++ b/Sources/GRPCCore/GRPCServer.swift @@ -29,13 +29,13 @@ private import Synchronization /// include request filtering, authentication, and logging. Once requests have been intercepted /// they are passed to a handler which in turn returns a response to send back to the client. /// -/// ## Creating and configuring a server +/// ## Configuring and starting a server /// -/// The following example demonstrates how to create and configure a server. +/// The following example demonstrates how to create and run a server. /// /// ```swift -/// // Create and an in-process transport. -/// let inProcessTransport = InProcessTransport() +/// // Create an transport +/// let transport: any ServerTransport = ... /// /// // Create the 'Greeter' and 'Echo' services. /// let greeter = GreeterService() @@ -44,19 +44,24 @@ private import Synchronization /// // Create an interceptor. /// let statsRecorder = StatsRecordingServerInterceptors() /// -/// // Finally create the server. -/// let server = GRPCServer( -/// transport: inProcessTransport.server, +/// // Run the server. +/// try await withGRPCServer( +/// transport: transport, /// services: [greeter, echo], /// interceptors: [statsRecorder] -/// ) +/// ) { server in +/// // ... +/// // The server begins shutting down when this closure returns +/// // ... +/// } /// ``` /// -/// ## Starting and stopping the server +/// ## Creating a client manually /// -/// Once you have configured the server call ``serve()`` to start it. Calling ``serve()`` starts the server's -/// transport too. A ``RuntimeError`` is thrown if the transport can't be started or encounters some other -/// runtime error. +/// If the `with`-style methods for creating a server isn't suitable for your application then you +/// can create and run it manually. This requires you to call the ``serve()`` method in a task +/// which instructs the server to start its transport and listen for new RPCs. A ``RuntimeError`` is +/// thrown if the transport can't be started or encounters some other runtime error. /// /// ```swift /// // Start running the server. @@ -235,3 +240,73 @@ public final class GRPCServer: Sendable { } } } + +/// Creates and runs a gRPC server. +/// +/// - Parameters: +/// - transport: The transport the server should listen on. +/// - services: Services offered by the server. +/// - interceptors: A collection of interceptors providing cross-cutting functionality to each +/// accepted RPC. The order in which interceptors are added reflects the order in which they +/// are called. The first interceptor added will be the first interceptor to intercept each +/// request. The last interceptor added will be the final interceptor to intercept each +/// request before calling the appropriate handler. +/// - isolation: A reference to the actor to which the enclosing code is isolated, or nil if the +/// code is nonisolated. +/// - handleServer: A closure which is called with the server. When the closure returns, the +/// server is shutdown gracefully. +/// - Returns: The result of the `handleServer` closure. +public func withGRPCServer( + transport: any ServerTransport, + services: [any RegistrableRPCService], + interceptors: [any ServerInterceptor] = [], + isolation: isolated (any Actor)? = #isolation, + handleServer: (GRPCServer) async throws -> Result +) async throws -> Result { + try await withGRPCServer( + transport: transport, + services: services, + interceptorPipeline: interceptors.map { .apply($0, to: .all) }, + isolation: isolation, + handleServer: handleServer + ) +} + +/// Creates and runs a gRPC server. +/// +/// - Parameters: +/// - transport: The transport the server should listen on. +/// - services: Services offered by the server. +/// - interceptorPipeline: A collection of interceptors providing cross-cutting functionality to each +/// accepted RPC. The order in which interceptors are added reflects the order in which they +/// are called. The first interceptor added will be the first interceptor to intercept each +/// request. The last interceptor added will be the final interceptor to intercept each +/// request before calling the appropriate handler. +/// - isolation: A reference to the actor to which the enclosing code is isolated, or nil if the +/// code is nonisolated. +/// - handleServer: A closure which is called with the server. When the closure returns, the +/// server is shutdown gracefully. +/// - Returns: The result of the `handleServer` closure. +public func withGRPCServer( + transport: any ServerTransport, + services: [any RegistrableRPCService], + interceptorPipeline: [ServerInterceptorPipelineOperation], + isolation: isolated (any Actor)? = #isolation, + handleServer: (GRPCServer) async throws -> Result +) async throws -> Result { + return try await withThrowingDiscardingTaskGroup { group in + let server = GRPCServer( + transport: transport, + services: services, + interceptorPipeline: interceptorPipeline + ) + + group.addTask { + try await server.serve() + } + + let result = try await handleServer(server) + server.beginGracefulShutdown() + return result + } +} diff --git a/Tests/GRPCCoreTests/GRPCClientTests.swift b/Tests/GRPCCoreTests/GRPCClientTests.swift index ed5396da1..ca8331b61 100644 --- a/Tests/GRPCCoreTests/GRPCClientTests.swift +++ b/Tests/GRPCCoreTests/GRPCClientTests.swift @@ -29,20 +29,17 @@ final class GRPCClientTests: XCTestCase { let client = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) let server = GRPCServer(transport: inProcess.server, services: services) - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await server.serve() - } - - group.addTask { - try await client.run() + try await withGRPCServer( + transport: inProcess.server, + services: services + ) { server in + try await withGRPCClient( + transport: inProcess.client, + interceptorPipeline: interceptorPipeline + ) { client in + try await Task.sleep(for: .milliseconds(100)) + try await body(client, server) } - - // Make sure both server and client are running - try await Task.sleep(for: .milliseconds(100)) - try await body(client, server) - client.beginGracefulShutdown() - server.beginGracefulShutdown() } } diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index 9b20785d5..b61fb2022 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -26,24 +26,20 @@ final class GRPCServerTests: XCTestCase { _ body: (InProcessTransport.Client, GRPCServer) async throws -> Void ) async throws { let inProcess = InProcessTransport() - let server = GRPCServer( + + try await withGRPCServer( transport: inProcess.server, services: services, interceptorPipeline: interceptorPipeline - ) - - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await server.serve() - } + ) { server in + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await inProcess.client.connect() + } - group.addTask { - try await inProcess.client.connect() + try await body(inProcess.client, server) + inProcess.client.beginGracefulShutdown() } - - try await body(inProcess.client, server) - inProcess.client.beginGracefulShutdown() - server.beginGracefulShutdown() } } diff --git a/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift b/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift new file mode 100644 index 000000000..930b18183 --- /dev/null +++ b/Tests/GRPCInProcessTransportTests/ClientServerWithMethods.swift @@ -0,0 +1,56 @@ +/* + * 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 GRPCInProcessTransport +import Testing + +@Suite("withGRPCServer / withGRPCClient") +struct WithMethods { + @Test("Actor isolation") + func actorIsolation() async throws { + let testActor = TestActor() + #expect(await !testActor.hasRun) + try await testActor.run() + #expect(await testActor.hasRun) + } +} + +fileprivate actor TestActor { + private(set) var hasRun = false + + func run() async throws { + let inProcess = InProcessTransport() + + try await withGRPCServer(transport: inProcess.server, services: []) { server in + do { + try await withGRPCClient(transport: inProcess.client) { client in + self.hasRun = true + } + } catch { + // Starting the client can race with the closure returning which begins graceful shutdown. + // If that happens the client run method will throw an error as the client is being run + // when it's already been shutdown. That's okay and expected so rather than slowing down + // the closure tolerate that specific error. + if let error = error as? RuntimeError { + #expect(error.code == .clientIsStopped) + } else { + Issue.record(error) + } + } + } + } +} From ef48ef883445178cd708eceeb3bae38fd3f5deba Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 22 Nov 2024 09:20:13 +0000 Subject: [PATCH 505/580] Use ServiceDescriptor within MethodDescriptor (#2127) Motivation: MethodDescriptor uses a string to represent the fully qualified name of the service the method belongs to. ServiceDescriptor also represents a fully qualified name of a service. It'd make more sense for MethodDescriptor to use a ServiceDescriptor instead of a string to describe the service. Modifications: - ServiceDescriptor now stores the fully qualified service name (as this is generally more useful) and computes the package and unqualified service name. - ServiceDescriptor can now be created from the fully qualified name or from a separate package and service name. - MethodDescriptor now holds a ServiceDescriptor instead of a String for the service Result: More coherent API --- .../ClientInterceptorPipelineOperation.swift | 2 +- .../ServerInterceptorPipelineOperation.swift | 2 +- Sources/GRPCCore/Internal/MethodConfigs.swift | 10 ++++- Sources/GRPCCore/MethodDescriptor.swift | 37 ++++++++++++++--- Sources/GRPCCore/ServiceDescriptor.swift | 40 ++++++++++++++----- ...entInterceptorPipelineOperationTests.swift | 8 ++-- .../ClientRPCExecutorTestHarness.swift | 2 +- .../ServerRPCExecutorTestHarness.swift | 2 +- .../Call/Server/RPCRouterTests.swift | 12 ++++-- .../ServerInterceptorPipelineOperation.swift | 8 ++-- Tests/GRPCCoreTests/GRPCClientTests.swift | 12 +++--- Tests/GRPCCoreTests/GRPCServerTests.swift | 4 +- .../Internal/MethodConfigsTests.swift | 10 ++--- .../GRPCCoreTests/MethodDescriptorTests.swift | 24 ++++++++--- .../ServiceDescriptorTests.swift | 32 +++++++++++---- .../Test Utilities/Services/BinaryEcho.swift | 8 ++-- .../Test Utilities/Services/HelloWorld.swift | 2 +- .../InProcessClientTransportTests.swift | 33 +++++---------- .../InProcessServerTransportTests.swift | 6 +-- .../InProcessTransportTests.swift | 5 ++- 20 files changed, 169 insertions(+), 90 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift index 4ae2df8d5..d67047bc7 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift @@ -61,7 +61,7 @@ public struct ClientInterceptorPipelineOperation: Sendable { return true case .services(let services): - return services.map({ $0.fullyQualifiedService }).contains(descriptor.service) + return services.contains(descriptor.service) case .methods(let methods): return methods.contains(descriptor) diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift index e511ea3ec..e657fdf1c 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift @@ -61,7 +61,7 @@ public struct ServerInterceptorPipelineOperation: Sendable { return true case .services(let services): - return services.map({ $0.fullyQualifiedService }).contains(descriptor.service) + return services.contains(descriptor.service) case .methods(let methods): return methods.contains(descriptor) diff --git a/Sources/GRPCCore/Internal/MethodConfigs.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift index 163122dfe..6aab8858d 100644 --- a/Sources/GRPCCore/Internal/MethodConfigs.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -51,7 +51,10 @@ package struct MethodConfigs: Sendable, Hashable { /// - descriptor: The ``MethodDescriptor`` for which to get or set a ``MethodConfig``. package subscript(_ descriptor: MethodDescriptor) -> MethodConfig? { get { - var name = MethodConfig.Name(service: descriptor.service, method: descriptor.method) + var name = MethodConfig.Name( + service: descriptor.service.fullyQualifiedService, + method: descriptor.method + ) if let configuration = self.elements[name] { return configuration @@ -70,7 +73,10 @@ package struct MethodConfigs: Sendable, Hashable { } set { - let name = MethodConfig.Name(service: descriptor.service, method: descriptor.method) + let name = MethodConfig.Name( + service: descriptor.service.fullyQualifiedService, + method: descriptor.method + ) self.elements[name] = newValue } } diff --git a/Sources/GRPCCore/MethodDescriptor.swift b/Sources/GRPCCore/MethodDescriptor.swift index 8d2795ac1..3cfc500c1 100644 --- a/Sources/GRPCCore/MethodDescriptor.swift +++ b/Sources/GRPCCore/MethodDescriptor.swift @@ -16,11 +16,8 @@ /// A description of a method on a service. public struct MethodDescriptor: Sendable, Hashable { - /// The name of the service, including the package name. - /// - /// For example, the name of the "Greeter" service in "helloworld" package - /// is "helloworld.Greeter". - public var service: String + /// A description of the service, including its package name. + public var service: ServiceDescriptor /// The name of the method in the service, excluding the service name. public var method: String @@ -39,8 +36,36 @@ public struct MethodDescriptor: Sendable, Hashable { /// - service: The name of the service, including the package name. For example, /// "helloworld.Greeter". /// - method: The name of the method. For example, "SayHello". - public init(service: String, method: String) { + public init(service: ServiceDescriptor, method: String) { self.service = service self.method = method } + + /// Creates a new method descriptor. + /// + /// - Parameters: + /// - fullyQualifiedService: The fully qualified name of the service, including the package + /// name. For example, "helloworld.Greeter". + /// - method: The name of the method. For example, "SayHello". + public init(fullyQualifiedService: String, method: String) { + self.service = ServiceDescriptor(fullyQualifiedService: fullyQualifiedService) + self.method = method + } + + @available(*, deprecated, renamed: "init(fullyQualifiedService:method:)") + /// Creates a new method descriptor. + /// + /// - Parameters: + /// - service: The fully qualified name of the service, including the package + /// name. For example, "helloworld.Greeter". + /// - method: The name of the method. For example, "SayHello". + public init(service: String, method: String) { + self.init(fullyQualifiedService: service, method: method) + } +} + +extension MethodDescriptor: CustomStringConvertible { + public var description: String { + self.fullyQualifiedMethod + } } diff --git a/Sources/GRPCCore/ServiceDescriptor.swift b/Sources/GRPCCore/ServiceDescriptor.swift index b09730c3b..3a9709d83 100644 --- a/Sources/GRPCCore/ServiceDescriptor.swift +++ b/Sources/GRPCCore/ServiceDescriptor.swift @@ -18,20 +18,33 @@ public struct ServiceDescriptor: Sendable, Hashable { /// The name of the package the service belongs to. For example, "helloworld". /// An empty string means that the service does not belong to any package. - public var package: String + public var package: String { + if let index = self.fullyQualifiedService.utf8.lastIndex(of: UInt8(ascii: ".")) { + return String(self.fullyQualifiedService[.. Void ) async throws { let inProcess = InProcessTransport() - let client = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) - let server = GRPCServer(transport: inProcess.server, services: services) + _ = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) + _ = GRPCServer(transport: inProcess.server, services: services) try await withGRPCServer( transport: inProcess.server, @@ -137,7 +137,7 @@ final class GRPCClientTests: XCTestCase { try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in try await client.unary( request: .init(message: [3, 1, 4, 1, 5]), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults @@ -157,7 +157,7 @@ final class GRPCClientTests: XCTestCase { try await writer.write([byte]) } }), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults @@ -173,7 +173,7 @@ final class GRPCClientTests: XCTestCase { try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in try await client.serverStreaming( request: .init(message: [3, 1, 4, 1, 5]), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults @@ -193,7 +193,7 @@ final class GRPCClientTests: XCTestCase { try await writer.write([byte]) } }), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index b61fb2022..c7856d7c0 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -164,7 +164,7 @@ final class GRPCServerTests: XCTestCase { func testUnimplementedMethod() async throws { try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in try await client.withStream( - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) @@ -248,7 +248,7 @@ final class GRPCServerTests: XCTestCase { interceptorPipeline: [.apply(.requestCounter(counter), to: .all)] ) { client, _ in try await client.withStream( - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) diff --git a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift index 095a5eded..bac916763 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -26,7 +26,7 @@ final class MethodConfigsTests: XCTestCase { let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() configurations.setDefaultConfig(defaultConfiguration) - let descriptor = MethodDescriptor(service: "test", method: "first") + let descriptor = MethodDescriptor(fullyQualifiedService: "test", method: "first") let retryPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -49,7 +49,7 @@ final class MethodConfigsTests: XCTestCase { let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() configurations.setDefaultConfig(defaultConfiguration) - let firstDescriptor = MethodDescriptor(service: "test", method: "") + let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "") let retryPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -60,7 +60,7 @@ final class MethodConfigsTests: XCTestCase { let overrideConfiguration = MethodConfig(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration - let secondDescriptor = MethodDescriptor(service: "test", method: "second") + let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "second") XCTAssertEqual(configurations[secondDescriptor], overrideConfiguration) } @@ -73,7 +73,7 @@ final class MethodConfigsTests: XCTestCase { let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() configurations.setDefaultConfig(defaultConfiguration) - let firstDescriptor = MethodDescriptor(service: "test1", method: "first") + let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test1", method: "first") let retryPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -84,7 +84,7 @@ final class MethodConfigsTests: XCTestCase { let overrideConfiguration = MethodConfig(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration - let secondDescriptor = MethodDescriptor(service: "test2", method: "second") + let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test2", method: "second") XCTAssertEqual(configurations[secondDescriptor], defaultConfiguration) } } diff --git a/Tests/GRPCCoreTests/MethodDescriptorTests.swift b/Tests/GRPCCoreTests/MethodDescriptorTests.swift index cf4568898..889a0c878 100644 --- a/Tests/GRPCCoreTests/MethodDescriptorTests.swift +++ b/Tests/GRPCCoreTests/MethodDescriptorTests.swift @@ -14,13 +14,25 @@ * limitations under the License. */ import GRPCCore -import XCTest +import Testing -final class MethodDescriptorTests: XCTestCase { +@Suite +struct MethodDescriptorTests { + @Test("Fully qualified name") func testFullyQualifiedName() { - let descriptor = MethodDescriptor(service: "foo.bar", method: "Baz") - XCTAssertEqual(descriptor.service, "foo.bar") - XCTAssertEqual(descriptor.method, "Baz") - XCTAssertEqual(descriptor.fullyQualifiedMethod, "foo.bar/Baz") + let descriptor = MethodDescriptor(fullyQualifiedService: "foo.bar", method: "Baz") + #expect(descriptor.service == ServiceDescriptor(fullyQualifiedService: "foo.bar")) + #expect(descriptor.method == "Baz") + #expect(descriptor.fullyQualifiedMethod == "foo.bar/Baz") + } + + @Test("CustomStringConvertible") + func description() { + let descriptor = MethodDescriptor( + service: ServiceDescriptor(fullyQualifiedService: "foo.Foo"), + method: "Bar" + ) + + #expect(String(describing: descriptor) == "foo.Foo/Bar") } } diff --git a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift index 0adfe524a..ef4ec8988 100644 --- a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift +++ b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift @@ -14,13 +14,31 @@ * limitations under the License. */ import GRPCCore -import XCTest +import Testing -final class ServiceDescriptorTests: XCTestCase { - func testFullyQualifiedName() { - let descriptor = ServiceDescriptor(package: "foo.bar", service: "Baz") - XCTAssertEqual(descriptor.package, "foo.bar") - XCTAssertEqual(descriptor.service, "Baz") - XCTAssertEqual(descriptor.fullyQualifiedService, "foo.bar.Baz") +@Suite +struct ServiceDescriptorTests { + @Test( + "Decompose fully qualified service name", + arguments: [ + ("foo.bar.baz", "foo.bar", "baz"), + ("foo.bar", "foo", "bar"), + ("foo", "", "foo"), + ("..", ".", ""), + (".", "", ""), + ("", "", ""), + ] + ) + func packageAndService(fullyQualified: String, package: String, service: String) { + let descriptor = ServiceDescriptor(fullyQualifiedService: fullyQualified) + #expect(descriptor.fullyQualifiedService == fullyQualified) + #expect(descriptor.package == package) + #expect(descriptor.service == service) + } + + @Test("CustomStringConvertible") + func description() { + let descriptor = ServiceDescriptor(fullyQualifiedService: "foo.Foo") + #expect(String(describing: descriptor) == "foo.Foo") } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index 8d0ece3c7..a0043eda3 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -98,19 +98,19 @@ struct BinaryEcho: RegistrableRPCService { enum Methods { static let get = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Get" ) static let collect = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Collect" ) static let expand = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Expand" ) static let update = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Update" ) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift index 01501e0bb..a464cec02 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift @@ -42,7 +42,7 @@ struct HelloWorld: RegistrableRPCService { enum Methods { static let sayHello = MethodDescriptor( - service: HelloWorld.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: HelloWorld.serviceDescriptor.fullyQualifiedService, method: "SayHello" ) } diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 16b915f41..36ef7b84a 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -110,10 +110,7 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { _ in + try await client.withStream(descriptor: .testTest, options: .defaults) { _ in // Once the pending stream is opened, close the client to new connections, // so that, once this closure is executed and this stream is closed, // the client will return from `connect()`. @@ -138,10 +135,7 @@ final class InProcessClientTransportTests: XCTestCase { client.beginGracefulShutdown() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { _ in } + try await client.withStream(descriptor: .testTest, options: .defaults) { _ in } } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) } @@ -157,10 +151,7 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream in try await stream.outbound.write(.message([1])) await stream.outbound.finish() let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } @@ -212,7 +203,7 @@ final class InProcessClientTransportTests: XCTestCase { serviceConfig: serviceConfig ) - let firstDescriptor = MethodDescriptor(service: "test", method: "first") + let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "first") XCTAssertEqual( client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first @@ -236,7 +227,7 @@ final class InProcessClientTransportTests: XCTestCase { serviceConfig: serviceConfig ) - let secondDescriptor = MethodDescriptor(service: "test", method: "second") + let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "second") XCTAssertEqual( client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first @@ -257,19 +248,13 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream in try await Task.sleep(for: .milliseconds(100)) } } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream in try await Task.sleep(for: .milliseconds(100)) } } @@ -309,3 +294,7 @@ final class InProcessClientTransportTests: XCTestCase { ) } } + +extension MethodDescriptor { + static let testTest = Self(fullyQualifiedService: "test", method: "test") +} diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 45baa66e4..9ecfc4a14 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -28,7 +28,7 @@ final class InProcessServerTransportTests: XCTestCase { RPCAsyncSequence, RPCWriter.Closable >( - descriptor: .init(service: "testService", method: "testMethod"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) @@ -59,7 +59,7 @@ final class InProcessServerTransportTests: XCTestCase { let firstStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( - descriptor: .init(service: "testService1", method: "testMethod1"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) @@ -83,7 +83,7 @@ final class InProcessServerTransportTests: XCTestCase { let secondStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( - descriptor: .init(service: "testService1", method: "testMethod1"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 3396b259f..11edbfd6c 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -109,7 +109,10 @@ private struct TestService: RegistrableRPCService { } extension MethodDescriptor { - fileprivate static let testCancellation = Self(service: "test", method: "cancellation") + fileprivate static let testCancellation = Self( + fullyQualifiedService: "test", + method: "cancellation" + ) } private struct UTF8Serializer: MessageSerializer { From 67efe3c91854f721dafdfec444c44a77bf5b760d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 22 Nov 2024 09:51:37 +0000 Subject: [PATCH 506/580] Add structured swift represntation for the generated client (#2125) Motivation: Follow-up from #2117, we also need the client code. Modifications: - Add structured swift for client code Result: Can build client code from structured swift --- .../Internal/StructuredSwift+Client.swift | 704 ++++++++++++++++++ .../StructuredSwift+ClientTests.swift | 544 ++++++++++++++ 2 files changed, 1248 insertions(+) create mode 100644 Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift new file mode 100644 index 000000000..b4e62194e --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -0,0 +1,704 @@ +/* + * 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. + */ + +extension ClosureInvocationDescription { + /// ``` + /// { response in + /// try response.message + /// } + /// ``` + static var defaultClientUnaryResponseHandler: Self { + ClosureInvocationDescription( + argumentNames: ["response"], + body: [.expression(.try(.identifierPattern("response").dot("message")))] + ) + } +} + +extension FunctionSignatureDescription { + /// ``` + /// func ( + /// request: GRPCCore.ClientRequest, + /// serializer: some GRPCCore.MessageSerializer, + /// deserializer: some GRPCCore.MessageDeserializer, + /// options: GRPCCore.CallOptions, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable + /// ``` + static func clientMethod( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool, + includeDefaults: Bool, + includeSerializers: Bool + ) -> Self { + var signature = FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name, isStatic: false), + generics: [.member("Result")], + parameters: [], // Populated below. + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + whereClause: WhereClause(requirements: [.conformance("Result", "Sendable")]) + ) + + signature.parameters.append( + ParameterDescription( + label: "request", + type: .clientRequest(forType: input, streaming: streamingInput) + ) + ) + + if includeSerializers { + signature.parameters.append( + ParameterDescription( + label: "serializer", + // Type is optional, so be explicit about which 'some' to use + type: ExistingTypeDescription.some(.serializer(forType: input)) + ) + ) + signature.parameters.append( + ParameterDescription( + label: "deserializer", + // Type is optional, so be explicit about which 'some' to use + type: ExistingTypeDescription.some(.deserializer(forType: output)) + ) + ) + } + + signature.parameters.append( + ParameterDescription( + label: "options", + type: .callOptions, + defaultValue: includeDefaults ? .memberAccess(.dot("defaults")) : nil + ) + ) + + signature.parameters.append( + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .clientResponse(forType: output, streaming: streamingOutput) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + sendable: true, + escaping: true + ) + ), + defaultValue: includeDefaults && !streamingOutput + ? .closureInvocation(.defaultClientUnaryResponseHandler) + : nil + ) + ) + + return signature + } +} + +extension FunctionDescription { + /// ``` + /// func ( + /// request: GRPCCore.ClientRequest, + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable { + /// try await self.( + /// request: request, + /// serializer: , + /// deserializer: , + /// options: options + /// onResponse: handleResponse, + /// ) + /// } + /// ``` + static func clientMethodWithDefaults( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool, + serializer: Expression, + deserializer: Expression + ) -> Self { + FunctionDescription( + signature: .clientMethod( + accessLevel: accessLevel, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput, + includeDefaults: true, + includeSerializers: false + ), + body: [ + .expression( + .try( + .await( + .functionCall( + calledExpression: .identifierPattern("self").dot(name), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request") + ), + FunctionArgumentDescription( + label: "serializer", + expression: serializer + ), + FunctionArgumentDescription( + label: "deserializer", + expression: deserializer + ), + FunctionArgumentDescription( + label: "options", + expression: .identifierPattern("options") + ), + FunctionArgumentDescription( + label: "onResponse", + expression: .identifierPattern("handleResponse") + ), + ] + ) + ) + ) + ) + ] + ) + } +} + +extension ProtocolDescription { + /// ``` + /// protocol : Sendable { + /// func foo( + /// ... + /// ) async throws -> Result + /// } + /// ``` + static func clientProtocol( + accessLevel: AccessModifier? = nil, + name: String, + methods: [MethodDescriptor] + ) -> Self { + ProtocolDescription( + accessModifier: accessLevel, + name: name, + conformances: ["Sendable"], + members: methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + signature: .clientMethod( + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + includeDefaults: false, + includeSerializers: true + ) + ) + ) + } + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension { + /// func foo( + /// request: GRPCCore.ClientRequest, + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable { + /// // ... + /// } + /// // ... + /// } + /// ``` + static func clientMethodSignatureWithDefaults( + accessLevel: AccessModifier? = nil, + name: String, + methods: [MethodDescriptor], + serializer: (String) -> String, + deserializer: (String) -> String + ) -> Self { + ExtensionDescription( + onType: name, + declarations: methods.map { method in + .function( + .clientMethodWithDefaults( + accessLevel: accessLevel, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + serializer: .identifierPattern(serializer(method.inputType)), + deserializer: .identifierPattern(deserializer(method.outputType)) + ) + ) + } + ) + } +} + +extension FunctionSignatureDescription { + /// ``` + /// func foo( + /// _ message: , + /// metadata: GRPCCore.Metadata = [:], + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + /// try response.message + /// } + /// ) async throws -> Result where Result: Sendable + /// ``` + static func clientMethodExploded( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + var signature = FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name), + generics: [.member("Result")], + parameters: [], // Populated below + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + whereClause: WhereClause(requirements: [.conformance("Result", "Sendable")]) + ) + + if !streamingInput { + signature.parameters.append( + ParameterDescription(label: "_", name: "message", type: .member(input)) + ) + } + + // metadata: GRPCCore.Metadata = [:] + signature.parameters.append( + ParameterDescription( + label: "metadata", + type: .metadata, + defaultValue: .literal(.dictionary([])) + ) + ) + + // options: GRPCCore.CallOptions = .defaults + signature.parameters.append( + ParameterDescription( + label: "options", + type: .callOptions, + defaultValue: .dot("defaults") + ) + ) + + if streamingInput { + signature.parameters.append( + ParameterDescription( + label: "requestProducer", + name: "producer", + type: .closure( + ClosureSignatureDescription( + parameters: [ParameterDescription(type: .rpcWriter(forType: input))], + keywords: [.async, .throws], + returnType: .identifierPattern("Void"), + sendable: true, + escaping: true + ) + ) + ) + ) + } + + signature.parameters.append( + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .clientResponse(forType: output, streaming: streamingOutput) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + sendable: true, + escaping: true + ) + ), + defaultValue: streamingOutput ? nil : .closureInvocation(.defaultClientUnaryResponseHandler) + ) + ) + + return signature + } +} + +extension [CodeBlock] { + /// ``` + /// let request = GRPCCore.StreamingClientRequest( + /// metadata: metadata, + /// producer: producer + /// ) + /// return try await self.foo( + /// request: request, + /// options: options, + /// onResponse: handleResponse + /// ) + /// ``` + static func clientMethodExploded( + name: String, + input: String, + streamingInput: Bool + ) -> Self { + func arguments(streaming: Bool) -> [FunctionArgumentDescription] { + let metadata = FunctionArgumentDescription( + label: "metadata", + expression: .identifierPattern("metadata") + ) + + if streaming { + return [ + metadata, + FunctionArgumentDescription( + label: "producer", + expression: .identifierPattern("producer") + ), + ] + } else { + return [ + FunctionArgumentDescription(label: "message", expression: .identifierPattern("message")), + metadata, + ] + } + } + + return [ + CodeBlock( + item: .declaration( + .variable( + kind: .let, + left: .identifierPattern("request"), + right: .functionCall( + calledExpression: .identifierType( + .clientRequest(forType: input, streaming: streamingInput) + ), + arguments: arguments(streaming: streamingInput) + ) + ) + ) + ), + CodeBlock( + item: .expression( + .return( + .try( + .await( + .functionCall( + calledExpression: .identifierPattern("self").dot(name), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request") + ), + FunctionArgumentDescription( + label: "options", + expression: .identifierPattern("options") + ), + FunctionArgumentDescription( + label: "onResponse", + expression: .identifierPattern("handleResponse") + ), + ] + ) + ) + ) + ) + ) + ), + ] + } +} + +extension FunctionDescription { + /// ``` + /// func foo( + /// _ message: , + /// metadata: GRPCCore.Metadata = [:], + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + /// try response.message + /// } + /// ) async throws -> Result where Result: Sendable { + /// // ... + /// } + /// ``` + static func clientMethodExploded( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + FunctionDescription( + signature: .clientMethodExploded( + accessLevel: accessLevel, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ), + body: .clientMethodExploded(name: name, input: input, streamingInput: streamingInput) + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension { + /// // (exploded client methods) + /// } + /// ``` + static func explodedClientMethods( + accessLevel: AccessModifier? = nil, + on extensionName: String, + methods: [MethodDescriptor] + ) -> ExtensionDescription { + ExtensionDescription( + onType: extensionName, + declarations: methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + .clientMethodExploded( + accessLevel: accessLevel, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + ) + } + ) + } +} + +extension FunctionDescription { + /// ``` + /// func ( + /// request: GRPCCore.ClientRequest, + /// serializer: some GRPCCore.MessageSerializer, + /// deserializer: some GRPCCore.MessageDeserializer, + /// options: GRPCCore.CallOptions = .default, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable { + /// try await self.(...) + /// } + /// ``` + static func clientMethod( + accessLevel: AccessModifier, + name: String, + input: String, + output: String, + serviceEnum: String, + methodEnum: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + let underlyingMethod: String + switch (streamingInput, streamingOutput) { + case (false, false): + underlyingMethod = "unary" + case (true, false): + underlyingMethod = "clientStreaming" + case (false, true): + underlyingMethod = "serverStreaming" + case (true, true): + underlyingMethod = "bidirectionalStreaming" + } + + return FunctionDescription( + accessModifier: accessLevel, + kind: .function(name: name), + generics: [.member("Result")], + parameters: [ + ParameterDescription( + label: "request", + type: .clientRequest(forType: input, streaming: streamingInput) + ), + ParameterDescription( + label: "serializer", + // Be explicit: 'type' is optional and '.some' resolves to Optional.some by default. + type: ExistingTypeDescription.some(.serializer(forType: input)) + ), + ParameterDescription( + label: "deserializer", + // Be explicit: 'type' is optional and '.some' resolves to Optional.some by default. + type: ExistingTypeDescription.some(.deserializer(forType: output)) + ), + ParameterDescription( + label: "options", + type: .callOptions, + defaultValue: .dot("defaults") + ), + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .clientResponse(forType: output, streaming: streamingOutput) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + sendable: true, + escaping: true + ) + ), + defaultValue: streamingOutput + ? nil + : .closureInvocation(.defaultClientUnaryResponseHandler) + ), + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + whereClause: WhereClause(requirements: [.conformance("Result", "Sendable")]), + body: [ + .try( + .await( + .functionCall( + calledExpression: .identifierPattern("self").dot("client").dot(underlyingMethod), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request") + ), + FunctionArgumentDescription( + label: "descriptor", + expression: .identifierPattern(serviceEnum) + .dot("Method") + .dot(methodEnum) + .dot("descriptor") + ), + FunctionArgumentDescription( + label: "serializer", + expression: .identifierPattern("serializer") + ), + FunctionArgumentDescription( + label: "deserializer", + expression: .identifierPattern("deserializer") + ), + FunctionArgumentDescription( + label: "options", + expression: .identifierPattern("options") + ), + FunctionArgumentDescription( + label: "onResponse", + expression: .identifierPattern("handleResponse") + ), + ] + ) + ) + ) + ] + ) + } +} + +extension StructDescription { + /// ``` + /// struct : { + /// private let client: GRPCCore.GRPCClient + /// + /// init(wrapping client: GRPCCore.GRPCClient) { + /// self.client = client + /// } + /// + /// // ... + /// } + /// ``` + static func client( + accessLevel: AccessModifier, + name: String, + serviceEnum: String, + clientProtocol: String, + methods: [MethodDescriptor] + ) -> Self { + StructDescription( + accessModifier: accessLevel, + name: name, + conformances: [clientProtocol], + members: [ + .variable(accessModifier: .private, kind: .let, left: "client", type: .grpcClient), + .function( + accessModifier: accessLevel, + kind: .initializer, + parameters: [ + ParameterDescription(label: "wrapping", name: "client", type: .grpcClient) + ], + whereClause: nil, + body: [ + .expression( + .assignment( + left: .identifierPattern("self").dot("client"), + right: .identifierPattern("client") + ) + ) + ] + ), + ] + + methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + .clientMethod( + accessLevel: accessLevel, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + serviceEnum: serviceEnum, + methodEnum: method.name.generatedUpperCase, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + ) + } + ) + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift new file mode 100644 index 000000000..a4abc0ee3 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -0,0 +1,544 @@ +/* + * 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 Testing + +@testable import GRPCCodeGen + +extension StructuedSwiftTests { + @Suite("Client") + struct Client { + @Test( + "func (request:serializer:deserializer:options:onResponse:)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func clientMethodSignature(access: AccessModifier, kind: RPCKind) { + let decl: FunctionSignatureDescription = .clientMethod( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput, + includeDefaults: false, + includeSerializers: true + ) + + let requestType = kind.streamsInput ? "StreamingClientRequest" : "ClientRequest" + let responseType = kind.streamsOutput ? "StreamingClientResponse" : "ClientResponse" + + let expected = """ + \(access) func foo( + request: GRPCCore.\(requestType), + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.\(responseType)) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + + #expect(render(.function(signature: decl)) == expected) + } + + @Test( + "func (request:serializer:deserializer:options:onResponse:) (with defaults)", + arguments: AccessModifier.allCases, + [true, false] + ) + func clientMethodSignatureWithDefaults(access: AccessModifier, streamsOutput: Bool) { + let decl: FunctionSignatureDescription = .clientMethod( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: false, + streamingOutput: streamsOutput, + includeDefaults: true, + includeSerializers: false + ) + + let expected: String + if streamsOutput { + expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + } else { + expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable + """ + } + + #expect(render(.function(signature: decl)) == expected) + } + + @Test("protocol Foo_ClientProtocol: Sendable { ... }", arguments: AccessModifier.allCases) + func clientProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .clientProtocol( + accessLevel: access, + name: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ] + ) + + let expected = """ + \(access) protocol Foo_ClientProtocol: Sendable { + /// Some docs + func bar( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + """ + + #expect(render(.protocol(decl)) == expected) + } + + @Test("func foo(...) { try await self.foo(...) }", arguments: AccessModifier.allCases) + func clientMethodFunctionWithDefaults(access: AccessModifier) { + let decl: FunctionDescription = .clientMethodWithDefaults( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: false, + streamingOutput: false, + serializer: .identifierPattern("Serialize()"), + deserializer: .identifierPattern("Deserialize()") + ) + + let expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.foo( + request: request, + serializer: Serialize(), + deserializer: Deserialize(), + options: options, + onResponse: handleResponse + ) + } + """ + + #expect(render(.function(decl)) == expected) + } + + @Test( + "extension Foo_ClientProtocol { ... } (methods with defaults)", + arguments: AccessModifier.allCases + ) + func extensionWithDefaultClientMethods(access: AccessModifier) { + let decl: ExtensionDescription = .clientMethodSignatureWithDefaults( + accessLevel: access, + name: "Foo_ClientProtocol", + methods: [ + MethodDescriptor( + documentation: "", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ], + serializer: { "Serialize<\($0)>()" }, + deserializer: { "Deserialize<\($0)>()" } + ) + + let expected = """ + extension Foo_ClientProtocol { + \(access) func bar( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.bar( + request: request, + serializer: Serialize(), + deserializer: Deserialize(), + options: options, + onResponse: handleResponse + ) + } + } + """ + + #expect(render(.extension(decl)) == expected) + } + + @Test( + "func foo(_:metadata:options:onResponse:) -> Result (exploded signature)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func explodedClientMethodSignature(access: AccessModifier, kind: RPCKind) { + let decl: FunctionSignatureDescription = .clientMethodExploded( + accessLevel: access, + name: "foo", + input: "Input", + output: "Output", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput + ) + + let expected: String + switch kind { + case .unary: + expected = """ + \(access) func foo( + _ message: Input, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable + """ + case .clientStreaming: + expected = """ + \(access) func foo( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable + """ + case .serverStreaming: + expected = """ + \(access) func foo( + _ message: Input, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + case .bidirectionalStreaming: + expected = """ + \(access) func foo( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + } + + #expect(render(.function(signature: decl)) == expected) + } + + @Test( + "func foo(_:metadata:options:onResponse:) -> Result (exploded body)", + arguments: [true, false] + ) + func explodedClientMethodBody(streamingInput: Bool) { + let blocks: [CodeBlock] = .clientMethodExploded( + name: "foo", + input: "Input", + streamingInput: streamingInput + ) + + let expected: String + if streamingInput { + expected = """ + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.foo( + request: request, + options: options, + onResponse: handleResponse + ) + """ + + } else { + expected = """ + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.foo( + request: request, + options: options, + onResponse: handleResponse + ) + """ + } + + #expect(render(blocks) == expected) + } + + @Test("extension Foo_ClientProtocol { ... } (exploded)", arguments: AccessModifier.allCases) + func explodedClientMethodExtension(access: AccessModifier) { + let decl: ExtensionDescription = .explodedClientMethods( + accessLevel: access, + on: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ) + ] + ) + + let expected = """ + extension Foo_ClientProtocol { + /// Some docs + \(access) func bar( + _ message: Input, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.bar( + request: request, + options: options, + onResponse: handleResponse + ) + } + } + """ + + #expect(render(.extension(decl)) == expected) + } + + @Test( + "func foo(request:serializer:deserializer:options:onResponse:) (client method impl.)", + arguments: AccessModifier.allCases + ) + func clientMethodImplementation(access: AccessModifier) { + let decl: FunctionDescription = .clientMethod( + accessLevel: access, + name: "foo", + input: "Input", + output: "Output", + serviceEnum: "BarService", + methodEnum: "Foo", + streamingInput: false, + streamingOutput: false + ) + + let expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: BarService.Method.Foo.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + """ + + #expect(render(.function(decl)) == expected) + } + + @Test("struct FooClient: Foo_ClientProtocol { ... }", arguments: AccessModifier.allCases) + func client(access: AccessModifier) { + let decl: StructDescription = .client( + accessLevel: access, + name: "FooClient", + serviceEnum: "BarService", + clientProtocol: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Unary docs", + name: .init( + base: "Unary", + generatedUpperCase: "Unary", + generatedLowerCase: "unary" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// ClientStreaming docs", + name: .init( + base: "ClientStreaming", + generatedUpperCase: "ClientStreaming", + generatedLowerCase: "clientStreaming" + ), + isInputStreaming: true, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// ServerStreaming docs", + name: .init( + base: "ServerStreaming", + generatedUpperCase: "ServerStreaming", + generatedLowerCase: "serverStreaming" + ), + isInputStreaming: false, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// BidiStreaming docs", + name: .init( + base: "BidiStreaming", + generatedUpperCase: "BidiStreaming", + generatedLowerCase: "bidiStreaming" + ), + isInputStreaming: true, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ), + ] + ) + + let expected = """ + \(access) struct FooClient: Foo_ClientProtocol { + private let client: GRPCCore.GRPCClient + + \(access) init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Unary docs + \(access) func unary( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: BarService.Method.Unary.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// ClientStreaming docs + \(access) func clientStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: BarService.Method.ClientStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// ServerStreaming docs + \(access) func serverStreaming( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: BarService.Method.ServerStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// BidiStreaming docs + \(access) func bidiStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: BarService.Method.BidiStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } + """ + + #expect(render(.struct(decl)) == expected) + } + } +} From 2cdc570904718c85fad12a28de85a4847b8aeae7 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 25 Nov 2024 12:07:59 +0000 Subject: [PATCH 507/580] Use new structured swift representations (#2128) Motivation: The new structured Swift representations are more thoroughly tested than the existing translators, so reimplement the translators in terms of the structured Swift. Modifications: - Reimplement translators Result: Most tests remain exactly the same, however the generated client code changes slightly to make the naming of generic placeholders and closure parameters clearer. --- .../GRPCCodeGen/CodeGenerationRequest.swift | 14 + .../Internal/StructuredSwift+Server.swift | 17 +- .../StructuredSwift+ServiceMetadata.swift | 40 ++ .../StructuredSwiftRepresentation.swift | 2 +- .../Translator/ClientCodeTranslator.swift | 653 ++---------------- .../IDLToStructuredSwiftTranslator.swift | 32 +- .../Translator/MetadataTranslator.swift | 49 ++ .../Translator/ServerCodeTranslator.swift | 466 +++---------- .../Translator/TypealiasTranslator.swift | 371 ---------- ...lientCodeTranslatorSnippetBasedTests.swift | 223 +++--- ...erverCodeTranslatorSnippetBasedTests.swift | 11 +- ...TypealiasTranslatorSnippetBasedTests.swift | 11 +- 12 files changed, 401 insertions(+), 1488 deletions(-) create mode 100644 Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift delete mode 100644 Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift diff --git a/Sources/GRPCCodeGen/CodeGenerationRequest.swift b/Sources/GRPCCodeGen/CodeGenerationRequest.swift index 432469d74..5106b3c58 100644 --- a/Sources/GRPCCodeGen/CodeGenerationRequest.swift +++ b/Sources/GRPCCodeGen/CodeGenerationRequest.swift @@ -335,3 +335,17 @@ extension Name { return self.base.replacingOccurrences(of: ".", with: "_") } } + +extension ServiceDescriptor { + var namespacedServicePropertyName: String { + let prefix: String + + if self.namespace.normalizedBase.isEmpty { + prefix = "" + } else { + prefix = self.namespace.normalizedBase + "_" + } + + return prefix + self.name.normalizedBase + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index c46986fa3..cc446825d 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -97,13 +97,16 @@ extension ExtensionDescription { return ExtensionDescription( onType: extensionName, declarations: [ - .function( - .registerMethods( - accessLevel: accessLevel, - serviceNamespace: serviceNamespace, - methods: methods, - serializer: serializer, - deserializer: deserializer + .guarded( + .grpc, + .function( + .registerMethods( + accessLevel: accessLevel, + serviceNamespace: serviceNamespace, + methods: methods, + serializer: serializer, + deserializer: deserializer + ) ) ) ] diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index a71cafd83..62e1933d8 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -302,3 +302,43 @@ extension EnumDescription { return description } } + +extension [CodeBlock] { + /// ``` + /// enum { + /// ... + /// } + /// + /// extension GRPCCore.ServiceDescriptor { + /// ... + /// } + /// ``` + package static func serviceMetadata( + accessModifier: AccessModifier? = nil, + service: ServiceDescriptor, + client: Bool, + server: Bool + ) -> Self { + var blocks: [CodeBlock] = [] + + let serviceNamespace: EnumDescription = .serviceNamespace( + accessModifier: accessModifier, + name: service.namespacedGeneratedName, + serviceDescriptorProperty: service.namespacedServicePropertyName, + client: client, + server: server, + methods: service.methods + ) + blocks.append(CodeBlock(item: .declaration(.enum(serviceNamespace)))) + + let descriptorExtension: ExtensionDescription = .serviceDescriptor( + accessModifier: accessModifier, + propertyName: service.namespacedServicePropertyName, + literalNamespace: service.namespace.base, + literalService: service.name.base + ) + blocks.append(CodeBlock(item: .declaration(.extension(descriptorExtension)))) + + return blocks + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index a9f9c33d7..16cfdbfe8 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -100,7 +100,7 @@ struct ImportDescription: Equatable, Codable, Sendable { /// A description of an access modifier. /// /// For example: `public`. -internal enum AccessModifier: String, Sendable, Equatable, Codable, CaseIterable { +package enum AccessModifier: String, Sendable, Equatable, Codable, CaseIterable { /// A declaration accessible outside of the module. case `public` diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index 3a628cf47..ec8850a17 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -75,624 +75,83 @@ /// } /// } ///``` -struct ClientCodeTranslator: SpecializedTranslator { - var accessLevel: SourceGenerator.Config.AccessLevel - - init(accessLevel: SourceGenerator.Config.AccessLevel) { - self.accessLevel = accessLevel - } - - func translate(from request: CodeGenerationRequest) throws -> [CodeBlock] { - var blocks = [CodeBlock]() - - for service in request.services { - let `protocol` = self.makeClientProtocol(for: service, in: request) - blocks.append(.declaration(.commentable(.preFormatted(service.documentation), `protocol`))) - - let defaultImplementation = self.makeDefaultImplementation(for: service, in: request) - blocks.append(.declaration(defaultImplementation)) - - let sugaredAPI = self.makeSugaredAPI(forService: service, request: request) - blocks.append(.declaration(sugaredAPI)) - - let clientStruct = self.makeClientStruct(for: service, in: request) - blocks.append(.declaration(.commentable(.preFormatted(service.documentation), clientStruct))) - } - - return blocks - } -} - -extension ClientCodeTranslator { - private func makeClientProtocol( - for service: ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest - ) -> Declaration { - let methods = service.methods.map { - self.makeClientProtocolMethod( - for: $0, - in: service, - from: codeGenerationRequest, - includeBody: false, - includeDefaultCallOptions: false - ) - } - - let clientProtocol = Declaration.protocol( - ProtocolDescription( - accessModifier: self.accessModifier, - name: "\(service.namespacedGeneratedName)_ClientProtocol", - conformances: ["Sendable"], - members: methods - ) - ) - return .guarded(self.availabilityGuard, clientProtocol) - } - - private func makeDefaultImplementation( - for service: ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest - ) -> Declaration { - let methods = service.methods.map { - self.makeClientProtocolMethod( - for: $0, - in: service, - from: codeGenerationRequest, - includeBody: true, - accessModifier: self.accessModifier, - includeDefaultCallOptions: true - ) - } - let clientProtocolExtension = Declaration.extension( - ExtensionDescription( - onType: "\(service.namespacedGeneratedName).ClientProtocol", - declarations: methods - ) - ) - return .guarded( - self.availabilityGuard, - clientProtocolExtension - ) - } - - private func makeSugaredAPI( - forService service: ServiceDescriptor, - request: CodeGenerationRequest - ) -> Declaration { - let sugaredAPIExtension = Declaration.extension( - ExtensionDescription( - onType: "\(service.namespacedGeneratedName).ClientProtocol", - declarations: service.methods.map { method in - self.makeSugaredMethodDeclaration( - method: method, - accessModifier: self.accessModifier - ) - } - ) - ) - - return .guarded(self.availabilityGuard, sugaredAPIExtension) - } - - private func makeSugaredMethodDeclaration( - method: MethodDescriptor, - accessModifier: AccessModifier? - ) -> Declaration { - let signature = FunctionSignatureDescription( - accessModifier: accessModifier, - kind: .function(name: method.name.generatedLowerCase), - generics: [.member("Result")], - parameters: self.makeParametersForSugaredMethodDeclaration(method: method), - keywords: [.async, .throws], - returnType: .identifierPattern("Result"), - whereClause: WhereClause( - requirements: [ - .conformance("Result", "Sendable") - ] - ) - ) - - let functionDescription = FunctionDescription( - signature: signature, - body: self.makeFunctionBodyForSugaredMethodDeclaration(method: method) - ) - - if method.documentation.isEmpty { - return .function(functionDescription) - } else { - return .commentable(.preFormatted(method.documentation), .function(functionDescription)) - } - } - - private func makeParametersForSugaredMethodDeclaration( - method: MethodDescriptor - ) -> [ParameterDescription] { - var parameters = [ParameterDescription]() - - // Unary inputs have a 'message' parameter - if !method.isInputStreaming { - parameters.append( - ParameterDescription( - label: "_", - name: "message", - type: .member([method.inputType]) - ) - ) - } - - parameters.append( - ParameterDescription( - label: "metadata", - type: .member(["GRPCCore", "Metadata"]), - defaultValue: .literal(.dictionary([])) - ) - ) - - parameters.append( - ParameterDescription( - label: "options", - type: .member(["GRPCCore", "CallOptions"]), - defaultValue: .memberAccess(.dot("defaults")) - ) - ) - - // Streaming inputs have a writer callback - if method.isInputStreaming { - parameters.append( - ParameterDescription( - label: "requestProducer", - type: .closure( - ClosureSignatureDescription( - parameters: [ - ParameterDescription( - type: .generic( - wrapper: .member(["GRPCCore", "RPCWriter"]), - wrapped: .member(method.inputType) - ) - ) - ], - keywords: [.async, .throws], - returnType: .identifierPattern("Void"), - sendable: true, - escaping: true - ) - ) - ) - ) - } - - // All methods have a response handler. - var responseHandler = ParameterDescription(label: "onResponse", name: "handleResponse") - let responseKind = method.isOutputStreaming ? "Streaming" : "" - responseHandler.type = .closure( - ClosureSignatureDescription( - parameters: [ - ParameterDescription( - type: .generic( - wrapper: .member(["GRPCCore", "\(responseKind)ClientResponse"]), - wrapped: .member(method.outputType) - ) - ) - ], - keywords: [.async, .throws], - returnType: .identifierPattern("Result"), - sendable: true, - escaping: true - ) - ) - - if !method.isOutputStreaming { - responseHandler.defaultValue = .closureInvocation( - ClosureInvocationDescription( - body: [.expression(.try(.identifierPattern("$0").dot("message")))] - ) - ) - } - - parameters.append(responseHandler) - - return parameters - } - - private func makeFunctionBodyForSugaredMethodDeclaration( - method: MethodDescriptor +struct ClientCodeTranslator { + init() {} + + func translate( + accessModifier: AccessModifier, + services: [ServiceDescriptor], + serializer: (String) -> String, + deserializer: (String) -> String ) -> [CodeBlock] { - // Produces the following: - // - // let request = GRPCCore.ClientRequest(message: message, metadata: metadata) - // return try await method(request: request, options: options, responseHandler: responseHandler) - // - // or: - // - // let request = GRPCCore.StreamingClientRequest(metadata: metadata, producer: writer) - // return try await method(request: request, options: options, responseHandler: responseHandler) - - // First, make the init for the ClientRequest - let requestType = method.isInputStreaming ? "Streaming" : "" - var requestInit = FunctionCallDescription( - calledExpression: .identifier( - .type( - .generic( - wrapper: .member(["GRPCCore", "\(requestType)ClientRequest"]), - wrapped: .member(method.inputType) - ) - ) - ) - ) - - if method.isInputStreaming { - requestInit.arguments.append( - FunctionArgumentDescription( - label: "metadata", - expression: .identifierPattern("metadata") - ) - ) - requestInit.arguments.append( - FunctionArgumentDescription( - label: "producer", - expression: .identifierPattern("requestProducer") - ) - ) - } else { - requestInit.arguments.append( - FunctionArgumentDescription( - label: "message", - expression: .identifierPattern("message") - ) - ) - requestInit.arguments.append( - FunctionArgumentDescription( - label: "metadata", - expression: .identifierPattern("metadata") - ) - ) - } - - // Now declare the request: - // - // let request = - let request = VariableDescription( - kind: .let, - left: .identifier(.pattern("request")), - right: .functionCall(requestInit) - ) - - var blocks = [CodeBlock]() - blocks.append(.declaration(.variable(request))) - - // Finally, call the underlying method. - let methodCall = FunctionCallDescription( - calledExpression: .identifierPattern("self").dot(method.name.generatedLowerCase), - arguments: [ - FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), - FunctionArgumentDescription(label: "options", expression: .identifierPattern("options")), - FunctionArgumentDescription(expression: .identifierPattern("handleResponse")), - ] - ) - - blocks.append(.expression(.return(.try(.await(.functionCall(methodCall)))))) - return blocks - } - - private func makeClientProtocolMethod( - for method: MethodDescriptor, - in service: ServiceDescriptor, - from codeGenerationRequest: CodeGenerationRequest, - includeBody: Bool, - accessModifier: AccessModifier? = nil, - includeDefaultCallOptions: Bool - ) -> Declaration { - let isProtocolExtension = includeBody - let methodParameters = self.makeParameters( - for: method, - in: service, - from: codeGenerationRequest, - // The serializer/deserializer for the protocol extension method will be auto-generated. - includeSerializationParameters: !isProtocolExtension, - includeDefaultCallOptions: includeDefaultCallOptions, - includeDefaultResponseHandler: isProtocolExtension && !method.isOutputStreaming - ) - let functionSignature = FunctionSignatureDescription( - accessModifier: accessModifier, - kind: .function( - name: method.name.generatedLowerCase, - isStatic: false - ), - generics: [.member("R")], - parameters: methodParameters, - keywords: [.async, .throws], - returnType: .identifierType(.member("R")), - whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]) - ) - - if includeBody { - let body = self.makeClientProtocolMethodCall( - for: method, - in: service, - from: codeGenerationRequest - ) - return .function(signature: functionSignature, body: body) - } else { - return .commentable( - .preFormatted(method.documentation), - .function(signature: functionSignature) + services.flatMap { service in + self.translate( + accessModifier: accessModifier, + service: service, + serializer: serializer, + deserializer: deserializer ) } } - private func makeClientProtocolMethodCall( - for method: MethodDescriptor, - in service: ServiceDescriptor, - from codeGenerationRequest: CodeGenerationRequest + private func translate( + accessModifier: AccessModifier, + service: ServiceDescriptor, + serializer: (String) -> String, + deserializer: (String) -> String ) -> [CodeBlock] { - let functionCall = Expression.functionCall( - calledExpression: .memberAccess( - MemberAccessDescription( - left: .identifierPattern("self"), - right: method.name.generatedLowerCase - ) - ), - arguments: [ - FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), - FunctionArgumentDescription( - label: "serializer", - expression: .identifierPattern(codeGenerationRequest.lookupSerializer(method.inputType)) - ), - FunctionArgumentDescription( - label: "deserializer", - expression: .identifierPattern( - codeGenerationRequest.lookupDeserializer(method.outputType) - ) - ), - FunctionArgumentDescription(label: "options", expression: .identifierPattern("options")), - FunctionArgumentDescription(expression: .identifierPattern("body")), - ] - ) - let awaitFunctionCall = Expression.unaryKeyword(kind: .await, expression: functionCall) - let tryAwaitFunctionCall = Expression.unaryKeyword(kind: .try, expression: awaitFunctionCall) - - return [CodeBlock(item: .expression(tryAwaitFunctionCall))] - } + var blocks = [CodeBlock]() - private func makeParameters( - for method: MethodDescriptor, - in service: ServiceDescriptor, - from codeGenerationRequest: CodeGenerationRequest, - includeSerializationParameters: Bool, - includeDefaultCallOptions: Bool, - includeDefaultResponseHandler: Bool - ) -> [ParameterDescription] { - var parameters = [ParameterDescription]() + let protocolName = "\(service.namespacedGeneratedName)_ClientProtocol" + let protocolTypealias = "\(service.namespacedGeneratedName).ClientProtocol" + let structName = "\(service.namespacedGeneratedName)_Client" - parameters.append(self.clientRequestParameter(for: method, in: service)) - if includeSerializationParameters { - parameters.append(self.serializerParameter(for: method, in: service)) - parameters.append(self.deserializerParameter(for: method, in: service)) - } - parameters.append( - ParameterDescription( - label: "options", - type: .member(["GRPCCore", "CallOptions"]), - defaultValue: includeDefaultCallOptions - ? .memberAccess(MemberAccessDescription(right: "defaults")) : nil - ) - ) - parameters.append( - self.bodyParameter( - for: method, - in: service, - includeDefaultResponseHandler: includeDefaultResponseHandler - ) - ) - return parameters - } - private func clientRequestParameter( - for method: MethodDescriptor, - in service: ServiceDescriptor - ) -> ParameterDescription { - let requestType = method.isInputStreaming ? "Streaming" : "" - let clientRequestType = ExistingTypeDescription.member([ - "GRPCCore", - "\(requestType)ClientRequest", - ]) - return ParameterDescription( - label: "request", - type: .generic( - wrapper: clientRequestType, - wrapped: .member(method.inputType) - ) + let clientProtocol: ProtocolDescription = .clientProtocol( + accessLevel: accessModifier, + name: protocolName, + methods: service.methods ) - } - - private func serializerParameter( - for method: MethodDescriptor, - in service: ServiceDescriptor - ) -> ParameterDescription { - return ParameterDescription( - label: "serializer", - type: ExistingTypeDescription.some( - .generic( - wrapper: .member(["GRPCCore", "MessageSerializer"]), - wrapped: .member(method.inputType) - ) + blocks.append( + CodeBlock( + comment: .preFormatted(service.documentation), + item: .declaration(.guarded(.grpc, .protocol(clientProtocol))) ) ) - } - private func deserializerParameter( - for method: MethodDescriptor, - in service: ServiceDescriptor - ) -> ParameterDescription { - return ParameterDescription( - label: "deserializer", - type: ExistingTypeDescription.some( - .generic( - wrapper: .member(["GRPCCore", "MessageDeserializer"]), - wrapped: .member(method.outputType) - ) - ) + let extensionWithDefaults: ExtensionDescription = .clientMethodSignatureWithDefaults( + accessLevel: accessModifier, + name: protocolTypealias, + methods: service.methods, + serializer: serializer, + deserializer: deserializer ) - } - - private func bodyParameter( - for method: MethodDescriptor, - in service: ServiceDescriptor, - includeDefaultResponseHandler: Bool - ) -> ParameterDescription { - let clientStreaming = method.isOutputStreaming ? "Streaming" : "" - let closureParameterType = ExistingTypeDescription.generic( - wrapper: .member(["GRPCCore", "\(clientStreaming)ClientResponse"]), - wrapped: .member(method.outputType) + blocks.append( + CodeBlock(item: .declaration(.guarded(.grpc, .extension(extensionWithDefaults)))) ) - let bodyClosure = ClosureSignatureDescription( - parameters: [.init(type: closureParameterType)], - keywords: [.async, .throws], - returnType: .identifierType(.member("R")), - sendable: true, - escaping: true + let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods( + accessLevel: accessModifier, + on: protocolTypealias, + methods: service.methods ) - - var defaultResponseHandler: Expression? = nil - - if includeDefaultResponseHandler { - defaultResponseHandler = .closureInvocation( - ClosureInvocationDescription( - body: [.expression(.try(.identifierPattern("$0").dot("message")))] - ) - ) - } - - return ParameterDescription( - name: "body", - type: .closure(bodyClosure), - defaultValue: defaultResponseHandler + blocks.append( + CodeBlock(item: .declaration(.guarded(.grpc, .extension(extensionWithExplodedAPI)))) ) - } - private func makeClientStruct( - for service: ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest - ) -> Declaration { - let clientProperty = Declaration.variable( - accessModifier: .private, - kind: .let, - left: "client", - type: .member(["GRPCCore", "GRPCClient"]) + let clientStruct: StructDescription = .client( + accessLevel: accessModifier, + name: structName, + serviceEnum: service.namespacedGeneratedName, + clientProtocol: protocolTypealias, + methods: service.methods ) - let initializer = self.makeClientVariable() - let methods = service.methods.map { - Declaration.commentable( - .preFormatted($0.documentation), - self.makeClientMethod(for: $0, in: service, from: codeGenerationRequest) + blocks.append( + CodeBlock( + comment: .preFormatted(service.documentation), + item: .declaration(.guarded(.grpc, .struct(clientStruct))) ) - } - - return .guarded( - self.availabilityGuard, - .struct( - StructDescription( - accessModifier: self.accessModifier, - name: "\(service.namespacedGeneratedName)_Client", - conformances: ["\(service.namespacedGeneratedName).ClientProtocol"], - members: [clientProperty, initializer] + methods - ) - ) - ) - } - - private func makeClientVariable() -> Declaration { - let initializerBody = Expression.assignment( - left: .memberAccess( - MemberAccessDescription(left: .identifierPattern("self"), right: "client") - ), - right: .identifierPattern("client") - ) - return .function( - signature: .init( - accessModifier: self.accessModifier, - kind: .initializer, - parameters: [ - .init(label: "wrapping", name: "client", type: .member(["GRPCCore", "GRPCClient"])) - ] - ), - body: [CodeBlock(item: .expression(initializerBody))] - ) - } - - private func clientMethod( - isInputStreaming: Bool, - isOutputStreaming: Bool - ) -> String { - switch (isInputStreaming, isOutputStreaming) { - case (true, true): - return "bidirectionalStreaming" - case (true, false): - return "clientStreaming" - case (false, true): - return "serverStreaming" - case (false, false): - return "unary" - } - } - - private func makeClientMethod( - for method: MethodDescriptor, - in service: ServiceDescriptor, - from codeGenerationRequest: CodeGenerationRequest - ) -> Declaration { - let parameters = self.makeParameters( - for: method, - in: service, - from: codeGenerationRequest, - includeSerializationParameters: true, - includeDefaultCallOptions: true, - includeDefaultResponseHandler: !method.isOutputStreaming - ) - let grpcMethodName = self.clientMethod( - isInputStreaming: method.isInputStreaming, - isOutputStreaming: method.isOutputStreaming - ) - let functionCall = Expression.functionCall( - calledExpression: .memberAccess( - MemberAccessDescription(left: .identifierPattern("self.client"), right: "\(grpcMethodName)") - ), - arguments: [ - .init(label: "request", expression: .identifierPattern("request")), - .init( - label: "descriptor", - expression: .identifierPattern( - "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" - ) - ), - .init(label: "serializer", expression: .identifierPattern("serializer")), - .init(label: "deserializer", expression: .identifierPattern("deserializer")), - .init(label: "options", expression: .identifierPattern("options")), - .init(label: "handler", expression: .identifierPattern("body")), - ] - ) - let body = UnaryKeywordDescription( - kind: .try, - expression: .unaryKeyword(kind: .await, expression: functionCall) ) - return .function( - accessModifier: self.accessModifier, - kind: .function( - name: "\(method.name.generatedLowerCase)", - isStatic: false - ), - generics: [.member("R")], - parameters: parameters, - keywords: [.async, .throws], - returnType: .identifierType(.member("R")), - whereClause: WhereClause(requirements: [.conformance("R", "Sendable")]), - body: [.expression(.unaryKeyword(body))] - ) - } - - fileprivate enum InputOutputType { - case input - case output + return blocks } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index fb5474859..0fcbc5388 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -26,24 +26,36 @@ struct IDLToStructuredSwiftTranslator: Translator { server: Bool ) throws -> StructuredSwiftRepresentation { try self.validateInput(codeGenerationRequest) + let accessModifier = AccessModifier(accessLevel) - var codeBlocks = [CodeBlock]() - - let typealiasTranslator = TypealiasTranslator( + let metadataTranslator = MetadataTranslator() + var codeBlocks = metadataTranslator.translate( + accessModifier: accessModifier, + services: codeGenerationRequest.services, client: client, - server: server, - accessLevel: accessLevel + server: server ) - codeBlocks.append(contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest)) if server { - let serverCodeTranslator = ServerCodeTranslator(accessLevel: accessLevel) - codeBlocks.append(contentsOf: try serverCodeTranslator.translate(from: codeGenerationRequest)) + let translator = ServerCodeTranslator() + let blocks = translator.translate( + accessModifier: accessModifier, + services: codeGenerationRequest.services, + serializer: codeGenerationRequest.lookupSerializer, + deserializer: codeGenerationRequest.lookupDeserializer + ) + codeBlocks.append(contentsOf: blocks) } if client { - let clientCodeTranslator = ClientCodeTranslator(accessLevel: accessLevel) - codeBlocks.append(contentsOf: try clientCodeTranslator.translate(from: codeGenerationRequest)) + let translator = ClientCodeTranslator() + let blocks = translator.translate( + accessModifier: accessModifier, + services: codeGenerationRequest.services, + serializer: codeGenerationRequest.lookupSerializer, + deserializer: codeGenerationRequest.lookupDeserializer + ) + codeBlocks.append(contentsOf: blocks) } let fileDescription = FileDescription( diff --git a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift new file mode 100644 index 000000000..23e68c2cc --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift @@ -0,0 +1,49 @@ +/* + * 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. + */ + +struct MetadataTranslator { + init() {} + + func translate( + accessModifier: AccessModifier, + services: [ServiceDescriptor], + client: Bool, + server: Bool + ) -> [CodeBlock] { + return services.flatMap { service in + self.translate( + accessModifier: accessModifier, + service: service, + client: client, + server: server + ) + } + } + + private func translate( + accessModifier: AccessModifier, + service: ServiceDescriptor, + client: Bool, + server: Bool + ) -> [CodeBlock] { + .serviceMetadata( + accessModifier: accessModifier, + service: service, + client: client, + server: server + ) + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index a08431ad9..e769e6b61 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -57,422 +57,114 @@ /// } /// } ///``` -struct ServerCodeTranslator: SpecializedTranslator { - var accessLevel: SourceGenerator.Config.AccessLevel - - init(accessLevel: SourceGenerator.Config.AccessLevel) { - self.accessLevel = accessLevel - } - - func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { - var codeBlocks = [CodeBlock]() - for service in codeGenerationRequest.services { - // Create the streaming protocol that declares the service methods as bidirectional streaming. - let streamingProtocol = CodeBlockItem.declaration(self.makeStreamingProtocol(for: service)) - codeBlocks.append(CodeBlock(item: streamingProtocol)) - - // Create extension for implementing the 'registerRPCs' function which is a 'RegistrableRPCService' requirement. - let conformanceToRPCServiceExtension = CodeBlockItem.declaration( - self.makeConformanceToRPCServiceExtension(for: service, in: codeGenerationRequest) - ) - codeBlocks.append( - CodeBlock( - comment: .doc("Conformance to `GRPCCore.RegistrableRPCService`."), - item: conformanceToRPCServiceExtension - ) - ) - - // Create the service protocol that declares the service methods as they are described in the Source IDL (unary, - // client/server streaming or bidirectional streaming). - let serviceProtocol = CodeBlockItem.declaration(self.makeServiceProtocol(for: service)) - codeBlocks.append(CodeBlock(item: serviceProtocol)) - - // Create extension for partial conformance to the streaming protocol. - let extensionServiceProtocol = CodeBlockItem.declaration( - self.makeExtensionServiceProtocol(for: service) - ) - codeBlocks.append( - CodeBlock( - comment: .doc( - "Partial conformance to `\(self.protocolName(service: service, streaming: true))`." - ), - item: extensionServiceProtocol - ) - ) - } - - return codeBlocks - } -} - -extension ServerCodeTranslator { - private func makeStreamingProtocol( - for service: ServiceDescriptor - ) -> Declaration { - let methods = service.methods.compactMap { - Declaration.commentable( - .preFormatted($0.documentation), - .function( - FunctionDescription( - signature: self.makeStreamingMethodSignature(for: $0, in: service) - ) - ) - ) - } - - let streamingProtocol = Declaration.protocol( - .init( - accessModifier: self.accessModifier, - name: self.protocolName(service: service, streaming: true), - conformances: ["GRPCCore.RegistrableRPCService"], - members: methods - ) - ) - - return .commentable( - .preFormatted(service.documentation), - .guarded(self.availabilityGuard, streamingProtocol) - ) - } - - private func makeStreamingMethodSignature( - for method: MethodDescriptor, - in service: ServiceDescriptor, - accessModifier: AccessModifier? = nil - ) -> FunctionSignatureDescription { - return FunctionSignatureDescription( - accessModifier: accessModifier, - kind: .function(name: method.name.generatedLowerCase), - parameters: [ - .init( - label: "request", - type: .generic( - wrapper: .member(["GRPCCore", "StreamingServerRequest"]), - wrapped: .member(method.inputType) - ) - ), - .init(label: "context", type: .member(["GRPCCore", "ServerContext"])), - ], - keywords: [.async, .throws], - returnType: .identifierType( - .generic( - wrapper: .member(["GRPCCore", "StreamingServerResponse"]), - wrapped: .member(method.outputType) - ) - ) - ) - } - - private func makeConformanceToRPCServiceExtension( - for service: ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest - ) -> Declaration { - let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) - let registerRPCMethod = self.makeRegisterRPCsMethod(for: service, in: codeGenerationRequest) - return .guarded( - self.availabilityGuard, - .extension( - onType: streamingProtocol, - declarations: [registerRPCMethod] - ) - ) - } - - private func makeRegisterRPCsMethod( - for service: ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest - ) -> Declaration { - let registerRPCsSignature = FunctionSignatureDescription( - accessModifier: self.accessModifier, - kind: .function(name: "registerMethods"), - parameters: [ - .init( - label: "with", - name: "router", - type: .member(["GRPCCore", "RPCRouter"]), - `inout`: true - ) - ] - ) - let registerRPCsBody = self.makeRegisterRPCsMethodBody(for: service, in: codeGenerationRequest) - return .guarded( - self.availabilityGuard, - .function(signature: registerRPCsSignature, body: registerRPCsBody) - ) - } - - private func makeRegisterRPCsMethodBody( - for service: ServiceDescriptor, - in codeGenerationRequest: CodeGenerationRequest +struct ServerCodeTranslator { + init() {} + + func translate( + accessModifier: AccessModifier, + services: [ServiceDescriptor], + serializer: (String) -> String, + deserializer: (String) -> String ) -> [CodeBlock] { - let registerHandlerCalls = service.methods.compactMap { - CodeBlock.expression( - Expression.functionCall( - calledExpression: .memberAccess( - MemberAccessDescription(left: .identifierPattern("router"), right: "registerHandler") - ), - arguments: self.makeArgumentsForRegisterHandler( - for: $0, - in: service, - from: codeGenerationRequest - ) - ) + return services.flatMap { service in + self.translate( + accessModifier: accessModifier, + service: service, + serializer: serializer, + deserializer: deserializer ) } - - return registerHandlerCalls } - private func makeArgumentsForRegisterHandler( - for method: MethodDescriptor, - in service: ServiceDescriptor, - from codeGenerationRequest: CodeGenerationRequest - ) -> [FunctionArgumentDescription] { - var arguments = [FunctionArgumentDescription]() - arguments.append( - FunctionArgumentDescription( - label: "forMethod", - expression: .identifierPattern(self.methodDescriptorPath(for: method, service: service)) - ) - ) - - arguments.append( - FunctionArgumentDescription( - label: "deserializer", - expression: .identifierPattern(codeGenerationRequest.lookupDeserializer(method.inputType)) - ) - ) + private func translate( + accessModifier: AccessModifier, + service: ServiceDescriptor, + serializer: (String) -> String, + deserializer: (String) -> String + ) -> [CodeBlock] { + var blocks = [CodeBlock]() - arguments.append( - FunctionArgumentDescription( - label: "serializer", - expression: .identifierPattern(codeGenerationRequest.lookupSerializer(method.outputType)) - ) + let serviceProtocolName = self.protocolName(service: service, streaming: false) + let serviceTypealiasName = self.protocolName( + service: service, + streaming: false, + joinedUsing: "." ) - - let rpcFunctionCall = Expression.functionCall( - calledExpression: .memberAccess( - MemberAccessDescription( - left: .identifierPattern("self"), - right: method.name.generatedLowerCase - ) - ), - arguments: [ - FunctionArgumentDescription(label: "request", expression: .identifierPattern("request")), - FunctionArgumentDescription(label: "context", expression: .identifierPattern("context")), - ] + let streamingServiceProtocolName = self.protocolName(service: service, streaming: true) + let streamingServiceTypealiasName = self.protocolName( + service: service, + streaming: true, + joinedUsing: "." ) - let handlerClosureBody = Expression.unaryKeyword( - kind: .try, - expression: .unaryKeyword(kind: .await, expression: rpcFunctionCall) + // protocol _StreamingServiceProtocol { ... } + let streamingServiceProtocol: ProtocolDescription = .streamingService( + accessLevel: accessModifier, + name: streamingServiceProtocolName, + methods: service.methods ) - - arguments.append( - FunctionArgumentDescription( - label: "handler", - expression: .closureInvocation( - ClosureInvocationDescription( - argumentNames: ["request", "context"], - body: [.expression(handlerClosureBody)] - ) - ) + blocks.append( + CodeBlock( + comment: .preFormatted(service.documentation), + item: .declaration(.guarded(.grpc, .protocol(streamingServiceProtocol))) ) ) - return arguments - } - - private func makeServiceProtocol( - for service: ServiceDescriptor - ) -> Declaration { - let methods = service.methods.compactMap { - self.makeServiceProtocolMethod(for: $0, in: service) - } - let protocolName = self.protocolName(service: service, streaming: false) - let streamingProtocol = self.protocolNameTypealias(service: service, streaming: true) - - return .commentable( - .preFormatted(service.documentation), - .guarded( - self.availabilityGuard, - .protocol( - ProtocolDescription( - accessModifier: self.accessModifier, - name: protocolName, - conformances: [streamingProtocol], - members: methods - ) - ) - ) + // extension _StreamingServiceProtocol> { ... } + let registerExtension: ExtensionDescription = .registrableRPCServiceDefaultImplementation( + accessLevel: accessModifier, + on: streamingServiceTypealiasName, + serviceNamespace: service.namespacedGeneratedName, + methods: service.methods, + serializer: serializer, + deserializer: deserializer ) - } - - private func makeServiceProtocolMethod( - for method: MethodDescriptor, - in service: ServiceDescriptor, - accessModifier: AccessModifier? = nil - ) -> Declaration { - let inputStreaming = method.isInputStreaming ? "Streaming" : "" - let outputStreaming = method.isOutputStreaming ? "Streaming" : "" - - let functionSignature = FunctionSignatureDescription( - accessModifier: accessModifier, - kind: .function(name: method.name.generatedLowerCase), - parameters: [ - .init( - label: "request", - type: - .generic( - wrapper: .member(["GRPCCore", "\(inputStreaming)ServerRequest"]), - wrapped: .member(method.inputType) - ) - ), - .init(label: "context", type: .member(["GRPCCore", "ServerContext"])), - ], - keywords: [.async, .throws], - returnType: .identifierType( - .generic( - wrapper: .member(["GRPCCore", "\(outputStreaming)ServerResponse"]), - wrapped: .member(method.outputType) - ) + blocks.append( + CodeBlock( + comment: .doc("Conformance to `GRPCCore.RegistrableRPCService`."), + item: .declaration(.guarded(.grpc, .extension(registerExtension))) ) ) - return .commentable( - .preFormatted(method.documentation), - .function(FunctionDescription(signature: functionSignature)) + // protocol _ServiceProtocol { ... } + let serviceProtocol: ProtocolDescription = .service( + accessLevel: accessModifier, + name: serviceProtocolName, + streamingProtocol: streamingServiceTypealiasName, + methods: service.methods ) - } - - private func makeExtensionServiceProtocol( - for service: ServiceDescriptor - ) -> Declaration { - let methods = service.methods.compactMap { - self.makeServiceProtocolExtensionMethod(for: $0, in: service) - } - - let protocolName = self.protocolNameTypealias(service: service, streaming: false) - return .guarded( - self.availabilityGuard, - .extension( - onType: protocolName, - declarations: methods + blocks.append( + CodeBlock( + comment: .preFormatted(service.documentation), + item: .declaration(.guarded(.grpc, .protocol(serviceProtocol))) ) ) - } - private func makeServiceProtocolExtensionMethod( - for method: MethodDescriptor, - in service: ServiceDescriptor - ) -> Declaration? { - // The method has the same definition in StreamingServiceProtocol and ServiceProtocol. - if method.isInputStreaming && method.isOutputStreaming { - return nil - } - - let response = CodeBlock(item: .declaration(self.makeResponse(for: method))) - let returnStatement = CodeBlock(item: .expression(self.makeReturnStatement(for: method))) - - return .function( - signature: self.makeStreamingMethodSignature( - for: method, - in: service, - accessModifier: self.accessModifier - ), - body: [response, returnStatement] - ) - } - - private func makeResponse( - for method: MethodDescriptor - ) -> Declaration { - let serverRequest: Expression - if !method.isInputStreaming { - // Transform the streaming request into a unary request. - serverRequest = Expression.functionCall( - calledExpression: .identifierType(.member(["GRPCCore", "ServerRequest"])), - arguments: [ - FunctionArgumentDescription(label: "stream", expression: .identifierPattern("request")) - ] + // extension _ServiceProtocol { ... } + let streamingServiceDefaultImplExtension: ExtensionDescription = + .streamingServiceProtocolDefaultImplementation( + accessModifier: accessModifier, + on: serviceTypealiasName, + methods: service.methods ) - } else { - serverRequest = Expression.identifierPattern("request") - } - // Call to the corresponding ServiceProtocol method. - let serviceProtocolMethod = Expression.functionCall( - calledExpression: .memberAccess( - MemberAccessDescription( - left: .identifierPattern("self"), - right: method.name.generatedLowerCase - ) - ), - arguments: [ - FunctionArgumentDescription(label: "request", expression: serverRequest), - FunctionArgumentDescription(label: "context", expression: .identifier(.pattern("context"))), - ] - ) - - let responseValue = Expression.unaryKeyword( - kind: .try, - expression: .unaryKeyword(kind: .await, expression: serviceProtocolMethod) - ) - - return .variable(kind: .let, left: "response", right: responseValue) - } - - private func makeReturnStatement( - for method: MethodDescriptor - ) -> Expression { - let returnValue: Expression - // Transforming the unary response into a streaming one. - if !method.isOutputStreaming { - returnValue = .functionCall( - calledExpression: .identifier(.type(.member(["GRPCCore", "StreamingServerResponse"]))), - arguments: [ - (FunctionArgumentDescription(label: "single", expression: .identifierPattern("response"))) - ] + blocks.append( + CodeBlock( + comment: .doc("Partial conformance to `\(streamingServiceProtocolName)`."), + item: .declaration(.guarded(.grpc, .extension(streamingServiceDefaultImplExtension))) ) - } else { - returnValue = .identifierPattern("response") - } - - return .unaryKeyword(kind: .return, expression: returnValue) - } - - fileprivate enum InputOutputType { - case input - case output - } - - /// Generates the fully qualified name of a method descriptor. - private func methodDescriptorPath( - for method: MethodDescriptor, - service: ServiceDescriptor - ) -> String { - return - "\(service.namespacedGeneratedName).Method.\(method.name.generatedUpperCase).descriptor" - } + ) - /// Generates the fully qualified name of the type alias for a service protocol. - internal func protocolNameTypealias( - service: ServiceDescriptor, - streaming: Bool - ) -> String { - if streaming { - return "\(service.namespacedGeneratedName).StreamingServiceProtocol" - } - return "\(service.namespacedGeneratedName).ServiceProtocol" + return blocks } - /// Generates the name of a service protocol. - internal func protocolName( + private func protocolName( service: ServiceDescriptor, - streaming: Bool + streaming: Bool, + joinedUsing join: String = "_" ) -> String { if streaming { - return "\(service.namespacedGeneratedName)_StreamingServiceProtocol" + return "\(service.namespacedGeneratedName)\(join)StreamingServiceProtocol" } - return "\(service.namespacedGeneratedName)_ServiceProtocol" + return "\(service.namespacedGeneratedName)\(join)ServiceProtocol" } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift deleted file mode 100644 index 8ae83f70e..000000000 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright 2023, 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. - */ - -/// Creates enums containing useful type aliases and static properties for the methods, services and -/// namespaces described in a ``CodeGenerationRequest`` object, using types from -/// ``StructuredSwiftRepresentation``. -/// -/// For example, in the case of the ``Echo`` service, the ``TypealiasTranslator`` will create -/// a representation for the following generated code: -/// ```swift -/// public enum Echo_Echo { -/// public static let descriptor = GRPCCore.ServiceDescriptor.echo_Echo -/// -/// public enum Method { -/// public enum Get { -/// public typealias Input = Echo_EchoRequest -/// public typealias Output = Echo_EchoResponse -/// public static let descriptor = GRPCCore.MethodDescriptor( -/// service: Echo_Echo.descriptor.fullyQualifiedService, -/// method: "Get" -/// ) -/// } -/// -/// public enum Collect { -/// public typealias Input = Echo_EchoRequest -/// public typealias Output = Echo_EchoResponse -/// public static let descriptor = GRPCCore.MethodDescriptor( -/// service: Echo_Echo.descriptor.fullyQualifiedService, -/// method: "Collect" -/// ) -/// } -/// // ... -/// -/// public static let descriptors: [GRPCCore.MethodDescriptor] = [ -/// Get.descriptor, -/// Collect.descriptor, -/// // ... -/// ] -/// } -/// -/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// public typealias StreamingServiceProtocol = Echo_EchoServiceStreamingProtocol -/// @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -/// public typealias ServiceProtocol = Echo_EchoServiceProtocol -/// -/// } -/// -/// extension GRPCCore.ServiceDescriptor { -/// public static let echo_Echo = Self( -/// package: "echo", -/// service: "Echo" -/// ) -/// } -/// ``` -/// -/// A ``CodeGenerationRequest`` can contain multiple namespaces, so the TypealiasTranslator will create a ``CodeBlock`` -/// for each namespace. -struct TypealiasTranslator: SpecializedTranslator { - let client: Bool - let server: Bool - let accessLevel: SourceGenerator.Config.AccessLevel - - init(client: Bool, server: Bool, accessLevel: SourceGenerator.Config.AccessLevel) { - self.client = client - self.server = server - self.accessLevel = accessLevel - } - - func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { - var codeBlocks = [CodeBlock]() - - for service in codeGenerationRequest.services { - codeBlocks.append( - CodeBlock( - item: .declaration( - try self.makeServiceEnum( - from: service, - named: service.namespacedGeneratedName - ) - ) - ) - ) - - codeBlocks.append( - CodeBlock(item: .declaration(self.makeServiceDescriptorExtension(for: service))) - ) - } - - return codeBlocks - } -} - -extension TypealiasTranslator { - private func makeServiceEnum( - from service: ServiceDescriptor, - named name: String - ) throws -> Declaration { - var serviceEnum = EnumDescription( - accessModifier: self.accessModifier, - name: name - ) - var methodsEnum = EnumDescription(accessModifier: self.accessModifier, name: "Method") - let methods = service.methods - - // Create the method specific enums. - for method in methods { - let methodEnum = self.makeMethodEnum(from: method, in: service) - methodsEnum.members.append(methodEnum) - } - - // Create the method descriptor array. - let methodDescriptorsDeclaration = self.makeMethodDescriptors(for: service) - methodsEnum.members.append(methodDescriptorsDeclaration) - - // Create the static service descriptor property. - let staticServiceDescriptorProperty = self.makeStaticServiceDescriptorProperty(for: service) - - serviceEnum.members.append(.variable(staticServiceDescriptorProperty)) - serviceEnum.members.append(.enum(methodsEnum)) - - if self.server { - // Create the streaming and non-streaming service protocol type aliases. - let serviceProtocols = self.makeServiceProtocolsTypealiases(for: service) - serviceEnum.members.append(contentsOf: serviceProtocols) - } - - if self.client { - // Create the client protocol type alias. - let clientProtocol = self.makeClientProtocolTypealias(for: service) - serviceEnum.members.append(clientProtocol) - - // Create type alias for Client struct. - let clientStruct = self.makeClientStructTypealias(for: service) - serviceEnum.members.append(clientStruct) - } - - return .enum(serviceEnum) - } - - private func makeMethodEnum( - from method: MethodDescriptor, - in service: ServiceDescriptor - ) -> Declaration { - var methodEnum = EnumDescription(name: method.name.generatedUpperCase) - - let inputTypealias = Declaration.typealias( - accessModifier: self.accessModifier, - name: "Input", - existingType: .member([method.inputType]) - ) - let outputTypealias = Declaration.typealias( - accessModifier: self.accessModifier, - name: "Output", - existingType: .member([method.outputType]) - ) - let descriptorVariable = self.makeMethodDescriptor( - from: method, - in: service - ) - methodEnum.members.append(inputTypealias) - methodEnum.members.append(outputTypealias) - methodEnum.members.append(descriptorVariable) - - methodEnum.accessModifier = self.accessModifier - - return .enum(methodEnum) - } - - private func makeMethodDescriptor( - from method: MethodDescriptor, - in service: ServiceDescriptor - ) -> Declaration { - let fullyQualifiedService = MemberAccessDescription( - left: .memberAccess( - MemberAccessDescription( - left: .identifierType(.member([service.namespacedGeneratedName])), - right: "descriptor" - ) - ), - right: "fullyQualifiedService" - ) - - let descriptorDeclarationLeft = Expression.identifier(.pattern("descriptor")) - let descriptorDeclarationRight = Expression.functionCall( - FunctionCallDescription( - calledExpression: .identifierType(.member(["GRPCCore", "MethodDescriptor"])), - arguments: [ - FunctionArgumentDescription( - label: "service", - expression: .memberAccess(fullyQualifiedService) - ), - FunctionArgumentDescription( - label: "method", - expression: .literal(method.name.base) - ), - ] - ) - ) - - return .variable( - accessModifier: self.accessModifier, - isStatic: true, - kind: .let, - left: descriptorDeclarationLeft, - right: descriptorDeclarationRight - ) - } - - private func makeMethodDescriptors( - for service: ServiceDescriptor - ) -> Declaration { - var methodDescriptors = [Expression]() - let methodNames = service.methods.map { $0.name.generatedUpperCase } - - for methodName in methodNames { - let methodDescriptorPath = Expression.memberAccess( - MemberAccessDescription( - left: .identifierType( - .member([methodName]) - ), - right: "descriptor" - ) - ) - methodDescriptors.append(methodDescriptorPath) - } - - return .variable( - accessModifier: self.accessModifier, - isStatic: true, - kind: .let, - left: .identifier(.pattern("descriptors")), - type: .array(.member(["GRPCCore", "MethodDescriptor"])), - right: .literal(.array(methodDescriptors)) - ) - } - - private func makeServiceProtocolsTypealiases( - for service: ServiceDescriptor - ) -> [Declaration] { - let streamingServiceProtocolTypealias = Declaration.typealias( - accessModifier: self.accessModifier, - name: "StreamingServiceProtocol", - existingType: .member("\(service.namespacedGeneratedName)_StreamingServiceProtocol") - ) - let serviceProtocolTypealias = Declaration.typealias( - accessModifier: self.accessModifier, - name: "ServiceProtocol", - existingType: .member("\(service.namespacedGeneratedName)_ServiceProtocol") - ) - - return [ - .guarded( - self.availabilityGuard, - streamingServiceProtocolTypealias - ), - .guarded( - self.availabilityGuard, - serviceProtocolTypealias - ), - ] - } - - private func makeClientProtocolTypealias( - for service: ServiceDescriptor - ) -> Declaration { - return .guarded( - self.availabilityGuard, - .typealias( - accessModifier: self.accessModifier, - name: "ClientProtocol", - existingType: .member("\(service.namespacedGeneratedName)_ClientProtocol") - ) - ) - } - - private func makeClientStructTypealias( - for service: ServiceDescriptor - ) -> Declaration { - return .guarded( - self.availabilityGuard, - .typealias( - accessModifier: self.accessModifier, - name: "Client", - existingType: .member("\(service.namespacedGeneratedName)_Client") - ) - ) - } - - private func makeServiceIdentifier(_ service: ServiceDescriptor) -> String { - let prefix: String - - if service.namespace.normalizedBase.isEmpty { - prefix = "" - } else { - prefix = service.namespace.normalizedBase + "_" - } - - return prefix + service.name.normalizedBase - } - - private func makeStaticServiceDescriptorProperty( - for service: ServiceDescriptor - ) -> VariableDescription { - let serviceIdentifier = makeServiceIdentifier(service) - - return VariableDescription( - accessModifier: self.accessModifier, - isStatic: true, - kind: .let, - left: .identifierPattern("descriptor"), - right: .memberAccess( - MemberAccessDescription( - left: .identifierPattern("GRPCCore.ServiceDescriptor"), - right: serviceIdentifier - ) - ) - ) - } - - private func makeServiceDescriptorExtension( - for service: ServiceDescriptor - ) -> Declaration { - let serviceIdentifier = makeServiceIdentifier(service) - - let serviceDescriptorInitialization = Expression.functionCall( - FunctionCallDescription( - calledExpression: .identifierType(.member("Self")), - arguments: [ - FunctionArgumentDescription( - label: "package", - expression: .literal(service.namespace.base) - ), - FunctionArgumentDescription( - label: "service", - expression: .literal(service.name.base) - ), - ] - ) - ) - - return .extension( - ExtensionDescription( - onType: "GRPCCore.ServiceDescriptor", - declarations: [ - .variable( - VariableDescription( - accessModifier: self.accessModifier, - isStatic: true, - kind: .let, - left: .identifier(.pattern(serviceIdentifier)), - right: serviceDescriptorInitialization - ) - ) - ] - ) - ) - } -} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 6dd72600c..cfb72633c 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -42,29 +42,29 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA - func methodA( + func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { - public func methodA( + public func methodA( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.methodA( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } @@ -75,8 +75,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ message: NamespaceA_ServiceARequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.ClientRequest( @@ -86,7 +86,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { return try await self.methodA( request: request, options: options, - handleResponse + onResponse: handleResponse ) } } @@ -100,22 +100,22 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodA - public func methodA( + public func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.client.unary( request: request, descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } } @@ -149,29 +149,29 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA - func methodA( + func methodA( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { - public func methodA( + public func methodA( request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.methodA( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } @@ -181,19 +181,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public func methodA( metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.StreamingClientRequest( metadata: metadata, - producer: requestProducer + producer: producer ) return try await self.methodA( request: request, options: options, - handleResponse + onResponse: handleResponse ) } } @@ -207,22 +207,22 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodA - public func methodA( + public func methodA( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.client.clientStreaming( request: request, descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } } @@ -256,27 +256,27 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA - func methodA( + func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { - public func methodA( + public func methodA( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.methodA( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } @@ -296,7 +296,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { return try await self.methodA( request: request, options: options, - handleResponse + onResponse: handleResponse ) } } @@ -310,20 +310,20 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodA - public func methodA( + public func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.client.serverStreaming( request: request, descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } } @@ -357,27 +357,27 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA - func methodA( + func methodA( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { - public func methodA( + public func methodA( request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.methodA( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } @@ -387,17 +387,17 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { public func methodA( metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable { let request = GRPCCore.StreamingClientRequest( metadata: metadata, - producer: requestProducer + producer: producer ) return try await self.methodA( request: request, options: options, - handleResponse + onResponse: handleResponse ) } } @@ -411,20 +411,20 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodA - public func methodA( + public func methodA( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.client.bidirectionalStreaming( request: request, descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } } @@ -466,52 +466,52 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA - func methodA( + func methodA( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable /// Documentation for MethodB - func methodB( + func methodB( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { - package func methodA( + package func methodA( request: GRPCCore.StreamingClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.methodA( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } - package func methodB( + package func methodB( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.methodB( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } @@ -521,19 +521,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { package func methodA( metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - requestProducer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.StreamingClientRequest( metadata: metadata, - producer: requestProducer + producer: producer ) return try await self.methodA( request: request, options: options, - handleResponse + onResponse: handleResponse ) } @@ -551,7 +551,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { return try await self.methodB( request: request, options: options, - handleResponse + onResponse: handleResponse ) } } @@ -565,40 +565,40 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodA - package func methodA( + package func methodA( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.client.clientStreaming( request: request, descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } /// Documentation for MethodB - package func methodB( + package func methodB( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> R - ) async throws -> R where R: Sendable { + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { try await self.client.serverStreaming( request: request, descriptor: NamespaceA_ServiceA.Method.MethodB.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } } @@ -632,29 +632,29 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) internal protocol ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA - func methodA( + func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension ServiceA.ClientProtocol { - internal func methodA( + internal func methodA( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.methodA( request: request, serializer: GRPCProtobuf.ProtobufSerializer(), deserializer: GRPCProtobuf.ProtobufDeserializer(), options: options, - body + onResponse: handleResponse ) } } @@ -665,8 +665,8 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { _ message: ServiceARequest, metadata: GRPCCore.Metadata = [:], options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { let request = GRPCCore.ClientRequest( @@ -676,7 +676,7 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { return try await self.methodA( request: request, options: options, - handleResponse + onResponse: handleResponse ) } } @@ -690,22 +690,22 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } /// Documentation for MethodA - internal func methodA( + internal func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { + ) async throws -> Result where Result: Sendable { try await self.client.unary( request: request, descriptor: ServiceA.Method.MethodA.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } } @@ -791,14 +791,19 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } private func assertClientCodeTranslation( - codeGenerationRequest: CodeGenerationRequest, + codeGenerationRequest request: CodeGenerationRequest, expectedSwift: String, accessLevel: SourceGenerator.Config.AccessLevel, file: StaticString = #filePath, line: UInt = #line ) throws { - let translator = ClientCodeTranslator(accessLevel: accessLevel) - let codeBlocks = try translator.translate(from: codeGenerationRequest) + let translator = ClientCodeTranslator() + let codeBlocks = translator.translate( + accessModifier: AccessModifier(accessLevel), + services: request.services, + serializer: request.lookupSerializer, + deserializer: request.lookupDeserializer + ) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) let contents = renderer.renderedContents() diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index fde5448d9..a7a17c575 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -634,12 +634,17 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } private func assertServerCodeTranslation( - codeGenerationRequest: CodeGenerationRequest, + codeGenerationRequest request: CodeGenerationRequest, expectedSwift: String, accessLevel: SourceGenerator.Config.AccessLevel ) throws { - let translator = ServerCodeTranslator(accessLevel: accessLevel) - let codeBlocks = try translator.translate(from: codeGenerationRequest) + let translator = ServerCodeTranslator() + let codeBlocks = translator.translate( + accessModifier: AccessModifier(accessLevel), + services: request.services, + serializer: request.lookupSerializer, + deserializer: request.lookupDeserializer + ) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) let contents = renderer.renderedContents() diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index a8294ef31..2c43f40c8 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -348,14 +348,19 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { extension TypealiasTranslatorSnippetBasedTests { private func assertTypealiasTranslation( - codeGenerationRequest: CodeGenerationRequest, + codeGenerationRequest request: CodeGenerationRequest, expectedSwift: String, client: Bool, server: Bool, accessLevel: SourceGenerator.Config.AccessLevel ) throws { - let translator = TypealiasTranslator(client: client, server: server, accessLevel: accessLevel) - let codeBlocks = try translator.translate(from: codeGenerationRequest) + let translator = MetadataTranslator() + let codeBlocks = translator.translate( + accessModifier: AccessModifier(accessLevel), + services: request.services, + client: client, + server: server + ) let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) let contents = renderer.renderedContents() From 8bc2a25602763744c1edbd7f4fe05fcec194c819 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 26 Nov 2024 11:17:37 +0000 Subject: [PATCH 508/580] Simplify client/server translator tests (#2129) Motivation: The client and server translator tests have a lot of repetition. Now that they are based on structured swift extension, which is tested separately, the tests can be much briefer as the translators just glue a few bit together. Modifications: - Remove a bunch of translator tests as they are already covered by lower level tests Result: Less maintenance burden --- ...lientCodeTranslatorSnippetBasedTests.swift | 717 +----------------- ...erverCodeTranslatorSnippetBasedTests.swift | 576 +------------- 2 files changed, 44 insertions(+), 1249 deletions(-) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index cfb72633c..d27bdc674 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -14,14 +14,14 @@ * limitations under the License. */ -#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) - -import XCTest +import Testing @testable import GRPCCodeGen -final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { - func testClientCodeTranslatorUnaryMethod() throws { +@Suite +struct ClientCodeTranslatorSnippetBasedTests { + @Test + func translate() { let method = MethodDescriptor( documentation: "/// Documentation for MethodA", name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), @@ -30,14 +30,15 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { inputType: "NamespaceA_ServiceARequest", outputType: "NamespaceA_ServiceAResponse" ) + let service = ServiceDescriptor( documentation: "/// Documentation for ServiceA", name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), methods: [method] ) - let expectedSwift = - """ + + let expectedSwift = """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { @@ -121,694 +122,22 @@ final class ClientCodeTranslatorSnippetBasedTests: XCTestCase { } """ - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testClientCodeTranslatorClientStreamingMethod() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: true, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), - namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - public func methodA( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.methodA( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - /// Documentation for MethodA - public func methodA( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.methodA( - request: request, - options: options, - onResponse: handleResponse - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } - """ - - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) + let rendered = self.render(accessLevel: .public, service: service) + #expect(rendered == expectedSwift) } - func testClientCodeTranslatorServerStreamingMethod() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: false, - isOutputStreaming: true, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), - namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - public func methodA( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.methodA( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - /// Documentation for MethodA - public func methodA( - _ message: NamespaceA_ServiceARequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.methodA( - request: request, - options: options, - onResponse: handleResponse - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } - """ - - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testClientCodeTranslatorBidirectionalStreamingMethod() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: true, - isOutputStreaming: true, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), - namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - public func methodA( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.methodA( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - /// Documentation for MethodA - public func methodA( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.methodA( - request: request, - options: options, - onResponse: handleResponse - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.bidirectionalStreaming( - request: request, - descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } - """ - - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testClientCodeTranslatorMultipleMethod() throws { - let methodA = MethodDescriptor( - documentation: "/// Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: true, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let methodB = MethodDescriptor( - documentation: "/// Documentation for MethodB", - name: Name(base: "MethodB", generatedUpperCase: "MethodB", generatedLowerCase: "methodB"), - isInputStreaming: false, - isOutputStreaming: true, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), - namespace: Name(base: "namespaceA", generatedUpperCase: "NamespaceA", generatedLowerCase: ""), - methods: [methodA, methodB] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceA_ClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - - /// Documentation for MethodB - func methodB( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - package func methodA( - request: GRPCCore.StreamingClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.methodA( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - - package func methodB( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.methodB( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - /// Documentation for MethodA - package func methodA( - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.StreamingClientRequest( - metadata: metadata, - producer: producer - ) - return try await self.methodA( - request: request, - options: options, - onResponse: handleResponse - ) - } - - /// Documentation for MethodB - package func methodB( - _ message: NamespaceA_ServiceARequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.methodB( - request: request, - options: options, - onResponse: handleResponse - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { - private let client: GRPCCore.GRPCClient - - package init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - package func methodA( - request: GRPCCore.StreamingClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.clientStreaming( - request: request, - descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - - /// Documentation for MethodB - package func methodB( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable { - try await self.client.serverStreaming( - request: request, - descriptor: NamespaceA_ServiceA.Method.MethodB.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } - """ - - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .package - ) - } - - func testClientCodeTranslatorNoNamespaceService() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "ServiceARequest", - outputType: "ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol ServiceA_ClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension ServiceA.ClientProtocol { - internal func methodA( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.methodA( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - onResponse: handleResponse - ) - } - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension ServiceA.ClientProtocol { - /// Documentation for MethodA - internal func methodA( - _ message: ServiceARequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.methodA( - request: request, - options: options, - onResponse: handleResponse - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal struct ServiceA_Client: ServiceA.ClientProtocol { - private let client: GRPCCore.GRPCClient - - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - internal func methodA( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.unary( - request: request, - descriptor: ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } - """ - - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .internal - ) - } - - func testClientCodeTranslatorMultipleServices() throws { - let serviceA = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: ""), - namespace: Name( - base: "nammespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "" - ), - methods: [] - ) - let serviceB = ServiceDescriptor( - documentation: """ - /// Documentation for ServiceB - /// - /// Line 2 - """, - name: Name(base: "ServiceB", generatedUpperCase: "ServiceB", generatedLowerCase: ""), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), - methods: [] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_ClientProtocol: Sendable {} - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ClientProtocol { - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - } - /// Documentation for ServiceB - /// - /// Line 2 - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol ServiceB_ClientProtocol: Sendable {} - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension ServiceB.ClientProtocol { - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension ServiceB.ClientProtocol { - } - /// Documentation for ServiceB - /// - /// Line 2 - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public struct ServiceB_Client: ServiceB.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - } - """ - - try self.assertClientCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - private func assertClientCodeTranslation( - codeGenerationRequest request: CodeGenerationRequest, - expectedSwift: String, - accessLevel: SourceGenerator.Config.AccessLevel, - file: StaticString = #filePath, - line: UInt = #line - ) throws { - let translator = ClientCodeTranslator() - let codeBlocks = translator.translate( - accessModifier: AccessModifier(accessLevel), - services: request.services, - serializer: request.lookupSerializer, - deserializer: request.lookupDeserializer - ) - let renderer = TextBasedRenderer.default - renderer.renderCodeBlocks(codeBlocks) - let contents = renderer.renderedContents() - try XCTAssertEqualWithDiff(contents, expectedSwift, file: file, line: line) + private func render( + accessLevel: AccessModifier, + service: ServiceDescriptor + ) -> String { + let translator = ClientCodeTranslator() + let codeBlocks = translator.translate(accessModifier: accessLevel, services: [service]) { + "GRPCProtobuf.ProtobufSerializer<\($0)>()" + } deserializer: { + "GRPCProtobuf.ProtobufDeserializer<\($0)>()" + } + let renderer = TextBasedRenderer.default + renderer.renderCodeBlocks(codeBlocks) + return renderer.renderedContents() } } - -#endif // os(macOS) || os(Linux) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index a7a17c575..e2b047ade 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -14,14 +14,14 @@ * limitations under the License. */ -#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) - -import XCTest +import Testing @testable import GRPCCodeGen -final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { - func testServerCodeTranslatorUnaryMethod() throws { +@Suite +final class ServerCodeTranslatorSnippetBasedTests { + @Test + func translate() { let method = MethodDescriptor( documentation: "/// Documentation for unaryMethod", name: Name(base: "UnaryMethod", generatedUpperCase: "Unary", generatedLowerCase: "unary"), @@ -30,6 +30,7 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { inputType: "NamespaceA_ServiceARequest", outputType: "NamespaceA_ServiceAResponse" ) + let service = ServiceDescriptor( documentation: "/// Documentation for ServiceA", name: Name( @@ -44,8 +45,8 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { ), methods: [method] ) - let expectedSwift = - """ + + let expectedSwift = """ /// Documentation for ServiceA @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { @@ -98,558 +99,23 @@ final class ServerCodeTranslatorSnippetBasedTests: XCTestCase { } """ - try self.assertServerCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testServerCodeTranslatorInputStreamingMethod() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for inputStreamingMethod", - name: Name( - base: "InputStreamingMethod", - generatedUpperCase: "InputStreaming", - generatedLowerCase: "inputStreaming" - ), - isInputStreaming: true, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for inputStreamingMethod - func inputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.inputStreaming( - request: request, - context: context - ) - } - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { - /// Documentation for inputStreamingMethod - func inputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - } - /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ServiceProtocol { - package func inputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.inputStreaming( - request: request, - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - } - """ - - try self.assertServerCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .package - ) - } - - func testServerCodeTranslatorOutputStreamingMethod() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for outputStreamingMethod", - name: Name( - base: "OutputStreamingMethod", - generatedUpperCase: "OutputStreaming", - generatedLowerCase: "outputStreaming" - ), - isInputStreaming: false, - isOutputStreaming: true, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name( - base: "ServiceATest", - generatedUpperCase: "ServiceA", - generatedLowerCase: "serviceA" - ), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for outputStreamingMethod - func outputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.outputStreaming( - request: request, - context: context - ) - } - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { - /// Documentation for outputStreamingMethod - func outputStreaming( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ServiceProtocol { - public func outputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.outputStreaming( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return response - } - } - """ - - try self.assertServerCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testServerCodeTranslatorBidirectionalStreamingMethod() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for bidirectionalStreamingMethod", - name: Name( - base: "BidirectionalStreamingMethod", - generatedUpperCase: "BidirectionalStreaming", - generatedLowerCase: "bidirectionalStreaming" - ), - isInputStreaming: true, - isOutputStreaming: true, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name( - base: "ServiceATest", - generatedUpperCase: "ServiceA", - generatedLowerCase: "serviceA" - ), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: NamespaceA_ServiceA.Method.BidirectionalStreaming.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.bidirectionalStreaming( - request: request, - context: context - ) - } - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { - /// Documentation for bidirectionalStreamingMethod - func bidirectionalStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ServiceProtocol { - } - """ - - try self.assertServerCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .package - ) - } - - func testServerCodeTranslatorMultipleMethods() throws { - let inputStreamingMethod = MethodDescriptor( - documentation: "/// Documentation for inputStreamingMethod", - name: Name( - base: "InputStreamingMethod", - generatedUpperCase: "InputStreaming", - generatedLowerCase: "inputStreaming" - ), - isInputStreaming: true, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let outputStreamingMethod = MethodDescriptor( - documentation: "/// Documentation for outputStreamingMethod", - name: Name( - base: "outputStreamingMethod", - generatedUpperCase: "OutputStreaming", - generatedLowerCase: "outputStreaming" - ), - isInputStreaming: false, - isOutputStreaming: true, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name( - base: "ServiceATest", - generatedUpperCase: "ServiceA", - generatedLowerCase: "serviceA" - ), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [inputStreamingMethod, outputStreamingMethod] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for inputStreamingMethod - func inputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - - /// Documentation for outputStreamingMethod - func outputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: NamespaceA_ServiceA.Method.InputStreaming.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.inputStreaming( - request: request, - context: context - ) - } - ) - router.registerHandler( - forMethod: NamespaceA_ServiceA.Method.OutputStreaming.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.outputStreaming( - request: request, - context: context - ) - } - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { - /// Documentation for inputStreamingMethod - func inputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - - /// Documentation for outputStreamingMethod - func outputStreaming( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ServiceProtocol { - internal func inputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.inputStreaming( - request: request, - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - - internal func outputStreaming( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.outputStreaming( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return response - } - } - """ - - try assertServerCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .internal - ) - } - - func testServerCodeTranslatorNoNamespaceService() throws { - let method = MethodDescriptor( - documentation: "/// Documentation for MethodA", - name: Name(base: "methodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "NamespaceA_ServiceARequest", - outputType: "NamespaceA_ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name( - base: "ServiceATest", - generatedUpperCase: "ServiceA", - generatedLowerCase: "serviceA" - ), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), - methods: [method] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for MethodA - func methodA( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: ServiceA.Method.MethodA.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.methodA( - request: request, - context: context - ) - } - ) - } - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol ServiceA_ServiceProtocol: ServiceA.StreamingServiceProtocol { - /// Documentation for MethodA - func methodA( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - } - /// Partial conformance to `ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension ServiceA.ServiceProtocol { - internal func methodA( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.methodA( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - } - """ - - try self.assertServerCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - accessLevel: .internal - ) + let rendered = self.render(accessLevel: .public, service: service) + #expect(rendered == expectedSwift) } - func testServerCodeTranslatorMoreServicesOrder() throws { - let serviceA = ServiceDescriptor( - documentation: "/// Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - let serviceB = ServiceDescriptor( - documentation: "/// Documentation for ServiceB", - name: Name(base: "ServiceB", generatedUpperCase: "ServiceB", generatedLowerCase: "serviceB"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - let expectedSwift = - """ - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) {} - } - /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} - /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceA.ServiceProtocol { - } - /// Documentation for ServiceB - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceB_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceB.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) {} - } - /// Documentation for ServiceB - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol NamespaceA_ServiceB_ServiceProtocol: NamespaceA_ServiceB.StreamingServiceProtocol {} - /// Partial conformance to `NamespaceA_ServiceB_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension NamespaceA_ServiceB.ServiceProtocol { - } - """ - - try self.assertServerCodeTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [serviceA, serviceB]), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - private func assertServerCodeTranslation( - codeGenerationRequest request: CodeGenerationRequest, - expectedSwift: String, - accessLevel: SourceGenerator.Config.AccessLevel - ) throws { + private func render( + accessLevel: AccessModifier, + service: ServiceDescriptor + ) -> String { let translator = ServerCodeTranslator() - let codeBlocks = translator.translate( - accessModifier: AccessModifier(accessLevel), - services: request.services, - serializer: request.lookupSerializer, - deserializer: request.lookupDeserializer - ) + let codeBlocks = translator.translate(accessModifier: accessLevel, services: [service]) { + "GRPCProtobuf.ProtobufSerializer<\($0)>()" + } deserializer: { + "GRPCProtobuf.ProtobufDeserializer<\($0)>()" + } + let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) - let contents = renderer.renderedContents() - try XCTAssertEqualWithDiff(contents, expectedSwift) + return renderer.renderedContents() } } - -#endif // os(macOS) || os(Linux) From d70fbad6c4c2243d9b872a8219958382158d024c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 26 Nov 2024 13:30:52 +0000 Subject: [PATCH 509/580] Simplify generated code (#2131) Motivation: A number of things have changed since the generated code was initially designed, which means it can be simplified and improved. Modifications: - As services are namespaced within a namespace-service enum (rather than within a service enum in a namespace enum), they no longer need to be grouped by namespace. - As deployment targets must be set in the package manifest, the generated code no longer needs to be annotated with availability guards so these have been removed. Result: Simpler code generation, better generated code. --- .../Internal/Renderer/TextBasedRenderer.swift | 5 +- .../Internal/StructuredSwift+Server.swift | 17 +++--- .../StructuredSwift+ServiceMetadata.swift | 11 ++-- .../Internal/StructuredSwift+Types.swift | 12 ---- .../StructuredSwiftRepresentation.swift | 2 +- .../Translator/ClientCodeTranslator.swift | 24 ++------ .../IDLToStructuredSwiftTranslator.swift | 60 ++++++++++++------- .../Translator/MetadataTranslator.swift | 16 ----- .../Translator/ServerCodeTranslator.swift | 24 ++------ .../StructuredSwift+MetadataTests.swift | 16 ----- ...lientCodeTranslatorSnippetBasedTests.swift | 6 +- ...uredSwiftTranslatorSnippetBasedTests.swift | 11 ++-- ...erverCodeTranslatorSnippetBasedTests.swift | 8 +-- ...TypealiasTranslatorSnippetBasedTests.swift | 34 +++-------- 14 files changed, 75 insertions(+), 171 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 9986164a9..3d25ee7f0 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -1134,8 +1134,9 @@ struct TextBasedRenderer: RendererProtocol { /// Renders the specified code block. func renderCodeBlock(_ description: CodeBlock) { if let comment = description.comment { renderComment(comment) } - let item = description.item - renderCodeBlockItem(item) + if let item = description.item { + renderCodeBlockItem(item) + } } /// Renders the specified code blocks. diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index cc446825d..c46986fa3 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -97,16 +97,13 @@ extension ExtensionDescription { return ExtensionDescription( onType: extensionName, declarations: [ - .guarded( - .grpc, - .function( - .registerMethods( - accessLevel: accessLevel, - serviceNamespace: serviceNamespace, - methods: methods, - serializer: serializer, - deserializer: deserializer - ) + .function( + .registerMethods( + accessLevel: accessLevel, + serviceNamespace: serviceNamespace, + methods: methods, + serializer: serializer, + deserializer: deserializer ) ) ] diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index 62e1933d8..7305f4e50 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -288,13 +288,10 @@ extension EnumDescription { typealiasNames.append("Client") } let typealiases: [Declaration] = typealiasNames.map { alias in - .guarded( - .grpc, - .typealias( - accessModifier: accessModifier, - name: alias, - existingType: .member(name + "_" + alias) - ) + .typealias( + accessModifier: accessModifier, + name: alias, + existingType: .member(name + "_" + alias) ) } description.members.append(contentsOf: typealiases) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift index 981566543..ac3499037 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift @@ -14,18 +14,6 @@ * limitations under the License. */ -extension AvailabilityDescription { - package static let grpc = AvailabilityDescription( - osVersions: [ - OSVersion(os: .macOS, version: "15.0"), - OSVersion(os: .iOS, version: "18.0"), - OSVersion(os: .watchOS, version: "11.0"), - OSVersion(os: .tvOS, version: "18.0"), - OSVersion(os: .visionOS, version: "2.0"), - ] - ) -} - extension ExistingTypeDescription { fileprivate static func grpcCore(_ typeName: String) -> Self { return .member(["GRPCCore", typeName]) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 16cfdbfe8..ca44b7f79 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -1207,7 +1207,7 @@ struct CodeBlock: Equatable, Codable, Sendable { var comment: Comment? /// The code block item that appears below the comment. - var item: CodeBlockItem + var item: CodeBlockItem? } /// A description of a Swift file. diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index ec8850a17..c5b49622b 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -79,22 +79,6 @@ struct ClientCodeTranslator { init() {} func translate( - accessModifier: AccessModifier, - services: [ServiceDescriptor], - serializer: (String) -> String, - deserializer: (String) -> String - ) -> [CodeBlock] { - services.flatMap { service in - self.translate( - accessModifier: accessModifier, - service: service, - serializer: serializer, - deserializer: deserializer - ) - } - } - - private func translate( accessModifier: AccessModifier, service: ServiceDescriptor, serializer: (String) -> String, @@ -114,7 +98,7 @@ struct ClientCodeTranslator { blocks.append( CodeBlock( comment: .preFormatted(service.documentation), - item: .declaration(.guarded(.grpc, .protocol(clientProtocol))) + item: .declaration(.protocol(clientProtocol)) ) ) @@ -126,7 +110,7 @@ struct ClientCodeTranslator { deserializer: deserializer ) blocks.append( - CodeBlock(item: .declaration(.guarded(.grpc, .extension(extensionWithDefaults)))) + CodeBlock(item: .declaration(.extension(extensionWithDefaults))) ) let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods( @@ -135,7 +119,7 @@ struct ClientCodeTranslator { methods: service.methods ) blocks.append( - CodeBlock(item: .declaration(.guarded(.grpc, .extension(extensionWithExplodedAPI)))) + CodeBlock(item: .declaration(.extension(extensionWithExplodedAPI))) ) let clientStruct: StructDescription = .client( @@ -148,7 +132,7 @@ struct ClientCodeTranslator { blocks.append( CodeBlock( comment: .preFormatted(service.documentation), - item: .declaration(.guarded(.grpc, .struct(clientStruct))) + item: .declaration(.struct(clientStruct)) ) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 0fcbc5388..2aedea18e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -28,34 +28,50 @@ struct IDLToStructuredSwiftTranslator: Translator { try self.validateInput(codeGenerationRequest) let accessModifier = AccessModifier(accessLevel) + var codeBlocks: [CodeBlock] = [] let metadataTranslator = MetadataTranslator() - var codeBlocks = metadataTranslator.translate( - accessModifier: accessModifier, - services: codeGenerationRequest.services, - client: client, - server: server - ) + let serverTranslator = ServerCodeTranslator() + let clientTranslator = ClientCodeTranslator() - if server { - let translator = ServerCodeTranslator() - let blocks = translator.translate( - accessModifier: accessModifier, - services: codeGenerationRequest.services, - serializer: codeGenerationRequest.lookupSerializer, - deserializer: codeGenerationRequest.lookupDeserializer + for service in codeGenerationRequest.services { + codeBlocks.append( + CodeBlock(comment: .mark("\(service.fullyQualifiedName)", sectionBreak: true)) ) - codeBlocks.append(contentsOf: blocks) - } - if client { - let translator = ClientCodeTranslator() - let blocks = translator.translate( + let metadata = metadataTranslator.translate( accessModifier: accessModifier, - services: codeGenerationRequest.services, - serializer: codeGenerationRequest.lookupSerializer, - deserializer: codeGenerationRequest.lookupDeserializer + service: service, + client: client, + server: server ) - codeBlocks.append(contentsOf: blocks) + codeBlocks.append(contentsOf: metadata) + + if server { + codeBlocks.append( + CodeBlock(comment: .mark("\(service.fullyQualifiedName) (server)", sectionBreak: false)) + ) + + let blocks = serverTranslator.translate( + accessModifier: accessModifier, + service: service, + serializer: codeGenerationRequest.lookupSerializer, + deserializer: codeGenerationRequest.lookupDeserializer + ) + codeBlocks.append(contentsOf: blocks) + } + + if client { + codeBlocks.append( + CodeBlock(comment: .mark("\(service.fullyQualifiedName) (client)", sectionBreak: false)) + ) + let blocks = clientTranslator.translate( + accessModifier: accessModifier, + service: service, + serializer: codeGenerationRequest.lookupSerializer, + deserializer: codeGenerationRequest.lookupDeserializer + ) + codeBlocks.append(contentsOf: blocks) + } } let fileDescription = FileDescription( diff --git a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift index 23e68c2cc..80675fe1f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift @@ -18,22 +18,6 @@ struct MetadataTranslator { init() {} func translate( - accessModifier: AccessModifier, - services: [ServiceDescriptor], - client: Bool, - server: Bool - ) -> [CodeBlock] { - return services.flatMap { service in - self.translate( - accessModifier: accessModifier, - service: service, - client: client, - server: server - ) - } - } - - private func translate( accessModifier: AccessModifier, service: ServiceDescriptor, client: Bool, diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index e769e6b61..5757b2ea4 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -61,22 +61,6 @@ struct ServerCodeTranslator { init() {} func translate( - accessModifier: AccessModifier, - services: [ServiceDescriptor], - serializer: (String) -> String, - deserializer: (String) -> String - ) -> [CodeBlock] { - return services.flatMap { service in - self.translate( - accessModifier: accessModifier, - service: service, - serializer: serializer, - deserializer: deserializer - ) - } - } - - private func translate( accessModifier: AccessModifier, service: ServiceDescriptor, serializer: (String) -> String, @@ -106,7 +90,7 @@ struct ServerCodeTranslator { blocks.append( CodeBlock( comment: .preFormatted(service.documentation), - item: .declaration(.guarded(.grpc, .protocol(streamingServiceProtocol))) + item: .declaration(.protocol(streamingServiceProtocol)) ) ) @@ -122,7 +106,7 @@ struct ServerCodeTranslator { blocks.append( CodeBlock( comment: .doc("Conformance to `GRPCCore.RegistrableRPCService`."), - item: .declaration(.guarded(.grpc, .extension(registerExtension))) + item: .declaration(.extension(registerExtension)) ) ) @@ -136,7 +120,7 @@ struct ServerCodeTranslator { blocks.append( CodeBlock( comment: .preFormatted(service.documentation), - item: .declaration(.guarded(.grpc, .protocol(serviceProtocol))) + item: .declaration(.protocol(serviceProtocol)) ) ) @@ -150,7 +134,7 @@ struct ServerCodeTranslator { blocks.append( CodeBlock( comment: .doc("Partial conformance to `\(streamingServiceProtocolName)`."), - item: .declaration(.guarded(.grpc, .extension(streamingServiceDefaultImplExtension))) + item: .declaration(.extension(streamingServiceDefaultImplExtension)) ) ) diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index 76997a643..d1b4ef147 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -21,18 +21,6 @@ import Testing extension StructuedSwiftTests { @Suite("Metadata") struct Metadata { - @Test("@available(...)") - func grpcAvailability() async throws { - let availability: AvailabilityDescription = .grpc - let structDecl = StructDescription(name: "Ignored") - let expected = """ - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - struct Ignored {} - """ - - #expect(render(.guarded(availability, .struct(structDecl))) == expected) - } - @Test("typealias Input = ", arguments: AccessModifier.allCases) func methodInputTypealias(access: AccessModifier) { let decl: TypealiasDescription = .methodInput(accessModifier: access, name: "Foo") @@ -263,9 +251,7 @@ extension StructuedSwiftTests { if config.server { expected += """ - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) \(access) typealias StreamingServiceProtocol = Foo_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) \(access) typealias ServiceProtocol = Foo_ServiceProtocol """ } @@ -276,9 +262,7 @@ extension StructuedSwiftTests { } expected += """ - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) \(access) typealias ClientProtocol = Foo_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) \(access) typealias Client = Foo_Client """ } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index d27bdc674..e6fb41c7d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -40,7 +40,6 @@ struct ClientCodeTranslatorSnippetBasedTests { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { /// Documentation for MethodA func methodA( @@ -51,7 +50,6 @@ struct ClientCodeTranslatorSnippetBasedTests { onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result ) async throws -> Result where Result: Sendable } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { public func methodA( request: GRPCCore.ClientRequest, @@ -69,7 +67,6 @@ struct ClientCodeTranslatorSnippetBasedTests { ) } } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ClientProtocol { /// Documentation for MethodA public func methodA( @@ -92,7 +89,6 @@ struct ClientCodeTranslatorSnippetBasedTests { } } /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { private let client: GRPCCore.GRPCClient @@ -131,7 +127,7 @@ struct ClientCodeTranslatorSnippetBasedTests { service: ServiceDescriptor ) -> String { let translator = ClientCodeTranslator() - let codeBlocks = translator.translate(accessModifier: accessLevel, services: [service]) { + let codeBlocks = translator.translate(accessModifier: accessLevel, service: service) { "GRPCProtobuf.ProtobufSerializer<\($0)>()" } deserializer: { "GRPCProtobuf.ProtobufDeserializer<\($0)>()" diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index c362e1c91..0f185d4ff 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -212,14 +212,14 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { @_spi(Secret) internal import Foo @_spi(Secret) internal import enum Foo.Bar + // MARK: - namespaceA.ServiceA + public enum NamespaceA_ServiceA { public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA public enum Method { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol } @@ -230,23 +230,20 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) } + // MARK: namespaceA.ServiceA (server) + /// Documentation for AService - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } /// Documentation for AService - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index e2b047ade..36cc93396 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -48,7 +48,6 @@ final class ServerCodeTranslatorSnippetBasedTests { let expectedSwift = """ /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Documentation for unaryMethod func unary( @@ -57,9 +56,7 @@ final class ServerCodeTranslatorSnippetBasedTests { ) async throws -> GRPCCore.StreamingServerResponse } /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( forMethod: NamespaceA_ServiceA.Method.Unary.descriptor, @@ -75,7 +72,6 @@ final class ServerCodeTranslatorSnippetBasedTests { } } /// Documentation for ServiceA - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { /// Documentation for unaryMethod func unary( @@ -84,7 +80,6 @@ final class ServerCodeTranslatorSnippetBasedTests { ) async throws -> GRPCCore.ServerResponse } /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) extension NamespaceA_ServiceA.ServiceProtocol { public func unary( request: GRPCCore.StreamingServerRequest, @@ -108,12 +103,11 @@ final class ServerCodeTranslatorSnippetBasedTests { service: ServiceDescriptor ) -> String { let translator = ServerCodeTranslator() - let codeBlocks = translator.translate(accessModifier: accessLevel, services: [service]) { + let codeBlocks = translator.translate(accessModifier: accessLevel, service: service) { "GRPCProtobuf.ProtobufSerializer<\($0)>()" } deserializer: { "GRPCProtobuf.ProtobufDeserializer<\($0)>()" } - let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) return renderer.renderedContents() diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 2c43f40c8..c0ec7771a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -57,13 +57,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { MethodA.descriptor ] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { @@ -101,13 +97,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { @@ -145,9 +137,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol } extension GRPCCore.ServiceDescriptor { @@ -185,9 +175,7 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { public enum Method { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { @@ -275,13 +263,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { MethodA.descriptor ] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias StreamingServiceProtocol = ServiceA_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ServiceProtocol = ServiceA_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias ClientProtocol = ServiceA_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) public typealias Client = ServiceA_Client } extension GRPCCore.ServiceDescriptor { @@ -319,13 +303,9 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { package enum Method { package static let descriptors: [GRPCCore.MethodDescriptor] = [] } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) package typealias Client = NamespaceA_ServiceA_Client } extension GRPCCore.ServiceDescriptor { @@ -355,12 +335,14 @@ extension TypealiasTranslatorSnippetBasedTests { accessLevel: SourceGenerator.Config.AccessLevel ) throws { let translator = MetadataTranslator() - let codeBlocks = translator.translate( - accessModifier: AccessModifier(accessLevel), - services: request.services, - client: client, - server: server - ) + let codeBlocks = request.services.flatMap { service in + translator.translate( + accessModifier: AccessModifier(accessLevel), + service: service, + client: client, + server: server + ) + } let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) let contents = renderer.renderedContents() From 12a736686d928f62c96a8a2dee21c96b376327d6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 27 Nov 2024 13:18:05 +0000 Subject: [PATCH 510/580] Use nested protocols (#2132) Motivation: The minimum Swift version we require supports protocols being declared within another type. This means that all protocols and types we generate for a service can be declared within the enum namespace for that service and we can remove the typealiases. Modifications: - Declare generated protocols within their enum namespace - Inline generated service descriptors Result: Simpler generated code --- .../StructuredSwift+ServiceMetadata.swift | 105 +++--- .../Translator/ClientCodeTranslator.swift | 60 ++-- .../IDLToStructuredSwiftTranslator.swift | 4 +- .../Translator/MetadataTranslator.swift | 11 +- .../Translator/ServerCodeTranslator.swift | 89 ++--- .../StructuredSwift+MetadataTests.swift | 82 ++--- ...lientCodeTranslatorSnippetBasedTests.swift | 79 ++--- ...uredSwiftTranslatorSnippetBasedTests.swift | 22 +- ...erverCodeTranslatorSnippetBasedTests.swift | 34 +- ...TypealiasTranslatorSnippetBasedTests.swift | 308 ++---------------- 10 files changed, 218 insertions(+), 576 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index 7305f4e50..5efa44749 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -43,12 +43,12 @@ extension TypealiasDescription { extension VariableDescription { /// ``` /// static let descriptor = GRPCCore.MethodDescriptor( - /// service: .descriptor.fullyQualifiedService, + /// service: GRPCCore.ServiceDescriptor(fullyQualifiedServiceName: ""), /// method: "" /// ``` package static func methodDescriptor( accessModifier: AccessModifier? = nil, - serviceNamespace: String, + literalFullyQualifiedService: String, literalMethodName: String ) -> Self { return VariableDescription( @@ -62,9 +62,11 @@ extension VariableDescription { arguments: [ FunctionArgumentDescription( label: "service", - expression: .identifierType( - .member([serviceNamespace, "descriptor"]) - ).dot("fullyQualifiedService") + expression: .functionCall( + .serviceDescriptor( + literalFullyQualifiedService: literalFullyQualifiedService + ) + ) ), FunctionArgumentDescription( label: "method", @@ -77,18 +79,34 @@ extension VariableDescription { } /// ``` - /// static let descriptor = GRPCCore.ServiceDescriptor. + /// static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: ) /// ``` package static func serviceDescriptor( accessModifier: AccessModifier? = nil, - namespacedProperty: String + literalFullyQualifiedService name: String ) -> Self { return VariableDescription( accessModifier: accessModifier, isStatic: true, kind: .let, left: .identifierPattern("descriptor"), - right: .identifier(.type(.serviceDescriptor)).dot(namespacedProperty) + right: .functionCall(.serviceDescriptor(literalFullyQualifiedService: name)) + ) + } +} + +extension FunctionCallDescription { + package static func serviceDescriptor( + literalFullyQualifiedService: String + ) -> Self { + FunctionCallDescription( + calledExpression: .identifier(.type(.serviceDescriptor)), + arguments: [ + FunctionArgumentDescription( + label: "fullyQualifiedService", + expression: .literal(literalFullyQualifiedService) + ) + ] ) } } @@ -97,16 +115,14 @@ extension ExtensionDescription { /// ``` /// extension GRPCCore.ServiceDescriptor { /// static let = Self( - /// package: "", - /// service: "" + /// fullyQualifiedService: /// ) /// } /// ``` package static func serviceDescriptor( accessModifier: AccessModifier? = nil, propertyName: String, - literalNamespace: String, - literalService: String + literalFullyQualifiedService: String ) -> ExtensionDescription { return ExtensionDescription( onType: "GRPCCore.ServiceDescriptor", @@ -117,17 +133,7 @@ extension ExtensionDescription { kind: .let, left: .identifier(.pattern(propertyName)), right: .functionCall( - calledExpression: .identifierType(.member("Self")), - arguments: [ - FunctionArgumentDescription( - label: "package", - expression: .literal(literalNamespace) - ), - FunctionArgumentDescription( - label: "service", - expression: .literal(literalService) - ), - ] + .serviceDescriptor(literalFullyQualifiedService: literalFullyQualifiedService) ) ) ] @@ -169,7 +175,7 @@ extension EnumDescription { accessModifier: AccessModifier? = nil, name: String, literalMethod: String, - serviceNamespace: String, + literalFullyQualifiedService: String, inputType: String, outputType: String ) -> Self { @@ -182,7 +188,7 @@ extension EnumDescription { .variable( .methodDescriptor( accessModifier: accessModifier, - serviceNamespace: serviceNamespace, + literalFullyQualifiedService: literalFullyQualifiedService, literalMethodName: literalMethod ) ), @@ -209,7 +215,7 @@ extension EnumDescription { /// ``` package static func methodsNamespace( accessModifier: AccessModifier? = nil, - serviceNamespace: String, + literalFullyQualifiedService: String, methods: [MethodDescriptor] ) -> EnumDescription { var description = EnumDescription(accessModifier: accessModifier, name: "Method") @@ -221,7 +227,7 @@ extension EnumDescription { accessModifier: accessModifier, name: method.name.base, literalMethod: method.name.base, - serviceNamespace: serviceNamespace, + literalFullyQualifiedService: literalFullyQualifiedService, inputType: method.inputType, outputType: method.outputType ) @@ -245,57 +251,31 @@ extension EnumDescription { /// enum Method { /// ... /// } - /// @available(...) - /// typealias StreamingServiceProtocol = ... - /// @available(...) - /// typealias ServiceProtocol = ... - /// ... /// } /// ``` package static func serviceNamespace( accessModifier: AccessModifier? = nil, name: String, - serviceDescriptorProperty: String, - client: Bool, - server: Bool, + literalFullyQualifiedService: String, methods: [MethodDescriptor] ) -> EnumDescription { var description = EnumDescription(accessModifier: accessModifier, name: name) - // static let descriptor = GRPCCore.ServiceDescriptor. + // static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "...") let descriptor = VariableDescription.serviceDescriptor( accessModifier: accessModifier, - namespacedProperty: serviceDescriptorProperty + literalFullyQualifiedService: literalFullyQualifiedService ) description.members.append(.variable(descriptor)) // enum Method { ... } let methodsNamespace: EnumDescription = .methodsNamespace( accessModifier: accessModifier, - serviceNamespace: name, + literalFullyQualifiedService: literalFullyQualifiedService, methods: methods ) description.members.append(.enum(methodsNamespace)) - // Typealiases for the various protocols. - var typealiasNames: [String] = [] - if server { - typealiasNames.append("StreamingServiceProtocol") - typealiasNames.append("ServiceProtocol") - } - if client { - typealiasNames.append("ClientProtocol") - typealiasNames.append("Client") - } - let typealiases: [Declaration] = typealiasNames.map { alias in - .typealias( - accessModifier: accessModifier, - name: alias, - existingType: .member(name + "_" + alias) - ) - } - description.members.append(contentsOf: typealiases) - return description } } @@ -312,18 +292,14 @@ extension [CodeBlock] { /// ``` package static func serviceMetadata( accessModifier: AccessModifier? = nil, - service: ServiceDescriptor, - client: Bool, - server: Bool + service: ServiceDescriptor ) -> Self { var blocks: [CodeBlock] = [] let serviceNamespace: EnumDescription = .serviceNamespace( accessModifier: accessModifier, name: service.namespacedGeneratedName, - serviceDescriptorProperty: service.namespacedServicePropertyName, - client: client, - server: server, + literalFullyQualifiedService: service.fullyQualifiedName, methods: service.methods ) blocks.append(CodeBlock(item: .declaration(.enum(serviceNamespace)))) @@ -331,8 +307,7 @@ extension [CodeBlock] { let descriptorExtension: ExtensionDescription = .serviceDescriptor( accessModifier: accessModifier, propertyName: service.namespacedServicePropertyName, - literalNamespace: service.namespace.base, - literalService: service.name.base + literalFullyQualifiedService: service.fullyQualifiedName ) blocks.append(CodeBlock(item: .declaration(.extension(descriptorExtension)))) diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index c5b49622b..d994fb6a0 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -86,25 +86,41 @@ struct ClientCodeTranslator { ) -> [CodeBlock] { var blocks = [CodeBlock]() - let protocolName = "\(service.namespacedGeneratedName)_ClientProtocol" - let protocolTypealias = "\(service.namespacedGeneratedName).ClientProtocol" - let structName = "\(service.namespacedGeneratedName)_Client" + let `extension` = ExtensionDescription( + onType: service.namespacedGeneratedName, + declarations: [ + // protocol ClientProtocol { ... } + .commentable( + .preFormatted(service.documentation), + .protocol( + .clientProtocol( + accessLevel: accessModifier, + name: "ClientProtocol", + methods: service.methods + ) + ) + ), - let clientProtocol: ProtocolDescription = .clientProtocol( - accessLevel: accessModifier, - name: protocolName, - methods: service.methods - ) - blocks.append( - CodeBlock( - comment: .preFormatted(service.documentation), - item: .declaration(.protocol(clientProtocol)) - ) + // struct Client: ClientProtocol { ... } + .commentable( + .preFormatted(service.documentation), + .struct( + .client( + accessLevel: accessModifier, + name: "Client", + serviceEnum: service.namespacedGeneratedName, + clientProtocol: "ClientProtocol", + methods: service.methods + ) + ) + ), + ] ) + blocks.append(.declaration(.extension(`extension`))) let extensionWithDefaults: ExtensionDescription = .clientMethodSignatureWithDefaults( accessLevel: accessModifier, - name: protocolTypealias, + name: "\(service.namespacedGeneratedName).ClientProtocol", methods: service.methods, serializer: serializer, deserializer: deserializer @@ -115,27 +131,13 @@ struct ClientCodeTranslator { let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods( accessLevel: accessModifier, - on: protocolTypealias, + on: "\(service.namespacedGeneratedName).ClientProtocol", methods: service.methods ) blocks.append( CodeBlock(item: .declaration(.extension(extensionWithExplodedAPI))) ) - let clientStruct: StructDescription = .client( - accessLevel: accessModifier, - name: structName, - serviceEnum: service.namespacedGeneratedName, - clientProtocol: protocolTypealias, - methods: service.methods - ) - blocks.append( - CodeBlock( - comment: .preFormatted(service.documentation), - item: .declaration(.struct(clientStruct)) - ) - ) - return blocks } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 2aedea18e..839ef0fa1 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -40,9 +40,7 @@ struct IDLToStructuredSwiftTranslator: Translator { let metadata = metadataTranslator.translate( accessModifier: accessModifier, - service: service, - client: client, - server: server + service: service ) codeBlocks.append(contentsOf: metadata) diff --git a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift index 80675fe1f..a7b91132e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/MetadataTranslator.swift @@ -19,15 +19,8 @@ struct MetadataTranslator { func translate( accessModifier: AccessModifier, - service: ServiceDescriptor, - client: Bool, - server: Bool + service: ServiceDescriptor ) -> [CodeBlock] { - .serviceMetadata( - accessModifier: accessModifier, - service: service, - client: client, - server: server - ) + .serviceMetadata(accessModifier: accessModifier, service: service) } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 5757b2ea4..20f78d3b3 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -68,36 +68,41 @@ struct ServerCodeTranslator { ) -> [CodeBlock] { var blocks = [CodeBlock]() - let serviceProtocolName = self.protocolName(service: service, streaming: false) - let serviceTypealiasName = self.protocolName( - service: service, - streaming: false, - joinedUsing: "." - ) - let streamingServiceProtocolName = self.protocolName(service: service, streaming: true) - let streamingServiceTypealiasName = self.protocolName( - service: service, - streaming: true, - joinedUsing: "." - ) + let `extension` = ExtensionDescription( + onType: service.namespacedGeneratedName, + declarations: [ + // protocol StreamingServiceProtocol { ... } + .commentable( + .preFormatted(service.documentation), + .protocol( + .streamingService( + accessLevel: accessModifier, + name: "StreamingServiceProtocol", + methods: service.methods + ) + ) + ), - // protocol _StreamingServiceProtocol { ... } - let streamingServiceProtocol: ProtocolDescription = .streamingService( - accessLevel: accessModifier, - name: streamingServiceProtocolName, - methods: service.methods - ) - blocks.append( - CodeBlock( - comment: .preFormatted(service.documentation), - item: .declaration(.protocol(streamingServiceProtocol)) - ) + // protocol ServiceProtocol { ... } + .commentable( + .preFormatted(service.documentation), + .protocol( + .service( + accessLevel: accessModifier, + name: "ServiceProtocol", + streamingProtocol: "\(service.namespacedGeneratedName).StreamingServiceProtocol", + methods: service.methods + ) + ) + ), + ] ) + blocks.append(.declaration(.extension(`extension`))) - // extension _StreamingServiceProtocol> { ... } + // extension .StreamingServiceProtocol> { ... } let registerExtension: ExtensionDescription = .registrableRPCServiceDefaultImplementation( accessLevel: accessModifier, - on: streamingServiceTypealiasName, + on: "\(service.namespacedGeneratedName).StreamingServiceProtocol", serviceNamespace: service.namespacedGeneratedName, methods: service.methods, serializer: serializer, @@ -110,45 +115,15 @@ struct ServerCodeTranslator { ) ) - // protocol _ServiceProtocol { ... } - let serviceProtocol: ProtocolDescription = .service( - accessLevel: accessModifier, - name: serviceProtocolName, - streamingProtocol: streamingServiceTypealiasName, - methods: service.methods - ) - blocks.append( - CodeBlock( - comment: .preFormatted(service.documentation), - item: .declaration(.protocol(serviceProtocol)) - ) - ) - // extension _ServiceProtocol { ... } let streamingServiceDefaultImplExtension: ExtensionDescription = .streamingServiceProtocolDefaultImplementation( accessModifier: accessModifier, - on: serviceTypealiasName, + on: "\(service.namespacedGeneratedName).ServiceProtocol", methods: service.methods ) - blocks.append( - CodeBlock( - comment: .doc("Partial conformance to `\(streamingServiceProtocolName)`."), - item: .declaration(.extension(streamingServiceDefaultImplExtension)) - ) - ) + blocks.append(.declaration(.extension(streamingServiceDefaultImplExtension))) return blocks } - - private func protocolName( - service: ServiceDescriptor, - streaming: Bool, - joinedUsing join: String = "_" - ) -> String { - if streaming { - return "\(service.namespacedGeneratedName)\(join)StreamingServiceProtocol" - } - return "\(service.namespacedGeneratedName)\(join)ServiceProtocol" - } } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index d1b4ef147..6169098e8 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -42,13 +42,13 @@ extension StructuedSwiftTests { func staticMethodDescriptorProperty(access: AccessModifier) { let decl: VariableDescription = .methodDescriptor( accessModifier: access, - serviceNamespace: "FooService", + literalFullyQualifiedService: "foo.Foo", literalMethodName: "Bar" ) let expected = """ \(access) static let descriptor = GRPCCore.MethodDescriptor( - service: FooService.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "foo.Foo"), method: "Bar" ) """ @@ -56,16 +56,18 @@ extension StructuedSwiftTests { } @Test( - "static let descriptor = GRPCCore.ServiceDescriptor.", + "static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService:)", arguments: AccessModifier.allCases ) func staticServiceDescriptorProperty(access: AccessModifier) { let decl: VariableDescription = .serviceDescriptor( accessModifier: access, - namespacedProperty: "foo" + literalFullyQualifiedService: "foo.Bar" ) - let expected = "\(access) static let descriptor = GRPCCore.ServiceDescriptor.foo" + let expected = """ + \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "foo.Bar") + """ #expect(render(.variable(decl)) == expected) } @@ -74,16 +76,12 @@ extension StructuedSwiftTests { let decl: ExtensionDescription = .serviceDescriptor( accessModifier: access, propertyName: "foo", - literalNamespace: "echo", - literalService: "EchoService" + literalFullyQualifiedService: "echo.EchoService" ) let expected = """ extension GRPCCore.ServiceDescriptor { - \(access) static let foo = Self( - package: "echo", - service: "EchoService" - ) + \(access) static let foo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.EchoService") } """ #expect(render(.extension(decl)) == expected) @@ -115,7 +113,7 @@ extension StructuedSwiftTests { accessModifier: access, name: "Foo", literalMethod: "Foo", - serviceNamespace: "Bar_Baz", + literalFullyQualifiedService: "bar.Bar", inputType: "FooInput", outputType: "FooOutput" ) @@ -125,7 +123,7 @@ extension StructuedSwiftTests { \(access) typealias Input = FooInput \(access) typealias Output = FooOutput \(access) static let descriptor = GRPCCore.MethodDescriptor( - service: Bar_Baz.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"), method: "Foo" ) } @@ -137,7 +135,7 @@ extension StructuedSwiftTests { func methodsNamespaceEnum(access: AccessModifier) { let decl: EnumDescription = .methodsNamespace( accessModifier: access, - serviceNamespace: "Bar_Baz", + literalFullyQualifiedService: "bar.Bar", methods: [ .init( documentation: "", @@ -156,7 +154,7 @@ extension StructuedSwiftTests { \(access) typealias Input = FooInput \(access) typealias Output = FooOutput \(access) static let descriptor = GRPCCore.MethodDescriptor( - service: Bar_Baz.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"), method: "Foo" ) } @@ -172,7 +170,7 @@ extension StructuedSwiftTests { func methodsNamespaceEnumNoMethods(access: AccessModifier) { let decl: EnumDescription = .methodsNamespace( accessModifier: access, - serviceNamespace: "Bar_Baz", + literalFullyQualifiedService: "bar.Bar", methods: [] ) @@ -189,9 +187,7 @@ extension StructuedSwiftTests { let decl: EnumDescription = .serviceNamespace( accessModifier: access, name: "Foo", - serviceDescriptorProperty: "foo", - client: false, - server: false, + literalFullyQualifiedService: "Foo", methods: [ .init( documentation: "", @@ -206,13 +202,13 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Foo { - \(access) static let descriptor = GRPCCore.ServiceDescriptor.foo + \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo") \(access) enum Method { \(access) enum Bar { \(access) typealias Input = BarInput \(access) typealias Output = BarOutput \(access) static let descriptor = GRPCCore.MethodDescriptor( - service: Foo.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo"), method: "Bar" ) } @@ -225,53 +221,23 @@ extension StructuedSwiftTests { #expect(render(.enum(decl)) == expected) } - @Test( - "enum { ... } (no methods)", - arguments: AccessModifier.allCases, - [(true, true), (false, false), (true, false), (false, true)] - ) - func serviceNamespaceEnumNoMethods(access: AccessModifier, config: (client: Bool, server: Bool)) - { + @Test("enum { ... } (no methods)", arguments: AccessModifier.allCases) + func serviceNamespaceEnumNoMethods(access: AccessModifier) { let decl: EnumDescription = .serviceNamespace( accessModifier: access, name: "Foo", - serviceDescriptorProperty: "foo", - client: config.client, - server: config.server, + literalFullyQualifiedService: "Foo", methods: [] ) - var expected = """ + let expected = """ \(access) enum Foo { - \(access) static let descriptor = GRPCCore.ServiceDescriptor.foo + \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo") \(access) enum Method { \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] - }\n - """ - - if config.server { - expected += """ - \(access) typealias StreamingServiceProtocol = Foo_StreamingServiceProtocol - \(access) typealias ServiceProtocol = Foo_ServiceProtocol - """ - } - - if config.client { - if config.server { - expected += "\n" + } } - - expected += """ - \(access) typealias ClientProtocol = Foo_ClientProtocol - \(access) typealias Client = Foo_Client - """ - } - - if config.client || config.server { - expected += "\n}" - } else { - expected += "}" - } + """ #expect(render(.enum(decl)) == expected) } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index e6fb41c7d..3516f4d67 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -39,16 +39,47 @@ struct ClientCodeTranslatorSnippetBasedTests { ) let expectedSwift = """ - /// Documentation for ServiceA - public protocol NamespaceA_ServiceA_ClientProtocol: Sendable { - /// Documentation for MethodA - func methodA( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result - ) async throws -> Result where Result: Sendable + extension NamespaceA_ServiceA { + /// Documentation for ServiceA + public protocol ClientProtocol: Sendable { + /// Documentation for MethodA + func methodA( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + + /// Documentation for ServiceA + public struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + public init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Documentation for MethodA + public func methodA( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } } extension NamespaceA_ServiceA.ClientProtocol { public func methodA( @@ -88,34 +119,6 @@ struct ClientCodeTranslatorSnippetBasedTests { ) } } - /// Documentation for ServiceA - public struct NamespaceA_ServiceA_Client: NamespaceA_ServiceA.ClientProtocol { - private let client: GRPCCore.GRPCClient - - public init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } - - /// Documentation for MethodA - public func methodA( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in - try response.message - } - ) async throws -> Result where Result: Sendable { - try await self.client.unary( - request: request, - descriptor: NamespaceA_ServiceA.Method.MethodA.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - onResponse: handleResponse - ) - } - } """ let rendered = self.render(accessLevel: .public, service: service) diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 0f185d4ff..7ed4727bb 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -215,35 +215,31 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // MARK: - namespaceA.ServiceA public enum NamespaceA_ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") public enum Method { public static let descriptors: [GRPCCore.MethodDescriptor] = [] } - public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol } extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) + public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") } // MARK: namespaceA.ServiceA (server) - /// Documentation for AService - public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + extension NamespaceA_ServiceA { + /// Documentation for AService + public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + + /// Documentation for AService + public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} + } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA_ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } - /// Documentation for AService - public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} - - /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. extension NamespaceA_ServiceA.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 36cc93396..7cb57cc18 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -47,13 +47,24 @@ final class ServerCodeTranslatorSnippetBasedTests { ) let expectedSwift = """ - /// Documentation for ServiceA - public protocol NamespaceA_ServiceA_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for unaryMethod - func unary( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse + extension NamespaceA_ServiceA { + /// Documentation for ServiceA + public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Documentation for unaryMethod + func unary( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + } + + /// Documentation for ServiceA + public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { + /// Documentation for unaryMethod + func unary( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + } } /// Conformance to `GRPCCore.RegistrableRPCService`. extension NamespaceA_ServiceA.StreamingServiceProtocol { @@ -71,15 +82,6 @@ final class ServerCodeTranslatorSnippetBasedTests { ) } } - /// Documentation for ServiceA - public protocol NamespaceA_ServiceA_ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { - /// Documentation for unaryMethod - func unary( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - } - /// Partial conformance to `NamespaceA_ServiceA_StreamingServiceProtocol`. extension NamespaceA_ServiceA.ServiceProtocol { public func unary( request: GRPCCore.StreamingServerRequest, diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index c0ec7771a..504dbcec3 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -14,13 +14,13 @@ * limitations under the License. */ -#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) - -import XCTest +import Testing @testable import GRPCCodeGen -final class TypealiasTranslatorSnippetBasedTests: XCTestCase { +@Suite +struct TypealiasTranslatorSnippetBasedTests { + @Test func testTypealiasTranslator() throws { let method = MethodDescriptor( documentation: "Documentation for MethodA", @@ -40,222 +40,16 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { ), methods: [method] ) - let expectedSwift = - """ + + let expectedSwift = """ public enum NamespaceA_ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA + public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") public enum Method { public enum MethodA { public typealias Input = NamespaceA_ServiceARequest public typealias Output = NamespaceA_ServiceAResponse public static let descriptor = GRPCCore.MethodDescriptor( - service: NamespaceA_ServiceA.descriptor.fullyQualifiedService, - method: "MethodA" - ) - } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - MethodA.descriptor - ] - } - public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - public typealias Client = NamespaceA_ServiceA_Client - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .public - ) - } - - func testTypealiasTranslatorNoMethodsServiceClientAndServer() throws { - let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - let expectedSwift = - """ - public enum NamespaceA_ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - public typealias Client = NamespaceA_ServiceA_Client - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .public - ) - } - - func testTypealiasTranslatorServer() throws { - let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - let expectedSwift = - """ - public enum NamespaceA_ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - public typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - public typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: false, - server: true, - accessLevel: .public - ) - } - - func testTypealiasTranslatorClient() throws { - let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - let expectedSwift = - """ - public enum NamespaceA_ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - public typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - public typealias Client = NamespaceA_ServiceA_Client - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: true, - server: false, - accessLevel: .public - ) - } - - func testTypealiasTranslatorNoClientNoServer() throws { - let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - let expectedSwift = - """ - public enum NamespaceA_ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA - public enum Method { - public static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - } - extension GRPCCore.ServiceDescriptor { - public static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: false, - server: false, - accessLevel: .public - ) - } - - func testTypealiasTranslatorEmptyNamespace() throws { - let method = MethodDescriptor( - documentation: "Documentation for MethodA", - name: Name(base: "MethodA", generatedUpperCase: "MethodA", generatedLowerCase: "methodA"), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "ServiceARequest", - outputType: "ServiceAResponse" - ) - let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name(base: "", generatedUpperCase: "", generatedLowerCase: ""), - methods: [method] - ) - let expectedSwift = - """ - public enum ServiceA { - public static let descriptor = GRPCCore.ServiceDescriptor.ServiceA - public enum Method { - public enum MethodA { - public typealias Input = ServiceARequest - public typealias Output = ServiceAResponse - public static let descriptor = GRPCCore.MethodDescriptor( - service: ServiceA.descriptor.fullyQualifiedService, + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA"), method: "MethodA" ) } @@ -263,91 +57,29 @@ final class TypealiasTranslatorSnippetBasedTests: XCTestCase { MethodA.descriptor ] } - public typealias StreamingServiceProtocol = ServiceA_StreamingServiceProtocol - public typealias ServiceProtocol = ServiceA_ServiceProtocol - public typealias ClientProtocol = ServiceA_ClientProtocol - public typealias Client = ServiceA_Client } extension GRPCCore.ServiceDescriptor { - public static let ServiceA = Self( - package: "", - service: "ServiceA" - ) + public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") } """ - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .public - ) - } - - func testTypealiasTranslatorNoMethodsService() throws { - let service = ServiceDescriptor( - documentation: "Documentation for ServiceA", - name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), - namespace: Name( - base: "namespaceA", - generatedUpperCase: "NamespaceA", - generatedLowerCase: "namespaceA" - ), - methods: [] - ) - let expectedSwift = - """ - package enum NamespaceA_ServiceA { - package static let descriptor = GRPCCore.ServiceDescriptor.namespaceA_ServiceA - package enum Method { - package static let descriptors: [GRPCCore.MethodDescriptor] = [] - } - package typealias StreamingServiceProtocol = NamespaceA_ServiceA_StreamingServiceProtocol - package typealias ServiceProtocol = NamespaceA_ServiceA_ServiceProtocol - package typealias ClientProtocol = NamespaceA_ServiceA_ClientProtocol - package typealias Client = NamespaceA_ServiceA_Client - } - extension GRPCCore.ServiceDescriptor { - package static let namespaceA_ServiceA = Self( - package: "namespaceA", - service: "ServiceA" - ) - } - """ - - try self.assertTypealiasTranslation( - codeGenerationRequest: makeCodeGenerationRequest(services: [service]), - expectedSwift: expectedSwift, - client: true, - server: true, - accessLevel: .package - ) + #expect(self.render(accessLevel: .public, service: service) == expectedSwift) } } extension TypealiasTranslatorSnippetBasedTests { - private func assertTypealiasTranslation( - codeGenerationRequest request: CodeGenerationRequest, - expectedSwift: String, - client: Bool, - server: Bool, - accessLevel: SourceGenerator.Config.AccessLevel - ) throws { + func render( + accessLevel: SourceGenerator.Config.AccessLevel, + service: ServiceDescriptor + ) -> String { let translator = MetadataTranslator() - let codeBlocks = request.services.flatMap { service in - translator.translate( - accessModifier: AccessModifier(accessLevel), - service: service, - client: client, - server: server - ) - } + let codeBlocks = translator.translate( + accessModifier: AccessModifier(accessLevel), + service: service + ) + let renderer = TextBasedRenderer.default renderer.renderCodeBlocks(codeBlocks) - let contents = renderer.renderedContents() - try XCTAssertEqualWithDiff(contents, expectedSwift) + return renderer.renderedContents() } } - -#endif // os(macOS) || os(Linux) From 311486ee0257809e5e4608e476a675cf2b0f6aa9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Wed, 27 Nov 2024 14:04:00 +0000 Subject: [PATCH 511/580] Add documentation to generated code (#2133) Motivation: The generated code doesn't consistently include documentation from the source IDL. It also doesn't explain the difference between each of the generated protocols and when to use them. Modifications: Add documentation to the generated code, and where possible, also include any documentation from the source IDL. Result: Better documented generated code --- .../Internal/StructuredSwift+Client.swift | 148 ++++++++++++++---- .../Internal/StructuredSwift+Server.swift | 54 ++++++- .../StructuredSwift+ServiceMetadata.swift | 89 ++++++++--- .../Translator/ClientCodeTranslator.swift | 43 ++++- .../Internal/Translator/Docs.swift | 64 ++++++++ .../Translator/ServerCodeTranslator.swift | 52 +++++- .../StructuredSwift+ClientTests.swift | 108 ++++++++++++- .../StructuredSwift+MetadataTests.swift | 20 +++ .../StructuredSwift+ServerTests.swift | 28 +++- ...lientCodeTranslatorSnippetBasedTests.swift | 85 +++++++++- .../Internal/Translator/DocsTests.swift | 101 ++++++++++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 35 ++++- ...erverCodeTranslatorSnippetBasedTests.swift | 58 ++++++- ...TypealiasTranslatorSnippetBasedTests.swift | 9 ++ 14 files changed, 809 insertions(+), 85 deletions(-) create mode 100644 Sources/GRPCCodeGen/Internal/Translator/Docs.swift create mode 100644 Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift index b4e62194e..6c026f2d3 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -210,7 +210,7 @@ extension ProtocolDescription { conformances: ["Sendable"], members: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(docs(for: method)), .function( signature: .clientMethod( name: method.name.generatedLowerCase, @@ -251,16 +251,19 @@ extension ExtensionDescription { ExtensionDescription( onType: name, declarations: methods.map { method in - .function( - .clientMethodWithDefaults( - accessLevel: accessLevel, - name: method.name.generatedLowerCase, - input: method.inputType, - output: method.outputType, - streamingInput: method.isInputStreaming, - streamingOutput: method.isOutputStreaming, - serializer: .identifierPattern(serializer(method.inputType)), - deserializer: .identifierPattern(deserializer(method.outputType)) + .commentable( + .preFormatted(docs(for: method, serializers: false)), + .function( + .clientMethodWithDefaults( + accessLevel: accessLevel, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + serializer: .identifierPattern(serializer(method.inputType)), + deserializer: .identifierPattern(deserializer(method.outputType)) + ) ) ) } @@ -495,11 +498,11 @@ extension ExtensionDescription { on extensionName: String, methods: [MethodDescriptor] ) -> ExtensionDescription { - ExtensionDescription( + return ExtensionDescription( onType: extensionName, declarations: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(explodedDocs(for: method)), .function( .clientMethodExploded( accessLevel: accessLevel, @@ -665,26 +668,36 @@ extension StructDescription { conformances: [clientProtocol], members: [ .variable(accessModifier: .private, kind: .let, left: "client", type: .grpcClient), - .function( - accessModifier: accessLevel, - kind: .initializer, - parameters: [ - ParameterDescription(label: "wrapping", name: "client", type: .grpcClient) - ], - whereClause: nil, - body: [ - .expression( - .assignment( - left: .identifierPattern("self").dot("client"), - right: .identifierPattern("client") + .commentable( + .preFormatted( + """ + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + """ + ), + .function( + accessModifier: accessLevel, + kind: .initializer, + parameters: [ + ParameterDescription(label: "wrapping", name: "client", type: .grpcClient) + ], + whereClause: nil, + body: [ + .expression( + .assignment( + left: .identifierPattern("self").dot("client"), + right: .identifierPattern("client") + ) ) - ) - ] + ] + ) ), ] + methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(docs(for: method)), .function( .clientMethod( accessLevel: accessLevel, @@ -702,3 +715,82 @@ extension StructDescription { ) } } + +private func docs( + for method: MethodDescriptor, + serializers includeSerializers: Bool = true +) -> String { + let summary = "/// Call the \"\(method.name.base)\" method." + + let request: String + if method.isInputStreaming { + request = "A streaming request producing `\(method.inputType)` messages." + } else { + request = "A request containing a single `\(method.inputType)` message." + } + + let parameters = """ + /// - Parameters: + /// - request: \(request) + """ + + let serializers = """ + /// - serializer: A serializer for `\(method.inputType)` messages. + /// - deserializer: A deserializer for `\(method.outputType)` messages. + """ + + let otherParameters = """ + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + """ + + let allParameters: String + if includeSerializers { + allParameters = parameters + "\n" + serializers + "\n" + otherParameters + } else { + allParameters = parameters + "\n" + otherParameters + } + + return Docs.interposeDocs(method.documentation, between: summary, and: allParameters) +} + +private func explodedDocs(for method: MethodDescriptor) -> String { + let summary = "/// Call the \"\(method.name.base)\" method." + var parameters = """ + /// - Parameters: + """ + + if !method.isInputStreaming { + parameters += "\n" + parameters += """ + /// - message: request message to send. + """ + } + + parameters += "\n" + parameters += """ + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + """ + + if method.isInputStreaming { + parameters += "\n" + parameters += """ + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + """ + } + + parameters += "\n" + parameters += """ + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + """ + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index c46986fa3..44093da6a 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -56,13 +56,31 @@ extension ProtocolDescription { name: String, methods: [MethodDescriptor] ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.normalizedBase)" method. + """ + + let parameters = """ + /// - Parameters: + /// - request: A streaming request of `\(method.inputType)` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `\(method.outputType)` messages. + """ + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) + } + return ProtocolDescription( accessModifier: accessLevel, name: name, conformances: ["GRPCCore.RegistrableRPCService"], members: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(docs(for: method)), .function( signature: .serverMethod( name: method.name.generatedLowerCase, @@ -123,13 +141,45 @@ extension ProtocolDescription { streamingProtocol: String, methods: [MethodDescriptor] ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.normalizedBase)" method. + """ + + let request: String + if method.isInputStreaming { + request = "A streaming request of `\(method.inputType)` messages." + } else { + request = "A request containing a single `\(method.inputType)` message." + } + + let returns: String + if method.isOutputStreaming { + returns = "A streaming response of `\(method.outputType)` messages." + } else { + returns = "A response containing a single `\(method.outputType)` message." + } + + let parameters = """ + /// - Parameters: + /// - request: \(request) + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: \(returns) + """ + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) + } + return ProtocolDescription( accessModifier: accessLevel, name: name, conformances: [streamingProtocol], members: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(docs(for: method)), .function( signature: .serverMethod( name: method.name.generatedLowerCase, diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index 5efa44749..ad7109a9d 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -127,13 +127,16 @@ extension ExtensionDescription { return ExtensionDescription( onType: "GRPCCore.ServiceDescriptor", declarations: [ - .variable( - accessModifier: accessModifier, - isStatic: true, - kind: .let, - left: .identifier(.pattern(propertyName)), - right: .functionCall( - .serviceDescriptor(literalFullyQualifiedService: literalFullyQualifiedService) + .commentable( + .doc("Service descriptor for the \"\(literalFullyQualifiedService)\" service."), + .variable( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern(propertyName)), + right: .functionCall( + .serviceDescriptor(literalFullyQualifiedService: literalFullyQualifiedService) + ) ) ) ] @@ -183,13 +186,22 @@ extension EnumDescription { accessModifier: accessModifier, name: name, members: [ - .typealias(.methodInput(accessModifier: accessModifier, name: inputType)), - .typealias(.methodOutput(accessModifier: accessModifier, name: outputType)), - .variable( - .methodDescriptor( - accessModifier: accessModifier, - literalFullyQualifiedService: literalFullyQualifiedService, - literalMethodName: literalMethod + .commentable( + .doc("Request type for \"\(literalMethod)\"."), + .typealias(.methodInput(accessModifier: accessModifier, name: inputType)) + ), + .commentable( + .doc("Response type for \"\(literalMethod)\"."), + .typealias(.methodOutput(accessModifier: accessModifier, name: outputType)) + ), + .commentable( + .doc("Descriptor for \"\(literalMethod)\"."), + .variable( + .methodDescriptor( + accessModifier: accessModifier, + literalFullyQualifiedService: literalFullyQualifiedService, + literalMethodName: literalMethod + ) ) ), ] @@ -222,14 +234,17 @@ extension EnumDescription { // Add a namespace for each method. let methodNamespaces: [Declaration] = methods.map { method in - return .enum( - .methodNamespace( - accessModifier: accessModifier, - name: method.name.base, - literalMethod: method.name.base, - literalFullyQualifiedService: literalFullyQualifiedService, - inputType: method.inputType, - outputType: method.outputType + return .commentable( + .doc("Namespace for \"\(method.name.base)\" metadata."), + .enum( + .methodNamespace( + accessModifier: accessModifier, + name: method.name.base, + literalMethod: method.name.base, + literalFullyQualifiedService: literalFullyQualifiedService, + inputType: method.inputType, + outputType: method.outputType + ) ) ) } @@ -240,7 +255,12 @@ extension EnumDescription { accessModifier: accessModifier, methodNamespaceNames: methods.map { $0.name.base } ) - description.members.append(.variable(methodDescriptorsArray)) + description.members.append( + .commentable( + .doc("Descriptors for all methods in the \"\(literalFullyQualifiedService)\" service."), + .variable(methodDescriptorsArray) + ) + ) return description } @@ -266,7 +286,12 @@ extension EnumDescription { accessModifier: accessModifier, literalFullyQualifiedService: literalFullyQualifiedService ) - description.members.append(.variable(descriptor)) + description.members.append( + .commentable( + .doc("Service descriptor for the \"\(literalFullyQualifiedService)\" service."), + .variable(descriptor) + ) + ) // enum Method { ... } let methodsNamespace: EnumDescription = .methodsNamespace( @@ -274,7 +299,12 @@ extension EnumDescription { literalFullyQualifiedService: literalFullyQualifiedService, methods: methods ) - description.members.append(.enum(methodsNamespace)) + description.members.append( + .commentable( + .doc("Namespace for method metadata."), + .enum(methodsNamespace) + ) + ) return description } @@ -302,7 +332,14 @@ extension [CodeBlock] { literalFullyQualifiedService: service.fullyQualifiedName, methods: service.methods ) - blocks.append(CodeBlock(item: .declaration(.enum(serviceNamespace)))) + blocks.append( + CodeBlock( + comment: .doc( + "Namespace containing generated types for the \"\(service.fullyQualifiedName)\" service." + ), + item: .declaration(.enum(serviceNamespace)) + ) + ) let descriptorExtension: ExtensionDescription = .serviceDescriptor( accessModifier: accessModifier, diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index d994fb6a0..b77cddbb9 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -91,7 +91,12 @@ struct ClientCodeTranslator { declarations: [ // protocol ClientProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.clientProtocolDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .protocol( .clientProtocol( accessLevel: accessModifier, @@ -103,7 +108,12 @@ struct ClientCodeTranslator { // struct Client: ClientProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.clientDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .struct( .client( accessLevel: accessModifier, @@ -126,7 +136,10 @@ struct ClientCodeTranslator { deserializer: deserializer ) blocks.append( - CodeBlock(item: .declaration(.extension(extensionWithDefaults))) + CodeBlock( + comment: .inline("Helpers providing default arguments to 'ClientProtocol' methods."), + item: .declaration(.extension(extensionWithDefaults)) + ) ) let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods( @@ -135,9 +148,31 @@ struct ClientCodeTranslator { methods: service.methods ) blocks.append( - CodeBlock(item: .declaration(.extension(extensionWithExplodedAPI))) + CodeBlock( + comment: .inline("Helpers providing sugared APIs for 'ClientProtocol' methods."), + item: .declaration(.extension(extensionWithExplodedAPI)) + ) ) return blocks } + + private func clientProtocolDocs(serviceName: String) -> String { + return """ + /// Generated client protocol for the "\(serviceName)" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + """ + } + + private func clientDocs(serviceName: String) -> String { + return """ + /// Generated client for the "\(serviceName)" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + """ + } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift new file mode 100644 index 000000000..5e0e57a11 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift @@ -0,0 +1,64 @@ +/* + * 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. + */ + +package enum Docs { + package static func suffix(_ header: String, withDocs footer: String) -> String { + if footer.isEmpty { + return header + } else { + let docs = """ + /// + \(Self.inlineDocsAsNote(footer)) + """ + return header + "\n" + docs + } + } + + package static func interposeDocs( + _ docs: String, + between header: String, + and footer: String + ) -> String { + let middle: String + + if docs.isEmpty { + middle = """ + /// + """ + } else { + middle = """ + /// + \(Self.inlineDocsAsNote(docs)) + /// + """ + } + + return header + "\n" + middle + "\n" + footer + } + + private static func inlineDocsAsNote(_ docs: String) -> String { + let header = """ + /// > Source IDL Documentation: + /// > + """ + + let body = docs.split(separator: "\n").map { line in + "/// > " + line.dropFirst(4) + }.joined(separator: "\n") + + return header + "\n" + body + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 20f78d3b3..f272b374f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -73,7 +73,12 @@ struct ServerCodeTranslator { declarations: [ // protocol StreamingServiceProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.streamingServiceDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .protocol( .streamingService( accessLevel: accessModifier, @@ -85,7 +90,12 @@ struct ServerCodeTranslator { // protocol ServiceProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.serviceDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .protocol( .service( accessLevel: accessModifier, @@ -110,7 +120,7 @@ struct ServerCodeTranslator { ) blocks.append( CodeBlock( - comment: .doc("Conformance to `GRPCCore.RegistrableRPCService`."), + comment: .inline("Default implementation of 'registerMethods(with:)'."), item: .declaration(.extension(registerExtension)) ) ) @@ -122,8 +132,42 @@ struct ServerCodeTranslator { on: "\(service.namespacedGeneratedName).ServiceProtocol", methods: service.methods ) - blocks.append(.declaration(.extension(streamingServiceDefaultImplExtension))) + blocks.append( + CodeBlock( + comment: .inline( + "Default implementation of streaming methods from 'StreamingServiceProtocol'." + ), + item: .declaration(.extension(streamingServiceDefaultImplExtension)) + ) + ) return blocks } + + private func streamingServiceDocs(serviceName: String) -> String { + return """ + /// Streaming variant of the service protocol for the "\(serviceName)" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + """ + } + + private func serviceDocs(serviceName: String) -> String { + return """ + /// Service protocol for the "\(serviceName)" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + """ + } } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift index a4abc0ee3..50d6f7ada 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -114,7 +114,21 @@ extension StructuedSwiftTests { let expected = """ \(access) protocol Foo_ClientProtocol: Sendable { - /// Some docs + /// Call the "Bar" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - request: A request containing a single `BarInput` message. + /// - serializer: A serializer for `BarInput` messages. + /// - deserializer: A deserializer for `BarOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. func bar( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -186,6 +200,15 @@ extension StructuedSwiftTests { let expected = """ extension Foo_ClientProtocol { + /// Call the "Bar" method. + /// + /// - Parameters: + /// - request: A request containing a single `BarInput` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func bar( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, @@ -330,7 +353,20 @@ extension StructuedSwiftTests { let expected = """ extension Foo_ClientProtocol { - /// Some docs + /// Call the "Bar" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func bar( _ message: Input, metadata: GRPCCore.Metadata = [:], @@ -456,11 +492,29 @@ extension StructuedSwiftTests { \(access) struct FooClient: Foo_ClientProtocol { private let client: GRPCCore.GRPCClient + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. \(access) init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - /// Unary docs + /// Call the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs + /// + /// - Parameters: + /// - request: A request containing a single `Input` message. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func unary( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -480,7 +534,21 @@ extension StructuedSwiftTests { ) } - /// ClientStreaming docs + /// Call the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > ClientStreaming docs + /// + /// - Parameters: + /// - request: A streaming request producing `Input` messages. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func clientStreaming( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -500,7 +568,21 @@ extension StructuedSwiftTests { ) } - /// ServerStreaming docs + /// Call the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > ServerStreaming docs + /// + /// - Parameters: + /// - request: A request containing a single `Input` message. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func serverStreaming( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -518,7 +600,21 @@ extension StructuedSwiftTests { ) } - /// BidiStreaming docs + /// Call the "BidiStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > BidiStreaming docs + /// + /// - Parameters: + /// - request: A streaming request producing `Input` messages. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func bidiStreaming( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index 6169098e8..8107b159c 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -81,6 +81,7 @@ extension StructuedSwiftTests { let expected = """ extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "echo.EchoService" service. \(access) static let foo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.EchoService") } """ @@ -120,8 +121,11 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Foo { + /// Request type for "Foo". \(access) typealias Input = FooInput + /// Response type for "Foo". \(access) typealias Output = FooOutput + /// Descriptor for "Foo". \(access) static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"), method: "Foo" @@ -150,14 +154,19 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Method { + /// Namespace for "Foo" metadata. \(access) enum Foo { + /// Request type for "Foo". \(access) typealias Input = FooInput + /// Response type for "Foo". \(access) typealias Output = FooOutput + /// Descriptor for "Foo". \(access) static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"), method: "Foo" ) } + /// Descriptors for all methods in the "bar.Bar" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ Foo.descriptor ] @@ -176,6 +185,7 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Method { + /// Descriptors for all methods in the "bar.Bar" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] } """ @@ -202,16 +212,23 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Foo { + /// Service descriptor for the "Foo" service. \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo") + /// Namespace for method metadata. \(access) enum Method { + /// Namespace for "Bar" metadata. \(access) enum Bar { + /// Request type for "Bar". \(access) typealias Input = BarInput + /// Response type for "Bar". \(access) typealias Output = BarOutput + /// Descriptor for "Bar". \(access) static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo"), method: "Bar" ) } + /// Descriptors for all methods in the "Foo" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ Bar.descriptor ] @@ -232,8 +249,11 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Foo { + /// Service descriptor for the "Foo" service. \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo") + /// Namespace for method metadata. \(access) enum Method { + /// Descriptors for all methods in the "Foo" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] } } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index 78dbac42d..2edde05a3 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -91,7 +91,19 @@ extension StructuedSwiftTests { let expected = """ \(access) protocol FooService: GRPCCore.RegistrableRPCService { - /// Some docs + /// Handle the "Foo" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - request: A streaming request of `FooInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `FooOutput` messages. func foo( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext @@ -122,7 +134,19 @@ extension StructuedSwiftTests { let expected = """ \(access) protocol FooService: FooService_StreamingServiceProtocol { - /// Some docs + /// Handle the "Foo" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - request: A request containing a single `FooInput` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `FooOutput` message. func foo( request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 3516f4d67..8c40eb7f4 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -40,9 +40,30 @@ struct ClientCodeTranslatorSnippetBasedTests { let expectedSwift = """ extension NamespaceA_ServiceA { - /// Documentation for ServiceA + /// Generated client protocol for the "namespaceA.ServiceA" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public protocol ClientProtocol: Sendable { - /// Documentation for MethodA + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - request: A request containing a single `NamespaceA_ServiceARequest` message. + /// - serializer: A serializer for `NamespaceA_ServiceARequest` messages. + /// - deserializer: A deserializer for `NamespaceA_ServiceAResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -52,15 +73,41 @@ struct ClientCodeTranslatorSnippetBasedTests { ) async throws -> Result where Result: Sendable } - /// Documentation for ServiceA + /// Generated client for the "namespaceA.ServiceA" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public struct Client: ClientProtocol { private let client: GRPCCore.GRPCClient + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - /// Documentation for MethodA + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - request: A request containing a single `NamespaceA_ServiceARequest` message. + /// - serializer: A serializer for `NamespaceA_ServiceARequest` messages. + /// - deserializer: A deserializer for `NamespaceA_ServiceAResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. public func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -81,7 +128,21 @@ struct ClientCodeTranslatorSnippetBasedTests { } } } + // Helpers providing default arguments to 'ClientProtocol' methods. extension NamespaceA_ServiceA.ClientProtocol { + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - request: A request containing a single `NamespaceA_ServiceARequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. public func methodA( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, @@ -98,8 +159,22 @@ struct ClientCodeTranslatorSnippetBasedTests { ) } } + // Helpers providing sugared APIs for 'ClientProtocol' methods. extension NamespaceA_ServiceA.ClientProtocol { - /// Documentation for MethodA + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. public func methodA( _ message: NamespaceA_ServiceARequest, metadata: GRPCCore.Metadata = [:], diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift new file mode 100644 index 000000000..69426663b --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift @@ -0,0 +1,101 @@ +/* + * 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 GRPCCodeGen +import Testing + +@Suite("Docs tests") +struct DocsTests { + @Test("Suffix with additional docs") + func suffixWithAdditional() { + let foo = """ + /// Foo + """ + + let additional = """ + /// Some additional pre-formatted docs + /// split over multiple lines. + """ + + let expected = """ + /// Foo + /// + /// > Source IDL Documentation: + /// > + /// > Some additional pre-formatted docs + /// > split over multiple lines. + """ + #expect(Docs.suffix(foo, withDocs: additional) == expected) + } + + @Test("Suffix with empty additional docs") + func suffixWithEmptyAdditional() { + let foo = """ + /// Foo + """ + + let additional = "" + #expect(Docs.suffix(foo, withDocs: additional) == foo) + } + + @Test("Interpose additional docs") + func interposeDocs() { + let header = """ + /// Header + """ + + let footer = """ + /// Footer + """ + + let additionalDocs = """ + /// Additional docs + /// On multiple lines + """ + + let expected = """ + /// Header + /// + /// > Source IDL Documentation: + /// > + /// > Additional docs + /// > On multiple lines + /// + /// Footer + """ + + #expect(Docs.interposeDocs(additionalDocs, between: header, and: footer) == expected) + } + + @Test("Interpose empty additional docs") + func interposeEmpty() { + let header = """ + /// Header + """ + + let footer = """ + /// Footer + """ + + let expected = """ + /// Header + /// + /// Footer + """ + + #expect(Docs.interposeDocs("", between: header, and: footer) == expected) + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 7ed4727bb..778aed337 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -214,32 +214,61 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // MARK: - namespaceA.ServiceA + /// Namespace containing generated types for the "namespaceA.ServiceA" service. public enum NamespaceA_ServiceA { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") + /// Namespace for method metadata. public enum Method { + /// Descriptors for all methods in the "namespaceA.ServiceA" service. public static let descriptors: [GRPCCore.MethodDescriptor] = [] } } extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") } // MARK: namespaceA.ServiceA (server) extension NamespaceA_ServiceA { - /// Documentation for AService + /// Streaming variant of the service protocol for the "namespaceA.ServiceA" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for AService public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} - /// Documentation for AService + /// Service protocol for the "namespaceA.ServiceA" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for AService public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} } - /// Conformance to `GRPCCore.RegistrableRPCService`. + // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } + // Default implementation of streaming methods from 'StreamingServiceProtocol'. extension NamespaceA_ServiceA.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 7cb57cc18..099ff3934 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -48,25 +48,72 @@ final class ServerCodeTranslatorSnippetBasedTests { let expectedSwift = """ extension NamespaceA_ServiceA { - /// Documentation for ServiceA + /// Streaming variant of the service protocol for the "namespaceA.AlongNameForServiceA" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for unaryMethod + /// Handle the "UnaryMethod" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for unaryMethod + /// + /// - Parameters: + /// - request: A streaming request of `NamespaceA_ServiceARequest` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `NamespaceA_ServiceAResponse` messages. func unary( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext ) async throws -> GRPCCore.StreamingServerResponse } - /// Documentation for ServiceA + /// Service protocol for the "namespaceA.AlongNameForServiceA" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { - /// Documentation for unaryMethod + /// Handle the "UnaryMethod" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for unaryMethod + /// + /// - Parameters: + /// - request: A request containing a single `NamespaceA_ServiceARequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `NamespaceA_ServiceAResponse` message. func unary( request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse } } - /// Conformance to `GRPCCore.RegistrableRPCService`. + // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( @@ -82,6 +129,7 @@ final class ServerCodeTranslatorSnippetBasedTests { ) } } + // Default implementation of streaming methods from 'StreamingServiceProtocol'. extension NamespaceA_ServiceA.ServiceProtocol { public func unary( request: GRPCCore.StreamingServerRequest, diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 504dbcec3..8713b784a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -42,23 +42,32 @@ struct TypealiasTranslatorSnippetBasedTests { ) let expectedSwift = """ + /// Namespace containing generated types for the "namespaceA.ServiceA" service. public enum NamespaceA_ServiceA { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") + /// Namespace for method metadata. public enum Method { + /// Namespace for "MethodA" metadata. public enum MethodA { + /// Request type for "MethodA". public typealias Input = NamespaceA_ServiceARequest + /// Response type for "MethodA". public typealias Output = NamespaceA_ServiceAResponse + /// Descriptor for "MethodA". public static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA"), method: "MethodA" ) } + /// Descriptors for all methods in the "namespaceA.ServiceA" service. public static let descriptors: [GRPCCore.MethodDescriptor] = [ MethodA.descriptor ] } } extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") } """ From e2629bc8ef37ca78db0133e89e6cbd7a86453728 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 28 Nov 2024 16:23:39 +0000 Subject: [PATCH 512/580] Add a 'simple' service protocol (#2134) Motivation: The generated service protocol requires users to deal with request/response objects. Many users won't care about metadata so can benefit from an easier-to-use API. Modifications: - Add a "simple" server protocol which doesn't make user of request/response objects Result: Easier to implement a service --- .../Internal/Renderer/TextBasedRenderer.swift | 9 +- .../Internal/StructuredSwift+Server.swift | 286 ++++++++++++++++++ .../Internal/StructuredSwift+Types.swift | 8 + .../StructuredSwiftRepresentation.swift | 14 +- .../Internal/Translator/Docs.swift | 2 +- .../Translator/ServerCodeTranslator.swift | 41 +++ .../StructuredSwift+ServerTests.swift | 94 ++++++ ...uredSwiftTranslatorSnippetBasedTests.swift | 15 + ...erverCodeTranslatorSnippetBasedTests.swift | 44 +++ 9 files changed, 509 insertions(+), 4 deletions(-) diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 3d25ee7f0..888d91fd3 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -613,7 +613,14 @@ struct TextBasedRenderer: RendererProtocol { writer.nextLineAppendsToLastLine() writer.writeLine("<") writer.nextLineAppendsToLastLine() - renderExistingTypeDescription(wrapped) + for (wrap, isLast) in wrapped.enumeratedWithLastMarker() { + renderExistingTypeDescription(wrap) + writer.nextLineAppendsToLastLine() + if !isLast { + writer.writeLine(", ") + writer.nextLineAppendsToLastLine() + } + } writer.nextLineAppendsToLastLine() writer.writeLine(">") case .optional(let existingTypeDescription): diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index 44093da6a..95643d4d0 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -473,3 +473,289 @@ extension ExtensionDescription { ) } } + +extension FunctionSignatureDescription { + /// ``` + /// func ( + /// request: , + /// context: GRPCCore.ServerContext, + /// ) async throws -> + /// ``` + /// + /// ``` + /// func ( + /// request: GRPCCore.RPCAsyncSequence, + /// response: GRPCCore.RPCAsyncWriter + /// context: GRPCCore.ServerContext, + /// ) async throws + /// ``` + static func simpleServerMethod( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + var parameters: [ParameterDescription] = [ + ParameterDescription( + label: "request", + type: streamingInput ? .rpcAsyncSequence(forType: input) : .member(input) + ) + ] + + if streamingOutput { + parameters.append(ParameterDescription(label: "response", type: .rpcWriter(forType: output))) + } + + parameters.append(ParameterDescription(label: "context", type: .serverContext)) + + return FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name), + parameters: parameters, + keywords: [.async, .throws], + returnType: streamingOutput ? nil : .identifier(.pattern(output)) + ) + } +} + +extension ProtocolDescription { + /// ``` + /// protocol SimpleServiceProtocol: { + /// ... + /// } + /// ``` + static func simpleServiceProtocol( + accessModifier: AccessModifier? = nil, + name: String, + serviceProtocol: String, + methods: [MethodDescriptor] + ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.normalizedBase)" method. + """ + + let requestText = + method.isInputStreaming + ? "A stream of `\(method.inputType)` messages." + : "A `\(method.inputType)` message." + + var parameters = """ + /// - Parameters: + /// - request: \(requestText) + """ + + if method.isOutputStreaming { + parameters += "\n" + parameters += """ + /// - response: A response stream of `\(method.outputType)` messages. + """ + } + + parameters += "\n" + parameters += """ + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + """ + + if !method.isOutputStreaming { + parameters += "\n" + parameters += """ + /// - Returns: A `\(method.outputType)` to respond with. + """ + } + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) + } + + return ProtocolDescription( + accessModifier: accessModifier, + name: name, + conformances: [serviceProtocol], + members: methods.map { method in + .commentable( + .preFormatted(docs(for: method)), + .function( + signature: .simpleServerMethod( + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + ) + } + ) + } +} + +extension FunctionCallDescription { + /// ``` + /// try await self.( + /// request: request.message, + /// response: writer, + /// context: context + /// ) + /// ``` + static func serviceMethodCallingSimpleMethod( + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + var arguments: [FunctionArgumentDescription] = [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request").dot(streamingInput ? "messages" : "message") + ) + ] + + if streamingOutput { + arguments.append( + FunctionArgumentDescription( + label: "response", + expression: .identifierPattern("writer") + ) + ) + } + + arguments.append( + FunctionArgumentDescription( + label: "context", + expression: .identifierPattern("context") + ) + ) + + return FunctionCallDescription( + calledExpression: .try(.await(.identifierPattern("self").dot(name))), + arguments: arguments + ) + } +} + +extension FunctionDescription { + /// ``` + /// func ( + /// request: GRPCCore.ServerRequest, + /// context: GRPCCore.ServerContext + /// ) async throws -> GRPCCore.ServerResponse { + /// return GRPCCore.ServerResponse( + /// message: try await self.( + /// request: request.message, + /// context: context + /// ) + /// metadata: [:] + /// ) + /// } + /// ``` + static func serviceProtocolDefaultImplementation( + accessModifier: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + func makeUnaryOutputArguments() -> [FunctionArgumentDescription] { + return [ + FunctionArgumentDescription( + label: "message", + expression: .functionCall( + .serviceMethodCallingSimpleMethod( + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ) + ) + ), + FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))), + ] + } + + func makeStreamingOutputArguments() -> [FunctionArgumentDescription] { + return [ + FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))), + FunctionArgumentDescription( + label: "producer", + expression: .closureInvocation( + argumentNames: ["writer"], + body: [ + .expression( + .functionCall( + .serviceMethodCallingSimpleMethod( + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ) + ) + ), + .expression(.return(.literal(.dictionary([])))), + ] + ) + ), + ] + } + + return FunctionDescription( + signature: .serverMethod( + accessLevel: accessModifier, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ), + body: [ + .expression( + .functionCall( + calledExpression: .return( + .identifierType( + .serverResponse(forType: output, streaming: streamingOutput) + ) + ), + arguments: streamingOutput ? makeStreamingOutputArguments() : makeUnaryOutputArguments() + ) + ) + ] + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension ServiceProtocol { + /// ... + /// } + /// ``` + static func serviceProtocolDefaultImplementation( + accessModifier: AccessModifier? = nil, + on extensionName: String, + methods: [MethodDescriptor] + ) -> Self { + ExtensionDescription( + onType: extensionName, + declarations: methods.map { method in + .function( + .serviceProtocolDefaultImplementation( + accessModifier: accessModifier, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + } + ) + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift index ac3499037..4706e9888 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift @@ -70,6 +70,14 @@ extension ExistingTypeDescription { .generic(wrapper: .grpcCore("RPCWriter"), wrapped: .member(type)) } + package static func rpcAsyncSequence(forType type: String) -> Self { + .generic( + wrapper: .grpcCore("RPCAsyncSequence"), + wrapped: .member(type), + .any(.member(["Swift", "Error"])) + ) + } + package static let callOptions: Self = .grpcCore("CallOptions") package static let metadata: Self = .grpcCore("Metadata") package static let grpcClient: Self = .grpcCore("GRPCClient") diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index ca44b7f79..c39be20d6 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -453,10 +453,10 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable { /// For example, `Foo?`. case optional(ExistingTypeDescription) - /// A wrapper type generic over a wrapped type. + /// A wrapper type generic over a list of wrapped types. /// /// For example, `Wrapper`. - case generic(wrapper: ExistingTypeDescription, wrapped: ExistingTypeDescription) + case generic(wrapper: ExistingTypeDescription, wrapped: [ExistingTypeDescription]) /// A type reference represented by the components. /// @@ -483,6 +483,16 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable { /// /// For example: `(String) async throws -> Int`. case closure(ClosureSignatureDescription) + + /// A wrapper type generic over a list of wrapped types. + /// + /// For example, `Wrapper`. + static func generic( + wrapper: ExistingTypeDescription, + wrapped: ExistingTypeDescription... + ) -> Self { + return .generic(wrapper: wrapper, wrapped: Array(wrapped)) + } } /// A description of a typealias declaration. diff --git a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift index 5e0e57a11..1d4b1e1fd 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift @@ -56,7 +56,7 @@ package enum Docs { """ let body = docs.split(separator: "\n").map { line in - "/// > " + line.dropFirst(4) + "/// > " + line.dropFirst(4).trimmingCharacters(in: .whitespaces) }.joined(separator: "\n") return header + "\n" + body diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index f272b374f..a2e91a83e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -105,6 +105,24 @@ struct ServerCodeTranslator { ) ) ), + + // protocol SimpleServiceProtocol { ... } + .commentable( + .preFormatted( + Docs.suffix( + self.simpleServiceDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), + .protocol( + .simpleServiceProtocol( + accessModifier: accessModifier, + name: "SimpleServiceProtocol", + serviceProtocol: "\(service.namespacedGeneratedName).ServiceProtocol", + methods: service.methods + ) + ) + ), ] ) blocks.append(.declaration(.extension(`extension`))) @@ -141,6 +159,19 @@ struct ServerCodeTranslator { ) ) + // extension _SimpleServiceProtocol { ... } + let serviceDefaultImplExtension: ExtensionDescription = .serviceProtocolDefaultImplementation( + accessModifier: accessModifier, + on: "\(service.namespacedGeneratedName).SimpleServiceProtocol", + methods: service.methods + ) + blocks.append( + CodeBlock( + comment: .inline("Default implementation of methods from 'ServiceProtocol'."), + item: .declaration(.extension(serviceDefaultImplExtension)) + ) + ) + return blocks } @@ -170,4 +201,14 @@ struct ServerCodeTranslator { /// use ``StreamingServiceProtocol``. """ } + + private func simpleServiceDocs(serviceName: String) -> String { + return """ + /// Simple service protocol for the "\(serviceName)" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + """ + } } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index 2edde05a3..45567bafd 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -356,5 +356,99 @@ extension StructuedSwiftTests { #expect(render(.extension(decl)) == expected) } + + @Test( + "func (request:response:context:) (simple)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func simpleServerMethod(access: AccessModifier, kind: RPCKind) { + let decl: FunctionSignatureDescription = .simpleServerMethod( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput + ) + + let expected: String + switch kind { + case .unary: + expected = """ + \(access) func foo( + request: FooInput, + context: GRPCCore.ServerContext + ) async throws -> FooOutput + """ + + case .clientStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> FooOutput + """ + + case .serverStreaming: + expected = """ + \(access) func foo( + request: FooInput, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + """ + + case .bidirectionalStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.RPCAsyncSequence, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + """ + } + + #expect(render(.function(signature: decl)) == expected) + } + + @Test("protocol SimpleServiceProtocol { ... }", arguments: AccessModifier.allCases) + func simpleServiceProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .simpleServiceProtocol( + accessModifier: access, + name: "SimpleServiceProtocol", + serviceProtocol: "ServiceProtocol", + methods: [ + .init( + documentation: "", + name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ) + ] + ) + + let expected = """ + \(access) protocol SimpleServiceProtocol: ServiceProtocol { + /// Handle the "Foo" method. + /// + /// - Parameters: + /// - request: A `Input` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Output` to respond with. + func foo( + request: Input, + context: GRPCCore.ServerContext + ) async throws -> Output + } + """ + + #expect(render(.protocol(decl)) == expected) + } } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 778aed337..a64fe0451 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -261,6 +261,17 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { /// > /// > Documentation for AService public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} + + /// Simple service protocol for the "namespaceA.ServiceA" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for AService + public protocol SimpleServiceProtocol: NamespaceA_ServiceA.ServiceProtocol {} } // Default implementation of 'registerMethods(with:)'. @@ -271,6 +282,10 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // Default implementation of streaming methods from 'StreamingServiceProtocol'. extension NamespaceA_ServiceA.ServiceProtocol { } + + // Default implementation of methods from 'ServiceProtocol'. + extension NamespaceA_ServiceA.SimpleServiceProtocol { + } """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest( diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 099ff3934..1b48ddadb 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -112,6 +112,35 @@ final class ServerCodeTranslatorSnippetBasedTests { context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse } + + /// Simple service protocol for the "namespaceA.AlongNameForServiceA" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA + public protocol SimpleServiceProtocol: NamespaceA_ServiceA.ServiceProtocol { + /// Handle the "UnaryMethod" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for unaryMethod + /// + /// - Parameters: + /// - request: A `NamespaceA_ServiceARequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `NamespaceA_ServiceAResponse` to respond with. + func unary( + request: NamespaceA_ServiceARequest, + context: GRPCCore.ServerContext + ) async throws -> NamespaceA_ServiceAResponse + } } // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { @@ -142,6 +171,21 @@ final class ServerCodeTranslatorSnippetBasedTests { return GRPCCore.StreamingServerResponse(single: response) } } + // Default implementation of methods from 'ServiceProtocol'. + extension NamespaceA_ServiceA.SimpleServiceProtocol { + public func unary( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.unary( + request: request.message, + context: context + ), + metadata: [:] + ) + } + } """ let rendered = self.render(accessLevel: .public, service: service) From 0fc49565a314d621394094f122659eb1f17b3dd6 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 3 Dec 2024 13:35:34 +0000 Subject: [PATCH 513/580] Add remote peer info to the server context (#2136) Motivation: It's often useful to know the identity of the remote peer when handling RPCs. Modifications: - Add a 'peer' to the server context - Implement this for the in-process transport - Make some in-process inits `package`, these should never have been `public` Result: Server RPCs have some idea what the address of remote peer is --- .../GRPCCore/Call/Server/ServerContext.swift | 21 ++++++- .../InProcessTransport+Client.swift | 2 +- .../InProcessTransport+Server.swift | 10 +++- .../InProcessTransport.swift | 3 +- Sources/GRPCInProcessTransport/Syscalls.swift | 38 ++++++++++++ .../ClientRPCExecutorTestHarness.swift | 4 +- .../ServerRPCExecutorTestHarness.swift | 1 + Tests/GRPCCoreTests/GRPCServerTests.swift | 5 +- .../InProcessClientTransportTests.swift | 10 ++-- .../InProcessServerTransportTests.swift | 4 +- .../InProcessTransportTests.swift | 59 +++++++++++++++++++ 11 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 Sources/GRPCInProcessTransport/Syscalls.swift diff --git a/Sources/GRPCCore/Call/Server/ServerContext.swift b/Sources/GRPCCore/Call/Server/ServerContext.swift index 4d8613f93..504e9563a 100644 --- a/Sources/GRPCCore/Call/Server/ServerContext.swift +++ b/Sources/GRPCCore/Call/Server/ServerContext.swift @@ -19,6 +19,19 @@ public struct ServerContext: Sendable { /// A description of the method being called. public var descriptor: MethodDescriptor + /// A description of the remote peer. + /// + /// The format of the description should follow the pattern ":